Repository navigation
feat(worktree): remove finished checkouts and stop crashed runs - #186
Conversation
Quest-Claim: 13c8c96caecb29b5
`quest worktree remove` deletes a finished checkout and drops its record without forcing: it refuses live runs, modified or untracked files, ignored files beyond the declared scratch, and commits no branch or remote-tracking ref reaches, unless origin's GitHub repository merged a PR whose head is exactly that commit. `quest worktree stop` kills what crashed supervisors left in their scopes through cgroup.kill and waits until they are empty. A run's inspection failure now lives on the run and clears on the next successful inspection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/quest-start outcomeThis finishes the worktree quest. Local This stays a draft. Five choices I made without asking are listed under Decisions needed, each with a recommendation already implemented: pruning before refusing over ignored files, what preserves a commit, which forge repository is asked, (Written by Claude Opus 5.5) |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4d04fe5841
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const own = git(root, "for-each-ref", "--format=%(objectname)", "refs/worktree", "refs/bisect", "refs/rewritten"); | ||
| const tips = new Set([git(root, "rev-parse", "HEAD").trim(), ...own.split("\n").filter(Boolean)]); | ||
| return [...tips].filter((tip) => git(root, "rev-list", "-n1", tip, "--not", "--branches", "--remotes").trim()); |
There was a problem hiding this comment.
Preserve commits referenced by per-worktree recovery state
When a detached checkout creates a commit and then runs git reset --hard HEAD~1, the discarded commit remains recoverable through that worktree's ORIG_HEAD and HEAD reflog, but this scan examines only HEAD and three ref namespaces. The checkout therefore appears clean and fully preserved, and git worktree remove deletes its administrative directory—including ORIG_HEAD and the reflog—leaving the commit unreachable and eligible for garbage collection. Include per-worktree pseudorefs and reflogs in the preservation check before allowing removal.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Not changing this. remove protects the checkout's live work: HEAD, plus the per-worktree refs Git deletes with it (refs/worktree, refs/bisect, and refs/rewritten for an in-progress rebase). ORIG_HEAD and the HEAD reflog record history that was already discarded with reset, rebase, commit --amend, or merge. Agents do those constantly, so counting every reflog entry or ORIG_HEAD as unsaved work would make remove refuse almost every checkout that was ever rebased or amended. The maintainer settled that only branches, remote-tracking refs, and exact merged-PR heads preserve a commit, and that reflog entries don't count. git worktree remove makes the same call. To keep a discarded commit, branch it before removing the checkout.
(Written by Claude Opus 5.5)
|
Merging after review.
(Written by Claude Opus 5.5) |
Finishes Worktree removal, following kixelated/quest#127 and kixelated/quest#178. Finished
quest worktreecheckouts can now be removed without losing work, and processes a crashed supervisor left behind can be stopped.What changed
quest worktree remove <id>deletes a finished checkout and drops its record. It never forces. It refuses:git status --untracked-files=normal, so astatus.showUntrackedFiles=noconfig can't hide them);HEADor under the checkout's own per-worktree refs (refs/worktree,refs/bisect,refs/rewritten, which Git deletes with the checkout), that no branch or remote-tracking ref reaches;Then it unlocks the checkout and runs
git worktree remove. If Git still refuses (for example, a checkout with submodules), the quest lock is restored and the record kept. A checkout already removed by hand only drops its record, as withrelease.Mergedcallback(repository, commit) => Promise<boolean>, so it stays forge-independent. The CLI implements it with GitHub'sassociatedPullRequestson origin's repository (a fork's parent) and accepts onlymerged && headRefOid === commit. A PR that merged an ancestor ofHEADdoesn't count. The forge is asked only when an unreached commit exists, so other removals need no token or network. The lookup runs outside the registry lock, and the locked pass inspects the checkout again before deleting anything.quest worktree stop <id>ends what crashed supervisors left behind. For each run whose owner provably exited, it kills the scope's cgroup throughcgroup.kill(resolving the cgroup with read-onlysystemctl --user show, neversystemctl stop) and waits forcgroup.eventsto report it empty. Reconciliation then releases the checkout. A run whose supervisor is alive is left to that supervisor and reported, with exit 1. This addsScopes.stop.runs[].failure) rather than on the checkout. The next successful inspection of that run clears it. The checkout'sfailurenow holds only release and creation failures.--helpdescriberemoveandstop. The quest file is deleted. Agent lifecycle integration and Own-PC runner keep a plan note on reusingrun,remove, andstopin place of the deleted link.Validation
HEAD;refs/bisectmark, live runs, and an undeclared ignored.env(the checkout is left released withcache/pruned);stopkilling only the crashed run's scope and reporting the live one;mergedHeadagainst a fake GitHub: exact head, later head, unmerged, unknown commit, and a non-commit object;stopandremove.quest worktree stopinstead ofsystemctl --user stop, then checks the checkout is released and the scope is gone.nix develop -c just _checkandnix develop -c just _testpass: 379 CLI tests (2 skipped on Linux), 306 Worker tests, the hook and installer suites, and both lifecycle checks. I ran the ungated recipes because the host's load average stayed near 2× the CPU count while other quests ran. The nested Worker gate still applied.mainin 4d04fe5, which resolved an import conflict insrc/cli/main.tswith kixelated/quest#192,nix develop -c just checkandnix develop -c just testpass (387 CLI tests with 2 skipped, 306 Worker tests), as doesquest check --base origin/main --branch quest/m0/worktree.Settled decisions
The maintainer accepted all five as implemented:
removeprunes the declared scratch before it checks for remaining ignored files, so that refusal leaves the checkout released (the error says so). A read-only pre-check was not chosen: it would have to match Git's collapsed ignored directories against scratch paths, and it refuses falsely when scratch is nested (cache/nestedunder an ignoredcache/). Scratch is disposable by declaration, andrestorereverses the release.upstream()used by claims. Without an origin, or off GitHub, an unreached commit is simply refused. A PR merged into any base branch counts.stopwith a live supervisor. It stops the crashed runs, then exits 1, naming the live runs with their PIDs, instead of killing them too. Signalling the live supervisor already ends its run cleanly.runs[].failurereplaces inspection reasons on the checkout'sfailure. This changes the experimentalstatusJSON, and makes "cleared by the next successful inspection" exact, with no message matching.Suggested follow-ups
git worktree remove. A process outsidequest worktree runcould create an ignored file after the check, andgit worktree removewould delete it. The README already says to stop unsupervised work first. Recommend: no quest unless agents start writing into checkouts outsiderun.removein agent workflows belongs to Agent lifecycle integration. Nothing new is needed.🤖 Generated with Claude Code
(Written by Claude Opus 5.5)