feat: post-mortem automation pipeline
E2E workflow for incident post-mortems: 1. /post-mortem skill generates structured post-mortem markdown 2. Woodpecker pipeline triggers on docs/post-mortems/*.md changes 3. parse-postmortem-todos.sh extracts safe TODOs (Alert/Config/Monitor) 4. postmortem-todo-resolver agent implements TODOs headlessly 5. Agent updates post-mortem with Follow-up Implementation table Components: - .claude/skills/post-mortem/ — writer skill + template - .claude/agents/postmortem-todo-resolver.md — headless agent - .woodpecker/postmortem-todos.yml — CI pipeline - scripts/parse-postmortem-todos.sh — TODO extractor - cluster-health skill — auto-suggest post-mortem after recovery Safety: only auto-implements Alert/Config/Monitor types. Architecture/Migration/Investigation items are skipped. [ci skip] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e832581caf
commit
8badb8181a
6 changed files with 406 additions and 0 deletions
89
scripts/parse-postmortem-todos.sh
Executable file
89
scripts/parse-postmortem-todos.sh
Executable file
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env bash
|
||||
# parse-postmortem-todos.sh — Extract auto-implementable TODOs from a post-mortem markdown file
|
||||
# Usage: bash scripts/parse-postmortem-todos.sh docs/post-mortems/2026-04-14-foo.md
|
||||
# Output: JSON with file path and list of TODOs
|
||||
#
|
||||
# Supports two table formats:
|
||||
# New: | Priority | Action | Type | Details | Status |
|
||||
# Old: | Action | Status | Details | (infers type from action text)
|
||||
set -euo pipefail
|
||||
|
||||
PM_FILE="${1:?Usage: $0 <post-mortem.md>}"
|
||||
|
||||
if [ ! -f "$PM_FILE" ]; then
|
||||
echo '{"file": "", "todos": [], "error": "File not found"}' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 -c "
|
||||
import re, json, sys
|
||||
|
||||
pm_file = sys.argv[1]
|
||||
with open(pm_file) as f:
|
||||
content = f.read()
|
||||
|
||||
safe_types = {'Alert', 'Config', 'Monitor'}
|
||||
|
||||
todos = []
|
||||
|
||||
# Format 1 (new template): | Priority | Action | Type | Details | Status |
|
||||
pattern_new = r'\|\s*(P[0-3])\s*\|\s*(.+?)\s*\|\s*(\w+)\s*\|\s*(.+?)\s*\|\s*TODO\s*\|'
|
||||
for priority, action, todo_type, details in re.findall(pattern_new, content):
|
||||
todos.append({
|
||||
'priority': priority.strip(),
|
||||
'action': action.strip(),
|
||||
'type': todo_type.strip(),
|
||||
'details': details.strip(),
|
||||
'safe': todo_type.strip() in safe_types
|
||||
})
|
||||
|
||||
# Format 2 (old): | Action | TODO/Done | Details | or | Action | Owner | Status |
|
||||
# Look for rows with TODO in any column
|
||||
if not todos:
|
||||
pattern_old = r'\|\s*(.+?)\s*\|\s*TODO\s*\|\s*(.+?)\s*\|'
|
||||
for action, details in re.findall(pattern_old, content):
|
||||
action = action.strip()
|
||||
details = details.strip()
|
||||
# Skip header rows and clean up leading pipes
|
||||
if action.startswith('--') or action.lower() == 'action':
|
||||
continue
|
||||
action = action.lstrip('| ').strip()
|
||||
# Infer type from action text
|
||||
action_lower = action.lower()
|
||||
if any(kw in action_lower for kw in ['prometheusrule', 'alert', 'alerting']):
|
||||
todo_type = 'Alert'
|
||||
elif any(kw in action_lower for kw in ['uptime kuma', 'monitor', 'ping', 'tcp check']):
|
||||
todo_type = 'Monitor'
|
||||
elif any(kw in action_lower for kw in ['config', 'manage', 'add.*option', 'document', 'nfs.conf']):
|
||||
todo_type = 'Config'
|
||||
elif any(kw in action_lower for kw in ['migrate', 'move']):
|
||||
todo_type = 'Migration'
|
||||
elif any(kw in action_lower for kw in ['review', 'investigate', 'verify']):
|
||||
todo_type = 'Investigation'
|
||||
else:
|
||||
todo_type = 'Config' # default to Config for ambiguous items
|
||||
|
||||
# Infer priority from section header context
|
||||
priority = 'P2' # default
|
||||
todos.append({
|
||||
'priority': priority,
|
||||
'action': action,
|
||||
'type': todo_type,
|
||||
'details': details,
|
||||
'safe': todo_type in safe_types
|
||||
})
|
||||
|
||||
safe_todos = [t for t in todos if t['safe']]
|
||||
unsafe_todos = [t for t in todos if not t['safe']]
|
||||
|
||||
result = {
|
||||
'file': pm_file,
|
||||
'todos': safe_todos,
|
||||
'skipped': unsafe_todos,
|
||||
'total_todos_in_doc': len(todos),
|
||||
'safe_todos': len(safe_todos),
|
||||
'skipped_todos': len(unsafe_todos)
|
||||
}
|
||||
|
||||
print(json.dumps(result, indent=2))
|
||||
" "$PM_FILE"
|
||||
Loading…
Add table
Add a link
Reference in a new issue