Decisions

ADR-086: Structural rule graduation — from prose to check to default

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…

#086

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-work refused 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.sh requires --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-pr structural block (build, test, lint, etc.).
  • Playbook validator findings in ai-research internal/playbook.
  • CI checks in .github/workflows/.

Layer 3 (structural default):

  • gyrum-start-work auto-workdir behaviour.
  • gyrum-complete-pr's enforced --merge strategy (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.