AgentStack
SKILL verified MIT Self-run

Beads Migrate To Dolt

skill-flurdy-agent-skills-beads-migrate-to-dolt · by flurdy

Migrate a beads installation from classic format (SQLite/JSONL on beads-sync worktree branch) to the new Dolt-based format.

No reviews yet
0 installs
15 views
0.0% view→install

Install

$ agentstack add skill-flurdy-agent-skills-beads-migrate-to-dolt

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Beads Migrate To Dolt? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

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.

bd Version Compatibility

This skill has been validated against:

  • bd 0.59.x – 0.63.x (server-mode Dolt: .beads/dolt/ with a sql-server)
  • bd 1.0.x (embedded Dolt: .beads/embeddeddolt/, no separate server) — recommended

Notable differences in bd 1.0+ that change the migration flow:

  • Default storage is .beads/embeddeddolt/, not .beads/dolt/
  • bd doctor prints "not yet supported in embedded mode" — fall back to bd list counts + round-trip bd export
  • bd doctor --migration=pre|post likewise unavailable; trust on-disk inspection and counts
  • bd sync is removed; the replacements are bd dolt push|pull|commit|status
  • bd init auto-commits a chunk of files (see step 6) — be prepared to revert if unwanted
  • bd init runs interactive prompts — pass --non-interactive (or set BD_NON_INTERACTIVE=1) when running through automation

When to Use

  • Repository has .beads/beads.db (SQLite) but no .beads/dolt/ or .beads/embeddeddolt/ directory
  • Repository has .beads/issues.jsonl from the classic format without Dolt
  • After upgrading bd CLI to v0.59.0+ which requires Dolt
  • When bd commands fail with backend/database errors on an old installation
  • Partial migration: .beads/dolt/ exists with a running sql-server but the database it expects (e.g. beads) is missing — bd list returns "database not found"

Usage

/beads-migrate

Prerequisites

  • bd CLI installed. Recommended: 1.0.3+ (brew upgrade bd). Earlier 0.59–0.63 also work but expose bd doctor paths the skill no longer relies on.
  • dolt binary on PATH only required for server-mode (bd ≤0.63). bd 1.0+ embedded mode bundles its own Dolt engine.
  • 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 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//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. On bd 1.0 in embedded mode, bd doctor outright refuses to run. Use the checks below as the gate.

bd --version
ls -la .beads/beads.db 2>/dev/null
ls -la .beads/issues.jsonl 2>/dev/null
ls -la .beads/dolt/ 2>/dev/null            # bd 0.59–0.63 server mode
ls -la .beads/embeddeddolt/ 2>/dev/null    # bd 1.0+ embedded mode
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.

Worktree-cached JSONL is often newer than .beads/issues.jsonl in main. When the classic worktree at .git/beads-worktrees// exists, also check wc -l .git/beads-worktrees//.beads/issues.jsonl — that file usually has the last bd sync snapshot, which can be more recent than the empty/stale JSONL in the main worktree.

Classify the state:

| State | Indicators | Action | |-------|-----------|--------| | Classic | beads.db exists, no dolt/ or embeddeddolt/ dir | Full migration | | JSONL-only | issues.jsonl exists, no beads.db, no Dolt dir | Init + import | | Already Dolt (server) | dolt/ exists, metadata says "backend": "dolt", server reachable | Stop — no migration needed | | Already Dolt (embedded) | embeddeddolt/ exists, metadata "dolt_mode": "embedded", bd list works | Stop — no migration needed | | Partial (server) | dolt/ exists but empty or sql-server says database "" not found | Resume migration | | Partial (embedded) | embeddeddolt/ exists but bd list errors out | Resume migration | | No beads | No .beads/ directory | Stop — not a beads repo |

If Already Dolt: inform the user and suggest bd dolt status (1.0) or bd doctor (≤0.63) if they have issues. If No beads: inform the user and suggest bd init for a fresh installation.

For Partial (server) state, take a last look at running processes before destroying state:

cat .beads/dolt-server.pid 2>/dev/null    # the sql-server we'll need to stop
cat .beads/dolt-monitor.pid 2>/dev/null   # bd's monitor that respawns the server
pgrep -af "dolt sql-server" 2>/dev/null

Step 5 will TERM both. Skipping this risks the monitor respawning the server during cleanup.

2. Pre-Migration Validation

On bd 0.59–0.63:

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.

