Skip to content

feat(pm): a seat timestamp comes from the clock that writes it — {{NOW}} on the write side, H56 on the read side - #17326

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-17314-stamped-timestamps
Sep 10, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-17314-stamped-timestamps

Conversation

@os-justin

@os-justin os-justin commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #17314

A seat's timestamp had two possible origins and only one of them was legal. This makes the illegal one unspellable on the write side and visible on the read side, and both halves read ONE definition of what a stamp is.

Filed as mechanism, not discipline, because the discipline remedy is spent: the triage seat post carries the same failure twice — R+164 (「轮次计时全靠感觉,错了约 4 倍」) and R+165, ~70 minutes off with roughly fifteen audit comments stamped from memory — with a "measure it next time" note written between them that did not hold.

Both halves, and why both

The card offers either half. Both landed, because neither alone removes the spelling:

  • the write side removes it on the channel it owns — a seat that calls the tool cannot type a time;
  • the read side is what sees the artefacts that did not come through the tool. The recorded failure's ~15 comments carried no token at all, so a write-side-only mechanism would not have caught it.

Write side — scripts/pm/post-stamped.mjs

Posts a comment (POST /repos/{o}/{r}/issues/{n}/comments) or rewrites a card body (PATCH …/issues/{n}) over REST with GH_TOKEN, body from --file=PATH or stdin. Two spellings, no third:

  • {{NOW}} — the clock read in THIS invocation. Two tokens in one body get one stamp: one clock, read once.
  • {{WAS:YYYY-MM-DDThh:mmZ}} — a stamp that is a reading of something else, declared and rendered verbatim. Its contents must be exactly one stamp, so it is a declaration and not a free-text escape.

Then it reads the artefact back and prints the proof: id, url, the platform's own write time (created_at for a comment, updated_at for a body edit — that is when the write happened), the DRIFT between that instant and the substituted stamp, and a byte comparison of the stored body against the bytes sent, so a body the sanitizer mutated is reported rather than assumed.

--dry-run prints the substituted body and writes nothing. It ACTS by default where its sibling sweep-closed-cards.mjs dry-runs, and the header argues the asymmetry: that one is a machine choosing its own targets across the archive, this one writes one artefact to a target the caller named, and a helper with friction gets bypassed for the channel that has none.

Read side — H56 in scripts/pm/check-half-states.mjs

Report-only, band state — H44's band, because it is H44's class one field over: a reading that cannot be dated (H44), and a reading whose date is wrong (H56). It reads exactly the corpus H44 already fetched and buys no request, placed after H44's loop and BEFORE H46, whose leg (b) widens the cache — reading after H46 would give the two censuses different populations.

Two false-positive sources are closed rather than tolerated:

  • a minute-grained stamp names a MINUTE, so 2026-09-10T06:37Z with a created_at of 06:37:48 is zero drift, not one rounded minute. Without this every correctly stamped comment in the corpus would file a row.
  • an EDITED comment keeps its original created_at while its text may legitimately have been rewritten, so the comparison is against the whole created_at..updated_at window. Where that window is wide the row goes quiet — a stated under-read, declared in the census clause.

A line carrying more than one stamp is HELD OUT and counted, never guessed at: which of two stamps is the act's own is a meaning question, and answering it by guessing would file a row against a correctly stamped artefact. So the rows are a LOWER BOUND and the census says by how much.

The refusals, demonstrated

$ node scripts/pm/post-stamped.mjs --comment=17314 --file=demo-bare.md ; echo "EXIT=$?"
post-stamped: REFUSED — 1 stamp-contract problem(s) in the body. Nothing was written.
  1. [positional] the opening line carries the bare stamp `2026-09-10T05:27Z`. That position belongs
     to the writing act, so a stamp typed there is the act's own time written from memory — the defect
     this tool exists to make unspellable. Write `{{NOW}}` there, or `{{WAS:2026-09-10T05:27Z}}` if it
     is genuinely a reading of something else.
EXIT=2

