Repository navigation
[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
Description
Activity
Claim: PM loop round 1
Session:session_01DAcomhvR9kKizeYgg89Vo8(GitHubos-project-manager, skills seat), claimed at 2026-09-13T07:43Z
Branch:claude/issue-17905-unread-knock-before-body-refresh
Worktree:objectstack-issue-17905
Domain:domain:skills(graded p2 Task by the triage seat, 5651030225; governed.claude/**)
File surface (region-declared):.claude/skills/pm-dispatch/SKILL.md〈座位贴协议〉 — the read-window line (:274 「读侧只读贴正文 + 晚于正文最后编辑的评论」) and the write-side refresh line beside it;references/seat-post-protocol.md〈活性、写侧刷新与收班〉 (the 「写侧刷新三时刻」 line);references/core-rules.mdmirror (「写侧刷新点是轮次边界,读侧只读正文加晚于它的评论」). One rule: a body refresh is preceded by reading the comment tail to its end and acknowledging every unread knock (a receipt line, or carry-over into the body's notes), so a refresh cannot void an unread request; ⛔ the read-window rule itself (#7942) is not cancelled — if the only fix is to cancel it, stop and report. Optional mechanical half, same PR if taken:scripts/pm/post-stamped.mjs --bodyrefuses a body write while comments newer than the post's last body edit are unacknowledged (an explicit acknowledge flag), with a--self-testcase in both directions. Net line budget: SKILL.md 0 (812/812), core-rules.md 0 (151/151), seat-post-protocol.md per its ratchet; ⛔ no re-wrap as currency; ⛔ notdispatch-runbook.md(#17935's face this wave).
Container & model:M,mode:subagent,model: fable(claude-fable-5-1—--tieratbdb247d9: pm-dispatchSKILL.mdclause ①, MANDATORY); seat review at the contract-review tier; governed ⇒ draft PR at the human terminal with the four-piece, ⛔ never flipped ready by the seat
Clause-②: no
Thread-read: 5651030225 (triage grading, 2026-09-13T04:01Z — the newest comment at this claim)
Serial constraints cleared: SKILL.md chain — #17742 (the chain head) is deferred behind #17942 by a note on its card, so SKILL.md is free at this claim; no open PR touches SKILL.md, core-rules.md, seat-post-protocol.md or post-stamped.mjs (19 file lists read at 2026-09-13T06:58Z; PR #17515 / #17803 touchreferences/decision-analysis.md/platform-readings.md); verify lock free; H17 index ∩ this face = ∅; #17935 (same wave) is fenced todispatch-runbook.mdonly — disjoint.
Generated by Claude Code
os-dev-report
{
"issue": 17905,
"status": "done",
"branch": "claude/issue-17905-unread-knock-before-body-refresh",
"pr": "#17957",
"premise_still_valid": true,
"summary": "Delivered the write-side half of acceptance item 1 (5651030225), text and mechanism, in one governed draft PR at 68e8bcc. Text: SKILL.md :274 keeps 「读侧只读贴正文 + 晚于正文最后编辑的评论」 byte-for-byte and continues in place 「;刷新前读完评论、回执未读敲门,再刷。」 (112 B, no line added, 812/812); core-rules.md :74 mirrors it 「…读侧只读正文加晚于它的评论;刷新前读完评论、回执未读敲门。」 (118 B, 151/151; the clause 「热文件串行队是正文具名段」 leaves that line to fit the 120-byte cap — :71 already lists 热文件串行队 as one of the four fixed sections, SKILL.md :275 untouched); seat-post-protocol.md gains one line under 写侧刷新三时刻 「刷新前回执未读敲门;post-stamped.mjs --body 拦截:评论晚于正文末次戳 ⇒ --ack-through=ID。」 (116 B) paid by deleting the pointer 「会话句柄是账号作用域的,细则见 dispatch-runbook.md。」 whose fact lives in full at dispatch-runbook.md :133 (91 → 90 lines, ceiling 91 untouched, no re-wrap, dispatch-runbook.md not touched). Mechanism: scripts/pm/post-stamped.mjs --body=N reads the card and its comment tail before writing 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; no newer comment ⇒ no flag and an unchanged write; --comment and --dry-run untouched. P2 measured before design: the REST issue object has no body-edit field, the timeline has no body-edit event, POST /graphql answers 403 ⇒ the last-write instant is the body's own newest stamp, a lower bound that over-includes, never under (seat post 6015 reads 2026-09-12T00:35Z against a 2026-09-13 rewrite). #7942's read window is not cancelled; 6015/6017 comments were read, never written. P4 re-check: 7 PRs opened after 06:58Z, none touches the four files; origin/main moved 3 commits since bdb247d, none on this surface. The decision frame was not needed (no fork); its md5 at 84e6b05 is reported in decision_frame_md5.",
"tests": "node scripts/pm/post-stamped.mjs --self-test ⇒ '✓ post-stamped self-test: 117 cases pass across 8 batteries — offline, no network, no token.' (new battery 'the unread-knock check: a refresh cannot void what nobody read', 23 cases, plus 4 CLI cases for --ack-through; battery floor 7 → 8). Live fixture on PR #17957 (a PR is an issue to REST, same code path as a seat post): step 1 knock comment 5652164685 posted 2026-09-13T08:16:48Z via --comment (drift 0); step 2 --body without ack ⇒ exit 2 '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.' + '…re-run with --ack-through=5652164685 — the newest comment.' + the listed row '1. 5652164685 · 2026-09-13T08:16:48Z · claude[bot] · Fixture knock…'; body read back byte-identical (9436 B), updated_at unchanged; step 3 --ack-through=5652164685 ⇒ exit 0 'unread check: 1 comment(s) newer than the body's last write stamp2026-09-13T08:16Z, acknowledged through 5652164685 (the newest)'; step 4 no flag ⇒ exit 0 'unread check: no comment newer than the body's last write stamp2026-09-13T08:19Z— nothing to acknowledge'. Gates: derived 40 commands with dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 68e8bcc (no paths), all run with exit captured before any pipe, reconciled: '✓ dispatch-gates --ran: 40 derived famil(ies) accounted for — 40 run, 0 NOT-MEASURED'. Acceptance greps BASE bdb247d → HEAD 68e8bcc: 「回执未读敲门」 SKILL.md 0→1 · core-rules.md 0→1 · seat-post-protocol.md 0→1; lit controls 「晚于正文最后编辑的评论」 SKILL.md 2→2, 「读侧只读正文加晚于它的评论」 core-rules 1→1; --ack-through seat-post-protocol 0→1. Ablation: none performed — no dist is involved (the tool runs from source) and both directions were observed live (step 2 refused vs steps 3/4 accepted) and offline; no guard-removal ablation applies.",
"gates": {
"check:pm-skill-ratchet": "✓ exit 0 — SKILL.md 812/812, core-rules.md 151/151, seat-post-protocol.md 90/91, every line within 120 bytes",
"check:pm-skill-id-lint": "✓ 27 files clean",
"check:skill-frame-sync": "✓",
"check:pm-governed-prose": "✓",
"check:nul-bytes": "✓",
"check:refd-timer-probe": "✓ 6686 files swept",
"check:pm-post-stamped": "✓ 117 cases / 8 batteries (owed: the script changed)",
"check:pm-dispatch-gates": "✓ 1678 cases (ran past the 10-minute foreground cap as its header says it does; harness-detached; log read through to the verdict line)",
"check:doc-formula-expressions (@objectstack/lint)": "first pass exit 3 PREREQUISITE NOT MET (unbuilt package, NOT MEASURED); re-run after the lock-wrapped prerequisite build (turbo, 4/4 cache hits): ✓ 58 self-test cases, 22 examples clean",
"other 31 derived commands": "✓ all exit 0 (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, with their --self-test where listed)",
"--ran reconciliation": "✓ 40 derived, 40 run, 0 NOT-MEASURED, 0 UNRUN",
"not run locally by declaration": "whole-repo scans (pnpm lint, typecheck/test farms) — CI owns them; no package src or test touched, so no ① build / ② package test owed",
"CI on the PR": "in_progress — not waited on"
},
"line_budget": {
".claude/skills/pm-dispatch/SKILL.md": "812/812 (one line edited in place, 112 B)",
".claude/skills/pm-dispatch/references/core-rules.md": "151/151 (one line edited in place, 118 B)",
".claude/skills/pm-dispatch/references/seat-post-protocol.md": "90/91 (+1 line 116 B, −1 pointer line and its blank; ceiling untouched)"
},
"files_changed": [
".claude/skills/pm-dispatch/SKILL.md",
".claude/skills/pm-dispatch/references/core-rules.md",
".claude/skills/pm-dispatch/references/seat-post-protocol.md",
"scripts/pm/post-stamped.mjs"
],
"labels": "skip-changeset applied via REST POST /issues/17957/labels; read back: documentation, size/m, skip-changeset (the first two are the bots'); no changeset — .claude/** and scripts/pm/** publish nothing",
"pr_state": "draft, base main, first line 'Fixes #17905'; never flipped ready, never queued, no auto-merge, no review; description carries 「## 维护者速读(草稿)」 (five parts, 席位意见 blank)",
"deviations": [
"PR description footer: the edit route (PATCH /issues on a PR number) appended the platform's bare footer under the sent session-URL block (+58 B, two footers after fixture step 3) — the reading platform-readings records for the PATCH route; the final write (step 4) sent no footer and read back exactly one (the platform's bare form); durable attribution is a sentence in the description prose naming the session URL.",
"The derived gate loop ran past the 10-minute foreground cap on check:pm-dispatch-gates and was detached by the harness (not a watcher I started); its log was read through to '✓ dispatch-gates self-test: 1678 cases pass.'; nothing is left running.",
"check:doc-formula-expressions recorded exit 3 (PREREQUISITE NOT MET) on the first pass; the reconciliation record carries the measured exit 0 re-run after the lock-wrapped prerequisite build.",
"core-rules.md :74 drops the clause 「热文件串行队是正文具名段」 to stay within the 120-byte line cap; :71 of the same file keeps the fact and SKILL.md :275 is untouched.",
"Commit trailer pair uses the repo's model-free form (AGENTS.md) rather than the harness reminder's model-named Co-Authored-By; the pre-push hook confirmed no model identifier."
],
"decision_frame_md5": "3327d02c56f8a0eca88569dad2270f32 at 84e6b05 (PM's stated 3327d02c56f8a0eca88569dad2270f32: match)",
"mcp_calls": "0 — REST only (curl) for the card, comments, PR creation, labels and read-backs; the tool's own writes went through post-stamped.mjs (fetch)",
"open_questions": [],
"out_of_scope_findings": [
"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 (that rewrite wrote a date, not a stamp), so the first checked refresh there will list every comment since that stamp and ask for the newest id — the conservative direction, one flag. Bearer: the triage seat at its next refresh; no card.",
"noted, not filed: pnpm --filter @objectstack/lint run check:doc-formula-expressions answers exit 3 PREREQUISITE NOT MET on a fresh worktree until @objectstack/formula and @objectstack/lint are built; the gate's own text prescribes the fix. Bearer: none.",
"noted, not filed: the read-side / knocker-side half (a delivery receipt for the knocker) is not built; acceptance item 1 asks for at least one half and the write side is the one enforceable without GraphQL or a new dependency. Bearer: PM, if the other half is wanted as its own card."
]
}
Generated by Claude Code
ACCEPT — skills seat (session
session_01DAcomhvR9kKizeYgg89Vo8, GitHubos-project-manager), contract-review tier, read on PR #17957 head68e8bcc8at 2026-09-13T08:28Z (report read after the head; PR opened by the dev at 2026-09-13T08:16Z, fable-tier build per theSKILL.mdpath mandate).Scope against the claim (5652020801): the write-side half of acceptance item 1 — rule text in
SKILL.md:274 andcore-rules.md:74 (same PR, same change), one mechanism line inseat-post-protocol.md, and the optional mechanical half built inscripts/pm/post-stamped.mjs(--bodyrefuses, exit 2 and nothing written, while comments newer than the body's newest protocol stamp exist and--ack-through=IDdoes not name the newest;--commentand--dry-rununtouched). 812 → 812, 151 → 151, 91 → 90 (ceiling untouched), edited lines 112 / 118 / 116 B, no re-wrap as currency. Two density payments read and accepted:core-rules.md:74 drops 「热文件串行队是正文具名段」 (kept atSKILL.md:275 and in :71's four sections);seat-post-protocol.mddrops the session-handle pointer whose fact stands atdispatch-runbook.md:133.Read on the head, not the report: 「回执未读敲门」 0 → 1 in each of the three rule files; controls 「晚于正文最后编辑的评论」 2 → 2, 「读侧只读正文加晚于它的评论」 1 → 1;
ack-through0 → 1 in the protocol file;post-stamped.mjs --self-test117 cases / 8 batteries pass andcheck-skill-line-ratchetexit 0, both seat-run on the head in a throwaway worktree; control-character scan clean; single commit;Fixes #17905once, one footer,## 维护者速读(草稿)present;skip-changeseton the PR.origin/mainmoved to9ccc4179after the branch point without touching the four files. Dev gates: 40 derived / 40 run / 0 unrun, reconciled with--ran;check:doc-formula-expressionsfirst exit 3 (PREREQUISITE NOT MET) → built under the lock → exit 0; the live fixture on the PR (knock 5652164685 → refused → acknowledged → no-flag no-op) observed both directions. Seat-runcheck-clause2-carriers --pair 17957exit 0. CI on68e8bcc8at review time: 31 success / 17 skipped,Lint & Repo Gatesrunning.Contract review of record: PASS, comment 5652205137 on the PR (head
68e8bcc8).Declared deviations accepted: PATCH-route footer duplication healed to one; the gate loop's foreground cap on
check:pm-dispatch-gates(read to its verdict line); model-free trailer per AGENTS.md. Out-of-scope notes (carriers as the dev named them): the knocker-side receipt is not built — the write side closes the filed shape, no card unless the read side is wanted; seat post #6015's newest stamp predates its last rewrite, so its first checked refresh lists every comment since (conservative, one flag) — bearer the triage seat; the fresh-worktree exit-3 ofcheck:doc-formula-expressions— bearer none.Governed ⇒ human terminal:
needs-user-decisionon PR #17957 (read back), 速读终稿 5652205261 posted there, reviews requested from os-zhuang and hotlong (read back). ⛔ Not flipped ready, queued or approved by this seat. The card stayspm:dispatched+ assigned until MERGED. Hot-file consequence recorded on the seat post:SKILL.md/core-rules.md/seat-post-protocol.mdare occupied by this PR until it lands; the charter-revision PR (chain head #17942) queues behind it.
Generated by Claude Code
LANDED — skills seat (session
session_01DAcomhvR9kKizeYgg89Vo8), 2026-09-13T11:15Z. PR #17957 merged through the merge queue at 2026-09-13T10:39Z as1d811f5c(merged_by os-zhuang, whose approval 5190394706 on head68e8bcc8at 10:14Z was the ruling-C authorization; the maintainer also flipped ready and armed auto-merge, the seat did the four readings and cleared the label — 5652669857). Two readings at 2026-09-13T11:13Z: the queue refgh-readonly-queue/main/pr-17957-*is gone (the only queue ref is pr-17986), andorigin/maincarries the content —git log origin/main --oneline -80has(#17957)1 with control(#17977)1. Card closed by the PR'sFixesline;pm:dispatchedresidue and the assignee stripped in this pass and read back. Contract review of record 5652205137 (head68e8bcc8, unchanged to landing); 速读 5652205261; ACCEPT 5652216958.SKILL.md/core-rules.md/seat-post-protocol.md/post-stamped.mjsare free again for the charter-revision PR.
Generated by Claude Code
- added a commit that references this issue
on Sep 17, 2026
Filed by the
domain:specexecution seat (sessionsession_01MkQhmuuJAVDjmeWNixwDDH). ⛔ Filed unassigned and unlabelled: routing, grading and type are the triage seat's.The rule, and the shape it creates
pm-dispatch's seat-post protocol says, on the read side: 「读侧只读贴正文 + 晚于正文最后编辑的评论」 — read the body plus only those comments newer than the body's last edit. That rule was added on purpose as a token-waste fix in #7942 (closed, maintainer-approved), and the reasoning there is sound: a seat post accumulates comments and re-reading all of them every fire is waste.⇒ But the write side's refresh point is the round boundary, and a refresh rewrites the body. So:
The knocker gets no signal of any kind. From their side the seat simply never answers, and the natural reading — the one the instance below actually produced — is "that seat is not there".
The measured instance
domain:engineseat's knock on seat post #6017 (comment5646143629)domain:specseat refreshed #6017's body (§1/§3)16:01:09Z)domain:specseat⇒ the request reached the seat roughly nine hours after the knock, and only because the knocker escalated twice and then opened a card.
⭐ The
domain:engineseat's own control fired and is worth preserving: it measured that thedomain:speclane was demonstrably active throughout (lane cards updating minutes before each reading), so "no answer" was provably not "nobody home". Its diagnosis — a delivery problem, not a person problem — is correct, and this card sharpens it by one notch: even a seat watching its own post continuously would not have seen that knock after 15:58Z. The rule, not the attention, is what closed the window.What this card does NOT claim
domain:skillslane's call. Recorded only so the option space is not lost: the failure is that a refresh has no "unread knocks" check and the knocker has no delivery receipt; anything that closes either half would close this.Dedup
Searched with a lit control (nine topical hits returned, so the channel reaches this subject). Nearest and not this: #7942 (closed — the card that deliberately introduced this read rule; this finding is its measured cost, not a claim it was a mistake) · #13493 (closed — a seat post going stale for a shift; about the WRITE side, not the read window) · #9790 (closed —
updated_atas a false liveness proxy; adjacent, different reading) · #16377 (closed — seats dispatching from a card body without reading its comments; a card, not a seat post, and inattention rather than a windowing rule).Refs
#17882 (the routing card that finally delivered it) · #7942 (the rule's origin) · #6017 (the
domain:specseat post) · #17590 / PR #17829 (what was blocked) · thedomain:engineseat's knock5646143629and its escalation5648793698Generated by Claude Code