diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62b9f8da8..048a4deff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
- **`changelog_fragments: { dir }`** (`lib/ship-policy.ts`, `lib/changelog-fragments.ts`): a PR adds `
/.md` with its entry text and optional `bump: minor|major` front matter. Fragments are read as Git blobs only; a symlink, gitlink, nested path, empty or oversized fragment, one with a release heading, or a reused name is `CHANGELOG_FRAGMENT_INVALID` before any version is allocated.
- **`gstack-restamp` folds fragments**: their bodies go above any legacy `[Unreleased]` section under one new heading, the files are deleted, and the highest declared bump wins over the policy's. The tree receipt's `changelog-fragments` rule accepts exactly that fold and nothing else.
- **`gstack-ship-queue land --pr `** (`lib/ship-queue-land.ts`): the per-PR merge step. It takes the land lock (`refs/notes/gstack-land`), checks the /ship receipt and checks at the pin, merges the base resolving release fields only (a clean dependency edit survives; a product conflict refuses), stamps, pushes a commit with parents [pin, base] under an exact lease, waits for checks, restamps if the base moved (at most twice), squashes with `--match-head-commit` and confirms the base tree. Every step is journaled and resumable. `--repair` opens the follow-up PR after `QUEUE_LAND_DRIFT`; `--census` prints what land would do with every open PR, writing nothing.
+- **/ship writes the fragment** (`lib/ship-policy.ts`, `ship/SKILL.md.tmpl`): under the key, `show-effective` prints a `ship: changelog_fragments` line, so Step 12 writes no version and Step 13 writes `/.md` (linted with `gstack-prose-lint --gate`) instead of an `[Unreleased]` section; the skill's policy paragraph now says its `ship:` lines bind Steps 12–16.
- **`restamp_command` gets `GSTACK_RESTAMP_BASE_SHA`**, the release predecessor, so a repo counter such as a SAVE_VERSION computes predecessor + 1 from the base and never from the branch.
- New result codes with troubleshooting anchors: `CHANGELOG_FRAGMENT_INVALID`, `QUEUE_LAND_LOCKED`, `QUEUE_LAND_CONFLICT`, `QUEUE_LAND_DRIFT`, `QUEUE_LAND_BASE_BUSY`.
@@ -16,7 +17,7 @@
### Tests
- `test/changelog-fragments.test.ts`: blob-only reads and refusals, bump precedence, fold order with a legacy section, the restamp fold and its tree receipt, an edited fold refused.
-- `test/ship-queue-land.test.ts` (bare origin and a gh shim): happy path; two PRs from one base without a collision; a legacy stamp plus a dependency edit; a title-derived entry; a product conflict; the lock and `--break-lock`; the base moving during CI; a foreign push never overwritten; a missing receipt; drift, `--repair` and the lock release; the census.
+- `test/ship-queue-land.test.ts` (bare origin and a gh shim): happy path; two PRs from one base without a collision; a legacy stamp plus a dependency edit; a title-derived entry; a product conflict; the lock and `--break-lock`; the base moving during CI; a foreign push never overwritten; a missing receipt; drift, `--repair` and the lock release; the census; a receipt that fails #3567's depth check refuses.
- `test/version-gate-fragments.test.ts`: fragment heads, premature version changes, a bot author, an advanced base, an invalid fragment, the stamp commit's effective bump, and the workflow's missing path filter.
- `test/restamp-command-boundary.test.ts`: the hook reads its counter from `GSTACK_RESTAMP_BASE_SHA`, not the branch.
diff --git a/bin/gstack-doctor-components.sh b/bin/gstack-doctor-components.sh
index dda793216..d02a688e8 100644
--- a/bin/gstack-doctor-components.sh
+++ b/bin/gstack-doctor-components.sh
@@ -315,6 +315,11 @@ gstack_result_anchor() {
RECEIPT_PROSE_MISMATCH) printf '%s' 'receipt-prose-mismatch' ;;
RECEIPT_CARRY_PREGATE_STALE) printf '%s' 'receipt-carry-pregate-stale' ;;
RECEIPT_GIT_TOO_OLD) printf '%s' 'receipt-git-too-old' ;;
+ CHANGELOG_FRAGMENT_INVALID) printf '%s' 'changelog-fragment-invalid' ;;
+ QUEUE_LAND_LOCKED) printf '%s' 'queue-land-locked' ;;
+ QUEUE_LAND_CONFLICT) printf '%s' 'queue-land-conflict' ;;
+ QUEUE_LAND_DRIFT) printf '%s' 'queue-land-drift' ;;
+ QUEUE_LAND_BASE_BUSY) printf '%s' 'queue-land-base-busy' ;;
PR_WAIT_BLOCKED) printf '%s' 'pr-wait-blocked' ;;
PR_WAIT_REFUSED) printf '%s' 'pr-wait-refused' ;;
APPROVAL_RECORD_INVALID) printf '%s' 'approval-record-invalid' ;;
diff --git a/docs/ship-policy.md b/docs/ship-policy.md
index 6fe54954d..8f255beb0 100644
--- a/docs/ship-policy.md
+++ b/docs/ship-policy.md
@@ -1184,6 +1184,11 @@ release heading, or one whose name already landed on the base with other text
is `CHANGELOG_FRAGMENT_INVALID`, and nothing is allocated. Only fragments the
branch adds against the base count.
+**/ship.** `show-effective` prints a `ship: changelog_fragments` line, so /ship
+writes no version in Step 12 and writes Step 13's entry as
+`/.md`, linted with `gstack-prose-lint --gate`, never an
+`[Unreleased]` section.
+
**The fold.** `gstack-restamp` (and so `land`) concatenates the fragment
bodies in path order above any legacy `[Unreleased]` section, re-heads both as
one `## [] - ` section at the top, and deletes the fragment
diff --git a/lib/ship-policy.ts b/lib/ship-policy.ts
index 5159d153d..62ece4e94 100644
--- a/lib/ship-policy.ts
+++ b/lib/ship-policy.ts
@@ -571,6 +571,8 @@ export function shipConsequences(policy: ShipPolicy, base: string): string[] {
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)');
+ const frag = policy.changelog_fragments?.dir.replace(/\/+$/, '');
+ if (frag) out.push(`changelog_fragments ${frag}: Step 12 writes no version and Step 13 writes the entry as ${frag}/.md (the entry text only, no heading; \`bump: minor\` or \`bump: major\` front matter only when the change warrants more than the policy bump), never an [Unreleased] section; lint it with \`gstack-prose-lint ${frag}/.md --gate\`; \`gstack-ship-queue land\` assigns the version at merge (docs/ship-policy.md#changelog-fragments)`);
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');
out.push(policy.review_scope === 'all'
? 'review_scope all: Step 9.4 fixes every finding on every pass (gstack-review-scope classify --policy all)'
diff --git a/lib/ship-queue-land.ts b/lib/ship-queue-land.ts
index c245f19e7..2e1bc3061 100644
--- a/lib/ship-queue-land.ts
+++ b/lib/ship-queue-land.ts
@@ -20,7 +20,7 @@ import { restamp, versionAtRef } from './restamp';
import { matchesAny, type ShipPolicy } from './ship-policy';
import { ciAtHead, loadBasePolicy, type QueueContext, type StepOutput } from './ship-queue';
import { QUEUE_DIR_REL, git, writeJsonAtomic, type QueueFailure } from './ship-queue-gate';
-import { bindReceipt } from './ship-receipt';
+import { bindShippedReceipt } from './ship-receipt-depth';
export const LAND_LOCK_REF = 'refs/notes/gstack-land';
export const MAX_RESTAMPS = 2;
@@ -297,8 +297,9 @@ export function land(ctx: QueueContext, o: LandOptions): StepOutput {
if (j && (j.phase === 'landed' || j.reviewed_pin !== info.headRefOid && j.last_pushed_head !== info.headRefOid)) j = null;
const pin = j?.reviewed_pin ?? info.headRefOid;
const lines = [`LAND: #${o.pr} branch=${info.headRefName} pin=${pin}${j ? ` (resuming at ${j.phase})` : ''}`];
- const receipt = bindReceipt(info.body ?? '', o.pr, [pin]);
- lines.push(receipt.line);
+ git(root, ['fetch', '-q', 'origin', ctx.base]);
+ const receipt = bindShippedReceipt(root, `origin/${ctx.base}`, info.body ?? '', o.pr, [pin]);
+ lines.push(receipt.line, ...(receipt.depthLine ? [receipt.depthLine] : []));
if (!receipt.ok) return fail(lines, { code: receipt.code, message: receipt.message, fix: 'run /ship at the pinned head first' }, run);
if (!j) {
const ci = ciAtHead(ctx, pin);
diff --git a/ship/SKILL.md b/ship/SKILL.md
index 5846b96d3..af3321709 100644
--- a/ship/SKILL.md
+++ b/ship/SKILL.md
@@ -473,7 +473,7 @@ git merge origin/ --no-edit
**If already up to date:** Continue silently.
**Ship policy:** `~/.claude/skills/gstack/bin/gstack-ship-policy show-effective --base `
-(docs/ship-policy.md); its `ship:` lines bind Steps 12, 16 and history.
+(docs/ship-policy.md); its `ship:` lines bind Steps 12–16 and history.
If integration changes the artifact or distribution configuration inspected in Step 2,
repeat Step 2 (decisions included) on the merged content, then Step 4.
diff --git a/ship/SKILL.md.tmpl b/ship/SKILL.md.tmpl
index 810bbd990..60b353f16 100644
--- a/ship/SKILL.md.tmpl
+++ b/ship/SKILL.md.tmpl
@@ -204,7 +204,7 @@ git merge origin/ --no-edit
**If already up to date:** Continue silently.
**Ship policy:** `~/.claude/skills/gstack/bin/gstack-ship-policy show-effective --base `
-(docs/ship-policy.md); its `ship:` lines bind Steps 12, 16 and history.
+(docs/ship-policy.md); its `ship:` lines bind Steps 12–16 and history.
If integration changes the artifact or distribution configuration inspected in Step 2,
repeat Step 2 (decisions included) on the merged content, then Step 4.
diff --git a/test/fixtures/golden/agy-ship-SKILL.md b/test/fixtures/golden/agy-ship-SKILL.md
index 59c5baac2..89f7b3e99 100644
--- a/test/fixtures/golden/agy-ship-SKILL.md
+++ b/test/fixtures/golden/agy-ship-SKILL.md
@@ -473,7 +473,7 @@ git merge origin/ --no-edit
**If already up to date:** Continue silently.
**Ship policy:** `~/.gemini/antigravity-cli/skills/gstack/bin/gstack-ship-policy show-effective --base `
-(docs/ship-policy.md); its `ship:` lines bind Steps 12, 16 and history.
+(docs/ship-policy.md); its `ship:` lines bind Steps 12–16 and history.
If integration changes the artifact or distribution configuration inspected in Step 2,
repeat Step 2 (decisions included) on the merged content, then Step 4.
diff --git a/test/fixtures/golden/claude-ship-SKILL.md b/test/fixtures/golden/claude-ship-SKILL.md
index 5846b96d3..af3321709 100644
--- a/test/fixtures/golden/claude-ship-SKILL.md
+++ b/test/fixtures/golden/claude-ship-SKILL.md
@@ -473,7 +473,7 @@ git merge origin/ --no-edit
**If already up to date:** Continue silently.
**Ship policy:** `~/.claude/skills/gstack/bin/gstack-ship-policy show-effective --base `
-(docs/ship-policy.md); its `ship:` lines bind Steps 12, 16 and history.
+(docs/ship-policy.md); its `ship:` lines bind Steps 12–16 and history.
If integration changes the artifact or distribution configuration inspected in Step 2,
repeat Step 2 (decisions included) on the merged content, then Step 4.
diff --git a/test/fixtures/golden/codex-ship-SKILL.md b/test/fixtures/golden/codex-ship-SKILL.md
index afd639848..8e3efeb73 100644
--- a/test/fixtures/golden/codex-ship-SKILL.md
+++ b/test/fixtures/golden/codex-ship-SKILL.md
@@ -472,7 +472,7 @@ git merge origin/ --no-edit
**If already up to date:** Continue silently.
**Ship policy:** `$GSTACK_ROOT/bin/gstack-ship-policy show-effective --base `
-(docs/ship-policy.md); its `ship:` lines bind Steps 12, 16 and history.
+(docs/ship-policy.md); its `ship:` lines bind Steps 12–16 and history.
If integration changes the artifact or distribution configuration inspected in Step 2,
repeat Step 2 (decisions included) on the merged content, then Step 4.
diff --git a/test/fixtures/golden/factory-ship-SKILL.md b/test/fixtures/golden/factory-ship-SKILL.md
index e2c3e30bc..c8172cdcd 100644
--- a/test/fixtures/golden/factory-ship-SKILL.md
+++ b/test/fixtures/golden/factory-ship-SKILL.md
@@ -474,7 +474,7 @@ git merge origin/ --no-edit
**If already up to date:** Continue silently.
**Ship policy:** `$GSTACK_ROOT/bin/gstack-ship-policy show-effective --base `
-(docs/ship-policy.md); its `ship:` lines bind Steps 12, 16 and history.
+(docs/ship-policy.md); its `ship:` lines bind Steps 12–16 and history.
If integration changes the artifact or distribution configuration inspected in Step 2,
repeat Step 2 (decisions included) on the merged content, then Step 4.
diff --git a/test/gstack-ship-policy.test.ts b/test/gstack-ship-policy.test.ts
index 983491aa5..20c0e3343 100644
--- a/test/gstack-ship-policy.test.ts
+++ b/test/gstack-ship-policy.test.ts
@@ -160,6 +160,9 @@ describe('gstack-ship-policy (bin)', () => {
expect(r.out).toMatch(/^ship: bump patch: Step 12 never asks/m);
expect(r.out).toMatch(/^ship: release_tool: `node gen\.js` runs only inside gstack-restamp/m);
expect(r.out).not.toMatch(/^ship: history/m);
+ expect(r.out).not.toMatch(/^ship: changelog_fragments/m);
+ const frag = fixture({ policy: { ...BASE_POLICY, changelog_fragments: { dir: 'changelog.d/' }, min_gstack_version: '1.92.0.0' } });
+ expect(runBin(frag, 'gstack-ship-policy', ['show-effective', '--base', 'main']).out).toMatch(/^ship: changelog_fragments changelog\.d: Step 12 writes no version and Step 13 writes the entry as changelog\.d\/\.md .*never an \[Unreleased\] section; lint it with `gstack-prose-lint changelog\.d\/\.md --gate`/m);
const strict = fixture({ policy: { ...BASE_POLICY, history: 'merge-only', queue_mode: 'claim-at-open', bump: 'ask', release_tool: null, preregistration_shas: ['0123456789abcdef0123456789abcdef01234567'] } });
const lines = runBin(strict, 'gstack-ship-policy', ['show-effective', '--base', 'main']).out.split('\n').filter(l => l.startsWith('ship: '));
expect(lines.map(l => l.split(':')[1]!.trim())).toEqual(['history merge-only', 'queue_mode claim-at-open', 'bump ask', 'review_scope frozen', 'preregistration']);
diff --git a/test/ship-queue-land.test.ts b/test/ship-queue-land.test.ts
index 2ff4225bf..335a19f58 100644
--- a/test/ship-queue-land.test.ts
+++ b/test/ship-queue-land.test.ts
@@ -200,6 +200,16 @@ describe('land: lock, races and refusals', () => {
expect(remote(fx, 'pr-8')).toBe(late);
}, 90_000);
+ test('the receipt check is the depth-checked one READY uses: a receipt whose writer records depth but which records none refuses', () => {
+ const fx = setup();
+ const head = openPr(fx, '31');
+ fx.write(d => { d.prs['31'].body = shippedBody('31', head, { gstack_version: '1.91.307.0' }); });
+ const r = land(fx, ['--pr', '31']);
+ expect(r.err).toContain('(QUEUE_RECEIPT_DEPTH_RAISED)');
+ expect(r.out).toMatch(/^REVIEW_DEPTH: receipt=none required=/m);
+ expect(remote(fx, 'pr-31')).toBe(head);
+ }, 60_000);
+
test('drift: a merge outside land between the check and the squash fails loudly and keeps the lock', () => {
const fx = setup();
openPr(fx, '9');
diff --git a/test/size-ledger/ship/gsta154-changelog-fragments.json b/test/size-ledger/ship/gsta154-changelog-fragments.json
new file mode 100644
index 000000000..69cbc7434
--- /dev/null
+++ b/test/size-ledger/ship/gsta154-changelog-fragments.json
@@ -0,0 +1,4 @@
+{
+ "bytes": 1,
+ "why": "GSTA-154: the ship-policy paragraph says its ship: lines bind Steps 12–16, so the changelog_fragments line reaches Step 13's entry"
+}