magus v0.4.3 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
37 min read

Knowledge graph

The knowledge graph is a deterministic, cache-backed graph of the magus domain. Every node and edge is EXTRACTED or rubric-INFERRED from workspace sources: no LLM pass, ever, so it is safe to rebuild implicitly and byte-for-byte reproducible from the same inputs. It is assembled from machinery magus already owns: the verified project dependency DAG, static magusfile extraction, the spell/module/diagnostic registries, markdown docs, and buzz source parsing.

It exists so agents and humans can ask "what is this, what touches it, how do these relate" and get a precise answer instead of grepping. Agents reach it over MCP; humans reach it through three verbs and the magus graph home.

{"figure":"knowledge-graph","stamp":"sha256:8edbd3a48dfbb0ee0f4b482991694386a43fdbd7b4a0abcf5369b2b48ab0fafa"}Every source writes a shard; every reader asks one graphmagus graph build assembles a shard per source: the registry and one per project from the magusfiles, the file adapters for Buzz, docs, directories, citations and CODEOWNERS, a lazy symbol shard per project from a SCIP index, git history and notes. The store fingerprints each shard and rewrites only the ones whose content changed, then merges them into one graph. The CLI and the MCP tools read that graph through the same query, explain, refs and path.KNOWLEDGE GRAPHEvery source writes a shard; every reader asks one graphA SHARD PER SOURCEMagusfiles + registry@registry, one per projectFile adaptersbuzz, docs, dirs, links, ownersSymbolsSCIP index, lazy shardsHistory@vcs, from gitNotes@notes shared + privateOne graphfingerprint, sync, mergeThe four verbsquery, explain, refs, pathREADERmagus CLItext or -o jsonREADERMCP clientmagus\query and kinLEGENDWHAT EVERY READER ASKSBUILT BY A SCIP INDEXER

What this graph is not

"Knowledge graph" now names architectures this one deliberately is not, and the distance is the point.

  • Not open-world. General knowledge graphs are genuinely hard: ontology design, entity resolution, and modeling discipline that stays expensive at scale. magus never attempts that problem. The build domain hands it a closed ontology (the node kinds and relations above are the complete list), so there is no modeling step to do well or badly, and no linking judgment to trust.
  • Not a second indexer. Most codebase graphs are built by a separate system that scans the repo and infers structure, and the drift between that system and your build stays invisible until it bites. This graph is assembled from the same declarations the build executes. A wrong edge is a broken build, and a broken build is loud.
  • Not synthesized links. No pass ever guesses an edge: no LLM, no fuzzy matching, no "related notes" heuristics. Every edge is extracted or rubric-inferred from a source you can open, and magus explain prints each edge's provenance so the claim is checkable per edge, not per marketing page.
  • Not agent memory. The graph is derived state: rebuilt from the workspace, never accumulated, never remembered. Nothing in magus remembers a session; an agent's harness owns that, and the graph answers everything derivable so there is little left to remember.

The two-concept model

  • query / explain / path READ the graph: daily retrieval.
  • magus graph IS the graph: emit it (deps), export it (export), or measure its shape (stats).
magus query "<terms>"       # ranked node matches plus their neighborhood
magus explain <node>        # one node: its edges, provenance, blast radius
magus path <a> <b>          # the shortest chain of edges between two nodes
magus refs <symbol>         # where an ingested code symbol is defined and referenced
magus graph stats           # god nodes, orphans, doc coverage
magus graph export -o json  # the whole graph as node-link JSON
magus graph diff base.json  # what this branch changed vs an exported baseline
magus graph export --open            # explore it visually in your browser (data stays local)

Prefer a picture? magus graph export --open launches the interactive Graph Explorer seeded with your own workspace: a force-directed, searchable view of the same graph. Your data never leaves your machine: it rides in the URL fragment (or a local loopback server with --serve), never reaching the site. This site's own graph is the live demo.

The committed MAGUS.md routing table is the entry point: it lists every node kind with its count, the query that lists it, and the highest-degree anchor nodes, so an agent knows what exists before running anything.

Query grammar

magus query takes free-text terms (AND) plus field matchers. A matcher is field<op>value, where the operator is = (match), != (exclude), or =~ (regex). Terms are scored with the same leaf-anchored fuzzy match that powers magus where.

Form Meaning
build free text: match node IDs, labels, and docs
kind=spell only nodes of that kind
project=pkg/foo the project node and its targets
relation=uses seed from nodes touching a uses edge
id=build substring match on the node ID
kind!=op exclude these
id=~build$ regex over the target; kind=~"spell|op" ORs
id=target:*build * wildcard: matches any run (in a value or term)
"exact phrase" a quoted span stays one term

The : grammar (kind:spell, -kind:op) is the pre-= spelling, kept as a compat alias so existing invocations keep working; new queries should use =/!=/=~.

A query resolves terms to seed nodes, then collects the induced neighborhood up to a node budget (--budget, default 50), so a match on a high-degree node cannot pull in the whole graph.

Questions you can ask

Recipes for the graph as a lens on the workspace. Rebuild first with magus graph build if you want it fresh; combine field filters freely.

What programs does the workspace actually run? magus owns the task layer, so it knows the concrete tool behind every operation, not just the source.

magus query "kind=tool"                    # the workspace's toolchain (go, buf, docker, ...)
magus explain "tool:go"                    # every op and spell that runs go
magus explain "op:go:go-test"              # an op's base argv (the `argv` attr) and its tool
magus path "target:.:test" "tool:go"       # a target reaches its tool via target->op->tool

Each spell op carries the base argv it runs on an argv attr (rendered with empty charms) and uses the tool:<program> node for its argv[0]: the program, its own kind because it is an entity, not an operation. The op's spell uses the tool too, so magus explain tool:go lists every op and spell that runs go; a target reaches the tool through its existing target --uses--> op edge. (There is no per-target command node: its argv was always identical to the op's, so the op carries the model.)

