Repository navigation
Commit 172eebb
feat(pm): the relay deletes a closed mailbox's claude/mailbox-* head, and mailbox-open --close sends it in the close stroke (#30782)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DWNMNPNycXM3h5dzFmhVC6n)_
Fixes #30350
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:中继(`objectstack-fleet[bot]`)多了一个只删不建的操作
`mailbox_branch_delete`,只删「已关闭、带 `pm:mailbox` 的 PR 自己的
`claude/mailbox-*` head 分支」;`mailbox-open --close` 关信箱时在同一笔里顺带删掉它。
- **为什么改**:落实 #30350 的裁决
A(「按推荐裁」):云端席位开信箱必须自己推一个分支,关信箱后这个分支删不掉,每坐一次云席就在仓库里留一个永不清理的 `claude/`
分支,预检还会把它读成认领。
- **风险与回滚**:中继 token 本来每笔都带 `contents: write`(给 auto-merge 用),所以
workflow 和权限都没改;路径拒绝表只对 `refs/heads/claude/mailbox-*`
这一种路径开口,建引用、改引用、写文件仍一律拒;删前在 runner 上现读 PR 和分支,未关闭、无标签、head 不符、fork、还有开着的
PR 用同一分支、或分支已不在 PR 关闭时的 head 提交上,任一条就不发 DELETE;`--close`
那一笔里删除被拒时,失败行打印可单独重发的那一条动作。回滚即 revert;中继只跑 main 上的代码,合并前不生效。
- **先看**:① `scripts/pm/fleet-write/ops.mjs` 的新行 `mailbox_branch_delete`
与 `REFUSED_PATH_FAMILIES` 里 `/git/` 那一条(唯一放行的路径形状);② `execute.mjs` 的
`mailboxBranchFenceFailures` / `mailboxBranchPreflight` /
`confirmMailboxBranch`;③ `mailbox-open.mjs` 的 `planClose` 与
`closeRemedy`。
- **两分钟验证**:`node scripts/pm/fleet-write/validate.mjs --self-test &&
node scripts/pm/fleet-write/execute.mjs --self-test && node
scripts/pm/mailbox-open.mjs --self-test`,预期三条全绿(337 / 276 / 87 例),其中
⭐「after close, no branch」那一条钉住关信箱后无分支。
- **需要你做的**:无,席位落地。
## What changes
The relay gains its one ref op, delete-only, by ruling 6674408912
(letter A). Nothing else in the op table moves.
- `scripts/pm/fleet-write/ops.mjs`
- New row `mailbox_branch_delete`: keys `pull` + `branch`, nothing else.
ONE `DELETE /repos/{repo}/git/refs/heads/{branch}`, no body. Permission
`contents`, no `alsoSpends`, not `alone`, so it can ride the close
stroke.
- `MAILBOX_BRANCH_SHAPE` (`claude/mailbox-` and a lower-case slug, no
slash) and `MAILBOX_LABEL` (`pm:mailbox`, now spelled once;
`mailbox-open.mjs` re-exports it).
- New field `branch`: a `str` held to that shape before the dispatch. A
refusal names the shape and never echoes the value.
- `REFUSED_PATH_FAMILIES`: the `/git/` entry becomes "every `/git/` path
but ONE `refs/heads/` ref of `MAILBOX_BRANCH_SHAPE`", built from the
shape's own source. `/contents/`, `/branches/` and every other family
are byte-identical.
- Header: a new section "The one row that deletes a ref", the ONE place
the fences and the landing are listed (`execute.mjs` and
`mailbox-open.mjs` point at it), and the three sentences that said no
row reaches a ref now name this one DELETE.
- `scripts/pm/fleet-write/validate.mjs`: `validateField` honours `shape`
on a `str` (no echo). Self-test pins below.
- `scripts/pm/fleet-write/execute.mjs`
- `mailboxBranchPreflight` reads the pull's own `GET /pulls/{n}`, `GET
/pulls?state=open&head={owner}:{branch}` and the ref `GET
/git/ref/heads/{branch}`, the last three reads before the DELETE.
`mailboxBranchFenceFailures` (pure) refuses, the DELETE not sent,
unless: the pull is closed; it carries `pm:mailbox`; its head ref is
`branch`; its head repository is this repository; no OPEN pull has that
head; and the ref's `object.sha` is the pull's `head.sha` (a commit
pushed after the close is not the mailbox's). A ref already absent (404)
fences nothing; an unreadable ref is refused.
- `confirmMailboxBranch`: the 204 is not the landing. The ref is read
back (`GET /git/ref/heads/{branch}`) and lands only when absent; a ref
already gone (the DELETE's 422) is an idempotent landing; a ref still
there is a FAILED action.
- Wiring: the preflight shares the re-run's `pre` slot and its not-sent
block; the confirm joins the landed chain. No other row's path changes.
- `scripts/pm/mailbox-open.mjs`
- `planClose({ repo, pull, head })`: when the head read is
`claude/mailbox-*`, `mailbox_branch_delete` of it is appended to the
close stroke, last, after `issue_patch state: closed`. Any other head (a
thread's own `claude/project-thread-*`, a local seat's
`mailbox/LOGIN/SLUG`) is left and the success line says so.
- `closeLive` re-plans with the head it already reads; the dry run,
which reads nothing, prints the same two actions as before plus a line
saying the delete rides when the head is `claude/mailbox-*`.
- `closeRemedy(plan)` (pure): when a close stroke that carried the
delete fails, `closeLive` prints it — the pull is closed by then, so
`--close` refuses a re-run, and the line gives the one bare
`mailbox_branch_delete` action to send through
`fleet-write/dispatch.mjs`.
- The cloud refusal of the open path now prescribes `--head
claude/mailbox-SLUG`, the one name `--close` deletes.
## Why
Every cloud seat that opens a mailbox has to push its own `--head`
branch, and `--close` closed the pull and deleted nothing, so each cloud
seating left a branch that the seat cannot remove (the card body
measured four cut-off `git push --delete` attempts) and that pre-flight
branch checks read as a claim.
**The first stroke's measurement (the ruling's confidence gap).** The
App token does not need any new grant, and the carve-out is one path
shape:
- GitHub's server-to-server permission ledger (`github/docs`,
`src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json`,
read 2026-11-06): `delete /repos/{owner}/{repo}/git/refs/{ref}` is
`contents: write`, `additional-permissions: false`. The create (`post
…/git/refs`) and the move (`patch …/git/refs/{ref}`) are the rows that
also need `workflows`; neither is spelled by any row.
- `.github/workflows/fleet-write.yml` already mints
`permission-contents: write` as a literal grant on every stroke (for the
two auto-merge mutations), so the workflow is unchanged and
`PERMISSIONS` is unchanged.
- The barrier was the table, not the token: `REFUSED_PATH_FAMILIES` is
what keeps a row off `/git/**`. It now lets exactly one shape through,
and the self-test pins that this row's DELETE is the only request any
row sends under `/git/`.
- The two leftover targets carry no ruleset and no branch protection
(`GET /rules/branches/…` answers `[]`, `GET /branches/…` answers
`protected: false`, both read 2026-11-06).
## Tests
- `validate.mjs --self-test`: 337 cases, 17 batteries (floor 16 → 17,
one battery for the row). The new battery: the shape and the label; a
dotted seat slug accepted; twelve names outside the shape refused with
the exact sentence and no echo (another thread's branch, a local seat's
`mailbox/` branch, an empty slug, upper case, a slash, a dot pair, a
trailing slash or dot, a space, `main`, a qualified ref, a path escape);
closed keys; the one bodiless DELETE; the close stroke accepted with the
five literal grants and nothing stroke-scoped. In "refused by
construction" (22 → 24): the ONE request under `/git/` is this row's
DELETE, and a positive control that a ref create, another ref, a nested
ref, a tag, a tree, a blob, a commit, the singular `ref` read path and
`/contents/` are all still caught while the one mailbox ref is caught by
none. The two `contents` pins now name the third spender.
- `execute.mjs --self-test`: 276 cases, 23 batteries (floor 22 → 23).
The new battery: the good case's call order (pull, open pulls, the ref
at the pull's head, DELETE under the App token, ref read-back) and row;
the open-pulls query filtered to `objectstack-ai:claude/mailbox-ui-3`;
each of nine fences refusing with the DELETE not sent and naming itself
(an open pull, no `pm:mailbox`, another head, a forked head, an open
pull on the head, a ref moved off the pull's head, an unread ref, an
unread pull, an unread open-pulls list); a 204 whose ref still reads
back is FAILED; a ref absent before and after, the DELETE answering 422,
is idempotent; ⭐ **after close, no branch** — `mailbox-open`'s own
`planClose` stroke run through the executor sends the comment, the
close, then the DELETE, reads the pull only after closing it, and ends
on the ref read back absent; and a head outside `claude/mailbox-*` is
closed with nothing sent under `/git/`.
- `mailbox-open.mjs --self-test`: 87 cases (the close-plan battery 3 →
7): the delete rides last for a `claude/mailbox-*` head and the
validator accepts the stroke; a thread branch, a `mailbox/` branch, two
look-alikes and no head read send no delete; a failed close stroke that
carried the delete names the bare re-send through `dispatch.mjs`, whose
one action the validator accepts, and a stroke with no delete names
none.
## Acceptance notes
- **Fences beyond the ruling's three, each a narrowing.** The ruling
names closed + `pm:mailbox` + `claude/mailbox-*`. The executor also
requires the head to be this pull's head on THIS repository (the card's
"a ref that is not that PR's head", and a fork's same-named branch is
not ours), refuses when any OPEN pull has that head, because deleting a
branch closes every open pull on it (the dispatch's "never one whose
mailbox PR is still open"), and refuses unless the ref still points at
the closed pull's `head.sha`, so a commit pushed to the name after the
close is never deleted (contract review should-fix 1, comment
6675668758).
- **A refused delete inside `--close` (should-fix 3).** If the relay's
pull read lags the stroke's own close, the fence refuses and the pull is
already closed, which `--close` then refuses; `closeRemedy` prints the
one bare action to re-send instead of leaving the branch behind.
- **Post-landing acceptance probe (the seat, after merge — the relay
runs `main`'s code, so it cannot be probed before).** Two of the three
leftovers qualify, read 2026-11-06: `claude/mailbox-ui-3` is the head of
mailbox #30141 and `claude/mailbox-ui-2` of mailbox #30142, both pulls
closed and labelled `pm:mailbox`. The third,
`mailbox/os-zhuang/triage-1`, is the head of OPEN mailbox #28646 and
outside the shape: ⛔ never touched. The actions file is one stroke, both
deletions:
`[{"op":"mailbox_branch_delete","pull":30141,"branch":"claude/mailbox-ui-3"},{"op":"mailbox_branch_delete","pull":30142,"branch":"claude/mailbox-ui-2"}]`
sent with `node scripts/pm/fleet-write/dispatch.mjs --repo
objectstack-ai/objectstack --actions-file FILE --json`, then re-checked
with `git ls-remote origin 'refs/heads/claude/mailbox-*'` (expected:
empty).
- **Stale prose outside the surface.**
`.github/workflows/fleet-write.yml` L134–135 still says the op table
"has NO contents op — no file, ref or branch write — so this grant is
spent only by those two mutations". That is false once this lands. The
file is Tier H and outside this card's surface (⛔
`.github/workflows/**`), so it is not edited here; reported to the seat
to card.
- **Line delta (`scripts/pm/**` net-negative ask).** +230 / −44 over
four files: code and header prose +141 / −29, self-tests +89 / −15.
`ops.mjs` 2367 → 2407, `validate.mjs` 2036 → 2061, `execute.mjs` 2994 →
3089, `mailbox-open.mjs` 689 → 715. Collapsing the fence prose to one
place (should-fix 2) paid back the executor's header section.
- **Overlap watch.** `execute.mjs`'s `requestLanded` (near L593, card
#30114's site) is untouched; the new functions sit beside the hierarchy
link's, and the loop edit is two lines.
- **Lane.** `scripts/pm/**` only, not governed, publishes nothing:
`skip-changeset`.
## Verification
<details><summary>Derived gate families (40 of 40) and pnpm lint, head
83464d0</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on the tree of head `83464d0b` (the review
fixes on `1465a855`; run on `594a4e4c`, whose tree is byte-identical,
the commit message alone reworded), each run with its exit code captured
before any pipe:
```
pnpm check:pm-dispatch-gates :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test :: exit 0
node scripts/check-self-test-wired.mjs :: exit 0
node scripts/check-self-test-wired.mjs --self-test :: exit 0
node scripts/check-self-test-workflow-commands.mjs :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0
node scripts/check-whole-set-label-write.mjs :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:bash32-floor :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cli-command-ids :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:entry-guard :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:parse-guard :: exit 0
pnpm check:pm-close-cards :: exit 0
pnpm check:pm-fleet-write-dispatch :: exit 0
pnpm check:pm-fleet-write-execute :: exit 0
pnpm check:pm-fleet-write-validate :: exit 0
pnpm check:pm-issue-create :: exit 0
pnpm check:pm-issue-transfer :: exit 0
pnpm check:pm-label-write :: exit 0
pnpm check:pm-post-stamped :: exit 0
pnpm check:pm-record-recognisers :: exit 0
pnpm check:pm-write-pace :: exit 0
pnpm check:pnpm-filter-targets :: exit 0
pnpm check:pnpm-flag-spellings :: exit 0
pnpm check:ratchet-remedy-authority :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:scripts-symbol-anchors :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm lint :: exit 0
```
`dispatch-gates --ran`: 40 derived, 40 run, 0 NOT-MEASURED, 0 UNRUN;
`pnpm lint` recorded outside the derivation. The same 40 ran green on
`32b7ca47` and on `1465a855` before the review fixes, and the derived
list is identical on all three heads.
</details>
<details><summary>The relay self-tests, head 83464d0</summary>
```
node scripts/pm/fleet-write/validate.mjs --self-test :: exit 0 337 cases, 17 batteries
node scripts/pm/fleet-write/execute.mjs --self-test :: exit 0 276 cases, 23 batteries
node scripts/pm/mailbox-open.mjs --self-test :: exit 0 87 cases, 10 batteries
node scripts/pm/fleet-write/dispatch.mjs --self-test :: exit 0 318 cases, 27 batteries (unchanged file; it derives its op lists from the table)
```
</details>
No test file outside the scripts names any of the four (`git grep` over
`*.test.*` / `*.spec.*`: 0 hits), so the self-tests are their whole
suites. No ablation: the diff is scripts only.
<!-- signed: domain:skills#1 · os-elon-musk ·
session_016WV4cwSv11c8FvyR8ZuWhv -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a255a62 commit 172eebb
4 files changed
Lines changed: 230 additions & 44 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
190 | 190 | | |
191 | 191 | | |
192 | 192 | | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
193 | 198 | | |
194 | 199 | | |
195 | 200 | | |
| |||
467 | 472 | | |
468 | 473 | | |
469 | 474 | | |
| 475 | + | |
470 | 476 | | |
471 | 477 | | |
472 | 478 | | |
| |||
893 | 899 | | |
894 | 900 | | |
895 | 901 | | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
896 | 942 | | |
897 | 943 | | |
898 | 944 | | |
| |||
1424 | 1470 | | |
1425 | 1471 | | |
1426 | 1472 | | |
1427 | | - | |
1428 | | - | |
| 1473 | + | |
| 1474 | + | |
1429 | 1475 | | |
1430 | 1476 | | |
1431 | 1477 | | |
| |||
1510 | 1556 | | |
1511 | 1557 | | |
1512 | 1558 | | |
| 1559 | + | |
1513 | 1560 | | |
1514 | 1561 | | |
1515 | 1562 | | |
1516 | 1563 | | |
1517 | | - | |
1518 | | - | |
1519 | | - | |
1520 | | - | |
1521 | | - | |
| 1564 | + | |
| 1565 | + | |
| 1566 | + | |
| 1567 | + | |
| 1568 | + | |
| 1569 | + | |
| 1570 | + | |
1522 | 1571 | | |
1523 | 1572 | | |
1524 | 1573 | | |
| |||
1593 | 1642 | | |
1594 | 1643 | | |
1595 | 1644 | | |
| 1645 | + | |
1596 | 1646 | | |
1597 | 1647 | | |
1598 | 1648 | | |
1599 | | - | |
1600 | | - | |
| 1649 | + | |
| 1650 | + | |
| 1651 | + | |
1601 | 1652 | | |
1602 | 1653 | | |
1603 | 1654 | | |
| |||
2457 | 2508 | | |
2458 | 2509 | | |
2459 | 2510 | | |
| 2511 | + | |
| 2512 | + | |
| 2513 | + | |
| 2514 | + | |
| 2515 | + | |
| 2516 | + | |
| 2517 | + | |
| 2518 | + | |
| 2519 | + | |
| 2520 | + | |
| 2521 | + | |
| 2522 | + | |
| 2523 | + | |
| 2524 | + | |
| 2525 | + | |
| 2526 | + | |
| 2527 | + | |
| 2528 | + | |
| 2529 | + | |
| 2530 | + | |
| 2531 | + | |
| 2532 | + | |
| 2533 | + | |
| 2534 | + | |
| 2535 | + | |
| 2536 | + | |
| 2537 | + | |
| 2538 | + | |
| 2539 | + | |
| 2540 | + | |
| 2541 | + | |
| 2542 | + | |
| 2543 | + | |
| 2544 | + | |
| 2545 | + | |
| 2546 | + | |
| 2547 | + | |
| 2548 | + | |
| 2549 | + | |
| 2550 | + | |
| 2551 | + | |
| 2552 | + | |
| 2553 | + | |
| 2554 | + | |
2460 | 2555 | | |
2461 | 2556 | | |
2462 | 2557 | | |
| |||
0 commit comments