Repository navigation
Commit a0aa8a1
pm-dispatch(contract-review): the lane reviews its own PRs, triage takes only an owed review nobody claims (drafts included); one Contract-review-claim line per head; an in-repo owed-review list (#27295)
Fixes #27062
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:契约复审归属改成「车道审自己的,分诊只接没人在审的」:欠审的 PR(含 draft)在现 head
上没有复审记录、没有有效认领,且满足 (a) 有交接行或等复核标记、(b) 等满一轮分诊约 60 分钟、(c)
车道席收班或不在总表之一,分诊才接;起审前先贴一行 `Contract-review-claim:`;欠审清单从会话临时脚本改成仓内脚本
`scripts/pm/check-owed-reviews.mjs`。
- 为什么改:维护者原话「应该车车道没有人审查的你才负责契约复审对吧」「如何避免重复审查」「所以你需要更新
skills吧」。之前分诊只认交接行、扫描又跳过 draft,两个 Tier H draft
带着交接行等了约五轮;同时没有任何东西能防两个复审者同时审同一个 head。
- 风险与回滚:只动协议文本、两个 PM 脚本与一个 package.json
脚本串,不碰产品包、门禁判定、workflow、队列守卫;认领行只是协调,⛔ 没有门禁读它。回滚 = revert 本 PR。
- 先看:① `.claude/skills/pm-dispatch/references/contract-review.md` 新增 6
行(L23–L28:触发条件、认领行形状、失效与车道优先)与 L60 改写;②
`scripts/pm/record-recognisers.mjs` 的 `readContractReviewClaim` /
`contractReviewClaimLapse` / `contractReviewClaims`(风险点:同 head 双认时谁持有);③
`scripts/pm/check-owed-reviews.mjs` 的
`owedReviewRow`(TAKE/WAIT/HELD/DONE 判定)。
- 两分钟验证:`node scripts/pm/check-owed-reviews.mjs`,预期首行
`check-owed-reviews — objectstack-ai/objectstack, …: N open PR(s) read
(M draft); K owe a contract review — …`,其后每个欠审 PR 一行,TAKE 在前,退出码 0。
- 需要你做的:无,席位落地(Tier S:席位达档契约复核 PASS 后入队)。
## What changes
The card's Wanted 1–6 as amended by the 12:23Z scope amendment (drafts
are in; an in-repo owed-review list).
- **Item 1, the lane reviews its own** — unchanged (`contract-review.md`
L20 「谁跑 = 派发席」).
- **Items 2–3, the gap-only trigger and the claim line** —
`references/contract-review.md` 〈复核归属与资格〉, six new lines after the
hand-off line: triage takes only an owed PR (draft or ready) with no
record and no live claim on the current head, and one of (a) a hand-off
line naming the current head or the waiting label, taken the same round;
(b) the head has waited one full triage round (about 60 minutes); (c)
the lane seat has stood down or is absent from the roster. Every
reviewer reads the PR first; a record or a live claim on the current
head means do not start; otherwise it posts one claim line per head
first:
```text
Contract-review-claim: SESSION · head FULL-SHA · UTC
```
(the file spells the placeholders in angle brackets; they are spelled
out here because the platform strips tag-shaped fragments). A claim
lapses when the head moves or 2 hours after posting with no record; when
a lane claim and a triage claim name one head the lane wins and triage
replies once to withdraw and discards its subagent's work. Delta
re-reviews stay with the earlier reviewer — the existing rule (now L41)
「只落发现的 commit 由同一复核者 delta 复核」 already says it for both sides, so no
line was added for it.
- **〈等复核标记〉** — L54 「只带标记无交接行者仍归本席,分诊 ⛔ 不碰…」 is deleted: the amendment
makes the label condition (a), so that sentence is now false; its second
half (a hand-off goes stale when the head moves) is carried by the new
(a) line, which counts only a hand-off naming the current head. The ⛔
line (now L60) keeps check, workflow and queue guard off the label and
adds the claim line to what they never read; 「巡查脚本」 leaves it because
the owed-review list now reads the label as one owed limb (report-only,
never a verdict).
- **Item 6, the round step** — `references/triage-duties.md` L11–L12:
each round runs `check-owed-reviews.mjs` first (open PRs including
drafts, hand-off and claim lines read), and an owed review is claimed
and taken the same round; L12 keeps the isolated at-tier subagent signed
by the seat and the quota rule. The dropped clause 「交接行读法
`record-recognisers.mjs`」 has its home in the script, which reads
hand-off lines through that file.
- **Item 5, the round record** — `references/triage-duties.md` L16 in
place: the review table row names the claimer (`认领者 · 记录 head→现
head→状态`).
- **Item 4, the recogniser** — `scripts/pm/record-recognisers.mjs`,
beside `readContractReviewHandoff`: `CONTRACT_REVIEW_CLAIM_KEY` /
`CONTRACT_REVIEW_CLAIM_TEMPLATE` / `CONTRACT_REVIEW_CLAIM_LAPSE_HOURS`;
`readContractReviewClaim(body)` (session in the signature line's session
shape, imported; the FULL 40-hex head; the UTC minute, seconds allowed;
read through `stripSignatureLine`); `contractReviewClaimLapse` (pure:
`live` / `head-moved` / `expired`, timed from the comment's
`created_at`); `contractReviewClaims(rows, head, { now })` (the holder,
lane first on a tie; a claim is triage's only when its comment is signed
`triage#N`). ⛔ No gate reads it.
- **Item 6, the owed-review list** — new
`scripts/pm/check-owed-reviews.mjs` (named as `scripts/pm/` names its
report-only readers, `check-half-states.mjs` the model): read-only,
prints one row per open PR (draft or ready) whose face owes a review —
the face from `reviewFace`'s path hits (reused, not re-implemented), the
PR body's `Clause-②:` line through `readClause2Line`, and the waiting
label; the record on the current head through `locateReviewOfRecord`
(stale records name their reviewer, the delta owner); the hand-off
through `contractReviewHandoff`; the claim through
`contractReviewClaims`; the lane seat from the card's `domain:*` label
against the open seat posts (`fleet-roster.mjs` `parseSeat`); a status
`TAKE` / `WAIT` / `HELD` / `DONE` and the trigger that holds. Exit 0 on
every read; a failed read prints `sweep INCOMPLETE`, never clean; usage
errors exit 2. `--self-test` carries a battery roster, floor and verdict
handshake.
- **Wiring (no workflow line)** — root `package.json`
`check:pm-record-recognisers` now runs `record-recognisers.mjs
--self-test && check-owed-reviews.mjs --self-test`, so the existing
`lint.yml` step runs the new self-test; `check:self-test-wired` is green
on it.
- **Companion registration** — the recogniser's claim battery pins the
template and its 2-hour lapse verbatim in `contract-review.md`, a read
of a governed file: the existing `self-test-reads` declaration (line 77,
unmoved) names both files and `GOVERNED_READ_FLOOR`
(`scripts/pm/dispatch-gates.data.mjs`) gains the row.
`dispatch-gates.self-test.mjs`'s census is unchanged (same declaration,
same line).
## Line budget
| file | before | after | ceiling |
|:--|--:|--:|--:|
| `.claude/skills/pm-dispatch/references/contract-review.md` | 55 | 60 |
60 |
| `.claude/skills/pm-dispatch/references/triage-duties.md` | 102 | 102 |
102 |
Every edited line is ≤120 bytes (`wc -c`): contract-review.md L23–L28
116, 116, 105, 117, 64 (the code line), 120, L60 113; triage-duties.md
L11 115, L12 118, L16 120. contract-review.md spends its 5 free lines
plus the one freed by deleting L54 (home above). No re-wrap was used to
buy a line.
## Tests
<details><summary>Self-tests, counts before → after</summary>
- `node scripts/pm/record-recognisers.mjs --self-test`: 276 → 297 cases
(batteries 15 → 16; the claim battery 21 cases, floor 21).
- `node scripts/pm/check-owed-reviews.mjs --self-test`: new, 49 cases in
8 batteries (floor 8), including a fixture modelled on the two Tier H
drafts that waited (a workflow-step draft with a hand-off on its current
head since 07:43Z; a narrowing draft whose first hand-off went stale on
a merge-forward and was renewed): both list as `TAKE` under `(a)
hand-off`, and read `HELD` once the triage claim is posted.
- Ablations, each restored with `blob == HEAD` and an empty `git diff
HEAD` (`scripts/ablation-replace.mjs`): the tie rule (`holder =
live[0]`) reds 「the LANE wins a tie」 (1 of 297); the lapse boundary
(`>=` → `>`) reds the lapse case (1 of 297); excluding drafts from the
owed list reds the owed-review self-test (exit 1).
</details>
<details><summary>Live run, read-only (14:39Z)</summary>
`node scripts/pm/check-owed-reviews.mjs` exit 0 in 47 s: 22 open PRs
read (16 draft); 16 owe a contract review — 3 to take, 8 waiting on the
lane, 0 claimed, 5 reviewed on this head. The narrowing draft named on
the card reads `DONE` (record on its current head, PASS, by the triage
session; hand-off `triage`; claim `lapsed`); the workflow-step draft has
since merged and is not listed.
</details>
## Acceptance notes
- The waiting label is hung by the dispatching seat at ACCEPT whenever a
review is owed and no PASS is on file (〈等复核标记〉 L58). With the amendment
it is condition (a), taken by triage the same round — so a lane seat
that will review itself posts its claim line before or with the label;
that is what keeps item 1 true. Noted, not changed: the label rule's
text is outside this card's ruled scope.
- `dispatch-runbook.md` 〈巡查常设项〉 still tells a cross-account reviewer to
post a `Claim:`-shaped comment before a review; that is a second
spelling of the review claim. Not touched here (that file is held by
another claim this wave); noted for the seat.
- The `lint.yml` step comment above `check:pm-record-recognisers` says
it runs one file's self-test; it now runs two through the package
script. A workflow comment is Tier H, so it is left as is.
- Trigger (b) times the head from the later of its commit's committer
date and the PR's opening; GitHub's API does not expose push time on the
pull row, so a force-push of an old commit can read as waited longer
than it has.
- The verdict word (PASS / FAIL) is read by the list for display only,
from the record's `VERDICT:` line; owed-ness never depends on it.
## Verification
<details><summary>Local gates at dca7881 (derived by `dispatch-gates
--commands`, reconciled with `--ran`)</summary>
`node scripts/pm/dispatch-gates.mjs --ran ran.list --repo
objectstack-ai/objectstack` → exit 0: 「50 derived famil(ies) accounted
for — 46 run, 4 NOT-MEASURED (4 DERIVED from a recorded exit 3)」. Exit 0
on all 46 run, among them `check:pm-skill-ratchet` (contract-review.md
60/60, triage-duties.md 102/102, headroom 0), `check:pm-skill-id-lint`
(104 files clean), `check:pm-record-recognisers` (297 + 49),
`check:pm-dispatch-gates` (1935 cases, fast tier),
`check:pm-half-states` (5034), `check:pm-governed-merges`,
`check:self-test-wired` (244 scripts), `check:ratchet-remedy-authority`,
`check:entry-guard`, `check:parse-guard`, `check:nul-bytes`;
`check:doc-formula-expressions` after building its closure under the
verify lock. Run by hand beyond the derivation:
`check:pm-governed-prose` exit 0, `check-governed-queue-guard.mjs
--self-test` exit 0 (401 cases). NOT MEASURED (exit 3, PREREQUISITE NOT
MET — a full `pnpm build`; the diff touches only a script string in the
root `package.json`): `check:dts-closure`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure`, `check:sourcemap-no-sources-content` — CI
runs them.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QSeKYmhU5KEj1nukYhp1Tg)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 65b4920 commit a0aa8a1
6 files changed
Lines changed: 825 additions & 11 deletions
File tree
- .claude/skills/pm-dispatch/references
- scripts/pm
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
23 | 29 | | |
24 | 30 | | |
25 | 31 | | |
| |||
51 | 57 | | |
52 | 58 | | |
53 | 59 | | |
54 | | - | |
55 | | - | |
| 60 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
12 | | - | |
| 11 | + | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
| 16 | + | |
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
84 | | - | |
| 84 | + | |
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
| |||
0 commit comments