So magus explain tool:go lists every op that runs go:

$ magus explain tool:go
tool:go   tool
tool: go
15 nodes reach this

used by (12)  op:go:go-build, op:go:go-clean, op:go:go-fuzz, op:go:go-generate,
              op:go:go-mod-download, op:go:go-mod-edit, op:go:go-mod-json,
              op:go:go-mod-tidy, op:go:go-run, op:go:go-test, op:go:go-vet,
              spell:go

View in Graph Explorer: http://127.0.0.1:7391/console/graph/#view=blast&node=tool%3Ago
open it signed in: open "http://127.0.0.1:7391/console/graph/#view=blast&node=tool%3Ago&code=$(magus config console token create --code --expires 12h)"
(start the magus server if the graph does not load)

next:
  magus path tool:go op:go:go-build
      path resolves the chain between two nodes, and this is the neighbor the card names most.

A target shows what it runs and what runs it, each relation in plain words:

$ magus explain target:.:test
target:.:test   target
Run the Go test suite; formats first.
source: .
engine: buzz
1 node reaches this

uses (2)    op:go:go-test, spell:go
depends on  target:.:format
part of     project:.

View in Graph Explorer: http://127.0.0.1:7391/console/graph/#view=blast&node=target%3A.%3Atest
open it signed in: open "http://127.0.0.1:7391/console/graph/#view=blast&node=target%3A.%3Atest&code=$(magus config console token create --code --expires 12h)"
(start the magus server if the graph does not load)

next:
  magus path target:.:test op:go:go-test

And path connects two nodes as a chain: a target reaches its tool through its op:

$ magus path target:.:test tool:go
target:.:test -> tool:go  (2 steps)

target:.:test
  uses  op:go:go-test
  uses  tool:go

(These blocks are generated by magus-examples from a fixture workspace and kept current by the drift gate; do not hand-edit the output.)

Where does a function or symbol live (as path:line), and where is it used?

magus refs <name>                          # the definition + every reference, each as path:line
magus refs <name> --definition             # the definition's exact lines, as path:start-end
magus refs <name> --source                 # the definition's body itself
magus explain "symbol:<id>"                # the node's `source` is the definition's path:line
magus explain "file:<path>"                # a file's `lines` and `bytes`
magus query "kind=symbol <name>" -o json   # each match's `.source` is "path:line"

Symbol nodes carry their definition as source: "path:line" and, where the indexer recorded an enclosing range, def_end_line; refs returns every reference the same way. --definition checks each range against the file on disk: verified when the file predates its index, changed (exit 1) when the symbol's name has left the start line, and unverified when the name is still there but the file was edited since, so the end may have moved. A symbol with no recorded end line says so. An agent (or an MCP tool) can read the exact lines straight from the graph and edit surgically instead of loading the whole file.

Where does risk concentrate?

# client tool, one field of magus\insight():
#   hotspots    churn x complexity per project, with blast radius
#   affinity    projects that change together: hidden coupling
#   ownership   author concentration and bus factor
magus explain <node>      # a node's edges and how many nodes reach it (blast radius)
magus path <a> <b>        # the shortest edge chain between two nodes

Which code lacks test coverage? magus runs the tests, so it owns the coverage profile; a pure code-graph tool cannot answer this.

magus explain "symbol:<id>"      # a function's coverage ratio + test_refs (test files that reference it)
magus query "kind=file" -o json  # each file node's attrs.coverage (covered/total statements)

After magus run test (or magus run ci), a coverage attr (with covered_stmts / total_stmts) folds onto file and symbol nodes, and test_refs counts the test files that reference a symbol. Sort symbols by coverage ascending for "what is untested", or cross it with insight hotspots to rank high-churn, low-coverage code first.

What may be unused? The graph offers scoped candidates, not a generic "dead code" verdict. magus graph stats reports structural orphans such as a declared spell no target uses. Where the symbol index is available, magus\insight().unreferenced, through the client tool, reports code symbols with no indexed cross-file reference.

Neither result proves that something is safe to delete. Entry points, external consumers, reflection, interface dispatch, generated code, build tags, package initializers, and intentionally navigational documentation can all be live without the relevant incoming edge. An isolated node likewise means only that the graph has no modeled connection. Treat each result as a candidate with a named visibility boundary; do not turn missing edges into a universal dead=true classification.

What does a target produce or consume, and is a file generated? magus indexes each target's declared magus\outputs / magus\inputs, so the graph knows the build's file flow, which a pure code-graph cannot.

magus explain "target:.:content-generate"   # the files a target produces and consumes
magus explain "doc:docs/spells/go.md"        # a "produced by" edge means it is generated

Each declared output/input becomes a produces / consumes edge to the file and doc node it matches, so a generated file is self-labeled by its producing target (no marker needed) and you can walk from a target to exactly what it writes.

Which markdown is what? Every authored markdown file in the workspace is a doc node tagged with a role from a universal filename convention, so it works in any repo.

magus query "kind=doc role=agent"    # the agent-instruction files (AGENTS.md, CLAUDE.md)
magus query "role:readme"            # every README, wherever it lives
magus query "kind=doc role=skill"    # skill definitions (SKILL.md)

Roles are readme, agent, skill, changelog, contributing, license, or a plain doc. Each doc attaches to the project whose directory holds it (project --contains--> doc), so from a project you reach its README and design notes as contextual docs.

Graph Explorer

magus graph export --open opens the graph in an interactive, force-directed Graph Explorer in your browser, privately. Your graph never leaves your machine: by default it rides in the link's URL #fragment (which browsers never send to a server), and --serve instead hands it to the page from an ephemeral 127.0.0.1 loopback server that serves once and stops. The hosted page is static; it decodes or fetches the graph locally.

