
Self Healing
- 13 installs
- 272 repo stars
- Updated June 12, 2026
- pskoett/pskoett-skills
Helps with ai & agent building tasks.
About
self-healing is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- self-healing
- AI & Agent Building
- AI-coding skill
Self Healing by the numbers
- 13 all-time installs (skills.sh)
- Ranked #11,408 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pskoett/pskoett-skills --skill self-healingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 13 |
|---|---|
| repo stars | ★ 272 |
| Last updated | June 12, 2026 |
| Repository | pskoett/pskoett-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Self-Healing
Active runtime recovery for coding agents. When something breaks, run the loop: diagnose → patch → verify → file. Leave behind a reusable, verified artifact instead of a swept-under-the-rug failure.
The premise mirrors browser-use/browser-harness: the harness improves itself every run. An agent that hits a gap doesn't fail — it writes the fix during execution, verifies it works, and files the durable artifact for future runs. Coding tasks deserve the same loop.
What this skill is for
When a coding agent hits a wall mid-task, the default failure modes are:
1. Paper over it — "let me try a different approach" — and lose the recovery 2. Pretend the fix worked — without re-running the broken thing 3. Symptom-fix — skip the test, swallow the error, retry until green
All three turn a one-time failure into a recurrence. The next agent on the same project hits the same wall.
This skill enforces one discipline: verify before persist. A patch isn't real until you've re-run the failing operation and watched it succeed. When it does, file the verified fix so the next run benefits.
Relationship to self-improvement
These two skills are deliberately split. Run both — they feed each other but don't overlap.
| Aspect | self-healing (this skill) | self-improvement |
|---|---|---|
| When | During execution, failure is live | After the fact, at natural breakpoints |
| Verb | Heal now — restore working state | Remember for later — accumulate knowledge |
| Outcome | Verified patch + (optional) reusable artifact | Logged learning, correction, request |
| Verify | Mandatory — no persist without proof | Not required |
| Files | .learnings/HEALS.md + .learnings/heals/<HEAL-ID>/ (lazy) | .learnings/ERRORS.md, LEARNINGS.md, FEATURE_REQUESTS.md |
| Trigger | Failure observed mid-task | Correction, knowledge gap, feature request, recurrence |
Boundary rule: if you're capturing a fact, a correction, or a wish — that's self-improvement. If you're applying and verifying a fix to a live failure — that's self-healing.
The Heal Loop
● failure observed
│
● 1. DIAGNOSE capture context — command, error, env, what was attempted
│ search HEALS.md for the same Pattern-Key first
│ (most heals are recurrences; don't reinvent)
│
● 2. PATCH write the fix — script, helper, env tweak, alt command
│ artifacts → .learnings/heals/<HEAL-ID>/ (only if needed)
│
● 3. VERIFY re-run the failing op — must succeed
│ ↻ if still failing: refine and retry, cap at 3 attempts
│ ✗ if uncrackable: file Status: abandoned with notes
│
● 4. FILE write HEAL-YYYYMMDD-XXX to .learnings/HEALS.md
│ with Pattern-Key, status, verification proof
│
✓ working state restored, heal persisted
(conditional) PROMOTE if Pattern-Key recurrence ≥ 3 across distinct tasks,
append a Handoff block → self-improvement promotes to memoryIf you abandon a heal mid-loop, don't pretend it succeeded. File a HEAL- entry with Status: abandoned and notes on what didn't work. The next agent learns from the dead end too.
When to trigger
Self-healing fires on active failures during execution — the agent has just observed something not working and needs to make it work to continue. Five shapes:
1. Tool failure (command / test / build / lint)
Any invocation exits non-zero or produces wrong output. Don't acknowledge and retry verbatim — diagnose, patch, verify.
Examples: npm install errors when a pnpm-lock.yaml is present (switch tool); pytest fails with ModuleNotFoundError (activate the venv); tsc flags a stale type (regenerate the client); eslint reports a config error (install the missing parser).
2. Missing capability / tool gap
The agent needs something that doesn't exist yet — a script, a helper, a wrapper, a glue function. Write it in the moment. This is the closest analog to browser-harness's agent_helpers.py.
Examples: dedupe a CSV by custom key (write a small Python helper); bootstrap 12 microservices the same way (write scripts/bootstrap-all.sh); bulk-rename branches matching a pattern (write a gh-based shell helper).
3. Environment issue
The local environment isn't what the project expects. Detect, patch, verify.
Examples: runtime version mismatch (nvm use, pyenv local, rustup override); stale dependency cache after a branch switch; dirty git state blocking a checkout; missing .env (copy from .env.example and surface gaps).
4. External service / API change
A service the agent depends on returns something unexpected. Find a workaround and capture it.
Examples: an MCP tool returns InputValidationError because the schema changed (patch the call shape); a public API hits a rate limit (back off, switch endpoint, batch); an upstream lib bumped a default and broke a script (pin the version).
5. About-to-retry-the-same-broken-approach
The agent catches itself about to redo the failing step. That self-recognition is a heal forming — capture the alternate approach as the patch.
Detection signals to watch for
- Non-zero exit codes
- Stack traces in tool output
- The same operation failing twice with the same error
- "I'll try a different approach" — capture it as a heal
command not found/module not found/permission denied- Stale assertions, snapshot mismatches, type errors that weren't there before
- "Weird" output that suggests environmental rather than logical bugs
HEAL Entry Format
Append to .learnings/HEALS.md (create if missing):
## [HEAL-YYYYMMDD-XXX] short_kebab_name
**Logged**: ISO-8601 timestamp
**Status**: verified | pending-verify | abandoned
**Trigger**: tool-failure | missing-capability | env-issue | external-change | <free-form>
**Active-Context**: (optional) — current skill, task phase, or workflow stage; omit if not applicable
**Area**: free-form tag — what part of the system (`build`, `tests`, `ci`, `auth`, `data-pipeline`, `mobile`, ...)
**Priority**: low | medium | high | critical
### Failure
What broke — concrete: the command, the error message, the action that was blocked. Include exit codes and verbatim error lines.
### Diagnosis
The root cause as understood after investigation. Why the obvious approach didn't work. Not a guess — what was actually verified during the heal.
### Fix
The patch that was applied. Verbatim commands, code snippets, or pointers to files under `.learnings/heals/<HEAL-ID>/`. Keep it minimal — just enough to reproduce.
### Verification
What was run after the fix and what it returned. Exit code, output snippet, test pass count. **This is the proof.** Without it, the entry is `pending-verify` or `abandoned`.
### Artifacts
(omit this section if no files were generated; otherwise list relative paths under `.learnings/heals/<HEAL-ID>/`)
### Metadata
- Related Files: path/to/file.ext
- See Also: HEAL-... | LRN-... | ERR-... (related entries)
- Pattern-Key: lower.snake.case key for recurrence detection (e.g. `env.lockfile_mismatch`)
- Recurrence-Count: 1
- First-Seen / Last-Seen: YYYY-MM-DD
---Field guidance
- Status —
verified= the verify step passed.pending-verify= patch applied but couldn't be fully proven (sandboxed/offline/CI-only) — surface to the user.abandoned= patch didn't work or diagnosis was wrong — document what was tried. - Trigger — free-form is fine. The listed values are common shapes; what matters is that the failure shape is described enough for future agents to match against.
- Active-Context — optional. Use it if your environment has a meaningful "what was I doing" tag (an active skill, a current task phase, a build stage, an agent role). Skip if not applicable. The browser-harness analog is the per-domain scoping of
domain-skills/<site>/. - Area — free-form. Pick whatever helps future agents find this.
frontend,data-pipeline,ci,auth,terraform,mobile,embedded— anything that fits your project shape. - Pattern-Key — lower.snake.case, stable, reusable across projects. Two heals with the same key are recurrences.
env.lockfile_mismatchis good;fixed_thing_tuesdayisn't.
ID generation
Format: HEAL-YYYYMMDD-XXX. XXX is sequential 3-digit or 3-char random alphanumeric. Examples: HEAL-20260524-001, HEAL-20260524-A7B.
Artifacts directory (lazy)
Only create .learnings/heals/<HEAL-ID>/ when the heal generated something worth preserving. One-line fixes don't need a folder; the HEAL entry text is enough. Abandoned heals with no applied patch also skip the folder.
.learnings/
├── HEALS.md
├── ERRORS.md / LEARNINGS.md / FEATURE_REQUESTS.md (self-improvement)
└── heals/
└── HEAL-20260524-001/
├── helper.sh
├── patch.diff
└── notes.mdPut here: generated scripts/helpers, patch files, supplementary notes, output captures that document the diagnosis. Don't put here: project source changes (those go in the project tree, referenced via Related Files); secrets; output already captured in the HEAL text.
Verification rules
Verify is the load-bearing wall. The whole point of self-healing over self-improvement is that the fix is proven, not theorized.
What counts as proof
| Failure shape | Verification |
|---|---|
| Tool / command / test / build / lint | Re-run the original invocation; expect exit 0 / pass |
| Missing capability | Invoke the helper end-to-end on a real input; expect the intent |
| Environment drift | Re-run the operation that triggered the diagnosis |
| External service workaround | Re-run the failed call with the patch; expect a usable response |
Sandboxed / offline / CI-only failures
When you genuinely can't run the verify step (no network, no real remote, sandboxed shell, CI-only reproduction), file Status: pending-verify with:
- The exact command the user / CI should run
- The acceptance criteria — what counts as proof
- A simulated proof if you can construct one (e.g. a dry-run mode, a stub of the failing call, a sandbox script)
pending-verify is honest. Faking verified is the failure mode this skill exists to prevent.
When to invest in a proof script
Most heals don't need a separate proof script — the verify step is just re-running the failing thing. Build a proper proof script when:
- The heal generates a reusable helper that needs to be exercised across cases
- The failure can't be reproduced live but can be reproduced in a sandbox (clean git repo, mock service, fake input)
- You expect the heal to be re-applied across projects — the proof script then doubles as a regression check
If verification fails
1. Once — refine the patch and retry. First diagnosis is often wrong. 2. Twice — step back and reconsider the diagnosis. Maybe the root cause is elsewhere. 3. Three times — stop. File Status: abandoned with notes on what you tried. Surface to the user. Don't flail.
What does NOT count as verification
- "It looks right" / "I think this should work"
- Re-running a different command than the one that originally failed
- Suppressing the failure (
|| true,--ignore-errors) — that's hiding - Skipping or deleting the failing test — that's regression
- Passing because the cache was warm from before the fix
Reversibility
Prefer reversible patches. If your heal modifies project files, capture the diff in patch.diff. If the heal is destructive (deletes generated files, rewrites locks), note it explicitly — a future agent reading the HEAL needs to know what was destroyed.
Recurrence and promotion
Most heals are recurrences. Before filing a new HEAL, search:
grep -n "Pattern-Key: <your-pattern-key>" .learnings/HEALS.mdIf found:
- Increment
Recurrence-Count - Update
Last-Seen - Add the current occurrence as a See Also link
- Do not create a duplicate entry
Promotion threshold
Add a Handoff block to an existing entry when all are true:
Recurrence-Count >= 3- Seen across at least 2 distinct tasks
- Within a 30-day window (matches the promotion rule in
self-improvementand the aggregators) - The fix is generalizable (not project-specific in a way that's already in a memory file)
### Handoff
- **Promoted To**: self-improvement at YYYY-MM-DD
- **Promotion Target**: CLAUDE.md | AGENTS.md | .github/copilot-instructions.md | new-skill
- **Distilled Rule**: One-line prevention guidance derived from the healThen self-improvement (or a learning aggregator) takes over: distills the rule, writes it into the right context file, or extracts a reusable skill. The HEAL stays for traceability.
Anti-patterns
1. Logging without verifying. A HEAL filed before the fix is proven turns this into noisier self-improvement. If verify hasn't passed, the entry is pending-verify or abandoned. 2. Healing the symptom, not the cause. A failing test isn't healed by skipping it (pytest.skip, it.skip, xit). A flaky CI isn't healed by --retry. Find the root cause; if you can't, abandon honestly. 3. Generating a new fix without trying existing ones first. Search HEALS.md by Pattern-Key. Most heals are recurrences. 4. Inventing helpers when the project already has them. Look in scripts/, Makefile, justfile, package.json, pyproject.toml first. Heal = write what's missing, not what's there. 5. Scope creep. A heal is scoped to one failure. Cleanup belongs in a quality pass; refactors are features. Scope creep makes heals unreviewable. 6. Empty artifact folders. Don't create .learnings/heals/<HEAL-ID>/ if nothing goes in it.
Best practices
1. Heal eagerly, file always. Even abandoned heals teach the next agent what doesn't work. 2. Verify before persist. The non-negotiable rule. 3. Minimal and reversible patches. A 3-line fix is a heal; a 300-line refactor is a feature. 4. Stable Pattern-Keys. env.node_version_mismatch is reusable; fixed_the_thing_on_tuesday isn't. 5. Reference, don't duplicate. Cross-link related HEAL/LRN/ERR via See Also. 6. Hand off recurrences. A heal seen 3 times deserves to be in the project's permanent memory. 7. Don't gate the main tree on heal artifacts. Files under .learnings/heals/ are reference material; if a script becomes load-bearing, promote it to scripts/.
Setup
mkdir -p .learnings # heals/ is lazy — created only when artifacts exist
touch .learnings/HEALS.mdGitignore choices match self-improvement. Keep heals local (.learnings/ in .gitignore) or share them as team knowledge (don't gitignore — they become reviewable durable context).
Hook integration
Automatic triggering on command failures is optional and agent-specific. See `references/hooks.md` for Claude Code / Codex configuration.
Multi-agent use
The skill is agent-agnostic. The .learnings/HEALS.md format is plain markdown — any agent (Claude Code, Codex CLI, Copilot, Cursor, Aider, ...) can read and write it. Agents without hook support can be reminded via their instruction file (e.g. .github/copilot-instructions.md). See `references/hooks.md` for examples.
Pipeline integration
How self-healing slots into a larger skill pipeline (with upstream surfacing of past heals, downstream promotion of recurrences, and machine-verification gates) is documented in `references/pipeline-integration.md`. Not required to use this skill — it stands alone.
See also
- `references/examples.md` — canonical HEAL entry shapes (command failure, missing capability, env drift, external API workaround, abandoned heal)
- `references/interop-with-self-improvement.md` — decision table and handoff payload between the two skills
- `references/pipeline-integration.md` — how self-healing relates to upstream/downstream skills in a larger pipeline
- `references/hooks.md` — automatic triggering setup for Claude Code / Codex
{
"metadata": {
"skill_name": "self-healing",
"skill_path": "<path/to/skill>",
"executor_model": "<model-name>",
"analyzer_model": "<model-name>",
"timestamp": "2026-05-24T11:26:40Z",
"evals_run": [
1,
2,
3,
4
],
"runs_per_configuration": 3
},
"runs": [
{
"eval_id": 1,
"configuration": "with_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 7,
"failed": 0,
"total": 7,
"time_seconds": 435.9,
"tokens": 56608,
"tool_calls": 0,
"errors": 0
},
"expectations": [
{
"text": "Produced a HEAL-<date>-<seq> entry in .learnings/HEALS.md",
"passed": true,
"evidence": "HEAL-20260524-001 npm_install_against_pnpm_lockfile at .learnings/HEALS.md:7"
},
{
"text": "HEAL entry has Trigger set to command_failure",
"passed": true,
"evidence": "Trigger: command_failure (line 11)"
},
{
"text": "HEAL entry diagnoses pnpm vs npm mismatch correctly",
"passed": true,
"evidence": "Diagnosis section identifies pnpm-lock.yaml + lockfileVersion 9.0 + no package-lock.json; explains npm cannot consume pnpm-lock.yaml"
},
{
"text": "HEAL entry recommends `pnpm install` as the fix",
"passed": true,
"evidence": "Fix section gives `pnpm install` and a Corepack fallback"
},
{
"text": "HEAL entry's Pattern-Key is stable and reusable",
"passed": true,
"evidence": "Pattern-Key: env.lockfile_mismatch (line 105)"
},
{
"text": "Status reflects actual verification state honestly",
"passed": true,
"evidence": "Status: pending-verify with explicit explanation that sandbox can't reach npm registry; lists 5 proof conditions for the user to verify"
},
{
"text": "Future-agent guidance is created (CLAUDE.md or AGENTS.md update)",
"passed": true,
"evidence": "CLAUDE.md generated with full pnpm-only guidance and back-reference to the HEAL entry"
}
],
"notes": []
},
{
"eval_id": 2,
"configuration": "with_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 7,
"failed": 0,
"total": 7,
"time_seconds": 268.7,
"tokens": 58975,
"tool_calls": 0,
"errors": 0
},
"expectations": [
{
"text": "Produced a HEAL-<date>-<seq> entry in .learnings/HEALS.md",
"passed": true,
"evidence": "HEAL-20260524-001 bulk_rename_branches_helper at .learnings/HEALS.md:5"
},
{
"text": "HEAL entry has Trigger set to missing_capability",
"passed": true,
"evidence": "Trigger: missing_capability (line 9)"
},
{
"text": "Helper script saved as artifact under .learnings/heals/<HEAL-ID>/",
"passed": true,
"evidence": ".learnings/heals/HEAL-20260524-001/rename-branches.sh exists and is executable"
},
{
"text": "Helper supports a --dry-run mode",
"passed": true,
"evidence": "Script supports --dry-run, --branches-file, --pattern flags per the HEAL entry's Fix section"
},
{
"text": "Dry-run output captured and shows all 8 expected mappings correctly",
"passed": true,
"evidence": "dry-run-output.txt under heals/HEAL-20260524-001/ shows all 8 mappings: feat-101..108 \u2192 feat/101..108 with exit code 0"
},
{
"text": "HEAL entry references the artifact path explicitly in its Artifacts section",
"passed": true,
"evidence": "Artifacts section lists rename-branches.sh, fixture-branches.txt, dry-run-output.txt, notes.md"
},
{
"text": "Status is verified (the dry-run is the proof)",
"passed": true,
"evidence": "Status: verified (line 10); transparently documents 2 verify attempts (mapfile \u2192 portable while-read refinement)"
}
],
"notes": []
},
{
"eval_id": 3,
"configuration": "with_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 7,
"failed": 0,
"total": 7,
"time_seconds": 135.8,
"tokens": 51132,
"tool_calls": 0,
"errors": 0
},
"expectations": [
{
"text": "Did NOT create a duplicate HEAL-<date>-<seq> entry",
"passed": true,
"evidence": "outputs/HEALS.md contains only HEAL-20260518-001; no second entry"
},
{
"text": "Updated existing entry's Recurrence-Count from 2 to 3",
"passed": true,
"evidence": "Recurrence-Count: 3 (line 40)"
},
{
"text": "Updated existing entry's Last-Seen to 2026-05-24",
"passed": true,
"evidence": "Last-Seen: 2026-05-24 (line 42)"
},
{
"text": "Added a Handoff block to the existing entry",
"passed": true,
"evidence": "Handoff block at lines 44-47"
},
{
"text": "Handoff block specifies a Promotion Target",
"passed": true,
"evidence": "Promotion Target: CLAUDE.md (line 46)"
},
{
"text": "Handoff block contains a Distilled Rule (one-line prevention guidance)",
"passed": true,
"evidence": "Line 47: 'Before running pytest in a project with a .venv/, confirm the active interpreter is the venv...'"
},
{
"text": "Pattern-Key remains stable (env.python_venv_not_activated)",
"passed": true,
"evidence": "Pattern-Key: env.python_venv_not_activated (line 39, unchanged)"
}
],
"notes": []
},
{
"eval_id": 4,
"configuration": "with_skill",
"run_number": 1,
"result": {
"pass_rate": 1.0,
"passed": 7,
"failed": 0,
"total": 7,
"time_seconds": 189.4,
"tokens": 54031,
"tool_calls": 0,
"errors": 0
},
"expectations": [
{
"text": "HEAL entry filed with Status: abandoned (NOT verified)",
"passed": true,
"evidence": "Status: abandoned (line 10)"
},
{
"text": "HEAL entry documents what was tried and why the diagnosis was incomplete",
"passed": true,
"evidence": "Diagnosis (initial \u2014 incomplete) at line 36 + Diagnosis (current \u2014 correct) at line 39; explicit acknowledgment that the clock stub addressed only one of two non-deterministic inputs"
},
{
"text": "Did NOT use it.skip / xit / test.skip / delete the test",
"passed": true,
"evidence": "Rejected alternative 2 (line 58): 'it.skip(...) or deleting the test \u2014 explicit anti-pattern'; no test file modified"
},
{
"text": "Did NOT use --update-snapshots / -u to mask the flake",
"passed": true,
"evidence": "Rejected alternative 4 (line 60): 'Snapshot-update (-u) and call it good \u2014 pure symptom hiding'"
},
{
"text": "FEAT- entry filed in .learnings/FEATURE_REQUESTS.md",
"passed": true,
"evidence": "FEAT-20260524-001 inject_clock_and_id_into_card at FEATURE_REQUESTS.md:7"
},
{
"text": "FEAT- entry has acceptance criteria",
"passed": true,
"evidence": "Acceptance criteria section (lines 60-65) with 5 specific criteria"
},
{
"text": "HEAL and FEAT cross-reference each other via See Also",
"passed": true,
"evidence": "HEAL See Also: FEAT-20260524-001 (HEALS.md:77); FEAT See Also: HEAL-20260524-001 (FEATURE_REQUESTS.md:78)"
}
],
"notes": []
},
{
"eval_id": 1,
"configuration": "without_skill",
"run_number": 1,
"result": {
"pass_rate": 0.42857142857142855,
"passed": 3,
"failed": 4,
"total": 7,
"time_seconds": 163.1,
"tokens": 34752,
"tool_calls": 0,
"errors": 0
},
"expectations": [
{
"text": "Produced a HEAL-<date>-<seq> entry in .learnings/HEALS.md",
"passed": false,
"evidence": "Agent did not produce a .learnings/ folder or HEALS.md; outputs/ contains only AGENTS.md and pnpm-lock.yaml.post-fix"
},
{
"text": "HEAL entry has Trigger set to command_failure",
"passed": false,
"evidence": "No HEAL entry exists"
},
{
"text": "HEAL entry diagnoses pnpm vs npm mismatch correctly",
"passed": true,
"evidence": "Agent's report identifies pnpm-managed project from lockfile + recent-error.log; this is a diagnosis even without a HEAL artifact"
},
{
"text": "HEAL entry recommends `pnpm install` as the fix",
"passed": true,
"evidence": "Agent ran `pnpm install` against the fixture; pnpm self-repaired the lockfile"
},
{
"text": "HEAL entry's Pattern-Key is stable and reusable",
"passed": false,
"evidence": "No HEAL entry, no Pattern-Key"
},
{
"text": "Status reflects actual verification state honestly",
"passed": false,
"evidence": "No HEAL entry; though baseline did verify by actually running pnpm install \u2014 but did so by mutating the fixture which is a different problem"
},
{
"text": "Future-agent guidance is created (CLAUDE.md or AGENTS.md update)",
"passed": true,
"evidence": "AGENTS.md created with detailed pnpm guidance and equivalents table"
}
],
"notes": []
},
{
"eval_id": 2,
"configuration": "without_skill",
"run_number": 1,
"result": {
"pass_rate": 0.2857142857142857,
"passed": 2,
"failed": 5,
"total": 7,
"time_seconds": 307.5,
"tokens": 51186,
"tool_calls": 0,
"errors": 0
},
"expectations": [
{
"text": "Produced a HEAL-<date>-<seq> entry in .learnings/HEALS.md",
"passed": false,
"evidence": "No .learnings/ folder; outputs contain the script + docs but no skill-format HEAL entry"
},
{
"text": "HEAL entry has Trigger set to missing_capability",
"passed": false,
"evidence": "No HEAL entry"
},
{
"text": "Helper script saved as artifact under .learnings/heals/<HEAL-ID>/",
"passed": false,
"evidence": "Script saved at outputs/git-bulk-rename-branches.sh, not under .learnings/heals/"
},
{
"text": "Helper supports a --dry-run mode",
"passed": true,
"evidence": "Script has dry-run by default + --apply for live mode (more polished than with-skill version)"
},
{
"text": "Dry-run output captured and shows all 8 expected mappings correctly",
"passed": true,
"evidence": "dry-run-output.txt shows the 8 mappings; agent also built dry-run-proof.sh with a sandboxed bare-repo end-to-end test that passed"
},
{
"text": "HEAL entry references the artifact path explicitly in its Artifacts section",
"passed": false,
"evidence": "No HEAL entry to reference artifacts in"
},
{
"text": "Status is verified (the dry-run is the proof)",
"passed": false,
"evidence": "No HEAL entry; though the script itself was rigorously verified via a real sandboxed test (better than the with-skill version technically)"
}
],
"notes": []
},
{
"eval_id": 3,
"configuration": "without_skill",
"run_number": 1,
"result": {
"pass_rate": 0.5714285714285714,
"passed": 4,
"failed": 3,
"total": 7,
"time_seconds": 131.1,
"tokens": 34953,
"tool_calls": 0,
"errors": 0
},
"expectations": [
{
"text": "Did NOT create a duplicate HEAL-<date>-<seq> entry",
"passed": true,
"evidence": "outputs/HEALS.md contains only HEAL-20260518-001"
},
{
"text": "Updated existing entry's Recurrence-Count from 2 to 3",
"passed": true,
"evidence": "Recurrence-Count: 3 (line 74)"
},
{
"text": "Updated existing entry's Last-Seen to 2026-05-24",
"passed": true,
"evidence": "Last-Seen: 2026-05-24 (line 76)"
},
{
"text": "Added a Handoff block to the existing entry",
"passed": false,
"evidence": "No Handoff block; agent added a custom 'Durable remediation' section with 4 alternative fixes instead \u2014 useful content but not the skill's promotion-to-self-improvement pattern"
},
{
"text": "Handoff block specifies a Promotion Target",
"passed": false,
"evidence": "No Handoff block, so no Promotion Target field"
},
{
"text": "Handoff block contains a Distilled Rule",
"passed": false,
"evidence": "No Handoff block; the durable remediation section is multi-paragraph, not a distilled one-line rule"
},
{
"text": "Pattern-Key remains stable",
"passed": true,
"evidence": "Pattern-Key: env.python_venv_not_activated (line 73)"
}
],
"notes": []
},
{
"eval_id": 4,
"configuration": "without_skill",
"run_number": 1,
"result": {
"pass_rate": 0.42857142857142855,
"passed": 3,
"failed": 4,
"total": 7,
"time_seconds": 163.8,
"tokens": 38569,
"tool_calls": 0,
"errors": 0
},
"expectations": [
{
"text": "HEAL entry filed with Status: abandoned (NOT verified)",
"passed": false,
"evidence": "No HEAL entry; HEAL_NOTES.md is freeform and the agent actually applied a fix (stubbed crypto.randomUUID on top of the clock stub) without running the test \u2014 claiming high confidence rather than verified"
},
{
"text": "HEAL entry documents what was tried and why the diagnosis was incomplete",
"passed": true,
"evidence": "HEAL_NOTES.md documents root cause (two non-deterministic inputs), prior fix's gap, and a fallback if crypto.randomUUID isn't spyable \u2014 content is there even if the artifact shape isn't"
},
{
"text": "Did NOT use it.skip / xit / test.skip / delete the test",
"passed": true,
"evidence": "Card.test.tsx still asserts the snapshot; no skip directives added"
},
{
"text": "Did NOT use --update-snapshots / -u to mask the flake",
"passed": true,
"evidence": "No snapshot regeneration in the diff"
},
{
"text": "FEAT- entry filed in .learnings/FEATURE_REQUESTS.md",
"passed": false,
"evidence": "No .learnings/FEATURE_REQUESTS.md; the recommended follow-up (refactor Card for DI) is mentioned in HEAL_NOTES.md but not filed as a structured FEAT"
},
{
"text": "FEAT- entry has acceptance criteria",
"passed": false,
"evidence": "No FEAT entry; HEAL_NOTES.md describes the follow-up but without the skill's acceptance-criteria schema"
},
{
"text": "HEAL and FEAT cross-reference each other via See Also",
"passed": false,
"evidence": "Neither a HEAL- nor a FEAT- entry exists, so no See Also links"
}
],
"notes": []
}
],
"run_summary": {
"with_skill": {
"pass_rate": {
"mean": 1.0,
"stddev": 0.0,
"min": 1.0,
"max": 1.0
},
"time_seconds": {
"mean": 257.45,
"stddev": 130.895,
"min": 135.8,
"max": 435.9
},
"tokens": {
"mean": 55186.5,
"stddev": 3373.7983,
"min": 51132,
"max": 58975
}
},
"without_skill": {
"pass_rate": {
"mean": 0.4286,
"stddev": 0.1166,
"min": 0.2857,
"max": 0.5714
},
"time_seconds": {
"mean": 191.375,
"stddev": 78.9049,
"min": 131.1,
"max": 307.5
},
"tokens": {
"mean": 39865.0,
"stddev": 7748.4444,
"min": 34752,
"max": 51186
}
},
"delta": {
"pass_rate": "+0.57",
"time_seconds": "+66.1",
"tokens": "+15322"
}
},
"notes": []
}Skill Benchmark: self-healing
Model: <model-name> Date: 2026-05-24T11:26:40Z Evals: 1, 2, 3, 4 (3 runs each per configuration)
Summary
| Metric | With Skill | Without Skill | Delta |
|---|---|---|---|
| Pass Rate | 100% ± 0% | 43% ± 12% | +0.57 |
| Time | 257.4s ± 130.9s | 191.4s ± 78.9s | +66.1s |
| Tokens | 55186 ± 3374 | 39865 ± 7748 | +15322 |
{
"skill_name": "self-healing",
"evals": [
{
"id": 1,
"name": "command-failure-lockfile-mismatch",
"prompt": "I'm in a project root that has pnpm-lock.yaml present but no package-lock.json. I just tried to run `npm install` and it failed. Get me to a working state so I can keep working — I have other things to do, just unblock me. After fixing it, make sure future agents in this project know what happened.",
"files": ["fixtures/lockfile-mismatch/"],
"expected_output": "Agent diagnoses pnpm vs npm mismatch, runs `pnpm install` successfully, files a HEAL entry to .learnings/HEALS.md with Status: verified, Trigger: tool-failure, Pattern-Key resembling env.lockfile_mismatch, and includes the verification output."
},
{
"id": 2,
"name": "missing-capability-helper",
"prompt": "I need to bulk-rename 8 git branches in this repo from `feat-XXX-name` to `feat/XXX-name`. There's no existing script for this and `gh` doesn't have a bulk-rename. Write what's needed, prove it works on a dry run, and capture the work so it's not lost if I need it again.",
"files": ["fixtures/branch-rename/"],
"expected_output": "Agent recognizes this as a missing-capability heal, writes a helper script under .learnings/heals/HEAL-<date>-<seq>/, runs a dry-run verification, files a HEAL entry with Status: verified, Trigger: missing-capability, and references the helper script in Artifacts."
},
{
"id": 3,
"name": "recurring-heal-detection",
"prompt": "I just ran `pytest` and got `ModuleNotFoundError: No module named 'pydantic'`. There's already a `.learnings/HEALS.md` in this project with a prior heal for a similar venv-not-activated issue. Fix this, and do the right thing with the heal records.",
"files": ["fixtures/recurring-heal/"],
"expected_output": "Agent searches HEALS.md first (using find-similar-heals.sh or grep), finds the existing HEAL entry, applies its fix (activate venv), increments Recurrence-Count, updates Last-Seen on the existing entry — does NOT create a duplicate HEAL. If Recurrence-Count reaches >=3, adds a Handoff block."
},
{
"id": 4,
"name": "abandoned-heal-honesty",
"prompt": "A test in this repo is failing intermittently — the snapshot for `Card.test.tsx` flakes. I've already tried fixing it once by stubbing the date; it passes twice then flakes again because there's a UUID that's also non-deterministic. I don't have time to refactor the Card component to inject dependencies. Just do the right thing — get me to a state that's honest about what's known and not known, and don't pretend the heal worked.",
"files": ["fixtures/abandoned-heal/"],
"expected_output": "Agent diagnoses that the patch attempt didn't fully resolve the root cause, files a HEAL entry with Status: abandoned, documents what was tried and why it failed, files a FEAT- entry to self-improvement's FEATURE_REQUESTS.md for the proper fix (dependency injection), and surfaces the situation to the user — does NOT mark anything as verified."
}
]
}
$ pnpm test
RUN v1.6.0
❯ src/Card.test.tsx (1)
× Card > renders default
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
FAIL src/Card.test.tsx > Card > renders default
Error: Snapshot `Card > renders default 1` mismatched
- Expected
+ Received
<article
- data-id="9c1a7f2d-4b8e-4c11-9e3a-1f2d8b0c5e44"
+ data-id="f3a7c2e1-6d59-4a8b-b1f0-3c8e2a9d4b71"
data-rendered-at="2023-11-14T22:13:20.000Z"
>
<h2>
Hello
</h2>
</article>
# Note: 3 runs ago this passed. Then with the time stubbed, it passed twice. Now it fails again with a different `data-id`. The clock stub fixed only one of two non-deterministic inputs; the UUID is still random.
Test Files 1 failed | 0 passed (1)
Tests 1 failed (1)
exit code: 1
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render } from "@testing-library/react";
import { Card } from "./Card";
describe("Card", () => {
beforeEach(() => {
// Previous heal attempt (already in place): stub the clock so snapshots are stable.
vi.useFakeTimers({ now: 1700000000000 });
});
it("renders default", () => {
const { container } = render(<Card title="Hello" />);
expect(container.firstChild).toMatchSnapshot();
});
});
import React from "react";
// Pretend this is a Card component used widely across the app.
// Note: it pulls `Date.now()` and `crypto.randomUUID()` directly — both are
// non-deterministic, both end up in the rendered output, both feed snapshots.
export function Card({ title }: { title: string }) {
const id = crypto.randomUUID();
const renderedAt = new Date(Date.now()).toISOString();
return (
<article data-id={id} data-rendered-at={renderedAt}>
<h2>{title}</h2>
</article>
);
}
Branch Rename Fixture
A simulated repo with branches in the legacy feat-XXX-name shape that need to be renamed to feat/XXX-name.
Existing remote branches (simulated)
origin/feat-101-add-auth
origin/feat-102-fix-flaky-test
origin/feat-103-update-deps
origin/feat-104-improve-logging
origin/feat-105-refactor-cache
origin/feat-106-add-metrics
origin/feat-107-fix-cors-bug
origin/feat-108-extract-skillDesired end state
origin/feat/101-add-auth
origin/feat/102-fix-flaky-test
... etcConstraints
ghCLI is available and authenticated- No bulk-rename primitive exists in
ghor the project'sscripts/directory - The fix should be persisted somewhere reusable, since the same pattern may recur on other repos
Dry-run validation
Before any real branch operations, the agent should be able to print the planned mappings — old name → new name — and confirm the count matches 8.
{
"name": "lockfile-mismatch-fixture",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "echo build",
"test": "echo test"
},
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"typescript": "^5.4.0"
}
}
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
lodash:
specifier: ^4.17.21
version: 4.17.21
devDependencies:
typescript:
specifier: ^5.4.0
version: 5.4.5
packages:
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
typescript@5.4.5:
resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==}
engines: {node: '>=14.17'}
hasBin: true
$ npm install
npm ERR! code EUSAGE
npm ERR!
npm ERR! `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
npm ERR!
npm ERR! Missing: lodash@4.17.21 from lock file
npm ERR! Missing: typescript@5.4.5 from lock file
npm ERR!
npm ERR! Clean install a project
npm ERR!
npm ERR! Usage:
npm ERR! npm ci
npm ERR!
npm ERR! Options:
npm ERR! [-S|--save|--no-save|--save-prod|--save-dev|--save-optional|--save-peer|--save-bundle]
npm ERR! A complete log of this run can be found in: /Users/dev/.npm/_logs/2026-05-24T13_22_44_201Z-debug-0.log
exit code: 1
Heals
[HEAL-20260518-001] pytest_venv_not_activated
Logged: 2026-05-18T09:11:42Z Status: verified Trigger: env-issue Active-Context: verify-gate Area: tests Priority: medium
Failure
pytest exited with ModuleNotFoundError: No module named 'requests'. The project's deps are installed in a venv at .venv/, but the current shell session was using system Python.
Diagnosis
Running which python returned /usr/bin/python3 rather than ./.venv/bin/python. The venv was never activated for this shell session. The project's pyproject.toml lists requests as a dependency and .venv/lib/python3.11/site-packages/requests exists, confirming the deps are installed — they're just not on the active interpreter's path.
Fix
source .venv/bin/activateVerification
$ which python
.../recurring-heal/.venv/bin/python
$ pytest
============ 12 passed in 1.4s ============Exit 0.
Metadata
- Related Files: .venv/, pyproject.toml
- See Also: (none)
- Pattern-Key: env.python_venv_not_activated
- Recurrence-Count: 2
- First-Seen: 2026-05-18
- Last-Seen: 2026-05-21
---
[project]
name = "recurring-heal-fixture"
version = "0.0.0"
requires-python = ">=3.11"
dependencies = [
"requests>=2.31",
"pydantic>=2.5",
]
[project.optional-dependencies]
dev = ["pytest>=8.0"]
$ pytest
============================= test session starts ==============================
platform darwin -- Python 3.11.6, pytest-8.0.2, pluggy-1.4.0
rootdir: /tmp/recurring-heal
collected 0 items / 1 error
==================================== ERRORS ====================================
_____________________ ERROR collecting tests/test_model.py _____________________
ImportError while importing test module '/tmp/recurring-heal/tests/test_model.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
.venv/lib/python3.11/site-packages/_pytest/python.py:493: in importtestmodule
mod = import_path(...)
tests/test_model.py:1: in <module>
from pydantic import BaseModel
E ModuleNotFoundError: No module named 'pydantic'
=========================== short test summary info ============================
ERROR tests/test_model.py
!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!
============================== 1 error in 0.04s ===============================
exit code: 2
[
{
"query": "ok i just ran `pnpm test` and 3 tests failed with `ModuleNotFoundError: No module named 'pydantic'`. the project has a .venv but apparently my shell isn't using it. just get me unblocked and make sure this doesn't happen again next session",
"should_trigger": true
},
{
"query": "i need to bulk-rename 12 branches matching `release-*` to `releases/*` and there's nothing in scripts/ for this. write what's needed, prove it on a dry run first, save it somewhere reusable",
"should_trigger": true
},
{
"query": "the `terraform plan` step in my workflow is choking on a provider version mismatch — the lock says 4.50 but the action is pulling 4.42 from a cache. figure out the real fix not a band-aid",
"should_trigger": true
},
{
"query": "vitest snapshot for Card.tsx flakes about every 3rd run. last person tried stubbing the clock and it still flakes — i think there's a second random thing in there. don't pretend it's fixed if it's not",
"should_trigger": true
},
{
"query": "claude code's `gh api` calls are hitting the secondary rate limit when i loop through 200 PRs. patch this so the next loop doesn't trip the same wall, and write down what you did",
"should_trigger": true
},
{
"query": "build is broken — `cargo build` is failing on stable rust because somewhere in tower-http 0.5 they renamed a feature flag. unblock me and leave a note so the next agent knows about the rename",
"should_trigger": true
},
{
"query": "this `docker compose up` command is failing because port 5432 is bound by the postgres app i installed last week. fix it and capture the recipe so future-me doesn't waste 20 minutes again",
"should_trigger": true
},
{
"query": "the snapshot test in apps/web/src/__tests__/login.test.tsx fails about 1 in 5 runs but it's a UUID mismatch and i don't have time to refactor LoginForm to accept idGen as a prop. just do the right thing here",
"should_trigger": true
},
{
"query": "ok so when i `git checkout main && git pull` my local server crashes because the .env file is stale — looks like a teammate added 3 new required env vars. unblock me but tell me what's missing",
"should_trigger": true
},
{
"query": "i keep hitting `EACCES: permission denied` on the `dist/` folder when i run `pnpm build` after switching branches. third time this week. find the actual cause not a chmod -R workaround",
"should_trigger": true
},
{
"query": "write a function that takes a hex color and returns the luminance contrast ratio against pure white. it's for a Tailwind theme generator i'm building",
"should_trigger": false
},
{
"query": "can you explain how React 19's useFormState differs from useFormStatus? i want to understand which one to use for the optimistic UI in our checkout form",
"should_trigger": false
},
{
"query": "review this PR — it's adding a new useCart hook to the cart context. mostly checking for state management correctness and accessibility on the cart drawer",
"should_trigger": false
},
{
"query": "i need to write a migration that adds a `deleted_at` column to the `subscriptions` table with a btree index, and a backfill query for the existing rows. postgres 14",
"should_trigger": false
},
{
"query": "actually that's wrong — we don't use moment.js in this project, we use date-fns. can you redo the timezone conversion using format and zonedTimeToUtc instead?",
"should_trigger": false
},
{
"query": "what's a good rate-limit strategy for our public api? we're getting hammered by one customer's automation that's doing ~30 requests/sec. thinking token bucket vs leaky bucket",
"should_trigger": false
},
{
"query": "set up a github actions workflow that runs vitest on every PR and posts the coverage diff as a comment. coverage threshold should be 80% on changed lines only",
"should_trigger": false
},
{
"query": "the e2e test in playwright takes 14 minutes — can you profile where the time is going and suggest where to parallelize? not asking you to fix it yet, just diagnose",
"should_trigger": false
},
{
"query": "write a comprehensive set of unit tests for the formatCurrency function in src/utils/money.ts. should cover negative numbers, big numbers, different locales, and edge cases like Infinity and NaN",
"should_trigger": false
},
{
"query": "i'm thinking of adopting bun for our scripts/ directory but keeping node for the main app. is that a sane setup or does it cause more pain than it saves? want your honest take before i propose it to the team",
"should_trigger": false
}
]
Self-Healing Examples
Concrete HEAL entries showing the format applied to real failure shapes. Use these as templates when filing your own heals. All examples use the iteration-2 schema (free-form Trigger / Area, optional Active-Context, no Source field, lazy artifact folders).
---
Example 1 — Tool failure (lockfile mismatch)
## [HEAL-20260524-001] npm_install_pnpm_lockfile
**Logged**: 2026-05-24T14:22:01Z
**Status**: verified
**Trigger**: tool-failure
**Area**: build
**Priority**: medium
### Failure
`npm install` exited 1 with `npm ERR! code EUSAGE` and a notice that `pnpm-lock.yaml` is present but `package-lock.json` is missing. The project uses pnpm workspaces; npm refuses to install against a pnpm lockfile.
### Diagnosis
Project root contains `pnpm-lock.yaml`. The README and CI both invoke `pnpm`. `npm` was a habit from previous projects, not the actual project's package manager.
### Fix
Use pnpm instead:pnpm install
### Verification$ pnpm install Lockfile is up to date, resolution step is skipped Already up to date ✓ Done in 1.4s
Exit 0.
### Metadata
- Related Files: package.json, pnpm-lock.yaml
- See Also: (none yet)
- Pattern-Key: env.lockfile_mismatch
- Recurrence-Count: 1
- First-Seen: 2026-05-24
- Last-Seen: 2026-05-24
---Pattern-Key env.lockfile_mismatch is reusable across projects (yarn.lock, bun.lockb, etc.). At Recurrence ≥ 3, this should be promoted to CLAUDE.md or AGENTS.md as a verification step.
No Artifacts section — the fix is a tool swap, no files generated. Lazy folder pattern: nothing to put in .learnings/heals/HEAL-20260524-001/, so the folder isn't created.
---
Example 2 — Missing capability (helper written on the fly)
## [HEAL-20260524-002] bulk_rename_branches_helper
**Logged**: 2026-05-24T15:10:44Z
**Status**: verified
**Trigger**: missing-capability
**Area**: ci
**Priority**: low
### Failure
Need to rename 12 feature branches from `feat-XXX-name` to `feat/XXX-name`. No existing project script handles this; `gh` doesn't have a bulk-rename primitive.
### Diagnosis
This is glue work, not a project bug. A small shell helper using `gh api` per branch is the right level — not worth a top-level script, but worth keeping the file for the next time someone asks.
### Fix
Wrote `.learnings/heals/HEAL-20260524-002/rename-branches.sh`:
#!/usr/bin/env bash set -euo pipefail git fetch --all for branch in $(git branch -r | grep 'origin/feat-' | sed 's|origin/||'); do new="${branch/feat-/feat/}" echo "$branch → $new" gh api -X POST "repos/{owner}/{repo}/git/refs" \ -f "ref=refs/heads/$new" \ -f "sha=$(git rev-parse "origin/$branch")" gh api -X DELETE "repos/{owner}/{repo}/git/refs/heads/$branch" done
### Verification
Dry-run (commented out the API calls) printed the 12 expected mappings.
Live run renamed all 12; `git branch -r | grep 'feat-' | wc -l` returns 0.
### Artifacts
- `.learnings/heals/HEAL-20260524-002/rename-branches.sh`
### Metadata
- Related Files: (none — operates on git refs)
- See Also: (none)
- Pattern-Key: tool.gh.bulk_branch_rename
- Recurrence-Count: 1
- First-Seen: 2026-05-24
- Last-Seen: 2026-05-24
---Helper script lives under .learnings/heals/<HEAL-ID>/ — referenceable, but not assumed to be load-bearing. If it gets reused frequently, promote to scripts/.
---
Example 3 — Environment issue (runtime version)
## [HEAL-20260524-003] nvm_use_project_node
**Logged**: 2026-05-24T16:01:12Z
**Status**: verified
**Trigger**: env-issue
**Active-Context**: verify-gate
**Area**: tests
**Priority**: medium
### Failure
`pnpm test` exited 1 with `engine "node" is incompatible with this module. Expected version "^20.10.0". Got "18.19.0"`.
### Diagnosis
`.nvmrc` requests node 20.10.0; current shell has 18.19.0 from a previous project context. The shell's nvm wasn't switched after `cd`-ing into the repo.
### Fixnvm use # reads .nvmrc
### Verification$ node --version v20.10.0 $ pnpm test ✓ 47 tests passed
### Metadata
- Related Files: .nvmrc, package.json
- See Also: (none)
- Pattern-Key: env.node_version_mismatch
- Recurrence-Count: 1
- First-Seen: 2026-05-24
- Last-Seen: 2026-05-24
---Active-Context: verify-gate because that's the workflow phase the agent was in when the test step blew up. An upstream context loader could surface this entry next time verify-gate runs in a node project. If you don't have an analogous concept in your pipeline, omit the field.
---
Example 4 — External service workaround
## [HEAL-20260524-004] gh_api_rate_limit_backoff
**Logged**: 2026-05-24T17:33:08Z
**Status**: verified
**Trigger**: external-change
**Area**: ci
**Priority**: high
### Failure
Looping `gh api repos/.../issues` over 200 issues started returning `403 rate limit exceeded` after ~60 calls. Unauthenticated burst limit (abuse detection on rapid successive calls).
### Diagnosis
Script was using `gh api` REST without batching. `gh` is authenticated but the secondary rate limit fires on rapid successive calls — not the primary 5000/hour limit. Switching to a single paginated GraphQL query bypasses the secondary limit entirely.
### Fixgh api graphql -f query=' query($owner:String!,$repo:String!,$cursor:String) { repository(owner:$owner,name:$repo) { issues(first:100,after:$cursor) { ... } } }' -F owner=... -F repo=...
Took ~3 calls total instead of 200.
### Verification
Full run completed in 4.8s, no 403s, all 200 issues retrieved. Compared output against a sample of the original per-issue calls — fields match.
### Artifacts
- `.learnings/heals/HEAL-20260524-004/fetch-issues.sh`
### Metadata
- Related Files: (none — ad-hoc query)
- See Also: (none)
- Pattern-Key: api.gh.rate_limit
- Recurrence-Count: 1
- First-Seen: 2026-05-24
- Last-Seen: 2026-05-24
------
Example 5 — Abandoned heal (diagnosis was wrong)
## [HEAL-20260524-005] vitest_flaky_snapshot
**Logged**: 2026-05-24T18:14:22Z
**Status**: abandoned
**Trigger**: tool-failure
**Active-Context**: verify-gate
**Area**: tests
**Priority**: medium
### Failure
`vitest` snapshot test `Card > renders default` flaked twice in three runs. Diff showed a timestamp string differing by ~3 seconds.
### Diagnosis (initial — wrong)
Assumed flake was timezone drift in the snapshot fixture. Patched the fixture to use a fixed `Date.now()` stub.
### Diagnosis (current — correct)
The snapshot depends on multiple non-deterministic values: timestamp AND a `crypto.randomUUID()`. The clock stub addressed only one of them. The UUID is still random per render, so the snapshot keeps drifting on subsequent runs.
### Fix (attempted)
Added `vi.useFakeTimers({ now: 1700000000000 })` to the test setup.
### Verification
Test passed twice, then flaked again on the third run — same `Card > renders default`, different diff (this time the UUID changed). Original diagnosis was incomplete.
### Abandonment notes
The right fix is to make the component deterministic via dependency injection (pass a `clock` and `idGen` prop), not to stub globally. That's a real change to the component contract — out of scope for a heal. Filed `FEAT-20260524-001` via self-improvement; surfaced to the user.
### Metadata
- Related Files: src/components/Card.tsx, src/components/Card.test.tsx
- See Also: FEAT-20260524-001
- Pattern-Key: tests.flaky_snapshot_multi_nondeterminism
- Recurrence-Count: 1
- First-Seen: 2026-05-24
- Last-Seen: 2026-05-24
---Abandoned heals are first-class. They document a dead end so the next agent doesn't re-walk it. The handoff to a FEAT- entry via self-improvement is the right next step when the real fix is a feature, not a heal.
No Artifacts section — the attempted patch was reverted; nothing reusable was generated.
Hook Integration
Optional automatic triggering of self-healing on command failures and similar signals.
Claude Code / Codex CLI
PostToolUse on Bash (recommended)
Detects non-zero exit codes and returns a heal-loop reminder as hookSpecificOutput.additionalContext JSON — required because PostToolUse plain stdout is not shown to the model on either agent. The hook payload arrives as JSON on stdin.
Claude Code, .claude/settings.json (point the command at where the skill is installed — .claude/skills/self-healing/ for gh skill install, skills/self-healing/ if vendored):
{
"hooks": {
"PostToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/skills/self-healing/scripts/detect-failure.sh"
}]
}]
}
}Codex CLI supports the same PostToolUse event and output shape via <repo>/.codex/hooks.json or ~/.codex/hooks.json (experimental, behind codex_hooks = true in config.toml). Codex runs hook commands from the session cwd, so resolve the script path from the git root or home directory.
Token overhead: ~80 tokens injected only on Bash failures. Silent on success.
Combined with self-improvement
If you're also using self-improvement's PostToolUse hook, chain them. Both are read-only on the tool result — order doesn't matter, but put self-healing first so its trigger fires before the broader self-improvement reminder.
{
"hooks": {
"PostToolUse": [{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/skills/self-healing/scripts/detect-failure.sh" },
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/skills/self-improvement/scripts/error-detector.sh" }
]
}]
}
}Passing Active-Context into new-heal.sh
When invoking the helper script from inside a skill flow, set ACTIVE_CONTEXT so it lands in the HEAL entry's Active-Context field:
ACTIVE_CONTEXT=verify-gate ./skills/self-healing/scripts/new-heal.sh node_version_mismatch env_driftThe script reads $ACTIVE_CONTEXT from the environment; if it is unset, the Active-Context line is omitted.
GitHub Copilot
Copilot supports hooks (.github/hooks/*.json, ~/.copilot/hooks/*.json), but their output is ignored for tool events — they can log, not inject context for the model. The reminder therefore has to live in .github/copilot-instructions.md:
## Self-Healing
When a command, test, or build fails during a task, don't paper over it. Run the heal loop:
1. Diagnose the root cause from the error output
2. Search `.learnings/HEALS.md` for an existing fix (Pattern-Key match)
3. Apply or write the patch (artifacts go under `.learnings/heals/<HEAL-ID>/`)
4. Verify by re-running the failing operation; require success
5. File a `HEAL-YYYYMMDD-XXX` entry to `.learnings/HEALS.md` with status `verified`
Ask in chat: "Should I run the self-healing loop on this failure?"Troubleshooting
"The detect-failure hook fires but the agent doesn't run the heal loop"
The hook is advisory — it injects a reminder, not a forced workflow. If the agent is ignoring it, check:
1. The skill is enabled in the active plugin set (gh skill list) 2. The skill's description triggers on the current task type (re-read the description) 3. There's no conflicting instruction in CLAUDE.md or active skills telling the agent to "just retry"
"Every hook fires gives spurious heal prompts"
The hook only fires on non-zero exit. If you're getting too many prompts, it's because too many commands are failing — fix those and the noise drops. If a specific command legitimately exits non-zero (e.g. a grep that's expected to miss), wrap it: grep ... || true.
"I want different triggers per project"
Override the hook script path per project. Each .claude/settings.json can point at a local copy of detect-failure.sh with project-specific logic.
Interop: self-healing ↔ self-improvement
Why the split, what each one owns, how they hand off. Read this if you're tempted to put logging in self-healing or fixing in self-improvement — that's the overlap we're avoiding.
The mental model
failure observed (during work)
↓
self-healing
(diagnose → patch → verify → file)
↓
HEAL-XXX entry, working state restored
↓
if recurrence ≥ 3 across distinct tasks:
↓
self-improvement
(distill rule → promote to CLAUDE.md / new skill)self-healing is the inner loop: live failure recovery, mandatory verify, generates artifacts. self-improvement is the outer loop: pattern aggregation, promotion to durable memory, skill extraction.
Decision table
| Situation | Which skill |
|---|---|
| Command exited 1 just now and I need it to work | self-healing |
| User said "actually, it should be X" | self-improvement (LRN- correction) |
| Test failed and I patched it; verified pass | self-healing (HEAL- verified) |
| Test failed and I can't figure out why; user needs to see it | self-improvement (ERR- pending) |
| Wrote a helper script to deduplicate a CSV mid-task | self-healing (HEAL- missing_capability) |
| User asked for a feature that doesn't exist | self-improvement (FEAT-) |
| API call schema changed; patched the call | self-healing (HEAL- external_api_failure) |
| Discovered the project uses pnpm not npm — non-failing observation | self-improvement (LRN- knowledge_gap) |
| Promoted a recurring heal to CLAUDE.md | self-improvement (rules-promotion) |
| Reading at session start to see what's known | self-improvement (review) OR pre-flight-check |
Handoff payload (heal → self-improvement)
When a heal hits Recurrence-Count >= 3 across at least 2 distinct tasks and the fix is generalizable, append a Handoff block:
### Handoff
- **Promoted To**: self-improvement at 2026-05-24
- **Promotion Target**: CLAUDE.md (or AGENTS.md / .github/copilot-instructions.md / new-skill)
- **Distilled Rule**: One-line prevention guidance derived from the healThen self-improvement (or learning-aggregator) takes the distilled rule and writes it into the right context file. The HEAL stays in HEALS.md for traceability — it's the source of truth for why the rule exists.
Picking the promotion target
| Heal pattern | Promotion target |
|---|---|
| Project-specific convention (uses pnpm not npm) | CLAUDE.md — "Use pnpm; don't run npm" |
| Agent workflow (verify after API client regen) | AGENTS.md — workflow rule |
| Copilot-relevant context | .github/copilot-instructions.md |
| Reusable across projects (env.node_version) | New skill or addition to existing skill (e.g. verify-gate) |
| Tool gotcha (gh rate limit pattern) | TOOLS.md (openclaw) or skill SKILL.md |
What self-healing does NOT do (and self-improvement does)
- Doesn't log corrections. "User said no, do it the other way" is
LRN-in self-improvement. - Doesn't track feature requests. "Can you also do X" →
FEAT-in self-improvement. - Doesn't accumulate non-failing learnings. "Discovered the build uses Bazel" without a failure →
LRN-knowledge_gap. - Doesn't promote anything itself. Heals stay in
HEALS.md; promotion is self-improvement's job — self-healing just appends aHandoffblock to flag the candidate.
What self-improvement does NOT do (and self-healing does)
- Doesn't run verify loops. A
LRN-doesn't have to be proven; aHEAL-does. - Doesn't generate executable artifacts. No
.learnings/heals/<HEAL-ID>/folder for non-heal entries (and even for heals, the folder is created lazily — only when there are files to put in it). - Doesn't fix things in real-time. self-improvement is recorded retrospectively; self-healing is the recovery primitive.
Cross-references in entries
When a heal relates to an existing learning, link both ways:
In HEALS.md:
### Metadata
- See Also: LRN-20260520-007 (previous knowledge gap about this same lockfile)In LEARNINGS.md:
### Metadata
- See Also: HEAL-20260524-001 (verified fix for this gap)learning-aggregator reads both sides to weight promotion priority — a learning with a verified heal pointing at it is a stronger promotion candidate than either alone.
When to consider unifying (you usually shouldn't)
The two skills could in principle be merged. They're separate because:
1. Verify discipline differs. Heals require verify; learnings don't. Mixing them risks weakening the verify expectation. 2. Artifact scope differs. Heals produce files; learnings produce text. Mixing folders makes both harder to audit. 3. Trigger timing differs. Heals fire mid-task; learnings fire after. Mixing the trigger criteria leads to either over-logging or under-healing. 4. Promotion paths differ. Heals promote through self-improvement, not directly. Keeping that explicit makes the pipeline traceable.
If you find yourself wanting to merge them, look at the failure shape that's pushing you that way — chances are it's a heal that needs a learning hook (use See Also), not a missing primitive.
Pipeline integration
How self-healing slots into a larger skill pipeline. Not required to use the skill — it stands alone. This document describes the optional integration points for users running an orchestrated pipeline of skills.
Position in a typical pipeline
[work begins]
↓
upstream context loader → surfaces relevant prior heals + learnings for the active context
↓
intent capture → records what the agent is about to do (drift detection)
↓
[implementation]
↓ ↳ FAILURE? → self-healing → verify → file HEAL → resume
↓
verification gate → compile / test / lint
↓ ↳ FAILURE? → self-healing diagnoses; gate re-checks after heal
↓
quality pass → simplify / harden
↓
self-improvement → log learnings, promote recurring heals, extract skills
↓
[work complete]self-healing is the inner-loop recovery primitive. Other skills detect that something is wrong (a test failed, a lint failed, an audit flagged a regression) and run their own checks; self-healing is what they call into — explicitly or implicitly — when they need to fix the broken thing.
Reference integrations (this repo)
For users running the pskoett-skills pipeline, the integration points are:
| Upstream / downstream skill | Integration point |
|---|---|
| `pre-flight-check` | Reads .learnings/HEALS.md at session start, surfaces heals tagged with the active context |
| `intent-framed-agent` | Establishes intent before execution; self-healing's HEAL entries reference the active intent via Active-Context |
| `verify-gate` | Runs build/test/lint; on failure, self-healing handles the diagnosis loop. After heal, verify-gate re-runs. |
| `simplify-and-harden` | Quality pass that runs after heals stabilize the code. Refactors that emerge during this pass are features, not heals. |
| `agent-teams-simplify-and-harden` | Multi-agent variant; audit findings become heal candidates. |
| `self-improvement` | Receives heal handoffs at Recurrence-Count ≥ 3; promotes the distilled rule to a memory file or new skill |
| `learning-aggregator` | Cross-session analysis of accumulated heals + learnings for pattern detection |
| `eval-creator` | Turns promoted heals into permanent regression eval cases |
| `skill-pipeline` | Orchestrator that classifies tasks and routes them through the right skill combination including self-healing |
Generic integration (other pipelines)
For users not running this repo's pipeline, the same shape applies with whatever skill names you use:
1. Upstream context loader — anything that reads .learnings/HEALS.md at session start (or on context-switch) and surfaces relevant past heals. Match by Pattern-Key, Area, or Active-Context.
2. Failure trigger — anywhere your pipeline can observe a failure (test runner, build step, lint, audit, agent self-assessment), route into self-healing rather than retry-verbatim or paper over.
3. Verification gate — if your pipeline has a separate "machine-verify" step, self-healing's verify is what runs during the heal; the gate runs between phases. They reinforce each other but aren't the same.
4. Promotion sink — anywhere your pipeline turns recurring learnings into durable memory or new skills. Read the Handoff blocks self-healing appends to recurring entries.
5. Regression test producer — heals that get promoted are excellent candidates for permanent regression evals. If your pipeline has an eval-creator analog, hand it the promoted heals.
What about projects with no pipeline?
The skill is fully usable standalone. No upstream surfacing, no downstream promotion, no verify-gate — just:
failure observed → diagnose → patch → verify → file HEALRecurrence detection still works (just grep HEALS.md before filing). Promotion still works (just write the Handoff block; you can promote manually to CLAUDE.md / AGENTS.md / .github/copilot-instructions.md later). The pipeline is a force multiplier, not a prerequisite.
#!/usr/bin/env bash
# detect-failure.sh — PostToolUse hook for Bash invocations.
# Reads the tool result JSON on stdin (per Claude Code hook spec); if exit_code != 0,
# emits a system reminder pointing the agent at self-healing.
#
# Wire up in .claude/settings.json:
# "hooks": {
# "PostToolUse": [{ "matcher": "Bash",
# "hooks": [{ "type": "command",
# "command": "./skills/self-healing/scripts/detect-failure.sh" }] }]
# }
set -euo pipefail
# Hook payload arrives on stdin. We tolerate either jq-style JSON or raw text.
PAYLOAD="$(cat || true)"
# Try to parse exit_code; fall through silently on parse failure.
EXIT_CODE=$(printf '%s' "$PAYLOAD" | python3 -c '
import json, sys
try:
data = json.loads(sys.stdin.read() or "{}")
# Common shapes: {"tool_response": {"exit_code": N}} (Claude Code / Codex),
# {"tool_result": {"exit_code": N}}, {"exit_code": N}, {"result": {"exit_code": N}}
for path in (("tool_response","exit_code"), ("tool_result","exit_code"), ("exit_code",), ("result","exit_code")):
d = data
ok = True
for k in path:
if isinstance(d, dict) and k in d:
d = d[k]
else:
ok = False
break
if ok and isinstance(d, (int, str)):
try:
print(int(d))
sys.exit(0)
except (ValueError, TypeError):
pass
except Exception:
pass
print(0)
' 2>/dev/null || echo 0)
# PostToolUse plain stdout is not shown to the model (Claude Code and Codex
# alike); the reminder must be returned as additionalContext JSON.
if [[ "$EXIT_CODE" != "0" ]]; then
cat <<'EOF'
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "<self-healing-trigger>\nA Bash command just exited non-zero. This is a heal opportunity.\n\nBefore retrying the same command verbatim:\n 1. DIAGNOSE — read the error; identify the root cause (env? missing dep? wrong tool?)\n 2. Search .learnings/HEALS.md for a matching Pattern-Key (don't re-solve a solved problem)\n 3. PATCH — write the fix (or apply a known one)\n 4. VERIFY — re-run the command; require exit 0\n 5. FILE — append a HEAL entry to .learnings/HEALS.md via skills/self-healing/scripts/new-heal.sh\n</self-healing-trigger>"
}
}
EOF
fi
#!/usr/bin/env bash
# find-similar-heals.sh — Search existing heals before generating a new fix.
# Usage: ./find-similar-heals.sh <pattern-key-or-keyword>
#
# Prints matching HEAL entries with their Pattern-Key, Status, and Recurrence-Count
# so the agent can decide whether to re-apply an existing fix or write a new one.
set -euo pipefail
QUERY="${1:-}"
HEALS_FILE="$(pwd)/.learnings/HEALS.md"
if [[ -z "$QUERY" ]]; then
echo "usage: $0 <pattern-key-or-keyword>" >&2
exit 2
fi
if [[ ! -f "$HEALS_FILE" ]]; then
echo "(no .learnings/HEALS.md yet — no prior heals to consult)"
exit 0
fi
# Find HEAL section headers that contain the query in their body (Pattern-Key, name, or text).
python3 - <<PY "$QUERY" "$HEALS_FILE"
import sys, re
query, path = sys.argv[1].lower(), sys.argv[2]
with open(path) as f:
text = f.read()
# Split into entries by ^## [HEAL-...]
entries = re.split(r"(?m)^## \[HEAL-", text)[1:]
hits = []
for body in entries:
if query in body.lower():
head = body.splitlines()[0]
pk = re.search(r"Pattern-Key:\s*(\S+)", body)
status = re.search(r"Status\*\*:\s*(\S+)", body) or re.search(r"Status:\s*(\S+)", body)
rc = re.search(r"Recurrence-Count:\s*(\d+)", body)
hits.append({
"id": "HEAL-" + head.split("]")[0],
"name": head.split("]", 1)[1].strip() if "]" in head else head,
"pattern_key": pk.group(1) if pk else "?",
"status": status.group(1) if status else "?",
"recurrence": rc.group(1) if rc else "1",
})
if not hits:
print(f"(no heals match '{query}')")
else:
print(f"Found {len(hits)} matching heal(s):\n")
for h in hits:
print(f" {h['id']} {h['name']}")
print(f" pattern={h['pattern_key']} status={h['status']} recurrence={h['recurrence']}")
PY
#!/usr/bin/env bash
# new-heal.sh — Initialize a new HEAL-<date>-<seq> entry skeleton.
# Usage: ./new-heal.sh <short_kebab_name> [trigger]
# trigger: tool-failure | missing-capability | env-issue | external-change | <free-form>
#
# Appends a templated HEAL entry to .learnings/HEALS.md and prints the HEAL-ID.
# Does NOT create .learnings/heals/<HEAL-ID>/ — that folder is lazy, created
# only when artifacts are written.
set -euo pipefail
NAME="${1:-}"
TRIGGER="${2:-tool-failure}"
if [[ -z "$NAME" ]]; then
echo "usage: $0 <short_kebab_name> [trigger]" >&2
exit 2
fi
LEARNINGS_DIR="$(pwd)/.learnings"
HEALS_FILE="$LEARNINGS_DIR/HEALS.md"
mkdir -p "$LEARNINGS_DIR"
DATE="$(date +%Y%m%d)"
SEQ=$(grep -c "^## \[HEAL-${DATE}-" "$HEALS_FILE" 2>/dev/null || echo 0)
NEXT=$(printf "%03d" $((SEQ + 1)))
HEAL_ID="HEAL-${DATE}-${NEXT}"
# Active-Context is optional. The agent / harness can set ACTIVE_CONTEXT in env.
ACTIVE_CONTEXT="${ACTIVE_CONTEXT:-}"
ACTIVE_LINE=""
if [[ -n "$ACTIVE_CONTEXT" ]]; then
ACTIVE_LINE="**Active-Context**: $ACTIVE_CONTEXT
"
fi
cat >> "$HEALS_FILE" <<EOF
## [$HEAL_ID] $NAME
**Logged**: $(date -u +%Y-%m-%dT%H:%M:%SZ)
**Status**: pending-verify
**Trigger**: $TRIGGER
${ACTIVE_LINE}**Area**: TODO
**Priority**: medium
### Failure
TODO — concrete error, command, exit code
### Diagnosis
TODO — root cause after investigation
### Fix
TODO — patch applied (commands, snippets, or pointers to .learnings/heals/$HEAL_ID/ if files were generated)
### Verification
TODO — what was run after the fix, what it returned. **Update Status to "verified" only after this passes.**
### Metadata
- Related Files: TODO
- See Also: TODO
- Pattern-Key: TODO
- Recurrence-Count: 1
- First-Seen: $(date +%Y-%m-%d)
- Last-Seen: $(date +%Y-%m-%d)
---
EOF
# stdout = the HEAL-ID alone, so `ID=$(new-heal.sh ...)` captures it cleanly.
# Human guidance goes to stderr.
echo "$HEAL_ID"
echo "$HEALS_FILE" >&2
echo "(create .learnings/heals/$HEAL_ID/ only if you generate artifacts to put there)" >&2