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.
Files changed (283) hide show
  1. package/.claude/agents/code-reviewer.md +2 -0
  2. package/.claude/agents/devops-engineer.md +2 -0
  3. package/.claude/agents/doc-fleet-agent.md +2 -0
  4. package/.claude/agents/pipeline-agent.md +2 -0
  5. package/.claude/agents/shell-script-specialist.md +2 -0
  6. package/.claude/agents/test-specialist.md +2 -0
  7. package/.claude/hooks/agent-crash-capture.sh +32 -0
  8. package/.claude/hooks/post-tool-use.sh +3 -2
  9. package/.claude/hooks/pre-tool-use.sh +35 -3
  10. package/README.md +22 -8
  11. package/claude-code/hooks/config-change.sh +18 -0
  12. package/claude-code/hooks/instructions-reloaded.sh +7 -0
  13. package/claude-code/hooks/worktree-create.sh +25 -0
  14. package/claude-code/hooks/worktree-remove.sh +20 -0
  15. package/config/code-constitution.json +130 -0
  16. package/config/defaults.json +25 -2
  17. package/config/policy.json +1 -1
  18. package/dashboard/middleware/auth.ts +134 -0
  19. package/dashboard/middleware/constants.ts +21 -0
  20. package/dashboard/public/index.html +8 -6
  21. package/dashboard/public/styles.css +176 -97
  22. package/dashboard/routes/auth.ts +38 -0
  23. package/dashboard/server.ts +117 -25
  24. package/dashboard/services/config.ts +26 -0
  25. package/dashboard/services/db.ts +118 -0
  26. package/dashboard/src/canvas/pixel-agent.ts +298 -0
  27. package/dashboard/src/canvas/pixel-sprites.ts +440 -0
  28. package/dashboard/src/canvas/shipyard-effects.ts +367 -0
  29. package/dashboard/src/canvas/shipyard-scene.ts +616 -0
  30. package/dashboard/src/canvas/submarine-layout.ts +267 -0
  31. package/dashboard/src/components/header.ts +8 -7
  32. package/dashboard/src/core/api.ts +5 -0
  33. package/dashboard/src/core/router.ts +1 -0
  34. package/dashboard/src/design/submarine-theme.ts +253 -0
  35. package/dashboard/src/main.ts +2 -0
  36. package/dashboard/src/types/api.ts +12 -1
  37. package/dashboard/src/views/activity.ts +2 -1
  38. package/dashboard/src/views/metrics.ts +69 -1
  39. package/dashboard/src/views/shipyard.ts +39 -0
  40. package/dashboard/types/index.ts +166 -0
  41. package/docs/plans/2026-02-28-compound-audit-and-shipyard-design.md +186 -0
  42. package/docs/plans/2026-02-28-skipper-shipwright-implementation-plan.md +1182 -0
  43. package/docs/plans/2026-02-28-skipper-shipwright-integration-design.md +531 -0
  44. package/docs/plans/2026-03-01-ai-powered-skill-injection-design.md +298 -0
  45. package/docs/plans/2026-03-01-ai-powered-skill-injection-plan.md +1109 -0
  46. package/docs/plans/2026-03-01-capabilities-cleanup-plan.md +658 -0
  47. package/docs/plans/2026-03-01-clean-architecture-plan.md +924 -0
  48. package/docs/plans/2026-03-01-compound-audit-cascade-design.md +191 -0
  49. package/docs/plans/2026-03-01-compound-audit-cascade-plan.md +921 -0
  50. package/docs/plans/2026-03-01-deep-integration-plan.md +851 -0
  51. package/docs/plans/2026-03-01-pipeline-audit-trail-design.md +145 -0
  52. package/docs/plans/2026-03-01-pipeline-audit-trail-plan.md +770 -0
  53. package/docs/plans/2026-03-01-refined-depths-brand-design.md +382 -0
  54. package/docs/plans/2026-03-01-refined-depths-implementation.md +599 -0
  55. package/docs/plans/2026-03-01-skipper-kernel-integration-design.md +203 -0
  56. package/docs/plans/2026-03-01-unified-platform-design.md +272 -0
  57. package/docs/plans/2026-03-07-claude-code-feature-integration-design.md +189 -0
  58. package/docs/plans/2026-03-07-claude-code-feature-integration-plan.md +1165 -0
  59. package/docs/research/BACKLOG_QUICK_REFERENCE.md +352 -0
  60. package/docs/research/CUTTING_EDGE_RESEARCH_2026.md +546 -0
  61. package/docs/research/RESEARCH_INDEX.md +439 -0
  62. package/docs/research/RESEARCH_SOURCES.md +440 -0
  63. package/docs/research/RESEARCH_SUMMARY.txt +275 -0
  64. package/docs/superpowers/specs/2026-03-10-pipeline-quality-revolution-design.md +341 -0
  65. package/package.json +2 -2
  66. package/scripts/lib/adaptive-model.sh +427 -0
  67. package/scripts/lib/adaptive-timeout.sh +316 -0
  68. package/scripts/lib/audit-trail.sh +309 -0
  69. package/scripts/lib/auto-recovery.sh +471 -0
  70. package/scripts/lib/bandit-selector.sh +431 -0
  71. package/scripts/lib/bootstrap.sh +104 -2
  72. package/scripts/lib/causal-graph.sh +455 -0
  73. package/scripts/lib/compat.sh +126 -0
  74. package/scripts/lib/compound-audit.sh +337 -0
  75. package/scripts/lib/constitutional.sh +454 -0
  76. package/scripts/lib/context-budget.sh +359 -0
  77. package/scripts/lib/convergence.sh +594 -0
  78. package/scripts/lib/cost-optimizer.sh +634 -0
  79. package/scripts/lib/daemon-adaptive.sh +14 -2
  80. package/scripts/lib/daemon-dispatch.sh +106 -17
  81. package/scripts/lib/daemon-failure.sh +34 -4
  82. package/scripts/lib/daemon-patrol.sh +25 -4
  83. package/scripts/lib/daemon-poll-github.sh +361 -0
  84. package/scripts/lib/daemon-poll-health.sh +299 -0
  85. package/scripts/lib/daemon-poll.sh +27 -611
  86. package/scripts/lib/daemon-state.sh +119 -66
  87. package/scripts/lib/daemon-triage.sh +10 -0
  88. package/scripts/lib/dod-scorecard.sh +442 -0
  89. package/scripts/lib/error-actionability.sh +300 -0
  90. package/scripts/lib/formal-spec.sh +461 -0
  91. package/scripts/lib/helpers.sh +180 -5
  92. package/scripts/lib/intent-analysis.sh +409 -0
  93. package/scripts/lib/loop-convergence.sh +350 -0
  94. package/scripts/lib/loop-iteration.sh +682 -0
  95. package/scripts/lib/loop-progress.sh +48 -0
  96. package/scripts/lib/loop-restart.sh +185 -0
  97. package/scripts/lib/memory-effectiveness.sh +506 -0
  98. package/scripts/lib/mutation-executor.sh +352 -0
  99. package/scripts/lib/outcome-feedback.sh +521 -0
  100. package/scripts/lib/pipeline-cli.sh +336 -0
  101. package/scripts/lib/pipeline-commands.sh +1216 -0
  102. package/scripts/lib/pipeline-detection.sh +101 -3
  103. package/scripts/lib/pipeline-execution.sh +897 -0
  104. package/scripts/lib/pipeline-github.sh +28 -3
  105. package/scripts/lib/pipeline-intelligence-compound.sh +431 -0
  106. package/scripts/lib/pipeline-intelligence-scoring.sh +407 -0
  107. package/scripts/lib/pipeline-intelligence-skip.sh +181 -0
  108. package/scripts/lib/pipeline-intelligence.sh +104 -1138
  109. package/scripts/lib/pipeline-quality-bash-compat.sh +182 -0
  110. package/scripts/lib/pipeline-quality-checks.sh +17 -711
  111. package/scripts/lib/pipeline-quality-gates.sh +563 -0
  112. package/scripts/lib/pipeline-stages-build.sh +730 -0
  113. package/scripts/lib/pipeline-stages-delivery.sh +965 -0
  114. package/scripts/lib/pipeline-stages-intake.sh +1133 -0
  115. package/scripts/lib/pipeline-stages-monitor.sh +407 -0
  116. package/scripts/lib/pipeline-stages-review.sh +1022 -0
  117. package/scripts/lib/pipeline-stages.sh +161 -2901
  118. package/scripts/lib/pipeline-state.sh +36 -5
  119. package/scripts/lib/pipeline-util.sh +487 -0
  120. package/scripts/lib/policy-learner.sh +438 -0
  121. package/scripts/lib/process-reward.sh +493 -0
  122. package/scripts/lib/project-detect.sh +649 -0
  123. package/scripts/lib/quality-profile.sh +334 -0
  124. package/scripts/lib/recruit-commands.sh +885 -0
  125. package/scripts/lib/recruit-learning.sh +739 -0
  126. package/scripts/lib/recruit-roles.sh +648 -0
  127. package/scripts/lib/reward-aggregator.sh +458 -0
  128. package/scripts/lib/rl-optimizer.sh +362 -0
  129. package/scripts/lib/root-cause.sh +427 -0
  130. package/scripts/lib/scope-enforcement.sh +445 -0
  131. package/scripts/lib/session-restart.sh +493 -0
  132. package/scripts/lib/skill-memory.sh +300 -0
  133. package/scripts/lib/skill-registry.sh +775 -0
  134. package/scripts/lib/spec-driven.sh +476 -0
  135. package/scripts/lib/test-helpers.sh +18 -7
  136. package/scripts/lib/test-holdout.sh +429 -0
  137. package/scripts/lib/test-optimizer.sh +511 -0
  138. package/scripts/shipwright-file-suggest.sh +45 -0
  139. package/scripts/skills/adversarial-quality.md +61 -0
  140. package/scripts/skills/api-design.md +44 -0
  141. package/scripts/skills/architecture-design.md +50 -0
  142. package/scripts/skills/brainstorming.md +43 -0
  143. package/scripts/skills/data-pipeline.md +44 -0
  144. package/scripts/skills/deploy-safety.md +64 -0
  145. package/scripts/skills/documentation.md +38 -0
  146. package/scripts/skills/frontend-design.md +45 -0
  147. package/scripts/skills/generated/.gitkeep +0 -0
  148. package/scripts/skills/generated/_refinements/.gitkeep +0 -0
  149. package/scripts/skills/generated/_refinements/adversarial-quality.patch.md +3 -0
  150. package/scripts/skills/generated/_refinements/architecture-design.patch.md +3 -0
  151. package/scripts/skills/generated/_refinements/brainstorming.patch.md +3 -0
  152. package/scripts/skills/generated/cli-version-management.md +29 -0
  153. package/scripts/skills/generated/collection-system-validation.md +99 -0
  154. package/scripts/skills/generated/large-scale-c-refactoring-coordination.md +97 -0
  155. package/scripts/skills/generated/pattern-matching-similarity-scoring.md +195 -0
  156. package/scripts/skills/generated/test-parallelization-detection.md +65 -0
  157. package/scripts/skills/observability.md +79 -0
  158. package/scripts/skills/performance.md +48 -0
  159. package/scripts/skills/pr-quality.md +49 -0
  160. package/scripts/skills/product-thinking.md +43 -0
  161. package/scripts/skills/security-audit.md +49 -0
  162. package/scripts/skills/systematic-debugging.md +40 -0
  163. package/scripts/skills/testing-strategy.md +47 -0
  164. package/scripts/skills/two-stage-review.md +52 -0
  165. package/scripts/skills/validation-thoroughness.md +55 -0
  166. package/scripts/sw +9 -3
  167. package/scripts/sw-activity.sh +9 -8
  168. package/scripts/sw-adaptive.sh +8 -7
  169. package/scripts/sw-adversarial.sh +2 -1
  170. package/scripts/sw-architecture-enforcer.sh +3 -1
  171. package/scripts/sw-auth.sh +12 -2
  172. package/scripts/sw-autonomous.sh +5 -1
  173. package/scripts/sw-changelog.sh +4 -1
  174. package/scripts/sw-checkpoint.sh +2 -1
  175. package/scripts/sw-ci.sh +15 -6
  176. package/scripts/sw-cleanup.sh +4 -26
  177. package/scripts/sw-code-review.sh +45 -20
  178. package/scripts/sw-connect.sh +2 -1
  179. package/scripts/sw-context.sh +2 -1
  180. package/scripts/sw-cost.sh +107 -5
  181. package/scripts/sw-daemon.sh +71 -11
  182. package/scripts/sw-dashboard.sh +3 -1
  183. package/scripts/sw-db.sh +71 -20
  184. package/scripts/sw-decide.sh +8 -2
  185. package/scripts/sw-decompose.sh +360 -17
  186. package/scripts/sw-deps.sh +4 -1
  187. package/scripts/sw-developer-simulation.sh +4 -1
  188. package/scripts/sw-discovery.sh +378 -5
  189. package/scripts/sw-doc-fleet.sh +4 -1
  190. package/scripts/sw-docs-agent.sh +3 -1
  191. package/scripts/sw-docs.sh +2 -1
  192. package/scripts/sw-doctor.sh +453 -2
  193. package/scripts/sw-dora.sh +4 -1
  194. package/scripts/sw-durable.sh +12 -7
  195. package/scripts/sw-e2e-orchestrator.sh +17 -16
  196. package/scripts/sw-eventbus.sh +13 -4
  197. package/scripts/sw-evidence.sh +364 -12
  198. package/scripts/sw-feedback.sh +550 -9
  199. package/scripts/sw-fix.sh +20 -1
  200. package/scripts/sw-fleet-discover.sh +6 -2
  201. package/scripts/sw-fleet-viz.sh +9 -4
  202. package/scripts/sw-fleet.sh +5 -1
  203. package/scripts/sw-github-app.sh +18 -4
  204. package/scripts/sw-github-checks.sh +3 -2
  205. package/scripts/sw-github-deploy.sh +3 -2
  206. package/scripts/sw-github-graphql.sh +18 -7
  207. package/scripts/sw-guild.sh +5 -1
  208. package/scripts/sw-heartbeat.sh +5 -30
  209. package/scripts/sw-hello.sh +67 -0
  210. package/scripts/sw-hygiene.sh +10 -3
  211. package/scripts/sw-incident.sh +273 -5
  212. package/scripts/sw-init.sh +18 -2
  213. package/scripts/sw-instrument.sh +10 -2
  214. package/scripts/sw-intelligence.sh +44 -7
  215. package/scripts/sw-jira.sh +5 -1
  216. package/scripts/sw-launchd.sh +2 -1
  217. package/scripts/sw-linear.sh +4 -1
  218. package/scripts/sw-logs.sh +4 -1
  219. package/scripts/sw-loop.sh +436 -1076
  220. package/scripts/sw-memory.sh +357 -3
  221. package/scripts/sw-mission-control.sh +6 -1
  222. package/scripts/sw-model-router.sh +483 -27
  223. package/scripts/sw-otel.sh +15 -4
  224. package/scripts/sw-oversight.sh +14 -5
  225. package/scripts/sw-patrol-meta.sh +334 -0
  226. package/scripts/sw-pipeline-composer.sh +7 -1
  227. package/scripts/sw-pipeline-vitals.sh +12 -6
  228. package/scripts/sw-pipeline.sh +54 -2653
  229. package/scripts/sw-pm.sh +16 -8
  230. package/scripts/sw-pr-lifecycle.sh +2 -1
  231. package/scripts/sw-predictive.sh +17 -5
  232. package/scripts/sw-prep.sh +185 -2
  233. package/scripts/sw-ps.sh +5 -25
  234. package/scripts/sw-public-dashboard.sh +17 -4
  235. package/scripts/sw-quality.sh +14 -6
  236. package/scripts/sw-reaper.sh +8 -25
  237. package/scripts/sw-recruit.sh +156 -2303
  238. package/scripts/sw-regression.sh +19 -12
  239. package/scripts/sw-release-manager.sh +3 -1
  240. package/scripts/sw-release.sh +4 -1
  241. package/scripts/sw-remote.sh +3 -1
  242. package/scripts/sw-replay.sh +7 -1
  243. package/scripts/sw-retro.sh +158 -1
  244. package/scripts/sw-review-rerun.sh +3 -1
  245. package/scripts/sw-scale.sh +14 -5
  246. package/scripts/sw-security-audit.sh +6 -1
  247. package/scripts/sw-self-optimize.sh +173 -6
  248. package/scripts/sw-session.sh +9 -3
  249. package/scripts/sw-setup.sh +3 -1
  250. package/scripts/sw-stall-detector.sh +406 -0
  251. package/scripts/sw-standup.sh +15 -7
  252. package/scripts/sw-status.sh +3 -1
  253. package/scripts/sw-strategic.sh +14 -6
  254. package/scripts/sw-stream.sh +13 -4
  255. package/scripts/sw-swarm.sh +20 -7
  256. package/scripts/sw-team-stages.sh +13 -6
  257. package/scripts/sw-templates.sh +7 -31
  258. package/scripts/sw-testgen.sh +17 -6
  259. package/scripts/sw-tmux-pipeline.sh +4 -1
  260. package/scripts/sw-tmux-role-color.sh +2 -0
  261. package/scripts/sw-tmux-status.sh +1 -1
  262. package/scripts/sw-tmux.sh +37 -1
  263. package/scripts/sw-trace.sh +3 -1
  264. package/scripts/sw-tracker-github.sh +3 -0
  265. package/scripts/sw-tracker-jira.sh +3 -0
  266. package/scripts/sw-tracker-linear.sh +3 -0
  267. package/scripts/sw-tracker.sh +3 -1
  268. package/scripts/sw-triage.sh +3 -2
  269. package/scripts/sw-upgrade.sh +3 -1
  270. package/scripts/sw-ux.sh +5 -2
  271. package/scripts/sw-webhook.sh +5 -2
  272. package/scripts/sw-widgets.sh +9 -4
  273. package/scripts/sw-worktree.sh +15 -3
  274. package/scripts/test-skill-injection.sh +1233 -0
  275. package/templates/pipelines/autonomous.json +27 -3
  276. package/templates/pipelines/cost-aware.json +34 -8
  277. package/templates/pipelines/deployed.json +12 -0
  278. package/templates/pipelines/enterprise.json +12 -0
  279. package/templates/pipelines/fast.json +6 -0
  280. package/templates/pipelines/full.json +27 -3
  281. package/templates/pipelines/hotfix.json +6 -0
  282. package/templates/pipelines/standard.json +12 -0
  283. package/templates/pipelines/tdd.json +12 -0
@@ -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.1.0"
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 # Auto-extend iterations when work is incomplete
81
- EXTENSION_SIZE=5 # Additional iterations per extension
82
- MAX_EXTENSIONS=3 # Max number of extensions (hard cap safety net)
83
- EXTENSION_COUNT=0 # Current number of extensions applied
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=3 # Consecutive low-progress iterations before stopping
87
- MIN_PROGRESS_LINES=5 # Minimum insertions to count as progress
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 .result from last element
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
- extracted=$(jq -r '.[-1].result // empty' "$json_file" 2>/dev/null) || true
509
- if [[ -n "$extracted" ]]; then
510
- echo "$extracted" > "$log_file"
511
- return 0
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 no jq — can't parse, use raw
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:-/tmp}/diagnoses.txt"
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 test_log="$LOG_DIR/tests-iter-${ITERATION}.log"
1245
- TEST_LOG_FILE="$test_log"
1246
- echo -e " ${DIM}Running ${test_mode} tests...${RESET}"
1247
- # Wrap test command with timeout (5 min default) to prevent hanging
1248
- local test_timeout="${SW_TEST_TIMEOUT:-300}"
1249
- local test_wrapper="$active_test_cmd"
1250
- if command -v timeout >/dev/null 2>&1; then
1251
- test_wrapper="timeout ${test_timeout} bash -c $(printf '%q' "$active_test_cmd")"
1252
- elif command -v gtimeout >/dev/null 2>&1; then
1253
- test_wrapper="gtimeout ${test_timeout} bash -c $(printf '%q' "$active_test_cmd")"
1254
- fi
1255
- if bash -c "$test_wrapper" > "$test_log" 2>&1; then
1256
- TEST_PASSED=true
1257
- TEST_OUTPUT="All tests passed (${test_mode} mode)."
1258
- else
1259
- TEST_PASSED=false
1260
- TEST_OUTPUT="$(tail -50 "$test_log")"
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
- if [[ -n "$TEST_CMD" ]]; then
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 2>/dev/null | grep -cE '^\+.*(TODO|FIXME|HACK|XXX)' || true)"
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 hardcoded descriptions
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:-/tmp}/strategy-attempts.txt" 2>/dev/null || true
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
- if [[ "$RESTART_COUNT" -ge 5 ]]; then
3228
- warn "Hard restart cap (5) reached — stopping"
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