Skip to content

fix(pm): refuse a claim's unreadable keyed lines before post-stamped writes the comment - #19435

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-19152-keyed-lines-refused-at-write-time
Sep 20, 2026
Merged

os-steve merged 2 commits into
mainfrom
claude/issue-19152-keyed-lines-refused-at-write-time

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #19152
Clause-②: no

A claim's Seat:, Thread-read: and Clause-②: lines are exact-value fields. Three post-hoc readers enforce them AFTER the claim is stored; one seat produced five half-state rows off three of them in a single shift, always the same shape — the key right, the value right, an explanation added on the same line. post-stamped.mjs is the one writer every claim passes through, and it said nothing.

It now reads those three lines on the --comment path, through the very functions that own them, and REFUSES the write before any request when one of them cannot be read — naming the key, quoting the offending line and printing the spelling that reads.

check-half-states.mjs prescribed exactly this instrument, in its own Seat: reader's docblock:

⛔ The presence probe below therefore does NOT widen CLAIM_SEAT_KEY_LINE: accepting a malformed declaration would leave the writer no way to find out either, which is the same defect wearing the other costume. Refusing it at the writing path is a different instrument's job.

The three owning readers, quoted beside mine

key the owner's own reader what this tool calls a refusal
Seat: claimSeatNumber (check-half-states.mjs): key line /^[ \t]*(?:[-*+][ \t]+)?>?[ \t]*(?:\*\*)??Seat?(?:\*\*)?[ \t]*:[ \t]*(.*)$/im, then #(\d+); absent ⇒ 1, present-but-unreadable ⇒ null (PR #19423) claimSeatNumber(body) === null — the imported call, nothing else
Thread-read: threadReadField returns {present, value}; h50ThreadReadMismatch then asks field.present && field.value === found.expected, where expected is commentIdText (/^[1-9]\d*$/) or the literal none the field reader for the line; a value outside `/^(?:[1-9]\d*
Clause-②: readClause2Line (check-clause2-carriers.mjs) — four-valued: declared / malformed / near-miss (describing, inline-key, spelling) / null anything that is neither null nor declared; --pair answers exit 4 on those

No second parser: the three readers are imported and called. The one spelling this file adds is the Thread-read: value shape, pinned in the self-test against check-half-states.mjs's own source text (its commentIdText pattern and its 'none' literal are not exported), which is the coupling the claim prescribed.

Before / after — measured, --dry-run on each body

body before (488f4f5) after
Seat: declared inside the opening sentence exit 0, stored exit 2, nothing written
Thread-read: listing nine ids exit 0, stored exit 2, nothing written
`Clause-②: yes` followed by prose (the describing near miss) exit 0, stored exit 2, nothing written
Clause-②: yes — the ruling states it outright (…) exit 0 exit 0 — deliberately unchanged, see the correction below
a well-formed Claim: exit 0 exit 0
an os-dev-report comment exit 0 exit 0
a ## Contract review body quoting a claim's Seat: line exit 0 exit 0

The refusal, on the first of those:

post-stamped: REFUSED — 1 keyed line(s) in this `Claim:` cannot be read by the checker that owns them. Nothing was written.
  1. [Seat] `claimSeatNumber` reads `null` here — the declaration is present and names no seat. The claim lands on NO seat, and H38 rows another seat's post over it.
      line:  Claim: PM loop round 1 — dispatched by the skills seat. Seat: domain:skills#2 — R1, 2026-09-20T20:31Z.
      write: `Seat: domain:LANE#N` at the START of a line, lane and number both — e.g. `Seat: domain:skills#2`

Two dispatch assumptions REFUTED, and corrected here

① Clause-② is NOT "the value and nothing after it". The dispatch order described the clause reader as accepting yes/no "and nothing after it". Measured on readClause2Line: trailing reasoning on a bare-key line is ACCEPTED, deliberately, and its own docblock records why — #13914's control case is «the PM claim comment on #12297 carries Clause-②: yes with reasoning» and is recorded there as the shape that is CORRECT, after a stricter reading rejected four real claims on the live board. What that reader refuses is structural: describing (the key twice on the line, or a code span opened before the key, closed later, and the line then continuing), inline-key (the key off the line start) and malformed (a value it cannot grade). So the measured shape in the card's table is refused here through its quoted-and-continued spelling, and a bare Clause-②: yes — reasoning is left alone. Refusing it would have made this tool reject the shape the owning gate prescribes — the second dialect this card exists to avoid.

