Skip to content

Commit 172eebb

Browse files
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

File tree

‎scripts/pm/fleet-write/execute.mjs‎

Lines changed: 104 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,11 @@
190190
* read the parent and ⛔ never re-send. Every read is an unpaced GET, and the
191191
* one write is paced like every write verb.
192192
*
193+
* ## The mailbox branch delete — `mailbox_branch_delete`
194+
*
195+
* Its fences and its landing are listed once, in `ops.mjs`'s header ("The one
196+
* row that deletes a ref"); they are AUTHORITATIVE here, each that fails named.
197+
*
193198
* ## The advisory rows that write — fenced on the ADVISORY, printing none of its text
194199
*
195200
* `ops.mjs`'s header section "The rows that write a security advisory"
@@ -467,6 +472,7 @@ import {
467472
GOVERNED_REVIEW_FILES_PAGE_SIZE,
468473
GOVERNED_REVIEW_OPS,
469474
LOGIN_SHAPE,
475+
MAILBOX_LABEL,
470476
OPS,
471477
PERMISSIONS,
472478
PR_LANDED_STATE,
@@ -893,6 +899,46 @@ export async function confirmSubIssue(req, answer, pre, payload, api, t, { sleep
893899
return { ok: false, why: `${platform} accepted, but ${last} ${windowMs} ms later — UNCONFIRMED: read #${parent}'s sub-issues before anything else, ⛔ do not re-send` };
894900
}
895901

902+
/**
903+
* The mailbox branch delete's fences on the reads (header): `[]` to send, else one sentence per fence that fails —
904+
* an unread pull is one, judged alone. `pull`, `open` and `ref` are `rest` answers; a 404 ref fences nothing. Pure.
905+
*/
906+
export function mailboxBranchFenceFailures({ repo, branch, pull, open, ref }) {
907+
if (pull?.status !== 200 || !pull.json || typeof pull.json !== 'object') return [`${pull?.call} -> HTTP ${pull?.status}: the pull could not be read`];
908+
const pr = pull.json;
909+
const out = [];
910+
if (pr.state !== 'closed') out.push(`the pull is ${pr.state}, not closed`);
911+
if (!labelNames(pr.labels).includes(MAILBOX_LABEL)) out.push(`the pull does not carry ${MAILBOX_LABEL}`);
912+
if (pr.head?.ref !== branch) out.push(`the pull's head is ${pr.head?.ref ?? 'unread'}, not ${branch}`);
913+
if (String(pr.head?.repo?.full_name ?? '').toLowerCase() !== repo.toLowerCase()) out.push(`the pull's head lives on ${pr.head?.repo?.full_name ?? 'no repository'}, not ${repo}`);
914+
if (open?.status !== 200 || !Array.isArray(open.json)) out.push(`${open?.call} -> HTTP ${open?.status}: the open pulls on ${branch} could not be read`);
915+
else if (open.json.length) out.push(`open pull(s) ${open.json.map((p) => `#${p?.number}`).join(', ')} have ${branch} as head — the DELETE would close them`);
916+
const at = ref?.status === 200 && ref.json?.ref === `refs/heads/${branch}` ? ref.json.object?.sha : undefined;
917+
if (ref?.status !== 404 && typeof at !== 'string') out.push(`${ref?.call} -> HTTP ${ref?.status}: the ref ${branch} could not be read`);
918+
else if (ref?.status !== 404 && at !== pr.head?.sha) out.push(`${branch} is at ${at.slice(0, 12)}, not the pull's head ${String(pr.head?.sha ?? 'unread').slice(0, 12)} — a commit pushed after the close is not the mailbox's`);
919+
return out;
920+
}
921+
922+
/** The fences, AUTHORITATIVE (header): the three reads, then `{}` to send or `{ fail }`, the DELETE NOT SENT. Never throws. */
923+
export async function mailboxBranchPreflight(req, payload, api, t) {
924+
const { pull, branch } = req.mailboxBranch;
925+
const repo = payload.repo;
926+
const pr = await rest(api, `/repos/${repo}/pulls/${pull}`, {}, t);
927+
const open = await rest(api, `/repos/${repo}/pulls?state=open&head=${encodeURIComponent(`${repo.split('/')[0]}:${branch}`)}`, {}, t);
928+
const ref = await rest(api, `/repos/${repo}/git/ref/heads/${branch}`, {}, t);
929+
const failures = mailboxBranchFenceFailures({ repo, branch, pull: pr, open, ref });
930+
return failures.length ? { fail: `REFUSED at the relay's fence on #${pull}'s head ${branch}, nothing sent — ${failures.join('; ')}` } : {};
931+
}
932+
933+
/** Did the delete LAND? The ref read back absent (header); a ref still there, or unread, is FAILED. Never throws. */
934+
export async function confirmMailboxBranch(req, answer, payload, api, t) {
935+
const { branch } = req.mailboxBranch;
936+
const back = await rest(api, `/repos/${payload.repo}/git/ref/heads/${branch}`, {}, t);
937+
const platform = `HTTP ${answer.status}${answer.detail ? ` — ${answer.detail}` : ''}`;
938+
if (back.status !== 404) return { ok: false, why: `${platform}, but ${back.call} answered HTTP ${back.status}: the branch ${back.status === 200 ? 'is still there' : 'could not be read back'} — read it before anything else` };
939+
return { ok: true, why: '', idempotent: !(answer.status >= 200 && answer.status < 300), result: `deleted ${branch} (read back absent)` };
940+
}
941+
896942
/**
897943
* The advisory's fences, AUTHORITATIVE (header: "The advisory rows that write"): its own GET, judged against
898944
* `ADVISORY_FENCES` — for `advisory_collaborators_add` an open advisory (`ADVISORY_OPEN_STATES`), for
@@ -1424,8 +1470,8 @@ export async function executeFleetWrite({ payload: raw, sender, token, api = DEF
14241470
break;
14251471
}
14261472
const req = filled.req;
1427-
// The failed-jobs re-run (header): its fences on the run and its pull, judged HERE on the reads before the POST — refused, NOT SENT.
1428-
const pre = req.rerunFailed ? await rerunFailedPreflight(req, payload, api, t) : null;
1473+
// The failed-jobs re-run and the mailbox branch delete (header): their fences, judged HERE on the reads before the write — refused, NOT SENT.
1474+
const pre = req.rerunFailed ? await rerunFailedPreflight(req, payload, api, t) : req.mailboxBranch ? await mailboxBranchPreflight(req, payload, api, t) : null;
14291475
if (pre?.fail) {
14301476
const why = scrub(pre.fail, [token]);
14311477
rows.push({ action: i + 1, op: action.op, call: `${req.verb} ${req.path}`, status: 'not sent', result: `FAILED: ${why}` });
@@ -1510,15 +1556,18 @@ export async function executeFleetWrite({ payload: raw, sender, token, api = DEF
15101556
// The failed-jobs re-run is confirmed by reading the run back (header): attempt 2, its failed jobs queued or in progress.
15111557
// The hierarchy link is confirmed by reading the parent's list back (header): the child's id listed, or no longer.
15121558
// The advisory rows are confirmed by their own answer (header): an id, every login sent, the state asked for — never by resultOf.
1559+
// The mailbox branch delete is confirmed by reading the ref back (header): absent, or it is FAILED.
15131560
const landed = req.updateBranch
15141561
? await confirmUpdateBranch(req, r, payload, api, t, ub)
15151562
: req.rerunFailed
15161563
? await confirmRerunFailed(req, r, pre, payload, api, t, rr)
1517-
: sub
1518-
? await confirmSubIssue(req, r, sub, payload, api, t, si)
1519-
: adv || req.advisoryReport || req.advisoryCreate
1520-
? confirmAdvisory(req, r, adv)
1521-
: requestLanded(req, r);
1564+
: req.mailboxBranch
1565+
? await confirmMailboxBranch(req, r, payload, api, t)
1566+
: sub
1567+
? await confirmSubIssue(req, r, sub, payload, api, t, si)
1568+
: adv || req.advisoryReport || req.advisoryCreate
1569+
? confirmAdvisory(req, r, adv)
1570+
: requestLanded(req, r);
15221571
const call = req.graphql ? `POST /graphql ${req.graphql.mutation}` : r.call;
15231572
rows.push({ action: i + 1, op: action.op, call, status: r.status, result: landed.ok ? (landed.idempotent ? 'already absent (idempotent)' : scrub(landed.result ?? resultOf(req, r.json, r.status), [token])) : `FAILED: ${scrub(landed.why, [token])}` });
15241573
if (!landed.ok) {
@@ -1593,11 +1642,13 @@ const SELF_TEST_BATTERIES = Object.freeze({
15931642
"the run_rerun_failed: the five fences judged HERE on the run and its pull, read immediately before ONE bodiless POST — the pull's current head by its own GET, never the run's embedded copy, and a run naming no pull refused by name; each refused with the POST not sent and accepted on the measured run; a run, pull, baseline or fence that fails is zero writes; the 201 confirmed only by reading attempt 2 back with its failed jobs queued or in progress, naming their job ids; attempt 2 never seen, a failed job completed, or a job list unread is FAILED; no annotation": 21,
15941643
"the hierarchy link: sub_issue_add and sub_issue_remove read the parent's and the child's own answers, then the child's parent (add) or the parent's whole list (remove), immediately before ONE write whose body is the child's REST id from its own answer — never its number, never replace_parent; a pull, a transferred or unread card, a child under another parent, one the parent does not list, or a read that is neither answer is zero writes; a child already under this parent is a no-op landed on the list; the answer confirmed only when the parent's list reads the child back (or no longer does) inside the window; a refused write, a list that never settles or never reads whole is FAILED; no annotation, and the rows ride a stroke beside other ops": 20,
15951644
"the advisory rows: advisory_report and advisory_create are ONE POST of their body each, landed on a 2xx carrying an id — a draft's reading state draft — that the row and a notice annotation name; advisory_collaborators_add, advisory_versions and advisory_state read the advisory first and send ONE PATCH — every existing login kept with the missing ones added, an all-present list a no-op, the versions entries alone, or the one state a legal transition reaches, the reopen and the publish among them — or nothing at all on a fence: an unread, other or wrong-state advisory, an illegal move, a list the versions may not replace or a publish whose versions are not set; each lands only when its answer shows the result, a publish only read back as published with the versions it was sent, and a landed publish alone is annotated with its id; ⛔ the marker in a report's or a draft's summary, description and packages and in every advisory answer's summary, description and vulnerabilities reaches no log line, row, step summary or annotation": 41,
1645+
"the mailbox branch delete: the pull's own answer, the open pulls on its head and the ref read immediately before ONE DELETE of the ref under the App token — an open, unlabelled, other-headed or forked pull, an open pull on that head, a ref moved off the pull's head, or a read that does not answer is zero writes with every failing fence named; the DELETE lands only when the ref reads back absent, a ref already gone an idempotent landing and one still there FAILED; and mailbox-open's close stroke — comment, close, delete — leaves no branch, while a head outside claude/mailbox-* sends no DELETE": 7,
15961646
"advisory_read: ONE GET after the sender gate and nothing written; the read, advisory and sender fences judged on that answer — target-write by the maintainer's ruling, a sender off the advisory's collaborators sealed for, one without write refused before the GET, any other gate closed — then the answer sealed to the seat's key and emitted as notice parts the reader parses back, opening byte for byte for that key and read alone; a failed or unsealed seal and an over-cap answer FAILED with nothing emitted; ⛔ the marker in the summary, description and vulnerabilities, and the advisory's id, reach no log line, row, step summary or annotation, and the sealed answer alone carries no marker": 18,
15971647
});
15981648
// Raised 17 → 18 with the governed-review follow-up battery, 18 → 19 with the run_rerun_failed battery, 19 → 20 with the
1599-
// hierarchy link battery, 20 → 21 with the advisory rows' battery, 21 → 22 with advisory_read's: one battery, one row.
1600-
const SELF_TEST_BATTERY_FLOOR = 22;
1649+
// hierarchy link battery, 20 → 21 with the advisory rows' battery, 21 → 22 with advisory_read's, 22 → 23 with the mailbox
1650+
// branch delete's: one battery, one row.
1651+
const SELF_TEST_BATTERY_FLOOR = 23;
16011652
const UNATTRIBUTED_BATTERY = '(unattributed)';
16021653

16031654
const batteryCases = new Map();
@@ -2457,6 +2508,50 @@ export async function selfTest() {
24572508
t('⛔ the parser is the reader\'s, imported — this file spells no pattern for the message of its own', [readReal(SELF_PATH, 'utf8').includes(['number=', '(['].join('')), typeof parseRelayAnnotation], [false, 'function']);
24582509
}
24592510

2511+
// ── the mailbox branch delete ───────────────────────────────────────────
2512+
battery("the mailbox branch delete: the pull's own answer, the open pulls on its head and the ref read immediately before ONE DELETE of the ref under the App token — an open, unlabelled, other-headed or forked pull, an open pull on that head, a ref moved off the pull's head, or a read that does not answer is zero writes with every failing fence named; the DELETE lands only when the ref reads back absent, a ref already gone an idempotent landing and one still there FAILED; and mailbox-open's close stroke — comment, close, delete — leaves no branch, while a head outside claude/mailbox-* sends no DELETE");
2513+
{
2514+
const PR = 30141;
2515+
const BR = 'claude/mailbox-ui-3';
2516+
const PULL = `GET /repos/${REPO}/pulls/${PR}`;
2517+
const OPEN = `GET /repos/${REPO}/pulls`;
2518+
const DEL = `DELETE /repos/${REPO}/git/refs/heads/${BR}`;
2519+
const BACK = `GET /repos/${REPO}/git/ref/heads/${BR}`;
2520+
const HEAD = 'a'.repeat(40);
2521+
const at = (sha) => ({ status: 200, json: { ref: `refs/heads/${BR}`, object: { sha, type: 'commit' } } });
2522+
const box = (o = {}) => ({ number: PR, state: 'closed', labels: [{ name: 'pm:mailbox' }, { name: 'pinned' }], head: { ref: BR, sha: HEAD, repo: { full_name: REPO } }, ...o });
2523+
const good = { ...allowed, [PULL]: { status: 200, json: box() }, [OPEN]: { status: 200, json: [] }, [DEL]: { status: 204, json: null }, [BACK]: [at(HEAD), { status: 404, json: { message: 'Not Found' } }] };
2524+
const drop = base([{ op: 'mailbox_branch_delete', pull: PR, branch: BR }]);
2525+
const ok = await run(drop, good);
2526+
t("the good case: the pull, the open pulls on its head, the ref at the pull's head, then ONE DELETE of the ref under the App token, then the ref read back absent — exit 0, the row naming it deleted", [ok.exit, ok.seen.slice(1).map((s) => s.call), writes(ok.seen).map((s) => [s.call, s.body, s.auth]), ok.rows[0].result, ok.annotations], [EXIT_OK, [PULL, OPEN, BACK, DEL, BACK], [[DEL, null, `Bearer ${TOKEN}`]], `deleted ${BR} (read back absent)`, []]);
2527+
const urls = [];
2528+
await mailboxBranchPreflight(OPS.mailbox_branch_delete.requests({ op: 'mailbox_branch_delete', pull: PR, branch: BR }, REPO)[0], { repo: REPO }, 'https://api.test', { token: TOKEN, fetch: async (url) => (urls.push(url), { status: 200, headers: new Headers(), json: async () => [] }) });
2529+
t("the open-pulls read is filtered to this repository's owner and the branch — an unfiltered or mis-owned read would answer for other heads", urls[1], `https://api.test/repos/${REPO}/pulls?state=open&head=objectstack-ai%3Aclaude%2Fmailbox-ui-3`);
2530+
const fences = [
2531+
[{ [PULL]: { status: 200, json: box({ state: 'open' }) } }, 'the pull is open, not closed'],
2532+
[{ [PULL]: { status: 200, json: box({ labels: [{ name: 'pinned' }] }) } }, 'does not carry pm:mailbox'],
2533+
[{ [PULL]: { status: 200, json: box({ head: { ref: 'claude/mailbox-ui-2', sha: HEAD, repo: { full_name: REPO } } }) } }, "the pull's head is claude/mailbox-ui-2, not claude/mailbox-ui-3"],
2534+
[{ [PULL]: { status: 200, json: box({ head: { ref: BR, sha: HEAD, repo: { full_name: 'someone/objectstack' } } }) } }, "head lives on someone/objectstack, not objectstack-ai/objectstack"],
2535+
[{ [BACK]: at('b'.repeat(40)) }, `${BR} is at bbbbbbbbbbbb, not the pull's head aaaaaaaaaaaa — a commit pushed after the close is not the mailbox's`],
2536+
[{ [BACK]: { status: 503, json: { message: 'down' } } }, 'the ref claude/mailbox-ui-3 could not be read'],
2537+
[{ [OPEN]: { status: 200, json: [{ number: 30999 }] } }, 'open pull(s) #30999 have claude/mailbox-ui-3 as head — the DELETE would close them'],
2538+
[{ [PULL]: { status: 404, json: { message: 'Not Found' } } }, 'the pull could not be read'],
2539+
[{ [OPEN]: { status: 503, json: { message: 'down' } } }, 'the open pulls on claude/mailbox-ui-3 could not be read'],
2540+
];
2541+
const fenced = await Promise.all(fences.map(async ([over, phrase]) => { const r = await run(drop, { ...good, ...over }); return [r.exit, writes(r.seen).length, r.rows[0]?.status, r.rows[0]?.result.includes(phrase)]; }));
2542+
t('⛔ each fence refuses with the DELETE NOT SENT and names itself: an open pull, no pm:mailbox, another head, a forked head, an open pull on the head, a ref moved past the head, an unread ref, an unread pull, an unread open-pulls list', fenced, fences.map(() => [EXIT_ACTION_FAILED, 0, 'not sent', true]));
2543+
const kept = await run(drop, { ...good, [BACK]: at(HEAD) });
2544+
t('⛔ a 204 whose ref still reads back is a FAILED action, never a landing', [kept.exit, kept.rows[0].status, kept.rows[0].result.includes('the branch is still there')], [EXIT_ACTION_FAILED, 204, true]);
2545+
const gone = await run(drop, { ...good, [BACK]: { status: 404, json: { message: 'Not Found' } }, [DEL]: { status: 422, json: { message: 'Reference does not exist' } } });
2546+
t('a ref already gone — absent before (no sha to fence), the DELETE answering 422, absent after — is an idempotent landing', [gone.exit, gone.rows[0].result], [EXIT_OK, 'already absent (idempotent)']);
2547+
const { planClose } = await import('../mailbox-open.mjs');
2548+
const closing = { ...good, [`POST /repos/${REPO}/issues/${PR}/comments`]: { status: 201, json: { id: 9 } }, [`PATCH /repos/${REPO}/issues/${PR}`]: { status: 200, json: { number: PR, state: 'closed' } } };
2549+
const closed = await run(base(planClose({ repo: REPO, pull: PR, head: BR }).actions), closing);
2550+
t("⭐ after close, no branch: mailbox-open's close stroke sends the comment, the close and then the DELETE, reads the pull only after closing it, and ends on the ref read back absent — exit 0", [closed.exit, writes(closed.seen).map((s) => s.call), closed.seen.map((s) => s.call).indexOf(PULL) > closed.seen.map((s) => s.call).indexOf(`PATCH /repos/${REPO}/issues/${PR}`), closed.seen.at(-1).call, closed.rows.at(-1).result], [EXIT_OK, [`POST /repos/${REPO}/issues/${PR}/comments`, `PATCH /repos/${REPO}/issues/${PR}`, DEL], true, BACK, `deleted ${BR} (read back absent)`]);
2551+
const other = await run(base(planClose({ repo: REPO, pull: PR, head: 'claude/project-thread-mvsts5' }).actions), closing);
2552+
t('…while a head outside claude/mailbox-* — a thread branch the seat still works on — is closed and left: no DELETE, nothing under /git/', [other.exit, writes(other.seen).map((s) => s.call), other.seen.some((s) => s.call.includes('/git/'))], [EXIT_OK, [`POST /repos/${REPO}/issues/${PR}/comments`, `PATCH /repos/${REPO}/issues/${PR}`], false]);
2553+
}
2554+
24602555
// ── the advisory rows that write ────────────────────────────────────────
24612556
battery("the advisory rows: advisory_report and advisory_create are ONE POST of their body each, landed on a 2xx carrying an id — a draft's reading state draft — that the row and a notice annotation name; advisory_collaborators_add, advisory_versions and advisory_state read the advisory first and send ONE PATCH — every existing login kept with the missing ones added, an all-present list a no-op, the versions entries alone, or the one state a legal transition reaches, the reopen and the publish among them — or nothing at all on a fence: an unread, other or wrong-state advisory, an illegal move, a list the versions may not replace or a publish whose versions are not set; each lands only when its answer shows the result, a publish only read back as published with the versions it was sent, and a landed publish alone is annotated with its id; ⛔ the marker in a report's or a draft's summary, description and packages and in every advisory answer's summary, description and vulnerabilities reaches no log line, row, step summary or annotation");
24622557
{

0 commit comments

Comments
 (0)