Skip to content

pm(half-states): H19 founds every verdict on the set it judges - #18654

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-18379-h19-blocked-by-body-only
Sep 17, 2026
Merged

os-justin merged 1 commit into
mainfrom
claude/issue-18379-h19-blocked-by-body-only

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #18379

H19 gathered Blocked-by: targets over the body ∪ comment union and judged the expiry against the body's set (#17564), but the UNJUDGED branch never moved with that split: it stayed founded on the resolution set, so a target a seat had superseded in the body could still veto the verdict from the comment archive whenever its number was unreadable. Two conditions had to coincide, which is why it outlived #17564 — the target is superseded (judging correctly ignores it) and the superseded number 404s (resolution cannot dismiss it either) — and the resulting row was unclearable by construction: the card's owner had already done the prescribed thing, and the only remaining lever, editing the archived comments, is the write the protocol forbids because those comments record what an earlier seat measured and why the line was migrated.

Authored by session_01Gqi43smmqjJ5sUrhfoPeKu (Claude Code). Every reading below was taken on this branch at ab30706ed, the head this body describes.

The two sets, before and after

blockerTargetsFor is untouched: DISCOVERY stays a union, because H26 and H28 share those resolutions and a comment-borne target is a perfectly good answer to what they ask. What changed is inside h19BlockOutlivedBlocker.

reading may FOUND a row — before may FOUND a row — after
CLOSED, body-borne (or comment-borne on a body-less card) yes yes (unchanged)
CLOSED, comment-only beside a body set no — hygiene clause no — hygiene clause (unchanged)
UNRESOLVED, body-borne (or on a body-less card) yes — UNJUDGED yes — UNJUDGED (unchanged)
UNRESOLVED, comment-only beside a body set yes — the unclearable row no — named in its own clause
OPEN, either channel no — withholds only no — withholds only (unchanged)

The denominator moved with it: an archive-only unreadable target no longer enters the row's N of M count, which was the other half of the sentence the card quotes (「1 of 2 … the set judged against the BODY's set」 on a card whose body names one target).

What the archive is still consulted for, and must be: evidence that the wait may still be RUNNING. An OPEN target it names still withholds a discharge, and an UNREADABLE one now withholds a FULL discharge in a new branch (「No target this row JUDGED is still open, but N target(s) the comment archive names … could not be read at all」). Narrowing that half to the body too would re-create #11747 — the measured mirror defect, where a re-park parks the live blocker in a comment while the body still names the spent one, and a card was released to pm:queue while its real blocker was open and dispatched. Only the power to FOUND a verdict is carrier-scoped; evidence of a live wait never is.

⛔ UNJUDGED is not silenced. Three controls say so live, below.

Pins

Battery H19 judged-set founding — the file's second registered battery (SELF_TEST_BATTERIES), 37 cases registered, pinned at 34; SELF_TEST_BATTERY_FLOOR raised 1 → 2, so deleting either battery now fails the roster rather than shrinking a total nobody reads.

It floors a NEGATIVE — a row that must not fire — plus the controls that keep the silence scoped:

Two existing pins were re-judged rather than deleted, and both are called out where they sit:

  1. H19 precedence: an UNRESOLVED comment-borne target is NOT silenced by the body asserted exactly the row this card measures as unclearable. Same fixture, re-judged to the direction the file's own asymmetry table always declared: it now founds no row beside a body set.
  2. H19 superseded: the clause rides the UNJUDGED branch as well used a fixture whose body named a target that appeared in no resolution. Its unreadable target is now the BODY's own, which is what the pin was always about: a hygiene clause can only ride a row something in the judged set founded.

