Skip to content

feat: GitHub Command Center MVP (v0.1.0.0) - #1

Merged
awjdean merged 133 commits into
mainfrom
feat/mvp
Mar 31, 2026
Merged

awjdean merged 133 commits into
mainfrom
feat/mvp

Conversation

@awjdean

@awjdean awjdean commented Mar 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

Complete MVP of GitHub Command Center — a macOS 14+ menu bar app that monitors your GitHub PRs and surfaces what needs your attention.

Core features:

  • PR triage dashboard split into "Needs Your Action", "Waiting on Others", and "Your Drafts"
  • Smart attention-count badge in the menu bar
  • Status dots with click popovers for CI, review, and merge status
  • Real-time notifications for CI failures, review requests, approvals, and merge conflicts
  • Adaptive polling (60s–5min) with ETag caching and rate limit awareness
  • Token validation with scope checking and fine-grained PAT support
  • Environment token bootstrapper for development workflows
  • Inline settings panel with collapsible repo list

Infrastructure:

  • GitHub Actions CI (build + test + lint) and release workflow (tagged builds → .zip)
  • Mise task runner for build, test, check-style, and sync commands
  • SwiftLint + swift-format style enforcement
  • XcodeGen project generation from project.yml

Quality:

  • 206 tests across 12 suites covering models, services, and view presentation logic
  • Thread-safe mock infrastructure (MockURLProtocol, MockGitHubDataSource)
  • @mainactor isolation for AppState and PollingEngine, actor isolation for GitHubRESTClient

Test Coverage

AI-assessed coverage: 118/136 code paths tested (87%)

  • Models: ~95% coverage (PRState triage, urgency, AppState health/panel states)
  • Services: ~85% coverage (REST client, polling, notifications, keychain, token bootstrapper)
  • Views: Presentation logic tested (StatusDotTooltip, PRRowPresentation, SettingsValidation, PanelMode)
  • Tests: 16 files, 206 tests

Pre-Landing Review

2 informational issues auto-fixed:

  • ci.yml: pinned jdx/mise-action@v3 → @v4.0.1 (matches release.yml)
  • ci.yml: pinned maxim-lobanov/setup-xcode@v1 → @v1.7.0 (matches release.yml)

3 informational notes (not blocking):

  • release.yml softprops/action-gh-release@v2 unpinned (consider SHA pin)
  • AppError.noToken stored in panel.error is functionally dead (UI uses authStatus)
  • deduplicatedCommitStatuses relies on API reverse-chronological order (correct assumption)

Adversarial Review

Codex adversarial review found 2 medium-impact items, no blockers:

  • fetchAllPRStates uses withThrowingTaskGroup — one transient error fails the whole poll (recovers on next cycle with backoff)
  • EnvironmentTokenBootstrapper walks ancestor dirs to / for .env files (only on first run with no keychain token)

Scope Drift

Scope Check: CLEAN — all changes directly serve the MVP feature set.

TODOS

No TODO items completed — all 3 items are V1.1+ / V2 post-MVP work.

Test plan

  • All Swift tests pass (206 tests, 12 suites, 0 failures)
  • Pre-commit hooks pass (trailing whitespace, merge conflict check)
  • CI version pins aligned between ci.yml and release.yml

🤖 Generated with Claude Code

awjdean and others added 30 commits March 30, 2026 15:25
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fecycle

AppState gains triage-sorted PR lists, panel content state machine, health
status, polling lifecycle management, staleness tracking, and session reset.
PRState gains urgencyScore, displayRole, and triage category logic.
AppError gains Equatable for test assertions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…andling

