brainchat

CI Ruby >= 3.2 License: MIT

Chat with your knowledge base from the terminal, with citations that resolve to real files. brainchat is a Ruby AI CLI that answers questions over a "knowledge-brain" vault (notes, ADRs, plans, commit history, docs) and labels every claim with the exact chunk it came from.

$ brainchat ask "what did we decide about hitgate caveat timing"

Ship the two-channel honesty check first, then the self-indexed caveat [1][2]. The
rationale: publishing the caveat first would anchor the evaluation story on a limitation
with no third-party evidence behind it [1].

Sources:
  [1] /vault/knowledge-brain/docs/adr/0012-caveat-investment-timing.md:1-34 (repo-docs/hitgate)
  [2] /vault/knowledge-brain/memory/caveat-timing-note.md:3-9 (memory/knowledge-brain)

How it works

brainchat deliberately does not reimplement retrieval. It composes two tools that each do one thing well:

  1. Your question goes to rag-query, the hybrid BM25 + cosine retrieval CLI that hitgate's regression gate benchmarks. brainchat shells out with Open3.capture3 and an argv-array (never a shell string) and treats the JSON output as the retrieval contract.
  2. The retrieved chunks are injected into a RubyLLM chat as numbered context. The system prompt requires [n] citations, and the CLI prints the [n] -> path:lines mapping after the answer, so every citation resolves to a file and line range you can open.

Provider-agnostic: Anthropic, OpenAI, or fully local with Ollama.

Requirements

  • Ruby >= 3.2
  • The rag-query executable from hitgate on your PATH (or point BRAINCHAT_RAG_QUERY_BIN at it), plus an indexed vault. rag-query is configured through its own RAG_* environment variables (e.g. RAG_INDEX_DIR), which brainchat passes straight through.
    • --scope-repo all needs hitgate at or after #134. Earlier versions turned the all sentinel into "no preference", which re-enabled cwd auto-scoping; since hitgate's source roots default to the working directory, every query got scoped to a repo named after whatever directory you were standing in and came back empty with exit 0. If brainchat ask finds nothing in an index you know is populated, check this first.
  • A provider: ANTHROPIC_API_KEY or OPENAI_API_KEY, or a local Ollama.

Install

Not on RubyGems yet. Build from source:

git clone https://github.com/LucasSantana-Dev/brainchat
cd brainchat && bin/setup
gem build brainchat.gemspec && gem install ./brainchat-0.1.0.gem

Or run it without installing: bundle exec exe/brainchat ask "...".

Usage

brainchat ask "what did we decide about hitgate caveat timing"
brainchat ask "how does the reranker fall back" --top 8 --scope-repo all
brainchat ask "..." --provider ollama --model qwen3:4b --assume-model-exists  # fully local
brainchat ask "..." --no-stream    # print the answer only when complete
brainchat ask "..." --rerank       # cross-encoder reranker (slower, better ranking)
Flag Purpose
--top N number of chunks to retrieve (default 5)
--scope-repo a,b restrict retrieval to named repos, or all to disable cwd scoping
--rerank enable the cross-encoder reranker (default: fast mode)
--provider NAME anthropic, openai, ollama
--model ID model id (default: RubyLLM's configured default)
--assume-model-exists skip model-registry validation, needed for local Ollama models
--no-stream print the complete answer at once instead of streaming

Configuration

Variable Purpose
ANTHROPIC_API_KEY Anthropic provider key
OPENAI_API_KEY OpenAI provider key
OLLAMA_API_BASE Ollama endpoint (default http://localhost:11434/v1)
BRAINCHAT_RAG_QUERY_BIN path to the rag-query executable (default: rag-query on PATH)
RAG_INDEX_DIR and other RAG_* hitgate index configuration, passed through to rag-query

Under the hood

Built around Ruby idioms, not ported line-for-line from anything:

  • Open3.capture3 with an argv-array, never a shell string. The question travels as one bare argv entry, so a query containing $(...), backticks or ; is inert data. The spec suite proves this with a shell-injection marker test that fails if the query is ever interpolated into a shell.
  • Data.define for the Chunk value object: immutable, keyword-constructed, with a #location helper that renders path:start-end (or bare path for line-less chunks like git commits).
  • RubyLLM wired explicitly. RubyLLM 1.16 reads no provider env vars on its own, so Chat.configure feeds it ANTHROPIC_API_KEY / OPENAI_API_KEY / OLLAMA_API_BASE itself.
  • Streaming by default via chat.ask(prompt) { |chunk| ... }; --no-stream prints the complete answer at once.
  • Thor for CLI dispatch.

Development

bin/setup
bundle exec rake   # rspec + rubocop

Specs stub both boundaries: the retriever is tested against a fixture of real rag-query JSON output (no subprocess), and the chat against a stubbed RubyLLM (no network).

  • hitgate: the retrieval engine and regression gate brainchat queries.
  • leakless: rule-driven secret scanner with redact-always AI triage, same Ruby-idiom-first approach.

License

MIT. See LICENSE.txt.