diff --git a/.changes/pr-pipeline/ci-pr-verify-handoff-5e18f3d6.md b/.changes/pr-pipeline/ci-pr-verify-handoff-5e18f3d6.md new file mode 100644 index 0000000000..0baf65d5b8 --- /dev/null +++ b/.changes/pr-pipeline/ci-pr-verify-handoff-5e18f3d6.md @@ -0,0 +1,7 @@ +--- +bump: minor +--- + +### Changed + +- pr-verify hands the judgment to the subscription job on run-claude-cli's `handoff` output instead of the `credit-stop` class, so a key the API refuses with status 401 or 402 (class `auth`) also falls back; a 403 still fails the key job. The key-leg warning says whether to replace the key or rotate one in. The subscription job's lane record is plain `pr-verify`, with the reason in `fallback-reason` (`credit-stop`, `auth` or `no-key`). Every ci-workflows action pr-verify uses is pinned to v0.58.0, whose run-claude-cli installs the latest Claude Code release by default. diff --git a/.github/actions/report-check-run/lane-record.mjs b/.github/actions/report-check-run/lane-record.mjs index 3cf406b4be..e58e154182 100644 --- a/.github/actions/report-check-run/lane-record.mjs +++ b/.github/actions/report-check-run/lane-record.mjs @@ -13,6 +13,8 @@ const CLASSES = new Set([ "overloaded", "max-turns", "timeout", + "install-failed", + "no-execution", "other", "skipped-validation", "incomplete", diff --git a/.github/actions/report-check-run/lane-record.test.mjs b/.github/actions/report-check-run/lane-record.test.mjs index 210308cd41..31ebbc1a03 100644 --- a/.github/actions/report-check-run/lane-record.test.mjs +++ b/.github/actions/report-check-run/lane-record.test.mjs @@ -50,6 +50,17 @@ test("the output record drops what fails its shape: a reset off rate-limit, a fo assert.equal(laneRecordOutput({ ...forgedRun, runId: "7", runAttempt: "0" }).externalId, null); }); +// The classes report-lane-outcome's failure-class output names at the pinned +// ci-workflows v0.56.0 beyond the original six. +test("an install-failed or no-execution run keeps its output record", () => { + for (const failureClass of ["install-failed", "no-execution"]) { + assert.equal( + laneRecordOutput({ lane: "pr-unblock", activity: "x", failureClass }).line, + `lane=pr-unblock activity=x class=${failureClass}`, + ); + } +}); + test("no record, an unknown class, or a malformed name add no output record", () => { assert.equal(laneRecordOutput({ lane: "pr-unblock", activity: "x" }), null); assert.equal(laneRecordOutput({ lane: "pr-unblock", activity: "x", failureClass: "forged class" }), null); diff --git a/.github/scripts/pr-verify/probe/parity.test.mjs b/.github/scripts/pr-verify/probe/parity.test.mjs index 23658e1500..71ca35f5e6 100644 --- a/.github/scripts/pr-verify/probe/parity.test.mjs +++ b/.github/scripts/pr-verify/probe/parity.test.mjs @@ -171,7 +171,7 @@ test("cliArgs is lane.cjs's argv for this lane, plus --settings", () => { }); test("the runner's CLI version, environment additions and managed settings", () => { - assert.equal(CLI_VERSION, "2.1.304"); + assert.equal(CLI_VERSION, "latest"); assert.deepEqual(LEG_ENV, { CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: "1", CLAUDE_CODE_RETRY_WATCHDOG: "1", diff --git a/.github/scripts/pr-verify/probe/replay.mjs b/.github/scripts/pr-verify/probe/replay.mjs index 8505445864..d69ae30a93 100644 --- a/.github/scripts/pr-verify/probe/replay.mjs +++ b/.github/scripts/pr-verify/probe/replay.mjs @@ -24,7 +24,9 @@ // https://code.claude.com/docs/en/settings-reference (scope Managed), as of // 2026-10-20; recheck when that row's scope changes. // - CLI version: CLI_VERSION, the cli-version default of run-claude-cli at -// RUN_CLAUDE_CLI_PIN; a live run refuses any other. +// RUN_CLAUDE_CLI_PIN. `latest` resolves, as lane.cjs does, with +// `npm view @anthropic-ai/claude-code version`; a live run refuses any +// other version. // - Environment: built from an allowlist, then lane.cjs legEnv's additions // and the judge step's env; nothing else of the caller's shell passes. // - MCP: --strict-mcp-config with no --mcp-config, as on the runner. @@ -58,8 +60,8 @@ const REPOSITORY = "melodic-software/claude-code-plugins"; // 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 = "7b362428c76085a160e28656cd2c75c3147a69ff"; -export const CLI_VERSION = "2.1.304"; +export const RUN_CLAUDE_CLI_PIN = "5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c"; +export const CLI_VERSION = "latest"; export const MANAGED_SETTINGS = Object.freeze({ apiKeyHelper: "", allowManagedHooksOnly: true, @@ -673,16 +675,17 @@ export async function main(argv = process.argv.slice(2)) { const models = resolveModels(); // A dry run never starts the CLI, so it needs none installed. const version = values.live ? /^(\d+\.\d+\.\d+)/.exec(sh(values.claude, ["--version"], { env: { PATH: process.env.PATH } }))?.[1] : "not checked"; + const runnerVersion = values.live && CLI_VERSION === "latest" ? sh("npm", ["view", "@anthropic-ai/claude-code", "version"]) : CLI_VERSION; const commit = sh("git", ["-C", REPO_ROOT, "rev-parse", "HEAD"]); console.log( - `pr-verify replay: commit ${commit}; run-claude-cli ${RUN_CLAUDE_CLI_PIN}; claude ${version} (runner ${CLI_VERSION}); ` + + `pr-verify replay: commit ${commit}; run-claude-cli ${RUN_CLAUDE_CLI_PIN}; claude ${version} (runner ${runnerVersion}); ` + `route ${JUDGE.modelRoute} -> ${models.profile}: model ${models.model}, effort ${models.effort || "default"}, max-turns ${models.maxTurns}; ` + `${cases.length} case(s) x ${runs} run(s); ${values.live ? "live" : "dry run"}`, ); const opts = { live: values.live, claude: values.claude, maxAttempts, out: values.out }; if (values.live) { - if (version !== CLI_VERSION) { - throw new Error(`claude is ${version}; the runner runs ${CLI_VERSION}`); + if (version !== runnerVersion) { + throw new Error(`claude is ${version}; the runner runs ${runnerVersion}`); } if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) { throw new Error("set CLAUDE_CODE_OAUTH_TOKEN (claude setup-token) for a live run"); diff --git a/.github/scripts/pr-verify/workflow.test.mjs b/.github/scripts/pr-verify/workflow.test.mjs index c4dcbb8fc4..31517a6024 100644 --- a/.github/scripts/pr-verify/workflow.test.mjs +++ b/.github/scripts/pr-verify/workflow.test.mjs @@ -38,12 +38,13 @@ const list = (block, key) => { return body.split("\n").map((line) => line.trim()).filter(Boolean); }; const PIN = /^melodic-software\/ci-workflows\/\.github\/actions\/([a-z-]+)@[0-9a-f]{40} # v\d+\.\d+\.\d+$/; -// The subscription job's one lane record names why the key did not judge. -const SUB_LANE = - "${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'pr-verify (key AUTOMATIONS_CLAUDE_API_KEY hit a credit stop)' || 'pr-verify (key AUTOMATIONS_CLAUDE_API_KEY is empty)' }}"; +// 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). +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' || '' }}"; const KEY_CLASS = "${{ steps.judge.outputs.lane-class == 'credit-stop' && 'rate-limit' || steps.judge.outputs.lane-class }}"; const KEY_REPORT_IF = - "(steps.judge.outcome == 'success' || steps.judge.outcome == 'failure') && (steps.judge.outputs.lane-class != 'credit-stop' || steps.key-leg.outcome == 'failure')"; + "(steps.judge.outcome == 'success' || steps.judge.outcome == 'failure') && (steps.judge.outputs.handoff != 'true' || steps.key-leg.outcome == 'failure')"; const CREDENTIAL = { [KEY]: ["api-key", "AUTOMATIONS_CLAUDE_API_KEY"], [SUB]: ["subscription-token", "AUTOMATIONS_CLAUDE_CODE_OAUTH_TOKEN"], @@ -130,7 +131,7 @@ test("each secret appears only in its own job", () => { assert.doesNotMatch(text, /secrets: inherit|toJSON\(secrets\)/); }); -test("the subscription job runs only on a clean credit stop or an empty key, on a fixed runner", () => { +test("the subscription job runs only on a clean handoff or an empty key, on a fixed runner", () => { const job = jobText(SUB); assert.match(job, /^ {4}needs: check-intent$/m); const gate = /^ {4}if: >-\n((?: {6}.*\n)+)/m.exec(job)?.[1].replace(/\s+/g, " ").trim(); @@ -141,7 +142,7 @@ test("the subscription job runs only on a clean credit stop or an empty key, on "&& github.event.workflow_run.event == 'pull_request'", "&& github.event.workflow_run.conclusion == 'success'", "&& github.event.workflow_run.head_repository.full_name == github.repository", - "&& ((needs.check-intent.outputs.lane-class == 'credit-stop'", + "&& ((needs.check-intent.outputs.handoff == 'true'", "&& needs.check-intent.outputs.key-leg-clean == 'true')", "|| needs.check-intent.outputs.credential-present == 'false')", ].join(" "), @@ -149,12 +150,12 @@ test("the subscription job runs only on a clean credit stop or an empty key, on // No status function, so GitHub adds success(): a failed key job skips it. assert.doesNotMatch(gate, /always\(\)|failure\(\)|cancelled\(\)|success\(\)/); assert.match(job, /^ {4}runs-on: ubuntu-24\.04$/m); - // The key job's outputs reach only that if and the record's lane name, - // which they pick between two literals; never a run: script. + // The key job's outputs reach only that if and the record's fallback + // reason, which they pick between three literals; never a run: script. const steps = job.slice(job.indexOf("\n steps:\n")); assert.deepEqual( steps.split("\n").filter((line) => line.includes("needs.")), - [` lane: ${SUB_LANE}`], + [` fallback-reason: ${FALLBACK_REASON}`], ); const key = jobText(KEY); const outputs = /^ {4}outputs:\n((?: {6}.*\n)+)/m.exec(key)?.[1]; @@ -162,6 +163,7 @@ test("the subscription job runs only on a clean credit stop or an empty key, on outputs, [ " lane-class: ${{ steps.judge.outputs.lane-class }}", + " handoff: ${{ steps.judge.outputs.handoff }}", " credential-present: ${{ steps.credential.outputs.present }}", " key-leg-clean: ${{ steps.key-leg.outputs.clean }}", "", @@ -173,20 +175,28 @@ test("the subscription job runs only on a clean credit stop or an empty key, on } }); -test("the key job hands the judgment on only after a clean credit stop or with no key", () => { +test("the key job hands the judgment on only after a clean handoff or with no key", () => { const leg = step("Check the key leg"); - assert.match(leg, /^ {8}if: steps\.judge\.outputs\.lane-class == 'credit-stop'$/m); + // run-claude-cli's handoff: a credit stop, or auth with API status 401 or + // 402; a 403 is auth too but hands nothing off, so it fails the key job. + assert.match(leg, /^ {8}if: steps\.judge\.outputs\.handoff == 'true'$/m); assert.match(leg, /^ {10}LOG_FILE: \$\{\{ steps\.judge\.outputs\.log-file \}\}$/m); + assert.match(leg, /^ {10}LANE_CLASS: \$\{\{ steps\.judge\.outputs\.lane-class \}\}$/m); assert.match(leg, /write-verdict\.mjs check-key-leg\n/); // It runs before the report, which reads its outcome, and its failure // leaves the red to "Fail without a verdict" after the report. assert.match(leg, /^ {8}continue-on-error: true$/m); assert.ok(index("Check the key leg") === index("Judge the pull request") + 1); assert.ok(index("Report the judgment's outcome") === index("Check the key leg") + 1); - // The owner's signal to rotate a key, printed before the denial check can stop the step. + // The owner's signal to replace the key (auth) or rotate one in (a credit + // stop), printed before the denial check can stop the step. The class + // reaches the script through env, never interpolated. const run = /^ {8}run: \|\n((?: {10}.*\n?)+)/m.exec(leg)[1]; - assert.match(run.split("\n")[0], /^ {10}echo "::warning::the automations role's API key \(AUTOMATIONS_CLAUDE_API_KEY\) hit a credit stop/); - assert.ok(run.indexOf("::warning::") < run.indexOf("check-key-leg")); + assert.equal(run.split("\n")[0], ' case "$LANE_CLASS" in'); + assert.match(run, /^ {12}auth\) echo "::warning::the API refused the automations role's API key \(AUTOMATIONS_CLAUDE_API_KEY\), class auth; the owner must replace the secret" ;;$/m); + assert.match(run, /^ {12}\*\) echo "::warning::the automations role's API key \(AUTOMATIONS_CLAUDE_API_KEY\) hit a credit stop; rotate a spare key in" ;;$/m); + assert.doesNotMatch(run, /\$\{\{/); + assert.ok(run.lastIndexOf("::warning::") < run.indexOf("check-key-leg")); assert.equal(index("Check the key leg", SUB), -1); for (const job of JOBS) { const credential = step("Check the credential", job); @@ -211,7 +221,7 @@ test("the subscription job repeats the key job's steps but for the credential", .replace(/^( {10})subscription-token: /m, "$1api-key: ") .replace(`if: ${KEY_REPORT_IF}`, "if: steps.judge.outcome == 'success' || steps.judge.outcome == 'failure'") .replace(`known-failure-class: ${KEY_CLASS}`, "known-failure-class: ${{ steps.judge.outputs.lane-class }}") - .replace(`lane: ${SUB_LANE}`, "lane: pr-verify") + .replace(`\n fallback-reason: ${FALLBACK_REASON}`, "") .replace(/ && steps\.credential\.outputs\.present != 'false' && steps\.key-leg\.outcome != 'success'$/m, ""), ); assert.deepEqual(normalize(STEPS[SUB]), normalize(STEPS[KEY])); @@ -272,7 +282,8 @@ test("the outcome is reported whenever a judge ran to a final class, and a missi } assert.match(step("Report the judgment's outcome", SUB), /^ {8}if: steps\.judge\.outcome == 'success' \|\| steps\.judge\.outcome == 'failure'$/m); assert.equal(input(step("Report the judgment's outcome", SUB), "known-failure-class"), "${{ steps.judge.outputs.lane-class }}"); - assert.equal(input(step("Report the judgment's outcome", SUB), "lane"), SUB_LANE); + assert.equal(input(step("Report the judgment's outcome", SUB), "lane"), "pr-verify"); + assert.equal(input(step("Report the judgment's outcome", SUB), "fallback-reason"), FALLBACK_REASON); // Only a hand-off spares the key job its red; an empty token fails the subscription job. assert.match( STEPS[KEY].at(-1).block, @@ -281,18 +292,27 @@ test("the outcome is reported whenever a judge ran to a final class, and a missi assert.match(STEPS[SUB].at(-1).block, /^ {8}if: always\(\) && steps\.agree\.outcome == 'success' && steps\.verdict\.outcome != 'success'$/m); }); -test("a credit stop leaves exactly one lane record, from the job that saw its end", () => { +test("a handoff leaves exactly one lane record, from the job that saw its end", () => { const report = step("Report the judgment's outcome", KEY); assert.equal(/^ {8}if: (.*)$/m.exec(report)[1], KEY_REPORT_IF); assert.equal(input(report, "lane"), "pr-verify"); + assert.equal(input(report, "fallback-reason"), undefined); // A key leg that is not clean hands nothing on, so the key job records the - // stop itself, as the class report-lane-outcome knows for a credit stop. + // stop itself: a credit stop as rate-limit, the class report-lane-outcome + // knows for it, and auth as auth. assert.equal(input(report, "known-failure-class"), KEY_CLASS); - // A clean stop skips this report; the subscription job, which runs exactly - // when key-leg-clean is true, records the run and names the stop in its lane. + // A clean handoff skips this report; the subscription job, which runs + // exactly when key-leg-clean is true, records the run with a plain lane + // name and says why in fallback-reason, one of report-lane-outcome's + // credit-stop, auth or no-key. assert.match(jobText(SUB), /needs\.check-intent\.outputs\.key-leg-clean == 'true'/); - assert.match(SUB_LANE, /lane-class == 'credit-stop' && '[^']*credit stop\)'/); - assert.doesNotMatch(SUB_LANE, /=[a-z]/, "no key=value token a lane reader could parse"); + assert.deepEqual([...FALLBACK_REASON.matchAll(/&& '([a-z-]+)'/g)].map((m) => m[1]), ["credit-stop", "auth", "no-key"]); + assert.match(FALLBACK_REASON, /lane-class == 'auth' && 'auth'/); + // No gate keys the subscription leg on the credit-stop class any more: + // run-claude-cli's handoff output is the one signal. + for (const line of text.split("\n").filter((l) => /^\s+(- )?if: /.test(l) || /^\s+&& /.test(l))) { + assert.doesNotMatch(line, /lane-class [!=]= 'credit-stop'/, line); + } }); test("every ci-workflows action is pinned to one release", () => { diff --git a/.github/scripts/pr-verify/write-verdict.mjs b/.github/scripts/pr-verify/write-verdict.mjs index 9d31c7156a..e6d85bcac4 100644 --- a/.github/scripts/pr-verify/write-verdict.mjs +++ b/.github/scripts/pr-verify/write-verdict.mjs @@ -135,7 +135,7 @@ export function earlierDenials(logText) { } } -// After the key leg's credit stop the subscription job judges on a new VM +// After the key leg hands off, the subscription job judges on a new VM // that cannot read this leg's log, so the key job records here whether the // leg was free of denials; the subscription job runs only on clean=true. export function checkKeyLeg(env = process.env) { diff --git a/.github/workflows/intake-label-needs-triage.yml b/.github/workflows/intake-label-needs-triage.yml index c75d6a9ca6..f163d7a7c3 100644 --- a/.github/workflows/intake-label-needs-triage.yml +++ b/.github/workflows/intake-label-needs-triage.yml @@ -56,7 +56,7 @@ jobs: if: ${{ !cancelled() && needs.born-triaged.outputs.skip != 'true' }} permissions: issues: write # passed to the reusable, which reads and adds issue labels - uses: melodic-software/ci-workflows/.github/workflows/intake-label-needs-triage.yml@182aca3ad32b79fd002def94e58eb498ac49e7af # v0.42.0 + uses: melodic-software/ci-workflows/.github/workflows/intake-label-needs-triage.yml@1bb8953644d16f21d10718962c6702f01ea3308a # v0.56.0 with: runner: ubuntu-24.04 label: needs-triage diff --git a/.github/workflows/intake-reenter.yml b/.github/workflows/intake-reenter.yml index 2b3dc02640..6452c8ffe9 100644 --- a/.github/workflows/intake-reenter.yml +++ b/.github/workflows/intake-reenter.yml @@ -16,11 +16,16 @@ name: intake-reenter # default branch only. # # Accepted exposure: only a trusted comment decides whether a re-entry -# happens, but the triage run it starts reads every comment on the issue, so -# any commenter's text reaches the triage model on each re-entry. Triage can -# only apply allowlisted labels and post with mentions broken, holds no write -# tools, and never adds `agent-ready`. Passing only trusted comments on a -# dispatch belongs in ci-workflows' intake-triage workflow. +# happens, and the triage run it starts passes the model only comments whose +# author id is in intake-triage.yml's allowed-author-ids (a person) or +# allowed-bot-sender-ids (a bot), never the lane's own triage comment. That +# bot list holds github-actions[bot], the dispatcher, so any other comment a +# workflow posts with a GITHUB_TOKEN counts as trusted too. The +# issue's title and body still reach the model on each re-entry, and that +# text, like the trusted comments, stays untrusted data. The model holds no +# GitHub token and only Read, Grep and Glob; a job with no model applies only +# allowlisted labels, posts with mentions broken, and never adds +# `agent-ready`. on: issue_comment: types: [created] diff --git a/.github/workflows/intake-triage.yml b/.github/workflows/intake-triage.yml index 535f72ecfd..e46ac54c86 100644 --- a/.github/workflows/intake-triage.yml +++ b/.github/workflows/intake-triage.yml @@ -104,7 +104,9 @@ jobs: # Bots the reusable admits: github-actions[bot] (41898282), the actor of # a dispatch sent with a GITHUB_TOKEN, and the lanes App when its id is # set. An issue the GITHUB_TOKEN opens starts no issues run, so listing - # 41898282 admits only its dispatches. + # 41898282 starts runs only for its dispatches. The same list picks the + # comments the model reads, so any comment a GITHUB_TOKEN posts (other + # than the lane's own marker comment) reaches the model as trusted text. - name: Compose the bot id list id: bots env: @@ -176,7 +178,7 @@ jobs: concurrency: group: intake-triage-${{ github.event.issue.number || inputs.issue-number || github.run_id }} cancel-in-progress: false - uses: melodic-software/ci-workflows/.github/workflows/intake-triage.yml@7b362428c76085a160e28656cd2c75c3147a69ff # v0.54.0 + uses: melodic-software/ci-workflows/.github/workflows/intake-triage.yml@1bb8953644d16f21d10718962c6702f01ea3308a # v0.56.0 with: runner: ubuntu-24.04 role: automations diff --git a/.github/workflows/maintenance-audit-test-selection.yml b/.github/workflows/maintenance-audit-test-selection.yml index e72c876c28..030cfc45ad 100644 --- a/.github/workflows/maintenance-audit-test-selection.yml +++ b/.github/workflows/maintenance-audit-test-selection.yml @@ -79,7 +79,7 @@ jobs: .github/requirements-ci.txt plugins/animation/requirements.txt - name: Install and verify ShellCheck toolchain - uses: melodic-software/ci-workflows/.github/actions/shellcheck@ab83b01273026ab5c23c6f3b40e946d97a863fa2 # v0.39.3 + uses: melodic-software/ci-workflows/.github/actions/shellcheck@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: paths: plugins/bash-format rcfile: .shellcheckrc diff --git a/.github/workflows/pr-require-checks.yml b/.github/workflows/pr-require-checks.yml index cdad071e3d..37d1d2df37 100644 --- a/.github/workflows/pr-require-checks.yml +++ b/.github/workflows/pr-require-checks.yml @@ -339,7 +339,7 @@ jobs: - name: Match the changed files against the filter groups id: match continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/detect-changes@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/detect-changes@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: filters: | shell: @@ -864,11 +864,11 @@ jobs: path: | ~/.npm/_npx ~/.npm/_cacache - key: npx-markdownlint-cli2-${{ runner.os }}-ab83b01273026ab5c23c6f3b40e946d97a863fa2 + key: npx-markdownlint-cli2-${{ runner.os }}-5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c - name: Lint markdown id: markdown continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/markdownlint@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/markdownlint@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: config: .markdownlint-cli2.jsonc globs: ${{ steps.changed_md.outputs.globs }} @@ -884,7 +884,7 @@ jobs: - name: Spell-check id: typos continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/typos@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/typos@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: config: _typos.toml # typos skips hidden directories; naming .changes checks the @@ -907,7 +907,7 @@ jobs: - name: Scan for secrets id: gitleaks continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/gitleaks@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/gitleaks@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: config: .gitleaks.toml scan-mode: git @@ -921,7 +921,7 @@ jobs: - name: Check local links and anchors id: lychee continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/lychee-offline@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/lychee-offline@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: config: lychee.toml exclude: >- @@ -931,7 +931,7 @@ jobs: - name: Check editorconfig conformance id: editorconfig continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/editorconfig-checker@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/editorconfig-checker@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: config: .editorconfig-checker.json @@ -945,13 +945,13 @@ jobs: id: actionlint if: needs.select-tests.outputs.run_workflows == 'true' continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/actionlint@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/actionlint@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 - name: Validate dependabot.yml id: dependabot_schema if: needs.select-tests.outputs.run_workflows == 'true' continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: builtin-schema: vendor.dependabot files: .github/dependabot.yml @@ -959,7 +959,7 @@ jobs: id: workflow_schema if: needs.select-tests.outputs.run_workflows == 'true' continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: builtin-schema: vendor.github-workflows files: >- @@ -971,12 +971,12 @@ jobs: - name: Verify shebang files are executable id: exec_bit continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/check-exec-bit@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/check-exec-bit@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 - name: Check for machine-specific paths id: machine_paths continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/check-machine-paths@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/check-machine-paths@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: # The guardrails plugin bundles a path-detection pattern lib whose # regex bodies self-match this lane's own detector. The autonomy @@ -1009,12 +1009,12 @@ jobs: - name: Check index-level EOL drift id: eol continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/check-line-endings@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/check-line-endings@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 - name: Scan comment hygiene id: comment_hygiene continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/check-comment-markers@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/check-comment-markers@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: # The audit-comment-residue skill IS a code-comment linter: its detector # fixtures and shape library necessarily contain the markers this policy @@ -1480,7 +1480,7 @@ jobs: - name: Lint shell scripts id: shellcheck continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/shellcheck@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/shellcheck@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: files: ${{ steps.changed_shell.outputs.files }} rcfile: .shellcheckrc @@ -1894,7 +1894,7 @@ jobs: id: marketplace_schema if: needs.select-tests.outputs.run_manifests == 'true' continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: schemafile: https://json.schemastore.org/claude-code-marketplace.json files: .claude-plugin/marketplace.json @@ -1902,7 +1902,7 @@ jobs: id: plugin_schema if: needs.select-tests.outputs.run_manifests == 'true' continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: schemafile: https://json.schemastore.org/claude-code-plugin-manifest.json files: plugins/*/.claude-plugin/plugin.json @@ -2301,7 +2301,7 @@ jobs: - name: Validate every eval set against the bundled schema id: evals_schema continue-on-error: true - uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: schemafile: plugins/skill-quality/reference/evals.schema.json files: plugins/*/skills/*/evals/evals.json @@ -2544,7 +2544,7 @@ jobs: # available to the bash-format contract suites in this same job. It # verifies the install on one small file that sources nothing; the # ShellCheck gate in `lint-shell` is what lints the repository. - uses: melodic-software/ci-workflows/.github/actions/shellcheck@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/shellcheck@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: files: scripts/aggregate-hygiene-results.sh rcfile: .shellcheckrc @@ -2969,8 +2969,9 @@ jobs: # HEAD. That replaces code-owner review of sync pull requests, so the job is a # trust boundary only while all of this holds: the action pinned by full SHA, # called from this code-owned file, exactly the permissions below, full - # history, and no paths filter. A merge group, schedule or dispatch run has - # no pull request diff and passes. The synced + # history, and no paths filter. A merge group run checks each queued pull + # request's own diff, read from the pulls API, by the same rules; a sync + # pull request must be queued alone. The synced # pr-check-managed-files-hosted.yml runs the same guard advisory and # paths-filtered; this job is the gate. check-managed-files: @@ -2991,7 +2992,7 @@ jobs: # The guard diffs base...head; both commits must be present. fetch-depth: 0 - name: Guard managed files - uses: melodic-software/ci-workflows/.github/actions/check-managed-files@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/check-managed-files@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 ci-status: needs: @@ -3017,9 +3018,12 @@ jobs: # `ci-lanes` commit status the last full run recorded on this SHA. if: ${{ !cancelled() }} runs-on: ubuntu-24.04 - # Nothing here waits on another run, so the job is sized by its own work: - # 9 s p95 and 42 s max in full mode, about 10 s contract-only. - timeout-minutes: 3 + # 9 s p95 and 42 s max in full mode, about 10 s contract-only. The cap is + # the worst case of the pinned composites' waits: two 60 s rate-limit waits + # (`rate-limit-wait-seconds` on check-contract and aggregate-results), 15 s + # of own-gate retries, 7 s of status-write retries and the 90 s + # `rerun-wait-seconds` wait, about four minutes. + timeout-minutes: 5 permissions: # `yield-to-full-run` lists this workflow's runs on the head SHA, and # `rerun-contract-only-siblings` re-runs the red contract-only ones once @@ -3034,7 +3038,7 @@ jobs: statuses: write steps: - name: Check the pull-request contract - uses: melodic-software/ci-workflows/.github/actions/pr-require-checks/check-contract@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/pr-require-checks/check-contract@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: token: ${{ github.token }} # Carried over verbatim from `pr-issue-linkage.yml`, the caller this @@ -3229,7 +3233,7 @@ jobs: # aggregation: the job still fails on the contract step's exit code, and # the `ci-lanes` status is on the SHA for the next contract-only run. if: ${{ !cancelled() }} - uses: melodic-software/ci-workflows/.github/actions/pr-require-checks/aggregate-results@594987abe8657a40bccc13d301f1f0d5cccb5348 # v0.39.8 + uses: melodic-software/ci-workflows/.github/actions/pr-require-checks/aggregate-results@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: # Every lane of the needs graph above — the single source of truth # for the lane list — through the ordered-skip check. Adding a lane @@ -3251,4 +3255,7 @@ jobs: yield-to-full-run: 'true' rerun-contract-only-siblings: ${{ github.event.pull_request.draft && 'false' || 'true' }} carry-forward-wait-seconds: '0' + # This step's own name, so the own-step lookup never depends on + # how many steps of the job are in progress. + step-name: Aggregate lane results token: ${{ github.token }} diff --git a/.github/workflows/pr-review-hosted.yml b/.github/workflows/pr-review-hosted.yml index 9d753d2909..7c6b03f32a 100644 --- a/.github/workflows/pr-review-hosted.yml +++ b/.github/workflows/pr-review-hosted.yml @@ -181,7 +181,7 @@ jobs: # reviewed-head marker comment id-token: write # OIDC, mints the Claude GitHub App token (required # even with an OAuth/API-key credential) - uses: melodic-software/ci-workflows/.github/workflows/pr-review.yml@96b50d451c4483c476f2f963674e0a3ba9d50fe3 # v0.48.0 + uses: melodic-software/ci-workflows/.github/workflows/pr-review.yml@3a2c748fdef9cfb9ceec5a66289e21842189689f # v0.55.0 with: runner: ubuntu-24.04 # Empty unless resolve-model succeeded, so a failed job's partial output @@ -192,6 +192,13 @@ jobs: # reviewed. Where the repository does not set this variable the value is # empty and the reusable reviews no bot. allowed-bot-sender-ids: ${{ vars.AUTOMATION_LANES_APP_SENDER_ID }} + # People whose closing issues the review reads in full, by numeric user + # id: the `human` entries of .github/standards/trusted-actors/trusted-actors.json + # (standards components/trusted-actors/trusted-actors.json). Empty + # would replace every person's closing issue with a placeholder. The + # bot ids in allowed-bot-sender-ids above also mark issues those bots + # wrote as trusted closing-issue context. + allowed-author-ids: '153232337' # Pass only the one named secret (least privilege) rather than # `secrets: inherit`, which would forward every parent secret to the # called workflow. diff --git a/.github/workflows/pr-review-security-hosted.yml b/.github/workflows/pr-review-security-hosted.yml index 4d813ec9fd..5a2779cb05 100644 --- a/.github/workflows/pr-review-security-hosted.yml +++ b/.github/workflows/pr-review-security-hosted.yml @@ -175,7 +175,7 @@ jobs: # the lane's reviewed-head marker comment id-token: write # OIDC, mints the Claude GitHub App token (required # even with an OAuth/API-key credential) - uses: melodic-software/ci-workflows/.github/workflows/pr-review-security.yml@96b50d451c4483c476f2f963674e0a3ba9d50fe3 # v0.48.0 + uses: melodic-software/ci-workflows/.github/workflows/pr-review-security.yml@3a2c748fdef9cfb9ceec5a66289e21842189689f # v0.55.0 with: runner: ubuntu-24.04 # Empty unless resolve-model succeeded, so a failed job's partial output diff --git a/.github/workflows/pr-run-activity-read.yml b/.github/workflows/pr-run-activity-read.yml index e2795fd108..9f5f97b08b 100644 --- a/.github/workflows/pr-run-activity-read.yml +++ b/.github/workflows/pr-run-activity-read.yml @@ -965,7 +965,7 @@ jobs: if: >- always() && inputs.issue-number != '' && (steps.act-skill.outcome == 'success' || steps.act-skill.outcome == 'failure') - uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@64fd515245f2bdeea8dedca1a3cea9ca39804bec # v0.43.0 + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: outcome: ${{ steps.act-result.outputs.outcome || steps.act-skill.outcome }} execution-file: >- diff --git a/.github/workflows/pr-run-activity-write.yml b/.github/workflows/pr-run-activity-write.yml index 19adc4f676..ce8c122686 100644 --- a/.github/workflows/pr-run-activity-write.yml +++ b/.github/workflows/pr-run-activity-write.yml @@ -1368,7 +1368,7 @@ jobs: - name: Record the lane outcome id: lane-record if: always() && (steps.act-skill.outcome == 'success' || steps.act-skill.outcome == 'failure') - uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@64fd515245f2bdeea8dedca1a3cea9ca39804bec # v0.43.0 + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: outcome: ${{ steps.act-result.outputs.outcome || steps.act-skill.outcome }} execution-file: >- diff --git a/.github/workflows/pr-verify.yml b/.github/workflows/pr-verify.yml index e47f046253..c01f9e599a 100644 --- a/.github/workflows/pr-verify.yml +++ b/.github/workflows/pr-verify.yml @@ -16,14 +16,15 @@ name: pr-verify # run-claude-cli as the automations role, on one credential per job. The # check-intent job holds only the API key. The check-intent-subscription job # holds only the subscription token and runs, on a new ubuntu-24.04 VM, only -# when check-intent's key leg ended in a credit stop with no permission +# 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, so one job per run judges. The second job repeats every step from -# a fresh checkout. From check-intent it reads only three outputs, each -# compared to a fixed string in its `if` (and lane-class once more to pick -# its lane record's name between two literals); none of them is written into -# a `run:` script, and it takes no artifact or cache. run-claude-cli sets -# lane-class after it restores the step files, so the model cannot set it. +# a fresh checkout. From check-intent it reads only four outputs, each +# compared to a fixed string in its `if` or in the fallback-reason expression +# that picks one of three literals; none of them is written into a `run:` +# script, and it takes no artifact or cache. run-claude-cli sets handoff and +# lane-class after it restores the step files, so the model cannot set them. # The job tokens can only read. Every script and local action comes from the # main checkout in .base; the Claude actions are ci-workflows', pinned by SHA. # @@ -69,6 +70,7 @@ jobs: actions: read # the upstream run, and verdicts already uploaded outputs: lane-class: ${{ steps.judge.outputs.lane-class }} + handoff: ${{ steps.judge.outputs.handoff }} credential-present: ${{ steps.credential.outputs.present }} key-leg-clean: ${{ steps.key-leg.outputs.clean }} steps: @@ -202,7 +204,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@7b362428c76085a160e28656cd2c75c3147a69ff # v0.54.0 + uses: melodic-software/ci-workflows/.github/actions/apply-claude-managed-settings@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 - name: Isolate the runner if: steps.agree.outcome == 'success' @@ -238,9 +240,9 @@ jobs: # 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. A credit stop skips - # the verdict steps, and a clean one the outcome step too: the - # subscription job judges and records the run instead. + # step; "Fail without a verdict" carries the red. A handoff skips the + # 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 that fits in the # job's 30. The model may share its job with the upload: it has Read @@ -252,7 +254,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@7b362428c76085a160e28656cd2c75c3147a69ff # v0.54.0 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 env: # No work may outlive the result message the launcher reads. CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" @@ -285,28 +287,33 @@ jobs: json-schema-file: ${{ runner.temp }}/pr-verify-schema.json leg-timeout-minutes: "10" - # After a credit stop: the owner's signal to rotate a key, then the - # denial check. A denial on the key leg means no verdict, so the + # After a handoff: the owner's signal to rotate or replace the key, then + # the denial check. A denial on the key leg means no verdict, so the # subscription 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. + # the red. A 403 hands nothing off, so this step skips and the run fails. - name: Check the key leg id: key-leg - if: steps.judge.outputs.lane-class == 'credit-stop' + if: steps.judge.outputs.handoff == 'true' continue-on-error: true env: LOG_FILE: ${{ steps.judge.outputs.log-file }} + LANE_CLASS: ${{ steps.judge.outputs.lane-class }} run: | - echo "::warning::the automations role's API key (AUTOMATIONS_CLAUDE_API_KEY) hit a credit stop; rotate a spare key in" + case "$LANE_CLASS" in + auth) echo "::warning::the API refused the automations role's API key (AUTOMATIONS_CLAUDE_API_KEY), class auth; the owner must replace the secret" ;; + *) echo "::warning::the automations role's API key (AUTOMATIONS_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" - # One lane record per run: a clean credit stop is recorded by the - # subscription job; a stop with a denial on the key leg, here, as - # rate-limit, the class report-lane-outcome knows for a credit stop. + # One lane record per run: a clean handoff is recorded by the + # subscription job; a handoff with a denial on the key leg, here: a + # credit stop as rate-limit, the class report-lane-outcome knows for + # it, and auth as auth. - name: Report the judgment's outcome - if: (steps.judge.outcome == 'success' || steps.judge.outcome == 'failure') && (steps.judge.outputs.lane-class != 'credit-stop' || steps.key-leg.outcome == 'failure') - uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@7b362428c76085a160e28656cd2c75c3147a69ff # v0.54.0 + if: (steps.judge.outcome == 'success' || steps.judge.outcome == 'failure') && (steps.judge.outputs.handoff != 'true' || steps.key-leg.outcome == 'failure') + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: outcome: ${{ steps.judge.outcome }} execution-file: ${{ steps.judge.outputs.execution-file }} @@ -355,7 +362,7 @@ jobs: echo '```' } >>"$GITHUB_STEP_SUMMARY" - # A clean credit stop or an empty key hands the judgment on; any other + # 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' @@ -377,7 +384,7 @@ jobs: && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_repository.full_name == github.repository - && ((needs.check-intent.outputs.lane-class == 'credit-stop' + && ((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 @@ -518,7 +525,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@7b362428c76085a160e28656cd2c75c3147a69ff # v0.54.0 + uses: melodic-software/ci-workflows/.github/actions/apply-claude-managed-settings@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 - name: Isolate the runner if: steps.agree.outcome == 'success' @@ -566,7 +573,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@7b362428c76085a160e28656cd2c75c3147a69ff # v0.54.0 + uses: melodic-software/ci-workflows/.github/actions/run-claude-cli@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 env: # No work may outlive the result message the launcher reads. CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" @@ -601,16 +608,18 @@ jobs: - name: Report the judgment's outcome if: steps.judge.outcome == 'success' || steps.judge.outcome == 'failure' - uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@7b362428c76085a160e28656cd2c75c3147a69ff # v0.54.0 + uses: melodic-software/ci-workflows/.github/actions/report-lane-outcome@5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c # v0.58.0 with: outcome: ${{ steps.judge.outcome }} execution-file: ${{ steps.judge.outputs.execution-file }} - lane: ${{ needs.check-intent.outputs.lane-class == 'credit-stop' && 'pr-verify (key AUTOMATIONS_CLAUDE_API_KEY hit a credit stop)' || 'pr-verify (key AUTOMATIONS_CLAUDE_API_KEY is empty)' }} + lane: 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 structured output, in the last result message of the execution # file, is untrusted model text: read from the file and checked by the diff --git a/README.md b/README.md index 18b0e60bdd..b9f96f3a8b 100644 --- a/README.md +++ b/README.md @@ -168,8 +168,8 @@ local command**. The gap is discoverability, not a missing runner: every gate be - **Basis:** the #3522 owner decision (document running the composite's script from a `ci-workflows` checkout). Verified: the script runs standalone and needs only `EXTENSIONS` and `EXCLUDE`. -- **As of:** `ci-workflows` v0.39.8 (`594987abe8657a40bccc13d301f1f0d5cccb5348`), the pin in - `.github/workflows/pr-require-checks.yml`, re-verified by hand on 2026-10-10: the `contract-only` default is unchanged. +- **As of:** `ci-workflows` v0.58.0 (`5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c`), the pin in + `.github/workflows/pr-require-checks.yml`, re-verified by hand on 2026-10-21: the `contract-only` default is unchanged. - **Recheck:** the `pr-require-checks.yml` pin moves, or the composite's entry script changes its environment contract. diff --git a/docs/conventions/pr-pipeline/README.md b/docs/conventions/pr-pipeline/README.md index d78ae66325..5954a155b1 100644 --- a/docs/conventions/pr-pipeline/README.md +++ b/docs/conventions/pr-pipeline/README.md @@ -1291,10 +1291,12 @@ is `split`, and Uncertainty Known, Knowable and Unknown map to `run`, `research- Each Claude lane's runs are paid by one funding role: `reviews`, `automations` or `issue-lane`. A lane on ci-workflows' `run-claude-cli` action passes its role with the role's two secrets, `_CLAUDE_API_KEY` and `_CLAUDE_CODE_OAUTH_TOKEN` (role upper-snake, so `issue-lane` -gives `ISSUE_LANE_`); the key runs first and the subscription token only after a credit stop. -`pr-verify` holds one credential per job: `check-intent` runs on the key, and -`check-intent-subscription` runs on the token, on a new runner, only after `check-intent`'s credit -stop with no permission denial or when the key is empty. Each lane's workflow names its role as a +gives `ISSUE_LANE_`); the key runs first and the subscription token only after the key leg hands +off (`run-claude-cli`'s `handoff` output: a credit stop, or class `auth` with API status 401 or +402; a 403 hands nothing off). `pr-verify` holds one credential per job: `check-intent` runs on the +key, and `check-intent-subscription` runs on the token, on a new runner, only after +`check-intent`'s handoff with no permission denial or when the key is empty; its lane record is +`pr-verify` with `fallback-reason` `credit-stop`, `auth` or `no-key`. Each lane's workflow names its role as a literal, and the runner policy admits it; no config key records it. `intake`, `pr-unblock`, `fix-ci`, `pr-verify` and `pr-refine` take `automations`; `claude-review` and `claude-security-review` take `reviews`; `work-resolve-issue` takes `issue-lane`. diff --git a/scripts/check-docs-only-gate.sh b/scripts/check-docs-only-gate.sh index bfa6126e86..36a1ce66fd 100755 --- a/scripts/check-docs-only-gate.sh +++ b/scripts/check-docs-only-gate.sh @@ -999,7 +999,7 @@ is_required() { [[ "$required_closure" == *$'\n'"$1"$'\n'* ]]; } # carry-forward branch on is NOT checked here and is checked nowhere else # either: ci-workflows tests its composite against its own pr-require-checks.yml, not against # this repository's. It was verified by hand at pin -# 594987abe8657a40bccc13d301f1f0d5cccb5348 (v0.39.8) on 2026-10-10, path +# 5e18f3d6cebd6fa5e66b0941ee0f7ed6ba288d6c (v0.58.0) on 2026-10-21, path # .github/actions/pr-require-checks/aggregate-results: its `contract-only` # default is unchanged and is this predicate token for token, the caller passes no # `contract-only`, and run.sh still branches its carry-forward on that input. It