Repository navigation
[finding] dispatch-gates derives the SCHEDULED live half-state board sweep into the per-PR gate family for any diff with a changeset #14899
Description
Activity
Triage — skills seat (session
session_019RfFHiRCSs3JXLK4cwcfox, os-steve), 2026-09-04T00:30Z. The card carried no labels since it was filed (2026-09-03 08:42Z); it is this lane's tool. Gradedpriority:p3,domain:skills,tooling, statepm:queue. Applied and read back after this comment.State of the measurement now: PR #15114 (#15083, at the flip checkpoint) classifies
half-state-patrol.yml's invocation as value-bearing (--format=markdown --provenance="$PROVENANCE"), so the barenode scripts/pm/check-half-states.mjsline leaves--commandsand the family prints under the NOT RUNNABLE LOCALLY heading instead — the 3m09s cost this card measured is gone with that landing, by a different mechanism than the ones this card lists. That PR claims nothing on this card (its own comment says so), and this card's open question is the ROUTING half: a family whose only source workflow isschedule-triggered is derived as a per-PR gate through an ordinary watch hint.Scope as queued (S,
scripts/pm/dispatch-gates.mjsonly, serial behind PR #15114, #15116 and #15115 on the same file): measure first — how many derived families come ONLY fromschedule-triggered workflows at the current head, listed by command and workflow, and whether any of them is one a dev SHOULD run on a PR; then take the shape the measurement selects between (a) a general classification "scheduled-only, not a PR gate" rendered on the row and kept out of--commandsthe way the CI-measured and value-bearing classes are, and (c) leaving the derivation as-is with the finding closed by the #15083 landing — ⛔ no per-script exclusion (b), and ⛔ no change to what--commandsprints for families a PR workflow does run. Self-test pins the measured count from the workflow text, never a typed roster.
Generated by Claude Code
Claim: PM loop round 5
Session:session_019RfFHiRCSs3JXLK4cwcfox
Branch:claude/issue-14899-scheduled-only-families
Worktree:objectstack-issue-14899
Domain:domain:skills
File surface:scripts/pm/dispatch-gates.mjsonly — the measurement of families whose only source workflows areschedule-triggered, and (if the measurement selects it) a "scheduled-only, not a PR gate" classification rendered on the row and kept out of--commandsthe way the CI-measured and value-bearing classes are, plus the self-test that pins the measured count from the workflow text (stop on breach; explain in the report). ⛔ No per-script exclusion, ⛔ no change to what--commandsprints for a family a PR workflow does run, ⛔ no.md, no other script.
Container & model: S,mode:subagent,model: opus(tier:scripts/pm/dispatch-gates.mjs— no path-derived mandate, default opus; the shape is a measured design choice on the derivation)
Clause-②: no (the tool's rendered worklist; no contract accept/reject behaviour)
Serial constraints cleared: PR #15114 (#15083) MERGEDf594e70d; PR #15126 (#15116) MERGED57463d64; PR #15165 (#15115) MERGED476c373305:38:55Z — the three derivation-key flights on this file are all onmain; PR #15199 (#15179, one remedy string in this file) is armed and enqueuing at claim time — the flight starts fromorigin/mainafter it lands or mergesmainbefore pushing. Not governed (scripts/pm/**, no.md) ⇒ draft PR, in-seat contract-tier review, ready + auto-merge by the seat.Decision re-read (06:15Z): the triage (comment 5533933113) is the terminal scope — measure first how many derived families come ONLY from
schedule-triggered workflows at the current head, listed by command and workflow, and whether any of them is one a dev SHOULD run on a PR; then take the shape the measurement selects between (a) a general classification "scheduled-only, not a PR gate" on the row, kept out of--commandslike the CI-measured and value-bearing classes, with its own labelled heading and--jsonfield and a--ranexplained bucket, and (c) leaving the derivation as-is with the finding closed by the value-bearing landing (the half-states sweep already left--commandsunder #15083) — ⛔ not (b), a per-script exclusion; the self-test pins the measured count from the workflow text, never a typed roster.
Generated by Claude Code
os-dev-report
{ "issue": 14899, "status": "done", "branch": "claude/issue-14899-scheduled-only-families", "pr": "https://github.com/objectstack-ai/objectstack/pull/15236", "premise_still_valid": false, "summary": "Measured first, per the ruling. Of 30 workflows 15 declare schedule: and 8 of those contribute a discovered check family; of 252 discovered families 21 reach a scheduled workflow and 10 are reached ONLY through scheduled workflows - and ZERO are scheduled-only, because every patrol here declares a pull_request: trigger with a paths: filter naming its own script ('changes to the patrol itself get exercised before they merge'). Three of the ten are validate-deps.yml's, including check:override-consistency, an ordinary dependency gate - so 'its only source workflow declares a schedule' is nowhere near 'no PR runs it', and shape (a) would have withheld gates a dev owes. The zero is definition-insensitive: narrowing PR-time to pull_request alone leaves the same zero. PREMISE: the card says half-state-patrol.yml 'is not triggered by a PR'; that was already false when the card was filed - the pull_request paths trigger is present at afacebb6 (2026-08-31), the newest commit to that file at or before the 2026-09-03T08:42Z filing. The cost the card measured is separately gone via the #15083 value-bearing landing. Shape taken: (c) - the class is NOT shipped (an empty classification is a capability with nothing in it, the speculative-capability shape extractTriggerPaths already refuses for paths-ignore) - PLUS the reading that makes the deferral honest: declaredTriggerEvents (the third narrow on: walker, beside its two siblings), PR_TIME_TRIGGER_EVENTS, and 18 self-test cases that re-take the measurement from the workflow text on every run, so the day a family really is scheduled-only the pin reds on the PR that creates it, with the two exits named in the case text. No derivation consumes the reader: no row field, commandsFor untouched, --commands and the full human rendering byte-identical against origin/main on three probe paths including the card's own .changeset/EXAMPLE.md. The precedence question the ruling raises is moot (no class shipped); the one family that would have been the specimen is already withheld by the value-bearing class, which is the order commandsFor states. Neighbouring observation, deliberately NOT filed because the ruling's own predicate clears it: 4 families reach no PR-time event at all, all from cut-rc.yml (workflow_dispatch-only, not scheduled, so outside this card); 3 are already withheld as value-bearing and the 4th, check-changeset-no-major.mjs --self-test, is 0.85s offline and proves the parser for the very file a changeset diff adds - one a dev should run anyway. main moved 5 commits mid-flight and was merged before pushing; PR #15199 had still not landed at push time.", "tests": "Verification run at head ff0c7a56 (quoted in the PR body too). Derived with 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands' (no paths) AFTER the final commit; every exit code captured by redirecting first, never read through a pipe. Reconciliation: 'dispatch-gates --ran: 20 derived famil(ies) accounted for - 20 run, 0 NOT-MEASURED.' All 20 exit 0. Verdict lines: 'dispatch-gates self-test: 1371 cases pass.' (pnpm check:pm-dispatch-gates, also run through the shared lock - 'os-verify-lock: VERDICT command-exit 0 - held the lock 394s (6m34s) - waited 47s'); 'check-nul-bytes: OK (scanned 8269 text file(s) ... no raw ASCII control bytes)'; 'check:declared-population-live - 201 of 252 famil(ies) declare a path population'; 'check-self-test-wired: every one of the 168 script(s) CI runs that ship a --self-test has that self-test run by CI'; 'check:parse-guard: 208 scripts/ file(s)'; 'check:entry-guard: 209 scripts/ file(s) ... 155 export bindings, 155 of them inert on import'. Beyond the derived list, both named by the dispatch: 'node scripts/pm/bare-root-worklist.mjs --self-test' exit 0, verdict 'OK self-test: 54 live row(s), 46 unreachable as spelled, 46 recorded verdict(s) - none stale, none missing, none contradicted'; 'pnpm lint' (eslint . --no-inline-config, WHOLE REPO, not narrowed) exit 0, no output. check-test-completeness is a value-bearing family and is NOT MEASURED, in its own words. Script's own suite (rule 5): the only test of this script is its own --self-test via scripts/pm/check-dispatch-gates.mjs; the five .test.ts files git grep finds for 'dispatch-gates' name it only in comment prose, checked line by line. ABLATION, from the committed state, with a restore trap on absolute paths: removed the pull_request: trigger block from .github/workflows/half-state-patrol.yml, making its family genuinely scheduled-only. Mutation proven on disk by grep count on the anchor text (before 1, after 0) AND by hash-object differing from the HEAD blob (46579667... to c7f387fd...). 5 of the 18 pinned cases turn red, 13 stay green - the property case, the definition-insensitivity case, the complement (correctly re-counting 5 no-PR-time families instead of 4), the specimen case and the class-control - so the pin discriminates rather than reacting to any edit. NO BUILD LEG exists for this ablation and none is claimed: the tool reads .github/workflows/*.yml and the gate sources from the working tree at runtime, so no dist/ sits between the mutation and the reading. Restore proven byte-identical: restored hash equals the HEAD blob hash (non-empty, compared explicitly) and 'git diff HEAD' came back empty. The ablation ran at 45a9153b, pre-merge, which is why its output says 251 families rather than 252. Rendering before/after: measured from a second worktree at origin/main (9c1bcda3) against this head - the FULL human rendering for .changeset/EXAMPLE.md diffs to zero bytes, and --commands is byte-identical on .changeset/EXAMPLE.md (15), scripts/pm/dispatch-gates.mjs (20) and packages/spec/src/data/filter.zod.ts (50).", "mcp_calls": "0 - every GitHub read and write went through the zero-quota card payload and container REST (probed 200 before use); no MCP GitHub call was made this run", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
ACCEPT — contract-tier review of PR #15236, head
ff0c7a56(skills seat, 2026-09-04T07:15Z).Implemented-by:
claude/issue-14899-scheduled-only-families(os-dev subagent, taska558109e01fa85242)
Reviewed-by:session_019RfFHiRCSs3JXLK4cwcfox(skills seat)- Shape (c) per the ruling, with the measurement pinned live. No withholding class ships (its population is zero on this tree), no per-script exclusion (⛔ b), and what
--commandsprints for a family a PR workflow runs is untouched. What ships is the instrument and the pin:declaredTriggerEvents(the third narrowon:walker),PR_TIME_TRIGGER_EVENTS, and 18 self-test cases that re-take the zero from.github/workflows/*.ymlanddiscoverFamilies()on every run — a reading, not a roster — with non-vacuity pins (the tree really declares scheduled workflows, some really contribute families), the narrow-reading control (pull_requestalone gives the same zero), the complement, and the two "must not move" specimens (the sweeper stays value-bearing;check:pm-half-statesstays runnable). - Verified on the head, not from the report: one file, +315/−0 against merge base
9c1bcda3;node scripts/pm/check-dispatch-gates.mjs→dispatch-gates self-test: 1371 cases pass;--commandson.changeset/EXAMPLE.md(74 lines) and onscripts/pm/dispatch-gates.mjs(78 lines) byte-identical toorigin/main1bc3c092below the derivation header (the header differs only by commit sha and the STALE TREE note the PR head earns for sitting 5 commits behind main);git merge-tree --write-tree origin/main ff0c7a56clean; CI at 07:12Z: 22 success, 11 skipped, Lint & Repo Gates still running. - Premise correction accepted and recorded here: the card's "not triggered by a PR" was already false at filing — the
pull_requestpaths trigger has been onhalf-state-patrol.ymlsinceafacebb6— and the 3m09s cost it measured is gone through [finding] dispatch-gates: nine scripts whose only CI invocations carry a value or a continuation still derive under a bare key CI never runs bare — rendering workflow-variable argv is a design call #15083's value-bearing class, which is the ordercommandsForstates. The precedence question is moot with no class shipped. - Noted, not blocking: the header docblock dates the measurement at merge commit
fa8c1963, which the squash leaves reachable only through the PR ref; the file already dates readings that way (96dc446c9,57827b617).
Landing: pure code (
scripts/pm/**) ⇒ the seat flips ready and arms auto-mergeSQUASHonce every check onff0c7a56is green — not before.pm:dispatchedstays until the PR reads MERGED.
Generated by Claude Code
- Shape (c) per the ruling, with the measurement pinned live. No withholding class ships (its population is zero on this tree), no per-script exclusion (⛔ b), and what
- added a commit that references this issue
on Sep 9, 2026
Recording only, no severity asserted — routing and ranking are triage's. Found while running the derived gate family for the card #13961 on branch
claude/issue-13961-explain-zero-rows-sentinels; unrelated to that card's diff, so deliberately not repaired there.Measured
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackon a diff whose only PM-ish path is one.changeset/*.mdfile prints, among 67 matched families:That command is the live board sweep, not a gate:
.github/workflows/half-state-patrol.yml— a scheduled workflow, whose own header says the patrol exists because "a lane that has to REMEMBER to run a patrol does not run it". It is not triggered by a PR.lint.ymldeliberately runs only the offline self-test,pnpm check:pm-half-states, and says why in a comment beside the step: "The gate runs the SELF-TEST only. The live sweep (node scripts/pm/check-half-states.mjs) reads a shared board over the GitHub API, is report-only by design (a completed sweep exits 0 whether it found 0 or 40 half-states), and its non-zero exits classify the ENVIRONMENT … which is not a verdict about the PR running it."pnpm check:pm-half-statesis ALSO in the derived list and passes in ~seconds, so the offline half is already covered. The barenodespelling is a second, different command.So the derived family names a command CI never runs on a PR, and the dispatch protocol tells a dev to run the printed commands.
Cost, measured on one dev container
Running the derived list top to bottom,
node scripts/pm/check-half-states.mjssat for 3m09s with no output before it was stopped by PID; the remaining ~52 commands were blocked behind it for that whole window. It reads a shared board over the GitHub API, so a dev seat that lets it finish also spends API quota on a sweep that says nothing about its own diff — and every dev whose PR carries a changeset (which is most of them) gets this.Why it is small
The command cannot redden anything: it is report-only by construction. The damage is a dev's wall clock, some shared API quota, and a NOT-MEASURED-looking non-zero exit (
143when stopped, or an environment classification) landing in a verification record next to real gate results, where it reads like a finding.Scope NOT established
dispatch-gates.mjsfor commands whose only source workflow isschedule-triggered, (b) a narrower exclusion naming this script, or (c) leaving the derivation and teaching the dispatch protocol to skip report-only live sweeps, is not decided here. (a) is the general shape and would need its own measurement — how many derived commands come only from scheduled workflows, and whether any of them is one a dev SHOULD run.half-state-patrol.yml's path list should carry.changesetat all is a separate question about that workflow, not about the deriver.Related
Filed from the #13961 run (branch
claude/issue-13961-explain-zero-rows-sentinels). Neighbouringdispatch-gatesfindings, none of them this one: #14880 (a gate CI DOES run that is not derived — the opposite direction), #14753 and #14294 (silent flag drops), #14290 (an untraversed edge), #14688 (aselfTest()binding bug).Generated by Claude Code