Skip to content

pm(dispatch-gates): derive a gate family for the governed file its self-test reads - #18735

Merged
os-justin merged 4 commits into
mainfrom
claude/issue-18673-dispatch-gates-derives-file-reads
Sep 17, 2026
Merged

os-justin merged 4 commits into
mainfrom
claude/issue-18673-dispatch-gates-derives-file-reads

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #18673

Clause-②: no

maskSelfTests blanks every self-test body before extractWatchHints runs, and that mask is right for the reason its own docblock measures — a self-test is made of fixture paths, and admitting them printed a gate in the MATCHED column for most of the tree. But a self-test case can also assert a STRUCTURAL fact about a real tracked file: scripts/pm/check-expected-skips.mjs asserts that the enqueue bar in .claude/skills/pm-dispatch/SKILL.md still names it. That read is not a fixture, it is a population claim on a file a card edits, and the mask dropped it with the fixtures. What it cost is on the record: a PR re-keying that bar derived its gate list with this tool, ran every derived family to exit 0, reconciled with --ran at 0 NOT-MEASURED, and Lint & Repo Gates went red on check:pm-expected-skips — the family the list did not contain. A reconciliation's zero is a statement about the DERIVED set, so a family outside that set is invisible to it by construction: a list that confirms itself is more dangerous than no list.

The derived list, before and after

Change set: .claude/skills/pm-dispatch/SKILL.md alone. Measured twice each way — once with the path fed on argv and once from a throwaway commit in a scratch worktree, --changed semantics; both derive the same set, because both reach derive(paths, …) by the same call.

derived commands pnpm check:pm-expected-skips --ran over the 18-line run record
before (f8eaf6704) 18 absent 18 derived, 18 run, 0 NOT-MEASURED, 0 UNRUN — the false green
after (26dc4f5369) 19 present 1 of 19 derived famil(ies) UNRUN — pnpm check:pm-expected-skips [absent from the run record]

The row now reads:

- pnpm check:pm-expected-skips   [lint.yml]   matched via .claude/skills/pm-dispatch/SKILL.md
  ⇢ declared self-test read by scripts/pm/check-expected-skips.mjs '.claude/skills/pm-dispatch/SKILL.md'

With all 19 run, the reconciliation closes again: 19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN. --ran is unchanged in meaning; it now reconciles a set that contains the family.

The mechanism — one declaration, one spelling

// dispatch-gates: self-test-reads PATH [PATH ...] -- REASON
#  dispatch-gates: self-test-reads PATH [PATH ...] -- REASON      (shell gates)
  • Why a declaration rather than a widening. anchoredReadTargets already resolves this read — it scans unmasked source, so the self-test's readFileSync(join(ROOT, '.claude/…/SKILL.md')) comes back from it today. What drops it is readProgramTargetsInSource's boundary: a followed read must be PROGRAM TEXT, because a gate that parses a data file it found by walking a tree would otherwise contribute every fixture it ever touched. Dropping that boundary re-admits the class the mask exists to refuse. A declaration is decidable where a widening is a guess, and it puts the fact on the file it describes.
  • ADMITTING only, never INVENTING. Every declared path is checked against anchoredReadTargets of the same source text and REFUSES (throws) on a path that is not there. The declaration can only re-admit a read the scan already sees and the boundary dropped. A declaration whose read is DELETED stops parsing and reddens every run of the tool — which is what makes the pin below unsatisfiable by deleting the read.
  • One spelling, deliberately. The two path-list markers (inherited-population and this one) are now built from one MARKER_LINE_HEAD, so the indent, comment form and key are spelled ONCE for both; a self-test case pins that the two patterns differ only in their key. Written as a // whole line with the reason on that same line, so it parses under today's grammar and survives both queued marker cards behind this one ([finding] dispatch-gates' population markers accept // and # only, so a declaration written inside a BLOCK comment parses as NOTHING — two live gates' examined-and-explained status is dropped with no tell #18661, comment forms; [finding] dispatch-gates: NO_CHECK_FAMILIES_MARKER and INHERITED_POPULATION_MARKER's reason half carry the same first-newline capture #18422 repaired for the three population markers — no live cut today, exposure only #18662, the reason-cut reading) whichever way they land.
  • The read edge carries a label. Discovery records declared-self-test in a new entry.readEdge map beside entry.readOrigin (the counterpart of hintEdge), and coveringKey prints declared self-test read by FILE rather than program text read — a dev reading the row has to know which claim to go check.

