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
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -323,7 +324,7 @@ bin/fm-teardown.sh <id>

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 <fork url> && 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)
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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/<id>/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.

Expand All @@ -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 |
Expand All @@ -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
Expand All @@ -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
```

Expand Down
6 changes: 4 additions & 2 deletions bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
@@ -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: <tool> (install: <command>)", "NEEDS_GH_AUTH",
# "CREW_HARNESS_OVERRIDE: <name>", "FLEET_SYNC: <repo>: skipped: <reason>".
# 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 <tool>...
# Install the named tools (only ones the captain approved).
set -u
Expand Down
46 changes: 43 additions & 3 deletions bin/fm-fleet-sync.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
#!/usr/bin/env bash
# Refresh project clones by fast-forwarding their checked-out local default branch
# to origin/<default> when it is safe to do so.
# Refresh project clones: fast-forward the checked-out local default branch to
# origin/<default> 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 [<project-dir>]
set -eu

Expand Down Expand Up @@ -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/<default> - 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)
Expand All @@ -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")"
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bin/fm-teardown.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down