Repository navigation
Commit 82cccaf
pm-dispatch(contract-review): narrow the at-tier review to the contract diff plus its implementing code, re-review only after must-fix or should-fix, adopt a card thread's record (#31289)
<!-- claude-projects-attribution: {"github_login":"huangyiirene"} -->
_Requested by **Irene** · [project
thread](https://claude.ai/code/project/chan_01TJXtQ1tVD26ecLDZxFS67x?thread=cmsg_01TJXtQ1tVD26ecLDZxFS67xQyzCMNZqYo7LLsSoK87H3J)_
Fixes #31179
Clause-②: no
Metadata-protocol: none
This PR changes the contract-review rules themselves: what the at-tier
review covers, when a delta review is owed, and when the seat adopts a
card thread's record instead of reviewing the same scope again.
`.claude/**` and `scripts/pm/**` are repo tooling and release nothing,
so it takes the `skip-changeset` label, with no package entry. If
`dispatch-gates` mandates a higher tier for this surface, that mandate
is waived by the maintainer's standing order in this project
(2026-11-03); this card ran on the project's default model. The
dispatching seat (domain:skills#3) runs the contract review.
## 维护者速读
- 改了什么:契约复核规则三处收窄。① 达档复核只看 `hits`(spec / skills / ADR / 受管规则文本)加上 diff
里直接实现或执行它们的代码,并审这些代码与契约是否一致;测试、无关模块、changeset 等仍由车道席位按 `TIER_DEFAULT` 在
ACCEPT 里看。② 每条发现分 must-fix / should-fix / nit 三级,只有前两级才要 delta 复审,只有 nit
的一轮即终轮。③ 卡线程的隔离子代理在现 head 落的同形记录(并贴出所用简报),席位核过简报、`Scope:`
与独立性对后直接采纳,ACCEPT 点名该记录的评论 id,只补审它没覆盖的部分;`Scope:` 窄于 `hits`
的记录不能采纳,席位的补缺记录只管缺口。
- 为什么改:田騏 2026-11-07 的指令(下方原文):开发线程与席位对同一 head 各跑一轮最高档复核、且覆盖整张 PR,token
花在测试和 changeset 上;真正值钱的是契约与实现码是否一致,而现行简报说被引代码「只读不审」,这一块反而没人负责。
- 风险与回滚:只改规则文本与 `record-recognisers.mjs` 打印的简报和模板(及其自测,368 → 375
例全过);不改队列守卫、任何门禁判定或棘轮上限;四个 `.md` 文件行数不变。收窄面两条裁决(6203713500 B 的全 diff
下限、6442046166 A 的 ④ 形状)的文字未动;收窄面简报只改了评级,与其他面统一为三级(含 nit)。回滚即 revert。
- 先看:`.claude/skills/pm-dispatch/references/contract-review.md` 的范围行、④
行、复审行和新增的采纳行;然后 `scripts/pm/record-recognisers.mjs` 的
`contractReviewBriefLines`(hits 面的 Scope 段与 ④ 段)。
- 两分钟验证:`node scripts/pm/record-recognisers.mjs --brief-template`,应看到
Scope 段写「every file IN THIS DIFF that directly implements or enforces
them」,④ 段写「grade each finding must-fix, should-fix or
nit」;`--brief-template --face narrowing` 的 ④ 形状句与 `main` 相同、评级为同一套三级。
- 需要你做的:`check-governed-merges` 判为 GOVERNED,Tier S(`.claude/**`
×4),席位达档复核 PASS 后可由席位落地。但这张 PR
改的正是复核规则本身:田騏的「同意立卡」是否足以让它落地、还是要再要一句话,由维护者决定。
## The order
田騏 (`marchtian`), 2026-11-07, project thread, verbatim and untranslated:
> 凡是改 spec、skills 或 ADR 的卡都要做一轮 Fable 全量复核,是复核所有代码吗?这个合理吗?还是只需要复核
spec、skills 或 ADR 的聂荣 (16:06:42Z)
The proposal put to the owner in reply (the project coordinator's words,
verbatim):
> 建议改成:
> - Fable 只复核 spec、skills、ADR 的改动,加上直接实现这份契约的代码。PR
里其他的代码改动(测试、无关模块、changeset 等)由 Opus 自审。
> - 修完之后的增量复核只在出现过 must-fix 或 should-fix 时才做,只有 nit 就不再复核。
> - 席位的契约复核已经由开发线程的 Fable 覆盖到的部分,就不重复看了。
The owner's answer: 「同意立卡」 (16:07:11Z), then 「通知席位并立skills卡」
(16:07:22Z).
The rule text spells the tiers `CONTRACT_REVIEW_TIER` and
`TIER_DEFAULT`; the quotes above are the only place a tier is named any
other way.
## What changes, rule line by rule line
| File · rule | Before | After |
|:--|:--|:--|
| contract-review.md · scope (item 1) | `` - 混合 diff 的达档复核范围 =
`reviewFace` 的 `hits` 路径 + 其 hunk 所断言的文件(读被引代码); `` / ``
规则与执行它的门禁同属一改者一并读;记录 `Scope:` 行列两者,可放宽、⛔ 不窄于 `hits`。 `` | `` - 混合 diff
的达档复核范围 = `reviewFace` 的 `hits` + diff 内直接实现或执行它的文件; `` / ``
后者审其合契约,diff 外被引代码只读;`Scope:` 行分列两者,可放宽、⛔ 不窄于 `hits`。 `` |
| contract-review.md · ④ line (item 1) | `被引代码读而不审;①–④ 合称完整复核,余面同形无 ④,⛔
无第二条记录。` | `实现文件审合契约、diff 外只读;①–④ 合称完整复核,余面同形无 ④,⛔ 无第二条记录。` |
| contract-review.md · re-review, old L42 (item 2) | `` - 实质 head
各欠一轮完整复核;只落发现的 commit 由同一复核者 delta 复核;无 must-fix 即止。 `` | `` - 实质 head
各欠一轮完整复核,发现逐条定 must-fix/should-fix/nit(无碍正确性与契约);有前两级 `` / `` 才由同一复核者
delta 复核修复;仅 nit 即终轮,nit 记接受的偏差、转后续卡或只改散文面。 `` |
| contract-review.md · 复核记录 (item 3) | `` - 复核记录 = 一条评论落 PR
或卡,席内与子代理同形;散文、dev 自评、`os-dev-report` 恒不算。 `` | `` - 复核记录 = 一条评论落 PR
或卡,席内与各子代理同形;散文、dev 自评、`os-dev-report` 不算。 `` |
| contract-review.md · adoption, two new lines after ④ 恒由… (item 3) |
(none) | `` - 卡线程隔离子代理照上落现 head 记录并贴简报 ⇒ 席核简报、`Scope:` 与独立性对后采纳, `` / ``
ACCEPT 点名其评论 id;`Scope:` 窄于 `hits` 不采纳;席的补缺记录只管缺口,⛔ 不单充 head 记录。 `` |
| contract-review.md · 子代理记采纳它的席位 (item 3) | unchanged | unchanged: a
card thread's sub-agent record names the dispatching seat that adopts
it, which the new adoption line makes explicit |
| SKILL.md L190 (items 3, 4) | `` - 指针 `ACCEPT → PR #n comment
⟨id⟩`;受管/spec 面欠完整复核 ①–④,席未达档起隔离达档复核。 `` | `` - 指针 `ACCEPT → PR #n
comment ⟨id⟩`;受管/spec 面欠 ①–④,席只达档补审现 head 记录未盖处。 `` |
| dispatch-runbook.md L137 (item 3) | `` -
席位留认领文本/派发令/契约复核/ACCEPT/落地;线程实施、开 draft PR、驱动 CI 与可合并。 `` | `` -
席位留认领/派发令/ACCEPT/落地/复核采纳或补缺;线程实施、开 draft PR、驱动 CI 与可合并。 `` |
| coordinator.md 〈四〉 L32 (item 3) | `` - 改技能(协议语义面)的卡恒契约复审档
`CONTRACT_REVIEW_TIER`;⛔ 不抄模型名。 `` | `` - 改协议语义面卡恒
`CONTRACT_REVIEW_TIER`,⛔ 不抄模型名;隔离子代理照 `--brief-template` 复核。 `` (117
bytes; the protocol-semantic scope is kept, as in execution-duties.md
and `dispatch-gates --tier`; the seat side, 「复核采纳或补缺」, is in
dispatch-runbook.md L137) |
(In the SKILL.md row the pointer's angle brackets are shown as ⟨⟩,
because the PR body sanitizer drops tag-shaped text.) SKILL.md carries
the order as one clause (seat ruling: one line within 120 bytes): the
seat reviews at tier only what the record on the current head does not
cover. The scope and re-review detail lives in contract-review.md. The
old line's "席未达档起隔离" condition keeps its home in
`references/execution-duties.md` L119 (「余面未达档 ⛔ 不自审,起隔离子代理」) and
contract-review.md L20-L21; an adopted record is at tier, so that line
still holds (listed under other sites below).
### Line payment in contract-review.md (deletion only)
contract-review.md gains three lines (re-review +1, adoption +2) and
pays them by deleting three lines, per the seat's rulings:
| Deleted line | Home |
|:--|:--|
| `` - ① derived judgments 逐项:diff 引出的接受集与公开面变化逐条点名判对错。 `` |
`--brief-template` prints, in "Judge, in the record's own sections": `①
Derived judgments — every accept-set and public-surface change the diff
implies, each named right or wrong.`; `--template` prints the `### ①
Derived judgments` heading. |
| `` - 同形带 `Local-runs:` 行:`none` 或 `probe — REASON`
具名唯一探针;有行必校,缺行旧记录不改判。 `` | `--template`'s Local-runs note carries all
three facts: the value (`` `none` — the read-only shape held … or `probe
— REASON`, naming the ONE local run ``), a present line is always
checked (`` a line present that reads neither is refused before the
write ``), and an old record is not re-judged (`` A record without the
line predates it and is not re-judged ``). L33-L35 still require the
record shape `--template` prints, and `readLocalRuns` is the reader. |
| `` - ② semver 定级与 changeset 声明一致;③ 边界旗:dev 挂旗与 `open_questions`
逐旗答复或升级。 `` | `--brief-template` prints `` ② Semver level — the
changeset (or `skip-changeset`) matches what the diff publishes, and the
`Clause-②:` line. `` and `` ③ Boundary flags — every dev flag and every
`open_questions` entry answered, or escalated. ``; `--template` prints
both headings. |
contract-review.md's own brief line (「简报从 `record-recognisers.mjs
--brief-template` 起稿」) makes every at-tier brief start from that
printout, and L33 keeps 「①②③逐项」 as the record shape. `node
scripts/pm/record-recognisers.mjs --brief-template` at this head prints
all three lines verbatim (checked).
Line counts (before → after, ceiling unchanged): SKILL.md 319 → 319,
contract-review.md 60 → 60, dispatch-runbook.md 245 → 245,
coordinator.md 49 → 49, `record-recognisers.mjs` 3356 → 3402 (not
ceilinged). Every edited `.md` line is at most 120 bytes.
### The brief and the template (`scripts/pm/record-recognisers.mjs`)
- `contractReviewBriefLines`, hits faces (schema, rules, bare,
`--paths`): the Scope paragraph now reads "plus every file IN THIS DIFF
that directly implements or enforces them — the gate that enforces a
rule, the code a jsdoc or schema line describes, the validator a skill
teaches — reviewed for agreement with the contract", says the record's
`Scope:` line names the implementing files apart from `hits`, and names
the remainder "(tests, unrelated modules, lockfiles, generated output,
changesets)". The ④ scope line reads "④ judges the `hits` files and
those implementing files ONLY … code outside the diff is READ to judge
them, never reviewed."
- The ④ judge line adds "For EVERY implementing file in the diff: does
the code agree with the contract, sentence by sentence?". Every ④ face
(schema, rules, narrowing) closes with the same grade lines
(`BRIEF_GRADE_LINES`): "Grade each finding must-fix, should-fix or nit —
a nit has no effect on correctness or the contract. A must-fix is a
FAIL; a must-fix or should-fix earns a delta review of its fix; nits
alone end the round. A nit is fixed only on the prose faces, or left as
an accepted deviation or a follow-up card: a fix anywhere else moves the
head, and this record no longer covers it."
- The brief's preamble (outside the copy fence) tells a card thread
handing it to its isolated sub-agent to post the filled brief beside the
record, for the seat to read before adopting.
- `--template` notes: the `Scope` note names the implementing files in
the diff and says code outside the diff is read, never reviewed; the ④
note asks the same agreement question and the three grades. The `Scope:`
record line itself (`PATH-ONE, PATH-TWO + CITED-FILE`) is unchanged, so
every reader and the queue guard read the same record shape.
- Jsdoc of `reviewScopeHits`, `RECORD_SCOPE_PLACEHOLDERS`,
`contractReviewRecordLines` and `contractReviewBriefLines` updated to
say the same.
### Self-tests
`node scripts/pm/record-recognisers.mjs --self-test`: **368 cases
before, 375 after**, all pass. Four pins were rewritten to the new words
(scope, ④ instruction, ④ scope rule, mixed path list). Seven pins were
added: the named remainder and read-only outside code (letter-C battery,
floor 18 → 19); the template notes' implementing files and grades
(template battery, floor 29 → 30); the nit grade, the implementing-file
④ line, the narrowing brief's ruled shape on the shared scale, where a
nit may be fixed on every ④ face, and the posted brief (brief battery,
floor 18 → 23). Measured counts: letter C 19/19, template 32/30, brief
25/23 (main: 18/18, 31/29, 20/18), so each battery's slack is as on
main.
## The narrowing-face rulings (item 1)
Ruling record 6203713500 letter B (the narrowing face's whole-diff
floor) and ruling record 6442046166 letter A (④ in the narrowing's own
shape) do **not** match the order: the narrowing floor is every path of
the diff outside the prose faces, wider than "`hits` plus the
implementing code", and it is reviewed over the whole diff rather than
`hits` plus implementing code. As the card says, they are maintainer
rulings and their words are left unchanged: the narrowing brief keeps
its floor paragraph and its ④ shape sentences as on `main`, with no
implementing-file rule. Neither ruling fixes how findings are graded, so
the narrowing face now grades on the same three-grade scale as every
other face (review S1): its brief closes with the shared grade lines in
place of "name each finding must-fix or should-fix", and a self-test
pins both the ruled sentences and the shared scale. In the shared
`--template` notes, the narrowing sentences keep their words; the Scope
note gains one clause saying `+` on the narrowing face still names every
file those hunks make a claim about, because the sentence that used to
say that for all faces now speaks of implementing files.
## Adoption: what the seat checks, and the accepted limit (review S2,
S3, S5)
The seat adopts a card thread's record only when it is on the current
head, the thread has posted the filled brief beside it, and the seat has
read that brief, the record's `Scope:` line and the independence pair.
The seat's ACCEPT names the adopted record's comment id. A record whose
`Scope:` is below `hits` is not adopted (L15 already forbids it). When
the seat reviews a gap, its record covers only that gap and never stands
as the head's record on its own.
Accepted limit: the seat can check that the posted brief came from
`--brief-template` (it diffs the brief against that printout and reads
the inputs list) and that it carries no drafter conclusions. It cannot
check that the brief was the reviewer's only input. That part rests on
the thread's word, the same way `Local-runs:` rests on the reviewer's
word. A seat that doubts it reviews the scope itself.
## How item 2's constraint is solved (no gate change)
The landing check wants an at-tier PASS on the current head, and the
queue guard carries a record forward only over a certified
pure-regeneration or prose-only hop. The rule now says what happens to a
nit after a final round: it is recorded as an accepted deviation in the
seat's ACCEPT (「接受的偏差」 is already one of the six ACCEPT checks), moved
to a follow-up card, or fixed only on the prose faces (`.changeset/`,
`CHANGELOG.md`, `content/docs/`, `apps/docs/`), which the existing prose
arm (`isOutsideReviewSurface`, ruling record 6159870311) already carries
forward with a `Regen-provenance:` line. A nit fixed anywhere else moves
the head like any other commit and needs a record on the new head.
`check-governed-queue-guard.mjs` was read and not edited.
## Other sites
- `references/execution-duties.md` L119 「自审随面定档:仅申报面车道席作 `TIER_DEFAULT`
记录;余面未达档 ⛔ 不自审,起隔离子代理。」 restates the old SKILL.md condition. Read with
the new adoption line it still holds: an adopted card-thread record is
at tier, so it covers that scope. Left unchanged (seat ruling).
- `references/lanes/review.md` L7 and `references/lanes/skills.md` L18
name the review tier only; unchanged.
- The project coordinator's standing instruction to card threads (the
2026-10-25 rule) lives outside the repo; the card says the coordinator
updates it from the owner's approval.
## Gates
Head `d8492ee8` = `d9049cb0` plus one commit that changes only
coordinator.md 〈四〉 (delta review M1: the protocol-semantic scope
restored). At `d8492ee8`: `node scripts/pm/record-recognisers.mjs
--self-test` exit 0 (375 cases), `node
scripts/pm/check-skill-line-ratchet.mjs` exit 0, `node
scripts/check-skills-token-ratchet.mjs` exit 0, `node
scripts/check-commit-card-trailers.mjs --range origin/main..HEAD` exit
0. The derived gate list below was run at `d9049cb0`.
`d9049cb0` is the reviewed commit `4925db94` (contract review PASS at
that head, no must-fix) plus one review delta commit for S1-S5.
`origin/main` has moved since 07559ce; none of its new commits touches
these paths, so the branch was not re-merged. `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived the same 41 commands at both heads.
Each ran under `timeout 900` with
`NODE_OPTIONS=--max-old-space-size=8192`, after `turbo run build` of
`@objectstack/formula`, `@objectstack/lint` and `@objectstack/objectql`
(exit 0), with the exit code captured before any pipe. At `d9049cb0`:
**41 exit 0** (also 41 exit 0 at `4925db94`). `node
scripts/pm/record-recognisers.mjs --self-test`: exit 0, 375 cases. `node
scripts/pm/check-skill-line-ratchet.mjs`: exit 0. `node
scripts/check-commit-card-trailers.mjs --range origin/main..HEAD`: exit
0.
```text
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test :: exit 0
node scripts/check-self-test-wired.mjs :: exit 0
node scripts/check-self-test-wired.mjs --self-test :: exit 0
node scripts/check-self-test-workflow-commands.mjs :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0
node scripts/pm/check-governed-queue-guard.mjs --self-test :: exit 0
node scripts/pm/check-harness-current.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:entry-guard :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:pm-dispatch-gates :: exit 0
pnpm check:pm-expected-skips :: exit 0
pnpm check:pm-governed-merges :: exit 0
pnpm check:pm-governed-prose :: exit 0
pnpm check:pm-record-recognisers :: exit 0
pnpm check:pm-skill-id-lint :: exit 0
pnpm check:pm-skill-ratchet :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:pnpm-flag-spellings :: exit 0
pnpm check:ratchet-remedy-authority :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:scripts-symbol-anchors :: exit 0
pnpm check:skill-frame-sync :: exit 0
pnpm check:watch-hint-literal :: exit 0
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01J8VVAmzE51TF4aEMC2PDrQ
<!-- signed: domain:skills#3 · huangyiirene ·
session_01J8VVAj7CvjkGXvgXzd6RZ7 -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 40eaae7 commit 82cccaf
5 files changed
Lines changed: 94 additions & 48 deletions
File tree
- .claude/skills/pm-dispatch
- references
- scripts/pm
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
190 | | - | |
| 190 | + | |
191 | 191 | | |
192 | 192 | | |
193 | 193 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
15 | | - | |
| 14 | + | |
| 15 | + | |
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
| 32 | + | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
37 | | - | |
38 | | - | |
39 | 36 | | |
40 | 37 | | |
41 | | - | |
42 | | - | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
43 | 41 | | |
44 | 42 | | |
45 | 43 | | |
46 | 44 | | |
| 45 | + | |
| 46 | + | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
| 32 | + | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
134 | 134 | | |
135 | 135 | | |
136 | 136 | | |
137 | | - | |
| 137 | + | |
138 | 138 | | |
139 | 139 | | |
140 | 140 | | |
| |||
0 commit comments