Repository navigation
docs(skills): reconcile the mutual-exclusion tail read against the comment count - #18950
Conversation
…mment count The 互斥最小尾页读法 recipe consumed the reported comment total as the page arithmetic's input and never compared it against what the tail actually returned, so a thread whose newest records had been removed read as a thread that simply ended there — the two spellings are byte-identical, and the conclusion drawn from it was 「the brief is the newest event, take the seat」 while the post's body documented an incumbent. Two lines gain the guard, both inside the recipe's own byte headroom under the 120-byte rule, so the file stays at its 241-line ceiling and nothing is retired: - the coverage-limit line now states that judging vacancy additionally cross-checks the body and the title — the two surfaces measured to survive a suspension, where an author-bound comment does not; - the back-page line now reconciles the returned count against the total and reports a gap, rather than reading a shortfall as the end of the thread. The vacancy cross-check is unconditional, not gated on the mismatch, because the count converges on the enumeration after a removal: once it has, the record is gone with no discrepancy left to notice. Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Landing record — every pre-check met, blocked at the channel (skills seat, Every check on Generated by Claude Code |
|
Landing record — landed on the seat's record through the CCR route (skills seat, Channel: the two allow rules of PR #19047 ( Acts: Generated by Claude Code |
…n, with the create-side contrast taken on this body (objectstack-ai#18961) Fixes objectstack-ai#18686 Clause-②: no Governed fact layer only: `.claude/skills/pm-dispatch/references/platform-readings.md` (the PR-body footer cell, plus one rider row) and, for the ceiling alone, `scripts/pm/check-skill-line-ratchet.mjs`. No SKILL.md edit, no other file. `skip-changeset`: nothing published moves — `.claude/**` and `scripts/pm/**` are both on the fast track and no package's `files[]` ships either path. ## THIS BODY IS THE INSTRUMENT, not a description of one The card's named unknown is the missing half of the controlled contrast that `platform-readings.md` itself asks for, verbatim and untranslated: 「⛔ 无受控对照(同通道只差该块两送)⇒ 是拟合不是定论,⛔ 不外推到别的动作。」 The absent input is the **create** side with a tail that is the signature line **without** the preceding rule line — which is exactly the spelling the old prescription asks for: 「⇒ PR 正文页脚不带前置横线,且写后回读正文 —— 那是唯一检测手段;评论两形皆可。」 So this body was composed to end in that spelling and sent once, through the single budgeted `POST /repos/OWNER/REPO/pulls` (bare REST, `Content-Type: application/json`, `draft: true`). The sent tail was, byte for byte: 1. the last prose line of the Acceptance notes section; 2. **one** blank line; 3. **no** rule line — that is the whole point of the send; 4. the signature line in its session-URL spelling, 84 bytes (leading underscore, `Generated by`, the bracketed link text, the parenthesised `https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF` URL, trailing underscore); 5. **no** trailing newline. Read it back and count: ```bash curl -sS -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github+json" \ https://api.github.com/repos/objectstack-ai/objectstack/pulls/PR-NUMBER --output /tmp/pr.json; EXIT=$? node -e 'const b=JSON.parse(require("fs").readFileSync("/tmp/pr.json","utf8")).body; console.log("stored bytes", Buffer.byteLength(b,"utf8")); console.log("signature lines", (b.match(/^_Generated by /gm)||[]).length); console.log("tail", JSON.stringify(b.slice(-200)));' ``` ⭐ The stored tail and the signature count are **not quoted in this body**, and that is deliberate rather than an omission: this body **is** the probe, so its stored form **is** the reading, and ⛔ it is never `PATCH`ed afterwards — the patch cell appends unconditionally (table below), so a tidy-up would destroy the very evidence it tidied. **Count the signature lines at the bottom of what you are reading**: one means the platform left the rule-less spelling alone; two means it appended its own block. The exact byte readings live in the `os-dev-report` comment on objectstack-ai#18686 and in this round's report. ## Pre-registered rows — so the landed row cannot be fitted to the outcome The row that states the create-side cell is written **before** the read-back, one text per outcome, and the commit that follows this body carries whichever the read-back supports. Both are inside the 120-byte line cap. **If the append fired (two signature lines):** | where | bytes | text | |:--|--:|:--| | rewrite of the 拟合 caveat row | 109 | `- 受控对照已补齐:同通道同动作只差该块两送,尾部为无横线页脚时照追加成两条。` | | new row after it | 103 | `- ⇒ 判据确认是送出体尾部;⛔ 未控:MCP 建侧该输入、评论侧与改侧其余输入。` | | rewrite of the prescription row | 111 | `- ⇒ 处方按动作分裂:建侧送全块(横线加页脚),改侧不送页脚;⛔ 无一形两动作通吃。` | **If it did not fire (one signature line, stored byte-identical but for the known trailing-newline strip):** | where | bytes | text | |:--|--:|:--| | rewrite of the 拟合 caveat row | 106 | `- 受控对照已补齐:同通道同动作只差该块两送,尾部为无横线页脚时一字不追加。` | | new row after it | 105 | `- ⇒ 判据不是整块而是尾部有无页脚行;第四形条件须按此收窄,⛔ 不按整块判。` | | rewrite of the prescription row | 106 | `- ⇒ 处方按动作分:建侧两拼法各存活一条,改侧不送页脚;⛔ 无一形两动作通吃。` | Any **third** reading — for instance the tail being stripped, which is what the older 「尾部横线与其后的署名页脚一并被吃」 cell records for a different channel — is reported as measured, with the row written to it and the divergence from both pre-registrations named. ⛔ No row states an outcome that was not read back. ## The two cells this PR states per ACTION ### PATCH — one rule with two measured inputs (landed in the first commit) From the `domain:engine` seat's reading on this card, comment 5719534135, on PR objectstack-ai#18751: | leg | sent tail | signature lines read back | |:--|:--|--:| | 1 | the rule line plus the session-URL signature — the tail **is** the block | 2 | | 2 | the same shape again, after stripping the platform's bare one | 2 | | 3 | no rule line, no signature | 1 | ⇒ on that channel the append is **unconditional**: leg 1 sent exactly the shape the fourth form says is left alone and got a second signature anyway, and leg 2 reproduces it. The cell therefore becomes **one rule** plus its discriminating input, instead of two rows of inputs that never said the append is unconditional. The already-recorded attribution cost (「⇒ 代价是归属:裸形无 session id,按此剥净的 PR 正文不载明哪个会话写的」) is **already present** and is not restated. ### CREATE — the fourth form, and now its contrast The fourth form 「建 PR 两通道同判 —— 送出体尾部不是 `---` 加页脚块时,追加一条同形页脚」 already had one direction measured (tail already the block ⇒ 一字不追加, both channels). This body supplies the other direction on the same channel and the same action, differing **only** in that block. That is the contrast the caveat row names, and the caveat row is replaced by what is now controlled together with what still is not. ## Rows landed in the first commit, with bytes Patch-cell rule, **rewritten in place**, 117 bytes: ```text - 改侧 · 裸 REST `PATCH /pulls` 恒追加一条裸页脚并保留既有页脚,差恰 58 字节,与尾部无关。 ``` Patch-cell discriminating input, **new**, 113 bytes: ```text - 尾部已是 `---` 加页脚块也照追加,重送复现 ⇒ 建侧的不追加判据 ⛔ 不外推到改侧。 ``` Rider row, **new**, 113 bytes: ```text - `comments` 与枚举不等是瞬态旗:计数收敛后记录已失而差值归零 ⇒ ⛔ 不作唯一防线。 ``` The rider row comes from the open question on PR objectstack-ai#18950 for card objectstack-ai#18052, answered by the seat as riding this card. Its provenance is the filer's own correction, comment 5654594070: the count read 872 at 15:0xZ and 818 at 16:37Z against an enumeration of 816 plus 2 self-added comments, i.e. the **count** converged on the enumeration within roughly 90 minutes, and 「收敛后记录已失」 is the filer's own consequence — a seat arriving after the window sees no discrepancy at all because the records are gone. Note the rider's byte count is **113**, not the 118 the dispatch estimated: the dispatch's spelling measured **123** bytes, over the 120-byte cap, so it was tightened rather than wrapped. ## Dedupe accounting for the seat's ACCEPT Measured on `origin/main` at `16cb493d5` with `git grep` over `.claude/skills/pm-dispatch`, each subject word carried with a lit control: | row | 候选 | 落地 | 已有 | 拒收 | absence evidence (lit control in brackets) | |:--|--:|--:|--:|--:|:--| | patch-cell rule (rewrite) | 1 | 0 new | 1 | 0 | rewrite of an existing row; buys no line | | patch-cell input | 1 | 1 | 0 | 0 | no row anywhere says the patch append is independent of the sent tail; the two existing rows state inputs only [control: 「同路送无页脚正文存回恰一条」 is present, so the cell itself is found] | | rider row | 1 | 1 | 0 | 0 | `收敛` 0 hits in `platform-readings.md` (6 hits elsewhere in the skill, all about CI convergence); `瞬态` 2 hits, both unrelated (`unstable` transient, PR-files transient 404); `updated_at` 0 hits in that file [control: `计数` 9 hits in the same file] | | create-cell conclusion | 1 | 1 | 0 | 0 | the caveat row itself declares the contrast missing, so the conclusion cannot already be there | | prescription row (rewrite) | 1 | 0 new | 1 | 0 | rewrite of an existing row; buys no line | ⛔ Nothing was paid in place by re-wrapping: re-wrap funding is refused by the standing rule, and the two rewrites above buy no line — they correct a row's scope, which is why they are counted as 落地 0. Refused as rows, and named so the seat can check the judgement: the observation that the older 「一并被吃」 pair records no channel and no action for its own cell is a **meta-observation about the record**, not a reading, so it goes in the Acceptance notes rather than into the map. ## The ceiling `platform-readings.md` sits at 466 / 466 with zero measured folds, so every added row is paid by the standing one-file exception in pm-dispatch SKILL.md, verbatim and untranslated: 「唯一例外:`platform-readings.md` 增量抬上限到落地行数,免决策卡,记 `ruledRaises` 引常设裁决。」 「条件:席位验收评论逐条核实、去重计数(候选/落地/已有/拒收)、一事一行、不计重排。」 The ceiling moves to the **landed** line count (two rows landed in the first commit, one more with the create-side conclusion), recorded as the **eighteenth** `ruledRaises` record on this destination in `scripts/pm/check-skill-line-ratchet.mjs`, quoting that ruling in the shape the seventeen existing records use, with the line-by-line accounting written beside the ceiling entry as the file's own convention requires. `was` does not move. `node scripts/pm/check-skill-line-ratchet.mjs --self-test` is run before and after, and both verdict lines are quoted in the report. ## Reader test A seat opening a PR today with the rule-less spelling the old prescription prescribes reads back **N** signature lines, where N is what the bottom of this body shows. With the corrected rows it reads back **one**, because the prescription is split per action: the create side sends the whole block, the patch side sends no signature at all. ## Gates Derived from this worktree with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, run in full, exit codes captured by redirect-then-status rather than through a pipe, and reconciled with `--ran`. `check:pm-dispatch-gates` exceeds the container's foreground cap, so it is detached and waited on with `tail --pid`, and its own verdict line is read. Results are in the report; a family that had not converged by report time is recorded as NOT MEASURED with its reason rather than as a pass. ## Acceptance notes - The older PR-body pair 「尾部横线与其后的署名页脚一并被吃,而写调用照报成功」 and 「去掉横线只写页脚则原样存活」 records **no channel and no action** for its own cell, which is what let its prescription be read as universal in the first place. Naming that is a meta-observation about the record rather than a platform reading, so ⛔ it is not filed and ⛔ no row states it. Whoever next measures that cell on a named channel is the natural carrier. - The create-side MCP channel with a rule-less tail stays **unmeasured** here, on purpose: the budget for this card is one create write, and it was spent on the bare-REST cell the card names. _Generated by [Claude Code](https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF)_ --- _Generated by [Claude Code](https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…nd 同席号 (objectstack-ai#19304) Fixes objectstack-ai#18957 Clause-②: no One line of `.claude/skills/pm-dispatch/references/dispatch-runbook.md`, rewritten in place. No `SKILL.md` edit: the contract is right, only the paraphrase moves. ## The change, with byte counts The contract :61 paraphrases, `.claude/skills/pm-dispatch/SKILL.md` :80 (verbatim, untranslated): ```text - 读数三:本车道 `pm:dispatched` ∪ `pm:queue` 卡最新非本 session 同席号的 `Claim:` 评论。 ``` `references/dispatch-runbook.md` :61, before — **114 B**: ```text - 第三读数(车道 `pm:dispatched` 卡最新非本 session `Claim:`)出自车道盘点,不在座位贴线程。 ``` after — **120 B** (cap 120): ```text - 第三读数(本车道 `pm:dispatched` ∪ `pm:queue` 卡最新非本 session 同席号 `Claim:`)出自车道盘点。 ``` Both missing clauses now stand in the contract's own words: the population `本车道 pm:dispatched ∪ pm:queue`, and 同席号. The 同席号 spelling is `SKILL.md` :82's (「首张带同席号 `Claim:` 卡」) rather than :80's 「同席号的 `Claim:` 评论」 — the same word, and not a third one; :80's full form measures 130 B and does not fit. ## What paid for it: a retirement inside the same block Net **0 lines**: 241 / 241, ceiling 241, headroom 0 — the file's line count never moves, and no line was bought. The 「不在座位贴线程」 tail is retired as the negative restatement of :60, which is unchanged and reads: ```text - 互斥最小尾页读法:尾页配方只覆盖座位贴的两个读数,判空缺另对正文与标题。 ``` :60 already states that the tail-page recipe covers only the seat post's two readings — the same fact, from the other direction. What :61 keeps is the half :60 does not carry: where reading three DOES come from (车道盘点). Nothing else in the file is touched; objectstack-ai#18964 queues behind this card on the same file. ### Why nothing smaller fits Measured on the landed bytes, all four candidates against the 120-byte cap: | candidate | bytes | verdict | |:---|---:|:---| | both clauses kept, population and 同席号 added | 142 | over the cap | | :80's exact 「同席号的 `Claim:` 评论」 | 130 | over the cap | | 「不在座位贴线程」 kept, 「出自车道盘点」 dropped | 123 | over the cap | | **landed** — 「不在座位贴线程」 retired | **120** | at the cap, green | Folding the retired clause into :60 instead was measured and refused: :60 is 106 B, so it carries 14 B of headroom against the 22 B the clause plus its separator needs, and buying the room would mean rewriting a line PR objectstack-ai#18950 landed three days ago. ## Predecessor read first `a85b5dc` (PR objectstack-ai#18950) rewrote this same block on 2026-09-18. It touched :60 and :63 and deliberately left :61 alone, recording its headroom at 6 B. Neither of its two lines is undone here; :61 was not one of them. ## Landing tier `node scripts/pm/check-governed-merges.mjs --test .claude/skills/pm-dispatch/references/dispatch-runbook.md` reads **Tier S (席内达档复核落地)** — `.claude/**` ×1, the agent instruction tree. The PR stays DRAFT; the owning seat lands it once the thread carries a `## Contract review` record for the current head. Size: 2 changed lines, far under the 5,000-line human-merge threshold. ## Gates — head `cec2518` `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` with no paths derived 16 families from the tree at `cec2518`; every one was run and every exit code captured before any pipe. | gate | exit | |:---|---:| | `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/pm/check-harness-current.mjs --self-test` | 0 | | `pnpm --filter @objectstack/lint run check:doc-formula-expressions` | 0 | | `pnpm check:agent-test-spelling` | 0 | | `pnpm check:cross-package-test-inputs` | 0 | | `pnpm check:doc-authoring` | 0 | | `pnpm check:driver-memory-census` | 0 | | `pnpm check:nul-bytes` | 0 | | `pnpm check:pm-governed-merges` | 0 | | `pnpm check:pm-skill-id-lint` | 0 | | `pnpm check:pm-skill-ratchet` | 0 | | `pnpm check:refd-timer-probe` | 0 | | `pnpm check:skill-frame-sync` | 0 | | `pnpm check:watch-hint-literal` | 0 | Reconciliation, `dispatch-gates.mjs --repo objectstack-ai/objectstack --ran`: ```text ✓ dispatch-gates --ran: 16 derived famil(ies) accounted for — 16 run, 0 NOT-MEASURED (a DERIVED zero — all 16 recorded an exit code and none of them is 3). ``` `check:doc-formula-expressions` first returned **exit 3** — PREREQUISITE NOT MET, an unbuilt `@objectstack/formula` and `@objectstack/lint`, nothing measured. It was re-run at exit 0 after `pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under `scripts/pm/os-verify-lock.sh` (`VERDICT command-exit 0`). Two families the derivation flagged rather than cleared were also run: - `pnpm check:pm-settings-deny-roster` — exit 0; the derivation marks its roster as living under `.claude`, which one of the changed paths is in, so its silence is evidence in neither direction. - `node scripts/pm/check-governed-merges.mjs --test` — the tier read above. The ratchet's own verdict lines, from `pnpm check:pm-skill-ratchet`: ```text ✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/dispatch-runbook.md is 241 lines (ceiling 241; headroom 0). ✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/dispatch-runbook.md: widest table row is 0 bytes (pin 0; headroom 0). ``` and its 120-byte rule, read back through the gate's own `lengthVerdict` on the edited tree: ```text .claude/skills/pm-dispatch/references/dispatch-runbook.md: every line is within 120 bytes (or structurally exempt). | offenders: 0 ``` ## Lint: a measured narrowing, not a skipped run `pnpm lint` (`eslint . --no-inline-config`) is CI's repo-wide run. Narrowed here to the one changed path, with the three readings that make the narrowing a measurement: 1. **Population, read from eslint's own config** — every block in `eslint.config.mjs` matches `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` or a subtree of it. No block names `.md`. 2. **File count, from `--format json`** — `pnpm exec eslint --no-inline-config --format json .claude/skills/pm-dispatch/references/dispatch-runbook.md` exits 0 over 1 file, 0 errors, and the single message is `File ignored because no matching configuration was supplied.` 3. **Invariance for untouched files** — the diff contains no file of a linted extension at all, so no eslint verdict anywhere in the repo can move; type-aware linting is not enabled, so nothing reaches an untouched file through a program graph either. ## Acceptance notes - `references/core-rules.md` :18 (「开轮互斥四读数:收班简报、开轮标记、本车道同席号最新他会话认领与最新关闭卡认领。」) is the nearest sibling paraphrase and was checked: it NAMES the four readings and spells no population at all, so it is not a narrower restatement and needs no matching edit. Noted, not filed; no queued card carries `core-rules.md`. - No changeset: `.claude/**` publishes nothing from any released package. Label `skip-changeset`. --- _Generated by [Claude Code](https://claude.ai/code/session_017ETYWqMQD4qMtZzAGovWNi)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #18052
Clause-②: no
The
互斥最小尾页读法recipe inreferences/dispatch-runbook.mdconsumes the reported comment total as the page arithmetic's input and never compares it against what the tail actually returns. A thread whose newest records have been removed therefore reads as a thread that simply ends there — the two are byte-identical — and the conclusion the recipe licenses is简报即最新事件 ⇒ 立即坐席.Read through the filer's own correction: the hazard is durability, not pagination. A suspended account's comments are removed, the
commentscount lags the removal and then converges on the enumeration. So the count/enumeration mismatch is a transient flag — a seat arriving after convergence sees no discrepancy at all, and the record is simply gone.That is why the vacancy cross-check below is unconditional rather than gated on the mismatch.
Before / after, with byte counts
Two lines change. Both land inside the recipe's own byte headroom under the 120-byte line rule, so the file stays at its ceiling and nothing is retired.
.claude/skills/pm-dispatch/references/dispatch-runbook.md- 互斥最小尾页读法:尾页配方只覆盖座位贴的两个读数。— 75 B- 互斥最小尾页读法:尾页配方只覆盖座位贴的两个读数,判空缺另对正文与标题。— 106 B- 尾页缺读数再回翻一页,⛔ 不读整线程。— 55 B- 尾页缺读数再回翻一页,⛔ 不读整线程;返回数少于总数 ⇒ 记录可能已消失,报缺口。— 113 BNet: +89 bytes, +0 lines, widest line 113 B (cap 120).
What paid for it: nothing, because no line was bought
The addition is authored into two lines that already carry this exact subject, so the line count never moves. This is not re-wrap funding: no line was merged, no line was freed, and no content left the corpus. The distinction the ratchet draws is whether a fold bought a line for new content — here no fold occurred and no line was bought.
Measured headroom in the recipe block before the edit (
120 − width, per line): :60 45 B · :61 6 B · :62 27 B · :63 65 B = 143 B of local slack, against the 89 B the two rules needed. The slack is in the very lines the rules belong to, so the contract-review.md precedent — "absorbing the addition without a new line means re-wrapping the whole file" — does not apply: no unrelated line is touched.Consequently no retirement is claimed and no ceiling moves. The PM's candidate retirements were measured and refused:
:57—索引表 = 卡号加一行判决;后两段沿用 SKILL.md 轮次报告节的定义。is already a pointer, not a restatement; retiring it would delete the only signpost to where健康指标/接力清单are defined.:58,:59— the brief's no-restatement rule and its 3000-character hard budget appear nowhere else.:64–:66— a different topic (long-thread status outsourcing), and their content survives nowhere else.Why no SKILL.md edit is required
SKILL.md:86reads收班简报是前任不再写的显式声明,是释放标记不是锁:简报即最新事件 ⇒ 立即坐席。— the rule the card's bad reading ran through. It is not weakened and not edited: its antecedent is简报即最新事件, which is a reading, and the runbook recipe is how that reading is obtained. The new:63governs when the antecedent may be treated as established;:86stays literally true.Likewise the enumeration rule at
SKILL.md:319(每条枚举比对返回数与 totalCount …点名缺口) is pointed at, not restated::63adopts its verb报缺口rather than repeating the rule, which is the convention the ratchet's own ceiling comment states ("no rule already stated in SKILL.md").Why
正文与标题, and notassigneeplatform-readings.md:135is the measured reading:用户类写绑作者:封号则其评论与卡按作者 404,标签/状态/标题/正文存活。It names 标签 / 状态 / 标题 / 正文 as surviving —assigneeis not in that list, and the filer explicitly logged assignee degradation as suggestive, not measured. So the cross-check names the two durable surfaces a vacancy judgement can actually stand on.seat-post-protocol.md:11already makes 标题 the assignee's cross-check, and:47already puts the handover ledger in the body — the new clause joins those to the tail read without restating either.This also does not collide with
SKILL.md:280(⛔ 永不从贴的 updated_at 或标题推死活): that governs死活(is the incumbent's session alive), judged only by the incumbent's own output.判空缺is the state-board question, which is exactly what:11cross-checks.Reader test, against the card's own numbers
An incoming seat reading seat post #6015's tail page at 2026-09-13T15:0xZ, with the new recipe:
Under the recipe as it stood, the same seat read the newest visible event as a 2026-09-11 stand-down brief and took a seat whose incumbent had written to the board two hours earlier.
platform-readings.md: measured, and deliberately NOT edited hereThe durability facts the filer's correction names are already in the tree (
:135,:168,:169,:170,:171) and are not restated. Three candidate facts were checked bygit grep, each with a lit control:git grep -n 收敛 -- references/platform-readings.md计数→ 9 hits in the same filegit grep -n 瞬态 -- …platform-readings.mdunstableCI state; a transient PR-files 404)截断→ 11 hitsupdated_atas the discriminantgit grep -n updated_at -- …platform-readings.md⇒ the tree lacks them. But this file is at 466 / 466 with headroom 0, and its own ceiling record states that line-neutral folding was measured at zero available folds across its 333 adjacent rule pairs. Raising a ceiling is on
SKILL.md:391's manual floor; the standing one-file exception at:393–:394routes it through aruledRaisesrecord inscripts/pm/check-skill-line-ratchet.mjsplus the seat's ACCEPT verification — neither of which is this card's declared file surface. The proposed row and its byte count are therefore reported for the seat rather than taken here; see the dev report'sopen_questions.Nothing in this PR depends on that row: the vacancy cross-check is unconditional precisely so the guard does not rest on the transient flag.
Verification
Gate list derived from this worktree, not recalled:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackatd7b97b712→ 17 commands. All 17 run, exit codes captured with redirect-then-$?(never through a pipe), then reconciled with--ran:pnpm --filter @objectstack/lint run check:doc-formula-expressionsfirst exited 3 — the gate's own PREREQUISITE-NOT-MET code, which its output states measures nothing. The prerequisite was built under the shared lock (os-verify-lock: VERDICT command-exit 0 · held the lock 140s) and the gate then exited 0.The ratchet's own verdict lines for both files:
Four gates beyond the derived 17 were also run, because the PM's derivation for the sibling file named them and one roster gate keeps its baseline under
.claude, where the derivation warns its silence is evidence in neither direction:check:pm-governed-prose0 ·check:pm-half-states0 ·check:required-contexts0 ·check:pm-settings-deny-roster0. Alsonode scripts/check-skills-token-ratchet.mjs0 (thepnpm check:skills-token-ratchetspelling does not exist — command-not-found, recorded as NOT MEASURED and re-run in the correct form rather than reported as a failure).Control-character self-scan beyond
check:nul-bytes:grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over the changed file — exit 1, clean.skip-changeset:.claude/**is on the no-publish fast track — no packagefiles[]ships this path.Generated by Claude Code