shipwright-cli 1.7.1 → 1.10.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 (115) hide show
  1. package/.claude/agents/code-reviewer.md +90 -0
  2. package/.claude/agents/devops-engineer.md +142 -0
  3. package/.claude/agents/pipeline-agent.md +80 -0
  4. package/.claude/agents/shell-script-specialist.md +150 -0
  5. package/.claude/agents/test-specialist.md +196 -0
  6. package/.claude/hooks/post-tool-use.sh +45 -0
  7. package/.claude/hooks/pre-tool-use.sh +25 -0
  8. package/.claude/hooks/session-started.sh +37 -0
  9. package/README.md +212 -814
  10. package/claude-code/CLAUDE.md.shipwright +54 -0
  11. package/claude-code/hooks/notify-idle.sh +2 -2
  12. package/claude-code/hooks/session-start.sh +24 -0
  13. package/claude-code/hooks/task-completed.sh +6 -2
  14. package/claude-code/settings.json.template +12 -0
  15. package/dashboard/public/app.js +4422 -0
  16. package/dashboard/public/index.html +816 -0
  17. package/dashboard/public/styles.css +4755 -0
  18. package/dashboard/server.ts +4315 -0
  19. package/docs/KNOWN-ISSUES.md +18 -10
  20. package/docs/TIPS.md +38 -26
  21. package/docs/patterns/README.md +33 -23
  22. package/package.json +9 -5
  23. package/scripts/adapters/iterm2-adapter.sh +1 -1
  24. package/scripts/adapters/tmux-adapter.sh +52 -23
  25. package/scripts/adapters/wezterm-adapter.sh +26 -14
  26. package/scripts/lib/compat.sh +200 -0
  27. package/scripts/lib/helpers.sh +72 -0
  28. package/scripts/postinstall.mjs +72 -13
  29. package/scripts/{cct → sw} +118 -22
  30. package/scripts/sw-adversarial.sh +274 -0
  31. package/scripts/sw-architecture-enforcer.sh +330 -0
  32. package/scripts/sw-checkpoint.sh +468 -0
  33. package/scripts/sw-cleanup.sh +359 -0
  34. package/scripts/sw-connect.sh +619 -0
  35. package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
  36. package/scripts/sw-daemon.sh +5574 -0
  37. package/scripts/sw-dashboard.sh +477 -0
  38. package/scripts/sw-developer-simulation.sh +252 -0
  39. package/scripts/sw-docs.sh +635 -0
  40. package/scripts/sw-doctor.sh +907 -0
  41. package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
  42. package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
  43. package/scripts/sw-github-checks.sh +521 -0
  44. package/scripts/sw-github-deploy.sh +533 -0
  45. package/scripts/sw-github-graphql.sh +972 -0
  46. package/scripts/sw-heartbeat.sh +293 -0
  47. package/scripts/{cct-init.sh → sw-init.sh} +144 -11
  48. package/scripts/sw-intelligence.sh +1196 -0
  49. package/scripts/sw-jira.sh +643 -0
  50. package/scripts/sw-launchd.sh +364 -0
  51. package/scripts/sw-linear.sh +648 -0
  52. package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
  53. package/scripts/sw-loop.sh +2217 -0
  54. package/scripts/{cct-memory.sh → sw-memory.sh} +514 -36
  55. package/scripts/sw-patrol-meta.sh +417 -0
  56. package/scripts/sw-pipeline-composer.sh +455 -0
  57. package/scripts/sw-pipeline-vitals.sh +1096 -0
  58. package/scripts/sw-pipeline.sh +7593 -0
  59. package/scripts/sw-predictive.sh +820 -0
  60. package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
  61. package/scripts/{cct-ps.sh → sw-ps.sh} +9 -6
  62. package/scripts/{cct-reaper.sh → sw-reaper.sh} +10 -6
  63. package/scripts/sw-remote.sh +687 -0
  64. package/scripts/sw-self-optimize.sh +1048 -0
  65. package/scripts/sw-session.sh +541 -0
  66. package/scripts/sw-setup.sh +234 -0
  67. package/scripts/sw-status.sh +796 -0
  68. package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
  69. package/scripts/sw-tmux.sh +591 -0
  70. package/scripts/sw-tracker-jira.sh +277 -0
  71. package/scripts/sw-tracker-linear.sh +292 -0
  72. package/scripts/sw-tracker.sh +409 -0
  73. package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
  74. package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
  75. package/templates/pipelines/autonomous.json +35 -6
  76. package/templates/pipelines/cost-aware.json +21 -0
  77. package/templates/pipelines/deployed.json +40 -6
  78. package/templates/pipelines/enterprise.json +16 -2
  79. package/templates/pipelines/fast.json +19 -0
  80. package/templates/pipelines/full.json +28 -2
  81. package/templates/pipelines/hotfix.json +19 -0
  82. package/templates/pipelines/standard.json +31 -0
  83. package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
  84. package/tmux/templates/accessibility.json +34 -0
  85. package/tmux/templates/api-design.json +35 -0
  86. package/tmux/templates/architecture.json +1 -0
  87. package/tmux/templates/bug-fix.json +9 -0
  88. package/tmux/templates/code-review.json +1 -0
  89. package/tmux/templates/compliance.json +36 -0
  90. package/tmux/templates/data-pipeline.json +36 -0
  91. package/tmux/templates/debt-paydown.json +34 -0
  92. package/tmux/templates/devops.json +1 -0
  93. package/tmux/templates/documentation.json +1 -0
  94. package/tmux/templates/exploration.json +1 -0
  95. package/tmux/templates/feature-dev.json +1 -0
  96. package/tmux/templates/full-stack.json +8 -0
  97. package/tmux/templates/i18n.json +34 -0
  98. package/tmux/templates/incident-response.json +36 -0
  99. package/tmux/templates/migration.json +1 -0
  100. package/tmux/templates/observability.json +35 -0
  101. package/tmux/templates/onboarding.json +33 -0
  102. package/tmux/templates/performance.json +35 -0
  103. package/tmux/templates/refactor.json +1 -0
  104. package/tmux/templates/release.json +35 -0
  105. package/tmux/templates/security-audit.json +8 -0
  106. package/tmux/templates/spike.json +34 -0
  107. package/tmux/templates/testing.json +1 -0
  108. package/tmux/tmux.conf +98 -9
  109. package/scripts/cct-cleanup.sh +0 -172
  110. package/scripts/cct-daemon.sh +0 -3189
  111. package/scripts/cct-doctor.sh +0 -414
  112. package/scripts/cct-loop.sh +0 -1332
  113. package/scripts/cct-pipeline.sh +0 -3844
  114. package/scripts/cct-session.sh +0 -284
  115. package/scripts/cct-status.sh +0 -169
@@ -4,8 +4,9 @@
4
4
  # ║ Tracks spending · Enforces budgets · Stage breakdowns · Trend analysis ║
5
5
  # ╚═══════════════════════════════════════════════════════════════════════════╝
6
6
  set -euo pipefail
7
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
7
8
 
8
- VERSION="1.7.1"
9
+ VERSION="1.10.0"
9
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
11
12
 
@@ -20,6 +21,9 @@ DIM='\033[2m'
20
21
  BOLD='\033[1m'
21
22
  RESET='\033[0m'
22
23
 
24
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
25
+ # shellcheck source=lib/compat.sh
26
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
23
27
  # ─── Output Helpers ─────────────────────────────────────────────────────────
24
28
  info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
25
29
  success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
@@ -41,7 +45,7 @@ format_duration() {
41
45
  }
42
46
 
43
47
  # ─── Structured Event Log ──────────────────────────────────────────────────
44
- EVENTS_FILE="${HOME}/.claude-teams/events.jsonl"
48
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
45
49
 
46
50
  emit_event() {
47
51
  local event_type="$1"
@@ -57,7 +61,7 @@ emit_event() {
57
61
  json_fields="${json_fields},\"${key}\":\"${val}\""
58
62
  fi
59
63
  done
60
- mkdir -p "${HOME}/.claude-teams"
64
+ mkdir -p "${HOME}/.shipwright"
61
65
  echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
62
66
  }
63
67
 
@@ -73,13 +77,36 @@ ensure_cost_dir() {
73
77
  }
74
78
 
75
79
  # ─── Model Pricing (USD per million tokens) ────────────────────────────────
76
- # Pricing as of 2025
77
- OPUS_INPUT_PER_M=15.00
78
- OPUS_OUTPUT_PER_M=75.00
79
- SONNET_INPUT_PER_M=3.00
80
- SONNET_OUTPUT_PER_M=15.00
81
- HAIKU_INPUT_PER_M=0.25
82
- HAIKU_OUTPUT_PER_M=1.25
80
+ # Default pricing (fallback when no config file exists)
81
+ _DEFAULT_OPUS_INPUT_PER_M=15.00
82
+ _DEFAULT_OPUS_OUTPUT_PER_M=75.00
83
+ _DEFAULT_SONNET_INPUT_PER_M=3.00
84
+ _DEFAULT_SONNET_OUTPUT_PER_M=15.00
85
+ _DEFAULT_HAIKU_INPUT_PER_M=0.25
86
+ _DEFAULT_HAIKU_OUTPUT_PER_M=1.25
87
+
88
+ MODEL_PRICING_FILE="${HOME}/.shipwright/model-pricing.json"
89
+
90
+ # Load pricing from config file or use defaults
91
+ _cost_load_pricing() {
92
+ if [[ -f "$MODEL_PRICING_FILE" ]]; then
93
+ OPUS_INPUT_PER_M=$(jq -r '.opus.input_per_m // empty' "$MODEL_PRICING_FILE" 2>/dev/null || true)
94
+ OPUS_OUTPUT_PER_M=$(jq -r '.opus.output_per_m // empty' "$MODEL_PRICING_FILE" 2>/dev/null || true)
95
+ SONNET_INPUT_PER_M=$(jq -r '.sonnet.input_per_m // empty' "$MODEL_PRICING_FILE" 2>/dev/null || true)
96
+ SONNET_OUTPUT_PER_M=$(jq -r '.sonnet.output_per_m // empty' "$MODEL_PRICING_FILE" 2>/dev/null || true)
97
+ HAIKU_INPUT_PER_M=$(jq -r '.haiku.input_per_m // empty' "$MODEL_PRICING_FILE" 2>/dev/null || true)
98
+ HAIKU_OUTPUT_PER_M=$(jq -r '.haiku.output_per_m // empty' "$MODEL_PRICING_FILE" 2>/dev/null || true)
99
+ fi
100
+ # Fallback to defaults for any missing values
101
+ OPUS_INPUT_PER_M="${OPUS_INPUT_PER_M:-$_DEFAULT_OPUS_INPUT_PER_M}"
102
+ OPUS_OUTPUT_PER_M="${OPUS_OUTPUT_PER_M:-$_DEFAULT_OPUS_OUTPUT_PER_M}"
103
+ SONNET_INPUT_PER_M="${SONNET_INPUT_PER_M:-$_DEFAULT_SONNET_INPUT_PER_M}"
104
+ SONNET_OUTPUT_PER_M="${SONNET_OUTPUT_PER_M:-$_DEFAULT_SONNET_OUTPUT_PER_M}"
105
+ HAIKU_INPUT_PER_M="${HAIKU_INPUT_PER_M:-$_DEFAULT_HAIKU_INPUT_PER_M}"
106
+ HAIKU_OUTPUT_PER_M="${HAIKU_OUTPUT_PER_M:-$_DEFAULT_HAIKU_OUTPUT_PER_M}"
107
+ }
108
+
109
+ _cost_load_pricing
83
110
 
84
111
  # cost_calculate <input_tokens> <output_tokens> <model>
85
112
  # Returns the cost in USD (floating point)
@@ -128,27 +155,32 @@ cost_record() {
128
155
  local cost_usd
129
156
  cost_usd=$(cost_calculate "$input_tokens" "$output_tokens" "$model")
130
157
 
131
- local tmp_file
132
- tmp_file=$(mktemp)
133
- jq --argjson input "$input_tokens" \
134
- --argjson output "$output_tokens" \
135
- --arg model "$model" \
136
- --arg stage "$stage" \
137
- --arg issue "$issue" \
138
- --arg cost "$cost_usd" \
139
- --arg ts "$(now_iso)" \
140
- --argjson epoch "$(now_epoch)" \
141
- '.entries += [{
142
- input_tokens: $input,
143
- output_tokens: $output,
144
- model: $model,
145
- stage: $stage,
146
- issue: $issue,
147
- cost_usd: ($cost | tonumber),
148
- ts: $ts,
149
- ts_epoch: $epoch
150
- }] | .entries = (.entries | .[-1000:])' \
151
- "$COST_FILE" > "$tmp_file" && mv "$tmp_file" "$COST_FILE"
158
+ (
159
+ if command -v flock &>/dev/null; then
160
+ flock -w 10 200 2>/dev/null || { warn "Cost lock timeout"; }
161
+ fi
162
+ local tmp_file
163
+ tmp_file=$(mktemp "${COST_FILE}.tmp.XXXXXX")
164
+ jq --argjson input "$input_tokens" \
165
+ --argjson output "$output_tokens" \
166
+ --arg model "$model" \
167
+ --arg stage "$stage" \
168
+ --arg issue "$issue" \
169
+ --arg cost "$cost_usd" \
170
+ --arg ts "$(now_iso)" \
171
+ --argjson epoch "$(now_epoch)" \
172
+ '.entries += [{
173
+ input_tokens: $input,
174
+ output_tokens: $output,
175
+ model: $model,
176
+ stage: $stage,
177
+ issue: $issue,
178
+ cost_usd: ($cost | tonumber),
179
+ ts: $ts,
180
+ ts_epoch: $epoch
181
+ }] | .entries = (.entries | .[-1000:])' \
182
+ "$COST_FILE" > "$tmp_file" && mv "$tmp_file" "$COST_FILE" || rm -f "$tmp_file"
183
+ ) 200>"${COST_FILE}.lock"
152
184
 
