Skip to content

ci(lanes): give every three-leg lane the key-credit gate and the empty-subscription handoff - #13608

Merged
kyle-sexton merged 8 commits into
mainfrom
ci/uniform-lane-legs
Nov 5, 2026
Merged

kyle-sexton merged 8 commits into
mainfrom
ci/uniform-lane-legs

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Refs: #13586

Summary

The owner asked in #13586 that every lane be "consistent and uniform". This PR gives the lanes that already had all three legs the two rules #13590 and #13592 added:

  • (a) the check-key-credit gate;
  • (b) an absent subscription credential hands to the fallback leg, which starts only when the fallback token is set.

Fix

Rule (b), an empty subscription token:

Lane Before After
pr-run-activity-read.yml (resolve-conflicts, address-feedback, review-dependency, fix-ci, fix-ci-rung-up, fix-docs, resolve-issue) second call failed red, no third call handed-off is also true on an empty token when the caller runs a third call (fallback-ready), so the caller's existing subscription-handoff job starts it
pr-verify.yml check-intent subscription job red, no fallback check-intent-fallback also runs on credential-present == 'false' with fallback-ready == 'true'
maintenance-audit-lane-runs.yml judge subscription leg judged nothing the subscription leg records a no-key handoff marker when the fallback token is set; handoff.mjs accepts no-key on that leg
pr-review-main.yml already hands on an empty token only when the fallback token is set unchanged

In each case the fallback leg's lane record names fallback reason no-key. With every token empty, nothing new starts.

Per-leg timing:

  • The read runner's reuse summary now runs on the fallback token in the third call. Before, it ran on the empty subscription secret there.
  • That summary and the upstream-apply-changelog scrub-probe legs now write lane-outcome records.
  • A new test fails any run-claude-cli job that has no report-lane-outcome with lane-id. The one exception is listed: maintenance-measure-skill-edit propose, open on ci(lanes): run every model lane on the same key, subscription and fallback chain #13586.

Rule (a), the credit gate:

  • pr-verify.yml, pr-review-main.yml and ci-workflows pr-review-cli.yml (used by pr-review-hosted and security-hosted) already have it.
  • Read runner (pr-run-activity-read.yml, all seven activities):
    • broker-check, which runs no head code, runs check-key-credit for the first call's new key-credit-job input. A credit-stopped key makes the call hand off before any model turn, and broker-check records the skipped key leg.
    • Permission change: broker-check holds actions: read, so all 21 read-runner caller jobs now grant it. The run job keeps its contents and pull-requests read pin.
    • The marker comes from a jobs-API read, never from a run-job output. The run job's fixed-name Mark a key credit stop step runs only on run-claude-cli's handoff-reason. handoff-evidence.sh reads that step as a new credit-stop fact, and each first call's handoff job uploads key-credit-stop-<file>-<activity> when the fact is true.
  • maintenance-audit-lane-runs.yml:
    • collect reads the key's credit history. A skipped key skips judge.
    • judge-handoffs then routes every unit to judge-subscription as a credit-stop handoff (handoff.mjs route-skipped). After a key leg's credit stop it uploads the marker.
  • intake-triage, in sequence:
    1. fix(runner-policy): admit actions: read on intake-triage.yml callers standards#949 approves actions: read in the runner-policy contract and syncs here.
    2. A follow-up then has the caller job grant actions: read. It is not in this PR: runner-policy refuses that grant until the contract allows it.
    3. Last, the reusable's gate lands: ci(intake-triage): skip a credit-stopped key with the check-key-credit gate ci-workflows#996, held with do-not-merge.
  • The credit-stop markers come from one key-credit-markers job per caller file. No job needs that job, so its step-level jobs-API read never delays a handoff. Each static marker name has its own row in .github/artifact-retention.json.

Verification

  • node --test over .github/actions/resolve-config, .github/scripts/{pr-unblock,pr-verify,work-resolve-issue,maintenance-audit-lane-runs,upstream-apply-changelog} and .github/actions/log-api-budget: 3,478 tests, 3,478 pass (plus .github/scripts/intake-triage: 2,128 with resolve-config).
  • New cases:
    • Read runner: an empty subscription token with fallback-ready hands off. Without fallback-ready it hands nothing on and the call still fails.
    • pr-verify: an empty token admits the fallback job. With every token empty it does not.
    • The audit lane accepts a no-key subscription marker.
  • zizmor --offline: no findings. actionlint: only the existing cache-mode/queue keys.

Related

Draft: the dispatching session reviews and marks it ready

🤖 Generated with Claude Code

…ery lane

