shipwright-cli 1.7.1 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +45 -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} +118 -22
- package/scripts/sw-adversarial.sh +274 -0
- package/scripts/sw-architecture-enforcer.sh +330 -0
- package/scripts/sw-checkpoint.sh +468 -0
- package/scripts/sw-cleanup.sh +359 -0
- package/scripts/sw-connect.sh +619 -0
- package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
- package/scripts/sw-daemon.sh +5574 -0
- 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/sw-loop.sh +2217 -0
- package/scripts/{cct-memory.sh → sw-memory.sh} +514 -36
- package/scripts/sw-patrol-meta.sh +417 -0
- package/scripts/sw-pipeline-composer.sh +455 -0
- package/scripts/sw-pipeline-vitals.sh +1096 -0
- package/scripts/sw-pipeline.sh +7593 -0
- 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} +9 -6
- package/scripts/{cct-reaper.sh → sw-reaper.sh} +10 -6
- package/scripts/sw-remote.sh +687 -0
- package/scripts/sw-self-optimize.sh +1048 -0
- package/scripts/sw-session.sh +541 -0
- package/scripts/sw-setup.sh +234 -0
- package/scripts/sw-status.sh +796 -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 +35 -6
- package/templates/pipelines/cost-aware.json +21 -0
- package/templates/pipelines/deployed.json +40 -6
- package/templates/pipelines/enterprise.json +16 -2
- package/templates/pipelines/fast.json +19 -0
- package/templates/pipelines/full.json +28 -2
- package/templates/pipelines/hotfix.json +19 -0
- package/templates/pipelines/standard.json +31 -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-cleanup.sh +0 -172
- package/scripts/cct-daemon.sh +0 -3189
- package/scripts/cct-doctor.sh +0 -414
- package/scripts/cct-loop.sh +0 -1332
- package/scripts/cct-pipeline.sh +0 -3844
- package/scripts/cct-session.sh +0 -284
- package/scripts/cct-status.sh +0 -169
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
# ║ Captures learnings · Injects context · Searches memory · Tracks metrics║
|
|
5
5
|
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
6
|
set -euo pipefail
|
|
7
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
7
8
|
|
|
8
|
-
VERSION="1.
|
|
9
|
+
VERSION="1.10.0"
|
|
9
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
11
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
11
12
|
|
|
@@ -20,6 +21,14 @@ DIM='\033[2m'
|
|
|
20
21
|
BOLD='\033[1m'
|
|
21
22
|
RESET='\033[0m'
|
|
22
23
|
|
|
24
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
25
|
+
# shellcheck source=lib/compat.sh
|
|
26
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
27
|
+
|
|
28
|
+
# ─── Intelligence Engine (optional) ──────────────────────────────────────────
|
|
29
|
+
# shellcheck source=sw-intelligence.sh
|
|
30
|
+
[[ -f "$SCRIPT_DIR/sw-intelligence.sh" ]] && source "$SCRIPT_DIR/sw-intelligence.sh"
|
|
31
|
+
|
|
23
32
|
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
24
33
|
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
25
34
|
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
@@ -30,7 +39,7 @@ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
|
30
39
|
now_epoch() { date +%s; }
|
|
31
40
|
|
|
32
41
|
# ─── Structured Event Log ──────────────────────────────────────────────────
|
|
33
|
-
EVENTS_FILE="${HOME}/.
|
|
42
|
+
EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
|
|
34
43
|
|
|
35
44
|
emit_event() {
|
|
36
45
|
local event_type="$1"
|
|
@@ -46,7 +55,7 @@ emit_event() {
|
|
|
46
55
|
json_fields="${json_fields},\"${key}\":\"${val}\""
|
|
47
56
|
fi
|
|
48
57
|
done
|
|
49
|
-
mkdir -p "${HOME}/.
|
|
58
|
+
mkdir -p "${HOME}/.shipwright"
|
|
50
59
|
echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
|
|
51
60
|
}
|
|
52
61
|
|
|
@@ -207,32 +216,272 @@ memory_capture_failure() {
|
|
|
207
216
|
'[.failures[]] | to_entries | map(select(.value.pattern == $pat)) | .[0].key // -1' \
|
|
208
217
|
"$failures_file" 2>/dev/null || echo "-1")
|
|
209
218
|
|
|
210
|
-
|
|
211
|
-
|
|
219
|
+
(
|
|
220
|
+
if command -v flock &>/dev/null; then
|
|
221
|
+
flock -w 10 200 2>/dev/null || { warn "Memory lock timeout"; return 1; }
|
|
222
|
+
fi
|
|
223
|
+
local tmp_file
|
|
224
|
+
tmp_file=$(mktemp "${failures_file}.tmp.XXXXXX")
|
|
225
|
+
|
|
226
|
+
if [[ "$existing_idx" != "-1" && "$existing_idx" != "null" ]]; then
|
|
227
|
+
# Update existing entry
|
|
228
|
+
jq --argjson idx "$existing_idx" \
|
|
229
|
+
--arg ts "$(now_iso)" \
|
|
230
|
+
'.failures[$idx].seen_count += 1 | .failures[$idx].last_seen = $ts' \
|
|
231
|
+
"$failures_file" > "$tmp_file" && mv "$tmp_file" "$failures_file" || rm -f "$tmp_file"
|
|
232
|
+
else
|
|
233
|
+
# Add new failure entry
|
|
234
|
+
jq --arg stage "$stage" \
|
|
235
|
+
--arg pattern "$pattern" \
|
|
236
|
+
--arg ts "$(now_iso)" \
|
|
237
|
+
'.failures += [{
|
|
238
|
+
stage: $stage,
|
|
239
|
+
pattern: $pattern,
|
|
240
|
+
root_cause: "",
|
|
241
|
+
fix: "",
|
|
242
|
+
seen_count: 1,
|
|
243
|
+
last_seen: $ts
|
|
244
|
+
}] | .failures = (.failures | .[-100:])' \
|
|
245
|
+
"$failures_file" > "$tmp_file" && mv "$tmp_file" "$failures_file" || rm -f "$tmp_file"
|
|
246
|
+
fi
|
|
247
|
+
) 200>"${failures_file}.lock"
|
|
248
|
+
|
|
249
|
+
emit_event "memory.failure" "stage=${stage}" "pattern=${pattern:0:80}"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# memory_record_fix_outcome <failure_hash_or_pattern> <fix_applied:bool> <fix_resolved:bool>
|
|
253
|
+
# Tracks whether suggested fixes actually worked. Builds effectiveness data
|
|
254
|
+
# so future memory injection can prioritize high-success-rate fixes.
|
|
255
|
+
memory_record_fix_outcome() {
|
|
256
|
+
local pattern_match="${1:-}"
|
|
257
|
+
local fix_applied="${2:-false}"
|
|
258
|
+
local fix_resolved="${3:-false}"
|
|
259
|
+
|
|
260
|
+
[[ -z "$pattern_match" ]] && return 1
|
|
261
|
+
|
|
262
|
+
ensure_memory_dir
|
|
263
|
+
local mem_dir
|
|
264
|
+
mem_dir="$(repo_memory_dir)"
|
|
265
|
+
local failures_file="$mem_dir/failures.json"
|
|
212
266
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
267
|
+
[[ ! -f "$failures_file" ]] && return 1
|
|
268
|
+
|
|
269
|
+
# Find matching failure by pattern substring
|
|
270
|
+
local match_idx
|
|
271
|
+
match_idx=$(jq --arg pat "$pattern_match" \
|
|
272
|
+
'[.failures[]] | to_entries | map(select(.value.pattern | contains($pat))) | .[0].key // -1' \
|
|
273
|
+
"$failures_file" 2>/dev/null || echo "-1")
|
|
274
|
+
|
|
275
|
+
if [[ "$match_idx" == "-1" || "$match_idx" == "null" ]]; then
|
|
276
|
+
warn "No matching failure found for: ${pattern_match:0:60}"
|
|
277
|
+
return 1
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# Update fix outcome tracking fields
|
|
281
|
+
local applied_inc=0 resolved_inc=0
|
|
282
|
+
[[ "$fix_applied" == "true" ]] && applied_inc=1
|
|
283
|
+
[[ "$fix_resolved" == "true" ]] && resolved_inc=1
|
|
284
|
+
|
|
285
|
+
(
|
|
286
|
+
if command -v flock &>/dev/null; then
|
|
287
|
+
flock -w 10 200 2>/dev/null || { warn "Memory lock timeout"; return 1; }
|
|
288
|
+
fi
|
|
289
|
+
local tmp_file
|
|
290
|
+
tmp_file=$(mktemp "${failures_file}.tmp.XXXXXX")
|
|
291
|
+
|
|
292
|
+
jq --argjson idx "$match_idx" \
|
|
293
|
+
--argjson app "$applied_inc" \
|
|
294
|
+
--argjson res "$resolved_inc" \
|
|
216
295
|
--arg ts "$(now_iso)" \
|
|
217
|
-
'.failures[$idx].
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
296
|
+
'.failures[$idx].times_fix_suggested = ((.failures[$idx].times_fix_suggested // 0) + 1) |
|
|
297
|
+
.failures[$idx].times_fix_applied = ((.failures[$idx].times_fix_applied // 0) + $app) |
|
|
298
|
+
.failures[$idx].times_fix_resolved = ((.failures[$idx].times_fix_resolved // 0) + $res) |
|
|
299
|
+
.failures[$idx].fix_effectiveness_rate = (
|
|
300
|
+
if ((.failures[$idx].times_fix_applied // 0) + $app) > 0 then
|
|
301
|
+
(((.failures[$idx].times_fix_resolved // 0) + $res) * 100 /
|
|
302
|
+
((.failures[$idx].times_fix_applied // 0) + $app))
|
|
303
|
+
else 0 end
|
|
304
|
+
) |
|
|
305
|
+
.failures[$idx].last_outcome_at = $ts' \
|
|
306
|
+
"$failures_file" > "$tmp_file" && mv "$tmp_file" "$failures_file" || rm -f "$tmp_file"
|
|
307
|
+
) 200>"${failures_file}.lock"
|
|
308
|
+
|
|
309
|
+
emit_event "memory.fix_outcome" \
|
|
310
|
+
"pattern=${pattern_match:0:60}" \
|
|
311
|
+
"applied=${fix_applied}" \
|
|
312
|
+
"resolved=${fix_resolved}"
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# memory_track_fix <error_sig> <success_bool>
|
|
316
|
+
# Convenience wrapper for memory_record_fix_outcome
|
|
317
|
+
memory_track_fix() {
|
|
318
|
+
local error_sig="${1:-}"
|
|
319
|
+
local success="${2:-false}"
|
|
320
|
+
[[ -z "$error_sig" ]] && return 0
|
|
321
|
+
memory_record_fix_outcome "$error_sig" "true" "$success" 2>/dev/null || true
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
# memory_query_fix_for_error <error_pattern>
|
|
325
|
+
# Searches failure memory for known fixes matching the given error pattern.
|
|
326
|
+
# Returns JSON with the best fix (highest effectiveness rate) or empty.
|
|
327
|
+
memory_query_fix_for_error() {
|
|
328
|
+
local error_pattern="$1"
|
|
329
|
+
[[ -z "$error_pattern" ]] && return 0
|
|
330
|
+
|
|
331
|
+
ensure_memory_dir
|
|
332
|
+
local mem_dir
|
|
333
|
+
mem_dir="$(repo_memory_dir)"
|
|
334
|
+
local failures_file="$mem_dir/failures.json"
|
|
335
|
+
|
|
336
|
+
[[ ! -f "$failures_file" ]] && return 0
|
|
337
|
+
|
|
338
|
+
# Search for matching failures with successful fixes
|
|
339
|
+
local matches
|
|
340
|
+
matches=$(jq -r --arg pat "$error_pattern" '
|
|
341
|
+
[.failures[]
|
|
342
|
+
| select(.pattern != null and .pattern != "")
|
|
343
|
+
| select(.pattern | test($pat; "i") // false)
|
|
344
|
+
| select(.fix != null and .fix != "")
|
|
345
|
+
| select((.fix_effectiveness_rate // 0) > 30)
|
|
346
|
+
| {fix, fix_effectiveness_rate, seen_count, category, stage, pattern}]
|
|
347
|
+
| sort_by(-.fix_effectiveness_rate)
|
|
348
|
+
| .[0] // null
|
|
349
|
+
' "$failures_file" 2>/dev/null) || true
|
|
350
|
+
|
|
351
|
+
if [[ -n "$matches" && "$matches" != "null" ]]; then
|
|
352
|
+
echo "$matches"
|
|
353
|
+
fi
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# memory_closed_loop_inject <error_sig>
|
|
357
|
+
# Combines error → memory → fix into injectable text for build retries.
|
|
358
|
+
# Returns a one-line summary suitable for goal augmentation.
|
|
359
|
+
memory_closed_loop_inject() {
|
|
360
|
+
local error_sig="$1"
|
|
361
|
+
[[ -z "$error_sig" ]] && return 0
|
|
362
|
+
|
|
363
|
+
local fix_json
|
|
364
|
+
fix_json=$(memory_query_fix_for_error "$error_sig") || true
|
|
365
|
+
[[ -z "$fix_json" || "$fix_json" == "null" ]] && return 0
|
|
366
|
+
|
|
367
|
+
local fix_text success_rate category
|
|
368
|
+
fix_text=$(echo "$fix_json" | jq -r '.fix // ""')
|
|
369
|
+
success_rate=$(echo "$fix_json" | jq -r '.fix_effectiveness_rate // 0')
|
|
370
|
+
category=$(echo "$fix_json" | jq -r '.category // "unknown"')
|
|
371
|
+
|
|
372
|
+
[[ -z "$fix_text" ]] && return 0
|
|
373
|
+
|
|
374
|
+
echo "[$category, ${success_rate}% success rate] $fix_text"
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
memory_capture_failure_from_log() {
|
|
378
|
+
local artifacts_dir="${1:-}"
|
|
379
|
+
local error_log="${artifacts_dir}/error-log.jsonl"
|
|
380
|
+
[[ ! -f "$error_log" ]] && return 0
|
|
381
|
+
|
|
382
|
+
ensure_memory_dir
|
|
383
|
+
local mem_dir
|
|
384
|
+
mem_dir="$(repo_memory_dir)"
|
|
385
|
+
local failures_file="$mem_dir/failures.json"
|
|
386
|
+
|
|
387
|
+
# Read last 50 entries
|
|
388
|
+
local entries
|
|
389
|
+
entries=$(tail -50 "$error_log" 2>/dev/null) || return 0
|
|
390
|
+
[[ -z "$entries" ]] && return 0
|
|
391
|
+
|
|
392
|
+
local captured=0
|
|
393
|
+
while IFS= read -r line; do
|
|
394
|
+
[[ -z "$line" ]] && continue
|
|
395
|
+
|
|
396
|
+
local err_type err_text
|
|
397
|
+
err_type=$(echo "$line" | jq -r '.type // "unknown"' 2>/dev/null) || continue
|
|
398
|
+
err_text=$(echo "$line" | jq -r '.error // ""' 2>/dev/null) || continue
|
|
399
|
+
[[ -z "$err_text" ]] && continue
|
|
400
|
+
|
|
401
|
+
# Deduplicate: skip if this exact pattern already exists in failures
|
|
402
|
+
local pattern_short
|
|
403
|
+
pattern_short=$(echo "$err_text" | head -1 | cut -c1-200)
|
|
404
|
+
local already_exists
|
|
405
|
+
already_exists=$(jq --arg pat "$pattern_short" \
|
|
406
|
+
'[.failures[] | select(.pattern == $pat)] | length' \
|
|
407
|
+
"$failures_file" 2>/dev/null || echo "0")
|
|
408
|
+
if [[ "${already_exists:-0}" -gt 0 ]]; then
|
|
409
|
+
continue
|
|
410
|
+
fi
|
|
411
|
+
|
|
412
|
+
# Feed into memory_capture_failure with the error type as stage
|
|
413
|
+
memory_capture_failure "$err_type" "$err_text" 2>/dev/null || true
|
|
414
|
+
captured=$((captured + 1))
|
|
415
|
+
done <<< "$entries"
|
|
416
|
+
|
|
417
|
+
if [[ "$captured" -gt 0 ]]; then
|
|
418
|
+
emit_event "memory.error_log_processed" "captured=$captured"
|
|
419
|
+
fi
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# _memory_aggregate_global
|
|
423
|
+
# Promotes high-frequency failure patterns to global.json for cross-repo learning
|
|
424
|
+
_memory_aggregate_global() {
|
|
425
|
+
ensure_memory_dir
|
|
426
|
+
local mem_dir
|
|
427
|
+
mem_dir="$(repo_memory_dir)"
|
|
428
|
+
local failures_file="$mem_dir/failures.json"
|
|
429
|
+
[[ ! -f "$failures_file" ]] && return 0
|
|
430
|
+
|
|
431
|
+
local global_file="$GLOBAL_MEMORY"
|
|
432
|
+
[[ ! -f "$global_file" ]] && return 0
|
|
433
|
+
|
|
434
|
+
# Find patterns with seen_count >= 3
|
|
435
|
+
local frequent_patterns
|
|
436
|
+
frequent_patterns=$(jq -r '.failures[] | select(.seen_count >= 3) | .pattern' \
|
|
437
|
+
"$failures_file" 2>/dev/null) || return 0
|
|
438
|
+
[[ -z "$frequent_patterns" ]] && return 0
|
|
439
|
+
|
|
440
|
+
local promoted=0
|
|
441
|
+
while IFS= read -r pattern; do
|
|
442
|
+
[[ -z "$pattern" ]] && continue
|
|
443
|
+
|
|
444
|
+
# Check if already in global
|
|
445
|
+
local exists
|
|
446
|
+
exists=$(jq --arg p "$pattern" \
|
|
447
|
+
'[.common_patterns[] | select(.pattern == $p)] | length' \
|
|
448
|
+
"$global_file" 2>/dev/null || echo "0")
|
|
449
|
+
if [[ "${exists:-0}" -gt 0 ]]; then
|
|
450
|
+
continue
|
|
451
|
+
fi
|
|
452
|
+
|
|
453
|
+
# Add to global, cap at 100 entries
|
|
454
|
+
local tmp_global
|
|
455
|
+
tmp_global=$(mktemp "${global_file}.tmp.XXXXXX")
|
|
456
|
+
jq --arg p "$pattern" \
|
|
223
457
|
--arg ts "$(now_iso)" \
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
458
|
+
--arg cat "general" \
|
|
459
|
+
'.common_patterns += [{pattern: $p, promoted_at: $ts, category: $cat, source: "aggregate"}] |
|
|
460
|
+
.common_patterns = (.common_patterns | .[-100:])' \
|
|
461
|
+
"$global_file" > "$tmp_global" && mv "$tmp_global" "$global_file" || rm -f "$tmp_global"
|
|
462
|
+
promoted=$((promoted + 1))
|
|
463
|
+
done <<< "$frequent_patterns"
|
|
464
|
+
|
|
465
|
+
if [[ "$promoted" -gt 0 ]]; then
|
|
466
|
+
emit_event "memory.global_aggregated" "promoted=$promoted"
|
|
233
467
|
fi
|
|
468
|
+
}
|
|
234
469
|
|
|
235
|
-
|
|
470
|
+
# memory_finalize_pipeline <state_file> <artifacts_dir>
|
|
471
|
+
# Single call that closes multiple feedback loops at pipeline completion
|
|
472
|
+
memory_finalize_pipeline() {
|
|
473
|
+
local state_file="${1:-}"
|
|
474
|
+
local artifacts_dir="${2:-}"
|
|
475
|
+
[[ -z "$state_file" || ! -f "$state_file" ]] && return 0
|
|
476
|
+
|
|
477
|
+
# Step 1: Capture pipeline-level learnings
|
|
478
|
+
memory_capture_pipeline "$state_file" "$artifacts_dir" 2>/dev/null || true
|
|
479
|
+
|
|
480
|
+
# Step 2: Process error log into failures.json
|
|
481
|
+
memory_capture_failure_from_log "$artifacts_dir" 2>/dev/null || true
|
|
482
|
+
|
|
483
|
+
# Step 3: Aggregate high-frequency patterns to global memory
|
|
484
|
+
_memory_aggregate_global 2>/dev/null || true
|
|
236
485
|
}
|
|
237
486
|
|
|
238
487
|
# memory_analyze_failure <log_file> <stage>
|
|
@@ -281,16 +530,41 @@ memory_analyze_failure() {
|
|
|
281
530
|
|
|
282
531
|
info "Analyzing failure in ${CYAN}${stage}${RESET} stage..."
|
|
283
532
|
|
|
533
|
+
# Gather past successful analyses for the same stage/category as examples
|
|
534
|
+
local past_examples=""
|
|
535
|
+
if [[ -f "$failures_file" ]]; then
|
|
536
|
+
past_examples=$(jq -r --arg stg "$stage" \
|
|
537
|
+
'[.failures[] | select(.stage == $stg and .root_cause != "" and .fix != "")] |
|
|
538
|
+
sort_by(-.fix_effectiveness_rate // 0) | .[:2][] |
|
|
539
|
+
"- Pattern: \(.pattern[:80])\n Root cause: \(.root_cause)\n Fix: \(.fix)"' \
|
|
540
|
+
"$failures_file" 2>/dev/null || true)
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
# Build valid categories list (from compat.sh if available, else hardcoded)
|
|
544
|
+
local valid_cats="test_failure, build_error, lint_error, timeout, dependency, flaky, config"
|
|
545
|
+
if [[ -n "${SW_ERROR_CATEGORIES:-}" ]]; then
|
|
546
|
+
valid_cats=$(echo "$SW_ERROR_CATEGORIES" | tr ' ' ', ')
|
|
547
|
+
fi
|
|
548
|
+
|
|
284
549
|
# Build the analysis prompt
|
|
285
550
|
local prompt
|
|
286
551
|
prompt="Analyze this pipeline failure. The stage was: ${stage}.
|
|
287
552
|
The error pattern is: ${last_pattern}
|
|
288
553
|
|
|
289
554
|
Log output (last 200 lines):
|
|
290
|
-
${log_tail}
|
|
555
|
+
${log_tail}"
|
|
556
|
+
|
|
557
|
+
if [[ -n "$past_examples" ]]; then
|
|
558
|
+
prompt="${prompt}
|
|
559
|
+
|
|
560
|
+
Here are examples of how similar failures were diagnosed in this repo:
|
|
561
|
+
${past_examples}"
|
|
562
|
+
fi
|
|
563
|
+
|
|
564
|
+
prompt="${prompt}
|
|
291
565
|
|
|
292
566
|
Return ONLY a JSON object with exactly these fields:
|
|
293
|
-
{\"root_cause\": \"one-line root cause\", \"fix\": \"one-line fix suggestion\", \"category\": \"one of:
|
|
567
|
+
{\"root_cause\": \"one-line root cause\", \"fix\": \"one-line fix suggestion\", \"category\": \"one of: ${valid_cats}\"}
|
|
294
568
|
|
|
295
569
|
Return JSON only, no markdown fences, no explanation."
|
|
296
570
|
|
|
@@ -315,11 +589,17 @@ Return JSON only, no markdown fences, no explanation."
|
|
|
315
589
|
return 1
|
|
316
590
|
fi
|
|
317
591
|
|
|
318
|
-
# Validate category against
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
592
|
+
# Validate category against shared taxonomy (compat.sh) or built-in list
|
|
593
|
+
if type sw_valid_error_category &>/dev/null 2>&1; then
|
|
594
|
+
if ! sw_valid_error_category "$category"; then
|
|
595
|
+
category="unknown"
|
|
596
|
+
fi
|
|
597
|
+
else
|
|
598
|
+
case "$category" in
|
|
599
|
+
test_failure|build_error|lint_error|timeout|dependency|flaky|config) ;;
|
|
600
|
+
*) category="unknown" ;;
|
|
601
|
+
esac
|
|
602
|
+
fi
|
|
323
603
|
|
|
324
604
|
# Update the most recent failure entry with root_cause, fix, category
|
|
325
605
|
local tmp_file
|
|
@@ -478,9 +758,27 @@ memory_capture_pattern() {
|
|
|
478
758
|
|
|
479
759
|
# memory_inject_context <stage_id>
|
|
480
760
|
# Returns a text block of relevant memory for a given pipeline stage.
|
|
761
|
+
# When intelligence engine is available, uses AI-ranked search for better relevance.
|
|
481
762
|
memory_inject_context() {
|
|
482
763
|
local stage_id="${1:-}"
|
|
483
764
|
|
|
765
|
+
# Try intelligence-ranked search first
|
|
766
|
+
if type intelligence_search_memory &>/dev/null 2>&1; then
|
|
767
|
+
local config="${REPO_DIR:-.}/.claude/daemon-config.json"
|
|
768
|
+
local intel_enabled="false"
|
|
769
|
+
if [[ -f "$config" ]]; then
|
|
770
|
+
intel_enabled=$(jq -r '.intelligence.enabled // false' "$config" 2>/dev/null || echo "false")
|
|
771
|
+
fi
|
|
772
|
+
if [[ "$intel_enabled" == "true" ]]; then
|
|
773
|
+
local ranked_result
|
|
774
|
+
ranked_result=$(intelligence_search_memory "$stage_id stage context" "$(repo_memory_dir)" 5 2>/dev/null || echo "")
|
|
775
|
+
if [[ -n "$ranked_result" ]] && [[ "$ranked_result" != *'"error"'* ]]; then
|
|
776
|
+
echo "$ranked_result"
|
|
777
|
+
return 0
|
|
778
|
+
fi
|
|
779
|
+
fi
|
|
780
|
+
fi
|
|
781
|
+
|
|
484
782
|
ensure_memory_dir
|
|
485
783
|
local mem_dir
|
|
486
784
|
mem_dir="$(repo_memory_dir)"
|
|
@@ -537,12 +835,28 @@ memory_inject_context() {
|
|
|
537
835
|
;;
|
|
538
836
|
|
|
539
837
|
build)
|
|
540
|
-
# Failure patterns to avoid +
|
|
838
|
+
# Failure patterns to avoid — ranked by relevance (recency + effectiveness + frequency)
|
|
541
839
|
echo "## Failure Patterns to Avoid"
|
|
542
840
|
if [[ -f "$mem_dir/failures.json" ]]; then
|
|
543
|
-
jq -r '
|
|
841
|
+
jq -r 'now as $now |
|
|
842
|
+
.failures | map(. +
|
|
843
|
+
{ relevance_score:
|
|
844
|
+
((.seen_count // 1) * 1) +
|
|
845
|
+
(if .fix_effectiveness_rate then (.fix_effectiveness_rate / 10) else 0 end) +
|
|
846
|
+
(if .last_seen then
|
|
847
|
+
(($now - ((.last_seen | sub("\\.[0-9]+Z$"; "Z") | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) // 0)) |
|
|
848
|
+
if . < 86400 then 5
|
|
849
|
+
elif . < 604800 then 3
|
|
850
|
+
elif . < 2592000 then 1
|
|
851
|
+
else 0 end)
|
|
852
|
+
else 0 end)
|
|
853
|
+
}
|
|
854
|
+
) | sort_by(-.relevance_score) | .[:10][] |
|
|
544
855
|
"- [\(.stage)] \(.pattern) (seen \(.seen_count)x)" +
|
|
545
|
-
if .fix != "" then
|
|
856
|
+
if .fix != "" then
|
|
857
|
+
"\n Fix: \(.fix)" +
|
|
858
|
+
if .fix_effectiveness_rate then " (effectiveness: \(.fix_effectiveness_rate)%)" else "" end
|
|
859
|
+
else "" end' \
|
|
546
860
|
"$mem_dir/failures.json" 2>/dev/null || echo "- No failures recorded."
|
|
547
861
|
fi
|
|
548
862
|
|
|
@@ -550,7 +864,8 @@ memory_inject_context() {
|
|
|
550
864
|
echo "## Known Fixes"
|
|
551
865
|
if [[ -f "$mem_dir/failures.json" ]]; then
|
|
552
866
|
jq -r '.failures[] | select(.root_cause != "" and .fix != "" and .stage == "build") |
|
|
553
|
-
"- [\(.category // "unknown")] \(.root_cause)\n Fix: \(.fix)"
|
|
867
|
+
"- [\(.category // "unknown")] \(.root_cause)\n Fix: \(.fix)" +
|
|
868
|
+
if .fix_effectiveness_rate then " (effectiveness: \(.fix_effectiveness_rate)%)" else "" end' \
|
|
554
869
|
"$mem_dir/failures.json" 2>/dev/null || echo "- No analyzed fixes yet."
|
|
555
870
|
else
|
|
556
871
|
echo "- No analyzed fixes yet."
|
|
@@ -618,19 +933,150 @@ memory_inject_context() {
|
|
|
618
933
|
;;
|
|
619
934
|
|
|
620
935
|
*)
|
|
621
|
-
# Generic context for any
|
|
936
|
+
# Generic context for any stage — inject top-K most relevant across all categories
|
|
622
937
|
echo "## Repository Patterns"
|
|
623
938
|
if [[ -f "$mem_dir/patterns.json" ]]; then
|
|
624
939
|
jq -r 'to_entries | map(select(.key != "known_issues")) | from_entries' \
|
|
625
940
|
"$mem_dir/patterns.json" 2>/dev/null || true
|
|
626
941
|
fi
|
|
942
|
+
|
|
943
|
+
# Inject top failures regardless of category (ranked by relevance)
|
|
944
|
+
echo ""
|
|
945
|
+
echo "## Relevant Failure Patterns"
|
|
946
|
+
if [[ -f "$mem_dir/failures.json" ]]; then
|
|
947
|
+
jq -r --arg stg "$stage_id" \
|
|
948
|
+
'.failures |
|
|
949
|
+
map(. + { stage_match: (if .stage == $stg then 10 else 0 end) }) |
|
|
950
|
+
sort_by(-(.seen_count + .stage_match + (.fix_effectiveness_rate // 0) / 10)) |
|
|
951
|
+
.[:5][] |
|
|
952
|
+
"- [\(.stage)] \(.pattern[:80]) (seen \(.seen_count)x)" +
|
|
953
|
+
if .fix != "" then "\n Fix: \(.fix)" else "" end' \
|
|
954
|
+
"$mem_dir/failures.json" 2>/dev/null || echo "- None recorded."
|
|
955
|
+
fi
|
|
956
|
+
|
|
957
|
+
# Inject recent decisions
|
|
958
|
+
echo ""
|
|
959
|
+
echo "## Recent Decisions"
|
|
960
|
+
if [[ -f "$mem_dir/decisions.json" ]]; then
|
|
961
|
+
jq -r '.decisions[-3:][] |
|
|
962
|
+
"- [\(.type // "decision")] \(.summary // "no description")"' \
|
|
963
|
+
"$mem_dir/decisions.json" 2>/dev/null || echo "- None recorded."
|
|
964
|
+
fi
|
|
627
965
|
;;
|
|
628
966
|
esac
|
|
629
967
|
|
|
968
|
+
# ── Cross-repo memory injection (global learnings) ──
|
|
969
|
+
if [[ -f "$GLOBAL_MEMORY" ]]; then
|
|
970
|
+
local global_patterns
|
|
971
|
+
global_patterns=$(jq -r --arg stage "$stage_id" '
|
|
972
|
+
.common_patterns // [] | .[] |
|
|
973
|
+
select(.category == $stage or .category == "general" or .category == null) |
|
|
974
|
+
.summary // .description // empty
|
|
975
|
+
' "$GLOBAL_MEMORY" 2>/dev/null | head -5 || true)
|
|
976
|
+
|
|
977
|
+
local cross_repo_learnings
|
|
978
|
+
cross_repo_learnings=$(jq -r '
|
|
979
|
+
.cross_repo_learnings // [] | .[-5:][] |
|
|
980
|
+
"- [\(.repo // "unknown")] \(.type // "learning"): bugs=\(.bugs // 0), warnings=\(.warnings // 0)"
|
|
981
|
+
' "$GLOBAL_MEMORY" 2>/dev/null | head -5 || true)
|
|
982
|
+
|
|
983
|
+
if [[ -n "$global_patterns" || -n "$cross_repo_learnings" ]]; then
|
|
984
|
+
echo ""
|
|
985
|
+
echo "## Cross-Repo Learnings (Global)"
|
|
986
|
+
[[ -n "$global_patterns" ]] && echo "$global_patterns"
|
|
987
|
+
[[ -n "$cross_repo_learnings" ]] && echo "$cross_repo_learnings"
|
|
988
|
+
fi
|
|
989
|
+
fi
|
|
990
|
+
|
|
630
991
|
echo ""
|
|
631
992
|
emit_event "memory.inject" "stage=${stage_id}"
|
|
632
993
|
}
|
|
633
994
|
|
|
995
|
+
# memory_get_actionable_failures [threshold]
|
|
996
|
+
# Returns JSON array of failure patterns with seen_count >= threshold.
|
|
997
|
+
# Used by daemon patrol to detect recurring failures worth fixing.
|
|
998
|
+
memory_get_actionable_failures() {
|
|
999
|
+
local threshold="${1:-3}"
|
|
1000
|
+
|
|
1001
|
+
ensure_memory_dir
|
|
1002
|
+
local mem_dir
|
|
1003
|
+
mem_dir="$(repo_memory_dir)"
|
|
1004
|
+
local failures_file="$mem_dir/failures.json"
|
|
1005
|
+
|
|
1006
|
+
if [[ ! -f "$failures_file" ]]; then
|
|
1007
|
+
echo "[]"
|
|
1008
|
+
return 0
|
|
1009
|
+
fi
|
|
1010
|
+
|
|
1011
|
+
jq --argjson t "$threshold" \
|
|
1012
|
+
'[.failures[] | select(.seen_count >= $t)] | sort_by(-.seen_count)' \
|
|
1013
|
+
"$failures_file" 2>/dev/null || echo "[]"
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
# memory_get_dora_baseline [window_days] [offset_days]
|
|
1017
|
+
# Calculates DORA metrics for a time window from events.jsonl.
|
|
1018
|
+
# Returns JSON: {deploy_freq, cycle_time, cfr, mttr, total, grades: {df, ct, cfr, mttr}}
|
|
1019
|
+
memory_get_dora_baseline() {
|
|
1020
|
+
local window_days="${1:-7}"
|
|
1021
|
+
local offset_days="${2:-0}"
|
|
1022
|
+
|
|
1023
|
+
local events_file="${HOME}/.shipwright/events.jsonl"
|
|
1024
|
+
if [[ ! -f "$events_file" ]]; then
|
|
1025
|
+
echo '{"deploy_freq":0,"cycle_time":0,"cfr":0,"mttr":0,"total":0}'
|
|
1026
|
+
return 0
|
|
1027
|
+
fi
|
|
1028
|
+
|
|
1029
|
+
local now_e
|
|
1030
|
+
now_e=$(now_epoch)
|
|
1031
|
+
local window_end=$((now_e - offset_days * 86400))
|
|
1032
|
+
local window_start=$((window_end - window_days * 86400))
|
|
1033
|
+
|
|
1034
|
+
# Extract pipeline events for the window
|
|
1035
|
+
local metrics
|
|
1036
|
+
metrics=$(jq -s --argjson start "$window_start" --argjson end "$window_end" '
|
|
1037
|
+
[.[] | select(.ts_epoch >= $start and .ts_epoch < $end)] as $events |
|
|
1038
|
+
[$events[] | select(.type == "pipeline.completed")] as $completed |
|
|
1039
|
+
($completed | length) as $total |
|
|
1040
|
+
[$completed[] | select(.result == "success")] as $successes |
|
|
1041
|
+
[$completed[] | select(.result == "failure")] as $failures |
|
|
1042
|
+
($successes | length) as $success_count |
|
|
1043
|
+
($failures | length) as $failure_count |
|
|
1044
|
+
|
|
1045
|
+
# Deploy frequency (per week)
|
|
1046
|
+
(if $total > 0 then ($success_count * 7 / '"$window_days"') else 0 end) as $deploy_freq |
|
|
1047
|
+
|
|
1048
|
+
# Cycle time median
|
|
1049
|
+
([$successes[] | .duration_s] | sort |
|
|
1050
|
+
if length > 0 then .[length/2 | floor] else 0 end) as $cycle_time |
|
|
1051
|
+
|
|
1052
|
+
# Change failure rate
|
|
1053
|
+
(if $total > 0 then ($failure_count / $total * 100) else 0 end) as $cfr |
|
|
1054
|
+
|
|
1055
|
+
# MTTR
|
|
1056
|
+
($completed | sort_by(.ts_epoch // 0) |
|
|
1057
|
+
[range(length) as $i |
|
|
1058
|
+
if .[$i].result == "failure" then
|
|
1059
|
+
[.[$i+1:][] | select(.result == "success")][0] as $next |
|
|
1060
|
+
if $next and $next.ts_epoch and .[$i].ts_epoch then
|
|
1061
|
+
($next.ts_epoch - .[$i].ts_epoch)
|
|
1062
|
+
else null end
|
|
1063
|
+
else null end
|
|
1064
|
+
] | map(select(. != null)) |
|
|
1065
|
+
if length > 0 then (add / length | floor) else 0 end
|
|
1066
|
+
) as $mttr |
|
|
1067
|
+
|
|
1068
|
+
{
|
|
1069
|
+
deploy_freq: ($deploy_freq * 10 | floor / 10),
|
|
1070
|
+
cycle_time: $cycle_time,
|
|
1071
|
+
cfr: ($cfr * 10 | floor / 10),
|
|
1072
|
+
mttr: $mttr,
|
|
1073
|
+
total: $total
|
|
1074
|
+
}
|
|
1075
|
+
' "$events_file" 2>/dev/null || echo '{"deploy_freq":0,"cycle_time":0,"cfr":0,"mttr":0,"total":0}')
|
|
1076
|
+
|
|
1077
|
+
echo "$metrics"
|
|
1078
|
+
}
|
|
1079
|
+
|
|
634
1080
|
# memory_update_metrics <metric_name> <value>
|
|
635
1081
|
# Track performance baselines and flag regressions.
|
|
636
1082
|
memory_update_metrics() {
|
|
@@ -838,6 +1284,34 @@ memory_search() {
|
|
|
838
1284
|
|
|
839
1285
|
local found=0
|
|
840
1286
|
|
|
1287
|
+
# ── Semantic search via intelligence (if available) ──
|
|
1288
|
+
if type intelligence_search_memory &>/dev/null 2>&1; then
|
|
1289
|
+
local semantic_results
|
|
1290
|
+
semantic_results=$(intelligence_search_memory "$keyword" "$mem_dir" 5 2>/dev/null || echo "")
|
|
1291
|
+
if [[ -n "$semantic_results" ]] && echo "$semantic_results" | jq -e '.results | length > 0' &>/dev/null; then
|
|
1292
|
+
echo -e " ${BOLD}${CYAN}Semantic Results (AI-ranked):${RESET}"
|
|
1293
|
+
local result_count
|
|
1294
|
+
result_count=$(echo "$semantic_results" | jq '.results | length')
|
|
1295
|
+
local i=0
|
|
1296
|
+
while [[ "$i" -lt "$result_count" ]]; do
|
|
1297
|
+
local file rel summary
|
|
1298
|
+
file=$(echo "$semantic_results" | jq -r ".results[$i].file // \"\"")
|
|
1299
|
+
rel=$(echo "$semantic_results" | jq -r ".results[$i].relevance // 0")
|
|
1300
|
+
summary=$(echo "$semantic_results" | jq -r ".results[$i].summary // \"\"")
|
|
1301
|
+
echo -e " ${GREEN}●${RESET} [${rel}%] ${BOLD}${file}${RESET} — ${summary}"
|
|
1302
|
+
i=$((i + 1))
|
|
1303
|
+
done
|
|
1304
|
+
echo ""
|
|
1305
|
+
found=$((found + 1))
|
|
1306
|
+
|
|
1307
|
+
# Also run grep search below for completeness
|
|
1308
|
+
echo -e " ${DIM}Grep results (supplemental):${RESET}"
|
|
1309
|
+
echo ""
|
|
1310
|
+
fi
|
|
1311
|
+
fi
|
|
1312
|
+
|
|
1313
|
+
# ── Grep-based search (fallback / supplemental) ──
|
|
1314
|
+
|
|
841
1315
|
# Search patterns
|
|
842
1316
|
if [[ -f "$mem_dir/patterns.json" ]]; then
|
|
843
1317
|
local pattern_matches
|
|
@@ -1083,6 +1557,7 @@ show_help() {
|
|
|
1083
1557
|
echo -e " ${CYAN}metric${RESET} <name> <value> Update a performance baseline"
|
|
1084
1558
|
echo -e " ${CYAN}decision${RESET} <type> <summary> Record a design decision"
|
|
1085
1559
|
echo -e " ${CYAN}analyze-failure${RESET} <log> <stage> Analyze failure root cause via AI"
|
|
1560
|
+
echo -e " ${CYAN}fix-outcome${RESET} <pattern> <applied> <resolved> Record fix effectiveness"
|
|
1086
1561
|
echo ""
|
|
1087
1562
|
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
1088
1563
|
echo -e " ${DIM}shipwright memory show${RESET} # View repo memory"
|
|
@@ -1136,6 +1611,9 @@ case "$SUBCOMMAND" in
|
|
|
1136
1611
|
analyze-failure)
|
|
1137
1612
|
memory_analyze_failure "$@"
|
|
1138
1613
|
;;
|
|
1614
|
+
fix-outcome)
|
|
1615
|
+
memory_record_fix_outcome "$@"
|
|
1616
|
+
;;
|
|
1139
1617
|
help|--help|-h)
|
|
1140
1618
|
show_help
|
|
1141
1619
|
;;
|