Documentation
Everything you need to run trie on a project: the five-minute quickstart, how each index works, what the commit gate enforces, and how to wire it into an agent, a CI runner, or a team.
1. What trie is
trie maintains one index with two halves in your repo, as plain Markdown, and keeps both honest with pre-commit gates.
-
Meaning (
triefacts/): one Markdown file per source file, one section per symbol, describing what each thing does. Generated by an LLM. Regenerated only when the source actually changes. Cascade-aware. Verified at commit time. -
Intent (
triefacts/triediffs/): one note per changed symbol. Recorded when the change is made. Enforced at commit time. Archived per commit as a digest.
trie never generates or edits your source code. You (or your agent) own every code change; trie owns the record of what it means and why it happened.
There is no server, no hosted index, no service. If you can clone the repo, you have all of it.
Requirements:
- Python 3.11+ and
uv -
ripgrep
(
rg) on PATH -
An Anthropic API key in
ANTHROPIC_API_KEYfor prose generation. The gates, graph, and queries run offline. - Languages indexed today: Python, TypeScript/TSX, JavaScript/JSX, Go, Rust, C, and Lua.
- Optional per-language language server for type-aware reference precision (method/member dispatch) — discovered on PATH, falls back to tree-sitter-only when absent.
2. Quickstart
Five minutes from install to a gated commit.
Install
$ uv tool install git+https://github.com/computer-reinvention/trie
$ trie --version
Initialise
$ cd your-project
$ export ANTHROPIC_API_KEY=...
$ trie init
init does four things:
- writes
trie.toml - adds
.trie/to.gitignore - builds the symbol graph
- offers to install the pre-commit hook
Tighten [scope] in trie.toml if you only
want trie pointed at part of the tree:
[scope]
include = ["src/**/*.py"]
exclude = ["**/tests/**", "**/__pycache__/**"]
Preview the bill
$ trie plan # ranked worklist + cost estimate (free token-count calls)
$ trie plan --offline # worklist only: no key, no network
Files are ranked by LOC × public symbols. The
estimate accounts for prompt caching; real bootstraps usually come
in under it.
Bootstrap the meaning index
$ trie sync --file src/some_module.py # one file, cheapest first taste
$ trie sync --limit 10 # top-ranked 10 files, sample the quality
$ trie sync --budget 5.00 # or spend at most $5
$ trie sync # or commit to the full plan
Preview before paying with trie sync --dry-run. It
regenerates to .trie/preview/ and prints unified
diffs.
Wire up your agent (optional)
$ trie setup # auto-detects opencode / claude-code / cursor / ...
Day-to-day
$ trie sync # regenerate exactly what drifted (+ callers)
$ trie patch create src/auth:require_auth -n "why it changed"
$ git commit # the gate takes it from here
That's the loop: init → plan → sync → commit.
Everything else is variations.
3. The meaning index: triefacts
One source file maps to one triefact under
triefacts/<source path>.md. Given:
# src/slugify.py
def slugify(text: str, max_len: int = 60) -> str:
"""Make a URL-safe slug."""
...
trie sync writes triefacts/src/slugify.md:
---
source: src/slugify.py
defines:
- { kind: function, qualified_name: "src/slugify:slugify", lines: 4-8 }
incoming_refs: 2
---
<!-- trie:section symbol=src/slugify:slugify fingerprint=9f3c… body_fp=8c9b… -->
## `slugify(text: str, max_len: int = 60) -> str`
Normalizes text to a URL-safe slug: strips accents via NFKD,
lowercases, collapses non-alphanumerics to single dashes, trims
to `max_len` without leaving a trailing dash.
<!-- trie:end -->
Anything you write outside the sentinel pairs is yours: notes,
warnings, ADR links. Regeneration preserves it byte-for-byte.
What keeps it true
"Docs per file" rots on the first refactor. trie's guarantees are mechanical:
-
Fingerprints. Each section sentinel carries a
hash of the source symbol and a hash of the prose body. Hashes
are whitespace and comment normalized, so formatting churn never
triggers regeneration.
trie verifycompares both directions offline: source that outran its prose, and prose that was hand-edited inside a generated section. Drift is a build break, not a TODO. -
The cascade. Change
slugify()and the reference graph knows which callers describe it. Their prose regenerates in the same pass. Hub symbols (>20 inbound references by default) cap the fan-out so one edit can't invalidate the world. -
Stale warnings on read. If prose is momentarily
behind the source (mid-session, before a sync), every read
surface says
⚠ STALE PROSE … run trie syncinstead of serving outdated text as truth. -
Scoped regeneration.
trie synctouches only stale sections plus their cascade.trie sync --graph-only(run automatically by the agent turn hook) rebuilds the symbol graph without LLM calls. Fresh clones andgit pullnever auto-spend.
Ownership contract
Generated sections (between trie:section /
trie:end sentinels) belong to trie.
Everything else in the file belongs to you and
survives regeneration verbatim. If verify reports a
hand-edit inside a generated section, move the prose above or
below the sentinels and re-run trie sync.
Editing triefacts directly is not recommended: the format is Markdown, but each generated section carries hashes and inbound/outbound reference counts that trie owns and will regenerate if tampered with. The space between generated sections, however, is preserved byte-for-byte — drop notes, warnings, or ADR links there with your normal editor and they become queryable context alongside the generated prose.
The front door: trie as a wiki
trie sync also regenerates
triefacts/README.md, a deterministic, LLM-free index
that turns the triefact tree into a browsable wiki: the
most-referenced public symbols (linked, role-tagged, with
one-liners) plus a per-directory table of contents. Browsing the
tree on GitHub starts there. Regenerate it on its own with
trie index.
4. The intent index: notes and digests
Code changes say what. They rarely say why. trie makes the why a first-class, enforced artifact.
The flow
# 1. Edit source however you like. trie never touches code.
# 2. Record why, per touched symbol (one or two sentences):
$ trie patch create src/slugify:slugify \
-n "Trim trailing dashes: slugs ending in '-' broke the CDN cache key"
# 3. Seal the notes (generates nothing):
$ trie patch apply -N "Fix CDN cache misses caused by malformed slugs"
- Removed symbols are noted with
--gone. -
Agents do the same through the
patch/batch_patchtools. -
patch applyrequires a real session note when more than one symbol is pending. Junk like "fix" or "wip" is rejected, because that note becomes the title of the commit's digest.
The digest
At commit time, the hook fuses the recorded notes (stated intent) with the triefact prose deltas (observed effect) into one immutable file:
## Fix CDN cache misses caused by malformed slugs · 2026-07-25 (parent bdadb22)
<≤120-word narrative synthesized from the notes and the prose diff>
### Changes
~ src/slugify:slugify · "…collapses non-alphanumerics…" →
"…trims to max_len without leaving a trailing dash."
+ src/slugify:_strip_accents · "Strip accents via NFKD normalization."
-
One file per commit under
triefacts/triediffs/.TRIE_DIFF.mdat the repo root symlinks to the latest. - In a PR, each digest appears as a brand-new file: pure additions, never a diff of a diff.
-
The
triediff-commentGitHub workflow (installed bytrie setup) comments every digest a PR adds onto the PR itself, so reviewers read the intent before opening a single file diff. - Amending a commit rewrites its digest instead of duplicating it.
- No API key? The narrative degrades to the deterministic evidence. The commit is never blocked.
Querying intent
$ trie read src/slugify:slugify --history
# …prose, callers, callees…
# history (2)
# 2026-07-25 · ~ src/slugify:slugify · Trim trailing dashes: slugs
# ending in '-' broke the CDN cache key
# ↳ Fix CDN cache misses caused by malformed slugs
--history works on trie read (symbols
and whole files) and the explain family, and as an opt-in flag on
the corresponding agent tools. Default reads are unchanged. The
trail costs tokens only when asked for.
5. The commit gate
The hook body is one command: trie gate. The guard
logic lives in trie, so installed hooks never go stale.
git commit → trie gate
├─ lock-check blocks if a trie writer is mid-flight offline
├─ verify blocks if prose drifted from source offline
├─ intent blocks if a changed symbol has no note offline
└─ diff --write writes + stages the commit's digest LLM narrative,
degrades gracefully
The intent check compares the working tree to HEAD at
the normalized-body level. Its scoping is
deliberately conservative:
- Formatting and line shifts never gate.
-
Synthetic
__module__symbols are exempt. Import shuffles are churn, not intent. - Untracked new files are included.
- Callers of a changed symbol are not pulled in.
Failures print copy-pasteable fix commands. Fix a
verify failure with trie sync. Fix an
intent failure by recording the note it asks for:
error: intent gate: 2 of 5 touched symbol(s) have no patch note
modified src/slugify:slugify
added src/slugify:_strip_accents
stage a note per symbol, then re-run:
trie patch create src/slugify:slugify -n "<why this changed>"
Where git hooks don't exist (CI runners), run
trie gate yourself before committing. Same guard,
same output. See Running in CI.
6. Agent integration
trie setup wires a coding agent in one idempotent pass:
-
Turn-boundary hook. Runs
trie sync --graph-only --after-turnwhen the session goes idle, so the graph tracks the agent's edits. No LLM cost. -
Tool overrides. The agent's built-in
grepandreadbecome trie-backed.traceand the explain/history family are added as new tools. -
Agent docs.
TRIE.md(the usage contract) plus a pointer inAGENTS.md/CLAUDE.md. -
MCP registration. Opt-in via
--with-mcp. The overrides cover most setups without it.
Supported targets: opencode, claude-code,
claude-desktop, cursor,
windsurf, vscode, codex.
Automation depth varies; targets without a hook/override surface
get MCP registration plus manual instructions.
The read verbs
| Tool | Returns |
|---|---|
grep(predicate, rank_by?, limit) |
Matching symbols with one-liners: enough to choose without opening anything. Falls back to ripgrep over in-scope source, attributing hits to enclosing symbols. |
read(qname | path, history?) |
A symbol's prose + caller/callee one-liners, or a file's compact triefact view. Raw source is the escape hatch. |
trace(qname, direction, depth) |
Signatures + one-liners across a depth-bounded graph slice: blast radius, call paths. |
These three verbs are the basis vectors for navigation: an agent
can find, understand, and trace any feature or flow with
the meaning stated in plain English for every symbol — often
tracing a whole call path in five lines of output without opening a
single source file. Every response carries one-liners pulled from
the prose at sync time, so walking the graph never requires opening
a triefact just to decide whether to open it. Identical JSON
envelopes come from the CLI
(trie grep/read/trace --json) for agents that prefer
shelling out.
The edit contract
Edit natively. Record intent through the patch tools.
patch_apply seals the notes. The
trie intent gate makes the contract enforced rather
than aspirational: a changed symbol with no note blocks the
agent's commit with copy-pasteable fix commands, so a gated agent
self-corrects.
Qnames
Symbols are addressed as path/to/file:LocalName
(methods: path/to/file:Class.method). No source
extension, forward slashes. This format is for
reading qnames that tools return, not constructing them:
look qnames up with grep first and round-trip them
unchanged.
What trie sync --graph-only costs
| State | When | Action |
|---|---|---|
unchanged |
stamp matches HEAD + mtimes | no-op |
no_stamp |
first run in a checkout | rebuild graph (no LLM) |
head_moved |
after git pull |
rebuild graph (no LLM); trust committed triefacts |
mtimes_moved |
local edits | scan + incremental sync (LLM only for real changes) |
7. Command reference
| Command | What | LLM? |
|---|---|---|
trie init |
Write trie.toml, scan the graph, offer setup |
no |
trie setup |
Wire an agent: hook + overrides + docs (+ --with-mcp) |
no |
trie plan |
Drift report + cost preview (--offline skips token counts) |
free calls |
trie sync |
Regenerate stale prose + cascade (--budget / --limit cap spend) |
yes |
trie gate |
The whole commit guard: lock + verify + intent + digest | digest only |
trie verify |
Bidirectional drift gate | no |
trie intent |
Changed-symbols-need-notes gate | no |
trie patch create <qname> -n "…" |
Record why a symbol changed (--gone for removals) |
no |
trie patch apply -N "…" |
Seal pending notes into the intent ledger | no |
trie diff |
This session's story: notes + prose deltas (--write emits the digest) |
narrative only |
trie grep / read / trace |
Query the indexes (add --history to reads for intent) |
no |
trie index |
Regenerate the wiki front door (triefacts/README.md) |
no |
trie sync --graph-only |
Freshness gate (what the turn hook calls) | only on real edits |
Verbosity is global:
trie -q sync: errors onlytrie sync: per-file progress + ETA (default)trie -v sync: token/cache breakdown per file
8. Configuration
Everything lives in trie.toml (written by
trie init, all knobs commented):
| Section | Controls |
|---|---|
[scope] |
Include/exclude globs. Out-of-scope files are never parsed, never sent to a model. |
[triefacts] |
Tree root and source root |
[models] |
Model ids for bootstrap and cascade generation |
[cascade] |
Cascade depth, hub-symbol threshold |
[sync] |
Parallelism, retry policy |
[diff] |
Digest narrative on/off, file locations, retention |
[mcp] |
Agent-surface tuning (result caps, fuzzy thresholds) |
[resolver] |
Type-aware reference resolution (LSP servers; see below) |
[debug] |
Local JSONL telemetry (off by default, never uploaded) |
Reference resolution
Reference edges come from two passes. Tree-sitter does the fast
structural pass — symbols, imports, containment, class bases,
and the calls it can resolve by name. Having no type information, it
can't resolve dispatch through a value
(obj.method(), self.helper(),
this.foo()) — most calls in object-oriented code.
An optional language server supplements it: for each
unresolved member call trie asks the server for the definition and
adds the edge. The passes are complementary; the LSP pass only fills
what tree-sitter drops. Servers are discovered on PATH; when none is
installed for a language, that language is tree-sitter-only (no
error).
| Language | Extensions | Default server |
|---|---|---|
| Python | .py | basedpyright → pyright |
| TypeScript / JavaScript | .ts .tsx .js .jsx .mjs .cjs | typescript-language-server |
| Go | .go | gopls |
| Rust | .rs | rust-analyzer |
| C | .c .h | clangd |
| Lua | .lua | lua-language-server |
[resolver]
enabled = true # master switch (or TRIE_DISABLE_RESOLVER=1)
# disabled_languages = ["rust", "go"] # force tree-sitter-only per language
# [resolver.servers] # override the server command per language
# python = ["basedpyright-langserver", "--stdio"]
9. Costs
-
trie planpreviews the bill before any generation. Per-file actuals print during sync. - Only prose generation and the digest narrative call a model. The gates, graph, queries, index, and intent recording are all offline.
-
First bootstrap is the big one, roughly proportional to symbols
in scope. Use
--limitto sample quality first. - Day-to-day syncs touch only what changed: cents, not dollars.
- Normalized fingerprints mean formatting passes cost nothing. The diff-aware regeneration rubric keeps cosmetic edits cheap.
10. Running in CI
Runners have no .git/hooks, no turn hooks, and a cold
.trie/ cache. The guard has to be explicit. The
pattern for an agent (or any automation) committing from a
workflow:
- uses: actions/checkout@v4
- run: |
pipx install uv
uv tool install git+https://github.com/computer-reinvention/trie
trie sync --graph-only # cold start: rebuild the symbol graph (no LLM)
# ... the agent works: edits code, records notes via the patch tools ...
- env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
trie sync # regenerate prose the edits staled
trie gate # the same guard the pre-commit hook runs
git add -A && git commit -m "..." && git push
-
trie gateis the whole contract: lock + verify + intent + digest, identical to the hook. Exit-1 output is copy-pasteable fix commands, so a gated agent can self-correct. -
Order matters on a cold runner.
trie sync --graph-onlyfirst (notes can only be recorded against symbols the graph knows), work, thensync → gate → commit. -
Record and commit in the same job. Staged notes
live in the runner-local store until the digest write archives
them into
triefacts/triediffs/at commit. The committed digest is the durable record. -
No key?
trie gate --no-digeststill enforces verify + intent. Keylesstrie syncfails loudly rather than pretending.
11. Adopting trie in a team
Who pays for sync (pick one)
-
Everyone has a key. Each committer regenerates
the prose their own edits stale.
trie plankeeps costs visible. Keyless teammates hit the verify gate with a loud, accurate error, never a silent green. -
One payer. A single maintainer (or a scheduled
job) runs
trie syncfor branches that need it, following the CI recipe above.
Merge conflicts in triefacts: regenerate, don't hand-merge
Two branches regenerating the same file produce textually different prose. Take either side wholesale and re-sync. Drifted sections regenerate from the merged source, which is the only truth that matters:
# .gitattributes
triefacts/** merge=ours
Reducing PR noise
Mark generated prose as collapsed-by-default, keeping digests expanded:
# .gitattributes
triefacts/** linguist-generated=true
triefacts/triediffs/** linguist-generated=false
What leaves your machine
- Changed symbols' source (plus neighbouring one-liners for context) goes to your configured model provider during sync.
[scope]excludes are never parsed or sent.- Telemetry is local-only.
-
Everything under
.trie/is a reconstructible local cache.
12. Troubleshooting
-
verifyreportstampered_body. Someone (you, an editor plugin, an agent) edited inside a generated section. Re-runtrie syncto regenerate, or revert the edit. Hand-written prose belongs between sections, where it survives regeneration verbatim. -
syncexits 2. It collided with an in-flight graph sync (the turn hook doing its job). Let the hook finish and retry. - The intent gate flags a symbol you didn't meaningfully change. The gate compares normalized bodies, so the body really changed. Record a short note. "Extracted helper, no behaviour change" is a valid intent.
-
A lookup returns nothing for a symbol you can see in
source. The file may be out of
[scope], or the graph is stale: runtrie sync --graph-only. Never hand-build qnames from file paths; look them up. -
The cascade missed a connection. Reference
detection is tree-sitter first (imports, same-module calls,
containment) plus an optional type-aware LSP pass that resolves
method/member dispatch (
obj.method(),self.helper()). If a connection is still missing, check the language's server is installed and on PATH — without it that language is tree-sitter-only. Truly dynamic dispatch (getattr-style) is out of reach for both. - PR diffs are noisy. See Reducing PR noise.
13. Status & roadmap
Pre-alpha, v0.1 in active development. Works today:
- The meaning index with cascade + verify
- The intent ledger with gate + digests + PR comments + history
- Agent integration for the listed harnesses
- Seven languages — Python, TypeScript/TSX, JavaScript/JSX, Go, Rust, C, Lua — with type-aware reference precision via optional per-language LSP servers over the tree-sitter pass
Next, roughly in order:
- Prose-stability tuning (kill cosmetic regeneration churn)
- Latency tuning for cold language servers
- More languages (Java, C#, Ruby, Swift as new resolver specs); Windows support
- A local-model path for source-cannot-leave environments
MIT licensed. Contributions welcome: issues, discussions, PRs at github.com/computer-reinvention/trie. For large changes, open an issue first.