Skip to content

Commit 8c7021d

Browse files
feat(pm): one mailbox per seat, not per account — the opener, the roster and the doorbell reader key it by seat (#27881)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D2cPZcSqcFUZKTUoTXR4rBS)_ Fixes #27637 ## 维护者速读 - 改了什么:信箱从「每账号一个」改为「每席一个」。开信箱必须带 `--seat`,分支、正文首行、消息首行都按席位写;总表按席位行显示信箱,按账号开的旧信箱单列标出,不再挂到该账号的各个席位上。 - 为什么改:一个账号常坐好几个席位,而一个 PR 的事件同一时间只送到一个订阅会话。按账号开的信箱只能叫醒其中一席,其余席位收不到信,发信方和收信方都不知道。 - 风险与回滚:只动 PM 工具脚本和两份规则文本,不碰产品包、不发版。合并后,没有 `To:` 行的旧格式信箱评论会被读成半态,所以三个旧信箱(#27523、#27357、#27309)要由后续卡线程关掉并按席重开。回滚就是 revert 本 PR。 - 先看:`scripts/pm/mailbox-open.mjs` 头注里「The key is the seat, never the login」一节和步骤 ①;`project-forms.md`〈信箱 PR〉的前四行。 - 两分钟验证:`node scripts/pm/mailbox-open.mjs --self-test`、`node scripts/pm/fleet-roster.mjs --self-test`、`node scripts/pm/record-recognisers.mjs --self-test` 三个都应通过;`node scripts/pm/mailbox-open.mjs --repo objectstack-ai/objectstack --seat domain:ui#4 --login os-bill --dry-run` 应打印分支 `mailbox/os-bill/ui-4`。 - 需要你做的:欠契约复审;Tier H,维护者批准。 ## What changes The mailbox PR is keyed by the **seat**, not the account. The shape is the one the card rules: the branch is `mailbox/LOGIN/SLUG` (e.g. `mailbox/os-bill/ui-4` for the seat `domain:ui#4`), the body opens `Mailbox-of: SEAT · LOGIN`, and a message opens `To: SEAT` with the doorbell key on the next line. - **`scripts/pm/mailbox-open.mjs`** - `--seat LANE#N` is required to open. A seat's lane is one of the four a seat post names (`domain:X`, `repo:X`, `triage`, `director`), so a login is never read as a seat. An absent number reads as seat 1. The key is canonical and lower-cased (`triage` becomes `triage#1`). The slug drops the default `domain:` kind and turns `:` and `#` into `-`. - Step ① refuses a second open mailbox for the same **seat**, whichever login holds it. A second seat under the same login opens freely. - A legacy per-login mailbox (head `mailbox/LOGIN`, or a body opening `To: LOGIN`) names no seat. It blocks nothing, and the open prints it so the account closes it. - `mailboxOwner(pr)` is now the one reading of whose mailbox a pull is. `fleet-roster.mjs` and the self-test use it. - The header's argument that "a login holds one seat at a time, so a per-login refusal is a per-seat one" is rewritten, not patched. - Unchanged: `--close`, the `pm:mailbox` / `pinned` / `skip-changeset` labels, `LEDGER_EXCLUSION`, "never merged, never readied, owes no CI", and the `--head` cloud path. - **`scripts/pm/fleet-roster.mjs`** - `mailboxLogin` is replaced by the opener's `mailboxOwner`. Its two callers (`buildRows`, `readLive`) were measured, and nothing outside this file imported it. - The mailbox column is now read per seat row. - A legacy per-login mailbox, or a per-seat one whose seat has no seat post, is listed under the table as assigned to no seat. It is never attributed to the login's rows. It is also in `--json` as `unassignedMailboxes`. - The fixture gains the per-seat, two-seats-under-one-login, head-only, legacy and no-seat-post shapes. - **`scripts/pm/record-recognisers.mjs`** - `readMailboxLine` reads a `To: SEAT` line after the optional signature and before the key, and returns it as `to`. The seat is read with the opener's `seatKey`, imported rather than spelled a second time. - It refuses a message with no `To:` line, a `To:` naming a login, and a `To:` with no key under it. - The 200-byte cap still counts the key line only. - Every key refusal in the doorbell battery is now asked under a `To:` line, so none can pass just because the envelope is missing. - **`.claude/skills/pm-dispatch/references/project-forms.md`** 〈信箱 PR〉: the text and the PR body template now say the same thing. One mailbox per seat, keyed by seat, opened at seating and subscribed by the seated session, closed at hand-over or account switch, and messages that open `To: SEAT`. The line count is unchanged at 54, and the ceiling is 54. - **`.claude/skills/pm-dispatch/references/seat-post-protocol.md`** 〈交接收尾清单〉: step 9 now names the close and the successor's reopen, folded into the existing line. The line count is unchanged at 88, and the ceiling is 88. ## Why One account routinely holds several seats. A PR's activity reaches one subscribed session at a time, so a mailbox keyed by login wakes one of that account's seats and the others never hear. Nothing tells the sender or the recipient. The card's board reading 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 had none. Measured on `origin/main` `ddab8b99`, the writer and the reader also disagreed. The opener wrote `To: LOGIN` as the body's first line, while the roster read `Mailbox-of:`. So every mailbox opened on a `--head` branch was invisible to the roster's mailbox column. Two of the three live mailboxes are on that path (#27523, #27357). This diff gives both sides one spelling, and a self-test case feeds the opener's own body to the reader. ## Verification - `node scripts/pm/dispatch-gates.mjs --commands` on head `c2792c09` derives 38 families, and all 38 ran and exited 0. The `--ran` reconciliation reads 38 derived, 38 run, 0 NOT-MEASURED, 0 UNRUN. `check:doc-formula-expressions` first exited 3 because its prerequisite, the `@objectstack/formula` and `@objectstack/lint` dist, was missing. After `pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` it exited 0, and the record carries that rerun. - These were also run, and each exited 0: - `node scripts/pm/mailbox-open.mjs --self-test`: 67 cases in 10 batteries. - `node scripts/pm/fleet-roster.mjs --self-test`: 82 cases. - `pnpm check:pm-record-recognisers`: 326 cases. - `node scripts/pm/check-skill-line-ratchet.mjs`. - `node scripts/pm/check-governed-queue-guard.mjs --self-test` and `node scripts/pm/check-half-states.mjs --self-test`. These are the two that the new `record-recognisers` → `mailbox-open` import could have deadlocked through a top-level-await cycle. Neither did. - `pnpm check:pm-governed-prose`. - `pnpm lint`. - `origin/main` moved 7 commits past the base, to `c91a004f`. None of them touches a path in this diff. Mutation probes. Each change was reverted in a scratch copy of the file, the self-test was run, and the copy was deleted: - `openMailboxesOf` matching by login turns `mailbox-open --self-test` red: "two seats under ONE login both open". - The roster's mailbox column attributed by account turns `fleet-roster --self-test` red, with 3 cases. - A `To:` line that accepts any value turns `record-recognisers --self-test` red, with 4 cases. In-flight intersection, re-run before the first edit: of the 17 open PRs on objectstack at about 01:55Z, 13 are not mailboxes or the Version Packages PR. All 13 heads were fetched and diffed against their merge base, and 0 touch any path in this diff. Clause-②: no Metadata-protocol: none ## Migration (card-thread follow-up after landing, not this diff) - Each of #27523, #27357 and #27309 is closed by its account (`mailbox-open.mjs --close N`) and reopened once per seat (`--seat LANE#N`). - ⚠️ `refs/heads/mailbox/os-justin` (#27309's head) must be **deleted after #27309 closes** and before that account's first per-seat open on the default path. Git refuses a ref nested under an existing ref. This was measured on 2026-10-30 on a local bare repository, where pushing `mailbox/os-justin/server-3` beside `mailbox/os-justin` was rejected with `failed to update ref`. The opener's header and its push-failure message say so. - Until then, a patrol reading the legacy mailboxes sees their old key-first comments as half-states (no `To:` line). That is report-only. ## Deviations - **Tier.** The register (`GOVERNED_SURFACES`, `scripts/pm/check-governed-merges.mjs`) rows `.claude/**` on objectstack as **Tier S**, and the only governed paths here are `.claude/skills/pm-dispatch/references/*`. Triage (comment 6501344388) writes **Tier H**. The seat takes the stricter one, **Tier H**, and the maintainer approves. - **File surface.** The claim's surface was amended before push to add `scripts/pm/record-recognisers.mjs` (a `To: SEAT` line broke its reader, measured: refused above the key, unreadable below it), `scripts/pm/fleet-roster.fixture.json` (the roster self-test's snapshot), and `seat-post-protocol.md` 〈交接收尾清单〉 (the hand-over steps live there, not in `project-forms.md`). - The `mailbox-open` and `fleet-roster` self-tests are not wired into any CI job or `package.json` script on `main`. They were run by hand here, and wiring them is out of scope. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (Not touched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (Publishes nothing: `skip-changeset`.) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not touched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. (Except the Chinese 维护者速读 channel.) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4d1d1pooSGzvbJymLKy)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 92493e3 commit 8c7021d

6 files changed

Lines changed: 348 additions & 154 deletions

File tree

‎.claude/skills/pm-dispatch/references/project-forms.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@
3333

3434
## 信箱 PR
3535

36-
- 每席一个 draft PR:分支 `mailbox/<login>`,标签 `pm:mailbox`+`pinned`,正文首行 `To: <login>`。
37-
- 坐席即开(`mailbox-open.mjs`)并订阅,存根指针行记其号;永不合并,移交或换账号关旧开新。
38-
- `pm:mailbox` PR 不欠 CI,一切落地与台账查询排除它。
39-
- 首行恒为下列七键之一(发→收;何时):一事一行 ≤200 字节,只写键与编号;表外即半态。
36+
- 每席一个 draft PR,键是席位 ⛔ 非账号:分支 `mailbox/<login>/<slug>`,标签 `pm:mailbox`+`pinned`。
37+
- 正文首行 `Mailbox-of: <席位> · <login>`;坐席即开(`mailbox-open.mjs`)并订阅,存根指针行记其号。
38+
- 永不合并,不欠 CI,落地与台账查询排除它;移交或换账号关旧开新;评论首行 `To: <席位>`。
39+
- 次行恒为下列七键之一(发→收;何时):一事一行 ≤200 字节,只写键与编号;表外即半态。
4040
- `Request: contract-review · PR #n · head SHA · REASON`:车道→分诊;本席复审档不可用,一 head 一次。
4141
- `Request: urgent-triage · #n · ONE-LINE-CRITERION`:任一席→分诊;p0 嫌疑、main 红、确定性队列红。
4242
- `Request: main-red · CHECK-NAME · anchor #n · suggested lane X`:发现席→分诊(缺则总监);锚卡立起时。

‎.claude/skills/pm-dispatch/references/seat-post-protocol.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
6. 贴正文改 `⏳ vacant` + 台账:前任会话 ID、注销时间、队列快照、串行队、跨车道备忘。
5252
7. 收班简报与审计评论存档;接任指引 = `/pm-dispatch 接手` + 先读座位贴。
5353
8. 在飞卡与留跑会话贴里点名(卡号+派发会话 ID);派出的会话离任 PM 自己 `archive_session`。
54-
9. 向维护者交最终报告。
54+
9. 关本席信箱(`mailbox-open.mjs --close N`),接任者坐席时按席重开;向维护者交最终报告。
5555

5656
## 座位 Routine 创建
5757

‎scripts/pm/fleet-roster.fixture.json‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@
2323
],
2424
"pulls": [
2525
{ "number": 25427, "state": "open", "head": { "ref": "claude/project-thread-aaaa" }, "body": "Fixes #25412", "labels": [] },
26-
{ "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" }] },
26+
{ "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" }] },
27+
{ "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" }] },
28+
{ "number": 25394, "state": "open", "head": { "ref": "mailbox/os-sales/devx-2" }, "body": "", "labels": [{ "name": "pm:mailbox" }] },
29+
{ "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" }] },
30+
{ "number": 25396, "state": "open", "head": { "ref": "claude/project-thread-ffff" }, "body": "Mailbox-of: domain:spec#5 · os-tesla", "labels": [{ "name": "pm:mailbox" }] },
2731
{ "number": 25433, "state": "open", "head": { "ref": "claude/project-thread-cccc" }, "body": "Fixes #25417", "labels": [] }
2832
],
2933
"comments": {

‎scripts/pm/fleet-roster.mjs‎

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,16 @@
5252
* last activity the newest `updated_at` of the post and its in-flight
5353
* cards. A seated seat older than `STALE_HOURS` with no
5454
* wall note is flagged 「疑似撞墙」.
55-
* mailbox open `pm:mailbox` pulls of the seat's account, by head
56-
* `mailbox/<login>` or the body's `Mailbox-of: <login>`.
55+
* mailbox open `pm:mailbox` pulls of the SEAT, whichever account
56+
* holds it: the seat on the body's first line
57+
* `Mailbox-of: <seat> · <login>`, or the head
58+
* `mailbox/<login>/<seat-slug>` (`mailboxOwner`, the
59+
* opener's one reading). A legacy per-login mailbox names
60+
* no seat, and an account holds several seats, so which of
61+
* them reads it cannot be told: it is listed under the
62+
* table as assigned to no seat, ⛔ never shown on the
63+
* login's rows. So is a per-seat one whose seat no post
64+
* carries.
5765
*
5866
* The footer names the generation time and every read made, so a reader can
5967
* re-run each one.
@@ -88,10 +96,11 @@ import { fileURLToPath } from 'node:url';
8896
import { isEntrypoint } from '../invoked-as.mjs';
8997
import { validateStroke } from './fleet-write/validate.mjs';
9098
import { ghApi, PER_PAGE, readAllPages } from './gh-pages.mjs';
91-
// The mailbox label, its ledger exclusion and its branch prefix have ONE owner,
92-
// the mailbox opener (#25417); this file reads them, never re-spells them.
99+
// The mailbox label, its ledger exclusion, its branch prefix and the reading of
100+
// whose mailbox a pull is have ONE owner, the mailbox opener (#25417, keyed by
101+
// seat since #27637); this file reads them, never re-spells them.
93102
import * as mailboxOpen from './mailbox-open.mjs';
94-
import { LEDGER_EXCLUSION, MAILBOX_BRANCH_PREFIX, MAILBOX_LABEL } from './mailbox-open.mjs';
103+
import { LEDGER_EXCLUSION, MAILBOX_BRANCH_PREFIX, MAILBOX_LABEL, mailboxOwner, seatKey } from './mailbox-open.mjs';
95104
// The seat record's heading and verdict have ONE reading, shared with H69 in
96105
// check-half-states; this file asks it, never re-spells the heading.
97106
import * as seatRecord from './seat-record.mjs';
@@ -232,12 +241,30 @@ export function hasLandingVerdict(comments) {
232241
});
233242
}
234243

235-
/** The login whose mailbox a `pm:mailbox` pull is, or null. Pure. */
236-
export function mailboxLogin(pr) {
237-
if (!labelNames(pr).includes(MAILBOX_LABEL)) return null;
238-
const ref = String(pr?.head?.ref ?? '');
239-
if (ref.startsWith(MAILBOX_BRANCH_PREFIX)) return ref.slice(MAILBOX_BRANCH_PREFIX.length);
240-
return /^Mailbox-of: (\S+)/m.exec(String(pr?.body ?? ''))?.[1] ?? null;
244+
/** A seat row's mailbox slug — its `LANE#N` as the opener spells it in a ref — or null. Pure. */
245+
const seatSlugOf = (row) => seatKey(`${row.lane}#${row.seat}`)?.slug ?? null;
246+
247+
/** Every open `pm:mailbox` pull in the snapshot, with whose it is (`mailboxOwner`), ordered by ref. Pure. */
248+
export function readMailboxes(snapshot) {
249+
const out = [];
250+
for (const repo of Object.keys(snapshot?.repos ?? {}).sort()) {
251+
for (const pr of snapshot.repos[repo]?.pulls ?? []) {
252+
const owner = pr?.state === 'closed' ? null : mailboxOwner(pr);
253+
if (owner !== null) out.push({ ref: ref(repo, pr.number), ...owner });
254+
}
255+
}
256+
return out.sort((a, b) => byRef(a.ref, b.ref));
257+
}
258+
259+
/**
260+
* The mailboxes no seat row reads — flagged under the table, ⛔ never attributed: a legacy per-login mailbox
261+
* (it names no seat, and its login holds several), or a per-seat one whose seat no seat post carries. Pure.
262+
*/
263+
export function unassignedMailboxes(snapshot, rows) {
264+
const read = new Set(rows.map(seatSlugOf).filter(Boolean));
265+
return readMailboxes(snapshot)
266+
.filter((m) => m.slug === null || !read.has(m.slug))
267+
.map((m) => ({ ...m, why: m.slug === null ? 'legacy per-login, names no seat' : 'no seat post carries this seat' }));
241268
}
242269

243270
const cardLaneMatch = (card, row) => {
@@ -275,16 +302,12 @@ export function buildRows(snapshot, { now }) {
275302
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));
276303

277304
const landing = new Map(); // card key → [pull refs]
278-
const mailboxes = [];
279305
for (const { repo, pr, comments } of pulls) {
280-
const owner = mailboxLogin(pr);
281-
if (owner !== null) {
282-
mailboxes.push({ login: lc(owner), ref: ref(repo, pr.number) });
283-
continue;
284-
}
285-
if (labelNames(pr).includes(MAILBOX_LABEL) || !String(pr?.head?.ref ?? '').startsWith(CARD_BRANCH_PREFIX) || !hasLandingVerdict(comments)) continue;
306+
// Every `pm:mailbox` pull is a mailbox, whosever it reads as, and ⛔ never counts as landing.
307+
if (mailboxOwner(pr) !== null || !String(pr?.head?.ref ?? '').startsWith(CARD_BRANCH_PREFIX) || !hasLandingVerdict(comments)) continue;
286308
for (const k of closingRefs(pr.body, repo)) landing.set(k, [...(landing.get(k) ?? []), ref(repo, pr.number)]);
287309
}
310+
const mailboxes = readMailboxes(snapshot);
288311

289312
const out = rows.map((row) => {
290313
const acct = lc(row.account);
@@ -304,7 +327,8 @@ export function buildRows(snapshot, { now }) {
304327
awaitingLanding: awaitingLanding.sort(byRef),
305328
lastActivity,
306329
suspectedWall: row.status === 'seated' && row.wallNote === null && ageHours !== null && ageHours > STALE_HOURS,
307-
mailbox: row.account ? mailboxes.filter((m) => m.login === acct).map((m) => m.ref).sort(byRef) : [],
330+
// Per seat, never per account: one account holds several seats, and each seat opens its own.
331+
mailbox: mailboxes.filter((m) => m.slug !== null && m.slug === seatSlugOf(row)).map((m) => m.ref),
308332
};
309333
});
310334

@@ -341,8 +365,8 @@ export function readsFor(repos) {
341365
]);
342366
}
343367

