From dac71371a29256835c2f436b734c8ff9e1d26674 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 1 Nov 2026 19:01:48 -0800 Subject: [PATCH 1/8] v1.92.52.0 ship: the parent runs Step 9.2.1's QA probes; the pass refuses a QA reader (GST-450) (#3686) * fix(ship): the parent runs Step 9.2.1 exploratory QA itself, as its section says; the pass program refuses a QA extra (GST-450) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * ship driver: trim the 9.2.1 lines so the #3670 merge stays under 12 KB 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> * fix: pre-landing review fixes (Step 11) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (pass 2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: GST-450 changelog says the parent ran the probes after the refusal Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(review-policy): the pass stage holds audit readers only; a QA extra is refused (GST-450) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * changelog: the Capy skill lists Steps 7, 8 and 14.5 as extras after the merge of main 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 | 8 +++++++ VERSION | 2 +- agents-digest/gstack-AGENTS.md | 2 +- capy/skill/gstack/SKILL.md | 2 +- docs/review-policy.md | 5 ++-- lib/review-pass.ts | 10 ++++++-- package.json | 2 +- ship/DRIVER.md | 10 ++++---- ship/DRIVER.md.tmpl | 10 ++++---- test/review-pass.test.ts | 42 ++++++++++++++++++++++++++-------- test/ship-driver.test.ts | 25 ++++++++++++++++++-- 11 files changed, 89 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b195eea2..bbb8e5a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.92.52.0] - 2026-11-02 + +**/ship on Capy runs Exploratory QA again.** Since v1.91.345.0 (#3648) the driver sent Step 9.2.1 out as a QA reader subagent, but the section says only the parent runs report-only discovery, so the reader refused on every pass ("parent-only") and the parent ran the probes afterward anyway, an extra reader call each pass (the C4-36 and C5-36 replays). + +### Fixed +- **The parent runs Step 9.2.1's probes itself, as the section says.** Phase 3 does the section's method Reads and no probe; Phase 4's one program runs the smoke and plan checks after Step 8.1 collects the plan checks and before Scope Check, dedup and Fix-First, and the record's Checks heading carries the `Exploratory QA:` line. The call count is unchanged, and the section text is untouched. +- **`gstack-review-dispatch pass` refuses a QA extra.** An extra named `qa`, or one whose prompt holds Step 9.2.1's "Only the parent runs report-only discovery.", stops the pass before any reader is created. A prompt file that cannot be read stops as an unreadable prompt, not a raw read error. The Capy skill lists Steps 7, 8 and 14.5 as extras, not Step 9.2.1. + ## [1.92.51.0] - 2026-11-01 **PRs no longer wait on the six-shard Windows suite. A short `windows-smoke` check is the required Windows check, and the full suite runs nightly on main.** Windows has minimal support (docs/support-tiers.md). Since 2026-11-01 06:00 UTC, Windows Free Tests finished last on 71 of 73 PR heads, with a wall time of 892 s at p50 and 1,886 s at p90 over 71 green PR runs, and each run used about 59 Windows runner-minutes. The new smoke check finished in 198 s on this PR (run 41325664750). With smoke in its place, the slowest required check on a PR drops from 892 s to 562 s at p50 and from 1,877 s to 1,193 s at p90. diff --git a/VERSION b/VERSION index fdd84fc4e..f38f1222d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.92.51.0 +1.92.52.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 306c1f587..513868411 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.92.51.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.92.52.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/capy/skill/gstack/SKILL.md b/capy/skill/gstack/SKILL.md index 7c87a2786..c6d85c010 100644 --- a/capy/skill/gstack/SKILL.md +++ b/capy/skill/gstack/SKILL.md @@ -46,7 +46,7 @@ Capy checklist for `/autoplan`: drive `gstack-autoplan`, never a hand adaptation Token caps on /ship: pass a brief's cap as `--token-cap `. /ship reads `cost.tokens` and `cost.credits` from `tools.thread.get({ thread_id: "" })` (its own jam; a subagent reading root `"this"` starts `--subagent --reading-from this --meter-scope tree`) inside the same execute program as the `gstack-ship-budget` command that takes them as `--reading --credits `, never alone. `ship/DRIVER.md` starts `--mode observe`: each program ends with `note`, and only a note past the cap stops (`docs/ship-budget.md`). -/ship on Capy is about a dozen calls, each one execute program (`ship/DRIVER.md`). Each review pass and Step 11 is one call to the checked-in pass program (fill only `input`): `const t0 = await now(); const p = (await tools.capy.bash({ command: ". '' && gstack-review-dispatch program", description: "Pass" })).output.split("\n"); return p[0] + "\n" + (await new (Object.getPrototypeOf(async () => {}).constructor)("tools", "sleep", "now", "input", JSON.parse(p[1]))(tools, sleep, now, { startedAt: t0, launch: "", cwd: "", base: "", out: "/p", pass: "-p", laneModel: "", planFlags: [], logStart: ["review"], jam: "", record: "", step: "9.2" }));` It plans, logs `logStart` (`reservation` mode from `RESERVATION_ID:`), sends each brief on `machine: "shared"`, then polls: one `tools.subagent.list` per ~20 s tick, returning 200 s after the program's first line, where a reader is settled only at status `done` or `failed` or with a `summary` (a just-created `idle` reader has not run); it starts readers under `rejected` with `tools.subagent.start` as slots free and stops on one stuck 20 minutes. It ends on `NEXT:` (`poll`: rerun) or `STOP:`. `models` maps roles to models (`outside-voice`: another family); Step 11 adds `stage: "voices"`; a full repeat pass, `diffBase: ""`. Steps 7, 8, 9.2.1 and 14.5 (`step: "14.5"`) use `extras`, JSON `{name, prompt_file, first, model}` (prompts verbatim from their sections, Step 7 `first`); they cannot block, so `model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }` (`anthropic/claude-haiku-5-5` also fits). Waits: never let 5 minutes pass between calls. While readers run, never end the turn on `wait`; rerun the loader. Otherwise re-issue `wait` with `duration_seconds: 200` and operation ids. At Step 22 report the `CALLS:` line of `gstack-ship-budget calls --events ` on your jam's transcript since /ship began. A small waiting child saves nothing. +/ship on Capy is about a dozen calls, each one execute program (`ship/DRIVER.md`). Each review pass and Step 11 is one call to the checked-in pass program (fill only `input`): `const t0 = await now(); const p = (await tools.capy.bash({ command: ". '' && gstack-review-dispatch program", description: "Pass" })).output.split("\n"); return p[0] + "\n" + (await new (Object.getPrototypeOf(async () => {}).constructor)("tools", "sleep", "now", "input", JSON.parse(p[1]))(tools, sleep, now, { startedAt: t0, launch: "", cwd: "", base: "", out: "/p", pass: "-p", laneModel: "", planFlags: [], logStart: ["review"], jam: "", record: "", step: "9.2" }));` It plans, logs `logStart` (`reservation` mode from `RESERVATION_ID:`), sends each brief on `machine: "shared"`, then polls: one `tools.subagent.list` per ~20 s tick, returning 200 s after the program's first line, where a reader is settled only at status `done` or `failed` or with a `summary` (a just-created `idle` reader has not run); it starts readers under `rejected` with `tools.subagent.start` as slots free and stops on one stuck 20 minutes. It ends on `NEXT:` (`poll`: rerun) or `STOP:`. `models` maps roles to models (`outside-voice`: another family); Step 11 adds `stage: "voices"`; a full repeat pass, `diffBase: ""`. Steps 7, 8 and 14.5 (`step: "14.5"`) use `extras`, JSON `{name, prompt_file, first, model}` (prompts verbatim from their sections, Step 7 `first`); they cannot block, so `model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }` (`anthropic/claude-haiku-5-5` also fits). Waits: never let 5 minutes pass between calls. While readers run, never end the turn on `wait`; rerun the loader. Otherwise re-issue `wait` with `duration_seconds: 200` and operation ids. At Step 22 report the `CALLS:` line of `gstack-ship-budget calls --events ` on your jam's transcript since /ship began. A small waiting child saves nothing. ## Plan, approve, ship diff --git a/docs/review-policy.md b/docs/review-policy.md index 1d62b14de..1d4e3612b 100644 --- a/docs/review-policy.md +++ b/docs/review-policy.md @@ -545,8 +545,9 @@ ones, and the arm lost blockers. `bin/gstack-review-dispatch` closes both ends: prompts prints `VERIFY: no brief records` (exit 3), never clean. 6. `pass --base --out --pass -- ` runs one pass from a staged manifest (`/pass-manifest.json`): audit readers marked first, then - the specialists with QA and the other audits, then the Red Team, then Step 11's - voices. Each reviewer keeps its plan entry and is rendered by the same code as + the specialists with the other audit readers, then the Red Team, then Step 11's + voices. A QA extra is refused before any reader is created: Step 9.2.1's probes + are the parent's. Each reviewer keeps its plan entry and is rendered by the same code as `render`, once its inputs exist; the Red Team's findings file is the specialists' `merged.jsonl` lines byte for byte. A brief leaves the bin in indexed chunks of at most 65,536 characters (`chunk`), so no tool output line carries a whole brief and a diff --git a/lib/review-pass.ts b/lib/review-pass.ts index 29928d8e9..365069720 100644 --- a/lib/review-pass.ts +++ b/lib/review-pass.ts @@ -3,7 +3,7 @@ * * gstack-review-dispatch pass turns gstack-review-plan's dispatches into a manifest: stage `first` * (audit readers that may write tests, so they settle before any reviewer reads the tree), stage - * `specialists` (the specialists, plus the other audit and QA readers), stage `red-team` and stage + * `specialists` (the specialists, plus the other audit readers), stage `red-team` and stage * `voices` (Step 11). Each reviewer keeps its plan entry (role, effort, exclusions) and is rendered * once, when its inputs exist, by lib/review-brief.ts; the Red Team's findings file is the * specialists' merged.jsonl lines, byte for byte. Briefs travel to the dispatch program in bounded @@ -28,7 +28,7 @@ export interface PassReader { role?: BriefRole; name?: string; effort?: string; exclude?: string; checklists?: string[]; /** The targeted reader: the plan's TARGETED classes and the merge base it reads on every pass. */ classes?: string[]; diff_base?: string; - /** An audit or QA reader: its prompt is a file the parent wrote from its section; its output is returned, never collected. */ + /** An audit reader: its prompt is a file the parent wrote from its section; its output is returned, never collected. */ prompt_file?: string; model?: PassModel; file?: string; sha256?: string; bytes?: number; chunks?: number; findings_sha256?: string; /** The fingerprint the reader's reply must report: the checkout's HEAD^{tree} and toplevel when its brief was rendered (GST-482). */ @@ -47,6 +47,9 @@ export interface Manifest { } export interface FixCheckFiles { fix_list: string; hunks: string; tests: string; callers?: string } export interface Extra { name: string; prompt_file: string; first?: boolean; model?: PassModel } +/** Step 9.2.1's own first line: its probes are the parent's, so a QA subagent only refuses (C4-36, C5-36: "parent-only"). */ +export const PARENT_ONLY_QA = 'Only the parent runs report-only discovery.'; +const qaRefusal = (name: string) => new BriefError(`Step 9.2.1 is the parent's report-only discovery (ship/sections/review-army.md: "${PARENT_ONLY_QA}"), never an audit reader: ${name}`); /** Brief characters per transport chunk: even with every character escaped, one chunk line stays far under 1 MiB. */ export const CHUNK_CHARS = 65_536; @@ -96,6 +99,9 @@ export function buildManifest(o: { } for (const e of o.extras ?? []) { if (!/^[a-z0-9-]+$/.test(e.name)) throw new BriefError(`an audit reader's name is lowercase letters, digits and dashes: ${e.name}`); + let brief = ''; + try { brief = fs.readFileSync(e.prompt_file, 'utf8'); } catch { brief = ''; } + if (/(^|-)qa(-|$)/.test(e.name) || brief.includes(PARENT_ONLY_QA)) throw qaRefusal(e.name); readers.push({ reader: `audit/${e.name}`, stage: e.first ? 'first' : 'specialists', title: `ship ${o.pass} audit/${e.name}`, state: 'planned', prompt_file: path.resolve(e.prompt_file), model: e.model }); } if (new Set(readers.map(r => r.reader)).size !== readers.length) throw new BriefError('two readers share a key in this pass'); diff --git a/package.json b/package.json index 56d0c665c..48e84b84b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.92.51", + "version": "1.92.52", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/ship/DRIVER.md b/ship/DRIVER.md index 3dec39134..ac19dd0bd 100644 --- a/ship/DRIVER.md +++ b/ship/DRIVER.md @@ -15,7 +15,7 @@ the next act. Read a section only when a phase or the list at the end sends you, up front. A STOP or decision this file does not cover: read `ship/SKILL.md` at that step, finish it there, come back. Unattended, take a question's recommended option (log it); gate dispositions and approvals stay pending and end the run. Keep a private invocation record (`--record`): write its six headings at Phase 1, then fill each as its phase ends: **Checks** -(Phases 2-4: lanes, pregate, evals, `Coverage:`, the plan checks, `Scope Check:`), **Decisions** (any +(Phases 2-4: lanes, pregate, evals, `Coverage:`, the plan checks, `Exploratory QA:`, `Scope Check:`), **Decisions** (any phase: each answered or auto-taken question, verbatim), **Reviews** (Phases 3-5: the plan line, start tokens, `Pre-Landing Review:`, `Gate edits:`, the reviewed tree), **Release** (Phases 5-7: version, CHANGELOG, commits), **Documentation** (Step 14.5) and **Next steps** (every phase: the next act). @@ -24,7 +24,7 @@ version, CHANGELOG, commits), **Documentation** (Step 14.5) and **Next steps** ( outside-voice and documentation brief, its severity and confidence rules and gates come from its section verbatim (placeholders filled); this file never shortens a reviewer's brief. A blocking reader's prompt is its `gstack-review-dispatch render` -file, unchanged; its findings reach `merged.jsonl` only via `collect`; `judge` refuses a drop or downgrade. Audit readers (Steps 7, 8, 9.2.1, 14.5) cannot block, so each runs on a +file, unchanged; its findings reach `merged.jsonl` only via `collect`; `judge` refuses a drop or downgrade. Audit readers (Steps 7, 8, 14.5) cannot block, so each runs on a small model at low effort; their findings go to the PR body. **Calls.** One pass with no fixes is 9 calls: Phases 1-8 and the report; at most 12 with a STOP's @@ -77,13 +77,13 @@ Never read TODOS.md or the full diff into the parent. A `PERSIST:` hint never re ## 3. Audits and review: Steps 7, 8, 9, 9.1, 9.2, 9.2.1 One program: the audit readers `READER:` names (Step 7 from `sections/test-coverage.md`, Step 8 from -`sections/plan-completion.md`, each run as written; Step 7 may write tests, so it settles first), Step 9.2.1's QA reader (that step of -`sections/review-army.md` as its brief) and the pass (above): Steps 9-9.2 as written. `Coverage:` and Step 8's line come from `--audit` when no +`sections/plan-completion.md`, each run as written; Step 7 may write tests, so it settles first), 9.2.1's Reads only and the pass +(above): Steps 9-9.2 from `sections/review-army.md` as written. `Coverage:` and Step 8's line come from `--audit` when no reader ran. ## 4. Judgment: Steps 8.1, 8.2, 9.3, 9.4 and Step 11 -Steps 8.1 and 8.2 (`Scope Check:`) on the todos and commits files, then the core checklist, Steps +Step 8.1, 9.2.1's probes, 8.2 (`Scope Check:`) on the todos and commits files, then the core checklist, Steps 9.3, 9.4 and the repeat decision as written (`gstack-review-plan` included; `JUDGMENT: ok` before any fix), fixes and persistence in one program. Prints `Pre-Landing Review:`, `Gate edits:`. `NEXT: stop` stops; `full` or diff --git a/ship/DRIVER.md.tmpl b/ship/DRIVER.md.tmpl index d4a874ae6..0298fb93e 100644 --- a/ship/DRIVER.md.tmpl +++ b/ship/DRIVER.md.tmpl @@ -13,7 +13,7 @@ the next act. Read a section only when a phase or the list at the end sends you, up front. A STOP or decision this file does not cover: read `ship/SKILL.md` at that step, finish it there, come back. Unattended, take a question's recommended option (log it); gate dispositions and approvals stay pending and end the run. Keep a private invocation record (`--record`): write its six headings at Phase 1, then fill each as its phase ends: **Checks** -(Phases 2-4: lanes, pregate, evals, `Coverage:`, the plan checks, `Scope Check:`), **Decisions** (any +(Phases 2-4: lanes, pregate, evals, `Coverage:`, the plan checks, `Exploratory QA:`, `Scope Check:`), **Decisions** (any phase: each answered or auto-taken question, verbatim), **Reviews** (Phases 3-5: the plan line, start tokens, `Pre-Landing Review:`, `Gate edits:`, the reviewed tree), **Release** (Phases 5-7: version, CHANGELOG, commits), **Documentation** (Step 14.5) and **Next steps** (every phase: the next act). @@ -22,7 +22,7 @@ version, CHANGELOG, commits), **Documentation** (Step 14.5) and **Next steps** ( outside-voice and documentation brief, its severity and confidence rules and gates come from its section verbatim (placeholders filled); this file never shortens a reviewer's brief. A blocking reader's prompt is its `gstack-review-dispatch render` -file, unchanged; its findings reach `merged.jsonl` only via `collect`; `judge` refuses a drop or downgrade. Audit readers (Steps 7, 8, 9.2.1, 14.5) cannot block, so each runs on a +file, unchanged; its findings reach `merged.jsonl` only via `collect`; `judge` refuses a drop or downgrade. Audit readers (Steps 7, 8, 14.5) cannot block, so each runs on a small model at low effort; their findings go to the PR body. **Calls.** One pass with no fixes is 9 calls: Phases 1-8 and the report; at most 12 with a STOP's @@ -75,13 +75,13 @@ Never read TODOS.md or the full diff into the parent. A `PERSIST:` hint never re ## 3. Audits and review: Steps 7, 8, 9, 9.1, 9.2, 9.2.1 One program: the audit readers `READER:` names (Step 7 from `sections/test-coverage.md`, Step 8 from -`sections/plan-completion.md`, each run as written; Step 7 may write tests, so it settles first), Step 9.2.1's QA reader (that step of -`sections/review-army.md` as its brief) and the pass (above): Steps 9-9.2 as written. `Coverage:` and Step 8's line come from `--audit` when no +`sections/plan-completion.md`, each run as written; Step 7 may write tests, so it settles first), 9.2.1's Reads only and the pass +(above): Steps 9-9.2 from `sections/review-army.md` as written. `Coverage:` and Step 8's line come from `--audit` when no reader ran. ## 4. Judgment: Steps 8.1, 8.2, 9.3, 9.4 and Step 11 -Steps 8.1 and 8.2 (`Scope Check:`) on the todos and commits files, then the core checklist, Steps +Step 8.1, 9.2.1's probes, 8.2 (`Scope Check:`) on the todos and commits files, then the core checklist, Steps 9.3, 9.4 and the repeat decision as written (`gstack-review-plan` included; `JUDGMENT: ok` before any fix), fixes and persistence in one program. Prints `Pre-Landing Review:`, `Gate edits:`. `NEXT: stop` stops; `full` or diff --git a/test/review-pass.test.ts b/test/review-pass.test.ts index 46f3df0f7..9d440c612 100644 --- a/test/review-pass.test.ts +++ b/test/review-pass.test.ts @@ -8,7 +8,7 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { promptHash, sectionTexts, withWorkspace } from '../lib/review-brief'; -import { CHUNK_CHARS, buildManifest, chunkCount, chunkOf, markReader, renderStage, specialistFindings, type Manifest } from '../lib/review-pass'; +import { CHUNK_CHARS, PARENT_ONLY_QA, buildManifest, chunkCount, chunkOf, markReader, renderStage, specialistFindings, type Manifest } from '../lib/review-pass'; import type { Dispatch } from '../lib/review-depth'; const ROOT = path.resolve(import.meta.dir, '..'); @@ -87,6 +87,13 @@ describe('pass manifest', () => { expect(m.readers[0]).toMatchObject({ state: 'created', path: '4' }); }); + // Value: protects=an unreadable extra prompt stops as a BriefError naming the reader; fails_when=a directory prompt_file throws a raw + // EISDIR from the QA guard's read (GST-450 adversarial #4); seam=none + test('a directory extra prompt stops as an unreadable prompt file, not a raw read error', () => { + const d = manifest([spec('testing')], { extras: [{ name: 'plan', prompt_file: tmp('rp-qa-') }] }); + expect(() => renderStage(d, 'specialists', texts, () => '')).toThrow(/cannot read audit\/plan's prompt file/); + }); + test('a brief over 1 MiB with invalid UTF-8 in its diff travels in bounded chunks that rejoin to the rendered sha', () => { const m = manifest([{ role: 'outside-voice', name: 'outside-voice', effort: 'host', exclude: [] }]); const diff = Buffer.concat([Buffer.from('+line \u00e9\n'.repeat(90_000)), Buffer.from([0xff, 0xfe, 0x0a]), Buffer.from('+"quoted" \\ back\n'.repeat(20_000))]).toString('utf8'); @@ -504,16 +511,33 @@ describe('Capy pass program (fake host, real bins)', () => { const elsewhere = tmp('rp-cwd-'); const out = path.join(dir, '.state/run/p1'); fs.mkdirSync(out, { recursive: true }); - fs.writeFileSync(path.join(out, 'qa.prompt.md'), 'QA brief'); + fs.writeFileSync(path.join(out, 'plan.prompt.md'), 'Plan completion brief'); const h = host(elsewhere, env, { lateSummary: true }); const res = await runPass(h, { base: 'main', out, pass: 'x13p1', cwd: dir, planFlags: ['--depth-floor', 'full'], - extras: [{ name: 'qa', prompt_file: path.join(out, 'qa.prompt.md'), model: { model_id: 'anthropic/claude-haiku-5-5', reasoning_mode: 'low' } }] }); + extras: [{ name: 'plan', prompt_file: path.join(out, 'plan.prompt.md'), model: { model_id: 'anthropic/claude-haiku-5-5', reasoning_mode: 'low' } }] }); expect(res).not.toContain('STOP:'); expect(res).toContain('NEXT: judge (Phase 4)'); - expect(h.creates.find(c => c.title === 'ship x13p1 audit/qa')!.prompt).toBe(withWorkspace('QA brief', fs.realpathSync(dir))); + expect(h.creates.find(c => c.title === 'ship x13p1 audit/plan')!.prompt).toBe(withWorkspace('Plan completion brief', fs.realpathSync(dir))); }, 180_000); - + // Value: protects=Step 9.2.1's probes stay the parent's, as its section says, so QA coverage is never a subagent's refusal; fails_when=the + // pass program dispatches a QA extra (C4-36, C5-36: the QA reader refused both passes, "parent-only", and the parent ran the probes after it); + // why_new=extras took any prompt (GST-450); seam=the pass program; no_claim=the parent's probes themselves (DRIVER Phase 4) + test('a QA extra, by name or by its section\'s parent-only brief, stops the pass before any reader is created', async () => { + const section = fs.readFileSync(path.join(ROOT, 'ship/sections/review-army.md'), 'utf8'); + expect(section).toContain(PARENT_ONLY_QA); + const brief = section.slice(section.indexOf('### Step 9.2.1'), section.indexOf('### Step 9.3')); + for (const [name, text] of [['qa', 'QA brief'], ['exploratory', brief]] as const) { + const { dir, env } = repo(); + const out = path.join(dir, '.state/run/p1'); + fs.mkdirSync(out, { recursive: true }); + fs.writeFileSync(path.join(out, name + '.prompt.md'), text); + const h = host(dir, env); + const res = await runPass(h, { base: 'main', out, pass: 'x14p1', planFlags: ['--depth-floor', 'full'], + extras: [{ name, prompt_file: path.join(out, name + '.prompt.md'), model: { model_id: 'anthropic/claude-haiku-5-5', reasoning_mode: 'low' } }] }); + expect([name, res.includes('STOP:'), res.includes("Step 9.2.1 is the parent's report-only discovery"), h.creates.length]).toEqual([name, true, true, 0]); + } + }, 240_000); test('decision 3.5 runs in the program: one fix-check reader whose prompt is the render CLI brief byte for byte, sent and collected', async () => { const { dir, env } = repo(); @@ -704,8 +728,8 @@ describe('a scoped pass after a blocker fix (GST-619)', () => { const out = path.join(dir, '.state/run/p2'); const extras = path.join(dir, '.state/extras.json'); fs.mkdirSync(path.join(dir, '.state'), { recursive: true }); - fs.writeFileSync(path.join(dir, '.state/qa.md'), 'QA the fix.\n'); - fs.writeFileSync(extras, JSON.stringify([{ name: 'qa', prompt_file: path.join(dir, '.state/qa.md') }])); + fs.writeFileSync(path.join(dir, '.state/plan.md'), 'Check the plan items against the fix.\n'); + fs.writeFileSync(extras, JSON.stringify([{ name: 'plan', prompt_file: path.join(dir, '.state/plan.md') }])); const args = ['pass', '--base', 'main', '--out', out, '--pass', 'p2', '--extras', extras, '--', '--pass', '1', '--last', 'full', '--pass1-depth', 'standard', '--fixed-blocking', ...flags, '--range-base', rangeBase.slice(0, 7)]; const p = bin(dir, env, args); expect(p.status, p.stdout + p.stderr).toBe(0); @@ -727,7 +751,7 @@ describe('a scoped pass after a blocker fix (GST-619)', () => { const s = scoped([]); expect(s.m.next).toBe('scoped'); expect(s.m.diff_base).toBe(s.mergeBase); - expect(s.m.readers.map(r => [r.reader, r.diff_base ?? null])).toEqual([['red-team', s.rangeBase], ['adversarial-native', null], ['outside-voice', null], ['audit/qa', null]]); + expect(s.m.readers.map(r => [r.reader, r.diff_base ?? null])).toEqual([['red-team', s.rangeBase], ['adversarial-native', null], ['outside-voice', null], ['audit/plan', null]]); settle(s, 'specialists'); const m = settle(s, 'red-team'); const redTeam = fs.readFileSync(m.readers.find(r => r.reader === 'red-team')!.file!, 'utf8'); @@ -745,7 +769,7 @@ describe('a scoped pass after a blocker fix (GST-619)', () => { test('after Step 11 ran (--adversarial-ran), the whole scoped pass reads the fix range', () => { const s = scoped(['--adversarial-ran']); expect(s.m.diff_base).toBe(s.rangeBase); - expect(s.m.readers.map(r => [r.reader, r.diff_base ?? null])).toEqual([['red-team', s.rangeBase], ['adversarial-native', null], ['audit/qa', null]]); + expect(s.m.readers.map(r => [r.reader, r.diff_base ?? null])).toEqual([['red-team', s.rangeBase], ['adversarial-native', null], ['audit/plan', null]]); }, 120_000); }); diff --git a/test/ship-driver.test.ts b/test/ship-driver.test.ts index 70cf50cc4..58f9dc471 100644 --- a/test/ship-driver.test.ts +++ b/test/ship-driver.test.ts @@ -219,16 +219,37 @@ describe('variant G: the call plan, observe budget, waits and audit readers (GST expect(flat(read('ship/sections/budget-handoff.md.tmpl'))).toContain('the wait until the last settles (`done`, `failed` or a final summary; a fresh `idle` has not run)'); }); - // Value: protects=audits that cannot block (Steps 7, 8, 9.2.1, 14.5) run on a small model at low effort with their sections' own briefs, and skip their reader when --audit decides them; fails_when=an audit reader inherits the lane's model and effort (#36: the coverage audit alone was 2.45M at Opus), or an audit brief is paraphrased; + // Value: protects=audits that cannot block (Steps 7, 8, 14.5) run on a small model at low effort with their sections' own briefs, and skip their reader when --audit decides them; fails_when=an audit reader inherits the lane's model and effort (#36: the coverage audit alone was 2.45M at Opus), or an audit brief is paraphrased; // why_new=audit model routing is new; seam=none; no_claim=the audits' recall test('audit readers run on a small model at low effort, named for Capy, and Phase 3 dispatches only what READER names', () => { - expect(flat(doc)).toContain('Audit readers (Steps 7, 8, 9.2.1, 14.5) cannot block, so each runs on a small model at low effort; their findings go to the PR body'); + expect(flat(doc)).toContain('Audit readers (Steps 7, 8, 14.5) cannot block, so each runs on a small model at low effort; their findings go to the PR body'); expect(flat(doc)).toContain('`Coverage:` and Step 8\'s line come from `--audit` when no reader ran'); const capy = read('capy/skill/gstack/SKILL.md'); expect(capy).toContain('model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }'); expect(capy).toContain('`anthropic/claude-haiku-5-5` also fits'); }); + // Value: protects=Step 9.2.1's report-only probes run in the parent's own Phase 4 program, as the section says, with its Reads in Phase 3, and + // their result line reaches the record, at no extra call; fails_when=the driver or the Capy skill sends Step 9.2.1 out as a QA reader + // (C4-36, C5-36: it refused both passes, "parent-only", and QA coverage was empty), the probes leave the record, or they take their own call; + // why_new=#3648 made Step 9.2.1 a reader (GST-450); seam=none; no_claim=the probes' recall + test('Step 9.2.1 is the parent\'s: Reads in Phase 3, probes in Phase 4\'s one program, Exploratory QA in the record, never an extra', () => { + const f = flat(doc); + const phase = (n: number) => f.slice(f.indexOf(`## ${n}. `), f.indexOf(`## ${n + 1}. `)); + expect(phase(3)).not.toMatch(/QA reader/); + expect(phase(3)).toContain("9.2.1's Reads only"); + expect(phase(4)).toContain("Step 8.1, 9.2.1's probes, 8.2 (`Scope Check:`)"); + const at = ['Step 8.1', "9.2.1's probes", '8.2 (`Scope Check:`)', 'Steps 9.3, 9.4 and the repeat decision'].map(s => phase(4).indexOf(s)); + expect(at[0]).toBeGreaterThanOrEqual(0); + expect(at).toEqual([...at].sort((a, b) => a - b)); + expect(phase(4)).toContain('one program'); + expect(f).toContain('the plan checks, `Exploratory QA:`, `Scope Check:`), **Decisions**'); + const capy = flat(read('capy/skill/gstack/SKILL.md')); + expect(capy).toContain('Steps 7, 8 and 14.5 (`step: "14.5"`) use `extras`'); + expect(capy).not.toMatch(/9\.2\.1[^.]*`extras`/); + expect(flat(read('ship/sections/review-army.md'))).toContain('### Step 9.2.1: Exploratory QA (before Fix-First) Only the parent runs report-only discovery.'); + }); + // Value: protects=a driven /ship dispatches every blocking reader on its rendered brief file unchanged, checks it with `sent`, merges findings only through `collect`, gates judgment with `judge` and verifies the run before the bind; fails_when=the driver lets the model write or trim a brief again or merge findings in prose (round 4b: arm D lost 8 blockers with 75 of 112 briefs paraphrased); // why_new=the driver named no renderer or hash check; seam=none; no_claim=the parent's adherence, which verify checks after the fact test('briefs go out as rendered and findings come back unchanged: render, sent, collect, judge, verify', () => { From 693ee7132a9d1d9f08a90f476aff9762618a67e7 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 1 Nov 2026 19:11:57 -0800 Subject: [PATCH 2/8] v1.92.53.0 fix(ubicloud): reap destroys only the caller's own VMs (GST-573) (#3756) * fix(ubicloud): pack and usage on macOS, empty projects (#3680, #3679, #3678) pack builds its archive in two tar passes plus gzip, which bsdtar and GNU tar both accept, with COPYFILE_DISABLE=1 so Apple's tar adds no ._* files. vm_rows skips the blank row Ubicloud returns for a project with no VMs. usage parenthesizes its ternary for the one-true-awk and exits 1 on an unreadable list or size; the quota preflight logs that and defers to the API. Co-authored-by: Jared <182125291+yolo-jared@users.noreply.github.com> Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (usage skips a VM destroyed since the list; spawn timeout) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (quota wait retries an unreadable usage) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: changelog fragment for the ubicloud macOS and empty-project fixes Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: TESTING_INTERNALS notes the quota preflight's unreadable-usage path Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(ubicloud): reap destroys only the caller's own VMs (GST-573) reap, which every up runs first, destroyed any VM in the project whose TTL had ended, whatever owner created it. That broke the owner-scoped cloud-deletes rule. It now destroys only the caller's own expired and old TTL-less VMs and logs a count of other owners' expired VMs. 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> * docs(ubicloud): trim the owner-scoped reap prose back under the context budget Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: TESTING_INTERNALS says the TTL reaper covers only this owner's VMs Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(ubicloud): restore the expiry and reap conditions in the trimmed reap prose Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: Jared <182125291+yolo-jared@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 +- docs/TESTING_INTERNALS.md | 16 +++++----- docs/skills.md | 6 ++-- package.json | 2 +- scripts/ubicloud/ubi-runner.sh | 37 +++++++++++++---------- test/ubicloud-runner-run.test.ts | 1 + test/ubicloud-runner-verify-quota.test.ts | 6 ++-- test/ubicloud-runner.test.ts | 36 +++++++++++++++++----- ubicloud/SKILL.md | 16 +++++----- ubicloud/SKILL.md.tmpl | 16 +++++----- 12 files changed, 91 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbb8e5a32..e75ec6b6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.92.53.0] - 2026-11-02 + +**`reap` and `up` now destroy only your own Ubicloud VMs (GST-573).** Before, every `up` destroyed any VM in the project whose TTL had ended, whoever created it, which broke the rule that cloud deletes stay with the owner who made the resource. Now reap destroys only the caller's own expired VMs and its own old TTL-less ones, and only counts other owners' expired VMs in its log (`reap: left N expired VMs of other owners; only their owners reap them`). + +### Changed +- **Owner-scoped reap.** Another owner's VM, even an expired one, and an untagged one are never reaped. A lane's leftover VM is destroyed by that owner's own next `up` or `reap`, so a lane that never runs again leaves its VM until someone with that owner runs `reap` or `down`. + ## [1.92.52.0] - 2026-11-02 **/ship on Capy runs Exploratory QA again.** Since v1.91.345.0 (#3648) the driver sent Step 9.2.1 out as a QA reader subagent, but the section says only the parent runs report-only discovery, so the reader refused on every pass ("parent-only") and the parent ran the probes afterward anyway, an extra reader call each pass (the C4-36 and C5-36 replays). diff --git a/VERSION b/VERSION index f38f1222d..222a2a9f5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.92.52.0 +1.92.53.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 513868411..716c0c598 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.92.52.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.92.53.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/docs/TESTING_INTERNALS.md b/docs/TESTING_INTERNALS.md index ea9a9f4ca..8c913e020 100644 --- a/docs/TESTING_INTERNALS.md +++ b/docs/TESTING_INTERNALS.md @@ -1002,7 +1002,7 @@ with `UBICLOUD: left=<0|1|unknown> owner= vm= others=`, checked against the project's VM list after teardown; anything but `left=0` turns a passing command into exit 1. pregate-remote.sh reads that line from the file named by `UBI_LEFT_FILE`, never from the command's output. No owner-age sweep runs by default (the TTL reaper -below covers every owner); set +below covers this owner's expired VMs); set `UBI_GC_HOURS` to a positive number (or run `gc HOURS`; `gc 0` is a no-op) to sweep only this owner's VMs older than that. For other commands, use the runner directly: `scripts/ubicloud/ubi-runner.sh run --setup