magus graph export --open           # default: gzip'd into the URL fragment (small/medium graphs)
magus graph export --open --serve   # loopback server (no size limit; serves once, then stops)
magus graph export --open --print   # print the URL instead of opening a browser
magus graph export --open --url <base>   # point at a self-hosted mirror of the explorer

Open your target graph

To open the target dependency graph (the magus\needs DAG, not the knowledge graph) in the same explorer:

magus graph export --open --targets                # whole workspace
magus graph export --open --targets .              # scope to root project
magus graph export --open --targets docs        # scope to one project by path
magus graph export --open --targets --print        # print the URL instead of opening

The --targets path always uses the URL fragment (no loopback server); it is incompatible with --serve. An unknown project path exits with code 2 and lists valid paths.

The explorer's filter box speaks the same fielded grammar as magus query (kind:, project:, relation:, id:, free text, "quotes", -negation); a query dims non-matching nodes so the subgraph stands out. Beyond the filter: double-click a node for its local graph (its neighborhood, [/] to change depth), click a legend color to isolate a kind, and use the hubs/orphans lenses (the visual twin of magus graph stats). The page is fully client-side and data-agnostic; it also loads any graph.json from magus graph export -o json via the Open-file button or drag-and-drop. This site's own graph is the demo.

Schema

A node is a magus-domain entity with a stable, human-readable ID (<kind>:<qualified-name>, e.g. target:pkg/foo:build). The ID is stable across builds so external consumers and agent memory can key on it. A rename is a delete-plus-add.

Node kinds: project, target, spell, op, charm, module, method, diagnostic, doc, file, function, import, rationale, owner, link, package.

Nodes also carry static metadata the extractors already parse, surfaced as attributes so magus explain answers a question without a second describe: a project reports its engine and target_count, each target inherits its project's engine, and a doc page carries its frontmatter title and tags. Attributes are additive and absent when unknown, so they never bump the schema version.

Edges are directed and carry provenance and a confidence tag: extracted (1.0, from a parseable source) or inferred (a rubric score, from a fuzzy match).

Relations: depends_on, contains, uses, calls, imports, references, documents, rationale_for, owns. calls spans two layers: buzz function to buzz function, and code symbol to code symbol from a SCIP index. rationale_for points at a buzz function, or at its symbol once a Buzz index covers the file.

Ownership is extracted from a committed CODEOWNERS file (checked at the repo root, .github/, or docs/): each owner becomes an owner node with an owns edge to every project and buzz file it covers, under GitHub's last-match-wins rule, with CODEOWNERS:<line> provenance. Only declared ownership is taken; blame-derived ownership is insight's job, not a graph edge, so "who owns the blast radius of this change" is one path query.

Both node-link JSON and GraphML carry a schema_version; external consumers and agent skills should check it, since a bump is a changelog event.

File layout

The graph lives under the cache dir at .magus/knowledge/, cache-owned and NOT committed by default: the build is cheap and deterministic, so committing derived data buys nothing (export exists for teams that want a snapshot).

.magus/knowledge/
  manifest.json        per-shard fingerprints and counts (the routing index)
  shards/<name>.json   one file per shard; SHARDS ARE AUTHORITATIVE
  guard.idx            what the agent guard asks the graph (see below)

guard.idx is written by magus graph build and after each run of the server's background symbol indexer. It lists the ids the guard's search rules check a pattern against (symbol names, doc sections, targets, diagnostics) and a stamp of every source they came from. The guard hook reads this one file instead of loading the graph, and treats any stamp that no longer matches the tree as "unknown", so a rule that needs the graph stays silent until the next build. Every lookup is capped at 150 ms.

There is no continuously maintained merged graph.json: at scale, rewriting a merged file on every edit is an O(graph) write. Merging happens in memory at load time; the merged export is produced on demand. Shards are per-project plus singletons for the registry (spells/modules/diagnostics), docs, buzz sources, and run history (@runtime, below). A query loads the store, fingerprint-checks each shard, and rebuilds only the stale ones, the "cache that gets hit first". First run pays a full build; steady state is a fingerprint check. --refresh forces a full rebuild.

The check is two-tiered. Each class carries a full stamp, which folds in the evaluated target graph (and so costs every magusfile a parse), and a fast stamp, which folds everything that evaluation is a function of without redoing it: the tree, the binary, the configuration, the cached provider answers, and every environment variable and file the magusfile top levels read the last time they ran (recorded with the shards). A read whose fast stamps match the manifest answers from the store without evaluating the workspace at all; one whose do not falls back to the full stamps, and only a full mismatch rebuilds. The manifest records the declared symbol indexes beside the stamps, each with the freshness verdict the evaluation reached and the identity of the index file it judged, so such a read reports the same coverage and the same stale indexes an evaluating read would, and goes back to the workspace the moment an index file or the sources differ.

Two optional knobs bound and share the store. knowledge.max_size_mb soft-caps the shard directory: over the cap, least-recently-used shard files are evicted (their manifest entries stay, so an evicted shard is restored from the remote cache or rebuilt on the next query; 0, the default, is unlimited). When a remote build cache is configured, deterministic shards ride it: pushed on build, restored by fingerprint, so teammates and CI can reuse them. The @runtime shard is never pushed: it is local run history, not shareable derived data.

Third-party packages (@packages)

Every manifest a shipped spell declares is read into package nodes, one per dependency a project declares directly, with a depends_on edge from the project. The node ID is package:<manager> <name>, and the manager matches the one its ecosystem's SCIP indexer writes into symbol monikers, so a package and the symbols indexed from it share a namespace.

Manager Manifest Versions read from
gomod go.mod go.mod itself: its require lines are exact versions
npm package.json pnpm-lock.yaml (v9), package-lock.json or npm-shrinkwrap.json, yarn.lock (berry)
python pyproject.toml uv.lock, poetry.lock, pdm.lock, Pipfile.lock
cargo Cargo.toml Cargo.lock

