Files
gastownhall__beads/docs/getting-started/upgrading.md
Julian Knutsen b3ef65c854 release: prep v1.3.0 — version bump, changelog rollup, release process docs, formula fixes (#6038)
* chore: bump version to 1.3.0

Runs scripts/update-versions.sh 1.3.0, the sanctioned path per RELEASING.md.
Covers everything scripts/check-versions.sh gates: cmd/bd/version.go, the
three plugin manifests plus the marketplace, the MCP pyproject/__init__/
uv.lock pin, npm-package/package.json, the tracked .githooks managed-section
markers, default.nix and the Windows PE resource metadata.

scripts/check-versions.sh passes at 1.3.0.

* docs(changelog): roll up [1.3.0] for 2026-08-28

Creates the [1.3.0] section dated 2026-08-28 and leaves [Unreleased] empty
but present.

- Rolls up everything that was under [Unreleased].
- Adds a 'Breaking changes for v1.2.2 users' section. v1.2.2 shipped the
  v1.1.2 tree, so its users never received [1.2.1]; the five behavioral
  breaks documented there are restated (close policy on bd update -s, search
  including closed, the interactive-only last-touched fallback, bd human list
  status handling, dolt push/sync remote-adoption consent) with a pointer to
  the historical section rather than a copy of it. The two breaks new in
  1.3.0 (BEADS_DOLT_PORT precedence, actor '--' decoding) are indexed here
  and written up in full under Changed.
- Adds upgrade notes: first invocation auto-migrates v53 -> v66 (13
  migrations, 0054..0066, verified against the embedded migrations dir);
  per-step progress on stderr is TTY-gated, so CI sees nothing; back up
  first; the forward skew guard means co-resident older binaries refuse the
  store, so a shared-store fleet upgrades together; rollback via the recovery
  runbook's cursor-rollback procedure.
- Adds entries for eight changes that had no changelog line at all, each
  verified against the code on this branch: --profile -> --cpu-profile with
  no alias (#5126); the one-time content-derived aux-row id rekey, main 0061
  (#5150); events becoming clone-local, 0062 (#5162); the interactions.jsonl
  sidecar becoming opt-in behind audit.enabled (#4688); bd import erroring on
  redirected stdin with no source (#5171); auto-backup defaulting off under a
  sql-server (wy-zrmqr); bd hooks install --chain/--force becoming no-ops
  under marker sections (#5284); the new *.gate.lock workspace/physical-root
  gate files (#5046, #5093); and purge/prune tier selection (#5995).
- Moves the 'sort on listIssues' entry (#5666) out of [1.2.2] and into
  [1.3.0]. Its commit 8aaefe2d4 is not an ancestor of the v1.2.2 tag, so it
  never shipped in that release.

* feat(info): add the 1.3.0 versionChanges entry

RELEASING.md makes cmd/bd/info.go a required release step: versionChanges is
the channel 'bd info --whats-new' uses to tell an agent what changed under it.
The entry matches the existing format and carries the same upgrade notes as
CHANGELOG's [1.3.0] — the v53->v66 migration and its cost, the fleet
implication of the forward skew guard, the rollback pointer, the seven
behavioral breaks a v1.2.2 user meets, and the headline changes.

* docs(releasing): document the release-branch process, fix the semver wording, close the audited checklist gaps

Release-branch process (now in use for 1.3.0): a new 'Release Branches'
section covers cutting release/x.y.z from a SHA green on both Main and
Nightly Full Tests, landing every release-targeted change as a PR based on
the release branch and labeled status/needs-review-auto, tagging from the
branch, and forward-porting to main afterwards. Hotfixes are folded into the
same shape rather than a parallel one.

Semver: MAJOR is now removals and incompatible interface changes; documented
behavioral breaks that ship with migration notes are a MINOR bump, with the
obligation that makes that legitimate (CHANGELOG entry naming the override,
plus an info.go summary). 1.3.0 is cited as the worked example.

Checklist gaps the audit found: cross-compiling every goreleaser target
before tagging (GH#5662 — PR CI builds none of them, and the freebsd break
burned the v1.2.0 tag), the docs cutover (cli-docs.pin bump plus regen, and
the hand-maintained release line on docs/index.md, which had gone stale at
1.1.0), a release-notes rollup check for both an unemptied [Unreleased] and
entries misfiled under a previous tag, an explicit check-versions.sh gate,
and the info.go entry.

Also retires the bump-version.sh instructions: that script has been a
deprecation stub that exits 1 for some time, so the document's primary
version-bump instruction could not work. Replaced with update-versions.sh
plus check-versions.sh, and the file list corrected against what the script
touches now (uv.lock, the copilot plugin, the .githooks markers, winres) —
the old list named cmd/bd/templates/hooks/* and PLUGIN.md, neither of which
it writes. 'The Easy Way' now names the release molecule; release.sh is a
gateway that prints it.

Fixes the drifted table of contents while in the area.

* fix(release-formula): bump via update-versions.sh, verify via check-versions.sh

The formula carried its own hand-rolled copy of the version bump, and it had
drifted from the scripts it was copied from. bump-plugin-json pointed at
claude-plugin/.claude-plugin/plugin.json, a path that has not existed since
the plugins moved under plugins/beads/ — so the step would have failed
outright — and the copy covered none of the files added to the gate since:
the codex and copilot plugin manifests, integrations/beads-mcp/uv.lock, and
the BEADS INTEGRATION markers in .githooks/*. A release driven by this
formula would either die on the stale path or ship inconsistent versions,
which is how the v1.1.1 tag burned (stale uv.lock, discovered only in the
tag-triggered run) and how the v1.2.0 bump reddened main (stale hook
markers).

Replaces the seven hand-rolled bump steps with one that runs
scripts/update-versions.sh, which is the sanctioned path and cannot drift
from itself, and turns verify-versions into scripts/check-versions.sh — the
same gate the release-tag pre-push hook and CI run — instead of a grep list
that has to be maintained in parallel with the real one. Both steps carry the
two failure modes worth knowing about: uv missing (leaves uv.lock stale, fails
only after the tag exists) and the hook markers being rewritten wholesale
rather than matched.

Also renames push-main to reflect the release-branch process now documented
in RELEASING.md, and adds the forward-port reminder there.

Verified: each listed file checked against scripts/update-versions.sh and
scripts/check-versions.sh as they exist on this branch, and against a real
./scripts/update-versions.sh 1.3.0 run. go test ./scripts/ -run Release
passes (the formula still parses, and the cleanup-stale-dolt-orphans,
verify-homebrew-core and local-install assertions are untouched).

* docs: point the release banner at 1.3.0 and tell 1.2.2 users what this upgrade costs

The docs homepage carries a hand-maintained 'These docs are for the X.Y.Z
release' line that nothing generates or validates, so it had gone stale at
1.1.0 while the CLI docs pin had already moved to v1.2.2. It now names 1.3.0
and sends the reader to Upgrading first, since this upgrade crosses a schema
migration. RELEASING.md now carries a checklist item for it.

docs/getting-started/upgrading.md was entirely version-agnostic: strong on
mechanism (install methods, hooks, the remote-migrate gate, backup-first,
the multi-clone flow) and silent on which release you are coming from. A
1.2.2 user could read the whole page without learning that their database is
at v53 and that this step crosses 13 migrations. Adds an 'Upgrading to 1.3.0'
section covering the v53->v66 jump and why it is bigger than a routine one,
the slower first invocation and its TTY-gated per-step progress, the
forward-skew consequence for a shared store (upgrade every client together),
and the cursor-rollback runbook. It links to the backup and multi-clone
procedures already on the page rather than restating them.

Also adds the schema-version-mismatch runbook to the Recovery index table.
It was reachable only from the sidebar and the CHANGELOG, and the new
Upgrading section links to it.

Generated docs untouched: scripts/check-cli-docs-drift.sh passes against the
v1.2.2 pin, and the pin bump is a post-tag step (generate-cli-docs.sh builds
bd from the pinned tag, which does not exist until v1.3.0 is pushed).
go test ./test/docsync and scripts/check-doc-freshness.sh pass.

* fix(release-notes): correct the backup advice, the migration count, and the breaks undercount

Review round on #6038. Four findings, all of which made the upgrade notes
actively misleading rather than merely incomplete.

CRITICAL — bare 'bd backup' takes no backup. backupCmd is a runless cobra
parent (no Run/RunE, children init/sync/restore/remove/status, no default),
so cobra prints usage and exits 0 without writing anything. A user following
'bd backup' before an irreversible 27-migration upgrade sees help text and
proceeds believing a snapshot exists; in a script, 'bd backup && ...' proceeds
on exit 0. Replaced with 'bd backup init <path>' then 'bd backup sync' in
CHANGELOG, info.go and upgrading.md, and said in each place that bare
'bd backup' takes no backup.

Also CRITICAL, same class: the backup had no ordering rule. Under 1.3.0
'bd export' triggers autoMigrateOnVersionBump before it exports, so a backup
taken after installing is a post-migration snapshot that cannot protect
against the migration going wrong. All three copies now say to back up with
the OLD binary before installing, and carry the neighbouring rule that
remote-backed stores must finish 'bd dolt push' first, since the
pending-migration gate refuses push/pull too.

Migration count: '13' was the main series only. schema.go runs the main
source then the ignored source through the same 'Applying migration %04d'
printer, and the ignored series moves 0011 -> 0025 here, so the run is ~27
migrations and the counter visibly restarts from 0066 back to 0012. That
backwards jump is exactly the signature an operator reads as a loop and
kills — while these same notes say 'do not interrupt it'. All three copies
now state ~27, show the restart in sample output with real migration names,
and label it as not a restart.

Breaks undercount: 'seven' read as an enumeration, but [1.2.1] carries three
more entries explicitly labelled BREAKING that a v1.2.2 user also meets.
Retitled as highlights with an explicit pointer to the full [1.2.1] section,
and added the three: '--readonly serve' now refused (load-bearing next to the
'restart any long-running bd serve' advice — it will not come back up), the
config kv-plane drop, and the backend package's orphan-handling removal. The
info.go entry gets the same pointer and the same three, since 'bd info' is the
only copy reachable after installing.

Upgrade-time reader is now pointed at 'bd upgrade review', which computes the
delta since the previously-run version via getVersionsSince, instead of
'bd info --whats-new', whose showWhatsNew loops the entire versionChanges
array with no cap (~80 entries) to deliver the ~11 relevant bullets.

* fix(check-versions): gate the copilot manifest and the winres files, stop overclaiming

Three review findings, all cases where the gate said more than it checked.

Copilot manifest was bumped but never gated. update-versions.sh:101 rewrites
plugins/beads/.copilot-plugin/plugin.json with a sed anchored on the OLD
version, and check-versions.sh never read the file — the string 'copilot' did
not appear in it. That is the same drift class that carried stale .githooks
markers through v1.2.0: once the file drifts the bump silently no-ops on it,
the green line prints, the pre-push hook and CI pass, and the Copilot plugin
ships advertising the previous version. Now gated. Verified it fails: drifting
the file to 1.2.2 reddens the run and exits 1.

winres was verified by nothing after the formula rewrite deleted the steps
that grepped it. Restored as real gates rather than formula prose, since
manifest.xml's <assemblyIdentity> version has no backstop at all —
gen-winres.sh re-derives winres.json's numeric fields from version.go at build
time, but the manifest's version is embedded verbatim, and update-versions.sh
rewrites it with a sed anchored on the old base version. Prerelease-correct:
file_version/product_version and the manifest's four-part version are checked
against the BASE version while FileVersion/ProductVersion are checked against
the full string, which is what update-versions.sh writes. The manifest grep is
anchored at line start because the XML declaration also carries a version=
attribute. Verified it fails on drift.

The success line claimed a 'released-docs policy pass' for a check that no
longer exists — the script reads no docs file — while this same release adds
a MANUAL docs-cutover checklist item precisely because nothing automated
covers docs. An operator reading that line would reasonably skip the manual
checks. Now says version files only, and names the manual steps. The two
vestigial echoes left behind by the deleted check are gone.

The failure remedy was also wrong, and wrong in the direction that loops the
operator: it said to re-run update-versions.sh, which derives CURRENT_VERSION
from cmd/bd/version.go (line 70) and substitutes old->new literally. After the
first bump version.go already holds the new version, so a re-run rewrites
new->new and no-ops on exactly the drifted file the gate just named. Verified
empirically — with the copilot manifest and manifest.xml drifted to 1.2.2,
'update-versions.sh 1.3.0' prints 'Bumping: 1.3.0 → 1.3.0' and leaves both at
1.2.2. Only the githooks markers (wholesale regex) and uv.lock (uv lock) heal
on a re-run, and the message now says so and gives the three remedies that
actually work.

shellcheck clean.

* docs(releasing): make the release-branch policy internally consistent

The policy landed in the previous commit but three procedures in the same
files still contradicted it, and the formula implemented the wrong one.

RELEASING.md said 'Nothing is pushed straight to the release branch' and then,
150 lines down, direct-pushed the release branch and minted the tag before the
merge. Two concrete failures: with branch protection the push is rejected
after the tag already exists locally, and with a squash merge the tag points
at a commit that is not an ancestor of release/x.y.z — goreleaser packages
CHANGELOG.md FROM THE TAG into every published archive, so the release ships
the pre-review notes. Both the CHANGELOG-commit block and the bump block now
open a PR, and tagging is a separate step against the merged commit. The
literal 'git push origin main' under the substitution blockquote is gone, as
is the direct push in the docs-pin block.

The formula implemented the wrong order too: commit-release -> create-tag ->
push-main. Reordered to commit-release -> push-main (land the PR) -> create-tag
(on the merged commit) -> push-tag, and create-tag now states why the order
matters.

preflight-pull was untouched by the last commit and still hard-coded main:
divergence recovery was 'git reset --hard origin/main'. Running the molecule on
a release branch and following that destroys the reviewed release-prep commit
and leaves the operator tagging an untested main SHA — precisely the failure
the policy exists to prevent. It now pins the release branch into a variable
first and resets to that; the backup branch moved to Option 1 rather than
Option 3.

'git push origin release/{{version}}' broke every prerelease run: the branch
convention is release/x.y.z, so an RC rendered release/1.3.0-rc.1 and failed
with 'src refspec does not match any', and the forward-port 'git log
main..release/{{version}}' then died on an unknown revision. Now derives the
base version and says why.

The RC workflow still cut tags from the tip of main, which is what the new
policy calls 'a SHA nobody has fully tested' — and worse, an RC exists to
validate the SHA the stable release ships, while the stable tag now comes off
the release branch, so it validated a different SHA by construction. RCs are
now cut on the base version's release branch, and promotion stays on it.

Accuracy fixes in the same pass: verify-versions no longer claims the (now
corrected) 'released-docs policy' string or tells the operator to re-run the
bump — a no-op after the first run, proven in the previous commit — and no
longer prints a prerelease-broken 'grep -c {{version}} winres.json # 4', which
yields 2 for an RC because the numeric PE fields carry the base version. It
now points at check-versions.sh, which gates winres correctly. The 'three
plugin manifests' and 'compares everything else against it' claims are
corrected to match what the script actually reads, and RELEASING.md names
default.nix and README.md as bumped-but-ungated.

go test ./scripts/ and ./test/docsync/ pass; formula still parses and the DAG
is acyclic.

* ci: run PR workflows on release/** branches

Release-targeted PRs currently run almost no CI. pr.yml, pr-risk.yml,
conformance.yml, regression.yml and cross-version-smoke.yml all declare
'pull_request: branches: [ main ]', and every push trigger in the repo is
'branches: [main]' or 'tags: v*'. So a push to release/1.3.0 fires zero
workflows, and a PR based on it gets only the three workflows with no branch
filter (migration-test, nix-build on its paths, docs-mintlify).

That puts the new release-branch policy on a contradiction: it routes the
version bump and 'any fix the release validation turns up' onto a branch where
they land with no unit, lint, conformance or regression coverage, and the
first full signal is the tag build — where a failure burns the tag by this
document's own rule. The policy was written to stop tagging untested SHAs and
was quietly producing them.

Adds release/** to the pull_request branch filter of those five. Push triggers
are deliberately left alone: release branches take changes through PRs, so the
pull_request event is the one that matters, and widening push would double
every run on merge.

cross-version-smoke is the highest-value of the five here — a release-targeted
PR is exactly where cross-version upgrade breakage is most expensive to find
at tag time.

This PR is the vehicle because it targets release/1.3.0, so the new filters
take effect for its own subsequent pushes.

All five files parse; go test ./scripts/ (which includes the workflow lint
tests) passes.

* docs: reconcile the 1.3.0 upgrade notes with the merged release set

Merging origin/release/1.3.0 changed three facts the upgrade notes assert.

Migration count. The merge added ignored/0026_dep_rekey_dedup_marker (#6045),
so the clone-local series now runs 0012 -> 0026, not 0012 -> 0025. Recounted
from the tree: 13 main-series (0054..0066, unchanged) + 15 clone-local = ~28,
not ~27. Corrected in CHANGELOG, cmd/bd/info.go and upgrading.md, including
the trailing line count in the 'counter restarts' explanation.

Shared servers. #6048 (#5920, #5043) means a shared dolt sql-server is no
longer auto-migrated at all — it waits for explicit consent via
'bd migrate schema'. The notes said flatly that 'the first invocation migrates
your schema in place', which is now true only for embedded/local stores and
would have sent a shared-server operator looking for a migration that never
runs. All three copies now scope that claim and point at the new 'Shared
servers' section upgrading.md gained in the same merge. Verified
'bd migrate schema' exists (cmd/bd/migrate.go:848) and that --global is a root
persistent flag (main.go:818), so the cited invocations are real.

#5896. The auto-export entry said #5896 'stays open for embedded mode, where
bd delete still wedges auto-export'. #6059 closed exactly that. Reworded to
point at the store-history heal rather than claiming a live bug.

check-versions.sh 25/25 green, go build ./... clean, go test ./scripts/... and
./test/docsync/ pass, check-cli-docs-drift and check-doc-freshness pass.

* docs(changelog): fold in the proposed changelog lines from the merged 1.3.0 set

Adds the 'proposed changelog' text carried in the bodies of the PRs that
merged into release/1.3.0 after this rollup was first written. Text is the
authors' own; where a body gave it in conventional-commit form rather than as
a bullet, the sentence is preserved and only the 'fix(scope):' prefix dropped
and a bold lead added to match the section.

Changed: #6048 (two bullets — shared-store consent, and the smart gate's
first-mover/fast-forward arms going embedded-only), #6036 (exported packages
marked EXPERIMENTAL), #6053's revision-token bullet, #6049 (migration-test and
upgrade-smoke wisp-plane coverage).

Fixed: #6040 (server-mode lost updates), #6044 (removed-backend rejection),
#6045 (convergent dependency rekey), #6046 (tracked ignored_schema_migrations
heal), #6054 (ignored-cursor floor clamp), #6059 (delete-wedge heal), #6057
(full GC after flatten/compact), #6060 (--storage-class routes), #6061
(journal actor attribution), #6062 (proxied/gateway init errors), and #6053's
two typed-JSON bullets.

Security: new subsection for #6047 (Go 1.26.7 toolchain bump closing seven
reachable stdlib advisories, plus the dependency bumps clearing all 25).
Placed after Fixed per Keep a Changelog ordering.

Documentation: #6052's multi-rig/proxied gate limitation. Its body calls it a
release-notes line rather than a changelog line, so it may want moving.

Contributor credit carried where the PR body gives it: #6040 keeps its
'Reported and fixed by @nova-submodules' sentence, and #6044 gains a note that
its detection helper is ported from @steveyegge's #4740. NOTE: #5064 (aux
row-key encoding drift) carries credit to @marcodelpin in its body but no
proposed changelog line, so it has no entry here — flagging rather than
inventing one.

#6043 was a REPLACEMENT, not an addition: it asked for the existing
MIGRATION-FREEZE bullet to be swapped for vendor-neutral text. Done — the old
bullet named the gt CLI, 'town root' and 'gt migrate thaw'; the new one
describes the generic marker discovery, exit code 14, and the wider set of
maintenance that stands down.

#6055 and #6056 were still OPEN at the time of writing. Their four proposed
lines are included but fenced in a PROVISIONAL HTML comment block that says to
delete it before tagging if either PR does not land.

check-versions.sh 25/25, go build clean, go test ./scripts/... and
./test/docsync/ pass, check-cli-docs-drift and check-doc-freshness pass.

* docs(changelog): credit the three carried contributor fixes

Adds the entries for the merged PRs that carried contributor work but shipped
no proposed changelog line of their own, in the maintainer's supplied wording,
expanded to the surrounding entries' voice.

ATTRIBUTION CORRECTED on #5064. The supplied wording read 'Reported by
@marcodelpin, fixed by @maphew'; the PR body says the opposite. Its '## Credit'
section: 'The fix is @marcodelpin's, from 767d5dcd on their fork... The
diagnosis behind it — the chunk-level characterisation, the negative result on
the minimised fixture, the dolt 2.1.10 re-test, the recover-rows verification —
is theirs too... isSchemaEncodingDriftErr and its table-driven test come from
that commit.' @maphew (the PR author) absorbed it with a review-requested
semantic rework. Written as 'Diagnosis and fix by @marcodelpin, carried and
reworked by @maphew' so the contributor is not demoted to reporter of a bug
they also fixed.

Scope corrected on the same entry. The supplied wording reads as new behavior,
but git grep against the v1.1.2 tag finds both isSchemaEncodingDriftErr and
aux_row_rekey_drifted already present in internal/storage/schema/
aux_row_id_backfill.go — v1.2.2 is the v1.1.2 tree, so v1.2.2 users already
have this fix, and it is already documented under [1.1.2]. #5064 carries it
onto the main line. The entry says so, so a v1.2.2 reader does not conclude
they were exposed and go looking for a migration that does not concern them.

#5783 and #6037 verified against their PR bodies before writing; both supplied
claims hold. #5783: Config.LenientOpen reached server mode but newServerMode
never read it and the field's doc comment said 'Ignored in server mode', so the
dirty-table refusal's own recovery command could not run — and server mode has
no 'move the directory aside and re-bootstrap' alternative. #6037: bd init
persists a git-origin-derived sync.remote into the tracked config by design,
bootstrap rejected exactly that shape before the origin auto-detect, and
Action=='none' conflated 'already exists' with 'refused', printing a success
tick and exiting 0 having created nothing.

Also notes in the PROVISIONAL fence that the RC is gated on #6055/#6056
merging, so the fence comes off at RC-prep time rather than being an open
question.

check-versions.sh 25/25, go test ./test/docsync/ passes.

* fix(release): create release-prep branch before pushing it

The new release-branch/prep-PR flow pushed `release-prep/v<version>`
without ever creating that branch, so `push-main` (and the RELEASING.md
runbook) failed on first run with
`error: src refspec release-prep/v<version> does not match any`.

Add `git checkout -b` before each prep-branch push — the formula
`push-main` step and both RELEASING.md landing snippets — so the branch
exists before it is pushed, mirroring the existing `REL_BRANCH` guard in
the same step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 22:19:35 -07:00

22 KiB
Raw Permalink Blame History

title, description
title description
Upgrading Upgrade the bd binary, refresh git hooks, run schema migrations, and handle remote-backed and cross-era databases

How to upgrade bd and keep your projects in sync.

Checking for Updates

# Current version
bd version

# What changed since the version you were running — the delta, not the archive
bd upgrade review
bd upgrade review --json  # Machine-readable

# The full release history (large; every version bd knows about)
bd info --whats-new

Short Version

  1. With your current bd, sync remote-backed databases before installing the new binary: bd dolt push bd dolt pull
  2. Back up with that same current binary, before installing: bd export --all -o .beads/backup/pre-migrate-$(date +%Y%m%d).jsonl
  3. Upgrade using the command that matches your install method.
  4. After upgrading: bd upgrade review bd hooks install bd version
  5. If crossing a schema migration on a remote-backed database, only the designated migrator runs: bd migrate bd dolt push

Other clones should install the new binary and run bd bootstrap, not independently migrate. The full procedure is below.

Upgrading

Use the command that matches your install method.

Install method Platforms Command
Quick install script macOS, Linux, FreeBSD curl -fsSL https://raw.githubusercontent.com/gastownhall/beads/main/scripts/install.sh | bash
PowerShell installer Windows irm https://raw.githubusercontent.com/gastownhall/beads/main/install.ps1 | iex
Homebrew macOS, Linux brew upgrade beads
go install (server-mode only) macOS, Linux, FreeBSD, Windows CGO_ENABLED=0 go install github.com/steveyegge/beads/cmd/bd@latest
go install (embedded-capable) macOS, Linux, Windows CGO_ENABLED=1 GOFLAGS=-tags=gms_pure_go go install github.com/steveyegge/beads/cmd/bd@latest
npm macOS, Linux, Windows npm update -g @beads/bd
bun macOS, Linux, Windows bun install -g --trust @beads/bd
From source (Unix shell) macOS, Linux, FreeBSD git pull && make build

Quick install script (macOS/Linux/FreeBSD)

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

PowerShell installer (Windows)

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

Homebrew

brew upgrade beads

{/* Canonical Homebrew tap-migration snippet. The installation page links here; keep the two in sync. */} If you still have the old tap formula installed as bd, switch to the Homebrew core formula:

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

go install

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

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

From Source

cd beads
git pull
make build
sudo mv bd /usr/local/bin/

After Upgrading

Important: After upgrading, update your hooks:

# 1. Check what changed
bd info --whats-new

# 2. Update git hooks to match new version
bd hooks install

# 3. Check for any outdated hooks
bd info  # Shows warnings if hooks are outdated

# 4. If using Dolt backend, restart the server
bd dolt stop && bd dolt start

Why update hooks? Git hooks are versioned with bd. Outdated hooks may miss export refresh, legacy fallback, or safety fixes.

Database Migrations

After major upgrades, check for database migrations:

# Inspect migration plan (AI agents)
bd migrate --inspect --json

# Preview migration changes
bd migrate --dry-run

# Apply migrations
bd migrate

# Migrate and clean up old files
bd migrate --yes

Upgrading to 1.3.0

A 1.2.2 or 1.1.x database sits at schema v53, and 1.3.0 knows v66. That is more than a routine upgrade: 1.2.2 was a recovery release that shipped the 1.1 code under a higher version number, so there is a release line's worth of schema between the two.

Back up first, with the binary you have now

Do this before you install 1.3.0. Under the new binary, bd export triggers the migration before it exports, so a snapshot taken afterwards is a post-migration snapshot and cannot protect you against the migration going wrong. The same ordering rule as the section below applies: do all syncing with your current binary, since once 1.3.0 is installed the pending-migration gate refuses bd dolt push and bd dolt pull too.

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

For 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:

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

What the migration looks like

On an embedded or local store, the first command you run after installing applies the whole set, in place. (A shared dolt sql-server is never auto-migrated — see Shared servers below.) The main series runs 0054 → 0066 (13 migrations), and then the clone-local series runs through the same printer with its own numbering, 0012 → 0026 — so it is about 28 migrations, and the counter visibly restarts partway through:

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 jumping backwards looks exactly like a loop. It is not one — let it finish. Expect the run to take noticeably longer than the commands after it (two passes rewrite rows rather than reshaping tables). It is crash-resumable and picks up where it left off.

Those lines go to stderr, and only when stderr is a terminal, so a piped or CI upgrade prints nothing at all. Silence there is not a stall either.

Upgrade every client that shares a store, together

A bd binary refuses a database migrated past the schema it knows, rather than proceeding blind. One machine upgrading takes the shared store forward and every client still on 1.2.2 stops working. Check for a second binary earlier in your PATH with which -a bd, and restart any long-running bd serve — noting that bd --readonly serve is now refused outright, so a server scripted with that flag will not come back up until you drop it.

If the store is a shared dolt sql-server rather than a local one, follow Shared servers below: 1.3.0 will not migrate it without explicit consent, precisely so the fleet upgrade can come first.

If you need to go back, the rollback is a schema-cursor rollback rather than a downgrade of the data — see Accidental v1.2.1 Release, whose procedure is the same for any cursor rollback even though its worked example is that release.

Once you are on the new binary, bd upgrade review prints exactly the changes between the version you were running and this one. Several commands changed defaults, so read it before your first session.

Remote-backed databases and multiple clones

bd refuses to silently apply pending schema migrations to a database that has a Dolt remote configured. Migrating more than one clone of a shared remote independently forks the schema, after which bd dolt pull can no longer merge — the break is silent and, across a primary-key-reshaping migration, unrecoverable (#4259). The supported flow is: one machine migrates and publishes; every other clone re-clones the migrated database.

This applies to every upgrade that crosses a pending migration on a remote-backed database — the same procedure whether you are moving to a prerelease or to a stable release.

The gate is state-aware by default (#4516): before blocking, bd consults the remote's cached schema state and

  • auto-migrates when the remote is at the same schema version as this clone — no one has migrated yet, so this clone is a safe first-mover (concurrent first-movers converge to identical tables). It reminds you to bd dolt push afterwards. This applies to embedded mode only: a shared server always stops for consent, because migrating it changes the schema every connected client sees (see Shared servers below).
  • stops and directs you to adopt (bd bootstrap) when the remote has already been migrated by another clone.
  • stops for a human decision when this clone and the remote applied different content for the same migration (a genuine fork), or when the remote's schema state cannot be read from the cached ref.

Set BD_SMART_GATE=0 to opt out and make the gate block unconditionally. The recipes below are the explicit path and work the same in either mode.

Important ordering: once the new binary is installed, a database with pending migrations is gated on every open — bd dolt push and bd dolt pull are refused too, not just bd migrate. So do all syncing with your current binary, before you install the new one.

Back up before you migrate. Schema migrations assume the database matches the shape the previous migrations left behind; real databases sometimes drift (interrupted writes, tooling bugs, very old bootstraps). A JSONL export is cheap, issue-complete, and importable by any bd version:

bd export --all -o .beads/backup/pre-migrate-$(date +%Y%m%d).jsonl

bd export captures issues, not Dolt history or config — for a full snapshot also copy the .beads directory (or dolt backup in server mode) while no bd command is running.

Single clone (including a solo user with a remote):

bd dolt push                              # 1. CURRENT binary: publish all local work
bd export --all -o .beads/backup/pre-migrate.jsonl   # 2. backup (see above)
# 3. install the new binary (see Upgrading above)
bd migrate                                # 4. migrate as the designated migrator
bd dolt push                              # 5. publish the migrated schema
bd version                                # 6. confirm the new version is active

If bd's remote-migrate gate blocks the run, it prints the available options — migrating here as the designated migrator, adopting the remote's already-migrated database, or recovering a fork — and asks for an explicit operator decision. Follow the guidance it prints.

For scripted or CI upgrades where nobody reads the output, run bd migrate as an explicit step in exactly one job, never in all of them. If the gate blocks a run it prints both the available options and the scripted override that fits the situation.

Multiple clones sharing one remote:

# 1. With your CURRENT (old) binary, on EVERY clone: publish all work and get in
#    sync, then stop editing until the upgrade is done.
bd dolt push
bd dolt pull

# 2. Designated migrator ONLY: back up, install the new binary, then migrate
#    and publish.
bd export --all -o .beads/backup/pre-migrate.jsonl
bd migrate
bd dolt push

# 3. Every OTHER clone: install the new binary, then ADOPT the migrated database.
#    (bd dolt pull is refused here — the clone still has pending migrations — so
#    re-clone instead. Safe because step 1 already pushed all work.)
bd bootstrap

bd bootstrap replaces the local database, so any work not pushed in step 1 is lost — that is why step 1 publishes everything first. If a clone was instead migrated independently and bd dolt pull later fails with cannot merge because table dependencies has different primary keys in its common ancestor, the schema has already forked — follow the recovery playbook: the pk-fork-refused runbook.

`bd doctor` includes a migration-content-skew check that flags a forked schema against the cached remote ref — a useful post-upgrade verification. It runs in both server and embedded modes.

Shared servers

A server-mode database is served to every bd client connected to that dolt sql-server, so a schema migration is not a local event: it promotes the schema version for all of them at once, and clients still running an older bd refuse the database until they are upgraded too. bd therefore never auto-migrates a shared database on a version bump — with or without a remote configured, though the two cases consent differently (#5920).

Upgrade one server's clients like this:

# 1. Upgrade bd on every client of the server. Reads keep working throughout —
#    an upgraded client reads the old schema, it just cannot write to it.
bd version                     # on each client, confirm the new version

# 2. Once, from a workspace already set up against this server: consent.
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 the window is a degraded one, so keep it short.

If the shared server also has a Dolt remote, step 2 is not enough. Two hazards now apply at once — the co-resident lockout above and the cross-clone fork of Remote-backed databases — so bd requires the stronger designated-migrator consent it describes: bd migrate --force, from exactly one machine, followed by bd dolt push. If another clone has already migrated and pushed, adopt its database with bd bootstrap instead of migrating. On a shared server, adopting also promotes the schema for every client of that server, so step 1 still comes first either way.

Migrating is one-way for the fleet: after step 2, a client still on the older `bd` refuses the database until it is upgraded. Do step 1 first, and confirm it — the gate cannot see the other clients' versions and will take your word for it.

A new client joining a server whose schema is behind is refused before its workspace is written, so there is nothing to run bd migrate schema from there: do step 2 from a client that is already set up, or join and migrate in one step with BD_ALLOW_REMOTE_MIGRATE=1 bd init ….

The same rules apply in proxied-server mode, which is a shared server reached through a local proxy. Two differences worth knowing:

  • read commands print a warning and keep serving the current schema, rather than the read simply not touching it;
  • bd serve refuses to start against a database with pending migrations, because a daemon has no operator to consent for it. Reconcile the schema first with step 2, then start the daemon — or, for an unattended service, put BD_ALLOW_REMOTE_MIGRATE=1 in its environment as an explicit, auditable standing consent.

Cross-era Upgrades

If you're upgrading from a much older version of bd, inspect the storage layout and metadata before running the current binary. A .beads/dolt/ directory alone does not identify a legacy workspace: supported current server mode uses that directory too. Current bd evaluates explicit server metadata, the presence of that local root, and the bounded .local_version witness together. An explicit server selection is not overridden by a stale .beads/embeddeddolt/ repository.

Storage layout Upgrade path
Current embedded metadata with .beads/embeddeddolt/ and no explicit server selection Direct current-era upgrade
Explicit server metadata plus .local_version from v0.55.4 through v0.62.0, whether or not .beads/dolt/ exists Explicit legacy Dolt export/import
Explicit server metadata plus .beads/dolt/ and a witness whose major version is 1 or newer Normal current server-mode upgrade
Explicit server metadata plus .beads/dolt/ and a missing or pre-v1 witness Explicit legacy Dolt export/import
Explicit server metadata plus .beads/dolt/ and a witness that is present but unreadable Normal current server-mode upgrade, with a warning
Explicit server metadata without .beads/dolt/, and a missing, malformed, or non-historical witness Normal current server-mode compatibility path
.beads/dolt/ with missing metadata or persisted dolt_mode blank/embedded Explicit legacy Dolt export/import, except for the configured shared-server compatibility path described below
One .beads/*.db file, such as beads.db or vc.db Sealed SQLite bridge

The witness is whatever bd held in its own version string when it last touched the workspace, so it may be a plain release, a release candidate, a build carrying metadata, or a Go pseudo-version; all of those are read as the version they name. A witness that is present but unreadable is not treated as a legacy marker — no pre-v1 bd could have written one — so bd warns and continues rather than refusing every command. A missing witness stays ambiguous and is still refused.

Current bd refuses recognized historical SQLite and legacy Dolt layouts before opening storage or rewriting metadata. This is intentional: preserve the source and complete the matching explicit migration below.

PostgreSQL and MySQL are removed backends, not supported cross-era upgrade paths. Current bd refuses metadata that selects either backend, and the sealed bridge below accepts SQLite sources only.

.beads/embeddeddolt/: direct upgrade

Upgrade the binary and run:

bd migrate

If the project was initialized before bd init automatically wired git origin as the Dolt remote, verify the remote after upgrading:

bd dolt remote list

When the list is empty, fix it on the machine whose local database is authoritative:

bd export -o .beads/issues.pre-remote.jsonl   # optional issue audit export
bd dolt remote add origin git+ssh://git@github.com/org/repo.git
bd dolt push

Commit the resulting .beads/config.yaml change so other clones can run bd bootstrap or bd dolt pull.

Historical Dolt server mode: explicit migration

Do not run current bd init --force when .beads/dolt/ has missing metadata or persisted dolt_mode is blank/embedded. Those old embedded layouts are never current embedded storage. The same explicit path applies when metadata selects backend: dolt, dolt_mode: server and .local_version records v0.55.4 through v0.62.0.

First take a native snapshot while every writer is stopped. Restore that snapshot to a disposable workspace, export it with the verified historical binary, then import the export into a fresh current project. Keep the original and snapshot unchanged until the cutover has been reviewed. The SQLite sealed-copy helper below does not start or manage a Dolt SQL server.

Explicit server metadata with a v0.55.4–v0.62.0 witness is always refused, including when there is no local .beads/dolt/ root. When that root does exist, the guard admits explicit server mode only with a syntactically valid witness whose major version is 1 or newer; a missing, malformed, or pre-v1 witness fails closed. Without the local root, a missing, malformed, or non-historical witness is admitted only as a compatibility layout.

The configured shared-server compatibility path applies only when persisted metadata is missing or leaves dolt_mode blank/embedded; it does not override an explicit server selection with a local root. Compatibility admission cannot prove that a workspace is modern. If you know it was created by v0.55.4 through v0.62.0, use this explicit bridge even when its witness was lost or damaged. Otherwise, follow the normal bd migrate --dry-run and bd migrate flow for an admitted server workspace.

One .beads/*.db file: sealed SQLite bridge

The old binary stored data in SQLite. The new binary uses Dolt.

Recommended: use the sealed-copy bridge (requires jq):

Stop every process that can write the old workspace before starting. Run this from a source checkout at the exact commit you intend to run; installed binaries do not include repository scripts. Record that commit with git rev-parse HEAD before executing the script. Download the old bd asset only from the official gastownhall/beads release and verify the asset with its published SHA-256:

sha256sum -c checksums.txt --ignore-missing

On macOS or BSD, use shasum -a 256 on the downloaded archive and compare the result with that archive's entry in checksums.txt.

scripts/migrate-legacy-to-current.sh \
  --source /absolute/path/to/old-project \
  --destination /absolute/path/to/old-project-cutover \
  --source-version v0.50.3 \
  --old-bd /absolute/path/to/verified-old-bd \
  --new-bd /absolute/path/to/current-bd \
  --prefix beads

For a source older than v0.49.6, also supply an authenticated v0.49.6 binary:

scripts/migrate-legacy-to-current.sh \
  --source /absolute/path/to/old-project \
  --destination /absolute/path/to/old-project-cutover \
  --source-version v0.17.0 \
  --old-bd /absolute/path/to/verified-old-bd \
  --canonicalizer-bd /absolute/path/to/verified-v0.49.6-bd \
  --new-bd /absolute/path/to/current-bd \
  --prefix beads

Verify the historical binary against its official release checksum before running the bridge. The script verifies each binary's reported version, rejects Dolt and ambiguous layouts, retains a sealed source copy, and compares the candidate export with the canonical historical export. Activate the cutover manually only after reviewing those retained artifacts.

Troubleshooting Upgrades

Hooks out of date

bd hooks install

Database schema changed

bd migrate --dry-run
bd migrate

Recovery after upgrade

If you need to restore from a backup:

bd init
bd backup restore [path] --force

Or pull from a Dolt remote:

bd dolt pull