fleet ops · · 5 min

Why every delegated task gets its own worktree

A delegated task is a scope, a worktree and an acceptance command. The worktree is the part we will not trade away, and the reason is whatever the resident agent happens to be doing at the same moment.

The resident agent in a repository is never at rest at the moment you delegate. It has three files half-edited toward a refactor it has not finished describing, a test run about to start, and a branch with four commits that has not been pushed anywhere. That is the normal state of a working session, not a mess waiting to be cleaned up. Now hand a second worker the same checkout. It inherits all of it: the half-edited files as if they were the intended input, the test run as a competitor for the same output directory, the branch as a place to add commits nobody asked for. When the worker fails you cannot tell whether it was wrong or whether it read a file mid-thought. When it succeeds you cannot tell what it succeeded against.

So every repo_write task runs in its own git worktree, on its own branch, under the delegation state directory as worktrees/workspace-<id>/task-<id>/attempt-<n>. The resident keeps the main checkout. A task is three things — a scope, a worktree and an acceptance command — and the worktree is the one that cannot be traded away, because the other two only mean anything once the starting state is fixed.

What the worker actually sees

A worker starts exactly at base_ref (usually HEAD) or at an explicit base_sha, and at nothing else. Files that exist only as uncommitted changes in the primary checkout are invisible to it. So is another task’s worktree, and so is any unmerged commit. Every path, file or commit named in the instructions has to be reachable from the pinned base, or the instructions describe something that does not exist.

That constraint lands on the delegating agent, not on the worker. When the primary checkout has to stay dirty — it usually does — the way to pass state along is an immutable snapshot commit whose full SHA becomes base_sha, which is authoritative whenever it is present. What is not allowed is the shortcut. Never stash, git clean, reset the index, or auto-commit unrelated changes merely to make a delegation possible; the working tree belongs to the session and the person in it, not to the scheduler. If the inputs cannot be represented by a safe commit, the work stays where it is.

The scope and the check

The rest of the task is a JSON contract: objective and instructions, task_class, write_policy, the base, allowed_paths, acceptance, routing, timeouts, max_attempts, integration_policy, depends_on.

{
  "title": "Add auth route integration tests",
  "write_policy": "repo_write",
  "base_ref": "HEAD",
  "allowed_paths": ["internal/auth/*_test.go"],
  "acceptance": [
    { "kind": "command", "command": ["go", "test", "./internal/auth/..."], "timeout_seconds": 180, "expected_exit_code": 0 }
  ],
  "integration_policy": "branch_only"
}

allowed_paths are the globs the worker may write, and the full diff is inspected against them afterwards. They are a cage, and a cage is only as good as its fit. The failure pattern worth naming: a worker given allowed_paths it cannot satisfy — the fix it needs is one directory over — either fails outright or does the wrong thing inside the cage, and the wasted attempt is attributed to the agent that wrote the contract, not the one that ran it. A scope you cannot draw precisely is not a task. Open-ended work — investigate this, decide an approach, clean up that area — goes to a resident agent as a message instead.

acceptance is a list of command checks, each given as an argv array rather than a shell string, each with its own timeout_seconds and expected_exit_code. It runs after the worker finishes, and only the authoritative task state completed, recorded after those checks, counts as done. A process exit code is not completion, and worker prose is certainly not. Every repo_write task also gets an automatic committed-diff whitespace gate on top of whatever you declared. Secrets stay out of all of it: they never travel in a task payload, prompt or log, and are referenced by path out of the keychain or environment.

# create the task from its contract file
prod task create --file tasks/auth-tests.json --json

# state, acceptance output, changed files
prod task show --id 2483 --json

# the same whitespace gate the server runs, base commit to result commit
git diff --check "$BASE_SHA".."$RESULT_SHA"

# after reading the diff, exactly one disposition
prod task integration set --id 2483 --status integrated --target-ref main --json

What happens when the task ends

A successful result is a commit on the task branch, and it is never merged automatically: integration_policy is branch_only. The primary agent reads the changed-file list and the full diff, confirms the writes stayed inside allowed_paths, reads the acceptance output, runs its own checks, then records exactly one integration disposition — integrated, rejected or superseded — and one verdict on the worker, which is a separate ledger.

Then the worktree starts aging out. Completed ones are kept seven days by default, failed ones fourteen, and a repo_write result is fail-closed past that deadline: cleanup will not touch it until the integration ledger says something. Cleanup also waits on unread inbox items, dependencies and active attempts before it removes the git worktree, the temporary branch and the directory. Right after an attempt goes terminal and the result is captured, the server prunes rebuildable, git-ignored dependency and generated-output directories inside the worktree. Source files, the result commit, the task branch and declared artifacts stay.

Isolation is also what makes the optional review campaign possible. After deterministic acceptance, reviewer roles — correctness, tests, security, maintainability, documentation, general — each run in a private disposable copy of the exact candidate commit. A reviewer may edit files, compile and run tests in that copy to check a claim before making it, and then the copy is discarded. The candidate worktree is verified unchanged across the whole review: HEAD, dirty and untracked paths, index identity, every byte of every observed path. Mutating it is a reviewer_write_violation, and only the original worker repairs blocking findings. Reviewers that can run the code but cannot touch the artifact are only affordable because a worktree is cheap.

When it still runs here

The contract is a filter as much as a mechanism, and applying it honestly often ends with nothing delegated. Work stays in the primary session when it is a quick edit that costs less than describing it, when the requirements are too vague for allowed_paths to be drawn, when two scopes would overlap, when it depends on uncommitted state outside its base that no safe commit can capture, and when there is no credible acceptance check that could establish success.

This post sits close to that last line. Its acceptance checks confirm that the site builds and the frontmatter validates against the schema; nothing in them can tell you whether the prose is any good. That gap is carried by routing, not by acceptance, and it is the contract working, not a limitation of it. A delegated task exists when the base can be pinned, the scope can be drawn and a command can decide. When one of the three cannot be written down, the answer is not a looser task — it is the resident agent doing the work in the checkout it already has, where someone is watching.