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_KEY for 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 verify compares 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 sync instead of serving outdated text as truth.
  • Scoped regeneration. trie sync touches 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 and git pull never 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_patch tools.
  • patch apply requires 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.md at 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-comment GitHub workflow (installed by trie 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:

  1. Turn-boundary hook. Runs trie sync --graph-only --after-turn when the session goes idle, so the graph tracks the agent's edits. No LLM cost.
  2. Tool overrides. The agent's built-in grep and read become trie-backed. trace and the explain/history family are added as new tools.
  3. Agent docs. TRIE.md (the usage contract) plus a pointer in AGENTS.md / CLAUDE.md.
  4. 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

ToolReturns
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

StateWhenAction
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

CommandWhatLLM?
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 only
  • trie 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):

SectionControls
[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).

LanguageExtensionsDefault server
Python.pybasedpyright → pyright
TypeScript / JavaScript.ts .tsx .js .jsx .mjs .cjstypescript-language-server
Go.gogopls
Rust.rsrust-analyzer
C.c .hclangd
Lua.lualua-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 plan previews 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 --limit to 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 gate is 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-only first (notes can only be recorded against symbols the graph knows), work, then sync → 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-digest still enforces verify + intent. Keyless trie sync fails loudly rather than pretending.

11. Adopting trie in a team

Who pays for sync (pick one)

  1. Everyone has a key. Each committer regenerates the prose their own edits stale. trie plan keeps costs visible. Keyless teammates hit the verify gate with a loud, accurate error, never a silent green.
  2. One payer. A single maintainer (or a scheduled job) runs trie sync for 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

  • verify reports tampered_body. Someone (you, an editor plugin, an agent) edited inside a generated section. Re-run trie sync to regenerate, or revert the edit. Hand-written prose belongs between sections, where it survives regeneration verbatim.
  • sync exits 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: run trie 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.