You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 8c7021d
Browse filesBrowse the repository at this point in the historyBrowse 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>
`${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
+
: []),
368
395
'',
369
396
`Generated ${now} by \`node scripts/pm/fleet-roster.mjs --repos ${repos.join(',')}\` from:`,
'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,
528
556
'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,
530
558
'paging: a three-page read through the loop equals the slurped array': 4,
531
559
});
532
560
constSELF_TEST_BATTERY_FLOOR=8;
@@ -601,7 +629,7 @@ async function selfTest() {
601
629
t('⛔ an ACCEPT line ABOVE the heading is not the record\'s verdict',hasLandingVerdict([{body: 'ACCEPT\n\n## 席位核对\n\nREWORK · rework'}]),false);
602
630
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']);
603
631
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']);
605
633
606
634
battery('the fixture snapshot: rows for the three repos');
t('a PR with no verdict does not count',row(1353,'objectstack-ai/hotcrm').awaitingLanding,[]);
629
657
t('an unassigned ruling sits on the lane’s lowest seated seat',[row(6023).awaitingRuling,row(20163).awaitingRuling],[[{repo: 'objectstack-ai/objectstack',number: 25373}],[]]);
630
658
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
+
constos=(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']],[[],[]]]);
632
663
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]);
633
664
t('last activity is the newest of post and in-flight cards',row(6023).lastActivity,'2026-10-23T13:28:00Z');
634
665
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() {
641
672
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]);
642
673
t('the suspected wall is spelled in the row',body.includes(`⚠️ ${SUSPECTED_WALL}`),true);
643
674
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',(()=>{
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]);
644
679
t('the attribution footer closes the body',body.endsWith('\n\n---\n_Generated by [Claude Code](https://claude.ai/code/session_01ABC)_'),true);
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() {
683
718
}catch{}
684
719
t('--fixture --json exits 0 with no gh on PATH (nothing reached for)',jsonRun.status,EXIT_OK);
685
720
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]);
686
722
t('--fixture prints the markdown table',cli(['--fixture',FIXTURE_PATH]).stdout.split('\n')[0],ROSTER_MARKER);
687
723
t('a usage error is exit 2',cli(['--write','x']).status,EXIT_USAGE);
688
724
t('a missing fixture is exit 3',cli(['--fixture',join(PM_DIR,'no-such-fixture.json')]).status,EXIT_PREREQUISITE);
0 commit comments