AGENT_HANDOFF.json clock-out artifact. The board is the dispatcher; the worktree is the durable world the dispatched work happens in. This guide composes Worktrees and handoff, the executor runner, and the Board API workspace routes into one workflow.
This guide drives the dispatch over REST (
POST /api/runtimes/:id/run) so the mechanics are explicit. In normal use the team orchestrator dispatches and resumes tasks for you; you don’t call these routes by hand. The point here is to see exactly how the pause/resume seam works.Prerequisites
- Two connected, worktree-capable runtimes. Of the five runtimes,
clawboo-native,claude-code,codex, andhermesdeclareworktrees: true; OpenClaw declaresworktrees: false(its agents run in Gateway-owned workspace dirs, so the runner never retargets them, and the executor runner refuses a connected substrate before it claims anything). See Connecting runtimes and the capability matrix. - A git repository on disk to branch from: its absolute
repoPath. - An existing board task in
todofor a file-mutating kind (code, the default). Read-only / research tasks (research,review) get no worktree, so there is nothing to hand off; provisioning one for them is refused with 422. See the board and Board API.
18790; substitute your resolved port (see Deployment).
The flow
Steps
1. Start the task on the first runtime
Dispatch the task on runtime A withrepoPath set and keepForResume: true. keepForResume is the load-bearing flag: it tells the runner to pause at the end of a successful run instead of completing. The kind defaults to code, which isolates to a worktree.
- Atomic claim. A single guarded UPDATE flips the task
todo → in_progress. A lost claim is a 409 and is never retried; it means another worker legitimately owns the task. - Worktree acquisition. Because
claude-codedeclarescapabilities().worktrees, the runner provisions a fresh isolated checkout on the branchclawboo/task-<id>, branched from a commit SHA (not the dirty tree), and writes + commits the system-of-record scaffold (TASK.md,task-progress.md,DECISIONS.json,init.sh,VERIFICATION.md). The worktree lives outside your repo, under the Clawboo state dir, namespaced by a hash of the repo path, so it never touches your repo’s owngit status. - Run. The runtime works in the checkout, mutating real files.
- Pause (because
keepForResume). On a successful terminal the runner writesAGENT_HANDOFF.jsoninto the worktree (withnextBestStepcarrying the run’s reported summary), then callsreleaseTask; the task drops back totodoand the worktree is kept. The verification gate does not run on a pause; it runs only on acomplete(step 4 below).
2. Inspect the handoff (optional)
The pause leaves a fully cold-resumable task. Confirm it with the cold-resume read:resume block is reconstructState’s output, computed purely from the worktree: AGENT_HANDOFF.json, falling back to task-progress.md + init.sh. No chat history, no board UI:
AGENT_HANDOFF.json is structured data, not prose; the next runtime parses it rather than interpreting English. Its runtime field is role-neutral and may be 'human', so a person can pick up (or hand off) the task from the exact same artifact. See Worktrees and handoff.3. Resume on a different runtime
Dispatch the sametaskId on runtime B. The task is back in todo, so runtime B claims it cleanly and reuses the kept worktree; this is the cross-runtime continuation path. Drop keepForResume to let runtime B finish for real.
acquireWorkspace finds the existing active worktree for the task and reuses it as-is rather than provisioning a new one. (If a garbage-collection sweep had reaped the directory, keeping the branch, the runner rebuilds the checkout from the retained branch before running, so a reused worktree is never a missing directory.) Either way it reconstructs the ResumeState from the worktree’s system-of-record and seeds the prompt’s context tier with the prior handoff, so runtime B starts from runtime A’s done / broken / next, not from scratch.
4. Let the resuming runtime complete
Because step 3 droppedkeepForResume, runtime B finishes the run through the normal completion path: the runner runs the worktree complete action, which decides clean-vs-retain by diffing the checkout against its baseline (the system-of-record bookkeeping files are excluded from that diff):
| Diff vs baseline | What happens | Terminal status |
|---|---|---|
| Empty | No deliverable: worktree + branch removed | done (verification gate intentionally bypassed) |
| Non-empty | Worktree retained, the verification gate runs (deterministic build/test/lint, plus an optional read-only critic) | pass → done · completed_with_debt over a green gate → done · debt over a red gate → blocked · fail → in_progress |
in_review → done. See Verification and the PATCH .../workspace complete action.
Variations
| You want… | Do this |
|---|---|
| Hand off to a human | Write AGENT_HANDOFF.json directly via POST /api/board/:taskId/workspace/handoff with runtime: "human". The person reads the worktree, runs ./init.sh to confirm the baseline, and works from done/broken/next. |
| Pause manually (no run) | PATCH /api/board/:taskId/workspace with {"action":"pause"} commits uncommitted work, drops the worktree directory, and keeps the branch, resumable. |
| Continue on the same runtime | If runtime B equals runtime A and the runtime supports native resume, the runner threads the persisted nativeSessionId into ctx.resume and resumes the exact native session. A cross-runtime pickup ignores nativeSessionId and resumes from the structured handoff alone; a session id is meaningless to a different runtime. |
| Pin the branch point | Pass baseSha (or baseRef) when provisioning via POST /api/board/:taskId/workspace; the default branch point is HEAD resolved to a full SHA. |
| Inspect the worktree’s files + diff | GET /api/board/:taskId/workspace/detail returns the system-of-record file contents and the unified diff against the baseline. |
Verify it worked
- After step 1,
GET /api/board/<task-id>shows the task back intodo(the pause released it) andGET /api/board/<task-id>/workspacereturnsresume.hasHandoff: truewithlastRuntimeset to runtime A. - After step 3, the task’s
assigneeRuntimereflects runtime B, and the run’s execution row (the cross-runtime ledger) records a second entry viaGET /api/board/:taskId/executions. - After step 4, the task lands in a terminal status (
done,in_review/blockedif a non-empty diff went through the verification gate, or back toin_progresson afailverdict).
Troubleshooting
A worktree is concurrency isolation, not a sandbox. It guarantees two parallel teammates can’t trample each other’s edits; it does not confine what code can do (network, filesystem, syscalls). Untrusted or Docker-running work needs
container-tier isolation (a container or microVM), which the worktrees subsystem documents as an escalation point but never provisions. See Worktrees and handoff.Don’t retry a 409 on claim or status change. A 409 from the claim (or an
illegal_transition on a status PATCH) is data, not a transient error; the task is simply taken or the transition is illegal. A dead in_progress task is recovered by the board’s orphan reconciliation, which releases it to todo for a normal re-claim. See the board.Related
- Worktrees and handoff: the system-of-record +
AGENT_HANDOFF.jsonmodel - The executor runner: claim → worktree → run → verify → handoff, and the
keepForResumepause - Board API: the
/api/board/:taskId/workspace*routes - Runtimes API:
POST /api/runtimes/:id/run, the dispatch entry point - Verification: the builder≠judge gate the resuming runtime triggers on completion
- Connecting runtimes: bring
claude-code/codex/hermes/clawboo-nativeonline - Multi-runtime team guide: build a team mixing runtimes
- Glossary: canonical term definitions