mbuvarp/gopher

★ 0Forks 0RustGitHub ↗Compare

README

Gopher

A native Rust macOS menu bar inbox for GitHub agent reviews. Tracks open PRs you authored, are assigned to, or are requested to review, grouped by repository under repo • organization headings, ordered by organization, repository, then ascending PR number.

Using Gopher

Left-click the gopher in the menu bar to open a native, scrollable review inbox. Each PR has a status icon, title, review status, and unresolved-thread count, followed by compact pills in its GitHub label colors. Labels use the detail row’s font size, wrap when needed, and refresh during normal polling or after a successful label change. Unacknowledged actionable updates have bold blue titles and blue status icons. Click a title to acknowledge its displayed update without closing the popover; its title returns to regular weight and both title and icon return to their normal colors after the change is saved.

The detail row also shows Checks running... in yellow, Checks failed in red, or Checks green in green. Confirmed merge conflicts show Conflicts in orange, taking precedence over every CI status and disabling the Merge action. While GitHub is calculating mergeability, the ordinary CI summary is shown. Failed checks take precedence while other checks are still running. No checks, or checks that all succeeded, were skipped, or finished neutral, count as green. This includes CI checks and the latest legacy status for each context, using the existing polling requests. The summary is hidden for stale or ignored PRs and does not affect review acknowledgements or notifications.

The buttons beneath each PR let you:

  • Open PR in the browser and acknowledge the displayed update after it opens successfully.
  • Expand Details to inspect reviewer evidence.
  • Open the right-aligned Actions dropdown for repository configuration, enabled PR actions, and Ignore at the bottom. Ignore hides the PR from the active list, review polling, and notifications until restored.

The header has Refresh and an Actions menu. Refresh is disabled and reads Refreshing... while PRs are being fetched. Actions contains Show ignored, Settings, notification settings, logs, launch at login, and Quit Gopher. Show ignored replaces the active list with ignored PRs, showing Restore buttons and a Back button in the header. Restoring resumes discovery for eligible open PRs.

The popover updates while open, retaining expanded details and each view's scroll position. Click outside to dismiss it. Right-click the menu bar icon for the alternative standard menu, where PR submenus include an Acknowledge update checkbox. Both interfaces share the same state and notification behavior.

Ignored PRs are checked at startup and every 15 minutes without fetching reviews. Closed or merged PRs disappear from the ignored list, but keep their ignore flags: if reopened, they reappear there and remain ignored. Unavailable identities keep their cached state and do not block updates to accessible PRs. They can still be restored while offline.

Settings and hotkeys

Choose Actions → Settings to customize shortcuts. Click a binding and press its replacement, use Clear to unset it, or Restore defaults to reset all eight. Escape cancels recording. Changes save and apply immediately. Conflicting or unavailable bindings show an error and leave your previous shortcut working. Open configuration file opens the advanced config.toml settings, which still require restart.

Action Default Scope
Open Gopher Unset Globally toggles the popover; requires Command, Option, or Control
Next PR J Inbox
Previous PR K Inbox
Acknowledge Space Active inbox
Open PR O Inbox
Toggle Details D Inbox
Open Actions A Active inbox; opens the menu for arrow-key navigation
Refresh R Active inbox

Keyboard navigation highlights a PR without acknowledging it. J starts at the first PR; K starts at the last. Moving beyond either end clears the highlight. The highlighted PR scrolls into view and stays selected through background updates and trips to other panels. Reopening the popover clears the highlight. Notification clicks highlight their target after acknowledgement and flash its title and icon blue three times to draw attention, then restore their normal color. The ignored list supports navigation, Open PR, and Details too.

Command+, is a fixed shortcut that opens Settings from the focused popover. It cannot be reassigned.

Escape is fixed: it returns settings, repository configuration, label pickers, and the ignored list to the main PR list, then closes the popover from there. An open dropdown or shortcut recording consumes Escape first. Navigation waits while changes are being saved.

Inbox shortcuts pause in settings, label pickers, text editing, and native menus. Bindings follow the recorded key position across keyboard layouts, and preferences persist across GitHub account changes.

PR actions

Choose Actions → Configure on any active PR to configure actions for its repository. The table has Action, Enabled, Condition, and Merge method columns. Changes save immediately to SQLite and apply to every PR in that repository, without restarting. Both actions start disabled; Merge defaults to the Approved condition and Merge commit method, while Label defaults to Always.

Conditions are Always, Reviewing, Comments, and Approved. Enabled actions remain visible but disabled when their condition is not met or PR data is stale. Draft PRs cannot be merged.

  • Merge on green checks appears in place of Merge while checks are running and the configured merge condition is met. Selecting it replaces Actions with Merge pending…; click that button to cancel. Once checks turn green, the normal five-second countdown begins. Failed checks, conflicts, changed review evidence or commit, stale data/polling failures, changed merge settings, or an account change cancel the pending merge. Pending merges survive navigation but not quitting. Checks must still be green during final validation.
  • Merge offers Merge commit, Squash, or Rebase. Clicking it replaces the dropdown with Cancel (5s), counting down before attempting the merge. Closing the menu/popover or navigating elsewhere does not cancel it; quitting Gopher does. Gopher rechecks the PR and targets the selected commit. GitHub blockers appear beneath the PR; Gopher does not enable auto-merge, join a merge queue, bypass protection, or retry automatically.
  • Label opens a persistent picker with colored dots and checked labels. Toggle several labels without leaving the panel; each change adds or removes only that label. The picker opens immediately from an account-scoped repository catalogue cached in SQLite, with checkmarks from the normal PR snapshots. Catalogues refresh in the background when first needed and about every 15 minutes; PRs in the same repository share one catalogue. Refresh labels refreshes that catalogue immediately, and Back returns to the PR list once pending saves finish. Back stays disabled while saving so failures remain visible in the picker.

These actions use the authenticated GitHub CLI account and require its normal repository permissions. They are available in the popover; the alternative standard menu retains its existing review controls.

Run

Building requires a Mac capable of running Xcode 26.5 or newer (macOS SDK 26.5+), Rust with the aarch64-apple-darwin target, and Python 3. Gopher runs on Apple Silicon and requires an installed GitHub CLI authenticated with access to your repositories:

gh auth login
cargo run --features dev -- doctor
rustup target add aarch64-apple-darwin
sh scripts/bundle.sh
mkdir -p ~/Applications
ditto "dist/Gopher Dev.app" "$HOME/Applications/Gopher Dev.app"
open "$HOME/Applications/Gopher Dev.app"

Allow notifications when prompted. Enable Actions → Launch at login to start Gopher automatically in your user session. Quit Gopher before replacing an installed build. The build script packages the application icon and uses an installed Apple Development signing identity, or GOPHER_SIGNING_IDENTITY if specified. Local builds without one fall back to ad-hoc signing and warn that notification authorization may fail.

Local bundles are Gopher Dev, with bundle identifier dev.mbuvarp.gopher.dev, a DEV icon badge, and data in ~/.config/gopher-dev. They can run alongside released Gopher and never use Sparkle. The Install Gopher Dev skill replaces only this app. Settings identifies the source commit and uncommitted changes. Dev has separate notification permissions and login registration; use different global shortcuts when running both apps.

For initial setup, copy config.toml into the Dev directory. If copying state.sqlite3 too, stop Gopher first or use SQLite backup; do not copy locks, logs, or session.json.

cargo run --features dev also starts Dev (plain cargo run retains the production data path), but notifications and launch at login require the bundled app. No web frontend, server, webhook setup, or separate system daemon is needed.

Package a release archive

sh scripts/bundle.sh --release

