511 lines
20 KiB
TypeScript
511 lines
20 KiB
TypeScript
// 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": "<sha256 below>",
|
|
// "evidence": "https://…/issues/4296#issuecomment-…", // REQUIRED
|
|
// "reason": "one-line attribution",
|
|
// "added_ts": "2026-09-07T00:00:00.000Z"
|
|
// }
|
|
// ]
|
|
// }
|
|
//
|
|
// - fingerprint = `--fingerprint` helper output = sha256("<file>\0<name>"),
|
|
// where <file> is the repo-relative test file and <name> 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 <slug> [--round N] [--only oxlint|typecheck|test:changed] [--no-cache]
|
|
// bun .octopus/skills/review-code/scripts/precondition-gate.ts --fingerprint <repo-relative-test-file> "<bun test name>"
|
|
//
|
|
// Harness mode (tests only, GATE_* env precedent — see GATE_ARCHIVE_DIR):
|
|
// GATE_CHECKS_JSON=<path> replace the check-set with a JSON array of
|
|
// { name, command, cwd, timeoutMs?, env? }
|
|
// GATE_WAIVER_FILE=<path> replace the default waiver file location
|
|
// (<repoRoot>/.octopus/known-failures.json)
|
|
//
|
|
// Exit codes: 0 = green (dispatch reviewers); 1 = RED — a PRECONDITION-BLOCK
|
|
// record has been appended to .octopus/runs/<slug>/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 // "<repo-relative file>::<bun test name>"
|
|
fingerprint: string // sha256("<file>\0<name>")
|
|
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<WaiverEntry>
|
|
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 "<file>::<name>"): ${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<string, ParsedFailure>()
|
|
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 <repo-relative-test-file> "<bun test name>"',
|
|
)
|
|
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 <slug> [--round N] [--only oxlint|typecheck|test:changed] [--no-cache]",
|
|
)
|
|
console.error(
|
|
' bun .octopus/skills/review-code/scripts/precondition-gate.ts --fingerprint <repo-relative-test-file> "<bun test name>"',
|
|
)
|
|
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<string, string>; 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<Check>
|
|
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<string, unknown> = {
|
|
schema_version: 3,
|
|
gate: "precondition-gate",
|
|
ts: new Date().toISOString(),
|
|
slug,
|
|
round,
|
|
fingerprint,
|
|
cache_hit: hit !== null,
|
|
checks: [] as Array<Record<string, unknown>>,
|
|
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<Record<string, unknown>>).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.`)
|
|
}
|