On bd 1.0+ in embedded mode, bd doctor is unavailable — it prints "not yet supported in embedded mode" and exits 0. Skip this step entirely; rely on step 1's on-disk inspection.

Otherwise, review the output for real blockers and stop if there are any.

Optional: upgrade bd first if you're on a pre-1.0 release. Newer bd is significantly easier to recover from edge cases (the import path is more forgiving, embedded mode avoids server lifecycle bugs). If the user is on, say, 0.59.x and the migration is otherwise unconstrained:

brew upgrade bd      # or whatever installer they used
bd --version

A major-version jump (e.g. 0.59 → 1.0) is generally safe for the migration path because the JSONL import format is stable across this range. The schema fixes in step 7 cover the known transition cases.

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
sqlite3 .beads/beads.db "SELECT status, COUNT(*) FROM issues GROUP BY status" 2>/dev/null
sqlite3 .beads/beads.db "SELECT COUNT(*) FROM dependencies" 2>/dev/null
sqlite3 .beads/beads.db "SELECT COUNT(*) FROM comments" 2>/dev/null
wc -l /dev/null
wc -l /dev/null
cat .beads/config.yaml 2>/dev/null

Note the authoritative issue count (almost always SQLite, sometimes the worktree JSONL is more recent than main worktree's empty JSONL) and the sync-branch value from config.yaml.

If SQLite count > .beads/issues.jsonl line count, plan to use the SQLite→JSONL converter in step 3a — relying on the empty/stale JSONL would silently drop rows.

3a. SQLite → JSONL Converter (when JSONL is stale or empty)

Skip if .beads/issues.jsonl already has all the data SQLite does.

When the main-worktree JSONL is empty or older than the SQLite db (common: classic bd's "auto-flush" leaves the main worktree's .beads/issues.jsonl at 0 bytes; only the beads-sync worktree gets non-empty exports), step 7's fallback bd import .beads/issues.jsonl would import nothing. Convert directly from SQLite first:

# Save as /tmp/bd-sqlite-to-jsonl.py and run with python3
import json, sqlite3
from collections import defaultdict
from pathlib import Path

DB = Path(".beads/beads.db")
OUT = Path("/tmp/bd-issues-from-sqlite.jsonl")

TOP_FIELDS = ["id","title","description","status","priority","issue_type",
              "owner","created_by","created_at","updated_at","closed_at",
              "close_reason","notes"]

def normalize_ts(ts):
    if not ts: return None
    if "T" not in ts and " " in ts: ts = ts.replace(" ","T",1)
    if not (ts.endswith("Z") or "+" in ts[10:] or ts.endswith("+00:00")):
        ts += "Z"
    return ts

con = sqlite3.connect(str(DB)); con.row_factory = sqlite3.Row

deps = defaultdict(list)
for r in con.execute("SELECT issue_id,depends_on_id,type,created_at,created_by FROM dependencies"):
    deps[r["issue_id"]].append({"issue_id": r["issue_id"], "depends_on_id": r["depends_on_id"],
        "type": r["type"], "created_at": normalize_ts(r["created_at"]) or "",
        "created_by": r["created_by"] or ""})

cmts = defaultdict(list)
for r in con.execute("SELECT id,issue_id,author,text,created_at FROM comments"):
    cmts[r["issue_id"]].append({"id": str(r["id"]), "issue_id": r["issue_id"],
        "author": r["author"] or "", "text": r["text"] or "",
        "created_at": normalize_ts(r["created_at"]) or ""})

n = 0
with OUT.open("w") as out:
    for r in con.execute("SELECT * FROM issues WHERE deleted_at IS NULL AND status != 'tombstone'"):
        d = {f: r[f] for f in TOP_FIELDS if f in r.keys() and r[f] not in (None,"")}
        for tf in ("created_at","updated_at","closed_at"):
            if tf in d: d[tf] = normalize_ts(d[tf])
        if r["id"] in deps:  d["dependencies"] = deps[r["id"]]
        if r["id"] in cmts:  d["comments"] = cmts[r["id"]]
        out.write(json.dumps(d, ensure_ascii=False) + "\n"); n += 1
print(f"wrote {n} issues to {OUT}")
python3 /tmp/bd-sqlite-to-jsonl.py
wc -l /tmp/bd-issues-from-sqlite.jsonl     # must equal SQLite count
head -1 /tmp/bd-issues-from-sqlite.jsonl | python3 -m json.tool   # spot-check

In step 7, import from /tmp/bd-issues-from-sqlite.jsonl instead of .beads/issues.jsonl.

Notes:

  • The SQLite issues table has many columns (compaction_level, event_kind, agent_state, etc.); only the TOP_FIELDS list maps cleanly to bd's import schema. Other columns are bd internals and shouldn't round-trip through user-facing JSONL.
  • comments[].id is converted to string here to satisfy bd ≥0.50's import schema.
  • Tombstones and soft-deleted issues are skipped — re-importing them would produce confusing dangling rows.

4. Back Up Old Data

This step is mandatory. Never skip it.

mkdir -p .beads-migration-backup

cp -a .beads/. .beads-migration-backup/ 2>/dev/null || true
cp /tmp/bd-issues-from-sqlite.jsonl .beads-migration-backup/ 2>/dev/null || true

(cp -a .beads/. snapshots everything — SQLite, JSONL, hooks, dolt directories, lockfiles. Cheap insurance vs. selectively copying individual files.)

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 JSONL directly.

5. Remove Old Backend

For Partial (server) state, stop the running Dolt server and monitor first — otherwise the monitor will respawn the server during cleanup:

[ -f .beads/dolt-server.pid ]  && kill -TERM "$(cat .beads/dolt-server.pid)"  2>/dev/null
[ -f .beads/dolt-monitor.pid ] && kill -TERM "$(cat .beads/dolt-monitor.pid)" 2>/dev/null
sleep 1
pgrep -af "dolt sql-server" 2>/dev/null    # should be empty

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/daemon.log
rm -f .beads/dolt-server.lock .beads/dolt-server.log .beads/dolt-server.pid \
      .beads/dolt-server.port .beads/dolt-server.activity .beads/dolt-config.log
rm -f .beads/dolt-monitor.pid .beads/dolt-monitor.pid.lock
rm -f .beads/.local_version .beads/last-touched
rm -rf .beads/dolt/                # for Partial (server) state
rm -rf .beads/embeddeddolt/        # for Partial (embedded) state

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 (or rely on /tmp/bd-issues-from-sqlite.jsonl from step 3a if JSONL is empty)

6. Initialize Dolt Backend

bd init --non-interactive --force
  • --force is needed because .beads/ already exists.
  • --non-interactive skips bd 1.0's wizard prompts (role, contributor, fork detection). Without it, bd init blocks waiting for input. (Auto-detected when stdin isn't a TTY or CI=true, but pass it explicitly for safety.)
  • For server-mode (bd ≤0.63 or bd 1.0 with external dolt sql-server), add --server.

