Repository navigation
ci(lint): key the PM dispatch-gates family on the files its battery opens - #22092
Merged
objectstack-fleet[bot] merged 3 commits intoOct 7, 2026
Merged
objectstack-fleet[bot] merged 3 commits into
objectstack-fleet[bot] merged 3 commits into
Conversation
…pens The pm_dispatch_gates arm of select-gate-families.sh ran the PM self-test for every change under scripts/ and every piece of agent configuration. One full battery run under an fs/child-process read hook shows the battery never opens a data or prose file under scripts/ outside scripts/pm/, nor skills/**, AGENTS.md, CLAUDE.md or the .claude/ prose and JSON outside the pm-dispatch rulebook. Those now skip the step on pull_request and merge_group; scripts/pm/** whole, every code file under any scripts/, the rulebook, .claude code, the workflow tree and every package manifest still run it, and a scripts/ data file a relative import specifier names runs it (the one edge by which the discovery could open one). push on main and the scheduled run keep the whole battery. Self-test: 12 new or re-pinned cases, floor raised to 68 cases / 354 checks. Claude-Session: https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw Co-authored-by: Claude <noreply@anthropic.com>
…kips the PM self-test The PM dispatch-gates discovery opens a file under a workspace package's scripts/ only as a gate source, which is a file a check:* command in a manifest or a workflow step spells; it follows no import into a package's scripts/. A test there that no package.json and nothing under .github/ names is therefore read only by the two whole-tree censuses, as a test under src/ is, and it now skips the step on pull_request and merge_group. A basename either place spells still runs it (superset grep, fail-open on any git grep error). Self-test: three more cases, floor 71 cases / 369 checks. Claude-Session: https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw Co-authored-by: Claude <noreply@anthropic.com>
…rrow-dispatch-gates-family
objectstack-fleet
Bot
deleted the
claude/issue-22076-narrow-dispatch-gates-family
branch
October 7, 2026 17:49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #22076
Clause-②: no
What changes
The
pm_dispatch_gatesarm ofscripts/ci/select-gate-families.shdecides theif:of thePM dispatch-gates self-teststep inLint & Repo Gatesonpull_requestandmerge_group. It now keys on the files the battery actually opens, measured, instead of on whole directories. Three classes stop selecting the step:scripts/outsidescripts/pm/(.json,.md,.txt: ratchet baselines, pinned ledgers, fixture logs). Exception: a file whose basename some file spells as a quoted relative specifier (./…or../…) still runs it. That is the one edge by which the discovery could open such a file: it follows imports out of gate sources.skills/**,AGENTS.md,CLAUDE.md,.claude/agents/**,.claude/settings.json, and the other skills under.claude/skills/.scripts/whose basename nopackage.jsonand nothing under.github/spells.Unchanged:
pushtomainand the scheduled run take the selector's unscoped*)event branch and run the whole battery (select-gate-families.sh:27–:28).Lint & Repo Gatesstays a required context.scripts/pm/dispatch-gates.mjs,scripts/pm/check-dispatch-gates.mjsor.github/workflows/lint.yml.Both new guards fail open: any
git grepanswer other than "no match" runs the family. Every extension the arm does not list also runs it.Read-set: before → after (the
pm_dispatch_gatesarm)scripts/pm/**(any file)scripts/.json/.md/.txtunderscripts/, outsidescripts/pm/scripts/scripts/.github/names itpackage.json.github/**.claude/skills/pm-dispatch/**.claude/**code (hook.sh, workflow.js)skills/**,AGENTS.md,CLAUDE.md, other.claude/**.md/.jsonHow the read-set was measured
I ran the full battery once (
node scripts/pm/dispatch-gates.mjs --self-test, tree3d9188502e, a detached worktree) under a preload hook. The hook was installed throughNODE_OPTIONS=--import, so every node child the battery spawns carried it too. It recorded:readFileSync/readdirSync/existsSync/statSync/openSyncunder the repo root, with the innermostselfTest()line that caused it;Result: 1,976 cases pass, exit 0. There were 66 node processes, 8,430 distinct files opened under the repo root, and 57
git ls-filescalls at the root.What the battery opens, by reader:
dispatch-gates.mjs:18406) and exposed-scratch-dir sweep (:26643), whole tree.ts .tsx .mts .mjs .js):20834):19045).sh, 30 (10 of them.claude/hooks/*.sh)scripts/and 40 underscripts/pm/; 31 underpackages/{spec,lint,platform-objects,services/service-messaging}/scripts/(29 gate sources, 2i18n-extract.config.ts, no test); rootpackage.json,packages/{client,lint,spec}/package.json, roottsconfig.json, root.gitignore:26122–:26290).mdunder.claude/skills/pm-dispatch/packages/objectql/src/engine.ts,packages/rest/src/rest-server.ts)Never opened: any
.jsonbeyond those four manifests andtsconfig.json, and any.mdoutside the rulebook. The same holds for:.txt;.ymloutside workflows and actions;skills/**,AGENTS.mdandCLAUDE.md;.claude/agents/**,.claude/settings.json,.claude/launch.jsonand the other.claude/skills/*;scripts/dirs.Name-level reads only:
existsSyncon.claude/agents/os-dev.md,skills/and threeskills/*/SKILL.md. These are the governed read floor and the frame-syncCOPIEStable, which the battery reads by importingscripts/check-skill-frame-sync.mjs..github/workflows/listing.Why the narrowing does not depend on this one tree:
resolveCheckToFilesadmits onlyscripts/…\.(mjs|cjs|js|sh|ts|mts|cts)as a gate source.firstPartyImportBindingsfollows only.//../specifiers that resolve under the rootscripts/, and never into a package'sscripts/.SKILL_RULEBOOK_ROOT).So the only tree-dependent ways into the three classes are a relative import, and a manifest or workflow naming a package-scripts test. Both are guarded.
Which input classes can move a verdict, and how each was measured
scripts/pm/**, rootscripts/code, packagescripts/gate sources, workflows/actions, the four manifests, rulebookselfTest()lines).claude/**andpackages/**code,.shanywhere:18406,:26643,:20834,:19045)scripts/.json/.md/.txt, agent prose/JSONresolveCheckToFiles/ import-follow code readingscripts/tests, unwiredsrc/testgit grepof every manifest and.github/for each of the 67 such tests (2 are named:root-entry-type-nameability.pin.test.tsis alint.ymlstep, andexport-list.test.tsis mentioned in aci.ymlcomment)git ls-filesspawns in the hook logCoverage that moves from PR time to
push/ scheduled runssrc/.Effect, replayed
I replayed the real selector, before and after, over the last 300 first-parent merges on
mainending at3d9188502e. Each merge ran as amerge_groupentry with base = first parent, in a no-checkout worktree.run → skipand none flip the other way.scripts/pm/. 25 of them arescripts/engine-double-contract.pinned.json, plusdoc-authoring-prose-id.baseline.json,test-shard-timings.jsonand others.skills/**.93125aeeb8, PR perf(spec): a bundle that never reads the ADR-0087 conversion table stops keeping it, 226 KB gzip off the console first screen (#22044) #22048, the queue entry the card measured at 10.2 min for the step. It touchedpackages/spec/scripts/conversions-major18-merge.test.tsandpure-schema-construction.test.ts.runbecause the specifier guard's superset grep hit:eslint.config.mjsreads./scripts/query-options-erasure-baseline.jsonthroughnew URL;scripts/docs-audit/README.mdnameshandwritten-docs.json.Pins (selector self-test case names)
Triage pin 1, "a PR touching only product code skips the family":
pull_request: a PR touching only packages/*/src/** runs no PM self-test (#22076 pin)(new; the card's own pin)pull_request: a spec source, an unwired test under packages/spec/scripts and a changeset run no PM self-test (#22076)(new; the perf(spec): a bundle that never reads the ADR-0087 conversion table stops keeping it, 226 KB gzip off the console first screen (#22044) #22048 shape)pull_request: the PR #19314 shape pays the two ratchets and the corpus, and no tooling self-test (the measurement this narrowing was ruled from)(kept)Triage pin 2, "a PR touching a gate's source, a workflow's gate wiring or
scripts/pm/**runs it":pull_request: a PR touching scripts/pm/** runs the PM self-test, prose included (#22076 pin)(new)merge_group: a data file under scripts/pm still runs the PM self-test -- scripts/pm is in its read-set whole(new)merge_group: a test under a package scripts/ that a check script names is a gate source and still runs the PM self-test(new)merge_group: a data file a relative import specifier names still runs the PM self-test -- the one edge by which the discovery opens a data file(new)merge_group: a workflow change runs every family built on the dispatch derivation, and not the scripts-only sweep(kept)merge_group: a composite action is part of the workflow tree the derivation discovers(kept)merge_group: a scripts/ subdirectory script is a gate source only(kept)merge_group: a package-local script is a gate source (the derivation families) and a masked source (corpus)(kept)Triage pin 3, "the scheduled run still runs it (control)":
schedule: a change the PR path skips the PM self-test for still runs it -- the hourly full run is the backstop (#22076 control)(new)push: the same change on main still runs the PM self-test (#22076 control)(new)merge_group: the same change, scoped, skips the PM self-test -- so the two controls above are not vacuous(new)Other new or re-pinned cases:
a ratchet baseline … not the PM self-test, re-pinned. It previously expected the PM self-test to run.agent prose the battery never opens … runs no family, re-pinned. It previously expected the PM self-test alone.prose and a fixture log under scripts/ …CLAUDE.md and the .claude settings JSON run no familythe pm-dispatch rulebook runs the PM self-test alonea .claude hook shell script still runs the PM self-test alonea scripts/ file of a kind the arm does not list still runs the PM self-test -- fail-open on the extensiona test under a package scripts/ that no manifest or workflow names … skipsThe floor goes from 56 cases / 293 checks to 71 cases / 369 checks.
Reverse verification (on committed
1ed9df2285)3d9188502e(git restore --source; on-disk blobe6c2f3c2== base blob), run under the new self-test. Exit 1, with 7 cases red and 20 checks failing — every narrowing case, plus the non-vacuity partner. Restore: blob76f368b7== HEAD, andgit diff HEADis empty.node scripts/ablation-replace.mjs --deleteon thenamed_by_relative_specifiercall (anchor 1 → 0). Exit 1; exactlya data file a relative import specifier names still runs the PM self-testgoes red (3 checks). Restore proven: blob == HEAD.named_in_gate_wiringcall. Exit 1; exactlya test under a package scripts/ that a check script names … still runs the PM self-testgoes red (3 checks). Restore proven.Gates (head
1ed9df2285)I derived the gate list with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 27 commands. I ran each one and wrote its exit code to disk before reading it.All 26 non-battery commands exit 0. They include:
pnpm check:select-gate-families(71 cases / 369 checks)check:bash32-floor,check:parse-guard,check:nul-bytescheck-ci-filter-parity,check-self-test-workflow-commands(with and without--self-test),check-comment-mask-corpus,check:entry-guardpnpm check:pm-dispatch-gates(the control) ran detached on1ed9df2285(not under the verify lock), on a shared 4-core container at load 5–8. Result: exit 0,✓ dispatch-gates self-test: 1976 cases pass.,the battery took 1004.0s on this box, wall 1,006 s (2026-10-07T14:39:27Z → 14:56:13Z). That is a reading of a contended agent box, not of a runner. The instrumented measurement run at3d9188502ealso passed all 1,976 cases. The record is reconciled with--ran:✓ dispatch-gates --ran: 27 derived famil(ies) accounted for — 27 run, 0 NOT-MEASURED.Premise checks
"Selects the step on most PRs." By rule, any
package.jsonand anything under anyscripts/selected it. Measured over 300 merges, it selected 95, which is 31.7%, not most. The scripts data files carried 32 of those 95 on their own."The censuses are deliberately off the PR path." In code this means the selector returns
skipfor product code, so those inputs never select the family. It does not mean the censuses are skipped once the family is selected: the battery has one tier, and every selected run pays all of them. The hook found four whole-tree censuses, not the two the old comment named. The two it did not name are the error-code literal census over 3,208 files and the.shcensus. It also found 10 live product specimens. The header now names all of them."A
package.jsonchange that does not touch acheck:*script may not move the verdict." It may not, but the narrowing is not taken. In the replay, 13 merges touched a workspace manifest:pnpm-lock.yaml(root config, so every family runs anyway);The set of manifests the discovery reads is also tree-dependent: on this tree it is root,
client,lintandspec, decided by the--filterrows workflows spell. Ascripts/namefield diff would neednodein the selector and a parse fail-open branch to save about 1 run in 300.Notes
--self-testflag. Its self-test is the siblingscripts/ci/select-gate-families.selftest.sh(pnpm check:select-gate-families), which is where the pins live, so the diff is those two files.dispatch-gates.mjs..github/files outside workflows and actions (CODEOWNERS,labeler.yml,dependabot.yml). The battery never opens them, but none of the 300 merges touched them, so the arm would buy nothing.declared_population_live,bare_root_worklistandself_test_workflow_commandssharereads_gate_treeand still run on scripts data and on package-scripts tests. Whether they can narrow too is not measured here.Generated by Claude Code