$ node scripts/pm/post-stamped.mjs --comment=17314 --file=demo-mixed.md ; echo "EXIT=$?"
post-stamped: REFUSED — 1 stamp-contract problem(s) in the body. Nothing was written.
  1. [mixed] this body uses `{{NOW}}` and also carries the bare stamp `2026-09-08T14:00Z`. One of the
     two clocks was read by this act and the other was typed; a reader cannot tell which. …
EXIT=2

$ node scripts/pm/post-stamped.mjs --dry-run --comment=17314 --file=demo-ok.md ; echo "EXIT=$?"
post-stamped: DRY RUN — nothing was written. 1 token(s) substituted with `2026-09-10T06:53Z`,
1 quoted stamp(s) rendered verbatim. Target would be objectstack-ai/objectstack#17314 (comment).
Claim: skills seat, session `session_x`, 2026-09-10T06:53Z — dispatched into the round.

The board read 2026-09-08T14:00Z still stands.
EXIT=0

⛔ The positional refusal is not the mixed one widened for tidiness. Mixed alone never fires on a seat that simply does not type {{NOW}} — which is every comment in the recorded failure.

The pointer line — LANDED, net-zero

.claude/skills/pm-dispatch/references/seat-post-protocol.md is at 91/91 with zero headroom, so the pointer was folded in place rather than added, at L28:

-- 写侧刷新点的三个时刻:派发时、收班时、暂停或交接前。
+- 写侧刷新三时刻:派发、收班、暂停或交接前;时间戳恒取自 `scripts/pm/post-stamped.mjs`。

