shipwright-cli 1.7.0 → 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.
Files changed (106) 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 +38 -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} +109 -21
  30. package/scripts/sw-adversarial.sh +274 -0
  31. package/scripts/sw-architecture-enforcer.sh +330 -0
  32. package/scripts/sw-checkpoint.sh +390 -0
  33. package/scripts/{cct-cleanup.sh → sw-cleanup.sh} +3 -1
  34. package/scripts/sw-connect.sh +619 -0
  35. package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
  36. package/scripts/{cct-daemon.sh → sw-daemon.sh} +2217 -204
  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/sw-init.sh +522 -0
  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/{cct-loop.sh → sw-loop.sh} +534 -44
  54. package/scripts/{cct-memory.sh → sw-memory.sh} +321 -38
  55. package/scripts/sw-patrol-meta.sh +417 -0
  56. package/scripts/sw-pipeline-composer.sh +455 -0
  57. package/scripts/{cct-pipeline.sh → sw-pipeline.sh} +2319 -178
  58. package/scripts/sw-predictive.sh +820 -0
  59. package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
  60. package/scripts/{cct-ps.sh → sw-ps.sh} +6 -4
  61. package/scripts/{cct-reaper.sh → sw-reaper.sh} +6 -4
  62. package/scripts/sw-remote.sh +687 -0
  63. package/scripts/sw-self-optimize.sh +947 -0
  64. package/scripts/sw-session.sh +519 -0
  65. package/scripts/sw-setup.sh +234 -0
  66. package/scripts/sw-status.sh +605 -0
  67. package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
  68. package/scripts/sw-tmux.sh +591 -0
  69. package/scripts/sw-tracker-jira.sh +277 -0
  70. package/scripts/sw-tracker-linear.sh +292 -0
  71. package/scripts/sw-tracker.sh +409 -0
  72. package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
  73. package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
  74. package/templates/pipelines/autonomous.json +27 -5
  75. package/templates/pipelines/full.json +12 -0
  76. package/templates/pipelines/standard.json +12 -0
  77. package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
  78. package/tmux/templates/accessibility.json +34 -0
  79. package/tmux/templates/api-design.json +35 -0
  80. package/tmux/templates/architecture.json +1 -0
  81. package/tmux/templates/bug-fix.json +9 -0
  82. package/tmux/templates/code-review.json +1 -0
  83. package/tmux/templates/compliance.json +36 -0
  84. package/tmux/templates/data-pipeline.json +36 -0
  85. package/tmux/templates/debt-paydown.json +34 -0
  86. package/tmux/templates/devops.json +1 -0
  87. package/tmux/templates/documentation.json +1 -0
  88. package/tmux/templates/exploration.json +1 -0
  89. package/tmux/templates/feature-dev.json +1 -0
  90. package/tmux/templates/full-stack.json +8 -0
  91. package/tmux/templates/i18n.json +34 -0
  92. package/tmux/templates/incident-response.json +36 -0
  93. package/tmux/templates/migration.json +1 -0
  94. package/tmux/templates/observability.json +35 -0
  95. package/tmux/templates/onboarding.json +33 -0
  96. package/tmux/templates/performance.json +35 -0
  97. package/tmux/templates/refactor.json +1 -0
  98. package/tmux/templates/release.json +35 -0
  99. package/tmux/templates/security-audit.json +8 -0
  100. package/tmux/templates/spike.json +34 -0
  101. package/tmux/templates/testing.json +1 -0
  102. package/tmux/tmux.conf +98 -9
  103. package/scripts/cct-doctor.sh +0 -328
  104. package/scripts/cct-init.sh +0 -282
  105. package/scripts/cct-session.sh +0 -284
  106. package/scripts/cct-status.sh +0 -169
