worktree start picks its mode from where it runs; finish gains --keep and a dirty-primary refusal #844
Labels
No labels
P0
P1
P2
P3
bug
category/code-change
category/cross-repo
category/eval-run
category/investigation
category/no-test-target
category/owner-ruling
category/prose-artifact
category/recurring-chore
category/structure
category/umbrella
crc-refactor
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
rule-proposal
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#844
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The implement skill always runs
worktree start ticket-<n>and always finishes that worktree.worktree startrefuses inside every linked worktree (start_command.rb:26,refuse_nested!). So a session that starts in its ownticket-<n>worktree (a failed run picked up) or in a persistent worktree stops at Setup. Work must never change main directly, but persistent worktrees meet that rule.This ticket is the
osplugin part:worktree startpicks a mode from where it runs, andworktree finishgains--keepand a dirty-primary refusal. The os-sdlc part (open places the worktree, payload, skill text) is a separate ticket blocked by this one. ADR-0253 records the design.Reproduce
From inside a linked worktree:
worktree start ticket-844exits 1 with "cwd is inside a worktree — refusing to nest; run start from the main checkout".Approach
StartCommand#runreplaces the blanket nesting refusal with mode rules. Reused worktrees are checked for ADR-0143 links and brought up to the base branch by merge (fast-forward when possible), never rebase. Base is the primary checkout's current branch, asFinishCommand#base_branchdefines it.FinishCommandgains a dirty-primary refusal (exit 6, free after #842) and--keep. Keep oneStartCommandwith private mode methods and one result object; do not add a class per mode.Required work:
worktree start NAMEfrom the primary checkout with no NAME worktree creates it as today and printsmode: ticket-newandfinish: worktree finish NAME— check:grep -rn "ticket-new" plugins/os/lib/worktreegit worktree list --porcelain,startreuses it:mode: ticket-continue, nogit worktree add.startexits 1 and names the branch.startprintsmode: ticket-continueand that worktree's path.ticket-and is not NAME,startexits 1 and the message names both branches.startprintsmode: persistent,branch:the worktree's branch, andfinish: worktree finish <branch> --keep.startrefuses (exit 1) unless.cc-osand.sdlc/pipeline.dbin the worktree are symlinks to the primary checkout's copies; the message names the entry. It never repairs them.startrunsgit merge --ff-only <base>in the worktree, elsegit merge --no-edit <base>. It never rebases. On conflict it exits 1, printsMerge conflicts in: <files>, and leaves the merge in progress. WithMERGE_HEADalready present it exits 1 with "merge in progress" and does not merge again — check:! grep -rn "rebase" plugins/os/lib/worktreestartoutput addsmode:andfinish:lines to the existingworktree:,branch:,path:,linked:,next:lines.finishexits 6 before any merge when the primary checkout has uncommitted changes to tracked files. Untracked files do not trigger it. This applies with and without--keep.finish BRANCH --keepruns the same refusals, merge, ancestry check, and AfterMerge, then runsgit merge --ff-only <base>in the worktree, and keeps the worktree and branch. It prints "merged BRANCH into ; worktree and branch kept" — check:grep -n "method_option :keep" plugins/os/lib/worktree/cli.rbpath:is the worktree's registered path fromgit worktree list --porcelain, notLayout#worktree_path; a persistent worktree can live outside.claude/worktrees/.next:printsEnterWorktree(path: "<path>")only when the cwd is not already that worktree; otherwise it printsnext: none (cwd is the worktree).git status --porcelain --untracked-files=no— check:grep -rn "untracked-files=no" plugins/os/lib/worktreeDocumentation, done by the orchestrator, not by a pipeline agent:
plugins/os/skills/worktree/SKILL.mdstart and finish sections for the modes,--keep, exit 6, and the conflict path (/mattpocock-skills:resolving-merge-conflicts).plugins/os/invariants.mditems 17-20,plugins/os/CLAUDE.mdlines 26-27 ("nesting guard"), anddocs/implementation-status/os.md(line 49 "refuses to nest", the exit list on line 51); add one History row.bin/refresh-pluginsafter the change.Rejected: rebase reused branches (rewrites shared history; finish exit 2 already prescribes a merge); repair unlinked worktrees (could replace a non-empty directory); separate
sync/status/integratesubcommands (one command and one flag suffice).Acceptance criteria
ticket-9worktree, whenworktree start ticket-9runs, then the worktree is created as today and stdout hasmode: ticket-newandfinish: worktree finish ticket-9.ticket-9worktree, whenworktree start ticket-9runs, then it exits 0, creates nothing, and stdout hasmode: ticket-continueand that worktree's path.ticket-9branch with no worktree, whenworktree start ticket-9runs, then it exits 1 and stderr namesticket-9.ticket-9, whenworktree start ticket-9runs, then it exits 0, creates nothing, and stdout hasmode: ticket-continue.ticket-8, whenworktree start ticket-9runs, then it exits 1 and stderr namesticket-8andticket-9.p, created withgit worktree addoutside.claude/worktrees/, with base one commit ahead and no commits of its own, whenworktree start ticket-9runs, thenpfast-forwards to base, and stdout hasmode: persistent,branch: p,path:that worktree's registered path, andfinish: worktree finish p --keep.pwith its own commit and base ahead, whenworktree start ticket-9runs, then base is merged intopwith a merge commit and no commit onpis rewritten.pconflicts, whenworktree start ticket-9runs, then it exits 1, stderr lists the conflicted files, andMERGE_HEADexists.MERGE_HEADexists in worktreep, whenworktree start ticket-9runs, then it exits 1 with "merge in progress" and HEAD does not change.pwhose.cc-osis a directory, not a symlink, whenworktree start ticket-9runs, then it exits 1, stderr names.cc-os, and the directory is unchanged.worktree finish ticket-9runs, then it exits 6 before any merge.worktree finish ticket-9runs, then it merges and removes as today.phas a commit not on base, whenworktree finish p --keepruns, then base contains the commit,pequals base, and the worktree directory and branchpstill exist.ticket-9, whenworktree start ticket-9runs, then stdout hasnext: none (cwd is the worktree).Retired tests
plugins/os/tests/worktree/start_command_refuses_nested_test.rb
Owner sketch
Origin
plugins/os-sdlc/skills/implement/SKILL.md← DESIGN: ADR-0157 (worktree CLI sole creator)Related: #842, which removes
--implementationand the before-remove hook fromworktree finish.Blocked by: #842 (both change
finish_command.rband exit numbering).VERDICT: CREATE — user-requested hard floor; ADR-0253 records the decision (ticket-skeptic, 2026-09-30).
User rulings 2026-09-30:
Pick the implement worktree mode from the start state: ticket, persistent, or mainto worktree start picks its mode from where it runs; finish gains --keep and a dirty-primary refusalWork started on branch ticket-844 (worktree .claude/worktrees/ticket-844) via /os-sdlc:implement. #842 merged at
6cd75d59.Resolution
Done: worktree start picks its mode from where it runs: ticket-new, ticket-continue (from main checkout or inside), refuse other ticket-*, persistent; reused worktrees must be linked (never repaired) and are brought up to base by ff-only else merge, never rebase; output adds mode:/finish:, next: is none when cwd is the worktree. finish refuses a dirty main checkout (exit 6); finish --keep merges, fast-forwards the branch, keeps worktree and branch. Registry and Checkout objects own lookup and worktree-side git. os:worktree skill, invariants 19-21, os CLAUDE.md, and status leaf updated.
Evidence: Runner commit
6b5c6a72(implementation 9, implementation_complete); docs commit9ac00e4a. ruby plugins/os/tests/all.rb: 35 runs, 0 failures, stable over 5 parallel runs after the WorktreeScenario capture fix. Required-work checks 1/8/11/14 verified by hand. ADR-0253.Follow-ups: Two follow-ups captured separately via /os-backlog:capture: (1) worktree guard blocks agents without Write from submitting handoffs containing the word git; (2) finish --keep exits 1 with no message when the worktree fast-forward fails. #846 (os-sdlc part) is now unblocked.