v1.91.282.0 fix(ship-queue): one CHANGELOG rule before and after the merge, base keys stay in slot stamps, a stacked child carries past its released parent (#3518)

* Ship-queue train tooling: one CHANGELOG rule, base keys stay, stacked-child carry (GSTA-129)

Four train-24 (GSTA-96) defects in one change.

verify --train applies the same CHANGELOG rule before and after the merge:
main's CHANGELOG plus the PR's own lines (its authored diff plus what
mergeChangelogs writes for it). A head the pre-merge verify accepted no longer
freezes the queue after its squash merge, and a freeze record lists a slot
GitHub reports MERGED as merged.

restamp's stamp_paths stage leaves a *_KEY_VERSION line the base introduced
after the fork point alone, base merge committed or staged, and says so.

mergeChangelogs with a base carries a stacked child's edits to its released
parent entry as "Correction to the <version> entry below" bullets under the
child's own Gate integrity section instead of returning null; the slot
one-liner prints each carried correction. #3453's two 1.91.276.0 corrections
are restored that way here.

The digest-freshness defect is already closed by #3451 (regeneratedAtBase);
docs/ship-policy.md now says how an unstamped generator change ships.

Docs: ship-policy § Run a merge train, captain.md § Merge trains and the
slot-call template carry the stamp-files and stacked-child rules.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Review fixes: carried union from slot 2, GitHub-merged slot-1 freeze, no-own-entry refusal, fail-closed base diff (GSTA-129)

Pre-landing review pass 1 (six specialists and the Red Team, all INFORMATIONAL):
- mergeStep unions the carried child from slot 2 on, so a stacked child behind
  another slot never repeats its released parent's text under a second
  [Unreleased] (Red Team, lib/ship-queue-train.ts).
- stopped() freezes the queue when any slot is MERGED on GitHub, so a slot-1
  squash that fails the post-merge check no longer answers "nothing merged"
  (Red Team, lib/ship-queue-train-verify.ts).
- ownEntryAboveMain refuses a CHANGELOG equal to main's: no entry of the PR's own
  (state-machines, lib/changelog-check.ts).
- branchAddedLines refuses when the base diff cannot run instead of reading it as
  "nothing added" (Red Team, lib/restamp.ts).
- slot-call.md and docs/captain.md name the stamp files as the ones the slot
  call names, STAMP_FILES plus a *_KEY_VERSION file the PR introduced, matching
  docs/ship-policy.md (maintainability).

Also: the coverage tests Step 7 added (post-merge parent check, carry shapes),
and two dependency-map rows (test/helpers/free-fixtures-data.ts for the
merge-train-3453 fixtures, scripts/test-pr-profile.ts for captain/templates/**)
so the paid-gate selector and the FREE_FIXTURES inventory accept the new files.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Review pass 2: an unreadable GitHub state at a failed slot refuses, never "nothing merged" (GSTA-129)

stopped() read prState's 'unreadable' as "not merged": with nothing in
t.merged, a slot-1 squash already on main while gh pr view failed answered
"nothing of the train merged, so the queue is not frozen". It now refuses with
QUEUE_PROOF_MISSING ("whether #<n> merged on GitHub could not be read, so
whether the queue must freeze is unknown") and asks for the verify to be rerun;
no freeze, no backstop. Raised independently by five pass-2 reviewers.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Docs audit: troubleshooting codes and ship-policy sections match the shipped train rules (GSTA-129)

The /document-release audit (edit mode) corrected two docs the PR had not
reached: docs/troubleshooting.md's QUEUE_TRAIN_TREE_MISMATCH, QUEUE_PROOF_MISSING,
QUEUE_TRAIN_FROZEN and POLICY_SOURCE_UNAVAILABLE entries, and docs/ship-policy.md's
train build conflict rule (the carried stacked child is the exception), the
verify freeze paragraph (slot-1 GitHub-MERGED freeze, unreadable-state refusal)
and the restamp stamp_paths stage (base-added lines stay; an unrunnable base diff
refuses). Five mid-sentence colons in the PR's own paragraphs are rewritten for
the prose lint, and the CHANGELOG Docs bullet names docs/troubleshooting.md.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* HARNESS_VERSION: scripts/test-pr-profile.ts row recorded non-behavioral at version 6 (GSTA-129)

The remote free tier's only red was the HARNESS_VERSION guard: scripts/test-pr-profile.ts is a pinned shared-harness file and gained the captain/templates/** FREE_ONLY_PR_FILES row. Recorded with bump-harness-version.ts --non-behavioral: no paid case reads the crew templates, so no selection, input or verdict changes and no pass-rate series resets.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: type the saved fixture PR row for the test-code ratchet (GSTA-129)

CI's typecheck:test ratchet refused one new TS2698 (spread of a loosely typed fixture row) in the slot-1 freeze test; the row is read as any, as the neighbouring state writes already do.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* ship-queue verify: build the freeze record from the one GitHub read that decided to freeze

freezeQueue took a second prState read per unverified slot and filed anything
but an exact MERGED under unmerged, so a slot that read MERGED at stopped()
and was unreadable a moment later was recorded as unmerged (the record half of
train 24's #3453 misfile). stopped() now reads each unverified slot once and
passes those states in; a MERGED slot stays merged, and a slot unreadable
beside a merged one is kept out of unmerged and named in the reason. The
fixture's gh shim gains a per-PR vanish count so a test can change GitHub's
answer between two reads.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* ship-queue verify: say what happens to a slot unreadable beside a merged one

The freeze reason, docs and CHANGELOG said the slot was kept out of unmerged
'until read', but nothing reads it again: the NEXT after the repair is built
from unmerged alone. Say so (the operator seats it in a train by hand once gh
answers) and pin the record shape with a test.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* v1.91.282.0 stamp

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-10-27 21:21:50 +00:00
1 parent 392f50d47b
commit 9ccb5233f3
21 files changed
+1080 -73

No files matched your search

+15
View File
@@ -207,6 +207,21 @@ files. In practice slot 1 needs the same merge whenever the base moved after the
taken. A stamp commit with one extra byte freezes the queue for every
lane behind it.
The slot stamp is the base merge, `mergeChangelogs`, and the stamp files the slot call
names (`STAMP_FILES` in CREW.md; gstack's are `CHANGELOG.md`, `VERSION`, `package.json` and
`agents-digest/gstack-AGENTS.md`, plus the file carrying a `*_KEY_VERSION` the PR
introduced, which the restamp moves on the PR's own line, when the call names it), never a
fifth: a file the restamp lists that the call did not name is not a stamp, so the lane
leaves it out and says so in its READY. The restamp itself never
moves a line the base added since the pin's fork point, base merge committed or staged. A
pin stacked on a PR that squash-merged earlier conflicts beyond the CHANGELOG where the
parent's squash touched the pin's lines, so the lane takes its pin's bytes for those files
(`verify` checks the tree against T<i>, which already carries the parent), and
`mergeChangelogs` carries the pin's edits to the parent's now-released entry as Gate
integrity corrections in the pin's own entry. The post-merge `verify` judges the CHANGELOG
by the same rule the pre-merge one did, so an accepted head never freezes the queue after
its merge ([ship-policy.md](ship-policy.md#run-a-merge-train)).
Options the controller writes into CREW.md rather than inheriting: squash versus merge
commits; lane stamps versus controller stamps; gate-ahead (slot i+1 gates on slot i's
head while i merges); the severe race (several severe lanes told at once that the next
+76 -12
View File
@@ -486,7 +486,9 @@ missing, cancelled or on another SHA). In a merge, version files take the
prefix's side and the CHANGELOG merges by section, since the stamp at merge
rewrites both; a PR whose CHANGELOG drops or renames a released heading (or
carries a released entry's text in a section of its own) is a CHANGELOG
conflict, never merged with the damage. READY receipts and train records are
conflict, never merged with the damage, except the stacked child whose parent
was released since their merge base, which `mergeChangelogs` carries (see *A
stacked child whose parent merged earlier*, below). READY receipts and train records are
keyed on origin's GitHub owner/repo, so lanes whose origin uses another URL
form (git.capy.ai, ssh, a token URL) share the coordinator's queue.
@@ -539,7 +541,7 @@ EVAL_POLICY records it. Bisect results never turn a train green.
```bash
git fetch origin main
git merge --no-ff --no-commit origin/main # expect a CHANGELOG.md conflict only
bun --no-env-file --config=/dev/null -e "import { mergeChangelogs } from './lib/changelog-check'; import { spawnSync } from 'node:child_process'; import { writeFileSync } from 'node:fs'; const s = (n) => spawnSync('git', ['show', ':' + n + ':CHANGELOG.md'], { encoding: 'utf8', maxBuffer: 1 << 26, timeout: 30000 }).stdout; const m = mergeChangelogs(s(2), s(3), s(1)); if (!m) { console.error('CHANGELOG needs a hand merge'); process.exit(1); } writeFileSync('CHANGELOG.md', m);"
bun --no-env-file --config=/dev/null -e "import { mergeChangelogsWithNotes } from './lib/changelog-check'; import { spawnSync } from 'node:child_process'; import { writeFileSync } from 'node:fs'; const s = (n) => spawnSync('git', ['show', ':' + n + ':CHANGELOG.md'], { encoding: 'utf8', maxBuffer: 1 << 26, timeout: 30000 }).stdout; const m = mergeChangelogsWithNotes(s(2), s(3), s(1)); for (const n of m.notes) console.error('CHANGELOG: carried as a Gate integrity correction: ' + n); if (!m.text) { console.error('CHANGELOG needs a hand merge'); process.exit(1); } writeFileSync('CHANGELOG.md', m.text);"
git add CHANGELOG.md && git commit --no-edit
gstack-restamp --next --base main --pr 42 --expect-base <main sha> --expect-order <order> --allow-repo-commands
git add CHANGELOG.md VERSION package.json <release outputs> && git commit -m "v<X> stamp" && git push
@@ -547,6 +549,47 @@ git add CHANGELOG.md VERSION package.json <release outputs> && git commit -m "v<
A conflict in any file other than CHANGELOG.md means the merge isn't the train's prefix. Stop and report it. When the helper says the CHANGELOG needs a hand merge, take main's CHANGELOG unchanged and put your own entry above it. `verify` accepts the resulting head because it contains main and its tree equals T<i> modulo stamps, with its CHANGELOG judged as main's plus lines your pin added, however the conflict was resolved; the `head:` line says "T<i> with main merged in".
**The stamp commit is the base merge plus the stamp files, never a fifth.** The
restamp writes the version files, the CHANGELOG, the `release_outputs` and the
`stamp_paths` lines the branch itself added (gstack: `CHANGELOG.md`, `VERSION`,
`package.json`, `agents-digest/gstack-AGENTS.md`, and a `*_KEY_VERSION` the PR
introduced). A line the base added since the pin's fork point is the base's,
whether the base merge is committed or still staged (`stage stamp_paths` says
"N line(s) the base added since the fork point are the base's"; train 24,
2026-10-27, moved main's `DOCS_CAPS_KEY_VERSION` in both slots before this
rule). A file the restamp lists that the slot call did not name is not a stamp:
leave it out and report it. The tree receipt refuses such a move after the fact
only while the moved version is older than the base's at the build, so the
restamp, not the receipt, is what keeps the base's key in place.
**A stacked child whose parent merged earlier.** When the pin was stacked on a
PR that squash-merged in an earlier slot or train, the base merge conflicts
beyond the CHANGELOG wherever the parent's squash and the pin touched the same
lines. Take the pin's bytes for those files: `verify` checks the tree against
T<i>, the train's own merge of that pin, so the parent's content is already in
the prefix. The CHANGELOG is `mergeChangelogs`' job and needs no hand merge for
this shape: the pin's `[Unreleased]` still holds the parent's entry (as forked,
maybe edited) while main carries it under a released heading the merge base
lacks, so the parent's lines leave the child's section and each edit the child
made to them becomes a `- Correction to the <version> entry below: "<old>"
should read "<new>".` bullet under the child's own `### Gate integrity` (the
one-liner prints each carried correction). The released entry stays main's byte
for byte, and `verify` accepts those bullets as the pin's own lines because
the tool writes them. A pin that does not carry base can still be refused here
(`ours's Unreleased section holds released <version> text`): that is the
GSTA-67 shape, a released heading dropped by hand, and stays a hand merge.
**A generator change on an unstamped head.** A release output such as
`agents-digest/gstack-AGENTS.md` is stamped at the slot, and an unstamped PR
changes it the ordinary way, which is to edit the generator, regenerate at the
branch's own `VERSION` (its merge base's), and commit both. CI's Version Gate
regenerates the output at that VERSION and reads a byte-identical result as
unstamped ([what /ship reads](#ship), since 1.91.237.0); the freshness test
compares against the checkout's VERSION, so it passes before and after the
slot's restamp regenerates the file at the new version. Never revert a
regenerated output to the base's bytes to get past the gate (#3434 did, on a
gate older than that rule, and shipped a stale digest line until its merge).
**5. Coordinator: verify, merge, verify again.** For each slot in order:
```bash
@@ -558,15 +601,36 @@ gstack-ship-queue verify --pr 41 --train <nonce> # VERIFY_TRAIN: #41 merged; m
Before a merge, main's tree must equal the prefix before the PR modulo stamps
and the PR must be at its pin (stamps aside) or contain main and be T<i>
modulo stamps (its pin with main merged in; its CHANGELOG must be main's with
only lines from the pin's own CHANGELOG diff above it); after it, main's tree
must equal the prefix after it. Anything else is `QUEUE_TRAIN_TREE_MISMATCH`
(or `QUEUE_TRAIN_MOVED`). When nothing of the train has merged, that is a plain
refusal: build a new train on current main. When earlier slots already
merged, main holds a prefix no gate covered on its own, so verify freezes the
queue: the freeze goes to local state and the notes ref first, then the full
gate is dispatched on main's SHA at once (the backstop), and the
`QUEUE_TRAIN_FROZEN:` line lists the merged PRs, main's tree and the backstop
run. While frozen, ordinary trains and other trains' merges are refused
only lines from the pin's own CHANGELOG diff, or lines `mergeChangelogs`
writes for that pin against main, above it); after it, main's tree must equal
the prefix after it modulo stamps, or differ from it in the CHANGELOG alone
while being its own first parent's CHANGELOG (the main the pre-merge verify
saw, itself the prefix before modulo stamps) plus the PR's own entry under the
same rule. One CHANGELOG rule applies before and after, so a head the pre-merge
verify accepted confirms after its squash merge with the same CHANGELOG, and a
stacked child whose one `[Unreleased]` became two released sections is not a
mismatch (train 24, 2026-10-27, froze on exactly that). Anything else is
`QUEUE_TRAIN_TREE_MISMATCH` (or `QUEUE_TRAIN_MOVED`). When nothing of the train
has merged, that is a plain refusal, and a new train is built on current main. When
earlier slots already merged, main holds a prefix no gate covered on its own,
so verify freezes the queue. The freeze goes to local state and the notes ref
first, then the full gate is dispatched on main's SHA at once (the backstop),
and the `QUEUE_TRAIN_FROZEN:` line lists the merged PRs, main's tree and the
backstop run. A slot GitHub reports MERGED counts as merged in that record,
verified tree or not, so the rebuild after the repair never seats it again
(the reason says "merged on GitHub without a verified tree"); a first slot
whose squash GitHub reports MERGED but whose tree fails the post-merge check
freezes the same way, since main already holds the unverified squash. The
record comes from the one GitHub read that decided to freeze, never a second
one: a slot that read MERGED stays merged even when GitHub stops answering
before the record is written, and a slot unreadable beside a merged one is
left out of `unmerged` and named in the reason; nothing reads it again, so
the `NEXT:` after the repair omits it and the operator seats it in a train by
hand once `gh pr view` answers. When
whether a slot merged on GitHub cannot be read, verify refuses
(`QUEUE_PROOF_MISSING`, "whether #<n> merged on GitHub could not be read")
and asks to be rerun once `gh pr view` answers; an unreadable state is never
"nothing merged". While frozen, ordinary trains and other trains' merges are refused
(`QUEUE_TRAIN_FROZEN`). Admit one repair PR at its exact head:
```bash
@@ -884,7 +948,7 @@ Stages, each journaled in `.gstack/tmp/restamp-journal.json`:
1. **version**: `gstack-version-bump write --version <X>` in the staging worktree (VERSION, the manifest, npm lockfiles); skipped when `restamp_command` owns the version.
2. **release_tool**: the policy command, under the contract below.
3. **changelog**: the PR's `[Unreleased]` or old-version section moves to the top under `<X>`, above its predecessor's entry. A duplicate heading the PR introduces (`CHANGELOG_DUPLICATE_HEADING`: the headings are counted against the base's CHANGELOG at `origin/<base>` and at the fork point, so a version the base already heads twice passes as long as the PR adds no further copy) and a top entry that is not `<X>` (`CHANGELOG_TOP_MISMATCH`) are errors; the section being re-headed itself may exist only once.
4. **stamp_paths**: exact `(was X)`, `since: X` and `vX` rewrites, plus every `stamp_patterns` template, within `stamp_paths` and `stamp_files`. A quote around X in the first two forms is part of the form and is kept as written: `since: 'X'`, `since: "X"`, `(was 'X')` and `(was "X")` move like the bare ones (the shape of a quoted source row such as gbrain's `{ since: '0.60.468.0', … }`), an opened quote must close right after X (`since: '1.2.3-beta'` and `since: '1.2.3"` are not the form, in the writer and in the tree receipt alike), and `vX` already accepts a quote before the `v`. Which X moves is decided by provenance. The branch's own tentative stamp (its VERSION when it moved past base) and `--was <X>`, when given, are each rewritten on every line of every stamp file. Beyond that, only lines the branch itself added are read, by their position in the file (`git diff --unified=0` from the fork point), so a base line with the same text, or a CRLF checkout whose diff is LF-normalized, is told apart: a stamp-form mention there of a version newer than the version at the branch's fork point is a tentative stamp and moves (this is what finds a quoted row after a lost slot and a merge of the base, since the base's release is newer than the fork point), and when the branch has no stamp of its own because its VERSION equals the base's and its fork point is the base itself (a branch rebased onto the base after it reached X, or one never stamped) a mention of the base version on a line it added is its stale stamp and moves too, while the same version on a line the base already carries is the base's release and stays. The stage detail names both counts per file (`VERSION equals base X, so X moved only on lines this branch added and stayed on the base's own lines (<file>: N moved, M kept)`), so the stamp commit shows which lines moved and why. The rule reads line ownership, not meaning: a branch-added line inside `stamp_paths` that means the base's own release in a stamp form (`Requires vX or later` written on a branch forked at X) moves with it, which the `--dry-run` diff and the per-file counts show before anything is written; write such a reference outside a stamp form, or keep that file out of `stamp_paths`. `--was X` is the opposite tool: it rewrites X everywhere, the base's own lines included, and re-heads the CHANGELOG section `## [X]`. Only the repo's own version width counts, so a 3-digit dependency pin in a 4-digit repo is never a candidate.
4. **stamp_paths**: exact `(was X)`, `since: X` and `vX` rewrites, plus every `stamp_patterns` template, within `stamp_paths` and `stamp_files`. A quote around X in the first two forms is part of the form and is kept as written: `since: 'X'`, `since: "X"`, `(was 'X')` and `(was "X")` move like the bare ones (the shape of a quoted source row such as gbrain's `{ since: '0.60.468.0', … }`), an opened quote must close right after X (`since: '1.2.3-beta'` and `since: '1.2.3"` are not the form, in the writer and in the tree receipt alike), and `vX` already accepts a quote before the `v`. Which X moves is decided by provenance. The branch's own tentative stamp (its VERSION when it moved past base) and `--was <X>`, when given, are each rewritten on every line of every stamp file. Beyond that, only lines the branch itself added are read, by their position in the file (`git diff --unified=0` from the fork point), so a base line with the same text, or a CRLF checkout whose diff is LF-normalized, is told apart: a stamp-form mention there of a version newer than the version at the branch's fork point is a tentative stamp and moves (this is what finds a quoted row after a lost slot and a merge of the base, since the base's release is newer than the fork point), and when the branch has no stamp of its own because its VERSION equals the base's and its fork point is the base itself (a branch rebased onto the base after it reached X, or one never stamped) a mention of the base version on a line it added is its stale stamp and moves too, while the same version on a line the base already carries is the base's release and stays. The stage detail names both counts per file (`VERSION equals base X, so X moved only on lines this branch added and stayed on the base's own lines (<file>: N moved, M kept)`), so the stamp commit shows which lines moved and why. A line the base added since the fork point is the base's even when that diff shows it (a merge of the base on the branch, committed or still staged, brings every line the base added since the fork into the diff from the fork point, main's own release keys included): it is left out, the detail says `N line(s) the base added since the fork point are the base's, not this branch's, and stay (<file>: N)`, and a base diff git cannot run refuses the stage (`POLICY_SOURCE_UNAVAILABLE`, `cannot diff <since>..<base> for the stamp_paths stage`) rather than reading as an empty set that would move the base's lines again. The rule reads line ownership, not meaning: a branch-added line inside `stamp_paths` that means the base's own release in a stamp form (`Requires vX or later` written on a branch forked at X) moves with it, which the `--dry-run` diff and the per-file counts show before anything is written; write such a reference outside a stamp form, or keep that file out of `stamp_paths`. `--was X` is the opposite tool: it rewrites X everywhere, the base's own lines included, and re-heads the CHANGELOG section `## [X]`. Only the repo's own version width counts, so a 3-digit dependency pin in a 4-digit repo is never a candidate.
5. **mirror**: tier 2; declared and validated, reported as not run.
6. **apply**: every changed file is written to the branch only after all stages validated, each against the fingerprinted preimage; a file edited meanwhile is `RESTAMP_CONFLICT` and the applied writes roll back (rollback refuses to overwrite a file whose bytes differ from the recorded postimage).
+30 -6
View File
@@ -2653,7 +2653,11 @@ from the working directory (a bare `bun gstack-pregate`), since a branch could
then pick the user policy: run `gstack-pregate` directly or as
`bun --no-env-file`. `gstack-ship-queue ready` under a train policy fetches
`origin/<base>` first and refuses with this code, quoting git's error, when
that fetch fails.
that fetch fails. `gstack-restamp`'s `stamp_paths` stage refuses with it when
the diff that tells the base's added lines from the branch's own cannot run
(`cannot diff <since>..<base> for the stamp_paths stage (<git's first stderr
line>); nothing written`), since an empty diff would read as "the base added
nothing" and move the base's lines again.
**Fix.** `git fetch origin <base> --depth=1`, then rerun. To run against the
branch's own file deliberately, pass `--policy-from head --allow-repo-commands`;
@@ -4089,7 +4093,10 @@ gate-ahead merge could not be recomputed with `git merge-tree`. The train
commands use it too: `train forget` on a record that does not exist or on a
train still building, running or merging, `train watch` on an unknown record,
`verify --pr <n> --screen` on a run with no verdict to read (an INCOMPLETE or
unreadable run), and `ready --screen` when the scratch merge branch cannot be
unreadable run), `verify --pr <n> --train <nonce>` when main's tree mismatches
and whether a slot merged on GitHub cannot be read (so whether the queue must
freeze is unknown; it never answers "nothing merged" from an unreadable
state), and `ready --screen` when the scratch merge branch cannot be
pushed. Nothing was written unless the `QUEUE_STAMP_REFUSED:` line says
`committed=yes`.
@@ -4327,17 +4334,34 @@ the repair merges (a changed train).
**Meaning.** Main's tree is neither the train's prefix before this PR nor the
one after it, modulo stamps, so the merge would not publish what the gate
tested.
tested. After a merge, a main that differs from the prefix after the PR in
the CHANGELOG alone, while being its own first parent's CHANGELOG plus the
PR's own entry (the rule the pre-merge verify applied to the head), is not a
mismatch ([one CHANGELOG rule, before and after](ship-policy.md#run-a-merge-train)).
**Fix.** Stop merging. If earlier PRs of the train merged, the queue is now
frozen (`QUEUE_TRAIN_FROZEN`); otherwise build a new train on current main.
**Fix.** Stop merging. If a PR of the train merged (verified, or reported
MERGED by GitHub without a verified tree, slot 1's own squash included), the
queue is now frozen (`QUEUE_TRAIN_FROZEN`); otherwise build a new train on
current main. When whether a slot merged on GitHub cannot be read, verify
refuses with `QUEUE_PROOF_MISSING` instead of either answer; rerun it once
`gh pr view` answers.
<a id="queue-train-frozen"></a>
### `QUEUE_TRAIN_FROZEN`
**Meaning.** A train stopped partway through publication, so main holds a
prefix no gate covered on its own. The `QUEUE_TRAIN_FROZEN:` line lists the
merged PRs, main's tree and the backstop run dispatched on main's SHA. The
merged PRs (a slot GitHub reports MERGED counts as merged, verified tree or
not, and the reason then says `merged on GitHub without a verified tree`, so
the repair's rebuild never seats it again), main's tree and the backstop run
dispatched on main's SHA. The record is built from the one GitHub read that
decided to freeze: a slot that read MERGED stays merged even when `gh pr view`
stops answering a moment later, and a slot whose state could not be read when
another slot had merged is left out of `unmerged` and named in the reason
(`#<n> unreadable on GitHub, left out of unmerged; seat it in a train by hand
once gh pr view answers`), so the rebuild after the repair never seats a PR
that may already be in; nothing reads that slot again, so the `NEXT:` after
the repair omits it and the operator seats it by hand. The
freeze lives in local state and on `refs/notes/gstack-trains`. Ordinary
trains and other trains' merges are refused until a repair lands.