344-
/** The roster body, markdown. Pure. */
345-
export function renderBody(rows, { now, repos, session = null }) {
368+
/** The roster body, markdown; `unassigned` is `unassignedMailboxes`' list. Pure. */
369+
export function renderBody(rows, { now, repos, session = null, unassigned = [] }) {
346370
const lines = [
347371
ROSTER_MARKER,
348372
'# 车队总表 — fleet roster',
@@ -365,6 +389,9 @@ export function renderBody(rows, { now, repos, session = null }) {
365389
lines.push(
366390
'',
367391
`${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).`,
392+
...(unassigned.length
393+
? ['', `⚠️ ${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(' · ')}.`]
394+
: []),
368395
'',
369396
`Generated ${now} by \`node scripts/pm/fleet-roster.mjs --repos ${repos.join(',')}\` from:`,
370397
...readsFor(repos).map((q) => `- \`${q}\``),
@@ -424,7 +451,7 @@ function readLive(repos) {
424451
const dispatched = new Set(Object.entries(snapshot.repos).flatMap(([repo, s]) => s.dispatched.map((i) => `${repo}#${i.number}`)));
425452
for (const [repo, s] of Object.entries(snapshot.repos)) {
426453
for (const p of s.pulls) {
427-
if (mailboxLogin(p) !== null || labelNames(p).includes(MAILBOX_LABEL) || !String(p.head?.ref ?? '').startsWith(CARD_BRANCH_PREFIX)) continue;
454+
if (mailboxOwner(p) !== null || !String(p.head?.ref ?? '').startsWith(CARD_BRANCH_PREFIX)) continue;
428455
if (!closingRefs(p.body, repo).some((k) => dispatched.has(k))) continue;
429456
s.comments[String(p.number)] = need(`/repos/${repo}/issues/${p.number}/comments?per_page=100`);
430457
}
@@ -478,12 +505,13 @@ export async function main(argv, { env = process.env } = {}) {
478505
}
479506
const now = new Date(opts.now ?? snapshot.now ?? Date.now()).toISOString().replace(/\.\d{3}Z$/, 'Z');
480507
const rows = buildRows(snapshot, { now });
508+
const unassigned = unassignedMailboxes(snapshot, rows);
481509
if (opts.json) {
482-
console.log(JSON.stringify({ now, repos, rows }, null, 2));
510+
console.log(JSON.stringify({ now, repos, rows, unassignedMailboxes: unassigned }, null, 2));
483511
return EXIT_OK;
484512
}
485513
const session = env.OS_FLEET_SESSION || env.CLAUDE_CODE_REMOTE_SESSION_ID || null;
486-
const body = renderBody(rows, { now, repos, session });
514+
const body = renderBody(rows, { now, repos, session, unassigned });
487515
if (opts.write === null) {
488516
console.log(body);
489517
return EXIT_OK;
@@ -523,10 +551,10 @@ const SELF_TEST_BATTERIES = Object.freeze({
523551
'arguments: modes and every refusal': 9,
524552
'seat titles: lane, seat, repo, status, account, project': 13,
525553
'wall notes, closing refs, verdicts and mailboxes': 13,
526-
'the fixture snapshot: rows for the three repos': 15,
527-
'the rendered body: marker, columns, footer, determinism': 8,
554+
'the fixture snapshot: rows for the three repos': 17,
555+
'the rendered body: marker, columns, footer, determinism': 9,
528556
'refusing to overwrite what is not the roster': 6,
529-
'the CLI: fixture runs offline, usage errors are exit 2': 5,
557+
'the CLI: fixture runs offline, usage errors are exit 2': 6,
530558
'paging: a three-page read through the loop equals the slurped array': 4,
531559
});
532560
const SELF_TEST_BATTERY_FLOOR = 8;
@@ -601,7 +629,7 @@ async function selfTest() {
601629
t('⛔ an ACCEPT line ABOVE the heading is not the record\'s verdict', hasLandingVerdict([{ body: 'ACCEPT\n\n## 席位核对\n\nREWORK · rework' }]), false);
602630
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']);
603631
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]);
604-
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]);
632+
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']);
605633

606634
battery('the fixture snapshot: rows for the three repos');
607635
const fixture = JSON.parse(readFileSync(FIXTURE_PATH, 'utf8'));
@@ -628,7 +656,10 @@ async function selfTest() {
628656
t('a PR with no verdict does not count', row(1353, 'objectstack-ai/hotcrm').awaitingLanding, []);
629657
t('an unassigned ruling sits on the lane’s lowest seated seat', [row(6023).awaitingRuling, row(20163).awaitingRuling], [[{ repo: 'objectstack-ai/objectstack', number: 25373 }], []]);
630658
t('a ruling assigned to seat 2’s account sits on seat 2', row(19287).awaitingRuling, [{ repo: 'objectstack-ai/objectstack', number: 25344 }]);
631-
t('the mailbox column is the account’s pm:mailbox PR', [row(6023).mailbox, row(20163).mailbox], [[{ repo: 'objectstack-ai/objectstack', number: 25392 }], []]);
659+
const os = (number) => ({ repo: 'objectstack-ai/objectstack', number });
660+
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)]]);
661+
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, []]);
662+
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']], [[], []]]);
632663
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]);
633664
t('last activity is the newest of post and in-flight cards', row(6023).lastActivity, '2026-10-23T13:28:00Z');
634665
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() {
641672
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]);
642673
t('the suspected wall is spelled in the row', body.includes(`⚠️ ${SUSPECTED_WALL}`), true);
643674
t('a shared seat reads = seat N in the in-flight cell', body.includes('| = seat 1 |'), true);
675+
t('the mailboxes on no seat row are flagged under the table, each with its owner and why', (() => {
676+
const flagged = renderBody(rows, { now: fixture.now, repos: Object.keys(fixture.repos).sort(), unassigned: unassignedMailboxes(fixture, rows) });
677+
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')];
678+
})(), [true, true, true, false]);
644679
t('the attribution footer closes the body', body.endsWith('\n\n---\n_Generated by [Claude Code](https://claude.ai/code/session_01ABC)_'), true);
645680
const shuffled = { ...fixture, repos: Object.fromEntries(Object.entries(fixture.repos).reverse().map(([k, v]) => [k, { ...v, seats: [...v.seats].reverse() }])) };
646681
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() {
683718
} catch {}
684719
t('--fixture --json exits 0 with no gh on PATH (nothing reached for)', jsonRun.status, EXIT_OK);
685720
t('the printed rows are the built rows', parsed?.rows?.length, rows.length);
721+
t('the printed JSON carries the mailboxes on no seat row', parsed?.unassignedMailboxes?.map((m) => m.ref.number), [25395, 25396]);
686722
t('--fixture prints the markdown table', cli(['--fixture', FIXTURE_PATH]).stdout.split('\n')[0], ROSTER_MARKER);
687723
t('a usage error is exit 2', cli(['--write', 'x']).status, EXIT_USAGE);
688724
t('a missing fixture is exit 3', cli(['--fixture', join(PM_DIR, 'no-such-fixture.json')]).status, EXIT_PREREQUISITE);

0 commit comments

Comments
 (0)