Skip to content

docs(agents): the PR body is the dev's one body write — a later correction is the seat's PATCH, and the Clause-② line rides the create - #18659

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-18443-os-dev-budget-body-patch
Sep 17, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-18443-os-dev-budget-body-patch

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #18443

Clause-②: no

Governed surface (.claude/**): draft, skip-changeset, the maintainer's approval lands it. File surface: .claude/agents/os-dev.md only. Head 1344eb515, base d6137fd6c. Line-neutral: 403 / 403.

The gap, re-measured on origin/main d6137fd6c

  • L52 and L55 read exactly as the card quotes them (117 B and 118 B): the four-write budget, and 预算外零写,⛔ 不 PATCH 正文.
  • How the body line is demanded today: Check Changeset (pr-automation.yml changeset-check) runs scripts/check-changeset-no-major.mjs --base MERGE_BASE, which reads the declaration from github.event.pull_request.body through readClause2Line (imported from scripts/pm/check-clause2-carriers.mjs; line-initial key, yes / no only). The red is CONDITIONAL, not unconditional as the card states: a missing line fails only as not-measured-material (exit 1) — a patch changeset on a package the diff grew, none of them raised to minor+; with no candidate offender it is not-measured-moot (exit 0), and the whole job is exempt under skip-changeset. The remedy when it does fail is still exactly one act, a body write, and the job is edited-subscribed so that write re-fires it without a push.
  • Whether the seat's committed orders now supply the line: no. On d6137fd6c the pm-dispatch template names the claim comment as the only carrier (SKILL.md :473 Branch:、Clause-②: 各占一行且行首, :476, :809 template), no committed text tells anyone to put it in the PR body, and os-dev.md did not contain the token Clause at all (grep: 0). The seat's own closure lives in its dispatch prompts (this card's order says put Clause-②: no in the PR body AT CREATION), not in the tree. So on origin/main the gap was still the whole class, not only the residual.
  • The residual class is measurable: three PR-body-scoped gates, all edited-subscribed, each cleared only by a body write — Check Changeset (the Clause-②: line), Duplicate Fix Guard (a Fixes line naming a card another open PR claims), Part-of Closing-Keyword Guard (a closing keyword beside Part of). Plus the two non-gate cases the card names: a seat's mid-task correction and a sanitizer-truncated body (platform-readings: the PR-body write side eats a trailing rule line plus footer while the call reports success; bare REST PATCH /pulls appends a bare footer beside an existing session-URL one, +58 B).
  • Provenance holds: lint: translation-target-unknown reads apps[].navigation only, so every locale key for a CONTRIBUTED navigation item (navigationContributions) is a false positive whose advice deletes a translation the runtime honours #18203's report comment 5696114759 lists write (4) PATCH /repos/.../pulls/18433 with the words the card quotes.

Shape chosen: B, with the line carried at creation

The card offers (A) an allowance for a declaration-repair PATCH (budget five, or the repair write uncounted) and (B) the remedy moved off the dev onto the seat. This PR proposes B plus one prevention line: the dev's PR body is written once, in the POST /pulls already in budget, and that one write carries the Clause-②: line copied from the claim comment; any correction the body needs afterwards is named in the report and written by the seat. The budget stays four; the dev never PATCHes a body. It is a variant of B, not a third shape — the rules decision (who writes the correction) is B's.

Four-axis reasons (the framework as the dispatch pasted it):

  • 实际业务需求 — measured, not read-as-useful: the only body author at creation is the dev (POST /pulls), and the gate reads the body, so carrying the line at creation closes the case lint: translation-target-unknown reads apps[].navigation only, so every locale key for a CONTRIBUTED navigation item (navigationContributions) is a false positive whose advice deletes a translation the runtime honours #18203 actually hit at zero extra writes. The residual (three gates + two non-gate cases) is a post-creation PR-state correction, and post-creation PR-state writes are already the seat's: the contract-review carrier moved to the seat on 2026-09-15 (41ff021ca), the ready flip and auto-merge were never the dev's. Under A the dev would pay a write per correction; under B the seat pays only in the residual class, in the review it already performs on the PR before hanging the four-piece.
  • 项目长远合理性 — A makes the budget a judgement (repair is a word every out-of-budget write will be argued under, round after round); B keeps it mechanical: four named endpoints, and a PATCH in api_writes is a refusal the seat can read without interpreting. B also keeps the write-identity design of docs(pm,agents,settings): write-identity locks 1–4 — deny MCP content writes, REST-only dev writes with api_writes, batch default 2, user-account roles #18072 intact (REST-only dev writes, a ledger the seat verifies) instead of carving an exception into it.
  • 防 AI 写错 — B gives the dev one branch fewer, not one more: write the body right once (a fixed line copied verbatim from the claim, so the two carriers agree by construction), never rewrite it. A would hand the dev the PR-body PATCH traps the platform-readings measure (the appended footer, the eaten rule line, the ⛔ never re-send a body carrying an appended footer rule) on the rare path — precisely where an AI is least rehearsed. Contract-first: fix at the producer (the body as created), not by tolerating a wrong body downstream.
  • 创业阶段不扩散需求 — B adds no capability, script or gate and widens nothing: it reallocates one existing seat write (PATCH .../issues/{n} body, ✓ in rest-channel.md) and deletes two lines of duplicated rule text. A widens the dev's write surface for a case prevention removes. No staged transition: the old sentence is gone, the new one is in force on landing.

Cost stated plainly: under B a correction in the residual class still costs a seat round (dev reports, seat writes) — the same cost as today, minus the conflict the dev had to flag. 「flag it rather than bury it」 is kept as an explicit rule for every other out-of-budget write (the rewritten L57).

The diff — before / after with byte counts

Rule 3 block (indented list under 六条基本规则 3):

  • L55 before (118 B): - 三类发现附查重词进报告交席位代立,dev 不 \POST /issues`;预算外零写,⛔ 不 `PATCH` 正文。`
  • L55 after (117 B): - 三类发现附查重词进报告交席位代立,dev 不 \POST /issues`、⛔ 不静默弃报;预算外零写。`
  • L56 new (117 B): - PR 正文 dev 只写一次,在开 PR 那一笔,⛔ 不 \PATCH`;事后要改的报告点名改法,席位代写。`
  • L58 (was L57) before (118 B): - 报告记 \api_writes`(次数 + 端点清单)与 `mcp_calls`(MCP GitHub 调用计数),席位对照预算核验。`
  • L58 after (119 B): - 报告记 \api_writes`(次数 + 端点清单)与 `mcp_calls`;越界真写了的照列注明缘由,⛔ 不漏记。`
  • old L58 deleted (112 B): - 立不成 ⇒ 发现连同缘由写进报告交 PM 代立;⛔ 不查重硬立与静默弃报同为禁形。
  • L52 unchanged (117 B): the four-write budget stands as written.

Definition of done block:

  • L279 new (113 B), directly under the Draft PR 指向 main line: - 正文行首照抄认领的 \Clause-②:` 行:`Check Changeset` 读正文不读卡,开 PR 那一笔就带上。`

字节与 sanitizer 纪律 block:

  • old L402 deleted (102 B): - 评论通路 MCP 与 REST 同判:整块原样存活,缺规则线则不认、再落整块留两个。

Whole file: 36842 B → 36858 B (+16 B), 403 → 403 lines, widest line 120 B before and after, zero lines over the ratchet's 120-byte cap.

Why the two deletions are content deletions, not re-wrap: old L58's conditional (立不成 ⇒ …交 PM 代立) has been dead since the dev stopped filing (L55: dev 不 POST /issues, findings go to the seat unconditionally), and 不查重硬立 guards an act the dev no longer performs; its one live clause — never drop a finding silently — is folded into L55 as ⛔ 不静默弃报. Old L402 restated AGENTS.md's issue-comment footer paragraph (「a bare footer UNDER the rule line is stored byte-identical, one footer; … NO rule line above it … leaving two」) which this file already routes the reader to twice (the 归属条款是 AGENTS.md 的 GitHub mutates body BYTES 条 line and 完整读数住 AGENTS.md 同条), and AGENTS.md binds the dev by the file's own opening. The MCP GitHub 调用计数 parenthetical dropped from L58 is restated by the report template's own mcp_calls field description eight lines below the JSON fence.

Rider not taken: the pending tail --pid rider (the line names no pid source) sits in the 干净收尾 block, not in either block this PR touches, so it does not fit line-neutrally here and is left for the next os-dev.md PR.

Gates

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the worktree at 1344eb515 (change set: 1 path vs merge base d6137fd6c; the tool flagged STALE TREE — origin/main moved one commit to 62d830e54, scripts/check-plugin-teardown-shape.mjs, which is not among the 18 families and does not touch this file). Every exit code captured redirect-then-$?:

  • node scripts/check-closing-keyword-parity.mjs :: exit 0
  • node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
  • node scripts/check-comment-mask-corpus.mjs :: exit 0
  • node scripts/pm/check-governed-queue-guard.mjs --self-test :: exit 0
  • node scripts/pm/check-harness-current.mjs --self-test :: exit 0
  • pnpm --filter @objectstack/lint run check:doc-formula-expressions :: first run exit 3 PREREQUISITE NOT MET (no dist/ for formula + lint), prerequisite built under os-verify-lock.sh (VERDICT command-exit 0, held 1s, waited 0s, turbo cache hit), re-run :: exit 0 (22 record-scoped formula example(s) across 438 files / 1378 TS blocks judged clean)
  • pnpm check:agent-model-declared :: exit 0
  • pnpm check:agent-test-spelling :: exit 0
  • pnpm check:commit-card-trailers :: exit 0
  • pnpm check:doc-authoring :: exit 0
  • pnpm check:driver-memory-census :: exit 0
  • pnpm check:nul-bytes :: exit 0
  • pnpm check:pm-governed-merges :: exit 0
  • pnpm check:pm-skill-id-lint :: exit 0
  • pnpm check:pm-skill-ratchet :: exit 0 — .claude/agents/os-dev.md is 403 lines (ceiling 403; headroom 0), same reading on the unedited tree
  • pnpm check:refd-timer-probe :: exit 0
  • pnpm check:skill-frame-sync :: exit 0
  • pnpm check:watch-hint-literal :: exit 0
  • extra, the roster the derivation marks ⛔ under .claude: pnpm check:pm-settings-deny-roster :: exit 0
  • node scripts/pm/check-governed-merges.mjs --test .claude/agents/os-dev.md :: exit 3 GOVERNED (.claude/** ×1 — the agent instruction tree), recorded as the expected answer, not a failure

--ran reconciliation: 18 derived famil(ies) accounted for — 18 run, 0 NOT-MEASURED (a DERIVED zero — all 18 recorded an exit code and none of them is 3). Pin checks: no script or doc outside this file pins any string rewritten or deleted here (grep over scripts/, .claude/, AGENTS.md, docs/: zero hits for each).

skip-changeset: .claude/** is on the fast lane (nothing published moves); check-governed-merges --test answered GOVERNED as above.

维护者速读(草稿)

改了什么 — 只动 .claude/agents/os-dev.md,行数不变(403 / 403)。dev 的 GitHub 写预算仍是四笔;新增两条规则:① PR 正文 dev 只在开 PR 那一笔写,⛔ 不 PATCH,事后要改的写进报告、由席位代写;② 开 PR 时把认领评论里的 Clause-②: 行照抄进正文行首,让 Check Changeset 一开就读得到。另把「越界真写了的照列注明缘由,⛔ 不漏记」写成明文。两行新增由两行删除付账:一条自 dev 不再立卡起就死掉的「立不成 ⇒ …」句(仅存的一句「不静默弃报」并入原句),一条与 AGENTS.md 逐字重复、本文件已两次指回 AGENTS.md 的评论页脚读数。

为什么改 — 卡面测到的矛盾:Check Changeset 判红时唯一的补救是改 PR 正文,而 L55 明禁 dev PATCH 正文;dev 只能报冲突、烧一轮往返。复测发现根因更靠前:树里没有任何一处告诉 dev 把 Clause-②: 行放进 PR 正文(派发模板只写认领评论),所以线是在建 PR 时就漏掉的。把它放回建 PR 那一笔,常规情形零额外写;剩下的残余类(三个读正文的门禁、席位中途改令、被截正文)本来就是建 PR 之后的 PR 状态修正,归已经持有这类写(contract-review 标签、ready 翻转、auto-merge)的席位。没选「预算抬到五」:那会把预算从机械清单变成每轮都要争辩的「算不算修复」。

风险与代价(含回滚) — 代价:残余类里的一次正文修正仍要一轮席位动作(dev 报、席位写),与今天相同,只是少了 dev 报冲突那一步。风险:席位写正文要走 REST PATCH .../issues/{n},平台对 PR 正文的页脚/横线变异读数已在 platform-readings,席位照读回即可;dev 侧少一个分支,不多。两处删除若被判为内容损失,可单独恢复(各一行、字节已列)。回滚:revert 本 PR 即回到 L52/L55 原文,无其它文件牵连、无脚本、无 changeset。

席位意见 — (留席位定稿)

你要做的 — 批准或退回本 PR:批准即接受「正文修正归席位、dev 零 PATCH」这一取舍;退回并注明「抬预算」即改走卡面的 A 形。

Acceptance notes

  • noted, not filed: the committed pm-dispatch template still names the claim comment as the only Clause-②: carrier; after this PR the dev copies that line into the body, so the template needs no change to close the case. 承接者:the domain:skills seat, on the next pm-dispatch SKILL.md PR, if it wants the body carriage stated on both sides.
  • noted, not filed: the card's sentence 「Check Changeset reds a PR whose body carries no line-start Clause-②: declaration」 is conditional in the gate (not-measured-material only); the remedy statement stands regardless. 承接者:无 — recorded here and in the report.
  • noted, not filed: the tail --pid rider is left in place (different block, see above). 承接者:the next os-dev.md PR.
  • The dispatch-gates STALE TREE flag: origin/main advanced by 62d830e54 during this round; re-derivation after a fetch names the same 18 families (the changed file is not one of them). 承接者:无.

Generated by Claude Code

…ction is the seat's PATCH, and the Clause-② line rides the create

The four-write budget stays four. What changes is where the two body-scoped
demands are met: the `Clause-②:` line the `Check Changeset` gate reads out of
the PR body is copied from the claim comment into the body at creation, so the
one `POST /pulls` already in budget carries it; and any body correction needed
afterwards — a gate remedy, a mid-task correction, a truncated body — is named
in the report and written by the seat, never `PATCH`ed by the dev. A write that
did cross the budget is listed in `api_writes` with its reason, never omitted.

Line-neutral (403 / 403): the two added rules are paid by two deletions —
the `立不成 ⇒ …` line (its only live clause, never drop a finding silently, is
folded into the finding rule; the rest was dead once the dev stopped filing)
and the comment-footer reading that duplicated AGENTS.md's issue-comment
paragraph, which this file already routes the reader to.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
@os-justin os-justin added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude
@github-actions github-actions Bot added size/xs documentation Improvements or additions to documentation labels Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

维护者速读(终稿)

skills 席 session_01Gqi43smmqjJ5sUrhfoPeKu,2026-09-17T13:19Z。对照本席自己读的 diff 校正 dev 草稿;席位意见已填。

改了什么 — 只改 .claude/agents/os-dev.md(+4 / −4,403 → 403 行,预算四笔不变)。写预算那段(L55–L58)由「预算外零写,⛔ 不 PATCH 正文」改成三条明确规则:PR 正文 dev 只写一次,就是开 PR 那一笔,⛔ 不 PATCH;事后要改的,报告里点名改法,席位代写;越界真写了的照列注明缘由,⛔ 不漏记(把「先报告不埋掉」写成明文)。PR 段新增一行:正文行首照抄认领评论的 Clause-②: 行,因为 Check Changeset 读的是正文不是卡。为保持行数,删掉两行重复文本(「立不成 ⇒ …」的活句并入 L55;评论页脚那行与 AGENTS.md 同条重复)。

为什么改 — 预算禁止 dev PATCH 正文,而 Check Changeset 判红时唯一的补救恰是 PATCH 正文:dev 按字面必须拒绝,照做就越界。#18203 那轮真的撞上了(dev 写了「WRITE (4) IS OUT OF BUDGET AND I AM FLAGGING IT」),白跑一轮。卡上给了两个方向:A 抬预算 / 列豁免;B 把修正挪给席位,并让 Clause-②: 行在开 PR 时就带上,常见情形根本不需要修正。本 PR 做的是 B。

风险与代价(含回滚) — 受管 agent 定义,只改规则文本;不加脚本、不加门禁、不扩 dev 写面(席位本来就持有 PATCH /issues/{n})。剩余类(中途修正、被 sanitizer 截断的正文)仍要一个席位回合,与今天一样,少的是 dev 的冲突报告那一步。本 PR 自己就是证据:正文开 PR 时带了 Clause-②: no,全程零 PATCH。回滚 = revert 一个提交。

席位意见 — 荐合 B。四轴一致:零额外写就关掉了实测的案例;预算保持机械的四端点清单,而不是每轮争论「什么算修补」;dev 少一个分支而不是多一个(正文 PATCH 正是平台读数里页脚会被吃/追加的那条路);不扩能力面。

你要做的(一个动作) — 认可 B 即 Approve 本 PR(或直接人工合并);席位随后按裁定 C 落地。要 A(抬预算到五笔或列豁免)则 Request changes,本席另起。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 17, 2026 14:38
@os-zhuang
os-zhuang enabled auto-merge September 17, 2026 14:38
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 0111212 Sep 17, 2026
39 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-18443-os-dev-budget-body-patch branch September 17, 2026 15:01
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…tack-ai#18681)

Fixes objectstack-ai#18456

Clause-②: no

`scripts/pm/` sits outside every workspace package, and the root package
is private, so no
`files[]` can ship this diff — `skip-changeset`.

## The defect

`check-clause2-carriers --pair` is the landing pre-check every seat
runs, and on one pair
(PR objectstack-ai#17917 / card objectstack-ai#17425) it answered **0 at 02:57Z, 4 at 03:04:09Z and
0 at 03:58:33Z on
2026-09-13 with an identical script blob**. Two explanations were ruled
out with controls
(no comment on that thread was ever edited; the board is resolved from
the environment, never
from the working directory), so the cause is still UNKNOWN — and the
three runs could not be
compared, because not one of them had SAID what it read. The judging
half is already
deterministic given a fixed document (`--pair-json` proves that); what
was unpinned is **what
document the live path builds**. This states it: every `--pair` run now
closes with a fenced
`clause2 input record` block on stderr, with the same field roster on
every exit, so two runs
that disagree are settled by **diffing their two blocks** — never by
re-running until one side
wins. ⛔ No guess at the cause is dressed as a fix here: no predicate, no
state, no row, no
count and no exit code reads one character of the record, and the
judging half is untouched.

## The record's field roster

Rendered from `INPUT_RECORD_RUN_FIELDS` and `INPUT_RECORD_PAIR_FIELDS`
and from nowhere else,
so a field cannot silently disappear: a declared field this run could
not fill renders with an
explicit token rather than vanishing, and a field the builder fills that
the roster does not
declare is NAMED in the block (`record.undeclared`). Values too long for
one line continue on
indented lines under their key.

| half | fields |
|:--|:--|
| run | `record.version` · `run.utc` · `run.mode` · `run.script.path` ·
`run.script.blob` · `run.script.bytes` · `run.node` · `board.repo` ·
`board.source` · `read.plan` · `read.api` · `read.token` · `read.served`
· `read.pair-json` · `run.requests` · `pairs.derived` |
| per pair (`pair.N.`) | `pr` · `card` · `derivation` · `head-sha` ·
`card-comments` · `card-comment-ids` · `card-comment-newest` ·
`pr-comments` · `pr-comment-ids` · `pr-comment-newest` · `claim.rule` ·
`claim.selected` · `claim.rejected` · `claim.clause2-line` ·
`pr-body.clause2-line` |

Four of them are worth naming for WHY they are there:

- **`run.requests`** — every read the run issued, in order, with its
channel, its exact path
and its **row count**. A page asked for with `per_page=100` that answers
with exactly 100
rows is the one shape a truncated read and a complete one share, and
nothing printed it.
- **`claim.rule` + `claim.selected` + `claim.rejected`** — the carrier,
the rule that picked it
and every candidate it did not pick, each with its reason. That
separates "the two runs
selected different comments" from "the two runs applied different
rules".
- **`claim.selected`'s body fingerprint** (bytes + `sha256:`) — the
field the measured 0/4/0
actually needs. A `misplaced` verdict on that thread requires the
governing claim to have
carried no readable declaration while a superseded one did; same ids
with a different verdict
is only possible if the BYTES differed, and the ids were all anybody
could see.
- **`run.script.blob`** — git's blob hash of this file, beside the path
it ran from. "The blob
was identical on both sides" was a claim in the incident; it is now a
printed fact any seat
checks with `git hash-object`. On this PR's head it reads
`25d204236aa8296644813109fa77541d6efe1644`,
which is exactly `git rev-parse
HEAD:scripts/pm/check-clause2-carriers.mjs`.

The `--json` sweep carries the same record under `inputs` — the same
record, ⛔ never a second
format.

## The two live blocks the card names

`--pair 17917` — the pair from the card. Both it and objectstack-ai#18654 have since
merged, so `--pair`
answers **exit 2** on each today (the pair cannot be formed from a
closed PR). ⭐ That is
precisely the class of exit the old code said the least about, and the
block is now complete
on it:

```text
----- clause2 input record v1 -----
record.version: 1
run.utc: 2026-09-17T14:10:26.210Z
run.mode: --pair 17917
run.script.path: /home/user/objectstack-issue-18456/scripts/pm/check-clause2-carriers.mjs
run.script.blob: 25d2042 (git blob sha1 — check it with `git hash-object` on the path above)
run.script.bytes: 513169
run.node: v22.22.2
board.repo: objectstack-ai/objectstack
board.source: default — NEITHER PM_SWEEP_REPO NOR GITHUB_REPOSITORY answered
read.plan: (i) token then (ii) token-less public read
read.api: https://api.github.com (REST, accept application/vnd.github+json)
read.token: present
read.served: token=1, public=0, pair-json=0
read.pair-json: (not named — this run read the network)
run.requests: 1 read(s), in the order they were issued
  objectstack-ai#1 (i) token /repos/objectstack-ai/objectstack/pulls?state=open&per_page=100&page=1 -> HTTP 200 (21 row(s))
pairs.derived: 0 pair(s)
record.how-to-read: two runs that DISAGREE about one pair are settled by diffing their two blocks — ⛔ never by re-running until one side wins. The blob line says whether the two runs were even the same instrument.
----- end clause2 input record -----
```

`--pair 18654` — the pair this seat landed today, which answered 0 at
12:32Z and is likewise
merged now (**exit 2**):

```text
----- clause2 input record v1 -----
record.version: 1
run.utc: 2026-09-17T14:10:27.163Z
run.mode: --pair 18654
run.script.path: /home/user/objectstack-issue-18456/scripts/pm/check-clause2-carriers.mjs
run.script.blob: 25d2042 (git blob sha1 — check it with `git hash-object` on the path above)
run.script.bytes: 513169
run.node: v22.22.2
board.repo: objectstack-ai/objectstack
board.source: default — NEITHER PM_SWEEP_REPO NOR GITHUB_REPOSITORY answered
read.plan: (i) token then (ii) token-less public read
read.api: https://api.github.com (REST, accept application/vnd.github+json)
read.token: present
read.served: token=1, public=0, pair-json=0
read.pair-json: (not named — this run read the network)
run.requests: 1 read(s), in the order they were issued
  objectstack-ai#1 (i) token /repos/objectstack-ai/objectstack/pulls?state=open&per_page=100&page=1 -> HTTP 200 (21 row(s))
pairs.derived: 0 pair(s)
record.how-to-read: two runs that DISAGREE about one pair are settled by diffing their two blocks — ⛔ never by re-running until one side wins. The blob line says whether the two runs were even the same instrument.
----- end clause2 input record -----
```

⭐ `diff` of those two blocks is **four lines**: `run.utc` and
`run.mode`, twice. Same roster,
same order, same shape — which is the property the card asked for.

## A live block on exit 0

`--pair 18659` (open at the time of writing) — **exit 0**, the full pair
half:

```text
----- clause2 input record v1 -----
record.version: 1
run.utc: 2026-09-17T14:10:36.744Z
run.mode: --pair 18659
run.script.path: /home/user/objectstack-issue-18456/scripts/pm/check-clause2-carriers.mjs
run.script.blob: 25d2042 (git blob sha1 — check it with `git hash-object` on the path above)
run.script.bytes: 513169
run.node: v22.22.2
board.repo: objectstack-ai/objectstack
board.source: default — NEITHER PM_SWEEP_REPO NOR GITHUB_REPOSITORY answered
read.plan: (i) token then (ii) token-less public read
read.api: https://api.github.com (REST, accept application/vnd.github+json)
read.token: present
read.served: token=5, public=0, pair-json=0
read.pair-json: (not named — this run read the network)
run.requests: 5 read(s), in the order they were issued
  objectstack-ai#1 (i) token /repos/objectstack-ai/objectstack/pulls?state=open&per_page=100&page=1 -> HTTP 200 (21 row(s))
  objectstack-ai#2 (i) token /repos/objectstack-ai/issues/18443 -> HTTP 200
  objectstack-ai#3 (i) token /repos/objectstack-ai/issues/18443/comments?per_page=100 -> HTTP 200 (4 row(s))
  objectstack-ai#4 (i) token /repos/objectstack-ai/objectstack/pulls/18659/files?per_page=100&page=1 -> HTTP 200 (1 row(s))
  objectstack-ai#5 (i) token /repos/objectstack-ai/issues/18659/comments?per_page=100 -> HTTP 200 (1 row(s))
pairs.derived: 1 pair(s)
pair.1.pr: 18659
pair.1.card: 18443
pair.1.derivation: `closing-keyword` (via a closing keyword) — body line: Fixes objectstack-ai#18443
pair.1.head-sha: 1344eb5
pair.1.card-comments: 4 row(s)
pair.1.card-comment-ids: 5713976124,5714587497,5714873191,5715029659
pair.1.card-comment-newest: 5715029659 at 2026-09-17T13:19:56Z
pair.1.pr-comments: 1 row(s)
pair.1.pr-comment-ids: 5715030051
pair.1.pr-comment-newest: 5715030051 at 2026-09-17T13:19:57Z
pair.1.claim.rule: the GOVERNING claim — the NEWEST comment whose body carries a line beginning `Claim:`/`Claimed:` AND whose `Branch:` line parses at least one protocol-shaped branch (newest by `created_at`; an unreadable stamp or a tie falls back to thread order, later row wins). The pool is every claim comment sharing that `created_at`; when NO claim names a branch at all, every claim comment is the pool. ⛔ Not earliest, ⛔ not a session match, ⛔ not the one whose body mentions the key.
pair.1.claim.selected: 1 comment(s) in the pool
  5714587497 at 2026-09-17T12:46:45Z — 2159 bytes, sha256:795e1df6c9fd
pair.1.claim.rejected: none — every claim comment on this thread is in the pool
pair.1.claim.clause2-line: DECLARED `no` — Clause-②: no
pair.1.pr-body.clause2-line: DECLARED `no` — Clause-②: no ⚠️ stated as an INPUT only — ⛔ no row here judges the PR body; the declaration limb is judged from the card, and `check-changeset-no-major.mjs` is what reads this line.
record.how-to-read: two runs that DISAGREE about one pair are settled by diffing their two blocks — ⛔ never by re-running until one side wins. The blob line says whether the two runs were even the same instrument.
----- end clause2 input record -----
```

## Pins

Battery **objectstack-ai#18456: the `--pair` input record — the same block on every
exit, so two runs that
disagree can be diffed**, registered in `SELF_TEST_BATTERIES` with a
floor of **38**; **41**
cases register. `SELF_TEST_BATTERY_FLOOR` raised 26 → 27 by exactly the
one battery this adds.

What is pinned, in the card's own terms:

- the record is **present and complete on exit 0**, on the **exit-4
(MISPLACED)** shape and on
a **refusal that formed no pair** — all three key lists asserted equal;
- the **field roster** cannot lose a field: a declared field that was
never filled still renders
(with `INPUT_RECORD_UNSET`), an empty record still carries every
declared key, and a key
  outside the roster is named rather than printed in silence;
- the **selected-claim rule is stated**, and it is the one constant
`claimCarrierSelection`
  applies — so the printed rule cannot drift from the applied one;
- a **rejected candidate is named with its reason**, and a thread with
nothing rejected says so;
- a **`--pair-json` run names that read path as such** and names the
document;
- the body fingerprint **moves when only the bytes move** while every id
field stays identical —
  the measured shape, asserted directly;
- `gitBlobSha1` is pinned against two values `git hash-object` prints.

⛔ CONTROLS in the same battery: the block carries no verdict, no exit
code and no finding row;
building it changes no reading; and the selection the block prints IS
the pool `cardDeclaration`
judged (ONE derivation — `cardDeclaration` now calls
`claimCarrierSelection` instead of deriving
the pool inline, so the record and the verdict cannot describe two
different comments).

`--self-test` on this head: **786 cases pass, exit 0** (745 before;
+41).

## Ablation

From the committed tree, blob `25d204236aa8296644813109fa77541d6efe1644`
(= this PR's head
blob), the pair half of the record removed on disk, mutation proved
before the run, restore by
blob hash under a `trap`:

```text
HEAD blob                25d2042
before: removed-text count=1 (want 1); injected count=0 (want 0)
after : removed-text count=0 (want 0); injected count=1 (want 1)
mutated blob             e88355f70e8648f1e3d30147f0c82b7c3c157609
VERDICT ablation-mutated self-test exit=1       ← 14 cases red
  ✗ every declared PAIR field is present once per derived pair, prefixed by its index
  ✗ the SELECTION RULE is printed, not merely applied — two runs must be comparable on the rule too
  ✗ …and it is the one constant, so the printed rule cannot drift from the applied one
  ✗ the SELECTED carrier is named by id and by date
  ✗ ⭐ …with a BODY FINGERPRINT: the one field that tells "same ids, different bytes" apart
  ✗ ⭐ …and it MOVES when only the bytes move: same ids, same count, same newest, different verdict
  ✗ every REJECTED candidate is named, with the reason it is not the carrier
  ✗ …and a thread whose claims are all in the pool says THAT, rather than going quiet
  ✗ a claim that parses ZERO branches leaves NO carrier, and the block names that claim
  ✗ an UNREAD thread reads UNREAD, ⛔ never 0 rows
  ✗ the line READ from the carrier is stated — declared, near miss or nothing
  ✗ the PAIRING quotes the body line it was derived from
  ✗ …and the branch-name fallback names the head ref instead of quoting a line that does not exist
  ✗ the PR-BODY line is read and stated — ⛔ and stated as an INPUT, never as a limb
restored blob            25d2042   (HEAD 25d2042)
git diff HEAD --name-only: []
VERDICT ablation-restored self-test exit=0
```

Direction predicted before the run and observed: **turns red**. The
module is run directly from
source by `node scripts/pm/…` — no build and no `dist/` between the edit
and the run, so the
on-disk proof is the whole preflight.

⚠️ **A named gap, not a hidden one**: the battery drives the builder and
the renderer, and it
cannot see `main`'s **emission**. An ablation that deleted the two lines
in `main`'s `finally`
would come back green. What covers emission is the three live blocks
quoted above, taken on this
head across three different exits.

## Candidate cause, unproven — ⛔ not fixed here

Two readings taken while wiring the record. Neither is acted on in this
PR.

**1. On the blob all three 2026-09-13 runs ran, exit 4 was the
DETERMINISTIC answer for that
pair — so what is unexplained is the two 0s, not the 4.**

- The file's last change before those runs was `a5ed18ced`
(2026-09-12T06:05:25Z, "a key-INITIAL
clause-② line that QUOTES the spelling is not a declaration"); its next
change was
`4e3a496ba` at 2026-09-13T17:19:18Z, after all three runs. `a5ed18ced`'s
blob is
`aecbb2d86683eb908468fdacaac2ff53753f06ef` — the same blob PR objectstack-ai#18448's
body independently cites
  as "the exact blob the 2026-09-13 readings were taken from".
- The governing claim on card objectstack-ai#17425 at that moment was comment
`5650083758`
(2026-09-13T01:57:23Z). Its line 3 opens `Clause-②: no —` and then
quotes the spelling again
inside the same line. Run first-hand against **that historical blob's
own
`readClause2Line`**: `{"kind":"near-miss","reason":"describing"}`, and
`cardDeclaration` on a
one-claim thread reads `missing` — ⛔ not a declaration. Today's copy
reads it identically.
- A `--pair-json` document assembled from the REAL thread as it stood at
03:04:09Z (its 9
comments, both carriers' real label event streams) answers **exit 4,
MISPLACED** on this PR's
head, quoting the superseded `Clause-②: yes` and naming `5650083758` as
the correction target —
which is what the 03:04Z reviewer and the 02:53Z dev round both
reported.
- ⇒ The 4 is reproducible and mechanically explained. The 0s are not. ⭐
Exactly the difference
the record's `claim.selected` fingerprint and `claim.clause2-line` would
have shown, had the
  0-runs printed one.
- ⚠️ Limits of this reading: the historical module was exercised for
`readClause2Line` (self
contained) and `cardDeclaration` (which imports today's sibling
modules); the commit ordering
is read from a shallow checkout, corroborated by objectstack-ai#18448's independent
citation of the same blob.

**2. The comment read — the one the declaration limb depends on — is the
only read here with no
page discipline.** `readCardComments` issues ONE request,
`/issues/N/comments?per_page=100`, with
no `page=` ladder and no short-read check. `readCarrierEvents` and
`readPullFiles` both page to
exhaustion and answer `null` (UNJUDGED, never clean) when their cap is
hit, for the reason their
own docblocks state. A card thread past 100 comments therefore loses its
tail silently, and the
claim pool is built from whatever came back. Not the cause on objectstack-ai#17425 (7
comments at 02:53Z, 16
today), but it is a live fail-open in this reading. The record makes it
visible for the first
time: request `objectstack-ai#3` prints its row count, so a `(100 row(s))` on a
`per_page=100` request is now
readable. ⛔ Not fixed here; the seat files or re-scopes.

## Gates

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`
from the worktree with no hand-fed path list; re-derived after rebasing
onto current `main`
(the derivation was STALE-TREE by 4 commits) — **identical command
list**. All 34 run at head
`f5773ce08`, exit codes captured redirect-then-`$?`:

```text
0 :: node scripts/check-adr-0087-registration.mjs --base origin/main
0 :: node scripts/check-adr-0087-registration.mjs --self-test
0 :: node scripts/check-changeset-no-major.mjs --base origin/main
0 :: node scripts/check-changeset-no-major.mjs --self-test
0 :: node scripts/check-ci-filter-parity.mjs
0 :: node scripts/check-closing-keyword-parity.mjs
0 :: node scripts/check-closing-keyword-parity.mjs --self-test
0 :: node scripts/check-comment-mask-corpus.mjs
0 :: node scripts/check-declaration-mirrors.mjs
0 :: node scripts/check-declaration-mirrors.mjs --self-test
0 :: node scripts/check-scripts-symbol-anchors.mjs
0 :: node scripts/check-scripts-symbol-anchors.mjs --self-test
0 :: node scripts/check-self-test-wired.mjs
0 :: node scripts/check-self-test-wired.mjs --self-test
0 :: node scripts/check-self-test-workflow-commands.mjs
0 :: node scripts/check-self-test-workflow-commands.mjs --self-test
0 :: node scripts/check-whole-set-label-write.mjs
0 :: node scripts/check-whole-set-label-write.mjs --self-test
0 :: node scripts/pm/bare-root-worklist.mjs --self-test
0 :: pnpm check:agent-test-spelling
0 :: pnpm check:bash32-floor
0 :: pnpm check:changeset-gate-self-tests
0 :: pnpm check:cli-command-ids
0 :: pnpm check:cross-package-test-inputs
0 :: pnpm check:driver-memory-census
0 :: pnpm check:entry-guard
0 :: pnpm check:nul-bytes
0 :: pnpm check:parse-guard
0 :: pnpm check:pm-clause2-carriers
0 :: pnpm check:pnpm-filter-targets
0 :: pnpm check:ratchet-remedy-authority
0 :: pnpm check:refd-timer-probe
0 :: pnpm check:watch-hint-literal
0 :: pnpm check:pm-dispatch-gates
```

Reconciled: `dispatch-gates --ran` ⇒ **34 derived, 34 run, 0
NOT-MEASURED, 0 UNRUN**.

Repo-wide `pnpm lint` (`eslint . --no-inline-config`) at `f5773ce08`:
**exit 0**.
`grep -naP` for control bytes over the changed file: no hits.

⛔ Outside these 34, as the derivation itself prints: 53 artifact-roster
families, 11
wide-population families, 7 pending-changeset families, 1 path-scheduled
CI job and the
always-runs tail. Their absence here is not a clearance.

## Acceptance notes

Out of scope, noted and ⛔ not filed:

- The read-path report and the input record now also print on the
`--pair-json` **usage
refusals** (a missing file, a non-JSON document, a board conflict),
because everything past the
board resolution moved inside one `try`/`finally`. One extra stderr line
on those paths, in the
direction the file's own header argues for. Carrier: whoever next edits
`main`.
- `main`'s `--pair` value is parsed in two places now (once for
`run.mode`, once for the pair
itself). Both read the same argv through `flagIndex`; a reader may
prefer one. Carrier: whoever
  next edits `main`.

---

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ipts/ablation-replace.mjs beside its sibling preflight (objectstack-ai#18725)

Fixes objectstack-ai#18599

Clause-②: no

Governed surface (`.claude/**`): draft, `skip-changeset`, an authorized
approval lands it and the `domain:skills` seat lands it from there. File
surface: `.claude/agents/os-dev.md` + `packages/qa/dogfood/README.md`
only. Head `fa55c9d0ad`, base `f6189a43f9` (`origin/main` at worktree
creation; the card's readings were taken on `ab1d35925` and re-read here
on the tip). Line-neutral on the governed file: **403 / 403**, widest
line 120 B before and after, zero lines over the ratchet's 120-byte cap.

## The defect

PR objectstack-ai#18598 (card objectstack-ai#18226) landed `scripts/ablation-replace.mjs` — the
on-disk mutation step of an ablation: replace through an anchor that
must hit, with the write and the restore verified against the disk
rather than an exit code, in place of the `sed -i` / `perl -i` family
that silently failed in two recorded ablations (2026-08-20, a `-0` run
whose `$/` interpolated into a NUL byte; 2026-09-14, an argument
swallowed and nothing written, exit 0). No instruction surface pointed
at it, while the two that route an agent to its sibling
`scripts/ablation-dist-preflight.mjs` do exist — so the helper could
only be found by grepping `scripts/`, which is the memory-dependent
shape objectstack-ai#18226 was filed against. This PR adds the pointer to exactly
those two surfaces, in the order an ablation runs the two tools: mutate
on disk through the replace helper, then prove the mutation reached
`dist/` with the preflight, then read the run. Adoption-style only: not
a mandatory path, no gate, no migration of existing ablation scripts
(objectstack-ai#18226's triage 5707288884 set that floor; the migration set measured
empty in objectstack-ai#18598's census).

## `.claude/agents/os-dev.md` — one line in, one true duplicate out

Inserted, new :254 (119 B), directly under the mutate-then-build line of
the ablation step (:253 「每一腿(变异与还原)都是:改动 → 证明它真落到了磁盘 → …」, 108 B) and
above the preflight line (old :254, now :255, 113 B):

> - 落盘走 `node scripts/ablation-replace.mjs`,不走 `-i`
家族:锚点必须命中,写入与还原按磁盘核验。

「`-i` 家族」 is the name the file's existing :258 already gives the trap
(`sed`、`perl -i`、`str.replace`、`re.sub` 零命中也 exit 0). The line is a
routing line in the register of the preflight line beside it: one
instrument, the reason in the same breath, no 「必须」, no gate.

Paid by deleting old :319 (72 B), the opening bullet of 「干净收尾」:

> - 报告落两次,GitHub 优先:卡片评论在前,终报消息在后。

What it lost: nothing the file does not state elsewhere, twice. Its
three claims — two deliveries, GitHub first, comment before terminal
message — are the header's :17 (「报告交付两次,GitHub 优先:先作 issue 评论,…」) and
:18 (「…再作为终报消息…」), and the very next bullet of the same section (now
:320, 「终报消息之前,把同一段 JSON 发成 issue 评论,…」) restates the ordering together
with the marker. The section now opens on that bullet.

Why a payment and not a fold: measured on this tree, of the file's 284
adjacent bullet pairs ZERO merge under the 120-byte cap (smallest merged
133 B), and in the ablation block :253–:264 ZERO of 11 (smallest 162 B);
the block's total slack is 249 B spread over lines holding 0–61 B each,
none of which fits a 35-byte script path plus its reason. Re-wrap
funding is refused by the ratchet rule in any case.

Whole file: 403 → 403 lines; widest line 120 B before and after;
control-byte scan (`grep -naP` over the C0/DEL range) on both files:
clean.

## `packages/qa/dogfood/README.md` — one sentence

Step 4 of 「Adding a golden test」, placed before the existing preflight
sentence (mutate, then prove `dist/`); the helper's usage placeholders
are written as words here because the platform sanitizer eats tag-shaped
fragments in a PR body — in the file they are the helper's own
spellings:

> Make the revert itself through `node scripts/ablation-replace.mjs
--file PATH --anchor TEXT --replacement TEXT -- CMD` rather than `sed
-i` / `perl -i`: the anchor must hit, the write and the restore are
verified against the disk, and a mutation that did not land exits
non-zero instead of handing you a green run.

Non-governed, no line budget, the README's English register. Net +4
lines in that paragraph.

## Verification by the card's own instrument (tree
`objectstack-ai/objectstack`)

| reading | card @ `ab1d35925` | base `f6189a43f9` | head `fa55c9d0ad` |
|---|---|---|---|
| `git grep -lI 'ablation-replace'` | 1 as reported (that sha itself
answers 0 — the helper landed in `d0b8ec2aaf` after it) | 1 (the helper
itself) | **3** — `.claude/agents/os-dev.md`,
`packages/qa/dogfood/README.md`, `scripts/ablation-replace.mjs` |
| `git grep -lI 'ablation-dist-preflight'` | 7 | 8 (the card's 7 plus
the helper's own header, which names its sibling) | 8, unchanged |
| control `git grep -cI 'dogfood'` os-dev.md / README.md | 1 / 6 | 1 / 6
| 1 / 6, unchanged |
| `git grep -cI 'ablation'` on
`.claude/skills/dogfood-verification/SKILL.md` | 0 | 0 | 0 — the third
routing document named in objectstack-ai#18598's report does not exist; not touched |

Per-file hits at head: `ablation-replace` — os-dev.md 1, README.md 1,
the helper 14; `ablation-dist-preflight` — os-dev.md 1, README.md 1.

## objectstack-ai#18226 option A — a reading, not implemented here

Option A (「在字节纪律里加一条具名陷阱」: name the `perl -0` + `$/`-interpolates-to-NUL
trap in the byte-discipline rules) was **routed, not ruled, and has not
landed**. objectstack-ai#18226's triage 5707288884 routed it to the skills seat (「选项
A(在字节纪律里加一条具名陷阱)要走 skills 席,⛔ 不在本卡内落地」), the claim 5710594253 repeated
the boundary, and PR objectstack-ai#18598 landed option C (the helper) only. On this
tree: `AGENTS.md` carries no control-character or `check:nul-bytes`
sentence at all (its only `perl -i` mention is :224, the Bash-guard
write shapes); `os-dev.md` :387–:390 (字节与 sanitizer 纪律) covers writing
control characters as escapes and the harm of a raw NUL, and :258 covers
mode ② (nothing written, exit 0) — neither names mode ① (`-0` + `$/`).
Left untouched here (this card is the pointer only) and reported in
`open_questions` for the seat to queue.

## Gates (worktree at `fa55c9d0ad`; every exit code captured by
redirect-then-`$?`, never through a pipe)

Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the worktree, no hand-fed paths (change
set vs merge base `f6189a43f`: the two files; committed 2, working tree
0, untracked 0). 53 commands; reconciled with `--ran` carrying `:: exit
N` per line: **53 derived, 48 run, 5 NOT-MEASURED, 0 UNRUN**.

Named by the dispatch:

- `pnpm check:pm-skill-ratchet` :: exit 0 — 「.claude/agents/os-dev.md is
403 lines (ceiling 403; headroom 0)」 · 「widest table row is 0 bytes (pin
0; headroom 0)」
- `pnpm check:nul-bytes` :: exit 0 — 「OK (scanned 8814 text file(s) … no
raw ASCII control bytes)」
- `pnpm check:agent-model-declared` :: exit 0 — 「1 agent definition(s)
under .claude/agents/ all declare a model」
- `pnpm check:pm-governed-merges` :: exit 0 (the post-merge audit).
`node scripts/pm/check-governed-merges.mjs --pr NUMBER` needs this PR's
number, so it runs after creation; its verdict (expected GOVERNED, exit
3, which is a verdict and not a failure) is recorded in the report
comment on the card.
- `pnpm lint` (repo-wide, `eslint . --no-inline-config`, as PR objectstack-ai#18654
ran it) :: exit 0, 100 s, on `fa55c9d0ad`

The other 43 that ran, all exit 0: `check-ci-filter-parity`,
`check-closing-keyword-parity` (+ `--self-test`),
`check-comment-mask-adoption` (+ `--self-test`),
`check-comment-mask-corpus`, `check-keyed-text-bounds` (+
`--self-test`), `check-platform-object-tenancy-census` (+
`--self-test`), `check-plugin-teardown-shape` (+ `--self-test`),
`check-registry-log-declared` (+ `--self-test`),
`check-rest-log-spy-declared` (+ `--self-test`),
`check-system-context-census` (+ `--self-test`),
`check-undeclared-dep-imports` (+ `--self-test`),
`docs-audit/check-affected-docs`, `docs-audit/check-drift-comment`,
`pm/check-governed-queue-guard --self-test`, `pm/check-harness-current
--self-test`, spec `check:empty-state` · `check:liveness` ·
`check:strictness-ledger` · `check:variant-docs`,
`check:agent-test-spelling`, `check:commit-card-trailers`,
`check:cross-package-test-inputs`, `check:doc-authoring`,
`check:driver-memory-census`, `check:org-identifier`,
`check:page-declaration-shape`, `check:pm-skill-id-lint`,
`check:published-files`, `check:refd-timer-probe`,
`check:skill-frame-sync`, `check:slot-lookup`,
`check:test-source-alias`, `check:tier-file-adoption`,
`check:type-source-resolution`, `check:watch-hint-literal`.

NOT MEASURED (exit 3, `PREREQUISITE NOT MET` — each loads BUILT
artifacts and refuses on this unbuilt worktree; the diff touches no
source, config or manifest, so their verdict is CI's on its correctly
built tree): `pnpm --filter @objectstack/lint run
check:doc-formula-expressions` (`@objectstack/formula` and
`@objectstack/lint` not built), `pnpm check:dts-closure`, `pnpm
check:dual-build-cjs-loads` (101 packages without `dist/`), `pnpm
check:lean-entry-closure` (`packages/objectql/dist` absent), `pnpm
check:sourcemap-no-sources-content`. Their only remedy is a whole-repo
`pnpm build`, not proportionate to a prose-only change; declared, not
hidden.

① / ②: the only package touched is `@objectstack/dogfood` (private, no
`files[]`, scripts `typecheck` + `test` only); the README is an input to
neither (the sole `README` mention under `packages/qa/dogfood/test` is a
comment in `rls-fixture.dogfood.test.ts:29`), so no closure build and no
package test is owed — a declared narrowing.

`skip-changeset`: `.claude/**` is on the fast lane and
`@objectstack/dogfood` is private with no `files[]` — nothing published
moves.

## Acceptance notes

- noted, not filed: the card attributes 「`git grep -lI
'ablation-replace'` → 1」 to `ab1d35925`, but that sha itself answers 0 —
the helper landed in `d0b8ec2aaf` (objectstack-ai#18598) after it; the reading was
evidently taken on a tree that already carried the merge. The premise is
unaffected: base `f6189a43f9` answers 1 (the helper alone). 承接者:无 —
recorded here and in the report.
- noted, not filed: `ablation-dist-preflight` answers 8 on the tip, not
the card's 7 — the eighth is the helper's own header cross-referencing
its sibling, which is the intended composition (the two tools prove
different halves and say so). 承接者:无.
- Rider not taken: the pending `tail --pid` rider in 干净收尾 (named by PR
objectstack-ai#18659) stays for the next os-dev.md PR; this PR's deletion in that
block is the duplicate opener, not a slot for it.

## 维护者速读(草稿)

**改了什么** — 只动两处文档。`.claude/agents/os-dev.md`(行数不变 403 / 403)在消融步骤里加一行,把
dev 指向 PR objectstack-ai#18598 新落地的锚点核验替换助手 `scripts/ablation-replace.mjs`,与已有的
`ablation-dist-preflight.mjs` 指向并排、按消融实际执行顺序(先落盘变异、再证明到达
`dist/`、再读结果);付账是删掉「干净收尾」节开头一条与文件头部及同节下一条重复的「报告落两次」句。`packages/qa/dogfood/README.md`
的「Adding a golden test」第 4 步加一句同样的指向。

**为什么改** — 这个助手落地了,却没有任何一份指令文档指向它;而指向其姊妹工具的文档有两份。一个采纳式工具若无人被指向,只有碰巧 grep
过 `scripts/` 的人会用到 —— 这正是卡 objectstack-ai#18226 要消灭的「靠记性」。⛔
不做成必经路径、不加门禁、不批量迁移旧消融脚本(objectstack-ai#18226 分诊已定这条地板)。

**风险与代价(含回滚)** — 零代码、零
changeset、零门禁改动;唯一的内容损失是被删的那条重复句,它的三个主张在文件头部(:17–:18)与同节下一条(:320)各有原文。回滚
= revert 本 PR,无其它文件牵连。

**席位意见** — (留空,席位定稿)

**你要做的** — 读新 :254 一行与被删 :319 一行;批准即由席位落地。若不同意付账形态,点名另一条重复句,席位代改。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_

Co-authored-by: Claude <noreply@anthropic.com>
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/xs skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] the os-dev write budget forbids the only act that clears a Check Changeset red — PATCH of the PR body has no compliant path

3 participants