Motivation
Two coding agents in one checkout write over each other, and a run that starts on a checkout someone else has edited builds on sand. A Git worktree per writer is the fix people already use by hand. The workspace contract does it inside the run: it records what the repository looked like, refuses to fork if that changed, gives one writer at a time the right to fork, and lands the work back when the step passes.Lifecycle
- Capture.
capture()records the repository’s revision and the state of its files, as an anchor. Pass Git pathspecs to limit it to the files a step may touch. - Verify.
verify(anchor)reads the repository again and reports any changed revision or path. A write you didn’t make shows up here, and you decide whether the run pauses. - Lease.
acquireLease()gives one writer the right to fork that anchor. The lease lives in a private Git ref, so a second writer is refused until the first releases it, and a delayed release can’t remove a later owner’s record. - Fork.
fork(anchor, child, token)creates a child branch and worktree beside the repository, at the anchor’s revision. A fork without a valid lease, or after the anchor changed, fails with a named kind instead of running on stale files. - Land back.
isolated(job)wraps any step in that cycle: it runs the step in its own worktree on a fork branch and, on pass, commits what the step left and merges the branch back, one merge at a time across the process. When the step ends, its worktree is removed and its branch deleted.
What a run leaves behind
Nothing, in most cases. When a step that runs in its own worktree ends, the run removes the worktree and deletes the step’s branch. This holds forisolated(), for a dag() node with isolate: true or the dag’s
isolation: 'worktree', and for each tournament() candidate. It holds
when the step passes, fails, is sent back by a review, or loses a
tournament.
Three things stay:
- An interrupted attempt. If the process stops while a step runs in its worktree, the worktree and its branch stay, so a person can recover the work by hand. The record says what a resume does with that step.
- A winner that could not merge. If a tournament’s winning candidate can’t merge, its branch stays so the work isn’t lost. Its worktree is removed.
- A step that threw. If a step throws instead of returning an outcome, it may have committed work that never landed, so its branch stays and the run logs a warning naming it. Its worktree is removed.
discarded. Until
Git prunes that commit, git branch <name> <sha> brings the branch back.
If a removal or a deletion fails, the run logs a warning, and the step’s
outcome stays the same.
Example
Capture, verify, lease and fork a fresh repository, then read the child:examples/workspace.ts (excerpt)
Example record
Limits
- One repository. The built-in provider captures one Git repository.
- Not a sandbox. Nothing stops a program outside the run from writing to the checkout. Verify reports that it happened; it doesn’t prevent it.
- You decide what a drift means. Verify reports changed paths. Pausing the run, and recording any branch or merge, is your program’s call. The graph executor doesn’t capture, verify or lease a workspace for you.
- No orphan cleanup. The provider can retry an incomplete fork, but it doesn’t remove abandoned child refs or worktrees.