shipwright-cli 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/code-reviewer.md +2 -0
- package/.claude/agents/devops-engineer.md +2 -0
- package/.claude/agents/doc-fleet-agent.md +2 -0
- package/.claude/agents/pipeline-agent.md +2 -0
- package/.claude/agents/shell-script-specialist.md +2 -0
- package/.claude/agents/test-specialist.md +2 -0
- package/.claude/hooks/agent-crash-capture.sh +32 -0
- package/.claude/hooks/post-tool-use.sh +3 -2
- package/.claude/hooks/pre-tool-use.sh +35 -3
- package/README.md +22 -8
- package/claude-code/hooks/config-change.sh +18 -0
- package/claude-code/hooks/instructions-reloaded.sh +7 -0
- package/claude-code/hooks/worktree-create.sh +25 -0
- package/claude-code/hooks/worktree-remove.sh +20 -0
- package/config/code-constitution.json +130 -0
- package/config/defaults.json +25 -2
- package/config/policy.json +1 -1
- package/dashboard/middleware/auth.ts +134 -0
- package/dashboard/middleware/constants.ts +21 -0
- package/dashboard/public/index.html +8 -6
- package/dashboard/public/styles.css +176 -97
- package/dashboard/routes/auth.ts +38 -0
- package/dashboard/server.ts +117 -25
- package/dashboard/services/config.ts +26 -0
- package/dashboard/services/db.ts +118 -0
- package/dashboard/src/canvas/pixel-agent.ts +298 -0
- package/dashboard/src/canvas/pixel-sprites.ts +440 -0
- package/dashboard/src/canvas/shipyard-effects.ts +367 -0
- package/dashboard/src/canvas/shipyard-scene.ts +616 -0
- package/dashboard/src/canvas/submarine-layout.ts +267 -0
- package/dashboard/src/components/header.ts +8 -7
- package/dashboard/src/core/api.ts +5 -0
- package/dashboard/src/core/router.ts +1 -0
- package/dashboard/src/design/submarine-theme.ts +253 -0
- package/dashboard/src/main.ts +2 -0
- package/dashboard/src/types/api.ts +12 -1
- package/dashboard/src/views/activity.ts +2 -1
- package/dashboard/src/views/metrics.ts +69 -1
- package/dashboard/src/views/shipyard.ts +39 -0
- package/dashboard/types/index.ts +166 -0
- package/docs/plans/2026-02-28-compound-audit-and-shipyard-design.md +186 -0
- package/docs/plans/2026-02-28-skipper-shipwright-implementation-plan.md +1182 -0
- package/docs/plans/2026-02-28-skipper-shipwright-integration-design.md +531 -0
- package/docs/plans/2026-03-01-ai-powered-skill-injection-design.md +298 -0
- package/docs/plans/2026-03-01-ai-powered-skill-injection-plan.md +1109 -0
- package/docs/plans/2026-03-01-capabilities-cleanup-plan.md +658 -0
- package/docs/plans/2026-03-01-clean-architecture-plan.md +924 -0
- package/docs/plans/2026-03-01-compound-audit-cascade-design.md +191 -0
- package/docs/plans/2026-03-01-compound-audit-cascade-plan.md +921 -0
- package/docs/plans/2026-03-01-deep-integration-plan.md +851 -0
- package/docs/plans/2026-03-01-pipeline-audit-trail-design.md +145 -0
- package/docs/plans/2026-03-01-pipeline-audit-trail-plan.md +770 -0
- package/docs/plans/2026-03-01-refined-depths-brand-design.md +382 -0
- package/docs/plans/2026-03-01-refined-depths-implementation.md +599 -0
- package/docs/plans/2026-03-01-skipper-kernel-integration-design.md +203 -0
- package/docs/plans/2026-03-01-unified-platform-design.md +272 -0
- package/docs/plans/2026-03-07-claude-code-feature-integration-design.md +189 -0
- package/docs/plans/2026-03-07-claude-code-feature-integration-plan.md +1165 -0
- package/docs/research/BACKLOG_QUICK_REFERENCE.md +352 -0
- package/docs/research/CUTTING_EDGE_RESEARCH_2026.md +546 -0
- package/docs/research/RESEARCH_INDEX.md +439 -0
- package/docs/research/RESEARCH_SOURCES.md +440 -0
- package/docs/research/RESEARCH_SUMMARY.txt +275 -0
- package/docs/superpowers/specs/2026-03-10-pipeline-quality-revolution-design.md +341 -0
- package/package.json +2 -2
- package/scripts/lib/adaptive-model.sh +427 -0
- package/scripts/lib/adaptive-timeout.sh +316 -0
- package/scripts/lib/audit-trail.sh +309 -0
- package/scripts/lib/auto-recovery.sh +471 -0
- package/scripts/lib/bandit-selector.sh +431 -0
- package/scripts/lib/bootstrap.sh +104 -2
- package/scripts/lib/causal-graph.sh +455 -0
- package/scripts/lib/compat.sh +126 -0
- package/scripts/lib/compound-audit.sh +337 -0
- package/scripts/lib/constitutional.sh +454 -0
- package/scripts/lib/context-budget.sh +359 -0
- package/scripts/lib/convergence.sh +594 -0
- package/scripts/lib/cost-optimizer.sh +634 -0
- package/scripts/lib/daemon-adaptive.sh +14 -2
- package/scripts/lib/daemon-dispatch.sh +106 -17
- package/scripts/lib/daemon-failure.sh +34 -4
- package/scripts/lib/daemon-patrol.sh +25 -4
- package/scripts/lib/daemon-poll-github.sh +361 -0
- package/scripts/lib/daemon-poll-health.sh +299 -0
- package/scripts/lib/daemon-poll.sh +27 -611
- package/scripts/lib/daemon-state.sh +119 -66
- package/scripts/lib/daemon-triage.sh +10 -0
- package/scripts/lib/dod-scorecard.sh +442 -0
- package/scripts/lib/error-actionability.sh +300 -0
- package/scripts/lib/formal-spec.sh +461 -0
- package/scripts/lib/helpers.sh +180 -5
- package/scripts/lib/intent-analysis.sh +409 -0
- package/scripts/lib/loop-convergence.sh +350 -0
- package/scripts/lib/loop-iteration.sh +682 -0
- package/scripts/lib/loop-progress.sh +48 -0
- package/scripts/lib/loop-restart.sh +185 -0
- package/scripts/lib/memory-effectiveness.sh +506 -0
- package/scripts/lib/mutation-executor.sh +352 -0
- package/scripts/lib/outcome-feedback.sh +521 -0
- package/scripts/lib/pipeline-cli.sh +336 -0
- package/scripts/lib/pipeline-commands.sh +1216 -0
- package/scripts/lib/pipeline-detection.sh +101 -3
- package/scripts/lib/pipeline-execution.sh +897 -0
- package/scripts/lib/pipeline-github.sh +28 -3
- package/scripts/lib/pipeline-intelligence-compound.sh +431 -0
- package/scripts/lib/pipeline-intelligence-scoring.sh +407 -0
- package/scripts/lib/pipeline-intelligence-skip.sh +181 -0
- package/scripts/lib/pipeline-intelligence.sh +104 -1138
- package/scripts/lib/pipeline-quality-bash-compat.sh +182 -0
- package/scripts/lib/pipeline-quality-checks.sh +17 -711
- package/scripts/lib/pipeline-quality-gates.sh +563 -0
- package/scripts/lib/pipeline-stages-build.sh +730 -0
- package/scripts/lib/pipeline-stages-delivery.sh +965 -0
- package/scripts/lib/pipeline-stages-intake.sh +1133 -0
- package/scripts/lib/pipeline-stages-monitor.sh +407 -0
- package/scripts/lib/pipeline-stages-review.sh +1022 -0
- package/scripts/lib/pipeline-stages.sh +161 -2901
- package/scripts/lib/pipeline-state.sh +36 -5
- package/scripts/lib/pipeline-util.sh +487 -0
- package/scripts/lib/policy-learner.sh +438 -0
- package/scripts/lib/process-reward.sh +493 -0
- package/scripts/lib/project-detect.sh +649 -0
- package/scripts/lib/quality-profile.sh +334 -0
- package/scripts/lib/recruit-commands.sh +885 -0
- package/scripts/lib/recruit-learning.sh +739 -0
- package/scripts/lib/recruit-roles.sh +648 -0
- package/scripts/lib/reward-aggregator.sh +458 -0
- package/scripts/lib/rl-optimizer.sh +362 -0
- package/scripts/lib/root-cause.sh +427 -0
- package/scripts/lib/scope-enforcement.sh +445 -0
- package/scripts/lib/session-restart.sh +493 -0
- package/scripts/lib/skill-memory.sh +300 -0
- package/scripts/lib/skill-registry.sh +775 -0
- package/scripts/lib/spec-driven.sh +476 -0
- package/scripts/lib/test-helpers.sh +18 -7
- package/scripts/lib/test-holdout.sh +429 -0
- package/scripts/lib/test-optimizer.sh +511 -0
- package/scripts/shipwright-file-suggest.sh +45 -0
- package/scripts/skills/adversarial-quality.md +61 -0
- package/scripts/skills/api-design.md +44 -0
- package/scripts/skills/architecture-design.md +50 -0
- package/scripts/skills/brainstorming.md +43 -0
- package/scripts/skills/data-pipeline.md +44 -0
- package/scripts/skills/deploy-safety.md +64 -0
- package/scripts/skills/documentation.md +38 -0
- package/scripts/skills/frontend-design.md +45 -0
- package/scripts/skills/generated/.gitkeep +0 -0
- package/scripts/skills/generated/_refinements/.gitkeep +0 -0
- package/scripts/skills/generated/_refinements/adversarial-quality.patch.md +3 -0
- package/scripts/skills/generated/_refinements/architecture-design.patch.md +3 -0
- package/scripts/skills/generated/_refinements/brainstorming.patch.md +3 -0
- package/scripts/skills/generated/cli-version-management.md +29 -0
- package/scripts/skills/generated/collection-system-validation.md +99 -0
- package/scripts/skills/generated/large-scale-c-refactoring-coordination.md +97 -0
- package/scripts/skills/generated/pattern-matching-similarity-scoring.md +195 -0
- package/scripts/skills/generated/test-parallelization-detection.md +65 -0
- package/scripts/skills/observability.md +79 -0
- package/scripts/skills/performance.md +48 -0
- package/scripts/skills/pr-quality.md +49 -0
- package/scripts/skills/product-thinking.md +43 -0
- package/scripts/skills/security-audit.md +49 -0
- package/scripts/skills/systematic-debugging.md +40 -0
- package/scripts/skills/testing-strategy.md +47 -0
- package/scripts/skills/two-stage-review.md +52 -0
- package/scripts/skills/validation-thoroughness.md +55 -0
- package/scripts/sw +9 -3
- package/scripts/sw-activity.sh +9 -8
- package/scripts/sw-adaptive.sh +8 -7
- package/scripts/sw-adversarial.sh +2 -1
- package/scripts/sw-architecture-enforcer.sh +3 -1
- package/scripts/sw-auth.sh +12 -2
- package/scripts/sw-autonomous.sh +5 -1
- package/scripts/sw-changelog.sh +4 -1
- package/scripts/sw-checkpoint.sh +2 -1
- package/scripts/sw-ci.sh +15 -6
- package/scripts/sw-cleanup.sh +4 -26
- package/scripts/sw-code-review.sh +45 -20
- package/scripts/sw-connect.sh +2 -1
- package/scripts/sw-context.sh +2 -1
- package/scripts/sw-cost.sh +107 -5
- package/scripts/sw-daemon.sh +71 -11
- package/scripts/sw-dashboard.sh +3 -1
- package/scripts/sw-db.sh +71 -20
- package/scripts/sw-decide.sh +8 -2
- package/scripts/sw-decompose.sh +360 -17
- package/scripts/sw-deps.sh +4 -1
- package/scripts/sw-developer-simulation.sh +4 -1
- package/scripts/sw-discovery.sh +378 -5
- package/scripts/sw-doc-fleet.sh +4 -1
- package/scripts/sw-docs-agent.sh +3 -1
- package/scripts/sw-docs.sh +2 -1
- package/scripts/sw-doctor.sh +453 -2
- package/scripts/sw-dora.sh +4 -1
- package/scripts/sw-durable.sh +12 -7
- package/scripts/sw-e2e-orchestrator.sh +17 -16
- package/scripts/sw-eventbus.sh +13 -4
- package/scripts/sw-evidence.sh +364 -12
- package/scripts/sw-feedback.sh +550 -9
- package/scripts/sw-fix.sh +20 -1
- package/scripts/sw-fleet-discover.sh +6 -2
- package/scripts/sw-fleet-viz.sh +9 -4
- package/scripts/sw-fleet.sh +5 -1
- package/scripts/sw-github-app.sh +18 -4
- package/scripts/sw-github-checks.sh +3 -2
- package/scripts/sw-github-deploy.sh +3 -2
- package/scripts/sw-github-graphql.sh +18 -7
- package/scripts/sw-guild.sh +5 -1
- package/scripts/sw-heartbeat.sh +5 -30
- package/scripts/sw-hello.sh +67 -0
- package/scripts/sw-hygiene.sh +10 -3
- package/scripts/sw-incident.sh +273 -5
- package/scripts/sw-init.sh +18 -2
- package/scripts/sw-instrument.sh +10 -2
- package/scripts/sw-intelligence.sh +44 -7
- package/scripts/sw-jira.sh +5 -1
- package/scripts/sw-launchd.sh +2 -1
- package/scripts/sw-linear.sh +4 -1
- package/scripts/sw-logs.sh +4 -1
- package/scripts/sw-loop.sh +436 -1076
- package/scripts/sw-memory.sh +357 -3
- package/scripts/sw-mission-control.sh +6 -1
- package/scripts/sw-model-router.sh +483 -27
- package/scripts/sw-otel.sh +15 -4
- package/scripts/sw-oversight.sh +14 -5
- package/scripts/sw-patrol-meta.sh +334 -0
- package/scripts/sw-pipeline-composer.sh +7 -1
- package/scripts/sw-pipeline-vitals.sh +12 -6
- package/scripts/sw-pipeline.sh +54 -2653
- package/scripts/sw-pm.sh +16 -8
- package/scripts/sw-pr-lifecycle.sh +2 -1
- package/scripts/sw-predictive.sh +17 -5
- package/scripts/sw-prep.sh +185 -2
- package/scripts/sw-ps.sh +5 -25
- package/scripts/sw-public-dashboard.sh +17 -4
- package/scripts/sw-quality.sh +14 -6
- package/scripts/sw-reaper.sh +8 -25
- package/scripts/sw-recruit.sh +156 -2303
- package/scripts/sw-regression.sh +19 -12
- package/scripts/sw-release-manager.sh +3 -1
- package/scripts/sw-release.sh +4 -1
- package/scripts/sw-remote.sh +3 -1
- package/scripts/sw-replay.sh +7 -1
- package/scripts/sw-retro.sh +158 -1
- package/scripts/sw-review-rerun.sh +3 -1
- package/scripts/sw-scale.sh +14 -5
- package/scripts/sw-security-audit.sh +6 -1
- package/scripts/sw-self-optimize.sh +173 -6
- package/scripts/sw-session.sh +9 -3
- package/scripts/sw-setup.sh +3 -1
- package/scripts/sw-stall-detector.sh +406 -0
- package/scripts/sw-standup.sh +15 -7
- package/scripts/sw-status.sh +3 -1
- package/scripts/sw-strategic.sh +14 -6
- package/scripts/sw-stream.sh +13 -4
- package/scripts/sw-swarm.sh +20 -7
- package/scripts/sw-team-stages.sh +13 -6
- package/scripts/sw-templates.sh +7 -31
- package/scripts/sw-testgen.sh +17 -6
- package/scripts/sw-tmux-pipeline.sh +4 -1
- package/scripts/sw-tmux-role-color.sh +2 -0
- package/scripts/sw-tmux-status.sh +1 -1
- package/scripts/sw-tmux.sh +37 -1
- package/scripts/sw-trace.sh +3 -1
- package/scripts/sw-tracker-github.sh +3 -0
- package/scripts/sw-tracker-jira.sh +3 -0
- package/scripts/sw-tracker-linear.sh +3 -0
- package/scripts/sw-tracker.sh +3 -1
- package/scripts/sw-triage.sh +3 -2
- package/scripts/sw-upgrade.sh +3 -1
- package/scripts/sw-ux.sh +5 -2
- package/scripts/sw-webhook.sh +5 -2
- package/scripts/sw-widgets.sh +9 -4
- package/scripts/sw-worktree.sh +15 -3
- package/scripts/test-skill-injection.sh +1233 -0
- package/templates/pipelines/autonomous.json +27 -3
- package/templates/pipelines/cost-aware.json +34 -8
- package/templates/pipelines/deployed.json +12 -0
- package/templates/pipelines/enterprise.json +12 -0
- package/templates/pipelines/fast.json +6 -0
- package/templates/pipelines/full.json +27 -3
- package/templates/pipelines/hotfix.json +6 -0
- package/templates/pipelines/standard.json +12 -0
- package/templates/pipelines/tdd.json +12 -0
package/scripts/sw-loop.sh
CHANGED
|
@@ -14,6 +14,7 @@ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
|
14
14
|
unset CLAUDECODE 2>/dev/null || true
|
|
15
15
|
# Ignore SIGHUP so tmux attach/detach doesn't kill long-running agent sessions
|
|
16
16
|
trap '' HUP
|
|
17
|
+
trap '' SIGPIPE
|
|
17
18
|
|
|
18
19
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
20
|
|
|
@@ -30,6 +31,42 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
30
31
|
if [[ -f "$SCRIPT_DIR/sw-db.sh" ]]; then
|
|
31
32
|
source "$SCRIPT_DIR/sw-db.sh" 2>/dev/null || true
|
|
32
33
|
fi
|
|
34
|
+
# Cross-pipeline discovery (learnings from other pipeline runs)
|
|
35
|
+
[[ -f "$SCRIPT_DIR/sw-discovery.sh" ]] && source "$SCRIPT_DIR/sw-discovery.sh" 2>/dev/null || true
|
|
36
|
+
# Source loop sub-modules for modular iteration management
|
|
37
|
+
[[ -f "$SCRIPT_DIR/lib/loop-iteration.sh" ]] && source "$SCRIPT_DIR/lib/loop-iteration.sh"
|
|
38
|
+
[[ -f "$SCRIPT_DIR/lib/loop-convergence.sh" ]] && source "$SCRIPT_DIR/lib/loop-convergence.sh"
|
|
39
|
+
[[ -f "$SCRIPT_DIR/lib/loop-restart.sh" ]] && source "$SCRIPT_DIR/lib/loop-restart.sh"
|
|
40
|
+
[[ -f "$SCRIPT_DIR/lib/loop-progress.sh" ]] && source "$SCRIPT_DIR/lib/loop-progress.sh"
|
|
41
|
+
# Intelligent session restart with enhanced briefings and cross-session tracking
|
|
42
|
+
[[ -f "$SCRIPT_DIR/lib/session-restart.sh" ]] && source "$SCRIPT_DIR/lib/session-restart.sh"
|
|
43
|
+
# Context window budget monitoring (issue #209)
|
|
44
|
+
# shellcheck source=lib/context-budget.sh
|
|
45
|
+
[[ -f "$SCRIPT_DIR/lib/context-budget.sh" ]] && source "$SCRIPT_DIR/lib/context-budget.sh" 2>/dev/null || true
|
|
46
|
+
# Convergence detection and scoring (issue #203)
|
|
47
|
+
[[ -f "$SCRIPT_DIR/lib/convergence.sh" ]] && source "$SCRIPT_DIR/lib/convergence.sh" 2>/dev/null || true
|
|
48
|
+
# Error actionability scoring and enhancement for better error context
|
|
49
|
+
# shellcheck source=lib/error-actionability.sh
|
|
50
|
+
[[ -f "$SCRIPT_DIR/lib/error-actionability.sh" ]] && source "$SCRIPT_DIR/lib/error-actionability.sh" 2>/dev/null || true
|
|
51
|
+
# Autonomous error recovery with model escalation
|
|
52
|
+
# shellcheck source=lib/auto-recovery.sh
|
|
53
|
+
[[ -f "$SCRIPT_DIR/lib/auto-recovery.sh" ]] && source "$SCRIPT_DIR/lib/auto-recovery.sh" 2>/dev/null || true
|
|
54
|
+
# Test execution optimization (issue #200)
|
|
55
|
+
# shellcheck source=lib/test-optimizer.sh
|
|
56
|
+
[[ -f "$SCRIPT_DIR/lib/test-optimizer.sh" ]] && source "$SCRIPT_DIR/lib/test-optimizer.sh" 2>/dev/null || true
|
|
57
|
+
# Audit trail for compliance-grade pipeline traceability
|
|
58
|
+
# shellcheck source=lib/audit-trail.sh
|
|
59
|
+
[[ -f "$SCRIPT_DIR/lib/audit-trail.sh" ]] && source "$SCRIPT_DIR/lib/audit-trail.sh" 2>/dev/null || true
|
|
60
|
+
# Process reward model for per-step iteration scoring (Phase 3)
|
|
61
|
+
# shellcheck source=lib/process-reward.sh
|
|
62
|
+
[[ -f "$SCRIPT_DIR/lib/process-reward.sh" ]] && source "$SCRIPT_DIR/lib/process-reward.sh" 2>/dev/null || true
|
|
63
|
+
# Cross-session reinforcement learning optimizer (Phase 7)
|
|
64
|
+
# shellcheck source=lib/rl-optimizer.sh
|
|
65
|
+
[[ -f "$SCRIPT_DIR/lib/rl-optimizer.sh" ]] && source "$SCRIPT_DIR/lib/rl-optimizer.sh" 2>/dev/null || true
|
|
66
|
+
# Autoresearch RL modules (Phase 8): reward aggregation, bandit selection, policy learning
|
|
67
|
+
[[ -f "$SCRIPT_DIR/lib/reward-aggregator.sh" ]] && source "$SCRIPT_DIR/lib/reward-aggregator.sh" 2>/dev/null || true
|
|
68
|
+
[[ -f "$SCRIPT_DIR/lib/bandit-selector.sh" ]] && source "$SCRIPT_DIR/lib/bandit-selector.sh" 2>/dev/null || true
|
|
69
|
+
[[ -f "$SCRIPT_DIR/lib/policy-learner.sh" ]] && source "$SCRIPT_DIR/lib/policy-learner.sh" 2>/dev/null || true
|
|
33
70
|
# Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
|
|
34
71
|
[[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
|
|
35
72
|
[[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
|
|
@@ -42,6 +79,7 @@ fi
|
|
|
42
79
|
if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
43
80
|
emit_event() {
|
|
44
81
|
local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
|
|
82
|
+
# shellcheck disable=SC2155
|
|
45
83
|
local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
|
|
46
84
|
while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
|
|
47
85
|
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
@@ -69,22 +107,27 @@ MAX_RESTARTS=$(_config_get_int "loop.max_restarts" 0 2>/dev/null || echo 0)
|
|
|
69
107
|
SESSION_RESTART=false
|
|
70
108
|
RESTART_COUNT=0
|
|
71
109
|
REPO_OVERRIDE=""
|
|
72
|
-
VERSION="3.
|
|
110
|
+
VERSION="3.3.0"
|
|
73
111
|
|
|
74
112
|
# ─── Token Tracking ─────────────────────────────────────────────────────────
|
|
75
113
|
LOOP_INPUT_TOKENS=0
|
|
76
114
|
LOOP_OUTPUT_TOKENS=0
|
|
77
115
|
LOOP_COST_MILLICENTS=0
|
|
78
116
|
|
|
79
|
-
# ─── Flexible Iteration Defaults
|
|
80
|
-
AUTO_EXTEND=true
|
|
81
|
-
EXTENSION_SIZE
|
|
82
|
-
MAX_EXTENSIONS
|
|
83
|
-
EXTENSION_COUNT=0
|
|
117
|
+
# ─── Flexible Iteration Defaults (all config-driven) ───────────────────────
|
|
118
|
+
AUTO_EXTEND=true
|
|
119
|
+
EXTENSION_SIZE=$(_smart_int "loop.extension_size" 5)
|
|
120
|
+
MAX_EXTENSIONS=$(_smart_int "loop.max_extensions" 3)
|
|
121
|
+
EXTENSION_COUNT=0
|
|
84
122
|
|
|
85
|
-
# ─── Circuit Breaker Defaults
|
|
86
|
-
CIRCUIT_BREAKER_THRESHOLD
|
|
87
|
-
MIN_PROGRESS_LINES
|
|
123
|
+
# ─── Circuit Breaker Defaults (config-driven) ─────────────────────────────
|
|
124
|
+
CIRCUIT_BREAKER_THRESHOLD=$(_smart_int "loop.circuit_breaker_threshold" 3)
|
|
125
|
+
MIN_PROGRESS_LINES=$(_smart_int "loop.min_progress_lines" 5)
|
|
126
|
+
|
|
127
|
+
# ─── Context Exhaustion Recovery ────────────────────────────────────────────────
|
|
128
|
+
CONTEXT_EXHAUSTION_PATTERNS="context.length.exceeded|maximum context length|context_length_exceeded|prompt is too long"
|
|
129
|
+
CONTEXT_RESTART_COUNT=0
|
|
130
|
+
CONTEXT_RESTART_LIMIT=$(_smart_int "loop.context_restart_limit" 2)
|
|
88
131
|
|
|
89
132
|
# ─── Audit & Quality Gate Defaults ───────────────────────────────────────────
|
|
90
133
|
AUDIT_ENABLED=false
|
|
@@ -95,6 +138,16 @@ AUDIT_RESULT=""
|
|
|
95
138
|
COMPLETION_REJECTED=false
|
|
96
139
|
QUALITY_GATE_PASSED=true
|
|
97
140
|
|
|
141
|
+
# ─── Multi-Test Defaults ──────────────────────────────────────────────────
|
|
142
|
+
ADDITIONAL_TEST_CMDS=() # Array of extra test commands (from --additional-test-cmds)
|
|
143
|
+
|
|
144
|
+
# ─── Context Budget ──────────────────────────────────────────────────────────
|
|
145
|
+
CONTEXT_BUDGET_CHARS="${CONTEXT_BUDGET_CHARS:-200000}" # Max prompt chars before trimming
|
|
146
|
+
|
|
147
|
+
# ─── Claude CLI Flags ─────────────────────────────────────────────────────────
|
|
148
|
+
EFFORT_LEVEL="${SW_EFFORT_LEVEL:-}"
|
|
149
|
+
FALLBACK_MODEL="${SW_FALLBACK_MODEL:-}" # Empty = no fallback flag (intelligent default)
|
|
150
|
+
|
|
98
151
|
# ─── Parse Arguments ──────────────────────────────────────────────────────────
|
|
99
152
|
show_help() {
|
|
100
153
|
echo -e "${CYAN}${BOLD}shipwright${RESET} ${DIM}v${VERSION}${RESET} — ${BOLD}Continuous Loop${RESET}"
|
|
@@ -109,7 +162,10 @@ show_help() {
|
|
|
109
162
|
echo -e " ${CYAN}--test-cmd${RESET} \"cmd\" Test command to run between iterations"
|
|
110
163
|
echo -e " ${CYAN}--fast-test-cmd${RESET} \"cmd\" Fast/subset test command (alternates with full)"
|
|
111
164
|
echo -e " ${CYAN}--fast-test-interval${RESET} N Run full tests every N iterations (default: 5)"
|
|
165
|
+
echo -e " ${CYAN}--additional-test-cmds${RESET} \"cmd\" Extra test command (repeatable)"
|
|
112
166
|
echo -e " ${CYAN}--model${RESET} MODEL Claude model to use (default: opus)"
|
|
167
|
+
echo -e " ${CYAN}--effort${RESET} low|medium|high Effort level for Claude reasoning (default: auto per stage)"
|
|
168
|
+
echo -e " ${CYAN}--fallback-model${RESET} MODEL Fallback model on rate limits (default: sonnet)"
|
|
113
169
|
echo -e " ${CYAN}--agents${RESET} N Number of parallel agents (default: 1)"
|
|
114
170
|
echo -e " ${CYAN}--roles${RESET} \"r1,r2,...\" Role per agent: builder,reviewer,tester,optimizer,docs,security"
|
|
115
171
|
echo -e " ${CYAN}--worktree${RESET} Use git worktrees for isolation (auto if agents > 1)"
|
|
@@ -183,6 +239,18 @@ while [[ $# -gt 0 ]]; do
|
|
|
183
239
|
shift 2
|
|
184
240
|
;;
|
|
185
241
|
--model=*) MODEL="${1#--model=}"; shift ;;
|
|
242
|
+
--effort)
|
|
243
|
+
EFFORT_LEVEL="${2:-}"
|
|
244
|
+
[[ -z "$EFFORT_LEVEL" ]] && { error "Missing value for --effort"; exit 1; }
|
|
245
|
+
shift 2
|
|
246
|
+
;;
|
|
247
|
+
--effort=*) EFFORT_LEVEL="${1#--effort=}"; shift ;;
|
|
248
|
+
--fallback-model)
|
|
249
|
+
FALLBACK_MODEL="${2:-}"
|
|
250
|
+
[[ -z "$FALLBACK_MODEL" ]] && { error "Missing value for --fallback-model"; exit 1; }
|
|
251
|
+
shift 2
|
|
252
|
+
;;
|
|
253
|
+
--fallback-model=*) FALLBACK_MODEL="${1#--fallback-model=}"; shift ;;
|
|
186
254
|
--agents)
|
|
187
255
|
AGENTS="${2:-}"
|
|
188
256
|
[[ -z "$AGENTS" ]] && { error "Missing value for --agents"; exit 1; }
|
|
@@ -233,6 +301,12 @@ while [[ $# -gt 0 ]]; do
|
|
|
233
301
|
shift 2
|
|
234
302
|
;;
|
|
235
303
|
--fast-test-interval=*) FAST_TEST_INTERVAL="${1#--fast-test-interval=}"; shift ;;
|
|
304
|
+
--additional-test-cmds)
|
|
305
|
+
ADDITIONAL_TEST_CMDS+=("${2:-}")
|
|
306
|
+
[[ -z "${2:-}" ]] && { error "Missing value for --additional-test-cmds"; exit 1; }
|
|
307
|
+
shift 2
|
|
308
|
+
;;
|
|
309
|
+
--additional-test-cmds=*) ADDITIONAL_TEST_CMDS+=("${1#--additional-test-cmds=}"); shift ;;
|
|
236
310
|
--max-restarts)
|
|
237
311
|
MAX_RESTARTS="${2:-}"
|
|
238
312
|
[[ -z "$MAX_RESTARTS" ]] && { error "Missing value for --max-restarts"; exit 1; }
|
|
@@ -270,6 +344,7 @@ done
|
|
|
270
344
|
|
|
271
345
|
# Auto-enable worktree for multi-agent
|
|
272
346
|
if [[ "$AGENTS" -gt 1 ]]; then
|
|
347
|
+
# shellcheck disable=SC2034
|
|
273
348
|
USE_WORKTREE=true
|
|
274
349
|
fi
|
|
275
350
|
|
|
@@ -306,6 +381,12 @@ if ! [[ "$MAX_RESTARTS" =~ ^[0-9]+$ ]]; then
|
|
|
306
381
|
exit 1
|
|
307
382
|
fi
|
|
308
383
|
|
|
384
|
+
# Validate effort level
|
|
385
|
+
if [[ -n "$EFFORT_LEVEL" ]] && [[ "$EFFORT_LEVEL" != "low" && "$EFFORT_LEVEL" != "medium" && "$EFFORT_LEVEL" != "high" ]]; then
|
|
386
|
+
error "--effort must be low, medium, or high (got: $EFFORT_LEVEL)"
|
|
387
|
+
exit 1
|
|
388
|
+
fi
|
|
389
|
+
|
|
309
390
|
# ─── Validate Inputs ─────────────────────────────────────────────────────────
|
|
310
391
|
|
|
311
392
|
if ! $RESUME && [[ -z "$GOAL" ]]; then
|
|
@@ -379,6 +460,16 @@ WORKTREE_DIR="$PROJECT_ROOT/.worktrees"
|
|
|
379
460
|
|
|
380
461
|
mkdir -p "$STATE_DIR" "$LOG_DIR"
|
|
381
462
|
|
|
463
|
+
# ─── Context Budget Initialization ────────────────────────────────────────────
|
|
464
|
+
# Initialize context window budget tracker (issue #209)
|
|
465
|
+
ARTIFACTS_DIR="${STATE_DIR}/pipeline-artifacts"
|
|
466
|
+
mkdir -p "$ARTIFACTS_DIR"
|
|
467
|
+
if type context_budget_init >/dev/null 2>&1; then
|
|
468
|
+
# Set total budget (default 800K, configurable via env/config)
|
|
469
|
+
CONTEXT_BUDGET="${CONTEXT_BUDGET_TOKENS:-800000}"
|
|
470
|
+
context_budget_init "$CONTEXT_BUDGET" "$ARTIFACTS_DIR" 2>/dev/null || true
|
|
471
|
+
fi
|
|
472
|
+
|
|
382
473
|
# ─── Adaptive Model Selection ────────────────────────────────────────────────
|
|
383
474
|
# Uses intelligence engine when available, falls back to defaults.
|
|
384
475
|
select_adaptive_model() {
|
|
@@ -502,16 +593,28 @@ _extract_text_from_json() {
|
|
|
502
593
|
local first_char
|
|
503
594
|
first_char=$(head -c1 "$json_file" 2>/dev/null || true)
|
|
504
595
|
|
|
505
|
-
# Case 2: Valid JSON array — extract
|
|
506
|
-
if [[ "$first_char" == "[" ]] && command -v jq >/dev/null 2>&1; then
|
|
596
|
+
# Case 2: Valid JSON (array or object) — extract text with jq
|
|
597
|
+
if [[ ("$first_char" == "[" || "$first_char" == "{") ]] && command -v jq >/dev/null 2>&1; then
|
|
507
598
|
local extracted
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
599
|
+
if [[ "$first_char" == "[" ]]; then
|
|
600
|
+
# Array: extract .result from last element
|
|
601
|
+
extracted=$(jq -r '.[-1].result // empty' "$json_file" 2>/dev/null) || true
|
|
602
|
+
if [[ -n "$extracted" ]]; then
|
|
603
|
+
echo "$extracted" > "$log_file"
|
|
604
|
+
return 0
|
|
605
|
+
fi
|
|
606
|
+
# Try .content fields
|
|
607
|
+
extracted=$(jq -r '.[].content // empty' "$json_file" 2>/dev/null | head -500) || true
|
|
608
|
+
else
|
|
609
|
+
# Object: extract .result directly
|
|
610
|
+
extracted=$(jq -r '.result // empty' "$json_file" 2>/dev/null) || true
|
|
611
|
+
if [[ -n "$extracted" ]]; then
|
|
612
|
+
echo "$extracted" > "$log_file"
|
|
613
|
+
return 0
|
|
614
|
+
fi
|
|
615
|
+
# Try .content field
|
|
616
|
+
extracted=$(jq -r '.content // empty' "$json_file" 2>/dev/null) || true
|
|
512
617
|
fi
|
|
513
|
-
# jq succeeded but result was null/empty — try .content or raw text
|
|
514
|
-
extracted=$(jq -r '.[].content // empty' "$json_file" 2>/dev/null | head -500) || true
|
|
515
618
|
if [[ -n "$extracted" ]]; then
|
|
516
619
|
echo "$extracted" > "$log_file"
|
|
517
620
|
return 0
|
|
@@ -522,7 +625,7 @@ _extract_text_from_json() {
|
|
|
522
625
|
return 0
|
|
523
626
|
fi
|
|
524
627
|
|
|
525
|
-
# Case 3: Looks like JSON but
|
|
628
|
+
# Case 3: Looks like JSON but jq is not available — can't parse, use raw
|
|
526
629
|
if [[ "$first_char" == "[" || "$first_char" == "{" ]]; then
|
|
527
630
|
warn "JSON output but jq not available — using raw output"
|
|
528
631
|
cp "$json_file" "$log_file"
|
|
@@ -543,6 +646,7 @@ write_loop_tokens() {
|
|
|
543
646
|
fi
|
|
544
647
|
local tmp_file
|
|
545
648
|
tmp_file=$(mktemp "${token_file}.XXXXXX" 2>/dev/null || mktemp)
|
|
649
|
+
# shellcheck disable=SC2064
|
|
546
650
|
trap "rm -f '$tmp_file'" RETURN
|
|
547
651
|
cat > "$tmp_file" <<TOKJSON
|
|
548
652
|
{"input_tokens":${LOOP_INPUT_TOKENS},"output_tokens":${LOOP_OUTPUT_TOKENS},"cost_usd":${cost_usd},"iterations":${ITERATION:-0}}
|
|
@@ -596,38 +700,8 @@ apply_adaptive_budget() {
|
|
|
596
700
|
ITERATION_LINES_CHANGED=""
|
|
597
701
|
VELOCITY_HISTORY=""
|
|
598
702
|
|
|
599
|
-
track_iteration_velocity() {
|
|
600
|
-
local changes
|
|
601
|
-
changes="$(git -C "$PROJECT_ROOT" diff --stat HEAD~1 2>/dev/null | tail -1 || echo "")"
|
|
602
|
-
local insertions
|
|
603
|
-
insertions="$(echo "$changes" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)"
|
|
604
|
-
ITERATION_LINES_CHANGED="${insertions:-0}"
|
|
605
|
-
if [[ -n "$VELOCITY_HISTORY" ]]; then
|
|
606
|
-
VELOCITY_HISTORY="${VELOCITY_HISTORY},${ITERATION_LINES_CHANGED}"
|
|
607
|
-
else
|
|
608
|
-
VELOCITY_HISTORY="${ITERATION_LINES_CHANGED}"
|
|
609
|
-
fi
|
|
610
|
-
}
|
|
611
703
|
|
|
612
704
|
# Compute average lines/iteration from recent history
|
|
613
|
-
compute_velocity_avg() {
|
|
614
|
-
if [[ -z "$VELOCITY_HISTORY" ]]; then
|
|
615
|
-
echo "0"
|
|
616
|
-
return 0
|
|
617
|
-
fi
|
|
618
|
-
local total=0 count=0
|
|
619
|
-
local IFS=','
|
|
620
|
-
local val
|
|
621
|
-
for val in $VELOCITY_HISTORY; do
|
|
622
|
-
total=$((total + val))
|
|
623
|
-
count=$((count + 1))
|
|
624
|
-
done
|
|
625
|
-
if [[ "$count" -gt 0 ]]; then
|
|
626
|
-
echo $((total / count))
|
|
627
|
-
else
|
|
628
|
-
echo "0"
|
|
629
|
-
fi
|
|
630
|
-
}
|
|
631
705
|
|
|
632
706
|
# ─── Timing Helpers ───────────────────────────────────────────────────────────
|
|
633
707
|
|
|
@@ -653,191 +727,10 @@ TEST_PASSED=""
|
|
|
653
727
|
TEST_OUTPUT=""
|
|
654
728
|
LOG_ENTRIES=""
|
|
655
729
|
|
|
656
|
-
initialize_state() {
|
|
657
|
-
ITERATION=0
|
|
658
|
-
CONSECUTIVE_FAILURES=0
|
|
659
|
-
TOTAL_COMMITS=0
|
|
660
|
-
START_EPOCH="$(now_epoch)"
|
|
661
|
-
STATUS="running"
|
|
662
|
-
LOG_ENTRIES=""
|
|
663
|
-
|
|
664
|
-
# Record starting commit for cumulative diff in quality gates
|
|
665
|
-
LOOP_START_COMMIT="$(git -C "$PROJECT_ROOT" rev-parse HEAD 2>/dev/null || echo "")"
|
|
666
|
-
|
|
667
|
-
write_state
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
resume_state() {
|
|
671
|
-
if [[ ! -f "$STATE_FILE" ]]; then
|
|
672
|
-
error "No state file found at $STATE_FILE"
|
|
673
|
-
echo -e " Start a new loop instead: ${DIM}shipwright loop \"<goal>\"${RESET}"
|
|
674
|
-
exit 1
|
|
675
|
-
fi
|
|
676
|
-
|
|
677
|
-
info "Resuming from $STATE_FILE"
|
|
678
730
|
|
|
679
|
-
# Save CLI values before parsing state (CLI takes precedence)
|
|
680
|
-
local cli_max_iterations="$MAX_ITERATIONS"
|
|
681
731
|
|
|
682
|
-
# Parse YAML front matter
|
|
683
|
-
local in_frontmatter=false
|
|
684
|
-
while IFS= read -r line; do
|
|
685
|
-
if [[ "$line" == "---" ]]; then
|
|
686
|
-
if $in_frontmatter; then
|
|
687
|
-
break
|
|
688
|
-
else
|
|
689
|
-
in_frontmatter=true
|
|
690
|
-
continue
|
|
691
|
-
fi
|
|
692
|
-
fi
|
|
693
|
-
if $in_frontmatter; then
|
|
694
|
-
case "$line" in
|
|
695
|
-
goal:*) [[ -z "$GOAL" ]] && GOAL="$(echo "${line#goal:}" | sed 's/^ *"//;s/" *$//')" ;;
|
|
696
|
-
iteration:*) ITERATION="$(echo "${line#iteration:}" | tr -d ' ')" ;;
|
|
697
|
-
max_iterations:*) MAX_ITERATIONS="$(echo "${line#max_iterations:}" | tr -d ' ')" ;;
|
|
698
|
-
status:*) STATUS="$(echo "${line#status:}" | tr -d ' ')" ;;
|
|
699
|
-
test_cmd:*) [[ -z "$TEST_CMD" ]] && TEST_CMD="$(echo "${line#test_cmd:}" | sed 's/^ *"//;s/" *$//')" ;;
|
|
700
|
-
model:*) MODEL="$(echo "${line#model:}" | tr -d ' ')" ;;
|
|
701
|
-
agents:*) AGENTS="$(echo "${line#agents:}" | tr -d ' ')" ;;
|
|
702
|
-
consecutive_failures:*) CONSECUTIVE_FAILURES="$(echo "${line#consecutive_failures:}" | tr -d ' ')" ;;
|
|
703
|
-
total_commits:*) TOTAL_COMMITS="$(echo "${line#total_commits:}" | tr -d ' ')" ;;
|
|
704
|
-
audit_enabled:*) AUDIT_ENABLED="$(echo "${line#audit_enabled:}" | tr -d ' ')" ;;
|
|
705
|
-
audit_agent_enabled:*) AUDIT_AGENT_ENABLED="$(echo "${line#audit_agent_enabled:}" | tr -d ' ')" ;;
|
|
706
|
-
quality_gates_enabled:*) QUALITY_GATES_ENABLED="$(echo "${line#quality_gates_enabled:}" | tr -d ' ')" ;;
|
|
707
|
-
dod_file:*) DOD_FILE="$(echo "${line#dod_file:}" | sed 's/^ *"//;s/" *$//')" ;;
|
|
708
|
-
auto_extend:*) AUTO_EXTEND="$(echo "${line#auto_extend:}" | tr -d ' ')" ;;
|
|
709
|
-
extension_count:*) EXTENSION_COUNT="$(echo "${line#extension_count:}" | tr -d ' ')" ;;
|
|
710
|
-
max_extensions:*) MAX_EXTENSIONS="$(echo "${line#max_extensions:}" | tr -d ' ')" ;;
|
|
711
|
-
esac
|
|
712
|
-
fi
|
|
713
|
-
done < "$STATE_FILE"
|
|
714
732
|
|
|
715
|
-
# CLI --max-iterations overrides state file
|
|
716
|
-
if $MAX_ITERATIONS_EXPLICIT; then
|
|
717
|
-
MAX_ITERATIONS="$cli_max_iterations"
|
|
718
|
-
fi
|
|
719
733
|
|
|
720
|
-
# Extract the log section (everything after ## Log)
|
|
721
|
-
LOG_ENTRIES="$(sed -n '/^## Log$/,$ { /^## Log$/d; p; }' "$STATE_FILE" 2>/dev/null || true)"
|
|
722
|
-
|
|
723
|
-
if [[ -z "$GOAL" ]]; then
|
|
724
|
-
error "Could not parse goal from state file."
|
|
725
|
-
exit 1
|
|
726
|
-
fi
|
|
727
|
-
|
|
728
|
-
if [[ "$STATUS" == "complete" ]]; then
|
|
729
|
-
warn "Previous loop completed. Start a new one or edit the state file."
|
|
730
|
-
exit 0
|
|
731
|
-
fi
|
|
732
|
-
|
|
733
|
-
# Reset circuit breaker on resume
|
|
734
|
-
CONSECUTIVE_FAILURES=0
|
|
735
|
-
START_EPOCH="$(now_epoch)"
|
|
736
|
-
STATUS="running"
|
|
737
|
-
|
|
738
|
-
# Set starting commit for cumulative diff (approximate: use earliest tracked commit)
|
|
739
|
-
if [[ -z "${LOOP_START_COMMIT:-}" ]]; then
|
|
740
|
-
LOOP_START_COMMIT="$(git -C "$PROJECT_ROOT" rev-list --max-parents=0 HEAD 2>/dev/null | tail -1 || echo "")"
|
|
741
|
-
fi
|
|
742
|
-
|
|
743
|
-
# If we hit max iterations before, warn user to extend
|
|
744
|
-
if [[ "$ITERATION" -ge "$MAX_ITERATIONS" ]] && ! $MAX_ITERATIONS_EXPLICIT; then
|
|
745
|
-
warn "Previous run stopped at iteration $ITERATION/$MAX_ITERATIONS."
|
|
746
|
-
echo -e " Extend with: ${DIM}shipwright loop --resume --max-iterations $(( MAX_ITERATIONS + 10 ))${RESET}"
|
|
747
|
-
exit 0
|
|
748
|
-
fi
|
|
749
|
-
|
|
750
|
-
# Restore Claude context for meaningful resume (source so exports persist to this shell)
|
|
751
|
-
if [[ -f "$SCRIPT_DIR/sw-checkpoint.sh" ]] && [[ -d "${PROJECT_ROOT:-}" ]]; then
|
|
752
|
-
source "$SCRIPT_DIR/sw-checkpoint.sh"
|
|
753
|
-
local _orig_pwd="$PWD"
|
|
754
|
-
cd "$PROJECT_ROOT" 2>/dev/null || true
|
|
755
|
-
if checkpoint_restore_context "build" 2>/dev/null; then
|
|
756
|
-
RESUMED_FROM_ITERATION="${RESTORED_ITERATION:-}"
|
|
757
|
-
RESUMED_MODIFIED="${RESTORED_MODIFIED:-}"
|
|
758
|
-
RESUMED_FINDINGS="${RESTORED_FINDINGS:-}"
|
|
759
|
-
RESUMED_TEST_OUTPUT="${RESTORED_TEST_OUTPUT:-}"
|
|
760
|
-
[[ -n "${RESTORED_ITERATION:-}" && "${RESTORED_ITERATION:-0}" -gt 0 ]] && info "Restored context from iteration ${RESTORED_ITERATION}"
|
|
761
|
-
fi
|
|
762
|
-
cd "$_orig_pwd" 2>/dev/null || true
|
|
763
|
-
fi
|
|
764
|
-
|
|
765
|
-
success "Resumed: iteration $ITERATION/$MAX_ITERATIONS"
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
write_state() {
|
|
769
|
-
local tmp_state="${STATE_FILE}.tmp.$$"
|
|
770
|
-
# Use printf instead of heredoc to avoid delimiter injection from GOAL
|
|
771
|
-
{
|
|
772
|
-
printf -- '---\n'
|
|
773
|
-
printf 'goal: "%s"\n' "$GOAL"
|
|
774
|
-
printf 'iteration: %s\n' "$ITERATION"
|
|
775
|
-
printf 'max_iterations: %s\n' "$MAX_ITERATIONS"
|
|
776
|
-
printf 'status: %s\n' "$STATUS"
|
|
777
|
-
printf 'test_cmd: "%s"\n' "$TEST_CMD"
|
|
778
|
-
printf 'model: %s\n' "$MODEL"
|
|
779
|
-
printf 'agents: %s\n' "$AGENTS"
|
|
780
|
-
printf 'started_at: %s\n' "$(now_iso)"
|
|
781
|
-
printf 'last_iteration_at: %s\n' "$(now_iso)"
|
|
782
|
-
printf 'consecutive_failures: %s\n' "$CONSECUTIVE_FAILURES"
|
|
783
|
-
printf 'total_commits: %s\n' "$TOTAL_COMMITS"
|
|
784
|
-
printf 'audit_enabled: %s\n' "$AUDIT_ENABLED"
|
|
785
|
-
printf 'audit_agent_enabled: %s\n' "$AUDIT_AGENT_ENABLED"
|
|
786
|
-
printf 'quality_gates_enabled: %s\n' "$QUALITY_GATES_ENABLED"
|
|
787
|
-
printf 'dod_file: "%s"\n' "$DOD_FILE"
|
|
788
|
-
printf 'auto_extend: %s\n' "$AUTO_EXTEND"
|
|
789
|
-
printf 'extension_count: %s\n' "$EXTENSION_COUNT"
|
|
790
|
-
printf 'max_extensions: %s\n' "$MAX_EXTENSIONS"
|
|
791
|
-
printf -- '---\n\n'
|
|
792
|
-
printf '## Log\n'
|
|
793
|
-
printf '%s\n' "$LOG_ENTRIES"
|
|
794
|
-
} > "$tmp_state"
|
|
795
|
-
if ! mv "$tmp_state" "$STATE_FILE" 2>/dev/null; then
|
|
796
|
-
warn "Failed to write state file: $STATE_FILE"
|
|
797
|
-
fi
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
write_progress() {
|
|
801
|
-
local progress_file="$LOG_DIR/progress.md"
|
|
802
|
-
local recent_commits
|
|
803
|
-
recent_commits=$(git -C "$PROJECT_ROOT" log --oneline -5 2>/dev/null || echo "(no commits)")
|
|
804
|
-
local changed_files
|
|
805
|
-
changed_files=$(git -C "$PROJECT_ROOT" diff --name-only HEAD~3 2>/dev/null | head -20 || echo "(none)")
|
|
806
|
-
local last_error=""
|
|
807
|
-
local prev_test_log="$LOG_DIR/tests-iter-${ITERATION}.log"
|
|
808
|
-
if [[ -f "$prev_test_log" ]] && [[ "${TEST_PASSED:-}" == "false" ]]; then
|
|
809
|
-
last_error=$(tail -10 "$prev_test_log" 2>/dev/null || true)
|
|
810
|
-
fi
|
|
811
|
-
|
|
812
|
-
# Use printf to avoid heredoc delimiter injection from GOAL content
|
|
813
|
-
local tmp_progress="${progress_file}.tmp.$$"
|
|
814
|
-
{
|
|
815
|
-
printf '# Session Progress (Auto-Generated)\n\n'
|
|
816
|
-
printf '## Goal\n%s\n\n' "${GOAL}"
|
|
817
|
-
printf '## Status\n'
|
|
818
|
-
printf -- '- Iteration: %s/%s\n' "${ITERATION}" "${MAX_ITERATIONS}"
|
|
819
|
-
printf -- '- Session restart: %s/%s\n' "${RESTART_COUNT:-0}" "${MAX_RESTARTS:-0}"
|
|
820
|
-
printf -- '- Tests passing: %s\n' "${TEST_PASSED:-unknown}"
|
|
821
|
-
printf -- '- Status: %s\n\n' "${STATUS:-running}"
|
|
822
|
-
printf '## Recent Commits\n%s\n\n' "${recent_commits}"
|
|
823
|
-
printf '## Changed Files\n%s\n\n' "${changed_files}"
|
|
824
|
-
if [[ -n "$last_error" ]]; then
|
|
825
|
-
printf '## Last Error\n%s\n\n' "$last_error"
|
|
826
|
-
fi
|
|
827
|
-
printf '## Timestamp\n%s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
828
|
-
} > "$tmp_progress" 2>/dev/null
|
|
829
|
-
mv "$tmp_progress" "$progress_file" 2>/dev/null || rm -f "$tmp_progress" 2>/dev/null
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
append_log_entry() {
|
|
833
|
-
local entry="$1"
|
|
834
|
-
if [[ -n "$LOG_ENTRIES" ]]; then
|
|
835
|
-
LOG_ENTRIES="${LOG_ENTRIES}
|
|
836
|
-
${entry}"
|
|
837
|
-
else
|
|
838
|
-
LOG_ENTRIES="$entry"
|
|
839
|
-
fi
|
|
840
|
-
}
|
|
841
734
|
|
|
842
735
|
# ─── Semantic Validation for Claude Output ─────────────────────────────────────
|
|
843
736
|
# Validates changed files before commit to catch syntax errors and API error leakage.
|
|
@@ -960,155 +853,12 @@ git_auto_commit() {
|
|
|
960
853
|
|
|
961
854
|
# ─── Fatal Error Detection ────────────────────────────────────────────────────
|
|
962
855
|
|
|
963
|
-
check_fatal_error() {
|
|
964
|
-
local log_file="$1"
|
|
965
|
-
local cli_exit_code="${2:-0}"
|
|
966
|
-
[[ -f "$log_file" ]] || return 1
|
|
967
|
-
|
|
968
|
-
# Known fatal error patterns from Claude CLI / Anthropic API
|
|
969
|
-
local fatal_patterns="Invalid API key|invalid_api_key|authentication_error|API key expired"
|
|
970
|
-
fatal_patterns="${fatal_patterns}|rate_limit_error|overloaded_error|billing"
|
|
971
|
-
fatal_patterns="${fatal_patterns}|Could not resolve host|connection refused|ECONNREFUSED"
|
|
972
|
-
fatal_patterns="${fatal_patterns}|ANTHROPIC_API_KEY.*not set|No API key"
|
|
973
|
-
|
|
974
|
-
if grep -qiE "$fatal_patterns" "$log_file" 2>/dev/null; then
|
|
975
|
-
local match
|
|
976
|
-
match=$(grep -iE "$fatal_patterns" "$log_file" 2>/dev/null | head -1 | cut -c1-120)
|
|
977
|
-
error "Fatal CLI error: $match"
|
|
978
|
-
return 0 # fatal error detected
|
|
979
|
-
fi
|
|
980
|
-
|
|
981
|
-
# Non-zero exit + tiny output = likely CLI crash
|
|
982
|
-
if [[ "$cli_exit_code" -ne 0 ]]; then
|
|
983
|
-
local line_count
|
|
984
|
-
line_count=$(grep -cv '^$' "$log_file" 2>/dev/null || true)
|
|
985
|
-
line_count="${line_count:-0}"
|
|
986
|
-
if [[ "$line_count" -lt 3 ]]; then
|
|
987
|
-
local content
|
|
988
|
-
content=$(head -3 "$log_file" 2>/dev/null | cut -c1-120)
|
|
989
|
-
error "CLI exited $cli_exit_code with minimal output: $content"
|
|
990
|
-
return 0
|
|
991
|
-
fi
|
|
992
|
-
fi
|
|
993
|
-
|
|
994
|
-
return 1 # no fatal error
|
|
995
|
-
}
|
|
996
856
|
|
|
997
857
|
# ─── Progress & Circuit Breaker ───────────────────────────────────────────────
|
|
998
858
|
|
|
999
|
-
check_progress() {
|
|
1000
|
-
local changes
|
|
1001
|
-
# Exclude loop bookkeeping files — only count real code changes as progress
|
|
1002
|
-
changes="$(git -C "$PROJECT_ROOT" diff --stat HEAD~1 \
|
|
1003
|
-
-- . ':!.claude/loop-state.md' ':!.claude/pipeline-state.md' \
|
|
1004
|
-
':!**/progress.md' ':!**/error-summary.json' \
|
|
1005
|
-
2>/dev/null | tail -1 || echo "")"
|
|
1006
|
-
local insertions
|
|
1007
|
-
insertions="$(echo "$changes" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)"
|
|
1008
|
-
if [[ "${insertions:-0}" -lt "$MIN_PROGRESS_LINES" ]]; then
|
|
1009
|
-
return 1 # No meaningful progress
|
|
1010
|
-
fi
|
|
1011
|
-
return 0
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
check_completion() {
|
|
1015
|
-
local log_file="$1"
|
|
1016
|
-
grep -q "LOOP_COMPLETE" "$log_file" 2>/dev/null
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
check_circuit_breaker() {
|
|
1020
|
-
# Vitals-driven circuit breaker (preferred over static threshold)
|
|
1021
|
-
if type pipeline_compute_vitals >/dev/null 2>&1 && type pipeline_health_verdict >/dev/null 2>&1; then
|
|
1022
|
-
local _vitals_json _verdict
|
|
1023
|
-
local _loop_state="${STATE_FILE:-}"
|
|
1024
|
-
local _loop_artifacts="${ARTIFACTS_DIR:-}"
|
|
1025
|
-
local _loop_issue="${ISSUE_NUMBER:-}"
|
|
1026
|
-
_vitals_json=$(pipeline_compute_vitals "$_loop_state" "$_loop_artifacts" "$_loop_issue" 2>/dev/null) || true
|
|
1027
|
-
if [[ -n "$_vitals_json" && "$_vitals_json" != "{}" ]]; then
|
|
1028
|
-
_verdict=$(echo "$_vitals_json" | jq -r '.verdict // "continue"' 2>/dev/null || echo "continue")
|
|
1029
|
-
if [[ "$_verdict" == "abort" ]]; then
|
|
1030
|
-
local _health_score
|
|
1031
|
-
_health_score=$(echo "$_vitals_json" | jq -r '.health_score // 0' 2>/dev/null || echo "0")
|
|
1032
|
-
error "Vitals circuit breaker: health score ${_health_score}/100 — aborting (${CONSECUTIVE_FAILURES} stagnant iterations)"
|
|
1033
|
-
STATUS="circuit_breaker"
|
|
1034
|
-
return 1
|
|
1035
|
-
fi
|
|
1036
|
-
# Vitals say continue/warn/intervene — don't trip circuit breaker yet
|
|
1037
|
-
if [[ "$_verdict" == "continue" || "$_verdict" == "warn" ]]; then
|
|
1038
|
-
return 0
|
|
1039
|
-
fi
|
|
1040
|
-
fi
|
|
1041
|
-
fi
|
|
1042
|
-
|
|
1043
|
-
# Fallback: static threshold circuit breaker
|
|
1044
|
-
if [[ "$CONSECUTIVE_FAILURES" -ge "$CIRCUIT_BREAKER_THRESHOLD" ]]; then
|
|
1045
|
-
error "Circuit breaker tripped: ${CIRCUIT_BREAKER_THRESHOLD} consecutive iterations with no meaningful progress."
|
|
1046
|
-
STATUS="circuit_breaker"
|
|
1047
|
-
return 1
|
|
1048
|
-
fi
|
|
1049
|
-
return 0
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
check_max_iterations() {
|
|
1053
|
-
if [[ "$ITERATION" -le "$MAX_ITERATIONS" ]]; then
|
|
1054
|
-
return 0
|
|
1055
|
-
fi
|
|
1056
|
-
|
|
1057
|
-
# Hit the cap — check if we should auto-extend
|
|
1058
|
-
if ! $AUTO_EXTEND || [[ "$EXTENSION_COUNT" -ge "$MAX_EXTENSIONS" ]]; then
|
|
1059
|
-
if [[ "$EXTENSION_COUNT" -ge "$MAX_EXTENSIONS" ]]; then
|
|
1060
|
-
warn "Hard cap reached: ${EXTENSION_COUNT} extensions applied (max ${MAX_EXTENSIONS})."
|
|
1061
|
-
fi
|
|
1062
|
-
warn "Max iterations ($MAX_ITERATIONS) reached."
|
|
1063
|
-
STATUS="max_iterations"
|
|
1064
|
-
return 1
|
|
1065
|
-
fi
|
|
1066
859
|
|
|
1067
|
-
# Checkpoint audit: is there meaningful progress worth extending for?
|
|
1068
|
-
echo -e "\n ${CYAN}${BOLD}▸ Checkpoint${RESET} — max iterations ($MAX_ITERATIONS) reached, evaluating progress..."
|
|
1069
|
-
|
|
1070
|
-
local should_extend=false
|
|
1071
|
-
local extension_reason=""
|
|
1072
|
-
|
|
1073
|
-
# Check 1: recent meaningful progress (not stuck)
|
|
1074
|
-
if [[ "${CONSECUTIVE_FAILURES:-0}" -lt 2 ]]; then
|
|
1075
|
-
# Check 2: agent hasn't signaled completion (if it did, guard_completion handles it)
|
|
1076
|
-
local last_log="$LOG_DIR/iteration-$(( ITERATION - 1 )).log"
|
|
1077
|
-
if [[ -f "$last_log" ]] && ! grep -q "LOOP_COMPLETE" "$last_log" 2>/dev/null; then
|
|
1078
|
-
should_extend=true
|
|
1079
|
-
extension_reason="work in progress with recent progress"
|
|
1080
|
-
fi
|
|
1081
|
-
fi
|
|
1082
|
-
|
|
1083
|
-
# Check 3: if quality gates or tests are failing, extend to let agent fix them
|
|
1084
|
-
if [[ "$TEST_PASSED" == "false" ]] || ! $QUALITY_GATE_PASSED; then
|
|
1085
|
-
should_extend=true
|
|
1086
|
-
extension_reason="quality gates or tests not yet passing"
|
|
1087
|
-
fi
|
|
1088
860
|
|
|
1089
|
-
if $should_extend; then
|
|
1090
|
-
# Scale extension size by velocity — good progress earns more iterations
|
|
1091
|
-
local velocity_avg
|
|
1092
|
-
velocity_avg="$(compute_velocity_avg)"
|
|
1093
|
-
local effective_extension="$EXTENSION_SIZE"
|
|
1094
|
-
if [[ "$velocity_avg" -gt 20 ]]; then
|
|
1095
|
-
# High velocity: grant more iterations
|
|
1096
|
-
effective_extension=$(( EXTENSION_SIZE + 3 ))
|
|
1097
|
-
elif [[ "$velocity_avg" -lt 5 ]]; then
|
|
1098
|
-
# Low velocity: grant fewer iterations
|
|
1099
|
-
effective_extension=$(( EXTENSION_SIZE > 2 ? EXTENSION_SIZE - 2 : 1 ))
|
|
1100
|
-
fi
|
|
1101
|
-
EXTENSION_COUNT=$(( EXTENSION_COUNT + 1 ))
|
|
1102
|
-
MAX_ITERATIONS=$(( MAX_ITERATIONS + effective_extension ))
|
|
1103
|
-
echo -e " ${GREEN}✓${RESET} Auto-extending: +${effective_extension} iterations (now ${MAX_ITERATIONS} max, extension ${EXTENSION_COUNT}/${MAX_EXTENSIONS})"
|
|
1104
|
-
echo -e " ${DIM}Reason: ${extension_reason} | velocity: ~${velocity_avg} lines/iter${RESET}"
|
|
1105
|
-
return 0
|
|
1106
|
-
fi
|
|
1107
861
|
|
|
1108
|
-
warn "Max iterations reached — no recent progress detected."
|
|
1109
|
-
STATUS="max_iterations"
|
|
1110
|
-
return 1
|
|
1111
|
-
}
|
|
1112
862
|
|
|
1113
863
|
# ─── Failure Diagnosis ─────────────────────────────────────────────────────────
|
|
1114
864
|
# Pattern-based root-cause classification for smarter retries (no Claude needed).
|
|
@@ -1153,7 +903,7 @@ diagnose_failure() {
|
|
|
1153
903
|
fi
|
|
1154
904
|
|
|
1155
905
|
# Check if we've seen this diagnosis before in this session
|
|
1156
|
-
local diagnosis_file="${LOG_DIR
|
|
906
|
+
local diagnosis_file="${LOG_DIR}/diagnoses.txt"
|
|
1157
907
|
local repeat_count=0
|
|
1158
908
|
if [[ -f "$diagnosis_file" ]]; then
|
|
1159
909
|
repeat_count=$(grep -c "^${diagnosis}$" "$diagnosis_file" 2>/dev/null || true)
|
|
@@ -1221,7 +971,7 @@ INSTRUCTION: This error has occurred $repeat_count times. The previous approach
|
|
|
1221
971
|
# ─── Test Gate ────────────────────────────────────────────────────────────────
|
|
1222
972
|
|
|
1223
973
|
run_test_gate() {
|
|
1224
|
-
if [[ -z "$TEST_CMD" ]]; then
|
|
974
|
+
if [[ -z "$TEST_CMD" ]] && [[ ${#ADDITIONAL_TEST_CMDS[@]} -eq 0 ]]; then
|
|
1225
975
|
TEST_PASSED=""
|
|
1226
976
|
TEST_OUTPUT=""
|
|
1227
977
|
return
|
|
@@ -1241,24 +991,91 @@ run_test_gate() {
|
|
|
1241
991
|
fi
|
|
1242
992
|
fi
|
|
1243
993
|
|
|
1244
|
-
local
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
if
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
994
|
+
local all_passed=true
|
|
995
|
+
local test_results="[]"
|
|
996
|
+
local combined_output=""
|
|
997
|
+
local test_timeout="${SW_TEST_TIMEOUT:-900}"
|
|
998
|
+
|
|
999
|
+
# Run primary test command
|
|
1000
|
+
if [[ -n "$active_test_cmd" ]]; then
|
|
1001
|
+
local test_log="$LOG_DIR/tests-iter-${ITERATION}.log"
|
|
1002
|
+
TEST_LOG_FILE="$test_log"
|
|
1003
|
+
echo -e " ${DIM}Running ${test_mode} tests...${RESET}"
|
|
1004
|
+
|
|
1005
|
+
local test_wrapper="$active_test_cmd"
|
|
1006
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
1007
|
+
test_wrapper="timeout ${test_timeout} bash -c $(printf '%q' "$active_test_cmd")"
|
|
1008
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
1009
|
+
test_wrapper="gtimeout ${test_timeout} bash -c $(printf '%q' "$active_test_cmd")"
|
|
1010
|
+
fi
|
|
1011
|
+
|
|
1012
|
+
local start_ts exit_code=0
|
|
1013
|
+
start_ts=$(date +%s)
|
|
1014
|
+
bash -c "$test_wrapper" > "$test_log" 2>&1 || exit_code=$?
|
|
1015
|
+
local duration=$(( $(date +%s) - start_ts ))
|
|
1016
|
+
|
|
1017
|
+
if command -v jq >/dev/null 2>&1; then
|
|
1018
|
+
test_results=$(echo "$test_results" | jq --arg cmd "$active_test_cmd" \
|
|
1019
|
+
--argjson exit "$exit_code" --argjson dur "$duration" \
|
|
1020
|
+
'. + [{"command": $cmd, "exit_code": $exit, "duration_s": $dur}]')
|
|
1021
|
+
fi
|
|
1022
|
+
|
|
1023
|
+
[[ "$exit_code" -ne 0 ]] && all_passed=false
|
|
1024
|
+
combined_output+="$(cat "$test_log" 2>/dev/null)"$'\n'
|
|
1025
|
+
fi
|
|
1026
|
+
|
|
1027
|
+
# Run additional test commands (discovered or explicit)
|
|
1028
|
+
# Mid-build discovery: find test files created since loop start
|
|
1029
|
+
local mid_build_cmds=()
|
|
1030
|
+
if [[ -n "${LOOP_START_COMMIT:-}" ]] && type detect_created_test_files >/dev/null 2>&1; then
|
|
1031
|
+
while IFS= read -r _cmd; do
|
|
1032
|
+
[[ -n "$_cmd" ]] && mid_build_cmds+=("$_cmd")
|
|
1033
|
+
done < <(detect_created_test_files "$LOOP_START_COMMIT" 2>/dev/null || true)
|
|
1034
|
+
fi
|
|
1035
|
+
local all_extra=("${ADDITIONAL_TEST_CMDS[@]+"${ADDITIONAL_TEST_CMDS[@]}"}" "${mid_build_cmds[@]+"${mid_build_cmds[@]}"}")
|
|
1036
|
+
|
|
1037
|
+
for extra_cmd in "${all_extra[@]+"${all_extra[@]}"}"; do
|
|
1038
|
+
[[ -z "$extra_cmd" ]] && continue
|
|
1039
|
+
local extra_log="${LOG_DIR}/tests-extra-iter-${ITERATION}.log"
|
|
1040
|
+
echo -e " ${DIM}Running additional: ${extra_cmd}${RESET}"
|
|
1041
|
+
|
|
1042
|
+
local extra_wrapper="$extra_cmd"
|
|
1043
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
1044
|
+
extra_wrapper="timeout ${test_timeout} bash -c $(printf '%q' "$extra_cmd")"
|
|
1045
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
1046
|
+
extra_wrapper="gtimeout ${test_timeout} bash -c $(printf '%q' "$extra_cmd")"
|
|
1047
|
+
fi
|
|
1048
|
+
|
|
1049
|
+
local start_ts exit_code=0
|
|
1050
|
+
start_ts=$(date +%s)
|
|
1051
|
+
bash -c "$extra_wrapper" >> "$extra_log" 2>&1 || exit_code=$?
|
|
1052
|
+
local duration=$(( $(date +%s) - start_ts ))
|
|
1053
|
+
|
|
1054
|
+
if command -v jq >/dev/null 2>&1; then
|
|
1055
|
+
test_results=$(echo "$test_results" | jq --arg cmd "$extra_cmd" \
|
|
1056
|
+
--argjson exit "$exit_code" --argjson dur "$duration" \
|
|
1057
|
+
'. + [{"command": $cmd, "exit_code": $exit, "duration_s": $dur}]')
|
|
1058
|
+
fi
|
|
1059
|
+
|
|
1060
|
+
[[ "$exit_code" -ne 0 ]] && all_passed=false
|
|
1061
|
+
combined_output+="$(cat "$extra_log" 2>/dev/null)"$'\n'
|
|
1062
|
+
done
|
|
1063
|
+
|
|
1064
|
+
# Write structured test evidence
|
|
1065
|
+
if command -v jq >/dev/null 2>&1; then
|
|
1066
|
+
echo "$test_results" > "${LOG_DIR}/test-evidence-iter-${ITERATION}.json"
|
|
1067
|
+
fi
|
|
1068
|
+
|
|
1069
|
+
# Audit: emit test gate event
|
|
1070
|
+
if type audit_emit >/dev/null 2>&1; then
|
|
1071
|
+
local cmd_count=0
|
|
1072
|
+
command -v jq >/dev/null 2>&1 && cmd_count=$(echo "$test_results" | jq 'length' 2>/dev/null || echo 0)
|
|
1073
|
+
audit_emit "loop.test_gate" "iteration=$ITERATION" "commands=$cmd_count" \
|
|
1074
|
+
"all_passed=$all_passed" "evidence_path=test-evidence-iter-${ITERATION}.json" || true
|
|
1261
1075
|
fi
|
|
1076
|
+
|
|
1077
|
+
TEST_PASSED=$all_passed
|
|
1078
|
+
TEST_OUTPUT="$(echo "$combined_output" | tail -50)"
|
|
1262
1079
|
}
|
|
1263
1080
|
|
|
1264
1081
|
write_error_summary() {
|
|
@@ -1349,7 +1166,18 @@ run_audit_agent() {
|
|
|
1349
1166
|
|
|
1350
1167
|
# Include verified test status so auditor doesn't have to guess
|
|
1351
1168
|
local test_context=""
|
|
1352
|
-
|
|
1169
|
+
local evidence_file="${LOG_DIR}/test-evidence-iter-${ITERATION}.json"
|
|
1170
|
+
if [[ -f "$evidence_file" ]] && command -v jq >/dev/null 2>&1; then
|
|
1171
|
+
local cmd_count total_cmds evidence_detail
|
|
1172
|
+
cmd_count=$(jq 'length' "$evidence_file" 2>/dev/null || echo 0)
|
|
1173
|
+
total_cmds=$(jq -r '[.[].command] | join(", ")' "$evidence_file" 2>/dev/null || echo "unknown")
|
|
1174
|
+
evidence_detail=$(jq -r '.[] | "- \(.command): exit \(.exit_code) (\(.duration_s)s)"' "$evidence_file" 2>/dev/null || echo "")
|
|
1175
|
+
test_context="## Verified Test Status (from harness, not from agent)
|
|
1176
|
+
Test commands run: ${cmd_count} (${total_cmds})
|
|
1177
|
+
${evidence_detail}
|
|
1178
|
+
Overall: $(if [[ "${TEST_PASSED:-}" == "true" ]]; then echo "ALL PASSING"; else echo "FAILING"; fi)"
|
|
1179
|
+
elif [[ -n "$TEST_CMD" ]]; then
|
|
1180
|
+
# Fallback to existing boolean
|
|
1353
1181
|
if [[ "${TEST_PASSED:-}" == "true" ]]; then
|
|
1354
1182
|
test_context="## Verified Test Status (from harness, not from agent)
|
|
1355
1183
|
Tests: ALL PASSING (command: ${TEST_CMD})"
|
|
@@ -1403,6 +1231,12 @@ AUDIT_PROMPT
|
|
|
1403
1231
|
audit_flags+=("--dangerously-skip-permissions")
|
|
1404
1232
|
fi
|
|
1405
1233
|
|
|
1234
|
+
# Use structured output for machine-parseable audit results
|
|
1235
|
+
local schema_file="${SCRIPT_DIR}/../schemas/audit-result.json"
|
|
1236
|
+
if [[ -f "$schema_file" ]]; then
|
|
1237
|
+
audit_flags+=("--json-schema" "$(cat "$schema_file")")
|
|
1238
|
+
fi
|
|
1239
|
+
|
|
1406
1240
|
local exit_code=0
|
|
1407
1241
|
claude -p "$audit_prompt" "${audit_flags[@]}" > "$audit_log" 2>&1 || exit_code=$?
|
|
1408
1242
|
|
|
@@ -1439,9 +1273,11 @@ run_quality_gates() {
|
|
|
1439
1273
|
gate_failures+=("uncommitted changes present")
|
|
1440
1274
|
fi
|
|
1441
1275
|
|
|
1442
|
-
# Gate 3: No TODO/FIXME/HACK/XXX in new code
|
|
1276
|
+
# Gate 3: No TODO/FIXME/HACK/XXX in new source code
|
|
1277
|
+
# Exclude .claude/, docs/plans/, and markdown files (which legitimately contain task markers)
|
|
1443
1278
|
local todo_count
|
|
1444
|
-
todo_count="$(git -C "$PROJECT_ROOT" diff HEAD~1
|
|
1279
|
+
todo_count="$(git -C "$PROJECT_ROOT" diff HEAD~1 -- ':!.claude/' ':!docs/plans/' ':!*.md' 2>/dev/null \
|
|
1280
|
+
| grep -cE '^\+.*(TODO|FIXME|HACK|XXX)' || true)"
|
|
1445
1281
|
todo_count="${todo_count:-0}"
|
|
1446
1282
|
if [[ "${todo_count:-0}" -gt 0 ]]; then
|
|
1447
1283
|
gate_failures+=("${todo_count} TODO/FIXME/HACK/XXX markers in new code")
|
|
@@ -1658,420 +1494,14 @@ HOLISTIC_PROMPT
|
|
|
1658
1494
|
}
|
|
1659
1495
|
|
|
1660
1496
|
# ─── Context Window Management ───────────────────────────────────────────────
|
|
1661
|
-
# Prevents prompt from exceeding Claude's context limit (~200K tokens).
|
|
1662
|
-
# Trims least-critical sections first when over budget.
|
|
1663
|
-
|
|
1664
|
-
CONTEXT_BUDGET_CHARS="${CONTEXT_BUDGET_CHARS:-180000}" # ~45K tokens at 4 chars/token
|
|
1665
|
-
|
|
1666
|
-
manage_context_window() {
|
|
1667
|
-
local prompt="$1"
|
|
1668
|
-
local budget="${CONTEXT_BUDGET_CHARS}"
|
|
1669
|
-
local current_len=${#prompt}
|
|
1670
|
-
|
|
1671
|
-
if [[ "$current_len" -le "$budget" ]]; then
|
|
1672
|
-
echo "$prompt"
|
|
1673
|
-
return
|
|
1674
|
-
fi
|
|
1675
|
-
|
|
1676
|
-
# Over budget — progressively trim sections (least important first)
|
|
1677
|
-
local trimmed="$prompt"
|
|
1678
|
-
|
|
1679
|
-
# 1. Trim DORA/Performance baselines (least critical for code generation)
|
|
1680
|
-
if [[ "${#trimmed}" -gt "$budget" ]]; then
|
|
1681
|
-
trimmed=$(echo "$trimmed" | awk '/^## Performance Baselines/{skip=1; next} skip && /^## [^#]/{skip=0} !skip{print}')
|
|
1682
|
-
fi
|
|
1683
|
-
|
|
1684
|
-
# 2. Trim file hotspots to top 5
|
|
1685
|
-
if [[ "${#trimmed}" -gt "$budget" ]]; then
|
|
1686
|
-
trimmed=$(echo "$trimmed" | awk '/## File Hotspots/{p=1; c=0} p && /^- /{c++; if(c>5) next} {print}')
|
|
1687
|
-
fi
|
|
1688
|
-
|
|
1689
|
-
# 3. Trim git log to last 10 entries
|
|
1690
|
-
if [[ "${#trimmed}" -gt "$budget" ]]; then
|
|
1691
|
-
trimmed=$(echo "$trimmed" | awk '/## Recent Git Activity/{p=1; c=0} p && /^[a-f0-9]/{c++; if(c>10) next} {print}')
|
|
1692
|
-
fi
|
|
1693
|
-
|
|
1694
|
-
# 4. Truncate memory context to first 20K chars
|
|
1695
|
-
if [[ "${#trimmed}" -gt "$budget" ]]; then
|
|
1696
|
-
trimmed=$(echo "$trimmed" | awk -v max=20000 '
|
|
1697
|
-
/## Memory Context/{mem=1; skip_rest=0; chars=0; print; next}
|
|
1698
|
-
mem && /^## [^#]/{mem=0; print; next}
|
|
1699
|
-
mem{chars+=length($0)+1; if(chars>max){print "... (memory truncated for context budget)"; skip_rest=1; mem=0; next}}
|
|
1700
|
-
skip_rest && /^## [^#]/{skip_rest=0; print; next}
|
|
1701
|
-
skip_rest{next}
|
|
1702
|
-
{print}
|
|
1703
|
-
')
|
|
1704
|
-
fi
|
|
1705
|
-
|
|
1706
|
-
# 5. Truncate test output to last 50 lines
|
|
1707
|
-
if [[ "${#trimmed}" -gt "$budget" ]]; then
|
|
1708
|
-
trimmed=$(echo "$trimmed" | awk '
|
|
1709
|
-
/## Test Results/{found=1; buf=""; print; next}
|
|
1710
|
-
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}
|
|
1711
|
-
found{buf=buf $0 "\n"; next}
|
|
1712
|
-
{print}
|
|
1713
|
-
')
|
|
1714
|
-
fi
|
|
1715
|
-
|
|
1716
|
-
# 6. Last resort: hard truncate with notice
|
|
1717
|
-
if [[ "${#trimmed}" -gt "$budget" ]]; then
|
|
1718
|
-
trimmed="${trimmed:0:$budget}
|
|
1719
|
-
|
|
1720
|
-
... [CONTEXT TRUNCATED: prompt exceeded ${budget} char budget. Focus on the goal and most recent errors.]"
|
|
1721
|
-
fi
|
|
1722
|
-
|
|
1723
|
-
# Log the trimming
|
|
1724
|
-
local final_len=${#trimmed}
|
|
1725
|
-
if [[ "$final_len" -lt "$current_len" ]]; then
|
|
1726
|
-
warn "Context trimmed from ${current_len} to ${final_len} chars (budget: ${budget})"
|
|
1727
|
-
emit_event "loop.context_trimmed" "original=$current_len" "trimmed=$final_len" "budget=$budget" 2>/dev/null || true
|
|
1728
|
-
fi
|
|
1729
|
-
|
|
1730
|
-
echo "$trimmed"
|
|
1731
|
-
}
|
|
1732
1497
|
|
|
1733
1498
|
# ─── Prompt Composition ──────────────────────────────────────────────────────
|
|
1734
|
-
|
|
1735
|
-
compose_prompt() {
|
|
1736
|
-
local recent_log
|
|
1737
|
-
# Get last 3 iteration summaries from log entries
|
|
1738
|
-
recent_log="$(echo "$LOG_ENTRIES" | tail -15)"
|
|
1739
|
-
if [[ -z "$recent_log" ]]; then
|
|
1740
|
-
recent_log="(first iteration — no previous progress)"
|
|
1741
|
-
fi
|
|
1742
|
-
|
|
1743
|
-
local git_log
|
|
1744
|
-
git_log="$(git_recent_log)"
|
|
1745
|
-
|
|
1746
|
-
local test_section
|
|
1747
|
-
if [[ -z "$TEST_CMD" ]]; then
|
|
1748
|
-
test_section="No test command configured."
|
|
1749
|
-
elif [[ -z "$TEST_PASSED" ]]; then
|
|
1750
|
-
test_section="No test results yet (first iteration). Test command: $TEST_CMD"
|
|
1751
|
-
elif $TEST_PASSED; then
|
|
1752
|
-
test_section="$TEST_OUTPUT"
|
|
1753
|
-
else
|
|
1754
|
-
test_section="TESTS FAILED — fix these before proceeding:
|
|
1755
|
-
$TEST_OUTPUT"
|
|
1756
|
-
fi
|
|
1757
|
-
|
|
1758
|
-
# Structured error context (machine-readable)
|
|
1759
|
-
local error_summary_section=""
|
|
1760
|
-
local error_json="$LOG_DIR/error-summary.json"
|
|
1761
|
-
if [[ -f "$error_json" ]]; then
|
|
1762
|
-
local err_count err_lines
|
|
1763
|
-
err_count=$(jq -r '.error_count // 0' "$error_json" 2>/dev/null || echo "0")
|
|
1764
|
-
err_lines=$(jq -r '.error_lines[]? // empty' "$error_json" 2>/dev/null | head -10 || true)
|
|
1765
|
-
if [[ "$err_count" -gt 0 ]] && [[ -n "$err_lines" ]]; then
|
|
1766
|
-
error_summary_section="## Structured Error Summary (${err_count} errors detected)
|
|
1767
|
-
${err_lines}
|
|
1768
|
-
|
|
1769
|
-
Fix these specific errors. Each line above is one distinct error from the test output."
|
|
1770
|
-
fi
|
|
1771
|
-
fi
|
|
1772
|
-
|
|
1773
|
-
# Build audit sections (captured before heredoc to avoid nested heredoc issues)
|
|
1774
|
-
local audit_section
|
|
1775
|
-
audit_section="$(compose_audit_section)"
|
|
1776
|
-
local audit_feedback_section
|
|
1777
|
-
audit_feedback_section="$(compose_audit_feedback_section)"
|
|
1778
|
-
local rejection_notice_section
|
|
1779
|
-
rejection_notice_section="$(compose_rejection_notice_section)"
|
|
1780
|
-
|
|
1781
|
-
# Memory context injection (failure patterns + past learnings)
|
|
1782
|
-
local memory_section=""
|
|
1783
|
-
if type memory_inject_context >/dev/null 2>&1; then
|
|
1784
|
-
memory_section="$(memory_inject_context "build" 2>/dev/null || true)"
|
|
1785
|
-
elif [[ -f "$SCRIPT_DIR/sw-memory.sh" ]]; then
|
|
1786
|
-
memory_section="$("$SCRIPT_DIR/sw-memory.sh" inject build 2>/dev/null || true)"
|
|
1787
|
-
fi
|
|
1788
|
-
|
|
1789
|
-
# DORA baselines for context
|
|
1790
|
-
local dora_section=""
|
|
1791
|
-
if type memory_get_dora_baseline >/dev/null 2>&1; then
|
|
1792
|
-
local dora_json
|
|
1793
|
-
dora_json="$(memory_get_dora_baseline 7 2>/dev/null || echo "{}")"
|
|
1794
|
-
local dora_total
|
|
1795
|
-
dora_total=$(echo "$dora_json" | jq -r '.total // 0' 2>/dev/null || echo "0")
|
|
1796
|
-
if [[ "$dora_total" -gt 0 ]]; then
|
|
1797
|
-
local dora_df dora_cfr
|
|
1798
|
-
dora_df=$(echo "$dora_json" | jq -r '.deploy_freq // 0' 2>/dev/null || echo "0")
|
|
1799
|
-
dora_cfr=$(echo "$dora_json" | jq -r '.cfr // 0' 2>/dev/null || echo "0")
|
|
1800
|
-
dora_section="## Performance Baselines (Last 7 Days)
|
|
1801
|
-
- Deploy frequency: ${dora_df}/week
|
|
1802
|
-
- Change failure rate: ${dora_cfr}%
|
|
1803
|
-
- Total pipeline runs: ${dora_total}"
|
|
1804
|
-
fi
|
|
1805
|
-
fi
|
|
1806
|
-
|
|
1807
|
-
# Append mid-loop memory refresh if available
|
|
1808
|
-
local memory_refresh_file="$LOG_DIR/memory-refresh-$(( ITERATION - 1 )).txt"
|
|
1809
|
-
if [[ -f "$memory_refresh_file" ]]; then
|
|
1810
|
-
memory_section="${memory_section}
|
|
1811
|
-
|
|
1812
|
-
## Fresh Context (from iteration $(( ITERATION - 1 )) analysis)
|
|
1813
|
-
$(cat "$memory_refresh_file")"
|
|
1814
|
-
fi
|
|
1815
|
-
|
|
1816
|
-
# GitHub intelligence context (gated by availability)
|
|
1817
|
-
local intelligence_section=""
|
|
1818
|
-
if [[ "${NO_GITHUB:-}" != "true" ]]; then
|
|
1819
|
-
# File hotspots — top 5 most-changed files
|
|
1820
|
-
if type gh_file_change_frequency >/dev/null 2>&1; then
|
|
1821
|
-
local hotspots
|
|
1822
|
-
hotspots=$(gh_file_change_frequency 2>/dev/null | head -5 || true)
|
|
1823
|
-
if [[ -n "$hotspots" ]]; then
|
|
1824
|
-
intelligence_section="${intelligence_section}
|
|
1825
|
-
## File Hotspots (most frequently changed)
|
|
1826
|
-
${hotspots}"
|
|
1827
|
-
fi
|
|
1828
|
-
fi
|
|
1829
|
-
|
|
1830
|
-
# CODEOWNERS context
|
|
1831
|
-
if type gh_codeowners >/dev/null 2>&1; then
|
|
1832
|
-
local owners
|
|
1833
|
-
owners=$(gh_codeowners 2>/dev/null | head -10 || true)
|
|
1834
|
-
if [[ -n "$owners" ]]; then
|
|
1835
|
-
intelligence_section="${intelligence_section}
|
|
1836
|
-
## Code Owners
|
|
1837
|
-
${owners}"
|
|
1838
|
-
fi
|
|
1839
|
-
fi
|
|
1840
|
-
|
|
1841
|
-
# Active security alerts
|
|
1842
|
-
if type gh_security_alerts >/dev/null 2>&1; then
|
|
1843
|
-
local alerts
|
|
1844
|
-
alerts=$(gh_security_alerts 2>/dev/null | head -5 || true)
|
|
1845
|
-
if [[ -n "$alerts" ]]; then
|
|
1846
|
-
intelligence_section="${intelligence_section}
|
|
1847
|
-
## Active Security Alerts
|
|
1848
|
-
${alerts}"
|
|
1849
|
-
fi
|
|
1850
|
-
fi
|
|
1851
|
-
fi
|
|
1852
|
-
|
|
1853
|
-
# Architecture rules (from intelligence layer)
|
|
1854
|
-
local repo_hash
|
|
1855
|
-
repo_hash=$(echo -n "$(pwd)" | shasum -a 256 2>/dev/null | cut -c1-12 || echo "unknown")
|
|
1856
|
-
local arch_file="${HOME}/.shipwright/memory/${repo_hash}/architecture.json"
|
|
1857
|
-
if [[ -f "$arch_file" ]]; then
|
|
1858
|
-
local arch_rules
|
|
1859
|
-
arch_rules=$(jq -r '.rules[]? // empty' "$arch_file" 2>/dev/null | head -10 || true)
|
|
1860
|
-
if [[ -n "$arch_rules" ]]; then
|
|
1861
|
-
intelligence_section="${intelligence_section}
|
|
1862
|
-
## Architecture Rules
|
|
1863
|
-
${arch_rules}"
|
|
1864
|
-
fi
|
|
1865
|
-
fi
|
|
1866
|
-
|
|
1867
|
-
# Coverage baseline
|
|
1868
|
-
local coverage_file="${HOME}/.shipwright/baselines/${repo_hash}/coverage.json"
|
|
1869
|
-
if [[ -f "$coverage_file" ]]; then
|
|
1870
|
-
local coverage_pct
|
|
1871
|
-
coverage_pct=$(jq -r '.coverage_percent // empty' "$coverage_file" 2>/dev/null || true)
|
|
1872
|
-
if [[ -n "$coverage_pct" ]]; then
|
|
1873
|
-
intelligence_section="${intelligence_section}
|
|
1874
|
-
## Coverage Baseline
|
|
1875
|
-
Current coverage: ${coverage_pct}% — do not decrease this."
|
|
1876
|
-
fi
|
|
1877
|
-
fi
|
|
1878
|
-
|
|
1879
|
-
# Error classification from last failure
|
|
1880
|
-
local error_log=".claude/pipeline-artifacts/error-log.jsonl"
|
|
1881
|
-
if [[ -f "$error_log" ]]; then
|
|
1882
|
-
local last_error
|
|
1883
|
-
last_error=$(tail -1 "$error_log" 2>/dev/null | jq -r '"Type: \(.type), Exit: \(.exit_code), Error: \(.error | split("\n") | first)"' 2>/dev/null || true)
|
|
1884
|
-
if [[ -n "$last_error" ]]; then
|
|
1885
|
-
intelligence_section="${intelligence_section}
|
|
1886
|
-
## Last Error Context
|
|
1887
|
-
${last_error}"
|
|
1888
|
-
fi
|
|
1889
|
-
fi
|
|
1890
|
-
|
|
1891
|
-
# Stuckness detection — compare last 3 iteration outputs
|
|
1892
|
-
local stuckness_section=""
|
|
1893
|
-
stuckness_section="$(detect_stuckness)"
|
|
1894
|
-
local _stuck_ret=$?
|
|
1895
|
-
local stuckness_detected=false
|
|
1896
|
-
[[ "$_stuck_ret" -eq 0 ]] && stuckness_detected=true
|
|
1897
|
-
|
|
1898
|
-
# Strategy exploration when stuck — append alternative strategy to GOAL
|
|
1899
|
-
if [[ "$stuckness_detected" == "true" ]]; then
|
|
1900
|
-
local last_error diagnosis
|
|
1901
|
-
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)
|
|
1902
|
-
[[ -z "$last_error" || "$last_error" == "null" ]] && last_error="unknown"
|
|
1903
|
-
diagnosis="${STUCKNESS_DIAGNOSIS:-}"
|
|
1904
|
-
local alt_strategy
|
|
1905
|
-
alt_strategy=$(explore_alternative_strategy "$last_error" "${ITERATION:-0}" "$diagnosis")
|
|
1906
|
-
GOAL="${GOAL}
|
|
1907
|
-
|
|
1908
|
-
${alt_strategy}"
|
|
1909
|
-
|
|
1910
|
-
# Handle model escalation
|
|
1911
|
-
if [[ "${ESCALATE_MODEL:-}" == "true" ]]; then
|
|
1912
|
-
if [[ -f "$SCRIPT_DIR/sw-model-router.sh" ]]; then
|
|
1913
|
-
source "$SCRIPT_DIR/sw-model-router.sh" 2>/dev/null || true
|
|
1914
|
-
fi
|
|
1915
|
-
if type escalate_model &>/dev/null; then
|
|
1916
|
-
MODEL=$(escalate_model "${MODEL:-sonnet}")
|
|
1917
|
-
info "Escalated to model: $MODEL"
|
|
1918
|
-
fi
|
|
1919
|
-
unset ESCALATE_MODEL
|
|
1920
|
-
fi
|
|
1921
|
-
fi
|
|
1922
|
-
|
|
1923
|
-
# Session restart context — inject previous session progress
|
|
1924
|
-
local restart_section=""
|
|
1925
|
-
if [[ "$SESSION_RESTART" == "true" ]] && [[ -f "$LOG_DIR/progress.md" ]]; then
|
|
1926
|
-
restart_section="## Previous Session Progress
|
|
1927
|
-
$(cat "$LOG_DIR/progress.md")
|
|
1928
|
-
|
|
1929
|
-
You are starting a FRESH session after the previous one exhausted its iterations.
|
|
1930
|
-
Read the progress above and continue from where it left off. Do NOT repeat work already done."
|
|
1931
|
-
fi
|
|
1932
|
-
|
|
1933
|
-
# Resume-from-checkpoint context — reconstruct Claude context for meaningful resume
|
|
1934
|
-
local resume_section=""
|
|
1935
|
-
if [[ -n "${RESUMED_FROM_ITERATION:-}" && "${RESUMED_FROM_ITERATION:-0}" -gt 0 ]]; then
|
|
1936
|
-
local _test_tail=" (none recorded)"
|
|
1937
|
-
[[ -n "${RESUMED_TEST_OUTPUT:-}" ]] && _test_tail="$(echo "$RESUMED_TEST_OUTPUT" | tail -20)"
|
|
1938
|
-
resume_section="## RESUMING FROM ITERATION ${RESUMED_FROM_ITERATION}
|
|
1939
|
-
|
|
1940
|
-
Continue from where you left off. Do NOT repeat work already done.
|
|
1941
|
-
|
|
1942
|
-
Previous work modified these files:
|
|
1943
|
-
${RESUMED_MODIFIED:- (none recorded)}
|
|
1944
|
-
|
|
1945
|
-
Previous findings/errors from earlier iterations:
|
|
1946
|
-
${RESUMED_FINDINGS:- (none recorded)}
|
|
1947
|
-
|
|
1948
|
-
Last test output (fix any failures, tail):
|
|
1949
|
-
${_test_tail}
|
|
1950
|
-
|
|
1951
|
-
---
|
|
1952
|
-
"
|
|
1953
|
-
# Clear after first use so we don't keep injecting on every iteration
|
|
1954
|
-
RESUMED_FROM_ITERATION=""
|
|
1955
|
-
RESUMED_MODIFIED=""
|
|
1956
|
-
RESUMED_FINDINGS=""
|
|
1957
|
-
RESUMED_TEST_OUTPUT=""
|
|
1958
|
-
fi
|
|
1959
|
-
|
|
1960
|
-
# Build cumulative progress summary showing all iterations' work
|
|
1961
|
-
local cumulative_section=""
|
|
1962
|
-
if [[ -n "${LOOP_START_COMMIT:-}" ]] && [[ "$ITERATION" -gt 1 ]]; then
|
|
1963
|
-
local cum_stat
|
|
1964
|
-
cum_stat="$(git -C "$PROJECT_ROOT" diff --stat "${LOOP_START_COMMIT}..HEAD" 2>/dev/null | tail -1 || true)"
|
|
1965
|
-
if [[ -n "$cum_stat" ]]; then
|
|
1966
|
-
cumulative_section="## Cumulative Progress (all iterations combined)
|
|
1967
|
-
${cum_stat}
|
|
1968
|
-
"
|
|
1969
|
-
fi
|
|
1970
|
-
fi
|
|
1971
|
-
|
|
1972
|
-
cat <<PROMPT
|
|
1973
|
-
You are an autonomous coding agent on iteration ${ITERATION}/${MAX_ITERATIONS} of a continuous loop.
|
|
1974
|
-
${resume_section}
|
|
1975
|
-
## Your Goal
|
|
1976
|
-
${GOAL}
|
|
1977
|
-
|
|
1978
|
-
${cumulative_section}
|
|
1979
|
-
## Current Progress
|
|
1980
|
-
${recent_log}
|
|
1981
|
-
|
|
1982
|
-
## Recent Git Activity
|
|
1983
|
-
${git_log}
|
|
1984
|
-
|
|
1985
|
-
## Test Results (Previous Iteration)
|
|
1986
|
-
${test_section}
|
|
1987
|
-
|
|
1988
|
-
${error_summary_section:+$error_summary_section
|
|
1989
|
-
}
|
|
1990
|
-
${memory_section:+## Memory Context
|
|
1991
|
-
$memory_section
|
|
1992
|
-
}
|
|
1993
|
-
${dora_section:+$dora_section
|
|
1994
|
-
}
|
|
1995
|
-
${intelligence_section:+$intelligence_section
|
|
1996
|
-
}
|
|
1997
|
-
${restart_section:+$restart_section
|
|
1998
|
-
}
|
|
1999
|
-
## Instructions
|
|
2000
|
-
1. Read the codebase and understand the current state
|
|
2001
|
-
2. Identify the highest-priority remaining work toward the goal
|
|
2002
|
-
3. Implement ONE meaningful chunk of progress
|
|
2003
|
-
4. Run tests if a test command exists: ${TEST_CMD:-"(none)"}
|
|
2004
|
-
5. Commit your work with a descriptive message
|
|
2005
|
-
6. When the goal is FULLY achieved, output exactly: LOOP_COMPLETE
|
|
2006
|
-
|
|
2007
|
-
${audit_section}
|
|
2008
|
-
|
|
2009
|
-
${audit_feedback_section}
|
|
2010
|
-
|
|
2011
|
-
${rejection_notice_section}
|
|
2012
|
-
|
|
2013
|
-
${stuckness_section}
|
|
2014
|
-
|
|
2015
|
-
## Rules
|
|
2016
|
-
- Focus on ONE task per iteration — do it well
|
|
2017
|
-
- Always commit with descriptive messages
|
|
2018
|
-
- If tests fail, fix them before ending
|
|
2019
|
-
- If stuck on the same issue for 2+ iterations, try a different approach
|
|
2020
|
-
- Do NOT output LOOP_COMPLETE unless the goal is genuinely achieved
|
|
2021
|
-
PROMPT
|
|
2022
|
-
}
|
|
1499
|
+
# NOTE: compose_prompt() is now in lib/loop-iteration.sh (extracted upstream)
|
|
2023
1500
|
|
|
2024
1501
|
# ─── Alternative Strategy Exploration ─────────────────────────────────────────
|
|
2025
1502
|
# When stuckness is detected, generate a context-aware alternative strategy.
|
|
2026
1503
|
# Uses pattern matching on error type + iteration count to suggest different approaches.
|
|
2027
1504
|
|
|
2028
|
-
explore_alternative_strategy() {
|
|
2029
|
-
local last_error="${1:-unknown}"
|
|
2030
|
-
local iteration="${2:-0}"
|
|
2031
|
-
local diagnosis="${3:-}"
|
|
2032
|
-
|
|
2033
|
-
# Track attempted strategies to avoid repeating them
|
|
2034
|
-
local strategy_file="${LOG_DIR:-/tmp}/strategy-attempts.txt"
|
|
2035
|
-
local attempted
|
|
2036
|
-
attempted=$(cat "$strategy_file" 2>/dev/null || true)
|
|
2037
|
-
|
|
2038
|
-
local strategy=""
|
|
2039
|
-
|
|
2040
|
-
# If quality gates are passing but evaluators disagree, suggest focusing on evaluator alignment
|
|
2041
|
-
if [[ "${TEST_PASSED:-}" == "true" ]] && [[ "${QUALITY_GATE_PASSED:-}" == "true" || "${AUDIT_RESULT:-}" == "pass" ]]; then
|
|
2042
|
-
if ! echo "$attempted" | grep -q "evaluator_alignment"; then
|
|
2043
|
-
echo "evaluator_alignment" >> "$strategy_file"
|
|
2044
|
-
strategy="## Alternative Strategy: Evaluator Alignment
|
|
2045
|
-
The code appears functionally complete (tests pass). Focus on satisfying the remaining
|
|
2046
|
-
quality gate evaluators. Check the DoD log and audit log for specific complaints, then
|
|
2047
|
-
address those exact points rather than adding new features."
|
|
2048
|
-
fi
|
|
2049
|
-
fi
|
|
2050
|
-
|
|
2051
|
-
# If no code changes in last iteration, suggest verifying existing work
|
|
2052
|
-
if echo "$last_error" | grep -qi "no code changes" || [[ "$diagnosis" == *"no code"* ]]; then
|
|
2053
|
-
if ! echo "$attempted" | grep -q "verify_existing"; then
|
|
2054
|
-
echo "verify_existing" >> "$strategy_file"
|
|
2055
|
-
strategy="## Alternative Strategy: Verify Existing Work
|
|
2056
|
-
Recent iterations made no code changes. The work may already be complete.
|
|
2057
|
-
Run the full test suite, verify all features work, and if everything passes,
|
|
2058
|
-
commit a verification message and declare LOOP_COMPLETE with evidence."
|
|
2059
|
-
fi
|
|
2060
|
-
fi
|
|
2061
|
-
|
|
2062
|
-
# Generic fallback: break the problem down
|
|
2063
|
-
if [[ -z "$strategy" ]]; then
|
|
2064
|
-
if ! echo "$attempted" | grep -q "decompose"; then
|
|
2065
|
-
echo "decompose" >> "$strategy_file"
|
|
2066
|
-
strategy="## Alternative Strategy: Decompose
|
|
2067
|
-
Break the remaining work into smaller, independent steps. Focus on one specific
|
|
2068
|
-
file or function at a time. Read error messages literally — the root cause may
|
|
2069
|
-
differ from your assumption."
|
|
2070
|
-
fi
|
|
2071
|
-
fi
|
|
2072
|
-
|
|
2073
|
-
echo "$strategy"
|
|
2074
|
-
}
|
|
2075
1505
|
|
|
2076
1506
|
# ─── Stuckness Detection ─────────────────────────────────────────────────────
|
|
2077
1507
|
# Multi-signal detection: text overlap, git diff hash, error repetition, exit code pattern, iteration budget.
|
|
@@ -2080,186 +1510,7 @@ differ from your assumption."
|
|
|
2080
1510
|
STUCKNESS_COUNT=0
|
|
2081
1511
|
STUCKNESS_TRACKING_FILE=""
|
|
2082
1512
|
|
|
2083
|
-
record_iteration_stuckness_data() {
|
|
2084
|
-
local exit_code="${1:-0}"
|
|
2085
|
-
[[ -z "$LOG_DIR" ]] && return 0
|
|
2086
|
-
local tracking_file="${STUCKNESS_TRACKING_FILE:-$LOG_DIR/stuckness-tracking.txt}"
|
|
2087
|
-
local diff_hash error_hash
|
|
2088
|
-
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")
|
|
2089
|
-
local error_log="${ARTIFACTS_DIR:-${STATE_DIR:-${PROJECT_ROOT:-.}/.claude}/pipeline-artifacts}/error-log.jsonl"
|
|
2090
|
-
if [[ -f "$error_log" ]]; then
|
|
2091
|
-
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")
|
|
2092
|
-
else
|
|
2093
|
-
error_hash="none"
|
|
2094
|
-
fi
|
|
2095
|
-
echo "${diff_hash}|${error_hash}|${exit_code}" >> "$tracking_file"
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
detect_stuckness() {
|
|
2099
|
-
STUCKNESS_HINT=""
|
|
2100
|
-
local iteration="${ITERATION:-0}"
|
|
2101
|
-
local stuckness_signals=0
|
|
2102
|
-
local stuckness_reasons=()
|
|
2103
|
-
local tracking_file="${STUCKNESS_TRACKING_FILE:-$LOG_DIR/stuckness-tracking.txt}"
|
|
2104
|
-
local tracking_lines
|
|
2105
|
-
tracking_lines=$(wc -l < "$tracking_file" 2>/dev/null || echo "0")
|
|
2106
|
-
|
|
2107
|
-
# Signal 1: Text overlap (existing logic) — compare last 2 iteration logs
|
|
2108
|
-
if [[ "$iteration" -ge 3 ]]; then
|
|
2109
|
-
local log1="$LOG_DIR/iteration-$(( iteration - 1 )).log"
|
|
2110
|
-
local log2="$LOG_DIR/iteration-$(( iteration - 2 )).log"
|
|
2111
|
-
local log3="$LOG_DIR/iteration-$(( iteration - 3 )).log"
|
|
2112
|
-
|
|
2113
|
-
if [[ -f "$log1" && -f "$log2" ]]; then
|
|
2114
|
-
local lines1 lines2 common total overlap_pct
|
|
2115
|
-
lines1=$(tail -50 "$log1" 2>/dev/null | grep -v '^$' | sort || true)
|
|
2116
|
-
lines2=$(tail -50 "$log2" 2>/dev/null | grep -v '^$' | sort || true)
|
|
2117
|
-
|
|
2118
|
-
if [[ -n "$lines1" && -n "$lines2" ]]; then
|
|
2119
|
-
total=$(echo "$lines1" | wc -l | tr -d ' ')
|
|
2120
|
-
common=$(comm -12 <(echo "$lines1") <(echo "$lines2") 2>/dev/null | wc -l | tr -d ' ' || echo "0")
|
|
2121
|
-
if [[ "$total" -gt 0 ]]; then
|
|
2122
|
-
overlap_pct=$(( common * 100 / total ))
|
|
2123
|
-
else
|
|
2124
|
-
overlap_pct=0
|
|
2125
|
-
fi
|
|
2126
|
-
if [[ "${overlap_pct:-0}" -ge 90 ]]; then
|
|
2127
|
-
stuckness_signals=$((stuckness_signals + 1))
|
|
2128
|
-
stuckness_reasons+=("high text overlap (${overlap_pct}%) between iterations")
|
|
2129
|
-
fi
|
|
2130
|
-
fi
|
|
2131
|
-
fi
|
|
2132
|
-
fi
|
|
2133
|
-
|
|
2134
|
-
# Signal 2: Git diff hash — last 3 iterations produced zero or identical diffs
|
|
2135
|
-
if [[ -f "$tracking_file" ]] && [[ "$tracking_lines" -ge 3 ]]; then
|
|
2136
|
-
local last_three
|
|
2137
|
-
last_three=$(tail -3 "$tracking_file" 2>/dev/null | cut -d'|' -f1 || true)
|
|
2138
|
-
local unique_hashes
|
|
2139
|
-
unique_hashes=$(echo "$last_three" | sort -u | grep -v '^$' | wc -l | tr -d ' ')
|
|
2140
|
-
if [[ "$unique_hashes" -le 1 ]] && [[ -n "$last_three" ]]; then
|
|
2141
|
-
stuckness_signals=$((stuckness_signals + 1))
|
|
2142
|
-
stuckness_reasons+=("identical or zero git diffs in last 3 iterations")
|
|
2143
|
-
fi
|
|
2144
|
-
fi
|
|
2145
|
-
|
|
2146
|
-
# Signal 3: Error repetition — same error hash in last 3 iterations
|
|
2147
|
-
if [[ -f "$tracking_file" ]] && [[ "$tracking_lines" -ge 3 ]]; then
|
|
2148
|
-
local last_three_errors
|
|
2149
|
-
last_three_errors=$(tail -3 "$tracking_file" 2>/dev/null | cut -d'|' -f2 || true)
|
|
2150
|
-
local unique_error_hashes
|
|
2151
|
-
unique_error_hashes=$(echo "$last_three_errors" | sort -u | grep -v '^none$' | grep -v '^$' | wc -l | tr -d ' ')
|
|
2152
|
-
if [[ "$unique_error_hashes" -eq 1 ]] && [[ -n "$(echo "$last_three_errors" | grep -v '^none$')" ]]; then
|
|
2153
|
-
stuckness_signals=$((stuckness_signals + 1))
|
|
2154
|
-
stuckness_reasons+=("same error in last 3 iterations")
|
|
2155
|
-
fi
|
|
2156
|
-
fi
|
|
2157
|
-
|
|
2158
|
-
# Signal 4: Same error repeating 3+ times (legacy check on error-log content)
|
|
2159
|
-
local error_log
|
|
2160
|
-
error_log="${ARTIFACTS_DIR:-$PROJECT_ROOT/.claude/pipeline-artifacts}/error-log.jsonl"
|
|
2161
|
-
if [[ -f "$error_log" ]]; then
|
|
2162
|
-
local last_errors
|
|
2163
|
-
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)
|
|
2164
|
-
local repeat_count
|
|
2165
|
-
repeat_count=$(echo "$last_errors" | awk '{print $1}' 2>/dev/null || echo "0")
|
|
2166
|
-
if [[ "${repeat_count:-0}" -ge 3 ]]; then
|
|
2167
|
-
stuckness_signals=$((stuckness_signals + 1))
|
|
2168
|
-
stuckness_reasons+=("same error repeated ${repeat_count} times")
|
|
2169
|
-
fi
|
|
2170
|
-
fi
|
|
2171
|
-
|
|
2172
|
-
# Signal 5: Exit code pattern — last 3 iterations had same non-zero exit code
|
|
2173
|
-
if [[ -f "$tracking_file" ]] && [[ "$tracking_lines" -ge 3 ]]; then
|
|
2174
|
-
local last_three_exits
|
|
2175
|
-
last_three_exits=$(tail -3 "$tracking_file" 2>/dev/null | cut -d'|' -f3 || true)
|
|
2176
|
-
local first_exit
|
|
2177
|
-
first_exit=$(echo "$last_three_exits" | head -1)
|
|
2178
|
-
if [[ "$first_exit" =~ ^[0-9]+$ ]] && [[ "$first_exit" -ne 0 ]]; then
|
|
2179
|
-
local all_same=true
|
|
2180
|
-
while IFS= read -r ex; do
|
|
2181
|
-
[[ "$ex" != "$first_exit" ]] && all_same=false
|
|
2182
|
-
done <<< "$last_three_exits"
|
|
2183
|
-
if [[ "$all_same" == true ]]; then
|
|
2184
|
-
stuckness_signals=$((stuckness_signals + 1))
|
|
2185
|
-
stuckness_reasons+=("same non-zero exit code (${first_exit}) in last 3 iterations")
|
|
2186
|
-
fi
|
|
2187
|
-
fi
|
|
2188
|
-
fi
|
|
2189
|
-
|
|
2190
|
-
# Signal 6: Git diff size — no or minimal code changes (existing)
|
|
2191
|
-
local diff_lines
|
|
2192
|
-
diff_lines=$(git -C "${PROJECT_ROOT:-.}" diff HEAD 2>/dev/null | wc -l | tr -d ' ' || echo "0")
|
|
2193
|
-
if [[ "${diff_lines:-0}" -lt 5 ]] && [[ "$iteration" -gt 2 ]]; then
|
|
2194
|
-
stuckness_signals=$((stuckness_signals + 1))
|
|
2195
|
-
stuckness_reasons+=("no code changes in last iteration")
|
|
2196
|
-
fi
|
|
2197
|
-
|
|
2198
|
-
# Signal 7: Iteration budget — used >70% without passing tests
|
|
2199
|
-
local max_iter="${MAX_ITERATIONS:-20}"
|
|
2200
|
-
local progress_pct=0
|
|
2201
|
-
if [[ "$max_iter" -gt 0 ]]; then
|
|
2202
|
-
progress_pct=$(( iteration * 100 / max_iter ))
|
|
2203
|
-
fi
|
|
2204
|
-
if [[ "$progress_pct" -gt 70 ]] && [[ "${TEST_PASSED:-false}" != "true" ]]; then
|
|
2205
|
-
stuckness_signals=$((stuckness_signals + 1))
|
|
2206
|
-
stuckness_reasons+=("used ${progress_pct}% of iteration budget without passing tests")
|
|
2207
|
-
fi
|
|
2208
|
-
|
|
2209
|
-
# Gate-aware dampening: if tests pass and the agent has made progress overall,
|
|
2210
|
-
# reduce stuckness signal count. The "no code changes" and "identical diffs" signals
|
|
2211
|
-
# fire when code is already complete and the agent is fighting evaluator quirks —
|
|
2212
|
-
# that's not genuine stuckness, it's "done but gates disagree."
|
|
2213
|
-
if [[ "${TEST_PASSED:-}" == "true" ]] && [[ "$stuckness_signals" -ge 2 ]]; then
|
|
2214
|
-
# If at least one quality signal is positive, dampen by 1
|
|
2215
|
-
if [[ "${AUDIT_RESULT:-}" == "pass" ]] || $QUALITY_GATE_PASSED 2>/dev/null; then
|
|
2216
|
-
stuckness_signals=$((stuckness_signals - 1))
|
|
2217
|
-
fi
|
|
2218
|
-
fi
|
|
2219
|
-
|
|
2220
|
-
# Decision: 2+ signals = stuck
|
|
2221
|
-
if [[ "$stuckness_signals" -ge 2 ]]; then
|
|
2222
|
-
STUCKNESS_COUNT=$(( STUCKNESS_COUNT + 1 ))
|
|
2223
|
-
STUCKNESS_DIAGNOSIS="${stuckness_reasons[*]}"
|
|
2224
|
-
if type emit_event >/dev/null 2>&1; then
|
|
2225
|
-
emit_event "loop.stuckness_detected" "signals=$stuckness_signals" "count=$STUCKNESS_COUNT" "iteration=$iteration" "reasons=${stuckness_reasons[*]}"
|
|
2226
|
-
fi
|
|
2227
|
-
STUCKNESS_HINT="IMPORTANT: The loop appears stuck. Previous approaches have not worked. You MUST try a fundamentally different strategy. Reasons: ${stuckness_reasons[*]}"
|
|
2228
|
-
warn "Stuckness detected (${stuckness_signals} signals, count ${STUCKNESS_COUNT}): ${stuckness_reasons[*]}"
|
|
2229
|
-
|
|
2230
|
-
local diff_summary=""
|
|
2231
|
-
local log1="$LOG_DIR/iteration-$(( iteration - 1 )).log"
|
|
2232
|
-
local log3="$LOG_DIR/iteration-$(( iteration - 3 )).log"
|
|
2233
|
-
if [[ -f "$log3" && -f "$log1" ]]; then
|
|
2234
|
-
diff_summary=$(diff <(tail -30 "$log3" 2>/dev/null) <(tail -30 "$log1" 2>/dev/null) 2>/dev/null | head -10 || true)
|
|
2235
|
-
fi
|
|
2236
|
-
|
|
2237
|
-
local alternatives=""
|
|
2238
|
-
if type memory_inject_context >/dev/null 2>&1; then
|
|
2239
|
-
alternatives=$(memory_inject_context "build" 2>/dev/null | grep -i "fix:" | head -3 || true)
|
|
2240
|
-
fi
|
|
2241
|
-
|
|
2242
|
-
cat <<STUCK_SECTION
|
|
2243
|
-
## Stuckness Detected
|
|
2244
|
-
${STUCKNESS_HINT}
|
|
2245
|
-
|
|
2246
|
-
${diff_summary:+Changes between recent iterations:
|
|
2247
|
-
$diff_summary
|
|
2248
|
-
}
|
|
2249
|
-
${alternatives:+Consider these alternative approaches from past fixes:
|
|
2250
|
-
$alternatives
|
|
2251
|
-
}
|
|
2252
|
-
Try a fundamentally different approach:
|
|
2253
|
-
- Break the problem into smaller steps
|
|
2254
|
-
- Look for an entirely different implementation strategy
|
|
2255
|
-
- Check if there's a dependency or configuration issue blocking progress
|
|
2256
|
-
- Read error messages more carefully — the root cause may differ from your assumption
|
|
2257
|
-
STUCK_SECTION
|
|
2258
|
-
return 0
|
|
2259
|
-
fi
|
|
2260
1513
|
|
|
2261
|
-
return 1
|
|
2262
|
-
}
|
|
2263
1514
|
|
|
2264
1515
|
compose_audit_section() {
|
|
2265
1516
|
if ! $AUDIT_ENABLED; then
|
|
@@ -2356,7 +1607,7 @@ compose_worker_prompt() {
|
|
|
2356
1607
|
role_desc="$recruit_desc"
|
|
2357
1608
|
fi
|
|
2358
1609
|
fi
|
|
2359
|
-
# Fallback to
|
|
1610
|
+
# Fallback to built-in role descriptions
|
|
2360
1611
|
if [[ -z "$role_desc" ]]; then
|
|
2361
1612
|
case "$role" in
|
|
2362
1613
|
builder) role_desc="Focus on implementation — writing code, fixing bugs, building features. You are the primary builder." ;;
|
|
@@ -2388,99 +1639,10 @@ PROMPT
|
|
|
2388
1639
|
|
|
2389
1640
|
# ─── Claude Execution ────────────────────────────────────────────────────────
|
|
2390
1641
|
|
|
2391
|
-
build_claude_flags() {
|
|
2392
|
-
local flags=()
|
|
2393
|
-
flags+=("--model" "$MODEL")
|
|
2394
|
-
flags+=("--output-format" "json")
|
|
2395
|
-
|
|
2396
|
-
if $SKIP_PERMISSIONS; then
|
|
2397
|
-
flags+=("--dangerously-skip-permissions")
|
|
2398
|
-
fi
|
|
2399
|
-
|
|
2400
|
-
if [[ -n "$MAX_TURNS" ]]; then
|
|
2401
|
-
flags+=("--max-turns" "$MAX_TURNS")
|
|
2402
|
-
fi
|
|
2403
|
-
|
|
2404
|
-
echo "${flags[*]}"
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
run_claude_iteration() {
|
|
2408
|
-
local log_file="$LOG_DIR/iteration-${ITERATION}.log"
|
|
2409
|
-
local json_file="$LOG_DIR/iteration-${ITERATION}.json"
|
|
2410
|
-
local prompt
|
|
2411
|
-
prompt="$(compose_prompt)"
|
|
2412
|
-
local final_prompt
|
|
2413
|
-
final_prompt=$(manage_context_window "$prompt")
|
|
2414
|
-
|
|
2415
|
-
local prompt_chars=${#final_prompt}
|
|
2416
|
-
local approx_tokens=$((prompt_chars / 4))
|
|
2417
|
-
info "Prompt: ~${approx_tokens} tokens (${prompt_chars} chars)"
|
|
2418
|
-
|
|
2419
|
-
local flags
|
|
2420
|
-
flags="$(build_claude_flags)"
|
|
2421
|
-
|
|
2422
|
-
local iter_start
|
|
2423
|
-
iter_start="$(now_epoch)"
|
|
2424
|
-
|
|
2425
|
-
echo -e "\n${CYAN}${BOLD}▸${RESET} ${BOLD}Iteration ${ITERATION}/${MAX_ITERATIONS}${RESET} — Starting..."
|
|
2426
|
-
|
|
2427
|
-
# Run Claude headless (with timeout + PID capture for signal handling)
|
|
2428
|
-
# Output goes to .json first, then we extract text into .log for compat
|
|
2429
|
-
local exit_code=0
|
|
2430
|
-
# shellcheck disable=SC2086
|
|
2431
|
-
local err_file="${json_file%.json}.stderr"
|
|
2432
|
-
if [[ -n "$TIMEOUT_CMD" ]]; then
|
|
2433
|
-
$TIMEOUT_CMD "$CLAUDE_TIMEOUT" claude -p "$final_prompt" $flags > "$json_file" 2>"$err_file" &
|
|
2434
|
-
else
|
|
2435
|
-
claude -p "$final_prompt" $flags > "$json_file" 2>"$err_file" &
|
|
2436
|
-
fi
|
|
2437
|
-
CHILD_PID=$!
|
|
2438
|
-
wait "$CHILD_PID" 2>/dev/null || exit_code=$?
|
|
2439
|
-
CHILD_PID=""
|
|
2440
|
-
if [[ "$exit_code" -eq 124 ]]; then
|
|
2441
|
-
warn "Claude CLI timed out after ${CLAUDE_TIMEOUT}s"
|
|
2442
|
-
fi
|
|
2443
|
-
|
|
2444
|
-
# Extract text result from JSON into .log for backwards compatibility
|
|
2445
|
-
# With --output-format json, stdout is a JSON array; .[-1].result has the text
|
|
2446
|
-
_extract_text_from_json "$json_file" "$log_file" "$err_file"
|
|
2447
1642
|
|
|
2448
|
-
local iter_end
|
|
2449
|
-
iter_end="$(now_epoch)"
|
|
2450
|
-
local iter_duration=$(( iter_end - iter_start ))
|
|
2451
|
-
|
|
2452
|
-
echo -e " ${GREEN}✓${RESET} Claude session completed ($(format_duration "$iter_duration"), exit $exit_code)"
|
|
2453
|
-
|
|
2454
|
-
# Accumulate token usage from this iteration's JSON output
|
|
2455
|
-
accumulate_loop_tokens "$json_file"
|
|
2456
|
-
|
|
2457
|
-
# Show verbose output if requested
|
|
2458
|
-
if $VERBOSE; then
|
|
2459
|
-
echo -e " ${DIM}─── Claude Output ───${RESET}"
|
|
2460
|
-
sed 's/^/ /' "$log_file" | head -100
|
|
2461
|
-
echo -e " ${DIM}─────────────────────${RESET}"
|
|
2462
|
-
fi
|
|
2463
|
-
|
|
2464
|
-
return $exit_code
|
|
2465
|
-
}
|
|
2466
1643
|
|
|
2467
1644
|
# ─── Iteration Summary Extraction ────────────────────────────────────────────
|
|
2468
1645
|
|
|
2469
|
-
extract_summary() {
|
|
2470
|
-
local log_file="$1"
|
|
2471
|
-
# Grab last meaningful lines from Claude output, skipping empty lines
|
|
2472
|
-
local summary
|
|
2473
|
-
summary="$(grep -v '^$' "$log_file" | tail -5 | head -3 2>/dev/null || echo "(no output)")"
|
|
2474
|
-
# Truncate long lines
|
|
2475
|
-
summary="$(echo "$summary" | cut -c1-120)"
|
|
2476
|
-
|
|
2477
|
-
# Sanitize: if summary is just a CLI/API error, replace with generic text
|
|
2478
|
-
if echo "$summary" | grep -qiE 'Invalid API key|authentication_error|rate_limit|API key expired|ANTHROPIC_API_KEY'; then
|
|
2479
|
-
summary="(CLI error — no useful output this iteration)"
|
|
2480
|
-
fi
|
|
2481
|
-
|
|
2482
|
-
echo "$summary"
|
|
2483
|
-
}
|
|
2484
1646
|
|
|
2485
1647
|
# ─── Display Helpers ─────────────────────────────────────────────────────────
|
|
2486
1648
|
|
|
@@ -2596,6 +1758,7 @@ cleanup() {
|
|
|
2596
1758
|
export SW_LOOP_STATUS="$STATUS"
|
|
2597
1759
|
export SW_LOOP_TEST_OUTPUT="${TEST_OUTPUT:-}"
|
|
2598
1760
|
export SW_LOOP_FINDINGS="${LOG_ENTRIES:-}"
|
|
1761
|
+
# shellcheck disable=SC2155
|
|
2599
1762
|
export SW_LOOP_MODIFIED="$(git diff --name-only HEAD 2>/dev/null | head -50 | tr '\n' ',' | sed 's/,$//')"
|
|
2600
1763
|
"$SCRIPT_DIR/sw-checkpoint.sh" save-context --stage build 2>/dev/null || true
|
|
2601
1764
|
|
|
@@ -2679,7 +1842,7 @@ DIM='\033[2m'
|
|
|
2679
1842
|
BOLD='\033[1m'
|
|
2680
1843
|
RESET='\033[0m'
|
|
2681
1844
|
|
|
2682
|
-
cd "$WORK_DIR"
|
|
1845
|
+
cd "$WORK_DIR" || { echo "ERROR: Cannot cd to WORK_DIR: $WORK_DIR" >&2; exit 1; }
|
|
2683
1846
|
ITERATION=0
|
|
2684
1847
|
CONSECUTIVE_FAILURES=0
|
|
2685
1848
|
|
|
@@ -2762,8 +1925,11 @@ PROMPT
|
|
|
2762
1925
|
break
|
|
2763
1926
|
fi
|
|
2764
1927
|
|
|
2765
|
-
# Auto-commit
|
|
1928
|
+
# Auto-commit — stage only source files, exclude build artifacts
|
|
2766
1929
|
git add -A 2>/dev/null || true
|
|
1930
|
+
git reset -- .claude/loop-logs/ .claude/loop-state.md .claude/intelligence-cache.json \
|
|
1931
|
+
.claude/platform-hygiene.json .claude/pipeline-artifacts/ .claude/code-review.json \
|
|
1932
|
+
.claude/hygiene-report.json .claude/pr-draft.md 2>/dev/null || true
|
|
2767
1933
|
if git commit -m "agent-${AGENT_NUM}: iteration ${ITERATION}" --no-verify 2>/dev/null; then
|
|
2768
1934
|
if ! git push origin "loop/agent-${AGENT_NUM}" 2>/dev/null; then
|
|
2769
1935
|
echo -e " ${YELLOW}⚠${RESET} git push failed for loop/agent-${AGENT_NUM} — remote may be out of sync"
|
|
@@ -2933,8 +2099,16 @@ cleanup_multi_agent() {
|
|
|
2933
2099
|
# ─── Main: Single-Agent Loop ─────────────────────────────────────────────────
|
|
2934
2100
|
|
|
2935
2101
|
run_single_agent_loop() {
|
|
2102
|
+
# Save original environment variables before loop starts
|
|
2103
|
+
local SAVED_CLAUDE_MODEL="${CLAUDE_MODEL:-}"
|
|
2104
|
+
local SAVED_ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}"
|
|
2105
|
+
|
|
2936
2106
|
if [[ "$SESSION_RESTART" == "true" ]]; then
|
|
2937
2107
|
# Restart: state already reset by run_loop_with_restarts, skip init
|
|
2108
|
+
# Restore environment variables for clean iteration state
|
|
2109
|
+
[[ -n "$SAVED_CLAUDE_MODEL" ]] && export CLAUDE_MODEL="$SAVED_CLAUDE_MODEL"
|
|
2110
|
+
# Reset context exhaustion counter for this session (it tracks restarts WITHIN a single session)
|
|
2111
|
+
CONTEXT_RESTART_COUNT=0
|
|
2938
2112
|
info "Session restart ${RESTART_COUNT}/${MAX_RESTARTS} — fresh context, reading progress"
|
|
2939
2113
|
elif $RESUME; then
|
|
2940
2114
|
resume_state
|
|
@@ -2956,11 +2130,16 @@ run_single_agent_loop() {
|
|
|
2956
2130
|
STUCKNESS_COUNT=0
|
|
2957
2131
|
STUCKNESS_TRACKING_FILE="$LOG_DIR/stuckness-tracking.txt"
|
|
2958
2132
|
: > "$STUCKNESS_TRACKING_FILE" 2>/dev/null || true
|
|
2959
|
-
: > "${LOG_DIR
|
|
2133
|
+
: > "${LOG_DIR}/strategy-attempts.txt" 2>/dev/null || true
|
|
2960
2134
|
|
|
2961
2135
|
show_banner
|
|
2962
2136
|
|
|
2963
2137
|
while true; do
|
|
2138
|
+
# Reset environment variables at start of each iteration
|
|
2139
|
+
# Prevents previous iterations from affecting model selection or API keys
|
|
2140
|
+
[[ -n "$SAVED_CLAUDE_MODEL" ]] && export CLAUDE_MODEL="$SAVED_CLAUDE_MODEL"
|
|
2141
|
+
[[ -n "$SAVED_ANTHROPIC_API_KEY" ]] && export ANTHROPIC_API_KEY="$SAVED_ANTHROPIC_API_KEY"
|
|
2142
|
+
|
|
2964
2143
|
# Pre-checks (before incrementing — ITERATION tracks completed count)
|
|
2965
2144
|
check_circuit_breaker || break
|
|
2966
2145
|
check_max_iterations || break
|
|
@@ -3044,6 +2223,11 @@ ${GOAL}"
|
|
|
3044
2223
|
# Record iteration data for stuckness detection (diff hash, error hash, exit code)
|
|
3045
2224
|
record_iteration_stuckness_data "$exit_code"
|
|
3046
2225
|
|
|
2226
|
+
# Dark factory: score this iteration with process reward model
|
|
2227
|
+
if type process_reward_score_iteration >/dev/null 2>&1; then
|
|
2228
|
+
process_reward_score_iteration "$PROJECT_ROOT" "${TEST_OUTPUT:-}" "$ITERATION" 2>/dev/null || true
|
|
2229
|
+
fi
|
|
2230
|
+
|
|
3047
2231
|
# Detect fatal CLI errors (API key, auth, network) — abort immediately
|
|
3048
2232
|
if check_fatal_error "$log_file" "$exit_code"; then
|
|
3049
2233
|
STATUS="error"
|
|
@@ -3054,6 +2238,32 @@ ${GOAL}"
|
|
|
3054
2238
|
return 1
|
|
3055
2239
|
fi
|
|
3056
2240
|
|
|
2241
|
+
# Detect context exhaustion and trigger intelligent restart
|
|
2242
|
+
local log_content=""
|
|
2243
|
+
[[ -f "$log_file" ]] && log_content=$(cat "$log_file" 2>/dev/null || true)
|
|
2244
|
+
local stderr_file="${LOG_DIR}/iteration-${ITERATION}.stderr"
|
|
2245
|
+
local stderr_content=""
|
|
2246
|
+
[[ -f "$stderr_file" ]] && stderr_content=$(cat "$stderr_file" 2>/dev/null || true)
|
|
2247
|
+
|
|
2248
|
+
if echo "${log_content}${stderr_content}" | grep -qiE "$CONTEXT_EXHAUSTION_PATTERNS" 2>/dev/null; then
|
|
2249
|
+
if [[ "${CONTEXT_RESTART_COUNT:-0}" -lt "${CONTEXT_RESTART_LIMIT:-2}" ]]; then
|
|
2250
|
+
CONTEXT_RESTART_COUNT=$(( CONTEXT_RESTART_COUNT + 1 ))
|
|
2251
|
+
STATUS="context_exhaustion_restart"
|
|
2252
|
+
write_state
|
|
2253
|
+
write_progress
|
|
2254
|
+
warn "Context exhaustion detected (iteration $ITERATION) — triggering intelligent restart ($CONTEXT_RESTART_COUNT/$CONTEXT_RESTART_LIMIT)"
|
|
2255
|
+
if type emit_event >/dev/null 2>&1; then
|
|
2256
|
+
emit_event "loop.context_exhaustion" "iteration=$ITERATION" "restart_count=$CONTEXT_RESTART_COUNT" "max_restarts=$MAX_RESTARTS"
|
|
2257
|
+
fi
|
|
2258
|
+
break
|
|
2259
|
+
else
|
|
2260
|
+
warn "Context exhaustion detected but restart limit ($CONTEXT_RESTART_LIMIT) reached"
|
|
2261
|
+
STATUS="context_exhaustion_fatal"
|
|
2262
|
+
write_state
|
|
2263
|
+
write_progress
|
|
2264
|
+
fi
|
|
2265
|
+
fi
|
|
2266
|
+
|
|
3057
2267
|
# Mid-loop memory refresh — re-query with current error context after iteration 3
|
|
3058
2268
|
if [[ "$ITERATION" -ge 3 ]] && type memory_inject_context >/dev/null 2>&1; then
|
|
3059
2269
|
local refresh_ctx
|
|
@@ -3099,6 +2309,15 @@ ${GOAL}"
|
|
|
3099
2309
|
fi
|
|
3100
2310
|
fi
|
|
3101
2311
|
|
|
2312
|
+
# Dark factory: update RL weights based on test outcome
|
|
2313
|
+
if type rl_update_weights >/dev/null 2>&1; then
|
|
2314
|
+
if [[ "${TEST_PASSED:-}" == "true" ]]; then
|
|
2315
|
+
rl_update_weights "success" 2>/dev/null || true
|
|
2316
|
+
elif [[ "${TEST_PASSED:-}" == "false" ]]; then
|
|
2317
|
+
rl_update_weights "failure" 2>/dev/null || true
|
|
2318
|
+
fi
|
|
2319
|
+
fi
|
|
2320
|
+
|
|
3102
2321
|
# Track fix outcome for memory effectiveness
|
|
3103
2322
|
if [[ -n "${_applied_fix_pattern:-}" ]]; then
|
|
3104
2323
|
if type memory_record_fix_outcome >/dev/null 2>&1; then
|
|
@@ -3117,15 +2336,98 @@ ${GOAL}"
|
|
|
3117
2336
|
export SW_LOOP_STATUS="${STATUS:-running}"
|
|
3118
2337
|
export SW_LOOP_TEST_OUTPUT="${TEST_OUTPUT:-}"
|
|
3119
2338
|
export SW_LOOP_FINDINGS="${LOG_ENTRIES:-}"
|
|
2339
|
+
# shellcheck disable=SC2155
|
|
3120
2340
|
export SW_LOOP_MODIFIED="$(git diff --name-only HEAD 2>/dev/null | head -50 | tr '\n' ',' | sed 's/,$//')"
|
|
3121
2341
|
"$SCRIPT_DIR/sw-checkpoint.sh" save-context --stage build 2>/dev/null || true
|
|
3122
2342
|
|
|
3123
2343
|
# Audit agent (reviews implementer's work)
|
|
3124
2344
|
run_audit_agent
|
|
3125
2345
|
|
|
2346
|
+
# Verification gap detection: audit failed but tests passed
|
|
2347
|
+
# Instead of a full retry (which causes context bloat/timeout), run targeted verification
|
|
2348
|
+
if [[ "${AUDIT_RESULT:-}" != "pass" ]] && [[ "${TEST_PASSED:-}" == "true" ]]; then
|
|
2349
|
+
echo -e " ${YELLOW}▸${RESET} Verification gap detected (tests pass, audit disagrees)"
|
|
2350
|
+
|
|
2351
|
+
local verification_passed=true
|
|
2352
|
+
|
|
2353
|
+
# 1. Re-run ALL test commands to double-check
|
|
2354
|
+
local recheck_log="${LOG_DIR}/verification-iter-${ITERATION}.log"
|
|
2355
|
+
if [[ -n "$TEST_CMD" ]]; then
|
|
2356
|
+
eval "$TEST_CMD" > "$recheck_log" 2>&1 || verification_passed=false
|
|
2357
|
+
fi
|
|
2358
|
+
for _vg_cmd in "${ADDITIONAL_TEST_CMDS[@]+"${ADDITIONAL_TEST_CMDS[@]}"}"; do
|
|
2359
|
+
[[ -z "$_vg_cmd" ]] && continue
|
|
2360
|
+
eval "$_vg_cmd" >> "$recheck_log" 2>&1 || verification_passed=false
|
|
2361
|
+
done
|
|
2362
|
+
|
|
2363
|
+
# 2. Check for uncommitted changes (quality gate)
|
|
2364
|
+
if ! git -C "$PROJECT_ROOT" diff --quiet 2>/dev/null; then
|
|
2365
|
+
echo -e " ${YELLOW}⚠${RESET} Uncommitted changes detected"
|
|
2366
|
+
verification_passed=false
|
|
2367
|
+
fi
|
|
2368
|
+
|
|
2369
|
+
if [[ "$verification_passed" == "true" ]]; then
|
|
2370
|
+
echo -e " ${GREEN}✓${RESET} Verification passed — overriding audit"
|
|
2371
|
+
AUDIT_RESULT="pass"
|
|
2372
|
+
emit_event "loop.verification_gap_resolved" \
|
|
2373
|
+
"iteration=$ITERATION" "action=override_audit"
|
|
2374
|
+
if type audit_emit >/dev/null 2>&1; then
|
|
2375
|
+
audit_emit "loop.verification_gap" "iteration=$ITERATION" \
|
|
2376
|
+
"resolution=override" "tests_recheck=pass" || true
|
|
2377
|
+
fi
|
|
2378
|
+
else
|
|
2379
|
+
echo -e " ${RED}✗${RESET} Verification failed — audit stands"
|
|
2380
|
+
emit_event "loop.verification_gap_confirmed" \
|
|
2381
|
+
"iteration=$ITERATION" "action=retry"
|
|
2382
|
+
if type audit_emit >/dev/null 2>&1; then
|
|
2383
|
+
audit_emit "loop.verification_gap" "iteration=$ITERATION" \
|
|
2384
|
+
"resolution=retry" "tests_recheck=fail" || true
|
|
2385
|
+
fi
|
|
2386
|
+
fi
|
|
2387
|
+
fi
|
|
2388
|
+
|
|
2389
|
+
# Auto-commit any remaining changes before quality gates
|
|
2390
|
+
# (audit agent, verification handler, or test evidence may create files)
|
|
2391
|
+
if ! git -C "$PROJECT_ROOT" diff --quiet 2>/dev/null || \
|
|
2392
|
+
! git -C "$PROJECT_ROOT" diff --cached --quiet 2>/dev/null || \
|
|
2393
|
+
[[ -n "$(git -C "$PROJECT_ROOT" ls-files --others --exclude-standard 2>/dev/null | head -1)" ]]; then
|
|
2394
|
+
git -C "$PROJECT_ROOT" add -A 2>/dev/null || true
|
|
2395
|
+
git -C "$PROJECT_ROOT" commit -m "loop: iteration $ITERATION — post-audit cleanup" --no-verify 2>/dev/null || true
|
|
2396
|
+
fi
|
|
2397
|
+
|
|
3126
2398
|
# Quality gates (automated checks)
|
|
3127
2399
|
run_quality_gates
|
|
3128
2400
|
|
|
2401
|
+
# Convergence detection (issue #203) — score iteration progress and detect convergence
|
|
2402
|
+
if type convergence_integrate >/dev/null 2>&1; then
|
|
2403
|
+
local conv_exit=0
|
|
2404
|
+
convergence_integrate || conv_exit=$?
|
|
2405
|
+
case "$conv_exit" in
|
|
2406
|
+
1)
|
|
2407
|
+
# Converged — stop successfully
|
|
2408
|
+
info "Build loop converged — stopping"
|
|
2409
|
+
STATUS="complete"
|
|
2410
|
+
write_state
|
|
2411
|
+
write_progress
|
|
2412
|
+
show_summary
|
|
2413
|
+
return 0
|
|
2414
|
+
;;
|
|
2415
|
+
2)
|
|
2416
|
+
# Diverging — stop with failure
|
|
2417
|
+
warn "Build loop diverging — stopping (scores declining consistently)"
|
|
2418
|
+
STATUS="diverging"
|
|
2419
|
+
write_state
|
|
2420
|
+
write_progress
|
|
2421
|
+
show_summary
|
|
2422
|
+
return 1
|
|
2423
|
+
;;
|
|
2424
|
+
3)
|
|
2425
|
+
# Oscillating — escalate to manual review
|
|
2426
|
+
warn "Build loop oscillating — consider manual review or model escalation"
|
|
2427
|
+
;;
|
|
2428
|
+
esac
|
|
2429
|
+
fi
|
|
2430
|
+
|
|
3129
2431
|
# Guarded completion (replaces naive grep check)
|
|
3130
2432
|
if guard_completion; then
|
|
3131
2433
|
STATUS="complete"
|
|
@@ -3138,6 +2440,10 @@ ${GOAL}"
|
|
|
3138
2440
|
# Check progress (circuit breaker)
|
|
3139
2441
|
if check_progress; then
|
|
3140
2442
|
CONSECUTIVE_FAILURES=0
|
|
2443
|
+
# Reset auto-recovery state on progress (tests passing, code advancing)
|
|
2444
|
+
if type recovery_reset >/dev/null 2>&1; then
|
|
2445
|
+
recovery_reset
|
|
2446
|
+
fi
|
|
3141
2447
|
echo -e " ${GREEN}✓${RESET} Progress detected — continuing"
|
|
3142
2448
|
else
|
|
3143
2449
|
CONSECUTIVE_FAILURES=$(( CONSECUTIVE_FAILURES + 1 ))
|
|
@@ -3216,6 +2522,52 @@ run_loop_with_restarts() {
|
|
|
3216
2522
|
if [[ "$STATUS" == "complete" ]]; then
|
|
3217
2523
|
return 0
|
|
3218
2524
|
fi
|
|
2525
|
+
|
|
2526
|
+
# Context exhaustion: treat as restart, not failure (unless restart limit hit)
|
|
2527
|
+
if [[ "$STATUS" == "context_exhaustion_restart" ]]; then
|
|
2528
|
+
if [[ "$CONTEXT_RESTART_COUNT" -lt "$CONTEXT_RESTART_LIMIT" ]]; then
|
|
2529
|
+
RESTART_COUNT=$(( RESTART_COUNT + 1 ))
|
|
2530
|
+
if type emit_event >/dev/null 2>&1; then
|
|
2531
|
+
emit_event "loop.restart" "restart=$RESTART_COUNT" "reason=context_exhaustion" "context_restart=$CONTEXT_RESTART_COUNT" "iteration=$ITERATION"
|
|
2532
|
+
fi
|
|
2533
|
+
info "Context exhaustion auto-recovery: restart $RESTART_COUNT/$MAX_RESTARTS (context restart $CONTEXT_RESTART_COUNT/$CONTEXT_RESTART_LIMIT)"
|
|
2534
|
+
|
|
2535
|
+
# Capture comprehensive state and generate briefing before restart
|
|
2536
|
+
if type restart_before_restart >/dev/null 2>&1; then
|
|
2537
|
+
restart_before_restart || warn "Failed to prepare restart briefing (continuing anyway)"
|
|
2538
|
+
fi
|
|
2539
|
+
|
|
2540
|
+
# Reset iteration-level state for fresh session
|
|
2541
|
+
SESSION_RESTART=true
|
|
2542
|
+
ITERATION=0
|
|
2543
|
+
CONSECUTIVE_FAILURES=0
|
|
2544
|
+
EXTENSION_COUNT=0
|
|
2545
|
+
STUCKNESS_COUNT=0
|
|
2546
|
+
STATUS="running"
|
|
2547
|
+
LOG_ENTRIES=""
|
|
2548
|
+
TEST_PASSED=""
|
|
2549
|
+
TEST_OUTPUT=""
|
|
2550
|
+
TEST_LOG_FILE=""
|
|
2551
|
+
GOAL="$ORIGINAL_GOAL"
|
|
2552
|
+
|
|
2553
|
+
# Archive old artifacts
|
|
2554
|
+
local restart_archive="$LOG_DIR/restart-${RESTART_COUNT}"
|
|
2555
|
+
mkdir -p "$restart_archive"
|
|
2556
|
+
for old_log in "$LOG_DIR"/iteration-*.log "$LOG_DIR"/tests-iter-*.log; do
|
|
2557
|
+
[[ -f "$old_log" ]] && mv "$old_log" "$restart_archive/" 2>/dev/null || true
|
|
2558
|
+
done
|
|
2559
|
+
[[ -f "$LOG_DIR/progress.md" ]] && cp "$LOG_DIR/progress.md" "$restart_archive/progress.md" 2>/dev/null || true
|
|
2560
|
+
[[ -f "$LOG_DIR/error-summary.json" ]] && cp "$LOG_DIR/error-summary.json" "$restart_archive/" 2>/dev/null || true
|
|
2561
|
+
|
|
2562
|
+
write_state
|
|
2563
|
+
sleep "$(_config_get_int "loop.sleep_between_iterations" 2 2>/dev/null || echo 2)"
|
|
2564
|
+
continue
|
|
2565
|
+
else
|
|
2566
|
+
warn "Context exhaustion limit reached — failing build"
|
|
2567
|
+
return "$loop_exit"
|
|
2568
|
+
fi
|
|
2569
|
+
fi
|
|
2570
|
+
|
|
3219
2571
|
if [[ "$MAX_RESTARTS" -le 0 ]]; then
|
|
3220
2572
|
return "$loop_exit"
|
|
3221
2573
|
fi
|
|
@@ -3223,9 +2575,11 @@ run_loop_with_restarts() {
|
|
|
3223
2575
|
warn "Max restarts ($MAX_RESTARTS) reached — stopping"
|
|
3224
2576
|
return "$loop_exit"
|
|
3225
2577
|
fi
|
|
3226
|
-
# Hard cap safety net
|
|
3227
|
-
|
|
3228
|
-
|
|
2578
|
+
# Hard cap safety net (configurable)
|
|
2579
|
+
local _hard_cap
|
|
2580
|
+
_hard_cap=$(_smart_int "loop.hard_restart_cap" 5)
|
|
2581
|
+
if [[ "$RESTART_COUNT" -ge "$_hard_cap" ]]; then
|
|
2582
|
+
warn "Hard restart cap ($_hard_cap) reached — stopping"
|
|
3229
2583
|
return "$loop_exit"
|
|
3230
2584
|
fi
|
|
3231
2585
|
|
|
@@ -3237,6 +2591,12 @@ run_loop_with_restarts() {
|
|
|
3237
2591
|
fi
|
|
3238
2592
|
|
|
3239
2593
|
RESTART_COUNT=$(( RESTART_COUNT + 1 ))
|
|
2594
|
+
|
|
2595
|
+
# Capture comprehensive state and generate briefing before restart
|
|
2596
|
+
if type restart_before_restart >/dev/null 2>&1; then
|
|
2597
|
+
restart_before_restart || warn "Failed to prepare restart briefing (continuing anyway)"
|
|
2598
|
+
fi
|
|
2599
|
+
|
|
3240
2600
|
if type emit_event >/dev/null 2>&1; then
|
|
3241
2601
|
emit_event "loop.restart" "restart=$RESTART_COUNT" "max=$MAX_RESTARTS" "iteration=$ITERATION"
|
|
3242
2602
|
fi
|