Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions .github/workflows/branch_sweep.yml
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion docs/pyautobrain/spawn_spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down
7 changes: 7 additions & 0 deletions scripts/spawn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading