diff --git a/.claude/skills/pm-dispatch/references/lanes/security.md b/.claude/skills/pm-dispatch/references/lanes/security.md index 54b48cc689c..f6c1a79cc4c 100644 --- a/.claude/skills/pm-dispatch/references/lanes/security.md +++ b/.claude/skills/pm-dispatch/references/lanes/security.md @@ -9,24 +9,43 @@ - 席位 `security#N`,首席 `security#1`;签名、座位贴、坐席与收班同他席。 - 无 `domain:security` 标签、无 `pm:queue`、无文件面:安全发现永不成卡,修复落在归属包。 - 坐席者 = 能读写本仓安全公告的账号(管理员或安全经理)的会话;读不了公告 ⛔ 不坐。 -- 读公告只经中继 `advisory_read`;建草稿与写公告只经中继,⛔ 永不以自身账号写。 +- 公告的读、建与写只照第 3 节。 ## 接收与台账 - 接收 = 发现者的一次 `Notify: security · GHSA-ID` 门铃,草稿公告已由其经中继建好。 -- 中继拒 ⇒ 机制在会话里交本席;本席空缺 ⇒ 交维护者(第 3 节);⛔ 不换账号重发。 -- 台账住座位贴,一份一行:不透明编号 · 会话 · 状态;⛔ 无细节。 -- 状态词表:`received` → `fixing` → `merged` → `awaiting publish` → `published`。 +- 中继拒或本席空缺照第 3、9 节。 +- 云席列不了本仓公告,中继也拒裸集合:编号只来自维护者或门铃。 +- 读出的公告明文只印本席进程 stdout(`--reveal`),⛔ 不进文件、日志或后台任务输出。 +- 台账住座位贴,一份一行:不透明编号 · 会话 · 状态;⛔ 无细节;修复的认领只记台账。 +- 状态词表:`received` → `fixing` → `merged` → `closed`;挑中发布再 `awaiting publish` → `published`。 ## 派修与复核 - 修复 = 本席向 `os-dev` 下无卡派发令,`xhigh`;机制只进派发令与公告,⛔ 不进 PR 与评论。 +- L/XL、build 重、长跑或浏览器验证的派修走 `mode:cloud`,余 `mode:subagent`(`../execution-duties.md`)。 - 修复 PR 的契约复核归本席:记录停在规则层,机制只在本席本地的 `security-note.md`(第 6 节)。 -- 要定契约的修复交维护者;发布公告恒归维护者,本席把状态推到 `awaiting publish` 即止。 +- 要定契约的修复交维护者(呈前照下节)。 + +## 呈维护者 + +- 任何问题呈维护者前,先由隔离 agent 以 `CONTRACT_REVIEW_TIER` 复核其分析;⛔ 不写模型名。 +- 复核者只拿分析与证据,⛔ 不拿本席结论;核四轴、选项无漏、推荐成立、证据为真。 +- 问题随附复核判词与全部异议。 +- 每次发版前呈发布与否清单:上次发版后 `closed` 的公告,高危、中危在前;先经上条复核。 +- 发布归维护者:在公告界面重开并发布,中继推不动已关公告。 + +## 收口(逐对象) + +- PR 由合并队列合入;本席确认 MERGED 后照 `../landing-operations.md` 收口其卡。 +- 卡:`Fixes #N` 合入即关,本席补落地记录并摘 `pm:*`;`Part of #N` 卡末段落地时本席自关。 +- 公告:修复合入即经中继 `advisory_state` 置 `closed` 并记座位贴,⛔ 不逐份请示。 +- 云卡:工作落地即由本席归档;子代理自行结束并删自己的 worktree。 ## 误公开的卡(第 7 节) - 本席接管:挂 `security` 标签,⛔ 不追加细节;是否移入公告并关卡交维护者定,⛔ 不代关。 +- 本席巡查自读 open 的 `security` 标签卡,标签即转交;分诊敲 `Notify: security · #N` 只为提速。 - 分诊只即推维护者并转交本席(`triage.md`);本席空缺时分诊照第 7 节处置。 ## 收班 diff --git a/.claude/skills/pm-dispatch/references/project-forms.md b/.claude/skills/pm-dispatch/references/project-forms.md index 3379d5d5e26..7ea6cbfc349 100644 --- a/.claude/skills/pm-dispatch/references/project-forms.md +++ b/.claude/skills/pm-dispatch/references/project-forms.md @@ -44,7 +44,7 @@ - 移卡入队者→车道:p0/p1 `Blocked-by` 关闭 `Notify: unblocked`;记裁决席→被改席 `Notify: ruling`。 - 席→兄弟席或他车道:认领文件面相交或须先落 `Notify: serial`,先落者落地 `Notify: landed`。 - 上下线→兄弟席与分诊 `Notify: seat-online`、`Notify: seat-offline`;清单变→兄弟席 `Notify: split`。 -- 发现者→`security#1`:中继草稿公告建成即 `Notify: security`,只带不透明编号。 +- 持发现的席→`security#1`:草稿公告建成或误公开卡挂标即 `Notify: security`,只带编号。 - ⛔ 不敲:回执、重发、催(逾期归升级);收方订阅 PR 已载者;问维护者走决策卡。 - 安全发现只敲上一键,⛔ 不带机制、路径或对象名;余照 `security-findings.md`。 - 每席每轮 ≤5 条,p0 豁免,超额归下轮,计入写节奏;全拼 `record-recognisers.mjs --mailbox-keys`。 diff --git a/scripts/pm/check-skill-line-ratchet.mjs b/scripts/pm/check-skill-line-ratchet.mjs index a5cc2918cc5..fa488205b29 100644 --- a/scripts/pm/check-skill-line-ratchet.mjs +++ b/scripts/pm/check-skill-line-ratchet.mjs @@ -1515,7 +1515,17 @@ export const CEILINGS = new Map([ // never-public rules stay in security-findings.md. A NEW file, so this is an // added row and no other row moves; set at the landed line count (headroom 0, // same convention). - ['.claude/skills/pm-dispatch/references/lanes/security.md', 34], + // 34 → 53 (#29726): the maintainer's working rules for the seat — the + // isolated `CONTRACT_REVIEW_TIER` review before any question reaches the + // maintainer, cloud orders for heavy fixes, the per-object close-out, the + // relay `closed` move on merge and the pre-release publish-or-not list, the + // intake and read rules — plus the review's S1 (two restated lines become + // pointers) and S2 (the label is the hand-over). Ruling of record: that card's + // body (rulings 1–3, each verbatim there; the order to write them in is + // 「这些都应该写到你的skills」), and its amendment comment 6595626876 (「目前的修复都关闭」, + // 「你处理完的可以都先关闭」, 「哪些高危要发布应该在我版本发布前决策吧」). Measured + // delta +19 (34 → 53 lines); landed count, headroom 0. + ['.claude/skills/pm-dispatch/references/lanes/security.md', 53], // Project Director seat charter (maintainer ruling 2026-08-27, verbatim: // 「项目总监 由人工定期指挥,负责处理 contract-review 类别的和需要决裁的。」; the file // carries it as a rule line since the rules-only rewrite, not as this quotation) diff --git a/scripts/pm/fleet-roster.mjs b/scripts/pm/fleet-roster.mjs index c4aa31bbf15..0c8eaa6b1e4 100644 --- a/scripts/pm/fleet-roster.mjs +++ b/scripts/pm/fleet-roster.mjs @@ -124,6 +124,11 @@ import { ghApi, PER_PAGE, readAllPages } from './gh-pages.mjs'; // seat since #27637); this file reads them, never re-spells them. import * as mailboxOpen from './mailbox-open.mjs'; import { LEDGER_EXCLUSION, MAILBOX_BRANCH_PREFIX, MAILBOX_LABEL, mailboxOwner, SEAT_EPIC_KIND, seatKey } from './mailbox-open.mjs'; +// The seat signature line has ONE spelling, check-half-states' `SEAT_SIGNATURE_LINE`; this file reads it, never +// re-spells it. The relay that imports `ROSTER_MARKER` from here (`fleet-write/dispatch.mjs`) already imports +// check-half-states itself, so this import loads nothing new there. The opener's lane-word lists cannot import it +// (the patrol loads the opener lazily, so that edge would be a cycle); the self-test holds the two equal. +import { SEAT_SIGNATURE_LINE } from './check-half-states.mjs'; // The seat record's heading and verdict have ONE reading, shared with H69 in // check-half-states; this file asks it, never re-spells the heading. import * as seatRecord from './seat-record.mjs'; @@ -292,19 +297,10 @@ export function epicSeatsHeld(lane, mailboxes) { } const isEpicLane = (lane) => lane.startsWith(`${SEAT_EPIC_KIND}:`); -/** - * The seat signature line `LANE#N · ACCOUNT · SESSION`, MIRRORED from `check-half-states.mjs`'s `SEAT_SIGNATURE_LINE` - * (#27113) — not imported, so the relay never loads the patrol; the self-test holds the two patterns byte-equal. - */ -const SIGNATURE_LINE = new RegExp( - '^((?:domain:[a-z][a-z0-9-]*|repo:[a-z][a-z0-9-]*|epic:#[1-9][0-9]*|triage|director|coordinator|security))#([1-9][0-9]*) · ([A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}) · (session_[A-Za-z0-9]{6,})$', - 'u', -); - /** The seat a body is signed by — its FIRST non-empty line, `readSignatureLine`'s reading — as `LANE#N`, or null. Pure. */ export function signedSeatOf(body) { const line = String(body ?? '').split(/\r?\n/).find((l) => l.trim() !== ''); - const m = line === undefined ? null : SIGNATURE_LINE.exec(line.trim()); + const m = line === undefined ? null : SEAT_SIGNATURE_LINE.exec(line.trim()); return m ? `${m[1]}#${m[2]}` : null; } @@ -657,7 +653,8 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'seat titles: lane, seat, repo, status, account, project': 13, 'wall notes, closing refs, verdicts and mailboxes': 13, 'the fixture snapshot: rows for the three repos': 25, - 'one account on two seats of a lane: the second seat reads its own traces': 13, + // 13 → 14: the lane-grammar pin (the signature alternation equals the opener's lane-word lists, `coordinator` aside). + 'one account on two seats of a lane: the second seat reads its own traces': 14, 'the rendered body: marker, columns, footer, determinism': 10, 'refusing to overwrite what is not the roster': 6, 'the CLI: fixture runs offline, usage errors are exit 2': 6, @@ -824,8 +821,12 @@ async function selfTest() { return [line.includes('| = seat 1 |'), line.includes(SUSPECTED_WALL)]; })(), [true, false]); t('the comment read starts STALE_HOURS before now, and the footer names it', [activitySince(NOW), readsFor([R]).some((q) => q.startsWith(`GET /repos/${R}/issues/comments?since=`))], ['2026-11-01T22:00:00Z', true]); - const { SEAT_SIGNATURE_LINE, readSignatureLine } = await import('./check-half-states.mjs'); - t('the signature pattern is check-half-states\' SEAT_SIGNATURE_LINE, byte for byte — mirrored, not imported', [SIGNATURE_LINE.source === SEAT_SIGNATURE_LINE.source, SIGNATURE_LINE.flags === SEAT_SIGNATURE_LINE.flags], [true, true]); + const { readSignatureLine } = await import('./check-half-states.mjs'); + t('the signature pattern is check-half-states\' SEAT_SIGNATURE_LINE, imported — no second spelling of the lane alternation in this file', [/^import \{ SEAT_SIGNATURE_LINE \} from '\.\/check-half-states\.mjs';$/mu.test(readFileSync(SELF_PATH, 'utf8')), readFileSync(SELF_PATH, 'utf8').includes(['triage', 'director', 'coordinator'].join('|'))], [true, false]); + // The lane grammar's two remaining owners, held equal here (the only file that imports both): the signature's + // alternation and the opener's lane-word lists. `coordinator` is the one lane that signs and has no mailbox. + const signatureLanes = (/^\^\(\(\?:([^()]*)\)\)#/u.exec(SEAT_SIGNATURE_LINE.source)?.[1] ?? '').split('|'); + t('⭐ the lane grammar is ONE set: the signature\'s kinds are the opener\'s kinds plus `epic`, its bare words the opener\'s bare lanes plus `coordinator` — a lane added to one side alone reds here', [signatureLanes.filter((a) => a.includes(':')).map((a) => a.slice(0, a.indexOf(':'))).sort(), signatureLanes.filter((a) => /^[a-z]+$/u.test(a)).sort(), signatureLanes.length], [[...mailboxOpen.SEAT_LANE_KINDS, SEAT_EPIC_KIND].sort(), [...mailboxOpen.SEAT_BARE_LANES, 'coordinator'].sort(), mailboxOpen.SEAT_LANE_KINDS.length + 1 + mailboxOpen.SEAT_BARE_LANES.length + 1]); const corpus = ['', sig('domain:server#2'), `\n\n ${sig('domain:engine-core#3')}\r\nbody`, `body\n${sig('triage#1')}`, sig('security#1'), 'epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U', 'domain:server#0 · os-pm · session_01SharedSeatSess', 'domain:server#2 · os-pm · cse_01SharedSeatSess']; t('signedSeatOf reads every body as readSignatureLine does', corpus.map(signedSeatOf), corpus.map((b) => { const r = readSignatureLine(b); return r ? `${r.lane}#${r.seat}` : null; })); } diff --git a/scripts/pm/mailbox-open.mjs b/scripts/pm/mailbox-open.mjs index 89c02d8e4e0..94b5f03614f 100644 --- a/scripts/pm/mailbox-open.mjs +++ b/scripts/pm/mailbox-open.mjs @@ -168,7 +168,8 @@ const SESSION_SHAPE = /^(?:session|cse)_[A-Za-z0-9]+$/; // three, so a login is never read as a seat. The lists below are the ONE spelling of those lane words: the shape is // built from them, and so is the reserved set a `domain:` lane may not be named after (`seatKey`). The same three // words close the seat signature's lane alternation (`SIGNATURE_LANE_SOURCE`, check-half-states.mjs), which this -// list does not import; a bare lane admitted there is admitted here in the same PR. +// list cannot import (the patrol loads this file lazily, so that edge would be a cycle); fleet-roster's self-test, +// which imports both, holds the two equal, `coordinator` (it signs and has no mailbox) aside. /** The lane kinds that carry a name after the colon: `domain:`, `repo:`. `domain` is the default kind the slug drops. */ export const SEAT_LANE_KINDS = Object.freeze(['domain', 'repo']); /** The bare lanes, a closed set: `triage`, `director`, `security` (the seat that owns security findings, `lanes/security.md`). */ diff --git a/scripts/pm/record-recognisers.mjs b/scripts/pm/record-recognisers.mjs index 55b8c74cd6a..6dbdbe85cdc 100644 --- a/scripts/pm/record-recognisers.mjs +++ b/scripts/pm/record-recognisers.mjs @@ -1286,7 +1286,7 @@ export const MAILBOX_KEYS = Object.freeze([ { id: 'notify:seat-online', kind: 'Notify', key: 'seat-online', from: 'the seating session (or a forced takeover, for the walled account)', to: 'the lane\'s sibling seats and triage', when: 'at seating', template: 'Notify: seat-online · LANE#N · LOGIN · mailbox #m · patrol TRIGGER_ID · takes: #a #b …' }, { id: 'notify:seat-offline', kind: 'Notify', key: 'seat-offline', from: 'the signing-off session', to: 'the lane\'s sibling seats and triage', when: 'at sign-off', template: 'Notify: seat-offline · LANE#N · in-flight: … · stay-behind: … · brief ID' }, { id: 'notify:split', kind: 'Notify', key: 'split', from: 'a seat of a multi-seat lane', to: 'its sibling seats', when: 'at round open, only when the list changed', template: 'Notify: split · LANE#N · takes: … · leaves: …' }, - { id: 'notify:security', kind: 'Notify', key: 'security', from: 'the finder — any seat, dev or QA runner', to: 'security#1', when: 'its relay `advisory_create` draft landed; the opaque advisory id ALONE — never a mechanism, a path or an object name', template: 'Notify: security · GHSA-ID' }, + { id: 'notify:security', kind: 'Notify', key: 'security', from: 'the seat holding the finding (a dev\'s or a QA runner\'s reaches its PM session privately first); triage, for a public card it labelled `security`', to: 'security#1', when: 'the seat\'s relay `advisory_create` draft landed: the opaque advisory id ALONE; or triage labelled a public card `security`: its number ALONE — never a mechanism, a path or an object name', template: 'Notify: security · GHSA-ID or #n' }, ]); /** One event, one line: the longest a mailbox line may be, in bytes (the text's 「≤200 字节」). */ export const MAILBOX_LINE_MAX_BYTES = 200; @@ -1316,8 +1316,10 @@ const SERIAL_REF = /^#([1-9][0-9]*) behind #([1-9][0-9]*)$/; /** A ruling's option LETTER as the record spells it (`A`, `B′`, `乙`): one short token, never a number reference. */ const RULING_LETTER_REF = /^[^\s#`]{1,12}$/u; /** - * `GHSA-xxxx-xxxx-xxxx` — a repository security advisory's opaque id, the ONE field `Notify: security` carries - * (`security-findings.md`: the id alone, never a mechanism, a path or an object name). MIRRORED from the relay's + * `GHSA-xxxx-xxxx-xxxx` — a repository security advisory's opaque id, the ONE field `Notify: security` carries from + * the seat holding a finding (`security-findings.md`: the id alone, never a mechanism, a path or an object name); + * triage's hand-over of a public card it labelled `security` carries that card's `#n` alone instead (`NUMBER_REF`, + * `lanes/security.md`). Never both on one line. MIRRORED from the relay's * `GHSA_ID_SHAPE` (`fleet-write/ops.mjs`, its reading of the id an advisory write lands) — not imported, so the * patrol graph never loads the relay; the self-test holds the two equal. */ @@ -1430,7 +1432,11 @@ const MAILBOX_FIELD_READERS = Object.freeze({ 'notify:security': (p) => { if (p.length !== 2) return null; const advisory = ADVISORY_ID_REF.exec(p[1]); - return advisory ? { advisory: advisory[0], numbers: [] } : null; + if (advisory) return { advisory: advisory[0], numbers: [] }; + // A public card triage labelled `security` (`lanes/security.md`): its number is already public and names no + // mechanism, so it rides the doorbell as the one field — the card, never the card AND an id. + const card = NUMBER_REF.exec(p[1]); + return card ? { card: Number(card[1]), numbers: [Number(card[1])] } : null; }, 'notify:contract-review': (p) => { if (p.length !== 3) return null; @@ -2213,7 +2219,8 @@ const SELF_TEST_BATTERIES = Object.freeze({ // per new key reading, the patrol and `patrol none` readings, the both-ways table pin that replaced the // verbatim-template pin, the placeholder-is-no-event pin, the `--mailbox-keys` printout, the invented // keys that stay refused, and the two end-to-end pins through check-half-states H72 (31 → 44 cases). - '⭐ the mailbox doorbell: `To: `, then one key of the table read — seat, kind, numbers, sha, login, time — and anything else refused': 40, + // Raised 40 → 41 with the `Notify: security · #n` reading (a labelled public card handed to the security seat). + '⭐ the mailbox doorbell: `To: `, then one key of the table read — seat, kind, numbers, sha, login, time — and anything else refused': 41, '⭐ the signature line: the reader reads LANE#N · ACCOUNT · SESSION exactly, and one leading signature changes no first-line reading': 22, '⭐ the `Contract-review-claim:` line — session, FULL head, time; lapses on a moved head or after two hours; the earliest live claim holds': 21, '⭐ the `Contract-review-hold:` line — session, head, reason, until; a new head clears it; at 24 hours it reads overdue and still holds': 23, @@ -2803,7 +2810,8 @@ export async function selfTest() { t('⭐ `Notify: ruling` reads the card, the letter and the record id — a primed or CJK letter reads, a sentence does not', (() => { const r = readMailboxLine(DB.ru); return r.state === 'read' && r.id === 'notify:ruling' && r.card === 28117 && r.letter === 'A' && r.record === 6545646924 && r.numbers.join() === '28117' && readMailboxLine(DB.ru.replace(' A ', ' B′ ')).letter === 'B′' && readMailboxLine(DB.ru.replace(' A ', ' 乙 ')).letter === '乙' && readMailboxLine(MSG('Notify: ruling · #28117 A (record 6545646924, 「同意」) · your draft #28430')).state === 'unreadable'; })()); t('⭐ `Notify: serial` reads the waiting card and the card it waits for; `Notify: landed` releases it', (() => { const r = readMailboxLine(DB.se); const l = readMailboxLine(DB.la); return r.state === 'read' && r.id === 'notify:serial' && r.card === 28572 && r.behind === 28562 && r.numbers.join() === '28572,28562' && l.state === 'read' && l.id === 'notify:landed' && l.card === 28562 && l.numbers.join() === '28562' && readMailboxLine(MSG('Notify: serial · #28572 · #28562')).state === 'unreadable'; })()); t('⭐ `Notify: security` under `To: security#1` reads to the security seat (a bare lane `seatKey` spells) with the advisory\'s opaque id and nothing else — it names no card, so `numbers` is empty', (() => { const r = readMailboxLine(DB.sec); return r.state === 'read' && r.to === 'security#1' && r.id === 'notify:security' && r.kind === 'Notify' && r.key === 'security' && r.advisory === 'GHSA-7q4h-9x2m-5c8v' && r.numbers.length === 0 && readMailboxLine(MSG('Notify: security · GHSA-7q4h-9x2m-5c8v', 'security')).to === 'security#1'; })()); - t('⛔ `Notify: security` carrying free text, a second field, a card number, an upper-case or short id, or no id at all is unreadable — nothing but the opaque id rides the doorbell', ['Notify: security · see the advisory', 'Notify: security · GHSA-7q4h-9x2m-5c8v · packages/x/src/door.ts', 'Notify: security · GHSA-7q4h-9x2m-5c8v · see the advisory', 'Notify: security · #28001', 'Notify: security · GHSA-7Q4H-9X2M-5C8V', 'Notify: security · GHSA-7q4h-9x2m', 'Notify: security'].every((l) => readMailboxLine(MSG(l)).state === 'unreadable')); + t('⛔ `Notify: security` carrying free text, a second field, an upper-case or short id, or no id at all is unreadable — nothing but the opaque id, or a labelled card\'s number alone, rides the doorbell', ['Notify: security · see the advisory', 'Notify: security · GHSA-7q4h-9x2m-5c8v · packages/x/src/door.ts', 'Notify: security · GHSA-7q4h-9x2m-5c8v · see the advisory', 'Notify: security · GHSA-7Q4H-9X2M-5C8V', 'Notify: security · GHSA-7q4h-9x2m', 'Notify: security'].every((l) => readMailboxLine(MSG(l)).state === 'unreadable')); + t('⭐ `Notify: security · #n` — triage handing a public card it labelled `security` to the seat — reads the card ALONE; the card beside an id, a second field, a PR, `#0` and the bare template stay unreadable', (() => { const r = readMailboxLine(MSG('Notify: security · #29761', 'security#1')); return r.state === 'read' && r.to === 'security#1' && r.id === 'notify:security' && r.card === 29761 && r.advisory === undefined && r.numbers.join() === '29761' && ['Notify: security · #29761 · GHSA-7q4h-9x2m-5c8v', 'Notify: security · #29761 · see the card', 'Notify: security · PR #29761', 'Notify: security · #0', 'Notify: security · GHSA-ID or #n'].every((l) => readMailboxLine(MSG(l)).state === 'unreadable'); })()); t('⭐ every key of the table is named, as `Kind: key` in a code span, in the protocol text (`references/project-forms.md` 〈信箱 PR〉), and that section names NO other key — the table in both directions — and points at `--mailbox-keys` for the full spellings', (() => { const forms = readFileSync(new URL('../../.claude/skills/pm-dispatch/references/project-forms.md', import.meta.url), 'utf8'); const at = forms.indexOf('\n## 信箱 PR\n'); const section = at < 0 ? '' : forms.slice(at + 1).split(/\n## /)[0]; const named = new Set([...section.matchAll(/`(Request|Notify): ([a-z-]+)`/g)].map((m) => `${m[1]}: ${m[2]}`)); const table = new Set(MAILBOX_KEYS.map((k) => `${k.kind}: ${k.key}`)); return section !== '' && named.size === table.size && [...table].every((k) => named.has(k)) && section.includes('--mailbox-keys'); })()); t('⭐ every template, sent VERBATIM, is not an event — a placeholder (`#n`, `SHA`, `LANE#N`, `p0 or p1`, `TRIGGER_ID`, `ID`) never reads as one', MAILBOX_KEYS.every((k) => readMailboxLine(MSG(k.template)).state === 'unreadable')); t('⭐ `--mailbox-keys` prints exactly `mailboxKeyLines()` — every template, with its row under it — and exits 0', (() => { const r = faceCli(['--mailbox-keys']); return r.status === 0 && r.stdout === `${mailboxKeyLines().join('\n')}\n` && MAILBOX_KEYS.every((k) => r.stdout.includes(`${k.template}\n ${k.from} → ${k.to}; ${k.when}\n`)) && usageLines().join('\n').includes('--mailbox-keys'); })());