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 donenames the commit that received resolution edits made outside the conflict markers, with anff absorbline to move them. JSON:done.outside.
Fixed¶
- The
ff resolveandff donehelp 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, ormerge) sets how a branch takes in a moved base, andfufu.<pattern>.pullsets it per branch by glob. It governsff pull's base step, the cascade under rewriting verbs, andff resolvewith no hold.ff config pulllists the setting and every pattern row with its scope. See configuration.
Changed¶
- A branch whose policy is
merge, orautowith any merge among its commits, is left standing when its base moves, where before only a merge of the base stopped it.ff pulland the cascade report it behind its base with the policy and where it was set; the branch a verb was given still replays.replayreplays it and flattens a merge of the base. JSON: skip reasonmerge-in-rangeis nowbehind, carryingpolicyandsource. ff resolvewith no hold merges the base into a branch left standing, and refuses a branch that should replay towardff restack, naming the policy.
Fixed¶
- Path positionals on
ff log,ff diff,ff show,ff restore,ff commit,ff absorb, andff liftresolved 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 withusage/no-such-path. ff restoreof a path that matches nothing exited 0 with an empty report. It now refuses withusage/no-such-path.- A conflicting
ff restackof a branch other than the current one, directly or throughff pull, replaced that branch's parked change with the current working tree, soff switchto it resumed the wrong files. ff doneon 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, andff resolvelays 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 withmerge/base:ff pullandff restackreplay onto it. A conflicting auto-merge records amergehold, exit 3, thatff resolveopens andff donelands.ff resolveon 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 namesff restack.--resolveonff restack,ff pull, andff mergeopens the resolution session in the same run when the branch underfoot conflicts, exit 3 still.fufu.onConflict(hold, the default, orresolve) makes it standing and--no-resolveholds for one run.ff difftakes 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 withusage/revset-not-a-range.- One view set on
ff diff,ff show, andff log:--stat,--name-only, and-U <n>, withff log -phanging each row's patch under it andff show --no-patchstopping at the message.ff log --bodyprints each row's message body. --fields <list>on every--jsonverb projectsdatato comma-separated dotted paths. A path that matches nothing is refused withusage/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 underflattened. 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, andff diff -rmeasure 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 gainsagainst.ff done --abandonon a resolution session closes the session and keeps the hold, soff resolveopens a fresh session over it. Dropping the hold too isff resolve --abandon's meaning.ff showprints the message whole, subject and body, and JSON gainsbody. The@row offf status,ff log, andff mapshows a multi-line pending description's subject only.
Fixed¶
ff doneon a resolution whose fix uncovered the next conflict refused withheld/unresolvedand 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. Oneff undosteps back a round.- A rewrite of a held branch left the hold standing, so
ff resolvelater replayed it onto a base it no longer had. A re-aim withff 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 withheld/already-held. ff diffandff showanswered a positional that names no path, such asmain..HEAD, with an empty patch and exit 0. Both now refuse it withusage/no-such-path.ff pullon 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. Afufu.jsfufu 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 asadditionalContext.ff hook qwen: Qwen Code, wired by five hook events merged into~/.qwen/settings.json. No skill.hook/failedfor a client file or plugin directory that could not be written or did not read back, andhook/malformedfor 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 readsCLAUDE_CODE_SESSION_ID.
Changed¶
ff hook codexwrites a plugin at~/.agents/plugins/fufu/with the skill inside, merges one entry into~/.agents/plugins/marketplace.json, and runscodex plugin add fufu@<marketplace>whencodexis onPATH, printing the command otherwise. Codex gainsSessionStart,Stop, andSessionEnd. 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/: thehooks.jsonentries,skills/fufu/, and the markedconfig.tomlblock.ff unhook codexremoves the plugin directory and the marketplace entry, and no longer opens~/.codex/config.toml.ff hook cursorwrites a user-local plugin at~/.cursor/plugins/local/fufu/with the skill inside, wiringpreToolUseonShell|Write|Delete,sessionStart, andsessionEnd; Cursor CLI discovers it on a new session. An install an earlier fufu merged into~/.cursor/hooks.jsonreads as wired and stale, andff hook -umigrates it.beforeSubmitPromptandstopare not wired: Cursor CLI 2026.09.10 gates them on user settings. Cloud agents get no user-local hooks.ff hook -l --jsonreports an unreadable client file as{"state":"unavailable","complaint":…}and a hand-written file as{"state":"hand-written","at":…}.- With signing enabled, the
@row onff status,ff log, andff showshows the open commit's sha, and@resolves to it; the close signs and lands a different sha, as it does forff commit -mor a partial close.open.pendingin--jsonfollows.
Removed¶
ff commit'sre-minted:line and theremintedfield 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 geminiandff unhook gemini: Gemini CLI's hook names diverged from the family, and~/.gemini/settings.jsonis never touched.ff trigger geministill captures and briefs for entries an earlier fufu wrote there.
Fixed¶
ff hook claudeon Windows wrote the binary's path unquoted, and Git Bash collapsed the backslashes, so every wired event exited 127 whileff hook -lstill said wired. The path is quoted now;ff hook claudeorff hook -urewrites an existing install.
v0.16.1 — 2026-09-14¶
Fixed¶
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.
v0.16.0 — 2026-09-14¶
Added¶
fufu.autoFetch: eligible commands refresh tracking refs at most once per cadence (10mby default), with a three-second fetch deadline and terminal prompts disabled. External helpers can still prompt, and some transport handshakes can exceed the deadline.falseorCIdisables automatic fetch;ff pulland explicit--fetchremain available.ff doctorreports the last refresh.- Global
--fetchand--no-fetch: force or skip a supported fetch; unsupported--fetchreturnsfetch/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-runpreviews deletion; capture, auto-fetch, and maintenance can still run.fufu.pruneGone: enables the same pruning during ff pull; defaults tofalse.- Seen records at
refs/fufu/seen/<branch>: reporting pulls, successfulff pushsends,ff switchcreating a remote-tracking branch, andff clonerecord the observed remote tip. Replacing or deleting remote history requires seen/tracking agreement; fast-forward pushes can proceed without it.
Changed¶
ff op trimreplacesff trim; the old spelling remains an unlisted alias and reportsop trimin JSON.- ff pull uses the global
--no-fetchflag with its existing meaning. - Fetches prune deleted remote-tracking refs. Doctor requests a fetch on every run when fetching is enabled.
ff undoafter ff branch -d restores the branch's timeline pointer, matchingff 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 waygit switchdoes; a name two remotes hold isbranch/ambiguous.- Bare
-bonff switch:ff switch main -bforks main onto an anonymous branch. --from <revset>onff absorband--into <rev>onff lift, with-mand--no-verifyon 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 HEADmoves 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 onff log,ff status, andff showis that commit's, andff commitmoves 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 are-minted:line. - The park is the open commit.
ff switchand its kin leave a dirty tree's change at that ref,git stash listno longer showsfufu: 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 resolvelays the change into the working copy. @^isHEADand@~nisHEAD~(n-1), so@~3..@is two commits plus the open change.ff startandff neware spellings offf switch, and every spelling is one operation: find the branch, else mint it.ff start <branch>continues the branch;-bforks.ff start @andff 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 absorbandff liftare one move and say the same thing:moved <n> file(s) from <sources> into <target>, then the restack and the drops.--jsoncarries the report undermove.- A dirty tree with no
user.namerefuses to switch away, since no open commit could stand as its park. ff hookandff unhookremove the MCP server registration an earlier fufu wrote forclaude,codex,cursor, andgemini.--jsonrenames:arrival.stategainsheldandlandedand losesstill_parked;openreplacesstashunderrestored,held,landed, andparked;open_leftreplacesparked_demoted;ff done --abandon'sleftreplacesstashed.
Removed¶
ff mcpand its registration in the four agent clients.- The declared extension side:
ff extension, the registry, the--ff-manifest,--ff-tools, and--ff-skillhandshakes, and theextension/*error ids.ff <name>still runsff-<name>from PATH. - The operation route:
route()in the op revset and therouteline and key onff op show. ff start's own help page and JSON envelope;ff help startis switch's page, and--jsonunder every spelling isswitch. TheabsorbandliftJSON keys; both verbs carrymove.
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.--staykeeps the branch on the result, and advances a target another worktree holds. A replay that would conflict isfold/conflictand changes nothing.--jsoncarries the report asfold.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.--jsoncarries it asbranch 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.--jsoncarrieschange_id,commit,operations, andsnapshots.
Changed¶
- The letters column on
ff log,ff status, the map, andff showis a change id, the identity a commit keeps through every rewrite fufu performs, in place of the op anchor.ff commitwrites it as jj'schange-idheader, and a commit made outside fufu derives one from its sha.--jsoncarrieschange_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 isusage/revset-divergent. Operation and capture ids are hex, twelve characters, like jj's, onff op log,ff evolog,ff history,ff undo,--at-op, and everyidthe JSON surface carries for an operation. A change id in an op slot isusage/rev-in-op-position, and an op id in a revision slot isusage/op-in-rev-position. ff switch <change id>redirects toff startat that commit, the wayff switch <sha>does.ff restackand the cascade drop a commit whose change id the base already holds:dropped … — superseded by <sha> in the base. JSONdroppedentries gainreasonand, undersuperseded,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>, andff undotakes 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 statusshows it on the base axis,ff branchshows no copy, and thebranch/aliased-copyrefusal goes. ff branch,ff worktree, andff extensiontake flags, not subcommands: bare lists, a positional creates or adds,-d/--deletedeletes or removes.--at,--at-op, and--allare the list's alone.ff git mergeruns underfufu.gitPolicy strict, the wayff git tagdoes, and the coaching line forgit mergenamesff pullandff git mergein place offf restack --onto.
Removed¶
id_lettersfromff status --jsonandff log --json;idis the operation's hex, and what--at-opreads.- The
list,create,delete,add, andremovesubcommand spellings underff branch,ff worktree, andff extension, and theusage/unknown-subcommandredirect fromff branch <name>toff describe -b.
Fixed¶
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, 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 pushon 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-idheader, and the commit comes back with a derived id. jj has the same limitation. - After
ff undothenff redoof 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>...andff 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 wayff restackresolves one.ff pull --allis 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-fetchbeside it reads what you already have. The exit is 3 when a branch would hold.ff status --jsoncarries the orientation an agent asks for first:root,worktree,basewith the count above it,remote, andlast_op.- Every operation carries its route,
shellortool, in its trailer: onff op showandff op log --json, and as theroute()filter in the op log's set language. - Every
ffinvocation readsCLAUDE_CODE_SESSION_IDwhen neither--sessionnorFF_SESSIONis set, so a shell verb under Claude Code carries the session its hook captures do;ff mcpreads it once at start. - jj's names as aliases:
ff bookmark,ff workspace,ff squash, andff rebaserunff branch,ff worktree,ff absorb, andff restack.ff abandonandff splitanswer withusage/foreign-verbnaming the moves that cover them. Every alias shows onff --help. - The manifest's
updateandbuildfields.ff updatewalks every declared extension after fufu by the rules it applies to itself, the passive release notice covers anofficialbuild with a github.comreleasesrecipe, andff extension addsays howff updatewill answer for the extension.update.jsoncarriesextensions. ff hook -urefreshes 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.shandinstall.ps1run it after placing the binary.ff hook --jsoncarriesextensions.ff doctor's Extensions floor reports a declared extension whose binary has moved past its record as that extension's own drift row, withff extension add <name>as the repair.
Changed¶
ff syncisff pullandff publishisff push; the old spellings stay as visible aliases. The error ids undersync/andpublish/arepull/andpush/, and the old ids resolve nowhere,ff explainincluded. The operation log, the JSON envelope'scmdanddatakey, and the countsff statusprints (N to pull,N to push) follow.- Bare
ff pullis the branch you stand on and the local bases beneath it, rather than every local branch;ff pull --allis the whole-repository run. ff mcpserves seven typed tools,status,pull,push,undo,redo,explain, andhelp, each taking the verb's own flags as fields and acwd, in place of the onefftool and its args array; every other verb is the shell.isErrorfollows 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 hasff mcpregistered.ff resolveopens a session branch the wayff editdoes, with the hold staying on the branch you left;ff donelands the fixes and returns in one operation, andff statuscarries the session underresolvingon both branches.- The manifest's
skillsfield names skills, andff hookasksff-<name> --ff-skill <skill>for each one's files, installed whole beside fufu's own; a failed handshake isextension/skill-failedorextension/bad-skill. A registry record from before reads as unreadable untilff extension add <name>rewrites it.undoableandverbs[].read_onlyare informational. ff update -yis 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 statuspins;ff status --jsonstill carries every ref. - A
git rebasetyped through the shell alias is coached towardff restackunderfufu.gitPolicy=coachand refused understrict.
Removed¶
fufu.toolPolicy, with the presence markerff mcpheld for it and theff trigger clauderefusal it drove.usage/mcp-verb-unavailable,usage/mcp-extension-undeclared,usage/mcp-extension-not-undoable, andusage/mcp-policy-write, with the sealed keys under the tool.ff co, the hidden alias on thecheckoutforeign 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 nextff undodeleted the files instead of stepping back.ff switchaway 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 removebeforeff hook claudeno 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
ffcounts a parked change's untracked files, so the map's(+ parked change, N)and the numberff switchprints 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 thatff synccovered one branch. - The refusal for an undeclared extension names
ff <name>as the command to run, where it showed only theff-<name>binary on PATH and left the shell exemption toff explain.
v0.12.0 — 2026-09-04¶
Added¶
- The cascade:
ff restack,ff sync,ff absorb,ff lift,ff describe <rev>, andff donereplay the branches stacked on the one they moved, inside the same operation. A branch that conflicts is held on its own;ff restackandff syncexit 3 when any did. ff synccovers 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 runsffin its shell while that tool is up.observe,coach, orstrict, the default.- Extensions can declare themselves.
ff extension add <name>records whatff-<name>says it is, and fufu then describes that verb to an agent everywhere it already speaks.docs/reference/extensions.mdis the reference. ff configrefuses a write tofufu.gitPolicyorfufu.toolPolicythrough thefftool, so an agent cannot lower the tier policing it.
Changed¶
ff <verb> --helpis shorter, and the pages take subheadings and lists.
Fixed¶
ff restackandff resolveon 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
contextis 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 doctorcompares a declared extension's recorded version, so an edit without a version bump drifts unreported. A changedmcp.argsis reported stale and not repaired by--fix.ff extension removebeforeff unhookleaves 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 idff explainknows, with its exit code and one-line meaning, generated from the registry.ff explain --jsonentries carryexit, the code the id exits with, andff explain --listprints it as a column. prepare-commit-msgandpost-commit, the two commit-time hooks fufu did not implement.ff commitnow runs all four:pre-commit,prepare-commit-msg,commit-msg,post-commit. Every commit hook runs withGIT_EDITOR=:, as git sets it for a command that will not open an editor.--no-verifyonff absorb,ff done, andff describe <rev>, the verbs that can now be declined by a hook.ff hook powershell.$PROFILEgetsfunction git { ff git @args }and a wrappedprompt, with the same marker and the sameff unhookas 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$SHELLispwsh.
Changed¶
ref/contendedexits 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 updatenames the command that updates this copy of fufu instead of downloading a binary over itself:cargo installfor a source build,brew upgrade fufufor 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-yor a typed yes, and-yon 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.updateCheckand still lands a one-line notice, but nothing installs itself any more. The notice names whichever command owns the binary. install.shandinstall.ps1land the new binary beside the old one and rename, rather than writing over it, so they can replace anffthat 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-onlyzip.
Fixed¶
- A CRLF rc file keeps its line endings through
ff unhookand through the retired-spelling rewriteff hookdoes; both rejoined the file with LF. ff absorb,ff done, andff describe <rev>ran no hooks at all, so content apre-commitgate would decline on a close landed in the commit anyway.pre-commitnow runs forff absorband for both offf done's landings — the edit session and the resolution — over the index staged with exactly what is landing, and the message hooks run forff describe <rev>and for anff donewhose session carries a new description.ff lift,ff restackandff syncstill run none, matchinggit rebase.ff syncfailed its whole run when a linked worktree's admin dir under.git/worktrees/held agitdirfile without a readablecommondir— 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 throughgit 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 doneunable to land any resolution: the blockff resolveshowed was no longer the block the step that owned it had written, and every fix was refused withno marker block to resolve at <path>. The chain now stops at that fold, the same way it stops when two conflicts interleave, soff resolveshows the mark as its owning commit wrote it andff donelands 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 commitaccepts-Sand--no-sign. ff logandff statusmark signed commits;ff log --signaturesverifies them and reports the verdict, tool, and key;ff showverifies the commit it prints;ff doctorreports whether the signing setup will work.fufu.gitPolicy, with levelsobserve,coach(default), andstrict, covering bothff gitand a baregitrun inside an agent's shell tool.ff doctorreports the tally.
Changed¶
ff --helpgroups its commands under the same headingsgit helpuses, instead of one alphabetical list.ff -hshows 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
StopandSubagentStopcapture events.
Removed¶
fufu.translate, replaced byfufu.gitPolicy. Command translation is gone entirely: fufu will not run a different command than the one given.
Fixed¶
ff trimreported 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 statusno 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, andff worktree list— making and taking a worktree are operations,ff undoreverses 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 watchsubscribes to the operation log, andff watch --allstreams every worktree in the repository on one tick.ff collidereports which branches would hit each other, backed by a new sideways-comparison axis in the core.ff -C <dir>runs fufu against another directory, andff <name>reaches for anff-<name>extension on PATH.ff restackmoves 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 diffshows the open change as a patch,ff showrenders one revision with header and patch,-pjoins 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 synctakes in,ff publishsends, publish remembers the push so sync stops reversing your undo,--dry-runpreviews, and each status count names the verb that clears it. ff initandff clonemake the starting point fufu's own;ff versionreplaces the uppercase flag.ff historyshows the moves rather than the operations,ff op logshows 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 opfamily arrives andff undosteps by runs; naming a branch isff describe -b. - Sessions become named spans of the capture chain — list them, diff them, group the log by them.
- The machine surface: every
--jsonoutput carries a versioned envelope, errors carry stable ids and exit codes carry the verdict, andff explaindocuments them;ff statuscomputes 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 startbegins new work on a fresh branch,ff helpgets a written page for every verb,ff doctorverifies the net,ff updateself-updates on two lanes,ff configspeaks 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, plusff evologfor 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 loghops 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 --versionnames the build it came from; a cargo-deny gate enforces permissive-only dependency licenses. ff statusshows one file list, one themed palette is chosen byfufu.theme, and usage lines sayffon 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 statusandff logwith human and--jsonoutput, a permanent differential test harness against the real git binary, and a zero-spawn latency proof. - Phase 1, capture: bare
ffsnapshots 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, andff 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.