SwarmSystem · Operator's Manual · Self-Healing Software

At 9:31 this morning, our dashboard fixed itself.

A crash alert fired. An AI read the stack traces and found one root cause behind six failing screens. It wrote the exact fix. A second AI tried to tear that fix apart, and failed. Then the full test suite voted. Only after every gate passed did the commit land, with an undo command attached. No human touched any of it. This page shows you how the whole system works, and how to build it into your own stack. Guardrails first.

6
crash groups closed by one fix
from our production log, July 14, 2026
3,020+
recorded errors behind those six groups
same log, same morning
1
commit, applied and pushed by the loop
with a revert command attached
0
human touches, end to end
reviewer and test suite gated it
Reference stack Sentry · Claude Code · git The pattern ports to any tracker and any agent CLI Written July 2026, from a running system
WAITING
What this is

Software that files its own fix. With the safety built first.

Self-healing here means one specific thing. When your system crashes, a loop detects it, diagnoses it, proposes an exact code fix, reviews that fix adversarially, runs your full test suite against it, and then either commits it with a complete audit trail or rolls it back to a clean tree. You choose how much of that runs unattended. The cage below is what makes that choice safe.

Detect Triage Propose Review Execute

Who this is for

You run an error tracker. We use Sentry; any tracker with an issues API works. You have a git repo with a test suite you actually trust. You can invoke an LLM agent from the command line with an enforceable tool whitelist. We use Claude Code; the pattern is agent-agnostic. And you have Slack, or any channel your team actually reads. If any of those are missing, build them first. This page will still be here.

How to read this page

Two registers. The main text teaches the pattern, which ports to any stack. Boxes like this one show our reference implementation: real numbers from our running system, so you can see one working instance. Our numbers are outputs of our environment. Tune yours.

WAITING
Part one · Guardrails

Build the cage before the animal.

Most people ask what the AI can fix. The better first question is: what stops a bad fix from shipping? Every unattended commit this system makes must pass through all eleven of these. Skip one and you are not running this system. You are running a slot machine with push access.

The eleven guardrails: each rule and the failure it prevents
GuardrailThe ruleThe failure it prevents
Full-suite gateEvery proposed diff runs your entire test suite before commit. Not a subset.A fix that quietly breaks three other things.
Exact-diff-only writesThe only thing that can touch the repo is a reviewed, machine-materialized git diff."The agent improvised" is not in this system's vocabulary.
Read-only proposerThe diagnosing agent gets read tools only. Enforced by whitelist, not by prompt.A helpful agent editing files mid-diagnosis.
Independent reviewerA second, fresh agent tries to refute every proposal. Any error in review counts as a fail.One model grading its own homework.
Two-strike breakerTwo consecutive failed or rejected proposals pause the whole loop until a human unpauses it.A bad day becoming a bad week.
Per-issue cooldownOne attempt per issue per 24 hours.The loop hammering the same wall all night.
Daily budgetAt most 5 proposals a day, one undecided at a time.Volume replacing judgment.
Regression watchdogAn auto-healed issue that returns within 7 days strikes the breaker.Declaring victory on a symptom.
Kill switchesIndependent flags shut down each layer. Remove the tracker key and everything is off.Needing a deploy to stop the machine.
Notify-with-revertEvery unattended commit posts to your channel with the exact revert command.Hunting through docs during an incident.
Audit lineageEvery transition writes an audit line. Every commit names its approver and the issues it resolves.Archaeology instead of history.
Your preflightWAITING

Tick what is true today. The dot resolves when you are actually ready, not when you feel ready.

0 of 10All ten, or stay at Level 1
When to stop at Level 1

Some stacks should not run this yet.

Your test suite is slow enough that you batch runs instead of running on every change. Or it is flaky, and a gate that lies is worse than no gate. Or coverage is thin exactly where the crashes happen. Or nobody reads the alert channel on weekends.

None of these are permanent. All of them are disqualifying today. A propose-only loop is still worth running while you fix them.

WAITING
Part two · Architecture

One real crash, walked through every gate.

Here is the morning of July 14 in slow motion. Six screens in our dashboard had been crashing for weeks, with six different error signatures. Every one traced to the same seam: a fetch wrapper that let a degraded server answer "success" with no data attached. Watch that one bug travel the whole loop.

01 · DetectDONE

The tracker catches every crash and groups them by signature. Three things make this layer trustworthy rather than noisy. Scrubbing: a before-send hook strips user data from every event, and session replay stays off. The scrubber is load-bearing; give it a regression test. Release stamping: every event carries version plus git sha, so "which commit broke this" is a lookup, not a hunt. Signal hygiene: security denials and expected noise stay in local audit logs. The tracker gets crashes only. Scheduled jobs post check-ins, so silence is itself an alert.

Reference

Our security layer once flooded the tracker with junk issue groups minted from routine denials. One mirror-filter regex ended the burn. Fix the generator, not the instances.

02 · TriageDONE

A poller pulls unresolved issues every 5 minutes, ranks them by event volume and recency, and renders a queue in our dashboard. The cache is stale-while-error: an API failure never wipes what you knew, it only marks it stale. Every issue deep-links back to the tracker. The queue is where a human, or later the loop itself, decides what gets attention.

03 · ProposeDONE

A read-only agent gets the stack traces and one instruction that matters more than the rest: cluster stack-similar issues to one root cause. Six signatures, one seam, one proposal that names all six. The agent cannot write. Its whitelist has read tools only, so "read-only" is enforced by the runtime, not by the prompt. Its output is verbatim find-and-replace edits, never a diff. The server materializes those edits into a canonical git diff and restores the tree byte-for-byte.

Reference

Our first live proposal covered all six issues in one diff: two hunks, one file, at the shared fetch seam. Risk: low. The clustering instruction is why one morning closed six groups.

04 · ReviewDONE

A second agent, fresh context, one job: try to refute the proposal. It reads the diff and every crash site, and hunts for the case where the fix makes things worse. It answers in line format, because a one-token verdict cannot be broken by a long string. And it fails closed: a parse error, a timeout, or an unreadable answer all count as a fail. Nothing ships on a mumble.

The verdict, as our reviewer wrote it (condensed)
VERDICT: pass
CONCERN: one endpoint still returns a bare object instead of the standard envelope
CONCERN: callers that relied on the malformed shape will now see a clean failure
REASONING: the guard normalizes bad GET envelopes to the standard error shape,
which every crash site already handles.

The reviewer passed our fix and still raised real concerns. One became a tracked task. A reviewer that only says yes is decoration.

05 · ExecuteLIVE

The executor is the only thing in the entire system that can write. It runs a fixed chain of gates. Passing every gate earns a commit. Failing any gate lands in the same place: a clean tree, nothing committed, the evidence kept.

  1. 1 · Preflight
    Are the diff's target files clean? A teammate editing the same file means wait, not strike.
    pass ↓fail ↳ wait
  2. 2 · Apply
    git apply the materialized diff. A diff that no longer applies stops here, tree untouched.
    pass ↓fail ↳ rollback
  3. 3 · Full suite
    Every test. Not the fast ones. Not the related ones. All of them.
    green ↓red ↳ gates 4 and 5
  4. 4 · Flake check
    A failing file re-runs alone, with the diff still applied. Passing alone means a contention flake, tolerated within a small bound.
    flake ↓real red ↳ gate 5
  5. 5 · Baseline check
    Is the suite red without the diff too? Then the main branch is broken, not the fix. Wait, do not blame the proposal.
    ours ↓baseline red ↳ wait
  6. 6 · Commit
    Lineage trailers: who approved, which issues this resolves, the heal id.
  7. 7 · Push + resolve
    Issues resolve in the tracker with the sha attached. A regression rule reopens them loudly if the crash returns.
  8. 8 · Notify
    The channel gets the summary and the exact undo command. You are never more than one paste away from reversing it.
    done
The commit, as it landed (identifiers generalized)
heal: guard the GET envelope at the shared fetch seam
      (resolves APP-9, APP-8, APP-A, APP-7, APP-B, APP-1P) [via error-queue]

