shipwright-cli 1.10.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 (108) hide show
  1. package/README.md +114 -36
  2. package/completions/_shipwright +212 -32
  3. package/completions/shipwright.bash +97 -25
  4. package/docs/strategy/01-market-research.md +619 -0
  5. package/docs/strategy/02-mission-and-brand.md +587 -0
  6. package/docs/strategy/03-gtm-and-roadmap.md +759 -0
  7. package/docs/strategy/QUICK-START.txt +289 -0
  8. package/docs/strategy/README.md +172 -0
  9. package/package.json +4 -2
  10. package/scripts/sw +208 -1
  11. package/scripts/sw-activity.sh +500 -0
  12. package/scripts/sw-adaptive.sh +925 -0
  13. package/scripts/sw-adversarial.sh +1 -1
  14. package/scripts/sw-architecture-enforcer.sh +1 -1
  15. package/scripts/sw-auth.sh +613 -0
  16. package/scripts/sw-autonomous.sh +664 -0
  17. package/scripts/sw-changelog.sh +704 -0
  18. package/scripts/sw-checkpoint.sh +1 -1
  19. package/scripts/sw-ci.sh +602 -0
  20. package/scripts/sw-cleanup.sh +1 -1
  21. package/scripts/sw-code-review.sh +637 -0
  22. package/scripts/sw-connect.sh +1 -1
  23. package/scripts/sw-context.sh +605 -0
  24. package/scripts/sw-cost.sh +1 -1
  25. package/scripts/sw-daemon.sh +432 -130
  26. package/scripts/sw-dashboard.sh +1 -1
  27. package/scripts/sw-db.sh +540 -0
  28. package/scripts/sw-decompose.sh +539 -0
  29. package/scripts/sw-deps.sh +551 -0
  30. package/scripts/sw-developer-simulation.sh +1 -1
  31. package/scripts/sw-discovery.sh +412 -0
  32. package/scripts/sw-docs-agent.sh +539 -0
  33. package/scripts/sw-docs.sh +1 -1
  34. package/scripts/sw-doctor.sh +59 -1
  35. package/scripts/sw-dora.sh +615 -0
  36. package/scripts/sw-durable.sh +710 -0
  37. package/scripts/sw-e2e-orchestrator.sh +535 -0
  38. package/scripts/sw-eventbus.sh +393 -0
  39. package/scripts/sw-feedback.sh +471 -0
  40. package/scripts/sw-fix.sh +1 -1
  41. package/scripts/sw-fleet-discover.sh +567 -0
  42. package/scripts/sw-fleet-viz.sh +404 -0
  43. package/scripts/sw-fleet.sh +8 -1
  44. package/scripts/sw-github-app.sh +596 -0
  45. package/scripts/sw-github-checks.sh +1 -1
  46. package/scripts/sw-github-deploy.sh +1 -1
  47. package/scripts/sw-github-graphql.sh +1 -1
  48. package/scripts/sw-guild.sh +569 -0
  49. package/scripts/sw-heartbeat.sh +1 -1
  50. package/scripts/sw-hygiene.sh +559 -0
  51. package/scripts/sw-incident.sh +617 -0
  52. package/scripts/sw-init.sh +88 -1
  53. package/scripts/sw-instrument.sh +699 -0
  54. package/scripts/sw-intelligence.sh +1 -1
  55. package/scripts/sw-jira.sh +1 -1
  56. package/scripts/sw-launchd.sh +363 -28
  57. package/scripts/sw-linear.sh +1 -1
  58. package/scripts/sw-logs.sh +1 -1
  59. package/scripts/sw-loop.sh +64 -3
  60. package/scripts/sw-memory.sh +1 -1
  61. package/scripts/sw-mission-control.sh +487 -0
  62. package/scripts/sw-model-router.sh +545 -0
  63. package/scripts/sw-otel.sh +596 -0
  64. package/scripts/sw-oversight.sh +689 -0
  65. package/scripts/sw-pipeline-composer.sh +1 -1
  66. package/scripts/sw-pipeline-vitals.sh +1 -1
  67. package/scripts/sw-pipeline.sh +687 -24
  68. package/scripts/sw-pm.sh +693 -0
  69. package/scripts/sw-pr-lifecycle.sh +522 -0
  70. package/scripts/sw-predictive.sh +1 -1
  71. package/scripts/sw-prep.sh +1 -1
  72. package/scripts/sw-ps.sh +1 -1
  73. package/scripts/sw-public-dashboard.sh +798 -0
  74. package/scripts/sw-quality.sh +595 -0
  75. package/scripts/sw-reaper.sh +1 -1
  76. package/scripts/sw-recruit.sh +573 -0
  77. package/scripts/sw-regression.sh +642 -0
  78. package/scripts/sw-release-manager.sh +736 -0
  79. package/scripts/sw-release.sh +706 -0
  80. package/scripts/sw-remote.sh +1 -1
  81. package/scripts/sw-replay.sh +520 -0
  82. package/scripts/sw-retro.sh +691 -0
  83. package/scripts/sw-scale.sh +444 -0
  84. package/scripts/sw-security-audit.sh +505 -0
  85. package/scripts/sw-self-optimize.sh +1 -1
  86. package/scripts/sw-session.sh +1 -1
  87. package/scripts/sw-setup.sh +1 -1
  88. package/scripts/sw-standup.sh +712 -0
  89. package/scripts/sw-status.sh +1 -1
  90. package/scripts/sw-strategic.sh +658 -0
  91. package/scripts/sw-stream.sh +450 -0
  92. package/scripts/sw-swarm.sh +583 -0
  93. package/scripts/sw-team-stages.sh +511 -0
  94. package/scripts/sw-templates.sh +1 -1
  95. package/scripts/sw-testgen.sh +515 -0
  96. package/scripts/sw-tmux-pipeline.sh +554 -0
  97. package/scripts/sw-tmux.sh +1 -1
  98. package/scripts/sw-trace.sh +485 -0
  99. package/scripts/sw-tracker-github.sh +188 -0
  100. package/scripts/sw-tracker-jira.sh +172 -0
  101. package/scripts/sw-tracker-linear.sh +251 -0
  102. package/scripts/sw-tracker.sh +117 -2
  103. package/scripts/sw-triage.sh +603 -0
  104. package/scripts/sw-upgrade.sh +1 -1
  105. package/scripts/sw-ux.sh +677 -0
  106. package/scripts/sw-webhook.sh +627 -0
  107. package/scripts/sw-widgets.sh +530 -0
  108. package/scripts/sw-worktree.sh +1 -1
