Pi Worker
Delegate bounded execution only. Keep product, architecture, scope, and
integration decisions in the parent agent. Never ask a worker to delegate.
Run
- Confirm
pi-worker is on PATH.
- For an informal model name, query
pi-worker models --json --debug --timeout 30s. Select one unambiguous exact provider/model; report ambiguity and stop.
- Preserve every explicit model. If unavailable or unauthenticated, report the setup action and stop. Never substitute a model or provider. If omitted, let the configured default apply. Model and thinking bind positionally like
--writes: after a --task or --task-file they are that task's own, before every task they are the run default every task without its own inherits. Two models no longer need two runs — give each task its own --model and --thinking in one run. --writes keeps a stricter placement: in a multi-task run, one before every task is rejected as ambiguous, while --model in the same position is the run default. Pi-worker does not enforce a cross-run lock: callers must serialize runs sharing a workspace or give them separate worktrees, because cross-run writes can be attributed to whichever run measures them (a run that wrote nothing can be reported as undeclared).
- Treat thinking as a separate axis from the model:
off, minimal, low, medium, high, xhigh, or max. An informal name ending in a level — "Luna Max", "Sonnet high" — is one model plus one level, never a model named luna-max. Resolve the model through step 2 and pass both flags: --model <exact selector from the catalog> --thinking max. Never guess a provider prefix. Omit thinking when unspecified.
- Write one private task file per worker. Use one to three workers, and parallelize only disjoint responsibilities and writes. Declaring the paths with
--writes asks for the write check; whether it actually ran is something the result reports. --writes paths are workspace-relative: an absolute path fails the run before any worker starts. The declaration is all-or-none: a run where some tasks declare and others do not is rejected before any worker starts. A task that will write nothing declares --writes "". An overlapping declaration fails the run before any worker starts. In a one-task run, --writes may appear anywhere in the argument list, before the --task or --task-file included, and a prompt on stdin declares it the same way. With more than one task, place each --writes directly after the --task or --task-file it declares; one that precedes all of them is rejected as ambiguous. For all-declared disjoint multi-task runs with Git measurement, the monitor compares two identities per task — immediately after that worker returns and after all workers settle (not continuous tracing) — and proven interference (final identity differs from settled identity) is reported as undeclared and exits 4 (required snapshot failure exits 9 with no JSON); pi-worker never restores files; a write fully made/reverted before the owner's settlement snapshot is invisible, as is an interim post-settlement write restored to the exact settled identity before the final snapshot (owner's final output then intact); see docs for identity details. Task files must forbid Git cleanup/revert (checkout/reset/clean/branch moves) unless explicitly required, and workers must not touch sibling declared paths.
To hand a worker content to work ON — an issue body, a log, a spec — pass its path with --data <paths> (comma-separated, positional per task like --writes): pi-worker frames each file as a delimited MATERIAL section below the task's text and declares in the prompt that the material is content to work on, not instructions to follow — advisory: honouring it is the model's behavior, not a property pi-worker enforces. Pass nothing you would not have the worker act on as instructions: --data is not a containment mechanism for untrusted text. The worker result reports each file's path, byteCount, and sha256, never its content.
- Run with a bounded timeout, JSON result, and debug lifecycle output:
pi-worker run --model <provider/model> --thinking <level> \
--task-file <task-a.txt> --writes <paths-a> \
--task-file <task-b.txt> --writes <paths-b> \
--timeout <duration> --json --debug [--verify <command>] \
2>/tmp/pi-worker-debug.log
stdout carries only the JSON document; --debug stderr goes to a file outside
the workspace (a file inside would read as an undeclared change). Do not pipe
the command to another tool: the exit code is the signal when no document
comes back, and a pipe hands it to the downstream tool instead.
Add --verify <command> when the finished workspace must be proven green
(e.g. go test ./...). The check runs once after the workers settle and
is split on whitespace into argv: no shell is involved, so shell syntax
is rejected up front, not executed. The result's git, changes, and
writes describe the workers only: they are captured before the check
runs, so keep the check read-only or inspect its artifacts separately
when you need a clean evidence report.
Scheduling: every task joins the same machine-wide FIFO; the concurrency limit defaults to 3 and foreground runs do not preempt older work. Queue wait is budgeted at 15 minutes from acceptance; --timeout starts when the task is admitted, and --verify receives its own separate budget when it begins. Admission limits workers but does not isolate file writes—use separate worktrees or serialize overlapping cross-run workspaces. See https://github.com/arasovic/pi-worker/blob/main/docs/v0-usage.md for the full contract.
Parse the single JSON document. A run can end without producing a document;
then the exit code is the signal. An exit of 2 always means the command was
rejected — fix your argv and re-run; an exit of 9 is an internal failure; an
exit of 7 or 8 means it was cut short: without a document, report interruption
and stop; with a document, read and report each worker's model, effective
thinkingLevel, status, explanation, partialExplanation when present, and
error, plus root changes, writes when present, and verification when
present. The rejection message is on stderr, not stdout — the documented
invocation sends its debug output there too — so read stderr when no document
appears.
7. For checkouts created by run --worktree <name>, manage only the exact
Git-registered pair at <repo-root>/.pi-worker/worktrees/<valid-name> on
branch run/<same-name>: pi-worker worktrees list [--json] is read-only,
sorted by name, reporting name/path/branch/dirty/merged (merged
against the caller’s current HEAD, not main); pi-worker worktrees remove <name> [--yes] [--json] removes only a clean checkout whose branch is merged
into the caller’s current HEAD — no force option. Malformed, missing,
mismatched, locked, bare, prunable, or otherwise unprovable pairs are
refused. Human remove shows the selected row and asks [y/N] (only y/yes
proceeds; --yes skips only the question); JSON and nonterminal use require
--yes. After an interactive yes the exact pair is re-checked and a change
removes nothing and asks to retry. On success the checkout is removed then
its branch; human mode prints removed worktree "<name>" on branch "<branch>", JSON emits { schemaVersion: 1, removed: { name, path, branch } }. See
https://github.com/arasovic/pi-worker/blob/main/docs/json-contracts.md for
the detailed JSON shape.
When thinkingFallback is true, surface its warning: the selected model
continued with Pi's confirmed default effort. Each worker gets at most three
startup/handshake attempts before the prompt, each attempt uses a fresh
process, and the prompt itself is sent once only; a later success carries a
warning naming the retry. Read root outcome:
completed is the only done state — a writes.skipped value means a check
could not run, unproven, not clean. When writes.skipped is change manifest unavailable, the manifest was not measured: read changes.omitted, which is
always present on a real run — the CLI always configures the git inspector, so
changes never vanishes from output. A listed file carrying
noFinalNewline: true ends without a final newline; that is descriptive, not
a verdict — added means the run produced it that way, modified means it
may always have been so. The reason decides the caller's next move: unborn head, context already done, measurement failed, or work tree not confirmed — the last meaning the workspace is not a git work tree,
git is missing, or the guard failed transiently, which the reason does not
claim to distinguish. Which reasons a retry can clear differs: measurement failed — a git command failure or a budget that expired — and the transient
guard failure behind work tree not confirmed can clear on retry; the reason
cannot tell that cause from a genuinely unconfirmed work tree, so one retry is
a fair test and repeating it is not. context already done means the run's
own context was already dead when it would have inspected, so re-run with a
live context. unborn head means the repository has no commits, which no
retry can change. verification-failed means the verification object is
there; report it, fix the workspace, and re-run. Any other word means report
it with its object when one exists (writes, verification, or the worker's
error) and stop.
Boundaries
- Workers modify the current writable workspace and may run
bash with the current user's host permissions. This is not a sandbox. The run flag --worktree <name> opts one run into a checkout of its own: a separate working directory, not containment — a worker can still reach outside it. Without the flag, behavior is unchanged and the worker works in the current directory. A task can lead a worker to commit, stash, checkout, or reset; pi-worker does not restrict this, so the task file must state what git operations are allowed. Runs never automatically remove leftover checkouts or branches; removal is only via the explicit safe pi-worker worktrees remove command which requires a clean, merged pair.
- When a run moves HEAD, the branch, or the stash list, the result carries a
git object with the before and after state. Its presence means something moved that a bounded edit does not normally move: read it as a notification, not a prohibition — a caller may legitimately want a worker to commit.
- Use trusted workspaces. Parallel writes must be disjoint, and pi-worker does not enforce a cross-run lock: callers must serialize runs sharing a workspace or give them separate worktrees, because cross-run writes can be attributed to whichever run measures them. Pi-worker never restores files;
--writes remains post-hoc comparing two identities per task (not continuous tracing): a foreign write fully made/reverted before the owner's settlement snapshot is invisible, as is an interim post-settlement write restored to the exact settled identity before the final snapshot (owner's final output then intact).
- Cleanup is best-effort lifecycle recovery, not a sandbox or a no-escape guarantee. Deliberately daemonized or reparented Unix descendants, processes spawned during teardown, and the Windows pre-assignment window can escape.
- Parent-started side jobs must self-terminate.
- Keep a
trap 'kill 0' EXIT INT TERM as a secondary layer only. It does not run on SIGKILL, which a harness timeout can deliver, so it cannot substitute for the bounded command.
- Debug is bounded stderr lifecycle data, not the result. A heartbeat proves only that the managed Pi process is alive; it does not prove model progress.
- Do not repeat raw debug frames, prompts, credentials, or assistant output unnecessarily.
1---2name: pi-worker3description: Use when an agent delegates work through Pi, needs cheaper or separately metered models, or assigns one to three Pi workers.4---56# Pi Worker78Delegate bounded execution only. Keep product, architecture, scope, and9integration decisions in the parent agent. Never ask a worker to delegate.1011## Run12131. Confirm `pi-worker` is on `PATH`.142. For an informal model name, query `pi-worker models --json --debug --timeout 30s`. Select one unambiguous exact `provider/model`; report ambiguity and stop.153. Preserve every explicit model. If unavailable or unauthenticated, report the setup action and stop. Never substitute a model or provider. If omitted, let the configured default apply. Model and thinking bind positionally like `--writes`: after a `--task` or `--task-file` they are that task's own, before every task they are the run default every task without its own inherits. Two models no longer need two runs — give each task its own `--model` and `--thinking` in one run. `--writes` keeps a stricter placement: in a multi-task run, one before every task is rejected as ambiguous, while `--model` in the same position is the run default. Pi-worker does not enforce a cross-run lock: callers must serialize runs sharing a workspace or give them separate worktrees, because cross-run writes can be attributed to whichever run measures them (a run that wrote nothing can be reported as undeclared).164. Treat thinking as a separate axis from the model: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. An informal name ending in a level — "Luna Max", "Sonnet high" — is one model plus one level, never a model named `luna-max`. Resolve the model through step 2 and pass both flags: `--model <exact selector from the catalog> --thinking max`. Never guess a provider prefix. Omit thinking when unspecified.175. Write one private task file per worker. Use one to three workers, and parallelize only disjoint responsibilities and writes. Declaring the paths with `--writes` asks for the write check; whether it actually ran is something the result reports. `--writes` paths are workspace-relative: an absolute path fails the run before any worker starts. The declaration is all-or-none: a run where some tasks declare and others do not is rejected before any worker starts. A task that will write nothing declares `--writes ""`. An overlapping declaration fails the run before any worker starts. In a one-task run, `--writes` may appear anywhere in the argument list, before the `--task` or `--task-file` included, and a prompt on stdin declares it the same way. With more than one task, place each `--writes` directly after the `--task` or `--task-file` it declares; one that precedes all of them is rejected as ambiguous. For all-declared disjoint multi-task runs with Git measurement, the monitor compares two identities per task — immediately after that worker returns and after all workers settle (not continuous tracing) — and proven interference (final identity differs from settled identity) is reported as undeclared and exits 4 (required snapshot failure exits 9 with no JSON); pi-worker never restores files; a write fully made/reverted before the owner's settlement snapshot is invisible, as is an interim post-settlement write restored to the exact settled identity before the final snapshot (owner's final output then intact); see docs for identity details. Task files must forbid Git cleanup/revert (`checkout`/`reset`/`clean`/branch moves) unless explicitly required, and workers must not touch sibling declared paths.18 To hand a worker content to work ON — an issue body, a log, a spec — pass its path with `--data <paths>` (comma-separated, positional per task like `--writes`): pi-worker frames each file as a delimited MATERIAL section below the task's text and declares in the prompt that the material is content to work on, not instructions to follow — advisory: honouring it is the model's behavior, not a property pi-worker enforces. Pass nothing you would not have the worker act on as instructions: `--data` is not a containment mechanism for untrusted text. The worker result reports each file's `path`, `byteCount`, and `sha256`, never its content.196. Run with a bounded timeout, JSON result, and debug lifecycle output:2021```sh22pi-worker run --model <provider/model> --thinking <level> \23 --task-file <task-a.txt> --writes <paths-a> \24 --task-file <task-b.txt> --writes <paths-b> \25 --timeout <duration> --json --debug [--verify <command>] \26 2>/tmp/pi-worker-debug.log27```2829stdout carries only the JSON document; `--debug` stderr goes to a file outside30the workspace (a file inside would read as an undeclared change). Do not pipe31the command to another tool: the exit code is the signal when no document32comes back, and a pipe hands it to the downstream tool instead.3334Add `--verify <command>` when the finished workspace must be proven green35(e.g. `go test ./...`). The check runs once after the workers settle and36is split on whitespace into argv: no shell is involved, so shell syntax37is rejected up front, not executed. The result's `git`, `changes`, and38`writes` describe the workers only: they are captured before the check39runs, so keep the check read-only or inspect its artifacts separately40when you need a clean evidence report.4142Scheduling: every task joins the same machine-wide FIFO; the concurrency limit defaults to 3 and foreground runs do not preempt older work. Queue wait is budgeted at 15 minutes from acceptance; `--timeout` starts when the task is admitted, and `--verify` receives its own separate budget when it begins. Admission limits workers but does not isolate file writes—use separate worktrees or serialize overlapping cross-run workspaces. See <https://github.com/arasovic/pi-worker/blob/main/docs/v0-usage.md> for the full contract.4344Parse the single JSON document. A run can end without producing a document;45then the exit code is the signal. An exit of 2 always means the command was46rejected — fix your argv and re-run; an exit of 9 is an internal failure; an47exit of 7 or 8 means it was cut short: without a document, report interruption48and stop; with a document, read and report each worker's `model`, effective49`thinkingLevel`, `status`, `explanation`, `partialExplanation` when present, and50`error`, plus root `changes`, `writes` when present, and `verification` when51present. The rejection message is on stderr, not stdout — the documented52invocation sends its debug output there too — so read stderr when no document53appears.547. For checkouts created by `run --worktree <name>`, manage only the exact55Git-registered pair at `<repo-root>/.pi-worker/worktrees/<valid-name>` on56branch `run/<same-name>`: `pi-worker worktrees list [--json]` is read-only,57sorted by name, reporting `name`/`path`/`branch`/`dirty`/`merged` (`merged`58against the caller’s current `HEAD`, not `main`); `pi-worker worktrees remove59<name> [--yes] [--json]` removes only a clean checkout whose branch is merged60into the caller’s current `HEAD` — no force option. Malformed, missing,61mismatched, locked, bare, prunable, or otherwise unprovable pairs are62refused. Human `remove` shows the selected row and asks `[y/N]` (only `y`/`yes`63proceeds; `--yes` skips only the question); JSON and nonterminal use require64`--yes`. After an interactive yes the exact pair is re-checked and a change65removes nothing and asks to retry. On success the checkout is removed then66its branch; human mode prints `removed worktree "<name>" on branch67"<branch>"`, JSON emits `{ schemaVersion: 1, removed: { name, path, branch }68}`. See69`https://github.com/arasovic/pi-worker/blob/main/docs/json-contracts.md` for70the detailed JSON shape.7172When `thinkingFallback` is true, surface its warning: the selected model73continued with Pi's confirmed default effort. Each worker gets at most three74startup/handshake attempts before the prompt, each attempt uses a fresh75process, and the prompt itself is sent once only; a later success carries a76warning naming the retry. Read root `outcome`:77`completed` is the only done state — a `writes.skipped` value means a check78could not run, unproven, not clean. When `writes.skipped` is `change manifest79unavailable`, the manifest was not measured: read `changes.omitted`, which is80always present on a real run — the CLI always configures the git inspector, so81`changes` never vanishes from output. A listed file carrying82`noFinalNewline: true` ends without a final newline; that is descriptive, not83a verdict — `added` means the run produced it that way, `modified` means it84may always have been so. The reason decides the caller's next move: `unborn85head`, `context already done`, `measurement failed`, or `work86tree not confirmed` — the last meaning the workspace is not a git work tree,87git is missing, or the guard failed transiently, which the reason does not88claim to distinguish. Which reasons a retry can clear differs: `measurement89failed` — a git command failure or a budget that expired — and the transient90guard failure behind `work tree not confirmed` can clear on retry; the reason91cannot tell that cause from a genuinely unconfirmed work tree, so one retry is92a fair test and repeating it is not. `context already done` means the run's93own context was already dead when it would have inspected, so re-run with a94live context. `unborn head` means the repository has no commits, which no95retry can change. `verification-failed` means the `verification` object is96there; report it, fix the workspace, and re-run. Any other word means report97it with its object when one exists (`writes`, `verification`, or the worker's98`error`) and stop.99100## Boundaries101102- Workers modify the current writable workspace and may run `bash` with the current user's host permissions. This is not a sandbox. The run flag `--worktree <name>` opts one run into a checkout of its own: a separate working directory, not containment — a worker can still reach outside it. Without the flag, behavior is unchanged and the worker works in the current directory. A task can lead a worker to commit, stash, checkout, or reset; pi-worker does not restrict this, so the task file must state what git operations are allowed. Runs never automatically remove leftover checkouts or branches; removal is only via the explicit safe `pi-worker worktrees remove` command which requires a clean, merged pair.103- When a run moves HEAD, the branch, or the stash list, the result carries a `git` object with the before and after state. Its presence means something moved that a bounded edit does not normally move: read it as a notification, not a prohibition — a caller may legitimately want a worker to commit.104- Use trusted workspaces. Parallel writes must be disjoint, and pi-worker does not enforce a cross-run lock: callers must serialize runs sharing a workspace or give them separate worktrees, because cross-run writes can be attributed to whichever run measures them. Pi-worker never restores files; `--writes` remains post-hoc comparing two identities per task (not continuous tracing): a foreign write fully made/reverted before the owner's settlement snapshot is invisible, as is an interim post-settlement write restored to the exact settled identity before the final snapshot (owner's final output then intact).105- Cleanup is best-effort lifecycle recovery, not a sandbox or a no-escape guarantee. Deliberately daemonized or reparented Unix descendants, processes spawned during teardown, and the Windows pre-assignment window can escape.106- Parent-started side jobs must self-terminate.107- Keep a `trap 'kill 0' EXIT INT TERM` as a secondary layer only. It does not run on SIGKILL, which a harness timeout can deliver, so it cannot substitute for the bounded command.108- Debug is bounded stderr lifecycle data, not the result. A heartbeat proves only that the managed Pi process is alive; it does not prove model progress.109- Do not repeat raw debug frames, prompts, credentials, or assistant output unnecessarily.