The class, not the instance — the census pin

governedReadCensus() scans every tracked scripts/** JS/TS source for an anchored read of a GOVERNED surface (AGENTS.md, CLAUDE.md, .claude/**, skills/**) and reports each (script, file) pair with whether the script declares it. Its eyes are deliberately WIDER than the derivation's follow: it scans with GOVERNED_READ_CALL, a strict superset adding the asynchronous and promises spellings, so a governed read the follow cannot carry is REPORTED rather than silently absent. GOVERNED_READ_FLOOR pins the population; four self-test cases hold the tree to it, and the fourth is the deliverable stated over the whole class: every governed file a script structurally reads is DERIVED for the family that runs that script, however it is spelled.

The measured census on this tree is three rows:

script governed file declared derived today, via
scripts/check-commit-card-trailers.mjs .claude/agents/os-dev.md no gate source
scripts/pm/check-expected-skips.mjs .claude/skills/pm-dispatch/SKILL.md yes declared self-test read
scripts/pm/check-settings-deny-roster.mjs .claude/settings.json no gate source

The two undeclared rows are the named list this PR does NOT declare: both already spell their governed path in their MODULE BODY as well as in their self-test, so extractWatchHints sees it and the derivation already names the family for a card touching that file — measured, both ways, at this landing. They cost nothing today, and declaring them would widen this PR onto two gates it was not dispatched to. They are a floor the next card lowers, and the roster reds in every direction that matters: a read that disappears, a new governed read, and a declared flag that moves either way.

Ablation, from the committed fix

Blob-verified at HEAD:scripts/pm/check-expected-skips.mjs = on-disk 775cc9f4, the declaration line deleted on disk (marker grep 1 to 0, blob ed526a71 — the mutation is proven landed, not inferred from an editor exit code), then:

  • derivation for the SKILL.md change set: back to 18 commands, check:pm-expected-skips absent;
  • node scripts/pm/dispatch-gates.mjs --self-test: 5 of 1788 cases fail, and only these five —
    • check:pm-expected-skips is derived for the SKILL.md its self-test reads (no key)
    • every governed read in the census is DERIVED for the file it reads — reading: check:pm-expected-skips, .claude/skills/pm-dispatch/SKILL.md, verdict silent
    • each pinned row's DECLARED flag still reads off the file (pinned declared=true, live declared=false)
    • the census is not vacuous — it finds the declared row AND undeclared ones
    • …and the 0 DECLARED data read(s) are the only hole in it, each one really carried (none)

Restored with git checkout HEAD -- FILE: on-disk blob back to 775cc9f4, git diff HEAD empty, git status --porcelain empty. The restore is proven by blob identity and an empty diff, never by an exit code.

Self-test readings, at 26dc4f5369

pnpm check:pm-dispatch-gates   exit 0   ✓ dispatch-gates self-test: 1788 cases pass.
                                        ✓ check:pm-dispatch-gates --self-test: the exit contract holds in all three directions.
pnpm check:pm-expected-skips   exit 0   ✓ check-expected-skips self-test: 99 cases pass

Derived gate list, with exit codes

Derived from this worktree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at the committed head 26dc4f5369 — that is, 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, since the change set is the same two files and neither family membership nor any other family's population moves. All 30 run, exit code captured with redirect-then-$?:

node scripts/check-ci-filter-parity.mjs                      exit 0
node scripts/check-closing-keyword-parity.mjs                exit 0
node scripts/check-closing-keyword-parity.mjs --self-test    exit 0
node scripts/check-comment-mask-corpus.mjs                   exit 0
node scripts/check-declaration-mirrors.mjs                   exit 0
node scripts/check-declaration-mirrors.mjs --self-test       exit 0
node scripts/check-scripts-symbol-anchors.mjs                exit 0
node scripts/check-scripts-symbol-anchors.mjs --self-test    exit 0
node scripts/check-self-test-wired.mjs                       exit 0
node scripts/check-self-test-wired.mjs --self-test           exit 0
node scripts/check-self-test-workflow-commands.mjs           exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test  exit 0
node scripts/check-whole-set-label-write.mjs                 exit 0
node scripts/check-whole-set-label-write.mjs --self-test     exit 0
node scripts/pm/bare-root-worklist.mjs --self-test           exit 0
pnpm check:agent-test-spelling                               exit 0
pnpm check:bash32-floor                                      exit 0
pnpm check:cli-command-ids                                   exit 0
pnpm check:cross-package-test-inputs                         exit 0
pnpm check:declared-population-live                          exit 0
pnpm check:driver-memory-census                              exit 0
pnpm check:entry-guard                                       exit 0
pnpm check:nul-bytes                                         exit 0
pnpm check:parse-guard                                       exit 0
pnpm check:pm-dispatch-gates                                 exit 0
pnpm check:pm-expected-skips                                 exit 0
pnpm check:pnpm-filter-targets                               exit 0
pnpm check:ratchet-remedy-authority                          exit 0
pnpm check:refd-timer-probe                                  exit 0
pnpm check:watch-hint-literal                                exit 0

Reconciled: dispatch-gates --ran: 30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED, 0 UNRUN.

Repo-wide pnpm lint (eslint . --no-inline-config) at 26dc4f5369: exit 0, no output. Not a narrowed run — the whole repository, so no narrowing has to be argued.

skip-changeset: the diff is two files under scripts/pm/, which no package's files[] publishes; nothing released moves.

Acceptance notes


Generated by Claude Code

…lf-test reads

`maskSelfTests` blanks a self-test body before the hint scan runs, so a
structural case asserting something about a real tracked file loses its
population claim with the fixtures. `check-expected-skips.mjs` asserts that
the enqueue bar in `.claude/skills/pm-dispatch/SKILL.md` still names it, and a
SKILL.md change derived 18 families without `check:pm-expected-skips` — a list
that reconciled against itself at 0 NOT-MEASURED while CI went red on the
nineteenth.

Adds the `dispatch-gates: self-test-reads <path> ... -- <reason>` declaration,
graded against the reads the source really performs, so it can only re-admit a
read the scan already sees and cannot be satisfied by deleting the read; wires
it into discovery as a `declared-self-test` read edge; and pins the CLASS with
a governed-read census over `scripts/**` held to a frozen floor.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
@os-justin os-justin added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude
@os-justin
os-justin marked this pull request as ready for review September 17, 2026 17:51
@os-justin
os-justin added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 0f10c65 Sep 17, 2026
37 checks passed
@os-justin
os-justin deleted the claude/issue-18673-dispatch-gates-derives-file-reads branch September 17, 2026 18:14
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… newline is not a lost byte (objectstack-ai#18758)

Fixes objectstack-ai#18709

`Clause-②: no`

`post-stamped` exited 4 (`EXIT_NOT_STORED`) on every artefact a seat
wrote carrying its own attribution footer — the block this fleet's
harness rule requires verbatim on every GitHub post. The platform stores
such a body with its trailing newline **moved** to before the footer's
own rule: sent `X\n\n---\n_Generated by …_\n`, stored
`X\n\n\n---\n_Generated by …_`. Equal length, one byte moved, zero lost,
byte-identical before the offset. `footerReAnchoring` recognised only an
**appended** footer — the stored body being the sent body, or its
newline-trimmed form, followed by exactly `PLATFORM_COMMENT_FOOTER` — so
a body that *already ended* in the footer matched neither arm, the first
difference landed inside the sent body, and `sentBodyLanded` read the
move as a lost byte. A write that landed whole, reported as a write that
did not: a caller obeying the new contract ("exit 4 ⇒ read the artefact,
do not retry") stops on every write, and one that ignores it has learned
to ignore exit 4 — the state PR objectstack-ai#18690 existed to end. It also induces
duplicate writes. This is the reverse-direction residue of the fix for
objectstack-ai#18663: a predicate tightened on one side must be re-measured on the
other.

## The before-readings, from the live artefacts

Fetched through the REST proxy and fed to `classifyReadBack` /
`sentBodyLanded` on `main` `30bac2880`. The sent side is the
landing-record shape (the body as the seat sent it, ending in the footer
block plus one newline); each row's byte counts and first-differing
offset reproduce the reading recorded on the card independently.

| artefact | mode | sent | stored | first difference | class |
`footerReAnchored` | `sentBodyLanded` | exit |
|---|---|--:|--:|--:|---|---|---|--:|
| comment 5717446818 on objectstack-ai#18426 | `--comment` | 1591 | 1591 | byte 1534 |
`mutated` | `false` | `false` | **4** |
| comment 5718507419 on objectstack-ai#6023 | `--comment` | 3633 | 3633 | byte 3576 |
`mutated` | `false` | `false` | **4** |
| body of objectstack-ai#18739 | `--body` | 6255 | 6255 | byte 6198 | `mutated` |
`false` | `false` | **4** |
| body of objectstack-ai#18740 | `--body` | 5155 | 5155 | byte 5098 | `mutated` |
`false` | `false` | **4** |

The tail on every one of them, byte-exact:

```
sent   tail: ' … act.\n\n---\n_Generated by [Claude Code](https://claude.ai/code)_\n'
stored tail: ' … act.\n\n\n---\n_Generated by [Claude Code](https://claude.ai/code)_'
```

Two measurement notes, both of which change what a reader should
conclude:

- **The simpler shape is not the live one.** Sent body plus footer plus
newline against stored body plus footer, with no blank-line insertion,
classifies as `trailing-newline-stripped` and has always exited 0.
Reproducing that shape offline proves nothing about this defect.
- **The `--body` half needed no mode gate of its own, and the card's
evidence comment is imprecise on the mechanism.** Comment 5718793488
reads `classifyReadBack`'s "the comment-only footer rule must be asked
for by name" as "in body mode the footer rule is not consulted at all".
Measured: that gate is on the **class** alone. `classifyReadBack`
records `footerReAnchored` whatever the mode, and that is the field `$?`
is decided from — `mode: 'body'` on the appended shape already answers
`footerReAnchored: true`, `sentBodyLanded: true`. So the two halves
really are one bug, the conclusion in that comment holds, and widening
the one predicate reaches both modes in one edit with no mode gate added
or removed. The misreading is now said out loud in the function's own
docblock, because it has been made.

A fifth artefact, comment 5718451961 on objectstack-ai#17183, shows the same shape but
reads back at 5026 bytes today against the 3480 recorded — it was
rewritten after that reading, so it is **not** pinned. Recorded here as
a measurement note, not a finding.

## The predicate: two arms, both exact bytes

`footerReAnchoring` now answers which of two **measured** shapes the
difference is, or `null`:

- **`appended`** — unchanged. The sent body carried no footer of its
own, and the stored body is it, or its newline-trimmed form, followed by
exactly `PLATFORM_COMMENT_FOOTER`.
- **`re-anchored`** — new. The sent body already ended in the footer
block with trailing newline(s) after it, and the stored body is that
same body with those newlines removed and exactly one newline inserted
immediately before the block.

Both arms are one `===` against a candidate **built from the sent
bytes** — never a pattern, never a length. Everything before the block
and every byte of the block itself is compared literally. The re-anchor
arm additionally requires the sent body to have carried trailing
newline(s) of its own: the moved newline is one the act sent, and a
stored body that gained a newline from nowhere is a cell nobody has
measured.

The **class vocabulary is untouched**. The re-anchored shape stays
`mutated` in both modes, because `footer-appended` says "the stored body
is that body **plus** exactly the platform's comment footer" and a moved
newline added nothing — naming it with that word would make the
vocabulary say something untrue about the bytes. `--json` still reports
`body_mutated: true`. What changed is what `$?` says, and one sentence
of the status line.

**The status line.** For a difference this tool has already measured as
benign, the read-back's first line no longer carries the prescription
"Read the artefact before trusting it: the sanitizer eats tag-shaped
fragments" — it reads "the platform NORMALISED its own footer block and
took nothing away. Class MUTATED, and nothing here to go read the
artefact for", and the third line names which normalisation it was. That
prescription is what sent the skills seat to re-read every exit-4
artefact by hand since the first shape landed; the header now records
that interim reading as retired, and why nobody should make it by eye
again (by eye it cannot tell a moved newline from a substitution the
same length).

**Not folded in: objectstack-ai#18693.** Read it. Its remedy is to move the body-mode
footer append and the trailing-rule re-anchor out of `mutated` into a
benign **class**, and to re-pin the control at the old :1662. That is a
different decision on a different surface — the class vocabulary, the
`--json` field and the header's "unmeasured cell" declaration are all
untouched here, and the pinned control that the body-mode append stays
`mutated` still passes. It remains the next card on this file.

## The pins

New battery, `the re-anchored footer: a newline the platform MOVED is
not a byte lost`, 39 cases, floor pinned at 39 (probed: at 40 the floor
fires and names the battery, so 39 is exact and not merely under).

- **The live read-backs.** Each of the four artefacts above is pinned
with its own byte count, its own bytes at and around the difference (the
last 24 bytes before the block, taken from the fetched artefact rather
than retyped — including the multi-byte tails of objectstack-ai#18739/objectstack-ai#18740 and
5718507419), and the offset that read-back recorded. Each asserts: both
sides equal the recorded byte count; the first difference is at the
recorded byte; exit 0; `footerReAnchored` true with shape `re-anchored`.
The shared head is filler because every byte of it is identical on both
sides by construction, and the pinned offset is what proves
`firstDifferingByte` walked all of it.
- **Both modes**, on the same bytes, plus a caller naming no mode at
all.
- **The appended arm kept**: it still names itself `appended`, the
comment-mode `footer-appended` class still answers to that arm alone,
the body-mode append still exits 0 with its warning kept, and its line
still says "appended", never the re-anchor's words.
- **The exit-4 controls**, none of which loosened: a byte **lost**
before the rule; a byte **changed** before the rule at equal length
(with an explicit assertion that the two sides *are* equal length, so
"equal length" is demonstrably not the test); a loss **inside** the
footer block; a footer whose link was rewritten; a sanitizer chew
**under** a moved newline (the re-anchor masks nothing); a newline that
came from nowhere; a newline inserted somewhere other than immediately
before the block; a truncation that happens to end in the block. All
exit 4.
- **`unreadable` stays exit 0 and UNVERIFIED**, as the header declares —
this rule did not widen into it.
- **The status line**: names the re-anchor, drops the prescription,
keeps the offset line whole.

## The ablation

Reverting the one arm that recognises the moved-newline shape, at
`9dd5db202a`:

```
HEAD blob hash: 219700f
marker occurrences BEFORE mutation: 1
marker occurrences AFTER mutation:  0
mutated blob hash: 4d7121495543616d9c14ce4386871d36ae7e82a4
ON-DISK PROOF: marker 1 -> 0, hash 219700f… -> 4d71214955…
ABLATED self-test exit: 1
✗ post-stamped self-test: 14 of 347 case(s) failed, 0 floor problem(s).
restored blob hash: 219700f
git diff HEAD after restore: (empty — no path listed)
RESTORED self-test exit: 0
✓ post-stamped self-test: 347 cases pass across 13 batteries
```

**Exactly the new cases go red and the old ones stay green.** 14 of 347
fail, all 14 in the new battery, all of them about the re-anchored
shape: for each of the four live artefacts, "it LANDED: exit 0" and
"measured as the re-anchor by name"; plus "ONE predicate covers both
modes", "a caller naming NO mode gets it too", the three status-line
cases, and "the predicate answers the SHAPE". The other 333 pass — every
one of the 308 pre-existing cases, and 25 of the 39 new ones. The 25 new
survivors are the right ones: the byte-count and offset assertions are
properties of the fixture, and **every exit-4 control stays green with
the arm deleted**, which is what a control is for — a control that went
red here would have been testing the arm, not the contract. 0 floor
problems: the battery still registers 39, so nothing was hidden by a
case that stopped running.

Restore is proven two ways, by blob hash equality against `HEAD` and by
an empty `git diff HEAD`, not by an exit code; the script carries `trap
… EXIT INT TERM` with absolute paths and treats an empty hash as
failure.

## Self-test

308 cases across 12 batteries → **347 across 13**.

## Derived gates

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the worktree, no hand-fed path list;
change set 1 path, `scripts/pm/post-stamped.mjs`. All 29 run at
`9dd5db202a`, each exit code captured with redirect-then-`$?`, **all
0**:

```
0  node scripts/check-ci-filter-parity.mjs
0  node scripts/check-closing-keyword-parity.mjs
0  node scripts/check-closing-keyword-parity.mjs --self-test
0  node scripts/check-comment-mask-corpus.mjs
0  node scripts/check-declaration-mirrors.mjs
0  node scripts/check-declaration-mirrors.mjs --self-test
0  node scripts/check-scripts-symbol-anchors.mjs
0  node scripts/check-scripts-symbol-anchors.mjs --self-test
0  node scripts/check-self-test-wired.mjs
0  node scripts/check-self-test-wired.mjs --self-test
0  node scripts/check-self-test-workflow-commands.mjs
0  node scripts/check-self-test-workflow-commands.mjs --self-test
0  node scripts/check-whole-set-label-write.mjs
0  node scripts/check-whole-set-label-write.mjs --self-test
0  node scripts/pm/bare-root-worklist.mjs --self-test
0  pnpm check:agent-test-spelling
0  pnpm check:bash32-floor
0  pnpm check:cli-command-ids
0  pnpm check:cross-package-test-inputs
0  pnpm check:driver-memory-census
0  pnpm check:entry-guard
0  pnpm check:nul-bytes
0  pnpm check:parse-guard
0  pnpm check:pm-dispatch-gates
0  pnpm check:pm-post-stamped
0  pnpm check:pnpm-filter-targets
0  pnpm check:ratchet-remedy-authority
0  pnpm check:refd-timer-probe
0  pnpm check:watch-hint-literal
```

Reconciled with `--ran`: **29 derived, 29 run, 0 NOT-MEASURED, 0 UNRUN**
— a derived zero, since all 29 records carry an exit code. Repo-wide
`pnpm lint` (`eslint . --no-inline-config`): **exit 0**, no findings.
`origin/main` was merged twice while this ran and the derived family
list came back byte-identical both times (29, no additions, no drops),
including across PR objectstack-ai#18735's change to `dispatch-gates.mjs` itself; the
last derivation and every reading above are from `9dd5db202a` with a
clean tree. `origin/main` keeps moving faster than a local sweep — CI on
this PR is the authority on convergence.

`skip-changeset`: `scripts/pm/**` is not shipped by any package's
`files[]`; nothing published moves.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants