mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 23:37:53 +01:00
* feat(loadouts): Opus 5.5 lite loadout with explicit model detection (GSTA-161)
modules/loadouts.json gains the lite loadout (eleven coaching preamble
pieces dropped; upgrade-check and brain-sync kept) and a models map that
sends only claude-opus-5-5 (and its dated snapshots) to it. Detection is
explicit: Capy passes --model to the installer, Claude Code opts in with
./setup --loadout lite; anything unmapped, invalid or unreadable serves full.
The checkout always stays the full render.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs(loadouts): Opus 5.5 lite, detection order, Capy SKILL_ROOT, setup --loadout; troubleshooting anchors; changelog fragment (GSTA-161)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(loadouts): claude_loadout read through CONFIG_BIN; capy skill context ceiling for the --model line (GSTA-161)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes (prune keeps a reused loadout folder, GSTACK_LOADOUT_SKILL_ROOT, claude_loadout guard test) (GSTA-161)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes (Capy skill passes the model as GSTACK_MODEL so an older installer ignores it) (GSTA-161)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes (prune only owned, unused folders; full root from the canonical Claude install; fallback reason and source) (GSTA-161)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: loadout source example, install and resolve commands, Capy launch-file note (GSTA-161)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(autoplan): --ceo-only runs the CEO review alone with zero questions (GST-537)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test: classify autoplan-ceo-only in the Codex host metadata
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: select autoplan-ceo-only on the skills it runs; say it needs Claude Code
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test: run autoplan-ceo-only in the marathon lane
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: changelog entry for /autoplan --ceo-only
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test: count the autoplan-ceo-only paid file in the shard inventory
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat: gstack-autoplan next --ceo-only runs the CEO review alone and ends reviewed (GST-537)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: budget and close recover a budget-stopped CEO-only run; reviewed is at rest for checkpoints
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: CEO-only finish leaves owner-only cap items undecided, refuses marker phase flags and a plan at the report path, and replaces ceo-review.md atomically
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: the CEO-only report-path guard compares real paths and refuses at the finish when the report file is the plan
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: CEO_ONLY_CONFLICT covers a plan linked to the report path
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(browse): failing-first device and touch tests (GSTA-227 F3, #3058)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: the report-path guard keeps the lexical check, skips safe hard links and compares bigint file ids
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* browse: per-session tabs for parallel agents; tab pins stop moving the shared active tab (#1961)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* browse: keep the server.ts tab-pin anchor comment for source guards
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(browse): per-tab phone emulation and touch input ($B device, tap, swipe, pinch, touch; #3058, GSTA-227 F3)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test: copy lib/result-codes-autoplan.ts into the fixtures that copy the result-code tables, and keep it out of derived closures
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs(qa, aside): touch pointer in /qa, Aside touch recipe verified on Aside 1.26, report Phones row (GSTA-227 F3, #3058)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* browse: drop the sessions paragraph from /browse to stay inside the parity cap
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* changelog: F3 phone emulation and touch entry
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* browse: hide the ps window in the session harness walk
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: the decisions.jsonl status list names auto_kept
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(browse): session cap, LRU eviction, headed release and scoped auto-session rows
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* browse: sessions keep tab <id> picks, sidebar claude stays on the user's tab, pinned rebuild and state load rebind the scope
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes (touch cancel with no finger held is a no-op)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: Step 11 review fixes (headed tab adoption, pinned viewport reset, CLI retry keeps --session, rebuild id remap, --json notice, evicted-session reset, device kept after annotated captures, render opts out of sessions, session: client ids reserved)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: headed viewport auto keeps the user's active tab; render's closetab opts out of sessions
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: sessions, phone emulation and touch in BROWSER.md; session: client ids reserved; TODOS #1961/#3058 shipped
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test: copy lib/result-codes-browse.ts into the fixtures that copy the result-code modules
lib/result-codes.ts imports ./result-codes-browse, so fixtures with an explicit
lib list failed with 'Cannot find module'. pr-dependencies lists it with the other
reason-code modules as selection data. GSTA-227
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat: gstack-clean reclaims per-repo .gstack scratch and logs, dry run by default (#2627)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* browse: name the Aside touch recipe's coordinates <tap-x>/<tap-y> with a numeric grammar
The generated-bash lint (INV-3) rejected the free-text <x>/<y> placeholders. GSTA-227
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(clean): leave hard-linked and unreadable logs alone; cut through a no-follow descriptor checked against the evaluated inode
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(clean): tests that expect tmp removal see a quiet ps, so a codex elsewhere on the host cannot flake them
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(clean): an unlistable tmp/ skips its group; liveness asked once per group on apply; BROWSER.md says which logs reset at daemon start
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(clean): write output synchronously so --json reaches a pipe whole; test fixes for root, wrapper git and the ps stub
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(clean): every change rechecks the verified folder and the measured file; full writes; EPIPE ends output quietly
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(clean): the early-close test checks the bin's own exit status
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: list gstack-clean among the standalone CLIs
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(clean): copied-install fixtures and the dependency list carry lib/result-codes-clean.ts
The remote pregate failed 11 cases in gstack-doctor, plan-tune-cathedral and
timeline-stop-hook-slug: each copies an explicit list of lib/result-codes-*.ts
files into a temp install, and lib/result-codes.ts now imports
./result-codes-clean. Same lists GSTA-168 extended for result-codes-eval.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* chore: regenerate gstack-doctor-components.sh for the gstack-clean result codes
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* qa: fold the touch pointer into the Phases sentence to fit the parity size cap after the main merge
qa's union was 95988 bytes (ratio 1.142 > maxSizeRatio 1.142, train 75). Now 95975 (1.1419). GSTA-227
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: #3058 troubleshooting 'Fixed in' lines name v1.92.10.0 after the main merge
GSTA-227
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(clean): classify gstack-tasks rows files (tasks-*) as kept, after main added the producer
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: #3058 troubleshooting 'Fixed in' lines name v1.92.20.0 after the main merge
GSTA-227
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes (restore the lib/result-codes-plan.ts ratchet row the base merge dropped)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(clean): the producer scan ends a name at a colon, so prose after a path names the file (train 78)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: #3058 troubleshooting 'Fixed in' lines name v1.92.30.0 after the main merge
GSTA-227
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* outside-voice-api: caller receipts, stop_reason, OpenAI max_completion_tokens
A caller can write its own egress receipt (sink, consent, payload class);
the default stays the outside-voice receipt, so existing callers send and
record the same bytes. The result carries the provider's stop reason, and
an OpenAI request gets max_completion_tokens only when the caller sets an
output limit (GST-537 PR 3).
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* autoplan gate: item classes and the owner's answer provenance
Gate items carry item_class, ruleKept() names the keep-direction pick, and
answered decision rows record owner_mode, owner_named, rec_hash and
answered_role parsed from the owner's own words (GST-537 PR 3).
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* autoplan: shadow-mode decider module, frozen prompt and spend accounting
The switch (off by default, fixed at init), the lease, one model call per
gate with a page retry and a truncation retry under one deadline, the
once-per-revision record with its journaled sha256, the 10% audit mark,
and shadow-labeled ledger rows that count against the cap, show as
shadow= on GATE_SPEND and stay out of the per-voice price (GST-537 PR 3).
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* gstack-autoplan: shadow every published gate, the shadow export, config and docs
Every driver command that prints a gate records the shadow first and
checkpoints after it; answer waits for a live lease and records a dead
one retrospectively; gstack-autoplan shadow exports answered revisions
for taste-replay R2 (SHADOW_BEFORE_ANSWER otherwise); gstack-config
autoplan_shadow / autoplan_shadow_model; gstack-artifact validate checks
the record's hash (GST-537 PR 3).
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* changelog: shadow mode for the CEO-only decider (GST-537 PR 3)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* autoplan shadow: /ship review fixes
- register test/fixtures/shadow-fake-model.ts in FREE_FIXTURES (free-fixtures inventory failed)
- withRunLock: count dead-owner reclaims apart from waitMs iterations, so a long wait can still reclaim
- buildPage: fill the decider template in one pass, so slot names inside the plan stay plan text
- revise regate: the shared publish step prints the GATE and ESCALATE lines once
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* autoplan shadow: reply files never follow a link; the retry reserves against the current cap
- a model reply is written by temp file and rename, so a link at the reply path is replaced, not written through
- each shadow reservation reads the run's current spend cap, so a budget change during the call is not undone
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* outside-voice-api: write the egress receipt under the caller's env
callModel resolved the receipt's state root from process.env even when the caller passed env, so the shadow
tests wrote to the real ~/.gstack (the free-suite home-containment guard caught it). The receipt now uses
req.env; the shadow tests give the fake model a private HOME, GSTACK_HOME and GSTACK_STATE_ROOT, and the receipt
test no longer sets process.env to pass.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(autoplan): CEO-only runs leave main's open Critical/High finding items to the owner (no decision row)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(autoplan): shadow pages carry main's finding items; a finding item is never ceo_eligible
After merging #3767 (main 90a170c), open Critical/High findings are gate items
(item_class finding). The shadow records one row per gate item, so its pages now
include them. A CEO-only run leaves those items to the owner, so their rows are
ceo_eligible false (rule_pick was already null). The shadow fixtures follow
main's gate (items, counts, "Do it" tokens); the two merge adapters follow
main's doNext(out, rest, price) and its per-voice reservation (--estimate-usd).
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs: #3058 troubleshooting 'Fixed in' lines name v1.92.61.0 after the main merge
GSTA-227
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(investigate): drop the five-phase step procedure (GST-938)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* changelog: GST-938 fragment wording
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(investigate): keep the web-search sanitize guard; update BROWSER.md and docs/skills.md (review pass 1)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* changelog: GST-938 wording
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(investigate): pin the web-search guard; changelog size (adversarial review)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test(clean): classify restamp-staging.json, the restamp producer main gained after #3779's base (wave browse)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes (wave browse): scoped newtab keeps a concurrent tab switch; shadow validate catches a deleted record; a failed shadow publish is journaled
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: a failed shadow publish journals under the run lock and never throws (wave browse review pass 2)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs(unattended): shadow publish failures and the missing-record validate check (wave browse docs audit)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* changelog: wave browse integrator fixes fragment
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: shadow reader dedupes by run folder, so two runs started in the same second both count (wave browse CI free-suite 16)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* chore: regenerate doctor result anchors in generator order (wave browse rebuild without #3818)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* changelog: name the wave fragment after the rebuilt wave
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
---------
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
899 lines
58 KiB
Markdown
899 lines
58 KiB
Markdown
# Unattended runs: the artifact contract
|
||
|
||
A gstack skill runs *unattended* when a parent agent starts it with
|
||
`GSTACK_SESSION_KIND=unattended` and reads its artifacts instead of its prose.
|
||
This page is the contract between that parent and the run: what the session
|
||
kind changes, which files a run writes, how the parent decides whether the run
|
||
finished, and how it reads the result without reading any markdown.
|
||
|
||
The session kinds are: `interactive` (a human answers prompts), `headless`
|
||
(`GSTACK_HEADLESS`, `CI`, the eval harness; a question BLOCKs), `spawned`
|
||
(an orchestrator subagent; the recommended option is auto-chosen) and
|
||
`unattended` (this page). `unattended` is honored only as the explicit
|
||
`GSTACK_SESSION_KIND=unattended` override, which outranks `GSTACK_HEADLESS`
|
||
so a parent can drive a run inside CI. Nothing in the test or eval harness sets
|
||
it, so `headless` keeps its BLOCK semantics.
|
||
|
||
## What the session kind changes
|
||
|
||
`gstack-skill-start` prints `SESSION_KIND: unattended`,
|
||
`UNATTENDED_SESSION: true` and the `unattended-session` instruction block
|
||
(`GSTACK_INSTRUCTION_BEGIN: unattended-session <session-id>` through
|
||
`GSTACK_INSTRUCTION_END`), then applies these rules for the whole skill:
|
||
|
||
- **No prompts.** The update check, upgrade offer, telemetry and sync consent,
|
||
onboarding tips and routing blocks are suppressed exactly as they are for
|
||
`spawned`. An unanswered consent is `off`.
|
||
- **No publishing or sync, enforced at the writers.** Usage analytics are not
|
||
written (`TELEMETRY_WRITE: skipped (unattended session)`), the artifacts
|
||
repo is neither pulled nor pushed (`ARTIFACTS_SYNC: skipped (unattended
|
||
session; mode=<m> queue=<n> retained)`), the sync spool is not fed
|
||
(`gstack-brain-enqueue` is a no-op, and `gstack-review-log` says `review-log:
|
||
sync skipped (unattended session)` on stderr), and `gstack-skill-end` drains
|
||
nothing. Inherited enabled settings and pending queue work are left for the
|
||
next human session. The upgrade offer becomes one line at skill end, read from
|
||
the local cache: `UPGRADE_AVAILABLE: <version>; run /gstack-upgrade` (`<version>+<n> (main at <sha>)` when
|
||
main moved <n> commits past the checkout without a new version).
|
||
- **Questions decide themselves, gates do not.** A question with a
|
||
`(recommended)` option takes it and appends the choice to `decisions.jsonl`
|
||
with `kind: "auto"`. A question with no recommendation, any consent, any
|
||
destructive or irreversible option, and the final approval gate are appended
|
||
with `kind: "approval"` and `status: "pending"`. The skill ends
|
||
at its next gate with the pending list rendered; it never approves and never
|
||
publishes.
|
||
- **State root durability is reported.** `STATE_ROOT: <path> durable=yes|no`
|
||
(`no` when the host sets `GSTACK_EPHEMERAL=1`). On an ephemeral root the run
|
||
says `learnings: skipped (state root is ephemeral; set GSTACK_STATE_ROOT)` and
|
||
`timing: analytics skipped (...)` instead of writing to a disk that vanishes.
|
||
Point `GSTACK_STATE_ROOT` at a durable directory when you want cross-run
|
||
history (see [state-root.md](state-root.md)).
|
||
- **The autoplan publication guard is named, not faked.** On hosts that do not
|
||
execute Claude Code hooks, and in every unattended session, `gstack-skill-start`
|
||
prints `autoplan guard: not enforced by this host; publication order is
|
||
unverified (GUARD_NOT_INSTALLED)` once. `gstack-artifact validate` requires
|
||
that line in an unattended run's `review-record.md`. Claude Code's
|
||
deny-on-absent behavior is unchanged.
|
||
|
||
An unknown `GSTACK_SESSION_KIND` value coerces to `interactive` and says so:
|
||
`SESSION_KIND: interactive (unknown kind '<x>')`.
|
||
|
||
## The terminal line
|
||
|
||
A run that writes a record ends with one line the parent greps:
|
||
|
||
```
|
||
GSTACK_RESULT: skill=<name> status=<status> run=<dir>
|
||
```
|
||
|
||
`status` is one of `complete`, `reviewed`, `gate_pending`, `incomplete`,
|
||
`interrupted` or `refused`; `run` is where the run's record is. `/autoplan`
|
||
always ends `gate_pending` when it reaches the final gate, because the gate is
|
||
written and never approved. `/autoplan --ceo-only` ends `reviewed`: it runs the
|
||
CEO phase alone, asks nothing, writes no gate and approves nothing (see
|
||
[CEO-only runs](#ceo-only-runs)). Pure queries (`gstack-artifact schema`,
|
||
`gstack-gate render`) print no result line.
|
||
|
||
Not every workflow writes this line or a run directory today:
|
||
|
||
- **`/autoplan`** writes the full run directory below through `gstack-autoplan`,
|
||
and `gstack-artifact validate` checks it. It is the only skill with steps
|
||
and helpers that write `run.json`, `decisions.jsonl` and `gate.json`.
|
||
- **Workflow commands** `gstack-pregate`, `gstack-regen`, `gstack-restamp` and
|
||
`gstack-ship-queue` print their own line, with `run=` naming their own record
|
||
(the pregate receipt, the regen worktree, the restamp journal, the queue
|
||
record), not a run directory.
|
||
- **A refused launch.** Any skill whose install check refuses it at startup
|
||
prints `GSTACK_RESULT: skill=<name> status=refused run=-` and stops.
|
||
- **`/ship`** ends with `gstack-ship-receipt result --pr <n>` (or `--body
|
||
.gstack/tmp/ship-receipt.md` after the handoff section), which binds the
|
||
receipt to the branch head, writes the run record and prints
|
||
`GSTACK_RESULT: skill=ship status=complete run=<dir>`. That line and that
|
||
record are the machine-readable READY evidence; [What /ship leaves for a
|
||
parent](#ship-evidence) below is the whole contract (the refused and
|
||
incomplete lines, the one publication an unattended run may make, the
|
||
`review_loop` counters, the outside voice on Capy and the `CARRY:` lines).
|
||
- **Every other workflow skill** (`/review`, `/qa`, `/design-review`,
|
||
`/spec`, `/land-and-deploy` and the rest) gets the session rules above (no
|
||
prompts, approvals stay pending, nothing published), but has no step that
|
||
writes a run directory or prints the line, so a parent should not wait for
|
||
either. `/eval-plan` asks for a `gate_pending` line at its spend gate, but no
|
||
helper writes its `gate.json` yet (`gstack-gate` renders and parses a gate
|
||
list; it does not create one). `/cso` skips the shared startup, so these
|
||
session rules do not reach it at all. Read each skill's own record instead:
|
||
the review-log rows (`gstack-review-read`) for `/review` and the
|
||
`qa-report-*.md` file for `/qa`. For a `/ship` run from before the result
|
||
line, read the receipt instead: when the run did not push (an unattended run
|
||
must not), read the receipt file in the child's checkout with
|
||
`gstack-ship-receipt read --body .gstack/tmp/ship-receipt.md --json`;
|
||
use `gstack-ship-receipt read --pr <n> --json` only once a PR exists. In both
|
||
cases compare the receipt's `head` to the branch SHA (`git rev-parse HEAD` in
|
||
the child's checkout): `read` does not check that the receipt is current, so
|
||
an older PR's receipt reads as valid; `result` does.
|
||
|
||
Every workflow command shares one exit table: `0` ok, `1` fail, `2` usage,
|
||
`3` refused or needs a flag. Error lines end with a result code in parentheses,
|
||
for example `findings.jsonl:12: ... (ARTIFACT_SCHEMA)`; each code has an anchor
|
||
in [troubleshooting.md](troubleshooting.md#unattended-runs-and-artifacts) with
|
||
a `fix:` clause. Grep the code, not the prose.
|
||
|
||
<a id="ship-evidence"></a>
|
||
## What /ship leaves for a parent
|
||
|
||
One section for everything a parent (a Captain, a crew thread, a merge queue)
|
||
reads after an unattended `/ship`: the READY evidence, the review loop's
|
||
counters, the outside voice's record, and the carry across a base merge.
|
||
|
||
### The READY evidence: `gstack-ship-receipt result`
|
||
|
||
`/ship` Step 22 runs `gstack-ship-receipt result --pr <n>` (or `--body
|
||
.gstack/tmp/ship-receipt.md` after the handoff section). It binds the receipt
|
||
in the PR body (or the handoff file) to HEAD, writes
|
||
`<state root>/ship/runs/<utc>-<head12>/` and prints the terminal line:
|
||
|
||
```
|
||
GSTACK_RESULT: skill=ship status=complete run=<dir>
|
||
```
|
||
|
||
The run directory holds `ship-receipt.md` (the block, copied) and
|
||
`ship-run.json` with `head`, `pr`, `base`, `version` (`"none"` for a
|
||
repository with no version source, [ship-policy.md](ship-policy.md#ship-receipt)),
|
||
`queue_mode`, `session_kind`, `receipt` (the copied block's path) and
|
||
`receipt_source`. It is a /ship record, not the `run` v1 manifest below, so
|
||
read it directly; `gstack-artifact validate` does not apply. A parent's merge
|
||
check is then mechanical: take `run=` from the last result line, read
|
||
`ship-run.json`, compare its `head` to the PR head, and quote the
|
||
`SHIP_RECEIPT: pr=<n> head=<head12> …` line in the READY block.
|
||
`gstack-ship-queue ready`, `train build` and `verify --train` make the same
|
||
comparison from the PR body ([ship-policy.md](ship-policy.md#ship-before-ready)).
|
||
|
||
The other endings, each greppable: when gh cannot read the PR,
|
||
`GSTACK_RESULT: skill=ship status=incomplete run=-` (exit 1); a receipt that is
|
||
missing or names another head, `GSTACK_RESULT: skill=ship status=refused run=-`
|
||
with `QUEUE_RECEIPT_MISSING` or `QUEUE_RECEIPT_STALE` on stderr (exit 3), the
|
||
line a run that stops before publication prints itself; `result --pr <n>`
|
||
refuses `QUEUE_RECEIPT_STALE` when HEAD is not the PR's pushed head; and a
|
||
receipt with a `base_red` block (Step 5 paused on a red base) prints
|
||
`SHIP_PAUSED: base-red …` on stderr, writes the run record and ends
|
||
`GSTACK_RESULT: skill=ship status=incomplete run=<dir>` (exit 1): the lane is
|
||
waiting on the base, not failed ([base_red](ship-policy.md#base-red)).
|
||
|
||
The one publication the unattended rules allow: when an open PR for the branch
|
||
exists and HEAD is its pushed head, Step 18 takes the pr-body section's
|
||
existing-PR path and posts the body (receipt first) and title through
|
||
`gstack-post`. It never runs `pr-create`, never pushes a commit and never runs
|
||
`gh pr ready`; a branch with no open PR, or whose PR head is not HEAD, takes
|
||
the handoff section. When the run committed anything (a CHANGELOG entry,
|
||
generated tests), the parent pushes those commits itself, then reruns the
|
||
receipt write, the body post and `result` at the pushed head: a receipt for an
|
||
unpushed head is `QUEUE_RECEIPT_STALE` to the queue.
|
||
|
||
### The review loop's counters
|
||
|
||
The receipt's `review_loop` object, written from `gstack-review-scope summary
|
||
--since <REVIEW_SINCE>`, is the loop's machine-readable outcome: `passes`
|
||
(Step 9 full passes this ship ran, 1 or 2; up to 3 under `review_scope: all`),
|
||
`blocking_found` (every blocker the readers found, rows and dispatch ledgers
|
||
both, [review-policy.md](review-policy.md#what-the-receipt-records)), `deferred`
|
||
(blocking findings in unchanged code filed as issues, Step 11's late defers
|
||
included), `noted`, `fixed` and `scope`. The
|
||
summary line a parent reads prints it as `passes=<n> blocking_found=<k>
|
||
deferred=<m>`; the PR body lists the deferred issues under `## Deferred
|
||
findings` and the below-floor items under `## Review notes`. A pass that finds
|
||
nothing is `NO FINDINGS` from every reviewer and a pass with no edits, which
|
||
is the loop's exit, never a failed review ([review-policy.md](review-policy.md)).
|
||
Three counters appear only when the run recorded them: `fix_checks` (fix-check
|
||
rows, never passes), `dispatches` (reviewer dispatches by role) and
|
||
`est_input_tokens` (prompt plus diff bytes the dispatches read, / 4, a relative
|
||
estimate and never a bill).
|
||
|
||
### Report length
|
||
|
||
A parent reads many lanes, so length is a cost it pays per lane. A READY block
|
||
holds its required lines plus at most 150 words; a completion report holds at
|
||
most 300 words; a subagent brief names the files to read instead of pasting
|
||
them. Evidence goes in files the report points to. Waits go on operations,
|
||
never on polling wakes.
|
||
|
||
### Token caps and fresh-context handoffs
|
||
|
||
A parent bounds a /ship run with `--token-cap <n>` (or `GSTACK_SHIP_TOKEN_CAP`).
|
||
It is an admission cap: /ship meters usage, reserves every step and dispatch
|
||
batch before it starts, and stops `status=incomplete` with a `RECOVER:` line
|
||
instead of starting work that would cross the cap. Long runs hand Steps 9–11.5
|
||
and 12–22 to fresh-context children. The flow, the BUDGET line and the result
|
||
codes are in [ship-budget.md](ship-budget.md).
|
||
|
||
### The outside voice on Capy
|
||
|
||
Step 11's outside adversarial review prefers the Codex CLI. On a Capy machine
|
||
the Codex CLI is normally absent (selecting a model in Capy installs and
|
||
authenticates no CLI), and the accepted outside voice is then a subagent
|
||
running a different model family from the parent: a full outside voice, not
|
||
degraded coverage, as long as the model that reviewed is recorded. Its
|
||
review-log row is the same row every Step 11 pass writes
|
||
(`ship/sections/adversarial.md`): `skill: "adversarial-review"`,
|
||
`phase: "adversarial"`, `source` naming the runner that actually reviewed
|
||
(the outside provider that reviewed, with the model recorded, for the
|
||
cross-family subagent; `codex` for completed Codex CLI output, as
|
||
`ship/sections/adversarial.md` prescribes),
|
||
`outside_provider` (the provider the host would have used, as the generated
|
||
prose names it), `outside_status` (`completed` for a finished outside pass;
|
||
`unavailable` when neither the CLI nor a subagent completed, the cause on the
|
||
`gstack-outside-voice run:` stderr line and in `<out>.meta.json` rather than
|
||
in the row; `disabled` under
|
||
`codex_reviews: disabled`), `status`
|
||
(`clean`, `issues_found` or `unavailable`), `completed`, `converged`, and the
|
||
subagent's model recorded with the pass (`gstack-outside-voice run --runner
|
||
host-subagent --result <file> --model <id> --prompt <file> --out <file>` binds
|
||
it; the row's text names it where the host reports it). The native in-host adversarial
|
||
subagent writes its own row with `source: "in-host"`; native completion never
|
||
credits outside coverage. A parent reads the rows with
|
||
`gstack-review-read` and treats `outside_status: unavailable` as missing
|
||
coverage, not as a clean outside review.
|
||
|
||
The pass program (`lib/capy-review-pass.js`) picks the outside voice's model
|
||
when `input.models['outside-voice']` is empty and the lane model is
|
||
`anthropic/`: `openai/gpt-6.1-sol` at medium reasoning, the pairing the no-loss
|
||
rounds ran. Any other lane must name a model. An outside voice from the same
|
||
provider as the lane model or the native voice stops the pass (`STOP:`) before
|
||
either voice is created, including on a rerun, and each outside voice prints an
|
||
`OUTSIDE_VOICE:` line saying whether it is the certified configuration.
|
||
|
||
### The carry across a base merge or a test-only fix
|
||
|
||
When the parent merges the base in or fixes a test after the receipt was
|
||
written, it runs `gstack-ship-receipt carry --pr <n> --base <base> --dry-run`
|
||
instead of another /ship; on `CARRY: would carry` it runs `gstack-pregate` at HEAD
|
||
and reruns carry with `--pregate .gstack/tmp/pregate.json`. `CARRY: carried from <old> to <new> (<reason>)` plus the
|
||
`GSTACK_RESULT: skill=ship status=complete run=<dir>` line means the receipt
|
||
now names HEAD with a `carry` object holding the `git range-diff` evidence, and
|
||
`ship-run.json` gains `carried_from` and `carry_reason`; the `review_loop`
|
||
counters are kept, because the reviewed product diff is unchanged or its only
|
||
difference passed a scoped review. `CARRY: current` means nothing moved.
|
||
`CARRY: refused (RECEIPT_CARRY_SCOPED_REVIEW)` means the base edited context
|
||
next to reviewed hunks. Carry without `--dry-run` writes the packet the
|
||
refusal names (a dry run writes none), one fix-check reviewer reads it, and a
|
||
clean record reruns carry with `--scoped-review <record.json>`
|
||
([troubleshooting](troubleshooting.md#receipt-carry-scoped-review)). `CARRY: refused (RECEIPT_CARRY_PRODUCT_CHANGED)`,
|
||
`(RECEIPT_CARRY_TREE_MISMATCH)` or `(RECEIPT_CARRY_REFUSED)` with the refused
|
||
result line means the product diff changed, or history was rewritten onto a
|
||
tree no review saw, so /ship runs again;
|
||
`(RECEIPT_GIT_TOO_OLD)` means the machine's git predates 2.39 and cannot run
|
||
`patch-id --verbatim` ([ship-policy.md](ship-policy.md#receipt-carry)).
|
||
|
||
## The run directory
|
||
|
||
`/autoplan` writes these files; the next section shows how a parent reads them.
|
||
|
||
| File | Schema | Contents |
|
||
|------|--------|----------|
|
||
| `run.json` | `run` v1 | The manifest: `run` id, `status`, `required_phases`, each phase's `native` and `outside` outcome (`status`, `model`, `provider`), input `snapshot` hashes, `counts`, every artifact's `path` (relative to the run directory) and `sha256`, `session_kind`, `gate_rev`, `deviations`, `consumed_by`. |
|
||
| `decisions.jsonl` | `decisions` v1 | One row per decision: `id` (`<run>-<label>` at gate revision 1, `<run>-r<rev>-<label>` after a reopen, since labels restart at each gate), `title`, `options`, `recommended`, `kind` (`auto`, `approval`, `user_challenge`), `status` (`pending`, `approved`, `overridden`, `rejected`, or `auto_kept` in a [CEO-only run](#ceo-only-runs)), `gate_rev`, and once answered `chosen`, `answered_at`, `reply`, plus how the owner answered: `owner_mode` (`rec`, `rec_except`, `tokens`, `as_is` or `unparsed`, parsed from the owner's own words), `owner_named` (his words named this item), `rec_hash` (the recommendations answered against) and `answered_role` (`human`, or `agent` relaying his words). |
|
||
| `shadow/` | private, not an artifact | [Shadow mode](#shadow-mode) only: `ceo-only.r<rev>.jsonl` (one hidden prediction per gate item, written before the answer), `r<rev>.prompt.md` and one `<call>.reply.txt` per model call, the audit `salt`, and `r<rev>.pending` while a call is in flight. Never in `export --public`, the gate render or the owner brief. |
|
||
| `findings.jsonl` | `findings` v1 | One row per reviewer finding: `id`, `phase`, `voice` (`native`/`outside`), `model`, `severity`, `title`, `disposition`, `plan_items`, `native_counterpart` (string or null), `source`. Rows share the review-log row shape, so `gstack-review-log --findings` derives review status from this file. |
|
||
| `tasks.jsonl` | `tasks` v2 | Implementation tasks with `id`, `title`, `status`, `depends_on`, `findings` (ids in `findings.jsonl`), `blocked_by`, `acceptance`, optional `pr`/`item`/`tier`. |
|
||
| `timing.json` | `timing` v1 | Per-phase `started_at`, `ended_at`, `wall_s`, `outside_s`, `native_s`; `started_at` and `total_wall_s` for the run; the Phase 0 `estimate`. |
|
||
| `plan.md` | prose | The `## Implementation plan` section, with `## URGENT, outside this plan` written above it. |
|
||
| `review-record.md` | prose | The review record and decision audit trail; carries the `GUARD_NOT_INSTALLED` line in an unattended run. |
|
||
| `gate.json` | see `gstack-gate --help` | The final gate list `/autoplan` renders and parses. |
|
||
|
||
Ids are run-bound: every row's `id` starts with `<run>-`, and `run.json` is the
|
||
only place the run id is declared. `gstack-artifact schema <name>` prints the
|
||
JSON Schema for any of `findings`, `decisions`, `tasks`, `timing`, `run`,
|
||
`pregate` and `ship-receipt`; `schema --list` prints the names.
|
||
|
||
## How a parent consumes a run
|
||
|
||
1. **Decide whether the run finished.** For an `/autoplan` run directory,
|
||
`gstack-artifact validate <dir>/run.json`
|
||
is the deterministic completion check (a `/ship` `run=` directory holds
|
||
`ship-run.json`, read directly as described above). It exits `0` and prints
|
||
`ARTIFACT_VALID: run=<id> status=<status>` only when the manifest parses and
|
||
matches its schema, every required phase and each reviewer voice has a
|
||
terminal outcome, every listed artifact exists inside the run directory with
|
||
the recorded hash, every JSONL file parses row by row against its schema with
|
||
run-bound unique ids, task dependencies and finding references resolve
|
||
without cycles, `counts` agree with the files, and (unattended) the guard
|
||
line is present. Otherwise it exits `1` with one error line per problem and
|
||
`ARTIFACT_INVALID: <n> error(s)`; `--json` returns the same as
|
||
`{ valid, run, status, run_dir, errors[] }`. A run that was interrupted shows up here as
|
||
`ARTIFACT_RUN_INTERRUPTED`, never as a partial success.
|
||
2. **Read the terminal line.** `grep '^GSTACK_RESULT:' <log> | tail -1`. With
|
||
`status=gate_pending`, the gate is in the artifacts; nothing was approved.
|
||
3. **List what is pending.**
|
||
```bash
|
||
jq -c 'select(.status=="pending")' <dir>/decisions.jsonl
|
||
gstack-gate render <dir>/gate.json --all # the numbered list and reply grammar, every page
|
||
```
|
||
4. **Answer the gate** by composing the reply grammar (`rec`, `as-is`,
|
||
`<id><option>` tokens such as `d3b uc1a`, or `rec except <tokens>`; bare
|
||
yes/no is rejected) and feeding it back with the gate revision you read:
|
||
```bash
|
||
gstack-gate parse <dir>/gate.json --reply "rec except d3b" --gate-rev 1 --json
|
||
gstack-gate decisions <dir>/gate.json --reply "rec except d3b" --gate-rev 1 >> <dir>/decisions.jsonl
|
||
```
|
||
A stale revision is `GATE_REV_STALE` and a reply with unparsed tokens is
|
||
`GATE_REPLY_UNPARSED`; both exit `1` and write nothing.
|
||
5. **Read findings and tasks without the prose.**
|
||
```bash
|
||
jq -c 'select(.severity=="Critical" and .disposition!="accepted")' <dir>/findings.jsonl
|
||
jq -r '.id + "\t" + .title' <dir>/tasks.jsonl
|
||
gstack-artifact urgent <dir>/findings.jsonl # the URGENT block, or "None."
|
||
```
|
||
6. **Acknowledge consumption** so a second parent (or a retry) can tell the run
|
||
was already read: `gstack-artifact ack <dir>/run.json --consumer <id>` is
|
||
idempotent and records `consumed_by` in the manifest.
|
||
|
||
## Driving `/autoplan` from a parent: `gstack-autoplan`
|
||
|
||
On a host where the parent dispatches the reviewers itself (Capy, any
|
||
orchestrator without a Claude Code harness), `bin/gstack-autoplan` is the
|
||
explicit, durable state machine for the whole run. The parent never reads the
|
||
review record; it runs a loop over two commands and reads the printed lines.
|
||
A parent on any host reads `autoplan/DRIVER.md` (its path is printed as
|
||
`DRIVER:` by `gstack-autoplan --help` and the first `next`) instead of
|
||
autoplan/SKILL.md: the phase order, the artifact contract and the gate protocol
|
||
in about a hundred lines, generated from the code.
|
||
|
||
```bash
|
||
AP=bin/gstack-autoplan
|
||
$AP next --out <dir> --plan <plan.md> [--ui|--no-ui] [--developer-tool] [--no-dx] [--light] [--spend-cap <usd>]
|
||
# PHASE: ceo snapshot=<sha> order=ceo,design,dx,eng
|
||
# SCOPE: ui=yes (...) dx=yes (detected: ...) source=ui:detected,dx:detected
|
||
# DRIVER: <root>/autoplan/DRIVER.md
|
||
# Skipped sections: none (scope: ui=yes,dx=yes,source=ui:detected,dx:detected; checklist=loaded)
|
||
# ATTEMPT: <run>-a1 phase=ceo voice=native prompt=<dir>/ceo-native-prompt.md result=<dir>/ceo-native.a1.md model_family=...
|
||
# ATTEMPT: <run>-a2 phase=ceo voice=outside prompt=<dir>/ceo-outside-prompt.md result=<dir>/ceo-outside.a2.md model_family=differs from the native reviewer
|
||
# SPEND: spent=0.00 reserved=0.00 unknown=0(0.00) worst_case=0.00 cap=none
|
||
# GSTACK_RESULT: skill=autoplan status=awaiting_result run=<dir>
|
||
```
|
||
|
||
The first `next` prints `SCOPE: … source=ui:<flag|marker|detected>,dx:<…>`
|
||
and, when CLI flags differ from the plan's marker, `SCOPE_PIN: add <!--
|
||
gstack-autoplan-scope: … --> as the first line of <file>`; gstack never writes
|
||
the plan.
|
||
|
||
1. **Dispatch each `ATTEMPT:`.** The native voice is a subagent on the parent's
|
||
own model given the prompt file; the outside voice is a subagent on another
|
||
model family, or `bin/gstack-outside-voice run --runner codex-cli|api
|
||
--prompt <file> --out <file> [--model <id>]`, which prints
|
||
`OUTSIDE_STATUS: <status> provider=<runner> model=<id> family=<f>`. Every
|
||
reviewer writes its result to the printed `result=` path under the RESULT
|
||
FORMAT below. `next` is idempotent: it reprints open attempts and never
|
||
redispatches on its own (`EXECUTION_UNKNOWN` lists attempts whose process
|
||
died without a result; `--redispatch <attempt>` mints a fresh one and
|
||
`release --attempt <id>` returns its reservation).
|
||
2. **Submit each result exactly once.**
|
||
```bash
|
||
$AP submit --out <dir> --phase ceo --voice native --result <dir>/ceo-native.a1.md --model <id> --attempt <run>-a1
|
||
$AP submit --out <dir> --phase ceo --voice outside --result <dir>/ceo-outside.a2.md --model <id> --attempt <run>-a2 [--runner host-subagent (default)] [--usage-usd <n>]
|
||
# BOUND: <run>-a1 phase=ceo voice=native model=<id> findings=7 sha256=<result hash prefix>
|
||
# PHASE_CLOSED: ceo confirmed=4 disagree=1 new=2 native_only=1 coverage=both
|
||
```
|
||
Submit seals the bytes it bound as `<phase>-<voice>.<attempt>.bound.md`
|
||
(read-only); a later write to the reviewer's file changes nothing bound,
|
||
and another attempt's result file is refused (`ATTEMPT_MISMATCH`).
|
||
Binding checks the receipt line against the phase snapshot
|
||
(`RESULT_RECEIPT_MISSING`), parses the canonical findings fence
|
||
(`RESULT_FINDINGS_MISSING`), refuses an outside model from the native
|
||
family (`MODEL_FAMILY_CONFLICT`), and refuses a second bind of the same
|
||
attempt or a bind while the phase is not awaiting it (`ATTEMPT_MISMATCH`,
|
||
`PHASE_NOT_AWAITING`, exit 3). Both voices in closes the phase: the
|
||
reconciliation (`confirmed` / `unverified` / `disagree` / `new` per outside finding, with
|
||
disagreement and resolution kept separate) is written to
|
||
`<phase>-consensus.{json,md}`, and every later phase's prompts (DX, design
|
||
and Eng alike) carry each closed predecessor's consensus as data; a phase
|
||
refuses to open without one (`CONSENSUS_MISSING`).
|
||
3. **Loop `next` until `status=gate_pending`.** Eng always runs last. The gate
|
||
(`gate.json`, `gate_rev`) lists user challenges as approval items,
|
||
disagreements as auto items, each open Critical or High finding as an
|
||
auto item (a twin pair is one item; `a` applies the reviewer's fix to the
|
||
plan and is recommended, `b` carries it into the build as a blocking task)
|
||
and `p1` plan approval; nothing is approved. An unanswered auto item whose
|
||
recommendation changes the plan keeps the plan's direction, so only `rec`,
|
||
`p1b` (approve with the recommended changes) or an explicit `<id>a` applies
|
||
a fix; a finding carried with `b` stays
|
||
open, so a rerun that raises it again asks again.
|
||
4. **Answer the gate** with the reply grammar and the revision you read:
|
||
```bash
|
||
$AP answer --out <dir> --gate-rev 1 --reply "rec" --by human # every recommendation, plan changes included (APPLY: ...; amend_pending)
|
||
$AP answer --out <dir> --gate-rev 1 --reply "as-is" --by human # APPROVED: keeps every direction, no new paid phase
|
||
$AP answer --out <dir> --gate-rev 1 --reply "rec except d1b" --by agent:<id> --owner-reply "<owner's words>"
|
||
```
|
||
`--by human` means the owner typed the tokens; `--by agent:<thread or id>`
|
||
means an agent relayed them, and then `--owner-reply` (or `-` for stdin,
|
||
`--owner-reply-file <path>`) stores the owner's words verbatim
|
||
(`OWNER_REPLY_REQUIRED`); owner words that parse to different choices are
|
||
refused (`GATE_OWNER_REPLY_MISMATCH`). The words default to the owner's: an
|
||
agent that answers in its own words (a Captain deciding on its own
|
||
authority) must pass `--words-by agent:<id>` with its own `--by` id. They
|
||
are then recorded as `answer_words` with `words_by`, never as
|
||
`owner_reply`; a later gate never carries them (`CARRY: no`), and
|
||
`gstack-approvals import` lists that row under the agent, not the owner.
|
||
An unattended run refuses an answer
|
||
without `--by` (`ANSWER_BY_REQUIRED`, exit 3) and records the value as
|
||
`answered_by` on every decision row and in the review record's gate line,
|
||
so a gate an agent closed is distinguishable from one the owner closed.
|
||
`gstack-autoplan recommend` changes one recommendation before relay; it
|
||
bumps the gate's `rec_rev`, and a later `rec` or `p1b` needs the `--rec-hash` from
|
||
the render the owner saw (`GATE_REC_STALE`). Every `GATE:` line is followed
|
||
by `GATE_SPEND:` and `GATE_REC_COST:` (also `gate_cost` in `--json`; see
|
||
`autoplan/DRIVER.md`).
|
||
A user challenge both voices raised on a confirmed finding (a title match,
|
||
not file:line proximity alone, which is `unverified`) with the same
|
||
`changes` value is recommended as `b` only with one shared proposal: a
|
||
`--reconcile` record naming it, or the same remedy from both voices.
|
||
Different or missing remedies keep `a`, and option b shows both verbatim
|
||
with the basis. `rec` (alias `all`) takes every recommendation, so it
|
||
accepts the items recommended `b`. Every accepted plan change prints
|
||
`APPLY: <decision id> <title> -> <chosen text>` and becomes a pending
|
||
amendment: `next` refuses with `PLAN_AMEND_PENDING` and `export` refuses a
|
||
`complete` run until `revise --incorporates` names it, or `revise --settle
|
||
<id>` settles a packaging or ordering change that needs no plan edit.
|
||
A stale revision (`GATE_REV_STALE`, exit 3), bare `yes`/`no`, or a
|
||
conflicting reply (`GATE_REPLY_UNPARSED`) writes nothing. A reply that
|
||
leaves approval items pending records only the answered auto items and
|
||
keeps the gate; a later reply never overwrites an earlier answer. An
|
||
accepted change reopens nothing at answer (status `amend_pending`); `revise`
|
||
reopens each phase whose reviewed plan changed plus Eng, archives their
|
||
prompts and consensus as `*.r<rev>.*` (sealed `*.bound.md` results stay in
|
||
place), and the next gate is `gate_rev + 1`; `p1d` reruns every phase on the
|
||
current plan; `p1e` ends `incomplete`.
|
||
5. **Export and validate.** `$AP export --out <dir>` writes `plan.md` (with
|
||
the `## URGENT, outside this plan` block first), `review-record.md`,
|
||
`tasks.jsonl`, `decisions.jsonl`, `findings.jsonl`, `timing.json` and
|
||
`run.json`; then `gstack-artifact validate <dir>/run.json` and the
|
||
consumption steps above apply unchanged. `$AP status --out <dir>` and
|
||
`resume` work after the run directory moves (paths are re-rooted; journal
|
||
paths are run-relative). With `GSTACK_AUTOPLAN_CHECKPOINT_DIR` set (on Capy,
|
||
`gstack-config set autoplan_checkpoint drive` defaults it to the private
|
||
project-user Drive folder; unset, the first `next` prints the export line),
|
||
a run at rest, after each bound voice and after each phase transition
|
||
publishes an immutable, hash-listed generation under
|
||
`<dir>/<run>/generations/` and switches `current.json` to it (the previous
|
||
generation is kept; a stale copy never replaces a newer one; a directory
|
||
overlapping the run is refused with `CHECKPOINT_OVERLAP`; a failure is a
|
||
`CHECKPOINT: failed` warning that leaves the command's exit code alone).
|
||
`$AP checkpoints --from <dir>` lists generations with their hash status, and
|
||
`$AP resume --out <new dir> --from <dir>/<run>` restores the newest valid
|
||
one after validating the staged run (`CHECKPOINT_INVALID`).
|
||
|
||
Concurrency and recovery: every state write is a locked read-modify-write
|
||
(`run.lock`; a second writer sees `RUN_LOCKED`), attempts are journaled to
|
||
`attempts.jsonl` before dispatch, and a parent killed before or after a
|
||
`submit` resumes with `next` without a duplicate charge
|
||
(`test/autoplan-run-recovery.test.ts`, `test/autoplan-run-concurrency.test.ts`,
|
||
`test/autoplan-answer-invalidation.test.ts`). `--spend-cap` admits each attempt
|
||
through `spend.json` (`SPEND_CAP_EXCEEDED` refuses, exit 3); host-subagent
|
||
spend is `unknown` and counted in `worst_case`. Under a cap, in any session
|
||
kind (or unattended on Capy), each host-subagent attempt, and each api attempt
|
||
whose model has no price, reserves the default per voice unless
|
||
`next --estimate-usd <n>` sets the reservation, which the run keeps for later
|
||
phases (`run` reserves an api attempt's priced worst case, prompt bytes / 3
|
||
input tokens plus the output limit, and settles it at its token usage; every
|
||
native call that went out and bound no result, whether it failed, was cancelled,
|
||
had its result refused, or was in flight when the process died (the next `run`
|
||
finds it), settles at its usage, or at its full reservation when the usage is
|
||
unknown, while one refused before anything was sent, such as with no API key,
|
||
keeps its attempt open for the rerun, and a failed outside call leaves the phase
|
||
native-only and stays counted at its reservation); `submit
|
||
--usage-usd <n>` settles an attempt at its real cost. `--deadline <min>` makes the
|
||
`run` subcommand cancel the children it started and marks the run
|
||
`interrupted`.
|
||
|
||
### RESULT FORMAT (what every reviewer file must contain)
|
||
|
||
Each prompt file ends with this contract; `submit` enforces it.
|
||
|
||
1. The first line is exactly `INPUT: <phase> <snapshot sha256>` (the receipt
|
||
that binds the result to the snapshot both voices reviewed).
|
||
2. The review is prose, then one fenced block of canonical findings, one JSON
|
||
object per line: `{"id":"F1","severity":"High","title":"...","file":"...","line":12,"fix":"..."}`
|
||
inside a ```` ```gstack-findings ```` fence. `severity` is Critical | High |
|
||
Medium | Low | Informational; `"user_challenge": true` marks a recommended
|
||
change to the plan's stated direction and needs `"changes": "scope" |
|
||
"cost" | "direction"` (what accepting it changes); `"urgent": true` with
|
||
`"suggested_owner"` marks a security or data-loss finding the plan does not
|
||
own. An empty fence means no findings; a missing fence is refused.
|
||
3. The whole result is written to the printed `result=` path.
|
||
|
||
### Outside-voice runners
|
||
|
||
| Runner | Access | Record label | Needs |
|
||
|---|---|---|---|
|
||
| `codex-cli` | repository, read-only, executes here | `repository review` | `codex` on PATH and logged in (`bin/gstack-codex-login` reads `OPENAI_API_KEY` from stdin) |
|
||
| `api` | supplied input only | `supplied-input review` | `--model <id>` and `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`; never silently equal to a repository review |
|
||
| `host-subagent` | the host's | `host-dispatched review` | `--result <file>` the subagent wrote, `--model <id>`; spend `unknown` |
|
||
|
||
`bin/gstack-outside-voice runners` prints each runner's availability on this
|
||
machine; `family <model>` prints the model family `submit` compares. A completed
|
||
`api` or `host-subagent` review is a full outside voice in the record, labeled
|
||
by its runner. Result codes: `EXECUTION_UNKNOWN`, `RUN_LOCKED`,
|
||
`RUN_NOT_INITIALIZED`, `PHASE_NOT_AWAITING`, `ATTEMPT_MISMATCH`,
|
||
`RESULT_RECEIPT_MISSING`, `RESULT_FINDINGS_MISSING`, `MODEL_FAMILY_CONFLICT`,
|
||
`SPEND_CAP_EXCEEDED`, `CONSENSUS_MISSING`, `OUTSIDE_RUNNER_UNAVAILABLE`
|
||
(anchors in [troubleshooting.md](troubleshooting.md)).
|
||
|
||
### Hand-run records, revised plans and admission
|
||
|
||
- **Hand-run reviews.** `submit --record <file>` binds a review that did not
|
||
follow the RESULT FORMAT: the `gstack-findings` fence, one findings table
|
||
(Severity and Title columns), one severity-tagged list (`- [High] title`) or
|
||
a `No findings.` line. Without its `INPUT:` line it binds only with
|
||
`--attest "<reason>"` and `--attempt <id>` (receipt `attested`; refused after
|
||
a later `revise`), and `gstack-artifact validate` prints `attested=<n>`.
|
||
`--attempt` may be omitted when exactly one attempt is open.
|
||
`submit --voice outside --unavailable "<reason>"` (or `--failed`) closes the
|
||
phase `native_only` once native is bound; `next` and `resume` never
|
||
redispatch it.
|
||
- **Revising the plan in the same run.** Edit the source plan, then
|
||
`$AP revise --out <dir> --plan <file> [--incorporates <decision id,...>]`.
|
||
Every phase whose reviewed Implementation-plan hash differs reopens (plus
|
||
Eng); newly in-scope phases are added; archives keep `.r<rev>` names with
|
||
`.v<k>` only on collision; a revise at `gate_pending` makes the next gate
|
||
`gate_rev + 1`. `--incorporates` discharges only the named amendments and
|
||
refuses an unchanged plan; `revise --out <dir> --withdraw <decision id>`
|
||
drops one as a journaled owner decision. `next` prints `PLAN_CHANGED` and
|
||
`answer` prints `PLAN_SOURCE_CHANGED` when the source plan moved since the
|
||
snapshot. A plan without both `## Implementation plan` and `## Review record`
|
||
refuses with `PLAN_SECTIONS_INVALID`; `next --scaffold` reviews a copy with
|
||
the missing heading added. `resume` completes an interrupted revise.
|
||
- **Admission and upgrades.** `next`, `submit`, `answer`, `revise`, `export`
|
||
and `run` refuse on a Capy host (`GSTACK_CAPY=1`) without an ok install
|
||
verdict for this checkout, HEAD and workflow at `$GSTACK_INSTALL_VERDICT`
|
||
(`INSTALL_UNVERIFIED`), and in any unattended session when the running Bun
|
||
is below gstack's supported minimum (`RUNTIME_BELOW_MINIMUM`); the decision
|
||
is the same one `gstack-skill-start` makes (see Install admission);
|
||
`GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1` overrides and records a run deviation.
|
||
`status` and `resume` are never gated. A run directory written by an older
|
||
gstack prints `GSTACK_VERSION_CHANGED` once and moves forward (legacy
|
||
decision ids are renamed and journaled); one written by a newer gstack is
|
||
refused.
|
||
|
||
### Carry-forward
|
||
|
||
A gate revision after the first (`gate_rev` 2 and later, written after a
|
||
`revise`) is answered with the owner's recorded words from the revision before
|
||
it when nothing new needs their decision. `gstack-autoplan` prints
|
||
`CARRIED: gate_rev=<n> <- gate_rev=<m> words="<their words>" by=<relayer> reason=<why>`,
|
||
then the usual `APPROVED:` lines, and journals `gate_carried`. The carry needs
|
||
all of: the earlier gate approved (`p1a` or `p1b`), no item there left
|
||
pending, and the owner's words recorded; that gate was not itself carried; every accepted change incorporated
|
||
or settled; the plan file's bytes equal those recorded at the gate or at the
|
||
`revise` that took the changes (any later edit refuses, an ELI5-only one
|
||
included); no urgent or
|
||
outside-plan finding open; and every new gate item one of the cases below.
|
||
Anything else prints `CARRY: no (<why>)` (journal `carry_refused`) and the gate
|
||
goes to the owner as before. `--no-carry-forward` on the first `next` turns it
|
||
off for the run.
|
||
|
||
An earlier answer given item by item (`<id><option>` tokens, not `rec` or "do
|
||
it") carries only what the owner already answered: every item on the new gate
|
||
must be carried by origin (the same proposal, so the same choice), and only an
|
||
unreviewed post-cap delta (below) may be new. The carry replays the owner's own
|
||
`p1` choice and records their tokens verbatim as its basis; any new or changed
|
||
item, "do it" cases included, goes to the owner.
|
||
|
||
- **"do it".** When the owner's words were "do it" (their words, not a
|
||
`captain:` relay of them), a new `auto` item that changes no scope, cost or
|
||
direction, whose recommended option changes no plan text, and is no round
|
||
or budget grant takes its recommendation. The
|
||
decision row gets `carried_under: do it`, the output prints
|
||
`CARRIED_UNDER_DO_IT:` per item, and the approval record lists them as
|
||
"taken under "do it", not seen by the owner". `rec` carries no new item.
|
||
- **Unreviewed post-cap delta.** A "plan changed after <phase> cap" item
|
||
recommending `a` (accept) is read by one reviewer before it carries. `CARRY_CHECK:` names the attempt and
|
||
the prompt (the delta against `plan-at-gate.r<m>.md`, the accepted changes,
|
||
the full plan); the run reserves the estimate in `spend.json` and refuses
|
||
when it is over `--carry-check-cap <usd>` (default 5) or the run's cap.
|
||
Dispatch it to a model family other than the plan reviewers', then
|
||
`check --out <dir> --attempt <id> --result <file> --model <id>`
|
||
(`--same-family "<why>"` when no other family is available). A High or
|
||
Critical finding, or `--unavailable "<reason>"`, leaves the gate for the
|
||
owner. The verdict is saved as `carry_check` on `gate.json`, on the
|
||
decision rows (`carry_check`) and in `<dir>/decisions.tsv`. A pending
|
||
check is void, its reservation released and the gate left for the owner,
|
||
when the owner answers the gate, the plan is revised, a new gate is
|
||
written, or the delta changed after dispatch.
|
||
|
||
Carried rows keep `answered_by: agent:gstack-autoplan` and name their source
|
||
(`carried_gate_from`, `carried_gate_rev`, `carried_answered_by`);
|
||
`gstack-approvals import --autoplan` writes the carried gate as its own row,
|
||
and `gstack-owner-brief render --captain` opens with "Nothing pending.
|
||
Carried forward from gate_rev <m>", followed by any items taken under "do it"
|
||
without being shown to the owner.
|
||
|
||
## CEO-only runs
|
||
|
||
`/autoplan --ceo-only` runs the CEO review by itself and asks nothing. It runs
|
||
in the session that invokes the skill and needs Claude Code: other hosts inline
|
||
autoplan's sections and do not carry this one yet. A parent agent on any host
|
||
gets the same run from the driver with `gstack-autoplan next --ceo-only` (see
|
||
below). It is for a harness or a crew that wants the strategy pass without the
|
||
full gauntlet, such as an eval comparing gstack's CEO review with a plain
|
||
prompt.
|
||
|
||
- **What runs:** Phase 0, the outside-reviewer preflight and the CEO phase.
|
||
Design, DX, Eng and the final gate do not run.
|
||
- **How it decides:** every question takes its recommendation and is logged in
|
||
the Decision Audit Trail. The review mode comes from plan-ceo-review's 0E
|
||
rule (file count, then greenfield, added capability or fix), not autoplan's
|
||
fixed SELECTIVE EXPANSION, and the mode line says `CEO-only run`. Consent,
|
||
destructive and irreversible options take the conservative choice.
|
||
- **What it never does:** apply a user challenge (the plan's direction stays;
|
||
each challenge is listed), chain to another skill, promote the CEO plan, or
|
||
approve anything.
|
||
- **One model:** add `--no-outside` to skip the outside voice, so every
|
||
reviewer runs on the host model.
|
||
- **Where it writes:** the run directory (`.gstack/tmp/autoplan/` in the
|
||
repository), the reviewed plan, and CEO plans and task files under
|
||
`GSTACK_STATE_ROOT`. Point that variable inside a trial folder to keep every
|
||
write there.
|
||
- **What it leaves:** the reviewed plan (its original is kept at the restore
|
||
point), `<run dir>/ceo-review.md` (user challenges, scope added,
|
||
conservative choices, findings and the audit trail), and the last line
|
||
`GSTACK_RESULT: skill=autoplan status=reviewed run=<dir>`.
|
||
|
||
`test/skill-e2e-autoplan-ceo-only.test.ts` runs it on a plan that makes the
|
||
interactive CEO review ask a question and fails if this mode asks anything.
|
||
|
||
**From the driver.** `gstack-autoplan next --out <dir> --plan <file>
|
||
--ceo-only` opens only the CEO phase; the parent dispatches and submits its
|
||
voices as usual. When CEO closes, the run prints `REVIEWED:` and ends
|
||
`GSTACK_RESULT: skill=autoplan status=reviewed run=<dir>`, with
|
||
`<dir>/ceo-review.md` in the same sections, hashed in `run.json`. Each item a
|
||
gate would have asked becomes a `decisions.jsonl` row of kind `auto` and status
|
||
`auto_kept` (nobody answered: the plan's direction is kept, and a round-cap item
|
||
only the owner can accept, such as a Critical left open, stays undecided with
|
||
no chosen option), and tasks are blocked on no decision. An open Critical or
|
||
High finding, which a full gate asks about, gets no row: nobody decides it, and
|
||
it stays in the report's Findings and in `tasks.jsonl` for the owner. The driver never edits the plan, writes no `gate.json` and never
|
||
approves, so `gstack-artifact validate` accepts the run as it stands.
|
||
|
||
- The flag goes on the first `next` only. It refuses beside `--ui`,
|
||
`--developer-tool` or `--agent-primary` on the command line or in the plan's
|
||
scope marker, when the marker names it, and on a plan at the report path
|
||
(`CEO_ONLY_CONFLICT`); a directory that holds a full run refuses it
|
||
(`CEO_ONLY_AFTER_INIT`).
|
||
- `answer`, `recommend` and `revise` refuse a CEO-only run, and so does
|
||
`gstack-approvals import` (`CEO_ONLY_NO_GATE`): there is no gate to answer and
|
||
nothing was approved. A run stopped by `--spend-cap` recovers as a full run
|
||
does: `budget` then `resume`, or `close --phase ceo --deferred`, which ends
|
||
it `reviewed`.
|
||
|
||
### Shadow mode
|
||
|
||
Shadow mode measures how often a model decider would pick what the owner
|
||
picks, before the CEO-only mode is allowed to decide for him. In a driver run
|
||
that turned it on, every gate the driver publishes gets one hidden prediction
|
||
per question, written before anyone answers: `action` (`decide`,
|
||
`send_back` or `hold`), `pick` (one of that item's options), `confidence`
|
||
(0 to 1), `cites` (finding ids from that item) and `reason` (at most 400
|
||
characters). The owner still gets the gate and answers it as usual; nothing
|
||
routes differently. Taste-replay round 2 (R2, in gstack-evals) compares the
|
||
predictions with his answers.
|
||
|
||
- **Off by default.** A crew turns it on with `gstack-config set
|
||
autoplan_shadow on`; `GSTACK_AUTOPLAN_SHADOW=1` or `0` overrides it for one
|
||
run. It is read once, at the run's first `next`, and stored in `run.json`
|
||
(`shadow`), so a resumed run cannot flip it. The first `next` prints
|
||
`SHADOW: on model=<m> ...`, or `SHADOW: off (<reason>)` when it was asked for
|
||
but cannot run: no API key for the model's family, a model with no price in
|
||
`lib/pricing.ts`, `--no-outside`, or a CEO-only run. R2's collection window
|
||
starts at the first recorded row on a crew gate, not at this release: check
|
||
one `SHADOW: recorded` line on a real gate before counting days.
|
||
- **The model and the cost.** `autoplan_shadow_model` (default
|
||
`claude-opus-5-5`) gets one call per gate: about $0.07 on your own API key.
|
||
The call sends the reviewed plan's Implementation plan section (cut to 40,000
|
||
characters) and the gate questions to that provider, with an egress receipt
|
||
of sink `autoplan-shadow` and consent `autoplan_shadow on`. Each call is
|
||
reserved against `--spend-cap` under its own ledger id labeled `shadow` and
|
||
shows as `shadow=<usd>` on `GATE_SPEND`; it never changes the per-voice price
|
||
on `GATE_REC_COST`. When the cap cannot cover a call and its retry, the
|
||
shadow is skipped (`SHADOW: failed budget`).
|
||
- **The decider.** `autoplan/shadow-decider.md` is a frozen, hand-written
|
||
prediction prompt (it is not generated from the CEO-only section). It shows
|
||
the reviewers' recommendation as one labeled input and gives the six decision
|
||
principles as the owner's tendencies, not as rules, so the model can
|
||
disagree with the recommendation. Its sha256 and version are on every row;
|
||
an edit bumps both.
|
||
- **The reply.** Exactly one JSON object per question. A duplicate, missing
|
||
or invalid object marks that item malformed and the page is asked once more;
|
||
items still malformed are recorded malformed with no pick. A reply cut at the
|
||
4,000-token output limit is asked once more at 8,000 instead. One 90-second
|
||
deadline covers both calls.
|
||
- **When it runs.** Every driver command that prints a gate (`next`,
|
||
`submit`, `close`, `resume`, `revise`, `run` and `check`) runs the
|
||
shadow first, prints `SHADOW: recorded model=<m> usd=<x>` or `SHADOW: failed
|
||
<code>` (status only, never a pick), then the gate, then checkpoints, so a
|
||
checkpoint taken at a gate holds its record. A whole-gate carry asks nothing,
|
||
so it gets no shadow. Interactive `/autoplan` and CEO-only runs write none.
|
||
A shadow failure (`budget`, `no_key`, `timeout`, `http`, `network`,
|
||
`truncated`, `unsafe_dir`, `locked`, `internal`) never blocks the gate or the answer. A
|
||
failure while publishing the record (`locked` or `internal`) is journaled
|
||
(`shadow_failed`) under the run lock and never throws; if the lock stays
|
||
taken, the lease expires and the next command settles that revision.
|
||
- **The lease.** The run lock is not held during the call: the command takes
|
||
a lease (`shadow/r<rev>.pending`, with its pid, host and deadline) under the
|
||
lock, calls the model, then retakes the lock (waiting up to 30 seconds) to
|
||
publish. Another writer that finds a live lease prints `SHADOW: pending` and
|
||
pays nothing, and `answer` waits for the record. A lease whose process died
|
||
or whose deadline passed is a crash: its sent calls settle `unknown` at
|
||
their reservation (they may have been billed), and `answer` records that
|
||
revision first with every row `retrospective: true`, which R2 excludes from
|
||
prospective evidence.
|
||
- **The record.** `shadow/ceo-only.r<rev>.jsonl`, one row per gate item: the
|
||
frozen item, `item_class` (`challenge2`, `challenge1`, `disagree`, `agent`,
|
||
`cap_deferred`, `cap_blocker`, `cap_delta`, `finding`, `p1`), `ceo_eligible`
|
||
(false for a `finding` item, which a CEO-only run leaves to the owner), the
|
||
prediction, `rule_pick` (the driver's keep-direction pick, the free
|
||
baseline), `rec_hash`, the plan and prompt hashes, the model, the gstack
|
||
version, the page's tokens and cost, and `audit`. It is written once per
|
||
revision with a link, so a second writer never overwrites it, and its sha256
|
||
is journaled (`shadow_recorded`). `gstack-artifact validate <run.json>`
|
||
checks each record against that hash and its schema, and reports a
|
||
journaled revision whose record file is missing as `ARTIFACT_STALE`.
|
||
- **The 10% audit mark.** A `decide` row carries `audit: true` when
|
||
sha256(salt + decision id) mod 1000 < 100, with the salt drawn once per run
|
||
into `shadow/salt`. Nothing routes on it yet; the live switch that lets the
|
||
mode decide alone will send these items to the owner.
|
||
- **The reader.** `gstack-autoplan shadow --out <dir> [--json]`, or `--runs
|
||
<file>` with one run directory per line, exports one row per item of each
|
||
answered gate revision: the shadow status (`recorded`, `malformed`,
|
||
`failed` with its code, `missing`, `retrospective`, or `off` when the run
|
||
never turned it on), the prediction, and the owner's answer with its
|
||
provenance. It prints counts by status, item class, `owner_mode` and
|
||
`ceo_eligible` and computes no agreement number. A revision whose plan
|
||
approval is unanswered or answered `c` (interrogate) refuses
|
||
`SHADOW_BEFORE_ANSWER`, and a CEO-only run `CEO_ONLY_NO_GATE`. The refusal is
|
||
a courtesy for honest tools, not access control: the journaled hash is the
|
||
tamper evidence.
|
||
|
||
## Owner brief
|
||
|
||
`gstack-owner-brief render --out <dir>` is what the owner sees at the final
|
||
gate, interactive or unattended: one page built from `gate.json`,
|
||
`decisions.jsonl` and `findings.jsonl` (`BRIEF_INPUT_MISSING` when one is
|
||
absent). It opens with the `URGENT, outside this plan` block, then a
|
||
plain-language summary (phases closed, findings accepted, how many decisions
|
||
take their default, the cycle time; `--summary <file>` or `<dir>/summary.md`
|
||
replaces the generated sentence with the run's own), then the numbered
|
||
decisions through the gate list: seven per page (`--page N`), stable ids,
|
||
each `auto` item annotated with the default taken if unanswered, each
|
||
`approval` item `pending until you answer` or `decided <id><letter> on <date>`.
|
||
It carries a `Cost so far:` sentence (spent, worst case, cap, and the estimate for
|
||
the phases `rec` can reopen); `render --captain` puts it in a `### Cost` section above
|
||
the relaying-agent trailer. The last line names `review-record.md`, where the full review lives, and the
|
||
`gstack-autoplan answer` call that answers the page. `--write` also saves
|
||
`brief.md` in the run directory. `/autoplan` Phase 4 presents this output
|
||
verbatim and writes the former long report to `review-record.md`.
|
||
|
||
After the final gate, `gstack-owner-brief close --out <dir> --plan <file>`
|
||
rewrites every `draft direction stands until the owner decides` line in the plan
|
||
to the decided option and date (`DRAFT_DIRECTION: line <n> <id> rewritten`);
|
||
lines whose decision is still pending stay and are reported as
|
||
`DRAFT_DIRECTION_UNRESOLVED` (exit 1). `gstack-autoplan answer` runs the same
|
||
rewrite when a reply completes the gate (journal event `draft_directions_closed`).
|
||
|
||
## Lane done: pregate, the sealed guard and the handoff lines
|
||
|
||
A lane under a coordinator runs `gstack-pregate run --allow-repo-commands` on
|
||
its branch before it reports done (the unit tier on the changed files plus
|
||
the repo's guards, [docs/pregate.md#guards](pregate.md#guards)). In a repo
|
||
whose `.gstack/pregate.json` has a `sealed` key it runs `gstack-pregate sealed`
|
||
before any push or PR creation, since a PR tool may not run local hooks. Its
|
||
handoff carries, right after the `GSTACK_RESULT` line, the `gstack: ok|fail
|
||
<rev>` line and then the `PREGATE:` line. The coordinator contract
|
||
(`gstack-autoplan contract`) states the same rule word for word.
|
||
|
||
## Timing
|
||
|
||
`/autoplan` prints `ESTIMATE: ...` at Phase 0 (`gstack-autoplan-timing
|
||
estimate --run <id> --out <dir>`, medians of the last ten recorded runs per
|
||
phase, or `no history`), records each phase close (`gstack-autoplan-timing
|
||
close`), and prints the `CYCLE:` line at the final gate (`gstack-autoplan-timing
|
||
summary`). A close without an explicit start measures from the previous phase's
|
||
end, else from the run start. The cross-run history lives at
|
||
`<state root>/analytics/autoplan-timing.jsonl` and is skipped, with a printed
|
||
line, on an ephemeral state root.
|
||
|
||
## Reference run
|
||
|
||
`test/fixtures/multi-agent-wave/reference-run/` is a complete unattended
|
||
`/autoplan` run (the one that produced this wave's plan) and validates clean.
|
||
`test/fixtures/multi-agent-wave/INCIDENTS.md` records what went wrong while it
|
||
was produced by hand and which validator rule now catches each incident.
|
||
|
||
## Install admission
|
||
|
||
An unattended run, and a `GSTACK_SESSION_KIND=spawned` run with `GSTACK_CAPY=1`,
|
||
starts only on a supported Bun and, on a Capy machine (`GSTACK_CAPY=1`, set by
|
||
the installer's `GSTACK_LAUNCH` line), with a passing install verdict for this
|
||
checkout, its HEAD and the workflow. Otherwise `gstack-skill-start` prints
|
||
`INSTALL: fail runtime|verdict — <detail>; fix: <command> (<CODE>)` and
|
||
`GSTACK_RESULT: skill=<s> status=refused run=-`, and exits 3; stop and run the
|
||
fix. `GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1` admits anyway (`INSTALL_VERDICT:
|
||
overridden`), recorded as a deviation. TypeScript entrypoints use
|
||
`checkInstallAdmission` from `lib/install-verdict.ts`; the Bash side is
|
||
`bin/gstack-install-verdict.sh`. See [capy-install.md](capy-install.md#the-launch-line).
|
||
|
||
## The decision trail
|
||
|
||
A long or unattended run keeps one tab-separated trail of its decisions at
|
||
`<run dir>/decisions.tsv`, written through `gstack-decision-trail <file>
|
||
<phase> <decision> <why> <evidence> <result>`, so the parent reads what the run
|
||
chose and why without the transcript. The helper stamps the time, writes the
|
||
header once, flattens each cell to one line and quotes a cell that starts
|
||
with `=`, `+`, `-` or `@`, so evidence lifted from a PR title or a filename
|
||
cannot run as a formula when a reviewer opens the file in a spreadsheet.
|
||
Log the forks, not the keystrokes: an approach chosen or dropped, a unit done
|
||
with its verification result, a revert and what triggered it, a blocker, one
|
||
row per iteration of a loop. Evidence is a pointer that resolves (a commit, a
|
||
PR, `file:line`, an artifact path), never prose. A run that joins a trail
|
||
other runs wrote (a pickup after a crash, a replacement machine, a later turn
|
||
of the same thread) first runs `gstack-decision-trail start <file> <run id>`,
|
||
which appends a `start` row naming the earlier rows and the run; appends with
|
||
`--run <id>` refuse (`DECISION_TRAIL_START_REQUIRED`) until that row is the
|
||
newest start row, and `start` is a no-op when it already is, so a rerun of the
|
||
same step writes nothing twice.
|
||
|
||
Before it hands back, the run audits the trail against what it did: every row
|
||
of its own stretch (from its `start` row to the next run's) maps to a real
|
||
decision or action, every evidence pointer resolves and shows what the row
|
||
claims, and a fork or abandoned approach that shaped the result but is not
|
||
logged gets a row. The audit corrects the log, never the story: no row is
|
||
edited or removed, an invented or wrong row stays and a new row supersedes it
|
||
with what actually happened. Then a reviewer on a different model family from
|
||
the one that did the work (a Capy subagent on another family, or the outside
|
||
voice) reads the trail and the transcript and flags what the owner should look
|
||
at: decisions with weak or missing evidence, verification claimed without
|
||
proof, choices that look risky in hindsight, gaps a skim would miss. The run's
|
||
final report ends with an `Attention` section that opens with `reviewed by
|
||
<model>` on its own line and lists each flag against its rows; `No flags` is a
|
||
valid body, a missing model line is not. Self-review does not stand in for
|
||
this. `decisions.jsonl` stays the typed record `gstack-artifact` validates; the
|
||
trail is the plain one a person reads, and `column -s$'\t' -t decisions.tsv`
|
||
renders it in a terminal.
|
||
|
||
## What an unattended session may do
|
||
|
||
A parent that starts a skill with `GSTACK_SESSION_KIND=unattended` grants it
|
||
exactly this much, and the run asks for nothing beyond it. Anything outside the
|
||
list is an approval item: appended to `decisions.jsonl` with `kind: "approval"`
|
||
and `status: "pending"`, named in the handoff, never taken.
|
||
|
||
- **Merge: never.** The run ends at a receipt bound to the pushed head
|
||
(`SHIP_RECEIPT`, [ship-policy.md#ship-before-ready](ship-policy.md#ship-before-ready))
|
||
or at a pending gate. Merging is the controller's act
|
||
([captain.md](captain.md)); a green check list, an approval, or a prompt that
|
||
says "merge when green" does not move it to the lane. The same holds for
|
||
enabling auto-merge, adding the PR to a merge queue, or `gh pr ready` on a
|
||
PR the run did not open.
|
||
- **Force-push: its own branch only, after a rebase the plan named, with a
|
||
pinned lease.** The branch is one the run created or was handed in its
|
||
prompt, never the base. The rebase is one the plan, the slot call or a
|
||
`/modernize` disposition names before it happens, not one the run decides
|
||
on its own; `git range-diff <old base>..<old head> <new base>..<new head>`
|
||
runs before the push and its output goes in the handoff. The push is
|
||
`git push --force-with-lease=<branch>:<sha observed before the rebase> origin <branch>`:
|
||
a bare `--force-with-lease` protects only against commits this clone never
|
||
fetched, and a plain `--force` is never allowed. A rejected lease means the
|
||
remote moved: fetch, read what landed, and start the disposition again.
|
||
`/ship` itself never rebases or force-pushes, and an unattended `/modernize`
|
||
stops at its verdict; this route is for a run whose plan or slot call named
|
||
the rebase before it started.
|
||
- **Tests: the methods the repository's instructions name, without asking
|
||
first.** AGENTS.md or CLAUDE.md, the named Setup commands, `package.json`
|
||
scripts and the pregate configuration define the set; running one of them
|
||
needs no question. A method outside the set (a framework the repo does not
|
||
use, paid work past the stated cap, anything that touches a shared or
|
||
production system, a test that writes outside the checkout) is pending.
|
||
- **Proof is what the run saw itself.** Command output, a test that failed
|
||
before the fix and passes after it, a screenshot, a file read in this
|
||
session. A handoff note, an earlier PR comment or a parent's summary is
|
||
context to recheck, not evidence; the handoff says which claims were
|
||
rechecked and which were not.
|
||
|
||
`/ship`'s handoff section (`ship/sections/handoff.md`) points here, so a lane
|
||
reads the grant where it reports done.
|