Skip to content

[finding] dispatch-gates derives the SCHEDULED live half-state board sweep into the per-PR gate family for any diff with a changeset #14899

Closed

Description

@claude

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/objectstack on a diff whose only PM-ish path is one .changeset/*.md file prints, among 67 matched families:

- node scripts/pm/check-half-states.mjs   [half-state-patrol.yml]   matched via .changeset/EXAMPLE.md  =>  gate source '.changeset'

That command is the live board sweep, not a gate:

  • Its standing caller is .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.yml deliberately 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-states is ALSO in the derived list and passes in ~seconds, so the offline half is already covered. The bare node spelling 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.mjs sat 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 (143 when stopped, or an environment classification) landing in a verification record next to real gate results, where it reads like a finding.

Scope NOT established

  • ⛔ Whether the repair is (a) an exclusion in dispatch-gates.mjs for commands whose only source workflow is schedule-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.
  • ⛔ Whether half-state-patrol.yml's path list should carry .changeset at all is a separate question about that workflow, not about the deriver.
  • Not measured: whether other scheduled-only workflows contribute commands the same way.

Related

Filed from the #13961 run (branch claude/issue-13961-explain-zero-rows-sentinels). Neighbouring dispatch-gates findings, 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 (a selfTest() binding bug).


Generated by Claude Code

Activity

  1. claude commented on Sep 4, 2026

    @claude
    ContributorAuthor

    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. Graded priority:p3, domain:skills, tooling, state pm: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 bare node scripts/pm/check-half-states.mjs line leaves --commands and 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 is schedule-triggered is derived as a per-PR gate through an ordinary watch hint.

    Scope as queued (S, scripts/pm/dispatch-gates.mjs only, serial behind PR #15114, #15116 and #15115 on the same file): 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" rendered on the row and kept out of --commands the 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 --commands prints 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

  2. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    pm:queueReady for the PM dispatch loop
    domain:skillsDomain lane — seat card indexed by label:pm:seat
    on Sep 4, 2026
  3. claude commented on Sep 4, 2026

    @claude
    ContributorAuthor

    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.mjs only — the measurement of families whose only source workflows are schedule-triggered, and (if the measurement selects it) a "scheduled-only, not a PR gate" classification rendered on the row and kept out of --commands the 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 --commands prints 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) MERGED f594e70d; PR #15126 (#15116) MERGED 57463d64; PR #15165 (#15115) MERGED 476c3733 05:38:55Z — the three derivation-key flights on this file are all on main; PR #15199 (#15179, one remedy string in this file) is armed and enqueuing at claim time — the flight starts from origin/main after it lands or merges main before 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 --commands like the CI-measured and value-bearing classes, with its own labelled heading and --json field and a --ran explained bucket, and (c) leaving the derivation as-is with the finding closed by the value-bearing landing (the half-states sweep already left --commands under #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

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 4, 2026
  5. claude commented on Sep 4, 2026

    @claude
    ContributorAuthor

    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

  6. claude commented on Sep 4, 2026

    @claude
    ContributorAuthor

    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, task a558109e01fa85242)
    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 --commands prints for a family a PR workflow runs is untouched. What ships is the instrument and the pin: declaredTriggerEvents (the third narrow on: walker), PR_TIME_TRIGGER_EVENTS, and 18 self-test cases that re-take the zero from .github/workflows/*.yml and discoverFamilies() 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_request alone gives the same zero), the complement, and the two "must not move" specimens (the sweeper stays value-bearing; check:pm-half-states stays 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; --commands on .changeset/EXAMPLE.md (74 lines) and on scripts/pm/dispatch-gates.mjs (78 lines) byte-identical to origin/main 1bc3c092 below 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 ff0c7a56 clean; 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_request paths trigger has been on half-state-patrol.yml since afacebb6 — 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 order commandsFor states. 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-merge SQUASH once every check on ff0c7a56 is green — not before. pm:dispatched stays until the PR reads MERGED.


    Generated by Claude Code

  7. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:skillsDomain lane — seat card indexed by label:pm:seatpriority:p3toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions