Skip to content

feat(ci): Claude credential switch (subscription/oidc/api) + per-implementation daily regen - #12487

Merged
MarkusNeusinger merged 7 commits into
mainfrom
feat/ci-claude-auth-switch
Oct 31, 2026
Merged

MarkusNeusinger merged 7 commits into
mainfrom
feat/ci-claude-auth-switch

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

Summary

  • CLAUDE_AUTH switch for all 13 Claude steps in 8 workflows. Unset/subscription = today (CLAUDE_CODE_OAUTH_TOKEN); oidc = GitHub OIDC workload identity federation into the Console workspace ci-regen (the four CLAUDE_WIF_* repo variables are already set; the Console rule accepts only runs on main), so the nightly regen runs on the subscriber API credits instead of the owner's interactive quota; api = ANTHROPIC_API_KEY as a fallback. A "Resolve Claude credentials" step per job decides; each Claude step passes exactly one credential; bad values fail loudly, never a silent fallback. Under oidc, runs federation would refuse (PR review events, other refs, review-retest) use the subscription and say so.
  • Cost and clean stops. After every Claude step a summary line with total_cost_usd, model and auth mode. Spend-limit / spend-cap / credit / subscription usage-limit errors (and, under api/oidc, refused credentials) are infrastructure stops: no retry, no counted attempt, never impl:<lib>:failed; reviews and repairs pause with ai-review-paused / ai-repair-paused (no score from a cut session), the watchdog skips paused PRs, one comment per pause.
  • REGEN_PAIRS_PER_DAY: daily-regen picks the N oldest implementations (spec × library) instead of whole specs; no re-pick loop (last real attempt within a 30-day window counts), paused PRs resume first, the pick fails closed. Unset = the old spec mode. daily-regen.yml stays disabled.
  • Default behaviour: with the variables unset nothing changes except the extra summary lines and one deliberate improvement: a subscription usage limit now parks pairs instead of burning their three attempts.

Plan

docs/workflows/overview.md ("Claude credentials", "When Claude stops a session", "Daily regeneration modes"); the Console setup was done by the owner on 2026-10-31.

Test plan

  • tests/unit (23,146 passed) incl. tests/unit/workflows/test_claude_auth.py (pins all 13 sites' credential expressions for the three modes, the resolve/summary scripts run under bash with fake gh, the stop/park/pause paths, id-token unchanged per job) and test_regen_pick_pairs.py; ruff, mypy, changelog check
  • actionlint 1.7.7: no new findings vs main (148 pre-existing, all shellcheck info/style + one events note)
  • Second opinion before the PR: Fable and Gemini 4 Argon, both "sound with changes", both found no default-behaviour change; their findings (scored-then-stopped review, paused-PR handling, auth errors, oidc routing, exposure docs, fail-closed pick) are fixed in 982a18e / 3995768
  • After the merge, CLAUDE_AUTH unset: one ordinary regen pair (bulk-generate.yml -f specification_id=<spec> -f library=<lib>) behaves as before, summary shows auth=subscription with a cost
  • CLAUDE_AUTH=oidc: one pair, Console usage lands in ci-regen
  • Spend-limit test (docs "Test the spend limit"): limit = spend + ~0.50 USD, the Opus review pauses cleanly, resume works; then raise the limit
  • Enable daily-regen with REGEN_PAIRS_PER_DAY=2

claude-code-action refuses to run on a PR that changes Claude-running workflows, so the live paths are verified by the post-merge steps above.

https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8

MarkusNeusinger and others added 5 commits October 31, 2026 16:00
…ode daily regen

- CLAUDE_AUTH (repository variable): unset or `subscription` keeps every
  claude-code-action step on CLAUDE_CODE_OAUTH_TOKEN; `api` moves it to
  ANTHROPIC_API_KEY. Each of the 13 steps passes exactly one non-empty
  credential. A "Check Claude credentials" step opens every Claude job and
  stops on an unknown value or on `api` without the key, never falling back
  to the subscription.
- "Claude cost to the job summary" after every Claude step: total_cost_usd,
  model and credential from the execution file, never fatal.
  collect_week.py --costs reads it, counts daily-regen too and splits out
  the API part.
- daily-regen pair mode: REGEN_PAIRS_PER_DAY on the schedule, or the new
  `pairs` input, picks the N oldest (spec, library) pairs
  (automation/scripts/regen_pick_pairs.py), skips the spec polish and
  dispatches each pair straight to impl-generate with bulk-generate's
  arguments and pace. The watchdog's cron rescue forwards the variable.
  Unset, the schedule runs spec mode as before.
- tests/unit/workflows/test_claude_auth.py pins the wiring and runs the
  check, cost, mode and dispatch scripts; docs and changelog fragment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
…-pick loop

- CLAUDE_AUTH=oidc: workload identity federation through the pinned
  action's native inputs, with the CLAUDE_WIF_RULE_ID, _ORG_ID,
  _SERVICE_ACCOUNT_ID and _WORKSPACE_ID variables. "Resolve Claude
  credentials" (id claude_auth) replaces the check step and hands the mode
  and the federation IDs to every Claude step as outputs, so a new mode
  changes one step; it stops on a missing variable, a non-ID value or a job
  without id-token. No job gains id-token; review-retest stays without it.
- "Claude session summary" (id <step>_cost) also reads, for api/oidc only,
  the API's spend-limit and credit messages and Claude Code's billing_error
  from the session's error fields. The retry is skipped, "Stop at the Claude
  spend limit" fails the job, impl-generate parks the pair (labels restored,
  own marker, no retry, no counted attempt, never :failed), impl-review and
  impl-repair hand the PR on without spending their auto-retry, and a
  daily-regen pre-flight at the limit skips that spec's dispatch.
  Subscription runs never take the new branches.
