Skip to content

goldziher/basemind

v0.29.2MIT

Full AI context layer over MCP — tree-sitter code-map, document RAG (PDF/Office/HTML/email + OCR + reranker), shared agent memory, on-demand web crawl, git history + blame + per-symbol diff. 300+ languages, 10+ coding-agent harnesses, content-addressed Fjall + LanceDB.

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

[0.29.2] - 2026-10-12

Added

  • Agent Plugins distribution (#69). The repository root now carries a conformant Agent Plugins 1.0.0 package (plugin.json, mcp.json, skills/), generated by ai-rulez (generate --plugin) and checked by validate --strict. Its MCP server runs basemind serve from PATH.

Fixed

  • A daemon from an older build that ignores Stop no longer wedges new sessions with "a previous basemind comms daemon ... did not stop": the takeover now escalates to SIGTERM, then SIGKILL, by pid.

[0.29.1] - 2026-10-11

Patch release: 0.29.0 shipped everywhere except crates.io, which rejected it. No index or blob format change.

Fixed

  • basemind 0.29.x can be published to crates.io. The vendored forks basemind-lsp-positions, basemind-tree-sitter-graph and basemind-tree-sitter-stack-graphs had moved to tree-sitter 0.27 without a version bump, so crates.io still served their 0.26 builds and basemind could not resolve a single tree-sitter. They are now 0.3.5, 0.12.1 and 0.10.1.
  • Windows build: an unused constant and unused test imports failed the -D warnings build; unix-only test fixtures no longer compile on Windows.

[0.29.0] - 2026-10-09

0.29.0 reshapes the CLI (one home for each command, a documented exit-code contract, honest output and daemon forwarding), makes the code map code-only with a separate document tier, adds a trigram prefilter for code grep, MCP resources, and a more robust comms protocol (v4). Read Breaking changes and Upgrading before installing.

Breaking changes

  • First scan after upgrading rebuilds the index. RELEASE_MINOR goes 28 to 29, so every view's index.msgpack and Fjall index are reset on open and every file is re-extracted (blobs are overwritten in place at their content-hash path; the git cache refills lazily). The LanceDB document tables and the blob store are not wiped; orphaned blobs are reclaimed by the post-scan sweep. Stop the old daemon first with basemind comms stop so the new binary starts its own.

  • CLI renames and removals, no aliases kept (the MCP admin tool is unchanged):

    RemovedUse instead
    admin statusstatus
    admin rescanrescan (also takes --no-git-history, --rebuild-git-history; runs the root guard)
    admin cache-stats / admin gc / admin cache-clearcache stats / cache gc / cache clear
    admin delta / admin checkpoint / admin wastedelta / checkpoint / detect-waste
    graph map --include-churngraph map --no-churn (churn is on by default)
    serve --git-cache-mem, --no-git-cache-disk, --no-watch, --documents-*none; serve is a stdio relay to the daemon and ignored them

    admin keeps repo, telemetry, compress, tokens and eval.

  • Exit-code contract: 0 ok, 1 error, 2 usage, 3 writer lock held by another process, 4 required daemon unreachable, 130 interrupted. scan and rescan used to print the lock notice and exit 0; they now exit 3. An invalid rescan path (outside the repo, or neither on disk nor indexed) and invalid tool parameters exit 2.

  • cache clear needs --yes (or a terminal confirmation) for every component except git-cache, refuses with exit 3 while a writer holds the workspace lock, and refuses blobs while the daemon runs. blobs is the machine-global store shared by every workspace.

  • cache gc deletes; --dry-run reports. The old admin gc reported while cache gc was a no-op.

  • Output changes: the human-mode timing footer moved from stdout to stderr (--json keeps elapsed_us / startup_us); human output no longer truncates strings to 200 characters (only table cells are shortened, and stderr says so).

  • --format accepts only json or toon on code semantic and memory documents; the other closed-set flags are now value enums (see Added).

  • agents / workspace --json reshaped to the MCP response types and pretty-printed. agents list gains description, skills, first_seen, last_seen; threads use last_activity_micros; message rows gain age_secs and body_sha; empty optionals are omitted.

  • hook install is safer, so old hooks should be refreshed. It asks git for the hooks directory (honours core.hooksPath and linked worktrees), refuses to overwrite a pre-commit hook it did not write unless --force (the old one is kept as pre-commit.bak), and the generated hook is fail-open (exit 0 when basemind is missing or the scan fails). Re-run hook install.

  • Code map is code-only. Markdown, reStructuredText, AsciiDoc, vimdoc, CSV, JSON, YAML, TOML, INI, XML, .properties, .env, diffs, .gitignore / .gitattributes and Fluent files go to the document tier (searchable with memory documents), so symbols, outline and grep no longer return README headings or config keys. [documents] include / exclude and the MIME allowlist gate them. Builds without the documents feature keep the old behaviour.

  • code outline / symbols no longer index names bound inside function, method or lambda bodies (locals, nested functions); module- and class-level definitions are unchanged. Value-like symbols store a shorter signature (MAX_RETRIES = 3 becomes = 3; initialisers cut at 80 characters).

  • MCP logging is retired (SEP-2577): no logging capability, no logging/setLevel, and rescan no longer emits the rescan_complete log notification. Counts are in the tool result; progress notifications are unchanged.

  • MCP memory tool schema dropped ~26 documents.* / llm.* override fields (llm_api_key, llm_model, reranker_*, embedding_preset, ...). They remain CLI flags, env vars and basemind.toml keys; memory mode documents keeps format, max_tokens, mime_type, scope, entity_category and keywords_contains. No API key travels as a tool argument any more.

  • agents wait is capped at 40 s (was 300).

  • Comms protocol version 4: requests carry ids. Stop the old daemon (basemind comms stop) after upgrading so clients and daemon speak the same version.

Added

  • basemind doctor (root, config, index, grammars, pre-commit hook and daemon checked with filesystem and process probes only; --json; exits 1 when a check fails), top-level status, completions <shell> (bash, zsh, fish, elvish, powershell) and man.
  • hook install --force, cache gc --dry-run, delta --new <FILE>, detect-waste --log <FILE>.
  • Value-enum flags (parse-time errors listing the choices, shell completion): graph --direction / --edges / --algorithm / --granularity / --format, code semantic --lane / --format, git search --field, git symbol-history --hash-mode, memory proposals --kind, admin telemetry --window, admin compress --level.
  • CLI pagination and size controls passed straight to the MCP params: --cursor on code symbols / grep / files / find / references / callers / implementations, git recent / search / touching / by-path / blame / blame-symbol / symbol-history and memory list / proposals; --max-tokens on code outline / symbols / grep / files / find / references / callers / implementations / semantic and memory documents; --rerank-top-k on code semantic; --format json|toon on the code list modes and memory documents.
  • CLI forwards to a running daemon: memory, references / callers / graph reads and the grep prefilter no longer fail with memory_by_key index not available or degrade to a truncating in-RAM projection. With no daemon and another process holding the index lock, the error names the remedy and exits 3.
  • Tool subcommand paths (including git paths and graph --focus) normalize to the repo-relative index key: absolute, ./-prefixed and relative spellings are the same file. rescan also validates paths and routes through the daemon instead of racing it.
  • CLI/MCP parity test (tests/cli_parity/): a capability table maps every MCP tool and mode to a CLI command (or declares it cli_only / mcp_only with a reason), compares per-mode parameters against reasoned exceptions, and checks that representative reads return equal --json payloads.
  • MCP resources basemind://status, basemind://repo/map and the templates basemind://outline/{path} and basemind://memory/{key} (with {path} completion). Bodies equal the tool results; paths are validated against traversal; listed on demand; no subscribe support.
  • Human-readable title on each of the nine tools.
  • agents post takes an optional idempotency_key (generated when omitted); the daemon stores it per (agent, thread) for an hour and returns the original message id on a repeat. The stdio relay replays keyed posts and read-only requests once on a replacement backend instead of failing with backend_restarted.
  • Cancellation: code grep, code semantic, graph map, graph calls, memory documents and agents wait honour notifications/cancelled (answering -32800 request_cancelled), and cancelling an offloaded task reaches the running tool. Mutating modes (rescan, put, ...) always run to completion.
  • web crawl reports MCP progress per page; task statusMessage mirrors rescan and crawl progress.
  • code grep trigram bloom prefilter: one bloom filter per indexed file (new grep_bloom keyspace, about 12.8 % of indexed code bytes) lets grep skip files that cannot match a required literal. Results, totals, hit order and cursors are identical to the full sweep, including for files edited since the last scan. No index wipe; blooms are built on the next scan. BASEMIND_GREP_BLOOM=0 forces the full sweep. See ADR-0012.
  • EXTRACT_EPOCH: extractor-output revision on every file entry and blob. The next scan re-extracts only stale files, so a patch release can fix extraction without a minor bump. The scan summary shows tier_migrated and refreshed. Epoch 1 refreshes signatures produced before the header-only fix.
  • Comms: BASEMIND_COMMS_REQUEST_TIMEOUT_SECS, BASEMIND_COMMS_HANDSHAKE_TIMEOUT_SECS (default 30 s), BASEMIND_RELAY_REQUEST_TIMEOUT_SECS; the detached daemon logs to a size-rotated <comms_dir>/daemon.log, shown by comms doctor.
  • BASEMIND_BLOB_GC_GRACE_SECS overrides the 6 hour age below which the blob sweep keeps a blob.

Changed

  • code find and code files cover every indexed path (code map plus document tier), labelling documents by grammar name or extension. find drops candidates scoring under 85 % of the best match. On a 90-task eval: recall 0.59 to 0.83, hit@1 0.58 to 0.82, precision 0.28 to 0.62.
  • Outline eval precision rises from 0.67 to 1.00 and the mean outline response shrinks from 1384 to 747 tokens with the narrower symbol scope.
  • Token-savings estimates are recalibrated from measured eval ratios (outline 1.2x, symbols 25x, references and implementations 1.2x, docs 2.5x; callers, dependents, find and files claim no saving).
  • Upgrading is self-cleaning: the blob sweep is tier-aware, and cache gc performs it (serialised machine-wide by cache/gc.lock; blobs younger than 6 hours are kept). A scan that migrated paths, re-extracted blobs or reset a view prints cleanup: reclaimed N orphaned blob(s); the daemon does the same a minute later. The MCP admin mode gc stays a non-destructive report. See docs/UPGRADING.md.
  • Performance: symbols and dependents answer from a resident term index; references, callers and implementations use a resident name dictionary (needles matching over 5 % of names fall back to the full walk); code grep reuses a per-thread read buffer. Results are unchanged.
  • Comms daemon runtime has 8 workers (was 4) and liveness probes run off the async workers.
  • The production rmcp dependency drops its default features. Dependencies upgraded: lancedb 0.37 to 0.40 (with its remote feature; lance 10 to 13), gix 0.89, oxc_* 0.153, crawlberg 1.10, xberg 1.3.8 and tree-sitter-language-pack 1.21.3 (both now chunk in linear time, so the quadratic stalls behind the Fixed entries below are fixed at the source; the cutovers stay as a backstop), zstd 0.14.2, plus lockfile refreshes. arrow-* stays on 58 to match lancedb 0.40.
  • Eval generators follow the new symbol scope; gen_find.py takes --indexed FILE.

Fixed

  • CLI --format toon crashed the renderer; human mode now renders the TOON response.
  • code references, callers and implementations from a daemon-backed session are complete. They used to answer from an in-RAM projection capped at [resources] max_map_cache_mb and returned a truncated lower bound behind a projections_capped notice. The scan is now forwarded to the daemon (IndexRead); the projection is a last-resort fallback only.
  • Ctrl-C during scan / rescan stops at a file boundary (completed batches stay committed, the stale purge is skipped), prints the summary and exits 130; a second Ctrl-C exits immediately.
  • Comms unresponsiveness: replies are correlated by request id, so an abandoned request (such as the delivery-notice probe timing out) can no longer leave a stale reply misread as unexpected response shape; requests and handshakes time out with a retryable comms: broker unresponsive, the relay answers backend_timeout, and a slow connect for one identity no longer blocks the others.
  • Comms daemon stalls under slow disk: blocking CommsStore calls and registry writes run on the blocking pool (mutations stay serialized through one write gate, so seq order holds), and the hourly retention sweep moved off the runtime.
  • The inbox read bounds its unread scan (unread is a lower bound past roughly 500 rows) and the delivery probe uses its own connection, at most once every 2 s.
  • agents wait stops as soon as the host cancels or the connection closes, and no longer returns instantly on messages a previous wait already returned.
  • A comms or shells daemon starting as the previous one exits no longer mistakes a just-released lock for a live peer and exits silently (the lock waits up to 300 ms).
  • Symbol signatures in indentation-based languages end at the grammar's body field instead of embedding the whole body; existing indexes refresh through EXTRACT_EPOCH.
  • macOS: the heap no longer shows up as GBs of "GPU" memory; mimalloc's default Mach VM tag 100 (VM_MEMORY_IOACCELERATOR) is retagged to 254 at startup.
  • A panic inside an embedding job no longer aborts the process; it surfaces as an embedding worker panicked error.
  • Flaky daemon_lock and store_layout tests (a sibling test's fork/exec holding a released flock; a test mutating the process-global BASEMIND_DATA_HOME).
  • Stale docs: serve flags, memory mine flags, --json help text and the admin command tables.

[0.28.1] - 2026-10-06

Added

  • basemind admin eval: a repo-agnostic retrieval-quality and token-savings evaluation. It runs a JSONL task file (modes symbols, outline, references, callers, grep, find, dependents, git_search, docs) through the same in-process tool code the MCP server uses, scores answers against gold (set P/R/F1; hit@1/@5, MRR and nDCG for ranked tasks), records per-call latency and response tokens, and reports per-mode p50/p95 latency and quality as NDJSON, JSON and markdown. Tasks may carry a baseline (grep command and/or whole-file reads); its output is counted with the same tokenizer and savings are only credited when recall reaches --min-recall, then compared with the fixed multipliers in src/mcp/savings.rs (modes deviating more than 25% are flagged). --baseline <report.json> exits non-zero on a regression beyond --tolerance / --cost-tolerance.
  • benchmarks/eval/: stdlib-only Python gold generators (gen_symbols, gen_outline, gen_references, gen_dependents, gen_grep, gen_find, gen_git_search, gen_docs), deterministic for a fixed HEAD sha and seed, with a workflow README.

Changed

  • [resources] onnx_provider (new) defaults to cpu. basemind previously left ONNX Runtime on its platform default, which on macOS is CoreML. CoreML compiles a new graph per input shape and measured 7.6 GB resident after embedding one 2 KB SVG, against 0.8 GB on the CPU provider (which is also ~3x faster end to end here); a scan of a repository with images and office documents grew to 33 GB. The setting covers the embedder, reranker, layout detection and NER. Set onnx_provider = "auto" to restore the old platform default, or "coreml" / "cuda" / "tensorrt" to pin one. Existing configs keep parsing; only the resource behaviour changes.
  • The document tier now serialises over the memory ceiling. FootprintGate::admit (unchanged, still advisory for the drive governor and large-file parses) admitted every waiter after five seconds, so N workers over max_footprint_mb meant N simultaneous extractions. Documents use the new admit_exclusive: over the ceiling exactly one document runs at a time, and an otherwise idle process is admitted immediately instead of stalling every item for the full wait.
  • [documents] max_archive_bytes (default 64 MiB) and max_archive_entries (default 1000) bound archive extraction when extract_archives = true (xberg's own defaults were 500 MB and 10,000).

Fixed

  • Stack-graph (L2) resolution no longer drives scan memory: each file is abandoned past 600,000 steps, 50,000 partial paths or 3 s, and at most two builds run at once. A wall-clock budget alone let parallel workers stitching 100-200 KB files take a large-monorepo scan from ~2 GB to 9-13 GB.
  • A scan could deadlock with max_concurrent_documents set: a scan worker holding a document slot waited on the embedding pool with ThreadPool::install, which keeps running queued jobs from the worker's own pool, so it started a second document that blocked on the slot the first still held (one scan thread and one slot hangs forever; more threads hang once every slot is held by a blocked frame). The embed hand-off now blocks on a channel and never runs foreign work.
  • Documents whose own estimated working set exceeds max_footprint_mb are skipped (counted as too large, with a warning naming the estimate and the limit) rather than extracted: a 7 MB-of-XML .docx measured 12 GB with CoreML, and a 30,000 x 30,000 pixel image header is priced from its dimensions, not its byte size.
  • A large text document could stall a scan for tens of minutes. xberg's markdown splitter (before 1.3.8) re-collects and re-sorts every remaining element for each chunk it emits, so chunking time grows with the square of the document (measured: 0.9 s at 256 KB, 3.3 s at 512 KB, 11.6 s at 1 MB, 40 s at 2 MB of log text, and 17 s at 256 KB of densely marked-up markdown, in a debug build). 0.29.0 routes csv, json, yaml, xml, txt and similar files to the document tier, so multi-megabyte data files reached it. Text-like documents over the new [documents] markdown_chunk_max_bytes (default 256 KiB) / plain_text_chunk_max_bytes (default 128 KiB) cutovers are now chunked by a linear fixed-size chunker; smaller documents chunk exactly as before. Degraded documents are logged and counted as docs_degraded in the scan summary.
  • [documents] extraction_timeout_secs is now enforced. It was passed to xberg but could not interrupt CPU-bound work. Extraction now runs under a watchdog: an overrunning document is abandoned, logged, skipped, retried on the next scan, and counted as doc_timeouts (also in failed).

[0.28.0] - 2026-10-04

Minor release — persisted schema bumped (RELEASE_MINOR 27 → 28). The first basemind scan after upgrading wipes and rebuilds the on-disk cache (blobs, the Fjall index, vector rows) from source. No manual step is needed; a full re-scan is automatic.

Added

  • Project-level .config/ convention (https://github.com/pi0/config-dir): configuration is now also discovered at .config/basemind.toml or .config/basemind/config.toml when no root basemind.toml (or legacy .basemind/basemind.toml) exists. The root file still wins when both are present. Any of these locations now counts as the durable "this is a basemind-managed root" marker for upward root discovery and the workspace-root guard, so a project that keeps its config under .config/ no longer needs --root.
  • basemind init --config-dir: scaffold the config under the .config/ convention. Accepts .config (writes .config/basemind.toml) or .config/basemind (writes .config/basemind/config.toml); any other value is rejected, since there is no read-time config-path flag and a config written elsewhere would never be auto-discovered.
  • [languages.<grammar>] per-grammar overrides keyed by tree-sitter-language-pack grammar name: enabled = false stops parsing that grammar's files as code (they fall through to the document tier; fixes .txt detected as vimdoc and .conf as nginx), extensions / filenames map extra suffixes and exact file names onto a grammar ahead of built-in detection, and preload makes basemind lang install fetch it. An unknown grammar name is a config error with near-match suggestions.
  • [scan] floor_allow: remove entries from the always-on exclude floor by directory, file name, glob or floor pattern. .git and .basemind can never be allowed; an entry naming nothing in the floor is ignored with a warning.
  • [documents] include / exclude (which non-code files become documents), max_file_bytes (per-document cap, default 50 MiB, independent of [scan] max_file_bytes), and embed_include alongside the existing embed_exclude; [code_search] embed_include with the same semantics (allow-list, embed_exclude wins, left-out files stay keyword-searchable).
  • Cached chunks and documents are fingerprinted against the settings that shape them, so changing a chunk-size, page-cap, keyword, NER, summarization or [llm] model setting re-processes only the affected files. The first scan after an embedding-scope change (embed, embed_include, embed_exclude, the enabled switches) deletes the vector rows of files that are no longer eligible and rebuilds the rows of newly eligible ones from cached blobs without re-embedding.
  • The shared daemon loads each workspace's config once, applies operator-raisable resource ceilings (BASEMIND_DAEMON_MAX_SCAN_THREADS, _EMBED_THREADS, _EMBED_BATCH, _CONCURRENT_DOCUMENTS, _FOOTPRINT_MB, _MAP_CACHE_MB, _CANDIDATES, _FILE_BYTES, _DOCUMENT_BYTES, _DOCUMENT_PAGES, _EXTRACTION_SECS, _CHUNKS_PER_DOCUMENT, _CRAWL_PAGES, _CRAWL_DEPTH, _CRAWL_BODY_BYTES, and the BASEMIND_DAEMON_MIN_DEBOUNCE_MS floor), and reloads a workspace's basemind.toml on the next request after it changes. A file that no longer parses keeps the last good config and logs a warning. admin status reports config_stamp to spot an edit.
  • Operator environment grants for settings a repository's own basemind.toml cannot set itself: BASEMIND_ALLOW_REPO_LLM, BASEMIND_ALLOW_FOLLOW_SYMLINKS, BASEMIND_ALLOW_EXTRA_ROOTS and, in the daemon, BASEMIND_DAEMON_ALLOW_EXTRACT_ARCHIVES. BASEMIND_ALLOW_EXTRA_ROOTS takes 1 / true / yes for every workspace, or a :-separated list of absolute workspace roots that grants only those (; on Windows); relative entries are ignored with a warning.
  • A new linked worktree's working view is seeded from a sibling checkout with a copy-on-write clone, so its first scan only touches files that differ. A sibling whose workspace lock is held, or one too large to copy without reflinks, is skipped; a crashed clone's .seed-tmp debris is swept on open. BASEMIND_NO_SEED=1 opts out.
  • A linked worktree whose HEAD descends from the indexed git-history head now appends to the shared index instead of skipping it.
  • Reserved config keys that nothing reads log a warning when set to a non-default value.

Changed

  • Bare names in every glob list ([scan], [documents], [code_search] include / exclude / embed_*) are gitignore-like: generated matches that path segment at any depth and everything beneath it, and docs/api is anchored at the root. A leading ./ or / and a trailing / are ignored. A negated (!) pattern, an invalid glob, or an empty [scan] include is a config error. For extra_roots files, globs match the path relative to the extra root.
  • Repo-supplied [llm] base_url, foreign api_key env references, [crawl] allow_private_network, [scan] follow_symlinks and [scan] extra_roots now need an operator grant in the environment (BASEMIND_ALLOW_REPO_LLM, BASEMIND_ALLOW_PRIVATE_HOSTS, BASEMIND_ALLOW_FOLLOW_SYMLINKS, BASEMIND_ALLOW_EXTRA_ROOTS). An api_key env reference is still honoured for the chosen provider's standard variable and BASEMIND_LLM_API_KEY.
  • BASEMIND_ALLOW_PRIVATE_HOSTS accepts 1, true or yes in both the config gate and the URL guard (the URL guard previously accepted only 1).
  • The daemon treats [resources], [scan] max_candidates / max_file_bytes, [documents] limits, [crawl] limits and [watch] debounce_ms as ceilings or floors, and clamps the per-call web crawl max_pages / max_depth overrides to the same caps.
  • mimalloc is the global allocator. ONNX Runtime memory is bounded in every process (no memory-pattern planning, no retained CPU arena; intra-op threads capped), and the daemon releases resident embedding engines once the last concurrent embedding pass ends.
  • The daemon caps its runtime and pool threads, serializes read-stack builds, defers the startup GC, bounds fjall worker threads and journal size (flushing an oversized journal on open), and runs git-history index sync on a dedicated two-thread pool.
  • Blob chunk state is read from the envelope header alone instead of decoding the blob.
  • Changing a reserved key ([documents.language] preferred_languages, [documents.ocr]) no longer re-extracts every document.
  • basemind.toml scaffold comments written by basemind init describe the new keys, the credential floor and the operator grants.

Fixed

  • Linked worktrees no longer wipe and rebuild the shared git-history index when they switch branches.
  • The file watcher rebuilds its include/exclude filter when basemind.toml changes, and detects an equal-length rewrite of the config inside one timestamp tick.
  • Incremental scans (watcher, rescan paths) evict files that became ineligible, skip symlinks, and cap the bytes read.
  • The embed policy is recorded only after a complete purge, and keyword postings are rebuilt when needed; empty-scope lance deletes are skipped and large ones are batched at 2000.
  • Extraction returns an error instead of panicking when its runtime cannot be built, and the git-history pool falls back to the caller's thread instead of panicking under thread exhaustion.
  • Re-flushing an external-root (extra_roots) document replaces its existing vector rows instead of leaving stale duplicates: the delete now uses the document's own per-root scope, matching the rows it inserts.
  • The last concurrent embedding pass releases resident engines under a lock, so a pass that starts concurrently cannot have the engines it is about to use freed underneath it.

Security

  • Credential and key material is excluded from the index by default, so no secret becomes searchable by every agent that can query it: .env and .env.* (including .env.example), .aws/, .ssh/, .gnupg/, .npmrc, .pypirc, .netrc, .git-credentials, id_rsa / id_dsa / id_ecdsa / id_ed25519, *.pem, *.key, *.p12, *.pfx, *.jks, *.keystore. [scan] floor_allow opts a class back in, but a credential entry needs the operator's BASEMIND_ALLOW_REPO_CREDENTIALS grant — a cloned repository's own basemind.toml cannot un-exclude the operator's untracked secrets (build-artifact entries still need no grant).
  • follow_symlinks is an operator decision: the repository's own basemind.toml cannot turn it on. Working-tree reads, including the watcher and rescan paths, refuse symlinked files and paths that resolve outside the workspace, and a basemind.toml that is a symlink leaving the workspace is not followed.
  • Repository-authored basemind.toml can no longer redirect an LLM endpoint or read an arbitrary environment variable as an API key, reach private hosts, follow symlinks, index directories outside the repository, or expand archives in the daemon, without an operator grant (see Changed). extra_roots additionally refuses credential directories (.ssh, .aws, .gnupg, /etc), filesystem roots and roots inside the repository, grant or not.
  • Daemon resource ceilings bound what a hostile repository config can cost: scan and embed threads, batch size, footprint, candidates, file and document sizes, pages, extraction time, chunks and crawl pages, depth and body size.

Removed

  • The TUI and agent stack was removed upstream and is not part of this release: the basemind-tui, basemind-agent, basemind-agent-ipc and basemind-ui crates, the basemind ui and basemind agent subcommands, the agent-tui / desktop-ui features, and the [agent] config table (now rejected as an unknown key; delete it from basemind.toml). None of it was ever part of a release archive: the agent-tui / desktop-ui features were omitted from full and the release workflow built only the basemind binary. basemind agents (multi-agent comms) is unrelated and unchanged. ADR-0006 is marked withdrawn.

[0.27.4] - 2026-09-28

Patch release — init defaults to local/gitignored config, a benchmark harness, and a read-only-store flush fix. No schema or MCP-surface change: RELEASE_MINOR stays 27, so the on-disk blob and index formats are untouched.

Added

  • basemind init now defaults every writable target to its .local/gitignored form: an ai-rulez config routes to .ai-rulez/local/rules/basemind-usage.md instead of the committed rules tree, and a new .gitignore-coverage check prompts (or, with --yes, auto-appends) a pattern for any .local target .gitignore doesn't already cover.
  • A new .claude/settings.local.json step in init, adding the basemind MCP permission entries via a surgical JSON merge (serde_json's preserve_order feature) that only touches the keys basemind owns — an existing skillOverrides or other hand-ordered content in that file is left untouched.
  • basemind admin tokens --stdin: counts stdin with the real o200k tokenizer (src/mcp/tokens.rs) and prints just the integer. Gated behind the tokenizer feature (already implied by documents), not the heavier documents/code-search builds.
  • benchmarks/: a harness (run.sh + tasks.example.yaml) that runs a fixed task list twice — once via a basemind command, once via the equivalent grep/cat/find baseline — and reports real token counts (via admin tokens --stdin) and wall-clock time for each side, plus totals. See benchmarks/README.md.

Fixed

  • admin rescan / scan_and_refresh could fail with ENOENT on views/working/index.msgpack.tmp on a workspace's first scan, whenever the Store was opened via open_read_only (which — unlike open_with_holder — never creates view_dir on disk). The MCP admin rescan tool and its CLI shim both open their Store this way, so this reproduced on any workspace's first-ever scan through either surface. Fixed with a new Store::ensure_dirs(), called inside scan_and_refresh right after acquiring the write lock, so a store opened read-only still gets its rebuildable directories materialized before the first flush().
  • Store::open_with_holder now acquires its lock before creating rebuildable cache subdirectories, closing a lock-ordering gap that could race a concurrent opener.

[0.27.3] - 2026-09-27

Patch release — the first publish that completes on every platform. Supersedes the unpublished v0.27.1 and v0.27.2 drafts: no crates.io, npm or PyPI version was cut from either.

Fixed

  • The release helpers no longer pass gh api an endpoint with a leading slash. Git Bash on the Windows runner rewrites an argument beginning with / into a filesystem path, so gh api "/repos/…" became C:/Program Files/Git/repos/… and the call failed with invalid API endpoint. The Windows build job hit this on its pre-upload resolve and skipped the upload, and finalize_release runs on every leg via always(). The endpoints now omit the leading slash, as the CLI's own error recommends; the absolute uploads.github.com URL is unaffected.

[0.27.2] - 2026-09-27

Never published as a GitHub release. The Linux builds were fixed below, but the Windows upload and the promotion still failed, so no registry version was cut. Superseded by [0.27.3].

Fixed

  • The shells feature no longer fails to compile on Linux. rustix was refreshed to 1.1.5, which renamed its experimental runtime module to a rotating mangled name (runtime_448b8ad740e2a26f) and reduced the old runtime path to a private alias. rmux-client 0.10.0 imports rustix::runtime::{…} from its Linux attach-resize path, so only the Linux builds broke (E0603); macOS and Windows were unaffected. rustix is now held at 1.1.4, the last release whose runtime module is public and the version rmux-client itself declares.

[0.27.1] - 2026-09-27

Never published as a GitHub release. The source below was tagged and pushed, but the Linux archives failed to build and the release record was never promoted. It is superseded by [0.27.3]; no registry version was cut from it.

Patch release — memory-safety follow-ups to #62, a stricter plugin launcher, and a release-workflow fix. No schema or MCP-surface change: RELEASE_MINOR stays 27, so the on-disk blob and index formats are untouched.

Changed

  • Dependency refresh: crawlberg 1.7.1 → 1.8.0, gix 0.87.1 → 0.88.0, jsonschema 0.56 → 0.58, and lru 0.18.4 → 0.18.5. lancedb stays held at 0.37: 0.38 and 0.39 still gate the Error::Http variant behind their remote feature while job.rs references it unconditionally, and enabling remote would pull an HTTP client stack in for no benefit here.

Fixed

  • Comms daemon memory growth (#62), continued:
    • The per-connection comms client map is now bounded with an LRU. It is keyed by the caller-supplied as_agent, so it previously grew one broker socket and one notification queue for every identity a session ever named.
    • The pending-notification queue is now capped, dropping the oldest entries. Clients that only issue ordinary requests never drain it, so it grew for the life of the link.
  • The git-cache RAM categories are now charged by bytes as well as entry count, each held to 32 MiB. A single CommitInfo or BlameResult has no natural size, so 1024 entries could reach gigabytes.
  • The file watcher's debounce queues are bounded, and overflow coalesces into a path set instead of queueing without limit. A rescan slower than the debounce interval could otherwise let batches accumulate unbounded.
  • gix 0.88 no longer accepts the Infallible turbofish, which src/git/commit.rs still passed.
  • The plugin launcher never falls back below 0.26.0, verifies each candidate is installable, and honours BASEMIND_BIN. With v0.27.0 incomplete, the old fallback exec'd 0.25.2, which predates the #62 memory bounds and OOM-killed the machine.
  • The release workflow now addresses assets by release id rather than by tag. A hand-promoted v0.27.0 draft left two records on one tag and split the archives across them; uploads replace in place, and every stage refuses an ambiguous tag.

[0.27.0] - 2026-09-18

Minor release — a toolchain dependency refresh and a lint-driven hardening pass. The agent tool surface and the config schema are unchanged, but RELEASE_MINOR advances 26 → 27, so the first scan after upgrading rebuilds the content-addressed blob cache automatically.

Changed

  • tree-sitter 0.27 (was 0.26), with tree-sitter-language-pack 1.20.0 to match. Two breaking API moves were adapted:
    • QueryMatch.captures became the method QueryMatch::captures(). The L1/L2 extractors (src/extract/l1.rs, src/extract/l2.rs, src/extract/locals.rs) and the intel resolver (src/intel/stackgraph.rs) were migrated.
    • Node::kind() now returns a tree-borrowing &str. The vendored tree-sitter-graph keeps SyntaxNodeRef's kind 'static by interning node-kind names into a small global table (crates/tree-sitter-graph/src/graph.rs).
  • rmcp 3.4 (was 3.2). The handshake types that collided with the wire protocol's serverInfo/clientInfo fields were renamed: ServerInfo → ServerConfig (src/mcp/server_handler.rs) and ClientInfo → ClientConfig (tests/tasks_smoke.rs). The server still advertises MCP logging; that capability line now carries #[allow(deprecated)] until the SEP-2577 deprecation removes logging in a future rmcp.
  • jsonschema 0.56 (was 0.52), crawlberg 1.7.1 (was 1.5.0), lru 0.18.4 (was 0.18.3), zstd 0.14.0 (was 0.13.3), and liter-llm 2.0.2 in the agent crate (was 1.19.1). lancedb stays pinned at 0.37: 0.38 and 0.39 still fail to compile without an HTTP-client stack basemind has no use for.
  • The provenance ledger in the layered config merge (src/config/layered.rs) is no longer inlined per field. The merge now records a field's ConfigSource through one record_provenance helper — which also skips the ledger entirely on the MCP override path that throws it away — and the merge's cyclomatic complexity drops from 43 to under the 40 ceiling. The two override tests pin the behaviour.

Fixed

  • The npm and pip installers' network paths were lint-harded: npm-package/install.js no longer returns from a Promise executor or reads the deprecated RegExp last-match globals (parseInt → Number), and its thrown errors carry { cause }; pip-package/basemind/ downloader.py moves the type-only import under TYPE_CHECKING.
  • Typos in tests (a misspelled repositories in a test name and a stray fixture path) and a dangling return; in scripts/codex-mcp-launch.mjs. The opencode plugin's console fallback explains why it exists instead of carrying a bare suppression directive (no-console was already ignored for JS in poly.toml, so the rule it guarded never fires).
  • poly.toml gained per-file ignores for the vendored fork crates and for CHANGELOG.md (length is intentional), and the shipped shell scripts were re-rendered by the shell formatter — whitespace-only, no behaviour change.

[0.26.0] - 2026-09-01

Minor release — cache rebuild, breaking changes, and a security fix. Read Breaking changes and Security before upgrading. Almost all of this release is issue #62: a comms daemon reached 43.8 GiB RSS in about two minutes on an initial scan of a large monorepo and was OOM-killed, taking a VS Code renderer with it; under a 2 GiB MemoryMax the same scan died in about 50 seconds.

Breaking changes

  1. A workspace root must be a project. It has to be a git repository, or a directory containing basemind.toml. Anything else is refused, and a filesystem or volume root is refused with no override at all. If you hit this: run basemind init in the directory (writes basemind.toml, the durable marker), or set BASEMIND_ALLOW_ANY_ROOT=1 for that invocation. A root that already had a working-view index before the upgrade keeps working untouched.
  2. [scan] extra_roots is ignored unless BASEMIND_ALLOW_EXTRA_ROOTS=1 is set in the environment that launches basemind. If you hit this: export the variable where your MCP host or shell starts basemind — it deliberately cannot be granted from the scanned repository's own config.
  3. [resources] max_footprint_mb = 0 now means auto, not disabled. Every config generated before this release carries that value, so upgrading gains a memory ceiling rather than keeping none. If you want the old behaviour: write max_footprint_mb = "off".
  4. Caches are wiped and rebuilt on the next scan. INDEX_PARTITION_REVISION advances 4 → 5, so the Fjall secondary index is recreated on next open — fjall persists a keyspace's max_memtable_size at create time and ignores the create options for a keyspace that already exists, so the new per-keyspace sizing only takes effect on a recreated index. RELEASE_MINOR advances 25 → 26 with the release cut, which also invalidates the content-addressed blob cache and the git cache. No action required: rebuild is automatic, but the first scan after upgrading is a full one.
  5. Git history is no longer indexed for a workspace root that is merely a subdirectory of an enclosing repository. If you relied on it: set BASEMIND_GIT_DISCOVER_PARENTS=1.

Security

  • The daemon's streamable-HTTP MCP endpoint is no longer unauthenticated, and no longer starts by default. Versions 0.23.0 through 0.25.2 bound 127.0.0.1:51786 unconditionally and gated requests only with a Host / :authority loopback check. That check defeats DNS rebinding and nothing else: any local process could open a TCP connection, send Host: 127.0.0.1, and reach the full shared tool router — which in the release build (--features full) includes the shell tool, enabled by default. Every process on the machine that could reach the port could therefore run commands as the daemon's user. Two changes close it:
    • The listener is opt-in. It binds only when BASEMIND_ALLOW_HTTP is truthy in the daemon's environment; unset, no socket exists. An environment variable rather than a config key because the daemon's environment belongs to whoever started it, whereas basemind.toml is authored by whoever wrote the repository.
    • When it does bind, every request on both routes (/mcp and /ui) must present a bearer token — 32 bytes from the OS CSPRNG, drawn per daemon process, checked before routing and before any workspace is named or opened, compared without an early exit. The token is published as the second line of <comms_dir>/http.addr, created mode 0600 at open time so it is never briefly world-readable. Authorization: Bearer <token> and ?token=<token> are both accepted (a browser navigating to /ui cannot set a header). basemind daemon ensure reports the token: on stderr for the human form, so the URL stays alone on stdout and URL=$(basemind daemon ensure) keeps working, and in the object for --json.
  • The Claude and Cursor plugin manifests no longer point at that port. They now launch scripts/mcp-launch.sh serve --root <project dir> over stdio. A hard-coded loopback port is not addressable to a process: whichever process bound it first is what the client talked to, so a local squatter could silently intercept every tool call, and no credential on the real daemon can fix a client that never reaches it. The stdio relay is path-addressed and 0600. The root is now passed explicitly instead of being inherited from the host's working directory — a failed expansion becomes a loud refusal from the root guard, where a wrong cwd was a silent scan of the wrong tree. tests/plugin_manifest_smoke.rs pins both properties, including that no manifest hard-codes 51786 anywhere.

Added

  • [scan] max_candidates (default 500_000, 0 disables) aborts a scan from inside the walk once more than that many candidate files have been kept — before extraction, before any index write, and before the candidate list itself can grow to hundreds of megabytes. The error names the heaviest contributing directories by their first two path segments, so a vendored or generated tree that slipped past .gitignore is a one-line fix rather than an out-of-memory kill. (#62)
  • BASEMIND_ALLOW_ANY_ROOT=1 accepts any directory as a workspace root, for the cases the new allow-list refuses. It does not override the refusal of a filesystem or volume root, and it applies only to the invocation that sets it: the index a hatched scan produces does not make the root permanently acceptable, so unsetting the variable refuses it again. (#62)
  • [scan] max_candidates also bounds the walk itself, not only what it keeps: a walk that visits a generous multiple of that many filesystem entries while accepting almost none of them now aborts with its own error, because a root broad enough to enumerate tens of millions of entries burns exactly the time and syscalls the ceiling exists to prevent. (#62)
  • basemind now reads the memory limit it is running under, not just its own usage: cgroup v2 (memory.max, taking the minimum over the whole hierarchy, because systemd puts MemoryMax on a slice above the scope a process actually lives in), cgroup v1 (with its no-limit sentinel recognised as no limit rather than as an 8-exabyte ceiling), /proc/meminfo on a Linux box with no memory cgroup, hw.memsize on macOS, and GlobalMemoryStatusEx on Windows. Usage on Linux is a working set — reclaimable page cache is subtracted, as kubelet does — so having read a large repository no longer reads as being near the limit. A cgroup that sets no limit — the default for a plain docker run — falls back to total RAM rather than reporting no ceiling, and a cgroup limit larger than the machine is not treated as the binding constraint. Issue #62 was reported from a process running under a 2 GiB MemoryMax that had no way to know the limit existed. (#62)
  • [resources] max_footprint_mb accepts "off" and "auto" alongside an integer. (#62)
  • [resources] max_map_cache_mb (default 256, 0 = unbounded) bounds the MCP read stack's decoded-outline cache, per workspace. Charged in bytes, not entries: a decoded outline spans three orders of magnitude (a ~1.4 KB median against a measured 697 KB maximum), so an entry cap would bound the count and not the footprint. A miss costs one blob read and never changes an answer. The code-graph memo is charged at half the same budget, and a read-only session's projected call / implementation indexes are charged against it too. (#62)
  • [code_search] max_chunks_per_file (default 2000) caps how many chunks one source file may contribute to the search index, mirroring the document tier's max_chunks_per_document. A generated parser table or a checked-in bundle otherwise fans one file out into a chunk count bounded only by [scan] max_file_bytes, and every chunk costs a BM25 posting entry carrying its full term list, a LanceDB row, and — with embed on — an ONNX embed. An over-cap file is still chunked and cached (the .chunk.msgpack sidecar is written, so outline and grep are unaffected); what it loses is the per-chunk fan-out, with a warning naming the file. (#62)
  • BASEMIND_NO_AUTOSPAWN=1 makes basemind connect to a daemon that is already running but never start one, so an operator can guarantee the daemon exists only inside a resource-controlled unit. A working systemd user unit — with install steps, the environment.d wiring, and the commands that verify the ceiling is real rather than merely configured — ships at docs/systemd/basemind-comms.service. Two deliberate exceptions: basemind comms start ignores the variable (an operator who typed a start command has stated the intent it withholds from implicit callers), and an already-live daemon is still used — connect-only means do not start one, not do not use one. (#62)
  • A scan leaves post-mortem evidence a SIGKILL cannot erase. <workspace_cache_dir>/scan-inflight.json is written before the pass starts and removed on Drop, so a surviving record proves the process never ran its destructors — the signature of an OOM kill, which is exactly the case the existing last-fatal.json structurally cannot report, because the code that writes it never runs. The record carries the pid, the build, the phase, the candidate count, and the root (a root of / is itself the finding). basemind comms doctor lists every such record whose pid is dead — filesystem-only, no RPC, so it stays usable on a wedged machine — and --clear-fatal now acknowledges stale scan records alongside the fatal-store record. A periodic rss_mb / ceiling_mb log line makes the growth curve reconstructable after the fact. (#62)
  • BASEMIND_GIT_DELTA_CACHE_BYTES overrides the per-Repository delta-base (pack) cache bound; 0 disables the cache. gix's default when core.deltaBaseCacheLimit is unset is 96 MiB per thread-local Repository, i.e. per rayon worker during a git-history build — roughly 3 GiB of decoded delta bases on a 32-core host that nothing in basemind accounted for. basemind now sets the key to 8 MiB, as a CLI-precedence override so a repository-local core.deltaBaseCacheLimit cannot raise the bound we set for our own protection. Undersizing it costs re-decoding, never a wrong answer. (#62)
  • BASEMIND_ALLOW_HTTP gates the daemon's streamable-HTTP MCP front-end — see Security. (#62)

Changed

  • Breaking: a workspace root must now be a git repository or contain basemind.toml. basemind opens a root read-write and indexes everything beneath it, and root discovery falls back to the starting directory, so an MCP host launched at / could previously hand the daemon the whole filesystem — which is how a scan reached 43.8 GiB and was OOM-killed. basemind init and BASEMIND_ALLOW_ANY_ROOT are the escape hatches, and a root that already had an index on disk before the upgrade is grandfathered so upgrades do not break a working setup. The guard resolves the root — collapsing .., following symlinks — before deciding, and hands the resolved path to whatever opens the store, so a path such as /.. can no longer be checked as a subdirectory and opened as the filesystem root. It now also covers basemind admin rescan (and the admin MCP tool), which walks the whole working tree in-process, and the daemon's git-history sync. There are three refusals, and the error text names every way out of the one you hit: not a project (a plain directory — overridable), filesystem root (/, C:\, a UNC share root — never overridable, by any marker, hatch or pre-existing index), and unresolvable (relative, missing or unreadable, so the guard cannot decide about it at all). It is a misconfiguration guard, not an authorization boundary: it does not defend against a caller who controls the process's arguments or environment. (#62)

  • The grandfather clause reads an explicit record rather than inferring consent from a side effect. The guard writes root-admission.json into the root's workspace cache dir, stamped pre_existing_index (a standing admission that survives with no env var — the upgrade path) or env_hatch (never a standing admission). Without it, the mere existence of a working-view index was proof of admission, which made a single BASEMIND_ALLOW_ANY_ROOT=1 basemind scan a permanent whitelist for that root: the scan the hatch authorized left behind the very file the next hatch-free run would accept as evidence. The first verdict is the standing one and is never overwritten, and the write is best-effort — a failed write only degrades the guard to its index-means-admitted behaviour and never fails a scan. (#62)

  • The HTTP transport's ?root= now runs the same root discovery the CLI does, so naming a subdirectory of a repository attaches to that repository instead of being refused — the two front-ends previously disagreed about what a root is. (#62)

  • Excluded directories are now pruned during the walk instead of being filtered per file after the walker has already descended into them, so a non-gitignored node_modules is no longer traversed and stat'd in full. The indexed set is unchanged. This also fixes the Linux watcher, which previously registered inotify watches inside trees it was excluding. (#62)

  • Breaking: [scan] extra_roots now requires the operator to set BASEMIND_ALLOW_EXTRA_ROOTS=1 in the environment. That key names directories outside the repository but is read from the scanned repository's own basemind.toml, so cloning and indexing a repo was enough to make basemind walk whatever it named — ~/.ssh, ~/.aws — and surface the contents through the code grep / search tools. The process environment is the one input a cloned repository cannot write, so it is what the grant hangs on. Extra roots also now count toward [scan] max_candidates (they were appended after the ceiling was evaluated), are refused when they resolve to a filesystem or volume root, follow symlinks only when [scan] follow_symlinks is on (it was hard-coded on), and stop when a scan is cancelled. (#62)

  • The scanner bounds what it stages into the index by bytes rather than by file count. BM25 keyword postings — one entry per (term, chunk), staged on essentially every scan — previously counted toward no limit at all and could not force a commit; they now participate in the byte budget.

    This is the single change that most explains issue #62's number. The old bound counted files, and postings were staged without touching that counter, so the one structure that grows with the product of files and distinct terms was the one structure nothing bounded. The other fixes in this release matter, but they are smaller than their prominence suggests: reverting the chunked drive loop to its previous whole-corpus form, measured at both arms on a 4 000-file corpus, moves peak RSS by ~38 MiB — about 10 KiB per file — because a file's postings are already cleared inside the worker before its result is returned. Worth having at monorepo scale, but not the 43.8 GiB. Directory pruning is a walk-cost win rather than a memory one. Stated explicitly so nobody re-derives the priorities from scratch next time. (#62)

  • Breaking: [resources] max_footprint_mb = 0 now means auto, not disabled. An existing config that carries the old default — as every config generated before this release does — therefore gains a memory ceiling on upgrade rather than keeping none. Auto budgets 75% of an enforced cgroup limit or 50% of total machine RAM, whichever the platform reports, floored at 512 MiB. Write max_footprint_mb = "off" to restore the previous behaviour, or a positive integer to state a ceiling explicitly. The gate remains best-effort — it parks workers and then admits anyway after five seconds rather than failing a scan — so the change shapes peak memory and cannot make a scan that used to succeed start failing. A long-lived daemon shipping with its only memory bound disabled by default is what made 43.8 GiB reachable. (#62)

  • The footprint gate works on Linux and Windows, not only macOS. Its sampler was sysres::phys_footprint, a Darwin-only metric returning None everywhere else, so on the machine that filed #62 the ceiling could not have throttled anything even if it had been configured. (#62)

  • The MCP read stack no longer keeps every file's decoded outline resident for the process lifetime. The old MapCache was the only O(total symbols) structure with permanent lifetime — plus a second full copy of every import, plus a deep clone of the whole map on every watcher batch, times up to 16 hot workspaces in one daemon. It splits into a permanent O(files) symbol-free index view and a byte-charged read-through LRU governed by [resources] max_map_cache_mb, keyed by content hash so one cache is safely shared across snapshots and identical files share an entry. Building the view now decodes zero outline blobs — it projects the index and probes blob existence in parallel, one stat per file instead of a full read and decode. A read-only session whose projections are truncated by the budget reports it through the existing lifecycle notice with retry: false, because rerunning cannot widen a budget. (#62)

  • Per-keyspace Fjall memtable sizing replaces the 64 MiB default on all 17 keyspaces: 16 MiB for the BM25 posting list (the densest writer by far), 8 MiB for the secondary indexes the scanner rewrites once per file, less for the keyspaces a scan barely touches — all within fjall's recommended 8–64 MiB band. This is what INDEX_PARTITION_REVISION 4 → 5 exists to force: fjall persists max_memtable_size at keyspace create time and ignores the create options on reopen, so an index built by an earlier build would have kept the 64 MiB default forever. (#62)

  • The staged-byte counter now models fjall's real per-entry cost (64 + heap(key) + heap(value), where a payload of 20 bytes or fewer inlines and anything larger carries an 8-byte refcount header). Charging key.len() + value.len() undercounted what a WriteBatch actually holds by 2–3×, so both the per-worker batch budget and the process-wide staging ceiling meant something other than what they said. It remains a deliberate lower bound: allocator size-class rounding and Vec growth slack are not modelled. (#62)

  • The resolve pass no longer materialises the whole corpus. It previously built every file's resolved-reference edge set at once — one edge per resolved identifier use — held them through fact staging, then transformed rather than dropped them. The per-file edge set now cannot escape the file that produced it (there is no field to retain it in, so keeping the corpus set is a compile error), and the serial index writers gained the same byte bounds the parallel workers already had, closing the last staging path with no byte bound at all. (#62)

  • basemind scan / rescan stream each file's line as the scan absorbs it instead of accumulating one result per file for the whole repository just to print it afterwards. ScanReport now carries the summary statistics only; a caller that wants the old whole-corpus vector asks for it explicitly. (#62)

  • Breaking: git history is no longer indexed for a workspace root that is a subdirectory of the repository git discovery lands on. Those commits are addressed by paths that do not exist under the root, and walking them costs the whole ancestor object database — a 4.4k-file subdirectory of a monorepo was driving gix over 275 MiB of packs and 101 655 objects. Discovery itself is unchanged (ascent is what lets basemind scan src/ lift to the repository root); only the history integration is gated. git init in the directory, or BASEMIND_GIT_DISCOVER_PARENTS=1, restores the old behaviour. (#62)

  • The Claude and Cursor plugin manifests launch scripts/mcp-launch.sh serve --root <project dir> over stdio instead of dialling http://127.0.0.1:51786/mcp?root=… — see Security. (#62)

  • Dependency refresh: xberg repinned to the current 1.1.0 revision (internal extraction fixes and its own dependency updates), crawlberg 1.4.2 → 1.5.0, rmcp 3.1 → 3.2, tree-sitter-language-pack 1.15.12 → 1.16.1, liter-llm 1.18.4 → 1.19.1, smallvec 1.15 → 1.16, getrandom 0.3 → 0.4. Two incompatible upgrades were deliberately not taken: tree-sitter stays at 0.26 because tree-sitter-language-pack 1.16.1 still requires ^0.26 and tree-sitter declares links = "tree-sitter", so the graph admits exactly one version; and lancedb stays at 0.37 because 0.38.0 does not build without its remote feature — it gates the Error::Http variant behind that feature while referencing it unconditionally from job.rs, and enabling remote would pull an HTTP client stack in for no benefit here.

Fixed

  • A scan of a repository holding exactly [scan] max_candidates candidate files could abort spuriously: the ceiling was checked against every walk entry, so once the candidate list reached the cap the next directory node or non-indexed file tripped it. Whether it fired depended on readdir order. (#62)
  • The "largest contributors" list in a max_candidates abort routinely named the wrong directory. The tally stopped at the breach and ignore::Walk is depth-first in readdir order, so a repo with aaa/ (30 files) and zzz/ (5000) was told to exclude aaa. The walk now surveys a bounded number of further entries before erroring, buckets by the first two path segments so monorepos get an actionable name, and groups root-level files instead of listing them one by one. (#62)
  • A draining daemon can now interrupt a running file walk. Previously cancellation was only checked once the walk had finished, so a runaway scan could not be stopped. (#62)
  • [resources] max_concurrent_documents was parsed and never consumed — the setting did nothing. A counting semaphore now enforces it. It is distinct from the footprint gate, which reacts to memory already allocated: a document extraction's spike (decoded page buffers, OCR bitmaps, an embedding batch) lands faster than the sampler can see it, so this bounds how many spikes may overlap before the first one starts. (#62)
  • Staging an entry that failed returned early without running the commit check, so bytes the failed upsert had already staged drifted out of the ledger permanently. (#62)
  • Commit cadence no longer collapses at high core counts. The process-wide staging ceiling had no hysteresis, so cadence scaled as ceiling / workers — a commit per file on a many-core host. A 1 MiB floor on that branch bounds the worst case at about 128 MiB of resident staged index at 64 workers, and no core count now commits more often than the 8-worker cadence. (#62)

Known limitations

  • Two concurrent full-tree scans of one workspace share one scan-inflight.json. The second scan's record overwrites the first's, and the first scan's completion then removes the file while the second is still running — a window in which a genuine hard death would leave no breadcrumb. This is not reachable today (the daemon serialises a workspace's scans behind its store lock, and a serve boot's deferred and inline passes are sequential), and a per-pid filename would trade the window for records that accumulate and never get cleaned, so the single-file design stands. Recorded because the evidence is only trustworthy if its gaps are known. (#62)
  • A memory ceiling on the daemon can only be enforced by whoever starts it. basemind spawns the daemon detached with setsid(2), which changes the session and the process group and not the cgroup. On Linux a child stays in the spawning process's cgroup, so a daemon auto-spawned from an interactive shell lands in that shell's scope — outside whatever MemoryMax an operator configured on a basemind unit — and a process cannot move its own children into a cgroup it does not control. There is no in-process fix, and none is claimed. The answer is BASEMIND_NO_AUTOSPAWN=1 everywhere plus a unit that starts the daemon itself; see docs/systemd/basemind-comms.service, which includes the two commands that check whether the ceiling is real rather than merely configured. (#62)
  • [resources] max_footprint_mb is advisory, not structural. The gate parks workers at admit points while the process is over the ceiling and then admits anyway after five seconds, trading an overshoot for guaranteed forward progress. It shapes the peak; it cannot enforce an invariant the allocator will not, and it cannot make a scan that used to succeed start failing. The structural bounds this release adds are the ones that abort or truncate: [scan] max_candidates, [code_search] max_chunks_per_file, [resources] max_map_cache_mb, and the byte budget on staged index entries.
  • [scan] max_candidates does not bound bytes read or index size, and does not apply to the Staged / Rev scan sources, which enumerate from git rather than from a walk.
  • Directory pruning is a walk-cost win, not a memory win. Pruned files were already rejected by the path filter and never entered the candidate list. What pruning removes is the descent and the per-file stat — real, and the reason the Linux watcher stopped registering inotify watches inside excluded trees — but no claim is made that it reduced peak memory.
  • SCANNER_STACK_SIZE (256 MiB, src/scanner_file.rs) is not a leak and was deliberately left alone. It is the per-worker stack reservation for the scanner's thread pool: lazily-committed virtual address space that never counts toward RSS or a cgroup limit. Shrinking it to "save memory" would save nothing and would reintroduce stack-overflow aborts on deeply nested syntax trees.

[0.25.2] - 2026-08-31

Added

  • basemind comms doctor --probe asks each comms daemon whether it can actually serve and reports the verdict per row (serving / LIVE BUT FAILING / LIVE BUT NOT RESPONDING). The default report stays RPC-free, so it remains safe to run on a wedged machine, and now states which question it answered: process liveness from the pidfile registry, not health.
  • A daemon that hits an unrecoverable store error records it to <comms_dir>/last-fatal.json before exiting. comms doctor reports that record — the failing method, pid, build, and age — so the evidence outlives the process; --clear-fatal acknowledges it.

Changed

  • Dependency refresh: xberg repinned to the current 1.1.0 revision, crawlberg 1.4.0 → 1.4.2, fjall 3.1.9 → 3.1.10, jsonschema 0.51 → 0.52, lru 0.18.2 → 0.18.3, mach2 0.6 → 0.7, tree-sitter-language-pack 1.15.8 → 1.15.12, and liter-llm 1.18.1 → 1.18.4.

Fixed

  • A comms daemon whose store became permanently unusable held the singleton forever: it kept the flock and the socket, so its pid stayed live and no replacement could take over, while every request failed identically. It now releases the lock and exits non-zero on the first such error, so the next comms start gets a clean daemon (#61).
  • basemind comms stop hung indefinitely against a daemon wedged before its accept loop, because the connect never returned — the documented recovery path blocked on the very daemon it was meant to reclaim. Both stop paths now time-box the RPC and escalate to the pid. A daemon that answers with a deliberate refusal (daemon_busy, protecting the clients attached to it) is reported and left running, never terminated.
  • comms status reported threads: 0 when the store could not be read, which is indistinguishable from a healthy empty broker. It now surfaces the store error instead.

[0.25.1] - 2026-08-26

Changed

  • Dependency refresh: xberg repinned to the current 1.1.0 revision (EPUB chapter/metadata loss, PDF reading-order and CCITT image decoding, and XML/MathML hardening fixes on the document extraction path), crawlberg 1.3.3 → 1.4.0, gix 0.87.0 → 0.87.1, jsonschema 0.50 → 0.51, and tree-sitter-language-pack 1.15.7 → 1.15.8.

Fixed

  • The Linux file watcher no longer aborts when a directory cannot be read (permission denied). inotify watches are now registered directory-by-directory, pruned by the same gitignore + exclude-glob filters a full scan uses, with registration failures downgraded to warnings. Directories created after startup are re-armed on create events, and the per-directory scheme is gated to Linux (macOS/Windows keep the single recursive call to avoid an FSEvents startup regression).

[0.25.0] - 2026-08-23

Added

  • MCP stdio sessions now reconnect through a persistent protocol-aware relay when the shared daemon restarts, replaying initialization and returning retryable errors for interrupted requests.
  • Agent comms now auto-register presence, deliver mailbox notices alongside ordinary MCP responses, notify scoped agents about discoverable threads, accept unambiguous short message references, and expose lifecycle status plus dry-run/apply retention cleanup.
  • Document extraction uses xberg 1.1 chunk content, offsets, language metadata, bounded extraction time/page limits, and basemind's shared batch embedding path. Binary builds pin the unreleased xberg source to an exact public Git revision for reproducibility.

Changed

  • Cache schema advances to 25. Existing machine-global index, blob, and comms cache state is rebuilt on the next scan after upgrading.
  • Cache GC repairs corrupt or old rebuildable views instead of aborting the global sweep, enforces the disk budget against rebuildable workspace data, and records degraded GC health.
  • Comms retention now bounds messages, archived threads, inactive agents, claims, and total threads; stale-agent cleanup removes routing state atomically while preserving authored history.
  • Heavy MCP work is admitted through a daemon-wide concurrency limit and returns a retryable server_busy response under sustained load.

Fixed

  • Inbox cursors paginate independently per thread without repeats or undercounted unread totals.
  • Message reads and acknowledgements enforce thread membership.
  • Cancelled inbox waits release their subscriptions immediately, and default MCP identities no longer collide across concurrent sessions in one workspace.
  • Standalone scan and MCP processes no longer hang during shutdown when Fjall's background worker teardown encounters the upstream database-drop deadlock.

[0.24.0] - 2026-08-07

Added

  • Interactive UI over HTTP + the ui tool (ADR-0006). A new ui MCP tool (and query ui CLI, in full parity) renders the code-graph and returns a URL to view and drive it: a live http://<addr>/ui?root=… page served by the basemind daemon's HTTP front-end when one is reachable, otherwise a file:// URL to the self-contained offline export (which the tool always writes). The served page is the same zero-dependency interactive canvas as display/graph_export html, so a browser — or an agent driving one — can navigate, search, pan/zoom, and screenshot it. open: false returns the URL without launching a viewer (agents/tests). The daemon's streamable-HTTP front-end gained a GET /ui route alongside POST /mcp (both behind the comms feature). Both routes are loopback-only: on a loopback bind the front-end rejects any request whose Host/:authority is not a loopback name (DNS-rebinding protection), skipped only on an explicit non-loopback bind.
  • Desktop UI launch path (ADR-0006, first slice). A basemind ui subcommand launches the desktop UI front-end (basemind-ui) by re-exec'ing the sibling binary built alongside basemind — mirroring how basemind agent launches basemind-tui. Both launchers are behind the desktop-ui and agent-tui features and ship in no release archive yet (see Changed); build them locally to use them. This slice wires the launch path; today the binary opens the offline, self-contained interactive HTML code graph (the same ADR-0005 payload the display tool produces). The resident Tauri window that drives the agent client/event/command seam lands behind a desktop feature in a later slice, so default builds stay free of the per-platform webview dependency.
  • Inline rationale extraction (ADR-0009). L1 extraction now classifies source comments carrying WHY: / RATIONALE: / NOTE: / TODO: / FIXME: / HACK: / XXX: / SAFETY: markers (and bare comments citing ADR/RFC decision records) into FileMapL1.rationale, which the read-side graph build consumes to emit Annotates / Cites edges. Blob-compatible (#[serde(default)]); no schema bump.
  • Single-daemon guarantees + leak safeguards. The comms daemon now takes an exclusive single-owner lock (daemon.lock) before binding its socket, so a redundant daemon on the same comms dir converges (exits 0) instead of racing — uniform across Unix and Windows. Every live daemon registers a pidfile under <data_home>/daemons/, and a new machine-wide ceiling (BASEMIND_MAX_DAEMONS, default 8) refuses to spawn past it rather than letting daemons pile up. New basemind comms doctor lists every live daemon (pid / comms dir / version / uptime, pruning dead entries) and flags a pile-up; basemind comms stop --all reclaims them all in one call.

Changed

  • Cache schema advances to 24. Existing machine-global index, blob, and comms cache state is rebuilt on the next scan after upgrading.

  • Release dependencies are current. tree-sitter-language-pack is updated to 1.14.3, crawlberg to 1.1.4, and liter-llm to 1.16.0. The optional LSP bincode integration is pinned to 2.0.1 because the published 3.0.0 crate is an intentionally non-compiling placeholder.

  • Agent shell sessions now default to headless presentation. Background MCP/CLI work no longer opens terminal tabs unless [shells].visual explicitly selects current or window.

  • The two interactive front-ends are held back from releases. The agent TUI (basemind-tui, ratatui) and the desktop UI (basemind-ui, tauri) are not ready to ship, so the release archive now contains the basemind binary alone, and the launcher subcommands that would re-exec them sit behind two new root-crate features, agent-tui and desktop-ui, which full deliberately omits. A released basemind therefore never advertises a command whose sibling binary is absent from the archive. Neither crate is affected otherwise: both remain workspace members, both still build under cargo build --workspace, the basemind-tui replay/PTY suite still runs in CI, and a local cargo install --path . --features full,agent-tui,desktop-ui still gets both launchers. Nothing was published from these crates before — both carry publish = false — so no crates.io surface changes.

  • Daemons self-reap sooner to stop the process/memory leak class. The idle-reap default drops from 30 min to 10 min, and a new bootstrap reaper self-terminates a daemon that no client ever connects to within ~2 min (env BASEMIND_COMMS_BOOTSTRAP_SECS) — catching the spawn-and-abandon shape (a test or script that ran daemon ensure then exited) that previously left a daemon, and its tens of threads, resident. Behavioural only; no schema/RELEASE_MINOR bump. Test binaries pin a fast reap so a parallel cargo test no longer accumulates one lingering daemon per binary.

  • BREAKING: basemind-agent's LLM-facing tool names follow the domain consolidation. The agent registered its code-map tools under the pre-consolidation flat names, so the model saw one vocabulary and the MCP surface another. They are now domain_mode in bare snake_case: outline → code_outline, search_symbols → code_symbols, find_references → code_references, find_callers → code_callers, workspace_grep → code_grep, call_graph → graph_calls, recent_changes → git_recent, blame_symbol → git_blame_symbol, diff_file → git_diff. shell_exec is unchanged — it is the agent's own tool, not an MCP domain. The underscore rather than the MCP surface's colon is a provider constraint, not a style choice: Anthropic, OpenAI and Google all validate tool names against ^[a-zA-Z0-9_-]{1,128}$, so code:outline is not a legal name to register. Only saved conversations that replay a raw tool name are affected. The token-savings estimator now rewrites the agent spelling to the MCP telemetry key at the boundary, keyed off the real mode vocabulary in src/mcp/mode.rs, so each baseline carries a single spelling — previously it carried both, and dropping either would have silently zeroed the agent TUI's "tokens saved" readout instead of failing a test.

  • call_graph is re-expressed over the shared codegraph. The tool no longer maintains its own call scan; it projects the resolved Calls edges of the shared, memoized graph the other graph tools already read. One user-visible consequence: a call whose callee does not resolve to an in-repo function-like definition (an external/library call) no longer surfaces as an empty-sites callee node — call_graph now reports only resolved, in-repo call relationships. The callers direction is unchanged. Read-side only; no blob/index schema change.

  • BREAKING: the 85-tool MCP surface collapses into nine domain tools, each dispatched by a required mode. Hosts defer MCP tools and surface them only through keyword search, so every extra tool name competed for the same query, and GH #50 showed one layer's schema defect took the whole registry down with it — the comms layer's schema broke code navigation, despite the two sharing nothing. Nine dense tools (code, graph, git, memory, admin, web, agents, workspace, shell) retrieve better than eighty-five thin ones, and each remaining description now carries the vocabulary an agent actually searches for. mode is required and never defaults — a default would let an agent omit it and silently get the wrong operation, which looks like an empty result rather than an error. The CLI collapses onto the same nine group names as real clap subcommands (basemind code outline, not a --mode flag), so every operation keeps its own --help and argument validation; tests/cli_parity.rs now asserts a strict (domain, mode) bijection between the two surfaces. All 85 operations survive with no deprecated shims — a name that still answered to the old spelling would keep competing in deferred-tool search, which is the cost this removes. Landed over four commits (88cd9da web, 6af6268 admin/memory/workspace, 36919a4 git/graph, 02bf4a5 code/agents/shell); design and trade-offs in ADR-0011. Two costs taken deliberately: no output_schema on any of the nine tools (each domain's modes return different response shapes, and SEP-2106 allows exactly one schema per tool), and per-tool annotations coarsen to the union of a domain's modes, resolving toward the side effect (e.g. shell advertises destructive_hint because kill is one of its modes, and graph advertises read_only_hint: false because display/open launch a viewer, even though seven of its nine modes are pure reads).

    Anyone with a saved prompt, script, or pinned tool name against the old surface needs the replacement below — <old tool> is now <domain> mode <mode>, and every CLI invocation moves from its old subcommand path to the new one. basemind registry is also renamed basemind workspace (its subcommands are unchanged), and basemind comms now holds only the broker daemon's own lifecycle (daemon / start / stop / status / doctor) — the agent-facing verbs it used to carry (register, agents, thread-*, join, leave, members, post, history, read, inbox, wait, plus a new ack) moved to basemind agents.

    Old MCP toolNew domain modeOld CLINew CLI
    outlinecode outlinequery outline <path> [--l2]code outline <path> [--l2]
    search_symbolscode symbolsquery search <needle> (alias query symbol)code symbols <needle>
    workspace_grepcode grepquery grep <pattern>code grep <pattern>
    list_filescode filesquery list-filescode files
    find_filescode findquery find-files <query>code find <query>
    goto_definitioncode definitionquery goto-definition <path> <line>code definition <path> <line>
    find_referencescode referencesquery references <name>code references <name>
    find_callerscode callersquery callers <path> <name>code callers <path> <name>
    find_implementationscode implementationsquery implementations <trait>code implementations <trait>
    dependentscode dependentsquery dependents <module>code dependents <module>
    expandcode expandquery expand <path> <name>code expand <path> <name>
    search_codecode semanticquery search-code <query>code semantic <query>
    get_chunkcode chunkquery get-chunk <path>code chunk <path>
    call_graphgraph callsquery call-graph <name>graph calls <name>
    neighborsgraph neighborsquery neighbors <name>graph neighbors <name>
    pathgraph pathquery path <from> <to>graph path <from> <to>
    subgraphgraph subgraphquery subgraph <name>graph subgraph <name>
    communitiesgraph communitiesquery communitiesgraph communities
    architecture_mapgraph mapquery architecture-mapgraph map
    graph_exportgraph exportquery graph-exportgraph export
    displaygraph displayquery displaygraph display
    uigraph openquery uigraph open
    working_tree_statusgit statusgit working-tree-statusgit status
    recent_changesgit recentgit recent-changesgit recent
    commits_touchinggit touchinggit commits-touching <path>git touching <path>
    find_commits_by_pathgit by_pathgit find-commits-by-path <pattern>git by-path <pattern>
    hot_filesgit churngit hot-filesgit churn
    diff_filegit diffgit diff-file <path> <old> <new>git diff <path> <old> <new>
    diff_outlinegit diff_outlinegit diff-outline <path>git diff-outline <path>
    blame_filegit blamegit blame-file <path>git blame <path>
    blame_symbolgit blame_symbolgit blame-symbol <path> <name>git blame-symbol <path> <name>
    symbol_historygit symbol_historygit symbol-history <path> <name>git symbol-history <path> <name>
    search_git_historygit searchgit search <pattern>git search <pattern>
    memory_putmemory putmemory put <key> <value>memory put <key> <value>
    memory_getmemory getmemory get <key>memory get <key>
    memory_listmemory listmemory listmemory list
    memory_searchmemory searchmemory search <query>memory search <query>
    memory_deletememory deletememory delete <key>memory delete <key>
    memory_auditmemory auditgovernance auditmemory audit
    search_documentsmemory documentsmemory search-documents <query>memory documents <query>
    proposals_minememory minegovernance minememory mine
    proposals_listmemory proposalsgovernance proposalsmemory proposals
    proposal_acceptmemory acceptgovernance accept <id>memory accept <id>
    proposal_rejectmemory rejectgovernance reject <id>memory reject <id>
    statusadmin statusquery statusadmin status
    repo_infoadmin repoquery repo-infoadmin repo
    rescanadmin rescanbasemind rescan [paths]admin rescan [paths]
    cache_statsadmin cache_statscache statsadmin cache-stats
    cache_gcadmin gccache gcadmin gc
    cache_clearadmin cache_clearcache clear --component <c>admin cache-clear --component <c>
    telemetry_summaryadmin telemetrybasemind telemetryadmin telemetry
    compressadmin compressbasemind compress-outputadmin compress
    deltaadmin deltabasemind deltaadmin delta
    checkpointadmin checkpointbasemind checkpointadmin checkpoint
    detect_wasteadmin wastebasemind detect-wasteadmin waste
    web_scrapeweb scrapeweb scrape <url>web scrape <url>
    web_crawlweb crawlweb crawl <seed-url>web crawl <seed-url>
    web_mapweb mapweb map <url>web map <url>
    agent_registeragents registercomms register --name <handle>agents register --name <handle>
    agent_listagents listcomms agents [--thread]agents list [--thread]
    thread_startagents thread_startcomms thread-startagents thread-start
    thread_listagents thread_listcomms threads [--subject-contains]agents thread-list [--subject-contains]
    thread_joinagents joincomms join <thread>agents join <thread>
    thread_leaveagents leavecomms leave <thread>agents leave <thread>
    thread_membersagents memberscomms members <thread>agents members <thread>
    thread_add_memberagents add_membercomms add-member <thread> <id>agents add-member <thread> <id>
    thread_remove_memberagents remove_membercomms remove-member <thread> <id>agents remove-member <thread> <id>
    thread_archiveagents archivecomms archive <thread>agents archive <thread>
    thread_postagents postcomms post <thread> <subject>agents post <thread> <subject>
    thread_historyagents historycomms history <thread>agents history <thread>
    message_getagents messagecomms read <id>agents message <id>
    inbox_readagents inboxcomms inbox [--mark-read]agents inbox [--mark-read]
    inbox_ackagents ack(new — no prior tool)agents ack
    inbox_waitagents waitcomms wait [--thread --timeout-secs]agents wait [--thread --timeout-secs]
    workspacesworkspace workspacesregistry workspacesworkspace workspaces
    worktreesworkspace worktreesregistry worktreesworkspace worktrees
    branchesworkspace branchesregistry branchesworkspace branches
    worktree_claimworkspace claimregistry claimworkspace claim
    worktree_releaseworkspace releaseregistry releaseworkspace release
    shell_spawnshell spawnshells spawn <command>shell spawn <command>
    shell_sendshell sendshells send <session-id> <text>shell send <session-id> <text>
    shell_captureshell captureshells capture <session-id>shell capture <session-id>
    shell_killshell killshells kill <session-id>shell kill <session-id>
    shell_listshell listshells listshell list
    shell_broadcastshell broadcastshells broadcast <text> --session <id>…shell broadcast <text> --session <id>…

Fixed

  • Recycling the shared daemon no longer disconnects other agents. basemind comms stop now refuses with daemon_busy while another live relay is connected, instead of entering a bounded drain that severed every stdio MCP transport after ten seconds and left hosts permanently showing Transport closed. Once the other sessions exit, the same stop request succeeds normally.
  • EndNote XML is rejected until xberg ships its quick-xml fix. The document scanner now blocks .enw / application/x-endnote+xml before xberg 1.0.14 can route untrusted input through biblib 0.4.3's vulnerable quick-xml 0.37 attribute parser (RUSTSEC-2026-0194). The narrow guard is tracked for removal in xberg #1398.
  • Resolved imported calls could remain inferred in code graphs. The reference index now prefers a terminal cross-file definition over the local import binding, and code-graph proof matches the resolver's identifier byte within the outline symbol span. Python callers and goto-definition now agree on the exported target.
  • Cache budget enforcement could remain over its configured limit. Eviction now prefers cold workspaces but falls back to hot ones when required, reclaims newly orphaned blobs in the same pass, and remeasures the actual cache footprint before selecting another workspace.
  • Starved or failed daemon GC cycles were visible only in logs. cache_stats.last_gc now records the latest attempt status, timestamp, actionable diagnosis, consecutive degraded-cycle count, and budget outcome while preserving counters from the last successful sweep.
  • Content-addressed blobs no longer consume raw msgpack size on disk (issue #49). New .fm, .doc, .rref, and .chunk blobs use zstd-1 envelopes. Filemap L1/L2 frames remain independent so outline reads do not decompress calls, and chunk embedding metadata stays plain for cheap cache validation. Existing uncompressed blobs remain readable and are rewritten when next produced.
  • Agent shell capture could return an empty screen for a successful command. The snapshot-only path dropped the first row for fresh CLI clients and let rmux's dead-pane row push short output out of lines=N captures. Capture now reads retained history, removes terminal synthetic and outer blank rows while preserving interior spacing, and returns a bounded recent tail, so a completed one-line command remains observable until kill.
  • Shared shell sessions could start in another workspace. Relative or omitted cwd values were interpreted from the machine-global rmux daemon's original process directory. Spawn now anchors every command to the calling workspace root, and accepts cwd = "." as that root.
  • A transient pane inspection could make shell list fail globally. The daemon now preserves the known session list and conservatively reports liveness when rmux cannot provide one pane-state snapshot, rather than making every session undiscoverable. Its idle reaper uses strict liveness separately, so persistent inspection failure cannot pin the daemon forever.
  • A resident daemon running different code at the same version was invisible. After a local rebuild or cargo install --path ., the version check compares only MAJOR.MINOR.PATCH, so the daemon left over from the previous build passes every test and is reused — answering with stale code while the newly installed binary sits unused. basemind comms status now reports a build= identity (a hash of the running executable, so two builds differ exactly when their bytes do) and warns explicitly when the daemon's build differs from the binary asking. The build id is diagnostic only: it deliberately does not trigger takeover, because two same-version binaries at different paths would then reclaim the socket from each other indefinitely.
  • admin rescan reported a refreshed document in no bucket at all. A rescan that re-indexed a changed PDF/Office/HTML file returned scanned: 3, skipped_unchanged: 2, updated: 0 — the document lane's work was counted by the scanner but dropped from the response, which read as "nothing happened" for documents users. The response now carries docs_indexed. Indexed data was never stale; only the accounting was.
  • agents post accepted an empty body and stored a message with nothing to read. An empty string is now refused with an error naming the mode and pointing at the subject-only form; omitting body entirely stays legal.
  • The machine registry never retired a row. Workspaces were recorded on registration and nothing ever removed them, so every throwaway checkout and test tempdir accumulated forever and buried the live repos workspace workspaces exists to surface. The daemon now prunes rows whose path is gone, once at startup and on its hourly maintenance pass.

[0.23.1] — 2026-07-31

Fixed

  • code semantic silently returned nothing on a reader session. Both non-vector retrieval lanes — BM25 keyword and exact symbol-name — read fjall, whose directory lock is exclusive, so any session that is not the daemon opens the store without an index and both lanes returned an empty vector. That empty was indistinguishable from "searched and found nothing": the call succeeded, in microseconds, with hits: []. A reader session now forwards the two lanes to the daemon (rankings only — chunk bodies still come from content-addressed blobs the caller can already read), and a daemon-hosted session reaches the same index straight through the host seam rather than dialling the daemon it runs inside. When a lane genuinely cannot run, the response carries degraded_lanes + degraded_reason and partial results are labelled as partial; when no lane can run at all the call is an error that says so, never a bare []. Keyword-lane hits also carry their matched_lanes / keyword_rank provenance, which only the fused path used to set.
  • The multi-agent-room skill now ships to every plugin tree. scripts/sync-plugin-skills.sh carried a hand-maintained list of nine skills while skills/ held ten, so multi-agent-room was copied into the Hermes bundle (generated separately by ai-rulez) but never into the Codex, Cursor or opencode plugins — while the agent-comms rule those plugins do ship tells the agent to consult it. The skill and command lists are now derived from the canonical skills/ and commands/ trees, so adding either ships it everywhere instead of only where someone remembered.
  • Documentation named tools and commands that no longer exist. The consolidation swept the README, llms.txt, docs/, skills/ and the plugin manifests but missed the website/ docs site, the pre-tool-guard hook (which suggested search_symbols / find_references / list_files on every Grep and Glob), and basemind git blame-file in the README quickstart. Every basemind <domain> <mode> command now appearing in the docs or plugin trees is verified to resolve against the built binary.
  • Runaway daemon spawning under cargo test (fork bomb). spawn_detached_daemon re-exec'd std::env::current_exe() with comms daemon appended. Under a test harness that executable is libtest, which reads the appended words as a test-name filter and re-runs the whole suite — and every test in that re-run reaching the spawn forked another generation. A single run reached 5610 orphaned daemons and 8256 processes, at which point fork itself began failing. The machine-wide ceiling could not contain it because init_isolated_cache minted a fresh $BASEMIND_DATA_HOME in every process, so each generation counted an empty registry. Now the spawn resolves a real basemind binary or refuses: a harness is redirected to the sibling target/<profile>/basemind, and BASEMIND_DAEMON_BINARY is the explicit override. The isolated test cache is stamped with a marker and re-adopted by child processes, so one registry — and one ceiling — covers the whole process tree, while an unmarked $BASEMIND_DATA_HOME (a developer's real cache) is never adopted.
  • Blank statusline in global-cache mode. With the index living in the machine-global cache there is no in-repo .basemind/ for the shell statusline to read, so it fell back to a permanent "no index" hint even on a freshly-scanned repo. basemind statusline gains a --root <path> mode that renders the rich per-repo line (file count · scan age · today's calls · tokens saved) from a cheap new status.json sidecar plus the telemetry.jsonl tail — never opening the Fjall index, so it is safe to refresh every few seconds. The sidecar is written after every working-view scan/rescan and is additive/best-effort (a missing or unrecognized-schema sidecar degrades to the "no index" hint). No blob/index schema change.

[0.23.0] — 2026-07-31

Minor release: .basemind/ is wiped and rebuilt on the next basemind scan (RELEASE_MINOR 22 → 23). Rebuild is automatic from source; no action needed.

Added

  • Streamable-HTTP MCP transport (rmcp 3.0, SEP-2567), hosted by the comms daemon. The single machine-global daemon now serves the rmcp router over a stateless loopback-HTTP front-end (127.0.0.1:51786 by default, BASEMIND_HTTP_ADDR to override; the bound address is published to <comms_dir>/http.addr) in addition to its Unix-socket relay. HTTP-native clients dial http://127.0.0.1:<port>/mcp?root=<repo> directly — no per-session server process. basemind daemon ensure brings the daemon + transport up and prints the URL.
  • Structured tool output (SEP-2106). Every tool advertises an outputSchema and returns structuredContent alongside the text mirror, so compliant clients get typed results.
  • tools/list / prompts/list cache hints (SEP-2549) — the tool surface is static per process, so the daemon advertises a TTL + public cache scope and clients stop re-listing every session.
  • Long-running tools as pollable tasks (SEP-2663). Slow tools (rescan, and the feature-gated search_documents / web_scrape / web_crawl / web_map) run as cancellable tasks when the client declares the Tasks capability; other clients keep the synchronous path.

Changed

  • rmcp 2.2 → 3.0, plus arrow, lancedb, xberg, crawlberg, and jsonschema dependency bumps.
  • basemind serve is now a thin stdio↔daemon relay, not a server in its own right. It ensures the daemon (the sole rmcp host and Fjall writer) and byte-pumps stdin/stdout to the daemon's relay socket. Every stdio MCP client keeps working with no config change; the daemon outlives any single client. The Claude Code and Cursor plugins switch to the HTTP transport (type: http) and dial the daemon directly.

Fixed

  • The MCP transport "drops and never returns" hang. serve previously fell back to an in-process stdio server whose lifetime was bound to the stdin pipe, so a slow tool call that timed out and closed stdin killed the whole server — and it did not come back until the client respawned it. The daemon-hosted model decouples server lifetime from any single connection: a dropped client ends only the throwaway relay (or a single stateless HTTP request), and the warm daemon serves the next connection immediately.
  • Document re-embed loop follow-ups (issue #44). Three residual thrash paths on the document tier are closed: (1) a hosted rescan now honors the daemon drain token, so comms stop / SIGTERM interrupts a mid-scan instead of letting it run to completion; (2) a reused document blob has its mtime refreshed on every scan, so the blob GC's grace window keeps protecting an actively-scanned doc through a rename's transient entry-less gap instead of reaping it and forcing a full re-embed; (3) a doc whose embedding deterministically yields no vectors (an unembeddable body, a missing ONNX runtime) is now recorded as attempted and no longer re-extracted + re-embedded on every scan — a content-hash or embedding-preset change re-opens the attempt.

[0.22.8] — 2026-07-27

Changed

  • basemind init no longer writes a committed CLAUDE.md / AGENTS.md unasked. The rules block is a shared, version-controlled surface, so onboarding now defaults to the personal, gitignored sibling instead: --rules-target auto (the default) routes to ai-rulez when it owns governance, otherwise to CLAUDE.local.md (or AGENTS.local.md when the repo uses AGENTS) — never a committed file. The /bm-init slash command asks the user where the block should go, and the interactive CLI prompts for it; --rules-target gains claude-local / agents-local values, and claude / agents remain as the explicit opt-in to the committed files.

Fixed

  • Comms daemon readiness flake (spawned comms daemon did not become ready within the timeout). The Unix comms front-end peeked a freshly-accepted connection's first byte inline in the accept loop (peek_first_byte(&stream).await before the spawn), so a single client that connected but was slow or silent to send its first byte parked the entire accept loop on readable(). Every other client then sat unaccepted in the kernel backlog — including ensure_daemon's readiness probe, whose short timeout reported a perfectly healthy, still-running daemon as unreachable, which in turn spawned a replacement that hit AddrInUse and exited (SpawnTimeout). The peek + relay/ legacy routing now runs inside the per-connection spawned task, so the accept loop only accept()s and spawns and never awaits client I/O — a slow/silent client harms only its own task. This aligns the Unix path with the Windows named-pipe front-end, which already reads the first byte in-task. No blob/index schema change.

[0.22.7] — 2026-07-25

Changed

  • basemind serve is now a thin relay to the singleton daemon; the daemon hosts the MCP router. Every client's serve used to open its own read-only store, build its own in-RAM code map, and lazily load its own ONNX + LanceDB (~300–370 MB RSS per process). On a comms build serving the working view, serve now dials the daemon, runs a tiny handshake, and pumps stdio bytes to it — the daemon hosts the rmcp router over one shared read stack per workspace (SharedReadStack), so the in-RAM map, ONNX model, LanceDB, and git caches are resident once per workspace instead of once per client. ServerState was split into a per-connection identity (agent_id, comms_clients, log_level) plus that shared stack, so distinct clients keep distinct memory-owner identities while sharing the heavy read state. Fallback is total: any handshake problem — no daemon, an older daemon (which rejects the RELAY_MAGIC preamble as an over-long frame), a proto/view mismatch, or BASEMIND_MCP_LEAN / a non-working view / BASEMIND_SERVE_INPROCESS — serves in-process exactly as before, so a client is never bricked. No blob/index schema change.
  • Daemon-hosted git-history now runs in-process instead of the daemon dialing itself. A hosted connection's history reads and its startup sync route through a new in-process seam (HistoryHost, implemented by the daemon Broker) that shares the Broker's single open git-history.fjall/ handle and per-repo build lock — so recent_changes / commits_touching / blame_* / search_git_history no longer pay a unix-socket round trip per call on the hosted path, while the single-holder + single-flight-build invariants are unchanged. A thin daemon_writer serve (the fallback) still forwards over the socket.
  • The serve relay now works on Windows named pipes. Named pipes have no non-destructive peek (the Unix path uses MSG_PEEK), so the accept loop consumes the connection's first byte and replays it — to the relay handshake via a small PrefixReader, or to a legacy comms link via a seeded frame buffer — routing rmcp relay connections apart from comms links without a fallback to in-process serve.

Fixed

  • Daemon-hosted precise resolved_refs (and search_code) no longer degrade. Hosted connections read the daemon's read-write fjall index directly in-process via a host seam, so cross-file find_callers / goto_definition see the refs_by_def edges an index-less thin serve could not — without the daemon dialing itself over its own socket.

Removed

  • Deleted the orphaned monitors/monitors.json + scripts/comms-monitor.sh poll-loop scripts (referenced by no manifest; the daemon's SubscribeInbox push channel supersedes them).

[0.22.6] — 2026-07-25

Fixed

  • The older-build daemon takeover now actually fires (#44). singleton::roundtrip decoded a bare CommsResponse, but the daemon frames every reply as a CommsOut::Response envelope, so the decode always failed and daemon_status always returned None. ensure_daemon never entered its version gate and reused whatever daemon held the socket — so a live pre-0.22.5 runaway daemon survived the client upgrade instead of being reaped (it had to be killed by hand). The probe now decodes the CommsOut envelope; the 0.22.5 cache/GC remediation actually reaches machines with a lingering older daemon. Regression present since the takeover was introduced.

Changed

  • Refreshed the dependency lockfile (auto_enums, derive_utils). cargo upgrade --incompatible is a no-op: every incompatible upgrade is a deliberately pinned crate (arrow-* =58, oxc_*, tree-sitter-python).

[0.22.5] — 2026-07-25

Hardening release after a starved-GC incident: a pre-0.22.4 daemon stuck in the #44 re-embed loop held the blob-GC read lock indefinitely, which silently stopped all daemon maintenance and let the machine-global cache grow to 116 GB with no signal anywhere. Updating also remediates lingering pre-fix daemons: a 0.22.5 client stops any older-build daemon on first contact (existing older-build takeover) instead of leaving it to run away.

Added

  • Cache size budget with cold-workspace eviction. BASEMIND_CACHE_BUDGET_MB (default 20 GiB; 0 = unlimited) bounds the machine-global cache. When workspaces + blobs exceed it, the GC sweep evicts the coldest workspace dirs (idle ≥ 24 h, never ones another process holds locked) and reclaims the blobs they pinned — the backstop the reference-counting GC lacked, since live-but-idle workspaces pin their blobs forever.
  • Persisted GC observability. Every destructive sweep records its outcome (timestamp, blobs removed, bytes freed, workspaces reaped/evicted) to gc-state.json in the cache root, and cache_stats surfaces it as last_gc — a GC that stops running is now visible instead of a silent disk leak.

Fixed

  • Blob GC runs at daemon startup and on its own task (#44). It previously ran only as the last step of the hourly prune loop: a daemon restarting more often than hourly never swept once, and a single starved GC await stopped message pruning, thread archiving, and workspace eviction for the life of the process.
  • The GC's lock wait is bounded (#44). Waiting for the blob-GC write lock now times out after 5 minutes (logged, retried next cycle) instead of parking forever behind a runaway rescan's read guard — which, the lock being FIFO, also queued every subsequent rescan behind the stuck writer and wedged the daemon machine-wide.
  • A draining daemon refuses new rescans (rescan_draining) instead of running doomed partial passes, and a rescan that queued behind a drain-cancelled pass fails fast instead of re-walking the tree — cancelled passes never advance the coalescing generation, so each one previously re-walked the whole monorepo.

Changed

  • Upgraded gix to 0.86 and xberg to 1.0.0-rc.37; arrow-array/arrow-schema are pinned to =58 (required by LanceDB 0.31).

[0.22.4] — 2026-07-23

Fixed

  • The daemon no longer loops full document re-embed passes on churny trees (#44 — ~700 % CPU for hours, RSS to 5.4 GB on a multi-session pnpm monorepo). Root cause: Store::write_doc went through the schema-only skip in write_blob, so when the serve boot's Deferred pass had written a doc blob vectorless (embedding_dim: 0), the follow-up Inline pass re-extracted and re-embedded the document — and then silently threw the embedded result away. The blob stayed vectorless forever, and every entry-less encounter of the same content (NoCache rename remove+create pairs from #43, git worktree churn, fresh worktree-rooted workspaces) re-ran ONNX embedding from scratch. Doc blobs now overwrite on upgrade (the same fix the code-chunk tier already had), and DocEntry records embed state so already-poisoned caches heal with exactly one persisted re-embed pass on the next scan — no wipe needed. The same skip also meant search_documents vectors never landed in LanceDB, so document vector search now actually works for repo docs.
  • basemind comms stop and SIGTERM now interrupt a mid-scan daemon (#44). The drain trips a cooperative per-file cancellation token threaded through the scanner, and the runtime teardown is bounded at 15 s instead of waiting forever on an in-flight rescan — previously only SIGKILL could end a scanning daemon, which fed the respawn loop. A cancelled pass commits completed files (including their LanceDB rows), never runs the stale purge, and surfaces as rescan_cancelled rather than a completed rescan.
  • Queued identical full rescans coalesce. N sessions forwarding full rescans of the same workspace serialized on the store lock and each re-walked the whole tree; a request that waited behind an identical-or-stronger full scan is now served that scan's stats.
  • Unreferenced blobs younger than 6 h survive the daemon's hourly GC sweep, protecting the Deferred→Inline and rename windows where content-addressed doc blobs are legitimately entry-less.

Changed

  • Dependencies: tree-sitter-language-pack 1.13.2 → 1.13.3, xberg rc.30 → rc.33, lockfile refresh (arrow stays on 58.x — lancedb 0.31 pins arrow 58).

Added

  • basemind agent — a terminal coding agent over the code map. A model-agnostic (bring-your-own-key) ratatui TUI that runs a turn-loop against basemind's in-process code-map and git tools. Launch it with basemind agent [prompt] (forwards --continue / --resume / the positional prompt); the basemind-tui binary now ships alongside basemind in every release archive, and basemind agent re-execs it.
  • Multi-agent room. Agents sharing a repo can join a room over the comms broker: a roster of peers in the status area, /post [subject:] <text> to broadcast, /roster and /leave commands, incoming peer messages plus join/leave notices in the transcript, an unread cue, and model-facing room:* tools. The live broker room is built behind the room feature; its incoming stream reconnects with backoff, refreshes the roster on a cadence, and dedups by message id.

[0.22.3] — 2026-07-21

Fixed

  • The file watcher no longer leaks memory or storms the CPU on large, churny trees. On macOS/Windows the notify-debouncer-full debouncer defaulted to its FileIdMap cache, which recursively walks and stats the entire watched subtree — following symlinks, at watch time and again on every directory create/rename — into an unbounded map. On a pnpm monorepo (a node_modules symlink farm plus in-repo git worktrees) that walk amplified without bound: RSS climbed 138 MB → 6.85 GB in ~3 minutes at 45–95 % CPU (#43, re-file of #41; CPU counterpart #33). Both the tree watcher (basemind watch / serve) and the internal view watcher now use NoCache — the same cache Linux already defaulted to — so the debouncer never walks the tree. basemind's own gitignore-aware IndexFilter still selects what a change rescans, and renames degrade to remove-old + create-new, which the incremental scanner already handles.

[0.22.2] — 2026-07-20

Fixed

  • Codex downstream workspaces now start the latest complete release reliably. Codex resolved the old relative launcher inside its installed plugin cache, indexing the plugin instead of the user's workspace. Its plugin now locates a bundled bootstrap, resolves the latest published GitHub tag, and uses the serialized, checksummed launcher; simultaneous first starts cannot corrupt a shared npx cache.
  • MCP tools stay available across a release window. The plugin launcher (mcp-launch.sh) hard-pinned to the plugin's exact version and aborted if that release's platform asset 404'd — so in the window between the plugin bumping to a new version and that release finishing its per-platform binary uploads, the session had no MCP server at all. On a missing pinned asset the launcher now re-execs pinned to the newest published GitHub release: GitHub exposes only finalized, non-draft releases at /releases/latest, and the publish workflow promotes a release only after every platform asset + checksums exist, so the fallback target is always complete and safe to run — even from an empty cache. A same-schema-minor cached binary remains the offline last resort. The launcher also now discovers the plugin manifest under any harness mirror (.claude-plugin / .codex-plugin / .cursor-plugin), so one launcher serves every coding agent.
  • The publish workflow heals partial releases and never publishes out of order. A rerun that found any single release asset previously assumed the release was complete and skipped the build matrix, so a release missing a platform binary could never recover. It now requires the full archive + checksums set before treating a release as published, so a rerun rebuilds only the missing platforms. Publish concurrency keys on the resolved tag (a tag push and a manual dispatch for the same tag no longer race) and never cancels a publish mid-flight, and the crates.io publish waits for the release to finalize like the npm/PyPI publishes.
  • Version updates no longer leak basemind processes. On update, the prior version's serve and comms daemon kept running beside the new generation, so the machine never converged on a single daemon. The launcher now terminates every basemind process (serve and comms/write/shell daemons — all the same binary) belonging to a different cached version before exec'ing the resolved binary, and prunes leftover install lock/staging dirs. Unix best-effort; never blocks or fails the launch. No API or on-disk-format change (blob/index schema unchanged).

[0.22.1] — 2026-07-20

Fixed

  • basemind is publishable to crates.io again. The 0.22.0 stack-graphs vendoring added four in-tree forks as versionless path dependencies with publish = false, which made cargo publish reject basemind ("all dependencies must have a version requirement specified"). The forks are renamed to owned, published crates — basemind-stack-graphs, basemind-lsp-positions, basemind-tree-sitter-graph, basemind-tree-sitter-stack-graphs — and referenced by version, so cargo install basemind works again. The publish workflow now releases the sub-crates in dependency order before basemind. No API or on-disk-format change (blob/index schema unchanged).

[0.22.0] — 2026-07-20

Index rebuild on upgrade. This release bumps the index/blob/comms schema (21 → 22), so every existing .basemind/ (legacy, per-repo) and the new global cache wipe and rebuild from source on the next basemind scan. The cache location itself also moves: state no longer lives under a repo's .basemind/ — it lives in a single global, content-addressed store under the XDG data directory (see below). This is the intended migration path; nothing you need to do beyond letting the next scan run.

Added

  • Machine-wide daemon — concurrent sessions now read AND write. A single background daemon per machine is the sole fjall writer for the index. Previously, one serve process per repo held the repo's fjall write-lock, so a second session on the same repo silently fell back to a read-only, increasingly stale view. serve now opens its store read-only (blobs only) and forwards writes (scan / rescan) to the daemon over a Unix domain socket (named pipe on Windows); the daemon starts on demand and is shared by every session on the machine. N sessions on one repo now all read and write without downgrade.
  • Global XDG cache + BASEMIND_DATA_HOME. Index data (content-addressed blobs, per-workspace views, LanceDB) moves out of the repo and into ~/.local/share/basemind/ (overridable with BASEMIND_DATA_HOME), keyed by workspace. The blob store is now a single global, content-addressed cache that dedupes identical files across every repo and worktree on the machine, not just within one repo. .basemind/ is no longer created in repos — the gitignore dance from init is gone.
  • Registry + worktree coordination tools. The daemon keeps a cheap, always-on registry of repos, worktrees, and branches built from git plumbing, exposed as new MCP tools: workspaces (known repos on the machine), worktrees (linked worktrees for a repo), branches (branches per worktree), and worktree_claim / worktree_release — an advisory claim so multiple agent sessions don't collide working the same worktree at once.
  • find_files — fuzzy, fzf/fd-style filename/path search across indexed files, ranked by match score. Complements the existing substring list_files.
  • basemind statusline — a CLI subcommand that queries the daemon for the workspaces currently active and prints a compact status line; prints nothing (idle) when no daemon is running.
  • Thread comms replace room comms. Agent coordination now uses threads instead of rooms: each thread is addressed by at least two of subject / path-glob / members, discovered by scope (member, cwd path-match, or subject filter — never global), with idle threads auto-archiving. See thread_start / thread_post / thread_list / inbox_read / agent_list in the MCP tool table.
  • Precise, scope- and import-aware name resolution for Python and Java (feature code-intel-stack, included in full and the release binary). Until now only JavaScript/TypeScript resolved precisely (via oxc); every other language fell back to heuristic name matching, so find_references / find_callers / goto_definition could not tell a shadowed local from an import. basemind now runs GitHub stack-graphs-style .tsg name-binding rules — executed by an in-tree, tree-sitter-0.26 fork of the tree-sitter-graph / tree-sitter-stack-graphs engines (maintained under crates/) — to build a per-file scope/definition graph and resolve references to their true definitions. Delivers intra-file precision (shadowing, per-function/method parameter scope, Python comprehension bindings, Java field-vs-local) and cross-file resolution for Python: dotted and relative imports resolve to their target module, and each call site of an imported name resolves through the import to the exported definition (reporting resolved: true), rather than matching by name. No new MCP tools or query surface — existing navigation just gets precise for these languages. Known limitations this iteration: Java cross-file resolution of member calls on an imported type (Foo.greet()) is not yet resolved (the imported type itself is), and a variable reused after a same-name Python comprehension may bind to the comprehension's own variable.
  • [code_intel] precise_resolution config toggle (default true). Set it to false in basemind.toml to skip the precise engines (oxc + stack-graphs) and fall back to fast tree-sitter locals scope binding for every language. Applies to files (re)scanned after the change.
  • [resources] governance config. A new table bounds basemind's memory and CPU footprint for fitting a large workspace on a modest host: embed_threads (caps the ONNX embedding pool; supersedes the deprecated [documents].embed_max_threads alias), embed_batch_size (default 32), scan_threads (caps the scanner rayon pool), document_models (full | code_only | none — narrows the document tier for a code-centric workspace), and max_footprint_mb, enforced by a best-effort backpressure gate that samples the process phys_footprint (mach TASK_VM_INFO, which counts compressed and swapped-out pages) at the document-extraction and embedding admit points and parks over-ceiling workers on a bounded backoff — never failing a scan. The schema change is additive (no RELEASE_MINOR/blob bump).
  • inbox_wait — a blocking comms tool that returns the moment a peer posts to a joined thread (or on timeout), replacing the inbox_read/thread_list polling loop. Backed by the broker's existing push fan-out via a membership-routed SubscribeInbox sink over an ephemeral connection, so a long wait can't head-of-line-block other comms calls.
  • Per-call latency in every tool response. Latency-relevant tools now return elapsed_us (tool body only — index/store lookups, git walks, ranking, response construction; excludes transport and serialization) and the CLI additionally reports startup_us, so query cost is observable and distinct from process startup. Microseconds because the warm code-map path is routinely sub-millisecond.

Changed

  • Cache moved out of the repo. Everything the scanner and index used to write under a repo's .basemind/ now lives under the global XDG cache, keyed by workspace — a repo checkout no longer gains any basemind-owned files or directories.
  • Blobs are global and deduped across repos. The content-addressed blob store used to be per-repo; it is now one machine-wide store, so identical file content scanned from different repos or worktrees is extracted and stored once.
  • find_callers reports a complete result; precision is now additive. The name scan (the same sound scan find_references runs) is the floor and defines total/hits; import resolution can only refine it, surfacing precision as a per-hit resolved flag plus a resolved_total lower bound. Previously resolution ran first and returned early, so a partial resolution could report a confident, complete-looking, wrong subset (see Fixed).
  • 1000-line module cap is now enforced. tests/max_lines.rs walks src/**/*.rs and fails cargo test on any file over the cap — the documented rust-max-lines poly check never actually existed (poly's custom rules are ast-grep matches and cannot count lines). Several files that had drifted past the cap were split along real seams into sibling modules, behaviour-preserving by construction (the keys.rs split keeps symbol_kind_byte's persisted u8 ordinals byte-identical).
  • Dependency upgrades. xberg 1.0.0-rc.30, crawlberg 1.0.7, tree-sitter-language-pack 1.13.1, and the latest compatible versions across the tree; arrow-array/arrow-schema held at 58 in lock-step with lancedb 0.31. stack-graphs is vendored under crates/ with two upstream panics fixed.

Removed

  • Per-repo .basemind/ writes. basemind no longer creates or gitignores a .basemind/ directory inside a repo; basemind init no longer adds it to .gitignore.
  • Room comms API. room_list / room_join / room_leave / room_post / room_history and the CLI basemind comms rooms / join / leave / room-create / post / history surface are replaced by thread comms (see Added above).

Fixed

  • basemind init no longer scaffolds into a parent directory. init resolved its target with the same ancestor-.basemind/ walk the other commands use to attach to an existing index, so running it inside a repo whose parent already had a .basemind/ wrote basemind.toml, .gitignore, and the rules block into the parent instead of the current repo. init now anchors to the closest enclosing git repository (falling back to the working directory when not in a repo), so it always scaffolds the project you run it in.
  • Root discovery no longer climbs across a nested subrepo boundary. discover_root_with_basemind walked up looking for an ancestor .basemind/ without a ceiling, so running from inside a nested git subrepo (checked out under a polyrepo that has its own root .basemind/) wrongly attached to the parent polyrepo's index. The upward walk is now bounded by the closest enclosing git repository — the subrepo's own root is the ceiling. A non-git monorepo (no git boundary anywhere) still walks freely to an ancestor .basemind/.
  • Fixed Codex plugin MCP startup by using the required root .mcp.json shape and resolving the launcher relative to the installed plugin instead of an unsupported ${PLUGIN_ROOT} placeholder.
  • find_callers reported 2 callers where there were 172. It ran import resolution first and returned early on any hit, so the name scan ran only as a fallback when resolution found nothing. When resolution saw 2 of 172 real call sites it reported total: 2 with no truncation flag — a confident, complete-looking, wrong answer an agent would act on. The name scan is now the floor and resolution only annotates it (a Python case that returned 7 of ~4,000 now returns the full set).
  • workspace_grep returned a fast, confident, wrong zero. scan_cap bounded files visited, so a default grep over a 68k-file workspace searched ~2.9% of it in arbitrary path order and stopped — and a rare identifier, which is precisely what one greps for, does not live in the first 2.9%. limit now caps hits, the whole corpus is scanned (rayon-parallel with a memmem literal prefilter), and total_matches/total_files_matched are exact. A latent cursor bug that dropped a file's remaining matches on a mid-file limit boundary is fixed by packing (candidate, hit ordinal) into the cursor.
  • The git-history index was built and read from nowhere on a --features full binary. Fjall's directory lock is exclusive even for a read-only open, so serve (which opens read-only under comms) could not hold the history DB; the handle was None, serve never built the index, and every git tool silently live-walked forever. The daemon now owns both the build and the reads (GitHistoryIndex is a Local | Remote backend enum), the per-repo build is serialized so N sessions cause one walk, and the one-shot CLI routes its history reads through the daemon instead of climbing a retry ladder and live-walking. Measured, daemon up: CLI startup ~1.56 s → ~5 ms, a git query ~1.59 s → ~10 ms.
  • search_documents could never see anything the web lane ingested. The scanner writes documents under the repo scope while web_scrape/web_crawl write under web:<host>, and the search hard-filtered on the repo scope with no way to name the other namespace — so every scraped page was written, embedded, and then permanently invisible. scope is now a search parameter (defaulting to the repo scope), and the ingest tools echo the scope they wrote under, so the pair composes.
  • A daemon --features full build embedded nothing it scanned. The forwarded initial scan ran a Deferred (code map + BM25 keyword) pass only; the vector-fill follow-up the non-daemon path spawns was never forwarded, so LanceDB stayed empty for every repository document and code chunk (web_scrape worked because it embeds inline). The daemon can now run an Inline embed pass, and serve forwards a detached vector-fill follow-up after the fast initial scan — off the boot handshake, so startup never blocks on ONNX. Live watcher rescans and the rescan tool forward it too.
  • A failure in an optional lane destroyed the code map. The scanner flushed the code map after the optional lanes (embeddings, documents, code-intel), so any lane that failed took the whole map with it — leaving gigabytes of blobs behind a file_count of 0 and a full re-scan on every launch. The map is now persisted before the optional lanes run, each lane runs under catch_unwind, and a per-file panic in the resolve pass costs only that file's edges.
  • The machine daemon's footprint climbed to ~13 GB at monorepo scale and never released. The Inline embed pass accumulated every file's embedding rows (a 768-dim vector + chunk text per chunk) for the whole corpus in RAM before one LanceDB write. It now streams per file, so peak embed RAM is one file independent of corpus size (a phys_footprint-ratchet regression test pins it flat across passes). A related serve-side RSS sawtooth — a whole-map rebuild on every no-op rescan — is gone: an (path, content-hash) fingerprint proves the blobs are byte-identical and the resident map is reused.
  • A panicking request handler pinned its daemon forever. The idle reaper counts live links, and the refcount was released by a plain statement after the accept loop, which an unwind walks past — so one panicking handler left the count above zero and that daemon never reaped, stacking an immortal process on every run. The refcount is now an RAII LinkGuard released by Drop (also closing the accept/exit race), and idle detection consults in-flight work, not link count alone.
  • Every CLI invocation rebuilt the whole in-RAM map cache — ~3–5 s flat, up to 11 s under load, even for repo-info, which never reads the map. The one-shot CLI now builds it lazily at the cache barrier, only for tools that scan the corpus (repo-info ~534 µs vs search_symbols ~1 s). En route this fixed complete() reading the map without taking the barrier every other cache-reading tool takes — an intermittent flake that would have become a permanent empty under lazy-cache.
  • Orphaned workspace caches pinned blobs forever. Nothing reaped the per-workspace cache dir of a deleted repo, so the global blob store could only grow (48 workspace dirs for 2 live repos). A marker-based reaper now records the canonical root and reclaims dead workspaces before the blob sweep — conservatively keeping anything it can't prove dead (held lock, or a root whose parent is also missing, e.g. an unmounted volume).
  • Two agents sharing one identity shared one inbox. The CLI's persisted-id tier read the pre-0.22 in-repo path that no longer exists, so every CLI call collapsed onto a shared "basemind-cli" constant (and serve onto "anon"). All four resolvers are consolidated to a single chain — env, config, then a generated id persisted in the correct per-workspace cache dir, then a process-unique id — never a shared constant, and honouring comms.agent_id.
  • A thread scoped to <dir>/** was invisible to an agent whose cwd is <dir>. globset's ** requires a component after the slash, so the recursive form never matched its own base — the most common cwd. The stripped base is retried, covering the root without widening the glob.
  • Cross-file goto_definition silently dropped every tsconfig-aliased import. oxc resolves a tsconfig paths alias against a canonicalized baseUrl, so on a symlinked root the resolved target's prefix didn't match and the cross-file edge was discarded. The resolver now discovers the governing tsconfig per importing file (what tsc applies) and to_repo_relative retries against the canonical root — a no-op on non-symlinked roots.
  • A daemon-writer rescan didn't invalidate in-flight search_symbols cursors on unchanged content. The cache_generation bump was nested inside the map rebuild, which the fingerprint optimization skips on a no-op rescan, so a stale cursor read as valid — contradicting the documented invalidated on rescan contract. The bump is now unconditional.
  • Telemetry recorded call latency in milliseconds, so every sub-millisecond tool read as 0. The sink now stamps microseconds (legacy elapsed_ms rows are rescaled on read), matching the response-level elapsed_us reading.
  • Fjall ran on its 32 MiB default block cache against a multi-GB index. The cache is now sized at 20% of the on-disk index, clamped to [32 MiB, 256 MiB] (BASEMIND_INDEX_CACHE_BYTES overrides); find_references("get") on django went 907 µs → 285 µs for +15.9 MB RSS.
  • web_crawl followed links off the seed host. crawlberg's stay_on_domain defaults off and the engine builder never set it, so a crawl wandered onto every domain it linked to. On-host scope is forced now (subdomains still excluded).
  • web_map returned every URL a site has, uncapped — 482k URLs on docs.rs drove peak RSS into the multi-GB range and OOM-killed the process. limit (default 100, max 1000) now caps the response, and crawlberg 1.0.7's map_limit bounds the sitemap fetch itself, holding peak to roughly the cap plus one child sitemap; total_urls becomes a floor with truncated set.
  • cache_stats no longer flakes when a *.tmp file vanishes mid size-walk, and an fsevents watcher test race (an event landing before stream registration is lost forever) is closed by re-writing until the watcher reports the path.

[0.21.1] — 2026-07-11

Changed

  • Extracted the Hermes Agent plugin into a standalone basemind-hermes-plugin PyPI package. The helper skills, slash commands, plugin.yaml, and the hermes_agent.plugins entry point now live in the pure-Python pip-package-hermes/ (import package basemind_hermes_plugin), which ships no binary and shells out to the basemind on PATH. The basemind pip package is now a pure binary-wrapper (no plugin, no entry point). Install the binary via Homebrew/npm/cargo/release and pip install basemind-hermes-plugin for the plugin, then hermes plugins enable basemind. The entry-point name stays basemind, so the enable command is unchanged.

Fixed

  • Binary download no longer crashes when /tmp and $HOME are on different filesystems (#38). ensure_binary staged the extracted binary in the system temp dir and then atomically renamed it into ~/.cache/basemind, which raised OSError: [Errno 18] Invalid cross-device link on hosts where those are separate mounts. Staging now happens under the cache root so the rename stays on one device.

Dependencies

  • Bumped xberg to 1.0.0-rc.21 and ran cargo upgrade --incompatible: crawlberg 1.0.5, jsonschema 0.47, rmcp 2.2, regex 1.13, lru 0.18.1, tree-sitter-language-pack 1.12.5. arrow-array/arrow-schema held at 58 to stay in lockstep with lancedb 0.31's transitive Arrow. Blob + index format unchanged (patch release).

[0.21.0] — 2026-07-10

Minor bump: RELEASE_MINOR advances 20 → 21, so the .basemind/ index + blob cache is wiped and rebuilt from source on the next basemind scan (expected — the msgpack blob store is content- addressed and rebuilds losslessly).

Added

  • basemind init onboarding + /bm-init slash command. A re-runnable setup flow that scaffolds the root basemind.toml, gitignores .basemind/, and injects an idempotent, clearly-delimited rules block into the repo's agent guidance (CLAUDE.md / AGENTS.md, or an .ai-rulez rule file when that's the source of truth) teaching agents to prefer basemind over grep/read/git per capability. Interactive by default; --yes plus --with/--without, --rules-target, --no-rules, and --print (dry-run) drive it non-interactively. The block is spliced between stable <!-- BEGIN/END basemind --> markers so re-running never duplicates it and never touches content outside the markers.
  • Monorepo-aware root resolution. basemind now walks up from the working directory to the nearest ancestor containing .basemind/ (the way git finds .git/), so running an agent from a subfolder of a monorepo attaches to the root index instead of finding nothing. .basemind discovery takes precedence over git-root discovery; --root still overrides.
  • Index lifecycle signalling on MCP responses + status. Read tools attach a notice (warming_up / building_index / rescanning) with an actionable retry hint so an agent never mistakes a mid-warmup or mid-scan result for "no matches". status gains warming + warm_ms.

Fixed

  • serve answers the MCP handshake before warming the code map. The in-RAM preload (a rayon par_iter over every L1/L2 blob) ran synchronously before .serve(transport), so on a loaded machine — several sessions' scans saturating the shared rayon pool — it could queue for minutes, blow the client's plugin-MCP startup window, and the tools would never register. The preload now runs in a background task; initialize/tools/list answer immediately regardless of tree size or contention. Cache-reading tools await a bounded (15s) ready signal so a query issued during warmup still returns complete data rather than an empty snapshot. diff_outline, blame_symbol, memory_audit, and proposal_accept — which read the map directly — now await warm too, closing a window where they returned a wrong diff, a spurious "not indexed" error, or a false stale verdict.
  • basemind init refuses to splice into a file with malformed markers. A stray or unpaired BEGIN/END marker (a hand-edit or bad merge) could make a re-run replace everything between an orphaned marker and an unrelated pair, silently deleting the user's own content. init now bails on any malformed marker state instead of guessing, and absorbs a CRLF trailing newline so a Windows-line-ending rules file converges to a byte-stable fixpoint (no phantom --print diff).

[0.20.0] — 2026-07-08

Minor bump: RELEASE_MINOR advances 19 → 20, so the .basemind/ index + blob cache is wiped and rebuilt from source on the next basemind scan (expected — the msgpack blob store is content- addressed and rebuilds losslessly).

Added

  • Root config at <root>/basemind.toml (committable). Config moved out of the wipe-on-schema-bump .basemind/ cache to the repo root so it survives cache wipes and can be committed. The legacy .basemind/basemind.toml is still read as a fallback; the root file wins when both exist. basemind init now scaffolds a fully-documented root config (every value is the built-in default) and adds .basemind/ to .gitignore. It refuses to overwrite an existing config and refuses to shadow a legacy in-cache config rather than silently stranding your settings.
  • scan.follow_symlinks (default false). Symlinks often escape the repo (e.g. Bazel's bazel-* convenience symlinks); the walker no longer follows them unless you opt in. extra_roots always follow symlinks regardless.
  • embed_exclude glob lists on code_search and documents. Keep a file chunked + keyword- indexed while skipping its embedding — useful for large generated or vendored files.
  • documents.extract_archives (default false). Opt into xberg's recursive archive extractor for .zip/.tar/.jar/…; off by default so one archive can't explode into thousands of embeds. True binaries (.so/.wasm/.exe/…) are always rejected regardless of this flag.

Changed

  • Code embeddings are now OFF by default (code_search.embed = false). A general-English model embeds code weakly, and the BM25 keyword lane already serves the NL→symbol query over the same text, so code embedding isn't worth the ONNX download + ORT pass + vector store. Code is still chunked and keyword-indexed; documents and images still embed. Existing code-search users who rely on vector search over code must set code_search.embed = true to keep it.
  • Always-on exclude floor. A hard floor of near-universal build-artifact / dependency-cache / VCS / editor directories (node_modules, target, dist, build, out, .venv, __pycache__, .pytest_cache, bazel-*, .git, .basemind, .idea, .DS_Store, …) is applied on top of scan.exclude — narrowing the user list can add to it but never drops a floor entry.

Fixed

  • Worktrees reuse the shared cache instead of redoing work. Scanning a fresh git worktree no longer re-parses files whose extraction already lives in the shared (main-worktree) blob store — the scanner deserializes the cached L1/L2 frame instead of re-running tree-sitter. The git-history index is likewise shared: it is derived entirely from the shared .git object database, so a linked worktree resolves it to the main worktree's .basemind (mirroring the blob store) rather than rebuilding its own. On a large monorepo a cold worktree scan drops from ~181s to ~25s (~7×) with ~2.6× less peak memory. The scan summary gains a reused counter reporting cache hits.
  • Re-embed on same-dim preset model swaps. The per-file embedding cache keyed on vector dimension only, but balanced and multilingual both produce 768-dim vectors — so switching between them reused the old model's cached vectors. Cache reuse now also requires the embedding model/preset to match (both the code and document tiers), so a preset change cleanly drops the old vectors and re-embeds. CodeChunkBlob gains a blob-compatible embedding_model field (no schema bump).
  • Hugging Face model downloads no longer hang forever behind a firewall. Bumped xberg to 1.0.0-rc.16, which adds connect/read timeouts to the HF model fetch so a blocked or silently dropped connection (corporate firewall, offline host) fails fast instead of stalling the embedding / OCR / NER model download indefinitely.

[0.19.2] — 2026-07-07

Added

  • Intel macOS (x86_64-apple-darwin) prebuilt binary. macOS is no longer Apple-Silicon-only — the release now ships a native Intel binary with full feature parity. ort has no static x86_64-apple-darwin ONNX Runtime (Microsoft dropped onnxruntime-osx-x86_64 after 1.23), so the Intel leg builds --features full,ort-dynamic (ort/load-dynamic) and package-release.sh vendors libonnxruntime.dylib + its closure next to the binary with @loader_path install-names; ort resolves it relative to the executable, so ONNX loads with zero end-user setup. brew install now works on Intel Macs too. The release completeness gate expects five archives.

Fixed

  • Apple Silicon under a Rosetta x86_64 shell now installs. Rosetta masks hw.optional.arm64, so the previous probe fell through to the Intel branch and aborted with "Intel macOS not supported" — even though the machine is Apple Silicon. All three installers (shell launcher, npm install.js, pip downloader.py) now also probe sysctl.proc_translated (set only under Rosetta, which exists only on Apple Silicon) and hand such shells the native arm64 binary. A genuine Intel Mac gets the new x86_64 binary.
  • /bm-statusline now selects the correct (highest) version. The persisted resolver ranked cached statusline.sh copies by mtime (ls -dt), so an older version dir touched more recently won — showing a stale bar. It now resolves the marketplace clone first, else the highest-versioned cache copy via sort -V, and never renders blank (a one-line hint replaces an empty bar).

Changed

  • Installers prune stale cached binaries. The launcher and pip downloader accrued a directory per release under ~/.cache/basemind/(bin/)<version> (users saw six). Each now removes old per-version dirs once the current version is confirmed present, so only the running version is kept.
  • Dependencies refreshed to latest (cargo upgrade --incompatible): xberg → 1.0.0-rc.11, fjall → 3.1.6, plus transitive lockfile updates. arrow-array/arrow-schema are intentionally held at 58 until lancedb's transitive arrow moves (they must stay in lockstep).

[0.19.1] — 2026-07-07

Fixed

  • scan.extra_roots now works on Windows. Files outside the repo root are keyed by their absolute path; the Windows drive-prefixed form (C:/…) is now recognized on the consuming side — previously only POSIX /… keys resolved, so external files were unqueryable there. Unix keys are unchanged (index format untouched).

Security

  • Bump crossbeam-epoch to 0.9.20 for RUSTSEC-2026-0204 (invalid pointer dereference in the fmt::Pointer impl for Atomic/Shared), pulled transitively via rayon.

[0.19.0] — 2026-07-07

Minor release — the index + blob schema version bumps (RELEASE_MINOR 18 → 19), so .basemind/ is wiped and rebuilt on the next scan.

Added

  • Hermes Agent plugin. basemind now integrates with Hermes Agent: pip install basemind exposes a Hermes plugin (discovered via the hermes_agent.plugins entry point) that bundles the helper skills, slash commands, and agent-comms notifications. Tools are wired through Hermes's MCP config (mcp_servers.basemind in ~/.hermes/config.yaml) — a Hermes plugin cannot declare an MCP server. See the README "Hermes" install section. Closes #36.

Changed

  • Claude status line survives version bumps. /bm-statusline now writes a version-independent resolver as the statusLine command (re-resolving the newest installed statusline.sh at each render) instead of a version-pinned path that broke on update. The bar also shows the running basemind version (v<version>, full/compact tiers; opt out with BASEMIND_STATUSLINE_VERSION=0).

Performance

Wide hot-path allocation + algorithmic sweep across the scanner, extraction, git, and MCP query paths. All changes are internal — no behavior, response-shape, or on-disk-format change, and the determinism assertions are unchanged.

  • call_graph (bfs_callees) precomputes a name→sites map once instead of re-scanning every indexed symbol for each discovered callee — O(max_nodes × symbols) → O(symbols + max_nodes).
  • architecture_map: callee_counts no longer allocates a String on every one of up to 4M scanned call sites (allocates only on first sight of a name); the symbol-tier fan-out set skips the allocation for duplicate callees; PageRank reuses a single accumulator buffer (20 allocations → 1). Output stays byte-identical.
  • Tree-sitter QueryCursor is now pooled per thread alongside the parser pool — ~3 fewer allocations per scanned file.
  • git: diff_file no longer clones the (potentially multi-MB) file buffers just to record existence; the blame / log / commit-files disk-cache writers serialize from borrowed slices instead of cloning the whole payload; dependents drops a RelPath↔PathBuf round-trip over the entire file index.
  • document tier: build_doc_rows moves per-chunk text + embedding vectors instead of cloning them; the archive-extension check is an AHashSet (O(1)) instead of a 40-element linear scan.
  • paths: the scanner (scan_paths) and watcher skip a \→/ String allocation on Unix.
  • BM25 index: the posting value drops a .to_vec() (lsm_tree::Slice: From<[u8; N]>) and the forward-map value is built in one sized allocation instead of an alloc-plus-realloc.

Internal

  • for_each_call_in_file (the dual-backend Fjall / in-RAM call-site scan) now lives once in mcp/helpers_calls.rs, shared by architecture_map and the call-graph helpers; capture_name is deduplicated into extract/mod.rs. Removed a dead last_emitted_key in find_implementations.

[0.18.1] — 2026-07-06

Patch release — blob + index format unchanged. RELEASE_MINOR stays 18; no .basemind/ rebuild. Query-side only.

Changed

  • architecture_map symbol tier now ranks by specificity-weighted fan-in. The repo-wide symbol tier previously ranked by raw name-based call count, so ubiquitous names (new, from, default) dominated — on a real repo the top ~60 hubs were all new, which misleads an agent routing on the result. Fan-in is now divided by the number of files defining that name (RepoGraph::def_counts), so a genuine single-definition hub outranks a name that is merely everywhere. The raw fan_in count is still reported verbatim on each node.

Fixed

  • architecture_map symbol-tier score is now monotonic with node order. fan_out was computed after the knee-cut, so the blended score never actually influenced selection or ordering and the emitted nodes were not sorted by their own score. Selection, knee-cut, and score now all key off the single specificity-weighted signal, matching the module/file tiers; fan_out and per-file churn remain reported but no longer pretend to affect the ranking.

[0.18.0] — 2026-07-06

Minor release — cache rebuild. RELEASE_MINOR bumps to 18, so both the Fjall index and the content-addressed blob store are wiped and rebuilt from source on the next basemind scan (the standard minor-release migration). No config or on-wire tool-response change.

Added

  • architecture_map MCP tool — deterministic, LLM-free architecture overview. Ranks modules and symbols by graph centrality + churn and surfaces circular-dependency clusters, all derived from real call edges (never hallucinated). A whole-repo adjacency graph is built in memory at query time from the cached call sites (Fjall calls_by_path, or the in-RAM call cache in read-only multi-session mode) — no new Fjall partition, no blob I/O. Metrics are cheap and deterministic: fan-in/fan-out degree, fixed-iteration PageRank (damping 0.85, 20 iterations, id-ordered accumulation — call-twice byte-identical), and iterative Tarjan SCC for cycle clusters. Three tiers via granularity: module (default; files collapsed to a directory prefix at depth), file (the base graph), and symbol (function→function drill-down under a focus). Results are ranked, Kneedle knee-cut, then bounded by the shared apply_budget token budgeter; edges are emitted only between surviving nodes. Optional include_churn overlay reuses the hot_files aggregation. Node/edge caps set truncated + truncation_reason on overflow. CLI: query architecture-map. New internal kneedle knee-detection helper (Satopaa 2011, dependency-free).

[0.17.0] — 2026-07-05

Minor release — cache rebuild. RELEASE_MINOR bumps to 17, so both the Fjall index and the content-addressed blob store are wiped and rebuilt from source on the next basemind scan (the standard minor-release migration). No config or on-wire tool-response change.

Added

  • Semantic code search (Phase 1, vector-only). New search_code MCP tool runs vector KNN over source-code chunks and get_chunk fetches a chunk's body — the same two-call pointer pattern as search_symbols → expand. Chunks are derived from the cached L1/L2 extraction + source bytes (no re-parse): one chunk per symbol span plus module-level gap chunks, oversized spans split with overlap. A content-addressed <hash>.chunk.msgpack sidecar caches chunks + embeddings so an unchanged file skips re-embedding. Vectors land in a new LanceDB code_chunks table alongside the documents/memory tables (shared embedding preset). CLI: query search-code / query get-chunk. Behind the code-search cargo feature (folded into full); BM25 keyword + RRF hybrid fusion land in later phases. The code_chunks table is created lazily on open, so it rides the existing minor-release .basemind/ rebuild — no separate schema bump.
  • Keyword code search (Phase 2, native BM25). search_code gains a mode parameter: semantic (the default vector lane) or keyword, a native Okapi BM25 (k1 = 1.2, b = 0.75) lane over each chunk's symbol + signature + doc + body text. Postings live in two new Fjall keyspaces (code_bm25_postings inverted + code_bm25_by_path forward for O(prefix) re-scan deletes); term frequency and document length are inlined so scoring a term is a single prefix scan, and corpus stats (N, avgdl) are recomputed once per scan. The keyword lane reads only Fjall and the chunk sidecar — no LanceDB and no embedder — so it works even with [code_search] embed = false. RRF fusion of the two lanes plus an exact symbol lane lands in Phase 3.
  • Hybrid code search (Phase 3, RRF fusion + exact lane + rerank). search_code gains a third mode, hybrid — now the default — that fuses three lanes via Reciprocal Rank Fusion: the vector (semantic) lane, the BM25 keyword lane, and a new exact symbol lane that resolves an identifier-shaped query against the symbols_by_name index to the chunks defining that symbol (the scope-aware signal a pure vector+keyword stack can't produce). Fusion joins the lanes on chunk_id and is score-scale-agnostic, so it blends an L2 distance, a BM25 score, and a symbol match order without normalization; it degrades gracefully, dropping any lane that is unavailable (e.g. the vector lane without embeddings). An optional cross-encoder rerank pass (rerank:true / [code_search.reranker], off by default) reuses the same xberg reranker as the documents tier. No index-schema change — reuses the existing keyspaces and the code_chunks table.
  • search_code why-matched provenance. Hybrid hits now carry per-lane provenance: matched_lanes (which of exact / vector / keyword produced the hit, in fixed lane order) plus the 1-based exact_rank / vector_rank / keyword_rank the chunk held in each contributing lane. Lets an agent tell an exact-symbol match from a semantic neighbor or a cross-lane agreement without a second call. Additive, non-breaking response fields (absent outside hybrid mode).
  • status reports boot-scan indexing state. When serve auto-scans an empty index on startup, that build cost was invisible and could fold into the first query's latency. status now carries indexing: true while the boot scan runs and index_build_ms once it completes — so a client can tell "index not ready yet, poll again" from "no matches", separating index-build time from query time. Additive fields, absent on the common ready path.
  • Code-intelligence tier: scope- and import-resolved navigation. A post-scan resolve pass links each reference to its actual definition — scope-aware, not a name match. Intra-file resolution runs across 80+ languages via tree-sitter locals (with vendored queries for Python / TypeScript / TSX / Go); JS/TS additionally get cross-file resolution via oxc scope analysis + oxc_resolver import stitching. New goto_definition MCP tool + query goto-definition CLI resolve a path:line:column to its definition, following cross-file import bindings. Behind the code-intel-js feature (folded into full) for the JS/TS engine; the locals tier is always on.
  • Document tier reuses the content-addressed cache instead of re-embedding every scan. The documents pipeline now tracks each file in a new Index.doc_files map and, before extracting, reads back the cached <hash>.doc.msgpack blob (which already carries chunks + embeddings): an unchanged doc is skipped, and byte-identical content at any other path — or, with the shared cache below, any other worktree — reuses the extraction instead of re-running xberg + ONNX. Removed docs now have their LanceDB rows and tracking entry pruned. doc_files is #[serde(default)] — additive, no schema bump.
  • Shared blob cache across git worktrees. Linked worktrees now resolve their content-addressed blob directory to the main worktree's .basemind/blobs (via gix common_dir), so a file's extraction + embedding is computed once and shared across every worktree of the clone; views + LanceDB stay per-worktree. Auto-GC is disabled while a shared cache exists (a single-worktree sweep could reap a sibling's blobs).
  • Bounded embedding threads. All ONNX embedding now runs on a dedicated rayon pool capped at documents.embed_max_threads (default 0 = auto max(2, cores/4)), and xberg's internal fan-out is capped to match. Code-map extraction keeps the full pool; embedding can no longer pin every core or balloon RSS on a large monorepo.
  • New config: documents.max_chunks_per_document (default 2000) caps vector rows per document; documents.extension_denylist extends the built-in archive/binary skip list; documents.embed_max_threads bounds the embedding pool. [scan] exclude gains Bazel defaults (bazel-out/, bazel-bin/, bazel-testlogs/, bazel-*/), and a large-candidate-count scan logs a warning.
  • mtime+size fast-path on rescan. An unchanged working-tree file is now confirmed with a single stat() (size + nanosecond mtime match + sidecars present) instead of a full read + blake3 hash — the bulk of the per-file cost on a large-monorepo warm rescan. mtime moved to nanosecond resolution so the fast-path is effectively race-free; it's an internal comparison value, so no schema bump.

Changed

  • Serve boot defers embedding to a background pass. On serve auto-scan of an empty index, the first pass now runs in a new EmbedMode::Deferred mode: it writes the code-map, the BM25 keyword lane, and the content-addressed blobs but skips the ONNX embedding, so outline / search_symbols / keyword search_code are queryable almost immediately instead of waiting on the embed of every file. A detached second pass then re-scans in EmbedMode::Inline to fill the vectors in (reusing the fast pass' content-addressed caches so only not-yet-embedded content is embedded, bounded by embed.max_threads), followed by GC. The CLI basemind scan, the watcher, and manual rescan stay Inline so scripted/CI use remains deterministic. No schema or config change.
  • Index schema revision — new refs_by_def / refs_by_path Fjall partitions back the resolved edges; per-file resolution facts persist as content-addressed <hash>.rref.msgpack blobs. This ships as a minor-release cut: RELEASE_MINOR bumps at release time, wiping and rebuilding every .basemind/ index + blob store on the next basemind scan (the resolve pass repopulates the new partitions). The code_bm25_postings / code_bm25_by_path partitions for the BM25 keyword lane (INDEX_PARTITION_REVISION → 4) ride the same index rebuild — the blobs are unchanged, so re-embedding is skipped and only the secondary index repopulates.

Fixed

  • Code-search BM25 postings survive an embedder failure. With [code_search] embed = true, a failed embedder load / inference / vector-count mismatch previously returned an error for the whole file, dropping its BM25 postings too — so a transient embedding fault silently punched keyword-lane holes. The scan now degrades to a keyword-only batch (chunks + BM25, no LanceDB rows, no embedded sidecar) and retries embedding on the next scan. BM25 is independent of embeddings and is indexed as such.
  • L1 extraction no longer drops (or crashes on) symbols in TSLP-fallback languages whose tags.scm leads a definition with an auxiliary capture. The combined-L1 dispatch keyed on the match's first capture, but adapted upstream tag queries (e.g. Ruby) prepend a @doc capture for a preceding comment — so a commented def classified as Other, panicking debug builds (debug_assert!) and silently dropping the symbol in release. A single such file could abort the whole parallel scan, leaving the index empty. Dispatch now scans to the first capture that classifies to a known L1 class, recovering those symbols across every fallback grammar.
  • rescan rejects path parameters that escape the repository root. The rescan tool took raw path strings and joined them straight onto the repo root, so with scan.respect_gitignore = false a paths: ["../../etc/passwd"] traversal would read (and index) a file outside the repo, then be retrievable via workspace_grep. Request paths now pass through the same normalize_query_path root-boundary guard as shell_spawn; an escaping path is rejected with invalid_params instead of being scanned. Default respect_gitignore = true already blocked it structurally.
  • Archives and binaries are no longer unpacked + embedded during a scan. should_extract_document rejected nothing by default, so xberg recursively unpacked .zip/.tar/.jar/.whl/... and embedded every entry, and binary blobs (.so/.wasm/.class/...) were embedded to no purpose — a large, pointless cost that pinned ~11 cores and grew .basemind to gigabytes on a monorepo. A built-in extension + MIME denylist now skips them before any xberg work (images stay allowed for OCR).
  • Document blobs are no longer reaped by the blob GC. Because docs weren't tracked in the index, collect_referenced_hashes never marked their .doc.msgpack blobs live, so the boot/background GC deleted the entire doc cache after every scan. Doc-tier hashes are now unioned into the live set.
  • Full scans of large repos no longer abort with a stack overflow. Tree-sitter parse trees and the msgpack (de)serialization of large extraction blobs can recurse past rayon's small default worker stack (~2 MiB) on pathological (deeply-nested / machine-generated) files, hard-aborting the process on a first full scan (e.g. a freshly-checked-out git worktree; a warm rescan hid it by skipping unchanged files). Extraction and the resolve pass now run on a dedicated rayon pool with a 256 MiB per-worker stack (reserved lazily, so it costs nothing on the common shallow path).

[0.16.0] — 2026-07-02

Minor release: RELEASE_MINOR bumps 15 → 16, so every .basemind/ index + blob store (including git-history.fjall/) is wiped and rebuilt on the next basemind scan.

Added

  • Resource footprint in cache_stats (disk + RAM). The cache_stats MCP tool and basemind cache stats CLI now report a total_bytes that reconciles with du exactly, a per-component breakdown that includes the git-history.fjall/ index (previously uncounted — the "1.15 GB reported vs 1.5 GB on disk" gap), an other_bytes catch-all so an uncounted directory can never silently vanish again, and process memory (rss_bytes + peak_rss_bytes) via a new cross-platform RSS reader (src/sysres.rs; mach on macOS, /proc + getrusage on Linux). Disk sizing now uses on-disk block counts, so Fjall's sparse journals are no longer over-reported. The Claude statusline gains the on-disk total + serve-process RSS.
  • Full CLI↔MCP parity. Every MCP tool an agent can call is now reachable from the CLI: new git search (→ search_git_history), governance audit (→ memory_audit), query expand (→ expand), and a new shells group (spawn/send/capture/kill/broadcast/list → the shell_* tools, --features shells; sessions persist via the shared rmux daemon). Three CLI-only text primitives gained MCP tools — delta, checkpoint, detect_waste. A new tests/cli_parity.rs guard enumerates the advertised MCP tool set and fails if any tool lacks a CLI counterpart.
  • scan.extra_roots — index directories outside the repository root (#34). Point the new [scan] extra_roots = [...] config knob at absolute paths outside the repo (e.g. a Bazel external repo cache, /private/var/tmp/_bazel_<user>/<hash>/external) and their files join the code map (symbol search, references, callers, outlines) and document search. External files are keyed by their absolute path — repo files stay repo-relative, so the two namespaces never collide and find_references resolves across the boundary. Missing/unreadable roots are skipped with a warning; a root inside the repo is ignored. Symlinks are followed (Bazel external/ is symlink-heavy). Extra roots are (re-)indexed on a full basemind scan only — the live watcher does not track them — and git blame short-circuits with a clear error for external (untracked) files. The feature itself changes no index/blob format; a re-scan populates external files (and this release's RELEASE_MINOR bump wipes + rebuilds .basemind/ anyway).

Fixed

  • Git author/keyword lookups routed to the wrong tool. search_git_history and recent_changes descriptions now state their contracts sharply — author/"what did X do"/keyword questions belong to the full-depth search_git_history (scans every commit reachable from HEAD), not the bounded 100-commit recent_changes window. Precision is verified against real git at full branch depth by the new tests/git_parity.rs harness (opt-in via BASEMIND_GIT_PARITY_REPO).
  • Double-run lock UX. basemind scan / rescan now pre-detect a live basemind serve/watch holding the store lock (via a non-blocking probe_writer_lock) and print an actionable notice naming the holder, exiting cleanly instead of colliding with a raw lock error.
  • Misleading status latency. status uses a non-blocking read, so a multi-minute rebuild holding the write lock is no longer recorded as the call's wall-clock; it returns immediately with rebuild_in_progress: true when a writer is live.
  • cache_stats no longer hard-fails on a stale/unreadable index. It reports sizes regardless and flags blob_accounting_ok: false when orphan accounting had to be skipped (e.g. a schema-version mismatch), instead of erroring out.
  • /bm-stats works offline. The plugin command is now CLI-first (like /bm-doctor), reading basemind cache stats / basemind telemetry when the MCP server is disconnected.

[0.15.1] — 2026-07-02

Patch release: docs / skills / plugin-manifest only. No schema or RELEASE_MINOR change, so existing .basemind/ caches are untouched on upgrade.

Added

  • Dedicated per-capability skills. Split the "use basemind" guidance into three new narrowly-triggerable skills alongside the existing umbrella basemind and basemind-comms: basemind-code-search (symbol search / outline / references / callers / call graph / implementations / workspace_grep), basemind-git-history (history / blame / diffs / churn / search_git_history), and basemind-documents (semantic + full-text document search, NER / keyword filters, and web ingestion). Mirrored into the Codex / Cursor / OpenCode plugin trees by scripts/sync-plugin-skills.sh.
  • llms.txt. Root-level llmstxt.org index pointing agents at the capability skills, docs, and config schema.

[0.15.0] — 2026-07-02

Minor release: RELEASE_MINOR bumps 14 → 15, so every .basemind/ index + blob store (including git-history.fjall/) is wiped and rebuilt on the next basemind scan — which is also how existing repos populate the new git-history full-text search index.

Added

  • search_git_history — full-text search over commit history. Search commit author name + email and message summary + full body, tokenized (lowercased, split on non-alphanumeric) and matched as an AND. field scopes to author, message, or all (default); limit + cursor paginate like the other git-log tools. Backed by two new git-history partitions — a gh_term_to_ords inverted term→commit-ordinal index (reusing the newest-first delta-varint posting encoding) and a gh_commit_text_by_ord body store kept out of the head-decode hot path. When the git-history index isn't fresh (read-only session / mid-build) the tool degrades to a bounded live walk over the recent window, flagged partial (author + summary only). To support it, commit extraction now captures author_email + full body, and the stored commit-meta head gains an author_email field (sha ‖ time ‖ author ‖ email ‖ summary ‖ files).

Fixed

  • Multi-session MCP contention: the writer→read-only downgrade race. A basemind serve that rightfully held the .basemind/.lock write lock could still come up read-only when a concurrent reader transiently held Fjall's single-holder index lock — leaving the repo with zero writers (no auto-scan / watcher / rescan → a silently stale index). Fixed on both single-holder Fjall stores: the writer now retries a transient Locked on open (it already owns .basemind/.lock, so the contention always clears), read-only openers probe .basemind/.lock and skip the Fjall open entirely when a writer is live (serving from the concurrently-readable blobs), and the schema-version check no longer does a throwaway double-open that widened the race window. The downgrade log now names the real lock holder instead of always blaming "another serve".

Changed

  • git-history / index / comms Fjall opens collapsed to a single Database open + inline schema read (was: a throwaway peek-open before the real open). GIT_HISTORY_SCHEMA bumps +4 → +5 for the FTS commit-meta layout, wiping and rebuilding .basemind/git-history.fjall/ on the next scan — a no-op for released users (the git-history index only became user-visible with this release).

[0.14.0] — 2026-06-30

Minor release: RELEASE_MINOR bumps 13 → 14 (a new SymbolKind::Heading variant), so every .basemind/ index + blob store is wiped and rebuilt on the next basemind scan — which is also how existing repos pick up Markdown headings.

Added — Markdown / Obsidian support

Markdown files (already parsed, but previously yielding no symbols) are now a first-class surface: point basemind at an Obsidian vault or any Markdown notes directory and the code-map tools work over it the same way they do over source. All of this is on by default — .md is scanned like any other file, headings ship via a src/queries/markdown.scm override, and the reference graph is harvested by a fence-aware byte-scan in extract/l2.rs (the tree-sitter block grammar models none of these inline constructs).

  • Headings → outline. ATX (#…######) and setext headings become Heading symbols, so outline and search_symbols (optionally kind: "heading") navigate a note's structure.
  • Wikilinks → backlinks. [[Note]], [[Note#Heading|alias]], and ![[Embed]] become references, so find_references "Note" returns the notes linking to it. Targets resolve by note name (#anchor and |alias stripped), matching Obsidian.
  • Standard Markdown links → backlinks. [text](Note.md) / ![alt](img/Diagram.png) also become references, normalized to the same note name (directory, .md, #anchor stripped, %20 decoded), so wikilink and Markdown-link vaults share one backlink graph. External URLs (http(s)://, mailto:) and bare #anchor links are ignored.
  • Tags → graph. Inline #tag / nested #area/sub and YAML frontmatter tags: (both [a, b] and - a list forms) become references keyed on the leading #, so find_references "#project" lists every note carrying that tag. Tags inside fenced code blocks are skipped.

Not yet mapped (deliberate follow-ups): block references (^id, [[Note#^id]]), frontmatter aliases / arbitrary properties, and task/callout items.

[0.13.0] — 2026-06-30

Minor release: RELEASE_MINOR bumps 12 → 13, so every .basemind/ index + blob store is wiped and rebuilt on the next basemind scan (intentional; no action needed).

Fixed

  • serve watcher no longer pegs multi-core CPU on gitignored / nested-.basemind churn (#33). A writer serve watching an umbrella repo woke on every filesystem event except those under its own .basemind/, then ran a no-op incremental scan that still re-serialized the index (store.flush) and rebuilt the entire MapCache over the whole corpus — on every debounced batch. With nested child repos each flushing their own .basemind/ in a mutual loop (or any node_modules / build churn) this rebuilt the cache indefinitely. Three fixes:
    • The watcher now drops every event a full scan would never index — including nested child-repo .basemind/ writes — before waking a rescan, honoring the full nested .gitignore hierarchy (via the ignore crate) plus the configured exclude globs. The incremental scan_paths is now gitignore-aware too, matching full-scan semantics.
    • scan_paths short-circuits when a batch changes nothing indexable — no store.flush, no work.
    • scan_and_refresh updates the MapCache incrementally for the changed paths (re-reading only those blobs) instead of rebuilding the whole corpus, and skips the rebuild and cache-generation bump entirely when nothing changed (which no longer needlessly invalidates paginating clients' cursors).
    • Note: on macOS, FSEvents is kernel-recursive, so excluded subtrees cannot be un-watched; their events are now filtered in-process in microseconds instead of triggering a rebuild.

Changed

  • Extracted the scanner's path-filtering (Filters, the new IndexFilter) into src/scanner_filter.rs to keep scanner.rs under the 1000-line cap.

[0.12.2] — 2026-06-29

Patch release: blob and index formats are unchanged (RELEASE_MINOR stays 12), so no .basemind/ rebuild.

Changed

  • tree-sitter-language-pack 1.9.0-rc.45 → 1.12.0. The grammar bootstrap (ensure_grammars) now uses tslp's prefetch, which probes real on-disk loadability instead of the in-memory has_language registry — replacing basemind's hand-rolled DownloadManager::ensure_languages workaround for the pre-1.12 download() short-circuit. tslp 1.12's lock-free static get_language fast path also removes a global-mutex hop on the per-thread parser pool's hot path (tree-sitter is ~30% of scan time). Validated against the full harden harness: all 8 OSS repos pass with 0 extraction failures and scan times at or under baseline.

Patch release: blob and index formats are unchanged (RELEASE_MINOR stays 12), so no .basemind/ rebuild.

Fixed

  • Multiple concurrent sessions on one repo no longer break code-map navigation. fjall takes an exclusive directory lock, so only one basemind serve process can open a repo's index at a time; additional editor/agent sessions fall back to read-only and previously got an error (read_only_index_unavailable) from every fjall-backed tool — the reported "blocked / not responsive". find_references, find_callers, find_implementations, and call_graph now answer from in-RAM indexes built from the content-addressed blobs (which are concurrently readable), so unlimited sessions work against one repo: one writer on fjall, N readers on the shared blobs. outline / search_symbols / dependents / git tools already worked read-only.

[0.12.0] — 2026-06-29

Minor release: the document/RAG and web-crawl engines move to their renamed, MIT-relicensed crates. RELEASE_MINOR bumps to 12, so the blob + index formats are considered incompatible — .basemind/ is wiped and rebuilt from source on the next basemind scan.

Changed

  • Document tier now builds on xberg 1.0.0-rc.1 (was kreuzberg 5.0.0-rc.35) and the web-crawl tier on crawlberg 1.0.1 (was kreuzcrawl 0.3.0-rc.55). These are the renamed, MIT-licensed successors of the same engines, so the documents, memory, and crawl feature surfaces are unchanged. The Elastic-2.0 license exceptions for the old crates are dropped from deny.toml.
  • Document extraction migrated to xberg's async extract API. xberg 1.0 removed the synchronous extract_file_sync wrapper; the scanner's doc path now drives the async extract(ExtractInput, &ExtractionConfig) on a shared multi-thread runtime and reads the per-document result out of the new batch-shaped ExtractionResult.results.
  • arc-swap 1.9.1 → 1.9.2 via cargo upgrade. arrow-array / arrow-schema stay pinned to 58 to match lancedb 0.30's transitive arrow.
  • Web crawling now blocks private/loopback/link-local targets by default (SSRF protection inherited from crawlberg 1.0). web_scrape / web_crawl / web_map against 127.0.0.1, 10.0.0.0/8, 169.254.0.0/16, etc. are rejected with an SSRF-policy violation. A new crawl.allow_private_network config flag (default false) re-enables them for internal-docs hosts you control; the CRAWLBERG_ALLOW_PRIVATE_NETWORK env var is also honoured as a process-wide override.

Fixed

  • Status line no longer renders blank on Linux — build_basemind_line read file mtimes with BSD stat -f %m first. On GNU coreutils -f means "display filesystem status" and succeeds, printing a multi-line blob instead of failing, so the || stat -c %Y fallback never ran; under set -euo pipefail the blob's bare File word aborted the command substitution as an unbound variable, emptying the whole line. An epoch_mtime helper now tries GNU stat -c %Y first and falls back to BSD stat -f %m only when -c genuinely fails (macOS), used at both mtime call sites (#32).

Patch release: blob and index formats are unchanged (RELEASE_MINOR stays 11), so no .basemind/ rebuild. A tar security bump on the npm installer, a Homebrew-tap fix, and raised runtime floors.

Security

  • npm installer bumps tar 6 → 7.5.16 (GHSA-vmf3-w455-68vh) — the bundled extractor was on a tar line whose PAX size-override parsing differential allows file smuggling. The fix only exists in tar 7.5.16+, which is ESM-only and requires Node ≥ 18; install.js now pulls tar in via a dynamic import() at the extract site, and the npm package's engine floor moves to Node ≥ 22.

Fixed

  • Homebrew tap no longer breaks on Intel hosts — the generated formula's on_intel block called odie at formula-load time, which aborted every brew command that read the tap on an Intel Mac (poisoning bottle builds and installs for all formulae in the tap, not just basemind). The Apple-Silicon-only constraint is now expressed via depends_on arch: :arm64, evaluated at install time instead of load time.

Changed

  • Runtime floors raised — the npm package (Node ≥ 22, was ≥ 14) and the opencode plugin (Node ≥ 22, was ≥ 18); pip package requires Python ≥ 3.10 (was ≥ 3.8). Older runtimes are end-of-life.

[0.11.0] — 2026-06-26

Minor release: RELEASE_MINOR bumps to 11, so the blob and Fjall index formats are considered stale and every .basemind/ is wiped and rebuilt from source on the next basemind scan (one-time, intentional). The headline is a precomputed git-history index that turns the history MCP tools from full-walk-per-query into sub-millisecond lookups, plus an install fix that unblocks Apple Silicon Macs running an x86_64 shell, Node, or Python under Rosetta.

Added

  • Precomputed git-history Fjall index — commits_touching, find_commits_by_path, and symbol_history previously walked the commit graph on every call, paying an O(history) cost per query. They are now backed by a per-repo Fjall index built once at scan time and refreshed incrementally for new commits, so queries are O(result) sub-millisecond lookups. Posting lists are stored newest-first, keeping commits_touching O(n) in the returned commits rather than in total history depth.

Fixed

  • Apple Silicon installs under Rosetta no longer abort — uname -m, os.arch(), and platform.machine() all report the process architecture, so an x86_64 shell, Node, or Python running under Rosetta reports x86_64 on Apple Silicon hardware. All three installers (the shell launcher, npm install.js, pip downloader.py) matched the Darwin/x86_64 branch and aborted with "Intel macOS not supported", even though the native arm64 binary runs fine. They now probe a hardware-level signal the translation layer cannot spoof (sysctl -n hw.optional.arm64) and resolve aarch64-apple-darwin whenever that probe — or the reported arch — indicates arm64. Fixes #28.
  • Windows harden test compiles again — the new on-disk index-size measurement used std::os::unix::fs::MetadataExt::blocks() unconditionally, breaking the windows-latest test leg's compile. The block-count path is now cfg(unix)-gated with a len() fallback on other platforms.

Performance

  • Path-scoped history walks — commit_touches_path computed the full recursive tree diff of every walked commit just to test membership of one path, so a single history query over a deep monorepo could run for minutes (measured >5 min on a 242k-commit repo). It now looks up the entry at the exact path in the commit tree and each parent via gix lookup_entry (O(path depth) object reads, no sibling recursion), comparing (blob oid, mode). Semantics are identical to git log --full-history -- <path>; the same query drops to ~11 s worst case and ~2.5 s typical.

Changed

  • Dependencies — bumped the embedded rmux shells trio (rmux-client / rmux-sdk / rmux-server) from 0.6 to 0.7 and refreshed the lockfile for compatible updates. arrow stays pinned at 58 to match what lancedb resolves transitively.

[0.10.3] — 2026-06-25

Patch release: blob and index formats are unchanged (RELEASE_MINOR stays 10), so no .basemind/ rebuild. Three Windows-only correctness fixes surfaced once the comms suite and the full tool sweep run on the windows-latest CI leg, plus install/release hardening so a partial publish can no longer leave the plugin launcher unable to install.

Fixed

  • Windows full scan now produces /-separated index keys — the full-scan walker was optimized to feed Path::to_str() straight into the index without the \→/ normalization the incremental scan_paths path still did, so on Windows every nested file was keyed with backslashes (vendored\inner.rs) and forward-slash lookups missed — breaking all subdirectory queries (outline / search / references for any file not at the repo root). Normalize at the walker source; the extra allocation is Windows-only and never touches the Unix hot path.
  • Windows comms named pipe now isolates by comms_dir — comms_socket_path derived the Windows pipe name from the username only (\\.\pipe\basemind-comms-<user>), ignoring BASEMIND_COMMS_DIR, so every comms dir on a host collapsed onto one per-user singleton pipe. Parallel comms suites (each isolated to its own tempdir) collided — daemons cross-contaminated, one test's teardown killed another's daemon, and concurrent comms start races hung to the CI timeout. The pipe name now mixes in a hash of comms_dir, mirroring the per-dir Unix socket; production (which leaves BASEMIND_COMMS_DIR unset) keeps a single stable per-user broker. A timeout-minutes: 30 backstop on the CI test job prevents any future hang from blocking the queue-not-cancel main concurrency.
  • Windows comms start no longer hangs — the detached broker daemon inherited the launcher's stdout/stderr: on Windows CreateProcess runs with bInheritHandles = TRUE whenever stdio is redirected, leaking every inheritable handle into the child — including the pipe a parent captured via Command::output() (or serve's MCP stdio). The long-lived daemon then held the write end open, so the capturing parent never saw EOF and blocked until the daemon died. Unix is immune (Stdio::null dup2's /dev/null over the child fds and Rust sets CLOEXEC on its own pipes). spawn_detached_daemon now clears HANDLE_FLAG_INHERIT on its std handles before the detached spawn, so the daemon inherits none of them; the real-daemon comms E2E suite runs on Windows again.
  • Plugin launcher names an incomplete release instead of failing opaquely — when a pinned release is missing a platform binary or its checksums file (a partial publish, as 0.10.0 was), mcp-launch.sh died with a bare "download failed" / "could not fetch checksums" that the MCP client surfaced only as "failed to connect". It now reports the release as incomplete and tells the user to update the plugin (Claude Code: /plugin update) to a complete release.
  • Releases publish atomically — create_release made the GitHub release live before its binaries finished uploading, so a failed platform build (as 0.10.0's Linux legs) left it half-populated with no checksums, and the launcher's checksum-verified download then fails closed for every user pinned to it. The release is now created as a draft; a finalize_release job promotes it only once all four platform archives and the checksums file are present (and the npm / PyPI / Homebrew publishes gate on that finalize, since they download from the release). A failed platform build now leaves a hidden draft, never a live, broken release.

[0.10.2] — 2026-06-25

Patch release: blob and index formats are unchanged (RELEASE_MINOR stays 10), so no .basemind/ rebuild. Fixes the Linux release archive, which 0.10.1 shipped broken on a clean host.

Fixed

  • Linux release binaries: bundled libraries could not find their siblings — the archive bundles native .sos (libheif, libaom, …) into lib/, but only the main binary carried the $ORIGIN/lib rpath. A bundled lib with a sibling dependency (libheif.so.1 → libaom.so.3, both in lib/) had no rpath of its own, so on a clean host the loader failed with libaom.so.3: cannot open shared object file — even on basemind --version. The in-container packaging smoke missed it because the build container has those codecs system-installed. package-release.sh now sets $ORIGIN on every bundled lib so sibling-to-sibling deps resolve, verified by running the real release artifact on a clean glibc-2.28 host.

[0.10.1] — 2026-06-25

Patch release: blob and index formats are unchanged (RELEASE_MINOR stays 10), so no .basemind/ rebuild. This release completes the 0.10.0 distribution — it ships the Linux binaries and the npm / PyPI / Homebrew packages that 0.10.0 could not produce.

Fixed

  • Linux release binaries (x86_64 + aarch64) — the 0.10.0 Linux build linked ort's prebuilt ONNX Runtime with cargo-zigbuild, which fails because zig links LLVM libc++ while the prebuilt is built against GNU libstdc++. The Linux binaries now build inside the official manylinux_2_28 containers with native gcc. The prebuilt ort also references glibc 2.38 symbols (__isoc23_strtol/ll/ull, __libc_single_threaded); a small compatibility shim (scripts/glibc228_compat.c) backfills them so the binaries keep a GLIBC_2.28 floor (RHEL 8 / Debian 11 / Ubuntu 20.04+ / Amazon Linux 2023). Because 0.10.0's binary legs failed, its checksums and the npm / PyPI / Homebrew publishes were skipped; 0.10.1 carries the same source and completes every distribution channel.

[0.10.0] — 2026-06-24

Minor release: RELEASE_MINOR bumps 9 → 10, so the on-disk index version changes and every .basemind/ cache rebuilds from source on the next scan. This rebuild is one-time and harmless — the blob and index formats are unchanged.

Added

  • Agent shells: embedded rmux daemon + six MCP tools — spawn detached headless shell sessions via shell_spawn with optional cwd, env overrides, and title. Drive sessions with shell_send (write stdin), shell_capture (visible screen), shell_broadcast (multicast input), shell_list (enumerate with liveness), and shell_kill (terminate). basemind embeds the daemon (re-execs itself with --__internal-daemon — no external rmux binary). Sessions are long-lived across tool calls and driven headless via MCP.
  • Visual attach (Unix + Windows) — when [shells].visual is not headless, shell_spawn opens the session in a terminal tab/window attached to it via a hidden basemind --__internal-attach re-exec (macOS / Linux emulators or Windows Terminal wt.exe; basemind ships no external rmux binary). Presentation is best-effort — a spawn never fails just because no terminal could be driven — and the response's attach_command is returned for manual re-attach.
  • Comms-coupled session rooms (Unix + Windows, comms feature) — spawned children auto-join a session-scoped comms room via inherited BASEMIND_SESSION_ID / BASEMIND_PARENT_AGENT_ID / BASEMIND_AGENT_ID, enabling bidirectional parent↔child messaging and forming parent→child inheritance chains across agents.
  • [shells] configuration sub-tree — controls visual presentation mode (current / window / web / headless; default current = new tab in open terminal), terminal emulator choice (auto / iterm2 / terminal_app / windows_terminal / gnome_terminal / konsole / wezterm / alacritty / kitty / xterm), and session pty dimensions (default_cols / default_rows) and lifecycle (keep_on_exit). Requires --features shells.
  • Multi-agent comms orchestration — one serve now drives many named sub-identities over a single connection via a per-call as_agent parameter, backed by a per-identity broker-client registry parented to the orchestrator. New dm_send delivers a direct message into another agent's inbox over a private pairwise room; agent_register / agent_list advertise and discover the live roster. The whole surface is mirrored on the CLI (basemind comms … --as-agent, dm, room-for-path), and a multi-agent-room skill + a code-review-panel demo show the pattern.
  • Per-repo comms rooms + recency — agents auto-join a default room keyed by the repo, created on demand via get_or_create_chat_room_for_path; Global is repurposed for machine-wide ops coordination. Reads are recency-aware: room_history / inbox_read default to the last 24h (since_hours widens, 0 reads the full append-only log), each message carries age_secs, and room_list flags a room stale after 7 days of silence.
  • Windows support for agent comms + shells — the comms broker (named-pipe transport, singleton, signal handling) and the embedded rmux shell runtime now build and run on Windows, and the comms MCP tools are exposed there. --features full therefore ships agent comms and agent shells on every published platform — macOS, Linux, and Windows — not Unix only. A dedicated Windows CI job builds and tests the comms + shells surface.

Changed

  • Dependency refresh — kreuzberg 5.0.0-rc.30 → rc.35 (document-tier extraction stack) and rmcp 1.7 → 1.8. arrow stays at 58 (lock-step with lancedb 0.30). rmcp 1.8 deprecates MCP logging (SEP-2577); basemind keeps the capability for now since the status line and rescan progress depend on it.
  • Linux release binaries target glibc 2.28 (RHEL 8 / Debian 11 / Ubuntu 20.04+ / Amazon Linux 2023) — the two *-unknown-linux-gnu artifacts are built with cargo-zigbuild (zig as the linker), pinning the required glibc symbol floor without changing runtime behaviour (still dynamically linked). A CI objdump guard fails the build if a dependency pulls a newer symbol. macOS / Windows artifacts are unchanged.

Removed

  • Experimental A2A server (--features a2a) — the gRPC + JSON-RPC/SSE Agent-to-Agent server, its protobuf codegen + proto/, the basemind a2a serve command, the buf CI job, and the a2a-only dependency stack (tonic, prost, axum, axum-server, tower, tower-http, rustls, reqwest, subtle, rcgen, …) are removed. It was opt-in and never part of the shipped release surface; the maintenance cost (pinned codegen, held-back deps, a dedicated buf CI job) outweighed its experimental value. The comms agent-card shapes stay A2A-schema-aligned, so an HTTP front-end can still be added behind the CommsFrontend trait later without it.

Fixed

  • Comms daemons no longer pile up — a busy broker that missed a single liveness ping could be wrongly judged dead and have its socket unlinked-and-rebound by a new daemon, orphaning the original on a dangling socket that nothing reaped promptly; across many sessions dozens accumulated. Two guards: the liveness probe now retries before declaring a daemon dead (so a live-but-busy broker is not reclaimed), and a Unix socket-ownership watchdog makes any daemon whose socket was unlinked or replaced self-terminate within seconds instead of lingering as an orphan.
  • Comms daemon takes over a previous build on load — when a serve or CLI brings up the broker and finds the socket held by an older or protocol-incompatible daemon, it now asks that daemon to stop and spawns a current one in its place, converging the singleton on the newest binary; if the predecessor will not yield, it errors out clearly (naming the version + pid) instead of silently talking to an incompatible daemon — which is how the pre-0.10 skew surfaced as an opaque "connection closed". A same-version (or newer) daemon is reused, so concurrent sessions still share one broker.
  • Comms messages now expire — the broker prunes messages past a 7-day TTL on startup and hourly thereafter, so the user-global comms store cannot grow without bound. Recency-aware reads already hid old messages; this reclaims their storage. Room records and per-room sequence counters are left intact so read cursors stay monotonic.
  • Concurrent serve sessions no longer collide (#26, #27) — the editor plugin spawns one basemind serve per session, but the store write lock is single-holder. A contending serve now starts in a read-only mode (instead of exiting and handing the MCP client an opaque -32000), so its tools register and the session is usable: outline, search_symbols, list_files, dependents, workspace_grep, and the git tools answer from the in-RAM map and git. Fjall takes an exclusive lock on its index, so the call/reference tools (find_references, find_callers, find_implementations, call_graph) cannot read it from a second process — they now return a clear "held by another basemind serve" error rather than a misleading empty result. The lock-holding serve stays the sole writer; a read-only serve's rescan returns the same clear error. Lock contention fails fast with the clean lock message rather than the multi-GB busy-spin reported on 0.9.0 (#26).
  • Agent-shells hardening — shell_spawn now honours the [shells].enabled master switch, confines cwd to the repository root (rejecting .. / absolute escapes), threads the configured default_cols / default_rows into the spawned pty, validates BASEMIND_SHELLS_SOCKET on the client path, rejects carriage returns in env keys/values, and widens the loader-injection warning list (LD_AUDIT, DYLD_FALLBACK_LIBRARY_PATH). The shells and comms modules build cleanly on both Unix and Windows (see Added); a build without those features still excludes them entirely.
  • Status line recognizes pre-0.9 indexes — the basemind status line keyed its "scanned yet?" check on the fused .fm blob introduced in 0.9, so an index written by an earlier binary (split .l1/.l2 blobs) showed "scanning…" indefinitely though it was healthy. It now accepts both layouts and never double-counts the secondary blob layer.

Security

  • quinn-proto bumped to 0.11.15 (RUSTSEC-2026-0185) — fixes a remote memory-exhaustion vector via unbounded out-of-order QUIC stream reassembly, pulled transitively under --features full.
  • Known/tracked advisories carried this release (no fix available yet): bincode 1.3.3 unmaintained (via rmux-proto), memmap2 0.9.10 unsound pointer offset (via lancedb, documents/memory).

[0.9.0] — 2026-06-23

Minor release: RELEASE_MINOR bumps 8 → 9, so the on-disk index version changes and every .basemind/ cache rebuilds from source on the next scan. This rebuild is structural — the per-file extraction blob format changed from two files (<hash>.l1.msgpack + <hash>.l2.msgpack) to one combined frame (<hash>.fm.msgpack); any old split blobs left on disk are reclaimed by cache gc. A scanner hot-path pass cuts cold-scan wall time ~22%. Bumps kreuzberg to 5.0.0-rc.30.

Changed

  • Scanner: ~22% faster cold scans — two flamegraph-ranked wins. (1) Per-file Fjall index commits are now batched per rayon worker (256 files per commit), removing the ~14% of scan time worker threads spent serializing on Fjall's single write lock. (2) The L1 outline and L2 calls are fused into one content-addressed blob (<hash>.fm.msgpack), so the default eager-L2 scan does one open + atomic rename per file instead of two. Measured min-of-7 cold scans on a 6.7k-file repo: 2.68 s → 2.07 s; the per-file read-before-write index atomicity is preserved.
  • Bump kreuzberg to 5.0.0-rc.30 — picks up the upstream Tesseract tessdata-path fix, so the released binary resolves trained data at runtime instead of baking in the build runner's path. Resolves #12.
  • Dependency refresh to latest compatible (gix 0.85, criterion 0.8, …). arrow stays at 58 to match lancedb 0.30; the experimental a2a feature's gRPC deps are held pending a tonic-0.14 migration.

Fixed

  • Comms daemons no longer leak across sessions. A daemon orphaned by a dead session (reparented to pid 1) used to keep its socket + flock and never exit, so they accumulated indefinitely. The broker now tracks connected links + last activity and self-terminates after 30 min with no clients, via the normal drain path. A live client (even a quiet subscriber) keeps it alive. Unix-only.
  • Removed the document-tier troubleshooting workarounds from the README now that both upstream kreuzberg bugs are fixed (TLS-MITM in rc.29, tessdata in rc.30). Resolves #14.

Removed

  • The pre-0.9 split-tier blob format (.l1.msgpack / .l2.msgpack), superseded by the combined .fm.msgpack frame. cache gc reclaims any left behind by the schema-bump refresh.

[0.8.0] — 2026-06-22

Minor release: RELEASE_MINOR bumps 7 → 8, so the on-disk index version changes and every .basemind/ cache rebuilds from source on the next scan (one-time; the blob/index formats are unchanged, so the rebuild is harmless). Deepens the MCP surface — prompts, argument completions, logging, and progress notifications — and hardens the consumer experience: cleaner document-tier scans, a cold-start scan path agents can invoke, serve diagnostics, and a recovery runbook. Bumps kreuzberg to 5.0.0-rc.29.

Added

  • MCP prompts (prompts/list + prompts/get) — four reusable, parameterized workflows that teach a client to drive basemind structure-first: onboard-repo, trace-symbol, explain-file, review-working-tree.
  • MCP argument completion (completion/complete) — autocompletes prompt arguments from the in-RAM code map: trace-symbol's symbol against indexed symbol names, explain-file's path against indexed file paths (pure prefix scan, no store lock).
  • MCP logging + progress — logging/setLevel plus a rescan_complete log with scan counts, and start/done progress notifications on rescan when the client supplies a progress token.
  • Tool annotations on every MCP tool (read_only_hint / destructive_hint / idempotent_hint / open_world_hint) so clients (Claude Code et al.) can auto-approve read-only tools and only prompt for mutating ones.
  • Cold-start indexing for agents — a bm-scan command + basemind-scan skill that run basemind scan via the CLI (no MCP server required), so an agent can build the index when basemind reports "no index".
  • Recovery runbook — a bm-doctor command + basemind-doctor skill: diagnose the index, detect a stale lock via the .lock.meta holder pid, rebuild via scan, and reconnect the MCP server (client-specific). Plus basemind serve now logs its lifecycle (startup pid/version/view and the exact exit reason) to its stderr / the client's MCP server logs.

Changed

  • Bump kreuzberg to 5.0.0-rc.29 — picks up the TLS-certificate fix for model downloads behind corporate TLS-MITM proxies (hf-hub now uses the platform native-tls provider, honoring the OS cert store + SSL_CERT_FILE/SSL_CERT_DIR). Resolves #14.

Fixed

  • The document tier no longer counts non-extractable files as failures. A file tree-sitter doesn't recognize as code (e.g. an Erlang .app.src, which mime_guess maps to application/x-wais-source) is now skipped — and logged at debug — instead of inflating the failed count; genuine extraction errors on real documents still count as failures.

[0.7.0] — 2026-06-22

Minor release: RELEASE_MINOR bumps 6 → 7, so the on-disk index version changes and every .basemind/ cache rebuilds from source on the next scan (intentional, one-time). The headline is a first-class code-aware token-reduction surface (the compress/expand tools, per-call budgets, TOON encoding, behavioral output compression) plus code-grounded memory/skill governance (memory_audit + git-mined skill proposals). Also ships MCP tool annotations so clients can auto-approve read-only tools, and a sweep of CLI/MCP bug fixes.

Added

  • compress MCP tool — code-aware compression (tree-sitter structural elision + cheap lexical passes) with an honest before/after token report; expand pulls a single symbol's body back.
  • Per-call max_tokens budgets on high-volume tools (outline, search_*, find_references, workspace_grep, search_documents, …): rank-to-fit with an explicit truncation marker + cursor.
  • Opt-in TOON encoding (format:"toon") for high-volume list responses, and an opt-in lean tool surface (BASEMIND_MCP_LEAN) that advertises three wrapper tools instead of the full set.
  • Behavioral output compression (basemind compress-output/delta/checkpoint/detect-waste) with credential-safe, fail-open semantics, plus opt-in plugin hooks (Bash-output compression, read-cache deltas).
  • Code-grounded governance — memory_audit verifies stored memories against the live code map (structural-hash drift → stale, decay + 90-day auto-archive) and git-mined skill proposals (proposals_mine/proposals_list/proposal_accept/proposal_reject); additive provenance/verification fields on MemoryRecord and two new lazy Fjall keyspaces.
  • MCP tool annotations on all tools (read_only_hint/destructive_hint/idempotent_hint/ open_world_hint) so consumers (Claude Code et al.) can auto-approve read-only tools.
  • Real tokenizer-backed token counts (o200k under documents) in the compress report and telemetry, and instructive -32602 parameter errors via a Lenient<T> wrapper + serde aliases (accept query/pattern/needle/name/regex/… across the search tools).
  • Plugin integrations for Kimi Code and pi, and a Codex-readable marketplace manifest.

Changed

  • Bump kreuzberg to 5.0.0-rc.28.
  • Honest telemetry: est_tokens_saved now credits document/list/web tools instead of reporting 0, and search_symbols.total (total_is_partial) / list_files.limit_clamped / status.blob_count expose what was previously silent.
  • scan / rescan exit 0 when per-file read failures occur but the index was still updated (previously exit 2 masked a successful scan); scan --rev skips submodule gitlinks instead of failing; -q/--quiet now suppresses subsystem INFO/WARN logs.
  • cache clear gains a views:<name> selector to clear a single view without nuking all of them.
  • Slimmer tool descriptions to cut per-session schema tokens.

Fixed

  • Lock-contention errors now name the actual holder (serve/watch/scan/rescan) via a .lock sidecar instead of always guessing watch (#11).
  • query commands resolve absolute and ./-prefixed paths to the indexed key (#19); serving or querying a never-scanned named view now errors instead of silently returning empty (#18).
  • comms status gives an actionable "daemon not running — start with basemind comms start" message (#21); comms verbs no longer emit a false "--json has no effect" warning (#20).
  • git_cache_bytes documented as the disk layer only (#23); CLI lock / LanceStore-shutdown crashes and legacy comms front-matter decoding fixed.
  • Known upstream issues in the document tier (OCR tessdata path #12, model download behind a TLS-MITM proxy #14) are documented with workarounds; fixes tracked in kreuzberg and picked up on a later bump.

[0.6.3] — 2026-06-21

Patch release: schema unchanged (RELEASE_MINOR stays 6). Reworks the plugin MCP launcher so it no longer depends on npx/uvx at runtime — fixing intermittent start-up failures when several agent sessions (or the comms-monitor poll loop) launch basemind concurrently.

Fixed

  • Plugin launcher npx/uvx race — scripts/mcp-launch.sh previously exec'd npx basemind@<ver> (then uvx) as the runtime. npx stages into a shared, spec-hashed ~/.npm/_npx/<hash> directory, so two concurrent launches raced on it and failed with ENOENT … package.json; the binary was also never cached, so every launch re-resolved over the network and inherited node/python start-up cost plus lavamoat postinstall blocks. The launcher now has a single install method: download the checksum-verified prebuilt release binary once into a stable per-user cache (${XDG_CACHE_HOME:-~/.cache}/basemind/bin/<version>/), serialized with an atomic lock, and exec it directly on every subsequent launch. npx/uvx and the BASEMIND_LAUNCHER override are removed; set BASEMIND_BIN=/path/to/basemind to point at a local dev build. The npm/PyPI/Homebrew/cargo install channels are unchanged.

[0.6.2] — 2026-06-21

Patch release: schema unchanged (RELEASE_MINOR stays 6). Fixes the macOS binary so the npm/PyPI/Homebrew/cargo wrappers actually run — 0.6.0 and 0.6.1 shipped an unrunnable macOS binary.

Fixed

  • macOS binary SIGKILL on launch — the release packaging bundles non-system dylibs into lib/ and rewrites their @loader_path install names with install_name_tool, which edits the Mach-O in place and invalidates the linker's ad-hoc code signature. macOS then killed the process on first page-in (SIGKILL, "Code Signature Invalid") with no output, so the binary downloaded by every wrapper was unrunnable. The packaging now re-signs each dylib and the binary ad-hoc after the rewrites and verifies --strict before packaging.
  • pip/uvx downloader — a stale or partial ~/.cache/basemind/<ver>/ made os.replace() fail with Errno 66 (cannot overwrite a non-empty directory), wedging installs until a manual cache wipe. The downloader now clears a stale cache dir under its lock and retries the atomic move.

[0.6.1] — 2026-06-21

Patch release: blob/index/comms schema unchanged (RELEASE_MINOR stays 6), so no .basemind/ rebuild. Fixes the Windows release build that blocked the 0.6.0 wrapper publish.

Fixed

  • Windows build — the agent-comms substrate is built on Unix domain sockets (UnixStream/UnixListener, peer-cred auth) with no Windows analogue, but comms imported tokio::net::UnixStream ungated, so the --features full x86_64-pc-windows-msvc build failed to compile (since comms landed in 0.5.0). This failed the Windows binary job and held back the npm/PyPI/Homebrew publishes. The comms surface is now gated on all(feature = "comms", unix) — on Windows the binary compiles with comms absent; unix is unchanged. 0.6.0 published to crates.io only; 0.6.1 is the first complete cross-channel release of the 0.6 line.

[0.6.0] — 2026-06-20

Minor release: RELEASE_MINOR bumps 5 → 6, so the blob, Fjall-index, and LanceDB schema versions advance — the first basemind scan / serve after upgrading wipes and rebuilds the .basemind/ cache and the LanceDB store in place. Headline: an experimental, opt-in Agent-to-Agent (A2A) task server, plus scanner/query hot-path perf wins and release-pipeline hardening.

Added

  • Experimental A2A server — basemind a2a serve, gated behind --features a2a and intentionally excluded from the default and full builds and from the shipped release binary. One axum listener serves the A2A task protocol three ways — gRPC A2AService, JSON-RPC 2.0, and SSE streaming — with the agent card at /.well-known/agent-card.json. Hardened surfaces: bearer-token auth (constant- time compare, non-loopback bind refused without a token), TLS termination (--tls-cert / --tls-key, ALPN h2 + http/1.1), and push-notification webhook delivery behind a real SSRF guard (rejects loopback, RFC-1918, link-local, and the cloud-metadata IP; re-checks the resolved address and pins it on connect).
  • rescan levers (CLI + MCP) plus robust auto-rescan on an empty working-view index.
  • comms inbox_ack — advances the per-agent read cursor (idempotent, non-destructive; the shared append-only room log is untouched) — and richer message front-matter (ts_micros, tags, reply_to, seq, and an optional scope path/glob set for relevance filtering).

Changed

  • Scanner/query perf — cache the parse-timeout env lookup, pre-classify L1 captures, and defer the walk allocation in the scanner inner loop; serve outline from the in-RAM cache to cut query-path allocations; bounded-concurrent crawl indexing aligned with the kreuzberg parallelism pool.

Fixed

  • Atomic release publishing + resilient wrappers — a version-consistency gate across all shipped surfaces, an unconditional checksums job asserting every archive, needs/result == success guards on each publish job, and fail-closed retrying npm/pip installers with an atomic cache. Fixes the v0.5.0 broken-install regression (missing checksums file → every wrapper install failed).
  • comms daemon reconnects and respawns on a broken pipe instead of failing the request.

0.5.0 — 2026-06-20

Minor release: RELEASE_MINOR bumps 4 → 5, so the blob, Fjall-index, and LanceDB schema versions advance — the first basemind scan / serve after upgrading wipes and rebuilds the .basemind/ cache and the LanceDB store in place. Headline: basemind becomes a multi-agent communication substrate (scoped rooms + per-agent inbox) on top of its context layer, with split memory and cross-harness delivery.

Added

  • Agent-to-agent communication. A singleton, user-global broker daemon (its own Fjall store over a Unix socket, independent of any repo's exclusive index lock) hosts scope-joined rooms — an agent auto-joins every room covering its git remote, a path prefix, or global. Messages are two-tier: a front-matter envelope (subject · from · id) that room_history / inbox_read scan cheaply, plus a body fetched on demand by message_get. An agent's own posts are excluded from its inbox. New MCP tools agent_register, agent_list, room_create, room_list, room_join, room_leave, room_post, room_history, message_get, inbox_read, with full basemind comms … CLI parity.
  • Split memory. Memory gains an individual (per-agent) tier alongside the existing group (shared) tier, selected by a visibility parameter; agent identity resolves from BASEMIND_AGENT_ID → config → persisted .basemind/agent-id → anon.
  • Cross-harness delivery. The comms mandate and a "prefer basemind over grep/git/manual" directive ship to every harness via the MCP server instructions, a new basemind-comms skill, and the generated per-harness instruction files. SessionStart + UserPromptSubmit hooks inject unread front-matter on boot and per turn; a background monitor (~15 s) surfaces new messages while an agent works or idles (Claude Code).
  • Statusline comms segment. The basemind statusline now shows the agent's unread message count (bright when non-zero) and identity, gated on a running broker and TTL-cached so it stays cheap.

Changed

  • Schema bump: RELEASE_MINOR 4 → 5. The blob, Fjall-index, and LanceDB memory/documents schema versions advance, so the first basemind scan / serve after upgrading wipes and rebuilds the .basemind/ cache and the LanceDB store in place (the latter now guarded by a memory_schema_ver in meta.json so the memory-table column add never faults at query time). Stored memory is rebuildable scratch, not a source of truth — re-memory_put anything you want to keep, or expect it to be re-derived.

0.4.0 — 2026-06-19

Minor release: RELEASE_MINOR bumps 3 → 4, so the blob + index schema versions advance. The first basemind scan / serve after upgrading rebuilds the cache in place. Prebuilt binaries now ship --features full (96 document formats, OCR, embeddings, reranker, semantic search, web crawl, shared memory). Windows asset triple changed: x86_64-pc-windows-gnu → x86_64-pc-windows-msvc (ONNX Runtime ships no MinGW prebuilts); anyone hard-coding the old asset name must update. Intel macOS (x86_64-apple-darwin) is no longer shipped — macOS prebuilts are Apple Silicon (aarch64-apple-darwin) only; Intel Mac users build from source (cargo install basemind --features full).

Added

  • Full-feature prebuilt binaries via native per-platform runner. All prebuilt binaries (Homebrew, npm, pip, GitHub Releases) now ship --features full: 96 document formats (PDF, Office, email, HTML, archives, structured, source, markup), OCR (Tesseract), HEIC/AVIF (libheif), embeddings + reranker + NER, shared memory, and web crawl. First use downloads ML models over the network; binaries are larger. Replaced goreleaser with a new native per-platform CI runner pipeline.
  • Enforced binary checksum verification. All install paths (npm, pip/uvx, direct download via mcp-launch.sh) now verify the binary's sha256 against basemind_<version>_checksums.txt and fail closed on mismatch or missing checksum. (This was previously claimed in the README but not implemented.)

Changed

  • find_references and find_implementations now do real substring matching. Previously documented as substring matching but actually exact-only. Now genuinely substring (case-sensitive), so Foo::bar() and bar() both match name="bar".
  • Dropped Intel macOS (x86_64-apple-darwin) prebuilt binaries. macOS releases are Apple Silicon only. npm/pip/Homebrew install on an Intel Mac now fails fast with a clear message; build from source instead.

Fixed

  • SSRF denylist + redirect revalidation for web crawler. Added safeguards to prevent server-side request forgery on the web_scrape and web_crawl tools.
  • Configured embedder preset. Eliminates cross-process vector state wipe, improving performance and cache isolation.
  • Atomic memory writes. Prevents torn writes on concurrent memory_put across sessions.
  • Index panic on corrupted entries. Fixed a panic when the Fjall index contained malformed keys (now degrades gracefully).
  • Git blame robustness. Improved handling of edge cases (missing commits, detached HEAD, shallow repos).
  • Config layer precedence fixes. MCP > CLI > env > TOML > defaults now strictly enforced.
  • Hot-path allocation cuts. Reduced allocations in scanner, extract, and query paths; removed unnecessary String::from_utf8 round-trips and clones in the Fjall secondary-index scans.

0.3.0 — 2026-06-18

Minor release: RELEASE_MINOR bumps 2 → 3, so the blob + index schema versions advance. Unlike previous minor bumps, the cache is no longer hard-wiped — see "Durable cache refresh" below. The first basemind scan / serve after upgrading re-extracts in place.

Added

  • Full MCP↔CLI parity. Every MCP tool now has a basemind CLI subcommand, running the identical in-process tool code against the same on-disk index (read-only, no lock — safe to run alongside a live basemind serve). Human-readable output by default, --json for the raw structured response. New groups: basemind query (outline, symbol, search, references, callers, implementations, call-graph, grep, list-files, status, repo-info, dependents), basemind git (status, history, blame, diffs, churn, symbol-history), basemind memory, basemind web, and basemind telemetry.
  • Cache garbage collection + cleanup. New cache_gc (reclaim orphaned blobs no view references), cache_stats (on-disk size + orphan accounting), and cache_clear MCP tools, plus basemind cache gc|stats|clear CLI commands. GC runs automatically in the background on serve startup. Blobs are shared across views; GC only sweeps blobs no view's index references, under the store lock so it never races a scan.
  • Continuous background watch. basemind serve now watches the working tree and incrementally refreshes its index in the background by default (debounced), keeping queries fresh without manual rescan. Opt out with basemind serve --no-watch for very large repos / CI.
  • basemind-cli skill documenting the CLI surface, and a restructured README with two equal install paths (MCP plugin vs CLI + skill).

Changed

  • Durable cache refresh on schema bump. A schema/version bump now refreshes the content-addressed blob store in place (re-extract overwrites blobs at their stable content-hash path; background GC reclaims the remainder) instead of deleting .basemind/blobs/. No window where the expensive extraction cache is gone. The Fjall secondary index (derived, cheap) still rebuilds from blobs on its own schema bump.
  • Git-cache schema is now tied to RELEASE_MINOR so a release refreshes it consistently (it auto-rebuilds from git).

Fixed

  • Tightened token-savings estimate. The telemetry_summary "tokens saved" figure no longer scales with total corpus size (it previously credited ~5% of the whole repo per grep-style call). Baselines are now derived from the actual response payload, decoupled from corpus size.
  • Read-only store degrades gracefully on a schema bump. A CLI query run before the first post-upgrade scan now reads an empty index ("run basemind scan") instead of erroring, and never opens the stale Fjall index.

0.2.6 — 2026-06-18

Changed

  • Responsive two-line statusline with per-capability metrics. The status line now renders a context row (model · output-style · vim · dir · branch · context%) above the basemind row, since a custom statusLine replaces Claude Code's default and cannot sit below it. The basemind row breaks telemetry down per capability — searches, git, docs, memory, web — showing only buckets with activity today, alongside calls and estimated tokens saved. Width comes from $COLUMNS with full/compact/minimal tiers; override with BASEMIND_STATUSLINE and hide the context row with BASEMIND_STATUSLINE_CONTEXT=0.

0.2.5 — 2026-06-18

Added

  • PreToolUse guard hook drives basemind usage. A configurable hook (hooks/pre-tool-guard) reaches the agent when it reaches for Grep/Glob and points it at the matching basemind tool — the only Claude Code lever that can also cover subagents. BASEMIND_GUARD selects the mode: nudge (default, advisory once per session), redirect (deny with a pointer to the basemind tool), or off.
  • Full-capability awareness in the agent surfaces. The always-injected MCP get_info instructions and the basemind skill now describe the whole surface — code map across 300+ languages, full-text + semantic search, git intelligence, document RAG over 90+ file formats, shared memory, and web crawl — instead of a code-map-only framing. The README leads with the same capability set.

Fixed

  • Store::open no longer races on a just-released lock. acquire_lock retries the exclusive flock with a short backoff, fixing a macOS-only flake where a sequential open → close → open hit Locked.

0.2.4 — 2026-06-18

Added

  • Context-economy operating discipline shipped across every agent surface. The MCP get_info instructions, the basemind skill, the SessionStart hook, and the README now state one default workflow: these tools return paths, line numbers, and signatures rather than file bodies, so agents outline before reading, search_symbols / find_references / workspace_grep before grep, and rescan after edits. The SessionStart hook injects this every session instead of only nudging when the statusline is unset.

0.2.3 — 2026-06-18

Added

  • serve auto-scans a fresh repo on startup. When the working-view index is empty, the server kicks off an initial scan in the background (reusing the in-process rescan path, so it never contends for the Fjall lock). The agent no longer has to run basemind scan by hand — the statusline flips from "scanning…" to live counts on its own.
  • .basemind/ is now self-ignoring. The first time the store is created it writes .basemind/.gitignore containing *, so a user's repository never accidentally commits the machine-local index. An existing .gitignore is left untouched.

Changed

  • README redesign. Centered hero with a uniform badge row, a fenced statusline preview (replacing a broken image reference), and collapsible architecture / harness-setup sections. Clarified that registering the marketplace and installing the plugin are two distinct steps.

Fixed

  • Nightly hardening CI is green again. The hardening job built --features full (kreuzberg's libheif + tesseract stack) but only installed protoc, so the libheif-sys build failed. The job now installs the codec dev libraries and builds libheif 1.23.0 from source (cached across runs).

0.2.2 — 2026-06-17

Added

  • Claude Code plugin now auto-installs the basemind binary. The MCP launcher (scripts/mcp-launch.sh) tries version-matched cached binaries, then npx (npm package), uvx (PyPI package), and finally downloads the prebuilt binary from the GitHub release with checksum verification. Override the launch strategy with BASEMIND_LAUNCHER=auto|npx|uvx|download.
  • SessionStart hook pre-warms the binary and nudges statusline setup. The hook runs in the background on session start to ensure the first tool call isn't a cold install, and suggests /bm-statusline if the statusline isn't yet configured.
  • /bm-statusline slash command wires the statusline into ~/.claude/settings.json. Plugins cannot set the main statusline automatically; /bm-statusline is the one-step opt-in.

0.2.1 — 2026-06-17

Fixed

  • Claude Code slash commands now load. The /bm and /bm-stats commands were under .claude-plugin/commands/, which Claude Code ignores — plugin component directories must sit at the plugin root, and basemind's marketplace source is "./", so the plugin root is the repo root. Moved commands/ (and confirmed skills/) to the repo root; .claude-plugin/ now holds only the manifests + statusline. The Codex / Cursor / OpenCode trees keep their own in-tree copies (their plugin root is their own directory), kept in sync by scripts/sync-plugin-skills.sh.

Changed

  • Publish workflow no longer hard-fails on a missing Homebrew token. The GitHub-release-assets job skips the Homebrew tap publisher when HOMEBREW_TOKEN is absent (and runs it when present), so an optional tap update can't fail the binary release.

0.2.0 — 2026-06-17

Minor release — schema wipe (RELEASE_MINOR=2). The blob and inverted-index formats are versioned to the release minor, so the next basemind scan rebuilds .basemind/ from source. This is the intentional migration path; no user action beyond a rescan is needed.

Changed

  • Dependencies on crates.io. kreuzberg moved from the v5.0.0-rc.15 git pin to the published crates.io release =5.0.0-rc.18; the temporary allow-git exemptions in deny.toml are dropped. With no remaining git dependencies, cargo publish to crates.io is unblocked — basemind now ships on all four registries (crates.io, npm, PyPI, Homebrew) from a single tag.
  • README is now a positioning document. Rewritten from a code-map feature inventory into a full AI-context-layer overview: four pillars (Code, Documents, Memory, Web), a feature table mapping every MCP tool to its backend, a three-flavour quickstart, comparisons against grep / vector-only RAG / repo-map indexers / GitHub search, and a differentiators section. Sub-package READMEs (npm / PyPI / OpenCode) collapse to install stubs linking the root.
  • Aligned package metadata across all surfaces. One canonical description and a positioning-led 5-keyword set (mcp, agent-context, rag, code-map, tree-sitter) now ship identically across Cargo.toml, the npm / PyPI manifests, and every harness plugin manifest. GitHub repo description + topics updated to match.

Added

  • Redesigned Claude Code statusline. Bright 256-color palette (no more low-contrast dim text), a ◆ brand mark, a dedicated serve/scan freshness dot, and width-aware wide/narrow layouts. The file count now reads the L1 blob set directly (exact, deduped) instead of estimating from index bytes; scan recency reads the view index mtime; an empty repo renders an actionable run: basemind scan hint instead of a blank line. The intelligence row (documents / memory / web) appears when a LanceDB store is present.
  • Agent-facing skills + slash commands bundled into every plugin tree. The basemind routing skill and basemind-stats dashboard, plus /bm and /bm-stats slash commands, now ship inside .claude-plugin/, .codex-plugin/, .cursor-plugin/, and the basemind-opencode npm tarball — so an installed plugin teaches the agent how to use the tools without manual onboarding. A new scripts/sync-plugin-skills.sh (wired as a poly hook) keeps the canonical source and the per-harness copies in lock-step.

0.1.1 — 2026-06-15

Fixed

  • npm-package — npx basemind … (and equivalent npm install -g basemind invocations) used to silently fall through to whatever basemind already lived on $PATH because npm skipped creating the node_modules/.bin/basemind symlink when the bin: target was missing at install time (the native binary is downloaded by postinstall). The wrapper now ships a Node.js shim at bin/basemind.js that spawnSync's the downloaded native binary, so the symlink target exists at install time and npx routes correctly through the wrapper.
  • publish.yaml — re-enabled the Homebrew tap step now that HOMEBREW_TOKEN is on the path to being provisioned. Set the secret on the basemind repo before pushing the next tag; goreleaser will then auto-update Goldziher/homebrew-tap's Formula/basemind.rb.

0.1.0 — 2026-06-15

Initial public release. First minor wipes the schema (RELEASE_MINOR=1) so any pre-tag .basemind/ cache rebuilds on next scan — intentional.

Feature-complete code-map server with the kreuzberg document tier surface (reranker, keywords, NER, summarization, language detection, TOON output) and schema-driven config across TOML / CLI / MCP / env vars, distributed across all major coding-agent harnesses (Claude Code, Codex, Cursor, Gemini, Factory Droid, OpenCode, Copilot CLI, and the generic MCP path) plus npm / PyPI / crates.io.

Added

  • basemind scan — rayon-parallel scanner that indexes a workspace into content-addressed msgpack blobs (.basemind/blobs/) plus a Fjall-backed inverted index (.basemind/views/<view>/index.fjall/). Two extraction tiers ship: L1 outlines (symbols, signatures, imports, docs) and L2 call sites; L3 structural hash available for symbol-history diffing.
  • basemind serve — stdio MCP server (rmcp) exposing the full code-map and git-history tool surface (outline, search_symbols, find_references, find_callers, list_files, dependents, repo_info, status, symbol_history, working_tree_status, recent_changes, commits_touching, find_commits_by_path, diff_file, diff_outline, hot_files, blame_file, blame_symbol).
  • Dynamic 300+ language coverage via tree-sitter-language-pack. Hand-written .scm overrides ship for Rust, Python, TypeScript, TSX, JavaScript, Go. Other languages for which TSLP ships a vendored tags.scm (kotlin, csharp, swift, cpp, scala, solidity, lua, …) get best-effort symbol + call extraction via the fallback adapter that rewrites GitHub-standard @definition.* / @reference.call captures into basemind's @symbol.* / @call.* shape.
  • Schema-driven config across TOML / CLI / MCP / env vars. Rust types in src/config/ derive schemars::JsonSchema; the snapshot at schema/basemind-config-v1.schema.json is regenerated from those types and asserted by tests/config_schema.rs. Adding a config field lights up all four surfaces via #[command(flatten)] (clap) and #[serde(flatten)] (MCP params). Precedence: MCP > CLI > env > TOML > defaults, with per-field provenance tracking via src/config/source.rs.
  • Document tier — search_documents MCP tool over Lance-backed embedding index with kreuzberg ingestion. Per-query overrides on every documents.* and llm.* setting, plus entity_category and keywords_contains post-filters.
  • TOON wire format for MCP responses via documents.output.format = "toon" (or --documents-output-format toon). Round-trip parity with JSON asserted in tests/mcp_smoke.rs.
  • Language-aware ingestion. documents.language.{auto_detect, min_confidence, detect_multiple} flows into kreuzberg's LanguageDetectionConfig and the chunking tokenizer. ISO 639-3 codes (e.g. "fra") surface on FileMapDoc.detected_languages.
  • Cross-encoder reranker as a post-step on search_documents. Off by default; documents.reranker.{enabled,preset,top_k} opts in. Preset is validated upfront against kreuzberg::get_reranker_preset; reranked index is bounds-checked before reorder.
  • Keyword extraction (YAKE / RAKE) and named entity recognition at extract time. New tail fields on FileMapDoc (keywords, entities) with #[serde(default)] — blob-compatible with prior blob shapes (asserted by tests/schema_bump.rs).
  • Extractive + abstractive summarization via documents.summarization. Abstractive routes through liter-llm with the new top-level [llm] section (model in provider/model form, api_key, base_url, temperature, timeout, retries, max_tokens). NER backend llm now wires the resolved LlmConfig.
  • SecretString newtype + ApiKey enum (Literal | Env | Unset). Secrets mask to "<redacted>" in Debug / Display and across Serialize (including the toml→serde_json validation round-trip).
  • Real-OSS hardening harness (tests/harden.rs, ./scripts/harden.sh) — clones 8 upstream repos (ripgrep, tokio, typescript, react, django, requests, gin, ripgrep-shallow), exercises every MCP tool against each, and pins canary lower bounds (tokio: find_references("spawn") >= 200, django: find_references("get") >= 200, react: search_symbols("useState") >= 20, ripgrep-shallow truncation surfaces).
  • Schema sync to release version — RELEASE_MINOR in src/version.rs drives both INDEX_SCHEMA_VER and the blob SCHEMA_VER. Minor-release bumps wipe .basemind/ on next scan; patch releases stay compatible.
  • Distribution: brew install Goldziher/tap/basemind, npm install -g basemind, pip install basemind, cargo install basemind --locked. Precompiled binaries on GitHub Releases for {x86_64,aarch64}-{linux-gnu,apple-darwin} and x86_64-pc-windows-gnu.
  • Per-harness install matrix — manifests + skill bundle for every major coding-agent harness, all bumped in lock-step by task release:sync-version VERSION=…:
    • Claude Code — .claude-plugin/plugin.json + marketplace.json + statusline.sh (live one-line summary of the indexed map with true-color brand mark, freshness dot, and call/token-savings telemetry from .basemind/telemetry.jsonl). Install via /plugin marketplace add Goldziher/basemind then /plugin install basemind@basemind.
    • Codex CLI / App — .codex-plugin/plugin.json with full interface block (displayName, category Developer Tools, capabilities, defaultPrompt, brandColor). scripts/sync-to-codex-plugin.sh mirrors into a fork of the openai/plugins marketplace.
    • Gemini CLI — root-level gemini-extension.json with contextFileName and mcpServers. Install via gemini extensions install https://github.com/Goldziher/basemind.
    • Cursor — .cursor-plugin/plugin.json.
    • OpenCode — published as the basemind-opencode npm package. Install via { "plugin": ["basemind-opencode@latest"] } in opencode.json. Skills are bundled into the tarball; the plugin shim does dual-mode resolution (bundled-path-first, repo-root-fallback) so monorepo dev and npm install both work without duplication.
    • Factory Droid / GitHub Copilot CLI — reuse the existing .claude-plugin/marketplace.json per their published patterns.
    • Every manifest carries the same canonical user-facing description and crates.io-capped 5-keyword set (mcp, tree-sitter, code-map, scanner, indexer).
  • Release pipeline — .github/workflows/publish.yaml publishes on every v* tag: GitHub release assets (cross-compiled binaries via goreleaser + zig), crates.io, npm × 2 (basemind binary wrapper + basemind-opencode OpenCode plugin), and PyPI. Per-registry idempotent via existing-version detection; OIDC-driven trusted publishers on all four registries.

Changed

  • tree-sitter-language-pack: =1.9.0-rc.27 → =1.9.0-rc.45. Hierarchical data_extraction + 17 data formats; rc.40–45 are CI/codegen-only.
  • kreuzberg bumped to the reranker / LLM API surface (=5.0.0-rc.7 baseline → published rc covering reranker + LLM at publish time).
  • alloc-stdlib = "=0.2.2" pin lifted (no longer binding after lock re-resolve; single alloc-no-stdlib 2.0.4 in the tree).
  • src/mcp/types.rs + src/mcp/helpers.rs split into _documents.rs siblings to stay under the 1000-line cap.

Performance

  • Harden 8/8 green across ripgrep / tokio / typescript / react / django / requests / gin / ripgrep-shallow. All canaries pass. Per-repo scan times within baseline: typescript 21.7 s (81 324 files), tokio 0.2 s (859 files), django 2.5 s (7 061 files), react 2.2 s, requests 0.7 s, gin 1.0 s, ripgrep 4.0 s, ripgrep-shallow 0.16 s. All 25 MCP tools clean across all repos.
  • search_documents post-processing releases the store read-lock before blob I/O; ahash::AHashMap / AHashSet on the post-filter path.