Full implementation: search-based PR discovery, concurrent per-PR detail
fetching (reviews, check runs, commit statuses), mergeable_state retry,
duplicate commit status deduplication, ETag/304 response caching, and
independent core/search rate limit bucket tracking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dynamic poll interval scales with PR count (60/120/300s) to stay within
rate limits. Auth and no-token errors stop polling immediately. Rate limit
errors back off until reset. Network errors use exponential backoff capped
at 60s. Username is validated once then reused across poll cycles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Detects CI passing→failing, changes requested, approved, merge conflicts,
review requested, and PR closed transitions between poll cycles. Skips
notifications on first poll (no baseline). Adds notificationHandler
injection point so tests can capture fired notifications without touching
UNUserNotificationCenter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PRListView: triage sections (Needs Your Action / Waiting on Others),
recently-closed transient section, shimmer skeleton on first load,
stale/rate-limit warning bars, auth error banners, empty/setup/auth-failed
states, footer with relative timestamp, refresh shortcut, and launch-at-login
toggle. SettingsView: token save/validate flow with live border feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds GitHubCommandCenterTests bundle.unit-test target with correct
BUNDLE_LOADER/TEST_HOST wiring so tests run in the app's process and
can access @testable imports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PRStateTests: triage, urgency, sort order, display role, merge status.
NotificationServiceTests: all transition cases, no-repeat guards, disappeared
PRs, first-poll silence. PollingEngineTests: intervals, auth/network/rate-limit
error handling, staleness, session reset, panel state. GitHubRESTClientTests:
HTTP status codes, mergeable_state mapping, CI/review/commit status parsing,
pagination, ETag/304 caching. KeychainServiceTests: CRUD, overwrite, unicode.
Helpers: MockGitHubDataSource, MockURLProtocol, PRState.fixture().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the build-only fallback that swallowed test failures with
"No tests yet — build-only CI passing".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add early-exit guard when app bundle path is empty in release workflow.
Consolidate xcuserdata/xcuserstate patterns and remove stale SPM entries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add project summary and CLI build/test commands to CLAUDE.md.
Add TESTING_GUIDE.md with step-by-step instructions for building,
running, and manually verifying the app.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
changesRequested now only contributes to urgency score on PRs you
authored — requesting reviewers shouldn't boost urgency for others.
Extract PRState.compareForNeedsAction as a static method for reuse.
Add AppError.incompleteSearchResults for GitHub search API edge case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Open up init with configurable service/account names so tests can
use isolated keychain items without touching the real credential.
Add distinct decodingFailed error case; make KeychainError Equatable;
throw instead of silently returning nil on data corruption.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a 304 arrives with no cached response, retry once without the
ETag rather than throwing a network error.
Throw incompleteSearchResults when GitHub signals the search index
was not fully consulted.
Track the search rate-limit reset date independently from the core
bucket so backoff is accurate for both buckets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Only fire "PR closed" notifications for PRs you authored — no need
to notify about others' PRs being closed.
Add PollingEngine.reset() to clear previousPRs and consecutiveFailures
without stopping the task. Make recentlyClosed clear task cancellable
and inject the delay for test control. Propagate Keychain errors as
authError rather than swallowing them. Handle incompleteSearchResults
without resetting the consecutive-failure counter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deduplicate the state-clearing logic into a private helper used by
both clearSessionStateForNewSession and resetPolling. Also call
pollingEngine?.reset() before stopping so previousPRs and failure
counters are cleared on token changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace custom hover tooltip in StatusDotView with native .help()
modifier — simpler and works correctly in menu bar panels.
Add unvalidated token state in SettingsView so editing a saved token
clears the valid indicator immediately; propagate Keychain load errors
to the UI; add @mainactor to saveToken Task.
Sync launchAtLogin toggle state on panel appear rather than only at
init. Add #Preview macros to MenuBarIconView.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace XCTest with Swift Testing (@Suite/@Test/#expect/Issue.record)
across all test files. Each test now creates its own Harness to avoid
shared state between cases.
New cases: 304-without-cached-response retries without ETag;
incompleteSearchResults is surfaced through validateTokenForAppAccess;
incomplete_results field added to search stub payloads.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces mise.toml with build/test/check/fix tasks, .swift-format and
.swiftlint.yml for enforced code style, and hk.pkl for pre-commit hooks.
Adds .mise.local.toml to .gitignore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces inline xcodebuild invocations with mise-action setup and
`mise run check` / `mise run build-release`, reducing duplication.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces raw xcodebuild commands with mise task equivalents in CLAUDE.md.
Rewrites TESTING_GUIDE.md as a development setup guide covering mise tasks,
swift-format/swiftlint configuration, hk pre-commit hooks, and manual testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Initial formatting pass using the newly added style tools. Changes include
trailing commas in collections, switch-case spacing, multi-line wrapping,
import ordering, and short identifier renames (q→query, s→status, u→authenticatedUsername).
Also removes force-unwraps in SettingsView and PRStateFixtures per swiftlint rules.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add PollingControlling protocol so tests can stub the engine without
touching Keychain. AppState gains makePollingEngine and
requestNotificationPermission injection points. PollingEngine adds
PollOutcome enum and handleAuthFailure helper to reduce duplication.
GitHubCommandCenterApp gets @mainactor and an injectable init so
AppStateTests can wire up the full startup path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a network or non-auth error occurs before any PRs are loaded,
show a dedicated error screen with a wifi icon, error description, and
a Retry button that calls forceRefresh(). Refactors the duplicate
authErrorBanner/noTokenBanner into a shared actionBanner helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduce a private RequestError type to layer HTTP errors before
mapping to AppError, letting fetchCheckRuns swallow 403/404 and fall
back to commit statuses instead of crashing the poll cycle. Fix CI
priority so a failing check beats a co-existing pending one. Fix
review status so DISMISSED is kept (clearing prior approvals), not
silently dropped. Remove unused rate-limit tracking state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
wasFailing replaces the old wasPassingOrNone check so a pending→failing
transition also triggers the notification. Extract
notifyChangesRequestedIfNeeded and notifyApprovalIfNeeded helpers to
reduce duplication.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move Harness and stubFullPRFlow into a shared GitHubRESTClientTestSupport
file. Add AppStateTests covering injection startup and loadError state.
New REST client tests: 403/404 check-run fallback, failed-beats-pending
CI priority, DISMISSED review clearing approval. New notification test:
pending→failing fires. New polling test: networkError returns
continueImmediately. Replace fixed Task.sleep in reset test with a
waitUntil helper. Harden Keychain setup helpers to throw on failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Checks: Read to the required fine-grained PAT scopes in both
SettingsView and the testing guide. Fix absolute file paths to relative
links in the testing guide. Add a V1.1 TODO for team-based review
request support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
awjdean and others added 27 commits March 31, 2026 15:13
…ild errors

