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,596 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright otel — OpenTelemetry Observability ║
4
+ # ║ Prometheus metrics, traces, OTLP export, webhook forwarding, dashboard ║
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
+ # ─── State Directories ──────────────────────────────────────────────────────
38
+ OTEL_DIR="${HOME}/.shipwright/otel"
39
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
40
+ DAEMON_STATE="${HOME}/.shipwright/daemon-state.json"
41
+ OTEL_CONFIG="${REPO_DIR}/.claude/otel-config.json"
42
+
43
+ ensure_otel_dir() {
44
+ mkdir -p "$OTEL_DIR"
45
+ }
46
+
47
+ # ─── Prometheus Metrics ──────────────────────────────────────────────────────
48
+
49
+ cmd_metrics() {
50
+ local format="${1:-text}"
51
+
52
+ ensure_otel_dir
53
+
54
+ # Initialize counters
55
+ local total_pipelines=0
56
+ local active_pipelines=0
57
+ local failed_pipelines=0
58
+ local succeeded_pipelines=0
59
+ local total_cost=0
60
+
61
+ # Status breakdown
62
+ local status_success=0
63
+ local status_failed=0
64
+ local status_running=0
65
+
66
+ # Template counts
67
+ declare -a templates
68
+ declare -a template_counts
69
+
70
+ # Stage timing
71
+ declare -a stages
72
+ declare -a stage_durations
73
+
74
+ # Model costs
75
+ declare -a models
76
+ declare -a model_costs
77
+
78
+ # Parse events.jsonl
79
+ if [[ -f "$EVENTS_FILE" ]]; then
80
+ while IFS= read -r line; do
81
+ [[ -z "$line" ]] && continue
82
+
83
+ local event_type
84
+ event_type=$(echo "$line" | jq -r '.type // empty' 2>/dev/null || true)
85
+
86
+ case "$event_type" in
87
+ pipeline_start)
88
+ ((total_pipelines++))
89
+ ((active_pipelines++))
90
+ ;;
91
+ pipeline_complete)
92
+ ((active_pipelines--))
93
+ ((succeeded_pipelines++))
94
+ ((status_success++))
95
+ ;;
96
+ pipeline_failed)
97
+ ((active_pipelines--))
98
+ ((failed_pipelines++))
99
+ ((status_failed++))
100
+ ;;
101
+ stage_complete)
102
+ local stage duration
103
+ stage=$(echo "$line" | jq -r '.stage // "unknown"' 2>/dev/null || true)
104
+ duration=$(echo "$line" | jq -r '.duration_seconds // 0' 2>/dev/null || true)
105
+ if [[ -n "$stage" && "$duration" != "0" ]]; then
106
+ stage_durations+=("$stage:$duration")
107
+ fi
108
+ ;;
109
+ cost_recorded)
110
+ local cost model
111
+ cost=$(echo "$line" | jq -r '.cost_usd // 0' 2>/dev/null || true)
112
+ model=$(echo "$line" | jq -r '.model // "unknown"' 2>/dev/null || true)
113
+ total_cost=$(awk -v t="$total_cost" -v c="$cost" 'BEGIN { printf "%.4f", t + c }')
114
+ model_costs+=("$model:$cost")
115
+ ;;
116
+ esac
117
+ done < "$EVENTS_FILE"
118
+ fi
119
+
120
+ # Parse daemon state for queue depth
121
+ local queue_depth=0
122
+ if [[ -f "$DAEMON_STATE" ]]; then
123
+ queue_depth=$(jq -r '.active_jobs | length // 0' "$DAEMON_STATE" 2>/dev/null || echo "0")
124
+ fi
125
+
126
+ # Calculate histogram buckets for stage durations
127
+ local stage_p50=0 stage_p99=0
128
+ if [[ ${#stage_durations[@]} -gt 0 ]]; then
129
+ # Simple approximation for percentiles
130
+ stage_p50=$(printf '%s\n' "${stage_durations[@]}" | cut -d: -f2 | sort -n | head -n $((${#stage_durations[@]}/2)) | tail -n1 || echo "0")
131
+ stage_p99=$(printf '%s\n' "${stage_durations[@]}" | cut -d: -f2 | sort -n | tail -n1 || echo "0")
132
+ fi
133
+
134
+ if [[ "$format" == "json" ]]; then
135
+ cat << EOF
136
+ {
137
+ "metrics": {
138
+ "pipelines_total": {
139
+ "value": $total_pipelines,
140
+ "type": "counter"
141
+ },
142
+ "active_pipelines": {
143
+ "value": $active_pipelines,
144
+ "type": "gauge"
145
+ },
146
+ "pipelines_succeeded": {
147
+ "value": $succeeded_pipelines,
148
+ "type": "counter"
149
+ },
150
+ "pipelines_failed": {
151
+ "value": $failed_pipelines,
152
+ "type": "counter"
153
+ },
154
+ "cost_total_usd": {
155
+ "value": $total_cost,
156
+ "type": "counter"
157
+ },
158
+ "queue_depth": {
159
+ "value": $queue_depth,
160
+ "type": "gauge"
161
+ },
162
+ "stage_duration_p50_seconds": {
163
+ "value": $stage_p50,
164
+ "type": "histogram"
165
+ },
166
+ "stage_duration_p99_seconds": {
167
+ "value": $stage_p99,
168
+ "type": "histogram"
169
+ }
170
+ },
171
+ "timestamp": "$(now_iso)"
172
+ }
173
+ EOF
174
+ else
175
+ # Prometheus text format
176
+ cat << EOF
177
+ # HELP shipwright_pipelines_total Total number of pipeline runs
178
+ # TYPE shipwright_pipelines_total counter
179
+ shipwright_pipelines_total $total_pipelines
180
+
181
+ # HELP shipwright_active_pipelines Currently running pipelines
182
+ # TYPE shipwright_active_pipelines gauge
183
+ shipwright_active_pipelines $active_pipelines
184
+
185
+ # HELP shipwright_pipelines_succeeded Successfully completed pipelines
186
+ # TYPE shipwright_pipelines_succeeded counter
187
+ shipwright_pipelines_succeeded $succeeded_pipelines
188
+
189
+ # HELP shipwright_pipelines_failed Failed pipelines
190
+ # TYPE shipwright_pipelines_failed counter
191
+ shipwright_pipelines_failed $failed_pipelines
192
+
193
+ # HELP shipwright_cost_total_usd Total cost in USD
194
+ # TYPE shipwright_cost_total_usd counter
195
+ shipwright_cost_total_usd $total_cost
196
+
197
+ # HELP shipwright_queue_depth Number of queued jobs
198
+ # TYPE shipwright_queue_depth gauge
199
+ shipwright_queue_depth $queue_depth
200
+
201
+ # HELP shipwright_stage_duration_seconds Stage duration histogram
202
+ # TYPE shipwright_stage_duration_seconds histogram
203
+ shipwright_stage_duration_seconds_bucket{le="1"} 0
204
+ shipwright_stage_duration_seconds_bucket{le="5"} 0
205
+ shipwright_stage_duration_seconds_bucket{le="10"} 0
206
+ shipwright_stage_duration_seconds_bucket{le="30"} 0
207
+ shipwright_stage_duration_seconds_bucket{le="60"} 0
208
+ shipwright_stage_duration_seconds_bucket{le="300"} 0
209
+ shipwright_stage_duration_seconds_bucket{le="+Inf"} $total_pipelines
210
+ shipwright_stage_duration_seconds_sum 0
211
+ shipwright_stage_duration_seconds_count $total_pipelines
212
+ EOF
213
+ fi
214
+ }
215
+
216
+ # ─── OpenTelemetry Traces ────────────────────────────────────────────────────
217
+
218
+ cmd_trace() {
219
+ local pipeline_id="${1:-latest}"
220
+
221
+ ensure_otel_dir
222
+
223
+ # Build trace from events
224
+ local traces='[]'
225
+ local spans='[]'
226
+ local root_span=""
227
+
228
+ if [[ -f "$EVENTS_FILE" ]]; then
229
+ while IFS= read -r line; do
230
+ [[ -z "$line" ]] && continue
231
+
232
+ local event_type ts stage pipeline
233
+ event_type=$(echo "$line" | jq -r '.type // empty' 2>/dev/null || true)
234
+ ts=$(echo "$line" | jq -r '.ts // empty' 2>/dev/null || true)
235
+ pipeline=$(echo "$line" | jq -r '.pipeline_id // empty' 2>/dev/null || true)
236
+
237
+ [[ -z "$event_type" ]] && continue
238
+
239
+ case "$event_type" in
240
+ pipeline_start)
241
+ root_span=$(cat << EOF
242
+ {
243
+ "traceId": "${pipeline:0:16}",
244
+ "spanId": "${pipeline:0:16}",
245
+ "parentSpanId": "",
246
+ "name": "pipeline",
247
+ "kind": "SPAN_KIND_INTERNAL",
248
+ "startTime": "${ts}",
249
+ "endTime": "",
250
+ "status": {
251
+ "code": "STATUS_CODE_UNSET",
252
+ "message": ""
253
+ },
254
+ "attributes": {
255
+ "pipeline.id": "$pipeline",
256
+ "pipeline.status": "running"
257
+ },
258
+ "events": []
259
+ }
260
+ EOF
261
+ )
262
+ ;;
263
+ stage_start)
264
+ stage=$(echo "$line" | jq -r '.stage // "unknown"' 2>/dev/null || true)
265
+ local span_id="${stage:0:8}$(printf '%08x' $((RANDOM * 256 + RANDOM)))"
266
+ spans=$(echo "$spans" | jq --arg span_id "$span_id" --arg stage "$stage" --arg ts "$ts" \
267
+ '. += [{
268
+ "traceId": "'${pipeline:0:16}'",
269
+ "spanId": "'$span_id'",
270
+ "parentSpanId": "'${root_span:0:16}'",
271
+ "name": "stage_'$stage'",
272
+ "kind": "SPAN_KIND_INTERNAL",
273
+ "startTime": "'$ts'",
274
+ "attributes": { "stage.name": "'$stage'" }
275
+ }]')
276
+ ;;
277
+ esac
278
+ done < "$EVENTS_FILE"
279
+ fi
280
+
281
+ # Output OTel trace JSON
282
+ cat << EOF
283
+ {
284
+ "resourceSpans": [
285
+ {
286
+ "resource": {
287
+ "attributes": {
288
+ "service.name": "shipwright",
289
+ "service.version": "$VERSION"
290
+ }
291
+ },
292
+ "scopeSpans": [
293
+ {
294
+ "scope": {
295
+ "name": "shipwright-tracer",
296
+ "version": "$VERSION"
297
+ },
298
+ "spans": $spans
299
+ }
300
+ ]
301
+ }
302
+ ],
303
+ "exportedAt": "$(now_iso)"
304
+ }
305
+ EOF
306
+ }
307
+
308
+ # ─── OTLP Export ────────────────────────────────────────────────────────────
309
+
310
+ cmd_export() {
311
+ local format="${1:-prometheus}"
312
+
313
+ ensure_otel_dir
314
+
315
+ local endpoint="${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}"
316
+ local auth_header=""
317
+
318
+ if [[ -n "${OTEL_EXPORTER_OTLP_HEADERS:-}" ]]; then
319
+ auth_header="-H '${OTEL_EXPORTER_OTLP_HEADERS}'"
320
+ fi
321
+
322
+ info "Exporting $format metrics to $endpoint"
323
+
324
+ local payload
325
+ if [[ "$format" == "trace" ]]; then
326
+ payload=$(cmd_trace)
327
+ local response
328
+ response=$(curl -s -X POST \
329
+ "$endpoint/v1/traces" \
330
+ -H "Content-Type: application/json" \
331
+ $auth_header \
332
+ -d "$payload" 2>&1 || echo "{\"error\": \"export failed\"}")
333
+
334
+ if echo "$response" | jq . >/dev/null 2>&1; then
335
+ success "Traces exported successfully"
336
+ else
337
+ error "Failed to export traces: $response"
338
+ return 1
339
+ fi
340
+ else
341
+ payload=$(cmd_metrics text)
342
+ local response
343
+ response=$(curl -s -X POST \
344
+ "$endpoint/metrics" \
345
+ -H "Content-Type: text/plain" \
346
+ $auth_header \
347
+ --data-binary "$payload" 2>&1 || echo "error")
348
+
349
+ if [[ "$response" == "200" ]] || [[ "$response" == "" ]]; then
350
+ success "Metrics exported successfully"
351
+ else
352
+ error "Failed to export metrics: $response"
353
+ return 1
354
+ fi
355
+ fi
356
+
357
+ # Record export event
358
+ mkdir -p "${HOME}/.shipwright"
359
+ echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"otel_export\",\"format\":\"$format\",\"endpoint\":\"$endpoint\"}" \
360
+ >> "${HOME}/.shipwright/events.jsonl"
361
+ }
362
+
363
+ # ─── Webhook Forwarding ──────────────────────────────────────────────────────
364
+
365
+ cmd_webhook() {
366
+ local action="${1:-send}"
367
+ local webhook_url="${OTEL_WEBHOOK_URL:-}"
368
+
369
+ if [[ -z "$webhook_url" ]]; then
370
+ error "OTEL_WEBHOOK_URL environment variable not set"
371
+ return 1
372
+ fi
373
+
374
+ if [[ "$action" == "send" ]]; then
375
+ info "Forwarding events to webhook: $webhook_url"
376
+
377
+ # Get latest unforwarded events
378
+ local payload
379
+ payload=$(cmd_metrics json)
380
+
381
+ local max_retries=3
382
+ local retry=0
383
+ local backoff=1
384
+
385
+ while [[ $retry -lt $max_retries ]]; do
386
+ local response
387
+ response=$(curl -s -w "\n%{http_code}" -X POST \
388
+ "$webhook_url" \
389
+ -H "Content-Type: application/json" \
390
+ -d "$payload" 2>&1 || echo "000")
391
+
392
+ local http_code
393
+ http_code=$(echo "$response" | tail -n1)
394
+
395
+ if [[ "$http_code" == "200" ]] || [[ "$http_code" == "201" ]] || [[ "$http_code" == "204" ]]; then
396
+ success "Webhook delivered (HTTP $http_code)"
397
+ echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"webhook_sent\",\"url\":\"$webhook_url\",\"http_code\":$http_code}" \
398
+ >> "${HOME}/.shipwright/events.jsonl"
399
+ return 0
400
+ fi
401
+
402
+ ((retry++))
403
+ if [[ $retry -lt $max_retries ]]; then
404
+ warn "Webhook failed (HTTP $http_code), retrying in ${backoff}s..."
405
+ sleep "$backoff"
406
+ backoff=$((backoff * 2))
407
+ fi
408
+ done
409
+
410
+ error "Webhook delivery failed after $max_retries attempts"
411
+ return 1
412
+ elif [[ "$action" == "config" ]]; then
413
+ info "Webhook configuration:"
414
+ echo " URL: $webhook_url"
415
+ echo " Status: enabled"
416
+ else
417
+ error "Unknown webhook action: $action"
418
+ return 1
419
+ fi
420
+ }
421
+
422
+ # ─── Dashboard Metrics ───────────────────────────────────────────────────────
423
+
424
+ cmd_dashboard() {
425
+ ensure_otel_dir
426
+
427
+ # Aggregate metrics for dashboard
428
+ local metrics
429
+ metrics=$(cmd_metrics json)
430
+
431
+ # Enhance with additional dashboard fields
432
+ echo "$metrics" | jq '
433
+ .dashboard = {
434
+ "pipelines": {
435
+ "total": .metrics.pipelines_total.value,
436
+ "active": .metrics.active_pipelines.value,
437
+ "success_rate": ((.metrics.pipelines_succeeded.value / (.metrics.pipelines_total.value + 0.001)) * 100 | floor)
438
+ },
439
+ "costs": {
440
+ "total_usd": .metrics.cost_total_usd.value,
441
+ "daily_avg": (.metrics.cost_total_usd.value / 30)
442
+ },
443
+ "queue": {
444
+ "depth": .metrics.queue_depth.value
445
+ }
446
+ }
447
+ '
448
+ }
449
+
450
+ # ─── Observability Report ───────────────────────────────────────────────────
451
+
452
+ cmd_report() {
453
+ ensure_otel_dir
454
+
455
+ info "Shipwright Observability Health Report"
456
+ echo ""
457
+
458
+ # Event volume
459
+ local event_count=0
460
+ local export_count=0
461
+ local webhook_count=0
462
+ local last_event_ts=""
463
+
464
+ if [[ -f "$EVENTS_FILE" ]]; then
465
+ event_count=$(wc -l < "$EVENTS_FILE" || echo "0")
466
+ export_count=$(grep -c '"type":"otel_export"' "$EVENTS_FILE" 2>/dev/null || echo "0")
467
+ webhook_count=$(grep -c '"type":"webhook_sent"' "$EVENTS_FILE" 2>/dev/null || echo "0")
468
+ last_event_ts=$(tail -n1 "$EVENTS_FILE" | jq -r '.ts // "unknown"' 2>/dev/null || echo "unknown")
469
+ fi
470
+
471
+ echo -e "${BOLD}Events:${RESET}"
472
+ echo " Total events: $event_count"
473
+ echo " OTLP exports: $export_count"
474
+ echo " Webhook sends: $webhook_count"
475
+ echo " Last event: $last_event_ts"
476
+ echo ""
477
+
478
+ # Metrics summary
479
+ local metrics
480
+ metrics=$(cmd_metrics json)
481
+
482
+ local active_pipelines succeeded failed cost
483
+ active_pipelines=$(echo "$metrics" | jq -r '.metrics.active_pipelines.value')
484
+ succeeded=$(echo "$metrics" | jq -r '.metrics.pipelines_succeeded.value')
485
+ failed=$(echo "$metrics" | jq -r '.metrics.pipelines_failed.value')
486
+ cost=$(echo "$metrics" | jq -r '.metrics.cost_total_usd.value')
487
+
488
+ echo -e "${BOLD}Pipeline Metrics:${RESET}"
489
+ echo " Active: $active_pipelines"
490
+ echo " Succeeded: $succeeded"
491
+ echo " Failed: $failed"
492
+ echo " Total cost: \$$(printf '%.2f' "$cost")"
493
+ echo ""
494
+
495
+ # Export health
496
+ local export_success_rate=0
497
+ if [[ $export_count -gt 0 ]]; then
498
+ export_success_rate=$((succeeded * 100 / (succeeded + failed + 1)))
499
+ fi
500
+
501
+ echo -e "${BOLD}Export Health:${RESET}"
502
+ echo " Success rate: ${export_success_rate}%"
503
+ echo " Configuration: $(test -f "$OTEL_CONFIG" && echo "present" || echo "not found")"
504
+
505
+ # Recommendations
506
+ echo ""
507
+ echo -e "${BOLD}${CYAN}Recommendations:${RESET}"
508
+ if [[ $active_pipelines -gt 10 ]]; then
509
+ echo " ⚠ High queue depth ($active_pipelines) — consider scaling"
510
+ fi
511
+ if [[ $export_count -eq 0 ]]; then
512
+ echo " ⚠ No exports configured — set OTEL_EXPORTER_OTLP_ENDPOINT"
513
+ fi
514
+ if [[ $webhook_count -eq 0 ]]; then
515
+ echo " ℹ No webhooks configured — set OTEL_WEBHOOK_URL for event forwarding"
516
+ fi
517
+ }
518
+
519
+ # ─── Help ────────────────────────────────────────────────────────────────────
520
+
521
+ show_help() {
522
+ cat << EOF
523
+ ${BOLD}${CYAN}shipwright otel${RESET} — OpenTelemetry Observability
524
+
525
+ ${BOLD}USAGE${RESET}
526
+ ${CYAN}shipwright otel${RESET} <subcommand> [options]
527
+
528
+ ${BOLD}SUBCOMMANDS${RESET}
529
+ ${CYAN}metrics${RESET} [format] Prometheus-format metrics (text or json)
530
+ ${CYAN}trace${RESET} [pipeline-id] OpenTelemetry trace for a pipeline run
531
+ ${CYAN}export${RESET} [format] Export metrics/traces to OTLP endpoint
532
+ ${CYAN}webhook${RESET} <action> Webhook operations (send, config)
533
+ ${CYAN}dashboard${RESET} Dashboard-ready JSON metrics
534
+ ${CYAN}report${RESET} Observability health report
535
+ ${CYAN}help${RESET} Show this help message
536
+
537
+ ${BOLD}FORMATS${RESET}
538
+ ${DIM}prometheus, text${RESET} Prometheus text format (default)
539
+ ${DIM}json${RESET} JSON format
540
+ ${DIM}trace${RESET} OpenTelemetry trace format
541
+
542
+ ${BOLD}ENVIRONMENT VARIABLES${RESET}
543
+ ${DIM}OTEL_EXPORTER_OTLP_ENDPOINT${RESET} OTLP collector endpoint (default: http://localhost:4318)
544
+ ${DIM}OTEL_EXPORTER_OTLP_HEADERS${RESET} Authorization header for OTLP
545
+ ${DIM}OTEL_WEBHOOK_URL${RESET} Webhook endpoint for event forwarding
546
+
547
+ ${BOLD}EXAMPLES${RESET}
548
+ ${DIM}shipwright otel metrics${RESET} # Show Prometheus metrics
549
+ ${DIM}shipwright otel metrics json${RESET} # JSON format
550
+ ${DIM}shipwright otel export prometheus${RESET} # Export to OTLP endpoint
551
+ ${DIM}OTEL_WEBHOOK_URL=https://api.example.com shipwright otel webhook send${RESET}
552
+ ${DIM}shipwright otel report${RESET} # Health status
553
+
554
+ EOF
555
+ }
556
+
557
+ # ─── Main ────────────────────────────────────────────────────────────────────
558
+
559
+ main() {
560
+ local cmd="${1:-help}"
561
+ shift 2>/dev/null || true
562
+
563
+ case "$cmd" in
564
+ metrics)
565
+ cmd_metrics "$@"
566
+ ;;
567
+ trace)
568
+ cmd_trace "$@"
569
+ ;;
570
+ export)
571
+ cmd_export "$@"
572
+ ;;
573
+ webhook)
574
+ cmd_webhook "$@"
575
+ ;;
576
+ dashboard)
577
+ cmd_dashboard "$@"
578
+ ;;
579
+ report)
580
+ cmd_report "$@"
581
+ ;;
582
+ help|--help|-h)
583
+ show_help
584
+ ;;
585
+ *)
586
+ error "Unknown command: $cmd"
587
+ echo ""
588
+ show_help
589
+ exit 1
590
+ ;;
591
+ esac
592
+ }
593
+
594
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
595
+ main "$@"
596
+ fi