diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68538aa8b..8a298b1e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,7 @@
### Added
- **`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, 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.
- **`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` (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 `** (`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.
- **/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`.
diff --git a/docs/ship-policy.md b/docs/ship-policy.md
index 749dcfa76..be2e10152 100644
--- a/docs/ship-policy.md
+++ b/docs/ship-policy.md
@@ -170,8 +170,12 @@ ci: green at (passed=31 failed=0 pending=0 skipped=2)
PREGATE: pass tree= head= checks=6
gate: command="" full_e2e=no gate-ahead=none
SHIP_RECEIPT: pr=42 head= …
+READY_RECEIPT: /ship-queue/ready//42-.json (notes pushed to origin refs/notes/gstack-trains)
```
+Under `changelog_fragments`, a `READY_RECEIPT:` line follows, written only when
+every check above passed; `land` refuses a pin without one.
+
CI's Version Gate (`scripts/compare-pr-version.ts`) accepts that head as
unstamped when `VERSION` and `package.json`'s version match its merge base, the
CHANGELOG change is confined to `[Unreleased]`, and any changed release output
@@ -1217,7 +1221,10 @@ stamp commit. Steps, each journaled in
`.gstack/tmp/ship-queue/land/.json` and resumable by rerunning the same
command:
-1. **Refuse** a fork head, a closed PR, a PR whose base is another branch, a /ship receipt that is missing or
+1. **Refuse** a fork head, a closed PR, a PR whose base is another branch, a
+ pin without a READY receipt (`gstack-ship-queue ready --pr ` records one
+ on `refs/notes/gstack-trains` once its pregate, full_e2e and receipt checks
+ pass; land checks nothing weaker), a /ship receipt that is missing or
bound to another head (`QUEUE_RECEIPT_MISSING`, `QUEUE_RECEIPT_STALE`), and
required checks that are not green at the pin.
2. **Lock.** Take the repository land lock, `refs/notes/gstack-land` (its own
diff --git a/lib/ship-queue-land.ts b/lib/ship-queue-land.ts
index 2a9eb8cd8..63e04aad8 100644
--- a/lib/ship-queue-land.ts
+++ b/lib/ship-queue-land.ts
@@ -21,6 +21,7 @@ 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 { bindShippedReceipt } from './ship-receipt-depth';
+import { loadReady, rebuildFromNotes, repoIdentity } from './ship-queue-train-record';
export const LAND_LOCK_REF = 'refs/notes/gstack-land';
export const MAX_RESTAMPS = 2;
@@ -191,6 +192,10 @@ function buildStamp(ctx: QueueContext, policy: ShipPolicy, o: { pin: string; bas
}
}
if (!git(wt, ['commit', '-q', '--no-verify', '--allow-empty', '-m', `land #${o.pr}: merge origin/${ctx.base}`]).ok) return { code: 'QUEUE_LAND_CONFLICT', message: 'cannot commit the merge in the scratch worktree' };
+ const exact = [versionRel, ...MANIFESTS, ...(policy.changelog ? [policy.changelog] : [])];
+ const linked = git(wt, ['ls-files', '-s', '-z']).out.split('\0').filter(rec => rec.startsWith('120000 ')).map(rec => rec.slice(rec.indexOf('\t') + 1))
+ .find(rel => exact.includes(rel) || matchesAny([...policy.release_outputs, ...policy.stamp_paths], rel)) ?? exact.map(rel => linkedSegment(wt, rel)).find(Boolean);
+ if (linked) return { code: 'QUEUE_LAND_CONFLICT', message: `${linked} is a symlink in the merged tree; the merge step writes release files only inside the repository` };
const r = restamp({ repoRoot: wt, base: ctx.base, next: true, policyFrom: 'base', allowRepoCommands: o.allowRepoCommands, dryRun: false, now: o.now, env: process.env });
if (r.status !== 'stamped' && r.status !== 'noop') return { code: r.code ?? 'QUEUE_LAND_CONFLICT', message: `restamp ${r.status}: ${r.message ?? ''}`, fix: r.fix };
lines.push(...r.stages.filter(s => s.status !== 'pending').map(s => `stage ${s.name}: ${s.status}${s.detail ? ` (${s.detail})` : ''}`));
@@ -295,14 +300,21 @@ function repair(ctx: QueueContext, policy: ShipPolicy, j: LandJournal | null, fi
if (!git(wt, ['commit', '-q', '--no-verify', '-m', `land: release entry for the merge outside land beside #${j.pr}`]).ok) return fail(lines, { code: 'QUEUE_NOT_READY', message: 'cannot commit the repair' }, run);
const head = git(wt, ['rev-parse', 'HEAD']).out;
const push = git(root, ['push', '-q', `--force-with-lease=refs/heads/${branch}:`, 'origin', `${head}:refs/heads/${branch}`]);
- if (!push.ok) lines.push(`branch: ${branch} already on origin (an earlier --repair pushed it); opening or finding its PR`);
+ let pushed = head;
+ if (!push.ok) {
+ git(root, ['fetch', '-q', 'origin', `refs/heads/${branch}`]);
+ const remote = git(root, ['log', '-1', '--format=%H%n%s', 'FETCH_HEAD']).out.split('\n');
+ if (!isSha(remote[0]) || remote[1] !== `land: release entry for the merge outside land beside #${j.pr}`) return fail(lines, { code: 'QUEUE_TRAIN_MOVED', message: `cannot push ${branch}, and origin holds no repair commit there: ${push.err.split('\n')[0]}` }, run);
+ pushed = remote[0]!;
+ lines.push(`branch: ${branch} already on origin at ${pushed.slice(0, 12)} (an earlier --repair pushed it); opening or finding its PR`);
+ }
const pr = ctx.gh(['pr', 'create', '--base', ctx.base, '--head', branch, '--title', `Release entry for the merge outside land beside #${j.pr}`, '--body', `QUEUE_LAND_DRIFT repair (docs/troubleshooting.md#queue-land-drift). Outside commits: ${outside.join(', ') || 'unknown'}.`]);
const n = /\/pull\/(\d+)/.exec(`${pr.out}\n${pr.err ?? ''}`)?.[1];
if (!n) return fail(lines, { code: 'QUEUE_NOT_READY', message: `gh pr create failed: ${(pr.err ?? pr.out).trim().slice(0, 160)}` }, run);
j.repair_pr = n; writeJsonAtomic(file, j);
const held = readLock(root);
if (held.lock?.pr === j.pr && held.lock.phase === 'drift') writeLock(root, held.sha, { ...held.lock, repair_pr: n });
- return { ok: true, run, lines: [...lines, `REPAIR_PR: #${n} ${branch} ${head}`, `NEXT: run /ship on #${n}, then gstack-ship-queue land --pr ${n} (it takes over #${j.pr}'s drift lock and releases it)`] };
+ return { ok: true, run, lines: [...lines, `REPAIR_PR: #${n} ${branch} ${pushed}`, `NEXT: run /ship on #${n}, then gstack-ship-queue land --pr ${n} (it takes over #${j.pr}'s drift lock and releases it)`] };
} finally {
git(root, ['worktree', 'remove', '--force', wt]);
fs.rmSync(wt, { recursive: true, force: true });
@@ -336,6 +348,10 @@ export function land(ctx: QueueContext, o: LandOptions): StepOutput {
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 (!policy.changelog_fragments) return fail(lines, { code: 'QUEUE_NOT_READY', message: 'the base policy has changelog_fragments off; land merges only under the key, where ready records the READY receipt it checks' }, run);
+ const keys = repoIdentity(root).keys;
+ if (!loadReady(keys, o.pr, pin)) rebuildFromNotes(root, ctx.remote ?? 'origin');
+ if (!loadReady(keys, o.pr, pin)) return fail(lines, { code: 'QUEUE_NOT_READY', message: `no READY receipt for #${o.pr} at the pin ${pin.slice(0, 12)}; ready checks the pregate and full_e2e evidence land merges on`, fix: `run gstack-ship-queue ready --pr ${o.pr}` }, run);
if (!j) {
const ci = ciAtHead(ctx, pin);
lines.push(`CI: ${ci.line}`);
diff --git a/lib/ship-queue.ts b/lib/ship-queue.ts
index e4588ea13..68ae3cf26 100644
--- a/lib/ship-queue.ts
+++ b/lib/ship-queue.ts
@@ -39,6 +39,7 @@ import {
type AttemptRecord, type QueueFailure,
} from './ship-queue-gate';
import { wtreeFingerprint } from './script-invocation';
+import { nowIso, persist, readyPath, repoIdentity } from './ship-queue-train-record';
import { buildReceipt, type TreeReceipt } from './tree-receipt';
export type Gh = (args: string[]) => { ok: boolean; out: string; err?: string };
@@ -277,6 +278,12 @@ export function ready(ctx: QueueContext, o: { pr: string; pregate?: string; gate
if (!pregateOk) return fail({ code: 'QUEUE_NOT_READY', message: `the pregate result is not a current pass: ${pregateLine}` }, lines, run);
if (gate && !gate.ok) return fail({ code: 'QUEUE_GATE_MISSING', message: `full_e2e is set and no passing gate is bound to ${head.slice(0, 12)}: ${gate.line}` }, lines, run);
if (!receipt.ok) return fail({ code: receipt.code, message: receipt.message }, lines, run);
+ if (!policy.policy.train && policy.policy.changelog_fragments) {
+ const repo = repoIdentity(ctx.repoRoot);
+ const now = nowIso();
+ const p = persist(ctx.repoRoot, [{ schema: 1, kind: 'ready', repo_key: repo.key, pr: o.pr, head, base_sha: baseSha, ready_at: now, screen: { choice: 'no-screen', policy: null, nonce: null, run: null, merge_sha: null }, updated_at: now }], { remote: ctx.remote ?? 'origin' });
+ lines.push(`READY_RECEIPT: ${readyPath(repo.key, o.pr, head)} (${p.detail})`);
+ }
return { ok: true, lines, run };
}
diff --git a/test/ship-queue-land.test.ts b/test/ship-queue-land.test.ts
index a54795fe4..4d5cc9b71 100644
--- a/test/ship-queue-land.test.ts
+++ b/test/ship-queue-land.test.ts
@@ -15,6 +15,7 @@ import { BASE_POLICY, cleanup, makeFixtureRepo, readFile, runBin, type FixtureRe
import { shippedBody } from './helpers/ship-queue-train-fixture';
import { resolveReleaseConflicts } from '../lib/ship-queue-land';
import { mergePolicy } from '../lib/ship-policy';
+import { persist, repoIdentity } from '../lib/ship-queue-train-record';
const POLICY = { ...BASE_POLICY, release_tool: null, release_outputs: [], changelog_fragments: { dir: 'changelog.d' }, min_gstack_version: '1.92.0.0' };
const repos: FixtureRepo[] = [];
@@ -58,7 +59,17 @@ function setup(policy: Record = POLICY): Fx {
}
/** A PR branch from origin/main, pushed, with a fragment (or none), and a /ship receipt for its head in the shim. */
-function openPr(fx: Fx, pr: string, opts: { fragment?: string | null; edit?: (work: string) => void; shipped?: boolean } = {}): string {
+/** The READY receipt `gstack-ship-queue ready` records (a note on origin refs/notes/gstack-trains), written into the fixture's own state root. */
+function markReady(fx: Fx, pr: string, head: string): void {
+ const prior = process.env.GSTACK_STATE_ROOT;
+ process.env.GSTACK_STATE_ROOT = path.join(fx.repo.root, 'ready-state');
+ try {
+ const now = new Date().toISOString();
+ persist(fx.repo.work, [{ schema: 1, kind: 'ready', repo_key: repoIdentity(fx.repo.work).key, pr, head, base_sha: fx.repo.git('rev-parse', 'origin/main'), ready_at: now, screen: { choice: 'no-screen', policy: null, nonce: null, run: null, merge_sha: null }, updated_at: now }]);
+ } finally { if (prior === undefined) delete process.env.GSTACK_STATE_ROOT; else process.env.GSTACK_STATE_ROOT = prior; }
+}
+
+function openPr(fx: Fx, pr: string, opts: { fragment?: string | null; edit?: (work: string) => void; shipped?: boolean; ready?: boolean } = {}): string {
const { repo } = fx;
repo.git('checkout', '-q', 'origin/main', '-B', 'pr-' + pr);
fs.writeFileSync(path.join(repo.work, 'pr-' + pr + '.txt'), pr + '\n');
@@ -72,6 +83,7 @@ function openPr(fx: Fx, pr: string, opts: { fragment?: string | null; edit?: (wo
repo.git('push', '-q', '-f', 'origin', 'HEAD:refs/heads/pr-' + pr);
const head = repo.git('rev-parse', 'HEAD');
fx.write(d => { d.prs[pr] = { state: 'OPEN', headRefName: 'pr-' + pr, isCrossRepository: false, title: 'PR ' + pr + ' title', body: opts.shipped === false ? '' : shippedBody(pr, head) }; });
+ if (opts.ready !== false) markReady(fx, pr, head);
repo.git('checkout', '-q', '--detach', 'origin/main');
return head;
}
@@ -131,6 +143,7 @@ describe('land: fold, push, merge', () => {
fx.repo.git('push', '-q', '-f', 'origin', 'HEAD:refs/heads/pr-3');
const head = fx.repo.git('rev-parse', 'HEAD');
fx.write(d => { d.prs['3'] = { state: 'OPEN', headRefName: 'pr-3', isCrossRepository: false, title: 'legacy', body: shippedBody('3', head) }; });
+ markReady(fx, '3', head);
const r = land(fx, ['--pr', '3']);
expect(r.err).toBe('');
expect(r.out).toMatch(/^legacy: the pin stamped 1\.2\.9\.0 itself/m);
@@ -236,6 +249,7 @@ describe('land: lock, races and refusals', () => {
expect(show(fx, n[2] + ':changelog.d/repair-9.md')).toBe('- Changes merged outside land beside #9: outside merge');
expect(land(fx, ['--pr', n[1]!]).err).toContain('(QUEUE_RECEIPT_MISSING)');
fx.write(d => { d.prs[n[1]!].body = shippedBody(n[1]!, n[2]!); });
+ markReady(fx, n[1]!, n[2]!);
const pushed = land(fx, ['--pr', n[1]!]);
expect(pushed.out).toContain("LOCK: taking over #9's drift lock for its repair");
const merged = land(fx, ['--pr', n[1]!]);
@@ -516,6 +530,7 @@ describe('land: one pass, base, lock and history', () => {
fx.repo.git('push', '-q', '-f', 'origin', 'HEAD:refs/heads/pr-11');
const head = fx.repo.git('rev-parse', 'HEAD');
fx.write(d => { d.prs['11'] = { state: 'OPEN', headRefName: 'pr-11', isCrossRepository: false, title: 'child', body: shippedBody('11', head) }; });
+ markReady(fx, '11', head);
fx.repo.git('checkout', '-q', '--detach', 'origin/main');
const r = land(fx, ['--pr', '11']);
expect(r.status).toBe(EXIT.refused);
@@ -592,6 +607,7 @@ describe('land: merged-after-error, live locks, repair recovery, legacy release
const n = /REPAIR_PR: #(\d+) gstack-land-repair\/23/.exec(rep.out)!;
const head = remote(fx, 'gstack-land-repair/23');
fx.write(d => { d.prs[n[1]!].body = shippedBody(n[1]!, head); });
+ markReady(fx, n[1]!, head);
fs.rmSync(path.join(fx.repo.work, '.gstack', 'tmp', 'ship-queue', 'land', '23.json'));
expect(land(fx, ['--pr', n[1]!]).out).toContain("LOCK: taking over #23's drift lock for its repair");
const merged = land(fx, ['--pr', n[1]!]);
@@ -610,6 +626,17 @@ describe('land: merged-after-error, live locks, repair recovery, legacy release
expect(fs.readFileSync(outside, 'utf8')).toBe('keep\n');
}, 90_000);
+ // Value: protects=restamp never folds fragments through a PR-controlled symlinked CHANGELOG; fails_when=the fold rewrites a file outside the checkout, even in --dry-run; why_new=the legacy guard runs only when the pin bumped VERSION; seam=none; no_claim=Windows symlink semantics
+ test('a pin whose CHANGELOG is a symlink refuses before restamp and leaves the target untouched', () => {
+ const fx = setup();
+ const outside = path.join(fx.repo.root, 'outside-changelog.md');
+ fs.writeFileSync(outside, '# Changelog\n\n## [1.2.3.0] - 2026-10-01\n\n- base release\n');
+ openPr(fx, '27', { edit: w => { fs.rmSync(path.join(w, 'CHANGELOG.md')); fs.symlinkSync(outside, path.join(w, 'CHANGELOG.md')); } });
+ const r = land(fx, ['--pr', '27', '--dry-run']);
+ expect(r.err).toMatch(/CHANGELOG\.md is a symlink in the merged tree.*\(QUEUE_LAND_CONFLICT\)/);
+ expect(fs.readFileSync(outside, 'utf8')).toBe('# Changelog\n\n## [1.2.3.0] - 2026-10-01\n\n- base release\n');
+ }, 90_000);
+
// Value: protects=a legacy pin in a repo whose version source is package.json keeps its dependency and script edits; fails_when=the whole manifest is reset to the merge base; why_new=every case used a VERSION file; seam=none; no_claim=lockfile shapes beyond package.json
test('version source package.json: a legacy pin keeps its dependency and script edits', () => {
const fx = setup();
@@ -631,3 +658,21 @@ describe('land: merged-after-error, live locks, repair recovery, legacy release
expect(pkg.version).toBe('1.2.4');
}, 90_000);
});
+
+// GStack Minder ruling (2026-10-29, GSTA-154): standalone land checks the evidence ready enforces.
+describe('land: the READY receipt', () => {
+ // Value: protects=land refuses a pin ready never accepted (pregate, full_e2e), before any lock or push; fails_when=land merges a head that skipped ready; why_new=every other case marks its PR ready; seam=none; no_claim=what ready itself checks (its own tests)
+ test('a pin with a /ship receipt but no READY receipt refuses, naming the ready command; nothing is locked or pushed', () => {
+ const fx = setup();
+ const pin = openPr(fx, '28', { ready: false });
+ for (const args of [['--pr', '28'], ['--pr', '28', '--dry-run']]) {
+ const r = land(fx, args);
+ expect(r.status).toBe(EXIT.refused);
+ expect(r.err).toMatch(/no READY receipt for #28 at the pin [0-9a-f]{12}.*fix: run gstack-ship-queue ready --pr 28 \(QUEUE_NOT_READY\)/);
+ }
+ expect(remote(fx, 'pr-28')).toBe(pin);
+ expect(fx.repo.git('ls-remote', 'origin', 'refs/notes/gstack-land')).toBe('');
+ markReady(fx, '28', pin);
+ expect(land(fx, ['--pr', '28']).out).toContain('PUSHED: ');
+ }, 90_000);
+});
diff --git a/test/ship-queue-train.test.ts b/test/ship-queue-train.test.ts
index 15e24c277..0926bc00d 100644
--- a/test/ship-queue-train.test.ts
+++ b/test/ship-queue-train.test.ts
@@ -108,6 +108,23 @@ describe('the policy gate', () => {
expect(flagged.err).toContain('--screen/--no-screen/--broad ignored: the base policy declares no train block');
expect(fs.existsSync(path.join(fx.state, 'ship-queue'))).toBe(false);
}, 120_000);
+
+ // Value: protects=under changelog_fragments ready records the READY receipt land checks, only after every ready check passed; fails_when=land has no proof of the pregate; why_new=the case above covers the key off; seam=none; no_claim=the notes push to a real GitHub remote
+ test('under changelog_fragments ready records a READY receipt; a failed pregate records none', () => {
+ const fx = make({ ...BASE_POLICY, changelog_fragments: { dir: 'changelog.d' }, min_gstack_version: '1.92.0.0' });
+ const head = openPr(fx, '1');
+ fx.write(d => { d.ci[head] = [{ status: 'completed', conclusion: 'success' }]; });
+ const tree = runBin(fx.repo, 'gstack-wtree', []).out.trim();
+ const pregate = path.join(fx.repo.root, 'pregate.json');
+ fs.writeFileSync(pregate, JSON.stringify({ schema_version: 1, tree, head, verdict: 'fail', checks: [{ id: 'unit', status: 'fail' }] }));
+ expect(queue(fx, ['ready', '--pr', '1', '--pregate', pregate]).status).toBe(EXIT.refused);
+ expect(fs.existsSync(path.join(fx.state, 'ship-queue'))).toBe(false);
+ fs.writeFileSync(pregate, JSON.stringify({ schema_version: 1, tree, head, verdict: 'pass', checks: [{ id: 'unit', status: 'pass' }] }));
+ const ok = queue(fx, ['ready', '--pr', '1', '--pregate', pregate]);
+ expect(ok.status).toBe(EXIT.ok);
+ expect(ok.out).toMatch(new RegExp(`^READY_RECEIPT: .*ship-queue/ready/[^/]+/1-${head}\\.json \\(notes pushed to origin refs/notes/gstack-trains\\)$`, 'm'));
+ expect(fx.repo.git('ls-remote', 'origin', 'refs/notes/gstack-trains')).not.toBe('');
+ }, 120_000);
});
describe('ready under a train policy: the screen choice', () => {