2. checkpointRun canonicalizes the collection and the run directory (symlinks resolved through the deepest existing ancestor) and refuses, before touching anything, when they are the same or one contains the other (CHECKPOINT_OVERLAP); restore applies the same check to source, target and staging. Before, <root>/<run> equal to the live run replaced it. 3. Publication writes an immutable generation (generations/g<revision>-…), renames it into place complete, then switches current.json with one rename; the previous generation is kept, so an interruption at the publish boundary (fault hook checkpoint-before-pointer) leaves the last checkpoint current. A generation never replaces a newer one (higher run revision): "CHECKPOINT: skipped … newer". Publication and restore hold the collection's .lock; restore also holds <out>.lock. 4. Restore checks every listed file's real path stays inside the generation (an ancestor symlink no longer copies outside files), parses run.json and runs validateRun on the staged run before publishing; a refusal leaves --out absent. readState reroots plan_input and plan_source for moved runs, and a restore with the owner's plan file gone points both at the restored copy, so a second restore cycle and a later revise work. 5. A checkpoint failure after a committed command prints "CHECKPOINT: failed for <run>: … ; the command's result stands" on stderr and keeps the command's exit code. 6. `gstack-autoplan checkpoints --from <dir>` lists generations newest first (run, gate_rev, status, revision, time, hash ok|bad, current). `resume --from <collection or root>` restores the newest generation that verifies, printing each corrupt newer one it skipped; a root holding several runs must name one. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
13 KiB
/autoplan driver: run the review loop from a parent agent
Read this file instead of autoplan/SKILL.md when you (a parent agent on any host) drive an
unattended /autoplan run through gstack-autoplan: the reviewers are subagents you dispatch,
and the runner keeps the state. The commands live in <root>/bin/, where <root> is the
folder that holds this autoplan/ directory; gstack-autoplan --help and the first next
print DRIVER: <root>/autoplan/DRIVER.md. Run every command with the GSTACK_LAUNCH
assignments your installer printed. Read printed lines and files, never prose: every workflow
command ends with GSTACK_RESULT: skill=autoplan status=<s> run=<dir>, and every error line
ends with (<CODE>).
Phases and scope
Order: ceo → design → dx → eng. Design runs only with UI scope, DX only with DX scope, Eng always and last.
Scope flags on the first next: --ui, --no-ui, --no-dx, --developer-tool, --agent-primary. --developer-tool and --agent-primary only enable DX.
The first next prints SCOPE: ui=… (terms; visual; files) dx=… (detected) source=ui:<s>,dx:<s>
(each axis is flag, marker or detected). UI needs a rendered file path or a UI term next
to a visual noun in the Implementation plan. To keep flags for later runs on the same plan, add
the SCOPE_PIN: marker it prints (<!-- gstack-autoplan-scope: … -->) as the plan's first
line yourself; gstack never writes the plan.
The loop
nextprints oneATTEMPT: <id> phase=<p> voice=native|outside prompt=<file> result=<file>per voice. Dispatch the native prompt to a subagent on your model and the outside prompt to a subagent on another model family (orgstack-outside-voice run). Each writesresult=.- Submit each voice exactly once (
BOUND:). Both voices in closes the phase (PHASE_CLOSED:). - Repeat
nextuntilstatus=gate_pending(GATE: gate_rev=<n>); render it withgstack-gate render <dir>/gate.json --alland relay every page. You never approve the gate for the owner. Offerrec(every recommendation, plan changes included) as the default. answerwith the owner's reply and--by human(the owner typed the tokens) or--by agent:<id> --owner-reply "<their words>"(you relayed them); without--byan unattended run refuses (ANSWER_BY_REQUIRED).APPLY: <id> <title> -> <change>lines are accepted plan changes (statusamend_pending): edit the plan, thenrevise(the printedNEXT:names it), orrevise --settle <id>when the line says no plan edit is needed. Until thennextrefuses withPLAN_AMEND_PENDINGandexportwill not mark the run complete.REOPENED: <phases>means loopnextagain (Eng last).APPROVED:meansexport.
On Capy
- Always drive
gstack-autoplan; never hand-adaptautoplan/SKILL.md. A plan you reviewed by hand is not an autoplan run: do not present it as one. - Set
GSTACK_AUTOPLAN_CHECKPOINT_DIRto the private project-user Drive folder so a run at rest survives machine replacement; restore withresume --out <new dir> --from <dir>/<run>.
Commands
gstack-autoplan next --out <dir> --plan <plan.md> [scope flags] [--light] [--spend-cap <usd>] [--scaffold]first call: init the run, print PHASE:, SCOPE:, DRIVER: and one ATTEMPT: per voice.gstack-autoplan next --out <dir> [--redispatch <attempt>]later calls: open the next phase or reprint open attempts; never redispatches on its own.gstack-autoplan submit --out <dir> --phase <p> --voice native|outside --record <file.md> --model <id> [--attempt <id>]bind a free-form review record (canonical fence, one findings table or list, orNo findings.).gstack-autoplan submit ... --record <file.md> --attest "<reason>" --attempt <id>bind a record with no INPUT: line as receipt: attested (a wrong INPUT: always refuses).gstack-autoplan submit --out <dir> --phase <p> --voice native|outside --result <file> --model <id> --attempt <id>bind a canonical result (the RESULT FORMAT below).gstack-autoplan submit --out <dir> --phase <p> --voice outside --unavailable|--failed "<reason>"close the phase native-only when no outside voice can run.gstack-autoplan answer --out <dir> --gate-rev <n> --reply "<reply>" --by human|agent:<id> [--owner-reply <text>|-] [--rec-hash <h>]answer the gate with the reply grammar below; an agent answer stores the owner's words verbatim.gstack-autoplan recommend --out <dir> --gate-rev <n> --item <id> --option <k> --basis "<why>"change one recommendation before relay; then render and relay again (arecneeds the new--rec-hash).gstack-autoplan revise --out <dir> --plan <plan v2> --incorporates <decision ids>bind the amended plan; reopens every phase whose reviewed plan changed, Eng last.gstack-autoplan revise --out <dir> --settle <decision id|d4>discharge a packaging or ordering change with no plan edit; reopens nothing.gstack-autoplan revise --out <dir> --withdraw <decision id>drop a pending amendment instead of applying it.gstack-autoplan export --out <dir>write the artifact set; thengstack-artifact validate <dir>/run.json.gstack-autoplan status|resume --out <dir>read the manifest (PENDING: lines name open attempts); take ownership after a crash (EXECUTION_UNKNOWN lists unfinished attempts).gstack-autoplan resume --out <new dir> --from <checkpoint>restore the newest valid checkpoint generation (GSTACK_AUTOPLAN_CHECKPOINT_DIR) into an empty directory and take ownership;checkpoints --from <dir>lists them.
Reviewer result format
Every prompt ends with this contract; submit --result enforces it, submit --record accepts
the looser record shapes above.
RESULT FORMAT (required by gstack-autoplan; the parent binds your file by this contract):
- The first line of your result is exactly: INPUT:
- Write the complete review as prose, then END with one fenced block of canonical findings, one JSON object per line:
{"id":"F1","severity":"High","title":"<one line>","file":"<path or omit>","line":12,"fix":"<the fix>"}
severity is Critical | High | Medium | Low | Informational. Add "user_challenge": true only when you recommend changing the plan's stated direction AND the change alters its scope, cost or direction (then also add "changes": "scope" | "cost" | "direction") or only its delivery packaging or ordering, with no Implementation-plan edit (then "changes": "packaging" | "ordering"). Naming and other reversible choices are ordinary findings, not user challenges. Add "urgent": true and "suggested_owner": "" for a security or data-loss finding this plan does not own. An empty fence means no findings; a missing fence is refused. 3. Write the whole result to this file:
Gate replies
A reply binds to the gate_rev you read (--gate-rev). Bare yes/no is unparsed, never guessed.
rec: every item takes its recommendation, plan changes included (each prints APPLY:);allis an alias.as-is: keep every direction; no plan change.d3b uc1a: one token per item you set; unanswered auto items take their default.rec except d3b: recommendations, with the listed overrides.
Run directory
Runner state: run.json, attempts.jsonl, run.lock, spend.json, plan-active.md, restore.md, plan-source.md. Reviewer files: <phase>-native-prompt.md,
<phase>-outside-prompt.md, <phase>-<voice>.<attempt>.md (the reviewer writes it), its sealed
<phase>-<voice>.<attempt>.bound.md (what submit bound), <phase>-consensus.{json,md},
gate.json, decisions.jsonl. export writes plan.md, review-record.md, findings.jsonl,
tasks.jsonl, timing.json and rehashes run.json; gstack-artifact validate <dir>/run.json
is the completion check. Copy the whole directory, never single files.
Result codes
RUN_NOT_INITIALIZED: start withgstack-autoplan next --plan <file> --out <dir>(the first call initializes the run)PLAN_SECTIONS_INVALID: add the missing heading, or rerun the firstgstack-autoplan nextwith --scaffold to review a scaffolded copy (your plan file is never edited); duplicate, reversed or empty sections are fixed by handRUN_LOCKED: wait for it to exit, or if its pid is gone rungstack-autoplan resume --out <dir>to take ownershipEXECUTION_UNKNOWN: submit the result file if the reviewer finished (gstack-autoplan submit ... --attempt <id>), or redispatch explicitly withgstack-autoplan next --redispatch <id>; nothing is redispatched automaticallySPEND_CAP_EXCEEDED: raise--spend-cap, settle or release unsettled attempts, or stop; unknown charges count as their reservationPHASE_NOT_AWAITING: rungstack-autoplan status --out <dir>and submit for the attemptnextprintedATTEMPT_MISMATCH: pass the attempt idnextprinted for this voice; a result is bound exactly onceRESULT_RECEIPT_MISSING: the reviewer must read the prompt file through EOF and begin its result with the INPUT line the prompt names; a result for another snapshot is refusedRESULT_FINDINGS_MISSING: end the review with one ```gstack-findings fence, one JSON object per line with at least severity and title;[]rows are not findings, an empty fence means no findingsMODEL_FAMILY_CONFLICT: pass--model <id>of a different family (anthropic, openai, google, xai, deepseek, qwen, zai, moonshot, meta, mistral); a same-family result is not an outside voiceCONSENSUS_MISSING: resume the run so the phase closes through reconciliation; the Eng prompt is never sent without every prior phase summaryGATE_REV_STALE: render the current gate list, read it, and reply with its gate_revGATE_REPLY_UNPARSED: reply withrec,as-is,<id><option>tokens (for exampled3b uc1a), orrec except <tokens>; bare yes/no is ambiguousGATE_REC_STALE: render the gate again (gstack-gate render <dir>/gate.json --all), relay it, and answer with the printed--rec-hashOWNER_REPLY_REQUIRED: pass the owner's words exactly with--owner-reply "<text>",--owner-reply -(stdin) or--owner-reply-file <path>GATE_OWNER_REPLY_MISMATCH: relay exactly what the owner chose (or their reply itself, e.g.--reply rec); ask the owner when their words and the tokens disagreePLAN_AMEND_PENDING: edit the plan, thengstack-autoplan revise --out <dir> --plan <file> --incorporates <decision id,...>; settle a packaging or ordering change withgstack-autoplan revise --out <dir> --settle <id>; or drop one withgstack-autoplan revise --out <dir> --withdraw <decision id>AMENDMENT_NOT_SETTLEABLE: edit the plan, thengstack-autoplan revise --out <dir> --plan <file> --incorporates <decision id>(add--settle <id>to settle packaging/ordering changes in the same call); settle alone only on the plan the run already reviewsCHECKPOINT_INVALID: rungstack-autoplan checkpoints --from <dir>to see each generation's hash status and restore an intact one; nothing was written to --outGSTACK_VERSION_CHANGED: nothing to do for an older run (journaled forward migrations ran); a run written by a newer gstack needs that gstack: upgrade with bin/gstack-upgrade
Coordinator contract (inherited by every lane)
Merge conditions. A lane's PR merges only when: the required gate lanes are green on the stamped tree or the tree receipt says gate-reuse: eligible; gstack-lane-check reports clear for its planned files against every other open lane; the PR body opens with the gstack-ship-receipt block; and the coordinator, not the lane, says merge. A lane never merges, rebases another lane, or force-pushes.
Behavior-change notices. Every user-visible behavior change outside the plan's own items is listed in the PR body under ## Behavior changes with the file, the old behavior and the new one, before the review starts. An omitted change found in review is a CRITICAL finding, whatever its size.
Spend caps. Each lane runs under the --spend-cap the coordinator names; a lane that reaches it stops with status=budget_exhausted and reports spent, reserved and unknown charges. Paid evals are never retried to change a verdict; a rerun needs a named repair. Host-subagent spend is reported as unknown, never zero.
Before done. A lane runs gstack-pregate run --allow-repo-commands on its branch (the unit tier on the changed files plus the repo's guards) and reports done only after it passes. In a repo whose .gstack/pregate.json has a sealed key, it runs gstack-pregate sealed before any push or PR creation; a refusal stops the push, because a PR tool may not run local hooks.
Handoff format. A lane reports in this order, identifiers first: PR URL (or no PR with branch and SHA), GSTACK_RESULT line, the gstack: ok|fail <rev> line, the PREGATE: line, gate summary (lanes passed/failed/skipped), behavior changes (or none), spend (spent/reserved/unknown), then what it could not verify. Full thread ids (jam_…), never short codes, in every cross-thread message. Prose after the identifiers may be truncated; nothing before them may be.