Skip to content

feat(pm): unread-knock check before a seat-post body refresh - #17957

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-17905-unread-knock-before-body-refresh
Sep 13, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-17905-unread-knock-before-body-refresh

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

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 --body enforces 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 at dispatch-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=N now 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=ID does 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.
  • "The body's last write" is derived from the body's own newest protocol stamp. Measured first (Zone 2 P2): the REST issue object exposes no body-edit time (updated_at moves on comments and labels too), the issue timeline carries no body-edit event, and POST /graphql answers 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 reads 2026-09-12T00:35Z against 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.
  • The control from acceptance item 3 holds exactly: no comment newer than the last write ⇒ no flag, no refusal, and the write is what it was before this check existed. The tool cannot tell a knock from any other comment (one shared identity, no content classifier), so one newer comment of any kind asks for one flag naming it. --comment and --dry-run are untouched; a refresh fetches only the tail (REST since, a superset of what the pure filter judges).
  • Self-test: a new battery "the unread-knock check: a refresh cannot void what nobody read" (23 cases: the filed shape refused, the control accepted, ack-newest accepted, ack-older refused with the trailing comments, unknown id refused, no-stamp conservative, minute boundary both sides, ordering by created_at, unreadable created_at treated as newest) plus 4 CLI cases for --ack-through; 117 cases across 8 batteries, battery floor 7 → 8.

Acceptance greps (BASE bdb247d9 → HEAD 68e8bcc8)

grep SKILL.md core-rules.md seat-post-protocol.md
「回执未读敲门」 (the receipt-before-refresh clause) 0 → 1 0 → 1 0 → 1
「晚于正文最后编辑的评论」 (lit control: the read window stays) 1 → 1
「读侧只读正文加晚于它的评论」 (lit control, the mirror) 1 → 1
--ack-through 0 → 0 0 → 0 0 → 1

Self-test verdict lines, both directions and the control (from node scripts/pm/post-stamped.mjs --self-test):

✓ ⭐ the filed shape: a knock newer than the body's last stamp, no acknowledgement ⇒ REFUSED
✓ ⭐ THE CONTROL: no comment newer than the last write ⇒ accepted with no flag at all
✓ ⭐ acknowledging the newest comment ⇒ accepted
✓ ⛔ acknowledging an OLDER comment is refused — the flag proves reading, it is not a switch
✓ post-stamped self-test: 117 cases pass across 8 batteries — offline, no network, no token.

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=17957 takes the same code path a seat post does. Comments on seat posts 6015 / 6017 were read, never written (acceptance item 4).

  1. Knock — comment 5652164685, posted at 2026-09-13T08:16:48Z through 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, the POST /pulls create).
  2. Refresh WITHOUT acknowledgement ⇒ refused, exit 2, nothing written. Verdict as printed (stamps quoted verbatim):
    post-stamped: REFUSED — 1 comment(s) on #17957 are newer than the body's last write stamp (`2026-09-13T08:16Z`) and this refresh acknowledges none of them. Nothing was written.
      …Read the tail to its end, receipt every request it carries (a reply comment, or a
      carry-over into the body), then re-run with --ack-through=5652164685 — the newest comment.
      1. 5652164685 · 2026-09-13T08:16:48Z · claude[bot] · Fixture knock for this PR's acceptance run …
    
    The description read back byte-identical to the pre-step body (9436 bytes) and its updated_at did not move.
  3. Refresh WITH --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)`.
  4. Refresh with nothing newer, no flag ⇒ accepted — this very write of the description. Measured against step 3's stamp, the knock (posted at 2026-09-13T08:16:48Z) is no longer inside the window, so the transcript line is 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 PATCH edit 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/objectstack at 68e8bcc8 (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).

family verdict
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 cap ✓ exit 0
check:pm-skill-id-lint ✓ 27 files clean
check:skill-frame-sync ✓ the one declared copy coherent
check:pm-governed-prose ✓ 5 governed surfaces named, no others
check:nul-bytes
check:refd-timer-probe ✓ 6686 files swept
check:pm-post-stamped — the tool's own tests, owed because the script changed ✓ 117 cases across 8 batteries
check:pm-dispatch-gates — 1678 cases; ran past the foreground cap and was detached by the harness, log read through to its verdict line
pnpm --filter @objectstack/lint run check:doc-formula-expressions first run exit 3 PREREQUISITE NOT MET (unbuilt workspace package: NOT MEASURED, not red); re-run after turbo run build --filter=@objectstack/formula --filter=@objectstack/lint under the shared verify lock (4/4 cache hits): ✓ 58 self-test cases, 22 record-scoped examples clean
the other 31 derived commands — reference-carrier-shape, ci-filter-parity, closing-keyword-parity, comment-mask-corpus, declaration-mirrors, scripts-symbol-anchors, self-test-wired, self-test-workflow-commands, whole-set-label-write, bare-root-worklist, governed-queue-guard, report-test-timings, agent-test-spelling, bash32-floor, cli-command-ids, cross-package-test-inputs, doc-authoring, driver-memory-census, entry-guard, parse-guard, pm-governed-merges, pnpm-filter-targets, ratchet-remedy-authority, watch-hint-literal, each with its --self-test where the list carries one ✓ all exit 0

Not run locally, by declaration: the whole-repo scans (pnpm lint, the typecheck and test farms) — CI owns them. The diff touches .claude/** and scripts/pm/** only; no package's src or 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

  • noted, not filed: seat post 6015's body carries 28 protocol stamps whose newest is 2026-09-12T00:35Z while 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).
  • noted, not filed: pnpm --filter @objectstack/lint run check:doc-formula-expressions answers exit 3 (PREREQUISITE NOT MET, package unbuilt) on a fresh worktree until @objectstack/formula and @objectstack/lint are built — documented by the gate's own text; bearer: none.
  • The read-side / knocker-side half (a delivery receipt for the knocker) is not built here; acceptance item 1 asks for at least one half, and the write side is the one the tool can enforce without GraphQL or a new dependency.

维护者速读(草稿)

改了什么:座位贴协议加了一条「刷新前先把评论读到底、回执未读敲门,再刷正文」的规则(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

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
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 13, 2026
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 13, 2026
@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

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 --ack-through must be refused, the one WITH --ack-through naming this comment's id must be accepted, and a later refresh with nothing newer must pass with no flag. Posted 2026-09-13T08:16Z through scripts/pm/post-stamped.mjs --comment.


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Head: 68e8bcc8 (PR #17957, card #17905) — read at 2026-09-13T08:25Z by the skills seat at the contract-review tier (served model claude-fable-5-1, get_session external_metadata.last_served_model at 2026-09-13T08:08Z); the build was at tier too (model: fable, the SKILL.md path mandate), so this record is the review of record rather than a compensating control.

① derived judgments — four files, +237/−9 against merge-base bdb247d9 (origin/main moved to 9ccc4179 since; no main-side touch on any of the four, measured empty diffstat). (a) Rule text: SKILL.md :274 keeps 「读侧只读贴正文 + 晚于正文最后编辑的评论」 byte-for-byte and continues 「;刷新前读完评论、回执未读敲门,再刷。」 (112 B); core-rules.md :74 mirrors it in the same PR (118 B) — the clause 「热文件串行队是正文具名段」 it drops to fit the cap survives at SKILL.md :275 and in :71's four fixed sections, so no rule is lost; seat-post-protocol.md gains the mechanism line 「刷新前回执未读敲门;post-stamped.mjs --body 拦截:评论晚于正文末次戳 ⇒ --ack-through=ID。」 (116 B) paid by deleting the pointer 「会话句柄是账号作用域的,细则见 dispatch-runbook.md。」 whose fact stands in full at dispatch-runbook.md :133 (read on the head). (b) Mechanism: scripts/pm/post-stamped.mjs --body=N reads the card and its comment tail first and REFUSES (exit 2, nothing written) while comments newer than the body's newest protocol stamp exist and --ack-through=ID does not name the newest of them; an older or unknown id is refused with the trailing comments listed; --comment is untouched and --ack-through beside it is refused as meaningless; --dry-run stays offline. The last-write instant is the body's own newest stamp — a lower bound that over-includes (REST exposes no body-edit field, the timeline no body-edit event, GraphQL answers 403: measured by the dev), i.e. the conservative direction, one flag. Seat-run on the head in a throwaway worktree: post-stamped.mjs --self-test → 117 cases / 8 batteries pass (the new battery holds the filed shape: knock newer than the stamp, no ack ⇒ REFUSED naming the newest id); check-skill-line-ratchet exit 0. The live fixture on this PR (knock 5652164685 → refusal → --ack-through → acceptance → no-flag no-op) is in the report and the comment stays on the PR as its record. (c) Acceptance greps head vs merge-base: 「回执未读敲门」 0 → 1 in each of the three rule files; controls 「晚于正文最后编辑的评论」 2 → 2 (SKILL.md), 「读侧只读正文加晚于它的评论」 1 → 1 (core-rules.md); ack-through 0 → 1 in seat-post-protocol.md; lines 812 → 812, 151 → 151, 91 → 90 (ceiling 91 untouched); control-character scan clean. No accept-set or public-surface change (check-clause2-carriers --pair 17957 exit 0; Clause-②: no, no widening tell).
② semver: .claude/** and scripts/pm/** publish nothing ⇒ skip-changeset (present, read back); consistent with the declaration.
③ boundary flags: none raised (open_questions: []). Declared deviations accepted: the PATCH-route footer duplication healed to one footer on the final write; check:pm-dispatch-gates (1678 cases) ran past the foreground cap and was read to its verdict line; the trailer pair uses the repo's model-free form. Out-of-scope note carried: the knocker-side receipt (the other half of acceptance item 1) is not built — the write side closes the filed shape, no card now; the PM reopens it as its own card only if the read side is wanted.

Implemented-by: claude/issue-17905-unread-knock-before-body-refresh (mode:subagent)
Reviewed-by: session_01DAcomhvR9kKizeYgg89Vo8

Verdict: PASS. Governed surface (.claude/**, SKILL.md at the mandatory tier): no gate carrier was hung (the content limb is reviewed here); the PR stays a draft for the maintainer's hand merge. Hot-file consequence: this PR now occupies SKILL.md, core-rules.md and seat-post-protocol.md, so the charter-revision PR (chain head #17942) queues behind its landing.


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)

改了什么 —— 规则一句、工具一道。规则:座位贴「读侧只读正文 + 晚于正文的评论」后面接上「刷新前读完评论、回执未读敲门,再刷」(SKILL.md :274 与 core-rules.md :74 同 PR 同改,seat-post-protocol.md 加一行机制说明)。工具:post-stamped.mjs --body 在改写座位贴正文前先读评论尾巴,若有评论晚于正文末次戳而没有用 --ack-through=ID 回执最新那条,就拒绝写(exit 2,什么都不动),并把没读的评论列出来。三个规则文件 812/812、151/151、91→90,没有抬上限。

为什么改 —— #17905 测到的形状:一次正文刷新会把所有「晚于正文」的敲门静默作废,读侧规则让后来者永远看不到它们。现在写侧被机械拦住,刷新前必须回执。

风险与代价(含回滚) —— 代价是每次正文刷新多一次读评论;误拒方向保守:「末次写入时刻」取正文里最新的协议戳(平台不暴露正文编辑时间,已实测),只会多列不会漏列,多列的代价是一次 --ack-through--comment 路径与 --dry-run 不受影响。回滚 = revert 这一个 PR。

席位意见 —— 收。本席在 head 68e8bcc8 上亲核:三个文件的新句各 0→1、对照句不变;dispatch-runbook.md :133 仍完整承载被删指针的事实;在一次性 worktree 里跑 post-stamped.mjs --self-test 117 例全过、棘轮 exit 0;check-clause2-carriers --pair 17957 exit 0;dev 侧 40 个派生门禁全 exit 0 并对账;契约复核 PASS 记录在本 PR 上一条。PR 上那条「Fixture knock」评论是验收现场的道具,留作记录。CI 在读时 31 绿、17 跳过、Lint & Repo Gates 仍在跑;base 前进过但不碰这四个文件。

你要做的(一个动作) —— 受管面(.claude/**,SKILL.md 在 fable 强制档):CI 转绿后 approve 并人工合入。⚠️ 这个 PR 占着 SKILL.md,你今早裁的章程修订(#17942 链:跟进到底、三分表、受管面分层、#17934 形状)要等它落地才能开 PR —— 先合它就解开了。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 13, 2026 10:14
@os-zhuang
os-zhuang enabled auto-merge September 13, 2026 10:14
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 13, 2026
@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Ruling C landing step — skills seat (session session_01DAcomhvR9kKizeYgg89Vo8), 2026-09-13T10:18Z. Authorized approval read: os-zhuang APPROVED on head 68e8bcc8 at 2026-09-13T10:14Z (review 5190394706; not dismissed). The maintainer also flipped the PR ready and armed auto-merge at the same minute, so the seat's remaining ruling-C duties were the four readings and the label: ① contract review of record 5652205137 names this head 68e8bcc8 (unchanged since); ② check-clause2-carriers --pair 17957 exit 0 at 2026-09-13T10:18Z; ③ 54 checks on 68e8bcc8, 0 pending, 0 red; ④ needs-user-decision cleared in this pass and read back (labels now documentation, size/m, skip-changeset). Queue ref gh-readonly-queue/main/pr-17957-* present at 2026-09-13T10:18Z — enqueued (the REST auto_merge field reads null on this queue repo; the queue ref is the reading). Landing record follows on card #17905 once origin/main carries (#17957).


Generated by Claude Code

Merged via the queue into main with commit 1d811f5 Sep 13, 2026
58 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-17905-unread-knock-before-body-refresh branch September 13, 2026 10:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants