Convergence assistant for knowledge corpora.
anneal reads a directory of markdown files, computes a typed knowledge graph, checks it for local consistency, and tracks convergence over time. It helps disconnected intelligences — agents across sessions with no shared memory — orient in a body of knowledge, recover relevant context quickly, and push it toward settledness.
A knowledge corpus grows across many sessions. No single agent sees the full history. Documents reference each other, supersede each other, track obligations. Without tooling, every arriving agent has to read everything to understand what's settled, what's drifting, and what's connected to what.
anneal makes that orientation instant:
$ anneal status
corpus 84 files, 1205 handles, 892 edges
1140 active, 65 frozen
pipeline 8 raw -> 5 draft -> 12 review -> 3 approved -> 6 published
health 23 errors, 11 warnings
convergence advancing (resolution +4, creation +2, obligations 0)
suggestions 7
4 S001 orphaned handles
2 S003 pipeline stalls
1 S005 concern group candidates
curl -fsSL https://raw.githubusercontent.com/flowerornament/anneal/master/install.sh | bashThe installer is the primary path for prebuilt binaries. It installs to ~/.local/bin by default, prints the exact target / URL / destination before it writes anything, fails fast on unsupported targets, and stays aligned with the published release matrix.
By default it installs only the binary. To also install the bundled anneal skill, pass one or more explicit skill targets. Each target is a directory path that anneal will populate with the bundled skill files.
Common variations:
# Install somewhere else
curl -fsSL https://raw.githubusercontent.com/flowerornament/anneal/master/install.sh | INSTALL_DIR="$HOME/bin" bash
# Same override, but passed as an installer flag
curl -fsSL https://raw.githubusercontent.com/flowerornament/anneal/master/install.sh | bash -s -- --install-dir "$HOME/bin"
# Preview what would happen without writing anything
curl -fsSL https://raw.githubusercontent.com/flowerornament/anneal/master/install.sh | bash -s -- --dry-run
# Install the bundled skill into agent-managed locations
curl -fsSL https://raw.githubusercontent.com/flowerornament/anneal/master/install.sh | bash -s -- \
--skill-target "$HOME/.agents/skills/anneal" \
--skill-target "$HOME/.codex/skills/anneal"Binaries available for: aarch64-apple-darwin, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu.
git clone https://github.com/flowerornament/anneal.git
cd anneal
cargo install --path . --lockedRun without installing:
nix run github:flowerornament/annealInstall into your profile:
nix profile install github:flowerornament/annealAdd as a flake input:
anneal = {
url = "github:flowerornament/anneal";
inputs.nixpkgs.follows = "nixpkgs";
};This installs the binary only unless you additionally wire skill installation
yourself. anneal keeps the same runtime config layout no matter how it was
installed:
- repo config:
anneal.tomlat the corpus root - user config:
$XDG_CONFIG_HOME/anneal/config.toml - derived history:
$XDG_STATE_HOME/anneal/...
For a declarative Nix-native setup, use the exported Home Manager module. It
installs anneal, writes the same XDG user config that non-Nix setups use,
and can optionally install the bundled anneal skill into agent-managed skill
directories.
Add the flake input:
anneal = {
url = "github:flowerornament/anneal";
inputs.nixpkgs.follows = "nixpkgs";
};Then include the module in your Home Manager configuration:
{
imports = [
inputs.anneal.homeManagerModules.default
];
programs.anneal = {
enable = true;
settings.state.historyDir = config.xdg.stateHome;
skill.enable = true;
skill.targets = [
".agents/skills/anneal"
".codex/skills/anneal"
];
};
}Available module options:
programs.anneal.enableprograms.anneal.packageprograms.anneal.settings.state.historyModeprograms.anneal.settings.state.historyDirprograms.anneal.skill.enableprograms.anneal.skill.targets
Repo-owned corpus behavior still lives in anneal.toml. Skill targets are
home-relative symlink paths, so you can install the anneal skill anywhere your
agent tooling expects it.
If you already use Home Manager through nix-darwin, add the exported module to
home-manager.sharedModules (or otherwise make it available in your shared
Home Manager module set), then enable programs.anneal in the user config that
owns your shell environment.
# Corpus shape, health, and convergence direction
anneal status
anneal status --json --compact
# Actionable issues in active work
anneal check
# Resolve a specific handle
anneal get REQ-12
anneal get REQ-12 --context
# Search handle identities
anneal find ADR --limit 25
# Reverse dependencies for a file or handle
anneal impact spec/api-v3.md
# Local graph neighborhood
anneal map --around=REQ-12 --depth=1
anneal map --render=text --full
# Linear namespace summary
anneal obligations
# Snapshot delta
anneal diff
# Infer configuration
anneal initanneal supports a practical loop for corpus work:
- Orient: run
anneal statusoranneal status --json --compact, thenanneal check, to understand the corpus shape and the actionable problems. - Locate context: use
anneal get --context, boundedanneal find --limit ..., andanneal map --around=...to understand the specific files, labels, or versions involved in the task. - Assess impact: run
anneal impact <file-or-handle>before editing to see what depends on the thing you are about to change. - Verify: run
anneal check --file=...for a local pass oranneal checkfor a broader pass after editing. - Review accumulated change: run
anneal diffto see what changed since the last snapshot, even when no single agent saw those changes happen.
This is what the tool buys you in practice: quick context recovery, structural inspection, and safer edits in a corpus that outlives any one session.
Handles are the unit of knowledge. Five kinds:
| Kind | Example | Description |
|---|---|---|
| file | spec/api-v3.md |
A markdown document |
| section | api-v3.md#authentication |
A heading within a file |
| label | REQ-12 |
A cross-reference tag (namespace + number) |
| version | v3 |
A versioned artifact |
| external | https://example.com/spec |
An external URL referenced from corpus metadata |
Edges are typed relationships between handles:
| Edge | Meaning |
|---|---|
| Cites | References without dependency |
| DependsOn | Structural dependency |
| Supersedes | Version chain (v3 supersedes v2) |
| Verifies | Formal verification link |
| Discharges | Obligation fulfillment |
Status is a frontmatter status: field, partitioned into active (in-progress) and terminal (settled). When pipeline ordering is configured, anneal shows a histogram of handles flowing through status levels.
Convergence tracks whether the corpus is advancing (more handles reaching terminal state), holding (stable), or drifting (creation outpacing resolution). Measured by comparing snapshots over time.
Single-screen dashboard for quick orientation. Shows corpus size, active/frozen partition, pipeline histogram, health (errors + warnings), convergence direction, and suggestion breakdown.
$ anneal status -v
corpus 84 files, 1205 handles, 892 edges
1140 active, 65 frozen
pipeline 8 raw -> 5 draft -> 12 review -> 3 approved -> 6 published
raw (8):
notes/2025-01-15-auth-redesign.md
notes/2025-01-20-rate-limiting.md
...
Appends a snapshot to local anneal history for convergence tracking. By default this is machine-local XDG state, not a repo file.
For agents and other tools, prefer:
anneal status --json --compactFive check rules and five suggestion rules with compiler-style diagnostics:
$ anneal check
error[E001]: broken reference: auth-flow.md not found
-> spec/api-v3.md
error[E001]: broken reference: REQ-99 not found
-> decisions/ADR-005.md
23 errors (23 in active files), 11 warnings, 1 info, 7 suggestions
anneal check reports active-file diagnostics by default. Use --include-terminal when you want the full picture, including settled material.
For interactive health checks, prefer the plain-text report. JSON is summary-first by default, then expands deliberately with explicit flags.
| Flag | Effect |
|---|---|
--include-terminal |
Include diagnostics from terminal (settled) files |
--active-only |
Skip diagnostics from terminal (settled) files |
--errors-only |
Errors only (for CI/pre-commit) |
--suggest |
Structural suggestions only (S001–S005) |
--stale |
Staleness warnings only (W001) |
--obligations |
Obligation diagnostics only (E002/I002) |
--file=path.md |
Scope diagnostics to a single file |
--diagnostics |
Include bounded diagnostics in JSON output |
--limit=N |
Cap JSON diagnostic samples |
--extractions-summary |
Include aggregate extraction facts in JSON output |
--full-extractions |
Include full extraction payloads in JSON output |
--full |
Explicitly request full diagnostics + extractions |
Resolve a handle and show bounded graph context:
$ anneal get REQ-12
REQ-12 (label)
File: requirements/REQUIREMENTS.md
Snippet: Requirements (REQ-): | REQ-12 | Authentication requests must be signed | draft |
Incoming:
Cites <- spec/api-v3.md
Cites <- decisions/ADR-003.md
Cites <- notes/2025-01-15-auth-redesign.md
Useful expansions:
anneal get REQ-12 --context # compact agent briefing
anneal get REQ-12 --refs # bounded incoming/outgoing references
anneal get REQ-12 --trace --full # full adjacency
anneal get REQ-12 --json # summary JSON with counts and samplesget includes a snippet when anneal can extract one from the source file. JSON reports edge counts and truncation by default instead of dumping the full adjacency list.
Reverse dependency traversal — what's affected if a handle changes:
$ anneal impact spec/api-v3.md
Directly affected (depend on this):
spec/api-v2.md
Indirectly affected (depend on the above):
spec/api-v1.md
archive/api-draft.md
Traverses DependsOn, Supersedes, and Verifies edges in reverse. Does not traverse Cites (citations are not dependencies).
Render or summarize the knowledge graph:
anneal map # Graph summary
anneal map --around=REQ-12 --depth=1 # Focused text neighborhood
anneal map --render=text --full # Full active graph (text)
anneal map --render=dot --full | dot -Tpng -o g.png
# Graphviz PNG
anneal map --json # Summary JSON
anneal map --json --nodes --limit-nodes 50 # Structured node sampleWhat changed since last session:
$ anneal diff
Since last snapshot:
Handles: +12 (+8 active, +4 frozen)
State: draft: 3 -> 5 (+2)
Obligations: +0 outstanding, +2 discharged, +0 mooted
Edges: +47
Three reference modes: last snapshot (default), --days=N (time-based), or a git ref (HEAD~3, main) for structural diff.
By default, diff reads the same machine-local snapshot history used by status and check. If you previously used repo-local .anneal/history.jsonl, anneal will continue reading that legacy history for compatibility.
Summarize outstanding, discharged, and mooted obligations for configured linear namespaces:
$ anneal obligations
Obligations: 3 outstanding, 12 discharged, 1 mooted
REQ
Outstanding:
REQ-14
REQ-19
REQ-22Use --json for machine-readable totals and per-namespace buckets.
Search handle identities:
anneal find ADR # Bounded active ADR-* sample
anneal find ADR --limit 50 # Larger sample
anneal find "" --status=draft # Broad but narrowed query
anneal find "" --kind=file --all # All files including terminal
anneal find "" --status=draft --json
# JSON sample with counts, truncation, and facetsfind is bounded by default. Use --offset to page through results or --full when you intentionally want the full match set.
Generate anneal.toml from inferred corpus structure:
anneal init # Write anneal.toml
anneal init --dry-run # Preview without writingInfers active/terminal partition, label namespaces, and frontmatter field mappings. Pipeline ordering and linear namespaces require manual tuning.
anneal.toml at the corpus root. Optional — anneal works without it (existence lattice mode: reference checking only).
anneal separates repo-owned corpus behavior from machine-local runtime preferences:
- Repo config lives in
anneal.tomlat the corpus root. - User config lives in XDG config:
$XDG_CONFIG_HOME/anneal/config.toml- fallback
~/.config/anneal/config.toml
- Derived snapshot history lives in XDG state by default:
$XDG_STATE_HOME/anneal/...- fallback
~/.local/state/anneal/...
This keeps automatic convergence tracking and anneal diff useful without dirtying the git worktree during normal use.
[convergence]
active = ["draft", "review", "approved"]
terminal = ["published", "archived", "superseded"]
ordering = ["raw", "draft", "review", "approved", "published"]
[handles]
confirmed = ["REQ", "ADR", "RFC"]
rejected = ["SHA", "GPT"]
linear = ["REQ"] # obligations: must be discharged exactly once
[suppress]
codes = ["I001"] # optional global suppressions
[[suppress.rules]]
code = "E001"
target = "synthesis/v17.md"
[freshness]
warn = 30 # days before staleness warning
error = 90 # days before staleness error
[frontmatter.fields.depends-on]
edge_kind = "DependsOn"
direction = "forward"
[frontmatter.fields.superseded-by]
edge_kind = "Supersedes"
direction = "forward"
[concerns]
api = ["REQ", "ADR"]
[state]
history_mode = "xdg" # optional: xdg | repo | offanneal.toml controls corpus semantics: statuses, namespaces, suppressions, frontmatter mappings, concern groups, and the history backend mode (xdg, repo, or off).
If you want repo-local snapshots, set:
[state]
history_mode = "repo"User config controls machine-local preferences such as the base directory for derived history:
[state]
history_dir = "/Users/alice/.local/state"Important boundary: repo config can choose whether history is machine-local, repo-local, or disabled, but it cannot choose an arbitrary machine-local path. Only user config can override history_dir.
For Nix users, the Home Manager module writes this same user config file declaratively. It does not introduce a separate anneal-specific config path or runtime mode.
| Code | Severity | Description |
|---|---|---|
| E001 | Error | Broken reference — handle not found |
| E002 | Error | Undischarged obligation — linear handle without Discharges edge |
| W001 | Warning | Stale reference — active handle references terminal one |
| W002 | Warning | Confidence gap — higher pipeline level depends on lower |
| W003 | Warning | Missing frontmatter — file without status: field |
| I001 | Info | Section reference summary |
| I002 | Info | Multiple discharges on single obligation |
| Code | Description |
|---|---|
| S001 | Orphaned handles — labels/versions with no incoming edges |
| S002 | Candidate namespaces — recurring prefix not in confirmed list |
| S003 | Pipeline stalls — status level with no outflow to next level |
| S004 | Abandoned namespaces — all members terminal or stale |
| S005 | Concern group candidates — label prefixes co-occurring across files |
All commands support --json for machine consumption. Risky commands now use progressive disclosure:
- default JSON is bounded and explicit about truncation
- expansion flags like
--diagnostics,--refs,--nodes, and--fullrequest more detail --prettyexists for humans; plain--jsonstays compact for tools
Use JSON for compact facts and jq-filtered summaries rather than dumping full structured output back into chat:
anneal status --json --compact | jq '.convergence'
anneal check --active-only --json | jq '.summary'
anneal check --active-only --json --diagnostics --limit 25 | jq '.diagnostics[:5]'
anneal find ADR --json | jq '._meta'
anneal map --json | jq '{nodes, edges, by_kind}'On every invocation, anneal walks a directory of markdown files and builds a typed knowledge graph in memory:
- Parse — split YAML frontmatter from body text, extract typed references, and scan markdown structure with pulldown-cmark
- Resolve — infer label namespaces from sequential cardinality (REQ-1 through REQ-50 is a namespace; SHA-256 is not), resolve cross-references to graph nodes with deterministic fallback candidates
- Lattice — partition observed
status:values into active and terminal sets, optionally infer from directory conventions (files only inarchive/→ terminal) - Check — run five consistency rules and five suggestion rules against the graph, then apply optional suppressions
- Snapshot — capture counts to local anneal history for convergence tracking over time
No persistent database. The graph is ephemeral — rebuilt from files each run. The only state is the append-only snapshot history, which is derived, deletable, and machine-local by default.
The underlying model borrows a simple idea from type theory and static analysis: statuses are ordered, so the tool can compare neighboring handles and ask questions like "is this dependency less settled than the thing that depends on it?" The active/terminal split is the simplest form of that ordering. A configured pipeline turns it into a richer progression. Convergence tracking measures how the population moves through that progression over time. Checks stay local — they compare a handle with its immediate neighbors rather than trying to solve a whole-program global proof — which keeps the tool fast and predictable.
src/
handle.rs Handle, HandleKind, NodeId
graph.rs DiGraph with dual adjacency lists
parse.rs Frontmatter + markdown scanning
extraction.rs Typed reference extraction
resolve.rs Handle resolution across namespaces
lattice.rs Convergence lattice, freshness
config.rs anneal.toml parsing
checks.rs Check rules + suggestion rules
impact.rs Reverse graph traversal
snapshot.rs JSONL history, convergence summary
cli.rs Commands + output formatting
style.rs Terminal styling via console crate
main.rs Entry point + CLI dispatch
The codebase is intentionally straightforward: parse markdown, build an in-memory graph, run local checks, and emit either human-readable output or JSON. The direct dependency set is small, the test suite covers the core graph/check/CLI paths, and the tool is fast enough for interactive use on a few-hundred-file corpus.
MIT