# 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` | `ask` | `patch` skips the MINOR/MAJOR prompt in `/ship` Step 12 and `gstack-restamp --next` bumps PATCH; `ask` keeps the prompt. | | `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 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 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`. | | `min_gstack_version` | version or `null` | `null` | The oldest gstack that may read the file; see the compatibility rule above. | | `train` | `{ gate_workflow, free_workflow, full_gate_check, record_artifact, max_size, fill_hours, screen_selection, trial_reservation_usd }` 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. `null` keeps the per-PR queue exactly as it is. 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": [], "changelog": "CHANGELOG.md", "history": "squash-ok", "stamp_sensitive_gates": ["tests"] } ``` `.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 for gstack Who does what under `stamp-at-merge`, 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. **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= … ``` 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. 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 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": 3, "screen_selection": "gstack-paid", "trial_reservation_usd": 1.5 }, "min_gstack_version": "" ``` gstack's own policy does not carry it yet: trains go live for gstack only after the workflow side (the train-nonce record, the `cases` and `prefix_shas` inputs, the dispatched screen) has merged and one dry-run train of two already-merged heads has passed. `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. **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 ``` Without a choice it prints the `SCREEN:` selection (with `screen_selection: gstack-paid`, the skill's own cases and `recommend --screen` or `--no-screen`: `--screen` when the screen selects a skill's own cases) 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`, today's full PR selection). 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. **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; when `max_size` or more unscreened PRs wait they form their own train and `HELD:` lists them), 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 under `max_size` waits until its oldest READY has waited `fill_hours` (`QUEUE_TRAIN_WAITING`); `--urgent` dispatches a one-PR train at once. 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_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, version files take the prefix's side and the CHANGELOG merges by section, since the stamp at merge rewrites both. **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`), 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. **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 still be at its pin (stamps aside); after it, main's tree must equal the prefix after it. Anything else is `QUEUE_TRAIN_TREE_MISMATCH` (or `QUEUE_TRAIN_MOVED`). When nothing of the train has merged, that is a plain refusal: build a new train 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. 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. **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:`). **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 today's 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 (NEW_VERSION, CHANGELOG entry text only, the merge's expected head) | | `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: 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`) | 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. ## `queue_mode: stamp-at-merge` Under `stamp-at-merge` nothing is claimed when a PR opens. The stamp happens when the PR is next, from the base branch's version at that moment, through the repo's own release tool, across every stamp location at once: ```bash gstack-restamp --next --base main --allow-repo-commands --pr 42 \ --expect-base --expect-order 40,41,42 ``` prints `ship, next in queue at `, `STAMP_VERSION: `, one line per stage, the diff limited to stamp files, `MERGE_CONDITION: sha=` (pass it to the merge API's expected-head `sha` so a check-to-merge race is rejected by the server) and the `GSTACK_RESULT:` line. `claims: ignored (stamp-at-merge)` says the legacy claim reader was not consulted. When the base or the order moved, the result is `QUEUE_STALE` (exit 3) with the exact `recompute` command; nothing is written. Stages, each journaled in `.gstack/tmp/restamp-journal.json`: 0. **restamp_command** (only when the policy sets it): the repo's own restamp, see [restamp_command](#restamp-command). 1. **version**: `gstack-version-bump write --version ` in the staging worktree (VERSION, the manifest, npm lockfiles); skipped when `restamp_command` owns the version. 2. **release_tool**: the policy command, under the contract below. 3. **changelog**: the PR's `[Unreleased]` or old-version section moves to the top under ``, above its predecessor's entry. Duplicate headings (`CHANGELOG_DUPLICATE_HEADING`) and a top entry that is not `` (`CHANGELOG_TOP_MISMATCH`) are errors. 4. **stamp_paths**: exact `(was X)`, `since: X` and `vX` rewrites, plus every `stamp_patterns` template, within `stamp_paths` and `stamp_files`. `X` is the branch's own tentative stamp (its VERSION when it moved past base), else `--was `, else the version an unstamped branch would have taken next (`base + bump`); mentions of a version the base never released (not a CHANGELOG heading at base, not a tag) are rewritten too, so a queue reorder that moves the base under a branch still finds its `since:` fields. Only the repo's own version width counts, so a 3-digit dependency pin in a 4-digit repo is never a candidate. 5. **mirror**: tier 2; declared and validated, reported as not run. 6. **apply**: every changed file is written to the branch only after all stages validated, each against the fingerprinted preimage; a file edited meanwhile is `RESTAMP_CONFLICT` and the applied writes roll back (rollback refuses to overwrite a file whose bytes differ from the recorded postimage). Idempotent: a second run with the same version is `noop`. `--dry-run` prints the full diff and writes nothing (not even the journal). `--version ` stamps exactly X; `--bump` overrides the level; `--queue-mode` overrides the policy. ### Gate-ahead: `--after ` A reorder is cheap only when the gate can run before the predecessor merges. `gstack-restamp --after --predecessor-ref ` merges HEAD and the predecessor's head in a throwaway worktree (`.gstack/tmp/gate-ahead-worktree`), resolving only version-file and CHANGELOG-ordering conflicts, stamps N+1 relative to the predecessor's version there, commits, and records `.gstack/tmp/gate-ahead.json` (`predecessor_head`, `branch_head`, `gated_head`, `gated_tree`, `version`, `evidence_branch_key`). The PR branch's history is never touched, so squash-history PRs stay clean. The gate runs in that worktree; `gstack-evidence bundle` there exports the evidence. ### The release-tool contract The release tool runs in an isolated staging worktree seeded from the working tree's content fingerprint (`gstack-wtree`), with the repo's ignored `node_modules` linked in. Its outputs are enumerated: the version files, lockfiles, the CHANGELOG, `stamp_paths` and `release_outputs`. A write outside that set refuses the whole restamp before anything reaches the branch (`RESTAMP_OUTPUT_OUTSIDE_ALLOWED`); a non-zero exit is `RESTAMP_RELEASE_TOOL_FAILED`. Preimages and postimages of every write are recorded in the journal. Pinning the command string does not freeze its implementation (`bun run release:restamp` still runs the PR's package script), so the receipt names the policy revision, the command, its working directory and the trust scope (`policy: base origin/main@` or `head (unreviewed)`). ### `restamp_command`: the repo owns the stamp A repo whose own tool knows its stamp locations sets `restamp_command`. `gstack-restamp` runs it as the first stage inside its staging worktree, a clean checkout of HEAD, with the allocated version as the last argument and in `GSTACK_RESTAMP_VERSION`. The caller's HEAD, index and worktree must be clean first (`RESTAMP_CONFLICT`, nothing written); the generic path keeps its working-tree fingerprint seed. Anything the command stages or commits in the staging worktree is folded back into plain edits; then output confinement applies as for the release tool (`RESTAMP_OUTPUT_OUTSIDE_ALLOWED`), the version file must hold the target version (`RESTAMP_VERSION_SOURCE`), and a non-zero exit is `RESTAMP_RELEASE_TOOL_FAILED`. In every failure nothing reaches the branch. gstack's version writer is skipped; the CHANGELOG and `stamp_paths` stages still run (a no-op when the tool already did the work). Read from the base branch and run only with `--allow-repo-commands`, like `release_tool`. A tool that fetches, merges the base branch or needs a version of its own choosing cannot run here; give it a mode that takes the version and writes only stamps. ## The tree receipt ```bash gstack-tree-receipt --from .gstack/tmp/gate-ahead.json --predecessor-pr 41 --bundle evidence-bundle.json gstack-tree-receipt --gated --current HEAD --json ``` Compares two **committed heads** (a working-tree fingerprint can name objects unreachable once the gate VM is gone) and prints the gated and current heads, both tree ids, the version pair, the predecessor the gate assumed, the diff limited to stamp files, one line per file with its rule and verdict, one line per gate, then two verdict lines: ``` TREE: same-modulo-stamps | changed () gate-reuse: eligible (reusable: tests; rerun: digest) | not-eligible () — fix: (GATE_REUSE_NOT_ELIGIBLE) ``` Hunks are classified per file type: the version file (old → new only), the manifest (`version` key only), npm and bun lockfiles (root `version` keys only, where `gstack-version-bump` writes; a dependency entry is `changed`), the CHANGELOG (a heading move or re-heading, compared as an order-independent multiset of entries), `stamp_paths` and `release_outputs` (every removed/added pair must be an exact `(was X)` / `since: X` / `vX` rewrite with X the gated version and the replacement the current one, so a dependency pin that happens to equal the old version is `changed`; `stamp_patterns` templates count as stamp forms too). With `stamp_files` set, a file outside it is `changed (stamp_files: not in the repo's stamp_files allowlist)` whatever its diff. Any other byte is a change. Equivalence is decided per gate. A lane in `stamp_sensitive_gates` always `rerun`s; a verified lane is `reusable` only when the tree is same-modulo-stamps and the execution identity in the bundle equals the current one: Bun and Node versions, OS and arch, every lockfile's identity (root version normalized away), the `.github/workflows` tree id, runtime pin files and `package.json#engines`, and the runner image when either side knows it. Without a bundle the identity is unknown and the receipt is `not-eligible`. When the recorded predecessor head is no longer the predecessor PR's head (checked through `--predecessor-ref` or `gh pr view --predecessor-pr`) the receipt prints `PREDECESSOR MOVED -> — fix: gstack-restamp --after again (RESTAMP_PREDECESSOR_MOVED)` and refuses `eligible`. `/ship` Step 16 consults the receipt; when it says `eligible`, a repeated full gate needs `--force-gate `, and the coordinator makes the call. ### The evidence bundle `gstack-evidence` is machine-local by design, so the portable form is a bundle: ```bash gstack-evidence bundle --out evidence-bundle.json --label tests --label e2e --require tests,e2e \ --selection tests='bun run test' --run-url tests=https://github.com/.../actions/runs/1 ``` A bundle binds to a committed head (`repo.commit`, `repo.tree`) and carries the comparison material (version source, CHANGELOG path, `stamp_paths`, `release_outputs`), the execution identity above, the required-lane manifest, and per lane the command, `cmd_sha256`, exit, timestamp, executed selection, run URL, log hash and whether it is `verified` (produced by the ledger on a clean committed tree at this head) or not (dirty tree, other commit, or an `--import`ed assertion). A fresh coordinator machine reads it with `gstack-tree-receipt --bundle` after the gate VM is gone; the receipt refuses reuse when the bundle was produced on another tree or lacks the identity (`EVIDENCE_IDENTITY_UNKNOWN`). ## The ship receipt The handoff record is a fenced block, identifiers first, validated against `gstack-artifact schema ship-receipt`: ```` ```gstack-ship-receipt { "schema_version": 1, "pr": 42, "head": "", "version": "1.2.7.0", "base": "main", "gated_tree": "", "predecessor": "", "tree": "same-modulo-stamps", "gate_reuse": "eligible (reusable: tests; rerun: digest)", "gate": { "tests": { "pass": 10, "fail": 0, "skip": 1 } }, "ci": { "checks": 3, "failed": 0 }, "spend_usd": 0.42, "session_kind": "unattended", "artifacts_consumed": "1/1", "run": "", "thread": "", "policy": "base origin/main@" } ``` ```` `gstack-ship-receipt write --pr --receipt --gate '' --ci '' --body ` makes it the first section of the PR body (`/ship` Step 18) and prints `SHIP_RECEIPT: pr=… head=… version=…` for the first line of the completion message. `/land-and-deploy` reads it with `gstack-ship-receipt read --pr --json` (`RECEIPT_MISSING`, `RECEIPT_INVALID`). ### The adoption census (P7) ```bash gstack-ship-receipt census --repo garrytan/gstack --since v1.91.70.0 --cohort threads.txt ``` reads merged PRs since the tag through `gh`, parses each receipt, deduplicates by `run` and `thread`, and prints `CENSUS: … unattended= consumed= adoption=/ durable=yes`. The cohort file lists one thread id per line (threads that never invoke gstack count in the denominator); without it the denominator is the threads the receipts name. ## History policy and preregistration (C7) Under `history: merge-only`, `/ship` refuses a squash or rebase plan and merges the base branch instead: `gstack-ship-policy check-history --plan squash` prints `HISTORY: refused (…) (HISTORY_POLICY_VIOLATION)` with exit 3, and without `--plan` it prints `HISTORY: ok` or `HISTORY: rewritten … (HISTORY_POLICY_VIOLATION)` (exit 1) when the pushed branch is no longer an ancestor of HEAD. `gstack-evidence ancestor --from-policy --base main` prints one `ANCESTOR: ok|missing ` line per `preregistration_shas` entry (`PREREGISTRATION_NOT_ANCESTOR`, exit 1), and the ship receipt records the result in `preregistration`. ## Contributor mode `/review` runs `gstack-contributor-mode detect` at Step 1 (decision D10). The PR author is outside the maintainer set when `maintainers` in `.gstack/ship-policy.json` on the base branch is non-empty and does not list the login (`source=policy`); with an empty list, when `gh api repos///collaborators//permission` reports less than push permission (`source=collaborators`, an egress receipt `contributor-mode` precedes the call); when neither is available the mode is off and `CONTRIBUTOR_SOURCE_UNAVAILABLE` names what was missing. `--contributor` forces it on (`source=flag`). `CODEOWNERS` is file ownership, not maintenance authority, and is never read. The printed line is `CONTRIBUTOR_MODE: on|off author= source=flag|policy|collaborators|off`, followed when on by the evidence-first instructions: a table per change (evidence value, risk surfaces, `accept` / `supersede` / `decline`), where `accept` keeps a direct path for a correct outside implementation judged by the same bar as a maintainer change, `supersede` rewrites from our own plan only after a plain-language approval, and the credit lines (`Contributed by @handle`, `Co-Authored-By:`, `Supersedes #N`) appear only when a diagnosis or a test carried over. `gstack-contributor-mode instructions` prints the same text without detecting. ## Banned terms `.gstack/banned-terms.json` lists terms the repository forbids outside named locations: `terms` (a string, or `{ "term", "allowed_paths", "reason" }`) and a top-level `allowed_paths` that every term inherits. Globs follow the same containment rules as the ship policy (repo-relative, no `..`, under 256 characters). `gstack-banned-terms check --base ` scans the lines the branch adds since its merge base (or whole files with `--files `), reading the term list from the committed copy on the base by default (`--policy-from head` reads the working tree). Each finding is one line, `BANNED_TERM: "" : allowed: — `, and the last line is `BANNED_TERMS: ok | found= | none` (`none`: the repository keeps no list). Exit 0 for ok or none, 1 when found, 3 when the file is invalid (`BANNED_TERMS_INVALID`, also from `validate`). `/review` turns each finding into a Step 4 finding and `/document-release` into a documentation finding; `gstack-banned-terms init` writes a commented example. ## Approval classes `.gstack/approval-classes.json` names the changes that need the owner's approval before they ship: upgrade-time data rewrites, trust or visibility changes, existing-user default changes, data repairs, anything the owner wants cited. Each entry in `classes` has a `name` (lowercase, dashes) and at least one matcher: `paths` (globs of changed files), `patterns` (literal text in an added line, case-sensitive) or `migrations: true` (the files the ship policy's `migrations` key describes). `allowed_paths` exempts files, such as tests; `description` is for people. Globs follow the ship policy's containment rules. Classes come only from the copy committed on `origin/`, so a branch can neither declare nor drop its own; `gstack-approval-classes validate` checks the working-tree copy before you commit it, and `init` writes an example. `gstack-approval-classes check --base ` scans the branch's changes since the merge base and prints one `APPROVAL_CLASS: :` line per hit, one `APPROVAL_SCOPE: @` line per declared class with hits, and `APPROVAL_CLASSES: ok | found= | none` last (exit 0, 1 when found, 3 when the base file is invalid or `origin/` does not resolve). Built-in detectors only report: the ship policy's migration files, when no class sets `migrations: true`, print as `(built-in, report-only)` and need no citation. The digest covers the class's effects: each matched file with its blob at the scanned head, and each matched added line. The scan reads git's native diff (`--no-ext-diff --no-textconv`), so a configured external diff driver or text conversion cannot hide a line; when the content diff cannot be read, or its added lines disagree with git's own count, `check` and the receipt refuse with `APPROVAL_SOURCE_UNAVAILABLE` instead of passing. Adding, removing or editing an effect changes the token, so an approval given earlier no longer binds. **Enforcement.** `gstack-ship-receipt write` refuses with `APPROVAL_CITATION_MISSING`, writing nothing, unless each declared class with hits has a line in the `--body` file (or an `--approval` value) of the form ``` Approved by via [for @[,@]] ``` that resolves and binds: - A plan sha (7 to 64 hex characters) must match the plan of an autoplan run passed with `--approval-run ` whose gate is APPROVED, and the plan must name the scope token, or the class and every effect path. - A GitHub comment or review URL must be written by a `maintainers` login from the base's ship policy that equals the cited owner. A review must be in state `APPROVED`; a comment-only, dismissed or changes-requested review is not an approval. The evidence is the body's first non-blank line only. It must start at column 0 with `Approved` or `Approve` (any case, optionally in `**` bold; no list marker, quote or indent) and name the token, or the class and every effect path. Nothing later in the body counts, so a rejection that quotes or nests an example approval approves nothing. For example, a comment whose first line is `Approved data-repair@0123456789ab: preview-first, never at upgrade.` verifies; one that opens with any other line does not. - A token is authoritative. Evidence that names the class with a different digest approved earlier effects: it does not bind, and it never falls back to matching the class and paths. - Anything else (a thread code, a chat message, a URL gstack cannot read) is recorded `citation: unverified` for the coordinator, and only when the line attests which scope it covers with `for @` matching the current token. The receipt's `approval` object records the source, `status` (`checked`, or `unavailable` when `origin/` does not resolve), one record per class (scope token, effects, `citation: verified | unverified`, owner, date, ref, `binding: token | paths | attested`, evidence) and any report-only hits; the `SHIP_RECEIPT:` line ends with `approval=verified`, `approval=unverified:`, `approval=none` or `approval=unchecked`. A repository without the file gets no `approval` object unless a built-in detector reported. A data repair is a class to declare, and /review checks its shape: explicit, preview-first, and never reachable from an upgrade or migration path.