153
185
  emit_event "cost.record" \
154
186
  "input_tokens=${input_tokens}" \
@@ -248,6 +280,268 @@ cost_remaining_budget() {
248
280
  echo "$remaining"
249
281
  }
250
282
 
283
+ # ─── Cost-Per-Outcome ──────────────────────────────────────────────────────
284
+
285
+ OUTCOMES_FILE="${COST_DIR}/cost-outcomes.json"
286
+
287
+ _ensure_outcomes_file() {
288
+ mkdir -p "$COST_DIR"
289
+ [[ -f "$OUTCOMES_FILE" ]] || echo '{"outcomes":[],"summary":{"total_pipelines":0,"successful":0,"failed":0,"total_cost":0}}' > "$OUTCOMES_FILE"
290
+ }
291
+
292
+ # cost_record_outcome <pipeline_id> <total_cost> <success> <model_used> <template>
293
+ # Records pipeline outcome with cost for efficiency tracking.
294
+ cost_record_outcome() {
295
+ local pipeline_id="${1:-unknown}"
296
+ local total_cost="${2:-0}"
297
+ local success_flag="${3:-false}"
298
+ local model_used="${4:-sonnet}"
299
+ local template="${5:-standard}"
300
+
301
+ _ensure_outcomes_file
302
+
303
+ local tmp_file
304
+ tmp_file=$(mktemp "${TMPDIR:-/tmp}/sw-cost-outcome.XXXXXX")
305
+ jq --arg pid "$pipeline_id" \
306
+ --arg cost "$total_cost" \
307
+ --arg success "$success_flag" \
308
+ --arg model "$model_used" \
309
+ --arg tpl "$template" \
310
+ --arg ts "$(now_iso)" \
311
+ --argjson epoch "$(now_epoch)" \
312
+ '
313
+ .outcomes += [{
314
+ pipeline_id: $pid,
315
+ cost_usd: ($cost | tonumber),
316
+ success: ($success == "true"),
317
+ model: $model,
318
+ template: $tpl,
319
+ ts: $ts,
320
+ ts_epoch: $epoch
321
+ }] |
322
+ .outcomes = (.outcomes | .[-500:]) |
323
+ .summary.total_pipelines = (.outcomes | length) |
324
+ .summary.successful = ([.outcomes[] | select(.success == true)] | length) |
325
+ .summary.failed = ([.outcomes[] | select(.success == false)] | length) |
326
+ .summary.total_cost = ([.outcomes[].cost_usd] | add // 0 | . * 100 | round / 100)
327
+ ' "$OUTCOMES_FILE" > "$tmp_file" && mv "$tmp_file" "$OUTCOMES_FILE" || rm -f "$tmp_file"
328
+
329
+ emit_event "cost.outcome_recorded" \
330
+ "pipeline_id=$pipeline_id" \
331
+ "cost_usd=$total_cost" \
332
+ "success=$success_flag" \
333
+ "model=$model_used" \
334
+ "template=$template"
335
+ }
336
+
337
+ # cost_show_efficiency [--json]
338
+ # Displays cost/success efficiency metrics.
339
+ cost_show_efficiency() {
340
+ local json_output=false
341
+ [[ "${1:-}" == "--json" ]] && json_output=true
342
+
343
+ _ensure_outcomes_file
344
+
345
+ local total_pipelines successful failed total_cost
346
+ total_pipelines=$(jq '.summary.total_pipelines // 0' "$OUTCOMES_FILE" 2>/dev/null || echo "0")
347
+ successful=$(jq '.summary.successful // 0' "$OUTCOMES_FILE" 2>/dev/null || echo "0")
348
+ failed=$(jq '.summary.failed // 0' "$OUTCOMES_FILE" 2>/dev/null || echo "0")
349
+ total_cost=$(jq '.summary.total_cost // 0' "$OUTCOMES_FILE" 2>/dev/null || echo "0")
350
+
351
+ local cost_per_success="N/A"
352
+ local cost_per_pipeline="N/A"
353
+ if [[ "$successful" -gt 0 ]]; then
354
+ cost_per_success=$(awk -v tc="$total_cost" -v s="$successful" 'BEGIN { printf "%.2f", tc / s }')
355
+ fi
356
+ if [[ "$total_pipelines" -gt 0 ]]; then
357
+ cost_per_pipeline=$(awk -v tc="$total_cost" -v tp="$total_pipelines" 'BEGIN { printf "%.2f", tc / tp }')
358
+ fi
359
+
360
+ local success_rate="0"
361
+ if [[ "$total_pipelines" -gt 0 ]]; then
362
+ success_rate=$(awk -v s="$successful" -v tp="$total_pipelines" 'BEGIN { printf "%.1f", (s / tp) * 100 }')
363
+ fi
364
+
365
+ # Model breakdown from outcomes
366
+ local model_breakdown
367
+ model_breakdown=$(jq '[.outcomes[] | {model, cost_usd, success}] |
368
+ group_by(.model) | map({
369
+ model: .[0].model,
370
+ count: length,
371
+ cost: ([.[].cost_usd] | add // 0 | . * 100 | round / 100),
372
+ successes: ([.[] | select(.success == true)] | length)
373
+ }) | sort_by(-.cost)' "$OUTCOMES_FILE" 2>/dev/null || echo "[]")
374
+
375
+ # Template breakdown
376
+ local template_breakdown
377
+ template_breakdown=$(jq '[.outcomes[] | {template, cost_usd, success}] |
378
+ group_by(.template) | map({
379
+ template: .[0].template,
380
+ count: length,
381
+ cost: ([.[].cost_usd] | add // 0 | . * 100 | round / 100),
382
+ successes: ([.[] | select(.success == true)] | length)
383
+ }) | sort_by(-.cost)' "$OUTCOMES_FILE" 2>/dev/null || echo "[]")
384
+
385
+ # Savings opportunity: estimate savings if sonnet handled opus stages
386
+ local opus_cost sonnet_equivalent_cost savings_estimate
387
+ opus_cost=$(jq '[.outcomes[] | select(.model == "opus") | .cost_usd] | add // 0' "$OUTCOMES_FILE" 2>/dev/null || echo "0")
388
+ # Sonnet is roughly 5x cheaper than opus (3/15 input, 15/75 output)
389
+ sonnet_equivalent_cost=$(awk -v oc="$opus_cost" 'BEGIN { printf "%.2f", oc / 5.0 }')
390
+ savings_estimate=$(awk -v oc="$opus_cost" -v sc="$sonnet_equivalent_cost" 'BEGIN { printf "%.2f", oc - sc }')
391
+
392
+ if [[ "$json_output" == "true" ]]; then
393
+ jq -n \
394
+ --argjson total "$total_pipelines" \
395
+ --argjson successful "$successful" \
396
+ --argjson failed "$failed" \
397
+ --argjson total_cost "$total_cost" \
398
+ --arg cost_per_success "$cost_per_success" \
399
+ --arg cost_per_pipeline "$cost_per_pipeline" \
400
+ --arg success_rate "$success_rate" \
401
+ --argjson model_breakdown "$model_breakdown" \
402
+ --argjson template_breakdown "$template_breakdown" \
403
+ --argjson savings_estimate "$savings_estimate" \
404
+ '{
405
+ total_pipelines: $total,
406
+ successful: $successful,
407
+ failed: $failed,
408
+ total_cost_usd: $total_cost,
409
+ cost_per_success_usd: $cost_per_success,
410
+ cost_per_pipeline_usd: $cost_per_pipeline,
411
+ success_rate_pct: $success_rate,
412
+ by_model: $model_breakdown,
413
+ by_template: $template_breakdown,
414
+ potential_savings_usd: $savings_estimate
415
+ }'
416
+ return 0
417
+ fi
418
+
419
+ echo ""
420
+ echo -e "${BOLD} COST EFFICIENCY${RESET}"
421
+ echo -e " Pipelines total ${CYAN}${total_pipelines}${RESET}"
422
+ echo -e " Successful ${GREEN}${successful}${RESET} (${success_rate}%)"
423
+ echo -e " Failed ${RED}${failed}${RESET}"
424
+ echo -e " Total cost ${CYAN}\$${total_cost}${RESET}"
425
+ echo -e " Cost per pipeline \$${cost_per_pipeline}"
426
+ echo -e " Cost per success \$${cost_per_success}"
427
+ echo ""
428
+
429
+ # Model breakdown
430
+ local model_count
431
+ model_count=$(echo "$model_breakdown" | jq 'length' 2>/dev/null || echo "0")
432
+ if [[ "$model_count" -gt 0 ]]; then
433
+ echo -e "${BOLD} BY MODEL${RESET}"
434
+ echo "$model_breakdown" | jq -r '.[] | " \(.model)\t$\(.cost)\t\(.successes)/\(.count) successful"' 2>/dev/null | \
435
+ while IFS=$'\t' read -r mdl cost stats; do
436
+ printf " %-12s %-12s %s\n" "$mdl" "$cost" "$stats"
437
+ done
438
+ echo ""
439
+ fi
440
+
441
+ # Template breakdown
442
+ local tpl_count
443
+ tpl_count=$(echo "$template_breakdown" | jq 'length' 2>/dev/null || echo "0")
444
+ if [[ "$tpl_count" -gt 0 ]]; then
445
+ echo -e "${BOLD} BY TEMPLATE${RESET}"
446
+ echo "$template_breakdown" | jq -r '.[] | " \(.template)\t$\(.cost)\t\(.successes)/\(.count) successful"' 2>/dev/null | \
447
+ while IFS=$'\t' read -r tpl cost stats; do
448
+ printf " %-16s %-12s %s\n" "$tpl" "$cost" "$stats"
449
+ done
450
+ echo ""
451
+ fi
452
+
453
+ # Savings opportunity
454
+ if awk -v s="$savings_estimate" 'BEGIN { exit !(s > 0.01) }' 2>/dev/null; then
455
+ echo -e "${BOLD} SAVINGS OPPORTUNITY${RESET}"
456
+ echo -e " If sonnet handled opus stages: ~\$${savings_estimate} potential savings"
457
+ echo -e " ${DIM}(Based on ~5x cost difference between opus and sonnet)${RESET}"
458
+ echo ""
459
+ fi
460
+ }
461
+
462
+ # ─── Pricing Management ──────────────────────────────────────────────────────
463
+
464
+ # cost_update_pricing [model] [input_per_m] [output_per_m]
465
+ # Updates model pricing config. With no args, shows current pricing.
466
+ cost_update_pricing() {
467
+ local model="${1:-}"
468
+ local input_price="${2:-}"
469
+ local output_price="${3:-}"
470
+
471
+ mkdir -p "$COST_DIR"
472
+
473
+ if [[ -z "$model" ]]; then
474
+ # Show current pricing
475
+ echo ""
476
+ echo -e "${BOLD} Model Pricing${RESET} (per 1M tokens)"
477
+ echo -e " ${DIM}Source: ${MODEL_PRICING_FILE:-defaults}${RESET}"
478
+ echo ""
479
+ printf " %-12s %-12s %-12s\n" "Model" "Input" "Output"
480
+ printf " %-12s %-12s %-12s\n" "─────" "─────" "──────"
481
+ printf " %-12s \$%-11s \$%-11s\n" "opus" "$OPUS_INPUT_PER_M" "$OPUS_OUTPUT_PER_M"
482
+ printf " %-12s \$%-11s \$%-11s\n" "sonnet" "$SONNET_INPUT_PER_M" "$SONNET_OUTPUT_PER_M"
483
+ printf " %-12s \$%-11s \$%-11s\n" "haiku" "$HAIKU_INPUT_PER_M" "$HAIKU_OUTPUT_PER_M"
484
+ echo ""
485
+ if [[ -f "$MODEL_PRICING_FILE" ]]; then
486
+ echo -e " ${GREEN}Using custom pricing from config${RESET}"
487
+ else
488
+ echo -e " ${DIM}Using default pricing (no config file)${RESET}"
489
+ fi
490
+ echo ""
491
+ return 0
492
+ fi
493
+
494
+ if [[ -z "$input_price" || -z "$output_price" ]]; then
495
+ error "Usage: shipwright cost update-pricing <model> <input_per_m> <output_per_m>"
496
+ return 1
497
+ fi
498
+
499
+ # Validate model name
500
+ case "$model" in
501
+ opus|sonnet|haiku) ;;
502
+ *) error "Unknown model: $model (expected opus, sonnet, or haiku)"; return 1 ;;
503
+ esac
504
+
505
+ # Validate prices are numbers
506
+ if ! echo "$input_price" | grep -qE '^[0-9]+\.?[0-9]*$'; then
507
+ error "Invalid input price: $input_price"
508
+ return 1
509
+ fi
510
+ if ! echo "$output_price" | grep -qE '^[0-9]+\.?[0-9]*$'; then
511
+ error "Invalid output price: $output_price"
512
+ return 1
513
+ fi
514
+
515
+ # Initialize config if missing
516
+ if [[ ! -f "$MODEL_PRICING_FILE" ]]; then
517
+ jq -n \
518
+ --argjson oi "$_DEFAULT_OPUS_INPUT_PER_M" --argjson oo "$_DEFAULT_OPUS_OUTPUT_PER_M" \
519
+ --argjson si "$_DEFAULT_SONNET_INPUT_PER_M" --argjson so "$_DEFAULT_SONNET_OUTPUT_PER_M" \
520
+ --argjson hi "$_DEFAULT_HAIKU_INPUT_PER_M" --argjson ho "$_DEFAULT_HAIKU_OUTPUT_PER_M" \
521
+ '{
522
+ opus: {input_per_m: $oi, output_per_m: $oo},
523
+ sonnet: {input_per_m: $si, output_per_m: $so},
524
+ haiku: {input_per_m: $hi, output_per_m: $ho},
525
+ updated_at: ""
526
+ }' > "$MODEL_PRICING_FILE"
527
+ fi
528
+
529
+ local tmp_file
530
+ tmp_file=$(mktemp "${TMPDIR:-/tmp}/sw-cost-pricing.XXXXXX")
531
+ jq --arg model "$model" \
532
+ --argjson input "$input_price" \
533
+ --argjson output "$output_price" \
534
+ --arg ts "$(now_iso)" \
535
+ '.[$model].input_per_m = $input | .[$model].output_per_m = $output | .updated_at = $ts' \
536
+ "$MODEL_PRICING_FILE" > "$tmp_file" && mv "$tmp_file" "$MODEL_PRICING_FILE" || rm -f "$tmp_file"
537
+
538
+ # Reload pricing
539
+ _cost_load_pricing
540
+
541
+ success "Pricing updated: ${model} → \$${input_price}/\$${output_price} per 1M tokens (in/out)"
542
+ emit_event "cost.pricing_updated" "model=$model" "input_per_m=$input_price" "output_per_m=$output_price"
543
+ }
544
+
251
545
  # ─── Dashboard ─────────────────────────────────────────────────────────────
252
546
 
253
547
  cost_dashboard() {
@@ -452,6 +746,30 @@ cost_dashboard() {
452
746
  echo ""
453
747
  fi
454
748
 
749
+ # Efficiency summary (if outcome data exists)
750
+ if [[ -f "$OUTCOMES_FILE" ]]; then
751
+ local outcome_count
752
+ outcome_count=$(jq '.summary.total_pipelines // 0' "$OUTCOMES_FILE" 2>/dev/null || echo "0")
753
+ if [[ "$outcome_count" -gt 0 ]]; then
754
+ local eff_successful eff_total_cost eff_cost_per_success eff_rate
755
+ eff_successful=$(jq '.summary.successful // 0' "$OUTCOMES_FILE" 2>/dev/null || echo "0")
756
+ eff_total_cost=$(jq '.summary.total_cost // 0' "$OUTCOMES_FILE" 2>/dev/null || echo "0")
757
+ eff_rate="0"
758
+ if [[ "$outcome_count" -gt 0 ]]; then
759
+ eff_rate=$(awk -v s="$eff_successful" -v t="$outcome_count" 'BEGIN { printf "%.0f", (s/t)*100 }')
760
+ fi
761
+ eff_cost_per_success="N/A"
762
+ if [[ "$eff_successful" -gt 0 ]]; then
763
+ eff_cost_per_success=$(awk -v tc="$eff_total_cost" -v s="$eff_successful" 'BEGIN { printf "%.2f", tc / s }')
764
+ fi
765
+
766
+ echo -e "${BOLD} EFFICIENCY${RESET}"
767
+ echo -e " Success rate ${eff_rate}% (${eff_successful}/${outcome_count})"
768
+ echo -e " Cost per success \$${eff_cost_per_success}"
769
+ echo ""
770
+ fi
771
+ fi
772
+
455
773
  echo -e "${PURPLE}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
456
774
  echo ""
457
775
  }
@@ -531,19 +849,26 @@ show_help() {
531
849
  echo ""
532
850
  echo -e "${BOLD}PIPELINE INTEGRATION${RESET}"
533
851
  echo -e " ${CYAN}record${RESET} <in> <out> <model> <stage> [issue] Record token usage"
852
+ echo -e " ${CYAN}record-outcome${RESET} <id> <cost> <success> <model> <tpl> Record pipeline outcome"
534
853
  echo -e " ${CYAN}calculate${RESET} <in> <out> <model> Calculate cost (no record)"
535
854
  echo -e " ${CYAN}check-budget${RESET} [estimated_usd] Check budget before starting"
536
855
  echo ""
856
+ echo -e "${BOLD}EFFICIENCY${RESET}"
857
+ echo -e " ${CYAN}efficiency${RESET} Show cost/success efficiency metrics"
858
+ echo -e " ${CYAN}efficiency${RESET} --json JSON output"
859
+ echo ""
537
860
  echo -e "${BOLD}MODEL PRICING${RESET}"
538
- echo -e " opus \$15.00 / \$75.00 per 1M tokens (in/out)"
539
- echo -e " sonnet \$3.00 / \$15.00 per 1M tokens (in/out)"
540
- echo -e " haiku \$0.25 / \$1.25 per 1M tokens (in/out)"
861
+ echo -e " ${CYAN}update-pricing${RESET} [model] [in] [out] Update model pricing"
862
+ echo -e " ${CYAN}update-pricing${RESET} Show current pricing"
863
+ echo -e " ${DIM}Current: opus \$${OPUS_INPUT_PER_M}/\$${OPUS_OUTPUT_PER_M}, sonnet \$${SONNET_INPUT_PER_M}/\$${SONNET_OUTPUT_PER_M}, haiku \$${HAIKU_INPUT_PER_M}/\$${HAIKU_OUTPUT_PER_M}${RESET}"
541
864
  echo ""
542
865
  echo -e "${BOLD}EXAMPLES${RESET}"
543
866
  echo -e " ${DIM}shipwright cost show${RESET} # 7-day cost summary"
544
867
  echo -e " ${DIM}shipwright cost show --period 30 --by-stage${RESET} # 30-day breakdown by stage"
545
868
  echo -e " ${DIM}shipwright cost budget set 50.00${RESET} # Set \$50/day limit"
546
869
  echo -e " ${DIM}shipwright cost budget show${RESET} # Check current budget"
870
+ echo -e " ${DIM}shipwright cost efficiency${RESET} # Cost per successful pipeline"
871
+ echo -e " ${DIM}shipwright cost update-pricing opus 15.00 75.00${RESET} # Update opus pricing"
547
872
  echo -e " ${DIM}shipwright cost calculate 50000 10000 opus${RESET} # Estimate cost"
548
873
  }
549
874
 
@@ -568,6 +893,9 @@ case "$SUBCOMMAND" in
568
893
  record)
569
894
  cost_record "$@"
570
895
  ;;
896
+ record-outcome)
897
+ cost_record_outcome "$@"
898
+ ;;
571
899
  calculate)
572
900
  cost_calculate "$@"
573
901
  echo ""
@@ -578,6 +906,12 @@ case "$SUBCOMMAND" in
578
906
  check-budget)
579
907
  cost_check_budget "$@"
580
908
  ;;
909
+ efficiency)
910
+ cost_show_efficiency "$@"
911
+ ;;
912
+ update-pricing)
913
+ cost_update_pricing "$@"
914
+ ;;
581
915
  help|--help|-h)
582
916
  show_help
583
917
  ;;