@@ -0,0 +1,699 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright instrument — Pipeline Instrumentation & Feedback Loops ║
4
+ # ║ Records predicted vs actual metrics · Enables learning · Trend analysis ║
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
+ format_tokens() {
49
+ local tokens="$1"
50
+ if [[ "$tokens" -ge 1000000 ]]; then
51
+ printf "%.1fM" "$(echo "scale=1; $tokens / 1000000" | bc)"
52
+ elif [[ "$tokens" -ge 1000 ]]; then
53
+ printf "%.1fK" "$(echo "scale=1; $tokens / 1000" | bc)"
54
+ else
55
+ printf "%d" "$tokens"
56
+ fi
57
+ }
58
+
59
+ format_cost() {
60
+ printf '$%.2f' "$(echo "scale=2; $1" | bc)"
61
+ }
62
+
63
+ percent_delta() {
64
+ local predicted="$1"
65
+ local actual="$2"
66
+ if [[ "$predicted" -eq 0 ]]; then
67
+ echo "N/A"
68
+ return
69
+ fi
70
+ local delta
71
+ delta=$(echo "scale=0; ($actual - $predicted) * 100 / $predicted" | bc)
72
+ if [[ "$delta" -ge 0 ]]; then
73
+ printf "+%d%%" "$delta"
74
+ else
75
+ printf "%d%%" "$delta"
76
+ fi
77
+ }
78
+
79
+ # ─── Structured Event Log ──────────────────────────────────────────────────
80
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
81
+
82
+ emit_event() {
83
+ local event_type="$1"
84
+ shift
85
+ local json_fields=""
86
+ for kv in "$@"; do
87
+ local key="${kv%%=*}"
88
+ local val="${kv#*=}"
89
+ if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
90
+ json_fields="${json_fields},\"${key}\":${val}"
91
+ else
92
+ val="${val//\"/\\\"}"
93
+ json_fields="${json_fields},\"${key}\":\"${val}\""
94
+ fi
95
+ done
96
+ mkdir -p "${HOME}/.shipwright"
97
+ echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
98
+ }
99
+
100
+ # ─── Instrumentation Storage ───────────────────────────────────────────────
101
+ INSTRUMENT_DIR="${HOME}/.shipwright/instrumentation"
102
+ INSTRUMENT_ACTIVE="${INSTRUMENT_DIR}/active"
103
+ INSTRUMENT_COMPLETED="${HOME}/.shipwright/instrumentation.jsonl"
104
+
105
+ ensure_instrument_dirs() {
106
+ mkdir -p "$INSTRUMENT_ACTIVE" "${HOME}/.shipwright"
107
+ }
108
+
109
+ # ─── Help ───────────────────────────────────────────────────────────────────
110
+ show_help() {
111
+ echo ""
112
+ echo -e "${CYAN}${BOLD} Shipwright Instrumentation${RESET} ${DIM}v${VERSION}${RESET}"
113
+ echo -e "${DIM} ══════════════════════════════════════════════════════════════${RESET}"
114
+ echo ""
115
+ echo -e " ${BOLD}USAGE${RESET}"
116
+ echo -e " shipwright instrument <command> [options]"
117
+ echo ""
118
+ echo -e " ${BOLD}COMMANDS${RESET}"
119
+ echo -e " ${CYAN}start${RESET} Begin instrumenting a pipeline run"
120
+ echo -e " ${CYAN}record${RESET} Record a metric during execution"
121
+ echo -e " ${CYAN}stage-start${RESET} Mark the start of a stage"
122
+ echo -e " ${CYAN}stage-end${RESET} Mark the end of a stage with result"
123
+ echo -e " ${CYAN}finish${RESET} Complete a pipeline run record"
124
+ echo -e " ${CYAN}summary${RESET} Show run summary (predicted vs actual)"
125
+ echo -e " ${CYAN}trends${RESET} Show prediction accuracy over time"
126
+ echo -e " ${CYAN}export${RESET} Export instrumentation data"
127
+ echo -e " ${CYAN}help${RESET} Show this help message"
128
+ echo ""
129
+ echo -e " ${BOLD}START OPTIONS${RESET}"
130
+ echo -e " --run-id ID Unique run identifier (required)"
131
+ echo -e " --issue N GitHub issue number"
132
+ echo -e " --repo PATH Repository path (default: current)"
133
+ echo -e " --predicted <json> Initial predicted values (optional)"
134
+ echo ""
135
+ echo -e " ${BOLD}RECORD OPTIONS${RESET}"
136
+ echo -e " --run-id ID Run identifier (required)"
137
+ echo -e " --stage NAME Stage name (required)"
138
+ echo -e " --metric NAME Metric name (required)"
139
+ echo -e " --value VAL Metric value (required)"
140
+ echo ""
141
+ echo -e " ${BOLD}STAGE OPTIONS${RESET}"
142
+ echo -e " --run-id ID Run identifier (required)"
143
+ echo -e " --stage NAME Stage name (required)"
144
+ echo -e " --result success|fail Result (stage-end only)"
145
+ echo ""
146
+ echo -e " ${BOLD}FINISH OPTIONS${RESET}"
147
+ echo -e " --run-id ID Run identifier (required)"
148
+ echo -e " --result success|fail Final pipeline result"
149
+ echo ""
150
+ echo -e " ${BOLD}SUMMARY OPTIONS${RESET}"
151
+ echo -e " --run-id ID Run identifier (required)"
152
+ echo ""
153
+ echo -e " ${BOLD}TRENDS OPTIONS${RESET}"
154
+ echo -e " --metric NAME Filter by metric (optional)"
155
+ echo -e " --last N Last N runs (default: 20)"
156
+ echo ""
157
+ echo -e " ${BOLD}EXPORT OPTIONS${RESET}"
158
+ echo -e " --format json|csv Output format (default: json)"
159
+ echo -e " --last N Last N runs (default: all)"
160
+ echo ""
161
+ echo -e " ${BOLD}EXAMPLES${RESET}"
162
+ echo -e " ${DIM}# Start instrumenting a run${RESET}"
163
+ echo -e " shipwright instrument start --run-id abc123 --issue 42"
164
+ echo ""
165
+ echo -e " ${DIM}# Record stage execution${RESET}"
166
+ echo -e " shipwright instrument stage-start --run-id abc123 --stage plan"
167
+ echo -e " ${DIM}# ... do work ...${RESET}"
168
+ echo -e " shipwright instrument stage-end --run-id abc123 --stage plan --result success"
169
+ echo ""
170
+ echo -e " ${DIM}# Record individual metric${RESET}"
171
+ echo -e " shipwright instrument record --run-id abc123 --stage build --metric iterations --value 7"
172
+ echo ""
173
+ echo -e " ${DIM}# Finish the run${RESET}"
174
+ echo -e " shipwright instrument finish --run-id abc123 --result success"
175
+ echo ""
176
+ echo -e " ${DIM}# Show what was predicted vs actual${RESET}"
177
+ echo -e " shipwright instrument summary --run-id abc123"
178
+ echo ""
179
+ echo -e " ${DIM}# Analyze trends across runs${RESET}"
180
+ echo -e " shipwright instrument trends --metric duration --last 30"
181
+ echo ""
182
+ echo -e " ${DIM}# Export data for analysis${RESET}"
183
+ echo -e " shipwright instrument export --format csv --last 50"
184
+ echo ""
185
+ }
186
+
187
+ # ─── Start Instrumentation ──────────────────────────────────────────────────
188
+ cmd_start() {
189
+ local run_id="" issue="" repo="." predicted=""
190
+
191
+ while [[ $# -gt 0 ]]; do
192
+ case "$1" in
193
+ --run-id) run_id="${2:-}"; shift 2 ;;
194
+ --issue) issue="${2:-}"; shift 2 ;;
195
+ --repo) repo="${2:-}"; shift 2 ;;
196
+ --predicted) predicted="${2:-}"; shift 2 ;;
197
+ *)
198
+ warn "Unknown option: $1"
199
+ shift
200
+ ;;
201
+ esac
202
+ done
203
+
204
+ if [[ -z "$run_id" ]]; then
205
+ error "Usage: shipwright instrument start --run-id ID [--issue N] [--repo PATH]"
206
+ return 1
207
+ fi
208
+
209
+ ensure_instrument_dirs
210
+
211
+ local run_file="${INSTRUMENT_ACTIVE}/${run_id}.json"
212
+ local tmp_file
213
+ tmp_file="$(mktemp "${INSTRUMENT_ACTIVE}/.tmp.XXXXXX")"
214
+
215
+ # Get repo info if not provided
216
+ if [[ "$repo" == "." ]]; then
217
+ repo="$(cd "$repo" && pwd 2>/dev/null || echo "unknown")"
218
+ fi
219
+
220
+ # Build initial record with jq
221
+ jq -n \
222
+ --arg run_id "$run_id" \
223
+ --argjson issue "$([ -n "$issue" ] && echo "$issue" || echo "null")" \
224
+ --arg repo "$repo" \
225
+ --arg started_at "$(now_iso)" \
226
+ --argjson started_epoch "$(now_epoch)" \
227
+ --arg predicted "$predicted" \
228
+ '{
229
+ run_id: $run_id,
230
+ issue: $issue,
231
+ repo: $repo,
232
+ started_at: $started_at,
233
+ started_epoch: $started_epoch,
234
+ finished_at: null,
235
+ finished_epoch: null,
236
+ result: null,
237
+ predicted: (if $predicted == "" then {} else ($predicted | fromjson) end),
238
+ actual: {},
239
+ stages: {},
240
+ metrics: []
241
+ }' > "$tmp_file" || { rm -f "$tmp_file"; return 1; }
242
+
243
+ mv "$tmp_file" "$run_file"
244
+ success "Started instrumentation for run ${CYAN}${run_id}${RESET} (issue #${issue})"
245
+ emit_event "instrument_start" "run_id=${run_id}" "issue=${issue}" "repo=${repo}"
246
+ }
247
+
248
+ # ─── Record Metric ───────────────────────────────────────────────────────────
249
+ cmd_record() {
250
+ local run_id="" stage="" metric="" value=""
251
+
252
+ while [[ $# -gt 0 ]]; do
253
+ case "$1" in
254
+ --run-id) run_id="${2:-}"; shift 2 ;;
255
+ --stage) stage="${2:-}"; shift 2 ;;
256
+ --metric) metric="${2:-}"; shift 2 ;;
257
+ --value) value="${2:-}"; shift 2 ;;
258
+ *)
259
+ warn "Unknown option: $1"
260
+ shift
261
+ ;;
262
+ esac
263
+ done
264
+
265
+ if [[ -z "$run_id" || -z "$stage" || -z "$metric" || -z "$value" ]]; then
266
+ error "Usage: shipwright instrument record --run-id ID --stage NAME --metric NAME --value VAL"
267
+ return 1
268
+ fi
269
+
270
+ ensure_instrument_dirs
271
+ local run_file="${INSTRUMENT_ACTIVE}/${run_id}.json"
272
+
273
+ if [[ ! -f "$run_file" ]]; then
274
+ error "Run not found: ${run_id}"
275
+ return 1
276
+ fi
277
+
278
+ local tmp_file
279
+ tmp_file="$(mktemp "${INSTRUMENT_ACTIVE}/.tmp.XXXXXX")"
280
+
281
+ # Parse value as number if it's numeric
282
+ local value_json
283
+ if [[ "$value" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
284
+ value_json="$value"
285
+ else
286
+ value_json="\"$value\""
287
+ fi
288
+
289
+ # Append metric record with jq
290
+ jq \
291
+ --arg stage "$stage" \
292
+ --arg metric "$metric" \
293
+ --argjson value "$value_json" \
294
+ --arg recorded_at "$(now_iso)" \
295
+ '.metrics += [
296
+ {
297
+ stage: $stage,
298
+ metric: $metric,
299
+ value: $value,
300
+ recorded_at: $recorded_at
301
+ }
302
+ ] |
303
+ .actual[$stage] //= {} |
304
+ .actual[$stage][$metric] = $value' \
305
+ "$run_file" > "$tmp_file" || { rm -f "$tmp_file"; return 1; }
306
+
307
+ mv "$tmp_file" "$run_file"
308
+ success "Recorded ${CYAN}${metric}${RESET}=${CYAN}${value}${RESET} for stage ${CYAN}${stage}${RESET}"
309
+ emit_event "instrument_record" "run_id=${run_id}" "stage=${stage}" "metric=${metric}" "value=${value}"
310
+ }
311
+
312
+ # ─── Stage Start ─────────────────────────────────────────────────────────────
313
+ cmd_stage_start() {
314
+ local run_id="" stage=""
315
+
316
+ while [[ $# -gt 0 ]]; do
317
+ case "$1" in
318
+ --run-id) run_id="${2:-}"; shift 2 ;;
319
+ --stage) stage="${2:-}"; shift 2 ;;
320
+ *)
321
+ warn "Unknown option: $1"
322
+ shift
323
+ ;;
324
+ esac
325
+ done
326
+
327
+ if [[ -z "$run_id" || -z "$stage" ]]; then
328
+ error "Usage: shipwright instrument stage-start --run-id ID --stage NAME"
329
+ return 1
330
+ fi
331
+
332
+ ensure_instrument_dirs
333
+ local run_file="${INSTRUMENT_ACTIVE}/${run_id}.json"
334
+
335
+ if [[ ! -f "$run_file" ]]; then
336
+ error "Run not found: ${run_id}"
337
+ return 1
338
+ fi
339
+
340
+ local tmp_file
341
+ tmp_file="$(mktemp "${INSTRUMENT_ACTIVE}/.tmp.XXXXXX")"
342
+
343
+ jq \
344
+ --arg stage "$stage" \
345
+ --arg started_at "$(now_iso)" \
346
+ --argjson started_epoch "$(now_epoch)" \
347
+ '.stages[$stage] //= {} |
348
+ .stages[$stage].started_at = $started_at |
349
+ .stages[$stage].started_epoch = $started_epoch' \
350
+ "$run_file" > "$tmp_file" || { rm -f "$tmp_file"; return 1; }
351
+
352
+ mv "$tmp_file" "$run_file"
353
+ success "Started stage ${CYAN}${stage}${RESET}"
354
+ }
355
+
356
+ # ─── Stage End ───────────────────────────────────────────────────────────────
357
+ cmd_stage_end() {
358
+ local run_id="" stage="" result=""
359
+
360
+ while [[ $# -gt 0 ]]; do
361
+ case "$1" in
362
+ --run-id) run_id="${2:-}"; shift 2 ;;
363
+ --stage) stage="${2:-}"; shift 2 ;;
364
+ --result) result="${2:-}"; shift 2 ;;
365
+ *)
366
+ warn "Unknown option: $1"
367
+ shift
368
+ ;;
369
+ esac
370
+ done
371
+
372
+ if [[ -z "$run_id" || -z "$stage" ]]; then
373
+ error "Usage: shipwright instrument stage-end --run-id ID --stage NAME [--result success|failure|timeout]"
374
+ return 1
375
+ fi
376
+
377
+ ensure_instrument_dirs
378
+ local run_file="${INSTRUMENT_ACTIVE}/${run_id}.json"
379
+
380
+ if [[ ! -f "$run_file" ]]; then
381
+ error "Run not found: ${run_id}"
382
+ return 1
383
+ fi
384
+
385
+ local tmp_file
386
+ tmp_file="$(mktemp "${INSTRUMENT_ACTIVE}/.tmp.XXXXXX")"
387
+
388
+ jq \
389
+ --arg stage "$stage" \
390
+ --arg finished_at "$(now_iso)" \
391
+ --argjson finished_epoch "$(now_epoch)" \
392
+ --arg result "$result" \
393
+ '.stages[$stage] //= {} |
394
+ .stages[$stage].finished_at = $finished_at |
395
+ .stages[$stage].finished_epoch = $finished_epoch |
396
+ (if .stages[$stage].started_epoch and .stages[$stage].finished_epoch then
397
+ .stages[$stage].duration_s = (.stages[$stage].finished_epoch - .stages[$stage].started_epoch)
398
+ else . end) |
399
+ (if $result != "" then .stages[$stage].result = $result else . end)' \
400
+ "$run_file" > "$tmp_file" || { rm -f "$tmp_file"; return 1; }
401
+
402
+ mv "$tmp_file" "$run_file"
403
+ success "Finished stage ${CYAN}${stage}${RESET} (${result})"
404
+ }
405
+
406
+ # ─── Finish Run ──────────────────────────────────────────────────────────────
407
+ cmd_finish() {
408
+ local run_id="" result=""
409
+
410
+ while [[ $# -gt 0 ]]; do
411
+ case "$1" in
412
+ --run-id) run_id="${2:-}"; shift 2 ;;
413
+ --result) result="${2:-}"; shift 2 ;;
414
+ *)
415
+ warn "Unknown option: $1"
416
+ shift
417
+ ;;
418
+ esac
419
+ done
420
+
421
+ if [[ -z "$run_id" ]]; then
422
+ error "Usage: shipwright instrument finish --run-id ID [--result success|failure|timeout]"
423
+ return 1
424
+ fi
425
+
426
+ ensure_instrument_dirs
427
+ local run_file="${INSTRUMENT_ACTIVE}/${run_id}.json"
428
+
429
+ if [[ ! -f "$run_file" ]]; then
430
+ error "Run not found: ${run_id}"
431
+ return 1
432
+ fi
433
+
434
+ local tmp_file
435
+ tmp_file="$(mktemp "${INSTRUMENT_ACTIVE}/.tmp.XXXXXX")"
436
+
437
+ # Update run record with finish data
438
+ jq \
439
+ --arg finished_at "$(now_iso)" \
440
+ --argjson finished_epoch "$(now_epoch)" \
441
+ --arg result "$result" \
442
+ '.finished_at = $finished_at |
443
+ .finished_epoch = $finished_epoch |
444
+ .result = $result |
445
+ (.finished_epoch - .started_epoch) as $total_duration |
446
+ .total_duration_s = $total_duration' \
447
+ "$run_file" > "$tmp_file" || { rm -f "$tmp_file"; return 1; }
448
+
449
+ # Compact and append to JSONL (single-line JSON)
450
+ jq -c '.' "$tmp_file" >> "$INSTRUMENT_COMPLETED"
451
+ rm -f "$tmp_file"
452
+
453
+ # Remove active file
454
+ rm -f "$run_file"
455
+
456
+ success "Finished instrumentation for run ${CYAN}${run_id}${RESET} (${result})"
457
+ emit_event "instrument_finish" "run_id=${run_id}" "result=${result}"
458
+ }
459
+
460
+ # ─── Show Summary ────────────────────────────────────────────────────────────
461
+ cmd_summary() {
462
+ local run_id=""
463
+
464
+ while [[ $# -gt 0 ]]; do
465
+ case "$1" in
466
+ --run-id) run_id="${2:-}"; shift 2 ;;
467
+ *)
468
+ warn "Unknown option: $1"
469
+ shift
470
+ ;;
471
+ esac
472
+ done
473
+
474
+ if [[ -z "$run_id" ]]; then
475
+ error "Usage: shipwright instrument summary --run-id ID"
476
+ return 1
477
+ fi
478
+
479
+ ensure_instrument_dirs
480
+
481
+ # Check active first, then completed
482
+ local run_file="${INSTRUMENT_ACTIVE}/${run_id}.json"
483
+ if [[ ! -f "$run_file" ]]; then
484
+ # Try to find in JSONL
485
+ run_file=$(grep -l "\"run_id\":\"${run_id}\"" "$INSTRUMENT_COMPLETED" 2>/dev/null | head -1 || true)
486
+ if [[ -z "$run_file" ]]; then
487
+ error "Run not found: ${run_id}"
488
+ return 1
489
+ fi
490
+ # Extract matching record from JSONL
491
+ local tmp_file
492
+ tmp_file="$(mktemp)"
493
+ grep "\"run_id\":\"${run_id}\"" "$INSTRUMENT_COMPLETED" | head -1 > "$tmp_file"
494
+ run_file="$tmp_file"
495
+ trap "rm -f '$tmp_file'" RETURN
496
+ fi
497
+
498
+ # Extract data
499
+ local issue result started_at finished_at total_dur
500
+ local pred_dur pred_iter pred_tokens pred_cost
501
+ local actual_dur actual_iter actual_tokens actual_cost
502
+
503
+ issue=$(jq -r '.issue // "N/A"' "$run_file")
504
+ result=$(jq -r '.result // "pending"' "$run_file")
505
+ started_at=$(jq -r '.started_at' "$run_file")
506
+ finished_at=$(jq -r '.finished_at // "in progress"' "$run_file")
507
+ total_dur=$(jq -r '.total_duration_s // 0' "$run_file")
508
+
509
+ # Predicted values
510
+ pred_dur=$(jq -r '.predicted.timeout // 0' "$run_file")
511
+ pred_iter=$(jq -r '.predicted.iterations // 0' "$run_file")
512
+ pred_tokens=$(jq -r '.predicted.tokens // 0' "$run_file")
513
+ pred_cost=$(jq -r '.predicted.cost // 0' "$run_file")
514
+
515
+ # Aggregate actual values
516
+ actual_iter=$(jq '[.metrics[] | select(.metric == "iterations") | .value] | max' "$run_file" 2>/dev/null || echo "0")
517
+ actual_tokens=$(jq '[.metrics[] | select(.metric == "tokens_total") | .value] | max' "$run_file" 2>/dev/null || echo "0")
518
+ actual_cost=$(jq '[.metrics[] | select(.metric == "cost_usd") | .value] | add // 0' "$run_file" 2>/dev/null || echo "0")
519
+ actual_dur="$total_dur"
520
+
521
+ # Print summary
522
+ echo ""
523
+ echo -e "${CYAN}${BOLD} Pipeline Run ${run_id}${RESET} ${DIM}Issue #${issue}${RESET}"
524
+ echo -e "${DIM} ═════════════════════════════════════════════════════════════════${RESET}"
525
+ echo ""
526
+ echo -e " ${BOLD}Result:${RESET} ${result}"
527
+ echo -e " ${DIM}Started: ${started_at}${RESET}"
528
+ echo -e " ${DIM}Finished: ${finished_at}${RESET}"
529
+ echo ""
530
+ echo -e " ${BOLD}Predicted vs Actual${RESET}"
531
+ echo ""
532
+
533
+ # Print table header
534
+ printf " %-20s %-15s %-15s %-10s\n" "Metric" "Predicted" "Actual" "Delta"
535
+ echo -e " ${DIM}────────────────────────────────────────────────────────────────${RESET}"
536
+
537
+ # Duration
538
+ local dur_fmt_pred dur_fmt_act dur_delta
539
+ dur_fmt_pred=$(format_duration "$pred_dur")
540
+ dur_fmt_act=$(format_duration "$actual_dur")
541
+ dur_delta=$(percent_delta "$pred_dur" "$actual_dur")
542
+ printf " %-20s %-15s %-15s %-10s\n" "Duration" "$dur_fmt_pred" "$dur_fmt_act" "$dur_delta"
543
+
544
+ # Iterations
545
+ if [[ "$pred_iter" -gt 0 || "$actual_iter" -gt 0 ]]; then
546
+ local iter_delta
547
+ iter_delta=$(percent_delta "$pred_iter" "$actual_iter")
548
+ printf " %-20s %-15s %-15s %-10s\n" "Iterations" "$pred_iter" "$actual_iter" "$iter_delta"
549
+ fi
550
+
551
+ # Tokens
552
+ if [[ "$pred_tokens" -gt 0 || "$actual_tokens" -gt 0 ]]; then
553
+ local tok_fmt_pred tok_fmt_act tok_delta
554
+ tok_fmt_pred=$(format_tokens "$pred_tokens")
555
+ tok_fmt_act=$(format_tokens "$actual_tokens")
556
+ tok_delta=$(percent_delta "$pred_tokens" "$actual_tokens")
557
+ printf " %-20s %-15s %-15s %-10s\n" "Tokens" "$tok_fmt_pred" "$tok_fmt_act" "$tok_delta"
558
+ fi
559
+
560
+ # Cost
561
+ if [[ $(echo "$pred_cost > 0" | bc) -eq 1 || $(echo "$actual_cost > 0" | bc) -eq 1 ]]; then
562
+ local cost_fmt_pred cost_fmt_act cost_delta
563
+ cost_fmt_pred=$(format_cost "$pred_cost")
564
+ cost_fmt_act=$(format_cost "$actual_cost")
565
+ cost_delta=$(percent_delta "$(echo "$pred_cost * 100" | bc)" "$(echo "$actual_cost * 100" | bc)")
566
+ printf " %-20s %-15s %-15s %-10s\n" "Cost" "$cost_fmt_pred" "$cost_fmt_act" "$cost_delta"
567
+ fi
568
+
569
+ echo ""
570
+ echo -e " ${BOLD}Stages${RESET}"
571
+ echo ""
572
+ jq -r '.stages | to_entries[] | " \(.key): \(.value.result // "pending") (\(.value.duration_s // "?")s)"' "$run_file"
573
+ echo ""
574
+ }
575
+
576
+ # ─── Show Trends ─────────────────────────────────────────────────────────────
577
+ cmd_trends() {
578
+ local metric="" last=20
579
+
580
+ while [[ $# -gt 0 ]]; do
581
+ case "$1" in
582
+ --metric) metric="${2:-}"; shift 2 ;;
583
+ --last) last="${2:-20}"; shift 2 ;;
584
+ *)
585
+ warn "Unknown option: $1"
586
+ shift
587
+ ;;
588
+ esac
589
+ done
590
+
591
+ if [[ ! -f "$INSTRUMENT_COMPLETED" ]]; then
592
+ warn "No completed runs found"
593
+ return
594
+ fi
595
+
596
+ echo ""
597
+ echo -e "${CYAN}${BOLD} Instrumentation Trends${RESET} ${DIM}(last ${last} runs)${RESET}"
598
+ echo -e "${DIM} ═══════════════════════════════════════════════════════════════════${RESET}"
599
+ echo ""
600
+
601
+ # Extract metrics and compute statistics
602
+ if [[ -n "$metric" ]]; then
603
+ echo -e " ${BOLD}${metric}${RESET}"
604
+ echo -e " ${DIM}────────────────────────────────────────${RESET}"
605
+ fi
606
+
607
+ # Use jq to analyze trends from JSONL file (with compact output)
608
+ if [[ -n "$metric" ]]; then
609
+ jq -c -s '[.[] | .metrics[] | select(.metric == "'$metric'")] | group_by(.metric) | map({metric: .[0].metric, count: length, avg: (map(.value | tonumber) | add / length), min: (map(.value | tonumber) | min), max: (map(.value | tonumber) | max)}) | .[]' "$INSTRUMENT_COMPLETED" | while read -r line; do
610
+ local m avg min max
611
+ m=$(echo "$line" | jq -r '.metric')
612
+ avg=$(echo "$line" | jq -r '.avg | round')
613
+ min=$(echo "$line" | jq -r '.min | round')
614
+ max=$(echo "$line" | jq -r '.max | round')
615
+ printf " %-25s avg: %-8s min: %-8s max: %-8s\n" "$m" "$avg" "$min" "$max"
616
+ done
617
+ else
618
+ jq -c -s '[.[] | .metrics[]] | group_by(.metric) | map({metric: .[0].metric, count: length, avg: (map(.value | tonumber) | add / length), min: (map(.value | tonumber) | min), max: (map(.value | tonumber) | max)}) | .[]' "$INSTRUMENT_COMPLETED" | while read -r line; do
619
+ local m avg min max
620
+ m=$(echo "$line" | jq -r '.metric')
621
+ avg=$(echo "$line" | jq -r '.avg | round')
622
+ min=$(echo "$line" | jq -r '.min | round')
623
+ max=$(echo "$line" | jq -r '.max | round')
624
+ printf " %-25s avg: %-8s min: %-8s max: %-8s\n" "$m" "$avg" "$min" "$max"
625
+ done
626
+ fi
627
+
628
+ echo ""
629
+ }
630
+
631
+ # ─── Export Data ─────────────────────────────────────────────────────────────
632
+ cmd_export() {
633
+ local format="json" last=""
634
+
635
+ while [[ $# -gt 0 ]]; do
636
+ case "$1" in
637
+ --format) format="${2:-}"; shift 2 ;;
638
+ --last) last="${2:-}"; shift 2 ;;
639
+ *)
640
+ warn "Unknown option: $1"
641
+ shift
642
+ ;;
643
+ esac
644
+ done
645
+
646
+ if [[ ! -f "$INSTRUMENT_COMPLETED" ]]; then
647
+ warn "No completed runs found"
648
+ return
649
+ fi
650
+
651
+ case "$format" in
652
+ json)
653
+ if [[ -n "$last" ]]; then
654
+ tail -n "$last" "$INSTRUMENT_COMPLETED" | jq -s '.'
655
+ else
656
+ jq -s '.' "$INSTRUMENT_COMPLETED"
657
+ fi
658
+ ;;
659
+ csv)
660
+ echo "run_id,issue,repo,started_at,result,duration_s,iterations,tokens,cost"
661
+ if [[ -n "$last" ]]; then
662
+ tail -n "$last" "$INSTRUMENT_COMPLETED"
663
+ else
664
+ cat "$INSTRUMENT_COMPLETED"
665
+ fi | jq -r '[.run_id, .issue // "", .repo, .started_at, .result // "", .total_duration_s // 0, (.metrics[] | select(.metric == "iterations") | .value) // 0, (.metrics[] | select(.metric == "tokens_total") | .value) // 0, (.metrics[] | select(.metric == "cost_usd") | .value) // 0] | @csv'
666
+ ;;
667
+ *)
668
+ error "Unknown format: ${format}. Use 'json' or 'csv'."
669
+ return 1
670
+ ;;
671
+ esac
672
+ }
673
+
674
+ # ─── Main Command Router ────────────────────────────────────────────────────
675
+ main() {
676
+ local cmd="${1:-help}"
677
+ shift 2>/dev/null || true
678
+
679
+ case "$cmd" in
680
+ start) cmd_start "$@" ;;
681
+ record) cmd_record "$@" ;;
682
+ stage-start) cmd_stage_start "$@" ;;
683
+ stage-end) cmd_stage_end "$@" ;;
684
+ finish) cmd_finish "$@" ;;
685
+ summary) cmd_summary "$@" ;;
686
+ trends) cmd_trends "$@" ;;
687
+ export) cmd_export "$@" ;;
688
+ help|--help|-h) show_help ;;
689
+ *)
690
+ error "Unknown command: ${cmd}"
691
+ show_help
692
+ exit 1
693
+ ;;
694
+ esac
695
+ }
696
+
697
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
698
+ main "$@"
699
+ fi