Self-Healing Loop: Bidirectional Rule-Audit Feedback¶
A pattern where rules feed audit checks AND audit checks feed rules, creating a system that grows organically from real usage.
The Problem¶
Most rule systems are write-once:
- Someone writes rules
- Someone writes audit checks
- Over time, new rules appear that no audit check covers
- Over time, audit checks find issues that no rule addresses
- The gap between "rules we have" and "rules we need" grows silently
The Pattern¶
The self-healing loop connects three components. Drift detection is
structurally enforced via cross_check.py, which is the actual
enforcement mechanism: it runs a bounded 2-pass cycle (detect, fix,
re-check, log remaining), compares expected state from the manifest
against live checks, and generates concrete write_back_suggestions
for any persistent drift. This moves the pattern beyond detection-only
— the system actively proposes how to resolve drift.
graph TD
A["Rules Files<br/>(what to do)"] -->|"UPDATE RULE fires"| B["Audit Checks<br/>(what to verify)"]
B -->|"Audit finds scattered items"| A
C["Session Learnings<br/>(what went wrong)"] -->|"Route to rules"| A
A -->|"New rule implies check?"| B
B -->|"Check reveals gap?"| A
B -->|"Persistent drift detected"| W["cross_check.py generates<br/>write_back_suggestions"]
W -->|"Suggest manifest update<br/>or investigation"| A
style A fill:#4a90d9,color:#fff
style B fill:#5cb85c,color:#fff
style C fill:#f0ad4e,color:#fff
style W fill:#9b59b6,color:#fff
Forward Flow: Rule → Audit Check¶
Every time a rule is added or updated, ask:
"Does this new rule imply something that should be verified automatically?"
If yes, add a check to your infrastructure checks or cross-check config.
Examples:
| New Rule | Implied Check |
|---|---|
| "Use pnpm, not npm" | grep -r "npm run" scripts/ \| grep -v pnpm \| wc -l should be 0 |
| "All API endpoints require auth" | grep -r "@public" routes/ \| wc -l should be 0 |
| "Database migrations must be reversible" | Each migration file has both up() and down() |
| "No TODO comments in main branch" | git diff main \| grep TODO \| wc -l should be 0 |
Backward Flow: Audit → Rule¶
When an audit check discovers something unexpected, ask:
"Does this finding represent a pattern that should be a rule?"
If yes, add a rule to your rules file, then apply the forward flow (does this new rule need a check?).
Examples:
| Audit Finding | New Rule |
|---|---|
| "3 files still reference old API v2 path" | "After any path change, grep the full codebase for the old path" |
| "Config file has stale count (says 12, actually 15)" | "After adding items, update all references to the count" |
| "Two scripts import from deprecated module" | "Module X is deprecated, import from module Y instead" |
The Learning Input¶
Session learnings (mistakes, surprises, operational discoveries) are the third input. They get routed to rules, which trigger the forward flow:
graph LR
A["Failure happens"] --> B["Learning captured"]
B --> C["Routed to rules file"]
C --> D{"Does rule imply<br/>a check?"}
D -->|YES| E["Add check"]
D -->|NO| F["Done"]
style A fill:#d9534f,color:#fff
style E fill:#5cb85c,color:#fff
Implementation¶
With YAML Config (Method A)¶
After editing rules/core-rules.md, check your startup-config.yaml:
# Ask: does the new rule imply a verifiable check?
checks:
- name: no-npm-usage # Added because of "use pnpm" rule
command: "grep -r 'npm run' scripts/ | grep -v pnpm | wc -l | tr -d ' '"
validator: "equals:0"
fail_message: "npm usage found — should be pnpm"
With SQLite (Method B)¶
-- After adding a rule
INSERT INTO rules (name, content, category, tier)
VALUES ('use-pnpm', 'Use pnpm, not npm...', 'tooling', 1);
-- Ask: does this imply a check?
INSERT INTO checks (name, command, validator, fail_message)
VALUES ('no-npm-usage',
'grep -r "npm run" scripts/ | grep -v pnpm | wc -l | tr -d '' ''',
'equals:0',
'npm usage found — should be pnpm');
Infinite Loop Guard¶
Both directions MUST check "is this already there?" before adding:
- Before adding a rule: search existing rules for a similar entry
- Before adding a check: search existing checks for one that covers the same thing
- Never re-process items you just added in the same session
The rule is: act on NEW items only. If it's already captured, move on.
When to Apply¶
Apply the self-healing check:
- After adding any rule → "Does this imply a check?"
- After any audit finding → "Does this imply a rule?"
- After any session learning → "Which rule file does this belong to?"
- After any file edit → "Is this scattered information that belongs in a consolidated file?"
The last question is RULE ZERO — see the Rule Zero pattern.
The Enforcement Mechanism: cross_check.py¶
cross_check.py is the engine that makes the self-healing loop
operational. It runs once per session after Tier 1 loads and implements
a bounded 2-pass design:
- Pass 1 — Detect and auto-heal. Compare every expected value in the
manifest's
cross_check.expected_countsagainst a live command result. Items markedauto_heal: truewith aheal_commandare fixed automatically. - Pass 2 — Re-check healed items only. Any healed item that still drifts is moved to the drifted list.
- Stop. No further passes run. The sentinel is marked
cross_check_done: trueto prevent re-runs within the same session.
This bounded design prevents infinite correction loops — the system gets exactly two chances to converge, then logs whatever remains.
Write-Back Suggestions¶
When drift persists after both passes, cross_check.py (lines 116-133)
generates structured fix suggestions and stores them in the sentinel
file at sentinel["write_back_suggestions"].
Two formats are used depending on whether the check had a heal command:
| Condition | Suggestion format |
|---|---|
Check has a heal_command (auto-heal was attempted but source still drifted) |
SUGGESTED FIX for '<name>': update expected value in manifest (X -> Y), or fix the source. |
Check has no heal_command (manual-only item) |
UNRESOLVED DRIFT '<name>': expected X, got Y. Update manifest or investigate. |
What triggers suggestions: Any item in the drifted list after
Pass 2 completes — meaning auto-heal either wasn't configured or
didn't resolve the mismatch.
Where stored: sentinel["write_back_suggestions"] — a JSON array
of suggestion strings written to the sentinel file in $TMPDIR.
What happens next: The agent reads the sentinel on subsequent prompts. It can act on suggestions directly (e.g., updating the manifest count) or flag them for human review if the drift source is unclear. The suggestions are also printed to stdout during the hook run so they appear in the session log.
Standalone Audit Runner¶
The audit.py script provides on-demand execution of the check side of
the self-healing loop. While startup checks run automatically at session
start, the audit runner lets you verify infrastructure state at any point
during a session. It uses the same validator framework, so checks written
for startup work identically when run standalone. See the
Audit Runner reference for usage and configuration.