Download Latest Version beads_1.3.1_windows_amd64.zip (54.1 MB) Google Add to Preferred Sources
Home / v1.3.0
Name Modified Size InfoDownloads / Week
Parent folder
beads-v1.3.0.spdx.json 2026-09-15 2.2 MB
checksums.txt 2026-09-15 881 Bytes
beads_1.3.0_darwin_amd64.tar.gz 2026-09-15 53.4 MB
beads_1.3.0_darwin_arm64.tar.gz 2026-09-15 48.3 MB
beads_1.3.0_contract_corpus.tar.gz 2026-09-15 4.1 kB
beads_1.3.0_windows_amd64.zip 2026-09-15 54.0 MB
beads_1.3.0_android_arm64.tar.gz 2026-09-15 31.3 MB
beads_1.3.0_linux_arm64.tar.gz 2026-09-15 49.3 MB
beads_1.3.0_linux_amd64.tar.gz 2026-09-15 53.2 MB
beads_1.3.0_freebsd_amd64.tar.gz 2026-09-15 31.9 MB
beads_1.3.0_windows_arm64.zip 2026-09-15 30.3 MB
README.md 2026-09-15 113.8 kB
v1.3.0 source code.tar.gz 2026-09-15 9.8 MB
v1.3.0 source code.zip 2026-09-15 12.0 MB
Totals: 14 Items   375.8 MB 0

Beads v1.3.0

v1.3.0 — the first tested release off main since the 1.1 line. Released 2026-09-15, cut from release/1.3.0 at f45b249ce.

What that means for you, concretely:

  • On v1.2.2 — you are running v1.1.2-era code. You receive the entire [1.2.1] section (2,236 changelog lines, the largest in the file) and everything in [1.3.0]. Whole subsystems below — an HTTP API server, work leases, a durable events journal, bd sync, a public Go API — do not exist in your build in any form. This is the upgrade to read the notes for.
  • On v1.1.2 — identical to the above. v1.2.2 is your tree with a different version string; the only functional difference is a forward-skew error message.
  • On v1.1.0 — the same, plus the [1.1.2] dolt#11131 aux-row-rekey drift fix (#4380), which is a data-availability fix and is re-listed under Fixed below.
  • On v1.2.1 — you should not be; it is retracted. bd will stop with schema version mismatch. Roll the schema cursor back first, per the recovery runbook.

There is no 1.1.1 and no 1.2.0 upgrade path: both tags burned before publishing anything.

Read the upgrading notes before installing.

