DEV Community

John
John

Posted on • Originally published at hexisteme.github.io

Stop AI Agent Drift Across Sessions With Versioned, Grep-able Rules

Originally published on hexisteme notes.

If you use AI agents daily, you have hit this: the agent gives a correct, well-reasoned answer on Monday. On Friday, in a fresh session, it gives a different answer to the same question. Not wrong — just different, shaped by whatever context happened to come up first.

That is drift. And it is not fixable with better prompts or more examples piled into a CLAUDE.md file. Those approaches scale poorly and they do not version. What actually works is treating each repeatable decision as a small, versioned artifact I call a Reusable Decision Unit (RDU).

The one-sentence version: encode each repeatable decision as a versioned markdown file with a grep-able trigger and a committed judgment, store it where the agent loads it at session start, and the rule fires automatically on trigger instead of relying on the model's memory. I run a library of 140+ of these across iOS deployment, trading, AI-agent design, game development, and real estate. This is how they are built and why they hold.

The problem with "just write it down"

The naive fix for drift is to document decisions: write a CLAUDE.md or AGENTS.md entry that says "when X happens, do Y." It fails for two reasons.

  1. Documentation requires reading. A rule buried in a long markdown file competes for attention with everything else in that file. It gets read once, and then it drifts as the file grows around it.
  2. There is no version control on the judgment itself. When you discover a better approach, you overwrite the old rule silently. There is no history of why the judgment changed, so the next time you question it you re-derive from scratch.

The fix is to treat decisions the way you treat code: versioned, trigger-driven, with explicit evolution conditions.

What an RDU looks like

Each RDU is a markdown file with YAML frontmatter. Here is a real one from the iOS-deployment domain:

---
id: RDU-047
title: "App Store Connect in-flight limits  1 version / 2+5 reviewSubmissions"
status: active
domain: [ios, deploy, app-store-connect]
version: 3
created: 2026-04-28
last_applied: 2026-05-10
sources: ["ASC API docs", "direct curl validation 2026-05-10"]
---

## Trigger
- `app_versions_create` returns 409 STATE_NOT_SUITABLE
- User wants to ship CPP + PPO + new version simultaneously

## Situation
ASC enforces two independent concurrency limits:
- **Version**: only 1 in-flight (PREPARE/WAITING/IN_REVIEW) per platform
- **reviewSubmissions**: max 2 in-flight, max 5 total

READY_FOR_REVIEW orphans can't be deleted via API (403) or canceled (409).
They auto-expire after 7 days or require manual ASC web UI action.

## Judgment
**Check both limits before any submission attempt.**
Bundle CPP + PPO + appEvent into one reviewSubmission to minimize slot consumption.
Never assume a clean slate — always query current state first.

## Action
```bash
# Check in-flight versions
GET /v1/appStoreVersions?filter[appStoreState]=PREPARE_FOR_SUBMISSION,WAITING_FOR_REVIEW,IN_REVIEW

# Check reviewSubmission slots
GET /v1/reviewSubmissions?filter[state]=READY_FOR_REVIEW,WAITING_FOR_REVIEW,IN_REVIEW
```
Enter fullscreen mode Exit fullscreen mode

The structural elements each exist to prevent a specific failure:

Section Purpose Anti-pattern it prevents
Trigger Grep-able condition that fires the rule Rules that require the agent to "remember" to check
Situation Why this judgment was needed Rules that become obsolete without anyone noticing
Judgment Committed decision, no hedging "It depends" answers that push the decision back to the user
version: N Explicit evolution tracking Silent rewrites that erase why a decision was made
sources Provenance of the judgment Rules derived from hallucinated "facts"

The four core principles

1. No absolutes

Every RDU is the best judgment at the time of writing. The meta-rule — RDU-000 — states it plainly: there are absolutely no absolutes. Every rule carries implicit evolution conditions, so if a better approach shows up you propose a version bump or a supersession; you never silent-rewrite. A rule that cannot be revised honestly is a rule that will quietly become wrong.

2. Trigger specificity

A trigger must be a grep-able token, not a vague description. "When working on iOS" is not a trigger. "When app_versions_create returns 409" is. If the trigger cannot be matched by a regex, the rule will not fire reliably — and a rule that fires unreliably is worse than no rule, because you stop trusting the whole system.

3. Proactive registration

When the agent notices it is re-deriving a judgment it has made before — or when the user signals "this is useful, save it" — it should propose an RDU draft immediately, not wait. The pattern is always propose → user approves → write file. No silent creation, because a rule you did not agree to is a rule you will not trust.

4. Domain-agnostic

The trigger–situation–judgment structure works across every domain. The 140+ RDU library spans:

  • iOS deployment — ASC API limits, fastlane patterns, Swift 6 concurrency
  • Trading — no LLM probability production (RDU-021), walkforward validation required (RDU-022)
  • AI-agent design — council orchestration patterns, stop-hook architectures
  • Game development — Godot platformer feel numbers (coyote time 100 ms, jump buffer 150 ms)
  • Real estate — LTV computation, loan-headroom calculation

The same five-section skeleton carries a Swift concurrency rule and a jump-buffer timing without any change in form.

How it works in practice

At session start, the agent loads a single index file — one line per rule. When a trigger condition appears in the conversation, the agent reads the full RDU file. The index stays short; the full rules load only when they are relevant. This is the whole efficiency argument against dumping everything into CLAUDE.md: a global, always-loaded file pays for every rule on every turn, while an index-plus-trigger design pays for a rule only when it fires.

Evolution is explicit. When a newer approach is discovered, the agent proposes a diff rather than editing in place:

