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

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. 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

  3. 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

  4. 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

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions