ask where, get exact lines
Two surfaces · one tree-sitter engine · 27 languages at runtime
Start · The seven tools · How it works · Proof · explore · Model · Docs
asciinema cast: docs/assets/grove_demo.cast — replay with asciinema play docs/assets/grove_demo.cast
Coding agents burn tokens and round-trips grep-ing and reading whole files to
answer where is this defined, what does it do, who calls it. grove answers each
with one symbol, by exact bytes — behind a stable id the agent reuses across
turns. It's not an LSP; it's the cheap syntactic layer beneath one.
Every answer, from either surface, comes back in the same shape:
javascript:routes/user.js#isLoggedIn@50
└── grammar └── path └── symbol └── line (1-based)
Pass that whole string to source to read the symbol's body, or to callers
to find its call sites.
60-second start
1 · Install
curl -fsSL https://raw.githubusercontent.com/Entelligentsia/grove/main/install.sh | shOne line — detects your platform, verifies the sha256, installs both
grove and grove-explore, and honors HTTP(S)_PROXY/ALL_PROXY/NO_PROXY.
Prefer Homebrew, npm, cargo, PowerShell, or building from source? →
Install.
2 · Wire it into your project
cd your-project
grove initgrove init detects your languages, fetches their grammars, pins grove.lock,
and registers grove with every coding agent it finds — Claude Code, Cursor,
Codex, Gemini CLI, Windsurf, VS Code — each at its own config path, plus a
steering note so the agent reaches for grove instead of grep. Scope it with
--agents claude-code,cursor,… (or all). → Setup.
3 · Ask your agent
Start a fresh session and ask a where / what / who-calls question —
"where is provision_project defined, and who calls it?" It routes through
grove, not grep. That's it: your agent now has structural sight.
Prefer a skill?
npx skills add Entelligentsia/groveinstalls grove as a cross-agent skill (Claude Code, Cursor, Codex, Cline, …) and self-installs the binary on first use if it's missing.
The seven tools
| Command | What it returns | |
|---|---|---|
| outline | grove outline <file> |
a file's definition skeleton (kind · name · parent · signature · id) |
| symbols | grove symbols <dir> --name <n> |
repo-wide symbol search — --name is exact, --name-contains for substring |
| source | grove source <id> |
one symbol's full source — no whole-file read |
| check | grove check <file> |
ERROR / MISSING nodes — post-edit syntax check (exit 1 if any) |
| callers | grove callers <name> -d <dir> |
call sites of a symbol, each with its enclosing function |
| map | grove map <dir> |
directory dependency graph: definitions + outgoing references, no bodies |
| definition | grove definition <name> / --at <f:l:c> |
go-to-def, by name or from a usage position |
Add --json to any command for the agent-facing shape. Full reference +
examples: Tools.
How it works
Every grove result carries a symbol-id — a stable handle the agent passes from one tool to the next:
outline a file to a skeleton of ids → source one id for its exact bytes →
callers that id for its call sites. One symbol at a time, by bytes — never a
whole-file read. Grammars load at runtime from a hosted WASM registry, so
adding a language is a registry entry, not a recompile.
- Token-cheap —
outlinea 1700-line file as a skeleton;sourceone symbol's body, not the file.mapreturns a directory's definitions + references in a single call. - Byte-precise & stable — the
symbol-idabove is exact and durable; pass it forward across turns instead of re-searching. - One engine, two surfaces — the same Rust core answers the CLI, the structural MCP server, and the delegate's inner loop.
- Runtime grammars — all 27 official tree-sitter grammars resolve from the registry; new languages need no recompile and no toolchain on your machine.
Not an LSP. grove is syntactic, not semantic: it speaks MCP (not LSP), parses (doesn't analyze), and locates (doesn't refactor) — no type inference, completion, rename, or type-resolved go-to-def. It's complementary to an LSP, sitting beneath where semantics begin. Full reasoning: Is grove an LSP? · Product vision:
VISION.md.
Proof
grove is measured in
is-grep-enough — a fair,
blind-judged comparison of three navigation regimes (text baseline, structural
grove, semantic lsp) given the same agent the same prompt across 50
tasks: 10 large, popular, grammar-backed repos × 5 rungs of climbing complexity
(locate a symbol → trace flow → recover an architecture). One variable — the
navigation capability.
Same answers, roughly half the context. grove ties on answer quality (grounding ~0.97, completeness ~0.99) while running ~2× leaner on tokens — widening to 2.8× on the hardest architecture traces — and reported honestly, including where it doesn't win.
The full curve — three findings, and the methodology
- Answer quality ties — and on the hardest traces grove is the more reliable one. Grounding ~0.97 and completeness ~0.99 across all three arms; grep is enough to be correct most of the time. But where text search drifts on a dense trace — the C++ L5 architecture trace drops baseline grounding to 0.80 — grove holds 0.97, because it cites the real syntax tree instead of guessing line numbers.
- grove runs leaner on context, and the lead widens with complexity. ~395K mean tokens vs 567K (lsp) and 780K (baseline) overall — roughly half the text-search context. By the hardest rung (architecture / binding-spine), baseline pushes ~1.5M tokens against grove's ~534K — 2.8× leaner (on the TypeScript L5 trace, 2.43M → 570K, 4.3× leaner) — while grove stays tightest on quality.
- Reported honestly, including where grove doesn't win. On trivial locate-one-symbol tasks (L1), grove's fixed structural-call overhead means it isn't the cheapest — plain text search is. grove pays off on the navigation that actually costs tokens: tracing flow, mapping a subsystem, recovering an architecture.
Token throughput isn't the billed bill — much of baseline's volume is cheap cache reads — but it is what drives context-window pressure and latency. Full methodology, per-repo data, blind judgements, and every raw transcript: is-grep-enough · live dashboard.
The optional second surface
Everything above is grove — the structural surface, and the one you want by
default. The seven tools all want a name. When you don't have one yet —
"which files handle billing?" — there is a second, optional surface:
grove-explore, a locator backed by a small model running on your machine.
A project registers one surface or the other, never both: running both puts eight tools in front of your agent with no rule for choosing between them.
grove init --as mcp (default) |
grove init --as mcp-llm |
|
|---|---|---|
| Registers | grove |
grove-explore |
| Tools | seven structural tools | one — mcp__grove__explore |
| How | tree-sitter, deterministic, no model | a small local model sweeps the tree |
| Ask it | a name you already know | "where is X?" before you know the file |
| Costs | milliseconds, no inference | one local inference run per call |
Switching modes swaps the registration — grove init strips the surface you
left. Start with --as mcp and stay there unless you find yourself sending
the agent hunting for files it can't name.
grove-explore — the code locator
mcp__grove__explore is one tool your agent calls with one narrow
"where is X" question. A small model running on your machine drives a short,
bounded tool-calling loop — grove's own structural tools plus glob/grep/read —
and replies with location lines only, most relevant first:
where are admin routes and admin middleware defined?
javascript:routes/admin/adminMiddleware.js#requireAdmin@22
javascript:routes/admin/adminMiddleware.js#checkNotImpersonating@58
javascript:app.js#adminMiddleware@106
javascript:app.js#adminApiUsers@108
javascript:app.js#adminApiAuth@117
javascript:app.js#adminJobsMount@517
✓ done · 4 turns · 44,773 tok · 19.3s
It locates; it does not explain. Every path is checked against the filesystem before it is returned, so hallucinated locations are dropped. Your agent then reads the cited lines itself — the line numbers are exact, so it reads a window at that offset rather than the whole file.
The outer agent never sees the inner tool calls, and never spends its own context on them. That is the trade: one local inference run in exchange for the sweep your expensive agent would otherwise do in its own context.
Keep each question single-focus. It is a locator, not a research agent — ask a few targeted questions and synthesize the results yourself.
Setting it up
grove init --as mcp-llm # registers grove-explore, opens the config screen grove-explore config # revisit settings at any time
The config screen probes the usual local ports, shows which engines are up and how many models each is serving, and fills in the endpoint and model for you. You choose which tools the delegate may use, and whether to record sessions.
When the provider is down
grove-explore starts anyway and says so where your agent can read it: the
explore tool returns an actionable error naming the endpoint and the fix,
rather than the server dying at startup and leaving your agent with a bare
transport error it cannot explain. There is no silent fallback to the
structural tools — if you registered the locator, you get the locator or a
reason why not.
The model — grove-explore-base
The locator model, published as GGUF. It is an off-the-shelf Qwen3.5-4B base — self-converted and quantized, adopted as grove's delegate. Not a grove fine-tune; the model card says so plainly, with full lineage.
| Quant | Size | Coverage (n=347) | ollama tag | Role |
|---|---|---|---|---|
| Q4_K_M | 2.78 GB | 80.6 | q4_k_m |
memory-lean serving default |
| Q8_0 | 4.6 GB | 82.1 | q8_0 |
canonical eval baseline |
Answer-sheet coverage on the grove explore holdout — 347 episodes across 9 pinned real-world repos spanning 9 languages, via is-grep-enough.
Serve it with llama.cpp:
llama serve -hf entelligentsia/grove-explore-base-GGUF:Q4_K_M
Or with ollama (0.32 or newer):
ollama pull bonigopalan/grove-explore-base:q4_k_m
Serve with thinking ON. With thinking off these models emit degenerate empty tool-calls. Recommended context 24,576, temperature 0. ollama returns chain-of-thought in a separate
reasoningfield and the answer incontent.
Then run grove init --as mcp-llm (or grove-explore config) and pick the
running engine — the screen fills in the endpoint and model for you.
Weights live on HuggingFace and ollama; model cards, Modelfiles, the quant matrix and provenance are in grove-models.
Seeing what the delegate did
Delegation is only trustworthy if you can audit it. Turn on Tap — a toggle
in grove-explore config, or just run grove-explore tap, which flips it on
for you — and every session is recorded to a per-session JSONL trace under
.grove/traces/.
grove-explore tap # enable tracing + browse grove-explore tap --no-enable # browse without changing the config
Drill session → call → turn: which tools the delegate chose, what came back,
token usage and wall time per turn, and the citations it finally returned. It
refreshes live, so you can watch a session as it runs. Retention keeps the last
trace_retain sessions (default 50).
grove config/grove tapstill work as deprecated forwarding shims togrove-explore config/grove-explore tap. Use thegrove-explorespelling directly; the shims print a one-line note and forward.
Languages
27 out of the box — one engine, grammars loaded at runtime from the hosted WASM registry:
| Agda2 | |||||||
| Embedded Template2 | JSDoc2 | ||||||
| CodeQL2 | Regex2 | Verilog2 |
2 minimal profile — core tools only (callers/definition degrade);
full profile = all tools. <kbd> = no official logo. Profiles are data, not
compiled in. See Languages & grammars.
Use grove as a Rust library
Embed the engine directly — no CLI, no subprocess
The same engine ships as a standalone crate, grove-core, so you can embed
grove's structural queries directly in Rust. The grove binary is a thin
clap + MCP shell over it. The crate is clap-free; grammars still load at
runtime from the WASM registry, so nothing is compiled in.
On crates.io as grove-cst — CST for the concrete syntax trees tree-sitter
builds (grove-core is taken by an unrelated crate). Alias it so imports stay
use grove_core::…:
# Cargo.toml [dependencies] grove_core = { package = "grove-cst", version = "0.4" }
use std::path::Path; use grove_core::{init, ops}; fn main() -> anyhow::Result<()> { let project = Path::new("."); // 1. Provision grammars for this project's languages — fetches any missing // grammar into the OS cache and pins grove.lock. Run once. for action in init::provision_project(project, false)? { println!("provisioned: {action}"); } // 2. Query — grammars resolve from the cache. Every definition under `src/`, // gitignore-aware, as typed results. for s in ops::symbols(&project.join("src"), None, None, false, false)? { println!("{} {} — {}:{}", s.kind, s.name, s.file, s.line); } Ok(()) }
(Offline? Set GROVE_REGISTRY=<dir> to resolve grammars from a pinned registry
and skip the fetch — see core/README.md.)
The consumer surface is the ops module — the same seven
tools (outline, symbols, source, check, callers, map, definition),
returning typed Symbol / Defect / CallSite / FileMap values (re-exported
at the crate root). init::provision_project is the grammar-provisioning entry
point behind grove init. Crate overview and full API surface:
core/README.md · core/src/lib.rs.
Documentation
| Guide | What's inside |
|---|---|
| Install | curl · Homebrew · npm · cargo · from source · the agent skill |
| Setup | grove init, --as mcp|skill|both|mcp-llm, --agents, what it writes, offline/dry-run |
| Languages & grammars | the WASM registry, fetch/lock, where grammars live, profiles |
| Tools | the seven tools, --json, symbol-id, examples |
| MCP server | grove serve, .mcp.json, steering, error model |
| grove-models | the delegate models — cards, quants, Modelfiles, provenance |
| grove-core | embed the engine in Rust — no CLI, no subprocess |
| Roadmap & repo layout | what's not done yet, source map |
| FAQ | Is grove an LSP? and other positioning questions |
Also: VISION.md (product vision) · CHANGELOG.md
(releases) · eval
is-grep-enough +
live dashboard · registry
grove-registry · Homebrew
tap homebrew-grove.
Status
Pre-1.0. callers/definition are name-based (no receiver-type resolution); 12
languages ship a minimal profile (core tools only); no incremental reparse yet.
Details and the rest of the roadmap: Roadmap.


