ADR-086: Structural rule graduation — from prose to check to default
Status: Accepted Date: 2026-04-25 Related: ADR-083 (rule promotion engine — same lineage), ADR-084 (outbound chokepoint is a graduated rule), ADR-085 (pipeline blocks as types is a graduating rule)
Decision (one paragraph)
Any recurring operational rule in the Gyrum platform passes through three layers on its way to being load-bearing — prose in a prompt or doc, a structural check that fails at gate time, and a structural default where the tool itself makes the right thing the only thing. The graduation is explicit and visible, not accidental.
When a prose rule drifts twice in the same month it is formally graduated to the next layer. ADRs that introduce a rule name the layer it starts in and the layer it should eventually reach.
Context
The Gyrum platform discovered on 2026-04-24 that prose rules DO drift. Examples observed in a single 14-hour session:
"Agents must work in /tmp isolated workspaces" → drifted until
gyrum-start-workrefused cwd /home/jon/work (ADR isolation, task #235) → now auto-creates the /tmp workspace (task #236)."Commit message bodies must be one paragraph per line" → user feedback on rendered PR bodies → hard-wrap detection hook (devtools v0.1.69) → dual-rule hook with over-stuffing detection (devtools v0.1.70).
"Playbook steps can reference earlier step outputs" → undeclared references silently resolved to empty strings → ADR-085 Phase 1 validator flags undeclared references (ai-research v0.32.93).
"Admin-merge only when size is sole blocker" → previous sessions showed silent admin-merges →
complete-pr.shrequires--admin+ audit log entry (devtools v0.1.66).
Every rule that ONLY existed in prose drifted at least once. Every rule that graduated to a check held. Every rule that reached a structural default became invisible — nobody needs to know about it; the system enforces it.
The three layers
| Layer | Example | Failure mode | Graduation trigger |
|---|---|---|---|
| Prose | "Don't hard-wrap commit messages" in CLAUDE.md | Drifts every session | Two drifts in a month |
| Structural check | commit-msg hook refuses hard-wrapped body | Requires every tool in the chain to enforce | Check bypassed via escape hatch twice |
| Structural default | Tool produces only the compliant shape | Cannot drift | N/A — end state |
The graduation criterion is observational, not theoretical. If prose loses twice, promote to check. If a check is bypassed twice via its escape hatch, promote to default — meaning the tool no longer offers the non-compliant shape.
Worked examples from 2026-04-24
Agent isolation rule. Prose: "Agents must work in /tmp". Drifted when agents branched from ~/work/ and contaminated the user's dev workspace (finding #019). Layer 2: gyrum-start-work refuses cwd under /home/jon/work/ with GYRUM_AGENT_ID set. Layer 3 came two days later: gyrum-start-work auto-creates the /tmp workspace transparently, so the cwd question never comes up.
Hard-wrap commit bodies. Prose: user pointed out the rendered PR body had visible mid-sentence line breaks. Layer 2 arrived the same evening: commit-msg hook refuses bodies with multi-line paragraphs. Escape hatch GYRUM_ALLOW_HARDWRAP=1. Two hours later the over-correction appeared (one giant run-on paragraph with no hard wraps) — Layer 2.5: sibling rule for over-stuffing, same hook.
Step output references. Prose: "downstream steps reference earlier outputs". Drifted when a playbook referenced a step output that didn't exist — runtime silently resolved to empty string. Layer 2: ADR-085 Phase 1 validator flags undeclared refs. Layer 3 (future Phase 4): type-enforcement at validate time, empty-string fallback removed.
The shared shape across all three: each rule was originally written down because someone wanted it. Each one drifted in production. Each one was promoted in the same session as the drift was observed — not deferred to a future planning meeting. The promotion latency from "drift caught" to "Layer 2 lands" was measured in hours, not weeks.
Graduation signals
Watch for these when a rule is a candidate for graduation:
The rule appears in three or more prompts / CLAUDE.md files / agent briefs (copy-paste drift).
The rule has an "I keep forgetting" comment in a retro or session log.
The rule's violations are caught by a human reader after the fact, not at creation time.
The rule exists because an incident already happened (finding in
~/.gyrum/findings/findings.jsonl).
Each is a vote for promotion. Three votes = promote.
The signals are deliberately leading indicators, not lagging ones. Waiting for an outage before mechanising a rule means the outage already happened. The signals above fire before the failure mode hits production — the goal is to catch graduation candidates while they are still cheap.
Tools the Gyrum platform uses for each layer
Layer 1 (prose):
- CLAUDE.md files in each repo.
- Prompts inside
gyrum-*scripts. - doc-standards, visual-language, check-stack shared docs.
Layer 2 (structural check):
- Pre-commit / pre-push / commit-msg hooks in
.githooks/. gyrum-review-prstructural block (build, test, lint, etc.).- Playbook validator findings in ai-research
internal/playbook. - CI checks in
.github/workflows/.
Layer 3 (structural default):
gyrum-start-workauto-workdir behaviour.gyrum-complete-pr's enforced--mergestrategy (never--squash).- Strict TypeScript types + normalize-at-boundary (ESLint rule
apifetch-requires-normalize). - Typed executors in playbook runtime (Phase 2 of ADR-085).
How this ADR is used in subsequent ADRs
Future ADRs that introduce a rule MUST name the layer the rule starts at and the layer it should eventually reach. The ADR template will grow a Layer field in its frontmatter once a second graduation-tracking ADR lands.
A rule that begins life as Layer 2 (structural check) is fine — not every rule needs a Layer 1 prose existence first. A rule may also legitimately stop at Layer 1 forever, when the cost of mechanisation exceeds the cost of drift. The ADR documents the intended ceiling, not just the floor.
ADRs that retire a rule (because Layer 3 made it invisible, or because the rule itself was wrong) link back to this ADR. The retirement is itself a structural moment: the prose / check / default is removed, and any callsite that depended on the rule existing is updated.
When NOT to graduate
Not every prose rule should be promoted. Drop the rule if:
It's project-specific and a sibling project doesn't share it.
Its violations have no observable failure (a prose rule that exists "for consistency" with no downstream cost).
The cost of the check exceeds the cost of the occasional drift (e.g. a rule that applies to 1% of PRs with low-severity failures).
Consequences
Positive: structural rules don't drift; operator and agent attention is freed from memorising; the system gets more "invisible" over time as more defaults arrive.
Negative: each graduation is work (hook + tests + doc + rollout); structural layers accrete complexity over time if not retired; escape hatches add audit noise.
Risks: promoting too aggressively causes hook fatigue (operators start --no-verify-ing); promoting too slowly means drift wins. The graduation criterion ("two drifts = promote") is a first-pass heuristic that should be calibrated from retro data.
Operational implication: each escape hatch on a Layer 2 check (GYRUM_ALLOW_HARDWRAP=1, --admin "<reason>", etc.) is itself a counter that feeds the Layer 2 → Layer 3 promotion signal. If the audit log shows the hatch firing twice in a month, that is the trigger to design the Layer 3 default that removes the non-compliant shape entirely.
Concretely, this means the audit logs at ~/.gyrum/admin-merges.log and the equivalent escape-hatch journals are first-class graduation inputs. They are not just a paper trail for compliance — they are the input to the next graduation decision. A retro that reviews them quarterly is the natural cadence for promoting Layer 2 checks to Layer 3 defaults.
Related
- ADR-083 — defense-in-depth gates, the same prose → check → default philosophy applied to compliance rules specifically.
- ADR-084 — a concrete Layer 3 default (toolkit makes typed calls the only path).
- ADR-085 — currently Layer 2, graduating to Layer 3 in Phase 4.
This ADR generalises the pattern those three already follow individually. Future ADRs that introduce, promote, or retire a rule should cite this one as the framework they are operating inside.