From 45618dfaadef8a64fa0c467b40c90841257c565e Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 31 Oct 2026 18:58:15 +0000 Subject: [PATCH] ship-policy: merge trains carry up to 5 READY PRs (train.max_size 3 -> 5) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- .gstack/ship-policy.json | 2 +- changelog.d/capy-gsta199-train-max5.md | 4 ++++ docs/ship-policy.md | 2 +- test/gstack-ship-policy.test.ts | 11 ++++++----- 4 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 changelog.d/capy-gsta199-train-max5.md diff --git a/.gstack/ship-policy.json b/.gstack/ship-policy.json index ad1b381fd..2ae73849b 100644 --- a/.gstack/ship-policy.json +++ b/.gstack/ship-policy.json @@ -13,7 +13,7 @@ "stamp_sensitive_gates": ["tests"], "pr_count": { "default_pr_count": 1, "split_requires_owner": true }, "maintainers": ["garrytan"], - "train": { "gate_workflow": "evals.yml", "free_workflow": "free-tests.yml", "full_gate_check": "gstack/full-gate", "record_artifact": "gstack-train-record", "max_size": 3, "fill_hours": 1, "screen_selection": "gstack-paid", "trial_reservation_usd": 1.5 }, + "train": { "gate_workflow": "evals.yml", "free_workflow": "free-tests.yml", "full_gate_check": "gstack/full-gate", "record_artifact": "gstack-train-record", "max_size": 5, "fill_hours": 1, "screen_selection": "gstack-paid", "trial_reservation_usd": 1.5 }, "changelog_fragments": { "dir": "changelog.d", "required_checks": ["free-tests", "windows-free-tests", "Check VERSION is not stale vs queue", "quality", "check-freshness", "actionlint"] }, "min_gstack_version": "1.91.340.0" } diff --git a/changelog.d/capy-gsta199-train-max5.md b/changelog.d/capy-gsta199-train-max5.md new file mode 100644 index 000000000..bafa1859e --- /dev/null +++ b/changelog.d/capy-gsta199-train-max5.md @@ -0,0 +1,4 @@ +**A merge train now carries up to five READY PRs instead of three.** That means fewer full gates for the same number of PRs. A short train still leaves one hour after its oldest READY. + +### Changed +- `.gstack/ship-policy.json`: `train.max_size` goes from 3 to 5 (Garry, 2026-10-31). `fill_hours` stays at 1. `test/gstack-ship-policy.test.ts` pins the new value. Four READY PRs wait for the hour, and the fifth fills the train. The example block in docs/ship-policy.md, which shows gstack's own policy, now reads 5. diff --git a/docs/ship-policy.md b/docs/ship-policy.md index 0f14a3409..0c4258ddd 100644 --- a/docs/ship-policy.md +++ b/docs/ship-policy.md @@ -351,7 +351,7 @@ warning on stderr). A block for gstack itself would read: "train": { "gate_workflow": "evals.yml", "free_workflow": "free-tests.yml", "full_gate_check": "gstack/full-gate", "record_artifact": "gstack-train-record", - "max_size": 3, "fill_hours": 1, "screen_selection": "gstack-paid", + "max_size": 5, "fill_hours": 1, "screen_selection": "gstack-paid", "trial_reservation_usd": 1.5 }, "min_gstack_version": "" diff --git a/test/gstack-ship-policy.test.ts b/test/gstack-ship-policy.test.ts index db0f9a97b..025b1bb57 100644 --- a/test/gstack-ship-policy.test.ts +++ b/test/gstack-ship-policy.test.ts @@ -162,16 +162,17 @@ describe("gstack's own .gstack/ship-policy.json", () => { expect(raw.changelog_fragments.required_checks.filter((n: string) => !jobs.has(n))).toEqual([]); }); - // Value: protects=the fill rule the coordinator relies on: a train dispatches at 3 READY PRs or one hour after the first READY, whichever comes first - // (2026-10-26: 5 PRs / 3 hours left READY PRs waiting most of a day); fails_when=the live block drifts back to a larger or slower fill; why_new=the values were unpinned; seam=the policy file; no_claim=fillDue's arithmetic (ship-queue-train-logic) - test('fills a train at 3 READY PRs or one hour after the first READY, whichever comes first', () => { + // Value: protects=the fill rule the coordinator relies on: a train dispatches at 5 READY PRs or one hour after the first READY, whichever comes first + // (2026-10-26: 5 PRs / 3 hours left READY PRs waiting most of a day, so the fill went to 3 PRs / 1 hour; 2026-10-31: Garry's B1 raised the size back to 5, keeping the 1-hour fill); fails_when=the live block drifts back to a larger or slower fill; why_new=the values were unpinned; seam=the policy file; no_claim=fillDue's arithmetic (ship-queue-train-logic) + test('fills a train at 5 READY PRs or one hour after the first READY, whichever comes first', () => { const raw = JSON.parse(fs.readFileSync(path.join(import.meta.dir, '..', '.gstack', 'ship-policy.json'), 'utf8')); - expect([raw.train.max_size, raw.train.fill_hours]).toEqual([3, 1]); + expect([raw.train.max_size, raw.train.fill_hours]).toEqual([5, 1]); const first = '2026-10-26T09:00:00.000Z'; const slots = (n: number) => Array.from({ length: n }, (_, i) => ({ pr: i + 1, ready_at: new Date(Date.parse(first) + i * 60_000).toISOString() })) as any; expect(fillDue(slots(2), raw.train, '2026-10-26T09:59:00Z', false)).toBe('2026-10-26T10:00:00.000Z'); expect(fillDue(slots(2), raw.train, '2026-10-26T10:00:00Z', false)).toBeNull(); - expect(fillDue(slots(3), raw.train, '2026-10-26T09:05:00Z', false)).toBeNull(); + expect(fillDue(slots(4), raw.train, '2026-10-26T09:05:00Z', false)).toBe('2026-10-26T10:00:00.000Z'); + expect(fillDue(slots(5), raw.train, '2026-10-26T09:05:00Z', false)).toBeNull(); }); });