Skip to content

Add a weekly lane triage: the model proposes, a model-free job applies - #896

Closed
seathatflowsinourveins wants to merge 15 commits into
mainfrom
claude/api-actions-w4d-triage-20261008
Closed

seathatflowsinourveins wants to merge 15 commits into
mainfrom
claude/api-actions-w4d-triage-20261008

Conversation

@seathatflowsinourveins

@seathatflowsinourveins seathatflowsinourveins commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Scope

  • What this PR changes, in one or two sentences: adds claude-triage.yml, a weekly and on-demand lane triage of the open issues and pull requests that carry no lane label. A model job with no write scope proposes labels through a JSON schema; a model-free job with issues: write re-checks each issue and adds an allow-listed lane label only to an open issue that still has none; pull requests get suggestions only. It runs only while CLAUDE_TRIAGE_ENABLED is true, so merging this starts nothing.

  • Effort: --effort high in claude_args since R5 (2026-10-09). From head c956398 (2026-10-08) until then it was low, the level the command center's effort mapping gives classification, routing and short probes; it returns to low only if a parity check on triage items shows that low holds. The labels are reversible: the model-free apply job re-checks each one against a fixed allow-list of three labels (lane:foundation, lane:trading, lane:shared). The level has to be in claude_args, because --restricted ignores the settings files that would otherwise carry it; the exact-pin test requires it.

  • Fixes from the pre-cue read of the sibling Bound the harness audit and move it to claude-code-action v1.0.247 #892 (2026-10-09, head 53379c7): the record names every behaviour of this new workflow. That covers the triggers; the slug, owner, first-attempt and variable guards; the timeouts and grants of each job; the guard exit and collect limits; the explicit action inputs display_report and track_progress (defaults in the pinned action.yml); the 14-day usage artifact; and the apply job. The bounds step now uses an allow-list (Glob, Grep, Read and StructuredOutput), fails on a start record without lists or without a structured output, and names each unmet bound. The step tests no longer skip without PyYAML. The 2026-10-03 cooldown waiver is cited.

  • Pre-cue toolkit read of this PR (J8 on 53379c7, 2026-10-09; Anthropic's pr-review-toolkit pr-test-analyzer and silent-failure-hunter): every finding at confidence 80 or above, and its status at head 5d9b35d. 55da753 had touched none of the three.

    Finding Status at 5d9b35d Fixing commit and evidence
    pr-test-analyzer (85): the guard step has no test, and GUARD is referenced nowhere fixed 5d9b35d: test_the_guard_refuses_debug_logging_and_a_pre_existing_settings_file runs the step with each debug signal, a settings file and a dangling link to one
    silent-failure-hunter (88): in apply, an issue that cannot be read takes the else branch, is reported as skipped, and the step exits 0 fixed 5d9b35d: a read or label error is recorded in the summary, the other issues are still tried, and the step exits 1; test_an_issue_that_cannot_be_read_fails_the_step_after_the_others_are_tried, test_a_label_that_cannot_be_added_fails_the_step_after_the_others_are_tried (renamed at e1ba3bb)
    silent-failure-hunter (60-80, counted at 80): test gaps fixed 5d9b35d: the guard test above; the read-error and label-error paths; an omitted item (test_an_item_the_model_leaves_out_gets_no_label, recorded as deliberate); the numbers step's always() and the validation step's success(); the usage artifact; each numbers-step failure asserted by its message, the three unasserted ones included; the shape tests no longer skip without PyYAML (the fallback parser reads every condition they check); the stray blank line removed

    Ten weakened copies of the workflow each fail at least one test (listed in the record).

  • Changes at 55da753 that this description had not named until then (the record names them): a final classify step, "Require the run's execution file", fails the job when items were collected and the action step succeeded without an execution file (before, that case was green), with test_a_green_run_with_items_always_has_an_execution_file pinning its condition; test_the_action_is_pinned_federated_and_fenced now also asserts show_full_output, display_report and track_progress as 'false'; ci-least-privilege.md is edited in place (two write-grant rows, one per-workflow row, and the federation-exemption sentence now naming each exempt job) besides its addendum; and the record gains a Visibility section (on this public repository every run's summary, proposed labels and reasons are public).

  • The J8 micro read of the fix (53379c7..5d9b35d, both agents) found all three listed findings fixed, with no new finding at confidence 80 or above. Its two smaller findings are fixed at ace2512 (the command center ruled both fixed, quality first): apply reads jq's exit status and treats only 1 (a false check) as a skip, so a body jq cannot check is a failure that names the exit (5 or 4 on jq 1.8.1), never a skip; and the label-failure test now proposes a second issue that is still labelled. Five more weakened copies each fail at least one test.

  • R4, debug logging set as a repository secret or variable (2026-10-09; the cc-native-practice lane's L3 read against anthropics/claude-code-action v1.0.247 practice): the guard binds (secrets.ACTIONS_STEP_DEBUG || vars.ACTIONS_STEP_DEBUG) == 'true' and (secrets.ACTIONS_RUNNER_DEBUG || vars.ACTIONS_RUNNER_DEBUG) == 'true' as booleans and refuses either before any token is requested; a test pins both bindings, and six weakened copies each fail at least one test. This is hardening: the action step already pins ACTIONS_STEP_DEBUG: 'false', which is what the action's full-output switch reads. zizmor's auditor persona reports secrets-outside-env on the two reads; CI runs the regular persona with --no-ignores, which reports nothing. The R4 lines are byte-identical in Bound the harness audit and move it to claude-code-action v1.0.247 #892, Add an on-demand, read-only pull request review by the Claude action #894, Add an on-demand Claude security review and a model-free flag on risky paths #895, Add a weekly lane triage: the model proposes, a model-free job applies #896 and Add a pre-cue read by Anthropic's pr-review-toolkit agents in Actions #909.

  • The J8 micro read of 5d9b35d..e1ba3bb (micro2, both agents, 2026-10-09) found the three listed findings fixed and no finding at confidence 80 or above. R5 fixes the residuals it raised: the guard description, per-source tests of the R4 bindings, the stale "read or labelled" wording, the R4 section's pointer to weakened copies, and checks re-run at the new head. Its lower-confidence notes (one refusal message for every debug signal, a valid-JSON body that is not an issue, jq exit numbers per version, a test loop without subTest) are not taken in R5.

  • R5, quality and correctness (2026-10-09, head 710d061). The command center narrowed this round to quality and correctness; security hardening from the 2026-10-09 read (debug-value widening, tool-list shape, extra deny rules, token-source and guard-step assertions) is filed as follow-ups before any enabling variable is set.

    R5 (2026-10-09, quality and correctness only, on e1ba3bb): claude_args keeps --setting-sources user beside --restricted; measured below, the two do not conflict, and each backs the other up. It raises --effort low to --effort high (it returns to low only if a parity check on triage items shows low holds) and applies the command center's caps of 2026-10-09: --max-turns 6 → 8 and --max-budget-usd 1 → 2. Triage has no measured run, so the caps are an estimate: 30 items of up to about 2,200 characters are about 27,000 input tokens, and up to about 15,000 output tokens are assumed at high effort, about $0.41 a run at Claude Opus 5.5's standard prices. That is doubled, with a margin for high-effort thinking, to $2, and one read and one answer is a typical run, so 8 turns bound a runaway; both caps are to be replaced by about twice the p95 of the first measured runs. The numbers step now bounds 1 to 8 assistant turns and $2. It requires Claude Code 2.1.295, the version the pin installs (base-action/action.yml:150 and src/entrypoints/run.ts:80 at 2dca132f, read through the GitHub API on 2026-10-09; a 2.1.295 start record carries claude_code_version: "2.1.295", measured on a J8 headless stream the same day), and names another version, or none, as an unmet bound. It counts a tool-list entry that is not a string as a forbidden tool named non-string tool entry instead of filtering it out, and the usage record's tools field keeps such an entry, named other. The apply step refuses an empty proposal and builds its rows before the loop, so a jq failure stops the step. It refuses (exit 2) a row count that differs from the proposal's length. Both gh api calls read < /dev/null, so neither can take the loop's remaining rows. Its closing message and comment say "read, checked or labelled", and two new comment sentences name the rows and /dev/null changes. The guard step's comment now says that debug logging set as a repository secret or variable is bound and refused. R4's == 'true' bindings are unchanged and now tested per source as they are; widening them (follow-up F-01) stays a follow-up, as does the rest of the tool-list shape (F-02). Tests: eight new (test_claude_args_are_pinned_exactly, test_each_source_of_a_debug_setting_reaches_the_guard, test_a_gh_call_cannot_read_the_rows_the_loop_has_not_reached, test_the_rows_are_built_and_counted_before_the_loop, test_a_tool_entry_that_is_not_a_string_is_a_forbidden_tool, test_the_usage_record_lists_a_tool_entry_that_is_not_a_string_as_other, test_the_session_must_run_the_client_version_the_pin_installs, test_the_caps_hold_at_eight_turns_and_two_dollars_and_fail_just_above). Eight changed: the flag list moved into the exact pin, the whole --settings object is pinned, an empty-proposal case was added, the closing message is asserted, and the new caps are used. The test harness gains a gh stand-in that reads stdin, Run.run_text, and a github_expression helper that evaluates the binding the workflow holds. The module runs 35 tests (27 before), and 30 weakened copies each fail their named test. The record corrects "the repository's label set" to the fixed three-label allow-list and restates the effort, caps and bounds. It adds three "What would overturn it" triggers (caps re-derived from the first measured runs, effort back to low on a parity check, the settings-source runs repeated on a new pin) and dates the replay of the action's argument parser as R4. Its dated R5 section names every change, the settings-source measurements, the shared agent-mode origin URL sentence and the follow-ups line, and notes that the symbolic-link strip does not apply, since no pull request head is checked out.

    Settings sources, measured by the command center on the installed Claude Code 2.1.295 on 2026-10-09. Each run used a throwaway home directory, a marker model in the settings file under test, and a loopback stand-in for the API; the model in each request showed whether that file had loaded.

    • Project file (.claude/settings.json): loaded with no flags and with --setting-sources user,project,local alone. Not loaded with --restricted alone, --restricted --setting-sources user,project,local (the list the action passes when claude_args gives none), --restricted --setting-sources user, or --setting-sources user alone.
    • Local file (.claude/settings.local.json): the same as the project file. --restricted keeps it out under every source list, and --setting-sources user alone keeps it out.
    • User file (~/.claude/settings.json): --restricted keeps it out, even with --setting-sources user; --setting-sources user without --restricted loads it. Under this workflow's flags, a user settings file the action writes is not loaded.
    • A --settings JSON value: honoured under --restricted --setting-sources user, so the workflow's deny rules apply, as the 2026-10-09 deny probes also showed.

    So --restricted wins over any source list, and for the two repository files either flag holds if the other's behaviour changes. The flags are the same in all five PRs of this series.

  • Base commit: aba02ec3 (the merge base, unchanged at 710d061; main had moved to 1304004a (Record Codex native review trial and credential-store research #916) at the last local fetch, and only manifests/evidence.json overlaps; the one rebase happens at landing)

  • Lane: lane:foundation

  • Owned paths touched: .github/workflows/claude-triage.yml (new), tests/test_claude_triage_workflow.py (new), tests/test_workflow_policy.py (two inventory entries, one exemption), tests/test_workflow_security_coverage.py (coverage set and one zizmor test), docs/decisions/2026-10-08-claude-actions-triage.md (new), docs/decisions/2026-10-04-ci-least-privilege.md (two write-grant rows, one per-workflow row and the exemption sentence edited in place, and a dated addendum), docs/github-automation.md (current-practice bullet), manifests/evidence.json (registration, last commit). R5 touches the workflow, its test module, the record and the manifest only.

R6 (2026-10-09), at 5c74982

  • Cost bound, the stated exception: the series sets bounds at the budget × 1.10, but triage keeps $2. With --json-schema, the pinned action returns no structured proposal from a budget stop (base-action/src/run-claude-sdk.ts:252-277 at 2dca132f), so there is nothing to apply or publish. The usage record keeps result_subtype, and the failure names the stop.
  • --max-turns 8 removed: the pinned action (run-claude-sdk.ts:241-250, since 6ef6450f) fails a success whose num_turns exceeds it. On Claude Code 2.1.295 num_turns counts transcript messages: 57 for 12 requests under --max-turns 12. That is a code reading plus a local measurement, not a hosted run. The numbers step's 1-to-8 assistant-turn check stays.
  • Exact pins: both compare canonical JSON.
  • Record: the shared cost paragraph, the exception's reason, and an upstream-issue draft (not filed).
  • Local checks at 5c74982:
    • validate.py and the manifest check pass (10,904 files);
    • six modules, 254 tests, OK;
    • this module, 36 tests with and without PyYAML;
    • actionlint and zizmor are clean;
    • 36 of 36 weakened copies fail their named test.
  • Hosted checks at 5c74982: not read yet.

SOTA sources

Evidence-class table

Claim Evidence class Command / receipt
Structured output works under the read-only flag set, and a planted instruction in an issue body produced no entry for an uncollected number native_proven, one local run on the installed 2.1.295 client (subscription, Haiku; not through the action) run output kept outside the repository
The collect, numbers, validation and apply steps behave as described synthetic tests/test_claude_triage_workflow.py, 35 tests at 710d061 (27 at e1ba3bb), all of them run without PyYAML too; each step's shell is taken from the workflow file and executed against a local stand-in for gh
The tests detect a weakened workflow synthetic 13 mutated copies on 2026-10-08 (that day's bounds), 13 failed at least one test; on 2026-10-09 three turn-bound mutants each failed, and the new bounds have their own tests; at 5d9b35d ten more weakened copies each failed at least one test; at 710d061 30 weakened copies each failed the test named for it (a script outside the repository restores the workflow after each and checks its sha256)
--restricted keeps the user, project and local settings files out under any source list, --setting-sources user alone keeps the project and local files out, and --settings still applies local_integration the command center's runs of the installed Claude Code 2.1.295, 2026-10-09 (throwaway home, marker settings files, loopback stand-in for the API); receipts kept outside the repository
The pin installs Claude Code 2.1.295, and a session's start record reports it source_review, with one measured start record base-action/action.yml:150 and src/entrypoints/run.ts:80 at 2dca132f, read through the GitHub API on 2026-10-09; a J8 headless stream on 2.1.295, 2026-10-09
The workflow is well formed and has no audit finding local_integration at 710d061: actionlint 1.17.0 exit 0, run without ShellCheck (not installed on the host that ran the R5 checks; hosted validate runs it); zizmor 1.30.1 offline, pedantic and regular with --no-ignores, no findings (two auditor-only secrets-outside-env findings on the R4 secret reads suppressed by persona). Earlier heads: actionlint with ShellCheck 0.9.0, and at R4 the action's argument parser replayed on the workflow text, including the JSON schema (not repeated at R5)
The $2 budget and the 15-minute timeout bound a runaway, and the 8-assistant-turn check fails one afterwards, without trimming a normal run not established (estimate) about $0.41 a run estimated; no run measured; the caps are to be replaced by about twice the p95 of the first measured runs
A hosted run authenticates, stays in bounds and labels correctly not established no hosted run is part of this PR

Local commands run

$ python3 scripts/evidence_manifest.py --check            # at 710d0616 (its exact bytes, before the commit)
{"files": 10904, "status": "passed"}   exit 0
$ python3 scripts/validate.py
{"components": 70, "hashed_files": 10904, "profiles": 4, "receipts": 239, "status": "passed"}   exit 0
$ uv run --quiet --no-project --python 3.12 --with pyyaml==6.0.3 python -m unittest tests.test_claude_triage_workflow tests.test_workflow_policy tests.test_workflow_security_coverage tests.test_workflow_hardening tests.test_github_automation_practice tests.test_adoption_docs_consistency
Ran 253 tests ... OK (skipped=2)   exit 0
$ uv run --quiet --no-project --python 3.12 python -m unittest tests.test_claude_triage_workflow
Ran 35 tests ... OK   exit 0
$ actionlint .github/workflows/claude-triage.yml        # no ShellCheck on this host
exit 0
$ zizmor --offline --no-config --persona=pedantic .github/workflows/claude-triage.yml
No findings to report. Good job! (2 suppressed)   exit 0
$ zizmor --offline --no-config --no-ignores --persona regular .github/workflows/claude-triage.yml
No findings to report. Good job! (2 suppressed)   exit 0
$ python3 <mutant script outside the repository>
30 of 30 mutants killed by the named test; the workflow's sha256 unchanged after the run   exit 0

The first head (096ad1a) failed hosted validate on one ShellCheck note (SC2016, a jq program kept in a shell variable); the second head keeps every jq program literal and passed actionlint with ShellCheck 0.9.0 locally. R5's new shell lines keep that form: every expansion is quoted, and the only single-quoted programs are arguments to jq and awk. Hosted checks at 710d061: not read yet; they run after the push.

Decision record

docs/decisions/2026-10-08-claude-actions-triage.md (two jobs so the model never holds a write scope, what an injected issue can do, effort, cost, changed test contracts, alternatives, what would overturn it). Its R5 section (2026-10-09) names every R5 change by name, with the settings-source measurements, the tests, the 30 weakened copies and the follow-ups. docs/decisions/2026-10-04-ci-least-privilege.md lists both jobs in its write-grant table and the workflow in its per-workflow table, names each exempt job in its federation-exemption sentence, and gets a dated addendum naming both jobs.

Host evidence

Not applicable: no file under evidence/hosts/ changes.

Checklist

  • New/changed GitHub Actions are pinned to a full commit SHA with a
    version comment (no floating tags).
  • New/changed workflows declare top-level permissions: {} and grant each job
    only what it needs (contents: read, or an explicitly justified addition).
  • No secrets are printed, logged or committed; no new required secret was
    added without a documented owner.
  • No new paid hosting, subscription or billing surface was introduced.
  • Peer-owned untracked files and worktrees were preserved (not deleted,
    moved or overwritten).

Activation is a separate step after landing: an owner sets CLAUDE_TRIAGE_ENABLED to true. Runs authenticate by workload identity federation and spend from the Console organization the four ANTHROPIC_* repository variables name; no Anthropic key is stored in GitHub. Each run has a $2 client budget and the job's 15-minute timeout while it lasts, and fails afterwards if it took more than 8 assistant turns (there is no client turn limit since R6); it is expected to cost about $0.40 until measured runs replace the estimate. This PR, #892, #894 and #895 all edit the write-grant inventory and the evidence manifest, so each later one rebases once.

R7 (2026-10-09, head fea7821)

Fixes from the J8 micro read of the R5 and R6 delta (e1ba3bb3..19cf2a0a):

  • the record and this description no longer say the run is capped at 8 turns;
  • the job summary names a budget stop;
  • a success subtype whose is_error is not false is named in the failure message;
  • the --max-turns check reads the raw text, and the $2 case asserts no overrun message.

Findings 4 and 6 are dispositioned in the record's R7 section. 37 tests; five weakened copies each fail; validate rc 0; zizmor pedantic and actionlint rc 0.

🤖 Generated with Claude Code

@seathatflowsinourveins seathatflowsinourveins added the lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers label Oct 8, 2026
@socket-security

socket-security Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Dependency limit exceeded — report not shown.

This pull request scan exceeded the 10,000-dependency limit applied to this scan, so the results are incomplete and may be inaccurate. To avoid reporting false positives, Socket has not posted a report.

Upgrade your plan to raise the dependency limit and get complete reports, or view the partial scan in the dashboard.

Socket is always free for open source. If this is a non-commercial open source project, contact us to request a free Team account.

seathatflowsinourveins and others added 2 commits October 8, 2026 18:51
claude-triage.yml runs weekly and on dispatch from main while
CLAUDE_TRIAGE_ENABLED is 'true'. The classify job collects the open issues and
pull requests without a lane label as data, and Claude (read-only fence, 6
turns, $1) answers through a JSON schema with an allow-listed lane per item.
A model-free step checks every number against the collected items and keeps
only high-confidence issue labels; the apply job, with no model and
issues: write only, re-reads each issue and adds the label only to an open
issue that still has none. Pull requests get suggestions in the summary.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@seathatflowsinourveins
seathatflowsinourveins force-pushed the claude/api-actions-w4d-triage-20261008 branch from 096ad1a to ca5ad56 Compare October 8, 2026 22:52
seathatflowsinourveins and others added 8 commits October 8, 2026 19:55
The classify job now passes `--effort low` in claude_args. Under the command
center's effort mapping of 2026-10-08, classification, routing and short
probes run at `low`; this job's labels are reversible and the model-free
apply job re-checks each one against the repository's label set. The level
has to be in claude_args because `--restricted` ignores the settings files
that would otherwise carry it, and Opus 5.5 runs at `medium` on the API
when a request leaves effort unset. The bounds test now requires the flag
(removing it fails the test).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The bounds step failed the job unless the result's num_turns was at most
the --max-turns value. On Claude Code 2.1.295 num_turns counts transcript
messages, tool results included: a local run with #894's prompt and
claude_args (api-actions LR, 2026-10-08) made 12 API requests under
--max-turns 12 and reported num_turns 57. The step now counts distinct
assistant message ids in the execution file and checks that count against
6; num_turns stays in the record and the job summary as "messages". The
test fixture emits two messages per turn id, as the client does, and a new
test passes a 6-turn run that reports 57 messages and fails a 7-turn one.
Three mutants (bound on num_turns, count forced to 0, count without
de-duplicating ids) each fail the tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
main moved to aba02ec (#889 and others), and manifests/evidence.json
conflicted, so no pull_request workflow ran on the branch. The manifest is
resolved by the hot-file protocol: main's copy, then this branch's 7
changed files re-registered with host_receipts.register_file, followed by
component_matrix.py --write and new_host_grand_list.py --write;
validate.py exits 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The pre-cue read of the sibling harness-audit PR found gaps that this
workflow shares; this commit carries the same fixes here.

Bounds step: an allow-list tool check (Glob, Grep, Read and
StructuredOutput), a session start record without its tool or MCP lists
fails, a run without a structured output fails (this job answers only
through --json-schema, so the answer bound is the structured output, not
result text), and the step names every bound it finds unmet.

Tests: the step tests read the workflow with the policy test's own
loader when PyYAML is absent, so they no longer skip; new cases cover an
unknown tool, a start record without lists, a missing structured output
and the named failures (18 tests).

Record: a section names every behaviour of the new workflow (triggers,
slug, owner, first-attempt and variable guards, timeouts and grants per
job, the guard exit, the collect limits, the explicit action inputs
verified in the pinned action.yml, the usage artifact, the apply job);
the evidence line says this workflow's exact claude_args have not run;
the 2026-10-03 cooldown waiver is cited.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The sweep of the W4 drafts for the classes #894's pre-cue toolkit read
found, applied here: when there were items to classify, a classify step
that succeeds without an execution file now fails the job (with no
items the step is skipped by design); show_full_output, display_report
and track_progress are asserted; the record states that a public run's
summary is public; ci-least-privilege lists both triage jobs in its
tables and names each exempt job (in place). The workflow has no paths
input, so the bash 3.2 empty-array class does not apply. 19 tests;
actionlint and zizmor report nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The pre-cue toolkit read of 53379c7 left three findings at confidence 80 or
above that 55da753 did not touch:

- The guard step had no test. It now has one: each debug signal, a settings
  file, and a dangling link to one.
- `apply` reported an issue it could not read as skipped and exited 0. It now
  records a read or label error in the summary, tries the remaining issues,
  and fails the step.
- Test gaps: the tests now assert the numbers step's `always()`, the
  validation step's `success()`, the usage artifact, an item the model leaves
  out, and each numbers-step failure by its message. The shape tests no
  longer skip without PyYAML.

The record's test count was stale at 18; it is now 25. Ten weakened copies of
the workflow each fail at least one test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The J8 micro read of 5d9b35d found the three earlier findings fixed and
two smaller gaps:

- A body jq could not check still fell to "skipped". That covers a body
  that is not JSON, an empty body, and a runtime error such as labels not
  being a list. The record's "never reported as skipped" was therefore
  wider than the code. `apply` now reads jq's exit status: only 1 (a false
  check) is a skip, and any other exit is a failure that names jq's exit
  status. Measured on jq 1.8.1: 5 for a body that is not JSON or a runtime
  error, 4 for an empty body.
- The label-failure test proposed a single issue, so it could not show
  that the others are still tried. It now proposes a failing label first
  and a good one second.

A new test covers the three unusable bodies followed by an issue that is
still labelled. Five more weakened copies each fail at least one test, and
the earlier ten still do. The test count is now 26.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ge guard

The cc-native-practice lane's L3 read against claude-code-action v1.0.247
practice (requirement R4) found that the guard step could not see debug
logging enabled through a repository secret or variable.

Step debug logging and runner diagnostic logging are each enabled by a
secret or a variable named ACTIONS_STEP_DEBUG or ACTIONS_RUNNER_DEBUG, the
secret taking precedence. Neither reaches a step's shell unbound, and
runner.debug does not reflect runner diagnostic logging.

- The guard now binds (secrets.X || vars.X) == 'true' for both, so only true
  or false reaches the shell, and refuses either before any token is
  requested.
- A test pins both bindings, and the guard test refuses each setting.
- Six weakened copies each fail at least one test.

This is hardening: the action step already pins ACTIONS_STEP_DEBUG 'false'.
zizmor's auditor persona reports secrets-outside-env on the two reads; CI
runs the regular persona.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
seathatflowsinourveins and others added 3 commits October 9, 2026 03:00
…nd raise the caps to 8 turns, $2 and high effort

R5, quality and correctness only. claude_args keeps --setting-sources user
beside --restricted: runs of the installed Claude Code 2.1.295 measured that
--restricted keeps the project, local and user settings files out under any
source list, that --setting-sources user alone keeps the project and local
files out, and that a --settings flag is still honoured. The caps follow
the command center's derivation: 8 turns, $2 and effort high, to be
replaced by the first measured runs. The numbers step bounds 1 to 8
assistant turns and $2, requires the client version the pin installs
(2.1.295, read at the action's pin), and counts every tool-list entry that
is not a string as a forbidden tool. The apply step refuses an empty
proposal, builds and counts its rows before the loop, and keeps both gh
calls off the loop's stdin. New tests pin claude_args and --settings
exactly; the module goes from 27 to 35 tests. The record gains a dated R5
section with the settings-source measurements.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…get stops, and drop --max-turns

R6. The W4 series sets each cost bound at the budget times 1.10; triage
is the stated exception and keeps $2: with --json-schema the pinned action
returns no structured proposal from a budget stop, so there is nothing to
apply or publish. The usage record keeps result_subtype and the failure
names the stop. --max-turns 8 leaves claude_args: the pinned action fails
a success whose num_turns exceeds it, and on Claude Code 2.1.295 num_turns
counts transcript messages (57 for 12 requests); the numbers step's check
of 1 to 8 assistant turns stays. Both exact pins compare canonical JSON.
The record gains the shared cost paragraph, the exception's reason and
an upstream-issue draft (not filed).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The command center's ruling on the GPT read of 5c74982: the draft's repro
now states the predicate exactly, "a successful run whose reported
num_turns (a message count) exceeds --max-turns", instead of a sample
invocation. Record only; the manifest row is rehashed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
seathatflowsinourveins added a commit that referenced this pull request Oct 9, 2026
…894)

### Scope

- What this PR changes, in one or two sentences: adds `claude-pr-review.yml`, an on-demand, read-only review of one same-repository pull request head by `anthropics/claude-code-action` v1.0.247. A maintainer dispatches it from `main` with the pull request number and the exact head commit; the review goes to the job summary and nothing is posted to the pull request. It runs only while the repository variable `CLAUDE_PR_REVIEW_ENABLED` is `true`, so merging this starts nothing.
- Effort (added 2026-10-08 at head a6d2379): `--effort max` in `claude_args`, because judgment work (designated reads, adjudication, pull request and security reviews, audits) runs at `max` under the command center's effort mapping, and every job records its level and reason. The level has to be in `claude_args`: `--restricted` ignores the settings files that would otherwise carry it, and Opus 5.5 runs at `medium` on the API when a request leaves effort unset. The bounds test now requires the flag; removing it fails the test. A loopback dry run of the installed 2.1.295 client with this workflow's `claude_args` sent `output_config.effort: "max"` on every request.
- Fixes from the pre-cue read of the sibling #892 (2026-10-09, head d6f8909): the record names every behaviour of this new workflow: the trigger; the slug, owner, first-attempt and variable guards; the timeout and grants; the guard and binding exits; the head-as-data checkout and diff limits; the explicit action inputs `display_report` and `track_progress` (defaults in the pinned `action.yml`); and the 14-day usage artifact. The bounds step now uses an allow-list, fails on a start record without lists or on an empty result, and names each unmet bound. The review cut is character-safe and says when it cut. The step tests no longer skip without PyYAML. The 2026-10-03 cooldown waiver is cited.
- Fixes from this PR's own pre-cue toolkit read (J8 on d6f8909, head 8e64120):
  - the empty paths array is now expanded safely on bash before 4.4;
  - a success without an execution file now fails the job;
  - `display_report` and `track_progress` are asserted;
  - the artifact name and the action-pin sentence are corrected, and the parity receipt's dependency on #892 is stated;
  - `claude-pr-review.yml:review` is now in both grant tables of ci-least-privilege;
  - the record states that a public run's summary is public.
- Pre-cue toolkit read of this PR (J8 on d6f8909, 2026-10-09; `pr-test-analyzer` and `silent-failure-hunter`): every finding at confidence 80 or above, and its status at head 41c9c15.

  | Finding | Status at 41c9c15 | Fixing commit and evidence |
  | --- | --- | --- |
  | `pr-test-analyzer` 1 (90): the record names the wrong artifact | fixed | 8e64120: the record names `claude-pr-review-usage-<run id>-<attempt>`, the upload's name; 41c9c15 adds `test_the_usage_artifact_is_named_for_the_run_and_its_attempt` |
  | `silent-failure-hunter` 2 (88): the record and the workflow disagree on the artifact | fixed | the same two commits |
  | `pr-test-analyzer` 2 (85): `docs/github-automation.md` claims the harness audit's action pin | fixed | 8e64120: the bullet says the two pins differ until #892 lands |
  | `silent-failure-hunter` 1 (92): the same false claim (pin and federation variables) | fixed | 8e64120, as above; the four federation variables are the same |
  | `pr-test-analyzer` 3 (85): the headline evidence file is not in this PR | fixed | 8e64120 states that #892 adds it and lands first; 41c9c15 cites its `LR` entry field by field |
  | `silent-failure-hunter` 3 (80): "#897" and "two blocking findings" have no source in that receipt | fixed | #892 adds `target_pull_request`, `target_head`, `report_blocking_findings` and `report_sha256` to the `LR` entry (a marked 2026-10-09 amendment from the run's harness receipt); 41c9c15 cites them, calls the verdict the model's own, and states that the prompt and `claude_args` are unchanged from the run's workflow commit a6d2379 to that head (R5 later raises the two caps and the prompt's turn sentence; see R5 below) |
- R4, debug logging set as a repository secret or variable (2026-10-09; the cc-native-practice lane's L3 read against `anthropics/claude-code-action` v1.0.247 practice): the guard binds `(secrets.ACTIONS_STEP_DEBUG || vars.ACTIONS_STEP_DEBUG) == 'true'` and `(secrets.ACTIONS_RUNNER_DEBUG || vars.ACTIONS_RUNNER_DEBUG) == 'true'` as booleans and refuses either before any token is requested; a test pins both bindings, and six weakened copies each fail at least one test. This is hardening: the action step already pins `ACTIONS_STEP_DEBUG: 'false'`, which is what the action's full-output switch reads. zizmor's auditor persona reports `secrets-outside-env` on the two reads; CI runs the regular persona with `--no-ignores`, which reports nothing. The R4 lines are byte-identical in #892, #894, #895, #896 and #909; R5 leaves them unchanged here.
- The report notice at the cap (2026-10-09; the J8 micro read of #892): the publish step wrote with `jq -r`, which adds a newline, so a 60,000-byte report was announced as cut. It writes with `jq -j` now; a test publishes exactly 60,000 and 60,001 bytes, and a copy that writes with `jq -r` fails it. R5 adds the 59,999-byte case.
- From the J8 micro read of 41c9c15 (2026-10-09; 4 of 6 listed findings fixed, PTA-3 and SFH-3 partly because the receipt the record cites was not in this tree): this PR now carries a byte-identical copy of `local-parity-receipt.json` (sha256 `ba498b82…`, the file #892 adds), so the citation no longer depends on which PR lands first, and the docs bullet states the action pin and the federation variables instead of comparing with the harness audit on `main`.
- R5 (2026-10-09, head d1bcb6c, limited by the command center to correctness and test quality) adds a model-free step, `Remove symbolic links from the pull request head`, right after the head checkout: `set -euo pipefail; find pr-head -path pr-head/.git -prune -o -type l -exec rm -f {} +`. It raises the approved caps from measurement: `--max-turns` 12→30 and `--max-budget-usd` 3→5. The prompt's "You have at most 30 turns", the numbers step's bounds (1 to 30 assistant turns, at most $5, with matching messages), the header comment and `docs/github-automation.md` follow. The numbers step now counts each non-string tool-list entry as a forbidden tool named `non-string tool entry` instead of dropping it. The R4 guard lines, the deny list and the `github_token` input are kept exactly as at a1bc3bc. The test module goes from 33 to 41 tests. Eight are new: the symlink step's position and exact text; its behaviour on planted links to `/proc/self/environ`, `../.git/config` and a subdirectory, plus a re-run on a link-free head; a 59,999-byte report published whole; object, null and number tool entries refused and named; the prompt stating the `--max-turns` value; and the caps at 30 turns and $5 and just above. Changed tests pin the full `claude_args` list and `--settings` JSON exactly, assert the exact `if:` strings of the publish and numbers steps, run the guard with production's debug-off values (false/false/empty), and check that the 59,999-byte prefix before a cut character survives. All 28 weakened copies of the workflow fail at least one test with PyYAML present, as the hosted validate job has it; without PyYAML 15 of 28 do, because the pins sit in the PyYAML-only shape tests. The decision record gains a dated R5 section and names the R4 guard change in its what-changed paragraph. It corrects "Read denies for every `.git` directory" to "for the files under every `.git` directory". It restates the cap derivation and the expected cost per run, and withdraws the dry $0.70–$1.30 estimate, which was below the measured first-read cost. It adds the shared follow-up line, the shared `origin` URL sentence and two action sources. `manifests/evidence.json` is rehashed for the four changed files.
  - Sources of the R5 items: the command center's security read of a1bc3bc (the symbolic links, the closed flag list, the exact `if:` strings, the guard's debug-off values, the 59,999-byte prefix, and the stale guard sentence in the record); the command center's caps decision of 2026-10-09; the GPT designated read of #895, P2 (non-string tool entries, applied to all five W4 workflows); the cc-native-practice lane's third audit, P3-1 (the shared `origin` URL sentence).
  - Caps derivation: `LR`, a first read of #897 with this workflow's prompt and `claude_args` on the installed client, used all 12 of its 12 assistant turns and cost $2.23 ($1.87 by the client's estimate). So the need is above 12, and 2.5 times that floor gives 30 turns; twice $2.23 is about $4.5, rounded up to $5. Four delta re-reads (LR2 to LR5) used 5, 6, 7 and 6 turns and cost $1.75, $1.35, $1.54 and $0.73. A run is expected to cost about $1.9 to $2.3 for a first read and $0.7 to $1.8 for a delta re-read.
  - Filed as follow-ups before any enabling variable is set, not in R5: the security hardening from the 2026-10-09 read (debug-value widening, the tool-list shape apart from the non-string entry above, extra deny rules, token-source and guard-step assertions); the rest of F-02 (a non-empty tool list, checking the `tool_use` names); F-16 (the pins that run only with PyYAML).
  - Not run in R5: a fence smoke with a planted symbolic link, which needs a model run; any hosted run. On the first run, watch whether the action appends an `--mcp-config` that `--strict-mcp-config` still honours (the read's G3); the numbers step fails the run on any MCP server.
- Base commit: `aba02ec3` (merge base at a1bc3bc and at d1bcb6c, which builds on a1bc3bc; as read at a1bc3bc, main had moved to `ebe6c223` with #890, #900, #897 and #907; only `manifests/evidence.json` overlaps, and the one rebase happens at landing)
- Lane: `lane:foundation`
- Owned paths touched: `.github/workflows/claude-pr-review.yml` (new), `tests/test_claude_pr_review_workflow.py` (new), `tests/test_workflow_policy.py` (one inventory entry, one exemption), `tests/test_workflow_security_coverage.py` (coverage set and one zizmor test), `docs/decisions/2026-10-08-claude-actions-pr-review.md` (new), `docs/decisions/2026-10-04-ci-least-privilege.md` (dated addendum only), `docs/github-automation.md` (current-practice bullet), `evidence/artifacts/claude-actions-fence-smoke-20261008/receipt.json` (new), `evidence/artifacts/claude-actions-fence-smoke-20261008/local-parity-receipt.json` (new; byte-identical to the copy #892 adds), `manifests/evidence.json` (registration, last commit).

### R6 (2026-10-09), at bd10ba3

- **Cost bound:** the budget × 1.10 = $5.50, from four measured J8 runs on a $5 budget ($5.46, $5.33, $5.16, $5.0007),
  re-derived after the first three hosted runs.
  - A budget stop at or under it publishes the text written before the stop, with a line naming the stop.
  - Publishing is gated on `!cancelled() && steps.numbers.outcome == 'success'`, because the action fails its own step on
    any budget stop.
- **`--max-turns 30` removed:** the pinned action (`base-action/src/run-claude-sdk.ts:241-250` at 2dca132f, since
  6ef6450f) fails a success whose `num_turns` exceeds it.
  - On Claude Code 2.1.295, `num_turns` counts transcript messages: 57 for 12 requests under `--max-turns 12`. That is a
    code reading plus a local measurement, not a hosted run.
  - The numbers step's 30-assistant-turn bound and the prompt's "at most 30 turns" stay.
- **Job timeout:** 20 → 30 minutes. 30 turns at the measured pace (12 turns in 512,670 ms) take about 21.4 minutes before
  setup. #892 uses 30.
- **Settings pins:** both compare canonical JSON, so `1` no longer passes for `true` (J8 R5 micro, N1, confidence 70).
- **Record:**
  - the shared cost paragraph;
  - an upstream-issue draft (not filed);
  - the seven changed test expectations the J8 R5 micro found undeclared (U1–U7).
- **Local checks at bd10ba3:**
  - `validate.py` and the manifest check pass (10,906 files);
  - six modules: 262 tests, OK;
  - this module: 44 tests (16 skip without PyYAML);
  - actionlint and zizmor are clean;
  - 39 of 39 weakened copies fail a test with PyYAML.
- **Hosted checks at bd10ba3:** not read yet.

### SOTA sources

- anthropics/claude-code-action, v1.0.247 at `2dca132ff0e0c4094ce6048b422c6915a071210b` (<https://github.com/anthropics/claude-code-action/tree/2dca132ff0e0c4094ce6048b422c6915a071210b>): `docs/security.md` ("Do not check out an untrusted ref into the workspace root before this action"; base ref at the root and head ref in a subdirectory), `docs/setup.md` (federation inputs; "a static credential takes precedence and federation will not be used"), `action.yml` (inputs and the `execution_file` output), `base-action/src/parse-sdk-options.ts` (argument parsing with `shell-quote`, default setting sources, debug forcing full output), `src/github/operations/git-config.ts` (git authentication in the root checkout; lines 129-134 write the job token into the checkout's `origin` URL when `allowed_non_write_users` is unset) and, new in R5, `src/modes/agent/index.ts` (lines 52-61, where agent mode calls that configuration). R5 read both files at v1.0.246 (`38c80c1`); the command center's compare of v1.0.246 and v1.0.247 changes only the bundled Claude Code version.
- Claude Code 2.1.295, the version the pin installs, `claude --help`: `--restricted` ("removes the built-in tools that run commands or code ... and ignores user, project and local settings files ... Also confines the file tools to the working directories"), `--permission-prompts none` ("anything that would prompt is denied automatically"), `--tools`, `--settings`, `--strict-mcp-config`, `--max-budget-usd` and, for the R5 caps, `--max-turns`. The help does not say whether a symbolic link's target is resolved, which is why R5 removes the links before the model runs.
- GNU findutils `find` 4.10.0 as installed (`find --version`), new in R5: `find pr-head -path pr-head/.git -prune -o -type l -exec rm -f {} +`, measured by the step test on planted links and on a link-free head (exit 0, no `rm` started). The runner image's `find` was not measured.
- GitHub, Contexts reference, `runner.debug` (set only while debug logging is on, so the guard's `RUNNER_DEBUG_SIGNAL` is empty on a normal run), new in R5 as the basis of the guard tests' debug-off values; taken from the command center's read of a1bc3bc, not re-read in R5 (no network).
- GitHub, OpenID Connect reference, "Filtering for pull_request events" and "Filtering for a specific branch" (<https://docs.github.com/en/actions/reference/security/oidc>); GitHub Security Lab, Preventing pwn requests (<https://securitylab.github.com/research/github-actions-preventing-pwn-requests/>).
- Anthropic documentation read 2026-10-08: Workload identity federation (<https://platform.claude.com/docs/en/manage-claude/workload-identity-federation>); How Claude Code uses prompt caching (<https://code.claude.com/docs/en/prompt-caching>); Pricing (<https://platform.claude.com/docs/en/about-claude/pricing>).

### Evidence-class table

| Claim | Evidence class | Command / receipt |
| --- | --- | --- |
| With the fence flags (`--restricted`, `--tools`/`--allowedTools` Read,Glob,Grep, `--strict-mcp-config`, `--permission-prompts none`, `--settings` with hooks off, `claudeMdExcludes` and three deny rules; Haiku 5.5, 10 turns, $0.50, as the receipt lists; this workflow adds `--setting-sources user`, `--add-dir`, `--effort max`, four deny rules and `blockReadsOutsideWorkingDirectories`, a key in the 2.1.295 build that the receipt does not exercise) the session's tools are Glob, Grep and Read; reads outside the tree and of `.git/config` are denied; planted hooks, instruction files and a skill under `pr-head` have no effect | `native_proven` on the installed 2.1.295 client (not through the action, not on a GitHub runner) | `evidence/artifacts/claude-actions-fence-smoke-20261008/receipt.json`, three runs |
| The action's own parser turns this workflow's `claude_args` into those flags and that JSON | `local_integration` (at a1bc3bc only) | the parser's three helpers replayed with `shell-quote` 1.10.0 on the workflow text at a1bc3bc; not re-run at d1bcb6c (no network to install `shell-quote`). R5 changes two plain values (`12` to `30`, `3` to `5`), and `test_claude_has_three_read_tools_and_fixed_bounds` asserts the whole list, split per line with `shlex`, and the `--settings` JSON |
| The guard, binding, symbolic-link, diff, numbers and review steps behave as described | `synthetic` | `tests/test_claude_pr_review_workflow.py`, 41 tests at d1bcb6c (the 26 step tests run without PyYAML too; the 15 shape tests need it); each step's shell is taken from the workflow file and executed against a local stand-in for `gh`, a local git repository or a planted `pr-head` tree |
| Symbolic links under `pr-head` are removed before the model runs, `pr-head/.git` is left as the checkout wrote it, and a link-free head passes | `synthetic` | `test_symbolic_links_in_the_head_are_removed_and_nothing_else` (links to `/proc/self/environ`, `../.git/config` and `../..`; GNU find 4.10.0); no fence smoke with a planted link has run on the client |
| The tests detect a weakened workflow | `synthetic` | 20 mutated copies on 2026-10-08 (that day's bounds), 20 failed at least one test; on 2026-10-09 three turn-bound mutants each failed, and the new bounds have their own tests; at 41c9c15 three artifact-name mutants each pass the 30 earlier tests and fail the new name test; at d1bcb6c 28 mutated copies (the symbolic-link step, the flag and settings pins, the `if:` strings, the guard's debug-off values, the report cap, the non-string tool entries, the caps and the prompt) each fail at least one test with PyYAML 6.0.3 present, as the hosted validate job has it; without PyYAML 15 of 28 do (the other 13 are pins in the shape tests; follow-up F-16) |
| This workflow's own prompt and `claude_args` review a real pull request within bounds at `--effort max` on Opus 5.5 | `local_integration` (installed client, billed to a second key; not through the action) | `evidence/artifacts/claude-actions-fence-smoke-20261008/local-parity-receipt.json`, carried byte-identical in this PR and in #892 (sha256 `ba498b82…`), its `LR` entry: workflow commit a6d2379, whose prompt and `claude_args` are unchanged at d1bcb6c apart from the two caps R5 raised (`--max-turns` 12 to 30, `--max-budget-usd` 3 to 5) and the prompt's turn sentence; #897 at `target_head` 24821c5; 12 of 12 assistant turns; $1.868667 of the then $3 budget; tools Glob/Grep/Read; no MCP; `success`; `report_blocking_findings: 2`, the report's own verdict line, unchecked (`report_sha256` c49bee04, a 2026-10-09 amendment) |
| The caps, 30 assistant turns and $5, bound a runaway at about twice the measured need; a run is expected to cost about $1.9 to $2.3 for a first read and $0.7 to $1.8 for a delta re-read | `local_integration` (installed 2.1.295 client, billed to the second key; not through the action) | the `LR` entry above (12 of 12 turns, client estimate $1.868667); the $2.23 figure and the delta re-reads LR2 to LR5 (5, 6, 7 and 6 turns; $1.75, $1.35, $1.54 and $0.73) come from the command center's caps decision of 2026-10-09 and are not in this repository |
| In agent mode the action writes the job token into the root checkout's `origin` URL, so `persist-credentials: false` does not keep it out of `.git/config`; the deny rules keep the model from reading it | `source_review` for the code path; the denial of `.git/config` reads is the fence receipt's (first row) | `src/github/operations/git-config.ts:129-134` and `src/modes/agent/index.ts:52-61`, read at v1.0.246 (`38c80c1`; the command center's compare shows only the bundled client version changing at v1.0.247); the record's shared sentence also cites two 2026-10-09 probes on the installed 2.1.295, not reproduced here |
| The workflow is well formed and has no audit finding | `local_integration` | at d1bcb6c: `actionlint` 1.17.0 exit 0; `zizmor` 1.30.1 offline, regular (`--no-ignores`) and pedantic, no findings (2 suppressed in each: `secrets-outside-env` on the R4 secret reads, as at a1bc3bc) |
| A hosted run authenticates, stays in bounds and reads the cache | not established | no hosted run is part of this PR |

### Local commands run

```
$ python3 scripts/validate.py            # on the R5 tree, committed as d1bcb6c
{"components": 70, "hashed_files": 10906, "profiles": 4, "receipts": 239, "status": "passed"}   exit 0
$ python3 scripts/evidence_manifest.py --check
{"files": 10906, "status": "passed"}   exit 0
$ uv run --quiet --no-project --python 3.12 --with pyyaml==6.0.3 python -m unittest tests.test_claude_pr_review_workflow tests.test_workflow_policy tests.test_workflow_security_coverage tests.test_workflow_hardening tests.test_github_automation_practice tests.test_adoption_docs_consistency
Ran 259 tests ... OK (skipped=2)   exit 0
$ uv run --quiet --no-project --python 3.12 python -m unittest tests.test_claude_pr_review_workflow    # without PyYAML
Ran 41 tests ... OK (skipped=15)   exit 0
$ actionlint .github/workflows/claude-pr-review.yml
exit 0
$ zizmor --offline --no-config --persona=pedantic .github/workflows/claude-pr-review.yml
No findings to report. Good job! (2 suppressed)   exit 0
$ zizmor --offline --no-config --no-ignores --persona regular .github/workflows/claude-pr-review.yml
No findings to report. Good job! (2 suppressed)   exit 0
$ uv run --quiet --no-project --python 3.12 --with pyyaml==6.0.3 python -B <scratchpad>/r5_pr894_mutants.py . a1bc3bc    # outside the repository
28 mutants, 28 caught, survivors: []   exit 0
$ uv run --quiet --no-project --python 3.12 python -B <scratchpad>/r5_pr894_mutants.py . a1bc3bc    # without PyYAML
28 mutants, 15 caught   exit 1 (expected: the 13 survivors are pins in the PyYAML-only shape tests; follow-up F-16)
```

Hosted checks at d1bcb6c: not read yet.

### Decision record

`docs/decisions/2026-10-08-claude-actions-pr-review.md` (why a manual dispatch and no pull request trigger, how the head is handled, the model's fence and what was checked at runtime, cost of one run with the measured runs and the cap derivation, changed test contracts, alternatives, what would overturn it), with a dated R5 section: the symbolic-link step, the caps from measurement, the non-string tool entries, the closed flag list and the boundary tests, the mutants, and the follow-ups. `docs/decisions/2026-10-04-ci-least-privilege.md` gets a dated addendum naming the second job on the federation rule.

### Host evidence

Not applicable: no file under `evidence/hosts/` changes.

### Checklist

- [x] New/changed GitHub Actions are pinned to a full commit SHA with a
      version comment (no floating tags).
- [x] New/changed workflows declare top-level `permissions: {}` and grant each job
      only what it needs (`contents: read`, or an explicitly justified addition).
- [x] No secrets are printed, logged or committed; no new required secret was
      added without a documented owner.
- [x] No new paid hosting, subscription or billing surface was introduced.
- [x] Peer-owned untracked files and worktrees were preserved (not deleted,
      moved or overwritten).

Activation is a separate step after landing: an owner sets `CLAUDE_PR_REVIEW_ENABLED` to `true`, then dispatches the workflow with a pull request number and head commit. Runs authenticate by workload identity federation and spend from the Console organization the four `ANTHROPIC_*` repository variables name; no Anthropic key is stored in GitHub. Each run is capped at 30 assistant turns and a $5 client budget, the owner's per-run spend limit on that organization. This PR and #892 both edit the write-grant inventory and the evidence manifest, so the second to land rebases once.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
seathatflowsinourveins and others added 2 commits October 9, 2026 12:02
…summary, is_error naming

The record no longer says the run is capped at 8 turns (there is no client turn limit since R6; the numbers step
bounds assistant turns after the run). The job summary names a budget stop. A success subtype whose is_error is not
false is named in the failure message and kept in the usage record. The --max-turns check reads the raw claude_args
text, and the $2 budget-stop case asserts no overrun message. Findings 4 and 6 are dispositioned in the record.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…t the N4 disposition

The upstream-issue draft no longer says every normal run under --max-turns throws; it states the predicate (a
successful, non-error result whose num_turns exceeds maxTurns). The N4 disposition no longer calls the 15,000-token
estimate a measurement and states the timeout case plainly; the numbers step's comment qualifies its cost claim.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@seathatflowsinourveins

Copy link
Copy Markdown
Collaborator Author

Parked: Claude review inside GitHub is retired (owner decision 2026-10-10, ~17:15Z); see #981 and docs/decisions/2026-10-10-retire-in-github-claude-review.md. No further pushes.

@seathatflowsinourveins

Copy link
Copy Markdown
Collaborator Author

Closing as superseded, with the owner's OK (CC, 2026-10-11).

It adds .github/workflows/claude-triage.yml, which runs the Claude action. main's tests.test_workflow_hardening.InGitHubClaudeReviewRetiredTests now forbids any workflow that runs it.

Claude review inside GitHub was retired by #981 (8bffa2c, docs/decisions/2026-10-10-retire-in-github-claude-review.md). The Anthropic API refuses the action's entry point on Console keys (CC correction #141; the action never had a successful run). AI reviews now run locally, and GitHub enforces them as required checks. The local route is #953 (the review worker).

Nothing is lost: this PR can be reopened, and any model-free part of it can come back as a new PR under the local-review route. Evidence: the CC's PR triage (research-cc-role-20261008/pr-triage-20261011.md, class CLOSE).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant