Skip to content

Changelog

Changes are grouped by release. Unreleased describes work on the development branch. Older entries describe their release's commands and behavior; use the current command reference for today's syntax and Project for support policy.

v0.19.1 — 2026-09-30

Changed

  • ff done names the commit that received resolution edits made outside the conflict markers, with an ff absorb line to move them. JSON: done.outside.

Fixed

  • The ff resolve and ff done help and the held rewrites page now say that an edit outside every marker lands in the last replayed commit.

v0.19.0 — 2026-09-28

Added

  • fufu.pull (auto, the default, replay, or merge) sets how a branch takes in a moved base, and fufu.<pattern>.pull sets it per branch by glob. It governs ff pull's base step, the cascade under rewriting verbs, and ff resolve with no hold. ff config pull lists the setting and every pattern row with its scope. See configuration.

Changed

  • A branch whose policy is merge, or auto with any merge among its commits, is left standing when its base moves, where before only a merge of the base stopped it. ff pull and the cascade report it behind its base with the policy and where it was set; the branch a verb was given still replays. replay replays it and flattens a merge of the base. JSON: skip reason merge-in-range is now behind, carrying policy and source.
  • ff resolve with no hold merges the base into a branch left standing, and refuses a branch that should replay toward ff restack, naming the policy.

Fixed

  • Path positionals on ff log, ff diff, ff show, ff restore, ff commit, ff absorb, and ff lift resolved from the repository root, so in a subdirectory they matched a root namesake or nothing. They now resolve from the current directory, and a path outside the repository is refused with usage/no-such-path.
  • ff restore of a path that matches nothing exited 0 with an empty report. It now refuses with usage/no-such-path.
  • A conflicting ff restack of a branch other than the current one, directly or through ff pull, replaced that branch's parked change with the current working tree, so ff switch to it resumed the wrong files.
  • ff done on a resolution whose parked change conflicted with the landed tip cleared the hold, leaving the parked change only in the op log. The hold now stands, and ff resolve lays the parked change into the working copy with markers.

v0.18.0 — 2026-09-21

Added

  • ff merge <branch> takes a local or remote-tracking branch into the current one by a merge commit, or fast-forwards when the branch has nothing of its own. The base is refused with merge/base: ff pull and ff restack replay onto it. A conflicting auto-merge records a merge hold, exit 3, that ff resolve opens and ff done lands.
  • ff resolve on a branch whose commits already hold a merge of its base takes the base in by another merge commit rather than refusing. A linear branch behind its base still names ff restack.
  • --resolve on ff restack, ff pull, and ff merge opens the resolution session in the same run when the branch underfoot conflicts, exit 3 still. fufu.onConflict (hold, the default, or resolve) makes it standing and --no-resolve holds for one run.
  • ff diff takes revisions: -r <revset> shows a connected set's whole patch, and --from <rev>/--to <rev> two points. A gapped, forked, or two-headed set is refused with usage/revset-not-a-range.
  • One view set on ff diff, ff show, and ff log: --stat, --name-only, and -U <n>, with ff log -p hanging each row's patch under it and ff show --no-patch stopping at the message. ff log --body prints each row's message body.
  • --fields <list> on every --json verb projects data to comma-separated dotted paths. A path that matches nothing is refused with usage/no-such-field. See JSON output and scripting.

Changed

  • Every rewrite (restack, fold, done, absorb, lift, the cascade, and pull's remote axis) carries a merge in its range instead of refusing with rewrite/merge-in-range: its parents are re-merged and its own change applied on top. A merge left with one parent becomes an ordinary commit, reported under flattened. A merge that resolved a conflict keeps the resolution, and a conflict beneath it on either parent passes through to the session.
  • ff show, ff log -p, and ff diff -r measure a merge against the auto-merge of its parents, so a clean merge is empty and a merge that resolved a conflict shows that. When the auto-merge itself conflicts they fall back to the first parent and say so. JSON gains against.
  • ff done --abandon on a resolution session closes the session and keeps the hold, so ff resolve opens a fresh session over it. Dropping the hold too is ff resolve --abandon's meaning.
  • ff show prints the message whole, subject and body, and JSON gains body. The @ row of ff status, ff log, and ff map shows a multi-line pending description's subject only.

Fixed

  • ff done on a resolution whose fix uncovered the next conflict refused with held/unresolved and left no way forward but abandoning. It now rolls the session to another round with the next conflict's markers in the working copy, exit 3 each round, and lands once a round leaves no conflict. One ff undo steps back a round.
  • A rewrite of a held branch left the hold standing, so ff resolve later replayed it onto a base it no longer had. A re-aim with ff restack --onto, or a fold of the source, now drops a held restack and says so; absorb, lift, describe, and done point the hold at the rewritten commit; a held absorb, lift, done, or arrival refuses every rewrite with held/already-held.
  • ff diff and ff show answered a positional that names no path, such as main..HEAD, with an empty patch and exit 0. Both now refuse it with usage/no-such-path.
  • ff pull on a branch holding a merge of its base discarded the whole run, local trunk included. The branch is now left alone and named while the rest of the run goes on.

v0.17.0 — 2026-09-18

Added

  • ff hook opencode: OpenCode, wired by one plugin module fufu owns whole at <config>/opencode/plugins/fufu.js, with the skill beside it. The plugin 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: GitHub Copilot CLI, wired by an Agent Plugins 1.0 plugin at ~/.agents/plugins/copilot/fufu/ with the skill inside, an entry in the marketplace file beside it, and a registration in ~/.copilot/settings.json. Copilot loads it live; the briefing arrives as additionalContext.
  • ff hook qwen: Qwen Code, wired by five hook events merged into ~/.qwen/settings.json. No skill.
  • hook/failed for a client file or plugin directory that could not be written or did not read back, and hook/malformed for a client file that is not the JSON object its client reads. Both leave the file untouched.
  • A shell verb under Codex, Qwen Code, OpenCode, Copilot CLI, or Cursor reads that client's session variable (CODEX_SESSION_ID, QWEN_CODE_SESSION_ID, OPENCODE_SESSION_ID, COPILOT_AGENT_SESSION_ID, CURSOR_CONVERSATION_ID) the way it reads CLAUDE_CODE_SESSION_ID.

Changed

  • ff hook codex writes a plugin at ~/.agents/plugins/fufu/ with the skill inside, merges one entry into ~/.agents/plugins/marketplace.json, and runs codex plugin add fufu@<marketplace> when codex is on PATH, printing the command otherwise. Codex gains SessionStart, Stop, and SessionEnd. The manifest's version carries a hash of the hooks table, so a rewritten table or a moved binary is a new version to Codex's cache. Once the plugin verifies, the install strips what an earlier fufu wrote under ~/.codex/: the hooks.json entries, skills/fufu/, and the marked config.toml block.
  • ff unhook codex removes the plugin directory and the marketplace entry, and no longer opens ~/.codex/config.toml.
  • ff hook cursor writes a user-local plugin at ~/.cursor/plugins/local/fufu/ with the skill inside, wiring preToolUse on Shell|Write|Delete, sessionStart, and sessionEnd; Cursor CLI discovers it on a new session. An install an earlier fufu merged into ~/.cursor/hooks.json reads as wired and stale, and ff hook -u migrates it. beforeSubmitPrompt and stop are not wired: Cursor CLI 2026.09.10 gates them on user settings. Cloud agents get no user-local hooks.
  • ff hook -l --json reports an unreadable client file as {"state":"unavailable","complaint":…} and a hand-written file as {"state":"hand-written","at":…}.
  • With signing enabled, the @ row on ff status, ff log, and ff show shows the open commit's sha, and @ resolves to it; the close signs and lands a different sha, as it does for ff commit -m or a partial close. open.pending in --json follows.

Removed

  • ff commit's re-minted: line and the reminted field in its --json. A close that lands a commit other than the open one is the ordinary case and says nothing about it.
  • ff hook gemini and ff unhook gemini: Gemini CLI's hook names diverged from the family, and ~/.gemini/settings.json is never touched. ff trigger gemini still captures and briefs for entries an earlier fufu wrote there.

Fixed

  • ff hook claude on Windows wrote the binary's path unquoted, and Git Bash collapsed the backslashes, so every wired event exited 127 while ff hook -l still said wired. The path is quoted now; ff hook claude or ff hook -u rewrites an existing install.

v0.16.1 — 2026-09-14

Fixed

  • ff push of 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.

v0.16.0 — 2026-09-14

Added

  • fufu.autoFetch: eligible commands refresh tracking refs at most once per cadence (10m by default), with a three-second fetch deadline and terminal prompts disabled. External helpers can still prompt, and some transport handshakes can exceed the deadline. false or CI disables automatic fetch; ff pull and explicit --fetch remain available. ff doctor reports the last refresh.
  • Global --fetch and --no-fetch: force or skip a supported fetch; unsupported --fetch returns fetch/not-here.
  • ff branch --prune: deletes local branches whose remote copy is gone, preserving branches with 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, auto-fetch, and maintenance can still run.
  • fufu.pruneGone: enables the same pruning during ff pull; defaults to false.
  • Seen records at refs/fufu/seen/<branch>: reporting pulls, successful ff push sends, ff switch creating a remote-tracking branch, and ff clone record the observed remote tip. Replacing or deleting remote history requires seen/tracking agreement; fast-forward pushes can proceed without it.

Changed

  • ff op trim replaces ff trim; the old spelling remains an unlisted alias and reports op trim in JSON.
  • ff pull uses the global --no-fetch flag with its existing meaning.
  • Fetches prune deleted remote-tracking refs. Doctor requests a fetch on every run when fetching is enabled.
  • ff undo after ff branch -d restores the branch's timeline pointer, matching ff fold.
  • Documentation and command help explain defaults, recovery scope, IDs, hooks, and network side effects; a dedicated revisions reference covers addressing. Historical design and performance evidence are labeled separately from current usage.

Fixed

  • Remote-history replacement checks use the seen record so an editor fetch, ff git fetch, or ff pull --dry-run cannot silently authorize overwriting newly fetched work.

Known issues

  • Seen records are absent in older repositories. A first fast-forward push records one; a replacement push without one returns push/unseen. A reporting pull records the observed tip.
  • Named off-branch pushes send the requested tips but can record the current checkout's tree as the target branches' open state. Switching there can resume unintended deletions; see the stacked-branch guide for recovery and the current-branch workaround.

v0.15.0 — 2026-09-12

Added

  • ff switch <branch> takes a branch only a remote holds, minting the local branch under that name and tracking it, the way git switch does; a name two remotes hold is branch/ambiguous.
  • Bare -b on ff switch: ff switch main -b forks main onto an anonymous branch.
  • --from <revset> on ff absorb and --into <rev> on ff lift, with -m and --no-verify on both: a contiguous run of commits moves into any commit on the branch's line, or the open change. ff absorb --from HEAD~2.. squashes two commits into the third; ff lift --from HEAD~3 --into HEAD moves a commit's content up the stack. A source the move empties is dropped and named.

Changed

  • The open change is a commit, written at refs/fufu/open/<branch> by every capture of a dirty tree. The @ row's sha on ff log, ff status, and ff show is that commit's, and ff commit moves the branch onto it, minting a new one only where it must (signing, a partial close, a hook that changed the tree or message) and saying so on a re-minted: line.
  • The park is the open commit. ff switch and its kin leave a dirty tree's change at that ref, git stash list no longer shows fufu: wip on <branch> rows, and 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 (exit 3), and ff resolve lays the change into the working copy.
  • @^ is HEAD and @~n is HEAD~(n-1), so @~3..@ is two commits plus the open change.
  • ff start and ff new are spellings of ff switch, and every spelling is one operation: find the branch, else mint it. ff start <branch> continues the branch; -b forks.
  • 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; ff start @ was refused.
  • ff absorb and ff lift are one move and say the same thing: moved <n> file(s) from <sources> into <target>, then the restack and the drops. --json carries the report under move.
  • A dirty tree with no user.name refuses to switch away, since no open commit could stand as its park.
  • ff hook and ff unhook remove the MCP server registration an earlier fufu wrote for claude, codex, cursor, and gemini.
  • --json renames: arrival.state gains held and landed and loses still_parked; open replaces stash under restored, held, landed, and parked; open_left replaces parked_demoted; ff done --abandon's left replaces stashed.

Removed

  • ff mcp and its registration in the four agent clients.
  • The declared extension side: ff extension, the registry, the --ff-manifest, --ff-tools, and --ff-skill 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 and key on ff op show.
  • ff start's own help page and JSON envelope; ff help start is switch's page, and --json under every spelling is switch. The absorb and lift JSON keys; both verbs carry move.

Fixed

  • A range revset — x.., x..y — 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; gix upgraded to 0.87.1 (#10).

v0.14.0 — 2026-09-10

Added

  • 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. --json carries the report as fold.
  • ff branch <name> [<rev>] creates a branch at <rev>, trunk by default, without switching, as one undoable operation. A branch name as <rev> is recorded as the parent, and @ is the commit under the open change. --json carries it as branch create.
  • 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. --json carries change_id, commit, operations, and snapshots.

Changed

  • 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. --json carries change_id.
  • A letters token in a revision slot is a change id or a unique prefix of one; an ambiguous prefix is usage/revset-ambiguous, and a change standing on more than one visible commit is usage/revset-divergent. Operation and capture ids are hex, twelve characters, like jj's, on ff op log, ff evolog, ff history, ff undo, --at-op, and every id the JSON surface carries for an operation. A change id in an op slot is usage/rev-in-op-position, and an op id in a revision slot is usage/op-in-rev-position.
  • ff switch <change id> redirects to ff start at that commit, the way ff switch <sha> does.
  • ff restack and the cascade drop a commit whose change id the base already holds: dropped … — superseded by <sha> in the base. JSON dropped entries gain reason and, under superseded, by.
  • A branch created outside fufu records the branch it was cut from as its base when that is certain: its tip is one other non-trunk branch's tip, or git's reflog names it. The absorb line says forked from <branch>, and ff undo takes the record back.
  • A git upstream under another local branch's name, or trunk's, is the branch's base rather than its shared copy: ff status shows it on the base axis, ff branch shows no copy, and the branch/aliased-copy refusal goes.
  • ff branch, ff worktree, and ff extension take flags, not subcommands: bare lists, a positional creates or adds, -d/--delete deletes or removes. --at, --at-op, and --all are the list's alone.
  • ff git merge runs under fufu.gitPolicy strict, the way ff git tag does, and the coaching line for git merge names ff pull and ff git merge in place of ff restack --onto.

Removed

  • id_letters from ff status --json and ff log --json; id is the operation's hex, and what --at-op reads.
  • The list, create, delete, add, and remove subcommand spellings under ff branch, ff worktree, and ff extension, and the usage/unknown-subcommand redirect from ff branch <name> to ff describe -b.

Fixed

  • ff commit -b <fresh> from a named branch no longer leaves its pending description on the branch it left.
  • ff restack --onto trims the replay by the target's reflog, not only the recorded base's, so a branch cut outside fufu no longer replays a stale copy of a commit its base has since rewritten. (#5)
  • ff push on a branch whose upstream is another branch's tracking ref creates the branch's own copy and records the upstream as its base, instead of pushing to that ref under a lease. (#7)

Known issues

  • A rebase or cherry-pick run outside fufu drops the change-id header, and the commit comes back with a derived id. jj has the same limitation.
  • After ff undo then ff redo of a partial close, the remainder can carry a different change id than it did between them.

v0.13.0 — 2026-09-08

Added

  • ff pull <branch>... and ff push <branch>... act on the branches named, from wherever you stand: a pull brings each with the local bases beneath it, a push sends each under its own lease, and a lease the remote refuses is that branch's alone. A name resolves the way ff restack resolves one. ff pull --all is every local branch; push has no --all.
  • ff pull --dry-run (-n) says which branches would fast-forward, replay, hold, or be skipped, and writes nothing but remote-tracking refs; --no-fetch beside it reads what you already have. The exit is 3 when a branch would hold.
  • ff status --json carries the orientation an agent asks for first: root, worktree, base with the count above it, remote, and last_op.
  • Every operation carries its route, shell or tool, in its trailer: on ff op show and ff op log --json, and as the route() filter in the op log's set language.
  • Every ff invocation reads CLAUDE_CODE_SESSION_ID when neither --session nor FF_SESSION is set, so a shell verb under Claude Code carries the session its hook captures do; ff mcp reads it once at start.
  • jj's names as aliases: ff bookmark, ff workspace, ff squash, and ff rebase run ff branch, ff worktree, ff absorb, and ff restack. ff abandon and ff split answer with usage/foreign-verb naming the moves that cover them. Every alias shows on ff --help.
  • The manifest's update and build fields. ff update walks every declared extension after fufu by the rules it applies to itself, the passive release notice covers an official build with a github.com releases recipe, and ff extension add says how ff update will answer for the extension. update.json carries extensions.
  • ff hook -u refreshes what is wired and adds nothing: every declared extension's manifest is re-asked, then the install re-run for every slug already wired. install.sh and install.ps1 run it after placing the binary. ff hook --json carries extensions.
  • ff doctor's Extensions floor reports a declared extension whose binary has moved past its record as that extension's own drift row, with ff extension add <name> as the repair.

Changed

  • 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/ are pull/ and push/, and the old ids resolve nowhere, ff explain included. The operation log, the JSON envelope's cmd and data key, and the counts ff status prints (N to pull, N to push) follow.
  • Bare ff pull is the branch you stand on and the local bases beneath it, rather than every local branch; ff pull --all is the whole-repository run.
  • ff mcp serves seven typed tools, status, pull, push, undo, redo, explain, and help, each taking the verb's own flags as fields and a cwd, in place of the one ff tool and its args array; every other verb is the shell. isError follows the envelope rather than the exit code, and every result carries the child's exit in _meta.exit. The briefing names the tools only where the client has ff mcp registered.
  • ff resolve opens a session branch the way ff edit does, with the hold staying on the branch you left; ff done lands the fixes and returns in one operation, and ff status carries the session under resolving on both branches.
  • The manifest's skills field names skills, and ff hook asks ff-<name> --ff-skill <skill> for each one's files, installed whole beside fufu's own; a failed handshake is extension/skill-failed or extension/bad-skill. A registry record from before reads as unreadable until ff extension add <name> rewrites it. undoable and verbs[].read_only are informational.
  • ff update -y is one answer for the whole walk: a channel it cannot drive is named at the end and the exit is 1 there, after every extension has been reached.
  • "Working copy" replaces "working tree" everywhere fufu speaks.
  • Changes made outside fufu render as one summary line in the reconcile preamble and the block ff status pins; ff status --json still carries every ref.
  • A git rebase typed through the shell alias is coached toward ff restack under fufu.gitPolicy=coach and refused under strict.

Removed

  • fufu.toolPolicy, with the presence marker ff mcp held for it and the ff trigger claude refusal it drove.
  • usage/mcp-verb-unavailable, usage/mcp-extension-undeclared, usage/mcp-extension-not-undoable, and usage/mcp-policy-write, with the sealed keys under the tool.
  • ff co, the hidden alias on the checkout foreign verb.

Fixed

  • ff switch, ff done, and the resolution landing recorded an end tree without the untracked files of the park they resumed, so the next ff undo deleted the files instead of stepping back.
  • ff switch away during a resolution no longer overwrites the branch's parked change with the marker tree.
  • ff pull, ff restack, ff absorb, and every other replay walked a range by commit date as well as ancestry, so a commit dated older than the base was left out of the replay, and a branch made only of such commits was refused as already sitting on its base. In a shallow clone the walk stops at the boundary.
  • ff extension remove before ff hook claude no longer leaves the extension's skills in the plugin. The Codex half of the v0.12.0 known issue stands.

v0.12.1 — 2026-09-04

Fixed

  • Bare ff counts a parked change's untracked files, so the map's (+ parked change, N) and the number ff switch prints on arrival agree.
  • The agent skill described ff restack, ff sync, and held rewrites as they were before the cascade, so an agent was told a conflicted replay left nothing changed and that ff sync covered one branch.
  • The refusal for an undeclared extension names ff <name> as the command to run, where it showed only the ff-<name> binary on PATH and left the shell exemption to ff explain.

v0.12.0 — 2026-09-04

Added

  • The cascade: ff restack, ff sync, ff absorb, ff lift, ff describe <rev>, and ff done replay the branches stacked on the one they moved, inside the same operation. A branch that conflicts is held on its own; ff restack and ff sync exit 3 when any did.
  • ff sync covers the whole repository: one fetch, then every local branch is brought up to date with its shared copy and its base, parent before child.
  • ff mcp, a Model Context Protocol server on stdio serving fufu's verbs as one tool. ff hook <client> registers it beside the capture hook.
  • fufu.toolPolicy: what fufu says when an agent runs ff in its shell while that tool is up. observe, coach, or strict, the default.
  • Extensions can declare themselves. ff extension add <name> records what ff-<name> says it is, and fufu then describes that verb to an agent everywhere it already speaks. docs/reference/extensions.md is the reference.
  • ff config refuses a write to fufu.gitPolicy or fufu.toolPolicy through the ff tool, so an agent cannot lower the tier policing it.

Changed

  • ff <verb> --help is shorter, and the pages take subheadings and lists.

Fixed

  • ff restack and ff resolve on a branch whose base was rewritten replay the branch's own commits alone, bounded at the fork point rather than at the merge base with the rewritten tip.

Known issues

  • A subscriber's injected context is uncapped, where fufu's own briefing line is dropped past 240 characters.
  • An extension's tool list is held for the life of the connection, so a client restart is what picks up an edited one. Two produced tools can collide on one name, and the later is dropped without a word.
  • ff doctor compares a declared extension's recorded version, so an edit without a version bump drifts unreported. A changed mcp.args is reported stale and not repaired by --fix.
  • ff extension remove before ff unhook leaves that extension's skills directory and its Cursor and Gemini registrations behind.

v0.11.0 — 2026-09-02

Added

  • The error id index, docs/reference/errors.md: every id ff explain knows, with its exit code and one-line meaning, generated from the registry. ff explain --json entries carry exit, the code the id exits with, and ff explain --list prints it as a column.
  • prepare-commit-msg and post-commit, the two commit-time hooks fufu did not implement. ff commit now runs all four: pre-commit, prepare-commit-msg, commit-msg, post-commit. Every commit hook runs with GIT_EDITOR=:, as git sets it for a command that will not open an editor.
  • --no-verify on ff absorb, ff done, and ff describe <rev>, the verbs that can now be declined by a hook.
  • ff hook powershell. $PROFILE gets function git { ff git @args } and a wrapped prompt, with the same marker and the same ff unhook as the other shells. 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 detected; elsewhere it is ~/.config/powershell/, detected when the profile exists or $SHELL is pwsh.

Changed

  • ref/contended exits 4 instead of 1, the one code that means nothing was touched and the same command run again is the answer. 3 keeps meaning a human is needed.
  • ff update names the command that updates this copy of fufu instead of downloading a binary over itself: cargo install for a source build, brew upgrade fufu for Homebrew, the install script for a binary at the install script's own path, and 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 only binary fufu will ever replace is the one at the install script's own path, and the install script is what replaces it.
  • The background update check still runs on fufu.updateCheck and still lands a one-line notice, but nothing installs itself any more. The notice names whichever command owns the binary.
  • install.sh and install.ps1 land the new binary beside the old one and rename, rather than writing over it, so they can replace an ff that is currently running.

Removed

  • fufu.autoUpdate. Silent background installs are gone; there is nothing left for the setting to turn off.
  • The in-process downloader — asset selection, sha256 verification, archive extraction, and the binary swap — along with the four dependencies it needed: sha2, tar, flate2, and the windows-only zip.

Fixed

  • A CRLF rc file keeps its line endings through ff unhook and through the retired-spelling rewrite ff hook does; both rejoined the file with LF.
  • ff absorb, ff done, and ff describe <rev> ran no hooks at all, so content a pre-commit gate would decline on a close landed in the commit anyway. pre-commit now runs for ff absorb and for both of ff done's landings — the edit session and the resolution — over the index staged with exactly what is landing, and the message hooks run for ff describe <rev> and for an ff done whose session carries a new description. ff lift, ff restack and ff sync still run none, matching git rebase.
  • ff sync failed its whole run when a linked worktree's admin dir under .git/worktrees/ held a gitdir file without a readable commondir — the state such a directory passes through while it is being created or removed. git ignores such a directory; fufu's native fetch stopped on it. The fetch is now retried once through git fetch, which walks past it, and the error names the offending admin dir when that fails too.
  • A held rewrite whose later commit merged its own change into a standing conflict marker left ff done unable to land any resolution: the block ff resolve showed was no longer the block the step that owned it had written, and every fix was refused with no marker block to resolve at <path>. The chain now stops at that fold, the same way it stops when two conflicts interleave, so ff resolve shows the mark as its owning commit wrote it and ff done lands the fix.

v0.10.0 — 2026-09-01

Added

  • Documentation site at https://tyler-johnson.github.io/fufu/: tutorial, concepts, task guides, CLI reference, and a section on running fufu behind an agent.
  • Commit signing, using git's own configuration (commit.gpgsign, gpg.format, user.signingkey, and the program keys) in all three formats git supports: openpgp, x509, and ssh. Rewrites sign as well. ff commit accepts -S and --no-sign.
  • ff log and ff status mark signed commits; ff log --signatures verifies them and reports the verdict, tool, and key; ff show verifies the commit it prints; ff doctor reports whether the signing setup will work.
  • fufu.gitPolicy, with levels observe, coach (default), and strict, covering both ff git and a bare git run inside an agent's shell tool. ff doctor reports the tally.

Changed

  • ff --help groups its commands under the same headings git help uses, instead of one alphabetical list. ff -h shows a short list of common verbs.
  • The CLI reference and the config key list are generated from the binary's help pages and checked byte for byte in CI.
  • Subagents, and repositories an agent has just entered, now receive the agent briefing. Claude Code's plugin installs Stop and SubagentStop capture events.

Removed

  • fufu.translate, replaced by fufu.gitPolicy. Command translation is gone entirely: fufu will not run a different command than the one given.

Fixed

  • ff trim reported a live branch as gone when its operations had aged out of the keep window.
  • With multiple worktrees, reconcile reported branch deletions and creations that had not happened, when another worktree held the branch.

Known issues

  • With signing enabled, ff status no longer predicts the next commit's sha.

v0.9.0 — 2026-08-27

Hooks become one family. ff hook, ff unhook, and ff trigger serve all four integration clients (agent, shell, editor, git) from a single core, and the index is populated before hooks run, matching git's own order. fufu now ships an agent skill: ff hook --skill installs it, and the agent briefing routes to the skill instead of carrying the manual inline. ff collide now answers one pair of branches per invocation, and two lints that arrived with Rust 1.98 are resolved.

v0.8.0 — 2026-08-25

Linked worktrees, done properly.

  • Each worktree gets its own operation chain, records only the refs it owns, and a branch can be open in only one worktree at a time; a second worktree's first command is no longer treated as a first run.
  • ff worktree add, ff worktree remove, and ff worktree list — making and taking a worktree are operations, ff undo reverses a worktree removal by capturing before it deletes, and the list shows the chains of worktrees that are gone.
  • Retention and the survey reach every chain, including the ones nobody stands in, so a dead bay's work stays reachable.
  • ff watch subscribes to the operation log, and ff watch --all streams every worktree in the repository on one tick.
  • ff collide reports which branches would hit each other, backed by a new sideways-comparison axis in the core.
  • ff -C <dir> runs fufu against another directory, and ff <name> reaches for an ff-<name> extension on PATH.
  • ff restack moves only the branch you named, the pager scrolls again (the X left LESS), and worktree paths get one spelling across platforms including Windows.

v0.7.0 — 2026-08-23

Remotes stop being invisible. ff publish --to names the remote and records it, ff sync's fetch speaks the git protocol itself, and ff doctor and ff status now see the remote's state. ff restack --onto accepts a base that lives on a remote, ff start gains a park line and can fork from remote branches, a branch rename carries its upstream, and branch lookups stop guessing that the remote is origin. ff -v routes through the version verb, the README's console blocks are generated from real output, and CI shards the process-spawning test legs.

v0.6.0 — 2026-08-21

The reading verbs and the remote verbs take shape.

  • ff diff shows the open change as a patch, ff show renders one revision with header and patch, -p joins the three views that list files, and the tree diff reads down to the line.
  • ff commit <paths> lands a slice of the open change and leaves the rest open; ff log <paths> filters the log by path.
  • Sync and publish get one verb each: ff sync takes in, ff publish sends, publish remembers the push so sync stops reversing your undo, --dry-run previews, and each status count names the verb that clears it.
  • ff init and ff clone make the starting point fufu's own; ff version replaces the uppercase flag.
  • ff history shows the moves rather than the operations, ff op log shows every operation and takes a revset as its argument, and the op read verbs capture first so @ means now.
  • Git translation becomes opt-in via the translate setting, and merge, blame, and tag join the git words fufu answers.
  • The map keeps the commits that relate branches, coded failures always point at a next step, the agent notice teaches fufu and the CLI holds agents to it, and CI runs green on all three platforms with a bench gate that stops coin-flipping.

v0.5.0 — 2026-08-19

History rewriting lands as a verb family. A rewrite substrate arrives behind ff describe <rev>; ff absorb and ff lift are the short reach, ff restack on any branch is the primitive, and ff edit sessions are the long reach. Conflicts run on your schedule: rewrites that conflict are held for ff resolve instead of stopping you mid-flight. ff sync reports divergence on both axes and attributes it to the right side via the rewrite map, a branch's base and remote become one axis per ref, and ff status names the capture its parent row was cut from.

v0.4.0 — 2026-08-17

Bare ff becomes the map: the repository's branches drawn as a skeleton, every branch name bolded, and the listing speaks the tool's dialect with color. Underneath, a branch now answers to a base and a remote, and futures arrive: ff status reports what syncing would cost — clean, conflict, or fast-forward — via a commit-by-commit replay probe, before you spend the operation. Short spellings land, and git-flavored words are answered with fufu's equivalent instead of parsed.

v0.3.0 — 2026-08-16

The internal model consolidates and the machine surface lands.

  • One log: the separate snapshot chain is retired, an operation is a log entry, and a snapshot is what an operation carries; the op log takes its own lock and stops growing with the commit log.
  • Revsets: one set language and one grammar for both address spaces, git's ~/^ suffixes walk the log, operations evaluate the language too, and a hex-shaped restore target is an id, never a duration.
  • The ff op family arrives and ff undo steps by runs; naming a branch is ff describe -b.
  • Sessions become named spans of the capture chain — list them, diff them, group the log by them.
  • The machine surface: every --json output carries a versioned envelope, errors carry stable ids and exit codes carry the verdict, and ff explain documents them; ff status computes one model that both renderings read.

v0.2.0 — 2026-08-14

The first wave of daily-driver verbs and the presentation layer.

  • New verbs: ff start begins new work on a fresh branch, ff help gets a written page for every verb, ff doctor verifies the net, ff update self-updates on two lanes, ff config speaks fufu.* git config with a typed registry, and auto-trim enforces retention on its own.
  • Change-centric ff log: an @ open-change row over ● commit rows with segment tips, plus ff evolog for the snapshot chain itself.
  • jj-style reverse-hex snapshot ids with unique-prefix highlighting, a colored spine, and pager and color infrastructure gated on the TTY.
  • A snapshot id index makes prefix resolution and the log family cost only the rows on screen, and ff log hops segments instead of walking the chain.
  • A bench suite that measures slopes rather than milliseconds, with fixtures keyed to the binary that built them; ff --version names the build it came from; a cargo-deny gate enforces permissive-only dependency licenses.
  • ff status shows one file list, one themed palette is chosen by fufu.theme, and usage lines say ff on Windows.

v0.1.0 — 2026-08-13

The founding release, in three phases on top of the DESIGN.md founding document.

  • Phase 0, bedrock: a native read core on gitoxide, read-only ff status and ff log with human and --json output, a permanent differential test harness against the real git binary, and a zero-spawn latency proof.
  • Phase 1, capture: bare ff snapshots the working tree, plus the timeline, ff restore, ff trim, git passthrough, and agent hooks.
  • Phase 2, time: an operation journal with whole-repo ff undo, tree memory, and ff commit, ff switch, ff new, ff branch, ff describe.
  • Integrations unify under ff hook <agent|shell|editor>, and release scaffolding lands: LICENSE, README, installers, CI and release workflows, and Windows portability gates.