@@ -0,0 +1,947 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright self-optimize — Learning & Self-Tuning System ║
4
+ # ║ Outcome analysis · Template tuning · Model routing · Memory evolution ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ set -euo pipefail
7
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
+
9
+ VERSION="1.9.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
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
29
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
30
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
31
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
32
+
33
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
34
+ now_epoch() { date +%s; }
35
+
36
+ # ─── Structured Event Log ────────────────────────────────────────────────────
37
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
38
+
39
+ emit_event() {
40
+ local event_type="$1"
41
+ shift
42
+ local json_fields=""
43
+ for kv in "$@"; do
44
+ local key="${kv%%=*}"
45
+ local val="${kv#*=}"
46
+ if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
47
+ json_fields="${json_fields},\"${key}\":${val}"
48
+ else
49
+ val="${val//\"/\\\"}"
50
+ json_fields="${json_fields},\"${key}\":\"${val}\""
51
+ fi
52
+ done
53
+ mkdir -p "${HOME}/.shipwright"
54
+ echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
55
+ }
56
+
57
+ # ─── Storage Paths ───────────────────────────────────────────────────────────
58
+ OPTIMIZATION_DIR="${HOME}/.shipwright/optimization"
59
+ OUTCOMES_FILE="${OPTIMIZATION_DIR}/outcomes.jsonl"
60
+ TEMPLATE_WEIGHTS_FILE="${OPTIMIZATION_DIR}/template-weights.json"
61
+ MODEL_ROUTING_FILE="${OPTIMIZATION_DIR}/model-routing.json"
62
+ ITERATION_MODEL_FILE="${OPTIMIZATION_DIR}/iteration-model.json"
63
+
64
+ ensure_optimization_dir() {
65
+ mkdir -p "$OPTIMIZATION_DIR"
66
+ [[ -f "$TEMPLATE_WEIGHTS_FILE" ]] || echo '{}' > "$TEMPLATE_WEIGHTS_FILE"
67
+ [[ -f "$MODEL_ROUTING_FILE" ]] || echo '{}' > "$MODEL_ROUTING_FILE"
68
+ [[ -f "$ITERATION_MODEL_FILE" ]] || echo '{}' > "$ITERATION_MODEL_FILE"
69
+ }
70
+
71
+ # ─── GitHub Metrics ──────────────────────────────────────────────────────
72
+
73
+ _optimize_github_metrics() {
74
+ type _gh_detect_repo &>/dev/null 2>&1 || { echo "{}"; return 0; }
75
+ _gh_detect_repo 2>/dev/null || { echo "{}"; return 0; }
76
+
77
+ local owner="${GH_OWNER:-}" repo="${GH_REPO:-}"
78
+ [[ -z "$owner" || -z "$repo" ]] && { echo "{}"; return 0; }
79
+
80
+ if type gh_actions_runs &>/dev/null 2>&1; then
81
+ local runs
82
+ runs=$(gh_actions_runs "$owner" "$repo" "" 50 2>/dev/null || echo "[]")
83
+ local success_rate avg_duration
84
+ success_rate=$(echo "$runs" | jq '[.[] | select(.conclusion == "success")] | length as $s | ([length, 1] | max) as $t | ($s / $t * 100) | floor' 2>/dev/null || echo "0")
85
+ avg_duration=$(echo "$runs" | jq '[.[] | .duration_seconds // 0] | if length > 0 then add / length | floor else 0 end' 2>/dev/null || echo "0")
86
+ jq -n --argjson rate "${success_rate:-0}" --argjson dur "${avg_duration:-0}" \
87
+ '{ci_success_rate: $rate, ci_avg_duration_s: $dur}'
88
+ else
89
+ echo "{}"
90
+ fi
91
+ }
92
+
93
+ # ═════════════════════════════════════════════════════════════════════════════
94
+ # OUTCOME ANALYSIS
95
+ # ═════════════════════════════════════════════════════════════════════════════
96
+
97
+ # optimize_analyze_outcome <pipeline_state_file>
98
+ # Extract metrics from a completed pipeline and append to outcomes.jsonl
99
+ optimize_analyze_outcome() {
100
+ local state_file="${1:-}"
101
+
102
+ if [[ -z "$state_file" || ! -f "$state_file" ]]; then
103
+ error "Pipeline state file not found: ${state_file:-<empty>}"
104
+ return 1
105
+ fi
106
+
107
+ ensure_optimization_dir
108
+
109
+ # Extract fields from the state file (markdown-style key: value)
110
+ local issue_number template_used result total_iterations total_cost labels model
111
+ issue_number=$(sed -n 's/^issue: *#*//p' "$state_file" | head -1 | tr -d ' ')
112
+ template_used=$(sed -n 's/^template: *//p' "$state_file" | head -1 | tr -d ' ')
113
+ result=$(sed -n 's/^status: *//p' "$state_file" | head -1 | tr -d ' ')
114
+ total_iterations=$(sed -n 's/^iterations: *//p' "$state_file" | head -1 | tr -d ' ')
115
+ total_cost=$(sed -n 's/^cost: *\$*//p' "$state_file" | head -1 | tr -d ' ')
116
+ labels=$(sed -n 's/^labels: *//p' "$state_file" | head -1)
117
+ model=$(sed -n 's/^model: *//p' "$state_file" | head -1 | tr -d ' ')
118
+
119
+ # Extract complexity score if present
120
+ local complexity
121
+ complexity=$(sed -n 's/^complexity: *//p' "$state_file" | head -1 | tr -d ' ')
122
+
123
+ # Extract stage durations from stages section
124
+ local stages_json="[]"
125
+ local stages_section=""
126
+ stages_section=$(sed -n '/^stages:/,/^---/p' "$state_file" 2>/dev/null || true)
127
+ if [[ -n "$stages_section" ]]; then
128
+ # Build JSON array of stage results
129
+ local stage_entries=""
130
+ while IFS= read -r line; do
131
+ local stage_name stage_status
132
+ stage_name=$(echo "$line" | sed 's/:.*//' | tr -d ' ')
133
+ stage_status=$(echo "$line" | sed 's/.*: *//' | tr -d ' ')
134
+ if [[ -n "$stage_name" && "$stage_name" != "stages" && "$stage_name" != "---" ]]; then
135
+ if [[ -n "$stage_entries" ]]; then
136
+ stage_entries="${stage_entries},"
137
+ fi
138
+ stage_entries="${stage_entries}{\"name\":\"${stage_name}\",\"status\":\"${stage_status}\"}"
139
+ fi
140
+ done <<< "$stages_section"
141
+ if [[ -n "$stage_entries" ]]; then
142
+ stages_json="[${stage_entries}]"
143
+ fi
144
+ fi
145
+
146
+ # Build outcome record using jq for proper escaping
147
+ local tmp_outcome
148
+ tmp_outcome=$(mktemp)
149
+ jq -c -n \
150
+ --arg ts "$(now_iso)" \
151
+ --arg issue "${issue_number:-unknown}" \
152
+ --arg template "${template_used:-unknown}" \
153
+ --arg result "${result:-unknown}" \
154
+ --arg model "${model:-opus}" \
155
+ --arg labels "${labels:-}" \
156
+ --argjson iterations "${total_iterations:-0}" \
157
+ --argjson cost "${total_cost:-0}" \
158
+ --argjson complexity "${complexity:-0}" \
159
+ --argjson stages "$stages_json" \
160
+ '{
161
+ ts: $ts,
162
+ issue: $issue,
163
+ template: $template,
164
+ result: $result,
165
+ model: $model,
166
+ labels: $labels,
167
+ iterations: $iterations,
168
+ cost: $cost,
169
+ complexity: $complexity,
170
+ stages: $stages
171
+ }' > "$tmp_outcome"
172
+
173
+ # Append to outcomes file (atomic: write to tmp, then cat + mv)
174
+ local outcome_line
175
+ outcome_line=$(cat "$tmp_outcome")
176
+ rm -f "$tmp_outcome"
177
+ echo "$outcome_line" >> "$OUTCOMES_FILE"
178
+
179
+ # Record GitHub CI metrics alongside outcome
180
+ local gh_ci_metrics
181
+ gh_ci_metrics=$(_optimize_github_metrics 2>/dev/null || echo "{}")
182
+ local ci_success_rate ci_avg_dur
183
+ ci_success_rate=$(echo "$gh_ci_metrics" | jq -r '.ci_success_rate // 0' 2>/dev/null || echo "0")
184
+ ci_avg_dur=$(echo "$gh_ci_metrics" | jq -r '.ci_avg_duration_s // 0' 2>/dev/null || echo "0")
185
+ if [[ "${ci_success_rate:-0}" -gt 0 || "${ci_avg_dur:-0}" -gt 0 ]]; then
186
+ # Append CI metrics to the outcome line
187
+ local ci_record
188
+ ci_record=$(jq -c -n \
189
+ --arg ts "$(now_iso)" \
190
+ --arg issue "${issue_number:-unknown}" \
191
+ --argjson ci_rate "${ci_success_rate:-0}" \
192
+ --argjson ci_dur "${ci_avg_dur:-0}" \
193
+ '{ts: $ts, type: "ci_metrics", issue: $issue, ci_success_rate: $ci_rate, ci_avg_duration_s: $ci_dur}')
194
+ echo "$ci_record" >> "$OUTCOMES_FILE"
195
+
196
+ # Warn if CI success rate is dropping
197
+ if [[ "${ci_success_rate:-0}" -lt 70 && "${ci_success_rate:-0}" -gt 0 ]]; then
198
+ warn "CI success rate is ${ci_success_rate}% — consider template escalation"
199
+ fi
200
+ fi
201
+
202
+ emit_event "optimize.outcome_analyzed" \
203
+ "issue=${issue_number:-unknown}" \
204
+ "template=${template_used:-unknown}" \
205
+ "result=${result:-unknown}" \
206
+ "iterations=${total_iterations:-0}" \
207
+ "cost=${total_cost:-0}"
208
+
209
+ success "Outcome recorded for issue #${issue_number:-unknown} (${result:-unknown})"
210
+ }
211
+
212
+ # ═════════════════════════════════════════════════════════════════════════════
213
+ # TEMPLATE TUNING
214
+ # ═════════════════════════════════════════════════════════════════════════════
215
+
216
+ # optimize_tune_templates [outcomes_file]
217
+ # Adjust template selection weights based on success/failure rates per label
218
+ optimize_tune_templates() {
219
+ local outcomes_file="${1:-$OUTCOMES_FILE}"
220
+
221
+ if [[ ! -f "$outcomes_file" ]]; then
222
+ warn "No outcomes data found at: $outcomes_file"
223
+ return 0
224
+ fi
225
+
226
+ ensure_optimization_dir
227
+
228
+ info "Tuning template weights..."
229
+
230
+ # Process outcomes: group by template+label, calculate success rates
231
+ # Uses a temp file approach compatible with Bash 3.2 (no associative arrays)
232
+ local tmp_stats tmp_weights
233
+ tmp_stats=$(mktemp)
234
+ tmp_weights=$(mktemp)
235
+
236
+ # Extract template, labels, result from each outcome line
237
+ while IFS= read -r line; do
238
+ local template result labels_str
239
+ template=$(echo "$line" | jq -r '.template // "unknown"' 2>/dev/null) || continue
240
+ result=$(echo "$line" | jq -r '.result // "unknown"' 2>/dev/null) || continue
241
+ labels_str=$(echo "$line" | jq -r '.labels // ""' 2>/dev/null) || continue
242
+
243
+ # Default label if none
244
+ if [[ -z "$labels_str" ]]; then
245
+ labels_str="unlabeled"
246
+ fi
247
+
248
+ # Record template+label combination with result
249
+ local label
250
+ # Split labels by comma
251
+ echo "$labels_str" | tr ',' '\n' | while IFS= read -r label; do
252
+ label=$(echo "$label" | tr -d ' ')
253
+ [[ -z "$label" ]] && continue
254
+ local is_success=0
255
+ if [[ "$result" == "success" || "$result" == "completed" ]]; then
256
+ is_success=1
257
+ fi
258
+ echo "${template}|${label}|${is_success}" >> "$tmp_stats"
259
+ done
260
+ done < "$outcomes_file"
261
+
262
+ # Calculate weights per template+label
263
+ local current_weights='{}'
264
+ if [[ -f "$TEMPLATE_WEIGHTS_FILE" ]]; then
265
+ current_weights=$(cat "$TEMPLATE_WEIGHTS_FILE")
266
+ fi
267
+
268
+ # Get unique template|label combos
269
+ if [[ -f "$tmp_stats" ]]; then
270
+ local combos
271
+ combos=$(cut -d'|' -f1,2 "$tmp_stats" | sort -u || true)
272
+
273
+ local new_weights="$current_weights"
274
+ while IFS= read -r combo; do
275
+ [[ -z "$combo" ]] && continue
276
+ local tmpl lbl
277
+ tmpl=$(echo "$combo" | cut -d'|' -f1)
278
+ lbl=$(echo "$combo" | cut -d'|' -f2)
279
+
280
+ local total successes rate
281
+ total=$(grep -c "^${tmpl}|${lbl}|" "$tmp_stats" || true)
282
+ total="${total:-0}"
283
+ successes=$(grep -c "^${tmpl}|${lbl}|1$" "$tmp_stats" || true)
284
+ successes="${successes:-0}"
285
+
286
+ if [[ "$total" -gt 0 ]]; then
287
+ rate=$(awk "BEGIN{printf \"%.2f\", ($successes/$total)*100}")
288
+ else
289
+ rate="0"
290
+ fi
291
+
292
+ # Get current weight (default 1.0)
293
+ local current_weight
294
+ current_weight=$(echo "$new_weights" | jq -r --arg t "$tmpl" --arg l "$lbl" '.[$t + "|" + $l] // 1.0' 2>/dev/null)
295
+ current_weight="${current_weight:-1.0}"
296
+
297
+ # Adjust weight: proportional update if enough samples, else skip
298
+ local new_weight="$current_weight"
299
+ if [[ "$total" -ge 5 ]]; then
300
+ # Calculate average success rate across all combos for dynamic thresholds
301
+ local all_total all_successes avg_rate
302
+ all_total=$(wc -l < "$tmp_stats" | tr -d ' ')
303
+ all_total="${all_total:-1}"
304
+ all_successes=$(grep -c "|1$" "$tmp_stats" || true)
305
+ all_successes="${all_successes:-0}"
306
+ avg_rate=$(awk -v s="$all_successes" -v t="$all_total" 'BEGIN { if (t > 0) printf "%.2f", (s/t)*100; else print "50" }')
307
+
308
+ # Proportional update: new_weight = old_weight * (rate / avg_rate), clamp [0.1, 2.0]
309
+ if awk -v ar="$avg_rate" 'BEGIN { exit !(ar > 0) }' 2>/dev/null; then
310
+ new_weight=$(awk -v cw="$current_weight" -v r="$rate" -v ar="$avg_rate" \
311
+ 'BEGIN { w = cw * (r / ar); if (w < 0.1) w = 0.1; if (w > 2.0) w = 2.0; printf "%.3f", w }')
312
+ fi
313
+ fi
314
+
315
+ # Update weights JSON
316
+ new_weights=$(echo "$new_weights" | jq --arg key "${tmpl}|${lbl}" --argjson w "$new_weight" '.[$key] = $w')
317
+ done <<< "$combos"
318
+
319
+ # Atomic write
320
+ echo "$new_weights" > "$tmp_weights" && mv "$tmp_weights" "$TEMPLATE_WEIGHTS_FILE"
321
+ fi
322
+
323
+ rm -f "$tmp_stats" "$tmp_weights" 2>/dev/null || true
324
+
325
+ emit_event "optimize.template_tuned"
326
+ success "Template weights updated"
327
+ }
328
+
329
+ # ═════════════════════════════════════════════════════════════════════════════
330
+ # ITERATION LEARNING
331
+ # ═════════════════════════════════════════════════════════════════════════════
332
+
333
+ # optimize_learn_iterations [outcomes_file]
334
+ # Build a prediction model for iterations by complexity bucket
335
+ optimize_learn_iterations() {
336
+ local outcomes_file="${1:-$OUTCOMES_FILE}"
337
+
338
+ if [[ ! -f "$outcomes_file" ]]; then
339
+ warn "No outcomes data found at: $outcomes_file"
340
+ return 0
341
+ fi
342
+
343
+ ensure_optimization_dir
344
+
345
+ info "Learning iteration patterns..."
346
+
347
+ # Read complexity bucket boundaries from config or use defaults (3, 6)
348
+ local clusters_file="${OPTIMIZATION_DIR}/complexity-clusters.json"
349
+ local low_max=3
350
+ local med_max=6
351
+
352
+ if [[ -f "$clusters_file" ]]; then
353
+ local cfg_low cfg_med
354
+ cfg_low=$(jq -r '.low_max // empty' "$clusters_file" 2>/dev/null || true)
355
+ cfg_med=$(jq -r '.med_max // empty' "$clusters_file" 2>/dev/null || true)
356
+ [[ -n "$cfg_low" && "$cfg_low" != "null" ]] && low_max="$cfg_low"
357
+ [[ -n "$cfg_med" && "$cfg_med" != "null" ]] && med_max="$cfg_med"
358
+ fi
359
+
360
+ # Group by complexity bucket
361
+ local tmp_low tmp_med tmp_high tmp_all_pairs
362
+ tmp_low=$(mktemp)
363
+ tmp_med=$(mktemp)
364
+ tmp_high=$(mktemp)
365
+ tmp_all_pairs=$(mktemp)
366
+
367
+ while IFS= read -r line; do
368
+ local complexity iterations
369
+ complexity=$(echo "$line" | jq -r '.complexity // 0' 2>/dev/null) || continue
370
+ iterations=$(echo "$line" | jq -r '.iterations // 0' 2>/dev/null) || continue
371
+
372
+ # Skip entries without iteration data
373
+ [[ "$iterations" == "0" || "$iterations" == "null" ]] && continue
374
+
375
+ # Store (complexity, iterations) pairs for potential k-means
376
+ echo "${complexity} ${iterations}" >> "$tmp_all_pairs"
377
+
378
+ if [[ "$complexity" -le "$low_max" ]]; then
379
+ echo "$iterations" >> "$tmp_low"
380
+ elif [[ "$complexity" -le "$med_max" ]]; then
381
+ echo "$iterations" >> "$tmp_med"
382
+ else
383
+ echo "$iterations" >> "$tmp_high"
384
+ fi
385
+ done < "$outcomes_file"
386
+
387
+ # If 50+ data points, compute k-means (3 clusters) to find natural boundaries
388
+ local pair_count=0
389
+ [[ -s "$tmp_all_pairs" ]] && pair_count=$(wc -l < "$tmp_all_pairs" | tr -d ' ')
390
+ if [[ "$pair_count" -ge 50 ]]; then
391
+ # Simple k-means in awk: cluster by complexity value into 3 groups
392
+ local new_boundaries
393
+ new_boundaries=$(awk '
394
+ BEGIN { n=0 }
395
+ { c[n]=$1; it[n]=$2; n++ }
396
+ END {
397
+ if (n < 50) exit
398
+ # Sort by complexity (simple bubble sort — small n)
399
+ for (i=0; i<n-1; i++)
400
+ for (j=i+1; j<n; j++)
401
+ if (c[i] > c[j]) {
402
+ tmp=c[i]; c[i]=c[j]; c[j]=tmp
403
+ tmp=it[i]; it[i]=it[j]; it[j]=tmp
404
+ }
405
+ # Split into 3 equal groups and find boundaries
406
+ third = int(n / 3)
407
+ low_boundary = c[third - 1]
408
+ med_boundary = c[2 * third - 1]
409
+ # Ensure boundaries are sane (1-9 range)
410
+ if (low_boundary < 1) low_boundary = 1
411
+ if (low_boundary > 5) low_boundary = 5
412
+ if (med_boundary < low_boundary + 1) med_boundary = low_boundary + 1
413
+ if (med_boundary > 8) med_boundary = 8
414
+ printf "%d %d", low_boundary, med_boundary
415
+ }' "$tmp_all_pairs")
416
+
417
+ if [[ -n "$new_boundaries" ]]; then
418
+ local new_low new_med
419
+ new_low=$(echo "$new_boundaries" | cut -d' ' -f1)
420
+ new_med=$(echo "$new_boundaries" | cut -d' ' -f2)
421
+
422
+ if [[ -n "$new_low" && -n "$new_med" ]]; then
423
+ # Write boundaries back to config (atomic)
424
+ local tmp_clusters
425
+ tmp_clusters=$(mktemp "${TMPDIR:-/tmp}/sw-clusters.XXXXXX")
426
+ jq -n \
427
+ --argjson low_max "$new_low" \
428
+ --argjson med_max "$new_med" \
429
+ --argjson samples "$pair_count" \
430
+ --arg updated "$(now_iso)" \
431
+ '{low_max: $low_max, med_max: $med_max, samples: $samples, updated: $updated}' \
432
+ > "$tmp_clusters" && mv "$tmp_clusters" "$clusters_file" || rm -f "$tmp_clusters"
433
+
434
+ emit_event "optimize.clusters_updated" \
435
+ "low_max=$new_low" \
436
+ "med_max=$new_med" \
437
+ "samples=$pair_count"
438
+ fi
439
+ fi
440
+ fi
441
+ rm -f "$tmp_all_pairs" 2>/dev/null || true
442
+
443
+ # Calculate mean and stddev for each bucket using awk
444
+ calc_stats() {
445
+ local file="$1"
446
+ if [[ ! -s "$file" ]]; then
447
+ echo '{"mean":0,"stddev":0,"samples":0}'
448
+ return
449
+ fi
450
+ awk '{
451
+ sum += $1; sumsq += ($1 * $1); n++
452
+ } END {
453
+ if (n == 0) { print "{\"mean\":0,\"stddev\":0,\"samples\":0}"; exit }
454
+ mean = sum / n
455
+ if (n > 1) {
456
+ variance = (sumsq / n) - (mean * mean)
457
+ if (variance < 0) variance = 0
458
+ stddev = sqrt(variance)
459
+ } else {
460
+ stddev = 0
461
+ }
462
+ printf "{\"mean\":%.1f,\"stddev\":%.1f,\"samples\":%d}\n", mean, stddev, n
463
+ }' "$file"
464
+ }
465
+
466
+ local low_stats med_stats high_stats
467
+ low_stats=$(calc_stats "$tmp_low")
468
+ med_stats=$(calc_stats "$tmp_med")
469
+ high_stats=$(calc_stats "$tmp_high")
470
+
471
+ # Build iteration model
472
+ local tmp_model
473
+ tmp_model=$(mktemp)
474
+ jq -n \
475
+ --argjson low "$low_stats" \
476
+ --argjson medium "$med_stats" \
477
+ --argjson high "$high_stats" \
478
+ --arg updated "$(now_iso)" \
479
+ '{low: $low, medium: $medium, high: $high, updated_at: $updated}' \
480
+ > "$tmp_model" && mv "$tmp_model" "$ITERATION_MODEL_FILE"
481
+
482
+ rm -f "$tmp_low" "$tmp_med" "$tmp_high" 2>/dev/null || true
483
+
484
+ success "Iteration model updated"
485
+ }
486
+
487
+ # ═════════════════════════════════════════════════════════════════════════════
488
+ # MODEL ROUTING
489
+ # ═════════════════════════════════════════════════════════════════════════════
490
+
491
+ # optimize_should_ab_test <stage>
492
+ # Returns 0 (true) ~20% of the time for A/B testing
493
+ optimize_should_ab_test() {
494
+ local threshold=20
495
+ local roll=$((RANDOM % 100))
496
+ [[ "$roll" -lt "$threshold" ]]
497
+ }
498
+
499
+ # optimize_route_models [outcomes_file]
500
+ # Track per-stage model success rates and recommend cheaper models when viable
501
+ optimize_route_models() {
502
+ local outcomes_file="${1:-$OUTCOMES_FILE}"
503
+
504
+ if [[ ! -f "$outcomes_file" ]]; then
505
+ warn "No outcomes data found at: $outcomes_file"
506
+ return 0
507
+ fi
508
+
509
+ ensure_optimization_dir
510
+
511
+ info "Analyzing model routing..."
512
+
513
+ # Collect per-stage, per-model stats
514
+ local tmp_stage_stats
515
+ tmp_stage_stats=$(mktemp)
516
+
517
+ while IFS= read -r line; do
518
+ local model result stages_arr
519
+ model=$(echo "$line" | jq -r '.model // "opus"' 2>/dev/null) || continue
520
+ result=$(echo "$line" | jq -r '.result // "unknown"' 2>/dev/null) || continue
521
+ local cost
522
+ cost=$(echo "$line" | jq -r '.cost // 0' 2>/dev/null) || continue
523
+
524
+ # Extract stage names from the stages array
525
+ local stage_count
526
+ stage_count=$(echo "$line" | jq '.stages | length' 2>/dev/null || echo "0")
527
+
528
+ local i=0
529
+ while [[ "$i" -lt "$stage_count" ]]; do
530
+ local stage_name stage_status
531
+ stage_name=$(echo "$line" | jq -r ".stages[$i].name" 2>/dev/null)
532
+ stage_status=$(echo "$line" | jq -r ".stages[$i].status" 2>/dev/null)
533
+ local is_success=0
534
+ if [[ "$stage_status" == "complete" || "$stage_status" == "success" ]]; then
535
+ is_success=1
536
+ fi
537
+ echo "${stage_name}|${model}|${is_success}|${cost}" >> "$tmp_stage_stats"
538
+ i=$((i + 1))
539
+ done
540
+ done < "$outcomes_file"
541
+
542
+ # Build routing recommendations
543
+ local routing='{}'
544
+ if [[ -f "$MODEL_ROUTING_FILE" ]]; then
545
+ routing=$(cat "$MODEL_ROUTING_FILE")
546
+ fi
547
+
548
+ if [[ -f "$tmp_stage_stats" && -s "$tmp_stage_stats" ]]; then
549
+ local stages
550
+ stages=$(cut -d'|' -f1 "$tmp_stage_stats" | sort -u || true)
551
+
552
+ while IFS= read -r stage; do
553
+ [[ -z "$stage" ]] && continue
554
+
555
+ # Sonnet stats for this stage
556
+ local sonnet_total sonnet_success sonnet_rate
557
+ sonnet_total=$(grep -c "^${stage}|sonnet|" "$tmp_stage_stats" || true)
558
+ sonnet_total="${sonnet_total:-0}"
559
+ sonnet_success=$(grep -c "^${stage}|sonnet|1|" "$tmp_stage_stats" || true)
560
+ sonnet_success="${sonnet_success:-0}"
561
+
562
+ if [[ "$sonnet_total" -gt 0 ]]; then
563
+ sonnet_rate=$(awk "BEGIN{printf \"%.1f\", ($sonnet_success/$sonnet_total)*100}")
564
+ else
565
+ sonnet_rate="0"
566
+ fi
567
+
568
+ # Opus stats for this stage
569
+ local opus_total opus_success opus_rate
570
+ opus_total=$(grep -c "^${stage}|opus|" "$tmp_stage_stats" || true)
571
+ opus_total="${opus_total:-0}"
572
+ opus_success=$(grep -c "^${stage}|opus|1|" "$tmp_stage_stats" || true)
573
+ opus_success="${opus_success:-0}"
574
+
575
+ if [[ "$opus_total" -gt 0 ]]; then
576
+ opus_rate=$(awk "BEGIN{printf \"%.1f\", ($opus_success/$opus_total)*100}")
577
+ else
578
+ opus_rate="0"
579
+ fi
580
+
581
+ # Recommend sonnet if it succeeds 90%+ with enough samples
582
+ local recommendation="opus"
583
+ if [[ "$sonnet_total" -ge 3 ]] && awk "BEGIN{exit !($sonnet_rate >= 90)}" 2>/dev/null; then
584
+ recommendation="sonnet"
585
+ emit_event "optimize.model_switched" \
586
+ "stage=$stage" \
587
+ "from=opus" \
588
+ "to=sonnet" \
589
+ "sonnet_rate=$sonnet_rate"
590
+ fi
591
+
592
+ routing=$(echo "$routing" | jq \
593
+ --arg stage "$stage" \
594
+ --arg rec "$recommendation" \
595
+ --argjson sonnet_rate "$sonnet_rate" \
596
+ --argjson opus_rate "$opus_rate" \
597
+ --argjson sonnet_n "$sonnet_total" \
598
+ --argjson opus_n "$opus_total" \
599
+ '.[$stage] = {
600
+ recommended: $rec,
601
+ sonnet_rate: $sonnet_rate,
602
+ opus_rate: $opus_rate,
603
+ sonnet_samples: $sonnet_n,
604
+ opus_samples: $opus_n
605
+ }')
606
+ done <<< "$stages"
607
+ fi
608
+
609
+ # Atomic write
610
+ local tmp_routing
611
+ tmp_routing=$(mktemp)
612
+ echo "$routing" > "$tmp_routing" && mv "$tmp_routing" "$MODEL_ROUTING_FILE"
613
+
614
+ rm -f "$tmp_stage_stats" 2>/dev/null || true
615
+
616
+ success "Model routing updated"
617
+ }
618
+
619
+ # ═════════════════════════════════════════════════════════════════════════════
620
+ # MEMORY EVOLUTION
621
+ # ═════════════════════════════════════════════════════════════════════════════
622
+
623
+ # optimize_evolve_memory
624
+ # Prune stale patterns, strengthen confirmed ones, promote cross-repo patterns
625
+ optimize_evolve_memory() {
626
+ local memory_root="${HOME}/.shipwright/memory"
627
+
628
+ if [[ ! -d "$memory_root" ]]; then
629
+ warn "No memory directory found"
630
+ return 0
631
+ fi
632
+
633
+ info "Evolving memory patterns..."
634
+
635
+ local pruned=0
636
+ local strengthened=0
637
+ local promoted=0
638
+ local now_e
639
+ now_e=$(now_epoch)
640
+
641
+ # Read adaptive timescales from config or use defaults
642
+ local timescales_file="${OPTIMIZATION_DIR}/memory-timescales.json"
643
+ local prune_days=30
644
+ local boost_days=7
645
+ local strength_threshold=3
646
+ local promotion_threshold=3
647
+
648
+ if [[ -f "$timescales_file" ]]; then
649
+ local cfg_prune cfg_boost
650
+ cfg_prune=$(jq -r '.prune_days // empty' "$timescales_file" 2>/dev/null || true)
651
+ cfg_boost=$(jq -r '.boost_days // empty' "$timescales_file" 2>/dev/null || true)
652
+ [[ -n "$cfg_prune" && "$cfg_prune" != "null" ]] && prune_days="$cfg_prune"
653
+ [[ -n "$cfg_boost" && "$cfg_boost" != "null" ]] && boost_days="$cfg_boost"
654
+ fi
655
+
656
+ # Read strength and cross-repo thresholds from config
657
+ local thresholds_file="${OPTIMIZATION_DIR}/memory-thresholds.json"
658
+ if [[ -f "$thresholds_file" ]]; then
659
+ local cfg_strength cfg_promotion
660
+ cfg_strength=$(jq -r '.strength_threshold // empty' "$thresholds_file" 2>/dev/null || true)
661
+ cfg_promotion=$(jq -r '.promotion_threshold // empty' "$thresholds_file" 2>/dev/null || true)
662
+ [[ -n "$cfg_strength" && "$cfg_strength" != "null" ]] && strength_threshold="$cfg_strength"
663
+ [[ -n "$cfg_promotion" && "$cfg_promotion" != "null" ]] && promotion_threshold="$cfg_promotion"
664
+ fi
665
+
666
+ local prune_seconds=$((prune_days * 86400))
667
+ local boost_seconds=$((boost_days * 86400))
668
+ local prune_cutoff=$((now_e - prune_seconds))
669
+ local boost_cutoff=$((now_e - boost_seconds))
670
+
671
+ # Process each repo's failures.json
672
+ local repo_dir
673
+ for repo_dir in "$memory_root"/*/; do
674
+ [[ -d "$repo_dir" ]] || continue
675
+ local failures_file="${repo_dir}failures.json"
676
+ [[ -f "$failures_file" ]] || continue
677
+
678
+ local entry_count
679
+ entry_count=$(jq '.failures | length' "$failures_file" 2>/dev/null || echo "0")
680
+ [[ "$entry_count" -eq 0 ]] && continue
681
+
682
+ local tmp_file
683
+ tmp_file=$(mktemp)
684
+
685
+ # Prune entries not seen within prune window
686
+ local pruned_json
687
+ pruned_json=$(jq --arg cutoff "$(date -u -r "$prune_cutoff" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")" \
688
+ '[.failures[] | select(.last_seen >= $cutoff or .last_seen == null)]' \
689
+ "$failures_file" 2>/dev/null || echo "[]")
690
+
691
+ local after_count
692
+ after_count=$(echo "$pruned_json" | jq 'length' 2>/dev/null || echo "0")
693
+ local delta=$((entry_count - after_count))
694
+ pruned=$((pruned + delta))
695
+
696
+ # Strengthen entries seen N+ times within boost window (adaptive thresholds)
697
+ pruned_json=$(echo "$pruned_json" | jq \
698
+ --arg cutoff_b "$(date -u -r "$boost_cutoff" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")" \
699
+ --argjson st "$strength_threshold" '
700
+ [.[] | if (.seen_count >= $st and .last_seen >= $cutoff_b) then
701
+ .weight = ((.weight // 1.0) * 1.5)
702
+ else . end]')
703
+
704
+ local strong_count
705
+ strong_count=$(echo "$pruned_json" | jq '[.[] | select(.weight != null and .weight > 1.0)] | length' 2>/dev/null || echo "0")
706
+ strengthened=$((strengthened + strong_count))
707
+
708
+ # Write back
709
+ jq -n --argjson f "$pruned_json" '{failures: $f}' > "$tmp_file" && mv "$tmp_file" "$failures_file"
710
+ done
711
+
712
+ # Promote patterns that appear in 3+ repos to global.json
713
+ local global_file="${memory_root}/global.json"
714
+ if [[ ! -f "$global_file" ]]; then
715
+ echo '{"common_patterns":[],"cross_repo_learnings":[]}' > "$global_file"
716
+ fi
717
+
718
+ # Collect all patterns across repos
719
+ local tmp_all_patterns
720
+ tmp_all_patterns=$(mktemp)
721
+ for repo_dir in "$memory_root"/*/; do
722
+ [[ -d "$repo_dir" ]] || continue
723
+ local failures_file="${repo_dir}failures.json"
724
+ [[ -f "$failures_file" ]] || continue
725
+ jq -r '.failures[]?.pattern // empty' "$failures_file" 2>/dev/null >> "$tmp_all_patterns" || true
726
+ done
727
+
728
+ if [[ -s "$tmp_all_patterns" ]]; then
729
+ # Find patterns appearing in N+ repos (adaptive threshold)
730
+ local promoted_patterns
731
+ promoted_patterns=$(sort "$tmp_all_patterns" | uniq -c | sort -rn | awk -v pt="$promotion_threshold" '$1 >= pt {$1=""; print substr($0,2)}' || true)
732
+
733
+ if [[ -n "$promoted_patterns" ]]; then
734
+ local tmp_global
735
+ tmp_global=$(mktemp)
736
+ local pcount=0
737
+ while IFS= read -r pattern; do
738
+ [[ -z "$pattern" ]] && continue
739
+ # Check if already in global
740
+ local exists
741
+ exists=$(jq --arg p "$pattern" '[.common_patterns[] | select(.pattern == $p)] | length' "$global_file" 2>/dev/null || echo "0")
742
+ if [[ "$exists" == "0" ]]; then
743
+ jq --arg p "$pattern" --arg ts "$(now_iso)" \
744
+ '.common_patterns += [{pattern: $p, promoted_at: $ts, source: "cross-repo"}]' \
745
+ "$global_file" > "$tmp_global" && mv "$tmp_global" "$global_file"
746
+ pcount=$((pcount + 1))
747
+ fi
748
+ done <<< "$promoted_patterns"
749
+ promoted=$((promoted + pcount))
750
+ fi
751
+ fi
752
+
753
+ rm -f "$tmp_all_patterns" 2>/dev/null || true
754
+
755
+ emit_event "optimize.memory_pruned" \
756
+ "pruned=$pruned" \
757
+ "strengthened=$strengthened" \
758
+ "promoted=$promoted"
759
+
760
+ success "Memory evolved: pruned=$pruned, strengthened=$strengthened, promoted=$promoted"
761
+ }
762
+
763
+ # ═════════════════════════════════════════════════════════════════════════════
764
+ # FULL ANALYSIS (DAILY)
765
+ # ═════════════════════════════════════════════════════════════════════════════
766
+
767
+ # optimize_full_analysis
768
+ # Run all optimization steps — designed for daily execution
769
+ optimize_full_analysis() {
770
+ echo ""
771
+ echo -e "${PURPLE}${BOLD}╔═══════════════════════════════════════════════════════════════╗${RESET}"
772
+ echo -e "${PURPLE}${BOLD}║ Self-Optimization — Full Analysis ║${RESET}"
773
+ echo -e "${PURPLE}${BOLD}╚═══════════════════════════════════════════════════════════════╝${RESET}"
774
+ echo ""
775
+
776
+ ensure_optimization_dir
777
+
778
+ optimize_tune_templates
779
+ optimize_learn_iterations
780
+ optimize_route_models
781
+ optimize_evolve_memory
782
+
783
+ echo ""
784
+ success "Full optimization analysis complete"
785
+ }
786
+
787
+ # ═════════════════════════════════════════════════════════════════════════════
788
+ # REPORT
789
+ # ═════════════════════════════════════════════════════════════════════════════
790
+
791
+ # optimize_report
792
+ # Generate a summary report of optimization trends over last 7 days
793
+ optimize_report() {
794
+ ensure_optimization_dir
795
+
796
+ echo ""
797
+ echo -e "${PURPLE}${BOLD}╔═══════════════════════════════════════════════════════════════╗${RESET}"
798
+ echo -e "${PURPLE}${BOLD}║ Self-Optimization Report ║${RESET}"
799
+ echo -e "${PURPLE}${BOLD}╚═══════════════════════════════════════════════════════════════╝${RESET}"
800
+ echo ""
801
+
802
+ if [[ ! -f "$OUTCOMES_FILE" ]]; then
803
+ warn "No outcomes data available yet"
804
+ return 0
805
+ fi
806
+
807
+ local now_e seven_days_ago
808
+ now_e=$(now_epoch)
809
+ seven_days_ago=$((now_e - 604800))
810
+ local cutoff_iso
811
+ cutoff_iso=$(date -u -r "$seven_days_ago" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
812
+
813
+ # Count outcomes in last 7 days
814
+ local total_recent=0
815
+ local success_recent=0
816
+ local total_cost_recent=0
817
+ local total_iterations_recent=0
818
+
819
+ while IFS= read -r line; do
820
+ local ts result cost iterations
821
+ ts=$(echo "$line" | jq -r '.ts // ""' 2>/dev/null) || continue
822
+ [[ "$ts" < "$cutoff_iso" ]] && continue
823
+
824
+ result=$(echo "$line" | jq -r '.result // "unknown"' 2>/dev/null) || continue
825
+ cost=$(echo "$line" | jq -r '.cost // 0' 2>/dev/null) || continue
826
+ iterations=$(echo "$line" | jq -r '.iterations // 0' 2>/dev/null) || continue
827
+
828
+ total_recent=$((total_recent + 1))
829
+ if [[ "$result" == "success" || "$result" == "completed" ]]; then
830
+ success_recent=$((success_recent + 1))
831
+ fi
832
+ total_cost_recent=$(awk "BEGIN{printf \"%.2f\", $total_cost_recent + $cost}")
833
+ total_iterations_recent=$((total_iterations_recent + iterations))
834
+ done < "$OUTCOMES_FILE"
835
+
836
+ # Calculate rates
837
+ local success_rate="0"
838
+ local avg_iterations="0"
839
+ local avg_cost="0"
840
+ if [[ "$total_recent" -gt 0 ]]; then
841
+ success_rate=$(awk "BEGIN{printf \"%.1f\", ($success_recent/$total_recent)*100}")
842
+ avg_iterations=$(awk "BEGIN{printf \"%.1f\", $total_iterations_recent/$total_recent}")
843
+ avg_cost=$(awk "BEGIN{printf \"%.2f\", $total_cost_recent/$total_recent}")
844
+ fi
845
+
846
+ echo -e "${CYAN}${BOLD} Last 7 Days${RESET}"
847
+ echo -e " ${DIM}─────────────────────────────────${RESET}"
848
+ echo -e " Pipelines: ${BOLD}$total_recent${RESET}"
849
+ echo -e " Success rate: ${BOLD}${success_rate}%${RESET}"
850
+ echo -e " Avg iterations: ${BOLD}${avg_iterations}${RESET}"
851
+ echo -e " Avg cost: ${BOLD}\$${avg_cost}${RESET}"
852
+ echo -e " Total cost: ${BOLD}\$${total_cost_recent}${RESET}"
853
+ echo ""
854
+
855
+ # Template weights summary
856
+ if [[ -f "$TEMPLATE_WEIGHTS_FILE" ]]; then
857
+ local weight_count
858
+ weight_count=$(jq 'keys | length' "$TEMPLATE_WEIGHTS_FILE" 2>/dev/null || echo "0")
859
+ if [[ "$weight_count" -gt 0 ]]; then
860
+ echo -e "${CYAN}${BOLD} Template Weights${RESET}"
861
+ echo -e " ${DIM}─────────────────────────────────${RESET}"
862
+ jq -r 'to_entries[] | " \(.key): \(.value)"' "$TEMPLATE_WEIGHTS_FILE" 2>/dev/null || true
863
+ echo ""
864
+ fi
865
+ fi
866
+
867
+ # Model routing summary
868
+ if [[ -f "$MODEL_ROUTING_FILE" ]]; then
869
+ local route_count
870
+ route_count=$(jq 'keys | length' "$MODEL_ROUTING_FILE" 2>/dev/null || echo "0")
871
+ if [[ "$route_count" -gt 0 ]]; then
872
+ echo -e "${CYAN}${BOLD} Model Routing${RESET}"
873
+ echo -e " ${DIM}─────────────────────────────────${RESET}"
874
+ jq -r 'to_entries[] | " \(.key): \(.value.recommended) (sonnet: \(.value.sonnet_rate)%, opus: \(.value.opus_rate)%)"' \
875
+ "$MODEL_ROUTING_FILE" 2>/dev/null || true
876
+ echo ""
877
+ fi
878
+ fi
879
+
880
+ # Iteration model summary
881
+ if [[ -f "$ITERATION_MODEL_FILE" ]]; then
882
+ local has_data
883
+ has_data=$(jq '.low.samples // 0' "$ITERATION_MODEL_FILE" 2>/dev/null || echo "0")
884
+ if [[ "$has_data" -gt 0 ]]; then
885
+ echo -e "${CYAN}${BOLD} Iteration Model${RESET}"
886
+ echo -e " ${DIM}─────────────────────────────────${RESET}"
887
+ echo -e " Low complexity: $(jq -r '.low | "\(.mean) ± \(.stddev) (\(.samples) samples)"' "$ITERATION_MODEL_FILE" 2>/dev/null)"
888
+ echo -e " Med complexity: $(jq -r '.medium | "\(.mean) ± \(.stddev) (\(.samples) samples)"' "$ITERATION_MODEL_FILE" 2>/dev/null)"
889
+ echo -e " High complexity: $(jq -r '.high | "\(.mean) ± \(.stddev) (\(.samples) samples)"' "$ITERATION_MODEL_FILE" 2>/dev/null)"
890
+ echo ""
891
+ fi
892
+ fi
893
+
894
+ emit_event "optimize.report" \
895
+ "pipelines=$total_recent" \
896
+ "success_rate=$success_rate" \
897
+ "avg_cost=$avg_cost"
898
+
899
+ success "Report complete"
900
+ }
901
+
902
+ # ═════════════════════════════════════════════════════════════════════════════
903
+ # HELP
904
+ # ═════════════════════════════════════════════════════════════════════════════
905
+
906
+ show_help() {
907
+ echo ""
908
+ echo -e "${PURPLE}${BOLD}shipwright self-optimize${RESET} — Learning & Self-Tuning System"
909
+ echo ""
910
+ echo -e "${CYAN}USAGE${RESET}"
911
+ echo " shipwright self-optimize <command>"
912
+ echo ""
913
+ echo -e "${CYAN}COMMANDS${RESET}"
914
+ echo " analyze-outcome <state-file> Analyze a completed pipeline outcome"
915
+ echo " tune Run full optimization analysis"
916
+ echo " report Show optimization report (last 7 days)"
917
+ echo " evolve-memory Prune/strengthen/promote memory patterns"
918
+ echo " help Show this help"
919
+ echo ""
920
+ echo -e "${CYAN}STORAGE${RESET}"
921
+ echo " ~/.shipwright/optimization/outcomes.jsonl Outcome history"
922
+ echo " ~/.shipwright/optimization/template-weights.json Template selection weights"
923
+ echo " ~/.shipwright/optimization/model-routing.json Per-stage model routing"
924
+ echo " ~/.shipwright/optimization/iteration-model.json Iteration predictions"
925
+ echo ""
926
+ }
927
+
928
+ # ═════════════════════════════════════════════════════════════════════════════
929
+ # MAIN
930
+ # ═════════════════════════════════════════════════════════════════════════════
931
+
932
+ main() {
933
+ local cmd="${1:-help}"
934
+ shift 2>/dev/null || true
935
+ case "$cmd" in
936
+ analyze-outcome) optimize_analyze_outcome "$@" ;;
937
+ tune) optimize_full_analysis ;;
938
+ report) optimize_report ;;
939
+ evolve-memory) optimize_evolve_memory ;;
940
+ help|--help|-h) show_help ;;
941
+ *) error "Unknown command: $cmd"; exit 1 ;;
942
+ esac
943
+ }
944
+
945
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
946
+ main "$@"
947
+ fi