feat(pm): unread-knock check before a seat-post body refresh - #17957
Conversation
A seat-post body refresh closes the read window (comments newer than the body's last edit) on every comment inside it, so an unread knock vanishes with no signal to the knocker. Text half: the read-window rule now says read the tail to its end and receipt every unread knock before refreshing (SKILL.md, core-rules.md mirror, seat-post-protocol.md). Mechanical half: `post-stamped.mjs --body` refuses the write while comments newer than the body's newest stamp exist and `--ack-through=ID` does not name the newest one; self-test covers both directions and the control. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8
|
Fixture knock for this PR's acceptance run (triage 5651030225, item 3: 「贴上有一条未读 knock,然后刷新正文」). This comment is newer than the description's last write stamp; the next description refresh WITHOUT Generated by Claude Code |
Contract reviewHead: ① derived judgments — four files, +237/−9 against merge-base Implemented-by: claude/issue-17905-unread-knock-before-body-refresh (mode:subagent) Verdict: PASS. Governed surface ( Generated by Claude Code |
维护者速读(终稿)改了什么 —— 规则一句、工具一道。规则:座位贴「读侧只读正文 + 晚于正文的评论」后面接上「刷新前读完评论、回执未读敲门,再刷」( 为什么改 —— #17905 测到的形状:一次正文刷新会把所有「晚于正文」的敲门静默作废,读侧规则让后来者永远看不到它们。现在写侧被机械拦住,刷新前必须回执。 风险与代价(含回滚) —— 代价是每次正文刷新多一次读评论;误拒方向保守:「末次写入时刻」取正文里最新的协议戳(平台不暴露正文编辑时间,已实测),只会多列不会漏列,多列的代价是一次 席位意见 —— 收。本席在 head 你要做的(一个动作) —— 受管面( Generated by Claude Code |
|
Ruling C landing step — skills seat (session Generated by Claude Code |
Fixes #17905
What this PR does
A seat-post body refresh shuts the read window — 「读侧只读贴正文 + 晚于正文最后编辑的评论」, the maintainer-approved rule this card explicitly does not contest — on every comment inside it, so an unread knock vanishes with no signal to the knocker. Measured on the card: a director-ruled cross-lane request knocked on seat post 6017 at 2026-09-12T13:20:47Z, the body was refreshed at 2026-09-12T15:58:46Z, and the request reached the seat about nine hours later, via an escalation card.
This PR delivers the write-side half of the acceptance (triage 5651030225, item 1: 「写侧:座位贴正文刷新前有一道「未读 knock」检查」): a refresh is preceded by reading the comment tail to its end and receipting every unread knock, and
scripts/pm/post-stamped.mjs --bodyenforces it mechanically. The read window itself is untouched (item 2).Text half (governed
.claude/**, all three files at their line ceilings)SKILL.md:274 — the read-window sentence stays byte-for-byte and the line continues: 「读侧只读贴正文 + 晚于正文最后编辑的评论;刷新前读完评论、回执未读敲门,再刷。」 (112 bytes, in place, no line added; 812/812).references/core-rules.md:74 — the mirror, same PR: 「写侧刷新点是轮次边界,读侧只读正文加晚于它的评论;刷新前读完评论、回执未读敲门。」 (118 bytes; 151/151). To fit the 120-byte line cap the clause 「热文件串行队是正文具名段」 leaves this line: :71 of the same file already lists 热文件串行队 as one of the body's four fixed sections, and the SKILL.md source line :275 (「热文件串行队是正文具名段:区域写不清就只能整文件串行」) is untouched.references/seat-post-protocol.md— one new line under 写侧刷新三时刻: 「刷新前回执未读敲门;post-stamped.mjs --body拦截:评论晚于正文末次戳 ⇒--ack-through=ID。」 (116 bytes), paid by deleting the pointer line 「会话句柄是账号作用域的,细则见dispatch-runbook.md。」 whose fact lives in full atdispatch-runbook.md:133 (「会话句柄是账号作用域的:三个会话工具对另一账号建的会话一律答not found」) and whose consequence the handover checklist's item 8 already carries (归档义务 ⛔ 不可移交). Net 91 → 90 lines against a ceiling of 91; no ceiling moved, no re-wrap anywhere.dispatch-runbook.md(fenced to pm-dispatch: 在飞 PR 重叠只查文件面 —— 实测到一次两个 PR 各自全绿、合到一棵树才红的耦合,文件面互不相交完全看不见它 #17935 this wave) is not touched.Mechanical half (
scripts/pm/post-stamped.mjs)--body=Nnow reads the card and its comment tail before writing, and refuses (exit 2, nothing written) while comments newer than the body's last write exist and--ack-through=IDdoes not name the newest of them. Naming an older comment is refused together with the ones that landed after it; an id that is not on the card is refused. The flag is a proof of reading — a seat cannot know the newest id without reading the tail to its end — not a switch.updated_atmoves on comments and labels too), the issue timeline carries no body-edit event, andPOST /graphqlanswers 403 from this container. The derivation is a lower bound — a refresh that carried no NOW token leaves an older stamp behind — so more comments count as unread, never fewer; seat post 6015's body reads2026-09-12T00:35Zagainst a real last rewrite on 2026-09-13, which is the safe direction. A body with no stamp at all counts every comment and says so in the refusal.--commentand--dry-runare untouched; a refresh fetches only the tail (RESTsince, a superset of what the pure filter judges).--ack-through; 117 cases across 8 batteries, battery floor 7 → 8.Acceptance greps (BASE
bdb247d9→ HEAD68e8bcc8)--ack-throughSelf-test verdict lines, both directions and the control (from
node scripts/pm/post-stamped.mjs --self-test):Live fixture run (on this PR, which is an issue to the REST API)
Body stamp of the latest write of this description, read by the act that wrote it: 2026-09-13T08:20Z.
All four steps run on this PR — a PR is an issue to the REST API, so
--body=17957takes the same code path a seat post does. Comments on seat posts 6015 / 6017 were read, never written (acceptance item 4).post-stamped.mjs --comment=17957(stamp 2026-09-13T08:16Z, drift 0). The description's last write stamp at that moment was 2026-09-13T08:16Z (step 0, thePOST /pullscreate).updated_atdid not move.--ack-through=5652164685⇒ accepted, exit 0, description written at 2026-09-13T08:19:36Z (stamp 2026-09-13T08:19Z, drift 0). Transcript line as printed:unread check: 1 comment(s) newer than the body's last write stamp \2026-09-13T08:16Z`, acknowledged through 5652164685 (the newest)`.unread check: no comment newer than the body's last write stamp \2026-09-13T08:19Z` — nothing to acknowledge`; the verbatim line is quoted in the report comment on [finding] A seat-post body refresh silently expires every unread knock — the read window is 「comments newer than the last body edit」, and a director-ruled cross-lane review took 9 hours to arrive because of it #17905.Read-back note, for the footer: the
PATCHedit route appended the platform's bare attribution footer under a sent session-URL block (+58 bytes, two footers after step 3 — the reading platform-readings already records for this route), so this final write sends no footer and lets the platform's stand; the durable attribution is this sentence: written from PM dispatch session https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8.Gates
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat68e8bcc8(no paths — the tool took the change set off the merge base itself): 40 commands. Every one ran in a foreground loop with its exit code captured before any pipe (eval "$cmd" > log 2>&1; status=$?), then the record was reconciled — its verdict line:✓ dispatch-gates --ran: 40 derived famil(ies) accounted for — 40 run, 0 NOT-MEASURED (a DERIVED zero — all 40 recorded an exit code and none of them is 3).check:pm-skill-ratchet— SKILL.md 812/812 · core-rules.md 151/151 · seat-post-protocol.md 90/91; every line within the 120-byte capcheck:pm-skill-id-lintcheck:skill-frame-synccheck:pm-governed-prosecheck:nul-bytescheck:refd-timer-probecheck:pm-post-stamped— the tool's own tests, owed because the script changedcheck:pm-dispatch-gates— 1678 cases; ran past the foreground cap and was detached by the harness, log read through to its verdict linepnpm --filter @objectstack/lint run check:doc-formula-expressionsPREREQUISITE NOT MET(unbuilt workspace package: NOT MEASURED, not red); re-run afterturbo run build --filter=@objectstack/formula --filter=@objectstack/lintunder the shared verify lock (4/4 cache hits): ✓ 58 self-test cases, 22 record-scoped examples clean--self-testwhere the list carries oneNot run locally, by declaration: the whole-repo scans (
pnpm lint, the typecheck and test farms) — CI owns them. The diff touches.claude/**andscripts/pm/**only; no package'ssrcor tests move, so no package build (①) or package test (②) is owed.Line budget
node scripts/pm/check-skill-line-ratchet.mjs: SKILL.md 812/812 · core-rules.md 151/151 · seat-post-protocol.md 90/91 — all within ceiling, no ceiling edited.Acceptance notes
2026-09-12T00:35Zwhile its last rewrite was on 2026-09-13 (the rewrite wrote a date, not a stamp), so on that post the first checked refresh will list every comment since 2026-09-12T00:35Z and ask for the newest id. That is the conservative direction and one flag; it needs no card. Bearer: the triage seat, at its next refresh. Comments on 6015 / 6017 were read, not touched (acceptance item 4).pnpm --filter @objectstack/lint run check:doc-formula-expressionsanswers exit 3 (PREREQUISITE NOT MET, package unbuilt) on a fresh worktree until@objectstack/formulaand@objectstack/lintare built — documented by the gate's own text; bearer: none.维护者速读(草稿)
改了什么:座位贴协议加了一条「刷新前先把评论读到底、回执未读敲门,再刷正文」的规则(SKILL.md、核心条款镜像、座位贴协议细则各一处),并让
scripts/pm/post-stamped.mjs --body在贴上有比正文末次写入戳更新的评论、而调用方没用--ack-through=最新评论 id点名它时拒绝写入(什么都不写,退出码 2)。#7942 批准的「读侧只读晚于正文最后编辑的评论」一字未动。为什么改:那条读窗规则省 token 的理由成立,但代价是正文一刷新,窗口里没读到的敲门就被静默作废,敲门方只看到「没回应」。卡上测到的实例:总监席裁决指派的跨车道复核迟到约 9 小时,PR 空转。分诊席的座位是第二例,靠一个人的手工纪律在绕行;这次把它变成机制。
风险与代价(含回滚):每次正文刷新前多一次评论尾部读取;贴上只要有比正文末次戳更新的评论(不论是不是敲门)就要多传一个
--ack-through参数,座位读完尾部才知道该填哪个 id。正文里没有协议时间戳的贴会把全部评论都算作「更新的」,拒绝文案会明说并要求在正文里写 NOW 令牌。不改任何已发布产物,无 changeset;回滚 = revert 本 PR 一个 commit。席位意见:
你要做的:确认「写侧先回执再刷新」是本卡想要的那一半(读侧/敲门侧的投递回执未做),然后人工直合;governed 面,座位不翻 ready、不入队。
Generated by Claude Code