diff --git a/.changes/pr-pipeline/fix-pr-verify-large-diffs-5c1e8a37.md b/.changes/pr-pipeline/fix-pr-verify-large-diffs-5c1e8a37.md new file mode 100644 index 00000000000..63d8d8be259 --- /dev/null +++ b/.changes/pr-pipeline/fix-pr-verify-large-diffs-5c1e8a37.md @@ -0,0 +1,11 @@ +--- +bump: patch +--- + +### Fixed + +- `pr-verify` judges a diff of any realistic size. It splits a diff into as many chunks of at most + 300000 bytes as it takes, up to 12 (`MAX_CHUNKS` in `chunk-diff.sh`, which holds a 3 MB diff), each + with its own judge step, and the head still passes only when every chunk and the final judge do. A + file over 300000 bytes, or a diff that needs more than 12 chunks, still ends the run with no + verdict and an error naming the file, or the diff's size and the chunk count. diff --git a/.github/scripts/pr-verify/chunk-diff.sh b/.github/scripts/pr-verify/chunk-diff.sh index f574619b42f..df572d52695 100755 --- a/.github/scripts/pr-verify/chunk-diff.sh +++ b/.github/scripts/pr-verify/chunk-diff.sh @@ -17,6 +17,13 @@ # shellcheck disable=SC2154 # the step's env sets DATA and the limits set -euo pipefail +# The most chunks a head is judged in, and the run's cost guard: each judging +# job in pr-verify.yml has a judge step for every chunk up to this, and a +# timeout that holds them all and the final judge (workflow.test.mjs holds both +# to it). 12 chunks of MAX_DIFF_BYTES hold a 3 MB diff of whole files. Only a +# test sets its own. +MAX_CHUNKS=${MAX_CHUNKS:-12} + here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) mkdir "$DATA/diff" status=0 diff --git a/.github/scripts/pr-verify/chunk-diff.test.mjs b/.github/scripts/pr-verify/chunk-diff.test.mjs index fba213d1d8f..1e33e71eaec 100644 --- a/.github/scripts/pr-verify/chunk-diff.test.mjs +++ b/.github/scripts/pr-verify/chunk-diff.test.mjs @@ -97,6 +97,39 @@ test("a diff that needs more chunks than the job has slots stops and says so", ( assert.match(stdout, /^::error::.*needs 3 chunks, more than 2/m); }); +// The limits the "Write the diff" step runs under: its env in pr-verify.yml, +// and no MAX_CHUNKS, so the script's own ceiling holds. +const WORKFLOW = readFileSync(new URL("../../workflows/pr-verify.yml", import.meta.url), "utf8"); +const LIMITS = { + ...Object.fromEntries(["MAX_DIFF_BYTES", "PART_BYTES", "PART_LINES"].map((key) => [key, new RegExp(`^ +${key}: "(\\d+)"$`, "m").exec(WORKFLOW)[1]])), + MAX_CHUNKS: undefined, +}; +const CEILING = Number(/^MAX_CHUNKS=\$\{MAX_CHUNKS:-(\d+)\}$/m.exec(readFileSync(SCRIPT, "utf8"))[1]); +// A file of about `bytes` bytes: each content line is `+x\n`, 3 bytes. +const sized = (name, bytes) => file(name, Math.floor(bytes / 3)); + +test("under the workflow's limits a 3 MB diff is judged in as many chunks as it needs, with no stop", () => { + // Each file over half a chunk, so each takes a chunk of its own: 12 chunks. + const patch = Array.from({ length: 12 }, (_, i) => sized(`src/file-${i + 1}.mjs`, 250000)).join(""); + assert.ok(Buffer.byteLength(patch) > 3000000); + const { status, data, outputs, stdout } = run(patch, LIMITS); + assert.equal(status, 0, stdout); + assert.equal(outputs, "chunks=12\n"); + assert.doesNotMatch(stdout, /::error::/); + for (const k of [1, 10, 12]) assert.ok(partsOf(data, k).join("").startsWith(`diff --git a/src/file-${k}.mjs `), `chunk ${k}`); + assert.equal(Array.from({ length: 12 }, (_, i) => partsOf(data, i + 1).join("")).join(""), patch); + assert.match(manifestOf(data), /^chunk 12 diff\/chunk-12\/part-001\.patch modified src\/file-12\.mjs$/m); +}); + +test("a diff past the ceiling stops, naming its size and the ceiling", () => { + const ceiling = CEILING; + const patch = Array.from({ length: ceiling + 1 }, (_, i) => sized(`f${i}`, 250000)).join(""); + const { status, outputs, stdout } = run(patch, LIMITS); + assert.equal(status, 1); + assert.equal(outputs, ""); + assert.match(stdout, new RegExp(`^::error::no verdict for this head: the diff is ${Buffer.byteLength(patch)} bytes; the diff needs ${ceiling + 1} chunks, more than ${ceiling}$`, "m")); +}); + test("the manifest names every file's part, in files of one Read each", () => { const names = Array.from({ length: 200 }, (_, i) => `src/module-${i}.mjs`); const { status, data, stdout } = run(names.map((name) => file(name, 1)).join(""), { MAX_DIFF_BYTES: "8000", PART_BYTES: "1000" }); diff --git a/.github/scripts/pr-verify/workflow.test.mjs b/.github/scripts/pr-verify/workflow.test.mjs index 03ac42beef4..a5886be2193 100644 --- a/.github/scripts/pr-verify/workflow.test.mjs +++ b/.github/scripts/pr-verify/workflow.test.mjs @@ -42,6 +42,11 @@ const list = (block, key) => { const body = new RegExp(`^ {10}${key}: \\|\\n((?: {12}.*\\n?)+)`, "m").exec(block)?.[1] ?? ""; return body.split("\n").map((line) => line.trim()).filter(Boolean); }; +// The most chunks a head is judged in: chunk-diff.sh's one MAX_CHUNKS, and +// each job holds a judge step for every one (the slot tests below). +const chunkScript = readFileSync(new URL("./chunk-diff.sh", import.meta.url), "utf8"); +const CEILING = Number(/^MAX_CHUNKS=\$\{MAX_CHUNKS:-([1-9][0-9]*)\}$/m.exec(chunkScript)?.[1]); +const SLOTS = Array.from({ length: CEILING }, (_, i) => i + 1); const PIN = /^melodic-software\/ci-workflows\/\.github\/actions\/([a-z-]+)@[0-9a-f]{40} # \S/; // The subscription job's one lane record names why the key did not judge, // in report-lane-outcome's fallback-reason (ci-workflows intake-triage.yml). @@ -99,7 +104,9 @@ const glob = (pattern) => { }; const REPO = "melodic-software/claude-code-plugins"; // A job output read from each chunk's step, chunk 1's id being `first`. -const chainOf = (build, first) => [first, `${first}-2`, `${first}-3`, `${first}-4`, `${first}-spec`].map(build).join(" || "); +const chainOf = (build, first) => [first, ...SLOTS.slice(1).map((k) => `${first}-${k}`), `${first}-spec`].map(build).join(" || "); +// Every leg check of a job left no clean handoff: "&& steps.-k.outcome != 'success'" for each slot and the spec. +const legsClear = (first) => chainOf((id) => `steps\\.${id}\\.outcome != 'success'`, first).replaceAll(" || ", " && "); const upstreamRun = (overrides = {}) => ({ github: { ref: "refs/heads/main", @@ -285,7 +292,7 @@ test("the subscription job runs only on a clean handoff or an empty key, on a fi const steps = job.slice(job.indexOf("\n steps:\n")); assert.deepEqual( steps.split("\n").filter((line) => line.includes("needs.")), - [CARRIED, ...Array(5).fill(` fallback-reason: ${FALLBACK_REASON}`)], + [CARRIED, ...Array(SLOTS.length + 1).fill(` fallback-reason: ${FALLBACK_REASON}`)], ); const key = jobText(KEY); const outputs = /^ {4}outputs:\n((?: {6}.*\n)+)/m.exec(key)?.[1]; @@ -339,7 +346,7 @@ test("the fallback job runs only on the subscription job's clean handoff, on a f const steps = job.slice(job.indexOf("\n steps:\n")); assert.deepEqual( steps.split("\n").filter((line) => line.includes("needs.")), - Array(5).fill(` fallback-reason: ${FB_REASON}`), + Array(SLOTS.length + 1).fill(` fallback-reason: ${FB_REASON}`), ); }); @@ -416,11 +423,11 @@ test("the subscription job repeats the key job's steps but for the credential", .replace("\n fallback-ready: ${{ steps.credential.outputs.fallback-ready }}", "") .replace(/PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN/g, "PR_VERIFY_CLAUDE_API_KEY") .replace(/^( {10})subscription-token: /m, "$1api-key: ") - .replace(/if: \(steps\.(judge(?:-\d|-spec)?)\.outcome == 'success' \|\| steps\.\1\.outcome == 'failure'\) && \(steps\.\1\.outputs\.handoff != 'true' \|\| steps\.(?:key|sub)-leg(?:-\d|-spec)?\.outcome == 'failure'\)/, "if: steps.$1.outcome == 'success' || steps.$1.outcome == 'failure'") - .replace(/known-failure-class: \$\{\{ steps\.(judge(?:-\d|-spec)?)\.outputs\.lane-class == 'credit-stop' && 'rate-limit' \|\| steps\.\1\.outputs\.lane-class \}\}/, "known-failure-class: ${{ steps.$1.outputs.lane-class }}") + .replace(/if: \(steps\.(judge(?:-\d+|-spec)?)\.outcome == 'success' \|\| steps\.\1\.outcome == 'failure'\) && \(steps\.\1\.outputs\.handoff != 'true' \|\| steps\.(?:key|sub)-leg(?:-\d+|-spec)?\.outcome == 'failure'\)/, "if: steps.$1.outcome == 'success' || steps.$1.outcome == 'failure'") + .replace(/known-failure-class: \$\{\{ steps\.(judge(?:-\d+|-spec)?)\.outputs\.lane-class == 'credit-stop' && 'rate-limit' \|\| steps\.\1\.outputs\.lane-class \}\}/, "known-failure-class: ${{ steps.$1.outputs.lane-class }}") .replace(`\n fallback-reason: ${FALLBACK_REASON}`, "") .replace(/ && steps\.credential\.outputs\.present != 'false'/, "") - .replace(/(?: && steps\.(?:key|sub)-leg(?:-\d|-spec)?\.outcome != 'success')+$/m, ""), + .replace(/(?: && steps\.(?:key|sub)-leg(?:-\d+|-spec)?\.outcome != 'success')+$/m, ""), ); assert.deepEqual(normalize(STEPS[SUB]), normalize(STEPS[KEY])); // The fallback job reads everything again, as the key job does; its @@ -550,11 +557,11 @@ test("the outcome is reported whenever a judge ran to a final class, and a missi // Only a hand-off spares the key job its red; an empty token fails the subscription job. assert.match( STEPS[KEY].at(-1).block, - /^ {8}if: always\(\) && steps\.agree\.outcome == 'success' && steps\.verdict\.outcome != 'success' && steps\.credential\.outputs\.present != 'false' && steps\.key-leg\.outcome != 'success' && steps\.key-leg-2\.outcome != 'success' && steps\.key-leg-3\.outcome != 'success' && steps\.key-leg-4\.outcome != 'success' && steps\.key-leg-spec\.outcome != 'success'$/m, + new RegExp(`^ {8}if: always\\(\\) && steps\\.agree\\.outcome == 'success' && steps\\.verdict\\.outcome != 'success' && steps\\.credential\\.outputs\\.present != 'false' && ${legsClear("key-leg")}$`, "m"), ); assert.match( STEPS[SUB].at(-1).block, - /^ {8}if: always\(\) && steps\.agree\.outcome == 'success' && steps\.verdict\.outcome != 'success' && steps\.sub-leg\.outcome != 'success' && steps\.sub-leg-2\.outcome != 'success' && steps\.sub-leg-3\.outcome != 'success' && steps\.sub-leg-4\.outcome != 'success' && steps\.sub-leg-spec\.outcome != 'success'$/m, + new RegExp(`^ {8}if: always\\(\\) && steps\\.agree\\.outcome == 'success' && steps\\.verdict\\.outcome != 'success' && ${legsClear("sub-leg")}$`, "m"), ); assert.match(STEPS[FB].at(-1).block, /^ {8}if: always\(\) && steps\.agree\.outcome == 'success' && steps\.verdict\.outcome != 'success'$/m); }); @@ -606,7 +613,6 @@ test("the resolver reads the trusted-actor list from main in both jobs", () => { } }); -const SLOTS = [1, 2, 3, 4]; const judgeSpec = (job) => step("Judge the whole spec", job); const SPEC_IF = "fromJSON(steps.diff.outputs.chunks || '0') > 1 && steps.chunk-results.outcome == 'success'"; const judgeId = (k) => (k === 1 ? "judge" : `judge-${k}`); @@ -616,11 +622,20 @@ const legName = { [KEY]: "Check the key leg", [SUB]: "Check the subscription leg const legId = { [KEY]: "key-leg", [SUB]: "sub-leg" }; const chain = (build) => [...SLOTS.map(build), build("spec")].join(" || "); +test("the chunk ceiling is set once, in chunk-diff.sh, and a 3 MB diff fits under it", () => { + assert.ok(Number.isInteger(CEILING), "chunk-diff.sh sets MAX_CHUNKS"); + assert.doesNotMatch(text, /MAX_CHUNKS: /, "no step sets its own"); + // 3 MB of whole files in chunks of MAX_DIFF_BYTES (chunk-diff.test.mjs splits one). + const chunkBytes = Number(input(step("Write the diff"), "MAX_DIFF_BYTES").replaceAll('"', "")); + assert.ok(CEILING * chunkBytes >= 3000000, `${CEILING} chunks of ${chunkBytes} bytes`); +}); + test("every job has one judge step per chunk, each repeating the first but for its chunk", () => { for (const job of JOBS) { const first = step("Judge the pull request", job); assert.equal(STEPS[job].filter((s) => /^ {8}uses: melodic-software\/ci-workflows\/\.github\/actions\/run-claude-cli@/m.test(s.block)).length, SLOTS.length + 1, job); - assert.equal(input(step("Write the diff", job), "MAX_CHUNKS"), `"${SLOTS.length}"`, job); + // The ceiling is chunk-diff.sh's; no step sets its own. + assert.equal(input(step("Write the diff", job), "MAX_CHUNKS"), undefined, job); assert.equal(input(first, "prompt-file"), "${{ runner.temp }}/pr-verify-prompt-1.md", job); const bare = (block) => block.split("\n").filter((line) => !line.trim().startsWith("#")).filter((line) => !/^ +(- )?(name|id|if): /.test(line)).join("\n").trim(); for (const k of SLOTS.slice(1)) { @@ -718,6 +733,12 @@ test("the prompt step gives each chunk its own parts, and a lone chunk no note", assert.match(prompt(1), /chunk 1 of 2/); assert.match(prompt(2), /chunk 2 of 2/); assert.ok(prompt(2).includes(`chunk-2/part-001.patch`)); + // Past nine chunks, each chunk still gets its own prompt and parts. + const many = write(12, Array.from({ length: 12 }, () => 1)); + assert.match(many.prompt(12), /chunk 12 of 12/); + assert.ok(many.prompt(12).includes(` - \`${many.cwd}/data/diff/chunk-12/part-001.patch\`\n`)); + assert.ok(!many.prompt(12).includes("chunk-1/part")); + assert.ok(many.prompt(10).includes(`chunk-10/part-001.patch`)); const one = write(1, [1]); assert.ok(!one.prompt(1).includes("data/manifest")); assert.ok(prompt(1).includes(`${cwd}/data/diff/chunk-N`) || prompt(1).includes(`relative to \`${cwd}/data\``)); @@ -750,7 +771,7 @@ test("the whole-spec step runs only on a chunked head, after every chunk, and ca const denied = list(judge, "disallowed-tools"); assert.ok(denied.includes("Read(./pr-verify-data/diff/**)"), job); assert.deepEqual(denied.filter((rule) => rule !== "Read(./pr-verify-data/diff/**)"), list(step("Judge the pull request", job), "disallowed-tools"), job); - assert.ok(index("Write the chunk results", job) > index("Upload chunk 4's sealed transcript", job), job); + assert.ok(index("Write the chunk results", job) > index(`Upload chunk ${SLOTS.length}'s sealed transcript`, job), job); assert.ok(index("Upload the spec judgment's sealed transcript", job) < index("Write the verdict", job), job); assert.equal(input(step("Report the spec judgment's outcome", job), "step-started-at"), "${{ steps.time-spec.outputs.started-at }}", job); assert.equal(input(step("Upload the spec judgment's sealed transcript", job), "job-id"), "${{ github.job }}-spec", job); @@ -784,7 +805,7 @@ test("after each judge a counted fail stops the slots and reaches the verdict st assert.match(check, /write-verdict\.mjs chunk-fail$/m, job); const name = k === 1 ? "Judge the pull request" : `Judge chunk ${k}`; assert.ok(index(`Read chunk ${k}'s verdict`, job) > index(name, job), job); - if (k < 4) assert.ok(index(`Read chunk ${k}'s verdict`, job) < index(`Time chunk ${k + 1}`, job), job); + if (k < SLOTS.length) assert.ok(index(`Read chunk ${k}'s verdict`, job) < index(`Time chunk ${k + 1}`, job), job); } assert.match(step("Summarize the verdict", job), /^ {8}if: \$\{\{ !cancelled\(\) && steps\.verdict\.outcome == 'success' \}\}$/m, job); } diff --git a/.github/scripts/pr-verify/write-verdict.mjs b/.github/scripts/pr-verify/write-verdict.mjs index 947945d5e6f..7d18e854415 100644 --- a/.github/scripts/pr-verify/write-verdict.mjs +++ b/.github/scripts/pr-verify/write-verdict.mjs @@ -524,9 +524,9 @@ export function checkChunk(env = process.env) { // Each chunk by position: `output`, or `error` for a chunk that did not count. function judgedChunks(env) { - const chunkDirs = readdirSync(env.DIFF_DIR).filter((name) => /^chunk-[1-9]$/.test(name)); + const chunkDirs = readdirSync(env.DIFF_DIR).filter((name) => /^chunk-[1-9][0-9]?$/.test(name)); const count = Number(env.CHUNKS); - if (!/^[1-9]$/.test(env.CHUNKS) || chunkDirs.length !== count) { + if (!/^[1-9][0-9]?$/.test(env.CHUNKS) || chunkDirs.length !== count) { throw new Error(`the chunk count does not match ${chunkDirs.length} chunk(s) of the diff`); } const executions = positions(env.EXECUTION_FILES); diff --git a/.github/scripts/pr-verify/write-verdict.test.mjs b/.github/scripts/pr-verify/write-verdict.test.mjs index b7974e0ce6a..da17b1c6ebc 100644 --- a/.github/scripts/pr-verify/write-verdict.test.mjs +++ b/.github/scripts/pr-verify/write-verdict.test.mjs @@ -647,6 +647,87 @@ test("main writes one verdict from every chunk, and none when a chunk leaves non } }); +test("a head of twelve chunks gets a verdict from every chunk, and none when one leaves none", () => { + const COUNT = 12; + const dir = tempDir("pr-verify-many-chunks-"); + const spec = path.join(dir, "spec.json"); + writeFileSync(spec, JSON.stringify([ISSUE])); + const diff = path.join(dir, "diff"); + const parts = Array.from({ length: COUNT }, (_, i) => { + mkdirSync(path.join(diff, `chunk-${i + 1}`), { recursive: true }); + const part = path.join(diff, `chunk-${i + 1}`, "part-001.patch"); + writeFileSync(part, `diff --git a/${i + 1} b/${i + 1}\n`); + return part; + }); + const manifestDir = path.join(dir, "manifest"); + mkdirSync(manifestDir); + const manifest = path.join(manifestDir, "part-001.txt"); + writeFileSync(manifest, "chunk 1 diff/chunk-1/part-001.patch modified 1\n"); + const resultsDir = path.join(dir, "results"); + mkdirSync(resultsDir); + const results = parts.map((_, i) => { + const file = path.join(resultsDir, `chunk-${i + 1}.json`); + writeFileSync(file, "{}"); + return file; + }); + let id = 0; + const write = (name, value) => { + const file = path.join(dir, `${name}-${(id += 1)}`); + writeFileSync(file, value); + return file; + }; + const execution = (read, output) => + write( + "execution.json", + JSON.stringify([ + { type: "assistant", message: { content: read.map((file, i) => ({ type: "tool_use", id: `t${i}`, name: "Read", input: { file_path: file } })) } }, + { type: "result", subtype: "success", structured_output: output }, + ]), + ); + const log = () => write("log.jsonl", JSON.stringify({ leg: 1, denials: { count: 0 } })); + const run = (label, outputs) => ({ + PR_NUMBER: "41", + HEAD_SHA: HEAD, + BASE_SHA: BASE, + HEAD_REF: "feat/x", + SPEC_FILE: spec, + GITHUB_RUN_ID: "9001", + CHUNKS: String(COUNT), + DIFF_DIR: diff, + VERDICT_DIR: path.join(dir, `verdict-${label}`), + GITHUB_OUTPUT: path.join(dir, `out-${label}`), + EXECUTION_FILES: `${outputs.map((output, i) => (output === null ? "" : execution([parts[i], manifest], output))).join("\n")}\n`, + LOG_FILES: `${outputs.map((output) => (output === null ? "" : log())).join("\n")}\n`, + FINAL_EXECUTION_FILE: execution([...results, spec, manifest], { ...FINAL_PASS, items: [{ item: "item 1", chunks: [12] }] }), + FINAL_LOG_FILE: log(), + RESULTS_DIR: resultsDir, + MANIFEST_DIR: manifestDir, + }); + const verdictOf = (env) => JSON.parse(readFileSync(path.join(env.VERDICT_DIR, "verdict.json"), "utf8")); + const all = (output) => Array.from({ length: COUNT }, () => output); + + const passed = run("pass", all(CHUNK_PASS)); + main(passed); + assert.equal(verdictOf(passed).verdict, "pass"); + assert.match(readFileSync(passed.GITHUB_OUTPUT, "utf8"), new RegExp(`^name=pr-verify-41-${HEAD}-pass\n`)); + assert.match(verdictOf(passed).summary, /^Chunk 12 of 12, pass: chunk fine$/m); + + const failing = all(CHUNK_PASS); + failing[10] = CHUNK_FAIL; + const failed = run("fail", failing); + main(failed); + assert.equal(verdictOf(failed).verdict, "fail"); + assert.deepEqual(verdictOf(failed).notes, CHUNK_FAIL.notes); + assert.match(verdictOf(failed).summary, /^Chunk 11 of 12, fail: chunk broken$/m); + + const missing = all(CHUNK_PASS); + missing[10] = null; + const none = run("none", missing); + assert.throws(() => main(none), /chunk 11 of 12/); + assert.equal(existsSync(path.join(none.VERDICT_DIR, "verdict.json")), false); + assert.equal(existsSync(none.GITHUB_OUTPUT), false); +}); + test("checkKeyLeg reads every chunk's log, so a denial in an earlier chunk stops the handoff", () => { const dir = tempDir("pr-verify-key-legs-"); const file = (name, count) => { diff --git a/.github/workflows/pr-verify.yml b/.github/workflows/pr-verify.yml index 3355adbaa01..7fa19712a77 100644 --- a/.github/workflows/pr-verify.yml +++ b/.github/workflows/pr-verify.yml @@ -92,16 +92,16 @@ jobs: && (github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure') && github.event.workflow_run.head_repository.full_name == github.repository runs-on: ubuntu-24.04 - timeout-minutes: 125 + timeout-minutes: 301 permissions: contents: read # check out main; read the PR's commits for the diff pull-requests: read # the PR, its comments and reviews actions: read # the upstream run, and verdicts already uploaded outputs: - lane-class: ${{ steps.judge.outputs.lane-class || steps.judge-2.outputs.lane-class || steps.judge-3.outputs.lane-class || steps.judge-4.outputs.lane-class || steps.judge-spec.outputs.lane-class }} - handoff: ${{ steps.judge.outputs.handoff == 'true' || steps.judge-2.outputs.handoff == 'true' || steps.judge-3.outputs.handoff == 'true' || steps.judge-4.outputs.handoff == 'true' || steps.judge-spec.outputs.handoff == 'true' }} + lane-class: ${{ steps.judge.outputs.lane-class || steps.judge-2.outputs.lane-class || steps.judge-3.outputs.lane-class || steps.judge-4.outputs.lane-class || steps.judge-5.outputs.lane-class || steps.judge-6.outputs.lane-class || steps.judge-7.outputs.lane-class || steps.judge-8.outputs.lane-class || steps.judge-9.outputs.lane-class || steps.judge-10.outputs.lane-class || steps.judge-11.outputs.lane-class || steps.judge-12.outputs.lane-class || steps.judge-spec.outputs.lane-class }} + handoff: ${{ steps.judge.outputs.handoff == 'true' || steps.judge-2.outputs.handoff == 'true' || steps.judge-3.outputs.handoff == 'true' || steps.judge-4.outputs.handoff == 'true' || steps.judge-5.outputs.handoff == 'true' || steps.judge-6.outputs.handoff == 'true' || steps.judge-7.outputs.handoff == 'true' || steps.judge-8.outputs.handoff == 'true' || steps.judge-9.outputs.handoff == 'true' || steps.judge-10.outputs.handoff == 'true' || steps.judge-11.outputs.handoff == 'true' || steps.judge-12.outputs.handoff == 'true' || steps.judge-spec.outputs.handoff == 'true' }} credential-present: ${{ steps.credential.outputs.present }} - key-leg-clean: ${{ steps.key-leg.outputs.clean || steps.key-leg-2.outputs.clean || steps.key-leg-3.outputs.clean || steps.key-leg-4.outputs.clean || steps.key-leg-spec.outputs.clean }} + key-leg-clean: ${{ steps.key-leg.outputs.clean || steps.key-leg-2.outputs.clean || steps.key-leg-3.outputs.clean || steps.key-leg-4.outputs.clean || steps.key-leg-5.outputs.clean || steps.key-leg-6.outputs.clean || steps.key-leg-7.outputs.clean || steps.key-leg-8.outputs.clean || steps.key-leg-9.outputs.clean || steps.key-leg-10.outputs.clean || steps.key-leg-11.outputs.clean || steps.key-leg-12.outputs.clean || steps.key-leg-spec.outputs.clean }} reads: ${{ steps.carry.outputs.reads }} steps: - name: Check out the default branch tip @@ -206,8 +206,10 @@ jobs: # the workspace. git diff reads no attributes or diff drivers from it, # so nothing in the PR runs; the patch is a data file. chunk-diff.sh cuts # it into chunks of whole files, and each chunk into parts one Read - # returns whole. A file over one chunk, or a diff over MAX_CHUNKS chunks, - # fails the step by name: no verdict, so no fail on size alone. + # returns whole, in as many chunks as the diff takes, up to the ceiling + # chunk-diff.sh sets, with a judge step below for each. A file over one + # chunk, or a diff over the ceiling, fails the step by name: no verdict, + # so no fail on size alone. - name: Write the diff id: diff if: steps.agree.outcome == 'success' @@ -220,9 +222,6 @@ jobs: # About 75k tokens of patch, the budget pr-review-main.yml sets: room # in the review profile's turns to read one chunk whole (judgment). MAX_DIFF_BYTES: "300000" - # The most chunks: the job's judge steps (workflow.test.mjs holds - # them equal). - MAX_CHUNKS: "4" # One Read returns at most 25,000 tokens # (https://code.claude.com/docs/en/tools-reference#large-files), and # a token is at least a byte, so a part this size comes back whole. @@ -325,7 +324,7 @@ jobs: echo "::error::the verify-pr profile's model is default, which run-claude-cli cannot pass; name an alias" exit 1 fi - if ! [[ $CHUNKS =~ ^[1-9]$ ]]; then + if ! [[ $CHUNKS =~ ^[1-9][0-9]?$ ]]; then echo "::error::the chunk count is malformed" exit 1 fi @@ -363,9 +362,9 @@ jobs: # verdict steps, and a clean one the outcome step too: the subscription # job judges and records the run instead. # One leg of leg-timeout-minutes and the CLI install fit in - # timeout-minutes, which the outcome step repeats, and five of those fit in - # the job's 125. The model may share its job with the upload: it has Read - # alone and the job token can only read. + # timeout-minutes, which the outcome step repeats, and the job's timeout + # holds one of those for every judge step. The model may share its job + # with the upload: it has Read alone and the job token can only read. # Under dontAsk only the allow rule's directory is readable; the deny # rules hold if the tools or the working directory change. - name: Judge the pull request @@ -773,46 +772,115 @@ jobs: MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail - # A diff in chunks: one more judge, with no diff, reads the spec and every - # chunk's checked result and fails an item no chunk delivers. The results - # are model text, checked as any verdict is and read as data; a chunk that - # fails the check fails here, so the head gets no verdict. The deny rule - # keeps this judge off the diff, and a denial voids the verdict. - - name: Write the chunk results - id: chunk-results - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') > 1 && steps.judge.outcome == 'success' && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-2.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 3 || steps.judge-3.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 4 || steps.judge-4.outcome == 'success') && steps.chunk-fail.outputs.fail != 'true' && steps.chunk-fail-2.outputs.fail != 'true' && steps.chunk-fail-3.outputs.fail != 'true' && steps.chunk-fail-4.outputs.fail != 'true' + + - name: Time chunk 5 + id: time-5 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 5 && steps.judge-4.outcome == 'success' && steps.judge-4.outputs.handoff != 'true' && steps.chunk-fail-4.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 5 + id: judge-5 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 5 && steps.judge-4.outcome == 'success' && steps.judge-4.outputs.handoff != 'true' && steps.chunk-fail-4.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-5.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + api-key: ${{ secrets.PR_VERIFY_CLAUDE_API_KEY }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + leg-timeout-minutes: "15" + + - name: Check the key leg, chunk 5 + id: key-leg-5 + if: steps.judge-5.outputs.handoff == 'true' + continue-on-error: true env: - EXECUTION_FILES: | - ${{ steps.judge.outputs.execution-file }} - ${{ steps.judge-2.outputs.execution-file }} - ${{ steps.judge-3.outputs.execution-file }} - ${{ steps.judge-4.outputs.execution-file }} LOG_FILES: | ${{ steps.judge.outputs.log-file }} ${{ steps.judge-2.outputs.log-file }} ${{ steps.judge-3.outputs.log-file }} ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + LANE_CLASS: ${{ steps.judge-5.outputs.lane-class }} + run: | + case "$LANE_CLASS" in + auth) echo "::warning::the API refused the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY), class auth; the owner must replace the secret" ;; + *) echo "::warning::the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY) hit a credit stop; rotate a spare key in" ;; + esac + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::check-intent-subscription judges on the subscription token" + + - name: Report chunk 5's outcome + if: (steps.judge-5.outcome == 'success' || steps.judge-5.outcome == 'failure') && (steps.judge-5.outputs.handoff != 'true' || steps.key-leg-5.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-5.outcome }} + execution-file: ${{ steps.judge-5.outputs.execution-file }} + log-file: ${{ steps.judge-5.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-5.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-5.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-5.outputs.lane-class == 'credit-stop' && 'rate-limit' || steps.judge-5.outputs.lane-class }} + + - name: Upload chunk 5's sealed transcript + if: always() && steps.judge-5.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-5.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-5 + + - name: Read chunk 5's verdict + id: chunk-fail-5 + if: ${{ !cancelled() && steps.judge-5.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-5.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-5.outputs.log-file }} + CHUNK: "5" CHUNKS: ${{ steps.diff.outputs.chunks }} DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff - RESULTS_DIR: ${{ github.workspace }}/pr-verify-data/chunk-results - SPEC_TEXT_FILE: ${{ github.workspace }}/pr-verify-data/spec-text.json - SPEC_FILE: ${{ github.workspace }}/pr-verify-data/spec.json - CONTEXT_FILE: ${{ github.workspace }}/pr-verify-data/context.json - PR_FILE: ${{ github.workspace }}/pr-verify-data/pr.json MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest - PROMPT_FILE: ${{ runner.temp }}/pr-verify-prompt-spec.md - PROMPT_TEMPLATE: .base/.github/scripts/pr-verify/spec-prompt.md - SCHEMA_FILE: ${{ runner.temp }}/pr-verify-spec-schema.json - run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-results + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail - - name: Time the spec judgment - id: time-spec - if: fromJSON(steps.diff.outputs.chunks || '0') > 1 && steps.chunk-results.outcome == 'success' + - name: Time chunk 6 + id: time-6 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 6 && steps.judge-5.outcome == 'success' && steps.judge-5.outputs.handoff != 'true' && steps.chunk-fail-5.outputs.fail != 'true' run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" - - name: Judge the whole spec - id: judge-spec - if: fromJSON(steps.diff.outputs.chunks || '0') > 1 && steps.chunk-results.outcome == 'success' + - name: Judge chunk 6 + id: judge-6 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 6 && steps.judge-5.outcome == 'success' && steps.judge-5.outputs.handoff != 'true' && steps.chunk-fail-5.outputs.fail != 'true' continue-on-error: true timeout-minutes: 22 uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 @@ -821,7 +889,7 @@ jobs: with: lane: pr-verify role: pr-verify - prompt-file: ${{ runner.temp }}/pr-verify-prompt-spec.md + prompt-file: ${{ runner.temp }}/pr-verify-prompt-6.md preamble: >- You run unattended; return only the verdict the schema asks for; nothing you write is posted. @@ -834,7 +902,6 @@ jobs: allowed-tools: Read(./pr-verify-data/**) disallowed-tools: | Read(./.base/**) - Read(./pr-verify-data/diff/**) Glob Grep Bash @@ -845,22 +912,22 @@ jobs: WebSearch Agent mcp__* - json-schema-file: ${{ runner.temp }}/pr-verify-spec-schema.json + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json leg-timeout-minutes: "15" - - name: Check the key leg, whole spec - id: key-leg-spec - if: steps.judge-spec.outputs.handoff == 'true' + - name: Check the key leg, chunk 6 + id: key-leg-6 + if: steps.judge-6.outputs.handoff == 'true' continue-on-error: true env: - CHUNKS: ${{ steps.diff.outputs.chunks }} LOG_FILES: | ${{ steps.judge.outputs.log-file }} ${{ steps.judge-2.outputs.log-file }} ${{ steps.judge-3.outputs.log-file }} ${{ steps.judge-4.outputs.log-file }} - ${{ steps.judge-spec.outputs.log-file }} - LANE_CLASS: ${{ steps.judge-spec.outputs.lane-class }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + LANE_CLASS: ${{ steps.judge-6.outputs.lane-class }} run: | case "$LANE_CLASS" in auth) echo "::warning::the API refused the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY), class auth; the owner must replace the secret" ;; @@ -869,410 +936,167 @@ jobs: node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg echo "::notice::check-intent-subscription judges on the subscription token" - - name: Report the spec judgment's outcome - if: (steps.judge-spec.outcome == 'success' || steps.judge-spec.outcome == 'failure') && (steps.judge-spec.outputs.handoff != 'true' || steps.key-leg-spec.outcome == 'failure') + - name: Report chunk 6's outcome + if: (steps.judge-6.outcome == 'success' || steps.judge-6.outcome == 'failure') && (steps.judge-6.outputs.handoff != 'true' || steps.key-leg-6.outcome == 'failure') continue-on-error: true uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 with: - outcome: ${{ steps.judge-spec.outcome }} - execution-file: ${{ steps.judge-spec.outputs.execution-file }} - log-file: ${{ steps.judge-spec.outputs.log-file }} + outcome: ${{ steps.judge-6.outcome }} + execution-file: ${{ steps.judge-6.outputs.execution-file }} + log-file: ${{ steps.judge-6.outputs.log-file }} lane: pr-verify lane-id: pr-verify - step-started-at: ${{ steps.time-spec.outputs.started-at }} + step-started-at: ${{ steps.time-6.outputs.started-at }} step-timeout-minutes: 22 - credential: ${{ steps.judge-spec.outputs.credential }} + credential: ${{ steps.judge-6.outputs.credential }} max-turns: ${{ steps.models.outputs.max-turns }} - known-failure-class: ${{ steps.judge-spec.outputs.lane-class == 'credit-stop' && 'rate-limit' || steps.judge-spec.outputs.lane-class }} + known-failure-class: ${{ steps.judge-6.outputs.lane-class == 'credit-stop' && 'rate-limit' || steps.judge-6.outputs.lane-class }} - - name: Upload the spec judgment's sealed transcript - if: always() && steps.judge-spec.outputs.execution-file != '' + - name: Upload chunk 6's sealed transcript + if: always() && steps.judge-6.outputs.execution-file != '' continue-on-error: true uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] with: - execution-file: ${{ steps.judge-spec.outputs.execution-file }} + execution-file: ${{ steps.judge-6.outputs.execution-file }} public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} - job-id: ${{ github.job }}-spec + job-id: ${{ github.job }}-6 - # The structured output, in the last result message of the execution - # file, is untrusted model text: read from the file and checked by the - # script, never interpolated into a command. - - name: Write the verdict - id: verdict - if: ${{ !cancelled() && (steps.chunk-fail.outputs.fail == 'true' || steps.chunk-fail-2.outputs.fail == 'true' || steps.chunk-fail-3.outputs.fail == 'true' || steps.chunk-fail-4.outputs.fail == 'true' || steps.chunk-results.outcome == 'failure' || (steps.judge.outcome == 'success' && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-2.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 3 || steps.judge-3.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 4 || steps.judge-4.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-spec.outcome == 'success'))) }} + - name: Read chunk 6's verdict + id: chunk-fail-6 + if: ${{ !cancelled() && steps.judge-6.outcome == 'success' }} + continue-on-error: true env: - # Each chunk's files in chunk order. A counted fail needs no others; - # anything else needs every chunk and the final judge. - EXECUTION_FILES: | - ${{ steps.judge.outputs.execution-file }} - ${{ steps.judge-2.outputs.execution-file }} - ${{ steps.judge-3.outputs.execution-file }} - ${{ steps.judge-4.outputs.execution-file }} - LOG_FILES: | - ${{ steps.judge.outputs.log-file }} - ${{ steps.judge-2.outputs.log-file }} - ${{ steps.judge-3.outputs.log-file }} - ${{ steps.judge-4.outputs.log-file }} + EXECUTION_FILE: ${{ steps.judge-6.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-6.outputs.log-file }} + CHUNK: "6" CHUNKS: ${{ steps.diff.outputs.chunks }} DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff - # The whole-spec judgment, which a head of two chunks or more must have. - FINAL_EXECUTION_FILE: ${{ steps.judge-spec.outputs.execution-file }} - FINAL_LOG_FILE: ${{ steps.judge-spec.outputs.log-file }} - RESULTS_DIR: ${{ github.workspace }}/pr-verify-data/chunk-results MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest - PR_NUMBER: ${{ steps.pr.outputs.pr-number }} - HEAD_SHA: ${{ steps.pr.outputs.head-sha }} - BASE_SHA: ${{ steps.pr.outputs.base-sha }} - HEAD_REF: ${{ steps.pr.outputs.head-ref }} - RELINK_SINCE_HEAD: ${{ steps.pr.outputs.relink-since-head }} - SPEC_FILE: ${{ github.workspace }}/pr-verify-data/spec.json - SPEC_TEXT_FILE: ${{ github.workspace }}/pr-verify-data/spec-text.json - VERDICT_DIR: ${{ runner.temp }}/pr-verify-verdict - run: node .base/.github/scripts/pr-verify/write-verdict.mjs + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail - - name: Upload the verdict - if: ${{ !cancelled() && steps.verdict.outcome == 'success' }} - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + - name: Time chunk 7 + id: time-7 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 7 && steps.judge-6.outcome == 'success' && steps.judge-6.outputs.handoff != 'true' && steps.chunk-fail-6.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 7 + id: judge-7 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 7 && steps.judge-6.outcome == 'success' && steps.judge-6.outputs.handoff != 'true' && steps.chunk-fail-6.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: - name: ${{ steps.verdict.outputs.name }} - path: ${{ runner.temp }}/pr-verify-verdict/verdict.json - retention-days: ${{ steps.verdict.outputs.retention-days }} - if-no-files-found: error + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-7.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + api-key: ${{ secrets.PR_VERIFY_CLAUDE_API_KEY }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + leg-timeout-minutes: "15" - - name: Summarize the verdict - if: ${{ !cancelled() && steps.verdict.outcome == 'success' }} + - name: Check the key leg, chunk 7 + id: key-leg-7 + if: steps.judge-7.outputs.handoff == 'true' + continue-on-error: true env: - VERDICT_FILE: ${{ runner.temp }}/pr-verify-verdict/verdict.json + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + LANE_CLASS: ${{ steps.judge-7.outputs.lane-class }} run: | - { - echo "### Verifier verdict" - echo - echo '```json' - jq '.summary |= gsub("`"; "'"'"'") | .notes |= map(gsub("`"; "'"'"'"))' "$VERDICT_FILE" - echo '```' - } >>"$GITHUB_STEP_SUMMARY" + case "$LANE_CLASS" in + auth) echo "::warning::the API refused the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY), class auth; the owner must replace the secret" ;; + *) echo "::warning::the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY) hit a credit stop; rotate a spare key in" ;; + esac + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::check-intent-subscription judges on the subscription token" - - name: Log the API budget - if: always() + - name: Report chunk 7's outcome + if: (steps.judge-7.outcome == 'success' || steps.judge-7.outcome == 'failure') && (steps.judge-7.outputs.handoff != 'true' || steps.key-leg-7.outcome == 'failure') continue-on-error: true - timeout-minutes: 1 - uses: ./.base/.github/actions/log-api-budget # zizmor: ignore[self-repository] + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 with: - tokens: github-token - - # A clean handoff or an empty key hands the judgment on; any other - # run that passed the gates and left no verdict fails here. - - name: Fail without a verdict - if: always() && steps.agree.outcome == 'success' && steps.verdict.outcome != 'success' && steps.credential.outputs.present != 'false' && steps.key-leg.outcome != 'success' && steps.key-leg-2.outcome != 'success' && steps.key-leg-3.outcome != 'success' && steps.key-leg-4.outcome != 'success' && steps.key-leg-spec.outcome != 'success' - run: | - echo "::error::the judgment left no verdict for this head" - exit 1 + outcome: ${{ steps.judge-7.outcome }} + execution-file: ${{ steps.judge-7.outputs.execution-file }} + log-file: ${{ steps.judge-7.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-7.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-7.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-7.outputs.lane-class == 'credit-stop' && 'rate-limit' || steps.judge-7.outputs.lane-class }} - # The subscription leg, as its own job on a new VM: check-intent's - # processes and environment do not reach it, and of its files only the - # pre-judge reads do, through the `reads` output. Its steps repeat - # check-intent's from a fresh checkout but for the credential, the - # key-leg check and the reads it takes from check-intent's `reads` output - # (workflow.test.mjs holds them equal), so the trigger gate, the kill - # switch and the hardening run again. Only this job references the - # subscription token. needs adds success(): a check-intent that failed, a - # denial on its key leg included, leaves this job skipped. - check-intent-subscription: - needs: check-intent - if: >- - github.ref == 'refs/heads/main' - && github.event.workflow_run.event == 'pull_request' - && (github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure') - && github.event.workflow_run.head_repository.full_name == github.repository - && ((needs.check-intent.outputs.handoff == 'true' - && needs.check-intent.outputs.key-leg-clean == 'true') - || needs.check-intent.outputs.credential-present == 'false') - runs-on: ubuntu-24.04 - timeout-minutes: 125 - permissions: - contents: read # check out main; read the PR's commits for the diff - pull-requests: read # the PR, its comments and reviews - actions: read # the upstream run, and verdicts already uploaded - outputs: - handoff: ${{ steps.judge.outputs.handoff == 'true' || steps.judge-2.outputs.handoff == 'true' || steps.judge-3.outputs.handoff == 'true' || steps.judge-4.outputs.handoff == 'true' || steps.judge-spec.outputs.handoff == 'true' }} - handoff-reason: ${{ steps.judge.outputs.handoff-reason || steps.judge-2.outputs.handoff-reason || steps.judge-3.outputs.handoff-reason || steps.judge-4.outputs.handoff-reason || steps.judge-spec.outputs.handoff-reason }} - leg-clean: ${{ steps.sub-leg.outputs.clean || steps.sub-leg-2.outputs.clean || steps.sub-leg-3.outputs.clean || steps.sub-leg-4.outputs.clean || steps.sub-leg-spec.outputs.clean }} - steps: - - name: Check out the default branch tip - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Upload chunk 7's sealed transcript + if: always() && steps.judge-7.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] with: - ref: refs/heads/main - path: .base - persist-credentials: false + execution-file: ${{ steps.judge-7.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-7 - - name: Set up Node - uses: ./.base/.github/actions/setup-pinned-node # zizmor: ignore[self-repository] - with: - node-version-file: .base/.node-version - - - name: Check the kill switch - id: kill - uses: ./.base/.github/actions/check-kill-switch # zizmor: ignore[self-repository] - with: - value: ${{ vars.CLAUDE_LANES_DISABLED }} - repo-value: ${{ vars.CLAUDE_LANES_REPO_DISABLED }} - - # The run fails, so the lost-run reconciler re-runs it once the switch - # is off and the head is judged then. - - name: Report the kill switch - if: steps.kill.outputs.proceed != 'true' - env: - REASON: ${{ steps.kill.outputs.reason }} - run: | - echo "::error::the lanes kill switch stops this run (reason: ${REASON:-unreadable}); no verdict until the lost-run reconciler re-runs it" - exit 1 - - # The resolver reads only, and skips on its own reads every run it - # would not judge (a head already judged or moved past, a draft, a run - # red for anything an approval does not lift, an author it does not - # judge), so it runs first and the gate's reads go only to a run it - # would judge. Nothing it writes reaches the model before the gate and - # the agree step pass. - - name: Resolve the pull request - id: pr - if: steps.kill.outputs.proceed == 'true' - env: - GITHUB_TOKEN: ${{ github.token }} - API_BUDGET_TOKEN: github-token - UPSTREAM_RUN_ID: ${{ github.event.workflow_run.id }} - SPEC_FILE: ${{ github.workspace }}/pr-verify-data/spec.json - SPEC_TEXT_FILE: ${{ github.workspace }}/pr-verify-data/spec-text.json - TRUSTED_ACTORS_PATH: .base/.github/standards/trusted-actors/trusted-actors.json - CONTEXT_FILE: ${{ github.workspace }}/pr-verify-data/context.json - # check-intent's reads, taken only when they hold (resolve-pr.mjs takeReads). - CARRIED: ${{ needs.check-intent.outputs.reads }} - run: | - mkdir -p "$(dirname "$SPEC_FILE")" - VERIFY_AUTHORS=$(yq '.merge."verify-authors" // "lane"' .base/docs/conventions/pr-pipeline.yaml) - LANE_IDENTITY=$(yq '.merge."agent-identity" // ""' .base/docs/conventions/pr-pipeline.yaml) - SPEC_FROM_BODY_APPS=$(yq '.adoption."spec-from-body-apps" // [] | .[]' .base/docs/conventions/pr-pipeline.yaml) - # The issue lane's web hosts as admission reads them (resolve-config issueLaneWebHostsFromBase). - ISSUE_LANE_WEB_HOSTS=$(yq -o=json -I=0 '(.activities."resolve-issue"."web-access" | select(.enabled == true) | .domains) // []' .base/docs/conventions/pr-pipeline.yaml) - export VERIFY_AUTHORS LANE_IDENTITY SPEC_FROM_BODY_APPS ISSUE_LANE_WEB_HOSTS - node .base/.github/scripts/pr-verify/resolve-pr.mjs - - # The upstream run's actors, the re-runner and the PR author must all be - # on the trusted-actor list. A re-run github-actions[bot] started passes - # only as the lost-run reconciler's recorded re-run of this run. - - name: Check the trigger - id: gate - if: steps.pr.outputs.proceed == 'true' - # Each API call keeps its rate-limit reading for the api-budget - # notice the job logs before its last step. - env: - API_BUDGET_TOKEN: github-token - uses: ./.base/.github/actions/check-trusted-trigger # zizmor: ignore[self-repository] - with: - trusted-actors-path: .base/.github/standards/trusted-actors/trusted-actors.json - triggering-actor: ${{ github.triggering_actor }} - reconciler-workflow: .github/workflows/maintenance-reconcile-lost-runs.yml - ci-status-rerun-workflow: .github/workflows/pr-feedback-dispatch.yml - - # The gate and the resolver read the PR separately; both must name the - # same PR at the run's head, or nothing is judged. - - name: Assert the gate and the resolver agree - id: agree - if: steps.gate.outputs.proceed == 'true' - env: - GATE_PR: ${{ steps.gate.outputs.pr-number }} - GATE_HEAD: ${{ steps.gate.outputs.head-sha }} - PR_NUMBER: ${{ steps.pr.outputs.pr-number }} - HEAD_SHA: ${{ steps.pr.outputs.head-sha }} - run: | - if [ "$GATE_PR" != "$PR_NUMBER" ] || [ "$GATE_HEAD" != "$HEAD_SHA" ]; then - echo "::error::the trigger gate and the resolver name different pull requests or heads" - exit 1 - fi - - # Carried from check-intent when the resolver took its reads. - - name: Select the trusted PR text - if: steps.agree.outcome == 'success' && steps.pr.outputs.carried != 'true' - env: - API_BUDGET_TOKEN: github-token - uses: ./.base/.github/actions/select-trusted-text # zizmor: ignore[self-repository] - with: - pr-number: ${{ steps.pr.outputs.pr-number }} - trusted-actors-path: .base/.github/standards/trusted-actors/trusted-actors.json - output-path: ${{ github.workspace }}/pr-verify-data/context.json - github-token: ${{ github.token }} - - # A blobless fetch of the two commits into its own repository outside - # the workspace. git diff reads no attributes or diff drivers from it, - # so nothing in the PR runs; the patch is a data file. chunk-diff.sh cuts - # it into chunks of whole files, and each chunk into parts one Read - # returns whole. A file over one chunk, or a diff over MAX_CHUNKS chunks, - # fails the step by name: no verdict, so no fail on size alone. - - name: Write the diff - id: diff - if: steps.agree.outcome == 'success' - env: - HEAD_SHA: ${{ steps.pr.outputs.head-sha }} - BASE_SHA: ${{ steps.pr.outputs.base-sha }} - PR_NUMBER: ${{ steps.pr.outputs.pr-number }} - DATA: ${{ github.workspace }}/pr-verify-data - SRC: ${{ runner.temp }}/pr-verify-src - # About 75k tokens of patch, the budget pr-review-main.yml sets: room - # in the review profile's turns to read one chunk whole (judgment). - MAX_DIFF_BYTES: "300000" - # The most chunks: the job's judge steps (workflow.test.mjs holds - # them equal). - MAX_CHUNKS: "4" - # One Read returns at most 25,000 tokens - # (https://code.claude.com/docs/en/tools-reference#large-files), and - # a token is at least a byte, so a part this size comes back whole. - PART_BYTES: "15000" - PART_LINES: "1000" - run: | - export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null - mkdir -p "$DATA" - git init -q "$SRC" - git -C "$SRC" remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" - git -C "$SRC" fetch -q --no-tags --filter=blob:none origin "$BASE_SHA" "$HEAD_SHA" - git -C "$SRC" diff --no-ext-diff --no-textconv --no-color "$BASE_SHA...$HEAD_SHA" >"$DATA/diff.patch" - jq -n --argjson pr "$PR_NUMBER" --arg head "$HEAD_SHA" --arg base "$BASE_SHA" \ - '{pr: $pr, head_sha: $head, base_sha: $base}' >"$DATA/pr.json" - echo "diff: $(wc -l <"$DATA/diff.patch") lines" - bash .base/.github/scripts/pr-verify/chunk-diff.sh - - # The jobs of the run this one was started by, each with its name and - # conclusion, so the model can judge whether the suites that cover the - # change ran (prompt.md). The token reads only. - - name: List the upstream run's jobs - if: steps.agree.outcome == 'success' - env: - GITHUB_TOKEN: ${{ github.token }} - API_BUDGET_TOKEN: github-token - UPSTREAM_RUN_ID: ${{ github.event.workflow_run.id }} - JOBS_FILE: ${{ github.workspace }}/pr-verify-data/upstream-jobs.json - run: node .base/.github/scripts/pr-verify/upstream-jobs.mjs - - # The judgment's model, turns and effort: the verify-pr route of the - # base config's models block. - - name: Resolve the verifier's model - id: models - if: steps.agree.outcome == 'success' - uses: ./.base/.github/actions/resolve-config # zizmor: ignore[self-repository] - with: - model-route: verify-pr - output-path: ${{ runner.temp }}/pr-verify-models.json - - # Only whether the secrets are set leaves this step, never a value. - - name: Check the credential - id: credential - if: steps.agree.outcome == 'success' - env: - PRESENT: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN != '' }} - SECRET_NAME: PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN - # Whether the fallback leg can run, never the token. - FALLBACK_PRESENT: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN != '' }} - run: | - if [ "$PRESENT" = true ]; then - echo "present=true" >>"$GITHUB_OUTPUT" - else - echo "::warning::$SECRET_NAME is empty; this job does not judge." - echo "present=false" >>"$GITHUB_OUTPUT" - fi - if [ "$FALLBACK_PRESENT" = true ]; then - echo "fallback-ready=true" >>"$GITHUB_OUTPUT" - else - echo "fallback-ready=false" >>"$GITHUB_OUTPUT" - fi - - # run-claude-cli refuses to run without these root-owned settings, which - # need sudo, so they are written before the runner is isolated. - - name: Apply the Claude managed settings - if: steps.agree.outcome == 'success' - uses: melodic-software/ci-workflows/.github/actions/apply-claude-managed-settings@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 - - - name: Isolate the runner - if: steps.agree.outcome == 'success' - uses: ./.base/.github/actions/setup-runner-isolation # zizmor: ignore[self-repository] - with: - install-subprocess-isolation: true - - # Files, not step outputs: run-claude-cli reads both. A `default` model - # resolves to an empty model, which the launcher cannot pass. started-at - # lets the outcome step tell a step timeout from other failures. - - name: Write the prompt and schema - id: prompt - if: steps.agree.outcome == 'success' + - name: Read chunk 7's verdict + id: chunk-fail-7 + if: ${{ !cancelled() && steps.judge-7.outcome == 'success' }} + continue-on-error: true env: - DATA: ${{ github.workspace }}/pr-verify-data - MODEL: ${{ steps.models.outputs.model }} + EXECUTION_FILE: ${{ steps.judge-7.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-7.outputs.log-file }} + CHUNK: "7" CHUNKS: ${{ steps.diff.outputs.chunks }} - PROMPT_STEM: ${{ runner.temp }}/pr-verify-prompt - SCHEMA_FILE: ${{ runner.temp }}/pr-verify-schema.json - run: | - if [ -z "$MODEL" ]; then - echo "::error::the verify-pr profile's model is default, which run-claude-cli cannot pass; name an alias" - exit 1 - fi - if ! [[ $CHUNKS =~ ^[1-9]$ ]]; then - echo "::error::the chunk count is malformed" - exit 1 - fi - template=$(<.base/.github/scripts/pr-verify/prompt.md) - for k in $(seq "$CHUNKS"); do - prompt=$template - # shellcheck disable=SC2016 # the backticks are Markdown, not a command - parts=$(printf ' - `%s`\n' "$DATA/diff/chunk-$k"/part-*.patch) - prompt=${prompt//DIFF_PARTS/$parts} - prompt=${prompt//CONTEXT_FILE/$DATA/context.json} - prompt=${prompt//PR_FILE/$DATA/pr.json} - prompt=${prompt//SPEC_TEXT_FILE/$DATA/spec-text.json} - prompt=${prompt//SPEC_FILE/$DATA/spec.json} - prompt=${prompt//JOBS_FILE/$DATA/upstream-jobs.json} - note= - if [ "$CHUNKS" -gt 1 ]; then - note=$(<.base/.github/scripts/pr-verify/chunk-note.md) - # shellcheck disable=SC2016 # the backticks are Markdown, not a command - manifest=$(printf ' - `%s`\n' "$DATA"/manifest/part-*.txt) - note=${note//MANIFEST_PARTS/$manifest} - note=${note//DATA_DIR/$DATA} - note=${note//CHUNK_INDEX/$k} - note=${note//CHUNK_COUNT/$CHUNKS} - note=$'\n'$note - fi - prompt=${prompt//CHUNK_NOTE/"$note"} - printf '%s\n' "$prompt" >"$PROMPT_STEM-$k.md" - done - printf '%s\n' '{"type":"object","properties":{"verdict":{"enum":["pass","pass-notes","fail"]},"summary":{"type":"string"},"notes":{"type":"array","items":{"type":"string"}}},"required":["verdict","summary","notes"],"additionalProperties":false}' >"$SCHEMA_FILE" - echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail - # continue-on-error keeps a launcher failure, which run-claude-cli - # reports by failing this step, from ending the job before the outcome - # step; "Fail without a verdict" carries the red. With the fallback - # token set, a limit stop, a credit stop or a 401 or 402 hands off, - # which skips the verdict steps, and a clean one the outcome step too: - # the fallback job judges and records the run instead. One leg of - # leg-timeout-minutes and the CLI install fit in timeout-minutes, which - # the outcome step repeats, and five of those fit in the job's 125. The model may - # share its job with the upload: it has Read alone and the job token can - # only read. - # Under dontAsk only the allow rule's directory is readable; the deny - # rules hold if the tools or the working directory change. - - name: Judge the pull request - id: judge - if: steps.credential.outputs.present == 'true' + - name: Time chunk 8 + id: time-8 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 8 && steps.judge-7.outcome == 'success' && steps.judge-7.outputs.handoff != 'true' && steps.chunk-fail-7.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 8 + id: judge-8 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 8 && steps.judge-7.outcome == 'success' && steps.judge-7.outputs.handoff != 'true' && steps.chunk-fail-7.outputs.fail != 'true' continue-on-error: true timeout-minutes: 22 uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 env: - # No work may outlive the result message the launcher reads. CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: lane: pr-verify role: pr-verify - prompt-file: ${{ runner.temp }}/pr-verify-prompt-1.md + prompt-file: ${{ runner.temp }}/pr-verify-prompt-8.md preamble: >- You run unattended; return only the verdict the schema asks for; nothing you write is posted. - subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + api-key: ${{ secrets.PR_VERIFY_CLAUDE_API_KEY }} model: ${{ steps.models.outputs.model }} effort: ${{ steps.models.outputs.effort }} max-turns: ${{ steps.models.outputs.max-turns }} @@ -1292,86 +1116,77 @@ jobs: Agent mcp__* json-schema-file: ${{ runner.temp }}/pr-verify-schema.json - fallback-ready: ${{ steps.credential.outputs.fallback-ready }} leg-timeout-minutes: "15" - # After a handoff, the denial check: a denial on the subscription leg - # means no verdict, so the fallback job runs only on this step's - # clean=true; continue-on-error lets the report record the stop, and - # "Fail without a verdict" carries the red. run-claude-cli warns of a - # refused token itself. - - name: Check the subscription leg - id: sub-leg - if: steps.judge.outputs.handoff == 'true' + - name: Check the key leg, chunk 8 + id: key-leg-8 + if: steps.judge-8.outputs.handoff == 'true' continue-on-error: true env: LOG_FILES: | ${{ steps.judge.outputs.log-file }} - HANDOFF_REASON: ${{ steps.judge.outputs.handoff-reason }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + LANE_CLASS: ${{ steps.judge-8.outputs.lane-class }} run: | + case "$LANE_CLASS" in + auth) echo "::warning::the API refused the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY), class auth; the owner must replace the secret" ;; + *) echo "::warning::the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY) hit a credit stop; rotate a spare key in" ;; + esac node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg - echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + echo "::notice::check-intent-subscription judges on the subscription token" - # One lane record per run: a clean handoff is recorded by the fallback - # job; a handoff with a denial on the subscription leg, here. - - name: Report the judgment's outcome - if: (steps.judge.outcome == 'success' || steps.judge.outcome == 'failure') && (steps.judge.outputs.handoff != 'true' || steps.sub-leg.outcome == 'failure') + - name: Report chunk 8's outcome + if: (steps.judge-8.outcome == 'success' || steps.judge-8.outcome == 'failure') && (steps.judge-8.outputs.handoff != 'true' || steps.key-leg-8.outcome == 'failure') continue-on-error: true uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 with: - outcome: ${{ steps.judge.outcome }} - execution-file: ${{ steps.judge.outputs.execution-file }} - log-file: ${{ steps.judge.outputs.log-file }} + outcome: ${{ steps.judge-8.outcome }} + execution-file: ${{ steps.judge-8.outputs.execution-file }} + log-file: ${{ steps.judge-8.outputs.log-file }} lane: pr-verify lane-id: pr-verify - step-started-at: ${{ steps.prompt.outputs.started-at }} + step-started-at: ${{ steps.time-8.outputs.started-at }} step-timeout-minutes: 22 - credential: ${{ steps.judge.outputs.credential }} + credential: ${{ steps.judge-8.outputs.credential }} max-turns: ${{ steps.models.outputs.max-turns }} - known-failure-class: ${{ steps.judge.outputs.lane-class }} - # Why this job ran, for the run's one lane record. - fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + known-failure-class: ${{ steps.judge-8.outputs.lane-class == 'credit-stop' && 'rate-limit' || steps.judge-8.outputs.lane-class }} - # The leg's transcript, failed legs included, sealed to the transcript - # key and kept 7 days for the puller; a failed upload never fails the job. - - name: Upload the sealed transcript - if: always() && steps.judge.outputs.execution-file != '' + - name: Upload chunk 8's sealed transcript + if: always() && steps.judge-8.outputs.execution-file != '' continue-on-error: true uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] with: - execution-file: ${{ steps.judge.outputs.execution-file }} + execution-file: ${{ steps.judge-8.outputs.execution-file }} public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} - job-id: ${{ github.job }} + job-id: ${{ github.job }}-8 - # A fail the head can count ends the judgment here (write-verdict.mjs - # checkChunk): the later chunks, the final judge and any handoff are - # skipped, and "Write the verdict" runs on it, so no later step can lose - # it to a re-run. A step that fails before it does not stop the check. - - name: Read chunk 1's verdict - id: chunk-fail - if: ${{ !cancelled() && steps.judge.outcome == 'success' }} + - name: Read chunk 8's verdict + id: chunk-fail-8 + if: ${{ !cancelled() && steps.judge-8.outcome == 'success' }} continue-on-error: true env: - EXECUTION_FILE: ${{ steps.judge.outputs.execution-file }} - LOG_FILE: ${{ steps.judge.outputs.log-file }} - CHUNK: "1" + EXECUTION_FILE: ${{ steps.judge-8.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-8.outputs.log-file }} + CHUNK: "8" CHUNKS: ${{ steps.diff.outputs.chunks }} DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail - # Chunk 2 on, each the first chunk's steps on its own prompt: chunk k runs - # only when the diff has k chunks and chunk k-1 judged and handed nothing - # on, so a handoff hands the whole judgment on and the next leg judges - # every chunk again. The leg check reads every chunk's log so far. - - name: Time chunk 2 - id: time-2 - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 2 && steps.judge.outcome == 'success' && steps.judge.outputs.handoff != 'true' && steps.chunk-fail.outputs.fail != 'true' + - name: Time chunk 9 + id: time-9 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 9 && steps.judge-8.outcome == 'success' && steps.judge-8.outputs.handoff != 'true' && steps.chunk-fail-8.outputs.fail != 'true' run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" - - name: Judge chunk 2 - id: judge-2 - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 2 && steps.judge.outcome == 'success' && steps.judge.outputs.handoff != 'true' && steps.chunk-fail.outputs.fail != 'true' + - name: Judge chunk 9 + id: judge-9 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 9 && steps.judge-8.outcome == 'success' && steps.judge-8.outputs.handoff != 'true' && steps.chunk-fail-8.outputs.fail != 'true' continue-on-error: true timeout-minutes: 22 uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 @@ -1380,11 +1195,11 @@ jobs: with: lane: pr-verify role: pr-verify - prompt-file: ${{ runner.temp }}/pr-verify-prompt-2.md + prompt-file: ${{ runner.temp }}/pr-verify-prompt-9.md preamble: >- You run unattended; return only the verdict the schema asks for; nothing you write is posted. - subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + api-key: ${{ secrets.PR_VERIFY_CLAUDE_API_KEY }} model: ${{ steps.models.outputs.model }} effort: ${{ steps.models.outputs.effort }} max-turns: ${{ steps.models.outputs.max-turns }} @@ -1404,69 +1219,78 @@ jobs: Agent mcp__* json-schema-file: ${{ runner.temp }}/pr-verify-schema.json - fallback-ready: ${{ steps.credential.outputs.fallback-ready }} leg-timeout-minutes: "15" - - name: Check the subscription leg, chunk 2 - id: sub-leg-2 - if: steps.judge-2.outputs.handoff == 'true' + - name: Check the key leg, chunk 9 + id: key-leg-9 + if: steps.judge-9.outputs.handoff == 'true' continue-on-error: true env: LOG_FILES: | ${{ steps.judge.outputs.log-file }} ${{ steps.judge-2.outputs.log-file }} - HANDOFF_REASON: ${{ steps.judge-2.outputs.handoff-reason }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + LANE_CLASS: ${{ steps.judge-9.outputs.lane-class }} run: | + case "$LANE_CLASS" in + auth) echo "::warning::the API refused the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY), class auth; the owner must replace the secret" ;; + *) echo "::warning::the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY) hit a credit stop; rotate a spare key in" ;; + esac node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg - echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + echo "::notice::check-intent-subscription judges on the subscription token" - - name: Report chunk 2's outcome - if: (steps.judge-2.outcome == 'success' || steps.judge-2.outcome == 'failure') && (steps.judge-2.outputs.handoff != 'true' || steps.sub-leg-2.outcome == 'failure') + - name: Report chunk 9's outcome + if: (steps.judge-9.outcome == 'success' || steps.judge-9.outcome == 'failure') && (steps.judge-9.outputs.handoff != 'true' || steps.key-leg-9.outcome == 'failure') continue-on-error: true uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 with: - outcome: ${{ steps.judge-2.outcome }} - execution-file: ${{ steps.judge-2.outputs.execution-file }} - log-file: ${{ steps.judge-2.outputs.log-file }} + outcome: ${{ steps.judge-9.outcome }} + execution-file: ${{ steps.judge-9.outputs.execution-file }} + log-file: ${{ steps.judge-9.outputs.log-file }} lane: pr-verify lane-id: pr-verify - step-started-at: ${{ steps.time-2.outputs.started-at }} + step-started-at: ${{ steps.time-9.outputs.started-at }} step-timeout-minutes: 22 - credential: ${{ steps.judge-2.outputs.credential }} + credential: ${{ steps.judge-9.outputs.credential }} max-turns: ${{ steps.models.outputs.max-turns }} - known-failure-class: ${{ steps.judge-2.outputs.lane-class }} - fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + known-failure-class: ${{ steps.judge-9.outputs.lane-class == 'credit-stop' && 'rate-limit' || steps.judge-9.outputs.lane-class }} - - name: Upload chunk 2's sealed transcript - if: always() && steps.judge-2.outputs.execution-file != '' + - name: Upload chunk 9's sealed transcript + if: always() && steps.judge-9.outputs.execution-file != '' continue-on-error: true uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] with: - execution-file: ${{ steps.judge-2.outputs.execution-file }} + execution-file: ${{ steps.judge-9.outputs.execution-file }} public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} - job-id: ${{ github.job }}-2 + job-id: ${{ github.job }}-9 - - name: Read chunk 2's verdict - id: chunk-fail-2 - if: ${{ !cancelled() && steps.judge-2.outcome == 'success' }} + - name: Read chunk 9's verdict + id: chunk-fail-9 + if: ${{ !cancelled() && steps.judge-9.outcome == 'success' }} continue-on-error: true env: - EXECUTION_FILE: ${{ steps.judge-2.outputs.execution-file }} - LOG_FILE: ${{ steps.judge-2.outputs.log-file }} - CHUNK: "2" + EXECUTION_FILE: ${{ steps.judge-9.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-9.outputs.log-file }} + CHUNK: "9" CHUNKS: ${{ steps.diff.outputs.chunks }} DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail - - name: Time chunk 3 - id: time-3 - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 3 && steps.judge-2.outcome == 'success' && steps.judge-2.outputs.handoff != 'true' && steps.chunk-fail-2.outputs.fail != 'true' + - name: Time chunk 10 + id: time-10 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 10 && steps.judge-9.outcome == 'success' && steps.judge-9.outputs.handoff != 'true' && steps.chunk-fail-9.outputs.fail != 'true' run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" - - name: Judge chunk 3 - id: judge-3 - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 3 && steps.judge-2.outcome == 'success' && steps.judge-2.outputs.handoff != 'true' && steps.chunk-fail-2.outputs.fail != 'true' + - name: Judge chunk 10 + id: judge-10 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 10 && steps.judge-9.outcome == 'success' && steps.judge-9.outputs.handoff != 'true' && steps.chunk-fail-9.outputs.fail != 'true' continue-on-error: true timeout-minutes: 22 uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 @@ -1475,11 +1299,11 @@ jobs: with: lane: pr-verify role: pr-verify - prompt-file: ${{ runner.temp }}/pr-verify-prompt-3.md + prompt-file: ${{ runner.temp }}/pr-verify-prompt-10.md preamble: >- You run unattended; return only the verdict the schema asks for; nothing you write is posted. - subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + api-key: ${{ secrets.PR_VERIFY_CLAUDE_API_KEY }} model: ${{ steps.models.outputs.model }} effort: ${{ steps.models.outputs.effort }} max-turns: ${{ steps.models.outputs.max-turns }} @@ -1499,70 +1323,79 @@ jobs: Agent mcp__* json-schema-file: ${{ runner.temp }}/pr-verify-schema.json - fallback-ready: ${{ steps.credential.outputs.fallback-ready }} leg-timeout-minutes: "15" - - name: Check the subscription leg, chunk 3 - id: sub-leg-3 - if: steps.judge-3.outputs.handoff == 'true' + - name: Check the key leg, chunk 10 + id: key-leg-10 + if: steps.judge-10.outputs.handoff == 'true' continue-on-error: true env: LOG_FILES: | ${{ steps.judge.outputs.log-file }} ${{ steps.judge-2.outputs.log-file }} ${{ steps.judge-3.outputs.log-file }} - HANDOFF_REASON: ${{ steps.judge-3.outputs.handoff-reason }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + ${{ steps.judge-10.outputs.log-file }} + LANE_CLASS: ${{ steps.judge-10.outputs.lane-class }} run: | + case "$LANE_CLASS" in + auth) echo "::warning::the API refused the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY), class auth; the owner must replace the secret" ;; + *) echo "::warning::the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY) hit a credit stop; rotate a spare key in" ;; + esac node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg - echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + echo "::notice::check-intent-subscription judges on the subscription token" - - name: Report chunk 3's outcome - if: (steps.judge-3.outcome == 'success' || steps.judge-3.outcome == 'failure') && (steps.judge-3.outputs.handoff != 'true' || steps.sub-leg-3.outcome == 'failure') + - name: Report chunk 10's outcome + if: (steps.judge-10.outcome == 'success' || steps.judge-10.outcome == 'failure') && (steps.judge-10.outputs.handoff != 'true' || steps.key-leg-10.outcome == 'failure') continue-on-error: true uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 with: - outcome: ${{ steps.judge-3.outcome }} - execution-file: ${{ steps.judge-3.outputs.execution-file }} - log-file: ${{ steps.judge-3.outputs.log-file }} + outcome: ${{ steps.judge-10.outcome }} + execution-file: ${{ steps.judge-10.outputs.execution-file }} + log-file: ${{ steps.judge-10.outputs.log-file }} lane: pr-verify lane-id: pr-verify - step-started-at: ${{ steps.time-3.outputs.started-at }} + step-started-at: ${{ steps.time-10.outputs.started-at }} step-timeout-minutes: 22 - credential: ${{ steps.judge-3.outputs.credential }} + credential: ${{ steps.judge-10.outputs.credential }} max-turns: ${{ steps.models.outputs.max-turns }} - known-failure-class: ${{ steps.judge-3.outputs.lane-class }} - fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + known-failure-class: ${{ steps.judge-10.outputs.lane-class == 'credit-stop' && 'rate-limit' || steps.judge-10.outputs.lane-class }} - - name: Upload chunk 3's sealed transcript - if: always() && steps.judge-3.outputs.execution-file != '' + - name: Upload chunk 10's sealed transcript + if: always() && steps.judge-10.outputs.execution-file != '' continue-on-error: true uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] with: - execution-file: ${{ steps.judge-3.outputs.execution-file }} + execution-file: ${{ steps.judge-10.outputs.execution-file }} public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} - job-id: ${{ github.job }}-3 + job-id: ${{ github.job }}-10 - - name: Read chunk 3's verdict - id: chunk-fail-3 - if: ${{ !cancelled() && steps.judge-3.outcome == 'success' }} + - name: Read chunk 10's verdict + id: chunk-fail-10 + if: ${{ !cancelled() && steps.judge-10.outcome == 'success' }} continue-on-error: true env: - EXECUTION_FILE: ${{ steps.judge-3.outputs.execution-file }} - LOG_FILE: ${{ steps.judge-3.outputs.log-file }} - CHUNK: "3" + EXECUTION_FILE: ${{ steps.judge-10.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-10.outputs.log-file }} + CHUNK: "10" CHUNKS: ${{ steps.diff.outputs.chunks }} DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail - - name: Time chunk 4 - id: time-4 - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 4 && steps.judge-3.outcome == 'success' && steps.judge-3.outputs.handoff != 'true' && steps.chunk-fail-3.outputs.fail != 'true' + - name: Time chunk 11 + id: time-11 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 11 && steps.judge-10.outcome == 'success' && steps.judge-10.outputs.handoff != 'true' && steps.chunk-fail-10.outputs.fail != 'true' run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" - - name: Judge chunk 4 - id: judge-4 - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 4 && steps.judge-3.outcome == 'success' && steps.judge-3.outputs.handoff != 'true' && steps.chunk-fail-3.outputs.fail != 'true' + - name: Judge chunk 11 + id: judge-11 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 11 && steps.judge-10.outcome == 'success' && steps.judge-10.outputs.handoff != 'true' && steps.chunk-fail-10.outputs.fail != 'true' continue-on-error: true timeout-minutes: 22 uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 @@ -1571,11 +1404,11 @@ jobs: with: lane: pr-verify role: pr-verify - prompt-file: ${{ runner.temp }}/pr-verify-prompt-4.md + prompt-file: ${{ runner.temp }}/pr-verify-prompt-11.md preamble: >- You run unattended; return only the verdict the schema asks for; nothing you write is posted. - subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + api-key: ${{ secrets.PR_VERIFY_CLAUDE_API_KEY }} model: ${{ steps.models.outputs.model }} effort: ${{ steps.models.outputs.effort }} max-turns: ${{ steps.models.outputs.max-turns }} @@ -1595,12 +1428,11 @@ jobs: Agent mcp__* json-schema-file: ${{ runner.temp }}/pr-verify-schema.json - fallback-ready: ${{ steps.credential.outputs.fallback-ready }} leg-timeout-minutes: "15" - - name: Check the subscription leg, chunk 4 - id: sub-leg-4 - if: steps.judge-4.outputs.handoff == 'true' + - name: Check the key leg, chunk 11 + id: key-leg-11 + if: steps.judge-11.outputs.handoff == 'true' continue-on-error: true env: LOG_FILES: | @@ -1608,45 +1440,162 @@ jobs: ${{ steps.judge-2.outputs.log-file }} ${{ steps.judge-3.outputs.log-file }} ${{ steps.judge-4.outputs.log-file }} - HANDOFF_REASON: ${{ steps.judge-4.outputs.handoff-reason }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + ${{ steps.judge-10.outputs.log-file }} + ${{ steps.judge-11.outputs.log-file }} + LANE_CLASS: ${{ steps.judge-11.outputs.lane-class }} run: | + case "$LANE_CLASS" in + auth) echo "::warning::the API refused the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY), class auth; the owner must replace the secret" ;; + *) echo "::warning::the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY) hit a credit stop; rotate a spare key in" ;; + esac node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg - echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + echo "::notice::check-intent-subscription judges on the subscription token" - - name: Report chunk 4's outcome - if: (steps.judge-4.outcome == 'success' || steps.judge-4.outcome == 'failure') && (steps.judge-4.outputs.handoff != 'true' || steps.sub-leg-4.outcome == 'failure') + - name: Report chunk 11's outcome + if: (steps.judge-11.outcome == 'success' || steps.judge-11.outcome == 'failure') && (steps.judge-11.outputs.handoff != 'true' || steps.key-leg-11.outcome == 'failure') continue-on-error: true uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 with: - outcome: ${{ steps.judge-4.outcome }} - execution-file: ${{ steps.judge-4.outputs.execution-file }} - log-file: ${{ steps.judge-4.outputs.log-file }} + outcome: ${{ steps.judge-11.outcome }} + execution-file: ${{ steps.judge-11.outputs.execution-file }} + log-file: ${{ steps.judge-11.outputs.log-file }} lane: pr-verify lane-id: pr-verify - step-started-at: ${{ steps.time-4.outputs.started-at }} + step-started-at: ${{ steps.time-11.outputs.started-at }} step-timeout-minutes: 22 - credential: ${{ steps.judge-4.outputs.credential }} + credential: ${{ steps.judge-11.outputs.credential }} max-turns: ${{ steps.models.outputs.max-turns }} - known-failure-class: ${{ steps.judge-4.outputs.lane-class }} - fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + known-failure-class: ${{ steps.judge-11.outputs.lane-class == 'credit-stop' && 'rate-limit' || steps.judge-11.outputs.lane-class }} - - name: Upload chunk 4's sealed transcript - if: always() && steps.judge-4.outputs.execution-file != '' + - name: Upload chunk 11's sealed transcript + if: always() && steps.judge-11.outputs.execution-file != '' continue-on-error: true uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] with: - execution-file: ${{ steps.judge-4.outputs.execution-file }} + execution-file: ${{ steps.judge-11.outputs.execution-file }} public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} - job-id: ${{ github.job }}-4 + job-id: ${{ github.job }}-11 - - name: Read chunk 4's verdict - id: chunk-fail-4 - if: ${{ !cancelled() && steps.judge-4.outcome == 'success' }} + - name: Read chunk 11's verdict + id: chunk-fail-11 + if: ${{ !cancelled() && steps.judge-11.outcome == 'success' }} continue-on-error: true env: - EXECUTION_FILE: ${{ steps.judge-4.outputs.execution-file }} - LOG_FILE: ${{ steps.judge-4.outputs.log-file }} - CHUNK: "4" + EXECUTION_FILE: ${{ steps.judge-11.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-11.outputs.log-file }} + CHUNK: "11" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + - name: Time chunk 12 + id: time-12 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 12 && steps.judge-11.outcome == 'success' && steps.judge-11.outputs.handoff != 'true' && steps.chunk-fail-11.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 12 + id: judge-12 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 12 && steps.judge-11.outcome == 'success' && steps.judge-11.outputs.handoff != 'true' && steps.chunk-fail-11.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-12.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + api-key: ${{ secrets.PR_VERIFY_CLAUDE_API_KEY }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + leg-timeout-minutes: "15" + + - name: Check the key leg, chunk 12 + id: key-leg-12 + if: steps.judge-12.outputs.handoff == 'true' + continue-on-error: true + env: + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + ${{ steps.judge-10.outputs.log-file }} + ${{ steps.judge-11.outputs.log-file }} + ${{ steps.judge-12.outputs.log-file }} + LANE_CLASS: ${{ steps.judge-12.outputs.lane-class }} + run: | + case "$LANE_CLASS" in + auth) echo "::warning::the API refused the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY), class auth; the owner must replace the secret" ;; + *) echo "::warning::the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY) hit a credit stop; rotate a spare key in" ;; + esac + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::check-intent-subscription judges on the subscription token" + + - name: Report chunk 12's outcome + if: (steps.judge-12.outcome == 'success' || steps.judge-12.outcome == 'failure') && (steps.judge-12.outputs.handoff != 'true' || steps.key-leg-12.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-12.outcome }} + execution-file: ${{ steps.judge-12.outputs.execution-file }} + log-file: ${{ steps.judge-12.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-12.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-12.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-12.outputs.lane-class == 'credit-stop' && 'rate-limit' || steps.judge-12.outputs.lane-class }} + + - name: Upload chunk 12's sealed transcript + if: always() && steps.judge-12.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-12.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-12 + + - name: Read chunk 12's verdict + id: chunk-fail-12 + if: ${{ !cancelled() && steps.judge-12.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-12.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-12.outputs.log-file }} + CHUNK: "12" CHUNKS: ${{ steps.diff.outputs.chunks }} DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest @@ -1659,18 +1608,34 @@ jobs: # keeps this judge off the diff, and a denial voids the verdict. - name: Write the chunk results id: chunk-results - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') > 1 && steps.judge.outcome == 'success' && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-2.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 3 || steps.judge-3.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 4 || steps.judge-4.outcome == 'success') && steps.chunk-fail.outputs.fail != 'true' && steps.chunk-fail-2.outputs.fail != 'true' && steps.chunk-fail-3.outputs.fail != 'true' && steps.chunk-fail-4.outputs.fail != 'true' + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') > 1 && steps.judge.outcome == 'success' && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-2.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 3 || steps.judge-3.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 4 || steps.judge-4.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 5 || steps.judge-5.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 6 || steps.judge-6.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 7 || steps.judge-7.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 8 || steps.judge-8.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 9 || steps.judge-9.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 10 || steps.judge-10.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 11 || steps.judge-11.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 12 || steps.judge-12.outcome == 'success') && steps.chunk-fail.outputs.fail != 'true' && steps.chunk-fail-2.outputs.fail != 'true' && steps.chunk-fail-3.outputs.fail != 'true' && steps.chunk-fail-4.outputs.fail != 'true' && steps.chunk-fail-5.outputs.fail != 'true' && steps.chunk-fail-6.outputs.fail != 'true' && steps.chunk-fail-7.outputs.fail != 'true' && steps.chunk-fail-8.outputs.fail != 'true' && steps.chunk-fail-9.outputs.fail != 'true' && steps.chunk-fail-10.outputs.fail != 'true' && steps.chunk-fail-11.outputs.fail != 'true' && steps.chunk-fail-12.outputs.fail != 'true' env: EXECUTION_FILES: | ${{ steps.judge.outputs.execution-file }} ${{ steps.judge-2.outputs.execution-file }} ${{ steps.judge-3.outputs.execution-file }} ${{ steps.judge-4.outputs.execution-file }} + ${{ steps.judge-5.outputs.execution-file }} + ${{ steps.judge-6.outputs.execution-file }} + ${{ steps.judge-7.outputs.execution-file }} + ${{ steps.judge-8.outputs.execution-file }} + ${{ steps.judge-9.outputs.execution-file }} + ${{ steps.judge-10.outputs.execution-file }} + ${{ steps.judge-11.outputs.execution-file }} + ${{ steps.judge-12.outputs.execution-file }} LOG_FILES: | ${{ steps.judge.outputs.log-file }} ${{ steps.judge-2.outputs.log-file }} ${{ steps.judge-3.outputs.log-file }} ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + ${{ steps.judge-10.outputs.log-file }} + ${{ steps.judge-11.outputs.log-file }} + ${{ steps.judge-12.outputs.log-file }} CHUNKS: ${{ steps.diff.outputs.chunks }} DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff RESULTS_DIR: ${{ github.workspace }}/pr-verify-data/chunk-results @@ -1704,7 +1669,7 @@ jobs: preamble: >- You run unattended; return only the verdict the schema asks for; nothing you write is posted. - subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + api-key: ${{ secrets.PR_VERIFY_CLAUDE_API_KEY }} model: ${{ steps.models.outputs.model }} effort: ${{ steps.models.outputs.effort }} max-turns: ${{ steps.models.outputs.max-turns }} @@ -1725,11 +1690,10 @@ jobs: Agent mcp__* json-schema-file: ${{ runner.temp }}/pr-verify-spec-schema.json - fallback-ready: ${{ steps.credential.outputs.fallback-ready }} leg-timeout-minutes: "15" - - name: Check the subscription leg, whole spec - id: sub-leg-spec + - name: Check the key leg, whole spec + id: key-leg-spec if: steps.judge-spec.outputs.handoff == 'true' continue-on-error: true env: @@ -1739,14 +1703,26 @@ jobs: ${{ steps.judge-2.outputs.log-file }} ${{ steps.judge-3.outputs.log-file }} ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + ${{ steps.judge-10.outputs.log-file }} + ${{ steps.judge-11.outputs.log-file }} + ${{ steps.judge-12.outputs.log-file }} ${{ steps.judge-spec.outputs.log-file }} - HANDOFF_REASON: ${{ steps.judge-spec.outputs.handoff-reason }} + LANE_CLASS: ${{ steps.judge-spec.outputs.lane-class }} run: | + case "$LANE_CLASS" in + auth) echo "::warning::the API refused the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY), class auth; the owner must replace the secret" ;; + *) echo "::warning::the pr-verify role's API key (PR_VERIFY_CLAUDE_API_KEY) hit a credit stop; rotate a spare key in" ;; + esac node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg - echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + echo "::notice::check-intent-subscription judges on the subscription token" - name: Report the spec judgment's outcome - if: (steps.judge-spec.outcome == 'success' || steps.judge-spec.outcome == 'failure') && (steps.judge-spec.outputs.handoff != 'true' || steps.sub-leg-spec.outcome == 'failure') + if: (steps.judge-spec.outcome == 'success' || steps.judge-spec.outcome == 'failure') && (steps.judge-spec.outputs.handoff != 'true' || steps.key-leg-spec.outcome == 'failure') continue-on-error: true uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 with: @@ -1759,8 +1735,7 @@ jobs: step-timeout-minutes: 22 credential: ${{ steps.judge-spec.outputs.credential }} max-turns: ${{ steps.models.outputs.max-turns }} - known-failure-class: ${{ steps.judge-spec.outputs.lane-class }} - fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + known-failure-class: ${{ steps.judge-spec.outputs.lane-class == 'credit-stop' && 'rate-limit' || steps.judge-spec.outputs.lane-class }} - name: Upload the spec judgment's sealed transcript if: always() && steps.judge-spec.outputs.execution-file != '' @@ -1776,7 +1751,7 @@ jobs: # script, never interpolated into a command. - name: Write the verdict id: verdict - if: ${{ !cancelled() && (steps.chunk-fail.outputs.fail == 'true' || steps.chunk-fail-2.outputs.fail == 'true' || steps.chunk-fail-3.outputs.fail == 'true' || steps.chunk-fail-4.outputs.fail == 'true' || steps.chunk-results.outcome == 'failure' || (steps.judge.outcome == 'success' && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-2.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 3 || steps.judge-3.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 4 || steps.judge-4.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-spec.outcome == 'success'))) }} + if: ${{ !cancelled() && (steps.chunk-fail.outputs.fail == 'true' || steps.chunk-fail-2.outputs.fail == 'true' || steps.chunk-fail-3.outputs.fail == 'true' || steps.chunk-fail-4.outputs.fail == 'true' || steps.chunk-fail-5.outputs.fail == 'true' || steps.chunk-fail-6.outputs.fail == 'true' || steps.chunk-fail-7.outputs.fail == 'true' || steps.chunk-fail-8.outputs.fail == 'true' || steps.chunk-fail-9.outputs.fail == 'true' || steps.chunk-fail-10.outputs.fail == 'true' || steps.chunk-fail-11.outputs.fail == 'true' || steps.chunk-fail-12.outputs.fail == 'true' || steps.chunk-results.outcome == 'failure' || (steps.judge.outcome == 'success' && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-2.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 3 || steps.judge-3.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 4 || steps.judge-4.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 5 || steps.judge-5.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 6 || steps.judge-6.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 7 || steps.judge-7.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 8 || steps.judge-8.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 9 || steps.judge-9.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 10 || steps.judge-10.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 11 || steps.judge-11.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 12 || steps.judge-12.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-spec.outcome == 'success'))) }} env: # Each chunk's files in chunk order. A counted fail needs no others; # anything else needs every chunk and the final judge. @@ -1785,11 +1760,27 @@ jobs: ${{ steps.judge-2.outputs.execution-file }} ${{ steps.judge-3.outputs.execution-file }} ${{ steps.judge-4.outputs.execution-file }} + ${{ steps.judge-5.outputs.execution-file }} + ${{ steps.judge-6.outputs.execution-file }} + ${{ steps.judge-7.outputs.execution-file }} + ${{ steps.judge-8.outputs.execution-file }} + ${{ steps.judge-9.outputs.execution-file }} + ${{ steps.judge-10.outputs.execution-file }} + ${{ steps.judge-11.outputs.execution-file }} + ${{ steps.judge-12.outputs.execution-file }} LOG_FILES: | ${{ steps.judge.outputs.log-file }} ${{ steps.judge-2.outputs.log-file }} ${{ steps.judge-3.outputs.log-file }} ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + ${{ steps.judge-10.outputs.log-file }} + ${{ steps.judge-11.outputs.log-file }} + ${{ steps.judge-12.outputs.log-file }} CHUNKS: ${{ steps.diff.outputs.chunks }} DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff # The whole-spec judgment, which a head of two chunks or more must have. @@ -1837,38 +1828,43 @@ jobs: with: tokens: github-token - # A clean handoff hands the judgment on; any other run that passed the - # gates and left no verdict, an empty token included, fails here. + # A clean handoff or an empty key hands the judgment on; any other + # run that passed the gates and left no verdict fails here. - name: Fail without a verdict - if: always() && steps.agree.outcome == 'success' && steps.verdict.outcome != 'success' && steps.sub-leg.outcome != 'success' && steps.sub-leg-2.outcome != 'success' && steps.sub-leg-3.outcome != 'success' && steps.sub-leg-4.outcome != 'success' && steps.sub-leg-spec.outcome != 'success' + if: always() && steps.agree.outcome == 'success' && steps.verdict.outcome != 'success' && steps.credential.outputs.present != 'false' && steps.key-leg.outcome != 'success' && steps.key-leg-2.outcome != 'success' && steps.key-leg-3.outcome != 'success' && steps.key-leg-4.outcome != 'success' && steps.key-leg-5.outcome != 'success' && steps.key-leg-6.outcome != 'success' && steps.key-leg-7.outcome != 'success' && steps.key-leg-8.outcome != 'success' && steps.key-leg-9.outcome != 'success' && steps.key-leg-10.outcome != 'success' && steps.key-leg-11.outcome != 'success' && steps.key-leg-12.outcome != 'success' && steps.key-leg-spec.outcome != 'success' run: | echo "::error::the judgment left no verdict for this head" exit 1 - # The fallback leg, the central fallback subscription, as its own job on a - # new VM, only after the subscription job handed off (a limit stop, a - # credit stop, or a 401 or 402, with the fallback token set) with no - # permission denial. Its steps repeat the subscription job's but for the - # credential, the leg check and the fallback reason (workflow.test.mjs holds - # them equal). It reads three outputs of the subscription job, each compared - # to a fixed string in its `if` or the fallback-reason expression, never - # written into a `run:` script. It is the last leg and never hands off: an - # empty subscription token fails the subscription job instead. - check-intent-fallback: - needs: check-intent-subscription + # The subscription leg, as its own job on a new VM: check-intent's + # processes and environment do not reach it, and of its files only the + # pre-judge reads do, through the `reads` output. Its steps repeat + # check-intent's from a fresh checkout but for the credential, the + # key-leg check and the reads it takes from check-intent's `reads` output + # (workflow.test.mjs holds them equal), so the trigger gate, the kill + # switch and the hardening run again. Only this job references the + # subscription token. needs adds success(): a check-intent that failed, a + # denial on its key leg included, leaves this job skipped. + check-intent-subscription: + needs: check-intent if: >- github.ref == 'refs/heads/main' && github.event.workflow_run.event == 'pull_request' && (github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure') && github.event.workflow_run.head_repository.full_name == github.repository - && needs.check-intent-subscription.outputs.handoff == 'true' - && needs.check-intent-subscription.outputs.leg-clean == 'true' + && ((needs.check-intent.outputs.handoff == 'true' + && needs.check-intent.outputs.key-leg-clean == 'true') + || needs.check-intent.outputs.credential-present == 'false') runs-on: ubuntu-24.04 - timeout-minutes: 125 + timeout-minutes: 301 permissions: contents: read # check out main; read the PR's commits for the diff pull-requests: read # the PR, its comments and reviews actions: read # the upstream run, and verdicts already uploaded + outputs: + handoff: ${{ steps.judge.outputs.handoff == 'true' || steps.judge-2.outputs.handoff == 'true' || steps.judge-3.outputs.handoff == 'true' || steps.judge-4.outputs.handoff == 'true' || steps.judge-5.outputs.handoff == 'true' || steps.judge-6.outputs.handoff == 'true' || steps.judge-7.outputs.handoff == 'true' || steps.judge-8.outputs.handoff == 'true' || steps.judge-9.outputs.handoff == 'true' || steps.judge-10.outputs.handoff == 'true' || steps.judge-11.outputs.handoff == 'true' || steps.judge-12.outputs.handoff == 'true' || steps.judge-spec.outputs.handoff == 'true' }} + handoff-reason: ${{ steps.judge.outputs.handoff-reason || steps.judge-2.outputs.handoff-reason || steps.judge-3.outputs.handoff-reason || steps.judge-4.outputs.handoff-reason || steps.judge-5.outputs.handoff-reason || steps.judge-6.outputs.handoff-reason || steps.judge-7.outputs.handoff-reason || steps.judge-8.outputs.handoff-reason || steps.judge-9.outputs.handoff-reason || steps.judge-10.outputs.handoff-reason || steps.judge-11.outputs.handoff-reason || steps.judge-12.outputs.handoff-reason || steps.judge-spec.outputs.handoff-reason }} + leg-clean: ${{ steps.sub-leg.outputs.clean || steps.sub-leg-2.outputs.clean || steps.sub-leg-3.outputs.clean || steps.sub-leg-4.outputs.clean || steps.sub-leg-5.outputs.clean || steps.sub-leg-6.outputs.clean || steps.sub-leg-7.outputs.clean || steps.sub-leg-8.outputs.clean || steps.sub-leg-9.outputs.clean || steps.sub-leg-10.outputs.clean || steps.sub-leg-11.outputs.clean || steps.sub-leg-12.outputs.clean || steps.sub-leg-spec.outputs.clean }} steps: - name: Check out the default branch tip uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -1915,6 +1911,9 @@ jobs: SPEC_FILE: ${{ github.workspace }}/pr-verify-data/spec.json SPEC_TEXT_FILE: ${{ github.workspace }}/pr-verify-data/spec-text.json TRUSTED_ACTORS_PATH: .base/.github/standards/trusted-actors/trusted-actors.json + CONTEXT_FILE: ${{ github.workspace }}/pr-verify-data/context.json + # check-intent's reads, taken only when they hold (resolve-pr.mjs takeReads). + CARRIED: ${{ needs.check-intent.outputs.reads }} run: | mkdir -p "$(dirname "$SPEC_FILE")" VERIFY_AUTHORS=$(yq '.merge."verify-authors" // "lane"' .base/docs/conventions/pr-pipeline.yaml) @@ -1958,8 +1957,9 @@ jobs: exit 1 fi + # Carried from check-intent when the resolver took its reads. - name: Select the trusted PR text - if: steps.agree.outcome == 'success' + if: steps.agree.outcome == 'success' && steps.pr.outputs.carried != 'true' env: API_BUDGET_TOKEN: github-token uses: ./.base/.github/actions/select-trusted-text # zizmor: ignore[self-repository] @@ -1973,8 +1973,10 @@ jobs: # the workspace. git diff reads no attributes or diff drivers from it, # so nothing in the PR runs; the patch is a data file. chunk-diff.sh cuts # it into chunks of whole files, and each chunk into parts one Read - # returns whole. A file over one chunk, or a diff over MAX_CHUNKS chunks, - # fails the step by name: no verdict, so no fail on size alone. + # returns whole, in as many chunks as the diff takes, up to the ceiling + # chunk-diff.sh sets, with a judge step below for each. A file over one + # chunk, or a diff over the ceiling, fails the step by name: no verdict, + # so no fail on size alone. - name: Write the diff id: diff if: steps.agree.outcome == 'success' @@ -1987,9 +1989,6 @@ jobs: # About 75k tokens of patch, the budget pr-review-main.yml sets: room # in the review profile's turns to read one chunk whole (judgment). MAX_DIFF_BYTES: "300000" - # The most chunks: the job's judge steps (workflow.test.mjs holds - # them equal). - MAX_CHUNKS: "4" # One Read returns at most 25,000 tokens # (https://code.claude.com/docs/en/tools-reference#large-files), and # a token is at least a byte, so a part this size comes back whole. @@ -2026,115 +2025,2503 @@ jobs: if: steps.agree.outcome == 'success' uses: ./.base/.github/actions/resolve-config # zizmor: ignore[self-repository] with: - model-route: verify-pr - output-path: ${{ runner.temp }}/pr-verify-models.json + model-route: verify-pr + output-path: ${{ runner.temp }}/pr-verify-models.json + + # Only whether the secrets are set leaves this step, never a value. + - name: Check the credential + id: credential + if: steps.agree.outcome == 'success' + env: + PRESENT: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN != '' }} + SECRET_NAME: PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN + # Whether the fallback leg can run, never the token. + FALLBACK_PRESENT: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN != '' }} + run: | + if [ "$PRESENT" = true ]; then + echo "present=true" >>"$GITHUB_OUTPUT" + else + echo "::warning::$SECRET_NAME is empty; this job does not judge." + echo "present=false" >>"$GITHUB_OUTPUT" + fi + if [ "$FALLBACK_PRESENT" = true ]; then + echo "fallback-ready=true" >>"$GITHUB_OUTPUT" + else + echo "fallback-ready=false" >>"$GITHUB_OUTPUT" + fi + + # run-claude-cli refuses to run without these root-owned settings, which + # need sudo, so they are written before the runner is isolated. + - name: Apply the Claude managed settings + if: steps.agree.outcome == 'success' + uses: melodic-software/ci-workflows/.github/actions/apply-claude-managed-settings@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + + - name: Isolate the runner + if: steps.agree.outcome == 'success' + uses: ./.base/.github/actions/setup-runner-isolation # zizmor: ignore[self-repository] + with: + install-subprocess-isolation: true + + # Files, not step outputs: run-claude-cli reads both. A `default` model + # resolves to an empty model, which the launcher cannot pass. started-at + # lets the outcome step tell a step timeout from other failures. + - name: Write the prompt and schema + id: prompt + if: steps.agree.outcome == 'success' + env: + DATA: ${{ github.workspace }}/pr-verify-data + MODEL: ${{ steps.models.outputs.model }} + CHUNKS: ${{ steps.diff.outputs.chunks }} + PROMPT_STEM: ${{ runner.temp }}/pr-verify-prompt + SCHEMA_FILE: ${{ runner.temp }}/pr-verify-schema.json + run: | + if [ -z "$MODEL" ]; then + echo "::error::the verify-pr profile's model is default, which run-claude-cli cannot pass; name an alias" + exit 1 + fi + if ! [[ $CHUNKS =~ ^[1-9][0-9]?$ ]]; then + echo "::error::the chunk count is malformed" + exit 1 + fi + template=$(<.base/.github/scripts/pr-verify/prompt.md) + for k in $(seq "$CHUNKS"); do + prompt=$template + # shellcheck disable=SC2016 # the backticks are Markdown, not a command + parts=$(printf ' - `%s`\n' "$DATA/diff/chunk-$k"/part-*.patch) + prompt=${prompt//DIFF_PARTS/$parts} + prompt=${prompt//CONTEXT_FILE/$DATA/context.json} + prompt=${prompt//PR_FILE/$DATA/pr.json} + prompt=${prompt//SPEC_TEXT_FILE/$DATA/spec-text.json} + prompt=${prompt//SPEC_FILE/$DATA/spec.json} + prompt=${prompt//JOBS_FILE/$DATA/upstream-jobs.json} + note= + if [ "$CHUNKS" -gt 1 ]; then + note=$(<.base/.github/scripts/pr-verify/chunk-note.md) + # shellcheck disable=SC2016 # the backticks are Markdown, not a command + manifest=$(printf ' - `%s`\n' "$DATA"/manifest/part-*.txt) + note=${note//MANIFEST_PARTS/$manifest} + note=${note//DATA_DIR/$DATA} + note=${note//CHUNK_INDEX/$k} + note=${note//CHUNK_COUNT/$CHUNKS} + note=$'\n'$note + fi + prompt=${prompt//CHUNK_NOTE/"$note"} + printf '%s\n' "$prompt" >"$PROMPT_STEM-$k.md" + done + printf '%s\n' '{"type":"object","properties":{"verdict":{"enum":["pass","pass-notes","fail"]},"summary":{"type":"string"},"notes":{"type":"array","items":{"type":"string"}}},"required":["verdict","summary","notes"],"additionalProperties":false}' >"$SCHEMA_FILE" + echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + # continue-on-error keeps a launcher failure, which run-claude-cli + # reports by failing this step, from ending the job before the outcome + # step; "Fail without a verdict" carries the red. With the fallback + # token set, a limit stop, a credit stop or a 401 or 402 hands off, + # which skips the verdict steps, and a clean one the outcome step too: + # the fallback job judges and records the run instead. One leg of + # leg-timeout-minutes and the CLI install fit in timeout-minutes, which + # the outcome step repeats, and the job's timeout holds one of those for + # every judge step. The model may share its job with the upload: it has + # Read alone and the job token can only read. + # Under dontAsk only the allow rule's directory is readable; the deny + # rules hold if the tools or the working directory change. + - name: Judge the pull request + id: judge + if: steps.credential.outputs.present == 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + # No work may outlive the result message the launcher reads. + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-1.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + # After a handoff, the denial check: a denial on the subscription leg + # means no verdict, so the fallback job runs only on this step's + # clean=true; continue-on-error lets the report record the stop, and + # "Fail without a verdict" carries the red. run-claude-cli warns of a + # refused token itself. + - name: Check the subscription leg + id: sub-leg + if: steps.judge.outputs.handoff == 'true' + continue-on-error: true + env: + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + HANDOFF_REASON: ${{ steps.judge.outputs.handoff-reason }} + run: | + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + + # One lane record per run: a clean handoff is recorded by the fallback + # job; a handoff with a denial on the subscription leg, here. + - name: Report the judgment's outcome + if: (steps.judge.outcome == 'success' || steps.judge.outcome == 'failure') && (steps.judge.outputs.handoff != 'true' || steps.sub-leg.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge.outcome }} + execution-file: ${{ steps.judge.outputs.execution-file }} + log-file: ${{ steps.judge.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.prompt.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge.outputs.lane-class }} + # Why this job ran, for the run's one lane record. + fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + + # The leg's transcript, failed legs included, sealed to the transcript + # key and kept 7 days for the puller; a failed upload never fails the job. + - name: Upload the sealed transcript + if: always() && steps.judge.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }} + + # A fail the head can count ends the judgment here (write-verdict.mjs + # checkChunk): the later chunks, the final judge and any handoff are + # skipped, and "Write the verdict" runs on it, so no later step can lose + # it to a re-run. A step that fails before it does not stop the check. + - name: Read chunk 1's verdict + id: chunk-fail + if: ${{ !cancelled() && steps.judge.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge.outputs.execution-file }} + LOG_FILE: ${{ steps.judge.outputs.log-file }} + CHUNK: "1" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + # Chunk 2 on, each the first chunk's steps on its own prompt: chunk k runs + # only when the diff has k chunks and chunk k-1 judged and handed nothing + # on, so a handoff hands the whole judgment on and the next leg judges + # every chunk again. The leg check reads every chunk's log so far. + - name: Time chunk 2 + id: time-2 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 2 && steps.judge.outcome == 'success' && steps.judge.outputs.handoff != 'true' && steps.chunk-fail.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 2 + id: judge-2 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 2 && steps.judge.outcome == 'success' && steps.judge.outputs.handoff != 'true' && steps.chunk-fail.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-2.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Check the subscription leg, chunk 2 + id: sub-leg-2 + if: steps.judge-2.outputs.handoff == 'true' + continue-on-error: true + env: + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + HANDOFF_REASON: ${{ steps.judge-2.outputs.handoff-reason }} + run: | + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + + - name: Report chunk 2's outcome + if: (steps.judge-2.outcome == 'success' || steps.judge-2.outcome == 'failure') && (steps.judge-2.outputs.handoff != 'true' || steps.sub-leg-2.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-2.outcome }} + execution-file: ${{ steps.judge-2.outputs.execution-file }} + log-file: ${{ steps.judge-2.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-2.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-2.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-2.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + + - name: Upload chunk 2's sealed transcript + if: always() && steps.judge-2.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-2.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-2 + + - name: Read chunk 2's verdict + id: chunk-fail-2 + if: ${{ !cancelled() && steps.judge-2.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-2.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-2.outputs.log-file }} + CHUNK: "2" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + - name: Time chunk 3 + id: time-3 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 3 && steps.judge-2.outcome == 'success' && steps.judge-2.outputs.handoff != 'true' && steps.chunk-fail-2.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 3 + id: judge-3 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 3 && steps.judge-2.outcome == 'success' && steps.judge-2.outputs.handoff != 'true' && steps.chunk-fail-2.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-3.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Check the subscription leg, chunk 3 + id: sub-leg-3 + if: steps.judge-3.outputs.handoff == 'true' + continue-on-error: true + env: + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + HANDOFF_REASON: ${{ steps.judge-3.outputs.handoff-reason }} + run: | + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + + - name: Report chunk 3's outcome + if: (steps.judge-3.outcome == 'success' || steps.judge-3.outcome == 'failure') && (steps.judge-3.outputs.handoff != 'true' || steps.sub-leg-3.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-3.outcome }} + execution-file: ${{ steps.judge-3.outputs.execution-file }} + log-file: ${{ steps.judge-3.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-3.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-3.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-3.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + + - name: Upload chunk 3's sealed transcript + if: always() && steps.judge-3.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-3.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-3 + + - name: Read chunk 3's verdict + id: chunk-fail-3 + if: ${{ !cancelled() && steps.judge-3.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-3.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-3.outputs.log-file }} + CHUNK: "3" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + - name: Time chunk 4 + id: time-4 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 4 && steps.judge-3.outcome == 'success' && steps.judge-3.outputs.handoff != 'true' && steps.chunk-fail-3.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 4 + id: judge-4 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 4 && steps.judge-3.outcome == 'success' && steps.judge-3.outputs.handoff != 'true' && steps.chunk-fail-3.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-4.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Check the subscription leg, chunk 4 + id: sub-leg-4 + if: steps.judge-4.outputs.handoff == 'true' + continue-on-error: true + env: + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + HANDOFF_REASON: ${{ steps.judge-4.outputs.handoff-reason }} + run: | + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + + - name: Report chunk 4's outcome + if: (steps.judge-4.outcome == 'success' || steps.judge-4.outcome == 'failure') && (steps.judge-4.outputs.handoff != 'true' || steps.sub-leg-4.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-4.outcome }} + execution-file: ${{ steps.judge-4.outputs.execution-file }} + log-file: ${{ steps.judge-4.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-4.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-4.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-4.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + + - name: Upload chunk 4's sealed transcript + if: always() && steps.judge-4.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-4.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-4 + + - name: Read chunk 4's verdict + id: chunk-fail-4 + if: ${{ !cancelled() && steps.judge-4.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-4.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-4.outputs.log-file }} + CHUNK: "4" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + + - name: Time chunk 5 + id: time-5 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 5 && steps.judge-4.outcome == 'success' && steps.judge-4.outputs.handoff != 'true' && steps.chunk-fail-4.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 5 + id: judge-5 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 5 && steps.judge-4.outcome == 'success' && steps.judge-4.outputs.handoff != 'true' && steps.chunk-fail-4.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-5.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Check the subscription leg, chunk 5 + id: sub-leg-5 + if: steps.judge-5.outputs.handoff == 'true' + continue-on-error: true + env: + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + HANDOFF_REASON: ${{ steps.judge-5.outputs.handoff-reason }} + run: | + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + + - name: Report chunk 5's outcome + if: (steps.judge-5.outcome == 'success' || steps.judge-5.outcome == 'failure') && (steps.judge-5.outputs.handoff != 'true' || steps.sub-leg-5.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-5.outcome }} + execution-file: ${{ steps.judge-5.outputs.execution-file }} + log-file: ${{ steps.judge-5.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-5.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-5.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-5.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + + - name: Upload chunk 5's sealed transcript + if: always() && steps.judge-5.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-5.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-5 + + - name: Read chunk 5's verdict + id: chunk-fail-5 + if: ${{ !cancelled() && steps.judge-5.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-5.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-5.outputs.log-file }} + CHUNK: "5" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + - name: Time chunk 6 + id: time-6 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 6 && steps.judge-5.outcome == 'success' && steps.judge-5.outputs.handoff != 'true' && steps.chunk-fail-5.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 6 + id: judge-6 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 6 && steps.judge-5.outcome == 'success' && steps.judge-5.outputs.handoff != 'true' && steps.chunk-fail-5.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-6.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Check the subscription leg, chunk 6 + id: sub-leg-6 + if: steps.judge-6.outputs.handoff == 'true' + continue-on-error: true + env: + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + HANDOFF_REASON: ${{ steps.judge-6.outputs.handoff-reason }} + run: | + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + + - name: Report chunk 6's outcome + if: (steps.judge-6.outcome == 'success' || steps.judge-6.outcome == 'failure') && (steps.judge-6.outputs.handoff != 'true' || steps.sub-leg-6.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-6.outcome }} + execution-file: ${{ steps.judge-6.outputs.execution-file }} + log-file: ${{ steps.judge-6.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-6.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-6.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-6.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + + - name: Upload chunk 6's sealed transcript + if: always() && steps.judge-6.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-6.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-6 + + - name: Read chunk 6's verdict + id: chunk-fail-6 + if: ${{ !cancelled() && steps.judge-6.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-6.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-6.outputs.log-file }} + CHUNK: "6" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + - name: Time chunk 7 + id: time-7 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 7 && steps.judge-6.outcome == 'success' && steps.judge-6.outputs.handoff != 'true' && steps.chunk-fail-6.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 7 + id: judge-7 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 7 && steps.judge-6.outcome == 'success' && steps.judge-6.outputs.handoff != 'true' && steps.chunk-fail-6.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-7.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Check the subscription leg, chunk 7 + id: sub-leg-7 + if: steps.judge-7.outputs.handoff == 'true' + continue-on-error: true + env: + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + HANDOFF_REASON: ${{ steps.judge-7.outputs.handoff-reason }} + run: | + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + + - name: Report chunk 7's outcome + if: (steps.judge-7.outcome == 'success' || steps.judge-7.outcome == 'failure') && (steps.judge-7.outputs.handoff != 'true' || steps.sub-leg-7.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-7.outcome }} + execution-file: ${{ steps.judge-7.outputs.execution-file }} + log-file: ${{ steps.judge-7.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-7.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-7.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-7.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + + - name: Upload chunk 7's sealed transcript + if: always() && steps.judge-7.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-7.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-7 + + - name: Read chunk 7's verdict + id: chunk-fail-7 + if: ${{ !cancelled() && steps.judge-7.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-7.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-7.outputs.log-file }} + CHUNK: "7" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + - name: Time chunk 8 + id: time-8 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 8 && steps.judge-7.outcome == 'success' && steps.judge-7.outputs.handoff != 'true' && steps.chunk-fail-7.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 8 + id: judge-8 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 8 && steps.judge-7.outcome == 'success' && steps.judge-7.outputs.handoff != 'true' && steps.chunk-fail-7.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-8.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Check the subscription leg, chunk 8 + id: sub-leg-8 + if: steps.judge-8.outputs.handoff == 'true' + continue-on-error: true + env: + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + HANDOFF_REASON: ${{ steps.judge-8.outputs.handoff-reason }} + run: | + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + + - name: Report chunk 8's outcome + if: (steps.judge-8.outcome == 'success' || steps.judge-8.outcome == 'failure') && (steps.judge-8.outputs.handoff != 'true' || steps.sub-leg-8.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-8.outcome }} + execution-file: ${{ steps.judge-8.outputs.execution-file }} + log-file: ${{ steps.judge-8.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-8.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-8.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-8.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + + - name: Upload chunk 8's sealed transcript + if: always() && steps.judge-8.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-8.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-8 + + - name: Read chunk 8's verdict + id: chunk-fail-8 + if: ${{ !cancelled() && steps.judge-8.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-8.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-8.outputs.log-file }} + CHUNK: "8" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + - name: Time chunk 9 + id: time-9 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 9 && steps.judge-8.outcome == 'success' && steps.judge-8.outputs.handoff != 'true' && steps.chunk-fail-8.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 9 + id: judge-9 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 9 && steps.judge-8.outcome == 'success' && steps.judge-8.outputs.handoff != 'true' && steps.chunk-fail-8.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-9.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Check the subscription leg, chunk 9 + id: sub-leg-9 + if: steps.judge-9.outputs.handoff == 'true' + continue-on-error: true + env: + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + HANDOFF_REASON: ${{ steps.judge-9.outputs.handoff-reason }} + run: | + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + + - name: Report chunk 9's outcome + if: (steps.judge-9.outcome == 'success' || steps.judge-9.outcome == 'failure') && (steps.judge-9.outputs.handoff != 'true' || steps.sub-leg-9.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-9.outcome }} + execution-file: ${{ steps.judge-9.outputs.execution-file }} + log-file: ${{ steps.judge-9.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-9.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-9.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-9.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + + - name: Upload chunk 9's sealed transcript + if: always() && steps.judge-9.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-9.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-9 + + - name: Read chunk 9's verdict + id: chunk-fail-9 + if: ${{ !cancelled() && steps.judge-9.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-9.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-9.outputs.log-file }} + CHUNK: "9" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + - name: Time chunk 10 + id: time-10 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 10 && steps.judge-9.outcome == 'success' && steps.judge-9.outputs.handoff != 'true' && steps.chunk-fail-9.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 10 + id: judge-10 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 10 && steps.judge-9.outcome == 'success' && steps.judge-9.outputs.handoff != 'true' && steps.chunk-fail-9.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-10.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Check the subscription leg, chunk 10 + id: sub-leg-10 + if: steps.judge-10.outputs.handoff == 'true' + continue-on-error: true + env: + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + ${{ steps.judge-10.outputs.log-file }} + HANDOFF_REASON: ${{ steps.judge-10.outputs.handoff-reason }} + run: | + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + + - name: Report chunk 10's outcome + if: (steps.judge-10.outcome == 'success' || steps.judge-10.outcome == 'failure') && (steps.judge-10.outputs.handoff != 'true' || steps.sub-leg-10.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-10.outcome }} + execution-file: ${{ steps.judge-10.outputs.execution-file }} + log-file: ${{ steps.judge-10.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-10.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-10.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-10.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + + - name: Upload chunk 10's sealed transcript + if: always() && steps.judge-10.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-10.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-10 + + - name: Read chunk 10's verdict + id: chunk-fail-10 + if: ${{ !cancelled() && steps.judge-10.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-10.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-10.outputs.log-file }} + CHUNK: "10" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + - name: Time chunk 11 + id: time-11 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 11 && steps.judge-10.outcome == 'success' && steps.judge-10.outputs.handoff != 'true' && steps.chunk-fail-10.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 11 + id: judge-11 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 11 && steps.judge-10.outcome == 'success' && steps.judge-10.outputs.handoff != 'true' && steps.chunk-fail-10.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-11.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Check the subscription leg, chunk 11 + id: sub-leg-11 + if: steps.judge-11.outputs.handoff == 'true' + continue-on-error: true + env: + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + ${{ steps.judge-10.outputs.log-file }} + ${{ steps.judge-11.outputs.log-file }} + HANDOFF_REASON: ${{ steps.judge-11.outputs.handoff-reason }} + run: | + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + + - name: Report chunk 11's outcome + if: (steps.judge-11.outcome == 'success' || steps.judge-11.outcome == 'failure') && (steps.judge-11.outputs.handoff != 'true' || steps.sub-leg-11.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-11.outcome }} + execution-file: ${{ steps.judge-11.outputs.execution-file }} + log-file: ${{ steps.judge-11.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-11.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-11.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-11.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + + - name: Upload chunk 11's sealed transcript + if: always() && steps.judge-11.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-11.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-11 + + - name: Read chunk 11's verdict + id: chunk-fail-11 + if: ${{ !cancelled() && steps.judge-11.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-11.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-11.outputs.log-file }} + CHUNK: "11" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + - name: Time chunk 12 + id: time-12 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 12 && steps.judge-11.outcome == 'success' && steps.judge-11.outputs.handoff != 'true' && steps.chunk-fail-11.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 12 + id: judge-12 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 12 && steps.judge-11.outcome == 'success' && steps.judge-11.outputs.handoff != 'true' && steps.chunk-fail-11.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-12.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Check the subscription leg, chunk 12 + id: sub-leg-12 + if: steps.judge-12.outputs.handoff == 'true' + continue-on-error: true + env: + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + ${{ steps.judge-10.outputs.log-file }} + ${{ steps.judge-11.outputs.log-file }} + ${{ steps.judge-12.outputs.log-file }} + HANDOFF_REASON: ${{ steps.judge-12.outputs.handoff-reason }} + run: | + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + + - name: Report chunk 12's outcome + if: (steps.judge-12.outcome == 'success' || steps.judge-12.outcome == 'failure') && (steps.judge-12.outputs.handoff != 'true' || steps.sub-leg-12.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-12.outcome }} + execution-file: ${{ steps.judge-12.outputs.execution-file }} + log-file: ${{ steps.judge-12.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-12.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-12.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-12.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + + - name: Upload chunk 12's sealed transcript + if: always() && steps.judge-12.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-12.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-12 + + - name: Read chunk 12's verdict + id: chunk-fail-12 + if: ${{ !cancelled() && steps.judge-12.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-12.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-12.outputs.log-file }} + CHUNK: "12" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + # A diff in chunks: one more judge, with no diff, reads the spec and every + # chunk's checked result and fails an item no chunk delivers. The results + # are model text, checked as any verdict is and read as data; a chunk that + # fails the check fails here, so the head gets no verdict. The deny rule + # keeps this judge off the diff, and a denial voids the verdict. + - name: Write the chunk results + id: chunk-results + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') > 1 && steps.judge.outcome == 'success' && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-2.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 3 || steps.judge-3.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 4 || steps.judge-4.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 5 || steps.judge-5.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 6 || steps.judge-6.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 7 || steps.judge-7.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 8 || steps.judge-8.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 9 || steps.judge-9.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 10 || steps.judge-10.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 11 || steps.judge-11.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 12 || steps.judge-12.outcome == 'success') && steps.chunk-fail.outputs.fail != 'true' && steps.chunk-fail-2.outputs.fail != 'true' && steps.chunk-fail-3.outputs.fail != 'true' && steps.chunk-fail-4.outputs.fail != 'true' && steps.chunk-fail-5.outputs.fail != 'true' && steps.chunk-fail-6.outputs.fail != 'true' && steps.chunk-fail-7.outputs.fail != 'true' && steps.chunk-fail-8.outputs.fail != 'true' && steps.chunk-fail-9.outputs.fail != 'true' && steps.chunk-fail-10.outputs.fail != 'true' && steps.chunk-fail-11.outputs.fail != 'true' && steps.chunk-fail-12.outputs.fail != 'true' + env: + EXECUTION_FILES: | + ${{ steps.judge.outputs.execution-file }} + ${{ steps.judge-2.outputs.execution-file }} + ${{ steps.judge-3.outputs.execution-file }} + ${{ steps.judge-4.outputs.execution-file }} + ${{ steps.judge-5.outputs.execution-file }} + ${{ steps.judge-6.outputs.execution-file }} + ${{ steps.judge-7.outputs.execution-file }} + ${{ steps.judge-8.outputs.execution-file }} + ${{ steps.judge-9.outputs.execution-file }} + ${{ steps.judge-10.outputs.execution-file }} + ${{ steps.judge-11.outputs.execution-file }} + ${{ steps.judge-12.outputs.execution-file }} + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + ${{ steps.judge-10.outputs.log-file }} + ${{ steps.judge-11.outputs.log-file }} + ${{ steps.judge-12.outputs.log-file }} + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + RESULTS_DIR: ${{ github.workspace }}/pr-verify-data/chunk-results + SPEC_TEXT_FILE: ${{ github.workspace }}/pr-verify-data/spec-text.json + SPEC_FILE: ${{ github.workspace }}/pr-verify-data/spec.json + CONTEXT_FILE: ${{ github.workspace }}/pr-verify-data/context.json + PR_FILE: ${{ github.workspace }}/pr-verify-data/pr.json + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + PROMPT_FILE: ${{ runner.temp }}/pr-verify-prompt-spec.md + PROMPT_TEMPLATE: .base/.github/scripts/pr-verify/spec-prompt.md + SCHEMA_FILE: ${{ runner.temp }}/pr-verify-spec-schema.json + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-results + + - name: Time the spec judgment + id: time-spec + if: fromJSON(steps.diff.outputs.chunks || '0') > 1 && steps.chunk-results.outcome == 'success' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge the whole spec + id: judge-spec + if: fromJSON(steps.diff.outputs.chunks || '0') > 1 && steps.chunk-results.outcome == 'success' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-spec.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + subscription-token: ${{ secrets.PR_VERIFY_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Read(./pr-verify-data/diff/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-spec-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Check the subscription leg, whole spec + id: sub-leg-spec + if: steps.judge-spec.outputs.handoff == 'true' + continue-on-error: true + env: + CHUNKS: ${{ steps.diff.outputs.chunks }} + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + ${{ steps.judge-10.outputs.log-file }} + ${{ steps.judge-11.outputs.log-file }} + ${{ steps.judge-12.outputs.log-file }} + ${{ steps.judge-spec.outputs.log-file }} + HANDOFF_REASON: ${{ steps.judge-spec.outputs.handoff-reason }} + run: | + node .base/.github/scripts/pr-verify/write-verdict.mjs check-key-leg + echo "::notice::the subscription leg handed off (${HANDOFF_REASON}); check-intent-fallback judges on the fallback token" + + - name: Report the spec judgment's outcome + if: (steps.judge-spec.outcome == 'success' || steps.judge-spec.outcome == 'failure') && (steps.judge-spec.outputs.handoff != 'true' || steps.sub-leg-spec.outcome == 'failure') + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-spec.outcome }} + execution-file: ${{ steps.judge-spec.outputs.execution-file }} + log-file: ${{ steps.judge-spec.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-spec.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-spec.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-spec.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.check-intent.outputs.lane-class == 'auth' && 'auth' || needs.check-intent.outputs.credential-present == 'false' && 'no-key' || '' }} + + - name: Upload the spec judgment's sealed transcript + if: always() && steps.judge-spec.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-spec.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-spec + + # The structured output, in the last result message of the execution + # file, is untrusted model text: read from the file and checked by the + # script, never interpolated into a command. + - name: Write the verdict + id: verdict + if: ${{ !cancelled() && (steps.chunk-fail.outputs.fail == 'true' || steps.chunk-fail-2.outputs.fail == 'true' || steps.chunk-fail-3.outputs.fail == 'true' || steps.chunk-fail-4.outputs.fail == 'true' || steps.chunk-fail-5.outputs.fail == 'true' || steps.chunk-fail-6.outputs.fail == 'true' || steps.chunk-fail-7.outputs.fail == 'true' || steps.chunk-fail-8.outputs.fail == 'true' || steps.chunk-fail-9.outputs.fail == 'true' || steps.chunk-fail-10.outputs.fail == 'true' || steps.chunk-fail-11.outputs.fail == 'true' || steps.chunk-fail-12.outputs.fail == 'true' || steps.chunk-results.outcome == 'failure' || (steps.judge.outcome == 'success' && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-2.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 3 || steps.judge-3.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 4 || steps.judge-4.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 5 || steps.judge-5.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 6 || steps.judge-6.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 7 || steps.judge-7.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 8 || steps.judge-8.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 9 || steps.judge-9.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 10 || steps.judge-10.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 11 || steps.judge-11.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 12 || steps.judge-12.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-spec.outcome == 'success'))) }} + env: + # Each chunk's files in chunk order. A counted fail needs no others; + # anything else needs every chunk and the final judge. + EXECUTION_FILES: | + ${{ steps.judge.outputs.execution-file }} + ${{ steps.judge-2.outputs.execution-file }} + ${{ steps.judge-3.outputs.execution-file }} + ${{ steps.judge-4.outputs.execution-file }} + ${{ steps.judge-5.outputs.execution-file }} + ${{ steps.judge-6.outputs.execution-file }} + ${{ steps.judge-7.outputs.execution-file }} + ${{ steps.judge-8.outputs.execution-file }} + ${{ steps.judge-9.outputs.execution-file }} + ${{ steps.judge-10.outputs.execution-file }} + ${{ steps.judge-11.outputs.execution-file }} + ${{ steps.judge-12.outputs.execution-file }} + LOG_FILES: | + ${{ steps.judge.outputs.log-file }} + ${{ steps.judge-2.outputs.log-file }} + ${{ steps.judge-3.outputs.log-file }} + ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + ${{ steps.judge-10.outputs.log-file }} + ${{ steps.judge-11.outputs.log-file }} + ${{ steps.judge-12.outputs.log-file }} + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + # The whole-spec judgment, which a head of two chunks or more must have. + FINAL_EXECUTION_FILE: ${{ steps.judge-spec.outputs.execution-file }} + FINAL_LOG_FILE: ${{ steps.judge-spec.outputs.log-file }} + RESULTS_DIR: ${{ github.workspace }}/pr-verify-data/chunk-results + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + PR_NUMBER: ${{ steps.pr.outputs.pr-number }} + HEAD_SHA: ${{ steps.pr.outputs.head-sha }} + BASE_SHA: ${{ steps.pr.outputs.base-sha }} + HEAD_REF: ${{ steps.pr.outputs.head-ref }} + RELINK_SINCE_HEAD: ${{ steps.pr.outputs.relink-since-head }} + SPEC_FILE: ${{ github.workspace }}/pr-verify-data/spec.json + SPEC_TEXT_FILE: ${{ github.workspace }}/pr-verify-data/spec-text.json + VERDICT_DIR: ${{ runner.temp }}/pr-verify-verdict + run: node .base/.github/scripts/pr-verify/write-verdict.mjs + + - name: Upload the verdict + if: ${{ !cancelled() && steps.verdict.outcome == 'success' }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: ${{ steps.verdict.outputs.name }} + path: ${{ runner.temp }}/pr-verify-verdict/verdict.json + retention-days: ${{ steps.verdict.outputs.retention-days }} + if-no-files-found: error + + - name: Summarize the verdict + if: ${{ !cancelled() && steps.verdict.outcome == 'success' }} + env: + VERDICT_FILE: ${{ runner.temp }}/pr-verify-verdict/verdict.json + run: | + { + echo "### Verifier verdict" + echo + echo '```json' + jq '.summary |= gsub("`"; "'"'"'") | .notes |= map(gsub("`"; "'"'"'"))' "$VERDICT_FILE" + echo '```' + } >>"$GITHUB_STEP_SUMMARY" + + - name: Log the API budget + if: always() + continue-on-error: true + timeout-minutes: 1 + uses: ./.base/.github/actions/log-api-budget # zizmor: ignore[self-repository] + with: + tokens: github-token + + # A clean handoff hands the judgment on; any other run that passed the + # gates and left no verdict, an empty token included, fails here. + - name: Fail without a verdict + if: always() && steps.agree.outcome == 'success' && steps.verdict.outcome != 'success' && steps.sub-leg.outcome != 'success' && steps.sub-leg-2.outcome != 'success' && steps.sub-leg-3.outcome != 'success' && steps.sub-leg-4.outcome != 'success' && steps.sub-leg-5.outcome != 'success' && steps.sub-leg-6.outcome != 'success' && steps.sub-leg-7.outcome != 'success' && steps.sub-leg-8.outcome != 'success' && steps.sub-leg-9.outcome != 'success' && steps.sub-leg-10.outcome != 'success' && steps.sub-leg-11.outcome != 'success' && steps.sub-leg-12.outcome != 'success' && steps.sub-leg-spec.outcome != 'success' + run: | + echo "::error::the judgment left no verdict for this head" + exit 1 + + # The fallback leg, the central fallback subscription, as its own job on a + # new VM, only after the subscription job handed off (a limit stop, a + # credit stop, or a 401 or 402, with the fallback token set) with no + # permission denial. Its steps repeat the subscription job's but for the + # credential, the leg check and the fallback reason (workflow.test.mjs holds + # them equal). It reads three outputs of the subscription job, each compared + # to a fixed string in its `if` or the fallback-reason expression, never + # written into a `run:` script. It is the last leg and never hands off: an + # empty subscription token fails the subscription job instead. + check-intent-fallback: + needs: check-intent-subscription + if: >- + github.ref == 'refs/heads/main' + && github.event.workflow_run.event == 'pull_request' + && (github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure') + && github.event.workflow_run.head_repository.full_name == github.repository + && needs.check-intent-subscription.outputs.handoff == 'true' + && needs.check-intent-subscription.outputs.leg-clean == 'true' + runs-on: ubuntu-24.04 + timeout-minutes: 301 + permissions: + contents: read # check out main; read the PR's commits for the diff + pull-requests: read # the PR, its comments and reviews + actions: read # the upstream run, and verdicts already uploaded + steps: + - name: Check out the default branch tip + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: refs/heads/main + path: .base + persist-credentials: false + + - name: Set up Node + uses: ./.base/.github/actions/setup-pinned-node # zizmor: ignore[self-repository] + with: + node-version-file: .base/.node-version + + - name: Check the kill switch + id: kill + uses: ./.base/.github/actions/check-kill-switch # zizmor: ignore[self-repository] + with: + value: ${{ vars.CLAUDE_LANES_DISABLED }} + repo-value: ${{ vars.CLAUDE_LANES_REPO_DISABLED }} + + # The run fails, so the lost-run reconciler re-runs it once the switch + # is off and the head is judged then. + - name: Report the kill switch + if: steps.kill.outputs.proceed != 'true' + env: + REASON: ${{ steps.kill.outputs.reason }} + run: | + echo "::error::the lanes kill switch stops this run (reason: ${REASON:-unreadable}); no verdict until the lost-run reconciler re-runs it" + exit 1 + + # The resolver reads only, and skips on its own reads every run it + # would not judge (a head already judged or moved past, a draft, a run + # red for anything an approval does not lift, an author it does not + # judge), so it runs first and the gate's reads go only to a run it + # would judge. Nothing it writes reaches the model before the gate and + # the agree step pass. + - name: Resolve the pull request + id: pr + if: steps.kill.outputs.proceed == 'true' + env: + GITHUB_TOKEN: ${{ github.token }} + API_BUDGET_TOKEN: github-token + UPSTREAM_RUN_ID: ${{ github.event.workflow_run.id }} + SPEC_FILE: ${{ github.workspace }}/pr-verify-data/spec.json + SPEC_TEXT_FILE: ${{ github.workspace }}/pr-verify-data/spec-text.json + TRUSTED_ACTORS_PATH: .base/.github/standards/trusted-actors/trusted-actors.json + run: | + mkdir -p "$(dirname "$SPEC_FILE")" + VERIFY_AUTHORS=$(yq '.merge."verify-authors" // "lane"' .base/docs/conventions/pr-pipeline.yaml) + LANE_IDENTITY=$(yq '.merge."agent-identity" // ""' .base/docs/conventions/pr-pipeline.yaml) + SPEC_FROM_BODY_APPS=$(yq '.adoption."spec-from-body-apps" // [] | .[]' .base/docs/conventions/pr-pipeline.yaml) + # The issue lane's web hosts as admission reads them (resolve-config issueLaneWebHostsFromBase). + ISSUE_LANE_WEB_HOSTS=$(yq -o=json -I=0 '(.activities."resolve-issue"."web-access" | select(.enabled == true) | .domains) // []' .base/docs/conventions/pr-pipeline.yaml) + export VERIFY_AUTHORS LANE_IDENTITY SPEC_FROM_BODY_APPS ISSUE_LANE_WEB_HOSTS + node .base/.github/scripts/pr-verify/resolve-pr.mjs + + # The upstream run's actors, the re-runner and the PR author must all be + # on the trusted-actor list. A re-run github-actions[bot] started passes + # only as the lost-run reconciler's recorded re-run of this run. + - name: Check the trigger + id: gate + if: steps.pr.outputs.proceed == 'true' + # Each API call keeps its rate-limit reading for the api-budget + # notice the job logs before its last step. + env: + API_BUDGET_TOKEN: github-token + uses: ./.base/.github/actions/check-trusted-trigger # zizmor: ignore[self-repository] + with: + trusted-actors-path: .base/.github/standards/trusted-actors/trusted-actors.json + triggering-actor: ${{ github.triggering_actor }} + reconciler-workflow: .github/workflows/maintenance-reconcile-lost-runs.yml + ci-status-rerun-workflow: .github/workflows/pr-feedback-dispatch.yml + + # The gate and the resolver read the PR separately; both must name the + # same PR at the run's head, or nothing is judged. + - name: Assert the gate and the resolver agree + id: agree + if: steps.gate.outputs.proceed == 'true' + env: + GATE_PR: ${{ steps.gate.outputs.pr-number }} + GATE_HEAD: ${{ steps.gate.outputs.head-sha }} + PR_NUMBER: ${{ steps.pr.outputs.pr-number }} + HEAD_SHA: ${{ steps.pr.outputs.head-sha }} + run: | + if [ "$GATE_PR" != "$PR_NUMBER" ] || [ "$GATE_HEAD" != "$HEAD_SHA" ]; then + echo "::error::the trigger gate and the resolver name different pull requests or heads" + exit 1 + fi + + - name: Select the trusted PR text + if: steps.agree.outcome == 'success' + env: + API_BUDGET_TOKEN: github-token + uses: ./.base/.github/actions/select-trusted-text # zizmor: ignore[self-repository] + with: + pr-number: ${{ steps.pr.outputs.pr-number }} + trusted-actors-path: .base/.github/standards/trusted-actors/trusted-actors.json + output-path: ${{ github.workspace }}/pr-verify-data/context.json + github-token: ${{ github.token }} + + # A blobless fetch of the two commits into its own repository outside + # the workspace. git diff reads no attributes or diff drivers from it, + # so nothing in the PR runs; the patch is a data file. chunk-diff.sh cuts + # it into chunks of whole files, and each chunk into parts one Read + # returns whole, in as many chunks as the diff takes, up to the ceiling + # chunk-diff.sh sets, with a judge step below for each. A file over one + # chunk, or a diff over the ceiling, fails the step by name: no verdict, + # so no fail on size alone. + - name: Write the diff + id: diff + if: steps.agree.outcome == 'success' + env: + HEAD_SHA: ${{ steps.pr.outputs.head-sha }} + BASE_SHA: ${{ steps.pr.outputs.base-sha }} + PR_NUMBER: ${{ steps.pr.outputs.pr-number }} + DATA: ${{ github.workspace }}/pr-verify-data + SRC: ${{ runner.temp }}/pr-verify-src + # About 75k tokens of patch, the budget pr-review-main.yml sets: room + # in the review profile's turns to read one chunk whole (judgment). + MAX_DIFF_BYTES: "300000" + # One Read returns at most 25,000 tokens + # (https://code.claude.com/docs/en/tools-reference#large-files), and + # a token is at least a byte, so a part this size comes back whole. + PART_BYTES: "15000" + PART_LINES: "1000" + run: | + export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null + mkdir -p "$DATA" + git init -q "$SRC" + git -C "$SRC" remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" + git -C "$SRC" fetch -q --no-tags --filter=blob:none origin "$BASE_SHA" "$HEAD_SHA" + git -C "$SRC" diff --no-ext-diff --no-textconv --no-color "$BASE_SHA...$HEAD_SHA" >"$DATA/diff.patch" + jq -n --argjson pr "$PR_NUMBER" --arg head "$HEAD_SHA" --arg base "$BASE_SHA" \ + '{pr: $pr, head_sha: $head, base_sha: $base}' >"$DATA/pr.json" + echo "diff: $(wc -l <"$DATA/diff.patch") lines" + bash .base/.github/scripts/pr-verify/chunk-diff.sh + + # The jobs of the run this one was started by, each with its name and + # conclusion, so the model can judge whether the suites that cover the + # change ran (prompt.md). The token reads only. + - name: List the upstream run's jobs + if: steps.agree.outcome == 'success' + env: + GITHUB_TOKEN: ${{ github.token }} + API_BUDGET_TOKEN: github-token + UPSTREAM_RUN_ID: ${{ github.event.workflow_run.id }} + JOBS_FILE: ${{ github.workspace }}/pr-verify-data/upstream-jobs.json + run: node .base/.github/scripts/pr-verify/upstream-jobs.mjs + + # The judgment's model, turns and effort: the verify-pr route of the + # base config's models block. + - name: Resolve the verifier's model + id: models + if: steps.agree.outcome == 'success' + uses: ./.base/.github/actions/resolve-config # zizmor: ignore[self-repository] + with: + model-route: verify-pr + output-path: ${{ runner.temp }}/pr-verify-models.json + + # Only whether the secret is set leaves this step, never its value. + - name: Check the credential + id: credential + if: steps.agree.outcome == 'success' + env: + PRESENT: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN != '' }} + SECRET_NAME: FALLBACK_CLAUDE_CODE_OAUTH_TOKEN + # Whether the fallback leg can run, never the token. + FALLBACK_PRESENT: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN != '' }} + run: | + if [ "$PRESENT" = true ]; then + echo "present=true" >>"$GITHUB_OUTPUT" + else + echo "::warning::$SECRET_NAME is empty; this job does not judge." + echo "present=false" >>"$GITHUB_OUTPUT" + fi + if [ "$FALLBACK_PRESENT" = true ]; then + echo "fallback-ready=true" >>"$GITHUB_OUTPUT" + else + echo "fallback-ready=false" >>"$GITHUB_OUTPUT" + fi + + # run-claude-cli refuses to run without these root-owned settings, which + # need sudo, so they are written before the runner is isolated. + - name: Apply the Claude managed settings + if: steps.agree.outcome == 'success' + uses: melodic-software/ci-workflows/.github/actions/apply-claude-managed-settings@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + + - name: Isolate the runner + if: steps.agree.outcome == 'success' + uses: ./.base/.github/actions/setup-runner-isolation # zizmor: ignore[self-repository] + with: + install-subprocess-isolation: true + + # Files, not step outputs: run-claude-cli reads both. A `default` model + # resolves to an empty model, which the launcher cannot pass. started-at + # lets the outcome step tell a step timeout from other failures. + - name: Write the prompt and schema + id: prompt + if: steps.agree.outcome == 'success' + env: + DATA: ${{ github.workspace }}/pr-verify-data + MODEL: ${{ steps.models.outputs.model }} + CHUNKS: ${{ steps.diff.outputs.chunks }} + PROMPT_STEM: ${{ runner.temp }}/pr-verify-prompt + SCHEMA_FILE: ${{ runner.temp }}/pr-verify-schema.json + run: | + if [ -z "$MODEL" ]; then + echo "::error::the verify-pr profile's model is default, which run-claude-cli cannot pass; name an alias" + exit 1 + fi + if ! [[ $CHUNKS =~ ^[1-9][0-9]?$ ]]; then + echo "::error::the chunk count is malformed" + exit 1 + fi + template=$(<.base/.github/scripts/pr-verify/prompt.md) + for k in $(seq "$CHUNKS"); do + prompt=$template + # shellcheck disable=SC2016 # the backticks are Markdown, not a command + parts=$(printf ' - `%s`\n' "$DATA/diff/chunk-$k"/part-*.patch) + prompt=${prompt//DIFF_PARTS/$parts} + prompt=${prompt//CONTEXT_FILE/$DATA/context.json} + prompt=${prompt//PR_FILE/$DATA/pr.json} + prompt=${prompt//SPEC_TEXT_FILE/$DATA/spec-text.json} + prompt=${prompt//SPEC_FILE/$DATA/spec.json} + prompt=${prompt//JOBS_FILE/$DATA/upstream-jobs.json} + note= + if [ "$CHUNKS" -gt 1 ]; then + note=$(<.base/.github/scripts/pr-verify/chunk-note.md) + # shellcheck disable=SC2016 # the backticks are Markdown, not a command + manifest=$(printf ' - `%s`\n' "$DATA"/manifest/part-*.txt) + note=${note//MANIFEST_PARTS/$manifest} + note=${note//DATA_DIR/$DATA} + note=${note//CHUNK_INDEX/$k} + note=${note//CHUNK_COUNT/$CHUNKS} + note=$'\n'$note + fi + prompt=${prompt//CHUNK_NOTE/"$note"} + printf '%s\n' "$prompt" >"$PROMPT_STEM-$k.md" + done + printf '%s\n' '{"type":"object","properties":{"verdict":{"enum":["pass","pass-notes","fail"]},"summary":{"type":"string"},"notes":{"type":"array","items":{"type":"string"}}},"required":["verdict","summary","notes"],"additionalProperties":false}' >"$SCHEMA_FILE" + echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + # continue-on-error keeps a launcher failure, which run-claude-cli + # reports by failing this step, from ending the job before the outcome + # step; "Fail without a verdict" carries the red. The fallback leg + # never hands off. One leg of + # leg-timeout-minutes and the CLI install fit in timeout-minutes, which + # the outcome step repeats, and the job's timeout holds one of those for + # every judge step. The model may share its job with the upload: it has + # Read alone and the job token can only read. + # Under dontAsk only the allow rule's directory is readable; the deny + # rules hold if the tools or the working directory change. + - name: Judge the pull request + id: judge + if: steps.credential.outputs.present == 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + # No work may outlive the result message the launcher reads. + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-1.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + fallback-token: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Report the judgment's outcome + if: steps.judge.outcome == 'success' || steps.judge.outcome == 'failure' + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge.outcome }} + execution-file: ${{ steps.judge.outputs.execution-file }} + log-file: ${{ steps.judge.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.prompt.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge.outputs.lane-class }} + # Why this job ran, for the run's one lane record. + fallback-reason: ${{ needs.check-intent-subscription.outputs.handoff-reason == 'limit-stop' && 'limit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'credit-stop' && 'credit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'auth' && 'auth' || '' }} + + # The leg's transcript, failed legs included, sealed to the transcript + # key and kept 7 days for the puller; a failed upload never fails the job. + - name: Upload the sealed transcript + if: always() && steps.judge.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }} + + # A fail the head can count ends the judgment here (write-verdict.mjs + # checkChunk): the later chunks, the final judge and any handoff are + # skipped, and "Write the verdict" runs on it, so no later step can lose + # it to a re-run. A step that fails before it does not stop the check. + - name: Read chunk 1's verdict + id: chunk-fail + if: ${{ !cancelled() && steps.judge.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge.outputs.execution-file }} + LOG_FILE: ${{ steps.judge.outputs.log-file }} + CHUNK: "1" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + # Chunk 2 on, each the first chunk's steps on its own prompt: chunk k runs + # only when the diff has k chunks and chunk k-1 judged and handed nothing + # on, so a handoff hands the whole judgment on and the next leg judges + # every chunk again. The leg check reads every chunk's log so far. + - name: Time chunk 2 + id: time-2 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 2 && steps.judge.outcome == 'success' && steps.judge.outputs.handoff != 'true' && steps.chunk-fail.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 2 + id: judge-2 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 2 && steps.judge.outcome == 'success' && steps.judge.outputs.handoff != 'true' && steps.chunk-fail.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-2.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + fallback-token: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Report chunk 2's outcome + if: steps.judge-2.outcome == 'success' || steps.judge-2.outcome == 'failure' + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-2.outcome }} + execution-file: ${{ steps.judge-2.outputs.execution-file }} + log-file: ${{ steps.judge-2.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-2.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-2.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-2.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent-subscription.outputs.handoff-reason == 'limit-stop' && 'limit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'credit-stop' && 'credit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'auth' && 'auth' || '' }} + + - name: Upload chunk 2's sealed transcript + if: always() && steps.judge-2.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-2.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-2 + + - name: Read chunk 2's verdict + id: chunk-fail-2 + if: ${{ !cancelled() && steps.judge-2.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-2.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-2.outputs.log-file }} + CHUNK: "2" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + - name: Time chunk 3 + id: time-3 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 3 && steps.judge-2.outcome == 'success' && steps.judge-2.outputs.handoff != 'true' && steps.chunk-fail-2.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 3 + id: judge-3 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 3 && steps.judge-2.outcome == 'success' && steps.judge-2.outputs.handoff != 'true' && steps.chunk-fail-2.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-3.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + fallback-token: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Report chunk 3's outcome + if: steps.judge-3.outcome == 'success' || steps.judge-3.outcome == 'failure' + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-3.outcome }} + execution-file: ${{ steps.judge-3.outputs.execution-file }} + log-file: ${{ steps.judge-3.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-3.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-3.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-3.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent-subscription.outputs.handoff-reason == 'limit-stop' && 'limit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'credit-stop' && 'credit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'auth' && 'auth' || '' }} + + - name: Upload chunk 3's sealed transcript + if: always() && steps.judge-3.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-3.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-3 + + - name: Read chunk 3's verdict + id: chunk-fail-3 + if: ${{ !cancelled() && steps.judge-3.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-3.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-3.outputs.log-file }} + CHUNK: "3" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + - name: Time chunk 4 + id: time-4 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 4 && steps.judge-3.outcome == 'success' && steps.judge-3.outputs.handoff != 'true' && steps.chunk-fail-3.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 4 + id: judge-4 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 4 && steps.judge-3.outcome == 'success' && steps.judge-3.outputs.handoff != 'true' && steps.chunk-fail-3.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-4.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + fallback-token: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Report chunk 4's outcome + if: steps.judge-4.outcome == 'success' || steps.judge-4.outcome == 'failure' + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-4.outcome }} + execution-file: ${{ steps.judge-4.outputs.execution-file }} + log-file: ${{ steps.judge-4.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-4.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-4.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-4.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent-subscription.outputs.handoff-reason == 'limit-stop' && 'limit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'credit-stop' && 'credit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'auth' && 'auth' || '' }} + + - name: Upload chunk 4's sealed transcript + if: always() && steps.judge-4.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-4.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-4 + + - name: Read chunk 4's verdict + id: chunk-fail-4 + if: ${{ !cancelled() && steps.judge-4.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-4.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-4.outputs.log-file }} + CHUNK: "4" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + + - name: Time chunk 5 + id: time-5 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 5 && steps.judge-4.outcome == 'success' && steps.judge-4.outputs.handoff != 'true' && steps.chunk-fail-4.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 5 + id: judge-5 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 5 && steps.judge-4.outcome == 'success' && steps.judge-4.outputs.handoff != 'true' && steps.chunk-fail-4.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-5.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + fallback-token: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Report chunk 5's outcome + if: steps.judge-5.outcome == 'success' || steps.judge-5.outcome == 'failure' + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-5.outcome }} + execution-file: ${{ steps.judge-5.outputs.execution-file }} + log-file: ${{ steps.judge-5.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-5.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-5.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-5.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent-subscription.outputs.handoff-reason == 'limit-stop' && 'limit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'credit-stop' && 'credit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'auth' && 'auth' || '' }} + + - name: Upload chunk 5's sealed transcript + if: always() && steps.judge-5.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-5.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-5 + + - name: Read chunk 5's verdict + id: chunk-fail-5 + if: ${{ !cancelled() && steps.judge-5.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-5.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-5.outputs.log-file }} + CHUNK: "5" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + - name: Time chunk 6 + id: time-6 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 6 && steps.judge-5.outcome == 'success' && steps.judge-5.outputs.handoff != 'true' && steps.chunk-fail-5.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 6 + id: judge-6 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 6 && steps.judge-5.outcome == 'success' && steps.judge-5.outputs.handoff != 'true' && steps.chunk-fail-5.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-6.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + fallback-token: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Report chunk 6's outcome + if: steps.judge-6.outcome == 'success' || steps.judge-6.outcome == 'failure' + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-6.outcome }} + execution-file: ${{ steps.judge-6.outputs.execution-file }} + log-file: ${{ steps.judge-6.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-6.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-6.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-6.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent-subscription.outputs.handoff-reason == 'limit-stop' && 'limit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'credit-stop' && 'credit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'auth' && 'auth' || '' }} + + - name: Upload chunk 6's sealed transcript + if: always() && steps.judge-6.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-6.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-6 + + - name: Read chunk 6's verdict + id: chunk-fail-6 + if: ${{ !cancelled() && steps.judge-6.outcome == 'success' }} + continue-on-error: true + env: + EXECUTION_FILE: ${{ steps.judge-6.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-6.outputs.log-file }} + CHUNK: "6" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail + + - name: Time chunk 7 + id: time-7 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 7 && steps.judge-6.outcome == 'success' && steps.judge-6.outputs.handoff != 'true' && steps.chunk-fail-6.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 7 + id: judge-7 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 7 && steps.judge-6.outcome == 'success' && steps.judge-6.outputs.handoff != 'true' && steps.chunk-fail-6.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" + with: + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-7.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + fallback-token: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" + + - name: Report chunk 7's outcome + if: steps.judge-7.outcome == 'success' || steps.judge-7.outcome == 'failure' + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-7.outcome }} + execution-file: ${{ steps.judge-7.outputs.execution-file }} + log-file: ${{ steps.judge-7.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-7.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-7.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-7.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent-subscription.outputs.handoff-reason == 'limit-stop' && 'limit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'credit-stop' && 'credit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'auth' && 'auth' || '' }} + + - name: Upload chunk 7's sealed transcript + if: always() && steps.judge-7.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-7.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-7 - # Only whether the secret is set leaves this step, never its value. - - name: Check the credential - id: credential - if: steps.agree.outcome == 'success' + - name: Read chunk 7's verdict + id: chunk-fail-7 + if: ${{ !cancelled() && steps.judge-7.outcome == 'success' }} + continue-on-error: true env: - PRESENT: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN != '' }} - SECRET_NAME: FALLBACK_CLAUDE_CODE_OAUTH_TOKEN - # Whether the fallback leg can run, never the token. - FALLBACK_PRESENT: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN != '' }} - run: | - if [ "$PRESENT" = true ]; then - echo "present=true" >>"$GITHUB_OUTPUT" - else - echo "::warning::$SECRET_NAME is empty; this job does not judge." - echo "present=false" >>"$GITHUB_OUTPUT" - fi - if [ "$FALLBACK_PRESENT" = true ]; then - echo "fallback-ready=true" >>"$GITHUB_OUTPUT" - else - echo "fallback-ready=false" >>"$GITHUB_OUTPUT" - fi + EXECUTION_FILE: ${{ steps.judge-7.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-7.outputs.log-file }} + CHUNK: "7" + CHUNKS: ${{ steps.diff.outputs.chunks }} + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail - # run-claude-cli refuses to run without these root-owned settings, which - # need sudo, so they are written before the runner is isolated. - - name: Apply the Claude managed settings - if: steps.agree.outcome == 'success' - uses: melodic-software/ci-workflows/.github/actions/apply-claude-managed-settings@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + - name: Time chunk 8 + id: time-8 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 8 && steps.judge-7.outcome == 'success' && steps.judge-7.outputs.handoff != 'true' && steps.chunk-fail-7.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" - - name: Isolate the runner - if: steps.agree.outcome == 'success' - uses: ./.base/.github/actions/setup-runner-isolation # zizmor: ignore[self-repository] + - name: Judge chunk 8 + id: judge-8 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 8 && steps.judge-7.outcome == 'success' && steps.judge-7.outputs.handoff != 'true' && steps.chunk-fail-7.outputs.fail != 'true' + continue-on-error: true + timeout-minutes: 22 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + env: + CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: - install-subprocess-isolation: true + lane: pr-verify + role: pr-verify + prompt-file: ${{ runner.temp }}/pr-verify-prompt-8.md + preamble: >- + You run unattended; return only the verdict the schema asks for; + nothing you write is posted. + fallback-token: ${{ secrets.FALLBACK_CLAUDE_CODE_OAUTH_TOKEN }} + model: ${{ steps.models.outputs.model }} + effort: ${{ steps.models.outputs.effort }} + max-turns: ${{ steps.models.outputs.max-turns }} + permission-mode: dontAsk + tools: Read + allowed-tools: Read(./pr-verify-data/**) + disallowed-tools: | + Read(./.base/**) + Glob + Grep + Bash + Edit + Write + NotebookEdit + WebFetch + WebSearch + Agent + mcp__* + json-schema-file: ${{ runner.temp }}/pr-verify-schema.json + fallback-ready: ${{ steps.credential.outputs.fallback-ready }} + leg-timeout-minutes: "15" - # Files, not step outputs: run-claude-cli reads both. A `default` model - # resolves to an empty model, which the launcher cannot pass. started-at - # lets the outcome step tell a step timeout from other failures. - - name: Write the prompt and schema - id: prompt - if: steps.agree.outcome == 'success' + - name: Report chunk 8's outcome + if: steps.judge-8.outcome == 'success' || steps.judge-8.outcome == 'failure' + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 + with: + outcome: ${{ steps.judge-8.outcome }} + execution-file: ${{ steps.judge-8.outputs.execution-file }} + log-file: ${{ steps.judge-8.outputs.log-file }} + lane: pr-verify + lane-id: pr-verify + step-started-at: ${{ steps.time-8.outputs.started-at }} + step-timeout-minutes: 22 + credential: ${{ steps.judge-8.outputs.credential }} + max-turns: ${{ steps.models.outputs.max-turns }} + known-failure-class: ${{ steps.judge-8.outputs.lane-class }} + fallback-reason: ${{ needs.check-intent-subscription.outputs.handoff-reason == 'limit-stop' && 'limit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'credit-stop' && 'credit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'auth' && 'auth' || '' }} + + - name: Upload chunk 8's sealed transcript + if: always() && steps.judge-8.outputs.execution-file != '' + continue-on-error: true + uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] + with: + execution-file: ${{ steps.judge-8.outputs.execution-file }} + public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} + job-id: ${{ github.job }}-8 + + - name: Read chunk 8's verdict + id: chunk-fail-8 + if: ${{ !cancelled() && steps.judge-8.outcome == 'success' }} + continue-on-error: true env: - DATA: ${{ github.workspace }}/pr-verify-data - MODEL: ${{ steps.models.outputs.model }} + EXECUTION_FILE: ${{ steps.judge-8.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-8.outputs.log-file }} + CHUNK: "8" CHUNKS: ${{ steps.diff.outputs.chunks }} - PROMPT_STEM: ${{ runner.temp }}/pr-verify-prompt - SCHEMA_FILE: ${{ runner.temp }}/pr-verify-schema.json - run: | - if [ -z "$MODEL" ]; then - echo "::error::the verify-pr profile's model is default, which run-claude-cli cannot pass; name an alias" - exit 1 - fi - if ! [[ $CHUNKS =~ ^[1-9]$ ]]; then - echo "::error::the chunk count is malformed" - exit 1 - fi - template=$(<.base/.github/scripts/pr-verify/prompt.md) - for k in $(seq "$CHUNKS"); do - prompt=$template - # shellcheck disable=SC2016 # the backticks are Markdown, not a command - parts=$(printf ' - `%s`\n' "$DATA/diff/chunk-$k"/part-*.patch) - prompt=${prompt//DIFF_PARTS/$parts} - prompt=${prompt//CONTEXT_FILE/$DATA/context.json} - prompt=${prompt//PR_FILE/$DATA/pr.json} - prompt=${prompt//SPEC_TEXT_FILE/$DATA/spec-text.json} - prompt=${prompt//SPEC_FILE/$DATA/spec.json} - prompt=${prompt//JOBS_FILE/$DATA/upstream-jobs.json} - note= - if [ "$CHUNKS" -gt 1 ]; then - note=$(<.base/.github/scripts/pr-verify/chunk-note.md) - # shellcheck disable=SC2016 # the backticks are Markdown, not a command - manifest=$(printf ' - `%s`\n' "$DATA"/manifest/part-*.txt) - note=${note//MANIFEST_PARTS/$manifest} - note=${note//DATA_DIR/$DATA} - note=${note//CHUNK_INDEX/$k} - note=${note//CHUNK_COUNT/$CHUNKS} - note=$'\n'$note - fi - prompt=${prompt//CHUNK_NOTE/"$note"} - printf '%s\n' "$prompt" >"$PROMPT_STEM-$k.md" - done - printf '%s\n' '{"type":"object","properties":{"verdict":{"enum":["pass","pass-notes","fail"]},"summary":{"type":"string"},"notes":{"type":"array","items":{"type":"string"}}},"required":["verdict","summary","notes"],"additionalProperties":false}' >"$SCHEMA_FILE" - echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff + MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest + run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail - # continue-on-error keeps a launcher failure, which run-claude-cli - # reports by failing this step, from ending the job before the outcome - # step; "Fail without a verdict" carries the red. The fallback leg - # never hands off. One leg of - # leg-timeout-minutes and the CLI install fit in timeout-minutes, which - # the outcome step repeats, and five of those fit in the job's 125. The model may - # share its job with the upload: it has Read alone and the job token can - # only read. - # Under dontAsk only the allow rule's directory is readable; the deny - # rules hold if the tools or the working directory change. - - name: Judge the pull request - id: judge - if: steps.credential.outputs.present == 'true' + - name: Time chunk 9 + id: time-9 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 9 && steps.judge-8.outcome == 'success' && steps.judge-8.outputs.handoff != 'true' && steps.chunk-fail-8.outputs.fail != 'true' + run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" + + - name: Judge chunk 9 + id: judge-9 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 9 && steps.judge-8.outcome == 'success' && steps.judge-8.outputs.handoff != 'true' && steps.chunk-fail-8.outputs.fail != 'true' continue-on-error: true timeout-minutes: 22 uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 env: - # No work may outlive the result message the launcher reads. CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: lane: pr-verify role: pr-verify - prompt-file: ${{ runner.temp }}/pr-verify-prompt-1.md + prompt-file: ${{ runner.temp }}/pr-verify-prompt-9.md preamble: >- You run unattended; return only the verdict the schema asks for; nothing you write is posted. @@ -2161,64 +4548,53 @@ jobs: fallback-ready: ${{ steps.credential.outputs.fallback-ready }} leg-timeout-minutes: "15" - - name: Report the judgment's outcome - if: steps.judge.outcome == 'success' || steps.judge.outcome == 'failure' + - name: Report chunk 9's outcome + if: steps.judge-9.outcome == 'success' || steps.judge-9.outcome == 'failure' continue-on-error: true uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 with: - outcome: ${{ steps.judge.outcome }} - execution-file: ${{ steps.judge.outputs.execution-file }} - log-file: ${{ steps.judge.outputs.log-file }} + outcome: ${{ steps.judge-9.outcome }} + execution-file: ${{ steps.judge-9.outputs.execution-file }} + log-file: ${{ steps.judge-9.outputs.log-file }} lane: pr-verify lane-id: pr-verify - step-started-at: ${{ steps.prompt.outputs.started-at }} + step-started-at: ${{ steps.time-9.outputs.started-at }} step-timeout-minutes: 22 - credential: ${{ steps.judge.outputs.credential }} + credential: ${{ steps.judge-9.outputs.credential }} max-turns: ${{ steps.models.outputs.max-turns }} - known-failure-class: ${{ steps.judge.outputs.lane-class }} - # Why this job ran, for the run's one lane record. + known-failure-class: ${{ steps.judge-9.outputs.lane-class }} fallback-reason: ${{ needs.check-intent-subscription.outputs.handoff-reason == 'limit-stop' && 'limit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'credit-stop' && 'credit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'auth' && 'auth' || '' }} - # The leg's transcript, failed legs included, sealed to the transcript - # key and kept 7 days for the puller; a failed upload never fails the job. - - name: Upload the sealed transcript - if: always() && steps.judge.outputs.execution-file != '' + - name: Upload chunk 9's sealed transcript + if: always() && steps.judge-9.outputs.execution-file != '' continue-on-error: true uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] with: - execution-file: ${{ steps.judge.outputs.execution-file }} + execution-file: ${{ steps.judge-9.outputs.execution-file }} public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} - job-id: ${{ github.job }} + job-id: ${{ github.job }}-9 - # A fail the head can count ends the judgment here (write-verdict.mjs - # checkChunk): the later chunks, the final judge and any handoff are - # skipped, and "Write the verdict" runs on it, so no later step can lose - # it to a re-run. A step that fails before it does not stop the check. - - name: Read chunk 1's verdict - id: chunk-fail - if: ${{ !cancelled() && steps.judge.outcome == 'success' }} + - name: Read chunk 9's verdict + id: chunk-fail-9 + if: ${{ !cancelled() && steps.judge-9.outcome == 'success' }} continue-on-error: true env: - EXECUTION_FILE: ${{ steps.judge.outputs.execution-file }} - LOG_FILE: ${{ steps.judge.outputs.log-file }} - CHUNK: "1" + EXECUTION_FILE: ${{ steps.judge-9.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-9.outputs.log-file }} + CHUNK: "9" CHUNKS: ${{ steps.diff.outputs.chunks }} DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail - # Chunk 2 on, each the first chunk's steps on its own prompt: chunk k runs - # only when the diff has k chunks and chunk k-1 judged and handed nothing - # on, so a handoff hands the whole judgment on and the next leg judges - # every chunk again. The leg check reads every chunk's log so far. - - name: Time chunk 2 - id: time-2 - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 2 && steps.judge.outcome == 'success' && steps.judge.outputs.handoff != 'true' && steps.chunk-fail.outputs.fail != 'true' + - name: Time chunk 10 + id: time-10 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 10 && steps.judge-9.outcome == 'success' && steps.judge-9.outputs.handoff != 'true' && steps.chunk-fail-9.outputs.fail != 'true' run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" - - name: Judge chunk 2 - id: judge-2 - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 2 && steps.judge.outcome == 'success' && steps.judge.outputs.handoff != 'true' && steps.chunk-fail.outputs.fail != 'true' + - name: Judge chunk 10 + id: judge-10 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 10 && steps.judge-9.outcome == 'success' && steps.judge-9.outputs.handoff != 'true' && steps.chunk-fail-9.outputs.fail != 'true' continue-on-error: true timeout-minutes: 22 uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 @@ -2227,7 +4603,7 @@ jobs: with: lane: pr-verify role: pr-verify - prompt-file: ${{ runner.temp }}/pr-verify-prompt-2.md + prompt-file: ${{ runner.temp }}/pr-verify-prompt-10.md preamble: >- You run unattended; return only the verdict the schema asks for; nothing you write is posted. @@ -2254,53 +4630,53 @@ jobs: fallback-ready: ${{ steps.credential.outputs.fallback-ready }} leg-timeout-minutes: "15" - - name: Report chunk 2's outcome - if: steps.judge-2.outcome == 'success' || steps.judge-2.outcome == 'failure' + - name: Report chunk 10's outcome + if: steps.judge-10.outcome == 'success' || steps.judge-10.outcome == 'failure' continue-on-error: true uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 with: - outcome: ${{ steps.judge-2.outcome }} - execution-file: ${{ steps.judge-2.outputs.execution-file }} - log-file: ${{ steps.judge-2.outputs.log-file }} + outcome: ${{ steps.judge-10.outcome }} + execution-file: ${{ steps.judge-10.outputs.execution-file }} + log-file: ${{ steps.judge-10.outputs.log-file }} lane: pr-verify lane-id: pr-verify - step-started-at: ${{ steps.time-2.outputs.started-at }} + step-started-at: ${{ steps.time-10.outputs.started-at }} step-timeout-minutes: 22 - credential: ${{ steps.judge-2.outputs.credential }} + credential: ${{ steps.judge-10.outputs.credential }} max-turns: ${{ steps.models.outputs.max-turns }} - known-failure-class: ${{ steps.judge-2.outputs.lane-class }} + known-failure-class: ${{ steps.judge-10.outputs.lane-class }} fallback-reason: ${{ needs.check-intent-subscription.outputs.handoff-reason == 'limit-stop' && 'limit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'credit-stop' && 'credit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'auth' && 'auth' || '' }} - - name: Upload chunk 2's sealed transcript - if: always() && steps.judge-2.outputs.execution-file != '' + - name: Upload chunk 10's sealed transcript + if: always() && steps.judge-10.outputs.execution-file != '' continue-on-error: true uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] with: - execution-file: ${{ steps.judge-2.outputs.execution-file }} + execution-file: ${{ steps.judge-10.outputs.execution-file }} public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} - job-id: ${{ github.job }}-2 + job-id: ${{ github.job }}-10 - - name: Read chunk 2's verdict - id: chunk-fail-2 - if: ${{ !cancelled() && steps.judge-2.outcome == 'success' }} + - name: Read chunk 10's verdict + id: chunk-fail-10 + if: ${{ !cancelled() && steps.judge-10.outcome == 'success' }} continue-on-error: true env: - EXECUTION_FILE: ${{ steps.judge-2.outputs.execution-file }} - LOG_FILE: ${{ steps.judge-2.outputs.log-file }} - CHUNK: "2" + EXECUTION_FILE: ${{ steps.judge-10.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-10.outputs.log-file }} + CHUNK: "10" CHUNKS: ${{ steps.diff.outputs.chunks }} DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail - - name: Time chunk 3 - id: time-3 - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 3 && steps.judge-2.outcome == 'success' && steps.judge-2.outputs.handoff != 'true' && steps.chunk-fail-2.outputs.fail != 'true' + - name: Time chunk 11 + id: time-11 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 11 && steps.judge-10.outcome == 'success' && steps.judge-10.outputs.handoff != 'true' && steps.chunk-fail-10.outputs.fail != 'true' run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" - - name: Judge chunk 3 - id: judge-3 - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 3 && steps.judge-2.outcome == 'success' && steps.judge-2.outputs.handoff != 'true' && steps.chunk-fail-2.outputs.fail != 'true' + - name: Judge chunk 11 + id: judge-11 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 11 && steps.judge-10.outcome == 'success' && steps.judge-10.outputs.handoff != 'true' && steps.chunk-fail-10.outputs.fail != 'true' continue-on-error: true timeout-minutes: 22 uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 @@ -2309,7 +4685,7 @@ jobs: with: lane: pr-verify role: pr-verify - prompt-file: ${{ runner.temp }}/pr-verify-prompt-3.md + prompt-file: ${{ runner.temp }}/pr-verify-prompt-11.md preamble: >- You run unattended; return only the verdict the schema asks for; nothing you write is posted. @@ -2336,53 +4712,53 @@ jobs: fallback-ready: ${{ steps.credential.outputs.fallback-ready }} leg-timeout-minutes: "15" - - name: Report chunk 3's outcome - if: steps.judge-3.outcome == 'success' || steps.judge-3.outcome == 'failure' + - name: Report chunk 11's outcome + if: steps.judge-11.outcome == 'success' || steps.judge-11.outcome == 'failure' continue-on-error: true uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 with: - outcome: ${{ steps.judge-3.outcome }} - execution-file: ${{ steps.judge-3.outputs.execution-file }} - log-file: ${{ steps.judge-3.outputs.log-file }} + outcome: ${{ steps.judge-11.outcome }} + execution-file: ${{ steps.judge-11.outputs.execution-file }} + log-file: ${{ steps.judge-11.outputs.log-file }} lane: pr-verify lane-id: pr-verify - step-started-at: ${{ steps.time-3.outputs.started-at }} + step-started-at: ${{ steps.time-11.outputs.started-at }} step-timeout-minutes: 22 - credential: ${{ steps.judge-3.outputs.credential }} + credential: ${{ steps.judge-11.outputs.credential }} max-turns: ${{ steps.models.outputs.max-turns }} - known-failure-class: ${{ steps.judge-3.outputs.lane-class }} + known-failure-class: ${{ steps.judge-11.outputs.lane-class }} fallback-reason: ${{ needs.check-intent-subscription.outputs.handoff-reason == 'limit-stop' && 'limit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'credit-stop' && 'credit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'auth' && 'auth' || '' }} - - name: Upload chunk 3's sealed transcript - if: always() && steps.judge-3.outputs.execution-file != '' + - name: Upload chunk 11's sealed transcript + if: always() && steps.judge-11.outputs.execution-file != '' continue-on-error: true uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] with: - execution-file: ${{ steps.judge-3.outputs.execution-file }} + execution-file: ${{ steps.judge-11.outputs.execution-file }} public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} - job-id: ${{ github.job }}-3 + job-id: ${{ github.job }}-11 - - name: Read chunk 3's verdict - id: chunk-fail-3 - if: ${{ !cancelled() && steps.judge-3.outcome == 'success' }} + - name: Read chunk 11's verdict + id: chunk-fail-11 + if: ${{ !cancelled() && steps.judge-11.outcome == 'success' }} continue-on-error: true env: - EXECUTION_FILE: ${{ steps.judge-3.outputs.execution-file }} - LOG_FILE: ${{ steps.judge-3.outputs.log-file }} - CHUNK: "3" + EXECUTION_FILE: ${{ steps.judge-11.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-11.outputs.log-file }} + CHUNK: "11" CHUNKS: ${{ steps.diff.outputs.chunks }} DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest run: node .base/.github/scripts/pr-verify/write-verdict.mjs chunk-fail - - name: Time chunk 4 - id: time-4 - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 4 && steps.judge-3.outcome == 'success' && steps.judge-3.outputs.handoff != 'true' && steps.chunk-fail-3.outputs.fail != 'true' + - name: Time chunk 12 + id: time-12 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 12 && steps.judge-11.outcome == 'success' && steps.judge-11.outputs.handoff != 'true' && steps.chunk-fail-11.outputs.fail != 'true' run: echo "started-at=$(date +%s)" >>"$GITHUB_OUTPUT" - - name: Judge chunk 4 - id: judge-4 - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 4 && steps.judge-3.outcome == 'success' && steps.judge-3.outputs.handoff != 'true' && steps.chunk-fail-3.outputs.fail != 'true' + - name: Judge chunk 12 + id: judge-12 + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') >= 12 && steps.judge-11.outcome == 'success' && steps.judge-11.outputs.handoff != 'true' && steps.chunk-fail-11.outputs.fail != 'true' continue-on-error: true timeout-minutes: 22 uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 @@ -2391,7 +4767,7 @@ jobs: with: lane: pr-verify role: pr-verify - prompt-file: ${{ runner.temp }}/pr-verify-prompt-4.md + prompt-file: ${{ runner.temp }}/pr-verify-prompt-12.md preamble: >- You run unattended; return only the verdict the schema asks for; nothing you write is posted. @@ -2418,40 +4794,40 @@ jobs: fallback-ready: ${{ steps.credential.outputs.fallback-ready }} leg-timeout-minutes: "15" - - name: Report chunk 4's outcome - if: steps.judge-4.outcome == 'success' || steps.judge-4.outcome == 'failure' + - name: Report chunk 12's outcome + if: steps.judge-12.outcome == 'success' || steps.judge-12.outcome == 'failure' continue-on-error: true uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 with: - outcome: ${{ steps.judge-4.outcome }} - execution-file: ${{ steps.judge-4.outputs.execution-file }} - log-file: ${{ steps.judge-4.outputs.log-file }} + outcome: ${{ steps.judge-12.outcome }} + execution-file: ${{ steps.judge-12.outputs.execution-file }} + log-file: ${{ steps.judge-12.outputs.log-file }} lane: pr-verify lane-id: pr-verify - step-started-at: ${{ steps.time-4.outputs.started-at }} + step-started-at: ${{ steps.time-12.outputs.started-at }} step-timeout-minutes: 22 - credential: ${{ steps.judge-4.outputs.credential }} + credential: ${{ steps.judge-12.outputs.credential }} max-turns: ${{ steps.models.outputs.max-turns }} - known-failure-class: ${{ steps.judge-4.outputs.lane-class }} + known-failure-class: ${{ steps.judge-12.outputs.lane-class }} fallback-reason: ${{ needs.check-intent-subscription.outputs.handoff-reason == 'limit-stop' && 'limit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'credit-stop' && 'credit-stop' || needs.check-intent-subscription.outputs.handoff-reason == 'auth' && 'auth' || '' }} - - name: Upload chunk 4's sealed transcript - if: always() && steps.judge-4.outputs.execution-file != '' + - name: Upload chunk 12's sealed transcript + if: always() && steps.judge-12.outputs.execution-file != '' continue-on-error: true uses: ./.base/.github/actions/upload-lane-transcript # zizmor: ignore[self-repository] with: - execution-file: ${{ steps.judge-4.outputs.execution-file }} + execution-file: ${{ steps.judge-12.outputs.execution-file }} public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }} - job-id: ${{ github.job }}-4 + job-id: ${{ github.job }}-12 - - name: Read chunk 4's verdict - id: chunk-fail-4 - if: ${{ !cancelled() && steps.judge-4.outcome == 'success' }} + - name: Read chunk 12's verdict + id: chunk-fail-12 + if: ${{ !cancelled() && steps.judge-12.outcome == 'success' }} continue-on-error: true env: - EXECUTION_FILE: ${{ steps.judge-4.outputs.execution-file }} - LOG_FILE: ${{ steps.judge-4.outputs.log-file }} - CHUNK: "4" + EXECUTION_FILE: ${{ steps.judge-12.outputs.execution-file }} + LOG_FILE: ${{ steps.judge-12.outputs.log-file }} + CHUNK: "12" CHUNKS: ${{ steps.diff.outputs.chunks }} DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff MANIFEST_DIR: ${{ github.workspace }}/pr-verify-data/manifest @@ -2464,18 +4840,34 @@ jobs: # keeps this judge off the diff, and a denial voids the verdict. - name: Write the chunk results id: chunk-results - if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') > 1 && steps.judge.outcome == 'success' && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-2.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 3 || steps.judge-3.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 4 || steps.judge-4.outcome == 'success') && steps.chunk-fail.outputs.fail != 'true' && steps.chunk-fail-2.outputs.fail != 'true' && steps.chunk-fail-3.outputs.fail != 'true' && steps.chunk-fail-4.outputs.fail != 'true' + if: steps.credential.outputs.present == 'true' && fromJSON(steps.diff.outputs.chunks || '0') > 1 && steps.judge.outcome == 'success' && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-2.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 3 || steps.judge-3.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 4 || steps.judge-4.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 5 || steps.judge-5.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 6 || steps.judge-6.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 7 || steps.judge-7.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 8 || steps.judge-8.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 9 || steps.judge-9.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 10 || steps.judge-10.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 11 || steps.judge-11.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 12 || steps.judge-12.outcome == 'success') && steps.chunk-fail.outputs.fail != 'true' && steps.chunk-fail-2.outputs.fail != 'true' && steps.chunk-fail-3.outputs.fail != 'true' && steps.chunk-fail-4.outputs.fail != 'true' && steps.chunk-fail-5.outputs.fail != 'true' && steps.chunk-fail-6.outputs.fail != 'true' && steps.chunk-fail-7.outputs.fail != 'true' && steps.chunk-fail-8.outputs.fail != 'true' && steps.chunk-fail-9.outputs.fail != 'true' && steps.chunk-fail-10.outputs.fail != 'true' && steps.chunk-fail-11.outputs.fail != 'true' && steps.chunk-fail-12.outputs.fail != 'true' env: EXECUTION_FILES: | ${{ steps.judge.outputs.execution-file }} ${{ steps.judge-2.outputs.execution-file }} ${{ steps.judge-3.outputs.execution-file }} ${{ steps.judge-4.outputs.execution-file }} + ${{ steps.judge-5.outputs.execution-file }} + ${{ steps.judge-6.outputs.execution-file }} + ${{ steps.judge-7.outputs.execution-file }} + ${{ steps.judge-8.outputs.execution-file }} + ${{ steps.judge-9.outputs.execution-file }} + ${{ steps.judge-10.outputs.execution-file }} + ${{ steps.judge-11.outputs.execution-file }} + ${{ steps.judge-12.outputs.execution-file }} LOG_FILES: | ${{ steps.judge.outputs.log-file }} ${{ steps.judge-2.outputs.log-file }} ${{ steps.judge-3.outputs.log-file }} ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + ${{ steps.judge-10.outputs.log-file }} + ${{ steps.judge-11.outputs.log-file }} + ${{ steps.judge-12.outputs.log-file }} CHUNKS: ${{ steps.diff.outputs.chunks }} DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff RESULTS_DIR: ${{ github.workspace }}/pr-verify-data/chunk-results @@ -2564,7 +4956,7 @@ jobs: # script, never interpolated into a command. - name: Write the verdict id: verdict - if: ${{ !cancelled() && (steps.chunk-fail.outputs.fail == 'true' || steps.chunk-fail-2.outputs.fail == 'true' || steps.chunk-fail-3.outputs.fail == 'true' || steps.chunk-fail-4.outputs.fail == 'true' || steps.chunk-results.outcome == 'failure' || (steps.judge.outcome == 'success' && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-2.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 3 || steps.judge-3.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 4 || steps.judge-4.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-spec.outcome == 'success'))) }} + if: ${{ !cancelled() && (steps.chunk-fail.outputs.fail == 'true' || steps.chunk-fail-2.outputs.fail == 'true' || steps.chunk-fail-3.outputs.fail == 'true' || steps.chunk-fail-4.outputs.fail == 'true' || steps.chunk-fail-5.outputs.fail == 'true' || steps.chunk-fail-6.outputs.fail == 'true' || steps.chunk-fail-7.outputs.fail == 'true' || steps.chunk-fail-8.outputs.fail == 'true' || steps.chunk-fail-9.outputs.fail == 'true' || steps.chunk-fail-10.outputs.fail == 'true' || steps.chunk-fail-11.outputs.fail == 'true' || steps.chunk-fail-12.outputs.fail == 'true' || steps.chunk-results.outcome == 'failure' || (steps.judge.outcome == 'success' && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-2.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 3 || steps.judge-3.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 4 || steps.judge-4.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 5 || steps.judge-5.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 6 || steps.judge-6.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 7 || steps.judge-7.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 8 || steps.judge-8.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 9 || steps.judge-9.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 10 || steps.judge-10.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 11 || steps.judge-11.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 12 || steps.judge-12.outcome == 'success') && (fromJSON(steps.diff.outputs.chunks || '0') < 2 || steps.judge-spec.outcome == 'success'))) }} env: # Each chunk's files in chunk order. A counted fail needs no others; # anything else needs every chunk and the final judge. @@ -2573,11 +4965,27 @@ jobs: ${{ steps.judge-2.outputs.execution-file }} ${{ steps.judge-3.outputs.execution-file }} ${{ steps.judge-4.outputs.execution-file }} + ${{ steps.judge-5.outputs.execution-file }} + ${{ steps.judge-6.outputs.execution-file }} + ${{ steps.judge-7.outputs.execution-file }} + ${{ steps.judge-8.outputs.execution-file }} + ${{ steps.judge-9.outputs.execution-file }} + ${{ steps.judge-10.outputs.execution-file }} + ${{ steps.judge-11.outputs.execution-file }} + ${{ steps.judge-12.outputs.execution-file }} LOG_FILES: | ${{ steps.judge.outputs.log-file }} ${{ steps.judge-2.outputs.log-file }} ${{ steps.judge-3.outputs.log-file }} ${{ steps.judge-4.outputs.log-file }} + ${{ steps.judge-5.outputs.log-file }} + ${{ steps.judge-6.outputs.log-file }} + ${{ steps.judge-7.outputs.log-file }} + ${{ steps.judge-8.outputs.log-file }} + ${{ steps.judge-9.outputs.log-file }} + ${{ steps.judge-10.outputs.log-file }} + ${{ steps.judge-11.outputs.log-file }} + ${{ steps.judge-12.outputs.log-file }} CHUNKS: ${{ steps.diff.outputs.chunks }} DIFF_DIR: ${{ github.workspace }}/pr-verify-data/diff # The whole-spec judgment, which a head of two chunks or more must have. diff --git a/docs/conventions/pr-pipeline/README.md b/docs/conventions/pr-pipeline/README.md index f954381fe63..b313746ba3f 100644 --- a/docs/conventions/pr-pipeline/README.md +++ b/docs/conventions/pr-pipeline/README.md @@ -2228,9 +2228,10 @@ them, `upstream-jobs.json` (`upstream-jobs.mjs`), lists the jobs of the pr-requi judgment was started by, each with its name and conclusion, read with the job's own token: the verifier judges from it whether the suites that cover the changed files ran, and runs no test or eval itself. A label or body-edit re-run skips `select-tests` and every suite, so its list is no -evidence either way. The diff is split at file boundaries into at most 4 chunks of at most 300000 bytes, each file whole -(`chunk-diff.sh`, `split-diff.py`; the same budget as `pr-review-main`), and each chunk gets its own -judge step with the same spec, cut into parts one Read returns whole. `manifest.txt` names the part +evidence either way. The diff is split at file boundaries into as many chunks of at most 300000 +bytes as it takes, up to 12 (`MAX_CHUNKS` in `chunk-diff.sh`, which holds a 3 MB diff), each file +whole (`chunk-diff.sh`, `split-diff.py`; the same chunk budget as `pr-review-main`), and each chunk +gets its own judge step with the same spec, cut into parts one Read returns whole. `manifest.txt` names the part each file starts in, so a judge reads another chunk's part when its change references a file there and judges both sides. A `pass` or `pass-notes` counts only when the run read every part whole with no permission denial; a `fail` needs the schema, no denial and, when it read short, a defect in its @@ -2245,7 +2246,7 @@ With no output from it, off the schema, or with a denial, and no counted fail, t verdict, so no chunk's `pass-notes` passes a chunked head alone. A single chunk has no final step. A chunk that fails, leaves no verdict or is off the schema leaves no verdict for the head, and a handoff from any chunk hands the whole judgment to the next leg, which judges every chunk again. A file over 300000 bytes, or a diff that -needs more than 4 chunks (over about 1.2 MB), ends the run red with no verdict and an error naming +needs more than 12 chunks (over about 3.6 MB), ends the run red with no verdict and an error naming the file or the chunk count, so size alone writes no `fail`. With no verdict `verdictHold` holds the head, the trusted approval of the head lifts that, and a `pr-verify-failed` label stays. The diff step logs a `pr-verify chunks` notice, and the verdict step a `pr-verify chunk` notice per chunk. The run