shipwright-cli 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/post-tool-use.sh +12 -5
- package/README.md +114 -36
- package/completions/_shipwright +212 -32
- package/completions/shipwright.bash +97 -25
- package/docs/strategy/01-market-research.md +619 -0
- package/docs/strategy/02-mission-and-brand.md +587 -0
- package/docs/strategy/03-gtm-and-roadmap.md +759 -0
- package/docs/strategy/QUICK-START.txt +289 -0
- package/docs/strategy/README.md +172 -0
- package/package.json +4 -2
- package/scripts/sw +217 -2
- package/scripts/sw-activity.sh +500 -0
- package/scripts/sw-adaptive.sh +925 -0
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +613 -0
- package/scripts/sw-autonomous.sh +664 -0
- package/scripts/sw-changelog.sh +704 -0
- package/scripts/sw-checkpoint.sh +79 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +192 -7
- package/scripts/sw-code-review.sh +637 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +812 -138
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +540 -0
- package/scripts/sw-decompose.sh +539 -0
- package/scripts/sw-deps.sh +551 -0
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +412 -0
- package/scripts/sw-docs-agent.sh +539 -0
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +59 -1
- package/scripts/sw-dora.sh +615 -0
- package/scripts/sw-durable.sh +710 -0
- package/scripts/sw-e2e-orchestrator.sh +535 -0
- package/scripts/sw-eventbus.sh +393 -0
- package/scripts/sw-feedback.sh +471 -0
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +567 -0
- package/scripts/sw-fleet-viz.sh +404 -0
- package/scripts/sw-fleet.sh +8 -1
- package/scripts/sw-github-app.sh +596 -0
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +569 -0
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +559 -0
- package/scripts/sw-incident.sh +617 -0
- package/scripts/sw-init.sh +88 -1
- package/scripts/sw-instrument.sh +699 -0
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +366 -31
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +507 -51
- package/scripts/sw-memory.sh +198 -3
- package/scripts/sw-mission-control.sh +487 -0
- package/scripts/sw-model-router.sh +545 -0
- package/scripts/sw-otel.sh +596 -0
- package/scripts/sw-oversight.sh +689 -0
- package/scripts/sw-pipeline-composer.sh +8 -8
- package/scripts/sw-pipeline-vitals.sh +1096 -0
- package/scripts/sw-pipeline.sh +2451 -180
- package/scripts/sw-pm.sh +693 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +1 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +4 -3
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +5 -3
- package/scripts/sw-recruit.sh +573 -0
- package/scripts/sw-regression.sh +642 -0
- package/scripts/sw-release-manager.sh +736 -0
- package/scripts/sw-release.sh +706 -0
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +520 -0
- package/scripts/sw-retro.sh +691 -0
- package/scripts/sw-scale.sh +444 -0
- package/scripts/sw-security-audit.sh +505 -0
- package/scripts/sw-self-optimize.sh +109 -8
- package/scripts/sw-session.sh +31 -9
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +192 -1
- package/scripts/sw-strategic.sh +658 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +583 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +515 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +485 -0
- package/scripts/sw-tracker-github.sh +188 -0
- package/scripts/sw-tracker-jira.sh +172 -0
- package/scripts/sw-tracker-linear.sh +251 -0
- package/scripts/sw-tracker.sh +117 -2
- package/scripts/sw-triage.sh +603 -0
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +677 -0
- package/scripts/sw-webhook.sh +627 -0
- package/scripts/sw-widgets.sh +530 -0
- package/scripts/sw-worktree.sh +1 -1
- package/templates/pipelines/autonomous.json +8 -1
- 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 +16 -2
- package/templates/pipelines/hotfix.json +19 -0
- package/templates/pipelines/standard.json +19 -0
package/scripts/sw-loop.sh
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
set -euo pipefail
|
|
11
11
|
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
12
12
|
|
|
13
|
+
# Allow spawning Claude CLI from within a Claude Code session (daemon, fleet, etc.)
|
|
14
|
+
unset CLAUDECODE 2>/dev/null || true
|
|
15
|
+
# Ignore SIGHUP so tmux attach/detach doesn't kill long-running agent sessions
|
|
16
|
+
trap '' HUP
|
|
17
|
+
|
|
13
18
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
19
|
|
|
15
20
|
# ─── Colors (matches shipwright theme) ──────────────────────────────────────────────
|
|
@@ -34,17 +39,25 @@ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
|
34
39
|
|
|
35
40
|
# ─── Defaults ─────────────────────────────────────────────────────────────────
|
|
36
41
|
GOAL=""
|
|
42
|
+
ORIGINAL_GOAL="" # Preserved across restarts — GOAL gets appended to
|
|
37
43
|
MAX_ITERATIONS="${SW_MAX_ITERATIONS:-20}"
|
|
38
44
|
TEST_CMD=""
|
|
45
|
+
FAST_TEST_CMD=""
|
|
46
|
+
FAST_TEST_INTERVAL=5
|
|
47
|
+
TEST_LOG_FILE=""
|
|
39
48
|
MODEL="${SW_MODEL:-opus}"
|
|
40
49
|
AGENTS=1
|
|
50
|
+
AGENT_ROLES=""
|
|
41
51
|
USE_WORKTREE=false
|
|
42
52
|
SKIP_PERMISSIONS=false
|
|
43
53
|
MAX_TURNS=""
|
|
44
54
|
RESUME=false
|
|
45
55
|
VERBOSE=false
|
|
46
56
|
MAX_ITERATIONS_EXPLICIT=false
|
|
47
|
-
|
|
57
|
+
MAX_RESTARTS=0
|
|
58
|
+
SESSION_RESTART=false
|
|
59
|
+
RESTART_COUNT=0
|
|
60
|
+
VERSION="2.0.0"
|
|
48
61
|
|
|
49
62
|
# ─── Flexible Iteration Defaults ────────────────────────────────────────────
|
|
50
63
|
AUTO_EXTEND=true # Auto-extend iterations when work is incomplete
|
|
@@ -75,12 +88,16 @@ show_help() {
|
|
|
75
88
|
echo -e "${BOLD}OPTIONS${RESET}"
|
|
76
89
|
echo -e " ${CYAN}--max-iterations${RESET} N Max loop iterations (default: 20)"
|
|
77
90
|
echo -e " ${CYAN}--test-cmd${RESET} \"cmd\" Test command to run between iterations"
|
|
91
|
+
echo -e " ${CYAN}--fast-test-cmd${RESET} \"cmd\" Fast/subset test command (alternates with full)"
|
|
92
|
+
echo -e " ${CYAN}--fast-test-interval${RESET} N Run full tests every N iterations (default: 5)"
|
|
78
93
|
echo -e " ${CYAN}--model${RESET} MODEL Claude model to use (default: opus)"
|
|
79
94
|
echo -e " ${CYAN}--agents${RESET} N Number of parallel agents (default: 1)"
|
|
95
|
+
echo -e " ${CYAN}--roles${RESET} \"r1,r2,...\" Role per agent: builder,reviewer,tester,optimizer,docs,security"
|
|
80
96
|
echo -e " ${CYAN}--worktree${RESET} Use git worktrees for isolation (auto if agents > 1)"
|
|
81
97
|
echo -e " ${CYAN}--skip-permissions${RESET} Pass --dangerously-skip-permissions to Claude"
|
|
82
98
|
echo -e " ${CYAN}--max-turns${RESET} N Max API turns per Claude session"
|
|
83
99
|
echo -e " ${CYAN}--resume${RESET} Resume from existing .claude/loop-state.md"
|
|
100
|
+
echo -e " ${CYAN}--max-restarts${RESET} N Max session restarts on exhaustion (default: 0)"
|
|
84
101
|
echo -e " ${CYAN}--verbose${RESET} Show full Claude output (default: summary)"
|
|
85
102
|
echo -e " ${CYAN}--help${RESET} Show this help"
|
|
86
103
|
echo ""
|
|
@@ -175,6 +192,30 @@ while [[ $# -gt 0 ]]; do
|
|
|
175
192
|
shift 2
|
|
176
193
|
;;
|
|
177
194
|
--max-extensions=*) MAX_EXTENSIONS="${1#--max-extensions=}"; shift ;;
|
|
195
|
+
--fast-test-cmd)
|
|
196
|
+
FAST_TEST_CMD="${2:-}"
|
|
197
|
+
[[ -z "$FAST_TEST_CMD" ]] && { error "Missing value for --fast-test-cmd"; exit 1; }
|
|
198
|
+
shift 2
|
|
199
|
+
;;
|
|
200
|
+
--fast-test-cmd=*) FAST_TEST_CMD="${1#--fast-test-cmd=}"; shift ;;
|
|
201
|
+
--fast-test-interval)
|
|
202
|
+
FAST_TEST_INTERVAL="${2:-}"
|
|
203
|
+
[[ -z "$FAST_TEST_INTERVAL" ]] && { error "Missing value for --fast-test-interval"; exit 1; }
|
|
204
|
+
shift 2
|
|
205
|
+
;;
|
|
206
|
+
--fast-test-interval=*) FAST_TEST_INTERVAL="${1#--fast-test-interval=}"; shift ;;
|
|
207
|
+
--max-restarts)
|
|
208
|
+
MAX_RESTARTS="${2:-}"
|
|
209
|
+
[[ -z "$MAX_RESTARTS" ]] && { error "Missing value for --max-restarts"; exit 1; }
|
|
210
|
+
shift 2
|
|
211
|
+
;;
|
|
212
|
+
--max-restarts=*) MAX_RESTARTS="${1#--max-restarts=}"; shift ;;
|
|
213
|
+
--roles)
|
|
214
|
+
AGENT_ROLES="${2:-}"
|
|
215
|
+
[[ -z "$AGENT_ROLES" ]] && { error "Missing value for --roles"; exit 1; }
|
|
216
|
+
shift 2
|
|
217
|
+
;;
|
|
218
|
+
--roles=*) AGENT_ROLES="${1#--roles=}"; shift ;;
|
|
178
219
|
--help|-h)
|
|
179
220
|
show_help
|
|
180
221
|
exit 0
|
|
@@ -203,6 +244,27 @@ if [[ "$AGENTS" -gt 1 ]]; then
|
|
|
203
244
|
USE_WORKTREE=true
|
|
204
245
|
fi
|
|
205
246
|
|
|
247
|
+
# Warn if --roles without --agents
|
|
248
|
+
if [[ -n "$AGENT_ROLES" ]] && [[ "$AGENTS" -le 1 ]]; then
|
|
249
|
+
warn "--roles requires --agents > 1 (roles are ignored in single-agent mode)"
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# Warn if --max-restarts with --agents > 1 (not yet supported)
|
|
253
|
+
if [[ "${MAX_RESTARTS:-0}" -gt 0 ]] && [[ "$AGENTS" -gt 1 ]]; then
|
|
254
|
+
warn "--max-restarts is ignored in multi-agent mode (restart support is single-agent only)"
|
|
255
|
+
MAX_RESTARTS=0
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
# Validate numeric flags
|
|
259
|
+
if ! [[ "$FAST_TEST_INTERVAL" =~ ^[1-9][0-9]*$ ]]; then
|
|
260
|
+
error "--fast-test-interval must be a positive integer (got: $FAST_TEST_INTERVAL)"
|
|
261
|
+
exit 1
|
|
262
|
+
fi
|
|
263
|
+
if ! [[ "$MAX_RESTARTS" =~ ^[0-9]+$ ]]; then
|
|
264
|
+
error "--max-restarts must be a non-negative integer (got: $MAX_RESTARTS)"
|
|
265
|
+
exit 1
|
|
266
|
+
fi
|
|
267
|
+
|
|
206
268
|
# ─── Validate Inputs ─────────────────────────────────────────────────────────
|
|
207
269
|
|
|
208
270
|
if ! $RESUME && [[ -z "$GOAL" ]]; then
|
|
@@ -224,6 +286,9 @@ if ! git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then
|
|
|
224
286
|
exit 1
|
|
225
287
|
fi
|
|
226
288
|
|
|
289
|
+
# Preserve original goal before any appending (memory fixes, human feedback)
|
|
290
|
+
ORIGINAL_GOAL="$GOAL"
|
|
291
|
+
|
|
227
292
|
# ─── Timeout Detection ────────────────────────────────────────────────────────
|
|
228
293
|
TIMEOUT_CMD=""
|
|
229
294
|
if command -v timeout &>/dev/null; then
|
|
@@ -266,6 +331,17 @@ select_adaptive_model() {
|
|
|
266
331
|
echo "$default_model"
|
|
267
332
|
return 0
|
|
268
333
|
fi
|
|
334
|
+
# Read learned model routing
|
|
335
|
+
local _routing_file="${HOME}/.shipwright/optimization/model-routing.json"
|
|
336
|
+
if [[ -f "$_routing_file" ]] && command -v jq &>/dev/null; then
|
|
337
|
+
local _routed_model
|
|
338
|
+
_routed_model=$(jq -r --arg r "$role" '.routes[$r].model // ""' "$_routing_file" 2>/dev/null) || true
|
|
339
|
+
if [[ -n "${_routed_model:-}" && "${_routed_model:-}" != "null" ]]; then
|
|
340
|
+
echo "${_routed_model}"
|
|
341
|
+
return 0
|
|
342
|
+
fi
|
|
343
|
+
fi
|
|
344
|
+
|
|
269
345
|
# Try intelligence-based recommendation
|
|
270
346
|
if type intelligence_recommend_model &>/dev/null 2>&1; then
|
|
271
347
|
local rec
|
|
@@ -317,6 +393,18 @@ apply_adaptive_budget() {
|
|
|
317
393
|
[[ -n "$tuned_cb" && "$tuned_cb" != "null" ]] && CIRCUIT_BREAKER_THRESHOLD="$tuned_cb"
|
|
318
394
|
fi
|
|
319
395
|
|
|
396
|
+
# Read learned iteration model
|
|
397
|
+
local _iter_model="${HOME}/.shipwright/optimization/iteration-model.json"
|
|
398
|
+
if [[ -f "$_iter_model" ]] && ! $MAX_ITERATIONS_EXPLICIT && command -v jq &>/dev/null; then
|
|
399
|
+
local _complexity="${ISSUE_COMPLEXITY:-${COMPLEXITY:-medium}}"
|
|
400
|
+
local _predicted_max
|
|
401
|
+
_predicted_max=$(jq -r --arg c "$_complexity" '.predictions[$c].max_iterations // ""' "$_iter_model" 2>/dev/null) || true
|
|
402
|
+
if [[ -n "${_predicted_max:-}" && "${_predicted_max:-}" != "null" && "${_predicted_max:-0}" -gt 0 ]]; then
|
|
403
|
+
MAX_ITERATIONS="${_predicted_max}"
|
|
404
|
+
info "Iteration model: ${_complexity} complexity → max ${_predicted_max} iterations"
|
|
405
|
+
fi
|
|
406
|
+
fi
|
|
407
|
+
|
|
320
408
|
# Try intelligence-based iteration estimate
|
|
321
409
|
if type intelligence_estimate_iterations &>/dev/null 2>&1 && ! $MAX_ITERATIONS_EXPLICIT; then
|
|
322
410
|
local est
|
|
@@ -481,31 +569,67 @@ resume_state() {
|
|
|
481
569
|
}
|
|
482
570
|
|
|
483
571
|
write_state() {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
572
|
+
local tmp_state="${STATE_FILE}.tmp.$$"
|
|
573
|
+
# Use printf instead of heredoc to avoid delimiter injection from GOAL
|
|
574
|
+
{
|
|
575
|
+
printf -- '---\n'
|
|
576
|
+
printf 'goal: "%s"\n' "$GOAL"
|
|
577
|
+
printf 'iteration: %s\n' "$ITERATION"
|
|
578
|
+
printf 'max_iterations: %s\n' "$MAX_ITERATIONS"
|
|
579
|
+
printf 'status: %s\n' "$STATUS"
|
|
580
|
+
printf 'test_cmd: "%s"\n' "$TEST_CMD"
|
|
581
|
+
printf 'model: %s\n' "$MODEL"
|
|
582
|
+
printf 'agents: %s\n' "$AGENTS"
|
|
583
|
+
printf 'started_at: %s\n' "$(now_iso)"
|
|
584
|
+
printf 'last_iteration_at: %s\n' "$(now_iso)"
|
|
585
|
+
printf 'consecutive_failures: %s\n' "$CONSECUTIVE_FAILURES"
|
|
586
|
+
printf 'total_commits: %s\n' "$TOTAL_COMMITS"
|
|
587
|
+
printf 'audit_enabled: %s\n' "$AUDIT_ENABLED"
|
|
588
|
+
printf 'audit_agent_enabled: %s\n' "$AUDIT_AGENT_ENABLED"
|
|
589
|
+
printf 'quality_gates_enabled: %s\n' "$QUALITY_GATES_ENABLED"
|
|
590
|
+
printf 'dod_file: "%s"\n' "$DOD_FILE"
|
|
591
|
+
printf 'auto_extend: %s\n' "$AUTO_EXTEND"
|
|
592
|
+
printf 'extension_count: %s\n' "$EXTENSION_COUNT"
|
|
593
|
+
printf 'max_extensions: %s\n' "$MAX_EXTENSIONS"
|
|
594
|
+
printf -- '---\n\n'
|
|
595
|
+
printf '## Log\n'
|
|
596
|
+
printf '%s\n' "$LOG_ENTRIES"
|
|
597
|
+
} > "$tmp_state"
|
|
598
|
+
if ! mv "$tmp_state" "$STATE_FILE" 2>/dev/null; then
|
|
599
|
+
warn "Failed to write state file: $STATE_FILE"
|
|
600
|
+
fi
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
write_progress() {
|
|
604
|
+
local progress_file="$LOG_DIR/progress.md"
|
|
605
|
+
local recent_commits
|
|
606
|
+
recent_commits=$(git -C "$PROJECT_ROOT" log --oneline -5 2>/dev/null || echo "(no commits)")
|
|
607
|
+
local changed_files
|
|
608
|
+
changed_files=$(git -C "$PROJECT_ROOT" diff --name-only HEAD~3 2>/dev/null | head -20 || echo "(none)")
|
|
609
|
+
local last_error=""
|
|
610
|
+
local prev_test_log="$LOG_DIR/tests-iter-${ITERATION}.log"
|
|
611
|
+
if [[ -f "$prev_test_log" ]] && [[ "${TEST_PASSED:-}" == "false" ]]; then
|
|
612
|
+
last_error=$(tail -10 "$prev_test_log" 2>/dev/null || true)
|
|
613
|
+
fi
|
|
614
|
+
|
|
615
|
+
# Use printf to avoid heredoc delimiter injection from GOAL content
|
|
616
|
+
local tmp_progress="${progress_file}.tmp.$$"
|
|
617
|
+
{
|
|
618
|
+
printf '# Session Progress (Auto-Generated)\n\n'
|
|
619
|
+
printf '## Goal\n%s\n\n' "${GOAL}"
|
|
620
|
+
printf '## Status\n'
|
|
621
|
+
printf -- '- Iteration: %s/%s\n' "${ITERATION}" "${MAX_ITERATIONS}"
|
|
622
|
+
printf -- '- Session restart: %s/%s\n' "${RESTART_COUNT:-0}" "${MAX_RESTARTS:-0}"
|
|
623
|
+
printf -- '- Tests passing: %s\n' "${TEST_PASSED:-unknown}"
|
|
624
|
+
printf -- '- Status: %s\n\n' "${STATUS:-running}"
|
|
625
|
+
printf '## Recent Commits\n%s\n\n' "${recent_commits}"
|
|
626
|
+
printf '## Changed Files\n%s\n\n' "${changed_files}"
|
|
627
|
+
if [[ -n "$last_error" ]]; then
|
|
628
|
+
printf '## Last Error\n%s\n\n' "$last_error"
|
|
629
|
+
fi
|
|
630
|
+
printf '## Timestamp\n%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
631
|
+
} > "$tmp_progress" 2>/dev/null
|
|
632
|
+
mv "$tmp_progress" "$progress_file" 2>/dev/null || rm -f "$tmp_progress" 2>/dev/null
|
|
509
633
|
}
|
|
510
634
|
|
|
511
635
|
append_log_entry() {
|
|
@@ -549,11 +673,50 @@ git_auto_commit() {
|
|
|
549
673
|
return 0
|
|
550
674
|
}
|
|
551
675
|
|
|
676
|
+
# ─── Fatal Error Detection ────────────────────────────────────────────────────
|
|
677
|
+
|
|
678
|
+
check_fatal_error() {
|
|
679
|
+
local log_file="$1"
|
|
680
|
+
local cli_exit_code="${2:-0}"
|
|
681
|
+
[[ -f "$log_file" ]] || return 1
|
|
682
|
+
|
|
683
|
+
# Known fatal error patterns from Claude CLI / Anthropic API
|
|
684
|
+
local fatal_patterns="Invalid API key|invalid_api_key|authentication_error|API key expired"
|
|
685
|
+
fatal_patterns="${fatal_patterns}|rate_limit_error|overloaded_error|billing"
|
|
686
|
+
fatal_patterns="${fatal_patterns}|Could not resolve host|connection refused|ECONNREFUSED"
|
|
687
|
+
fatal_patterns="${fatal_patterns}|ANTHROPIC_API_KEY.*not set|No API key"
|
|
688
|
+
|
|
689
|
+
if grep -qiE "$fatal_patterns" "$log_file" 2>/dev/null; then
|
|
690
|
+
local match
|
|
691
|
+
match=$(grep -iE "$fatal_patterns" "$log_file" 2>/dev/null | head -1 | cut -c1-120)
|
|
692
|
+
error "Fatal CLI error: $match"
|
|
693
|
+
return 0 # fatal error detected
|
|
694
|
+
fi
|
|
695
|
+
|
|
696
|
+
# Non-zero exit + tiny output = likely CLI crash
|
|
697
|
+
if [[ "$cli_exit_code" -ne 0 ]]; then
|
|
698
|
+
local line_count
|
|
699
|
+
line_count=$(grep -cv '^$' "$log_file" 2>/dev/null || echo 0)
|
|
700
|
+
if [[ "$line_count" -lt 3 ]]; then
|
|
701
|
+
local content
|
|
702
|
+
content=$(head -3 "$log_file" 2>/dev/null | cut -c1-120)
|
|
703
|
+
error "CLI exited $cli_exit_code with minimal output: $content"
|
|
704
|
+
return 0
|
|
705
|
+
fi
|
|
706
|
+
fi
|
|
707
|
+
|
|
708
|
+
return 1 # no fatal error
|
|
709
|
+
}
|
|
710
|
+
|
|
552
711
|
# ─── Progress & Circuit Breaker ───────────────────────────────────────────────
|
|
553
712
|
|
|
554
713
|
check_progress() {
|
|
555
714
|
local changes
|
|
556
|
-
|
|
715
|
+
# Exclude loop bookkeeping files — only count real code changes as progress
|
|
716
|
+
changes="$(git -C "$PROJECT_ROOT" diff --stat HEAD~1 \
|
|
717
|
+
-- . ':!.claude/loop-state.md' ':!.claude/pipeline-state.md' \
|
|
718
|
+
':!**/progress.md' ':!**/error-summary.json' \
|
|
719
|
+
2>/dev/null | tail -1 || echo "")"
|
|
557
720
|
local insertions
|
|
558
721
|
insertions="$(echo "$changes" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)"
|
|
559
722
|
if [[ "${insertions:-0}" -lt "$MIN_PROGRESS_LINES" ]]; then
|
|
@@ -568,6 +731,30 @@ check_completion() {
|
|
|
568
731
|
}
|
|
569
732
|
|
|
570
733
|
check_circuit_breaker() {
|
|
734
|
+
# Vitals-driven circuit breaker (preferred over static threshold)
|
|
735
|
+
if type pipeline_compute_vitals &>/dev/null 2>&1 && type pipeline_health_verdict &>/dev/null 2>&1; then
|
|
736
|
+
local _vitals_json _verdict
|
|
737
|
+
local _loop_state="${STATE_FILE:-}"
|
|
738
|
+
local _loop_artifacts="${ARTIFACTS_DIR:-}"
|
|
739
|
+
local _loop_issue="${ISSUE_NUMBER:-}"
|
|
740
|
+
_vitals_json=$(pipeline_compute_vitals "$_loop_state" "$_loop_artifacts" "$_loop_issue" 2>/dev/null) || true
|
|
741
|
+
if [[ -n "$_vitals_json" && "$_vitals_json" != "{}" ]]; then
|
|
742
|
+
_verdict=$(echo "$_vitals_json" | jq -r '.verdict // "continue"' 2>/dev/null || echo "continue")
|
|
743
|
+
if [[ "$_verdict" == "abort" ]]; then
|
|
744
|
+
local _health_score
|
|
745
|
+
_health_score=$(echo "$_vitals_json" | jq -r '.health_score // 0' 2>/dev/null || echo "0")
|
|
746
|
+
error "Vitals circuit breaker: health score ${_health_score}/100 — aborting (${CONSECUTIVE_FAILURES} stagnant iterations)"
|
|
747
|
+
STATUS="circuit_breaker"
|
|
748
|
+
return 1
|
|
749
|
+
fi
|
|
750
|
+
# Vitals say continue/warn/intervene — don't trip circuit breaker yet
|
|
751
|
+
if [[ "$_verdict" == "continue" || "$_verdict" == "warn" ]]; then
|
|
752
|
+
return 0
|
|
753
|
+
fi
|
|
754
|
+
fi
|
|
755
|
+
fi
|
|
756
|
+
|
|
757
|
+
# Fallback: static threshold circuit breaker
|
|
571
758
|
if [[ "$CONSECUTIVE_FAILURES" -ge "$CIRCUIT_BREAKER_THRESHOLD" ]]; then
|
|
572
759
|
error "Circuit breaker tripped: ${CIRCUIT_BREAKER_THRESHOLD} consecutive iterations with no meaningful progress."
|
|
573
760
|
STATUS="circuit_breaker"
|
|
@@ -646,16 +833,88 @@ run_test_gate() {
|
|
|
646
833
|
return
|
|
647
834
|
fi
|
|
648
835
|
|
|
836
|
+
# Determine which test command to use this iteration
|
|
837
|
+
local active_test_cmd="$TEST_CMD"
|
|
838
|
+
local test_mode="full"
|
|
839
|
+
if [[ -n "$FAST_TEST_CMD" ]]; then
|
|
840
|
+
# Use full test every FAST_TEST_INTERVAL iterations, on first iteration, and on final iteration
|
|
841
|
+
if [[ "$ITERATION" -eq 1 ]] || [[ $(( ITERATION % FAST_TEST_INTERVAL )) -eq 0 ]] || [[ "$ITERATION" -ge "$MAX_ITERATIONS" ]]; then
|
|
842
|
+
active_test_cmd="$TEST_CMD"
|
|
843
|
+
test_mode="full"
|
|
844
|
+
else
|
|
845
|
+
active_test_cmd="$FAST_TEST_CMD"
|
|
846
|
+
test_mode="fast"
|
|
847
|
+
fi
|
|
848
|
+
fi
|
|
849
|
+
|
|
649
850
|
local test_log="$LOG_DIR/tests-iter-${ITERATION}.log"
|
|
650
|
-
|
|
851
|
+
TEST_LOG_FILE="$test_log"
|
|
852
|
+
echo -e " ${DIM}Running ${test_mode} tests...${RESET}"
|
|
853
|
+
# Wrap test command with timeout (5 min default) to prevent hanging
|
|
854
|
+
local test_timeout="${SW_TEST_TIMEOUT:-300}"
|
|
855
|
+
local test_wrapper="$active_test_cmd"
|
|
856
|
+
if command -v timeout &>/dev/null; then
|
|
857
|
+
test_wrapper="timeout ${test_timeout} bash -c $(printf '%q' "$active_test_cmd")"
|
|
858
|
+
elif command -v gtimeout &>/dev/null; then
|
|
859
|
+
test_wrapper="gtimeout ${test_timeout} bash -c $(printf '%q' "$active_test_cmd")"
|
|
860
|
+
fi
|
|
861
|
+
if bash -c "$test_wrapper" > "$test_log" 2>&1; then
|
|
651
862
|
TEST_PASSED=true
|
|
652
|
-
TEST_OUTPUT="All tests passed."
|
|
863
|
+
TEST_OUTPUT="All tests passed (${test_mode} mode)."
|
|
653
864
|
else
|
|
654
865
|
TEST_PASSED=false
|
|
655
866
|
TEST_OUTPUT="$(tail -50 "$test_log")"
|
|
656
867
|
fi
|
|
657
868
|
}
|
|
658
869
|
|
|
870
|
+
write_error_summary() {
|
|
871
|
+
local error_json="$LOG_DIR/error-summary.json"
|
|
872
|
+
|
|
873
|
+
# Only write on test failure
|
|
874
|
+
if [[ "${TEST_PASSED:-}" != "false" ]]; then
|
|
875
|
+
# Clear previous error summary on success
|
|
876
|
+
rm -f "$error_json" 2>/dev/null || true
|
|
877
|
+
return
|
|
878
|
+
fi
|
|
879
|
+
|
|
880
|
+
local test_log="${TEST_LOG_FILE:-$LOG_DIR/tests-iter-${ITERATION}.log}"
|
|
881
|
+
[[ ! -f "$test_log" ]] && return
|
|
882
|
+
|
|
883
|
+
# Extract error lines (last 30 lines, grep for error patterns)
|
|
884
|
+
local error_lines_raw
|
|
885
|
+
error_lines_raw=$(tail -30 "$test_log" 2>/dev/null | grep -iE '(error|fail|assert|exception|panic|FAIL|TypeError|ReferenceError|SyntaxError)' | head -10 || true)
|
|
886
|
+
|
|
887
|
+
local error_count=0
|
|
888
|
+
if [[ -n "$error_lines_raw" ]]; then
|
|
889
|
+
error_count=$(echo "$error_lines_raw" | wc -l | tr -d ' ')
|
|
890
|
+
fi
|
|
891
|
+
|
|
892
|
+
local tmp_json="${error_json}.tmp.$$"
|
|
893
|
+
|
|
894
|
+
# Build JSON with jq (preferred) or plain-text fallback
|
|
895
|
+
if command -v jq &>/dev/null; then
|
|
896
|
+
jq -n \
|
|
897
|
+
--argjson iteration "${ITERATION:-0}" \
|
|
898
|
+
--arg timestamp "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
|
899
|
+
--argjson error_count "${error_count:-0}" \
|
|
900
|
+
--arg error_lines "$error_lines_raw" \
|
|
901
|
+
--arg test_cmd "${TEST_CMD:-}" \
|
|
902
|
+
'{
|
|
903
|
+
iteration: $iteration,
|
|
904
|
+
timestamp: $timestamp,
|
|
905
|
+
error_count: $error_count,
|
|
906
|
+
error_lines: ($error_lines | split("\n") | map(select(length > 0))),
|
|
907
|
+
test_cmd: $test_cmd
|
|
908
|
+
}' > "$tmp_json" 2>/dev/null && mv "$tmp_json" "$error_json" || rm -f "$tmp_json" 2>/dev/null
|
|
909
|
+
else
|
|
910
|
+
# Fallback: write plain-text error summary (still machine-parseable)
|
|
911
|
+
cat > "$tmp_json" <<ERRJSON
|
|
912
|
+
{"iteration":${ITERATION:-0},"error_count":${error_count:-0},"error_lines":[],"test_cmd":"test"}
|
|
913
|
+
ERRJSON
|
|
914
|
+
mv "$tmp_json" "$error_json" 2>/dev/null || rm -f "$tmp_json" 2>/dev/null
|
|
915
|
+
fi
|
|
916
|
+
}
|
|
917
|
+
|
|
659
918
|
# ─── Audit Agent ─────────────────────────────────────────────────────────────
|
|
660
919
|
|
|
661
920
|
run_audit_agent() {
|
|
@@ -884,6 +1143,21 @@ compose_prompt() {
|
|
|
884
1143
|
$TEST_OUTPUT"
|
|
885
1144
|
fi
|
|
886
1145
|
|
|
1146
|
+
# Structured error context (machine-readable)
|
|
1147
|
+
local error_summary_section=""
|
|
1148
|
+
local error_json="$LOG_DIR/error-summary.json"
|
|
1149
|
+
if [[ -f "$error_json" ]]; then
|
|
1150
|
+
local err_count err_lines
|
|
1151
|
+
err_count=$(jq -r '.error_count // 0' "$error_json" 2>/dev/null || echo "0")
|
|
1152
|
+
err_lines=$(jq -r '.error_lines[]? // empty' "$error_json" 2>/dev/null | head -10 || true)
|
|
1153
|
+
if [[ "$err_count" -gt 0 ]] && [[ -n "$err_lines" ]]; then
|
|
1154
|
+
error_summary_section="## Structured Error Summary (${err_count} errors detected)
|
|
1155
|
+
${err_lines}
|
|
1156
|
+
|
|
1157
|
+
Fix these specific errors. Each line above is one distinct error from the test output."
|
|
1158
|
+
fi
|
|
1159
|
+
fi
|
|
1160
|
+
|
|
887
1161
|
# Build audit sections (captured before heredoc to avoid nested heredoc issues)
|
|
888
1162
|
local audit_section
|
|
889
1163
|
audit_section="$(compose_audit_section)"
|
|
@@ -1006,6 +1280,16 @@ ${last_error}"
|
|
|
1006
1280
|
local stuckness_section=""
|
|
1007
1281
|
stuckness_section="$(detect_stuckness)"
|
|
1008
1282
|
|
|
1283
|
+
# Session restart context — inject previous session progress
|
|
1284
|
+
local restart_section=""
|
|
1285
|
+
if [[ "$SESSION_RESTART" == "true" ]] && [[ -f "$LOG_DIR/progress.md" ]]; then
|
|
1286
|
+
restart_section="## Previous Session Progress
|
|
1287
|
+
$(cat "$LOG_DIR/progress.md")
|
|
1288
|
+
|
|
1289
|
+
You are starting a FRESH session after the previous one exhausted its iterations.
|
|
1290
|
+
Read the progress above and continue from where it left off. Do NOT repeat work already done."
|
|
1291
|
+
fi
|
|
1292
|
+
|
|
1009
1293
|
cat <<PROMPT
|
|
1010
1294
|
You are an autonomous coding agent on iteration ${ITERATION}/${MAX_ITERATIONS} of a continuous loop.
|
|
1011
1295
|
|
|
@@ -1021,6 +1305,8 @@ ${git_log}
|
|
|
1021
1305
|
## Test Results (Previous Iteration)
|
|
1022
1306
|
${test_section}
|
|
1023
1307
|
|
|
1308
|
+
${error_summary_section:+$error_summary_section
|
|
1309
|
+
}
|
|
1024
1310
|
${memory_section:+## Memory Context
|
|
1025
1311
|
$memory_section
|
|
1026
1312
|
}
|
|
@@ -1028,6 +1314,8 @@ ${dora_section:+$dora_section
|
|
|
1028
1314
|
}
|
|
1029
1315
|
${intelligence_section:+$intelligence_section
|
|
1030
1316
|
}
|
|
1317
|
+
${restart_section:+$restart_section
|
|
1318
|
+
}
|
|
1031
1319
|
## Instructions
|
|
1032
1320
|
1. Read the codebase and understand the current state
|
|
1033
1321
|
2. Identify the highest-priority remaining work toward the goal
|
|
@@ -1189,6 +1477,37 @@ compose_worker_prompt() {
|
|
|
1189
1477
|
local base_prompt
|
|
1190
1478
|
base_prompt="$(compose_prompt)"
|
|
1191
1479
|
|
|
1480
|
+
# Role-specific instructions
|
|
1481
|
+
local role_section=""
|
|
1482
|
+
if [[ -n "$AGENT_ROLES" ]] && [[ "${agent_num:-0}" -ge 1 ]]; then
|
|
1483
|
+
# Split comma-separated roles and get role for this agent
|
|
1484
|
+
local role=""
|
|
1485
|
+
local IFS_BAK="$IFS"
|
|
1486
|
+
IFS=',' read -ra _roles <<< "$AGENT_ROLES"
|
|
1487
|
+
IFS="$IFS_BAK"
|
|
1488
|
+
if [[ "$agent_num" -le "${#_roles[@]}" ]]; then
|
|
1489
|
+
role="${_roles[$((agent_num - 1))]}"
|
|
1490
|
+
# Trim whitespace and skip empty roles (handles trailing comma)
|
|
1491
|
+
role="$(echo "$role" | tr -d ' ')"
|
|
1492
|
+
fi
|
|
1493
|
+
|
|
1494
|
+
if [[ -n "$role" ]]; then
|
|
1495
|
+
local role_desc=""
|
|
1496
|
+
case "$role" in
|
|
1497
|
+
builder) role_desc="Focus on implementation — writing code, fixing bugs, building features. You are the primary builder." ;;
|
|
1498
|
+
reviewer) role_desc="Focus on code review — look for bugs, security issues, edge cases in recent commits. Make fixes via commits." ;;
|
|
1499
|
+
tester) role_desc="Focus on test coverage — write new tests, fix failing tests, improve assertions and edge case coverage." ;;
|
|
1500
|
+
optimizer) role_desc="Focus on performance — profile hot paths, reduce complexity, optimize algorithms and data structures." ;;
|
|
1501
|
+
docs) role_desc="Focus on documentation — update README, add docstrings, write usage guides for new features." ;;
|
|
1502
|
+
security) role_desc="Focus on security — audit for vulnerabilities, fix injection risks, validate inputs, check auth boundaries." ;;
|
|
1503
|
+
*) role_desc="Focus on: ${role}. Apply your expertise in this area to advance the goal." ;;
|
|
1504
|
+
esac
|
|
1505
|
+
role_section="## Your Role: ${role}
|
|
1506
|
+
${role_desc}
|
|
1507
|
+
Prioritize work in your area of expertise. Coordinate with other agents via git log."
|
|
1508
|
+
fi
|
|
1509
|
+
fi
|
|
1510
|
+
|
|
1192
1511
|
cat <<PROMPT
|
|
1193
1512
|
${base_prompt}
|
|
1194
1513
|
|
|
@@ -1196,6 +1515,8 @@ ${base_prompt}
|
|
|
1196
1515
|
You are Agent ${agent_num} of ${total_agents}. Other agents are working in parallel.
|
|
1197
1516
|
Check git log to see what they've done — avoid duplicating their work.
|
|
1198
1517
|
Focus on areas they haven't touched yet.
|
|
1518
|
+
|
|
1519
|
+
${role_section}
|
|
1199
1520
|
PROMPT
|
|
1200
1521
|
}
|
|
1201
1522
|
|
|
@@ -1268,7 +1589,14 @@ extract_summary() {
|
|
|
1268
1589
|
local summary
|
|
1269
1590
|
summary="$(grep -v '^$' "$log_file" | tail -5 | head -3 2>/dev/null || echo "(no output)")"
|
|
1270
1591
|
# Truncate long lines
|
|
1271
|
-
echo "$summary" | cut -c1-120
|
|
1592
|
+
summary="$(echo "$summary" | cut -c1-120)"
|
|
1593
|
+
|
|
1594
|
+
# Sanitize: if summary is just a CLI/API error, replace with generic text
|
|
1595
|
+
if echo "$summary" | grep -qiE 'Invalid API key|authentication_error|rate_limit|API key expired|ANTHROPIC_API_KEY'; then
|
|
1596
|
+
summary="(CLI error — no useful output this iteration)"
|
|
1597
|
+
fi
|
|
1598
|
+
|
|
1599
|
+
echo "$summary"
|
|
1272
1600
|
}
|
|
1273
1601
|
|
|
1274
1602
|
# ─── Display Helpers ─────────────────────────────────────────────────────────
|
|
@@ -1547,17 +1875,21 @@ done
|
|
|
1547
1875
|
echo -e "\n${DIM}Agent ${AGENT_NUM} finished after ${ITERATION} iterations${RESET}"
|
|
1548
1876
|
WORKEREOF
|
|
1549
1877
|
|
|
1550
|
-
# Replace placeholders
|
|
1878
|
+
# Replace placeholders — use awk for all values to avoid sed injection
|
|
1879
|
+
# (sed breaks on & | \ in paths and test commands)
|
|
1551
1880
|
sed_i "s|__AGENT_NUM__|${agent_num}|g" "$worker_script"
|
|
1552
1881
|
sed_i "s|__TOTAL_AGENTS__|${total_agents}|g" "$worker_script"
|
|
1553
|
-
sed_i "s|__WORK_DIR__|${wt_path}|g" "$worker_script"
|
|
1554
|
-
sed_i "s|__LOG_DIR__|${LOG_DIR}|g" "$worker_script"
|
|
1555
1882
|
sed_i "s|__MAX_ITERATIONS__|${MAX_ITERATIONS}|g" "$worker_script"
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1883
|
+
# Paths and commands may contain sed-special chars — use awk
|
|
1884
|
+
awk -v val="$wt_path" '{gsub(/__WORK_DIR__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
|
|
1885
|
+
&& mv "${worker_script}.tmp" "$worker_script"
|
|
1886
|
+
awk -v val="$LOG_DIR" '{gsub(/__LOG_DIR__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
|
|
1887
|
+
&& mv "${worker_script}.tmp" "$worker_script"
|
|
1888
|
+
awk -v val="$TEST_CMD" '{gsub(/__TEST_CMD__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
|
|
1889
|
+
&& mv "${worker_script}.tmp" "$worker_script"
|
|
1890
|
+
awk -v val="$claude_flags" '{gsub(/__CLAUDE_FLAGS__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
|
|
1891
|
+
&& mv "${worker_script}.tmp" "$worker_script"
|
|
1892
|
+
awk -v val="$GOAL" '{gsub(/__GOAL__/, val); print}' "$worker_script" > "${worker_script}.tmp" \
|
|
1561
1893
|
&& mv "${worker_script}.tmp" "$worker_script"
|
|
1562
1894
|
chmod +x "$worker_script"
|
|
1563
1895
|
echo "$worker_script"
|
|
@@ -1577,10 +1909,14 @@ launch_multi_agent() {
|
|
|
1577
1909
|
MULTI_WINDOW_NAME="sw-loop-$(date +%s)"
|
|
1578
1910
|
tmux new-window -n "$MULTI_WINDOW_NAME" -c "$PROJECT_ROOT"
|
|
1579
1911
|
|
|
1912
|
+
# Capture the first pane's ID (stable regardless of pane-base-index)
|
|
1913
|
+
local monitor_pane_id
|
|
1914
|
+
monitor_pane_id="$(tmux list-panes -t "$MULTI_WINDOW_NAME" -F '#{pane_id}' 2>/dev/null | head -1)"
|
|
1915
|
+
|
|
1580
1916
|
# First pane becomes monitor
|
|
1581
|
-
tmux send-keys -t "$
|
|
1917
|
+
tmux send-keys -t "$monitor_pane_id" "printf '\\033]2;loop-monitor\\033\\\\'" Enter
|
|
1582
1918
|
sleep 0.2
|
|
1583
|
-
tmux send-keys -t "$
|
|
1919
|
+
tmux send-keys -t "$monitor_pane_id" "clear && echo 'Loop Monitor — watching agent logs...'" Enter
|
|
1584
1920
|
|
|
1585
1921
|
# Create worker panes
|
|
1586
1922
|
for i in $(seq 1 "$AGENTS"); do
|
|
@@ -1596,12 +1932,12 @@ launch_multi_agent() {
|
|
|
1596
1932
|
|
|
1597
1933
|
# Layout: monitor pane on top (35%), worker agents tile below
|
|
1598
1934
|
tmux select-layout -t "$MULTI_WINDOW_NAME" main-vertical 2>/dev/null || true
|
|
1599
|
-
tmux resize-pane -t "$
|
|
1935
|
+
tmux resize-pane -t "$monitor_pane_id" -y 35% 2>/dev/null || true
|
|
1600
1936
|
|
|
1601
1937
|
# In the monitor pane, tail all agent logs
|
|
1602
|
-
tmux select-pane -t "$
|
|
1938
|
+
tmux select-pane -t "$monitor_pane_id"
|
|
1603
1939
|
sleep 0.5
|
|
1604
|
-
tmux send-keys -t "$
|
|
1940
|
+
tmux send-keys -t "$monitor_pane_id" "clear && tail -f $LOG_DIR/agent-*-iter-*.log 2>/dev/null || echo 'Waiting for agent logs...'" Enter
|
|
1605
1941
|
|
|
1606
1942
|
success "Launched $AGENTS worker agents in window: $MULTI_WINDOW_NAME"
|
|
1607
1943
|
echo ""
|
|
@@ -1656,12 +1992,13 @@ wait_for_multi_completion() {
|
|
|
1656
1992
|
|
|
1657
1993
|
cleanup_multi_agent() {
|
|
1658
1994
|
if [[ -n "$MULTI_WINDOW_NAME" ]]; then
|
|
1659
|
-
# Send Ctrl-C to all panes
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1995
|
+
# Send Ctrl-C to all panes using stable pane IDs (not indices)
|
|
1996
|
+
# Pane IDs (%0, %1, ...) are unaffected by pane-base-index setting
|
|
1997
|
+
local pane_id
|
|
1998
|
+
while IFS= read -r pane_id; do
|
|
1999
|
+
[[ -z "$pane_id" ]] && continue
|
|
2000
|
+
tmux send-keys -t "$pane_id" C-c 2>/dev/null || true
|
|
2001
|
+
done < <(tmux list-panes -t "$MULTI_WINDOW_NAME" -F '#{pane_id}' 2>/dev/null || true)
|
|
1665
2002
|
sleep 1
|
|
1666
2003
|
tmux kill-window -t "$MULTI_WINDOW_NAME" 2>/dev/null || true
|
|
1667
2004
|
fi
|
|
@@ -1673,7 +2010,10 @@ cleanup_multi_agent() {
|
|
|
1673
2010
|
# ─── Main: Single-Agent Loop ─────────────────────────────────────────────────
|
|
1674
2011
|
|
|
1675
2012
|
run_single_agent_loop() {
|
|
1676
|
-
if $
|
|
2013
|
+
if [[ "$SESSION_RESTART" == "true" ]]; then
|
|
2014
|
+
# Restart: state already reset by run_loop_with_restarts, skip init
|
|
2015
|
+
info "Session restart ${RESTART_COUNT}/${MAX_RESTARTS} — fresh context, reading progress"
|
|
2016
|
+
elif $RESUME; then
|
|
1677
2017
|
resume_state
|
|
1678
2018
|
else
|
|
1679
2019
|
initialize_state
|
|
@@ -1683,6 +2023,9 @@ run_single_agent_loop() {
|
|
|
1683
2023
|
apply_adaptive_budget
|
|
1684
2024
|
MODEL="$(select_adaptive_model "build" "$MODEL")"
|
|
1685
2025
|
|
|
2026
|
+
# Track applied memory fix patterns for outcome recording
|
|
2027
|
+
_applied_fix_pattern=""
|
|
2028
|
+
|
|
1686
2029
|
show_banner
|
|
1687
2030
|
|
|
1688
2031
|
while true; do
|
|
@@ -1691,12 +2034,42 @@ run_single_agent_loop() {
|
|
|
1691
2034
|
check_max_iterations || break
|
|
1692
2035
|
ITERATION=$(( ITERATION + 1 ))
|
|
1693
2036
|
|
|
2037
|
+
# Try memory-based fix suggestion on retry after test failure
|
|
2038
|
+
if [[ "${TEST_PASSED:-}" == "false" ]]; then
|
|
2039
|
+
local _last_error=""
|
|
2040
|
+
local _prev_log="$LOG_DIR/iteration-$(( ITERATION - 1 )).log"
|
|
2041
|
+
if [[ -f "$_prev_log" ]]; then
|
|
2042
|
+
_last_error=$(tail -20 "$_prev_log" 2>/dev/null | grep -iE '(error|fail|exception)' | head -1 || true)
|
|
2043
|
+
fi
|
|
2044
|
+
local _fix_suggestion=""
|
|
2045
|
+
if type memory_closed_loop_inject &>/dev/null 2>&1 && [[ -n "${_last_error:-}" ]]; then
|
|
2046
|
+
_fix_suggestion=$(memory_closed_loop_inject "$_last_error" 2>/dev/null) || true
|
|
2047
|
+
fi
|
|
2048
|
+
if [[ -n "${_fix_suggestion:-}" ]]; then
|
|
2049
|
+
_applied_fix_pattern="${_last_error}"
|
|
2050
|
+
GOAL="KNOWN FIX (from past success): ${_fix_suggestion}
|
|
2051
|
+
|
|
2052
|
+
${GOAL}"
|
|
2053
|
+
info "Memory fix injected: ${_fix_suggestion:0:80}"
|
|
2054
|
+
fi
|
|
2055
|
+
fi
|
|
2056
|
+
|
|
1694
2057
|
# Run Claude
|
|
1695
2058
|
local exit_code=0
|
|
1696
2059
|
run_claude_iteration || exit_code=$?
|
|
1697
2060
|
|
|
1698
2061
|
local log_file="$LOG_DIR/iteration-${ITERATION}.log"
|
|
1699
2062
|
|
|
2063
|
+
# Detect fatal CLI errors (API key, auth, network) — abort immediately
|
|
2064
|
+
if check_fatal_error "$log_file" "$exit_code"; then
|
|
2065
|
+
STATUS="error"
|
|
2066
|
+
write_state
|
|
2067
|
+
write_progress
|
|
2068
|
+
error "Fatal CLI error detected — aborting loop (see iteration log)"
|
|
2069
|
+
show_summary
|
|
2070
|
+
return 1
|
|
2071
|
+
fi
|
|
2072
|
+
|
|
1700
2073
|
# Mid-loop memory refresh — re-query with current error context after iteration 3
|
|
1701
2074
|
if [[ "$ITERATION" -ge 3 ]] && type memory_inject_context &>/dev/null 2>&1; then
|
|
1702
2075
|
local refresh_ctx
|
|
@@ -1733,6 +2106,7 @@ run_single_agent_loop() {
|
|
|
1733
2106
|
|
|
1734
2107
|
# Test gate
|
|
1735
2108
|
run_test_gate
|
|
2109
|
+
write_error_summary
|
|
1736
2110
|
if [[ -n "$TEST_CMD" ]]; then
|
|
1737
2111
|
if [[ "$TEST_PASSED" == "true" ]]; then
|
|
1738
2112
|
echo -e " ${GREEN}✓${RESET} Tests: passed"
|
|
@@ -1741,6 +2115,18 @@ run_single_agent_loop() {
|
|
|
1741
2115
|
fi
|
|
1742
2116
|
fi
|
|
1743
2117
|
|
|
2118
|
+
# Track fix outcome for memory effectiveness
|
|
2119
|
+
if [[ -n "${_applied_fix_pattern:-}" ]]; then
|
|
2120
|
+
if type memory_record_fix_outcome &>/dev/null 2>&1; then
|
|
2121
|
+
if [[ "${TEST_PASSED:-}" == "true" ]]; then
|
|
2122
|
+
memory_record_fix_outcome "$_applied_fix_pattern" "true" "true" 2>/dev/null || true
|
|
2123
|
+
else
|
|
2124
|
+
memory_record_fix_outcome "$_applied_fix_pattern" "true" "false" 2>/dev/null || true
|
|
2125
|
+
fi
|
|
2126
|
+
fi
|
|
2127
|
+
_applied_fix_pattern=""
|
|
2128
|
+
fi
|
|
2129
|
+
|
|
1744
2130
|
# Audit agent (reviews implementer's work)
|
|
1745
2131
|
run_audit_agent
|
|
1746
2132
|
|
|
@@ -1751,6 +2137,7 @@ run_single_agent_loop() {
|
|
|
1751
2137
|
if guard_completion; then
|
|
1752
2138
|
STATUS="complete"
|
|
1753
2139
|
write_state
|
|
2140
|
+
write_progress
|
|
1754
2141
|
show_summary
|
|
1755
2142
|
return 0
|
|
1756
2143
|
fi
|
|
@@ -1771,6 +2158,7 @@ run_single_agent_loop() {
|
|
|
1771
2158
|
$summary
|
|
1772
2159
|
"
|
|
1773
2160
|
write_state
|
|
2161
|
+
write_progress
|
|
1774
2162
|
|
|
1775
2163
|
# Update heartbeat
|
|
1776
2164
|
"$SCRIPT_DIR/sw-heartbeat.sh" write "${PIPELINE_JOB_ID:-loop-$$}" \
|
|
@@ -1799,9 +2187,77 @@ HUMAN FEEDBACK (received after iteration $ITERATION): $human_msg"
|
|
|
1799
2187
|
|
|
1800
2188
|
# Write final state after loop exits
|
|
1801
2189
|
write_state
|
|
2190
|
+
write_progress
|
|
1802
2191
|
show_summary
|
|
1803
2192
|
}
|
|
1804
2193
|
|
|
2194
|
+
# ─── Session Restart Wrapper ─────────────────────────────────────────────────
|
|
2195
|
+
|
|
2196
|
+
run_loop_with_restarts() {
|
|
2197
|
+
while true; do
|
|
2198
|
+
local loop_exit=0
|
|
2199
|
+
run_single_agent_loop || loop_exit=$?
|
|
2200
|
+
|
|
2201
|
+
# If completed successfully or no restarts configured, exit
|
|
2202
|
+
if [[ "$STATUS" == "complete" ]]; then
|
|
2203
|
+
return 0
|
|
2204
|
+
fi
|
|
2205
|
+
if [[ "$MAX_RESTARTS" -le 0 ]]; then
|
|
2206
|
+
return "$loop_exit"
|
|
2207
|
+
fi
|
|
2208
|
+
if [[ "$RESTART_COUNT" -ge "$MAX_RESTARTS" ]]; then
|
|
2209
|
+
warn "Max restarts ($MAX_RESTARTS) reached — stopping"
|
|
2210
|
+
return "$loop_exit"
|
|
2211
|
+
fi
|
|
2212
|
+
# Hard cap safety net
|
|
2213
|
+
if [[ "$RESTART_COUNT" -ge 5 ]]; then
|
|
2214
|
+
warn "Hard restart cap (5) reached — stopping"
|
|
2215
|
+
return "$loop_exit"
|
|
2216
|
+
fi
|
|
2217
|
+
|
|
2218
|
+
# Check if tests are still failing (worth restarting)
|
|
2219
|
+
if [[ "${TEST_PASSED:-}" == "true" ]]; then
|
|
2220
|
+
info "Tests passing but loop incomplete — restarting session"
|
|
2221
|
+
else
|
|
2222
|
+
info "Tests failing and loop exhausted — restarting with fresh context"
|
|
2223
|
+
fi
|
|
2224
|
+
|
|
2225
|
+
RESTART_COUNT=$(( RESTART_COUNT + 1 ))
|
|
2226
|
+
if type emit_event &>/dev/null 2>&1; then
|
|
2227
|
+
emit_event "loop.restart" "restart=$RESTART_COUNT" "max=$MAX_RESTARTS" "iteration=$ITERATION"
|
|
2228
|
+
fi
|
|
2229
|
+
info "Session restart ${RESTART_COUNT}/${MAX_RESTARTS} — resetting iteration counter"
|
|
2230
|
+
|
|
2231
|
+
# Reset ALL iteration-level state for the new session
|
|
2232
|
+
# SESSION_RESTART tells run_single_agent_loop to skip init/resume
|
|
2233
|
+
SESSION_RESTART=true
|
|
2234
|
+
ITERATION=0
|
|
2235
|
+
CONSECUTIVE_FAILURES=0
|
|
2236
|
+
EXTENSION_COUNT=0
|
|
2237
|
+
STATUS="running"
|
|
2238
|
+
LOG_ENTRIES=""
|
|
2239
|
+
TEST_PASSED=""
|
|
2240
|
+
TEST_OUTPUT=""
|
|
2241
|
+
TEST_LOG_FILE=""
|
|
2242
|
+
# Reset GOAL to original — prevent unbounded growth from memory/human injections
|
|
2243
|
+
GOAL="$ORIGINAL_GOAL"
|
|
2244
|
+
|
|
2245
|
+
# Archive old artifacts so they don't get overwritten or pollute new session
|
|
2246
|
+
local restart_archive="$LOG_DIR/restart-${RESTART_COUNT}"
|
|
2247
|
+
mkdir -p "$restart_archive"
|
|
2248
|
+
for old_log in "$LOG_DIR"/iteration-*.log "$LOG_DIR"/tests-iter-*.log; do
|
|
2249
|
+
[[ -f "$old_log" ]] && mv "$old_log" "$restart_archive/" 2>/dev/null || true
|
|
2250
|
+
done
|
|
2251
|
+
# Archive progress.md and error-summary.json from previous session
|
|
2252
|
+
[[ -f "$LOG_DIR/progress.md" ]] && cp "$LOG_DIR/progress.md" "$restart_archive/progress.md" 2>/dev/null || true
|
|
2253
|
+
[[ -f "$LOG_DIR/error-summary.json" ]] && mv "$LOG_DIR/error-summary.json" "$restart_archive/" 2>/dev/null || true
|
|
2254
|
+
|
|
2255
|
+
write_state
|
|
2256
|
+
|
|
2257
|
+
sleep 2
|
|
2258
|
+
done
|
|
2259
|
+
}
|
|
2260
|
+
|
|
1805
2261
|
# ─── Main: Entry Point ───────────────────────────────────────────────────────
|
|
1806
2262
|
|
|
1807
2263
|
main() {
|
|
@@ -1815,7 +2271,7 @@ main() {
|
|
|
1815
2271
|
launch_multi_agent
|
|
1816
2272
|
show_summary
|
|
1817
2273
|
else
|
|
1818
|
-
|
|
2274
|
+
run_loop_with_restarts
|
|
1819
2275
|
fi
|
|
1820
2276
|
}
|
|
1821
2277
|
|