authored intent vs extracted truth
stele compiles a typed knowledge graph from three authored sources — the file tree, comment anchors, and typed blocks in AGENTS.md files — and reconciles it against a graph extracted from the code. CI asserts the two against each other in both directions: an undeclared dependency means the code broke the signature, an unbacked declaration means the doc lied. The same graph answers agent queries and materializes standard-compliant AGENTS.md files, so every harness gets the full content with zero engine.
stele /ˈstiːli/ — an inscribed standing stone that publicly declares the law. The territory is checked against it.
Install
One static executable. The installer resolves a release, verifies the
sha256, and drops the binary in ~/.local/bin.
curl -fsSL https://raw.githubusercontent.com/jgeschwendt/stele/main/scripts/install.sh | bash
# pin a version curl -fsSL https://raw.githubusercontent.com/jgeschwendt/stele/main/scripts/install.sh | bash -s v0.2.0 # prereleases curl -fsSL https://raw.githubusercontent.com/jgeschwendt/stele/main/scripts/install.sh | channel=canary bash
# from source
cargo install --git https://github.com/jgeschwendt/stele stele-cli
The degradation ladder
Files → CLI → MCP is a spec invariant, not an implementation detail. Each rung adds capability; none becomes a reading dependency.
stele binary adds verification and lazy
navigation: query verbs read the committed lock, and check enforces
the graph against the code in CI.stele serve speaks JSON-RPC 2.0 over stdio and
exposes the read verbs as tools. Never required — every tool has a CLI equivalent, every CLI
answer is derivable from the files.Adoption path
Bare repo to CI-enforced graph. The long form is
GUIDE.md; every rule behind it is in the SPEC.
- Install — one binary, pinned in CI to the version you tested against.
- Scaffold —
stele initfrom the repo root scaffolds AGENTS.md skeletons: asystemnode at the root,containernodes for candidate directories. Fill inpurpose, realcommands, and the claims worth enforcing; delete the skeletons that don't deserve a node. The graph follows interface boundaries, not the tree. - Anchor your claims — every invariant and hazard names an anchor:
path#symbolfor a definition or markdown heading, orlm:<slug>for a named landmark comment that survives renames and moves. Anchors are what make a claim checkable. - Build —
stele buildcompiles authored sources into.stele/graph.lockand.stele/index/. Build is the reconciliation point: it re-stamps each claim's verified mark against current code, so run it locally once you've reviewed that the claim still holds. - Emit —
stele emitrenders the router regions into AGENTS.md, the transpose indexes, and the shim. Anything outside a marker region stays yours. - Commit — the AGENTS.md files, the lock, the indexes. The lock is the
canonical graph: queries and CI read it and never re-derive it. Paths the engine should never
scan go in a root
.steleignore, gitignore syntax. - Enforce — CI checks out full history (freshness walks blame), then runs
stele checkfollowed bystele emit --check. CI never runsstele build— that would launder staleness. - Query — agents and humans read the graph instead of grepping docs, over the CLI or MCP.
stele init # scaffold nodes stele build && stele emit # compile the lock, render the files stele check # six assertion classes, exit 1 on violations stele emit --check # committed files vs what the lock renders
# the CI gate - uses: actions/checkout@v5 with: fetch-depth: 0 # full history — freshness checks use blame - run: curl -fsSL https://raw.githubusercontent.com/jgeschwendt/stele/main/scripts/install.sh | bash -s v0.2.0 - run: ~/.local/bin/stele check - run: ~/.local/bin/stele emit --check
The assertion suite
Six classes over lock vs. code. stele check exits
nonzero with findings; --only <class> narrows it,
--run-commands actually executes documented commands,
--freeze accepts the current structure as the baseline, and
--json gives the machine envelope.
| class | catches |
|---|---|
| referential | anchors pointing at symbols, landmarks, or files that no longer exist |
| structural | declared dependencies the code doesn't have — and code dependencies never declared |
| exhaustiveness | directories no node covers — unrouted context an agent can't navigate to |
| budget | node docs exceeding their token budget |
| freshness | anchored code changed since the claim was last verified (stele blame <node>/<slug> shows who and when) |
| liveness | documented commands: that no longer resolve or run |
stele emit --check is the
seventh gate in practice: it fails when committed AGENTS.md files diverge from what the lock
would render.
Query surface
Plain subcommands and flags — no query grammar. The cross-cutting queries are the capability nested files cannot provide at any size: the transpose of the whole graph, materialized from one lock.
stele root # the system node — start here stele unfold payments --depth 2 # a node with its children stele invariants --touching src/charge.rs # claims governing the file you're editing stele hazards # every active hazard stele nodes --kind container # the graph by kind stele blame payments/idempotent-charges # provenance of one claim
For MCP-capable harnesses,
stele serve exposes the same read verbs as tools:
{ "mcpServers": { "stele": { "command": "stele", "args": ["serve"] } } }
Undercover mode
v0.2.0's headline. When you're the only stele user on a shared repo — a work, upstream, or open-source checkout whose collaborators must never see a stele artifact — run the graph privately.
stele init --undercover
- the graph home is the parent of the git common dir — the repo root in a normal checkout, the shared bare root in a grove layout, so every linked worktree reads the same private graph and it survives worktree churn.
- node sources live in an untracked overlay —
<home>/.stele/tree/<dir>/AGENTS.md, mirroring tree paths, alongside the private lock and transpose indexes. - one materialized file —
CLAUDE.local.md, a relative@-import of the overlay root that Claude Code auto-loads. Analogous to any other.localfile: yours, never committed. - git status stays clean — a managed block in the
common dir's
info/excludehides the private paths. Only the marker's presence selects the mode; without it every command is byte-for-byte normal. - the ladder trades its files rung for invisibility —
no CI story, because nothing is committed for a pipeline to gate;
stele checkis yours to run locally, and your surface is CLI → MCP plus the shim.
Artifacts
The repository carries the whole design; this page is a summary of the first two.
| artifact | what it is |
|---|---|
| GUIDE.md | the adoption path — install, scaffold, anchor, build, enforce, query, undercover |
| SPEC.md | the contract — model, sources, assertion suite, query surface, process contract |
| EXAMPLE.md | worked example on a fictional monorepo: every artifact, an agent session with token accounting, a CI failure gallery |
| research/report.md | the evidence base — cited survey the design decisions trace to |
| jgeschwendt/stele | the engine — a Rust single binary, self-hosting on its own graph |
stele v0.2.0 · SPEC Draft 0.9 · this page mirrors README.md and GUIDE.md; the SPEC is the contract.