diff --git a/AGENTS.md b/AGENTS.md index 9f5f6e16192..8e3d1cb5e2a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,8 +19,8 @@ Hard rules, in priority order: 1. **Never write to a project.** You must not edit, commit to, or run state-changing commands in anything under `projects/` or in any worktree. You read projects to understand them; crewmates change them. - Three sanctioned exceptions: tool-driven project initialization (section 6), clean fast-forwarding a clone's local default branch to match `origin` via `bin/fm-fleet-sync.sh`, and the approved local merge for a `local-only` project, which firstmate performs with `bin/fm-merge-local.sh` once the captain approves (section 7). - The fleet sync exception advances only the checked-out local default branch and never forces, creates merge commits, stashes, or changes treehouse worktrees. + Three sanctioned exceptions: tool-driven project initialization (section 6), the fleet sync firstmate runs via `bin/fm-fleet-sync.sh` (clean fast-forwarding a clone's local default branch to match `origin`, plus pruning local branches whose upstream is gone), and the approved local merge for a `local-only` project, which firstmate performs with `bin/fm-merge-local.sh` once the captain approves (section 7). + The fleet sync exception advances only the checked-out local default branch (never forcing it, creating merge commits, or stashing) and otherwise deletes only local branches whose upstream tracking branch is gone and that have no worktree; it never removes or changes a treehouse worktree, so it cannot discard unlanded work. 2. **Never merge a PR without the captain's explicit word.** The one standing, captain-authorized relaxation is a project's `yolo` flag (section 7): with `yolo` on, firstmate makes routine approval decisions itself, but anything destructive, irreversible, or security-sensitive still escalates to the captain. 3. **Never tear down a worktree that holds unlanded work.** @@ -53,7 +53,7 @@ README.md public overview and development notes .github/workflows/ shared CI and PR enforcement, committed .agents/skills/ shared skills, committed .claude/skills symlink to .agents/skills for claude compatibility -bin/ helper scripts, committed, including fm-fleet-sync.sh for clean default-branch refreshes; read each script's header before first use +bin/ helper scripts, committed, including fm-fleet-sync.sh for clean default-branch refreshes and gone-branch pruning; read each script's header before first use config/crew-harness crewmate harness override; LOCAL, gitignored; absent or "default" = same as firstmate data/ personal fleet records; LOCAL, gitignored as a whole backlog.md task queue, dependencies, history @@ -81,7 +81,8 @@ Bootstrap is detect, then consent, then install. Never install anything the captain has not approved in this session. Run `bin/fm-bootstrap.sh`. -Bootstrap also refreshes the fleet via `bin/fm-fleet-sync.sh`: it fetches each remote-backed clone and clean-fast-forwards its local default branch when safe, best-effort and non-fatal. +Bootstrap also refreshes the fleet via `bin/fm-fleet-sync.sh`: it fetches each remote-backed clone, clean-fast-forwards its local default branch when safe, and prunes local branches whose upstream is gone and that no worktree still needs, best-effort and non-fatal. +Set `FM_FLEET_PRUNE=0` to temporarily disable that branch pruning. Silence means all good: say nothing and move on. Otherwise it prints one line per problem; handle each: @@ -323,7 +324,7 @@ bin/fm-teardown.sh The script refuses if the worktree holds unpushed work; treat a refusal as a stop-and-investigate, not an obstacle. Known benign case: after an external-PR task, a squash merge leaves the branch commits reachable only on the contributor's fork; add the fork as a remote and fetch (`git remote add fork && git fetch fork`), then retry - never reach for `--force`. -After a successful PR-based teardown, it also runs `bin/fm-fleet-sync.sh` for that project, best-effort, so the clone's local default catches up to the merge immediately. +After a successful PR-based teardown, it also runs `bin/fm-fleet-sync.sh` for that project, best-effort, so the clone's local default catches up to the merge and the just-merged branch, now gone on the remote and free of its worktree, is pruned immediately. Then move the task to Done in `data/backlog.md` (with the full `https://...` PR URL or local merge note and date), re-evaluate the queue, and dispatch anything that was blocked on this task. ### Scout tasks (report instead of PR) diff --git a/README.md b/README.md index 8ed1d448c1c..8bb679dfad2 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ There is no app to install; the whole orchestrator is an `AGENTS.md` file that a The first mate dispatches, supervises, escalates only real decisions, and reports plain outcomes about work that is ready, blocked, or needs your call. - **A visible crew** - every crewmate lives in a tmux window. Watch any of them work, or type into their window to intervene; the first mate reconciles. -- **Guarded by construction** - the first mate is read-only over your projects except for clean local default-branch refreshes and approved `local-only` fast-forward merges; crewmates work in disposable [treehouse](https://github.com/kunchenguid/treehouse) worktrees. +- **Guarded by construction** - the first mate is read-only over your projects except for clean local default-branch refreshes, safe pruning of local branches whose remote is gone, and approved `local-only` fast-forward merges; crewmates work in disposable [treehouse](https://github.com/kunchenguid/treehouse) worktrees. Ship tasks follow each project's delivery mode, and scout tasks produce local reports without pushing anything. This is not an agent harness. This is not a skill. This is not a CLI. @@ -117,7 +117,7 @@ firstmate works from any terminal - outside tmux, crewmates land in a detached ` - **Two task shapes** - ship tasks change projects and ship by project mode (`no-mistakes`, `direct-PR`, or `local-only`); scout tasks investigate, plan, reproduce bugs, or audit, then leave a report at `data//report.md` and never push. - **Project modes are explicit** - `data/projects.md` records each project's delivery mode and optional `+yolo` autonomy flag. `no-mistakes` projects run the full validation pipeline, `direct-PR` projects open PRs without that pipeline, and `local-only` projects stay local until firstmate performs an approved fast-forward merge. -- **Local clones stay fresh** - bootstrap and PR-based teardown refresh remote-backed project clones with clean default-branch fast-forwards when the clone is on the default branch and has no local work. +- **Local clones stay fresh** - bootstrap and PR-based teardown refresh remote-backed project clones with clean default-branch fast-forwards when the clone is on the default branch and has no local work, and prune local branches whose remote is gone and that no worktree still needs. - **Restart-proof** - all state lives in tmux, status files, and local markdown under `data/`. Kill the first mate session anytime; the next one reconciles and carries on. @@ -128,7 +128,7 @@ The first mate drives these; you rarely need to, but they work by hand too. | Script | Description | | ----------------- | ------------------------------------------------------------------------------------------- | | `fm-bootstrap.sh` | Detect missing toolchain pieces; refresh clones best-effort; install tools only after consent | -| `fm-fleet-sync.sh` | Fetch clones and clean-fast-forward their checked-out local default branches when safe | +| `fm-fleet-sync.sh` | Fetch clones, clean-fast-forward their checked-out default branches, and safely prune branches whose remote is gone | | `fm-brief.sh` | Scaffold a ship brief, or a report-only scout brief with `--scout` | | `fm-guard.sh` | Warn when tasks are in flight but the watcher liveness beacon is stale or missing | | `fm-spawn.sh` | Window → treehouse worktree → agent launched with its brief; records ship/scout task kind | @@ -150,7 +150,7 @@ The shared orchestrator behavior lives in `AGENTS.md` - edit it like any prompt Personal preferences for one captain's fleet live locally in `data/captain.md`; it is gitignored and read after `data/projects.md` during bootstrap. Harness support is a table in section 4: claude, codex, opencode, and pi are all empirically verified; new harnesses get verified through a supervised trial task before joining the table. -Watcher tuning via environment variables (defaults shown): +Runtime tuning via environment variables (defaults shown): ```sh FM_POLL=15 # seconds between watcher cycles @@ -161,6 +161,7 @@ FM_CHECK_TIMEOUT=30 # seconds allowed per slow check script FM_GUARD_GRACE=300 # seconds a stale watcher beacon may age before guard warnings FM_SIGNAL_GRACE=30 # seconds to coalesce nearby status and turn-end signals into one wake FM_FLEET_SYNC_BOOTSTRAP_TIMEOUT=20 # seconds allowed for bootstrap's best-effort clone refresh +FM_FLEET_PRUNE=1 # set to 0 to skip pruning local branches whose upstream is gone FM_BUSY_REGEX='esc (to )?interrupt|Working\.\.\.' # busy-pane signatures, extend per harness ``` diff --git a/bin/fm-bootstrap.sh b/bin/fm-bootstrap.sh index 0fd457966e1..621a15a1432 100755 --- a/bin/fm-bootstrap.sh +++ b/bin/fm-bootstrap.sh @@ -1,10 +1,12 @@ #!/usr/bin/env bash -# Bootstrap detection, best-effort fleet refresh, and installs. +# Bootstrap detection, best-effort fleet refresh/prune, and installs. # Usage: fm-bootstrap.sh # Detect: prints one line per problem and exits 0. Silent = all good. # Lines: "MISSING: (install: )", "NEEDS_GH_AUTH", # "CREW_HARNESS_OVERRIDE: ", "FLEET_SYNC: : skipped: ". -# The fleet refresh is bounded by FM_FLEET_SYNC_BOOTSTRAP_TIMEOUT, default 20s. +# Fleet sync fetches, fast-forwards, and prunes gone local branches; +# it is bounded by FM_FLEET_SYNC_BOOTSTRAP_TIMEOUT, default 20s. +# Set FM_FLEET_PRUNE=0 to skip branch pruning during that refresh. # fm-bootstrap.sh install ... # Install the named tools (only ones the captain approved). set -u diff --git a/bin/fm-fleet-sync.sh b/bin/fm-fleet-sync.sh index c0fe73af4fc..8a824929ba6 100755 --- a/bin/fm-fleet-sync.sh +++ b/bin/fm-fleet-sync.sh @@ -1,8 +1,12 @@ #!/usr/bin/env bash -# Refresh project clones by fast-forwarding their checked-out local default branch -# to origin/ when it is safe to do so. +# Refresh project clones: fast-forward the checked-out local default branch to +# origin/ when safe, and prune local branches whose upstream tracking +# branch is gone (the remote branch was deleted, i.e. its PR merged) and that no +# worktree still needs. # Skips local-only/no-origin projects, dirty clones, non-default checkouts, # diverged branches, and fetch/fast-forward failures without forcing or stashing. +# Pruning never deletes the checked-out branch or a branch that still has a +# worktree, so it cannot discard unlanded work; set FM_FLEET_PRUNE=0 to disable it. # Usage: fm-fleet-sync.sh [] set -eu @@ -47,6 +51,40 @@ first_line() { printf '%s\n' "$1" | sed -n '1s/[[:space:]]\{1,\}/ /g;1p' } +prune_gone_branches() { + # Delete local branches whose upstream tracking branch is gone - the remote + # branch was deleted, which in this fleet means its PR merged - as long as + # nothing still needs them. Never the checked-out branch, and never a branch + # that still has a worktree (a live or not-yet-torn-down task). "Gone" plus + # "no worktree" already proves the work landed: teardown removes a branch's + # worktree only after confirming the work reached the remote. We deliberately + # do NOT also require the branch to be an ancestor of origin/ - PRs in + # this fleet are squash-merged, so a merged branch is never an ancestor and + # such a check would prune nothing. The no-worktree guard is the real safety + # net. Set FM_FLEET_PRUNE=0 to skip pruning entirely. + [ "${FM_FLEET_PRUNE:-1}" != "0" ] || return 0 + + local worktree_branches current refline branch track + worktree_branches=$(git -C "$PROJ" worktree list --porcelain 2>/dev/null \ + | sed -n 's#^branch refs/heads/##p') + current=$(git -C "$PROJ" symbolic-ref --quiet --short HEAD 2>/dev/null || true) + + while IFS= read -r refline; do + branch=${refline%% *} + track=${refline#* } + [ "$track" = "[gone]" ] || continue + [ -n "$branch" ] || continue + [ "$branch" != "$current" ] || continue + if printf '%s\n' "$worktree_branches" | grep -Fxq -- "$branch"; then + continue + fi + if git -C "$PROJ" branch -D -- "$branch" >/dev/null 2>&1; then + echo "$label: pruned $branch" + fi + done < <(git -C "$PROJ" for-each-ref \ + --format='%(refname:short) %(upstream:track)' refs/heads 2>/dev/null) +} + sync_project() { PROJ=$1 label=$(project_label) @@ -70,7 +108,7 @@ sync_project() { return 0 fi - if ! fetch_output=$(git -C "$PROJ" fetch origin --quiet 2>&1); then + if ! fetch_output=$(git -C "$PROJ" fetch origin --prune --quiet 2>&1); then reason="fetch failed" if [ -n "$fetch_output" ]; then reason="$reason: $(first_line "$fetch_output")" @@ -79,6 +117,8 @@ sync_project() { return 0 fi + prune_gone_branches || true + DEFAULT=$(default_branch) || { echo "$label: skipped: cannot determine default branch" return 0 diff --git a/bin/fm-teardown.sh b/bin/fm-teardown.sh index 269db282935..17a6583aafb 100755 --- a/bin/fm-teardown.sh +++ b/bin/fm-teardown.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Tear down a finished task: return the treehouse worktree, kill the tmux window, -# clear volatile state, then refresh the project's clone for PR-based ship tasks. +# clear volatile state, then refresh/prune the project's clone for PR-based ship tasks. # REFUSES if the worktree holds work not on any remote, because treehouse return # hard-resets the worktree and kills its processes. # Scout tasks (kind=scout in meta) carve out of that check: their worktree is