tooling(pm): key the gate derivation on (script, args) and name the artifact-roster silences - #15081
Queued
claude[bot] wants to merge 5 commits into
Queued
tooling(pm): key the gate derivation on (script, args) and name the artifact-roster silences#15081claude[bot] wants to merge 5 commits into
claude[bot] wants to merge 5 commits into
Conversation
…cript path `dispatch-gates` keyed a discovered family on the script PATH, so CI's two invocations of one script collapsed into a single entry — and the entry kept was the plain one, because the direct matcher captured the path and dropped the argument tail. Measured on PR #14958: `lint.yml` runs `node scripts/check-tenant-audit-census.mjs --self-test` beside the plain run, the red was carried entirely by the `--self-test` invocation, and the derived list named only the invocation that was already green. A dev following the list verbatim could not see the failure mode at all. The key is now (script, args). The argv half is admitted only when this tool can render the invocation runnably — a complete run of flag-shaped tokens; a tail carrying a value, or continued onto the next line, keeps the bare path key it has today rather than being truncated into a command that cannot run. Read from this tree's workflow text: 28 scripts in `lint.yml` are invoked more than once under different argv, 41 across all workflow files, and every one of them carried a `check-` basename and so collapsed. 204 discovered families become 242; three `check-` scripts CI never invokes plainly lose a bare key that named a command nobody runs. Also: a `--self-test` invocation is never CI-measured-only. The payload-access read that classification rests on is taken from the gate's work body, which a self-test run does not execute, so suppressing it from `--commands` would hide a command a dev can run. And the usage line printed on the derivation-failure path moves `--residue` inside the alternation, so it no longer advertises the `--tier --residue` pair the CLI has refused since #14753. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
…side the derivation A family whose declared literals all name tracked FILES declares a roster — a baseline, an allowlist of the members it already has — and never a population. A list of the files that already exist can never contain one added tomorrow, so this derivation scores those families `silent` for every card in the tree, and no path a caller passes can move them. Two measured CI reds were carried by exactly that shape: `check:optional-error-sink` on PR #14866 and `check:error-code-provenance` on PR #14930, both invisible to a `--commands` harvest by construction, for every card. `artifactOnlyNote` already said all of this — per family, but only inside the silent listing, which is behind a flag no dispatch brief tells anyone to pass. The block says it where the default run shows it, and where `--commands` puts every other accounting: on stderr, so the stream a consumer executes still carries commands and nothing else. Measured before building it, over the SILENT bucket for the diff of PR #14866: 32 of 120 silent families declare only tracked artifacts, 5 of them with the roster sitting in a directory one of that card's paths is in. A minority, so the block enumerates rather than only counting, and marks the correlated subset. The block is never counted among the derived families and never merged into the runnable list. That is structural, not a filter: rosters are `silent`, and `commandsFor` reads only the matched, convention and always-runs rows. It deliberately does NOT call these gates repo-wide scanners. Whether a roster is a baseline sitting in a directory or a census taken of that directory is intent, and intent is not in the tree — the two live side by side here. The block states what is true of every member instead, and points at the producer-side remedy the residue already carries: declare the scan surface beside the roster. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
…spatch-gates-coverage
This was referenced Sep 3, 2026
…owes The (script, args) split gives a gate CI invokes both plainly and with `--self-test` two family keys, and `bare-root-worklist`'s rows are keyed on `family constant word` — so nine rows landed FRESH in one edit without any gate changing. Its self-test says so and names the remedy for a FRESH row: record a verdict. Recorded, one per new key, in a labelled section of their own. Each is the twin of a row already carrying a decision about the identical literal, in the identical file, under the identical constant, so the verdict and (where the verdict requires one) the spelling transfer whole. No count is restated: the map's own docblock bans carrying a sibling's numbers into a new row because two rows are normally two populations measured at two times, and here they are one population read through two keys — restating a figure would mint a reading this pass never took. The section also records the structural alternative and why it was not taken here: keying the sweep's dedupe on the gate SOURCE FILE rather than on the family would fold the twins, but it re-decides which family a surviving row is attributed to and would strand the existing row as STALE — a redesign of this file's keying, which is not what the FRESH remedy names. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
os-steve
marked this pull request as ready for review
September 3, 2026 21:37
os-steve
enabled auto-merge
September 3, 2026 21:38
os-steve
added this pull request to the merge queue
Sep 3, 2026
Any commits made after this event will not be merged.
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 #14880
Fixes #15036
Three commits on two files under
scripts/pm/:dispatch-gates.mjs(the derivation key, the--commandsharvest, the usage line, the self-test) andbare-root-worklist.mjs(its recorded-verdict table only — see "Patch round" at the end for why that file joined the surface). No gate script underscripts/check-*.mjsand no workflow is edited: a gate whose declared roster is its own artifacts is reported here, per gate, never fixed here.Verification head:
2412b019(git rev-parse --short HEADof the run that produced every reading below, taken after the last commit).Authored in Claude Code session
session_019RfFHiRCSs3JXLK4cwcfox(recorded in prose because a body edit normalises the footer's session URL away).Commit 1 — the derivation key is
(script, args), one entry per workflow invocationMechanism 3 on the card.
discoverFamilieskeyed a family on the script path, because the direct matcher captured the path and dropped the argument tail. CI's two invocations of one script therefore collapsed into one entry, and the entry kept was the plain one.Measured on PR #14958 by the seat that filed the mechanism: the red was carried entirely by
node scripts/check-tenant-audit-census.mjs --self-test(exit 1,1 of 19 case(s) failed) while the plain run exited 0 — and the derived list named only the plain run. The list did not merely omit a gate; it returned a pass for a family CI fails.Measurement first, as the claim asked — how common is the collapsed shape?
Read from the workflow text through the tool's own
run:body reader, never typed:.github/workflows/lint.ymlare invoked more than once under different argv.check-links.yml1,ci.yml1,cut-rc.yml3,governed-surface-guard.yml1,lint.yml28,pr-automation.yml3,prerelease-pin-watch.yml1,release-coverage-patrol.yml2,validate-deps.yml1).lint.ymlcarry acheck-basename, so every one of them collapsed. There is no multi-argv script inlint.ymlthat escaped the old key.The 28 in
lint.yml:The census pair CI runs, quoted from the workflow as it stands today (the line numbers on the card have moved; grepped by text):
And the collapse, confirmed before the edit, on the harvest for PR #14958's own diff paths — one derived entry, the green one:
The change
The key becomes
scriptplus the invocation's argv, and the argv half is admitted only when the tool can render the invocation runnably: a complete run of flag-shaped tokens. A tail carrying a value keeps the bare path key it has today rather than being truncated into a command that cannot run. Live refused specimens, none invented:check-adr-0087-registration.mjs --base "$MERGE_BASE",check-engine-split-ratio.mjs --days 90,check-test-completeness.mjs "$RUNNER_TEMP/test-core.log", andcheck-shard-attestation.mjs --emitcontinued onto two more lines that carry--job,--totaland--out.A line-continuation backslash is deliberately not a tail terminator — it stays inside the tail so
renderableArgvrefuses the whole thing. Ending the tail at the backslash was measured to be worse than the status quo: it produces--emit, which reads as a complete argv and is not. A redirection is a terminator, because the redirection belongs to the shell and not to this argv.Effect on the tree, measured: 204 discovered families become 242; 36 scripts now carry more than one family; three
check-scripts CI never invokes plainly lose a bare key that named a command nobody runs (check-dev-prereqs.mjs,check-doc-route-spelling.mjs,check-skill-frame-freshness.mjs).Before/after
--commands, on the three diffs the card was measured onRe-derived with
--repo objectstack-ai/objectstack --commandsover each PR's real file list.The delta is identical on all three (the split is a property of the workflows, not of the card). Added:
Removed, on all three:
node scripts/check-doc-route-spelling.mjs— a command CI never runs. It is invoked only as--advisoryand as--self-test, both of which are now derived in its place.⭐ The line the card is about is in the harvest for PR #14958:
node scripts/check-tenant-audit-census.mjs --self-test.One more narrowing, in the same commit and for the same reason
A
--self-testinvocation is never CI-measured-only.payloadEnvDependencereads the gate's module body with self-test bodies masked out, so the payload access that classification rests on belongs to the script's work — the invocation this one is not. Without it the split would have creatednode scripts/pm/check-governed-queue-guard.mjs --self-testand then suppressed it from--commands, hiding a command a dev can run. Cost on the tree today: zero — all twelve pre-existing self-test families scorepayloadEnvnull, and the ciOnly count is 1 before and 1 after.The #15036 member, folded in
The usage line printed on the derivation-failure path had
[--residue]outside the alternation, which is that notation's way of saying it combines with every member — including the--tierthe CLI has refused since #14753. It moves inside, attached to the three modes it really does modify:(The two placeholders are spelled FILE and PATH here; the source keeps its angle-bracket spelling.) Deleting
[--residue]instead would understate it — the flag is legal with the other three and with the plain human rendering. The line moved into a constant so the pin can run: reaching the print site needs a checkout wherechangedPathsFromGit()refuses.Commit 2 — the artifact-roster silences, named in their own block
Mechanisms 1 and 2. A family whose declared literals all name tracked FILES declares a roster — a baseline, an allowlist of the members it already has — never a population. A list of the files that already exist can never contain one added tomorrow, so the derivation scores those families
silentfor every card in the tree and no path a caller passes can move them. Both measured reds were carried by that shape:check:optional-error-sink(PR #14866) andcheck:error-code-provenance(PR #14930).Measurement first, and the decision it forced
Over the SILENT residue bucket for PR #14866's diff, at this head:
32 of 120 is a minority, not the whole or nearly the whole of the bucket, so the stop condition does not fire and the block is built and enumerates rather than only counting.
The 32, by command (
--residueon PR #14866's diff, ⛔ marks the 5 correlated ones):Where the block goes, and what it refuses to say
artifactOnlyNotealready said all of this per family — but only inside the silent listing, which is behind a flag no dispatch brief tells anyone to pass. The block says it in the default human run, directly under the reconciliation total (the placement is the claim: everything under that line is outside the answer), and on stderr in--commands, where every other accounting there goes. stdout keeps carrying commands and nothing else; a labelled block in that stream is prose for a harvest to pattern-match.--jsongets the same rows underartifactRosterSilences, so no rendering is shorter than another.⛔ The block is never counted among the derived families and never merged into the runnable list, and that is structural rather than a filter someone has to remember: roster families are
silent, andcommandsFor/familyReconciliationread only the matched, convention and always-runs rows.artifactOnlyNote's own docblock carries the measurement that the two live side by side here (check:where-matchernames one baseline and walkspackages/**;check-entry-guardnamed ten files underscripts/and walked all of it). Calling all 32 scanners would be a fabricated lead over the members for which it is false, which this file's header prices as the expensive direction. The block states what is true of every member instead — their silence is a fact about a list, not about your paths; run them or read them, never read the silence as a clearance — marks the correlated subset ⛔, and points at the producer-side remedy the residue already carries: declare the scan surface beside the roster.check:optional-error-sink, the card's own headline instance, is in the UNDETERMINED bucket (it declares no path at all), not in SILENT. The measurement the dispatch scoped to the SILENT bucket therefore reaches mechanism 2 and not mechanism 1. The block closes the harvest gap for the artifact-roster class; the undetermined class remains what it was.Two existing self-test cases repaired, both reported rather than rewritten
Both are cases whose stated invariant survives and whose implementation detail was the thing this card changes. Neither intent was rewritten.
a check- script invoked with the flag stays ONE family under its bare path key(thedualWffixture, [finding] dispatch-gates never discovers a gate whose script lacks acheck-prefix - five required-lane steps, including the one that shipped a red on PR #11397, are absent from the family universe entirely #11404). It asserted two things at once: a count (one invocation, one family — the no-double-count property thecheck-skip protects) and a key (the bare path — the defect). Old:length === 1 && names[0] === 'scripts/check-adr-0087-registration.mjs'. New: the count half is asserted alone and unchanged, and the key expectation moves to'scripts/check-adr-0087-registration.mjs --self-test', with two further cases pinning that it still resolves to the script FILE and is marked as a self-test invocation.promoting N module(s) to gate files subtracts no inherited hint from any other family. Its input was "files of self-test families", which after the split includes files that were already gate files (CI also runs them plainly), so it read a refusal that predates the self-test admission as a loss that admission caused — four false LOSTs (check-adr-links.mjs,check-self-test-workflow-commands.mjsand two more). Narrowed to the modules a self-test family is the only reason to treat as a gate file: 14 of them,subtracted: 0.because those families are in the MATCHED list instead, each one exactly oncecounted a substring, andcheck-empty-changesetis now legitimately two families. It counts per command now — no duplicate rows, and the plain command exactly once — which is the invariant it always protected.Ablation
Run from the committed state, one mutation script with a
trap … EXIT INT TERMrestore and absolute paths. Both legs proved on disk, not by exit code.Mutation leg — the derivation key put back on the script path (empty argv capture) and
--residueput back outside the alternation. Anchors: each1 hit(s) before, then deleted-text0and injected-text1for both. Worktree blob moved9733568a…→ea621093…, so the edit reached the disk.Result —
dispatch-gates self-test: 9 of 1307 case(s) failed(lock VERDICTcommand-exit 1), and the nine are exactly the cases about this change:The sixth line is worth reading twice: the live case prints the count it derived, and under ablation that count is 0, which is the proof the number is read from the workflow rather than typed.
Restore leg —
git checkout HEAD -- ABSOLUTE_PATH, thengit hash-objectback to9733568a0a40d9e6ce87293e4addf7c3023aebd1(equal to the HEAD blob),git diff HEADempty,git status --porcelainempty. No rebuild leg exists: the self-test spawns the tool by path.The commit-2 block has no separate ablation run; its refusal is pinned instead (
⛔ a roster command is not in the runnable union, whatever the block prints, driven against the realcommandsFor), and the live end-to-end case asserts on a real--commandsrun that not one block row appears in stdout.Gates
Derived at
2412b019withnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no paths — the script takes the change set from the merge base itself). 24 commands: the 20 the dispatch named, plus three--self-testfamilies this PR's own change created, plusnode scripts/pm/bare-root-worklist.mjs --self-test, which the derivation added the moment that file entered the diff. Every exit code captured before any pipe (cmd > log 2>&1; EXIT=$?); each row quotes the gate's own verdict line.node scripts/check-ci-filter-parity.mjsOK: all 135 declared cross-package glob(s) (95 unique) are covered by core or crosspkg …node scripts/check-closing-keyword-parity.mjscheck-closing-keyword-parity: OK (3 parsers agree on all 9 keywords …)node scripts/check-closing-keyword-parity.mjs --self-test✓ check-closing-keyword-parity --self-test: 24 assertions, 5 mutations of the shipped parsers each driven to red.node scripts/check-comment-mask-corpus.mjs✓ comment-mask corpus sweep: 5845 files, 0 disagree, 0 unparseable, 47.9snode scripts/check-cross-package-test-inputs.mjsOK: 25 package(s) read outside themselves, all declared …node scripts/check-self-test-wired.mjs✓ check-self-test-wired: every one of the 166 script(s) CI runs that ship a --self-test has that self-test run by CI.node scripts/check-self-test-wired.mjs --self-testcheck-self-test-wired --self-test: 4 live ledger row(s) verified … every battery at or above its pinned floor.node scripts/check-shard-attestation.mjs✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).node scripts/check-test-completeness.mjsPREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named.Its text names this exact situation: "Arrived here from the gate family scripts/pm/dispatch-gates.mjs derives? … the local reading for this gate is NOT MEASURED. ⛔ It is not a red."node scripts/check-whole-set-label-write.mjs✓ check-whole-set-label-write: 0 violations — 257 file(s) over 3 root(s) …node scripts/check-whole-set-label-write.mjs --self-test✓ check-whole-set-label-write --self-test: all cases pass (24 fixture trees + 5 refusals + 1 allowlist hatch)node scripts/pm/bare-root-worklist.mjs --self-testOK self-test: 66 live row(s), 58 unreachable as spelled, 58 recorded verdict(s) — none stale, none missing, none contradicted. 27 record(s) carry a spelling and every one of 14 distinct spelling(s) is pinned LIVE, PRECISE and COMPLETE …pnpm check:agent-test-spelling✓ check-agent-test-spelling: 0 violations — 437 file(s) …pnpm check:bash32-floor✓ check-bash32-floor: 26 tracked shell file(s) … name no bash 4+ construct …pnpm check:cli-command-ids✓ check-cli-command-ids: 324 command-id literal(s) across 114 file(s) … all resolvepnpm check:cross-package-test-inputsOK: 25 package(s) read outside themselves, all declared …pnpm check:declared-population-live✓ check:declared-population-live — 190 of 240 famil(ies) declare a path population, and every one of them reaches this tree's 8206 tracked file(s).pnpm check:entry-guard✓ check:entry-guard: 206 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 153 export bindings, 153 of them inert on importpnpm check:nul-bytescheck-nul-bytes: OK (scanned 8199 text file(s) … no raw ASCII control bytes).pnpm check:parse-guard✓ check:parse-guard: 205 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.pnpm check:pm-dispatch-gates✓ dispatch-gates self-test: 1319 cases pass.(through the shared verify lock:VERDICT command-exit 0 · held the lock 237s)pnpm check:pnpm-filter-targets✓ check:pnpm-filter-targets: 142/181 --filter occurrence(s) across 33 file(s) resolve against 79 workspace package(s)pnpm check:refd-timer-probeOK check-refd-timer-probe: 5840 source file(s) swept; the process-global timer probe is read in … and nowhere else.pnpm check:watch-hint-literal✓ check-watch-hint-literal: 48 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.pnpm lint(repo-wide ESLint,eslint . --no-inline-config)VERDICT command-exit 0 · held the lock 62scheck:declared-population-liveis worth noting on its own: it reads this tool's own family count and reports 240 families, all of whose declared populations still reach the tree — the split did not invent a family with an unreachable population.The script's own test suite is its
--self-test(pnpm check:pm-dispatch-gates, above);git grepfinds no vitest suite that exercisesscripts/pm/dispatch-gates.mjs— the three.test.tsfiles naming it do so in prose.skip-changeset: nothing is published from any released package — the diff is one file underscripts/pm/.Patch round — the coupling this PR's own change created, and the file it added
CI's
Lint & Repo Gateswent red at PM bare-root worklist self-test on the first head, and it is this PR's own coupling rather than a flake:bare-root-worklistkeys each row onfamily constant word. Splitting the derivation key gives a gate CI invokes both plainly and with--self-testtwo family keys, so the identical bare-root literal — identical file, identical constant, identical root — is reached a second time and lands as a FRESH, untriaged row. Nine of them, with no gate having changed.Followed the remedy that self-test itself names for a FRESH row — record a verdict — which is a table inside
scripts/pm/bare-root-worklist.mjs, so that one file joins the file surface and nothing else does. The nine records sit in their own labelled section. Each carries the verdict (and, where the verdict requires one, the spelling) of the row already deciding the identical literal, because a different verdict on one population would have the map assert two decisions about it.⛔ No count is restated in any of the nine. The map's docblock bans carrying a sibling's numbers into a new row, on the grounds that two rows are normally two populations measured at two times. Here they are one population read through two keys, so restating a figure would mint a reading this pass never took — the same defect the ban is written against, arriving by the one route its wording does not cover. Each
whysays that in as many words instead.The
--self-testis untouched: no case weakened, no case skipped, and it is now in this card's own derived gate list (row above), green at the new head.Not a governed surface — re-run on the final two-file list:
node scripts/pm/check-governed-merges.mjs --test scripts/pm/dispatch-gates.mjs scripts/pm/bare-root-worklist.mjs→0 of 2 path(s) hit the register. No.mdin the diff, so this stays a draft for the seat's contract-tier review.Generated by Claude Code