Repository navigation
ci(lint): tooling self-tests leave the product PR path — pm_dispatch_gates and the other script self-tests run on a PR only when their own inputs change (ruling #208 on #19491) #19498
Description
Path: none | tooling | maintainer-directed (ruling #208 on #19491) — dispatched at filing, never queued
Filed and dispatched by the director seat, summon #25 (session_012GcsUbuqFGBibkEDMRC1eE), under the maintainer's verbatim direct-dispatch instruction: Ruling #208 on #19491 — maintainer 「19491 接受你的建议,并立刻派发处理相关任务。」 (2026-09-21, chat, on the director seat's 档 2 package R1–R7). Direct-dispatch channel (SKILL.md 〈多仓协调〉规则 4 「维护者直派通道」): the audit quote above is the authorization; ⛔ not a triage grading, ⛔ never queued. The full analysis and measured table are the ruling record on #19491.
Why (measured on PR #19314's head 7d67e1ee41, run 35506407130, job 「Lint & Repo Gates」)
A product PR (diff under packages/spec) paid 27.4 minutes of wall clock in 「Lint & Repo Gates」 — the longest job in its CI, above the longest test shard (19.7 min) — over 184 steps. 56 of those steps are self-tests, ratchets, corpora and censuses of the tooling, 18.6 minutes (68%); the single step 「PM dispatch-gates self-test」 (pnpm check:pm-dispatch-gates) took 11.8 minutes on a PR that changes no PM tool. The selector scripts/ci/select-gate-families.sh already scopes five families by read-set (FAMILIES='pm_dispatch_gates query_options_erasure slot_lookup verify_lock comment_mask_corpus') on pull_request and merge_group, and its own header says the full battery runs on every push to main and on nightly — so moving a self-test off the PR path loses nothing post-merge. Gate weakening is a maintainer floor; the sentence that authorizes it is the ruling quote above, and the director seat's analysis on #19491 named this item (R4) explicitly.
The change (scripts/ci/** + .github/workflows/lint.yml — neither on the governed register: Tier S)
- Find why
pm_dispatch_gatesran on spec: declaresCollection reads a pipe's authorable side, so a preprocess-wrapped collection key cannot silently leave the merge refusal set (#19150) #19314. Reproduce withOS_GATE_EVENT_NAME=pull_request OS_GATE_PR_BASE_REF=main RUNNER_TEMP=/tmp/rt bash scripts/ci/select-gate-families.shagainst that PR's changed-file list (RESTpulls/19314/files); read the family's read-set (the header, lines ~54–74: 「that are neither a manifest nor a script skip it」). Narrow the read-set to the tool's own inputs —scripts/pm/**,.github/workflows/**,.github/actions/**,turbo.json, rootpackage.json, and whateverdispatch-gates.mjsitself parses (read its population declaration) — so a diff confined topackages/**source and tests skips it. Keep the self-test's own refusal semantics (an unreadable population still refuses). - Census the remaining unconditional self-test steps in
lint.yml: every step whose subject is a script's own--self-test/ selftest / corpus agreement (e.g. 「scripts/ shared-module self-tests」, 「PM bare-root worklist self-test」, 「Self-test workflow-command gate」, 「Comment mask agrees with a real parser over the whole corpus」, the changeset-family gate self-tests) — list them with the measured minutes from the job above. ⛔ Product ratchets and censuses stay unconditional (query_options_erasure,slot_lookup, the tenancy / tenant-audit censuses guard product code). For each self-test step, add it to the selector as a family with a read-set of its own inputs (its script, the files it reads), and the matchingif: steps.gate-families.outputs.<id> != 'skip'inlint.yml; extendscripts/ci/select-gate-families.selftest.sh(it pins the workflow'sif:set againstFAMILIES) so the pin covers the new ids. If a step's cost is under 0.3 minutes, leave it unconditional and say so — the list, not the principle, decides. - Nothing changes for
pushto main or nightly: prove it by running the selector withOS_GATE_EVENT_NAME=push— every familyrun.
Acceptance
- Selector dry runs quoted in the PR body: (a) event
pull_request, changed files = spec: declaresCollection reads a pipe's authorable side, so a preprocess-wrapped collection key cannot silently leave the merge refusal set (#19150) #19314's list →pm_dispatch_gates skipand each new familyskip; (b) same event, changed filescripts/pm/dispatch-gates.mjs→pm_dispatch_gates run; (c) eventpush→ every familyrun; (d)bash scripts/ci/select-gate-families.selftest.shexit 0. - Expected: 「Lint & Repo Gates」 on a product PR ≤ 16 minutes (the director seat measures it on the first product PR after landing and records the number on this card). State the expected number and the steps moved, with their minutes, in the PR body.
- Every family
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives for this diff (workflow lint, self-test-wired, gate-population, ci-filter parity), exit codes captured before any pipe;skip-changeset.
Out of scope
Deleting or rewriting any self-test; touching scripts/pm/**; the patrol workflow (sibling card); the charter (sibling card).
Generated by Claude Code
Activity
os-project-manager commentedon Sep 21, 2026
Claim: objectstack#19498 — director seat, summon #25,
session_012GcsUbuqFGBibkEDMRC1eE, 2026-09-21T04:12ZBranch:
claude/issue-19498-self-tests-off-pr-pathSeat: director
Mode: subagent
Clause-②: no
Direct dispatch under the maintainer's verbatim 「19491 接受你的建议,并立刻派发处理相关任务。」 (ruling #208 on #19491). File surface declared for the in-flight intersection check: scripts/ci/select-gate-families.sh, scripts/ci/select-gate-families.selftest.sh, .github/workflows/lint.yml (if: lines on self-test steps). Concurrency: three maintainer-directed tooling devs in flight at once for this ruling (#19496, #19497, #19498), disjoint file surfaces; the ≤1 tooling dev rule of ruling #202 B yields to the maintainer's 「立刻派发」 for this batch only. Review: at-tier, diff-scoped, CI read from check-runs (R5's shape, applied to these PRs first).
Generated by Claude Code
os-project-manager commentedon Sep 21, 2026
os-dev-report
{
"issue": 19498,
"status": "done",
"branch": "claude/issue-19498-self-tests-off-pr-path",
"pr": "#19511",
"head": "9a1ca2bab200fd924323fa4b2af5164641d03359",
"session": "session_012GcsUbuqFGBibkEDMRC1eE",
"reported_at": "2026-09-21T04:59Z",
"premise_still_valid": true,
"summary": "Found why pm_dispatch_gates ran on PR #19314: its read-set carried the battery's whole-tree censuses, so any masked source, any .sh file, any nested .gitignore and - through the tracked-NAME sweep - any ADDED path anywhere ran it; #19314's three paths hit that rule three times over. Narrowed the family to the tool's own inputs (the workflow tree and composite actions, gate sources under scripts/ and a workspace package's own scripts/, the package.json that names a check:* script, .claude/, skills/, AGENTS.md, CLAUDE.md, root config), censused the job's other unconditional steps from the real step timings, and put the four tooling self-tests above 0.3 min behind the selector as families with their own read-sets (entry_guard 0.62, self_test_workflow_commands 0.48, declared_population_live 0.40, bare_root_worklist 0.32) plus their if: lines in lint.yml; product ratchets and censuses are untouched and ESLint is not scoped. The selector self-test grew from 30/120 at its floor to 44 cases / 228 checks (floor moved to 42/220) and now pins nine ids, the new read-sets, the #19314 shape under both events, and one pin_step per new family. push and the hourly schedule still take the unscoped branch and run all nine; the weaker claim a tooling-self-test skip makes is stated in the selector header, on the scoped steps, and in the PR body rather than papered over.",
"tests": "Every family
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives for this diff (51 commands) was run locally, each exit code captured before any pipe by a runner recording rc, index, seconds and command: 50 exit 0 - including check-self-test-wired(+self-test), check-self-test-workflow-commands(+self-test), check-step-collectors(+self-test), check-aggregator-roster, check-ci-filter-parity, check:declared-population-live, check:entry-guard, check:watch-hint-literal, check:required-contexts, check:workflow-status-functions, check:workflow-step-name-quoting, check:bash32-floor, check:nul-bytes, check:select-gate-families and bare-root-worklist --self-test.pnpm check:pm-dispatch-gateswas run detached per its own header and passed: 'dispatch-gates self-test: 1883 cases pass.' / 'the battery took 1056.1s on this box.', zero failure marks in the log. NOT MEASURED:pnpm check:type-check-debtexits 3 = PREREQUISITE NOT MET in a fresh worktree (31 workspace deps have no built dist), which the gate's own text declares is neither a pass nor a finding; this diff touches no TypeScript and CI builds the closure before that step. Selector dry runs, all exit 0, quoted verbatim in the PR body: (a) pull_request with PR #19314's exact file list, reproduced in a throwaway detached worktree off origin/main c9b23cd and driven with this branch's selector -> '3 run, 6 skipped', pm_dispatch_gates and all four new families skip, the two ratchets and the corpus run; (b) same event, changed file scripts/pm/dispatch-gates.mjs -> '6 run, 3 skipped', pm_dispatch_gates run; (c) push -> '9 run, 0 skipped', every family 'event push is not scoped -- the full battery runs'; (d)pnpm check:select-gate-families-> 'all 44 cases passed (228 checks)', VERDICT command-exit=0. This PR's own diff under pull_request runs all five tooling families. REVERSE VERIFICATION (one-shot, after the implementation was committed, throughnode scripts/ablation-replace.mjs --file .github/workflows/lint.yml --anchor 'the entry_guard if: line, verbatim' --delete -- pnpm check:select-gate-families): the mutation was proved on disk (anchor 1 -> 0, blob 01a87d77814e -> b531650650b0), the self-test turned RED with 'FAIL the workflow scopes exactly the families the script decides', 'FAIL each family gates exactly one step', 'FAIL entry_guard gates the step running: pnpm check:entry-guard', 'SELFTEST FAILED (44 cases, 228 checks)', command exit 1; the restore was verified by the tool (blob == HEAD 01a87d77814e and git diff HEAD empty) and by a clean git status afterwards. That ablation ran AFTER the PR body was written and a dev writes the body once, so it is not in the body - the seat can append it under Acceptance as a fifth block.","mcp_calls": "0 - no MCP GitHub tool was called, read or write.",
"api_writes": "4 -
git push -u origin claude/issue-19498-self-tests-off-pr-path(empty branch, the routing probe),git push origin claude/issue-19498-self-tests-off-pr-path(the one commit),POST /repos/objectstack-ai/objectstack/pulls(draft, HTTP 201, PR 19511; the body was read back and is byte-identical to what was sent apart from a trimmed trailing newline, one session-URL footer, no sanitizer mutation), and thisPOST /repos/objectstack-ai/objectstack/issues/19498/commentsthrough scripts/pm/post-stamped.mjs. No labels, no ready flip, no auto-merge, no assignee, no issues filed. Everything else was a REST GET: the card, its comments, ruling comment 5755284987, the check-runs of head 7d67e1e, actions/jobs/106066910262 for the step timings, pulls/19314/files, and the PR read-back.","open_questions": [
{
"question": "The dispatch asked for
Claude-Session:plus 'the card relation trailer the hook prescribes', but scripts/check-commit-card-trailers.mjs prescribes the opposite: a card relation in a commit message is one of its two finding classes, the PR body being a relation's only carrier. I followed the hook and os-dev.md's standing clause, so the commit carries no card trailer;node scripts/check-commit-card-trailers.mjs --range origin/main..HEADand the pre-push hook both passed. Flagging the wording so the next dispatch does not send a dev into a refusal.","options": [
"A - the dispatch meant the model-free trailer PAIR (Claude-Session + Co-Authored-By) and should say so",
"B - there is a card relation trailer this seat does not know about, in which case the hook's header is wrong"
],
"recommendation": "A, because the hook's header is explicit that the pull request body is a card relation's only carrier, and the body carries Fixes 19498."
},
{
"question": "The PR body carries no maintainer-summary section. os-dev.md asks for one on a rules-layer PR, but that classification is defined over governed paths and this diff hits none (check-governed-merges --branch reports '0 of 3 path(s) hit the register'), while AGENTS.md requires GitHub artefacts to be English. Declared rather than left silent.",
"options": [
"A - none is owed: the fact-layer / rules-layer split only applies to a diff that touches a governed surface",
"B - it is owed on any rules-shaped PR, and its Chinese is the ruled exception to the English-artefact rule"
],
"recommendation": "A, because the classifying sentence is about governed paths and this PR has none; if the seat reads it as B, the section is one comment away."
}
],
"out_of_scope_findings": [
"noted, not filed: scripts/ci/select-gate-families.selftest.sh pins its battery as two TOTALS (cases / checks), the shape AGENTS.md's 'Writing a --self-test' section argues against ('one pinned TOTAL rots the moment a sibling grows'); its sibling select-shard-packages.selftest.sh looks the same. Not filed on purpose: ruling 208 R6 puts the single tooling slot on removals first and refuses new instrument work. Successor: the next author of either selector - this PR moved the floor 30/120 to 42/220 in passing, so the row is in front of them. Dedupe words: select-gate-families selftest battery floor pinned total.",
"noted, not filed: the selector's FAMILIES line declared itself 'in the order the job runs them' while listing pm_dispatch_gates first, when lint.yml runs slot_lookup first - stale prose, not a defect. Repaired inside this PR, because adding four ids to that line would otherwise have made the sentence wronger. Successor: this PR.",
"noted, not filed: docs/audits/gate-census-2026-09.md carries retire verdicts for check:select-gate-families and check:select-shard-packages and a family-scoped row for scripts/pm/check-dispatch-gates.mjs. It is a dated census, so it is left exactly as measured; a reader taking its dispositions as current would be reading a reading, not the tree. Successor: whoever next acts on that audit's worklist."
]
}
Generated by Claude Code
os-project-manager commentedon Sep 21, 2026
Landing record — director seat, summon #25 (
session_012GcsUbuqFGBibkEDMRC1eE), 2026-09-21T05:49Z. PR #19511 merged through the queue (cf464f6b495c4ce27c65fac91028a6d623a28769, 2026-09-21T05:48Z), verified on fetchedorigin/main:scripts/ci/select-gate-families.shnow declares nine families (FAMILIES=line read on main) and.github/workflows/lint.ymlcarries nineif: steps.gate-families.outputs.…lines. Contract review of record 5755718157 (PASS on head9a1ca2ba), provenance 5755734183. Ruling #208 R4 (#19491) is thereby in force:pm_dispatch_gatesand the four tooling self-test families run on a pull request only when their own inputs change, and whole on push-to-main and nightly. Expected 「Lint & Repo Gates」 on a product PR ≈ 13.8 min (was 27.4); this seat records the first measured value on #19491. GitHub closed this card on theFixeskeyword;pm:dispatchedremoved in this stroke so the close is whole.Generated by Claude Code
ci(lint): tooling self-tests run on a PR only when their own inputs c…
ci(lint): key the PM dispatch-gates family on the files its battery o…