claude-skills/engineering/skills/chaos-engineering/scripts/experiment_postmortem.py
Claude 23eefc2e9a
feat(skills): ship chaos-engineering (Phase 3 — resilience testing discipline)
Phase 3 of the multi-skill build effort. Same 14-step pipeline. Composes
explicitly with feature-flags-architect (kill switches as abort triggers)
and kubernetes-operator (operators are common chaos targets).

## What landed

### New skill: engineering/chaos-engineering

End-to-end chaos engineering discipline. Published as BOTH:
- Standalone plugin: engineering/chaos-engineering/
- Bundled mirror:    engineering/skills/chaos-engineering/

3 stdlib-only Python tools (Karpathy complexity 95/100 — best in portfolio):
- experiment_designer.py        — generates structured plans with hypothesis,
                                   steady-state, blast radius, abort criteria,
                                   rollback. Refuses to render plans without
                                   abort criteria (exit code 1).
- blast_radius_calculator.py    — computes affected users + error budget
                                   consumption + GREEN/YELLOW/RED risk score.
                                   Validates inputs (0 ≤ traffic-share ≤ 1).
- experiment_postmortem.py      — blameless postmortems from plan + result log;
                                   detects blame-laden language ("fault of",
                                   "should have known", "stupid", etc.) and
                                   warns at write time.

4 reference docs:
- chaos_principles.md      — 4 founding principles + 5th abort principle,
                              maturity model, history, when-to-start checklist
- experiment_design.md      — 7-section plan structure, pre-flight checklist,
                              time-boxing, escalation
- attack_taxonomy.md        — 7 attack types (latency / error / resource /
                              network-partition / dependency-failure / time-skew
                              / infrastructure) with magnitudes and tooling
- tooling_landscape.md      — Chaos Toolkit / Mesh / Litmus / Gremlin / AWS FIS
                              / DIY decision tree

Templates:
- experiment_template.md    — fill-in plan with all 7 sections
- postmortem_template.md    — blameless postmortem structure

Plus: SKILL.md (213 lines), README.md, /chaos-experiment slash command.

### Audit verdict (evidence-based)

Closest existing skills:
- engineering-team/incident-response — for actual incidents, not prevention
- engineering-team/red-team — adversarial; different goal (find attack paths)
- engineering-team/threat-detection — hunting; different goal
- engineering/observability-designer — measurement, not fault injection
None cover the chaos-engineering discipline (hypothesis-driven fault injection
with bounded blast radius). Verdict: BUILD. Gap is real and tooling-shaped.

### Composition story (Phase 1+2+3 form a stack)

```
feature-flags-architect.kill_switch_audit.py
  ↓ defines kill switches that ↓
chaos-engineering.experiment_designer.py
  ↓ designs experiments against ↓
kubernetes-operator (and other targets)
```

Together: a complete progressive-delivery + resilience-testing stack.

### Marketplace / registry

- marketplace.json: chaos-engineering registered as standalone plugin
- engineering-advanced-skills bundle: 47 → 48 skills, version → 2.4.2
- engineering/.claude-plugin/plugin.json: version + skill list updated
- mkdocs.yml: nav entry under "Engineering - POWERFUL"
- docs/skills/engineering/chaos-engineering.md: docs page (manual,
  pending generate-docs.py classification fix)
- docs/commands/chaos-experiment.md: auto-generated
- .codex/, .gemini/: synced

### Karpathy-coder gates

- complexity_checker (strict): 95/100 average — BEST score in the new
  portfolio. Only 1 WARN (depth 5 in blast_radius_calculator.py validation
  branches; the other 2 scripts hit no findings whatsoever).
- All 1666 tests pass (was 1648; added 18 for the new skill).
- mkdocs build --strict: succeeded in 13.33s.

### Verifiable success criteria (all green)

