prun ยท diff

git:20260913.050ee0b to git:20260914.e26fafb

84 added, 127 removed. Audit A to A.

---
name: prun
- description: Parallel delegation fan-out. The coordinating session decomposes and integrates while task units run in parallel on workers, never on the coordinator. Sonnet is the in-session executor for anything that needs session tools. Agy (Gemini through the Antigravity CLI) takes the larger share of the rest; it runs unattended in a scratch dir or throwaway clone and gets follow-up turns while slower units finish. Codex is not a prun executor. Unit count follows the dependency graph rather than a small fixed cap. Units may read or write code; workers never commit or push, and the session plus the user are the final integration gate.
+ description: Parallel delegation fan-out on Agy. The coordinating session decomposes and integrates while task units run in parallel as Agy processes (Gemini through the Antigravity CLI), never on the coordinator and never on Claude-side workers such as Sonnet subagents. Each unit runs unattended in a scratch dir or throwaway clone and gets follow-up turns while slower units finish. Codex is not a prun executor either. Unit count follows the dependency graph rather than a small fixed cap. Units may read or write code; workers never commit or push, and the session plus the user are the final integration gate.
---
# prun (parallel run)
## Overview
`prun` fans a task out into independent units that run in parallel while the current session only
- coordinates. Workers are **Sonnet** subagents inside a Claude session and **Agy** processes running
- Gemini through the Antigravity CLI. Sonnet is the in-session executor and the only route to session
- tools. Agy supplies the independent Google model family, a separate subscription pool, and the
- faster turnaround, so it carries the larger share of ordinary units. **Codex is not a prun executor**;
- reserve its higher-cost quota for the default `/vet` gatekeeper role. The coordinator decomposes the
- task, dispatches the units, gathers their results, reviews their diffs, and integrates. It never
- runs a unit itself.
+ coordinates. **Every worker is an Agy process** running Gemini through the Antigravity CLI, on the
+ Google AI plan authenticated in `agy`. The coordinator decomposes the task, dispatches the units,
+ gathers their results, reviews their diffs, and integrates. It never runs a unit itself.
- The orchestrator picks the executor per unit: session-internal tools go to Sonnet, and Agy takes
- the larger share of everything else while its pool is healthy. Sonnet units share the current
- Claude account. Agy units use the Google AI plan authenticated in `agy`. Exact plan buckets can
- change, so inspect current quota before a large batch.
+ **No Claude-side workers.** A Sonnet subagent, a Workflow agent, or any other Agent-tool worker draws
+ on the same Claude account as the coordinating session, so a fan-out of them spends that account's
+ quota at the rate of the fan-out. That is the pool the coordinating session itself needs, and it
+ drained fast once `prun` routed units to Sonnet. **Codex is not a prun executor** either; its
+ higher-cost quota is reserved for the default `/vet` gatekeeper role. Exact plan buckets can change,
+ so inspect current Agy quota before a large batch.
## Relationship to the native Workflow tool
The native Workflow tool fans a task out across **Claude** subagents under a deterministic script,
with structured output, judge panels, and resume. A Workflow run counts against the Anthropic plan's
usage and rate limits, and its agents use the session model unless the script routes a stage to a
different Claude model.
- `prun` supports two account paths. A **Sonnet** unit shares the current Claude account with the
- coordinating session. An **Agy** unit runs Gemini through an authenticated Antigravity CLI and uses
- the Google AI plan attached to it. Use the Executors rule below to choose each unit's executor.
- The coordinating session also spends a small Anthropic amount while it
- decomposes, dispatches, reads results, and integrates.
-
- The two relate in two ways, both with the current session as the orchestrator:
+ `prun` is the fan-out that stays off that account. Its units run on Agy and use the Google AI plan;
+ the coordinating session spends only the small Anthropic amount it needs to decompose, dispatch,
+ read results, and integrate. `prun` therefore never starts a Workflow or a Claude subagent, not as a
+ unit, a fallback, or a second panel. When the user explicitly asks for a Claude panel, that is a
+ Workflow run the user asked for, and it happens outside `prun`. A cross-vendor read on staged work
+ is what `/vet` is for.
- - **Substitute (quota).** Read both pools with `agent-quota`, including snapshot age and reset
- times. If Claude cannot accommodate the next batch, route suitable units to Agy or defer them.
- Do not silently shrink a genuinely parallel task to an arbitrary two or three workers. A quota
- reading does not establish that the other account can finish the batch.
- - **Complement (diversity).** When a Workflow is affordable and the user's request or an
- applicable project requirement calls for a cross-vendor perspective, run a Claude panel through
- the Workflow and a Gemini panel through Agy. Use the same structured
- contract and the same question on both sides, then cross-check. Agreement across vendors is usually
- a stronger signal than agreement inside one model family, because shared model lineage and tools
- can share blind spots. Invoke them together in one natural-language request; no special mode is
- needed. Reserve this for high-stakes work (a review, an audit, a hard design call), since it spends
- both pools and the coordinator must merge two result sets.
+ When the Agy Gemini group cannot accommodate the next batch, queue or defer units instead of moving
+ them onto the Claude account. Read the meter with `agent-quota`, including snapshot age and reset
+ times, and do not silently shrink a genuinely parallel task to an arbitrary two or three workers.
+ The dispatcher's own quota route, described under dispatch-task usage, already stops a unit from
+ launching into an empty group.
## When to use
Use `prun` when the task splits into **independent units that can run at once** (different
modules, separate research questions, parallel analyses). Units may be heterogeneous, and there
can be **many of them**: a dozen or twenty in parallel is normal when the task warrants it.
Do not use `prun` when the task is one sequential unit, or units depend on each other's output,
or a unit's result cannot be checked without redoing it.
## Executors
| Executor | Quota | Notes |
|---|---|---|
- | Sonnet subagent | Current Claude account; check Settings > Usage for the applicable limits or credits | **In-session executor.** The only route to session-internal tools (MCP / email / Artifacts), and the fallback when the Agy pool is the constraint. |
- | Agy (`agy`) | Google AI plan authenticated in Antigravity | **Takes the larger share.** Gemini 3.8 Flash High at `high` effort; fast, separately funded, and dispatched with full unattended tool permission inside a scratch dir or throwaway clone. |
+ | Agy (`agy`) | Google AI plan authenticated in Antigravity | **The only worker.** Gemini 3.8 Flash High at `high` effort; fast, separately funded, and dispatched with full unattended tool permission inside a scratch dir or throwaway clone. |
| Claude session (this session) | Current Claude account; check Settings > Usage for the applicable limits or credits | Coordinator and integrator only, on whatever model is selected. Never a unit. |
- Rule: **units never run on the coordinator (the Claude session itself).** The orchestrator picks the
- executor per unit:
+ Rules:
- - **Session-internal tools stay on Sonnet.** An external Agy process cannot use the coordinator's
- MCP, email connectors, or Artifact tool. That capability boundary is independent of quota. A
- Sonnet subagent inherits the session's available tools but starts with fresh context, so put all
- needed state in its unit prompt.
- - **Agy takes the larger share of everything else** while its pool is healthy: research,
- verification, extraction, cross-checks, and code-writing units in a throwaway clone. It is fast,
- its quota is separate from the Claude plan, and the dispatcher gives it the same unattended
- capability as the `/vet` Agy reviewer, so a unit can verify numbers, run experiments, and fetch
- the web. Agy defaults to `gemini-3.8-flash-high` at the CLI's maximum `high` effort. Route a
- unit to Sonnet instead when the Agy pool is the constraint, when the unit needs a Claude-side
- tool, or when the user or a project-local routing policy says so.
- - **Keep the Agy pool busy with follow-up turns.** Agy units usually return well before the Sonnet
- ones. When one returns while others are still running, dispatch a follow-up Agy unit rather than
- idling, provided the follow-up discharges real work: an acceptance criterion the result left
- open, a claim it made without evidence, a source it cited but did not fetch, a check it proposed
- but did not run, or the next independent unit in the queue. A slower sibling is not by itself a
- reason to invent work. `--continue-from <state-dir>` resumes the same conversation, so the
- follow-up keeps the earlier context; a fresh prompt with a fresh result path is the alternative.
- Record each follow-up in the ledger like any other unit.
+ - **Every unit runs on Agy.** Research, verification, extraction, cross-checks, and code-writing
+ units in a throwaway clone all go through `dispatch-task-agy`. The dispatcher gives a unit the
+ same unattended capability as the `/vet` Agy reviewer, so it can verify numbers, run experiments,
+ and fetch the web. Agy defaults to `gemini-3.8-flash-high` at the CLI's maximum `high` effort.
+ - **Never a Claude-side worker.** Do not spawn an Agent-tool subagent (Sonnet or any other model)
+ or a Workflow agent for a unit, including as a fallback when the Agy pool is short. Those workers
+ spend the coordinating session's own Claude account. When Agy cannot take a batch, queue it or
+ tell the user.
- **Codex is excluded from prun.** Its quota is intentionally reserved for the `/vet` reviewer
role. Do not route a prun unit to `codex exec`, even if a legacy dispatcher remains on disk for
compatibility with old state directories.
- - **When in doubt: session tools to Sonnet, everything else to Agy.** An explicit user instruction
- or a project-local routing policy may change the split for a run.
- - **The Claude session stays the coordinator, never a unit.** A single small session-tool action
- may stay inline; independent substantive work belongs in workers.
+ - **Session-internal tools stay outside the fan-out.** An Agy process cannot use the coordinator's
+ MCP, email connectors, or Artifact tool. Gather what a unit needs from those tools in the
+ coordinating session before dispatch, and put it in the unit prompt; leave a small action that
+ needs them to the coordinator as one inline step. A task whose substantive work needs those tools
+ throughout is not a `prun` task.
+ - **Keep the Agy pool busy with follow-up turns.** Units return at different times. When one returns
+ while others are still running, dispatch a follow-up unit rather than idling, provided the
+ follow-up discharges real work: an acceptance criterion the result left open, a claim it made
+ without evidence, a source it cited but did not fetch, a check it proposed but did not run, or
+ the next independent unit in the queue. A slower sibling is not by itself a reason to invent
+ work. `--continue-from <state-dir>` resumes the same conversation, so the follow-up keeps the
+ earlier context; a fresh prompt with a fresh result path is the alternative. Record each
+ follow-up in the ledger like any other unit.
+ - **The Claude session stays the coordinator, never a unit.** Independent substantive work belongs
+ in Agy workers.
- **Why the split is Sonnet plus Agy.** They draw on separate subscription pools and provide model-
- family diversity without spending the higher-cost Codex pool used by `/vet`. Agy carries the larger
- share because it is fast and its pool is large; that is a routing and cost decision, not a
- universal quality ranking, and the coordinator still reviews every result and every diff. Check
- current quota before a large batch, but do not convert changing meter readings into an arbitrary
- low worker cap.
+ **Why Agy alone.** Its pool is separate from the Claude plan, it is fast, and it adds an independent
+ model family without spending the higher-cost Codex pool used by `/vet`. The earlier split put
+ Sonnet beside Agy on the grounds that the two draw on separate pools. They do, but Sonnet's pool is
+ the coordinator's own Claude account, so every Sonnet worker spent the quota the coordinating
+ session runs on, and a wide fan-out consumed it quickly. The coordinator still reviews every result
+ and every diff. Check current quota before a large batch, but do not convert changing meter
+ readings into an arbitrary low worker cap.
## Concurrency
The orchestrator decides the unit count autonomously. Partition the task by **dependency
structure** (split only along genuinely independent boundaries) and **balanced workload**
(roughly equal-sized units, each worth a full worker run). High autonomy is the intent: do not
target a fixed number, and do not cap artificially. A dozen-plus in parallel is fine when the
task genuinely decomposes that way.
Two soft bounds, not hard rules: local CPU/RAM (enough concurrent workers eventually contend and
- the excess queues) and the headroom of the Claude and Agy pools. `agent-quota` reads current
- snapshots for both. The usual real ceiling is
+ the excess queues) and the headroom of the Agy pool. `agent-quota` reads the current snapshot of
+ both Agy groups. The usual real ceiling is
**integration bandwidth**, since the orchestrator must read and reconcile every result, so
prefer fewer well-scoped units over many tiny ones. Over-splitting into trivial units wastes
worker startup and tends to produce thin results. Dispatch in batches that fit the runtime's
concurrent-worker limit and the available quota, and leave the rest queued; a runtime's in-flight
limit is separate from how many units a run may have in total.
## What a unit may do, and the one rule
A unit may **read or write code**, run commands, and fetch the web, with full access. The single
hard rule: a worker **never commits, pushes, or runs destructive git** (`commit`, `push`,
branch/tag mutation, `reset --hard`, `clean`). Everything else is allowed. The final gate is
**the Claude session integrating the results and the user deciding**; workers never touch the real repo history.
This is enforced structurally, not by trust:
- **Read-only / research units** run from a per-unit scratch cwd, so accidental writes stay out of
the repo. `dispatch-task` does this by default.
- **Code-writing units** run inside a **throwaway local clone** of the repo with its remote removed:
```
git clone --local -c core.longpaths=true <repo> <clone-dir> # longpaths: Windows MAX_PATH safety
git -C <clone-dir> remote remove origin
```
The worker edits freely in the clone. An accidental `git push` has no remote to reach (GitHub /
Overleaf stay untouched); an accidental `git commit` only lands in the throwaway clone. The coordinator reads
`git -C <clone-dir> diff`, integrates the wanted changes into the real tree, and **the user
approves the actual commit**. That is the only gate.
No credential scrubbing or sandbox wall: the user writes the prompts, the clone has no path to the
real remotes, and the Claude session plus the user are the integration gate. That is the whole safety model.
## Flow
1. **Gate**: confirm the task splits into independent, checkable units. Else use a single worker.
2. **Decompose**: write one prompt per unit. State the task; for a code-writing unit, that the
working dir is a throwaway clone to edit freely but **not** commit or push; that the unit writes
a result summary to its result file (a fresh path, in one write).
- 3. **Assign**: apply the Executors rule (session tools to Sonnet, the larger share of the rest to
- Agy) and record the routing reason in the ledger. Also pick read-only (scratch) or code-writing
- (clone) mode. For a web-heavy unit, "Web access" below covers which executor fits.
- 4. **Dispatch in parallel**:
- - Agy unit: run `<python> scripts/dispatch-task-agy.py` in the background. With no `--mode`
- it runs `accept-edits` with `--dangerously-skip-permissions` in a scratch directory it
- creates. A caller-supplied workspace, meaning `PRUN_SCRATCH_CWD` (a throwaway clone for a
- code-writing unit) or `--add-dir` (a clone or snapshot the unit should see), requires an
- explicit `--mode accept-edits` or `--mode plan`, so the write-capable mode is a named choice
- for any directory the dispatcher did not create. `--continue-from <state-dir>` resumes an
- earlier unit's conversation for a follow-up turn.
- - Sonnet unit: spawn a background Agent subagent with `model: sonnet`. It inherits the session's
- available tools, including MCP and connector tools; if you set a `tools` allowlist, include every
- connector, Artifact, file, shell, and web tool the unit needs. The subagent starts with fresh
- context, so put any needed state in its prompt. For code-writing it works in a clone too, under
- Claude's `guard.py`, which already gates commit/push.
- 5. **Monitor (do not go idle)**: the shell monitor covers **Agy** units only. It reads the
- `dispatch-task-agy` state markers (`tail`, `dispatch-pid`, `result-file`) that a Sonnet Agent
- invocation never writes, and it takes no Agent identifier. For **Sonnet** units, track the Agent
- identifiers recorded in the ledger and use the runtime's own task-status and completion tools,
- checking again on a schedule while any unit is outstanding. Result-file validation and the
- step-6 reconciliation are common to both. For the Agy units, launch
- `scripts/monitor.{sh,ps1} <state-dir> ...` in the background
+ 3. **Assign**: every unit goes to Agy. Gather anything a unit needs from session-internal tools
+ first and write it into that unit's prompt. Pick read-only (scratch) or code-writing (clone)
+ mode, and record the mode in the ledger.
+ 4. **Dispatch in parallel**: run `<python> scripts/dispatch-task-agy.py` in the background for each
+ unit. With no `--mode` it runs `accept-edits` with `--dangerously-skip-permissions` in a scratch
+ directory it creates. A caller-supplied workspace, meaning `PRUN_SCRATCH_CWD` (a throwaway clone
+ for a code-writing unit) or `--add-dir` (a clone or snapshot the unit should see), requires an
+ explicit `--mode accept-edits` or `--mode plan`, so the write-capable mode is a named choice for
+ any directory the dispatcher did not create. `--continue-from <state-dir>` resumes an earlier
+ unit's conversation for a follow-up turn.
+ 5. **Monitor (do not go idle)**: launch `scripts/monitor.{sh,ps1} <state-dir> ...` in the background
(`run_in_background=true`) and wait on its completion. It wakes you on the first actionable event:
all done, any unit **stalled** (tail no-growth for `PRUN_STALL_THRESHOLD`, default 10 min), or any
unit **failed** (`FALLBACK` result or dead dispatch), printing a per-unit digest. On a stall,
surface it to the user with a likely cause (capacity or concurrency pressure; suggest lowering the
worker count or re-dispatching) rather than waiting silently; act, then re-launch the monitor on
the still-running units until all are done. `monitor` only observes. The Agy dispatcher relies on
the CLI's bounded `--print-timeout`; it does not scan for or terminate unrelated agent processes.
(`gather.{sh,ps1}` remains for the plain wait-for-all case.)
6. **Reconcile, then integrate**: before integrating, **reconcile the ledger**: every dispatched unit
must have a non-empty result. If any is missing or empty, do **not** integrate the partial set;
- recover an Agy worker's output from its `<state-dir>/tail` (dispatch-task-agy also salvages the
- tail into the result file automatically under a `FALLBACK` header), or retrieve a Sonnet
- worker's returned output through its recorded Agent identifier using the runtime's
- completion/output tools. If no usable result can be recovered, re-dispatch that unit or flag the
- user. Then the coordinator reads each result plus each clone's `git diff`, merges the wanted changes into
+ recover the worker's output from its `<state-dir>/tail` (dispatch-task-agy also salvages the
+ tail into the result file automatically under a `FALLBACK` header). If no usable result can be
+ recovered, re-dispatch that unit or flag the user. Then the coordinator reads each result plus
+ each clone's `git diff`, merges the wanted changes into
the real tree, runs verification, and **asks the user before any commit**.
Resolve scripts via this order, first hit wins: `skills/prun/scripts/`, then
`.claude/skills/prun/scripts/`, then `.agent-config/repo/skills/prun/scripts/`.
## dispatch-task usage (Agy)
```
<python> scripts/dispatch-task-agy.py --prompt-file <prompt> --result-file <fresh abs result> --unit-id <id>
```
- Emits exactly one stdout line `STATE-DIR <abs-path>`; Agy stream events and stderr land in the
state directory, the conversation id from Agy's `init` event is recorded to
`<state-dir>/conversation-id`, and the final response is published atomically to the result path.
- Defaults to `gemini-3.8-flash-high` at `high` effort. Override with
`ANTIGRAVITY_DISPATCH_MODEL` and `ANTIGRAVITY_DISPATCH_EFFORT`. Agy takes
`--effort` for its Gemini models only, so the dispatcher omits the flag for
the second group below rather than having Agy reject the whole call.
- Agy Ultra exposes a second quota group for `claude-sonnet-4-6`,
`claude-opus-4-6-thinking`, and `gpt-oss-120b-medium`. Keep Gemini as the
- default because it adds an independent model family to the Sonnet fan-out.
+ default: it is the larger group, and it keeps a unit's model family
+ independent of the Claude coordinator that reviews the result.
The second group is an explicit overflow option through
`ANTIGRAVITY_DISPATCH_MODEL`; it adds quota, not reviewer-family diversity.
**Naming it is the user's call.** A fan-out that reached for it on its own
spent 646 generations of that group in a day, on units a Gemini worker would
have taken, and the group is the smaller of the two.
- **The dispatcher checks group quota before launching.** The two groups are
metered separately, and one dispatch names one model, so a batch aimed at an
empty group fails once per unit: on 2026-09-11 four units of a seven-unit
fan-out died in a row, each carrying `Individual quota reached ... Resets in
34m`. Before launching, the dispatcher reads the snapshot `agent-quota`
maintains and decides:
- The Claude and GPT group is empty and Gemini is not: dispatch the Gemini
default instead, and record the swap. The line `MODEL-FALLBACK from=... to=...
reason=claude-and-gpt-quota-exhausted resets=...` goes to stderr and to
`<state-dir>/quota-note`. `<state-dir>/model` always names the model that
actually ran, so the ledger's executor column is not the model the caller
asked for when the two differ.
- The Gemini group is empty: exit `75` without launching, and say so. It does
not escalate into the metered group on its own; the message names
`ANTIGRAVITY_DISPATCH_MODEL` for the operator who wants that.
- Both groups are empty: exit `75` with both reset times.
- A group the snapshot does not report is unknown rather than empty, and an
unreadable snapshot skips the check entirely. The gate stops a dispatch
only into a group it read as empty. `PRUN_AGY_QUOTA_GATE=off` disables it.
A run that fails at the backend forces a snapshot refresh before exiting,
past the readout's own five-minute TTL, because the meter it just hit is
newer evidence than the snapshot. Later units then route on what it recorded.
This is not a guarantee: a refresh that cannot run, a meter that is
unavailable, and units already in flight can still produce repeated quota
errors.
- `--mode` defaults to `accept-edits` with `--dangerously-skip-permissions`, the same unattended
capability the implement-review Gemini reviewer already runs with, so a unit can verify numbers,
run experiments, and fetch the web without a permission prompt. The default applies only to the
scratch directory the dispatcher creates. When the caller supplies a workspace through
`PRUN_SCRATCH_CWD` or `--add-dir`, the dispatcher refuses to launch until `--mode` is given,
because the write-capable mode could otherwise reach a directory it did not create. Safety stays
structural either way: point those at a throwaway clone with no remote or a read-only snapshot,
never the real tree. `--mode plan` is the strictly read-only opt-in; it keeps request-review
permissions and never gets the skip flag. In headless use a tool request that needs an approval
nobody can give (`run_command`, `read_url`, browser tools) is denied, and the process can still
exit 0 with a normal-looking result that reports it could not verify. A normal result file
therefore does not prove those checks ran; read its `Verification` and `Open items` fields. If
the worker has not written a non-empty result file, a missing, empty, or shorter-than-20-byte
final response produces `FALLBACK`, and so does a final `result` event whose `status` is not
`SUCCESS`. A standing `permissions.allow` rule in Agy's own `settings.json`
(`~/.gemini/antigravity-cli/settings.json`, entries such as `read_url(*)` or `command(*)`) is the
alternative for a plan-mode unit.
- `--add-dir PATH` (repeatable) adds a directory outside the unit's working directory to its
workspace without copying a repository into the scratch area, in either mode; it requires an
explicit `--mode`. Point it at a clone or a read-only snapshot, never the real tree, since
`accept-edits` can write there. The dispatcher resolves each path to absolute and refuses to
launch if it is empty or not an existing directory.
- `--continue-from STATE_DIR` resumes the conversation recorded at `<STATE_DIR>/conversation-id`
for a follow-up dispatch that should keep the earlier turn's context instead of re-embedding
the prior result in a new prompt. It still needs its own fresh `--result-file`; an empty
`STATE_DIR` argument, or one whose conversation id file is missing or empty, is a pre-launch
error.
- Requires a fresh result path and refuses to overwrite an existing result. The final response is
published to that path, unless the worker already wrote a non-empty result file there itself: then
the worker's file is kept and the final response lands beside it as `<result>.response.<ext>`, so
a one-line closing reply never replaces a full result. If no non-empty worker result exists, a
failed preflight, launch, worker run, or timeout, or an unusable final response, produces an
atomic `FALLBACK` result with captured tails.
- Both signals decide the outcome. A non-zero process exit fails the unit, and after an exit of 0
the final `result` event's `status` is consulted, because Agy exits 0 when it stops on a quota
limit and that `ERROR` event still carries the opening narration in `response`. Publishing that
response would hand the coordinator work that never happened. Any status other than `SUCCESS`
fails the unit and carries the event's `error` text into the `FALLBACK` result. The one exception
is a status that is missing or blank, which counts as success so that an older Agy keeps working.
- A failed run whose worker had already written its own result keeps that file, because the worker
may have finished before the backend stopped. The partial response lands beside it as
`<result>.response.<ext>`, and the dispatcher exits non-zero with the backend error on stderr.
Both monitors classify a stable worker-written result as `done` without reading the backend
status. Before integrating such a unit, wait for the dispatcher to finish and check its exit
code. When that code is unavailable, read the `result` event's `status` and `error` in
`<state-dir>/tail` and the captured dispatch diagnostics. The sibling response is supporting
context: a successful run writes one too, and it records no status.
- `ANTIGRAVITY_DISPATCH_TIMEOUT_SECONDS` defaults to 2700 and is passed to Agy's bounded
`--print-timeout`. The dispatcher never enumerates or terminates another agent process.
- The dispatcher omits Agy's `--sandbox` flag by default. On Windows that sandbox starts an
elevated admin broker and raises a UAC prompt for every unit that runs a command; a declined
prompt fails the command. `PRUN_AGY_SANDBOX` controls whether the flag is added; it does not
disable a sandbox enabled in Agy's own settings (`enableTerminalSandbox`). Accepted values are
`1`/`true`/`yes`/`on` to add the flag and `0`/`false`/`no`/`off`, empty, or unset to omit it.
Values ignore case and surrounding whitespace; anything else exits 2 before state creation or
launch. Scratch directories and throwaway clones reduce accidental changes to the working
repository. They do not enforce filesystem or network isolation; the worker must follow the
prompt's ban on commit, push, and destructive git.
- The legacy `dispatch-task.{sh,ps1}` Codex scripts remain shipped only so older deployments and
state directories retain their recovery tooling. Current `prun` routing never selects them.
- ## Sonnet usage
-
- Sonnet is the executor for a unit needing session-internal tools (see Executors) and the
- fallback when the Agy pool is the constraint. Spawn an Agent-tool subagent
- with `model: sonnet`. It inherits the session's available tools but starts with **fresh context** (it
- does not see the conversation), so put any needed state in the unit prompt. Give it the same return
- contract and result-file path. For a code-writing unit, point it at a clone dir; commit and push are
- also gated by `guard.py` on the Claude side.
-
## gather usage
```
scripts/gather.sh <result-file-1> <result-file-2> ...
```
- Prints `GATHER-START count=N timeout=Ss`, then `DONE <abs-path>` per file as it lands; exits 0 when
all land, exits 2 with `TIMEOUT remaining=<k>`.
- A file is "landed" when it exists, is non-empty, and has been quiet for the stable window
(default 10s); no startup-snapshot race.
- **Use a fresh result path per unit per run** (delete any stale file before dispatch). Have each unit
write its result in one operation.
## monitor usage
```
scripts/monitor.sh <state-dir-1> <state-dir-2> ...
```
- Takes the `STATE-DIR` paths from each dispatch (not result files); reads each unit's `tail` (growth),
`result-file` (done/fail), and `dispatch-pid` (liveness).
- Prints `MONITOR-START units=N stall-threshold=Ts timeout=Ss`, then on the first actionable event
`MONITOR-EVENT <all-done|stall|fail|timeout>` and one `UNIT <name> <status>` line per unit (`done` /
`failed(fallback)` / `failed(dispatch-dead)` / `stalled(Ns)` / `growing`).
- Exit: `0` all done, `3` attention needed (a stall or fail), `2` hard timeout.
- Env: `PRUN_STALL_THRESHOLD` (default 600, ten minutes; raise it for long code-writing units),
`PRUN_MONITOR_POLL` (default 15), `PRUN_MONITOR_TIMEOUT` (default 3600), `PRUN_MONITOR_STABLE_WINDOW`
(default 10).
- Run it in the background; after handling a stall or fail, re-launch on the still-running units so a
resolved unit is not re-flagged.
## report-state usage
```
scripts/report-state.sh [--root DIR] [--json] [--summary] [--sort path|tail-bytes-desc]
[--min-tail-bytes N] [--include-legacy-pid]
scripts\report-state.ps1 (same flags)
```
Read-only. It inspects `prun-task-*` directories left behind by earlier runs and writes nothing at
all, which `tests/test_prun_report.py` checks by hashing the tree before and after a run. Reach for
it when a fan-out was interrupted and you need to know which unit output survived. `--root` repeats,
and defaults to the system temp directory.
Every unit carries two independent fields instead of one verdict. A single label such as
"salvageable" would read as permission to act, and this command cannot support that reading without
the process identity it deliberately does not record.
| `result_path_state` | Meaning |
|---|---|
| `resolved` | the unit recorded a result path and it could be read |
| `absent-entry` | no `result-file` entry was written |
| `invalid-entry` | the entry was empty, or a relative path escaping its unit |
| `unreadable` | the entry exists but could not be read |
| `result` | Meaning |
|---|---|
| `present` | the result file exists and holds bytes |
| `empty` | the result file exists and is zero bytes |
| `missing` | the recorded path does not exist |
| `unknown` | nothing is claimed: either the path never resolved, or it resolved and the target could not be observed |
`result` is `unknown` for every `result_path_state` other than `resolved`, and `resolved` may also
carry it. Only `FileNotFoundError` proves a target is gone; a denial or an I/O error yields
`resolved`/`unknown` plus an entry in that unit's `errors`, so a failed observation is never
reported as an outcome. No other pairing can be emitted, and
`test_no_illegal_pair_can_be_emitted` checks that against the table the module exports.
Remaining JSON fields:
| Field | Meaning |
|---|---|
| `schema_version` | `1`; bump on any field change |
| `roots` | absolute directories inspected |
| `unit_count` | units inspected, counted before any display filter |
| `discovery_errors` | roots or matching entries that could not be listed or stated |
| `unit` | absolute path of the unit directory |
| `tail_bytes` | size of the unit's `tail`, `0` when absent, or `null` when it could not be stated or is not a regular file |
| `result_target` | the resolved result path, or `null` |
| `errors` | per-unit observation failures; see the table below |
| `legacy_pid_unverified` | shown only under `--include-legacy-pid` |
| `safety` | the sentence below, present on every run |
Each `errors` entry is `{"stage": <where>, "error": <value>}`. The value is an exception class name,
or one of two names for a condition that raises nothing: `NotARegularFile` when the path exists but
is a directory, FIFO, or device, and `EntryTooLarge` when a `result-file` or `dispatch-pid` entry
exceeds 64 KiB. That size limit reports rather than truncates. A truncated entry can strip down to
a real path and be mistaken for a complete one. Consumers branch on `stage`:
| `stage` | What could not be observed |
|---|---|
| `result-entry` | the unit's `result-file` exists but could not be read |
| `result-target` | the recorded path could not be stated, or is not a regular file |
| `result` | classification raised unexpectedly; the unit is still reported |
| `tail` | the unit's `tail` could not be stated, or is not a regular file |
| `legacy-pid` | `dispatch-pid` exists but could not be read, under `--include-legacy-pid` |
Discovery failures sit apart from any unit, in a top-level `discovery_errors` array whose entries
carry `stage` (`root` or `unit-entry`), the offending `root` or `unit`, and `error`. They are
separate because a root that cannot be listed produces no unit to attach a failure to, and used to
read as an empty corpus. Any entry in either place sets exit `1`.
`--summary` adds two byte counters that never overlap. `missing_or_empty_result` covers units whose
result path resolved to a file that is missing or empty. `unresolved` covers units whose result was
never classified while their tail still holds bytes. Each counter names what was observed rather
than what may be done about it, because neither a missing target nor an empty one proves that no
other copy exists or that a live producer will not fill it. Both appear because the second group is
easy to lose: across a live corpus of 220 units the first counter read 24.3 MiB while another
0.4 MiB sat in a unit nothing had classified.
Under `--json`, those counters arrive in a `summary` object:
| Summary field | Meaning |
|---|---|
| `units` | units inspected, matching `unit_count` |
| `by_result` | count per `result` value |
| `by_path_state` | count per `result_path_state` value |
| `missing_or_empty_result_units` / `missing_or_empty_result_bytes` | resolved path, result file missing or empty, tail holds bytes |
| `unresolved_units` / `unresolved_bytes` | result never classified, tail holds bytes |
`--min-tail-bytes` hides small units from the listing and moves no unit between classes; `unit_count`
still counts them. `--include-legacy-pid` stays off by default. A recorded PID may be stale, or
reused by an unrelated process, so it can never show that a worker is alive.
Exit codes: `0` every root was listed and every unit inspected cleanly, `1` at least one entry
was recorded in a unit's `errors` or in `discovery_errors` while everything readable was still
reported, `2` a usage error. An unreadable root is never reported as an empty one.
## snapshot-tail usage
```
scripts/snapshot-tail.sh --unit DIR [--dest DIR | --output FILE] [--json]
scripts\snapshot-tail.ps1 (same flags)
```
Copies one unit's `tail` into a ZIP holding exactly two members, `tail.bin` and `manifest.json`,
both stored without compression. Only a regular file, or a symlink to one, may be snapshotted; a
directory, FIFO, or device exits `4` and publishes nothing. Without that rule a device such as
`/dev/null` reported zero bytes and published an empty archive as a complete capture, and a FIFO
with no writer blocked the open indefinitely. The copy is byte-for-byte, so a tail carrying NUL or CR arrives
unchanged. Given neither `--dest` nor `--output`, the archive lands in a per-user state directory:
`%LOCALAPPDATA%\anywhere-agents\prun\snapshots` on Windows, and
`$XDG_STATE_HOME/anywhere-agents/prun/snapshots` elsewhere, falling back to `~/.local/state` when
that variable is unset.
On POSIX the command creates the directory mode `0700` and the archive mode `0600`. A snapshot
extends the lifetime of prompts and tool output, so a directory that already exists and is group- or
world-accessible is refused, with the `chmod` that fixes it named in the message.
Publication goes through `os.link`. That is the one portable operation which is both atomic and
refuses to replace: `os.replace` overwrites, `os.rename` differs by platform, and checking first
races. An existing destination therefore exits `3` and leaves the file byte-identical. Six
concurrent attempts on one name produce exactly one winner. Any other link failure exits `6` rather
than falling back to an operation that could overwrite.
| Manifest field | Meaning |
|---|---|
| `schema_version` | `1` |
| `captured_at` | UTC timestamp of the capture |
| `source_path` | absolute path of the tail that was read |
| `source_size_at_open` | size taken from `fstat` on the already-open handle |
| `bytes_copied` | bytes actually written |
| `sha256` | digest of the copied bytes, re-verified after the archive closes |
| `source_may_be_live` | always `true` |
| `capture_outcome` | `complete_bounded_read` when the two counts agree, `short_read` otherwise |
| `note` | records that equal counts do not prove the source held still |
The read is bounded by `source_size_at_open`, and it is best-effort. Equal counts do not establish
that the source held still, because bytes can arrive from different generations of a growing file
and still total the same number. Read `complete_bounded_read` as "the reader returned `source_size_at_open` bytes before EOF", never
as "the source was unchanged" or "this is a consistent point-in-time copy". A truncate-and-regrow
sequence can also total exactly that many bytes.
JSON output adds `published`, the final path, and `warning`, which is `null` on a clean run. A
warning appears when the archive is linked into place but the temporary file could not be removed.
The snapshot is valid in that case, so the command still exits `0`.
Exit codes: `0` published, `3` the destination already existed, `4` the tail could not be opened
or is not a regular file,
`5` archive validation failed, `6` publication failed. Every failure other than `3` leaves no file
at the final name.
### The safety sentence
**Snapshotting a tail is the only safe operation offered here. This output does not establish that
deleting, overwriting, or promoting any unit is safe.**
`report-state` prints those words on every run, in both text and JSON. `snapshot-tail` does not
repeat them, so apply them yourself after a successful capture: holding a snapshot does not make the
unit disposable. Deciding that a unit is finished needs process identity, which this slice records
nowhere. See anywhere-agents#29 Part B.
## Return contract (every unit writes this)
```
# <unit-id> result
Conclusion: <one line>
Files: <files created/modified in the clone, or "none (read-only)">
Open items: <blockers or follow-ups, or "none">
Verification: <what was run/checked/searched, or "none">
<body: the findings, survey, analysis, or change summary>
```
## Ledger
Keep a simple run ledger (a file in a scratch area) recording each unit: id, executor, mode, prompt
- file, clone-dir, result file, status (dispatched / done / failed), start/end, and the handle the
- unit is tracked by, meaning its state-dir for an Agy unit and its Agent identifier for a Sonnet
- one. An Agy unit also records the routing reason from the Executors rule, which makes the choice
- reviewable afterwards. Use the ledger to report progress and to
- relaunch only units whose result is missing or fails validation.
+ file, clone-dir, result file, status (dispatched / done / failed), start/end, and the unit's
+ state-dir. Take the executor column from `<state-dir>/model`, which names the model that actually
+ ran, so a quota fallback shows in the ledger. Use the ledger to report progress and to relaunch
+ only units whose result is missing or fails validation.
**Where a unit's own files go**: four kinds of file belong under an `agent-io` directory inside the scratch area. They are the per-unit prompt, the result file, the shared-context file every worker reads, and the run ledger. The directory name tells the writing-style hook to skip them, because none of that text is the coordinator's prose to rewrite. A unit prompt is an instruction to a worker, and a result file holds what the worker sent back. Anything the fan-out produces for a human reader stays outside `agent-io`.
## Web access
- Both executors reach the web by different paths, each with its own strengths, so assign per unit.
-
**Agy** runs on the user's local machine, so its requests leave from the user's local network
rather than the cloud fetcher's egress IP, often a residential IP. That can reach some pages a cloud
fetcher gets `403` on, though a hardened site can still block on bot score, fingerprint, or rate. It
also surfaces pages a cloud fetch would miss. The dispatcher's default mode grants the web and the
shell unattended, so a worker can fetch through `read_url` or a local-shell curl. It does not ask
for Agy's own `--sandbox`: on Windows that sandbox starts an elevated admin broker
(`agy --exebox-admin-broker`), which raises a UAC prompt for every unit that runs a command, and
a declined prompt fails the command. Set `PRUN_AGY_SANDBOX=1` to add the flag where the broker
is acceptable. Only `--mode plan` withholds the web and the shell: it runs Agy in `request-review` mode, and a
headless run denies the permission prompt. The process can still exit 0 with a result that says the
fetch did not happen, so read a plan-mode result's `Verification` and `Open items` fields before
trusting it.
- **Sonnet** units get web from an `agentType` granting built-in `WebSearch` and `WebFetch`, and,
- where the session permits it, the same local shell the curl recipe below uses. Claude's `WebSearch`
- is strong at broad discovery, finding the right page when the URL is unknown, which makes it the
- right default for ordinary web work. A Sonnet worker with local shell access leaves from the same
- local network an Agy worker does, so a cloud `WebFetch` returning `403` does not on its own buy a
- second model run.
-
- Routing heuristic (apply the Executors rule):
+ Web units, all on Agy:
- - **Discover a page when the URL is unknown**: use a **Sonnet** unit; Claude's `WebSearch` is the
- stronger discovery tool.
- - **Fetch a known URL**: either executor. An Agy unit fetches unattended through `read_url` or
- curl in the default mode, and it is the cheaper pool.
- - **A page the Sonnet worker's `WebFetch` is blocked on**: have that same unit retry through its
- permitted local shell first, then hand the URL to an **Agy** unit, and record which path failed.
- - **A high-stakes fact that might be stale or blocked**: run one executor first, then add the
- other as a cross-check when the task explicitly calls for a second vendor's read on that claim.
+ - **Discover a page when the URL is unknown**: give the unit the question and let it search; ask it
+ to list the candidate URLs it considered, so a thin search shows up in the result.
+ - **Fetch a known URL**: the unit fetches unattended through `read_url` or curl in the default mode.
+ - **A page that blocks the fetch**: have the unit retry through curl from the local network, and
+ record which path failed and the HTTP status each returned.
+ - **A high-stakes fact that might be stale or blocked**: dispatch a second unit that verifies the
+ claim from an independent source, and have the coordinator compare the two results.
An Agy web-fetch unit can use curl in the default mode (`--mode plan` denies it). Report the
HTTP status per URL so a cloud-vs-local block shows up in the result. In Windows PowerShell, name
the binary `curl.exe`, since a bare `curl` can resolve to the `Invoke-WebRequest` alias instead:
```bash
curl -sSL -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36" -o <body-file> -w "%{http_code} %{url_effective}\n" <URL>
```
```powershell
curl.exe -sSL -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36" -o <body-file> -w "%{http_code} %{url_effective}\n" <URL>
```