Merge

How moatlog reconciles moat.json when multiple developers distill independently, and how to use the merge command in your workflow.

Why merge exists

When you run moatlog distill, it compresses all event logs into moat.json — a single file that summarizes file access patterns, prompt windows, and activity across your project. In a team, this creates a problem:

  1. Developer A distills their local events into moat.json and commits.
  2. Developer B distills their local events independently, creating a different moat.json with their own event history.
  3. When B's PR merges, A's distill is outdated. B's version doesn't include A's file access patterns.

This means the moat's shared context is incomplete — neither developer's memory includes the other's exploration. Merge solves this by combining both distills into one unified moat.json that contains the collaborative knowledge of both teams.

How three-way delta merge works

Moatlog uses a three-way merge strategy (like git) to combine moat.json files. It needs three inputs:

  • Base — the common ancestor moat.json (from the merge-base of your branches)
  • Ours — the current branch's moat.json (your local distill)
  • Theirs — the branch being merged in (the PR's moat.json)

Merge combines these intelligently:

  • Counts sum — For numeric fields (writeCount, totalEvents, etc.), merge uses delta math: base + (ours - base) + (theirs - base). This avoids double-counting and handles resets correctly (if you reindex files locally, the count drops, but merge detects this as a reset rather than data loss).
  • Arrays union — For sessions, prompt windows, and agent names, merge takes the union (keeps entries from both sides). Duplicates by ID are resolved by keeping yours and using the higher quality rating.
  • Base version handles resets — The base moat.json is critical: it lets merge distinguish between a regression (count dropped, might be wrong) and a reset (you re-ran distill with filtered logs). If count drops below 50% of base, merge flags it as COUNT_REGRESSION.

Conflict detection and resolution cascade

Most merges succeed without conflicts. But when the delta math can't resolve a situation cleanly, moatlog flags it as a conflict and hands it to an LLM for resolution.

Conflict types:

  • POSSIBLE_RENAME — A file exists in ours but not theirs (or vice versa), but they have the same co-access partners (files accessed together). Did one side rename src/foo.ts to src/bar.ts?
  • POSSIBLE_DELETION — A file appears in base and theirs but not ours (or vice versa), and has low activity. Was it deleted or just not touched?
  • COUNT_REGRESSION — A file's count dropped below 50% of the base value. Merge flags this for manual review (you may have reindexed or filtered logs).
  • QUALITY_CONFLICT — A prompt window exists in both versions but with different quality ratings (high vs low). Auto-resolved by taking the higher quality.

When conflicts are detected and --no-llm is not set, merge attempts automatic resolution in this order:

  1. cursor — If the cursor CLI is on your PATH, asks cursor to resolve conflicts.
  2. claude — Falls back to claude CLI if available.
  3. ANTHROPIC_API_KEY — If neither CLI is available but ANTHROPIC_API_KEY is set, uses the Anthropic API directly.
  4. Manual fallback — If no agent is detected, prints conflicts to stdout and exits with an error. You must resolve manually and re-distill.

The LLM sees conflict details (type, evidence, file profiles) and returns resolution decisions: keep_ours, keep_theirs, use_merged, drop_theirs_path, or rename_theirs_to_ours.

Git merge driver

Moatlog automatically registers a git merge driver when you run moatlog init. This makes moat.json merge conflict-free in git itself.

When you git merge, git checks .gitattributes and sees that .moatlog/moat.json uses the driver merge=moatlog-union. Instead of git's text-based merge (which would produce a conflicted file), it runs:

shell
moatlog merge-driver <ancestor> <current> <other> <output>

This handler performs a three-way delta merge and writes the resolved moat.json to the output path. Git treats it as a successful merge (no conflict markers).

Common workflows

Merge before committing

Before you distill and commit moat.json, merge in any upstream changes:

shell
# Pull latest from main
git fetch origin

# Merge main's moat.json into yours
moatlog merge --branch origin/main

# Now distill and commit
moatlog distill
git add .moatlog/moat.json
git commit -m "Distill: merge with main and add new sessions"

This ensures your commits include knowledge from all teams.

After a git merge conflict on moat.json

If moat.json still shows a conflict (rare, since the merge driver handles it), resolve it with moatlog:

shell
# Resolve via delta merge
moatlog merge

# Mark as resolved
git add .moatlog/moat.json
git commit --no-edit

Dry-run to preview changes

Before actually merging, see what would change:

shell
moatlog merge --dry-run

# Shows summary of changes without writing moat.json
# merged: N new files from theirs, N files with summed counts, N new prompt windows

Deterministic merge (no LLM)

In CI or when you want just the delta math, use --no-llm to skip LLM resolution:

shell
moatlog merge --no-llm

# If conflicts exist, prints them and exits with code 1
# No LLM calls, fully deterministic

Flags

FlagTypeDefaultDescription
--branchstringmainBranch (or ref) to merge from. Can be a local branch, origin/branch, or commit hash.
--dry-runbooleanfalseShow what would change without writing moat.json. Useful for previewing before committing.
--no-llmbooleanfalseDeterministic merge only (delta math). Surface conflicts as text and exit with code 1 if any remain. Skips all LLM calls.

Exit codes

  • 0 — Merge succeeded (with or without LLM resolution). moat.json written.
  • 1 — Conflicts remain (--no-llm was set, or LLM resolution failed). moat.json not written.