The lanes that already ran three legs stopped when their subscription
token was empty, even with FALLBACK_CLAUDE_CODE_OAUTH_TOKEN set. They now
follow the rule #13590 set: an absent subscription credential hands to
the fallback leg, which starts only when the fallback token is set, and
the fallback leg's lane record names fallback reason no-key.

- pr-run-activity-read.yml: the second call's handed-off output is true
  on an empty token when the caller runs a third call (fallback-ready),
  so the caller's subscription-handoff job starts the fallback call;
  the call ends green instead of failing. This covers every read-runner
  activity (resolve-conflicts, address-feedback, review-dependency,
  fix-ci, fix-ci-rung-up, fix-docs, resolve-issue).
- pr-verify.yml: check-intent-fallback also runs on the subscription
  job's empty token with the fallback token set.
- maintenance-audit-lane-runs.yml: judge-subscription records a no-key
  marker for an empty token with the fallback token set.
- pr-review-main.yml already followed the rule; unchanged.

Refs: #13586

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… every leg

The read runner's reuse summary had a key leg and a subscription leg, so
on a third call it ran on the empty subscription secret. It now runs on
the fallback token there, and writes a lane-outcome record for the leg
it ran. upstream-apply-changelog's scrub-probe legs record theirs too.
A test now fails any run-claude-cli job with no lane record (lane-id),
so every leg's credential, outcome, handoff and durationMs reach lane
telemetry; the maintenance role's propose pass is the one listed
exception, open on #13586.

Refs: #13586

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton kyle-sexton changed the title ci(lanes): give every three-leg lane the key-credit gate and the empty-subscription handoff ci(lanes): hand an empty subscription token to the fallback leg in every three-leg lane Nov 5, 2026
kyle-sexton and others added 3 commits November 5, 2026 16:30
…sweep

The read runner's broker-check, which runs no head code, now runs
ci-workflows check-key-credit for the first call's key-credit-job. A
credit stop inside KEY_CREDIT_WINDOW_HOURS skips the key: the call hands
off before any model turn and the caller's next call runs on the
subscription. broker-check grants actions: read, so every read-runner
caller job does too; the run job keeps contents and pull-requests read.

The marker comes from a jobs-API read, never a run job output: the run
job's fixed-name "Mark a key credit stop" step runs only on
run-claude-cli's handoff-reason, handoff-evidence.sh reads it as the new
credit-stop fact, and each first call's handoff job uploads
key-credit-stop-<file>-<activity> on it.

maintenance-audit-lane-runs.yml gets the same gate: collect reads the
key's credit history, a skipped key skips judge, judge-handoffs routes
every unit to judge-subscription as a credit-stop handoff
(handoff.mjs route-skipped), and uploads the marker after a key leg's
credit stop.

Refs: #13586

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…redit gate

The ci-workflows intake-triage.yml reusable is getting the same
check-key-credit gate the review reusable runs, which reads the
repository's credit-stop markers with actions: read. A called workflow
can hold only what its caller grants, so this caller grants it first;
the reusable's gate lands after this merges.

Refs: #13586

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton kyle-sexton changed the title ci(lanes): hand an empty subscription token to the fallback leg in every three-leg lane ci(lanes): give every three-leg lane the key-credit gate and the empty-subscription handoff Nov 5, 2026
@kyle-sexton
kyle-sexton marked this pull request as ready for review November 5, 2026 21:43
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-05T21:46:11.196918Z 55a86d9 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 9bfdf140741453813c16bf73d0d3ddb19d904ec2: read 16 of 16 changed file(s); 0 finding(s), 0 as inline comments.

Security review: PR #13608 (head 9bfdf14)

No security issues found. I read every hunk of all 16 changed files in .claude-lane/pr.diff. This lane already reviewed an earlier head (55a86d9), so this review reports only CRITICAL and IMPORTANT findings (blocking only: yes). No plugin manifests changed. No standing instruction is deleted or softened.

critical-findings: 0

Changes since the last review

1. The new key-credit-markers jobs in pr-unblock.yml, pr-refine.yml and work-resolve-issue.yml, for example pr-unblock.yml#L4664-L4860:

  • Token and secrets: each job gets only permissions: { actions: read, contents: read } and no secret, and its one token is github.token.
  • Code it runs: a sparse .base checkout of the default branch tip, pinned by a SHA that ls-remote resolves and the job checks against ^[0-9a-f]{40}$, with persist-credentials: false. It checks out only handoff-evidence.sh and log-api-budget.
  • Inputs: it reads no output from any call (CALLS is a literal). The marker file holds only $GITHUB_RUN_ID.
  • No head or model code reaches it, and nothing waits on it. It cannot widen a token or reach a credential.

2. The new credit-stop fact in handoff-evidence.sh (L351-L354):

  • It is a fixed member of the case allowlist (L346).
  • It reads a step name the base workflow sets (pr-run-activity-read.yml#L1769-L1771).
  • A forged credit stop can only move later runs from the API key to the subscription leg. That leaks nothing, compromises nothing and adds no charge.

Rechecked from the last review

1. The actions: read grants

  • broker-check holds the scope (pr-run-activity-read.yml#L442-L446), and the read-runner caller jobs grant it so the reusable workflow can request it.
  • The run job keeps contents: read, pull-requests: read. No job that runs model or head code gains a scope.

2. The empty subscription token now hands off to the fallback leg

  • Read runner handed-off: gated on token-present == 'false' && inputs.fallback-ready == 'true' (L1011).
  • pr-verify check-intent-fallback: gated on credential-present / fallback-ready (pr-verify.yml#L4066-L4069).
  • Audit lane's Record the handoff: same gate (maintenance-audit-lane-runs.yml#L779).
  • All three come from presence checks that run before any model turn, so head code cannot forge them.
  • The fallback leg runs the same activity on the same inputs it already ran after a subscription handoff. No new untrusted text reaches a credentialed leg.
  • pr-verify verdict: Fail without a verdict now steps aside only for that handoff. The fallback job must still produce the verdict, so no merge hold is weakened.

3. The audit lane's skipped-key path

  • route-skipped validates every unit against UNIT = /^[acd][1-8]$/ through marker().
  • UNITS reaches the script through env, not interpolated into the shell.
  • no-key is added to the subscription leg's closed reason set.

4. The summary-fallback step

  • It has the same tool limits as the other summary legs: tools: Read, Read(./.summary/**), with .base/** and /proc/** denied.
  • It runs only when inputs.fallback == 'fallback', and receives the same data the third call already gets.

5. Trust controls: none changed. Nothing touches the trusted-trigger check, the lane token request or broker, the kill switch, or any merge-hold script.

Notes

  • The resolved thread on pr-unblock.yml (the handoff delay) is settled (lines changed since resolution: no). The delay it raised is fixed by the separate key-credit-markers job.
  • The PR description mentions an intake-triage.yml actions: read grant, but that file is not in this diff (the merge of main brought its change in). Nothing to review.
  • Spend: an empty subscription token can now start a third call on FALLBACK_CLAUDE_CODE_OAUTH_TOKEN. That is subscription usage only, not a hold.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-review: 1 inline finding(s) on 55a86d968022de1f2d0a8577901764ad5b3cc8f3; the summary is in this lane's pull request comment.

claude-lane: claude-review

Comment thread .github/workflows/pr-unblock.yml Resolved
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed 9bfdf140741453813c16bf73d0d3ddb19d904ec2: read 16 of 16 changed file(s); 0 finding(s), 0 as inline comments.

Code review: #13608, ci(lanes): give every three-leg lane the key-credit gate and the empty-subscription handoff

No blocking findings. This lane already reviewed an earlier head (55a86d96), so this pass reports blocking findings only and leaves out advisory ones. I read every hunk of all 16 changed files in .claude-lane/pr.diff. I also read the unchanged code the changed conditions depend on, listed below.

Earlier finding is fixed. My earlier advisory finding said the credit-stop read in each *-handoff job slowed the subscription call. The owner replied with the fix and resolved the thread. The read now lives in a separate key-credit-markers job in pr-unblock.yml, pr-refine.yml and work-resolve-issue.yml, and no job has it in its needs. Its read uses HANDOFF_EVIDENCE_LAG: notice and every read step has continue-on-error. The *-handoff jobs hold no credit-stop step any more. callers.test.mjs asserts all of this, but I could not run it here.

What checks out

  • pr-verify check-intent-fallback. The new credential-present / fallback-ready outputs come from the subscription job's Check the credential step (pr-verify.yml#L2375-L2377). That step only runs once agree has passed, so a run that never passed the gate cannot start the fallback leg. The parenthesised OR keeps TRIGGER_GATE on both branches. Fail without a verdict is skipped on the same condition.
  • Read runner. The credit step in broker-check runs only on the first call (inputs.fallback == '') and only after the broker allowed the call. When it says skip-key, the run job's key-present step reports present=false, so the existing empty-key handoff takes over. The run job already needs: broker-check (pr-run-activity-read.yml#L660-L661). The new handed-off term needs inputs.fallback-ready, which only the second call passes, so the first and third calls are unaffected.
  • Read runner permissions. All 21 caller jobs of the read runner now grant actions: read, which broker-check's new grant requires. apply-docs-patch does not call the read runner and correctly gets no new grant.
  • Audit lane. collect already holds actions: read (maintenance-audit-lane-runs.yml#L64), which check-key-credit needs. When the key is skipped, download and route are skipped and route-skipped fills the output matrix. When the subscription token is empty, the leg records a no-key marker, and REASONS.subscription now accepts no-key. The fallback leg's fallback-reason names no-key.
  • Artifact retention. scripts/check-artifact-retention.mjs matches a literal upload name to its exact family key. So the eight new exact key-credit-stop-<file>-<job> rows each claim their own upload and do not collide with key-credit-stop-*.

Closing issue. The PR says Refs: #13586 and closes no issue. The PR context file has no closing-issue section, so there were no acceptance criteria to check.

PR body vs. diff. The body says the intake-triage.yml caller job "now grants actions: read", but that file is not in this diff. The grant is already on the head (intake-triage.yml#L766), most likely brought in by the merge from main, so the end state matches the body.

Verification. The test counts and the zizmor / actionlint results in the PR body are the author's claims. I could not run anything here, so they are unverified. Two things depend on ci-workflows code outside this repository, so I could not check them: the marker name key-credit-stop-<caller file>-<key job> matching what check-key-credit gate.cjs looks up, and the skip-key output.

Spend, not a hold. An empty role subscription token now starts the fallback leg on FALLBACK_CLAUDE_CODE_OAUTH_TOKEN in the read runner, pr-verify and the audit lane. In the read runner's third call, the reuse summary also runs on that token. All of this draws on subscription usage only.

kyle-sexton and others added 3 commits November 5, 2026 16:57
The handoff jobs' credit-stop read waits for the jobs listing's step
lists, which can lag for minutes, and held up every subscription call
behind it. Each caller file now reads it in one key-credit-markers job
that no job needs, one read and marker per first call.

- .github/artifact-retention.json: an exact row per static marker name
  (a `*` family never takes a static name), scan none.
- write-runner-never-rules.test.sh: the reuse summary's third leg.
- handoff-evidence.test.mjs: the key-credit-markers jobs also name
  HANDOFF_EVIDENCE_LAG.
- intake-triage.yml: the caller's actions: read waits for the
  runner-policy contract to approve it in melodic-software/standards.

Refs: #13586

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	.github/actions/resolve-config/callers.test.mjs
@kyle-sexton
kyle-sexton added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit a3dc5ef Nov 5, 2026
77 checks passed
@kyle-sexton
kyle-sexton deleted the ci/uniform-lane-legs branch November 5, 2026 22:20
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Post-merge mutation testing

Killed 6, survived 1, timeout 0, no coverage 0, invalid 0, not run 0.
Covered-code score 86%; mutation score 86%.

File Killed Survived Timeout No coverage Invalid Not run
.github/scripts/lib/handoff-evidence.sh 4 0 0 0 0 0
.github/scripts/maintenance-audit-lane-runs/handoff.mjs 2 1 0 0 0 0

Surviving mutants

Location Operator Disposition Cause
.github/scripts/maintenance-audit-lane-runs/handoff.mjs:99 ROR productive input-gap

The run's summary has the full report.

kyle-sexton added a commit to melodic-software/standards that referenced this pull request Nov 5, 2026
…949)

Refs: melodic-software/claude-code-plugins#13586

## Summary

This adds `actions: read` to the reviewed `allowedCallerPermissions` of
the `intake-triage.yml` contract in
`components/runner-policy/policy.json`. After the change, a caller job
that grants `actions: read` passes `runner-policy`.

## Fix

-
[melodic-software/ci-workflows#996](melodic-software/ci-workflows#996)
adds the `check-key-credit` gate to `intake-triage.yml`;
`pr-review-cli.yml` already has it. The gate lists the repository's
credit-stop markers with `actions: read`. A called workflow holds only
the permissions its caller job grants, so every caller job must grant
`actions: read`.
- Until this contract lists that permission, `runner-policy` rejects the
claude-code-plugins caller: "reusable workflow caller permissions has
unapproved properties". That rejection is the lint-repo failure on
[melodic-software/claude-code-plugins#13608](melodic-software/claude-code-plugins#13608).
- `actions: read` adds no write access and no token beyond the job's own
`GITHUB_TOKEN`.
- Callers float on `@main`, so `approvedShas` stays as it is.

## Verification

- `node --test components/runner-policy/runner-policy.test.mjs`: 226
tests, 226 pass.

## Related

Merge order:
1. This PR, then sync it to the callers.
2. The claude-code-plugins caller grants `actions: read`. That change
was taken out of #13608 until this PR syncs, and goes back in
afterwards.
3. ci-workflows#996, held with `do-not-merge` until step 2 lands.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant