Architecture guardrail
The other agent tools describe an architecture recovered from the code. The guardrail does the opposite: you write down the architecture you intend, and it checks code against it — before new code is written, before a cross-component import is added, and after every change. It is aimed at agents that write code, not just agents that read it.
The spec: architecture.spec.json
Section titled “The spec: architecture.spec.json”The contract lives in the repository root as architecture.spec.json. It maps path globs to
components, assigns components to layers, and lists allowed and forbidden dependencies, each
forbidden one with a reason:
{ "components": [ {"name": "api", "match": "**/api/**", "layer": "presentation"}, {"name": "service", "match": "**/service/**", "layer": "application"}, {"name": "store", "match": "**/store/**", "layer": "infrastructure"} ], "layers": ["presentation", "application", "domain", "infrastructure"], "allow": [{"from": "presentation", "to": "application"}, {"from": "application", "to": "infrastructure"}], "forbid": [{"from": "presentation", "to": "infrastructure", "why": "the API goes through the service layer"}], "budgets": {"no_cycles": true, "max_fan_in": 8}}| Key | Meaning |
|---|---|
components |
name, a path glob in match (brace globs such as **/{api,web}/** work), an optional layer, and optional keywords used when ranking placements. |
layers |
Layer names in order, outermost first. A dependency that points against this order is a layer-violation unless an allow rule permits it. |
allow / forbid |
Rules between components or layers (names may be globs). Each forbid should carry a why — it is echoed back to the agent with the violation. |
budgets |
Optional limits: no_cycles and min_turbomq (errors), max_fan_in and max_component_entities (warnings), and max_new_smells, which is evaluated only when a baseline smell count is supplied and is otherwise reported as not evaluated. |
You don’t have to start from a blank file: init_spec (or arcade-guard init) scaffolds one
from a layered, hexagonal, clean or mvc template. Adjust the match globs to your
layout before relying on the verdicts.
The five tools
Section titled “The five tools”All five are registered on the MCP server alongside the other tools. Each looks the spec up at
<source_path>/architecture.spec.json unless spec_path is given.
| Tool | What it answers | When the agent calls it |
|---|---|---|
init_spec |
Write a starter spec from a template (layered, hexagonal, clean, mvc). |
Once, when a project has no spec yet. Refuses to overwrite an existing spec unless overwrite=true. |
propose_placement |
Given a plain-language intent, which component (and layer) does the new code belong in, what path convention does it follow, and what may and must it not depend on? |
Before writing new code. |
preview_impact |
Would a dependency from from_component to to_component be allowed? If not, which rule it breaks and a fix. |
Before adding a cross-component import. |
check_architecture |
PASS / WARN / FAIL for the whole codebase, every violation with the rule it breaks and a concrete fix, component edges and sizes, and how many entities no glob matched. | After each change, until it passes. Accepts a dep_graph session ID from parse to skip re-parsing, and max_tokens. |
remediate |
The same violations as an ordered fix plan, errors first. | When a check fails and the agent needs a plan rather than a list. |
The verdict is FAIL if any error-severity violation exists, WARN if there are only
warnings, and PASS otherwise. Errors are forbidden dependencies, layer violations,
dependency cycles between components, and the min_turbomq / max_new_smells budgets;
fan-in and component-size budgets are warnings.
The arcade-guard CLI
Section titled “The arcade-guard CLI”The same checks run from the command line, so a person, a pre-commit hook or a CI job gets exactly the verdict an agent gets:
arcade-guard init . --template layeredarcade-guard propose . --intent "an endpoint that lists orders"arcade-guard preview . --from api --to store # WOULD VIOLATEarcade-guard check . --fail-on error # exit 1 on FAILarcade-guard remediate .Every sub-command takes --spec <path> to point at a spec elsewhere and --json to print the
raw tool result instead of the human-readable summary. check exits 1 on FAIL, unless
--fail-on never makes it informational; a missing spec or an unknown component exits 2.
Warnings never fail the command.
Pre-commit and CI
Section titled “Pre-commit and CI”Because check is a plain exit-code gate, wiring it in needs no special integration. A
pre-commit local hook, for example:
repos: - repo: local hooks: - id: arcade-guard name: architecture guardrail entry: arcade-guard check . --fail-on error language: system pass_filenames: falseand the equivalent CI step, assuming arcade-agent ≥ 0.4.0 is installed in the job:
- run: arcade-guard check . --fail-on errorThis is a different gate from the CI drift baseline. The drift baseline compares each change against the recovered architecture of the last merge and is informational by default; the guardrail compares against the architecture you wrote down and blocks a violating change.
Determinism
Section titled “Determinism”An agent may consult the guardrail dozens of times in one session, and an agent that gets a different answer on retry thrashes. So the verdicts are deterministic by construction:
- Path globs, not clustering. Entities are assigned to components by matching their file
paths against the spec’s
matchglobs. No recovery algorithm is involved, so the same code always receives the same verdict. - One rule function. A single function decides whether a component-to-component
dependency is allowed.
preview_impactapplies it to a hypothetical edge andcheck_architectureto every actual edge, so the answer an agent gets before writing an import cannot disagree with the verdict it gets after.propose_placementlists as allowed exactly the dependenciespreview_impactwould allow.
Limitations
Section titled “Limitations”- Only referenced dependencies count. Conformance is computed over the dependency edges the parser records. An import that is never used, or used only in a type annotation, does not create an edge — so it does not violate the spec, even if it names a forbidden component.
- Structure, not behaviour. The guardrail checks which components depend on which. It says nothing about whether the code is correct.
- The spec is only as good as its globs. Entities no
matchglob covers are reported asunmapped_entitiesand are not checked; a high count usually means the globs don’t fit the layout yet.
Does it change what an agent builds?
Section titled “Does it change what an agent builds?”In a controlled experiment on one greenfield task (a small layered Python service), Claude
Haiku 4.5 ran 20 times per condition. With the spec alone in the repository, 5 of 20 runs
violated it; with the guardrail tools, 0 of 20 (one-sided Fisher exact test, p = 0.024).
Stating the rules in the prompt also brought violations to 0 of 20, so the guardrail matters
most when the rules live in the repository rather than in every prompt. Most of the effect
came from the in-loop check, not the up-front advice: in 12 of the 20 guardrail runs the agent
wrote the forbidden import anyway, check_architecture reported it, and the agent repaired it
before finishing. Every run in every condition passed the functional test; guardrail runs took
longer. It is one task, one language and one agent harness — evidence that the mechanism
works, not a measure of how often it matters in practice.