Skip to content

fix(pm): the clause-② correction remedy names THIS card's claim comment, never a specimen from another card - #18448

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-17919-pair-claim-comment-cross-card
Sep 16, 2026
Merged

os-warren merged 1 commit into
mainfrom
claude/issue-17919-pair-claim-comment-cross-card

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #17919

Clause-②: no

A CLI gate script exports no published symbol; the two new exports
(commentCardNumber, correctionTarget) live in scripts/pm/, which no
package's files[] ships. The seat declared this on the card (claim comment
5696260545) and the diff did not move it.

The finding, and what it actually was

check-clause2-carriers --pair reported the clause-② declaration MISPLACED and,
inside that message, named claim comment 5642248126 — a Claim: on card
#17366, unrelated to the card under test.

The card reads that as the script's selection. It is not. Part (3) of the
C2 remedy was a module-level constant that carried the id as a literal:

'(3) Post ONE new comment on this card whose FIRST line is `Clause-②-correction: 5642248126` — '

That is the #17366 specimen, hard-coded into the sentence every C2 row prints.
Nothing selected it, so no guard over a selection could have fired — a constant
makes no selection. The id printed on every card, in every C2 state that
carries the remedy
, and had done so since the constant was written.

Verified on the exact blob the 2026-09-13 readings were taken from —
git cat-file -p aecbb2d86683eb908468fdacaac2ff53753f06ef | grep -n 5642248126
→ line 1445, inside CORRECTION_REMEDY. Same literal, same sentence.

Reproduced deterministically, offline, on a card of its own

--pair-json judges a pre-fetched pair with no network, so the reading is
exactly reproducible. A synthetic card #99002 whose whole thread is comments
900000001 and 900000002, judged by the origin/main copy of the script:

$ node scripts/pm/check-clause2-carriers.mjs --pair 99001 --pair-json misplaced-pair.json
exit=4
… whose FIRST line is `Clause-②-correction: 5642248126` …

An id that is on neither comment of the thread it just read, printed inside the
verdict about that thread. ⇒ the class is re-observable today, on any card,
without reproducing the non-determinism the evidence upgrade measured.

The cut, and why it is here rather than where the card offered it

The card offers a guard that the selected comment's issue_url matches the card
under test, explicitly as a shape. Measurement moved it one level up: the only
ids this file may print are ids it read from the pool the reading was
built from
, and each is checked against the card under test before it is
printed.

  • commentCardNumber(row) — the issue a row says it belongs to, read off
    issue_url; this is the card's own control, mechanised.
  • correctionTarget(pool, card) — resolves the printable id from the SAME pool
    cardDeclaration built its readings from. A positive disagreement drops the
    id and says so; ⛔ absence of issue_url is not read as a mismatch (an
    offline document and this file's own fixtures carry rows without it, and a row
    that states nothing contradicts nothing); a reading with no card number names
    no id at all — fail-closed.
  • CORRECTION_REMEDY is a function of the card under test. When no id survives,
    part (3) names the key and no digits, plus the reason.

⛔ Nothing here resolves a state, a row or an exit code.

Both directions, judged — this is a gate other PRs must pass

Does the guard suppress a message that was correct? No. Word-diff of the
whole output, same input, origin/main copy vs this one:

… whose FIRST line is `Clause-②-correction: [-5642248126`-]{+900000001`+} — the numeric id of the
claim comment it corrects, digits [-only-]{+only, read from card #99002's own thread+} — followed by …

That is the entire delta. The MISPLACED verdict, its sentence, its remedy and
exit=4 are byte-identical. A self-test case asserts each of the five remedy-
carrying states still renders a non-empty row, so the controls cannot pass
vacuously.

Does any exit code move on a pair judged correctly? No. All 16 open PRs,
both copies run back to back, 2026-09-16T11:03–11:04Z:

exit PRs
0 18444 18438 18437 18436 18433 18430 18429 18428 18427 18414 18403 18389 18319
2 18420, 17076
4 18131

Identical on both copies, 16/16, including all three non-zero verdicts.

Population — previously unmeasured, now partly measured

  • Which paths: the remedy is printed by 5 of the 6 C2 branches — misplaced,
    malformed, and all three missing variants. absent does not print it (no
    claim comment exists to correct). Enumerated mechanically: the ablation's own
    failure detail names all five.
  • How often: on the live board at 2026-09-16T10:56Z, sweep over 15 pairs
    derived from 16 open PRs — 0 with NO CLAIM COMMENT, 0 with a claim comment
    but NO DECLARATION LINE ⇒ 0 pairs on the printing path today. The class is
    a failure-path constant, not a standing one.
  • ⛔ Still unmeasured: how often the board has historically been on that path.

Tests

--self-test, the safety net, on 2620cb2b3d: 715 cases pass, exit 0
(689 before; +26). New battery #17919: the correction remedy names THIS card's claim comment, never another card's, floor raised by exactly one.

Ablation — the guard removed, part (3) restored to the pre-fix literal, on a
committed tree, mutation proved on disk before the run and the restore proved by
git diff HEAD and a blob-hash comparison:

HEAD blob                fe6601d55f30b3cdf490d167d04d131713c608f2
on-disk: injected marker count=1 (want 1); removed-text count=0 (want 0)
mutated blob             847524b84e71639d317c6e9e9e373cb877c5c672
VERDICT ablation-mutated self-test exit=1       ← 9 cases red
  ✗ ⭐ …and NOT ONE of them carries a comment id from another card
       ["misplaced","malformed","missing","missing/describing","missing/inline-key"]
restored blob            fe6601d55f30b3cdf490d167d04d131713c608f2   (= HEAD)
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/… — there is no build and no
dist/ between the edit and the run, so the on-disk proof is the whole
preflight.

Gates: all 36 commands derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack run at 2620cb2b3d, exit 0 —
including pnpm check:pm-clause2-carriers, pnpm check:nul-bytes and
pnpm check:pm-dispatch-gates (496.6s; a first run capped at 240s read
exit=124, NOT MEASURED, and was re-run to completion). Reconciled:
dispatch-gates --ran ⇒ 36 derived, 36 run, 0 NOT-MEASURED, 0 UNRUN.

No changeset: the root package is private and scripts/pm/ sits outside every
workspace package, so no files[] can ship it.

Acceptance notes

Out of scope, noted and ⛔ not filed:


Generated by Claude Code

…nt, never a specimen from another card

Part (3) of the C2 remedy carried a LITERAL comment id — 5642248126, the
#17366 specimen, which lives on card #17366 — so every C2 row on every card
printed a real `Claim:` comment id belonging to a different card, inside a
verdict about this one, and nothing in the output told a reader that.

Nothing SELECTED that comment. A constant makes no selection, so a guard over
the selection could never have fired; the cut is at the ids this file may
PRINT. `correctionTarget` resolves them from the same pool `cardDeclaration`
built its readings from, checks each against the card under test through the
row's own `issue_url`, and withholds — loudly — any id it cannot show belongs
to that card. Absence of `issue_url` is not read as a mismatch; only a
positive disagreement is, and a reading with no card number names no id at all.

No state, row or exit code moves: what changes is which digits part (3)
carries, and whether it carries any. The self-test case that used to pin the
foreign literal as the remedy's content is replaced by the property, and a new
battery reds without the guard.

Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
@os-warren os-warren added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 16, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

Contract review

87/87 at-tier stamps (control: an os-dev round in this same session reads 156/156 claude-opus-5)
Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2620cb2b3df65229fabbd4fdfb3580d29330e062

① Derived judgments

Scope of the diff: one file, scripts/pm/check-clause2-carriers.mjs, blob beed7f7bab to fe6601d55f, +257/-18, no other file at 2620cb2 differs from merge-base 55fd5ee; origin/main (read 2026-09-16T11:37:56Z as 66abef3) still carries beed7f7bab for this file, so base-vs-head here is also main-vs-head.

Defect as actually found — CONFIRMED, and the card's offered shape could never have fired. On origin/main the id 5642248126 is a literal inside the CORRECTION_REMEDY string (line 1657), printed by every C2 row that carries the remedy, on every card. Nothing selected it. Card comments are read from /issues/{card}/comments (line 4143) and the card number comes from the PR body/branch, so on the live path every row already belongs to the card and a guard on the selected comment's issue_url has no input to fire on. The comment lookup the card relied on is re-verified: 5642248126 resolves to #17366 (claude[bot], "Claim: PM loop round 1"); control 5650083758 resolves to #17425. The round's account is correct; the card's mechanism attribution was wrong; the card's observation stands and is re-observable on any card via --pair-json.

Direction A — nothing correct is silenced. The remedy is printed by exactly five states in c2Sentence (misplaced, malformed, missing, missing/describing, missing/inline-key); absent prints none. Each of the five was rendered on both copies from synthetic --pair-json documents (11:36:57Z): a C2 row prints on both, exit 4 on both, and the whole word-diff of the full output is 5642248126 replaced by the card's own governing claim id plus the clause "read from card #N's own thread". absent, a declared-with-yes pair (exit 4 via C3) and a declared-with-no pair (exit 0) are byte-identical across copies.

Direction B — no exit code moves. Live, 19 open PRs, base then head per PR (11:34:34Z to 11:35:58Z): 17 pairs exit 0, 18420 and 17076 exit 2, identical 19/19 verdict-by-verdict; the only output delta is the x-ratelimit-remaining counter. Zero C2 rows printed live on either copy (control: 12 C2 rows on the synthetic set), so no live pair is on the printing path today and the live sweep exercises none of the changed lines; the synthetic set carries the accept/refuse comparison. 15 synthetic pairs: exits equal on both copies in every case (4/4, 0/0, 2/2).

Fail-closed — correct, and absence is not conflated with disagreement. Rows without an id: no digits printed, sentence "found no claim comment id on card #N's own thread". Rows with an id but no issue_url: the id is printed (a row that states nothing contradicts nothing). A row declaring another parent: the id is withheld and the drop is printed with comment id and declared issue. Two ids in the pool at one timestamp: neither is printed, both are listed. Superseded plus governing claim: the governing id is printed. No card number (direct call): no id, "carries no card number". The printable pool is the same pool applicableCorrection accepts a correction from, so the remedy can only ever name an id that would land.

Self-test — the pre-existing pin is real and the new battery bites. Base line 4452 pinned the literal as expected content, which is why 689 cases stayed green over the defect. Head replaces it with a property (key present, no foreign digits) and adds battery "#17919 ..." (25 cases, floor 24; roster floor 24 to 25; 689 + 25 + 1 = 715). Base 689 pass exit 0; head 715 pass exit 0. Ablation with part (3) restored to the literal: exit 1, 10 red, the sweep case naming all five printing states. Ablation with the foreign guard removed: exit 1 (by a TypeError at the battery's own foreign[0].card read; with that one access made optional, 4 red). Ablation loosening the fail-closed id rule: exit 1, 3 red. Direction predicted and observed in all three: the battery would not be green without the fix.

Accept/refuse changes, one by one: (1) SELF_TEST_BATTERIES gains one entry, floor 24 to 25 — self-test only, tightens what a green self-test means. (2) commentCardNumber, correctionTarget — new pure exports of a repo-root script; no verdict reads them. (3) cardDeclaration takes an optional card and its withNote results carry a correctionTarget field — state, value and detail unchanged (asserted by the battery and confirmed by identical exits); no consumer serializes or deep-compares the object (0 hits, control 36 JSON.stringify sites). (4) CORRECTION_REMEDY constant becomes correctionRemedy(target) plus correctionTargetNote — text inside C2 rows only. (5) c2DeclarationUnreadable passes pair.card. Net: the gate accepts and refuses exactly what it did; only the digits in part (3) of the remedy, and a stated reason when there are none, change.

Clause-②: no HOLDS. scripts/pm/ is under the private root package (@objectstack/spec-monorepo, private true); no package.json in the tree names scripts/pm (0 hits; control: 70 package.json files declare files[]); the two new exports are reachable only by importing the script by path. Nothing reaches a published package.

Unexplained and NOT claimed by this fix — the 0 / 4 / 0 reading. The evidence-upgrade comment records the same command on the same pair answering 0, 4, 0 from two working directories with an identical script blob. This diff touches nothing in the fetch, the claim ordering, the repo resolution (env-driven, no cwd read) or the read-path selection, so it does not bear on that reading at all, and the reading remains open. I re-read #17425's 16 comments: every one has created_at equal to updated_at, so comment edits are excluded as the cause. The PR body's "without reproducing the non-determinism" is accurate; closing #17919 must not be read as settling the non-determinism.

② Semver level

skip-changeset, no changeset file — CORRECT. The rule: .claude/agents/os-dev.md lines 296-297 (the only criterion is that nothing published moves, published meaning what each package's files[] ships; fast path names scripts/pm/** as non-publishing), AGENTS.md lines 1064-1067 (the label is for a diff that publishes nothing from any released package), .github/workflows/pr-automation.yml lines 691-760 (route 2), .claude/skills/pm-dispatch/references/lanes/devx.md line 21 (root scripts/ implies skip-changeset), references/review-checklist.md line 20 (this repo takes the label, never an empty changeset). Verified: root package private, scripts/pm in no files[], labels read 11:33:00Z are size/m and skip-changeset, Check Changeset check run success on the head sha.

③ Boundary flags

  • "misplaced/malformed say NOT in the card's claim comment on threads with no claim comment": reachable, verified by direct call on both copies (state misplaced with zero Claim: rows). Verdict and exit are right; the clause presupposes a claim comment. On base that path also printed the foreign literal; on head it prints the no-id reason. Prose imprecision, pre-existing, out of this card's scope; acceptable as noted-not-filed, seat's call whether to file.
  • Header line 367 literal: inside the /** */ block at lines 355-372, never printed — 0 remedy-line hits for 5642248126 across 19 live and 15 synthetic runs on head. Acceptable as left.
  • The self-test pin written from the thing it pins: confirmed at base line 4452 and replaced; nothing left to file.
  • os_dev_md_divergences (7 items): drift between the loaded os-dev.md and origin/main; none changes what this diff does. The operative one is consistent with the PR as measured: needs:contract-review NOT on PR fix(pm): the clause-② correction remedy names THIS card's claim comment, never a specimen from another card #18448 (labels 11:33:00Z), --pair 18448 exit 0 on both copies (11:34:48Z). Not adjudicated here; the doc sync is the PM's.
  • population_measured "0 pairs on the printing path today": confirmed independently, 0 C2 rows over 19 open PRs on both copies at 11:34-11:36Z.
  • open_questions is empty: it should not be. The 0 / 4 / 0 non-determinism recorded on the card is not explained, not claimed fixed, and not carried anywhere in the report. ESCALATE to the seat: carry it explicitly on close of [finding] check-clause2-carriers --pair can quote a Claim: comment belonging to a DIFFERENT card when it reports MISPLACED #17919 (remaining open item) or as a follow-up card, so the fix's landing does not read as settling it.
  • Minor, non-blocking: the battery assertion at head line 6193 (foreign[0].card) throws rather than fails when the guard is absent; the self-test still exits 1, so the net holds, but by stack trace rather than a named red case.
  • CI: at 11:40:12Z, 23 success, 11 skipped, Lint & Repo Gates still in_progress; the seat should read its conclusion before landing.

Implemented-by: claude/issue-17919-pair-claim-comment-cross-card
Reviewed-by: session_01KB5PFtxuy1x3dcR5gxudx6

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Provenance — at-tier PASS, landing pre-check clear

domain:spec execution seat, session session_01KB5PFtxuy1x3dcR5gxudx6, 2026-09-16T11:49Z.

  • Review of record: comment 5696882388 on this PR — ## Contract review, VERDICT: PASS, Head-sha: 2620cb2b3df65229fabbd4fdfb3580d29330e062, this PR's current head.
  • Served tier, verified rather than asserted: the reviewer's transcript carries 87 "model" stamps, all claude-fable-5-1, 0 of anything else. Control: an os-dev round in this same session reads 156/156 claude-opus-5 ⇒ the instrument discriminates.
  • Independence pair: Implemented-by: claude/issue-17919-pair-claim-comment-cross-card vs Reviewed-by: session_01KB5PFtxuy1x3dcR5gxudx6 — different identities, ⛔ not a self-review. Fed the card, its rulings and this PR only; ⛔ never the dispatch order, ⛔ never the seat's conclusions. Adopted verbatim.
  • Carriers: none to clear — the pair declares Clause-②: no, --pair 18448 exits 0 with no widening tell, so needs:contract-review was correctly never hung.

Landing pre-check, all three: ① the PASS above names this head. ② --pair 18448 → exit 0 (re-run at 11:49Z). ③ every check green as the latest run per check name — 35 runs over 31 names, 4 names carrying more than one run, 0 not-green. Governed-surface predicate on the script-derived file list: 0 of 1 path hits the register ⇒ NOT governed.

⚠️ What this PR does NOT settle — carried before landing, deliberately

The review escalated that open_questions should not have been empty: #17919's 0 / 4 / 0 non-determinism is not explained, not claimed fixed, and must not be read as settled by this landing. That remainder is now carried on its own card, #18456, filed before this PR was enqueued so the record cannot be read the other way.

The seat added one measurement to that card which kills the leading hypothesis: this script resolves its board from PM_SWEEP_REPO → GITHUB_REPOSITORY → default and reads the working directory nowhere — process.cwd 0, git remote 0, execSync 0 on origin/main (control: the file's own name, 34). So "it ran in a different directory" cannot by itself change the verdict. Together with the review's finding that all 16 comments on #17425 have created_at == updated_at, the two cheapest explanations are both excluded.

⇒ this PR fixes #17919's observation (a remedy naming another card's claim comment). The reproducibility of landing pre-check ② remains open on #18456.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 16, 2026 11:49
@os-warren
os-warren added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit 2502b8a Sep 16, 2026
37 checks passed
@os-warren
os-warren deleted the claude/issue-17919-pair-claim-comment-cross-card branch September 16, 2026 12:11
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
… must answer what prints or executes it (objectstack-ai#19039)

Fixes objectstack-ai#18958

Clause-②: no

## What changed

One rule line lands in `.claude/skills/pm-dispatch/SKILL.md` 〈平台读数纪律〉,
beside the zero-hit rules it mirrors: a POSITIVE grep hit offered as
evidence that a defect is still alive must answer who prints or executes
it; unanswered, it does not prove the defect alive; a hit in a comment,
a docblock, a fixture or the remedy's own regression pin does not count.
Nothing else in the file moves.

The discipline already refused a bare zero (:163 「零命中须配同主体必中词,否则该零作废」,
:165 「控制通过 ≠ 问题问对」) and said nothing about a bare positive — so "grep
found it ⇒ still alive" had no rule behind it, and three seats in a row
re-derived the same hits on a literal that no executed or printed path
carries (specimen below). The card and the triage prescribe the shape,
not the wording; the wording here is the seat's.

## The band, before / after (`.claude/skills/pm-dispatch/SKILL.md`,
`main` `75c0dacff` → this branch `97d0a55bd`)

Rule added after :166 (now :167), 115 bytes:

```text
- 正命中须答谁打印/执行它,否则不证缺陷仍活;注释/文档块/夹具/修复的回归 pin 不算。
```

Paid by compressing the band's two timestamp bullets (:170 106 B and
:171 105 B on `main`) into one (now :171), 118 bytes:

```text
before :170  - 板面/树/队列读数恒带 UTC 取数时刻;认领、派发令、复核、轮报与座位贴皆同。
before :171  - 时间戳形如 `YYYY-MM-DDThh:mmZ`,树读数另带 ref 或 tip;无时间戳的读数按未取处理。
after  :171  - 读数与认领等座位文本恒带 UTC 取数时刻 `YYYY-MM-DDThh:mmZ`;树读数另带 ref/tip,无者按未取。
```

What the fold deletes, named: the enumeration of reading kinds (板面/树/队列
— subsumed by 读数, which :171's own second clause already generalised:
无时间戳的读数按未取处理 applies to every reading) and the enumeration of seat texts
(认领、派发令、复核、轮报与座位贴 — kept as the class 认领等座位文本). No rule leaves: the UTC
stamp, its `YYYY-MM-DDThh:mmZ` shape, the tree reading's ref/tip and the
no-stamp ⇒ not-taken verdict all survive on one line. This is
compression in place, not a whitespace re-wrap: the two source lines sum
to 211 bytes against the 120-byte cap, so no re-flow could have merged
them — the ratchet header's own measurement (601 adjacent bullet pairs,
zero merge under the cap) is why a paid line in this file is always a
content compression, and the two prior commits on this band paid the
same way.

Ratchet: `.claude/skills/pm-dispatch/SKILL.md is 812 lines (ceiling 812;
headroom 0)` before and after (gate line quoted from
`check:pm-skill-ratchet` on `97d0a55bd`).
`references/platform-readings.md` untouched (466 / 466; held by PR
objectstack-ai#18961 — no fact row is owed: the incident is a discipline gap, not a
platform behaviour). `references/core-rules.md` untouched (151 / 151; it
is a declared subset — 「⛔ 不新增规则」 — and carries no mirror of this line).

The band still reads as a pair after the edit: :163–:166 say what a ZERO
owes (a lit same-subject control, a radius, a known target outside it);
:167 says what a POSITIVE owes (who prints or executes it); :168–:169
(units and the counterfactual) follow unchanged; no orphaned 「同主体」
reference.

## The specimen the rule is written for

Card objectstack-ai#18376 (state: completed) claimed the C2 remedy of
`scripts/pm/check-clause2-carriers.mjs` printed a hardcoded
claim-comment id. The remedy landed as `2502b8a1` (PR objectstack-ai#18448) on
2026-09-16T11:49:48Z — six and a half hours after the card was filed —
and afterwards the triage seat re-derived the same coordinates and
graded p2, and the execution seat judged the premise intact and
dispatched. Three readings, all positive, none asked what prints or runs
the hit.

Measured on `main` `75c0dacff` (`git grep -n 5642248126 --
scripts/pm/check-clause2-carriers.mjs`): 13 lines, every one of them off
the executed / printed path.

| line | form | reachability | origin |
|---|---|---|---|
| :387 | docblock example (` * Clause-②-correction: …`) | header prose,
never printed | pre-remedy |
| :1333 | docblock prose | never printed | added by `2502b8a1` |
| :1359 | docblock prose | never printed | added by `2502b8a1` |
| :2442 | docblock prose | never printed | added by `2502b8a1` |
| :6315 | `//` comment inside `selfTest()` | never executed | added by
`2502b8a1` |
| :6324 | self-test assertion `says(missingLine, '5642248126') ===
false` | regression pin asserting ABSENCE | added by `2502b8a1` |
| :7446 | `const C_CLAIM_ID = 5642248126` | self-test fixture constant |
pre-remedy |
| :7914 | `//` comment inside `selfTest()` | never executed | added by
`2502b8a1` |
| :7939 | self-test assertion `=== false` | regression pin asserting
ABSENCE | added by `2502b8a1` |
| :7953 | self-test assertion `every(… !says(row, …))` | regression pin
asserting ABSENCE on every rendered row | added by `2502b8a1` |
| :7959 | `C19_CLAIM({ id: 5642248126, … })` | self-test fixture row
(the foreign claim the remedy must drop) | added by `2502b8a1` |
| :7963 | self-test assertion `=== false` | regression pin asserting
ABSENCE in the remedy | added by `2502b8a1` |
| :7964 | self-test assertion `says(C19_FOREIGN, 'Comment 5642248126')`
| regression pin asserting the DROP diagnostic names the dropped id |
added by `2502b8a1` |

Totals: 4 docblock + 2 line comments + 2 fixtures + 5 self-test
assertions (4 pin absence, 1 pins the loud-drop diagnostic) = 13; 11 of
the 13 were written by the remedy itself (`git show 2502b8a`: 11 added
lines carry the literal, 2 removed — the remedy string and the old
presence pin; the pre-remedy tree `2502b8a1^` carried 4). The card's 「6
处是修复自己的回归 pin」 is this table's 5 assertions plus the fixture row that
feeds them — same set, one boundary choice apart.

Cheap reachability probes, all on `75c0dacff`: `--help` prints 19 lines,
0 hits; `--template` prints 40 lines, 0 hits; `--pair 18958` took the
input-record path (exit 2, no card derivable from that number), 24
lines, 0 hits; the C2 remedy limb itself now interpolates — :2458 reads
`whose FIRST line is \`Clause-②-correction: ${id}\``. The seat that
greps this literal and finds 13 lines must now say which of them
anything prints or runs before treating the defect as alive; here the
answer is none, and the six pins asserting absence are the remedy's
evidence of having landed.

## Reader test

A seat that greps a literal and finds it must now say whether anything
prints or runs it before calling the defect alive — the rule asks
exactly that (须答谁打印/执行它), voids the hit otherwise (否则不证缺陷仍活), and names
the four places a hit that fails the question usually lives
(注释/文档块/夹具/修复的回归 pin).

## Verification (all on `97d0a55bd`, exit codes captured before any
pipe)

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the change set (stderr line 1: `gate
list derived from the tree of 'objectstack-ai/objectstack' at commit
75c0dac`): 20 commands, every one run, `COMMAND :: exit CODE`:

```text
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/pm/check-governed-queue-guard.mjs --self-test :: exit 0
node scripts/pm/check-harness-current.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 3 (PREREQUISITE NOT MET, nothing measured) → built @objectstack/formula + @objectstack/lint under os-verify-lock (VERDICT command-exit 0, 148 s) → re-run :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:pm-expected-skips :: exit 0
pnpm check:pm-governed-merges :: exit 0
pnpm check:pm-governed-prose :: exit 0
pnpm check:pm-half-states :: exit 0  (4978 self-test cases pass)
pnpm check:pm-skill-id-lint :: exit 0  (27 files clean)
pnpm check:pm-skill-ratchet :: exit 0  (SKILL.md is 812 lines, ceiling 812, headroom 0)
pnpm check:refd-timer-probe :: exit 0
pnpm check:skill-frame-sync :: exit 0
pnpm check:watch-hint-literal :: exit 0
```

Named by the dispatch beyond the derivation: `node
scripts/pm/check-governed-merges.mjs --test
.claude/skills/pm-dispatch/SKILL.md :: exit 3` (GOVERNED — expected;
`.claude/**` ×1); `pnpm check:pm-dispatch-gates` :: exit 0
(cap-exceeding per its own header, so started detached and waited on in
the foreground with `tail --pid`; `dispatch-gates self-test: 1849 cases
pass`, 751.7 s on this box). Reconciliation `dispatch-gates.mjs --ran`
:: exit 0 — `20 derived famil(ies) accounted for — 20 run, 0
NOT-MEASURED` (the doc-formula line recorded at its post-build re-run,
exit 0). The 120-byte line rule is part of `check:pm-skill-ratchet` (its
`### Why 120 bytes` section), not of `check-skill-id-lint`; both ran.