- Pair mode: a pair's real attempts are its merged or closed implementation
  PRs and its failed generations (impl-fail comments without the infra tag,
  read lazily per spec issue); a pair touched within the attempt window
  (attempt_window_days, REGEN_ATTEMPT_WINDOW_DAYS, default 30) is skipped.
  Infrastructure failures and spend-limit stops are no attempt.
- collect_week counts auth=oidc as API credits; docs, tests, changelog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
…ure stops in every mode

- "Claude session summary" now detects the limit in all three modes, and
  only in the error fields of a session that failed (the last result is
  missing or is_error): the API's spend-limit (400 "specified [workspace]
  API usage limits"), tier spend-cap (429 enforced_spend_limit_reached) and
  credit messages, Claude Code's billing_error and "Credit balance (is) too
  low", and its subscription usage-limit messages (the SDK's
  USAGE_LIMIT_ERROR_PREFIXES, plus the older "usage limit reached"). A
  transient rate limit keeps today's retry and infra path.
- Output limit_reached, ::error:: and the summary line "Claude spend/usage
  limit reached (auth=<mode>)"; the retry is skipped; impl-generate parks
  the pair before any attempt counting, for first implementations too
  (never impl:<lib>:failed); impl-review and impl-repair hand the PR on
  without spending their auto-retry; the pause comments carry their own
  *-paused-claude-limit markers, so the pair-mode pick never counts them.
- daily-regen: each spec's similarity audit runs before its pairs are
  dispatched, so once a pair's chain reaches the limit, later specs of the
  night stop at their audit; pairs already dispatched fail fast and park.
- Tests: fixtures with the API's and Claude Code's exact texts in every
  mode, and the negatives (transient rate limit, usage warning, quoted text,
  a session that recovered). Docs and changelog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
…ntials

Round-3 review fixes for the Claude credential switch:

- impl-review: a session Claude stopped gets no score, verdict or gate
  decision even when it wrote quality_score.txt first ("Extract quality
  score" is skipped on the stop).
- Stopped reviews and repairs get their own labels, ai-review-paused and
  ai-repair-paused, instead of ai-review-failed; the watchdog skips paused
  PRs, daily-regen's pair mode re-dispatches them first (oldest first,
  counted against N), and the resumed run removes the label.
- With api or oidc, refused credentials (401/403, authentication_error,
  permission_error, "Authentication failed", "invalid x-api-key") stop the
  session like a limit: no retry, no counted attempt, never :failed. The
  subscription keeps its handling. Outputs are now stopped/stop_reason.
- oidc routes the runs federation refuses (pull-request events, other
  refs, jobs without id-token such as review-retest) to the subscription
  with a notice, and fails fast when the OAuth token is missing.
- The three non-Claude steps that run model-written code in id-token jobs
  unset ACTIONS_ID_TOKEN_REQUEST_URL/TOKEN first.
- regen_pick_pairs.py exits 1 when the implementation PR listing fails and
  leaves out a spec whose failure comments can't be read.
- Docs: "When Claude stops a session", manual resume, the spend-limit test
  on day one of oidc before REGEN_PAIRS_PER_DAY, and the federation
  exposure (disable the Console rule when oidc is off, job_workflow_ref
  pinning unverified).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
