Repository navigation
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
Conversation
…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>
维护者速读(终稿)skills 席 改了什么 — 只改 为什么改 — 预算禁止 dev PATCH 正文,而 风险与代价(含回滚) — 受管 agent 定义,只改规则文本;不加脚本、不加门禁、不扩 dev 写面(席位本来就持有 席位意见 — 荐合 B。四轴一致:零额外写就关掉了实测的案例;预算保持机械的四端点清单,而不是每轮争论「什么算修补」;dev 少一个分支而不是多一个(正文 PATCH 正是平台读数里页脚会被吃/追加的那条路);不扩能力面。 你要做的(一个动作) — 认可 B 即 Approve 本 PR(或直接人工合并);席位随后按裁定 C 落地。要 A(抬预算到五笔或列豁免)则 Request changes,本席另起。 Generated by Claude Code |
…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>
…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>
Fixes #18443
Clause-②: no
Governed surface (
.claude/**): draft,skip-changeset, the maintainer's approval lands it. File surface:.claude/agents/os-dev.mdonly. Head1344eb515, based6137fd6c. Line-neutral: 403 / 403.The gap, re-measured on
origin/maind6137fd6c预算外零写,⛔ 不 PATCH 正文.Check Changeset(pr-automation.ymlchangeset-check) runsscripts/check-changeset-no-major.mjs --base MERGE_BASE, which reads the declaration fromgithub.event.pull_request.bodythroughreadClause2Line(imported fromscripts/pm/check-clause2-carriers.mjs; line-initial key,yes/noonly). The red is CONDITIONAL, not unconditional as the card states: a missing line fails only asnot-measured-material(exit 1) — apatchchangeset on a package the diff grew, none of them raised tominor+; with no candidate offender it isnot-measured-moot(exit 0), and the whole job is exempt underskip-changeset. The remedy when it does fail is still exactly one act, a body write, and the job isedited-subscribed so that write re-fires it without a push.d6137fd6cthe pm-dispatch template names the claim comment as the only carrier (SKILL.md:473Branch:、Clause-②:各占一行且行首, :476, :809 template), no committed text tells anyone to put it in the PR body, andos-dev.mddid not contain the tokenClauseat all (grep: 0). The seat's own closure lives in its dispatch prompts (this card's order says putClause-②: noin the PR body AT CREATION), not in the tree. So onorigin/mainthe gap was still the whole class, not only the residual.edited-subscribed, each cleared only by a body write —Check Changeset(theClause-②:line),Duplicate Fix Guard(aFixesline naming a card another open PR claims),Part-of Closing-Keyword Guard(a closing keyword besidePart 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 RESTPATCH /pullsappends a bare footer beside an existing session-URL one, +58 B).translation-target-unknownreadsapps[].navigationonly, 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 comment5696114759lists write (4)PATCH /repos/.../pulls/18433with 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 thePOST /pullsalready in budget, and that one write carries theClause-②: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 neverPATCHes 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):
POST /pulls), and the gate reads the body, so carrying the line at creation closes the case lint:translation-target-unknownreadsapps[].navigationonly, 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.repairis a word every out-of-budget write will be argued under, round after round); B keeps it mechanical: four named endpoints, and aPATCHinapi_writesis 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 withapi_writes,batchdefault 2, user-account roles #18072 intact (REST-only dev writes, a ledger the seat verifies) instead of carving an exception into it.PATCHtraps 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.PATCH .../issues/{n}body, ✓ inrest-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):
- 三类发现附查重词进报告交席位代立,dev 不 \POST /issues`;预算外零写,⛔ 不 `PATCH` 正文。`- 三类发现附查重词进报告交席位代立,dev 不 \POST /issues`、⛔ 不静默弃报;预算外零写。`- PR 正文 dev 只写一次,在开 PR 那一笔,⛔ 不 \PATCH`;事后要改的报告点名改法,席位代写。`- 报告记 \api_writes`(次数 + 端点清单)与 `mcp_calls`(MCP GitHub 调用计数),席位对照预算核验。`- 报告记 \api_writes`(次数 + 端点清单)与 `mcp_calls`;越界真写了的照列注明缘由,⛔ 不漏记。`- 立不成 ⇒ 发现连同缘由写进报告交 PM 代立;⛔ 不查重硬立与静默弃报同为禁形。Definition of done block:
Draft PR 指向 mainline:- 正文行首照抄认领的 \Clause-②:` 行:`Check Changeset` 读正文不读卡,开 PR 那一笔就带上。`字节与 sanitizer 纪律 block:
- 评论通路 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. TheMCP GitHub 调用计数parenthetical dropped from L58 is restated by the report template's ownmcp_callsfield description eight lines below the JSON fence.Rider not taken: the pending
tail --pidrider (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/objectstackfrom the worktree at1344eb515(change set: 1 path vs merge based6137fd6c; the tool flagged STALE TREE —origin/mainmoved one commit to62d830e54,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 0node scripts/check-closing-keyword-parity.mjs --self-test:: exit 0node scripts/check-comment-mask-corpus.mjs:: exit 0node scripts/pm/check-governed-queue-guard.mjs --self-test:: exit 0node scripts/pm/check-harness-current.mjs --self-test:: exit 0pnpm --filter @objectstack/lint run check:doc-formula-expressions:: first run exit 3 PREREQUISITE NOT MET (nodist/for formula + lint), prerequisite built underos-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 0pnpm check:agent-test-spelling:: exit 0pnpm check:commit-card-trailers:: exit 0pnpm check:doc-authoring:: exit 0pnpm check:driver-memory-census:: exit 0pnpm check:nul-bytes:: exit 0pnpm check:pm-governed-merges:: exit 0pnpm check:pm-skill-id-lint:: exit 0pnpm check:pm-skill-ratchet:: exit 0 —.claude/agents/os-dev.md is 403 lines (ceiling 403; headroom 0), same reading on the unedited treepnpm check:refd-timer-probe:: exit 0pnpm check:skill-frame-sync:: exit 0pnpm check:watch-hint-literal:: exit 0.claude:pnpm check:pm-settings-deny-roster:: exit 0node 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--ranreconciliation: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 overscripts/,.claude/,AGENTS.md,docs/: zero hits for each).skip-changeset:.claude/**is on the fast lane (nothing published moves);check-governed-merges --testanswered 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 明禁 devPATCH正文;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
Clause-②:carrier; after this PR the dev copies that line into the body, so the template needs no change to close the case. 承接者:thedomain:skillsseat, on the next pm-dispatch SKILL.md PR, if it wants the body carriage stated on both sides.Check Changesetreds a PR whose body carries no line-startClause-②:declaration」 is conditional in the gate (not-measured-materialonly); the remedy statement stands regardless. 承接者:无 — recorded here and in the report.tail --pidrider is left in place (different block, see above). 承接者:the next os-dev.md PR.origin/mainadvanced by62d830e54during this round; re-derivation after a fetch names the same 18 families (the changed file is not one of them). 承接者:无.Generated by Claude Code