Beads Migration: Classic to Dolt
Migrate a beads installation from the classic format (SQLite + JSONL with git worktree sync branch) to the modern Dolt-based format. This is a one-time migration that prioritizes safety through backups and pre-flight validation.
When to Use
- Repository has
.beads/beads.db(SQLite) but no.beads/dolt/directory - Repository has
.beads/issues.jsonlfrom the classic format without Dolt - After upgrading
bdCLI to v0.59.0+ which requires Dolt - When
bdcommands fail with backend/database errors on an old installation
Usage
/beads-migrate
Prerequisites
bdCLI v0.59.0+ installeddoltbinary installed and on PATH (brew install dolt, or https://github.com/dolthub/dolt/releases)- Git repository with existing
.beads/directory containing classic format data - The repo may have a
beads-sync(or similar) branch used as a worktree for issue commits
Instructions
0. Pre-Flight: Stop Legacy Daemons
Legacy bd (≤0.55.x) ran a background daemon per workspace that holds the SQLite WAL open. Stop them cleanly first, or WAL writes may be lost.
cat ~/.beads/registry.json 2>/dev/null
pgrep -af "bd daemon\|bd .* daemon" 2>/dev/null
For any daemon matching a .beads/ workspace you're migrating, kill -TERM <pid> and wait a second. A graceful SIGTERM checkpoints the WAL into the main db on shutdown.
Verify after: .beads/beads.db-wal and .beads/beads.db-shm should be gone (absorbed into beads.db).
Note: if bd was uninstalled while the daemon was running, /proc/<pid>/exe points at a deleted binary — SIGTERM still works.
1. Detect Current State
On-disk inspection is authoritative. Do NOT trust bd doctor / bd doctor --migration=pre for this — on newer bd with classic data on disk, the doctor reports "Already using Dolt backend" because it checks the configured backend, not files. Use the checks below as the gate.
ls -la .beads/beads.db 2>/dev/null
ls -la .beads/issues.jsonl 2>/dev/null
ls -la .beads/dolt/ 2>/dev/null
cat .beads/metadata.json 2>/dev/null
cat .beads/config.yaml 2>/dev/null
git worktree list
If beads.db exists, also sanity-check that SQLite and JSONL are in sync (otherwise a later JSONL re-import may lose rows):
sqlite3 .beads/beads.db "SELECT COUNT(*) FROM issues"
wc -l .beads/issues.jsonl
If the counts differ, the SQLite db has unflushed writes. Normally a graceful daemon stop in step 0 fixes this; if not, the user needs to decide whether to trust JSONL or SQLite as source of truth.
Classify the state:
| State | Indicators | Action |
|---|---|---|
| Classic | beads.db exists, no dolt/ dir |
Full migration |
| JSONL-only | issues.jsonl exists, no beads.db, no dolt/ |
Init + import |
| Already Dolt | dolt/ exists, metadata says "backend": "dolt" |
Stop — no migration needed |
| Partial | dolt/ exists but empty/broken, classic files remain |
Resume migration |
| No beads | No .beads/ directory |
Stop — not a beads repo |
If Already Dolt: inform the user and suggest bd doctor if they have issues.
If No beads: inform the user and suggest bd init for a fresh installation.
2. Pre-Migration Validation
bd doctor --migration=pre
Known false positive: on bd 0.62.0+ with on-disk classic data, this prints "Already using Dolt backend" with JSONL Count: 0. Ignore it if step 1 classified the repo as Classic or JSONL-only — the doctor only looks at runtime config.
Otherwise, review the output for real blockers and stop if there are any.
3. Record Pre-Migration State
Capture current state for post-migration verification. bd list won't work on classic data from a newer bd, so read counts directly:
sqlite3 .beads/beads.db "SELECT COUNT(*) FROM issues" 2>/dev/null
wc -l < .beads/issues.jsonl 2>/dev/null
cat .beads/config.yaml 2>/dev/null
Note the issue count and sync-branch value from config.yaml.
4. Back Up Old Data
This step is mandatory. Never skip it.
mkdir -p .beads-migration-backup
cp .beads/beads.db .beads-migration-backup/ 2>/dev/null || true
cp .beads/issues.jsonl .beads-migration-backup/ 2>/dev/null || true
cp .beads/config.yaml .beads-migration-backup/ 2>/dev/null || true
Then try a structured JSONL backup:
bd backup
ls -la .beads/backup/
This is expected to fail on bd 0.62.0+ with classic data — bd backup errors out with "no beads database found" because the runtime backend is Dolt and there's no Dolt db yet. Continue anyway; the raw copies in .beads-migration-backup/ are the real fallback and step 7 imports from .beads/issues.jsonl directly.
5. Remove Old Backend
Remove old database files and any stale locks to prepare for Dolt initialization:
rm -f .beads/beads.db .beads/beads.db-shm .beads/beads.db-wal
rm -f .beads/metadata.json
rm -f .beads/daemon.lock .beads/dolt-server.lock
Do NOT remove:
.beads/config.yaml— contains sync-branch and team settings.beads/backup/— just created in step 4.beads/issues.jsonl— primary data source for step 7
6. Initialize Dolt Backend
bd init --force
The --force flag is needed because .beads/ already exists.
What bd init --force does and does NOT do:
- Creates an empty Dolt database under
.beads/dolt/, writes newmetadata.json, starts a local Dolt sql-server on a random port - Modifies
.gitignore(adds Dolt-related entries) and createsAGENTS.mdin the repo root — both will show up as modified/untracked ingit statusafter migration - Does NOT import
.beads/issues.jsonl— it creates an empty database. Importing happens in step 7.
Verify initialization:
ls -la .beads/dolt/
cat .beads/metadata.json
Confirm metadata shows "backend": "dolt".
6a. Repair Husky Integration (if applicable)
Known bd bug, not fixed in 0.63.3 (latest as of 2026-03-30). When a repo uses husky for git hooks, bd init sets core.hooksPath to .beads/hooks/ and copies hook content into it — but it mishandles husky's helper layout. The copied hook silently fails or no-ops unless repaired.
Skip this section if the repo does not have a .husky/ directory.
Detect the husky version by looking at the first line of .husky/pre-commit:
cat .husky/pre-commit
Husky v8 style (hook sources _/husky.sh at the top):
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
bd copies this content into .beads/hooks/pre-commit but does NOT copy .husky/_/ into .beads/hooks/_/, so the source line fails at runtime. Fix by symlinking the helper dir:
ln -s ../../.husky/_ .beads/hooks/_
Verify: ls -la .beads/hooks/_/husky.sh should resolve to an existing file.
Husky v9 style (hook is just the command list, no sourcing):
#!/usr/bin/env sh
npx lint-staged
bd installs its own h dispatcher at .beads/hooks/h and writes a .beads/hooks/<name> that sources it with . "$(dirname "$0")/h". The dispatcher then looks for the real hook at $(dirname "$(dirname "$0")")/<name>, which resolves to .beads/<name> instead of .husky/<name>. That file doesn't exist, so h silently exits 0 and none of your husky checks run.
Fix by inlining the .husky/<name> commands directly into .beads/hooks/<name>, replacing the broken h source line. Example for .beads/hooks/pre-commit:
#!/usr/bin/env sh
# Inlined from .husky/pre-commit (bd's 'h' dispatcher resolves wrong path at this depth)
export PATH="node_modules/.bin:$PATH"
<commands from .husky/pre-commit>
# --- BEGIN BEADS INTEGRATION v0.62.0 ---
# ... existing beads block preserved unchanged
Repeat for every hook the repo uses (pre-commit, pre-push, commit-msg, etc.). Check sh -n <hookfile> for syntax validity afterwards.
Note the export PATH="node_modules/.bin:$PATH" — husky's v9 h dispatcher normally adds this so lint-staged and friends resolve; when inlining, preserve it.
7. Restore Data
Preferred path (works if step 4's bd backup succeeded, which it won't on classic data):
bd backup restore .beads/backup/
Fallback path (the actual path for classic-data migrations): import directly from the JSONL that step 5 preserved.
bd import .beads/issues.jsonl
Note: .beads-migration-backup/ contains raw SQLite+JSONL copies, not the structured format bd backup restore expects — so don't try bd backup restore .beads-migration-backup/, it will fail.
Schema mismatch: legacy comments[].id (int → string)
If bd import errors with:
failed to parse issue from JSONL: json: cannot unmarshal number into Go struct field Comment.comments.id of type string
…the JSONL was written by a legacy bd (≤0.49.x era) that stored comment IDs as integers; newer bd expects strings. Transform the file and re-import:
python3 - <<'PY'
import json
src = ".beads/issues.jsonl"
dst = "/tmp/bd-issues-fixed.jsonl"
with open(src) as fh, open(dst, "w") as out:
for line in fh:
line = line.strip()
if not line: continue
d = json.loads(line)
for c in d.get("comments") or []:
if isinstance(c.get("id"), int):
c["id"] = str(c["id"])
out.write(json.dumps(d) + "\n")
print(f"wrote {dst}")
PY
bd import /tmp/bd-issues-fixed.jsonl
rm /tmp/bd-issues-fixed.jsonl
If a different schema error comes up (other fields flipping int↔string), apply the same pattern: locate the field, cast it, retry. Preserve the original .beads-migration-backup/ throughout.
If import still fails after schema fixes, stop and report. The original data is safe in .beads-migration-backup/.
8. Verify Migration
Regenerate .beads/issues.jsonl from Dolt so the on-disk file matches the new schema (bd's auto-export doesn't always trigger immediately after an import):
bd export -o .beads/issues.jsonl
Note: bd export writes to stdout by default — -o <file> is required.
Then verify:
bd doctor --migration=post
bd list 2>&1 | wc -l
bd doctor
Compare the post-migration issue count with the pre-migration count from step 3. If counts don't match, warn the user with both numbers and ask whether to proceed or investigate.
bd doctor --migration=post may still report JSONL Valid: false as a stale sub-check even after a successful export — trust the full bd doctor output (0 errors) as the real signal.
9. Set Up Sync Branch
If config.yaml had a sync-branch value (found in step 3):
bd migrate sync <branch-name>
If no sync branch was configured, ask the user if they want one. For team projects, recommend it.
10. Clean Up Legacy Artifacts
Remove old format files and worktrees:
bd doctor --check=artifacts --fix
Check for and remove old beads worktrees:
git worktree list
If a beads worktree exists at .git/beads-worktrees/<branch>/:
git worktree remove .git/beads-worktrees/<branch-name>
If removal fails due to uncommitted changes, use --force and inform the user.
Clear the legacy daemon registry: ~/.beads/registry.json is the per-user daemon discovery file used by bd ≤0.55.x. bd 0.62.0+ in Dolt mode doesn't use it, so any entries there are stale dead PIDs pointing at vanished SQLite files. On the last migrated repo for this user, clear it:
echo '[]' > ~/.beads/registry.json
11. Migration Backup Cleanup
Ask the user before removing the backup:
Migration completed successfully. The backup is at
.beads-migration-backup/. Would you like to keep it as a safety net, or remove it?
If user agrees:
rm -rf .beads-migration-backup/
12. Update Documentation
Search the repo for files containing outdated beads references:
grep -rl "bd sync\|bd daemon\|beads\.db\|sqlite\|\.jsonl" \
.beads/PRIME.md CLAUDE.md AGENTS.md .claude/ docs/ README.md \
2>/dev/null
Outdated patterns to look for:
bd sync— removed in v0.59.0, Dolt handles sync nativelybd daemon— removed, no background daemon in Dolt mode- References to
beads.db,sqlite, or.jsonlas the storage backend - References to the old worktree-based sync mechanism
- Outdated
bdsubcommands or flags
For each file with matches, show the user the outdated lines and suggest replacements. Common files to check:
.beads/PRIME.md— AI workflow context (most likely to have outdated commands)CLAUDE.md/.claude/— Claude Code project instructionsAGENTS.md— agent configurationREADME.md,docs/— project documentation
13. Report
Summarize the migration:
- Previous format (Classic SQLite or JSONL-only)
- New format: Dolt
- Issues migrated: count
- Sync branch: configured name or "not configured"
- Artifacts cleaned: yes/no
- Migration backup: kept/removed
Handling Edge Cases
- JSONL-only (no SQLite): Skip the
beads.db*removal in step 5. Step 7's fallback (bd import .beads/issues.jsonl) is already the correct path. Warn that events not captured in JSONL may be lost. bd backupfails on old format: Expected — step 7's fallback handles it.- Schema mismatch on import (
cannot unmarshal number into ... string): Legacy bd wrote certain fields as ints that newer bd expects as strings. Comment IDs (comments[].id) are the known case — see the Python transform in step 7. Apply the same pattern for any other field that trips the import. - SQLite/JSONL count mismatch in step 1: Classic daemons had unflushed WAL writes. Re-run step 0's graceful stop; if daemons are already dead, SQLite recovers the WAL on next open (run
sqlite3 .beads/beads.db ".recover"or just letbd initdo it). - Partial previous migration: If
dolt/exists but is empty or corrupt, remove it (rm -rf .beads/dolt/) and re-runbd init --force, then proceed with restore. - Issue count mismatch (post-migration): Common cause is infrastructure beads (agents, rigs) excluded from default export. Suggest re-exporting with
--allflag. - Worktree removal fails: Try
git worktree remove --force <path>. If still fails, inform user for manual cleanup. - Multiple beads worktrees: List all with
git worktree list, identify beads-related ones (path containsbeads-worktrees), remove each. - Config.yaml missing: Proceed without sync branch setup. After migration, suggest
bd config set sync.branch <name>if needed. - Daemon binary already deleted but process still running:
brew uninstallremoves the on-disk binary but a running daemon keeps it mmap'd.kill -TERM <pid>still works; the daemon shuts down cleanly from the in-memory code. - Multiple repos to migrate: Each workspace needs its own run (daemon stop → backup → remove → init → import → export → verify). The
~/.beads/registry.jsoncleanup in step 10 only needs to happen once at the end. - Husky integration broken after
bd init: See step 6a. bd 0.62.0 and 0.63.3 both mishandle husky helper layout — v8 needs a symlink fix, v9 needs the dispatcher inlined. Silently breaks commits if not repaired. - Unwanted
AGENTS.mdand.gitignorechanges frombd init: If the user runs beads in stealth mode (personal tool, team doesn't use it), they'll want to revert.gitignore(git checkout -- .gitignore) and delete the generatedAGENTS.md. The Dolt-related patterns bd adds (*.db,.dolt/,.beads-credential-key) are already covered by the user's stealth.git/info/excludeentries. Ask the user whether to keep or revert. .beads-migration-backup/showing as untracked: Add it to.git/info/exclude(not.gitignore) for stealth-mode users, alongside their existing.beads/entry.
Rules
- Never skip the backup step (step 4). If backup fails, stop and inform the user.
- Never delete
.beads-migration-backup/without user confirmation. - Never proceed past verification failures without explicit user approval.
- Preserve
config.yamlthrough the migration — it contains team settings. - Always run
bd doctor --migration=postbefore declaring success. - If any step fails, stop and report the error. Do not force through.