A manifest holds ranges, so the version is the one the lockfile pins. The lockfile is the one the project resolves by walking up from its directory, so a lock hoisted to a workspace root serves each member. Python names are PEP 503 normalized, so Pillow and pillow are one node. A dependency with no registry release (a workspace, path, link or git dependency, an npm alias) gets no node.

Not read: a yarn classic (v1) yarn.lock, bun.lockb (binary), setup.py (executable) and setup.cfg, neither of which has a lock. A manifest whose lockfile is missing or not understood contributes no nodes, and the graph build logs one info line naming the project.

Each node carries manager and version, plus version_conflict (every version, when projects pin different ones), indirect and replaced when they apply. magus explain package:npm @connectrpc/connect prints a docs: line: pkg.go.dev, npmjs.com, pypi.org or docs.rs at the pinned version, one line per version in a conflict. The URL is derived from manager, name and version and never fetched, so it can point at a page that does not exist, such as a private module or a failed docs.rs build. A replaced package prints none.

Runtime enrichment

Beyond the static graph, magus records which diagnostics (MGSxxxx codes) each target trips during real runs, as emits edges in the isolated @runtime shard. A run captures every fired diagnostic through one sink that also feeds the report stream, and persists the set to <cache>/knowledge/runtime.json. This answers "what has this target tripped": history the static documents edge cannot. The same shard also folds observed performance onto target nodes from the local timing history: duration_p75_ms, cache_hit_rate, and run_samples, so an agent planning work sees a target's cost without a separate history query. It also folds each target's last_output_ref (the refxxxxxxxx id of its most recent captured run) and last_run_ok (true/false for that run) from the local output store, so an agent goes query -> target -> the last captured output in two hops (magus query output <ref>). Timings and refs for a target no longer in any magusfile are dropped rather than left as phantom nodes. This is the graph's only non-deterministic input, so it is quarantined: a distinct shard, excluded from remote export, derived from local run records rather than workspace sources.

Agent contact (@session)

Once magus session load has folded an agent host's transcript into the per-repo session store, the @session shard rolls those events up onto the file and directory nodes the graph already holds: agent_sessions (how many distinct sessions touched it), agent_reads, agent_writes, agent_denials, and agent_last_touched (the host's own event time, not the load time). That answers "which code do agents actually touch" and "where do refusals concentrate" from a node, with no second query. Events themselves never become nodes: merge is a set union, so repeated contact would collapse to one edge, losing the count, which is the whole signal. A denial is credited only to an event that carries a path; a refused shell command names no file, so crediting it to the directory the session happened to be working in would invent attribution. A path that matches no node is counted and dropped, never minted. Like @coverage, the shard loads with the symbol layer rather than the default graph, because the file nodes it annotates are the symbol shards' own; like @runtime, it is local-only, never pushed to a remote cache, and stripped from magus graph export --reproducible.

Code symbols (SCIP ingestion)

magus never parses source code. To bring code symbols into the graph, it ingests a SCIP index file that a per-language indexer (scip-go, scip-typescript, ...) emits, so any language with an indexer works, with no magus code per language.

This is automatic. Every symbol-capable spell (go, typescript, python, rust, buzz) declares its indexer, and magus runs it as an op. Importing the language's spells is the entire opt-in: each project bound to such a spell is ingested with no knowledge: config. Build the index the same way you run any target:

magus run pkg/foo::scip        # forks the language's SCIP indexer
magus run ::scip-buzz          # the Buzz index of the root project

A project keeps one index per indexer op among the spells it binds. An indexer runs as scip unless its SymbolIndexer declares an op of its own; the buzz spell declares scip-buzz, so the root project of this repository builds its Go index with scip and its Buzz index with scip-buzz. Each op has its own cache entry, freshness verdict and failure: a missing scip-buzz leaves the Buzz index unbuilt and the Go index untouched. Two spells bound to one project under the same op share that op's one index, and the first one bound names its language.

The index is a build artifact, so it lives under the magus cache dir, never in the source tree: magus hands the indexer the destination through a MAGUS_SYMBOL_INDEX environment variable it injects for the op, and reads that same path back at query time. The next graph query folds the symbols in, every index of a project unioned.

The server keeps it fresh for you. While the server runs, background auto-indexing re-runs each symbol-capable project's indexer ops when its sources change, so symbols stay current with no manual step. It is deliberately unobtrusive: a burst of edits coalesces into one run (a quiet window), a project re-indexes at most once per interval, a run starts only when nothing else is running, and it cancels itself the moment your own work needs a slot. Each run goes through the normal path, so it shows up as an ordinary journaled job, not hidden work. It is on by default in the server; a one-shot CLI never auto-indexes. Tune or disable it under knowledge.symbol_indexing (disabled, quiet_seconds, min_interval_seconds). If an indexer is not installed the background run of that index just fails and backs off, while the project's other indexes keep running; install it, or run magus run <project>::scip yourself, or index in CI. magus status and the gaps a lookup reports name a missing indexer and where to get it.

An index that has not been built yet is simply skipped, so symbols appear once its op has run. To point a project at indexes your own build already emits somewhere in the tree instead, override it:

# magus.yaml
knowledge:
  symbols:
    - project: pkg/foo
      index: build/custom.scip # a workspace-relative path magus reads as-is
    - project: pkg/foo
      index: build/buzz.scip   # a second index of the same project
      language: buzz           # what its symbols are written in

A project's entries are its whole index set: the first one replaces every index magus would have derived for that project, and each further entry for it adds one. An entry without language takes the language of the project's indexing spell, so a project with an index per language names each one.

Once a Buzz index defines a file's top-level function, that function is the index's symbol node, and the AST walk's function node and its intra-file calls edges drop out when the symbols merge (a function the index does not define, such as an extern fun, keeps its node); the file node, its imports and its NOTE/WHY rationale stay, each rationale now explaining its function's symbol. The committed default graph still carries the function nodes, since it never depends on what a machine's cache holds, and a function id resolves to its symbol in magus refs.

