Skip to content

fix(cli): os migrate meta --from N lists the conversions its tombstones prescribe, and an empty range stops reading as success - #17462

Merged
os-zhuang merged 5 commits into
mainfrom
claude/issue-17134-migrate-meta-default-range
Sep 11, 2026
Merged

os-zhuang merged 5 commits into
mainfrom
claude/issue-17134-migrate-meta-default-range

Conversation

@os-justin

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

Copy link
Copy Markdown
Collaborator

Fixes #17134

os migrate meta --from 17 — the invocation 29 shipped tombstones prescribe — replayed an empty chain and answered ✓ Nothing to migrate, exit 0, for the very conversions that sent the author there.

What was actually wrong

--to defaulted to PROTOCOL_MAJOR, the major the runtime implements. Retirements land throughout a major's line and their ADR-0087 conversions register under the next one: @objectstack/spec@17.4.0 tombstones dashboard.refreshInterval while dashboard-refresh-interval-to-refresh-interval-seconds is toMajor: 18.

The retiredKey() house sentence names the major the source was authored against, so the tombstone is correct to say --from 17. What the template presumes is that the default terminus is at least the conversion's own toMajor — a presumption that holds only after the next major ships. For the whole 17.x line it composed 17 → 17, and composeMigrationChain keeps m > fromMajor, so no step was selected at all.

⚠️ That makes the failure worse than "lists nothing". With an empty chain, applied and todos are empty for every input, so the answer is unfalsifiable: os migrate meta --from at the installed major could not have reported anything else, for any stack, ever. The published acceptance check in skills/objectstack-upgrade was built on exactly that answer.

The repair

Both halves live in packages/cli/src/commands/migrate/meta.ts.

  1. --to defaults to the chain terminus — Math.max(PROTOCOL_MAJOR, ...MIGRATION_MAJORS), the highest major this build of @objectstack/spec carries a step for. The tombstone template's presumption now holds in every window instead of only after a major release, and it stays true one major on by construction. ⛔ This is not "migrating past what the runtime runs": every registered conversion maps a shape the installed schemas already refuse onto the one they accept, which is why the terminus is the only target for which this command's own schemaValid verdict is reachable. Math.max keeps the runtime's major as the floor for the reverse case.
  2. A range holding no step is answered as one — already canonical was a green verdict on a check that never ran. It now says so, names the range that would list the conversions, and no longer returns past the schema verdict that contradicted it. That early return was the second half of the defect: the same run reported schemaValid: false in --json while the human output claimed canonical and stopped.

packages/spec is untouched — it is the evidence here, and migrations/registry.ts is held by three open PRs.

Acceptance notes

All readings below were taken on f08b8ddf unless a different sha is named.

The tombstone count triage asked for

29 occurrences across 15 shipped source files prescribe os migrate meta --from 17:

git grep -c 'Run `os migrate meta --from 17`' -- 'packages/**/*.ts' ':!*CHANGELOG*' ':!*.test.ts'

The same command with --from 16 returns 78 across 25 files — those work today and keep working, which is what identifies --from 17 as the broken class rather than the sentence template being wrong.

Before / after, driven on the card's reproduction

Five dashboards authoring dashboard.refreshInterval, real CLI process, NO_COLOR=1. Before is the branch point 331a1a2b; after is f08b8ddf.

run before after
--from 17, no --to ✓ Nothing to migrate — the metadata is already canonical for this range. · exit 0 · 516 bytes Applied 5 mechanical change(s) then ✓ Migrated stack is schema-valid · exit 0
--from 17 --json applied: 0, todos: 0, schemaValid: false applied: 5, schemaValid: true
--from 17 --to 17 — the old default, now reachable only by typing it the same silent ✓ ⚠ No migration step exists for protocol 17 → 17, so this run replayed nothing … then ⚠ Protocol 17 → 18 has 5 mechanical and 112 manual change(s) for this stack — re-run with --to 18 … then ⚠ Stack does not pass schema validation, and this run replayed no conversion … · exit 0
--from 13 --to 14 on a canonical stack (control) ✓ Nothing to migrate ✓ Nothing to migrate — unchanged, plus the schema verdict the early return used to skip

