fix(cli): retire stranded legacy github-scan launchd runner on upgrade and daemon start - #1994
Draft
serenakeyitan wants to merge 2 commits into
Draft
fix(cli): retire stranded legacy github-scan launchd runner on upgrade and daemon start#1994serenakeyitan wants to merge 2 commits into
serenakeyitan wants to merge 2 commits into
Conversation
…e and daemon start Pre-#775 installs of the removed `github scan` subsystem left a per-user launchd job (`com.first-tree.github-scan.runner.<login>.<profile>`) with KeepAlive=true and an absolute binary path baked into ProgramArguments. After an upgrade removed the subsystem or relocated the binary, launchd kept restarting the dead path: a KeepAlive crash-loop that also squatted the legacy default HTTP port (7878). Add `retireLegacyGithubScanRunner()` (darwin-only, idempotent, never throws): it unions labels found via on-disk plists under `~/.first-tree/github-scan/runner/launchd/` and via `launchctl list` (covers zombies whose plist was hand-deleted), boots each out of the gui domain, removes the plists, and folds away the launchd dir only once empty. Hooked into `upgrade` (after a successful install, before any restart branching, so it runs under --no-restart too) and `daemon start` (before the switch-block/credentials gates, so even a logged-out machine self-heals on first run of the new version). Fixes #995 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Review follow-ups: match launchd's stable `Boot-out failed: <code>:` error codes (3, 113) in addition to the message text so idempotence detection does not hinge on exact wording, and add a test where a loaded-only label and a plist-only label are unioned and retired in one pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #995 — GoF competition entry (Fable). Draft by design; submission is signalled via labels (
fire_wip→fire_submitted), not by marking ready.Problem
Installs that ever ran the pre-#775
github scansubsystem have a per-user launchd job stranded on disk:com.first-tree.github-scan.runner.<login>.<profile>, plist under~/.first-tree/github-scan/runner/launchd/(a pre-multi-channel hardcoded path).KeepAlive: truewith an absolute node/CLI path baked intoProgramArguments.When an upgrade removed the subsystem (or relocated the binary), launchd kept restarting the dead path forever: a KeepAlive crash-loop that spams the system log and squats the legacy default HTTP port 7878. Nothing in the current CLI ever cleaned it up.
Solution
New core module
apps/cli/src/core/legacy-github-scan-cleanup.tsexposingretireLegacyGithubScanRunner():launchctl list(catches a zombie whose plist was already hand-deleted).launchctl bootout gui/<uid>/<label>for each label (a "not loaded / could not find service" answer is the expected idempotent case, not an error), delete the matching plists, then fold away the launchd dir only if empty (rmdirSyncrefuses non-empty dirs, so anything unexpected is preserved). The rest of~/.first-tree/github-scan/(config, logs, repos) is user data and is left alone.Properties: darwin-only, idempotent (nothing found → single
launchctl listand done), never throws (all failures degrade to warnings in the returned summary), and cannot touch a live service — the label prefix is exclusively owned by the retired subsystem; no current channel (first-tree/first-tree-staging/first-tree-dev) uses it.Hook points
first-tree upgrade— after a successful install, before any restart branching, so cleanup also runs under--no-restartand when no background service is installed. Retired labels / removed plists / warnings are printed.first-tree daemon start— right after daemon.env loading, before the switch-block and credentials gates, so even a logged-out machine self-heals on the first run of the new version ("first run" coverage from the issue scope). Covers the supervisor child, foreground mode, and the delegating parent alike.Deliberately not hooked into
installLaunchd()/installClientService(): those paths assert exactlaunchctlcall sequences (seeservice-install-core.test.ts) and already have a documented stance that legacy-label cleanup does not belong inside install; command-level hooks keep the blast radius to exactly the two entry points named in the issue.Impact / risk
checked: false).launchctl list(10 s cap) perupgrade/daemon start.bootoutis denied.apps/cli.Verification
apps/cli/src/__tests__/legacy-github-scan-cleanup.test.ts(10 cases): non-darwin no-op, healthy no-op, bootout+plist removal with foreign files preserved, empty-dir folding + idempotent re-run, plistless zombie, not-loaded-as-success, unexpected bootout failure → warning + sweep continues,launchctl listfailure → warning + on-disk sweep continues, loaded-only + plist-only label union in one pass, multi-profile deterministic order.core/index.js(mock stub added in 3 test files) plus one new assertion that a successfulupgrade --no-restartinvokes the cleanup.biome checkclean on all touched files (repo-wide pre-existing diagnostics inpackages/web/croncommands untouched).tsc --noEmitforapps/cli: no errors in any touched file (one pre-existing, environment-specific pino typing error inpackages/clientreproduces identically on cleanorigin/main).vitest: 117 files / 1355 tests pass including all touched suites; the only failing files (client-switch-transaction-extra,update-detect-install-mode,core-attention-update-extra,tree-init-command-extra) fail identically on cleanorigin/mainin this environment (live First Tree daemon on the host +FIRST_TREE_INSTALL_MODEset in the agent env), i.e. pre-existing and unrelated.🤖 Generated with Claude Code