Writing ADRs for AI Agents
Guidelines for writing Architectural Decision Records (ADRs) that are consumed by AI coding agents (Claude Code, Hydra pipeline, etc.).
Core Principle
Every line in an ADR competes for attention with the actual work.
ADRs are injected into every agent session as context. Bloated ADRs cause agents to ignore rules — the same way humans skip long documents. An ADR with 50 crisp rules outperforms one with 200 verbose rules.
What Goes In an ADR
ONLY rules that require JUDGMENT to apply.
A rule belongs in an ADR if:
- A tool cannot enforce it automatically
- The agent needs to understand WHEN and WHY, not just WHAT
- Applying the rule requires understanding the codebase context
Examples of JUDGMENT rules:
- "Controllers MUST be thin — routing + validation + response only" (judgment: what is "business logic"?)
- "Use schema.org vocabulary where equivalent exists" (judgment: does an equivalent exist?)
- "Multi-tenant isolation at API/service level" (judgment: how to scope data access)
What Does NOT Go In an ADR
Rules that can be enforced by tools.
| Rule | Enforced By | NOT an ADR |
|---|---|---|
| "Lines must not exceed 150 chars" | PHPCS | Script handles it |
| "No var_dump() calls" | PHPCS | Script handles it |
| "Scoped styles required" | ESLint/Stylelint | Script handles it |
| "Named arguments in PHP" | PHPCS custom sniff | Script handles it |
| "SPDX license headers" | reuse lint | Script handles it |
| "No hardcoded colors" | Stylelint | Script handles it |
| "publiccode.yml required" | CI check | Script handles it |
Also not ADRs: Workflow/process rules about HOW to use the pipeline (these belong in skill definitions or pipeline docs, not in architecture context).
Format
Use this compact format — no headers, no essays, no "context" or "alternatives" sections:
## Topic Name (ADR-NNN references)
- RULE in imperative form. Brief explanation if needed.
- Another RULE. Example: `code example` if it clarifies.
- Exception: when X, then Y instead.
Do
## Backend Layering
- Controller → Service → Mapper (strict 3-layer). Controllers NEVER call mappers directly.
- Controllers: thin (<10 lines/method). Routing + validation + response only.
- Entity setters: POSITIONAL args only. `$e->setName('val')` — NEVER named args.
Don't
## ADR-099: Backend Layering — Controller, Service, and Mapper Separation
### Context
In our Nextcloud applications, we've observed that some developers put business
logic directly in controllers, which leads to fat controllers that are hard to test...
### Decision
We have decided to enforce a strict three-layer architecture where controllers
handle only HTTP concerns, services contain all business logic, and mappers
handle database operations...
### Consequences
This means that all new code must follow this pattern. Existing code should be
refactored when touched...
The "Don't" version is 3x longer and says the same thing. The agent doesn't need to know WHY the rule exists — it needs to know WHAT to do.
Size Budget
| Scope | Target | Max |
|---|---|---|
| Single ADR topic | 5-15 lines | 20 lines |
| All ADRs combined | 80-120 lines | 200 lines |
| Estimated tokens | 1,000-2,000 | 3,000 |
At 200 lines / 3,000 tokens, the ADRs consume ~1.5% of a 200K context window. Above that, you're paying diminishing returns.
Token Efficiency Tips
- Use bullet points, not paragraphs. Bullets are ~40% shorter than prose.
- Use code examples instead of descriptions.
$e->setName('val')is clearer than "use positional arguments when calling entity setter methods." - Combine related rules. "Controller → Service → Mapper (strict 3-layer)" replaces three separate rules.
- State the rule, not the reason. "NEVER
\OC::$server" vs "Because the Nextcloud DI container should be used instead of the legacy static service locator pattern, you must never use\OC::$server." - Use exceptions sparingly. If a rule has more exceptions than applications, reconsider whether it's a rule.
Deduplication
Each rule should appear ONCE. If a rule applies to multiple topics, put it in the most specific topic and reference it:
## Security
- Entity setters: positional args only (see Backend Layering).
NOT:
## Backend Layering
- Entity setters: POSITIONAL args only.
## Security
- Entity setters: POSITIONAL args only. This is important for security.
## Code Quality
- Entity setters: POSITIONAL args only. This prevents bugs.
Compounding Improvements
When an agent makes a mistake that an ADR should have prevented:
- Check if the rule already exists — maybe it's too wordy and got ignored
- If missing, add a 1-line rule to the relevant topic
- If existing but ignored, make it shorter and more prominent
Never add a new ADR file for a single rule. Add it to the existing topic.
Review Checklist
Before merging an ADR change:
- Every rule requires judgment (can't be a script)
- No rule is duplicated across topics
- No paragraphs — bullets only
- Total ADR file is under 200 lines
- Added rules have been tested by running the agent and verifying it follows them
ADRs in practice — where they live
Every Conduction app repo follows a clean split:
| Location | Scope | Who writes |
|---|---|---|
hydra/openspec/architecture/ | Org-wide ADRs — apply to every Conduction app | Humans (architecture-level decisions) |
<app>/openspec/architecture/ | Repo-specific ADRs — apply only to that app (data model choices, domain standards, storage decisions) | Authored by Specter during research; evolved by humans |
The authoritative org-wide list is the directory itself — hydra/openspec/architecture/ on main. GitHub renders it as a browsable index, so we don't mirror it here (the mirror would drift the moment a new ADR lands).
App repos do NOT carry copies of the org-wide ADRs. Earlier they had stale duplicates that drifted (e.g. a copy saying fetch() while hydra's master said axios) — those copies were removed across every app repo that had them.
How agents see org-wide ADRs:
- Reviewer + builder containers copy the relevant ADRs from the hydra repo at image-build time.
- Agents operating in an app repo outside a container (IDE humans, manual
/opsx-ffruns) read them from hydra'smainbranch directly. specter-prepare-contextsurfaces the applicable org-wide ADRs incontext-brief.mdfor each spec so the builder sees them pre-loaded.
Rule of thumb for where a new ADR belongs:
- Applies to ≥2 Conduction apps → org-wide, in
hydra/openspec/architecture/. - Applies only to one app's domain/storage/auth choice → app-specific, in
<app>/openspec/architecture/.