Docs

Which command, and how to read what it says.

Every answer is one glia <command> <repo> … call. Add --json for structured output; progress lines go to stderr. Open the file:line an answer gives and read it there rather than grepping for it. This page is the glia skill, the same text a Claude Code agent reads.

Install

CLI

git clone https://github.com/James-Chahwan/glia
cd glia
cargo build --release -p glia-cli
cp target/release/glia ~/.local/bin/

Rust 1.95 or newer. A zero-install glia-cli package is in progress for 0.5.4.

MCP server

uvx mcp-repo-graph install

repo-graph wraps the engine as six MCP tools for Cursor, VS Code and Claude Desktop. Its README has the one-click installs.

Python

pip install glia-py

abi3 wheels for Linux, macOS and Windows, x86_64 and arm64. The API.

First run

# what is here, and how does it talk?
glia arch . --json

# the blind spots of this repo, before trusting "not found"
glia coverage . --json

# once per repo, and again after an upgrade: history for the ranked answers
glia history sync . --blame
glia history calibrate .

Which command

Question Command
What services are there, and how do they talk? glia arch <repo> --json
Where is X? (a name, qname or fragment) glia find <repo> X --json
Which files is this task, ticket or PR title about? glia find <repo> "<text>" --files --json
What should I read for task T, within a token budget? glia pack <repo> "<T>" --budget 4000 --json
What does this stacktrace / failing test id / diff point at? glia resolve <repo> "<signal>" --json
What does changing X affect? glia blast-radius <repo> X --json
What does my uncommitted change affect? A pasted diff? glia diff-impact <repo> --json, glia diff-impact <repo> --diff <file> --json
How does feature Y flow, end to end? glia trace <repo> Y --json
What are the entry points, and where does each go? glia flows <repo> --json
Why are A and B connected? glia why <repo> A B --json
Who serves GET /path, or consumes topic T? glia serves <repo> "GET /path" --json
Who implements, extends or overrides X? glia implementors <repo> X --json
What does X write, send or call outside the process? glia effects <repo> X --json
Which tests cover X, or my change? glia tests-for <repo> X --json, glia tests-for <repo> --base main --json
What did my change do to the graph? glia delta <repo> --json
My change as one PR report: impact, tests, edges, new rule violations? glia review <repo> --base main --json
Did my change break a client of an API or message contract? glia contract-breaks <repo> --base main --json
Does the frontend's model of an API response or request match the backend's (or the OpenAPI op, or the GraphQL schema)? glia contracts <repo> --with <backend> --fields --json
What usually changes along with my change? glia cochange <repo> --base main --json
Are there loops between services, or import cycles? glia cycles <repo> --json
Does the code break the repo's declared rules or architecture? glia check <repo> --json
Which feature flags are dead, undefined, read in one place, or quiet? glia flags <repo> --json
What changes often and has much depending on it? glia hotspots <repo> --json
What are the tightly coupled clusters? glia communities <repo> --json
What carries the most load: utilities, orchestrators, cross-service connectors? glia hubs <repo> --json
A structural question no command answers (every route with no handler, every call from web/ into api/)? glia query <repo> -e '<datalog>' --json, glia query <repo> --gql -e "MATCH ..." --json
Which packages, messages, contract ops or client / server models are one thing across these repos, and which copies diverged? glia identity <repo> --with <other> --json
Where is glia blind in this repo? glia coverage <repo> --json
What could glia not pair? What looks dead? glia gaps <repo> --json
Which docs govern X? glia docs-for <repo> X --json

hotspots, cochange and the quiet finding of flags read git history from a snapshot, never from git itself: run glia history sync <repo> --blame once first (it writes <repo>/.glia/history-snapshot/). Without one, hotspots and cochange come back empty with the absence reason no_history (exit 1), and flags reports quiet_evaluated: false.

The ranked answers read the same snapshot. With one, blast-radius, diff-impact, review and pack put the files that usually change with the seeds' files first (co-change first, then PageRank), and review / find --files rank by past commit messages too. Then glia history calibrate <repo> --json replays the repo's own history and records, per answer and seed shape, which ranking predicted its past commits best (<repo>/.glia/calibration-snapshot/); each ranked answer then uses that pick and says so in selection (source: calibrated, or fallback with the reason, e.g. absent before any calibration, build_stamp after a glia upgrade: calibrate again). pack is the exception: it never reads the record (calibration changes no pack), and its selection reads fallback with reason pack_uncalibrated for the default ranking. So, once per repo and again after an upgrade: glia history sync <repo>, then glia history calibrate <repo>. Neither runs inside a build, and without them every answer keeps its structural order. --ranking ppr|ppr-wide|bfs forces one ranking instead of the calibrated pick (for pack, the ranking itself, default ppr), --no-cochange drops the co-change tier, and --top-files <N> (blast-radius, diff-impact) sizes the ranked files list.

The rest of the CLI: - glia pages <repo>: frontend router pages and dead links. - glia contracts <repo>: producer vs consumer message types per queue topic; --fields diffs fields (schema copies, OpenAPI vs Pact, and the client / server models in the table above). - glia spec-status <repo>: OpenAPI / feature.yaml operations implemented or missing. - glia projects <repo>: the labels --scope takes. - glia impact <repo> X: plain reachability, unranked. - glia patterns <repo>: each service's route-handler convention and the handlers off it. - glia splits <repo>: where a scope splits into services at the least coupling (a suggestion, tier heuristic). - glia duplicate-flows <repo>: entry points whose flows reach the same nodes (an aliased route, a v1 / v2 pair, copies drifting apart). - glia timeline build <repo>, then glia timeline history <repo> <qname>: when each edge of a node appeared and went, over the last 20 commits. - glia scip import <repo> <index.scip>: load a compiler-grade SCIP index; the next answer reads its references as FACT-tier CALLS / USES / IMPLEMENTS / INHERITS_FROM. - glia watch <repo>: keep the build open in the foreground and print one NDJSON line per change saying what it did to the graph (a ready line first); it rewrites <repo>/.glia/graph/ on exit and every 60 s. For a long session, not for one question.

glia <command> --help lists every flag.

Flags most commands share: - --with <repo> (repeatable) merges more repos in, so links pair across them. With one service per repo, pass the others this way. Each row's file stays relative to its own repo's root. - --scope <path-or-label> narrows the answer within a repo. - The ranked walks also take --top-k, --depth and --direction forward|backward|both. - --no-collapse (on find, blast-radius, trace, flows, tests-for, serves and arch) shows one row per copy instead of one per identity class.

Reading an answer

  • Location. Node rows carry file and line: 1-based, relative to the repo. Some nodes have no single definition: endpoint:GET:/users (an outbound call), data_entity:sql:prefs (a table) and queue topics can have file: null. For those, the edge's site_file / site_line (in path, witness and site) locates the code.
  • Liveness. live: false (⊘ in tables) means no entry point reaches the node. It is likely dead, or it is reached from an entry point glia does not know.
  • Tier. tier is fact (declared in the source), derived (inferred by a resolver, e.g. a client URL paired to a route) or heuristic (paired by name). Case varies by command. Weigh the answer by it.
  • Empty answers. An empty answer is not "nothing exists". It carries absence: {tier, reason, note, caveats, suggestions}. Each caveat names an edge kind glia is known to miss (edge_category, language), how many it did find (edges_found) and what to grep instead (verify).
  • Blind spots. glia coverage <repo> --json lists the same caveats for the whole repo. Run it once before trusting "not found".
  • Identity. With --with, one package, message, contract op or SCIP symbol that several repos hold is one row, its best copy, with identity: {tier, rules, diverged, also_in}. also_in lists the other copies with their repo, qname, file and line, and diverged: true means the copies differ (a message's fields, say). Vendored copies are heuristic candidates in glia identity and are never folded. A frontend model and the server struct of the HTTP exchange it reads (rule http_model), and a GraphQL client's result model with its schema type (graphql_model), are derived classes too, inside one repo as well: in a monorepo find shows a TS interface and its Go struct as one row, the other one under also_in.
  • External sinks. A call whose every site dials a public host outside the build (fetch("https://nominatim.openstreetmap.org/search?…")) is an ENDPOINT marked external, never paired with a route. effects rows name the host in external_hosts; the table's downstream cell reads external: <host>. serves "GET /search" --json lists it after any route, "kind":"ENDPOINT","match":"external", so the channel is answered, not absent. external_hosts is [] on every other row. Such a call is real traffic, not a gap.
  • Docs. docs-for and the DOCUMENTS edges read only the markdown the build ingests. That is the well-known files (README*.md, CLAUDE.md, ...) and the docs/ tree at the repo root and at every project root, a top-level .ai/ tree, ADR directories, SDD feature docs (features/<feature>/*.md, spec-kit specs/<NNN-slug>/) and synced docs (glia docs sync). A section documents a symbol only when it names it in single backticks. A README inside a source directory is not read. glia coverage states this as a DOCUMENTS row.
  • Dead symbols. A gaps dead_symbol row is a function, method or class with no entry point reaching it and no incoming call or use. Never a row: a method that IMPLEMENTS a called method (an interface method, an abstract member: dispatch reaches it); the members a framework or the runtime calls on a class something uses (Angular lifecycle hooks, form-control and validator members, pipe transform, interceptors, guards, Flutter build / createState / dispose, ...) and its constructor; generated code (ORIGIN generated / generated_proto). stderr counts what it held back: [dead-framework] dead_symbol rows withheld: invoked=<i> generated=<g>. A method an Angular template or host: binding calls (inline or through templateUrl), a member a decorator binds (@HostListener, an @Input setter), what a Flutter widget's closures call and a TypeScript function passed by value (a route guard, a provider, a comparator) are reached through real edges, so they are not dead either. So are a Dart getter read, tear-off or member named in any value position, a call on a Riverpod provider read (ref.read(p).m()), a Go method called on a range variable's element or a package-qualified field, a Go func or method passed by value and a struct a composite literal builds, a pkg.F route handler, and a TypeScript call through an Angular signal query (viewChild(T)), an inline inject(T), a const x = f() local or a getter on another typed object. Entry points in their own right: a Go init, an iOS @main type, a Flutter Android activity, and a Python script with an if __name__ == "__main__": guard (the guard's calls are the module's). Live when their class is used: a Go method the runtime calls through an interface, checked by signature (Error, String, MarshalJSON, Scan, Value, ServeHTTP, Len / Less / Swap together); an @main app delegate's callbacks; the members a library base calls on a class extending it (a Transloco loader's getTranslation, an Angular ErrorHandler's handleError or Resolve's resolve, a dio interceptor's onRequest). stderr counts the invoked members by framework: [invoked] rows=<n> angular=.. constructor=.. flutter=.. go_runtime=.. apple_app=.. transloco=.. dio=.. sig_rejected=<r> (sig_rejected: methods named like a runtime method whose own signature or owner's sibling does not match, Scan() bool; they are not withheld). On two audited full-stack repos, every remaining row was truly dead.
  • Unpaired routes and endpoints. unpaired_route leaves out a route a WebSocket client connects through ([gaps] ws-served routes=<n>) and a route only test code registers ([gaps] unpaired_route rows withheld: <n> (test-file routes)). A GraphQL client's HTTP transport (endpoint:POST:/graphql) is no unpaired_endpoint when its project's operations pair a resolver on a server with no route at that path; any other unpaired transport's detail starts graphql transport; ([gaps] graphql transports implied=<n> unpaired=<m>).
  • Ranking. blast-radius and diff-impact list files after the rows: the most relevant files ranked (rank, file, repo, cochange, ppr, depth under bfs, rows: how many result rows sit in it, 0 for a file the walk never reached, which co-change alone put there). selection (set, shape, pick, source, reason, test_commits) names the ranking an answer used; the table form prints it as one ranking: line. review with a title (--title, or by default the newest commit subject of <base>..HEAD) adds intent: the ids the title names (a ticket LANG-12, a spec-kit FR-001 / T012, or the shapes .glia/overlay.toml [ids] declares) routed to the task or requirement rows defining them and the files those rows name, past commits ranked by the title's words, and intent.files ranked by the repo's pick (selection_intent); impact rows in files an id routes to are listed first. find --files answers the same question for a query: files with idr (an id in the text routes there) or text (past commit messages match) above 0, ranked; with neither it is an absence whose note says which input was missing (no id resolved, no history messages, no past message matches), exit 0.
  • Exit codes. 0 for any answer, empty included, except: pack, hotspots, hubs, cochange, splits, query and identity exit 1 when the answer is empty. why exits 1 when no edge joins the two nodes (it still prints a witness path). check exits 1 on a BLOCKING violation: one whose tier is at least its rule's min_tier (default derived). A heuristic violation never blocks; it is listed as advisory. A declared rule that checks nothing (a stanza anchored nowhere or rejected, one the graph does not hold, a scope no node sits in) is listed under unanchored ("Rules that check nothing") with its reason and a fix, and changes the exit code only with --strict (then 2). review exits 1 when the change adds a blocking violation or a blocking model break, and contract-breaks when a change is breaking, leaves a client with no provider, or is a blocking model break. A model break (a client / server model drifted across the change) blocks only at or above --models-min-tier (default fact, so the resolver-paired derived rows are advisory: counted in advisory, not breaking). blast-radius exits 3 when no seed resolved. 2 means a usage, git or build error, a query that does not parse or analyse, or a rule check could not evaluate.

Cost, and what gets written

  • There is no daemon and no index to keep fresh: each call builds the graph from source in memory, then answers. Rather than looping, pass several seeds to one call (blast-radius, effects and tests-for take many).
  • The query commands write nothing to the repo.
  • Five commands save a parse cache at <repo>/.glia/graph/parse_cache.bin (self-gitignored): delta, diff-impact against a rev (its default), tests-for --base, review and contract-breaks. The --diff forms write nothing, and cochange --base persists nothing.
  • flows --features without --json writes <repo>/.glia/graph/flows/.
  • glia history sync writes <repo>/.glia/history-snapshot/ (with each commit's redacted message; --no-messages keeps none), and glia history calibrate writes <repo>/.glia/calibration-snapshot/. glia timeline build writes <repo>/.glia/graph/timeline.gmap, and timeline history rebuilds and rewrites a stale .glia/graph/ layout.
  • glia build <repo> writes the .glia/graph/ layout that the MCP server and Python bindings load. The CLI does not need it.
  • glia watch <repo> writes the same layout and the parse cache on exit and every --persist-every seconds (60 by default). It does not read GLIA_NO_PERSIST: writing the layout is what it is for, so never run it on a repo you must not write to.
  • GLIA_NO_PERSIST=1 is for probing a repo you must not write to: set it on every call. It stops glia rewriting a stale layout (glia merge --gmap, timeline history) and timeline build writing its sidecar. It does not stop the parse cache, the flows files or the history snapshot above. On such a repo, use the --diff forms and flows --features --json instead: git -C <repo> diff | GLIA_NO_PERSIST=1 glia diff-impact <repo> --diff - --json.