Approved-By: heal-reviewer (auto, operator tier: low)
Heal-Id: fe3c740e
Sentry-Issue: https://yourorg.sentry.io/issues/APP-9/

The best thing this system did all week was refuse to ship.

Every FAIL branch ends in a clean tree, not a hopeful one

What the loop said that day

9:31 AMRegression alert. A previously resolved crash came back. Queue updated, watchdog noted.
laterProposal ready. One fix, six issues. Risk: low. Diff and diagnosis attached. Waiting on review.
+1 minReview: PASS with 3 concerns. Handing to the executor.
+5 minHealed. Full suite green. Committed, pushed, six issues resolved. Undo: git revert dc52e1d

Condensed from our alerts channel, July 14, 2026. Relative times shown where the exact minute is not in the log.

WAITING
Part three · Implementation

Four levels. Earn each one.

Do not build this in a weekend and flip everything on. Each level below is a complete, useful system on its own. Each ends with a gate you must witness in your own repo before moving up. The autonomy is not the achievement. The gates are.

Level 0 · Watch: detect and triage

needs: TRACKER_DSN

Wire the tracker with a scrubber and release stamping. Set two alert rules: first-seen and regression, both into your channel. Add check-ins to every scheduled job, so a job that silently stops running becomes an alert instead of a mystery. Then build the poller and the ranked queue.

poller.js · a shape, not a library
const EVERY = 5 * 60 * 1000;                      // 5 minutes
setInterval(async () => {
  try {
    const issues = await tracker.listIssues({ status: 'unresolved' });
    const ranked = issues
      .map(i => ({ ...i, score: i.events / Math.max(1, hoursSince(i.lastSeen)) }))
      .sort((a, b) => b.score - a.score);
    cache.write({ issues: ranked, fetchedAt: Date.now() });
  } catch (err) {
    cache.markStale(err.message);                 // stale-while-error: never wipe what you knew
  }
}, EVERY);
scrub.js · the before-send hook is load-bearing
tracker.init({
  dsn: process.env.TRACKER_DSN,                   // unset = the whole integration is off
  sendDefaultPii: false,
  release: `app@${VERSION}+${gitShortSha()}`,     // "which commit broke this" is a lookup
  beforeSend(event) { return scrubUserData(event); }  // give this a regression test
});
The gate out of L0

Throw a deliberate error in production code. It must reach your channel in under a minute, scrubbed of user data, stamped with a release. Skip nothing until this works.

Level 1 · Diagnose: propose-only

adds: HEAL_LOOP=on

Spawn a read-only agent against your ranked queue. Give it the stack traces, the repo, and the clustering instruction. Store its proposals in a queue with the cooldowns from the cage. Nothing writes anything yet. You read the diagnoses and apply the good ones by hand.

propose.js · read-only is a runtime property
const proposal = await agent.run({
  tools: ['Read', 'Grep', 'Glob'],                // no Write, no Bash: it cannot touch the repo
  cwd: REPO_ROOT,
  prompt: buildPrompt(issueCluster),              // traces + "cluster to ONE root cause"
  timeoutMs: 15 * 60 * 1000,
});
// output contract, strictly validated:
// { summary, risk, resolvesIssues: [...], edits: [{ file, find, replace }], testPlan }
const diff = materializeDiff(proposal.edits);     // server-side, below
materialize.js · agents never write diffs
function materializeDiff(edits) {
  assertInsideRepo(edits.map(e => e.file));       // refuse absolute paths and ".." segments
  const before = snapshotBytes(edits);            // exact bytes, for the restore
  try {
    for (const e of edits) applyExactlyOnce(e);   // find must match verbatim, exactly once
    return git('diff');                           // git writes the canonical, appliable diff
  } finally {
    restoreBytes(before);                         // the repo is untouched after this returns
  }
}
The gate out of L1

Run it for a week minimum. Apply at least one proposed diff by hand, after verifying the diagnosis yourself. If the diagnoses are wrong, stay here. Wrong at L1 costs reading time. Wrong at L3 costs a bad commit.

Level 2 · Approve: human-gated commits