Highlights

  • beads has an HTTP API server. bd serve binds a port and answers the whole work loop over 41 OpenAPI-specified operations across 35 paths — ready/list/get/query/count/related, stats, dependencies (list, count, tree, blocking, cycles), config, memories, events, and the writes: claim, claimNext, release, close, reopen, PATCH, batchCreate, batchClose, batchApply, delete, sweep, casMetadata, dependency add/remove. Errors are RFC 9457 problem+json with a frozen machine-readable code vocabulary and one HTTP status per code, so a client classifies a claim conflict from a typed 409 instead of substring-matching error prose; listing pages with an opaque keyset cursor that does not expire and survives restart; GET /v0/beads/context reports which operations the running build actually implements. The spec is hand-written and checked in, the Go wire types are generated from it, and make api-check fails a change that edits one without the other. This is 100% net-new on upgrade — internal/httpapi/spec/openapi.v0.yaml does not exist at v1.1.2 — and it is the subsystem the current release notes had been missing entirely.
  • bd serve is deployable, not just a loopback toy (#5516). --auth-token-file names a file of accepted bearer tokens, one per line; every operation except GET /healthz then requires Authorization: Bearer <token>, including GET /v0/beads/context. The file is re-read while the server runs, so revocation — not just rotation — is a file rewrite with no restart, and a failed or empty re-read keeps the last-good set. There is deliberately no --auth-token flag: a credential in argv is readable out of ps. --allow-non-loopback now requires a token file, with --insecure-no-auth as the explicit auditable opt-out, and --allowed-host extends the DNS-rebinding allowlist. Read the omissions as contract: there is no TLS (the deployment supplies confidentiality), a token is a shared secret granting the whole surface rather than an identity, actor stays caller-asserted provenance, hooks do not fire on HTTP mutations, and the surface includes destructive operations (issues:sweep, issues:delete).
  • A multi-agent coordination layer, from claim to recovery. A claim used to be permanent: a worker that died mid-task stranded its bead in_progress forever with no recovery verb. Claims now carry a lease — lease_expires_at (default TTL 5m) and heartbeat_at, schema v54 — with bd heartbeat <id> to extend it, bd reclaim --older-than <dur> to revert expired ones back to ready, and bd unclaim to give one back (#4537). Because Dolt has no row locking and merges concurrent commits cell-by-cell, every ownership-mutating path also rewrites a shared row_lock cell, forcing a racing heartbeat-vs-reclaim into a serialization conflict the retry layer replays rather than cell-merging into a zombie claim; the same landing wrapped the work-queue hot paths in serialization-conflict retry, so N workers draining one queue stop surfacing raw MySQL 1213/1205 errors. Leases are replica-aware: leases.granted_node records the granting replica and bd reclaim skips a lease another replica granted unless you pass --any-replica (wy-jpd3.7). The guard is opt-in and fail-open, armed by node_id / BEADS_NODE_ID, so an upgrade can never strand a lease the reaper could previously recover.
  • Compare-and-set updates close the two coordination transitions no verb could express (bd-wsqvw). bd update --if-assignee / --if-status apply only if the bead's current value still equals the expected one — one atomic transaction, nothing written on a mismatch. --if-assignee '' means "expected unassigned". A stale guard is machine-distinguishable from infrastructure failure: exit code 13 when every failure in the run was a guard mismatch (a racer won — skip gracefully) versus 1 for anything else, with "guard_mismatch": true per failed entry under --json. That is what makes bd update <id> --if-assignee worker -a mayor — reassign X→Y only while X still holds it — safe without a read-then-write race. bd unclaim --if-assignee is the inverse spelling, and claim.pools (bd-bguz6) turns the dispatcher-fleet pattern into a one-step atomic take: aliases listed in bd config set claim.pools "fable-crew,night-crew" become claimable by any actor through the same compare-and-swap, while beads assigned to a real actor keep their anti-steal protection.
  • bd sync is the federation loop as one verb (wy-jpd3.4). Pull, detect conflicts, recompute is_blocked, push, with bounded retry (default 3) when another replica wins the push race. Two properties are the point. Conflicts are detected positively, from the merge's own captured conflict rows and from dolt_conflicts, never inferred from the pull's exit status — a pull fails for plenty of reasons that are not conflicts, and a settled-but-conflicted merge aborts leaving dolt_conflicts empty, so an exit-status guess invents phantom conflicts and misses real ones. And a conflict it cannot settle is never resolved by picking a side: it halts before recomputing or pushing, with no --strategy override. The recompute between pull and push is not bookkeeping — is_blocked is denormalized, so a merge that brings in another replica's dependency edge leaves bd ready stale until it runs, which is precisely the step a hand-rolled shell loop omits. Exit codes are the machine contract: 0 synced, 1 error, 2 merge conflict (halted, nothing pushed), 3 push-race retries exhausted, and — new in 1.3.0 — 4 for a dirty working set that is stuck rather than busy. bd conflicts list|show|resolve is the companion that clears an exit-2 halt without dropping into the raw dolt CLI.
  • A durable events journal, and bd events to read it (bd-opisf). Every committed bead mutation writes one ordered record in the same transaction as the mutation, carrying the operation, the mutated id, and the bead's full post-mutation snapshot including is_blocked — so a downstream mirror stays correct without re-querying the graph. Off by default; opt in per workspace with bd config set events-journal true or BD_EVENTS_JOURNAL=1. bd events tail --since <seq> prints JSON lines and --follow streams as writes commit; bd events export prints from the beginning; bd events prune --before <seq> takes an earlier cut. A read that cannot resume fails rather than lying: a --since below the oldest retained record exits 1 with a typed events_journal_truncated error carrying since, floor and head. Retention floors (events-journal-retain-days 7, events-journal-retain-rows 100000) are enforced by a throttled prune that can never fail a command. Scope it correctly: the journal is clone-local working-set state (dolt_ignored) — never versioned, never pushed or federated, per branch and per replica, so a checkpoint from one replica is meaningless against another. External tooling previously had only fire-and-forget hooks (which may not run) or snapshot polling (which misses anything that changed twice between reads); this is the third option.
  • The biggest lever on agent token cost in the whole arc: --brief and --brief-deps (#5546, [#5547], [#5549], [#5554], [#5586]). On one real 16,051-bead store bd ready --json returned 454,021 bytes, with notes plus description accounting for 76% of it — on exactly the call an agent makes to decide what to work on next. --brief on bd list and bd ready omits the free-form text (description, design, acceptance_criteria, notes, payload, waiters) and measured 93.4% smaller. Separately, one bd show --json returned 214,456 bytes of which 193,039 was the dependencies array, one dependency carrying a 180,975-byte notes field; --brief-deps projects each dependency to id, title, status, issue_type, priority, dependency_type and measured 89.3% smaller. Both are opt-in, both have HTTP twins (brief, brief_deps), and both are refused wherever they could not be honored rather than accepted and dropped. One caveat to design around: the JSON response carries no marker for the omission — an omitted field is indistinguishable from a genuinely empty one, so only the caller that passed the flag knows its rows are partial.
  • One guided, crash-resumable migration from v53 to v66. On an embedded or local store, the first invocation after installing migrates your schema in place — 13 main-series migrations plus 15 clone-local ones, about 28 migrations, with the counter visibly restarting partway through (that is the clone-local series, not a loop). Back up first, with the binary you have now.
  • A shared Dolt sql-server is never auto-migrated. Migrating one promotes the schema for every attached client at once, so a version bump there now waits for explicit consent — bd migrate schema after the fleet is upgraded, or BD_ALLOW_REMOTE_MIGRATE=1 in scripted use (#5920, [#6048]). The gate also runs on the proxied bd serve open path, which had been migrating shared databases on every open, reads included (#6055), and is scoped so a server bd auto-starts for a single workspace still migrates on open as it always has (#6088). Write commands respect a MIGRATION-FREEZE sentinel with a dedicated exit code 14 (#6043), and bd doctor honors --readonly and the freeze instead of --fix-ing through them (#6056).
  • Data-integrity heals across the Dolt plane. Duplicate typed dependency edges merge instead of aborting the rekey half-applied (#6045), a legacy tracked migration-cursor table is untracked at open so bd dolt pull unwedges (#6046), bd delete no longer wedges auto-export forever (#6059), an ignored-series sentinel gap no longer replays the whole clone-local chain and restamps wisp timestamps (#6054), and server-mode DOLT_ADD/DOLT_COMMIT now run after the SQL transaction commits, ending concurrent lost updates (#6040, carrying @nova-submodules' [#5740]).
  • Two long-standing correctness bugs that were silently costing you work. A dated defer now means what it says: --until set status=deferred and a timestamp, but nothing ever flipped the status back, so an expired defer stayed invisible to bd ready forever — one deployment measured 241 beads, including P1s, silently dark this way, regenerating daily from correct-looking automation. Ready-front reads now run a lazy wake sweep first (bd-i8qx8). And --label-any was emitted by bd list but silently dropped by bd ready and bd ready --claim on every backend, so a worker fencing itself to its own lane (bd ready --claim --label-any lane-a --parent epic-1) would happily claim another lane's bead and believe it was fenced (bd-s10oa).
  • JSON contracts fixed before 1.3.0 freezes them. String revision tokens, --set-metadata scalar typing, and bd create --json nano timestamps (#6053); --storage-class wired through the proxied, markdown, and graph create paths (#6060); journal delete and cascade rows carry the request actor (#6061); and GET /v0/beads/issues gains sort with order-bound cursors (#5666).
  • Errors tell the truth. bd bootstrap probes git remotes for Dolt data instead of rejecting them, and exits non-zero when it declines (#6037); proxied/gateway bd init attributes identity safely and classifies access denials honestly (#6062); the legacy-backend tombstone names the exact heal instead of destructive advice (#6044); a Homebrew --HEAD version stamp is recognized rather than misread as 0.0.0 and routed into a .dolt-deleting recovery (#6079); bd prime says when the memory plane could not be read (#5877).
  • Security posture for the tag. The Go toolchain moves to 1.26.7, closing seven stdlib advisories reachable from the shipped bd binary, alongside dependency bumps clearing all 25 open advisories with per-cluster reachability verdicts (#6047). Carried from the same arc: bd dolt push and bd sync no longer adopt a git-origin-derived Dolt remote without consent (#5068), the settings plane no longer serves the kv. memory plane by any read (bd-rfwtv, bd-klko9), and the no-ID "last touched issue" fallback is interactive-only (#4839).
  • The upgrade path is tested where it bites. A new wisp-plane upgrade corpus seeds and asserts the clone-local plane — wisps, leases, events, the ignored-migration cursor — that the 1.3.0 chain actually rewrites (#6049).

Upgrading Notes

  • On an embedded or local store, the first invocation migrates your schema, in place, from v53 to v66. A v1.2.2 (or any 1.1.x) database sits at main-series schema v53; this binary knows v66, so the first command that opens the store applies 13 main-series migrations, 0054_add_lease_columns through 0066_add_events_journal_actor. Two of those passes rewrite rows rather than just reshaping tables — the aux-row id rekey and the events dolt_ignore flip described under Changed — so on a large store the first invocation is noticeably slower than the ones after it. It is crash-resumable and picks up where it left off, but do not interrupt it if you can avoid it.
  • A shared dolt sql-server is never auto-migrated (#5920, [#6048]). A server-mode database is served to every client attached to it, so migrating it promotes the schema for all of them at once and locks out anything still on an older binary. Upgrade that server's clients first, then consent once:

```bash # 1. upgrade bd on every client of the server; reads keep working throughout bd version # on each client, confirm the new version

# 2. once, from a workspace already set up against this server: bd migrate schema # add --global for the shared global database

# 3. confirm bd doctor ```

Between steps 1 and 2 an upgraded client reads normally and its writes are refused with the gate's guidance — nothing is silently promoted, so there is no deadline, but keep the window short. BD_ALLOW_REMOTE_MIGRATE=1 is the scripted, auditable standing consent, and a client joining a behind-schema server is refused before its workspace is written, so join and migrate in one step with BD_ALLOW_REMOTE_MIGRATE=1 bd init …. If the shared server also has a Dolt remote, step 2 is not enough: two hazards apply at once and bd requires the stronger designated-migrator consent (bd migrate --force from exactly one machine, then bd dolt push). Recipes in Shared servers.

  • The counter restarts partway through, and that is not a loop. The clone-local (dolt_ignored) series runs after the main one, through the same printer and its own numbering, and it moves 0011 → 0026 on this upgrade. So the run is about 28 migrations, not 13, and what you see on stderr is 13 lines counting up to 0066 followed by 15 lines starting again at 0012:

Applying migration 0065: widen_wisp_comments_text… Applying migration 0066: add_events_journal_actor… Applying migration 0012: create_leases… ← clone-local series, not a restart

A counter that jumps backwards is the signature operators kill runs over. Let it finish. Progress prints only when stderr is a terminal — piped and CI runs see nothing at all, which is deliberate: a silent-looking CI upgrade is not a stuck one.

  • Back up first, with the binary you have now. Take the backup before you install 1.3.0. Under the new binary bd export triggers the auto-migration before it exports, so a snapshot taken afterwards is a post-migration snapshot and cannot protect you against the migration going wrong. On a remote-backed store, finish syncing with the old binary too: once 1.3.0 is installed the pending-migration gate refuses bd dolt push and bd dolt pull as well, not just bd migrate.

bash # with your CURRENT bd, before installing 1.3.0: bd dolt push # remote-backed stores only bd export --all -o .beads/backup/pre-1.3.0-$(date +%Y%m%d).jsonl

A JSONL export is cheap, issue-complete, and importable by any bd version. If you want a Dolt-native snapshot that keeps history and config, configure a destination and sync it — bare bd backup takes no backup, it is a command group that prints help and exits 0:

bash bd backup init <path-or-dolthub-url> # once, to configure a destination bd backup sync # take the snapshot

  • Upgrade every client that shares a store, together. The forward schema-skew guard means an older co-resident binary — a second bd earlier in PATH, a long-running bd serve, another clone's cron job — refuses a database migrated past what it knows, rather than proceeding blind. That is the guard working, not a bug, but it makes a mixed-version fleet a broken fleet: one machine running 1.3.0 takes the whole store forward and every 1.2.2 client stops. Run which -a bd after installing, and on a remote-backed store follow the designated-migrator procedure (one clone migrates and pushes; the rest pull or re-clone). See Upgrading for the per-install-method recipes and the multi-clone flow.
  • Expect your ready front to grow, and your first purge to clear more than usual. Two fixes in this arc surface work that was previously unreachable. Expired dated defers now wake back onto the ready front (bd-i8qx8) — if you use bd defer --until at all, beads you believed would come back have probably not been coming back, and they reappear after upgrading. And bd purge / bd prune now select candidates by tier (#5995), so wisps minted before the ephemeral column was set — one production database held 858 of them, reachable by no sweep — are finally swept.
  • Dolt is the only storage backend; SQLite, PostgreSQL and MySQL are gone. If you read the [1.2.1] changelog section directly, note that its "Storage backend scope simplified" entry states SQLite remains a supported storage path. That sentence is stale and does not describe this release. The direct PostgreSQL and MySQL adapters were rolled back before ever entering a tagged release; SQLite was removed in the open-core split. At the tag the supported paths are embedded Dolt, Dolt server, and any conformance-passing backend registered through the public backend package. sqlite, postgres and mysql survive only as recognized-and-rejected names so a stale "backend" field in .beads/metadata.json produces a targeted heal instead of a confusing failure (#6044), and bd migrate legacy-sqlite --source-db PATH is a read-only JSONL extraction path off an old database.
  • If you need to go back, the rollback is a schema-cursor rollback, not a downgrade of the data: the procedure is written up in the recovery runbook (repo copy: docs/recovery/accidental-1-2-1-release.md). Its worked example is the v53↔v65 case from the accidental 1.2.1 release; the steps are the same for v66, with the version numbers adjusted.
  • After upgrading, bd upgrade review prints exactly the entries between the version you were running and this one. Prefer it to bd info --whats-new, which dumps the entire release history. Several commands changed defaults, so read it before your first session.

Breaking changes for v1.2.2 users

This is a highlights list, not the complete set. A v1.2.2 user is crossing two releases at once, and the breaks below are the ones most likely to stop a script or a service. The full set is the [1.2.1] section of the CHANGELOG plus the Changed section here — read [1.2.1] in full before upgrading, since v1.2.2 withheld it and v1.2.1 itself was pulled, so nobody on the supported line has seen it.

Carried from [1.2.1], never shipped to v1.2.2 users:

  • bd update --status <done-status> now enforces close policy — open children or a live direct blocker refuse the move, matching bd close. Override with bd update --force; an unforced refusal rolls back the entire batch.
  • bd search includes closed issues by default (bd-t5yex). Narrow with --status open for the old behavior; bd list keeps its open-only default. Note the trade: matches beyond --limit (default 50) are dropped under a status-blind sort, so a broad query on a large database can now fill the page with closed matches and silently drop open ones.
  • The no-ID "last touched issue" fallback on bd update / bd close is interactive-only (#4839). A scripted bd update $ID … with an empty $ID now refuses instead of mutating whatever was touched last. Set BD_LAST_TOUCHED_FALLBACK=1 if a script genuinely relied on it.
  • bd human list hides done/frozen and pinned beads by default, and validates --status (#5332). A --status typo is now an error rather than an empty list.
  • bd dolt push and bd sync no longer adopt a git-origin-derived Dolt remote without consent (#5068). Adoption now prompts (defaulting to no) and fails closed non-interactively; --yes/-y consents ahead of time, and --no-adopt / BD_NO_REMOTE_ADOPT=1 disables it entirely and wins over --yes. CI and cron that relied on the silent adoption will now fail closed.
  • bd --readonly serve is refused instead of binding a server that cannot do what it advertises. Anything scripted as a "safe" read-only server does not start after this upgrade — drop the flag. Worth checking before you restart a long-running bd serve as part of the fleet upgrade above.
  • bd config list and GET /v0/beads/config no longer enumerate the kv. plane, which is where bd remember memories live — that closed an unauthenticated endpoint handing out every stored memory. A later landing in the same era closed point reads too: bd config get kv.memory.<slug> now answers exactly as a key nothing ever stored does (echoed key, empty value, nil error), because bd remember derives its key from the content it stores and the names are guessable. Use bd kv, bd recall and bd memories, which read the store directly and are unaffected; bd config set/unset still take a verbatim kv. key as the escape hatch for a wedged memory.
  • bd delete --force on a team server orphans dependents instead of deleting them (bd-x82so). Against a proxied server bd delete X --force used to delete X and its whole dependent subtree, and --cascade was refused outright. Both routes now carry the local database's meanings: plain bd delete X is refused if anything outside the request depends on X, --force orphans dependents, --cascade --force takes the subtree. A script that relied on the implicit cascade will start leaving orphans — add --cascade.
  • bd dep cycles --json emits a new shape (bd-wfkbv). An array of {"members": [{"id", "issue"?}], "partial": bool} where it used to emit an array of arrays of issues. The report is also canonical now (members rotated so the lowest id leads, cycles sorted against each other) and honest about members it cannot resolve, which previously vanished from the path — so a three-node cycle could render as a two-node one and look complete.
  • bd list --ready refuses a filter it cannot honor (bd-yby99.8) instead of silently ignoring it. bd list --ready --id tst-d68 used to answer with every ready bead in the workspace. Refused with --ready: --id, --title, --spec, the *-contains family, --external-ref, any date bound, --deferred, --overdue, --empty-description, --no-labels, --no-parent, --pinned, --priority-min/max. A script that passed one was already getting the wrong answer.
  • bd list --status=all stops hiding pinned beads (#5332). all promises every status, but the pinned exclusion compared the raw selector string, so all fell through and kept forcing Pinned=false — and --status=pinned,closed excluded the pinned beads it explicitly asked for. Add --no-pinned to keep the old result.
  • A dependency type is bounded at 32 characters, not 50 (bd-yby99.3). The column on both dependency planes is VARCHAR(32), so the old bound accepted a range no row could carry. In practice nothing should notice — the longest well-known type, conditional-blocks, is 18 characters — but the error text changed: messages that read max 50 chars now name 32.
  • Text-input commands refuse two sources (#5332). bd comment, bd note and bd comments add used to let --stdin/--file win over positional text and drop the positional half silently; the dropped half was, in every case found, the text the caller meant. Naming two sources is now an error.
  • bd label add/remove over many beads is no longer one atomic transaction (ga-26w10, [#5489]). bd label add a b c mylabel records three history entries instead of one, and a failure on the third leaves the first two written where the old shape rolled them back.
  • --dolt-auto-commit batch/off actually defer version commits in SQL-server mode (bd-4wamg). The mode was silently inert there. If you explicitly configured dolt.auto-commit: off, it was behaving like on; it now genuinely stops minting version commits until an explicit bd dolt commit. Proxied-server routes never apply auto-commit policy, so batch/off remain inert there.
  • The published backend package drops orphan handling (bd-gwryr). A Go consumer that names backend.OrphanHandling or its constants no longer compiles. Delete the option — every call site passed OrphanAllow, which is exactly what the code still does.
  • bd purge / bd prune version-control messages changed (bd-pn231). The entry is now bd: sweep <n> <tier> bead(s) on both routes, replacing bd: prune <n> bead(s) and bd: delete <n> issue(s).

New in 1.3.0 (full entries under Changed):

  • --profile is now --cpu-profile, with no alias (#5126). The old spelling fails as an unknown flag rather than silently doing nothing.
  • An explicitly configured Dolt server port now outranks the ambient BEADS_DOLT_PORT environment variable.
  • Actor matching decodes an exact -- run to / instead of collapsing it to a generic separator, so gastown--mayor matches gastown/mayor and stops matching gastown__mayor.

Added

First shipped on a tested release here — the withheld [1.2.1] work

  • bd serve — the beads work surface over HTTP (bd-serve v0; [#5410], [#5417], [#5422], [#5423], [#5429], [#5506], [#5507], [#5508], [#5510], [#5535], [#5536], [#5540]). One process answering 41 OpenAPI-specified operations across 35 paths instead of a bd subprocess forked per call. Reads: context, ready, issues (list, get, query, count, related, comments), stats, dependencies (list, count, tree, blocking, cycles), config and config/{key}, memories, events and events:watch. Writes: :claim, :claimNext, :release, :close, :reopen, PATCH /issues/{id}, :batchCreate, :batchClose, :batchApply, :delete, :sweep, :casMetadata, dependencies:add/:remove. Close and reopen are idempotent and say so in the body (already_closed: true) rather than in a status. PATCH publishes nineteen members, four of them nullable where an explicit null clears them — a closed, machine-checked set, so a null on any other member is a 400 rather than an unannounced clear. expected_version on close/reopen/delete plus revision on the detail read give the row's optimistic-concurrency token, so a read-modify-write loop can start from a read rather than seeding its first guard from a write it did not want to make. Two operating caveats: an embedded-Dolt workspace is permanently refused (that backend commits outside the SQL transaction, so per-request atomicity would be a lie), and --addr 127.0.0.1:0 takes an ephemeral port with no mutual exclusion — pass an explicit port. Probe readiness with GET /v0/beads/ready?limit=1; /healthz is liveness only and stays green while the database is unreachable.
  • Bearer authentication and a supported beyond-loopback deployment for bd serve (#5516). See the Highlights entry for the full posture. BEADS_SERVE_TOKEN_FILE is the environment fallback; 401/unauthenticated joins the frozen code vocabulary with a fixed detail that never echoes the presented credential, and missing header, wrong scheme and unknown token are deliberately one code.
  • Bd-Project-Id request stamping, and the project.enforce capability. A client that knows which workspace it means to address stamps the request with that workspace's project id; a server serving a different one refuses before any database work, so a misdirected read or write mutates nothing. An absent header is served exactly as before, so this is additive wire surface, not a new precondition. GET /healthz and GET /v0/beads/context are exempt — liveness must answer whatever workspace a caller believed it reached, and the handshake is where a client learns the id to stamp with. The refusal is the only one carrying server_project_id, so its presence is the signal that this check fired rather than the Host gate or a deployment's auth layer.
  • Work leases: bd heartbeat, bd reclaim, bd unclaim (#4537, schema v54, migration 0054_add_lease_columns — the first of the 13 main-series migrations this upgrade applies). See Highlights. bd reclaim also takes the full claim-side scope surface — --label, --label-any, --exclude-label, --assignee, --id (wy-jpd3.3) — so a supervisor can point its reaper at exactly the partition it claims from; filters AND-combine and never widen the stale set, and a scope flag supplied with no usable value is a hard error rather than a silent degrade into a global sweep.
  • Replica-aware leases (wy-jpd3.7, ignored migration 0016). leases.granted_node rides the JSONL interchange so an imported lease keeps its provenance, and --any-replica is the escape hatch for a replica that is permanently gone. Deliberately no hostname fallback: on a shared dolt sql-server many hosts are clients of one store, and a hostname guard there would stop a supervisor reaping any worker's lease at all. Documented limitation: a heartbeat proves the holder is alive, not that the lease moved, so granted_node is backfilled but never overwritten — a renamed replica keeps reading foreign and is recoverable only with --any-replica.
  • bd sync — the federation loop as one verb (wy-jpd3.4), and bd conflicts list|show|resolve (wy-jpd3.5) as the companion that clears an exit-2 halt. bd conflicts show renders each conflicted row field by field (only disagreeing fields unless --all-fields); bd conflicts resolve takes named beads row by row or whole tables with --all, via --ours/--theirs/--strategy, then concludes the merge. Note bd conflicts is not supported in proxied-server mode.
  • Compare-and-set updates: bd update --if-assignee / --if-status (bd-wsqvw, epic wy-mdi5h). See Highlights. Guards require a field update to ride on, are mutually exclusive with --claim (its own CAS), and compose with each other and with the engine's ExpectedVersion row CAS. Library consumers get them through new UpdateIssueOptions.ExpectedAssignee/ExpectedStatus on the existing UpdateIssueChecked — no interface change. An out-of-tree Storage implementation that ignores the new fields simply does not enforce them, and should add support before advertising guard semantics.
  • Pool-aware claiming via the claim.pools config key (bd-bguz6). Off by default; with no claim.pools configured, behavior is unchanged. One gotcha: if a pool take's lease expires, bd reclaim returns the bead to the unassigned pool, not to the pool alias it was dispatched to.
  • A durable events journal, and bd events tail|export|prune (bd-opisf). See Highlights. HTTP twins are GET /v0/beads/events and a streaming GET /v0/beads/events:watch. Not journaled, by design: bd dolt pull, merge-settled changes, raw bd sql DML, store-open migrations and compaction rewrites. Size the retention floors for the longest outage a consumer must survive — they are a recent-window guarantee, not a consumer watermark. Reference: docs/reference/events-journal.md (github.com).
  • A public Go API: the backend and issueops packages plus a conformance suite (bd-h3dib.2, [#4415], bd-yby99, [#4911], facade waves 1–4). See Highlights. Note the beads.Storage interface gained required methods across this arc (IssueClaimer(), IssueReader(), ReadyClaimer(), BatchCloser(), DependencyEditor(), Commenter(), IssueRelations(), UpdateIssueChecked, MergeMetadata) — callers are unaffected, but any external type that implements it must add them to compile.
  • --brief on bd list / bd ready and --brief-deps on bd show (#5546, [#5547], [#5549], [#5554], [#5586]), with brief / brief_deps on the HTTP twins. See Highlights for the measurements. Refused where it could not be honored: on bd ready it requires --json and is refused with --claim, --gated, --mol and --explain; on bd list it works in text mode but is refused with --watch, the --parent tree walk, and --format, where a caller's template could print a dropped field with nothing to mark it. Text mode carries the single visible marker: bd list --long --brief prints Description: (omitted by --brief).
  • --max-rows on the walking reads, plus BEADS_MAX_ROWS. A hard upper bound on rows returned by bd list, bd ready, bd graph, bd dep tree and bd find-duplicates, exiting 2 when exceeded; 0 disables. Honored on both the direct and the proxied-server route. A circuit breaker for CI and agent rigs against one bad filter returning a multi-hundred-megabyte result, settable fleet-wide by environment.
  • bd provenance record|log|by-ref — an append-only provenance event log (#4461). Records typed bindings from a bead to an opaque external artifact: --kind (cut, claim, suspend, resume, handoff, commit, land, used), --source, --ref with --ref-kind (git-sha, pr, work-id, transcript, branch). Append-only — no update, no delete — and idempotent on a deterministic id computed from source:issue:kind:(ref or --at), so a git hook or CI job that fires twice is harmless. beads never interprets the actor or the ref; only kind and ref-kind are structurally validated. This is the supported way to bind beads to commits, PRs and transcripts without abusing labels or metadata.
  • bd schema — a published JSON Schema for --json and export output (#5098). Reflected from the same Go structs bd actually serializes, so it cannot drift from the real output; named string enums carry their allowed values and referenced types are inlined, so each record schema is self-contained and directly consumable by codegen (bd schema | jq '.types.issue'). Runs without a workspace or database. Reference: docs/reference/json-schema.md (github.com).
  • Cursor agent hooks, and Cursor at parity with Claude Code and Codex. bd setup cursor installs .cursor/hooks.json alongside the rules file, wiring three lifecycle events to a hidden bd cursor-hook: sessionStart injects full bd prime context into every new agent session, preCompact arms a one-shot refresh marker, and postToolUse re-injects bd prime exactly once after a compaction then no-ops — so beads context survives compaction instead of being forgotten mid-session. Existing user hooks are preserved and --remove only removes the beads-managed entries. Parity work rides along: bd init auto-installs Cursor the way it already does Claude Code and Codex; --global writes ~/.cursor/hooks.json and the agent skill to ~/.agents/skills/beads; .cursor/rules/beads.mdc now wraps the shared recipes.Template instead of a hand-maintained copy that drifted; and bd doctor reports Cursor Integration, Cursor Settings Health and Cursor Hook Completeness. Verified on the Cursor 2026.06 line; early-2026 CLI builds only fired shell hooks.
  • Vendor-neutral credential resolution for a protected or gateway database. A new credential seam resolves what bd uses to open a protected database at command time, backend- and issuer-neutral: a source yields either a secret that lands in the password slot of a direct connection, or an identity presented as the connection username to an authenticating gateway, and an ordered ladder takes the first configured hit and fails closed when a configured source errors. The built-in rung is the standard credential-process idiom (kubectl ExecCredential, AWS credential_process, git credential helper): the command's stdout is a short-lived token, bare or in a {token,expirationTimestamp} / {access_token,expires_in} envelope. Configured via BEADS_DOLT_CREDENTIAL_COMMAND — environment only, deliberately not metadata.json, because a metadata-sourced command is arbitrary code run on open. The credential's kind is decided by the config slot that produced it and never inferred from the value, so an identity token can never be mistaken for a password.
  • bd migrate --force, and automatic fast-forward when a remote-ahead adopt is provably loss-free (#4259, [#4516]). bd migrate --force (and bd migrate schema --force) is the CLI twin of BD_ALLOW_REMOTE_MIGRATE=1 for the single designated migrator, and it is process-local so it cannot leak into child processes (git hooks, dolt subprocesses) the way an exported variable does. Separately, when the smart gate finds the remote ahead with no content skew, this clone's local Dolt history a strict ancestor of the remote's with a clean working set, and the fast-forward would land exactly at this binary's own latest migration, bd fast-forwards automatically rather than stopping with an adopt directive — nothing local is discarded. An unpushed local commit, a dirty working set, or a remote not exactly at this binary's latest migration all disqualify it and fall back to the manual adopt directive, never a forced write. BD_SMART_GATE=0 opts out. Directly relevant on upgrade day, when every clone is behind at once.
  • bd dolt clean-databases --purge-dropped, including in proxied-server mode (be-pq5, [#3663], p1-9lf). DROP DATABASE only moves a database's directory under .dolt_dropped_databases/; Dolt keeps the data there — recoverable via CALL DOLT_UNDROP(name) — until an explicit purge, so disk usage on a shared server stayed high across repeated cleanup runs. The flag runs that purge, and fires even on a run that finds nothing stale, since a prior run's residue is not visible to SHOW DATABASES. Read the warning as written: CALL DOLT_PURGE_DROPPED_DATABASES() is server-global and irreversible — Dolt cannot scope it to the databases a given run dropped, so this also permanently deletes every other dropped-but-unpurged database on the server and removes DOLT_UNDROP recovery for all of them. It defaults to off for exactly that reason.
  • Deployment-mode migration commands, and bd migrate legacy-sqlite. Four bd migrate mode-switch subcommands move a workspace between Dolt deployment modes in place — from-server-to-proxied-server, from-proxied-server-to-server, and the two shared-server variants — all marked experimental. Both proxied-server and server mode root their dolt sql-server at the same .beads/dolt directory, so the switch is a config change rather than a data move. This matters because bd serve refuses embedded Dolt: these are the supported route from the default workspace mode to one that can serve HTTP. bd migrate legacy-sqlite --source-db PATH --output PATH|- reads an authenticated legacy SQLite database read-only and emits JSONL. bd migrate-personal moves your own planning beads out of a project database into your personal planning repo.
  • New global flags: --no-color, --database, --mem-profile. --no-color also honors NO_COLOR=1 / CLICOLOR=0. --database runs a single invocation against a different server database without changing the project's configured database (proxied-server mode only) — and note the paired semantic shift: in proxied-server mode a --db value that is not an existing path is now treated as a database-name override. --mem-profile FILE writes a heap profile on exit (also BEADS_MEM_PROFILE).
  • New bd init deployment flags, all experimental where marked: --team-server (the shared database's schema is managed externally; bd never creates the database or runs migrations, only verifies the schema version — proxied-server mode only), --server-tls (require TLS for the init-time Dolt server connection; not persisted, so set the environment or credentials for subsequent commands), --proxied-server-port, --proxied-server-idle-timeout (default 30s; 0 keeps the proxy alive indefinitely), and the --proxied-server-external-tls-* trio.
  • Assorted new flags across existing commands. bd create --storage-class {versioned|unversioned|ephemeral}, --status, --allow-empty-description; bd list --deps[=scheduling|all], --external-ref, --external-contains; bd ready --label-regex, --label-pattern; bd graph --open; bd status --no-blocked (not supported in proxied-server mode); bd export --exclude-owner (repeatable; also reads export.exclude_owners) and --verbose; bd prime --no-memories, --max-memories, --max-memory-chars — agent context budget is now tunable; bd types --sections; bd history --events; bd prune --ignore-references; bd q --parent; bd human respond --file/--stdin; bd worktree remove --merged-into; bd dolt pull --strategy; bd dolt remote reset-data (the recovery step after a history squash, where a plain push cannot advance a rewritten history); and bd formula schema, which prints every exported struct a .formula.toml/.formula.json can declare, generated from source so it cannot drift.

New in [1.3.0]

  • The events journal records WHO performed each mutation. bd_events_journal gains an actor column (migration 0066 plus its ignored-series twin 0025), stamped inside the mutating transaction with the same identity the audit-events table resolves. bd events tail / bd events export JSON gains an additive, omit-when-empty actor field — empty means the path had no actor, never a user. Delete and cascade dep_remove rows carry the request actor too, and reading a disabled journal now says so instead of returning silence (#6061, [#5985]).
  • sort on GET /v0/beads/issues (#5666). Two values, and the set is closed: created (the existing order, now spellable) and priority (bd list's flagless ordering) — measured over a 1400-row store at the 200-row page an HTTP client actually uses, ?sort=priority turns a 7-request page-and-resort walk into 1 request. Cursors are now v2 tokens that carry their order, and a cursor replayed under a different sort is refused as invalid_cursor instead of silently paging a different total order; outstanding v1 tokens stay readable, so no traversal in flight has to restart. Absent sort still means created, permanently.
  • bd warns when it stores a label containing a space (#5813). -l 'good first issue' is stored as asked, but a missed comma looks identical at the point of writing, so a stderr line catches it at the keystroke: ⚠ Stored "auth backend" as ONE label — it contains a space. Advice, not an error; --quiet silences it. bd doctor gains a matching warn-only Label Whitespace check, and bd doctor --fix repairs damage already in the database.
  • bd stale and bd blocked gain --label, --label-any and --exclude-label (#5822), with the same meanings they already have on bd list and bd ready. Filtering happens in SQL ahead of LIMIT, so --label x --limit 10 returns ten matching beads.
  • New *.gate.lock files appear next to and inside .beads (#5046, [#5093]). A two-level cooperative flock gate serializes operations that cannot safely overlap — ordinary commands take it shared, maintenance like bd backup restore takes it exclusive. The files are the lock's name, not its state: created once and deliberately never deleted. Both are covered by the *.gate.lock* gitignore pattern bd doctor maintains.
  • A wisp-plane upgrade corpus (#6049). Neither upgrade harness had ever seeded a wisp, so the clone-local plane the 1.3.0 chain rewrites — wisps, wisp_dependencies, wisp_comments, leases, events, and the ignored-migration cursor — used to reach the candidate binary empty. A new lane seeds those classes and asserts them across the upgrade, including detection of an ignored-track replay the old harness was structurally blind to.

Changed

First shipped on a tested release here — the withheld [1.2.1] work

  • bd search includes closed issues by default (bd-t5yex). The dominant real-world query is "was this already found/filed/fixed?", exactly the query where silently excluding closed beads produced a false "no" — downstream repos had grown shell wrappers purely to undo the old default. bd list keeps its open-only default, and bd query (plus the HTTP q= endpoint) deliberately keeps its closed-exclusion default with opt-in --all. Applies to both the embedded and proxied-server paths.
  • Cross-type blocking dependencies are now allowed (bd-wg7ve, [#4034], supersedes GH#1495). bd dep add <task> <epic> — gating a work item on an epic completing — used to fail with a backwards-reading error. The blanket same-type rule is replaced by a hierarchy deadlock guard that rejects only the cases that actually wedge the graph: gating a bead on its own ancestor (which cannot close until its descendants finish) or on its own descendant (blocked status cascades down to the very bead that must close to clear the gate). Sibling ordering edges stay allowed, and the guard now covers conditional-blocks, which previously skipped cross-type validation entirely. A task gated on an epic becomes ready when the epic closes. Over HTTP the refusal is 409 dependency_cycle carrying issue_id, blocker_id and blocker_is_ancestor; the absence of those members is what tells a client it got a plain scheduling cycle instead.
  • bd delete is one transactional role across every route. Existence probe, dependents guard, cascade expansion, deletion and the [deleted:ID] reference rewrite now run as one transaction on a shared role used by the direct route, the proxied route and bd serve. Consequences beyond the team-server break listed above: bd delete --cascade on a wisp no longer marks live beads as deleted in other beads' text (the rewrite set was computed differently from the delete set, and bd wisp gc ran exactly this path), a one-id delete without --force refuses when the bead has an outside dependent instead of printing a preview and exiting 0, and the whole request is refused when any id names no bead. Know the trade: the transaction is now as large as the deletion plus its graph neighbourhood, so a delete whose neighbourhood exceeds the backend write timeout fails whole rather than deleting rows and leaving stale text — split very large --from-file batches. Text already corrupted by earlier builds is not repaired.
  • bd purge and bd prune select and delete in one transaction (bd-pn231). The count a sweep reports is now the set it deleted; previously a bead closed between the two transactions could be counted and not deleted, or deleted and not counted. Same trade as above: the Dolt server backend can no longer batch wisp deletions 200 at a time, so a purge big enough to exceed Dolt's write timeout now fails whole instead of deleting part of the workspace and reporting nothing — sweep in slices with --pattern or --closed-before. Over HTTP, POST /v0/beads/issues:sweep defaults protect_referenced to true where the CLI's --ignore-references is opt-out, on purpose: being authenticated says nothing about whether this particular deletion was meant.
  • bd create --file is all-or-nothing, and its plan-wide flags now mean something (bd-lu170). Both routes build one batch request on a shared role, so a markdown plan is created as one act with one history entry on either. A file the workspace refuses now creates nothing — each template goes through the same validation a single bd create goes through — and a declared dependency on an id that does not exist is an error rather than a silently dropped edge, which is how a plan file with a typo used to produce a graph that looked complete and was not. --ephemeral, --no-history, --mol-type and --validate are now honoured on the direct route, where all four were accepted and silently ignored while the proxied route acted on them. Re-running a file that used to fail can give a different outcome, because the earlier run may have landed rows this one refuses to duplicate — check bd list before re-running a plan that errored on an older version. bd create --graph is untouched.
  • bd serve runs on a registered storage backend, and context stops guessing. bd serve alone used to reimplement a creation path hard-wired to Dolt's SQL wire; it now uses the store the root command already opened, through the same registry dispatch — one creation path rather than two, with the startup line naming both source and backend. Relatedly, the shared identity projection behind GET /v0/beads/context and bd context hardcoded backend: dolt and copied dolt_mode and database unconditionally, so a registered-backend workspace was confidently reported as backend="dolt", dolt_mode="embedded", database="beads" on the one endpoint automation is told to trust for a server's identity. The backend is now named as the store open resolves it, which also closes a latent CGO_ENABLED=0 bug where a registered workspace was handed to the Dolt provider and either failed with a misleading error or connected to a defaulted host and served the wrong database.
  • Field-level three-way auto-merge on pull. beads stamps issues.updated_at on every mutation, so any two edits to the same bead on two replicas between syncs collide on that cell even when the semantic fields are disjoint — machine A adds a comment, machine B adds a label, and the row conflicts on nothing but the timestamp both bumped. The observed conflict rate was therefore far higher than the semantic-conflict rate, and the row-level last-write-wins resolver could only take the safe half of it: it declined whenever both sides had moved updated_at past the merge base, because taking one side's whole row would drop the other side's field edits. A field-level three-way merge now encodes beads' actual write semantics — a column only one side changed relative to the merge base keeps that side's value, and only a genuinely contested cell falls to last-write-wins. Reached through bd dolt pull and bd sync; there is no flag.
  • bd label add/remove over many beads is no longer one atomic transaction (ga-26w10, [#5489]). Both routes now end at the shared lifecycle and reader roles, deleting the four-way (add|remove) × (wisp|durable) switch the proxied route hand-rolled below them. The deliberate delta: a multi-bead edit is N guarded updates rather than one raw transaction, so it records N history entries and a failure partway leaves earlier edits written. Neither shape is obviously right — the new one attributes each edit to its own bead and lands what it could — but a script that depended on all-or-nothing label edits needs to know the guarantee changed. bd label list-all and bd label propagate are unchanged and deliberately stay off the role.
  • Text-input commands refuse two sources, and bd human matches bd list (#5332). See the breaking list for bd comment/bd note/bd comments add and --status=all. Alongside them: bd human respond gains the same input layer (--response is no longer required) and bd human dismiss takes its reason positionally; bd human list matches bd list's status handling while hiding no bead type, because a human label is an explicit request for a person's attention; and bd human stats classifies dismissals by the Dismissed close-reason prefix shared as one constant with bd human dismiss, instead of matching the substring dismiss anywhere in the reason. Beads closed before this with a lowercase or mid-string "dismiss" move from the Dismissed count to the Responded count.
  • --dolt-auto-commit batch/off actually defer version commits in SQL-server mode (bd-4wamg). The mode was silently inert there: the CLI's policy was embedded-only and the storage layer minted one Dolt commit inside every write transaction regardless — measured at one commit per write through all five configuration paths. The write verbs now thread the deferral to the storage layer's commit sites in server and embedded mode alike. The server-mode default changes spelling, not behavior: it used to resolve to off while behaving like on, and now resolves to on, naming what a default server-mode write has always done. On a shared server, staging is table-level, so an on-mode writer's commit may sweep up another client's deferred rows — deferral bounds who creates commits, not commit contents.
  • The PostgreSQL and MySQL adapters were rolled back before entering a tagged release (bd-sadcd). The rationale is worth stating because it is the shape of the storage story: dialect, credential, schema-lifecycle, migration, CI and operational complexity, against a project whose version-control semantics are Dolt's. An existing PostgreSQL or MySQL workspace stops before its configured database is opened or modified. See the Upgrading note above for what the supported storage paths actually are at this tag.

New in [1.3.0]

  • Shared stores refuse version-bump migration without explicit consent (#5920, [#6048], [#6055], [#6088]; closes the gate leg of [#5043]). Embedded and single-user databases still auto-migrate silently, and fresh databases still migrate on creation — creating a database is consent for its schema. But a shared dolt sql-server accepts co-resident clients by construction, so a version bump there refuses instead of taking the whole fleet's schema forward as a side effect of one upgraded client; the smart gate's "safe first-mover" and auto-fast-forward arms are embedded-only for the same reason (#4516). The gate now also runs on the proxied bd serve open path, which had been migrating shared databases on every open — including from read commands — since v1.2.2: read commands warn and continue, writes refuse, bd serve fails to start until the schema is reconciled, bd migrate schema works in proxied mode instead of refusing, and gate failures print their full actionable block instead of one truncated line. "Shared" is drawn deliberately (#6088): shared-server mode, an operator-managed server, a remote host, a TLS endpoint, a unix socket, or no workspace at all — a server bd auto-starts for a single workspace is a database no other client can observe, and migrates on open as it always has.
  • bd doctor respects --readonly and the migration freeze (#6056; fixes [#6028]). bd doctor --fix previously mutated workspaces that strict --readonly and an active freeze had both declared off-limits — and plain bd doctor, with no --fix at all, could rewrite .local_version or auto-migrate a frozen store as a side effect of diagnosis. Both bypasses are closed, with no doctor-side override, and both gates key on the directory doctor was pointed at rather than only the one it was launched in.
  • Write commands refuse to run while a MIGRATION-FREEZE marker is present (dc-6jaq, [#6043]). Discovery is generic: the marker is found in the workspace directory, the working directory, or any ancestor of either, and BD_MIGRATION_FREEZE_FILE is authoritative when set. Every write command (~120 call sites, bd q included), plus bd init --reinit-local and bd bootstrap, prints ⛔ workspace is frozen for migration naming the marker's own path and exits 14 (ExitMigrationFrozen) instead of a generic 1. Read commands keep working, and bd's own maintenance stands down with them — auto-migration, JSONL auto-import, Dolt auto-commit, auto-backup, auto-export and auto-push are all skipped, so a frozen store is never rewritten just because someone ran a read. A running bd serve is not gated: stop it before freezing.
  • Migration 0061 rekeys every events, comments, snapshot and compaction-snapshot row to a content-derived id (#5150). Random UUIDv7 ids never converged under newest-wins replication; ids are now UUIDv5 over the row's own content, so the same fact derives the same id everywhere. Existing rows are converted by a one-time, crash-resumable bulk rekey on the first open after upgrade — the main reason that invocation is slower. Any external reference to an event, comment or snapshot id taken before the upgrade will not resolve afterwards.
  • The events audit table is now clone-local (#5162). Migration 0062 moves events onto the dolt_ignored plane, preserving every row. Events keep full SQL durability locally and bd history <id> --events still reads them, but events rows no longer replicate — minting a versioned Dolt commit per audit row was the dominant source of commit churn on a busy store. Comments stay versioned.
  • The interactions.jsonl audit sidecar is opt-in (#4688). audit.enabled defaults to false and bd init no longer creates the file; the database-backed replacement is bd history <id> --events.
  • Auto-backup defaults to off under a Dolt sql-server (wy-zrmqr) — on one shared server, ~31 clients had each independently decided to back up the same database. Embedded mode is unchanged; an explicit backup.enabled always wins, and bd config get backup.enabled now prints the effective value with its source.
  • --profile is now --cpu-profile, with no alias (#5126). The old spelling fails as an unknown flag instead of silently doing nothing.
  • bd import refuses a redirected stdin rather than quietly ignoring it (#5171). bd import < file.jsonl looked like bd import - and behaved like bare bd import; it now errors and names both fixes.
  • bd hooks install --chain and --force are accepted no-ops (#5284). Managed marker sections always preserve content outside them and always keep an existing hook running alongside the bd section; symlinked and git-tracked hook paths are refused before anything is written.
  • An explicitly configured Dolt server port now outranks the ambient BEADS_DOLT_SERVER_PORT/BEADS_DOLT_PORT environment variables (GH#4052). A port asserted by the user — bd init --server-port, config, a library caller — is no longer silently replaced by whatever the surrounding shell exported, and the legacy BEADS_DOLT_PORT spelling no longer overrides configured ports. Ports bd resolved for itself stay overridable by either spelling, and a stale .beads/dolt-server.port left by a crashed server no longer makes auto-start refuse over a port nobody configured.
  • The legacy-backend tombstone rejection is truthful (#6044). A workspace whose metadata.json still says "backend": "sqlite" (v1.2.x opened these as Dolt anyway) is still refused fail-closed, but when live Dolt data is detected the message now carries the exact one-field heal instead of advising an export/reinit that would have destroyed a database one edit from healthy. Detection helper ported from @steveyegge's [#4740].
  • The new exported Go packages are marked experimental before the tag freezes them (#6036). backend, beadserrors, issueops, journalops, memoryops, schema and the conformance profiles ship usable but explicitly unfrozen — the extension door for out-of-tree backends stays open without a v2 to walk back through. Pin an exact beads version and re-run the conformance suite on every bump.
  • bd show labels created_by as Created by:, not Owner: (be-ss66); bd dep add names the implicit type=blocks default to interactive operators (#5854); bd reclaim summarizes replica-guard skips in one line instead of one line per stranded lease (wy-sp2l4); beads_dir and repo_root become OPTIONAL in ContextResponse (bd serve still publishes both — the relaxation is a promise to clients, not a switch on the server); and actor matching decodes an exact -- run to / (be-p7dzx).

Fixed

First shipped on a tested release here — the withheld [1.2.1] work

  • A dated defer now means what it says: expired defers return to the ready front (bd-i8qx8). bd defer --until and bd update --defer set status=deferred plus a defer_until timestamp, but nothing ever flipped the status back — so an expired defer stayed invisible to bd ready forever until a human ran bd undefer. One deployment measured 241 beads, including P1s, silently dark this way, regenerating daily from correct-looking automation. Ready-front reads and claims (bd ready, bd ready --claim, bd list --ready, and the serve/proxied reader roles) now run a lazy wake sweep first: every bead and wisp with status=deferred and defer_until <= now flips to status=open, defer_until=NULL, byte-identical to what bd undefer writes so a later dateless re-defer cannot inherit a stale past date. Each wake records a status_changed event with actor bd-defer-wake. What does not change: a dateless defer is the indefinite icebox and never auto-wakes. The sweep is a no-op when nothing has expired (no Dolt commit is minted), advisory by contract (a ready listing never fails because the sweep could not run; strict --readonly skips it), and identical in embedded, server and proxied-server modes.
  • --label-any is no longer silently dropped by bd ready and bd ready --claim (bd-s10oa). The ready-work WHERE builder emitted clauses for --label and --exclude-label but none for --label-any, so the OR-set filter was ignored on the ready and claim paths on every backend — while bd list and bd search honored it. On an atomic claim this was dangerous rather than merely wrong: a worker fencing itself to its own lane would claim another lane's bead and believe it was fenced. An exhausted lane now claims nothing instead of falling back to unfenced work. Related: bd ready --claim now honours the directory-label scope, where the branch applying a directory's configured directory.labels default tested the filter it had already written the default into, so it could never fire.
  • bd query no longer drops matches from an OR or NOT query (bd-pohmh). To answer an expression the storage filter cannot express, both routes fetched max(3 × --limit, 100) rows and applied the expression to those in memory. A match past that window was absent from the page — and the truncation hint said nothing, so the answer looked complete: bd query "type=bug OR label=urgent" over a workspace with more than a hundred beads has been returning an arbitrary prefix of its answer. The window is gone; the page is the first --limit matches and the truncation hint is exact. --offset N now works with OR/NOT where it used to be refused outright. Your results will change, and they change by getting bigger. The cost is real: a broad OR/NOT expression over a large workspace now reads every row its plain filters admit — narrow the expression if that matters.
  • Claim-family writes are verified by re-read in Dolt server mode (bd-zccb9, incident wy-ejph3). Under a degraded sql-server the exit status of bd update --claim / bd claim / unclaim was not truth in either direction: a claim could report success while the server-side transaction died with the abandoned connection and rolled back — a phantom claim that later cost a duplicate implementation — and conversely a connection error could print with the write actually applied. bd now re-reads the bead's assignee and status on a fresh connection after the claim transaction and resolves the outcome against the database: a reported success that did not land fails loudly, and an ambiguous commit-phase loss is settled by the re-read (verified applied becomes an accurate success; verified rolled back is replayed once). New metrics bd.claim_verify_lost_total and bd.claim_verify_recovered_total. Applies to claim, ready-claim and unclaim in server mode; wisps and embedded mode are unchanged.
  • Script hooks now fire on both write plumbings, and a command waits for its own hooks (bd-opisf). bd has two write plumbings and only one ran the workspace's hook scripts. The storage decorator chain fires on_create/on_update/on_close after every mutation it lands; the unit-of-work plumbing — the one proxied-server mode writes through — fired nothing, so an integration wired to .beads/hooks/ silently missed every mutation that went through it. Four commands had grown hand-wired hook calls to paper over the gap; every other write ran no hook at all. A notifying wrapper now fires them from the plumbing, buffered during the transaction and drained only after the commit succeeds. If you run a team server and wired anything to .beads/hooks/, it has been silently missing most mutations and will start running on upgrade. One change every workspace sees: a command now waits for its own fire-and-forget hooks before exiting (bounded by the 10s per-hook timeout), because a short command could previously return from main before a hook had even started; the wait happens after the store closes, so a hook script's own bd can open the workspace. bd serve still runs no hooks, and bd import still fires none on either plumbing.
  • Team-server data corruption: proxied bd init, bd create --id, and bd update --ephemeral (bd-zl3u8, bd-7oyh5, bd-xt6de). Three independent silent-corruption bugs on the --proxied-server route. (1) bd init --proxied-server against a team server whose database another rig had already identified wrote its own locally-derived issue prefix and project id straight over the stored pair — renaming every id the co-tenant was about to mint. It now reads the identity first and prints Adopted project identity from existing database; there is no flag that restores the overwrite. (2) bd create --id <id> on a proxied server silently upserted every column of a resident bead — title, type, description, labels — and exited zero, with no history entry naming a create and nothing in the output to suggest anything had been replaced; both routes now return an already-exists error naming the id and leave the stored row untouched. (3) Proxied bd update --ephemeral/--no-history ran a plain column update on whichever table the row already sat in, so the bead stayed in issues carrying ephemeral = 1 — still durable, still versioned, still replicated, but invisible to every wisp-plane read and skipped by JSONL export; both routes now perform the atomic aggregate move. Caveat: the --ephemeral fix does not repair existing rows. Any bead a proxied bd update --ephemeral produced before this release is still in issues with the flag set, and nothing here finds it.
  • More team-server parity, from the facade programme. Proxied bd config set status.custom and types.custom now project into their tables so the setting and its effect land together (previously bd config set types.custom "session" reported success and bd create -t session failed for as long as the workspace lived); bd config set-many issue_prefix=<x> is refused as bd config set always was, where it used to walk past the guard and re-prefix the workspace; proxied bd reopen reopens a bead in any configured done status; bd update --parent on a proxied server replaces every parent edge instead of stopping at the first; bd dep remove on a wisp-sourced edge actually removes it; a dependency on a bead in another repository no longer fails with a raw foreign-key Error 1452; bd dep tree on a team server resolves partial ids and honors --max-rows; and proxied bd create/reopen/update --json emit the direct route's shape, gaining the labels field they were silently dropping.
  • bd purge --pattern / bd prune --pattern stop reporting success on a malformed glob, and --dry-run matches the real run (bd-pn231). Both routes matched with the error return of filepath.Match discarded, so bd prune --pattern '[' --force printed No closed beads to prune and exited 0 — indistinguishable from a correct pattern over an empty set, which is the worst possible failure mode for a cleanup script. And --dry-run asked for the refuse-if-any-external-dependent path while the confirmed run asked for the force path, so a dry run could report has dependents not in deletion set for a sweep --force finishes, with the CLI then swallowing the error and printing zeros.
  • bd dep cycles is deterministic and no longer shortens a cycle it cannot fully describe (bd-wfkbv). The walk iterated a Go map, so two runs against an unchanged database disagreed on both cycle order and each cycle's starting point — and on a graph with overlapping cycles even the set of cycles could differ. Members that could not be looked up were silently dropped from the path, so a three-node cycle rendered as a two-node one and a cycle none of whose members resolved vanished from the report and from the Found N dependency cycles count entirely. Diffing two cycle snapshots is now meaningful, which it never was. The same sweep backs the post-add cycle warning that bd dep add, bd dep --blocks and bd link print, on both routes. See the breaking list for the --json shape change.
  • Telemetry no longer taxes every bd invocation. Two startup costs paid on every command are gone. The machine-scoped distinct ID was recomputed on every invocation — a fork of the platform machine-id probe (ioreg on macOS, measured at 20.2±1.2ms) — even with BD_DISABLE_METRICS=1 and even for bd --version; it is now resolved only when metrics are enabled and cached at ~/.beads/machine-id (0600), with a probe failure retried next run rather than cached. Second, every invocation unconditionally spawned a detached bd send-metrics child — a full re-exec of the binary plus an HTTPS upload attempt, with no check that anything was queued; the spawn now requires at least one queued event batch and is throttled to one attempt per 5 minutes. Every bd call gets measurably faster, including calls from users who had already opted out of telemetry. Telemetry content, opt-out semantics and endpoint pinning are unchanged.
  • A long or multi-paragraph close reason renders as body text in bd show (#5595). Every other free-text field reaches the terminal through the markdown renderer, which word wraps and indents; the close reason was formatted into the metadata block beside Owner: and Created:, so it never wrapped and its second and later lines read as separate metadata entries — a blank line and a bare - bullet sitting directly under Created:. bd close --reason-file exists so agents can write structured close reports, and those were exactly the reasons that came out corrupted. Anything larger than one metadata line now gets a CLOSE REASON section rendered by the same call the other body fields make; the JSON payload is untouched.
  • Smaller listing and reporting corrections. bd list and bd children print blocker ids in ascending order with repeats collapsed, on both routes, where they used to come out in map-iteration order so an unchanged workspace could print different bytes on two consecutive runs — which makes diff-based snapshot scripts stable for the first time. bd list --max-rows reports its own malformed value before the filter's. bd status --assigned reports the underlying error instead of a causeless "failed to get assigned statistics". bd count is one implementation again on a shared role, with byte-identical output. Multi-id bd dep list no longer changes its JSON shape on failure, reports ids that name nothing on stderr, answers a repeated anchor once, and no longer prints an empty section header under --type. Claim/unclaim refusals steer toward coordinating with the holder instead of suggesting bd unclaim <id> — the old copy taught an unclaim-then-claim steamroller that evicted a live, heartbeated claim mid-review (bd-at6rc, [#4675]). And a single-bead bd create --id P.N no longer leaves child_counters stale, which could re-mint an already-used suffix once such children were archived (GH#4750).
  • FreeBSD builds compile again (#5661; CI gap tracked as GH#5662). The dbproxy process-identity arc shipped linux/darwin/windows implementations with no fallback, breaking GOOS=freebsd compilation — caught only by the release build's cross-compile, which is what burned the v1.2.0 tag pre-publish. Unsupported platforms now get stubs that fail proxied-mode spawns with a clean, actionable error; classic (non-proxied) bd is unaffected, matching what v1.1.2 shipped on freebsd.

New in [1.3.0]

  • Convergent dependency rekey (#6045; fixes [#5268]). A database can legally hold the same logical edge twice under different typed columns; the rekey derived the same UUIDv5 for both, died on the duplicate primary key, and — because the cursor commits per step — left the store half-applied. The planner now orders convergible states and merges duplicate typed edges, the rename path that minted them is fixed at the source, databases left half-rekeyed by earlier binaries are repaired on the next pass, and the refusals that remain can no longer brick an open.
  • Legacy tracked ignored_schema_migrations is untracked at open (#6046; fixes [#4356]). On legacy lineages the clone-local migration cursor predated its dolt_ignore pattern and sat committed at HEAD, so every migration pass dirtied a tracked table bd could never clean — push worked and every bd dolt pull died with cannot merge with uncommitted changes, permanently (reported independently on two fleets, 24 and 17 databases). An open-time reconcile untracks it and unwedges the pull.
  • bd delete no longer wedges auto-export forever (#6059, completing [#5806]; fixes [#5896]). Auto-export's orphan guard refused to overwrite an issues.jsonl holding a deleted bead's record, refused again on every subsequent command, and the documented recovery re-imported the JSONL — resurrecting the bead the user deleted. Deletions are now proven via store history (a new storage.HistoryPresence capability that the default embedded store implements too) and the export proceeds; unprovable cases still refuse.
  • Ignored-cursor sentinel replay is clamped to floor 11 (#6054; [#5981] class, [#5366] follow-up). A missing clone-local sentinel used to zero the migration cursor and replay the whole ignored chain from 0001 on every upgrade from a released tree — v1.1.0/v1.1.2/v1.2.x all top that cursor at 11 and have no leases table, so the sentinel was always missing. That replay dragged in ignored/0007's unguarded UPDATE wisps SET is_blocked = 0, silently restamping wisps.updated_at across the plane, unrecoverable because the wisp plane has no committed history. Upgrades now apply exactly the pending set, ignored/0012–0026.
  • Server-mode DOLT_ADD/DOLT_COMMIT run after the SQL transaction commits (#6040, carrying @nova-submodules' [#5740]). Staging inside the still-open transaction materialized Dolt commits from the BEGIN-time snapshot, silently reverting rows concurrent sessions had committed in the window — observed in production as claims reverting minutes after being made. The commit now stages the post-merge working set.
  • bd bootstrap probes git remotes for Dolt data instead of rejecting them, and exits non-zero when it declines (#6037; fixes [#5743], [#5663]). Bootstrap rejected the very git-origin-derived remote bd init persists, printed ✓ Database already exists for the refusal, and exited 0 with nothing created — a closed loop with bd init --reinit-local pointing back at it.
  • Truthful errors and safe identity attribution for proxied/gateway bd init (#6062). Nothing on the open path asserted the SQL session was on the database bd asked for, so a credential-scoped front door could serve its own database while bd attributed the reads to the requested one; every USE failure was labeled database not found, including access denials. Both are fixed, along with a silent-data-hazard variant worse than the reported error, and --init-if-missing is honored in proxied-server mode.
  • --storage-class is honored on every create path (#6060). The flag was accepted and ignored on the proxied route (minting a durable versioned row for a request that asked for ephemeral), on --file markdown batches, and on --graph plans; the direct door was fixed in-window (#5149, [#5164]). The ephemeral/versioned conflict is now rejected on the proxied route with the same message as the direct one.
  • Three JSON-contract defects fixed before 1.3.0 froze them (#6053): revision tokens are opaque decimal strings again on the way out and must be sent as strings in expected_version — JSON numbers exceeded JavaScript's 2^53 precision; --set-metadata k=5 stores the typed scalar 5 (restoring v1.2.2's inference); and bd create --json timestamps carry nanosecond precision again.
  • A Homebrew --HEAD version stamp is recognized instead of being misread as 0.0.0 (#6079, carrying @anisoptera's [#5625]; refs [#5603], [#5650]). Version comparison scans dot-separated parts with %d and leaves what it cannot read at 0, so every non-semver stamp bd has written compared as "older than 0.56.0" and handed a live workspace to the pre-v56 recovery — which deletes .dolt when the .bd-dolt-ok marker is absent. Two stamps reach that call: HEAD-<shortsha> from brew install --HEAD, and the v1.1.1-0.2026… Go pseudo-version that bricked five production cities in [#5650]. The recovery is now gated on IsValidSemver before the comparison, so it can only remove predecessors from the recovery set, never add one. Alongside it: bd doctor reports a --HEAD build as healthy instead of nagging brew upgrade beads (which would undo what the user asked for), a changed HEAD stamp counts as an upgrade so the post-upgrade reconciliation runs, and bd upgrade status/review yields no delta rather than dumping the entire release history.
  • bd flatten / bd compact run a full GC after the rewrite (#6057; fixes [#5907]). Dolt's generational GC never revisits the old generation, so the post-rewrite pass freed ~nothing and orphaned history kept resolving, contradicting flatten's own contract. bd gc --full is new, and a low-reclaim pass now hints at it.
  • bd no longer serializes every invocation behind the schema-init advisory lock (#6022). Measured on an 18-seat rig, the lock was held 96.7% of the time and cost 0.4–2.4s of pure waiting on every claim, heartbeat, list, comment and mail check. The steady-state probe now answers without the lock and takes GET_LOCK only on the path that can actually migrate; it issues no writes and fails closed.
  • bd purge and bd prune select candidates by tier (#5995). A wisp minted before the ephemeral column was set belonged to neither sweep — one production database had 858 rows no sweep could ever reach. The tiers are now complementary predicates, so the first purge after upgrade may clear considerably more than usual.
  • Server mode honors Config.LenientOpen (#5783 by @Toady00; fixes [#5781]). The dirty-table refusal's documented recovery — bd dolt commit — could not itself open a server-backed store, a deadlock previously broken for embedded mode only. Found in the wild: 15 of 21 databases on one deployed server carried a permanently dirty config table.
  • The aux-row rekey survives dolt#11131 encoding drift (#5064; fixes [#4380]). A drifted events/comments table panicked Dolt inside the re-key scan, aborting the migration and leaving the database unopenable. Drifted tables are now skipped with a warning, recorded clone-locally in aux_row_rekey_drifted, and re-keyed on a later pass; healing the drift itself is Dolt's schema-encoding-drift recover-rows. This already reached 1.1.x and v1.2.2 users — it is the [1.1.2] fix — and lands on the main line here, so a store upgrading from v1.2.2 is not newly exposed. It is load-bearing for this upgrade specifically: the v53 → v66 chain runs a second aux-row rekey pass through the same drift-protected code path when a v53 store crosses main-series v61, and at this tag the dirty-table exemption is computed per-pass and scoped to exactly the tables the upcoming rewrite will touch, so a post-marker resume cannot sweep a non-drifted table's pre-existing user edits into the migration commit. Diagnosis and fix by @marcodelpin, carried and reworked by @maphew.
  • Incremental auto-export actually takes the incremental path (#5806), and three format/scope regressions the dead code path was hiding — leaked memories, included owner issues, a missing _type discriminator — are pinned by tests. Server-mode only; embedded mode still full-exports every cycle.
  • Disabling telemetry no longer strands the queued metrics backlog forever (GH#5712) — 2M+ files / 15.8GB observed on one control VM; the prune child now runs, network-free, until the backlog decays.
  • Quality-of-life truth-telling: bd prime says when the memory plane could not be read instead of impersonating an empty one (#5877); bd show on a deleted or purged bead points at bd history <id> instead of printing text identical to an ID that never existed (ga-m6inyb); bd vc commit / bd dolt commit sweep the entire working set and report Nothing to commit honestly; every CLI label write normalizes its input so a label can match its own filter — one real database had 150 such rows across 111 beads (#5813, fixes [#5812]); bd show no longer corrupts quoted shell globs via CommonMark emphasis pairing (#5799); and bd blocked no longer silently ignores the label filters it already accepted (#5822).
  • An externally-managed Dolt sql-server was classified as bd's own, silently disarming the shared-store migrate gate (#6118). This is the one to read if you run a shared server. When the port arrived from BEADS_DOLT_SERVER_PORT, from bd init --server-port — the documented way to attach a workspace to an existing server — or from a stale port file left by a bd server that had since died and been replaced on that port, bd treated a genuinely shared server as workspace-owned. The gate added by [#5920]/#6048 to prevent exactly this never fired, so bd promoted the shared schema in place with no prompt, no warning and exit 0, after which every older client of that server was hard-refused with schema version mismatch. Ownership is now proven rather than inferred: bd must have bound that port itself, per its own port file, and the PID it recorded must still be alive. Everything else is shared. The pre-existing explicit-external arms run after the proof, so proof of a bd-managed server can never un-gate a topology [#5920]/#6048 already gated.
  • A dolt.mode: server workspace declared in .beads/config.yaml was refused as a legacy workspace (#6119). The upgrade guard resolved the connection mode from metadata.json and nothing else, while Config.IsDoltServerMode documents six further layers — one of which is dolt.mode in the workspace's own config.yaml. A workspace of that shape resolved to embedded, found a .beads/dolt directory, and was refused before any store was constructed, so every command failed. The guard now resolves the mode through the same precedence chain store selection uses.
  • Host-implied server mode was not applied when config.yaml declared a mode (#6120), so a workspace could resolve to embedded against a host that implies a server.

Security

First shipped on a tested release here — the withheld [1.2.1] work

  • bd dolt push and bd sync no longer adopt a git-origin-derived Dolt remote without consent (#5068). On a rig with no Dolt remote configured, both commands silently derived one from git remote get-url origin, added it, persisted sync.remote into .beads/config.yaml, committed that config change under the user's git identity, and uploaded the full issue history — no prompt, no flag, no opt-out. A public git origin therefore published the whole issue database on a command the user believed targeted an already-configured remote. Adoption is now a consent decision and fails closed: interactively bd shows the derived URL and every side effect that follows a yes, and defaults to no; non-interactively it refuses and exits non-zero, naming the URL it would have adopted. --yes/-y consents ahead of time for scripted use; --no-adopt or BD_NO_REMOTE_ADOPT=1 disables adoption entirely and wins over --yes. Workspace resolution moved below the gate, since nothing may mutate before consent is established. Rigs with a remote already configured are unaffected.
  • The settings plane no longer serves the KV plane by any read (bd-rfwtv, bd-klko9). bd kv keys and the bd remember memories nested under them are user data stored as config rows — they ride in the settings table because there is one table, not because they are settings — and both doors onto the settings plane were listing them with their values. On the HTTP door that meant an unauthenticated GET /v0/beads/config handed every stored memory to anything that could reach the port, and the surface's key-name-based redaction is no defence there because a memory's content is in the value. The exclusion lives in the shared role both doors call, not in the HTTP handler, so the CLI and the API cannot drift on what a setting is. Frame it honestly: this is plane hygiene, not a confidentiality boundary — bd serve still serves every memory in full through /v0/beads/memories by design.
  • The no-ID "last touched issue" fallback is interactive-only (bd-m00pb, [#4839]). A scripted bd update $ID … with an accidentally empty $ID silently mutated whatever bead was touched last — a real agent session corrupted an unrelated closed bead this way. The refusal now happens in argument validation, before any store open, migration, or auto-import side effect.
  • bd serve's bearer authentication (#5516) is listed under Added because it is new surface, but read it as a security control: without it, bd serve is only ever a loopback developer tool, and both flags default to today's behavior so a bare bd serve is byte-for-byte the server it always was.

New in [1.3.0]

  • Go toolchain 1.26.5 → 1.26.7 (#6047), closing seven stdlib advisories reachable from the shipped bd binary: quadratic net/url path resolution, html/template JavaScript-regexp context tracking, unbounded post-handshake crypto/tls messages, a missing ReadHeaderTimeout on net/http's unencrypted HTTP/2 check, unbounded recursion in encoding/xml and encoding/asn1, and the x/net/idna Punycode bug in net/http's vendored copy — bd serve is a real HTTP server and bd makes outbound TLS calls. The go directive stays at 1.26.5, so importers of the module keep their current floor.
  • Dependency bumps clearing all 25 open advisories (#6047), one commit per cluster with a reachability verdict each: golang.org/x/crypto 0.55.0, golang.org/x/mod 0.40.0, klauspost/compress 1.18.7, moby/go-archive 0.3.3, kin-openapi 0.144.0 with oapi-codegen 2.7.1, and a refreshed beads-mcp/uv.lock. None of the fixed code is reachable from the shipped binary — the headline critical is kin-openapi's openapi3filter middleware, which enters the graph only through the tool directive for spec codegen. No behavior change; no dolt bump was required.

Troubleshooting (upgrading from v1.2.2)

Most upgrades need no command at all — on an embedded or local store the first invocation migrates in place. The papercuts below are the shapes worth recognizing before you file a bug:

Symptom Cause Fix
Migration counter jumps backwards to 0012 after reaching 0066 The clone-local series runs after the main one, with its own numbering Let it finish — it is not a loop
A piped or CI upgrade prints nothing Progress goes to stderr only when it is a terminal Not a stall; wait for exit
A shared dolt sql-server did not migrate at all It is never auto-migrated; consent is explicit (#5920, [#6048]) Upgrade every client, then bd migrate schema once — or BD_ALLOW_REMOTE_MIGRATE=1 in scripted use
bd serve refuses to start against a proxied store A daemon has no operator to consent for it, and the gate now covers that path (#6055) Reconcile the schema first, or set BD_ALLOW_REMOTE_MIGRATE=1 as standing consent for the service
bd serve refuses an embedded workspace outright Embedded Dolt commits outside the SQL transaction, so per-request atomicity would be a lie — this refusal is permanent Move to a Dolt sql-server mode: bd migrate from-server-to-proxied-server and siblings (experimental)
Another machine's bd refuses the database after one client upgraded Forward schema-skew guard: a mixed-version fleet is a broken fleet Upgrade every client together; check which -a bd; designated-migrator flow for multi-clone stores
bd dolt push / bd dolt pull refused after installing 1.3.0 The pending-migration gate covers sync, not just bd migrate Finish syncing with the old binary first, or migrate and push from the designated migrator
bd dolt push in CI now fails asking for consent Git-origin remote adoption no longer happens silently (#5068) Add --yes, or configure the Dolt remote explicitly; --no-adopt to forbid it entirely
A long-running bd --readonly serve does not come back up The flag is now refused instead of binding a silently writable server Drop --readonly from the serve invocation
⛔ workspace is frozen for migration, exit 14 A MIGRATION-FREEZE sentinel in the workspace root, the cwd, or an ancestor of either Remove the file the message names (or unset BD_MIGRATION_FREEZE_FILE) once the migration is done
A bd update exits 13 instead of 0 or 1 A --if-assignee/--if-status guard did not match: a racer won, nothing was written Skip gracefully — 13 means stale precondition, not infrastructure failure
bd sync exits 2 and nothing was pushed A merge conflict it will not resolve by picking a side bd conflicts list, then bd conflicts resolve <id> --ours\|--theirs
bd ready suddenly lists beads you deferred months ago Expired dated defers now wake back onto the ready front (bd-i8qx8) Expected, one-time; dateless defers still never auto-wake
A bd search script's hit count changed bd search now includes closed beads by default (bd-t5yex) Add --status open, and raise --limit when hunting live work
bd dep cycles --json breaks a parser The shape is now {members:[{id,issue?}],partial} (bd-wfkbv) Update the parser; issue is absent, never null, for an unresolvable member
A team-server bd delete --force left orphans behind --force no longer implies cascade on the proxied route (bd-x82so) Add --cascade where you meant the subtree
Open refused over "backend": "sqlite" in .beads/metadata.json A stale field from the 1.2.x era; the live data is Dolt Apply the exact one-field edit the rejection message gives
First bd purge after upgrade clears far more than usual Legacy typed wisps are now reachable by the tier predicate Expected, one-time (#5995)
A cross-rig bead gate stays pending under a proxied-server rig Known limitation: multi-rig prefix routing (routes.jsonl) is not supported with proxied-server rigs (#5861) bd gate check, or bd close --force

Validation

  • The release was cut from release/1.3.0, and every change on the branch landed through a reviewed PR against it — 28 of them, driven by a full pre-tag release audit.
  • The full check matrix is green at the branch tip. The release-prep PR (#6038), whose merge commit b3ef65c8 is the tip, was tested on a head that had already merged [#6088]: 122 checks green, 1 skipped, 0 failing. That matrix is the Embedded Dolt Cmd shards (20), Embedded Dolt Storage shards (5), Proxied Dolt Cmd shards (15) and Server Dolt Full Suite shards (16), plus Embedded Dolt Conformance (core and audit), Server Dolt Conformance, the storage-backend conformance oracle, the contract corpus, the Differential Regression against the v0.49.6 baseline, the macOS lane, the Windows lanes (native/msys2/cygwin make shells, dbproxy server, doltversion, cmd/bd liveness, worktree-remove boundary), and nix build .#default.
  • The last red lanes were closed before the prep PR, and only one was a product defect. [#6088] was that one — the [#6048] consent gate had swept in bd-owned auto-started servers and re-checked itself on every schema-init retry, red across the Server Dolt matrix — now fixed and pinned by TestSharedServerDatabase in both directions. The other three were harness bugs: a jsonOutput global leak breaking the shared-migrate-refusal subtests in full-package runs (#6073), a freeze-marker path comparison that was not symlink-safe, which is what the macOS doctor freeze-gate failures were (#6087), and the missing assertion for [#6053]'s string revision token in both show suites (#6086).
  • Upgrade smoke ran the five legs that matter for this release — v1.1.0, v1.1.2, v1.2.1, v1.2.2 and v1.2.2-rc.1 → candidate — alongside 14 historical upgrade lanes from v0.9.1 through v1.2.2.
  • A new wisp-plane upgrade corpus (#6049) seeds and asserts the clone-local plane the 1.3.0 migration chain rewrites — wisps, wisp deps and comments, leases, events, and the ignored-track cursor — across v1.0.1/v1.1.x/v1.2.2 → candidate, closing the coverage hole behind this release's sentinel-replay and restamp fixes.
  • The v53 → v66 upgrade, the shared-server consent flow, the backup-first recipes, and the multi-clone designated-migrator flow are documented in Upgrading, with the cursor-rollback procedure in the recovery runbook.
  • Two release candidates preceded this tag, and rc.2's content is what shipped. v1.3.0-rc.1 (9c6a69ec1) and v1.3.0-rc.2 (c185735c3) each completed the full release pipeline with both registry publish jobs correctly skipped by prerelease gating. rc.2 fixed four defects that only appeared when rc.1 was driven against a real downstream integration: [#6118], [#6119], [#6120] (all described under Fixed) and a CI-only timeout. The v1.3.0 tree is rc.2's tree plus the version stamp — the commit reverts an unreferenced package that no RC had validated, leaving the shipped code byte-identical to what rc.2 was tested as.
  • The v1.3.0 release pipeline completed green. verify-version-consistency, both goreleaser legs (linux + macOS), both package gates (MCP, npm), release attestation, and — for the first time, since prerelease gating no longer applies — publish-pypi and publish-npm. The tag-triggered Migration Test Harness and Cross-Version Smoke Tests both passed. 11 assets published: linux amd64/arm64, darwin amd64/arm64, windows amd64/arm64, freebsd amd64, android arm64, the contract corpus, an SPDX SBOM, and checksums.
  • Independently qualified against a real downstream consumer before the tag. Gas City ran its full acceptance matrix against a bd built from the exact release content, with schema parity asserted in TestMain and no replace directive: the seven-shape init-topology matrix (proxied-local, direct-local, direct-external ×2, proxied-external, legacy externally-managed, deferred-init) at 8/8 subtests each, both migration paths including the live-server refusal, and its dashboard probe and pin suites. All pass, zero skips, zero non-zero exits.
  • A local validation battery against the tag passed the full release stability gate — all 7 upgrade scenarios from each of v1.2.2, v1.1.2 and v1.1.0, including the new wisp-plane leg — plus fresh-workspace, migration-UX (28 steps, counter restart as documented), forward schema-skew guard, migration-freeze, and journal-actor checks.

Known issues

  • A store whose events table is missing stays write-dead, and 1.3.0 does not repair it (#6142). Reads all succeed, so the store looks healthy until the first write, which fails with a raw Error 1146: table not found: events. The cause is that events, bd_events_journal and bd_events_seq are created by the clone-local migration series but are not among its sentinel tables, so a cursor that claims they were created is believed even when they are absent and the creating migration is never replayed. 1.3.0's new cursor-reality floor heals wisps and wisp_dependencies this way but not these three. #6547 adds them to the floor and is held for 1.3.1 rather than landing after the release candidates had already been qualified.
  • Proxied-server mode has no backup path. bd backup and bd restore are refused in proxied-server mode. Use server or embedded mode if you need bd backup, or take backups through Dolt remotes with bd dolt push.
  • bd dolt start is not proxied-aware. On a proxied-server workspace bd dolt status reports the server as not running, and bd dolt start would launch a second, unmanaged dolt sql-server over the same data directory. Use the proxy's own lifecycle rather than bd dolt start on those workspaces.

Performance

1.3.0's proxied-server topology changes the resource profile rather than raw throughput, and the difference is in what a workspace costs when nobody is using it. Measured on one host with bd built from the release content, comparing a per-workspace dolt sql-server against proxied-server mode with its built-in 30-second idle timeout:

Workspaces Direct — idle Proxied-local — idle Proxied, one shared backend — idle
1 130 MB, 1 process 0 MB, 0 processes 123 MB, 1 process
5 616 MB, 5 processes 0 MB, 0 processes 185 MB, 1 process
20 2456 MB, 20 processes 0 MB, 0 processes 603 MB, 1 process

Direct mode's idle footprint scales linearly with workspace count and is held indefinitely; a proxied workspace reaps its backend once idle and returns to zero. At 20 workspaces that is 2.46 GB reclaimed.

The honest counterweight: proxied mode costs more while active — 3445 MB against direct's 2483 MB at 20 workspaces, because a proxy process sits alongside each backend — and a reaped workspace pays a cold start on next use, measured at 0.30–0.63 s across every arm and scale. Idle CPU is not the interesting axis: direct burned roughly 1.6 CPU-seconds per server per hour, real but small, and proxied burned none because the processes no longer exist. If your workspaces are continuously busy, direct is cheaper; if you carry many workspaces that are idle most of the time, proxied is dramatically cheaper.

Caveats worth stating: single host, a dolt CLI one patch behind the pinned version, and a machine under heavy unrelated load — so treat the figures as representative of the shape, not as precise benchmarks.

Installing

Use the command that matches your install method. The package managers below track the latest stable release, which v1.3.0 is; the binaries attached to this release are there if you would rather take one directly:

:::bash
# macOS / Linux / FreeBSD
curl -fsSL https://raw.githubusercontent.com/gastownhall/beads/main/scripts/install.sh | bash

# Homebrew (macOS, Linux)
brew upgrade beads

# npm
npm update -g @beads/bd

# go install — server-mode only
CGO_ENABLED=0 go install github.com/steveyegge/beads/cmd/bd@latest

# go install — embedded-capable
CGO_ENABLED=1 GOFLAGS=-tags=gms_pure_go go install github.com/steveyegge/beads/cmd/bd@latest

:::pwsh
# Windows
irm https://raw.githubusercontent.com/gastownhall/beads/main/install.ps1 | iex

If you still have the old tap formula installed as bd, switch to the Homebrew core formula:

:::bash
brew uninstall bd
brew untap gastownhall/beads 2>/dev/null || true
brew untap steveyegge/beads 2>/dev/null || true
brew install beads

Whatever you use, run which -a bd afterwards: a second binary earlier in PATH is the fleet-skew failure described in the upgrading notes.

Contributors

Thanks to everyone who contributed to v1.3.0 (between v1.2.2 and release/1.3.0 — which, because v1.2.2 re-shipped the v1.1.2 tree, spans the whole 1.2 line's work):

@A3Ackerman, @AJBcoding, @anisoptera, @aphexcx, @arcaven, @aryrabelo, @athosmartins, @banozz0, @bee-ghosttrack, @boardthatpowder, @brendan-appstart, @chrisjunlee, @coffeegoddd, @cosentinode, @csauer02-personal-user, @csells, @cuongbphv, @daniel-jasinski, @davevan2, @DyrtyJax, @ecuthiell, @enieuwy, @eric-richardson1, @Ethee, @GraemeF, @HackAttack, @harry-miller-trimble, @heymatthew, @iamthebot, @idirectships, @idvorkin-ai-tools, @imkp1, @itsandyking, @jacobhausler, @jamelt, @jdelic, @jjgarzella, @johnzook, @joshuaguyervs, @julianknutsen, @kevglynn, @Kevinwochan, @liviux, @lumaks-redox, @maphew, @marcodelpin, @marlon-costa-dc, @maxinflection, @mccraigmccraig, @mlushpenko, @mohamedramadan14, @Mosnar, @MovGP0, @mwotton, @nova-submodules, @ousamabenyounes, @Photobombastic, @postoso, @prmichaelsen, @pvinis, @quad341, @RaviTharuma, @remuscazacu, @rjc123, @Rome-1, @ryanwclark1, @scotthamilton77, @shaunc, @shiminshen, @ShiroKSH, @shon-yuan, @sjarmak, @srobroek, @steveyegge, @swedeinasia-flow, @thewoolleyman, @Toady00, @uschtwill, @vishnujayvel, @Wldc4rd, @zach-source, @Zireael

Special thanks to the external contributors whose fixes were carried onto the release branch:

  • @marcodelpin (Marco Del Pin), whose diagnosis and fix for dolt#11131 encoding drift lets the migration path survive tables it cannot read — carried and reworked as [#5064] by @maphew (matt wilkie)
  • @Toady00 (Brandon Dennis), whose [#5783] makes server mode honor Config.LenientOpen so the dirty-table refusal's own recovery works
  • @nova-submodules (Nova Latent), whose [#5740] diagnosis and fix for server-mode lost updates was carried as [#6040]
  • @anisoptera (Isis Anisoptera), whose [#5625] recognizer for Homebrew --HEAD version stamps was carried as [#6079], keeping a HEAD build out of the .dolt-deleting pre-v56 recovery

Full Changelog: https://github.com/gastownhall/beads/compare/v1.2.2...release/1.3.0

Source: README.md, updated 2026-09-15