tooling: a merge driver for .beads/issues.jsonl

issues.jsonl is a passive export whose rows are ordered by insertion, so two
sides that file issues collide in the same region even when the issues are
disjoint. Git line-merges it and produces conflict markers INSIDE the JSONL,
which breaks bd's auto-export silently -- observed today, where a stash-pop
left `<<<<<<< Updated upstream` in the file and auto-export was skipping with
"invalid character '<'" until the file was regenerated.

The driver does no merge logic of its own, because bd already has the
semantics: `bd import` skips any row whose updated_at is not strictly newer
than the local one, creates ids it has not seen, and keeps the local row on a
tie. So importing BOTH sides cannot move the database backwards, and the file
is then regenerated from it. The database is the truth; the file is derived.

Deliberately not reimplemented here: field-level reconciliation. `bd conflicts
show/resolve` already does that against the Dolt working set, which is the real
sync path -- a second copy against the text export would have to track bd's
schema by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
This commit is contained in:
Edward Langley
2026-09-21 14:22:08 -07:00
parent e4adf86c1a
commit c5e77591d3
2 changed files with 37 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env sh
# Git merge driver for .beads/issues.jsonl. Reasoning: context/code/tooling.md.
set -eu
BASE="$1"; OURS="$2"; THEIRS="$3"; PATHNAME="${4:-.beads/issues.jsonl}"
say() { echo "beads-merge: $*" >&2; }
if ! command -v bd >/dev/null 2>&1; then
say "bd is not on PATH; leaving $PATHNAME conflicted"
exit 1
fi
# Opt-in, because a merge driver that reaches the network fails in ways a merge
# cannot report. Off by default; the export alone is enough for a git merge.
if [ "${BEADS_MERGE_PULL:-0}" = "1" ]; then
bd dolt pull "${BEADS_MERGE_REMOTE:-gh}" >&2 || say "dolt pull failed; continuing from local"
fi
# Both sides, not just theirs: `bd import` skips any row whose updated_at is not
# strictly newer than the local one, so neither call can move the database
# backwards, and importing OURS restores a database that has lost rows.
for side in "$OURS" "$THEIRS"; do
[ -s "$side" ] || continue
bd import "$side" >&2 || { say "import failed; leaving $PATHNAME conflicted"; exit 1; }
done
bd export -o "$OURS" || { say "export failed; leaving $PATHNAME conflicted"; exit 1; }
if grep -qE '^(<<<<<<<|=======|>>>>>>>)' "$OURS"; then
say "export produced conflict markers; refusing"
exit 1
fi
say "resolved $PATHNAME from the database ($(wc -l < "$OURS" | tr -d ' ') rows)"
exit 0