Exit codes — read, deliberately not changed

Triage flagged the exit code as half of why this read as success. It stays 0 on every path above, and that is a decision rather than an oversight: this command reports findings instead of exiting on them — its existing schemaValid: false arm has always been a warning at exit 0 — so moving that convention is a wider contract change than this card, and the text now carries the signal. ⚠️ Flagged for the maintainer rather than decided silently; the pin asserts the 0 explicitly so a later ruling changes it deliberately.

Cost, measured rather than asserted

--from 17 now replays one hop further, so it prints that hop's semantic TODOs: the run above is 341,968 bytes (5 mechanical, 112 manual). ⚠️ Not a wall this change invents — --from 16 composes a real step today and prints 239,560 bytes (77 manual) for the same reason. The old --from 17 was 516 bytes only because it replayed nothing. The mechanical rewrite list is printed first, above the TODOs.

Reverse verification — both halves, from the committed state

Each leg mutated the committed source, proved the mutation reached disk by anchor counts plus a git hash-object differing from the HEAD blob, ran the pins, then restored with git checkout HEAD -- and proved the restored blob hash equals the HEAD blob hash with an empty git diff HEAD.

ablation result
--to default reverted to PROTOCOL_MAJOR 3 failed / 3 passed — exactly the three default pins
empty-range branch selection disabled (both result.hops.length === 0 arms) 2 failed / 4 passed — exactly the two empty-range pins

The sixth test — Nothing to migrate still answers a range that HAS steps and rewrote nothing — stays green under both, which is its job: it guards against over-fixing.

Tier placement

The pin spawns the real CLI (a flag default and a printed sentence have no in-process seam) but deliberately does not carry the .e2e name, because that name selects the nightly population and a p1 pinned only there sits outside the merge queue's required set. Queue by name, integration by behaviour — the combination vitest-tiers.ts sanctions. One process per distinct invocation; the file runs in ~34s.