✓  scripts/*.py --help     → exit 0 for all 3 scripts
✓  SKILL.md frontmatter    → name + description + tags + compatible_tools
✓  plugin.json schema      → 8 fields exact (verified by check_plugin_json.py)
✓  sync_skill_bundles      → standalone ↔ bundled mirror in sync
✓  marketplace.json        → standalone entry + bundle counts updated
✓  generate-docs.py        → command page generated (skill page manual)
✓  mkdocs build --strict   → succeeded
✓  cross-tool sync         → codex + gemini synced
✓  pytest tests/           → 1666 passed, 0 failed
✓  CHANGELOG.md            → [Unreleased] entry expanded for Phase 3
✓  Self-test (RED case)    → 50% blast radius on 99.9% baseline correctly
                             classifies as RED (17.33% of monthly budget) and
                             returns ABORT recommendation
✓  Composition test        → references named skills explicitly compose

## Phase 1+2+3 cumulative

- 3 new skills: feature-flags-architect, kubernetes-operator, chaos-engineering
- 9 new Python tools (all stdlib, all <200 LOC, average complexity 90/100)
- 12 new reference docs (~250-500 lines each)
- 3 new slash commands (/flag-cleanup, /operator-audit, /chaos-experiment)
- 2 repo-infrastructure scripts (sync_skill_bundles, check_plugin_json)
- 1 pre-existing test fix (full-page-screenshot CI red)

## Files

- engineering/chaos-engineering/                                (new standalone plugin)
- engineering/skills/chaos-engineering/                         (new bundled mirror)
- commands/chaos-experiment.md                                  (new slash command)
- docs/skills/engineering/chaos-engineering.md                  (new docs page)
- docs/commands/chaos-experiment.md                             (auto-generated)
- mkdocs.yml                                                    (nav entries)
- .claude-plugin/marketplace.json                               (registered)
- engineering/.claude-plugin/plugin.json                        (bundle bumped)
- CHANGELOG.md                                                  ([Unreleased] expanded)
- .codex/, .gemini/                                             (cross-tool sync)

https://claude.ai/code/session_01Dq12xJakFRxwaoU8Pqejdm
2026-05-09 21:24:16 +00:00

144 lines
5.1 KiB
Python
Executable file

#!/usr/bin/env python3
"""Generate a structured chaos experiment postmortem.
Takes an experiment plan (JSON from experiment_designer.py) plus a results
file (free-form text or structured key=value lines), and produces a markdown
postmortem with hypothesis verdict, learning, surprises, and follow-up actions.
Catches common postmortem failure modes: no learning, no follow-up, blame-laden
language.
"""
import argparse
import json
import os
import re
import sys
from datetime import datetime, timezone
BLAME_PHRASES = [
"fault of",
"should have known",
"stupid",
"incompetent",
"obvious",
"lazy",
"didn't bother",
]
REQUIRED_RESULT_FIELDS = {
"outcome": "Did the hypothesis hold? (held|refuted|inconclusive)",
"duration_actual_min": "Actual experiment duration in minutes",
"aborted": "Was the experiment aborted? (true|false)",
}
def _parse_results(path):
"""Parse a results file. Lines like 'key=value' OR free text. Returns dict."""
if not os.path.isfile(path):
return {"_raw_text": ""}
with open(path, "r", encoding="utf-8", errors="replace") as f:
text = f.read()
parsed = {}
for line in text.splitlines():
m = re.match(r"^\s*([\w_.\-]+)\s*=\s*(.+?)\s*$", line)
if m:
parsed[m.group(1)] = m.group(2)
parsed["_raw_text"] = text
return parsed
def _check_blame(text):
found = []
low = text.lower()
for phrase in BLAME_PHRASES:
if phrase in low:
found.append(phrase)
return found
def build_postmortem(plan, results, follow_ups):
raw_text = results.get("_raw_text", "")
blame = _check_blame(raw_text)
pm = {
"experiment_id": plan.get("experiment_id", "?"),
"target": plan.get("target", "?"),
"created": datetime.now(timezone.utc).isoformat(),
"hypothesis": plan.get("hypothesis", "?"),
"outcome": results.get("outcome", "<UNRECORDED — must record>"),
"aborted": results.get("aborted", "<unrecorded>"),
"duration_actual_min": results.get("duration_actual_min", "<unrecorded>"),
"duration_planned_min": plan.get("attack", {}).get("duration_min", "?"),
"what_we_learned": results.get("learned", "<UNRECORDED — must record at least one learning>"),
"what_surprised_us": results.get("surprised", "<unrecorded>"),
"what_failed": results.get("failed", "<none recorded>"),
"what_held": results.get("held", "<none recorded>"),
"follow_ups": follow_ups,
"blame_warnings": blame,
"raw_results_excerpt": raw_text[:500],
}
return pm
def render_markdown(pm):
lines = []
lines.append(f"# Postmortem: {pm['experiment_id']}")
lines.append("")
lines.append(f"- **Target:** `{pm['target']}`")
lines.append(f"- **Postmortem date:** {pm['created']}")
lines.append(f"- **Outcome:** {pm['outcome']}")
lines.append(f"- **Aborted:** {pm['aborted']}")
lines.append(f"- **Duration:** planned={pm['duration_planned_min']}min, actual={pm['duration_actual_min']}min")
lines.append("")
lines.append("## Hypothesis")
lines.append(f"> {pm['hypothesis']}")
lines.append("")
lines.append("## What we learned")
lines.append(pm["what_we_learned"])
lines.append("")
lines.append("## What surprised us")
lines.append(pm["what_surprised_us"])
lines.append("")
lines.append("## What failed")
lines.append(pm["what_failed"])
lines.append("")
lines.append("## What held")
lines.append(pm["what_held"])
lines.append("")
lines.append("## Follow-up actions")
if pm["follow_ups"]:
for f in pm["follow_ups"]:
lines.append(f"- [ ] {f}")
else:
lines.append("- _none recorded — every experiment should produce ≥1 follow-up_")
if pm["blame_warnings"]:
lines.append("")
lines.append("## ⚠️ Blame warning")
lines.append("Blame-laden language detected — postmortems should be blameless.")
for b in pm["blame_warnings"]:
lines.append(f"- '{b}'")
return "\n".join(lines)
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--plan", required=True, help="Path to experiment plan JSON (from experiment_designer.py --format json)")
ap.add_argument("--result-log", required=True, help="Path to result log (free-form text OR key=value lines)")
ap.add_argument("--follow-up", action="append", default=[], help="A follow-up action; repeat for multiple")
ap.add_argument("--format", choices=["markdown", "json"], default="markdown")
args = ap.parse_args()
if not os.path.isfile(args.plan):
print(f"ERROR: plan not found: {args.plan}", file=sys.stderr)
return 2
with open(args.plan, "r", encoding="utf-8") as f:
plan = json.load(f)
results = _parse_results(args.result_log)
pm = build_postmortem(plan, results, args.follow_up)
if args.format == "json":
print(json.dumps(pm, indent=2))
else:
print(render_markdown(pm))
return 0
if __name__ == "__main__":
sys.exit(main())