Files
gstack/docs/unattended.md
T
Garry Tanandcapy-ai[bot] ada251bbc0 v1.91.96.0 fix: honest install and self-upgrade on Capy (field wave A, items 1-5) (#3164)
* 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(doctor): --min-revision, a launch Bun found off PATH, planning-only pins SKIP

gstack-doctor --check gains --min-revision <v>|unknown: below it the revision
row FAILs with REVISION_BELOW_MINIMUM naming the Capy wrapper or
bin/gstack-upgrade; unknown says so and never claims the minimum. The runtime
and pins rows name a supported Bun found in $BUN_INSTALL/bin (not on PATH)
with the export/GSTACK_LAUNCH fix (gstack_bun_launch_dir, one helper in
bin/gstack-bun-version.sh). --for a planning-only workflow (autoplan, the plan
reviews, office-hours) reports a repo test-pin mismatch as SKIP pins
(planning-only; ...). New result codes UPGRADE_REFUSED, UPGRADE_FETCH_FAILED,
REVISION_BELOW_MINIMUM, CAPY_WRAPPER_STALE, INSTALL_UNVERIFIED with
troubleshooting anchors; REVISION_UNMET's fix names bin/gstack-upgrade.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(install): install verdict and install admission at skill start

bin/gstack-install-verdict.sh (Bash) and lib/install-verdict.ts (TypeScript)
are the one reader of the install verdict gstack-capy-install writes to
<state root>/install-verdicts/<sha256(checkout realpath)>.json (checkout,
HEAD, workflow and required components, planning-only pins, trailer). The
source checkout and capy/support.env resolve through gstack_source_root /
sourceRoot, which follow an installed root's .source-path.

gstack-skill-start, after SESSION_KIND: an unattended session (any host)
refuses on a PATH Bun below the supported minimum; with GSTACK_CAPY=1 an
unattended or explicit spawned session also refuses without a passing
verdict for this checkout, HEAD and workflow (INSTALL_UNVERIFIED). A refusal
prints the INSTALL: row, an install-refused instruction block bound to its
SESSION_ID, GSTACK_RESULT ... status=refused run=- and exits 3. Ambient
spawned sessions warn; GSTACK_ALLOW_UNSUPPORTED_RUNTIME=1 admits and says so.
capy/support.env declares GSTACK_MIN_REVISION and GSTACK_CAPY_MIN_WRAPPER.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(capy-install): launch PATH, Bun reuse and CI pins, setup skip on HEAD, verdict, --check

The GSTACK_LAUNCH line carries PATH=<dir>:$PATH when the certified Bun is not
first on PATH, plus GSTACK_CAPY=1 and GSTACK_INSTALL_VERDICT, and
GSTACK_LAUNCH_PATH: <dir>; the doctor runs with that PATH. A supported Bun in
$BUN_INSTALL/bin is reused instead of downloaded again. Project pins are read
with bin/gstack-runtime-pins.ts (CI lanes included): when every exact pin is
gstack's verified version the archive is installed alongside a different PATH
Bun; any other exact pin is RUNTIME_PIN_CONFLICT. Both ./setup runs are
skipped when this HEAD was set up with the same hosts, model and browser flag
and the check passes (never after a fast-forward); a failed setup ends with
gstack: fail setup and is retried next run. --check certifies only;
GSTACK_CAPY_LOCK_HELD=1 accepts the bootstrapper's held lock; result anchors
come from the generated components table; REV_NOTE names bin/gstack-upgrade.

Intentional pin change: test/gstack-capy-install.test.ts --revision trailer
now names <root>/bin/gstack-upgrade (the old text named a command that never
existed).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(upgrade): bin/gstack-upgrade, the unattended fast-forward every fix line names

--check fetches and prints current|behind <installed> -> <remote> (<n>
commits); the default fast-forwards to the remote default branch tip and
reinstalls (bin/gstack-capy-install on a Capy install, else ./setup
--refresh-registered), completing a pending reinstall on the next run. It
refuses (UPGRADE_REFUSED, exit 3, nothing changed) a dirty tracked byte, a
diverged commit, a pinned checkout, the incoming Bun floor and a held
.setup-lock; never stashes or resets; an unreachable remote is
UPGRADE_FETCH_FAILED. Fetches are receipted (fail-open). The unattended
skill-end UPGRADE_AVAILABLE line is followed by UPGRADE_COMMAND naming it.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(capy): vendored Capy skill, frozen bootstrapper and full wrapper bin/gstack-capy-session

capy/skill/gstack/ is the Drive skill's source of truth. scripts/install.sh
is a frozen bootstrapper: it prints gstack-capy-skill: wrapper <n> (<path>),
clones or fetches under .gstack-capy-install.lock, reads the remote
capy/support.env (FETCH_HEAD), fast-forwards a clean unpinned checkout below
GSTACK_MIN_REVISION (refusing dirty, diverged or pinned checkouts with the
exact commands; GSTACK_CAPY_AUTO_UPGRADE=0 disables), and execs the
checkout's own bin/gstack-capy-session with GSTACK_CAPY_LOCK_HELD=1 so the
held lock is never taken twice. Offline it says the remote minimum is
unknown. A Drive copy older than GSTACK_CAPY_MIN_WRAPPER prints an owner
action (CAPY_WRAPPER_STALE). SKILL.md: check at session start, install
lazily before the first workflow, the GSTACK_LAUNCH line, the trailer rule,
the subagent first-report line, gstack-pregate sealed before any push, and
the autoplan/DRIVER.md pointer. The context-budget fixture gains the skill's
entry.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: Capy install layers, lazy install, snapshot bake, forward-only recovery, raising the minimum

docs/capy-install.md describes the three layers (Drive bootstrapper, full
wrapper, installer), the lazy install, the GSTACK_LAUNCH line, install
admission, baking gstack into a Capy snapshot, and that recovery after an
interrupted fast-forward completes or refuses the pending install stages
(checking out an older commit is not a rollback). docs/CHANGELOG_STYLE.md
(the release doc; there is no docs/RELEASING.md) says when
GSTACK_MIN_REVISION may be raised.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* refactor(capy): the stale-copy owner action moves from the frozen bootstrapper to bin/gstack-capy-session

The checkout-owned full wrapper reads GSTACK_CAPY_MIN_WRAPPER through
gstack_support_env_get(gstack_source_root) and compares it with the
bootstrapper revision it was handed, so the rule can change without
recopying the Drive skill. The bootstrapper keeps only what a stale checkout
cannot do: clone, fetch, read the remote minimum, fast-forward, exec.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* v1.91.96.0: honest install and self-upgrade on Capy; supported minimum 1.91.96.0

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>

* test: bound the new sync spawns in the install-verdict and wrapper tests (spawnSync tripwire)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* ci(paid-pr-profile): the vendored Capy skill is free-only; free-only files are not prompts needing paid coverage

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore(harness): non-behavioral HARNESS_VERSION decision for the PR-profile free-only rule

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(doctor): lane A runtime and pins cases are platform-neutral (bash paths; a lane-free .bun-version pin)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(designs): field wave plan with amendments and the gate rev 2-3 review record (ARTIFACT_VALID, 3 gate revisions)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(capy-install): follow a project's exact Bun pin above the minimum when gstack has its verified archive (adds Bun 1.4.3)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: the installer follows a verified exact project Bun pin (capy-install, troubleshooting, changelog)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(capy-install): the install reason names the project's pin and gstack's real minimum

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>
2026-10-13 14:41:58 +00:00

19 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 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.
  • 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).
  • 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

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

  1. Decide whether the run finished. gstack-artifact validate <dir>/run.json is the deterministic completion check. 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.
    jq -c 'select(.status=="pending")' <dir>/decisions.jsonl
    gstack-gate render <dir>/gate.json          # the numbered list and reply grammar
    
  4. Answer the gate by composing the reply grammar (all, <id><option> tokens such as d3b uc1a, or all except <tokens>; bare yes/no is rejected) and feeding it back with the gate revision you read:
    gstack-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.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.
    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.

AP=bin/gstack-autoplan
$AP next --out <dir> --plan <plan.md> [--ui|--no-ui] [--developer-tool] [--light] [--spend-cap <usd>]
#   PHASE: ceo snapshot=<sha> order=ceo,design,dx,eng
#   Skipped sections: none (scope: ui=yes,dx=yes,source=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>
  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.
    $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=both
    
    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 / disagree / new per 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).
  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 and p1 plan approval; nothing is approved.
  4. Answer the gate with the reply grammar and the revision you read:
    $AP answer --out <dir> --gate-rev 1 --reply "all"              # APPROVED: status=complete, no new paid phase
    $AP answer --out <dir> --gate-rev 1 --reply "all except d1b"   # REOPENED: ceo,eng (Eng last); next continues the loop
    
    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 non-recommended choice on a reviewed item reopens that phase plus Eng, archives their bound files as *.r<rev>.*, and the next gate is gate_rev + 1; p1d reopens every phase; 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).

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.

  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; "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).

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).

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.