skills/** line budget

reading before after ceiling
skills/objectstack-upgrade/SKILL.md lines 595 600 —
skills/objectstack-upgrade/SKILL.md tokens 7,199 7,269 8,333
all SKILL.md lines in the package 6,127 6,132 —
authored bundle tokens (whole ratcheted set) 128,870 128,940 145,552

pnpm check:skills-token-ratchet exits 0. The three edits are corrections, not expansion: that skill prescribed a replay check that was vacuous before this PR (empty range, same answer for every input) and unsatisfiable after it, so it now reads applied from --json with the one-sentence reason. This is breakage the diff causes, not scope growth.

Noted, not filed

  • Two step-18 semantic entries open their replacement with "Nothing to migrate to, because …", so the command's own verdict phrase appears inside its own TODO prose. A grep for the phrase can report the verdict present on a run that never printed it — which is a second, independent reason the corrected acceptance check reads applied rather than the headline. Successor: the skill correction in this PR. Carrier: none beyond it.
  • The tombstone/registry disagreement itself — a key tombstoned in the 17.x line whose conversion registers at toMajor: 18 — is a packages/spec fact, out of this lane and on a file three open PRs hold. This PR makes the CLI correct for it rather than changing it. Successor: none needed if the terminus default is accepted.

Verification

  • dispatch-gates --ran: 75 derived families, 75 run, 0 NOT-MEASURED, 0 UNRUN — every one recorded with its exit code, all 0.
  • pnpm lint (eslint . --no-inline-config, whole repo, no narrowing): exit 0 over 6,562 files, 0 errors, 0 warnings.
  • pnpm --filter @objectstack/cli typecheck: exit 0. pnpm --filter @objectstack/spec check:generated: exit 0, 15/15 artifacts current.
  • packages/cli pins: 6 passed / 6.

Clause-②: no — no new flag, no new exported symbol; the change pulls a prescribed invocation back to the contract its own tombstones declare.

Authored by Claude Code, session session_01DapQyvYrFb1MxSYe7BL2nt.

维护者速读(草稿)

改了什么 — os migrate meta 的 --to 默认值,从「运行时自己的 protocol major」改为「本次构建的迁移注册表里最高的那个 major」。外加:一个不含任何迁移步骤的区间,不再借用成功文案,并且不再跳过与它相矛盾的 schema 判定行。

为什么改 — 墓碑里写的 os migrate meta --from 17 是 29 处已发布提示语共同指定的命令。它组合出的区间 17 → 17 一个步骤都选不中,于是对任何输入都回答「没有要迁移的」并以 0 退出。作者照着平台自己的处方走,得到的是一个永远不会失败的检查——已发布技能里的验收步骤正是建立在这个答案上的。

风险与代价(含回滚) — 跨 major 的运行会多跑一跳,输出体积 516 字节涨到 341,968 字节;但这正是 --from 16 今天已有的形态(239,560 字节),不是本次新造的噪音,机械改写清单仍在最上方。退出码一律不变。回滚 = 还原 meta.ts 的一处默认值与一个分支,无数据迁移、无 schema 改动、无新增公开面。

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

你要做的 — 本 PR 触及受管面 skills/**,按 Prime Directive #14 只能由维护者手动合入。请确认两点:① 终点默认值改为「注册表最高 major」这一裁决;② 退出码保持 0 是否接受(triage 曾点名它,本 PR 明确不改并把它写进了 pin)。


Generated by Claude Code

…nswer an empty range as an empty range

`--to` defaulted to `PROTOCOL_MAJOR`, but retirements land throughout a
major's line: `@objectstack/spec@17.4.0` tombstones keys whose ADR-0087
conversion is registered `toMajor: 18`. The tombstone sentence names the
major the source was AUTHORED against (`--from 17`), so the prescribed
invocation composed `17 → 17` — a range `composeMigrationChain` selects no
step for — and reported `Nothing to migrate` for the very conversions that
sent the author there. 29 shipped tombstones across 15 files prescribe it.

The terminus is now read off `MIGRATION_MAJORS`, floored at `PROTOCOL_MAJOR`,
so the tombstone template's presumption holds in every window rather than
only after a major release.

Second half: an empty range no longer borrows the success answer. A range
holding no step replayed nothing, so `already canonical` was a green verdict
on a check that never ran; it now says so, names the range that would list
the conversions, and no longer returns past the schema verdict that
contradicted it.

Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
Co-authored-by: Claude <noreply@anthropic.com>
…; correct the upgrade skill's replay check

The new spawned suite drives the card's reproduction — five sites authoring a
tombstoned key — through the real CLI, and derives every expectation from
`MIGRATION_MAJORS` / `PROTOCOL_MAJOR` so no major is written down.

`skills/objectstack-upgrade` prescribed `os migrate meta --from <target>` and
"must say Nothing to migrate" as its acceptance check. With the terminus
default that check is unsatisfiable — and it was vacuous before it, since the
range it composed selected no step and answered the same way for every input.
It now reads `applied` from `--json`, with the reason stated once.

Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 10, 2026
@github-actions

github-actions Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 4 documentable anchor(s).

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/automation/hook-bodies.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/data-modeling/fields.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/data-modeling/objects.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/data-modeling/queries.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/deployment/cli.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/deployment/index.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/protocol/objectql/query-syntax.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/protocol/objectui/actions.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/ui/apps.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/upgrading.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))

⛔ 5 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/releases/v17/17-0.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/releases/v17/17-1.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/releases/v17/17-3.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))
  • content/docs/releases/v17/17-4.mdx (via os migrate meta (command, read off packages/cli/src/commands/migrate/meta.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e758131b3900eb13260f03643e295ca6d625c42b → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6aeda441bba045d4647e72967337d1d28e5eaa79 — the merge of head f08b8ddfc56da030ab241b7daaeefc9a9a7a193a into base e758131b3900eb13260f03643e295ca6d625c42b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6aeda441bba045d4647e72967337d1d28e5eaa79 && git checkout 6aeda441bba045d4647e72967337d1d28e5eaa79
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e758131b3900eb13260f03643e295ca6d625c42b f08b8ddfc56da030ab241b7daaeefc9a9a7a193a && git checkout -B drift-repro e758131b3900eb13260f03643e295ca6d625c42b && git merge --no-ff f08b8ddfc56da030ab241b7daaeefc9a9a7a193a

node scripts/docs-audit/affected-docs.mjs --json e758131b3900eb13260f03643e295ca6d625c42b

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs e758131b3900eb13260f03643e295ca6d625c42b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…the whole verdict sentence

`*.e2e.test.ts` selects the nightly population, so a p1 pinned only there is
outside the merge queue's required set. The file keeps its spawn (a flag
default and a printed sentence have no in-process seam) and drops the name, so
it is queue by name and `integration` by behaviour — the combination
`vitest-tiers.ts` sanctions — and shares one process per distinct invocation.

The headline assertion now names the whole sentence: two step-18 semantic
entries open with "Nothing to migrate to, because …", so the bare phrase
appears in prose that is not this command's verdict.

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

Copy link
Copy Markdown
Collaborator Author

维护者速读(终稿)

domain:cli 执行 PM 席(#6024)对照自己读过的 diff 校正了 dev 的草稿并填入席位意见。⚠️ 本 PR 触及受管面 skills/objectstack-upgrade/SKILL.md,按规矩只能由你手动合入 —— ⛔ 本席未入队、未挂 auto-merge。

改了什么 — os migrate meta 的 --to 默认值,从「运行时自己的 protocol major」改为「本次构建的迁移注册表里最高的那个 major」。附带两处:一个不含任何迁移步骤的区间不再借用成功文案;那条分支里的提前 return 不再跳过与它自相矛盾的 schema 判定。受管面那 8 行改的是同一个技能里被本次修复弄假的一条验收命令。

为什么改 — 墓碑里写的 os migrate meta --from 17 是 29 处已发布提示语共同指定的命令。它组合出的区间 17 → 17 一个步骤都选不中,于是对任何输入都回答「没有要迁移的」并以 0 退出。作者照着平台自己的处方走,得到的是一个永远不会失败的检查 —— 而已发布技能里的验收步骤正是建立在这个答案上的。

⭐ 本席复核时确认了一处比卡面更重的事实:那个空区间不只是「没帮上忙」,而是不可证伪 —— composeMigrationChain 保留 m > fromMajor,所以 --from N --to N 选不中任何步骤,applied / todos 对一切输入都为空。另有一处此前无人点名:零改动分支的提前 return 会跳过 schema 判定,于是同一次运行在 --json 里报 schemaValid: false,而人类看到的输出却宣称「已是规范形态」并停下。

风险与代价(含回滚) — 跨 major 的运行会多跑一跳,输出体积从 516 字节涨到约 342KB;⚠️ 但这正是 --from 16 今天已有的形态(约 240KB),不是本次新造的噪音,机械改写清单仍在最上方。退出码一律不变。回滚 = 还原 meta.ts 的一处默认值与一个分支(受管面那 8 行随之还原),无数据迁移、无 schema 改动、无新增公开面。

席位意见 — 建议采纳。

  • ⭐ 受管面的改动是本次修复的后果,不是搭车扩写:技能里那条 --from 17 # must say "Nothing to migrate" 被本 PR 弄假了,不修就是留一条错的已发布指令。而且修法是加强而非打补丁 —— 从「grep 标题行」改成「读 --json 的 applied」。dev 还测到第二个理由:有两条 step-18 条目的 replacement 正文里就写着「Nothing to migrate to, because …」,所以按标题短语 grep 会在根本没打印过该判决的运行上误报。⇒ 旧检查在两个独立方向上都脆,现在都不脆了。
  • 预算全部在线内并已申报:文件 595 → 600 行、7199 → 7269 token(上限 8333);整包 128870 → 128940(上限 145552);check:skills-token-ratchet 退出 0。
  • 证据是驱动出来的,不是断言:卡面复现跑了前后两次,并且保留了成功文案(--from 13 --to 14 在规范栈上仍答「Nothing to migrate」)且有专门测试防止改过头;消融两半各自只红掉对应的 pin。
  • ⛔ 一处必须讲明的限制:受管面 PR 的复核席按规矩应跑在契约复审档位,本席不在那个档位(2026-09-10T03:12Z 裁决把它保留给 skills 席、spec 席的条款②复核与总监席)。⇒ 上面是默认档复核,不是规矩要求的那次达档复核。本席已另立卡记录这条规则缺口,⛔ 未含糊带过。

你要做的 — 确认一件事即可:**终点默认值改为「注册表最高 major」**这个方向对不对。

⚠️ 附带一个可选的第二问(⛔ 不阻塞本 PR):分诊曾点名「退出码 0」是缺陷的一半。本 PR 不改它并说明了理由 —— 该命令的 schema-invalid 分支一向以 0 警告,改它会挪动整个 migrate 家族的约定。这个 0 已被写进 pin,所以你若裁反方向,是一行代码加一条断言的事。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 11, 2026 04:41
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit fb39b38 Sep 11, 2026
47 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-17134-migrate-meta-default-range branch September 11, 2026 05:08
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…eview to the skills seat (objectstack-ai#17806)

Fixes objectstack-ai#17490

## What changed

One line of `.claude/skills/pm-dispatch/SKILL.md` (the governed ACCEPT
fork, rule ①) is replaced in place. Nothing else moves: same line count,
the four-axis frame block untouched, no reference twin, the tiering
ruling's own text untouched.

| | line 610 | bytes (no newline) |
|:--|:--|--:|
| before (`a77b4e92e`, identical on the dispatch tip `6682c5d8`) | `- ①
复核结论照常写在 issue 上;技能面 PR 的复核席须跑在契约复审档位。` | 96 |
| graded candidate (comment 5624178855, re-measured on the base) | `- ①
复核结论照常写在 issue 上;技能面 PR 须由契约复审档的席复核,档外席交 skills 席。` | 114 |
| **after (this PR)** | `- ① 复核结论照常写在 issue 上;技能面 hunk 须由契约复审档的席复核,档外席先交
skills 席。` | **119** |

Line cap is 120 bytes (`scripts/pm/check-skill-line-ratchet.mjs`, "Why
120 bytes"); 119 is under it. File stays 812 lines, ceiling 812,
headroom 0 — no line added, no rule deleted, no re-wrap, no density
payment.

### The three ruled elements, mapped to the words

The ruled direction is option 3 on the card (5624178855): the seat that
cannot run the tier hands the `skills/**` hunk's review to the skills
seat, which records it on the card before the four-piece terminal fires;
the lane's own review of the code half stands.

| element | words |
|:--|:--|
| (a) the conclusion still goes on the issue | `复核结论照常写在 issue 上` —
unchanged from the old line |
| (b) the review of the `skills/**` hunk runs at the contract-review
tier | `技能面 hunk 须由契约复审档的席复核` — the skills-face hunk must be reviewed by
a seat AT the contract-review tier |
| (c) a seat outside that tier hands that review to the skills seat |
`档外席先交 skills 席` — an out-of-tier seat hands it to the skills seat first
|

Two wording changes against the graded 114 B candidate, both inside the
ruled direction and inside the cap:

- `PR` → `hunk` (+2 B). The ruling hands over the **hunk's** review and
keeps the lane's review of the code half; with `PR` the sentence read as
handing the whole PR review across seats, which contradicts that second
clause. `hunk` is already vocabulary in this skill
(`references/dispatch-runbook.md` lines 101 and 151). For a PR that is
entirely `skills/**`, hunk = PR, so nothing narrows.
- `交` → `先交` (+3 B). The ruling says the handed-off review is recorded
on the card **before the four-piece fires**; `先` carries that ordering,
closing the failure mode where a seat fires the four-piece and hands off
afterwards, leaving the PR at the maintainer with no at-tier read.

Kept from the candidate: `技能面` (the term the old line and line 600's `触
skills/** 的 PR` pairing already use), `契约复审档`, `skills 席`.

### core-rules twin check

`git grep -n '契约复审档位' origin/main --
.claude/skills/pm-dispatch/references/core-rules.md` ⇒ exit 1 (no hit).
A wider probe `git grep -n '复核席\|技能面 PR' origin/main --
.claude/skills/pm-dispatch/references/core-rules.md` ⇒ exit 1 (no hit).
The only tier sentence in core-rules (line 112, `语义面卡恒契约复审档施工,契约卡达档复核归
spec 席`) is about the build tier and the spec seat's clause-② review,
not the governed fork's rule ① — not a twin. `references/core-rules.md`
is untouched.

The phrase `契约复审档位` also appears in `references/contract-review.md` line
57 (`产出裁决的每轮都须读到契约复审档位`) — that is the contract-review reading's own
tier requirement, a different rule; untouched.

### Premise check (all three PM readings held)

1. Line 610 on `6682c5d8` and on the actual base `a77b4e92e`
(origin/main moved by one commit between dispatch and branch cut — PR
objectstack-ai#17794, which touches only `references/dispatch-runbook.md`; `git diff
--stat 6682c5d HEAD -- SKILL.md` is empty) is the 96 B sentence above,
97 B with its newline. Ratchet verdict on the base: `SKILL.md is 812
lines (ceiling 812; headroom 0)`. No open PR touches SKILL.md or
core-rules.md: the 30 most recently updated open PRs (newest at 06:19Z,
i.e. none after the PM's 06:29Z reading) were listed via REST; the two
pm/ci PRs in that list (objectstack-ai#17799, objectstack-ai#17801) were file-listed and touch
`scripts/pm/check-clause2-carriers.mjs` and workflow/script files only.
2. The interim was already executed at tier by the previous skills seat
on card 17134 (PR objectstack-ai#17462); that card was read only, not touched.
3. `node scripts/pm/dispatch-gates.mjs --tier
.claude/skills/pm-dispatch/SKILL.md` prints `Model tier — MANDATORY:
claude-fable-5-1`. This PR was authored at that tier (claude-fable-5-1).

### Governed verdict

`node scripts/pm/check-governed-merges.mjs --test
.claude/skills/pm-dispatch/SKILL.md` ⇒ exit 3: `governed-surface
predicate: 1 of 1 path(s) hit the register (5 surfaces, repo-agnostic).`
/ `⛔ GOVERNED — a human merge is the review record for this PR`. ⇒ draft
at the human terminal; no ready flip, no queue, no auto-merge from any
seat. `skip-changeset` applied additively (`.claude/**` publishes
nothing).

## Verification (final commit `8b7dae6c0`)

Gate set derived, not recalled: `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` (change set derived
three-dot from the merge base `a77b4e92e`, 1 path) ⇒ 16 commands. All 16
run on `8b7dae6c0` with exit captured before any pipe (`cmd > log 2>&1;
ex=$?`), all exit 0; reconciliation `node scripts/pm/dispatch-gates.mjs
--repo objectstack-ai/objectstack --ran FILE` ⇒ `Run reconciliation — 16
derived, 16 run, 0 NOT-MEASURED, 0 UNRUN.` The same 16 ran once before
the commit on the identical bytes; the command list was byte-identical
pre/post commit.

Verdict lines as printed by the gates on `8b7dae6c0`:

- `✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/SKILL.md is
812 lines (ceiling 812; headroom 0).`
- `✓ check-skill-id-lint: 27 file(s) clean (pattern /#[0-9]{3,}/g).`
- `✓ check-skill-frame-sync: the one declared copy of the decision frame
is internally coherent (.claude/skills/pm-dispatch/SKILL.md; no second
copy to compare — …)` — plus the direct proof the block did not move:
`sed -n 734,755p SKILL.md | md5sum` ⇒ `3327d02c56f8a0eca88569dad2270f32`
before and after the edit (the dispatch's expected hash).
- `✓ check-governed-prose: 2 instruction surface(s) name all 5
registered governed surfaces (…) and claim no others.`
- `check-nul-bytes: OK (scanned 8456 text file(s) … no raw ASCII control
bytes).` — plus the manual control-byte self-scan on SKILL.md (`grep
-naP` over the C0/DEL range) ⇒ no hits.
- `pnpm check:pm-governed-merges`, `check-governed-queue-guard
--self-test`, `check-closing-keyword-parity` (+ `--self-test`),
`check-comment-mask-corpus`, `check:agent-test-spelling`,
`check:doc-authoring`, `check:driver-memory-census`,
`check:refd-timer-probe`, `check:watch-hint-literal` — each printed its
own ✓/OK line, exit 0.
- `pnpm --filter @objectstack/lint run check:doc-formula-expressions`:
first run exit 3 `PREREQUISITE NOT MET` (`@objectstack/formula` /
`@objectstack/lint` not built — NOT MEASURED, not a finding). Built both
under the shared verify lock (`os-verify-lock.sh`, `VERDICT command-exit
0`, held 182 s), re-run ⇒ `✓ check:doc-formula-expressions: 22
record-scoped formula example(s) across 438 files / 1374 TS blocks
judged clean`, exit 0; included in the post-commit union above.

On-disk proof of the edit: `grep -cF` of the old line 1 → 0 and of the
new line 0 → 1; `git show HEAD:…SKILL.md | sed -n 610p` prints the new
line. No repo-wide `pnpm lint` run locally (CI-owned); no package
build/test owed — the diff touches no package (no ① closure, no ②).

Not measured locally: CI convergence (owned by the review side).

## Acceptance notes

- noted, not filed: `references/contract-review.md` line 57 uses the
spelling `契约复审档位` for the contract-review reading's own tier, while
SKILL.md line 610 now spells the same tier `契约复审档` (as lines 505–527
do). Both spellings already coexisted on the tip; no gate reads either.
Carrier: whoever next edits contract-review.md; none queued.
- Out of scope, untouched by design: the tiering ruling's text (card
17285 / PR 17294 — those cards remain as they are), card 17134's labels,
the four-axis block (lines 734–755).

## 维护者速读(草稿)

**改了什么**:`.claude/skills/pm-dispatch/SKILL.md` 第 610 行(治理面 ACCEPT
分叉的规则①)原地换了一句话,96 字节变 119 字节,行数 812 不变。旧句要求「技能面 PR
的复核席须跑在契约复审档位」;新句改为「技能面 hunk 须由契约复审档的席复核,档外席先交 skills 席」。

**为什么改**:旧规则①与分档裁决(契约复审档只留给 skills 席、spec 席的条款②复核与维护者召集的
director)构成一对谁都满足不了的矛盾:一个 `domain:cli` 席复核一张顺手修了 `skills/**` 里一行的
PR,被要求跑一个它被禁止跑的档位。这不是边角:「已发布的说法本轮变假就地修、不立卡」这条规则天然会让普通车道的 PR 顺带碰到
`skills/**`。一张 p1(PR objectstack-ai#17462)已经撞上。方向由 skills 席自分诊定为选项
3(5624178855,轮次报告否决窗已过):档外席把 `skills/**` 那一块的复核交给 skills 席,skills
席在四件套之前记到卡上;代码那一半仍由本车道自己复核。

**风险与代价(含回滚)**:每次顺带的 skills 修补多一次跨席交接 —— 几行
hunk、一条评论,不是一张卡。文字层面只动这一行,分档裁决原文、四轴框架块、objectstack-ai#17134
的标签都没碰;`references/core-rules.md` 无孪生句(已 grep 核实),未动。回滚 = revert 这一个
commit,不牵连任何生成物。

**席位意见**:(留空,席位定稿时填)

**你要做的**:读第 610 行这一句,认可则人工直合;不认可则在此 PR 评论里写下你要的措辞,由 skills 席改。

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

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/m tests tooling

Projects

None yet

3 participants