# 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 ` 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= queue= 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: ; run /gstack-upgrade` (`+ (main at )` when main moved 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: 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 '')`. ## The terminal line A run that writes a record ends with one line the parent greps: ``` GSTACK_RESULT: skill= status= run= ``` `status` is one of `complete`, `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. 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= status=refused run=-` and stops. - **`/ship`** ends with `gstack-ship-receipt result --pr ` (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=`. 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 --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. ## 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 ` (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 `/ship/runs/-/` and prints the terminal line: ``` GSTACK_RESULT: skill=ship status=complete run= ``` 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= head= …` 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; and `result --pr ` refuses `QUEUE_RECEIPT_STALE` when HEAD is not the PR's pushed head. 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 `, 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` (distinct blocking findings across them), `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= blocking_found= deferred=`; 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 ` (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 `.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 --model --prompt --out ` 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 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 --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 to ()` plus the `GSTACK_RESULT: skill=ship status=complete run=` 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. `CARRY: current` means nothing moved. `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` (`-