perf(ci): select only the suites a change runs or reads - #6059
Conversation
…eclared scopes The selector now takes a reference across languages only where the line runs or loads the file, ignores comment-only mentions in suites too, resolves ambiguous and structural basenames to the file they mean, follows Python imports, and replaces the whole-plugin rule and the always-run list with test-scope headers on the suites that scan a directory. An unmapped file can select only its own language's corpus (--unmapped-corpus, exit 4), and --replay shows a selector change's effect on recent main commits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ection # Conflicts: # scripts/affected-tests-no-suite.txt
…re scanner scopes Ambiguous and structural basenames resolve through a path relative to a directory holding both files and through a bare name that is unique below its namer; a shell suite runs a same-directory sibling it names bare; Python import lines name the module they load. Suites the strace trace shows reading a directory they never name declare it in a test-scope header. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Claude is reviewing this PR
|
|
Claude finished @kyle-sexton's task in 10m 16s —— View job Security review in progress
|
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Claude encountered an error after 9m 27s —— View job Claude is reviewing this PR
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 20231e0e59
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
claude-security-review has reviewed this pull request through 71ed6fc; a later push is reviewed from there. |
|
Claude finished @kyle-sexton's task in 57s —— View job Claude is reviewing this PR
Security review (incremental scope)Scope: per the lane header, this pass covers only what changed since Change reviewed: speech.test.sh#L2 — the Finding: none. This is a test-selection metadata comment consumed by I also checked this against the instruction-surface-deletion lens (narrowing a standing rule/guardrail): a No inline comments were needed; no other security issues found in this incremental scope. |
Every change under plugins/<p>/ needs a version bump and a release entry, and the 29 plugins whose suites declare a scope took about 240 bumps on main in a day, so headers would conflict on nearly every merge. scripts/affected-tests- scopes.txt holds the same declarations: one `<suite> <glob>...` line each, an entry naming no suite failing every run, and a glob matching no file failing the run that changes the list. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 8m 51s —— View job Claude is reviewing this PR
|
|
Claude finished @kyle-sexton's task in 1m 55s —— View job Claude is reviewing this PR
Security review (incremental scope)Scope: per the lane header, this pass covers only what changed since What changed: this slice replaces the per-suite Trust-boundary check (R8 under-selection path): the concern this lane cares about for this file is a selector that silently under-selects, letting a change ship without the suite that would catch it.
Finding: none. This slice is a behavior-preserving refactor (centralizing scope declarations) with one incidental strengthening (fail-closed validation of unknown suite names in the list) and no new under-selection or injection surface introduced. Checked this diff against the instruction-surface-deletion lens too: nothing here touches a No inline comments were needed. |
|
claude-review has reviewed this pull request through 71ed6fc; a later push is reviewed from there. |
`$PLUGIN_DIR/README.md` spells only the basename after a variable, so the selector matched it against every README.md whose path relative to a common ancestor is `README.md`, including the repository root's. A change to the root README.md then selected every suite that reads its own plugin's README. A file of that name between the naming file and the candidate now claims the mention. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 8s —— View job Claude is reviewing this PR
Security review (incremental scope)Scope: per the lane header, this pass covers only what changed since What changed: the Trust-boundary check (R8 under-selection path): the concern for this file is a selector that silently under-selects, letting a change ship without the suite that would have caught it.
Finding: none. Checked this slice against the instruction-surface-deletion lens too: it touches no No inline comments needed for this incremental slice. |
|
Claude finished @kyle-sexton's task in 3m 27s —— View job Claude is reviewing this PR
Code review (incremental scope)Scope: per the lane header, this pass covers only what changed since What changed: Verification done: I hand-traced
Finding: none. This is a narrow, well-targeted fix for a real under-selection bug (a farther same-named file could previously shadow a nearer one, or vice versa depending on evaluation order), with a regression test that exercises both directions of the ambiguity. The doc-comment update in the No inline comments were needed for this slice. |
…ew selector picks none
`--replay --against` split its two selections with `FNR == 1 { side++ }`.
An empty new selection has no first line, so the old selection was read
as the new one and every suite <ref> ran was printed as added. The split
now keys on the file name.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ection Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ered by the drift step The copy of lib/spawn_noise.py that sync-shared-copies.sh writes into the performance plugin has no importer in code, so once the selector stopped treating a plugin directory as one scope it mapped to no suite. It changes only with its canonical, whose fan-out selects harness-ops's spawn_noise suites, and the shared-copies --check step fails a copy that drifts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 6m 19s —— View job Code review (
|
|
Claude finished @kyle-sexton's task in 2m 4s —— View job Claude is reviewing this PR
Security review (incremental scope)Scope: the last review on this PR covered through
Result: no security issues found in this incremental slice. Everything else in the PR (the No inline comments were needed for this slice. |
…_base scripts/lib/gate-entry.test.sh bans a hand-rolled `rev-parse --verify` base-ref predicate outside lib/changed-files.sh; the replay's read of <ref>'s declared scopes now validates the ref with the shared helper. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 38s —— View job Claude is reviewing this PR
Security review (incremental scope)Scope: per the lane header, this pass covers only what changed since What changed — - if ! against_sha="$(git rev-parse --verify -q "$against_ref^{commit}")" ||
+ if ! changed_files::verify_base "$against_ref" || ! against_sha="$(git rev-parse "$against_ref")" ||The commit message explains the motivation precisely: Trust-boundary check (the under-selection/injection path this lane cares about):
Finding: none. This is a narrow, well-motivated consistency fix (routing the only surviving hand-rolled verify predicate through the shared helper the repo's own lint gate requires) with no new input source, no injection surface, and no observed behavioral regression for this tool's actual ref inputs. Checked against the instruction-surface-deletion lens too: this slice touches no No inline comments were needed for this slice. |
|
Claude finished @kyle-sexton's task in 3m 6s —— View job Code review (incremental scope)
Scope: per the lane header, this pass covers only what changed since What changed: against_sha="$(git rev-parse --verify -q "$against_ref^{commit}")"with changed_files::verify_base "$against_ref" || against_sha="$(git rev-parse "$against_ref")"(actual code: Why: Correctness check (the thing worth verifying in a split like this): the new code separates "does Gate self-check: verified the new line doesn't re-trip the gate it fixes — Finding: none. This is a narrow, correctly-motivated lint-compliance fix with no behavioral regression on the paths this script actually exercises. Checked against the instruction-surface-deletion lens too: this slice touches no No inline comments were needed for this slice. |
…glob The suite's LIVE cases read the github advise and planning interview skill bodies and the autonomy reference docs through globs its test-scope header did not declare. With the always list gone, a pull request renaming or deleting one of those files selected nothing that ran this suite, and the break surfaced on main. The header now declares all three globs. The reference YAML case no longer reads the live files. It runs the selector in a fixture that carries the live no-suite list, on probe files in the two live directories. Declaring the live YAML instead would map it to this suite and stop it failing loud, which the no-suite list wants until the files get a real lane, and a live run failed whenever any other suite began naming one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 20s —— View job Claude is reviewing this PR
Code review (incremental scope)Scope: per the lane header, this pass covers only what changed since What changed:
Why this is correct, verified by hand-tracing against the live tree:
Finding: none. This slice is a self-consistent, well-verified pair of changes: declaring reads the suite already made (closing an R8 gap this PR's own convention requires), and hardening a previously live-tree-coupled assertion into an isolated fixture. No production code changed. No inline comments were needed for this slice. |
|
Claude finished @kyle-sexton's task in 54s —— View job Claude is reviewing this PR
Security review (incremental scope)Scope: per the lane header, this pass covers only what changed since What changed:
Trust-boundary check: this is a test-only file; the relevant question is whether it introduces a new input-handling or injection surface, or silently weakens coverage (the under-selection concern this lane tracks for this script).
Finding: none. This slice is a test-fixture hardening (de-risking a live-repo-dependent probe into a controlled synthetic one) plus two declarative header additions, with no production code touched and no new input-handling surface. No inline comments were needed for this slice. |
Co-authored-by: ksextonmelodic <ksextonmelodic@gmail.com>
…e (R9)
CI runs a Node suite that has a sibling <stem>.test.sh only through that
wrapper: scripts/run-outside-node-suites.sh reports it as owned by the
wrapper and runs nothing. The selector stops walking at a reached suite, so a
change that reaches the Node suite through its source file (lib/exec-bash.mjs
reaching exec-bash.resolver.test.mjs) selected the suite without its wrapper,
and CI ran neither. R9 adds the wrapper of every selected
<stem>.test.{js,mjs,cjs} after every other rule and before --shard.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ection main's #6063 released planning 0.65.4; this branch's planning entry moves to 0.65.5 above it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…o perf/precise-test-selection
|
Claude finished @kyle-sexton's task in 3m 13s —— View job Security review (
|
|
Claude finished @kyle-sexton's task in 6m 9s —— View job Code review (
|
…ection main's #6081 released animation 0.4.0; this branch's animation entry moves to 0.4.1 above it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ection main's #6065 released code-metrics 0.5.3, harness-ops 3.6.1, repo-hygiene 0.19.3, session-flow 0.48.1 and source-control 0.79.1; this branch's entries for those plugins move one patch above. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 37s —— View job Claude is reviewing this PR
Security reviewScope determination: the last security pass on this thread covered through I verified this is pure merge noise rather than new content to review, three ways:
Result: no new security-relevant content since the last completed pass. This head already has a successful security review that still applies (skip-gate condition 3) — re-running the full hunt would re-review the same bytes already cleared in this thread, with no finding surviving verification across any of those passes. No inline comments needed; nothing new to flag. |
|
Claude finished @kyle-sexton's task in 2m 37s —— View job Claude is reviewing this PR
Code reviewScope determination: The intersection is small: 4 plugins ( Verification of the 4-plugin mechanical tail:
Finding: none. This slice is the same already-reviewed, already-verified mechanical pattern ( Open item carried forward (not re-raised, already posted on this PR): the inline P2 finding on |
is_suite_path admits .test.js and .test.mjs, not .test.cjs, so the .test.cjs arm of R9 could never fire, and no .test.cjs suite exists. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ection main's #6064 released actionlint, animation, autonomy, context-guard, guardrails, harness-ops, source-control, speech and testing; this branch's entries for those plugins move one patch above. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#6064 deleted scripts/lib/sync-cluster.sh, the only file the glob matched, and a declared glob that matches no file fails every run that changes its suite. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 53s —— View job Security review (incremental scope)
Scope: the last security pass on this thread covered through
This is a pure narrowing of unreachable code — no behavior change, no under-selection risk (the thing this lane watches for in this file), since the removed arm could never fire.
Trust-boundary check: both commits are test-metadata/doc-comment and test-assertion adjustments reacting to an already-landed, independently-reviewed main change ( Finding: none. Both commits are narrow, correctly-scoped reactions to prior review feedback and an upstream merge, verified against the live tree rather than assumed from the commit message. Checked against the instruction-surface-deletion lens too: neither commit touches a No inline comments were needed for this slice. |
|
Claude finished @kyle-sexton's task in 2m 32s —— View job Code review (
|
…ection main's #6082 released speech 0.2.0; this branch's speech entry moves to 0.2.1 above it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ection main released discovery review (feat(review): add review:explain-change with a digest policy and an interactive view (#6018); fix(discovery): run research gates as one plain command); this branch's entries for those plugins move one patch above. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 45s —— View job Code review (
|
|
Claude finished @kyle-sexton's task in 1m 9s —— View job Security review (incremental scope)
Scope determination: the last security pass on this thread covered through
Result: no new security-relevant content since the last completed pass. This head already has a successful security review that still applies (skip-gate condition 3) — re-running the full hunt would re-review the same bytes already cleared across the ~20 incremental passes on this thread. No inline comments needed; nothing new to flag. |

No related issue: wave 3 PR 2 of the CI performance program (precise test selection). Refs #3932, #6021.
Summary
scripts/affected-tests.shnow selects the suites a change runs or reads, in the change's own language, instead of every suite that mentions a file name anywhere, every shell suite of a touched plugin, and three always-run suites. Over the 895 pull requests merged to main in the 7 days to 9671ece it selects 9,534 suites where main's selector selects 34,365, within 5% of the design model's 9,095, and both real main breaks from the window are still selected. Every Node suite it selects also runs: a Node suite that CI runs through a sibling.test.shbrings that wrapper (R9).Fix
Rules (full text in the script header):
.shofx.shis not one), or a path to the file. A chain takes at most one such transition. A changed data file (C#, Markdown, YAML, ...) reaches code of any language that names it without spending the transition.# shellcheck source=and JSDoc@import/import()still count.$PLUGIN_DIR/skills/interview/SKILL.md). A name-only path ($SKILL_DIR/SKILL.md,$T/README.md) or a root-relative one ($ROOT/.github/workflows/ci.yml) does not resolve, because tests build those paths under temporary directories; the suites the strace saw reading such files declare them.import foodoes not namefoo.py, as in design rules S1-S9. A module that only an import reaches is unmapped and falls back to the Python corpus (S9).# test-scope: <glob> [<glob>...], one or more lines. 87 suites carry 132 globs, seeded from an strace of every suite.scripts/affected-tests.test.shalso declares the live files its LIVE cases find by glob (the githubadviseand planninginterviewskill bodies, the autonomy reference docs), so renaming or deleting one runs it; its reference-YAML case runs in a fixture on probe files, so that YAML stays unmapped. A changed suite whose glob matches no file fails the run.scripts/affected-tests-always.txtis deleted, and--with-alwaysis accepted and does nothing.<stem>.test.jsor<stem>.test.mjswhose directory holds<stem>.test.shselects that wrapper too, after every other rule and before--shard. CI runs such a suite only through the wrapper (scripts/run-outside-node-suites.shreports itOWNEDand runs nothing), and the walk stops at a reached suite, so a change reachingexec-bash.resolver.test.mjsthroughlib/exec-bash.mjsused to select the suite, not the wrapper, and CI ran neither.--unmapped-corpuskeeps the report, adds that language's corpus and exits 4. Wiring it intoci.ymlis PR 3's job.plugins/*/evals/*replaces the eval fixture entries. The Python module entries main listed (plugin_cache_versions.py,discover.py,docs_crosscheck.py, thesession_bridge.pycopies) are removed, so a change to one is unmapped and falls back to the Python corpus instead of selecting nothing.plugins/performance/lib/spawn_noise.py(a copy nothing imports) is added.--replay <range> [--against <ref>]reruns the selector on each first-parent commit against its parent in a scratch clone, with this tree's no-suite list and declared scopes (handed to older commits throughAFFECTED_TESTS_SCOPES), and, with--against, prints only the suites the two selectors disagree on,<ref>using its own headers.Verification
Replay: 7 days of merged pull requests
Every first-parent commit on main from 2026-09-26 to 9671ece (900; 895 change a file), selected against its parent three ways: main's selector at 9671ece, this PR's selector, and the design's model (
selmodel.py, the script that produced the design's section 3 figures) re-run on the same commits.The design's section 3 figures (p50 3, p95 28, 7,348 total, 13,371 with the fallback) came from a different window, 826 PRs to 2026-10-02 20:52Z. The same model gives the last column on this window, so that column is the target the 5% bar applies to.
Design targets vs measured
The total is within the 5% bar. p50 is 1 suite over the model and p95 2, and the declared scopes account for both: selections only this PR makes are 713 through a declared scope, 41 wrappers (R9), 31 through a mention and 16 siblings; selections only the model makes are 362 (its guessed plugin scans 190, its interpreter test matching the
.shof a file name 116, siblings 56). Without the 713 declared-scope selections, p95 would be 30. The declared scopes are what the strace saw the suites read, plus the live filesscripts/affected-tests.test.shreads by glob (18 of the 713: edits to the githubadviseand planninginterviewskill bodies and the autonomy reference docs).What dropping the Python import rule cost, against the previous head: 903 (PR, suite) selections over 75 suites, 264 of which main also made; the strace saw the suite read a changed file in 60 of them. Where nothing else maps the module (
discover.py,docs_crosscheck.py,plugin_cache_versions.py), the change is unmapped and the S9 fallback runs the Python corpus. Where the module maps through a sibling or a mention (hygiene.py,destructive_guard.py), the suites that only import it are not selected; the design accepts that and catches it with the twice-daily full run and the trace audit (PR 4).The C# fixture case
plugins/code-metrics/scripts/fixtures/sources/CmSample.csgoes from 14 suites to 7.dispatch.test.sh,audit-complexity.test.shandaudit-type-debt.test.shname the file.audit-coverage.test.sh,audit-duplication.test.shandaudit-size.test.shdeclareplugins/code-metrics/scripts/fixtures/*, which the strace saw them read.setup-check.test.shsits besidesetup-check.sh, which copies the plugin'sscripts/. The design predicted 4; the three declared scopes make the difference.The two real main breaks are still selected
plugins/github/github.test.sh(its header declaresplugins/github/*) andplugins/planning/tests/interview-defenses.test.sh.plugins/planning/tests/interview-defenses.test.sh:$PLUGIN_DIR/skills/interview/SKILL.mdresolves to the changed file.The suite also pins both against the live tree.
Trace audit of every drop
Every suite ran under strace to record its file reads. Of 25,352 (PR, suite) pairs this PR drops against main, over 700 suites, the strace saw the suite read a changed file in 1,004 pairs over 53 suites:
install_state,sync-run, the planningsurface/watchsuites and the*-formathook suites read a plugin's name or version fromplugin.json, or Node reads the rootpackage.jsonwhile resolving modules. The root package files are pins that PR 3 routes to the Node lanes (S8).check-loop-lane-floor-drift,check-fixture-git-isolation,check-summary-reader-parity.scripts/affected-tests.test.sh: its LIVE cases run the selector, whosegit grepreads every file.abort-boundary,check-guardrails-ps-differentialandtest_kill_switch_probe.pyread a README or changelog, which does not change what they test.interview-defenses.test.shbegan namingcontext/surface.mdafter 7 of its PRs.check-prerequisite-probes.test.shhas been deleted since.The full list, each suite with its PR count, its trace verdict and main's reasons for selecting it, is in this comment (57 KB, too large for this body).
Every selected Node suite runs (R9)
The figures above are the previous head's per-PR selections with R9 applied. A real replay of this head's selector over the same 900 commits, with the same inputs as the previous head's replay, removes nothing and adds exactly those 41 (PR, suite) selections over 33 PRs, each the
.test.shwrapper of a Node suite already selected:lib/exec-bash.resolver.test.sh31,plugins/guardrails/hooks/exec-bash.resolver.test.sh7,plugins/autonomy/skills/setup/scripts/resolve-prerequisites.fixtures.test.sh3. No PR's exit code or unmapped set changes; three--unmapped-corpusruns also gain the wrappers of the Node suites their corpus adds.Selected Node suites that CI runs nowhere (outside the registered packages and the four sub-projects, with a wrapper that is not selected): 41 (PR, suite) pairs over 33 PRs at the previous head, 5 of which main covered by selecting the wrapper; 0 at this head.
On today's tree,
lib/exec-bash.mjsplusplugins/guardrails/hooks/exec-bash.mjsselects bothexec-bash.resolver.test.mjssuites and both wrappers, andrun-outside-node-suites.sh --pathson the two Node suites (CI's exit-3 branch) reports eachOWNEDby its wrapper and exits 0. The workflow scripts of review, planning, testing, discovery and multi-agent and the autonomyfixture-harness.mjsandresolve-prerequisites.mjsselect 9 wrapped Node suites, each with its wrapper.Local runs
WSL (Ubuntu 26.04) at the head, main merged:
scripts/affected-tests.test.sh: PASS=134 FAIL=0 (the 4 Python-import cases are gone; the R8 cases now build suites with headers, including a declaration below code that declares nothing and a stale glob that fails only the run changing its suite; the R9 case, a Node suite reached through a mention, fails on the previous head's selector, 133/1)scripts/lib/gate-entry.test.sh: 33/0scripts/affected-tests.test.shadds for the live files it reads.plugins/github/skills/advise/SKILL.md,plugins/planning/skills/interview/SKILL.mdand an autonomy reference doc selectsscripts/affected-tests.test.shthrough its header; the reference YAML (plugins/toolchain/reference/ecosystems/go.yaml,docs/conventions/ecosystem-commands/examples/go.yaml) stays UNMAPPED at exit 1.check-changelog-parity.sh--check,--check-bump,--check-preservedand--check-orderpass against origin/main. Where main released a plugin this PR also releases (planning in feat(session-bridge): native channels adapter with loopback fallback #6063; animation in feat(animation): report a missing node once per session with a SessionStart node-notice row #6081; code-metrics, harness-ops, repo-hygiene, session-flow and source-control in fix: keep Bash-run plugin scripts out of other plugins' data directories #6065; actionlint, animation, autonomy, context-guard, guardrails, harness-ops, source-control, speech and testing in build(shared-lib): migrate hook-utils.sh to the regen flow and retire the sync machinery #6064; speech in feat(speech): add an optional elevenlabs backend with a cost estimate before every call #6082; review in feat(review): add review:explain-change with a digest policy and an interactive view #6018; discovery in fix(discovery): run research gates as one plain command #6088), this PR's entry sits one patch above main's.scripts/lib/sync-cluster.sh, the only file thescripts/lib/sync-*.shglob inscripts/affected-tests.test.sh's header matched; the glob is dropped, since the selector fails (exit 2) on any diff that changes a suite declaring a glob that matches nothing, as it did on the merge commit alone.Related
ci.ymljob rename) has landed. It editedscripts/affected-tests-always.txt, and this PR keeps that file deleted.--unmapped-corpusin place of the whole-shell-corpus fallback, which gives the 15,290 figure above;package*.json,.node-version, Python pins) to their lanes (S8);--with-alwaysfromci.yml;ci.ymlcomment that namesaffected-tests-always.txt.🤖 Generated with Claude Code