What bd init --force does and does NOT do (bd 1.0+):

Creates the Dolt store:

  • .beads/embeddeddolt/ (1.0 embedded, default) or .beads/dolt/ (server mode)
  • New metadata.json (e.g. {"backend":"dolt","dolt_mode":"embedded","dolt_database":""})

Auto-commits a chunk of repo files in a single bd init: initialize beads issue tracking commit:

  • .gitignore (adds Dolt-related ignores: *.db, embeddeddolt/, etc.)
  • .beads/.gitignore, .beads/metadata.json, .beads/issues.jsonl
  • .beads/hooks/{post-checkout,post-merge,pre-commit,pre-push,prepare-commit-msg} (its own hooks dir; core.hooksPath repointed)
  • AGENTS.md — appends a `` block (preserves existing content), or creates the file if missing
  • CLAUDE.md at the repo root — created from scratch (~70 lines). If the repo already organises Claude config under .claude/CLAUDE.md, the new repo-root file is redundant and may need to be removed/merged. Surface this to the user.
  • .claude/settings.json — appends bd-related entries

Does NOT import .beads/issues.jsonl — it creates an empty database. Importing happens in step 7.

Verify initialization:

ls -la .beads/embeddeddolt/   # or .beads/dolt/ in server mode
cat .beads/metadata.json
git log --oneline -1          # should show the bd init commit
git show --stat HEAD          # review what bd auto-committed

Confirm metadata shows "backend": "dolt" (and "dolt_mode": "embedded" for 1.0).

After init, ask the user whether to keep or revert specific auto-committed files. The most common ask: revert/delete the new repo-root CLAUDE.md if .claude/CLAUDE.md is the project's canonical location. Use git revert of the init commit + cherry-pick the parts they want to keep, or git reset HEAD~1 if the init commit is HEAD and they want to selectively re-stage.

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 th

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.