Skip to content
Like what we’re building? Star on GitHub

ADR-064: Worktree finalization requires delegated workspace authority ​

Context ​

The worktree finalization path (see the engine's worktree: auto handling) is powerful and destructive-adjacent: on run close it creates an iterion/run/* branch on the worktree HEAD, best-effort fast-forwards the operator's checked-out branch, and removes the tree. Two failure modes were observed where that authority was exercised over a tree iterion did not own:

  1. A worktree: none run launched via the CLI from inside a foreign linked worktree (e.g. a Claude Code session's own worktree) was promoted to a managed worktree and queued close-time finalization — branching, FF-merging, and cleaning a tree iterion never created.
  2. A phantom-worktree run (Worktree=true but WorkDir == RepoRoot) could, on cancel, git add -A && git commit unrelated operator WIP directly onto their live branch.

iterion cannot distinguish "a worktree I was handed" from "a worktree the operator happens to be standing in" without an explicit delegation signal — the prior heuristic (promote any linked worktree it finds) guessed, and guessed wrong on both counts.

Decision ​

Worktree promotion and finalization are gated on the workspace being explicitly delegated to the engine, on two independent seams:

  • Delegation signal at entry.WithWorkDir sets workDirDelegated = dir != "" — a non-empty workspace is one a caller (dispatcher per-issue worktree, studio-bound dir) explicitly handed over. A defaulted CWD (resolved from os.Getwd() at run time) is never adopted. The promotion path in engine_run.go requires workDirDelegated before adopting a linked-worktree workspace as a managed-worktree baseline.
  • Refuse-at-the-root invariant.finalizeWorktree (and the recovery path RecoverFinalize) refuse outright when samePath(wtPath, repoRoot) — preserving the tree and warning, instead of running the wip-bank commit in the operator's live checkout. Finalize therefore only ever commits inside a dedicated, delegated worktree.

The two guards are complementary: the delegation flag stops a defaulted CWD from becoming a managed worktree in the first place; the samePath refusal is the backstop that stops any commit landing on the operator's branch even if a run's persisted state collapses WorkDir to the repo root.

Trade-offs ​

Gating on delegation trades a small amount of convenience — a run launched from inside a linked worktree no longer auto-branches its commits — for the guarantee that iterion never branches, FF-merges, or commits onto a tree the operator owns. The honest concession: the delegation signal is a heuristic on CWD provenance ("was a workspace handed to us explicitly?"), not a first-class ownership assertion; a caller that explicitly passes the operator's own checkout via WithWorkDir still grants authority over it.

Alternatives considered ​

1. Promote any foreign linked worktree (prior behaviour) ​

Detect that the CWD is a git linked worktree and adopt it as a managed worktree with finalization authority, regardless of how the run was launched.

Rejected because: iterion cannot tell "a worktree I was handed" from "a worktree the operator happens to be standing in" without an explicit delegation signal. The heuristic branched, FF-merged, and cleaned trees it did not own — the exact bug this ADR closes.

2. Rely on the step-5 "never merge a wip-banked HEAD" guard alone ​

Keep promoting freely and lean on the existing late-stage guard that refuses to merge a HEAD that was wip-banked at close.

Rejected because: for the phantom-worktree case the commit is already on the operator's branch by the time that guard runs — the wip-bank git commit executed in the main checkout. The guard cannot un-commit; the refusal has to happen before the commit, at samePath.

Consequences ​

  • Finalize only ever touches a delegated, dedicated worktree. Both the promotion gate and the samePath refusal must pass, so no branch/FF/commit ever lands on a tree iterion did not create.
  • CLI runs from inside foreign worktrees are safe. A worktree: none run launched from a Claude Code session worktree is left exactly as found — no branch, no FF, no cleanup.
  • Phantom-worktree cancels preserve, not commit. When a run's WorkDir collapses to the repo root, finalize preserves the tree and warns instead of committing unrelated WIP onto the operator's branch.
  • Delegation is provenance-based, not asserted. The gate infers authority from "was the workspace passed explicitly?"; there is no ownership token, so an explicit WithWorkDir(operatorCheckout) still grants authority.
  • Re-challenge: if runs gain an explicit workspace-ownership token / manifest, the CWD-delegation heuristic could be replaced by that assertion.