From 6e37cd14fa689137ae8964d515c7fb67d7c3938c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 16:49:19 +0000 Subject: [PATCH] tooling(pm): carry the review-independence pair on the verdict, and read a subagent by its branch The template half of the ruled direction A. The guard half (finding row C4 in `check-clause2-carriers.mjs`) landed alone and has been inert since: no verdict on the board carries the two lines it compares. - `references/contract-review.md` gains one bullet in the review-checklist section: the verdict declares `Implemented-by:` (the identity that produced the diff) and `Reviewed-by:` (the reviewing seat's session); the same session on both lines is a SELF-REVIEW and is not an independent review, while a verdict carrying neither line stays silent forever. - `check-clause2-carriers.mjs` admits a `mode:subagent` dev's BRANCH as a first-class `Implemented-by:` value beside a session id, because a subagent has no session of its own and a seat-session token there would name the reviewer by construction. `Reviewed-by:` stays session-only. The two grammars are disjoint, so equality still means what the ruling says: the same session. - `check-skill-line-ratchet.mjs` raises the contract-review.md ceiling 65 -> 68, the landed count, headroom 0, with both authorising rulings quoted verbatim. Self-test 138 -> 150 cases, the twelve new ones pinning both directions of the subagent reading. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox --- .../pm-dispatch/references/contract-review.md | 4 + scripts/pm/check-clause2-carriers.mjs | 110 +++++++++++++++--- scripts/pm/check-skill-line-ratchet.mjs | 33 +++++- 3 files changed, 128 insertions(+), 19 deletions(-) diff --git a/.claude/skills/pm-dispatch/references/contract-review.md b/.claude/skills/pm-dispatch/references/contract-review.md index 983dcf6170..974d1f1adb 100644 --- a/.claude/skills/pm-dispatch/references/contract-review.md +++ b/.claude/skills/pm-dispatch/references/contract-review.md @@ -36,6 +36,10 @@ 隔离的 fable 复核子代理**出第二意见,只喂卡片、既有裁决与 PR 本体,⛔ 不喂派发令与 派发席自己的结论(污染即失独立性);简报写成**对抗性**(找拒绝理由)。未达档席 ⛔ 不凭 自述复核,恒走本路径;子代理裁决按转录核验采信(下节),逐字采纳或整体作废。 +- **裁决载独立性对(机读,C4 行比对)**(2026-09-01 裁定 A、2026-09-02 裁定读法 a):产出 diff + 的身份写 `Implemented-by:` —— `mode:subagent` dev 记其分支(`claude/issue-…`,子代理无自有 + session),`mode:remote` dev 记 session id;`Reviewed-by:` = 出裁决席位的 session。两者同 + session ⇒ 报 SELF-REVIEW,⛔ 不作独立复核;两行皆无的历史裁决恒静默(⛔ 缺失永不转红)。 - **放行 = 清标即落地**(2026-08-25 裁定):PASS ⇒ 同席同笔剥标(双载体), 清标评论引 2026-08-31 裁定作 provenance,按 `landing-operations.md` 走落地前检 → 转 ready → 挂 auto-merge/入队;轮报设**复审清单**专 diff --git a/scripts/pm/check-clause2-carriers.mjs b/scripts/pm/check-clause2-carriers.mjs index da121123dc..e6d9df44ff 100644 --- a/scripts/pm/check-clause2-carriers.mjs +++ b/scripts/pm/check-clause2-carriers.mjs @@ -105,11 +105,13 @@ * comment on the card. * * ⭐ C4 is the ONE thing this file reads out of a verdict comment, and it is not - * the verdict: the `Implemented-by:` / `Reviewed-by:` session-ID pair the - * verdict declares about its own AUTHORSHIP. No PASS or FAIL token is read to - * reach it, so the boundary above is narrowed by exactly one fact and not - * crossed — a self-issued verdict is refused on WHO wrote it, never on what it - * concluded, and the row is report-only like every other one here. + * the verdict: the `Implemented-by:` / `Reviewed-by:` identity pair the verdict + * declares about its own AUTHORSHIP (a session id on both, or a `mode:subagent` + * dev's BRANCH on the left — the grammar note beside AUTHORSHIP_KEYS). No PASS + * or FAIL token is read to reach it, so the boundary above is narrowed by + * exactly one fact and not crossed — a self-issued verdict is refused on WHO + * wrote it, never on what it concluded, and the row is report-only like every + * other one here. * * ## How a COMPLETED review is told from a gate that never ran (#14155) * @@ -720,11 +722,12 @@ const VERDICT_MARKER = /^[ \t]*(?:>[ \t]*)?(?:[-*][ \t]+)?(?:\*\*)?`?VERDICT`?(? * The two lines a verdict carries about its own authorship, in the ONE spelling * that counts (maintainer 2026-09-01, verbatim and untranslated: 「同意 A」). * - * `Implemented-by:` names the session that produced the diff — read from the - * implementation claim, not invented — and `Reviewed-by:` names the session - * rendering the verdict. Case-sensitive, exactly like the `Clause-②` key one - * section up: this file has one convention for machine spellings and a second - * one would be the drift it exists against. + * `Implemented-by:` names the IDENTITY that produced the diff — read from the + * implementation claim, not invented; a `mode:remote` dev's session id, a + * `mode:subagent` dev's branch (maintainer 2026-09-02, reading a) — and + * `Reviewed-by:` names the session rendering the verdict. Case-sensitive, + * exactly like the `Clause-②` key one section up: this file has one convention + * for machine spellings and a second one would be the drift it exists against. */ export const AUTHORSHIP_KEYS = Object.freeze(['Implemented-by', 'Reviewed-by']); @@ -736,19 +739,63 @@ const AUTHORSHIP_KEY_LINES = new Map( ); /** - * The value token — a session ID, read immediately after the colon. + * The value token — an IDENTITY, read immediately after the colon. * * The same calibration `readValueToken` states for `Clause-②`: the token must be * the FIRST thing after the colon, and what a seat writes after it is their * argument, which this file does not read. Real verdicts wrap the ID in * backticks, so the same decoration is tolerated and nothing else is. + * + * ⭐ TWO grammars, and which key admits which is the whole of the 2026-09-02 + * ruling's reading a (verbatim and untranslated: 「同意」). A `mode:subagent` dev + * has no session of its own — under that backend the dev IS a subagent of the + * dispatching seat, so a seat-session token on `Implemented-by:` would name the + * REVIEWER by construction and make every subagent-dispatched card read as a + * self-review, which is the false positive this reading forecloses. Its identity + * bit is the one the claim protocol already gives it: its BRANCH, carried on the + * implementation claim's own `Branch:` line, so a reader can cross-check the + * token rather than take the verdict's word for it. `Implemented-by:` therefore + * admits a branch as a first-class value beside a session id (a `mode:remote` + * dev keeps its session), while `Reviewed-by:` admits a SESSION ONLY — a verdict + * is rendered by a seat, and a seat always has one. + * + * ⭐ The two grammars are DISJOINT, which is what keeps the comparison the + * ruling's own: `claude/…` can never equal `session_…`, so equality on the pair + * still means exactly what the ruling says it means — the SAME SESSION on both + * lines — and a branch on the left is silent without a second rule to say so. */ +const SESSION_TOKEN = /^(?:\*\*)?(?:`)?[ \t]*(session_[A-Za-z0-9]+)(?![A-Za-z0-9_])/; + +/** + * A `mode:subagent` dev's identity: the branch the claim protocol names it by. + * Anchored on the `claude/` prefix every dispatched branch carries and closed on + * an alphanumeric, so the seat's reasoning may follow the token — a trailing + * `,` or `.` belongs to the prose, never to the branch. + */ +const BRANCH_TOKEN = /^(?:\*\*)?(?:`)?[ \t]*(claude\/[A-Za-z0-9._/-]*[A-Za-z0-9])/; + function readSessionToken(raw) { + const m = SESSION_TOKEN.exec(String(raw ?? '').replace(/^[ \t]+/, '')); + return m ? m[1] : null; +} + +/** The `Implemented-by:` value — a session id, or a dev branch. */ +function readImplementerToken(raw) { const rest = String(raw ?? '').replace(/^[ \t]+/, ''); - const m = /^(?:\*\*)?(?:`)?[ \t]*(session_[A-Za-z0-9]+)(?![A-Za-z0-9_])/.exec(rest); + const m = SESSION_TOKEN.exec(rest) ?? BRANCH_TOKEN.exec(rest); return m ? m[1] : null; } +/** + * What each key admits, and what it is called when it admits nothing. Per KEY, + * because the two identities are not interchangeable: a reviewer is a seat and + * has a session, an implementer may be a subagent and has only its branch. + */ +const AUTHORSHIP_KEY_VALUES = new Map([ + ['Implemented-by', { read: readImplementerToken, noun: 'identity — neither a session id nor a `claude/…` dev branch' }], + ['Reviewed-by', { read: readSessionToken, noun: 'session ID' }], +]); + /** * The authorship pair declared by ONE comment. * @@ -773,7 +820,7 @@ export function readVerdictAuthorship(text) { for (const [key, re] of AUTHORSHIP_KEY_LINES) { if (seen.has(key)) continue; const m = re.exec(line); - if (m) seen.set(key, { token: readSessionToken(m[1]), line: quoteLine(line) }); + if (m) seen.set(key, { token: AUTHORSHIP_KEY_VALUES.get(key).read(m[1]), line: quoteLine(line) }); } } if (seen.size === 0) return { kind: 'legacy' }; @@ -783,7 +830,7 @@ export function readVerdictAuthorship(text) { if (missing.length > 0 || unreadable.length > 0) { const parts = []; for (const key of missing) parts.push(`no \`${key}:\` line`); - for (const key of unreadable) parts.push(`\`${key}:\` carries no readable session ID (${JSON.stringify(seen.get(key).line)})`); + for (const key of unreadable) parts.push(`\`${key}:\` carries no readable ${AUTHORSHIP_KEY_VALUES.get(key).noun} (${JSON.stringify(seen.get(key).line)})`); return { kind: 'malformed', detail: parts.join('; ') }; } return { @@ -836,6 +883,12 @@ export function cardVerdictAuthorship(commentRows) { return { state: 'malformed', at: governing.at, detail: governing.read.detail }; } const { implementedBy, reviewedBy } = governing.read; + // ⭐ Equality IS the same-session test, and needs no second rule to be one: + // the two identity grammars are disjoint, so a `mode:subagent` dev's branch on + // the left can never equal the reviewing seat's session on the right. That is + // reading a of the 2026-09-02 ruling, mechanized — a subagent-dispatched card + // reads INDEPENDENT, and only a seat that coded in-session and passed its own + // diff reads self-review. if (implementedBy !== reviewedBy) return { state: 'independent', implementedBy, reviewedBy }; return { state: 'self-review', session: implementedBy, at: governing.at, detail: governing.read.line }; } @@ -863,9 +916,11 @@ export function c4VerdictSelfReview(pair) { const head = `card #${pair?.card} (delivering open PR #${pair?.pr}${pair?.draft ? ' (draft)' : ''})`; const fixed = - 'the fixed spelling is `Implemented-by: session_…` (the session that produced the diff, read ' + - 'from the implementation claim) and `Reviewed-by: session_…` (the session rendering this ' + - 'verdict), each token immediately after its colon, the seat\'s reasoning free to follow it'; + 'the fixed spelling names the identity that produced the diff, read from the implementation ' + + 'claim — `Implemented-by: session_…` for a `mode:remote` dev, `Implemented-by: claude/…` (its ' + + 'BRANCH) for a `mode:subagent` dev, which has no session of its own — and `Reviewed-by: ' + + 'session_…` for the session rendering this verdict, which is a seat and always has one; each ' + + 'token immediately after its colon, the seat\'s reasoning free to follow it'; const legacyIsSilent = '⚠️ A verdict carrying NEITHER line is a LEGACY verdict and is silent here — it predates the ' + 'carrier and ⛔ is never turned red by its absence.'; @@ -1448,7 +1503,8 @@ export function selfTest() { t('ONE line alone is MALFORMED — half a pair compares to nothing', readVerdictAuthorship(VERDICT([`Reviewed-by: \`${REVIEW_SESSION}\``]).body)?.kind === 'malformed'); t('…and the row can say WHICH line is missing', says(readVerdictAuthorship(VERDICT([`Reviewed-by: \`${REVIEW_SESSION}\``]).body)?.detail, 'Implemented-by')); t('a key with no readable session ID is MALFORMED, not a reading', readVerdictAuthorship(VERDICT(['Implemented-by: the dispatching seat', `Reviewed-by: \`${REVIEW_SESSION}\``]).body)?.kind === 'malformed'); - t('…and says so rather than blaming the absent line', says(readVerdictAuthorship(VERDICT(['Implemented-by: the dispatching seat', `Reviewed-by: \`${REVIEW_SESSION}\``]).body)?.detail, 'no readable session ID')); + t('…and says so rather than blaming the absent line', says(readVerdictAuthorship(VERDICT(['Implemented-by: the dispatching seat', `Reviewed-by: \`${REVIEW_SESSION}\``]).body)?.detail, 'no readable identity')); + t('…naming BOTH identities that key admits, so the remedy is executable for either dev backend', says(readVerdictAuthorship(VERDICT(['Implemented-by: the dispatching seat', `Reviewed-by: \`${REVIEW_SESSION}\``]).body)?.detail, 'session id') && says(readVerdictAuthorship(VERDICT(['Implemented-by: the dispatching seat', `Reviewed-by: \`${REVIEW_SESSION}\``]).body)?.detail, 'claude/')); // the spelling, mirroring the Clause-② reader exactly t('the token may be followed by the seat\'s reasoning — the same calibration Clause-② uses', readVerdictAuthorship(VERDICT([`Implemented-by: ${IMPL_SESSION} (implementation claim, 09:24Z)`, `Reviewed-by: ${REVIEW_SESSION}`]).body)?.implementedBy === IMPL_SESSION); @@ -1471,9 +1527,27 @@ export function selfTest() { const halfRow4 = c4VerdictSelfReview(reviewed([VERDICT([`Reviewed-by: \`${REVIEW_SESSION}\``])])); t('⭐ a ONE-LINE-only verdict is its own reportable malformed state', typeof halfRow4 === 'string' && says(halfRow4, 'HALF')); t('…and quotes the fixed spelling so the remedy is executable', says(halfRow4, 'Implemented-by: session_') && says(halfRow4, 'Reviewed-by: session_')); + t('…including the subagent BRANCH form, the half a session-only spelling could not express', says(halfRow4, 'Implemented-by: claude/')); t('…and says in the same breath that a legacy verdict is NOT this state', says(halfRow4, 'LEGACY') && says(halfRow4, 'never turned red')); t('C4 never prescribes a write from this script either', says(halfRow4, '自查放行')); + // the SUBAGENT identity — reading a (maintainer 2026-09-02, verbatim 「同意」) + // A `mode:subagent` dev has no session of its own, so the implementation claim + // names it by its BRANCH; a session token on that line would name the + // dispatching seat and make every subagent-dispatched card a false self-review. + const DEV_BRANCH = 'claude/issue-14209-review-template-half'; + const SUBAGENT_PAIR = [`Implemented-by: \`${DEV_BRANCH}\``, `Reviewed-by: \`${REVIEW_SESSION}\``]; + t('⭐ a `mode:subagent` dev is named by its BRANCH, and that reads as a first-class pair', readVerdictAuthorship(VERDICT(SUBAGENT_PAIR).body)?.kind === 'pair' && readVerdictAuthorship(VERDICT(SUBAGENT_PAIR).body)?.implementedBy === DEV_BRANCH); + t('⭐ …so a subagent-dispatched card is SILENT — the reviewing seat did not write the diff', c4VerdictSelfReview(reviewed([VERDICT(SUBAGENT_PAIR)])) === null); + t('…and it reads INDEPENDENT rather than unjudged — silence here is a reading, not a gap', cardVerdictAuthorship([VERDICT(SUBAGENT_PAIR)]).state === 'independent' && pairUnjudged(reviewed([VERDICT(SUBAGENT_PAIR)])) === null); + t('⭐ …while the same-session pair still FIRES: widening the grammar did not disarm the row', typeof c4VerdictSelfReview(reviewed([VERDICT(SELF_PAIR)])) === 'string'); + t('the branch token ends at the branch — the seat\'s reasoning may follow it', readVerdictAuthorship(VERDICT([`Implemented-by: ${DEV_BRANCH} (implementation claim, 16:29Z)`, `Reviewed-by: ${REVIEW_SESSION}`]).body)?.implementedBy === DEV_BRANCH); + t('…and a trailing sentence mark belongs to the prose, never to the branch', readVerdictAuthorship(VERDICT([`Implemented-by: ${DEV_BRANCH}.`, `Reviewed-by: ${REVIEW_SESSION}`]).body)?.implementedBy === DEV_BRANCH); + t('⛔ `Reviewed-by:` admits a session ONLY — a verdict is rendered by a seat, and a seat has one', readVerdictAuthorship(VERDICT([`Implemented-by: \`${DEV_BRANCH}\``, `Reviewed-by: \`${DEV_BRANCH}\``]).body)?.kind === 'malformed'); + t('…and says WHICH key refused the token, not the other one', says(readVerdictAuthorship(VERDICT([`Implemented-by: \`${DEV_BRANCH}\``, `Reviewed-by: \`${DEV_BRANCH}\``]).body)?.detail, 'Reviewed-by') && !says(readVerdictAuthorship(VERDICT([`Implemented-by: \`${DEV_BRANCH}\``, `Reviewed-by: \`${DEV_BRANCH}\``]).body)?.detail, '`Implemented-by:` carries')); + t('⛔ a bare `claude` with no branch path is not an identity', readVerdictAuthorship(VERDICT(['Implemented-by: claude', `Reviewed-by: \`${REVIEW_SESSION}\``]).body)?.kind === 'malformed'); + t('⛔ nor is a branch that merely appears LATER in the line — the token is first after the colon', readVerdictAuthorship(VERDICT([`Implemented-by: the dev on ${DEV_BRANCH}`, `Reviewed-by: \`${REVIEW_SESSION}\``]).body)?.kind === 'malformed'); + // the governing verdict — why the newest one carrying the pair decides t('⭐ a self-review followed by an INDEPENDENT re-review reads clean — the remedy clears the row', c4VerdictSelfReview(reviewed([VERDICT(SELF_PAIR, '2026-09-01T11:21:18Z'), VERDICT(INDEPENDENT_PAIR, '2026-09-01T12:56:27Z')])) === null); t('…and the reverse order still fires, so a later self-review is not hidden by an earlier clean one', typeof c4VerdictSelfReview(reviewed([VERDICT(INDEPENDENT_PAIR, '2026-09-01T09:00:00Z'), VERDICT(SELF_PAIR, '2026-09-01T12:56:27Z')])) === 'string'); diff --git a/scripts/pm/check-skill-line-ratchet.mjs b/scripts/pm/check-skill-line-ratchet.mjs index 39fdf20e58..f7c2519c8c 100644 --- a/scripts/pm/check-skill-line-ratchet.mjs +++ b/scripts/pm/check-skill-line-ratchet.mjs @@ -366,7 +366,38 @@ export const CEILINGS = new Map([ // 120-byte cap, and no sixth-line spelling exists that keeps 不是出货缺陷, the // ruling's own justification for declining the taxonomy. Landed count, headroom // 0, same convention. - ['.claude/skills/pm-dispatch/references/contract-review.md', 65], + // Raised 65 → 68 by the review-INDEPENDENCE carrier — the template half of + // direction A, whose guard half (finding row C4 in + // `check-clause2-carriers.mjs`) landed on its own and has been INERT ever + // since, because no verdict on the board carries the two lines it compares. + // The addition is one bullet in 复核归属与资格(席内): the verdict declares + // `Implemented-by:` (the identity that produced the diff — a `mode:subagent` + // dev's BRANCH, since a subagent has no session of its own, and a + // `mode:remote` dev's session id) and `Reviewed-by:` (the reviewing seat's + // session); the same session on both lines is a SELF-REVIEW and is not an + // independent review, while a verdict carrying neither line stays silent + // forever. Two rulings authorise it, both verbatim and untranslated. The + // direction (maintainer, 2026-09-01, live session with the director seat): + // 「同意 A」 + // and the raise itself together with the spelling reading (maintainer, + // 2026-09-02, live PM chat, decision batch #12, adopting the recommendation + // "(i) approve the raise, (ii) reading a"): + // 「同意」 + // The 2026-09-02 batch #1 ruling 「14324 等我发版,其他同意」 authorised the + // same raise on the branch that has since merged guard-only; ruling (i) above + // re-issues it for THIS patch round, which is why the raise is not a seat's + // own judgement about its own diff — the one act this card exists to record. + // Paid in place FIRST, and the payment is not available: the file's 64 lines + // hold 2,007 bytes of slack under the 120-byte cap (mean width 88.6), so the + // 436 bytes of ruled addition would fit only by re-flowing the whole file — + // re-wrap funding is refused per the 2026-08-17 rule, exactly as on the two + // raises above — and the only other in-place payment is deleting a ruled + // clause, refused on the state-machine precedent. The CROSS-FILE MOVE path + // below does not apply either: no other ceilinged file holds this fact, so + // there is no source deletion to pay with, and manufacturing one to dodge a + // ruled raise would grow the corpus by exactly the same lines with the + // warrant hidden. Landed count, headroom 0, same convention. + ['.claude/skills/pm-dispatch/references/contract-review.md', 68], // Business-perspective decision-analysis writing guide (maintainer ruling // 2026-08-20: the four-facet analysis must argue from the business // standpoint). Set at landed line count (headroom 0, same convention).