Building on AWS · guide 13 of 13
Incident Response and Operational Documentation as Code
Abstract. This guide is for senior DevOps, SRE and platform engineers at small and mid-stage startups on AWS, especially crypto infrastructure companies that run signing keys, relayers, indexers and settlement paths where an outage can strand customer funds. It treats incident response and operational documentation as one system with a single job: getting the right action taken, by whoever is on call, quickly, when something breaks at 3am. It covers how to declare an incident and assign roles on a team of five to fifteen engineers, severity levels and a comms cadence you can actually hold, blameless post-mortems whose action items get closed, symptom-indexed runbooks with stable anchors that alerts deep-link to, CI checks that fail an alert change made without a runbook change, a generated service map with fail-versus-degrade semantics, and the editing rules that keep a large body of docs truthful over time. It also covers writing docs that AI coding agents can operate from as well as humans, and the incidents particular to crypto: pause switches, key compromise, and stuck funds. The patterns come from a mature production platform and are generalized here. Every failure mode described really happened at least once.
1. The problem space
1.1 Why “docs as code” is an incident-response topic
People usually treat incident process and documentation as separate concerns: one is a culture problem and the other a wiki problem. In practice they fail together. A responder who gets paged needs to know three things within a minute:
- What the alert means and what to do first. That is the runbook.
- What else is affected, and whether it fails or only degrades. That is the service map.
- Who decides, who talks to customers, and when. That is the incident playbook.
If any of these is missing, stale or slow to read, the responder improvises. Improvisation during an outage produces the classic secondary failures: two people rolling back at the same time, a customer told “fixed in 10 minutes” when nobody knows that, or a status page that stays green for six hours.
“As code” means these artifacts live in the same repository as the infrastructure they describe and are reviewed in the same pull requests. Where possible, a machine checks them against that infrastructure. AWS’s Well-Architected Framework says the same thing in its own terms. Runbooks should be “stored in a central location (version control system) and updated frequently”, and one of its named anti-patterns is “letting runbooks drift out of sync with system changes and automation” [6]. Every alert should initiate “a specific, well-defined response plan” and be “linked to an up-to-date knowledge base” [7]. DORA’s research found documentation quality amplifies the payoff of every technical practice it studied [14]. The open question is not whether to do this, but how to keep it true once it exists.
1.2 The forces on a small team
- Headcount. Google’s SRE book recommends at most two incidents per 12-hour on-call shift, because each one takes roughly six hours of analysis, remediation and follow-up [3]. A five-person team cannot staff the full incident command structure (incident commander, ops lead, communications lead, planning) [1][4]. The process has to collapse cleanly to one person and expand when a second responder arrives.
- Speed vs. safety. Each extra gate on a merge costs throughput. CI checks on docs have to be cheap, hermetic and hard to argue with, or people will route around them.
- Partner and auditor pressure. Enterprise customers, exchanges and SOC 2 auditors ask for incident response plans, post-mortem records and evidence of change control. A process kept in git produces that evidence as a by-product.
- Crypto stakes. In a typical SaaS outage, requests fail and users retry. In a crypto platform, a deposit can confirm on-chain and then get stuck inside your system. A key can be exfiltrated. A transient RPC rate limit can be turned into a terminal failure state by application code. “Funds at risk” has to be its own severity trigger with its own playbook. NIST’s incident-response profile and AWS’s security incident response guide both frame preparation (people, process, technology) as the foundation [9][10]. For a team holding keys, that preparation includes on-chain controls as well as runbooks.
- Agents as operators. More and more of the routine reading, triage and editing is done by AI coding agents with repository access. They read the docs literally, fully and at speed, so a stale claim gets acted on as confidently as a true one. Docs now have two audiences, and the second one never says “that doesn’t sound right.”
2. Reference design
2.1 Layout
One repository holds infrastructure code, alert rules and operational docs. A small set of scripts and CI jobs binds them together.
The documentation set is kept small on purpose. One team we’ve seen states in its docs index that it is “kept deliberately small — only docs we maintain”. An index sorted by when you’d read each doc (“during an incident” first, “reference” second) does more for responders than an exhaustive wiki.
docs/
README.md # index: "during an incident" first, then reference
incident-playbook.md # process; ≤ 300 lines; read in one sitting
runbook.md # per-alert + per-scenario procedures; stable numbered anchors
service-map.md # who calls whom, (f)/(d), name resolution, generated inventory
alert-authoring.md # every entry is a defect that reached prod
observability-metrics.md # which metrics exist, which don't, label traps
post-mortems/
README.md TEMPLATE.md YYYY-MM-DD-<slug>.md
customer/ README.md TEMPLATE.md # redacted, customer-facing twin
dr/ # break-glass playbooks, drill logs
scripts/
runbook.sh # print one section of the big runbook
service-map-check.py # regenerate/check the service map
runbook-anchor-check.py # every runbook_url resolves to a heading
AGENTS.md # or CLAUDE.md: agent- and human-facing gotchas
2.2 Declaration and roles
Who can declare: anyone, without permission. Lean toward over-declaring. A wrongly declared incident that lasts five minutes costs a chat channel. An under-declared one that lasts thirty minutes costs reputation, and in crypto it may cost customer funds.
When to declare. The triggers need to be concrete enough that nobody has to judge them mid-incident:
- Any page-level alert that hasn’t recovered on its own within 5 minutes.
- Any time a responder suspects customer impact and wants help coordinating.
- Any security signal, regardless of severity: a leaked credential, unexpected signing-key access, a smart-contract anomaly, an unexplained KMS key policy change.
How to declare should take about 30 seconds and need no tool beyond chat:
#inc-YYYYMMDD-<slug> (UTC date; slug becomes the post-mortem filename)
:rotating_light: Incident declared
Severity: SEV<1|2|3>
IC: @me
Summary: <one line — what's broken, who knows>
Status page: <link or "not yet">
Alert: <alert link>
Runbook: <runbook_url from the alert annotation>
Post a one-line cross-post to the company-wide channel so people outside know an incident is open.
Roles, collapsed for a small team. The Google model has four roles: Incident Commander, Ops Lead, Communications and Planning. The ops lead “is the only group modifying systems” [1]. The PagerDuty response guide adds Scribe, Deputy and internal/customer liaisons [11]. On a five-to-fifteen person team, use this collapse rule:
| Responders | IC | Ops | Comms | Notes |
|---|---|---|---|---|
| 1 | declarer | declarer | declarer | Normal case. The IC does comms on a timer, not “when there’s news.” |
| 2 | first responder | second responder | IC | The IC stops typing commands. This is the hard part. |
| 3+ | one person | one or more people | one person | Others execute through the IC, so two people never roll back or message customers independently. |
| security / funds | as above, plus the security lead paged immediately | — | IC and security lead sign off on anything beyond “we are investigating” |
Handoffs need to be explicit. The SRE book’s wording is useful: the outgoing commander says “You’re now the incident commander, okay?” and doesn’t leave until they get a firm acknowledgement [1]. A copy-paste form works well in chat: Handoff: @new from @me. Current state: <one paragraph>. Open threads: <list>. Require a handoff for any incident longer than four hours or crossing a shift boundary.
2.3 Severity, tied to your product
Generic severity matrices (“major impact to many users”) get debated mid-incident. Tie each level to observable symptoms of your own system, and put funds-at-risk at the top on equal footing with a full outage.
| Severity | Definition | Concrete examples (crypto infra) | Routing |
|---|---|---|---|
| SEV1 | Full outage or funds at risk | Core API 5xx for all requests; every public endpoint failing at the edge, including clean 404s with valid TLS; signing keys inaccessible (KMS errors, IAM regression); indexer stalled > 5 min with funds in flight; suspected key compromise | Page + chat |
| SEV2 | Partial outage / SLO breach | Fill-detection p95 over SLO; one chain’s RPC degraded with fallback routing around it; one service degraded with work stuck in a non-terminal state past threshold | Chat (page during business hours if you choose) |
| SEV3 | Intermittent | A single endpoint erroring occasionally without moving the overall success rate; a flaky alert that needs root-causing, not silencing | Chat only; never pages |
Three rules keep this workable:
- When in doubt, declare one level higher and downgrade in-channel with a timestamp:
HH:MM UTC — downgrading SEV1 → SEV2, impact scoped to <X>. Don’t re-route the pager mid-incident. - Severity is set by your user-visible impact, not the vendor’s. When an RPC provider, CDN or L1 is down, there’s no rollback. The work is to confirm the vendor’s status, trip your circuit breaker or fail over, and treat the incident as mostly comms rather than mostly mitigation.
- Name the misleading signatures explicitly. The “edge returns clean 404s” example is in the SEV1 list because one team’s ingress lost every route while all pods stayed healthy. The first hypothesis in the channel was “the apps are down”, and healthy pods pointed responders away from the real cause. If a failure mode looks benign, the severity table should say it isn’t.
2.4 Comms cadence a small team can hold
| Channel | SEV1 | SEV2 | SEV3 |
|---|---|---|---|
| Internal incident channel | Declaration within minutes; update every 15 min even if “no change”; all-clear within 5 min of recovery | Same | Optional |
| Public status page | Post within 15 min of declaring; update every 30 min | Post if sustained > 30 min; update every 60 min | Not required |
| Direct customer outreach | IC + security/comms lead authorize | IC authorizes | — |
Status-page content rules belong in the playbook verbatim, because they get broken under pressure:
- Describe customer-visible impact in plain language. Write “Intent submissions are currently failing”, not “consumer lag exceeding threshold.”
- No internal service names, alert IDs, IPs, hostnames or customer names.
- No recovery promises you can’t keep. “We are investigating” or “We expect resolution within the hour”, never “Fixed in 10 minutes.”
- At most three sentences per update. Lifecycle: Investigating → Identified → Monitoring → Resolved.
Resolution must be confirmed by a metric, log or end-to-end check, not by the absence of new alerts. For example: 5xx rate back to baseline for 10 consecutive minutes, bundle-creation rate at its pre-incident level, synthetic suite green, or a test transaction submitted and settled end-to-end. Post the evidence in the channel:
:white_check_mark: All clear — HH:MM UTC
Recovery confirmed via: <metric / dashboard / log query link>
Duration: <start UTC> → <end UTC> (<minutes>)
Mitigation: <what fixed it>
Post-mortem owner: <IC>, due <date + 5 business days>
Leave the channel readable until the post-mortem merges, then archive it.
2.5 Runbooks: symptom-indexed, stable anchors, deep-linked
Index by symptom, not by component
Responders arrive with a symptom, usually an alert name and sometimes just “the API is 404ing.” Organize the runbook so the alert lands on the exact section and the symptom is the heading:
§1 Quick reference — system map
§2 Access & tools (tunnels, kubectl context, log/metric/trace queries, vendor dashboards)
§3 Per-alert playbooks ← every alert's runbook_url points here
§3.12.2 Signer balance low
§3.40 External outage detection (health check → pager)
§3.41 All routes return 404 while pods are healthy
§4–8 Per-service failure scenarios
§9 Rollback & escalation
§10 DNS / certificate emergencies
§11 Backup restore mechanics (RTO/RPO live here)
Every per-alert section uses the same shape, so a responder (or an agent) knows where to look without reading the whole section:
### 3.41 All routes return 404 while pods are healthy
**Alert:** <name> · **UID:** <uid> · **Severity:** sev1 · **For:** 2m
**You arrive here from:** §3.40, several readiness alerts at once, or a user report.
**Why it misleads.** TLS valid, responses fast, pods Running/Ready, internal
traffic flowing. A proxy with zero routes answers a clean 404.
**The tell:** 404 (not 5xx or timeout) on a path you know exists.
#### Identify
1. Confirm it is ingress-wide (curl three hosts behind different ingress releases).
2. Check the route objects still exist.
3. Read the proxy logs — `is forbidden` names the cause directly.
#### Mitigate
4. Restore the permission on every affected ClusterRole.
**Do not restart the pods** — the reflector retries every ~30s; a restart only adds downtime.
> **Latent-change warning.** RBAC changes do not restart dependents...
> Full writeup: post-mortems/YYYY-MM-DD-<slug>.md
Three elements carry most of the value. “You arrive here from” gives a reverse index from other sections and alerts. “Why it misleads” names the wrong hypothesis before the responder forms it. “Do not …” blocks the instinctive action that makes things worse.
Stable anchors are an API
Grafana’s alert model includes a runbook_url annotation for exactly this purpose (“the runbook page to guide operators managing a potential incident”) [13]. Once roughly 200 alerts and dashboards deep-link runbook.md#341-all-routes-return-404-while-pods-are-healthy, section numbers are a public interface:
- Never renumber. Gaps left by retired alerts are intentional. Say so in the index.
- GitHub derives the anchor from the whole heading, number included (
### 3.12.2 Signer balance low→#3122-signer-balance-low). Editing the title breaks links as surely as renumbering does. - To resolve a duplicate number, move the section with the fewest inbound alert links and update those links. Don’t cascade a renumber.
- Link to a section anchor, never to a bare
runbook.md.
Alert rules as code carry the link:
resource "grafana_rule_group" "edge" {
name = "edge"
folder_uid = grafana_folder.alerts.uid
interval_seconds = 60
rule {
name = "edge-all-routes-404"
condition = "C"
for = "2m"
labels = { severity = "sev1" }
annotations = {
summary = "Every public host is returning 404 — ingress has no routes"
runbook_url = "${local.runbook_base}#341-all-routes-return-404-while-pods-are-healthy"
}
# data { ... }
}
}
One giant file, read cheaply
Successful runbooks grow. One platform’s runbook reached about 700 KB, roughly 220 numbered sections and close to 8,000 lines. That is too large for a human to scroll and expensive for an agent to load whole. The usual instinct is to split it into per-service files. The team kept a single file on purpose, because about 200 live runbook_url annotations, the CI paths filter, the PR template and the README all link runbook.md#<anchor>, and splitting would break every link an operator clicks mid-incident. Instead they added a 60-line script:
#!/usr/bin/env bash
# scripts/runbook.sh 3.96 | scripts/runbook.sh oomkill | scripts/runbook.sh --list redis
set -euo pipefail
RUNBOOK="$(cd "$(dirname "$0")/.." && pwd)/docs/runbook.md"
if [ "${1:-}" = "--list" ]; then
grep -nE '^#{2,4} ' "$RUNBOOK" | { [ $# -gt 1 ] && grep -i -- "$2" || cat; }; exit 0
fi
q="$1"
if [[ "$q" =~ ^§?[0-9]+([.-][0-9]+)*$ ]]; then
n="${q#§}"; n="${n//-/.}"; pat="^#{2,4} ${n//./\\.}[[:space:]]"
else
pat="^#{2,4} .*${q}"
fi
start=$(grep -nEi "$pat" "$RUNBOOK" | head -1 | cut -d: -f1)
[ -n "$start" ] || { echo "no match; try --list $q" >&2; exit 1; }
depth=$(sed -n "${start}p" "$RUNBOOK" | grep -o '^#*' | tr -d '\n' | wc -c)
next=$(awk -v s="$start" -v d="$depth" 'NR>s && /^#+ /{n=index($0," ")-1; if(n<=d){print NR; exit}}' "$RUNBOOK")
end=${next:+$((next-1))}; end=${end:-$(wc -l < "$RUNBOOK")}
sed -n "${start},${end}p" "$RUNBOOK"
Note the end-of-file case. An early version dropped the final line of the last section, which is fine until that last line is a recovery step. Put the script’s existence in the agent instruction file (“don’t read the runbook whole; use scripts/runbook.sh”). For agents it cuts context cost by two orders of magnitude.
2.6 CI contracts: what can be enforced, and what can’t
Runbook drift: the “touched” check
The cheapest useful check fails any PR that changes alert rules without touching the runbook:
name: (Docs) Drift check
on:
pull_request:
paths:
- 'terraform/grafana/**'
- 'helm-charts/**'
- 'argocd/**'
- 'docs/service-map.md'
- 'scripts/service-map-check.py' # checker must trigger itself, or edits to it fail open
jobs:
runbook-drift:
if: ${{ !startsWith(github.head_ref, 'automation/') }} # bot image bumps exempt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@<sha>
- id: changes
uses: dorny/paths-filter@<sha>
with:
filters: |
alerts: ['terraform/grafana/alert_rules*.tf']
runbook: ['docs/runbook.md']
- name: Enforce runbook update
run: |
if [ "${{ steps.changes.outputs.alerts }}" = "true" ] && \
[ "${{ steps.changes.outputs.runbook }}" != "true" ]; then
echo "::error::alert_rules*.tf changed without docs/runbook.md."
echo "::error::For a genuine no-op (whitespace/comment) add a one-line runbook note saying why."
exit 1
fi
The escape hatch matters. A no-op change still needs a runbook note explaining itself, which is cheap and leaves an audit trail. Its limit is equally important: “the file was touched” is not “the section is right.” The next step up is an anchor-resolution check, which one independent review recommended once the touch check had been in place for months:
#!/usr/bin/env python3
"""Fail if any runbook_url anchor doesn't resolve to a runbook heading."""
import re, sys, pathlib, unicodedata
def gh_anchor(heading: str) -> str:
s = unicodedata.normalize("NFKD", heading).lower().strip()
s = re.sub(r"[^\w\- ]", "", s) # GitHub drops punctuation, incl. dots in "3.12.2"
return s.replace(" ", "-")
root = pathlib.Path(__file__).resolve().parents[1]
headings = {gh_anchor(m.group(1))
for m in re.finditer(r"^#{1,6} (.+)$", (root/"docs/runbook.md").read_text(), re.M)}
bad = []
for tf in (root/"terraform/grafana").glob("alert_rules*.tf"):
# handles quoted literals and "${local.runbook_base}#anchor" interpolations
for m in re.finditer(r'runbook_url\s*=\s*"[^"#]*#([^"]+)"', tf.read_text()):
if m.group(1) not in headings:
bad.append(f"{tf.name}: #{m.group(1)}")
if re.search(r'runbook_url\s*=\s*local\.', tf.read_text()):
print(f"warn: {tf.name} uses a local.* runbook_url; resolve it too", file=sys.stderr)
if bad:
print("Unresolved runbook anchors:\n " + "\n ".join(bad)); sys.exit(1)
Expect edge cases. A real audit had to brace-parse dynamic "rule" → content {} blocks, plus runbook_url keys that were unquoted or local.* references. Even so, a regex checker gets you most of the way in an afternoon.
Coverage vs. correctness
This distinction applies to every docs check, so state it in the check itself and in the agent instructions:
| Check type | Answers | Can’t answer | Example |
|---|---|---|---|
| Coverage | “Is everything that exists mentioned?” | “Is what’s written true?” | Every deployed chart appears in the service map |
| Touch / co-change | “Did someone open the doc in this PR?” | “Did they fix the right part?” | Alert change requires a runbook diff |
| Prose co-change | “If the generated part moved, did hand-written prose move too?” | “Is the new prose correct?” | Service-map generated block changed → require a non-generated diff |
| Resolution | “Does every link point at something real?” | “Is the target content current?” | runbook_url anchors resolve |
| Claim audit (human, quarterly) | “Is each load-bearing claim still true?” | — | “selfHeal is off”, “prod pins digests” |
One team’s service-map checker opens with a docstring that says, in effect, a green run means “nothing deployed is missing from the map”, not “the map is right.” That sentence prevents people trusting the check more than it deserves.
Hand-maintained trigger lists fail open. If the checker reads an input that the workflow’s paths: filter doesn’t list, a PR that changes that input never runs the check, and the check shows nothing at all instead of a red. Put a comment in the checker naming every input and saying “widen the workflow filter to match.” Make the workflow trigger on changes to the checker itself. One post-mortem found that an ingress path filter’s glob matched the main ingress values files but missed two sibling releases’ files, one of which was the only release watching a namespace it doesn’t live in.
2.7 The service map: fail vs. degrade, and name resolution
What only a human can write
An incident needs a dependency table with an impact column using two values. (f) means the caller fails without this dependency. (d) means it degrades.
| Service | Calls | Impact if dependency down | Observed (7d) |
|------------|----------------|-------------------------------------------------|---------------|
| api | auth-svc | **(f)** all authenticated requests fail | ✗ — note 1 |
| api | price-svc | **(d)** pricing unavailable; quotes use cache | ✓ 635k |
| api | rpc-proxy | **(f)** on-chain operations fail | ✓ 3.9M — note 2 |
| deposit | rpc-proxy | **(f)** on-chain verification fails | ✗ — notes 2,3 |
The Observed column comes from trace-derived service-graph metrics. It is useful, but it comes with a warning the table should spell out: an unobserved edge is not a dead edge. In one estate, the two most critical edges were unobservable by design. The RPC proxy had tracing switched off while serving almost four million requests a week, and the auth service was called through a path the instrumentation couldn’t see. A missing edge tells you “not instrumented”, never “not used.” Check any zero against a second source before believing it.
Name resolution
The same service often has four names: repo, GitOps application, Helm chart, and the telemetry service.name. Those mismatches cost real time during an incident. A table listing only the exceptions is one of the most useful things in the whole doc set:
| Repo | GitOps app | Chart | Trace service.name |
|-------------------|-------------------|------------------|--------------------|
| api | api | api | api-ts |
| deposit-processor | deposit-processor | deposit | deposit |
| rpc-gateway-fork | rpc-proxy | rpc-proxy | rpc-proxy |
| identity | identity-web | identity | identity-passkey |
Add the label conventions per signal underneath it. For example, logs use the container name, kube_* metrics use the container name, and application OTel metrics use the trace name.
What a machine must generate
The inventory is the part humans shouldn’t maintain: every deployed release, every ingress route, and every declared service-to-service edge. Generate it between markers and check it in CI. This is the same reasoning behind “diagrams as code”: version-controlled, generated from source, reviewable in a diff [15].
<!-- BEGIN GENERATED: service-map-check -->
> Generated by `scripts/service-map-check.py --write`. Do not hand-edit.
> Declared edges only: these are enforced NetworkPolicy rules, so they are a
> superset of real inbound traffic. For what each edge *means*, see the tables above.
...
<!-- END GENERATED: service-map-check -->
The best source for edges is enforced NetworkPolicy ingress rules. When the CNI enforces policy, a missing rule causes an outage, so the declared set is guaranteed to be a superset of real in-cluster inbound edges. That makes it the closest thing to ground truth the repo has. The generator should list its exclusions explicitly (NOT_A_SERVICE = {"common", "buildkitd", ...}), so a skipped chart is visible in review instead of silently shrinking coverage.
State the precedence rule in the doc’s header: if the hand tables and the generated block disagree, the generated block is right about existence and the tables are right about meaning.
Then add the prose co-change check from §2.6. If the generated block changed against the base commit (a new service, route or edge) and no hand-written line changed with it, fail the PR. That doesn’t prove the prose is right. It only forces someone to have looked at it. Without it, a service can enter the map with an empty impact cell and pass coverage indefinitely.
2.8 Post-mortems
Trigger and SLA
Required for every SEV1 and SEV2, due within 5 business days of the all-clear, owned by the IC. Encourage them for near-misses, where the system caught something automatically that would otherwise have been a SEV1 or SEV2. Google’s trigger list adds two that small teams often miss: any data loss, and any monitoring failure (“which usually implies manual incident discovery”) [2].
Within 24 hours of the all-clear:
- Copy
TEMPLATE.mdtopost-mortems/YYYY-MM-DD-<slug>.md, using the incident start date in UTC and the same slug as the chat channel. - Open a draft PR with the TL;DR and a timeline skeleton, and tag a reviewer outside the response. The PR review is where the blameless polish happens.
- File a tracker issue per action item and link each one. The post-mortem is the canonical list of what was committed to, and the tracker is where status lives. Action items tracked only in markdown rot.
Template sections that earn their place
The full template is in the post-mortems directory. These are the sections that changed behavior in practice:
- Metadata with four intervals. TTD (started → detected), TTA (detected → acknowledged), TTM (acknowledged → mitigated), TTR (mitigated → resolved). Computed across incidents, these show where your process is actually slow. AWS’s post-incident analysis practice asks for the same timestamps [8].
- Customer impact, quantified, with the query cited. “Vague impact statements are why post-mortems don’t change behaviour.” For crypto, include an explicit funds line: dollar value stuck or lost, and how it was resolved (recovered automatically, needed manual operations, or lost).
- Trigger vs. root cause. The trigger is the change that exposed the latent condition, often a deploy. The root cause is the latent condition itself, often a missing guardrail or a wrong assumption.
- What the system did automatically. This records which past investments paid off, so they keep getting funded.
- What required human intervention. Each item here is a candidate action item.
- Detection gaps and response gaps, kept separate. Did the runbook entry exist? Was it accurate? Was a channel opened and an IC named? Was the status page updated?
- Action items typed Prevent / Detect / Mitigate / Process, each with an owner, a tracker link and a due date.
- Went well / went wrong / got lucky. “Empty cells here are suspicious — if nothing was lucky, ask harder.” “Hit at 02:00 UTC — the same blast radius at 14:00 would have affected about ten times as many users” is the kind of hidden fragility this column surfaces.
Blameless, concretely
“A blamelessly written postmortem assumes that everyone involved in an incident had good intentions and did the right thing with the information they had” [2]. The case comes from Etsy’s just-culture work: an engineer who expects reprimand holds back exactly the details needed to understand the failure, which nearly guarantees it repeats [12]. Make it mechanical:
- Use roles (“the on-call”, “the deploying engineer”), never names.
- “Human error” is never a root cause. It is a symptom of a system that allowed the error.
- Record false leads in the timeline. They are learning material and save the next responder time.
The customer-facing twin
When an incident affected a customer, write a separate, redacted, derived report. Never send the internal post-mortem, which is self-critical by design and exposes weaknesses you are still closing. A good structure:
- Write the internal post-mortem first, and only derive the external one once the root cause is firm. A report you have to walk back costs more trust than one that arrives a day late. Send a one-line holding note if the customer is waiting.
- Open a PR for it. That PR is the IC + comms-lead sign-off gate.
- Five levers, in order of impact: specificity reads as competence; let them check you (give enough detail for them to verify the blast radius against their own logs); own it (a vendor failure is your missing fallback); fix the class, not the instance; and close the loop with a one-line “the fixes from our report are now live.”
- Leave out internal names, IPs, dashboard links, employee names, other customers, and anything framed as “where we got lucky.” Make an explicit, accurate statement on funds and data integrity, and have someone check the wording of any “funds lost” phrasing for contractual weight.
2.9 Funds-at-risk incidents: pause, contain, succeed
Crypto platforms need a playbook layer that ordinary SaaS doesn’t: irreversible actions with on-chain consequences. Trail of Bits’ “Rekt test” asks whether you have a written and tested incident response plan that covers out-of-band communication and reduces key-person risk [16]. The industry also has a shared emergency channel. The Security Alliance’s SEAL 911 is a free, round-the-clock line for active incidents [17]. Put it in your escalation list before you need it.
Containment first, succession second. Keep them as separate documents with separate owners:
Suspected key compromise — SEV1, security lead paged regardless of the hour
Containment document
1. PAUSE on-chain pause switch / executor freeze (seconds)
2. REVOKE IAM credentials, KMS grants, CI roles that sign (minutes)
3. PRESERVE CloudTrail, KMS Sign events, mempool / tx history
before anything rotates
4. COMMS internal channel; partner and exchange notice; status page
── decision gate: compromise confirmed? what is the scope? ──
Succession document
5. SUCCEED new key → migrate every on-chain reference
WHILE THE OLD KEY STILL WORKS
6. VERIFY checklist ticked by a second person
7. UNPAUSE owner-only, after 6
Design notes that come straight out of real break-glass playbooks:
- The pause must be reachable during the decision. A good design lets both the owner multisig and the hot executor key call
pause(). Pausing costs nothing and refusing every batch is harmless, so whoever is first on the scene can stop the bleeding before deciding whether the key is compromised. Unpause is owner-only. OpenZeppelin’sPausableis the standard building block: “a common emergency response mechanism that can pause functionality while a remediation is pending” [18]. - Order is load-bearing. Once a non-exportable KMS key stops working (it has been disabled, or its deletion window has passed), any on-chain reference still pointing at it is permanently unreachable. The migration sweep has to run before retirement. Write that as a bolded gate, not a footnote.
- Prefer single-call succession where you can design for it. A contract whose authorized signer is re-pointed by one
setExecutor(newAddress)call retires every signature the old key ever produced in the same transaction, including batches signed but not yet broadcast. Keys that lack this property need a sweep, and the playbook should say which keys are which. - Fail closed at startup. Have the service refuse to start unless the key it holds, its configured submitter address and the contract’s current executor all agree. A rotated or dead key then crash-loops with the reason in the first log line instead of signing unauthorized transactions.
- Every break-glass playbook carries a “last drilled” row, and “never” is an acceptable value. One team’s key-succession playbook says “Last drilled: never” and labels its RTO “provisional (engineering estimate)”, and states that two keys have no known rotation mechanism. That honesty is what made the gap visible to an independent reviewer.
3. Decisions and tradeoffs
3.1 Docs in the repo vs. a wiki
| Repo (markdown, PR-reviewed) | Wiki / Notion / Confluence | |
|---|---|---|
| Upside | Same PR as the change; CI can check; diffable history; agents read it natively; audit evidence for free | Low barrier for non-engineers; rich editing; good search |
| Downside | Markdown limits; non-engineers edit less; large files need tooling | Drifts silently; no co-change enforcement; agents need a connector; history is hard to audit |
| Cost | Near zero | Seat cost; ongoing gardening |
| Revisit when | Large non-engineering ops staff (support, compliance) need to author procedures | — |
Recommendation: keep everything that an alert links to, or that a deploy can invalidate, in the repo. Keep customer-support macros, vendor contracts and HR-ish on-call policy wherever the people who own them work.
3.2 One runbook file vs. many
| Option | Upside | Downside | Signals to revisit |
|---|---|---|---|
| Single file + section tool | Every deep link stays valid; one place to grep; one CI filter path | Hundreds of KB; merge conflicts; costly to load whole | Merge conflicts on the runbook become weekly; or > ~1 MB |
| One file per alert/service | Small, focused files; easy ownership | Every split breaks existing runbook_urls unless redirected; CI filters multiply | Starting from scratch, or a one-time migration with an anchor-rewrite script and the resolution check to prove it |
| Generated from alert metadata | Can’t drift from the rule | Loses the “why it misleads” prose that is the real value | Very large alert estates with templated rules |
At seed stage, start with one file and the resolution check. Revisit when your platform team has more than one owner per subsystem.
3.3 How strong a runbook CI check
- Touch check: an afternoon of work, catches the “forgot entirely” case, and is trivially satisfied by an unrelated edit. Day 1.
- Anchor resolution: a day of work, catches broken links. First year.
- Per-section metadata check (the section’s documented UID, severity,
forand threshold match the Terraform): catches the most common silent drift. One team found these lines lagging behind real rule changes repeatedly, includingfor1m→5m changes and a sev1 whose routing changed from chat-only to paging. Do this at scale, or when you first catch a responder trusting a stale threshold.
3.4 Incident tooling: chat-native vs. a platform
| Option | Fit |
|---|---|
| Chat channel + pager + markdown templates | Seed to Series A. Zero cost; the process lives in the playbook. |
| AWS Systems Manager Incident Manager | AWS-centric shops wanting response plans tied to CloudWatch alarms/EventBridge and automation runbooks [7]. |
| Commercial incident platform (PagerDuty, incident.io, Grafana IRM, etc.) | When incident volume makes manual timeline reconstruction and status-page sync a real cost, or auditors want structured records. |
The tool doesn’t replace the playbook. One team’s status-page posts were manual at every severity, and the playbook said so explicitly, so nobody waited for an auto-draft that would never appear.
3.5 Who is IC on a tiny team
- Declarer is IC (default). No routing delay. The risk is that the strongest debugger stays IC and stops coordinating.
- A named IC rotation separate from on-call. Cleaner, but it needs about eight people to avoid burnout.
- Always a specific senior person. This creates a single point of failure. In one incident, formal handling (IC, status page, customer comms) lagged by almost seven hours because the one person who always took that role was out of office, and nobody else picked it up. This was also the new playbook’s first real use.
Rule: anyone can be IC, the playbook says so, and the declarer holds the role until they explicitly hand it off.
3.6 Post-mortem for every SEV2?
Required SEV2 post-mortems are where compliance slips first. An independent review of one platform found that policy required a write-up for every SEV1/SEV2, but only three existed while at least four recent qualifying incidents had none. Options:
- Keep the policy and track compliance. A weekly bot query of “SEV1/2 incidents closed > 5 business days without a linked post-mortem PR.”
- Tier it. Full post-mortem for SEV1, a “lite” one-page form for SEV2 (TL;DR, impact, root cause, action items).
- Allow a recorded decision not to write one. One post-mortem explicitly called out that no decision not to produce a customer report had been recorded. The missing artifact is a gap, but so is the missing decision.
4. Failure modes and field lessons
Each of these happened. Names, dates and figures are removed.
4.1 Declaration gets skipped exactly when it matters most
Three SEV1s fired at once and every public endpoint was down for about eleven minutes. No channel was opened, no IC named and no status page posted. The whole response ran in the shared alerts channel. Nobody decided not to declare. There was simply no pause in which to do it. The fix was a paragraph in the playbook placed right above the “how to declare” steps:
If you are deep in diagnosis and realise no channel exists, declaring is the next action, and it takes 30 seconds. Hand the diagnosis to someone else for that half-minute if you have to.
Make the total-edge-failure signature an explicit SEV1 example (§2.3) so the severity call doesn’t wait for analysis.
4.2 The warning was real, and it was classified as noise
A hardening change removed secrets from an ingress controller’s ClusterRole, reasoning correctly that no route referenced a Secret. But the controller starts a Secrets informer unconditionally at boot. Running pods kept serving from their already-loaded routing tables and logged secrets is forbidden continuously for more than three hours. Someone noticed the log spam and opened a PR proposing the right fix (a namespaced Role), but it was closed in favor of a PR that suppressed the log line. That PR’s own deploy rolled the pods, and every public host started returning 404.
Lessons that generalize:
- Deleting a signal changes the system’s safety properties. Review it as one. Any PR that silences a warning should have to answer: “What happens if the underlying condition is real?”
- Permission changes can be latent. Applying RBAC doesn’t restart dependents. Cause and effect ended up separated in time, in authorship, and across three PRs.
- The rollback was armed and pointed at a sensor that couldn’t see the failure.
atomicHelm upgrades,maxUnavailable: 0,minReadySeconds: 30were all present. All of them gate on Readiness, and Readiness was wired to an internal/pingthat returns 200 whether or not any routes loaded. The fix was to make readiness traverse the router table. That was the only action item that didn’t depend on a human reading a signal correctly. - Halves of tickets ship. The original hardening ticket said “drop cluster-wide access and replace it with a namespaced Role.” Only the removal shipped. The gap between intent and implementation was the deeper root cause.
4.3 Documentation ships in a day, behavior changes don’t
The same post-mortem’s action-item table, reviewed four days later, read: five of the ten shipped within a day of the incident, and all five are documentation, log plumbing or a probe. The five still open are every item that changes how the system behaves under the same fault. That pattern is close to universal. Process and docs items are easy to finish, while Prevent items need design and verification and stall.
Countermeasures:
- Sort the action-item table by type and report Prevent/Mitigate closure separately from Detect/Process.
- Put a status-as-of date column in the post-mortem’s action table and refresh it at a fixed interval (one week, then one month), so stalled rows are visible in the canonical doc.
- Google’s workbook recommends at least one high-priority bug per user-affecting post-mortem, tracking closure centrally, and occasional “FixIt” weeks that reward closing items rather than writing post-mortems [5].
4.4 “Done” in the tracker is not “fixed” in the system
A security finding about a CI credential’s scope was closed as Done via a docs-only PR and stayed open in fact for weeks, until a later ticket fixed half of it. Another ticket’s alert half was closed on the log-unsuppression PR, even though that PR said “prerequisite for the alert, which lands separately.” In miniature, that is the same failure as the incident: the signal was restored and the thing that reads it was never built.
Rules:
- Before trusting a “fixed” claim, grep for the fix. Look at the config, the workflow, the live API. Ticket status is not evidence.
- Each action item should have a verifiable end state (“readiness returns 503 when the router count is 0, verified by a deliberate pod restart on dev”), not “improve X.” Google’s workbook makes the same point about measurability [5].
- When a fix is partial, record what remains in the ticket and in the post-mortem table. Don’t let the tracker round “partial” up to “done.”
4.5 Target-state docs read as current state
A post-mortem on a cache that lost all its data found that the architecture doc already mandated a primary and replica across AZs, noeviction with any eviction alerting at SEV1, daily snapshots and a quarterly restore drill. Production had none of these. It was a single node with the default eviction policy, zero snapshot retention and no memory alert. The doc “was written as target state and never reconciled against the real deployment, so the gap was invisible.” The same incident disproved a runbook claim that the store ran noeviction, since eviction counters showed it was active, and a belief that “config self-rebuilds from traffic”, which only held when a user reopened the UI.
Lessons:
- Label target state as target state, in the heading, or keep it in a separate design doc. An architecture doc mixing “is” and “should be” will be read as “is.”
- Write the claim with its evidence: “verified via
describe-replication-groups,<date>.” A claim without a date or source is a hypothesis. - Self-healing claims are the most dangerous claims. They decide whether a responder acts or waits. Test them, or phrase them as “expected, unverified.”
4.6 Silenced alerts take neighbors with them
An RPC provider’s fail-rate alert had been paused because a different, known limitation tripped it on every new address registration. When the provider’s plan quota ran out and it started returning 429s, the pause also silenced the quota signal. The application turned the transient 429 into a terminal ACCOUNT_CREATION_FAILED state, and on-chain-confirmed deposits were permanently failed until someone retried them by hand. The team learned about it from a downstream partner. Then mitigation stalled, because no on-call engineer had access to the vendor dashboard to raise the limit.
Lessons:
- A paused alert needs an owner, a reason and an expiry date, the same as any “temporary” (§4.10).
- Audit vendor-console access as part of on-call readiness. “Who can raise our RPC quota at 3am?” belongs in the runbook’s access section.
- In funds-moving code, classify errors as transient or terminal deliberately. A rate limit is never terminal.
4.7 Appended corrections become contradictions
A platform’s agent instruction file once said, in one bullet, that observability ran on an EC2 instance. A later bullet added that it had moved to EKS. Both read as current for months. The fix became the first lines of the file:
When a fact changes, replace the claim — never append a correction next to it. “X is now Y (this file used to say Z)” is how contradictions survive: a new bullet is added while the stale one stays, and both read as current. State only what is true now.
There is one deliberate exception, and it’s as important as the rule. Keep negative facts about the world: “there is no observability EC2 any more”, “the worker_nonce_resync_total counter has zero series”, “the ingress controller isn’t scraped at all (its metrics values block is inert)”, “the internal-DB tunnel port is declared and never used.” These aren’t changelog entries. They stop someone, human or agent, spending an hour looking for something that was never there. Changelog-style history (“we used to…”) gets deleted. Facts of absence stay.
4.8 “Not observed” read as “not used”
A trace-derived service graph showed no edge between two services. The edge was the most critical one in the system: the RPC proxy had tracing deliberately off. Separately, any query window that reached back before a decommission included traffic from a deleted environment, which the metric couldn’t distinguish from prod because it had no environment label at the time. Write these traps into the service map beside the column they affect (§2.7), and into the observability doc for anyone writing queries.
4.9 Section renumbering and metadata drift
Renumbering a runbook to “clean up gaps” breaks every alert link at once, and nothing notices until someone clicks one during an incident. The defense is the “never renumber” rule plus the resolution check. Subtler drift is the per-section metadata (UID, severity, for, threshold) falling behind the Terraform. Schedule a periodic diff, or generate those lines.
4.10 “Temporary” is permanent
An independent review of one platform listed 13 redundant IAM policies still marked “removed in a follow-up”, along with paused alerts, provisional RTOs and half-finished migrations. Its recommendation: grep -rn TEMPORAR (and TODO(remove, follow-up, stopgap) should produce a tracked list with owners and dates, reviewed like any other backlog. A cheap version is a CI job that fails when the count of such markers rises without a matching ledger entry.
4.11 Agents as operators: new failure modes
- Agents act on stale claims with full confidence. An agent reading “ArgoCD never retries a failed sync” will tell an operator to sync manually when, in fact, retries and prod self-heal had landed weeks earlier. The review that caught this listed several such load-bearing stale claims, including “prod pins digests” (it pinned 7-character tags) and a directory tree that was never built.
- Agents can ignore “research only.” Under retry or usage-limit pressure, a delegated agent told to only read has been seen making real commits and opening PRs. Treat instructions as context, not enforcement. Anything that must not happen needs a permission rule or a hook [20]. Audit branch, status and PR state after any batch of agent work.
- Agents trust a green check. Say what each check doesn’t verify in the instruction file (§2.6), or the agent will report “service map verified” after a coverage check.
5. Writing docs for humans and agents
AGENTS.md has become a common convention: “a README for agents” holding the context and instructions that would clutter human docs [19]. Claude Code loads CLAUDE.md (and can read AGENTS.md) at the start of every session, recommends keeping each file under about 200 lines for adherence, and recommends moving multi-step procedures into skills or path-scoped rules that load on demand [20]. Mature platforms tend to converge on this structure:
1. Editing rules first. Replace, don’t append; keep negative facts (§4.7). These rules govern every later line, so they go at the top.
2. A routing table, not content. For example, “read docs/alert-authoring.md before writing or reviewing any rule in alert_rules*.tf” and “read docs/service-map.md before answering any ‘what calls X’ question.” The pointer is what the agent needs every session. The detail loads when it’s relevant.
3. “Docs that must change with the code.” This table makes co-change explicit and states which rows CI enforces:
| If your change touches | Also update | Enforced? |
|---|---|---|
| a new or removed chart | service map, with (f)/(d) impact | ✅ coverage |
| NetworkPolicy ingress | service map internal-calls block (regenerate) | ✅ block regen |
| ingress routes | service map routes (regen) + architecture ingress tree (by hand) | ✅ partial |
| a new external vendor key | service map External APIs, with impact if the vendor is down | ❌ |
| decommissioning a service | both docs including diagrams, then grep -ri '<name>' docs/ | ❌ |
| alert rules | runbook section | ✅ |
| an SLO target | 5 further hand-duplicated places (listed) | ❌ |
| DNS / certificates | DNS doc; re-probe CA issuers before changing CAA | ❌ |
The ❌ rows matter as much as the ✅ rows. They tell the reader (or agent) exactly where vigilance is on them. One decommission left a diagram describing a dead service for three days, which is why “including the diagrams” is in bold.
4. Key facts as gotchas, each with the mechanism. “ArgoCD retries a failed sync 5 times, then stays stuck” is fine as a rule. It becomes useful when it carries why (auto-sync skips a revision whose previous attempt failed), how you’d notice (OutOfSync while health stays Healthy), what pages (prod only, after 30 minutes) and what doesn’t fix it (a hard refresh clears the error but doesn’t converge). An agent can act on a rule that comes with its mechanism. A bare rule gets misapplied at the edges.
5. Multi-merge orderings stated as procedures. The most expensive gotchas are transitions that no plan or diff shows. A new alert and the scrape job feeding it must be two merges, rule second, or the rule pages on NoData before its metric exists. Moving values between files needs two merges because the file list and the file contents apply on different schedules. Write these as numbered orders with a verification step between merges. A clean plan and an empty rendered diff are not evidence for this class of change, because both describe the end state, not the transition.
6. Persistent agent memory, kept honest. If agents keep their own notes across sessions, apply the same editing rules to them. Index entries should be one line each, superseded memories should be marked superseded or deleted, and “Done ≠ verified” belongs there as a standing rule.
6. Best-practice checklist
Day 1 (seed, < 10 engineers)
-
incident-playbook.md: anyone declares; 5-minute declare trigger; chat-channel naming convention; copy-paste opening message; declarer is IC until an explicit handoff. - Severity table tied to your symptoms, with funds-at-risk = SEV1 and misleading signatures (edge 404s) named.
- Comms cadence table (internal every 15 min; status page within 15 min for SEV1) and status-page content rules.
- Resolution requires metric/log/E2E evidence posted in-channel.
- Security lead reachable through three documented paths (pager responder, new incident, phone).
-
runbook.mdwith numbered, symptom-titled sections; every alert carries arunbook_urlto a section anchor; “never renumber” written in its header. - CI touch check: alert rules can’t change without a runbook diff.
- Post-mortem template with TTD/TTA/TTM/TTR, quantified impact with a funds line, trigger vs. root cause, typed action items, and a got-lucky column; SEV1/2 required, 5 business days.
- Agent instruction file starting with the replace-don’t-append rule and a docs routing table.
- On-chain pause reachable by the hot key and the owner; unpause owner-only.
First year (Series A)
- Anchor-resolution check in CI.
- Service map with (f)/(d) column, name-resolution table, and a generated inventory from enforced NetworkPolicy and ingress config; coverage + prose co-change checks; the checker’s docstring states its limits.
- Section-reading tool for the runbook, referenced in the agent instruction file.
- Customer-facing post-mortem twin with a sign-off PR and a pre-send checklist.
- Action-item status-as-of column refreshed at 1 week and 1 month; Prevent closure reported separately.
- Post-mortem compliance query (SEV1/2 incidents with no linked post-mortem after 5 business days).
- Vendor-console access audit in the on-call readiness checklist.
- Break-glass playbooks per funds/data scenario, each with “last drilled” (honest “never” allowed), irreversibility gates and a verification checklist; SEAL 911 and partner/exchange contacts in the escalation list.
- Paused-alert and “temporary” ledger with owners and expiry dates.
At scale (multiple platform owners, auditors)
- Per-section runbook metadata generated from or checked against alert rules.
- Quarterly claim audit: sample load-bearing claims (“X is configured as Y”) from the architecture doc, agent file and runbook, verify each against config or the live API, and replace any that fail.
- Separate IC rotation; regular game-days and tabletop exercises, including a key-compromise tabletop [4][16].
- Incident platform or AWS Incident Manager with structured timelines, if volume justifies it [7].
- Docs size budget: when a reference doc passes ~60–80 KB, split off what nothing deep-links to.
- Agent permission rules/hooks enforcing what instruction files only ask for.
7. Maturity roadmap
| Level | Incident process | Runbooks | Service map | Post-mortems | Docs integrity |
|---|---|---|---|---|---|
| 0 — Ad hoc | Whoever notices fixes it in the alerts channel | Tribal knowledge; a few wiki pages | Architecture diagram from launch | None, or a Slack thread | None |
| 1 — Written | Playbook exists; severity table; declare trigger | One file, per-alert sections; alerts link to it | Hand-written dependency table | Template; SEV1 required | Touch check on alert changes |
| 2 — Enforced | Declaration happens in big incidents (verified in post-mortems); comms cadence held | Stable anchors; resolution check; section tool; “why it misleads” sections | (f)/(d) + name resolution + generated inventory + coverage CI | SEV1/2 within 5 days, compliance tracked; customer twin | Replace-don’t-append; negative facts; co-change table with enforced/unenforced rows |
| 3 — Verified | IC rotation; drills; break-glass playbooks drilled with logged results | Metadata generated from rules; runbook steps partially automated (SSM Automation, scripts) [6] | Prose co-change check; observed-vs-declared reconciliation with second-source checks | Action-item closure measured by type; Prevent items have verifiable end states | Quarterly claim audit; “temporary” ledger; agent hooks enforce the critical rules |
Most teams can get from Level 0 to Level 2 in a quarter with one engineer spending part of their time on it. The Level 3 investments are the ones that stop the documentation drifting back. Two observations from the review that prompted this guide: the docs culture was “unusually honest” (it stated plainly when something had never been drilled), and documentation still “drifts more slowly than the code but still drifts, and the drift is in load-bearing claims.” Mechanical checks cover the claims that can be checked mechanically, such as anchors, counts and versions. A scheduled human claim audit covers the rest.
8. References
- Google, Site Reliability Engineering, ch. 14 “Managing Incidents.” https://sre.google/sre-book/managing-incidents/
- Google, Site Reliability Engineering, ch. 15 “Postmortem Culture: Learning from Failure.” https://sre.google/sre-book/postmortem-culture/
- Google, Site Reliability Engineering, ch. 11 “Being On-Call.” https://sre.google/sre-book/being-on-call/
- Google, The Site Reliability Workbook, ch. 9 “Incident Response.” https://sre.google/workbook/incident-response/
- Google, The Site Reliability Workbook, ch. 10 “Postmortem Culture: Learning from Failure.” https://sre.google/workbook/postmortem-culture/
- AWS Well-Architected Framework, OPS07-BP03 “Use runbooks to perform procedures.” https://docs.aws.amazon.com/wellarchitected/latest/framework/ops_ready_to_support_use_runbooks.html
- AWS Well-Architected Framework, OPS10-BP02 “Have a process per alert.” https://docs.aws.amazon.com/wellarchitected/latest/operational-excellence-pillar/ops_event_response_process_per_alert.html
- AWS Well-Architected Framework, OPS11-BP02 “Perform post-incident analysis.” https://docs.aws.amazon.com/wellarchitected/latest/framework/ops_evolve_ops_perform_rca_process.html
- AWS, AWS Security Incident Response Guide. https://docs.aws.amazon.com/whitepapers/latest/aws-security-incident-response-guide/introduction.html
- NIST SP 800-61 Rev. 3, Incident Response Recommendations and Considerations for Cybersecurity Risk Management: A CSF 2.0 Community Profile (April 2025). https://csrc.nist.gov/pubs/sp/800/61/r3/final
- PagerDuty, Incident Response Documentation (roles, severity, post-mortems). https://response.pagerduty.com/
- J. Allspaw, “Blameless PostMortems and a Just Culture,” Etsy Code as Craft (2012). https://www.etsy.com/codeascraft/blameless-postmortems/
- Grafana Labs, “Annotations and labels” (alert rule
runbook_urlannotation). https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/annotation-label/ - DORA, “Capabilities: Documentation quality.” https://dora.dev/capabilities/documentation-quality/
- Thoughtworks Technology Radar, “Diagrams as code.” https://www.thoughtworks.com/en-us/radar/techniques/diagrams-as-code
- Trail of Bits, “Can you pass the Rekt test?” (2023). https://blog.trailofbits.com/2023/08/14/can-you-pass-the-rekt-test/
- Security Alliance (SEAL), SEAL 911 emergency response. https://securityalliance.org/
- OpenZeppelin Contracts 5.x, Utilities:
Pausable. https://docs.openzeppelin.com/contracts/5.x/api/utils#Pausable - AGENTS.md, “A simple, open format for guiding coding agents.” https://agents.md/
- Anthropic, Claude Code docs, “How Claude remembers your project” (CLAUDE.md, AGENTS.md, rules, hooks vs. instructions). https://code.claude.com/docs/en/memory