docs: stamped trains and train land; QUEUE_TRAIN_STAMP_MISMATCH anchor

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-29 08:52:02 +00:00
1 parent ead4a7c273
commit 7567f3c8d1
4 files changed
+71 -6

No files matched your search

+11 -6
View File
@@ -5,21 +5,26 @@
**A repository can now assign its version when a PR merges instead of inside the PR: PRs carry unversioned CHANGELOG fragments and `gstack-ship-queue land` stamps, pushes and merges.** Today every PR picks a version before it merges, so each merge makes the next PR's version stale and its lane redoes the stamp (a median 14.5 minutes from stamp commit to merge over 30 slots, Oct 27-29; the queue froze on hand-made stamps in #3422 and #3487). With `changelog_fragments` on, two PRs cut from one base cannot collide, and nobody stamps by hand. gstack's own policy does not turn it on in this release (docs/ship-policy.md#changelog-fragments).
### Added
- **`changelog_fragments: { dir }`** (`lib/ship-policy.ts`, `lib/changelog-fragments.ts`): a PR adds `<dir>/<slug>.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, a reused name, or one whose text is already released on the base (a parent PR's fragment carried in by stacked history) is `CHANGELOG_FRAGMENT_INVALID` before any version is allocated.
- **`changelog_fragments: { dir }`** (`lib/ship-policy.ts`, `lib/changelog-fragments.ts`): a PR adds `<dir>/<slug>.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 <n>`** (`lib/ship-queue-land.ts`): the per-PR merge step. It takes the land lock (`refs/notes/gstack-land`), checks the /ship receipt, the READY 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` (merges with `--merge` under `history: merge-only`) and confirms the base tree. A PR whose base is another branch refuses before the lock; a refusal before the push releases the lock. Every step is journaled and resumable, also from the stamp commit's pin when the local journal is gone. `--repair` opens the follow-up PR after `QUEUE_LAND_DRIFT`; `--census` prints what land would do with every open PR, writing nothing.
- **`gstack-ship-queue land --pr <n>`** (`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 `<dir>/<branch-slug>.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`.
- **Stamped merge trains** (`lib/ship-queue-train-stamp.ts`, `lib/ship-queue-train-land.ts`): under `changelog_fragments`, `train build` also builds S_i = stamp(merge(S_(i-1), pin_i)) for every slot, checks each is the merge modulo stamps, keeps every earlier release entry byte for byte and equals the plain prefix outside release paths (`QUEUE_TRAIN_STAMP_MISMATCH`), and gates S_k. `train land --train <nonce> --pr <n>` publishes slot i's S_i exactly: a stamp commit on [pin, main] pushed under the pin's lease with the `evals-no-debounce` label, checks, `--match-head-commit`, main's tree confirmed; any other main freezes the queue as before. `verify --train` refuses a stamped train and names `train land`.
### Changed
- **Version Gate runs on every PR** (the path filter is gone), so under `changelog_fragments` a PR that touches no release file still proves it carries a fragment. An unstamped head keeps its release fields at its merge base and adds a valid fragment (legacy `[Unreleased]` passes; a `[bot]` author needs none); a merge-step stamp commit must have parents [pin, base] and carry the base plus its fragments' effective bump, so a lane that stamps its own head fails. Without the key nothing changes.
- **The main backstop covers a stamped train's merges by exact tree** (`scripts/train-backstop.ts`, `scripts/train-gate.ts`): a record's S_i prefixes cover main only when its tree is exactly S_i; plain T_i prefixes still cover modulo stamps.
- **Version Gate runs on every PR** (the path filter is gone), so under `changelog_fragments` a PR that touches no release file still proves it carries a fragment. An unstamped head keeps its release fields at its merge base and adds a valid fragment (legacy `[Unreleased]` passes; a `[bot]` author needs none); a merge-step stamp commit must carry the base plus its fragments' effective bump. Without the key nothing changes.
### Tests
- `test/changelog-fragments.test.ts`: blob-only reads and refusals, a carried-back fragment refused on whole released lines only, 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; a receipt that fails #3567's depth check refuses; a base with a four-part package.json in `stamp_files` lands four parts (#3557), gstack's own shape stays three; one run without `--no-wait` merges; a PR on another base refuses; `--break-lock` refuses a drift lock; a conflict releases the lock; a stacked parent's fragment refuses; a lost journal resumes; `merge-only` merges with `--merge`; a merge GitHub completed behind an error reply reconciles; `--break-lock` refuses a holder mid-merge; `--repair` resumes after a failed PR create and its PR takes over from a clean checkout; a symlinked legacy VERSION refuses in `--dry-run`; a `package.json` version source keeps its dependency edits.
- `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 (open version claims do not apply to it), and the workflow's missing path filter.
- `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; a receipt that fails #3567's depth check refuses; a base with a four-part package.json in `stamp_files` lands four parts (#3557), gstack's own shape stays three.
- `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.
- `test/ship-queue-train-stamp.test.ts`: S2 keeps S1's release and runs the versions in order with each slot's bump; a stacked child folds only its own fragment; a tampered earlier version or a product change is refused.
- `test/ship-queue-train-land.test.ts`: build records S1 and S2 and gates S2; land publishes each slot exactly with the label; a foreign push is never overwritten; a merge outside the train freezes the queue; a plain train refuses `train land`.
- `test/train-backstop.test.ts`: main equal to S1 is covered; the same tree with another VERSION is gated; a chain mixing stamped and plain prefixes is refused.
## [1.91.312.0] - 2026-10-29
+7
View File
@@ -254,6 +254,13 @@ loop, per train:
`gh pr merge --match-head-commit`; `verify --train` again.
4. `train prune` when every slot has merged.
Under `changelog_fragments` ([ship-policy.md](ship-policy.md#train-land)) lanes READY with a
fragment (`changelog.d/<slug>.md`) instead of an `[Unreleased]` section, there is no slot
call and no lane stamp, and step 3 is one command per slot: `train land --train <nonce> --pr
<n>` commits the stamped prefix S_i on [pin, main], pushes it, waits for checks, merges and
confirms main is S_i exactly. Outside a train, `gstack-ship-queue land --pr <n>` is the same
step for one PR ([ship-policy.md](ship-policy.md#land)).
A red train: `train bisect` on the red cases over every prefix. An attributed PR leaves
the train and goes back to its lane; red on the base branch itself is fixed forward
through an urgent one-PR train (or `train build --repair` while the queue is frozen, which
+39
View File
@@ -365,6 +365,45 @@ 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.
<a id="train-land"></a>
### Stamped trains under `changelog_fragments`: `train land`
When the base policy also declares [`changelog_fragments`](#changelog-fragments),
`train build` builds every prefix twice. T_i is the plain chain above. S_i is
what slot i publishes: S_0 is the base and S_i = stamp(merge(S_(i-1), pin_i)),
stamped by `gstack-restamp` with S_(i-1) as its base, so slot i folds only its
own fragments (a stacked pin's parent fragments are already folded) and the
versions run in slot order, each with its slot's effective bump. Before
anything is pushed, each S_i must be merge(S_(i-1), pin_i) modulo stamps,
keep every release entry of S_(i-1) byte for byte, and equal T_i outside
release paths (version files, CHANGELOG, fragments, `release_outputs`,
`stamp_paths`, `stamp_files`); otherwise `QUEUE_TRAIN_STAMP_MISMATCH`. Pass
`--allow-repo-commands` when the policy has a `restamp_command` or
`release_tool`. The record keeps both chains (`prefixes` and `stamped`), the
train head and branch are S_k, and the one full gate runs on S_k, the exact
tree the last merge publishes.
Slots merge with `train land --train <nonce> --pr <n>`, never a slot call:
1. main's tree must equal S_(i-1)'s exactly (slot 1: the train base), and the
PR's head its pin, with its /ship receipt.
2. It commits S_i's tree with parents [pin, main], adds the
`evals-no-debounce` label (a tool-made head gets no later push, so the
10-minute quiet window protects nothing), and pushes under
`--force-with-lease=<branch>:<pin>`; a foreign push is never overwritten
(`QUEUE_TRAIN_MOVED`).
3. It waits for the PR's checks on that commit (`--no-wait` prints the
command). Required checks run live at the slot until GSTA-156's exact-tree
reuse is on main.
4. It refuses if main moved during the checks, merges with
`--match-head-commit`, and confirms main's tree is S_i exactly. Any other
main is the freeze path above (`QUEUE_TRAIN_TREE_MISMATCH`, backstop).
`verify --train` refuses a stamped train and names `train land`. The backstop
covers main only when its exact tree is some S_i of a green record (plain
T_i records still cover modulo stamps). A dropped slot changes every later
version, so the train is rebuilt, as today.
<a id="ship-before-ready"></a>
**0. Lane: /ship before READY.** Every READY follows a /ship run on the branch
at the head being READY'd, including a re-READY after a push (Garry,
+14
View File
@@ -4267,6 +4267,20 @@ the two allowed restamps, so `land` stopped instead of restamping again.
it), then rerun `gstack-ship-queue land --pr <n>`; the lock was released and
the rerun restamps from the pin.
<a id="queue-train-stamp-mismatch"></a>
### `QUEUE_TRAIN_STAMP_MISMATCH`
**Meaning.** While building a stamped train (`changelog_fragments`), a prefix
S_i could not be stamped, or it is not the merge of S_(i-1) and its pin
modulo stamps, or it rewrote an earlier release entry, or it differs from the
plain prefix T_i outside release paths. Nothing was pushed or dispatched. See
[stamped trains](ship-policy.md#train-land).
**Fix.** Read the named slot and files. A stamp hook (`restamp_command`,
`release_tool`) that writes product files is fixed in the hook; an invalid
fragment goes back to its lane. Otherwise rebuild the train without that slot
and report it.
<a id="queue-train-moved"></a>
### `QUEUE_TRAIN_MOVED`