adds: AUTO_APPROVE=off · breaker + budgets live

Build the executor with the full gate chain from the loop section. Add an approve and reject control on an exact diff view, with a double-confirm. The breaker, cooldown, and daily budget go live here. Every approval is a named human. Every transition writes an audit line.

execute.js · the gate order, exactly
async function applyProposal(p) {
  if (dirtyTargets(p.diff))   return wait('someone is editing these files');
  if (!gitApply(p.diff))      return fail('diff no longer applies');    // tree untouched
  const suite = runFullSuite();
  if (!suite.green) {
    if (isolatedPass(suite.redFiles, { withDiff: true }))
      { /* contention flake, tolerated within a small bound */ }
    else if (!baselineGreen()) { rollback(); return wait('main is red without this diff'); }
    else                       { rollback(); return fail('the diff broke the suite'); }
  }
  const sha = commit(p, { trailers: lineage(p) }); // Approved-By, issue links, heal id
  push(); resolveIssues(p.resolvesIssues, sha);
  notify(`Healed. ${sha}. Undo: git revert ${sha}`);
  return ok(sha);
}
The gate out of L2

You have personally watched one red suite roll back to a clean tree with nothing committed. Trigger it on purpose with a deliberately bad edit if you must. Do not proceed on faith.

Level 3 · Unattend: reviewer-gated auto, low-risk only

adds: AUTO_APPROVE=low · regression watchdog

Add the adversarial reviewer between proposal and executor. The chain becomes: propose, review, and only a reviewer PASS on a low-risk proposal reaches the executor unattended. Medium and high risk still wait for a human click. The 7-day regression watchdog arms on every auto-healed issue. Every auto-commit posts notify-with-revert.

review.js · fail closed, parse by line
const text     = await freshAgent.run({ tools: ['Read','Grep','Glob'], prompt: refutePrompt(p) });
const verdict  = /^\s*VERDICT:\s*(pass|fail)\b/im.exec(text)?.[1];
const concerns = [...text.matchAll(/^\s*CONCERN:\s*(.+)$/gim)].map(m => m[1]);
if (!verdict) return { pass: false, error: 'unparseable review' };   // fail closed
return { pass: verdict === 'pass', concerns };
Reference

We ran the manual path first. Four things failed on our first live day, and each one became a permanent gate (see the museum below). Only after that, and an explicit operator decision, did we set AUTO_APPROVE=low. Never start at "all".

The gate into L3

Two clean weeks at L2, plus the ladder contract in the governance section signed by whoever owns the repo. Then flip to low, not all. Revisit the tier after a week of unattended heals.

Separate appendix, separate switch: runtime actions (restart a wedged job, clear a stale lock) can run behind their own flag. Keep it off until you have supervised dry-runs, and never point it at anything that moves money.

WAITING
Part four · Evidence

Four failures we keep on display.

Every guardrail on this page exists because something real broke. These four broke on our first live day. Each is now a permanent part of the system. Steal the lessons without paying for them.

July 14, 2026 · first manual approval

The corrupt patch

The first approved fix died at the apply gate: "corrupt patch at line 25." The agent had hand-written a unified diff and got the arithmetic wrong. Nothing shipped; the rollback held. Agents now output verbatim find-and-replace edits, and git itself writes the diff.

What it built: exact-diff-only writes

July 14, 2026 · first auto review

The verdict that broke JSON

The reviewer returned its verdict as JSON, and one long explanation string arrived unterminated. The parse failed, and fail-closed held: no commit. Verdicts moved to line format, where a one-token answer cannot be broken by a long sentence.

What it built: the fail-closed reviewer

July 14, 2026 · mid-morning

The flake that killed a good fix

A reviewer-passed heal was rejected because one unrelated test file goes red under parallel load. It had done the same thing earlier that day, without any diff. The executor now re-runs red files in isolation with the diff applied. Genuine reds still roll back.

What it built: flake discipline inside the suite gate

July 14, 2026 · same afternoon

The red that wasn't ours

A teammate's commit turned the main branch red while a heal was in flight. The loop blamed the proposal and burned a strike on someone else's bug. Now it checks the baseline: a suite that is red without the diff means wait, not strike.

What it built: baseline awareness

WAITING
Part five · The honest ledger

What it costs. What it pays.

CostsWORKING

Agent tokens. Every heal is two agent runs: a proposer and a reviewer, minutes of runtime each. Cheap next to an engineer-hour, but not free.

Tracker plan realities. Alert rules and issue APIs are standard. Extras are not: budget for cron monitors if you want vendor-side check-in alerts, and treat vendor AI root-cause add-ons as optional. The loop is complete without them.

The discipline tax. A green, fast, trusted test suite is the entry fee, and it never stops being due. The loop amplifies whatever your suite is: rigor or noise.

Human time, early. The L1 and L2 weeks are real hours spent reading diffs. That reading is what buys the right to stop reading.

Reference

Our plan includes one active cron monitor; more run about $0.78 per month each (public Sentry pricing). The vendor's AI root-cause add-on answered 402 until budgeted. We link out to it and let the propose agent do the diagnosis.

PaysDONE

Crashes triaged in minutes, not weeks. The 5-minute poll turns a crash into a ranked queue entry with the stack trace attached before anyone notices the symptom.

Silent-error debt stops compounding. Six of our screens crashed for weeks because nobody had a spare afternoon. One unattended morning ended all six.

Mornings become a log read. The channel tells you what crashed, what the loop proposed, what the reviewer thought, what shipped, and how to undo it. Two minutes, and you know.

The audit trail is free. Lineage trailers answer "what changed and why" forever, for every heal, without anyone writing a postmortem.

WAITING
Part six · Ownership

Who holds the keys.

Autonomy is a governance decision wearing a technical costume. Write this part down before you flip anything.

The ladder contract

  1. Name the owner. One human owns the repo's autonomy level. Not a team, not a rotation. A name.
  2. L0 to L1: the owner turns on proposals after the L0 gate passes.
  3. L1 to L2: the owner reads a week of diagnoses and signs off on their quality.
  4. L2 to L3: two clean weeks, and the owner has personally watched a rollback happen.
  5. Any incident: anyone on the team can flip the loop off, no permission needed. Turning it back on takes the owner.

What never gets automated

Money paths: billing, payouts, bank syncs. Schema migrations. Security configuration. Anything that talks to a customer. The loop fixes crashes. It does not do surgery. Ours will not even auto-restart a wedged job on a money path; those stay propose-only regardless of any flag.

The circuit panel

Every layer has its own switch. Shell environment beats the env file for these flags, so a supervised terminal can dry-run a level without changing the system's defaults.

Kill switches and autonomy flags, and what each setting does
FlagEffect
TRACKER_DSN removedEverything off. The app boots identically without it.
HEAL_LOOP=offLoop off. No proposals, no reviews, no commits.
AUTO_APPROVE=offDefault. Every diff waits for a human click.
AUTO_APPROVE=lowReviewer-gated auto for low-risk proposals only.
AUTO_APPROVE=allAny risk, unattended. We do not run this. Neither should you.
AUTO_HEAL=offDefault. Runtime restart actions propose only. A separate ladder with its own dry-runs.

Tune our numbers to your suite

Every constant on this page is an output of our environment. Our full suite is 1,900+ tests and finishes in about 4 minutes, so 5 proposals a day is cheap for us. If your suite takes an hour, your budget is different and your cooldowns are different. Copy the gates. Do not copy the constants.

And the last line of the contract: you own this loop, its commits, and its risk. This page is the map, not a warranty.

WAITING
What changes

What mornings look like now.

The tracker is quiet, or it isn't. Either way the channel tells the story: what crashed overnight, what the loop proposed, what the reviewer thought, what shipped, and the one line that undoes it. You read for two minutes and start the day knowing.

The morning of July 14, ours read: one alert at 9:31, one proposal, one passing review, one green suite, six issues closed, one revert command nobody needed. We built the cage first. Then we let it run.

Building your own second brain?

Write us what you are wiring up and where it fights you. We answer operator to operator.

Write to us See what we build