This builds an Apple Silicon app with a macOS 13.0 deployment target, then produces dist/Gopher.app, dist/Gopher-<version>-macos-arm64.zip, a matching .zip.sha256 file, and dist/install.sh. Only the app is inside the archive. The script selects the macOS SDK through xcrun, passes its path to Cargo, and requires SDK 26.5+ for both local and release bundles. It verifies the SDK recorded in the executable as well as architecture, minimum OS, metadata, and code signature both before and after extracting the ZIP. It stages a fresh bundle so removed resources cannot linger, and replaces existing outputs only after validation succeeds.

Release archives require an Apple signing identity and never fall back to ad-hoc signing. The default is the first available Apple Development identity; set GOPHER_SIGNING_IDENTITY to select a different certificate name or fingerprint. A missing or invalid identity fails packaging. This initial distribution is unnotarized: macOS may require System Settings → Privacy & Security → Open Anyway after the first launch attempt. A checksum detects corruption; the installer also verifies the Apple signature, and the bundled updater verifies signed feeds and archives. Do not disable Gatekeeper or strip quarantine attributes to install Gopher.

Cargo.toml is the version source. The app's display version matches it; the numeric build version is (major + 1).minor.patch so that 0.1.0 sorts after the historical build number 1. Packaging supports stable versions only, with major up to 9998 and minor/patch up to 99 to fit macOS build fields. When bumping the package version, update its Cargo.lock entry too; packaging uses --locked and does not bump versions or publish a release. Users of the packaged app need macOS 13+, Apple Silicon, and authenticated gh, but no Rust, Xcode, or Python installation.

Verify a downloaded archive from its directory with shasum -a 256 -c Gopher-<version>-macos-arm64.zip.sha256, extract it, and move Gopher.app to ~/Applications. Quit an existing copy first and preserve ~/.config/gopher. A browser download on a separate Mac is needed to test the normal Gatekeeper experience; copying via SSH alone does not reproduce it.

In Finder, use Go → Go to Folder… and enter ~/Applications to reach your user Applications folder. Moving the app there does not remove the Apple trust warning. If the first launch is blocked, open System Settings → Privacy & Security, choose Open Anyway for Gopher, and confirm the prompt. This grants an exception for the app without disabling macOS security protections.

Packaging checks: python3 -m unittest discover -s scripts -p 'test_*.py'.

Install or update

Once a release is published with a v<version> tag and the assets above, the same command installs or updates Gopher:

curl -fsSL https://github.com/mbuvarp/gopher/releases/latest/download/install.sh | bash

Requires Apple Silicon, macOS 13+, and installed/authenticated gh. It needs no sudo, Python, Rust, or Xcode. The script resolves one stable release, downloads its versioned ZIP and checksum, and validates the archive, app version, and Apple signature against Gopher's bundle identifier and signing team before invoking the verified app's installation helper. The helper installs into ~/Applications/Gopher.app, rejects downgrades and unverifiable existing apps, and leaves an already-current installation alone.

Fresh installations open automatically. Upgrades relaunch only if Gopher was running; a stopped app stays stopped. To suppress launching, use bash -s -- --no-launch at the end of the pipeline. Settings, cached PRs, acknowledgements, and Launch at login preferences are preserved. Gopher finishes submitted merge/label requests before exiting, cancels pending countdowns and unsent label changes, and rejects new actions while shutting down. An older running build without this shutdown support must be quit manually before its first upgrade.

Downloads and staging finish before Gopher is asked to quit. Replacement holds both an installer lock and Gopher's instance lock; a failed replacement restores the previous app. Shutdown has a 150-second installer timeout and never force-kills Gopher. If macOS refuses the launch request after installation, the error identifies the installed app and retained recovery files; the installer does not roll back a version that might already have opened the database. Quarantine and Gatekeeper settings are not disabled. This remains an unnotarized build and may require Privacy & Security → Open Anyway.

Review behavior

  • Unknown: No reliable current-commit result, missing reviewer evidence, or stale cached data.
  • Reviewing: At least one participating agent is running, or a final result is being confirmed across polls.
  • Comments: All participating agents have finished and unresolved review threads remain.
  • Approved: All participating agents have an explicit clean result for the current changes, with no unresolved threads.

Reviewing includes elapsed time since Gopher observed the state, such as Reviewing (7m) or Reviewing (1h 2m). Minutes are rounded down. The timer survives restarts and resets on a new commit or when the PR enters Reviewing again; it does not use GitHub's review start time.

Findings are held until every participating reviewer finishes. Completed results must remain stable for 30 seconds by default, avoiding notifications while an agent is still publishing its output. An approval describes agent review only; it is not a guarantee of CI success, human approval, or mergeability.

Unacknowledged results affect the menu bar icon, with comments taking priority over approvals. The plain cartoon gopher is used both when nothing needs attention and while reviews are running or being confirmed; background polling alone does not change the icon. The question mark is reserved for errors or unacknowledged unknown/stale PRs. Acknowledgement silences the displayed update and clears its notification; meaningful new updates need acknowledgement again.

Clicking a review notification acknowledges its update, opens the active PR list in the popover, and scrolls the PR into view. An already open popover stays open. Navigation waits while a PR menu is in use or labels are saving; a label error remains visible until you leave the picker or retry successfully. Notifications for PRs no longer in the active list open the inbox without restoring ignored PRs. Its action menu offers Acknowledge and Open PR; Open PR opens the browser and acknowledges after it opens successfully. Older notifications cannot acknowledge newer updates.

Codex detection uses its bot reactions, commit-specific reviews, and persistent review summary. Cubic uses checks and explicit issue counts. CodeRabbit uses checks and explicit review verdicts. Successful checks alone do not establish approval. Unknown formats and reactions without a reliable commit association stay Unknown. Resolved threads alone do not establish a clean review.

Reviewers are inferred from activity on each PR, including previously observed agents. Explicit subscription-limit, paused-review, or Cubic branch-rewrite notices are shown as Skipped and excluded from automatic participation; an explicitly required reviewer that skips keeps the result Unknown. If a skipped reviewer's activity disappears, its cached skip does not make it an unfinished participant; new activity brings it back. Missing results from other previously participating reviewers still block, with the historical participation explained in Details and logs. If a subscription or repository setup changes, use an explicit reviewer list. Polling can miss a complete rerun between polls, particularly one represented only by reactions; ambiguous evidence is intentionally conservative.

Configuration and data

Everything is stored in ~/.config/gopher:

config.toml           Optional settings; restart to apply
state.sqlite3         PR cache, review evidence, acknowledgements, ignored PRs, action settings, notification history
logs/gopher.jsonl     Structured logs, at most 10,000 retained lines
gopher.lock           Prevents concurrent app instances
session.json          Last app session and completed shutdown, if recorded

SQLite may also create state.sqlite3-wal and state.sqlite3-shm. GitHub remains authoritative. Cached data stays marked stale until successfully fetched. Changing the active GitHub account resets the active cache and acknowledgements; ignored PR flags, their archived details, and repository action settings persist across account changes. Pending actions are not restored after restart.

Use Actions → Settings → Open configuration file to create/open a configuration file, or copy config.example.toml. Settings include polling/discovery intervals, request timeout, result confirmation interval, logging verbosity, gh_path, notifications, and repository overrides:

[repositories."owner/repo"]
reviewers = ["codex", "cubic"]
# ignore = true

Gopher polls active PRs every 30 seconds and discovers new PRs every two minutes by default. Authored PRs come from GitHub's direct API; assignments and review requests use search. Search omissions do not remove known PRs or erase acknowledgements: for the same account, Gopher keeps monitoring a tracked PR until direct evidence confirms closure or you ignore it.

Polling uses gh api, fully paginates PR connections and checks, and checks that the head commit did not change during a fetch. Up to three PRs are fetched concurrently. Final head and account checks are batched across up to 50 PRs per request. REST reads use ETags and reuse cached responses when GitHub returns 304 Not Modified. Errors trigger bounded exponential backoff; Refresh retries immediately unless GitHub has imposed a rate-limit cooldown. Quotas are scoped to the active credential and API resource, so exhausting REST does not postpone GraphQL-only ignored-PR checks. Label refresh authentication can use REST while GraphQL is exhausted. During cooldowns, a local credential check detects account switches without calling GitHub; tokens stay in memory and are never logged or persisted. All request paths respect Retry-After and quota reset times; secondary limits without a retry time wait at least one minute and back off further on repeated failures. Mutations are never automatically retried. Authentication and network errors remain visible in the inbox and produce a notification when the error changes.

Logs include GitHub quota limits, remaining budget and reset times, rate-limit cooldowns, repository label refreshes, request timing and conditional cache hits at debug, reviewer evidence, state transitions, notification scheduling, and acknowledgement events. Failed requests include the request type, HTTP status when available, CLI exit code, and a classified cause; PR fetch failures include the repository and PR number. Tokens and full API responses are not logged. Individual records are limited to 16 KiB; oversized records are replaced with a diagnostic entry. The asynchronous log queue is bounded; overload drops entries with a stderr diagnostic. The SQLite cache does contain private PR content and should be treated accordingly.

Lifecycle diagnostics are recorded even when the configured log level suppresses normal events: app_started, shutdown_requested (Quit, SIGTERM, or SIGINT), event_loop_stopped, app_stopped, and app_failed. Rust panics record their message, thread, source location, and backtrace as rust_panic. These diagnostics wait up to two seconds for a disk flush; long diagnostic text is truncated while preserving the event. Disk failures or a panic in the logging thread fall back to stderr.

The session marker is completed only after the app returns through its shutdown path. A subsequent launch reports previous_session_unclean if completion was not recorded, including the previous PID and start time. This indicates an incomplete shutdown, not a proven crash or cause. Force kills and power loss cannot log at the moment they happen; native crashes may instead produce a report in ~/Library/Logs/DiagnosticReports. The read-only doctor and inspect commands do not change the session marker.

Diagnose and develop

cargo run -- doctor
cargo run -- inspect owner/repo 123
cargo fmt --check
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked

doctor verifies configuration, CLI discovery, and authentication. inspect fetches live evidence and prints a JSON verdict without writing PR state or sending notifications. It does not have the running app's historical evidence and does not apply the confirmation interval.

See AGENTS.md for project conventions. Use Conventional Commits.

In-app updates

Release builds include Sparkle; no separate updater installation is needed. By default, Gopher checks daily, downloads updates, and installs them when you quit. It does not restart itself without your request. Settings has switches for automatic checks and downloads; changes apply immediately. Sparkle owns these preferences in macOS defaults and its temporary downloads in macOS caches. PR data, configuration, and Gopher logs remain in ~/.config/gopher.

The Actions menu entry above Quit Gopher changes from Check for updates… to Update available…. Open it to see release notes and install/relaunch using the standard update dialog. Updates wait for submitted PR actions to finish before quitting. Background failures are logged without repeated alerts. Local development builds do not check for releases or replace themselves.

Update feeds become available after the first release is published. See update maintenance and validation for signing and testing details.

Releases are created manually through the Release GitHub Actions workflow on main. Start with its default validate mode to check signed packaging; choose publish explicitly to release. A newer Cargo version and matching nonempty CHANGELOG.md entry are required after the first release. See release setup and recovery.

Use the repository's $release skill to prepare a release. It asks for a version choice and changelog approval, then validates, commits, pushes directly to main when permitted, and follows the release workflow. It can propose the existing version for a first release, and respects requests to only prepare or validate. The first publication still requires GOP-7's end-to-end validation to be complete.

Contributors

mbuvarp

Issues