Skip to content

Run a flow in a git worktree - #166

Merged
adamw merged 18 commits into
masterfrom
implement-worktree-runs
Aug 28, 2026
Merged

Run a flow in a git worktree#166
adamw merged 18 commits into
masterfrom
implement-worktree-runs

Conversation

@adamw

@adamw adamw commented Aug 27, 2026

Copy link
Copy Markdown
Member

Closes #150.

Adds a --worktree flag. With it, orca creates a git worktree at startup and runs the whole flow there, so several runs can work on one repository at once without sharing a checkout.

The design is in docs/plans/worktree-runs/ (specification and implementation plan), added by this branch.

How it works

The worktree goes in <main checkout>/.orca/worktrees/<promptHash>. The hash is the one that already names the run's progress log, so the same task always gets the same worktree — which is how a resumed run finds its way back. Re-run with the same task text and --worktree again.

The location is found from git (--git-common-dir), not from the current directory, so it is the same whether you start in the main checkout or in a worktree.

.orca/worktrees/ gets a self-ignoring .gitignore, like .orca/cache/ already has. Without it git add -A would stage the worktree as an embedded repo.

The run happens on an orca-worktree-<hash> branch. The worktree is created detached, but cannot be left that way: a detached worktree records the literal HEAD as the run's starting branch, which recovery refuses as unsafe, so an interrupted run could never be resumed.

Orca removes neither the worktree nor that branch — a failed run's worktree holds the log that resumes it, and a successful one holds the work. Full cleanup is git worktree remove .orca/worktrees/<hash> and git branch -D orca-worktree-<hash>. A plain git clean -xdf skips the worktree.

The shell scans per checkout: the checkout that made the worktrees sees itself and all of them, a worktree sees only itself. Run the shell from the checkout to survey every run.

What's refused

--worktree --skip-branch and --worktree --keep-changes are errors. The first asks git to check one branch out twice; the second promises to keep uncommitted files that a worktree does not carry over.

In the interactive shell the question cannot produce either pair: "Where should this run's work go?" is one choice of three — a new branch in this checkout (the default), the branch checked out now, or a new worktree.

Things to know

  • Uncommitted work in your checkout does not come along. A worktree starts from a commit.
  • The first run in a worktree pays a cold build — no target/, no node_modules.
  • An editor or indexer that ignores .gitignore will see the second checkout.

Left open by review

  • WorktreeScan caps the scan at 20 worktrees, ranked by last run. Past the cap a worktree is invisible to the resume offer and the session list. Documented in its scaladoc.
  • Two suggestions to make the illegal flag pairs unrepresentable (a TreeMode/RunTarget ADT) were declined as too wide for this change. The rule is enforced where the flags are parsed, and the interactive prompt only asks about a worktree when the branch answer was yes.
  • Three doc and test gaps hit the review's iteration limit: Worktrees' object doc still says "the two reads", WorktreeRun.resolve's doc does not list every Left, and two error cases lack tests.

adamw added 18 commits August 27, 2026 18:29
The scans looked for worktrees under the repository's main checkout, no
matter which checkout the shell was started in. From inside a worktree that
meant its siblings were scanned but the main checkout was not — an
asymmetry nobody chose. A test named "the shell's own directory is never
listed twice" pinned it as a side effect of asserting deduplication.

`.orca/` is per-checkout, and a worktree's runs leave their progress log and
session manifests in the worktree. So the rule is now the same shape as the
storage: a checkout scans itself and the worktrees hanging off its own
`.orca/worktrees/`. The checkout that made them sees all of them; a
worktree, whose own `.orca/worktrees/` is empty, sees only itself.

This drops the main-checkout derivation from the scan, which leaves
`mainCheckout`'s two-argument overload with no caller.
When the run's branch has gained commits, orca refuses rather than moving
it, and offered three ways out: merge it, delete it, or remove the worktree.
The third does not work. The refusal is about the branch, so removing the
worktree just makes the next run recreate it detached and hit the same
refusal.

The message now names `git branch -D <branch>`. The test asserted the
refusal mentions the worktree path, so it encoded the same mistake; it now
asserts the remedy that works.
The spec said a worktree is created detached and left that way until the
feature branch is bound. The code puts it on an orca-worktree-<hash> branch
right after creating it, because a detached worktree records the literal
HEAD as the run's starting branch, which recovery refuses as an unsafe ref —
so an interrupted run could never be resumed.

The README and ADR 0018 already said this; only the spec still said the
opposite. It also now notes that the branch is never deleted either, so full
cleanup is two commands.
The shell asked two yes/no questions: create a branch, then — only if the
answer was yes — run in a worktree. That is one decision split in two, with
the second question silently disappearing depending on the first.

It also left the pair orca refuses (--worktree with --skip-branch)
expressible, prevented only by the order the questions were asked in and an
early return. Nothing in the types stopped a later edit from reordering
them.

Now one prompt with three rows: a new branch in this checkout (the default),
the branch checked out now, or a new worktree. RunTarget maps each to its
flags, and no case sets both, so the refused pair has no representation
here.

NewBranch leads the rows because ConsoleUiShell cannot honor preselect: the
first row is what the cursor starts on, so its position is what keeps Enter
doing what it did before.
The §3 amendment still said the scan spans the worktrees orca made for the
repository. It is now scoped to the checkout the shell runs in, matching
where the data lives.
@adamw
adamw merged commit 58affce into master Aug 28, 2026
6 checks passed
@adamw
adamw deleted the implement-worktree-runs branch August 28, 2026 10:06
adamw added a commit that referenced this pull request Sep 11, 2026
Closes the four doc and test gaps the review of #166 left open. Docs and
tests only — no behaviour change.

**`Worktrees`' object doc** talked about "the two reads", which the
object
has outgrown. It now states the rule for all of the reads, and names
`mainCheckoutOrReason` as the one that gives a reason instead of a blank
"not known".

**`WorktreeRun.resolve`'s doc** listed three of the messages a caller
can
get back. It lists all of them now, and says what each one means.

**Two of the three "no main checkout" messages had no test**: the one
for
a repository whose main worktree is not a checkout (`--separate-git-dir`
or a submodule), and the one for a git that does not answer the query.
The second is reached with a repository path holding a newline, which
makes git's answer unreadable in the same way an old git does.

**`headBranch` and `onABranch` answer "not on a branch" when a worktree
cannot be read at all**, which nothing tested. The new test deletes the
worktree directory first, so git really cannot be run there; it fails if
that fail-closed answer is taken out. os-lib prints the failed exec's
stack trace from a thread of its own, so the test log shows it while
passing — noted in the test.

The `--separate-git-dir` repo two tests were building inline is now
`GitRepo.seededSeparateGitDir`.

`sbt scalafmtAll` and `sbt test` are green: 2239 tests, 0 failures.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Supporting git.createWorktree?

1 participant