You keep one main repo open — dev server running, editor attached — and you use it to run and verify work that your agents produce. Each agent works in its own git worktree on its own feature branch, so several can run at once without stepping on each other.
The trouble starts the moment you want to try an agent's work. To run it in your main repo you check out its branch — and git refuses: a branch can't be checked out in two worktrees at once. So you detach, or juggle copies. Then the loop tightens: you spot a fix and make it on main, but you can't commit it onto the agent's branch. You want to pull the agent's latest while keeping your own half-finished edit on top, but the agent made more changes and now the two have diverged and you're resolving merge conflicts — and this is happening across two or three agents at once, so you're also switching main between branches, losing track of which stash was for which feature.
Managing all of this is possible with git — stashes, pulling, merging, checking out from a branch. It's just constant friction around a simple intent: hand work back and forth between a worktree and main. You may have many branches, but for any given branch, the work is linear. Our tools should embrace that.
faa is a single bash script that simplifies this dance.
I made it specifically optimized for my workflow with OpenCode and weak local agents. It is likely not a good fit for most people working with agents.
- I do not trust the model to decide what to commit. I don't let it touch git at all. I made this under the assumption that all git handling is managed manually. If your agents are smart enough, maybe you can just let them do their thing.
- The task's success is not easily testable. The model can fail to implement simple features or bug fixes. Even when it is as simple as "make the button bigger", I want to visually run the program and make sure it behaves as I want it to behave.
- Running the dev server is costly - long install times, limited available ports, slow start ups. If you prefer to only run one dev server, this might be for you.
- If you're developing on one branch at the time,
faawill only make things more complicated.
For a feature branch (say add-button), main can't sit on add-button while the
worktree holds it. So main works on a per-feature mirror branch faa-add-button. Each side only ever moves its own branch and reads the other's, so the "checked out twice" / "can't force-update" errors are impossible,
and every sync is a fast-forward (faa never has to merge committed history).
agent worktree main repo
────────────── ─────────
branch: add-button ── faa ──▶ branch: faa-add-button (verify / tweak)
(agent edits here) ◀── faa ── (your fixes here)
│ │
└──────── shared .git ────────┘
| Command | Where | What it does |
|---|---|---|
faa |
worktree | Save your work (commit). If main pushed something, it pulls that first — then run faa again to save your edits on top. |
faa |
main | Check out & pull the newest finished worktree (switching to it if needed). If it's the feature you're already on, your uncommitted edits stay on top. Refuses to switch if that would abandon uncommitted edits — push or stash first. |
faa push |
main | Commit your local tweaks onto the mirror so the agent can pull them. Aborts if the worktree has advanced — run faa to pull first. |
faa reset |
either | "I'm done / scratch all this": declare the current state as truth, then run faa reset sync on the other side. |
faa reset sync |
other side | Adopt the latest reset (a hard reset — discards local changes here; the old tip stays in the reflog). |
faa loop ... -- CMD |
main | Run CMD in N worktrees at the current commit; each run's stdout is saved as summary-<ts>/run-N.md. See faa loop. |
faa loop --clean |
main | Remove the reused worktree pool, discarding the work still in it. |
faa -l, --list [N] |
any | List the last N worktrees in work (default 5). |
faa -p, --pick [N] |
main | List the last N and pick one (by number) to verify. |
faa -c, --checkout <B> |
main | Verify feature branch B (creates/updates mirror faa-B). |
faa -h, --help |
any | Show help. |
# in the agent's worktree
faa # save the agent's work
# in main (dev server picks up faa-add-button)
faa # pull it in and run/verify it
# ...make a fix...
faa push # send your fix back
# in the worktree
faa # agent pulls your fix and keeps goingThe verify loop above is for work you keep. faa loop is the opposite: run the
same command in N disposable worktrees at the current commit, and keep what
each run printed.
faa loop [-n N] [--seq] [--no-reuse] -- CMD [ARGS...]CMD runs inside each worktree — faa doesn't know or care what it is: an
agent, a chain of agents, a plain script. The contract is just streams:
- stdout becomes the run's summary, saved as
summary-<timestamp>/run-N.mdin the main repo (the dir is auto-added to.git/info/exclude, so git status stays clean). - stderr passes through to your terminal and is not saved.
Since agent CLIs like opencode run are synchronous, multi-stage runs are
plain && chains — no support from faa needed. Route the working stages'
output to stderr so only the last stage's markdown lands in the summary:
faa loop -n 5 -- sh -c '
opencode run "Implement streaming for X" >&2 &&
opencode run "Summarize the changes in this repo as markdown"'| Flag | What it does |
|---|---|
-n N |
Number of runs (default 5). |
--seq |
Run one at a time (default is all in parallel; same total tokens either way — parallel only buys wall-clock, so go --seq if your provider rate-limits). |
--no-reuse |
Use fresh throwaway worktrees under the summary dir, removed once the run is done, instead of the reused pool. |
By default the worktrees are a persistent pool at .faa-loop-pool/wt-N.
Each loop resets the slots it needs (git clean -fdx + git reset --hard)
rather than creating worktrees from scratch — cheaper, because git only
rewrites what actually differs instead of materializing the whole tree N times.
Two useful consequences: N can change freely between runs (extra slots are
created on demand, and shrinking just leaves the unused ones alone), and each
run's work stays on disk in its slot until the next loop reclaims it — so if
a summary makes you want the actual diff, it's still there in
.faa-loop-pool/wt-N.
When you're done with a batch, tear the pool down:
faa loop --cleanThat removes .faa-loop-pool/ and everything still in it — including the last
loop's work — so grab any diff you care about first. The next faa loop
rebuilds the pool from scratch.
The pool assumes one faa loop at a time. If you need two loops running
concurrently, pass --no-reuse so they don't reset each other's worktrees.
The worktrees are detached, so they never collide with the mirror-branch
machinery above and don't show up in faa -l. Runs start from whatever commit
main is currently on — to fan out from somewhere else, check that out first.
faa is one file with no dependencies beyond git and the coreutils that ship
with Git Bash. Put it on your PATH:
cp faa /usr/bin/faa # or any dir on PATH
chmod +x /usr/bin/faaThen run faa from main or from any worktree.
- Topology: worktrees of one repo (shared
.git) created withgit worktree add. No remotes/fetch involved — everything is local refs. - Mirror prefix:
faa-by default. Change it at the top of the script (FAA_MIRROR_PREFIX) or via the environment (FAA_MIRROR_PREFIX=mir- faa). - One rule: feature branches must not start with the mirror prefix (they'd be mistaken for mirrors). faa errors out if they do.
- Multiple features at once: each gets its own mirror, so plain
faaon main always follows the newest finished worktree (andfaa -cjumps to a specific one). Each mirror keeps its own tweaks. Switching away from a feature with uncommitted edits is refused —faa push(or stash) them first. - Commit messages are auto-generated (timestamp + changed files); this workflow
treats them as throwaway. Use
faa resetto bake the final state in.