Table of Contents
I’d just finished cleaning up after a v4 upgrade of my Personal AI Infrastructure. Everything technically worked. Services were up, hooks were firing, the file structure was where I’d left it. But something felt off — that hum you get when a system is running but not quite right.
So I did something I don’t usually do. I asked my AI an open question.
“What gaps are you noticing?”
I expected a list of small bugs. Maybe a TODO surface, maybe a “you forgot to wire up X.” What I got instead stopped me.
“Your PAI doesn’t watch itself. When something drifts, no one knows until it breaks.”
That was the whole answer. Not a bug — a structural blindspot.
And then the reframe, which I think is the actual insight: PRDs already declare ideal state. I’d been writing design docs for months that specified exactly what each piece of the system should look like — what symlinks should exist, what services should be running, what context should be loadable. The specs were already there. I just wasn’t using them as source of truth. The system kept moving, the PRDs sat in a directory, and nothing was checking whether reality still matched the declaration.
A few hours later, I had five phases shipped.
Phase A is the manifest. A YAML file at ~/.claude/PAI/USER/MANIFEST.yaml declares what should exist. The first version covers the Pattern 1 cascade symlinks — the user-content symlinks that have broken on every PAI upgrade I’ve done. A probe script reads the manifest, checks reality, writes a JSON report. Healthy or drift. Per entry. No interpretation.
Phase B does the same thing for open followup items in PRDs. A scanner walks all the - [ ] lines and tracks each by stable identity (sha256 of the line content). The first version aged items by the PRD’s updated: timestamp — which turned out to be useless, because every PRD edit reset every item’s age. So I rewrote it the same day. Now each item carries its own first-seen timestamp via snapshot diff. Drift surfaces at the item level, not the file level.
Phase C is /health. A slash command that reads the probe outputs, rolls them into a single verdict — healthy, drift, or error — and exits with 0/1/2 so cron can consume it. It also writes a timestamped snapshot of the verdict on every invocation. That snapshot trail is what Phase E reads.
Phase D is where it gets interesting. Auto-repair. The whole point of declaring expected state is being able to act on the gap, but auto-repair is also where trust gets put on the line. So /heal is paranoid by design. Default-off allowlist — handlers never run unless I’ve explicitly named them in a YAML config the framework checks for ownership and mode. Missing-only repair — the handler creates symlinks where nothing exists, refuses to overwrite or delete. Whitelisted target paths. Append-only JSON log of every action. Re-validates state at execute time, never trusts the dry-run plan.
Before I wrote a line of Phase D code, I ran a RedTeam pass on the design. Seven attack vectors surfaced — manifest tampering, target path traversal, dry-run trust gap, allowlist bypass, log injection, and two more. Each became a hard rule in the implementation. When the code-reviewer agent ran later that day, it caught one I’d still missed: the probe state file lacked the same ownership check as the allowlist. Trivial fix, real bug, and a lesson worth keeping — RedTeam-first for trust-critical work, every time.
Phase E is /decay-check. It scans the snapshot history, finds probes that have been failing for seven or more consecutive days, and escalates via voice + log + Telegram, deduped to once per probe per ISO-week so I don’t get the same alarm twenty times. There’s a systemd timer that fires it daily at 09:00. The system watches itself now. Nobody has to ask it to.
I ran /heal --execute for the first time on a real drift — a missing symlink — and a single symlink(2) syscall later, the cascade was whole again, the action recorded in the log, exit 0. There’s something quiet and correct about that moment. The kind of moment you build for.
What I keep coming back to is that one reframe at the top. The PRDs were already telling the system what it should look like. I just hadn’t taught the system to read them. Once I did, the rest was an afternoon of careful plumbing — and a paranoid little auto-repair handler that earns trust the only way trust gets earned: through constraints.
If you’re building anything that needs to keep running while you’re not watching, write down what “healthy” looks like first. Then build the thing that reads it.