## Acceptance notes

- noted, not filed: `references/core-rules.md` :44–:45 carry the
zero-hit rules in summary form and no positive-hit mirror; the file is a
declared subset at 151 / 151, so a mirror there is a separate
compression decision for the skills seat, not a gap in this PR. 承接者:
skills seat, at the next core-rules compression.
- noted, not filed: `check-clause2-carriers.mjs --pair N` reads N as a
PR number, so `--pair` on a card number lands on the input-record path
(exit 2) rather than the remedy limb; the filer's 「对本卡自身跑 --pair 出现 0 次」
was a probe of that path too. Not a defect (documented in `--help`);
承接者: 无.
- No fact row for `references/platform-readings.md`: `git grep -c`
counting matching lines (not occurrences) is git behaviour every seat
already reads from `git grep --help`, not a platform reading this repo
measured.
- Skip-changeset: the diff touches `.claude/**` only — nothing any
package's `files[]` ships.

## 维护者速读(草稿)

**改了什么**:PM 技能的「平台读数纪律」一节加了一条规则:座位用 grep
查到某个字符串,要拿它当「缺陷还活着」的证据,必须先答一句「是谁在打印或执行它」;答不出,这个命中就不算证据;命中落在注释、文档块、测试夹具、或者修复自己写的回归
pin 里,都不算。为了不增加文件行数(棘轮 812 / 812),同一节里两条讲时间戳的规则压成了一条,规则内容没有丢。