Each ingested index becomes a per-project <project>@symbols shard: symbol nodes (keyed by their version-stripped SCIP moniker), defines edges from the defining file, references edges from each using file (one per file, carrying an occurrence count and capped lines), and calls edges between symbols.

A call edge is attributed, not inferred: SCIP records an enclosing range for each definition, so a reference occurrence that falls inside one was written in that definition's body, and the enclosing symbol is the caller. Two restrictions keep the relation honest. The callee must be something callable: an enclosing range spans the whole declaration, signature included, so most occurrences inside it are types and fields rather than calls. Callability is read off the moniker's SCIP descriptor suffix rather than the optional SymbolInformation.Kind, so it does not depend on an indexer choosing to populate a field: scip-typescript sets no kinds at all, and a kind-based rule would silently produce no calls for an entire language. And the callee must be defined in this workspace: a call into a dependency has no body to navigate to, and its usage is already recorded by the referencing file's references edge. A symbol seen only as a reference still gets a node, so cross-project usage resolves. Every indexed source file also becomes a browsable file node the edges land on, linked to the project that owns it, so a .go or .ts file sits in the graph the same way a .buzz file does, reachable from its project and the workspace. SCIP paths are relative to the indexer's root, so magus rebases them onto the project's workspace path; a nested project's files land under the right project, not the workspace root.

Both file and symbol nodes carry a language attr, so magus query language:go groups every Go source file and symbol (and language:buzz the buzz sources), one filter across everything the graph knows, however it was extracted (magus's own AST walk or a foreign SCIP index).

SCIP makes a document's language optional and not every indexer sets it (scip-typescript sets it on none), so magus falls back to the language the producing spell declares, the same declaration that made the project symbol-capable. A document that names its own language still wins, since one index may legitimately span several.

Symbol shards can dwarf the domain graph, so they are lazily loaded: the default magus query/magus graph stats/magus graph export --open/warm graph never touch them. They load only when a query is symbol-seeded: kind:symbol, a symbol: ID, relation:defines/references/calls, or the refs verb. magus refs <symbol> lists a symbol's definition and every referencing file (client calling magus\refs). At very large scale a derived shards/@symbols.routing.json (symbol hash to referencing shard names, rebuilt with the shards) lets an exact-ID lookup load only the shards that mention the symbol rather than all of them; a missing routing file just falls back to loading all.

The third verdict: when an empty answer is not a fact

A lookup that returns nothing has two very different meanings, and collapsing them is how a blind spot gets recorded as a fact. magus query, magus explain, and magus refs all say which one they mean:

verdict what it asserts what to do
found the lookup returned something read it
absent nothing matched, and everything that could match was searched trust it
unknown nothing matched, but part of the workspace was not searchable close the gap, then re-run

An unknown verdict names its cause. symbol-index-missing lists the projects whose declared SCIP index magus could not read, which magus graph build fixes. symbols-not-loaded means the lookup never consulted the symbol layer at all: a bare magus query someFunc searches domain entities, so it says nothing about whether a code symbol by that name exists, and magus refs someFunc is the verb that would know. coverage-unknown means the probe itself failed, so magus cannot say what it searched.

A verdict is not only for empty results: a populated list drawn from a half-indexed workspace is as misleading as an empty one, because the projects it omits are invisible either way. So a gap makes the answer unknown whether or not the lookup matched.

The verdict rides the structured output as an answer field, so an agent branches on answer.verdict rather than pattern-matching prose:

magus refs SomeSymbol -o json    # .answer.verdict, .answer.reason, .answer.gaps[]

Over MCP every verdict comes back as a result rather than an error, so an agent branches on the field in all three cases rather than pattern-matching an error string for one of them.

Exit codes follow the same split. refs and explain exit 2 on absent (the request cannot be carried out as stated, and magus verified that) and 1 on unknown, where the invocation was fine and a prerequisite artifact was missing. A refs lookup that resolved but found no references follows the verdict too, exiting 1 when magus could not verify the emptiness: "nothing uses this" is a negative claim like any other. magus query exits 0 when it can answer (it matched, or it verified the absence) and 1 when it cannot: nothing matched and the verdict is unknown, the case magus graph build fixes. It never exits 2, and it never fails on a populated result, whose unknown caveats rows that are facts already. An empty result set stays a legitimate answer to a search; a blind spot is not one, and a caller that reads it as "not in the graph" goes back to grepping.

The coverage probe is one stat per declared index, and it is skipped entirely when the symbol layer was irrelevant to the question: kind:author returning nothing has nothing to do with a missing symbol index, so nothing about one is reported. The probe deliberately does not decode each index to check it parses: that is a full unmarshal per lookup to catch a case the graph build already logs, while a never-built index is the case that actually occurs.

A URL written in a code comment is almost always a pointer at documentation, and until it is indexed it is invisible to every query. The @links shard reads them: every http(s) URL in a Go or Buzz comment, every absolute markdown link on a page, and the source a generated page names in its generated_from frontmatter.

Each citation lands in one of three classes:

class what it names what the graph does
docs a page this workspace holds an edge to that doc, or to the docsection its anchor names
source a forge URL naming a path this workspace holds an edge to that file or dir
upstream anything else an edge to a link node keyed by the normalized URL

Only upstream mints a node, so kind=link is exactly the set of external documents this workspace depends on, and magus explain link:buzz-lang.dev/0.5.0/reference/std/fs.html lists every file citing it. The other two cross-link to a node that already exists, so a docs reorg moves the edge with the page instead of stranding a URL.

Which relation an edge carries follows the citing kind: a page that names a source path is describing it (documents), while a comment that names anything is only pointing at it (references). magus graph stats reads the first of those as file doc coverage.

Resolution never asks where the site is deployed, because nothing declares it. It matches the URL's trailing path against the page's own path with the docs/ prefix dropped, and an ambiguous match resolves to nothing rather than guessing.

Nothing is fetched, ever. A link node asserts that something here points there, never that anything is at the other end. Only text a LEXER classified as a comment is scanned, so a URL in a string literal is out of reach by construction, and a citation must clear a closed scheme set (http, https), carry no userinfo, sit under a length cap, and name a host that is not an IP literal, a single label, or an RFC 2606 reserved name. Those rules exist because comments are full of URL-shaped prose written to show a format (http://<host, https://endpoint/bucket/key, http://127.0.0.1:7391@evil.com), and a link node minted from one is a phantom nothing can retire.

Git history (@vcs)

Opt-in, off by default: enable it to fold each file's git history onto its file node.

# magus.yaml
knowledge:
  vcs:
    enabled: true
    max_commits: 1000 # optional: bound the history walk (default 1000)
    authorship: true # optional: include author nodes + authored edges (default on)

When enabled and the workspace is a git repo, a @vcs shard adds four attrs to every file node: vcs_last_commit (short SHA of the most recent commit touching the file), vcs_last_modified (its date), vcs_last_author (who last touched it), and vcs_commits (commits touching the file within the window). It also mints an author node per contributor with an authored edge to each file they touched in the window: who edits what, so an agent can ask magus explain author:Ada or trace ownership. These edges are uncapped: max_commits already bounds the scan, so a dominant maintainer legitimately having many is a fact to teach, not a smell. Set authorship: false to keep only the per-file vcs_* attrs and drop the author layer.

The values are EXTRACTED from git and deterministic per commit, so the shard is remote-shareable like the other extracted shards. The git log walk is bounded by max_commits and keyed by an input fingerprint (schema, HEAD, the window, the dirty-file set, and the authorship flag), so the standard shard store reuses it whole and it re-runs only when one of those actually moves, never on the query path. A non-git workspace or a git error simply yields no shard. Because the vcs_* attrs vary by commit, magus graph diff strips them from both sides, so a file node is not reported as changed just because its last commit moved; the diff stays structural.

Human-authored notes

Everything else in the graph is DERIVED from the workspace: a doc from markdown, a rationale from a comment, a symbol from an index, an author from git history. Delete the graph, rebuild it, and all of that comes back.

A note is the exception. Its content originates with a person, nothing in the repository corroborates it, and no rebuild recovers it. That single property is why the store looks the way it does.

If you have played Dark Souls, you have already used this. Players there cannot talk to each other; they can only leave a short message on the ground where they are standing, and everyone who passes that spot afterwards reads it. A few are jokes. Most are someone who just got caught by something, telling you what caught them, and they beat any wiki, because they are lying exactly where you needed them.

That is what a note is for, and the rest of this section is the machinery that keeps the promise. Not documentation filed somewhere central and read by nobody: one sentence left at the spot in the code that earned it, for whoever arrives next. An anchor is how it stays at that spot when the code moves, verify is how it speaks up when the spot quietly stops meaning what the note said, and git is how the person who left it has their name on it.

Where a note goes, and what that means

There is one question that matters to a reader: may I act on this without checking it? The answer the store gives is that somebody accountable pressed commit. An agent may never write a note, but the rule is not really "a human typed this": a person pasting an agent's prose into $EDITOR always passed it. What git records is who stood behind it.

magus notes edit opens a scaffold and gets out of the way. magus notes capture files the review conversation under way, your remarks and your colleagues', as one note anchored to the hunks it discussed, so the comment that mattered is findable after the pull request merges.

knowledge:
  notes:
    shared: notes        # a directory IN the repo: committed, so git records who wrote each note

One sentence each, and the whole surface follows from them:

  • notes.shared: a person stands behind it, the team has it, git says who. Must live inside the checkout; outside it there is no commit to attribute a note to and no review to have seen it, so "shared" would be a claim the location cannot back.
  • knowledge.notes.private: superseded, still read. A second notes location, yours rather than the team's, anywhere on disk. It is no longer the recommended shape: a private note is uncommitted, unattributed, unreviewed and unrecoverable, so nothing a reader can check stands behind it. Existing stores keep working; new workspaces should not declare one.

Every note node carries a scope attr (shared or private), so a reader can always tell which of the two they are looking at without knowing which shard it came from.

What a note looks like, and living in a vault

Everything magus stores sits under one magus: frontmatter key. Nothing outside it belongs to magus, and nothing inside it belongs to anyone else:

---
tags: [architecture, cache]   # yours (or Obsidian's); magus never reads or rewrites these
aliases: [pairing]            # yours
magus:                        # magus's, all of it
  id: cache-pairing           # optional, stable identity - survives moving or renaming the file
  title: The two caches invalidate together
  anchors:
    - kind: symbol            # survives the code moving file and line; breaks on rename or delete
      target: "m internal/cache/Store#Put()."
    - kind: file              # a path, when no single symbol covers it
      target: internal/cache/cache.go
---

Nothing in the code says these must be cleared together.

That key is also the opt-in. A file with no magus: block is not a magus note, no matter what else is in it. This is what makes pointing notes.private at an Obsidian vault work: a vault of a few thousand files contributes only the handful you actually anchored, and the rest are read past in silence rather than reported as malformed notes.

Three consequences worth knowing:

  • magus never rewrites your other frontmatter. When it records an anchor fingerprint it replaces the magus: block and leaves every other key, and their order, exactly as they were.
  • id is what survives a reorganization. Without it a note is identified by its path, so renaming the file in Obsidian (which rewrites your [[wikilinks]] and knows nothing about magus) changes the note's identity and dangles anchors pointing at it. magus stamps an id on every note it creates; add one by hand to a vault note you intend to keep. Notes are looked up by id first, then by path.
  • Nested folders are walked, dot-directories (.obsidian, .trash) are skipped, and a scan stops at 5,000 markdown files and says so rather than silently loading half a vault.

symbol, file, project, target, and note are the whole set. There is deliberately no line-anchored kind: a node ID is checkable, so its breakage is reportable, while a line number changes on the next edit above it with nothing to detect. Anchor as narrowly as the knowledge allows: a coarse anchor is more durable, and multiple anchors are how a note records something no single comment could hold ("these two caches must be invalidated together").

Staying honest

magus notes verify answers two questions per anchor:

  • Does it still resolve? A renamed or deleted subject reports dangling-anchor, with the coarser anchor it degrades to. Nothing is ever re-pointed at a guess.
  • Did the content change? A stored fingerprint of the anchored definition detects the more common and more dangerous case: the code still exists and quietly stopped saying what the note claims. The fingerprint ignores whitespace, so reformatting is free, and reacts to tokens, so a real edit is loud.

Recording that fingerprint is a deliberate human act: magus notes edit stamps it, because the person just had the note and its subject in front of them. Verify only ever reports.

Retrieval labels, and never reorders. A match whose subject has moved on carries its verdict and outrun_days so a reader can see "400 days behind its subject" beside the result, but it keeps its rank.

Ranking on staleness was tried and removed, because it points the wrong way. Code that keeps changing is where knowledge is worth most: relative churn is one of the better predictors of defect density, so demoting the prose about a moving subject hides it exactly where it is needed. Elapsed days measure calendar time rather than divergence: a note and its subject both untouched for a year are settled, not stale. And prose whose subject is gone is often the only surviving record of why it went, which is the last thing a search should bury. Systems that solved this in production reached the same answer: Google's g3doc carries a "last reviewed" byline, and Guru keeps an unverified card searchable and visible with its lapsed state shown. Both label. Neither demotes.

Exporting to external tools

magus emits; it does not render. To look at the graph, export it and open the file in a graph tool; files are the interface.

magus graph export -o json > graph.json       # node-link JSON (NetworkX, D3, ...)
magus graph export -o graphml > graph.graphml  # GraphML (Gephi, yEd, ...)

For a specific neighborhood rather than the whole graph, --select reuses the query engine, and the layout format, DOT, becomes available (it is unreadable on the full graph, so it requires a scope):

magus graph export --select "kind=spell go" -o dot
magus graph export --select "project=pkg/foo" --budget 80 -o dot

Diffing against a baseline

magus graph diff reports what a branch did to the domain's shape: the nodes and edges added or removed, and (for nodes) which fields changed. Export a baseline on the base branch, then diff the working tree against it: the PR blast-radius artifact.

magus graph diff --rev HEAD~1                    # against a git revision, no export needed
magus graph diff --rev main -o markdown          # a CI comment vs the base branch

git stash && magus graph export -o json > /tmp/base.json && git stash pop
magus graph diff /tmp/base.json                  # against an export file
magus graph diff /tmp/base.json -o json          # machine-readable, with before/after

--rev builds the base graph from that revision's tracked files (domain-only, using the current config) in an isolated throwaway tree that never touches your real cache; it and the positional baseline are mutually exclusive, and it cannot be combined with --global (the base is a single-workspace build). A baseline file must be a whole-graph magus graph export -o json (symbol shards in it are matched automatically; pass --global if the baseline was global). Edge diffs are structural: an edge is identified by (source, target, relation), so a re-scored or re-provenanced edge that keeps those three is not reported as a change.

Global graph (across workspaces)

An org running magus across many repos can query all of them at once. Register extra workspace roots in config, then pass --global:

# magus.yaml
knowledge:
  workspaces:
    - ../api
    - ../web
magus query "kind=spell" --global   # matches across every registered workspace
magus graph stats --global          # union shape across repos

--global is available on query, explain, path, and magus graph export/stats. Each workspace's node IDs are namespaced by the workspace (api//spell:go, web//spell:go) so IDs from different repos cannot collide; the unqualified ID stays a readable substring, so magus explain go --global still resolves. A registered workspace that cannot be opened is skipped rather than failing the query. There is no cross-workspace edge inference: a union with qualified IDs only.

Extraction diagnostics

When extraction cannot resolve something cleanly it records a silent MGS7xxx code as a node attribute (visible via magus explain), rather than logging, so an implicit rebuild stays quiet while the ambiguity stays queryable. The first two are MGS7001 (a buzz import that resolves to no file) and MGS7002 (a doc citing an unregistered code).

For agents

The MCP server exposes those verbs through the client tool, which runs magus\query, magus\explain, magus\path, magus\stats, magus\refs, and magus\output (a target's captured output by ref). See MCP for wiring. Prefer these over grep to find and relate magus-domain entities; start from the MAGUS.md routing table, which is already in context in a fresh clone.

For a large result set, magus\query and magus\refs take limit and offset and still report the full count. Filter in the script before returning: the client tool does not page a result for you.

magus agent install .agents/skills equips Codex with Agent Skills, and prints the always-on AGENTS.md block for you to paste; magus never writes that file. Claude Code uses magus agent install .claude/skills. The skills teach HOW to use magus (the repo's MAGUS.md says WHAT is in the workspace): knowledge-graph verbs, target-first execution, generated-file triage, and graph-grounded refactoring. They ship with the binary and teach only the tool surface, so they stay current with the magus version rather than the workspace. Each installed file carries a version footer, and magus doctor reports actionable drift after an upgrade. See Agents for the full host setup.

Prior art

Two projects shaped this design, and both deserve credit. The thread starts earlier than either: Andrej Karpathy's April 2026 post on X described keeping a raw folder of papers, notes, and screenshots and wanting to query across it without rereading every file. Graphify was built within days as a direct answer to that post, and the magus knowledge graph is a further step down the same path: the querying idea applied to the one domain a build tool already understands precisely.

Graphify established the pattern of a queryable, committable code knowledge graph with an honest audit trail, and its verb vocabulary was good enough that magus reuses it outright: query, explain, path. The two tools have different jobs, though. Graphify is a general document indexer: point it at any folder of code, docs, papers, or media and it extracts a graph, using an LLM pass for non-code content. magus only ever models its own domain, and its graph is assembled entirely from declarations it already verifies as a build tool (the project DAG, target sources and outputs, spell and module registries), so the build is deterministic, runs with zero LLM involvement, and stays cache-owned rather than committed. If you want a graph of an arbitrary document set, Graphify is the right tool; the magus graph is narrower and, within its domain, checkable edge by edge.

Obsidian shaped the notes side: durable, linked markdown the user owns and any tool can read. magus borrows that files-first stance deliberately, but keeps the scope small: prose a person stands behind, anchored to the code it is about. magus notes verify makes malformed notes and anchors that no longer resolve visible instead of quietly skipping them.

knowledge graphqueryexplainpathgraphschemanode-linkgraphmlmcp
Last updated (895d94e4)
Earlier changes on this page (7)

Full history ↗ · Blame source ↗

Glossary

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. See workspace.

Project

A directory magus recognizes as a unit of work (it has a magusfile); the unit of caching, scheduling, and dependency tracking. See workspace.

Magusfile

The magusfile.buzz that declares a project's targets (as export funs) and binds its spells. See targets.

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. See targets.

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). See operations.

Spell

A language/runtime adapter (e.g. go, md) that maps generic targets onto a toolchain's real commands. See spells.

Manifest

The file that declares what a package is: its name, dependencies, entry points and tool config (go.mod, package.json, pyproject.toml, Cargo.toml). How the workspace builds, tests and lints the package belongs in the magusfile, not in the manifest's scripts. See spells.

Charm

An execution modifier attached with : (lint:rw) that changes how a target runs, not which one; the built-in rw flips a check-only target to mutate in place, and ci always strips it. See charms.

Ward

A coded diagnostic that inspects a resolved op and nudges or blocks an anti-pattern before it runs. See wards.

Module

A magus stdlib namespace a magusfile imports for host capabilities: filesystem, exec, vcs, and more. See the module reference.

Buzz

The language magusfiles are written in (the .buzz engine). See engines.

Engine

The interpreter a magusfile runs on; magus embeds the Buzz engine. See engines.

Cache

The content-addressed store magus consults before running a target, so unchanged work is skipped. See cache.

Server

The background process a person starts with magus server start. It serves MCP, the console, background jobs and the warm knowledge graph, and adopts nested magus calls into one pool. See server.

CI

An ordinary magusfile-defined target you compose yourself with magus\needs: magus does not hardcode its stages. Magus.RunCI treats it specially only in that it strips the rw charm, it is the anchor magus affected ci keys off, and a selected scope with no project declaring it is a load error rather than a silent no-op. See targets.

Trace

OpenTelemetry's name for one whole magus invocation; every target it runs is a span beneath it. See telemetry.

Span

OpenTelemetry's name for one unit of work under a trace: a target execution, whose sub-operations are child spans. An output reference points at a span's captured output. See telemetry.

Slot

One unit of the pool's capacity. A target acquires the slots it needs to run (most take one) and releases them when it finishes; the pool tracks capacity (total slots), running (acquired), and queued (blocked). See server.

Snapshot

A point-in-time view of live state (the pool's occupancy or a tick of exported metrics), as opposed to accumulated history. See server.

Knowledge graph

The queryable graph of a workspace's spells, targets, docs, and code relationships; query it with magus query/explain/path. See knowledge.

MAGUS.md

The committed routing index at a workspace root, regenerated from the knowledge graph: it lists every node and points at the exact query for a given question, so it is the entry point an agent reads first. See knowledge.

Insight

The reports magus derives over the graph and history (hotspots, affinity, ownership, trend, volatility, unreferenced). See insight.

Hotspot

An insight lens: edit frequency times complexity, the prime refactoring targets. The project view heat-colors the dependency graph by churn; --files ranks individual files. See insight.

Affinity

An insight lens: projects that change together (temporal coupling). A pair that co-changes without either declaring a dependency on the other is a candidate architectural smell. See insight.

Ownership

An insight lens for author concentration: the primary author and their share, the distinct-author count (the bus factor), and abandonment. See insight.

Session

An agent host's conversation, by the id the host delivers to its hooks. magus never mints one: a record with no session is unattributed, and the OS user it carries says whose account ran it.

Invocation

One magus process's recorded facts (the targets it finished, their outcomes, the lease it acted as, and the session it ran in when a host delivered one), kept in a repo-scoped store every worktree shares. magus session lists them; the store prunes itself by last-fact age.

Window

The terminal a command runs in. It keys fire-once notices for a caller no host gave a session, and is never recorded as a session.

Job

The unit of delegated work, and one row of the job store: what an orchestrating agent handed out, with its goal, the checkpoint it was cut against, the paths it may write or must not touch, and the one check it runs. A job's holder is either a session, for work an orchestrator handed out, or the server, for its own maintenance. The store records; the agent guard is what reads those facts back when grading a write. See doctrine.

A job is not a run. magus run build web is a run, and no job exists for it. A job causes runs: its check executes as one, and a server job records the invocation of its last one. Jobs are listed with magus ls jobs and in the console's Jobs view; runs are listed in the Runs view.

Run

One target executing under one magus invocation, such as magus run test web or magus affected ci. A run keeps its captured output behind an output reference. Every magus run is a run whether or not any job asked for it; see Job for how the two relate.

Lease

The grant a holder takes on a job: the write and read paths that job declared, enforced in the checkout that took it with magus job exec. A job is the piece of work; a lease is permission over it.

Conventions

Placeholders

Angle brackets mark a value you replace with your own; never type the brackets:

magus run <target>
magus completion <shell>    # e.g. bash, zsh, fish

<target>, <path>, <shell>, <name> and the like are stand-ins, not literal text.