While a Claude stop lasts, every pair-mode night re-dispatches the paused
reviews and repairs and each pauses again. impl-review and impl-repair now
re-add the pause label but skip the comment when the PR's latest bot
comment already starts with the same pause marker (spec, library, and for
a repair the attempt). A failed read posts the comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
Copilot AI lite review requested due to automatic review settings October 31, 2026 16:32

Copilot AI 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.

🟡 Changes recommended

Active moderate and critical findings remain: the implementation-branch listing can miss existing PRs, and invalid exception syntax prevents the pair-picker module and its tests from importing.

2 open issues
What changed in this PR

This pull request adds configurable Claude credential routing, session-cost reporting, clean handling of spend and authentication stops, and per-implementation daily regeneration across the CI workflows.

Changes:

  • Adds CLAUDE_AUTH support for subscription, OIDC, and API-key authentication.
  • Pauses or parks work when Claude reaches usage, spend, credit, or credential limits.
  • Adds REGEN_PAIRS_PER_DAY pair-mode regeneration with paused-work resumption and attempt tracking.
  • Updates documentation, automation scripts, workflows, and unit tests.
File Description
tests/​unit/​workflows/​test_regen_gate_workflows.py Updates regeneration gate expectations for Claude-stop paths.
tests/​unit/​workflows/​test_impl_generate_adopt.py Verifies stopped Claude sessions skip adopt-mode retries.
tests/​unit/​skills/​test_collect_week.py Tests credential-aware session-cost aggregation.
tests/​unit/​automation/​scripts/​test_regen_pick_pairs.py Tests pair discovery, ordering, attempt windows, pauses, and fail-closed behavior.
prompts/​workflow-prompts/​README.md Documents credential inputs for Claude workflow actions.
prompts/​README.md Updates the general Claude workflow wiring example.
docs/​workflows/​overview.md Documents credential modes, stop handling, pair-mode regeneration, and operational procedures.
changelog.d/​ci-claude-auth-switch.md Adds the changelog fragment for the feature.
automation/​scripts/​regen_pick_pairs.py Implements pair selection, attempt tracking, and paused-work resumption.
automation/​scripts/​README.md Documents the pair-picker script and its interface.
agentic/​docs/​project-guide.md Updates the workflow architecture guide with credentials and pair-mode behavior.
.github/​workflows/​watchdog-stuck-jobs.yml Skips paused PRs and forwards pair-mode settings during cron rescue.
.github/​workflows/​util-claude.yml Adds credential resolution, cost reporting, and stop handling.
.github/​workflows/​spec-create.yml Adds configurable credentials, cost reporting, and stopped-session handling.
.github/​workflows/​review-retest.yml Adds credential resolution and cost reporting for retest sessions.
.github/​workflows/​report-validate.yml Adds configurable credentials and session-stop reporting.
.github/​workflows/​impl-review.yml Adds credential routing, cost reporting, and paused-review handling.
.github/​workflows/​impl-repair.yml Adds credential routing, cost reporting, and paused-repair handling.
.github/​workflows/​impl-generate.yml Adds credential routing, cost reporting, and generation parking on Claude stops.
.github/​workflows/​daily-regen.yml Adds pair-mode selection, paused-work resumption, and per-pair dispatching.
.claude/​skills/​review-regen-week/​SKILL.md Updates weekly regeneration-review guidance for pair mode and API costs.
.claude/​skills/​review-regen-week/​collect_week.py Aggregates session costs by credential mode, including daily regeneration jobs.

Review effort: Lite

Comment thread automation/scripts/regen_pick_pairs.py Resolved
Comment thread automation/scripts/regen_pick_pairs.py Resolved
@codecov

codecov Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

The main() tests ran the picker as a subprocess, which coverage doesn't
see. In-process tests with a fake runner now cover the gh listing
wrappers, read_records and its fail-closed implementation-PR listing,
issue_failures, the paused-label parsing edges, summary_table, argument
checks and the GITHUB_OUTPUT and step-summary writing. No behaviour
change; regen_pick_pairs.py is at 100 % line and branch coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
@MarkusNeusinger
MarkusNeusinger merged commit 523199b into main Oct 31, 2026
16 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the feat/ci-claude-auth-switch branch October 31, 2026 19:41
MarkusNeusinger added a commit that referenced this pull request Oct 31, 2026
)

