Skip to content

feat(report-lane-outcome): record why a fallback credential ran - #834

Merged
kyle-sexton merged 1 commit into
mainfrom
fix/806-fallback-reason
Oct 20, 2026
Merged

kyle-sexton merged 1 commit into
mainfrom
fix/806-fallback-reason

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

Closes #806

Summary

When intake-triage (or any caller of the two-job pattern) falls back to the subscription token, the run's only lane record comes from the subscription job and does not say why it ran. This adds a machine-readable fallback=<reason> token so a credit stop on a role's API key shows in the lane record.

Fix

  • report-lane-outcome: new optional fallback-reason input, credit-stop or no-key. recordTokens prints fallback=<reason> right after credential=, only for a value in FALLBACK_REASONS. The step script fails (core.setFailed, naming the allowed values, before any annotation or output) on any other non-empty value. The value reaches the script through env only.
  • intake-triage.yml: the subscription job's "Report triage outcome" step passes fallback-reason: ${{ needs.claude.outputs.lane-class == 'credit-stop' && 'credit-stop' || needs.claude.outputs.credential-present == 'false' && 'no-key' || '' }}. That is a with: input on a uses: step, so the subscription job still writes no needs. value into a run: or script:.
  • intake-triage-legs.test.cjs: the parity test strips exactly this one input from the subscription job (after asserting its exact text), and a new test evaluates it: credit-stop gives credit-stop, an empty key gives no-key, anything else gives empty.
  • README: the run-record token list and the two-job pattern describe the input.

Out of scope, per the brief: an auth value; the claude-code-plugins follow-ups (pr-verify passing the input, the morning brief counting the token).

Verification

  • node --test .github/scripts/*.test.cjs .github/actions/*/*.test.cjs: 632 pass, 0 fail.
  • New tests were run red before the change: 3 in report-lane-outcome (record token order and vocabulary, notice and class line carry credential=... fallback=..., out-of-vocabulary value fails the step with no outputs), and 2 in the legs test (parity and expression evaluation) before the workflow edit.
  • Existing record tests are unchanged and pass, so records without the input are byte-identical.
  • actionlint, biome, typos and markdownlint-cli2 (README) are clean. No shell was touched.

Related

🤖 Generated with Claude Code

Add an optional fallback-reason input, credit-stop or no-key, printed as
fallback=<reason> right after credential= on the lane record. Any other
non-empty value fails the step before it reports. intake-triage's
subscription job passes it from the claude job's lane-class and
credential-present outputs, and the leg parity test allows that one
difference.

Closes #806

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 20, 2026 21:39
@claude

claude Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 53s —— View job


Security review

  • Gather context (PR diff, changed files)
  • Review report-lane-outcome action.yml + classify.cjs for new fallback-reason input handling
  • Review intake-triage.yml expression wiring
  • Check instruction-surface / trust-boundary concerns
  • Post findings

No security issues found.

Notes from the review (informational, not findings):

  • The new fallback-reason input reaches the github-script step only through env.FALLBACK_REASON (action.yml#L163), never interpolated into a shell run: block, so there's no expression/command-injection surface there.
  • intake-triage.yml's new line (intake-triage.yml#L1289) is a with: input on a uses: step, and the ternary only ever yields one of the two fixed literals 'credit-stop' / 'no-key' or '' regardless of what needs.claude.outputs.lane-class actually contains — so even an adversarial value there can't widen what reaches the composite action.
  • action.yml validates fallback-reason against the fixed FALLBACK_REASONS vocabulary and calls core.setFailed (with no annotation/output written) before any reporting logic runs on an out-of-vocabulary value, so a caller wiring mistake fails closed rather than silently fabricating a token.
  • classify.cjs's recordTokens independently gates on FALLBACK_REASONS.includes(...) before emitting the token, so even if the upstream check were ever bypassed, the printed record still can't carry an arbitrary string.
  • No instruction-surface deletions (CLAUDE.md/AGENTS.md/rules files/hooks) in this diff — purely additive.

claude-lane-summary: lane=claude-security-review sha=5daaf70d25b429429c2dcb8653bf81fe1e8b1abf run=39502188889

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 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-10-20T21:40:56.270806Z 5daaf70 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.

@claude

claude Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 41s —— View job


Code review complete

  • Gather context (diff, rule files, CLAUDE.md, README)
  • Review report-lane-outcome action.yml + classify.cjs changes
  • Review test coverage (classify.test.cjs, outcome-step.test.cjs)
  • Review intake-triage.yml wiring + legs test
  • Review README updates
  • Check PR body contract (pr-body-contract.md rule)
  • Post findings

Summary

No blocking or high-signal findings. This is a clean, well-tested change:

  • classify.cjs's FALLBACK_REASONS closed vocabulary and recordTokens ordering (credential= then fallback=) match the PR description and the updated action.yml/README prose exactly.
  • action.yml's validation (core.setFailed + return before touching execution-file or emitting any output/annotation) correctly rejects any value outside credit-stop/no-key, confirmed by reading the composite step script directly.
  • The new fallback-reason: wiring in intake-triage.yml (line 1289) reads needs.claude.outputs.* inside a with: input on a uses: step, consistent with this file's existing script-injection avoidance for the two-job pattern; the expression is only reachable when the subscription job's own if gate (needs.claude.outputs.lane-class == 'credit-stop' || needs.claude.outputs.credential-present == 'false') already holds, so the '' branch of the ternary is dead by construction — not a bug, just confirms the two expressions are intentionally kept in sync.
  • intake-triage-legs.test.cjs's new test reuses the existing actions-expression.cjs evaluator rather than inventing a new one, and its stripped-input parity test still asserts the exact wired text before deleting it.
  • Test additions in classify.test.cjs and outcome-step.test.cjs run the actual shipped script/function (not a reimplementation) and cover the forged-value rejection path (case variants, leading space, embedded ::error::), the success-notice and failure-annotation record lines, and the empty/no-fallback case.

Not independently verified: I could not execute node --test in this sandbox (command execution required approval that wasn't available), so the PR's "632 pass, 0 fail" and the red-then-green TDD claims are author-stated and unverified by me — I verified correctness by reading the shipped script/function logic directly instead.

Unread: no .claude-lane/pr-context.md was present, so issue #806's acceptance criteria were not available to check against; nothing closing-issue-shaped was verified beyond the PR body's own description.

PR body contract: compliant — opens with Closes #806, includes ## Summary, ## Fix, ## Verification, ## Related.

claude-lane-summary: lane=claude-review sha=5daaf70d25b429429c2dcb8653bf81fe1e8b1abf run=39502188889

@github-actions

Copy link
Copy Markdown

claude-security-review has reviewed this pull request through 5daaf70; a later push is reviewed from there.

@github-actions

Copy link
Copy Markdown

claude-review has reviewed this pull request through 5daaf70; a later push is reviewed from there.

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.

report-lane-outcome: record a key-to-subscription fallback as a lane-record token

1 participant