Repository navigation
feat(pm): a seat timestamp comes from the clock that writes it — {{NOW}} on the write side, H56 on the read side - #17326
Conversation
…{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>
维护者速读改了什么 —— 让「席位凭感觉写时间戳」在机械上无法拼写。写侧:新工具 为什么改 —— 分诊席座位贴两班连续记录了同一失败(一次约 4 倍,一次约 70 分钟、约 15 条审计评论写了估计的时间),中间那句「下次要量」没起作用。协议早就要求带 UTC 取数时刻,缺的是把「估计出来的戳」这个拼写取消掉。 风险与代价(含回滚) —— 不进任何发布包,不改门禁强度:H56 是只报告的巡查行,不挡任何 PR。工具默认就写(不像 sweep 默认 dry-run),因为它只写调用者点名的一张卡、一份调用者写的正文,多要一个 席位意见 —— 本席按契约复审档自审通过:读完工具全文与 H56 全部 diff(+420 行,含 68 条自测);三种拒发与令牌路径本席在复核 worktree 离线实跑;工具自测 68 例、 你要做的 —— 人工合并本 draft PR(受管面 Generated by Claude Code |
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:
Write side —
scripts/pm/post-stamped.mjsPosts a comment (
POST /repos/{o}/{r}/issues/{n}/comments) or rewrites a card body (PATCH …/issues/{n}) over REST withGH_TOKEN, body from--file=PATHor 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_atfor a comment,updated_atfor 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-runprints the substituted body and writes nothing. It ACTS by default where its siblingsweep-closed-cards.mjsdry-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.mjsReport-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:
2026-09-10T06:37Zwith acreated_atof06:37:48is zero drift, not one rounded minute. Without this every correctly stamped comment in the corpus would file a row.created_atwhile its text may legitimately have been rewritten, so the comparison is against the wholecreated_at..updated_atwindow. 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
⛔ 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.mdis at 91/91 with zero headroom, so the pointer was folded in place rather than added, at L28:91 lines before and after (
check:pm-skill-ratchetgreen), 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--helpand refuses every other spelling, so naming the tool names the whole mechanism.references/platform-readings.mdwas 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
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 arecheck:dual-build-cjs-loads,check:lean-entry-closureandcheck: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 frompackage.jsonbeing in the diff, not from any package changing — no package source is touched, so no build closure is owed.check:doc-formula-expressionsalso refused its prerequisite first; it was cleared by building@objectstack/formulaand@objectstack/lintunderscripts/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 commit0e9f0fd93. Run whole rather than narrowed, so no narrowing claim is being made.os-dev-reportcomment 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 backexit 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.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 readdrift 0 — one clock, one act..claude/**,.github/workflows/**, the private rootpackage.json("private": true,@objectstack/spec-monorepo) andscripts/pm/**— the skip-changeset fast track by shape; nothing any package'sfiles[]ships moves.skip-changesetapplied by the author.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 fourscripts/pm/**, workflow andpackage.jsonpaths 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.check:pm-post-stampedis inpackage.jsonand run by alint.ymlstep, socheck-self-test-wiredsees 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 everycheck:pm-*step around it — CI runs the offline half only.sweep-closed-cards.mjscomposes 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.{{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