ADR-179: Remove the pre-1.0 in-place migrator; keep the guards and the transition fallbacks¶
Summary¶
- Decided: remove the pre-1.0
ways migratecommand and its code. Keep the in-place guards and the path fallbacks. - Trades away: an in-place upgrade from a pre-1.0 install. Those installs are pointed at the release tag where the migrator still lives.
- One-way? No. The migrator stays reachable at its tag, and the guards name where.
- Probes: Confident: the guards still catch a legacy install, since they were kept. Not confident: whether any pre-1.0 install is still in use and would hit the escape hatch.
- Inversion: one end keeps the migrator forever, compiled in. The other end also removes the guards and fallbacks. This removes the command and keeps the safety net. Is that the right cut line?
Context¶
ADR-144 §5 shipped ways migrate (plan / --what-if / --execute) to move a
pre-1.0 in-place ~/.claude clone onto the 1.0 projection model, and gave it an
explicit deprecation lifecycle: dormant at 1.0.0, escalating SessionStart
pressure across 1.0.x, removed at 1.1.0. The escape hatch was named in the same
section — the migrator lives forever at the last tag that ships it, so removal
never means a user cannot migrate.
The removal has been deferred twice. 1.1.0 shipped with the migrator still
present; 1.2.0 (691b0e6) corrected the docs and moved the window to 1.3.0.
That target passed too. The line is now at 1.8.3 and the migrator is still
compiled in — roughly 1,000 lines across migrate.rs and migrate_exec.rs,
plus the CLI variant, a reconcile parameter that exists only for it, and
migration instructions in the README, the install guide, install.sh, the
deployment way, and the ways-update skill.
The deferrals bought adopter time. They have also left the escalation curve's
end state unreached: eight minor releases past the announced window, every
surface still presents ways migrate as a live command, and the deprecation
notices in those surfaces name removal targets (1.1, 1.3) that came and went.
ADR-144 §5 also drew a second line that the removal must respect. Its "the cliff
is a ramp" clause says removing the migrator removes assisted migration, not
function: a post-removal binary on an un-migrated ~/.claude still reads
correctly through the transition fallbacks — cache from the legacy
claude-ways dir, events from ~/.claude/stats. Those fallbacks live in
ways-core/src/paths.rs and are cheap, non-destructive, and inert on a current
install.
Two guards share the migrator's detector, reconcile::is_legacy_in_place.
ways reconcile refuses to run against an in-place clone because projecting
symlinks over a live repo would strand the user's checkout; ways update
refuses for the same reason. Both currently route the user to ways migrate.
ADR-144 §5 listed "migrator and in-place check removed" as one step. Removing
the checks would let ways reconcile clobber an in-place clone — an actively
destructive outcome for exactly the users the never-strand clause protects.
Decision¶
Remove the migrator. Keep the in-place guards and the transition fallbacks.
Removed:
tools/ways-cli/src/cmd/migrate.rsandtools/ways-cli/src/cmd/migrate_exec.rs, theirmoddeclarations, theCommands::Migrateclap variant, and its dispatch arm.ways_core::paths::cache_root_canonical, which exists solely as the migrator's rename destination. Runtime reads use the fallback-awarecache_root.reconcile::run'sallow_in_placeparameter and the bypass it gates. The migrator is the only caller that ever passedtrue; with it gone the guard is unconditional.- The migration walkthrough in
docs/migration-1.0.md, theways migrateinvocations inscripts/install.sh,skills/ways-update/SKILL.md, thedeploymentway, README, anddocs/install-guide.md.
Kept:
reconcile::is_legacy_in_placeand both guards. Their messages retarget from "runways migrate" to the tag escape hatch. A user who reaches these guards is told what their install is and where the migrator still lives.- The
paths.rstransition fallbacks —LEGACY_CACHEresolution inresolve_cache, the~/.claude/stats/events.jsonlfallback inresolve_events, and theevents_log_sourcesunion (ADR-153 §1). These are the ramp ADR-144 §5 promised. The union also recovers orphanedsession_startlines on migrated installs whose shell hooks predated the path fix, so it earns its keep independent of the in-place case. - The legacy
~/.claude/ways.jsondisabled layer, a lower-precedence config read with no coupling to the migrator.
Escape hatch: ways-v1.8.3 is the last tag shipping the migrator. Migrating
after removal means git clone --branch ways-v1.8.3 … && ways migrate --execute,
then updating. docs/migration-1.0.md is rewritten around that route rather than
deleted, and the guards point at it.
Removal lands in 1.9.0.
Consequences¶
Positive¶
- About 1,000 lines of transitional code leave the binary, along with a
destructive code path (whole-
~/.clauderelocation) that no current install exercises. reconcile::runloses a boolean parameter whose only non-default caller is being deleted, so the in-place guard becomes unconditional and the function's contract simplifies.- Every user-facing surface stops advertising a deprecation target that has already passed. The migration story becomes one route (the tag) instead of a live command shadowed by stale removal dates.
Negative¶
- A pre-1.0 adopter who has not migrated by 1.9.0 now has a two-step path (clone the tag, migrate, update) instead of one command. This is the cost ADR-144 §5 accepted when it named the tag as the escape hatch.
- The migrator's crash-safe phase machinery and its tests go with it. Reviving the capability would mean recovering it from the tag.
Neutral¶
- ADR-144 §5's lifecycle is executed, not superseded — its escalation dates were the cadence, not the contract. This ADR records the divergence on one point: the in-place check stays.
- The
deploymentway keeps its legacy-in-place branch. The detection guidance is still correct; only the remedy changes.
Alternatives Considered¶
- Remove the guards too, as ADR-144 §5 literally specified. Rejected: with
the guard gone,
ways reconcileon an in-place clone projects over a live git repo and strands the checkout. Deleting assistance is within the lifecycle; adding a destructive path is not. - Remove the
paths.rstransition fallbacks in the same change. Rejected for now: they are inert on a current install, and dropping them turns "reads correctly, unassisted" into "silently re-fetches the model and stops reading its own stats." Theevents_log_sourcesunion has a second justification that outlives the in-place case. If these come out, it is as their own decision with its own reasoning. - Defer again to 2.0.0. Rejected: two deferrals have already passed with no signal that a third would be used differently, and each one leaves the shipped docs quoting a removal date that has expired.
- Keep the migrator indefinitely as a dormant command. Rejected: it carries a destructive code path and a phase machine that no test run outside its own fixtures exercises, and its presence is why the docs still carry a transition narrative eight releases past 1.0.