Symbol-aware repo search
grep's symbol argument is backed by a lightweight symbol index
(internal/indexer). When the agent looks up a bare identifier — a function,
method, type, class, const or variable name — with grep {"symbol": "Name"},
the result starts with a ranked definitions: block, followed by the usual
case-insensitive full-text matches (the usages):
3 definitions:
internal/agent/searcher.go:31 type RepoSearcher type RepoSearcher struct {
...
42 matches:
internal/agent/llm_runtime.go:222: searcher RepoSearcher
...
Each definition line has the form path:line kind name signature.
Definitions are ranked exact name → case-insensitive exact → prefix →
substring. Queries that are not identifier-shaped (phrases, paths, regexes)
skip the index entirely and behave exactly like before.
The prompt guidance steers symbol lookups to grep with symbol first;
grep with a pattern stays the default for regexes, phrases and
non-symbol text. The two forms are exclusive: a call passes one or the other.
This lookup used to be a separate repo-search tool (granted to every agent
holding grep by the v7 manifest migration). Manifest v12 folded it into
grep; repo-search {"query": q} still works as an alias of
grep {"symbol": q}, and an empty symbol lists every file.
How the index works
- Backends are pluggable. The built-in backend extracts symbols with
per-language line patterns for Go, Python, and JavaScript/TypeScript
(
.js/.jsx/.ts/.tsx/.mjs/.cjs). Unsupported languages simply fall back to plain full-text search — the index never makes a search slower than grep. - Lazy, shared and cached. Nothing is scanned until the first symbol
lookup of a session (the TUI warms the index in the background at
startup). There is one index per workspace root per process, shared by
the TUI and every session, persisted at
<project>/.spettro/cache/symbols.idxand reloaded by the next process (oldersymbols.jsonandsymbols.gobcaches are removed). The cache is rewritten only when the index changed, never by a start-up on an unchanged tree. - Freshness. Only the first lookup of a process with no usable cache
waits for a full scan. After that a lookup answers from memory at once
(a few milliseconds on a 60k-file tree) and never waits for a re-scan;
instead:
- the files the symbol search's usage
grepmatched are re-checked by mtime and size (and re-parsed, or added, when they changed) before the definitions are listed. Every file defining the name contains it, so unless the usages were cut at 200 matches the definitions are current even for edits made outside Spettro a moment ago; - the files of the best 50 definitions are re-checked the same way, so a listed definition never points into a changed or deleted file;
- the agent's own
file-write/file-edittools queue the touched file for re-parsing before the next lookup; - a full re-scan (new, changed and deleted files) starts in the
background when the last one is more than 30 seconds old, or after a
bashcommand (foreground or background job) or a checkpoint rewind. So a definition can be missing only from a file changed outside Spettro that the usage grep did not reach (a name with more than 200 matches), until that background re-scan finishes.
- the files the symbol search's usage
- Bounded. Indexing stops at 100k source files or 10 seconds (reading
and parsing run in parallel), skips files over 1 MiB, and respects
.gitignorefiles the waygrepdoes (nested ones included; files above the project root do not apply, so a dotfiles~/.gitignorecannot hide a project's sources) plus the usual junk directories (.git,node_modules,vendor,dist,build, virtualenvs). Symlinks to source files are indexed under the link's path; symlinked directories are not followed. When the file cap or the time bound cuts the index short, the result says so, also when no definition was found (the definition may be in the part never indexed). - Usages. The matches after the definitions are a case-insensitive
literal
grepfor the name, so they skip ignored, binary and oversized files like anygrep, and stop at 200 (with the usual truncation note). An empty symbol lists the workspace's files asglob**does, capped at
