From 87f02db9c0047e79179d5f76579120656a5a5726 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 02:03:37 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(pm):=20key=20the=20mailbox=20by=20seat?= =?UTF-8?q?=20=E2=80=94=20mailbox-open=20opens=20one=20per=20seat,=20fleet?= =?UTF-8?q?-roster=20reads=20it=20per=20seat=20row?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One account routinely holds several seats, and a PR's events reach one subscribed session at a time, so a mailbox keyed by login wakes one of that account's seats and the others never hear. mailbox-open.mjs: `--seat LANE#N` is required to open. The branch is `mailbox//`, the body opens `Mailbox-of: · `, and step ① refuses a second open mailbox for the same SEAT, whichever login holds it; a second seat under one login opens freely. A legacy per-login mailbox blocks nothing and is named so the account closes it. The header's "a login holds one seat" argument is rewritten, not patched. `--close`, the labels, the ledger exclusion and the `--head` cloud path are unchanged. fleet-roster.mjs: `mailboxLogin` gives way to the opener's `mailboxOwner`, read per seat row. A legacy per-login mailbox, or a seat no post carries, is flagged under the table, never attributed to the login's seats. This also aligns the reader with the writer: the opener wrote `To: ` while the roster read `Mailbox-of:`, so every `--head` mailbox was invisible to the mailbox column. Claude-Session: https://claude.ai/code/session_016WV4d1d1pooSGzvbJymLKy Co-authored-by: Claude --- scripts/pm/fleet-roster.fixture.json | 6 +- scripts/pm/fleet-roster.mjs | 94 ++++++--- scripts/pm/mailbox-open.mjs | 284 +++++++++++++++++++-------- 3 files changed, 273 insertions(+), 111 deletions(-) diff --git a/scripts/pm/fleet-roster.fixture.json b/scripts/pm/fleet-roster.fixture.json index 4d0b09043bc..7469d654808 100644 --- a/scripts/pm/fleet-roster.fixture.json +++ b/scripts/pm/fleet-roster.fixture.json @@ -23,7 +23,11 @@ ], "pulls": [ { "number": 25427, "state": "open", "head": { "ref": "claude/project-thread-aaaa" }, "body": "Fixes #25412", "labels": [] }, - { "number": 25392, "state": "open", "head": { "ref": "claude/project-thread-bbbb" }, "body": "Mailbox-of: os-elon-musk\n\nFixes #25416", "labels": [{ "name": "pm:mailbox" }, { "name": "skip-changeset" }] }, + { "number": 25392, "state": "open", "head": { "ref": "claude/project-thread-bbbb" }, "body": "Mailbox-of: domain:devx#1 · os-elon-musk\n\nFixes #25416", "labels": [{ "name": "pm:mailbox" }, { "name": "skip-changeset" }] }, + { "number": 25393, "state": "open", "head": { "ref": "mailbox/os-elon-musk/skills-2" }, "body": "Mailbox-of: domain:skills#2 · os-elon-musk\n\nThe mailbox of the seat `domain:skills#2`", "labels": [{ "name": "pm:mailbox" }, { "name": "pinned" }] }, + { "number": 25394, "state": "open", "head": { "ref": "mailbox/os-sales/devx-2" }, "body": "", "labels": [{ "name": "pm:mailbox" }] }, + { "number": 25395, "state": "open", "head": { "ref": "claude/project-thread-eeee" }, "body": "To: os-sales\n\nThe mailbox of the seat held by `os-sales`", "labels": [{ "name": "pm:mailbox" }, { "name": "pinned" }] }, + { "number": 25396, "state": "open", "head": { "ref": "claude/project-thread-ffff" }, "body": "Mailbox-of: domain:spec#5 · os-tesla", "labels": [{ "name": "pm:mailbox" }] }, { "number": 25433, "state": "open", "head": { "ref": "claude/project-thread-cccc" }, "body": "Fixes #25417", "labels": [] } ], "comments": { diff --git a/scripts/pm/fleet-roster.mjs b/scripts/pm/fleet-roster.mjs index db8d104f664..ef558f6ba82 100644 --- a/scripts/pm/fleet-roster.mjs +++ b/scripts/pm/fleet-roster.mjs @@ -52,8 +52,16 @@ * last activity the newest `updated_at` of the post and its in-flight * cards. A seated seat older than `STALE_HOURS` with no * wall note is flagged 「疑似撞墙」. - * mailbox open `pm:mailbox` pulls of the seat's account, by head - * `mailbox/` or the body's `Mailbox-of: `. + * mailbox open `pm:mailbox` pulls of the SEAT, whichever account + * holds it: the seat on the body's first line + * `Mailbox-of: · `, or the head + * `mailbox//` (`mailboxOwner`, the + * opener's one reading). A legacy per-login mailbox names + * no seat, and an account holds several seats, so which of + * them reads it cannot be told: it is listed under the + * table as assigned to no seat, ⛔ never shown on the + * login's rows. So is a per-seat one whose seat no post + * carries. * * The footer names the generation time and every read made, so a reader can * re-run each one. @@ -88,10 +96,11 @@ import { fileURLToPath } from 'node:url'; import { isEntrypoint } from '../invoked-as.mjs'; import { validateStroke } from './fleet-write/validate.mjs'; import { ghApi, PER_PAGE, readAllPages } from './gh-pages.mjs'; -// The mailbox label, its ledger exclusion and its branch prefix have ONE owner, -// the mailbox opener (#25417); this file reads them, never re-spells them. +// The mailbox label, its ledger exclusion, its branch prefix and the reading of +// whose mailbox a pull is have ONE owner, the mailbox opener (#25417, keyed by +// 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 } from './mailbox-open.mjs'; +import { LEDGER_EXCLUSION, MAILBOX_BRANCH_PREFIX, MAILBOX_LABEL, mailboxOwner, seatKey } from './mailbox-open.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'; @@ -232,12 +241,30 @@ export function hasLandingVerdict(comments) { }); } -/** The login whose mailbox a `pm:mailbox` pull is, or null. Pure. */ -export function mailboxLogin(pr) { - if (!labelNames(pr).includes(MAILBOX_LABEL)) return null; - const ref = String(pr?.head?.ref ?? ''); - if (ref.startsWith(MAILBOX_BRANCH_PREFIX)) return ref.slice(MAILBOX_BRANCH_PREFIX.length); - return /^Mailbox-of: (\S+)/m.exec(String(pr?.body ?? ''))?.[1] ?? null; +/** A seat row's mailbox slug — its `LANE#N` as the opener spells it in a ref — or null. Pure. */ +const seatSlugOf = (row) => seatKey(`${row.lane}#${row.seat}`)?.slug ?? null; + +/** Every open `pm:mailbox` pull in the snapshot, with whose it is (`mailboxOwner`), ordered by ref. Pure. */ +export function readMailboxes(snapshot) { + const out = []; + for (const repo of Object.keys(snapshot?.repos ?? {}).sort()) { + for (const pr of snapshot.repos[repo]?.pulls ?? []) { + const owner = pr?.state === 'closed' ? null : mailboxOwner(pr); + if (owner !== null) out.push({ ref: ref(repo, pr.number), ...owner }); + } + } + return out.sort((a, b) => byRef(a.ref, b.ref)); +} + +/** + * The mailboxes no seat row reads — flagged under the table, ⛔ never attributed: a legacy per-login mailbox + * (it names no seat, and its login holds several), or a per-seat one whose seat no seat post carries. Pure. + */ +export function unassignedMailboxes(snapshot, rows) { + const read = new Set(rows.map(seatSlugOf).filter(Boolean)); + return readMailboxes(snapshot) + .filter((m) => m.slug === null || !read.has(m.slug)) + .map((m) => ({ ...m, why: m.slug === null ? 'legacy per-login, names no seat' : 'no seat post carries this seat' })); } const cardLaneMatch = (card, row) => { @@ -275,16 +302,12 @@ export function buildRows(snapshot, { now }) { rows.sort((a, b) => (a.repo !== b.repo ? (a.repo < b.repo ? -1 : 1) : a.lane !== b.lane ? (a.lane < b.lane ? -1 : 1) : a.seat !== b.seat ? a.seat - b.seat : a.post - b.post)); const landing = new Map(); // card key → [pull refs] - const mailboxes = []; for (const { repo, pr, comments } of pulls) { - const owner = mailboxLogin(pr); - if (owner !== null) { - mailboxes.push({ login: lc(owner), ref: ref(repo, pr.number) }); - continue; - } - if (labelNames(pr).includes(MAILBOX_LABEL) || !String(pr?.head?.ref ?? '').startsWith(CARD_BRANCH_PREFIX) || !hasLandingVerdict(comments)) continue; + // Every `pm:mailbox` pull is a mailbox, whosever it reads as, and ⛔ never counts as landing. + if (mailboxOwner(pr) !== null || !String(pr?.head?.ref ?? '').startsWith(CARD_BRANCH_PREFIX) || !hasLandingVerdict(comments)) continue; for (const k of closingRefs(pr.body, repo)) landing.set(k, [...(landing.get(k) ?? []), ref(repo, pr.number)]); } + const mailboxes = readMailboxes(snapshot); const out = rows.map((row) => { const acct = lc(row.account); @@ -304,7 +327,8 @@ export function buildRows(snapshot, { now }) { awaitingLanding: awaitingLanding.sort(byRef), lastActivity, suspectedWall: row.status === 'seated' && row.wallNote === null && ageHours !== null && ageHours > STALE_HOURS, - mailbox: row.account ? mailboxes.filter((m) => m.login === acct).map((m) => m.ref).sort(byRef) : [], + // Per seat, never per account: one account holds several seats, and each seat opens its own. + mailbox: mailboxes.filter((m) => m.slug !== null && m.slug === seatSlugOf(row)).map((m) => m.ref), }; }); @@ -341,8 +365,8 @@ export function readsFor(repos) { ]); } -/** The roster body, markdown. Pure. */ -export function renderBody(rows, { now, repos, session = null }) { +/** The roster body, markdown; `unassigned` is `unassignedMailboxes`' list. Pure. */ +export function renderBody(rows, { now, repos, session = null, unassigned = [] }) { const lines = [ ROSTER_MARKER, '# 车队总表 — fleet roster', @@ -365,6 +389,9 @@ export function renderBody(rows, { now, repos, session = null }) { lines.push( '', `${rows.length} seat posts · ${seated.length} seated · ${rows.filter((r) => r.suspectedWall).length} ${SUSPECTED_WALL} (seated, no \`Wall:\` line, last activity older than ${STALE_HOURS} h).`, + ...(unassigned.length + ? ['', `⚠️ ${unassigned.length} mailbox PR(s) on no seat row — never attributed to an account's seats; close a legacy one and open one per seat: ${unassigned.map((m) => `${shortRef(m.ref)} (${[m.seat ?? m.slug, m.login].filter(Boolean).join(' · ') || 'no owner'}: ${m.why})`).join(' · ')}.`] + : []), '', `Generated ${now} by \`node scripts/pm/fleet-roster.mjs --repos ${repos.join(',')}\` from:`, ...readsFor(repos).map((q) => `- \`${q}\``), @@ -424,7 +451,7 @@ function readLive(repos) { const dispatched = new Set(Object.entries(snapshot.repos).flatMap(([repo, s]) => s.dispatched.map((i) => `${repo}#${i.number}`))); for (const [repo, s] of Object.entries(snapshot.repos)) { for (const p of s.pulls) { - if (mailboxLogin(p) !== null || labelNames(p).includes(MAILBOX_LABEL) || !String(p.head?.ref ?? '').startsWith(CARD_BRANCH_PREFIX)) continue; + if (mailboxOwner(p) !== null || !String(p.head?.ref ?? '').startsWith(CARD_BRANCH_PREFIX)) continue; if (!closingRefs(p.body, repo).some((k) => dispatched.has(k))) continue; s.comments[String(p.number)] = need(`/repos/${repo}/issues/${p.number}/comments?per_page=100`); } @@ -478,12 +505,13 @@ export async function main(argv, { env = process.env } = {}) { } const now = new Date(opts.now ?? snapshot.now ?? Date.now()).toISOString().replace(/\.\d{3}Z$/, 'Z'); const rows = buildRows(snapshot, { now }); + const unassigned = unassignedMailboxes(snapshot, rows); if (opts.json) { - console.log(JSON.stringify({ now, repos, rows }, null, 2)); + console.log(JSON.stringify({ now, repos, rows, unassignedMailboxes: unassigned }, null, 2)); return EXIT_OK; } const session = env.OS_FLEET_SESSION || env.CLAUDE_CODE_REMOTE_SESSION_ID || null; - const body = renderBody(rows, { now, repos, session }); + const body = renderBody(rows, { now, repos, session, unassigned }); if (opts.write === null) { console.log(body); return EXIT_OK; @@ -523,10 +551,10 @@ const SELF_TEST_BATTERIES = Object.freeze({ 'arguments: modes and every refusal': 9, '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': 15, - 'the rendered body: marker, columns, footer, determinism': 8, + 'the fixture snapshot: rows for the three repos': 17, + 'the rendered body: marker, columns, footer, determinism': 9, 'refusing to overwrite what is not the roster': 6, - 'the CLI: fixture runs offline, usage errors are exit 2': 5, + 'the CLI: fixture runs offline, usage errors are exit 2': 6, 'paging: a three-page read through the loop equals the slurped array': 4, }); const SELF_TEST_BATTERY_FLOOR = 8; @@ -601,7 +629,7 @@ async function selfTest() { t('⛔ an ACCEPT line ABOVE the heading is not the record\'s verdict', hasLandingVerdict([{ body: 'ACCEPT\n\n## 席位核对\n\nREWORK · rework' }]), false); t('the heading is H69\'s marker, imported not re-spelled: the three measured bodies read record alike', ['## 席位核对\n\n**ACCEPT** · x', '## 席位核对 — ACCEPT · x', '## Contract review\n\n**VERDICT: PASS**'].map((b) => `${seatRecord.SEAT_RECORD_MARKER.test(b)}/${hasLandingVerdict([{ body: b }])}`), ['true/true', 'true/true', 'false/true']); t('the mailbox constants are the opener\'s own, imported not re-spelled', [MAILBOX_LABEL, LEDGER_EXCLUSION, MAILBOX_BRANCH_PREFIX], [mailboxOpen.MAILBOX_LABEL, mailboxOpen.LEDGER_EXCLUSION, mailboxOpen.MAILBOX_BRANCH_PREFIX]); - t('a mailbox is read by head, then by body line; no label ⇒ none', [mailboxLogin({ labels: ['pm:mailbox'], head: { ref: 'mailbox/os-bill' } }), mailboxLogin({ labels: ['pm:mailbox'], head: { ref: 'claude/x' }, body: 'Mailbox-of: os-sales\n' }), mailboxLogin({ labels: [], head: { ref: 'mailbox/os-bill' } })], ['os-bill', 'os-sales', null]); + t('whose mailbox a pull is, and a seat row\'s slug, are the opener\'s own readings — imported, not re-spelled', [mailboxOwner === mailboxOpen.mailboxOwner, seatKey === mailboxOpen.seatKey, seatSlugOf({ lane: 'domain:ui', seat: 4 }), seatSlugOf({ lane: 'triage', seat: 1 })], [true, true, 'ui-4', 'triage-1']); battery('the fixture snapshot: rows for the three repos'); const fixture = JSON.parse(readFileSync(FIXTURE_PATH, 'utf8')); @@ -628,7 +656,10 @@ async function selfTest() { t('a PR with no verdict does not count', row(1353, 'objectstack-ai/hotcrm').awaitingLanding, []); t('an unassigned ruling sits on the lane’s lowest seated seat', [row(6023).awaitingRuling, row(20163).awaitingRuling], [[{ repo: 'objectstack-ai/objectstack', number: 25373 }], []]); t('a ruling assigned to seat 2’s account sits on seat 2', row(19287).awaitingRuling, [{ repo: 'objectstack-ai/objectstack', number: 25344 }]); - t('the mailbox column is the account’s pm:mailbox PR', [row(6023).mailbox, row(20163).mailbox], [[{ repo: 'objectstack-ai/objectstack', number: 25392 }], []]); + const os = (number) => ({ repo: 'objectstack-ai/objectstack', number }); + t('⭐ the mailbox column is the SEAT\'s pm:mailbox PR — by body line on a --head branch, by head slug, by both', [row(6023).mailbox, row(19287).mailbox, row(20163).mailbox], [[os(25392)], [os(25393)], [os(25394)]]); + t('⛔ one login on three seats: each seat shows only its own mailbox, and a seat that opened none shows none', [row(6023).mailbox.length, row(19287).mailbox.length, row(1353, 'objectstack-ai/hotcrm').mailbox], [1, 1, []]); + t('⛔ a legacy per-login mailbox and a seat no post carries sit on no row — flagged, never attributed to the login\'s seats', [unassignedMailboxes(fixture, rows).map((m) => [m.ref.number, m.seat, m.login, m.why]), [row(5560, 'objectstack-ai/objectui').mailbox, row(10243, 'objectstack-ai/objectui').mailbox]], [[[25395, null, 'os-sales', 'legacy per-login, names no seat'], [25396, 'domain:spec#5', 'os-tesla', 'no seat post carries this seat']], [[], []]]); t('stale and no wall note ⇒ 疑似撞墙; a wall note silences it; vacant never', [row(20163).suspectedWall, row(5560, 'objectstack-ai/objectui').suspectedWall, row(5560, 'objectstack-ai/objectui').wallNote, row(7623).suspectedWall], [true, false, 'limit reached, resets at 15:00Z', false]); t('last activity is the newest of post and in-flight cards', row(6023).lastActivity, '2026-10-23T13:28:00Z'); t('one account on two seats of a lane: cards on the lowest seat, the other reads = seat 1', [row(5560, 'objectstack-ai/objectui').inFlight.length, row(10243, 'objectstack-ai/objectui').sharesWith, row(10243, 'objectstack-ai/objectui').inFlight, row(10243, 'objectstack-ai/objectui').suspectedWall], [1, 1, [], true]); @@ -641,6 +672,10 @@ async function selfTest() { t('the footer names the generation time and each read', [body.includes(`Generated ${fixture.now}`), body.includes('`GET /repos/objectstack-ai/hotcrm/issues?state=open&labels=pm:seat`')], [true, true]); t('the suspected wall is spelled in the row', body.includes(`⚠️ ${SUSPECTED_WALL}`), true); t('a shared seat reads = seat N in the in-flight cell', body.includes('| = seat 1 |'), true); + t('the mailboxes on no seat row are flagged under the table, each with its owner and why', (() => { + const flagged = renderBody(rows, { now: fixture.now, repos: Object.keys(fixture.repos).sort(), unassigned: unassignedMailboxes(fixture, rows) }); + return [flagged.includes('⚠️ 2 mailbox PR(s) on no seat row'), flagged.includes('objectstack#25395 (os-sales: legacy per-login, names no seat)'), flagged.includes('objectstack#25396 (domain:spec#5 · os-tesla: no seat post carries this seat)'), body.includes('on no seat row')]; + })(), [true, true, true, false]); t('the attribution footer closes the body', body.endsWith('\n\n---\n_Generated by [Claude Code](https://claude.ai/code/session_01ABC)_'), true); const shuffled = { ...fixture, repos: Object.fromEntries(Object.entries(fixture.repos).reverse().map(([k, v]) => [k, { ...v, seats: [...v.seats].reverse() }])) }; t('the same facts in another read order render the same body', renderBody(buildRows(shuffled, { now: fixture.now }), { now: fixture.now, repos: Object.keys(fixture.repos).sort() }), renderBody(rows, { now: fixture.now, repos: Object.keys(fixture.repos).sort() })); @@ -683,6 +718,7 @@ async function selfTest() { } catch {} t('--fixture --json exits 0 with no gh on PATH (nothing reached for)', jsonRun.status, EXIT_OK); t('the printed rows are the built rows', parsed?.rows?.length, rows.length); + t('the printed JSON carries the mailboxes on no seat row', parsed?.unassignedMailboxes?.map((m) => m.ref.number), [25395, 25396]); t('--fixture prints the markdown table', cli(['--fixture', FIXTURE_PATH]).stdout.split('\n')[0], ROSTER_MARKER); t('a usage error is exit 2', cli(['--write', 'x']).status, EXIT_USAGE); t('a missing fixture is exit 3', cli(['--fixture', join(PM_DIR, 'no-such-fixture.json')]).status, EXIT_PREREQUISITE); diff --git a/scripts/pm/mailbox-open.mjs b/scripts/pm/mailbox-open.mjs index d72332eebda..7d91cd8a611 100644 --- a/scripts/pm/mailbox-open.mjs +++ b/scripts/pm/mailbox-open.mjs @@ -2,10 +2,10 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * mailbox-open — open (or close) a seat's mailbox PR (#25417; one per seat since the per-seat doorbell). + * mailbox-open — open (or close) a seat's mailbox PR (#25417; keyed by the seat, not the login, since #27637). * - * node scripts/pm/mailbox-open.mjs --repo owner/name --login [--base main] [--dry-run] [--json] - * node scripts/pm/mailbox-open.mjs --repo owner/name --login --head [--dry-run] + * node scripts/pm/mailbox-open.mjs --repo owner/name --seat --login [--base main] [--dry-run] [--json] + * node scripts/pm/mailbox-open.mjs --repo owner/name --seat --login --head [--dry-run] * node scripts/pm/mailbox-open.mjs --repo owner/name --close [--dry-run] * node scripts/pm/mailbox-open.mjs --self-test # offline, no network, no git * @@ -17,32 +17,59 @@ * ~70 s, a comment from a second account in 4 s. An issue comment wakes * nothing. So each SEAT — a lane seat, the triage seat, the director seat — * keeps ONE draft PR as its mailbox: labelled `pm:mailbox` and `pinned`, on - * the branch `mailbox/` (the account that holds the seat), its body's - * first line `To: `, opened at seating and subscribed by the seated - * session, ⛔ never merged, closed at hand-over or account switch (the - * successor opens its own). A comment on it is ONE line opening with one of - * the seven doorbell keys (`Request:` / `Notify:`), read by - * `record-recognisers.mjs`. The protocol text — the keys, who sends what to - * whom — is the pm-dispatch skill's `references/project-forms.md` 〈信箱 PR〉; - * this file is only the tooling. + * the branch `mailbox//` (`mailbox/os-bill/ui-4` for the + * seat `domain:ui#4` held by `os-bill`), its body's first line + * `Mailbox-of: · `, opened at seating and subscribed by the + * seated session, ⛔ never merged, closed at hand-over or account switch (the + * successor opens its own). A message on it is ONE comment: the line + * `To: `, then ONE line opening with one of the seven doorbell keys + * (`Request:` / `Notify:`), read by `record-recognisers.mjs`. The protocol + * text — the keys, who sends what to whom — is the pm-dispatch skill's + * `references/project-forms.md` 〈信箱 PR〉; this file is only the tooling. + * + * ## The key is the seat, never the login + * + * One account routinely holds several seats — on 2026-10-29 one login held two + * `domain:server` seats and `domain:ui#1` behind ONE mailbox, and another held + * three `domain:ui` seats and none — while the platform delivers a PR's events + * to one subscribed session at a time: a second session's subscription + * succeeds and then hears nothing while the first holds the PR. A mailbox keyed + * by login therefore wakes one of its account's seats, the others never hear, + * and nothing tells the sender or the recipient. An earlier version of this + * header argued that a login holds one seat at a time, so a per-login refusal + * was a per-seat one; the board falsified that premise, and nothing here reads + * a login as a seat any more. + * + * A seat is spelled `LANE#N` — `domain:ui#4`, `repo:cloud#1`, `triage` — with + * an absent number read as seat 1 (`seatKey`). It is compared lower-cased and + * written canonically with its number (`triage#1`). Its SLUG is that key as one + * ref segment, because a ref cannot carry `:` or `#`: the default `domain:` + * kind dropped, every other `:` and the `#` turned into `-` (`ui-4`, + * `repo-cloud-1`, `triage-1`). No live lane is named both `domain:X` and `X`, + * which is what keeps the slug one-to-one. * * ## What it does, in order (open) * * ① READ the open pulls and REFUSE (exit 4) when one already carries - * `pm:mailbox` for this login — by head `mailbox/` or by the body's - * `To: ` line. One mailbox per login per repo, which IS one per - * seat: a login holds one seat at a time, and a lane's second seat is a - * second login (measured 2026-10-28 on the seat posts, so this refusal - * needed no seat name). + * `pm:mailbox` for this SEAT, whichever login holds it — by the seat on + * the body's `Mailbox-of:` line or by a head `mailbox//` + * (`mailboxOwner`). A second seat under the same login opens freely. A + * legacy per-login mailbox (head `mailbox/`, or a body opening + * `To: `) names no seat, so it blocks nothing; the open prints it, + * so that the account closes it once each of its seats holds its own. * ② the BRANCH. Default: one empty commit on `origin/` (`git * commit-tree` of the base's own tree — no checkout, no index, no working - * tree touched), pushed as `mailbox/` through `with-fleet.sh - * --kind 'git push'`, the write gate every push goes through. The relay - * has no ref-creating op, so a cloud seat container cannot do this step: - * with-fleet refuses there (exit 3) and nothing else is sent. `--head - * ` names a branch that already exists instead (a thread's own - * pushed branch, as #25392 used) and skips the push; it must be ahead of - * ``, because GitHub refuses a pull with no commits. + * tree touched), pushed as `mailbox//` through `with-fleet.sh + * --kind 'git push'`, the write gate every push goes through. ⚠️ A legacy + * branch `mailbox/` blocks that push: git refuses a ref nested + * under an existing ref (measured 2026-10-30 on a local bare repository: + * `failed to update ref`), so the legacy branch is deleted after its + * mailbox closes. The relay has no ref-creating op, so a cloud seat + * container cannot do this step: with-fleet refuses there (exit 3) and + * nothing else is sent. `--head ` names a branch that already + * exists instead (a thread's own pushed branch, as #25392 used) and skips + * the push; it must be ahead of ``, because GitHub refuses a pull + * with no commits. * ③ ONE relay stroke: `pr_create` (draft is forced by the relay) with labels * `pm:mailbox`, `pinned` and `skip-changeset`, sent through * `fleet-write/dispatch.mjs` as `objectstack-fleet[bot]`. `pinned` is @@ -129,15 +156,33 @@ const REPO_SHAPE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/; const LOGIN_SHAPE = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/; const BRANCH_SHAPE = /^(?!\/|.*\/\/|.*\.\.|.*[\s~^:?*[\\]|.*\.lock$|.*\/$)[A-Za-z0-9._/-]+$/; const SESSION_SHAPE = /^(?:session|cse)_[A-Za-z0-9]+$/; +// A seat: its lane (`domain:ui`, `repo:cloud`, `triage`, `director`), then `#N`; absent = seat 1. Read lower-cased. +const SEAT_SHAPE = /^([a-z]+(?::[a-z0-9._-]+)?)(?:#([1-9][0-9]*))?$/; +/** A per-seat mailbox body's first line, `Mailbox-of: · `. */ +const MAILBOX_OF_LINE = /^Mailbox-of:[ \t]*(\S+)[ \t]*·[ \t]*(\S+)$/; +/** The first lines a legacy per-login mailbox opened with: `To: ` (this file's), `Mailbox-of: `. */ +const LEGACY_OWNER_LINE = /^(?:To|Mailbox-of):[ \t]*(\S+)$/; + +/** + * A seat spelled `LANE#N` → `{ key, slug }`, or null when it is not one. `key` is canonical — lower case, the + * number always spelled (`triage` → `triage#1`); `slug` is the key as one ref segment (header). Pure. + */ +export function seatKey(text) { + const m = SEAT_SHAPE.exec(String(text ?? '').trim().toLowerCase()); + if (!m) return null; + const n = m[2] ?? '1'; + return { key: `${m[1]}#${n}`, slug: `${m[1].replace(/^domain:/, '').replace(/:/g, '-')}-${n}` }; +} -export const mailboxBranch = (login) => `${MAILBOX_BRANCH_PREFIX}${login}`; +/** `mailbox//`; `seat` is a `seatKey` reading. */ +export const mailboxBranch = (login, seat) => `${MAILBOX_BRANCH_PREFIX}${login}/${seat.slug}`; // --------------------------------------------------------------------------- // The pure core // --------------------------------------------------------------------------- export function parseArgs(argv) { - const opts = { repo: null, login: null, head: null, base: 'main', close: null, dryRun: false, json: false, selfTest: false, help: false, errors: [] }; + const opts = { repo: null, seat: null, login: null, head: null, base: 'main', close: null, dryRun: false, json: false, selfTest: false, help: false, errors: [] }; const args = [...argv]; const value = (flag) => { const v = args.shift(); @@ -154,6 +199,7 @@ export function parseArgs(argv) { else if (a === '--dry-run') opts.dryRun = true; else if (a === '--json') opts.json = true; else if (a === '--repo') opts.repo = value(a); + else if (a === '--seat') opts.seat = value(a); else if (a === '--login') opts.login = value(a); else if (a === '--head') opts.head = value(a); else if (a === '--base') opts.base = value(a); @@ -168,9 +214,14 @@ export function parseArgs(argv) { if (opts.close !== null) { if (Number.isNaN(opts.close)) opts.errors.push('--close takes a pull number'); if (opts.head !== null) opts.errors.push('--close takes no --head'); + if (opts.seat !== null) opts.errors.push('--close takes no --seat'); } else { + const seat = opts.seat === null ? null : seatKey(opts.seat); + if (!opts.seat) opts.errors.push('--seat is required to open a mailbox — the mailbox is the seat\'s, never the login\'s'); + else if (!seat) opts.errors.push(`--seat ${JSON.stringify(opts.seat)} is not a seat — spell it LANE#N, e.g. domain:ui#4`); if (!opts.login) opts.errors.push('--login is required to open a mailbox'); else if (!LOGIN_SHAPE.test(opts.login)) opts.errors.push(`--login ${JSON.stringify(opts.login)} is not a GitHub login`); + else if (seat && opts.head === null && !BRANCH_SHAPE.test(mailboxBranch(opts.login, seat))) opts.errors.push(`the seat's branch ${JSON.stringify(mailboxBranch(opts.login, seat))} is not a legal ref`); if (opts.head !== null && !BRANCH_SHAPE.test(opts.head)) opts.errors.push(`--head ${JSON.stringify(opts.head)} is not a branch name`); if (opts.base !== null && !BRANCH_SHAPE.test(opts.base)) opts.errors.push(`--base ${JSON.stringify(opts.base)} is not a branch name`); } @@ -183,25 +234,28 @@ export function bodyFooter(session) { return `\n\n---\n_Generated by [Claude Code](https://claude.ai/code${id ? `/${id}` : ''})_`; } -export function mailboxBody({ login, session = null }) { +/** `seat` is the canonical key (`seatKey(...).key`). */ +export function mailboxBody({ seat, login, session = null }) { return [ - `To: ${login}`, + `Mailbox-of: ${seat} · ${login}`, '', - `The mailbox of the seat held by \`${login}\` (\`${MAILBOX_LABEL}\`): one per seat, opened at seating, subscribed by the seated session.`, + `The mailbox of the seat \`${seat}\`, held by \`${login}\` (\`${MAILBOX_LABEL}\`): one per seat, opened at seating, subscribed by the seated session.`, '', '- ⛔ Never merged, never readied. Closed at hand-over or account switch: `node scripts/pm/mailbox-open.mjs --close N` (N = this number); the successor opens its own.', - '- A message is ONE comment whose first line is one of the seven doorbell keys (`Request:` / `Notify:`; pm-dispatch `references/project-forms.md` 〈信箱 PR〉): one event, one line, keys and numbers only. Nothing else is allowed here — no acknowledgements, no re-sends.', + `- A message is ONE comment: the line \`To: ${seat}\`, then ONE line opening with one of the seven doorbell keys (\`Request:\` / \`Notify:\`; pm-dispatch \`references/project-forms.md\` 〈信箱 PR〉): one event, keys and numbers only. Nothing else is allowed here — no acknowledgements, no re-sends.`, `- **CI: owes none.** \`skip-changeset\` exempts Check Changeset; any other check red on this empty diff is RED BY DESIGN — never fix, rerun or report it. \`${STALE_EXEMPT_LABEL}\` keeps the stale sweep off it.`, `- Excluded from every landing ledger: queries add \`${LEDGER_EXCLUSION}\`.`, ].join('\n') + bodyFooter(session); } -export function planOpen({ repo, login, head = null, base = 'main', session = null }) { - const branch = head ?? mailboxBranch(login); +export function planOpen({ repo, seat, login, head = null, base = 'main', session = null }) { + const s = seatKey(seat); + if (!s) return { kind: 'open', repo, seat: null, login, branch: head, base, push: false, actions: [], errors: [`${JSON.stringify(seat)} is not a seat`] }; + const branch = head ?? mailboxBranch(login, s); const actions = [ - { op: 'pr_create', title: `pm:mailbox — ${login}`, head: branch, base, body: mailboxBody({ login, session }), labels: [...PR_LABELS] }, + { op: 'pr_create', title: `pm:mailbox — ${s.key} · ${login}`, head: branch, base, body: mailboxBody({ seat: s.key, login, session }), labels: [...PR_LABELS] }, ]; - return { kind: 'open', repo, login, branch, base, push: head === null, actions, errors: validateStroke(repo, actions) }; + return { kind: 'open', repo, seat: s, login, branch, base, push: head === null, actions, errors: validateStroke(repo, actions) }; } export function planClose({ repo, pull }) { @@ -213,16 +267,42 @@ export function planClose({ repo, pull }) { } const labelNames = (pr) => (Array.isArray(pr?.labels) ? pr.labels.map((l) => (typeof l === 'string' ? l : l?.name)) : []); -/** The body's first line, `To: ` — the addressee, which is the seat the mailbox belongs to. */ -const addresseeOf = (pr) => /^To: (\S+)/.exec(typeof pr?.body === 'string' ? pr.body : '')?.[1] ?? null; +const firstLine = (text) => String(text ?? '').split(/\r?\n/).find((l) => l.trim() !== '')?.trim() ?? ''; -/** The open `pm:mailbox` pulls that belong to `login`, by head branch or by the body's `To:` line. Pure. */ -export function openMailboxesOf(pulls, login) { +/** + * Whose mailbox a pull is — `{ seat, slug, login }` — or null when it carries no `pm:mailbox`. A per-seat mailbox + * names its seat on the body's first line `Mailbox-of: · ` (`seat` is then the canonical key), or + * only by its head `mailbox//` (`seat` null, `slug` set). A legacy per-login mailbox — head + * `mailbox/`, or a first line `To: ` / `Mailbox-of: ` — names a login and NO seat, so its + * `slug` is null: unassigned to a seat, ⛔ never attributed to the login's seats. A `pm:mailbox` pull that names + * neither reads `{ seat: null, slug: null, login: null }`. The body outranks the head. The one reading of a + * mailbox's owner: `fleet-roster.mjs` imports it, never re-spells it. Pure. + */ +export function mailboxOwner(pr) { + if (!labelNames(pr).includes(MAILBOX_LABEL)) return null; + const first = firstLine(pr?.body); + const perSeat = MAILBOX_OF_LINE.exec(first); + const seat = perSeat && LOGIN_SHAPE.test(perSeat[2]) ? seatKey(perSeat[1]) : null; + if (seat) return { seat: seat.key, slug: seat.slug, login: perSeat[2] }; + const ref = String(pr?.head?.ref ?? ''); + const parts = ref.startsWith(MAILBOX_BRANCH_PREFIX) ? ref.slice(MAILBOX_BRANCH_PREFIX.length).split('/') : []; + if (parts.length === 2 && LOGIN_SHAPE.test(parts[0]) && parts[1] !== '') return { seat: null, slug: parts[1].toLowerCase(), login: parts[0] }; + const legacy = LEGACY_OWNER_LINE.exec(first)?.[1] ?? (parts.length === 1 ? parts[0] : null); + return { seat: null, slug: null, login: legacy !== null && LOGIN_SHAPE.test(legacy) ? legacy : null }; +} + +const openMailboxes = (pulls) => + (Array.isArray(pulls) ? pulls : []).filter((pr) => pr?.state === 'open').map((pr) => ({ pr, owner: mailboxOwner(pr) })).filter((m) => m.owner !== null); + +/** The open mailboxes of `seat` (a `seatKey` reading), whichever login holds them — matched by slug. Pure. */ +export function openMailboxesOf(pulls, seat) { + return openMailboxes(pulls).filter((m) => m.owner.slug === seat.slug).map((m) => m.pr); +} + +/** The open legacy per-login mailboxes of `login`: they name no seat, so they block nothing — the open names them. Pure. */ +export function legacyMailboxesOf(pulls, login) { const want = String(login).toLowerCase(); - return (Array.isArray(pulls) ? pulls : []).filter((pr) => { - if (pr?.state !== 'open' || !labelNames(pr).includes(MAILBOX_LABEL)) return false; - return String(pr?.head?.ref ?? '').toLowerCase() === mailboxBranch(want) || String(addresseeOf(pr) ?? '').toLowerCase() === want; - }); + return openMailboxes(pulls).filter((m) => m.owner.slug === null && String(m.owner.login ?? '').toLowerCase() === want).map((m) => m.pr); } /** Why `pr` may not be closed as a mailbox, or null when it may. Pure. */ @@ -236,7 +316,7 @@ export function closeRefusal(pr, pull) { export function planText(plan) { const lines = [`mailbox-open: DRY RUN — nothing read, nothing pushed, nothing sent (${plan.repo}).`]; if (plan.kind === 'open') { - lines.push(` ① read open pulls; refuse if a ${MAILBOX_LABEL} PR for ${plan.login} is open`); + lines.push(` ① read open pulls; refuse if a ${MAILBOX_LABEL} PR for the seat ${plan.seat.key} is open, whoever holds it; name ${plan.login}'s legacy per-login one`); lines.push( plan.push ? ` ② git commit-tree origin/${plan.base}^{tree} -p origin/${plan.base} (empty commit) → with-fleet.sh --kind 'git push' → refs/heads/${plan.branch}` @@ -283,21 +363,26 @@ async function openLive(plan, opts) { console.error(`✗ mailbox-open: could not read the open pulls — ${pulls.error}. Nothing was sent.`); return EXIT_PREREQUISITE; } - const existing = openMailboxesOf(pulls.value, plan.login); + const existing = openMailboxesOf(pulls.value, plan.seat); if (existing.length) { - console.error(`✗ mailbox-open: ${plan.login} already has an open mailbox on ${plan.repo}: ${existing.map((p) => `#${p.number}`).join(', ')}. Nothing was sent.`); + const held = existing.map((p) => `#${p.number}${mailboxOwner(p).login ? ` (held by ${mailboxOwner(p).login})` : ''}`).join(', '); + console.error(`✗ mailbox-open: the seat ${plan.seat.key} already has an open mailbox on ${plan.repo}: ${held}. Nothing was sent.`); return EXIT_REFUSED; } + const legacy = legacyMailboxesOf(pulls.value, plan.login); + if (legacy.length) { + console.error(`⚠️ mailbox-open: ${plan.login} still holds a legacy per-login mailbox on ${plan.repo}: ${legacy.map((p) => `#${p.number}`).join(', ')}. It names no seat, so it blocks nothing here; close it (--close N) once each of this account's seats holds its own.`); + } if (plan.push) { const fetched = run('git', ['fetch', '--quiet', 'origin', plan.base]); - const sha = fetched.ok ? run('git', ['commit-tree', `origin/${plan.base}^{tree}`, '-p', `origin/${plan.base}`, '-m', `chore(pm): mailbox of ${plan.login}`]) : fetched; + const sha = fetched.ok ? run('git', ['commit-tree', `origin/${plan.base}^{tree}`, '-p', `origin/${plan.base}`, '-m', `chore(pm): mailbox of ${plan.seat.key} · ${plan.login}`]) : fetched; if (!sha.ok || !/^[0-9a-f]{40}$/.test(sha.out)) { console.error(`✗ mailbox-open: could not build the empty commit on origin/${plan.base}. Nothing was sent.`); return EXIT_PREREQUISITE; } const pushed = run('bash', [join(PM_DIR, 'with-fleet.sh'), '--kind', 'git push', '--', 'git', 'push', 'origin', `${sha.out}:refs/heads/${plan.branch}`]); if (!pushed.ok) { - console.error(`✗ mailbox-open: the push of ${plan.branch} was refused (with-fleet exit ${pushed.status}). A cloud seat cannot push it — pass --head . No PR was opened.`); + console.error(`✗ mailbox-open: the push of ${plan.branch} was refused (with-fleet exit ${pushed.status}). A cloud seat cannot push it — pass --head . A legacy branch ${MAILBOX_BRANCH_PREFIX}${plan.login} refuses it too: git nests no ref under an existing one, so delete that branch once its mailbox is closed. No PR was opened.`); return EXIT_PREREQUISITE; } } else { @@ -316,8 +401,8 @@ async function openLive(plan, opts) { console.error(`⚠️ mailbox-open: the stroke completed but no open pull on ${plan.branch} was read back — read the board before re-running.`); return EXIT_PREREQUISITE; } - console.log(`✓ mailbox #${pr.number} ${pr.html_url} — subscribe_pr_activity ${plan.repo} ${pr.number}`); - if (opts.json) console.log(JSON.stringify({ number: pr.number, html_url: pr.html_url, repo: plan.repo, branch: plan.branch })); + console.log(`✓ mailbox #${pr.number} of ${plan.seat.key} ${pr.html_url} — subscribe_pr_activity ${plan.repo} ${pr.number}`); + if (opts.json) console.log(JSON.stringify({ number: pr.number, html_url: pr.html_url, repo: plan.repo, seat: plan.seat.key, branch: plan.branch })); return EXIT_OK; } @@ -339,7 +424,7 @@ async function closeLive(plan, opts) { const USAGE = [ 'usage:', - ' node scripts/pm/mailbox-open.mjs --repo owner/name --login [--head ] [--base main] [--dry-run] [--json]', + ' node scripts/pm/mailbox-open.mjs --repo owner/name --seat --login [--head ] [--base main] [--dry-run] [--json]', ' node scripts/pm/mailbox-open.mjs --repo owner/name --close [--dry-run]', ' node scripts/pm/mailbox-open.mjs --self-test', ` Exits: ${EXIT_OK} done · ${EXIT_USAGE} usage or refused stroke · ${EXIT_PREREQUISITE} prerequisite · ${EXIT_REFUSED} refused on board state · 5+ the relay's own exit`, @@ -358,7 +443,7 @@ export async function main(argv, { env = process.env } = {}) { return EXIT_USAGE; } const session = env.OS_FLEET_SESSION || env.CLAUDE_CODE_REMOTE_SESSION_ID || null; - const plan = opts.close !== null ? planClose({ repo: opts.repo, pull: opts.close }) : planOpen({ repo: opts.repo, login: opts.login, head: opts.head, base: opts.base, session }); + const plan = opts.close !== null ? planClose({ repo: opts.repo, pull: opts.close }) : planOpen({ repo: opts.repo, seat: opts.seat, login: opts.login, head: opts.head, base: opts.base, session }); if (plan.errors.length) { for (const e of plan.errors) console.error(`✗ mailbox-open: REFUSED by the relay validator — ${e}`); console.error(' Nothing was sent.'); @@ -378,16 +463,18 @@ export async function main(argv, { env = process.env } = {}) { let selfTestReachedVerdict = false; const SELF_TEST_BATTERIES = Object.freeze({ - 'arguments: the two modes and every refusal': 10, - 'the open plan: branch, labels, body, and the relay validator': 9, + 'arguments: the two modes and every refusal': 13, + 'the seat: canonical key, ref slug, and the per-seat branch': 7, + 'the open plan: branch, labels, body, and the relay validator': 10, 'the close plan: comment then close, judged by the validator': 3, - 'finding an open mailbox: head, body line, label, state': 6, + 'whose mailbox a pull is: the seat on the body line or the head, a legacy login, neither': 7, + "finding a seat's open mailbox: by seat, never by login": 9, 'refusing to close what is not an open mailbox': 4, 'the ledger exclusion and the footer': 4, - 'the CLI: dry run prints the actions file and sends nothing': 5, + 'the CLI: dry run prints the actions file and sends nothing': 6, 'paging: a three-page read through the loop equals the slurped array': 4, }); -const SELF_TEST_BATTERY_FLOOR = 8; +const SELF_TEST_BATTERY_FLOOR = 10; async function selfTest() { let failed = 0; @@ -406,30 +493,44 @@ async function selfTest() { }; battery('arguments: the two modes and every refusal'); - const open = parseArgs(['--repo', 'o/r', '--login', 'os-bill']); - t('open: repo, login, default base main, no head', [open.errors, open.repo, open.login, open.base, open.head], [[], 'o/r', 'os-bill', 'main', null]); + const OPEN = ['--repo', 'o/r', '--seat', 'domain:ui#4', '--login', 'os-bill']; + const open = parseArgs(OPEN); + t('open: repo, seat, login, default base main, no head', [open.errors, open.repo, open.seat, open.login, open.base, open.head], [[], 'o/r', 'domain:ui#4', 'os-bill', 'main', null]); const close = parseArgs(['--repo', 'o/r', '--close', '12']); - t('close: a number, no login needed', [close.errors, close.close], [[], 12]); - t('no --repo is refused', parseArgs(['--login', 'a']).errors.some((e) => e.includes('--repo')), true); - t('a repo not shaped owner/name is refused', parseArgs(['--repo', 'nope', '--login', 'a']).errors.some((e) => e.includes('owner/name')), true); - t('open without --login is refused', parseArgs(['--repo', 'o/r']).errors.some((e) => e.includes('--login')), true); - t('a login GitHub would refuse is refused', parseArgs(['--repo', 'o/r', '--login', '-bad--']).errors.some((e) => e.includes('not a GitHub login')), true); + t('close: a number, no seat and no login needed', [close.errors, close.close], [[], 12]); + t('no --repo is refused', parseArgs(['--seat', 'triage', '--login', 'a']).errors.some((e) => e.includes('--repo')), true); + t('a repo not shaped owner/name is refused', parseArgs(['--repo', 'nope', '--seat', 'triage', '--login', 'a']).errors.some((e) => e.includes('owner/name')), true); + t('⭐ open without --seat is refused, naming why — the mailbox is the seat\'s, never the login\'s', parseArgs(['--repo', 'o/r', '--login', 'os-bill']).errors.some((e) => e.includes('--seat is required') && e.includes('never the login')), true); + t('a --seat that is not LANE#N is refused', ['ui 4', 'domain:ui#0', '#4', 'domain:', 'domain:ui#4#5'].every((s) => parseArgs(['--repo', 'o/r', '--seat', s, '--login', 'a']).errors.some((e) => e.includes('is not a seat'))), true); + t('open without --login is refused', parseArgs(['--repo', 'o/r', '--seat', 'triage']).errors.some((e) => e.includes('--login')), true); + t('a login GitHub would refuse is refused', parseArgs(['--repo', 'o/r', '--seat', 'triage', '--login', '-bad--']).errors.some((e) => e.includes('not a GitHub login')), true); + t('a seat whose branch is no legal ref is refused', parseArgs(['--repo', 'o/r', '--seat', 'repo:a..b#1', '--login', 'a']).errors.some((e) => e.includes('not a legal ref')), true); t('--close with a non-number is refused', parseArgs(['--repo', 'o/r', '--close', '1x']).errors.some((e) => e.includes('pull number')), true); - t('--close with --head is refused', parseArgs(['--repo', 'o/r', '--close', '3', '--head', 'x']).errors.some((e) => e.includes('no --head')), true); - t('an unknown flag is refused, never ignored', parseArgs(['--repo', 'o/r', '--login', 'a', '--bogus']).errors.some((e) => e.includes('--bogus')), true); - t('a --head with a space or .. is refused', [parseArgs(['--repo', 'o/r', '--login', 'a', '--head', 'a b']).errors.length > 0, parseArgs(['--repo', 'o/r', '--login', 'a', '--head', 'a..b']).errors.length > 0], [true, true]); + t('--close with --head or --seat is refused', [parseArgs(['--repo', 'o/r', '--close', '3', '--head', 'x']).errors.some((e) => e.includes('no --head')), parseArgs(['--repo', 'o/r', '--close', '3', '--seat', 'triage']).errors.some((e) => e.includes('no --seat'))], [true, true]); + t('an unknown flag is refused, never ignored', parseArgs([...OPEN, '--bogus']).errors.some((e) => e.includes('--bogus')), true); + t('a --head with a space or .. is refused', [parseArgs([...OPEN, '--head', 'a b']).errors.length > 0, parseArgs([...OPEN, '--head', 'a..b']).errors.length > 0], [true, true]); + + battery('the seat: canonical key, ref slug, and the per-seat branch'); + t('domain:ui#4 → key domain:ui#4, slug ui-4 (the default kind dropped)', seatKey('domain:ui#4'), { key: 'domain:ui#4', slug: 'ui-4' }); + t('an absent number is seat 1, spelled', [seatKey('triage'), seatKey('domain:skills')], [{ key: 'triage#1', slug: 'triage-1' }, { key: 'domain:skills#1', slug: 'skills-1' }]); + t('another kind keeps its name, its colon turned into a hyphen', [seatKey('repo:cloud#1').slug, seatKey('repo:www.objectos.ai#2').slug], ['repo-cloud-1', 'repo-www.objectos.ai-2']); + t('read lower-cased, so one seat has one key', [seatKey('Domain:UI#4').key, seatKey(' domain:ui#4 ').key], ['domain:ui#4', 'domain:ui#4']); + t('not a seat ⇒ null', ['', 'ui 4', 'domain:ui#0', '#4', 'domain:', null].map(seatKey), [null, null, null, null, null, null]); + t('the branch is mailbox// — no `:` and no `#` in a ref', mailboxBranch('os-bill', seatKey('domain:ui#4')), 'mailbox/os-bill/ui-4'); + t('⭐ two seats under ONE login get two branches', [mailboxBranch('os-sales', seatKey('domain:server#1')), mailboxBranch('os-sales', seatKey('domain:ui#1'))], ['mailbox/os-sales/server-1', 'mailbox/os-sales/ui-1']); battery('the open plan: branch, labels, body, and the relay validator'); - const p = planOpen({ repo: 'objectstack-ai/objectstack', login: 'os-bill', session: 'cse_01ABC' }); - t('the default branch is mailbox/ and it is pushed', [p.branch, p.push], ['mailbox/os-bill', true]); - t('one pr_create action, base main', [p.actions.length, p.actions[0].op, p.actions[0].base, p.actions[0].head], [1, 'pr_create', 'main', 'mailbox/os-bill']); + const p = planOpen({ repo: 'objectstack-ai/objectstack', seat: 'domain:ui#4', login: 'os-bill', session: 'cse_01ABC' }); + t('the default branch is mailbox// and it is pushed', [p.branch, p.push], ['mailbox/os-bill/ui-4', true]); + t('one pr_create action, base main, titled by seat and login', [p.actions.length, p.actions[0].op, p.actions[0].base, p.actions[0].head, p.actions[0].title], [1, 'pr_create', 'main', 'mailbox/os-bill/ui-4', 'pm:mailbox — domain:ui#4 · os-bill']); t('labels: pm:mailbox, pinned (the stale-sweep exemption) and skip-changeset (the changeset-check exemption)', p.actions[0].labels, ['pm:mailbox', 'pinned', 'skip-changeset']); t('⭐ pinned IS stale.yml\'s exemption, read off the workflow — and pm:mailbox is not one', (() => { const y = readFileSync(join(REPO_ROOT, '.github', 'workflows', 'stale.yml'), 'utf8'); const m = /exempt-pr-labels:\s*'([^']*)'/.exec(y); const names = m ? m[1].split(',').map((s) => s.trim()) : []; return [names.includes(STALE_EXEMPT_LABEL), names.includes(MAILBOX_LABEL)]; })(), [true, false]); - t('the body opens with To: — the addressee is the seat', p.actions[0].body.split('\n')[0], 'To: os-bill'); + t('⭐ the body opens with Mailbox-of: · — the seat is the key, the login only who holds it', p.actions[0].body.split('\n')[0], 'Mailbox-of: domain:ui#4 · os-bill'); + t('the body tells a sender to open with To: ', p.actions[0].body.includes('the line `To: domain:ui#4`, then ONE line'), true); t('the body states the CI posture, the never-merged rule and the closed vocabulary', [/RED BY DESIGN/.test(p.actions[0].body), /Never merged/.test(p.actions[0].body), /seven doorbell keys/.test(p.actions[0].body) && /no acknowledgements/.test(p.actions[0].body)], [true, true, true]); t('the relay validator accepts the stroke', p.errors, []); - const ph = planOpen({ repo: 'o/r', login: 'os-bill', head: 'claude/project-thread-x' }); - t('--head names the branch and skips the push', [ph.branch, ph.push, ph.actions[0].head], ['claude/project-thread-x', false, 'claude/project-thread-x']); + const ph = planOpen({ repo: 'o/r', seat: 'triage', login: 'os-bill', head: 'claude/project-thread-x' }); + t('--head names the branch and skips the push; the body still names the seat', [ph.branch, ph.push, ph.actions[0].head, ph.actions[0].body.split('\n')[0]], ['claude/project-thread-x', false, 'claude/project-thread-x', 'Mailbox-of: triage#1 · os-bill']); t('no draft key is sent — the relay forces draft itself', 'draft' in p.actions[0], false); battery('the close plan: comment then close, judged by the validator'); @@ -438,14 +539,34 @@ async function selfTest() { t('the relay validator accepts the stroke', c.errors, []); t('the comment carries the bare footer under its rule line', c.actions[0].body.endsWith('\n\n---\n_Generated by [Claude Code](https://claude.ai/code)_'), true); - battery('finding an open mailbox: head, body line, label, state'); + battery('whose mailbox a pull is: the seat on the body line or the head, a legacy login, neither'); const pr = (o) => ({ state: 'open', number: 1, labels: [{ name: 'pm:mailbox' }], head: { ref: 'x' }, body: '', ...o }); - t('found by head mailbox/', openMailboxesOf([pr({ head: { ref: 'mailbox/os-bill' } })], 'os-bill').length, 1); - t('found by the body line on another head', openMailboxesOf([pr({ body: 'To: os-bill\n\nx' })], 'os-bill').length, 1); - t('login match is case-insensitive', openMailboxesOf([pr({ head: { ref: 'mailbox/OS-Bill' } })], 'os-bill').length, 1); - t('without the label it is not a mailbox', openMailboxesOf([pr({ labels: [], head: { ref: 'mailbox/os-bill' } })], 'os-bill').length, 0); - t('another login’s mailbox is not ours', openMailboxesOf([pr({ head: { ref: 'mailbox/os-elon' } })], 'os-bill').length, 0); - t('a closed one does not block a new one', openMailboxesOf([pr({ state: 'closed', head: { ref: 'mailbox/os-bill' } })], 'os-bill').length, 0); + t('⭐ the body this file writes reads back as its seat — writer and reader are one spelling', mailboxOwner(pr({ head: { ref: 'claude/project-thread-x' }, body: ph.actions[0].body })), { seat: 'triage#1', slug: 'triage-1', login: 'os-bill' }); + t('a head mailbox// with no body line reads the slug and the login', mailboxOwner(pr({ head: { ref: 'mailbox/os-bill/ui-4' } })), { seat: null, slug: 'ui-4', login: 'os-bill' }); + t('the body outranks the head', mailboxOwner(pr({ head: { ref: 'mailbox/os-bill/ui-4' }, body: 'Mailbox-of: domain:ui#2 · os-bill' })).slug, 'ui-2'); + t('⛔ a legacy per-login mailbox names a login and NO seat — head mailbox/, a To: or a Mailbox-of: first line', [mailboxOwner(pr({ head: { ref: 'mailbox/os-justin' } })), mailboxOwner(pr({ head: { ref: 'claude/x' }, body: 'To: os-sales\n\nThe mailbox' })), mailboxOwner(pr({ body: 'Mailbox-of: os-elon-musk\n' }))], [{ seat: null, slug: null, login: 'os-justin' }, { seat: null, slug: null, login: 'os-sales' }, { seat: null, slug: null, login: 'os-elon-musk' }]); + t('a pm:mailbox pull naming neither is a mailbox of nobody, never a non-mailbox', mailboxOwner(pr({ body: 'hello' })), { seat: null, slug: null, login: null }); + t('a malformed seat or login on the body line names no seat', [mailboxOwner(pr({ body: 'Mailbox-of: ui 4 · os-bill' })).slug, mailboxOwner(pr({ body: 'Mailbox-of: domain:ui#4 · -bad-' })).slug], [null, null]); + t('without the label it is not a mailbox', mailboxOwner(pr({ labels: [], head: { ref: 'mailbox/os-bill/ui-4' } })), null); + + battery("finding a seat's open mailbox: by seat, never by login"); + const UI4 = seatKey('domain:ui#4'); + const opened = (seat, login, number) => pr({ number, head: { ref: `claude/project-thread-${number}` }, body: planOpen({ repo: 'o/r', seat, login, head: `claude/project-thread-${number}` }).actions[0].body }); + t('found by the body line on a --head branch', openMailboxesOf([opened('domain:ui#4', 'os-bill', 7)], UI4).map((x) => x.number), [7]); + t('found by the head slug on the default branch', openMailboxesOf([pr({ head: { ref: planOpen({ repo: 'o/r', seat: 'domain:ui#4', login: 'os-bill' }).branch } })], UI4).length, 1); + t('⭐ two seats under ONE login both open: with seat 3\'s mailbox open seat 4 finds none, then each finds exactly its own', (() => { + const s3 = seatKey('domain:server#3'); + const s4 = seatKey('domain:server#4'); + const one = [opened('domain:server#3', 'os-elon-musk', 3)]; + const both = [...one, opened('domain:server#4', 'os-elon-musk', 4)]; + return [openMailboxesOf(one, s4).length, openMailboxesOf(both, s3).map((x) => x.number), openMailboxesOf(both, s4).map((x) => x.number)]; + })(), [0, [3], [4]]); + t('⭐ the same seat held by ANOTHER login is still that seat\'s mailbox — the refusal is per seat', openMailboxesOf([opened('domain:ui#4', 'os-sales', 9)], UI4).map((x) => x.number), [9]); + t('the seat match is case- and spelling-insensitive (domain:skills ≡ domain:skills#1)', [openMailboxesOf([opened('domain:skills', 'a', 1)], seatKey('Domain:Skills#1')).length, openMailboxesOf([pr({ head: { ref: 'mailbox/a/UI-4' } })], UI4).length], [1, 1]); + t('⛔ a legacy per-login mailbox blocks no seat of its login', [openMailboxesOf([pr({ head: { ref: 'mailbox/os-bill' } }), pr({ body: 'To: os-bill' })], UI4).length], [0]); + t('…and is named for its login, case-insensitively, never for another', [legacyMailboxesOf([pr({ number: 5, head: { ref: 'mailbox/OS-Bill' } }), pr({ number: 6, body: 'To: os-bill' }), pr({ number: 8, body: 'To: os-sales' }), opened('domain:ui#4', 'os-bill', 7)], 'os-bill').map((x) => x.number)], [[5, 6]]); + t('without the label it is not a mailbox', openMailboxesOf([pr({ labels: [], head: { ref: 'mailbox/os-bill/ui-4' } })], UI4).length, 0); + t('a closed one does not block a new one', openMailboxesOf([pr({ state: 'closed', head: { ref: 'mailbox/os-bill/ui-4' } })], UI4).length, 0); battery('refusing to close what is not an open mailbox'); t('an open pm:mailbox PR may be closed', closeRefusal(pr({}), 1), null); @@ -478,16 +599,17 @@ async function selfTest() { battery('the CLI: dry run prints the actions file and sends nothing'); const cli = (args) => spawnSync(process.execPath, [SELF_PATH, ...args], { encoding: 'utf8', env: { ...process.env, PATH: '/nonexistent', OS_FLEET_SESSION: '', CLAUDE_CODE_REMOTE_SESSION_ID: '' } }); - const dry = cli(['--repo', 'objectstack-ai/objectstack', '--login', 'os-bill', '--dry-run']); + const dry = cli(['--repo', 'objectstack-ai/objectstack', '--seat', 'domain:ui#4', '--login', 'os-bill', '--dry-run']); t('open dry run exits 0 with no gh/git on PATH (nothing reached for)', dry.status, EXIT_OK); const printed = dry.stdout.split('--- actions file ---\n')[1] ?? ''; let parsed = null; try { parsed = JSON.parse(printed); } catch {} - t('the printed actions file parses and is the plan', parsed?.[0]?.op === 'pr_create' && parsed?.[0]?.head === 'mailbox/os-bill', true); + t('the printed actions file parses and is the plan', parsed?.[0]?.op === 'pr_create' && parsed?.[0]?.head === 'mailbox/os-bill/ui-4', true); + t('the printed plan names the seat it refuses on', /refuse if a pm:mailbox PR for the seat domain:ui#4 is open/.test(dry.stdout), true); t('close dry run exits 0 and prints both actions', [cli(['--repo', 'o/r', '--close', '7', '--dry-run']).status, (cli(['--repo', 'o/r', '--close', '7', '--dry-run']).stdout.match(/"op":/g) ?? []).length], [EXIT_OK, 2]); - t('a usage error is exit 2', cli(['--repo', 'o/r']).status, EXIT_USAGE); + t('a usage error is exit 2 — an open with no --seat among them', [cli(['--repo', 'o/r']).status, cli(['--repo', 'o/r', '--login', 'os-bill', '--dry-run']).status], [EXIT_USAGE, EXIT_USAGE]); t('--help is exit 0', cli(['--help']).status, EXIT_OK); const floor = []; From c2792c090212bea9fa9e5a1bb5cc3c2e19ff170f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 02:10:08 +0000 Subject: [PATCH 2/2] feat(pm): a mailbox message opens `To: `; the rules text names the seat key and the hand-over close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit record-recognisers.mjs: `readMailboxLine` reads a `To: ` line after the optional signature and before the key, returns it as `to` (the mailbox opener's canonical `seatKey`, imported, never a second seat shape), and refuses a message without one, a `To:` naming a login, or a `To:` with no key under it. The byte cap still counts the key line only. The doorbell battery asks every key refusal under a `To:` line, so none can pass on the missing envelope, and gains six cases. mailbox-open.mjs: a seat's lane is one of the four a seat post names (`domain:`, `repo:`, `triage`, `director`), so a login is never read as a seat. project-forms.md 〈信箱 PR〉 and the mailbox body now say the same thing: one per seat, keyed by seat, branch `mailbox//`, body `Mailbox-of: · `, messages `To: ` then the key. seat-post-protocol.md 〈交接收尾清单〉 step 9 names the close and the successor's reopen. Both files keep their line count. Claude-Session: https://claude.ai/code/session_016WV4d1d1pooSGzvbJymLKy Co-authored-by: Claude --- .../pm-dispatch/references/project-forms.md | 8 +- .../references/seat-post-protocol.md | 2 +- scripts/pm/mailbox-open.mjs | 25 +++-- scripts/pm/record-recognisers.mjs | 105 +++++++++++------- 4 files changed, 86 insertions(+), 54 deletions(-) diff --git a/.claude/skills/pm-dispatch/references/project-forms.md b/.claude/skills/pm-dispatch/references/project-forms.md index 402c95fd1f1..18238648447 100644 --- a/.claude/skills/pm-dispatch/references/project-forms.md +++ b/.claude/skills/pm-dispatch/references/project-forms.md @@ -33,10 +33,10 @@ ## 信箱 PR -- 每席一个 draft PR:分支 `mailbox/`,标签 `pm:mailbox`+`pinned`,正文首行 `To: `。 -- 坐席即开(`mailbox-open.mjs`)并订阅,存根指针行记其号;永不合并,移交或换账号关旧开新。 -- `pm:mailbox` PR 不欠 CI,一切落地与台账查询排除它。 -- 首行恒为下列七键之一(发→收;何时):一事一行 ≤200 字节,只写键与编号;表外即半态。 +- 每席一个 draft PR,键是席位 ⛔ 非账号:分支 `mailbox//`,标签 `pm:mailbox`+`pinned`。 +- 正文首行 `Mailbox-of: <席位> · `;坐席即开(`mailbox-open.mjs`)并订阅,存根指针行记其号。 +- 永不合并,不欠 CI,落地与台账查询排除它;移交或换账号关旧开新;评论首行 `To: <席位>`。 +- 次行恒为下列七键之一(发→收;何时):一事一行 ≤200 字节,只写键与编号;表外即半态。 - `Request: contract-review · PR #n · head SHA · REASON`:车道→分诊;本席复审档不可用,一 head 一次。 - `Request: urgent-triage · #n · ONE-LINE-CRITERION`:任一席→分诊;p0 嫌疑、main 红、确定性队列红。 - `Request: main-red · CHECK-NAME · anchor #n · suggested lane X`:发现席→分诊(缺则总监);锚卡立起时。 diff --git a/.claude/skills/pm-dispatch/references/seat-post-protocol.md b/.claude/skills/pm-dispatch/references/seat-post-protocol.md index 6f80396b58d..2bb54341033 100644 --- a/.claude/skills/pm-dispatch/references/seat-post-protocol.md +++ b/.claude/skills/pm-dispatch/references/seat-post-protocol.md @@ -51,7 +51,7 @@ 6. 贴正文改 `⏳ vacant` + 台账:前任会话 ID、注销时间、队列快照、串行队、跨车道备忘。 7. 收班简报与审计评论存档;接任指引 = `/pm-dispatch 接手` + 先读座位贴。 8. 在飞卡与留跑会话贴里点名(卡号+派发会话 ID);派出的会话离任 PM 自己 `archive_session`。 -9. 向维护者交最终报告。 +9. 关本席信箱(`mailbox-open.mjs --close N`),接任者坐席时按席重开;向维护者交最终报告。 ## 座位 Routine 创建 diff --git a/scripts/pm/mailbox-open.mjs b/scripts/pm/mailbox-open.mjs index 7d91cd8a611..cf9661197e0 100644 --- a/scripts/pm/mailbox-open.mjs +++ b/scripts/pm/mailbox-open.mjs @@ -40,13 +40,15 @@ * was a per-seat one; the board falsified that premise, and nothing here reads * a login as a seat any more. * - * A seat is spelled `LANE#N` — `domain:ui#4`, `repo:cloud#1`, `triage` — with - * an absent number read as seat 1 (`seatKey`). It is compared lower-cased and - * written canonically with its number (`triage#1`). Its SLUG is that key as one - * ref segment, because a ref cannot carry `:` or `#`: the default `domain:` - * kind dropped, every other `:` and the `#` turned into `-` (`ui-4`, - * `repo-cloud-1`, `triage-1`). No live lane is named both `domain:X` and `X`, - * which is what keeps the slug one-to-one. + * A seat is spelled `LANE#N` — `domain:ui#4`, `repo:cloud#1`, `triage` — its + * lane one of the four a seat post's title names (`domain:`, `repo:`, + * `triage`, `director`), so a login never reads as a seat, and an absent + * number read as seat 1 (`seatKey`). It is compared lower-cased and written + * canonically with its number (`triage#1`). Its SLUG is that key as one ref + * segment, because a ref cannot carry `:` or `#`: the default `domain:` kind + * dropped, every other `:` and the `#` turned into `-` (`ui-4`, + * `repo-cloud-1`, `triage-1`). No `domain:` lane is named `triage`, + * `director` or `repo-…`, which is what keeps the slug one-to-one. * * ## What it does, in order (open) * @@ -156,8 +158,9 @@ const REPO_SHAPE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/; const LOGIN_SHAPE = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/; const BRANCH_SHAPE = /^(?!\/|.*\/\/|.*\.\.|.*[\s~^:?*[\\]|.*\.lock$|.*\/$)[A-Za-z0-9._/-]+$/; const SESSION_SHAPE = /^(?:session|cse)_[A-Za-z0-9]+$/; -// A seat: its lane (`domain:ui`, `repo:cloud`, `triage`, `director`), then `#N`; absent = seat 1. Read lower-cased. -const SEAT_SHAPE = /^([a-z]+(?::[a-z0-9._-]+)?)(?:#([1-9][0-9]*))?$/; +// A seat: its lane — `domain:`, `repo:`, `triage`, `director`, the four a seat post's title names — then `#N`; +// absent = seat 1. Read lower-cased. The bare lanes are a closed pair, so a login is never read as a seat. +const SEAT_SHAPE = /^((?:domain|repo):[a-z0-9._-]+|triage|director)(?:#([1-9][0-9]*))?$/; /** A per-seat mailbox body's first line, `Mailbox-of: · `. */ const MAILBOX_OF_LINE = /^Mailbox-of:[ \t]*(\S+)[ \t]*·[ \t]*(\S+)$/; /** The first lines a legacy per-login mailbox opened with: `To: ` (this file's), `Mailbox-of: `. */ @@ -501,7 +504,7 @@ async function selfTest() { t('no --repo is refused', parseArgs(['--seat', 'triage', '--login', 'a']).errors.some((e) => e.includes('--repo')), true); t('a repo not shaped owner/name is refused', parseArgs(['--repo', 'nope', '--seat', 'triage', '--login', 'a']).errors.some((e) => e.includes('owner/name')), true); t('⭐ open without --seat is refused, naming why — the mailbox is the seat\'s, never the login\'s', parseArgs(['--repo', 'o/r', '--login', 'os-bill']).errors.some((e) => e.includes('--seat is required') && e.includes('never the login')), true); - t('a --seat that is not LANE#N is refused', ['ui 4', 'domain:ui#0', '#4', 'domain:', 'domain:ui#4#5'].every((s) => parseArgs(['--repo', 'o/r', '--seat', s, '--login', 'a']).errors.some((e) => e.includes('is not a seat'))), true); + t('a --seat that is not LANE#N is refused, a login among them', ['ui 4', 'domain:ui#0', '#4', 'domain:', 'domain:ui#4#5', 'os-bill', 'marchtian'].every((s) => parseArgs(['--repo', 'o/r', '--seat', s, '--login', 'a']).errors.some((e) => e.includes('is not a seat'))), true); t('open without --login is refused', parseArgs(['--repo', 'o/r', '--seat', 'triage']).errors.some((e) => e.includes('--login')), true); t('a login GitHub would refuse is refused', parseArgs(['--repo', 'o/r', '--seat', 'triage', '--login', '-bad--']).errors.some((e) => e.includes('not a GitHub login')), true); t('a seat whose branch is no legal ref is refused', parseArgs(['--repo', 'o/r', '--seat', 'repo:a..b#1', '--login', 'a']).errors.some((e) => e.includes('not a legal ref')), true); @@ -515,7 +518,7 @@ async function selfTest() { t('an absent number is seat 1, spelled', [seatKey('triage'), seatKey('domain:skills')], [{ key: 'triage#1', slug: 'triage-1' }, { key: 'domain:skills#1', slug: 'skills-1' }]); t('another kind keeps its name, its colon turned into a hyphen', [seatKey('repo:cloud#1').slug, seatKey('repo:www.objectos.ai#2').slug], ['repo-cloud-1', 'repo-www.objectos.ai-2']); t('read lower-cased, so one seat has one key', [seatKey('Domain:UI#4').key, seatKey(' domain:ui#4 ').key], ['domain:ui#4', 'domain:ui#4']); - t('not a seat ⇒ null', ['', 'ui 4', 'domain:ui#0', '#4', 'domain:', null].map(seatKey), [null, null, null, null, null, null]); + t('not a seat ⇒ null — ⛔ a login above all, the confusion a per-login mailbox made', ['', 'ui 4', 'domain:ui#0', '#4', 'domain:', null, 'os-bill', 'marchtian', 'epic:#12'].map(seatKey), [null, null, null, null, null, null, null, null, null]); t('the branch is mailbox// — no `:` and no `#` in a ref', mailboxBranch('os-bill', seatKey('domain:ui#4')), 'mailbox/os-bill/ui-4'); t('⭐ two seats under ONE login get two branches', [mailboxBranch('os-sales', seatKey('domain:server#1')), mailboxBranch('os-sales', seatKey('domain:ui#1'))], ['mailbox/os-sales/server-1', 'mailbox/os-sales/ui-1']); diff --git a/scripts/pm/record-recognisers.mjs b/scripts/pm/record-recognisers.mjs index b00080e8ef6..542632df5a4 100644 --- a/scripts/pm/record-recognisers.mjs +++ b/scripts/pm/record-recognisers.mjs @@ -33,7 +33,7 @@ * isOutsideReviewSurface the prose faces a head may move on and keep its record * contractReviewTemplateLines / contractReviewRecordLines what `--template` prints * contractReviewBriefLines what `--brief-template` prints — the brief a seat hands its reviewer - * readMailboxLine / mailboxEvents / MAILBOX_KEYS the mailbox DOORBELL — the seven first-line keys a `pm:mailbox` comment may open with, read; anything else refused + * readMailboxLine / mailboxEvents / MAILBOX_KEYS the mailbox DOORBELL — `To: `, then one of the seven keys a `pm:mailbox` comment may carry, read; anything else refused * * Every definition below was MOVED, names unchanged, out of * `check-clause2-carriers.mjs` at the ruling's last step, which deleted the @@ -119,6 +119,10 @@ import { SELF_REPO_ID, governedPathsIn } from './check-governed-merges.mjs'; // line's own quoting helper, one spelling for both. The narrowing face is read // with the fleet's ONE reader of that line, imported, ⛔ never a second parser. import { quoteLine, readClause2Line } from './clause2-line.mjs'; +// A mailbox message's `To:` line names a SEAT, spelled as the mailbox opener spells a seat — its +// `seatKey`, imported, ⛔ never a second seat shape. That file imports nothing of this graph's +// top-level-await half (only the relay validator and `gh-pages.mjs`), so the edge adds no cycle. +import { seatKey } from './mailbox-open.mjs'; // ── the tier FOLLOWS THE FACE that made the review owed ───────────────────── @@ -1211,7 +1215,7 @@ export function contractReviewHold(rows, headSha, { now } = {}) { // --------------------------------------------------------------------------- /** - * The seven keys a mailbox comment may open with — the doorbell a seat rings + * The seven keys a mailbox comment may carry — the doorbell a seat rings * on a sibling seat's or the triage seat's mailbox PR (`pm:mailbox`, one per * seat, opened by `mailbox-open.mjs`), because only a comment on a PR wakes * the session subscribed to it; a line on an issue wakes nobody. The protocol @@ -1221,8 +1225,11 @@ export function contractReviewHold(rows, headSha, { now } = {}) { * verbatim in that file, so the text and the reader cannot drift apart. * * The rules the reader enforces, from the same text: the FIRST line of the - * comment is the key and nothing else is allowed on a mailbox; one event, one - * line, at most {@link MAILBOX_LINE_MAX_BYTES} bytes, keys and numbers only — + * comment is `To: ` — the seat the message is for, in `seatKey`'s + * spelling, so a message that reached the wrong mailbox says so — and the next + * is the key; nothing else is allowed on a mailbox. One event, one key line of + * at most {@link MAILBOX_LINE_MAX_BYTES} bytes (the `To:` line is the envelope, + * as the signature is, and is not counted), keys and numbers only — * reasons and readings stay on the card or the PR; no acknowledgement; no * re-send. Anything outside the table is a half-state. The vocabulary is * CLOSED on the maintainer's word (「只沟通必要的信息」): an eighth key is @@ -1246,6 +1253,8 @@ export const MAILBOX_LINE_MAX_BYTES = 200; /** The two kinds, in `keyLineRegex`'s one convention as ONE alternation — a third kind cannot arrive unnoticed. */ const MAILBOX_KINDS = Object.freeze(['Request', 'Notify']); const MAILBOX_KEY_LINE = keyLineRegex(`(${MAILBOX_KINDS.join('|')})`); +/** The addressee line above the key, `To: `, in the same convention. */ +const MAILBOX_TO_LINE = keyLineRegex('To'); /** ` · ` — the middle dot the templates spell between fields; the spaces around it are optional. */ const MAILBOX_SEPARATOR = /[ \t]*·[ \t]*/; const NUMBER_REF = /^#([1-9][0-9]*)$/; @@ -1342,16 +1351,19 @@ const MAILBOX_FIELD_READERS = Object.freeze({ }); /** - * What ONE mailbox comment says, read off its first line. Body in, reading out; nothing is fetched. - * - * read the first line is one of the seven keys and spells its template: `id`, `kind`, `key`, - * the key's own fields, and `numbers` (every card, PR or mailbox number the line names) - * refused the first line is not a key line at all, or names a key outside the seven — free text, - * an acknowledgement, an eighth key: the half-state the text names + * What ONE mailbox comment says, read off its `To:` line and the key line under it. Body in, reading + * out; nothing is fetched. + * + * read `To: `, then one of the seven keys spelling its template: `id`, `kind`, `key`, `to` + * (the addressed seat's canonical key, `seatKey`'s), the key's own fields, and `numbers` + * (every card, PR or mailbox number the line names) + * refused no `To: ` line first — a key on the first line, free text, an acknowledgement — + * a `To:` naming no seat (a login is not one), no key line under it, or a key outside the + * seven: the half-state the text names * unreadable a known key whose line does not spell its template, runs over the byte cap, or is * followed by a second line of content (the platform's appended footer is not one) * - * The key is read in `keyLineRegex`'s one convention (a bullet, a blockquote, bold or backticks around + * Both lines are read in `keyLineRegex`'s one convention (a bullet, a blockquote, bold or backticks around * the key are decoration), the fields are split on ` · `, and a head is 7 to 40 hex as everywhere here. * * @returns {{ state: 'refused', reason: string, kind?: string, line?: string } @@ -1362,8 +1374,14 @@ export function readMailboxLine(text) { // The key is the first line AFTER one leading signature line (`stripSignatureLine`, #27113): the // signature names who rang, and is neither the key nor a second line of content. const lines = stripSignatureLine(text).split(/\r?\n/); - const at = lines.findIndex((l) => l.trim() !== ''); - if (at < 0) return { state: 'refused', reason: 'an empty comment' }; + const toAt = lines.findIndex((l) => l.trim() !== ''); + if (toAt < 0) return { state: 'refused', reason: 'an empty comment' }; + const toLine = MAILBOX_TO_LINE.exec(lines[toAt]); + if (!toLine) return { state: 'refused', reason: 'the first line is not `To: ` — a message names the seat it is for, then its key', line: quoteLine(lines[toAt]) }; + const to = seatKey(stripValueDecoration(toLine[1]).replace(/(?:[ \t]|\*\*|`)+$/, '')); + if (!to) return { state: 'refused', reason: '`To:` names no seat — spell it LANE#N, e.g. domain:ui#4; a login is not a seat', line: quoteLine(lines[toAt]) }; + const at = lines.findIndex((l, i) => i > toAt && l.trim() !== ''); + if (at < 0) return { state: 'refused', reason: 'no key line under `To:`', line: quoteLine(lines[toAt]) }; const first = lines[at]; const m = MAILBOX_KEY_LINE.exec(first); if (!m) return { state: 'refused', reason: 'the first line is not a `Request:` or `Notify:` key line', line: quoteLine(first) }; @@ -1379,7 +1397,7 @@ export function readMailboxLine(text) { if (!tail.every((l) => FOOTER_RULE.test(l) || FOOTER_LINE.test(l))) return unreadable('more than one line — reasons and readings stay on the card or the PR'); const fields = MAILBOX_FIELD_READERS[entry.id](parts.map((part) => part.trim())); if (fields === null) return unreadable(`does not spell \`${entry.template}\``); - return { state: 'read', id: entry.id, kind, key: entry.key, ...fields }; + return { state: 'read', id: entry.id, kind, key: entry.key, to: to.key, ...fields }; } /** @@ -2048,7 +2066,7 @@ const SELF_TEST_BATTERIES = Object.freeze({ '⭐ 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': 13, - '⭐ the mailbox doorbell: the seven first-line keys read — kind, numbers, sha, login, time — and anything else refused': 21, + '⭐ the mailbox doorbell: `To: `, then one of the seven keys read — seat, kind, numbers, sha, login, time — and anything else refused': 27, '⭐ 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 lane wins a tie': 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, @@ -2547,40 +2565,51 @@ export async function selfTest() { // -- the mailbox doorbell ----------------------------------------------------- // // One mailbox PR per seat, and a CLOSED vocabulary on it (the maintainer's - // 「只沟通必要的信息」): the first line of a mailbox comment is one of the seven - // keys and nothing else is allowed. These cases read one example of each key - // and refuse free text, an acknowledgement, an eighth key, a second line and - // an over-long line — the half-state faces — and pin each key's spelling to - // the protocol text, so the reader and the text cannot drift apart. - battery('⭐ the mailbox doorbell: the seven first-line keys read — kind, numbers, sha, login, time — and anything else refused'); + // 「只沟通必要的信息」): a mailbox comment is `To: `, then one of the seven + // keys, and nothing else is allowed. These cases read one example of each key + // under its `To:` line and refuse a key with no `To:` (the shape before the + // seat key), a `To:` naming a login, free text, an acknowledgement, an eighth + // key, a second line and an over-long line — the half-state faces — and pin + // each key's spelling to the protocol text, so the reader and the text cannot + // drift apart. Every refusal of a KEY is asked under a `To:` line, so it + // cannot pass vacuously on the missing envelope. + battery('⭐ the mailbox doorbell: `To: `, then one of the seven keys read — seat, kind, numbers, sha, login, time — and anything else refused'); const DB_ROW = (body, id = 5101, at = '2026-10-28T07:00:00Z') => ({ id, created_at: at, user: { login: 'os-bill' }, body }); + // A message: the addressee line, then the key line. + const MSG = (line, to = 'domain:skills#2') => `To: ${to}\n${line}`; const DB = { - cr: `Request: contract-review · PR #26766 · head ${HO_HEAD} · ${CONTRACT_REVIEW_TIER_NAME} refused, HTTP 429`, - ut: 'Request: urgent-triage · #27047 · main red on the merge group, deterministic', - mr: 'Request: main-red · Test Core · anchor #27090 · suggested lane server', - nc: 'Notify: contract-review PASS · PR #26766 · record 6450011111', - on: 'Notify: seat-online · domain:skills#2 · os-bill · mailbox #27120 · takes: #27077 #27062', - off: 'Notify: seat-offline · domain:skills#2 · in-flight: #27077 · stay-behind: #27082 #26749 · brief 6450022222', - sp: 'Notify: split · domain:skills#2 · takes: #27077 #27062 · leaves: #27047', + cr: MSG(`Request: contract-review · PR #26766 · head ${HO_HEAD} · ${CONTRACT_REVIEW_TIER_NAME} refused, HTTP 429`, 'triage'), + ut: MSG('Request: urgent-triage · #27047 · main red on the merge group, deterministic', 'triage'), + mr: MSG('Request: main-red · Test Core · anchor #27090 · suggested lane server', 'triage'), + nc: MSG('Notify: contract-review PASS · PR #26766 · record 6450011111'), + on: MSG('Notify: seat-online · domain:skills#2 · os-bill · mailbox #27120 · takes: #27077 #27062', 'domain:skills#1'), + off: MSG('Notify: seat-offline · domain:skills#2 · in-flight: #27077 · stay-behind: #27082 #26749 · brief 6450022222', 'domain:skills#1'), + sp: MSG('Notify: split · domain:skills#2 · takes: #27077 #27062 · leaves: #27047', 'domain:skills#1'), }; t('⭐ `Request: contract-review` reads the PR, the head (7–40 hex) and the reason — the reason may carry a comma', (() => { const r = readMailboxLine(DB.cr); return r.state === 'read' && r.id === 'request:contract-review' && r.kind === 'Request' && r.key === 'contract-review' && r.pr === 26766 && r.head === HO_HEAD && r.reason === `${CONTRACT_REVIEW_TIER_NAME} refused, HTTP 429` && r.numbers.join() === '26766'; })()); t('⭐ `Request: urgent-triage` reads the card and the one-line criterion', (() => { const r = readMailboxLine(DB.ut); return r.state === 'read' && r.id === 'request:urgent-triage' && r.card === 27047 && r.criterion === 'main red on the merge group, deterministic' && r.numbers.join() === '27047'; })()); t('⭐ `Request: main-red` reads the check name, the anchor card and the suggested lane', (() => { const r = readMailboxLine(DB.mr); return r.state === 'read' && r.id === 'request:main-red' && r.check === 'Test Core' && r.anchor === 27090 && r.lane === 'server' && r.numbers.join() === '27090'; })()); - t('⭐ `Notify: contract-review` reads the verdict, the PR and the record id — PASS and FAIL, and the template\'s own `PASS or FAIL` is a placeholder, not a verdict', (() => { const p = readMailboxLine(DB.nc); const f = readMailboxLine(DB.nc.replace('PASS', 'FAIL')); const tpl = readMailboxLine('Notify: contract-review PASS or FAIL · PR #26766 · record 6450011111'); return p.state === 'read' && p.id === 'notify:contract-review' && p.verdict === 'PASS' && p.pr === 26766 && p.record === 6450011111 && p.numbers.join() === '26766' && f.state === 'read' && f.verdict === 'FAIL' && tpl.state === 'unreadable'; })()); + t('⭐ `Notify: contract-review` reads the verdict, the PR and the record id — PASS and FAIL, and the template\'s own `PASS or FAIL` is a placeholder, not a verdict', (() => { const p = readMailboxLine(DB.nc); const f = readMailboxLine(DB.nc.replace('PASS', 'FAIL')); const tpl = readMailboxLine(MSG('Notify: contract-review PASS or FAIL · PR #26766 · record 6450011111')); return p.state === 'read' && p.id === 'notify:contract-review' && p.verdict === 'PASS' && p.pr === 26766 && p.record === 6450011111 && p.numbers.join() === '26766' && f.state === 'read' && f.verdict === 'FAIL' && tpl.state === 'unreadable'; })()); t('⭐ `Notify: seat-online` reads the seat (lane and number), the login, the mailbox and the takes list', (() => { const r = readMailboxLine(DB.on); return r.state === 'read' && r.id === 'notify:seat-online' && r.seat.lane === 'domain:skills' && r.seat.n === 2 && r.login === 'os-bill' && r.mailbox === 27120 && r.takes.join() === '27077,27062' && r.numbers.join() === '27120,27077,27062'; })()); t('⭐ `Notify: seat-offline` reads the seat, the in-flight and stay-behind lists and the brief id', (() => { const r = readMailboxLine(DB.off); return r.state === 'read' && r.id === 'notify:seat-offline' && r.seat.n === 2 && r.inFlight.join() === '27077' && r.stayBehind.join() === '27082,26749' && r.brief === 6450022222 && r.numbers.join() === '27077,27082,26749'; })()); t('⭐ `Notify: split` reads the seat and the takes and leaves lists', (() => { const r = readMailboxLine(DB.sp); return r.state === 'read' && r.id === 'notify:split' && r.seat.lane === 'domain:skills' && r.takes.join() === '27077,27062' && r.leaves.join() === '27047' && r.numbers.join() === '27077,27062,27047'; })()); t('⭐ every key\'s template is spelled VERBATIM, in a code span, in the protocol text (`references/project-forms.md` 〈信箱 PR〉) — and that text lists exactly seven', (() => { const forms = readFileSync(new URL('../../.claude/skills/pm-dispatch/references/project-forms.md', import.meta.url), 'utf8'); const keyLines = forms.split('\n').filter((l) => /^- `(Request|Notify): /.test(l)); return MAILBOX_KEYS.every((k) => forms.includes(`\`${k.template}\``)) && keyLines.length === MAILBOX_KEYS.length && MAILBOX_KEYS.length === 7; })()); - t('decoration reads: a bulleted bolded key and a blockquoted line are the same event', (() => { const a = readMailboxLine(`- **Request:** contract-review · PR #26766 · head ${HO_HEAD} · quota exhausted`); const b = readMailboxLine(`> ${DB.sp}`); return a.state === 'read' && a.pr === 26766 && b.state === 'read' && b.id === 'notify:split'; })()); + t('decoration reads: a bulleted bolded key and a blockquoted message are the same event', (() => { const a = readMailboxLine(MSG(`- **Request:** contract-review · PR #26766 · head ${HO_HEAD} · quota exhausted`, 'triage')); const b = readMailboxLine(DB.sp.split('\n').map((l) => `> ${l}`).join('\n')); return a.state === 'read' && a.pr === 26766 && b.state === 'read' && b.id === 'notify:split' && b.to === 'domain:skills#1'; })()); t('the attribution block the platform APPENDS (bare or session-URL footer under a rule line) is not a second line', ['\n\n---\n_Generated by [Claude Code](https://claude.ai/code)_', `\n\n---\n_Generated by [Claude Code](https://claude.ai/code/${RECORD_SESSION})_`].every((tail) => readMailboxLine(`${DB.on}${tail}`).state === 'read')); - t('⛔ free text, an acknowledgement and an empty comment are REFUSED, naming the line — the half-state the text names', (() => { const a = readMailboxLine('Seat note: picking up the review now, will report on the card.'); const b = readMailboxLine('收到,谢谢!'); const c = readMailboxLine(''); return a.state === 'refused' && typeof a.line === 'string' && b.state === 'refused' && c.state === 'refused'; })()); - t('⛔ an EIGHTH key is refused, whatever its kind — the vocabulary is closed — and the kinds are case-sensitive', ['Request: coffee · #27077', 'Notify: ack · #27077', 'Ack: #27077', 'request: urgent-triage · #27047 · x', 'Request: contract-review-done · PR #1'].every((l) => readMailboxLine(l).state === 'refused')); - t(`⛔ a line over ${MAILBOX_LINE_MAX_BYTES} bytes is unreadable — keys and numbers only, the reasoning stays on the card`, (() => { const r = readMailboxLine(`Request: contract-review · PR #26766 · head ${HO_HEAD} · ${'x'.repeat(160)}`); return r.state === 'unreadable' && /bytes/.test(r.reason) && readMailboxLine(DB.cr).state === 'read'; })()); + t('⛔ free text, an acknowledgement and an empty comment are REFUSED, naming the line — the half-state the text names', (() => { const a = readMailboxLine(MSG('Seat note: picking up the review now, will report on the card.')); const b = readMailboxLine(MSG('收到,谢谢!')); const c = readMailboxLine(''); const bare = readMailboxLine('收到,谢谢!'); return a.state === 'refused' && /not a `Request:` or `Notify:` key line/.test(a.reason) && typeof a.line === 'string' && b.state === 'refused' && c.state === 'refused' && bare.state === 'refused'; })()); + t('⛔ an EIGHTH key is refused, whatever its kind — the vocabulary is closed — and the kinds are case-sensitive', ['Request: coffee · #27077', 'Notify: ack · #27077', 'Ack: #27077', 'request: urgent-triage · #27047 · x', 'Request: contract-review-done · PR #1'].every((l) => readMailboxLine(MSG(l)).state === 'refused')); + t(`⛔ a line over ${MAILBOX_LINE_MAX_BYTES} bytes is unreadable — keys and numbers only, the reasoning stays on the card`, (() => { const r = readMailboxLine(MSG(`Request: contract-review · PR #26766 · head ${HO_HEAD} · ${'x'.repeat(160)}`)); return r.state === 'unreadable' && /bytes/.test(r.reason) && readMailboxLine(DB.cr).state === 'read'; })()); t('⛔ a second line of content is unreadable — one event, one line', (() => { const r = readMailboxLine(`${DB.cr}\n\nbecause the reviewer tier was refused twice this round`); return r.state === 'unreadable' && /more than one line/.test(r.reason); })()); - t('⛔ a known key that does not spell its template is unreadable: no head, a 6-hex head, a login GitHub refuses, no mailbox, no lane, a record that is not a number', [`Request: contract-review · PR #26766 · ${HO_HEAD} · x`, 'Request: contract-review · PR #26766 · head 1b03a9 · x', 'Notify: seat-online · domain:skills#2 · -bad-- · mailbox #27120 · takes: none', 'Notify: seat-online · domain:skills#2 · os-bill · takes: none', 'Request: main-red · Test Core · anchor #27090', 'Notify: contract-review PASS · PR #26766 · record six'].every((l) => { const r = readMailboxLine(l); return r.state === 'unreadable' && /does not spell/.test(r.reason); })); - t('lists: `none` is the empty list, `#a #b` the numbers; a comma, an empty list or a `PR #n` item is unreadable', (() => { const ok = readMailboxLine('Notify: split · domain:skills#2 · takes: none · leaves: #1 #2'); return ok.state === 'read' && ok.takes.length === 0 && ok.leaves.join() === '1,2' && ['Notify: split · domain:skills#2 · takes: #1, #2 · leaves: none', 'Notify: split · domain:skills#2 · takes: · leaves: none', 'Notify: split · domain:skills#2 · takes: PR #1 · leaves: none'].every((l) => readMailboxLine(l).state === 'unreadable'); })()); - t('the spaces around the ` · ` separator are optional', readMailboxLine('Request: urgent-triage·#27047·main red').state === 'read' && readMailboxLine('Request: urgent-triage ·#27047· main red').card === 27047); - t('a seat with no number reads (`triage`), and the `LANE#N` placeholder itself does not', readMailboxLine('Notify: split · triage · takes: none · leaves: none').seat.lane === 'triage' && readMailboxLine('Notify: split · triage · takes: none · leaves: none').seat.n === null && readMailboxLine('Notify: split · LANE#N · takes: none · leaves: none').state === 'unreadable'); + t('⛔ a known key that does not spell its template is unreadable: no head, a 6-hex head, a login GitHub refuses, no mailbox, no lane, a record that is not a number', [`Request: contract-review · PR #26766 · ${HO_HEAD} · x`, 'Request: contract-review · PR #26766 · head 1b03a9 · x', 'Notify: seat-online · domain:skills#2 · -bad-- · mailbox #27120 · takes: none', 'Notify: seat-online · domain:skills#2 · os-bill · takes: none', 'Request: main-red · Test Core · anchor #27090', 'Notify: contract-review PASS · PR #26766 · record six'].every((l) => { const r = readMailboxLine(MSG(l)); return r.state === 'unreadable' && /does not spell/.test(r.reason); })); + t('lists: `none` is the empty list, `#a #b` the numbers; a comma, an empty list or a `PR #n` item is unreadable', (() => { const ok = readMailboxLine(MSG('Notify: split · domain:skills#2 · takes: none · leaves: #1 #2')); return ok.state === 'read' && ok.takes.length === 0 && ok.leaves.join() === '1,2' && ['Notify: split · domain:skills#2 · takes: #1, #2 · leaves: none', 'Notify: split · domain:skills#2 · takes: · leaves: none', 'Notify: split · domain:skills#2 · takes: PR #1 · leaves: none'].every((l) => readMailboxLine(MSG(l)).state === 'unreadable'); })()); + t('the spaces around the ` · ` separator are optional', readMailboxLine(MSG('Request: urgent-triage·#27047·main red')).state === 'read' && readMailboxLine(MSG('Request: urgent-triage ·#27047· main red')).card === 27047); + t('a seat with no number reads (`triage`), and the `LANE#N` placeholder itself does not', readMailboxLine(MSG('Notify: split · triage · takes: none · leaves: none')).seat.lane === 'triage' && readMailboxLine(MSG('Notify: split · triage · takes: none · leaves: none')).seat.n === null && readMailboxLine(MSG('Notify: split · LANE#N · takes: none · leaves: none')).state === 'unreadable'); + t('⭐ `To: ` reads as the addressed seat\'s canonical key (`seatKey`\'s), beside the key\'s own fields — `to` never shadows a key\'s own `seat`', (() => { const r = readMailboxLine(DB.sp); return r.to === 'domain:skills#1' && r.seat.lane === 'domain:skills' && r.seat.n === 2 && readMailboxLine(DB.ut).to === 'triage#1' && readMailboxLine(MSG('Notify: split · triage · takes: none · leaves: none', 'Domain:UI#4')).to === 'domain:ui#4'; })()); + t('⛔ a key on the FIRST line — the shape a mailbox took before the seat key — is REFUSED, naming the missing `To:`', ['Notify: split · domain:skills#2 · takes: none · leaves: none', `Request: contract-review · PR #26766 · head ${HO_HEAD} · x`].every((l) => { const r = readMailboxLine(l); return r.state === 'refused' && /`To: `/.test(r.reason) && typeof r.line === 'string'; })); + t('⛔ a `To:` naming a login, or no seat at all, is REFUSED — a login is not a seat', ['os-bill', 'marchtian', '', 'domain:ui#0', 'ui 4'].every((to) => { const r = readMailboxLine(`To: ${to}\nRequest: urgent-triage · #27047 · x`); return r.state === 'refused' && /names no seat/.test(r.reason); })); + t('⛔ a `To:` with no key line under it is refused, and so is a signature or a second `To:` in the key\'s place', [readMailboxLine('To: triage'), readMailboxLine('To: triage\n\n'), readMailboxLine('To: triage\nTo: director\nRequest: urgent-triage · #1 · x')].every((r) => r.state === 'refused')); + t(`the \`To:\` line is the envelope, ⛔ never counted against the ${MAILBOX_LINE_MAX_BYTES}-byte key line, and a blank line may sit between them`, (() => { const key = `Request: urgent-triage · #27047 · ${'x'.repeat(MAILBOX_LINE_MAX_BYTES - 40)}`; return Buffer.byteLength(key, 'utf8') <= MAILBOX_LINE_MAX_BYTES && Buffer.byteLength(MSG(key), 'utf8') > MAILBOX_LINE_MAX_BYTES && readMailboxLine(MSG(key)).state === 'read' && readMailboxLine(`To: triage\n\n${key}`).state === 'read'; })()); + t('decoration on the `To:` line reads as on a key: bold, backticks, a bullet', ['**To:** `triage`', '- To: triage', '`To:` triage'].every((to) => readMailboxLine(`${to}\nRequest: urgent-triage · #1 · x`).to === 'triage#1')); t('⭐ `mailboxEvents` reads a thread: each row\'s comment id, time and author beside its reading (the key id never shadows the comment id), and the half-states by comment id', (() => { const r = mailboxEvents([DB_ROW(DB.cr, 5101, '2026-10-28T07:00:00Z'), DB_ROW('thanks, on it', 5102, '2026-10-28T07:05:00Z'), DB_ROW(DB.on, 5103, '2026-10-28T07:10:00Z')]); return r.state === 'read' && r.events.length === 3 && r.events[0].state === 'read' && r.events[0].id === 'request:contract-review' && r.events[0].comment === 5101 && r.events[0].at === '2026-10-28T07:00:00Z' && r.events[0].by === 'os-bill' && r.events[2].mailbox === 27120 && r.events[2].comment === 5103 && r.halfStates.join() === '5102'; })()); t('a thread that is not an array is a read that could not be made, never an empty one', mailboxEvents(null).state === 'unreadable' && mailboxEvents([]).state === 'read' && mailboxEvents([]).events.length === 0); t('⛔ no gate reads it: the queue guard\'s record reading is untouched — a doorbell comment beside a record on this head still reads FOUND', (() => { const r = locateReviewOfRecord(pairOf({ prComments: [RECORD_ON_9AF9, DB_ROW(DB.nc, 5104)] })); return r.state === 'found' && r.id === 3301; })()); @@ -2889,7 +2918,7 @@ export async function selfTest() { + 'every direction; the copyable template, per face, driven back through the ' + 'locator and printed by `--template`; the `Local-runs:` reading — none, one named probe, absent not re-judged, ' + 'unreadable refused; the reviewer brief printed by `--brief-template` with its read-only shape; the mailbox ' - + 'doorbell — seven keys read, each spelled verbatim in the protocol text, and free text, an acknowledgement, ' + + 'doorbell — `To: ` then seven keys read, each spelled verbatim in the protocol text, and a key with no `To:`, a `To:` naming a login, free text, an acknowledgement, ' + 'an eighth key, a second line and an over-long line refused; the review claim line — read signed and unsigned, ' + 'spelled verbatim in the rule text, lapsing on a moved head or after two hours, the lane winning a tie; the review hold ' + 'line — read signed and unsigned, spelled verbatim in the rule text, per head and cleared only by a new head, an unreadable '