Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Reconciling a failed merge

Three Quipu merges can stop on a conflict:

MergeCommandHow it stops
Share reconnectquipu merge <share-dir>exit 2, conflicts in the JSON result
Fork promotionquipu fork promote <name>“promotion held”, exit 1, the fork stays open
Git pendant mergequipu git-merge <ref>exit 2, <pendant>/decisions.json beside the pack

All three use the same shape-aware operator. A slot (one subject and predicate) that both sides changed differently, past an sh:maxCount bound, is held at its base value. Nothing is guessed and nothing is written. This page is the one way to finish such a merge, for a person or an agent.

The four steps

StepShareForkGit
1. Emitquipu merge <dir> --emit-decisions f.json --proposequipu fork promote <name> --emit-decisions f.json --proposewritten by quipu git-merge
2. Showquipu merge show f.jsonsamequipu merge show <pendant>/decisions.json
3. Resolvequipu merge resolve f.json <row> --choose <choice> --by <who>samesame, or quipu git-merge resolve <pendant> <key> ...
4. Applyquipu merge <dir> --decisions f.json --reviewer <who>quipu fork promote <name> --decisions f.json --reviewer <who>git add <pendant> and commit; CI pendant-check verifies

Over MCP, step 1 is quipu_merge_decisions (fork: <name> for a fork, present: true for the layout), step 3 is quipu_merge_resolve, and step 4 is quipu_merge_apply. quipu_merge_resolve touches no store; it returns the updated decisions for you to pass to apply.

Run apply with --dry-run (dry_run: true) first. It makes every check, including SHACL for a fork, and writes nothing.

Choices

ChoiceResult
oursthe local side: ROOT, or Git HEAD
theirsthe incoming side: the share, the fork, or MERGE_HEAD
basethe value both sides started from
bothours and theirs together; offered only when that fits sh:maxCount
valuevalues you type, as N-Triples terms (--value '"wontfix"'); share and fork only
deferleave the row undecided; apply keeps refusing until it is decided

Git alias proposals (two new IRIs that may name one entity) take accept (adds owl:sameAs), reject or defer.

What the user sees

quipu merge show prints one fixed layout. Agents show it as printed, with --format md in chat:

Merge needs your decision: fork 'triage' -> ROOT
1 conflict, 1 undecided. Nothing has been written.

[d1] Ticket 42 · status
  rule     sh:maxCount 1 on status: ours and theirs together hold 2 values
  base     "open"
  ours     "blocked"  (alice, 2026-10-27T14:02:00Z, tx 118)
  theirs   "closed"  (triage-bot, 2026-10-27T15:40:00Z, tx 131)
  suggest  theirs: theirs is newer: fork tx 131 >= ROOT tx 118
  choose   1) ours  2) theirs  3) base  4) value  5) defer
  decided  (none)

Next: quipu merge resolve f.json d1 --choose <ours|theirs|base|value|defer> --by <you>
Then: quipu fork promote triage --decisions f.json --reviewer <you> (add --dry-run first)

Each block names the entity and the field by rdfs:label, the rule that made it a conflict, base, ours and theirs with who changed each and when, the mechanical suggestion with its reason, and the numbered choices. The last lines always give the exact next command. --format json gives the same data as merge-view/v1 for tools.

When an agent may decide

  • Decide alone only when every side of the row was written by agents and you take the suggested choice. Record your agent id with --by and tell the user in one line.
  • Ask when a person wrote either side, when you cannot tell who did, for value and both, and for every sh:maxCount conflict on human-owned data. Ask once, for all undecided rows, with the numbered choices, and record each answer with --by <the person>.
  • High-stakes predicates (money, legal, credentials, irreversible changes) will need a signed human verdict once hardware-backed verdicts ship.

Refusals

Apply writes nothing and says why when a row is undecided or deferred, when the decisions are stale (a reviewed slot moved on either side, or the set of conflicts changed), when a decision has more values than sh:maxCount, when a value is not one RDF term, when the file has a field its schema does not define, or, for a fork, when SHACL refuses the result. A stale file is emitted again and re-decided; never edit its hashes.

Applying the same decisions a second time reports already-applied with the original transaction and writes nothing, so a retried apply is safe.

Share reconnect and fork promotion use the common temporal write core. A one-sided incoming share addition keeps the share manifest’s created_at as its authored time. A one-sided removal closes at that same share time, clamped against the local row’s start; losing local values in a decided conflict close at decision time instead. A decided conflict first records incoming values as receipts with the empty valid interval [Td, Td), then closes losing local values and starts selected new values at decision time Td. Both ordered transactions commit atomically; a refused decision rolls the received step back too. A receipt is visible in transaction history, never at a valid time. The reported transaction is the final step, and counts describe the final live graph changes rather than counting receipts as additions.

Plain share merges also carry a stable merge identity. Reapplying the same incoming/base pair reports already-applied and creates no new transaction.

See also

  • CLI: Sharing & Legacy Packs: quipu merge, the decisions file fields, and the Git pendant commands.
  • MCP tools: quipu_merge_decisions, quipu_merge_resolve, quipu_merge_apply.
  • Design: docs/design/merge-reconcile.md.