91 lines before and after (check:pm-skill-ratchet green), 112 bytes against the 120-byte per-line budget, all three refresh moments intact. The {{NOW}} token itself is NOT spelled in the skill file — it did not fit the byte budget without dropping one of the three moments, and the tool documents it in --help and refuses every other spelling, so naming the tool names the whole mechanism. references/platform-readings.md was not touched (PR #17317 holds it).

维护者速读(草稿)

改了什么 — 座位往 GitHub 写的每一个时间戳,现在都由写它的那一次调用去读钟。新工具 scripts/pm/post-stamped.mjs 负责发评论/改正文:正文里写 {{NOW}} 占位,工具在同一次调用里取 UTC 时刻替换,写完再回读一次,把平台自己盖的时间、与替换时刻的差、以及存下来的字节是否被改动都打印出来。配套的 check-half-states.mjs 加了一条只报告不拦截的 H56 行,拿平台的 created_at 去核对评论里手写的时间戳,超过 15 分钟的列出来。座位贴协议在原有行数内改写一行,点名这个工具。

为什么改 — 同一个错误在分诊座位贴上记录了两次(R+164 与 R+165,后者差了约 70 分钟、约 15 条审计评论带着估算时间),中间那次「下回记得测量」的纪律性补救没有守住。估算出来的时间戳此前有拼法可写,这个 PR 把这个拼法删掉。

风险与代价(含回滚) — 新工具没有任何自动调用方:workflow 只跑它的离线自检,不会往任何卡片写字。风险集中在 H56 落地当天可能在巡查锚点上一次性冒出若干行——它是 report-only、band 为 state,预算不够时本来就会被裁掉,不会挡任何门禁。误报的两个来源(分钟粒度、评论被编辑过)已在实现里各自关掉。回滚代价极低:H56 一行注册加一段循环,工具是独立新文件,都可以单独还原,不牵动任何已发布的包。

席位意见 —

你要做的 — 只需决定要不要把「时间戳走这个工具」推成全体座位的默认写法。合并本身按受管面规矩由维护者手动合;不需要发版,不涉及任何已发布的包。

Acceptance notes

  • Gates: 74 derived, 71 run green, 3 NOT MEASURED, 0 UNRUN. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran <record> exits 0 — 「74 derived famil(ies) accounted for — 71 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3)」. Every exit code was captured before any pipe and recorded as <command> :: exit <code>, which is what lets the tool derive the NOT-MEASURED count instead of taking the runner's word for it. The three are check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt, each exiting 3 PREREQUISITE NOT MET because it loads BUILT output; each ran its own self-test green first. They are derived from package.json being in the diff, not from any package changing — no package source is touched, so no build closure is owed. check:doc-formula-expressions also refused its prerequisite first; it was cleared by building @objectstack/formula and @objectstack/lint under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0 · held the lock 156s · waited 0s) and then exited 0.
  • pnpm lint (repo-wide, eslint . --no-inline-config): exit 0 on the final commit 0e9f0fd93. Run whole rather than narrowed, so no narrowing claim is being made.
  • The tool refuses a body that discusses its own tokens. The os-dev-report comment on A timestamp a seat writes into GitHub must come from the clock read by the same act that writes it — make an estimated stamp unspellable (helper token and/or a report-only patrol row), after two recorded ~1h mis-estimates #17314 came back exit 2 [quoted-not-a-stamp] on its first attempt, because its prose spelled the quoted-stamp token literally. That is the contract holding — there is deliberately no escape hatch — and the remedy is the author's: name the token without its braces. Recorded because every reader who writes about the mechanism meets it once.
  • The read-back earned its keep on the first real post. That same comment reports sent 10337 byte(s), stored 10337 — the platform MUTATED the body: GitHub inserted one blank line before the attribution rule and dropped the trailing newline, net zero bytes. A length comparison would have called that identical. The stamp leg read drift 0 — one clock, one act.
  • Changeset: none owed. The diff is .claude/**, .github/workflows/**, the private root package.json ("private": true, @objectstack/spec-monorepo) and scripts/pm/** — the skip-changeset fast track by shape; nothing any package's files[] ships moves. skip-changeset applied by the author.
  • Terminal: GOVERNED. node scripts/pm/check-governed-merges.mjs --test … exits 3: 「1 of 5 path(s) hit the register … ⛔ GOVERNED — a human merge is the review record for this PR」, on .claude/** ×1. Quoted because the register is what decides it: the four scripts/pm/**, workflow and package.json paths are explicitly NOT on it — 「paths not on the register: .github/workflows/lint.yml, package.json, scripts/pm/check-half-states.mjs, scripts/pm/post-stamped.mjs」. So this stays a DRAFT: no seat flips it ready, enqueues it or arms auto-merge.
  • Self-test wiring. check:pm-post-stamped is in package.json and run by a lint.yml step, so check-self-test-wired sees it as named AND self-tested rather than as a script whose only instrument nothing runs. Its live path writes to a card, so — like every check:pm-* step around it — CI runs the offline half only.
  • Out of scope, noted, not filed: sweep-closed-cards.mjs composes the comments it posts with its own timestamps and does not route through this tool. Folding it in is a behaviour change to a scheduled writer and belongs on its own card, not on this one. 承接者:the skills seat, when the token route stops being new.
  • Out of scope, noted, not filed: the {{NOW}} token is not named in any skill file — only the tool is (byte budget, above). If the seat wants the token spelled in the protocol, that is a line the skills seat lands with its own ratchet accounting.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MoTv7pn338AZ71owsp19gQ


Generated by Claude Code


Generated by Claude Code

…{NOW}}` on the write side, H56 on the read side (#17314)

An estimated stamp had a spelling: a seat typed the digits. The triage seat
post records the same failure twice — R+164 (轮次计时全靠感觉,错了约 4 倍)
and R+165, ~70 minutes off with roughly fifteen audit comments carrying the
estimate — with a "measure it next time" note written between them that did
not hold, which is why this lands as mechanism rather than as a discipline
line.

WRITE SIDE — `scripts/pm/post-stamped.mjs`. Posts a comment or rewrites a
card body over REST with `GH_TOKEN`, reading the body from a file or stdin
and substituting `{{NOW}}` with a clock read in the same invocation, so two
tokens in one body get one stamp. `{{WAS:YYYY-MM-DDThh:mmZ}}` is the only
other spelling: a stamp that is a reading of something else, declared and
rendered verbatim. Three refusals, none of which a flag turns off —
POSITIONAL (a bare stamp on the opening line or a subscript reading-time
line, the two positions that belong to the writing act), MIXED (the token
plus a bare stamp elsewhere, the case the card names), and UNKNOWN (a
`{{…}}` token that survived substitution, so a mistyped `{{now}}` cannot
post literally and leave the artefact unstamped). A bare stamp in ordinary
prose is passed through: quoting a ruling's date is a reading of something
else, and refusing it would push seats back onto the channel with no
contract at all. The artefact is then read back — id, url, the platform's
own write time, the drift between it and the substituted stamp, and a byte
comparison of the stored body against the bytes sent — so the transcript
carries proof rather than an assumption.

READ SIDE — H56 in `check-half-states.mjs`, report-only, band `state`
(H44's band, because it is H44's class one field over: a reading that
cannot be dated, and a reading whose date is wrong). It reads exactly the
corpus H44 already fetched and buys nothing, placed after H44's loop and
before H46 so the two censuses describe one population. Two false-positive
sources are closed rather than tolerated: a minute-grained stamp is widened
to its own minute before any comparison, and an edited comment is judged
against the whole `created_at`..`updated_at` window. A line carrying more
than one stamp is held out and counted, never guessed at, so the rows are a
lower bound and the summary clause says by how much.

The two sides import ONE definition of a stamp, one position reader and one
tolerance from `check-half-states.mjs`, so the tool that writes a stamp and
the patrol that audits it cannot come to disagree.

The seat-post protocol names the mechanism in one line, folded net-zero into
the write-side-refresh line at 91/91 (112 bytes, under the 120-byte budget);
the three refresh moments are unchanged.

Self-tests: `check-half-states` 3103 → 3171 cases; `post-stamped` 68 cases
across 6 batteries, offline, no network and no token.

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

Copy link
Copy Markdown
Collaborator Author

维护者速读

改了什么 —— 让「席位凭感觉写时间戳」在机械上无法拼写。写侧:新工具 scripts/pm/post-stamped.mjs,席位发评论或改卡片正文时只写令牌,工具在同一次调用里读时钟替换;开头行或 <sub> 读数行上出现手打的裸时间戳一律拒发,令牌和裸戳混用拒发,拼错的令牌拒发(否则会原样贴上去且没戳),引用别处读数用声明式的 {{WAS:…}} 原样保留;发完读回,打印平台自己的写入时刻、与戳的漂移、以及存储字节是否与发送一致。读侧:巡查锚上新增报告行 H56,拿评论里的戳和平台 created_at 比,超过 15 分钟就点名那条评论;只读开头行和 <sub> 行两个位置,一行里有两个戳的不猜、只计数,编辑过的评论按整个 created..updated 窗口判。两边共用同一份戳定义、同一位置读取器、同一容差常数,不会各说各话。座位贴协议一行净零改成「时间戳恒取自该工具」。

为什么改 —— 分诊席座位贴两班连续记录了同一失败(一次约 4 倍,一次约 70 分钟、约 15 条审计评论写了估计的时间),中间那句「下次要量」没起作用。协议早就要求带 UTC 取数时刻,缺的是把「估计出来的戳」这个拼写取消掉。

风险与代价(含回滚) —— 不进任何发布包,不改门禁强度:H56 是只报告的巡查行,不挡任何 PR。工具默认就写(不像 sweep 默认 dry-run),因为它只写调用者点名的一张卡、一份调用者写的正文,多要一个 --write 只会让人绕回没有约束的通道;--dry-run 可先看替换结果。代价是 CI 多一步离线自测(不联网、不写卡)。回滚 = revert 一个提交。

席位意见 —— 本席按契约复审档自审通过:读完工具全文与 H56 全部 diff(+420 行,含 68 条自测);三种拒发与令牌路径本席在复核 worktree 离线实跑;工具自测 68 例、check-half-states 自测 3171 例、棘轮、id-lint 全绿;受管判定 exit 3(只因 seat-post-protocol.md 那一行,scripts/pm/** 本身不在受管清单)。dev 报告三条派发前提偏差都合理,其中「120 字节/行预算」使协议行只点工具不点令牌,可接受。

你要做的 —— 人工合并本 draft PR(受管面 .claude/** 一行)。合入后本席开始用该工具发评论。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation needs-user-decision Blocked on a maintainer decision — do not dispatch; decision cards only, never on a PR size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants