Skip to content

Commit ceb8e2c

Browse files
committed
docs: add branch-gardener design doc and wire into Milestone 2 roadmap
1 parent cbecb76 commit ceb8e2c

2 files changed

Lines changed: 254 additions & 0 deletions

File tree

ROADMAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Goal: support real hosted installations without losing task state or leaking ten
3535
- Task audit log and terminal states.
3636
- Marker-backed GitHub status comments that are edited in place per task.
3737
- Maintainer command router for status, stop, retry, explain, and approve.
38+
- **Branch Gardener** — scheduled branch hygiene skill: classify branches, delete dead ones, open draft PRs for PRless work, report to Cave. See [`docs/branch-gardener.md`](docs/branch-gardener.md).
3839

3940
## Milestone 3: GitHub Correctness
4041

docs/branch-gardener.md

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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

Comments
 (0)