One prose correction rides along: the summary line's H19 shortfall clause promised that every unresolved target is named on its own card's row. That clause was measured false once already (#11218) by promising more than the rows deliver, and a repair that narrows what a row founds owes it a second correction — it now names the archive-only class it no longer founds a row for.

Ablation — from the committed fix, restored by blob hash under a trap

Mutation: the founding filter state === 'unresolved' && carries(r) reverted to state === 'unresolved', on disk, at HEAD = ab30706ed.

HEAD blob: fe92847f1b12633e42f47790b6a47888e0f174ee
BEFORE: fixed-line count = 1     AFTER: fixed-line count = 0
BEFORE: mutant-line count = 1    AFTER: mutant-line count = 2   (one pre-existing identical line in H4's prose clause)
AFTER:  on-disk blob = f764989fbeebeedd2d9920f7dd7678da0abdff1c   (differs from HEAD blob)
ABLATED self-test exit: 1 — 8 of 4583 cases failed
RESTORED: blob fe92847f1b12633e42f47790b6a47888e0f174ee == HEAD blob; git diff HEAD empty

Direction: RED, as predicted, and the reds are the right ones — five negatives plus three counts:

✗ H19 precedence: an UNRESOLVED comment-only target founds NO row beside a body set
✗ H19 #18379: …and the unclearable row is GONE
✗ H19 #18379: …⛔ with no quieter row invented in its place
✗ H19 #18379: a superseded target the archive alone names founds nothing
✗ H19 #18379: …and that holds for a LOCAL superseded number too
✗ H19 #18379 withhold: …and the count stays the judged set's
✗ H19 #18379 clauses: …and neither enters the count
✗ H19 #18379: the UNJUDGED denominator never counts an archive-only target

The got value on the card's own fixture is the measured sentence, reproduced verbatim by the ablated code:

pm:blocked and 1 of 2 Blocked-by: target(s) could NOT be resolved this sweep (#13515 (HTTP 404)) — the set judged against the BODY's Blocked-by: set …

Self-test

node scripts/pm/check-half-states.mjs --self-test   ::  exit 0
✓ check-half-states self-test: 4583 cases pass. Batteries: H66 released queue card 183/172, H19 judged-set founding 37/34.

Run under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0), as were the ablated run and the live sweep.

Live sweep — report-only, from this worktree, after the fix

node scripts/pm/check-half-states.mjs at 2026-09-17T12:25Z, head ab30706ed, under the verify lock (VERDICT command-exit 0, held 366s): swept 409 open pm-/p0-labeled issues, 462 half-states, 12 H19 rows.

Before — anchor #9857 as the scheduled patrol rewrote it at 2026-09-17T07:50:06Z (four H19 rows, all UNJUDGED, which sort above the body's size trim): 11973, 11975, 15140, 15214.

H19 #11975 — pm:blocked and 1 of 2 Blocked-by: target(s) could NOT be resolved this sweep (#13515 (HTTP 404)) — the set judged against the BODY's Blocked-by: set …

After — this run:

Gates

Derived from this worktree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack scripts/pm/check-half-states.mjs (list re-derived after the commit: identical), every command run, each exit code captured by redirect-then-$?, reconciled with --ran:

✓ dispatch-gates --ran: 38 derived familie(s) accounted for — 38 run, 0 NOT-MEASURED
  (a DERIVED zero — all 38 recorded an exit code and none of them is 3)

All 38 exited 0. In full:

node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-changeset-no-major.mjs --self-test :: exit 0
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
node scripts/pm/board-snapshot.mjs --self-test :: exit 0
node scripts/pm/check-governed-queue-guard.mjs --self-test :: exit 0
node scripts/pm/sweep-closed-cards.mjs --self-test :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:closing-target-claim :: exit 0
pnpm check:commit-card-trailers :: exit 0
pnpm check:cross-package-test-inputs :: 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:partof-closing-keyword :: exit 0
pnpm check:pm-dispatch-gates :: exit 0
pnpm check:pm-half-states :: 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
pnpm lint  (repo-wide, as PR #18514 did) :: exit 0

skip-changeset: scripts/pm/** is the fast lane — nothing in any package's files[] moves, and the patrol script is not published.

Acceptance notes


Generated by Claude Code

H19 resolved `Blocked-by:` targets over the body∪comment union and judged
against the body's set, but the UNJUDGED branch sat on the RESOLUTION step:
a target superseded in the body and unreadable in the archive could still
veto the verdict, producing a row no correct action by the card's owner
could ever clear.

Both founding states are now taken over the judged set. What the archive
still does — and must, per #11747 — is withhold: an OPEN target it names
still refuses a discharge, and an UNREADABLE one refuses a FULL discharge
and is named in its own clause.

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 12:51
@os-justin
os-justin added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 14d2544 Sep 17, 2026
39 checks passed
@os-justin
os-justin deleted the claude/issue-18379-h19-blocked-by-body-only branch September 17, 2026 13:12
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…tack-ai#18681)

Fixes objectstack-ai#18456

Clause-②: no

`scripts/pm/` sits outside every workspace package, and the root package
is private, so no
`files[]` can ship this diff — `skip-changeset`.

## The defect

`check-clause2-carriers --pair` is the landing pre-check every seat
runs, and on one pair
(PR objectstack-ai#17917 / card objectstack-ai#17425) it answered **0 at 02:57Z, 4 at 03:04:09Z and
0 at 03:58:33Z on
2026-09-13 with an identical script blob**. Two explanations were ruled
out with controls
(no comment on that thread was ever edited; the board is resolved from
the environment, never
from the working directory), so the cause is still UNKNOWN — and the
three runs could not be
compared, because not one of them had SAID what it read. The judging
half is already
deterministic given a fixed document (`--pair-json` proves that); what
was unpinned is **what
document the live path builds**. This states it: every `--pair` run now
closes with a fenced
`clause2 input record` block on stderr, with the same field roster on
every exit, so two runs
that disagree are settled by **diffing their two blocks** — never by
re-running until one side
wins. ⛔ No guess at the cause is dressed as a fix here: no predicate, no
state, no row, no
count and no exit code reads one character of the record, and the
judging half is untouched.

## The record's field roster

Rendered from `INPUT_RECORD_RUN_FIELDS` and `INPUT_RECORD_PAIR_FIELDS`
and from nowhere else,
so a field cannot silently disappear: a declared field this run could
not fill renders with an
explicit token rather than vanishing, and a field the builder fills that
the roster does not
declare is NAMED in the block (`record.undeclared`). Values too long for
one line continue on
indented lines under their key.

| half | fields |
|:--|:--|
| run | `record.version` · `run.utc` · `run.mode` · `run.script.path` ·
`run.script.blob` · `run.script.bytes` · `run.node` · `board.repo` ·
`board.source` · `read.plan` · `read.api` · `read.token` · `read.served`
· `read.pair-json` · `run.requests` · `pairs.derived` |
| per pair (`pair.N.`) | `pr` · `card` · `derivation` · `head-sha` ·
`card-comments` · `card-comment-ids` · `card-comment-newest` ·
`pr-comments` · `pr-comment-ids` · `pr-comment-newest` · `claim.rule` ·
`claim.selected` · `claim.rejected` · `claim.clause2-line` ·
`pr-body.clause2-line` |

Four of them are worth naming for WHY they are there:

- **`run.requests`** — every read the run issued, in order, with its
channel, its exact path
and its **row count**. A page asked for with `per_page=100` that answers
with exactly 100
rows is the one shape a truncated read and a complete one share, and
nothing printed it.
- **`claim.rule` + `claim.selected` + `claim.rejected`** — the carrier,
the rule that picked it
and every candidate it did not pick, each with its reason. That
separates "the two runs
selected different comments" from "the two runs applied different
rules".
- **`claim.selected`'s body fingerprint** (bytes + `sha256:`) — the
field the measured 0/4/0
actually needs. A `misplaced` verdict on that thread requires the
governing claim to have
carried no readable declaration while a superseded one did; same ids
with a different verdict
is only possible if the BYTES differed, and the ids were all anybody
could see.
- **`run.script.blob`** — git's blob hash of this file, beside the path
it ran from. "The blob
was identical on both sides" was a claim in the incident; it is now a
printed fact any seat
checks with `git hash-object`. On this PR's head it reads
`25d204236aa8296644813109fa77541d6efe1644`,
which is exactly `git rev-parse
HEAD:scripts/pm/check-clause2-carriers.mjs`.

The `--json` sweep carries the same record under `inputs` — the same
record, ⛔ never a second
format.

## The two live blocks the card names

`--pair 17917` — the pair from the card. Both it and objectstack-ai#18654 have since
merged, so `--pair`
answers **exit 2** on each today (the pair cannot be formed from a
closed PR). ⭐ That is
precisely the class of exit the old code said the least about, and the
block is now complete
on it:

```text
----- clause2 input record v1 -----
record.version: 1
run.utc: 2026-09-17T14:10:26.210Z
run.mode: --pair 17917
run.script.path: /home/user/objectstack-issue-18456/scripts/pm/check-clause2-carriers.mjs
run.script.blob: 25d2042 (git blob sha1 — check it with `git hash-object` on the path above)
run.script.bytes: 513169
run.node: v22.22.2
board.repo: objectstack-ai/objectstack
board.source: default — NEITHER PM_SWEEP_REPO NOR GITHUB_REPOSITORY answered
read.plan: (i) token then (ii) token-less public read
read.api: https://api.github.com (REST, accept application/vnd.github+json)
read.token: present
read.served: token=1, public=0, pair-json=0
read.pair-json: (not named — this run read the network)
run.requests: 1 read(s), in the order they were issued
  objectstack-ai#1 (i) token /repos/objectstack-ai/objectstack/pulls?state=open&per_page=100&page=1 -> HTTP 200 (21 row(s))
pairs.derived: 0 pair(s)
record.how-to-read: two runs that DISAGREE about one pair are settled by diffing their two blocks — ⛔ never by re-running until one side wins. The blob line says whether the two runs were even the same instrument.
----- end clause2 input record -----
```

`--pair 18654` — the pair this seat landed today, which answered 0 at
12:32Z and is likewise
merged now (**exit 2**):

```text
----- clause2 input record v1 -----
record.version: 1
run.utc: 2026-09-17T14:10:27.163Z
run.mode: --pair 18654
run.script.path: /home/user/objectstack-issue-18456/scripts/pm/check-clause2-carriers.mjs
run.script.blob: 25d2042 (git blob sha1 — check it with `git hash-object` on the path above)
run.script.bytes: 513169
run.node: v22.22.2
board.repo: objectstack-ai/objectstack
board.source: default — NEITHER PM_SWEEP_REPO NOR GITHUB_REPOSITORY answered
read.plan: (i) token then (ii) token-less public read
read.api: https://api.github.com (REST, accept application/vnd.github+json)
read.token: present
read.served: token=1, public=0, pair-json=0
read.pair-json: (not named — this run read the network)
run.requests: 1 read(s), in the order they were issued
  objectstack-ai#1 (i) token /repos/objectstack-ai/objectstack/pulls?state=open&per_page=100&page=1 -> HTTP 200 (21 row(s))
pairs.derived: 0 pair(s)
record.how-to-read: two runs that DISAGREE about one pair are settled by diffing their two blocks — ⛔ never by re-running until one side wins. The blob line says whether the two runs were even the same instrument.
----- end clause2 input record -----
```

⭐ `diff` of those two blocks is **four lines**: `run.utc` and
`run.mode`, twice. Same roster,
same order, same shape — which is the property the card asked for.

## A live block on exit 0

`--pair 18659` (open at the time of writing) — **exit 0**, the full pair
half:

```text
----- clause2 input record v1 -----
record.version: 1
run.utc: 2026-09-17T14:10:36.744Z
run.mode: --pair 18659
run.script.path: /home/user/objectstack-issue-18456/scripts/pm/check-clause2-carriers.mjs
run.script.blob: 25d2042 (git blob sha1 — check it with `git hash-object` on the path above)
run.script.bytes: 513169
run.node: v22.22.2
board.repo: objectstack-ai/objectstack
board.source: default — NEITHER PM_SWEEP_REPO NOR GITHUB_REPOSITORY answered
read.plan: (i) token then (ii) token-less public read
read.api: https://api.github.com (REST, accept application/vnd.github+json)
read.token: present
read.served: token=5, public=0, pair-json=0
read.pair-json: (not named — this run read the network)
run.requests: 5 read(s), in the order they were issued
  objectstack-ai#1 (i) token /repos/objectstack-ai/objectstack/pulls?state=open&per_page=100&page=1 -> HTTP 200 (21 row(s))
  objectstack-ai#2 (i) token /repos/objectstack-ai/issues/18443 -> HTTP 200
  objectstack-ai#3 (i) token /repos/objectstack-ai/issues/18443/comments?per_page=100 -> HTTP 200 (4 row(s))
  objectstack-ai#4 (i) token /repos/objectstack-ai/objectstack/pulls/18659/files?per_page=100&page=1 -> HTTP 200 (1 row(s))
  objectstack-ai#5 (i) token /repos/objectstack-ai/issues/18659/comments?per_page=100 -> HTTP 200 (1 row(s))
pairs.derived: 1 pair(s)
pair.1.pr: 18659
pair.1.card: 18443
pair.1.derivation: `closing-keyword` (via a closing keyword) — body line: Fixes objectstack-ai#18443
pair.1.head-sha: 1344eb5
pair.1.card-comments: 4 row(s)
pair.1.card-comment-ids: 5713976124,5714587497,5714873191,5715029659
pair.1.card-comment-newest: 5715029659 at 2026-09-17T13:19:56Z
pair.1.pr-comments: 1 row(s)
pair.1.pr-comment-ids: 5715030051
pair.1.pr-comment-newest: 5715030051 at 2026-09-17T13:19:57Z
pair.1.claim.rule: the GOVERNING claim — the NEWEST comment whose body carries a line beginning `Claim:`/`Claimed:` AND whose `Branch:` line parses at least one protocol-shaped branch (newest by `created_at`; an unreadable stamp or a tie falls back to thread order, later row wins). The pool is every claim comment sharing that `created_at`; when NO claim names a branch at all, every claim comment is the pool. ⛔ Not earliest, ⛔ not a session match, ⛔ not the one whose body mentions the key.
pair.1.claim.selected: 1 comment(s) in the pool
  5714587497 at 2026-09-17T12:46:45Z — 2159 bytes, sha256:795e1df6c9fd
pair.1.claim.rejected: none — every claim comment on this thread is in the pool
pair.1.claim.clause2-line: DECLARED `no` — Clause-②: no
pair.1.pr-body.clause2-line: DECLARED `no` — Clause-②: no ⚠️ stated as an INPUT only — ⛔ no row here judges the PR body; the declaration limb is judged from the card, and `check-changeset-no-major.mjs` is what reads this line.
record.how-to-read: two runs that DISAGREE about one pair are settled by diffing their two blocks — ⛔ never by re-running until one side wins. The blob line says whether the two runs were even the same instrument.
----- end clause2 input record -----
```

## Pins

Battery **objectstack-ai#18456: the `--pair` input record — the same block on every
exit, so two runs that
disagree can be diffed**, registered in `SELF_TEST_BATTERIES` with a
floor of **38**; **41**
cases register. `SELF_TEST_BATTERY_FLOOR` raised 26 → 27 by exactly the
one battery this adds.

What is pinned, in the card's own terms:

- the record is **present and complete on exit 0**, on the **exit-4
(MISPLACED)** shape and on
a **refusal that formed no pair** — all three key lists asserted equal;
- the **field roster** cannot lose a field: a declared field that was
never filled still renders
(with `INPUT_RECORD_UNSET`), an empty record still carries every
declared key, and a key
  outside the roster is named rather than printed in silence;
- the **selected-claim rule is stated**, and it is the one constant
`claimCarrierSelection`
  applies — so the printed rule cannot drift from the applied one;
- a **rejected candidate is named with its reason**, and a thread with
nothing rejected says so;
- a **`--pair-json` run names that read path as such** and names the
document;
- the body fingerprint **moves when only the bytes move** while every id
field stays identical —
  the measured shape, asserted directly;
- `gitBlobSha1` is pinned against two values `git hash-object` prints.

⛔ CONTROLS in the same battery: the block carries no verdict, no exit
code and no finding row;
building it changes no reading; and the selection the block prints IS
the pool `cardDeclaration`
judged (ONE derivation — `cardDeclaration` now calls
`claimCarrierSelection` instead of deriving
the pool inline, so the record and the verdict cannot describe two
different comments).

`--self-test` on this head: **786 cases pass, exit 0** (745 before;
+41).

## Ablation

From the committed tree, blob `25d204236aa8296644813109fa77541d6efe1644`
(= this PR's head
blob), the pair half of the record removed on disk, mutation proved
before the run, restore by
blob hash under a `trap`:

```text
HEAD blob                25d2042
before: removed-text count=1 (want 1); injected count=0 (want 0)
after : removed-text count=0 (want 0); injected count=1 (want 1)
mutated blob             e88355f70e8648f1e3d30147f0c82b7c3c157609
VERDICT ablation-mutated self-test exit=1       ← 14 cases red
  ✗ every declared PAIR field is present once per derived pair, prefixed by its index
  ✗ the SELECTION RULE is printed, not merely applied — two runs must be comparable on the rule too
  ✗ …and it is the one constant, so the printed rule cannot drift from the applied one
  ✗ the SELECTED carrier is named by id and by date
  ✗ ⭐ …with a BODY FINGERPRINT: the one field that tells "same ids, different bytes" apart
  ✗ ⭐ …and it MOVES when only the bytes move: same ids, same count, same newest, different verdict
  ✗ every REJECTED candidate is named, with the reason it is not the carrier
  ✗ …and a thread whose claims are all in the pool says THAT, rather than going quiet
  ✗ a claim that parses ZERO branches leaves NO carrier, and the block names that claim
  ✗ an UNREAD thread reads UNREAD, ⛔ never 0 rows
  ✗ the line READ from the carrier is stated — declared, near miss or nothing
  ✗ the PAIRING quotes the body line it was derived from
  ✗ …and the branch-name fallback names the head ref instead of quoting a line that does not exist
  ✗ the PR-BODY line is read and stated — ⛔ and stated as an INPUT, never as a limb
restored blob            25d2042   (HEAD 25d2042)
git diff HEAD --name-only: []
VERDICT ablation-restored self-test exit=0
```

Direction predicted before the run and observed: **turns red**. The
module is run directly from
source by `node scripts/pm/…` — no build and no `dist/` between the edit
and the run, so the
on-disk proof is the whole preflight.

⚠️ **A named gap, not a hidden one**: the battery drives the builder and
the renderer, and it
cannot see `main`'s **emission**. An ablation that deleted the two lines
in `main`'s `finally`
would come back green. What covers emission is the three live blocks
quoted above, taken on this
head across three different exits.

## Candidate cause, unproven — ⛔ not fixed here

Two readings taken while wiring the record. Neither is acted on in this
PR.

**1. On the blob all three 2026-09-13 runs ran, exit 4 was the
DETERMINISTIC answer for that
pair — so what is unexplained is the two 0s, not the 4.**

- The file's last change before those runs was `a5ed18ced`
(2026-09-12T06:05:25Z, "a key-INITIAL
clause-② line that QUOTES the spelling is not a declaration"); its next
change was
`4e3a496ba` at 2026-09-13T17:19:18Z, after all three runs. `a5ed18ced`'s
blob is
`aecbb2d86683eb908468fdacaac2ff53753f06ef` — the same blob PR objectstack-ai#18448's
body independently cites
  as "the exact blob the 2026-09-13 readings were taken from".
- The governing claim on card objectstack-ai#17425 at that moment was comment
`5650083758`
(2026-09-13T01:57:23Z). Its line 3 opens `Clause-②: no —` and then
quotes the spelling again
inside the same line. Run first-hand against **that historical blob's
own
`readClause2Line`**: `{"kind":"near-miss","reason":"describing"}`, and
`cardDeclaration` on a
one-claim thread reads `missing` — ⛔ not a declaration. Today's copy
reads it identically.
- A `--pair-json` document assembled from the REAL thread as it stood at
03:04:09Z (its 9
comments, both carriers' real label event streams) answers **exit 4,
MISPLACED** on this PR's
head, quoting the superseded `Clause-②: yes` and naming `5650083758` as
the correction target —
which is what the 03:04Z reviewer and the 02:53Z dev round both
reported.
- ⇒ The 4 is reproducible and mechanically explained. The 0s are not. ⭐
Exactly the difference
the record's `claim.selected` fingerprint and `claim.clause2-line` would
have shown, had the
  0-runs printed one.
- ⚠️ Limits of this reading: the historical module was exercised for
`readClause2Line` (self
contained) and `cardDeclaration` (which imports today's sibling
modules); the commit ordering
is read from a shallow checkout, corroborated by objectstack-ai#18448's independent
citation of the same blob.

**2. The comment read — the one the declaration limb depends on — is the
only read here with no
page discipline.** `readCardComments` issues ONE request,
`/issues/N/comments?per_page=100`, with
no `page=` ladder and no short-read check. `readCarrierEvents` and
`readPullFiles` both page to
exhaustion and answer `null` (UNJUDGED, never clean) when their cap is
hit, for the reason their
own docblocks state. A card thread past 100 comments therefore loses its
tail silently, and the
claim pool is built from whatever came back. Not the cause on objectstack-ai#17425 (7
comments at 02:53Z, 16
today), but it is a live fail-open in this reading. The record makes it
visible for the first
time: request `objectstack-ai#3` prints its row count, so a `(100 row(s))` on a
`per_page=100` request is now
readable. ⛔ Not fixed here; the seat files or re-scopes.

## Gates

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`
from the worktree with no hand-fed path list; re-derived after rebasing
onto current `main`
(the derivation was STALE-TREE by 4 commits) — **identical command
list**. All 34 run at head
`f5773ce08`, exit codes captured redirect-then-`$?`:

```text
0 :: node scripts/check-adr-0087-registration.mjs --base origin/main
0 :: node scripts/check-adr-0087-registration.mjs --self-test
0 :: node scripts/check-changeset-no-major.mjs --base origin/main
0 :: node scripts/check-changeset-no-major.mjs --self-test
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:changeset-gate-self-tests
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-clause2-carriers
0 :: pnpm check:pnpm-filter-targets
0 :: pnpm check:ratchet-remedy-authority
0 :: pnpm check:refd-timer-probe
0 :: pnpm check:watch-hint-literal
0 :: pnpm check:pm-dispatch-gates
```

Reconciled: `dispatch-gates --ran` ⇒ **34 derived, 34 run, 0
NOT-MEASURED, 0 UNRUN**.

Repo-wide `pnpm lint` (`eslint . --no-inline-config`) at `f5773ce08`:
**exit 0**.
`grep -naP` for control bytes over the changed file: no hits.

⛔ Outside these 34, as the derivation itself prints: 53 artifact-roster
families, 11
wide-population families, 7 pending-changeset families, 1 path-scheduled
CI job and the
always-runs tail. Their absence here is not a clearance.

## Acceptance notes

Out of scope, noted and ⛔ not filed:

- The read-path report and the input record now also print on the
`--pair-json` **usage
refusals** (a missing file, a non-JSON document, a board conflict),
because everything past the
board resolution moved inside one `try`/`finally`. One extra stderr line
on those paths, in the
direction the file's own header argues for. Carrier: whoever next edits
`main`.
- `main`'s `--pair` value is parsed in two places now (once for
`run.mode`, once for the pair
itself). Both read the same argv through `flagIndex`; a reader may
prefer one. Carrier: whoever
  next edits `main`.

---

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu

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

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ion, rendered as written (objectstack-ai#18730)

Fixes objectstack-ai#18543

Clause-②: no

`scripts/pm/post-stamped.mjs` only. Non-governed `scripts/pm/**`;
`skip-changeset` (nothing in any package's `files[]` moves).

## The defect, in one paragraph

The tool substituted the act-clock token **wherever it appeared**,
including inside a passage that was *quoting the token itself* — because
substitution ran on bytes and a fence was treated as a rendering
instruction with no meaning for it. A seat writing about the stamp
contract therefore could not quote this tool's own diagnostic through
this tool: the quotation came back rewritten, the write succeeded, the
read-back reported clean, and the only signal was a substitution
**count** that nothing compared to intent. Measured three times inside
one hour, by two seats: a false quotation published on objectstack-ai#14251; the same
hit in the sentence *describing* it and again in the comment *reporting*
that (5707634696); and the dispatch claim for this card refused
`[quoted-not-a-stamp]` for spelling the contract's own documentation
placeholder (5717851201). The triage seat carries a hand-written
workaround in its standing prompt — 「⛔ 描述这个语法时不要写出令牌字面形状,会被当真令牌解析」 —
which is what this change retires.

## Before-readings — this tree at `f6189a43f9`, offline, through
`--dry-run`

All three taken before any edit. Every token below is written in the
**new** quoting spelling, so this PR body is itself a demonstration; the
inputs are shown as they were typed.

**(a) The card's measured instance — the token in an inline code span
inside a blockquote.** Input: a blockquote quoting this tool's own
status line, with the token inside backticks, plus one live token in a
subscript line.

```
post-stamped: DRY RUN — nothing was written. 2 token(s) substituted with `2026-09-17T16:34Z`, 0 quoted stamp(s) rendered verbatim.
...
> `post-stamped` reported it faithfully: 「*stamp: none substituted — this body carried no `2026-09-17T16:34Z`, so there is no clock to check*」
```

⇒ substituted **inside the quotation**; `substitutions: 2` where the
author meant 1. Exit 0. A sentence this tool never printed, published as
a quotation of it.

**(b1) UNESTABLISHED POINT 1, now measured — the quoted-stamp form
inside a quotation.** Input: `> The second spelling is ` + the
quoted-stamp token in backticks, plus one live token.

```
post-stamped: DRY RUN — nothing was written. 1 token(s) substituted with `2026-09-17T16:34Z`, 1 quoted stamp(s) rendered verbatim.
...
> The second spelling is `2026-09-08T14:00Z` — a reading of something else.
```

⇒ **worse than (a)**: the braces vanish, so the quotation no longer even
shows which route it was about, and the status line's `1 quoted` is
byte-identical to what a *legitimate* declared reading prints. There is
no count to compare against intent at all.

**(b2) UNESTABLISHED POINT 2, now measured — a fenced code block.**
Input: the card's own re-check recipe inside a triple-backtick block,
plus one live token.

```
post-stamped: DRY RUN — nothing was written. 2 token(s) substituted with `2026-09-17T16:34Z`, 0 quoted stamp(s) rendered verbatim.
...
printf 'quoting the token: 2026-09-17T16:34Z\n' | node scripts/pm/post-stamped.mjs --dry-run --comment 18543
```

⇒ a fence was **not** a quotation: substituted exactly like prose. So
"no substitution inside fenced code" was a real behaviour change to
measure, and fencing alone would not have covered (a), which was an
inline span.

**(c) The claim's refusal.** Input: the quoted-stamp form with an
ellipsis payload, inside backticks.

```
post-stamped: REFUSED — 1 stamp-contract problem(s) in the body. Nothing was written.
  1. [quoted-not-a-stamp] ... does not declare a stamp. The quoted route renders a reading of something else VERBATIM, so its contents must be one `YYYY-MM-DDThh:mmZ` and nothing else — it is a declaration, not a free-text escape from the contract.
```

Exit 2. The same refusal fires on the documentation placeholder
`YYYY-MM-DDThh:mmZ`, which is the spelling **this file's own refusal
text uses** — so the tool could not quote its own refusal back through
itself.

## The chosen spelling: Markdown code is a quotation

> **INSIDE A QUOTED SPAN THIS TOOL RENDERS TEXT, NOT TOKENS.**

A quoted span is one of Markdown's two code constructs: a **fenced code
block** or a **backtick code span**. Inside one, exactly three rules
stand down — substitution, the opener scan, and the quoted-stamp
validation. Nothing was added to the token contract: it still has
exactly two spellings, and there is still no flag that turns it off.

### The four axes

- **实际业务需求.** Measured, not speculative: three live hits inside one hour
across two seats, a false quotation standing on a public card until
someone re-read it, and a hand-written workaround living in the triage
seat's standing prompt for dozens of rounds. All four measurements above
are code constructs — an inline span, a fenced block, a backticked
declaration — so the structural rule is the one the evidence actually
points at. A fence-only rule would have missed (a), the card's own
instance.
- **项目长远合理性.** Contract-first and no workaround: the quoting rule is
Markdown's own, not a second vocabulary this tool invents and then has
to document, and the token contract is untouched. The sustainable
version of "explain the contract on a card" is that the contract is
quotable; the temporary version is every seat carrying its own
prompt-level workaround, which is the state being retired.
- **防 AI 写代码犯错.** This is the axis that decides it. A structural rule is
**visible in the stored artefact**: a reader who has never heard of this
tool sees backticks and knows the passage is quoted, in the spelling
every other quotation on the board already uses. A magic word is
invisible and has to be memorised — and would itself need quoting the
first time anybody documented it. The contract also tightens rather than
loosens: quoting suppresses what *renders* a token, never what *judges a
stamp a human typed*, so no stamp can hide behind backticks, and the one
direction that could have weakened a refusal (making the MIXED trigger
quote-aware) is deliberately not taken.
- **创业阶段不扩散需求.** Zero new surface: no third token, no flag, no option,
no config. One predicate (`insideQuotedSpan`) and one span scanner,
consumed by the four rules that already existed. No transition window
and no double spelling — the old behaviour is gone in the same commit,
which is what 「短期不考虑渐进」 asks for.

### Rejected, one line each

- **`--expect-now=N` (the card's candidate A).** Not a quotation
spelling at all — it refuses the body instead of letting the quotation
be written, so the seat still cannot quote the diagnostic; and being
opt-in, it protects only the author who already remembered.
- **An escape spelling (candidate C).** A third spelling in a contract
whose whole value is that it has exactly two, invisible to a reader who
does not know the tool, and subject to the same regress the moment
somebody has to quote *it*.
- **An explicit opt-out flag.** The one thing this file says there is no
flag for, and it leaves no trace in the artefact of why a token was not
substituted — a reader cannot tell a quotation from a bug.

## What it does NOT suppress — the load-bearing asymmetry

Quoting changes what is **rendered**, never what was **authored**. A
stamp inside a fence is still digits on the board.

| rule | inside a quoted span | why |
|:---|:---|:---|
| POSITIONAL | reads every line, code included | a bare stamp in a code
span on the opening line is the act's own time typed by hand, backticks
or not |
| MIXED | triggers on the act-clock token appearing **anywhere**, quoted
or not | an author who spells the token knows it exists; deliberately
NOT made quote-aware, because that is the one direction this change
could have weakened a refusal |
| MASKING | `maskQuotedStamps` blanks a quoted-stamp token only where it
IS a token | otherwise a hand-typed stamp could hide from the bare-stamp
scan behind backticks — the accident this rule may never buy |

**A bare stamp inside the quoting spelling is REFUSED, exactly as in
prose** (pinned, both positions). One body changes direction as a
consequence: the quoted-stamp form carrying **real digits** beside a
live token used to be accepted and rendered as bare digits — that
acceptance *was* defect (b1) — and is now MIXED-refused. Because the old
remedy text would then prescribe a route that cannot work inside a
quotation, the refusal gained a clause naming the quotation and pointing
at the placeholder form; a stamp with even one unquoted occurrence still
gets the ordinary remedy, with no such clause.

## The status line no longer leaves a count as the only signal

Before, the line carried the substitution count, the token spelling and
a quoted count, and stopped there — the card quotes that form. After,
the same line carries a third number, and on the same body as
before-reading (a) its new tail reads:

```
· verbatim: 1 opener(s) inside a quoted span, left exactly as written
```

So (a) now reports one substituted and one left verbatim, where the
filed run could only say `2`. Those two numbers are what (a) had no way
to produce. `--json` gains `verbatim`, and the `DRY RUN` line spells the
same three counts through one renderer (`substitutionSummary`), so the
three outputs cannot describe one render three ways.

## After-readings — same four inputs, same tree, after the change

- (a) exit 0 — one substituted, zero quoted, `verbatim: 1`; the
quotation comes back byte-identical and the act's own token, outside it,
still gets the clock.
- (b1) exit 2 — MIXED, with the quoted-span clause. The argued outcome:
real digits inside a quotation are digits on the board.
- (b2) exit 0 — one substituted, zero quoted, `verbatim: 1`; the fenced
recipe survives intact.
- (c) exit 0 — the ellipsis payload and the documentation placeholder
are both **quoted verbatim, not refused**. The dispatch claim for this
card would now pass byte-for-byte, which is the retirement of the triage
seat's standing workaround.

## Pins

New battery, `the quoting spelling: Markdown code is a quotation, and a
quotation is rendered as written`, 51 cases, registered in
`SELF_TEST_BATTERIES` with the battery floor raised 11 → 12. It covers,
in order: the card's blockquote-inline-span instance rendering verbatim
**and** its before-reading control (the same body with the backticks
removed is still substituted, 2); the fenced block, a tilde fence, an
info string, a backtick-carrying info string (not a fence), an unclosed
fence, a fence inside a blockquote, the blockquote ending it, and a
four-space indented block being explicitly **not** the spelling; the
claim's ellipsis refusal now rendering verbatim with its prose control
still refused; the quoted-stamp form inside the spelling (rendered as
the token, counted verbatim, not as a quoted stamp) with its prose
control still rendering from the declaration; an unknown token name and
an unclosed opener inside a quotation being text, both still refused in
prose; the bare-stamp asymmetry in both positions, the mask rule, the
quote-unaware MIXED trigger, and the remedy clause with its
one-unquoted-occurrence control; the code-span mechanics (unmatched run,
double run, one-line-only search, two spans on a line, a backtick run
inside a fence); and the prose control asserting byte-identical output
with `verbatim: 0`.

One pre-existing case was flipped rather than added — the opener-scan
battery's `⛔ …and a token inside backticks is STILL substituted — a
fence is not an escape`, which is precisely the sentence this card
retires. **Every other existing case is untouched and green**: the first
run after the implementation, before any new case existed, reported `1
of 257 case(s) failed` — that one — so the 256 others are byte-identical
in behaviour.

Self-test line: **257 cases across 11 batteries → 308 cases across 12
batteries.**

## Ablation

Committed the fix first, then reverted the single predicate that
recognises the quoting spelling — `insideQuotedSpan` made to answer
`false` always, one line, with every other line of the change left in
place. The mutation was proved on disk by occurrence count before the
run and the restore proved by hash against the HEAD blob, under a `trap
… EXIT INT TERM` holding an absolute path. The result is recorded below
with both legs' numbers. (No build step is involved: this file is run
directly by `node`, resolves through no package `exports`, and has no
`dist/`, so `ablation-dist-preflight.mjs` has nothing to preflight.)

| leg | worktree blob | on-disk proof | self-test |
|:---|:---|:---|:---|
| before (= `HEAD` blob) | `baba105b5625fc16` | predicate body present
×1, marker ×0 | 308 pass / 12 batteries, **exit 0** |
| ABLATED — `insideQuotedSpan` answers `false` | `62cb814417c1fd1a` |
predicate body ×0, marker ×1 | **24 of 308 failed**, exit 1 |
| restored — `git checkout HEAD -- ABSOLUTE_PATH` | `baba105b5625fc16`
(identical to the `HEAD` blob) | marker ×0, `git diff HEAD` 0 lines |
308 pass / 12 batteries, **exit 0** |

**Exactly the new cases red, the old ones green.** All 24 belong to this
change: 23 of the new battery's 51, plus the one flipped opener-scan
case. **Zero pre-existing cases went red.** The other 28 new-battery
cases are CONTROLS — prose behaviour, the before-reading controls, the
prose-still-refused halves — and stay green under the ablation by
design, which is what a control is for.

One failure detail is worth reading on its own: the counts case fails
with `sub=2 verb=0`, which is byte-for-byte the filed reading
`substitutions: 2` from before-reading (a). The ablation does not merely
turn the pins red; it reproduces the card's measured defect exactly.

## Gates

Derived from this worktree with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` (no hand-fed path list;
change set = 1 path, `scripts/pm/post-stamped.mjs`, three-dot against
merge base `f6189a43f`). Every derived command run, exit code captured
with redirect-then-`$?`, then reconciled with `--ran`.

**29 derived, 29 run, every one exit 0.**

- `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: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-post-stamped` :: **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` ⇒ `29 derived famil(ies) accounted
for — 29 run, 0 NOT-MEASURED (a DERIVED zero — all 29 recorded an exit
code and none of them is 3)`, **exit 0**.

Repo-wide, as PR objectstack-ai#18654 did: `pnpm lint` (`eslint . --no-inline-config`)
**exit 0**, run at `d7dffeb045`, which is this branch's final commit —
no commit has landed on the branch since.

⚠️ NOT MEASURED, declared rather than claimed: the 53 artifact-roster
families, the 11 declared wide-population families, the 14
pending-changeset families, the 2 families taking a value from the
workflow and the 1 path-scheduled CI job are each outside the derived 29
and are CI's. `dispatch-gates` prints every one of them under its own
heading; none is read here as a clearance.

## This body passes the tool it is fixing

`node scripts/pm/post-stamped.mjs --dry-run --comment=18543
--file=THIS_BODY` ⇒ **exit 0**. Its first draft did not: it quoted the
act-clock token literally *and* carried the concrete stamps from the
before-readings, and the MIXED refusal fired twice — correctly, and with
the new quoted-span clause naming the quotation each time. That shape is
the measured cost of keeping the MIXED trigger quote-unaware, and it is
the rule holding rather than a gap: every quotation above is verbatim,
and what changed to clear the refusal was spelling the token in words
outside the quoted evidence, not editing the evidence.

## Not in scope

objectstack-ai#18693 (the header's unmeasured issue-body footer cell) and objectstack-ai#18709 (exit
4 on the seat's own footer re-anchor) are separate cards queued on this
file behind this one; both were read and neither is addressed here.
`classifyReadBack`, `footerReAnchoring` and `sentBodyLanded` are
untouched, and nothing about what is SENT changed except the rendering
of quoted tokens. No retry logic, nothing under `.claude/**`.

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

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…nd one constant holds both tools to it (objectstack-ai#18738)

Fixes objectstack-ai#18701

Clause-②: no

## The defect

`check-clause2-carriers.mjs --template` prints, and has always printed,
that a review of record is `ONE comment on the PR or its card`; C6/C7
judge it on both threads through `locateReviewOfRecord`; the governed
text says the same in four places. The Governed Surface Queue Guard's
REFERENCES tier read **the pull request's thread alone**. The two tools
already shared every recogniser — the heading marker, the head-sha span
test, the newest-of resolution, the `Reviewed-by:` key line, the
`Served-tier:` reader — and the one thing they did not share was the
**set of threads** to run them over, which is the only fact a
spelled-out copy of the filter could not copy. Measured cost, on one
pull request in one afternoon: the skills seat posted its `## Contract
review` on carrier card objectstack-ai#18426 (comment `5716694216`, 15:10:43Z),
`--pair 18689` read it as C6-RECORD on the `card thread`, the
`merge_group` leg (run `35238999808`) refused with `0 comment(s) read on
the PR thread`, PR objectstack-ai#18689 was dequeued `CI_FAILURE` at 15:36:18Z, and
only a **second copy** of the same comment on the PR thread
(`5717108355`) cured it.

## The canonical sentence — the governed text is not silent

Read on `origin/main` (`080688b561`, the branch base). Quoted verbatim
and untranslated, because rewriting a quoted ruling rewrites the ruling:

- `.claude/skills/pm-dispatch/references/contract-review.md` **:27** —
「复核记录 = 一条评论落 PR 或卡,达档与默认档同形;散文、dev 自评、`os-dev-report` 恒不算。」
- same file **:40** — 「落地前检三条:① 席内条款②复核 PASS 在案,即 PR 或卡上同形的复核记录(档位按实测)。」
- same file **:59** — 「PASS、FAIL 与作废都落 PR 或卡;……」
- `.claude/skills/pm-dispatch/SKILL.md` 〈入队与落地〉 **:640** —
「交付后复核归派发席:达档席内审,未达档循保险丝起子代理;记录 = 同形评论落 PR 或卡。」

(The claim comment's aside that `references/contract-review.md` carries
no such sentence is the one premise this card's dispatch got wrong; :27
is that sentence, and it is on `main`. Nothing else in the premise moved
— the guard, the template, the docblocks and the objectstack-ai#18689 evidence all
read as the card describes them.)

**Which tool disagrees with it:
`scripts/pm/check-governed-queue-guard.mjs`.** So the GUARD moves, all
of it. No governed file is touched by this PR.

## Why the guard and not the template — the four axes

- **实际业务需求.** The record on the card is not a hypothesis: it is what a
seat did, on 2026-09-17, following this repo's own `--template`. Both
carriers are live on every pair — the clause-② declaration is hung on
both, `--pair` judges both, the carrier label is stripped from both —
and `check-clause2-carriers` has read both threads since objectstack-ai#17302. The
population that would be served by moving the template instead is empty:
nobody is asking for the card to stop counting.
- **项目长远合理性.** Contract-first. The governed text is the contract and the
two tools are its consumers; a consumer reading a narrower set than the
contract declares is the defect, and the repair belongs at the consumer,
not at the contract. 基本裁决原则 puts 声明 above 实现 above 文档面 — moving the
template would be narrowing the declaration to match an implementation
gap, which is the one direction that rule forbids.
- **防 AI 写代码犯错.** This is a declared-but-not-honoured capability in its
purest form: an instruction tells an author where to put an artifact,
and a gate silently does not look there. The structural repair is not
"say the same thing in more places" — it is ONE exported constant that
the instruction and the reader both derive from, plus a pin that reds if
either is edited alone. Moving the template would leave three more
hand-written location sentences (the guard's option-3 text and two
docblocks) free to drift the same way one layer along.
- **创业阶段不扩散需求.** Moving the guard is the SMALLER change and adds no
capability: the helper already exists, the card number is read off the
pull object the head read already fetches (zero extra requests for it),
and at most one extra thread read is bought — only for a references-tier
pull request with no authorized approval, a population refused outright
today, so nothing that passes now pays anything. Moving the template
would be a RETIREMENT of a carrier the protocol uses everywhere else,
across governed files, which is a maintainer's ruling and not a dev PR.

No axis conflicts with another. A fifth, decisive fact: the template
side cannot move from here at all — its sentences live in
`.claude/skills/pm-dispatch/**`, off this card's file surface and
governed.

## The shared constant, and one derivation of "the record on this head"

`check-clause2-carriers.mjs` — the file that owns the reader — now
declares the thread set once:

```js
export const REVIEW_OF_RECORD_THREADS = Object.freeze([
  Object.freeze({ where: 'PR',   rows: 'prComments',   number: 'pr',   words: 'the PR' }),
  Object.freeze({ where: 'card', rows: 'cardComments', number: 'card', words: 'its card' }),
]);
export const REVIEW_OF_RECORD_LOCATION = REVIEW_OF_RECORD_THREADS.map((thread) => thread.words).join(' or ');
```

Three consumers, all derived from it and none spelling a set of its own:

1. `locateReviewOfRecord` builds the rows it searches, its gap sentences
and its per-thread read counts from the list. `--template`'s and
`--pair`'s output is **byte-identical** to `main`'s.
2. `contractReviewTemplateLines` builds its printed sentence from
`REVIEW_OF_RECORD_LOCATION`, so an instruction can never offer a thread
no reader searches.
3. `check-governed-queue-guard.mjs` fetches **one thread per entry in
the loaded list** — and its whole record reading is now
`locateReviewOfRecord`, imported through the existing lazy loader rather
than re-spelled. The guard's own two-line filter is gone; what is left
there is the tier gate on top of the located record (`Served-tier:`
stands, and the objectstack-ai#18060 model-identifier refusal), which is that leg's
own fact.

The card the record may live on is **derived, never configured**:
`deliveredCardNumber(pull)` enumerates the numbers the body or branch
could be naming and asks `deliveryEvidence` — the one relation
`derivePairs`, H8 and H31 already pair a PR with its card by — so it can
never accept a card that relation rejects. A pull request naming no
card, or two cards at equal strength, searches NO card thread and the
refusal says so: the refusing direction, the only one a governed reading
may be wrong in. The card's number and the head sha come from ONE `GET
/repos/{owner}/{repo}/pulls/{number}` — the request the head read
already made — so the card thread costs no extra call for the number,
and the guard keeps its zero-write, event-payload-only posture with no
checkout.

`REVIEW_OF_RECORD_LOCATION` is mirrored at the guard's module scope for
the renderer only (`renderGuardVerdict` is pure and synchronous, and a
module-scope import of the sibling is the measured exit-13 deadlock its
own docblock records). The SET the leg acts on is never a mirror — it is
the loaded list — and the mirror's words are pinned to the constant in
the guard's battery.

## The pin

`check-clause2-carriers.mjs` owns the reader, so the pin lives in its
self-test — and it is a **measurement**, not two lists written side by
side:

- **STATED** — the locations read out of the text
`contractReviewTemplateLines()` actually prints.
- **READ** — obtained by **driving `check-governed-queue-guard.mjs`'s
`runGuard`** once per entry in `REVIEW_OF_RECORD_THREADS`, with a record
built from this file's own `contractReviewRecordLines()` sitting on that
thread **alone**, and collecting the threads whose run answers `exitCode
=== EXIT_CLEAR` with `record.state === 'stands'`.

The case asserts `stated === read === the constant's own set`, plus its
own controls: the set has two locations (not vacuous); each run located
the record on the thread it was posted to; the guard fetched exactly one
thread per entry, no more and no fewer; and a run with the record on
NEITHER thread is refused, so the pin can fail. `selfTest()` became
`async` to take that lazy import — the dispatch uses `.then(...)`, never
a top-level `await`.

## Counterfactual — the objectstack-ai#18689 dequeue, offline

One fixture, driven through `runGuard` with no network and no git: a
references-tier pair whose record sits only on the card thread. The same
script runs against either generation (it passes both the old
`fetchPullHead` and the new `fetchPull`).

| | exitCode | conclusion | `record.state` | `record.where` |
|---|---|---|---|---|
| **before** (`080688b561`) | `3` REFUSED | `refused` | `absent` | — |
| **after** | `0` | `cleared` | `stands` | `card` |

Before, rendered: `⛔ NO review of record on this head either (1
comment(s) read on the PR thread)` — the objectstack-ai#18689 sentence, reproduced.
After, rendered: `✅ review of record on this head: comment 5716694216
(2026-09-16T15:10:43Z) on the card thread is a ## Contract review
comment naming ...`

## Ablation

Two legs, each: on-disk proof of the mutation (`grep -c` of the removed
text and of the injected text, plus the `git hash-object` blob before
and after), then the self-tests, then restore, then a byte-identity
check against the HEAD blob. Both scripts carry `trap restore EXIT INT
TERM` with an absolute path. No build or `dist/` is involved — these two
files are run directly by node and by `package.json`, so the on-disk
proof is the whole preflight.

**Leg A — the loop that unifies the thread set** (`for (const thread of
recognisers.threads)` narrowed to the first entry). Blob `9a111f6c` →
`b57227d0`; removed-text count 1 → 0, injected-text count 0 → 1.
Cross-tool pin: **4 of 802 failed**. Guard battery: **25 of 253 failed**
— the loop is load-bearing for the whole references leg, not only for
the card.

**Leg B — the line that supplies the card's number** (`card: card.card`
→ `card: null`), which reproduces the pre-objectstack-ai#18701 thread set exactly.
Blob `9a111f6c` → `9d935d92`; counts 1 → 0 and 0 → 1. This is the clean
reading the card asks for:

- **new pin RED**: `check-clause2-carriers` **3 of 802 failed**, all
three the new battery — `stated=[PR,card] read=[PR]`, the card run
locating nothing, and the fetch list `[[4101],[4101]]` instead of
`[[4101,4102],[4101,4102]]`.
- **new guard cases RED**: `check-governed-queue-guard` **5 of 253
failed**, all five in the `objectstack-ai#18701` battery (record on the card alone,
both threads fetched, the rendering naming the thread, newest-governs
across threads, the branch-name fallback).
- **old cases GREEN**: 799 of 802 and 248 of 253 — every pre-existing
battery in both files, `objectstack-ai#18020` included, passes under the ablation.

Restore, both legs: worktree blob `9a111f6c…` equals the HEAD blob, `git
diff HEAD` empty, `git status --porcelain` empty, and both self-tests
back to exit 0.

## Self-tests

| family | before | after |
|---|---|---|
| `node scripts/pm/check-governed-queue-guard.mjs --self-test` | 239
cases, 21 batteries declared | **253 cases**, 22 batteries — new: `⭐
objectstack-ai#18701: the record lives on the PR or its card, and BOTH are read` (14,
floor 14); `SELF_TEST_BATTERY_FLOOR` 20 → 21 |
| `pnpm check:pm-clause2-carriers` | 786 cases, 28 batteries declared |
**802 cases**, 29 batteries — new: `objectstack-ai#18701: ONE thread set -- what the
template STATES is what the queue guard READS` (16, floor 16);
`SELF_TEST_BATTERY_FLOOR` 27 → 28 |

Both exit 0. `--template`, `--help` and the no-event refusal path (exit
1, its own message) all still answer.

## Gates

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived from the worktree with no hand-fed
path list (change set: the two scripts, 2 committed / 0 working tree / 0
untracked, three-dot against merge base `080688b56`). **36 derived, 36
run, every one exit 0**, reconciled with `--ran` after a fresh `git
fetch origin main` — `✓ dispatch-gates --ran: 36 derived famil(ies)
accounted for — 36 run, 0 NOT-MEASURED (a DERIVED zero — all 36 recorded
an exit code and none of them is 3)`.

```text
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-changeset-no-major.mjs --self-test :: exit 0
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
node scripts/pm/check-governed-queue-guard.mjs --self-test :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:closing-target-claim :: exit 0
pnpm check:cross-package-test-inputs :: 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-clause2-carriers :: exit 0
pnpm check:pm-dispatch-gates :: 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
```

Repo-wide `pnpm lint` (`eslint . --no-inline-config`, the whole tree, as
PR objectstack-ai#18654 did) :: **exit 0**, no output. Every exit code above was
captured with redirect-then-`$?`, never across a pipe.

`origin/main` moved to `62bce5c297` while this ran; its three commits
touch `packages/**` and `.changeset/**` only — nothing under
`scripts/**` and nothing this diff reads — so no merge was taken and the
derivation above is unaffected.

## Scope

Two files, `scripts/pm/check-governed-queue-guard.mjs` and
`scripts/pm/check-clause2-carriers.mjs`. No governed file.
`skip-changeset`: nothing published moves — `scripts/pm/**` is not in
any package's `files[]`.

⚠️ **The card read this PR adds is built on `readCardComments`'s paged
sibling, not on the un-paged path objectstack-ai#18683 names.** The guard's own
`makeCommentReader` is paginated (100 per page, 10 pages, and it THROWS
rather than truncating), and it is what serves both threads here — `GET
/repos/{owner}/{repo}/issues/{number}/comments` is the same route for a
pull request and for a card. objectstack-ai#18683 remains exactly as it was, on
`check-clause2-carriers`' own reader, untouched by this PR.

Not in scope, as dispatched: `check-clause2-carriers`' un-paged
`readCardComments` (objectstack-ai#18683 — read, not fixed); objectstack-ai#18536's machine-side
lane/tier re-keying; the REFERENCES-tier rule itself (which paths
qualify), untouched.

## Acceptance notes

- `deliveryEvidence`'s ranking had no exported ordering, so
`DELIVERY_EVIDENCE_PRECEDENCE` is declared in
`check-clause2-carriers.mjs` as a MIRROR and measured against that
function pair by pair in the self-test rather than trusted. If
`check-half-states.mjs` ever wants to own it, that is a one-line move;
noted, not filed — no PR or person reaches for a ranking that today has
exactly one caller.
- The guard no longer loads `check-half-states.mjs` directly: every
recogniser it reads now arrives through the file that composes them, and
a rename upstream stops THAT module loading, which the leg already turns
into a fail-closed refusal with the loader's own reason.
`RECOGNISER_SOURCES` is down to one entry and the self-test pins that.
Keeping the old direct load would have been a check on something this
file no longer reads.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… make a near miss audible (objectstack-ai#18756)

Fixes objectstack-ai#18680

Clause-②: no

## The defect

`CLAIM_COMMENT_MARKER` and `RELEASE_COMMENT_MARKER` anchor the bare word
at line start and tolerate only a leading blockquote, so a line written
in the decorated spelling a seat uses when it bolds the directive —
`**Release:** …` — begins with an asterisk and the record reads as
ABSENT. Nothing goes red; the ownership rows (H2, H47, H66, H67) simply
read a different history than the thread carries, which is the silent
direction. objectstack-ai#10102 made exactly this judgement for the other directive
family (`Blocked-by:` / `Restart-when:`) and never for these two
markers; PR objectstack-ai#18678 landed H67 declaring the loss on its own row rather
than widening, because widening moves three landed rows' populations and
is its own card. This is that card.

## Before-readings (reproduced offline, on the fetched specimen)

The live specimen is objectstack#16529 comment **5691473966**
(os-try-charles, 2026-09-16T03:11:44Z), fetched through the REST proxy
and never retyped. ⚠️ One correction to the card's prose: the
`**Release:**` line is the **45th** line of that comment, not its first
— both markers are `m`-flagged and scan every line, so line position was
never what hid the record. The asterisks were.

| probe | `RELEASE_COMMENT_MARKER` (bare) |
|:--|:--|
| the specimen's whole comment body | `false` |
| the specimen's release line alone | `false` |
| the SAME line with `**Release:**` replaced by `Release:` (control) |
`true` |
| `**Claim:** …` / `` `Release:` … `` / `__Release:__ …` / `## Release:
…` / `- Release: …` | all `false` |

`undecorateProseLine` — the objectstack-ai#10102 function — was measured rather than
assumed: its body is a single `replace` over a character class holding
exactly a backtick and an asterisk. It removes backticks and asterisks
and **nothing else** — not underscores, not a heading hash. That
measurement is what decides which spellings this repair reaches and
which become near misses.

Consumers of the two exported constants, all found by grep across
`scripts/`: nine call sites inside `check-half-states.mjs` itself (H2,
`governingClaim`, `latestClaimComment`, H34's guard, `h44ArtefactShape`,
`h46ClaimNamesBranch`, `latestMarkedComment` — which is H47's, H49's,
H50's, H53's and H67's shared resolver — `SEAT_SIGNATURE_FORMS`, and
`h66ReleaseVerdict`), each fed a raw comment body or one raw line; and
one cross-file importer, `scripts/pm/check-clause2-carriers.mjs`, which
feeds `CLAIM_COMMENT_MARKER` a raw comment body. That importer is
**outside this card's file surface and is deliberately unaffected**: the
constants keep their bare semantics.

## The ONE place decoration is handled

`markerMatches(marker, text)`, declared beside the two markers. It tries
the bare reading FIRST and short-circuits, then re-tests against the
same `undecorateProseLine` the `Blocked-by:` family uses. All nine
in-file call sites now go through it; ⛔ no regex was widened, ⛔ no
second stripper exists, and the two constants still describe the bare
directive (the cases that pin them still assert on them directly, and
they stay green).

Two properties fall out:

- **Strictly additive, by construction rather than by inspection.** The
bare test short-circuits, so ⛔ no body that read before can stop
reading. Measured on 770 live comments below: 0 regressions.
- **It refuses to undecorate through a markdown LIST ITEM.** The shared
stripper takes every asterisk, so a `* Claim:` bullet would become a
directive while H20's pinned `- Claim:` stays refused. A list marker is
followed by whitespace and a decoration is not; that is the whole
discriminator, and it is pinned both ways.

## The near-miss vocabulary — a line that looks like a marker makes a
sound

Widening alone leaves the same silence one decoration further out, which
is the triage's second half (comment 5716952460).
`OWNERSHIP_MARKER_NEAR_MISS_FORMS` is a frozen, named roster in the
register of objectstack-ai#18560's `SCHEMA_PROPERTY_FORMS`; each member carries its
own `example` fixture, and the roster is asserted EQUAL to a frozen list
of ids, so a form added without a fixture reds and a form silently
dropped reds.

| id | what | example |
|:--|:--|:--|
| `heading` | the directive written as a markdown heading | `## Release:
…` |
| `list-item` | the directive written as a markdown list item | `-
Release: …` |
| `underscore-emphasis` | emphasised with underscores, which the shared
stripper does not remove | `__Release:__ …` |
| `inflected-word` | a spelling the marker's vocabulary does not carry |
`Released: …` |
| `separator` | the canonical word with a separator that is not the
canonical colon | `Release — …` |

`ownershipMarkerNearMisses(commentRows)` is the reader; it buys nothing
(the sweep hands it threads other rows already paid for), files no
finding and proposes no state. It reports on an **unconditional summary
clause** (`Ownership-marker near misses: …`) naming the card, the
comment id and the offending prefix, capped at five named entries with
the remainder counted. A line the reading DOES read is ⛔ never a near
miss — the two are complements by construction, so a future widening
shrinks this census automatically.

H67's declared loss is retired in the same edit, on its row and in its
summary clause: both said the decorated line was invisible, and that is
no longer true.

## Pins

New battery **`H2/H47/H66 decorated ownership marker`**, 98 cases,
pinned at 94. The roster floor rose 4 to 5.

- the fetched objectstack-ai#16529 specimen: refused by the bare marker, READ through
`markerMatches`; the derived bare spelling of the same line matches both
ways (control); a release is still not a claim; the record reads from
any line of the body
- the rows: H2 goes clean on a decorated claim (with the no-claim
control still firing); `latestMarkedComment` locates a decorated
release; H66 reads it on the canonical leg with destination `pm:queue`,
quoted undecorated
- what the stripper measures: backticked and bold-italic directives
read; `__Release:__` does NOT, and is a near miss instead
- the firing controls of a widening, inside the same battery: seven bare
spellings still read, prose containing the word still does not,
`Released:` is still MALFORMED, the fullwidth colon still does not match
(the 2026-08-11 ruling is untouched), a dash-written claim is still
H34's row
- the bullet guard: an asterisk bullet, a hyphen bullet, an ordered `1.`
marker and a blockquoted bullet all refused; the whitespace
discriminator pinned as a pair
- the vocabulary: every member driven against its own fixture — not read
by either marker, reports naming its own form, with the comment id and
the offending prefix — plus the counterfactual roster-equality pin,
frozen-ness, distinct ids, and ⛔ no `g` and ⛔ no `m` flag
- the summary clause: counts, named entries, the cap clause,
unconditional rendering, render order, the forwarding contract, and ⛔ no
`undefined`
- **the PR objectstack-ai#18678 pin, EDITED and not deleted**: `H67 ⚠️ loss: a
DECORATED **Release:** line does not stand the row down` becomes `H67 ⚠️
loss CLOSED: … now STANDS THE ROW DOWN, as the bare one always did`, and
its companion flips from "the row DECLARES that blind spot" to "the row
no longer DECLARES a blind spot it no longer has". Its two control cases
are untouched and still green, because they assert on the marker
CONSTANT — which this PR does not change.

Self-test: **4782 cases / 4 batteries becomes 4881 cases / 5 batteries**
(98 battery cases, plus one case the per-anchor summary-clause coverage
loop registers for the new clause automatically).

## Ablation

Revert the one call that routes the markers through the undecorated line
(delete the undecorated leg of `markerMatches`, leaving the bare test
alone), from the committed state, with an EXIT/INT/TERM trap restoring
by absolute path.

- on-disk proof before reading any result: injected marker present 1
time, deleted text `const undecorated = raw` present 0 times, HEAD blob
`44aed794…` vs mutated blob `b4ab48a2…` (different, so ⛔ not a no-op)
- result: **17 red, 4864 green**. Every red is a case about the new
reading — the four objectstack-ai#16529 cases, H2/H47/H66 on a decorated record, the
five decoration cases, the two bullet-discriminator cases, and the
flipped H67 pin. Every bare-spelling control, every near-miss vocabulary
case, the `Released:` / fullwidth / prose negatives and the
marker-constant pins stayed green.
- restore verified by hash (`44aed794…` again) AND by an empty `git diff
HEAD` — ⛔ not by an exit code

## Live-board delta — report-only, ⛔ no state write of any kind

Two full sweeps, `node scripts/pm/check-half-states.mjs` against
`objectstack-ai/objectstack`: BEFORE on a detached worktree at
`62bce5c29` (17:53Z to 18:01Z), AFTER on this branch (18:01Z to 18:09Z).

**H2 / H47 / H66 verdicts: identical.** H2 fired on objectstack-ai#13597 and objectstack-ai#15638 in
both; H47 and H66 listed nothing in either. Eight rows differ between
the two runs (H14 objectstack-ai#18617, H38 objectstack-ai#7623, H52 objectstack-ai#18617 dropped; H1 objectstack-ai#18709, H19
objectstack-ai#18734, H36 objectstack-ai#18414/objectstack-ai#18720/objectstack-ai#18741 appeared) and every one is board churn
in the eight minutes between them — none reads an ownership marker.

**objectstack-ai#16529 specifically** still lists on H67 in both, and the reason has
nothing to do with the marker: its newest merge is now PR objectstack-ai#18678 (merged
2026-09-17), which is NEWER than the 2026-09-16 release record, so
"nobody has looked since the delivery landed" is a correct reading. Its
row text did change — the declared loss is gone.

Because a sweep only judges threads it bought, the zero above
understates the reading. So the same question was asked directly, over
the 286 open `pm:queue` / `pm:dispatched` cards and their 770 comments:

- **NEWLY READ as an ownership record: 4, on 4 cards; REGRESSIONS: 0.**
- objectstack-ai#16529 comment 5691473966 — `**Release:** …` (the card's own specimen)
  - objectstack-ai#17852 comment 5700605769 — a backticked `Release:`
  - objectstack-ai#15468 comment 5549954050 — `**Claim:** …`
  - #14026 comment 5486688759 — a backticked `Claim:`
- **Near misses over the same population: 2** — objectstack-ai#16233 comment
5704218834 and objectstack-ai#18143 comment 5707808263, both `### Release:` (form
`heading`). Over the sweep's wider 187-thread corpus the clause named
three: objectstack-ai#18336 comment 5693290763 (`list-item`), objectstack-ai#15768 comment
5556889538 (`heading`), objectstack-ai#6736 comment 5235658231 (`separator`).

And the counterfactual the rows themselves cannot show, offering the
SAME live thread to both readings — **3 of 6 cards change**:

| card | H47 | H66 |
|:--|:--|:--|
| objectstack-ai#16529 | FIRES becomes **clean** (the release now answers the claim) |
none becomes **`pm:queue`** |
| objectstack-ai#17852 | FIRES becomes **clean** | none becomes **the maintainer** |
| objectstack-ai#15468 | clean becomes **FIRES** (a bolded claim nobody has answered)
| unchanged |
| #14026, objectstack-ai#16233, objectstack-ai#18143 | unchanged | unchanged |

⛔ Nothing was written to any card, PR or label from either sweep, and ⛔
no verdict here is a proposal about any of those cards.

## Gates

Derived from this worktree with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` (⛔ no hand-fed path list;
change set: `scripts/pm/check-half-states.mjs`, one path). 38 families,
every one run, exit code captured by redirect-then-`$?` before any pipe,
reconciled with `--ran`.

**37 of 38 exited 0**, with the two qualifications named below. Notably:
`check:pm-half-states` 0 · `check:nul-bytes` 0 ·
`check:closing-target-claim` 0 · `check:commit-card-trailers` 0 ·
`check:self-test-wired` 0 · `check:scripts-symbol-anchors` 0 ·
`check:declaration-mirrors` 0 · `check:whole-set-label-write` 0 ·
`check:changeset-no-major` 0 · `check:pm-governed-queue-guard` 0 ·
`check:cross-package-test-inputs` 0 · `check:parse-guard` 0.

⚠️ `pnpm check:nul-bytes` exited **1 on the first pass** and was right
to: an editing tool had materialised a backslash-u-0001 escape (written
out in words here for the same reason) into a real 0x01 byte in the
near-miss dedupe key — the exact slip that gate exists for. Fixed by
writing the escape text (byte-identical at runtime), re-run green, and
the rule's own `grep -naP` control-byte self-scan over the file returns
nothing.

⚠️ `pnpm check:pm-dispatch-gates` is the one family whose self-test runs
longer than this container's foreground ceiling: a first attempt reached
1768 green cases and was killed by the timeout wrapper at 560s (exit 124
= no verdict reached, which is NOT MEASURED and ⛔ not a red). It was
re-run detached; its verdict is reported in this card's `os-dev-report`
comment rather than guessed here. ⛔ Its diff-relevant half is unaffected
either way — this PR touches neither `dispatch-gates.mjs` nor its
fixtures.

Repo-wide `pnpm lint` (`eslint . --no-inline-config`): **exit 0**, as PR
objectstack-ai#18654 did.

## Not in scope, read and left alone

- **objectstack-ai#18664** (`ISSUE_BODY_LIMIT`, queued behind this card on this file)
— read, ⛔ not touched.
- **H67's own row logic** beyond the pin flip and the two sentences that
declared a loss which no longer exists.
- **The objectui copy of this file** — byte-pinned, re-synced only
through objectui#9395, ⛔ never hand-mirrored.
- **How a verdict is written to a card** — unchanged; this file still
writes nothing.

## Acceptance notes

- H66's summary clause still carries the dated reading 「Measured
2026-09-16 over 29 threads on two boards, the canonical `Release:` line
appeared ZERO times」. It names its date and its boards, so it stays true
as written, but it was taken with the bare reader and this PR changes
what a re-measure would find. Noted, not filed — the sentence is a dated
measurement, not a live claim. Who would meet it: the next author of
H66's buy-order or clause.
- `check-clause2-carriers.mjs` reads `CLAIM_COMMENT_MARKER` against a
raw comment body and therefore still cannot see a decorated claim. That
is correct for this card's file surface (the constant is unchanged) and
is a reading about that file, not a defect in this one. Noted, not
filed; the seat decides whether that gate wants the same reading. Who
would meet it: whoever next touches that gate's claim leg.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ipts/ablation-replace.mjs beside its sibling preflight (objectstack-ai#18725)

Fixes objectstack-ai#18599

Clause-②: no

Governed surface (`.claude/**`): draft, `skip-changeset`, an authorized
approval lands it and the `domain:skills` seat lands it from there. File
surface: `.claude/agents/os-dev.md` + `packages/qa/dogfood/README.md`
only. Head `fa55c9d0ad`, base `f6189a43f9` (`origin/main` at worktree
creation; the card's readings were taken on `ab1d35925` and re-read here
on the tip). Line-neutral on the governed file: **403 / 403**, widest
line 120 B before and after, zero lines over the ratchet's 120-byte cap.

## The defect

PR objectstack-ai#18598 (card objectstack-ai#18226) landed `scripts/ablation-replace.mjs` — the
on-disk mutation step of an ablation: replace through an anchor that
must hit, with the write and the restore verified against the disk
rather than an exit code, in place of the `sed -i` / `perl -i` family
that silently failed in two recorded ablations (2026-08-20, a `-0` run
whose `$/` interpolated into a NUL byte; 2026-09-14, an argument
swallowed and nothing written, exit 0). No instruction surface pointed
at it, while the two that route an agent to its sibling
`scripts/ablation-dist-preflight.mjs` do exist — so the helper could
only be found by grepping `scripts/`, which is the memory-dependent
shape objectstack-ai#18226 was filed against. This PR adds the pointer to exactly
those two surfaces, in the order an ablation runs the two tools: mutate
on disk through the replace helper, then prove the mutation reached
`dist/` with the preflight, then read the run. Adoption-style only: not
a mandatory path, no gate, no migration of existing ablation scripts
(objectstack-ai#18226's triage 5707288884 set that floor; the migration set measured
empty in objectstack-ai#18598's census).

## `.claude/agents/os-dev.md` — one line in, one true duplicate out

Inserted, new :254 (119 B), directly under the mutate-then-build line of
the ablation step (:253 「每一腿(变异与还原)都是:改动 → 证明它真落到了磁盘 → …」, 108 B) and
above the preflight line (old :254, now :255, 113 B):

> - 落盘走 `node scripts/ablation-replace.mjs`,不走 `-i`
家族:锚点必须命中,写入与还原按磁盘核验。

「`-i` 家族」 is the name the file's existing :258 already gives the trap
(`sed`、`perl -i`、`str.replace`、`re.sub` 零命中也 exit 0). The line is a
routing line in the register of the preflight line beside it: one
instrument, the reason in the same breath, no 「必须」, no gate.

Paid by deleting old :319 (72 B), the opening bullet of 「干净收尾」:

> - 报告落两次,GitHub 优先:卡片评论在前,终报消息在后。

What it lost: nothing the file does not state elsewhere, twice. Its
three claims — two deliveries, GitHub first, comment before terminal
message — are the header's :17 (「报告交付两次,GitHub 优先:先作 issue 评论,…」) and
:18 (「…再作为终报消息…」), and the very next bullet of the same section (now
:320, 「终报消息之前,把同一段 JSON 发成 issue 评论,…」) restates the ordering together
with the marker. The section now opens on that bullet.

Why a payment and not a fold: measured on this tree, of the file's 284
adjacent bullet pairs ZERO merge under the 120-byte cap (smallest merged
133 B), and in the ablation block :253–:264 ZERO of 11 (smallest 162 B);
the block's total slack is 249 B spread over lines holding 0–61 B each,
none of which fits a 35-byte script path plus its reason. Re-wrap
funding is refused by the ratchet rule in any case.

Whole file: 403 → 403 lines; widest line 120 B before and after;
control-byte scan (`grep -naP` over the C0/DEL range) on both files:
clean.

## `packages/qa/dogfood/README.md` — one sentence

Step 4 of 「Adding a golden test」, placed before the existing preflight
sentence (mutate, then prove `dist/`); the helper's usage placeholders
are written as words here because the platform sanitizer eats tag-shaped
fragments in a PR body — in the file they are the helper's own
spellings:

> Make the revert itself through `node scripts/ablation-replace.mjs
--file PATH --anchor TEXT --replacement TEXT -- CMD` rather than `sed
-i` / `perl -i`: the anchor must hit, the write and the restore are
verified against the disk, and a mutation that did not land exits
non-zero instead of handing you a green run.

Non-governed, no line budget, the README's English register. Net +4
lines in that paragraph.

## Verification by the card's own instrument (tree
`objectstack-ai/objectstack`)

| reading | card @ `ab1d35925` | base `f6189a43f9` | head `fa55c9d0ad` |
|---|---|---|---|
| `git grep -lI 'ablation-replace'` | 1 as reported (that sha itself
answers 0 — the helper landed in `d0b8ec2aaf` after it) | 1 (the helper
itself) | **3** — `.claude/agents/os-dev.md`,
`packages/qa/dogfood/README.md`, `scripts/ablation-replace.mjs` |
| `git grep -lI 'ablation-dist-preflight'` | 7 | 8 (the card's 7 plus
the helper's own header, which names its sibling) | 8, unchanged |
| control `git grep -cI 'dogfood'` os-dev.md / README.md | 1 / 6 | 1 / 6
| 1 / 6, unchanged |
| `git grep -cI 'ablation'` on
`.claude/skills/dogfood-verification/SKILL.md` | 0 | 0 | 0 — the third
routing document named in objectstack-ai#18598's report does not exist; not touched |

Per-file hits at head: `ablation-replace` — os-dev.md 1, README.md 1,
the helper 14; `ablation-dist-preflight` — os-dev.md 1, README.md 1.

## objectstack-ai#18226 option A — a reading, not implemented here

Option A (「在字节纪律里加一条具名陷阱」: name the `perl -0` + `$/`-interpolates-to-NUL
trap in the byte-discipline rules) was **routed, not ruled, and has not
landed**. objectstack-ai#18226's triage 5707288884 routed it to the skills seat (「选项
A(在字节纪律里加一条具名陷阱)要走 skills 席,⛔ 不在本卡内落地」), the claim 5710594253 repeated
the boundary, and PR objectstack-ai#18598 landed option C (the helper) only. On this
tree: `AGENTS.md` carries no control-character or `check:nul-bytes`
sentence at all (its only `perl -i` mention is :224, the Bash-guard
write shapes); `os-dev.md` :387–:390 (字节与 sanitizer 纪律) covers writing
control characters as escapes and the harm of a raw NUL, and :258 covers
mode ② (nothing written, exit 0) — neither names mode ① (`-0` + `$/`).
Left untouched here (this card is the pointer only) and reported in
`open_questions` for the seat to queue.

## Gates (worktree at `fa55c9d0ad`; every exit code captured by
redirect-then-`$?`, never through a pipe)

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the worktree, no hand-fed paths (change
set vs merge base `f6189a43f`: the two files; committed 2, working tree
0, untracked 0). 53 commands; reconciled with `--ran` carrying `:: exit
N` per line: **53 derived, 48 run, 5 NOT-MEASURED, 0 UNRUN**.

Named by the dispatch:

- `pnpm check:pm-skill-ratchet` :: exit 0 — 「.claude/agents/os-dev.md is
403 lines (ceiling 403; headroom 0)」 · 「widest table row is 0 bytes (pin
0; headroom 0)」
- `pnpm check:nul-bytes` :: exit 0 — 「OK (scanned 8814 text file(s) … no
raw ASCII control bytes)」
- `pnpm check:agent-model-declared` :: exit 0 — 「1 agent definition(s)
under .claude/agents/ all declare a model」
- `pnpm check:pm-governed-merges` :: exit 0 (the post-merge audit).
`node scripts/pm/check-governed-merges.mjs --pr NUMBER` needs this PR's
number, so it runs after creation; its verdict (expected GOVERNED, exit
3, which is a verdict and not a failure) is recorded in the report
comment on the card.
- `pnpm lint` (repo-wide, `eslint . --no-inline-config`, as PR objectstack-ai#18654
ran it) :: exit 0, 100 s, on `fa55c9d0ad`

The other 43 that ran, all exit 0: `check-ci-filter-parity`,
`check-closing-keyword-parity` (+ `--self-test`),
`check-comment-mask-adoption` (+ `--self-test`),
`check-comment-mask-corpus`, `check-keyed-text-bounds` (+
`--self-test`), `check-platform-object-tenancy-census` (+
`--self-test`), `check-plugin-teardown-shape` (+ `--self-test`),
`check-registry-log-declared` (+ `--self-test`),
`check-rest-log-spy-declared` (+ `--self-test`),
`check-system-context-census` (+ `--self-test`),
`check-undeclared-dep-imports` (+ `--self-test`),
`docs-audit/check-affected-docs`, `docs-audit/check-drift-comment`,
`pm/check-governed-queue-guard --self-test`, `pm/check-harness-current
--self-test`, spec `check:empty-state` · `check:liveness` ·
`check:strictness-ledger` · `check:variant-docs`,
`check:agent-test-spelling`, `check:commit-card-trailers`,
`check:cross-package-test-inputs`, `check:doc-authoring`,
`check:driver-memory-census`, `check:org-identifier`,
`check:page-declaration-shape`, `check:pm-skill-id-lint`,
`check:published-files`, `check:refd-timer-probe`,
`check:skill-frame-sync`, `check:slot-lookup`,
`check:test-source-alias`, `check:tier-file-adoption`,
`check:type-source-resolution`, `check:watch-hint-literal`.

NOT MEASURED (exit 3, `PREREQUISITE NOT MET` — each loads BUILT
artifacts and refuses on this unbuilt worktree; the diff touches no
source, config or manifest, so their verdict is CI's on its correctly
built tree): `pnpm --filter @objectstack/lint run
check:doc-formula-expressions` (`@objectstack/formula` and
`@objectstack/lint` not built), `pnpm check:dts-closure`, `pnpm
check:dual-build-cjs-loads` (101 packages without `dist/`), `pnpm
check:lean-entry-closure` (`packages/objectql/dist` absent), `pnpm
check:sourcemap-no-sources-content`. Their only remedy is a whole-repo
`pnpm build`, not proportionate to a prose-only change; declared, not
hidden.

① / ②: the only package touched is `@objectstack/dogfood` (private, no
`files[]`, scripts `typecheck` + `test` only); the README is an input to
neither (the sole `README` mention under `packages/qa/dogfood/test` is a
comment in `rls-fixture.dogfood.test.ts:29`), so no closure build and no
package test is owed — a declared narrowing.

`skip-changeset`: `.claude/**` is on the fast lane and
`@objectstack/dogfood` is private with no `files[]` — nothing published
moves.

## Acceptance notes

- noted, not filed: the card attributes 「`git grep -lI
'ablation-replace'` → 1」 to `ab1d35925`, but that sha itself answers 0 —
the helper landed in `d0b8ec2aaf` (objectstack-ai#18598) after it; the reading was
evidently taken on a tree that already carried the merge. The premise is
unaffected: base `f6189a43f9` answers 1 (the helper alone). 承接者:无 —
recorded here and in the report.
- noted, not filed: `ablation-dist-preflight` answers 8 on the tip, not
the card's 7 — the eighth is the helper's own header cross-referencing
its sibling, which is the intended composition (the two tools prove
different halves and say so). 承接者:无.
- Rider not taken: the pending `tail --pid` rider in 干净收尾 (named by PR
objectstack-ai#18659) stays for the next os-dev.md PR; this PR's deletion in that
block is the duplicate opener, not a slot for it.

## 维护者速读(草稿)

**改了什么** — 只动两处文档。`.claude/agents/os-dev.md`(行数不变 403 / 403)在消融步骤里加一行,把
dev 指向 PR objectstack-ai#18598 新落地的锚点核验替换助手 `scripts/ablation-replace.mjs`,与已有的
`ablation-dist-preflight.mjs` 指向并排、按消融实际执行顺序(先落盘变异、再证明到达
`dist/`、再读结果);付账是删掉「干净收尾」节开头一条与文件头部及同节下一条重复的「报告落两次」句。`packages/qa/dogfood/README.md`
的「Adding a golden test」第 4 步加一句同样的指向。

**为什么改** — 这个助手落地了,却没有任何一份指令文档指向它;而指向其姊妹工具的文档有两份。一个采纳式工具若无人被指向,只有碰巧 grep
过 `scripts/` 的人会用到 —— 这正是卡 objectstack-ai#18226 要消灭的「靠记性」。⛔
不做成必经路径、不加门禁、不批量迁移旧消融脚本(objectstack-ai#18226 分诊已定这条地板)。

**风险与代价(含回滚)** — 零代码、零
changeset、零门禁改动;唯一的内容损失是被删的那条重复句,它的三个主张在文件头部(:17–:18)与同节下一条(:320)各有原文。回滚
= revert 本 PR,无其它文件牵连。

**席位意见** — (留空,席位定稿)

**你要做的** — 读新 :254 一行与被删 :319 一行;批准即由席位落地。若不同意付账形态,点名另一条重复句,席位代改。

---
_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/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants