* docs(designs): gstack field wave plan (2026-10-12) and the shared simulated Capy host fixture
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(pregate): price_command config key for the non-executing call plan a paid gate is priced from
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(pregate): repo guards a lane must pass, verified launch Bun and per-file serial runs in lanes, unknown flags exit 2
The guards check runs each .gstack/pregate.json guard whose globs match a
touched file in its own process with its own timeout (PREGATE_GUARD_FAILED,
never downgradable, incomplete without --allow-repo-commands). The lanes
runner and guards get the first Bun in $BUN_INSTALL/bin or ~/.bun/bin that
satisfies gstack's and the project's engines.bun; serial_patterns files run
one process each. price_file replaces the unreleased price_command key: a
paid gate is priced from a call-plan data file, never by running a command.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(pricing): Sonnet 5.5, Gemini 3.x and gbrain embedding rows from fetched pages; Opus 4.7 at its current list price
Each new row carries its source URL and fetch date (2026-10-12). Embedding
models bill input only, so they live in EMBEDDING_PRICING beside PRICING
(eval arms never resolve to them); pricingRow also accepts gbrain's
provider:model ids (voyage:voyage-4). claude-opus-4-7 moves from 15/75 to the
5/25 the Anthropic pricing page lists; benchmark-runner's expected costs follow.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(eval-plan): price-gate prices a paid gate from a call-plan data file, never executing it
gstack-eval-plan price-gate --gate "<exact command>" reads a GSTACK_EVAL_CALLS
v1 file (--plan-file, else the base policy's price_file at HEAD), checks its
binding (gate command string equality, head with only the plan file changed
since, every input's sha256 at HEAD), prices each call purpose (aggregate
totals) and each one-call reservation, and writes a receipt binding the price
to the gate, head, inputs and plan bytes. Unpriced models print unknown, never
zero (EVAL_PRICE_MISSING); a missing plan or no call lines is
EVAL_GATE_UNPRICED unless --allow-unpriced records an owner override; over
--cap-usd / --smoke-cap-usd is EVAL_GATE_OVER_CAP; malformed or partial files
refuse EVAL_DRY_RUN_FAILED and stale ones EVAL_PLAN_STALE. dry-run --cap adds
the same reservation-fits-cap check. Wire format, receipt and the reference
emitter (formatEvalCalls): docs/eval-calls.md.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(eval-plan): the approval step prices every paid gate with price-gate
Step 4 runs gstack-eval-plan price-gate for each paid gate the plan names and
puts its GATE_PRICE line in the ask; the dry-run checklist documents
reservation-fits-cap under --cap.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(sealed): sealed-output guard over every published commit, in the pre-push hook and gstack-pregate sealed
A repo with a sealed key in .gstack/pregate.json (base branch union the
pushed commit, so a push can tighten but never relax) refuses publishing
sealed paths, transcripts, quoted answers, seeds or local home paths to a
public or unknown destination (SEALED_OUTPUT_REFUSED); private warns.
Path rules see every added, renamed or copied path in each published
commit (git rev-list <local> --not --remotes=<remote>), text rules each
commit's added lines; waivers only from the base branch allow file; a
malformed policy fails closed; visibility caches public 24 h, private 1 h.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix(eval-plan): only an undeclared plan is unpriced; a missing price_file is stale, a bad base policy refuses
price-gate's EVAL_GATE_UNPRICED (and --allow-unpriced) now covers only no
plan declared at all or a plan with zero call lines. A declared price_file
missing from the HEAD commit is EVAL_PLAN_STALE (exit 1, not overridable); an
unresolvable origin/<base> is POLICY_SOURCE_UNAVAILABLE and an invalid
.gstack/pregate.json is PREGATE_CONFIG_INVALID (both exit 3).
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(restamp): the repo's stamp rule (stamp_files, literal stamp_patterns) and restamp_command in the staging worktree
stamp_files is an allowlist applied before every stamp rule; stamp_patterns
are bounded literal {version} templates shared by the restamp writer and the
tree receipt. restamp_command runs first in the staging worktree with the
target version (argument and GSTACK_RESTAMP_VERSION), needs a clean caller
tree, skips gstack's version writer and keeps output confinement; a failure
or a wrong version writes nothing. migrations { dir, id } is the policy key
gstack-pregate regate reads.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(pregate): gstack-pregate regate --against <gated-head> with per-lane reuse and migration collisions
Reuse comes only from a tree receipt evaluated now, with a bundle produced
on the gated tree, the current execution identity and an unmoved, known
predecessor head. Each lane prints reusable/rerun/not-reusable; REGATE:
not-needed needs stamp-only, every bundle and --require lane reusable (a
stamp-sensitive lane reruns) and a finished migration lookup. Migration
numbers the branch adds are checked against the base tree and every other
open PR (gh pr list --limit 1000; 100-file lists refetched with gh api
--paginate); any lookup that cannot finish is unknown and blocks
not-needed (MIGRATION_COLLISION). Bare --against routes to the verb.
docs/pregate.md gains the regate section and the gbrain worked configs.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* feat(contract): lanes run gstack-pregate (and the sealed guard where configured) before done; handoff carries the gstack trailer and PREGATE: line
The coordinator contract gains a 'Before done' paragraph and the handoff
order GSTACK_RESULT, gstack: ok|fail <rev>, PREGATE:, gate summary; the
spec section is regenerated and docs/unattended.md states the same rule.
A new test pins the terms and their order; the byte-identity pins stay.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* v1.91.98.0: pregate guards, priced paid gates, regate and the sealed-output guard
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* test: drop the helper-only Capy host fixture test (test-of-test ratchet); lane tests exercise the helper
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: price-gate --gate-file keeps free text out of the /eval-plan fence; shortest dependency traces; bounded test spawns
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* ci(windows): curate out the new pregate/restamp tests that spawn bin/ shebang scripts through runBin
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>
23 KiB
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 and
UNATTENDED_SESSION: true, 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 isoff. - 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-enqueueis a no-op, andgstack-review-logsaysreview-log: sync skipped (unattended session)on stderr), andgstack-skill-enddrains 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. - Questions decide themselves, gates do not. A question with a
(recommended)option takes it and appends the choice todecisions.jsonlwithkind: "auto". A question with no recommendation, any consent, any destructive or irreversible option, and the final approval gate are appended withkind: "approval"andstatus: "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(nowhen the host setsGSTACK_EPHEMERAL=1). On an ephemeral root the run sayslearnings: skipped (state root is ephemeral; set GSTACK_STATE_ROOT)andtiming: analytics skipped (...)instead of writing to a disk that vanishes. PointGSTACK_STATE_ROOTat a durable directory when you want cross-run history (see 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-startprintsautoplan guard: not enforced by this host; publication order is unverified (GUARD_NOT_INSTALLED)once.gstack-artifact validaterequires that line in an unattended run'sreview-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
Every workflow skill ends an unattended run with one line the parent greps:
GSTACK_RESULT: skill=<name> status=<status> run=<dir>
status is one of complete, gate_pending, incomplete, interrupted or
refused; run is the directory that holds the artifacts below. /autoplan
always ends gate_pending when it reaches the final gate, because the gate is
written and never approved. Pure queries (gstack-artifact schema,
gstack-gate render) print no result line.
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 with
a fix: clause. Grep the code, not the prose.
The run directory
| 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), gate_rev, and once answered chosen, answered_at, reply. |
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
- Decide whether the run finished.
gstack-artifact validate <dir>/run.jsonis the deterministic completion check. It exits0and printsARTIFACT_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,countsagree with the files, and (unattended) the guard line is present. Otherwise it exits1with one error line per problem andARTIFACT_INVALID: <n> error(s);--jsonreturns the same as{ valid, run, status, run_dir, errors[] }. A run that was interrupted shows up here asARTIFACT_RUN_INTERRUPTED, never as a partial success. - Read the terminal line.
grep '^GSTACK_RESULT:' <log> | tail -1. Withstatus=gate_pending, the gate is in the artifacts; nothing was approved. - List what is pending.
jq -c 'select(.status=="pending")' <dir>/decisions.jsonl gstack-gate render <dir>/gate.json # the numbered list and reply grammar - Answer the gate by composing the reply grammar (
all,<id><option>tokens such asd3b uc1a, orall except <tokens>; bare yes/no is rejected) and feeding it back with the gate revision you read:A stale revision isgstack-gate parse <dir>/gate.json --reply "all except d3b" --gate-rev 1 --json gstack-gate decisions <dir>/gate.json --reply "all except d3b" --gate-rev 1 >> <dir>/decisions.jsonlGATE_REV_STALEand a reply with unparsed tokens isGATE_REPLY_UNPARSED; both exit1and write nothing. - Read findings and tasks without the prose.
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." - 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 recordsconsumed_byin 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.
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.md model_family=...
# ATTEMPT: <run>-a2 phase=ceo voice=outside prompt=<dir>/ceo-outside-prompt.md result=<dir>/ceo-outside.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.
- 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, orbin/gstack-outside-voice run --runner codex-cli|api --prompt <file> --out <file> [--model <id>], which printsOUTSIDE_STATUS: <status> provider=<runner> model=<id> family=<f>. Every reviewer writes its result to the printedresult=path under the RESULT FORMAT below.nextis idempotent: it reprints open attempts and never redispatches on its own (EXECUTION_UNKNOWNlists attempts whose process died without a result;--redispatch <attempt>mints a fresh one andrelease --attempt <id>returns its reservation). - Submit each result exactly once.
Binding checks the receipt line against the phase snapshot (
$AP submit --out <dir> --phase ceo --voice native --result <dir>/ceo-native.md --model <id> --attempt <run>-a1 $AP submit --out <dir> --phase ceo --voice outside --result <dir>/ceo-outside.md --model <id> --attempt <run>-a2 [--runner host-subagent] [--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=bothRESULT_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/disagree/newper outside finding, with disagreement and resolution kept separate) is written to<phase>-consensus.{json,md}, and the Eng prompt refuses to open without every closed prior phase's consensus (CONSENSUS_MISSING). - Loop
nextuntilstatus=gate_pending. Eng always runs last. The gate (gate.json,gate_rev) lists user challenges as approval items, disagreements as auto items andp1plan approval; nothing is approved. - Answer the gate with the reply grammar and the revision you read:
A user challenge both voices raised on a confirmed finding with the same
$AP answer --out <dir> --gate-rev 1 --reply "all" # every recommendation; agreeing plan changes print APPLY: $AP answer --out <dir> --gate-rev 1 --reply "all as-is" # APPROVED: keeps every direction, no new paid phase $AP answer --out <dir> --gate-rev 1 --reply "all except d1b" # APPLY: <id> ...; REOPENED: ceo,eng; NEXT: gstack-autoplan revise ...changesvalue is recommended asb, and option b shows both voices' remedies (recommended: b (both voices; remedies shown)); a--reconcilerecord naming one sharedproposalshows that instead. Disagreeing or missing remedies keepa. Every accepted plan change printsAPPLY: <decision id> <title> -> <chosen text>and becomes a pending amendment:nextrefuses withPLAN_AMEND_PENDINGandexportrefuses acompleterun untilrevise --incorporatesnames it (see below). A stale revision (GATE_REV_STALE, exit 3), bareyes/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. A choice that changes the plan reopens that phase plus Eng, archives their bound files as*.r<rev>.*, and the next gate isgate_rev + 1;p1dreruns every phase on the current plan;p1eendsincomplete. - Export and validate.
$AP export --out <dir>writesplan.md(with the## URGENT, outside this planblock first),review-record.md,tasks.jsonl,decisions.jsonl,findings.jsonl,timing.jsonandrun.json; thengstack-artifact validate <dir>/run.jsonand the consumption steps above apply unchanged.$AP status --out <dir>andresumework after the run directory moves (paths are re-rooted).
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. --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.
- The first line is exactly
INPUT: <phase> <snapshot sha256>(the receipt that binds the result to the snapshot both voices reviewed). - 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-findingsfence.severityis Critical | High | Medium | Low | Informational;"user_challenge": truemarks a recommended change to the plan's stated direction and needs"changes": "scope" | "cost" | "direction"(what accepting it changes);"urgent": truewith"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. - 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).
Hand-run records, revised plans and admission
- Hand-run reviews.
submit --record <file>binds a review that did not follow the RESULT FORMAT: thegstack-findingsfence, one findings table (Severity and Title columns), one severity-tagged list (- [High] title) or aNo findings.line. Without itsINPUT:line it binds only with--attest "<reason>"and--attempt <id>(receiptattested; refused after a laterrevise), andgstack-artifact validateprintsattested=<n>.--attemptmay be omitted when exactly one attempt is open.submit --voice outside --unavailable "<reason>"(or--failed) closes the phasenative_onlyonce native is bound;nextandresumenever 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 atgate_pendingmakes the next gategate_rev + 1.--incorporatesdischarges only the named amendments and refuses an unchanged plan;revise --out <dir> --withdraw <decision id>drops one as a journaled owner decision.nextprintsPLAN_CHANGEDandanswerprintsPLAN_SOURCE_CHANGEDwhen the source plan moved since the snapshot. A plan without both## Implementation planand## Review recordrefuses withPLAN_SECTIONS_INVALID;next --scaffoldreviews a copy with the missing heading added.resumecompletes an interrupted revise. - Admission and upgrades.
next,submit,answer,revise,exportandrunrefuse 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 onegstack-skill-startmakes (see Install admission);GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1overrides and records a run deviation.statusandresumeare never gated. A run directory written by an older gstack printsGSTACK_VERSION_CHANGEDonce and moves forward (legacy decision ids are renamed and journaled); one written by a newer gstack is refused.
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>.
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). 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.