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
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="1.9.0"
9
+ VERSION="2.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -0,0 +1,520 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright replay — Pipeline run replay, timeline viewing, narratives ║
4
+ # ║ DVR for pipeline execution: list, show, narrative, diff, export, compare ║
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
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
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
+ # ─── Helpers ─────────────────────────────────────────────────────────────────
29
+
30
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
31
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
32
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
33
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
34
+
35
+ # Check if jq is available
36
+ check_jq() {
37
+ if ! command -v jq &>/dev/null; then
38
+ error "jq is required. Install with: brew install jq"
39
+ exit 1
40
+ fi
41
+ }
42
+
43
+ # Format duration in seconds to human readable
44
+ format_duration() {
45
+ local seconds=$1
46
+ if [[ $seconds -lt 60 ]]; then
47
+ echo "${seconds}s"
48
+ elif [[ $seconds -lt 3600 ]]; then
49
+ local mins=$((seconds / 60))
50
+ local secs=$((seconds % 60))
51
+ echo "${mins}m${secs}s"
52
+ else
53
+ local hours=$((seconds / 3600))
54
+ local mins=$(((seconds % 3600) / 60))
55
+ echo "${hours}h${mins}m"
56
+ fi
57
+ }
58
+
59
+ # Status color coding
60
+ color_status() {
61
+ local status="$1"
62
+ case "$status" in
63
+ success) echo -e "${GREEN}${BOLD}✓${RESET} $status" ;;
64
+ failure) echo -e "${RED}${BOLD}✗${RESET} $status" ;;
65
+ retry) echo -e "${YELLOW}${BOLD}⚠${RESET} $status" ;;
66
+ in_progress) echo -e "${CYAN}${BOLD}→${RESET} $status" ;;
67
+ *) echo "$status" ;;
68
+ esac
69
+ }
70
+
71
+ # ─── List subcommand ───────────────────────────────────────────────────────
72
+
73
+ cmd_list() {
74
+ check_jq
75
+
76
+ if [[ ! -f "$EVENTS_FILE" ]]; then
77
+ warn "No pipeline runs recorded yet (events file not found)"
78
+ exit 0
79
+ fi
80
+
81
+ info "Pipeline runs (${DIM}from $EVENTS_FILE${RESET})"
82
+ echo ""
83
+
84
+ # Extract unique pipeline runs, sorted by start time
85
+ # Use --slurpfile to handle parsing errors gracefully
86
+ jq -r 'select(.type == "pipeline.started") | [.ts, .issue, .pipeline, .model, .goal] | @tsv' "$EVENTS_FILE" 2>/dev/null | \
87
+ sort -r | \
88
+ while IFS=$'\t' read -r ts issue pipeline model goal; do
89
+ # Find corresponding completion event
90
+ local completion
91
+ completion=$(jq -r "select(.type == \"pipeline.completed\" and .issue == $issue) | .result, .duration_s" "$EVENTS_FILE" 2>/dev/null | head -2)
92
+
93
+ if [[ -n "$completion" ]]; then
94
+ local result duration
95
+ result=$(echo "$completion" | head -1)
96
+ duration=$(echo "$completion" | tail -1)
97
+ duration="${duration:-0}"
98
+
99
+ # Format date
100
+ local date time
101
+ date=$(echo "$ts" | cut -d'T' -f1)
102
+ time=$(echo "$ts" | cut -d'T' -f2 | cut -d'Z' -f1)
103
+
104
+ # Truncate goal to 40 chars
105
+ local goal_trunc="${goal:0:40}"
106
+ [[ ${#goal} -gt 40 ]] && goal_trunc="${goal_trunc}…"
107
+
108
+ printf " ${CYAN}#%-5s${RESET} ${BOLD}%s${RESET} %s %s ${DIM}%s${RESET} %s\n" \
109
+ "$issue" "$date" "$time" "$(color_status "${result:-success}")" "$(format_duration "$duration")" "$goal_trunc"
110
+ fi
111
+ done
112
+
113
+ echo ""
114
+ success "Use 'shipwright replay show <issue>' to see details"
115
+ }
116
+
117
+ # ─── Show subcommand ───────────────────────────────────────────────────────
118
+
119
+ cmd_show() {
120
+ local issue="${1:-}"
121
+ check_jq
122
+
123
+ if [[ -z "$issue" ]]; then
124
+ error "Usage: shipwright replay show <issue>"
125
+ exit 1
126
+ fi
127
+
128
+ if [[ ! -f "$EVENTS_FILE" ]]; then
129
+ error "No events recorded yet"
130
+ exit 1
131
+ fi
132
+
133
+ # Find pipeline run for this issue
134
+ local pipeline_start
135
+ pipeline_start=$(jq -r "select(.type == \"pipeline.started\" and .issue == $issue) | [.ts, .pipeline, .model, .goal] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
136
+
137
+ if [[ -z "$pipeline_start" ]]; then
138
+ error "No pipeline run found for issue #$issue"
139
+ exit 1
140
+ fi
141
+
142
+ local start_ts pipeline_type model goal
143
+ start_ts=$(echo "$pipeline_start" | cut -f1)
144
+ pipeline_type=$(echo "$pipeline_start" | cut -f2)
145
+ model=$(echo "$pipeline_start" | cut -f3)
146
+ goal=$(echo "$pipeline_start" | cut -f4)
147
+
148
+ info "Pipeline Timeline for Issue #$issue"
149
+ echo ""
150
+ echo -e " ${BOLD}Pipeline Type:${RESET} $pipeline_type"
151
+ echo -e " ${BOLD}Model:${RESET} $model"
152
+ [[ -n "$goal" ]] && echo -e " ${BOLD}Goal:${RESET} $goal"
153
+ echo ""
154
+
155
+ # Find all stage events for this issue
156
+ echo -e " ${BOLD}Stages:${RESET}"
157
+ jq -r "select(.issue == $issue and .type == \"stage.completed\") | [.ts, .stage, .duration_s // 0, .result // \"success\"] | @tsv" "$EVENTS_FILE" 2>/dev/null | \
158
+ while IFS=$'\t' read -r ts stage duration result; do
159
+ local status_icon
160
+ case "$result" in
161
+ success) status_icon="${GREEN}${BOLD}✓${RESET}" ;;
162
+ failure) status_icon="${RED}${BOLD}✗${RESET}" ;;
163
+ retry) status_icon="${YELLOW}${BOLD}⚠${RESET}" ;;
164
+ *) status_icon="•" ;;
165
+ esac
166
+
167
+ local time
168
+ time=$(echo "$ts" | cut -d'T' -f2 | cut -d'Z' -f1)
169
+
170
+ printf " %s ${CYAN}%-20s${RESET} ${DIM}%s${RESET} %s\n" \
171
+ "$status_icon" "$stage" "$(format_duration "$duration")" "$time"
172
+ done
173
+
174
+ # Find pipeline completion
175
+ local completion
176
+ completion=$(jq -r "select(.type == \"pipeline.completed\" and .issue == $issue) | [.result // \"unknown\", .duration_s // 0, .input_tokens // 0, .output_tokens // 0] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
177
+
178
+ if [[ -n "$completion" ]]; then
179
+ echo ""
180
+ local result duration input_tokens output_tokens
181
+ result=$(echo "$completion" | cut -f1)
182
+ duration=$(echo "$completion" | cut -f2)
183
+ input_tokens=$(echo "$completion" | cut -f3)
184
+ output_tokens=$(echo "$completion" | cut -f4)
185
+
186
+ echo -e " ${BOLD}Result:${RESET} $(color_status "$result")"
187
+ echo -e " ${BOLD}Duration:${RESET} $(format_duration "$duration")"
188
+ echo -e " ${BOLD}Tokens:${RESET} in=$input_tokens, out=$output_tokens"
189
+ fi
190
+
191
+ echo ""
192
+ }
193
+
194
+ # ─── Narrative subcommand ──────────────────────────────────────────────────
195
+
196
+ cmd_narrative() {
197
+ local issue="${1:-}"
198
+ check_jq
199
+
200
+ if [[ -z "$issue" ]]; then
201
+ error "Usage: shipwright replay narrative <issue>"
202
+ exit 1
203
+ fi
204
+
205
+ if [[ ! -f "$EVENTS_FILE" ]]; then
206
+ error "No events recorded yet"
207
+ exit 1
208
+ fi
209
+
210
+ local pipeline_start
211
+ pipeline_start=$(jq -r "select(.type == \"pipeline.started\" and .issue == $issue) | [.ts, .goal // \"\", .pipeline // \"standard\"] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
212
+
213
+ if [[ -z "$pipeline_start" ]]; then
214
+ error "No pipeline run found for issue #$issue"
215
+ exit 1
216
+ fi
217
+
218
+ local start_ts goal pipeline_type
219
+ start_ts=$(echo "$pipeline_start" | cut -f1)
220
+ goal=$(echo "$pipeline_start" | cut -f2)
221
+ pipeline_type=$(echo "$pipeline_start" | cut -f3)
222
+
223
+ # Get pipeline completion
224
+ local completion
225
+ completion=$(jq -r "select(.type == \"pipeline.completed\" and .issue == $issue) | [.result // \"unknown\", .duration_s // 0, .input_tokens // 0, .output_tokens // 0] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
226
+
227
+ local result duration input_tokens output_tokens
228
+ if [[ -n "$completion" ]]; then
229
+ result=$(echo "$completion" | cut -f1)
230
+ duration=$(echo "$completion" | cut -f2)
231
+ input_tokens=$(echo "$completion" | cut -f3)
232
+ output_tokens=$(echo "$completion" | cut -f4)
233
+ else
234
+ result="in_progress"
235
+ duration="0"
236
+ input_tokens="0"
237
+ output_tokens="0"
238
+ fi
239
+
240
+ # Count stages
241
+ local stage_count
242
+ stage_count=$(jq -r "select(.issue == $issue and .type == \"stage.completed\") | .stage" "$EVENTS_FILE" 2>/dev/null | wc -l)
243
+
244
+ # Build narrative
245
+ info "Pipeline Narrative"
246
+ echo ""
247
+ echo "Pipeline processed issue #$issue"
248
+ [[ -n "$goal" ]] && echo "Goal: $goal"
249
+ echo "in ${duration}s across $stage_count stages."
250
+ echo ""
251
+ echo "Pipeline Type: $pipeline_type"
252
+ echo "Result: $(color_status "$result")"
253
+ echo "Tokens Used: $input_tokens input, $output_tokens output"
254
+ echo ""
255
+
256
+ # Key events
257
+ local retry_count build_iterations test_failures
258
+ retry_count=$(jq -r "select(.issue == $issue and .type == \"stage.completed\" and .result == \"retry\") | .stage" "$EVENTS_FILE" 2>/dev/null | wc -l)
259
+ build_iterations=$(jq -r "select(.issue == $issue and .type == \"build.iteration\") | .iteration" "$EVENTS_FILE" 2>/dev/null | tail -1)
260
+ test_failures=$(jq -r "select(.issue == $issue and .type == \"test.failed\") | .test" "$EVENTS_FILE" 2>/dev/null | wc -l)
261
+
262
+ echo "Key Events:"
263
+ [[ $retry_count -gt 0 ]] && echo " • $retry_count stage retries"
264
+ [[ -n "$build_iterations" && "$build_iterations" != "null" ]] && echo " • $build_iterations build iterations"
265
+ [[ $test_failures -gt 0 ]] && echo " • $test_failures test failures encountered"
266
+
267
+ echo ""
268
+ }
269
+
270
+ # ─── Diff subcommand ──────────────────────────────────────────────────────
271
+
272
+ cmd_diff() {
273
+ local issue="${1:-}"
274
+ check_jq
275
+
276
+ if [[ -z "$issue" ]]; then
277
+ error "Usage: shipwright replay diff <issue>"
278
+ exit 1
279
+ fi
280
+
281
+ if ! command -v git &>/dev/null; then
282
+ error "git is required for diff subcommand"
283
+ exit 1
284
+ fi
285
+
286
+ if [[ ! -f "$EVENTS_FILE" ]]; then
287
+ error "No events recorded yet"
288
+ exit 1
289
+ fi
290
+
291
+ # Check if issue was processed
292
+ local found
293
+ found=$(jq -r "select(.issue == $issue and .type == \"pipeline.completed\") | .issue" "$EVENTS_FILE" | head -1)
294
+
295
+ if [[ -z "$found" ]]; then
296
+ error "No pipeline run found for issue #$issue"
297
+ exit 1
298
+ fi
299
+
300
+ info "Git commits for issue #$issue"
301
+ echo ""
302
+
303
+ # Try to find commits with issue reference
304
+ git log --all --grep="#$issue" --oneline || true
305
+
306
+ # Also try to find by branch name pattern
307
+ local branch_pattern="issue-${issue}"
308
+ if git show-ref --verify "refs/heads/$branch_pattern" &>/dev/null; then
309
+ echo ""
310
+ info "Commits on branch '$branch_pattern':"
311
+ git log "$branch_pattern" --oneline || true
312
+ fi
313
+
314
+ echo ""
315
+ }
316
+
317
+ # ─── Export subcommand ──────────────────────────────────────────────────────
318
+
319
+ cmd_export() {
320
+ local issue="${1:-}"
321
+ check_jq
322
+
323
+ if [[ -z "$issue" ]]; then
324
+ error "Usage: shipwright replay export <issue>"
325
+ exit 1
326
+ fi
327
+
328
+ if [[ ! -f "$EVENTS_FILE" ]]; then
329
+ error "No events recorded yet"
330
+ exit 1
331
+ fi
332
+
333
+ local pipeline_start
334
+ pipeline_start=$(jq -r "select(.type == \"pipeline.started\" and .issue == $issue) | [.ts, .goal // \"\", .pipeline // \"standard\"] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
335
+
336
+ if [[ -z "$pipeline_start" ]]; then
337
+ error "No pipeline run found for issue #$issue"
338
+ exit 1
339
+ fi
340
+
341
+ local start_ts goal pipeline_type
342
+ start_ts=$(echo "$pipeline_start" | cut -f1)
343
+ goal=$(echo "$pipeline_start" | cut -f2)
344
+ pipeline_type=$(echo "$pipeline_start" | cut -f3)
345
+
346
+ local completion
347
+ completion=$(jq -r "select(.type == \"pipeline.completed\" and .issue == $issue) | [.result // \"unknown\", .duration_s // 0] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
348
+
349
+ local result duration
350
+ if [[ -n "$completion" ]]; then
351
+ result=$(echo "$completion" | cut -f1)
352
+ duration=$(echo "$completion" | cut -f2)
353
+ else
354
+ result="in_progress"
355
+ duration="0"
356
+ fi
357
+
358
+ # Output markdown report
359
+ cat << EOF
360
+ # Pipeline Report: Issue #$issue
361
+
362
+ **Date:** $(echo "$start_ts" | cut -d'T' -f1)
363
+ **Type:** $pipeline_type
364
+ **Result:** $result
365
+ **Duration:** $(format_duration "$duration")
366
+
367
+ ## Goal
368
+ $goal
369
+
370
+ ## Timeline
371
+
372
+ | Stage | Duration | Status |
373
+ |-------|----------|--------|
374
+ EOF
375
+
376
+ # Add stage rows
377
+ jq -r "select(.issue == $issue and .type == \"stage.completed\") | [.stage, .duration_s // 0, .result // \"success\"] | @tsv" "$EVENTS_FILE" 2>/dev/null | \
378
+ while IFS=$'\t' read -r stage duration result; do
379
+ printf "| %s | %s | %s |\n" "$stage" "$(format_duration "$duration")" "$result"
380
+ done
381
+
382
+ cat << EOF
383
+
384
+ ## Summary
385
+
386
+ - **Issue Number:** #$issue
387
+ - **Pipeline Type:** $pipeline_type
388
+ - **Overall Result:** $result
389
+ - **Total Duration:** $(format_duration "$duration")
390
+
391
+ ## Events
392
+
393
+ $(jq -r "select(.issue == $issue) | [.ts, .type] | @tsv" "$EVENTS_FILE" 2>/dev/null | awk '{print "- " $1 " — " $2}')
394
+
395
+ EOF
396
+
397
+ success "Markdown report generated above"
398
+ }
399
+
400
+ # ─── Compare subcommand ───────────────────────────────────────────────────
401
+
402
+ cmd_compare() {
403
+ local issue1="${1:-}" issue2="${2:-}"
404
+ check_jq
405
+
406
+ if [[ -z "$issue1" || -z "$issue2" ]]; then
407
+ error "Usage: shipwright replay compare <issue1> <issue2>"
408
+ exit 1
409
+ fi
410
+
411
+ if [[ ! -f "$EVENTS_FILE" ]]; then
412
+ error "No events recorded yet"
413
+ exit 1
414
+ fi
415
+
416
+ info "Comparing pipeline runs: #$issue1 vs #$issue2"
417
+ echo ""
418
+
419
+ # Get both runs
420
+ local run1 run2
421
+ run1=$(jq -r "select(.type == \"pipeline.started\" and .issue == $issue1) | [.goal // \"\", .pipeline, .model] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
422
+ run2=$(jq -r "select(.type == \"pipeline.started\" and .issue == $issue2) | [.goal // \"\", .pipeline, .model] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
423
+
424
+ if [[ -z "$run1" || -z "$run2" ]]; then
425
+ error "Could not find both pipeline runs"
426
+ exit 1
427
+ fi
428
+
429
+ # Extract details
430
+ local goal1 type1 model1
431
+ goal1=$(echo "$run1" | cut -f1)
432
+ type1=$(echo "$run1" | cut -f2)
433
+ model1=$(echo "$run1" | cut -f3)
434
+
435
+ local goal2 type2 model2
436
+ goal2=$(echo "$run2" | cut -f1)
437
+ type2=$(echo "$run2" | cut -f2)
438
+ model2=$(echo "$run2" | cut -f3)
439
+
440
+ # Get completions
441
+ local comp1 comp2
442
+ comp1=$(jq -r "select(.type == \"pipeline.completed\" and .issue == $issue1) | [.result // \"unknown\", .duration_s // 0] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
443
+ comp2=$(jq -r "select(.type == \"pipeline.completed\" and .issue == $issue2) | [.result // \"unknown\", .duration_s // 0] | @tsv" "$EVENTS_FILE" 2>/dev/null | head -1)
444
+
445
+ local result1 duration1 result2 duration2
446
+ result1=$(echo "$comp1" | cut -f1)
447
+ duration1=$(echo "$comp1" | cut -f2)
448
+ result2=$(echo "$comp2" | cut -f1)
449
+ duration2=$(echo "$comp2" | cut -f2)
450
+
451
+ # Comparison table
452
+ printf "%-20s | %-15s | %-15s\n" "Metric" "#$issue1" "#$issue2"
453
+ printf "%-20s | %-15s | %-15s\n" "---" "---" "---"
454
+ printf "%-20s | %-15s | %-15s\n" "Type" "$type1" "$type2"
455
+ printf "%-20s | %-15s | %-15s\n" "Model" "$model1" "$model2"
456
+ printf "%-20s | %-15s | %-15s\n" "Result" "$result1" "$result2"
457
+ printf "%-20s | %-15s | %-15s\n" "Duration" "$(format_duration "$duration1")" "$(format_duration "$duration2")"
458
+
459
+ echo ""
460
+ }
461
+
462
+ # ─── Help and main ────────────────────────────────────────────────────────
463
+
464
+ show_help() {
465
+ cat << EOF
466
+ ${BOLD}shipwright replay${RESET} — Pipeline DVR: replay, timeline, narrative, and analysis
467
+
468
+ ${BOLD}USAGE${RESET}
469
+ shipwright replay <subcommand> [options]
470
+
471
+ ${BOLD}SUBCOMMANDS${RESET}
472
+ ${CYAN}list${RESET} Show all past pipeline runs
473
+ ${CYAN}show${RESET} <issue> Display timeline for a specific run
474
+ ${CYAN}narrative${RESET} <issue> Generate AI-readable summary of what happened
475
+ ${CYAN}diff${RESET} <issue> Show git commits made during pipeline run
476
+ ${CYAN}export${RESET} <issue> Export run as markdown report
477
+ ${CYAN}compare${RESET} <issue1> <issue2> Compare two pipeline runs side-by-side
478
+ ${CYAN}help${RESET} Show this help message
479
+
480
+ ${BOLD}EXAMPLES${RESET}
481
+ shipwright replay list # See all runs
482
+ shipwright replay show 42 # Timeline for #42
483
+ shipwright replay narrative 42 # Summary of #42
484
+ shipwright replay diff 42 # Commits for #42
485
+ shipwright replay export 42 # Markdown report for #42
486
+ shipwright replay compare 42 43 # Compare two runs
487
+
488
+ ${DIM}Pipeline events are read from: $EVENTS_FILE${RESET}
489
+
490
+ EOF
491
+ }
492
+
493
+ # ─── Main entry point ────────────────────────────────────────────────────────
494
+
495
+ main() {
496
+ local cmd="${1:-help}"
497
+ shift 2>/dev/null || true
498
+
499
+ case "$cmd" in
500
+ list) cmd_list "$@" ;;
501
+ show) cmd_show "$@" ;;
502
+ narrative) cmd_narrative "$@" ;;
503
+ diff) cmd_diff "$@" ;;
504
+ export) cmd_export "$@" ;;
505
+ compare) cmd_compare "$@" ;;
506
+ help|--help|-h)
507
+ show_help
508
+ ;;
509
+ *)
510
+ error "Unknown subcommand: $cmd"
511
+ echo ""
512
+ show_help
513
+ exit 1
514
+ ;;
515
+ esac
516
+ }
517
+
518
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
519
+ main "$@"
520
+ fi