Skip to content

feat(pm): the relay deletes a closed mailbox's claude/mailbox-* head, and mailbox-open --close sends it in the close stroke - #30782

Draft
objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/project-thread-5r60ie
Draft

objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/project-thread-5r60ie

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Fixes #30350
Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:中继(objectstack-fleet[bot])多了一个只删不建的操作 mailbox_branch_delete,只删「已关闭、带 pm:mailbox 的 PR 自己的 claude/mailbox-* head 分支」;mailbox-open --close 关信箱时在同一笔里顺带删掉它。
  • 为什么改:落实 mailbox-open: a cloud seat must push its own --head branch, and --close never deletes it, so every cloud seating leaves a stray claude/ branch #30350 的裁决 A(「按推荐裁」):云端席位开信箱必须自己推一个分支,关信箱后这个分支删不掉,每坐一次云席就在仓库里留一个永不清理的 claude/ 分支,预检还会把它读成认领。
  • 风险与回滚:中继 token 本来每笔都带 contents: write(给 auto-merge 用),所以 workflow 和权限都没改;路径拒绝表只对 refs/heads/claude/mailbox-* 这一种路径开口,建引用、改引用、写文件仍一律拒;删前在 runner 上现读 PR,未关闭、无标签、head 不符、fork、或还有开着的 PR 用同一分支,任一条就不发 DELETE。回滚即 revert;中继只跑 main 上的代码,合并前不生效。
  • 先看:① scripts/pm/fleet-write/ops.mjs 的新行 mailbox_branch_delete 与 REFUSED_PATH_FAMILIES 里 /git/ 那一条(唯一放行的路径形状);② execute.mjs 的 mailboxBranchFenceFailures / mailboxBranchPreflight / confirmMailboxBranch;③ mailbox-open.mjs 的 planClose。
  • 两分钟验证: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 / 85 例),其中 ⭐「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", 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} and GET /pulls?state=open&head={owner}:{branch}, the last two 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; and no OPEN pull has that head.
    • 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-*.
    • 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, DELETE under the App token, read-back) and row; the open-pulls query filtered to objectstack-ai:claude/mailbox-ui-3; each of seven fences refusing with the DELETE not sent and naming itself; a 204 whose ref still reads back is FAILED; a 422 with the ref absent 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: 85 cases (the close-plan battery 3 → 5): 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.

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), and 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").
  • No sha fence. The ref's tip is not compared with the pull's head sha. The open-pull fence covers a branch reused by a new mailbox; a branch pushed to after its pull closed, with no pull on it, is deleted. Named here so the review can ask for it.
  • 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 pm:mailbox — domain:ui#3 · os-justin #30141 and claude/mailbox-ui-2 of mailbox pm:mailbox — domain:ui#2 · os-justin #30142, both pulls closed and labelled pm:mailbox. The third, mailbox/os-zhuang/triage-1, is the head of OPEN mailbox pm:mailbox — triage#1 · os-zhuang #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). +211 / −44 over four files: code and header prose +129 / −29, self-tests +82 / −15. ops.mjs 2367 → 2403, validate.mjs 2036 → 2061, execute.mjs 2994 → 3086, mailbox-open.mjs 689 → 703.
  • Overlap watch. execute.mjs's requestLanded (near L593, card pm-dispatch skill: on a private repository (objectstack-ai/cloud), a security finding is an ordinary issue labelled security, not a draft advisory #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

Derived gate families (40 of 40) and pnpm lint, head 1465a85

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on head 1465a855 (the branch commit 32b7ca47 with origin/main 2445cb49 merged in), 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 also ran green on 32b7ca47 before the merge, and the derived list is identical on both heads.

The relay self-tests, head 1465a85
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           85 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)

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.


Generated by Claude Code

… mailbox-open --close sends it

The relay gains mailbox_branch_delete, its one ref op: a DELETE of one
refs/heads/claude/mailbox-* ref, refused unless the pull it names is
closed, carries pm:mailbox, has that branch as its head on this
repository, and no open pull has that head. The fences are judged by the
executor on the pull's own answers immediately before the DELETE, and the
delete lands only when the ref reads back absent. REFUSED_PATH_FAMILIES
keeps refusing every other /git/ path; contents: write is already a
literal grant on every stroke, so the workflow is untouched.

mailbox-open --close appends the delete to its close stroke when the
head it reads is claude/mailbox-*, so a cloud seat's mailbox leaves no
branch.

Claude-Session: https://claude.ai/code/session_016WV4cwSv11c8FvyR8ZuWhv
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Nov 6, 2026
@github-actions github-actions Bot added the size/m label Nov 6, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-elon-musk · session_016WV4d1mkgEris4MHHLjtnk

Contract-review-claim: session_016WV4d1mkgEris4MHHLjtnk · head 1465a85 · 2026-11-06T02:49Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-elon-musk · session_016WV4d1mkgEris4MHHLjtnk

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1465a85502a330f34ec087527b54020c55c381b7
Local-runs: probe — offline --self-test of validate.mjs (337/17), execute.mjs (276/23), mailbox-open.mjs (85/10) and dispatch.mjs (318/27), all exit 0, in a detached worktree at the head sha under the scratchpad, removed after; no relay, no dispatch, no network write
Scope: (hits) scripts/pm/fleet-write/ops.mjs, scripts/pm/fleet-write/validate.mjs, scripts/pm/fleet-write/execute.mjs, scripts/pm/mailbox-open.mjs + (cited, read not reviewed) .github/workflows/fleet-write.yml, scripts/pm/write-pace.mjs, scripts/pm/fleet-write/dispatch.mjs, .claude/skills/pm-dispatch/references/rest-channel.md

① Derived judgments

  • New op row mailbox_branch_delete (ops.mjs L2186-2191): required: ['pull','branch'], optional: [], permission: 'contents', no alsoSpends, not alone; its one request is DELETE /repos/REPO/git/refs/heads/BRANCH with no body and a mailboxBranch descriptor. Correct: delete-only, one request, rides the close stroke (validate.mjs L1283 pins the close stroke's token as the five literal grants, nothing stroke-scoped).
  • New field branch (ops.mjs L1809): str, max: MAX_REF_BYTES, shape: MAILBOX_BRANCH_SHAPE, what sentence; validate.mjs L336 enforces shape with a message that names the shape and never echoes the value. Correct. The shape /^claude\/mailbox-[a-z0-9]+(?:[._-][a-z0-9]+)*$/ (ops.mjs L1732) admits no second /, no .., no %, no upper case, no space, no empty or trailing-separator slug, so the raw interpolation into the path at L2190 cannot escape refs/heads/claude/mailbox-.
  • MAILBOX_LABEL moved to ops.mjs L1734 and re-exported by mailbox-open.mjs L160 (import L147): one spelling. Correct.
  • REFUSED_PATH_FAMILIES (ops.mjs L2369-2375): only the /git/ entry changed, to new RegExp('/git/(?!refs/heads/' + shape-without-anchors + '$)') (L2373); /merge$, /pulls/N/reviews, /branches/, /contents/, the actions and advisory families are byte-identical. Correct and exactly one path shape. Note: the family list is a construction pin only — grep shows it used solely in validate.mjs self-tests (L1225, L1227, L1281, L1384, L1410, …), never at run time in execute.mjs — so the runtime boundary is the closed op table + closed keys + field shape, re-judged by validatePayload(raw) inside executeFleetWrite (execute.mjs L1354) before any request.
  • PERMISSIONS unchanged (validate.mjs L1239 pin unchanged: contents: 'write' was already there); .github/workflows/fleet-write.yml L136 already mints permission-contents: write as a literal input, confirming the dev's "no workflow / PERMISSIONS change". Correct. The validate self-test pins at L1253 / L1254 now name the third contents spender and that its one REST request is a DELETE of a head ref.
  • execute.mjs: mailboxBranchFenceFailures (L911-922, pure), mailboxBranchPreflight (L925-933) reads GET /pulls/PULL and GET /pulls?state=open&head=OWNER:BRANCH fresh under the App token; confirmMailboxBranch (L935-941) reads GET /git/ref/heads/BRANCH back and lands only on 404, a non-2xx DELETE with an absent ref being an idempotent landing. Wired at L1475 (the pre slot, refused rows pushed as not sent and the stroke stopped) and L1566 (the landed chain). requestLanded (card pm-dispatch skill: on a private repository (objectstack-ai/cloud), a security finding is an ordinary issue labelled security, not a draft advisory #30114's site) untouched. Correct.
  • mailbox-open.mjs: planClose({ repo, pull, head = null }) (L296-303) appends mailbox_branch_delete last, only when head matches the shape (L297, L301); closeLive re-plans with the live head after closeRefusal (L457-458); dry-run text and the cloud refusal message (L422) now prescribe --head claude/mailbox-SLUG. Correct; closeRefusal (L346-350) still refuses a non-open or unlabelled pull before anything is sent.
  • Self-test floors: validate 16 to 17 (L1027), execute 22 to 23 (L1652); mailbox-open close battery 3 to 5; dispatch.mjs unchanged (its op lists derive from OP_NAMES: BODY_OPS L942, RUN_OPS L945, CONTENT_CREATING_OPS L1402; READ_BACK_LOCATORS is pinned to exactly BODY_OPS at L2414, and the new row carries no body). Consistent.

② Semver level

None: scripts/pm/** only, no published package or exports map touched; skip-changeset label on the PR is correct. PR body Fixes #30350, Clause-②: no, Metadata-protocol: none re-derived from the 4-file surface: all correct.

③ Boundary flags

PR author objectstack-fleet[bot] (relay-written; no login note owed). Dev deviations, each answered:

  • (1) Two extra fences (head repo == this repo, L918; no OPEN pull on that head, L919-920): both narrowing, both inside ruling A's "anything else refused". Accepted.
  • (2) No sha fence: see should-fix 1 below; disclosed in the PR body, not a ruling condition.
  • (3) Acceptance probe only after merge (relay runs main): true by construction of the relay; the probe's actions file names only pm:mailbox — domain:ui#3 · os-justin #30141 / pm:mailbox — domain:ui#2 · os-justin #30142, both closed and labelled; mailbox/os-zhuang/triage-1 is outside the shape. Accepted.
  • (4) Merge of origin/main rather than rebase: no bearing on the surface.
    Out-of-scope findings confirmed: fleet-write.yml L134-135 ("has NO contents op — no file, ref or branch write") becomes false on landing (Tier H, outside the surface; the seat to card it); rest-channel.md holds no full op registry (only prose naming advisory_read, automerge_disable at L50, L58, L60), so the gap is a doc mention, not a registry failure (Tier S, outside the surface). open_questions: []. Head check-runs at 2026-11-06T02:57Z: 23 success, 21 skipped, 1 in progress (Test Core 1/8), 0 red; the seat lands only on all green.

④ Design judgment

Must-checks (a)-(i), each against the head:

  • (a) Carve-out reachable by one path shape only: the only /git/ path any row spells is L2190; every other row that interpolates a caller string into a path encodes it (labels_remove L2047, workflow_dispatch L2195 via enc) or shape-validates it (ghsa_id), so no row can produce /git/. Traversal, %2F, .., case, empty slug, trailing segment, qualified ref, space: refused by the shape (validate.mjs L1277-1278, twelve names). Tags, trees, blobs, commits, nested refs, the create path refs, the singular ref path and /contents/: still caught (positive control L1227). Other repos: payload.repo is TARGET_REPO_SHAPE and the head-repo fence (L918). POST/PATCH on the same path: no row (L1225 pins the single /git/ request as ['mailbox_branch_delete','DELETE',…]; ALLOWED_MUTATIONS L2367 unchanged). PASS.
  • (b) All conditions judged server-side on a fresh GET /pulls/PULL (L927) and the open-pulls list (L929), the last two reads before the DELETE (L1475 then L1507); the request contributes only pull and branch. TOCTOU: one runner turn with no sleep between the reads and the DELETE; the read-back (L937) verifies the outcome. Reasoned in both headers (ops.mjs L58-74, execute.mjs L193-202). Acceptable.
  • (c) Delete-only: one DELETE, no create or move anywhere (L1225, L2367). PASS.
  • (d) Loud refusals: validator sentence at L336; each fence names itself at L913-920 and the row is not sent with REFUSED at the relay's fence … (L932). PASS.
  • (e) --close sends comment, close, delete in one stroke (L296-303, L457-458); execute.mjs L2547 pins that stroke through the executor: DELETE sent after the PATCH, pull read after the close, last call the ref read back absent. PASS.
  • (f) Refusal cases: open pull, no label, other head, forked head, open pull on head, unread pull, unread list (execute L2529-2539); 204-but-present FAILED (L2541); 422-and-absent idempotent (L2543); non-mailbox head closed with nothing under /git/ (L2549); good case (L2525); twelve bad names (validate L1277-1278); closed keys (L1279). PASS.
  • (g) Registries: validate.mjs minimal ledger and three per-op ledgers updated (diff L1172, L1372, L1529, L1796); dispatch.mjs derives; WIRED_WRITE_TOOLS (write-pace.mjs L1334-1344) needs no entry because mailbox-open issues no write verb itself (it spawns dispatch.mjs, L380). PASS.
  • (h) Only the /git/ row of REFUSED_PATH_FAMILIES changed. PASS.
  • (i) No .github/workflows/** in the 4-file list; skip-changeset correct; body lines correct; surface held. PASS.

Per hits file:

  • ops.mjs: the row, the shape, the field and the family carve-out are sound; the carve-out is built from the shape's own source (L2373), so the two cannot drift. One remark: the family regex is method-blind (a PATCH of the same ref path would pass it), which is fine today only because the family is a construction pin and L1225 pins the verb; keep that pin.
  • validate.mjs: shape honoured only on str (L336); refusal names the shape, no echo. Sound.
  • execute.mjs: fences and read-back sound; the label fence is an accident guard rather than a privilege guard, since the same write-permitted sender can labels_add + issue_patch in the same stroke, so the effective boundary is "head of a closed pull, named claude/mailbox-*, on this repo, with no open pull on it", which is what ruling A asked for.
  • mailbox-open.mjs: planClose keeps the delete last; the dry run reads nothing and says the delete rides conditionally. Sound.

should-fix 1: no sha fence (execute.mjs L911-922). A closed pm:mailbox pull is a permanent licence to delete whatever claude/mailbox-SLUG later holds, as long as no OPEN pull has that head; commits force-pushed to that name after close, with no pull on them, would be deleted. Blast radius is bounded to a name mailbox-open reserves for empty-commit mailbox heads (L79-80, L422), so not a must-fix, but one extra fence (GET /git/ref/heads/BRANCH before the DELETE, object sha == pull head.sha, else refuse) would make "that PR's head" literal and cost one read.
should-fix 2: proportion vs card #29471 (net +167): code is proportionate to the ruling's pins, but the fence list is restated in prose four times (ops.mjs L58-74, the PERMISSIONS doc-comment, execute.mjs L193-202, mailbox-open.mjs L98-103); keep one and point at it.
should-fix 3: if the relay's GET /pulls/PULL lags the PATCH close of the same stroke, the fence refuses, the pull is already closed and closeRefusal (L346-349) then refuses a re-run of --close, leaving exactly the leftover the card is about; have the failure path name the bare-actions remedy (one mailbox_branch_delete action via dispatch.mjs).

Implemented-by: claude/project-thread-5r60ie
Reviewed-by: session_016WV4d1mkgEris4MHHLjtnk

VERDICT: PASS


Generated by Claude Code

… close-stroke delete names its bare re-send

Contract review should-fixes on #30782:

- execute.mjs: mailboxBranchPreflight reads GET /git/ref/heads/{branch}
  as the last read before the DELETE and refuses unless the ref points at
  the closed pull's head.sha, so the op deletes that PR's head and never a
  commit pushed after the close. A ref already absent fences nothing (the
  DELETE's 422 lands idempotent). Self-test: a moved ref and an unread ref
  each refused with nothing sent; the good case reads the ref before the
  DELETE.
- mailbox-open.mjs: when a --close stroke that carried the delete fails,
  closeRemedy prints the one bare mailbox_branch_delete action to send
  through fleet-write/dispatch.mjs, since --close refuses a closed pull.
  Self-test pins the action and that the validator accepts it.
- The fence list now lives only in ops.mjs's header; execute.mjs and
  mailbox-open.mjs point at it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwSv11c8FvyR8ZuWhv
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-elon-musk · session_016WV4d1mkgEris4MHHLjtnk

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 83464d0baa3a07a2c1dcf46c7eb582375a617aae
Local-runs: probe — offline --self-test of execute.mjs (276/23), mailbox-open.mjs (87/10), validate.mjs (337/17) and dispatch.mjs (318/27), all exit 0, in a detached worktree at the head sha under the scratchpad, removed after; no relay, no dispatch, no network write
Scope: Delta review of 1465a85..83464d0; prior PASS 6675668758 carries for unchanged hunks. (hits) scripts/pm/fleet-write/execute.mjs, scripts/pm/fleet-write/ops.mjs (header only), scripts/pm/mailbox-open.mjs + (cited, confirmed byte-identical to the prior head) scripts/pm/fleet-write/validate.mjs, .github/**, scripts/pm/write-pace.mjs, scripts/pm/fleet-write/dispatch.mjs

① Derived judgments

  • One commit on 1465a85 (83464d0b, parent 1465a855), 3 files, +54/−35; the PR surface stays the same 4 files, labels size/m, skip-changeset unchanged, still draft. No row, field, family, permission or workflow moved: the ops.mjs delta is header prose only (L66-78; the diff touches no export, no row object, no family entry), and the row at L2190-2194, the field at L1813, the shape at L1736 and the /git/ family at L2377 are byte-identical to the reviewed head. Correct.
  • Sha fence (execute.mjs L906-920): mailboxBranchFenceFailures now takes a ref answer; at is object.sha only when the answer is 200 AND its ref equals refs/heads/BRANCH exactly (L916), so a prefix-matched or malformed answer yields no sha; any non-404 answer without a string sha is refused as unread (L917); any non-404 answer whose sha differs from the pull's head.sha is refused by name (L918), an unread head.sha included (at !== undefined). A 404 fences nothing. Correct.
  • Read order in mailboxBranchPreflight (L923-931): pull (L926), open pulls on OWNER:BRANCH (L927), ref (L928), then the DELETE at the loop's send (L1474 preflight, the write after it), then the read-back in confirmMailboxBranch (L934-938, unchanged). The self-test pins the exact sequence [PULL, OPEN, BACK, DEL, BACK] with one write (L2526). Correct and as the coordinator's brief states.
  • The 404 case: an absent ref lets the DELETE go, which deletes nothing (GitHub answers 422, read back 404, landed as idempotent, L2546). It cannot delete anything that existed at the pre-read; the only exposure is a ref created inside the preflight-to-DELETE window, which is the same CAS-less window every fence here has (GitHub's ref DELETE takes no precondition), and the name space is the mailbox one. Reasoned; acceptable.
  • No new reachable path or verb: the added read is an executor GET of the same /git/ref/heads/BRANCH path the read-back already used (L928 vs L936), under the same App token; it is not a row request, so validate.mjs L1225 ("the ONE request any row sends under /git/ is the DELETE") still holds, and validate.mjs itself is unchanged (337/17 re-run green). Correct.
  • closeRemedy (mailbox-open.mjs L347-351): returns null without a delete in the plan (L348), else a sentence carrying node scripts/pm/fleet-write/dispatch.mjs --repo REPO --actions-file FILE --json and the one-action JSON; the self-test parses that JSON back and runs it through validateStroke with [] errors (L605) and pins null for a delete-less plan (L606). closeLive prints it on any non-OK exit when the stroke carried the delete (L468), with the prose conditioned on "if the stroke stopped at mailbox_branch_delete". The remedy names a valid action. Correct.
  • Prose dedup: execute.mjs header now points at ops.mjs (L193-196); mailbox-open.mjs header points there too (L100); the fence list is spelled once (ops.mjs L69-78). Should-fix 2 of the prior record is met.
  • Self-test accounting: execute battery text updated, still 7 cases (the fence table grew to 9 entries inside one t, L2542); mailbox-open close battery 5 to 7 (87 total); floors unchanged (execute L1651 = 23). Consistent with the dev's counts; verified by the probe.

② Semver level

None, unchanged: scripts/pm/** only; skip-changeset correct; Fixes #30350, Clause-②: no, Metadata-protocol: none still correct for the surface.

③ Boundary flags

PR author objectstack-fleet[bot]. Prior should-fix 1 (sha fence) met at L916-918 with its order pinned; prior should-fix 3 (remedy) met at L347-351 and L468; prior should-fix 2 (prose) met. No new deviation declared. Head check-runs at 2026-11-06T03:30Z: 9 success, 16 skipped, 13 in progress (Test Core 1/8 and 2/8, the Lint and Type Check jobs), 0 red; the seat lands only on all green. Out-of-scope items from the prior record stand (fleet-write.yml L134-135 stale prose, Tier H; rest-channel.md mention, Tier S).

④ Design judgment

  • execute.mjs: the fence is correct and ordered as pull, open pulls, ref, DELETE, read-back; each refusal names itself with the DELETE not sent (L2535-2542: a moved ref and an unread ref join the seven prior fences, all not sent, zero writes); a 204 with the ref still present stays FAILED (L2544). The exact-ref equality at L916 is the right guard against a prefix-matched answer. Sound.
  • ops.mjs: header is now the single statement of the fences and the landing (L69-78); no code moved. Sound.
  • mailbox-open.mjs: closeRemedy is pure and validator-checked; printed only when a delete rode the stroke. Sound.

should-fix 1: the PR body is stale on this head: its "Acceptance notes" still say "No sha fence" (body L56), "the last two reads before the DELETE" (L28), the execute battery's call order without the ref read (L50), 85 cases for mailbox-open (L51, L124) and the line delta +211 / −44 (L61, now +230/−44 on the PR). Re-derive those lines so the body describes the boundary that lands.

Implemented-by: claude/project-thread-5r60ie
Reviewed-by: session_016WV4d1mkgEris4MHHLjtnk

VERDICT: PASS


Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mailbox-open: a cloud seat must push its own --head branch, and --close never deletes it, so every cloud seating leaves a stray claude/ branch

1 participant