From c5e77591d310581b38e9f26a2719ae571a42b85d Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Mon, 21 Sep 2026 14:22:08 -0700 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH --- .gitattributes | 1 + scripts/beads-merge-driver.sh | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100755 scripts/beads-merge-driver.sh diff --git a/.gitattributes b/.gitattributes index e69de29b..305730ff 100644 --- a/.gitattributes +++ b/.gitattributes @@ -0,0 +1 @@ +.beads/issues.jsonl merge=beads diff --git a/scripts/beads-merge-driver.sh b/scripts/beads-merge-driver.sh new file mode 100755 index 00000000..d3e19d1e --- /dev/null +++ b/scripts/beads-merge-driver.sh @@ -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