28 Commits

Author SHA1 Message Date
Vishnu J c5e49a3b78 docs(config): document audit.enabled in configuration reference (#5940)
The optional JSONL interaction sidecar at .beads/interactions.jsonl is
gated behind audit.enabled (default false, internal/config/config.go:245)
with a BD_AUDIT_ENABLED env override (internal/audit/audit.go:67-72), but
the key is absent from docs/reference/configuration.md, so there is no
documented way to discover it outside `bd audit` help text.

Two changes, both on the same page:

- Add an `audit.enabled` row to the Tool-Level Settings table covering the
  default, the env var, what enabling it does, the fact that both
  `bd audit record` and `bd audit label` refuse to write while it is off
  (both go through audit.AppendIfEnabled -- cmd/bd/audit.go:103 and :146),
  and the fact that issue history is recorded in the database regardless.
- Add `audit.*` and `storage-class.*` to the list of namespaces routed to
  config.yaml. Both are in config.IsYamlOnlyKey's prefix list
  (internal/config/yaml_config.go:118) but were missing from the doc; the
  list now matches that prefix list exactly.

Reported by @treystout in #5904.
2026-09-02 18:11:08 -07:00
Bee 67556264c4 feat(events-journal): record the mutating actor on every journal row (#5908)
* feat(events-journal): record the mutating actor on every journal row (bd-mbrxm)

The bd_events_journal row recorded op/ts/issue_id/payloads but never WHO
performed the mutation, so downstream attribution (e.g. same-field,
different-actor conflict detection over the journal) had nothing to key on.

- Schema: `actor VARCHAR(255) NOT NULL DEFAULT ''` on bd_events_journal, on
  both migration planes (0066 + ignored/0025) so the two-door shape converges.
- Record*InTx takes the actor explicitly (no ambient ctx) — every call site
  already sits beside an audit-event emit that has the actor in hand; thread
  that same value (incl. the UOW door's MovePersistence). Comment rows carry
  the comment's author. Blank = unattributed (derived maintenance, deletes —
  follow-up bd-zxt49 — and pre-column rows); never user attribution.
- `bd events tail/export` JSON gains an additive, omitempty `actor` field;
  the wire vocabulary is otherwise untouched (openapi spec, reference doc,
  CHANGELOG; golden regenerated deliberately).
- CLI bundle: direct-DDL override for 0066 in cliCompatibleMigrationSQL —
  dolt 2.2.0 (CI's pin) no-ops the prepared ALTER (dolthub/dolt#11345), so
  without it the parity oracle sees actor only on the runtime side. Measured:
  AllMigrationsSQL() through 2.2.0 lands bd_events_journal WITHOUT actor and
  2.3.1 WITH; with the override both land it. String assertions + a
  requireDoltColumnShape guard pin it without a Dolt binary.

Verified (enumerated): go build ./...; gofmt; go vet; migration hygiene
(BASE_SHA=07c163df0); internal/storage/schema (string test + CLI-apply test
under dolt 2.3.1 AND 2.2.0); internal/storage/domain/...; eventsjournal;
journalops; httpapi (wire-tag bijection incl. actor); storage/dolt
-run 'Journal|EventsJournal|ActorAttribution' against real dolt;
cmd/bd -run Events (golden); storage/uow -p 1 (-timeout 90m): every test green except 3
timeout-shaped reds (ConcurrentInstantiation lock-acquire, EndToEnd 30s
init-schema, PreexistingDirty docker reaper) in a /private/tmp worktree
under box load 40-68; the same 3 at origin/main in that worktree also
red, and at THIS commit in the primary clone all 3 PASS (130s) — box
flake, not the diff.

* docs(events-journal): align seq:4 example with the actorless golden fixture

Maintainer review follow-up on PR #5908 (bd-mbrxm). Both reviewers flagged that
the reference JSONL example's seq:4 is_blocked derived-maintenance row carried
"actor":"worker-1", contradicting the record-contract field table (derived
is_blocked recomputes are actorless) and the golden fixture
cmd/bd/testdata/events_journal_records.jsonl, which emits that same seq:4/bd-100
row without an actor. Drop the actor member so the doc example mirrors the
fixture byte-for-byte and consumers do not misclassify a system recompute as a
user write.

The other, optional review nit (the "stray typographic quote" in the
insertEventRow doc comment) is intentionally left as-is: that glyph is gofmt's
canonical rendering of '' in a Go 1.19+ doc comment (TeX-style quoting turns two
ASCII apostrophes into U+201D), so the reviewers' literal '' suggestion is a
no-op the enforced pre-commit gofmt reverts on every commit. It is cosmetic and
non-gating; the DDL and INSERT are already correct.

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

---------

Co-authored-by: Eddie the Engineer <julianknutsen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-23 13:55:59 -07:00
Julian Knutsen 9a1f086a41 docs: pin the Dolt version instead of steering users to releases/latest (ga-m0dlk) (#5899)
The install instructions told users to run the upstream install.sh from
releases/latest, which today resolves to Dolt 2.3.1. Dolt 2.3.0 regressed
CALL DOLT_RESET('--hard'): a few percent of freshly created databases come
up with that procedure answering "Error 1105 (HY000): context canceled" for
the life of the server process. bd flatten and the Dolt-history compaction
in bd admin compact both finish by hard-resetting main onto a temporary
branch, and the merge-settle path behind bd dolt pull falls back to a hard
reset when it abandons a merge — so the documented happy path installed a
Dolt that breaks those operations on a database that otherwise looks fine.

Point the docs at a pinned version instead, with the measurements and the
raise criterion, and tell anyone already on 2.3.x how to check a database
and what to do about it. The pin and its rationale live in
scripts/ci/install-dolt.sh; the docs name the version once, on the Dolt
page, and everything else links there.

Also note that releases/latest resolves to the most recently created
release rather than the highest version, so it can move backwards — v1.88.2
was created after v2.2.4 and v2.3.0 — which makes "install latest" unsafe
independently of this defect.

test/docsync/doltpin_test.go keeps the documented pin, the copy-paste
install snippet and testutil.DoltDockerImage from drifting apart, which is
how the docs ended up on a broken release in the first place.

This documents the pin that gastownhall/beads#5894 applies to CI; land
after it so the reference to scripts/ci/install-dolt.sh resolves.
2026-08-20 22:41:24 -07:00
Bùi Phan Viết Cường aa8cc065f4 docs(troubleshooting): document the two native-Windows integration traps (#5838)
* docs(troubleshooting): document the two native-Windows integration traps

bd.exe is a native Windows binary, and two consequences of that are
undocumented. Both cost real debugging time because neither fails loudly.

- npm installs bd as a generated bd.cmd shim, so Node's execFile/spawn --
  which run their target directly, without a shell -- never resolve it and
  die with "spawn bd ENOENT" while bd works in the same terminal.
- Git Bash resolves /tmp to its own temp directory; bd, being native,
  resolves it against the current drive root. `bd export -o /tmp/x.jsonl`
  reports success and writes C:\tmp\x.jsonl.

Adds a subsection for each under Platform-Specific Issues, following the
symptom/solution shape already used there, plus cross-links from the FAQ
Windows entry and the installation Windows notes.

Agent-Signature: claude-code-claude-opus-5[1m]-unknown-reasoning on behalf of CuongBPV

* Update troubleshooting for Windows `/tmp` path issues

Clarify behavior of `/tmp` paths in Git Bash and provide solutions for Windows path handling.

* Update FAQ for Windows integration details

Clarify integration details regarding `bd.exe` and `/tmp` paths in Windows.

---------

Co-authored-by: CuongBPV <cuongbpv@fpt.com>
2026-08-20 05:41:39 -07:00
Jacob Hausler d2fc47949d feat(lint): configurable per-type lint sections via lint.sections.<type> (#5855)
* feat(lint): configurable per-type lint sections via lint.sections.<type>

bd lint, bd create --validate, doctor conventions, and markdown import all
check issue descriptions against built-in required sections that are
hardcoded per issue type. This adds a config knob:

  bd config set lint.sections.epic "Standards scorecard, Cost"

- lint.sections.<type> holds a comma-separated list of section headings,
  stored in config.yaml (yaml-only key, same path as validation.*).
- The list is ADDITIVE to types.IssueType.RequiredSections: built-in
  requirements are never relaxed, only extended.
- Entries are normalized to "## <heading>" (any leading '#' run and
  surrounding whitespace stripped) and deduped case-insensitively against
  both the built-ins and each other, so re-declaring a built-in changes
  nothing.
- Consumed in internal/validation.ValidateTemplate (the single choke point
  behind LintIssue and every lint consumer) via mergeLintSections.
- lint.* is a recognized config prefix and documented in bd config and
  bd lint help.

* docs(config): list the lint.* namespace and lint.sections.<type> in YAML-only keys

Address review feedback: the PR routes lint.* through config.yaml
(internal/config/yaml_config.go) but the YAML-only Keys section of
docs/reference/configuration.md did not list it. Add the namespace and a
one-line key description. (The pre-existing audit. and storage-class.
omissions are left as-is, out of scope for this PR.)
2026-08-19 11:23:33 -07:00
matt wilkie 21834e720d feat(doltversion): dolt CLI discovery, hardened version probe, and warn-only version policy (contract part 1) (#5092)
* feat(doltversion): own dolt CLI discovery, hardened probe, and version policy in a leaf package

PR-1 of the dolt CLI install/version contract (design:
mybd reports/2026-07-25-psxg4-dolt-cli-contract-design.md). The dolt
executable managed proxied-server mode spawns was discovered by a bare
exec.LookPath with no override, probed nowhere, and version-checked only
by one advisory archive_level probe; a missing binary erred after
.beads/ creation and an incompatible one died opaquely.

internal/doltversion (stdlib-only) now owns:
- resolution order BEADS_DOLT_BIN env > clone-local sidecar (hook point;
  the sidecar itself lands with identity threading in PR-2) > PATH.
  Explicit values are absolutized before validation (a bare name would
  otherwise validate the cwd file but exec a PATH lookup) and an
  explicitly-named broken binary is an error, never a silent
  fall-through;
- a hardened probe: timeout + process kill, 64KiB output caps,
  unconditional symlink resolution recording given and real paths,
  errno-based exec-format diagnostics naming the likely arch mismatch,
  and file identity captured for later change detection;
- version parsing generalized from the archive_level probe, tolerating
  prerelease/dev suffixes;
- policy: NO hard version floor (adversarial review rejected 2.2.0 as
  policy-without-evidence); RecommendedMin 2.0.0 warns only, backed by
  the 2026-07-25 cross-version matrix run (cross-read of module-written
  storage requires >= 2.0.0). An unparseable version string is likewise
  a warning, not a refusal - the only hard failures are a missing,
  non-executable, or probe-failing binary.

Wired at the two entry points: the managed proxied UOW factory replaces
its bare LookPath and derives archive-level support from the probe it
already ran (no second dolt-version fork on the per-command hot path;
warning is printed once, suppressed under --quiet/--json), and bd init's
proxied preflight fails before any .beads/ write when the binary is
missing or broken (external proxied config is exempt - bd never spawns
that server). doltserver.Start's own bare LookPath is deliberately
untouched here; identity threading through every spawn site is PR-2.

Agent-Signature: claude-fable-5-medium on behalf of matt wilkie
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQHiBWVPSXDCcUSG9EGw9d

* fix(doltversion): fail closed on prerelease archive_level; dedupe dolt advisory

Two review findings on this PR (see reports/2026-07-26-reviewer-dispatch-sweep.md
#5092 for the adversarial-review reasoning):

1. archive_level fail-open regression (major, re gastownhall/beads#4986).
   newManagedProxiedServerUOWProvider derived archiveLevelSupported straight
   from doltID.Version.AtLeast(MinDoltVersionForArchiveLevelConfig), but
   AtLeast deliberately ignores Version.Prerelease (by design, for the
   separate warn-only RecommendedMin check). That meant a prerelease build
   at or above the floor, e.g. "1.52.1-rc1", satisfied the >= 1.52.1 gate
   and had auto_gc_behavior.archive_level written into the YAML — even
   though the old doltVersionAtLeast (gc_config.go) already failed closed
   for any prerelease-suffixed string via strconv.Atoi. If that prerelease
   build predates the field, dolt sql-server's yaml.UnmarshalStrict refuses
   to start: the exact #4986 regression. Extracted the derivation into
   doltArchiveLevelSupported(id) and added a Prerelease == "" gate, plus
   TestDoltArchiveLevelSupported (table test covering above/at/below the
   floor and prerelease-at/-above-floor) and
   TestDoltArchiveLevelSupported_UnparsedVersionFailsClosed.

2. Duplicate dolt-version advisory (major, UX). `bd init --proxied-server`
   probed and printed the version-recommendation warning twice: once in its
   own preflight (init_proxied_server.go) and again ~90 lines later when
   newProxiedServerUOWProvider reaches newManagedProxiedServerUOWProvider,
   which resolved+probed again. The existing doltVersionWarnOnce didn't
   cover init's own Fprintf. Extracted one shared resolveAndProbeDolt(ctx,
   errPrefix) helper (uow_factory.go) that does resolve + probe +
   once-gated warn, called from both sites; init_proxied_server.go no
   longer needs its own copy of the Resolve/ProbeWithPolicy/error-wrapping
   sequence. This also folds the two call sites' warning gates (previously
   !in.quiet vs !quietFlag && !jsonOutput) into one JSON-mode-safe gate.

Verified: CGO_ENABLED=1 go build -tags gms_pure_go ./... clean;
CGO_ENABLED=1 go vet -tags gms_pure_go ./cmd/bd/... clean; gofmt -l on all
changed files empty; targeted tests pass (TestDoltArchiveLevelSupported,
TestDoltArchiveLevelSupported_UnparsedVersionFailsClosed,
TestNewProxiedServerUOWProvider_RoutesExternalConfigToExternalProvider,
TestNewExternalProxiedServerUOWProvider_*, TestBuildProxiedServerClientInfo,
and internal/doltversion's own suite).

Agent-Signature: builder-unknown-model-unknown-reasoning on behalf of maphew

* test(doltversion): resolve symlinks in RealPath expectations (macOS fix)

TestProbeNormalOutput and TestProbeSymlink failed on macOS CI because
t.TempDir lives under /var/folders, itself a symlink to
/private/var/folders. Probe derives RealPath via filepath.EvalSymlinks,
so it correctly returned the /private/var form while the tests compared
against the raw TempDir path.

Map the expected path through the same EvalSymlinks resolution (new
resolveSymlinks test helper). Verified against a symlinked TMPDIR on
Linux, which reproduced the failure before this change.

Agent-Signature: claude-fable-5-high on behalf of maphew

* refactor(cmd/bd): narrow resolveAndProbeDolt to drop the unused identity return

Upstream removed the archive-level gate from proxied-server mode
(68afa3d51), which was the only consumer of the probed identity at these
call sites; returning it now trips unparam on the PR-introduced line.
PR-2's revalidation consumer re-widens the signature when it lands.

Agent-Signature: claude-fable-5-high on behalf of maphew

* fix(doltversion): cross-process probe cache, PATHEXT completion, daily warning dedup

Review fixes (all three MERGE-AFTER-FIXES items from the 2026-08-10 review):

1. The per-command `dolt version` fork on the store-open hot path is gone
   for the steady state: ProbeWithPolicyCached fronts the fork with a JSON
   cache in the user cache dir, keyed by the binary's (real path, size,
   mtime) fingerprint - the exact fields Identity documents for caching.
   A fingerprint hit costs a stat; any change to the binary re-probes.
   Hard probe errors are never cached, and every cache failure direction
   fails open to the uncached behavior.

2. Windows: an explicit BEADS_DOLT_BIN spelled without its extension now
   completes via exec.LookPath's PATHEXT handling (exact spelled path wins
   when both exist), and the behavioral tests run on Windows through .cmd
   stubs instead of skipping wholesale - plus a focused windows-latest CI
   job (test-windows-liveness pattern) so those branches actually execute
   at PR time. skipOnWindows remains only where the subject genuinely
   doesn't exist there (exec-bit check, symlink creation).

3. The old/unverifiable-version advisory now repeats at most once per day
   (warn stamp in the same cache file) instead of on every bd invocation;
   troubleshooting docs state the cadence.

Nits from the same review: Ctrl-C during the probe no longer prints the
misleading install hint (call site branches on context.Canceled); Probe's
stat branch now uses validateExplicitPath's not-found/not-executable
taxonomy split; ParseVersion documents its trailing-integer laxity and the
tightening required before part-2 enforcement.

Agent-Signature: claude-fable-5-high on behalf of maphew

* fix(doltversion): honor init's local --quiet for the advisory; defer sidecar claim in docs

Cross-vendor review follow-ups: bd init defines a local --quiet that
shadows the persistent flag, so resolveAndProbeDolt now takes quiet as a
parameter (init passes in.quiet || quietFlag) instead of reading the
global; and the troubleshooting entry no longer advertises the clone-local
sidecar resolution source, which is a part-2 hook not yet wired
(SidecarValue is always empty in this PR).

Agent-Signature: claude-fable-5-high on behalf of maphew

* test: stop the Windows counting stub double-counting forks

cmd.exe includes the space before a mid-line '>>' in the written text,
so the batch stub's 'set /p =x >> file' appended two bytes ('x ') per
invocation and forkCount — which counts bytes — read every fork as two.
All four Windows failures were exactly doubled expectations; the cache
itself hit and skipped forks correctly. Lead with the redirection so
exactly one byte lands per invocation.

Agent-Signature: claude-fable-5-high on behalf of maphew
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019CwiLhLbAGZdJtKYT76CPp

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 21:18:24 -07:00
ecuthiell 766e6c17f2 fix: reject incompatible timeout in generated hooks (#5522)
Select timeout or gtimeout only after a successful GNU coreutils identity probe, validate BEADS_HOOK_TIMEOUT as positive whole seconds, and preserve the Perl and warned direct fallbacks under inherited shell options.

Keep GNU and Perl deadlines soft and backend-scoped, preserve natural status 137, regenerate every tracked hook artifact, and exercise the real process boundary through the required three-host preflight.

Fixes #5503

Agent-Signature: codex-gpt-5-unknown-reasoning on behalf of Ewen Cuthiell
2026-08-10 20:47:53 -07:00
Julian Knutsen ff75b45c91 docs: the ext-api-store documentation tranche — post-#5516 auth sweep, wave doc coherence, role checklist refresh (#5545)
Three beads (ga-uwix0.1, ga-2yaqp.5, ga-2yaqp.6) plus a fable review round.
Docs and spec prose only: all 19 touched .go files are gofmt-normalized,
comment-stripped byte-identical to main. The one exception is deliberate and
reviewed — scripts/check_doc_freshness_test.go gains a data row, because the
delegated gate ruling required it.

ga-uwix0.1 — #5516 gave bd serve optional bearer auth and made it MANDATORY
beyond loopback; fifteen prose sites still said the opposite. The three the
council named are reworded to the stronger true form (the actor is not the
authenticated principal even where a bearer is required; a shared token admits
a client to the WHOLE surface and grants no narrower right). The sweep for a
fourth found eight more in five spellings the audit's grep could not reach,
including claim.go's "any client can claim as any name", release.go's
no-AUTHORIZATION variant, and six memory/settings-plane sites where the true
statement is sharper: a bearer decides WHO may call, never what a caller who
is let in may read. The review round found four more in the CHANGELOG's
UNRELEASED section, which round one had misclassified as released history —
they would have shipped false in the same release as the auth entry.

The serve runbook's "Binding beyond loopback" section was actionably wrong: it
said the flag "prints a warning and changes nothing else", and its refusals
table quoted a string not in the binary. Rewritten against a build, with every
flag, error string, event name and reason value produced by running the
command. It gains the operational trap nobody had written down: /healthz is
auth-exempt and GET /v0/beads/ready is not, so an unauthenticated readiness
probe earns a 401 forever while liveness stays green. The runbook is now IN
scripts/check-doc-freshness.sh, pinned to the four files it is read against.

ga-2yaqp.5 — nine prose defects, every count re-derived rather than trusted;
three of the audit's own numbers had moved again. 38 route rows and 36
capability-bearing ones; thirteen pinned schemas, not twelve or seven; six
missing entries in the no-query-parameter list, not five; twenty-eight role
accessors, not twenty-seven; nineteen patch members, not thirteen. Two prose
"code counts" were counting paragraphs. CodeAlreadyClaimed claimed the holder
is always attached when only one of four producers always attaches it and
releaseIssue never does. MetadataCAS's leaf contradicted its own shipped wire.
The path/operationId inversion is now recorded where a spec author looks.

ga-2yaqp.6 — the role checklist refreshed against what this wave's additions
actually cost, traced to real commits. Step 10 sent people to a name list
#5460 deleted. The "four more edits" claim was stated twice and undercounted
both times: the two freshest wire slices share a nine-file core plus
roles_test.go. The step-with-no-number gained the fact that explains its
instances — write-role decorators wrap, read-role decorators recurse, so the
call lands on the stub, and both real instances are reads. The wave got its
first CHANGELOG entry: six new public operations, not three, and zero new role
accessors in the window.

Review verdict: FIX-REQUIRED then all items addressed. The reviewer
live-falsified the strong auth form and failed to break it, and confirmed
every recount re-derives exact.
2026-08-10 08:05:07 -07:00
Julian Knutsen d59200bd39 refactor: delete the dead orphan-handling capability (bd-gwryr) (#5497)
BatchCreateOptions.OrphanHandling had four modes and one behavior. Every
production call site passed OrphanAllow (or left the field at its zero
value, which took the same branch), so CheckOrphan returned (false, nil)
on every path it could reach — after paying a SELECT COUNT(*)
parent-existence probe on every hierarchical-ID create to feed a switch
whose only reachable arm ignored the answer.

OrphanStrict and OrphanSkip were reachable from exactly one conformance
case, testAuditCreateOrphanHandling, which existed to execute them.
OrphanResurrect was never a branch at all: it shared the default arm with
OrphanAllow, and the "auto-resurrects missing parents from database
history" behavior its doc comment described left with the JSONL sync
layer. internal/beads' TestImportWithDeletedParent still asserted that
removed behavior; it goes too.

This removes the type, the four constants, the field, CheckOrphan and its
call site, the backend package's alias and re-exports (a breaking change
to a published package, signed off by the owner), the dead
ImportOptions.OrphanHandling, and the import.orphan_handling row in the
yaml-only-key test — replaced with import.unlisted-key, which keeps the
"import.* is exact-match, not a prefix namespace" assertion the row was
actually providing.

The behavior that remains is what OrphanAllow always did: a hierarchical
create whose parent is missing is accepted, and the child's auxiliary
counter row is skipped because it would have no owner
(TestReconcileChildCountersSkipsMissingParent pins that). The wasted
query is gone from the create path.

Co-authored-by: CI Bot <ci@beads.test>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 12:03:18 -07:00
CI Bot 19e4af709a feat(httpapi): stream the events journal over SSE (bd-opisf)
GET /v0/beads/events:watch holds a text/event-stream response open and emits
each committed mutation as it lands, so a consumer learns about a write when it
happens instead of on its next interval. It is a SIBLING of the paged read, not
a mode of it: `follow=true` would have given one operationId two media types,
two lifetimes, two limit rules and two capacity stories, and a client generated
from the document could not have described either.

THE CURSOR IS STILL THE CONTRACT, which is what makes this cheap to publish.
The server keeps no per-consumer state and remembers no subscription; a stream
is the `since` reads a poller would have performed, performed on its behalf
against the same ReadEventsJournalPage through the same read-only cursor. Every
event carries `id: <seq>`, so the checkpoint a client holds while streaming is
the number it would have held while polling, and `Last-Event-ID` — the header a
browser's EventSource resends by itself — takes exactly the value `since` takes.
That header WINS over the parameter, because the client that sends it did not
choose to: an EventSource reconnects to the URL it was built with, so honoring
`since` there would replay every record since the consumer started, on every
reconnect, forever. A nonempty unparseable one is a 400 naming the header rather
than a silent fallback, which would start a stream somewhere the client did not
ask for and never tell it; an EMPTY one means "no id yet" and defers to `since`,
because a client that always sets the header sends it empty on its first
connect and refusing that would break the request the header exists to serve.
Delivery is at-least-once against the client's own checkpoint: exactly once and
in order within a stream, duplicates only across a reconnect. No engine or
storage change — this consumes the poll read path.

EVERY REFUSAL IS DECIDED BEFORE THE FIRST BYTE — validation, the disabled 409,
the flush check, the stream cap, and then the first read, which is what turns a
pruned-past cursor into the paged read's own 410 rather than a 200 that
immediately reports its own failure. Exactly one failure can arrive after the
status is spent: a prune racing an open stream, emitted as a NAMED `truncated`
event carrying the 410's own body in the same encoding on one line (asserted
against Write's output, not merely agreeing with it), behind a raised `retry`
and followed by a close. Naming only the exception is what lets a bare
`onmessage` consumer treat an unrecognized event as "stop, this is not a
record"; one that ignores it does not lose records silently, it stalls loudly on
connect-time 410s, slowly. The other unreportable failure — a stored payload
that will not encode — ends the stream too, and names its seq in the log,
because it will end every stream that reaches that row and nothing else would
say which one it is.

THREE LIMITS HAD TO MOVE, and the middle one is the trap.

  - requestDeadline (60s) does not apply to a streaming row. For every other
    operation it is the backstop against a request holding resources forever;
    here it would do nothing but sever the stream. Each READ carries a 15s
    deadline instead — a liveness budget rather than a database one, since it
    also bounds how long a stream can go without a heartbeat and how long it can
    ignore a shutdown.
  - http.Server.ReadTimeout (30s) had to be cleared per request, BEFORE the
    connect read. There is no WriteTimeout on this server — statusWriter rolls a
    deadline forward before every write, which bounds one stalled write rather
    than a stream, and needs nothing here — but ReadTimeout is a deadline on the
    WHOLE request, and net/http keeps a background read running while a handler
    writes: the read that notices a disconnect. Its expiry cancels the request
    context, so an idle stream would end itself at thirty seconds with no client
    involved, and a database wedged during the connect read would be booked as
    `client_closed`. Clearing it costs no disconnect detection — a client that
    leaves still produces a read error — and a peer that vanishes without a FIN
    is reaped by TCP keepalive, which Go's listener enables by default
    (measured: 15s idle, 15s interval, 9 probes; ~150s).
  - The database slot moves from the request to the read. Sixteen slots and
    forty-eight stream places means one slot per open stream would let a handful
    of idle consumers starve every other operation for hours with /healthz
    green; a stream now takes a slot for the moment of each poll and gives it
    back, and slot pressure delays a pass instead of ending a stream.

Streams are capped at 48, SIXTEEN BELOW the 64-connection limit, and the gap is
load-bearing arithmetic rather than caution: LimitListener returns a connection
slot only when the connection closes, which is after the handler returned and
after this counter came down, so a cap at maxConns could never be observed — the
connect that should earn the 503 would never be accepted, and neither would the
poll it is told to fall back to, nor a mutation, nor a fresh-connection
/healthz. The headroom makes the refusal deliverable; `events_watch_admitted`
carries the live gauge on every connect so accumulation is visible before the
cliff. Each stream jitters its own poll phase by ±10%, because consumers
reconnect together after a restart on one shared `retry` and a fixed interval
would march them into a single read burst per second forever. And a graceful
shutdown signals its streams (RegisterOnShutdown) rather than waiting them out;
the loop consults that signal and the client on EVERY pass, including the
draining ones, so a week-old backlog stays cancellable within one batch.

Tests: handler-level over live delivery (mutations land AFTER the connect),
id=seq, heartbeats, Last-Event-ID precedence including a stale header against a
live `since` and the empty-header polarity, the 400/409/410 connect legs, a
prune under an open stream, the cap and its 503, the cap's headroom against
maxConns, backlog drains cancelled by both a hangup and a shutdown, slot release
on every exit, a stream sharing a single-slot semaphore with a paged read, the
flusher refusal, the read-deadline clear and its ordering against the connect
read, jitter spread, and a clean drain with a stream open — all under -race.
Framing is pinned against a record whose payload carries literal newlines, a CR
and a `data:` line of its own. Two e2e legs through a real `bd serve`: mutate on
one connection while streaming on another under a 10s arrival bound, then
reconnect with Last-Event-ID and require the resume, plus the disabled refusal.
Mutated and each caught: the mid-stream truncated event, Last-Event-ID
precedence, the empty-header polarity, the cap decrement, the cap's headroom,
the drain-cancel fast path, the read-deadline clear and its ordering, the poll
jitter, the shutdown exit, and the deadline exemption.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Agent-Signature: claude-code-claude-opus-5[1m]-unknown-reasoning on behalf of julianknutsen
2026-08-09 07:55:23 +00:00
CI Bot 7992c00702 feat(httpapi): publish the events journal on the v0 surface (bd-opisf)
GET /v0/beads/events pages the durable mutation journal from a caller-held
checkpoint, so a hosted consumer stops shelling out to `bd events tail`. The
journal and its retention contract already landed; this is the consumption
surface for them.

THE RECORDS ARE THE CLI'S RECORDS, and that is a machine fact rather than a
promise. The envelope and its projection moved out of cmd/bd into
internal/eventsjournal, beside the activation and auto-prune logic that already
had to be reachable from two binaries; the spec pins EventRecord to that struct
with x-go-type, pinning.go proves the generator emitted an alias, and the
committed golden fixture now renders through the same function the handler
serves from. A second wire struct in internal/httpapi would have passed its own
tests while quietly shipping a different `ts` shape or a `dep` that appeared
when empty, and the consumer who notices is the one reconciling an HTTP mirror
against a CLI export.

THREE ANSWERS THE DATA CANNOT TELL APART, which is most of the design:

  - CAUGHT UP is a 200 with an empty list. "Nothing new yet" is an ordinary
    answer about a log, and a poller whose steady state was a 404 would have to
    treat this surface's miss vocabulary as its happy path.
  - DISABLED is a 409 `events_journal_disabled`. A journal that is off reads as
    zero rows and a head of zero — byte-identical to an enabled journal nothing
    has written yet — so without this gate a consumer would poll a workspace
    that will never emit a record and call itself caught up forever. Activation
    is workspace state, so it is resolved by cmd/bd (eventsjournal.EnabledFor)
    and handed in; this package resolves no workspace state of its own. A 409
    for CodeNotClosable's reason: a statement about state the client cannot see,
    which the same request stops earning once an operator changes it.
  - PRUNED PAST is a 410 `events_journal_truncated` carrying `since`, `floor`
    and `head`. It is the surface's only 410 and it is the right one: those
    records existed, were addressable by exactly this request, and were
    deliberately deleted — retrying the same `since` can never succeed, which is
    what a 404 would wrongly invite. The mapping lives in ClassifyError, keyed
    on the typed error with errors.As, so neither database source can forget it;
    the code is storage's own constant, so the CLI's `--json` failure and this
    body cannot drift to two spellings of one condition.

`head` is why the read seam grew. ReadEventsJournal answers what came next and
cannot answer "keep asking, or wait?", so ReadEventsPageInTx returns the rows
and the counter head from ONE transaction, read in that order — a head taken
first could land below the last row served, which a consumer reads as "past the
end" and stalls on. `bd events tail --follow` keeps the cheaper path: it runs
the truncation decision every second and pays for a head read only in the
ambiguous cases.

The handler holds storage.EventsJournalCursor, a read-only interface, not the
wider accessor the CLI takes. There is no prune, no acknowledge and no
truncate over HTTP: retention stays a workspace decision, and a surface
documented to publish the journal should not be one line from a delete.

EventsJournal is the first CONDITIONAL role on httpapi.Config — required
exactly when the workspace's journal is enabled. A storage backend with no
journal seam is an ordinary backend as long as nobody asked it to record
anything (eventsjournal.Apply takes the same deal at open time), and demanding
it unconditionally refused to start two registered-backend servers that had no
use for it.

`limit` runs 1..10000 defaulting to 1000, and `0` is REFUSED rather than
meaning unlimited as it does on the issue listings: a caller resuming from an
old checkpoint would otherwise ask one process to buffer the whole retained
window. `since` is required — defaulting a missing checkpoint to zero serves
the entire window to a consumer that meant to resume, which arrives as a flood
of duplicates rather than as an error.

Tests: handler-level over both refusals, the caught-up/empty/disabled trio, the
limit bounds and an int64 checkpoint; a storage-level head guard against the
real embedded store; and three end-to-end legs through a real `bd serve` —
mutate over HTTP then read the journal back gapless, prune and meet the 410 then
recover from floor-1, and a disabled workspace refusing. The truncation mapping,
the disabled gate, the window members and the limit ceiling were each mutated
and each failed a test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Agent-Signature: claude-code-claude-opus-5[1m]-unknown-reasoning on behalf of julianknutsen
2026-08-09 06:01:23 +00:00
CI Bot d05d946262 feat(events): bounded-by-default auto-prune to the retention floors (bd-opisf)
Enabling the journal now yields a BOUNDED feed. Until this commit the only
thing that deleted a record was an operator typing `bd events prune`, so the
person who turned the feature on was not the person who discovered, months
later, that a table nobody reads had become the largest thing in the database.
Retention floors already described the window a consumer is guaranteed; this
applies them without being asked.

The shape is SQLite's WAL auto-checkpoint rather than a background compactor,
with Dolt's auto-GC as the second reference: the WRITER pays, immediately after
its own commit, only when a cheap persisted threshold says work is due, and
never for more than a fixed slice of it. Neither needs a daemon, and neither can
turn a user's command into an unbounded wait. Only the trigger and amortization
pattern transfer — the journal needs no finalization data movement, so our
checkpoint analog is pure prefix deletion.

POLICY IS THE FLOORS, and it is not a second implementation.
ComputeEventsAutoPruneBoundInTx hands the EXISTING resolver an unbounded request
(before = head+1) and keeps whatever the floors leave, through the same two
scalar reads `bd events prune` uses — now factored into one place so a floor
cannot mean one thing when an operator asks for it and another when maintenance
applies it. Both floors disabled means the operator chose an unbounded ledger,
and auto-prune becomes a no-op that does not even read the throttle.

TRIGGER is the root PersistentPostRunE maintenance net, last after Dolt
auto-commit, auto-backup, auto-export and auto-push, before the store closes.
That is the narrowest existing seam with the four properties this needs: the
command succeeded (cobra never reaches PostRunE on an error return), its
transaction is closed, a plumbing is still open, and the read classification is
already made there. It fires at most once per process. Four gates keep it
writer-pays, each one load-bearing rather than decorative: the post-command
maintenance policy (strict --readonly, `bd serve`); the read-only
classification, without which `bd list` prunes the journal, since a read-only
classification still opens a writable store; preview commands (--dry-run,
--inspect), which promised not to mutate and are held to it by a
write-refusing open; and the whole `bd events` family, because reading a feed
must never be what prunes it and `bd events prune` has just resolved the bound
by hand. `bd events tail` joins the read-only classification while it is at it
— it is a pure read that had been opening a writable store. Both write
plumbings are covered: the store path and the proxied-server unit-of-work
provider, which commits maintenance in the ephemeral form (no Dolt commit) that
dolt_ignored tables require.

`bd serve` has no command boundary, so it drives the same routine from a ticker
with a clean, self-contained shutdown. It reads the same persisted watermark
every CLI process reads, so a server and the CLIs beside it share one schedule
rather than three.

EXECUTION is its own transaction per batch: `DELETE ... WHERE seq < ? ORDER BY
seq ASC LIMIT 10000`, at most 3 batches per pass, all of it under a 30-second
pass budget and on NO-RETRY transactions. Maintenance is humble by
construction: the retry helper a user's write deserves would spend up to
fifteen seconds of backoff per transaction losing races on work nobody asked
for, and a maintenance transaction that loses a race has nothing to recover —
the prefix delete is idempotent and the next trigger resumes. ORDER BY is not
decoration —
without it a limited delete may remove an arbitrary subset, and a hole above the
floor is silent loss the left-edge truncation check cannot report. Verified
against the real engine that Dolt accepts the ordered, limited single-table
delete. A backlog larger than one pass drains over the next few commands instead
of stalling this one. Every failure is logged and swallowed: maintenance must
never fail a user's command.

THROTTLE is a watermark (ts + head) in local_metadata, the clone-local ignored
plane, read together with the seq counter in ONE query — the not-due answer, the
overwhelmingly common one, costs a single indexed lookup. Due means an hour
elapsed or 5k records advanced; both are constants, because they tune how often
maintenance amortizes rather than promising anything. A stamp far in the FUTURE
(a clock stepped forward and later corrected) also reads as due, or the
interval trigger would never fire again on a workspace that looks healthy. The
watermark is stamped BEFORE the deletes, in a transaction of its OWN, so a
persistently failing prune degrades to no maintenance rather than to a retry on
every command — sharing that transaction with the bound read, as the first cut
did, rolls the stamp back with the failure and reinstates exactly the
per-command penalty the ordering exists to prevent. Racing processes are
idempotent: a prefix delete that already happened is indistinguishable from one
that just did. A workspace with the journal off pays nothing at all.

CONFIG: `events-journal-auto-prune`, default true, BD_EVENTS_JOURNAL_AUTO_PRUNE,
a config.yaml startup key like the other three (YamlOnlyKeys — without that entry
`bd config set` reports success and changes nothing; all four are also added to
the recognized-key set, so the documented opt-out no longer prints an
unrecognized-key warning beside a command that worked). The resolver now reads
the YAML 1.1 boolean spellings, which is a data-deletion fix rather than a
nicety: an unreadable value falls through to the key's DEFAULT, so
`events-journal-auto-prune: no` had read as "keep pruning" — an opt-out that
went on deleting the records it was written to preserve. Its two resolution
arms (no workspace yet, and a resolved workspace) now agree on an unreadable
value instead of splitting false/true. Manual `bd events prune` remains for an
earlier, on-demand cut below the floors; physical disk reclaim still rides
`dolt gc`.

SCOPE: a pass maintains the workspace whose command triggered it. A routed
write (`bd create --repo ../other`) journals into the target, but the trigger
runs against the launcher's own workspace and plumbing — one command must not
open maintenance transactions on every database it wrote to in passing — so a
workspace only ever written remotely relies on commands run in it, or on its
own `bd serve`. Stated in the docs rather than papered over.

Consumer safety is unchanged. ComputeEventsPruneWhere, the truncation contract
and the seq counter are untouched; deletion is still prefix-only and still
bounded by the floors, and a laggard still gets the typed truncation error.

Docs corrected, because four places said the opposite:
  - docs/reference/events-journal.md: "manual to prune" in the opening, and
    "Pruning is manual. Nothing deletes journal records on your behalf." at the
    head of the retention section, which is rewritten around the new default;
    the config table gains the key and stops attributing the floors to the CLI
    command alone.
  - cmd/bd/events.go: `bd events prune` said "Pruning is manual: nothing deletes
    journal rows on your behalf"; it now says retention is enforced to the
    floors automatically and this command is for an EARLIER, on-demand cut
    BELOW the floors — it cannot cut deeper than they allow, so shrinking the
    retained window means lowering them. `bd events` gains the same summary.
  - CHANGELOG.md: "Pruning is manual — nothing deletes journal records on your
    behalf" replaced by the bounded-by-default story, including the volume
    trigger and the per-workspace scope.
  - internal/config/config.go and internal/storage/issueops/journal_prune.go:
    floors are no longer described as belonging to `bd events prune`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Agent-Signature: claude-code-claude-opus-5[1m]-unknown-reasoning on behalf of julianknutsen
2026-08-09 01:58:17 +00:00
CI Bot 98a849fdeb docs(events): document the events journal and bd events (bd-opisf)
Adds docs/reference/events-journal.md: what the journal is (a durable,
replayable, clone-local record of committed mutations), how to enable it,
the record contract and its seven ops, the typed truncation error and how a
consumer recovers, manual pruning and the retention floors, the boundaries
(per replica, per branch, sync/raw SQL/store-open/compaction non-coverage),
and how the dolt_ignored tables sit in Dolt.

Every technical claim is derived from the code on this branch — cmd/bd
events help text, the issueops journal engine, the migrations, and the
config defaults — and states the same contract the CLI help does.

It lands in Reference rather than Core Concepts because the page specifies a
machine-facing contract rather than teaching the bead/dependency model; it
sits beside the JSON output contract and observability, the two other pages
an integrator reads. A short orientation section separates the three
event-ish systems (script hooks, audit history, the journal) so a reader
picks the right one, and reference/advanced.md no longer claims there is no
`bd events` command.

CHANGELOG records the new commands, the opt-in journal and its three config
keys, the events_journal_truncated failure mode, and is_blocked joining the
documented /v0 issue schemas.

Both the page and migration 0064's down script also record the one destructive
outcome that raises no error: dropping and recreating the journal tables, or
losing bd_events_seq, restarts the sequence space at 1, so a parked consumer's
checkpoint sits above the new head and reads as "caught up" forever. Nothing
about the new journal looks pruned, so the typed truncation error cannot report
it — the only remedy is to re-baseline, and the only defence is saying so where
someone will be standing when it happens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Agent-Signature: claude-code-claude-opus-5[1m]-unknown-reasoning on behalf of julianknutsen
2026-08-09 01:58:17 +00:00
Steve Yegge f215b312cf docs: re-review and bump the three 90-day-stale freshness markers (#5394)
* docs: re-review and bump the three 90-day-stale freshness markers

azure-devops.md, json-schema.md, and otel-data-model.md all carried
Last reviewed: 2026-05-08 — exactly 91 days old today, so the doc-flags
freshness gate went red on every branch at once. Each doc was genuinely
re-reviewed against its Freshness source paths on main; drift found and
fixed:

- azure-devops.md: proxied-server mode note (ado sync/status/projects
  hard-error there); --parent and --project flags documented; duplicate
  --issues row dropped; real ado.org error text quoted; ado.pat's
  yaml-only secret storage noted. Source bug found during review filed
  separately: the cmd-level config reader cannot see a yaml-only PAT,
  so the documented Quick Start fails on main (bd-wvf77).

- json-schema.md: bd show/close/update --json return top-level ARRAYS
  (contract-test-pinned); jq recipes fixed accordingly; envelope
  pagination key documented; show's always-present revision field and
  import's unchanged counter added; error envelope (optional code/hint,
  envelope-mode shape, stdout paths, exit 1) corrected; deprecation
  notice's tty-only condition noted.

- otel-data-model.md: hard opt-in via BD_OTEL_ENABLED + standard OTel
  SDK env vars (BD_OTEL_* legacy); console-only trace export; dolt span
  attrs (shared doltSpanAttrs, dolt.remote, dolt.force_push,
  dolt.conflicts, dolt.merge_with_strategy); facade-wave storage role
  spans; seven new Dolt metrics; AI attrs (bd.ai.duration_ms, 4-attempt
  retry); appendix re-pinned to current main.

scripts/check-doc-freshness.sh passes locally.

* docs(otel): complete claim_verify_lost op label enumeration; qualify retry policy as compact-only

Review findings on #5394: op also takes guarded-update
(claim_verify.go:113) and ready-claim (issues.go:439); the retry-policy
sentence describes compact's loop, find_duplicates makes one unretried
call.
2026-08-07 02:35:11 -07:00
Steve Yegge 80af94c0a7 fix(export): promoted no-history wisps survive import round-trip (bd-r9uce) (#5368)
* fix(export): promoted no-history wisps survive import round-trip (bd-r9uce)

A promoted no-history wisp is a durable issues-table row that still
carried no_history=true (PromoteFromEphemeralInTx cleared only
Ephemeral), and both import routes inferred the storage plane from row
flags (issueops.IsWisp: Ephemeral || NoHistory). Importing an export
stream containing such a row silently re-planed it into the wisps table,
dropping its cross-plane relations as cross-bucket dependencies on the
way — classic-mode data loss across export->import->export.

Belt and braces, per triage + the #5351 review endorsement:

1. Promote clears NoHistory too (and normalizes an explicit ephemeral
   StorageClass to empty, the NormalizePersistenceMode promotion rule):
   a promoted wisp is fully durable and the flag has no post-promotion
   meaning.

2. Import routes the plane by an explicit marker, never by no_history.
   Export stamps "wisp": true on rows that live in the WISPS table and
   whose flags alone cannot prove it (the no_history shape; ephemeral
   rows self-describe), classified by table membership via a new
   PartitionWispIDs store capability (classic: batched wisps-membership
   query behind an Unwrap walk through the telemetry/hook decorators;
   proxied: the plane-pinned GetWispsByIDs read). Both parse loops
   (bd import in both storage modes, bootstrap/init/auto-import) resolve
   the plane through one shared helper, applyImportWispPlane, which pins
   marker-less no_history records to the durable plane via a new
   non-serialized Issue.WispPlaneOverride consulted by IsWisp. The
   no_history flag itself is preserved on the row, keeping the
   export->import->export byte-identity for wild data; the legacy
   v0.35-v0.37 "wisp"-as-ephemeral alias behavior is unchanged.

Tests: the promoted shape now lives IN the proxied round-trip fixture
(cross_mode_byte_identical covers it, with relations); a classic-mode
round-trip test (fails on base: cross-bucket dependency drop + missing
plane marker); a proxied import plane-routing subtest; embeddeddolt pins
for promote clearing NoHistory and PartitionWispIDs membership
classification.

* docs(schema): document the export stream's explicit wisp plane marker (bd-r9uce)

* fix(export): fresh wisp_plane marker key — legacy-alias reuse was lossy under rollout skew (bd-r9uce)

Adopts lion's #5368 review recommendation: pre-fix v0.38+ binaries' alias
branch (hasWisp && !Ephemeral => Ephemeral=true) would import a marked
genuine no-history wisp as ephemeral — purge-eligible and excluded from
that rig's next export — making the common skew case lossy. A fresh key
degrades to flag routing on older readers, data-safe in both directions.
Legacy 'wisp' read alias preserved verbatim.

* fix(storage): clone WispPlaneOverride in cloneIssueForHook (bd-r9uce)

The hook snapshot guard (TestCloneIssueForHookCoversReferenceFields)
correctly flagged the new *bool field as uncloned: a hook mutating the
snapshot could alias the live issue's plane pin. Deep-copy it and add
it to the guard's covered-fields manifest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FZXjihTayFcngYzo6xPd3p

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 12:48:44 -07:00
Graeme Foster 1cf7490c71 refactor(telemetry): adopt standard OTel SDK env vars with BD_OTEL_* back-compat (#3859)
* refactor(telemetry): adopt standard OpenTelemetry SDK env vars with explicit BD_OTEL_ENABLED gate

Telemetry configuration moves from BD_OTEL_* data variables to the
standard OTEL_* environment variables. Activation now requires
BD_OTEL_ENABLED=true so a machine-global OTEL_* setting (for some other
instrumented tool) cannot silently turn bd telemetry on.

The resource picks up resource.WithFromEnv() so OTEL_SERVICE_NAME and
OTEL_RESOURCE_ATTRIBUTES take effect — users can override service.name
or stamp deployment.environment without forking.

Backwards compatibility: BD_OTEL_METRICS_URL, BD_OTEL_LOGS_URL, and
BD_OTEL_STDOUT continue to work and activate telemetry on their own.
Each translates to its OTEL_* equivalent unconditionally — a legacy
value wins over a pre-existing OTEL_* value so cross-tool environments
don't redirect bd traffic. Every invocation that sees a BD_OTEL_*
variable logs a one-line deprecation warning to stderr.

OTLP endpoint discovery is delegated to the SDK; the explicit URL
helper in internal/telemetry/otlp.go is removed.

OTEL_SDK_DISABLED=true forces telemetry off even when an opt-in is in
effect, matching SDK conventions.

Tests cover: legacy var translation + override semantics, master-switch
activation, OTEL_*-alone-doesn't-activate, OTEL_SDK_DISABLED override,
WithFromEnv resource attribute merge, deprecation warning emission, and
otlpMetricsEndpointSet detection (specific endpoint, generic endpoint,
negative).

Part of the gastownhall/beads PR #3475 split: isolates the OTEL_* env
var standardisation from the WrapStorage wiring (#3858) and the
bd.prefix attribute work (separate follow-up).

* refactor(cmd/bd): extract command-telemetry wiring into testable helpers

cmd/bd/main.go's PreRunE block contained two pieces of OTel wiring —
telemetry.Init and the bd.command.<name> root span construction — that
silently decay if a future refactor drops them. Same regression class
as the WrapStorage bug (telemetry implemented but never called) —
wiring buried in cobra PreRunE escapes review.

Extract two helpers in cmd/bd/command_telemetry.go:

  - commandSpanAttrs(cmdName, version, args): pure function returning
    the initial attribute set for the bd.command.<name> span. The
    bd.actor attribute is stamped later via SetAttributes once
    getActorWithGit() resolves.
  - startCommandTelemetry(...): composes Init + Tracer.Start. Single
    callable line in main.go.

Three tests cover the commandSpanAttrs output (full attrs and the
empty-args edge case) and the startCommandTelemetry wiring contract:
it must always return a non-nil context and span, since the
bd.command.<name> span is the parent for every storage and AI span
downstream — silently returning nil would break trace nesting across
the whole invocation. Whether the span actually records is governed
by telemetry.Init which is covered in internal/telemetry/telemetry_test.go.

main.go shrinks by 10 lines.

* fixup(cmd/bd): stage command_telemetry.go split omitted from merge commit

The previous merge commit (def9f97a4) resolved main.go's PersistentPreRunE
conflict to call initTelemetry()/startCommandSpan() but the working-tree edit
to cmd/bd/command_telemetry.go that defines those two helpers was never
staged, so the merge commit as recorded does not compile:

  cmd/bd/main.go:680:3: undefined: initTelemetry
  cmd/bd/main.go:706:26: undefined: startCommandSpan

This stages that omitted edit: command_telemetry.go now defines
initTelemetry (the Init half) and startCommandSpan (the span-start half,
using commandSpanAttrs) as separate functions, with startCommandTelemetry
kept as their combined form for callers/tests that don't need to interleave
other setup between the two steps. See the merge commit message for why the
split is necessary (main's usage-metrics bootstrap block sits between the
two steps for one internal subcommand).

Verified: CGO_ENABLED=0 go build -tags gms_pure_go ./... now succeeds.

Agent-Signature: claude-fable-5-high on behalf of matt wilkie
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(telemetry): gate the OTLP metric exporter on OTEL_METRICS_EXPORTER

A console-only or none selection — including the legacy BD_OTEL_STDOUT=true
translation — previously still installed an OTLP reader whenever a
machine-global OTEL_EXPORTER_OTLP_ENDPOINT existed, silently exporting bd
metrics to an endpoint configured for some other tool. The selector is now
parsed per the SDK spec (comma-separated; unset defaults to otlp; none
disables) and the OTLP reader requires the selection to include otlp.

The BD_OTEL_STDOUT translation keeps both destinations when
BD_OTEL_METRICS_URL is also set (console,otlp), preserving the existing
both-exporters behavior for that combination.

Also bound the test-cleanup Shutdown with a 2s context so suites configured
with unreachable endpoints (example.invalid) don't spend ~30s in OTLP
retries.

Both issues found by a cross-vendor (Codex) review of the rebased branch.

Agent-Signature: claude-fable-5-high on behalf of matt wilkie

---------

Co-authored-by: matt wilkie <maphew@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 00:05:06 -07:00
Kevin Glynn f4fcc318d2 docs: remove stale 8-char prefix limit claim from rename-prefix help (GH#770) (#4813)
Commit ab3feda00 removed the len(prefix) > 8 check from validatePrefix,
but "Max length: 8 characters" survived in the command's Long help text,
the plugin skill page, and docs/reference/advanced.md. Remove from all
three sources; generated CLI docs are release-pinned and untouched.
2026-07-26 12:29:39 -07:00
Athos Bernardes 3102cbd4d4 fix(show): flag comment_count as omitted when comments are not streamed (#4865)
* fix(show): flag comment_count as omitted when comments are not streamed

bd show --json's count-only default (be-ijck6q, pinned by
TestJSONContract_ShowOmitsOptInPayloadsByDefault) returns comment_count
with no comments key unless --include-comments is passed. A caller that
checks only `.comments` reads absence as "no comments" — wrong whenever
comment_count > 0. A caller that reads comment_count has no signal that
the array was left out on purpose rather than omitted by a client bug.

Add comments_omitted (bool, omitempty) to IssueDetails, set true only
when CommentCount > 0 and Comments was not populated. Leaves the
count-only default and its perf rationale untouched — this does not
revert cfcc95799/e538f2881, it makes the partial representation
self-announcing. Symmetric dependent_count/dependents case is a known,
separate gap left for a follow-up (not reported by ga-clgh, out of
scope here).

Did not bump JSONSchemaVersion / regen the golden corpus: this adds an
optional field via omitempty (no existing consumer's parse breaks,
no existing corpus fixture has comment_count > 0 for `show`), and
corpus-regen needs a live Dolt+Docker this sandbox doesn't have to
verify. Deferring that call to whoever can run `make corpus-regen` and
review the diff.

Fixes ga-clgh.

* fix(show): set comments_omitted on the proxied path too

The direct path flags comment_count as omitted when comments are not
streamed (ga-clgh); the proxied path silently left the flag unset. Mirror
the else-branch in proxiedBuildDetails, add proxied-path coverage for
present/included/zero cases, and document comments_omitted in the JSON
schema reference. No schema version bump: additive optional field already
emitted by the direct path.

Addresses the 2026-07-23 review on gastownhall/beads#4865.

Agent-Signature: claude-sonnet-5-medium on behalf of maphew

---------

Co-authored-by: matt wilkie <maphew@gmail.com>
2026-07-25 16:16:41 -07:00
Chris Sells 8c974d8dc4 docs: align user docs and CLI reference with the v1.1.0 release (#4874)
* docs: align user docs and CLI reference with the v1.1.0 release

The Mintlify site publishes from main, so the docs had drifted ahead of the
latest release — documenting features that no released binary ships.

- Remove architecture/storage-backends: v1.1.0 supports only Dolt (the
  SQLite/Postgres/MySQL backends are post-tag work on main). Redirect the
  old routes to /architecture/dolt and drop the alternate-backend claims
  from README, core-concepts, architecture index, and FAQ.
- Stamp the docs homepage with the release: "These docs are for the 1.1.0
  release" plus a link to the v1.1.0 release notes.
- Regenerate the CLI reference (docs/cli-reference/, docs/CLI_REFERENCE.md,
  docs.json nav) from the released v1.1.0 binary via tools/docsmint: removes
  pages for post-1.1.0 commands (heartbeat, migrate-personal, reclaim,
  unclaim) and post-1.1.0 flags.
- Fix hand-written pages that referenced post-1.1.0 CLI surface, verified
  against the v1.1.0 binary: bd dep relate/unrelate (not top-level
  relate/unrelate), bd admin reset (not bd reset), bd hooks list (not
  status), bd history / bd vc status (not vc log/diff), release the claim
  via bd assign + bd update (no bd unclaim), drop bd migrate-personal and
  bd daemon mentions, and reduce the Cursor integration page to the rules
  file that v1.1.0's bd setup cursor actually installs.
- Stop documenting the BD_ALLOW_REMOTE_MIGRATE escape hatch (and the
  nonexistent-in-1.1.0 bd migrate --force): the remote-migrate gate prints
  its own operator guidance by design; docs now defer to it.

* docs: auto-regenerate CLI reference

Applied from the cli-docs-freshness-patch artifact of https://github.com/gastownhall/beads/actions/runs/29613329749
(generated with CI's canonical pinned build). See
scripts/check-cli-docs-drift.sh for how drift is attributed.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-17 14:17:28 -07:00
Julian Knutsen 0cf3b3ca09 Revert "Simplify the supported backends: roll back the direct PostgreSQL and MySQL adapters" (#4857) 2026-07-16 16:35:51 -07:00
Test User 98573a4983 docs(storage): explain focused Dolt and SQLite support (bd-sadcd)
Document the rollback of direct PostgreSQL and MySQL adapters because supporting additional general-purpose server databases adds operational and maintenance complexity. Our goal is to keep Beads as simple as possible and consume as few resources as possible.

Clarify that embedded Dolt, Dolt server, and SQLite remain supported, including Dolt server use of the MySQL wire protocol.

Existing PostgreSQL and MySQL databases are not opened or modified. Migration requires an export from a supporting build, reinitialization guided by bd help init-safety, and import; JSONL remains issue-level interchange rather than a complete backup.

Refresh generated CLI reference material alongside the storage guidance.

Agent-Signature: codex-gpt-5-unknown-reasoning on behalf of Test User
2026-07-16 03:42:17 +00:00
Steve Yegge a530479774 test(protocol): pin the §J interchange clauses; fix the bd import header gap (wy-ltox1)
WP2 of the beads protocol conformance corpus (PROPOSAL-beads-protocol-v0 §11).
Seven §J clauses had no test; the keystone one had no test anywhere.

cmd/bd/protocol/interchange_test.go pins:

  J6.6  export -> import into an EMPTY store -> export reproduces the stream.
        bd-go had same-DB byte-stability and per-field round-trip tests, but
        nothing asserted CROSS-store equivalence — the exact blind spot where an
        import that drops, defaults, or rewrites a field hides. The fixture
        spans P0 (the zero-value priority), every core issue_type, a closed
        issue with close_reason, labels, inline comments, both dep orientations,
        metadata, assignee, and memories.
  J1.2  unknown fields ignored — a record carrying future fields at the top
        level, inside a dependency, and inside a comment still imports cleanly.
  J1.3  `_schema` header records skipped, on both the file and stdin readers.
  J6.1  import never deletes: a partial stream does not remove the ids it omits.
  J6.3  a dependency whose target id is absent is skipped AND reported
        (skipped_dependencies in `bd import --json`).
  J6.5  `bd import -` reads a stream from stdin.
  J2.4  zero/unknown timestamps export as the Unix epoch, never year-0001
        (which json.Marshal refuses outright, failing the whole export).

J1.3 needed a code fix, not just a test. parseJSONLFile (the bootstrap reader)
skipped the header record, but the `bd import` reader loop — the one `bd import`
and `bd import -` both run through — did not: the header line fell through to
the issue path, unmarshaled into an empty Issue, and aborted the WHOLE import
with "validation failed for issue : title is required". So a canonical export
carrying a provenance header could not be imported at all. Verified against the
new test: it fails with exactly that error without the guard.

Two stale artifacts found during the extraction, fixed here:

  - TestCLI_Import_PrefixValidation_E2E asserted that import REJECTS a foreign
    id prefix without --skip-prefix-validation, a flag that no longer exists
    (every import path now passes SkipPrefixValidation). Being build-tagged out
    of CI (cgo && integration), it rotted unnoticed. Rewritten as
    TestCLI_Import_ForeignPrefix_E2E, pinning the behavior bd actually has —
    which the protocol requires anyway: §J6.6 round-trips through a store with
    its own prefix, and §J7.1 makes ids opaque.
  - docs/reference/json-schema.md claimed export emits schema_version per line.
    It does not (and must not): schema_version belongs to the --json command
    envelope; the interchange's version marker is the optional _schema header.
    The import --json summary field list was also missing five fields.

Gates: gofmt + go vet clean, go build ./... ok, full cmd/bd/protocol package
green (280s, -count=1).
2026-07-13 11:20:06 -07:00
Chris Sells 69e33099b8 docs: apply PR #4722 review feedback to user docs
- FAQ: fix bulk migration/export commands (tracker pull/push need explicit
  IDs; bulk lives on sync --pull-only / --pull / --push-only / --push) and
  correct the cross-project isolation claim (hydration + external targets)
- quickstart: replace the broken DOLT.md blob URL with site links
- installation: Quick Install Script heading matches its macOS/Linux/FreeBSD
  body
- rewrite 33 stale SCREAMING_CASE link labels to the target pages' titles
- add description frontmatter to the 55 authored pages missing it
- docs.json: wildcard redirects for the old /next/ and version-prefixed
  route spaces (1.0.0, 1.0.4, 1.0.5)
2026-07-12 11:06:57 -07:00
Chris Sells 5b660081ed docs: fix fact errors found by adversarial review
Every fix verified against the binary or a traced source call path:

- How Beads Works: parent-child DOES affect ready work (a blocked parent
  blocks its children — the blocked-state SQL inherits it), hash-ID
  inputs are title/description/creator/creation-time plus a collision
  nonce (live generator is internal/idgen, not the legacy one), and
  bead gates dropped from the gates bullet (currently unresolvable).
- Graph-link type strings are hyphenated (relates-to, replies-to);
  underscore forms are rejected by the parser. Fixed corpus-wide
  including the README feature bullet.
- upgrading.md: the version-era table's boundaries were refuted by git
  archaeology (embedded Dolt shipped in v0.60.0 and always used
  .beads/embeddeddolt/; nothing changed at the claimed v0.63.3
  boundary) — replaced with a layout-based identification table.
- Phantom .beads/dolt/sql-server.log path in sync-failures and the FAQ;
  the managed server writes .beads/dolt-server.log.
- FAQ: ID growth matches DefaultAdaptiveConfig (min 3, max 8,
  probability-driven), dependency-type wording no longer claims exactly
  four types, and unmeasured performance figures are hedged instead of
  stated as facts.
- quickstart: bd migrate --cleanup doesn't exist; init installs hooks
  by default (no prompt) and has no merge-driver step; ready output
  quote and bd restore recovery source corrected.
- formulas.md: step types map to issue types (task/bug/feature/epic/
  chore; anything else falls back to task — human/gate are not step
  types), and there is no built-in formula search tier.
- todo.md: bd update takes --set-labels, not --labels.
- graph-links.md: broken #dependencies fragment → #add-dependencies.
2026-07-12 10:37:17 -07:00
Chris Sells cf2711f13e docs(nav): nest the CLI Reference under Reference
The 113-page generated CLI Reference moves from a top-level nav group to
a nested group inside Reference — Mintlify renders nested groups
collapsed by default, so the command list no longer dominates the
sidebar. The docsmint nav splice is depth-agnostic (indentation is
derived from the pages key's line), verified by regenerating and by
re-pointing its test fixture at the nested shape. Reference overview and
the beads-docs skill IA note updated.
2026-07-12 10:37:17 -07:00
Chris Sells 5c2fe0d496 docs: loss-check merge legacy docs/ originals into the site pages
Sixteen same-ground pairs reconciled: the ported website page won and every
fact living only in the older docs/ original was merged in before deleting
it (INSTALLING, SETUP, ARCHITECTURE, CONFIG, GIT_INTEGRATION, ADVANCED,
TROUBLESHOOTING, FAQ, MOLECULES, ROUTING, MULTI_REPO_AGENTS, and the five
agent-integration guides). The two divergent FAQs are now one canonical
FAQ with the comparison-first framing leading and pre-1.0 version gating
removed.

The routing pair inverted: the ported page described a routing system that
does not exist (bd routes, bd hydrate, .beads/routes.jsonl); the page was
rebuilt from the accurate sources (ROUTING.md + MULTI_REPO_AGENTS.md)
against the real surface (routing.mode config, role detection,
repos.primary/additional hydration, --repo override).

Every claim was verified against the Go source or the generated CLI
reference. Phantom surfaces removed across the corpus: bd pin/hook/pour/
wisp/routes/hydrate/reserve/lock/merge/stats, bd hooks status, bd vc
conflicts/log, bd dolt set shared-server, fictional beads_* MCP tool names
(real names taken from beads-mcp server.py), import.orphan_handling and
export.error_policy config keys, database.wal_mode/cache_size, stale
.beads/dolt/sql-server.log paths, and SQLite-era file paths. Notable
corrections: min_hash_length default is 3, .beads/metadata.json is
git-tracked, tombstone status is removed, wisps live in the main database
under an ephemeral flag.

Freshness markers moved with their content (configuration, ide-setup) and
the freshness gate + DOC_INVENTORY ledger now point at the new locations.
Load-bearing anchors preserved: #branchless-workflows-jujutsu--jj,
#circuit-breaker-server-appears-down-failing-fast, #extensible-database,
#multi-repo-hydration. Pointer stubs remain at docs/{SETUP,INSTALLING,
GIT_INTEGRATION}.md because released binaries print those paths
(allowlisted in the docsync guard).

Gates: go test ./test/docsync PASS; generate-cli-docs.sh --check PASS;
check-doc-freshness.sh PASS; mint broken-links: no broken links; mint dev
serves all sections (11 routes spot-checked at 200).
2026-07-12 10:37:07 -07:00
Chris Sells 54a7547b0d docs: canonical concept page, section overviews, and concept diagrams
- Rewrite docs/core-concepts/index.md as 'How Beads Works' — the one
  canonical concept page (modeled on Gas City's how-gas-city-works):
  problem-first framing, the bead -> dependencies -> ready-work loop, hash
  IDs, the formula -> proto -> molecule/wisp pipeline, gates, Dolt sync,
  federation, and storage modes, led by four mermaid diagrams. Other pages
  link here instead of re-deriving the model.
- Fact fixes caught while writing (verified against source/live help):
  hash IDs are content-derived SHA-256 (internal/types/id_generator.go),
  not random; bd pour/bd wisp/bd pin/bd hook are not commands (the real
  surface is bd mol pour, bd mol wisp, bd assign, bd update --claim);
  wisps are not stored in .beads-wisp/; cross-repo dependency syntax is
  external:<project>:<capability> (cmd/bd/dep.go); recovery index claimed
  SQLite errors in a Dolt product.
- Section overview pages: rewrite workflows/index, extend recovery,
  multi-agent, and integrations indexes to list every child with one-line
  summaries; add reference/index and put it in the nav.
- New concept diagrams: sequential-vs-hash ID merge collision
  (core-concepts/hash-ids) and embedded-vs-server mode
  (architecture/dolt).
- website/README.md: real build/verify instructions and parallel-run
  status for the Docusaurus site.
- Repoint example READMEs and page labels at the real quickstart page.
2026-07-12 10:37:07 -07:00
Chris Sells a86a2c5dfc docs: port user docs into the Mintlify site tree under docs/
Port the website's current (non-versioned) authored pages into docs/ as
Mintlify pages: frontmatter title as H1 (body H1s removed), Docusaurus
admonitions converted to Mintlify components, root-relative extensionless
links, mermaid fences kept as-is. website/docs/** is untouched and keeps
serving the Docusaurus site until cutover.

git-mv the user-facing docs/ originals with no website twin into the new
IA (core-concepts/, architecture/, workflows/, recovery/, multi-agent/,
integrations/, reference/), converted to Mintlify form. The six files
previously mirrored by sync-website-docs.sh now live once in the site.
ERROR_HANDLING.md and CONTRIBUTOR_NAMESPACE_ISOLATION.md move to engdocs/
(contributor pattern guide and design doc per content review).

Pointer stubs remain at docs/{DOLT,STORAGE-BACKENDS,PLUGIN,RECOVERY,
JSON_SCHEMA}.md because released bd binaries print those paths; QUICKSTART
and DOLT-BACKEND stay as existing pointer stubs.

Corrected while porting: the intro's storage diagram claimed data lives at
.beads/dolt/; embedded mode (the default) uses .beads/embeddeddolt/
(internal/storage/embeddeddolt/store.go), server mode uses .beads/dolt/.
2026-07-12 10:37:07 -07:00