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 100303c
Browse filesBrowse the repository at this point in the historyBrowse files
pm-dispatch: an open PR names its owner — pr_create assigns the claim's Account, a hand-over re-assigns it, and H76 reports an unowned PR (#30796)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DMkvesLnTm6vYxaHQzFtixa)_
domain:skills#1 · os-elon-musk · session_016WV4d1MNyKiUFDBccFX8Cc
Fixes#29828
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:按裁决 A 落地「PR 挂处理人」。开发经中继 `pr_create` 开 PR 时就把处理人挂成认领评论里的
`Account:`(即卡的处理人);换手(兄弟席接管、释放、改派)与认领变更同笔改 PR 处理人;交棒请审时仍 ⛔
不请席位自己的账号。半状态巡查新增 H76 行:开着的、非信箱 PR 没有处理人就列出来(只报告,不是门禁)。
- **为什么改**:原规则禁止 PR 挂处理人,实测今天开着的 52 个 PR 里 0 个有处理人,GitHub 列表上看不出谁负责,排查闲置
PR 只能逐个读评论。
- **风险与回滚**:只动 `.claude/**` 规则文本(Tier S)与 `scripts/pm/`
巡查工具,不发布(`skip-changeset`)。中继本来就支持 `pr_create` 带 `assignees`,不改代码。H76
不写任何标签、处理人或评论,不挡合并。各规则文件行数不变、每行仍在 120 字节内,未抬任何上限。回滚即 revert。
- **先看**:① `.claude/skills/pm-dispatch/references/execution-duties.md`
认领节第 69 行(规则本体与换手同笔);② `scripts/pm/check-half-states.mjs` 的
`h76OpenPrWithoutAssignee` 与 sweep 里的接线;③
`.claude/skills/pm-dispatch/SKILL.md` 第 198–199 行(交棒请审只留「不请席位自身账号」)。
- **两分钟验证**:`node scripts/pm/check-half-states.mjs
--self-test`,末行电池清单里应见 `H76 unowned open PR 12/11`,并以 0 退出。
- **需要你做的**:无,席位落地(Tier S,席位达档复核 PASS 且全绿后落地;存量 PR
的一次性补挂由席位在规则落地后经中继执行)。
## What changes
Ruling record 6674390671 (letter A, maintainer 「按推荐裁」) on #29828,
encoded in the three texts it names plus every restatement found under
`.claude/**`:
- `.claude/agents/os-dev.md` rule 2 (L38): the PR's assignee is the
claim's `Account:`, carried by `pr_create`; a hand-over is the seat's.
Was 「PR ⛔ 不带 `assignees`(中继虽收),席位交棒才派」.
- `.claude/agents/os-dev.md` write budget (L62): `pr_create` carries
labels and assignees. Was 「⛔ 无 assignees」.
- `.claude/skills/pm-dispatch/references/execution-duties.md` 认领 (L69;
L67 at `1371b9d8`): the PR is assigned to the claim's account and a
hand-over re-assigns it in the same stroke. Was 「PR ⛔ 不派席位账号」.
- `.claude/skills/pm-dispatch/SKILL.md` hand-off step ③ (L198-199): 「⛔
永不派/请席位自身账号」 keeps only the request half (the ruling replaces the assign
half alone; a review request to the seat's own account stays forbidden);
「`pr_create` 不请不派」 becomes 「`pr_create` 派不请」.
- `.claude/skills/pm-dispatch/references/dispatch-runbook.md` (L140): a
card thread does not write the CARD's assignee — the PR's is now written
by its `pr_create`.
- `scripts/pm/check-half-states.mjs`: row **H76**, report-only, band
`state` (H1's): an open PR with no assignee, a `pm:mailbox` PR excepted.
The mailbox label is `mailbox-open.mjs`'s `MAILBOX_LABEL`, imported
lazily as H72 does; when it cannot be loaded every unowned PR fires
UNJUDGED, never clean. Drafts are included. Free: it reads the open
listing the sweep already holds. ⛔ No gate wiring.
Every file keeps its line count (each edit rewrites its own line in
place) and every touched line stays within the 120-byte line budget; no
ceiling moved.
## Tests
- `node scripts/pm/check-half-states.mjs --self-test`: new battery `H76
unowned open PR` (12 cases, pin 11): the firing, a ready PR firing like
a draft, the assignee exemption (list field and legacy single field),
the mailbox exemption beside its unlabelled twin, closed and missing
rows, the unread-label UNJUDGED leg for null / undefined / empty, an
assigned PR staying clean with the label unread, not-loud, band +
emitted, the lazy-import wiring, the roster floor.
- Live probe (read-only, offline predicate over a REST listing,
2026-11-06 ~03:05Z): 52 open PRs, 0 assigned, 13 unassigned `pm:mailbox`
PRs exempt, 39 would fire H76.
## Acceptance notes
- The relay needs no change: `scripts/pm/fleet-write/validate.mjs`
accepts `assignees` on `pr_create` (its own self-test pins the shape)
and `ops.mjs` sends it as the second leg. Two relay COMMENTS still
restate the old rule — the `ops.mjs` header (L103-105, "a dev's
`pr_create` carries no `assignees` at all") and the `pr_create` row
comment (L2093-2094). They are outside this card's surface while PR
#30782 edits `ops.mjs`; carrier: the seat's relay-docs follow-up card.
- `AGENTS.md` L403-404 restates the old rule ("nor the PR's: `pr_create`
carries none; the seat sets it at hand-over"). `AGENTS.md` is Tier H and
outside this card's surface; carrier: the seat files it.
- The relay's governed-review plan (`execute.mjs`, `pr_ready`) and H43
count an ASSIGNED approver as covered. A claim's `Account:` is never a
`GOVERNED_APPROVERS` account by rule (SKILL.md: an approval account
never sits a seat), so the new assignee cannot mask a review request;
noted because one open card (#30292) carries an approver account as
assignee — its PR is not on a governed path today.
- H76 reads no "unassigned with a note" exemption: a bot PR the seat
leaves unassigned keeps reporting each sweep (report-only). Assigning
the owning lane seat's account, the ruling's first arm, silences it.
- The one-time backfill is NOT run here: the per-PR list (PR → card →
assignee to set) is in the report comment on #29828, for the seat to run
through the relay after this lands.
- Numbered H76 because H75 is PR #30778's.
## Verification
Local subset, recorded as `COMMAND :: exit CODE` and reconciled with
`node scripts/pm/dispatch-gates.mjs --ran FILE --repo
objectstack-ai/objectstack`:
- At `7c76c755` (this change before the `origin/main` merge): all **58**
derived families exit 0, reconciliation 58 derived / 58 run / 0
NOT-MEASURED; `pnpm lint` exit 0. That run includes
`check:pm-half-states`, `check:pm-skill-ratchet`,
`check:pm-skill-id-lint`, `check:pm-governed-prose`,
`check:skill-frame-sync`, `check:doc-authoring` and
`check:pm-dispatch-gates`.
- The head carries one more commit, a plain merge of `origin/main`
(bringing in PR #30778, which also edits `os-dev.md`, `SKILL.md` and
`check-half-states.mjs`); the branch's delta against `main` is
byte-identical (5 files, +92 / -6). The same 58 families are re-run on
that head; their record is in the os-dev report on #29828.
- CI families (Lint & Repo Gates, Test Core and the rest) are read from
this PR's check-runs, not re-run locally.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No schema 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.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016WV4d1MNyKiUFDBccFX8Cc)_
Co-authored-by: Claude <noreply@anthropic.com>
// through this predicate — lives in record-recognisers.mjs's doorbell battery,
25310
25373
// because this self-test cannot import that module (H62's banner).
25311
25374
'H72 mailbox off-table bells': 20,
25375
+
// Registered with the unowned-PR row, pin just under the count: the firing, the assignee/mailbox/closed
25376
+
// exemptions, the unread-label UNJUDGED leg, the text, the band and the wiring.
25377
+
'H76 unowned open PR': 11,
25312
25378
// Registered with the ruled-card-in-the-box row (#29386), pin just under the
25313
25379
// count: the firing case, the label / fence / anchoring / id controls each
25314
25380
// beside its positive twin, the threshold boundary, the governing stamp, the
@@ -28656,6 +28722,26 @@ async function selfTest() {
28656
28722
b(BATTERY28675, '⭐ H72: the sweep takes the ONE reader and the opener\'s label LAZILY — `mailboxEvents` from record-recognisers.mjs, MAILBOX_LABEL from mailbox-open.mjs — and spells no doorbell key of its own', (() => { const src = readFileSync(fileURLToPath(import.meta.url), 'utf8'); return src.includes("import('./record-recognisers.mjs')") && src.includes("import('./mailbox-open.mjs')") && !/^import [^\n]*from '\.\/(?:record-recognisers|mailbox-open)\.mjs'/mu.test(src) && !src.includes(['MAILBOX_KEYS', '='].join(' ')); })(), true);
28657
28723
b(BATTERY28675, 'floor: this battery is DECLARED on the roster, with a positive pin', Object.prototype.hasOwnProperty.call(SELF_TEST_BATTERIES, BATTERY28675) && SELF_TEST_BATTERIES[BATTERY28675] > 0, true);
28658
28724
28725
+
// -- H76: an open PR with no assignee (ruling record 6674390671, letter A). The fixture is a relay-opened dev
28726
+
// draft as the card measured them (26 of 27 open PRs unassigned), offline, never a live read.
b(BATTERY76, 'H76 fires: an open dev draft with no assignee and no mailbox label', row76.length > 0 && !isUnjudgedFinding(row76), true);
28732
+
b(BATTERY76, 'H76 row: names the ruling, the claim\'s `Account:`, the relay `assign` op, the bot-PR arm and the report-only boundary', ['ruling record 6674390671', 'the claim\'s `Account:`', '`assign` op', 'owning lane seat\'s account', 'Report-only'].every((x) => row76.includes(x)), true);
28733
+
b(BATTERY76, 'H76: a ready (non-draft) PR fires exactly like a draft — drafts are included, not the only population', typeof h76OpenPrWithoutAssignee(pr76({ draft: false }), MAILBOX76), 'string');
28734
+
b(BATTERY76, '⛔ H76: a PR with an assignee is clean — the list field or the legacy single field', [h76OpenPrWithoutAssignee(pr76({ assignees: [{ login: 'os-elon-musk' }] }), MAILBOX76), h76OpenPrWithoutAssignee(pr76({ assignee: { login: 'os-elon-musk' } }), MAILBOX76)].join(), ',');
28735
+
b(BATTERY76, '⛔ H76: an unassigned `pm:mailbox` PR is exempt, and its unlabelled twin is not', [h76OpenPrWithoutAssignee(pr76({ labels: [{ name: MAILBOX76 }, { name: 'pinned' }] }), MAILBOX76), typeof h76OpenPrWithoutAssignee(pr76({ labels: [{ name: 'pinned' }] }), MAILBOX76)].join(), ',string');
28736
+
b(BATTERY76, '⛔ H76: a closed PR and a missing row are silent, never a crash', [h76OpenPrWithoutAssignee(pr76({ state: 'closed' }), MAILBOX76), h76OpenPrWithoutAssignee(undefined, MAILBOX76), h76OpenPrWithoutAssignee(null, MAILBOX76)].join(), ',,');
b(BATTERY76, 'H76: an UNREAD mailbox label fires UNJUDGED, never clean — null, undefined and empty alike', unread76.every((t) => t.startsWith(UNJUDGED_MARKER) && isUnjudgedFinding(t)), true);
28739
+
b(BATTERY76, '⛔ H76: …but an ASSIGNED PR stays clean with the label unread — the label is only the mailbox exemption', h76OpenPrWithoutAssignee(pr76({ assignees: [{ login: 'os-elon-musk' }] }), null), null);
28740
+
b(BATTERY76, 'H76: not a loud finding — it never escalates a sweep', [isLoudFinding(row76), isLoudFinding(unread76[0])].join(), 'false,false');
28741
+
b(BATTERY76, 'H76 band: registered as `state` (H1\'s), and the sweep really pushes it', `${familyBand('H76')}/${familyRegistryCoverage().emitted.includes('H76')}`, 'state/true');
28742
+
b(BATTERY76, 'H76 wiring: every listed open PR is judged with the label taken LAZILY from mailbox-open.mjs, never spelled in the sweep', (() => { const src = readFileSync(fileURLToPath(import.meta.url), 'utf8'); return src.includes("({ MAILBOX_LABEL: mailboxLabel } = await import('./mailbox-open.mjs'));") && src.includes('const unowned = h76OpenPrWithoutAssignee(pr, mailboxLabel);'); })(), true);
28743
+
b(BATTERY76, 'floor: this battery is DECLARED on the frozen roster with a positive pin', Object.hasOwn(SELF_TEST_BATTERIES, BATTERY76) && SELF_TEST_BATTERIES[BATTERY76] > 0 && Object.isFrozen(SELF_TEST_BATTERIES), true);
28744
+
28659
28745
// -- H73 (#29386): a RULED decision card still sitting in the decision box.
28660
28746
// Positive controls beside every negative one: the label, the fence, the
28661
28747
// anchoring, the id, the threshold, the governing stamp, the re-entry line.
0 commit comments