Rulebearing Playground Coverage Schemas GitHub

Rulebearing for coding agents

A rule file is the best interface a coding agent has to an architecture: it is text, it is in the repository, it names the fence and the reason, and CI runs it. What this page covers is the part that makes an agent able to act on it inside its own loop, not only fail CI afterwards (design § Rules an agent can implement and follow, ADR-0021). Everything here is a subcommand of the one binary, the MCP and LSP servers included (serve.md).

The hooks

rulebearing hooks install --claude-code

merges three hooks into .claude/settings.json, keeping every other setting and hook, and adding nothing on a second run:

Event Runs So that
SessionStart rulebearing summary --format agent The session starts with the open violations by rule, each with its fix, the ratchets' headroom and any vacuous rule
PreToolUse on Edit and Write rulebearing impact --from-hook Before a file changes, the agent sees the rules that mention it, its dependents, whether it is on a cycle and the ratchets its edges count toward
Stop rulebearing cruise --output-type agent --from-hook Before the turn ends, the findings, cheapest fix first

Both --from-hook commands answer in Claude Code's hook protocol and exit 0, so a hook never fails a turn by accident:

The hook without the wait: guard --watch

On a large repository a cruise at the end of every turn is seconds the agent waits for. guard --watch does the work as files are saved, so the hook only reads a file (Wave 3, Step 16; design § The agentic engineering hat):

rulebearing guard --watch            # in a terminal beside the session; Ctrl-D stops it
Step What happens
Start One extraction, keeping each file's state; the answer cruise --from-hook would give is written to .graph/guard/findings.json with writtenAt and seenUpTo (milliseconds since the epoch), configHash (the configuration files' hash), and a key of the command line it answers for
A source is saved That file alone is extracted again (the extractor's incremental entry, .cs files in source mode) and the answer rewritten; rechecked names the file, latencyMs the time from its modification to the answer written, timings the split
A configuration, manifest, tsconfig, solution or project file changes, or a source is added or removed Everything is read again
Nothing changes The file is rewritten every second, so its age says the guard is alive and has seen every change until then
Standard input closes The guard removes its findings and exits 0

The Stop hook (cruise --from-hook) serves the guard's answer when the findings are younger than 5 s, name the configuration's current hash and the same command line (every cruise flag except those that only shape how the answer is printed: the reporter, the destination, progress, colour, the cache and the mode), and were written by the same build, and once the guard confirms it has seen every change until the hook asked: the hook writes .graph/guard/request with the time, and serves the answer when the findings' seenUpTo (the start of the scan the answer reflects) passes it. A file saved a moment before the turn ends is therefore in the answer. The guard confirms on its next check, a few milliseconds when nothing changed, or after checking the change again when something did. When no guard confirms within 5 s, or the findings do not answer for this run, the hook cruises as before; the findings guard without --watch writes are never confirmed, so they are for a script to read, not for the hook. The guard takes the flags the answer depends on (--config, the paths, --affected, --affected-depth, --liveness, --max-findings, --sidecar, --mode), so guard --watch --affected HEAD pairs with a hook of cruise --output-type agent --from-hook --affected HEAD. It reads .NET in source mode unless --mode compiled is given, so its answer for a .NET file is approximate and says so. It polls (--interval, 25 ms by default) rather than subscribing to file-system events, writes nothing outside .graph/guard/, and logs one line per check to stderr. After a check that changed the graph it also writes the merged document to .graph/guard/graph.json, on a thread of its own after the findings, which serve --mcp and serve --lsp take when it is newer than the saved graph (serve.md); it removes that file with its findings when it stops. guard without --watch writes one answer and exits.

A check of one saved file takes under 100 ms on the fixture the integration test uses (crates/rb-cli/tests/guard.rs). On the 5,500-module synthetic tree of perf.md it took about 470 ms; it now takes a p50 of 74 to 81 ms and a p95 of 88 to 98 ms on an idle laptop (about 18 ms extracting, about 55 ms evaluating and reporting), under the 100 ms NFR-PERF-03 asks for. On a 4-vCPU Linux runner the same check took a p95 of 255 ms, so the 100 ms is set for a developer machine (ADR-0060). A save that leaves the graph as it was (no import changed) gets the earlier answer again without evaluating, in about 45 ms; one that changes the graph evaluates every rule again. testbeds/synth/guard.sh measures both kinds, and the nightly bench workflow fails when either p95 reaches 100 ms (perf.md) (Wave 3, Step 16).

Questions before the import is written

Command Answers
rulebearing can-import <from> <to> Would this import be allowed? yes (exit 0), or no with the rule, its comment and its fix (exit 1). It answers from the worktree-aware cache, so it is fast. The target's kind (npm-dev, core, its licence) comes from the graph; a target the graph has never seen and that is not a file on disk exits 2 rather than guess. --json prints the same answer as one object (verdict, from, to, violations with each rule's name, severity, id, comment and fix, and warnings), with the violation id the gate would give the edge; eslint-plugin-rulebearing reads it
rulebearing impact <file> [--depth N] [--json] What the file is subject to: the rules that mention it, its dependents to depth N, whether it sits on a cycle and the ratchets its edges count toward; text, or JSON with --json
rulebearing explain <rule> [--plain] The rule as one English sentence, why it exists, what to do, and the first edges it matched
rulebearing rules --json Every rule, its family, severity, fix, its lifecycle fields (since, deprecated, replacedBy), and how many modules each side matches
rulebearing count --from <regex> --to <regex> How many direct edges match, against a budget with --budget
rulebearing fix --plan <rule> For each violation of the rule, how to clear it (below)

The query commands (can-import, impact, propose, place, fix --plan) read --graph FILE when given; otherwise the worktree-aware cache under .graph/cache/<key>/, keyed by the worktree root, HEAD and the configuration's hash (plan 0002, Step 13), so two worktrees of one repository never read each other's graph. A miss extracts the paths given and writes the cache; --no-cache neither reads nor writes it. The key follows commits, not uncommitted edits: pass --no-cache to see an edit before committing it.

Plans for a violation

rulebearing fix --plan <rule> prints, for each violation of the rule, the cheapest refactor of each of the three kinds the design names that clears it, as JSON (plan 0004 § 1.6, ADR-0077). The tool edits nothing; the agent executes the plan it picks.

{
  "rule": "no-cross-app-imports",
  "violations": [
    { "id": "RB-c7eade13", "from": "apps/web/src/x.mjs", "to": "apps/worker/src/y.mjs",
      "plans": [
        { "kind": "move", "target": "packages/", "edgesTouched": 2, "legalBy": "can-import" },
        { "kind": "port", "package": "packages", "edgesTouched": 2, "legalBy": "can-import" }
      ] }
  ]
}
Kind The change edgesTouched
move from moves to the folder target, keeping its imports and its importers its imports plus the imports of it
port a new module in the folder package; from imports it in place of to, and to imports it 2
invert from stops importing to, and to imports from; dependentsToChange lists the other modules that import to the two imports, plus one per dependent

edgesTouched counts the import statements to edit, and the plans are sorted by it. Every plan printed is legal on a copy of the graph with the change made: can-import allows each edge it adds (legalBy), and the whole rule set no longer reports the violation and reports nothing new at error severity. A violation planned on a module alone (an orphan, a required rule, a dependents count), or for which no candidate is legal, has an empty plans and a reason. A reachability finding is planned on the first import of its path. fix --plan reads the graph as the other query commands do (above), exits 0 with the plans, 2 when the graph cannot be read, and 3 for a rule the configuration does not have. cruise --output-type agent --with-plans adds the same plans (and reason) to each violation the report shows.

The agent reporter

cruise --output-type agent (and fmt -T agent) writes JSON grouped by rule, with each rule's fix and decision token, each violation's id, line and column, and a cost: how many edges must move and how many modules depend on the target. Cheapest fixes come first, and --max-findings N keeps the answer inside a context window while count keeps the total (reporters.md).

Rules an agent writes, held to the same bar

An agent that adds a rule can make it wrong in ways that read green. Five checks catch that before merge (design § Rules an agent writes):

Guard Catches
Liveness, on by default A rule whose from matches nothing: exit 2 under a rulebearing.yaml, a warning under a dependency-cruiser file, listed in summary.vacuousRules either way (ADR-0007, ADR-0032)
rulebearing test A rule that does not flag its own forbidden examples, or flags its allowed ones
rulebearing config lint Rules that can never match, shadowed rules, an allowed list that admits everything, missing or empty fix text
--require-comment-token A rule with no adr:NNNN or plan:<slug> in its comment
Ratchets A budget that would rise: count --write refuses, and an exceeded ceiling fails cruise

Proving what ran

rulebearing attest --config rulebearing.yaml src          # writes .graph/attest.json
rulebearing attest --verify --config rulebearing.yaml src # exit 1, naming the hash that differs

The receipt holds SHA-256 hashes of the configuration files, of every extracted source file (or the --graph document) and of the violations, with the commit and the time. --verify recomputes them, so a claim that "the gate passed at this commit" can be checked rather than trusted. This repository's CI runs both after its self-check.

Starting in a repository

Situation Command
No rules yet rulebearing init proposes a rulebearing.yaml from what the repository holds (apps and packages, feature folders, framework entry points), baselines what fails today, and writes it only when a cruise with it exits 0
Already on dependency-cruiser rulebearing adopt keeps the configuration as it is, writes a rulebearing.yaml that extends it with a baseline (each entry with an owner and an expiry), adds the CI step (installing dependencies first when the folder has a lockfile) and the pre-commit hook at the repository root, running in the configuration's folder when it is below the root, and docs/architecture/rulebearing.md beside the configuration, and opens one pull request that is green. A repository whose hooks lefthook, pre-commit or simple-git-hooks manage gets no hook; the pull request says what to add