Upgrading
Since 0.7.0: fairlead migrate brings a repository to the release you
just installed. It updates what an older release left behind: the hooks
hooks install wrote, the config’s fairlead version floor, and the
version pins in package.json and the workflows. It then lists the changes
since your release that need a person to decide.
Each step looks at what the repository has, not at which version it came from. So one run catches up from any older release, and a second run finds nothing to do.
Upgrade in five steps
-
Install the new release the way you installed the old one:
bun add -d fairlead@latest # or npm, pnpm, yarn; this updates package.json and the lockfile # or the shell or PowerShell installer from Install -
See what would change. Nothing is written yet:
fairlead migratemigrate: from ^0.4.2 (package.json) to 0.7.0 would update .claude/settings.json: Fairlead's hooks: adds the brief nudge and the Stop hook would update lefthook.yml: the commit stage runs `bun x fairlead guard check --staged` instead of `fairlead guard check --staged` would update fairlead.toml: `fairlead = "0.4"` becomes "0.6": the config uses `done`, which 0.6 added would update .github/workflows/ci.yml: Fairlead's action at v0.4.2 becomes v0.8.0 review (0.5.0): `plan.ignore` applies to source files too: ... review (0.6.0): `hooks install` adds a Stop hook, ... migrate: dry run; `fairlead migrate --write` makes these 4 change(s) -
Read the
reviewlines. They come from Release by release below, filtered to the releases since yours and to what applies to this repository. Settle each one before you write: for example, setbrief.nudge = falsefirst if you don’t want the nudge added. -
Write the changes:
fairlead migrate --writeIf it moved Fairlead’s version in
package.json, it says which command updates the lockfile (bun install,pnpm install,yarn installornpm install). Run that command; migrate never edits a lockfile. -
Check the result and commit it:
fairlead doctor fairlead config check fairlead migrate --check # exits 0 once nothing is left
--from VERSION names the release you came from, when migrate can’t tell
or guesses wrong. It reads the release from package.json, then from a
workflow’s pin, then from the config’s floor.
What migrate changes
| What | When it changes | How |
|---|---|---|
Claude Code hooks in .claude/settings.json and settings.local.json, and, since 0.9.0, Codex hooks in .codex/hooks.json and Gemini CLI hooks in .gemini/settings.json | Fairlead’s entries aren’t the ones this release’s hooks install would write for your config | Fairlead’s entries are replaced with this release’s, and every other setting and hook stays as it was. Uninstall still restores the file from before Fairlead, byte for byte, when nobody else changed it. |
The commit stage in lefthook.yml | It runs another command than this release writes, such as fairlead by name where the project has its own copy | The run: line is replaced in place, with its indentation, quotes and comments kept |
The fairlead = "X.Y" floor in the project config | The floor is older than a table or key the config uses, such as [done] (0.6) or [guard] (0.4) | The floor is raised to that release, so a teammate on an older binary is told which version to install |
package.json | dependencies, devDependencies or optionalDependencies pins an older release, as 0.4.2, ^0.4.2, ~0.4.2 or >=0.4.2 | The version moves to this release and keeps its ^ or ~ |
Workflows under .github/workflows/ | A step uses mahkassem/fairlead@vX.Y.Z, or passes version: vX.Y.Z, for an older release | Both move to this release |
Since 0.11.0: .github/workflows/fairlead-insights.yml | ci workflow --insights wrote it (it starts with the generated-file marker) and this release writes it differently, as 0.11.0 does with the scopes replay fetch needs | It’s rewritten as ci workflow --insights --write would. A file without the marker is the team’s own and is left alone. |
What migrate never does:
- It never installs anything or runs a package manager, and never edits a lockfile.
- It never adds hooks to a repository that has none; that’s
hooks install. - It never adds a floor to a config that has none, never changes a setting that decides what a plan selects, and never moves a pin to an older release.
- It leaves pins by branch, by commit or
latestalone.
In CI
fairlead migrate --check writes nothing and exits 1 when anything would
change, so a dependency bot’s version bump can’t be merged half done:
- run: npx fairlead migrate --check
It exits 0 when the repository matches the installed release, and 2 when the config doesn’t load or a file can’t be read.
Release by release
Every release with something to do, oldest first. Plan ids change with every release, so a plan made by one release isn’t reused by another.
To 0.4.0 (from 0.3.0)
[[guard.external]]refusesstages = ["write"], because the file isn’t written yet when the write hook runs. Give such a rulecommitorci.- A plan’s config digest leaves out
guardandhooks, so changing only those keeps plan ids. Nothing to do.
To 0.5.0
Migrate updates:
- The hooks of a repository that lists Fairlead as a package dependency, so
they run the project’s own copy instead of
fairleadon thePATH.
For you to decide:
plan.ignoreapplies to source files too. A changed source that nothing imports and that matches it now selects nothing, where it used to widen to every test. Under the defaults that’sdocs/**.- An owner rule whose
matchnames no test file no longer counts as covering its paths, so they fall totests.unreached.config checkwarns about each one; a rule meant to select nothing should be aplan.ignoreentry instead. key = []over a list that already has items changes nothing, because lists append.{ replace = [] }clears it, andconfig checkwarns where a layer does this.- A Go module’s
go.modandgo.sumare graph edges instead ofplan.run_alldefaults, so a dependency bump selects that module and its importers. If your config lists them inplan.run_allitself, it still runs everything on them; drop them to get the narrower plan. - Go, Python, PHP, Vue, Svelte and Astro files are in the import graph. A
repository that ran a whole suite through a
[[checks]]entry can give its tests a runner instead (Import graph). - Replay datasets fetched before 0.5.0 keep no lines for a bun test file that failed to load. Fetch them again to count those failures.
To 0.5.1
- A changed path that matches
plan.ignoreand reaches no test is ignored even when other files import it. In 0.5.0 it went totests.unreached. Plans for such changes get smaller; nothing to do.
To 0.6.0
Migrate updates:
- Hooks installed before 0.6.0 get the Stop hook and the brief nudge, which
hooks installadds since 0.6.0.hooks installitself leaves existing hooks alone, so a repository that only re-ran it kept the old set.
For you to decide:
- The Stop hook sends an agent back while
fairlead donehasn’t passed for the tree it leaves (The done gate). Setdone.on_stop = "off"beforemigrate --writeto leave it out, or"require"to ask every time. - The nudge notes an edit made with no brief (The brief). Set
brief.nudge = falseto leave it out.
To 0.7.0
Migrate updates:
- The config’s
fairleadfloor, to 0.7, where the config uses[[quarantine]].
For you to decide:
- Java and Kotlin files are in the import graph. A Maven or Gradle build that
ran everything through a
[[checks]]entry can give its tests a runner instead, with{class}or{classes}in its command (Java and Kotlin). fairlead migraterefreshes Claude Code’s hooks. Hooks for Codex (hooks install --codex) and Gemini CLI (hooks install --gemini) are new in 0.7.0, so there’s nothing older to bring along.
To 0.7.1
Migrate updates:
- The config’s
fairleadfloor, to 0.7.1, where a runner usesall_commandorexclude_arg.
For you to decide:
- A runner whose tool, given no files, finds more tests than its
matchclaims (a barebun testthat also finds end-to-end specs) can setall_commandfor the run of everything. A runner holding a test in[[quarantine]]can setexclude_argso that run stays whole (Configuration).
To 0.8.0
Migrate updates:
- Claude Code hooks installed before 0.8.0 get
Skillin the edit hook’s matcher, so a skill loaded through theSkilltool counts as a use, and aSessionStarthook that runsfairlead resume --hook. Setbrief.resume = falsebeforemigrate --writeto leave that one out (Context and resume). - The config’s
fairleadfloor, to 0.8, where the config uses[memory],[skills],[agents]orbrief.resume.
For you to decide:
- Lessons, skill routes and the AGENTS.md block stay off until a repository
adds them.
fairlead import lessonsturns a lessons document a team already keeps into lesson files, andfairlead import rulesturns path-scoped rule files into routed skills (Lessons, Skills). - CI stages stay off until the config sets
[stages]or a runner’s or check’sfrom. Thenci planreads the stage from the GitHub event, a merge skips what its pull request passed on the same tree, andfairlead ci workflowwrites a staged workflow or prints theif:lines for one you keep (Stages). - Three kinds of change no longer select everything: a bun dependency’s
version bump runs the tests of the files that import it, an edit to
fairlead.tomlplans with the new config, and a workflow that only runs by hand or on a schedule selects nothing by itself (The test plan). migraterefreshes Claude Code’s hooks only. Gemini CLI hooks installed by 0.7 don’t matchread_file, so reading a SKILL.md there isn’t counted as a use untilfairlead hooks uninstall --geminiandhooks install --geminiwrite this release’s.
To 0.12.0
Migrate updates nothing in your files.
For you to decide:
- The
nextpack’s end-to-end rule, every spec that callspage.goto(reaching every route andmiddleware, is its own pack now. Add"next-e2e"toextendsto keep it, or leave it out when your owner rules already map pages to specs (Framework packs, #212). - With more than one runner, a runner whose folder holds another runner’s
tests names its own when everything runs, so its command line lists
them;
all_commandkeeps the tool’s own discovery (Planning, #210). - Under Bun,
"trustedDependencies": ["fairlead"]hasbun installdownload the binary instead of the first command (Install).
To 0.11.0
Migrate updates:
.github/workflows/fairlead-insights.yml, whenci workflow --insightswrote it: it’s rewritten with thepull-requestsandchecksread scopesreplay fetchneeds (#201).
For you to decide:
- Sessions in a linked worktree count now: every worktree writes to the
main checkout’s event log, so
doctor --score,insightsandtokens reportmay show more than before. - A Next.js app can add
extends = ["next"], whichfairlead initnow names (Framework packs). - A workspace can declare
[[workspace.edges]]between its repositories (Edges across repositories). - Building from source needs Rust 1.95.
To 0.10.0
Migrate updates nothing in your files: every new table is optional.
For you to decide:
- Replay sets recurring failures aside: a test failing in one job across
three or more unrelated pull requests gets the outcome
recurringand leaves adjusted recall, so a gate can pass that failed before. Confirm the[[replay.quarantine]]entries it lists (Recurring failures). fairlead doctor --scorerates the setup out of 100 and names the three fixes worth the most (Readiness score);fairlead ci workflow --insights --writeadds the weekly summary (Insights).fairlead mcpserves read-only tools to an agent (MCP server),[tracker]puts a branch’s task in the brief (Trackers), andci.escapes = "file"opens an issue per escape (CI).
To 0.9.0
Migrate updates:
- Codex hooks in
.codex/hooks.jsonand Gemini CLI hooks in.gemini/settings.json, whichmigratenow refreshes as it does Claude Code’s. Gemini CLI hooks from 0.7 getread_filein the nudge’s matcher, so a SKILL.md read there counts as a use; the uninstall and install the 0.8.0 notes asked for is no longer needed.
For you to decide:
- A project whose pushes run the plan and whose full suite runs on a
schedule can add
--event scheduleto itsreplay fetch, so replay counts what the nightly caught and no push ran (Scheduled runs). fairlead graph whynames edge kinds in words (type import,path literal) where it printedtypeimportandpathliteral; a script that reads its output needs the new names.
Going back to an older release
Install the older release, then run its hooks uninstall and
hooks install, so the hooks are the ones it writes. A config whose floor
names a newer release than the binary fails to load with the release it
needs; lower the floor only if the config uses nothing newer.