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
|
@@ -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
|