From 35a676ff68b88619308591727ace7ac733650841 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 1 Nov 2026 15:36:31 -0800 Subject: [PATCH] v1.92.47.0 fix(ship): /ship Step 5 runs test lanes one at a time on 4 cores or fewer, with --lanes serial|parallel (GST-851) (#3821) * fix(ship-preflight): run Step 5 lanes one at a time on 4 cores or fewer, with --lanes serial|parallel (GST-851) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * changelog: GST-851 fragment Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(ship): Step 5 says lanes run one at a time on 4 or fewer cores and names --lanes (GST-851 docs audit) 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> --- CHANGELOG.md | 7 ++++ VERSION | 2 +- agents-digest/gstack-AGENTS.md | 2 +- lib/ship-bin-common.ts | 34 +++++++++++---- lib/ship-preflight.ts | 16 ++++--- package.json | 2 +- ship/sections/tests.md | 5 ++- ship/sections/tests.md.tmpl | 5 ++- test/ship-preflight.test.ts | 42 +++++++++++++++++++ .../ship/gst-851-serial-lanes.json | 4 ++ 10 files changed, 100 insertions(+), 19 deletions(-) create mode 100644 test/size-ledger/ship/gst-851-serial-lanes.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 2619a01d1..dab48b0c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.92.47.0] - 2026-11-01 + +- **`/ship` Step 5 runs its test lanes one at a time on machines with 4 or fewer cores, and `--lanes serial|parallel` picks the mode by hand** (`gstack-ship-preflight`, GST-851). Preflight used to start every `--lane` at once. On garrytan/gstack-evals#115 six lanes (a 1,451-test bun suite, tsc, a Docker tier and an arm tarball trial) started within 60 ms of each other on a 4-core, 16 GB machine, and running lanes together has taken machines down before (gstack-evals #101, #106, #107). Now, with no flag, preflight runs one lane at a time when `os.availableParallelism()` is 4 or less and all of them at once above that. `--lanes serial` or `--lanes parallel` overrides the default, and any other value, or `--lanes` with no value, is a usage error (exit 2). A new `LANES: serial|parallel (--lanes| cores)` line before the `LANE` lines says which mode ran and why. Each lane still prints its own `LANE` line with its own `tests_ran` count, in the order the lanes were given. Serial runs take the sum of the lane times instead of the longest one. There is no ship-policy key for this: a new key needs key versioning and a `min_gstack_version` bump, and the core-count default already covers the 4-core case. + +### Tests +- `test/ship-preflight.test.ts`: `--lanes serial` runs lanes one after another and keeps each lane's count; `--lanes parallel` keeps lane order when a later lane finishes first; an unflagged run names the core count; a bad or missing `--lanes` value is a usage error; and the core-count default is serial at 4 cores or fewer. + ## [1.92.46.0] - 2026-11-01 **brain-sync no longer publishes a synced JSON file that doesn't parse, and the profile and doctor now say when one is broken.** When two machines edit `developer-profile.json` before syncing, git's merge can leave conflict markers in it. brain-sync committed and pushed that file, so every machine got a profile that no longer parsed, while `gstack-developer-profile --profile` and `gstack-doctor` both reported everything ok (#3763). diff --git a/VERSION b/VERSION index cbd266e1d..f0f525626 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.92.46.0 +1.92.47.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 7d20d84f4..f4e17a48a 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.92.46.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.92.47.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/lib/ship-bin-common.ts b/lib/ship-bin-common.ts index 2702f251f..0bb6e6b05 100644 --- a/lib/ship-bin-common.ts +++ b/lib/ship-bin-common.ts @@ -7,6 +7,7 @@ */ import { spawn, spawnSync } from 'node:child_process'; import * as fs from 'node:fs'; +import * as os from 'node:os'; import * as path from 'node:path'; import { fragmentSlug } from './changelog-fragments'; import { parseUnifiedDiff, stampOnlyLine, versionAtRef } from './restamp'; @@ -150,13 +151,32 @@ export class Report { } } -/** Run each lane through gstack-evidence run in parallel and read its recorded line. */ -export async function runLanes(cwd: string, list: { label: string; command: string }[]): Promise<{ label: string; exit: number | null; testsRan: string; log: string }[]> { - return Promise.all(list.map(async l => { - const r = await binAsync(cwd, 'gstack-evidence', ['run', '--label', l.label, '--', l.command]); - const m = /gstack-evidence: recorded label=\S+ exit=(\S+) tests_ran=(\S+) log=(\S+)/.exec(r.stderr); - return { label: l.label, exit: r.status, testsRan: m?.[2] ?? 'unknown', log: m?.[3] ?? '-' }; - })); +/** + * How many lanes run at once (GST-851): `--lanes serial` one, `--lanes parallel` all; unset, one on a machine with 4 or fewer + * cores (six lanes at once took 4-core machines down on gstack-evals) and all above that. Any other value is a usage error. + */ +export function laneConcurrency(mode: string | undefined, cpus = os.availableParallelism()): number | string { + if (mode === 'serial') return 1; + if (mode === 'parallel') return Infinity; + if (mode !== undefined) return `--lanes ${mode} is not serial or parallel`; + return cpus <= 4 ? 1 : Infinity; +} + +/** Run each lane through gstack-evidence run, at most `concurrency` at a time, and read its recorded line; results keep the lanes' order. */ +export async function runLanes(cwd: string, list: { label: string; command: string }[], concurrency = Infinity): Promise<{ label: string; exit: number | null; testsRan: string; log: string }[]> { + const out: { label: string; exit: number | null; testsRan: string; log: string }[] = []; + let next = 0; + const worker = async () => { + while (next < list.length) { + const i = next++; + const l = list[i]!; + const r = await binAsync(cwd, 'gstack-evidence', ['run', '--label', l.label, '--', l.command]); + const m = /gstack-evidence: recorded label=\S+ exit=(\S+) tests_ran=(\S+) log=(\S+)/.exec(r.stderr); + out[i] = { label: l.label, exit: r.status, testsRan: m?.[2] ?? 'unknown', log: m?.[3] ?? '-' }; + } + }; + await Promise.all(Array.from({ length: Math.max(1, Math.min(concurrency, list.length)) }, worker)); + return out; } /** Step 5's lane verdict: a lane passes on its runner count, never exit 0 alone. */ diff --git a/lib/ship-preflight.ts b/lib/ship-preflight.ts index ed1f48f50..519ee5475 100644 --- a/lib/ship-preflight.ts +++ b/lib/ship-preflight.ts @@ -6,22 +6,24 @@ * agent (a merge conflict, a failing or zero-run lane, the carry route, the Apple target). */ import * as fs from 'node:fs'; +import * as os from 'node:os'; import * as path from 'node:path'; import { classifyDiff } from './review-depth'; import { audit } from './ship-audit'; -import { bin, firstLine, flagReader, gh, git, hostedOrigin, laneLine, lanes, Report, runLanes, shipPolicy, short } from './ship-bin-common'; +import { bin, firstLine, flagReader, gh, git, hostedOrigin, laneConcurrency, laneLine, lanes, Report, runLanes, shipPolicy, short } from './ship-bin-common'; export const PREFLIGHT_HELP = `gstack-ship-preflight — /ship Steps 1-3 and Step 5's pregate and lanes in one call (ship/SKILL.md) gstack-ship-preflight --base [--lane