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
Activity
- addedpriority:p1High: required for production / M2High: required for production / M2
on Sep 21, 2026 os-project-manager commented
on Sep 21, 2026 CollaboratorAuthorMore actionsClaim: objectstack#19498 — director seat, summon #25,
session_012GcsUbuqFGBibkEDMRC1eE, 2026-09-21T04:12Z
Branch:claude/issue-19498-self-tests-off-pr-path
Seat: director
Mode: subagent
Clause-②: noDirect 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 commented
on Sep 21, 2026 CollaboratorAuthorMore actionsos-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 familynode 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 forClaude-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 commented
on Sep 21, 2026 CollaboratorAuthorMore actionsLanding 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
- added a commit that references this issue
on Sep 28, 2026 - added a commit that references this issue
on Oct 9, 2026
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 selectorscripts/ci/select-gate-families.shalready scopes five families by read-set (FAMILIES='pm_dispatch_gates query_options_erasure slot_lookup verify_lock comment_mask_corpus') onpull_requestandmerge_group, and its own header says the full battery runs on everypushto 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)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).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.pushto main or nightly: prove it by running the selector withOS_GATE_EVENT_NAME=push— every familyrun.Acceptance
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.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