# RDU evolution example
- version: 2 → version: 3
- Judgment updated: fastlane now works in READY_FOR_SALE state (ASC MCP doesn't)
- History: v2 assumed ASC MCP parity — incorrect for Editable Properties
- Source added: direct fastlane validation 2026-05-10
Enter fullscreen mode Exit fullscreen mode

The value is not in having 140 rules. It is in the trigger mechanism. A rule that fires automatically is worth 10× a rule that requires the agent to remember to check.

What RDUs don't solve

RDUs encode repeatable judgments. They do not handle novel situations, value questions (user preferences, risk appetite), or decisions that genuinely depend on runtime context. For those, the agent should ask. But every judgment you move into an RDU shrinks the surface area of what actually needs asking — which means the questions the agent does raise are the ones that genuinely need a human.

Getting started

The minimal viable setup is three things: a single directory for the rule files, an index file, and a template. You do not need 140 rules to start; you need one and the discipline to add the next. The signal to write one is concrete — when you catch yourself re-explaining the same judgment twice, or when you fix a bug caused by the agent forgetting a previous decision, that is an RDU waiting to be written.

Here is the template:

---
id: RDU-XXX
title: [one-line description]
status: active
domain: [list of domains]
version: 1
created: YYYY-MM-DD
last_applied: YYYY-MM-DD
sources: ["where did this judgment come from"]
---

## Trigger
[Specific, grep-able condition]

## Situation
[Why was this judgment needed? What was the failure mode without it?]

## Judgment
**[Committed decision — no hedging, no "it depends" without explicit conditions]**

## Action
[Concrete steps when trigger fires]
Enter fullscreen mode Exit fullscreen mode

None of this is exotic. It is the same instinct that makes you write a test instead of manually re-checking a behavior every time: encode the judgment once, in a form that fires on its own, and stop paying the cost of remembering. The drift does not come back, because the rule is no longer in anyone's memory — it is on disk, versioned, waiting for its trigger.

More notes at hexisteme.github.io/notes.

Top comments (2)

Collapse
 
max_quimby profile image
Max Quimby

The "documentation requires reading" line is the crux, and it maps onto something we've felt directly: a single growing memory file has a crowding problem. Every rule you add competes for the model's attention with every other rule, so the one that mattered on Monday gets out-shouted by whatever loaded first on Friday. Splitting each decision into its own trigger-fired unit sidesteps that — the rule doesn't have to win an attention contest, it just fires on match.

The versioned-judgment part is underrated. Overwriting a rule silently loses the why, so next time you question it you re-derive from zero and often land somewhere different — that's drift with extra steps. Keeping the evolution history committed is what actually stops the loop.

The failure mode I'd be curious about at 140+ RDUs: trigger collision. When two units match overlapping triggers in one session — say an iOS-deploy rule and a more general agent-design rule both fire — how do you resolve precedence? Specificity ordering, explicit priority in the frontmatter, or does the model just get both and reconcile? That seems like the thing that gets sharp as the library grows.

Collapse
 
hexisteme profile image
John

Trigger collision is the right thing to worry about, and I'll give you the honest answer rather than the tidy one: the system doesn't resolve precedence, it avoids the collision — and where it can't avoid it, nothing arbitrates. I'm at 161 now, and that gap hasn't bitten yet, but your question is aimed exactly where it will if it does.

The avoidance is a registration gate, not a runtime rule. A unit can't register unless its trigger is a grep-able concrete token — a compile-error string, an API name, a specific file pattern. "iOS development" gets rejected at authoring time; .symbolEffect(.bounce, options:) on a project targeting iOS 17 gets in. That narrows collision but it does not eliminate it, and I should be careful not to claim more than it does: a full-stack session pulls iOS, backend, and harness context into one window, so tokens from different domains absolutely can land together. What the gate actually buys is that when two triggers do co-occur, they're specific enough that both firing is usually informative rather than contradictory — they matched different concrete signals. That's a weaker claim than "they don't collide," and it's the true one.

There's a frontmatter specificity field, and I want to be precise about it because it looks like it should be the answer to your question and isn't. It gates registration (low-specificity units are refused), it does not order precedence. There is no priority field. I didn't build one, deliberately — a numeric priority is a second thing to keep correct, and it drifts against the triggers it's supposed to rank.

Where it actually hurts is inside a single dense domain. iOS is 49 of the 161. Two iOS rules with genuinely overlapping triggers is the case the gate doesn't catch, and there the honest answer is that nothing arbitrates: the model gets both and reconciles. That has held so far, but I want to be straight that "held so far at 161" is an observation, not a safety property — if two rules in the same domain ever genuinely contradict rather than merely co-fire, the system has no answer, and I wouldn't find out until the wrong one won. That's the real hole your question points at, and I don't have it closed.

If I had to formalize it, I'm fairly sure priority numbers are the wrong move — they'd rot against the triggers they rank. The direction I'd try first is specificity-of-match at fire time: prefer the unit whose trigger matched the more specific signal — an exact error string over a broad keyword — computed from the match rather than declared up front. I'll be honest that I haven't built it and it may not survive contact: "more specific match" is easy to say and harder to make into a total order two rules can't both claim. So it's a direction, not a solution, and I'd rather let a real collision dictate its shape than build precedence machinery against a failure I haven't actually hit. You've named the thing that gets sharp on the way to 300, though, and it's the part of this I'm least sure of.

The crowding point you opened with is the whole reason for the trigger-fired design, so it's good to hear it land the same way from the outside — a rule that only has to match, not win an attention contest, is the one thing that scales when the file would otherwise just get louder.