ADR-XXX: Replace qmd with llm-backed commonplace search
Status
Proposed
Context
Commonplace currently uses qmd as an optional semantic-search layer. The connect skill uses semantic search as a secondary discovery path after directory and index scanning, mainly to find vocabulary-mismatched body/source matches. The markdown library remains the source of truth; qmd is derived operational state.
qmd has accumulated operational friction disproportionate to that role:
- qmd writes to
~/.config/qmd,~/.cache/qmd, andnode-llama-cpplocal build directories under the global npm package install. - Codex
workspace-writesessions need extra writable roots for qmd caches and build locks. - GPU-dependent embedding/reranking does not fit Codex shell sandboxes; the MCP daemon workaround moves work outside the sandbox but introduces a separate always-on process.
- qmd issue #343 means custom
--indexdoes not reliably carry through HTTP MCP, forcing default-index mirroring. - The HTTP MCP server appears single-client per process, so parallel Codex sessions require separate qmd MCP processes on separate ports.
The replacement should preserve the useful part of qmd: collection-scoped semantic recall over markdown. It should not preserve qmd-specific URI schemes, daemon requirements, GPU assumptions, or MCP behavior.
The evaluated options were:
llmCLI with SQLite storage and pluggable embedding backends.sqlite-vecwith a Commonplace-owned Python driver.- Reseek, checked and excluded because it is a hosted second-brain product rather than a local substrate.
Decision
Replace qmd with an llm-backed Commonplace search interface.
Commonplace will expose its own stable commands instead of asking skills to invoke raw llm commands directly:
commonplace-search-update
commonplace-search-query
commonplace-search-status
The implementation may shell out to llm in v1. The public contract is the Commonplace command surface, not the llm CLI shape.
Search configuration will be substrate-neutral. Replace qmd-collections.yml with semantic-search.yml, keeping the existing collection shape:
collections:
notes:
path: /absolute/project/kb/notes
pattern: "**/*.md"
Derived search state will live under a project-local directory such as .search/ and will be rebuildable.
Stable record IDs will use repo-relative paths plus a content unit:
kb/notes/context-engineering.md#file
Collection is stored as metadata, not duplicated in the ID:
id: kb/notes/context-engineering.md#file
collection: notes
path: kb/notes/context-engineering.md
unit: file
content_hash: sha256(...)
Whole-file embeddings are the v1 default. Chunking is deferred until a benchmark shows whole-file retrieval is too coarse.
sqlite-vec remains the fallback if llm's schema, CLI behavior, or metadata handling become load-bearing constraints.
Consequences
Positive:
- Removes qmd's GPU, MCP daemon, and node-llama-cpp build-lock problems from the happy path.
- Supports parallel agent sessions through normal CLI/SQLite read behavior rather than MCP session ownership.
- Keeps the source of truth as markdown and the index as rebuildable derived state.
- Gives Commonplace a substrate-neutral search interface that can later move from
llmtosqlite-vecwithout changing skills. - Lets embedding generation be selected by environment: API-backed, local CPU model, or another
llmplugin.
Negative:
- Adds a Commonplace-owned search driver and tests.
- Introduces a dependency on
llmand at least one embedding backend/plugin. - Requires benchmarking because qmd's recall may differ from the selected
llmembedding model. - Whole-file embeddings may be too coarse for long sources; chunking may be needed later.
- qmd-related docs, install instructions, scaffold assets, and skill instructions need migration.
Implementation Notes
Migration should happen in phases:
- Prototype in
kb/work/semantic-search-replacement/and benchmark against qmd on real queries. - Add
commonplace-search-*CLI commands behind tests. - Update
commonplace-initto generatesemantic-search.ymland ignore.search/. - Update connect skill, report templates, and reference docs to use substrate-neutral semantic search language.
- Remove qmd MCP/setup guidance from the happy path after the new search path passes the benchmark.
The qmd path can remain temporarily as a fallback during validation. It should not remain as a long-term parallel substrate unless a concrete recall regression requires it.
Open Questions
- Which embedding backend should be the documented default: OpenAI via
llm, localllm-sentence-transformers, or both? - Should
llmand the embedding plugin be package extras, install-doc prerequisites, or checked at runtime with actionable errors? - Where exactly should derived search state live:
.search/llm.db,.index/search.db, or another path? - What benchmark threshold is sufficient to declare qmd replaced?
- If chunking is needed, what stable chunk-boundary rule should be used?
References
kb/work/semantic-search-replacement/README.mdkb/work/semantic-search-replacement/llm-vs-sqlite-vec.mdkb/work/semantic-search-replacement/plan-replace-qmd-with-llm.md- qmd issue #343: https://github.com/tobi/qmd/issues/343