From fdcebfc816639918877172cde9dc2be1c35a912f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 13:38:35 +0000 Subject: [PATCH 1/2] feature(ci): add the dispatchable branch sweep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to PyAutoBrain's `bin/branch_sweep.sh` — this repo hosts the trigger, the Brain hosts the logic (the workflow checks Brain out, so there is one implementation and no copy to drift). Why it exists: a cloud Claude session can audit branches but cannot delete a remote ref (403 for the session credential; no delete-ref call in its GitHub tool surface), so cleanup was laptop-only. A sweep on 2026-08-25 found 176 branches here, 135 of them provably contained in main, and could not act on any of it. A workflow's GITHUB_TOKEN has the `contents: write` this repo already grants dashboard_refresh.yml, so the sweep runs here instead. Dispatch with mode=audit (the default) to see the contained / unmerged / protected split in the run summary; mode=delete to act, with an optional limit for a first cautious batch. The weekly cron is audit-only and forces that mode regardless of input, so nothing is ever removed unattended. Note this is the backstop, not the fix: this repo has "Automatically delete head branches" off, which is why every merged PR head since #22 is still present. Turning that on prevents the pile-up; the sweep clears what predates it, branches pushed without a PR, and heads whose PR closed unmerged. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KwqicJpMqmcT5RVbyNwdKq --- .github/workflows/branch_sweep.yml | 100 +++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 .github/workflows/branch_sweep.yml diff --git a/.github/workflows/branch_sweep.yml b/.github/workflows/branch_sweep.yml new file mode 100644 index 000000000..b8cc6bf6e --- /dev/null +++ b/.github/workflows/branch_sweep.yml @@ -0,0 +1,100 @@ +name: Branch Sweep + +# Deletes feature branches whose content is already in `main`, and reports the +# ones it will not touch. +# +# WHY A WORKFLOW AND NOT THE SESSION. A cloud Claude session — the phone, or +# claude.ai/code — can audit branches perfectly well but cannot remove one: +# `git push origin --delete` returns 403 for the session credential, and the +# GitHub tool surface those sessions get has no delete-ref call at all. So +# branch cleanup was a laptop-only chore, and it showed: 233 branches across +# Mind and Brain by 2026-08-25, 188 of them provably spent, because nothing +# deletes a merged head automatically. +# +# A workflow's GITHUB_TOKEN is a *different* credential, and this repo already +# trusts it with `contents: write` (dashboard_refresh.yml commits to main with +# it). Running the sweep here means any surface that can dispatch a workflow +# can drive the cleanup — including a chat on a phone. +# +# THIS IS THE BACKSTOP, NOT THE FIX. The primary fix is the repo setting +# Settings → General → "Automatically delete head branches", which removes each +# PR head at merge and prevents the pile-up in the first place. This workflow +# exists for the backlog that predates it, for branches pushed without a PR, +# and for heads whose PR was closed unmerged. +# +# The safety gates (never `main`, never `archive/condemned/*` Gut transit refs, +# never an open PR's head, never a branch git cannot prove is contained) live +# in the script, not here — see PyAutoBrain/bin/branch_sweep.sh and +# PyAutoBrain/skills/repo_cleanup/SKILL.md. + +on: + workflow_dispatch: + inputs: + mode: + description: "audit = report only · delete = actually remove" + type: choice + options: [audit, delete] + default: audit + limit: + description: "Max branches to delete (0 = no cap). Ignored in audit mode." + type: string + default: "0" + schedule: + # Weekly, audit-only: keeps the backlog visible without ever acting + # unattended. Deletion always requires someone to dispatch it. + - cron: "10 4 * * 0" + +permissions: + contents: write + pull-requests: read + +concurrency: + group: branch-sweep-${{ github.repository }} + cancel-in-progress: false + +jobs: + sweep: + runs-on: ubuntu-latest + steps: + - name: Check out this repo (full history) + uses: actions/checkout@v4 + with: + # Containment is an ancestry question: on a shallow clone every + # branch looks unmerged, so the sweep would protect everything and + # quietly do nothing. The script re-checks and deepens if needed. + fetch-depth: 0 + + - name: Check out PyAutoBrain (the sweep logic lives there) + uses: actions/checkout@v4 + with: + repository: PyAutoLabs/PyAutoBrain + path: .brain + fetch-depth: 1 + + - name: Sweep + env: + GH_TOKEN: ${{ github.token }} + run: | + set -o pipefail + # A scheduled run never deletes, whatever anyone edits into the cron. + mode='${{ inputs.mode }}' + if [ '${{ github.event_name }}' != 'workflow_dispatch' ]; then + mode=audit + fi + mode="${mode:-audit}" + + .brain/bin/branch_sweep.sh \ + --repo "$GITHUB_WORKSPACE" \ + --owner '${{ github.repository_owner }}' \ + --name '${{ github.event.repository.name }}' \ + --mode "$mode" \ + --limit '${{ inputs.limit || 0 }}' 2>&1 | tee sweep.log + + # The run summary is the readable surface on a phone. + { + echo "## Branch sweep — \`$mode\`" + echo + echo '```' + cat sweep.log + echo '```' + } >> "$GITHUB_STEP_SUMMARY" From 9c17c35bd4d74ab416cb7c66deb48e07841e14ab Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 13:48:25 +0000 Subject: [PATCH 2/2] fix(spawn): classify branch_sweep.yml as instance automation (rule 9c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PR-time spawn contract test failed on the new workflow: every tracked file must match a MIND_RULES entry, so spawn can decide whether it travels into the fresh-slate template. `branch_sweep.yml` matched nothing. DROP, on both of rule 9c's existing grounds. It checks out PyAutoLabs/PyAutoBrain for the sweep logic — dashboard_refresh.yml's failure mode, where owner substitution yields the literal YOURORG/PyAutoBrain and the run dies on checkout — and it carries a weekly cron, which rule 9's no-unattended-trigger condition rejects independently. A fresh org would inherit a scheduled job that fails every Sunday and cannot be made to work until it has a Brain. The sweep is worth having in a mature organism; it is worth re-adding deliberately rather than by inheritance. Spec table extended first, then mirrored into MIND_RULES, in the order the assertion message asks for. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KwqicJpMqmcT5RVbyNwdKq --- docs/pyautobrain/spawn_spec.md | 2 +- scripts/spawn.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/pyautobrain/spawn_spec.md b/docs/pyautobrain/spawn_spec.md index 879662df3..5fab491e6 100644 --- a/docs/pyautobrain/spawn_spec.md +++ b/docs/pyautobrain/spawn_spec.md @@ -49,7 +49,7 @@ deliberately, never silently shipped into a template. | 9 | `.github/**` | **Per file, by the succeed-on-a-fresh-repo test below.** Not a blanket rule: owner substitution alone does NOT make a workflow work, because `YOURORG` is a literal placeholder — the template's own `spawn_drift` run failed `repository 'https://github.com/YOURORG/PyAutoMind/' not found`. See rules 9a–9d | | 9a | `.github/workflows/lifecycle_drift.yml` | KEEP verbatim — operates only on its own repo (checkout + local scripts) and contains no owner reference at all, so it needs no substitution and succeeds unmodified in a fresh org. Empirically the one green workflow in the template's run history | | 9b | `.github/workflows/spawn_drift.yml` | DROP — was "keep with the `schedule:` stripped", revised in #125. The self-heal added there makes this workflow depend on `secrets.PAT_PYAUTOLABS` AND on published `*-template` repos, neither of which a freshly-spawned org has, so **every** path in it is unrunnable there and the secret reference alone breaks the no-configured-secret condition. "When in doubt DROP" applies: an org that later publishes templates can adopt this workflow deliberately, having read it. The template still ships `scripts/spawn.py` + `tests/`, so the generator and its guards travel; only the org-coupled automation does not | -| 9c | `.github/workflows/{dashboard_refresh,registry_reconcile,morning_status,morning_health,arxiv_papers,firewall_gate,pages_dashboard}.yml`, `.github/scripts/**` | DROP — instance automation. `dashboard_refresh.yml` checks out `PyAutoLabs/PyAutoBrain` (the dashboard renderer lives with the intake conductor, not in Mind), so it fails on checkout in any org that has no such sibling — and owner substitution only turns that into the literal `YOURORG/PyAutoBrain`. The rest hardcode sibling repo lists, organ-specific workflow names (`PyAutoHeart`/`PyAutoBrain`/`PyAutoHands`), org secrets (`PYAUTO_PAPERS_WEBHOOK_URL`, `CLAUDE_CODE_OAUTH_TOKEN`) and, in `arxiv_fetch.py`, strong-lensing search vocabulary plus dated incident notes. All 13 failing runs in the published template came from these. Two later additions join them (2026-08, first caught by the 2026-08-24 drift run): `firewall_gate.yml` checks out `PyAutoLabs/{PyAutoBrain,PyAutoHeart,PyAutoHands}` by name — `dashboard_refresh.yml`'s failure mode three times over; and `pages_dashboard.yml` needs a GitHub Pages site the default token cannot create on a fresh repo (the Hands lesson already recorded for Memory's `knowledge_board.yml`) and takes `pages: write` + `id-token: write` | +| 9c | `.github/workflows/{dashboard_refresh,registry_reconcile,morning_status,morning_health,arxiv_papers,firewall_gate,pages_dashboard,branch_sweep}.yml`, `.github/scripts/**` | DROP — instance automation. `dashboard_refresh.yml` checks out `PyAutoLabs/PyAutoBrain` (the dashboard renderer lives with the intake conductor, not in Mind), so it fails on checkout in any org that has no such sibling — and owner substitution only turns that into the literal `YOURORG/PyAutoBrain`. The rest hardcode sibling repo lists, organ-specific workflow names (`PyAutoHeart`/`PyAutoBrain`/`PyAutoHands`), org secrets (`PYAUTO_PAPERS_WEBHOOK_URL`, `CLAUDE_CODE_OAUTH_TOKEN`) and, in `arxiv_fetch.py`, strong-lensing search vocabulary plus dated incident notes. All 13 failing runs in the published template came from these. Two later additions join them (2026-08, first caught by the 2026-08-24 drift run): `firewall_gate.yml` checks out `PyAutoLabs/{PyAutoBrain,PyAutoHeart,PyAutoHands}` by name — `dashboard_refresh.yml`'s failure mode three times over; and `pages_dashboard.yml` needs a GitHub Pages site the default token cannot create on a fresh repo (the Hands lesson already recorded for Memory's `knowledge_board.yml`) and takes `pages: write` + `id-token: write`. A third joins them (2026-08-25): `branch_sweep.yml` checks out `PyAutoLabs/PyAutoBrain` for the sweep logic — `dashboard_refresh.yml`'s failure mode again — and carries a weekly cron, so it would also trip rule 9's no-unattended-trigger condition on arrival. The sweep is worth having in a mature organism and worth re-adding deliberately; it is not worth a fresh org inheriting a scheduled job that fails on checkout every Sunday | | 9d | any other `.github/**` | **No catch-all rule — UNMATCHED by design.** A fallback here is fail-*open*: a workflow added to Mind later would ride it into the template carrying whatever schedule and secrets it has, which is precisely the defect 9a–9c fix. A new `.github` file must fail the run and get an explicit entry above, like every other new file class | | 10 | `.claude/**`, `.codex/**` | DROP — agent-discovery symlinks are install artifacts recreated by the PyAutoBrain installer, not source content | diff --git a/scripts/spawn.py b/scripts/spawn.py index 0be8df830..fc429e878 100644 --- a/scripts/spawn.py +++ b/scripts/spawn.py @@ -142,6 +142,13 @@ # so it is unrunnable on arrival. scripts/ and the renderer still travel; # an adopter re-adds the publisher deliberately. (".github/workflows/pages_dashboard.yml", "DROP"), + # 9c also: the branch sweep checks out PyAutoLabs/PyAutoBrain for its logic + # (dashboard_refresh.yml's failure mode again — YOURORG/PyAutoBrain does not + # exist), and carries a weekly cron, which rule 9's no-unattended-trigger + # condition rejects on its own. Worth re-adding deliberately once an + # adopter has a Brain; not worth inheriting a scheduled job that fails on + # checkout every Sunday. + (".github/workflows/branch_sweep.yml", "DROP"), (".github/scripts/*", "DROP"), # NO `.github/*` catch-all, deliberately. A catch-all is fail-OPEN: a new # Mind workflow would ride it into the template carrying whatever schedule