Skip to content

feat(import): let the user choose leave, replace, or merge - #96

Merged
tanglearncode merged 2 commits into
mainfrom
feat/import-choose-how-to-reconcile
Aug 23, 2026
Merged

feat(import): let the user choose leave, replace, or merge#96
tanglearncode merged 2 commits into
mainfrom
feat/import-choose-how-to-reconcile

Conversation

@tanglearncode

Copy link
Copy Markdown
Contributor

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.

replace and merge now collapse into one reconcile action that prices each answer and takes none of them:

Import action: reconcile
"Team Checkout" came from this bundle, and the bundle has moved on since this copy was taken.
  Their revision: 2 (you last took revision 1)
  Domain profile: changed
  Routing description: unchanged
  Knowledge files: 1 added, 1 updated, 0 removed
    Add: runbook.md
    Update: session-summary.md
This copy has been edited here since it arrived, so replacing discards that work.
Choose one — nothing here is written until you do:
  leave it
      Stop here. This copy stays exactly as it is.
  replace
      Take the bundle whole: re-run this import with --replace --yes.
  merge
      Keep both sides: reconcile them into one copy, then apply it with --merged-from.
Merge inputs, needed only if you choose merge:
Context name: Team Checkout
Context id: context:team-checkout-57b5b99bd929
...

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.

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 --replace flag rather than riding on --yes. The old merge path deliberately printed no --yes at all — import-reconcile.test.mjs asserted "a merge is never offered as a one-key overwrite". That invariant survives in a stronger form: --yes alone is no longer an answer to anything, so a confirmation that forgot its --merged-from re-prints the choice instead of overwriting the merge it was meant to apply. Applying is --replace --yes; bare --replace gives 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 --yes is discarded without a fresh warning, since the base hash is recomputed inside the applying run. Previously such an edit would have flipped the action to merge and the --yes would have been ignored. The two-step --replace--replace --yes path 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. --replace wired into the four host CLIs (pi ships no import surface). Four host command/skill docs, both READMEs, and tests/import-reconcile.test.mjs.

Verification

  • npm run check — clean, including the packaged-core staleness check
  • npm run e2e:commands — 58/58 loadable across 5 plugins
  • node --test — 573 passing, 0 failing
  • Hand-run of the full reconcile flow against the Claude Code CLI: the offer, --yes alone declining to write, the --replace confirmation, and the applied replacement

🤖 Generated with Claude Code

tanglearncode and others added 2 commits August 23, 2026 03:52
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>
@tanglearncode
tanglearncode merged commit fd4fe3a into main Aug 23, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant