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.
- package/README.md +114 -36
- package/completions/_shipwright +212 -32
- package/completions/shipwright.bash +97 -25
- package/docs/strategy/01-market-research.md +619 -0
- package/docs/strategy/02-mission-and-brand.md +587 -0
- package/docs/strategy/03-gtm-and-roadmap.md +759 -0
- package/docs/strategy/QUICK-START.txt +289 -0
- package/docs/strategy/README.md +172 -0
- package/package.json +4 -2
- package/scripts/sw +208 -1
- package/scripts/sw-activity.sh +500 -0
- package/scripts/sw-adaptive.sh +925 -0
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +613 -0
- package/scripts/sw-autonomous.sh +664 -0
- package/scripts/sw-changelog.sh +704 -0
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +637 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +432 -130
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +540 -0
- package/scripts/sw-decompose.sh +539 -0
- package/scripts/sw-deps.sh +551 -0
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +412 -0
- package/scripts/sw-docs-agent.sh +539 -0
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +59 -1
- package/scripts/sw-dora.sh +615 -0
- package/scripts/sw-durable.sh +710 -0
- package/scripts/sw-e2e-orchestrator.sh +535 -0
- package/scripts/sw-eventbus.sh +393 -0
- package/scripts/sw-feedback.sh +471 -0
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +567 -0
- package/scripts/sw-fleet-viz.sh +404 -0
- package/scripts/sw-fleet.sh +8 -1
- package/scripts/sw-github-app.sh +596 -0
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +569 -0
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +559 -0
- package/scripts/sw-incident.sh +617 -0
- package/scripts/sw-init.sh +88 -1
- package/scripts/sw-instrument.sh +699 -0
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +363 -28
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +64 -3
- package/scripts/sw-memory.sh +1 -1
- package/scripts/sw-mission-control.sh +487 -0
- package/scripts/sw-model-router.sh +545 -0
- package/scripts/sw-otel.sh +596 -0
- package/scripts/sw-oversight.sh +689 -0
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +687 -24
- package/scripts/sw-pm.sh +693 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +1 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +573 -0
- package/scripts/sw-regression.sh +642 -0
- package/scripts/sw-release-manager.sh +736 -0
- package/scripts/sw-release.sh +706 -0
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +520 -0
- package/scripts/sw-retro.sh +691 -0
- package/scripts/sw-scale.sh +444 -0
- package/scripts/sw-security-audit.sh +505 -0
- package/scripts/sw-self-optimize.sh +1 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +658 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +583 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +515 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +485 -0
- package/scripts/sw-tracker-github.sh +188 -0
- package/scripts/sw-tracker-jira.sh +172 -0
- package/scripts/sw-tracker-linear.sh +251 -0
- package/scripts/sw-tracker.sh +117 -2
- package/scripts/sw-triage.sh +603 -0
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +677 -0
- package/scripts/sw-webhook.sh +627 -0
- package/scripts/sw-widgets.sh +530 -0
- package/scripts/sw-worktree.sh +1 -1
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright model-router — Intelligent Model Routing & Cost Optimization ║
|
|
4
|
+
# ║ Route tasks to optimal Claude models based on complexity and stage ║
|
|
5
|
+
# ║ Escalate on failure · Track costs · A/B test configurations ║
|
|
6
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
9
|
+
|
|
10
|
+
VERSION="2.0.0"
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
13
|
+
|
|
14
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
15
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
16
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
17
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
18
|
+
GREEN='\033[38;2;74;222;128m' # success
|
|
19
|
+
YELLOW='\033[38;2;250;204;21m' # warning
|
|
20
|
+
RED='\033[38;2;248;113;113m' # error
|
|
21
|
+
DIM='\033[2m'
|
|
22
|
+
BOLD='\033[1m'
|
|
23
|
+
RESET='\033[0m'
|
|
24
|
+
|
|
25
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
26
|
+
# shellcheck source=lib/compat.sh
|
|
27
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
28
|
+
|
|
29
|
+
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
30
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
31
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
32
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
33
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
34
|
+
|
|
35
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
36
|
+
now_epoch() { date +%s; }
|
|
37
|
+
|
|
38
|
+
# ─── File Paths ────────────────────────────────────────────────────────────
|
|
39
|
+
MODEL_ROUTING_CONFIG="${HOME}/.shipwright/model-routing.json"
|
|
40
|
+
MODEL_USAGE_LOG="${HOME}/.shipwright/model-usage.jsonl"
|
|
41
|
+
AB_RESULTS_FILE="${HOME}/.shipwright/ab-results.jsonl"
|
|
42
|
+
|
|
43
|
+
# ─── Model Costs (per million tokens) ───────────────────────────────────────
|
|
44
|
+
HAIKU_INPUT_COST="0.80"
|
|
45
|
+
HAIKU_OUTPUT_COST="4.00"
|
|
46
|
+
SONNET_INPUT_COST="3.00"
|
|
47
|
+
SONNET_OUTPUT_COST="15.00"
|
|
48
|
+
OPUS_INPUT_COST="15.00"
|
|
49
|
+
OPUS_OUTPUT_COST="75.00"
|
|
50
|
+
|
|
51
|
+
# ─── Default Routing Rules ──────────────────────────────────────────────────
|
|
52
|
+
# Stages that default to haiku (low complexity, fast)
|
|
53
|
+
HAIKU_STAGES="intake|monitor"
|
|
54
|
+
# Stages that default to sonnet (medium complexity)
|
|
55
|
+
SONNET_STAGES="test|review"
|
|
56
|
+
# Stages that default to opus (high complexity, needs deep thinking)
|
|
57
|
+
OPUS_STAGES="plan|design|build|compound_quality"
|
|
58
|
+
|
|
59
|
+
# ─── Complexity Thresholds ──────────────────────────────────────────────────
|
|
60
|
+
COMPLEXITY_LOW=30 # Below this: use sonnet
|
|
61
|
+
COMPLEXITY_HIGH=80 # Above this: use opus
|
|
62
|
+
|
|
63
|
+
# ─── Ensure Config File Exists ──────────────────────────────────────────────
|
|
64
|
+
ensure_config() {
|
|
65
|
+
mkdir -p "${HOME}/.shipwright"
|
|
66
|
+
|
|
67
|
+
if [[ ! -f "$MODEL_ROUTING_CONFIG" ]]; then
|
|
68
|
+
cat > "$MODEL_ROUTING_CONFIG" <<'CONFIG'
|
|
69
|
+
{
|
|
70
|
+
"version": "1.0",
|
|
71
|
+
"default_routing": {
|
|
72
|
+
"intake": "haiku",
|
|
73
|
+
"plan": "opus",
|
|
74
|
+
"design": "opus",
|
|
75
|
+
"build": "opus",
|
|
76
|
+
"test": "sonnet",
|
|
77
|
+
"review": "sonnet",
|
|
78
|
+
"compound_quality": "opus",
|
|
79
|
+
"pr": "sonnet",
|
|
80
|
+
"merge": "sonnet",
|
|
81
|
+
"deploy": "sonnet",
|
|
82
|
+
"validate": "haiku",
|
|
83
|
+
"monitor": "haiku"
|
|
84
|
+
},
|
|
85
|
+
"complexity_thresholds": {
|
|
86
|
+
"low": 30,
|
|
87
|
+
"high": 80
|
|
88
|
+
},
|
|
89
|
+
"escalation_policy": "linear",
|
|
90
|
+
"cost_aware_mode": false,
|
|
91
|
+
"max_cost_per_pipeline": 50.0,
|
|
92
|
+
"a_b_test": {
|
|
93
|
+
"enabled": false,
|
|
94
|
+
"percentage": 10,
|
|
95
|
+
"variant": "cost-optimized"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
CONFIG
|
|
99
|
+
success "Created default routing config at $MODEL_ROUTING_CONFIG"
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# ─── Determine Model by Stage and Complexity ────────────────────────────────
|
|
104
|
+
route_model() {
|
|
105
|
+
local stage="$1"
|
|
106
|
+
local complexity="${2:-50}"
|
|
107
|
+
|
|
108
|
+
# Validate inputs
|
|
109
|
+
if [[ -z "$stage" ]]; then
|
|
110
|
+
error "stage is required"
|
|
111
|
+
return 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if ! [[ "$complexity" =~ ^[0-9]+$ ]] || [[ "$complexity" -lt 0 ]] || [[ "$complexity" -gt 100 ]]; then
|
|
115
|
+
error "complexity must be 0-100, got: $complexity"
|
|
116
|
+
return 1
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
local model=""
|
|
120
|
+
|
|
121
|
+
# Complexity-based override (applies to all stages)
|
|
122
|
+
if [[ "$complexity" -lt "$COMPLEXITY_LOW" ]]; then
|
|
123
|
+
model="sonnet"
|
|
124
|
+
elif [[ "$complexity" -gt "$COMPLEXITY_HIGH" ]]; then
|
|
125
|
+
model="opus"
|
|
126
|
+
else
|
|
127
|
+
# Stage-based routing for medium complexity
|
|
128
|
+
if [[ "$stage" =~ $HAIKU_STAGES ]]; then
|
|
129
|
+
model="haiku"
|
|
130
|
+
elif [[ "$stage" =~ $SONNET_STAGES ]]; then
|
|
131
|
+
model="sonnet"
|
|
132
|
+
elif [[ "$stage" =~ $OPUS_STAGES ]]; then
|
|
133
|
+
model="opus"
|
|
134
|
+
else
|
|
135
|
+
# Default to sonnet for unknown stages
|
|
136
|
+
model="sonnet"
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
echo "$model"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# ─── Escalate to Next Model Tier ───────────────────────────────────────────
|
|
144
|
+
escalate_model() {
|
|
145
|
+
local current_model="$1"
|
|
146
|
+
|
|
147
|
+
if [[ -z "$current_model" ]]; then
|
|
148
|
+
error "current model is required"
|
|
149
|
+
return 1
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
local next_model=""
|
|
153
|
+
case "$current_model" in
|
|
154
|
+
haiku) next_model="sonnet" ;;
|
|
155
|
+
sonnet) next_model="opus" ;;
|
|
156
|
+
opus) next_model="opus" ;; # Already at top
|
|
157
|
+
*) error "Unknown model: $current_model"; return 1 ;;
|
|
158
|
+
esac
|
|
159
|
+
|
|
160
|
+
echo "$next_model"
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# ─── Show Configuration ─────────────────────────────────────────────────────
|
|
164
|
+
show_config() {
|
|
165
|
+
ensure_config
|
|
166
|
+
|
|
167
|
+
info "Model Routing Configuration"
|
|
168
|
+
echo ""
|
|
169
|
+
|
|
170
|
+
if command -v jq &>/dev/null; then
|
|
171
|
+
jq . "$MODEL_ROUTING_CONFIG" 2>/dev/null || cat "$MODEL_ROUTING_CONFIG"
|
|
172
|
+
else
|
|
173
|
+
cat "$MODEL_ROUTING_CONFIG"
|
|
174
|
+
fi
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# ─── Set Configuration Value ───────────────────────────────────────────────
|
|
178
|
+
set_config() {
|
|
179
|
+
local key="$1"
|
|
180
|
+
local value="$2"
|
|
181
|
+
|
|
182
|
+
if [[ -z "$key" ]] || [[ -z "$value" ]]; then
|
|
183
|
+
error "Usage: shipwright model config set <key> <value>"
|
|
184
|
+
return 1
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
ensure_config
|
|
188
|
+
|
|
189
|
+
if ! command -v jq &>/dev/null; then
|
|
190
|
+
error "jq is required for config updates"
|
|
191
|
+
return 1
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
# Use jq to safely update the config
|
|
195
|
+
local tmp_config
|
|
196
|
+
tmp_config=$(mktemp)
|
|
197
|
+
|
|
198
|
+
if [[ "$value" == "true" ]] || [[ "$value" == "false" ]]; then
|
|
199
|
+
jq ".${key} = ${value}" "$MODEL_ROUTING_CONFIG" > "$tmp_config"
|
|
200
|
+
elif [[ "$value" =~ ^[0-9]+\.?[0-9]*$ ]]; then
|
|
201
|
+
jq ".${key} = ${value}" "$MODEL_ROUTING_CONFIG" > "$tmp_config"
|
|
202
|
+
else
|
|
203
|
+
jq ".${key} = \"${value}\"" "$MODEL_ROUTING_CONFIG" > "$tmp_config"
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
mv "$tmp_config" "$MODEL_ROUTING_CONFIG"
|
|
207
|
+
success "Updated $key = $value"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# ─── Estimate Total Pipeline Cost ──────────────────────────────────────────
|
|
211
|
+
estimate_cost() {
|
|
212
|
+
local template="${1:-standard}"
|
|
213
|
+
local complexity="${2:-50}"
|
|
214
|
+
|
|
215
|
+
info "Estimating cost for template: $template, complexity: $complexity"
|
|
216
|
+
echo ""
|
|
217
|
+
|
|
218
|
+
# Typical token usage by stage (estimated)
|
|
219
|
+
local stage_tokens=(
|
|
220
|
+
"intake:5000"
|
|
221
|
+
"plan:50000"
|
|
222
|
+
"design:50000"
|
|
223
|
+
"build:100000"
|
|
224
|
+
"test:30000"
|
|
225
|
+
"review:20000"
|
|
226
|
+
"compound_quality:40000"
|
|
227
|
+
"pr:10000"
|
|
228
|
+
"merge:5000"
|
|
229
|
+
"deploy:5000"
|
|
230
|
+
"validate:5000"
|
|
231
|
+
"monitor:5000"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
local total_cost="0"
|
|
235
|
+
local total_input_tokens="0"
|
|
236
|
+
local total_output_tokens="0"
|
|
237
|
+
|
|
238
|
+
echo -e "${BOLD}Stage${RESET} $(printf '%-15s' 'Model') $(printf '%-15s' 'Input Tokens') $(printf '%-15s' 'Output Tokens') $(printf '%-10s' 'Cost')"
|
|
239
|
+
echo "─────────────────────────────────────────────────────────────────────"
|
|
240
|
+
|
|
241
|
+
for stage_info in "${stage_tokens[@]}"; do
|
|
242
|
+
local stage="${stage_info%%:*}"
|
|
243
|
+
local tokens="${stage_info#*:}"
|
|
244
|
+
|
|
245
|
+
# Estimate input/output split (roughly 70% input, 30% output)
|
|
246
|
+
local input_tokens=$((tokens * 7 / 10))
|
|
247
|
+
local output_tokens=$((tokens * 3 / 10))
|
|
248
|
+
|
|
249
|
+
local model
|
|
250
|
+
model=$(route_model "$stage" "$complexity")
|
|
251
|
+
|
|
252
|
+
local input_cost="0" output_cost="0"
|
|
253
|
+
case "$model" in
|
|
254
|
+
haiku)
|
|
255
|
+
input_cost=$(awk "BEGIN {printf \"%.4f\", $input_tokens * $HAIKU_INPUT_COST / 1000000}")
|
|
256
|
+
output_cost=$(awk "BEGIN {printf \"%.4f\", $output_tokens * $HAIKU_OUTPUT_COST / 1000000}")
|
|
257
|
+
;;
|
|
258
|
+
sonnet)
|
|
259
|
+
input_cost=$(awk "BEGIN {printf \"%.4f\", $input_tokens * $SONNET_INPUT_COST / 1000000}")
|
|
260
|
+
output_cost=$(awk "BEGIN {printf \"%.4f\", $output_tokens * $SONNET_OUTPUT_COST / 1000000}")
|
|
261
|
+
;;
|
|
262
|
+
opus)
|
|
263
|
+
input_cost=$(awk "BEGIN {printf \"%.4f\", $input_tokens * $OPUS_INPUT_COST / 1000000}")
|
|
264
|
+
output_cost=$(awk "BEGIN {printf \"%.4f\", $output_tokens * $OPUS_OUTPUT_COST / 1000000}")
|
|
265
|
+
;;
|
|
266
|
+
esac
|
|
267
|
+
|
|
268
|
+
local stage_cost
|
|
269
|
+
stage_cost=$(awk "BEGIN {printf \"%.4f\", $input_cost + $output_cost}")
|
|
270
|
+
total_cost=$(awk "BEGIN {printf \"%.4f\", $total_cost + $stage_cost}")
|
|
271
|
+
total_input_tokens=$((total_input_tokens + input_tokens))
|
|
272
|
+
total_output_tokens=$((total_output_tokens + output_tokens))
|
|
273
|
+
|
|
274
|
+
printf "%-15s %-15s %-15d %-15d \$%-10s\n" "$stage" "$model" "$input_tokens" "$output_tokens" "$stage_cost"
|
|
275
|
+
done
|
|
276
|
+
|
|
277
|
+
echo "─────────────────────────────────────────────────────────────────────"
|
|
278
|
+
echo -e "${BOLD}Total${RESET} ${BOLD}\$${total_cost}${RESET}"
|
|
279
|
+
echo ""
|
|
280
|
+
echo "Tokens: $total_input_tokens input + $total_output_tokens output = $((total_input_tokens + total_output_tokens)) total"
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# ─── Record Model Usage ─────────────────────────────────────────────────────
|
|
284
|
+
record_usage() {
|
|
285
|
+
local stage="$1"
|
|
286
|
+
local model="$2"
|
|
287
|
+
local input_tokens="${3:-0}"
|
|
288
|
+
local output_tokens="${4:-0}"
|
|
289
|
+
|
|
290
|
+
mkdir -p "${HOME}/.shipwright"
|
|
291
|
+
|
|
292
|
+
local cost
|
|
293
|
+
cost=$(awk "BEGIN {}" ) # Calculate actual cost
|
|
294
|
+
case "$model" in
|
|
295
|
+
haiku)
|
|
296
|
+
cost=$(awk "BEGIN {printf \"%.4f\", ($input_tokens * $HAIKU_INPUT_COST + $output_tokens * $HAIKU_OUTPUT_COST) / 1000000}")
|
|
297
|
+
;;
|
|
298
|
+
sonnet)
|
|
299
|
+
cost=$(awk "BEGIN {printf \"%.4f\", ($input_tokens * $SONNET_INPUT_COST + $output_tokens * $SONNET_OUTPUT_COST) / 1000000}")
|
|
300
|
+
;;
|
|
301
|
+
opus)
|
|
302
|
+
cost=$(awk "BEGIN {printf \"%.4f\", ($input_tokens * $OPUS_INPUT_COST + $output_tokens * $OPUS_OUTPUT_COST) / 1000000}")
|
|
303
|
+
;;
|
|
304
|
+
esac
|
|
305
|
+
|
|
306
|
+
local record="{\"ts\":\"$(now_iso)\",\"stage\":\"$stage\",\"model\":\"$model\",\"input_tokens\":$input_tokens,\"output_tokens\":$output_tokens,\"cost\":$cost}"
|
|
307
|
+
echo "$record" >> "$MODEL_USAGE_LOG"
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# ─── A/B Test Configuration ────────────────────────────────────────────────
|
|
311
|
+
configure_ab_test() {
|
|
312
|
+
local percentage="${1:-10}"
|
|
313
|
+
local variant="${2:-cost-optimized}"
|
|
314
|
+
|
|
315
|
+
if ! [[ "$percentage" =~ ^[0-9]+$ ]] || [[ "$percentage" -lt 0 ]] || [[ "$percentage" -gt 100 ]]; then
|
|
316
|
+
error "Percentage must be 0-100, got: $percentage"
|
|
317
|
+
return 1
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
ensure_config
|
|
321
|
+
|
|
322
|
+
if ! command -v jq &>/dev/null; then
|
|
323
|
+
error "jq is required for A/B test configuration"
|
|
324
|
+
return 1
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
local tmp_config
|
|
328
|
+
tmp_config=$(mktemp)
|
|
329
|
+
|
|
330
|
+
jq ".a_b_test = {\"enabled\": true, \"percentage\": $percentage, \"variant\": \"$variant\"}" \
|
|
331
|
+
"$MODEL_ROUTING_CONFIG" > "$tmp_config"
|
|
332
|
+
|
|
333
|
+
mv "$tmp_config" "$MODEL_ROUTING_CONFIG"
|
|
334
|
+
success "Configured A/B test: $percentage% of pipelines will use $variant variant"
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# ─── Log A/B Test Result ───────────────────────────────────────────────────
|
|
338
|
+
log_ab_result() {
|
|
339
|
+
local run_id="$1"
|
|
340
|
+
local variant="$2"
|
|
341
|
+
local success_status="$3"
|
|
342
|
+
local cost="$4"
|
|
343
|
+
local duration="${5:-0}"
|
|
344
|
+
|
|
345
|
+
mkdir -p "${HOME}/.shipwright"
|
|
346
|
+
|
|
347
|
+
local record="{\"ts\":\"$(now_iso)\",\"run_id\":\"$run_id\",\"variant\":\"$variant\",\"success\":$success_status,\"cost\":$cost,\"duration_seconds\":$duration}"
|
|
348
|
+
echo "$record" >> "$AB_RESULTS_FILE"
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# ─── Show Usage Report ──────────────────────────────────────────────────────
|
|
352
|
+
show_report() {
|
|
353
|
+
info "Model Usage Report"
|
|
354
|
+
echo ""
|
|
355
|
+
|
|
356
|
+
if [[ ! -f "$MODEL_USAGE_LOG" ]]; then
|
|
357
|
+
warn "No usage data yet. Run pipelines to collect metrics."
|
|
358
|
+
return 0
|
|
359
|
+
fi
|
|
360
|
+
|
|
361
|
+
if ! command -v jq &>/dev/null; then
|
|
362
|
+
error "jq is required to view reports"
|
|
363
|
+
return 1
|
|
364
|
+
fi
|
|
365
|
+
|
|
366
|
+
# Summary stats
|
|
367
|
+
local total_runs
|
|
368
|
+
total_runs=$(wc -l < "$MODEL_USAGE_LOG" || echo "0")
|
|
369
|
+
|
|
370
|
+
local haiku_runs
|
|
371
|
+
haiku_runs=$(grep -c '"model":"haiku"' "$MODEL_USAGE_LOG" || true)
|
|
372
|
+
|
|
373
|
+
local sonnet_runs
|
|
374
|
+
sonnet_runs=$(grep -c '"model":"sonnet"' "$MODEL_USAGE_LOG" || true)
|
|
375
|
+
|
|
376
|
+
local opus_runs
|
|
377
|
+
opus_runs=$(grep -c '"model":"opus"' "$MODEL_USAGE_LOG" || true)
|
|
378
|
+
|
|
379
|
+
local total_cost
|
|
380
|
+
total_cost=$(jq -s 'map(.cost) | add' "$MODEL_USAGE_LOG" 2>/dev/null || echo "0")
|
|
381
|
+
|
|
382
|
+
echo -e "${BOLD}Summary${RESET}"
|
|
383
|
+
echo " Total runs: $total_runs"
|
|
384
|
+
echo " Haiku runs: $haiku_runs"
|
|
385
|
+
echo " Sonnet runs: $sonnet_runs"
|
|
386
|
+
echo " Opus runs: $opus_runs"
|
|
387
|
+
echo " Total cost: \$$total_cost"
|
|
388
|
+
echo ""
|
|
389
|
+
|
|
390
|
+
echo -e "${BOLD}Cost Per Model${RESET}"
|
|
391
|
+
jq -s '
|
|
392
|
+
group_by(.model) |
|
|
393
|
+
map({
|
|
394
|
+
model: .[0].model,
|
|
395
|
+
count: length,
|
|
396
|
+
total_cost: (map(.cost) | add),
|
|
397
|
+
avg_cost: (map(.cost) | add / length),
|
|
398
|
+
input_tokens: (map(.input_tokens) | add),
|
|
399
|
+
output_tokens: (map(.output_tokens) | add)
|
|
400
|
+
}) |
|
|
401
|
+
sort_by(.model)
|
|
402
|
+
' "$MODEL_USAGE_LOG" 2>/dev/null | jq -r '.[] | " \(.model): \(.count) runs, $\(.total_cost | tostring), avg $\(.avg_cost | round)"' || true
|
|
403
|
+
|
|
404
|
+
echo ""
|
|
405
|
+
echo -e "${BOLD}Top Stages by Cost${RESET}"
|
|
406
|
+
jq -s '
|
|
407
|
+
group_by(.stage) |
|
|
408
|
+
map({stage: .[0].stage, cost: (map(.cost) | add), runs: length}) |
|
|
409
|
+
sort_by(.cost) | reverse | .[0:5]
|
|
410
|
+
' "$MODEL_USAGE_LOG" 2>/dev/null | jq -r '.[] | " \(.stage): $\(.cost), \(.runs) runs"' || true
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
# ─── Show A/B Test Results ─────────────────────────────────────────────────
|
|
414
|
+
show_ab_results() {
|
|
415
|
+
info "A/B Test Results"
|
|
416
|
+
echo ""
|
|
417
|
+
|
|
418
|
+
if [[ ! -f "$AB_RESULTS_FILE" ]]; then
|
|
419
|
+
warn "No A/B test data yet."
|
|
420
|
+
return 0
|
|
421
|
+
fi
|
|
422
|
+
|
|
423
|
+
if ! command -v jq &>/dev/null; then
|
|
424
|
+
error "jq is required to view A/B test results"
|
|
425
|
+
return 1
|
|
426
|
+
fi
|
|
427
|
+
|
|
428
|
+
jq -s '
|
|
429
|
+
group_by(.variant) |
|
|
430
|
+
map({
|
|
431
|
+
variant: .[0].variant,
|
|
432
|
+
total_runs: length,
|
|
433
|
+
successful: (map(select(.success == true)) | length),
|
|
434
|
+
failed: (map(select(.success == false)) | length),
|
|
435
|
+
success_rate: ((map(select(.success == true)) | length) / length * 100),
|
|
436
|
+
avg_cost: (map(.cost) | add / length),
|
|
437
|
+
total_cost: (map(.cost) | add),
|
|
438
|
+
avg_duration: (map(.duration_seconds) | add / length)
|
|
439
|
+
})
|
|
440
|
+
' "$AB_RESULTS_FILE" 2>/dev/null | jq -r '.[] | "\(.variant):\n Runs: \(.total_runs)\n Success: \(.successful)/\(.total_runs) (\(.success_rate | round)%)\n Avg Cost: $\(.avg_cost | round)\n Total Cost: $\(.total_cost | round)\n Avg Duration: \(.avg_duration | round)s"' || true
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
# ─── Help Text ──────────────────────────────────────────────────────────────
|
|
444
|
+
show_help() {
|
|
445
|
+
echo -e "${BOLD}shipwright model${RESET} — Intelligent Model Routing & Optimization"
|
|
446
|
+
echo ""
|
|
447
|
+
echo -e "${BOLD}USAGE${RESET}"
|
|
448
|
+
echo " ${CYAN}shipwright model${RESET} <subcommand> [options]"
|
|
449
|
+
echo ""
|
|
450
|
+
echo -e "${BOLD}SUBCOMMANDS${RESET}"
|
|
451
|
+
echo " ${CYAN}route${RESET} <stage> [complexity] Route task to optimal model (returns: haiku|sonnet|opus)"
|
|
452
|
+
echo " ${CYAN}escalate${RESET} <model> Get next tier model (haiku→sonnet→opus)"
|
|
453
|
+
echo " ${CYAN}config${RESET} [show|set <key> <val>] Show/set routing configuration"
|
|
454
|
+
echo " ${CYAN}estimate${RESET} [template] [complexity] Estimate pipeline cost"
|
|
455
|
+
echo " ${CYAN}ab-test${RESET} [enable|disable] [pct] [variant] Configure A/B testing"
|
|
456
|
+
echo " ${CYAN}report${RESET} Show model usage and cost report"
|
|
457
|
+
echo " ${CYAN}ab-results${RESET} Show A/B test results"
|
|
458
|
+
echo " ${CYAN}help${RESET} Show this help message"
|
|
459
|
+
echo ""
|
|
460
|
+
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
461
|
+
echo " ${DIM}shipwright model route plan 65${RESET} # Route 'plan' stage with 65% complexity"
|
|
462
|
+
echo " ${DIM}shipwright model escalate haiku${RESET} # Upgrade from haiku"
|
|
463
|
+
echo " ${DIM}shipwright model config show${RESET} # View routing rules"
|
|
464
|
+
echo " ${DIM}shipwright model estimate standard 50${RESET} # Estimate standard pipeline cost"
|
|
465
|
+
echo " ${DIM}shipwright model ab-test enable 15 cost-optimized${RESET} # 15% A/B test"
|
|
466
|
+
echo " ${DIM}shipwright model report${RESET} # Show usage stats"
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
# ─── Main ───────────────────────────────────────────────────────────────────
|
|
470
|
+
main() {
|
|
471
|
+
local subcommand="${1:-help}"
|
|
472
|
+
|
|
473
|
+
case "$subcommand" in
|
|
474
|
+
route)
|
|
475
|
+
shift 2>/dev/null || true
|
|
476
|
+
route_model "$@"
|
|
477
|
+
;;
|
|
478
|
+
escalate)
|
|
479
|
+
shift 2>/dev/null || true
|
|
480
|
+
escalate_model "$@"
|
|
481
|
+
;;
|
|
482
|
+
config)
|
|
483
|
+
shift 2>/dev/null || true
|
|
484
|
+
case "${1:-show}" in
|
|
485
|
+
show)
|
|
486
|
+
show_config
|
|
487
|
+
;;
|
|
488
|
+
set)
|
|
489
|
+
shift 2>/dev/null || true
|
|
490
|
+
set_config "$@"
|
|
491
|
+
;;
|
|
492
|
+
*)
|
|
493
|
+
error "Unknown config subcommand: $1"
|
|
494
|
+
show_help
|
|
495
|
+
exit 1
|
|
496
|
+
;;
|
|
497
|
+
esac
|
|
498
|
+
;;
|
|
499
|
+
estimate)
|
|
500
|
+
shift 2>/dev/null || true
|
|
501
|
+
estimate_cost "$@"
|
|
502
|
+
;;
|
|
503
|
+
ab-test)
|
|
504
|
+
shift 2>/dev/null || true
|
|
505
|
+
if [[ "${1:-}" == "enable" ]]; then
|
|
506
|
+
shift
|
|
507
|
+
configure_ab_test "$@"
|
|
508
|
+
elif [[ "${1:-}" == "disable" ]]; then
|
|
509
|
+
# Disable A/B testing
|
|
510
|
+
ensure_config
|
|
511
|
+
if command -v jq &>/dev/null; then
|
|
512
|
+
local tmp_config
|
|
513
|
+
tmp_config=$(mktemp)
|
|
514
|
+
jq ".a_b_test.enabled = false" "$MODEL_ROUTING_CONFIG" > "$tmp_config"
|
|
515
|
+
mv "$tmp_config" "$MODEL_ROUTING_CONFIG"
|
|
516
|
+
success "Disabled A/B testing"
|
|
517
|
+
else
|
|
518
|
+
error "jq is required"
|
|
519
|
+
return 1
|
|
520
|
+
fi
|
|
521
|
+
else
|
|
522
|
+
configure_ab_test "$@"
|
|
523
|
+
fi
|
|
524
|
+
;;
|
|
525
|
+
|
|
526
|
+
report)
|
|
527
|
+
show_report
|
|
528
|
+
;;
|
|
529
|
+
ab-results)
|
|
530
|
+
show_ab_results
|
|
531
|
+
;;
|
|
532
|
+
help|--help|-h)
|
|
533
|
+
show_help
|
|
534
|
+
;;
|
|
535
|
+
*)
|
|
536
|
+
error "Unknown subcommand: $subcommand"
|
|
537
|
+
show_help
|
|
538
|
+
exit 1
|
|
539
|
+
;;
|
|
540
|
+
esac
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
544
|
+
main "$@"
|
|
545
|
+
fi
|