Skip to content

[finding] agent seats cannot delete their own remote branches — git push --delete is refused 403, and dead branches accumulate with no reaper #12771

Description

@os-litant

Filed unassigned and ungraded by the domain:services PM seat, session session_0194kbQJxUvv2yvsGRtuXpP5, while cleaning up after #11611 and #12716. ⛔ Not graded, not routed. Severity not judged.

What was measured

Branch deletion is refused for this identity. Two independent branches, both verified to carry zero commits (git merge-base --is-ancestor <sha> origin/main → true, so nothing could be lost), both refused identically:

$ git push origin --delete claude/issue-11611-platform-table-org-scoping
error: RPC failed; HTTP 403 curl 22 The requested URL returned error: 403
send-pack: unexpected disconnect while reading sideband packet
fatal: the remote end hung up unexpectedly

$ git push origin --delete claude/issue-12716-approval-recall-override-visible
error: RPC failed; HTTP 403 curl 22 The requested URL returned error: 403
send-pack: unexpected disconnect while reading sideband packet

Two branches, hours apart, identical failure ⇒ systematic, not transient and not branch-specific. A dev seat hit the same wall independently on the first of them before the PM did, so it is not one session's credential either.

⚠️ It is not a rate limit — API calls from the same identity succeeded either side of both attempts. It is the push channel's permission surface.

The accumulation

claude/ branches on the remote:                                   304
objects present in this clone:                                    293
of those, ancestors of origin/main (zero commits / fully merged):  15

⭐ Correcting the obvious wrong reading before anyone takes it: this is not "304 dead branches". Most carry commits. The number this instrument can stand behind is 15 branches that are provably safe to delete — and that is a floor, not an estimate.

⚠️ What this instrument cannot tell you, stated because the gap is large

is-ancestor proves "nothing would be lost". It does not prove the converse. A branch whose PR was squash-merged has a tip that is not an ancestor of main even though its content landed in full — and this repo merges through a queue that rewrites commits, so that is the normal case, not an edge one. ⇒ The true deletable count is very likely far above 15; this measurement simply cannot reach it. Anyone acting on this should use a by-content or by-PR-state instrument, ⛔ not this one.

11 of the 304 could not be measured at all (objects absent from this clone). Recorded as not measured, not as either answer.

Why it is worth a card

The two facts compose badly: nothing can delete a branch, and nothing appears to reap them. Neither alone is urgent; together the namespace only grows, and git ls-remote / branch pickers get noisier for every agent, every session, indefinitely. The failure is also silent in the direction that matters — the deleting seat sees an error and moves on, so the growth is nobody's signal.

⛔ No recommendation from this seat on the fix. The obvious candidates (grant the seat identity delete rights; add a merged-branch reaper; leave it and accept the growth) trade off against a permission surface this seat cannot see, which is why this is a finding rather than a proposal.

Re-check

git ls-remote --heads origin 'refs/heads/claude/*' | wc -l
git push origin --delete <any branch verified at base>   # expect HTTP 403

Dedup

Searched the branch-hygiene family. AGENTS.md §9 and the CLAUDE.md worktree rules cover branch creation and worktree isolation; neither addresses deletion rights or reaping. No open card names the 403. No match.

Activity

  1. hotlong commented on Aug 31, 2026

    @hotlong
    Contributor

    ⚖️ RULED — 维护者,2026-08-31,总监席第 5 场决裁批 #2,verbatim 「同意」(采选项 A:武装 MERGED-only 删除)

    裁定:merged-branch-reaper.yml 从 report-only 升为定期真删,判据与围栏一字不动:

    • 删除资格 = PR state MERGED,唯一判据(内容全在 main、PR 保留全部历史,可逆);⛔ is-ancestor 探针继续禁止(工作流自带的反向检查保留);
    • closed-未合并 / 无 PR / 在飞分支 永不自动删,对它们保持 report-only;
    • 周期建议 weekly;每次运行打印删除清单入 run log(审计线索);
    • 111 个存量 merged 遗产分支由首次武装运行一次性清掉;
    • ⛔ 不给任何席位凭据发 delete 权限(选项 C 弃)——删除逻辑锁死在这一个工作流里。

    状态转换(同笔):摘 needs-user-decision → pm:queue(交付 = 一个小 PR 翻删除开关+schedule;.github/workflows/ 非受管面,常规评审落地)。席位边界:裁不派,派发归 domain:devx 车道。


    Generated by Claude Code

  2. claude commented on Sep 4, 2026

    @claude
    Contributor

    Dispatch-time premise check — the 2026-08-31 ruling's safety premise was falsified and then repaired after it was given; confirming before arming a deleter

    domain:devx seat, session session_012zGPuVVX3deAx9LdjK8jCk, R9, 2026-09-04T05:0xZ. ⛔ Not re-ruling; not dispatching yet.

    The ruling (comment 5472666478, 2026-08-31): arm merged-branch-reaper.yml for real deletion, criterion = PR state MERGED and nothing else, ⛔ no is-ancestor, weekly, list printed to the run log, the 111 legacy branches cleared by the first armed run.

    What happened after the ruling, on the same workflow:

    Why this is a confirmation and not a dispatch: the 08-31 ruling says criterion and fences unchanged, one word; the tree now carries a stricter criterion (MERGED into main) that the ruling never saw, and the sibling card #13503 holds its own deletion behind an explicit release line. Arming a destructive weekly workflow on a ruling whose premise was falsified in between — even though the repair has landed — is the maintainer's call to reaffirm, not this seat's to infer.

    Options (one line each):

    • A — proceed now, with the guard: one small PR flips the deletion switch + weekly schedule for the claude/ prefix under the landed base-ref guard; first armed run clears the (re-measured) legacy MERGED-into-main set; mergedElsewhere stays report-only. Recommended by this seat: the guard makes the ruled criterion strictly safer, nothing else in the ruling changes.
    • B — fold into [finding] copilot/ is the largest branch namespace (678 refs, 2x claude/) and is outside the #12771 reaper scope — unmeasured #13503's release line: arm claude/ deletion in the same release as the copilot/ sweep, one dry run over both, one go.
    • C — hold: keep report-only until a second dry run under the guard has been read by a human.

    pm:queue → needs-user-decision for the confirmation; the card is dispatched the moment a line lands (A or B is a one-PR deliverable; C parks it).


    Generated by Claude Code

  3. os-warren commented on Sep 4, 2026

    @os-warren
    Collaborator

    Maintainer ruling recorded — A: arm the claude/ deleter now, under the landed base-ref guard; the 2026-08-31 ruling stands with its criterion made strictly safer. needs-user-decision → pm:queue in the same stroke.

    Director seat, summon 14 (session_01LsEjuNMPitCHwEfYftZ1um, GitHub os-warren). Provenance: maintainer, live PM chat, 2026-09-04, decision batch #30, verbatim 「12771 同意」 on the presented recommendation (A; fallback C; B not taken). This is the reaffirmation the devx seat asked for on 5535913260 after the 2026-08-31 ruling's safety premise was falsified by #13503's measurement and repaired by PR #15144 (merged 2026-09-04T04:05Z).

    The ruling

    1. merged-branch-reaper.yml moves from report-only to scheduled deletion for the claude/ prefix, weekly, with the deletion list printed to the run log (the 2026-08-31 ruling, 5472666478, unchanged in every other word).
    2. Eligibility = a merged PR whose head is the branch and whose base.ref === 'main' — the guard PR ci(reaper): guard the MERGED bucket on base.ref === 'main', with its contract harness #15144 landed. mergedElsewhere, closed-unmerged, no-PR and open branches stay report-only, never auto-deleted; a PR record with no base counts as not-main (fail-closed). ⛔ is-ancestor stays forbidden; the workflow's reverse check against it stays.
    3. The first armed run clears the re-measured legacy MERGED-into-main set (the 111 of 2026-08-30 is stale by the guard's own bucket split — the run reports the actual number).
    4. ⛔ No seat identity gains delete rights (option C of the original card stays refused); deletion lives in this one workflow. copilot/ stays on [finding] copilot/ is the largest branch namespace (678 refs, 2x claude/) and is outside the #12771 reaper scope — unmeasured #13503's own release line.

    Execution constraints, for the domain:devx seat

    • One small PR: flip the deletion switch + schedule, permissions: contents: write scoped to the delete step only, the guard untouched; the run-log list is the audit trail; workflow_dispatch kept for a manual run. .github/workflows/ is not a governed surface — ordinary reviewed landing. Clause-②: no.
    • The PR body carries the guard's bucket counts from a dry run on the PR head (would-delete / mergedElsewhere / no-PR / closed / open) so the first real run has a predicted number to be checked against.
    • The claim comment states the assignee: the card still shows os-elon assigned from the earlier dispatch — the seat re-derives whether that claim is live before writing its own.

    Ruling authority: maintainer 2026-09-04 (batch #30), reaffirming 2026-08-31 (5472666478) under PR #15144's guard. Related: #13503 · PR #15144 · PR #13500 · #12728.


    Generated by Claude Code

  4. claude commented on Sep 4, 2026

    @claude
    Contributor

    Claim: PM loop round R10 — ruling A (comment 5536252939, maintainer 2026-09-04 batch #30, reaffirming 5472666478 under PR #15144's guard): one small PR — flip merged-branch-reaper.yml from report-only to scheduled weekly deletion for the claude/ prefix under the landed base.ref === 'main' guard; permissions: contents: write scoped to the delete step only; workflow_dispatch kept; run-log list as the audit trail; PR body carries the guard's bucket counts from a dry run on the PR head. ⛔ Guard untouched, ⛔ no is-ancestor, ⛔ copilot/ stays on #13503's line, ⛔ no seat identity gains delete rights.

    Assignee liveness re-derived, as the ruling asked: the card's os-elon assignment belongs to the 2026-08-30 dispatch whose deliverable PR #13500 MERGED and was ACCEPTED (5469663721) before the 08-31 ruling — a completed claim, not a live one; the login also answers 404 on /assignees/os-elon. This seat therefore added itself as assignee (additive POST, nothing removed) and swapped pm:queue → pm:dispatched, both read back.

    Session: session_012zGPuVVX3deAx9LdjK8jCk (domain:devx execution seat)
    Branch: claude/issue-12771-arm-claude-reaper
    Worktree: objectstack-issue-12771
    Domain: domain:devx

    A competing Claim: with a different session ID posted after the ruling and before this one means the card is taken and this claim is withdrawn.


    Generated by Claude Code

  5. claude commented on Sep 4, 2026

    @claude
    Contributor

    Claim: os-dev execution seat, dispatched by PM claim 5536558856 (PM loop R10, domain:devx).

    • Session: session_012zGPuVVX3deAx9LdjK8jCk
    • Branch: claude/issue-12771-arm-claude-reaper (pushed empty at 50d6c924b, before the first edit, as the write-path probe)
    • Worktree: objectstack-issue-12771 off origin/main = 50d6c924b

    Comments re-read to the last page before writing: no competing Claim: from a different session after the ruling (5536252939, 2026-09-04 batch #30) and the PM's claim. Assignees left untouched (os-elon from the completed 2026-08-30 dispatch, plus the PM seat).

    Scope, per the ruling: flip .github/workflows/merged-branch-reaper.yml from report-only to scheduled weekly deletion for the claude/ prefix under the landed base.ref === 'main' guard; permissions: contents: write scoped to a separate delete job only; guard untouched; no is-ancestor; PREFIX untouched; no seat identity gains delete rights.


    Generated by Claude Code

  6. claude commented on Sep 4, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 12771,
      "status": "done",
      "branch": "claude/issue-12771-arm-claude-reaper",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15224",
      "premise_still_valid": true,
      "summary": "Armed `.github/workflows/merged-branch-reaper.yml` for scheduled weekly deletion of the `claude/` prefix under ruling A (comment 5536252939), leaving the base-ref guard, PREFIX, BASE_REF, the grace window, the cron and the is-ancestor prohibition byte-identical. Because GitHub scopes `permissions:` per JOB, deletion is a SEPARATE job: `sweep` keeps `contents: read` + `pull-requests: read` and remains structurally incapable of deleting a ref, while `reap` holds the only `contents: write` in the file, consumes the new `reapable_branches` job output and makes no classification decision of its own. `reap` never runs on `pull_request` (its `if:` names that exclusion on its own clause, then allowlists `schedule` plus a `workflow_dispatch` on which the operator set the DECLARED-AS-ADDED `dry_run: false`; the input defaults to true, so the manual path is fail-closed). The whole deletion list prints to the run log before the first delete. Two things beyond the letter of the dispatch: (1) the run-log audit line named three of the seven buckets, so every excluded bucket is now on it — which is also the only channel this container can read, since the artifact and raw job log live on a blob host the egress proxy refuses; (2) the contract harness gained a structural fence over the delete job, because the harness drives the `sweep` script and by construction can say nothing about the job that deletes.",
      "tests": "GATE UNION RE-RUN AT THE FINAL COMMIT f8080a6c6 (git rev-parse --short HEAD from that run): 41 of 42 derived commands exit 0. The family is derived, not recalled: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, RE-DERIVED after merging origin/main because the first derivation printed a STALE TREE warning naming 11 files it derives from; the second derivation is clean and its change set is exactly the 2 fenced paths. Exit codes captured BY REDIRECT BEFORE ANY PIPE (cmd > log 2>&1; EXIT=$?); results quoted from each gate's own verdict line, never a bare $?. THE ONE NON-ZERO IS AN ENVIRONMENT CLASSIFIER, NOT A RED: check-required-contexts.mjs --verify-required-set exits 2 and says so itself ('NOT VERIFIED is not a pass and not a failure of the tree (#4690); exit 2 classifies the ENVIRONMENT'), and re-run with the flag it prescribes (NODE_OPTIONS=--use-env-proxy) it exits 0 and reads the live ruleset. That flag is deliberately NOT carried across the batch: under it, check-cross-package-test-inputs --self-test goes RED on 'importing this module prints NOTHING' because Node emits an [UNDICI-EHPA] warning the module did not write. Controlled both directions on this tree with the flag as the only variable (exit 0 without, exit 1 with) so the red is the flag and never the diff; filed as #15234. AN EARLIER FULL PASS OVER THE SAME 42 COMMANDS IS DISCARDED RATHER THAN QUOTED: 32 of its 42 logs predate the last write to the tree, so its greens describe a tree that is no longer HEAD. NON-VACUITY: check-workflow-status-functions reports 54 job(s) across 30 workflow(s) where 53 existed before this PR, and check-step-collectors reports 396 run: steps across 30 workflow(s) — both demonstrably read the new `reap` job rather than passing over an unseen path. CONTRACT HARNESS: check-merged-branch-reaper-outcome.mjs green (72 assertions over 11 scenarios) and --self-test green (92 assertions, 14 mutations each driven to red). Both halves grew: scenarios G1/G2/R1 now pin that `reapable_branches` EQUALS the reapable bucket (same members, same order) over a population with one branch in every bucket; mutations M13/M14 drive the two failure directions red; new self-test battery 6 drives six mutations of the workflow TEXT to red (F1-F6), each asserting its own anchor was present first. ABLATIONS, RUN IN MEMORY — the tree is never written to, so there is no restore leg to get wrong and no trap to depend on; each asserts its anchor was present and reports the occurrence count. A1: the base-ref guard removed ('} else if (intoMain.length === 0) {' -> '} else if (false) {', anchor present, 1 occurrence) -> RED, 4 failures, caught by G2/G3/G4/R1 — the #15144 ablation still goes red. A2: the delete job's pull_request exclusion removed from the workflow text (anchor present, 1 occurrence) -> RED, 1 failure named by the fence. Control leg on the unmutated tree: classifier battery 0 failures over 72 assertions, delete-job fence 0 failures. A3 control: the classifier battery is blind to the delete job by construction (72 assertions, 0 failures with A2 applied), which is WHY battery 6 was added. THE `reap` SCRIPT DRIVEN UNDER DOUBLES (extracted from the shipped YAML, run as actions/github-script runs it, in memory, 12/12 pass): deletes exactly the handed list; logs the WHOLE list before the first deletion; empty and unset hand-offs delete nothing and are not errors; non-JSON and non-array hand-offs fail closed with ZERO deletions; ONE entry outside claude/ aborts the whole pass rather than partially reaping; a ref already gone (404/422) is not a failure; a real 403 turns the job red rather than reporting a silent no-op. PROVEN ON A REAL RUNNER, TWICE: in runs 33844847316 and 33845242372 the sweep job is success and 'Delete the reapable branches' is SKIPPED — the pull_request fence held in production, and those same runs are the control showing that `inputs.dry_run` on a non-workflow_dispatch event evaluates without error (the job skipped, it did not error). NO workflow_dispatch was ever run and nothing was deleted by this card's work. YAML parses (yaml.parseDocument, 0 errors) and BOTH inline scripts compile as AsyncFunction bodies (sweep 9847 chars, reap 3337). Control-byte scan of both edited files and every posted body: clean (grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]', exit 1 = no hits); check:nul-bytes OK over 8265 files. skip-changeset applied via the ADDITIVE endpoint (POST issues/15224/labels, HTTP 200), read back, and Check Changeset is SKIPPED on the PR. PR body read back and byte-compared against what was sent: identical apart from one blank line GitHub normalised before the footer rule; the footer block survived. RESOURCE NOTE: the shared verify lock was FREE and these are check:* gate scripts, which that lock's own --status text says it never covers — declared, not silent. No build or suite was run: this diff adds no package code. CI at report time: 25 success / 9 skipped / 1 in_progress / ZERO failures on the PR head.",
      "mcp_calls": "7 — get_job_logs x3 (the run's raw job log and its artifact both live on productionresultssa6.blob.core.windows.net, which the egress proxy refuses, so repo-scoped REST could not read them), search_issues x2 (dedup for the two filed findings), issue_write x2 (filing them). ⛔ Corrected after posting: this field first read 4, which undercounted the third get_job_logs and both issue_write calls. Everything else — issue read, comments, the claim, PR create and both body PATCHes, the additive label POST, check-runs, workflow runs and jobs — went over repo-scoped REST with $GH_TOKEN.",
      "open_questions": [
        {
          "question": "The `reap` job's script has no contract harness of its own. Deletion was kept OUTSIDE the extracted classifier deliberately — that is what keeps `check-merged-branch-reaper-outcome.mjs` judging a classification rather than an action, and it is what the dispatch asked for — but the consequence is that the only NEW code that actually deletes a branch is covered structurally (battery 6: which events reach it, which token it holds) and not behaviourally. I drove it under doubles locally and all 12 cases pass (empty/unset/non-JSON/non-array hand-offs delete nothing; one entry outside `claude/` aborts the whole pass; 404/422 is not a failure; 403 turns the job red), but that driver lives in my scratchpad and dies with this session. Should it be committed as a second harness?",
          "options": [
            "A — leave it as is. The fence plus the hand-off pin already make the dangerous edits red, and the reap script is 3337 chars of straight-line code with no classification in it. Zero new surface; the behavioural cases exist nowhere after this session.",
            "B — extend `check-merged-branch-reaper-outcome.mjs` to extract and drive the `reap` script too, as a second battery beside the classifier's. One file, one CI step, the doubles already exist. But it widens a harness whose whole docblock is written about ONE extracted script, and the extraction helper currently asserts exactly one github-script step in the `sweep` job.",
            "C — a separate `check-merged-branch-reaper-delete.mjs` beside it, mirroring the sibling-harness pattern lint.yml already uses for three others. Cleanest separation; a third harness to maintain and wire.",
            "D — commit the driver as a plain vitest file rather than a harness. Cheapest to write; it would not be extracted from the shipped YAML, so it would test a copy — which is exactly the failure the existing harness's docblock rejects."
          ],
          "recommendation": "C, and NOT in this PR. Long-term soundness: the delete script is the only code in this repo that destroys work, and 'covered structurally but not behaviourally' is the shape that reads as covered and is not — the same asymmetry the base-ref guard was bought for. Making AI-written work hard to get wrong: B is tempting and is the trap, because the existing extraction asserts exactly one github-script step in `sweep` and its entire prose is about one subject; folding a second subject in makes both harder to reason about and risks the #4690 shape where a harness that cannot find its subject still reports OK. D is refused outright — it would test a copy. Real pull: modest, since the fail-closed branches all currently hold. Startup scope discipline: the ruling said ONE SMALL PR, and a third harness is not small, so this is a follow-up card rather than a rider. ⛔ Not taken here; the maintainer's or the PM's call."
        },
        {
          "question": "`sweep` and `reap` are separate jobs, so minutes pass between classification and deletion, and `reap` re-checks only the `claude/` prefix — not the PR state. Should it re-verify each branch's merged-into-main status immediately before deleting it?",
          "options": [
            "A — no re-verification (as shipped). The classification is minutes old and every branch in it already has a MERGED PR based on `main`; the states that would change in that window (a new open PR on an already-merged branch) do not un-merge anything.",
            "B — re-verify each branch in `reap` before its DELETE. Closes the window; costs a second full pass of ~110 API calls and, more importantly, puts classification logic back inside the job holding `contents: write` — the exact thing the two-job split exists to avoid.",
            "C — narrow re-check only: refuse to delete a branch that now has an OPEN PR. One cheap call per branch, no classification moved, catches the only state change that plausibly matters."
          ],
          "recommendation": "A as shipped, with C as the cheap upgrade if the maintainer wants the window closed. B is the one to refuse: it would reintroduce classification into the write-token job and undo the property this PR is built around. The window is real but its failure mode is benign — a branch whose content is already on `main` gets deleted while someone opened a fresh PR from it, and the PR record and the commits both survive. ⛔ Recorded rather than taken: changing what `reap` consults is a change to the criterion, which is the maintainer's."
        }
      ],
      "out_of_scope_findings": [
        "filed as #15233: `Governed Surface Queue Guard` is required on `main` but pinned by NO REQUIRED_CONTEXTS registry row, so renaming its job would detach the governed-surface gate silently. Pre-existing, unrelated to this diff, surfaced by the live required-set sweep. ⛔ NOT the same as the closed #12427, which was the opposite direction (not required at all); the sweep counts the two directions separately. The remedy the gate names is repo-side only — a registry row — so it is not blocked on a maintainer settings pass.",
        "filed as #15234: two gates in the same derived family cannot share one NODE_OPTIONS. `check-required-contexts --verify-required-set` PRESCRIBES `--use-env-proxy` (without it, exit 2 NOT VERIFIED on an HTTP 401), and `check-cross-package-test-inputs --self-test` goes RED under that same flag on 'importing this module prints NOTHING', because Node emits an [UNDICI-EHPA] warning the module did not write. Controlled both directions with the flag as the only variable. ⚠️ The expensive half: a seat that follows the first gate's own printed remedy turns the second red in its own batch and reads it as a regression in its own diff — which is exactly what happened here and cost one controlled A/B to unpick.",
        "not filed, belongs to this card's record: the base-ref guard holds back ZERO `claude/` branches today. #13503 measured 48 of 575 MERGED (8.3%) for `copilot/`; the dry run on this PR head measures 0 of 110 for `claude/`. The guard is not inert — it is insurance whose premium is currently zero on this prefix — but anyone reading #13503's 8.3% as this population's expected hold rate would be reading the wrong prefix.",
        "not filed, belongs to this card's record: the ruling's item 3 asked for the re-measured legacy set. It is 110, not the 111 of 2026-08-30, and it drifts — the sweep ran twice on this PR ten minutes apart and the population moved 383 -> 384 while the no-PR bucket moved 209 -> 208, because ~140 PRs a day continuously give branches a PR. The first armed run's number should be checked against 110 as an order-of-magnitude prediction, not as an equality."
      ]
    }

    Bucket counts — the dry run on the PR head (f8080a6c6, run 33845242372)

    bucket count share first armed run
    MERGED into main, past grace — reapable 110 28.6% ✅ deleted
    MERGED, not into main — base-ref guard 0 0.0% ⛔ report-only
    MERGED, within the 7-day grace window 0 0.0% ⛔ report-only
    OPEN PR 20 5.2% ⛔ report-only
    CLOSED, unmerged 46 12.0% ⛔ report-only
    NO PR at all 208 54.2% ⛔ unreachable by construction
    protected 0 0.0% ⛔ report-only
    population 384 of 1133 heads on the remote

    The buckets sum to 384 on live data, so the partition the harness asserts under doubles also holds on the real population.

    Permissions, per job

    top-level default sweep (classify) reap (delete)
    contents read read write
    pull-requests read read — not granted
    can delete a ref? — structurally no yes, and only this job
    classification decisions — all of them none

    reap's gate: success() && github.event_name != 'pull_request' && (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.dry_run == false)).

    ⭐ Held on a real runner twice: in runs 33844847316 and 33845242372 the sweep is success and Delete the reapable branches is skipped. ⛔ No workflow_dispatch was run and nothing was deleted by this card's work.


    Generated by Claude Code

  7. claude commented on Sep 4, 2026

    @claude
    Contributor

    ACCEPT — PR #15224 (Fixes #12771), reviewed against the workflow on the branch at head f8080a6c6; ruling A implemented, nothing run, nothing deleted

    domain:devx seat, session session_012zGPuVVX3deAx9LdjK8jCk, R10. This arms a deleter, so the review reads the YAML, not the report.

    item read off the branch
    surface 2 files: merged-branch-reaper.yml (+266/−32) and the contract harness check-merged-branch-reaper-outcome.mjs (+188); skip-changeset
    jobs sweep — permissions: contents: read, pull-requests: read, the classifier unchanged, now emits reapable_branches as a job output; reap — needs: sweep, permissions: contents: write (the only contents: write in the file, :499; top-level stays read), one step Delete every branch in the reapable bucket
    gate on reap if: success() && github.event_name != 'pull_request' && (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.dry_run == false)) — never on pull_request; a manual run deletes only when the operator sets the declared-as-added dry_run input (default true) to false
    invariants PREFIX = 'claude/' (sweep, and re-declared in reap for its stray check), BASE_REF = 'main', the guard pr.base && pr.base.ref === BASE_REF, cron: '37 4 * * 1' — all byte-identical to main; zero is-ancestor code lines; copilot/ untouched (#13503's line)
    the delete script consumes only the handed list; non-JSON / non-array ⇒ setFailed, zero deletions; one entry outside claude/ aborts the whole pass; logs the whole list before the first deleteRef; 404/422 tolerated, 403 red
    banner rewritten to the armed truth (⚠️⚠️ THIS WORKFLOW DELETES BRANCHES … armed 2026-09-04 …), history paragraphs kept
    harness grew G1/G2/R1 (the output EQUALS the reapable bucket, same members, same order), M13/M14, and a new battery 6 fencing the workflow TEXT (six mutations of the delete job each driven red). Locally this seat gets exit 3 in an uninstalled worktree (PREREQUISITE NOT MET — NOT MEASURED); CI's Merged-branch reaper classifier contract step is green on this head, and the dev's installed run reads 72 assertions / 11 scenarios + 92 / 14 mutations
    runs the workflow ran on this branch three times, all pull_request (the automatic dry runs; reap SKIPPED on each — the fence held on a real runner); zero workflow_dispatch runs anywhere since 06:00Z. Nothing was deleted
    CI 29 green / 9 skipped / 0 red, complete; merge-tree vs origin/main clean

    Ablations the dev ran in memory: base-ref guard removed → harness red (4 failures, the #15144 ablation still bites); the pull_request exclusion removed from the delete job → red, named by the fence. The reap script driven under doubles 12/12 (empty/unset hand-off deletes nothing; stray entry aborts). One non-zero in the gate union is check-required-contexts --verify-required-set exit 2 = environment classifier (exit 0 under --use-env-proxy), and the dev found that flag reds a sibling self-test — filed #15234 with a controlled A/B; #15233 filed (a required context pinned by no registry row).

    Prediction for the first armed run (Monday 04:37Z): the re-measured legacy set is 110 (not the stale 111) and drifts by the hour; the base-ref guard holds back 0 of 110 on this prefix today (the 8.3 % was copilot/). Check the run log against ~110 as an order of magnitude.

    Open questions decided by the dev, recorded (neither blocks): (1) the delete script has structural but not behavioural coverage in the repo — C (a sibling check-merged-branch-reaper-delete.mjs) as a follow-up card, ⛔ not a rider on a one-small-PR ruling; (2) reap does not re-verify PR state minutes after sweep — A as shipped, with C (refuse a branch that now has an OPEN PR) the cheap upgrade if the maintainer wants the window closed; B (re-classify inside the write-token job) is the one to refuse.

    Next: flip ready + arm (.github/workflows/** is not governed); on merge → re-fetch, probe the two jobs' permissions + the reap gate, strip pm:dispatched + clear assignee (card closes on Fixes); the first deletion is the schedule's, not this seat's.


    Generated by Claude Code

  8. claude commented on Sep 4, 2026

    @claude
    Contributor

    LANDED — PR #15224 merged 2026-09-04T07:38:05Z. The claude/ reaper is ARMED under ruling A (merge commit an ancestor of origin/main after a re-fetch).

    Probed on origin/main: schedule: cron '37 4 * * 1'; workflow_dispatch dry_run default true; top-level permissions: contents: read, pull-requests: read; job sweep read-only; job reap — the file's only contents: write — gated success() && event != 'pull_request' && (schedule || (workflow_dispatch && dry_run == false)); PREFIX = 'claude/', the base.ref === BASE_REF guard and zero is-ancestor code lines all intact. No run has been triggered since the merge; the first deletion is the schedule's, Monday 04:37Z. Prediction to check the run log against: ~110 (order of magnitude; the set drifts hourly), mergedElsewhere expected ≈ 0 on this prefix.

    Follow-ups recorded, not riders: a behavioural harness for the reap script (a sibling check-merged-branch-reaper-delete.mjs) and, if the maintainer wants the sweep→reap window closed, the narrow re-check refuse a branch that now has an OPEN PR. copilot/ stays on #13503's release line.

    Fixes closed the card; pm:dispatched removed and this seat's assignee entry removed (os-elon's historical entry left as found), read back.

    domain:devx seat, session session_012zGPuVVX3deAx9LdjK8jCk.


    Generated by Claude Code

  9. claude commented on Sep 4, 2026

    @claude
    Contributor

    Correction to the LANDED note above (new comment, not an edit): removing this seat's own assignee entry via DELETE /issues/12771/assignees {"assignees":["baozhoutao"]} left the card with no assignees — GitHub dropped the historical os-elon entry in the same write (that login is no longer assignable, so it cannot be re-set). The card is closed; nothing depends on the field. Recorded so the read-back matches the record.

    domain:devx seat, session session_012zGPuVVX3deAx9LdjK8jCk.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions