shipwright-cli 2.3.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -28
- package/completions/_shipwright +1 -1
- package/completions/shipwright.bash +3 -8
- package/completions/shipwright.fish +1 -1
- package/config/defaults.json +111 -0
- package/config/event-schema.json +81 -0
- package/config/policy.json +155 -2
- package/config/policy.schema.json +162 -1
- package/dashboard/coverage/coverage-summary.json +14 -0
- package/dashboard/public/index.html +1 -1
- package/dashboard/server.ts +306 -17
- package/dashboard/src/components/charts/bar.test.ts +79 -0
- package/dashboard/src/components/charts/donut.test.ts +68 -0
- package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
- package/dashboard/src/components/charts/sparkline.test.ts +125 -0
- package/dashboard/src/core/api.test.ts +309 -0
- package/dashboard/src/core/helpers.test.ts +301 -0
- package/dashboard/src/core/router.test.ts +307 -0
- package/dashboard/src/core/router.ts +7 -0
- package/dashboard/src/core/sse.test.ts +144 -0
- package/dashboard/src/views/metrics.test.ts +186 -0
- package/dashboard/src/views/overview.test.ts +173 -0
- package/dashboard/src/views/pipelines.test.ts +183 -0
- package/dashboard/src/views/team.test.ts +253 -0
- package/dashboard/vitest.config.ts +14 -5
- package/docs/TIPS.md +1 -1
- package/docs/patterns/README.md +1 -1
- package/package.json +15 -5
- package/scripts/adapters/docker-deploy.sh +1 -1
- package/scripts/adapters/tmux-adapter.sh +11 -1
- package/scripts/adapters/wezterm-adapter.sh +1 -1
- package/scripts/check-version-consistency.sh +1 -1
- package/scripts/lib/architecture.sh +126 -0
- package/scripts/lib/bootstrap.sh +75 -0
- package/scripts/lib/compat.sh +89 -6
- package/scripts/lib/config.sh +91 -0
- package/scripts/lib/daemon-adaptive.sh +3 -3
- package/scripts/lib/daemon-dispatch.sh +39 -16
- package/scripts/lib/daemon-health.sh +1 -1
- package/scripts/lib/daemon-patrol.sh +24 -12
- package/scripts/lib/daemon-poll.sh +37 -25
- package/scripts/lib/daemon-state.sh +115 -23
- package/scripts/lib/daemon-triage.sh +30 -8
- package/scripts/lib/fleet-failover.sh +63 -0
- package/scripts/lib/helpers.sh +30 -6
- package/scripts/lib/pipeline-detection.sh +2 -2
- package/scripts/lib/pipeline-github.sh +9 -9
- package/scripts/lib/pipeline-intelligence.sh +85 -35
- package/scripts/lib/pipeline-quality-checks.sh +16 -16
- package/scripts/lib/pipeline-quality.sh +1 -1
- package/scripts/lib/pipeline-stages.sh +242 -28
- package/scripts/lib/pipeline-state.sh +40 -4
- package/scripts/lib/test-helpers.sh +247 -0
- package/scripts/postinstall.mjs +3 -11
- package/scripts/sw +10 -4
- package/scripts/sw-activity.sh +1 -11
- package/scripts/sw-adaptive.sh +109 -85
- package/scripts/sw-adversarial.sh +4 -14
- package/scripts/sw-architecture-enforcer.sh +1 -11
- package/scripts/sw-auth.sh +8 -17
- package/scripts/sw-autonomous.sh +111 -49
- package/scripts/sw-changelog.sh +1 -11
- package/scripts/sw-checkpoint.sh +144 -20
- package/scripts/sw-ci.sh +2 -12
- package/scripts/sw-cleanup.sh +13 -17
- package/scripts/sw-code-review.sh +16 -36
- package/scripts/sw-connect.sh +5 -12
- package/scripts/sw-context.sh +9 -26
- package/scripts/sw-cost.sh +6 -16
- package/scripts/sw-daemon.sh +75 -70
- package/scripts/sw-dashboard.sh +57 -17
- package/scripts/sw-db.sh +506 -15
- package/scripts/sw-decompose.sh +1 -11
- package/scripts/sw-deps.sh +15 -25
- package/scripts/sw-developer-simulation.sh +1 -11
- package/scripts/sw-discovery.sh +112 -30
- package/scripts/sw-doc-fleet.sh +7 -17
- package/scripts/sw-docs-agent.sh +6 -16
- package/scripts/sw-docs.sh +4 -12
- package/scripts/sw-doctor.sh +134 -43
- package/scripts/sw-dora.sh +11 -19
- package/scripts/sw-durable.sh +35 -52
- package/scripts/sw-e2e-orchestrator.sh +11 -27
- package/scripts/sw-eventbus.sh +115 -115
- package/scripts/sw-evidence.sh +748 -0
- package/scripts/sw-feedback.sh +3 -13
- package/scripts/sw-fix.sh +2 -20
- package/scripts/sw-fleet-discover.sh +1 -11
- package/scripts/sw-fleet-viz.sh +10 -18
- package/scripts/sw-fleet.sh +13 -17
- package/scripts/sw-github-app.sh +6 -16
- package/scripts/sw-github-checks.sh +1 -11
- package/scripts/sw-github-deploy.sh +1 -11
- package/scripts/sw-github-graphql.sh +2 -12
- package/scripts/sw-guild.sh +1 -11
- package/scripts/sw-heartbeat.sh +49 -12
- package/scripts/sw-hygiene.sh +45 -43
- package/scripts/sw-incident.sh +284 -67
- package/scripts/sw-init.sh +35 -37
- package/scripts/sw-instrument.sh +1 -11
- package/scripts/sw-intelligence.sh +362 -51
- package/scripts/sw-jira.sh +5 -14
- package/scripts/sw-launchd.sh +2 -12
- package/scripts/sw-linear.sh +8 -17
- package/scripts/sw-logs.sh +4 -12
- package/scripts/sw-loop.sh +641 -90
- package/scripts/sw-memory.sh +243 -17
- package/scripts/sw-mission-control.sh +2 -12
- package/scripts/sw-model-router.sh +73 -34
- package/scripts/sw-otel.sh +11 -21
- package/scripts/sw-oversight.sh +1 -11
- package/scripts/sw-patrol-meta.sh +5 -11
- package/scripts/sw-pipeline-composer.sh +7 -17
- package/scripts/sw-pipeline-vitals.sh +1 -11
- package/scripts/sw-pipeline.sh +478 -122
- package/scripts/sw-pm.sh +2 -12
- package/scripts/sw-pr-lifecycle.sh +203 -29
- package/scripts/sw-predictive.sh +16 -22
- package/scripts/sw-prep.sh +6 -16
- package/scripts/sw-ps.sh +1 -11
- package/scripts/sw-public-dashboard.sh +2 -12
- package/scripts/sw-quality.sh +77 -10
- package/scripts/sw-reaper.sh +1 -11
- package/scripts/sw-recruit.sh +15 -25
- package/scripts/sw-regression.sh +11 -21
- package/scripts/sw-release-manager.sh +19 -28
- package/scripts/sw-release.sh +8 -16
- package/scripts/sw-remote.sh +1 -11
- package/scripts/sw-replay.sh +48 -44
- package/scripts/sw-retro.sh +70 -92
- package/scripts/sw-review-rerun.sh +220 -0
- package/scripts/sw-scale.sh +109 -32
- package/scripts/sw-security-audit.sh +12 -22
- package/scripts/sw-self-optimize.sh +239 -23
- package/scripts/sw-session.sh +3 -13
- package/scripts/sw-setup.sh +8 -18
- package/scripts/sw-standup.sh +5 -15
- package/scripts/sw-status.sh +32 -23
- package/scripts/sw-strategic.sh +129 -13
- package/scripts/sw-stream.sh +1 -11
- package/scripts/sw-swarm.sh +76 -36
- package/scripts/sw-team-stages.sh +10 -20
- package/scripts/sw-templates.sh +4 -14
- package/scripts/sw-testgen.sh +3 -13
- package/scripts/sw-tmux-pipeline.sh +1 -19
- package/scripts/sw-tmux-role-color.sh +0 -10
- package/scripts/sw-tmux-status.sh +3 -11
- package/scripts/sw-tmux.sh +2 -20
- package/scripts/sw-trace.sh +1 -19
- package/scripts/sw-tracker-github.sh +0 -10
- package/scripts/sw-tracker-jira.sh +1 -11
- package/scripts/sw-tracker-linear.sh +1 -11
- package/scripts/sw-tracker.sh +7 -24
- package/scripts/sw-triage.sh +24 -34
- package/scripts/sw-upgrade.sh +5 -23
- package/scripts/sw-ux.sh +1 -19
- package/scripts/sw-webhook.sh +18 -32
- package/scripts/sw-widgets.sh +3 -21
- package/scripts/sw-worktree.sh +11 -27
- package/scripts/update-homebrew-sha.sh +67 -0
- package/templates/pipelines/tdd.json +72 -0
- package/scripts/sw-pipeline.sh.mock +0 -7
package/scripts/sw-loop.sh
CHANGED
|
@@ -23,6 +23,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
23
23
|
# Canonical helpers (colors, output, events)
|
|
24
24
|
# shellcheck source=lib/helpers.sh
|
|
25
25
|
[[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
|
|
26
|
+
[[ -f "$SCRIPT_DIR/lib/config.sh" ]] && source "$SCRIPT_DIR/lib/config.sh"
|
|
26
27
|
# Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
|
|
27
28
|
[[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
|
|
28
29
|
[[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
|
|
@@ -40,15 +41,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
|
40
41
|
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
41
42
|
}
|
|
42
43
|
fi
|
|
43
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
44
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
45
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
46
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
47
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
48
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
49
|
-
DIM="${DIM:-\033[2m}"
|
|
50
|
-
BOLD="${BOLD:-\033[1m}"
|
|
51
|
-
RESET="${RESET:-\033[0m}"
|
|
52
44
|
|
|
53
45
|
# ─── Defaults ─────────────────────────────────────────────────────────────────
|
|
54
46
|
GOAL=""
|
|
@@ -67,11 +59,11 @@ MAX_TURNS=""
|
|
|
67
59
|
RESUME=false
|
|
68
60
|
VERBOSE=false
|
|
69
61
|
MAX_ITERATIONS_EXPLICIT=false
|
|
70
|
-
MAX_RESTARTS
|
|
62
|
+
MAX_RESTARTS=$(_config_get_int "loop.max_restarts" 0 2>/dev/null || echo 0)
|
|
71
63
|
SESSION_RESTART=false
|
|
72
64
|
RESTART_COUNT=0
|
|
73
65
|
REPO_OVERRIDE=""
|
|
74
|
-
VERSION="
|
|
66
|
+
VERSION="3.0.0"
|
|
75
67
|
|
|
76
68
|
# ─── Token Tracking ─────────────────────────────────────────────────────────
|
|
77
69
|
LOOP_INPUT_TOKENS=0
|
|
@@ -335,13 +327,13 @@ if [[ -n "$REPO_OVERRIDE" ]]; then
|
|
|
335
327
|
info "Using repository: $(pwd)"
|
|
336
328
|
fi
|
|
337
329
|
|
|
338
|
-
if ! command -v claude
|
|
330
|
+
if ! command -v claude >/dev/null 2>&1; then
|
|
339
331
|
error "Claude Code CLI not found. Install it first:"
|
|
340
332
|
echo -e " ${DIM}npm install -g @anthropic-ai/claude-code${RESET}"
|
|
341
333
|
exit 1
|
|
342
334
|
fi
|
|
343
335
|
|
|
344
|
-
if ! git rev-parse --is-inside-work-tree
|
|
336
|
+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
345
337
|
error "Not inside a git repository. The loop requires git for progress tracking."
|
|
346
338
|
exit 1
|
|
347
339
|
fi
|
|
@@ -351,15 +343,15 @@ ORIGINAL_GOAL="$GOAL"
|
|
|
351
343
|
|
|
352
344
|
# ─── Timeout Detection ────────────────────────────────────────────────────────
|
|
353
345
|
TIMEOUT_CMD=""
|
|
354
|
-
if command -v timeout
|
|
346
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
355
347
|
TIMEOUT_CMD="timeout"
|
|
356
|
-
elif command -v gtimeout
|
|
348
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
357
349
|
TIMEOUT_CMD="gtimeout"
|
|
358
350
|
fi
|
|
359
|
-
CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT
|
|
351
|
+
CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-$(_config_get_int "loop.claude_timeout" 1800 2>/dev/null || echo 1800)}" # 30 min default
|
|
360
352
|
|
|
361
353
|
if [[ "$AGENTS" -gt 1 ]]; then
|
|
362
|
-
if ! command -v tmux
|
|
354
|
+
if ! command -v tmux >/dev/null 2>&1; then
|
|
363
355
|
error "tmux is required for multi-agent mode."
|
|
364
356
|
echo -e " ${DIM}brew install tmux${RESET} (macOS)"
|
|
365
357
|
exit 1
|
|
@@ -393,7 +385,7 @@ select_adaptive_model() {
|
|
|
393
385
|
fi
|
|
394
386
|
# Read learned model routing
|
|
395
387
|
local _routing_file="${HOME}/.shipwright/optimization/model-routing.json"
|
|
396
|
-
if [[ -f "$_routing_file" ]] && command -v jq
|
|
388
|
+
if [[ -f "$_routing_file" ]] && command -v jq >/dev/null 2>&1; then
|
|
397
389
|
local _routed_model
|
|
398
390
|
_routed_model=$(jq -r --arg r "$role" '.routes[$r].model // ""' "$_routing_file" 2>/dev/null) || true
|
|
399
391
|
if [[ -n "${_routed_model:-}" && "${_routed_model:-}" != "null" ]]; then
|
|
@@ -403,7 +395,7 @@ select_adaptive_model() {
|
|
|
403
395
|
fi
|
|
404
396
|
|
|
405
397
|
# Try intelligence-based recommendation
|
|
406
|
-
if type intelligence_recommend_model
|
|
398
|
+
if type intelligence_recommend_model >/dev/null 2>&1; then
|
|
407
399
|
local rec
|
|
408
400
|
rec=$(intelligence_recommend_model "$role" "${COMPLEXITY:-5}" "${BUDGET:-0}" 2>/dev/null || echo "")
|
|
409
401
|
if [[ -n "$rec" ]]; then
|
|
@@ -422,7 +414,7 @@ select_adaptive_model() {
|
|
|
422
414
|
select_audit_model() {
|
|
423
415
|
local default_model="haiku"
|
|
424
416
|
local opt_file="$HOME/.shipwright/optimization/audit-tuning.json"
|
|
425
|
-
if [[ -f "$opt_file" ]] && command -v jq
|
|
417
|
+
if [[ -f "$opt_file" ]] && command -v jq >/dev/null 2>&1; then
|
|
426
418
|
local success_rate
|
|
427
419
|
success_rate=$(jq -r '.haiku_success_rate // 100' "$opt_file" 2>/dev/null || echo "100")
|
|
428
420
|
if [[ "${success_rate%%.*}" -lt 90 ]]; then
|
|
@@ -442,7 +434,7 @@ accumulate_loop_tokens() {
|
|
|
442
434
|
[[ ! -f "$log_file" ]] && return 0
|
|
443
435
|
|
|
444
436
|
# If jq is available and the file looks like JSON, parse structured output
|
|
445
|
-
if command -v jq
|
|
437
|
+
if command -v jq >/dev/null 2>&1 && head -c1 "$log_file" 2>/dev/null | grep -q '\['; then
|
|
446
438
|
local input_tok output_tok cache_read cache_create cost_usd
|
|
447
439
|
# The result object is the last element in the JSON array
|
|
448
440
|
input_tok=$(jq -r '.[-1].usage.input_tokens // 0' "$log_file" 2>/dev/null || echo "0")
|
|
@@ -458,6 +450,20 @@ accumulate_loop_tokens() {
|
|
|
458
450
|
local cost_millicents
|
|
459
451
|
cost_millicents=$(echo "$cost_usd" | awk '{printf "%.0f", $1 * 100000}' 2>/dev/null || echo "0")
|
|
460
452
|
LOOP_COST_MILLICENTS=$(( ${LOOP_COST_MILLICENTS:-0} + ${cost_millicents:-0} ))
|
|
453
|
+
else
|
|
454
|
+
# Estimate cost from tokens when Claude doesn't provide it (rates per million tokens)
|
|
455
|
+
local total_in total_out
|
|
456
|
+
total_in=$(( ${input_tok:-0} + ${cache_read:-0} + ${cache_create:-0} ))
|
|
457
|
+
total_out=${output_tok:-0}
|
|
458
|
+
local cost=0
|
|
459
|
+
case "${MODEL:-${CLAUDE_MODEL:-sonnet}}" in
|
|
460
|
+
*opus*) cost=$(awk -v i="$total_in" -v o="$total_out" 'BEGIN{printf "%.6f", (i * 15 + o * 75) / 1000000}') ;;
|
|
461
|
+
*sonnet*) cost=$(awk -v i="$total_in" -v o="$total_out" 'BEGIN{printf "%.6f", (i * 3 + o * 15) / 1000000}') ;;
|
|
462
|
+
*haiku*) cost=$(awk -v i="$total_in" -v o="$total_out" 'BEGIN{printf "%.6f", (i * 0.25 + o * 1.25) / 1000000}') ;;
|
|
463
|
+
*) cost=$(awk -v i="$total_in" -v o="$total_out" 'BEGIN{printf "%.6f", (i * 3 + o * 15) / 1000000}') ;;
|
|
464
|
+
esac
|
|
465
|
+
cost_millicents=$(echo "$cost" | awk '{printf "%.0f", $1 * 100000}' 2>/dev/null || echo "0")
|
|
466
|
+
LOOP_COST_MILLICENTS=$(( ${LOOP_COST_MILLICENTS:-0} + ${cost_millicents:-0} ))
|
|
461
467
|
fi
|
|
462
468
|
else
|
|
463
469
|
# Fallback: regex-based parsing for non-JSON output
|
|
@@ -491,7 +497,7 @@ _extract_text_from_json() {
|
|
|
491
497
|
first_char=$(head -c1 "$json_file" 2>/dev/null || true)
|
|
492
498
|
|
|
493
499
|
# Case 2: Valid JSON array — extract .result from last element
|
|
494
|
-
if [[ "$first_char" == "[" ]] && command -v jq
|
|
500
|
+
if [[ "$first_char" == "[" ]] && command -v jq >/dev/null 2>&1; then
|
|
495
501
|
local extracted
|
|
496
502
|
extracted=$(jq -r '.[-1].result // empty' "$json_file" 2>/dev/null) || true
|
|
497
503
|
if [[ -n "$extracted" ]]; then
|
|
@@ -542,7 +548,7 @@ TOKJSON
|
|
|
542
548
|
# Reads tuning config for smarter iteration/circuit-breaker thresholds.
|
|
543
549
|
apply_adaptive_budget() {
|
|
544
550
|
local tuning_file="$HOME/.shipwright/optimization/loop-tuning.json"
|
|
545
|
-
if [[ -f "$tuning_file" ]] && command -v jq
|
|
551
|
+
if [[ -f "$tuning_file" ]] && command -v jq >/dev/null 2>&1; then
|
|
546
552
|
local tuned_max tuned_ext tuned_ext_count tuned_cb
|
|
547
553
|
tuned_max=$(jq -r '.max_iterations // ""' "$tuning_file" 2>/dev/null || echo "")
|
|
548
554
|
tuned_ext=$(jq -r '.extension_size // ""' "$tuning_file" 2>/dev/null || echo "")
|
|
@@ -560,7 +566,7 @@ apply_adaptive_budget() {
|
|
|
560
566
|
|
|
561
567
|
# Read learned iteration model
|
|
562
568
|
local _iter_model="${HOME}/.shipwright/optimization/iteration-model.json"
|
|
563
|
-
if [[ -f "$_iter_model" ]] && ! $MAX_ITERATIONS_EXPLICIT && command -v jq
|
|
569
|
+
if [[ -f "$_iter_model" ]] && ! $MAX_ITERATIONS_EXPLICIT && command -v jq >/dev/null 2>&1; then
|
|
564
570
|
local _complexity="${ISSUE_COMPLEXITY:-${COMPLEXITY:-medium}}"
|
|
565
571
|
local _predicted_max
|
|
566
572
|
_predicted_max=$(jq -r --arg c "$_complexity" '.predictions[$c].max_iterations // ""' "$_iter_model" 2>/dev/null) || true
|
|
@@ -571,7 +577,7 @@ apply_adaptive_budget() {
|
|
|
571
577
|
fi
|
|
572
578
|
|
|
573
579
|
# Try intelligence-based iteration estimate
|
|
574
|
-
if type intelligence_estimate_iterations
|
|
580
|
+
if type intelligence_estimate_iterations >/dev/null 2>&1 && ! $MAX_ITERATIONS_EXPLICIT; then
|
|
575
581
|
local est
|
|
576
582
|
est=$(intelligence_estimate_iterations "${GOAL:-}" "${COMPLEXITY:-5}" 2>/dev/null || echo "")
|
|
577
583
|
if [[ -n "$est" && "$est" =~ ^[0-9]+$ ]]; then
|
|
@@ -619,9 +625,6 @@ compute_velocity_avg() {
|
|
|
619
625
|
|
|
620
626
|
# ─── Timing Helpers ───────────────────────────────────────────────────────────
|
|
621
627
|
|
|
622
|
-
now_iso() { date -u +%Y-%m-%dT%H:%M:%SZ; }
|
|
623
|
-
now_epoch() { date +%s; }
|
|
624
|
-
|
|
625
628
|
format_duration() {
|
|
626
629
|
local secs="$1"
|
|
627
630
|
local mins=$(( secs / 60 ))
|
|
@@ -730,6 +733,21 @@ resume_state() {
|
|
|
730
733
|
exit 0
|
|
731
734
|
fi
|
|
732
735
|
|
|
736
|
+
# Restore Claude context for meaningful resume (source so exports persist to this shell)
|
|
737
|
+
if [[ -f "$SCRIPT_DIR/sw-checkpoint.sh" ]] && [[ -d "${PROJECT_ROOT:-}" ]]; then
|
|
738
|
+
source "$SCRIPT_DIR/sw-checkpoint.sh"
|
|
739
|
+
local _orig_pwd="$PWD"
|
|
740
|
+
cd "$PROJECT_ROOT" 2>/dev/null || true
|
|
741
|
+
if checkpoint_restore_context "build" 2>/dev/null; then
|
|
742
|
+
RESUMED_FROM_ITERATION="${RESTORED_ITERATION:-}"
|
|
743
|
+
RESUMED_MODIFIED="${RESTORED_MODIFIED:-}"
|
|
744
|
+
RESUMED_FINDINGS="${RESTORED_FINDINGS:-}"
|
|
745
|
+
RESUMED_TEST_OUTPUT="${RESTORED_TEST_OUTPUT:-}"
|
|
746
|
+
[[ -n "${RESTORED_ITERATION:-}" && "${RESTORED_ITERATION:-0}" -gt 0 ]] && info "Restored context from iteration ${RESTORED_ITERATION}"
|
|
747
|
+
fi
|
|
748
|
+
cd "$_orig_pwd" 2>/dev/null || true
|
|
749
|
+
fi
|
|
750
|
+
|
|
733
751
|
success "Resumed: iteration $ITERATION/$MAX_ITERATIONS"
|
|
734
752
|
}
|
|
735
753
|
|
|
@@ -807,6 +825,85 @@ ${entry}"
|
|
|
807
825
|
fi
|
|
808
826
|
}
|
|
809
827
|
|
|
828
|
+
# ─── Semantic Validation for Claude Output ─────────────────────────────────────
|
|
829
|
+
# Validates changed files before commit to catch syntax errors and API error leakage.
|
|
830
|
+
validate_claude_output() {
|
|
831
|
+
local workdir="${1:-.}"
|
|
832
|
+
local issues=0
|
|
833
|
+
|
|
834
|
+
# Check for syntax errors in changed files
|
|
835
|
+
local changed_files
|
|
836
|
+
changed_files=$(git -C "$workdir" diff --cached --name-only 2>/dev/null || git -C "$workdir" diff --name-only 2>/dev/null)
|
|
837
|
+
|
|
838
|
+
while IFS= read -r file; do
|
|
839
|
+
[[ -z "$file" ]] && continue
|
|
840
|
+
[[ ! -f "$workdir/$file" ]] && continue
|
|
841
|
+
|
|
842
|
+
case "$file" in
|
|
843
|
+
*.sh)
|
|
844
|
+
if ! bash -n "$workdir/$file" 2>/dev/null; then
|
|
845
|
+
warn "Syntax error in shell script: $file"
|
|
846
|
+
issues=$((issues + 1))
|
|
847
|
+
fi
|
|
848
|
+
;;
|
|
849
|
+
*.py)
|
|
850
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
851
|
+
if ! python3 -c "import ast, sys; ast.parse(open(sys.argv[1]).read())" "$workdir/$file" 2>/dev/null; then
|
|
852
|
+
warn "Syntax error in Python file: $file"
|
|
853
|
+
issues=$((issues + 1))
|
|
854
|
+
fi
|
|
855
|
+
fi
|
|
856
|
+
;;
|
|
857
|
+
*.json)
|
|
858
|
+
if command -v jq >/dev/null 2>&1 && ! jq empty "$workdir/$file" 2>/dev/null; then
|
|
859
|
+
warn "Invalid JSON: $file"
|
|
860
|
+
issues=$((issues + 1))
|
|
861
|
+
fi
|
|
862
|
+
;;
|
|
863
|
+
*.ts|*.js|*.tsx|*.jsx)
|
|
864
|
+
# Check for obvious corruption: API error text leaked into source
|
|
865
|
+
if grep -qE '(CLAUDE_CODE_OAUTH_TOKEN|api key|rate limit|503 Service|DOCTYPE html)' "$workdir/$file" 2>/dev/null; then
|
|
866
|
+
warn "Claude API error leaked into source file: $file"
|
|
867
|
+
issues=$((issues + 1))
|
|
868
|
+
fi
|
|
869
|
+
;;
|
|
870
|
+
esac
|
|
871
|
+
done <<< "$changed_files"
|
|
872
|
+
|
|
873
|
+
# Check for obviously corrupt output (API errors dumped as code)
|
|
874
|
+
local total_changed
|
|
875
|
+
total_changed=$(echo "$changed_files" | grep -c '.' 2>/dev/null || echo "0")
|
|
876
|
+
if [[ "$total_changed" -eq 0 ]]; then
|
|
877
|
+
warn "Claude iteration produced no file changes"
|
|
878
|
+
issues=$((issues + 1))
|
|
879
|
+
fi
|
|
880
|
+
|
|
881
|
+
return "$issues"
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
# ─── Budget Gate (hard stop when exhausted) ───────────────────────────────────
|
|
885
|
+
check_budget_gate() {
|
|
886
|
+
[[ ! -x "$SCRIPT_DIR/sw-cost.sh" ]] && return 0
|
|
887
|
+
local remaining
|
|
888
|
+
remaining=$(bash "$SCRIPT_DIR/sw-cost.sh" remaining-budget 2>/dev/null || echo "")
|
|
889
|
+
[[ -z "$remaining" ]] && return 0
|
|
890
|
+
[[ "$remaining" == "unlimited" ]] && return 0
|
|
891
|
+
|
|
892
|
+
# Parse remaining as float, check if <= 0
|
|
893
|
+
if awk -v r="$remaining" 'BEGIN { exit !(r <= 0) }' 2>/dev/null; then
|
|
894
|
+
error "Budget exhausted (remaining: \$${remaining}) — stopping pipeline"
|
|
895
|
+
emit_event "pipeline.budget_exhausted" "remaining=$remaining"
|
|
896
|
+
return 1
|
|
897
|
+
fi
|
|
898
|
+
|
|
899
|
+
# Warn at 10% threshold (remaining < 1.0 when typical job ~$5+)
|
|
900
|
+
if awk -v r="$remaining" 'BEGIN { exit !(r < 1.0) }' 2>/dev/null; then
|
|
901
|
+
warn "Budget low: \$${remaining} remaining"
|
|
902
|
+
fi
|
|
903
|
+
|
|
904
|
+
return 0
|
|
905
|
+
}
|
|
906
|
+
|
|
810
907
|
# ─── Git Helpers ──────────────────────────────────────────────────────────────
|
|
811
908
|
|
|
812
909
|
git_commit_count() {
|
|
@@ -834,6 +931,14 @@ git_auto_commit() {
|
|
|
834
931
|
fi
|
|
835
932
|
|
|
836
933
|
git -C "$work_dir" add -A 2>/dev/null || true
|
|
934
|
+
|
|
935
|
+
# Semantic validation before commit — skip commit if validation fails
|
|
936
|
+
if ! validate_claude_output "$work_dir"; then
|
|
937
|
+
warn "Validation failed — skipping commit for this iteration"
|
|
938
|
+
git -C "$work_dir" reset --hard HEAD 2>/dev/null || true
|
|
939
|
+
return 1
|
|
940
|
+
fi
|
|
941
|
+
|
|
837
942
|
git -C "$work_dir" commit -m "loop: iteration $ITERATION — autonomous progress" --no-verify 2>/dev/null || return 1
|
|
838
943
|
return 0
|
|
839
944
|
}
|
|
@@ -897,7 +1002,7 @@ check_completion() {
|
|
|
897
1002
|
|
|
898
1003
|
check_circuit_breaker() {
|
|
899
1004
|
# Vitals-driven circuit breaker (preferred over static threshold)
|
|
900
|
-
if type pipeline_compute_vitals
|
|
1005
|
+
if type pipeline_compute_vitals >/dev/null 2>&1 && type pipeline_health_verdict >/dev/null 2>&1; then
|
|
901
1006
|
local _vitals_json _verdict
|
|
902
1007
|
local _loop_state="${STATE_FILE:-}"
|
|
903
1008
|
local _loop_artifacts="${ARTIFACTS_DIR:-}"
|
|
@@ -989,6 +1094,113 @@ check_max_iterations() {
|
|
|
989
1094
|
return 1
|
|
990
1095
|
}
|
|
991
1096
|
|
|
1097
|
+
# ─── Failure Diagnosis ─────────────────────────────────────────────────────────
|
|
1098
|
+
# Pattern-based root-cause classification for smarter retries (no Claude needed).
|
|
1099
|
+
# Returns markdown context to inject into the next iteration's goal.
|
|
1100
|
+
|
|
1101
|
+
diagnose_failure() {
|
|
1102
|
+
local error_output="$1"
|
|
1103
|
+
local changed_files="$2"
|
|
1104
|
+
local iteration="$3"
|
|
1105
|
+
|
|
1106
|
+
local diagnosis=""
|
|
1107
|
+
local strategy="retry_with_context" # default
|
|
1108
|
+
|
|
1109
|
+
# Pattern-based classification (fast, no Claude needed)
|
|
1110
|
+
if echo "$error_output" | grep -qiE 'import.*not found|cannot find module|no module named'; then
|
|
1111
|
+
diagnosis="missing_import"
|
|
1112
|
+
strategy="fix_imports"
|
|
1113
|
+
elif echo "$error_output" | grep -qiE 'syntax error|unexpected token|parse error'; then
|
|
1114
|
+
diagnosis="syntax_error"
|
|
1115
|
+
strategy="fix_syntax"
|
|
1116
|
+
elif echo "$error_output" | grep -qiE 'type.*not assignable|type error|TypeError'; then
|
|
1117
|
+
diagnosis="type_error"
|
|
1118
|
+
strategy="fix_types"
|
|
1119
|
+
elif echo "$error_output" | grep -qiE 'undefined.*variable|not defined|ReferenceError'; then
|
|
1120
|
+
diagnosis="undefined_reference"
|
|
1121
|
+
strategy="fix_references"
|
|
1122
|
+
elif echo "$error_output" | grep -qiE 'timeout|timed out|ETIMEDOUT'; then
|
|
1123
|
+
diagnosis="timeout"
|
|
1124
|
+
strategy="optimize_performance"
|
|
1125
|
+
elif echo "$error_output" | grep -qiE 'assertion.*fail|expect.*to|AssertionError'; then
|
|
1126
|
+
diagnosis="test_assertion"
|
|
1127
|
+
strategy="fix_logic"
|
|
1128
|
+
elif echo "$error_output" | grep -qiE 'permission denied|EACCES|forbidden'; then
|
|
1129
|
+
diagnosis="permission_error"
|
|
1130
|
+
strategy="fix_permissions"
|
|
1131
|
+
elif echo "$error_output" | grep -qiE 'out of memory|heap|OOM|ENOMEM'; then
|
|
1132
|
+
diagnosis="resource_error"
|
|
1133
|
+
strategy="reduce_resource_usage"
|
|
1134
|
+
else
|
|
1135
|
+
diagnosis="unknown"
|
|
1136
|
+
strategy="retry_with_context"
|
|
1137
|
+
fi
|
|
1138
|
+
|
|
1139
|
+
# Check if we've seen this diagnosis before in this session
|
|
1140
|
+
local diagnosis_file="${LOG_DIR:-/tmp}/diagnoses.txt"
|
|
1141
|
+
local repeat_count=0
|
|
1142
|
+
if [[ -f "$diagnosis_file" ]]; then
|
|
1143
|
+
repeat_count=$(grep -c "^${diagnosis}$" "$diagnosis_file" 2>/dev/null || echo "0")
|
|
1144
|
+
fi
|
|
1145
|
+
echo "$diagnosis" >> "$diagnosis_file"
|
|
1146
|
+
|
|
1147
|
+
# Escalate strategy if same diagnosis repeats
|
|
1148
|
+
if [[ "$repeat_count" -ge 2 ]]; then
|
|
1149
|
+
strategy="alternative_approach"
|
|
1150
|
+
fi
|
|
1151
|
+
|
|
1152
|
+
# Try memory-based fix lookup
|
|
1153
|
+
local known_fix=""
|
|
1154
|
+
if type memory_query_fix_for_error &>/dev/null; then
|
|
1155
|
+
local fix_json
|
|
1156
|
+
fix_json=$(memory_query_fix_for_error "$error_output" 2>/dev/null || true)
|
|
1157
|
+
if [[ -n "$fix_json" && "$fix_json" != "null" ]]; then
|
|
1158
|
+
known_fix=$(echo "$fix_json" | jq -r '.fix // ""' 2>/dev/null | head -5)
|
|
1159
|
+
fi
|
|
1160
|
+
fi
|
|
1161
|
+
|
|
1162
|
+
# Build diagnosis context for Claude
|
|
1163
|
+
local diagnosis_context="## Failure Diagnosis (Iteration $iteration)
|
|
1164
|
+
Classification: $diagnosis
|
|
1165
|
+
Strategy: $strategy
|
|
1166
|
+
Repeat count: $repeat_count"
|
|
1167
|
+
|
|
1168
|
+
if [[ -n "$known_fix" ]]; then
|
|
1169
|
+
diagnosis_context+="
|
|
1170
|
+
Known fix from memory: $known_fix"
|
|
1171
|
+
fi
|
|
1172
|
+
|
|
1173
|
+
# Strategy-specific guidance
|
|
1174
|
+
case "$strategy" in
|
|
1175
|
+
fix_imports)
|
|
1176
|
+
diagnosis_context+="
|
|
1177
|
+
INSTRUCTION: The error is about missing imports/modules. Check that all imports are correct, packages are installed, and paths are right. Do NOT change the logic - just fix the imports."
|
|
1178
|
+
;;
|
|
1179
|
+
fix_syntax)
|
|
1180
|
+
diagnosis_context+="
|
|
1181
|
+
INSTRUCTION: This is a syntax error. Carefully check the exact line mentioned in the error. Look for missing brackets, semicolons, commas, or mismatched quotes."
|
|
1182
|
+
;;
|
|
1183
|
+
fix_types)
|
|
1184
|
+
diagnosis_context+="
|
|
1185
|
+
INSTRUCTION: Type mismatch error. Check the types at the error location. Ensure function signatures match their usage."
|
|
1186
|
+
;;
|
|
1187
|
+
fix_logic)
|
|
1188
|
+
diagnosis_context+="
|
|
1189
|
+
INSTRUCTION: Test assertion failure. The code logic is wrong, not the syntax. Re-read the test expectations and fix the implementation to match."
|
|
1190
|
+
;;
|
|
1191
|
+
alternative_approach)
|
|
1192
|
+
diagnosis_context+="
|
|
1193
|
+
INSTRUCTION: This error has occurred $repeat_count times. The previous approach is not working. Try a FUNDAMENTALLY DIFFERENT approach:
|
|
1194
|
+
- If you were modifying existing code, try rewriting the function from scratch
|
|
1195
|
+
- If you were using one library, try a different one
|
|
1196
|
+
- If you were adding to a file, try creating a new file instead
|
|
1197
|
+
- Step back and reconsider the requirements"
|
|
1198
|
+
;;
|
|
1199
|
+
esac
|
|
1200
|
+
|
|
1201
|
+
echo "$diagnosis_context"
|
|
1202
|
+
}
|
|
1203
|
+
|
|
992
1204
|
# ─── Test Gate ────────────────────────────────────────────────────────────────
|
|
993
1205
|
|
|
994
1206
|
run_test_gate() {
|
|
@@ -1018,9 +1230,9 @@ run_test_gate() {
|
|
|
1018
1230
|
# Wrap test command with timeout (5 min default) to prevent hanging
|
|
1019
1231
|
local test_timeout="${SW_TEST_TIMEOUT:-300}"
|
|
1020
1232
|
local test_wrapper="$active_test_cmd"
|
|
1021
|
-
if command -v timeout
|
|
1233
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
1022
1234
|
test_wrapper="timeout ${test_timeout} bash -c $(printf '%q' "$active_test_cmd")"
|
|
1023
|
-
elif command -v gtimeout
|
|
1235
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
1024
1236
|
test_wrapper="gtimeout ${test_timeout} bash -c $(printf '%q' "$active_test_cmd")"
|
|
1025
1237
|
fi
|
|
1026
1238
|
if bash -c "$test_wrapper" > "$test_log" 2>&1; then
|
|
@@ -1072,7 +1284,7 @@ write_error_summary() {
|
|
|
1072
1284
|
local tmp_json="${error_json}.tmp.$$"
|
|
1073
1285
|
|
|
1074
1286
|
# Build JSON with jq (preferred) or plain-text fallback
|
|
1075
|
-
if command -v jq
|
|
1287
|
+
if command -v jq >/dev/null 2>&1; then
|
|
1076
1288
|
jq -n \
|
|
1077
1289
|
--argjson iteration "${ITERATION:-0}" \
|
|
1078
1290
|
--arg timestamp "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
|
@@ -1298,6 +1510,79 @@ guard_completion() {
|
|
|
1298
1510
|
return 0
|
|
1299
1511
|
}
|
|
1300
1512
|
|
|
1513
|
+
# ─── Context Window Management ───────────────────────────────────────────────
|
|
1514
|
+
# Prevents prompt from exceeding Claude's context limit (~200K tokens).
|
|
1515
|
+
# Trims least-critical sections first when over budget.
|
|
1516
|
+
|
|
1517
|
+
CONTEXT_BUDGET_CHARS="${CONTEXT_BUDGET_CHARS:-180000}" # ~45K tokens at 4 chars/token
|
|
1518
|
+
|
|
1519
|
+
manage_context_window() {
|
|
1520
|
+
local prompt="$1"
|
|
1521
|
+
local budget="${CONTEXT_BUDGET_CHARS}"
|
|
1522
|
+
local current_len=${#prompt}
|
|
1523
|
+
|
|
1524
|
+
if [[ "$current_len" -le "$budget" ]]; then
|
|
1525
|
+
echo "$prompt"
|
|
1526
|
+
return
|
|
1527
|
+
fi
|
|
1528
|
+
|
|
1529
|
+
# Over budget — progressively trim sections (least important first)
|
|
1530
|
+
local trimmed="$prompt"
|
|
1531
|
+
|
|
1532
|
+
# 1. Trim DORA/Performance baselines (least critical for code generation)
|
|
1533
|
+
if [[ "${#trimmed}" -gt "$budget" ]]; then
|
|
1534
|
+
trimmed=$(echo "$trimmed" | awk '/^## Performance Baselines/{skip=1; next} skip && /^## [^#]/{skip=0} !skip{print}')
|
|
1535
|
+
fi
|
|
1536
|
+
|
|
1537
|
+
# 2. Trim file hotspots to top 5
|
|
1538
|
+
if [[ "${#trimmed}" -gt "$budget" ]]; then
|
|
1539
|
+
trimmed=$(echo "$trimmed" | awk '/## File Hotspots/{p=1; c=0} p && /^- /{c++; if(c>5) next} {print}')
|
|
1540
|
+
fi
|
|
1541
|
+
|
|
1542
|
+
# 3. Trim git log to last 10 entries
|
|
1543
|
+
if [[ "${#trimmed}" -gt "$budget" ]]; then
|
|
1544
|
+
trimmed=$(echo "$trimmed" | awk '/## Recent Git Activity/{p=1; c=0} p && /^[a-f0-9]/{c++; if(c>10) next} {print}')
|
|
1545
|
+
fi
|
|
1546
|
+
|
|
1547
|
+
# 4. Truncate memory context to first 20K chars
|
|
1548
|
+
if [[ "${#trimmed}" -gt "$budget" ]]; then
|
|
1549
|
+
trimmed=$(echo "$trimmed" | awk -v max=20000 '
|
|
1550
|
+
/## Memory Context/{mem=1; skip_rest=0; chars=0; print; next}
|
|
1551
|
+
mem && /^## [^#]/{mem=0; print; next}
|
|
1552
|
+
mem{chars+=length($0)+1; if(chars>max){print "... (memory truncated for context budget)"; skip_rest=1; mem=0; next}}
|
|
1553
|
+
skip_rest && /^## [^#]/{skip_rest=0; print; next}
|
|
1554
|
+
skip_rest{next}
|
|
1555
|
+
{print}
|
|
1556
|
+
')
|
|
1557
|
+
fi
|
|
1558
|
+
|
|
1559
|
+
# 5. Truncate test output to last 50 lines
|
|
1560
|
+
if [[ "${#trimmed}" -gt "$budget" ]]; then
|
|
1561
|
+
trimmed=$(echo "$trimmed" | awk '
|
|
1562
|
+
/## Test Results/{found=1; buf=""; print; next}
|
|
1563
|
+
found && /^## [^#]/{found=0; n=split(buf,arr,"\n"); start=(n>50)?(n-49):1; for(i=start;i<=n;i++) if(arr[i]!="") print arr[i]; print; next}
|
|
1564
|
+
found{buf=buf $0 "\n"; next}
|
|
1565
|
+
{print}
|
|
1566
|
+
')
|
|
1567
|
+
fi
|
|
1568
|
+
|
|
1569
|
+
# 6. Last resort: hard truncate with notice
|
|
1570
|
+
if [[ "${#trimmed}" -gt "$budget" ]]; then
|
|
1571
|
+
trimmed="${trimmed:0:$budget}
|
|
1572
|
+
|
|
1573
|
+
... [CONTEXT TRUNCATED: prompt exceeded ${budget} char budget. Focus on the goal and most recent errors.]"
|
|
1574
|
+
fi
|
|
1575
|
+
|
|
1576
|
+
# Log the trimming
|
|
1577
|
+
local final_len=${#trimmed}
|
|
1578
|
+
if [[ "$final_len" -lt "$current_len" ]]; then
|
|
1579
|
+
warn "Context trimmed from ${current_len} to ${final_len} chars (budget: ${budget})"
|
|
1580
|
+
emit_event "loop.context_trimmed" "original=$current_len" "trimmed=$final_len" "budget=$budget" 2>/dev/null || true
|
|
1581
|
+
fi
|
|
1582
|
+
|
|
1583
|
+
echo "$trimmed"
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1301
1586
|
# ─── Prompt Composition ──────────────────────────────────────────────────────
|
|
1302
1587
|
|
|
1303
1588
|
compose_prompt() {
|
|
@@ -1348,7 +1633,7 @@ Fix these specific errors. Each line above is one distinct error from the test o
|
|
|
1348
1633
|
|
|
1349
1634
|
# Memory context injection (failure patterns + past learnings)
|
|
1350
1635
|
local memory_section=""
|
|
1351
|
-
if type memory_inject_context
|
|
1636
|
+
if type memory_inject_context >/dev/null 2>&1; then
|
|
1352
1637
|
memory_section="$(memory_inject_context "build" 2>/dev/null || true)"
|
|
1353
1638
|
elif [[ -f "$SCRIPT_DIR/sw-memory.sh" ]]; then
|
|
1354
1639
|
memory_section="$("$SCRIPT_DIR/sw-memory.sh" inject build 2>/dev/null || true)"
|
|
@@ -1356,7 +1641,7 @@ Fix these specific errors. Each line above is one distinct error from the test o
|
|
|
1356
1641
|
|
|
1357
1642
|
# DORA baselines for context
|
|
1358
1643
|
local dora_section=""
|
|
1359
|
-
if type memory_get_dora_baseline
|
|
1644
|
+
if type memory_get_dora_baseline >/dev/null 2>&1; then
|
|
1360
1645
|
local dora_json
|
|
1361
1646
|
dora_json="$(memory_get_dora_baseline 7 2>/dev/null || echo "{}")"
|
|
1362
1647
|
local dora_total
|
|
@@ -1385,7 +1670,7 @@ $(cat "$memory_refresh_file")"
|
|
|
1385
1670
|
local intelligence_section=""
|
|
1386
1671
|
if [[ "${NO_GITHUB:-}" != "true" ]]; then
|
|
1387
1672
|
# File hotspots — top 5 most-changed files
|
|
1388
|
-
if type gh_file_change_frequency
|
|
1673
|
+
if type gh_file_change_frequency >/dev/null 2>&1; then
|
|
1389
1674
|
local hotspots
|
|
1390
1675
|
hotspots=$(gh_file_change_frequency 2>/dev/null | head -5 || true)
|
|
1391
1676
|
if [[ -n "$hotspots" ]]; then
|
|
@@ -1396,7 +1681,7 @@ ${hotspots}"
|
|
|
1396
1681
|
fi
|
|
1397
1682
|
|
|
1398
1683
|
# CODEOWNERS context
|
|
1399
|
-
if type gh_codeowners
|
|
1684
|
+
if type gh_codeowners >/dev/null 2>&1; then
|
|
1400
1685
|
local owners
|
|
1401
1686
|
owners=$(gh_codeowners 2>/dev/null | head -10 || true)
|
|
1402
1687
|
if [[ -n "$owners" ]]; then
|
|
@@ -1407,7 +1692,7 @@ ${owners}"
|
|
|
1407
1692
|
fi
|
|
1408
1693
|
|
|
1409
1694
|
# Active security alerts
|
|
1410
|
-
if type gh_security_alerts
|
|
1695
|
+
if type gh_security_alerts >/dev/null 2>&1; then
|
|
1411
1696
|
local alerts
|
|
1412
1697
|
alerts=$(gh_security_alerts 2>/dev/null | head -5 || true)
|
|
1413
1698
|
if [[ -n "$alerts" ]]; then
|
|
@@ -1459,6 +1744,34 @@ ${last_error}"
|
|
|
1459
1744
|
# Stuckness detection — compare last 3 iteration outputs
|
|
1460
1745
|
local stuckness_section=""
|
|
1461
1746
|
stuckness_section="$(detect_stuckness)"
|
|
1747
|
+
local _stuck_ret=$?
|
|
1748
|
+
local stuckness_detected=false
|
|
1749
|
+
[[ "$_stuck_ret" -eq 0 ]] && stuckness_detected=true
|
|
1750
|
+
|
|
1751
|
+
# Strategy exploration when stuck — append alternative strategy to GOAL
|
|
1752
|
+
if [[ "$stuckness_detected" == "true" ]]; then
|
|
1753
|
+
local last_error diagnosis
|
|
1754
|
+
last_error=$(tail -1 "${ARTIFACTS_DIR:-${PROJECT_ROOT:-.}/.claude/pipeline-artifacts}/error-log.jsonl" 2>/dev/null | jq -r '"Type: \(.type), Exit: \(.exit_code), Error: \(.error | split("\n") | first)"' 2>/dev/null || true)
|
|
1755
|
+
[[ -z "$last_error" || "$last_error" == "null" ]] && last_error="unknown"
|
|
1756
|
+
diagnosis="${STUCKNESS_DIAGNOSIS:-}"
|
|
1757
|
+
local alt_strategy
|
|
1758
|
+
alt_strategy=$(explore_alternative_strategy "$last_error" "${ITERATION:-0}" "$diagnosis")
|
|
1759
|
+
GOAL="${GOAL}
|
|
1760
|
+
|
|
1761
|
+
${alt_strategy}"
|
|
1762
|
+
|
|
1763
|
+
# Handle model escalation
|
|
1764
|
+
if [[ "${ESCALATE_MODEL:-}" == "true" ]]; then
|
|
1765
|
+
if [[ -f "$SCRIPT_DIR/sw-model-router.sh" ]]; then
|
|
1766
|
+
source "$SCRIPT_DIR/sw-model-router.sh" 2>/dev/null || true
|
|
1767
|
+
fi
|
|
1768
|
+
if type escalate_model &>/dev/null; then
|
|
1769
|
+
MODEL=$(escalate_model "${MODEL:-sonnet}")
|
|
1770
|
+
info "Escalated to model: $MODEL"
|
|
1771
|
+
fi
|
|
1772
|
+
unset ESCALATE_MODEL
|
|
1773
|
+
fi
|
|
1774
|
+
fi
|
|
1462
1775
|
|
|
1463
1776
|
# Session restart context — inject previous session progress
|
|
1464
1777
|
local restart_section=""
|
|
@@ -1470,9 +1783,36 @@ You are starting a FRESH session after the previous one exhausted its iterations
|
|
|
1470
1783
|
Read the progress above and continue from where it left off. Do NOT repeat work already done."
|
|
1471
1784
|
fi
|
|
1472
1785
|
|
|
1786
|
+
# Resume-from-checkpoint context — reconstruct Claude context for meaningful resume
|
|
1787
|
+
local resume_section=""
|
|
1788
|
+
if [[ -n "${RESUMED_FROM_ITERATION:-}" && "${RESUMED_FROM_ITERATION:-0}" -gt 0 ]]; then
|
|
1789
|
+
local _test_tail=" (none recorded)"
|
|
1790
|
+
[[ -n "${RESUMED_TEST_OUTPUT:-}" ]] && _test_tail="$(echo "$RESUMED_TEST_OUTPUT" | tail -20)"
|
|
1791
|
+
resume_section="## RESUMING FROM ITERATION ${RESUMED_FROM_ITERATION}
|
|
1792
|
+
|
|
1793
|
+
Continue from where you left off. Do NOT repeat work already done.
|
|
1794
|
+
|
|
1795
|
+
Previous work modified these files:
|
|
1796
|
+
${RESUMED_MODIFIED:- (none recorded)}
|
|
1797
|
+
|
|
1798
|
+
Previous findings/errors from earlier iterations:
|
|
1799
|
+
${RESUMED_FINDINGS:- (none recorded)}
|
|
1800
|
+
|
|
1801
|
+
Last test output (fix any failures, tail):
|
|
1802
|
+
${_test_tail}
|
|
1803
|
+
|
|
1804
|
+
---
|
|
1805
|
+
"
|
|
1806
|
+
# Clear after first use so we don't keep injecting on every iteration
|
|
1807
|
+
RESUMED_FROM_ITERATION=""
|
|
1808
|
+
RESUMED_MODIFIED=""
|
|
1809
|
+
RESUMED_FINDINGS=""
|
|
1810
|
+
RESUMED_TEST_OUTPUT=""
|
|
1811
|
+
fi
|
|
1812
|
+
|
|
1473
1813
|
cat <<PROMPT
|
|
1474
1814
|
You are an autonomous coding agent on iteration ${ITERATION}/${MAX_ITERATIONS} of a continuous loop.
|
|
1475
|
-
|
|
1815
|
+
${resume_section}
|
|
1476
1816
|
## Your Goal
|
|
1477
1817
|
${GOAL}
|
|
1478
1818
|
|
|
@@ -1522,55 +1862,163 @@ PROMPT
|
|
|
1522
1862
|
}
|
|
1523
1863
|
|
|
1524
1864
|
# ─── Stuckness Detection ─────────────────────────────────────────────────────
|
|
1525
|
-
#
|
|
1865
|
+
# Multi-signal detection: text overlap, git diff hash, error repetition, exit code pattern, iteration budget.
|
|
1866
|
+
# Returns 0 when stuck, 1 when not. Outputs stuckness section and sets STUCKNESS_HINT when stuck.
|
|
1867
|
+
# When stuck: increments STUCKNESS_COUNT, emits event; if STUCKNESS_COUNT >= 3, caller triggers session restart.
|
|
1868
|
+
STUCKNESS_COUNT=0
|
|
1869
|
+
STUCKNESS_TRACKING_FILE=""
|
|
1870
|
+
|
|
1871
|
+
record_iteration_stuckness_data() {
|
|
1872
|
+
local exit_code="${1:-0}"
|
|
1873
|
+
[[ -z "$LOG_DIR" ]] && return 0
|
|
1874
|
+
local tracking_file="${STUCKNESS_TRACKING_FILE:-$LOG_DIR/stuckness-tracking.txt}"
|
|
1875
|
+
local diff_hash error_hash
|
|
1876
|
+
diff_hash=$(git -C "${PROJECT_ROOT:-.}" diff HEAD 2>/dev/null | (md5 -q 2>/dev/null || md5sum 2>/dev/null | cut -d' ' -f1) || echo "none")
|
|
1877
|
+
local error_log="${ARTIFACTS_DIR:-${STATE_DIR:-${PROJECT_ROOT:-.}/.claude}/pipeline-artifacts}/error-log.jsonl"
|
|
1878
|
+
if [[ -f "$error_log" ]]; then
|
|
1879
|
+
error_hash=$(tail -5 "$error_log" 2>/dev/null | sort -u | (md5 -q 2>/dev/null || md5sum 2>/dev/null | cut -d' ' -f1) || echo "none")
|
|
1880
|
+
else
|
|
1881
|
+
error_hash="none"
|
|
1882
|
+
fi
|
|
1883
|
+
echo "${diff_hash}|${error_hash}|${exit_code}" >> "$tracking_file"
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1526
1886
|
detect_stuckness() {
|
|
1527
|
-
|
|
1528
|
-
|
|
1887
|
+
STUCKNESS_HINT=""
|
|
1888
|
+
local iteration="${ITERATION:-0}"
|
|
1889
|
+
local stuckness_signals=0
|
|
1890
|
+
local stuckness_reasons=()
|
|
1891
|
+
local tracking_file="${STUCKNESS_TRACKING_FILE:-$LOG_DIR/stuckness-tracking.txt}"
|
|
1892
|
+
local tracking_lines
|
|
1893
|
+
tracking_lines=$(wc -l < "$tracking_file" 2>/dev/null || echo "0")
|
|
1894
|
+
|
|
1895
|
+
# Signal 1: Text overlap (existing logic) — compare last 2 iteration logs
|
|
1896
|
+
if [[ "$iteration" -ge 3 ]]; then
|
|
1897
|
+
local log1="$LOG_DIR/iteration-$(( iteration - 1 )).log"
|
|
1898
|
+
local log2="$LOG_DIR/iteration-$(( iteration - 2 )).log"
|
|
1899
|
+
local log3="$LOG_DIR/iteration-$(( iteration - 3 )).log"
|
|
1900
|
+
|
|
1901
|
+
if [[ -f "$log1" && -f "$log2" ]]; then
|
|
1902
|
+
local lines1 lines2 common total overlap_pct
|
|
1903
|
+
lines1=$(tail -50 "$log1" 2>/dev/null | grep -v '^$' | sort || true)
|
|
1904
|
+
lines2=$(tail -50 "$log2" 2>/dev/null | grep -v '^$' | sort || true)
|
|
1905
|
+
|
|
1906
|
+
if [[ -n "$lines1" && -n "$lines2" ]]; then
|
|
1907
|
+
total=$(echo "$lines1" | wc -l | tr -d ' ')
|
|
1908
|
+
common=$(comm -12 <(echo "$lines1") <(echo "$lines2") 2>/dev/null | wc -l | tr -d ' ' || echo "0")
|
|
1909
|
+
if [[ "$total" -gt 0 ]]; then
|
|
1910
|
+
overlap_pct=$(( common * 100 / total ))
|
|
1911
|
+
else
|
|
1912
|
+
overlap_pct=0
|
|
1913
|
+
fi
|
|
1914
|
+
if [[ "${overlap_pct:-0}" -ge 90 ]]; then
|
|
1915
|
+
stuckness_signals=$((stuckness_signals + 1))
|
|
1916
|
+
stuckness_reasons+=("high text overlap (${overlap_pct}%) between iterations")
|
|
1917
|
+
fi
|
|
1918
|
+
fi
|
|
1919
|
+
fi
|
|
1529
1920
|
fi
|
|
1530
1921
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1922
|
+
# Signal 2: Git diff hash — last 3 iterations produced zero or identical diffs
|
|
1923
|
+
if [[ -f "$tracking_file" ]] && [[ "$tracking_lines" -ge 3 ]]; then
|
|
1924
|
+
local last_three
|
|
1925
|
+
last_three=$(tail -3 "$tracking_file" 2>/dev/null | cut -d'|' -f1 || true)
|
|
1926
|
+
local unique_hashes
|
|
1927
|
+
unique_hashes=$(echo "$last_three" | sort -u | grep -v '^$' | wc -l | tr -d ' ')
|
|
1928
|
+
if [[ "$unique_hashes" -le 1 ]] && [[ -n "$last_three" ]]; then
|
|
1929
|
+
stuckness_signals=$((stuckness_signals + 1))
|
|
1930
|
+
stuckness_reasons+=("identical or zero git diffs in last 3 iterations")
|
|
1931
|
+
fi
|
|
1932
|
+
fi
|
|
1534
1933
|
|
|
1535
|
-
#
|
|
1536
|
-
if [[
|
|
1537
|
-
|
|
1934
|
+
# Signal 3: Error repetition — same error hash in last 3 iterations
|
|
1935
|
+
if [[ -f "$tracking_file" ]] && [[ "$tracking_lines" -ge 3 ]]; then
|
|
1936
|
+
local last_three_errors
|
|
1937
|
+
last_three_errors=$(tail -3 "$tracking_file" 2>/dev/null | cut -d'|' -f2 || true)
|
|
1938
|
+
local unique_error_hashes
|
|
1939
|
+
unique_error_hashes=$(echo "$last_three_errors" | sort -u | grep -v '^none$' | grep -v '^$' | wc -l | tr -d ' ')
|
|
1940
|
+
if [[ "$unique_error_hashes" -eq 1 ]] && [[ -n "$(echo "$last_three_errors" | grep -v '^none$')" ]]; then
|
|
1941
|
+
stuckness_signals=$((stuckness_signals + 1))
|
|
1942
|
+
stuckness_reasons+=("same error in last 3 iterations")
|
|
1943
|
+
fi
|
|
1538
1944
|
fi
|
|
1539
1945
|
|
|
1540
|
-
#
|
|
1541
|
-
local
|
|
1542
|
-
|
|
1543
|
-
|
|
1946
|
+
# Signal 4: Same error repeating 3+ times (legacy check on error-log content)
|
|
1947
|
+
local error_log
|
|
1948
|
+
error_log="${ARTIFACTS_DIR:-$PROJECT_ROOT/.claude/pipeline-artifacts}/error-log.jsonl"
|
|
1949
|
+
if [[ -f "$error_log" ]]; then
|
|
1950
|
+
local last_errors
|
|
1951
|
+
last_errors=$(tail -5 "$error_log" 2>/dev/null | jq -r '.error // .message // .error_hash // empty' 2>/dev/null | sort | uniq -c | sort -rn | head -1 || true)
|
|
1952
|
+
local repeat_count
|
|
1953
|
+
repeat_count=$(echo "$last_errors" | awk '{print $1}' 2>/dev/null || echo "0")
|
|
1954
|
+
if [[ "${repeat_count:-0}" -ge 3 ]]; then
|
|
1955
|
+
stuckness_signals=$((stuckness_signals + 1))
|
|
1956
|
+
stuckness_reasons+=("same error repeated ${repeat_count} times")
|
|
1957
|
+
fi
|
|
1958
|
+
fi
|
|
1544
1959
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1960
|
+
# Signal 5: Exit code pattern — last 3 iterations had same non-zero exit code
|
|
1961
|
+
if [[ -f "$tracking_file" ]] && [[ "$tracking_lines" -ge 3 ]]; then
|
|
1962
|
+
local last_three_exits
|
|
1963
|
+
last_three_exits=$(tail -3 "$tracking_file" 2>/dev/null | cut -d'|' -f3 || true)
|
|
1964
|
+
local first_exit
|
|
1965
|
+
first_exit=$(echo "$last_three_exits" | head -1)
|
|
1966
|
+
if [[ "$first_exit" =~ ^[0-9]+$ ]] && [[ "$first_exit" -ne 0 ]]; then
|
|
1967
|
+
local all_same=true
|
|
1968
|
+
while IFS= read -r ex; do
|
|
1969
|
+
[[ "$ex" != "$first_exit" ]] && all_same=false
|
|
1970
|
+
done <<< "$last_three_exits"
|
|
1971
|
+
if [[ "$all_same" == true ]]; then
|
|
1972
|
+
stuckness_signals=$((stuckness_signals + 1))
|
|
1973
|
+
stuckness_reasons+=("same non-zero exit code (${first_exit}) in last 3 iterations")
|
|
1974
|
+
fi
|
|
1975
|
+
fi
|
|
1547
1976
|
fi
|
|
1548
1977
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1978
|
+
# Signal 6: Git diff size — no or minimal code changes (existing)
|
|
1979
|
+
local diff_lines
|
|
1980
|
+
diff_lines=$(git -C "${PROJECT_ROOT:-.}" diff HEAD 2>/dev/null | wc -l | tr -d ' ' || echo "0")
|
|
1981
|
+
if [[ "${diff_lines:-0}" -lt 5 ]] && [[ "$iteration" -gt 2 ]]; then
|
|
1982
|
+
stuckness_signals=$((stuckness_signals + 1))
|
|
1983
|
+
stuckness_reasons+=("no code changes in last iteration")
|
|
1984
|
+
fi
|
|
1551
1985
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1986
|
+
# Signal 7: Iteration budget — used >70% without passing tests
|
|
1987
|
+
local max_iter="${MAX_ITERATIONS:-20}"
|
|
1988
|
+
local progress_pct=0
|
|
1989
|
+
if [[ "$max_iter" -gt 0 ]]; then
|
|
1990
|
+
progress_pct=$(( iteration * 100 / max_iter ))
|
|
1556
1991
|
fi
|
|
1992
|
+
if [[ "$progress_pct" -gt 70 ]] && [[ "${TEST_PASSED:-false}" != "true" ]]; then
|
|
1993
|
+
stuckness_signals=$((stuckness_signals + 1))
|
|
1994
|
+
stuckness_reasons+=("used ${progress_pct}% of iteration budget without passing tests")
|
|
1995
|
+
fi
|
|
1996
|
+
|
|
1997
|
+
# Decision: 2+ signals = stuck
|
|
1998
|
+
if [[ "$stuckness_signals" -ge 2 ]]; then
|
|
1999
|
+
STUCKNESS_COUNT=$(( STUCKNESS_COUNT + 1 ))
|
|
2000
|
+
STUCKNESS_DIAGNOSIS="${stuckness_reasons[*]}"
|
|
2001
|
+
if type emit_event >/dev/null 2>&1; then
|
|
2002
|
+
emit_event "loop.stuckness_detected" "signals=$stuckness_signals" "count=$STUCKNESS_COUNT" "iteration=$iteration" "reasons=${stuckness_reasons[*]}"
|
|
2003
|
+
fi
|
|
2004
|
+
STUCKNESS_HINT="IMPORTANT: The loop appears stuck. Previous approaches have not worked. You MUST try a fundamentally different strategy. Reasons: ${stuckness_reasons[*]}"
|
|
2005
|
+
warn "Stuckness detected (${stuckness_signals} signals, count ${STUCKNESS_COUNT}): ${stuckness_reasons[*]}"
|
|
1557
2006
|
|
|
1558
|
-
if [[ "$overlap_pct" -ge 90 ]]; then
|
|
1559
2007
|
local diff_summary=""
|
|
1560
|
-
|
|
2008
|
+
local log1="$LOG_DIR/iteration-$(( iteration - 1 )).log"
|
|
2009
|
+
local log3="$LOG_DIR/iteration-$(( iteration - 3 )).log"
|
|
2010
|
+
if [[ -f "$log3" && -f "$log1" ]]; then
|
|
1561
2011
|
diff_summary=$(diff <(tail -30 "$log3" 2>/dev/null) <(tail -30 "$log1" 2>/dev/null) 2>/dev/null | head -10 || true)
|
|
1562
2012
|
fi
|
|
1563
2013
|
|
|
1564
|
-
# Gather memory-based alternative approaches
|
|
1565
2014
|
local alternatives=""
|
|
1566
|
-
if type memory_inject_context
|
|
2015
|
+
if type memory_inject_context >/dev/null 2>&1; then
|
|
1567
2016
|
alternatives=$(memory_inject_context "build" 2>/dev/null | grep -i "fix:" | head -3 || true)
|
|
1568
2017
|
fi
|
|
1569
2018
|
|
|
1570
2019
|
cat <<STUCK_SECTION
|
|
1571
2020
|
## Stuckness Detected
|
|
1572
|
-
|
|
1573
|
-
You appear to be stuck on the same approach.
|
|
2021
|
+
${STUCKNESS_HINT}
|
|
1574
2022
|
|
|
1575
2023
|
${diff_summary:+Changes between recent iterations:
|
|
1576
2024
|
$diff_summary
|
|
@@ -1584,7 +2032,10 @@ Try a fundamentally different approach:
|
|
|
1584
2032
|
- Check if there's a dependency or configuration issue blocking progress
|
|
1585
2033
|
- Read error messages more carefully — the root cause may differ from your assumption
|
|
1586
2034
|
STUCK_SECTION
|
|
2035
|
+
return 0
|
|
1587
2036
|
fi
|
|
2037
|
+
|
|
2038
|
+
return 1
|
|
1588
2039
|
}
|
|
1589
2040
|
|
|
1590
2041
|
compose_audit_section() {
|
|
@@ -1675,7 +2126,7 @@ compose_worker_prompt() {
|
|
|
1675
2126
|
local role_desc=""
|
|
1676
2127
|
# Try to pull description from recruit's roles DB first
|
|
1677
2128
|
local recruit_roles_db="${HOME}/.shipwright/recruitment/roles.json"
|
|
1678
|
-
if [[ -f "$recruit_roles_db" ]] && command -v jq
|
|
2129
|
+
if [[ -f "$recruit_roles_db" ]] && command -v jq >/dev/null 2>&1; then
|
|
1679
2130
|
local recruit_desc
|
|
1680
2131
|
recruit_desc=$(jq -r --arg r "$role" '.[$r].description // ""' "$recruit_roles_db" 2>/dev/null) || true
|
|
1681
2132
|
if [[ -n "$recruit_desc" && "$recruit_desc" != "null" ]]; then
|
|
@@ -1735,6 +2186,12 @@ run_claude_iteration() {
|
|
|
1735
2186
|
local json_file="$LOG_DIR/iteration-${ITERATION}.json"
|
|
1736
2187
|
local prompt
|
|
1737
2188
|
prompt="$(compose_prompt)"
|
|
2189
|
+
local final_prompt
|
|
2190
|
+
final_prompt=$(manage_context_window "$prompt")
|
|
2191
|
+
|
|
2192
|
+
local prompt_chars=${#final_prompt}
|
|
2193
|
+
local approx_tokens=$((prompt_chars / 4))
|
|
2194
|
+
info "Prompt: ~${approx_tokens} tokens (${prompt_chars} chars)"
|
|
1738
2195
|
|
|
1739
2196
|
local flags
|
|
1740
2197
|
flags="$(build_claude_flags)"
|
|
@@ -1750,9 +2207,9 @@ run_claude_iteration() {
|
|
|
1750
2207
|
# shellcheck disable=SC2086
|
|
1751
2208
|
local err_file="${json_file%.json}.stderr"
|
|
1752
2209
|
if [[ -n "$TIMEOUT_CMD" ]]; then
|
|
1753
|
-
$TIMEOUT_CMD "$CLAUDE_TIMEOUT" claude -p "$
|
|
2210
|
+
$TIMEOUT_CMD "$CLAUDE_TIMEOUT" claude -p "$final_prompt" $flags > "$json_file" 2>"$err_file" &
|
|
1754
2211
|
else
|
|
1755
|
-
claude -p "$
|
|
2212
|
+
claude -p "$final_prompt" $flags > "$json_file" 2>"$err_file" &
|
|
1756
2213
|
fi
|
|
1757
2214
|
CHILD_PID=$!
|
|
1758
2215
|
wait "$CHILD_PID" 2>/dev/null || exit_code=$?
|
|
@@ -1835,12 +2292,13 @@ show_summary() {
|
|
|
1835
2292
|
|
|
1836
2293
|
local status_display
|
|
1837
2294
|
case "$STATUS" in
|
|
1838
|
-
complete)
|
|
1839
|
-
circuit_breaker)
|
|
1840
|
-
max_iterations)
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2295
|
+
complete) status_display="${GREEN}✓ Complete (LOOP_COMPLETE detected)${RESET}" ;;
|
|
2296
|
+
circuit_breaker) status_display="${RED}✗ Circuit breaker tripped${RESET}" ;;
|
|
2297
|
+
max_iterations) status_display="${YELLOW}⚠ Max iterations reached${RESET}" ;;
|
|
2298
|
+
budget_exhausted) status_display="${RED}✗ Budget exhausted${RESET}" ;;
|
|
2299
|
+
interrupted) status_display="${YELLOW}⚠ Interrupted by user${RESET}" ;;
|
|
2300
|
+
error) status_display="${RED}✗ Error${RESET}" ;;
|
|
2301
|
+
*) status_display="${DIM}$STATUS${RESET}" ;;
|
|
1844
2302
|
esac
|
|
1845
2303
|
|
|
1846
2304
|
local test_display
|
|
@@ -1909,6 +2367,15 @@ cleanup() {
|
|
|
1909
2367
|
--iteration "$ITERATION" \
|
|
1910
2368
|
--git-sha "$(git rev-parse HEAD 2>/dev/null || echo unknown)" 2>/dev/null || true
|
|
1911
2369
|
|
|
2370
|
+
# Save Claude context for meaningful resume (goal, findings, test output)
|
|
2371
|
+
export SW_LOOP_GOAL="$GOAL"
|
|
2372
|
+
export SW_LOOP_ITERATION="$ITERATION"
|
|
2373
|
+
export SW_LOOP_STATUS="$STATUS"
|
|
2374
|
+
export SW_LOOP_TEST_OUTPUT="${TEST_OUTPUT:-}"
|
|
2375
|
+
export SW_LOOP_FINDINGS="${LOG_ENTRIES:-}"
|
|
2376
|
+
export SW_LOOP_MODIFIED="$(git diff --name-only HEAD 2>/dev/null | head -50 | tr '\n' ',' | sed 's/,$//')"
|
|
2377
|
+
"$SCRIPT_DIR/sw-checkpoint.sh" save-context --stage build 2>/dev/null || true
|
|
2378
|
+
|
|
1912
2379
|
# Clear heartbeat
|
|
1913
2380
|
"$SCRIPT_DIR/sw-heartbeat.sh" clear "${PIPELINE_JOB_ID:-loop-$$}" 2>/dev/null || true
|
|
1914
2381
|
|
|
@@ -1934,7 +2401,7 @@ setup_worktrees() {
|
|
|
1934
2401
|
fi
|
|
1935
2402
|
|
|
1936
2403
|
# Create branch if it doesn't exist
|
|
1937
|
-
if ! git -C "$PROJECT_ROOT" rev-parse --verify "$branch_name"
|
|
2404
|
+
if ! git -C "$PROJECT_ROOT" rev-parse --verify "$branch_name" >/dev/null 2>&1; then
|
|
1938
2405
|
git -C "$PROJECT_ROOT" branch "$branch_name" HEAD 2>/dev/null || true
|
|
1939
2406
|
fi
|
|
1940
2407
|
|
|
@@ -1996,6 +2463,17 @@ CONSECUTIVE_FAILURES=0
|
|
|
1996
2463
|
echo -e "${CYAN}${BOLD}▸${RESET} Agent ${AGENT_NUM}/${TOTAL_AGENTS} starting in ${WORK_DIR}"
|
|
1997
2464
|
|
|
1998
2465
|
while [[ "$ITERATION" -lt "$MAX_ITERATIONS" ]]; do
|
|
2466
|
+
# Budget gate: stop if daily budget exhausted
|
|
2467
|
+
if [[ -x "$SCRIPT_DIR/sw-cost.sh" ]]; then
|
|
2468
|
+
budget_remaining=$("$SCRIPT_DIR/sw-cost.sh" remaining-budget 2>/dev/null || echo "")
|
|
2469
|
+
if [[ -n "$budget_remaining" && "$budget_remaining" != "unlimited" ]]; then
|
|
2470
|
+
if awk -v r="$budget_remaining" 'BEGIN { exit !(r <= 0) }' 2>/dev/null; then
|
|
2471
|
+
echo -e " ${RED}✗${RESET} Budget exhausted (\$${budget_remaining}) — stopping agent ${AGENT_NUM}"
|
|
2472
|
+
break
|
|
2473
|
+
fi
|
|
2474
|
+
fi
|
|
2475
|
+
fi
|
|
2476
|
+
|
|
1999
2477
|
ITERATION=$(( ITERATION + 1 ))
|
|
2000
2478
|
echo -e "\n${CYAN}${BOLD}▸${RESET} Agent ${AGENT_NUM} — Iteration ${ITERATION}/${MAX_ITERATIONS}"
|
|
2001
2479
|
|
|
@@ -2064,8 +2542,12 @@ PROMPT
|
|
|
2064
2542
|
# Auto-commit
|
|
2065
2543
|
git add -A 2>/dev/null || true
|
|
2066
2544
|
if git commit -m "agent-${AGENT_NUM}: iteration ${ITERATION}" --no-verify 2>/dev/null; then
|
|
2067
|
-
git push origin "loop/agent-${AGENT_NUM}" 2>/dev/null
|
|
2068
|
-
|
|
2545
|
+
if ! git push origin "loop/agent-${AGENT_NUM}" 2>/dev/null; then
|
|
2546
|
+
echo -e " ${YELLOW}⚠${RESET} git push failed for loop/agent-${AGENT_NUM} — remote may be out of sync"
|
|
2547
|
+
type emit_event >/dev/null 2>&1 && emit_event "loop.push_failed" "branch=loop/agent-${AGENT_NUM}"
|
|
2548
|
+
else
|
|
2549
|
+
echo -e " ${GREEN}✓${RESET} Committed and pushed"
|
|
2550
|
+
fi
|
|
2069
2551
|
fi
|
|
2070
2552
|
|
|
2071
2553
|
# Circuit breaker: check for progress
|
|
@@ -2083,7 +2565,7 @@ PROMPT
|
|
|
2083
2565
|
break
|
|
2084
2566
|
fi
|
|
2085
2567
|
|
|
2086
|
-
sleep
|
|
2568
|
+
sleep __SLEEP_BETWEEN_ITERATIONS__
|
|
2087
2569
|
done
|
|
2088
2570
|
|
|
2089
2571
|
echo -e "\n${DIM}Agent ${AGENT_NUM} finished after ${ITERATION} iterations${RESET}"
|
|
@@ -2094,11 +2576,14 @@ WORKEREOF
|
|
|
2094
2576
|
sed_i "s|__AGENT_NUM__|${agent_num}|g" "$worker_script"
|
|
2095
2577
|
sed_i "s|__TOTAL_AGENTS__|${total_agents}|g" "$worker_script"
|
|
2096
2578
|
sed_i "s|__MAX_ITERATIONS__|${MAX_ITERATIONS}|g" "$worker_script"
|
|
2579
|
+
sed_i "s|__SLEEP_BETWEEN_ITERATIONS__|$(_config_get_int "loop.sleep_between_iterations" 2 2>/dev/null || echo 2)|g" "$worker_script"
|
|
2097
2580
|
# Paths and commands may contain sed-special chars — use awk
|
|
2098
2581
|
awk -v val="$wt_path" '{gsub(/__WORK_DIR__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
|
|
2099
2582
|
&& mv "${worker_script}.tmp" "$worker_script"
|
|
2100
2583
|
awk -v val="$LOG_DIR" '{gsub(/__LOG_DIR__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
|
|
2101
2584
|
&& mv "${worker_script}.tmp" "$worker_script"
|
|
2585
|
+
awk -v val="$SCRIPT_DIR" '{gsub(/__SCRIPT_DIR__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
|
|
2586
|
+
&& mv "${worker_script}.tmp" "$worker_script"
|
|
2102
2587
|
awk -v val="$TEST_CMD" '{gsub(/__TEST_CMD__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
|
|
2103
2588
|
&& mv "${worker_script}.tmp" "$worker_script"
|
|
2104
2589
|
awk -v val="$claude_flags" '{gsub(/__CLAUDE_FLAGS__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
|
|
@@ -2137,11 +2622,12 @@ launch_multi_agent() {
|
|
|
2137
2622
|
local worker_script
|
|
2138
2623
|
worker_script="$(generate_worker_script "$i" "$AGENTS")"
|
|
2139
2624
|
|
|
2140
|
-
|
|
2625
|
+
local worker_pane_id
|
|
2626
|
+
worker_pane_id="$(tmux split-window -t "$MULTI_WINDOW_NAME" -c "$PROJECT_ROOT" -P -F '#{pane_id}')"
|
|
2141
2627
|
sleep 0.1
|
|
2142
|
-
tmux send-keys -t "$
|
|
2628
|
+
tmux send-keys -t "$worker_pane_id" "printf '\\033]2;agent-${i}\\033\\\\'" Enter
|
|
2143
2629
|
sleep 0.1
|
|
2144
|
-
tmux send-keys -t "$
|
|
2630
|
+
tmux send-keys -t "$worker_pane_id" "bash '$worker_script'" Enter
|
|
2145
2631
|
done
|
|
2146
2632
|
|
|
2147
2633
|
# Layout: monitor pane on top (35%), worker agents tile below
|
|
@@ -2181,7 +2667,7 @@ wait_for_multi_completion() {
|
|
|
2181
2667
|
latest_log="$(ls -t "$LOG_DIR"/agent-"${i}"-iter-*.log 2>/dev/null | head -1)"
|
|
2182
2668
|
if [[ -n "$latest_log" ]]; then
|
|
2183
2669
|
local age
|
|
2184
|
-
age=$(( $(now_epoch) - $(
|
|
2670
|
+
age=$(( $(now_epoch) - $(file_mtime "$latest_log") ))
|
|
2185
2671
|
if [[ $age -lt 300 ]]; then # Active within 5 minutes
|
|
2186
2672
|
running=$(( running + 1 ))
|
|
2187
2673
|
fi
|
|
@@ -2200,7 +2686,7 @@ wait_for_multi_completion() {
|
|
|
2200
2686
|
fi
|
|
2201
2687
|
fi
|
|
2202
2688
|
|
|
2203
|
-
sleep 5
|
|
2689
|
+
sleep "$(_config_get_int "loop.multi_agent_sleep" 5 2>/dev/null || echo 5)"
|
|
2204
2690
|
done
|
|
2205
2691
|
}
|
|
2206
2692
|
|
|
@@ -2239,6 +2725,10 @@ run_single_agent_loop() {
|
|
|
2239
2725
|
|
|
2240
2726
|
# Track applied memory fix patterns for outcome recording
|
|
2241
2727
|
_applied_fix_pattern=""
|
|
2728
|
+
STUCKNESS_COUNT=0
|
|
2729
|
+
STUCKNESS_TRACKING_FILE="$LOG_DIR/stuckness-tracking.txt"
|
|
2730
|
+
: > "$STUCKNESS_TRACKING_FILE" 2>/dev/null || true
|
|
2731
|
+
: > "${LOG_DIR:-/tmp}/strategy-attempts.txt" 2>/dev/null || true
|
|
2242
2732
|
|
|
2243
2733
|
show_banner
|
|
2244
2734
|
|
|
@@ -2246,17 +2736,48 @@ run_single_agent_loop() {
|
|
|
2246
2736
|
# Pre-checks (before incrementing — ITERATION tracks completed count)
|
|
2247
2737
|
check_circuit_breaker || break
|
|
2248
2738
|
check_max_iterations || break
|
|
2739
|
+
check_budget_gate || {
|
|
2740
|
+
STATUS="budget_exhausted"
|
|
2741
|
+
write_state
|
|
2742
|
+
write_progress
|
|
2743
|
+
error "Budget exhausted — stopping pipeline"
|
|
2744
|
+
show_summary
|
|
2745
|
+
return 1
|
|
2746
|
+
}
|
|
2249
2747
|
ITERATION=$(( ITERATION + 1 ))
|
|
2250
2748
|
|
|
2251
|
-
#
|
|
2749
|
+
# Root-cause diagnosis and memory-based fix on retry after test failure
|
|
2252
2750
|
if [[ "${TEST_PASSED:-}" == "false" ]]; then
|
|
2751
|
+
# Source memory module for diagnosis and fix lookup
|
|
2752
|
+
[[ -f "$SCRIPT_DIR/sw-memory.sh" ]] && source "$SCRIPT_DIR/sw-memory.sh" 2>/dev/null || true
|
|
2753
|
+
|
|
2754
|
+
# Capture failure for memory (enables memory_analyze_failure and future fix lookup)
|
|
2755
|
+
if type memory_capture_failure &>/dev/null && [[ -n "${TEST_OUTPUT:-}" ]]; then
|
|
2756
|
+
memory_capture_failure "test" "$TEST_OUTPUT" 2>/dev/null || true
|
|
2757
|
+
fi
|
|
2758
|
+
|
|
2759
|
+
# Pattern-based diagnosis (no Claude needed) — inject into goal for smarter retry
|
|
2760
|
+
local _changed_files=""
|
|
2761
|
+
_changed_files=$(git diff --name-only HEAD 2>/dev/null | head -50 | tr '\n' ',' | sed 's/,$//')
|
|
2762
|
+
local _diagnosis
|
|
2763
|
+
_diagnosis=$(diagnose_failure "${TEST_OUTPUT:-}" "$_changed_files" "$ITERATION" 2>/dev/null || true)
|
|
2764
|
+
|
|
2765
|
+
if [[ -n "$_diagnosis" ]]; then
|
|
2766
|
+
GOAL="${GOAL}
|
|
2767
|
+
|
|
2768
|
+
${_diagnosis}"
|
|
2769
|
+
info "Failure diagnosis injected (classification from error pattern)"
|
|
2770
|
+
fi
|
|
2771
|
+
|
|
2772
|
+
# Memory-based fix suggestion (from past successful fixes)
|
|
2253
2773
|
local _last_error=""
|
|
2254
2774
|
local _prev_log="$LOG_DIR/iteration-$(( ITERATION - 1 )).log"
|
|
2255
2775
|
if [[ -f "$_prev_log" ]]; then
|
|
2256
2776
|
_last_error=$(tail -20 "$_prev_log" 2>/dev/null | grep -iE '(error|fail|exception)' | head -1 || true)
|
|
2257
2777
|
fi
|
|
2778
|
+
[[ -z "$_last_error" ]] && _last_error=$(echo "${TEST_OUTPUT:-}" | head -3 | tr '\n' ' ')
|
|
2258
2779
|
local _fix_suggestion=""
|
|
2259
|
-
if type memory_closed_loop_inject
|
|
2780
|
+
if type memory_closed_loop_inject >/dev/null 2>&1 && [[ -n "${_last_error:-}" ]]; then
|
|
2260
2781
|
_fix_suggestion=$(memory_closed_loop_inject "$_last_error" 2>/dev/null) || true
|
|
2261
2782
|
fi
|
|
2262
2783
|
if [[ -n "${_fix_suggestion:-}" ]]; then
|
|
@@ -2266,6 +2787,14 @@ run_single_agent_loop() {
|
|
|
2266
2787
|
${GOAL}"
|
|
2267
2788
|
info "Memory fix injected: ${_fix_suggestion:0:80}"
|
|
2268
2789
|
fi
|
|
2790
|
+
|
|
2791
|
+
# Analyze failure via Claude (background, non-blocking) for richer root_cause/fix in memory
|
|
2792
|
+
if type memory_analyze_failure &>/dev/null && [[ "${INTELLIGENCE_ENABLED:-auto}" != "false" ]]; then
|
|
2793
|
+
local _test_log="${TEST_LOG_FILE:-$LOG_DIR/tests-iter-$(( ITERATION - 1 )).log}"
|
|
2794
|
+
if [[ -f "$_test_log" ]]; then
|
|
2795
|
+
memory_analyze_failure "$_test_log" "test" 2>/dev/null &
|
|
2796
|
+
fi
|
|
2797
|
+
fi
|
|
2269
2798
|
fi
|
|
2270
2799
|
|
|
2271
2800
|
# Run Claude
|
|
@@ -2274,6 +2803,9 @@ ${GOAL}"
|
|
|
2274
2803
|
|
|
2275
2804
|
local log_file="$LOG_DIR/iteration-${ITERATION}.log"
|
|
2276
2805
|
|
|
2806
|
+
# Record iteration data for stuckness detection (diff hash, error hash, exit code)
|
|
2807
|
+
record_iteration_stuckness_data "$exit_code"
|
|
2808
|
+
|
|
2277
2809
|
# Detect fatal CLI errors (API key, auth, network) — abort immediately
|
|
2278
2810
|
if check_fatal_error "$log_file" "$exit_code"; then
|
|
2279
2811
|
STATUS="error"
|
|
@@ -2285,7 +2817,7 @@ ${GOAL}"
|
|
|
2285
2817
|
fi
|
|
2286
2818
|
|
|
2287
2819
|
# Mid-loop memory refresh — re-query with current error context after iteration 3
|
|
2288
|
-
if [[ "$ITERATION" -ge 3 ]] && type memory_inject_context
|
|
2820
|
+
if [[ "$ITERATION" -ge 3 ]] && type memory_inject_context >/dev/null 2>&1; then
|
|
2289
2821
|
local refresh_ctx
|
|
2290
2822
|
refresh_ctx=$(tail -20 "$log_file" 2>/dev/null || true)
|
|
2291
2823
|
if [[ -n "$refresh_ctx" ]]; then
|
|
@@ -2331,7 +2863,7 @@ ${GOAL}"
|
|
|
2331
2863
|
|
|
2332
2864
|
# Track fix outcome for memory effectiveness
|
|
2333
2865
|
if [[ -n "${_applied_fix_pattern:-}" ]]; then
|
|
2334
|
-
if type memory_record_fix_outcome
|
|
2866
|
+
if type memory_record_fix_outcome >/dev/null 2>&1; then
|
|
2335
2867
|
if [[ "${TEST_PASSED:-}" == "true" ]]; then
|
|
2336
2868
|
memory_record_fix_outcome "$_applied_fix_pattern" "true" "true" 2>/dev/null || true
|
|
2337
2869
|
else
|
|
@@ -2341,6 +2873,15 @@ ${GOAL}"
|
|
|
2341
2873
|
_applied_fix_pattern=""
|
|
2342
2874
|
fi
|
|
2343
2875
|
|
|
2876
|
+
# Save Claude context for checkpoint resume (goal, findings, test output)
|
|
2877
|
+
export SW_LOOP_GOAL="$GOAL"
|
|
2878
|
+
export SW_LOOP_ITERATION="$ITERATION"
|
|
2879
|
+
export SW_LOOP_STATUS="${STATUS:-running}"
|
|
2880
|
+
export SW_LOOP_TEST_OUTPUT="${TEST_OUTPUT:-}"
|
|
2881
|
+
export SW_LOOP_FINDINGS="${LOG_ENTRIES:-}"
|
|
2882
|
+
export SW_LOOP_MODIFIED="$(git diff --name-only HEAD 2>/dev/null | head -50 | tr '\n' ',' | sed 's/,$//')"
|
|
2883
|
+
"$SCRIPT_DIR/sw-checkpoint.sh" save-context --stage build 2>/dev/null || true
|
|
2884
|
+
|
|
2344
2885
|
# Audit agent (reviews implementer's work)
|
|
2345
2886
|
run_audit_agent
|
|
2346
2887
|
|
|
@@ -2396,7 +2937,16 @@ HUMAN FEEDBACK (received after iteration $ITERATION): $human_msg"
|
|
|
2396
2937
|
fi
|
|
2397
2938
|
fi
|
|
2398
2939
|
|
|
2399
|
-
|
|
2940
|
+
# Stuckness-triggered restart: if detected 3+ times, break to allow session restart
|
|
2941
|
+
if [[ "${STUCKNESS_COUNT:-0}" -ge 3 ]]; then
|
|
2942
|
+
STATUS="stuck_restart"
|
|
2943
|
+
write_state
|
|
2944
|
+
write_progress
|
|
2945
|
+
warn "Stuckness detected 3+ times — triggering session restart"
|
|
2946
|
+
break
|
|
2947
|
+
fi
|
|
2948
|
+
|
|
2949
|
+
sleep "$(_config_get_int "loop.sleep_between_iterations" 2 2>/dev/null || echo 2)"
|
|
2400
2950
|
done
|
|
2401
2951
|
|
|
2402
2952
|
# Write final state after loop exits
|
|
@@ -2437,7 +2987,7 @@ run_loop_with_restarts() {
|
|
|
2437
2987
|
fi
|
|
2438
2988
|
|
|
2439
2989
|
RESTART_COUNT=$(( RESTART_COUNT + 1 ))
|
|
2440
|
-
if type emit_event
|
|
2990
|
+
if type emit_event >/dev/null 2>&1; then
|
|
2441
2991
|
emit_event "loop.restart" "restart=$RESTART_COUNT" "max=$MAX_RESTARTS" "iteration=$ITERATION"
|
|
2442
2992
|
fi
|
|
2443
2993
|
info "Session restart ${RESTART_COUNT}/${MAX_RESTARTS} — resetting iteration counter"
|
|
@@ -2448,6 +2998,7 @@ run_loop_with_restarts() {
|
|
|
2448
2998
|
ITERATION=0
|
|
2449
2999
|
CONSECUTIVE_FAILURES=0
|
|
2450
3000
|
EXTENSION_COUNT=0
|
|
3001
|
+
STUCKNESS_COUNT=0
|
|
2451
3002
|
STATUS="running"
|
|
2452
3003
|
LOG_ENTRIES=""
|
|
2453
3004
|
TEST_PASSED=""
|
|
@@ -2469,7 +3020,7 @@ run_loop_with_restarts() {
|
|
|
2469
3020
|
|
|
2470
3021
|
write_state
|
|
2471
3022
|
|
|
2472
|
-
sleep 2
|
|
3023
|
+
sleep "$(_config_get_int "loop.sleep_between_iterations" 2 2>/dev/null || echo 2)"
|
|
2473
3024
|
done
|
|
2474
3025
|
}
|
|
2475
3026
|
|