AppError: extract RateLimitContext struct so the reset date and human-
readable description are computed once at throw time, removing the shared
NSLock on the formatter.

GitHubRESTClient: replace the array-based LRU touch (O(n)) with a
doubly-linked list (O(1)). Add a dedicated checkRunsMaxPageLimit
constant and log the status code when fetchCheckRuns falls back early.
Remove the silent-drop pattern in concurrent PR state builds — errors now
propagate so callers can surface them. Add a test confirming that a 500
on one PR throws rather than silently dropping the PR.

PollingEngine: update RateLimitContext call sites; move
clearRecentlyClosedTaskID assignment before the Task is created to close
a narrow race where the task could complete before the ID was stored.

Tests: update pagination test to verify the final non-full page stops
further requests; fix MockURLProtocol to skip the Content-Type header
on 304 responses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move "Your Draft PRs" section after "Waiting on Others" in the triage
categories doc to match the visual order in the panel. Rephrase two bug
descriptions in improvements-to-make.md for clarity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…o PR trigger

- Pin CI and release workflows to Xcode 26.4 (was 16.4)
- Add `mise run fix` step before `mise run check` and verify neither produces dirty changes
- Switch CI trigger from branch pushes to pull_request only, with concurrency cancellation
- Add `permissions: contents: read` and skip draft PRs
- Update project.yml and TESTING_GUIDE.md to match new Xcode version

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Normalise the version string to standard three-part semver.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the top-level Spacing enum and Font extension, nesting them as
Theme.Spacing and Theme.Fonts respectively. All view call sites updated.

Co-located fixes included where changes touched the same lines:
- MenuBarIconView: set tick=true on task start so animation begins immediately
- StatusDotView: replace fixed padding with minWidth/minHeight frame for
  accessibility tap target
- SettingsView: save keychain token before computing success outcome to
  avoid a cancelled-task gap

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GitHub's search API returns at most 1,000 items. Previously the client
would silently page through all results; now it stops at 1,000 and
attaches a warning message to the fetch result.

- Add PRFetchResult (wraps [PRState] + optional warningMessage)
- Update GitHubDataSource protocol to return PRFetchResult
- GitHubRESTClient: stop paging once 1,000 items collected, set
  searchCapWarningMessage when totalCount > 1,000
- Fix mergeability retry condition: use && instead of || so the retry
  only fires when the field is both present and unknown
- AppState.PanelState gains a warningMessage field propagated from
  each poll cycle
- PollingEngine: consume PRFetchResult, propagate warningMessage,
  refactor catch branches into private helper methods, add OSLog
- PRListView: show a yellow warning bar when warningMessage is set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MockGitHubDataSource updated to return PRFetchResult; marked @unchecked Sendable
- PollingEngineTests: add successResult() helper, update all call sites,
  add tests for serverError retry, paginationLimitExceeded regular interval,
  warning message stored/cleared on success
- AppStateTests: add warningMessage field to fixture snapshots, new tests
  for panelContentState_warningWithExistingPRs and
  healthStatus_warningDoesNotOverrideExistingTriageHealth
- GitHubRESTClientPaginationTests: test that search cap stops at page 10
  and attaches warning message when totalCount > 1,000
- GitHubRESTClientMergeStateTests: test that resolveDisappearedPRs with
  unknown mergeableState does not retry when retry is disabled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- mise.toml: add inline comments clarifying the scope difference between
  `check` (full verification) and `fix` (auto-fixes only, no tests)
- PR ordering plan: guard setOrder to removeObject when ids is empty
  instead of storing an empty array

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…inject authorizationRequester

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ken and sleep

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rror in health status

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…o 90% capacity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…andle CI link open errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace direct Task.sleep call with the injected sleep closure so the
interval is fully testable without real time passing; add a test that
verifies the injected closure receives the correct poll interval.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cap reviewer display at two names and append "and N more" for any
extras; extract a shared formattedReviewers helper used by approved,
changesRequested, and requested review states.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mark Handler and MockResponse as Sendable and annotate matcher
closures with @sendable to satisfy strict concurrency checking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use exact path matching for pull-details stubs to prevent cross-test
interference; rename test to match actual behaviour (returns warning,
not throws); rename shadowed local variable in MockGitHubDataSource;
loosen AppErrorTests timing and remove locale-sensitive assertion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@awjdean awjdean self-assigned this Mar 31, 2026
@awjdean
awjdean merged commit ddcb785 into main Mar 31, 2026
1 check passed
@awjdean
awjdean deleted the feat/mvp branch March 31, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant