Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,19 +199,22 @@ Import a context bundle shared by someone else. The shared folder is only ever
read: importing makes your own local copy and never writes back to it.

A bundle you already imported can be imported again — that is how you pick up a
teammate's newer work. Import recognises the copy it gave you and says what
taking the update would cost, rather than building a second context beside it:
teammate's newer work. Import recognises the copy it gave you, rather than
building a second context beside it:

- Nothing new in the bundle, and it says so.
- Your copy untouched since it arrived, so the newer one replaces it whole once
you confirm. It stays the same context, so a session connected to it picks the
- The same context with newer material upstream, so it shows you what changed,
tells you what a replacement would cost here, and asks which you want: leave
your copy alone, take theirs whole, or reconcile the two into one. Whichever
you pick, it stays the same context, so a session connected to it picks the
material up immediately.
- Both copies changed, so the two are reconciled into one and previewed before
anything is written. Your work is never dropped in favour of theirs.
- A name already taken by a context with no shared origin, which import will not
guess about: it asks whether the two are the same context or a collision, and
waits.

Import decides who a bundle is; it never decides what your work is worth.
Nothing is written until you say which answer you want.

A context is never deleted to make room for an imported one.

After importing, connect it with `/neatcontext:use <name>`.
Expand Down
15 changes: 9 additions & 6 deletions codex-marketplace/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,19 +164,22 @@ Import a context bundle shared by someone else. The shared folder is only ever
read: importing makes your own local copy and never writes back to it.

A bundle you already imported can be imported again — that is how you pick up a
teammate's newer work. Import recognises the copy it gave you and says what
taking the update would cost, rather than building a second context beside it:
teammate's newer work. Import recognises the copy it gave you, rather than
building a second context beside it:

- Nothing new in the bundle, and it says so.
- Your copy untouched since it arrived, so the newer one replaces it whole once
you confirm. It stays the same context, so a session connected to it picks the
- The same context with newer material upstream, so it shows you what changed,
tells you what a replacement would cost here, and asks which you want: leave
your copy alone, take theirs whole, or reconcile the two into one. Whichever
you pick, it stays the same context, so a session connected to it picks the
material up immediately.
- Both copies changed, so the two are reconciled into one and previewed before
anything is written. Your work is never dropped in favour of theirs.
- A name already taken by a context with no shared origin, which import will not
guess about: it asks whether the two are the same context or a collision, and
waits.

Import decides who a bundle is; it never decides what your work is worth.
Nothing is written until you say which answer you want.

A context is never deleted to make room for an imported one.

After importing, connect it with `$neatcontext:use <name>`.
Expand Down
12 changes: 8 additions & 4 deletions codex-marketplace/plugins/neatcontext/skills/import/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,20 @@ The source bundle is read-only throughout. Never modify, move, or delete it.
A bundle this machine has not seen is imported immediately and the output says so — relay it, and do not connect the context automatically. Otherwise follow the printed `Import action`:

- `current` — the context here already holds everything in the bundle. Relay that and stop.
- `replace` — the local copy came from this bundle and has not been edited since, so the newer copy can be taken whole. Relay the preview, ask the user to confirm, and only then rerun the same command with `--yes`.
- `merge` — both copies have changed. Reconcile them yourself, below.
- `reconcile` — the bundle is a newer copy of a context already here. Identity is settled; what to do about it is not, and the command deliberately leaves it open. Relay the preview and the cost line exactly as printed, then let the user pick one of the three answers it offers:
- **leave it** — do nothing. Stop; nothing has been written.
- **replace** — take the bundle whole, discarding whatever only the local copy holds. Rerun the same command with `--replace --yes`.
- **merge** — keep both sides. Reconcile them yourself, below.
- `unlinkable` — the bundle carries no context id, so it cannot be tied to anything already here. Relay that, and offer `--name "<new-name>"` to bring it in as its own context.
- `choose` — the target is not decidable. Either a context of the same name is here but nothing records a shared origin, or several contexts are copies of this bundle because one was forked. Relay the options and stop until the user picks: rerun with `--into "<name>"` to name the context they mean, or with `--name "<new-name>"` to keep a separate copy.

