shipwright-cli 1.9.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.claude/hooks/post-tool-use.sh +12 -5
  2. package/README.md +114 -36
  3. package/completions/_shipwright +212 -32
  4. package/completions/shipwright.bash +97 -25
  5. package/docs/strategy/01-market-research.md +619 -0
  6. package/docs/strategy/02-mission-and-brand.md +587 -0
  7. package/docs/strategy/03-gtm-and-roadmap.md +759 -0
  8. package/docs/strategy/QUICK-START.txt +289 -0
  9. package/docs/strategy/README.md +172 -0
  10. package/package.json +4 -2
  11. package/scripts/sw +217 -2
  12. package/scripts/sw-activity.sh +500 -0
  13. package/scripts/sw-adaptive.sh +925 -0
  14. package/scripts/sw-adversarial.sh +1 -1
  15. package/scripts/sw-architecture-enforcer.sh +1 -1
  16. package/scripts/sw-auth.sh +613 -0
  17. package/scripts/sw-autonomous.sh +664 -0
  18. package/scripts/sw-changelog.sh +704 -0
  19. package/scripts/sw-checkpoint.sh +79 -1
  20. package/scripts/sw-ci.sh +602 -0
  21. package/scripts/sw-cleanup.sh +192 -7
  22. package/scripts/sw-code-review.sh +637 -0
  23. package/scripts/sw-connect.sh +1 -1
  24. package/scripts/sw-context.sh +605 -0
  25. package/scripts/sw-cost.sh +1 -1
  26. package/scripts/sw-daemon.sh +812 -138
  27. package/scripts/sw-dashboard.sh +1 -1
  28. package/scripts/sw-db.sh +540 -0
  29. package/scripts/sw-decompose.sh +539 -0
  30. package/scripts/sw-deps.sh +551 -0
  31. package/scripts/sw-developer-simulation.sh +1 -1
  32. package/scripts/sw-discovery.sh +412 -0
  33. package/scripts/sw-docs-agent.sh +539 -0
  34. package/scripts/sw-docs.sh +1 -1
  35. package/scripts/sw-doctor.sh +59 -1
  36. package/scripts/sw-dora.sh +615 -0
  37. package/scripts/sw-durable.sh +710 -0
  38. package/scripts/sw-e2e-orchestrator.sh +535 -0
  39. package/scripts/sw-eventbus.sh +393 -0
  40. package/scripts/sw-feedback.sh +471 -0
  41. package/scripts/sw-fix.sh +1 -1
  42. package/scripts/sw-fleet-discover.sh +567 -0
  43. package/scripts/sw-fleet-viz.sh +404 -0
  44. package/scripts/sw-fleet.sh +8 -1
  45. package/scripts/sw-github-app.sh +596 -0
  46. package/scripts/sw-github-checks.sh +1 -1
  47. package/scripts/sw-github-deploy.sh +1 -1
  48. package/scripts/sw-github-graphql.sh +1 -1
  49. package/scripts/sw-guild.sh +569 -0
  50. package/scripts/sw-heartbeat.sh +1 -1
  51. package/scripts/sw-hygiene.sh +559 -0
  52. package/scripts/sw-incident.sh +617 -0
  53. package/scripts/sw-init.sh +88 -1
  54. package/scripts/sw-instrument.sh +699 -0
  55. package/scripts/sw-intelligence.sh +1 -1
  56. package/scripts/sw-jira.sh +1 -1
  57. package/scripts/sw-launchd.sh +366 -31
  58. package/scripts/sw-linear.sh +1 -1
  59. package/scripts/sw-logs.sh +1 -1
  60. package/scripts/sw-loop.sh +507 -51
  61. package/scripts/sw-memory.sh +198 -3
  62. package/scripts/sw-mission-control.sh +487 -0
  63. package/scripts/sw-model-router.sh +545 -0
  64. package/scripts/sw-otel.sh +596 -0
  65. package/scripts/sw-oversight.sh +689 -0
  66. package/scripts/sw-pipeline-composer.sh +8 -8
  67. package/scripts/sw-pipeline-vitals.sh +1096 -0
  68. package/scripts/sw-pipeline.sh +2451 -180
  69. package/scripts/sw-pm.sh +693 -0
  70. package/scripts/sw-pr-lifecycle.sh +522 -0
  71. package/scripts/sw-predictive.sh +1 -1
  72. package/scripts/sw-prep.sh +1 -1
  73. package/scripts/sw-ps.sh +4 -3
  74. package/scripts/sw-public-dashboard.sh +798 -0
  75. package/scripts/sw-quality.sh +595 -0
  76. package/scripts/sw-reaper.sh +5 -3
  77. package/scripts/sw-recruit.sh +573 -0
  78. package/scripts/sw-regression.sh +642 -0
  79. package/scripts/sw-release-manager.sh +736 -0
  80. package/scripts/sw-release.sh +706 -0
  81. package/scripts/sw-remote.sh +1 -1
  82. package/scripts/sw-replay.sh +520 -0
  83. package/scripts/sw-retro.sh +691 -0
  84. package/scripts/sw-scale.sh +444 -0
  85. package/scripts/sw-security-audit.sh +505 -0
  86. package/scripts/sw-self-optimize.sh +109 -8
  87. package/scripts/sw-session.sh +31 -9
  88. package/scripts/sw-setup.sh +1 -1
  89. package/scripts/sw-standup.sh +712 -0
  90. package/scripts/sw-status.sh +192 -1
  91. package/scripts/sw-strategic.sh +658 -0
  92. package/scripts/sw-stream.sh +450 -0
  93. package/scripts/sw-swarm.sh +583 -0
  94. package/scripts/sw-team-stages.sh +511 -0
  95. package/scripts/sw-templates.sh +1 -1
  96. package/scripts/sw-testgen.sh +515 -0
  97. package/scripts/sw-tmux-pipeline.sh +554 -0
  98. package/scripts/sw-tmux.sh +1 -1
  99. package/scripts/sw-trace.sh +485 -0
  100. package/scripts/sw-tracker-github.sh +188 -0
  101. package/scripts/sw-tracker-jira.sh +172 -0
  102. package/scripts/sw-tracker-linear.sh +251 -0
  103. package/scripts/sw-tracker.sh +117 -2
  104. package/scripts/sw-triage.sh +603 -0
  105. package/scripts/sw-upgrade.sh +1 -1
  106. package/scripts/sw-ux.sh +677 -0
  107. package/scripts/sw-webhook.sh +627 -0
  108. package/scripts/sw-widgets.sh +530 -0
  109. package/scripts/sw-worktree.sh +1 -1
  110. package/templates/pipelines/autonomous.json +8 -1
  111. package/templates/pipelines/cost-aware.json +21 -0
  112. package/templates/pipelines/deployed.json +40 -6
  113. package/templates/pipelines/enterprise.json +16 -2
  114. package/templates/pipelines/fast.json +19 -0
  115. package/templates/pipelines/full.json +16 -2
  116. package/templates/pipelines/hotfix.json +19 -0
  117. package/templates/pipelines/standard.json +19 -0
@@ -0,0 +1,617 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright incident — Autonomous Incident Detection & Response ║
4
+ # ║ Detect failures · Triage · Root cause analysis · Auto-remediate ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ set -euo pipefail
7
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
+
9
+ VERSION="2.0.0"
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
+
13
+ # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
14
+ CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
15
+ PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
16
+ BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
17
+ GREEN='\033[38;2;74;222;128m' # success
18
+ YELLOW='\033[38;2;250;204;21m' # warning
19
+ RED='\033[38;2;248;113;113m' # error
20
+ DIM='\033[2m'
21
+ BOLD='\033[1m'
22
+ RESET='\033[0m'
23
+
24
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
25
+ # shellcheck source=lib/compat.sh
26
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
27
+
28
+ # ─── Output Helpers ─────────────────────────────────────────────────────────
29
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
30
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
31
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
32
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
33
+
34
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
35
+ now_epoch() { date +%s; }
36
+
37
+ format_duration() {
38
+ local secs="$1"
39
+ if [[ "$secs" -ge 3600 ]]; then
40
+ printf "%dh %dm %ds" $((secs/3600)) $((secs%3600/60)) $((secs%60))
41
+ elif [[ "$secs" -ge 60 ]]; then
42
+ printf "%dm %ds" $((secs/60)) $((secs%60))
43
+ else
44
+ printf "%ds" "$secs"
45
+ fi
46
+ }
47
+
48
+ # ─── Structured Event Log ──────────────────────────────────────────────────
49
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
50
+
51
+ emit_event() {
52
+ local event_type="$1"
53
+ shift
54
+ local json_fields=""
55
+ for kv in "$@"; do
56
+ local key="${kv%%=*}"
57
+ local val="${kv#*=}"
58
+ if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
59
+ json_fields="${json_fields},\"${key}\":${val}"
60
+ else
61
+ val="${val//\"/\\\"}"
62
+ json_fields="${json_fields},\"${key}\":\"${val}\""
63
+ fi
64
+ done
65
+ mkdir -p "${HOME}/.shipwright"
66
+ echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
67
+ }
68
+
69
+ # ─── State Directories ──────────────────────────────────────────────────────
70
+ INCIDENTS_DIR="${HOME}/.shipwright/incidents"
71
+ INCIDENT_CONFIG="${INCIDENTS_DIR}/config.json"
72
+ MONITOR_PID_FILE="${INCIDENTS_DIR}/monitor.pid"
73
+
74
+ ensure_incident_dir() {
75
+ mkdir -p "$INCIDENTS_DIR"
76
+ [[ -f "$INCIDENT_CONFIG" ]] || cat > "$INCIDENT_CONFIG" << 'EOF'
77
+ {
78
+ "auto_response_enabled": true,
79
+ "p0_auto_hotfix": true,
80
+ "p1_auto_hotfix": false,
81
+ "auto_rollback_enabled": false,
82
+ "notification_channels": ["stdout"],
83
+ "severity_thresholds": {
84
+ "p0_impact_count": 3,
85
+ "p0_deploy_failure": true,
86
+ "p1_test_regression_count": 5,
87
+ "p1_pipeline_failure_rate": 0.3
88
+ },
89
+ "root_cause_patterns": {
90
+ "timeout_keywords": ["timeout", "deadline", "too slow"],
91
+ "memory_keywords": ["out of memory", "OOM", "heap"],
92
+ "dependency_keywords": ["dependency", "import", "require", "not found"],
93
+ "auth_keywords": ["auth", "permission", "forbidden", "401", "403"]
94
+ }
95
+ }
96
+ EOF
97
+ }
98
+
99
+ # ─── Failure Detection ──────────────────────────────────────────────────────
100
+
101
+ detect_pipeline_failures() {
102
+ local since="${1:-3600}" # Last N seconds
103
+ local cutoff_time=$(($(now_epoch) - since))
104
+
105
+ [[ ! -f "$EVENTS_FILE" ]] && return 0
106
+
107
+ awk -v cutoff="$cutoff_time" -F'"' '
108
+ BEGIN { count=0 }
109
+ /pipeline\.failed|stage\.failed|test\.failed|deploy\.failed/ {
110
+ for (i=1; i<=NF; i++) {
111
+ if ($i ~ /ts_epoch/) {
112
+ ts_epoch_val=$(i+2)
113
+ gsub(/^[^0-9]*/, "", ts_epoch_val)
114
+ gsub(/[^0-9].*/, "", ts_epoch_val)
115
+ if (ts_epoch_val+0 > cutoff) {
116
+ print $0
117
+ count++
118
+ }
119
+ }
120
+ }
121
+ }
122
+ END { exit (count > 0 ? 0 : 1) }
123
+ ' "$EVENTS_FILE"
124
+ }
125
+
126
+ get_recent_failures() {
127
+ local since="${1:-3600}"
128
+ local cutoff_time=$(($(now_epoch) - since))
129
+
130
+ [[ ! -f "$EVENTS_FILE" ]] && echo "[]" && return 0
131
+
132
+ jq -s --arg cutoff "$cutoff_time" '
133
+ map(
134
+ select(
135
+ (.ts_epoch | tonumber) > ($cutoff | tonumber) and
136
+ (.type | contains("failed") or contains("error") or contains("timeout"))
137
+ ) |
138
+ {
139
+ ts: .ts,
140
+ ts_epoch: .ts_epoch,
141
+ type: .type,
142
+ issue: .issue,
143
+ stage: .stage,
144
+ reason: .reason,
145
+ error: .error
146
+ }
147
+ )
148
+ ' "$EVENTS_FILE" 2>/dev/null || echo "[]"
149
+ }
150
+
151
+ # ─── Severity Classification ───────────────────────────────────────────────
152
+
153
+ classify_severity() {
154
+ local failure_type="$1"
155
+ local impact_scope="$2" # Number of affected resources
156
+
157
+ case "$failure_type" in
158
+ deploy.failed|pipeline.critical_error)
159
+ echo "P0"
160
+ ;;
161
+ test.regression|stage.failed)
162
+ if [[ "$impact_scope" -gt 5 ]]; then
163
+ echo "P0"
164
+ else
165
+ echo "P1"
166
+ fi
167
+ ;;
168
+ stage.timeout|health_check.failed)
169
+ echo "P2"
170
+ ;;
171
+ *)
172
+ echo "P3"
173
+ ;;
174
+ esac
175
+ }
176
+
177
+ # ─── Root Cause Analysis ───────────────────────────────────────────────────
178
+
179
+ analyze_root_cause() {
180
+ local failure_log="$1"
181
+ local config="$2"
182
+
183
+ local timeout_hits error_hits memory_hits dependency_hits
184
+ timeout_hits=$(echo "$failure_log" | grep -ic "timeout\|deadline\|too slow" || echo "0")
185
+ memory_hits=$(echo "$failure_log" | grep -ic "out of memory\|OOM\|heap" || echo "0")
186
+ dependency_hits=$(echo "$failure_log" | grep -ic "dependency\|import\|require\|not found" || echo "0")
187
+ error_hits=$(echo "$failure_log" | grep -c . || echo "0")
188
+
189
+ if [[ "$timeout_hits" -gt 0 ]]; then
190
+ echo "Performance degradation: Timeout detected (${timeout_hits} occurrences)"
191
+ elif [[ "$memory_hits" -gt 0 ]]; then
192
+ echo "Memory pressure: OOM or heap allocation issue (${memory_hits} occurrences)"
193
+ elif [[ "$dependency_hits" -gt 0 ]]; then
194
+ echo "Dependency failure: Missing or incompatible dependency (${dependency_hits} occurrences)"
195
+ else
196
+ echo "Unknown cause: Check logs (${error_hits} error lines)"
197
+ fi
198
+ }
199
+
200
+ # ─── Incident Record Management ─────────────────────────────────────────────
201
+
202
+ create_incident_record() {
203
+ local incident_id="$1"
204
+ local severity="$2"
205
+ local root_cause="$3"
206
+ local failure_events="$4"
207
+
208
+ local incident_file="${INCIDENTS_DIR}/${incident_id}.json"
209
+ local created_at
210
+ created_at="$(now_iso)"
211
+
212
+ cat > "$incident_file" << EOF
213
+ {
214
+ "id": "$incident_id",
215
+ "created_at": "$created_at",
216
+ "severity": "$severity",
217
+ "status": "open",
218
+ "root_cause": "$root_cause",
219
+ "failure_events": $failure_events,
220
+ "timeline": [],
221
+ "remediation": null,
222
+ "resolved_at": null,
223
+ "mttr_seconds": null,
224
+ "post_mortem_url": null
225
+ }
226
+ EOF
227
+
228
+ emit_event "incident.created" "incident_id=$incident_id" "severity=$severity"
229
+ }
230
+
231
+ # ─── Hotfix Creation ───────────────────────────────────────────────────────
232
+
233
+ create_hotfix_issue() {
234
+ local incident_id="$1"
235
+ local severity="$2"
236
+ local root_cause="$3"
237
+
238
+ if ! command -v gh &>/dev/null; then
239
+ warn "gh CLI not found, skipping GitHub issue creation"
240
+ return 1
241
+ fi
242
+
243
+ local title="[HOTFIX] $severity: $root_cause"
244
+ local body="**Incident ID:** $incident_id
245
+ **Severity:** $severity
246
+ **Root Cause:** $root_cause
247
+
248
+ ## Timeline
249
+ See incident details: \`shipwright incident show $incident_id\`
250
+
251
+ ## Automated Detection
252
+ This issue was automatically created by the incident commander.
253
+ "
254
+
255
+ local issue_url
256
+ issue_url=$(gh issue create --title "$title" --body "$body" --label "hotfix" 2>/dev/null || echo "")
257
+
258
+ if [[ -n "$issue_url" ]]; then
259
+ success "Created hotfix issue: $issue_url"
260
+ echo "$issue_url"
261
+ return 0
262
+ fi
263
+
264
+ warn "Failed to create GitHub issue"
265
+ return 1
266
+ }
267
+
268
+ # ─── Watch Command ─────────────────────────────────────────────────────────
269
+
270
+ cmd_watch() {
271
+ local interval="${1:-60}"
272
+
273
+ if [[ -f "$MONITOR_PID_FILE" ]]; then
274
+ local old_pid
275
+ old_pid=$(cat "$MONITOR_PID_FILE" 2>/dev/null || echo "")
276
+ if [[ -n "$old_pid" ]] && kill -0 "$old_pid" 2>/dev/null; then
277
+ warn "Monitor already running with PID $old_pid"
278
+ return 1
279
+ fi
280
+ fi
281
+
282
+ info "Starting incident monitoring (interval: ${interval}s)"
283
+
284
+ # Background process
285
+ (
286
+ echo $$ > "$MONITOR_PID_FILE"
287
+ trap "rm -f '$MONITOR_PID_FILE'" EXIT
288
+
289
+ while true; do
290
+ sleep "$interval"
291
+
292
+ # Check for recent failures
293
+ local failures_json
294
+ failures_json=$(get_recent_failures "$interval")
295
+ local failure_count
296
+ failure_count=$(echo "$failures_json" | jq 'length')
297
+
298
+ if [[ "$failure_count" -gt 0 ]]; then
299
+ info "Detected $failure_count failure(s)"
300
+
301
+ # Generate incident
302
+ local incident_id
303
+ incident_id="inc-$(date +%s)"
304
+
305
+ local severity
306
+ severity=$(classify_severity "$(echo "$failures_json" | jq -r '.[0].type')" "$failure_count")
307
+
308
+ local root_cause
309
+ root_cause=$(analyze_root_cause "$(echo "$failures_json" | jq -r '.[0] | tostring')" "$INCIDENT_CONFIG")
310
+
311
+ create_incident_record "$incident_id" "$severity" "$root_cause" "$failures_json"
312
+
313
+ info "Incident $incident_id created (severity: $severity)"
314
+ emit_event "incident.detected" "incident_id=$incident_id" "severity=$severity"
315
+
316
+ # Auto-response for P0/P1
317
+ if [[ "$severity" == "P0" ]] || [[ "$severity" == "P1" ]]; then
318
+ local auto_hotfix
319
+ auto_hotfix=$(jq -r '.p0_auto_hotfix // .p1_auto_hotfix' "$INCIDENT_CONFIG" 2>/dev/null || echo "false")
320
+ if [[ "$auto_hotfix" == "true" ]]; then
321
+ create_hotfix_issue "$incident_id" "$severity" "$root_cause"
322
+ fi
323
+ fi
324
+ fi
325
+ done
326
+ ) &
327
+
328
+ success "Monitor started in background (PID: $!)"
329
+ }
330
+
331
+ # ─── List Command ──────────────────────────────────────────────────────────
332
+
333
+ cmd_list() {
334
+ local format="${1:-table}"
335
+
336
+ local incident_files
337
+ incident_files=$(find "$INCIDENTS_DIR" -name '*.json' -not -name '*postmortem*' -type f 2>/dev/null || true)
338
+
339
+ if [[ -z "$incident_files" ]]; then
340
+ info "No incidents recorded"
341
+ return 0
342
+ fi
343
+
344
+ case "$format" in
345
+ json)
346
+ echo "["
347
+ local first=true
348
+ while IFS= read -r incident_file; do
349
+ [[ -z "$incident_file" ]] && continue
350
+ if [[ "$first" == true ]]; then
351
+ first=false
352
+ else
353
+ echo ","
354
+ fi
355
+ cat "$incident_file"
356
+ done <<< "$incident_files"
357
+ echo "]"
358
+ ;;
359
+ *)
360
+ echo -e "${BOLD}Recent Incidents${RESET}"
361
+ echo -e "${DIM}────────────────────────────────────────────────────────────────${RESET}"
362
+
363
+ while IFS= read -r incident_file; do
364
+ [[ -z "$incident_file" ]] && continue
365
+
366
+ local id severity status cause
367
+ id=$(jq -r '.id // "unknown"' "$incident_file" 2>/dev/null || echo "unknown")
368
+ severity=$(jq -r '.severity // "P3"' "$incident_file" 2>/dev/null || echo "P3")
369
+ status=$(jq -r '.status // "open"' "$incident_file" 2>/dev/null || echo "open")
370
+ cause=$(jq -r '.root_cause // "unknown"' "$incident_file" 2>/dev/null || echo "unknown")
371
+ cause="${cause:0:50}"
372
+
373
+ case "$severity" in
374
+ P0) severity="${RED}${BOLD}$severity${RESET}" ;;
375
+ P1) severity="${YELLOW}${BOLD}$severity${RESET}" ;;
376
+ P2) severity="${BLUE}$severity${RESET}" ;;
377
+ *) severity="${DIM}$severity${RESET}" ;;
378
+ esac
379
+
380
+ printf "%-20s %s %-8s %s\n" "$id" "$severity" "$status" "$cause"
381
+ done <<< "$incident_files"
382
+ ;;
383
+ esac
384
+ }
385
+
386
+ # ─── Show Command ──────────────────────────────────────────────────────────
387
+
388
+ cmd_show() {
389
+ local incident_id="$1"
390
+ [[ -z "$incident_id" ]] && { error "Usage: shipwright incident show <incident_id>"; return 1; }
391
+
392
+ local incident_file="${INCIDENTS_DIR}/${incident_id}.json"
393
+ [[ ! -f "$incident_file" ]] && { error "Incident not found: $incident_id"; return 1; }
394
+
395
+ info "Incident: $incident_id"
396
+ echo ""
397
+
398
+ jq . "$incident_file" | while read -r line; do
399
+ echo " $line"
400
+ done
401
+ }
402
+
403
+ # ─── Report Command ────────────────────────────────────────────────────────
404
+
405
+ cmd_report() {
406
+ local incident_id="$1"
407
+ [[ -z "$incident_id" ]] && { error "Usage: shipwright incident report <incident_id>"; return 1; }
408
+
409
+ local incident_file="${INCIDENTS_DIR}/${incident_id}.json"
410
+ [[ ! -f "$incident_file" ]] && { error "Incident not found: $incident_id"; return 1; }
411
+
412
+ local incident
413
+ incident=$(jq . "$incident_file")
414
+
415
+ local report_file="${INCIDENTS_DIR}/${incident_id}-postmortem.md"
416
+
417
+ cat > "$report_file" << EOF
418
+ # Post-Incident Report
419
+ **Incident ID:** $incident_id
420
+ **Generated:** $(now_iso)
421
+
422
+ ## Summary
423
+ $(echo "$incident" | jq -r '.root_cause')
424
+
425
+ ## Timeline
426
+ EOF
427
+
428
+ echo "$incident" | jq -r '.failure_events[] | "- \(.ts): \(.type)"' >> "$report_file"
429
+
430
+ cat >> "$report_file" << EOF
431
+
432
+ ## Impact
433
+ - Severity: $(echo "$incident" | jq -r '.severity')
434
+ - Status: $(echo "$incident" | jq -r '.status')
435
+
436
+ ## Resolution
437
+ $(echo "$incident" | jq -r '.remediation // "Pending"')
438
+
439
+ ## Prevention
440
+ 1. Monitor for similar patterns
441
+ 2. Add alerting thresholds
442
+ 3. Improve automated detection
443
+ EOF
444
+
445
+ success "Report generated: $report_file"
446
+ echo "$report_file"
447
+ }
448
+
449
+ # ─── Stats Command ──────────────────────────────────────────────────────────
450
+
451
+ cmd_stats() {
452
+ local format="${1:-table}"
453
+
454
+ if [[ ! -d "$INCIDENTS_DIR" ]] || [[ -z "$(ls -1 "$INCIDENTS_DIR"/*.json 2>/dev/null | grep -v postmortem)" ]]; then
455
+ info "No incident data available"
456
+ return 0
457
+ fi
458
+
459
+ local total_incidents
460
+ total_incidents=$(ls -1 "$INCIDENTS_DIR"/*.json 2>/dev/null | grep -v postmortem | wc -l)
461
+
462
+ local incident_files
463
+ incident_files=$(find "$INCIDENTS_DIR" -name '*.json' -not -name '*postmortem*' -type f 2>/dev/null || true)
464
+ local p0_count p1_count p2_count p3_count resolved_count mttr_sum mttr_avg
465
+ p0_count=0
466
+ p1_count=0
467
+ p2_count=0
468
+ p3_count=0
469
+ resolved_count=0
470
+ mttr_sum=0
471
+
472
+ while IFS= read -r incident_file; do
473
+ [[ -z "$incident_file" ]] && continue
474
+ local sev status mttr
475
+ sev=$(jq -r '.severity // "P3"' "$incident_file" 2>/dev/null || echo "P3")
476
+ status=$(jq -r '.status // "open"' "$incident_file" 2>/dev/null || echo "open")
477
+ mttr=$(jq -r '.mttr_seconds // 0' "$incident_file" 2>/dev/null || echo "0")
478
+
479
+ case "$sev" in
480
+ P0) ((p0_count++)) ;;
481
+ P1) ((p1_count++)) ;;
482
+ P2) ((p2_count++)) ;;
483
+ *) ((p3_count++)) ;;
484
+ esac
485
+
486
+ if [[ "$status" == "resolved" ]]; then
487
+ ((resolved_count++))
488
+ mttr_sum=$((mttr_sum + mttr))
489
+ fi
490
+ done <<< "$incident_files"
491
+
492
+ mttr_avg=0
493
+ if [[ "$resolved_count" -gt 0 ]]; then
494
+ mttr_avg=$((mttr_sum / resolved_count))
495
+ fi
496
+
497
+ case "$format" in
498
+ json)
499
+ jq -n \
500
+ --arg total "$total_incidents" \
501
+ --arg p0 "$p0_count" \
502
+ --arg p1 "$p1_count" \
503
+ --arg p2 "$p2_count" \
504
+ --arg p3 "$p3_count" \
505
+ --arg resolved "$resolved_count" \
506
+ --arg mttr "$mttr_avg" \
507
+ '{
508
+ total: ($total | tonumber),
509
+ by_severity: {p0: ($p0 | tonumber), p1: ($p1 | tonumber), p2: ($p2 | tonumber), p3: ($p3 | tonumber)},
510
+ resolved: ($resolved | tonumber),
511
+ mttr_seconds: ($mttr | tonumber)
512
+ }'
513
+ ;;
514
+ *)
515
+ echo -e "${BOLD}Incident Statistics${RESET}"
516
+ echo -e "${DIM}────────────────────────────────────────────────────────────────${RESET}"
517
+ echo "Total Incidents: $total_incidents"
518
+ echo " P0 (Critical): $p0_count"
519
+ echo " P1 (High): $p1_count"
520
+ echo " P2 (Medium): $p2_count"
521
+ echo " P3 (Low): $p3_count"
522
+ echo ""
523
+ echo "Resolved: $resolved_count"
524
+ echo "MTTR (avg): $(format_duration "$mttr_avg")"
525
+ ;;
526
+ esac
527
+ }
528
+
529
+ # ─── Stop Command ──────────────────────────────────────────────────────────
530
+
531
+ cmd_stop() {
532
+ if [[ -f "$MONITOR_PID_FILE" ]]; then
533
+ local pid
534
+ pid=$(cat "$MONITOR_PID_FILE" 2>/dev/null || echo "")
535
+ if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
536
+ kill "$pid"
537
+ rm -f "$MONITOR_PID_FILE"
538
+ success "Monitor stopped (PID: $pid)"
539
+ else
540
+ warn "Monitor not running"
541
+ fi
542
+ else
543
+ warn "Monitor not running"
544
+ fi
545
+ }
546
+
547
+ # ─── Help Command ──────────────────────────────────────────────────────────
548
+
549
+ show_help() {
550
+ echo -e "${CYAN}${BOLD}shipwright incident${RESET} — Autonomous incident detection & response"
551
+ echo ""
552
+ echo -e "${BOLD}USAGE${RESET}"
553
+ echo -e " ${CYAN}shipwright incident${RESET} <command> [options]"
554
+ echo ""
555
+ echo -e "${BOLD}COMMANDS${RESET}"
556
+ echo -e " ${CYAN}watch${RESET} [interval] Start monitoring for incidents (default: 60s)"
557
+ echo -e " ${CYAN}stop${RESET} Stop incident monitoring"
558
+ echo -e " ${CYAN}list${RESET} [format] List recent incidents (table|json)"
559
+ echo -e " ${CYAN}show${RESET} <incident-id> Show details for an incident"
560
+ echo -e " ${CYAN}report${RESET} <incident-id> Generate post-mortem report"
561
+ echo -e " ${CYAN}stats${RESET} [format] Show incident statistics (table|json)"
562
+ echo -e " ${CYAN}config${RESET} <cmd> Configure incident response (show|set)"
563
+ echo -e " ${CYAN}help${RESET} Show this help"
564
+ echo ""
565
+ echo -e "${BOLD}EXAMPLES${RESET}"
566
+ echo -e " ${DIM}shipwright incident watch # Start monitoring${RESET}"
567
+ echo -e " ${DIM}shipwright incident list # Show all incidents${RESET}"
568
+ echo -e " ${DIM}shipwright incident show inc-1702 # Show incident details${RESET}"
569
+ echo -e " ${DIM}shipwright incident report inc-1702 # Generate post-mortem${RESET}"
570
+ echo -e " ${DIM}shipwright incident stats # Show MTTR and frequency${RESET}"
571
+ }
572
+
573
+ # ─── Main Router ───────────────────────────────────────────────────────────
574
+
575
+ main() {
576
+ ensure_incident_dir
577
+
578
+ local cmd="${1:-help}"
579
+ shift 2>/dev/null || true
580
+
581
+ case "$cmd" in
582
+ watch)
583
+ cmd_watch "$@"
584
+ ;;
585
+ stop)
586
+ cmd_stop "$@"
587
+ ;;
588
+ list)
589
+ cmd_list "$@"
590
+ ;;
591
+ show)
592
+ cmd_show "$@"
593
+ ;;
594
+ report)
595
+ cmd_report "$@"
596
+ ;;
597
+ stats)
598
+ cmd_stats "$@"
599
+ ;;
600
+ config)
601
+ error "config command not yet implemented"
602
+ return 1
603
+ ;;
604
+ help|--help|-h)
605
+ show_help
606
+ ;;
607
+ *)
608
+ error "Unknown command: $cmd"
609
+ show_help
610
+ exit 1
611
+ ;;
612
+ esac
613
+ }
614
+
615
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
616
+ main "$@"
617
+ fi