mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
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:
1 parent
ade554087b
commit
7fc73e8892
5 files changed
+242
-1
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user