From 4143b36ff6e655c0eeb1e7bc2640ef30aeeb146a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 14:31:53 +0000 Subject: [PATCH 1/3] docs(pm-dispatch): an exhausted contract-review tier hands the review to triage, and triage tracks stale records The dispatching seat still runs its own contract review at the contract-review tier. Only a quota-refused call hands the review over: one PR comment carrying a `Contract-review-handoff:` line beside the existing label, which the triage seat reads each round and runs in an isolated subagent at the same tier. Triage also compares every record it rendered against the PR's current head and re-reviews or notes a moved head. `record-recognisers.mjs` gains the one reader of that line, with self-test pins; no gate reads it. Claude-Session: https://claude.ai/code/session_01ADHkmSUY4HbWjTHDfPyAYU Co-authored-by: Claude --- .../pm-dispatch/references/contract-review.md | 6 +- .../pm-dispatch/references/triage-duties.md | 12 +- scripts/pm/record-recognisers.mjs | 129 +++++++++++++++++- 3 files changed, 137 insertions(+), 10 deletions(-) diff --git a/.claude/skills/pm-dispatch/references/contract-review.md b/.claude/skills/pm-dispatch/references/contract-review.md index b439e6c980f..28bc6e0c4c8 100644 --- a/.claude/skills/pm-dispatch/references/contract-review.md +++ b/.claude/skills/pm-dispatch/references/contract-review.md @@ -18,6 +18,8 @@ - 已发布面以包的 `exports` 映射为准:从入口类型图不可达、且 `exports` 不可寻址的 `.d.ts` 是 出货字节,⛔ 不是已发布接受集,改它不触发 `Clause-②: yes`;legacy node10 深引用在契约之外。 - 四面皆不碰 ⇒ CI 加席位自读 ⛔ 不起第二个 agent;谁跑 = 派发席,达档者席内审。 +- 派发席达档调用被配额拒(HTTP 429)⇒ ⛔ 不降档审;PR 落交接行、留等复核标记,交分诊席。 +- `Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (, ) · head ` - 复核记录 = 一条评论落 PR 或卡,席内与子代理同形;散文、dev 自评、`os-dev-report` 恒不算。 - 同形 = `## Contract review` 题头、head sha 独占码段、①②③ 逐项(申报面外三面加 ④)、 独立性对、PASS/FAIL。 @@ -32,8 +34,6 @@ 被引代码读而不审;①–④ 合称完整复核,余面同形无 ④,⛔ 无第二条记录。 - 实质 head 各欠一轮完整复核;只落发现的 commit 由同一复核者 delta 复核;无 must-fix 即止。 - 另记作者:PR `user.login` 非 `objectstack-fleet[bot]` ⇒ ③ 注点名该 login(脱中继写),落地前读。 -- 复核形状:只读 diff 与卡片,check 结论取 head 的 check-runs,⛔ 永不本地重跑派生门禁族。 -- 只喂卡片、既有裁决与 PR 本体,⛔ 不喂派发令与派发席自己的结论;简报写成对抗性。 - 简报从 `node scripts/pm/record-recognisers.mjs --brief-template` 起稿: 只读条款与输入集内建,⛔ 不删 ⛔ 行。 - 隔离复核子代理暂存全写按所审 PR 命名的 `/pr-/`,⛔ 不读非本轮自写的暂存。 @@ -51,5 +51,5 @@ - 只作等复核标记,⛔ 不是闸门;单载体 = PR,卡上副本在规则外、不是要求。 - 挂:ACCEPT 时条款②任一肢命中而现 head 无同形 PASS 在案 ⇒ 派发席同笔挂于 PR。 - 摘:现 head 同形 PASS 在案 ⇒ 写记录的席同笔摘;PR 合并或关闭 ⇒ 派发席摘;FAIL 不摘。 -- 读者 = 维护者过滤 `is:pr is:open label:needs:contract-review` 与席位巡查、交接。 +- 只带标记无交接行者仍归本席,分诊 ⛔ 不碰;交接后 head 再动 ⇒ 新交接行或本席自审。 - ⛔ 无 check、workflow、队列守卫或巡查脚本读它;入队只认同形记录,标记有无不改判。 diff --git a/.claude/skills/pm-dispatch/references/triage-duties.md b/.claude/skills/pm-dispatch/references/triage-duties.md index 94df158fb81..b90a25b74b1 100644 --- a/.claude/skills/pm-dispatch/references/triage-duties.md +++ b/.claude/skills/pm-dispatch/references/triage-duties.md @@ -9,6 +9,12 @@ - 查重缓存住席内:新会话全量拉,其后 `since` 增量;⛔ 不建分支、不共享、不依赖备份。 - 每日一 fire 跑两仓全量对账并归集日频职责;选层按 fire 时刻 ⛔ 不用计数器,简报写明层。 - 每条枚举比对返回数与 `totalCount`,不等 ⇒ 报 `sweep INCOMPLETE` 点名缺口,⛔ 永不报干净。 +- 复审交接:每轮列最新交接行点名现 head 的 open PR,隔离子代理达档跑同形记录、署本席。 +- 本席也被配额拒 ⇒ PR 写一次、留标,⛔ 永不降档审;交接行读法 `record-recognisers.mjs`。 +- 每轮比对自审记录 `Head-sha` 与现 head,越过即过期,纯重生成/只动散文面例外(判已提交树)。 +- 自审 FAIL 过期 ⇒ 同轮达档重审新 head,逐条复查 must-fix,delta commit 当改动审。 +- 自审 PASS 过期 ⇒ PR 记一条 PASS 不再管辖、欠 delta 复审,归本席或新交接行。 +- 轮次记录逐张自审 PR 写 `记录 head → 现 head → 状态`(现行/过期已重审/过期已记)。 - Backlog sweep 是常设职责,每 fire 扫任一析取命中的卡。 - 析取 ①:全裸(无 `pm:*`、无 `needs-user-decision`、无 `domain:*`)。 - 析取 ②:有 `pm:queue` 无 `domain:*`(仅多车道仓);析取 ③:有 `domain:*` 无 pm-state。 @@ -21,7 +27,6 @@ - 排序:队列每轮清空时最新优先;清不空的那一刻起改最老优先。 - 裸卡数 >15 ⇒ 下一 fire 整轮改域分批集中模式,每批 ≤5 张同族卡,照样跑到清空。 - 紧急卡直接分诊:维护者点名或 p0 嫌疑 ⇒ 立即起默认判断档分诊子代理,不等定时轮。 -- 紧急分诊的授权面 = 分诊本身,⛔ 不写码不认领;产出落卡,与定时轮分诊同格式同效力。 - main-red 锚卡由席位先立先派,首触事后补作记录级:可改域,⛔ 不改动作,在飞照补。 - 分类动作每张三选一,外加一个修复通道;裁定写方向、给六态之一。 - 方向在 ADR/本卡裁决/不可重裁规则:贴 `check-prior-rulings.mjs` 的 `Prior rulings read:` 行,或不点。 @@ -38,8 +43,6 @@ - (c) AI 写元数据会被运行时拒收或静默丢弃的陷阱;其余进 PR `## Acceptance notes`,⛔ 不立卡。 - (a) 的分界是不完整 vs 错误,不是文档 vs 代码:示例照抄即失败是 (a),漏列成员不是。 - 门禁头注、self-test 文案与 `check-*` 处方句 ⛔ 非已声明契约;(b) 须用户或已发布包读得到。 -- (c) 元数据 = 由写它的人以外的人存储并再作者化的键:React prop 不是,存储的视图配置是。 -- 把作者引向运行时会兑现却让事情更糟的元数据的警告不在 (c) 内;记为边界,⛔ 不扩类。 - 三类同欠 `reach:`,分诊只把关不测;观察、休眠、零拉动 ⛔ 不立卡、不进收口卡、无配额。 - 缺 `reach:` 且非例外 ⇒ 定 `priority:p3`,并入收口卡或清扫卡,⛔ 不单独派发、不因此关卡。 - 先修复:正文被 sanitizer 截断的卡不可派发,评论修复指令后跳过。 @@ -51,8 +54,6 @@ - 清单项三态定级(会 fail / 步骤够不到 / 无项断言)与定级判据见 `references/filing-gate.md`。 - 「无则关」= 首触即关 not_planned,带理由与入队三门,⛔ 不定 p3、不 hold;下行同此。 - 所入车道队列 P0/P1 开着(= 未派发,在飞不算)时,三门皆无的 p2/p3 `tooling` 卡同样关。 -- 只报告仪器(放宽 tell、半状态巡查)的修复卡首触即关 not_planned,引裁决 208;在途只有删除。 -- 路由是分诊的技术判断,⛔ 永不升级哪个仓的问题;父单是协调节点,永不派发。 - 父单已有子结构的:父单队列标签即可,分诊逐个展开路由、补 `Blocked-by:` 排序。 - 跨层功能点(≥2 层)立父单带 `area:*`+`pm:epic`+清单项级,逐层子单按 `Blocked-by:` 排。 - 每张留一条英文审计评论(`Triage: lands in …; rationale: …`),可选带 `Size/model suggestion:` 行。 @@ -73,7 +74,6 @@ - 在飞活性复查:认领逾一天无自有产出(commit/PR/评论)⇒ 敲门一次,首行键 `Owned-24h re-check:`。 - 敲门后 `RECLAIM_AFTER_HOURS = 12` 小时无答复 ⇒ `label-write.mjs --release` 一笔中继释放,贴不动。 - 释放:`Release:` 评论(出处三件)+ `Resume-from:` 行 + `pm:dispatched`→`pm:queue` + 清 assignee。 -- 车道席可附证据/前提重验,⛔ 不定级,改标限席改标;每张卡欠一次分诊首触,⛔ 不自分诊。 - 域分批与 sweep 打包晋级五条照用;每批约定与积压告警见 `references/dispatch-runbook.md`。 - `pm:retriage` 每 fire 先答所求、同笔摘标;须维护者答的入箱;既裁砍项与席改标退回执行席。 - retriage 的异议判据认内容(有评论写明所求)⛔ 不认形状(有一条异议评论)。 diff --git a/scripts/pm/record-recognisers.mjs b/scripts/pm/record-recognisers.mjs index 462459281f4..e0f0373fc5e 100644 --- a/scripts/pm/record-recognisers.mjs +++ b/scripts/pm/record-recognisers.mjs @@ -26,6 +26,7 @@ * reviewScopeHits the at-tier review's scope floor — `reviewFace`'s `hits`, the record's `Scope:` line * recordCarriesDesignJudgment whether a face's record carries item ④, the full review's design judgment * readLocalRuns the `Local-runs:` line, and `localRunsStand` on it — judged when present, absent not re-judged + * contractReviewHandoff whose review a PR's contract review is — the newest `Contract-review-handoff:` line against the head; `readContractReviewHandoff` reads the line * isModelIdentifierToken the token a refusal must never quote back * REVIEW_OF_RECORD_THREADS the ONE thread set, and REVIEW_OF_RECORD_LOCATION its words * unexplainedPathsBetween the pure-regeneration test a carried record re-runs, prose arm included @@ -68,6 +69,7 @@ */ import process from 'node:process'; +import { readFileSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { isEntrypoint } from '../invoked-as.mjs'; @@ -796,6 +798,103 @@ export function localRunsStand(reading) { return reading?.state === 'missing' || reading?.state === 'read'; } +/** + * The `Contract-review-handoff:` key line -- the ONE way a dispatching seat + * hands its contract review to the triage seat, read with `Reviewed-by:`'s own + * key discipline. + * + * ⭐ The rule it makes machine-findable (the skill's contract-review reference, + * 〈复核归属与资格〉, and the triage duties' round step): the seat that + * dispatched the work runs the review at the contract-review tier. ONLY when + * that tier's call is quota-refused does the seat hand the review over -- it + * never reviews at a lower tier -- and the hand-off is one PR comment carrying + * this line beside the existing `needs:contract-review` label: + * + * Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429, 2026-10-27T13:00Z) · head 1b03a9c3 + * + * Three facts, each required: the TARGET (`triage`, the closed set of one), the + * TIER that was unavailable -- the constant's NAME, exactly as `Served-tier:` + * spells it, ⛔ never a model id -- with its reason and UTC time in the + * parentheses, and the HEAD the hand-off is for, 7 to 40 hex. A line missing + * any of them is started-and-unreadable: a hand-off nobody can date, or one + * that names no head, is the ambiguity this line exists to remove. + * + * ⛔ No gate reads it. The queue guard keeps reading the ONE `## Contract + * review` record; this line decides only WHO renders that record, and the + * triage seat's record is the same shape, `Reviewed-by:` naming the triage + * seat. `contractReviewHandoff` below is the reading a seat or patrol takes. + */ +export const CONTRACT_REVIEW_HANDOFF_KEY = 'Contract-review-handoff'; +const CONTRACT_REVIEW_HANDOFF_LINE = keyLineRegex(CONTRACT_REVIEW_HANDOFF_KEY); +/** The same key as a MARKER over a whole comment body -- `latestMarkedComment`'s convention (multiline, `g`-less). */ +const CONTRACT_REVIEW_HANDOFF_MARKER = new RegExp(CONTRACT_REVIEW_HANDOFF_LINE.source, 'm'); +/** `triage`, a separator (an em dash, a hyphen pair, a hyphen or a colon), then the rest. */ +const HANDOFF_TARGET = /^triage\b[ \t]*(?:—|--|-|:)?[ \t]*(.*)$/i; +/** `(reason, UTC time)` then ` · head SHA` -- the time is the LAST comma-separated item in the parentheses. */ +const HANDOFF_TAIL = /^\(([^()]*?)[ \t]*,[ \t]*([^,()]+)\)[ \t]*·[ \t]*head[ \t]+`?([0-9a-fA-F]{7,40})`?[ \t]*$/; + +/** + * What one comment declares as a hand-off of its contract review. + * + * @returns {{ state: 'missing' } + * | { state: 'unreadable', line: string } + * | { state: 'read', to: 'triage', tier: string, reason: string, at: string, head: string }} + */ +export function readContractReviewHandoff(text) { + for (const line of String(text ?? '').split(/\r?\n/)) { + const m = CONTRACT_REVIEW_HANDOFF_LINE.exec(line); + if (!m) continue; + const unreadable = { state: 'unreadable', line: quoteLine(line) }; + const target = HANDOFF_TARGET.exec(stripValueDecoration(m[1]).replace(/(?:[ \t]|\*\*|`)+$/, '')); + if (!target) return unreadable; + // The tier is read by NAME, through the one token reader `Served-tier:` uses, so a model id + // written here is refused for the same reason it is refused there. + const named = stripValueDecoration(target[1]); + const tier = TIER_TOKEN.exec(named); + if (!tier || tier[1] !== CONTRACT_REVIEW_TIER_NAME) return unreadable; + const rest = stripValueDecoration(named.slice(tier[0].length)); + const tail = /^unavailable\b[ \t]*(.*)$/i.exec(rest); + const parts = tail ? HANDOFF_TAIL.exec(tail[1].trim()) : null; + if (!parts || parts[1].trim() === '') return unreadable; + return { state: 'read', to: 'triage', tier: tier[1], reason: parts[1].trim(), at: parts[2].trim(), head: parts[3] }; + } + return { state: 'missing' }; +} + +/** + * Whose review this pull request's contract review is, read off its comment thread: + * + * absent no hand-off line on any comment -- the dispatching seat's own review; a + * `needs:contract-review` label alone changes nothing here, and triage does not touch it + * triage the NEWEST hand-off line names the current head -- the triage seat renders the record + * stale the newest hand-off line names an earlier head -- the head moved past the hand-off, so + * it needs a fresh line or the seat's own review; triage does not act on it + * unreadable the thread or the head could not be read, or the newest hand-off line cannot be graded + * + * Newest governs, chosen by `latestMarkedComment` -- the one resolution this file's locator uses -- + * and the head is matched by PREFIX, as `contractReviewHeadMatch` matches a record's span, so the + * abbreviated and the full spelling answer alike. + * + * @param {{ id?: number, body?: string, created_at?: string }[]} rows the PR's comment rows + * @param {string} headSha the pull request's current head + */ +export function contractReviewHandoff(rows, headSha) { + const gaps = []; + if (!Array.isArray(rows)) gaps.push("the pull request's comment thread"); + const head = String(headSha ?? '').toLowerCase(); + if (head.length < H51_SHA_MIN_HEX) gaps.push("the pull request's head sha"); + if (gaps.length > 0) return { state: 'unreadable', gaps }; + const newest = latestMarkedComment(rows, CONTRACT_REVIEW_HANDOFF_MARKER); + if (!newest) return { state: 'absent', read: rows.length }; + const row = rows[newest.index]; + const line = readContractReviewHandoff(row?.body); + const stamp = { id: row?.id ?? null, at: row?.created_at ?? null }; + if (line.state !== 'read') return { state: 'unreadable', gaps: [`the newest hand-off line (comment ${stamp.id ?? '?'}) cannot be read: ${line.line ?? 'no line'}`], ...stamp }; + // PREFIX either way, as `regenChainToHead` meets two spellings of one commit: the line may abbreviate the head. + const current = shaMeets(head, line.head.toLowerCase()); + return { state: current ? 'triage' : 'stale', head: line.head, reason: line.reason, handedAt: line.at, ...stamp }; +} + // --------------------------------------------------------------------------- // The record TEMPLATE — the one machine-read artefact with nothing to copy (#18042) // --------------------------------------------------------------------------- @@ -1446,6 +1545,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ '⭐ B′ with C: a PROSE-ONLY head move keeps the record, decided on the COMMITTED trees like a regeneration': 6, '⭐ letter C: the at-tier review of a mixed diff is SCOPED — `hits` plus what they claim, on the `Scope:` line': 18, '⭐ letter B: a declared NARROWING is a face of its own at CONTRACT_REVIEW_TIER — keyed on the `Clause-②:` line, exact': 17, + '⭐ the `Contract-review-handoff:` line — names the current head ⇒ triage\'s review; head moved ⇒ stale; absent ⇒ the seat\'s own': 14, }); // DELETING an entry silences that battery's floor exactly as effectively as @@ -1458,7 +1558,8 @@ const SELF_TEST_BATTERIES = Object.freeze({ // registering reds here instead of leaving the ④ lines unmeasured. The per-surface scenario lines // and the governed-register wording added one pin to each: 27 → 28 and 16 → 17. Hanging ④ on the narrowing // face (ruling record 6442046166, letter A) flipped three pins and added one to each: 28 → 29 and 17 → 18. -const SELF_TEST_BATTERY_FLOOR = 12; +// Raised 12 → 13 with the hand-off battery (the maintainer's exhausted-tier rule): one battery, one row. +const SELF_TEST_BATTERY_FLOOR = 13; // The key an assertion is filed under when no battery is open. It is not a // declared battery, so it reds by the same set difference rather than silently @@ -1820,6 +1921,32 @@ export async function selfTest() { t('the key is ONE regex, built by the one key-line convention — never a second spelling', LOCAL_RUNS_LINE.source === keyLineRegex('Local-runs').source); t('prose naming the key mid-sentence is not the line — the declaration is line-initial', readLocalRuns('the Local-runs: line goes here').state === 'missing'); + // -- the `Contract-review-handoff:` line -------------------------------------- + // + // The maintainer's rule: a seat with a working contract-review tier reviews + // its own dispatches; only an exhausted tier hands the review to triage, and + // triage must be able to TELL that case apart from a label alone. The line is + // the telling; these cases pin the three readings a seat or patrol takes. + battery('⭐ the `Contract-review-handoff:` line — names the current head ⇒ triage\'s review; head moved ⇒ stale; absent ⇒ the seat\'s own'); + const HO_HEAD = '1b03a9c36be249502c1160b1f262f835f7ccaf02'; + const HO_LINE = (head = HO_HEAD, reason = 'HTTP 429', at = '2026-10-27T13:00Z') => `${CONTRACT_REVIEW_HANDOFF_KEY}: triage — ${CONTRACT_REVIEW_TIER_NAME} unavailable (${reason}, ${at}) · head ${head}`; + const HO_ROW = (line, id = 4401, at = '2026-10-27T13:01:00Z') => ({ id, created_at: at, body: `Seat note — the review tier was refused this round.\n\n${line}\n\nLabel kept.` }); + const RULES_TEXT = readFileSync(new URL('../../.claude/skills/pm-dispatch/references/contract-review.md', import.meta.url), 'utf8'); + t('⭐ the line names the CURRENT head ⇒ the review is triage\'s, with the reason, the time and the comment that handed it off', (() => { const r = contractReviewHandoff([HO_ROW(HO_LINE())], HO_HEAD); return r.state === 'triage' && r.head === HO_HEAD && r.reason === 'HTTP 429' && r.handedAt === '2026-10-27T13:00Z' && r.id === 4401; })()); + t('⭐ the head MOVED past the line ⇒ stale, naming the head the hand-off was for — a fresh line or the seat\'s own review', (() => { const r = contractReviewHandoff([HO_ROW(HO_LINE())], 'c260fddc'.padEnd(40, '0')); return r.state === 'stale' && r.head === HO_HEAD; })()); + t('⭐ NO hand-off line on any comment ⇒ absent, the seat\'s own review — a record, a seat note, a label mention change nothing', (() => { const r = contractReviewHandoff([RECORD_ON_9AF9, { id: 4402, created_at: '2026-10-27T13:02:00Z', body: 'ACCEPT — `needs:contract-review` stays until the seat reviews.' }], HO_HEAD); return r.state === 'absent' && r.read === 2; })()); + t('the head is matched by PREFIX, as a record\'s span is: an abbreviated head on the line names the full head, and the reverse', contractReviewHandoff([HO_ROW(HO_LINE(HO_HEAD.slice(0, 7)))], HO_HEAD).state === 'triage' && contractReviewHandoff([HO_ROW(HO_LINE(HO_HEAD))], HO_HEAD.slice(0, 8)).state === 'triage'); + t('NEWEST governs — an older line on an old head under a newer one on the current head reads triage\'s, and the reverse reads stale', contractReviewHandoff([HO_ROW(HO_LINE('deadbeef1'), 4403, '2026-10-27T12:00:00Z'), HO_ROW(HO_LINE(), 4404, '2026-10-27T13:30:00Z')], HO_HEAD).state === 'triage' && contractReviewHandoff([HO_ROW(HO_LINE(), 4403, '2026-10-27T12:00:00Z'), HO_ROW(HO_LINE('deadbeef1'), 4404, '2026-10-27T13:30:00Z')], HO_HEAD).state === 'stale'); + t('decoration reads: a bulleted bolded key, a backticked tier name, a backticked head, a blockquoted line', (() => { const dec = `> - **${CONTRACT_REVIEW_HANDOFF_KEY}:** triage — \`${CONTRACT_REVIEW_TIER_NAME}\` unavailable (quota exhausted, 2026-10-27T13:00Z) · head \`${HO_HEAD.slice(0, 12)}\``; const r = readContractReviewHandoff(dec); return r.state === 'read' && r.head === HO_HEAD.slice(0, 12) && r.reason === 'quota exhausted' && contractReviewHandoff([HO_ROW(dec)], HO_HEAD).state === 'triage'; })()); + t('the separator is any of the four spellings, and the reason may itself carry a comma — the time is the LAST item in the parentheses', ['—', '--', '-', ':'].every((sep) => readContractReviewHandoff(`${CONTRACT_REVIEW_HANDOFF_KEY}: triage ${sep} ${CONTRACT_REVIEW_TIER_NAME} unavailable (HTTP 429, quota exhausted, 2026-10-27T13:00Z) · head ${HO_HEAD}`).reason === 'HTTP 429, quota exhausted')); + t('⛔ a line with no reason, no time or no head is started-and-unreadable, never a reading — the ambiguity the line exists to remove', [`${CONTRACT_REVIEW_HANDOFF_KEY}: triage — ${CONTRACT_REVIEW_TIER_NAME} unavailable (, 2026-10-27T13:00Z) · head ${HO_HEAD}`, `${CONTRACT_REVIEW_HANDOFF_KEY}: triage — ${CONTRACT_REVIEW_TIER_NAME} unavailable (HTTP 429) · head ${HO_HEAD}`, `${CONTRACT_REVIEW_HANDOFF_KEY}: triage — ${CONTRACT_REVIEW_TIER_NAME} unavailable (HTTP 429, 2026-10-27T13:00Z)`, `${CONTRACT_REVIEW_HANDOFF_KEY}: triage — ${CONTRACT_REVIEW_TIER_NAME} unavailable (HTTP 429, 2026-10-27T13:00Z) · head 1b03a`, `${CONTRACT_REVIEW_HANDOFF_KEY}:`].every((l) => readContractReviewHandoff(l).state === 'unreadable')); + t('⛔ the target is the closed set of one and the tier is the constant\'s NAME — another seat, the default tier or a model id is unreadable', [`${CONTRACT_REVIEW_HANDOFF_KEY}: director — ${CONTRACT_REVIEW_TIER_NAME} unavailable (HTTP 429, 2026-10-27T13:00Z) · head ${HO_HEAD}`, `${CONTRACT_REVIEW_HANDOFF_KEY}: triage — ${TIER_DEFAULT_NAME} unavailable (HTTP 429, 2026-10-27T13:00Z) · head ${HO_HEAD}`, `${CONTRACT_REVIEW_HANDOFF_KEY}: triage — ${CONTRACT_REVIEW_TIER} unavailable (HTTP 429, 2026-10-27T13:00Z) · head ${HO_HEAD}`].every((l) => readContractReviewHandoff(l).state === 'unreadable')); + t('an unreadable NEWEST line makes the thread unreadable, naming the comment — never absent, never stale', (() => { const r = contractReviewHandoff([HO_ROW(`${CONTRACT_REVIEW_HANDOFF_KEY}: triage — ${CONTRACT_REVIEW_TIER_NAME} unavailable (HTTP 429) · head ${HO_HEAD}`, 4405)], HO_HEAD); return r.state === 'unreadable' && r.gaps.length === 1 && r.gaps[0].includes('4405'); })()); + t('a thread that is not an array, or a head too short to match, is a read that could not be made', contractReviewHandoff(null, HO_HEAD).state === 'unreadable' && contractReviewHandoff([], 'abc').state === 'unreadable' && contractReviewHandoff([], HO_HEAD).state === 'absent'); + t('the key is ONE regex, built by the one key-line convention, and prose naming it mid-sentence is not the line', CONTRACT_REVIEW_HANDOFF_LINE.source === keyLineRegex(CONTRACT_REVIEW_HANDOFF_KEY).source && readContractReviewHandoff(`the ${CONTRACT_REVIEW_HANDOFF_KEY}: line goes on the PR`).state === 'missing' && contractReviewHandoff([HO_ROW(`the ${CONTRACT_REVIEW_HANDOFF_KEY}: line goes on the PR`)], HO_HEAD).state === 'absent'); + t('⭐ the rulebook spells the line this reader reads — key, target, the tier by NAME, `unavailable`, and `head` — on ONE line', RULES_TEXT.split(/\r?\n/).some((l) => l.includes(`\`${CONTRACT_REVIEW_HANDOFF_KEY}: triage — ${CONTRACT_REVIEW_TIER_NAME} unavailable (`) && l.includes(') · head '))); + t('⛔ no gate reads it: the queue guard\'s record reading is untouched — a hand-off line beside a record on this head still reads FOUND', (() => { const r = locateReviewOfRecord(pairOf({ prComments: [RECORD_ON_9AF9, HO_ROW(HO_LINE(HEAD_9AF9))] })); return r.state === 'found' && r.id === 3301; })()); + // -- #18042: the copyable record TEMPLATE ---------------------------------- // // `references/contract-review.md` DESCRIBES the record in prose and once From c59cd9c6537db26a8190b372720490c56b0dc338 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 14:52:49 +0000 Subject: [PATCH 2/3] fix(pm): the hand-off battery pins the reader alone, not a read of the rulebook A structural read of a governed file from a script is a census the gate derivation classifies, so the self-test keeps to rows and strings. Claude-Session: https://claude.ai/code/session_01ADHkmSUY4HbWjTHDfPyAYU Co-authored-by: Claude --- scripts/pm/record-recognisers.mjs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scripts/pm/record-recognisers.mjs b/scripts/pm/record-recognisers.mjs index e0f0373fc5e..0aae8481a2b 100644 --- a/scripts/pm/record-recognisers.mjs +++ b/scripts/pm/record-recognisers.mjs @@ -69,7 +69,6 @@ */ import process from 'node:process'; -import { readFileSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { isEntrypoint } from '../invoked-as.mjs'; @@ -1545,7 +1544,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ '⭐ B′ with C: a PROSE-ONLY head move keeps the record, decided on the COMMITTED trees like a regeneration': 6, '⭐ letter C: the at-tier review of a mixed diff is SCOPED — `hits` plus what they claim, on the `Scope:` line': 18, '⭐ letter B: a declared NARROWING is a face of its own at CONTRACT_REVIEW_TIER — keyed on the `Clause-②:` line, exact': 17, - '⭐ the `Contract-review-handoff:` line — names the current head ⇒ triage\'s review; head moved ⇒ stale; absent ⇒ the seat\'s own': 14, + '⭐ the `Contract-review-handoff:` line — names the current head ⇒ triage\'s review; head moved ⇒ stale; absent ⇒ the seat\'s own': 13, }); // DELETING an entry silences that battery's floor exactly as effectively as @@ -1931,7 +1930,6 @@ export async function selfTest() { const HO_HEAD = '1b03a9c36be249502c1160b1f262f835f7ccaf02'; const HO_LINE = (head = HO_HEAD, reason = 'HTTP 429', at = '2026-10-27T13:00Z') => `${CONTRACT_REVIEW_HANDOFF_KEY}: triage — ${CONTRACT_REVIEW_TIER_NAME} unavailable (${reason}, ${at}) · head ${head}`; const HO_ROW = (line, id = 4401, at = '2026-10-27T13:01:00Z') => ({ id, created_at: at, body: `Seat note — the review tier was refused this round.\n\n${line}\n\nLabel kept.` }); - const RULES_TEXT = readFileSync(new URL('../../.claude/skills/pm-dispatch/references/contract-review.md', import.meta.url), 'utf8'); t('⭐ the line names the CURRENT head ⇒ the review is triage\'s, with the reason, the time and the comment that handed it off', (() => { const r = contractReviewHandoff([HO_ROW(HO_LINE())], HO_HEAD); return r.state === 'triage' && r.head === HO_HEAD && r.reason === 'HTTP 429' && r.handedAt === '2026-10-27T13:00Z' && r.id === 4401; })()); t('⭐ the head MOVED past the line ⇒ stale, naming the head the hand-off was for — a fresh line or the seat\'s own review', (() => { const r = contractReviewHandoff([HO_ROW(HO_LINE())], 'c260fddc'.padEnd(40, '0')); return r.state === 'stale' && r.head === HO_HEAD; })()); t('⭐ NO hand-off line on any comment ⇒ absent, the seat\'s own review — a record, a seat note, a label mention change nothing', (() => { const r = contractReviewHandoff([RECORD_ON_9AF9, { id: 4402, created_at: '2026-10-27T13:02:00Z', body: 'ACCEPT — `needs:contract-review` stays until the seat reviews.' }], HO_HEAD); return r.state === 'absent' && r.read === 2; })()); @@ -1944,7 +1942,6 @@ export async function selfTest() { t('an unreadable NEWEST line makes the thread unreadable, naming the comment — never absent, never stale', (() => { const r = contractReviewHandoff([HO_ROW(`${CONTRACT_REVIEW_HANDOFF_KEY}: triage — ${CONTRACT_REVIEW_TIER_NAME} unavailable (HTTP 429) · head ${HO_HEAD}`, 4405)], HO_HEAD); return r.state === 'unreadable' && r.gaps.length === 1 && r.gaps[0].includes('4405'); })()); t('a thread that is not an array, or a head too short to match, is a read that could not be made', contractReviewHandoff(null, HO_HEAD).state === 'unreadable' && contractReviewHandoff([], 'abc').state === 'unreadable' && contractReviewHandoff([], HO_HEAD).state === 'absent'); t('the key is ONE regex, built by the one key-line convention, and prose naming it mid-sentence is not the line', CONTRACT_REVIEW_HANDOFF_LINE.source === keyLineRegex(CONTRACT_REVIEW_HANDOFF_KEY).source && readContractReviewHandoff(`the ${CONTRACT_REVIEW_HANDOFF_KEY}: line goes on the PR`).state === 'missing' && contractReviewHandoff([HO_ROW(`the ${CONTRACT_REVIEW_HANDOFF_KEY}: line goes on the PR`)], HO_HEAD).state === 'absent'); - t('⭐ the rulebook spells the line this reader reads — key, target, the tier by NAME, `unavailable`, and `head` — on ONE line', RULES_TEXT.split(/\r?\n/).some((l) => l.includes(`\`${CONTRACT_REVIEW_HANDOFF_KEY}: triage — ${CONTRACT_REVIEW_TIER_NAME} unavailable (`) && l.includes(') · head '))); t('⛔ no gate reads it: the queue guard\'s record reading is untouched — a hand-off line beside a record on this head still reads FOUND', (() => { const r = locateReviewOfRecord(pairOf({ prComments: [RECORD_ON_9AF9, HO_ROW(HO_LINE(HEAD_9AF9))] })); return r.state === 'found' && r.id === 3301; })()); // -- #18042: the copyable record TEMPLATE ---------------------------------- From 5956f4ba7048a5ca2943d12ce50b1dd66514ff48 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 15:18:22 +0000 Subject: [PATCH 3/3] docs(pm-dispatch): the triage round step pays with pointer lines, not with rules only the dev file carries The (c)-metadata boundary and the routing rule stay in the triage duties; three lines that only pointed at filing-gate.md and dispatch-runbook.md pay for them instead. Claude-Session: https://claude.ai/code/session_01ADHkmSUY4HbWjTHDfPyAYU Co-authored-by: Claude --- .claude/skills/pm-dispatch/references/triage-duties.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/skills/pm-dispatch/references/triage-duties.md b/.claude/skills/pm-dispatch/references/triage-duties.md index b90a25b74b1..7691ae7e354 100644 --- a/.claude/skills/pm-dispatch/references/triage-duties.md +++ b/.claude/skills/pm-dispatch/references/triage-duties.md @@ -38,11 +38,12 @@ - 碰迁移形状或删已发布能力也进决策箱;说明书脱节不进:有裁定出处才是 (b),否则改文档。 - 决策卡速读居四棱块之上、同题,用业务语言先讲事情与选项,末句只问一字:A/B/C 或是否。 - 首行写立卡门类别 ①–④;四类定义、⛔ 清单、四答、理由判据与配额见 `filing-gate.md`。 -- 首触先按业务理由三出口分流,再定级路由;批量卡审计评论带 `Batch-of:` 行。 - `finding` = 三类内待定级:(a) 可复现缺陷(复现或失败探针);(b) 违背已声明契约(引契约文); - (c) AI 写元数据会被运行时拒收或静默丢弃的陷阱;其余进 PR `## Acceptance notes`,⛔ 不立卡。 - (a) 的分界是不完整 vs 错误,不是文档 vs 代码:示例照抄即失败是 (a),漏列成员不是。 - 门禁头注、self-test 文案与 `check-*` 处方句 ⛔ 非已声明契约;(b) 须用户或已发布包读得到。 +- (c) 元数据 = 由写它的人以外的人存储并再作者化的键:React prop 不是,存储的视图配置是。 +- 把作者引向运行时会兑现却让事情更糟的元数据的警告不在 (c) 内;记为边界,⛔ 不扩类。 - 三类同欠 `reach:`,分诊只把关不测;观察、休眠、零拉动 ⛔ 不立卡、不进收口卡、无配额。 - 缺 `reach:` 且非例外 ⇒ 定 `priority:p3`,并入收口卡或清扫卡,⛔ 不单独派发、不因此关卡。 - 先修复:正文被 sanitizer 截断的卡不可派发,评论修复指令后跳过。 @@ -51,9 +52,9 @@ - 判据:违背已声明契约 ⇒ `Bug`,扩大接受集/公开面越出已声明 ⇒ `Feature`,其余 ⇒ `Task`。 - 定级按北极星「优先级」1–3 条;首行 `Path: <定义项> | <清单项 id/缺项> | P<路步> | none`。 - 定义项取北极星「做出来的是什么」或仪器卡的车队决定;无则关;`none` 非分诊缺口。 -- 清单项三态定级(会 fail / 步骤够不到 / 无项断言)与定级判据见 `references/filing-gate.md`。 - 「无则关」= 首触即关 not_planned,带理由与入队三门,⛔ 不定 p3、不 hold;下行同此。 - 所入车道队列 P0/P1 开着(= 未派发,在飞不算)时,三门皆无的 p2/p3 `tooling` 卡同样关。 +- 路由是分诊的技术判断,⛔ 永不升级哪个仓的问题;父单是协调节点,永不派发。 - 父单已有子结构的:父单队列标签即可,分诊逐个展开路由、补 `Blocked-by:` 排序。 - 跨层功能点(≥2 层)立父单带 `area:*`+`pm:epic`+清单项级,逐层子单按 `Blocked-by:` 排。 - 每张留一条英文审计评论(`Triage: lands in …; rationale: …`),可选带 `Size/model suggestion:` 行。 @@ -74,7 +75,6 @@ - 在飞活性复查:认领逾一天无自有产出(commit/PR/评论)⇒ 敲门一次,首行键 `Owned-24h re-check:`。 - 敲门后 `RECLAIM_AFTER_HOURS = 12` 小时无答复 ⇒ `label-write.mjs --release` 一笔中继释放,贴不动。 - 释放:`Release:` 评论(出处三件)+ `Resume-from:` 行 + `pm:dispatched`→`pm:queue` + 清 assignee。 -- 域分批与 sweep 打包晋级五条照用;每批约定与积压告警见 `references/dispatch-runbook.md`。 - `pm:retriage` 每 fire 先答所求、同笔摘标;须维护者答的入箱;既裁砍项与席改标退回执行席。 - retriage 的异议判据认内容(有评论写明所求)⛔ 不认形状(有一条异议评论)。 - 人工地板(恒呈维护者):功能新增;ADR;协议/公开契约变化;破坏性或难回滚动作。