Skip to content

Commit f4826f8

Browse files
fix(pm): mailbox doorbell key fields read the seat with seatKey, which refuses reserved domain lane names (#27986)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DGworhuv2mwSdzy4ZEH46c4)_ Fixes #27891 ## What changes - `scripts/pm/record-recognisers.mjs`: the seven doorbell keys' `LANE#N` fields (`Notify: seat-online`, `seat-offline`, `split`) are read with `seatKey`, imported from `mailbox-open.mjs`. The second, looser seat shape `SEAT_REF` is deleted. The short form `skills#2` is now refused in a key field (`unreadable`, "does not spell its template"), exactly as it is on the `To:` line. An absent number reads as seat 1 (`triage` → `triage#1`), and the `seat` reading carries the canonical `key` beside `lane` and `n`. - `scripts/pm/mailbox-open.mjs`: `seatKey` refuses a `domain:` lane named after a reserved lane word — `domain:triage`, `domain:director`, `domain:repo-…` — whose slug would be another seat's (`triage-1`, `repo-cloud-1`). The lane words are lifted into the exported `SEAT_LANE_KINDS` and `SEAT_BARE_LANES`, the one spelling that `SEAT_SHAPE` and the reserved set are both built from. A name that merely contains a reserved word (`domain:triage-tools`, `domain:directory`) still reads. - Pins: 4 new cases in the mailbox-open self-test (short form refused, reserved name refused, containing-a-word still reads, the shape and the reserved set share one list), 3 new and 1 reworded in the recognisers' doorbell battery (well-formed `domain:X#N` reads as before with its `key`; the short form refused in a key field and on `To:`; a reserved name refused in both places). ## Why PR #27881 (card #27637) keyed the mailbox by seat and read the `To:` line with `seatKey`, but left the key fields on `SEAT_REF`, which accepted `skills#2`. One message could then name a seat `To:` refused — two grammars for one thing. The contract review of #27881 (record 6513700361, should-fixes 1 and 4) asked for one grammar and for the reserved-word refusal the `mailbox-open.mjs` header assumed but did not enforce; triage 6513886980 ruled both in, and ruled that a well-formed `To: domain:X#N` line behaves exactly as before. `Clause-②: no` — the accepted set narrows (the short form and the reserved names are refused; nothing new is accepted). No gate, workflow or queue guard reads a mailbox, so nothing lands or fails differently. `.claude/skills/pm-dispatch/references/project-forms.md` 〈信箱 PR〉 is untouched: it spells `LANE#N` and `To: <席位>`, neither of which now reads wrong. ## Verification - `node scripts/pm/mailbox-open.mjs --self-test` — 71 cases in 10 batteries pass (was 67). - `node scripts/pm/record-recognisers.mjs --self-test` — 329 cases pass (was 326). - Ablations, each run once and the tree restored: dropping the reserved refusal in `seatKey` fails the two reserved pins in mailbox-open and the reserved pin in the recognisers; letting `readSeat` fall back to the old loose shape fails the short-form pin and the reserved pin; dropping `key` from the reading fails the three `key`-carrying pins. Nothing else moves. - Gate families derived by `node scripts/pm/dispatch-gates.mjs --commands` (2 paths vs merge base 9cd54fa): 31 families (`check-closing-keyword-parity`, `check-declaration-mirrors`, `check-self-test-wired`, `check-self-test-workflow-commands`, `check-whole-set-label-write`, `plan-test-core-shards --self-test`, `bare-root-worklist --self-test`, `check:pm-record-recognisers`, `check:pm-dispatch-gates`, `check:scripts-symbol-anchors`, `check:entry-guard`, `check:parse-guard` and the rest of the derived list) all exit 0 after `pnpm install --frozen-lockfile --ignore-scripts`; `--ran` with the recorded codes: 31 accounted for, 31 run, 0 NOT-MEASURED. `eslint` on the two files exit 0. `git merge-tree` against `origin/main` 4a5396b clean; no commit on main since the base touches either file. Clause-②: no Metadata-protocol: none ## 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 on the two files. - [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. (nothing publishes: `scripts/pm/**` only, `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. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. ## 维护者速读 - 改了什么:信箱门铃七键里的 `LANE#N` 字段改用 `seatKey` 读,短写 `skills#2` 一律拒收;`seatKey` 另拒收撞保留车道词的 `domain:` 名(`domain:triage`、`domain:repo-…`)。 - 为什么改:#27881 之后 `To:` 行与键字段各用一套席位语法,一条消息可能一边读通一边拒收;分诊 6513886980 裁定统一到一套。 - 风险与回滚:只收窄接受集;无门禁、workflow 或队列守卫读信箱,落地行为不变。回滚即 revert 本 PR。 - 先看:`scripts/pm/record-recognisers.mjs` 的 `readSeat`,与 `scripts/pm/mailbox-open.mjs` 的 `seatKey`。 - 两分钟验证:`node scripts/pm/mailbox-open.mjs --self-test && node scripts/pm/record-recognisers.mjs --self-test`。 - 需要你做的:无。契约复审欠席位(contract review owed);落地档以登记簿为准(`check-governed-merges.mjs --pr`)。 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016WV4d1a5iE1F79QjJupxQe Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6dc06b2 commit f4826f8

2 files changed

Lines changed: 40 additions & 12 deletions

File tree

‎scripts/pm/mailbox-open.mjs‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
* canonically with its number (`triage#1`). Its SLUG is that key as one ref
4848
* segment, because a ref cannot carry `:` or `#`: the default `domain:` kind
4949
* dropped, every other `:` and the `#` turned into `-` (`ui-4`,
50-
* `repo-cloud-1`, `triage-1`). No `domain:` lane is named `triage`,
51-
* `director` or `repo-…`, which is what keeps the slug one-to-one.
50+
* `repo-cloud-1`, `triage-1`). No `domain:` lane may be named `triage`,
51+
* `director` or `repo-…` — `seatKey` refuses one — which keeps the slug one-to-one.
5252
*
5353
* ## What it does, in order (open)
5454
*
@@ -159,20 +159,34 @@ const LOGIN_SHAPE = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
159159
const BRANCH_SHAPE = /^(?!\/|.*\/\/|.*\.\.|.*[\s~^:?*[\\]|.*\.lock$|.*\/$)[A-Za-z0-9._/-]+$/;
160160
const SESSION_SHAPE = /^(?:session|cse)_[A-Za-z0-9]+$/;
161161
// A seat: its lane — `domain:<x>`, `repo:<x>`, `triage`, `director`, the four a seat post's title names — then `#N`;
162-
// absent = seat 1. Read lower-cased. The bare lanes are a closed pair, so a login is never read as a seat.
163-
const SEAT_SHAPE = /^((?:domain|repo):[a-z0-9._-]+|triage|director)(?:#([1-9][0-9]*))?$/;
162+
// absent = seat 1. Read lower-cased. The bare lanes are a closed pair, so a login is never read as a seat. The
163+
// two lists below are the ONE spelling of those lane words: the shape is built from them, and so is the
164+
// reserved set a `domain:` lane may not be named after (`seatKey`).
165+
/** The lane kinds that carry a name after the colon: `domain:<x>`, `repo:<x>`. `domain` is the default kind the slug drops. */
166+
export const SEAT_LANE_KINDS = Object.freeze(['domain', 'repo']);
167+
/** The bare lanes, a closed pair: `triage`, `director`. */
168+
export const SEAT_BARE_LANES = Object.freeze(['triage', 'director']);
169+
const SEAT_SHAPE = new RegExp(`^((?:${SEAT_LANE_KINDS.join('|')}):[a-z0-9._-]+|${SEAT_BARE_LANES.join('|')})(?:#([1-9][0-9]*))?$`);
170+
// What a `domain:` lane may NOT be named, so the slug stays one-to-one (header): a bare lane word (`domain:triage`
171+
// would slug to `triage-1`, the triage seat's), or another kind's name with its colon turned into the hyphen the slug
172+
// uses (`domain:repo-cloud` would slug to `repo-cloud-1`, `repo:cloud#1`'s).
173+
const RESERVED_DOMAIN_LANE = new RegExp(`^(?:(?:${SEAT_BARE_LANES.join('|')})$|(?:${SEAT_LANE_KINDS.filter((k) => k !== 'domain').join('|')})-)`);
164174
/** A per-seat mailbox body's first line, `Mailbox-of: <seat> · <login>`. */
165175
const MAILBOX_OF_LINE = /^Mailbox-of:[ \t]*(\S+)[ \t]*·[ \t]*(\S+)$/;
166176
/** The first lines a legacy per-login mailbox opened with: `To: <login>` (this file's), `Mailbox-of: <login>`. */
167177
const LEGACY_OWNER_LINE = /^(?:To|Mailbox-of):[ \t]*(\S+)$/;
168178

169179
/**
170180
* A seat spelled `LANE#N` → `{ key, slug }`, or null when it is not one. `key` is canonical — lower case, the
171-
* number always spelled (`triage` → `triage#1`); `slug` is the key as one ref segment (header). Pure.
181+
* number always spelled (`triage` → `triage#1`); `slug` is the key as one ref segment (header). A `domain:` lane
182+
* named after a reserved lane word (`domain:triage`, `domain:repo-cloud`) is not a seat either: its slug would be
183+
* another seat's, and the header's one-to-one promise is kept here, not assumed. This is the fleet's ONE seat
184+
* grammar — the mailbox `To:` line and the doorbell keys' `LANE#N` fields read with it, never a second shape. Pure.
172185
*/
173186
export function seatKey(text) {
174187
const m = SEAT_SHAPE.exec(String(text ?? '').trim().toLowerCase());
175188
if (!m) return null;
189+
if (m[1].startsWith('domain:') && RESERVED_DOMAIN_LANE.test(m[1].slice('domain:'.length))) return null;
176190
const n = m[2] ?? '1';
177191
return { key: `${m[1]}#${n}`, slug: `${m[1].replace(/^domain:/, '').replace(/:/g, '-')}-${n}` };
178192
}
@@ -519,6 +533,10 @@ async function selfTest() {
519533
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']);
520534
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']);
521535
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]);
536+
t('⛔ a short form is not a seat — `skills#2` names no lane, so it is refused as a login is', ['skills#2', 'ui#4', 'server', 'skills'].map(seatKey), [null, null, null, null]);
537+
t('⛔ a `domain:` lane named after a reserved lane word is refused — its slug would be another seat\'s', ['domain:triage', 'domain:director#2', 'domain:repo-cloud#1', 'Domain:Triage#1'].map(seatKey), [null, null, null, null]);
538+
t('…while a `domain:` lane that merely CONTAINS a reserved word, and the reserved words as their own lanes, still read', [seatKey('domain:triage-tools#1').slug, seatKey('domain:directory').slug, seatKey('domain:repository').slug, seatKey('triage#2').slug, seatKey('repo:cloud').slug], ['triage-tools-1', 'directory-1', 'repository-1', 'triage-2', 'repo-cloud-1']);
539+
t('the shape and the reserved set are built from the ONE pair of lane-word lists', SEAT_LANE_KINDS.join() === 'domain,repo' && SEAT_BARE_LANES.join() === 'triage,director' && SEAT_BARE_LANES.every((w) => seatKey(w)?.key === `${w}#1` && seatKey(`domain:${w}`) === null), true);
522540
t('the branch is mailbox/<login>/<slug> — no `:` and no `#` in a ref', mailboxBranch('os-bill', seatKey('domain:ui#4')), 'mailbox/os-bill/ui-4');
523541
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']);
524542

‎scripts/pm/record-recognisers.mjs‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,18 +1266,23 @@ const RECORD_REF = /^record `?#?([1-9][0-9]*)`?$/;
12661266
const BRIEF_REF = /^brief `?#?([1-9][0-9]*)`?$/;
12671267
const LANE_REF = /^suggested lane ([A-Za-z][A-Za-z0-9:_-]*)$/;
12681268
const VERDICT_REF = /^contract-review (PASS|FAIL)$/;
1269-
/** `LANE#N` — `domain:skills#2`, `skills#1`, or a seat that carries no number (`triage`, `director`). */
1270-
const SEAT_REF = /^([A-Za-z][A-Za-z0-9:_-]*)(?:#([1-9][0-9]*))?$/;
12711269
/** GitHub's own login rule, as `mailbox-open.mjs` spells it for `--login`: alphanumerics and single inner hyphens, ≤ 39. */
12721270
const LOGIN_REF = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
12731271
/** The attribution block the platform APPENDS to a comment (AGENTS.md): a rule line and the bare or session-URL footer. Not a second line. */
12741272
const FOOTER_RULE = /^-{3,}$/;
12751273
const FOOTER_LINE = /^_Generated by \[Claude Code\]\(https:\/\/claude\.ai\/code(?:\/session_[A-Za-z0-9]+)?\)_$/;
12761274

1277-
/** `LANE#N` → `{ lane, n }`, or null. */
1275+
/**
1276+
* A key's `LANE#N` field → `{ key, lane, n }`, or null. Read with `seatKey`, the ONE seat grammar (#27891): the
1277+
* lane is `domain:<x>`, `repo:<x>`, `triage` or `director`, so the short form `skills#2` is refused here exactly
1278+
* as on the `To:` line, an absent number is seat 1 (`triage` → `triage#1`), and a `domain:` lane named after a
1279+
* reserved lane word is no seat. `key` is the canonical spelling; `lane` and `n` are its two halves.
1280+
*/
12781281
function readSeat(part) {
1279-
const m = SEAT_REF.exec(String(part ?? '').trim());
1280-
return m ? { lane: m[1], n: m[2] === undefined ? null : Number(m[2]) } : null;
1282+
const s = seatKey(part);
1283+
if (!s) return null;
1284+
const at = s.key.lastIndexOf('#');
1285+
return { key: s.key, lane: s.key.slice(0, at), n: Number(s.key.slice(at + 1)) };
12811286
}
12821287

12831288
/** `NAME: #a #b …` → the numbers; `NAME: none` → `[]`; anything else (a comma, a word, an empty list) → null. */
@@ -2572,7 +2577,9 @@ export async function selfTest() {
25722577
// key, a second line and an over-long line — the half-state faces — and pin
25732578
// each key's spelling to the protocol text, so the reader and the text cannot
25742579
// drift apart. Every refusal of a KEY is asked under a `To:` line, so it
2575-
// cannot pass vacuously on the missing envelope.
2580+
// cannot pass vacuously on the missing envelope. A key's `LANE#N` field is
2581+
// read with `seatKey` (#27891): the short form and a reserved `domain:` name
2582+
// are refused there exactly as on `To:`.
25762583
battery('⭐ the mailbox doorbell: `To: <seat>`, then one of the seven keys read — seat, kind, numbers, sha, login, time — and anything else refused');
25772584
const DB_ROW = (body, id = 5101, at = '2026-10-28T07:00:00Z') => ({ id, created_at: at, user: { login: 'os-bill' }, body });
25782585
// A message: the addressee line, then the key line.
@@ -2603,7 +2610,10 @@ export async function selfTest() {
26032610
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); }));
26042611
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'); })());
26052612
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);
2606-
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');
2613+
t('a seat with no number reads as seat 1 (`triage` → `triage#1`, as `seatKey` spells it), and the `LANE#N` placeholder itself does not', (() => { const r = readMailboxLine(MSG('Notify: split · triage · takes: none · leaves: none')); return r.seat.lane === 'triage' && r.seat.n === 1 && r.seat.key === 'triage#1' && readMailboxLine(MSG('Notify: split · LANE#N · takes: none · leaves: none')).state === 'unreadable'; })());
2614+
t('⭐ a key\'s `LANE#N` field is read with `seatKey` — ONE seat grammar (#27891): a well-formed `domain:X#N` reads as before, carries its canonical `key`, and is read lower-cased', (() => { const r = readMailboxLine(DB.on); const u = readMailboxLine(MSG('Notify: seat-online · Domain:Skills#2 · os-bill · mailbox #27120 · takes: none')); return r.state === 'read' && r.seat.key === 'domain:skills#2' && r.seat.lane === 'domain:skills' && r.seat.n === 2 && u.state === 'read' && u.seat.key === 'domain:skills#2'; })());
2615+
t('⛔ the short form `skills#2` is refused in a key field, the same as on `To:` — the key does not spell its template', ['Notify: seat-online · skills#2 · os-bill · mailbox #27120 · takes: none', 'Notify: seat-offline · skills#2 · in-flight: none · stay-behind: none · brief 6450022222', 'Notify: split · skills#2 · takes: none · leaves: none', 'Notify: split · server · takes: none · leaves: none'].every((l) => { const r = readMailboxLine(MSG(l)); return r.state === 'unreadable' && /does not spell/.test(r.reason); }) && readMailboxLine('To: skills#2\nRequest: urgent-triage · #27047 · x').state === 'refused');
2616+
t('⛔ a `domain:` lane named after a reserved lane word is refused in a key field and on `To:` alike — its slug would be another seat\'s', ['domain:triage', 'domain:director#2', 'domain:repo-cloud#1'].every((seat) => readMailboxLine(MSG(`Notify: split · ${seat} · takes: none · leaves: none`)).state === 'unreadable' && readMailboxLine(`To: ${seat}\nRequest: urgent-triage · #27047 · x`).state === 'refused') && readMailboxLine(MSG('Notify: split · domain:triage-tools#1 · takes: none · leaves: none')).seat.key === 'domain:triage-tools#1');
26072617
t('⭐ `To: <seat>` 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'; })());
26082618
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: <seat>`/.test(r.reason) && typeof r.line === 'string'; }));
26092619
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); }));

0 commit comments

Comments
 (0)