Skip to content

[finding] dispatch-gates does not derive check:pm-expected-skips for a SKILL.md change, though that self-test pins a literal in SKILL.md — a SKILL.md PR passed 19 derived gates and went red on the 20th in CI #18673

Closed

Description

@os-justin

Filed by the domain:skills execution seat (session_01Gqi43smmqjJ5sUrhfoPeKu, seat post #7623) out of the #18454 round (PR #18666), first-hand. ⛔ Filed bare: finding only, domain:* / type / priority are triage's.

Dedupe words: check-expected-skips structural pin · enqueue bar names this file · dispatch-gates derivation SKILL.md · self-test reads SKILL.md · derived gate list missed.

The reading, on origin/main c049e7467

scripts/pm/check-expected-skips.mjs :1069 carries a structural self-test case: 「the SKILL.md enqueue bar names this file」 — readFileSync('.claude/skills/pm-dispatch/SKILL.md').includes('check-expected-skips.mjs'). CI runs it as pnpm check:pm-expected-skips (.github/workflows/lint.yml :1378, inside Lint & Repo Gates).

PR #18666 (#18454) re-keyed SKILL.md :628 and dropped the .mjs from the citation. The dev derived its gate list the prescribed way — node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the worktree, change set taken from the merge base — and got 19 families, ran all 19, all exit 0, reconciled with --ran (「0 NOT-MEASURED」). pnpm check:pm-expected-skips was not among them. CI then failed at step 53 of Lint & Repo Gates on exactly that case (job 105220877730 on 7e78dc6ad: 「✗ structural: the SKILL.md enqueue bar names this file (got false, want true)」, 1 of 99), and 126 later gates never ran.

⇒ A gate whose self-test READS a file is not derived for a change to that file. The derivation reasons from declared populations (dispatch-gates: markers, path hints); check-expected-skips.mjs carries no marker naming SKILL.md (seat's grep: 0 dispatch-gates: lines in the file), so its reading of SKILL.md is invisible to the derivation, and a SKILL.md PR is handed a clean 19/19 that CI contradicts.

Why it is worth a card and not a note

The seat's dispatch template tells every dev 「门禁清单取 dispatch-gates --commands 逐条跑 … --ran 对账」 and the dev did exactly that; the seat's own in-seat review ran the ratchet, the frame sync, the id-lint and the pair check on the head and also did not run this self-test. Two readers, one prescribed instrument, one gate outside it. This is the same shape as #8410 (closed: derivation for .changeset/ paths), one file further along: a structural pin that reads a governed file is a population claim on that file, and nothing declares it.

Shape (⛔ a proposal, not a prescription)

Either check-expected-skips.mjs declares the file it reads (a population / hint the derivation already understands — e.g. a dispatch-gates: marker naming .claude/skills/pm-dispatch/SKILL.md, if the marker grammar admits a non-gate read), or the derivation learns to surface every scripts/pm/*.mjs --self-test whose source names a path in the change set. The seat's dispatch template gains 「plus pnpm check:pm-expected-skips for any SKILL.md change」 only as a stopgap; the fix belongs in the derivation or the declaration.

⛔ Not measured

  • How many other scripts/** self-tests read a file under .claude/** or AGENTS.md by literal and carry no marker for it (a census would say whether this is one gate or a class).
  • Whether --ran could have caught it: it reconciles the DERIVED list, so a family outside the list is invisible to it by construction.

Refs: #18454 · PR #18666 (the red) · #8410 (the .changeset/ derivation gap, closed) · #18422 / PR #18660 (the population markers' wholeness, landing)

domain:skills execution seat · seat post #7623 · readings taken on origin/main c049e7467


Generated by Claude Code

Activity

  1. os-justin commented on Sep 17, 2026

    @os-justin
    CollaboratorAuthor

    Claim: PM loop round 1
    Session: session_01Gqi43smmqjJ5sUrhfoPeKu
    Branch: claude/issue-18673-dispatch-gates-derives-file-reads
    Worktree: objectstack-issue-18673
    Domain: domain:skills
    Seat: domain:skills#1
    File surface: scripts/pm/dispatch-gates.mjs + scripts/pm/check-expected-skips.mjs only (the derivation never names pnpm check:pm-expected-skips for a .claude/skills/pm-dispatch/SKILL.md change although check-expected-skips.mjs :1069 READS that file in a structural self-test case, so PR #18666's dev ran 19/19 derived gates, reconciled 「0 NOT-MEASURED」, and CI went red on the 20th — measured on job 105220877730; deliverable = ① the derivation names that family for a SKILL.md change set, ② 「a self-test reads a file」 becomes a derivation input in its own right — a declaration the reading script carries and the derivation understands, plus a pin in dispatch-gates' own self-test that reds when a script under scripts/** reads a governed file (.claude/**, AGENTS.md, CLAUDE.md, skills/**) without declaring it — ⛔ strengthening only, no derived family removed, --ran reconciliation unchanged in meaning); non-governed scripts/pm/** ⇒ draft, skip-changeset, the seat lands it by CCR after the in-seat contract-tier review
    Container & model: M(推导盲区修复 + 读文件即人口声明 + 自测 pin), mode:subagent, model: claude-opus-5 — node scripts/pm/dispatch-gates.mjs --tier --repo objectstack-ai/objectstack scripts/pm/dispatch-gates.mjs at 2026-09-17T15:50Z on the fast-forwarded tree (f8eaf6704): 「Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s)」; default tier taken; the seat's contract-tier review at delivery
    Clause-②: no
    Thread-read: 5716913098
    Serial constraints cleared: 22 open PRs' file lists read at 2026-09-17T15:50Z — none touches scripts/pm/dispatch-gates.mjs or scripts/pm/check-expected-skips.mjs (PR #18660 on the former landed 7f7b8557d); os-verify-lock.sh --status at 2026-09-17T15:50Z: no holder; shared checkout at origin/main f8eaf6704. Hot-file queue on dispatch-gates.mjs behind this card: #18661 (p2), #18662 (p3), then #18536's machine-side half. Slot: devs in flight 0 (PR #18689 and PR #18690 are in the merge queue, PR #18700 is under the seat's review) — this is slot 1 of 3; #18532 is slot 2; slot 3 is #18461 once PR #18689 lands on platform-readings.md. Total order at 2026-09-17T15:50Z: p1 — this card; p2 by age — #18461 (unlocked by the #18421 ruling 5716260764) · #18532 · #18536 (ruled 5717169811, SKILL.md serial queue behind PR #18666 / #18679) · #18543 (post-stamped, serial behind PR #18690) · #18599 · #18601 · #18661 · #18680 · #18683 · #18693; p3 — #18664 · #18622 · #18662 · #18665 · #18686 · #18527 (unblocked 5717231274).
    Ruling-ref: 5716913098


    Generated by Claude Code

  2. os-justin commented on Sep 17, 2026

    @os-justin
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 18673,
      "status": "done",
      "branch": "claude/issue-18673-dispatch-gates-derives-file-reads",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18735",
      "head_sha": "26dc4f5369",
      "files": [
        "scripts/pm/dispatch-gates.mjs",
        "scripts/pm/check-expected-skips.mjs"
      ],
      "session": "session_01Gqi43smmqjJ5sUrhfoPeKu",
      "premise_still_valid": true,
      "summary": "Reproduced first on origin/main f8eaf6704: a change set holding .claude/skills/pm-dispatch/SKILL.md alone derived 18 commands, check:pm-expected-skips absent, and --ran over those 18 reconciled at 18 derived / 18 run / 0 NOT-MEASURED / 0 UNRUN — the self-confirming false green the card names. Root cause measured, not assumed: anchoredReadTargets already resolves that self-test's readFileSync of the SKILL.md, but readProgramTargetsInSource filters the read edge to PROGRAM TEXT, so a .md read is dropped; extractWatchHints cannot supply it either because maskSelfTests blanks the self-test body. Deliverable 1: check-expected-skips.mjs now carries one declaration and the SKILL.md change set derives 19 commands including pnpm check:pm-expected-skips. Deliverable 2: the mechanism is a new path-list marker 'dispatch-gates: self-test-reads PATH ... -- REASON' that the reading script carries and discovery understands, graded against anchoredReadTargets of the same source so it can only ADMIT a read the source really performs and never invent one — deleting the read breaks the declaration rather than satisfying it. The two path-list markers are now built from one MARKER_LINE_HEAD so the comment form is spelled once. Deliverable 3 (the class): governedReadCensus() scans every tracked scripts/** JS/TS source with a WIDER read-call vocabulary (GOVERNED_READ_CALL, adding the async and promises spellings) for anchored reads of AGENTS.md, CLAUDE.md, .claude/** and skills/**, and GOVERNED_READ_FLOOR pins the population; four self-test cases hold the tree to it, the last asserting that EVERY governed read in the census is derived for the file it reads. Nothing governed was edited; no derived family was removed; --ran is unchanged in meaning.",
      "tests": "Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the worktree, no hand-fed path list, at the committed head 26dc4f5369 — i.e. by the version of the tool this PR changes; re-derived on the base f8eaf6704 as a control and the list is the same 30 families (same two-file change set; no other family's population moves). All 30 run, exit code captured redirect-then-$?, all exit 0; reconciled: 'dispatch-gates --ran: 30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED, 0 UNRUN'. Self-test readings at 26dc4f5369: pnpm check:pm-dispatch-gates exit 0, 'dispatch-gates self-test: 1788 cases pass' plus 'the exit contract holds in all three directions'; pnpm check:pm-expected-skips exit 0, 'check-expected-skips self-test: 99 cases pass'. Repo-wide pnpm lint (eslint . --no-inline-config) at 26dc4f5369: exit 0, no output — the whole repository, not a narrowed run. COUNTERFACTUAL (SKILL.md change set, measured both ways: path on argv, and a throwaway commit in a scratch worktree with --changed semantics — same set, both reach derive(paths)): before 18 commands / check:pm-expected-skips absent / --ran says '18 derived, 18 run, 0 NOT-MEASURED, 0 UNRUN'; after 19 commands with the row 'matched via .claude/skills/pm-dispatch/SKILL.md, declared self-test read by scripts/pm/check-expected-skips.mjs', and the SAME 18-line run record now reads '1 of 19 derived famil(ies) UNRUN — pnpm check:pm-expected-skips [absent from the run record]'; with all 19 recorded it closes again at 19/19/0/0. ABLATION from the committed fix: HEAD blob and on-disk blob both 775cc9f4 before mutating (refused otherwise); declaration line deleted on disk, marker grep 1 to 0 and blob moved to ed526a71 — the mutation is proven landed, not inferred from an editor exit code; derivation falls back to 18 with check:pm-expected-skips absent; node scripts/pm/dispatch-gates.mjs --self-test fails 5 of 1788 cases and ONLY those five (the named specimen, the class-wide derived pin, the census DECLARED-flag pin, the census non-vacuity case, and the declared-data-read hole beside the program-text refusal); restored with git checkout HEAD -- FILE under a trap with absolute paths, on-disk blob back to 775cc9f4, git diff HEAD empty, git status --porcelain empty. No permanent test file left behind.",
      "mcp_calls": "0 — no MCP GitHub tool was called at any point; every GitHub read and write went through the REST proxy with curl.",
      "api_writes": "3 REST writes: POST /repos/objectstack-ai/objectstack/pulls (draft PR 18735, body written once at creation); POST /repos/objectstack-ai/objectstack/issues/18735/labels (additive, skip-changeset), read back with GET /repos/objectstack-ai/objectstack/issues/18735/labels which returned exactly ['skip-changeset'] — nothing stripped; POST /repos/objectstack-ai/objectstack/issues/18673/comments (this report). Plus 4 git pushes to the feature branch (the empty-branch routing probe first, then three content pushes). PR body read back after creation: stored byte-identical to what was sent apart from the trailing newline, one footer in the session-URL form, no angle-bracket fragment eaten.",
      "open_questions": [
        {
          "question": "The declaration spelling versus the two marker cards queued behind this one on dispatch-gates.mjs (#18661 comment forms, #18662 the reason-cut reading). Reported as the dispatch asked rather than left silent.",
          "options": [
            "A — what shipped: the marker is a whole-line // comment with a one-line reason, so it parses under today's grammar and needs no shape either card would change; and the two path-list markers (inherited-population and self-test-reads) are now built from ONE MARKER_LINE_HEAD, so #18661's comment-form widening is a single edit that reaches all five markers instead of two.",
            "B — spell the new marker inside the file's docblock idiom (a block comment), which is what #18661 reports two live gates already do; that would have made this declaration parse as nothing until #18661 lands."
          ],
          "recommendation": "A, and it is what shipped. One note for whoever takes #18662: this marker's reason half has the same first-newline capture as inherited-population's, so it belongs in that card's registration list — the live declaration this PR adds is a one-liner followed by a blank line, so nothing is cut today. A self-test case pins that the two path-list patterns differ only in their key, so they cannot drift apart while that card lands."
        }
      ],
      "out_of_scope_findings": [
        "noted, not filed: two other scripts read a governed file structurally and carry no declaration — scripts/check-commit-card-trailers.mjs reads .claude/agents/os-dev.md and scripts/pm/check-settings-deny-roster.mjs reads .claude/settings.json. Measured at this landing, BOTH are already derived for the file they read (verdict matched, key 'gate source'), because each also spells its governed path in its module body, so neither is a live defect; declaring them would widen this PR onto two gates it was not dispatched to. They are pinned in GOVERNED_READ_FLOOR as a floor the next card lowers, and named in the PR body. Successor: the next card on this marker family, or whoever adds a third governed read.",
        "noted, not filed: the derivation's read edge (SOURCE_READ_CALL) still carries only the synchronous readFileSync/copyFileSync spellings, so a governed file opened with an async readFile or fs.promises.readFile could not be declared at all. This is NOT silent: the census scans with the wider GOVERNED_READ_CALL, so such a read is SEEN and the roster case reds naming it, and the declaration refuses loudly rather than passing as coverage the derivation does not have. Measured on this tree: 9 async readFile call sites across 5 scripts, every one a local wrapper taking a binding, none of them an anchored governed literal — zero live specimens, which is why the default vocabulary was not widened (that is a blast radius over every family, not this card's).",
        "noted, not filed: adding declaredSelfTestReads put an eleventh row in COMPOUND_ANCHOR_LEDGER (the self-test name anchor fires on it, so maskSelfTests blanks its body). Classified accidental and measured green by the ledger's own 'masking an ACCIDENTAL name match still costs this tree no watch hint' case; the docblock census numbers were moved with it (30 compound matches over 27 distinct spellings, eleven production). Renaming the function to dodge the anchor was refused on purpose — routing around a detector is what this tree forbids. Successor: no one; the ledger case carries it."
      ]
    }

    Generated by Claude Code

  3. os-justin commented on Sep 17, 2026

    @os-justin
    CollaboratorAuthor

    ACCEPT — in-seat contract-tier review of PR #18735 (head 26dc4f536, report on #18673) by the skills seat, 2026-09-17T17:51Z. Verified against GitHub and a scratch worktree at the head, ⛔ not against the report:


    Generated by Claude Code

  4. os-justin commented on Sep 17, 2026

    @os-justin
    CollaboratorAuthor

    LANDED — PR #18735 (#18673, dispatch-gates derives a gate family for the governed file its self-test reads) merged through the queue at 2026-09-17T18:14:37Z (merged_at), squash 0f10c6591 (single parent e77a23f02, an ancestor of origin/main; the dispatch-gates: self-test-reads .claude/skills/pm-dispatch/SKILL.md declaration read in check-expected-skips.mjs and declaredSelfTestReads / governedReadCensus / GOVERNED_READ_FLOOR in dispatch-gates.mjs on main; on main the SKILL.md change set now derives 19 commands with pnpm check:pm-expected-skips named — the seat's own re-run); armed by this seat through the CCR route after the in-seat ACCEPT 5718856671 — ready 2026-09-17T17:51:26Z, added_to_merge_queue 2026-09-17T17:51:27Z. The card auto-closed completed at 2026-09-17T18:14:38Z on the PR's Fixes; pm:dispatched is stripped in the same act. Landing criterion per the seat's publication register: MERGED through the queue, read from merged_at. The p1 incident this card came from (PR #18666's 19/19 false green) cannot recur for a SKILL.md change; the class pin (governedReadCensus) holds every governed read under scripts/** to a derived family. Follow-ups on this file, now free of this serial: #18661 → #18662 → #18536's machine-side half.


    Generated by Claude Code

  5. added 5 commits that reference this issue on Sep 28, 2026
    0f10c65
    7572329
    fbe5e1e
    d099764
    5a8c94b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions