From da67a3cd2b6405abea4b577340244d7d27477c44 Mon Sep 17 00:00:00 2001 From: garrytan Date: Fri, 16 Oct 2026 23:36:55 +0000 Subject: [PATCH] fix(ship): shared-queue pointer rides the show-effective ship: line (ship skeleton has 2 bytes of parity headroom) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> (cherry picked from commit 5b33d4a01594242259abee3c8c91229c16d3f7ce) --- docs/ship-policy.md | 1 + lib/ship-policy.ts | 1 + ship/SKILL.md | 5 ----- ship/SKILL.md.tmpl | 5 ----- test/ship-policy-compat.test.ts | 1 + 5 files changed, 3 insertions(+), 10 deletions(-) diff --git a/docs/ship-policy.md b/docs/ship-policy.md index be02b863c..815369386 100644 --- a/docs/ship-policy.md +++ b/docs/ship-policy.md @@ -262,6 +262,7 @@ which carry the workflow consequences the skill prose would otherwise spell out: |---|---| | `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 | diff --git a/lib/ship-policy.ts b/lib/ship-policy.ts index 113f55637..e72ecfefa 100644 --- a/lib/ship-policy.ts +++ b/lib/ship-policy.ts @@ -458,6 +458,7 @@ export function shipConsequences(policy: ShipPolicy, base: string): string[] { if (policy.history === 'merge-only') out.push('history merge-only: never rebase, squash or force-push this branch; `gstack-ship-policy check-history --plan ` refuses a rewrite plan (HISTORY_POLICY_VIOLATION)'); if (policy.queue_mode === 'stamp-at-merge') { out.push(`queue_mode stamp-at-merge: Step 12 items 1-4 are replaced by \`gstack-restamp --next --base ${base} --pr --expect-base \` when this PR is next in the queue (\`--after \` gates ahead in a throwaway worktree when it is not); follow its next: line; claims are ignored`); + out.push(`shared merge queue: when a coordinator runs the queue, \`gstack-ship-queue ready --pr \`, \`gate-ahead --after --pr \` and \`stamp --pr --version --expect-gated --expect-base \` replace the calls above; stamp only when your slot is called, send the READY block and the QUEUE_STAMP: line; the coordinator runs \`gstack-ship-queue verify\` (docs/ship-policy.md#run-a-shared-merge-queue)${policy.gate_command ? `; gate command: \`${policy.gate_command}\`` : ''}`); out.push('gate-ahead: with .gstack/tmp/gate-ahead.json present, Step 16 reuses only the lanes `gstack-tree-receipt --from .gstack/tmp/gate-ahead.json --predecessor-pr --bundle --json > .gstack/tmp/tree-receipt.json` marks reusable; stamp-sensitive and not-eligible lanes run live; `--force-gate ` is the coordinator\'s call'); } else out.push('queue_mode claim-at-open: Step 12 runs as written (claims through gstack-next-version)'); out.push(policy.bump === 'patch' ? 'bump patch: Step 12 never asks for MINOR or MAJOR; PATCH or MICRO by size' : 'bump ask: Step 12 asks for MINOR and MAJOR as written'); diff --git a/ship/SKILL.md b/ship/SKILL.md index d3b21dd0b..fcc7d06ae 100644 --- a/ship/SKILL.md +++ b/ship/SKILL.md @@ -770,11 +770,6 @@ 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 diff --git a/ship/SKILL.md.tmpl b/ship/SKILL.md.tmpl index da69f0768..68021885b 100644 --- a/ship/SKILL.md.tmpl +++ b/ship/SKILL.md.tmpl @@ -235,11 +235,6 @@ 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 diff --git a/test/ship-policy-compat.test.ts b/test/ship-policy-compat.test.ts index 20578fd5a..be9efabdb 100644 --- a/test/ship-policy-compat.test.ts +++ b/test/ship-policy-compat.test.ts @@ -56,6 +56,7 @@ describe('through the binaries', () => { expect(r.out).toContain(`KEY: gate_command introduced=${SHIP_QUEUE_KEYS_VERSION}\n`); expect(r.out).toContain(`MIN_READER: gstack ${SHIP_QUEUE_KEYS_VERSION} `); expect(JSON.parse(runBin(repo, 'gstack-ship-policy', ['validate', '--json']).out).min_reader).toBe(SHIP_QUEUE_KEYS_VERSION); + expect(runBin(repo, 'gstack-ship-policy', ['show-effective', '--base', 'main']).out).toMatch(/^ship: shared merge queue: .*gstack-ship-queue ready --pr .*\(docs\/ship-policy\.md#run-a-shared-merge-queue\); gate command: `bun run test`$/m); const explain = runBin(repo, 'gstack-ship-policy', ['init', '--explain']); expect(explain.out).toMatch(new RegExp(`^full_e2e: .* default: false introduced: ${SHIP_QUEUE_KEYS_VERSION.replace(/\./g, '\\.')}$`, 'm')); }, 60_000);