Releases: tyler-johnson/fufu
Release list
fufu v0.17.0
fufu 0.17.0 wires three more agent clients — OpenCode, GitHub Copilot CLI, and Qwen Code — and moves the Codex and Cursor installs onto each client's native plugin. Gemini CLI's adapter is gone.
New clients
ff hook opencode writes one plugin module fufu owns whole, with the skill beside it. It captures before every tool call and puts the briefing in the system prompt of every model call, so it survives compaction. A fufu.js fufu did not write is reported as hand-written and left alone.
ff hook copilot writes an Agent Plugins 1.0 plugin under ~/.agents/plugins/copilot/, an entry in the marketplace beside it, and a registration in ~/.copilot/settings.json. Copilot loads it live; the next session captures and is briefed.
ff hook qwen merges five hook events into ~/.qwen/settings.json. It ships no skill.
A shell verb under any of these, or under Codex or Cursor, carries the client's session the way it already did under Claude Code.
Codex and Cursor as plugins
ff hook codex writes a plugin at ~/.agents/plugins/fufu/ with the skill inside, merges an entry into the personal marketplace, and runs codex plugin add when codex is on PATH. Codex gains SessionStart, Stop, and SessionEnd. Once the plugin verifies, the install strips what an earlier fufu wrote under ~/.codex/, and ff unhook codex no longer opens ~/.codex/config.toml. Codex still trusts a hook by its hash: run /hooks there and approve fufu's, or nothing captures.
ff hook cursor writes a user-local plugin at ~/.cursor/plugins/local/fufu/, which Cursor CLI discovers on a new session. An install an earlier fufu merged into ~/.cursor/hooks.json reads as wired and stale; ff hook -u migrates it. beforeSubmitPrompt and stop are not wired, as Cursor CLI gates them on user settings, and cloud agents get no user-local hooks.
Removed
ff hook gemini and ff unhook gemini are gone; Gemini CLI's hook names diverged from the family. ff trigger gemini still serves entries an earlier fufu wrote.
ff commit no longer prints a re-minted: line or carries reminted in --json. With signing on, the @ row shows the open commit's sha and the close lands a different one, as it does for a partial close or a message set on the command line.
Fixes
ff hook claude on Windows wrote the binary's path unquoted, and Git Bash collapsed the backslashes, so every wired event failed while ff hook -l still said wired. The path is quoted now; ff hook claude or ff hook -u rewrites an existing install.
Miscellaneous
hook/failednames a client file or plugin directory that could not be written or did not read back;hook/malformednames a client file that is not the JSON object its client reads. Both leave the file untouched.ff hook -l --jsonreports an unreadable client file asunavailablewith its complaint, and a hand-written file ashand-written.
fufu v0.16.1
fufu 0.16.1 is a patch release with one fix.
Fixes
ff pushof a branch that is not checked out no longer overwrites that branch's parked work with the current checkout's tree. Work already affected is recovered from a retained pre-push capture; the stacked-branch guide shows how.
fufu v0.16.0
fufu 0.16.0 adds automatic fetching, pruning for branches whose remote copy is gone, and seen records for remote-history replacement checks. The documentation and command help cover everyday workflows, revision addressing, defaults, and recovery limits.
Fetching
Eligible commands refresh tracking refs on a ten-minute cadence by default. fufu.autoFetch=false or CI disables automatic fetch; global --fetch and --no-fetch force or skip supported fetches. Fetches prune deleted remote-tracking refs, and ff doctor requests a refresh on every run when fetching is enabled. Automatic fetch has a three-second deadline and disables terminal prompts, though external helpers can still prompt and some handshakes can exceed the deadline. See fetching and dry runs.
Branch pruning
ff branch --prune deletes local branches whose shared copy is gone, preserving unique work, the current branch, branches checked out elsewhere, and held rewrites. Child branches are redirected to the deleted branch's base. --dry-run previews deletion; capture, automatic fetch, and maintenance can still run. Set fufu.pruneGone=true to prune during ff pull; it defaults to false.
Push checks
Replacing or deleting remote history requires agreement between the tracking ref and fufu's seen record. A background fetch cannot authorize overwriting newly fetched work. Reporting pulls and successful ff push sends record the observed tip, as do cloning and creating a local branch from a remote-tracking branch. Fast-forward pushes can proceed without a seen record; replacement pushes from older repositories without one return push/unseen.
Known issues
Named off-branch pushes can record the current checkout's tree as another branch's open state. Switching there can resume unintended deletions. Push each branch while it is current; the stacked-branch guide covers inspection and recovery from retained captures.
Miscellaneous
ff op trimreplacesff trim; the old spelling remains an unlisted alias and reportsop trimin JSON.ff undoafter branch deletion restores the branch's timeline pointer.- The tutorial, workflow guides, command help, and error explanations have been revised. A dedicated revisions reference covers addressing and IDs.
fufu v0.15.0
fufu 0.15.0 makes the open change a commit: the @ row's sha is real, ff commit moves the branch onto it, and a park is that commit rather than a stash entry. ff start and ff new are spellings of ff switch, ff absorb and ff lift are one move that takes --from and --into, and ff mcp and the declared extension side are gone.
The open commit
Every capture of a dirty tree writes the open change as a commit at refs/fufu/open/<branch>, the tree over HEAD with the pending description and the change-id header. The @ row's sha on ff log, ff status, and ff show is that commit's, and ff commit moves the branch onto it, so the sha @ showed is the sha the closed commit wears. Where it cannot — signing is on, a partial close, a hook that changed the tree or the message — it mints one and says so on a re-minted: line.
@^ is HEAD and @~n is HEAD~(n-1), so @~3..@ is two commits plus the open change.
The park
ff switch and its kin leave a dirty tree's change at the open ref, where git log --all shows it one above the branch. git stash list no longer shows fufu: wip on <branch> rows; a park made by an earlier fufu folds into the open commit on the first arrival there. Staged state is not carried across a park: a staged hunk comes back as an unstaged edit.
Arriving on a branch whose tip moved under its park replays the open commit onto the new tip. A replay that conflicts holds the branch with exit 3, ff resolve lays the change into the working copy with markers in place, and ff resolve --abandon drops it.
One switch
ff start and ff new are spellings of ff switch, and the rule under every spelling is one sentence: find the branch, else mint it. ff switch <branch> takes a branch only a remote holds, minting the local branch and tracking it, the way git switch does. ff switch main -b forks main onto an anonymous branch. ff start @ and ff branch <name> @ carry a copy of the open change onto the new branch while the branch left behind keeps its own. Every spelling is one operation, and one ff undo takes back the mint, the copy, and the move.
One move
ff absorb and ff lift are one move: a contiguous run of commits, the open change allowed on top, into any commit on the branch's line or the open change. --from <revset> names the run and --into <rev> the target, with -m and --no-verify on both. ff absorb --from HEAD~2.. squashes two commits into the third, ff lift --from HEAD~2.. uncommits both, and ff lift --from HEAD~3 --into HEAD moves a commit's content up the stack. A source the move empties is dropped and named. Both verbs report moved <n> file(s) from <sources> into <target>, and --json carries the report under move.
Removed
ff mcp and its registration in Claude, Codex, Cursor, and Gemini are gone; ff hook and ff unhook remove the registration an earlier fufu wrote. The declared extension side goes with it: ff extension, the registry, the manifest handshakes, and the extension/* error ids. ff <name> still runs ff-<name> from PATH. The operation route, route() in the op revset and the route line on ff op show, is gone too.
Miscellaneous
- A dirty tree with no
user.namerefuses to switch away, since no open commit could stand as its park. --jsonrenames:arrival.stategainsheldandlandedand losesstill_parked;openreplacesstashunderrestored,held,landed, andparked;open_leftreplacesparked_demoted;ff done --abandon'sleftreplacesstashed.
Fixes
- A range revset no longer leaks a ref tip that shares a commit time with the hidden commit.
ff pull's native fetch no longer opens every linked worktree once per advertised ref.
fufu v0.14.0
fufu 0.14.0 gives every commit a change id, the identity it keeps through every rewrite, and adds ff fold, which lands the branch you stand on into another. ff branch, ff worktree, and ff extension take flags in place of subcommands.
Change ids
The letters column on ff log, ff status, the map, and ff show is a change id, the identity a commit keeps through every rewrite fufu performs, in place of the op anchor. ff commit writes it as jj's change-id header, and a commit made outside fufu derives one from its sha. A letters token in a revision slot is a change id or a unique prefix of one, and ff switch <change id> redirects to ff start at that commit, the way a sha does. ff restack and the cascade drop a commit whose change id the base already holds.
Operation and capture ids are hex, twelve characters, like jj's, everywhere an operation is named: ff op log, ff history, ff undo, --at-op, and the JSON surface. id_letters is gone from ff status --json and ff log --json.
ff evolog <rev> drills into a change: every operation, on any worktree's chain, that produced a commit carrying its change id, then the captures behind the close.
ff fold
ff fold [<target>] [--stay] lands the branch you stand on into another, trunk when none is named. Its commits replay onto the target's tip, the target fast-forwards, the branch goes to trash, and the worktree moves to the target with the open change still open, one undoable operation with the branches above re-aimed at the target. --stay keeps the branch on the result and advances a target another worktree holds. A replay that would conflict is fold/conflict and changes nothing.
Flags, not subcommands
ff branch, ff worktree, and ff extension take flags: bare lists, a positional creates or adds, -d/--delete deletes or removes. ff branch <name> [<rev>] creates a branch at <rev>, trunk by default, without switching. The list, create, delete, add, and remove spellings are gone.
Miscellaneous
- A branch created outside fufu records the branch it was cut from as its base when that is certain, and the absorb line says
forked from <branch>. - A git upstream under another local branch's name, or trunk's, is the branch's base rather than its shared copy, and the
branch/aliased-copyrefusal goes. ff git mergeruns underfufu.gitPolicy strict, the wayff git tagdoes.
Fixes
ff commit -b <fresh>from a named branch no longer leaves its pending description on the branch it left.ff restack --ontotrims the replay by the target's reflog too, so a branch cut outside fufu no longer replays a stale copy of a commit its base has since rewritten.ff pushon a branch whose upstream is another branch's tracking ref creates the branch's own copy and records the upstream as its base.
Known issues
- A rebase or cherry-pick run outside fufu drops the
change-idheader, and the commit comes back with a derived id. - After
ff undothenff redoof a partial close, the remainder can carry a different change id than it did between them.
fufu v0.13.0
fufu 0.13.0 renames ff sync and ff publish to ff pull and ff push, and both take branch names. ff mcp serves typed tools in place of one ff tool, and ff update moves declared extensions as well as fufu.
ff pull and ff push
ff sync is ff pull and ff publish is ff push. The old spellings stay as visible aliases. The error ids under sync/ and publish/ moved to pull/ and push/, and the old ids resolve nowhere, ff explain included. The operation log, the JSON envelope, and the counts ff status prints follow the new words.
Both verbs take branch names, from wherever you stand. ff pull <branch>... brings each named branch level with its shared copy along with the local bases beneath it. ff push <branch>... sends each under its own lease, and a lease the remote refuses is that branch's alone: the rest go out and the exit is 1. Bare ff pull is now the branch you stand on and its bases rather than every local branch; ff pull --all is the whole-repository run. Push has no --all.
ff pull --dry-run says which branches would fast-forward, replay, hold, or be skipped, and writes nothing beyond remote-tracking refs. --no-fetch beside it reads what you already have.
Typed tools
ff mcp serves seven tools, status, pull, push, undo, redo, explain, and help, each taking the verb's own flags as fields and a cwd. Every other verb is the shell. isError follows the envelope rather than the exit code, so a held pull is a successful call carrying data, and every result carries the child's exit in _meta.exit. The briefing names the tools only where the client has ff mcp registered.
fufu.toolPolicy is gone, along with the refusals it drove.
Extensions update
A manifest can carry update, a block of recipes keyed by channel (brew, install with bin, releases), and build, official or source. ff update walks every declared extension after fufu by the rules it applies to itself, and the passive release notice covers an official build with a GitHub releases page. ff hook -u refreshes what is wired, re-asking each manifest first, and the install scripts run it after placing the binary. ff doctor reports a binary that has moved past its record as that extension's own drift row.
The manifest's skills field names skills, and ff hook asks the extension for each one's files.
Miscellaneous
ff resolveopens a session branch the wayff editdoes, andff donelands the fixes and returns in one operation.- jj's names are aliases:
ff bookmark,ff workspace,ff squash, andff rebase.ff abandonandff splitname the moves that cover them. ff status --jsoncarriesroot,worktree,base,remote, andlast_op, and every operation records its route,shellortool.ffreadsCLAUDE_CODE_SESSION_IDwhen no session is given another way.- "Working copy" replaces "working tree" everywhere fufu speaks.
Fixes
ff switch,ff done, and the resolution landing recorded an end tree without the untracked files of the park they resumed, so the nextff undodeleted the files.ff switchaway during a resolution no longer overwrites the branch's parked change with the marker tree.- Every replay walked a range by commit date as well as ancestry, so a commit dated older than the base was left out, and a branch made only of such commits was refused as already on its base.
ff extension removebeforeff hook claudeno longer leaves the extension's skills in the plugin.
fufu v0.12.1
fufu 0.12.1 is a patch release: three fixes to what fufu reports and to what it tells an agent.
Fixes
- Bare
ffrendered a branch's parked change as(+ parked change, 1 file)where switching to that branch reported two. A park writes untracked files into a tree of their own, and the map was counting the tracked diff alone. Both surfaces now ask one function, which answers the tracked diff plus the paths under the untracked tree, deduped. - The skill fufu ships to an agent was written before the cascade and described the verbs as they stood: a conflicted replay left nothing changed, where the operation has landed and a branch above is holding, and
ff syncwas about the branch you stand on, where the verb now moves the whole repository. Rewriting, held rewrites and remotes are all restated, andff extension addis on the machine surface. - The refusal for an undeclared extension ended "an ff-tower nobody declared runs from a shell", and an agent reading it ran the hyphenated binary. It leads with the action now, spells the command
ff <name>, and says out loud that the shell is the one place an undeclared extension runs.
fufu v0.12.0
fufu 0.12.0 lets a tool built on fufu introduce itself. An extension declared with ff extension add is described to an agent everywhere fufu already speaks — served by the MCP tool, reachable through ff help, subscribed to the agent event — and ff mcp is the server that serves it. Sync and every rewrite now carry the branches stacked above them.
Declared extensions
ff <name> has always run ff-<name> from PATH. That is a filename, and a filename is all fufu could honestly tell an agent about the verb.
ff extension add <name> runs ff-<name> --ff-manifest and records what comes back. Declaring buys the extension no capability and no environment — an undeclared ff-<name> runs from a shell exactly as it always did. What it buys is that fufu describes the verb: served by the ff tool and named on its card, reachable through ff help <name> and ff explain <name>/<id>, one capped line in the agent briefing, skill files shipped with the hook install, a subscription to the agent event, and an MCP server of its own beside fufu's.
An extension may also produce typed MCP tools of its own, generated from the definitions its CLI is already built from so there is no second spelling to keep in step.
docs/reference/extensions.md is the reference for building one.
ff mcp
A Model Context Protocol server on stdio, exposing one tool for fufu's own verbs: ff, whose input is the command line after ff as an array of words. Every call runs the binary as a child with --json, so capture, fufu.gitPolicy, sessions, and error ids all hold unchanged.
ff hook claude, codex, cursor, and gemini register it beside the capture hook they already wire. ff unhook removes exactly that, and a registration you wrote by hand is reported and left alone.
fufu.toolPolicy decides what fufu says when an agent runs ff in its shell while the tool is up for it: observe says nothing, coach names the tool once per session, and strict, the default, refuses and carries the call to make instead. It is silent unless a server that client spawned is actually alive.
ff config refuses a write to fufu.gitPolicy or fufu.toolPolicy through the tool, naming a shell as the place to make it, so an agent cannot lower the tier policing it. Reading is untouched.
The cascade
ff restack, ff sync, ff absorb, ff lift, ff describe <rev>, and ff done now replay every branch stacked on the one they moved, parent before child, inside the verb's own operation. One ff undo takes the whole cascade back with the rewrite.
A branch whose replay conflicts is held on its own, with everything above it left alone. One checked out in another worktree, one already holding a rewrite, and one carrying a merge are skipped and named. ff restack and ff sync exit 3 when any branch held.
ff sync now covers the whole repository. After one fetch, every local branch is brought up to date with its shared copy and then with the base beneath it, parent before child. A branch you have not changed follows its shared copy wherever it went, a force-push included; one you have changed takes in new work and replays your commits on top.
The help pages
ff <verb> --help is shorter. The pages can hold ### subheadings and bullet lists now, which they could not before, and the design argument behind each verb has moved to the documentation site. What is left is what a person running the command needs.
Fixes
ff restackandff resolveon a branch whose base was rewritten replay the branch's own commits alone. The range is bounded where the branch forked from the base's history, read from the base's reflog the waygit rebase --fork-pointreads it, rather than at the merge base with the rewritten tip — which handed the base's old commits back as the branch's and conflicted on them once the rewrite changed their content.
fufu v0.11.0
fufu 0.11.0 runs git's commit hooks — all four of them, on every verb that makes a commit. It also stops installing itself over itself, wires PowerShell, and publishes an index of every error id it can raise.
Commit hooks
ff commit ran pre-commit and commit-msg. It now runs prepare-commit-msg and post-commit as well, in git's order, each with GIT_EDITOR=: — what git sets for a command that will not open an editor.
The larger gap was which verbs ran them at all. ff absorb, ff done, and ff describe <rev> all close a commit and all ran nothing, so content a pre-commit gate would have declined landed anyway. pre-commit now runs for ff absorb and for both of ff done's landings, the edit session and the resolution, over an index staged with exactly what is landing. The message hooks run for ff describe <rev> and for an ff done whose session carries a new description. Each of the three takes --no-verify.
ff lift, ff restack and ff sync still run no hooks, matching git rebase.
ff update dispatches
fufu used to download a release archive, verify it, and swap the running binary. That is the package manager's job, and doing it badly means a binary that replaces itself without the thing that installed it knowing.
ff update now names the command that updates this copy: cargo install for a source build, brew upgrade fufu for Homebrew, the install script for a binary sitting at the install script's own path, the releases page for anything else. It runs that command only on -y or a typed yes, and -y on a channel it cannot drive exits 1. The one binary fufu will replace is the one the install script owns, and the install script is what replaces it.
The background update check still runs on fufu.updateCheck and still lands its one-line notice, naming whichever command owns the binary. fufu.autoUpdate is gone — there is nothing left for it to turn off — and so are the downloader and the four dependencies it needed.
install.sh and install.ps1 now land the new binary beside the old one and rename, so they can replace an ff that is currently running.
PowerShell
ff hook powershell writes function git { ff git @args } and a wrapped prompt into $PROFILE, with the same marker and the same ff unhook as bash, zsh and fish. On Windows the profile is PowerShell 7's under the Documents known folder, or Windows PowerShell 5.1's when that is the only one on disk, and the slug is always offered; elsewhere it is ~/.config/powershell/, detected when the profile exists or $SHELL is pwsh.
Error ids
docs/reference/errors.md lists every id ff explain knows, with its exit code and a one-line meaning, generated from the registry. ff explain --json entries carry exit, and ff explain --list prints it as a column.
ref/contended exits 4 rather than 1. 4 means nothing was touched and running the same command again is the answer; 3 keeps meaning a human is needed.
Fixes
- A held rewrite whose later commit merged its own change into a standing conflict marker left
ff doneunable to land any resolution — every fix refused withno marker block to resolve at <path>. The chain now stops at that fold, as it does when two conflicts interleave. ff syncfailed its whole run when a linked worktree's admin dir held agitdirfile without a readablecommondir, the state such a directory passes through while being created or removed. The fetch retries once throughgit fetch, which walks past it.- A CRLF rc file keeps its line endings through
ff unhookand through the retired-spelling rewriteff hookdoes.
fufu v0.10.0
fufu 0.10.0 adds a documentation site, a setting that steers agents away from raw git, and commit signing. Two reports that were telling you things that weren't true are fixed along the way.
Documentation
There is a docs site: thirty-two pages covering a tutorial, the concepts, five task guides, and a section on running fufu behind an agent. Every console block on it was pasted from a script run against the real binary, so regenerating a page's output is one command.
The CLI reference and the config key list are generated from the binary's own help pages and checked byte for byte in CI, so they cannot drift from --help. The help text itself is now markdown, rendered to 72 columns for the terminal.
ff --help used to list forty commands in one alphabetical block. They are now grouped under the same headings git help uses.
Steering agents away from raw git
fufu.gitPolicy replaces fufu.translate, which quietly ran ff commit when you typed git commit. Running a different command than the one you asked for is the wrong way to fix this, so that path is deleted rather than turned off.
Three levels:
observe— records raw git use and says nothingcoach(the default) — names the fufu equivalent the first time a git word comes upstrict— refuses the git words fufu has a verb for, and says what to run instead
It covers both ff git and a bare git inside an agent's shell tool. Only the eighteen git words fufu actually has an answer for are touched, so git apply, git bisect and git gc pass straight through and ff git stays a working escape hatch even under strict. Anything fufu cannot parse cleanly is left alone. ff doctor reports how much raw git the repository has seen and how much was refused.
Agent briefing
Subagents were never told fufu was there. They share their parent's session id and fire no prompt event, so the marker fufu kept counted them as already briefed. The marker now tracks which audiences have been briefed, so a subagent gets its own, and so does a repository the agent has just cd'd into.
Claude Code installs two more capture events, Stop and SubagentStop. fufu snapshots before an action, so files an agent wrote at the very end of a turn sat uncaptured until the next thing happened.
Commit signing
fufu writes commit objects itself and never read commit.gpgsign, so commits came out unsigned whatever git config said, and rewrites dropped any signature they found. A repository that requires signed commits could not use fufu.
fufu now signs using git's own configuration — commit.gpgsign, gpg.format, user.signingkey and the program keys — in all three formats git supports: openpgp through gpg, x509 through gpgsm, ssh through ssh-keygen. No new fufu settings.
Rewrites sign too. Git makes you set rebase.gpgSign separately and silently unsigns the branch if you don't, and a restack that quietly unsigned three commits is what this exists to prevent. ff commit takes -S and --no-sign for one-offs.
ff log and ff status mark signed commits for free, since carrying a signature is a header they already read. ff log --signatures checks them and prints the verdict, the tool and the key — verified gpg 9B295D68 — with the checks running in parallel. ff show verifies the commit it prints, and ff doctor says whether your signing setup will work.
One regression: with signing on, ff status no longer predicts the next commit's sha, because that is not knowable without running the signer.
Fixes
ff trimsaid "branch is gone" about branches that were alive. A pointer is deleted either because the branch is gone or because its operations aged out of the keep window, and both printed the same line; they now read differently.- With two worktrees, parking and resuming in one made the other report branches deleted and created that had never moved. Refs another worktree holds are hidden from observation, so the stored ref table and the live one were filtered differently. The table now carries hidden entries forward.
Miscellaneous
- Three oversized files became directory modules with no behavior change: the core's
rewrite.rs, the CLI'srender.rs, anddoctor.rs. - The docs site deploys itself on a push to main, and docs-only pushes skip the rust build.
- A test that compared two renderings byte for byte stopped reading the clock, having flaked when the two reads straddled a second.