A Field Guide

Phronesis &
the rules
that do not fade.

Notes on a small RETE rules engine, the MCP server that hosts it, and the hook surfaces we use to keep project guidance alive across a long working session.

On the name — phronesis, practical wisdom.

Phronesis (φρόνησις) is Aristotle's term for practical wisdom — the judgment an experienced practitioner brings to a concrete situation when the general principle does not quite reach. It is not theoretical knowledge, and it is not technique. It is knowing what to do here, now, in this particular case.

The work this engine does has that shape. A rule that says "don't use .unwrap() in src/" is not a theorem. It is a situated judgment: in this codebase, under these constraints, given what we learned the hard way, do this rather than that. What the engine persists across compression boundaries is exactly that kind of small, hard-won, project-specific maxim — a particular team's practical wisdom about a particular system, kept durable across sessions in which context windows fill, conversations get summarized, and the guidance you most need fades fastest.

§01
1

The premise — guidance fades, work continues.

Claude Code, Gemini CLI, and the rest of the LLM-assisted development family share one startup pattern: load whatever project guidance the user has set down — CLAUDE.md, AGENTS.md, a system primer — and place it in the model's context window. Then the session begins. The window fills with code, command output, and conversation. Older content is summarized by auto-compaction. Even content that remains literally in context draws less attention from the model as new material accumulates ahead of it. The directive you most need at hour three was last read carefully somewhere around token eight hundred.

This is not a bug. Context windows are finite, and compaction is the price of long-running sessions. But it means that any project convention encoded only in prose — any instruction that lives solely in the loaded markdown — has a practical half-life. The deflective phrasing you warned the model against in turn one returns in turn ninety. The workspace-flags rule you established on Monday is no longer reliably surfaced on Thursday afternoon. The shape of the project — the small, hard-won particulars one is meant to honor — fades, whether by explicit summarization or by the gentler erosion of attention.

Phronesis moves enforcement out of the conversation entirely. Rules live on disk in .phronesis/rules.json. Lightweight hooks re-read them at every relevant tool call. They fire from outside the context window — and so cannot be compressed away, because they were never loaded into context to begin with. They fire the same in token nine hundred thousand as they do in token eight hundred.

Fig. 1 · Persistence of guidance, by state x: tokens of session · y: signal level
MAX MID · 0 50k 120k 200k A · CLAUDE.MD IN CONTEXT compression begins B · RULES ON DISK each mark · one hook fire · one rule check One mechanism degrades; the other does not.
§02
2

What it is — a small engine, hosted as a hook.

Phronesis is a RETE rules engine. RETE — developed by Charles Forgy at Carnegie Mellon in the late 1970s and published in its canonical 1982 paper Rete: A Fast Algorithm for the Many Pattern / Many Object Pattern Match Problem — has lived inside production-rule systems ever since: expert systems, business-rules platforms, event-correlation engines, the JBoss Drools framework one of the authors used in earlier participatory-modeling work, and any number of others.

The algorithm is straightforward. Facts enter a network. Conditions match against them in a compiled discrimination graph. Consequences fire when every condition of a rule is satisfied. Think of a card-game referee: each card laid on the table is a fact; the rules of the game are the conditions; the referee speaks up only when something is out of bounds — a card from the wrong suit, a play out of turn. That is the engine's whole job.

Our use of the algorithm is not especially novel. The new thing is the purpose. We use RETE to lint LLM tool calls.

Phr-MCP is the wrapper around the engine: a single Rust binary that exposes it three ways. It runs as an MCP server over stdio, for any MCP-capable client. It runs as a set of CLI subcommands that bind directly to the Claude Code and Gemini CLI hook protocols. And it runs as a whole-tree audit tool. The hooks are where the durability property comes from. They are invoked by the host (Claude Code, Gemini CLI) at well-defined moments — before a tool fires, after it applies, when a session opens, when the user submits a prompt — and they re-read the rules file from disk on every invocation.

That last property is the structural point. The hook does not cache anything between invocations. It does not depend on the model's memory or on any state retained in the conversation. It opens a file, parses it, runs the network against the facts extracted from the current tool call, prints a result, and exits. Whatever the model has forgotten, the hook still has access to — because the hook never knew anything to begin with, and reads the rules fresh from disk every time it is asked to fire.

Fig. 2 · A tool call, end-to-end edit · bash · write  →  hook  →  rete  →  exit code
step 1 Tool call Edit · Bash Write · … step 2 · hook pre-check post-check (stdin: payload) .phronesis/ rules.json step 3 · engine RETE network extracted facts diff · ast · path · cmd step 4 · decision exit 0 allow exit 1 warn (post) exit 2 block (pre) the host (Claude Code · Gemini CLI) honors the exit code · the hook never modifies state itself

We have kept the picture deliberately small. There is no daemon, no socket, no shared state between invocations of any kind. Every hook fire is a fresh process that reads the rules from disk, evaluates them against the current tool call's facts, and exits. The persistence we care about comes from the file system, not from anything the engine itself is asked to remember.

A rule is a small refusal the project keeps making, even after it has forgotten the conversation in which it was first imagined.
§03
3

Anatomy of a rule — JSON, conditions, consequence.

Rules are a small DSL, and JSON is its surface syntax. We considered a dedicated grammar — Drools DRL did this years ago and the expressivity is genuinely attractive — but landed on JSON instead. The reasoning was practical: anyone using phronesis is already writing in Rust, Python, TypeScript, or Swift, and we did not want to add a parser to the learning curve. JSON brings syntax highlighting in every editor and a familiar shape; the rest is five fields per rule — an identifier, a phase, an integer priority, a when array, and a then action.

The DSL lives in the predicate vocabulary, not in the grammar. Each entry in the when array is a single-key object whose key names a predicate and whose value is its argument; predicates are evaluated against facts extracted from the tool call. When every entry matches, the then clause fires.

In practice, then is a single-key object mapping a verb to a message: "block" refuses the tool call at pre-phase, "warn" emits an advisory, "log" records silently. The message string is what the model sees — in the hook's stderr output and in the session-start summary — which is how a rule's authored intent gets communicated back to the model that triggered it.

Fig. 3 · A single rule, dissected enforce-no-unwrap-in-src
RULE · JSON "id": "enforce-no-unwrap-in-src", "phase": "pre", "priority": 10, "audit": true, "when": [ { "new_content_contains": ".unwrap()" }, { "file_path_matches": "src" } ], "then": { "block": "Avoid .unwrap()…" } stable identifier used in logs, audit reports, hook output phase pre · blocks · before edit applies post · warns · after edit applies audit · silent at hook · sweep only when predicate-as-key objects. ALL must match for the rule to fire. then verb → message. block · warn · log + the human-readable message. A rule is one JSON object · when/then · the rules file is an array of these.
predicate
A named fact-matcher provided by the host. Examples: new_content_contains (substring match against the diff), file_extension_is (gate), function_returns_result_string (AST), function_param_count_high (AST), cargo_command_lacks_workspace (Bash content), file_line_count_above (audit-only gate).
fact
A tuple produced from the tool call payload: the file path, the added text, the removed text, the AST-derived shapes, the Bash command string. Facts are inserted into the RETE network; conditions unify against them.
phase
When a rule runs. pre rules block (exit 2); post rules warn (exit 1); audit rules are silent at hook time and only fire during phr-mcp audit.
§04
4

Four surfaces — enforce, sweep, re-inject, detect drift.

The same rule format serves four distinct moments in the development cycle — each with a different cadence, a different audience, and a different mode of action. Together they cover the life of a piece of project guidance: moment-of-action enforcement at the hook, periodic debt review across the whole tree, session-wide prose steering through the durable-directive file, and heuristic drift detection that surfaces the gap between what a team has decided and what is actually enforced. No single surface is adequate on its own; the four in combination close the gap that motivated the project.

Hook-time

block · warn · allow

Pre-check rules fire before an Edit, Write, or Bash call applies its changes. A failing condition exits 2, which the host honors by refusing the call. Post-check rules fire after the edit has been applied; they exit 1 to warn but cannot reverse what has already been done to the tree.

  • phase: pre — blocks the call
  • phase: post — warns on the call
  • fires once per tool invocation

Audit

debt · sweep · trend

A whole-tree scan against every rule tagged audit: true, producing per-rule hit counts with file and line states. Each audit run also appends a snapshot to the action log, so that phr-mcp trend can report whether the pile is shrinking from one week to the next.

  • phase: audit — never at hook time
  • opt-in via audit: true
  • writes a trend snapshot per run

Durable directives

prose · re-inject

A small markdown file at .phronesis/durable.md. Its contents are re-injected into the model's context at every SessionStart and every UserPromptSubmit. CLAUDE.md fades; this does not. Reserved for the few directives that absolutely must survive context compression.

  • injected by session-context
  • injected by turn-context
  • prose, not enforced — re-read every turn

Drift detection

gap · triage · suggest

Three heuristic tools compare what the project says (in CLAUDE.md, in auto-memory, in ADR-style decision pages) against what the rule pack actually enforces. Uncovered items surface as triage candidates — gaps that should either become rules or be explicitly marked as non-lintable by design. All three use Jaccard token overlap, no LLM call.

  • claude-md-drift — CLAUDE.md bullets
  • memory-drift — auto-memory entries
  • wiki-drift — ADR decision pages
  • --suggest emits draft rule JSON
⁘ ⁘ ⁘
§05
5

How it slots in — one binary, one rules file.

phr-mcp init writes a small set of files into a project to wire phronesis into its host environment. Hook configuration lands in each host's settings file — .claude/settings.local.json for Claude Code, .gemini/settings.json for Gemini CLI (which also carries Gemini's MCP server registration). A separate .mcp.json registers the server for Claude Code and any other MCP-capable client. The starter rules pack lives at .phronesis/rules.json, and entries are appended to .gitignore so phronesis's log and backup files are not committed by accident. A wiki scaffold at .phronesis/wiki/decisions/ is created for ADR-style decision pages — these are carved back out of the gitignore so they travel with the repo.

Re-running is idempotent. Existing configuration is preserved; only the entries owned by phronesis are added or refreshed. You can run init against an established project without fear of clobbering other tooling already present.

.claude/settings.local.json

Claude Code hooks

PreToolUse and PostToolUse entries point at phr-mcp pre-check and post-check. SessionStart and UserPromptSubmit entries handle context injection.

.gemini/settings.json

Gemini CLI hooks

BeforeTool and AfterTool entries provide the equivalent pre/post enforcement. BeforeModelRequest is Gemini's name for the per-turn context injection point.

.mcp.json

MCP server registration

Exposes mcp__phronesis__* tools — assert facts, fire rules, audit the codebase, query the trend — to any MCP-capable client over stdio.

.phronesis/rules.json

The rules pack

A JSON array. Composable starter packs ship with the binary: llm for behavior, rust for code shape, plus python, typescript, swift. Edit in place; rules are re-read from disk on every hook.

# a typical bootstrap
$ cargo install --path .
$ phr-mcp install              # user-scope MCP registration
$ cd ~/Git/my-project
$ phr-mcp init --packs llm,rust # project-scope wiring + starter rules
$ phr-mcp audit                # sweep existing debt
$ phr-mcp trend                # debt over time
$ phr-mcp claude-md-drift      # which CLAUDE.md bullets lack rules
$ phr-mcp memory-drift         # which auto-memory entries lack rules
$ phr-mcp wiki-drift           # which ADR decisions lack rules
$ phr-mcp decision new my-slug # scaffold a new ADR page
§06
6

A closing note — on what rules cannot do.

A word on what phronesis does not claim. It persists the prompt-to-think; it does not persist the thinking. A rule that warns trace the call chain end-to-end before claiming done will fire reliably on every git commit -m, and the model — or the human at the keyboard — will see the warning. Whether the operator then traces the chain remains the operator's choice. The rule cannot make anyone do the work; it can only ensure the question is asked again, long after the conversation in which it was first imagined has been compressed away.

What moving enforcement to disk buys you is durability — a fixed set of refusals and reminders that fires the same in hour one and hour ten, in token eight hundred and token nine hundred thousand. That is a smaller claim than is now fashionable to make about software of this kind. In our experience, it is also a useful one. The small project-specific maxims you would otherwise have to repeat across sessions are simply there, on disk, doing their work whether or not the conversation remembers them.

The rule fires the same in token nine hundred thousand as in token eight hundred.

References

Forgy, C. L. (1982). Rete: A Fast Algorithm for the Many Pattern / Many Object Pattern Match Problem. Artificial Intelligence, 19(1), 17–37.

Waterman, A. & García Barrios, L. (2009). Playing with the Rules: Participatory Modeling and Network Gaming through a Rules Engine. Proceedings of RulesFest 2009. El Colegio de la Frontera Sur, San Cristóbal de Las Casas, México.

García-Barrios, L., García-Barrios, R., Waterman, A. & Cruz-Morales, J. (2011). Social dilemmas and individual/group posination strategies in a complex rural land-use game. International Journal of the Commons, 5(2). DOI: 10.18352/ijc.289.

Aristotle. Nicomachean Ethics, Book VI. On φρόνησις (phronesis) as practical wisdom.