docs(ship-policy): Run a shared merge queue; new keys and compatibility rule; troubleshooting anchors; /ship pointer

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-16 23:35:07 +00:00
1 parent ade554087b
commit 7fc73e8892
5 files changed
+242 -1

No files matched your search

+7
View File
@@ -213,6 +213,13 @@ gstack_result_anchor() {
BASH_UNAVAILABLE) printf '%s' 'bash-unavailable' ;;
IOS_LAUNCH_ENV_INVALID) printf '%s' 'ios-launch-env-invalid' ;;
OUTSIDE_VOICE_UNAVAILABLE) printf '%s' 'outside-voice-unavailable' ;;
POLICY_GSTACK_TOO_OLD) printf '%s' 'policy-gstack-too-old' ;;
QUEUE_NOT_READY) printf '%s' 'queue-not-ready' ;;
QUEUE_PROOF_MISSING) printf '%s' 'queue-proof-missing' ;;
QUEUE_STAMP_NOT_STAMP_ONLY) printf '%s' 'queue-stamp-not-stamp-only' ;;
QUEUE_VERIFY_MISMATCH) printf '%s' 'queue-verify-mismatch' ;;
QUEUE_GATE_MISSING) printf '%s' 'queue-gate-missing' ;;
GATE_AHEAD_ATTEMPT_INVALID) printf '%s' 'gate-ahead-attempt-invalid' ;;
*) return 1 ;;
esac
}
+128 -1
View File
@@ -18,6 +18,7 @@ introduces or changes the policy runs under the reviewed bootstrap path
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)),
`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).
@@ -27,7 +28,14 @@ code from [troubleshooting.md](troubleshooting.md#queue-aware-ship-policy-restam
`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.
`.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 <version>`, the floor every lane reading the policy needs.
| Key | Values | Default | Meaning |
|---|---|---|---|
@@ -47,6 +55,9 @@ prints this table. JSON carries no comments, so examples live in
| `stamp_patterns` | literal templates | `[]` | At most 16 templates of at most 128 characters, each with exactly one `{version}` slot (`"version": "{version}"`, `since: '{version}'`, `<!-- x-stamp: {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 stamp` and `verify` refuse without one (`QUEUE_GATE_MISSING`). Needs `min_gstack_version`. |
| `min_gstack_version` | version or `null` | `null` | The oldest gstack that may read the file; see the compatibility rule above. |
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,
@@ -125,6 +136,122 @@ 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.
<a id="run-a-shared-merge-queue"></a>
## 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=<path>`. 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 <head> STAMP_VERSION=unstamped
pr: https://github.com/<owner>/<repo>/pull/42
head: <head>
version: unstamped
base: origin/main@<sha>
ci: green at <head12> (passed=31 failed=0 pending=0 skipped=2)
PREGATE: pass tree=<tree12> head=<head12> checks=6
gate: command="<gate_command>" full_e2e=no gate-ahead=none
SHIP_RECEIPT: pr=42 head=<head12> …
```
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=<id> gated=<sha> tree=<tree> merge_tree=<tree>
predecessor=#41@<sha12> version=<N+1>`, 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 <worktree> && <gate command>) > gate.log 2>&1
gstack-ship-queue gate-ahead --record --pr 42 --attempt <id> --exit $? --log gate.log
```
`GATE_RECORD: pr=42 attempt=<id> exit=0 gated_tree=<tree> log_sha256=<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 <id>)`.
`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 `<sha>`, 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 <gated> \
--expect-base <sha> --expect-order 41,42 --allow-repo-commands
```
`<gated>` 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 `<sha>` 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=<sha> gated=<sha> tree=same-modulo-stamps receipt=<sha256>
```
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=<sha> committed=no|yes <sha> stage=<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 <head> 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 <gated> --receipt <sha256> --expect-head <head>
gstack-ship-queue verify --pr 42 --record <gate-ahead record> --receipt <sha256> --expect-head <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=<sha>
gated=<sha> tree=same-modulo-stamps receipt=<same sha256>`. 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 <branch head> <predecessor head>`
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=<head>`: 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.
<a id="ship"></a>
## What `/ship` reads from it
+97
View File
@@ -3334,3 +3334,100 @@ unset, not in bare mode). Then re-run the review.
**Expected result.** An `OUTSIDE VOICE (...)` report, or `CODEX SAYS (plan
review — outside voice)` when the provider runs.
<a id="policy-gstack-too-old"></a>
### `POLICY_GSTACK_TOO_OLD`
**Meaning.** `.gstack/ship-policy.json` on the base branch sets
`min_gstack_version` above the gstack reading it. The policy uses keys this
gstack would read differently or not at all, so every ship-policy reader
(`gstack-restamp`, `gstack-tree-receipt`, `gstack-ship-queue`,
`gstack-ship-policy`) refuses it instead of guessing. A gstack older than
`min_gstack_version` itself sees that key as unknown and fails with
`POLICY_INVALID`, which names the key.
**Fix.** Upgrade gstack to the version the message names
(`bin/gstack-upgrade`, or rerun the session install), then rerun. A crew lead
raising `min_gstack_version` raises the crew's install floor at the same
time; `gstack-ship-policy validate` prints `MIN_READER:` for exactly that.
<a id="queue-not-ready"></a>
### `QUEUE_NOT_READY`
**Meaning.** `gstack-ship-queue ready` will not print a READY block the
coordinator could misread: the working tree has uncommitted tracked changes,
HEAD is not the PR's pushed head (read through `gh pr view`), CI at that
exact head is not green (failed, pending or unreadable), or the pregate result
is missing, failed or recorded on another tree (`PREGATE_STALE`).
**Fix.** Commit and push, wait for CI on that head, rerun
`gstack-pregate run --allow-repo-commands` when the tree changed, then rerun
`gstack-ship-queue ready --pr <n>`. The block it prints before refusing shows
which line is not yet true.
<a id="queue-proof-missing"></a>
### `QUEUE_PROOF_MISSING`
**Meaning.** A queue step lacks an input it needs to prove its claim, or an
input does not match: the gated head is not a commit here, HEAD differs from
the gated head beyond stamps (the gate never covered this tree), an earlier
stamp with other inputs is unfinished, the stamp journal's head moved, or a
gate-ahead merge could not be recomputed with `git merge-tree`. Nothing was
written unless the `QUEUE_STAMP_REFUSED:` line says `committed=yes`.
**Fix.** Supply the named input (fetch the gated head, pass the current base
sha), or regate the tree you mean to stamp. For an unfinished stamp, rerun the
identical command to resume, or revert its files and delete
`.gstack/tmp/ship-queue/stamp-<pr>.json`.
<a id="queue-stamp-not-stamp-only"></a>
### `QUEUE_STAMP_NOT_STAMP_ONLY`
**Meaning.** `gstack-ship-queue stamp` committed the stamp, and the tree
receipt between the gated head and the stamped head found a difference
outside the stamp rules (the residual paths are printed). The gate's result
does not cover that head. The refusal line says `committed=yes <sha>`.
**Fix.** Inspect the residual paths: drop the stray edit and stamp again, or
run the gate on the stamped head.
<a id="queue-verify-mismatch"></a>
### `QUEUE_VERIFY_MISMATCH`
**Meaning.** The coordinator's `gstack-ship-queue verify` could not
reproduce the lane's claim from the PR's pushed commits: the head moved, the
head differs from the gated tree beyond stamps, the receipt digest differs
from the one in `QUEUE_STAMP`, or, for a gate-ahead record, `git merge-tree
--write-tree` gives another merge tree, the recorded stamp patch does not
rebuild the recorded gated tree or changes more than stamps, or the
predecessor PR moved after the gate.
**Fix.** Do not merge. Ask the lane to rerun `gstack-ship-queue stamp` (or
`gate-ahead`) on the current queue and report again.
<a id="queue-gate-missing"></a>
### `QUEUE_GATE_MISSING`
**Meaning.** The ship policy sets `full_e2e: true`, and no passing full-gate
result is bound to the tree being stamped or verified: no recorded gate-ahead
attempt for that gated head, no `--gate-exit 0 --gate-log <file>` from a run
on it, or the recorded run exited non-zero.
**Fix.** Run the gate the policy's `gate_command` names in the gate-ahead
worktree and bind it with `gstack-ship-queue gate-ahead --record`, or pass the
exit code and log of a passing run on the gated head to `stamp`.
<a id="gate-ahead-attempt-invalid"></a>
### `GATE_AHEAD_ATTEMPT_INVALID`
**Meaning.** `gstack-ship-queue gate-ahead --record` could not bind the gate
result: the attempt id is unknown for that PR, the attempt already holds a
different result, the log file is missing, or the attempt's worktree no longer
holds the gated commit (its HEAD moved or a tracked file changed), so the
result would not describe the gated tree.
**Fix.** `gstack-ship-queue gate-ahead --list --pr <n>` shows every attempt
and the current one. Record the live attempt, or prepare a new one with
`gate-ahead --after <pr>` and run the gate there without editing tracked
files.
+5
View File
@@ -770,6 +770,11 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
`ship: queue_mode stamp-at-merge` replaces items 1–4.
**Shared merge queue:** when a coordinator runs the queue, use
`~/.claude/skills/gstack/bin/gstack-ship-queue ready | gate-ahead | stamp`
(docs/ship-policy.md#run-a-shared-merge-queue): stamp only when your slot is
called, and send the printed READY and `QUEUE_STAMP:` lines.
1. **Classify state** — pure reader, never writes:
```bash
bun run ~/.claude/skills/gstack/bin/gstack-version-bump classify --base <base>
+5
View File
@@ -235,6 +235,11 @@ chooses it in item 2 and ALREADY_BUMPED derives it in item 1.
`ship: queue_mode stamp-at-merge` replaces items 1–4.
**Shared merge queue:** when a coordinator runs the queue, use
`~/.claude/skills/gstack/bin/gstack-ship-queue ready | gate-ahead | stamp`
(docs/ship-policy.md#run-a-shared-merge-queue): stamp only when your slot is
called, and send the printed READY and `QUEUE_STAMP:` lines.
1. **Classify state** — pure reader, never writes:
```bash
bun run ~/.claude/skills/gstack/bin/gstack-version-bump classify --base <base>