diff --git a/.changes/pr-pipeline/perf-pr-verify-skip-judged-head-3b7e2c91.md b/.changes/pr-pipeline/perf-pr-verify-skip-judged-head-3b7e2c91.md new file mode 100644 index 00000000000..ae4028bf3d1 --- /dev/null +++ b/.changes/pr-pipeline/perf-pr-verify-skip-judged-head-3b7e2c91.md @@ -0,0 +1,8 @@ +--- +bump: patch +--- + +### Changed + +- pr-verify reads the key's credit history only for a head it would judge: the `check-intent-credit` job is gone, and its gate and skipped-leg report run inside `check-intent` after the resolver, the trigger gate and the reads hand-on. A run that skips at the resolver (a judged head, a draft, a run red for anything but a hold, no single PR) no longer spends the gate's reads or its own VM. A key the gate skips runs no pre-judge setup and no judge, writes no "no verdict" failure, and hands on through the `skip-key` job output, so `check-intent-subscription` now takes the carried reads instead of reading the PR again. +- The gate now reads the key's credit stop from one artifact marker (ci-workflows check-key-credit at `bc62beb`): one artifact-list call per judged run instead of a run list and up to three job lists. After a judge step's credit-stop handoff, `check-intent` writes, scans and uploads the artifact the gate's `marker` output names, kept one day; the step that carried a record forward is gone, since the window counts from the marker's upload. pr-verify's other ci-workflows actions move to the same pin. diff --git a/.github/actions/resolve-config/callers.test.mjs b/.github/actions/resolve-config/callers.test.mjs index 0784738d559..60c30c16232 100644 --- a/.github/actions/resolve-config/callers.test.mjs +++ b/.github/actions/resolve-config/callers.test.mjs @@ -2149,7 +2149,7 @@ test("no run-claude-cli plugin-dirs or add-dirs value names runner.temp, RUNNER_ // does; check-permission-mode.sh accepts default for dontAsk on that basis. // 7b362428: lane.cjs:311-312; 164c2809: lane.cjs:355-358; d02b8697 and e5167fc2: lane.cjs:357-358; // ba7af78d291f10bd75651857a0db2357c23df0fe, 30b251532b2e9f7e0c4215e1dee5be0d3c688743, 81a603a6e2b0319428c981bf28f3fe923032afb2 -// and 2476d5dde8979eab34374150b7e4b0a2b99f4e88: lane.cjs:404-405; in cliArgs. The org requires +// 2476d5dde8979eab34374150b7e4b0a2b99f4e88 and bc62beb95a989641f30552a932a132ebb8ecd801: lane.cjs:404-405; in cliArgs. The org requires // actions pinned to a full SHA, so a pin bump fails here until its lane.cjs is read and its SHA added. const PERMISSION_PROMPTS_NONE_LAUNCHERS = new Set([ "7b362428c76085a160e28656cd2c75c3147a69ff", @@ -2160,6 +2160,7 @@ const PERMISSION_PROMPTS_NONE_LAUNCHERS = new Set([ "30b251532b2e9f7e0c4215e1dee5be0d3c688743", "81a603a6e2b0319428c981bf28f3fe923032afb2", "2476d5dde8979eab34374150b7e4b0a2b99f4e88", + "bc62beb95a989641f30552a932a132ebb8ecd801", ]); test("every run-claude-cli step pin in this repo's workflows is a launcher checked to pass --permission-prompts none", () => { diff --git a/.github/artifact-retention.json b/.github/artifact-retention.json index f794bd585fa..9d13ef4f1df 100644 --- a/.github/artifact-retention.json +++ b/.github/artifact-retention.json @@ -33,6 +33,14 @@ ".github/workflows/pr-verify.yml#check-intent-fallback" ] }, + "key-credit-stop-pr-verify.yml-check-intent": { + "retention-days": 1, + "readers": [ + ".github/workflows/pr-verify.yml check-intent's \"Read the key's credit history\" step runs ci-workflows check-key-credit, which reads the newest marker of this exact name inside KEY_CREDIT_WINDOW_HOURS (1 hour unset, 24 at most for this retention)" + ], + "reason": "The marker only says the key hit a credit stop; the gate counts it for its window, an hour by default, so a day covers any window up to 24 hours and an expired marker runs the key, as before the gate.", + "scan": "upload" + }, "pr-review-main-*": { "retention-days": "max", "readers": [ diff --git a/.github/scripts/maintenance-reconcile-lost-runs/reconcile.test.mjs b/.github/scripts/maintenance-reconcile-lost-runs/reconcile.test.mjs index 506aea67a75..633a2458ae3 100644 --- a/.github/scripts/maintenance-reconcile-lost-runs/reconcile.test.mjs +++ b/.github/scripts/maintenance-reconcile-lost-runs/reconcile.test.mjs @@ -697,21 +697,22 @@ test("a verify run is read across all three legs: a fallback write held, a fallb assert.deepEqual(records(lost.outputs), [`reconcile-rerun-201-2-${MAIN_TIP}`, "", ""]); }); -// With the key out of credit, the credit gate skips check-intent and the -// subscription leg judges: its own steps tell a write held from a run lost. +// With the key out of credit, check-intent's credit gate skips its key leg and +// the subscription leg judges: its own steps tell a write held from a run lost. test("a verify run whose credit gate skipped the key is read on the subscription leg", async () => { - const gate = { - name: "check-intent-credit", + const key = { + name: "check-intent", status: "completed", conclusion: "success", steps: [ { name: "Set up job", status: "completed", conclusion: "success" }, + { name: "Hand the reads on", status: "completed", conclusion: "success" }, { name: "Read the key's credit history", status: "completed", conclusion: "success" }, - { name: "Record the key's credit stop in run 200", status: "completed", conclusion: "success" }, + { name: JUDGE_PREDECESSOR, status: "completed", conclusion: "skipped" }, + { name: JUDGE_STEP, status: "completed", conclusion: "skipped" }, { name: "Complete job", status: "completed", conclusion: "success" }, ], }; - const key = { name: "check-intent", status: "completed", conclusion: "skipped", steps: [] }; const subscription = (write, upload) => ({ name: "check-intent-subscription", status: "completed", @@ -725,9 +726,9 @@ test("a verify run whose credit gate skipped the key is read on the subscription { name: "Complete job", status: "completed", conclusion: "success" }, ], }); - const held = await reconcile({ pulls: [], runs: [{ ...verifyRun(201, { conclusion: "failure" }), jobs: [gate, key, subscription("success", "failure")] }] }); + const held = await reconcile({ pulls: [], runs: [{ ...verifyRun(201, { conclusion: "failure" }), jobs: [key, subscription("success", "failure")] }] }); assert.deepEqual(held.reruns, []); - const lost = await reconcile({ pulls: [], runs: [{ ...verifyRun(201, { conclusion: "failure" }), jobs: [gate, key, subscription("skipped", "skipped")] }] }); + const lost = await reconcile({ pulls: [], runs: [{ ...verifyRun(201, { conclusion: "failure" }), jobs: [key, subscription("skipped", "skipped")] }] }); assert.deepEqual(lost.reruns, [201]); assert.deepEqual(records(lost.outputs), [`reconcile-rerun-201-2-${MAIN_TIP}`, "", ""]); }); diff --git a/.github/scripts/pr-verify/probe/replay.mjs b/.github/scripts/pr-verify/probe/replay.mjs index df91394057c..b00f80bfd46 100644 --- a/.github/scripts/pr-verify/probe/replay.mjs +++ b/.github/scripts/pr-verify/probe/replay.mjs @@ -84,7 +84,7 @@ export const jobUrl = (runId, jobId) => `https://github.com/${REPOSITORY}/action // apply-claude-managed-settings to. cliArgs, LEG_ENV, CLI_VERSION and // MANAGED_SETTINGS were read from that SHA; the parity test fails when the // pin moves, so they are read again. -export const RUN_CLAUDE_CLI_PIN = "2476d5dde8979eab34374150b7e4b0a2b99f4e88"; +export const RUN_CLAUDE_CLI_PIN = "bc62beb95a989641f30552a932a132ebb8ecd801"; export const CLI_VERSION = "latest"; export const MANAGED_SETTINGS = Object.freeze({ apiKeyHelper: "", diff --git a/.github/scripts/pr-verify/workflow.test.mjs b/.github/scripts/pr-verify/workflow.test.mjs index f0d28dc1081..cc9bdc30b14 100644 --- a/.github/scripts/pr-verify/workflow.test.mjs +++ b/.github/scripts/pr-verify/workflow.test.mjs @@ -20,8 +20,6 @@ const KEY = "check-intent"; const SUB = "check-intent-subscription"; const FB = "check-intent-fallback"; const JOBS = [KEY, SUB, FB]; -// The credit gate before the key job: no model, no secret. -const PROBE = "check-intent-credit"; const jobText = (id) => { const found = new RegExp(`\\n ${id}:\\n([\\s\\S]*?)(?=\\n [a-z-]+:\\n|$)`).exec(text); assert.ok(found, `a job ${id}`); @@ -33,7 +31,7 @@ const stepsOf = (id) => .split(/\n(?= - name: )/) .slice(1) .map((block) => ({ name: /^ - name: (.*)$/m.exec(block)[1], block })); -const STEPS = Object.fromEntries([...JOBS, PROBE].map((id) => [id, stepsOf(id)])); +const STEPS = Object.fromEntries(JOBS.map((id) => [id, stepsOf(id)])); const index = (name, job = KEY) => STEPS[job].findIndex((step) => step.name === name); const step = (name, job = KEY) => { const found = STEPS[job].find((s) => s.name === name); @@ -55,7 +53,7 @@ const PIN = /^melodic-software\/ci-workflows\/\.github\/actions\/([a-z-]+)@[0-9a // in report-lane-outcome's fallback-reason (ci-workflows intake-triage.yml); // a key the credit gate skipped is a credit stop too. const 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' || needs.check-intent-credit.outputs.skip-key == 'true' && 'credit-stop' || '' }}"; + "${{ 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' || needs.check-intent.outputs.skip-key == 'true' && 'credit-stop' || '' }}"; // The fallback job's record names why the subscription did not judge, from // its handoff-reason (ci-workflows intake-triage.yml, the fallback job). const FB_REASON = @@ -76,6 +74,12 @@ const FALLBACK_READY = [ const CARRIED = " CARRIED: ${{ needs.check-intent.outputs.reads }}"; const CARRY_ENV = [" CONTEXT_FILE: ${{ github.workspace }}/pr-verify-data/context.json", CARRIED]; const NOT_CARRIED = " && steps.pr.outputs.carried != 'true'"; +// The key job's credit gate: a key it skipped runs no pre-judge setup. +const KEY_SKIP = " && steps.credit.outputs.skip-key != 'true'"; +const CREDIT_STEPS = ["Read the key's credit history", "Report the skipped key leg"]; +// The key's credit-stop marker, which only the key job writes and uploads. +const MARKER_STEPS = ["Write the key's credit-stop marker", "Scan the key's credit-stop marker for credentials", "Upload the key's credit-stop marker"]; +const ifOf = (job, base) => `${base}${job === KEY ? KEY_SKIP : ""}`; const KEY_CLASS = "${{ steps.judge.outputs.lane-class }}"; const REPORT_IF = "steps.judge.outcome == 'success' || steps.judge.outcome == 'failure'"; const CREDENTIAL = { @@ -176,12 +180,12 @@ test("a FAIL index never replaces or keeps a name its run already holds", () => assert.equal((ACTION.match(/if: \$\{\{ steps\.slots\.outputs\.n\d+ != '' \}\}/g) ?? []).length, 13); }); -test("the upload is each job's only artifact step and follows the written verdict", () => { +test("the verdict is each job's only artifact upload but the key job's credit-stop marker, and follows the written verdict", () => { for (const job of JOBS) { const uploads = STEPS[job].filter((s) => /uses: actions\/upload-artifact@/.test(s.block)); assert.deepEqual( uploads.map((s) => s.name), - ["Upload the verdict"], + job === KEY ? ["Upload the verdict", "Upload the key's credit-stop marker"] : ["Upload the verdict"], job, ); assert.ok(index("Judge the pull request", job) >= 0 && index("Write the verdict", job) > index("Judge the pull request", job), job); @@ -320,55 +324,88 @@ const gateOf = (id) => /^ {4}if: >-\n((?: {6}.*\n)+)/m.exec(jobText(id))?.[1].re const withNeeds = (needs, overrides) => ({ ...upstreamRun(overrides), needs }); test("the key job runs on main for a same-repository pull_request run that passed or failed", () => { - // !cancelled(): a failed or skipped credit gate still runs the key. - assert.equal(keyGate(), `needs.${PROBE}.outputs.skip-key != 'true' && !cancelled() && ${TRIGGER_GATE}`); - assert.equal(gateOf(PROBE), TRIGGER_GATE); + assert.equal(keyGate(), TRIGGER_GATE); + assert.doesNotMatch(jobText(KEY), /^ {4}needs:/m); }); // ci-workflows check-key-credit (gate.cjs at the pinned SHA): skip-key is -// `true` only while a credit stop recorded in this workflow is inside the -// window; any failure leaves it empty, so the key runs as before the gate. -test("the credit gate skips the key job only on skip-key true, and a gate failure runs the key", () => { - assert.match(jobText(KEY), /^ {4}needs: check-intent-credit$/m); - assert.ok(!admits(keyGate(), withNeeds({ [PROBE]: { outputs: { "skip-key": "true" } } }))); - for (const skip of ["false", "", undefined]) { - assert.ok(admits(keyGate(), withNeeds({ [PROBE]: { result: skip === undefined ? "failure" : "success", outputs: { "skip-key": skip } } })), String(skip)); - } - assert.ok(!admits(keyGate(), withNeeds({ [PROBE]: { outputs: {} } }), { cancelled: true })); -}); - -test("the credit gate holds no secret, checks nothing out and reads only this workflow's runs", () => { - const job = jobText(PROBE); - assert.match(job, /^ {4}permissions:\n {6}actions: read #.*\n {4}outputs:\n {6}skip-key: \$\{\{ steps\.gate\.outputs\.skip-key \}\}\n/m); - assert.doesNotMatch(job, /secrets\.|actions\/checkout|run-claude-cli|\.\/\.base/); - const gate = step("Read the key's credit history", PROBE); - assert.match(gate, /^ {8}id: gate$/m); +// `true` only while the newest credit-stop marker this job uploaded is inside +// the window; any failure leaves it empty, so the key runs as before the gate. +// It spends one artifact-list call, so it runs only for a head the resolver +// would judge: a run that skips there (a judged head, a draft, a run red for +// anything but a hold) spends none. +const GATED_SETUP = ["Resolve the verifier's model", "Check the credential", "Apply the Claude managed settings", "Isolate the runner", "Write the prompt and schema"]; +test("the credit gate runs in the key job after the resolver, the trigger gate and the reads hand-on, and only then", () => { + assert.equal(text.match(/check-key-credit@/g).length, 1); + const gate = step("Read the key's credit history"); + assert.match(gate, /^ {8}id: credit$/m); + assert.match(gate, /^ {8}if: steps\.agree\.outcome == 'success'$/m); assert.match(gate, /^ {8}continue-on-error: true$/m); assert.match(gate, /^ {8}uses: melodic-software\/ci-workflows\/\.github\/actions\/check-key-credit@[0-9a-f]{40} # \S/m); - // The key job's display name is its id: the gate reads its record there and - // in this job, `-credit`. + assert.doesNotMatch(gate, /secrets\./); + for (const before of ["Resolve the pull request", "Check the trigger", "Assert the gate and the resolver agree", "Hand the reads on"]) { + assert.ok(index(before) >= 0 && index(before) < index("Read the key's credit history"), before); + } + for (const after of GATED_SETUP) assert.ok(index(after) > index("Read the key's credit history"), after); + // The marker's name carries the key-job input; the key job's id stays its display name. assert.equal(input(gate, "key-job"), KEY); assert.doesNotMatch(jobText(KEY), /^ {4}name:/m); assert.equal(input(gate, "window-hours"), "${{ vars.KEY_CREDIT_WINDOW_HOURS || '1' }}"); }); -// check-key-credit RECORD_PATTERN: /^Record the key's credit stop in run (\d{1,20})$/. -const RECORD_PATTERN = /^Record the key's credit stop in run (\d{1,20})$/u; -test("the record steps carry the pattern the gate reads, the key's only after a credit-stop handoff", () => { - const carried = "Record the key's credit stop in run ${{ steps.gate.outputs.origin-run }}"; - const own = "Record the key's credit stop in run ${{ github.run_id }}"; - assert.match(carried.replace("${{ steps.gate.outputs.origin-run }}", "12345"), RECORD_PATTERN); - assert.match(own.replace("${{ github.run_id }}", "12345"), RECORD_PATTERN); - assert.match(step(carried, PROBE), /^ {8}if: steps\.gate\.outputs\.skip-key == 'true'$/m); - const record = step(own, KEY); - const condition = /^ {8}if: (.*)$/m.exec(record)[1]; - assert.equal(condition, `always() && (${chainOf((id) => `steps.${id}.outputs.handoff-reason == 'credit-stop'`, "judge")})`); - assert.match(record, /^ {8}continue-on-error: true$/m); - // Only the key job records: a subscription credit stop says nothing of the key. - for (const job of [SUB, FB]) assert.equal(index(own, job), -1, job); +test("a key the credit gate skipped runs no pre-judge setup, no judge and no verdict failure, and hands on", () => { + const SKIPPED = KEY_SKIP; + for (const name of GATED_SETUP) assert.match(step(name), new RegExp(`^ {8}if: steps\\.agree\\.outcome == 'success'${SKIPPED.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m"), name); + // Every judge step needs the credential step's present=true, which a skip leaves empty. + assert.match(step("Judge the pull request"), /^ {8}if: steps\.credential\.outputs\.present == 'true'$/m); + const fail = /^ {8}if: (.*)$/m.exec(step("Fail without a verdict"))[1]; + assert.ok(fail.startsWith(`always() && steps.agree.outcome == 'success'${SKIPPED} && `), fail); + const key = jobText(KEY); + assert.match(key, /^ {6}skip-key: \$\{\{ steps\.credit\.outputs\.skip-key \}\}$/m); +}); + +// ci-workflows#973 (bc62beb): the gate reads one exact-name artifact, its +// `marker` output, instead of record steps in run history, so nothing +// carries a record forward and the key job uploads that name after a +// credit-stop handoff (pr-review-cli.yml's key job at that SHA does the same). +test("the key job uploads the gate's marker only after a credit-stop handoff, and nothing carries a record", () => { + assert.match(step("Read the key's credit history"), / {8}uses: [^\n]*\/check-key-credit@bc62beb95a989641f30552a932a132ebb8ecd801 /); + assert.doesNotMatch(text, /Record the key's credit stop|origin-run/); + const [write, scan, upload] = MARKER_STEPS; + const writeStep = step(write); + assert.match(writeStep, /^ {8}id: credit-marker$/m); + const condition = /^ {8}if: (.*)$/m.exec(writeStep)[1]; + // gate.cjs markerName at bc62beb: `key-credit-stop-${workflow file}-${key-job}`. + // The upload writes it out so its retention row claims it; the write runs only + // while the gate's `marker` output equals it, so a contract change uploads none. + const keyJob = /^ {10}key-job: (\S+)$/m.exec(step("Read the key's credit history"))[1]; + const marker = `key-credit-stop-pr-verify.yml-${keyJob}`; + assert.equal(marker, "key-credit-stop-pr-verify.yml-check-intent"); + assert.equal(condition, `always() && steps.credit.outputs.marker == '${marker}' && (${chainOf((id) => `steps.${id}.outputs.handoff-reason == 'credit-stop'`, "judge")})`); + assert.match(writeStep, /echo "\$GITHUB_RUN_ID" >"\$RUNNER_TEMP\/key-credit-stop\/run-id"/); + const scanStep = step(scan); + assert.match(scanStep, /^ {8}id: credit-marker-scan$/m); + assert.match(scanStep, /^ {8}if: always\(\) && steps\.credit-marker\.outcome == 'success'$/m); + assert.match(scanStep, /^ {8}uses: \.\/\.base\/\.github\/actions\/scan-upload /m); + const uploadStep = step(upload); + assert.match(uploadStep, /^ {8}if: \(always\(\) && steps\.credit-marker\.outcome == 'success'\) && steps\.credit-marker-scan\.outputs\.clean == 'true'$/m); + // The name the gate reads is the name the job uploads; the scan covers the same file. + assert.equal(input(uploadStep, "name"), marker); + assert.equal(input(uploadStep, "path"), "${{ runner.temp }}/key-credit-stop/run-id"); + assert.equal(input(scanStep, "path"), input(uploadStep, "path")); + assert.equal(input(uploadStep, "retention-days"), "1"); + assert.equal(input(uploadStep, "if-no-files-found"), "error"); + for (const name of MARKER_STEPS) assert.match(step(name), /^ {8}continue-on-error: true$/m, name); + // Written after every judge step, then scanned, then uploaded. + const judges = STEPS[KEY].map((s, i) => (/run-claude-cli@/.test(s.block) ? i : -1)).filter((i) => i >= 0); + assert.ok(index(write) > Math.max(...judges)); + assert.equal(index(scan), index(write) + 1); + assert.equal(index(upload), index(scan) + 1); + // Only the key job writes one: a subscription credit stop says nothing of the key. + for (const job of [SUB, FB]) for (const name of MARKER_STEPS) assert.equal(index(name, job), -1, `${job} ${name}`); // The skipped key leg still leaves its lane record, so leg counts compare. - const report = step("Report the skipped key leg", PROBE); - assert.match(report, /^ {8}if: steps\.gate\.outputs\.skip-key == 'true'$/m); + const report = step("Report the skipped key leg"); + assert.match(report, /^ {8}if: steps\.credit\.outputs\.skip-key == 'true'$/m); assert.equal(input(report, "outcome"), "skipped"); assert.equal(input(report, "lane-id"), "pr-verify"); assert.equal(input(report, "credential"), CREDENTIAL[KEY][1]); @@ -409,7 +446,7 @@ test("a same-repository pull_request run on a feature branch passes the trigger test("the subscription job runs only on a clean handoff, an empty key or a key the credit gate skipped, on a fixed runner", () => { const job = jobText(SUB); - assert.match(job, /^ {4}needs: \[check-intent-credit, check-intent\]$/m); + assert.match(job, /^ {4}needs: check-intent$/m); const gate = gateOf(SUB); assert.equal( gate, @@ -419,25 +456,25 @@ test("the subscription job runs only on a clean handoff, an empty key or a key t "&& ((needs.check-intent.result == 'success'", "&& ((needs.check-intent.outputs.handoff == 'true'", "&& needs.check-intent.outputs.key-leg-clean == 'true')", - "|| needs.check-intent.outputs.credential-present == 'false'))", - "|| (needs.check-intent.result == 'skipped'", - "&& needs.check-intent-credit.outputs.skip-key == 'true'))", + "|| needs.check-intent.outputs.credential-present == 'false'", + "|| needs.check-intent.outputs.skip-key == 'true')))", ].join(" "), ); // !cancelled() keeps a gate-skipped key job from skipping this one, so the // key job's success is spelled out: a failed key job skips it. assert.doesNotMatch(gate, /always\(\)|failure\(\)|success\(\)/); - const run = (key, probe = { outputs: { "skip-key": "" } }) => admits(gate, withNeeds({ [KEY]: key, [PROBE]: probe })); + const run = (key) => admits(gate, withNeeds({ [KEY]: key })); const handedOff = { handoff: "true", "key-leg-clean": "true", "credential-present": "true" }; assert.ok(run({ result: "success", outputs: handedOff })); assert.ok(run({ result: "success", outputs: { "credential-present": "false" } })); assert.ok(!run({ result: "success", outputs: { ...handedOff, "key-leg-clean": "" } })); assert.ok(!run({ result: "success", outputs: { "credential-present": "true" } })); assert.ok(!run({ result: "failure", outputs: handedOff })); - assert.ok(run({ result: "skipped", outputs: {} }, { result: "success", outputs: { "skip-key": "true" } })); + assert.ok(run({ result: "success", outputs: { "skip-key": "true" } })); + assert.ok(!run({ result: "success", outputs: { "skip-key": "false", "credential-present": "true" } })); + assert.ok(!run({ result: "failure", outputs: { "skip-key": "true" } })); // A key job skipped for any other reason (the trigger gate) judges nothing. - assert.ok(!run({ result: "skipped", outputs: {} }, { result: "skipped", outputs: {} })); - assert.ok(!run({ result: "skipped", outputs: {} }, { result: "success", outputs: { "skip-key": "false" } })); + assert.ok(!run({ result: "skipped", outputs: {} })); assert.match(job, /^ {4}runs-on: ubuntu-24\.04$/m); // The key job's outputs reach only that if, the resolver's env and the // record's fallback reason, which they pick between three literals; never a @@ -457,6 +494,7 @@ test("the subscription job runs only on a clean handoff, an empty key or a key t " credential-present: ${{ steps.credential.outputs.present }}", ` key-leg-clean: \${{ ${chainOf((id) => `steps.${id}.outputs.clean`, "key-leg")} }}`, " reads: ${{ steps.carry.outputs.reads }}", + " skip-key: ${{ steps.credit.outputs.skip-key }}", ...WAKE_OUTPUTS, "", ].join("\n"), @@ -557,7 +595,7 @@ test("the key job hands the judgment on only after a clean handoff or with no ke assert.equal(index("Check the key leg", FB), -1); for (const job of JOBS) { const credential = step("Check the credential", job); - assert.match(credential, /^ {8}if: steps\.agree\.outcome == 'success'$/m, job); + assert.equal(/^ {8}if: (.*)$/m.exec(credential)[1], ifOf(job, "steps.agree.outcome == 'success'"), job); assert.match(credential, new RegExp(`^ {10}PRESENT: \\$\\{\\{ secrets\\.${CREDENTIAL[job][1]} != '' \\}\\}$`, "m"), job); assert.ok(index("Check the credential", job) < index("Judge the pull request", job), job); } @@ -578,13 +616,16 @@ test("the subscription job repeats the key job's steps but for the credential", !s.name.startsWith("Check the key leg") && !s.name.startsWith("Check the subscription leg") && s.name !== "Hand the reads on" && - // The key's credit record: only a key leg's credit stop says the key is out. - s.name !== "Record the key's credit stop in run ${{ github.run_id }}", + // The key's credit-stop marker: only a key leg's credit stop says the key is out. + !MARKER_STEPS.includes(s.name) && + // The credit gate: only the key leg can be out of credit. + !CREDIT_STEPS.includes(s.name), ) .map((s) => lines(s.block) .replace(`\n${CARRY_ENV.join("\n")}`, "") .replace(`if: steps.agree.outcome == 'success'${NOT_CARRIED}`, "if: steps.agree.outcome == 'success'") + .replace(KEY_SKIP, "") .replace(`\n${FALLBACK_PRESENT}`, "") .replace(`\n${FALLBACK_READY}`, "") .replace("\n fallback-ready: ${{ steps.credential.outputs.fallback-ready }}", "") @@ -701,7 +742,7 @@ test("each job checks out main fresh and writes the managed settings before isol assert.match(STEPS[job][0].block, /^ {10}persist-credentials: false$/m, job); const settings = step("Apply the Claude managed settings", job); assert.equal(PIN.exec(/^ {8}uses: (.*)$/m.exec(settings)[1])?.[1], "apply-claude-managed-settings", job); - assert.match(settings, /^ {8}if: steps\.agree\.outcome == 'success'$/m, job); + assert.equal(/^ {8}if: (.*)$/m.exec(settings)[1], ifOf(job, "steps.agree.outcome == 'success'"), job); assert.ok(index("Apply the Claude managed settings", job) < index("Isolate the runner", job), job); assert.ok(index("Isolate the runner", job) < index("Judge the pull request", job), job); } @@ -732,7 +773,7 @@ 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, - new RegExp(`^ {8}if: always\\(\\) && steps\\.agree\\.outcome == 'success' && steps\\.verdict\\.outcome != 'success' && steps\\.credential\\.outputs\\.present != 'false' && ${legsClear("key-leg")}$`, "m"), + new RegExp(`^ {8}if: always\\(\\) && steps\\.agree\\.outcome == 'success' && steps\\.credit\\.outputs\\.skip-key != 'true' && steps\\.verdict\\.outcome != 'success' && steps\\.credential\\.outputs\\.present != 'false' && ${legsClear("key-leg")}$`, "m"), ); assert.match( STEPS[SUB].at(-1).block, @@ -1018,7 +1059,7 @@ test("the diff is folded at the line length the review reads whole", async () => test("the report steps only record, so a broken one cannot stop the judgment", () => { for (const job of JOBS) { const reports = STEPS[job].filter((s) => /actions\/report-lane-outcome@/.test(s.block)); - assert.equal(reports.length, SLOTS.length + 1, job); + assert.equal(reports.length, SLOTS.length + 1 + (job === KEY ? 1 : 0), job); for (const report of reports) { assert.match(report.block, /^ {8}continue-on-error: true$/m, `${job} ${report.name}`); assert.doesNotMatch(report.block, /^ {8}id: /m, `${job} ${report.name}`); diff --git a/.github/workflows/pr-verify.yml b/.github/workflows/pr-verify.yml index cb7c98c2ac9..485b62e4a3a 100644 --- a/.github/workflows/pr-verify.yml +++ b/.github/workflows/pr-verify.yml @@ -29,8 +29,8 @@ name: pr-verify # when check-intent's key leg handed off (run-claude-cli's handoff output: # a credit stop, or class auth with API status 401 or 402) with no permission # denial, or its key is empty; then check-intent writes no verdict and does -# not fail. It also runs when check-intent-credit skipped check-intent because -# the key's credit stop is recent. With the org secret FALLBACK_CLAUDE_CODE_OAUTH_TOKEN set, the +# not fail. It also runs when check-intent's credit gate skipped the key leg +# because the key's credit stop is recent. With the org secret FALLBACK_CLAUDE_CODE_OAUTH_TOKEN set, the # subscription leg hands off the same way (a limit stop, a credit stop, or a # 401 or 402) and the check-intent-fallback job judges once on that token, on # a third VM; with it empty, nothing hands off past the subscription job. So @@ -91,73 +91,9 @@ defaults: shell: bash jobs: - # Whether the key is still out of credit: a check-intent credit stop in this - # workflow inside the window (vars.KEY_CREDIT_WINDOW_HOURS, 1 hour unset) - # skips check-intent, and check-intent-subscription judges instead - # (ci-workflows check-key-credit, gate.cjs). Its record step carries the - # origin run forward in its name, so the window counts from the credit stop, - # not the last skip, and a topped-up or rotated key is tried once it ends. - # It holds no secret, checks nothing out and reads only this workflow's runs. - # Every failure runs the key: check-intent's `if` starts with !cancelled(). - check-intent-credit: - 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 - runs-on: ubuntu-24.04 - timeout-minutes: 10 - cache-mode: none - permissions: - actions: read # this workflow's runs and their jobs - outputs: - skip-key: ${{ steps.gate.outputs.skip-key }} - steps: - - name: Read the key's credit history - id: gate - continue-on-error: true - timeout-minutes: 2 - uses: melodic-software/ci-workflows/.github/actions/check-key-credit@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 - with: - key-job: check-intent - window-hours: ${{ vars.KEY_CREDIT_WINDOW_HOURS || '1' }} - - # Read by the next runs' gate; its name is the record (check-key-credit - # RECORD_PATTERN). - - name: Record the key's credit stop in run ${{ steps.gate.outputs.origin-run }} - if: steps.gate.outputs.skip-key == 'true' - continue-on-error: true - timeout-minutes: 1 - run: echo "carried to the next run's check-key-credit read" - - # The skipped key leg's lane record: outcome skip, class credit-stop, - # handed off, so leg counts before and after the gate compare. - - name: Report the skipped key leg - if: steps.gate.outputs.skip-key == 'true' - continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@6b9c640bf209c0c7e76f1111c5d18c96707348a2 # 6b9c640 2026-11-02 - with: - outcome: skipped - lane: pr-verify - lane-id: pr-verify - credential: PR_VERIFY_CLAUDE_API_KEY - known-failure-class: credit-stop - handoff: "true" - - - name: Log the API budget - if: always() - continue-on-error: true - timeout-minutes: 1 - uses: melodic-software/ci-workflows/.github/actions/report-api-budget@2476d5dde8979eab34374150b7e4b0a2b99f4e88 # 2476d5d 2026-10-29 - with: - tokens: github-token - check-intent: - needs: check-intent-credit if: >- - needs.check-intent-credit.outputs.skip-key != 'true' - && !cancelled() - && github.ref == 'refs/heads/main' + 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 @@ -174,6 +110,7 @@ jobs: 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-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 }} + skip-key: ${{ steps.credit.outputs.skip-key }} # For wake-pr-merge: the pull request, and the verdict once it uploaded. pr-number: ${{ steps.pr.outputs.pr-number }} verdict: ${{ steps.upload-verdict.outcome == 'success' && steps.verdict.outputs.verdict || '' }} @@ -352,11 +289,43 @@ jobs: CONTEXT_FILE: ${{ github.workspace }}/pr-verify-data/context.json run: node .base/.github/scripts/pr-verify/resolve-pr.mjs carry + # Whether the key is still out of credit: a check-intent credit stop in + # this workflow inside the window (vars.KEY_CREDIT_WINDOW_HOURS, 1 hour + # unset) skips this job's key leg, and check-intent-subscription judges + # on the reads handed on above (ci-workflows check-key-credit, gate.cjs: + # one artifact-list call for the `marker` this job uploads after a + # credit stop). It runs only for a head the resolver would judge, so a + # run that skips there spends no call. Every failure runs the key: + # skip-key is then empty. + - name: Read the key's credit history + id: credit + if: steps.agree.outcome == 'success' + continue-on-error: true + timeout-minutes: 2 + uses: melodic-software/ci-workflows/.github/actions/check-key-credit@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 + with: + key-job: check-intent + window-hours: ${{ vars.KEY_CREDIT_WINDOW_HOURS || '1' }} + + # The skipped key leg's lane record: outcome skip, class credit-stop, + # handed off, so leg counts before and after the gate compare. + - name: Report the skipped key leg + if: steps.credit.outputs.skip-key == 'true' + continue-on-error: true + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@6b9c640bf209c0c7e76f1111c5d18c96707348a2 # 6b9c640 2026-11-02 + with: + outcome: skipped + lane: pr-verify + lane-id: pr-verify + credential: PR_VERIFY_CLAUDE_API_KEY + known-failure-class: credit-stop + handoff: "true" + # 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' + if: steps.agree.outcome == 'success' && steps.credit.outputs.skip-key != 'true' env: API_BUDGET_TOKEN: github-token uses: ./.base/.github/actions/resolve-config # zizmor: ignore[self-repository] @@ -367,7 +336,7 @@ jobs: # Only whether the secret is set leaves this step, never its value. - name: Check the credential id: credential - if: steps.agree.outcome == 'success' + if: steps.agree.outcome == 'success' && steps.credit.outputs.skip-key != 'true' env: PRESENT: ${{ secrets.PR_VERIFY_CLAUDE_API_KEY != '' }} SECRET_NAME: PR_VERIFY_CLAUDE_API_KEY @@ -382,11 +351,11 @@ jobs: # 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 + if: steps.agree.outcome == 'success' && steps.credit.outputs.skip-key != 'true' + uses: melodic-software/ci-workflows/.github/actions/apply-claude-managed-settings@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 - name: Isolate the runner - if: steps.agree.outcome == 'success' + if: steps.agree.outcome == 'success' && steps.credit.outputs.skip-key != 'true' uses: ./.base/.github/actions/setup-runner-isolation # zizmor: ignore[self-repository] with: install-subprocess-isolation: true @@ -396,7 +365,7 @@ jobs: # lets the outcome step tell a step timeout from other failures. - name: Write the prompt and schema id: prompt - if: steps.agree.outcome == 'success' + if: steps.agree.outcome == 'success' && steps.credit.outputs.skip-key != 'true' env: DATA: ${{ github.workspace }}/pr-verify-data MODEL: ${{ steps.models.outputs.model }} @@ -456,7 +425,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: # No work may outlive the result message the launcher reads. CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" @@ -576,7 +545,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -675,7 +644,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -775,7 +744,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -877,7 +846,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -979,7 +948,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -1082,7 +1051,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -1186,7 +1155,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -1291,7 +1260,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -1397,7 +1366,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -1504,7 +1473,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -1612,7 +1581,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -1784,7 +1753,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -1986,15 +1955,41 @@ jobs: echo '```' } >>"$GITHUB_STEP_SUMMARY" - # Read by the next runs' check-intent-credit gate: a step named for this - # run that succeeds only after a judge step's credit-stop handoff, which - # on this key leg means the key is out of credit. Its name is the record; - # keep it equal to check-key-credit's RECORD_PATTERN. - - name: Record the key's credit stop in run ${{ github.run_id }} - if: always() && (steps.judge.outputs.handoff-reason == 'credit-stop' || steps.judge-2.outputs.handoff-reason == 'credit-stop' || steps.judge-3.outputs.handoff-reason == 'credit-stop' || steps.judge-4.outputs.handoff-reason == 'credit-stop' || steps.judge-5.outputs.handoff-reason == 'credit-stop' || steps.judge-6.outputs.handoff-reason == 'credit-stop' || steps.judge-7.outputs.handoff-reason == 'credit-stop' || steps.judge-8.outputs.handoff-reason == 'credit-stop' || steps.judge-9.outputs.handoff-reason == 'credit-stop' || steps.judge-10.outputs.handoff-reason == 'credit-stop' || steps.judge-11.outputs.handoff-reason == 'credit-stop' || steps.judge-12.outputs.handoff-reason == 'credit-stop' || steps.judge-spec.outputs.handoff-reason == 'credit-stop') + # Read by the next runs' credit gate: the artifact its `marker` output + # names (check-key-credit gate.cjs markerName), uploaded only after a + # judge step's credit-stop handoff, which on this key leg means the key + # is out of credit. The name is written out so its retention row + # (.github/artifact-retention.json) claims it; a gate that names + # another marker writes none. Its upload time starts the window; one + # day's retention covers windows up to 24 hours. + - name: Write the key's credit-stop marker + id: credit-marker + if: always() && steps.credit.outputs.marker == 'key-credit-stop-pr-verify.yml-check-intent' && (steps.judge.outputs.handoff-reason == 'credit-stop' || steps.judge-2.outputs.handoff-reason == 'credit-stop' || steps.judge-3.outputs.handoff-reason == 'credit-stop' || steps.judge-4.outputs.handoff-reason == 'credit-stop' || steps.judge-5.outputs.handoff-reason == 'credit-stop' || steps.judge-6.outputs.handoff-reason == 'credit-stop' || steps.judge-7.outputs.handoff-reason == 'credit-stop' || steps.judge-8.outputs.handoff-reason == 'credit-stop' || steps.judge-9.outputs.handoff-reason == 'credit-stop' || steps.judge-10.outputs.handoff-reason == 'credit-stop' || steps.judge-11.outputs.handoff-reason == 'credit-stop' || steps.judge-12.outputs.handoff-reason == 'credit-stop' || steps.judge-spec.outputs.handoff-reason == 'credit-stop') + continue-on-error: true + timeout-minutes: 1 + run: | + mkdir -p "$RUNNER_TEMP/key-credit-stop" + echo "$GITHUB_RUN_ID" >"$RUNNER_TEMP/key-credit-stop/run-id" + + - name: Scan the key's credit-stop marker for credentials + id: credit-marker-scan + if: always() && steps.credit-marker.outcome == 'success' + continue-on-error: true + timeout-minutes: 1 + uses: ./.base/.github/actions/scan-upload # zizmor: ignore[self-repository] + with: + path: ${{ runner.temp }}/key-credit-stop/run-id + + - name: Upload the key's credit-stop marker + if: (always() && steps.credit-marker.outcome == 'success') && steps.credit-marker-scan.outputs.clean == 'true' continue-on-error: true timeout-minutes: 1 - run: echo "the key ended in a credit stop" + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: key-credit-stop-pr-verify.yml-check-intent + path: ${{ runner.temp }}/key-credit-stop/run-id + if-no-files-found: error + retention-days: 1 - name: Log the API budget if: always() @@ -2004,10 +1999,11 @@ jobs: with: tokens: github-token - # A clean handoff or an empty key hands the judgment on; any other + # A clean handoff, an empty key or a key the credit gate skipped 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-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' + if: always() && steps.agree.outcome == 'success' && steps.credit.outputs.skip-key != 'true' && 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 @@ -2021,9 +2017,9 @@ jobs: # switch and the hardening run again. Only this job references the # subscription token. It runs after a check-intent that succeeded, so one # that failed, a denial on its key leg included, leaves this job skipped; - # or after check-intent-credit skipped check-intent, with no reads to take. + # or after check-intent's credit gate skipped its key leg. check-intent-subscription: - needs: [check-intent-credit, check-intent] + needs: check-intent if: >- !cancelled() && github.ref == 'refs/heads/main' @@ -2033,9 +2029,8 @@ jobs: && ((needs.check-intent.result == 'success' && ((needs.check-intent.outputs.handoff == 'true' && needs.check-intent.outputs.key-leg-clean == 'true') - || needs.check-intent.outputs.credential-present == 'false')) - || (needs.check-intent.result == 'skipped' - && needs.check-intent-credit.outputs.skip-key == 'true')) + || needs.check-intent.outputs.credential-present == 'false' + || needs.check-intent.outputs.skip-key == 'true'))) runs-on: ubuntu-24.04 timeout-minutes: 301 cache-mode: read @@ -2249,7 +2244,7 @@ jobs: # 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 + uses: melodic-software/ci-workflows/.github/actions/apply-claude-managed-settings@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 - name: Isolate the runner if: steps.agree.outcome == 'success' @@ -2323,7 +2318,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: # No work may outlive the result message the launcher reads. CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" @@ -2393,7 +2388,7 @@ jobs: known-failure-class: ${{ steps.judge.outputs.lane-class }} handoff: ${{ steps.judge.outputs.handoff == 'true' && steps.sub-leg.outcome != 'failure' }} # Why this job ran, for its 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' || needs.check-intent-credit.outputs.skip-key == 'true' && 'credit-stop' || '' }} + 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' || needs.check-intent.outputs.skip-key == 'true' && 'credit-stop' || '' }} # 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. @@ -2441,7 +2436,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -2503,7 +2498,7 @@ jobs: max-turns: ${{ steps.models.outputs.max-turns }} known-failure-class: ${{ steps.judge-2.outputs.lane-class }} handoff: ${{ steps.judge-2.outputs.handoff == 'true' && steps.sub-leg-2.outcome != 'failure' }} - 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' || needs.check-intent-credit.outputs.skip-key == 'true' && 'credit-stop' || '' }} + 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' || needs.check-intent.outputs.skip-key == 'true' && 'credit-stop' || '' }} - name: Upload chunk 2's sealed transcript if: always() && steps.judge-2.outputs.execution-file != '' @@ -2538,7 +2533,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -2601,7 +2596,7 @@ jobs: max-turns: ${{ steps.models.outputs.max-turns }} known-failure-class: ${{ steps.judge-3.outputs.lane-class }} handoff: ${{ steps.judge-3.outputs.handoff == 'true' && steps.sub-leg-3.outcome != 'failure' }} - 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' || needs.check-intent-credit.outputs.skip-key == 'true' && 'credit-stop' || '' }} + 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' || needs.check-intent.outputs.skip-key == 'true' && 'credit-stop' || '' }} - name: Upload chunk 3's sealed transcript if: always() && steps.judge-3.outputs.execution-file != '' @@ -2636,7 +2631,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -2700,7 +2695,7 @@ jobs: max-turns: ${{ steps.models.outputs.max-turns }} known-failure-class: ${{ steps.judge-4.outputs.lane-class }} handoff: ${{ steps.judge-4.outputs.handoff == 'true' && steps.sub-leg-4.outcome != 'failure' }} - 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' || needs.check-intent-credit.outputs.skip-key == 'true' && 'credit-stop' || '' }} + 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' || needs.check-intent.outputs.skip-key == 'true' && 'credit-stop' || '' }} - name: Upload chunk 4's sealed transcript if: always() && steps.judge-4.outputs.execution-file != '' @@ -2736,7 +2731,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -2801,7 +2796,7 @@ jobs: max-turns: ${{ steps.models.outputs.max-turns }} known-failure-class: ${{ steps.judge-5.outputs.lane-class }} handoff: ${{ steps.judge-5.outputs.handoff == 'true' && steps.sub-leg-5.outcome != 'failure' }} - 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' || needs.check-intent-credit.outputs.skip-key == 'true' && 'credit-stop' || '' }} + 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' || needs.check-intent.outputs.skip-key == 'true' && 'credit-stop' || '' }} - name: Upload chunk 5's sealed transcript if: always() && steps.judge-5.outputs.execution-file != '' @@ -2836,7 +2831,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -2902,7 +2897,7 @@ jobs: max-turns: ${{ steps.models.outputs.max-turns }} known-failure-class: ${{ steps.judge-6.outputs.lane-class }} handoff: ${{ steps.judge-6.outputs.handoff == 'true' && steps.sub-leg-6.outcome != 'failure' }} - 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' || needs.check-intent-credit.outputs.skip-key == 'true' && 'credit-stop' || '' }} + 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' || needs.check-intent.outputs.skip-key == 'true' && 'credit-stop' || '' }} - name: Upload chunk 6's sealed transcript if: always() && steps.judge-6.outputs.execution-file != '' @@ -2937,7 +2932,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -3004,7 +2999,7 @@ jobs: max-turns: ${{ steps.models.outputs.max-turns }} known-failure-class: ${{ steps.judge-7.outputs.lane-class }} handoff: ${{ steps.judge-7.outputs.handoff == 'true' && steps.sub-leg-7.outcome != 'failure' }} - 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' || needs.check-intent-credit.outputs.skip-key == 'true' && 'credit-stop' || '' }} + 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' || needs.check-intent.outputs.skip-key == 'true' && 'credit-stop' || '' }} - name: Upload chunk 7's sealed transcript if: always() && steps.judge-7.outputs.execution-file != '' @@ -3039,7 +3034,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -3107,7 +3102,7 @@ jobs: max-turns: ${{ steps.models.outputs.max-turns }} known-failure-class: ${{ steps.judge-8.outputs.lane-class }} handoff: ${{ steps.judge-8.outputs.handoff == 'true' && steps.sub-leg-8.outcome != 'failure' }} - 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' || needs.check-intent-credit.outputs.skip-key == 'true' && 'credit-stop' || '' }} + 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' || needs.check-intent.outputs.skip-key == 'true' && 'credit-stop' || '' }} - name: Upload chunk 8's sealed transcript if: always() && steps.judge-8.outputs.execution-file != '' @@ -3142,7 +3137,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -3211,7 +3206,7 @@ jobs: max-turns: ${{ steps.models.outputs.max-turns }} known-failure-class: ${{ steps.judge-9.outputs.lane-class }} handoff: ${{ steps.judge-9.outputs.handoff == 'true' && steps.sub-leg-9.outcome != 'failure' }} - 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' || needs.check-intent-credit.outputs.skip-key == 'true' && 'credit-stop' || '' }} + 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' || needs.check-intent.outputs.skip-key == 'true' && 'credit-stop' || '' }} - name: Upload chunk 9's sealed transcript if: always() && steps.judge-9.outputs.execution-file != '' @@ -3246,7 +3241,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -3316,7 +3311,7 @@ jobs: max-turns: ${{ steps.models.outputs.max-turns }} known-failure-class: ${{ steps.judge-10.outputs.lane-class }} handoff: ${{ steps.judge-10.outputs.handoff == 'true' && steps.sub-leg-10.outcome != 'failure' }} - 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' || needs.check-intent-credit.outputs.skip-key == 'true' && 'credit-stop' || '' }} + 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' || needs.check-intent.outputs.skip-key == 'true' && 'credit-stop' || '' }} - name: Upload chunk 10's sealed transcript if: always() && steps.judge-10.outputs.execution-file != '' @@ -3351,7 +3346,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -3422,7 +3417,7 @@ jobs: max-turns: ${{ steps.models.outputs.max-turns }} known-failure-class: ${{ steps.judge-11.outputs.lane-class }} handoff: ${{ steps.judge-11.outputs.handoff == 'true' && steps.sub-leg-11.outcome != 'failure' }} - 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' || needs.check-intent-credit.outputs.skip-key == 'true' && 'credit-stop' || '' }} + 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' || needs.check-intent.outputs.skip-key == 'true' && 'credit-stop' || '' }} - name: Upload chunk 11's sealed transcript if: always() && steps.judge-11.outputs.execution-file != '' @@ -3457,7 +3452,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -3529,7 +3524,7 @@ jobs: max-turns: ${{ steps.models.outputs.max-turns }} known-failure-class: ${{ steps.judge-12.outputs.lane-class }} handoff: ${{ steps.judge-12.outputs.handoff == 'true' && steps.sub-leg-12.outcome != 'failure' }} - 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' || needs.check-intent-credit.outputs.skip-key == 'true' && 'credit-stop' || '' }} + 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' || needs.check-intent.outputs.skip-key == 'true' && 'credit-stop' || '' }} - name: Upload chunk 12's sealed transcript if: always() && steps.judge-12.outputs.execution-file != '' @@ -3627,7 +3622,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -3702,7 +3697,7 @@ jobs: max-turns: ${{ steps.models.outputs.max-turns }} known-failure-class: ${{ steps.judge-spec.outputs.lane-class }} handoff: ${{ steps.judge-spec.outputs.handoff == 'true' && steps.sub-leg-spec.outcome != 'failure' }} - 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' || needs.check-intent-credit.outputs.skip-key == 'true' && 'credit-stop' || '' }} + 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' || needs.check-intent.outputs.skip-key == 'true' && 'credit-stop' || '' }} - name: Upload the spec judgment's sealed transcript if: always() && steps.judge-spec.outputs.execution-file != '' @@ -4071,7 +4066,7 @@ jobs: # 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 + uses: melodic-software/ci-workflows/.github/actions/apply-claude-managed-settings@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 - name: Isolate the runner if: steps.agree.outcome == 'success' @@ -4143,7 +4138,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: # No work may outlive the result message the launcher reads. CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" @@ -4241,7 +4236,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -4324,7 +4319,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -4407,7 +4402,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -4491,7 +4486,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -4574,7 +4569,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -4657,7 +4652,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -4740,7 +4735,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -4823,7 +4818,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -4906,7 +4901,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -4989,7 +4984,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -5072,7 +5067,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: @@ -5218,7 +5213,7 @@ jobs: 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 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@bc62beb95a989641f30552a932a132ebb8ecd801 # bc62beb 2026-11-03 env: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" with: