Skip to content

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 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.

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 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:

Terminal window
arcade-guard init . --template layered
arcade-guard propose . --intent "an endpoint that lists orders"
arcade-guard preview . --from api --to store # WOULD VIOLATE
arcade-guard check . --fail-on error # exit 1 on FAIL
arcade-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.

Because check is a plain exit-code gate, wiring it in needs no special integration. A pre-commit local hook, for example:

.pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: arcade-guard
name: architecture guardrail
entry: arcade-guard check . --fail-on error
language: system
pass_filenames: false

and the equivalent CI step, assuming arcade-agent ≥ 0.4.0 is installed in the job:

- run: arcade-guard check . --fail-on error

This 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.

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 match globs. 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_impact applies it to a hypothetical edge and check_architecture to every actual edge, so the answer an agent gets before writing an import cannot disagree with the verdict it gets after. propose_placement lists as allowed exactly the dependencies preview_impact would allow.
  • 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 match glob covers are reported as unmapped_entities and are not checked; a high count usually means the globs don’t fit the layout yet.

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.