## Summary
- **Watcher scripts end on their own.** `watch-deploys.sh` exits 0 with
`no deploy for <sha>` after 6 empty polls (157 s measured on 523199b,
which triggered no deploy), now passes `--project=anyplot` (the project
guide's rule; a wrong default project would otherwise read as "no
deploy"), and exits 2 at once when `gcloud builds list` fails.
`watch-checks.sh` waits 90 polls (about 45 minutes) and its `TIMEOUT`
line names the checks still pending. Both caps are stated in `open-pr`
sections 3a and 4.
- **Rules and skills from the retro's findings** (owner-approved list,
items A1 to A14 and B15 to B20): `CLAUDE.md` gets the delegate stall
check with a post-compaction re-check, a classifier denial that is final
for every agent of the session, the first Copilot review at about 5
minutes, and merges only on a named OK (mirrored in
`copilot-instructions.md`); `open-pr` gets a hadolint row (CI's 2.15.1,
threshold `warning`), tests for new code before the PR opens (the
codecov triage folded into section 1), the 16 real check names, the
GraphQL `requestReviews` mechanic, the nvm prefix for site-smoke, a
`pull --ff-only` of the shared checkout and a once-per-session worktree
report; `zweitmeinung` gets the 60 KB diff rule, the `--attach` path
gotcha, ledger-measured costs and the root-owned-files cleanup;
`verify-core` the system-`python3` gotcha; `guardrails.md` the
delegation, denial, Console-wizard and stalled-delegate sections.
- **Recipes that lived only in chat:** the single-library
`impl-generate.yml` dispatch with its optional inputs
(`docs/workflows/overview.md`), the eval-matrix forms real runs used
(`agents/README.md`), and the describe, edit, import route for a
second-generation Cloud Build trigger (`agentic/docs/project-guide.md`).
Every gate command that runs ruff now names the `dev` extra.

Decisions taken within the brief:
- The trigger route went into the project guide's deployment section,
next to the existing "Checking whether something actually deployed";
`infra/` holds only Cloudflare.
- The hadolint row covers all five Dockerfiles CI lints, not only the
renderer and agents ones. "Must print nothing" became "no `warning` or
`error` line": `agents/Dockerfile` and `toolchain.Dockerfile` print
`info` lines (DL3059, DL3066) on main today, below CI's threshold.
- `--extra dev` went into every skill, command and doc that runs ruff
(also `docs/development.md`, `project-guide.md`,
`copilot-instructions.md`, the audit and test commands); historical
plans under `agentic/specs/` and `agentic/audits/` and the CI workflow
(which syncs `--extra dev`) stay as they are.
- The classifier-denial rule has no mirror in `copilot-instructions.md`:
the classifier is a Claude Code harness mechanism, and that file carries
no external-writes rule to extend.
- The Gemini cost lines in `guardrails.md` were updated with the same
ledger figures, so the skill and the guardrails agree.
- `agents/README.md` said `--thinking-budget` takes 0 to 4,096;
`matrix.py` caps it at 8,192 (`MAX_THINKING_BUDGET`). Fixed while there.

## Plan
N/A. The source is the retro of session 402df83f (2026-10-28 to
2026-10-31), sections 3, 4 and 6: ranked findings, proposed changes, and
the first-try accuracy table of documented commands.

## Test plan
- [x] `watch-deploys.sh 523199b` (a `.github/workflows/` merge): `no
deploy for 523199b`, exit 0 after 157 s; `f356a57b1` the same;
`e9b11e373` (two deploys): `ALL DEPLOYS FINISHED for e9b11e3, 0 not
SUCCESS`, exit 0 after 32 s
- [x] `watch-checks.sh 12487`: exits at once, `ALL 16 CHECKS FINISHED on
3a6d00a, 0 not green`; the TIMEOUT name parsing checked on sample
lines
- [x] The worktree report and the `pull --ff-only` guard run read-only
against the shared checkout; the `git diff` exclude pathspec and the
hadolint command run locally (all five Dockerfiles exit 0)
- [x] `RequestReviewsInput` lists `botIds` (GraphQL introspection); the
Copilot bot id `BOT_kgDOCnlnWA` read from #12487's review
- [x] `uv run --extra dev ruff check . && uv run --extra dev ruff format
--check .`; `uv run python -m tools.changelog check --base origin/main`
- [x] Gemini 3.8 Flash pre-PR check ($0.38): 6 findings, 1 real
(detached worktrees missing from the report) fixed in 39f906e;
`botIds` and the hadolint entrypoint were false positives (verified),
the rest pre-existing or no false result

https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8

---------

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.

2 participants