**为什么改**:现有纪律只管「查不到」的情况(查不到要配一个必定能查到的对照词),不管「查到了」的情况。结果一张卡的修复已经合进 main
六个半小时后,立卡、分诊、执行三个席位各自又 grep 了一遍,三次都命中、三次都判「缺陷还在」,还派发了一次开发——而那 13
处命中全部在注释和测试里,其中 6 处正是修复自己写的、断言「这个字面量不许出现」的 pin。这是成文纪律的缺口,不是某一席手滑。


**风险与代价(含回滚)**:只改一段散文,不动任何代码或门禁;成本是一行读数纪律,每个席位每轮多答一句话。压缩的那两条时间戳规则丢掉的只是举例列表(板面/树/队列;认领、派发令、复核、轮报、座位贴),规则本身保留。回滚就是还原这一个文件的这两处改动。机械守卫(自动判断「这一行是不是注释」)是另一张卡,本
PR 不做。

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


**你要做的**:这是受管面(`.claude/skills/**`)的规则层改动,需要你确认并人工合并;看一眼新增的那一行措辞是否合你的意,以及压缩后的时间戳规则是否可接受。

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

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

Development

Successfully merging this pull request may close these issues.

[finding] check-clause2-carriers --pair can quote a Claim: comment belonging to a DIFFERENT card when it reports MISPLACED

2 participants