# The repo ship policy: `.gstack/ship-policy.json`
`/ship` reads one machine-readable file per repository that says how releases
are stamped and merged: the bump level, the queue mode, the repo's own release
tool, the paths where version mentions are rewritten, the CHANGELOG, a paired
mirror, the history policy and the preregistered evals. Without the file
nothing changes: every key has a default that reproduces today's behavior.
The file is **read from the committed copy on the base branch**
(`origin/`), never from the PR's working tree, so a PR cannot smuggle a
command into its own gate. **File presence never authorizes execution:**
`gstack-restamp` runs `release_tool` only under `/ship` or with
`--allow-repo-commands`; standalone it lists the commands and exits 3 with
`repo commands not executed (pass --allow-repo-commands)`. A PR that
introduces or changes the policy runs under the reviewed bootstrap path
(`--policy-from head --allow-repo-commands`) and its receipt says
`policy: head (unreviewed)`.
Tools: `gstack-ship-policy init|validate|show-effective|check-history`,
`gstack-restamp`, `gstack-tree-receipt`, `gstack-ship-receipt`,
`gstack-ship-queue` ([shared merge queue](#run-a-shared-merge-queue), [merge trains](#run-a-merge-train)),
`gstack-evidence bundle|ancestor`. They share the exit table (`0` ok, `1`
fail, `2` usage, `3` refused or needs a flag) and end every error line with a
code from [troubleshooting.md](troubleshooting.md#queue-aware-ship-policy-restamp-tree-receipt-ship-receipt).
## Schema v1
`gstack-ship-policy init` writes every key at its default; `init --explain`
prints this table. JSON carries no comments, so examples live in
`.gstack/ship-policy.example.json`. Unknown keys are errors, and there is no
extension mechanism: a key from a newer gstack, or a misspelled one, fails
closed (`POLICY_INVALID`) instead of being ignored. Every key has an
introducing version (`init --explain` and `validate` print it). A key newer
than 1.91.98.0 set to a non-default value requires `min_gstack_version` at
least at its introducing version, and a gstack older than `min_gstack_version`
refuses the file (`POLICY_GSTACK_TOO_OLD`). `validate` ends with
`MIN_READER: gstack `, the floor every lane reading the policy needs.
| Key | Values | Default | Meaning |
|---|---|---|---|
| `bump` | `patch` \| `ask` \| `never` | `ask` | `patch` skips the MINOR/MAJOR prompt in `/ship` Step 12 and `gstack-restamp --next` bumps PATCH; `ask` keeps the prompt, and in a session no person answers any version Step 12 would write (`--bump` on a fresh branch, `--version ` on a bumped one) stops with `BUMP_ANSWER_REQUIRED` until `--answer ""`; `never` is for a repo whose unit PRs never change VERSION: Step 12 keeps it with no question, refuses `--bump`, and `gstack-restamp --next` needs an explicit `--bump` or `--version` (the release process bumps). CI's Version Gate passes a head that keeps VERSION; a release PR that moves it takes the ordinary bump and collision rules. `never` needs `min_gstack_version` at its introducing release and cannot be combined with `queue_mode: stamp-at-merge`, `train` or `changelog_fragments`, which stamp a version at every merge. |
| `queue_mode` | `claim-at-open` \| `stamp-at-merge` | `claim-at-open` | See [stamp-at-merge](#stamp-at-merge). `claim-at-open` is today's queue (`gstack-next-version` claims a slot when the PR opens). |
| `release_tool` | command or `null` | `null` | The repo's own release command (`bun run release:restamp`). Runs in an isolated staging worktree under the [release-tool contract](#release-tool-contract). |
| `release_outputs` | globs | `[]` | Paths the release tool may write beyond the version files, lockfiles, CHANGELOG and `stamp_paths`. Anything else is `RESTAMP_OUTPUT_OUTSIDE_ALLOWED`. |
| `stamp_paths` | globs | `[]` | Where exact `(was X)`, `since: X` and `vX` mentions of the branch's tentative version (quoted or bare: `since: 'X'` counts) are rewritten on a restamp. |
| `changelog` | path or `null` | `CHANGELOG.md` | Re-headed on a restamp: the PR's `## [Unreleased]` (or old-version) section moves to the top as `## [] - `. |
| `mirror` | relative path or `null` | `null` | A paired checkout to restamp identically (tier 2: validated and confined today, not yet run). |
| `history` | `merge-only` \| `squash-ok` | `squash-ok` | `merge-only` makes `/ship` refuse a squash or rebase plan; `gstack-ship-policy check-history` reports a rewritten branch (`HISTORY_POLICY_VIOLATION`). |
| `preregistration_shas` | commit ids | `[]` | Each must be an ancestor of HEAD before push: `gstack-evidence ancestor --from-policy`; the ship receipt records the result. |
| `stamp_sensitive_gates` | evidence labels | `[]` | Gates that read VERSION or generated bytes (gstack's own `tests` lane does, through the agents-digest freshness test). The tree receipt never marks them reusable. |
| `pr_count` | `{ default_pr_count, split_requires_owner }` | `{1, true}` | The PR-count row in plan reviews. |
| `maintainers` | GitHub logins | `[]` | Maintainer set for `/review` contributor mode; empty means repository push permission. |
| `stamp_files` | globs | `[]` | The repo's stamp rule: when set, only these files may differ by a stamp (list the version file, manifests, plugin trees and the files holding the PR's own `since:` rows). It applies before every other stamp rule, so a doc `vX` mention outside it is a difference. |
| `stamp_patterns` | literal templates | `[]` | At most 16 templates of at most 128 characters, each with exactly one `{version}` slot (`"version": "{version}"`, `since: '{version}'`, ``); matched literally at a version boundary, never as a regex. The restamp writer, its mention scanner and the tree receipt use them beside the built-in forms. |
| `restamp_command` | command or `null` | `null` | The repo's own restamp, run first in the staging worktree; see [restamp_command](#restamp-command). |
| `migrations` | `{ dir, id }` or `null` | `null` | The migration directory and a regex with exactly one capture group for the number in a file name (`"^v(\\d+)-"`); `gstack-pregate regate` reports `MIGRATION_COLLISION`. |
| `gate_command` | command or `null` | `null` | The repo's full pre-merge gate. `gstack-ship-queue gate-ahead` and `ready` print it for the caller to run (in the background or in CI); gstack never executes it. Needs `min_gstack_version`. |
| `full_e2e` | `true` \| `false` | `false` | `true`: every merge needs a passing full-gate result bound to a tree equal to the merging one modulo stamps; `gstack-ship-queue ready`, `stamp` and `verify` refuse without one (`QUEUE_GATE_MISSING`): a gate-ahead record, or the lane's `--gate-exit 0 --gate-log ` from its run on the gated head. Needs `min_gstack_version`. |
| `review_scope` | `frozen` \| `diff-plus-blocking` \| `all` | `frozen` | The finding scope of `/ship`'s review loop; see [review_scope](#review-scope). `frozen` fixes the diff on pass 1, files blocking findings in unchanged code as issues, and later passes read only the range since the previous pass. `diff-plus-blocking` is the same but fixes a blocking finding in unchanged code in this PR instead of filing it. `all` is the earlier loop: every finding is fixed on every pass. Needs `min_gstack_version` when not `frozen`. |
| `review_exclude_paths` | git glob pathspecs | `[]` | Content files (data, docs, binary, `.log`) `/ship`'s reviewers leave out; read from the base only, with code, gate, config, executable and named-by-code files kept. See [review_exclude_paths](#review-exclude-paths). Needs `min_gstack_version`. |
| `min_gstack_version` | version or `null` | `null` | The oldest gstack that may read the file; see the compatibility rule above. |
| `docs` | `{ caps: [{ path, max_bytes }] }` or `null` | `null` | Documentation files with a byte cap. `/ship` Step 14.5 runs `gstack-ship-receipt docs-caps --base ` (the caps come from `origin/`'s policy, never the branch's copy; `--policy ` is for a repository bootstrapping its policy) before the verification gate and prints one `DOCS_CAP: / ok\|over` line per cap; any `over` (an absent file counts) exits 1 and blocks the ship until the file is trimmed or the cap raised on the base branch (`DOCS_AUDIT_BLOCKED`). At most 64 caps, each path contained. See [the ship receipt](#ship-receipt). Needs `min_gstack_version`. |
| `train` | `{ gate_workflow, free_workflow, full_gate_check, record_artifact, max_size, fill_hours, screen_selection, trial_reservation_usd, land? }`, `{ gates, land, max_size, fill_hours }` or `null` | `null` | Turns on [merge trains](#run-a-merge-train), read from the base revision only. Every field is required and an unknown field is `POLICY_INVALID`: `max_size` 1 to 6, `fill_hours` above 0 and at most 24, `screen_selection` `gstack-paid` \| `none`, `trial_reservation_usd` above 0 and at most 100. A block with `gates` ([any repository](#train-gates)) takes `max_size` 1 to 5 and needs `min_gstack_version` at least `TRAIN_GATES_KEY_VERSION`. `null` keeps the per-PR queue exactly as it is. Needs `min_gstack_version`. |
| `base_red` | `{ setup, select, timeout_sec }` or `null` | `null` | How `/ship` Step 5 proves a failure is the base branch's own before pausing on it; see [base_red](#base-red). Read from the base revision only. Needs `min_gstack_version`. |
| `changelog_fragments` | `{ dir, required_checks? }` or `null` | `null` | PRs carry unversioned [CHANGELOG fragments](#changelog-fragments) in `dir` (one flat, repo-relative directory, no `..`) and the merge step ([`land`](#land)) assigns the version. `required_checks` (up to 50 distinct names) are the checks [`train land`](#stamped-trains-under-changelog_fragments-train-land) requires present and green on a stamp commit. `null` keeps `[Unreleased]` sections and per-PR stamps. Needs `min_gstack_version`. |
Containment, checked by `validate` and again at write time: paths and globs are
repo-relative with no `..` segment, under 256 characters, at most 64 per list,
and a glob's regex expansion under 512 characters; `release_tool` is under 512
characters with no newline; a matched file that resolves through a symlink to
outside the repository is refused (`POLICY_CONTAINMENT`). A mirror may start
with `..` (a sibling checkout) but has no interior `..`; mirror writes are
confined to that directory.
## gstack's own files
gstack's policy (`.gstack/ship-policy.json`, tracked beside `generated.json`
and `pregate.json` under the otherwise ignored `.gstack/`) runs the repo on
stamp-at-merge: the version is allocated once, when the PR is next in the queue,
every bump is PATCH, and `/review` reads the maintainer set from the file. It
names the one release tool `gstack-version-bump --regen-digest` used to
hard-code:
```json
{
"bump": "patch",
"queue_mode": "stamp-at-merge",
"release_tool": "bun scripts/gen-agents-digest.ts",
"release_outputs": ["agents-digest/gstack-AGENTS.md"],
"stamp_paths": ["lib/ship-policy.ts"],
"stamp_patterns": ["_KEY_VERSION = '{version}'"],
"changelog": "CHANGELOG.md",
"history": "squash-ok",
"stamp_sensitive_gates": ["tests"],
"changelog_fragments": { "dir": "changelog.d", "required_checks": ["free-tests", "windows-smoke", "Check VERSION is not stale vs queue", "quality", "check-freshness", "actionlint"] },
"min_gstack_version": "1.91.340.0"
}
```
gstack itself runs on [changelog fragments](#changelog-fragments): a PR adds
`changelog.d/.md` and never a version, and trains merge through
[`train land`](#stamped-trains-under-changelog_fragments-train-land) instead
of slot calls. `required_checks` names the pull_request checks that run on
every PR (branch protection enforces only `free-tests`); a workflow with a path
filter (Windows Setup E2E, make-pdf gate, Native ARM Setup Smoke, Dependency
Review) is left out, because a stamp commit of a PR outside its paths never
gets that check and `train land` would hold on it. `min_gstack_version` is
1.91.340.0, the first release that reads `required_checks` and counts a
stamped slot as merged only when GitHub reports it MERGED, so an older gstack
refuses the file by name (`POLICY_GSTACK_TOO_OLD`).
`.gstack/ship-policy.example.json` shows every key set the way a serial-merge
repo with a release script, docs mentions and a mirror would set them.
### Queue-side procedure under `stamp-at-merge` without fragments
gstack itself no longer follows these steps. Since #3654 (v1.91.342.0) its
policy sets [`changelog_fragments`](#changelog-fragments), so a gstack PR adds
a fragment and never a version, and trains merge through
[`train land`](#train-land). The steps
below are for a repo on `stamp-at-merge` whose policy has no
`changelog_fragments`. Who does what there, for the PR author and for the
thread running the merge queue:
1. **Open.** The branch carries a tentative stamp of the base's version plus
PATCH (`gstack-version-bump write --version --regen-digest`)
and its CHANGELOG section headed with that version directly under
`# Changelog`; `## [Unreleased]` is also accepted. Nothing is claimed and
`gstack-next-version` is not consulted; the tentative version is only the
guess that makes the common case free. CI runs on this head as usual.
2. **Register.** The queue records the PR and its intended order.
3. **Stamp when next.** With every check green on the current head and
`origin/main` at the SHA the queue assumed, the PR owner (never the queue)
runs on the branch:
`gstack-restamp --next --base main --allow-repo-commands --pr --expect-base --expect-order `.
When the base has not moved past the tentative stamp it prints
`RESTAMP: X -> X (noop)` and `FILES: (none)`, and the green head is the
merge candidate with no further CI. When the base moved, it stamps base +
PATCH, re-heads the CHANGELOG above the base's top entry, reruns the
release tool, prints `STAMP_VERSION: ` and the stamp diff, and refuses
with `QUEUE_STALE` (nothing written) when the base or order moved after
`--expect-base` was read. The owner commits and pushes the stamp commit,
lets CI run on it, and reports `READY # STAMP_VERSION=`.
4. **Merge.** The queue verifies on that exact SHA: `VERSION` equals
`STAMP_VERSION`, the CHANGELOG's top heading is `[]` and the heading
below it is the base's current version, every check is green on that SHA,
and, when a stamp commit was pushed, it changed only `VERSION`,
`package.json`, `CHANGELOG.md` and the release outputs
(`gstack-tree-receipt --gated --current ` prints the
per-file verdict). It then squash-merges with the merge API's expected-head
`sha` set to that SHA, so a late push is rejected by the server. "VERSION
above main" is replaced by "VERSION equals the restamp's STAMP_VERSION on
the SHA being merged, and that version is base + PATCH at merge time".
5. **Not next.** A PR waiting behind another may gate ahead locally with
`gstack-restamp --after `; it pushes nothing. When the
predecessor lands, the waiting PR merges `origin/main` in (never a rebase),
keeping its own CHANGELOG section above the newly merged one, and step 3
runs.
Until this policy is on `origin/main`, `gstack-restamp --next` reads the base's
`bump: ask` and falls back to a MICRO bump; pass `--bump patch` on the first
PR stamped after the flip if its base predates it.
## Run a shared merge queue
`gstack-ship-queue` strings `gstack-restamp`, `gstack-tree-receipt` and the
ship receipt into the coordinator protocol, so nobody proves a stamp diff by
hand. Lanes own their PRs and never merge; the coordinator owns the order and
every merge. Each step ends in a line the other side acts on, then
`GSTACK_RESULT: skill=ship-queue status=complete|refused run=`. Nothing
here pushes, except `ready` under `changelog_fragments` (its READY receipt goes
to `refs/notes/gstack-trains`) and [`land`](#land).
**1. Lane: report ready (unstamped).** After the gate and pregate pass on the
pushed head:
```bash
gstack-pregate run --allow-repo-commands
git push
gstack-ship-queue ready --pr 42
```
```
READY #42 STAMP_VERSION=unstamped
pr: https://github.com///pull/42
head:
version: unstamped
base: origin/main@
ci: green at (passed=31 failed=0 pending=0 skipped=2)
PREGATE: pass tree= head= checks=6
gate: command="" full_e2e=no gate-ahead=none
SHIP_RECEIPT: pr=42 head= …
READY_RECEIPT: /ship-queue/ready//42-.json (notes pushed to origin refs/notes/gstack-trains)
```
Under `changelog_fragments`, a `READY_RECEIPT:` line follows, written only when
every check above passed; `land` refuses a pin without one.
CI's Version Gate (`scripts/compare-pr-version.ts`) accepts that head as
unstamped when `VERSION` and `package.json`'s version match its merge base, the
CHANGELOG change is confined to `[Unreleased]`, and any changed release output
is byte for byte what `release_tool` generates at the head's merge base
`VERSION` (the head's own, so a later stamp on main changes nothing), so a
generator change ships with its regenerated output and the slot's restamp
regenerates it again. A release output that differs from that (a hand edit, one generated at a
bumped version, or one the tool never writes), a `VERSION` bump or a
`package.json` version change classifies the head as stamped, and it must then
carry base + bump. The tool runs as head code in a throwaway worktree with a
bare environment and no tokens; one that fails at the head is an error, never
a pass.
**A PR that conflicts with the base on CHANGELOG.md alone.** GitHub shows every
READY PR as conflicting each time main stamps; the train merges the CHANGELOG
by section, so the lane never merges main for it. The conflict does mean GitHub
has no merge ref for the PR, so no `pull_request` CI runs on the head and
`ready` prints `ci: none at ` with a `ci-hint:` line. The lane then
runs `ready … --dispatch-ci free-tests.yml,windows-smoke.yml [--timeout
]` (under a train policy): it dispatches each named workflow on the branch
(`gh workflow run --ref `, skipped when a run at this head already
concluded, and skipped with a note when the base has the workflow file but the
branch predates it, since GitHub reads the file from the branch), waits for the
runs (`CI_DISPATCH: --ref run `), clears
every requires-remote pregate lane whose workflow is one of them with the run
URL (`PREGATE_CLEARED: `; a lane of another workflow stays
uncleared and is named in the refusal), and then reads the head as usual,
which passes because the dispatched check runs attach to the commit; the
`ci-hint:` line then says those non-pull_request runs count. A red run clears
no lane and refuses (`QUEUE_NOT_READY`); a run still going at `--timeout`
(default 2700 s) is `QUEUE_TRAIN_PENDING`, and rerunning the command keeps
waiting; a fork head is never dispatched here (`QUEUE_TRAIN_UNTRUSTED`). By hand the same four steps are `gh workflow
run --ref `, wait, `gstack-pregate clear --remote --run-url
`, rerun `ready`. The `ci-hint:` line is advisory text for the lane;
nothing parses it. `ready --screen` on such a PR has no merge ref to screen,
so it builds the scratch merge described below (`SCREEN_MERGE:`, pushed as
`screen/-`) and screens that; only a real conflict with the base
(or a remote whose refs cannot be listed) is `QUEUE_SCREEN_STALE_REF`. A CHANGELOG-only conflict is screenable, and a
lane whose skill's own paid cases ran focused may still READY with `--no-screen`.
`ci:` counts the latest run per check (one app's check name), as GitHub's
rollup does: a check that was re-run or superseded on the same commit (a
cancelled run beside a later pass of the same name from a second workflow run)
is judged by its newest run (latest `completed_at`, then id; a run still in
progress is newest); two apps reporting the same name stay separate; a run with
no name or no readable `completed_at` counts on its own; commit-status contexts
are counted one each.
It refuses (`QUEUE_NOT_READY`) when the working tree has tracked edits, HEAD
is not the PR's pushed head, CI at that exact head is not green, or the pregate
result is not a current pass, and (`QUEUE_RECEIPT_MISSING`,
`QUEUE_RECEIPT_STALE`) when the PR body carries no /ship receipt for that
head: /ship runs before every READY ([details](#ship-before-ready)). It also
refuses `QUEUE_RECEIPT_DEPTH_RAISED` when the receipt's review depth is below
what the head needs, `QUEUE_RECEIPT_TARGETED_MISSING` when a targeted class
the head needs has no done targeted pass in the receipt, and
`QUEUE_RECEIPT_EXCEPTION_INVALID` when an owner exception the receipt records
does not stand ([the ship receipt](#ship-receipt)).
Send the block as printed.
**2. Lane behind another PR: gate ahead.** Gate the tree the queue will have
once the predecessor lands, while it is still open:
```bash
git fetch origin pull/41/head
gstack-ship-queue gate-ahead --after 41 --pr 42 --predecessor-ref FETCH_HEAD --allow-repo-commands
```
prints `GATE_AHEAD: pr=42 attempt= gated= tree= merge_tree=
predecessor=#41@ version=`, the attempt's own `worktree:`, its
`record:` file, `GATE_COMMAND:` (the policy's `gate_command`) and the exact
`next:` line. Run the gate there, in the background or in CI, then bind it:
```bash
(cd && ) > gate.log 2>&1
gstack-ship-queue gate-ahead --record --pr 42 --attempt --exit $? --log gate.log
```
`GATE_RECORD: pr=42 attempt= exit=0 gated_tree= log_sha256=
current=yes`. The record keeps the log and removes only that attempt's
worktree, and refuses (`GATE_AHEAD_ATTEMPT_INVALID`) when the worktree no
longer holds the gated commit. If the predecessor moves, prepare a new
attempt; attempts never share a worktree, the newest is current, and
recording an older one later prints `current=no (superseded by )`.
`gate-ahead --list --pr 42` shows them all. Send the record file with READY.
**3. Coordinator: call the slot.** Name the version, the base sha and the
order: "#42 next at 1.2.7.0, base ``, order 41,42".
**4. Lane: stamp.** A gate-ahead lane first merges `origin/main` (with the
predecessor in it) into its branch, never a rebase. Then:
```bash
gstack-ship-queue stamp --pr 42 --version 1.2.7.0 --expect-gated \
--expect-base --expect-order 41,42 --allow-repo-commands
```
`` is the head the gate ran on: the pushed head the lane gated, or the
record's `gated=` after a gate-ahead. The preflight refuses before anything is
written unless the tree to stamp equals the gated one modulo stamps, the base
is `` and, under `full_e2e`, a passing gate result is bound to the gated
head. Then it runs the restamp, commits, reruns the tree receipt and prints
```
QUEUE_STAMP: 42 1.2.7.0 head= gated= tree=same-modulo-stamps receipt=
```
Each stage is journaled in `.gstack/tmp/ship-queue/stamp-42.json`; the
identical command resumes at the first unfinished stage, or returns the stored
`QUEUE_STAMP` when it already finished. A refusal prints
`QUEUE_STAMP_REFUSED: 42 head= committed=no|yes stage=`
and the coded line (`QUEUE_STALE` carries a `recompute:` command). Push the
stamp commit, wait for CI on it, run `ready` again
(`READY #42 STAMP_VERSION=1.2.7.0`) and send it with the
`QUEUE_STAMP` line.
**5. Coordinator: verify, then merge.** From the coordinator's own clone:
```bash
gstack-ship-queue verify --pr 42 --gated --receipt --expect-head [--gate-exit 0 --gate-log ]
gstack-ship-queue verify --pr 42 --record --receipt --expect-head
```
It fetches `refs/pull/42/head` and reruns the tree receipt there, so
`QUEUE_STAMP` is never a pasted claim: `VERIFY: 42 1.2.7.0 head=
gated= tree=same-modulo-stamps receipt=`. Under `full_e2e`,
`--gated` also needs the lane's passing gate log from its run on that head
(`--gate-exit 0 --gate-log `), else `QUEUE_GATE_MISSING`. With a gate-ahead
record, whose gated commit never left the lane's machine, it first recomputes
the merge with `git merge-tree --write-tree `
and compares it with the recorded merge tree, rebuilds the gated tree from
the recorded stamp patch and compares it with the recorded `gated_tree`,
proves that patch is stamps only, checks the predecessor did not move, and
prints `merge_tree:`, `gated_tree:` and `gate:` lines. The last line is
`MERGE_CONDITION: sha=`: merge with that expected head. Any
`QUEUE_VERIFY_MISMATCH` means do not merge.
**Rolling out `gate_command` and `full_e2e`.** Commit them (with
`min_gstack_version` at their introducing version, which `init --explain`
prints) only after every lane's gstack is at least that version, the crew's
install floor (`GSTACK_MIN_REVISION`). A lane on an older gstack refuses the
policy by name rather than reading it differently.
## Run a merge train
A merge train gates several READY PRs with one full gate instead of one gate
each. The coordinator merges the pinned, unstamped PR heads in slot order onto
main in a scratch worktree, records each prefix tree (T0 is main, Tk is the
train head), runs the free tests and then one full gate on the train head, and
merges the PRs one at a time, each only while main's tree equals the prefix
the gate covered modulo stamps. A red train is bisected on its red cases only.
The surface is off unless the base revision's `.gstack/ship-policy.json`
declares a `train` block. Without one, `train …` and `verify --train` refuse
with `QUEUE_TRAIN_OFF`, and `ready --pr ` prints exactly the per-PR READY
block above (`--screen`, `--no-screen` and `--broad` are ignored with a
warning on stderr). A repository without gstack's eval workflow declares
[`gates`](#train-gates) instead. A block for gstack itself would read:
```json
"train": {
"gate_workflow": "evals.yml", "free_workflow": "free-tests.yml",
"full_gate_check": "gstack/full-gate", "record_artifact": "gstack-train-record",
"max_size": 5, "fill_hours": 1, "screen_selection": "gstack-paid",
"trial_reservation_usd": 1.5
},
"min_gstack_version": ""
```
gstack's own policy carries this block. It went live after the workflow side
(the train-nonce record, the `cases` and `prefix_shas` inputs, the dispatched
screen) merged and one dry-run train passed: two unstamped scratch docs PRs on
a scratch base whose `route` step read the policy from that base instead of
the default branch. `gate_workflow` is dispatched with
`evals_all`, `expected_sha` and `nonce` (bisects add `cases` and
`prefix_shas`, screens add `screen_pr`, `screen_merge_sha` and
`screen_policy`). That run publishes `full_gate_check` as a check run on the
train head and the train record as the `record_artifact` artifact only when
its census is complete and green; gstack reads both and never writes a
check. `screen_selection: gstack-paid` computes gstack's paid case selection
for the screen recommendation; `none` leaves the choice to the lane.
`trial_reservation_usd` is what the bisect reserves for a trial without cost
data.
Every command prints its lines, the coded error line on stderr, then
`NEXT: ` and `GSTACK_RESULT: skill=ship-queue status=… run=`.
Records live under `$GSTACK_STATE_ROOT/ship-queue/` (`trains//train.json`
with one attempt record per dispatch, `ready/` receipts and `freeze/`) and
are mirrored to origin as git notes on `refs/notes/gstack-trains` before any
remote effect, so a lost machine loses nothing: `train status` on a clean
state root rebuilds them. `GSTACK_SHIP_QUEUE_NOW` pins the clock for tests.
### Stamped trains under `changelog_fragments`: `train land`
When the base policy also declares [`changelog_fragments`](#changelog-fragments),
`train build` builds every prefix twice. T_i is the plain chain above. S_i is
what slot i publishes: S_0 is the base and S_i = stamp(merge(S_(i-1), pin_i)),
stamped by `gstack-restamp` with S_(i-1) as its base, so slot i folds only its
own fragments (a stacked pin's parent fragments are already folded) and the
versions run in slot order, each with its slot's effective bump. Before
anything is pushed, each S_i must be merge(S_(i-1), pin_i) modulo stamps,
keep every release entry of S_(i-1) byte for byte, and equal T_i outside
release paths (version files, CHANGELOG, fragments, `release_outputs`,
`stamp_paths`, `stamp_files`); otherwise `QUEUE_TRAIN_STAMP_MISMATCH`. Pass
`--allow-repo-commands` when the policy has a `restamp_command` or
`release_tool`. The record keeps both chains (`prefixes` and `stamped`), the
train head and branch are S_k, and the one full gate runs on S_k, the exact
tree the last merge publishes.
**One land lock.** `train land` takes the same repository land lock as
[`land`](#land), `refs/notes/gstack-land`, as `train slot (#)`,
before its first read of main, so a standalone land and a train slot never
both publish onto one main: whichever starts second refuses with
`QUEUE_LAND_LOCKED`, naming the holder. Once a slot has merged, the train keeps
the lock in phase `between`, which only the same train's next slot takes over,
until its last landable slot lands; a refusal before a later slot's push (a
failed `gh` or `git fetch` read, say) keeps it too, so no standalone land slips
in while the train waits on a rerun. Before any slot merged, a refusal before
the push releases it. After a push, a failure keeps the lock in its phase and a
rerun of the same slot takes it back. A stop that freezes the queue
leaves the lock in phase `freeze`, which `--break-lock` never breaks: only the
repair train the freeze admits takes it over, and it is released when that
train clears the freeze. `--break-lock` prints the holder (`LOCK_HOLDER:`),
keeps land's staleness, mid-push and drift rules, refuses a `between` lock
younger than 10 minutes, and refuses a live train run's lock unless
`--break-train-lock` is also given. `train watch --land` goes through `train
land`, so it stops with `NEXT:` on `QUEUE_LAND_LOCKED` like any refusal. A train
`train land` refuses (one built without stamped prefixes or a gate) still merges
through `verify --train` and `gh pr merge`, which neither take nor check this
lock.
Slots merge with `train land --train --pr `, never a slot call:
1. The record must be the gated head's own chain. Walking first parents from
S_k gives T0 and S_1..S_k with the recorded commits and trees, and each
S_i's second parent is slot i's pin. A record rebuilt from notes that
differs anywhere refuses (`QUEUE_PROOF_MISSING`), and every later step uses
only that verified chain. main's tree must equal S_(i-1)'s exactly (slot 1:
the train base), and the PR's head its pin, with its /ship receipt. main
equal to an earlier prefix refuses and names the slot to land first; main
equal to a later prefix confirms the slot (a rerun). A base that cannot be
fetched refuses (`QUEUE_PROOF_MISSING`), a fork head refuses
(`QUEUE_TRAIN_UNTRUSTED`), and a PR whose base is no longer the train's
base refuses (`QUEUE_NOT_READY`), here and again before the merge.
2. It commits S_i's tree with parents [pin, main], adds the
`evals-no-debounce` label (a tool-made head gets no later push, so the
10-minute quiet window protects nothing), and pushes under
`--force-with-lease=:`; a foreign push is never overwritten
(`QUEUE_TRAIN_MOVED`). The stamp commit is deterministic and journaled
(`land-.json` beside the train record) before the push, so a rerun
adopts its own stamp head instead of reading it as a move. A push that
fails while the branch still reads the pin is `QUEUE_NOT_READY`; rerun.
3. It waits for the PR's checks on that commit (`--no-wait` prints the
command). Required checks run live at the slot until GSTA-156's exact-tree
reuse is on main. Every name in `changelog_fragments.required_checks` must
be present and green on the stamp commit (a check run of that name, all of
its ` (…)` matrix runs, or a status context); a missing one holds the
merge (`QUEUE_NOT_READY`) and the fix names it. Without the list, any green
set counts, so set it to the checks branch protection does not enforce.
4. It refuses if main moved during the checks, merges with
`--match-head-commit`, and confirms main's tree is S_i exactly. Any other
main is the freeze path above (`QUEUE_TRAIN_TREE_MISMATCH`, backstop).
A slot counts as merged only when GitHub reports its PR MERGED. Main at
S_i with the PR still open refuses (`QUEUE_PROOF_MISSING`), and the
train's merged count, its freeze decision and the freeze record read
GitHub's state for every slot, never the record's `merged` list alone.
`verify --train` refuses a stamped train and names `train land`. The backstop
covers main only when its exact tree is some S_i of a green record (plain
T_i records still cover modulo stamps). A dropped slot changes every later
version, so the train is rebuilt, as today.
### Trains for any repository: `gates`
A repository without gstack's eval workflow declares its own gates instead of
`gate_workflow` and the other gstack-evals keys. Such a train gates every
prefix, not just its tip, so a red train names its culprit without a bisect:
```json
"train": {
"gates": [
{ "kind": "workflows", "workflows": ["ci.yml"], "required_jobs": ["ci.yml/test", "ci.yml/lint"] },
{ "kind": "command", "command": "bun test", "timeout_sec": 1800, "spend_cap_usd": 3 }
],
"land": { "merge_method": "squash", "required_checks": ["test", "lint"] },
"max_size": 5, "fill_hours": 3
},
"min_gstack_version": "1.92.0.0"
```
A block with `gates` holds exactly these four keys; a gstack-evals key beside
them is refused, and a block without `gates` is the gstack-evals kind and
behaves exactly as above. `min_gstack_version` must be at least
`TRAIN_GATES_KEY_VERSION` (lib/ship-policy.ts), so an older gstack refuses the
policy by name. `max_size` is at most 5, because each gate runs on k+1
prefixes.
- **`workflows` gates.** Each workflow is dispatched on a pushed
`train-prefix//t` ref with a `nonce` input; the train records the
nonce before it sends the dispatch and adopts only the workflow_dispatch run
whose run-name carries `[eval-bg ]`, never a run found by commit.
A prefix is GREEN only when that run tested the prefix head exactly, every
`required_jobs` entry (`/`)
concluded success and the run concluded success. With `record` set (an
artifact name), the run must also upload a JUnit XML or `{ "tests": [...] }`
JSON file reporting at least one executed test, and failed test ids become
the red cases. A red run gets one `gh run rerun --failed`: green on the
rerun is GREEN with a `FLAKY:` line, red again is real, and both attempts'
jobs stay in the record. `post_merge_checks` names checks that must succeed
on main after each land before the next slot lands (`verify --post-merge
--checks `; `train land` checks the previous slot's merge and
refuses the next slot until they are green, and `train watch --land` runs it
between slots). A workflow whose concurrency
group is not keyed by `github.ref` (or the nonce) gets one prefix at a
time.
- **`command` gates.** Each prefix is checked out clean and detached and runs
`command` on a Ubicloud VM through scripts/ubicloud/ubi-runner.sh
(standard-16, `UBI_OWNER` required). The VM's `git write-tree` must equal
the prefix tree, and exit 0 is GREEN. Only names in `pass_env` reach the VM
(`UBICLOUD_*` never). Each run reserves its timeout plus 10 minutes at the
list price, and each command gate's `spend_cap_usd` covers every run of that
gate in the train, reruns included (3 dollars covers six prefixes and one rerun at 1800 s; `train init`
prints that default and `train doctor` flags a cap below a full train). The
next run past the cap refuses (`QUEUE_TRAIN_SPEND_CAP`); raising the cap is a
policy commit that moves main, so it is followed by a new train.
**The verdict.** T0 red is red on main (`QUEUE_TRAIN_RED_ON_MAIN`, fix
forward). Otherwise the first non-green prefix decides: incomplete stops the
train, red with a later prefix green is unattributed (repair or a new order,
never an unchanged rebuild), and red with every later prefix red names its
slot as the culprit (`QUEUE_TRAIN_RED`); the slots before it may land.
`train bisect` and `train rebuild` refuse such trains, since every prefix
already has its verdict. A green T0 run of the same gate at main's exact
commit in another train is reused instead of run again.
**Base-red PRs.** A lane whose /ship paused on a red base (its receipt carries
`base_red`) READYs with `ready --pr --no-screen --base-red --fixer `:
the condition must have been classified on today's base and not have expired.
The fixer READYs with `--fixes-base-red `. The fixer rides at slot 1
(moved there unless `--order` says otherwise, which then refuses), T0 may be
red on exactly the fixer's cases and nothing else, and every conditional PR
behind it is gated on its own prefix with nothing excluded. A conditional PR
without its fixer at slot 1, or on a gstack-evals train, refuses
(`QUEUE_TRAIN_BASE_RED`, `QUEUE_BASE_RED_NO_TRAIN`).
**Landing.** `train land` publishes T_i, or S_i when the train stamps
(`changelog_fragments`, or `gates` with `queue_mode: stamp-at-merge`), with
main's tree equal to the prefix before it. It records the mode, and the update
commit when there is one, before its first remote effect:
1. `direct`: `gh pr merge -- --match-head-commit `, used
only when branch protection is readable and not strict, the pin merges onto
main as the gated tree exactly, and the pin's own checks (every
`required_checks` name) are green.
2. `update`: the gated tree committed on [pin, main], pushed under the pin's
lease, its checks waited for, then merged at that commit. It is forced for
a stamped train, strict or unreadable protection, a conditional PR, a merge
that resolved release files, and a pin whose checks are red. `rebase`
always refuses (`QUEUE_TRAIN_LAND_METHOD`): it would replay commits, and
the replayed tree is not the gated one.
Required checks are `changelog_fragments.required_checks`, else
`train.land.required_checks`. Fork heads refuse. Either way main's tree must
be the prefix afterwards, or the queue freezes as above; the backstop
dispatches each gate workflow on main by nonce. `train watch --train
--land` runs build, verdict and every landable slot's land in order, and stops
at the first refusal.
**Setting one up.** `train init [--command ""]` reads branch
protection, the base's workflows and the open PRs, and prints a block mapping
each required check to its workflow job, the `nonce` input, run-name and
concurrency snippet each gate workflow needs, and the land mode slots will
use. It recommends `changelog_fragments` when open PRs bump the version, and
refuses (`QUEUE_TRAIN_INIT_REFUSED`) only when they do and the policy neither
declares fragments nor stamps at merge. After the block lands on the base,
`train doctor` re-checks it: a missing workflow, nonce input or run-name, a
required job no workflow defines, a concurrency group not keyed by ref, a push
trigger that matches `train-prefix/` refs, a workflow never green on the base,
a command gate cap below what a full train reserves, a merge method GitHub has
turned off, and `rebase` as the merge method (`QUEUE_TRAIN_DOCTOR_FINDINGS`).
Neither command writes anything.
### Wave PRs: several PRs in one slot
When the queue is long, PRs that touch the same part of the code travel
together as one **wave PR**: one integrator branch, one /ship, one READY, one
train slot and one version (Garry's rule R46, 2026-10-31: "consolidating
functional areas into larger wave PRs ... so we can land them faster with fewer
test passes"). Each slot otherwise costs its own stamp commit and a full round
of required checks before it merges, one slot after another, so a wave of
three saves two of those rounds and two versions.
```bash
gstack-ship-queue wave plan # read-only (merge-tree scratch objects aside): QUEUE, WAVE_ADVISED, WAVE_GROUP lines
gstack-ship-queue wave build --area ship --prs 41@,42@,43@
gstack-ship-queue wave build --wave --continue # after resolving a conflict in the printed worktree
gstack-ship-queue wave build --wave --drop 42 # rebuild without #42 as a new wave
gstack-ship-queue wave build --wave --release # end the wave unlanded; its PRs ride alone
gstack-ship-queue wave status [--wave ]
gstack-ship-queue wave settle --wave [--dry-run] # after the wave PR merged
```
- **When.** `wave plan` counts the live READY PRs (open, head equal to their
READY receipt, CI green at that head; a READY head whose CI is red, still
running or absent prints a `NOT_GREEN` line and stays out of the plan; a
READY head that does not merge onto main with the train's merge rules prints
`NOT_MERGEABLE` with the conflicting files and stays out too, while a
conflict only in the CHANGELOG, which the train merges, does not count). A
READY PR whose CI, head, diff or merge onto main cannot be read fails the
whole plan with `QUEUE_PROOF_MISSING`. Over the
trigger (6) it prints `WAVE_ADVISED` and suggested groups of 3 to 6: PRs
whose changes to a common code file conflict (`git merge-tree` cannot merge
the two heads, or the check itself fails) always share a group, so that
conflict is resolved once. A PR that renames a file and one that edits the
old path are linked when the edit conflicts at the new path. A group of
linked PRs is never split across waves: one larger than the wave maximum
prints `WAVE_OVERSIZE` with no build command (land part of it as single
slots, then plan again). Hub
files never link PRs: anything under `docs/`, Markdown (generated skill
renders included; their `.tmpl` sources still link) and every `.json` or
`.jsonl` file (ledgers and registries). Most PRs append to them, and
linking through them once chained a 55-PR queue into a single area
(GST-770). Each group takes its largest PR's functional
area (a skill directory, the first word of a `lib/`, `bin/` or `test/`
module name, or `docs`), smaller groups ride solo, and the integrator is
the PR with the most changed lines. `train build` handed more than 6 pins
prints the same `WAVE_ADVISED` line. It is advice: urgent and repair PRs
still ride alone. While the queue stays long, plan new work as area waves
from the start, so each piece skips its own /ship.
- **Build.** `wave build` checks every pin is an open, same-repository PR at
that head (`QUEUE_WAVE_MOVED`, `QUEUE_TRAIN_UNTRUSTED`), not already in a
live wave (`QUEUE_WAVE_DUPLICATE`), 3 to 6 of them (`QUEUE_WAVE_SIZE`;
`--min`/`--max` change it). It merges the heads in order onto main with the
train's merge rules, each as a merge commit whose second parent is the pin,
so no constituent is rebased or squashed. The wave record (kind `wave`) is
written locally and to `refs/notes/gstack-trains` before `wave/` is
pushed, and each constituent gets one "frozen in wave" comment. Then the
integrator opens the wave PR (the printed `gh pr create` and body file, or
the host's PR tool), runs /ship on it and READYs it.
- **Conflicts.** A merge that conflicts outside the CHANGELOG and the stamp
files stops with `QUEUE_WAVE_CONFLICT` and leaves the merge in progress in a
worktree under the state root. The integrator resolves and commits it there,
then `--continue` checks that commit merges exactly that pin onto the
recorded commit and finishes the chain.
- **Frozen heads.** Once in, a constituent's head does not move. Fixes go on
the wave branch as integrator commits, which the chain check allows. A
constituent that has to change goes back to its owner: `--drop ` builds
a new wave on a new branch (never a force-push; the integrator closes the
old wave PR first, and the old record is marked superseded), refused with
`QUEUE_WAVE_DEPENDENT` when another pin contains the dropped one. A drop
that would leave fewer PRs than the minimum releases the wave instead, and
`--release` does the same for a wave PR closed without merging: the record
is marked released and every constituent can be READY'd on its own again.
Both wait until the wave PR is closed, since a train that already admitted
it would still land it. A constituent must target the base branch; a
stacked PR is refused, because its pin carries its parent's commits.
- **READY and the train.** `ready --pr ` and `train build` read the
wave record for a `wave/` head branch (rebuilt from the notes ref on a
fresh machine; `QUEUE_WAVE_UNKNOWN` when there is none). They walk the
branch's first parents back to the recorded base: every pin must be merged
unchanged, in order, and any other merge is refused. Every constituent must
still be open at its pin. READY gains `WAVE:` and `CONSTITUENTS:` lines.
`ready` and `train build` refuse a constituent of a live wave on its own
(`QUEUE_WAVE_DUPLICATE`), so it cannot land twice. A wave head may carry
train land's stamp commits after its last constituent, so a stamped wave
is re-READY'd like any stamped PR. From there the wave PR is an ordinary slot: `train
land`, `verify`, the stamp and the backstop treat it like any PR, and under
`changelog_fragments` its one stamp folds every constituent's fragment into
one version section.
- **After the merge.** `wave settle` finds the wave PR merged into the base
branch, checks that its merge commit is on that branch and carries its head
(the head's tree, or the head merged onto the merge's parent when the train
merged the PR directly), and walks its chain (train land's stamp or update
commits on top are allowed). A
constituent still at its pin gets a "Landed in wave #W as vX (sha)" comment
naming any integrator commits or hand-resolved merges that touched its
files, and is closed. One whose head moved after the wave froze stays open
with a comment saying its newer work did not land. Comments carry a marker, so a
rerun posts nothing twice; `--dry-run` shows the plan.
**0. Lane: /ship before READY.** Every READY follows a /ship run on the branch
at the head being READY'd, including a re-READY after a push (Garry,
2026-10-25: "we should really be using the gstack ship skill before merging
PRs"). /ship runs the tests, both reviews, the gate-integrity list and the docs
audit, and ends by writing its receipt into the PR body
(`gstack-ship-receipt write --pr … --body `, posted with
`gstack-post pr-body`). Under `queue_mode: stamp-at-merge` it bumps no version
and never merges or merges main into the branch; the train still stamps at the
slot. The queue enforces this at every step from the PR body alone:
- `ready` refuses with `QUEUE_RECEIPT_MISSING` when the body has no valid
receipt and with `QUEUE_RECEIPT_STALE` when the receipt's `head` is not the
head being READY'd (or its `pr` is another PR); the informational
`SHIP_RECEIPT:` line still prints. It refuses `QUEUE_RECEIPT_DEPTH_RAISED`
(with a `REVIEW_DEPTH:` line) when the receipt records a review depth below
what the head needs, `QUEUE_RECEIPT_TARGETED_MISSING` (with a
`REVIEW_TARGETED:` line) when a targeted class the head needs has no done
targeted pass in the receipt, and `QUEUE_RECEIPT_EXCEPTION_INVALID` when an
owner exception it records does not stand ([the ship receipt](#ship-receipt)).
There is no flag around it.
- `train build` refuses a pin whose PR has no current receipt with the same
codes, naming the PR, before anything is written, pushed or dispatched,
`--dry-run` included.
- `verify --train` refuses the slot with `QUEUE_RECEIPT_MISSING` or
`QUEUE_RECEIPT_STALE` before printing `MERGE_CONDITION`; it does not
recompute the review depth.
**The stamp keeps the pin's receipt.** The slot's stamp commit changes only
stamp files (and, from slot 2 on, merges main into the pin), so no /ship runs
on it: the receipt written for the pinned head stays the binding one. `verify
--train` accepts a receipt whose `head` is either the slot's pinned head or
the current head, and the re-READY after the stamp push (`ready` at the stamp
commit) accepts the receipt for the pin when HEAD is the pin plus stamp files,
or (from slot 2 on) the pin merged with main plus stamp files on top of the
merge; any other head is `QUEUE_RECEIPT_STALE`, including a merge of main with
no stamp above it (a lane's own push under rule 14) and a merge whose pin side
carries an edit made after /ship.
This is a process gate, not an authenticity check: the receipt is a block in
the PR body, which the PR author writes, so the codes prove that /ship was run
and recorded for this head, not that nobody typed the block by hand. Fork heads
are already refused (`QUEUE_TRAIN_UNTRUSTED`). The `ship-run.json` that
`gstack-ship-receipt result` writes under the lane's state root is a
normalized copy of that same block bound to the head it was run at, not an
authenticity proof either.
**1. Lane: READY with a screen choice.** Under a train policy READY needs an
explicit choice:
```bash
gstack-ship-queue ready --pr 42 # prints SCREEN: lines and the recommendation, refuses
gstack-ship-queue ready --pr 42 --no-screen # READY receipt; the PR rides unscreened
gstack-ship-queue ready --pr 42 --screen [--broad] # dispatches one paid screen on the PR's merge SHA (a scratch merge when GitHub's merge ref lags the head; QUEUE_SCREEN_STALE_REF only on a real conflict)
```
Without a choice it prints the `SCREEN:` selection (with
`screen_selection: gstack-paid`, the skill's own cases, the selector's mode and
`recommend --screen` or `--no-screen`: `--screen` only when the selector's
mode is `full-fallback`, an unknown dependency or a shared runtime input that
restores every case, or when it cannot select at all; every other PR rides
unscreened, and the train gate's full census runs its cases on the tree that
merges) and refuses with `READY_SCREEN_CHOICE_REQUIRED`. The price line says
the dollar figure is pending the cost replay; until it lands, `SCREEN:` shows
case counts and the recommendation only. A lane that already ran focused
local cases says so in the READY block and may pass `--no-screen`. `--screen`
dispatches `gate_workflow` with `screen_pr`, `screen_merge_sha` (the
`refs/pull//merge` SHA) and `screen_policy` (`narrow`, or `broad` with
`--broad`, the full PR selection). A narrow screen, and `eval:bg:pr`, skip a
changed file whose derived references reach every gate case and nothing
narrower selects, along with its derived judges: the plan's reason names it,
and the train gate's full census runs those cases. A `pull_request` run, the
merge gate when the base has no `train` block, keeps the broad selection. Full-gate files, shared runtime and paid-runner
inputs, and files the derivation cannot place still run the full gate on a
narrow screen. The nonce key carries the purpose and
the policy, so a repeat follows the same run and a broad screen never adopts
a narrow one. The READY block gains `screen: …`, and the receipt is written
locally and to the notes ref. `--dry-run` prints the dispatch without sending
it. Before dispatching, `ready` fetches the merge ref and requires its second
parent to be the PR head and its first parent the current base: a missing,
lagging or unfetchable merge ref (GitHub stops recomputing it while the PR
conflicts with the base, CHANGELOG-only conflicts included) is replaced by a
scratch merge of the head onto the base, pushed as `screen/-`
(the `SCREEN_MERGE:` line says which); a screen dispatched on a stale ref would
plan no cases and report success. Only a real conflict with the base (or a
remote whose refs cannot be listed) is `QUEUE_SCREEN_STALE_REF`. Under either
choice `ready` first reads what `gstack-pr-wait`'s base-overlap event reports,
from git: when the base moved since the head forked and its new commits touch
files the PR changes, `BASE_OVERLAP: moved to and touches files
in this PR: ; the train's merge onto is clean` (or `conflicts
beyond the CHANGELOG and the stamp files ()`), the train's own merge
rules run as a dry run in object storage; a git read that fails prints
`conflicts unknown` and refuses nothing. A conflict under `--no-screen` is
`QUEUE_TRAIN_CONFLICT` at READY, since the train would stop at this PR: the
lane reports it to the controller and, on its word (crew template rule 14),
merges the base into the pin, resolves, pushes and runs /ship before READY,
instead of learning it when the train is built. A finished screen that planned no cases (no
`paid-plan` artifact, which a superseded run never uploads, or a plan manifest
with nothing planned) is never followed as a screen: `ready --screen`
dispatches a fresh one and `train build` prints `SCREEN: # run planned
no cases …; counted as not-run` and seats the slot unscreened. A plan the
queue cannot read keeps the slot screened, with `SCREEN: # ;
counted as screened, unverified`.
**2. Coordinator: build.** Name the pins exactly as READY reported them:
```bash
gstack-ship-queue train build --prs 41@,42@,43@ --dry-run
gstack-ship-queue train build --prs 41@,42@,43@
```
It prints `ORDER:` (the slot order to announce: screened PRs first in READY
order, then unscreened PRs fill the remaining seats, and `HELD:` lists the PRs
over `max_size`; `--order ,,...` naming every
pinned PR once replaces that rule, for a PR that must merge before another,
and the train's later `NEXT:` rebuilds keep it), one `PREFIX: T
tree=` per prefix and one `SLOT:` per PR. `--dry-run` adds the
`ESTIMATE:` line and writes, pushes and dispatches nothing. Otherwise it
writes `train.json`, pushes `train/`, dispatches `free_workflow` there
and waits (`--free-timeout `, default 2700) for a green
`workflow_dispatch` run on that exact SHA, then dispatches the full gate
through eval-bg's nonce. A train fills at `max_size` READY PRs or when its
oldest READY has waited `fill_hours`, whichever comes first; until then
`QUEUE_TRAIN_WAITING` names the time, and `--urgent` dispatches a one-PR
train at once. The scratch merges run before the fill rule: every `train
build` prints `# BASE_OVERLAP: moved to and touches files in
this PR: ` for a seated pin whose files the base moved onto since it
forked (the chain's merge onto the pin's own prefix is the verdict), refuses
`QUEUE_TRAIN_CONFLICT` for a pin that does not merge, and otherwise prints
`MERGE_CHECK: T1..T merge clean onto @`, so a
`QUEUE_TRAIN_WAITING` result is also a clean dry-run merge of the pool (the
crew template's sibling trial merge, done by the tool). gstack's own block fills at 5 PRs or one hour (it was 5 PRs or
three hours until 2026-10-26, which left READY PRs waiting most of a day, then 3 PRs or one hour until 2026-10-31). The same pins on the same base return the live train
instead of a second one; `train build --train ` resumes a train from
its record. Refusals, all before any spend: `QUEUE_TRAIN_UNTRUSTED` (a fork
head), `QUEUE_TRAIN_MOVED` (a head moved from its pin),
`QUEUE_RECEIPT_MISSING` and `QUEUE_RECEIPT_STALE` (the PR body has no /ship
receipt for the pin; see [/ship before READY](#ship-before-ready)),
`QUEUE_RECEIPT_DEPTH_RAISED`, `QUEUE_RECEIPT_TARGETED_MISSING` and
`QUEUE_RECEIPT_EXCEPTION_INVALID` (the receipt's review depth is below what
the pin needs, a targeted class the pin needs has no done targeted pass, or an
owner exception it records does not stand; see [the ship receipt](#ship-receipt)),
`QUEUE_NOT_READY` (no READY receipt at that head), `QUEUE_TRAIN_CONFLICT` (the printed
`QUEUE_TRAIN_CONFLICT: train stopped at # (conflicts: ) after
merging #,#; nothing dispatched, no branch pushed; fix: …` line names
the build without that PR) and `QUEUE_TRAIN_FREE_RED` (free tests red,
missing, cancelled or on another SHA). In a merge, the version file takes the
prefix's side, a manifest or npm lockfile conflict merges three-way with the
prefix's version (so a pin's dependency edit survives), and the CHANGELOG
merges by section, since the stamp at merge rewrites them; a PR whose CHANGELOG drops or renames a released heading (or
carries a released entry's text in a section of its own) is a CHANGELOG
conflict, never merged with the damage, except the stacked child whose parent
was released since their merge base, which `mergeChangelogs` carries (see *A
stacked child whose parent merged earlier*, below). READY receipts and train records are
keyed on origin's GitHub owner/repo, so lanes whose origin uses another URL
form (git.capy.ai, ssh, a token URL) share the coordinator's queue.
**3. Coordinator: status and verdict.**
```bash
gstack-ship-queue train status [--train ]
gstack-ship-queue train verdict --train
```
`status` never spends. When a dispatch was attempted but its run never showed
up (`QUEUE_TRAIN_DISPATCH_UNKNOWN`), it lists the workflow's dispatch runs and
either adopts the run carrying the nonce or marks the dispatch `absent`; only
an absent dispatch is sent again, by `train build --train `, under the
same nonce. `verdict` reads the run, its record and the `full_gate_check`
check: `GREEN` only for a complete census bound to the train nonce, head and
tree; `QUEUE_TRAIN_PENDING` while it runs; `QUEUE_TRAIN_INCOMPLETE` for INFRA
or INCOMPLETE panels, unbound, partial or cancelled shards, a slice killed
before upload, or no record (never green); `QUEUE_TRAIN_RED` with the red
cases.
**4. A red train: bisect, then at most one rebuild.**
```bash
gstack-ship-queue train bisect --train --spend-cap 5 --dry-run
gstack-ship-queue train bisect --train --spend-cap 5
gstack-ship-queue train rebuild --train
```
The bisect dispatches the red cases (`cases`) on every prefix T0..Tk
(`prefix_shas`; for a [stamped train](#train-land), T0 and S_1..S_k, the
chain its gate ran), one trial each, after checking every prefix is an ancestor
of `train/` (`QUEUE_TRAIN_PREFIX_INVALID`). It reserves every
case×prefix trial (unknown costs at `trial_reservation_usd`) and launches only
the prefixes that fit the cap (`QUEUE_TRAIN_BISECT_CAPPED`; completed prefixes
are kept and a rerun with a higher cap runs only the rest). Rerun it to read
the results. The pattern decides: `ATTRIBUTED #` only when monotone
(green before, red from that PR on), and `NEXT:` builds the train without it;
red on every prefix with T0 included is `QUEUE_TRAIN_RED_ON_MAIN` (fix forward
on main first); anything else is `QUEUE_TRAIN_UNATTRIBUTED`. An all-green
bisect earns exactly one unchanged rebuild (`train rebuild`, reporting both
runs); a non-monotone pattern earns none and goes to a repair lane. Every
other rebuild is `QUEUE_TRAIN_REBUILD_REFUSED`, and a second red stays red.
An INFRA/INCOMPLETE-only census earns the single re-dispatch
EVAL_POLICY.infraRedispatch allows: `train rebuild --train --infra`.
The rebuild-once rule lives in the train module beside that constant until
EVAL_POLICY records it. Bisect results never turn a train green.
**Stamping a slot.** When the slot is called, the lane stamps it at main plus PATCH. Slot 1's pin applies cleanly to main. From slot 2 on, main already carries the earlier slots' stamps, so the pin conflicts on CHANGELOG.md and GitHub can't merge it. The lane merges main into the pin first, then stamps:
```bash
git fetch origin main
git merge --no-ff --no-commit origin/main # expect a CHANGELOG.md conflict only
bun --no-env-file --config=/dev/null -e "import { mergeChangelogsWithNotes } from './lib/changelog-check'; import { spawnSync } from 'node:child_process'; import { writeFileSync } from 'node:fs'; const s = (n) => spawnSync('git', ['show', ':' + n + ':CHANGELOG.md'], { encoding: 'utf8', maxBuffer: 1 << 26, timeout: 30000 }).stdout; const m = mergeChangelogsWithNotes(s(2), s(3), s(1)); for (const n of m.notes) console.error('CHANGELOG: carried as a Gate integrity correction: ' + n); if (!m.text) { console.error('CHANGELOG needs a hand merge'); process.exit(1); } writeFileSync('CHANGELOG.md', m.text);"
git add CHANGELOG.md && git commit --no-edit
gstack-restamp --next --base main --pr 42 --expect-base --expect-order --allow-repo-commands # stages what it writes (STAGED:)
git commit -m "v stamp" && git push
```
A conflict in any file other than CHANGELOG.md means the merge isn't the train's prefix. Stop and report it. When the helper says the CHANGELOG needs a hand merge, take main's CHANGELOG unchanged and put your own entry above it. `verify` accepts the resulting head because it contains main and its tree equals T modulo stamps, with its CHANGELOG judged as main's plus lines your pin added, however the conflict was resolved; the `head:` line says "T with main merged in".
**The stamp commit is the base merge plus the stamp files, never a fifth.** The
restamp writes the version files, the CHANGELOG, the `release_outputs` and the
`stamp_paths` lines the branch itself added (gstack: `CHANGELOG.md`, `VERSION`,
`package.json`, `agents-digest/gstack-AGENTS.md`, and a `*_KEY_VERSION` the PR
introduced). A line the base added since the pin's fork point is the base's,
whether the base merge is committed or still staged (`stage stamp_paths` says
"N line(s) the base added since the fork point are the base's"; train 24,
2026-10-27, moved main's `DOCS_CAPS_KEY_VERSION` in both slots before this
rule). Restamp stages what it writes and prints `STAGED:`; commit what restamp
staged. A file the slot call does not name is not a stamp: `git restore --staged
--worktree` it and report it. A stamp file holding unstaged edits of your own refuses before
anything is written (`RESTAMP_UNSTAGED_EDITS`), and a staging failure leaves the
files written for the noop rerun to stage (`RESTAMP_NOT_STAGED`), so a bare
commit after a `--no-commit` base merge can no longer drop the stamp (#3527). The tree receipt refuses such a move after the fact
only while the moved version is older than the base's at the build, so the
restamp, not the receipt, is what keeps the base's key in place.
**Size caps compose; nothing re-pins at the slot.** Ship's union-size cap is a
ledger, `test/size-ledger/ship/`: `base.json` plus one `.json` per
branch that changed ship's union bytes (`{ "bytes": , "why": "…" }`),
and the parity suite requires ship's union bytes to equal the base plus every
entry, exactly. A lane records its own change once, before /ship, with `bun
--no-env-file --config=/dev/null scripts/size-ledger.ts record ship --why
""`. The entry is a property of the branch's own diff, so it
stays right across base merges, and each prefix carries the entries of the
slots in it, so T passes with no re-pin and the stamp commit stays the four
files (a cap re-pinned at the slot would be a fifth file on a tree the gate
never ran). A clean merge adds byte deltas exactly; overlapping hunks are a
train conflict anyway, and two branches making an identical edit (git keeps
one copy, both entries count) fail closed with the same record command as the
fix. A branch that grows ship beyond its entry, or shrinks it without a
negative one, fails on its own tree and on every prefix that includes it.
`scripts/size-ledger.ts check ship` prints the ledger; `fold ship
--measured-at ` moves the entries into `base.json` in a tidy-up PR,
never in a stamp commit. Other skills keep their `maxSizeRatio` caps in
`test/helpers/carve-guards.ts`.
**A stacked child whose parent merged earlier.** When the pin was stacked on a
PR that squash-merged in an earlier slot or train, the base merge conflicts
beyond the CHANGELOG wherever the parent's squash and the pin touched the same
lines. Take the pin's bytes for those files: `verify` checks the tree against
T, the train's own merge of that pin, so the parent's content is already in
the prefix. The CHANGELOG is `mergeChangelogs`' job and needs no hand merge for
this shape: the pin's `[Unreleased]` still holds the parent's entry (as forked,
maybe edited) while main carries it under a released heading the merge base
lacks, so the parent's lines leave the child's section and each edit the child
made to them becomes a `- Correction to the entry below: ""
should read "".` bullet under the child's own `### Gate integrity` (the
one-liner prints each carried correction). The released entry stays main's byte
for byte, and `verify` accepts those bullets as the pin's own lines because
the tool writes them. A pin that does not carry base can still be refused here
(`ours's Unreleased section holds released text`): that is the
GSTA-67 shape, a released heading dropped by hand, and stays a hand merge.
**A generator change on an unstamped head.** A release output such as
`agents-digest/gstack-AGENTS.md` is stamped at the slot, and an unstamped PR
changes it the ordinary way, which is to edit the generator, regenerate at the
branch's own `VERSION` (its merge base's), and commit both. CI's Version Gate
regenerates the output at that VERSION and reads a byte-identical result as
unstamped ([what /ship reads](#ship), since 1.91.237.0); the freshness test
compares against the checkout's VERSION, so it passes before and after the
slot's restamp regenerates the file at the new version. Never revert a
regenerated output to the base's bytes to get past the gate (#3434 did, on a
gate older than that rule, and shipped a stale digest line until its merge).
**5. Coordinator: verify, merge, verify again.** For each slot in order:
```bash
gstack-ship-queue verify --pr 41 --train # MERGE_CONDITION: sha=
# merge #41 with that expected head; gstack-restamp stamps it at merge
gstack-ship-queue verify --pr 41 --train # VERIFY_TRAIN: #41 merged; main's tree is T1 …
```
Before a merge, main's tree must equal the prefix before the PR modulo stamps
and the PR must be at its pin (stamps aside) or contain main and be T
modulo stamps (its pin with main merged in; its CHANGELOG must be main's with
only lines from the pin's own CHANGELOG diff, or lines `mergeChangelogs`
writes for that pin against main, above it); after it, main's tree must equal
the prefix after it modulo stamps, or differ from it in the CHANGELOG alone
while being its own first parent's CHANGELOG (the main the pre-merge verify
saw, itself the prefix before modulo stamps) plus the PR's own entry under the
same rule. One CHANGELOG rule applies before and after, so a head the pre-merge
verify accepted confirms after its squash merge with the same CHANGELOG, and a
stacked child whose one `[Unreleased]` became two released sections is not a
mismatch (train 24, 2026-10-27, froze on exactly that). Before
`MERGE_CONDITION`, the head must also carry its own stamp: its `VERSION` above
main's, and its CHANGELOG headed with that version directly above main's top
release, with no `[Unreleased]` section main does not have; otherwise
`QUEUE_TRAIN_UNSTAMPED` names the rule and both versions (train 33 merged #3527
under #3526's version without it). verify prints `STAMP_VERSION:` from the head
itself. A post-merge confirmation needs GitHub to report the PR MERGED: an open
PR whose stamp-only diff already matches main (a release-only PR, #3530) takes
the pre-merge path, and any other open or unreadable PR refuses
`QUEUE_PROOF_MISSING` without freezing, as does a verify that could not read
the PR's current head (`--no-fetch` or a failed fetch) instead of judging the
unstamped pin. Anything else is
`QUEUE_TRAIN_TREE_MISMATCH` (or `QUEUE_TRAIN_MOVED`). When nothing of the train
has merged, that is a plain refusal, and a new train is built on current main. When
earlier slots already merged, main holds a prefix no gate covered on its own,
so verify freezes the queue. The freeze goes to local state and the notes ref
first, then the full gate is dispatched on main's SHA at once (the backstop),
and the `QUEUE_TRAIN_FROZEN:` line lists the merged PRs, main's tree and the
backstop run. A slot GitHub reports MERGED counts as merged in that record,
verified tree or not, so the rebuild after the repair never seats it again
(the reason says "merged on GitHub without a verified tree"); a first slot
whose squash GitHub reports MERGED but whose tree fails the post-merge check
freezes the same way, since main already holds the unverified squash. The
record comes from the one GitHub read that decided to freeze, never a second
one: a slot that read MERGED stays merged even when GitHub stops answering
before the record is written, and a slot unreadable beside a merged one is
left out of `unmerged`, recorded under `unreadable` and printed on an
`unreadable:` line; the `NEXT:` after the repair names it (`gh pr view
--json state,headRefOid (unreadable at the freeze: #@); seat each
that is OPEN in a train`).
When every slot of the train already merged on GitHub and main's SHA is a
prefix another train's verify confirmed (read after the notes ref, so a
machine that never ran that train still sees it), the mismatch says nothing
about this train: verify refuses with `QUEUE_TRAIN_STALE` and `NEXT:
gstack-ship-queue train prune --stale --train `, freezing nothing and
dispatching no backstop (train 24's record froze the queue that way on a main
train 28 had verified, #3519). Only a verify that fetched main takes this
exit: under `--no-fetch` the local main may be older than GitHub's, so the
mismatch freezes as before. When
whether a slot merged on GitHub cannot be read, verify refuses
(`QUEUE_PROOF_MISSING`, "whether # merged on GitHub could not be read")
and asks to be rerun once `gh pr view` answers; an unreadable state is never
"nothing merged". While frozen, ordinary trains and other trains' merges are refused
(`QUEUE_TRAIN_FROZEN`). Admit one repair PR at its exact head:
```bash
gstack-ship-queue train build --repair --prs 44@
```
It runs the normal free tests, full gate and verification on main plus the
repair. When verify confirms the repair merged, it prints `FREEZE: cleared`
and `NEXT:` builds a train of the PRs still unmerged.
**5b. Coordinator: after the merge.** `gstack-ship-queue verify --post-merge
[--timeout ] [--checks ]` (the merged commit's full 40-character SHA) watches
every check run attached to it (the push workflows: free tests, the Windows
subset, the main backstop) and every commit status until none is still
running, prints one `CHECK: ` per check and
`POST_MERGE: green|red|pending|none (passed= failed= pending= skipped=)`.
Green needs at least one completed check and no failure; `neutral` and
`skipped` (the backstop superseding itself) count as skipped; a pending
commit status keeps the commit pending and a failed one makes it red. A
failure is `QUEUE_POST_MERGE_RED` naming the checks; a timeout (default 2700 s) is
`QUEUE_TRAIN_PENDING` with the same command as `NEXT:`. With `--checks` (a
workflows gate's `post_merge_checks`), each named check must also run and
succeed: one that did not succeed is `QUEUE_POST_MERGE_RED`, and one that
never ran is `QUEUE_TRAIN_PENDING` at the timeout. It keeps no process
state, so rerunning it after a machine restart continues the watch.
**6. Clean up.** `gstack-ship-queue train prune [--train ] [--dry-run]`
deletes the `train/` branches of merged, superseded and dropped trains
(`WOULD-DELETE:`, `DELETED:`, `GONE:`). `train prune --stale` also settles a
`merging` or `green` train whose every PR is MERGED or CLOSED on GitHub (its
PRs landed through another train or by hand): it logs each PR's state on a
`STALE:` line, marks the record `stale` and deletes its branch; a train with an
OPEN PR is kept (`KEEP: … still needs the train`). The same run deletes every
`screen/-*` scratch branch whose PR is MERGED or CLOSED and keeps one whose
PR is still OPEN. `train forget --train | --receipt
@ [--dry-run]` removes one record (a settled train with its attempts,
or one READY receipt) from local state and from `refs/notes/gstack-trains`
with an exact lease on the notes ref, printing `FORGET:` then `FORGOTTEN:`; a
train that is still building, running or merging (`built`, `pushed`,
`free-pending`, `free-green`, `dispatch-unknown`, `dispatch-absent`,
`gate-pending`, `merging`) is refused; a rejected lease removes nothing and
restores the local notes ref.
**Following a train without a daemon.** `gstack-ship-queue train watch --train
[--timeout ]` is the one resumable follow command: it reads the
record, runs the step its state asks for (`train build --train` to push, run
the free tests and dispatch the gate; `train status` to settle an unconfirmed
dispatch), waits for the pending run by polling gh, reads the verdict and
prints the last step's `NEXT:`. It never dispatches anything the record would
not dispatch on its own, so rerunning it after an interruption continues from
the record (`WATCH:` lines say what it did; a timeout, default 7200 s, is
`QUEUE_TRAIN_PENDING` with the same command as `NEXT:`; `--free-timeout `,
default 2700, bounds the wait for the free tests).
**Screens are read from the run.** `ready --screen` and `verify --pr
--screen` read a dispatched screen's verdict from the run's own artifacts
(`gate-binding-a`, `report-verdict-a`), never from a lane's
prose, and record it in the READY receipt (`screen.verdict`): `SCREEN_VERDICT:
# run GREEN|RED|NOT-RUN|INCOMPLETE [red=…] [flaky=…]` plus one
`FLAKY:` line per rule-16 flaky case. `PENDING` is a run still going,
`UNKNOWN` one the queue cannot read, and `INCOMPLETE` a finished run that bound
no verdict (its report job did not finish): `ready --screen` dispatches afresh
instead of following it, `verify --screen` refuses it (`QUEUE_PROOF_MISSING`)
and `train build` seats the pin unscreened, as for `NOT-RUN`. A RED screen refuses `ready` under
either choice, is never re-dispatched at the same merge SHA and is not seated
by `train build`, whose `NEXT:` names the build without that PR
(`QUEUE_SCREEN_RED`); a run that concluded success with no slice (its route
job found the head superseded or the plan selected nothing) is `NOT-RUN`:
`verify --screen` refuses with `QUEUE_SCREEN_NOT_RUN`, `ready --screen`
dispatches afresh instead of following it, and `train build` seats the pin
unscreened with a `SCREEN: … counted as not-run` line. When GitHub's
`refs/pull//merge` lags the PR head or is missing, `ready --screen` plans
the screen on a scratch merge of the head onto the base, pushed as
`screen/-` (`SCREEN_MERGE:` names it; `prune --stale` deletes it
once the PR merged or closed); only a real conflict with the base (or a remote
whose refs cannot be listed) is `QUEUE_SCREEN_STALE_REF`.
**Stacked tests before the gate.** `train build --prs … --dry-run --stack-tests
[--stack-tests-command ""]` runs each PR's new test files (added relative
to the base, with a name ending in `.test`, `_test`, `.spec` or `_spec` and a
JS or TS extension, bun's own rule, so a helper or fixture under `test/` is not
one) on the integrated prefix that includes it, T, in a scratch worktree, and prints
`STACK_TESTS: # T new test file(s) pass|FAIL ()` or `no new
test files`. A failure is `QUEUE_TRAIN_STACK_RED` (nothing written, pushed or
dispatched): the tests pass on the PR head alone only because nothing before
it in the train had changed what they read, so fix them against that prefix or
reorder with `--order`. The default command is `bun --no-env-file
--config=/dev/null test` with the files appended.
**Escape hatches.** Remove the `train` block on the base branch to return
every lane to the per-PR queue at once. `--urgent` ships one PR alone,
`--no-screen` skips the paid screen, `--broad` widens it to the full PR selection,
and `train build --prs ` drops a PR at any step.
## What `/ship` reads from it
`/ship` Step 3 runs `show-effective --base ` and follows its `ship:` lines,
which carry the workflow consequences the skill prose would otherwise spell out:
| Line | Effect |
|---|---|
| `ship: history merge-only: …` | never rebase, squash or force-push; `check-history --plan ` refuses a rewrite plan (`HISTORY_POLICY_VIOLATION`, exit 3) |
| `ship: queue_mode stamp-at-merge: …` | Step 12 items 1–4 are replaced by `gstack-restamp --next --base --pr --expect-base ` when the PR is next, `--after ` when it is not; the restamp's own `next:` line says what follows: under `stamp-at-merge`, `commit and push only the stamp diff above (FILES), with no edit of your own` and the merge's expected head (the stamp commit holds restamp's output alone); under the per-PR queue, NEW_VERSION and the CHANGELOG entry text |
| `ship: shared merge queue: …` | in a coordinator-run queue, `gstack-ship-queue ready`, `gate-ahead` and `stamp` replace the restamp calls; see [Run a shared merge queue](#run-a-shared-merge-queue) |
| `ship: gate-ahead: …` | with `.gstack/tmp/gate-ahead.json` present, Step 16 reuses only the lanes `gstack-tree-receipt … --json > .gstack/tmp/tree-receipt.json` marks reusable; stamp-sensitive and not-eligible lanes run live; `--force-gate ` is the coordinator's call |
| `ship: bump patch: …` | Step 12 never asks for MINOR or MAJOR; PATCH or MICRO by size |
| `ship: bump never: …` | Step 12 prints `VERSION: kept (policy: bump never; no write)`, asks nothing and refuses `--bump`; a branch that moved VERSION stops until it is put back |
| `ship: bump ask: …` | Step 12 asks for MINOR and MAJOR; on FRESH, `--keep-current` (or `--version `) keeps VERSION, and in a session no person answers any version Step 12 would write waits for `--answer` |
| `ship: preregistration: …` | `gstack-evidence ancestor --from-policy --base ` must print only `ANCESTOR: ok` before push |
| `ship: release_tool: …` | the command runs only inside `gstack-restamp` (staging worktree, outputs confined to `release_outputs`) |
| `ship: review_scope …` | Step 9.4 item 0 runs `gstack-review-scope classify` on each pass's findings; see [review_scope](#review-scope) |
| `ship: changelog_fragments : …` | Step 12 writes no version; Step 13 writes the entry as `/.md` (entry text only, `bump:` front matter only when the change warrants more than the policy bump), never an `[Unreleased]` section, linted with `gstack-prose-lint --gate`; `gstack-ship-queue land` assigns the version; see [changelog fragments](#changelog-fragments) |
The PR body's first section is the ship receipt (`gstack-ship-receipt write … --body`),
written by the pr-body section of `/ship` after the body is composed.
## The ship receipt
The ```` ```gstack-ship-receipt ```` block is the handoff record: identifiers
first (`pr`, `head`, `version`, `base`), then the evidence (`gate`, `ci`,
`head_tree`, `product_patch_id`, `gstack_version`, `review_depth`, `pregate`,
`issues`, `approval`, `docs`, `review_loop`, `review_loop_exception`, `carry`),
validated
against the `ship-receipt` schema (`gstack-artifact schema ship-receipt`) and
read back by `gstack-ship-receipt read`, `result`, `carry` and the queue.
**`version`.** The version source at HEAD: the path `.gstack/version-path`
pins, else `VERSION`, else `package.json#version`. A repository with none of
those, none of the policy's `stamp_files` and no CHANGELOG (the policy's
`changelog`, or `CHANGELOG.md`) at HEAD has no release version to record, and
the receipt says so with `"version": "none"`: `/ship` Step 12 printed
`Version bump: skipped (no version source: )` and Step 13
`CHANGELOG: skipped (none at )`, the PR title carries no version prefix,
and `read`, `result`, `gstack-ship-queue ready`, `train build` and `verify
--train` accept the receipt like any other. A repository that has a CHANGELOG
but no version source records no `version` at all (the audit still says what
it looked for); pass `--version` to name one.
**`docs`.** Step 14.5's outcome, from `gstack-ship-receipt write --docs
`: `{ status: updated | current | blocked, scope, exception? }`,
where `scope` names the docs roots the audit covered and `exception` is
`{ accepted_by, date, via, reason }`, the owner's acceptance of a risk the
audit could not clear. A `blocked` status with no exception is
[`DOCS_AUDIT_BLOCKED`](troubleshooting.md#docs-audit-blocked) and nothing is
written: only the owner accepts a documentation risk, so an unattended run
stops there. With `--body`, the body's `## Documentation` section must carry
the exception word for word (`Documentation exception: accepted by
via [: `), or the write refuses with the same code. Doc
caps: a repository declares `docs.caps: [{ path, max_bytes }]` in its policy,
and Step 14.5 runs `gstack-ship-receipt docs-caps --base ` before the
verification gate, reading the caps from `origin/`'s policy so the branch
cannot raise its own (`--policy ` only while bootstrapping a policy; an
unreadable base policy is `POLICY_SOURCE_UNAVAILABLE`); one
`DOCS_CAP: / ok|over` line per cap
(`absent/ over` for a path the tree lacks), exit 1 on any `over`, whose
repair is trimming the file, never raising the cap in the same PR.
**`review_depth` and `gstack_version`.** `write` records the gstack that
wrote the receipt and the review depth at the checked-out head
([review-policy.md#review-depth](review-policy.md#review-depth)):
`{ depth, class, runtime_lines, rules, base, registry, gate_scan: { tagged, head }, reasons }`,
computed from git objects for the range against `origin/`
(`gstack-diff-scope --head ` for the scope signals), with the gate
scan run on that head. `--head` must be the checked-out HEAD
(`RECEIPT_INVALID` otherwise), so the record and `head_tree` describe the
tree the review read; tracked edits in the checkout make the scan unavailable,
which records `full`. `gstack-ship-queue ready` and `train build` recompute
the depth the head needs from the same range and scope and print `REVIEW_DEPTH: receipt= required=
runtime_lines= class= reasons=<…> inputs=base@,registry@,gate_scan@]