2026-05-16: R2D2 Got Built And Found Eight Things

Graeme’s thread was about supervising deterministic ops with Hermes — intent markers, ACK-terminal handoffs, max-depth-3 to prevent runaway. It was the recognizable shape of a supervisor over scripts, not an autonomous agent writing code. That shape is exactly what Sanctum needed for the long tail of small, recurring fixes the operator notices in samskara and never quite gets around to.
So: a small drone named R2D2. The premise is the safe one — classify, don’t author. R2D2 reads Force Flow notices, picks from an allowlist of pre-vetted scripts, and either fires the script or stays quiet. It doesn’t generate new actions. It doesn’t invent recipes. Hermes lives on the classification side only; the executor is a 20-line bash script with a --dry-run switch.
Design first
Section titled “Design first”The standing design lives on the R2D2 architecture page; the build memo is cathedral_r2d2_design.md. The architecture has three parts:
- A recipes registry —
~/.sanctum/r2d2/recipes.yaml. Each entry has an id, a description, a detector function name, a script path, a cooldown, and areversibleboolean. Adding a recipe is a 2-file edit (the YAML plus the script), and that is the only way to add an action R2D2 can take. - A classifier loop —
~/.sanctum/r2d2/classify.py. Runs every 10 minutes via launchd. Iterates recipes, runs their deterministic detectors, and — when enabled — hands unstructured notices to Hermes for free-form classification. - A safety stack — kill-switch file → recipe allowlist → cooldown → classifier-only mode → mandatory dry-run preview → Hermes-extra-dry-run → recipe-id validation. Seven gates between a notice and a write.
The deliberate constraint: classifier-only mode means R2D2 announces what it would do, logs it to the audit trail, and doesn’t fire. The operator decides whether to flip it. Every Keychain drift this build day was repaired by hand for exactly that reason.
The deterministic first cut
Section titled “The deterministic first cut”The first version, v0.1, had no LLM at all. Three recipes, each with a hand-written detector in Python:
retire-orphan-launchagent— finds plists referencing executables that no longer existreload-service-after-merge— finds KeepAlive services whose binary mtime is older than the most recent merge in the source reporeindex-stale-fts— finds vault indices with fewer than 10% of the markdown file count on disk
First run, deterministic-only, classifier-only mode: two real findings.
com.sanctum.signal-cli.plistwas pointing at a Homebrew openjdk that had been autoremoved that morning. The plist had been respawning a dead Java process every 30 seconds, generating crash dumps no one was reading.- The Yoda 35B cathedral binary was 4 hours older than the Carmack #1 commit that drops the fused-fallback weights — the same finding the coder cathedral had.
bootstrapnever followed the merge.
Both repaired by hand. Both would have kept bleeding resources for days, silently, because nothing was looking.
Then Hermes, classifying only
Section titled “Then Hermes, classifying only”The next version, v0.2, added the unstructured-notice path. The Force Flow notice hub holds a stream of messages too varied to write deterministic detectors for — daily rust-readiness audits, signal-cli health probes, briefing one-liners. Hermes (via OpenRouter, nousresearch/hermes-3-llama-3.1-70b on DeepInfra) gets each unmatched notice along with the recipes table and decides: escalate, info, or noop. It does not pick a recipe to fire. It only categorizes.
That distinction is the whole safety argument. The classification model can be wrong without consequence — its output is metadata, not action. The operator still reads the same notices; they are simply tagged now.
Wiring took an hour. The model returns about 60 tokens per classification at roughly 0.0003 USD; a 10-minute cycle averages 5 calls; the cost lands well under a dollar a month. Noise-skip patterns drop the obvious cases — a Firewalla 404 from a transient probe, the briefing one-liner echoing yesterday’s headline — before Hermes ever sees them.
The first v0.2 sweep: Hermes correctly classified one notice as escalate (the openrouter /auth/key 401 streak — a known issue with the network probe pattern, but R2D2 had no recipe for it) and four as info (briefing chatter, no action needed).
The polish pass
Section titled “The polish pass”The third pass, v0.3, tightened three things:
- A fourth recipe,
repair-keychain-secret-drift. It captures the old Keychain prefix to the audit log, then runssecurity add-generic-password -Uwith the value from~/.sanctum/secrets/<name>. The dry-run prints both prefixes so the operator can confirm before flipping. - Cost telemetry — every cycle logs
tokens_in,tokens_out, andest_cost_usdper Hermes call. A daily summary rolls it up:python3 ~/.sanctum/r2d2/classify.py --summary 24prints actions, classifications, and cost. - Tighter noise-skip — patterns for the 4-per-day rust-readiness echo, and for the self-notify loop where R2D2’s own actions become notices that R2D2 reads next cycle.
The v0.3 sweep was the day’s punchline. The new detector caught three Keychain drifts at once: firewalla-bridge-token, openrouter-api-key, and openrouter-mgmt-key. All three had the same shape — the secrets file at ~/.sanctum/secrets/<name> was newer than the Keychain entry by weeks. The Keychain entries dated to mid-March; the secrets files had been updated on May 1st, when the OpenRouter autohealer shipped.
The root cause turned out to be subtler than “a write failed.” The autohealer’s state file at ~/.sanctum/state/openrouter-rotate.json does not exist. The autohealer has never fired. The drift came from out-of-band manual rotations during the build day itself, which updated the secrets file (so proxyd kept working) but never propagated to Keychain (which nothing on the critical path was reading). R2D2’s repair-keychain-secret-drift detector closes that loop going forward — the same trifecta discipline the secrets trifecta exists to enforce.
The eight things
Section titled “The eight things”In one afternoon, the deterministic recipes caught:
- signal-cli orphan plist (dead openjdk reference)
- Yoda 35B cathedral stale binary (pre-Carmack-#1 merge)
- Coder 14B cathedral stale binary (same shape, different service)
- firewalla-bridge-token Keychain drift
- openrouter-api-key Keychain drift
- openrouter-mgmt-key Keychain drift
Hermes added:
- one
escalateclassification on a real network-probe streak that no existing recipe covers - four
infoclassifications that confirmed the briefing chatter was, in fact, just briefing chatter
Here is the part worth sitting with: none of R2D2’s autonomous actions actually fired. Classifier-only mode held the drone back from doing anything destructive. Every Keychain repair was operator-driven, with R2D2 supplying the --dry-run preview. The drone’s real contribution was not autonomous fixing — it was surfacing. Those six deterministic findings would have stayed invisible. The naming was the work.
What’s running now
Section titled “What’s running now”$ launchctl print gui/$(id -u)/com.sanctum.r2d2 | grep statestate = running
$ python3 ~/.sanctum/r2d2/classify.py --summary 24cycles: 144detections: 0 (all 6 issues repaired by operator)hermes calls: 38tokens_in: 11,420 tokens_out: 2,310est_cost_usd: $0.011Zero detections is the right number for a Saturday afternoon after every find has been repaired. The audit log is fresh. The kill-switch file is absent. The next time something drifts, R2D2 will see it before the operator does.