Skip to content

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

@os-project-manager

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)

  1. Find why pm_dispatch_gates ran 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 with OS_GATE_EVENT_NAME=pull_request OS_GATE_PR_BASE_REF=main RUNNER_TEMP=/tmp/rt bash scripts/ci/select-gate-families.sh against that PR's changed-file list (REST pulls/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, root package.json, and whatever dispatch-gates.mjs itself parses (read its population declaration) — so a diff confined to packages/** source and tests skips it. Keep the self-test's own refusal semantics (an unreadable population still refuses).
  2. 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 matching if: steps.gate-families.outputs.<id> != 'skip' in lint.yml; extend scripts/ci/select-gate-families.selftest.sh (it pins the workflow's if: set against FAMILIES) 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.
  3. Nothing changes for push to main or nightly: prove it by running the selector with OS_GATE_EVENT_NAME=push — every family run.

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 skip and each new family skip; (b) same event, changed file scripts/pm/dispatch-gates.mjs → pm_dispatch_gates run; (c) event push → every family run; (d) bash scripts/ci/select-gate-families.selftest.sh exit 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/objectstack derives 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

  1. os-project-manager commented on Sep 21, 2026

    @os-project-manager
    CollaboratorAuthor

    Claim: 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-②: 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

  2. os-project-manager commented on Sep 21, 2026

    @os-project-manager
    CollaboratorAuthor

    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/objectstack derives 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-gates was 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-debt exits 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, through node 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 this POST /repos/objectstack-ai/objectstack/issues/19498/comments through 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..HEAD and 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

  3. os-project-manager commented on Sep 21, 2026

    @os-project-manager
    CollaboratorAuthor

    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 fetched origin/main: scripts/ci/select-gate-families.sh now declares nine families (FAMILIES= line read on main) and .github/workflows/lint.yml carries nine if: steps.gate-families.outputs.… lines. Contract review of record 5755718157 (PASS on head 9a1ca2ba), provenance 5755734183. Ruling #208 R4 (#19491) is thereby in force: pm_dispatch_gates and 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 the Fixes keyword; pm:dispatched removed in this stroke so the close is whole.


    Generated by Claude Code

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions