fix(jobs): reap a job's branch with its worktree, and sweep the refs that outlived their record - #174
Merged
Merged
Conversation
…that outlived their record Every managed worktree created a `job/<id>` branch and nothing ever deleted one: `worktree.prune` answered `branchRetained: true` unconditionally, and deleting a session never pruned at all. 153 refs had piled up in this checkout against an empty job store, 92 of them not merged into main, with no record left to say which carried work (#157). The rule is containment, not merge state: a `job/*` ref goes when every commit it points at is reachable from some other local or remote ref, and stays when it holds work found nowhere else. The namespace is the app's own, so no user needs to be asked. - `worktree.prune` removes the checkout and then reaps the branch under that rule; `branchRetained` now reports what happened. - New `worktree.reap` sweeps orphans: every `job/*` ref with no registered worktree and no commit of its own, never a branch outside the namespace. The orchestrator runs it once per repository per process before the first worktree it creates there, and a failure only logs. - Deleting a worktree session permanently (`?force=true`) archives its job, prunes its checkout and reaps its ref. It refuses with `SESSION_WORKTREE_RUNNING` while the job runs and `SESSION_WORKTREE_DIRTY` while the checkout has uncommitted changes, and the session stays in both cases. Archiving keeps checkout and ref. Run against this checkout's 153 orphans: all 153 reaped, 0 retained, and every one of the 35 distinct former tips is still reachable from a surviving ref (`main`, `origin/*` or `checkpoint-c`) - the 92 "unmerged" refs pointed at commits other branches already held. Closes #157.
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.
Problem
#157: every managed worktree created a
job/<id>branch and nothing ever deleted one.worktree.pruneansweredbranchRetained: trueunconditionally, and deleting a session did not prune at all. This checkout had 153job/job-*refs against an empty job store — 92 not merged intomain— with no record left to say which carried work.The rule
Containment, not merge state. A
job/*ref is deleted when every commit it points at is reachable from some other local or remote ref (git for-each-ref --contains), and kept when it holds work found nowhere else. The namespace is the app's own, so it needs no user. "Not merged into main" was the wrong question: 80 of the 153 pointed at one commit thatcheckpoint-cholds.Changes
Native core (
git.rs)worktree.pruneremoves the checkout and then reaps the branch under the rule above; the reply'sbranchRetainednow means what it says.worktree.reap: sweeps everyjob/*ref with no registered worktree and no commit of its own; reports the rest asretained; never touches a branch outside the namespace; rejects params.Server
GjcGitClient.reap(). The orchestrator calls it once per repository per process before the first worktree it creates there; a failure logs and never blocks a run.releaseSessionWorktree(session-worktree-paths.ts): archive the job → prune the checkout (reaps the ref) → on refusal, unarchive. Wired intodeleteOrArchiveSessionById(..., { force: true })through the session-worktrees service. Refusals areAppErrors the route already serialises:SESSION_WORKTREE_RUNNING(409) while the job isreserved|queued|running|aborting,SESSION_WORKTREE_DIRTY(409) while the checkout has uncommitted or ignored-file changes. The session row is untouched in both cases. Archiving a session still keeps checkout and ref (existing test).GJC-LIVE-SPEC.md› Native job authority: the ref lifecycle in one paragraph.Verification
cargo test(git module): prune reaps an empty branch / retains one with unlanded commits; reap handles empty, landed, unlanded and live-worktree cases, is idempotent, rejects params, and ignoresfeature/*and a branch literally namedjobs.npm run check:coreclean.gjc-job-orchestrator.worktree.test.ts: real core, real repo — first job reapsjob/job-emptyandjob/job-landed, keepsjob/job-unlandedandfeature/not-a-job, creates its own branch, and a second turn does not sweep again.session-worktree-runtime.test.ts: four new tests over the real jobs authority — clean delete removes the checkout, reaps the ref, deletes the session and archives the job (visible through the list filter); a dirty checkout is refused and the job is not left archived, then a branch with unlanded commits survives the delete; a running job is refused without touching the checkout; a never-prepared session deletes with nothing to release.worktree.reapon the live repo →reaped=153 retained=0. All 35 distinct former tips verified still reachable from a surviving ref.npm test: server 1454 pass / 1 skip, client 540, bun all green.typecheck,lintclean.Closes #157.