Skip to content

Commit 6e66dd4

Browse files
committed
docs(pm-dispatch): contract review r0's three should-fixes for the review seats
S1: SKILL.md's lane table names the review seat beside security on the same row (333 bytes, within the 341-byte row pin; 319/319 lines) and points at lanes/review.md, so the charter has an entry point from the skill itself. S2: check-owed-reviews reads the PR's own accounts (the body's attribution login and the signer of each Claim: comment on the card) and bars a review seat seated on either; with every seated review seat barred the lane's own seat takes at once (state `barred`, named in the row), never a further round. Four new self-test pins (82 cases). S3: the mailbox-open comment above SEAT_LANE_KINDS says "The same four words". Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzhXV3ABCbTkU1iRRn7
1 parent 911fc60 commit 6e66dd4

3 files changed

Lines changed: 61 additions & 13 deletions

File tree

‎.claude/skills/pm-dispatch/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
162162
| `domain:spec` | `packages/spec` 整包:schema 形状、`contracts/**`、退役行为半边、strictness 台账;describe/JSDoc/墓碑散文/错误 guidance 与 alias 表;`packages/spec/scripts/**`、`packages/spec/docs/**` 及按锚定规则的例外归本域的工具链(域边界枚举与席内分派见 `references/lanes/spec.md`) |
163163
| `domain:ui` | `ui/**` 全部代码:`@object-ui/*` 库与 `ui/apps/*`、`ui/packages/types` 与 schema 语料、`ui/scripts` 与门禁工具、文档随所记录的面走;`packages/console`(dist 由 `pnpm console:build` 生成 ⛔ 不手改);协议恒归 `domain:spec`,受管面归 `domain:skills`;详见 `references/lanes/ui.md` |
164164
| `domain:cloud` | 涉 cloud 即整卡归 cloud PM(`repo:cloud` 席,同挂 `repo:cloud`):读或改 cloud 代码、生产方、事实或带 cloud 半边;本仓车道 ⛔ 不认领;cloud PM 跟到关卡,余下纯本仓活由它写明 cloud 事实后另立卡;判据见 `references/cross-repo-coordination.md` |
165-
| `security`(车道,无 `domain:*` 标签) | 安全发现(无文件面;修复经无卡派发令落在归属包);席位 `security#N`、接收、台账、派修与复核见 `references/lanes/security.md`;永不公开的规则只住 `references/security-findings.md` |
165+
| `security` · `review`(车道,无 `domain:*` 标签) | 安全发现(无文件面;修复经无卡派发令落在归属包);席位 `security#N`、接收、台账、派修与复核见 `references/lanes/security.md`;永不公开的规则只住 `references/security-findings.md`。复核席 `review#N` 见 `references/lanes/review.md` |
166166
| (无固定归属,按落点分诊) | `packages/apps/*`、`examples/*`(归它演练的子系统)、`docs/audits/**` |
167167

168168
- 表未覆盖的包首次分诊时归类并走 PR 更新本表;新增或退役 `domain:*` 必须同批改本表。

‎scripts/pm/check-owed-reviews.mjs‎

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@
6060
* lane seat the lane of the PR's card (`domain:*`) against the open seat
6161
* posts: `seated`, `stood-down`, `absent`, `unknown`.
6262
* review seat the review seats (`review#N`) against the same posts: `seated`
63-
* (and which), `stood-down`, `absent`, `unknown` (roster unread).
63+
* (and which), `barred` (every seated one sits on this PR's own
64+
* implementing or dispatching account — `lanes/review.md` 独立性;
65+
* a barred seat is never named), `stood-down`, `absent`,
66+
* `unknown` (roster unread).
6467
* status `TAKE` (owed, nobody on it, a trigger holds), `WAIT` (owed,
6568
* nobody on it, no trigger yet — the lane's own, a lane's live hold
6669
* included), `HELD` (a live claim on this head), `DONE` (a record on
@@ -75,7 +78,8 @@
7578
* taker on a TAKE row, who takes it: the seat the hand-off names; else
7679
* a seated review seat FIRST (named), the lane seat joining only
7780
* once the head has waited a further round past (b); with no
78-
* review seat seated, the lane seat; with the lane seat gone (c),
81+
* review seat seated, or every one barred, the lane seat at
82+
* once; with the lane seat gone (c),
7983
* a review seat or the lane's next seat; with the roster unread,
8084
* either is named. ⛔ Never triage.
8185
*
@@ -92,7 +96,7 @@
9296
import process from 'node:process';
9397

9498
import { isEntrypoint } from '../invoked-as.mjs';
95-
import { CONTRACT_REVIEW_HEADING_MARKER, latestMarkedComment } from './check-half-states.mjs';
99+
import { CLAIM_COMMENT_MARKER, CONTRACT_REVIEW_HEADING_MARKER, latestMarkedComment, markerMatches, readSignatureLine } from './check-half-states.mjs';
96100
import { repoIdForSlug } from './check-governed-merges.mjs';
97101
import { readClause2Line } from './clause2-line.mjs';
98102
import { parseSeat } from './fleet-roster.mjs';
@@ -249,12 +253,43 @@ export function laneSeatOf(cardLabels, seats) {
249253
* The review seats (`lanes/review.md`: lane `review`, seats `review#N`) on the open seat posts. Pure.
250254
* `seats` null means the roster could not be read. `seated` names every seated one, canonically.
251255
*/
252-
export function reviewSeatsOf(seats) {
253-
if (!Array.isArray(seats)) return { state: 'unknown', seated: [] };
256+
export function reviewSeatsOf(seats, ownAccounts = new Set()) {
257+
if (!Array.isArray(seats)) return { state: 'unknown', seated: [], barred: [] };
254258
const rows = seats.filter((s) => s?.lane === REVIEW_LANE);
255-
const seated = rows.filter((s) => s.status === 'seated').map((s) => `${REVIEW_LANE}#${Number(s.seat) > 0 ? Number(s.seat) : 1}`).sort();
256-
if (seated.length > 0) return { state: 'seated', seated };
257-
return { state: rows.length > 0 ? 'stood-down' : 'absent', seated: [] };
259+
const name = (s) => `${REVIEW_LANE}#${Number(s.seat) > 0 ? Number(s.seat) : 1}`;
260+
const seatedRows = rows.filter((s) => s.status === 'seated');
261+
// `lanes/review.md` 独立性: a review seat never reviews a PR its own account dispatched or implemented.
262+
const isBarred = (s) => typeof s.account === 'string' && ownAccounts.has(s.account.toLowerCase());
263+
const seated = seatedRows.filter((s) => !isBarred(s)).map(name).sort();
264+
const barred = seatedRows.filter(isBarred).map(name).sort();
265+
if (seated.length > 0) return { state: 'seated', seated, barred };
266+
if (barred.length > 0) return { state: 'barred', seated: [], barred };
267+
return { state: rows.length > 0 ? 'stood-down' : 'absent', seated: [], barred: [] };
268+
}
269+
270+
/**
271+
* The accounts no review seat of which may take this PR (`lanes/review.md` 独立性): the IMPLEMENTING
272+
* account — the login in the body's `claude-projects-attribution` line — and the DISPATCHING account —
273+
* the login signing each `Claim:` comment on the delivered card. Lowercased logins; an unreadable or
274+
* absent line names no account. Pure.
275+
*/
276+
export function ownAccountsOf(pull) {
277+
const out = new Set();
278+
const m = /claude-projects-attribution:\s*(\{[^\n]*?\})/.exec(String(pull?.body ?? ''));
279+
if (m) {
280+
try {
281+
const login = JSON.parse(m[1])?.github_login;
282+
if (typeof login === 'string' && login !== '') out.add(login.toLowerCase());
283+
} catch {
284+
// an unreadable attribution object names no account
285+
}
286+
}
287+
for (const c of Array.isArray(pull?.cardComments) ? pull.cardComments : []) {
288+
if (!markerMatches(CLAIM_COMMENT_MARKER, c?.body)) continue;
289+
const account = readSignatureLine(c?.body)?.account;
290+
if (typeof account === 'string' && account !== '') out.add(account.toLowerCase());
291+
}
292+
return out;
258293
}
259294

260295
/**
@@ -278,7 +313,7 @@ export function owedReviewRow(pull, { now, seats, repo = DEFAULT_REPO }) {
278313
const claim = contractReviewClaims(pull.comments, head, { now });
279314
const hold = contractReviewHold(pull.comments, head, { now });
280315
const lane = laneSeatOf(pull.cardLabels, seats);
281-
const review = reviewSeatsOf(seats);
316+
const review = reviewSeatsOf(seats, ownAccountsOf(pull));
282317
const triggers = [];
283318
if (handoff.state === 'handed') triggers.push('(a) hand-off');
284319
if (owed.limbs.includes('label')) triggers.push('(a) label');
@@ -320,6 +355,7 @@ export function owedReviewRow(pull, { now, seats, repo = DEFAULT_REPO }) {
320355
laneSeat: lane.seat,
321356
reviewSeat: review.state,
322357
reviewSeats: review.seated,
358+
reviewBarred: review.barred,
323359
status,
324360
trigger: status === 'TAKE' || status === 'WAIT' ? (triggers[0] ?? 'none yet') : null,
325361
taker: takerOf(status, handoff, lane, review, waited),
@@ -347,6 +383,10 @@ export function takerOf(status, handoff, lane, review = { state: 'unknown', seat
347383
return `${named} first`;
348384
}
349385
if (review?.state === 'unknown') return laneGone ? `a review seat, or ${seat} next on duty` : `${seat} or a review seat`;
386+
if (review?.state === 'barred') {
387+
const why = `review seat ${review.barred.join(', ')} is on this PR's own account`;
388+
return laneGone ? `${seat} next on duty (${why})` : `${seat} (${why})`;
389+
}
350390
return laneGone ? `${seat} next on duty (no review seat seated)` : `${seat} (no review seat seated)`;
351391
}
352392

@@ -380,7 +420,7 @@ export function renderRow(row) {
380420
`claim ${claim}`,
381421
`hold ${holdText(row.hold)}`,
382422
`lane ${row.lane ?? '?'} ${row.laneSeat}`,
383-
`review seat ${row.reviewSeat}${row.reviewSeats.length > 0 ? ` ${row.reviewSeats.join(',')}` : ''}`,
423+
`review seat ${row.reviewSeat}${row.reviewSeats.length > 0 ? ` ${row.reviewSeats.join(',')}` : ''}${row.reviewBarred.length > 0 ? ` (barred ${row.reviewBarred.join(',')})` : ''}`,
384424
];
385425
const tail = row.trigger === null ? '' : ` → ${row.trigger}${row.taker ? ` · take: ${row.taker}` : ''}`;
386426
const listed = row.hold.listed ? ` ⏳ HOLD ${holdAge(row.hold)} — list it in the triage round record` : '';
@@ -533,7 +573,8 @@ const SELF_TEST_BATTERIES = Object.freeze({
533573
'the owed face: paths, the Clause-② line, the label — drafts included, nothing else owes': 9,
534574
'the record: on this head (by whom, as stated), on an earlier head, none, unjudged': 8,
535575
'the triggers: (a) hand-off or label the same round, (b) a full round, (c) the lane seat': 10,
536-
'review-seat first: a seated review seat takes before the lane, the lane joins after a further round, and takes alone while none is seated': 10,
576+
// 10 → 14: S2 of PR #29967's contract review — a review seat on the PR's own account is barred (`lanes/review.md` 独立性).
577+
'review-seat first: a seated review seat takes before the lane, the lane joins after a further round, and takes alone while none is seated': 14,
537578
'a live claim holds the head and a record settles it; a lapsed claim holds nothing': 6,
538579
'the live instance: two Tier H drafts with a hand-off on the current head list as TAKE under (a)': 4,
539580
'the live read: every read through one reader; a failed read is INCOMPLETE, never clean': 6,
@@ -645,6 +686,13 @@ export function selfTest() {
645686
t('the roster unread: either is named, as before, and the row says unknown', (() => { const r = rv({ headCommittedAt: '2026-10-28T11:59:00Z' }, null); return r.reviewSeat === 'unknown' && r.taker === 'the domain:ui seat or a review seat'; })());
646687
t('a hand-off names its seat whatever the roster says — a sibling lane seat under a seated review seat included', rv({ headCommittedAt: fresh, comments: [handoff(HEAD, '2026-10-28T12:40:00Z', 'domain:ui#2')] }).taker === 'domain:ui#2' && rv({ headCommittedAt: fresh, comments: [handoff(HEAD, '2026-10-28T12:40:00Z')] }, SEATED).taker === 'review#1');
647688
t('⭐ a live claim by a review seat ⇒ HELD on the review side, no taker, nobody else starts', (() => { const r = rv({ labels: [{ name: WAITING_LABEL }], comments: [claimRow(REVIEWER, HEAD, '2026-10-28T12:18:56Z', `review#1 · os-zhuang · ${REVIEWER}`)] }); return r.status === 'HELD' && r.claim.holder.side === 'review' && r.taker === null && renderRow(r).includes('(review,'); })());
689+
const ATTR = '<!-- claude-projects-attribution: {"github_login":"OS-Support-AI"} -->\n_Requested by **Y**_\n\nPart of #25922\n\nClause-②: no\nMetadata-protocol: none';
690+
const CLAIMED = [{ id: 7, created_at: '2026-10-28T09:00:00Z', body: 'domain:skills#3 · os-zhuang · session_01Abcdefgh\n\nClaim: PM loop round 1 · 2026-10-28T09:00Z\nBranch: `claude/x`' }];
691+
const OWN = [...SEATED, { lane: REVIEW_LANE, seat: 1, status: 'seated', account: 'os-support-ai' }];
692+
t('⭐ own accounts: the body\'s attribution login (lowercased) and the signer of each Claim: comment on the card; an unsigned or non-claim card comment, an unreadable attribution and no body name none', [...ownAccountsOf({ body: ATTR, card: 25922, cardComments: [...CLAIMED, { body: 'os-dev-report\n{}' }, { body: 'domain:ui#1 · os-ui · session_01Abcdefgh\n\nnote' }] })].sort().join() === 'os-support-ai,os-zhuang' && ownAccountsOf({ body: '<!-- claude-projects-attribution: {"github_login":} -->' }).size === 0 && ownAccountsOf({}).size === 0);
693+
t('⭐ S2: the only seated review seat sits on the IMPLEMENTING account ⇒ barred, the lane\'s own seat takes at once — no further round, no triage', (() => { const r = rv({ body: ATTR, headCommittedAt: fresh, labels: [{ name: WAITING_LABEL }] }, OWN); return r.status === 'TAKE' && r.reviewSeat === 'barred' && r.reviewSeats.length === 0 && r.reviewBarred.join() === 'review#1' && r.taker === "the domain:ui seat (review seat review#1 is on this PR's own account)" && !/triage/.test(renderRow(r)) && renderRow(r).includes('review seat barred (barred review#1)'); })());
694+
t('⭐ S2: a review seat on the DISPATCHING account (the card claim\'s signer) is barred too; a review seat on a third account still takes first, the barred one named', (() => { const r = rv({ card: 25922, cardComments: CLAIMED, headCommittedAt: fresh, labels: [{ name: WAITING_LABEL }] }, [...SEATED, { lane: REVIEW_LANE, seat: 1, status: 'seated', account: 'OS-Zhuang' }, { lane: REVIEW_LANE, seat: 2, status: 'seated', account: 'os-other' }]); const alone = rv({ card: 25922, cardComments: CLAIMED, headCommittedAt: '2026-10-28T10:59:00Z' }, [...SEATED, { lane: REVIEW_LANE, seat: 1, status: 'seated', account: 'os-zhuang' }]); return r.reviewSeat === 'seated' && r.reviewSeats.join() === 'review#2' && r.reviewBarred.join() === 'review#1' && r.taker === 'a review seat (review#2) first' && alone.taker === "the domain:ui seat (review seat review#1 is on this PR's own account)"; })());
695+
t('S2: the lane seat gone and the only review seat barred ⇒ the lane\'s next seat; a review seat with no account on its post is never barred', rv({ body: ATTR, headCommittedAt: fresh }, [{ lane: REVIEW_LANE, seat: 1, status: 'seated', account: 'os-support-ai' }]).taker === "the domain:ui seat next on duty (review seat review#1 is on this PR's own account)" && rv({ body: ATTR, headCommittedAt: fresh, labels: [{ name: WAITING_LABEL }] }, [...SEATED, { lane: REVIEW_LANE, seat: 1, status: 'seated', account: null }]).taker === 'a review seat (review#1) first');
648696
t('the printed row carries the review-seat reading and the taker', renderRow(rv({ headCommittedAt: fresh, labels: [{ name: WAITING_LABEL }] })).includes(`review seat seated review#1,review#2 → (a) label · take: ${BOTH} first`) && renderRow(rv({ headCommittedAt: fresh, labels: [{ name: WAITING_LABEL }] }, SEATED)).includes('review seat absent → (a) label · take: the domain:ui seat (no review seat seated)'));
649697

650698
battery('a live claim holds the head and a record settles it; a lapsed claim holds nothing');

‎scripts/pm/mailbox-open.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ const SESSION_SHAPE = /^(?:session|cse)_[A-Za-z0-9]+$/;
166166
// A seat: its lane — `domain:<x>`, `repo:<x>`, `triage`, `director`, `security`, `review`, the six a seat post's title
167167
// names, or the epic lane `epic:#<parent>` — then `#N`; absent = seat 1. Read lower-cased. The bare lanes are a closed
168168
// set of four, so a login is never read as a seat. The lists below are the ONE spelling of those lane words: the shape is
169-
// built from them, and so is the reserved set a `domain:` lane may not be named after (`seatKey`). The same three
169+
// built from them, and so is the reserved set a `domain:` lane may not be named after (`seatKey`). The same
170170
// four words close the seat signature's lane alternation (`SIGNATURE_LANE_SOURCE`, check-half-states.mjs), which this
171171
// list cannot import (the patrol loads this file lazily, so that edge would be a cycle); fleet-roster's self-test,
172172
// which imports both, holds the two equal, `coordinator` (it signs and has no mailbox) aside.

0 commit comments

Comments
 (0)