协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
prun【开发与工程】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 …
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
prun fans a task out into independent units that run in parallel while the current session only
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.
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.
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 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.
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.
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.
| Executor | Quota | Notes |
|---|---|---|
Agy (agy) | Google AI plan authenticated in Antigravity | The only worker. The newest Gemini Flash High the account lists, 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. |
Rules:
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 the newest gemini-*-flash-high the account lists
(gemini-3.8-flash-high as of 2026-09) at the CLI's maximum high effort./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.prun task.--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.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.
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 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.
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:
dispatch-task-agy.py does this by default.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.The worker's environment is scrubbed. dispatch-task-agy.py builds the child environment
with worker_env(), which drops every name containing KEY, TOKEN, SECRET, PASSWORD,
PASSWD, CREDENTIAL, APIKEY, or AUTH, and every name starting AWS_. ANTIGRAVITY_* is
kept, because the CLI's own session plumbing lives there. The names withheld from a run are
listed in <state-dir>/env-withheld; values are never written anywhere. A unit that genuinely
needs one variable through gets it with PRUN_KEEP_ENV=NAME1,NAME2.
This exists because the dispatcher previously passed os.environ.copy() straight through. A
worker only has to list its environment once, in an env command or a traceback, for a live
key to enter a third-party model's context. On 2026-09-20 a scan found two live credentials in
90 files across 18 Agy conversations over three days. A worker never calls a model, so it never
needed them.
Beyond that: 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. Scrubbing removes the credential class of accident; it is not a sandbox, and a worker can still read any file the user can.
<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.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.)<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/.
<python> scripts/dispatch-task-agy.py --prompt-file <prompt> --result-file <fresh abs result> --unit-id <id>
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.gemini-*-flash-high at high effort. The constants
DEFAULT_MODEL (gemini-3.8-flash-high) and SECOND_MODEL
(claude-sonnet-5-5-high) are family templates: Gemini Flash and Claude
Sonnet. The preflight's agy models listing supplies the newest version in
each family, so a new Flash or Sonnet release runs without an edit. The
effort tier may change too: Agy 1.2.16 retired claude-sonnet-4-6 for
slugs that carry the tier, and the old template would still have reached
claude-sonnet-5-5-high. The tier is chosen first (the template's own,
then high), and the newest version within it second. An older high
therefore beats a newer medium. An unset ANTIGRAVITY_DISPATCH_EFFORT
follows the chosen slug's tier, because Agy rejects a conflicting
--effort. The switch shows as a MODEL-RESOLVE from=... to=... line
on stderr and in <state-dir>/quota-note, and <state-dir>/model names the
model that ran. Quota routing picks the group first, and floating never
moves a unit to the other group. One exception keeps a batch alive after a
larger rename. When Agy lists nothing in the Sonnet family, a unit routed
there runs on the Gemini default, recorded as MODEL-FALLBACK ... reason=template-unlisted. The Gemini group's quota is checked again first,
so a fallback into an empty group exits 75 without launching.
A model named in ANTIGRAVITY_DISPATCH_MODEL runs verbatim, and
ANTIGRAVITY_PREFLIGHT=off skips the listing, so the templates run as
written. ANTIGRAVITY_DISPATCH_EFFORT overrides the effort. The dispatcher
passes --effort for the Gemini models only. Agy rejected the flag for the
older Claude models. Since 1.2.16 it rejects any effort that conflicts with
the tier in a Claude slug, so the flag is omitted for the second group.gpt-oss-120b-medium, as of 2026-10), metered apart from the Gemini group. A unit that names no
model goes to whichever group has the freer meter, with the newest Claude
Sonnet as the second group's model. A unit is
shallow work that either group handles, so the meter decides rather than the
model family. The worker is the Agy CLI either way, so a Claude model here
spends Agy quota and never the Claude account the coordinator runs on. This is
a routing policy the user set on 2026-09-15, after a 198-unit batch spent 77
points of the Gemini five-hour meter in an hour while the second group sat
untouched. An agent still does not reach for that group on its own outside
this rule: one that did spent 646 generations of it in a day.
ANTIGRAVITY_DISPATCH_MODEL disables headroom balancing for the run. The
exhaustion rules below still apply to the model it names, including the
fallback from an exhausted Claude and GPT group to Gemini.Individual quota reached ... Resets in 34m. Before launching, the dispatcher reads the snapshot agent-quota
maintains and decides:
MODEL-BALANCE from=... to=... reason=freer-meter own=... other=... goes to
stderr and to <state-dir>/quota-note.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.75 without launching, and
say so. A model someone chose is not escalated into the metered group on its
own; the message names ANTIGRAVITY_DISPATCH_MODEL for the operator who
wants that.75 with both reset times.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.<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.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.<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.--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.prun used before 2026-09-13 are archived in legacy/prun-codex-worker/ in the source repositories and are no longer shipped. They remain available if pricing makes a Codex worker the cheaper pool again. The report-state and snapshot-tail launchers recover old unit state through prun_state.py without them.scripts/gather.sh <result-file-1> <result-file-2> ...
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>.scripts/monitor.sh <state-dir-1> <state-dir-2> ...
STATE-DIR paths from each dispatch (not result files); reads each unit's tail (growth),
result-file (done/fail), and dispatch-pid (liveness).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).0 all done, 3 attention needed (a stall or fail), 2 hard timeout.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).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.
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.
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.
# <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>
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 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.
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.
Web units, all on Agy:
read_url or curl in the default mode.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:
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>
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>