shipwright-cli 1.7.1 → 1.9.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.
- package/.claude/agents/code-reviewer.md +90 -0
- package/.claude/agents/devops-engineer.md +142 -0
- package/.claude/agents/pipeline-agent.md +80 -0
- package/.claude/agents/shell-script-specialist.md +150 -0
- package/.claude/agents/test-specialist.md +196 -0
- package/.claude/hooks/post-tool-use.sh +38 -0
- package/.claude/hooks/pre-tool-use.sh +25 -0
- package/.claude/hooks/session-started.sh +37 -0
- package/README.md +212 -814
- package/claude-code/CLAUDE.md.shipwright +54 -0
- package/claude-code/hooks/notify-idle.sh +2 -2
- package/claude-code/hooks/session-start.sh +24 -0
- package/claude-code/hooks/task-completed.sh +6 -2
- package/claude-code/settings.json.template +12 -0
- package/dashboard/public/app.js +4422 -0
- package/dashboard/public/index.html +816 -0
- package/dashboard/public/styles.css +4755 -0
- package/dashboard/server.ts +4315 -0
- package/docs/KNOWN-ISSUES.md +18 -10
- package/docs/TIPS.md +38 -26
- package/docs/patterns/README.md +33 -23
- package/package.json +9 -5
- package/scripts/adapters/iterm2-adapter.sh +1 -1
- package/scripts/adapters/tmux-adapter.sh +52 -23
- package/scripts/adapters/wezterm-adapter.sh +26 -14
- package/scripts/lib/compat.sh +200 -0
- package/scripts/lib/helpers.sh +72 -0
- package/scripts/postinstall.mjs +72 -13
- package/scripts/{cct → sw} +109 -21
- package/scripts/sw-adversarial.sh +274 -0
- package/scripts/sw-architecture-enforcer.sh +330 -0
- package/scripts/sw-checkpoint.sh +390 -0
- package/scripts/{cct-cleanup.sh → sw-cleanup.sh} +3 -1
- package/scripts/sw-connect.sh +619 -0
- package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
- package/scripts/{cct-daemon.sh → sw-daemon.sh} +2217 -204
- package/scripts/sw-dashboard.sh +477 -0
- package/scripts/sw-developer-simulation.sh +252 -0
- package/scripts/sw-docs.sh +635 -0
- package/scripts/sw-doctor.sh +907 -0
- package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
- package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
- package/scripts/sw-github-checks.sh +521 -0
- package/scripts/sw-github-deploy.sh +533 -0
- package/scripts/sw-github-graphql.sh +972 -0
- package/scripts/sw-heartbeat.sh +293 -0
- package/scripts/{cct-init.sh → sw-init.sh} +144 -11
- package/scripts/sw-intelligence.sh +1196 -0
- package/scripts/sw-jira.sh +643 -0
- package/scripts/sw-launchd.sh +364 -0
- package/scripts/sw-linear.sh +648 -0
- package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
- package/scripts/{cct-loop.sh → sw-loop.sh} +534 -44
- package/scripts/{cct-memory.sh → sw-memory.sh} +321 -38
- package/scripts/sw-patrol-meta.sh +417 -0
- package/scripts/sw-pipeline-composer.sh +455 -0
- package/scripts/{cct-pipeline.sh → sw-pipeline.sh} +2319 -178
- package/scripts/sw-predictive.sh +820 -0
- package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
- package/scripts/{cct-ps.sh → sw-ps.sh} +6 -4
- package/scripts/{cct-reaper.sh → sw-reaper.sh} +6 -4
- package/scripts/sw-remote.sh +687 -0
- package/scripts/sw-self-optimize.sh +947 -0
- package/scripts/sw-session.sh +519 -0
- package/scripts/sw-setup.sh +234 -0
- package/scripts/sw-status.sh +605 -0
- package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
- package/scripts/sw-tmux.sh +591 -0
- package/scripts/sw-tracker-jira.sh +277 -0
- package/scripts/sw-tracker-linear.sh +292 -0
- package/scripts/sw-tracker.sh +409 -0
- package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
- package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
- package/templates/pipelines/autonomous.json +27 -5
- package/templates/pipelines/full.json +12 -0
- package/templates/pipelines/standard.json +12 -0
- package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
- package/tmux/templates/accessibility.json +34 -0
- package/tmux/templates/api-design.json +35 -0
- package/tmux/templates/architecture.json +1 -0
- package/tmux/templates/bug-fix.json +9 -0
- package/tmux/templates/code-review.json +1 -0
- package/tmux/templates/compliance.json +36 -0
- package/tmux/templates/data-pipeline.json +36 -0
- package/tmux/templates/debt-paydown.json +34 -0
- package/tmux/templates/devops.json +1 -0
- package/tmux/templates/documentation.json +1 -0
- package/tmux/templates/exploration.json +1 -0
- package/tmux/templates/feature-dev.json +1 -0
- package/tmux/templates/full-stack.json +8 -0
- package/tmux/templates/i18n.json +34 -0
- package/tmux/templates/incident-response.json +36 -0
- package/tmux/templates/migration.json +1 -0
- package/tmux/templates/observability.json +35 -0
- package/tmux/templates/onboarding.json +33 -0
- package/tmux/templates/performance.json +35 -0
- package/tmux/templates/refactor.json +1 -0
- package/tmux/templates/release.json +35 -0
- package/tmux/templates/security-audit.json +8 -0
- package/tmux/templates/spike.json +34 -0
- package/tmux/templates/testing.json +1 -0
- package/tmux/tmux.conf +98 -9
- package/scripts/cct-doctor.sh +0 -414
- package/scripts/cct-session.sh +0 -284
- 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.
|
|
9
|
+
VERSION="1.9.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}/.
|
|
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}/.
|
|
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
|
-
#
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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 "
|
|
539
|
-
echo -e "
|
|
540
|
-
echo -e "
|
|
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
|
;;
|