② The scope is NOT "first line opens with Claim:". The claim and the card both spelled it that way; the fleet's own definition is CLAIM_COMMENT_MARKER through markerMatches, which matches a Claim: line anywhere in the body, and newestLaneClaim's header refuses a stricter reading by name: «Defining a stricter first-line-only claim for this row alone would leave the file disagreeing with itself about what a claim IS — H2 calling a card claimed while H38 called the same comment invisible». A first-line rule here would decline to judge bodies H50 reads as claims, which is the hole restated one layer up. The marker is imported.

Assumptions ①(the tool reads no keyed line today — zero mentions of the three keys before this diff), ③(reuse EXIT_REFUSED; a refusal before any request) and ④(extend the existing --self-test, no second entry point) held as stated.

REFUSE, not warn — on the four axes

  • 实际业务需求 — measured, not supposed: five rows in one shift, three keys, three detectors, every one found minutes to hours later; and one of them ([finding] a claim whose Seat: line is not line-initial is silently read as seat 1 — three claims in one round misattributed, producing a false H38 stale row on another seat post #19108) accused ANOTHER seat's post. A warning on a tool whose stdout is read by scripts is a row nobody clears — this file's own header records the sibling case where a refusal was piped away and the write that followed landed anyway.
  • 项目长远合理性 — SKILL.md ranks remedies «(a) 删容许出错的构造 → (b) 让正确形态成唯一拼写 → (c) 加检查», and the card's own reading is that every existing reader is a post-hoc (c). Refusing at the single writer makes the readable spelling the only storable one: a (b) fix at the one chokepoint, with no new checker to keep in step.
  • 防 AI 写代码犯错 — the refusal is the contract-first direction: loud at authoring time, with the exact spelling printed, rather than a lenient consumer. A warning would be precisely the tolerant seam this repo forbids — the malformed line would still be stored, and the reader that cannot parse it would still file its row.
  • 创业阶段不扩散 — three keys, the ones the card measured, and no other; no --force, no flag, no config. The template's other keyed lines (Session:, Branch:, Worktree:, Domain:, File surface:, Container & model:, Serial constraints cleared:) are untouched: they have no exact-value reader to disagree with, so there is nothing to mirror.

The two decisions the card asked to be pinned

A key inside a code fence or a code span is judged exactly as in prose. The tool already has a quoted-span mask (quotedSpans) and it is deliberately NOT reused here. All three owning readers read raw text, so masking would let a body store the exact line the patrol then files — a refusal bought off with backticks. This is the asymmetry this file already states for stamps: quoting changes what is RENDERED, never what was AUTHORED. Two self-test cases pin both directions: backticks around a mid-sentence declaration buy no exemption, while a line-initial declaration inside a fence still reads as the declaration its owner reads there.

A key present twice reads the way its owner reads it — the first line-initial declaration decides, and a later one is neither read nor refused. That is not a choice this file makes; it is what CLAIM_SEAT_KEY_LINE.exec, THREAD_READ_KEY_LINE.exec and readClause2Line's first-declaration-wins loop already do, and a write side that judged the second line would refuse a body its readers call clean. Pinned by a case: a readable declaration followed by a malformed duplicate passes.

Scope, budget and verification

  • scripts/pm/post-stamped.mjs only, --comment only (a claim IS a comment; a seat POST's body carries a Seat: line no claim reader judges). check-half-states.mjs and check-clause2-carriers.mjs are untouched — read, imported, not copied.
  • Net +120 / -0 lines, the claim's budget exactly, self-test cases included.
  • node scripts/pm/post-stamped.mjs --self-test :: exit 0 — 530 cases across 18 batteries, the new battery pinned at a floor of 20.
  • The derived union on the merged head (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, reconciled with --ran): 30 commands, every exit captured before any pipe. All green.
  • pnpm exec eslint --no-inline-config scripts/pm/post-stamped.mjs :: exit 0. The repo-wide scan is CI's run.
  • skip-changeset: scripts/pm/** publishes nothing from any released package.
  • node scripts/pm/check-governed-merges.mjs --pr N reads NOT governed — scripts/pm/ is on no register row.

Acceptance notes

  • Noted, not filed: the Thread-read: write-time rule can only decide SHAPE — whether the id is the right one is H50's, and it needs the thread this act has not fetched. A future --thread-read proof-of-reading flag (the shape --ack-through already has on --body) would close that half; nobody is blocked on it today. 承接者: the next card on this file's --comment path.
  • Noted, not filed: commentIdText and the 'none' literal in check-half-states.mjs are not exported, so this file pins their spelling by reading that file's source. Exporting them would turn the coupling into an import. 承接者: whoever next edits H50's reader.

Generated by Claude Code

os-steve and others added 2 commits September 20, 2026 20:30
…is written

`Seat:`, `Thread-read:` and `Clause-②:` are exact-value fields three post-hoc
readers enforce after the claim is on the board. `post-stamped --comment` now
reads them through those same readers — `claimSeatNumber`, `threadReadField`
and `readClause2Line`, imported, never re-spelled — and REFUSES the write
(EXIT_REFUSED, before any request) when one of them cannot read a line,
naming the key, the offending line and the spelling that reads.

Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi
Co-authored-by: Claude <noreply@anthropic.com>
…sed-at-write-time

Claude-Session: https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d7537e2409af923fe251efe9a0ca51086e64eba1

① Derived judgments

  • Published accept set / public surface: none moves — one PM seat-write tool (scripts/pm/post-stamped.mjs, +120 / −0, exactly the claim's budget); no published package, no schema, no label, no new flag, no new request (the refusal fires BEFORE any request). check-governed-merges --pr 19435: NOT governed — ordinary queue landing. check-widening-tells --declaration no: 1 file NOT MEASURED — the no stands on the seat's reading. Judged correct.
  • The fix, read line by line: on --comment only, a body carrying the fleet's claim marker (CLAIM_COMMENT_MARKER through markerMatches — imported, ⛔ not a first-line-only narrowing, which newestLaneClaim's header refuses by name) has its Seat: / Thread-read: / Clause-②: lines judged by the three readers that own them — claimSeatNumber and threadReadField imported from check-half-states.mjs, readClause2Line from check-clause2-carriers.mjs — and a line those readers cannot read is EXIT_REFUSED with the key, the offending line and the spelling that reads printed; ⛔ no second parser, no restated rule (a structural self-test case pins that every reader is imported and that the CLI wires this on --comment only). What is refused is the measured class: a Seat: inside the opening sentence (the owner reads null since PR fix(pm): a present-but-unreadable Seat: declaration reads as null, never as seat 1 #19423), a Thread-read: off the line start or listing nine ids, a quoted-and-continued or inline-key Clause-②; what is NOT refused is the owning gate's own recorded-correct shape (Clause-②: yes followed by reasoning on a bare-key line — The Clause-②: yes | no machine spelling is missing from the claim comment on 2 of 3 measured cards — the enqueue gate's predicate reads it there, and it is not there #13914's control, which a stricter reading once rejected on four live claims) — the dispatch's assumption 2 was refuted there by reading the owner and corrected publicly, the right consequence. Fences are deliberately NOT masked (all three owners are quote-blind, so masking would store exactly the row the patrol then files), pinned both ways. Judged correct.
  • Evidence, seat-run on this head in a detached worktree: post-stamped.mjs --self-test exit 0 — 530 cases across 18 batteries, the new battery at 20 / floor 20; the tool AS BUILT dry-run against this seat's own artefacts — three recent claims, an ACCEPT, a ## Contract review record and a Release: note — all exit 0 (well-formed writes pass unchanged); the three measured malformed shapes (a Seat: in the opening sentence; a nine-id Thread-read:; a backticked Clause-②: yes with prose after it) each exit 2 = EXIT_REFUSED naming the one unreadable line; merge-tree vs origin/main c27e160: 0 conflicts; CI on d7537e2: 35 check runs, 1 still running, 0 red. The dev's 30-family derived union 30 / 30 exit 0 with --ran reconciled (a DERIVED zero), check:pm-dispatch-gates and check:pm-post-stamped included; the report itself was posted with the tool as built.

② Semver level

None — nothing published; skip-changeset on the PR is the correct declaration. Judged correct.

③ Boundary flags

open_questions empty. The dispatch's assumption 2 refuted in two places and corrected (the Clause-② reasoning shape; the marker scope) — both by reading the owners, accepted. The route's fence suggestion measured and not taken, pinned. Boundaries the dev pinned: the write-time Thread-read: rule decides SHAPE only — whether the id is the right one stays H50's (a proof-of-reading flag like --ack-through on --body would close that half; carrier: the next card on this path); commentIdText and the none literal are not exported by check-half-states.mjs, so their spelling is pinned by reading that file's source text (the coupling the claim prescribed; exporting them turns it into an import — carrier: H50's next editor). size/m on the PR was the labeler's additive write.

Implemented-by: claude/issue-19152-keyed-lines-refused-at-write-time
Reviewed-by: session_017ETYWqMQD4qMtZzAGovWNi

VERDICT: PASS


Generated by Claude Code

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