Files
Edward Langley cd8f3e3ec0 docs: a beads-merge skill, and the export the driver test regenerated
The skill covers the four things that bit us today: the per-clone git config
the driver needs (.gitattributes names it, but the driver lives in git config,
which a checkout does not carry -- without it git silently line-merges), how to
recover a file that already has markers in it, where genuine same-issue
divergence is reconciled (bd conflicts, against Dolt -- not reconstructed from
the JSONL), and the autostash trap that writes markers without anyone running
`git stash`.

Driver verified against a real conflict: plain text merge produced 3 marker
lines and 3 invalid JSONL lines; the driver produced 470 rows, 0 invalid, and
left the database byte-identical (470 rows before and after).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
2026-09-21 14:22:58 -07:00

4.2 KiB

name, description
name description
beads-merge Resolve or prevent git conflicts in .beads/issues.jsonl, and reconcile genuinely divergent issues. Use when a merge, rebase, cherry-pick or stash pop conflicts on that file, when auto-export reports "invalid character", or when setting up a fresh clone.

Merging .beads/issues.jsonl

The database is the truth; the file is derived. .beads/issues.jsonl is a passive export of the local Dolt DB, which syncs over refs/dolt/data. Nothing here ever merges the file — it regenerates it.

The one rule

Never hand-edit it, never git checkout it (that fires the post-checkout hook and desynchronises export-state.json), and never resolve its conflict markers by editing. Use bd and regenerate.

Setup — required once per clone

.gitattributes names the driver; the driver itself lives in git config, which is per-clone and not carried by a checkout. Without this, git silently falls back to line-merging and writes conflict markers into the JSONL.

git config merge.beads.name "beads issues.jsonl: import both sides, regenerate from the database"
git config merge.beads.driver "$PWD/scripts/beads-merge-driver.sh %O %A %B %P"
git check-attr merge -- .beads/issues.jsonl   # must print "merge: beads"

A worktree shares .git/config with its parent, so a worktree needs no separate setup — but a fresh git clone does.

What the driver does, and why it needs no merge logic

scripts/beads-merge-driver.sh imports both sides and re-exports. It writes no merge logic of its own because bd import already has the semantics:

  • an id the DB has not seen is created
  • a row whose updated_at is strictly newer than the local one wins
  • an older row is skipped (reported as stale_skipped_ids)
  • a tie keeps every local column — updated_at has second granularity, so a same-second tie can be two distinct updates, and local wins

So neither import can move the database backwards, which is why importing ours as well as theirs is safe: it restores a database that has lost rows without overwriting one that is ahead.

Set BEADS_MERGE_PULL=1 to bd dolt pull first (remote via BEADS_MERGE_REMOTE, default gh). Off by default: a merge driver that reaches the network fails in ways a merge cannot report.

Reconciling a genuinely divergent issue

The driver resolves by recency, which is right for the common case (two sides filing different issues) and wrong when both sides meaningfully edited the same issue — the older edit is skipped silently.

Do not reconstruct that from the JSONL. bd already does it field by field against the Dolt working set, which is the real sync path:

bd conflicts list                    # which issues are conflicted
bd conflicts show improvise-XXXX     # base / ours / theirs, per field
bd conflicts resolve improvise-XXXX --ours     # or --theirs
bd conflicts resolve --all --theirs

If the driver already took the newer side and you need the other, the losing content is in git history: git show <their-commit>:.beads/issues.jsonl, find the row, and apply the fields with bd update. Never import a whole stale file to recover one issue — that is what --allow-stale does, and it overwrites every newer local row.

Recovering a corrupted file

If auto-export reports failed to compare existing JSONL against local store: invalid character '<', the file has conflict markers in it and auto-export has been silently skipping — possibly for many commits.

grep -nE '^(<<<<<<<|=======|>>>>>>>)' .beads/issues.jsonl   # confirm
bd export -o .beads/issues.jsonl                           # regenerate from the DB

Check whether the markers reached a commit (git log -S '<<<<<<<' -- .beads/issues.jsonl). They are usually working-tree only.

The trap that caused this

A git stash pop conflict writes <<<<<<< Updated upstream / >>>>>>> Stashed changes into the file. Git's autostash during a dirty-tree merge or rebase does this without anyone running stash explicitly, and the orphaned entry stays in git stash list. The merge driver covers stash pops too, but the habit that avoids it is: commit before merging, and treat a stray autostash in git stash list as a live hazard.