|
| 1 | +# Branch Gardener |
| 2 | + |
| 3 | +`branch-gardener` is a built-in skill for `coven-github` that keeps a repository's |
| 4 | +branch list tidy on a schedule. |
| 5 | + |
| 6 | +It automates the pattern of: scan every branch → classify it → merge PRless active |
| 7 | +work → delete merged or dead branches → report to Cave. The gardener never deletes |
| 8 | +without evidence and never merges without a PR. |
| 9 | + |
| 10 | +--- |
| 11 | + |
| 12 | +## Problem |
| 13 | + |
| 14 | +Active OpenCoven repositories accumulate branches rapidly. Codex agents, familiar |
| 15 | +worktrees, and exploratory work all leave behind branches at different life stages: |
| 16 | + |
| 17 | +- **Active** — unmerged commits, possibly a PR open, work in progress |
| 18 | +- **PRless** — unmerged commits, no PR yet; waiting to be surfaced |
| 19 | +- **Dead** — zero commits ahead of main; merged or abandoned |
| 20 | +- **Scratch** — detached worktrees, `/tmp` checkouts, no associated branch |
| 21 | + |
| 22 | +Without a regular sweep these pile up into dozens of stale branches that make `git |
| 23 | +branch -a` noisy and obscure what is actually in flight. |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## What the Gardener Does |
| 28 | + |
| 29 | +Each run follows four steps in order: |
| 30 | + |
| 31 | +``` |
| 32 | +1. Scan — list all remote branches and classify each one |
| 33 | +2. Prune — delete branches with 0 commits ahead of default branch |
| 34 | +3. Surface — open draft PRs for active branches that have no PR yet |
| 35 | +4. Report — post a Cave Board task card summarising the run |
| 36 | +``` |
| 37 | + |
| 38 | +The gardener never takes a destructive action on a branch with unmerged commits. |
| 39 | +It only opens PRs (proposing review) and deletes branches that are definitively |
| 40 | +dead (0 commits ahead). |
| 41 | + |
| 42 | +--- |
| 43 | + |
| 44 | +## Branch Classification |
| 45 | + |
| 46 | +| Class | Condition | Action | |
| 47 | +|------------|-------------------------------------------------|------------------| |
| 48 | +| `dead` | 0 commits ahead of default branch | auto-delete | |
| 49 | +| `prless` | ≥1 commits ahead, no open PR | open draft PR | |
| 50 | +| `active` | ≥1 commits ahead, PR open | no action | |
| 51 | +| `merged` | PR merged, branch not yet deleted | auto-delete | |
| 52 | + |
| 53 | +Merged branches are detected by checking PR state against the branch ref; GitHub |
| 54 | +normally deletes them on merge when "delete branch on merge" is enabled, but many |
| 55 | +repos leave this off. |
| 56 | + |
| 57 | +--- |
| 58 | + |
| 59 | +## Autonomy Tiers |
| 60 | + |
| 61 | +The gardener operates in one of three tiers, set per installation in the familiar |
| 62 | +config TOML: |
| 63 | + |
| 64 | +```toml |
| 65 | +[gardener] |
| 66 | +autonomy = "propose" # propose | prune-dead | full |
| 67 | +schedule = "0 4 * * *" # cron — default: 04:00 UTC daily |
| 68 | +``` |
| 69 | + |
| 70 | +| Tier | Prune dead | Open PRs | Cave approval before delete | |
| 71 | +|---------------|------------|----------|------------------------------| |
| 72 | +| `propose` | ❌ | ✅ | n/a — read-only + PR opening | |
| 73 | +| `prune-dead` | ✅ | ✅ | ❌ — dead branches deleted immediately | |
| 74 | +| `full` | ✅ | ✅ | ✅ — Cave card, human approves | |
| 75 | + |
| 76 | +`propose` is the safe default for new installations. Operators promote to |
| 77 | +`prune-dead` once they trust the classification logic against their workflow. |
| 78 | + |
| 79 | +--- |
| 80 | + |
| 81 | +## Architecture |
| 82 | + |
| 83 | +### New crate: `crates/gardener` |
| 84 | + |
| 85 | +``` |
| 86 | +crates/gardener/ |
| 87 | + src/ |
| 88 | + lib.rs — public API: GardenerConfig, GardenerRun, run() |
| 89 | + scan.rs — BranchRecord, BranchClass, scan_repo() |
| 90 | + prune.rs — delete_branch() with pre-delete re-check |
| 91 | + surface.rs — open_draft_pr() with commit subject as title |
| 92 | + report.rs — CaveReport builder, task card payload |
| 93 | + schedule.rs — parse cron schedule, emit next-run metadata |
| 94 | + tests/ |
| 95 | + scan_test.rs |
| 96 | + prune_test.rs |
| 97 | + surface_test.rs |
| 98 | +``` |
| 99 | + |
| 100 | +### Trigger |
| 101 | + |
| 102 | +Branch gardener runs are triggered in two ways: |
| 103 | + |
| 104 | +**Scheduled (primary)** — the coven-github worker fleet runs a cron job per |
| 105 | +installation using the configured schedule. The worker calls `gardener::run()` |
| 106 | +with the installation's GitHub token and config. |
| 107 | + |
| 108 | +**On-demand (secondary)** — a maintainer posts `/coven garden` in any issue or PR |
| 109 | +comment. The webhook router recognises this as a `gardener` command and enqueues a |
| 110 | +one-shot run. |
| 111 | + |
| 112 | +```mermaid |
| 113 | +flowchart LR |
| 114 | + cron[Cron trigger] |
| 115 | + command[Maintainer: /coven garden] |
| 116 | + webhook[Webhook router] |
| 117 | + queue[Task queue] |
| 118 | + worker[Gardener worker] |
| 119 | + github[GitHub API] |
| 120 | + cave[Cave Board task card] |
| 121 | +
|
| 122 | + cron --> queue |
| 123 | + command --> webhook --> queue |
| 124 | + queue --> worker |
| 125 | + worker --> github |
| 126 | + worker --> cave |
| 127 | +``` |
| 128 | + |
| 129 | +### GitHub API calls per run |
| 130 | + |
| 131 | +| Step | API call | Auth scope needed | |
| 132 | +|---------|---------------------------------------------------|------------------------| |
| 133 | +| Scan | `GET /repos/{owner}/{repo}/branches` | `contents:read` | |
| 134 | +| Scan | `GET /repos/{owner}/{repo}/pulls?head=...` | `pull_requests:read` | |
| 135 | +| Prune | `DELETE /repos/{owner}/{repo}/git/refs/heads/{b}` | `contents:write` | |
| 136 | +| Surface | `POST /repos/{owner}/{repo}/pulls` | `pull_requests:write` | |
| 137 | +| Report | Cave Board API (internal) | Cave task token | |
| 138 | + |
| 139 | +The gardener re-fetches the branch ref immediately before any delete to guard |
| 140 | +against races (branch pushed to between scan and delete). |
| 141 | + |
| 142 | +--- |
| 143 | + |
| 144 | +## Familiar Config TOML |
| 145 | + |
| 146 | +```toml |
| 147 | +[repositories."OpenCoven/coven-cave"] |
| 148 | +familiar = "nova" |
| 149 | +trigger_labels = ["coven:fix", "coven:review"] |
| 150 | + |
| 151 | +[repositories."OpenCoven/coven-cave".gardener] |
| 152 | +enabled = true |
| 153 | +autonomy = "prune-dead" |
| 154 | +schedule = "0 4 * * *" |
| 155 | +base = "main" # default branch override (optional) |
| 156 | +exclude = [ # branches to never touch |
| 157 | + "release/*", |
| 158 | + "hotfix/*", |
| 159 | +] |
| 160 | +draft_pr_label = "coven:garden" # label applied to gardener-opened PRs |
| 161 | +``` |
| 162 | + |
| 163 | +--- |
| 164 | + |
| 165 | +## Cave Report Card |
| 166 | + |
| 167 | +After each run the gardener posts a Cave Board task card. It is edited in place on |
| 168 | +subsequent runs for the same installation + repo (one card per repo, not one per |
| 169 | +run). |
| 170 | + |
| 171 | +``` |
| 172 | +🌿 Branch Gardener — OpenCoven/coven-cave |
| 173 | +Run: 2026-06-20 04:00 UTC |
| 174 | +
|
| 175 | +Pruned (dead, 0 ahead): 3 branches deleted |
| 176 | +Surfaced (PRs opened): 2 draft PRs opened |
| 177 | +Active (no action): 4 branches, PRs open |
| 178 | +Skipped (excluded): 0 |
| 179 | +
|
| 180 | +Next run: 2026-06-21 04:00 UTC |
| 181 | +Trigger: /coven garden to run now |
| 182 | +``` |
| 183 | + |
| 184 | +Pruned and surfaced branches are listed with links. If the run produced no changes |
| 185 | +the card notes "nothing to do" without spamming. |
| 186 | + |
| 187 | +--- |
| 188 | + |
| 189 | +## Safety Constraints |
| 190 | + |
| 191 | +- **Never delete a branch with ≥1 unmerged commit** regardless of autonomy tier. |
| 192 | +- **Re-check before delete** — fetch the branch ref again immediately before the |
| 193 | + API delete call. If it gained commits since the scan, skip it and log a warning. |
| 194 | +- **Exclude list** — `release/*`, `hotfix/*`, and any pattern in `gardener.exclude` |
| 195 | + are never touched, not even for PR opening. |
| 196 | +- **Bot self-loop guard** — do not open a PR if the branch's only commits are from |
| 197 | + the coven-github bot user. |
| 198 | +- **Cave card, not comment spam** — one edited card per repo, not a comment per |
| 199 | + branch. Humans can mute the cave card without losing GitHub noise. |
| 200 | +- **Dry-run mode** — `autonomy = "propose"` is effectively a dry run for deletes; |
| 201 | + the report lists what would have been pruned without doing it. |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +## Roadmap Placement |
| 206 | + |
| 207 | +Branch gardener fits into **Milestone 2: Hosted Control Plane** from `ROADMAP.md`. |
| 208 | + |
| 209 | +It depends on: |
| 210 | +- Persistent task store (for run history and the edited Cave card) |
| 211 | +- Installation-scoped familiar routing (for per-repo gardener config) |
| 212 | +- Cave Board task API (for the report card) |
| 213 | + |
| 214 | +It is a good **Milestone 2 capstone**: it exercises the full task lifecycle, the |
| 215 | +Cave Board integration, and the maintainer command router (`/coven garden`) in a |
| 216 | +low-risk, high-value workflow that every team immediately benefits from. |
| 217 | + |
| 218 | +--- |
| 219 | + |
| 220 | +## Implementation Plan |
| 221 | + |
| 222 | +### Phase A — Core classifier (no side effects) |
| 223 | + |
| 224 | +1. Add `crates/gardener` crate with `scan.rs` and `BranchRecord`/`BranchClass` |
| 225 | +2. Implement `scan_repo()` against the GitHub REST API |
| 226 | +3. Unit tests with fixture JSON (no live API calls) |
| 227 | +4. Wire `BranchClass` output to a structured log — visible without any mutation |
| 228 | + |
| 229 | +### Phase B — Prune and surface |
| 230 | + |
| 231 | +5. Implement `prune.rs`: delete dead branches with pre-delete re-check |
| 232 | +6. Implement `surface.rs`: open draft PRs with `coven:garden` label |
| 233 | +7. Respect autonomy tier and exclude list |
| 234 | +8. Integration tests against a test repo |
| 235 | + |
| 236 | +### Phase C — Schedule and trigger |
| 237 | + |
| 238 | +9. Add cron schedule parsing in `schedule.rs` |
| 239 | +10. Add `/coven garden` command to the webhook router's command table |
| 240 | +11. Enqueue gardener runs through the existing task queue |
| 241 | + |
| 242 | +### Phase D — Cave reporting |
| 243 | + |
| 244 | +12. Implement `report.rs`: CaveReport builder, edited-in-place card |
| 245 | +13. Connect to Cave Board task API |
| 246 | +14. Surface run history in Cave (last 5 runs per repo) |
| 247 | + |
| 248 | +### Phase E — Config and docs |
| 249 | + |
| 250 | +15. Add `[gardener]` block to the familiar config TOML schema |
| 251 | +16. Validation: warn on unknown schedule, invalid autonomy tier, bad exclude globs |
| 252 | +17. Update self-hosting docs with gardener setup instructions |
| 253 | +18. Update `ROADMAP.md` to mark branch-gardener under Milestone 2 |
0 commit comments