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.
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.
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.
uvx mcp-repo-graph installrepo-graph wraps the engine as six MCP tools for Cursor, VS Code and Claude Desktop. Its README has the one-click installs.
pip install glia-pyabi3 wheels for Linux, macOS and Windows, x86_64 and arm64. The API.
# 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 .
| 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.
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.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 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.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).glia coverage <repo> --json lists the same caveats for the whole repo. Run it once before trusting "not found".--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.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-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.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_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>).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.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.blast-radius, effects and tests-for take many).<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.