// Mechanical-green precondition gate for review-code (#2598). // // Runs the CI-aligned checks BEFORE any reviewer is dispatched — at Phase A // entry and at every Phase D re-entry — so an LLM review round is never spent // on a diff a deterministic check would reject: // 1. oxlint — `bun oxlint --deny-warnings` (repo root) // 2. typecheck — `bun turbo typecheck` (repo root) // 3. test:changed — `cd packages/octopus && CI=true TEST_SHARDS=3 bun run // test:changed` (mirrors .gitea/workflows/ci.yml exactly) // // `test:parallel` is deliberately NOT a gate check — the full suite belongs // to verify (#2598 check-tiering). // // Known-failure waivers (#4380): a main-preexisting red can reach this gate // through test:changed's transitive import closure with zero causal link to // the diff under review (instance: #4296 N-01, TD-930 red via the `Config` // edge). Waivers are declared in `.octopus/known-failures.json` (tracked — // they ride PRs like code) as: // // { // "schema_version": 1, // "entries": [ // { // "test": "packages/octopus/test/foo.test.ts::widget > broken", // "fingerprint": "", // "evidence": "https://…/issues/4296#issuecomment-…", // REQUIRED // "reason": "one-line attribution", // "added_ts": "2026-09-07T00:00:00.000Z" // } // ] // } // // - fingerprint = `--fingerprint` helper output = sha256("\0"), // where is the repo-relative test file and is the bun test // name exactly as printed in the `(fail)` line (describe chain joined // with " > ", timing suffix stripped). // - ALL failures of a `test:changed` run matched by fingerprint downgrade // that check to WARN: the gate passes (`blocked: false`) and the record // carries `waivers_applied` with the evidence URL. oxlint/typecheck // failures, timeouts, and unparsable runs are NEVER waivable (fail-closed). // - Invalidation is automatic — 指纹消失即失效: once the waived test no // longer fails while its file still ran (fix landed), the entry matches // nothing and the gate emits a `waiver-stale` warning + record entry; // physical removal rides the fix PR (the gate never rewrites repo files). // // Result cache: each run is keyed on a tree fingerprint (HEAD sha + tracked // diff + untracked file contents + the check-set). If the newest record for // this slug with the same fingerprint is green, the mechanical run is // skipped (cache hit) — the Developer pre-handoff gate (Phase C step 4) and // the Phase A/D dispatch gate collapse into ONE execution per tree state. // verify Phase 2.0 consumes the same green record as its mechanical-evidence // reuse source (eff-gate-cache: run this script; a cache-hit GREEN transfers // to verify's typecheck/lint DoD without a local re-run). Only GREEN results are ever reused; a red result always re-runs. The // fingerprint does NOT cover the environment (bun version, node_modules // state) — pass --no-cache to force a full re-run after an env change. A // waived green is cacheable like any other green: the waiver file is part of // the tree (tracked diff or untracked contents), so editing it changes the // fingerprint and forces a re-run. // // Usage (from the workflow worktree root): // bun .octopus/skills/review-code/scripts/precondition-gate.ts [--round N] [--only oxlint|typecheck|test:changed] [--no-cache] // bun .octopus/skills/review-code/scripts/precondition-gate.ts --fingerprint "" // // Harness mode (tests only, GATE_* env precedent — see GATE_ARCHIVE_DIR): // GATE_CHECKS_JSON= replace the check-set with a JSON array of // { name, command, cwd, timeoutMs?, env? } // GATE_WAIVER_FILE= replace the default waiver file location // (/.octopus/known-failures.json) // // Exit codes: 0 = green (dispatch reviewers); 1 = RED — a PRECONDITION-BLOCK // record has been appended to .octopus/runs//reviews/code/precondition-gate.jsonl // (Tier 1); do NOT dispatch reviewers, hand the output to the Developer // (Phase C) and re-run after the fix. 2 = usage error. // // Every invocation (green, red, or cache hit) appends a record — per-check // durations feed the gate-latency telemetry (#2598 DoD). import { spawnSync } from "node:child_process" import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs" import { createHash } from "node:crypto" import { join } from "node:path" // ---- Waiver types & helpers (#4380) ---- interface WaiverEntry { test: string // "::" fingerprint: string // sha256("\0") evidence: string // attribution URL (issue comment) reason?: string added_ts?: string } interface WaiverLoad { entries: WaiverEntry[] warnings: string[] } function testFingerprint(file: string, name: string): string { return createHash("sha256").update(`${file}\0${name}`).digest("hex") } // Fail-closed loader: any malformation drops the affected entries (or the // whole file) with a warning — never grants an exemption it cannot verify. function loadWaivers(waiverPath: string): WaiverLoad { const warnings: string[] = [] if (!existsSync(waiverPath)) return { entries: [], warnings } let parsed: unknown try { parsed = JSON.parse(readFileSync(waiverPath, "utf8")) } catch (e) { return { entries: [], warnings: [ `malformed known-failures file (unparsable JSON) at ${waiverPath}: ${(e as Error).message} — treating as no waivers`, ], } } const wf = parsed as { schema_version?: unknown; entries?: unknown } if (typeof wf.schema_version !== "number" || wf.schema_version !== 1) { return { entries: [], warnings: [`malformed known-failures file at ${waiverPath}: schema_version must be 1 — treating as no waivers`], } } if (!Array.isArray(wf.entries)) { return { entries: [], warnings: [`malformed known-failures file at ${waiverPath}: entries must be an array — treating as no waivers`], } } const entries: WaiverEntry[] = [] for (const raw of wf.entries) { const e = raw as Partial const desc = typeof e.test === "string" ? e.test : JSON.stringify(raw).slice(0, 120) if (typeof e.test !== "string" || !e.test.includes("::")) { warnings.push(`waiver entry ignored (test must be "::"): ${desc}`) continue } if (typeof e.fingerprint !== "string" || !/^[0-9a-f]{64}$/.test(e.fingerprint)) { warnings.push(`waiver entry ignored (fingerprint must be 64-hex — run --fingerprint): ${desc}`) continue } if (typeof e.evidence !== "string" || !/^https?:\/\//.test(e.evidence)) { warnings.push(`waiver entry ignored (evidence must be an issue/comment URL): ${desc}`) continue } entries.push({ test: e.test, fingerprint: e.fingerprint, evidence: e.evidence, ...(typeof e.reason === "string" ? { reason: e.reason } : {}), ...(typeof e.added_ts === "string" ? { added_ts: e.added_ts } : {}), }) } return { entries, warnings } } // ---- bun test output parsing (formats verified against bun 1.3.14 non-TTY) ---- // // stdout carries the test:changed selection list BEFORE execution: // test:changed: N test file(s) selected from M changed file(s): // packages/octopus/test/foo.test.ts // stderr carries per-file headers + failure lines (passing-only files and // (pass) lines leave no trace): // test/foo.test.ts: // (fail) widget > known broken thing [0.08ms] interface ParsedFailure { file: string // repo-relative name: string fingerprint: string } function parseSelection(stdoutText: string): string[] { const lines = stdoutText.split(/\r?\n/) const selected: string[] = [] let inList = false for (const line of lines) { if (!inList) { if (/^test:changed: \d+ test file\(s\) selected from /.test(line)) inList = true continue } const m = line.match(/^ (\S+\.(?:test|spec)\.(?:ts|tsx))(?: \(isolated\))?$/) if (!m) break // the list ends at the first non-entry line selected.push(m[1]!) } return selected } // bun prints file headers exactly as the path was passed on the CLI; // test:changed passes package-relative paths, so resolve against the // repo-relative selection list by unique suffix. Ambiguous/unresolvable // headers stay raw — a properly-authored waiver then misses, which is the // fail-closed direction. function resolveRepoRelative(header: string, selection: string[]): string { if (selection.includes(header)) return header const candidates = selection.filter((s) => s.endsWith(`/${header}`)) return candidates.length === 1 ? candidates[0]! : header } function parseFailures(outputText: string, selection: string[]): ParsedFailure[] { const byFp = new Map() let header: string | null = null for (const line of outputText.split(/\r?\n/)) { const h = line.match(/^(\S+\.(?:test|spec)\.(?:ts|tsx)):\s*$/) if (h) { header = h[1]! continue } const f = line.match(/^\(fail\) (.+) \[[0-9.]+ms\]$/) if (f && header !== null) { const file = resolveRepoRelative(header, selection) const name = f[1]! byFp.set(testFingerprint(file, name), { file, name, fingerprint: testFingerprint(file, name) }) } } return [...byFp.values()] } // ---- CLI ---- const args = process.argv.slice(2) const fpIdx = args.indexOf("--fingerprint") if (fpIdx >= 0) { const file = args[fpIdx + 1] const name = args[fpIdx + 2] if (!file || !name) { console.error( 'usage: bun .octopus/skills/review-code/scripts/precondition-gate.ts --fingerprint ""', ) process.exit(2) } console.log(testFingerprint(file, name)) process.exit(0) } let slug = "" let round = 1 let only: string | null = null let noCache = false for (let i = 0; i < args.length; i++) { if (args[i] === "--round") round = Number(args[++i] ?? 1) else if (args[i] === "--no-cache") noCache = true else if (args[i] === "--only") only = args[++i] ?? "" else slug = args[i] } if (!slug || !/^[a-zA-Z0-9._-]+$/.test(slug) || !Number.isFinite(round) || round < 1) { console.error( "usage: bun .octopus/skills/review-code/scripts/precondition-gate.ts [--round N] [--only oxlint|typecheck|test:changed] [--no-cache]", ) console.error( ' bun .octopus/skills/review-code/scripts/precondition-gate.ts --fingerprint ""', ) process.exit(2) } const root = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }) if (root.status !== 0 || !root.stdout) { console.error("precondition-gate: not inside a git worktree") process.exit(2) } const repoRoot = root.stdout.trim() type Check = { name: string; command: string; cwd: string; env?: Record; timeoutMs: number } const defaultChecks: Check[] = [ { name: "oxlint", command: "bun oxlint --deny-warnings", cwd: repoRoot, timeoutMs: 5 * 60_000 }, { name: "typecheck", command: "bun turbo typecheck", cwd: repoRoot, timeoutMs: 10 * 60_000 }, { name: "test:changed", command: "bun run test:changed", cwd: join(repoRoot, "packages", "octopus"), env: { CI: "true", TEST_SHARDS: "3" }, // mirror ci.yml Test step timeoutMs: 20 * 60_000, }, ] // Harness mode (#4380): GATE_CHECKS_JSON replaces the production check-set // with fixture commands (GATE_* env precedent). Unset in production. let allChecks: Check[] = defaultChecks if (process.env.GATE_CHECKS_JSON) { try { const parsed = JSON.parse(readFileSync(process.env.GATE_CHECKS_JSON, "utf8")) as unknown if (!Array.isArray(parsed) || parsed.length === 0) throw new Error("must be a non-empty JSON array") for (const c of parsed) { const chk = c as Partial if (typeof chk.name !== "string" || typeof chk.command !== "string" || typeof chk.cwd !== "string") { throw new Error("each check needs string name/command/cwd") } if (chk.timeoutMs !== undefined && typeof chk.timeoutMs !== "number") throw new Error("timeoutMs must be a number") if (chk.env !== undefined && typeof chk.env !== "object") throw new Error("env must be an object") } allChecks = parsed as Check[] } catch (e) { console.error( `precondition-gate: invalid GATE_CHECKS_JSON (${process.env.GATE_CHECKS_JSON}): ${(e as Error).message}`, ) process.exit(2) } } const checks = only ? allChecks.filter((c) => c.name === only) : allChecks if (checks.length === 0) { console.error(`--only must be one of: ${allChecks.map((c) => c.name).join(", ")}`) process.exit(2) } const runsDir = join(repoRoot, ".octopus", "runs", slug, "reviews", "code") const gateLog = join(runsDir, "precondition-gate.jsonl") const gitOut = (gitArgs: string[]): string => { const r = spawnSync("git", gitArgs, { encoding: "utf8", cwd: repoRoot }) return r.status === 0 ? r.stdout : "" } // Fingerprint = check-set + HEAD + tracked diff (staged+unstaged) + untracked // contents. Above the untracked cap the fingerprint is made unique so this // run can never cache-hit (fail-safe: always re-run). const UNTRACKED_CACHE_CAP = 500 function treeFingerprint(): string { const h = createHash("sha256") h.update("gate-fingerprint-v1\n") h.update(checks.map((c) => c.name).join(",") + "\n") h.update(gitOut(["rev-parse", "HEAD"])) h.update("\0") h.update(gitOut(["diff", "HEAD"])) h.update("\0") const untracked = gitOut(["ls-files", "--others", "--exclude-standard"]).split(/\r?\n/).filter(Boolean) if (untracked.length > UNTRACKED_CACHE_CAP) return `nocache-${Date.now()}-${Math.random()}` const hashes = spawnSync("git", ["hash-object", "--stdin-paths"], { input: untracked.join("\n"), encoding: "utf8", cwd: repoRoot, }) h.update(hashes.status === 0 ? hashes.stdout : "") return h.digest("hex") } // Only a GREEN record with the identical fingerprint may skip a re-run; red // always re-runs (an environment-caused red must not wedge the gate until // the tree changes). function lastGreenHit(fp: string): { round: number; ts: string } | null { if (!existsSync(gateLog)) return null const lines = readFileSync(gateLog, "utf8") .split(/\r?\n/) .filter((l) => l.trim() !== "") for (let i = lines.length - 1; i >= 0; i--) { try { const r = JSON.parse(lines[i]!) as { fingerprint?: string; blocked?: boolean; round?: number; ts?: string } if (r.fingerprint === fp && r.blocked === false && typeof r.ts === "string") { return { round: r.round ?? 0, ts: r.ts } } } catch { // tolerate a malformed/truncated line — keep scanning } } return null } const fingerprint = treeFingerprint() const hit = noCache ? null : lastGreenHit(fingerprint) const waiverPath = process.env.GATE_WAIVER_FILE ?? join(repoRoot, ".octopus", "known-failures.json") const waivers = loadWaivers(waiverPath) const record: Record = { schema_version: 3, gate: "precondition-gate", ts: new Date().toISOString(), slug, round, fingerprint, cache_hit: hit !== null, checks: [] as Array>, blocked: false, } if (hit) { record.checks = checks.map((c) => ({ name: c.name, skipped: true })) console.log(`PRECONDITION-GATE GREEN (cache hit — slug=${slug} round=${round})`) console.log(`Reusing green result from ${hit.ts} (round ${hit.round}) — identical tree fingerprint.`) } else { const tail = (s: string | null | undefined): string[] => (s ?? "") .split(/\r?\n/) .filter((l) => l.trim() !== "") .slice(-15) for (const w of waivers.warnings) console.log(`[waiver] WARN ${w}`) let blocked = false for (const c of checks) { const started = Date.now() const r = spawnSync(c.command, { shell: true, cwd: c.cwd, encoding: "buffer", env: { ...process.env, ...c.env }, timeout: c.timeoutMs, }) const timedOut = r.error?.name === "TimeoutError" || (r.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT" const exitCode = timedOut ? 124 : (r.status ?? 1) const failed = exitCode !== 0 const outText = r.stdout?.toString() ?? "" const errText = r.stderr?.toString() ?? "" // ---- waiver pass (#4380): test:changed only ---- // A red is waivable only when every parsed failure matches a waiver // fingerprint. Timeouts and zero-failure reds (crash, harness gap, output // drift) stay blocking — no fingerprint to vouch for, fail closed. let waived = false let applied: WaiverEntry[] = [] let failures: ParsedFailure[] = [] let stale: WaiverEntry[] = [] if (c.name === "test:changed") { const selection = parseSelection(outText) failures = parseFailures(`${outText}\n${errText}`, selection) if (failed && !timedOut && failures.length > 0 && waivers.entries.length > 0) { const fpSet = new Set(failures.map((f) => f.fingerprint)) applied = waivers.entries.filter((e) => fpSet.has(e.fingerprint)) waived = applied.length === failures.length } // 指纹消失即失效: the entry's file ran in this selection yet its // fingerprint is absent from the failures — the waiver is inert now. // Report only; removal rides the fix PR (the gate never rewrites // repo-tracked state). if (waivers.entries.length > 0 && selection.length > 0) { const failFps = new Set(failures.map((f) => f.fingerprint)) stale = waivers.entries.filter((e) => { const file = e.test.split("::")[0]! return selection.includes(file) && !failFps.has(e.fingerprint) }) } } if (failed && !waived) blocked = true const errorLines = failed ? tail(errText || outText) : [] ;(record.checks as Array>).push({ name: c.name, command: c.command, cwd: c.cwd.replace(/\\/g, "/"), exit_code: exitCode, timed_out: timedOut, ...(failures.length > 0 ? { failures } : {}), ...(waived ? { waived: true } : {}), duration_ms: Date.now() - started, ...(errorLines.length > 0 ? { error_tail: errorLines } : {}), }) if (applied.length > 0) { record.waivers_applied = [ ...((record.waivers_applied as WaiverEntry[]) ?? []), ...applied.map((e) => ({ test: e.test, fingerprint: e.fingerprint, evidence: e.evidence })), ] } if (stale.length > 0) { record.waivers_stale = [ ...((record.waivers_stale as WaiverEntry[]) ?? []), ...stale.map((e) => ({ test: e.test, fingerprint: e.fingerprint })), ] } if (waived) { console.log( `[${c.name}] WARN (waived — ${applied.length} known failure(s) exempted) (${((Date.now() - started) / 1000).toFixed(1)}s)`, ) for (const e of applied) console.log(` [waiver] ${e.test} — evidence: ${e.evidence}`) } else if (failed && applied.length > 0) { console.log( `[${c.name}] FAIL (${applied.length}/${failures.length} failure(s) waived — unmatched failures block the gate) (${((Date.now() - started) / 1000).toFixed(1)}s)`, ) for (const e of applied) console.log(` [waiver] ${e.test} — evidence: ${e.evidence}`) } else { console.log(`[${c.name}] ${failed ? "FAIL" : "ok"} (${((Date.now() - started) / 1000).toFixed(1)}s)`) } for (const e of stale) { console.log( ` [waiver-stale] ${e.test} — fingerprint absent from this run; entry is inert, remove it in the fix PR`, ) } if (failed) for (const l of errorLines.slice(0, 8)) console.log(` ${l}`) } record.blocked = blocked } try { mkdirSync(runsDir, { recursive: true }) appendFileSync(gateLog, JSON.stringify(record) + "\n") } catch (e) { // The gate verdict must not depend on Tier 1 logging succeeding; surface but proceed. console.error(`[warn] could not append gate record: ${(e as Error).message}`) } console.log() if (record.blocked) { console.log("PRECONDITION-BLOCK — mechanically red. Do NOT dispatch reviewers.") console.log("Route the failing check output above to the Developer (Phase C), fix, then re-run this gate.") process.exit(1) } const appliedCount = ((record.waivers_applied as WaiverEntry[]) ?? []).length if (hit) { // cache-hit path already printed above } else if (appliedCount > 0) { console.log( `PRECONDITION-GATE GREEN (slug=${slug} round=${round}) — ${appliedCount} known failure(s) waived; evidence in the gate record. Dispatch reviewers.`, ) } else { console.log(`PRECONDITION-GATE GREEN (slug=${slug} round=${round}) — dispatch reviewers.`) }