incident Skill
Inspiration: see NOTICE (MIT — alirezarezvani/claude-skills, clean-room).
Purpose: classify an incident's brand_survival_threshold in <60s, gate PIR drafting behind a GDPR Art. 33/34 notification-trigger evaluation, and scaffold a redaction-gated internal PIR matching the shape of knowledge-base/engineering/operations/post-mortems/dashboard-error-postmortem.md.
Directory convention: PIRs (incident records) live under knowledge-base/engineering/operations/post-mortems/. Procedural runbooks (recovery procedures, rotation playbooks, audit checklists) live under knowledge-base/engineering/operations/runbooks/. The split is semantic: runbooks have triggers: frontmatter and are scanned by Phase 3 for routing; PIRs do not and are not scanned.
Operator-invoked only. No Sentry/cron auto-fire substrate. Pre-write redaction sentinel ([scripts/redact-sentinel.sh](./scripts/redact-sentinel.sh)) is load-bearing — it runs BEFORE the draft is emitted inline to the conversation transcript AND before any file is written to disk. Transcripts ARE write boundaries; sentinel must precede inline-emit, not just file-commit.
All prod-touching steps are advisory + ack-gated per hr-menu-option-ack-not-prod-write-auth. The commit gate accepts a single literal token (COMMIT-PIR); LLM fuzzy-interpretation of "ok looks good" must never write a PIR.
Headless / Dry-run modes
--headless: suppress interactive prompts. On any blocking ack, exit non-zero with a structured error message instead of waiting. Phase 8 still requiresstatus: resolved.--dry-run <fixture.json>: read fields from a synthetic JSON fixture instead of operator prompts. Used by [scripts/dry-run.sh](./scripts/dry-run.sh) to drive AC8-AC13 against fixtures undertest/fixtures/. Dry-run never writes topost-mortems/and never invokescompound-capture; it emits the would-be PIR to stdout.
Phase 0 — Capture facts
No-SSH fact-pulling (Soleur vision —hr-no-dashboard-eyeball-pull-data-yourself). The operator is non-technical: NEVER ask them to SSH, rundf -h, or read a dashboard, and do NOT trust the report's stated mechanism — pull the actual prod error/state yourself. This includes NEVER asking the operator to paste verbatim error output, rungrep/stat/git configprobes, or eyeball logs — the operator decides, they do not retrieve. Also pull Better StackSOLEUR_*markers yourself (doppler run -p soleur -c prd_terraform -- scripts/betterstack-query.sh --since <N> --grep <marker>). Toolchain: DopplerDATABASE_URL_POOLER(prod DB read), Sentry issues viadoppler run -p soleur -c prd -- scripts/sentry-issue.sh <id>/--latest-event(prefer the least-privilegeSENTRY_ISSUE_RO_TOKEN; falls back toSENTRY_ISSUE_RW_TOKEN;SENTRY_AUTH_TOKEN403s on issues — the producer's real stderr is inexception.values[].value; runbooksentry-issue-read.md),/soleur:trigger-cron, prod HTTP/gh run. If a needed signal has no no-SSH read path, BUILD one — add a monitored stdoutSOLEUR_*marker in the emitting code (or emit to a GitHub issue/DB/endpoint) so the next occurrence self-reports, rather than deferring to the operator. Why: #4886 — the incident report blamed ENOSPC; the real cause (a dirty-clone.claude/settings.jsonblockinggit pull) was one Sentry-issue read away. #5934 — the worktree-wedge diagnosis twice asked the operator to pastegrep/statoutput that the observability layer held once instrumented (SOLEUR_GIT_CONFIG_TARGET_MASKED,SOLEUR_GIT_WORKTREE_VERIFY_FAILED). See [[2026-06-03-no-ssh-prod-signal-toolchain-never-hand-the-operator-an-ssh-task]] and [[2026-07-08-self-pull-observability-in-diagnostic-loops-never-ask-operator-to-fetch]].
Unreachable host with no log-shipping — READ before you MUTATE (hr-no-dashboard-eyeball-pull-data-yourselfextended to diagnosis). When the failing surface is a host that answers nothing (no SSH, no Vector/journald shipping, no Sentry emit), the self-serve read path is the provider API + rescue mode — it is NOT an operator-console task. Ladder, in order: (1) Provider API reads —GET /servers/{id}(status,private_net),/metrics?type=cpu,disk,network,/actions. Query the BOOT window, not the current window: an unreachable-but-idle host readsnet=0/disk=0and looks "dead" when it merely has no clients. (2) Rescue-mode disk read (Hetzner:enable_rescuewith{"type":"linux64","ssh_keys":[<id>]}to inject a registered key whose private half is in your agent → key auth, nosshpass; scope SSH in byset_rulesallowing ONLY your egress IP on:22and restore{"rules":[]}immediately after;reboot→ SSH →lsblk(the OS disk is NOT necessarilysda— an attached volume can take it) → mount the boot partition → read/var/log/cloud-init-output.log,grep -iE 'error|fail' /var/log/cloud-init.log,/var/lib/cloud/data/status.json). (3) Only then mutate, and only via the sanctioned destroy-guarded dispatch (apply-web-platform-infra.yml apply_target=…) — never a hand-rolledterraform apply: an out-of-band apply on an unverified hypothesis diverges prod frommainand makes the guarded recovery abortout_of_scope=1, blocking the real fix. Guard aborts are information — read their counters (out_of_scope=,server_replaced=,volume_created=) to learn which recovery path actually fits. AndApply completeis not proof:GETthe resource to confirm the provider's reality. Why: #6400 — four hypotheses (credential → firewall → store OOM → boot) were mutated-then-tested over hours; a rescue read ofcloud-init.lognamed the real cause (a transient IMDS failure left the host with no private NIC) in one shot, and a hypothesis-driven firewall apply blocked the sanctioned recreate three times. See [[2026-07-15-infra-incident-diagnose-before-mutate-and-no-out-of-band-applies]].
Collect from the operator (or from the dry-run fixture):
title— short prose, e.g."dashboard error boundary outage 2026-05-14".detected_at— ISO-8601 UTC. The skill validates the regex^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$before substitution (FR7 LLM-trust boundary). This is the incident START time.symptom— operator prose, free-form.suspected_change—PR #Nor commit SHA. Skill validatesincident_pris numeric.affected_user_count— integer estimate; 0 is valid.data_categories_breached— array of GDPR Art. 4(1) categories (email, userId, IP, billing, content). Empty array = no personal-data breach.risk_to_subjects— enumnone | low | medium | high. Phase 2 reads this for Art. 34 trigger evaluation.incident_overview— operator prose, 1-2 sentence executive summary (distinct fromsymptom, which is the full operator prose). DefaultTBD.recovery_at— ISO-8601 UTC, OPTIONAL. The incident END time. Validated against the SAME regex asdetected_atbefore substitution. Empty → status not yet resolved → MTTR renderedTBD.monitoring_detected_at— ISO-8601 UTC, OPTIONAL. The moment a monitoring system first flagged the incident (used only whendetection_method == monitoring). Same regex validation.detection_method— enummonitoring | external | manual. Feeds the Detection section and the MTTD computation.triggered_by— enumuser | system | market | provider.participants— operator prose (people + systems involved). DefaultOperator (single founder).resolution— operator prose: which actions brought resolution. DefaultTBD.version_triggered— repo + version / PR / commit SHA that triggered the outage. DefaultTBD.version_restored— repo + version / PR / commit SHA that restored the service. DefaultN/A — not yet restored.services_impacted— operator prose. DefaultTBD.revenue_impact— operator prose. DefaultUnknown / N/A(never fabricate a number).- `team_impac