feat(import): let the user choose leave, replace, or merge - #96
Merged
Conversation
Import decided for you. A copy it could prove untouched was offered as a replacement; one it could not was pushed into a merge with no way out. Both rulings answered a question the evidence cannot reach: whether the material a replacement would discard was worth keeping. Only the person who wrote it knows that, and the two wrong answers cost a week of work or an hour of merging nobody needed. So `replace` and `merge` collapse into one `reconcile` action that prices each answer and takes none of them. Identity is still decided here — `create`, `current`, `choose`, and `unlinkable` are unchanged — because who a bundle is is a fact, and what your work is worth is not. Replacing gets its own `--replace` flag rather than riding on `--yes`. The old merge path deliberately printed no `--yes` at all, so that a diverged copy could never be overwritten by one key; that invariant survives in a stronger form, since `--yes` alone is now not an answer to anything. A confirmation that forgot its `--merged-from` re-prints the choice instead of overwriting the merge it was meant to apply. Divergence splits into `divergedBy: "edits" | "unproven" | null` along the way. The boolean conflated a copy with local work in it against a copy whose baseline is merely missing — after an identity-only adoption, the second was being told its work was at risk when it may never have been touched. Known limit: a save landing in the context between the offer and `--replace --yes` is discarded without a fresh warning, since the base hash is recomputed inside the applying run. The two-step `--replace` then `--replace --yes` path re-prints the current cost line for anyone who wants the re-check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diff coverage caught it: the guard I put on the reconcile branch needed an else, and the resolver has no sixth answer to send there. The old shape had the last action fall through unguarded for exactly this reason, so it does again — dead code that reads like defensiveness is worse than the invariant stated in a comment, because nothing ever proves it still holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Import decided for you. A copy it could prove untouched was offered as a replacement; one it could not was pushed into a merge with no way out. Both rulings answered a question the evidence cannot reach — whether the material a replacement would discard was worth keeping — and the two wrong answers cost a week of work or an hour of merging nobody needed.
replaceandmergenow collapse into onereconcileaction that prices each answer and takes none of them:Identity is still decided here.
create,current,choose, andunlinkableare unchanged, because who a bundle is is a fact and what your work is worth is not.Three decisions worth reviewing
All three answers are offered uniformly, including on a copy that is provably untouched — where merging can only reproduce the bundle. Withholding it there would be deciding again one level down, so it is offered and its pointlessness is stated: "replacing discards nothing and a merge could only reproduce the bundle."
Replacing gets its own
--replaceflag rather than riding on--yes. The old merge path deliberately printed no--yesat all —import-reconcile.test.mjsasserted "a merge is never offered as a one-key overwrite". That invariant survives in a stronger form:--yesalone is no longer an answer to anything, so a confirmation that forgot its--merged-fromre-prints the choice instead of overwriting the merge it was meant to apply. Applying is--replace --yes; bare--replacegives a second confirmation screen.Divergence splits into
divergedBy: "edits" | "unproven" | null. This was a real bug the change surfaced: the boolean conflated a copy with local work in it against a copy whose baseline is merely missing. After an identity-only adoption, the second was being told "This copy has been edited here since it arrived" when it may never have been touched. Three states, three sentences — and the docs now tell each host not to report the unproven one as edits.Known limit
A save landing in the context between the offer and
--replace --yesis discarded without a fresh warning, since the base hash is recomputed inside the applying run. Previously such an edit would have flipped the action tomergeand the--yeswould have been ignored. The two-step--replace→--replace --yespath re-prints the current cost line for anyone who wants the re-check; requiring an echoed-back hash on every replace seemed a heavy toll for the common lossless case, but that is a reviewable call.Scope
shared/core/{context-store,import-commands}.mjs, synced into all five host packages.--replacewired into the four host CLIs (pi ships no import surface). Four host command/skill docs, both READMEs, andtests/import-reconcile.test.mjs.Verification
npm run check— clean, including the packaged-core staleness checknpm run e2e:commands— 58/58 loadable across 5 pluginsnode --test— 573 passing, 0 failing--yesalone declining to write, the--replaceconfirmation, and the applied replacement🤖 Generated with Claude Code