Never answer `choose` on the user's behalf. Two people naming a context the same thing is not evidence that it is the same context, and the two answers are not recoverable from each other.
Never answer `choose` or `reconcile` on the user's behalf. Two people naming a context the same thing is not evidence that it is the same context, and only the user knows whether the material a replacement would discard was worth keeping. None of these answers is recoverable from the others.

Relay the cost line rather than summarising it, because the three it can print mean different things. "Nothing here has been edited" means replacing is lossless and a merge could only reproduce the bundle — say so, and still let the user choose. "This copy has been edited here" means replacing destroys that work. "Nothing here records what the two copies once had in common" means the copy may be untouched and cannot prove it; do not report that one as edits.

## Merging

Use the exact `Context name`, `Context id`, `Base hash`, `Bundle hash`, `Profile path`, `Knowledge folder`, `Bundle profile`, and `Bundle knowledge` values the command printed. The three hashes are what prove the merge is for this context, was built on its current contents, and consumed this version of the bundle; a merge that gets any of them wrong is refused rather than applied. Read the local profile and every file in the local knowledge folder, then read the bundle's profile and every file in its knowledge folder.
Use the exact `Context name`, `Context id`, `Base hash`, `Bundle hash`, `Profile path`, `Knowledge folder`, `Bundle profile`, and `Bundle knowledge` values printed under `Merge inputs`. The three hashes are what prove the merge is for this context, was built on its current contents, and consumed this version of the bundle; a merge that gets any of them wrong is refused rather than applied. Read the local profile and every file in the local knowledge folder, then read the bundle's profile and every file in its knowledge folder.

Merge them the way a save merges a conversation into an existing context:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ async function commandImport(flags) {
name: typeof flags.name === "string" ? flags.name : "",
into: typeof flags.into === "string" ? flags.into : "",
mergedFrom: typeof flags["merged-from"] === "string" ? flags["merged-from"] : "",
replace: flags.replace === true || flags.replace === "true",
confirmed: flags.yes === true || flags.yes === "true",
consume: flags.consume === true || flags.consume === "true",
useCommand: "$neatcontext:use"
Expand Down
34 changes: 25 additions & 9 deletions codex-marketplace/plugins/neatcontext/src/core/context-store.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1156,18 +1156,34 @@ export async function resolveImportTarget({ bundleFolder, into }) {
return { ...base, action: "current" };
}

// They have moved, so this is about what taking it would cost here. No
// baseline means no way to prove this copy is untouched, and replacing an
// edited copy loses the edits — merge is the answer whenever it cannot be
// ruled out.
// They have moved, so this is about what taking the bundle would cost here —
// and the cost is reported rather than acted on. Which way to take it is not
// a fact about the two copies but a judgement about whose material matters,
// and only the person who wrote the local half can make it. Deciding here
// means either overwriting work nobody offered to sacrifice or demanding a
// merge over edits the user would have thrown away without a second thought.
//
// A baseline left by a different origin does not count, which is what makes
// adoption safe: saying two contexts are the same does not make this copy's
// material disposable, and it came from somewhere else entirely.
// What the evidence does settle is the price of each answer, and that travels
// with the resolution. No baseline means no way to prove this copy is
// untouched, so it is reported as diverged: unprovable and edited cost the
// same to replace.
//
// A baseline left by a different origin does not count either, which is what
// keeps adoption honest. Saying two contexts are the same does not establish
// that they ever shared contents, so an adopted copy's material is never
// assumed disposable.
//
// Why it diverged is carried out separately from whether it did, because the
// two reasons cost the same and read nothing alike. A copy with edits in it
// has work to lose; a copy with no usable baseline may be byte-identical to
// the bundle and simply unable to prove it. Saying the second one has been
// edited would be inventing a history, which is the one thing this whole path
// exists to avoid.
const baseline =
record.importedFrom?.id === bundleId ? record.importedFrom.fingerprint : null;
const diverged = typeof baseline !== "string" || baseline !== baseHash;
return { ...base, action: diverged ? "merge" : "replace" };
const divergedBy =
typeof baseline !== "string" ? "unproven" : baseline === baseHash ? null : "edits";
return { ...base, action: "reconcile", diverged: divergedBy !== null, divergedBy };
}

// The fast-forward: this copy has not been touched since it arrived, so the
Expand Down
Loading