bmalph 1.0.0 → 2.2.1

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 (455) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -217
  3. package/bmad/bmm/agents/analyst.agent.yaml +43 -36
  4. package/bmad/bmm/agents/architect.agent.yaml +29 -28
  5. package/bmad/bmm/agents/dev.agent.yaml +38 -38
  6. package/bmad/bmm/agents/pm.agent.yaml +44 -46
  7. package/bmad/bmm/agents/qa.agent.yaml +58 -0
  8. package/bmad/bmm/agents/quick-flow-solo-dev.agent.yaml +32 -32
  9. package/bmad/bmm/agents/sm.agent.yaml +37 -36
  10. package/bmad/bmm/agents/tech-writer/tech-writer-sidecar/documentation-standards.md +223 -223
  11. package/bmad/bmm/agents/tech-writer/tech-writer.agent.yaml +46 -45
  12. package/bmad/bmm/agents/ux-designer.agent.yaml +27 -26
  13. package/bmad/bmm/data/project-context-template.md +26 -26
  14. package/bmad/bmm/module-help.csv +31 -31
  15. package/bmad/bmm/module.yaml +50 -44
  16. package/bmad/bmm/teams/default-party.csv +20 -21
  17. package/bmad/bmm/teams/team-fullstack.yaml +12 -12
  18. package/bmad/bmm/workflows/1-analysis/create-product-brief/product-brief.template.md +10 -10
  19. package/bmad/bmm/workflows/1-analysis/create-product-brief/steps/step-01-init.md +177 -177
  20. package/bmad/bmm/workflows/1-analysis/create-product-brief/steps/step-01b-continue.md +161 -161
  21. package/bmad/bmm/workflows/1-analysis/create-product-brief/steps/step-02-vision.md +199 -199
  22. package/bmad/bmm/workflows/1-analysis/create-product-brief/steps/step-03-users.md +202 -202
  23. package/bmad/bmm/workflows/1-analysis/create-product-brief/steps/step-04-metrics.md +205 -205
  24. package/bmad/bmm/workflows/1-analysis/create-product-brief/steps/step-05-scope.md +219 -219
  25. package/bmad/bmm/workflows/1-analysis/create-product-brief/steps/step-06-complete.md +162 -162
  26. package/bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md +57 -58
  27. package/bmad/bmm/workflows/1-analysis/research/domain-steps/step-01-init.md +137 -137
  28. package/bmad/bmm/workflows/1-analysis/research/domain-steps/step-02-domain-analysis.md +229 -229
  29. package/bmad/bmm/workflows/1-analysis/research/domain-steps/step-03-competitive-landscape.md +238 -238
  30. package/bmad/bmm/workflows/1-analysis/research/domain-steps/step-04-regulatory-focus.md +206 -206
  31. package/bmad/bmm/workflows/1-analysis/research/domain-steps/step-05-technical-trends.md +234 -234
  32. package/bmad/bmm/workflows/1-analysis/research/domain-steps/step-06-research-synthesis.md +443 -443
  33. package/bmad/bmm/workflows/1-analysis/research/market-steps/step-01-init.md +182 -182
  34. package/bmad/bmm/workflows/1-analysis/research/market-steps/step-02-customer-behavior.md +237 -237
  35. package/bmad/bmm/workflows/1-analysis/research/market-steps/step-03-customer-pain-points.md +249 -249
  36. package/bmad/bmm/workflows/1-analysis/research/market-steps/step-04-customer-decisions.md +259 -259
  37. package/bmad/bmm/workflows/1-analysis/research/market-steps/step-05-competitive-analysis.md +177 -177
  38. package/bmad/bmm/workflows/1-analysis/research/market-steps/step-06-research-completion.md +475 -475
  39. package/bmad/bmm/workflows/1-analysis/research/research.template.md +29 -29
  40. package/bmad/bmm/workflows/1-analysis/research/technical-steps/step-01-init.md +137 -137
  41. package/bmad/bmm/workflows/1-analysis/research/technical-steps/step-02-technical-overview.md +239 -239
  42. package/bmad/bmm/workflows/1-analysis/research/technical-steps/step-03-integration-patterns.md +248 -248
  43. package/bmad/bmm/workflows/1-analysis/research/technical-steps/step-04-architectural-patterns.md +202 -202
  44. package/bmad/bmm/workflows/1-analysis/research/technical-steps/step-05-implementation-research.md +233 -239
  45. package/bmad/bmm/workflows/1-analysis/research/technical-steps/step-06-research-synthesis.md +486 -486
  46. package/bmad/bmm/workflows/1-analysis/research/workflow-domain-research.md +54 -0
  47. package/bmad/bmm/workflows/1-analysis/research/workflow-market-research.md +54 -0
  48. package/bmad/bmm/workflows/1-analysis/research/workflow-technical-research.md +54 -0
  49. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/data/domain-complexity.csv +14 -12
  50. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/data/prd-purpose.md +197 -197
  51. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/data/project-types.csv +10 -10
  52. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-c/step-01-init.md +191 -191
  53. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-c/step-01b-continue.md +153 -153
  54. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-c/step-02-discovery.md +224 -224
  55. package/bmad/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-02b-vision.md +154 -0
  56. package/bmad/bmm/workflows/2-plan-workflows/create-prd/steps-c/step-02c-executive-summary.md +170 -0
  57. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-c/step-03-success.md +226 -226
  58. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-c/step-04-journeys.md +213 -213
  59. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-c/step-05-domain.md +207 -207
  60. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-c/step-06-innovation.md +226 -226
  61. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-c/step-07-project-type.md +237 -237
  62. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-c/step-08-scoping.md +228 -228
  63. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-c/step-09-functional.md +231 -231
  64. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-c/step-10-nonfunctional.md +242 -242
  65. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-c/step-11-polish.md +217 -217
  66. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-c/step-12-complete.md +124 -124
  67. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-e/step-e-01-discovery.md +247 -247
  68. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-e/step-e-01b-legacy-conversion.md +208 -208
  69. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-e/step-e-02-review.md +249 -249
  70. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-e/step-e-03-edit.md +253 -253
  71. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-e/step-e-04-complete.md +168 -168
  72. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-v/step-v-01-discovery.md +226 -218
  73. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-v/step-v-02-format-detection.md +191 -191
  74. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-v/step-v-02b-parity-check.md +209 -209
  75. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-v/step-v-03-density-validation.md +174 -174
  76. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-v/step-v-04-brief-coverage-validation.md +214 -214
  77. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-v/step-v-05-measurability-validation.md +228 -228
  78. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-v/step-v-06-traceability-validation.md +217 -217
  79. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-v/step-v-07-implementation-leakage-validation.md +205 -205
  80. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-v/step-v-08-domain-compliance-validation.md +243 -243
  81. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-v/step-v-09-project-type-validation.md +263 -263
  82. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-v/step-v-10-smart-validation.md +209 -209
  83. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-v/step-v-11-holistic-quality-validation.md +264 -264
  84. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-v/step-v-12-completeness-validation.md +242 -242
  85. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/steps-v/step-v-13-report-complete.md +231 -231
  86. package/bmad/bmm/workflows/2-plan-workflows/{prd → create-prd}/templates/prd-template.md +10 -10
  87. package/bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-create-prd.md +63 -0
  88. package/bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-edit-prd.md +65 -0
  89. package/bmad/bmm/workflows/2-plan-workflows/create-prd/workflow-validate-prd.md +63 -0
  90. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-01-init.md +135 -135
  91. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-01b-continue.md +127 -127
  92. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-02-discovery.md +190 -190
  93. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-03-core-experience.md +216 -216
  94. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-04-emotional-response.md +219 -219
  95. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-05-inspiration.md +234 -234
  96. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-06-design-system.md +252 -252
  97. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-07-defining-experience.md +254 -254
  98. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-08-visual-foundation.md +224 -224
  99. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-09-design-directions.md +224 -224
  100. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-10-user-journeys.md +241 -241
  101. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-11-component-strategy.md +248 -248
  102. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-12-ux-patterns.md +237 -237
  103. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-13-responsive-accessibility.md +264 -264
  104. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/steps/step-14-complete.md +171 -171
  105. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/ux-design-template.md +13 -13
  106. package/bmad/bmm/workflows/2-plan-workflows/create-ux-design/workflow.md +42 -43
  107. package/bmad/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-01-document-discovery.md +184 -190
  108. package/bmad/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-02-prd-analysis.md +172 -178
  109. package/bmad/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-03-epic-coverage-validation.md +173 -179
  110. package/bmad/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-04-ux-alignment.md +133 -139
  111. package/bmad/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-05-epic-quality-review.md +245 -252
  112. package/bmad/bmm/workflows/3-solutioning/check-implementation-readiness/steps/step-06-final-assessment.md +129 -135
  113. package/bmad/bmm/workflows/3-solutioning/check-implementation-readiness/templates/readiness-report-template.md +4 -4
  114. package/bmad/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md +54 -55
  115. package/bmad/bmm/workflows/3-solutioning/create-architecture/architecture-decision-template.md +12 -12
  116. package/bmad/bmm/workflows/3-solutioning/create-architecture/data/domain-complexity.csv +12 -10
  117. package/bmad/bmm/workflows/3-solutioning/create-architecture/data/project-types.csv +6 -6
  118. package/bmad/bmm/workflows/3-solutioning/create-architecture/steps/step-01-init.md +153 -153
  119. package/bmad/bmm/workflows/3-solutioning/create-architecture/steps/step-01b-continue.md +164 -164
  120. package/bmad/bmm/workflows/3-solutioning/create-architecture/steps/step-02-context.md +224 -224
  121. package/bmad/bmm/workflows/3-solutioning/create-architecture/steps/step-03-starter.md +331 -331
  122. package/bmad/bmm/workflows/3-solutioning/create-architecture/steps/step-04-decisions.md +318 -318
  123. package/bmad/bmm/workflows/3-solutioning/create-architecture/steps/step-05-patterns.md +359 -359
  124. package/bmad/bmm/workflows/3-solutioning/create-architecture/steps/step-06-structure.md +379 -379
  125. package/bmad/bmm/workflows/3-solutioning/create-architecture/steps/step-07-validation.md +359 -359
  126. package/bmad/bmm/workflows/3-solutioning/create-architecture/steps/step-08-complete.md +76 -76
  127. package/bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md +49 -50
  128. package/bmad/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-01-validate-prerequisites.md +259 -259
  129. package/bmad/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-02-design-epics.md +233 -233
  130. package/bmad/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-03-create-stories.md +272 -272
  131. package/bmad/bmm/workflows/3-solutioning/create-epics-and-stories/steps/step-04-final-validation.md +149 -149
  132. package/bmad/bmm/workflows/3-solutioning/create-epics-and-stories/templates/epics-template.md +57 -57
  133. package/bmad/bmm/workflows/3-solutioning/create-epics-and-stories/workflow.md +58 -59
  134. package/bmad/bmm/workflows/4-implementation/code-review/checklist.md +23 -23
  135. package/bmad/bmm/workflows/4-implementation/code-review/instructions.xml +226 -226
  136. package/bmad/bmm/workflows/4-implementation/code-review/workflow.yaml +44 -51
  137. package/bmad/bmm/workflows/4-implementation/correct-course/checklist.md +288 -288
  138. package/bmad/bmm/workflows/4-implementation/correct-course/instructions.md +207 -206
  139. package/bmad/bmm/workflows/4-implementation/correct-course/workflow.yaml +54 -60
  140. package/bmad/bmm/workflows/4-implementation/create-story/checklist.md +358 -358
  141. package/bmad/bmm/workflows/4-implementation/create-story/instructions.xml +346 -345
  142. package/bmad/bmm/workflows/4-implementation/create-story/template.md +49 -49
  143. package/bmad/bmm/workflows/4-implementation/create-story/workflow.yaml +53 -61
  144. package/bmad/bmm/workflows/4-implementation/dev-story/checklist.md +80 -80
  145. package/bmad/bmm/workflows/4-implementation/dev-story/instructions.xml +410 -410
  146. package/bmad/bmm/workflows/4-implementation/dev-story/workflow.yaml +21 -27
  147. package/bmad/bmm/workflows/4-implementation/retrospective/instructions.md +1444 -1443
  148. package/bmad/bmm/workflows/4-implementation/retrospective/workflow.yaml +53 -58
  149. package/bmad/bmm/workflows/4-implementation/sprint-planning/checklist.md +33 -33
  150. package/bmad/bmm/workflows/4-implementation/sprint-planning/instructions.md +226 -225
  151. package/bmad/bmm/workflows/4-implementation/sprint-planning/sprint-status-template.yaml +55 -55
  152. package/bmad/bmm/workflows/4-implementation/sprint-planning/workflow.yaml +47 -54
  153. package/bmad/bmm/workflows/4-implementation/sprint-status/instructions.md +230 -229
  154. package/bmad/bmm/workflows/4-implementation/sprint-status/workflow.yaml +25 -36
  155. package/bmad/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-01-mode-detection.md +174 -156
  156. package/bmad/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-02-context-gathering.md +118 -120
  157. package/bmad/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-03-execute.md +111 -113
  158. package/bmad/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-04-self-check.md +111 -113
  159. package/bmad/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-05-adversarial-review.md +104 -106
  160. package/bmad/bmm/workflows/bmad-quick-flow/quick-dev/steps/step-06-resolve-findings.md +146 -140
  161. package/bmad/bmm/workflows/bmad-quick-flow/quick-dev/workflow.md +50 -50
  162. package/bmad/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-01-understand.md +191 -189
  163. package/bmad/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-02-investigate.md +144 -144
  164. package/bmad/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-03-generate.md +127 -128
  165. package/bmad/bmm/workflows/bmad-quick-flow/quick-spec/steps/step-04-review.md +200 -191
  166. package/bmad/bmm/workflows/bmad-quick-flow/quick-spec/tech-spec-template.md +74 -74
  167. package/bmad/bmm/workflows/bmad-quick-flow/quick-spec/workflow.md +79 -79
  168. package/bmad/bmm/workflows/document-project/checklist.md +245 -245
  169. package/bmad/bmm/workflows/document-project/documentation-requirements.csv +12 -12
  170. package/bmad/bmm/workflows/document-project/instructions.md +130 -221
  171. package/bmad/bmm/workflows/document-project/templates/deep-dive-template.md +345 -345
  172. package/bmad/bmm/workflows/document-project/templates/index-template.md +169 -169
  173. package/bmad/bmm/workflows/document-project/templates/project-overview-template.md +103 -103
  174. package/bmad/bmm/workflows/document-project/templates/project-scan-report-schema.json +160 -160
  175. package/bmad/bmm/workflows/document-project/templates/source-tree-template.md +135 -135
  176. package/bmad/bmm/workflows/document-project/workflow.yaml +22 -30
  177. package/bmad/bmm/workflows/document-project/workflows/deep-dive-instructions.md +298 -298
  178. package/bmad/bmm/workflows/document-project/workflows/deep-dive.yaml +31 -31
  179. package/bmad/bmm/workflows/document-project/workflows/full-scan-instructions.md +1106 -1106
  180. package/bmad/bmm/workflows/document-project/workflows/full-scan.yaml +31 -31
  181. package/bmad/bmm/workflows/generate-project-context/project-context-template.md +21 -0
  182. package/bmad/bmm/workflows/generate-project-context/steps/step-01-discover.md +184 -0
  183. package/bmad/bmm/workflows/generate-project-context/steps/step-02-generate.md +318 -0
  184. package/bmad/bmm/workflows/generate-project-context/steps/step-03-complete.md +278 -0
  185. package/bmad/bmm/workflows/generate-project-context/workflow.md +49 -0
  186. package/bmad/bmm/workflows/qa/automate/checklist.md +33 -0
  187. package/bmad/bmm/workflows/qa/automate/instructions.md +110 -0
  188. package/bmad/bmm/workflows/qa/automate/workflow.yaml +44 -0
  189. package/bmad/core/agents/bmad-master.agent.yaml +30 -30
  190. package/bmad/core/module-help.csv +9 -11
  191. package/bmad/core/module.yaml +25 -25
  192. package/bmad/core/tasks/editorial-review-prose.xml +102 -91
  193. package/bmad/core/tasks/editorial-review-structure.xml +209 -198
  194. package/bmad/core/tasks/help.md +85 -0
  195. package/bmad/core/tasks/index-docs.xml +64 -64
  196. package/bmad/core/tasks/review-adversarial-general.xml +48 -48
  197. package/bmad/core/tasks/shard-doc.xml +107 -108
  198. package/bmad/core/tasks/workflow.xml +234 -234
  199. package/bmad/core/workflows/advanced-elicitation/methods.csv +51 -51
  200. package/bmad/core/workflows/advanced-elicitation/workflow.xml +116 -116
  201. package/bmad/core/workflows/brainstorming/brain-methods.csv +61 -61
  202. package/bmad/core/workflows/brainstorming/steps/step-01-session-setup.md +197 -197
  203. package/bmad/core/workflows/brainstorming/steps/step-01b-continue.md +122 -122
  204. package/bmad/core/workflows/brainstorming/steps/step-02a-user-selected.md +225 -225
  205. package/bmad/core/workflows/brainstorming/steps/step-02b-ai-recommended.md +237 -237
  206. package/bmad/core/workflows/brainstorming/steps/step-02c-random-selection.md +209 -209
  207. package/bmad/core/workflows/brainstorming/steps/step-02d-progressive-flow.md +264 -264
  208. package/bmad/core/workflows/brainstorming/steps/step-03-technique-execution.md +399 -399
  209. package/bmad/core/workflows/brainstorming/steps/step-04-idea-organization.md +303 -303
  210. package/bmad/core/workflows/brainstorming/template.md +15 -15
  211. package/bmad/core/workflows/brainstorming/workflow.md +58 -58
  212. package/bmad/core/workflows/party-mode/steps/step-01-agent-loading.md +138 -138
  213. package/bmad/core/workflows/party-mode/steps/step-02-discussion-orchestration.md +187 -187
  214. package/bmad/core/workflows/party-mode/steps/step-03-graceful-exit.md +168 -157
  215. package/bmad/core/workflows/party-mode/workflow.md +194 -194
  216. package/bundled-versions.json +3 -0
  217. package/dist/cli.js +61 -6
  218. package/dist/commands/check-updates.d.ts +5 -0
  219. package/dist/commands/check-updates.js +63 -0
  220. package/dist/commands/doctor.d.ts +39 -1
  221. package/dist/commands/doctor.js +348 -79
  222. package/dist/commands/init.d.ts +2 -0
  223. package/dist/commands/init.js +41 -15
  224. package/dist/commands/status.d.ts +7 -1
  225. package/dist/commands/status.js +111 -42
  226. package/dist/commands/upgrade.d.ts +7 -1
  227. package/dist/commands/upgrade.js +43 -12
  228. package/dist/installer.d.ts +19 -2
  229. package/dist/installer.js +305 -66
  230. package/dist/transition/artifacts.d.ts +2 -0
  231. package/dist/transition/artifacts.js +46 -0
  232. package/dist/transition/context.d.ts +19 -0
  233. package/dist/transition/context.js +261 -0
  234. package/dist/transition/fix-plan.d.ts +15 -0
  235. package/dist/transition/fix-plan.js +94 -0
  236. package/dist/transition/index.d.ts +9 -0
  237. package/dist/transition/index.js +16 -0
  238. package/dist/transition/orchestration.d.ts +2 -0
  239. package/dist/transition/orchestration.js +243 -0
  240. package/dist/transition/specs-changelog.d.ts +3 -0
  241. package/dist/transition/specs-changelog.js +75 -0
  242. package/dist/transition/specs-index.d.ts +22 -0
  243. package/dist/transition/specs-index.js +157 -0
  244. package/dist/transition/story-parsing.d.ts +7 -0
  245. package/dist/transition/story-parsing.js +124 -0
  246. package/dist/transition/tech-stack.d.ts +3 -0
  247. package/dist/transition/tech-stack.js +79 -0
  248. package/dist/transition/types.d.ts +60 -0
  249. package/dist/transition/types.js +1 -0
  250. package/dist/utils/config.d.ts +4 -0
  251. package/dist/utils/config.js +14 -4
  252. package/dist/utils/constants.d.ts +70 -0
  253. package/dist/utils/constants.js +97 -0
  254. package/dist/utils/dryrun.d.ts +7 -0
  255. package/dist/utils/dryrun.js +48 -0
  256. package/dist/utils/errors.d.ts +63 -0
  257. package/dist/utils/errors.js +86 -0
  258. package/dist/utils/file-system.d.ts +24 -0
  259. package/dist/utils/file-system.js +99 -0
  260. package/dist/utils/github.d.ts +83 -0
  261. package/dist/utils/github.js +230 -0
  262. package/dist/utils/json.js +3 -3
  263. package/dist/utils/logger.d.ts +6 -0
  264. package/dist/utils/logger.js +27 -0
  265. package/dist/utils/state.d.ts +4 -7
  266. package/dist/utils/state.js +147 -26
  267. package/dist/utils/validate.d.ts +40 -0
  268. package/dist/utils/validate.js +175 -1
  269. package/package.json +75 -59
  270. package/ralph/RALPH-REFERENCE.md +412 -0
  271. package/ralph/lib/circuit_breaker.sh +463 -330
  272. package/ralph/lib/date_utils.sh +104 -53
  273. package/ralph/lib/enable_core.sh +815 -0
  274. package/ralph/lib/response_analyzer.sh +884 -768
  275. package/ralph/lib/task_sources.sh +577 -0
  276. package/ralph/lib/timeout_utils.sh +145 -145
  277. package/ralph/lib/wizard_utils.sh +547 -0
  278. package/ralph/ralph_import.sh +636 -0
  279. package/ralph/ralph_loop.sh +1793 -1391
  280. package/ralph/ralph_monitor.sh +125 -0
  281. package/ralph/templates/AGENT.md +158 -158
  282. package/ralph/templates/PROMPT.md +285 -292
  283. package/ralph/templates/fix_plan.md +27 -27
  284. package/ralph/templates/ralphrc.template +102 -0
  285. package/ralph/templates/specs/.gitkeep +1 -1
  286. package/slash-commands/advanced-elicitation.md +1 -1
  287. package/slash-commands/adversarial-review.md +1 -1
  288. package/slash-commands/analyst.md +1 -1
  289. package/slash-commands/architect.md +1 -1
  290. package/slash-commands/bmad-help.md +1 -1
  291. package/slash-commands/bmalph-implement.md +152 -152
  292. package/slash-commands/brainstorm-project.md +1 -1
  293. package/slash-commands/brainstorming.md +1 -1
  294. package/slash-commands/correct-course.md +1 -1
  295. package/slash-commands/create-architecture.md +1 -1
  296. package/slash-commands/create-brief.md +1 -1
  297. package/slash-commands/create-epics-stories.md +1 -1
  298. package/slash-commands/create-prd.md +1 -1
  299. package/slash-commands/create-story.md +1 -1
  300. package/slash-commands/create-ux.md +1 -1
  301. package/slash-commands/dev.md +1 -1
  302. package/slash-commands/document-project.md +1 -1
  303. package/slash-commands/domain-research.md +1 -1
  304. package/slash-commands/editorial-prose.md +1 -1
  305. package/slash-commands/editorial-structure.md +1 -1
  306. package/slash-commands/execute-workflow.md +1 -1
  307. package/slash-commands/generate-project-context.md +1 -0
  308. package/slash-commands/implementation-readiness.md +1 -1
  309. package/slash-commands/index-docs.md +1 -1
  310. package/slash-commands/market-research.md +1 -1
  311. package/slash-commands/party-mode.md +1 -1
  312. package/slash-commands/pm.md +1 -1
  313. package/slash-commands/qa-automate.md +1 -0
  314. package/slash-commands/qa.md +1 -0
  315. package/slash-commands/quick-dev.md +1 -1
  316. package/slash-commands/quick-flow-solo-dev.md +1 -1
  317. package/slash-commands/retrospective.md +1 -1
  318. package/slash-commands/shard-doc.md +1 -1
  319. package/slash-commands/sm.md +1 -1
  320. package/slash-commands/sprint-planning.md +1 -1
  321. package/slash-commands/sprint-status.md +1 -1
  322. package/slash-commands/tech-spec.md +1 -1
  323. package/slash-commands/tech-writer.md +1 -0
  324. package/slash-commands/technical-research.md +1 -1
  325. package/slash-commands/ux-designer.md +1 -1
  326. package/slash-commands/validate-architecture.md +1 -1
  327. package/slash-commands/validate-brief.md +1 -1
  328. package/slash-commands/validate-epics-stories.md +1 -1
  329. package/slash-commands/validate-prd.md +1 -1
  330. package/slash-commands/validate-story.md +1 -1
  331. package/slash-commands/validate-ux.md +1 -1
  332. package/bmad/bmm/agents/tea.agent.yaml +0 -63
  333. package/bmad/bmm/sub-modules/claude-code/config.yaml +0 -4
  334. package/bmad/bmm/sub-modules/claude-code/injections.yaml +0 -242
  335. package/bmad/bmm/sub-modules/claude-code/readme.md +0 -87
  336. package/bmad/bmm/testarch/knowledge/adr-quality-readiness-checklist.md +0 -350
  337. package/bmad/bmm/testarch/knowledge/api-request.md +0 -442
  338. package/bmad/bmm/testarch/knowledge/api-testing-patterns.md +0 -843
  339. package/bmad/bmm/testarch/knowledge/auth-session.md +0 -552
  340. package/bmad/bmm/testarch/knowledge/burn-in.md +0 -273
  341. package/bmad/bmm/testarch/knowledge/ci-burn-in.md +0 -675
  342. package/bmad/bmm/testarch/knowledge/component-tdd.md +0 -486
  343. package/bmad/bmm/testarch/knowledge/contract-testing.md +0 -957
  344. package/bmad/bmm/testarch/knowledge/data-factories.md +0 -500
  345. package/bmad/bmm/testarch/knowledge/email-auth.md +0 -721
  346. package/bmad/bmm/testarch/knowledge/error-handling.md +0 -725
  347. package/bmad/bmm/testarch/knowledge/feature-flags.md +0 -750
  348. package/bmad/bmm/testarch/knowledge/file-utils.md +0 -463
  349. package/bmad/bmm/testarch/knowledge/fixture-architecture.md +0 -401
  350. package/bmad/bmm/testarch/knowledge/fixtures-composition.md +0 -382
  351. package/bmad/bmm/testarch/knowledge/intercept-network-call.md +0 -430
  352. package/bmad/bmm/testarch/knowledge/log.md +0 -429
  353. package/bmad/bmm/testarch/knowledge/network-error-monitor.md +0 -405
  354. package/bmad/bmm/testarch/knowledge/network-first.md +0 -486
  355. package/bmad/bmm/testarch/knowledge/network-recorder.md +0 -527
  356. package/bmad/bmm/testarch/knowledge/nfr-criteria.md +0 -670
  357. package/bmad/bmm/testarch/knowledge/overview.md +0 -286
  358. package/bmad/bmm/testarch/knowledge/playwright-config.md +0 -730
  359. package/bmad/bmm/testarch/knowledge/probability-impact.md +0 -601
  360. package/bmad/bmm/testarch/knowledge/recurse.md +0 -421
  361. package/bmad/bmm/testarch/knowledge/risk-governance.md +0 -615
  362. package/bmad/bmm/testarch/knowledge/selective-testing.md +0 -732
  363. package/bmad/bmm/testarch/knowledge/selector-resilience.md +0 -527
  364. package/bmad/bmm/testarch/knowledge/test-healing-patterns.md +0 -644
  365. package/bmad/bmm/testarch/knowledge/test-levels-framework.md +0 -473
  366. package/bmad/bmm/testarch/knowledge/test-priorities-matrix.md +0 -373
  367. package/bmad/bmm/testarch/knowledge/test-quality.md +0 -664
  368. package/bmad/bmm/testarch/knowledge/timing-debugging.md +0 -372
  369. package/bmad/bmm/testarch/knowledge/visual-debugging.md +0 -524
  370. package/bmad/bmm/testarch/tea-index.csv +0 -35
  371. package/bmad/bmm/workflows/1-analysis/research/market-steps/step-02-customer-insights.md +0 -200
  372. package/bmad/bmm/workflows/1-analysis/research/workflow.md +0 -173
  373. package/bmad/bmm/workflows/2-plan-workflows/prd/validation-report-prd-workflow.md +0 -433
  374. package/bmad/bmm/workflows/2-plan-workflows/prd/workflow.md +0 -150
  375. package/bmad/bmm/workflows/bmad-quick-flow/quick-dev/data/project-levels.yaml +0 -59
  376. package/bmad/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-library.json +0 -90
  377. package/bmad/bmm/workflows/excalidraw-diagrams/_shared/excalidraw-templates.yaml +0 -127
  378. package/bmad/bmm/workflows/excalidraw-diagrams/create-dataflow/checklist.md +0 -39
  379. package/bmad/bmm/workflows/excalidraw-diagrams/create-dataflow/instructions.md +0 -130
  380. package/bmad/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml +0 -27
  381. package/bmad/bmm/workflows/excalidraw-diagrams/create-diagram/checklist.md +0 -43
  382. package/bmad/bmm/workflows/excalidraw-diagrams/create-diagram/instructions.md +0 -141
  383. package/bmad/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml +0 -27
  384. package/bmad/bmm/workflows/excalidraw-diagrams/create-flowchart/checklist.md +0 -49
  385. package/bmad/bmm/workflows/excalidraw-diagrams/create-flowchart/instructions.md +0 -241
  386. package/bmad/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml +0 -27
  387. package/bmad/bmm/workflows/excalidraw-diagrams/create-wireframe/checklist.md +0 -38
  388. package/bmad/bmm/workflows/excalidraw-diagrams/create-wireframe/instructions.md +0 -133
  389. package/bmad/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml +0 -27
  390. package/bmad/bmm/workflows/testarch/atdd/atdd-checklist-template.md +0 -363
  391. package/bmad/bmm/workflows/testarch/atdd/checklist.md +0 -374
  392. package/bmad/bmm/workflows/testarch/atdd/instructions.md +0 -806
  393. package/bmad/bmm/workflows/testarch/atdd/workflow.yaml +0 -47
  394. package/bmad/bmm/workflows/testarch/automate/checklist.md +0 -582
  395. package/bmad/bmm/workflows/testarch/automate/instructions.md +0 -1324
  396. package/bmad/bmm/workflows/testarch/automate/workflow.yaml +0 -54
  397. package/bmad/bmm/workflows/testarch/ci/checklist.md +0 -247
  398. package/bmad/bmm/workflows/testarch/ci/github-actions-template.yaml +0 -198
  399. package/bmad/bmm/workflows/testarch/ci/gitlab-ci-template.yaml +0 -149
  400. package/bmad/bmm/workflows/testarch/ci/instructions.md +0 -536
  401. package/bmad/bmm/workflows/testarch/ci/workflow.yaml +0 -47
  402. package/bmad/bmm/workflows/testarch/framework/checklist.md +0 -320
  403. package/bmad/bmm/workflows/testarch/framework/instructions.md +0 -481
  404. package/bmad/bmm/workflows/testarch/framework/workflow.yaml +0 -49
  405. package/bmad/bmm/workflows/testarch/nfr-assess/checklist.md +0 -407
  406. package/bmad/bmm/workflows/testarch/nfr-assess/instructions.md +0 -726
  407. package/bmad/bmm/workflows/testarch/nfr-assess/nfr-report-template.md +0 -461
  408. package/bmad/bmm/workflows/testarch/nfr-assess/workflow.yaml +0 -49
  409. package/bmad/bmm/workflows/testarch/test-design/checklist.md +0 -407
  410. package/bmad/bmm/workflows/testarch/test-design/instructions.md +0 -1158
  411. package/bmad/bmm/workflows/testarch/test-design/test-design-architecture-template.md +0 -213
  412. package/bmad/bmm/workflows/testarch/test-design/test-design-qa-template.md +0 -286
  413. package/bmad/bmm/workflows/testarch/test-design/test-design-template.md +0 -294
  414. package/bmad/bmm/workflows/testarch/test-design/workflow.yaml +0 -71
  415. package/bmad/bmm/workflows/testarch/test-review/checklist.md +0 -472
  416. package/bmad/bmm/workflows/testarch/test-review/instructions.md +0 -628
  417. package/bmad/bmm/workflows/testarch/test-review/test-review-template.md +0 -390
  418. package/bmad/bmm/workflows/testarch/test-review/workflow.yaml +0 -48
  419. package/bmad/bmm/workflows/testarch/trace/checklist.md +0 -642
  420. package/bmad/bmm/workflows/testarch/trace/instructions.md +0 -1030
  421. package/bmad/bmm/workflows/testarch/trace/trace-template.md +0 -675
  422. package/bmad/bmm/workflows/testarch/trace/workflow.yaml +0 -57
  423. package/bmad/core/resources/excalidraw/README.md +0 -160
  424. package/bmad/core/resources/excalidraw/excalidraw-helpers.md +0 -127
  425. package/bmad/core/resources/excalidraw/library-loader.md +0 -50
  426. package/bmad/core/resources/excalidraw/validate-json-instructions.md +0 -79
  427. package/bmad/core/tasks/bmad-help.md +0 -62
  428. package/dist/commands/guide.d.ts +0 -1
  429. package/dist/commands/guide.js +0 -19
  430. package/dist/commands/implement.d.ts +0 -1
  431. package/dist/commands/implement.js +0 -83
  432. package/dist/commands/plan.d.ts +0 -5
  433. package/dist/commands/plan.js +0 -44
  434. package/dist/commands/reset.d.ts +0 -5
  435. package/dist/commands/reset.js +0 -35
  436. package/dist/commands/resume.d.ts +0 -1
  437. package/dist/commands/resume.js +0 -44
  438. package/dist/commands/start.d.ts +0 -5
  439. package/dist/commands/start.js +0 -54
  440. package/dist/transition.d.ts +0 -52
  441. package/dist/transition.js +0 -656
  442. package/slash-commands/atdd.md +0 -1
  443. package/slash-commands/continuous-integration.md +0 -1
  444. package/slash-commands/create-dataflow.md +0 -1
  445. package/slash-commands/create-diagram.md +0 -1
  446. package/slash-commands/create-flowchart.md +0 -1
  447. package/slash-commands/create-wireframe.md +0 -1
  448. package/slash-commands/nfr-assess.md +0 -1
  449. package/slash-commands/tea.md +0 -1
  450. package/slash-commands/test-automate.md +0 -1
  451. package/slash-commands/test-design.md +0 -1
  452. package/slash-commands/test-framework.md +0 -1
  453. package/slash-commands/test-review.md +0 -1
  454. package/slash-commands/test-trace.md +0 -1
  455. package/slash-commands/validate-test-design.md +0 -1
@@ -1,1391 +1,1793 @@
1
- #!/bin/bash
2
-
3
- # Claude Code Ralph Loop with Rate Limiting and Documentation
4
- # Adaptation of the Ralph technique for Claude Code with usage management
5
-
6
- set -e # Exit on any error
7
-
8
- # Source library components
9
- SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
10
- source "$SCRIPT_DIR/lib/date_utils.sh"
11
- source "$SCRIPT_DIR/lib/timeout_utils.sh"
12
- source "$SCRIPT_DIR/lib/response_analyzer.sh"
13
- source "$SCRIPT_DIR/lib/circuit_breaker.sh"
14
-
15
- # Configuration
16
- # Ralph-specific files live in .ralph/ subfolder
17
- RALPH_DIR=".ralph"
18
- PROMPT_FILE="$RALPH_DIR/PROMPT.md"
19
- LOG_DIR="$RALPH_DIR/logs"
20
- DOCS_DIR="$RALPH_DIR/docs/generated"
21
- STATUS_FILE="$RALPH_DIR/status.json"
22
- PROGRESS_FILE="$RALPH_DIR/progress.json"
23
- CLAUDE_CODE_CMD="claude"
24
- MAX_CALLS_PER_HOUR=100 # Adjust based on your plan
25
- VERBOSE_PROGRESS=false # Default: no verbose progress updates
26
- CLAUDE_TIMEOUT_MINUTES=20 # Default: 20 minutes timeout for Claude Code execution
27
- SLEEP_DURATION=3600 # 1 hour in seconds
28
- CALL_COUNT_FILE="$RALPH_DIR/.call_count"
29
- TIMESTAMP_FILE="$RALPH_DIR/.last_reset"
30
- USE_TMUX=false
31
-
32
- # Modern Claude CLI configuration (Phase 1.1)
33
- CLAUDE_OUTPUT_FORMAT="json" # Options: json, text
34
- CLAUDE_ALLOWED_TOOLS="Write,Bash(git *),Read" # Comma-separated list of allowed tools
35
- CLAUDE_USE_CONTINUE=true # Enable session continuity
36
- CLAUDE_SESSION_FILE="$RALPH_DIR/.claude_session_id" # Session ID persistence file
37
- CLAUDE_MIN_VERSION="2.0.76" # Minimum required Claude CLI version
38
-
39
- # Session management configuration (Phase 1.2)
40
- # Note: SESSION_EXPIRATION_SECONDS is defined in lib/response_analyzer.sh (86400 = 24 hours)
41
- RALPH_SESSION_FILE="$RALPH_DIR/.ralph_session" # Ralph-specific session tracking (lifecycle)
42
- RALPH_SESSION_HISTORY_FILE="$RALPH_DIR/.ralph_session_history" # Session transition history
43
- # Session expiration: 24 hours default balances project continuity with fresh context
44
- # Too short = frequent context loss; Too long = stale context causes unpredictable behavior
45
- CLAUDE_SESSION_EXPIRY_HOURS=${CLAUDE_SESSION_EXPIRY_HOURS:-24}
46
-
47
- # Valid tool patterns for --allowed-tools validation
48
- # Tools can be exact matches or pattern matches with wildcards in parentheses
49
- VALID_TOOL_PATTERNS=(
50
- "Write"
51
- "Read"
52
- "Edit"
53
- "MultiEdit"
54
- "Glob"
55
- "Grep"
56
- "Task"
57
- "TodoWrite"
58
- "WebFetch"
59
- "WebSearch"
60
- "Bash"
61
- "Bash(git *)"
62
- "Bash(npm *)"
63
- "Bash(bats *)"
64
- "Bash(python *)"
65
- "Bash(node *)"
66
- "NotebookEdit"
67
- )
68
-
69
- # Exit detection configuration
70
- EXIT_SIGNALS_FILE="$RALPH_DIR/.exit_signals"
71
- RESPONSE_ANALYSIS_FILE="$RALPH_DIR/.response_analysis"
72
- MAX_CONSECUTIVE_TEST_LOOPS=3
73
- MAX_CONSECUTIVE_DONE_SIGNALS=2
74
- TEST_PERCENTAGE_THRESHOLD=30 # If more than 30% of recent loops are test-only, flag it
75
-
76
- # Colors for terminal output
77
- RED='\033[0;31m'
78
- GREEN='\033[0;32m'
79
- YELLOW='\033[1;33m'
80
- BLUE='\033[0;34m'
81
- PURPLE='\033[0;35m'
82
- NC='\033[0m' # No Color
83
-
84
- # Initialize directories
85
- mkdir -p "$LOG_DIR" "$DOCS_DIR"
86
-
87
- # Check if tmux is available
88
- check_tmux_available() {
89
- if ! command -v tmux &> /dev/null; then
90
- log_status "ERROR" "tmux is not installed. Please install tmux or run without --monitor flag."
91
- echo "Install tmux:"
92
- echo " Ubuntu/Debian: sudo apt-get install tmux"
93
- echo " macOS: brew install tmux"
94
- echo " CentOS/RHEL: sudo yum install tmux"
95
- exit 1
96
- fi
97
- }
98
-
99
- # Setup tmux session with monitor
100
- setup_tmux_session() {
101
- local session_name="ralph-$(date +%s)"
102
- local ralph_home="${RALPH_HOME:-$HOME/.ralph}"
103
-
104
- log_status "INFO" "Setting up tmux session: $session_name"
105
-
106
- # Create new tmux session detached
107
- tmux new-session -d -s "$session_name" -c "$(pwd)"
108
-
109
- # Split window vertically to create monitor pane on the right
110
- tmux split-window -h -t "$session_name" -c "$(pwd)"
111
-
112
- # Start monitor in the right pane
113
- if command -v ralph-monitor &> /dev/null; then
114
- tmux send-keys -t "$session_name:0.1" "ralph-monitor" Enter
115
- else
116
- tmux send-keys -t "$session_name:0.1" "'$ralph_home/ralph_monitor.sh'" Enter
117
- fi
118
-
119
- # Start ralph loop in the left pane (exclude tmux flag to avoid recursion)
120
- local ralph_cmd
121
- if command -v ralph &> /dev/null; then
122
- ralph_cmd="ralph"
123
- else
124
- ralph_cmd="'$ralph_home/ralph_loop.sh'"
125
- fi
126
-
127
- if [[ "$MAX_CALLS_PER_HOUR" != "100" ]]; then
128
- ralph_cmd="$ralph_cmd --calls $MAX_CALLS_PER_HOUR"
129
- fi
130
- if [[ "$PROMPT_FILE" != "$RALPH_DIR/PROMPT.md" ]]; then
131
- ralph_cmd="$ralph_cmd --prompt '$PROMPT_FILE'"
132
- fi
133
-
134
- tmux send-keys -t "$session_name:0.0" "$ralph_cmd" Enter
135
-
136
- # Focus on left pane (main ralph loop)
137
- tmux select-pane -t "$session_name:0.0"
138
-
139
- # Set window title
140
- tmux rename-window -t "$session_name:0" "Ralph: Loop | Monitor"
141
-
142
- log_status "SUCCESS" "Tmux session created. Attaching to session..."
143
- log_status "INFO" "Use Ctrl+B then D to detach from session"
144
- log_status "INFO" "Use 'tmux attach -t $session_name' to reattach"
145
-
146
- # Attach to session (this will block until session ends)
147
- tmux attach-session -t "$session_name"
148
-
149
- exit 0
150
- }
151
-
152
- # Initialize call tracking
153
- init_call_tracking() {
154
- log_status "INFO" "DEBUG: Entered init_call_tracking..."
155
- local current_hour=$(date +%Y%m%d%H)
156
- local last_reset_hour=""
157
-
158
- if [[ -f "$TIMESTAMP_FILE" ]]; then
159
- last_reset_hour=$(cat "$TIMESTAMP_FILE")
160
- fi
161
-
162
- # Reset counter if it's a new hour
163
- if [[ "$current_hour" != "$last_reset_hour" ]]; then
164
- echo "0" > "$CALL_COUNT_FILE"
165
- echo "$current_hour" > "$TIMESTAMP_FILE"
166
- log_status "INFO" "Call counter reset for new hour: $current_hour"
167
- fi
168
-
169
- # Initialize exit signals tracking if it doesn't exist
170
- if [[ ! -f "$EXIT_SIGNALS_FILE" ]]; then
171
- echo '{"test_only_loops": [], "done_signals": [], "completion_indicators": []}' > "$EXIT_SIGNALS_FILE"
172
- fi
173
-
174
- # Initialize circuit breaker
175
- init_circuit_breaker
176
-
177
- log_status "INFO" "DEBUG: Completed init_call_tracking successfully"
178
- }
179
-
180
- # Log function with timestamps and colors
181
- log_status() {
182
- local level=$1
183
- local message=$2
184
- local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
185
- local color=""
186
-
187
- case $level in
188
- "INFO") color=$BLUE ;;
189
- "WARN") color=$YELLOW ;;
190
- "ERROR") color=$RED ;;
191
- "SUCCESS") color=$GREEN ;;
192
- "LOOP") color=$PURPLE ;;
193
- esac
194
-
195
- echo -e "${color}[$timestamp] [$level] $message${NC}"
196
- echo "[$timestamp] [$level] $message" >> "$LOG_DIR/ralph.log"
197
- }
198
-
199
- # Update status JSON for external monitoring
200
- update_status() {
201
- local loop_count=$1
202
- local calls_made=$2
203
- local last_action=$3
204
- local status=$4
205
- local exit_reason=${5:-""}
206
-
207
- cat > "$STATUS_FILE" << STATUSEOF
208
- {
209
- "timestamp": "$(get_iso_timestamp)",
210
- "loop_count": $loop_count,
211
- "calls_made_this_hour": $calls_made,
212
- "max_calls_per_hour": $MAX_CALLS_PER_HOUR,
213
- "last_action": "$last_action",
214
- "status": "$status",
215
- "exit_reason": "$exit_reason",
216
- "next_reset": "$(get_next_hour_time)"
217
- }
218
- STATUSEOF
219
- }
220
-
221
- # Check if we can make another call
222
- can_make_call() {
223
- local calls_made=0
224
- if [[ -f "$CALL_COUNT_FILE" ]]; then
225
- calls_made=$(cat "$CALL_COUNT_FILE")
226
- fi
227
-
228
- if [[ $calls_made -ge $MAX_CALLS_PER_HOUR ]]; then
229
- return 1 # Cannot make call
230
- else
231
- return 0 # Can make call
232
- fi
233
- }
234
-
235
- # Increment call counter
236
- increment_call_counter() {
237
- local calls_made=0
238
- if [[ -f "$CALL_COUNT_FILE" ]]; then
239
- calls_made=$(cat "$CALL_COUNT_FILE")
240
- fi
241
-
242
- ((calls_made++))
243
- echo "$calls_made" > "$CALL_COUNT_FILE"
244
- echo "$calls_made"
245
- }
246
-
247
- # Wait for rate limit reset with countdown
248
- wait_for_reset() {
249
- local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
250
- log_status "WARN" "Rate limit reached ($calls_made/$MAX_CALLS_PER_HOUR). Waiting for reset..."
251
-
252
- # Calculate time until next hour
253
- local current_minute=$(date +%M)
254
- local current_second=$(date +%S)
255
- local wait_time=$(((60 - current_minute - 1) * 60 + (60 - current_second)))
256
-
257
- log_status "INFO" "Sleeping for $wait_time seconds until next hour..."
258
-
259
- # Countdown display
260
- while [[ $wait_time -gt 0 ]]; do
261
- local hours=$((wait_time / 3600))
262
- local minutes=$(((wait_time % 3600) / 60))
263
- local seconds=$((wait_time % 60))
264
-
265
- printf "\r${YELLOW}Time until reset: %02d:%02d:%02d${NC}" $hours $minutes $seconds
266
- sleep 1
267
- ((wait_time--))
268
- done
269
- printf "\n"
270
-
271
- # Reset counter
272
- echo "0" > "$CALL_COUNT_FILE"
273
- echo "$(date +%Y%m%d%H)" > "$TIMESTAMP_FILE"
274
- log_status "SUCCESS" "Rate limit reset! Ready for new calls."
275
- }
276
-
277
- # Check if we should gracefully exit
278
- should_exit_gracefully() {
279
- log_status "INFO" "DEBUG: Checking exit conditions..." >&2
280
-
281
- if [[ ! -f "$EXIT_SIGNALS_FILE" ]]; then
282
- log_status "INFO" "DEBUG: No exit signals file found, continuing..." >&2
283
- return 1 # Don't exit, file doesn't exist
284
- fi
285
-
286
- local signals=$(cat "$EXIT_SIGNALS_FILE")
287
- log_status "INFO" "DEBUG: Exit signals content: $signals" >&2
288
-
289
- # Count recent signals (last 5 loops) - with error handling
290
- local recent_test_loops
291
- local recent_done_signals
292
- local recent_completion_indicators
293
-
294
- recent_test_loops=$(echo "$signals" | jq '.test_only_loops | length' 2>/dev/null || echo "0")
295
- recent_done_signals=$(echo "$signals" | jq '.done_signals | length' 2>/dev/null || echo "0")
296
- recent_completion_indicators=$(echo "$signals" | jq '.completion_indicators | length' 2>/dev/null || echo "0")
297
-
298
- log_status "INFO" "DEBUG: Exit counts - test_loops:$recent_test_loops, done_signals:$recent_done_signals, completion:$recent_completion_indicators" >&2
299
-
300
- # Check for exit conditions
301
-
302
- # 1. Too many consecutive test-only loops
303
- if [[ $recent_test_loops -ge $MAX_CONSECUTIVE_TEST_LOOPS ]]; then
304
- log_status "WARN" "Exit condition: Too many test-focused loops ($recent_test_loops >= $MAX_CONSECUTIVE_TEST_LOOPS)"
305
- echo "test_saturation"
306
- return 0
307
- fi
308
-
309
- # 2. Multiple "done" signals
310
- if [[ $recent_done_signals -ge $MAX_CONSECUTIVE_DONE_SIGNALS ]]; then
311
- log_status "WARN" "Exit condition: Multiple completion signals ($recent_done_signals >= $MAX_CONSECUTIVE_DONE_SIGNALS)"
312
- echo "completion_signals"
313
- return 0
314
- fi
315
-
316
- # 3. Safety circuit breaker - force exit after 5 consecutive completion indicators
317
- # Bug #2 Fix: Prevents infinite loops when EXIT_SIGNAL is not explicitly set
318
- # but completion patterns clearly indicate work is done. Threshold of 5 is higher
319
- # than normal threshold (2) to avoid false positives while preventing API waste.
320
- if [[ $recent_completion_indicators -ge 5 ]]; then
321
- log_status "WARN" "🚨 SAFETY CIRCUIT BREAKER: Force exit after 5 consecutive completion indicators ($recent_completion_indicators)" >&2
322
- echo "safety_circuit_breaker"
323
- return 0
324
- fi
325
-
326
- # 4. Strong completion indicators (only if Claude's EXIT_SIGNAL is true)
327
- # This prevents premature exits when heuristics detect completion patterns
328
- # but Claude explicitly indicates work is still in progress via RALPH_STATUS block.
329
- # The exit_signal in .response_analysis represents Claude's explicit intent.
330
- local claude_exit_signal="false"
331
- if [[ -f "$RESPONSE_ANALYSIS_FILE" ]]; then
332
- claude_exit_signal=$(jq -r '.analysis.exit_signal // false' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "false")
333
- fi
334
-
335
- if [[ $recent_completion_indicators -ge 2 ]] && [[ "$claude_exit_signal" == "true" ]]; then
336
- log_status "WARN" "Exit condition: Strong completion indicators ($recent_completion_indicators) with EXIT_SIGNAL=true" >&2
337
- echo "project_complete"
338
- return 0
339
- elif [[ $recent_completion_indicators -ge 2 ]]; then
340
- log_status "INFO" "DEBUG: Completion indicators ($recent_completion_indicators) present but EXIT_SIGNAL=false, continuing..." >&2
341
- fi
342
-
343
- # 5. Check fix_plan.md for completion
344
- # Bug #3 Fix: Support indented markdown checkboxes with [[:space:]]* pattern
345
- if [[ -f "$RALPH_DIR/@fix_plan.md" ]]; then
346
- local total_items=$(grep -cE "^[[:space:]]*- \[" "$RALPH_DIR/@fix_plan.md" 2>/dev/null)
347
- local completed_items=$(grep -cE "^[[:space:]]*- \[x\]" "$RALPH_DIR/@fix_plan.md" 2>/dev/null)
348
-
349
- # Handle case where grep returns no matches (exit code 1)
350
- [[ -z "$total_items" ]] && total_items=0
351
- [[ -z "$completed_items" ]] && completed_items=0
352
-
353
- log_status "INFO" "DEBUG: .ralph/@fix_plan.md check - total_items:$total_items, completed_items:$completed_items" >&2
354
-
355
- if [[ $total_items -gt 0 ]] && [[ $completed_items -eq $total_items ]]; then
356
- log_status "WARN" "Exit condition: All fix_plan.md items completed ($completed_items/$total_items)" >&2
357
- echo "plan_complete"
358
- return 0
359
- fi
360
- else
361
- log_status "INFO" "DEBUG: .ralph/@fix_plan.md file not found" >&2
362
- fi
363
-
364
- log_status "INFO" "DEBUG: No exit conditions met, continuing loop" >&2
365
- echo "" # Return empty string instead of using return code
366
- }
367
-
368
- # =============================================================================
369
- # MODERN CLI HELPER FUNCTIONS (Phase 1.1)
370
- # =============================================================================
371
-
372
- # Check Claude CLI version for compatibility with modern flags
373
- check_claude_version() {
374
- local version=$($CLAUDE_CODE_CMD --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
375
-
376
- if [[ -z "$version" ]]; then
377
- log_status "WARN" "Cannot detect Claude CLI version, assuming compatible"
378
- return 0
379
- fi
380
-
381
- # Compare versions (simplified semver comparison)
382
- local required="$CLAUDE_MIN_VERSION"
383
-
384
- # Convert to comparable integers (major * 10000 + minor * 100 + patch)
385
- local ver_parts=(${version//./ })
386
- local req_parts=(${required//./ })
387
-
388
- local ver_num=$((${ver_parts[0]:-0} * 10000 + ${ver_parts[1]:-0} * 100 + ${ver_parts[2]:-0}))
389
- local req_num=$((${req_parts[0]:-0} * 10000 + ${req_parts[1]:-0} * 100 + ${req_parts[2]:-0}))
390
-
391
- if [[ $ver_num -lt $req_num ]]; then
392
- log_status "WARN" "Claude CLI version $version < $required. Some modern features may not work."
393
- log_status "WARN" "Consider upgrading: npm update -g @anthropic-ai/claude-code"
394
- return 1
395
- fi
396
-
397
- log_status "INFO" "Claude CLI version $version (>= $required) - modern features enabled"
398
- return 0
399
- }
400
-
401
- # Validate allowed tools against whitelist
402
- # Returns 0 if valid, 1 if invalid with error message
403
- validate_allowed_tools() {
404
- local tools_input=$1
405
-
406
- if [[ -z "$tools_input" ]]; then
407
- return 0 # Empty is valid (uses defaults)
408
- fi
409
-
410
- # Split by comma
411
- local IFS=','
412
- read -ra tools <<< "$tools_input"
413
-
414
- for tool in "${tools[@]}"; do
415
- # Trim whitespace
416
- tool=$(echo "$tool" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
417
-
418
- if [[ -z "$tool" ]]; then
419
- continue
420
- fi
421
-
422
- local valid=false
423
-
424
- # Check against valid patterns
425
- for pattern in "${VALID_TOOL_PATTERNS[@]}"; do
426
- if [[ "$tool" == "$pattern" ]]; then
427
- valid=true
428
- break
429
- fi
430
-
431
- # Check for Bash(*) pattern - any Bash with parentheses is allowed
432
- if [[ "$tool" =~ ^Bash\(.+\)$ ]]; then
433
- valid=true
434
- break
435
- fi
436
- done
437
-
438
- if [[ "$valid" == "false" ]]; then
439
- echo "Error: Invalid tool in --allowed-tools: '$tool'"
440
- echo "Valid tools: ${VALID_TOOL_PATTERNS[*]}"
441
- echo "Note: Bash(...) patterns with any content are allowed (e.g., 'Bash(git *)')"
442
- return 1
443
- fi
444
- done
445
-
446
- return 0
447
- }
448
-
449
- # Build loop context for Claude Code session
450
- # Provides loop-specific context via --append-system-prompt
451
- build_loop_context() {
452
- local loop_count=$1
453
- local context=""
454
-
455
- # Add loop number
456
- context="Loop #${loop_count}. "
457
-
458
- # Extract incomplete tasks from @fix_plan.md
459
- # Bug #3 Fix: Support indented markdown checkboxes with [[:space:]]* pattern
460
- if [[ -f "$RALPH_DIR/@fix_plan.md" ]]; then
461
- local incomplete_tasks=$(grep -cE "^[[:space:]]*- \[ \]" "$RALPH_DIR/@fix_plan.md" 2>/dev/null || echo "0")
462
- context+="Remaining tasks: ${incomplete_tasks}. "
463
- fi
464
-
465
- # Add circuit breaker state
466
- if [[ -f "$RALPH_DIR/.circuit_breaker_state" ]]; then
467
- local cb_state=$(jq -r '.state // "UNKNOWN"' "$RALPH_DIR/.circuit_breaker_state" 2>/dev/null)
468
- if [[ "$cb_state" != "CLOSED" && "$cb_state" != "null" && -n "$cb_state" ]]; then
469
- context+="Circuit breaker: ${cb_state}. "
470
- fi
471
- fi
472
-
473
- # Add previous loop summary (truncated)
474
- if [[ -f "$RESPONSE_ANALYSIS_FILE" ]]; then
475
- local prev_summary=$(jq -r '.analysis.work_summary // ""' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null | head -c 200)
476
- if [[ -n "$prev_summary" && "$prev_summary" != "null" ]]; then
477
- context+="Previous: ${prev_summary}"
478
- fi
479
- fi
480
-
481
- # Limit total length to ~500 chars
482
- echo "${context:0:500}"
483
- }
484
-
485
- # Get session file age in hours (cross-platform)
486
- # Returns: age in hours on stdout, or -1 if stat fails
487
- # Note: Returns 0 for files less than 1 hour old
488
- get_session_file_age_hours() {
489
- local file=$1
490
-
491
- if [[ ! -f "$file" ]]; then
492
- echo "0"
493
- return
494
- fi
495
-
496
- local os_type
497
- os_type=$(uname)
498
-
499
- local file_mtime
500
- if [[ "$os_type" == "Darwin" ]]; then
501
- # macOS (BSD stat)
502
- file_mtime=$(stat -f %m "$file" 2>/dev/null)
503
- else
504
- # Linux (GNU stat)
505
- file_mtime=$(stat -c %Y "$file" 2>/dev/null)
506
- fi
507
-
508
- # Handle stat failure - return -1 to indicate error
509
- # This prevents false expiration when stat fails
510
- if [[ -z "$file_mtime" || "$file_mtime" == "0" ]]; then
511
- echo "-1"
512
- return
513
- fi
514
-
515
- local current_time
516
- current_time=$(date +%s)
517
-
518
- local age_seconds=$((current_time - file_mtime))
519
- local age_hours=$((age_seconds / 3600))
520
-
521
- echo "$age_hours"
522
- }
523
-
524
- # Initialize or resume Claude session (with expiration check)
525
- #
526
- # Session Expiration Strategy:
527
- # - Default expiration: 24 hours (configurable via CLAUDE_SESSION_EXPIRY_HOURS)
528
- # - 24 hours chosen because: long enough for multi-day projects, short enough
529
- # to prevent stale context from causing unpredictable behavior
530
- # - Sessions auto-expire to ensure Claude starts fresh periodically
531
- #
532
- # Returns (stdout):
533
- # - Session ID string: when resuming a valid, non-expired session
534
- # - Empty string: when starting new session (no file, expired, or stat error)
535
- #
536
- # Return codes:
537
- # - 0: Always returns success (caller should check stdout for session ID)
538
- #
539
- init_claude_session() {
540
- if [[ -f "$CLAUDE_SESSION_FILE" ]]; then
541
- # Check session age
542
- local age_hours
543
- age_hours=$(get_session_file_age_hours "$CLAUDE_SESSION_FILE")
544
-
545
- # Handle stat failure (-1) - treat as needing new session
546
- # Don't expire sessions when we can't determine age
547
- if [[ $age_hours -eq -1 ]]; then
548
- log_status "WARN" "Could not determine session age, starting new session"
549
- rm -f "$CLAUDE_SESSION_FILE"
550
- echo ""
551
- return 0
552
- fi
553
-
554
- # Check if session has expired
555
- if [[ $age_hours -ge $CLAUDE_SESSION_EXPIRY_HOURS ]]; then
556
- log_status "INFO" "Session expired (${age_hours}h old, max ${CLAUDE_SESSION_EXPIRY_HOURS}h), starting new session"
557
- rm -f "$CLAUDE_SESSION_FILE"
558
- echo ""
559
- return 0
560
- fi
561
-
562
- # Session is valid, try to read it
563
- local session_id=$(cat "$CLAUDE_SESSION_FILE" 2>/dev/null)
564
- if [[ -n "$session_id" ]]; then
565
- log_status "INFO" "Resuming Claude session: ${session_id:0:20}... (${age_hours}h old)"
566
- echo "$session_id"
567
- return 0
568
- fi
569
- fi
570
-
571
- log_status "INFO" "Starting new Claude session"
572
- echo ""
573
- }
574
-
575
- # Save session ID after successful execution
576
- save_claude_session() {
577
- local output_file=$1
578
-
579
- # Try to extract session ID from JSON output
580
- if [[ -f "$output_file" ]]; then
581
- local session_id=$(jq -r '.metadata.session_id // .session_id // empty' "$output_file" 2>/dev/null)
582
- if [[ -n "$session_id" && "$session_id" != "null" ]]; then
583
- echo "$session_id" > "$CLAUDE_SESSION_FILE"
584
- log_status "INFO" "Saved Claude session: ${session_id:0:20}..."
585
- fi
586
- fi
587
- }
588
-
589
- # =============================================================================
590
- # SESSION LIFECYCLE MANAGEMENT FUNCTIONS (Phase 1.2)
591
- # =============================================================================
592
-
593
- # Get current session ID from Ralph session file
594
- # Returns: session ID string or empty if not found
595
- get_session_id() {
596
- if [[ ! -f "$RALPH_SESSION_FILE" ]]; then
597
- echo ""
598
- return 0
599
- fi
600
-
601
- # Extract session_id from JSON file (SC2155: separate declare from assign)
602
- local session_id
603
- session_id=$(jq -r '.session_id // ""' "$RALPH_SESSION_FILE" 2>/dev/null)
604
- local jq_status=$?
605
-
606
- # Handle jq failure or null/empty results
607
- if [[ $jq_status -ne 0 || -z "$session_id" || "$session_id" == "null" ]]; then
608
- session_id=""
609
- fi
610
- echo "$session_id"
611
- return 0
612
- }
613
-
614
- # Reset session with reason logging
615
- # Usage: reset_session "reason_for_reset"
616
- reset_session() {
617
- local reason=${1:-"manual_reset"}
618
-
619
- # Get current timestamp
620
- local reset_timestamp
621
- reset_timestamp=$(get_iso_timestamp)
622
-
623
- # Always create/overwrite the session file using jq for safe JSON escaping
624
- jq -n \
625
- --arg session_id "" \
626
- --arg created_at "" \
627
- --arg last_used "" \
628
- --arg reset_at "$reset_timestamp" \
629
- --arg reset_reason "$reason" \
630
- '{
631
- session_id: $session_id,
632
- created_at: $created_at,
633
- last_used: $last_used,
634
- reset_at: $reset_at,
635
- reset_reason: $reset_reason
636
- }' > "$RALPH_SESSION_FILE"
637
-
638
- # Also clear the Claude session file for consistency
639
- rm -f "$CLAUDE_SESSION_FILE" 2>/dev/null
640
-
641
- # Clear exit signals to prevent stale completion indicators from causing premature exit (issue #91)
642
- # This ensures a fresh start without leftover state from previous sessions
643
- if [[ -f "$EXIT_SIGNALS_FILE" ]]; then
644
- echo '{"test_only_loops": [], "done_signals": [], "completion_indicators": []}' > "$EXIT_SIGNALS_FILE"
645
- [[ "${VERBOSE_PROGRESS:-}" == "true" ]] && log_status "INFO" "Cleared exit signals file"
646
- fi
647
-
648
- # Clear response analysis to prevent stale EXIT_SIGNAL from previous session
649
- rm -f "$RESPONSE_ANALYSIS_FILE" 2>/dev/null
650
-
651
- # Log the session transition (non-fatal to prevent script exit under set -e)
652
- log_session_transition "active" "reset" "$reason" "${loop_count:-0}" || true
653
-
654
- log_status "INFO" "Session reset: $reason"
655
- }
656
-
657
- # Log session state transitions to history file
658
- # Usage: log_session_transition from_state to_state reason loop_number
659
- log_session_transition() {
660
- local from_state=$1
661
- local to_state=$2
662
- local reason=$3
663
- local loop_number=${4:-0}
664
-
665
- # Get timestamp once (SC2155: separate declare from assign)
666
- local ts
667
- ts=$(get_iso_timestamp)
668
-
669
- # Create transition entry using jq for safe JSON (SC2155: separate declare from assign)
670
- local transition
671
- transition=$(jq -n -c \
672
- --arg timestamp "$ts" \
673
- --arg from_state "$from_state" \
674
- --arg to_state "$to_state" \
675
- --arg reason "$reason" \
676
- --argjson loop_number "$loop_number" \
677
- '{
678
- timestamp: $timestamp,
679
- from_state: $from_state,
680
- to_state: $to_state,
681
- reason: $reason,
682
- loop_number: $loop_number
683
- }')
684
-
685
- # Read history file defensively - fallback to empty array on any failure
686
- local history
687
- if [[ -f "$RALPH_SESSION_HISTORY_FILE" ]]; then
688
- history=$(cat "$RALPH_SESSION_HISTORY_FILE" 2>/dev/null)
689
- # Validate JSON, fallback to empty array if corrupted
690
- if ! echo "$history" | jq empty 2>/dev/null; then
691
- history='[]'
692
- fi
693
- else
694
- history='[]'
695
- fi
696
-
697
- # Append transition and keep only last 50 entries
698
- local updated_history
699
- updated_history=$(echo "$history" | jq ". += [$transition] | .[-50:]" 2>/dev/null)
700
- local jq_status=$?
701
-
702
- # Only write if jq succeeded
703
- if [[ $jq_status -eq 0 && -n "$updated_history" ]]; then
704
- echo "$updated_history" > "$RALPH_SESSION_HISTORY_FILE"
705
- else
706
- # Fallback: start fresh with just this transition
707
- echo "[$transition]" > "$RALPH_SESSION_HISTORY_FILE"
708
- fi
709
- }
710
-
711
- # Generate a unique session ID using timestamp and random component
712
- generate_session_id() {
713
- local ts
714
- ts=$(date +%s)
715
- local rand
716
- rand=$RANDOM
717
- echo "ralph-${ts}-${rand}"
718
- }
719
-
720
- # Initialize session tracking (called at loop start)
721
- init_session_tracking() {
722
- local ts
723
- ts=$(get_iso_timestamp)
724
-
725
- # Create session file if it doesn't exist
726
- if [[ ! -f "$RALPH_SESSION_FILE" ]]; then
727
- local new_session_id
728
- new_session_id=$(generate_session_id)
729
-
730
- jq -n \
731
- --arg session_id "$new_session_id" \
732
- --arg created_at "$ts" \
733
- --arg last_used "$ts" \
734
- --arg reset_at "" \
735
- --arg reset_reason "" \
736
- '{
737
- session_id: $session_id,
738
- created_at: $created_at,
739
- last_used: $last_used,
740
- reset_at: $reset_at,
741
- reset_reason: $reset_reason
742
- }' > "$RALPH_SESSION_FILE"
743
-
744
- log_status "INFO" "Initialized session tracking (session: $new_session_id)"
745
- return 0
746
- fi
747
-
748
- # Validate existing session file
749
- if ! jq empty "$RALPH_SESSION_FILE" 2>/dev/null; then
750
- log_status "WARN" "Corrupted session file detected, recreating..."
751
- local new_session_id
752
- new_session_id=$(generate_session_id)
753
-
754
- jq -n \
755
- --arg session_id "$new_session_id" \
756
- --arg created_at "$ts" \
757
- --arg last_used "$ts" \
758
- --arg reset_at "$ts" \
759
- --arg reset_reason "corrupted_file_recovery" \
760
- '{
761
- session_id: $session_id,
762
- created_at: $created_at,
763
- last_used: $last_used,
764
- reset_at: $reset_at,
765
- reset_reason: $reset_reason
766
- }' > "$RALPH_SESSION_FILE"
767
- fi
768
- }
769
-
770
- # Update last_used timestamp in session file (called on each loop iteration)
771
- update_session_last_used() {
772
- if [[ ! -f "$RALPH_SESSION_FILE" ]]; then
773
- return 0
774
- fi
775
-
776
- local ts
777
- ts=$(get_iso_timestamp)
778
-
779
- # Update last_used in existing session file
780
- local updated
781
- updated=$(jq --arg last_used "$ts" '.last_used = $last_used' "$RALPH_SESSION_FILE" 2>/dev/null)
782
- local jq_status=$?
783
-
784
- if [[ $jq_status -eq 0 && -n "$updated" ]]; then
785
- echo "$updated" > "$RALPH_SESSION_FILE"
786
- fi
787
- }
788
-
789
- # Global array for Claude command arguments (avoids shell injection)
790
- declare -a CLAUDE_CMD_ARGS=()
791
-
792
- # Build Claude CLI command with modern flags using array (shell-injection safe)
793
- # Populates global CLAUDE_CMD_ARGS array for direct execution
794
- # Uses -p flag with prompt content (Claude CLI does not have --prompt-file)
795
- build_claude_command() {
796
- local prompt_file=$1
797
- local loop_context=$2
798
- local session_id=$3
799
-
800
- # Reset global array
801
- CLAUDE_CMD_ARGS=("$CLAUDE_CODE_CMD")
802
-
803
- # Check if prompt file exists
804
- if [[ ! -f "$prompt_file" ]]; then
805
- log_status "ERROR" "Prompt file not found: $prompt_file"
806
- return 1
807
- fi
808
-
809
- # Add output format flag
810
- if [[ "$CLAUDE_OUTPUT_FORMAT" == "json" ]]; then
811
- CLAUDE_CMD_ARGS+=("--output-format" "json")
812
- fi
813
-
814
- # Add allowed tools (each tool as separate array element)
815
- if [[ -n "$CLAUDE_ALLOWED_TOOLS" ]]; then
816
- CLAUDE_CMD_ARGS+=("--allowedTools")
817
- # Split by comma and add each tool
818
- local IFS=','
819
- read -ra tools_array <<< "$CLAUDE_ALLOWED_TOOLS"
820
- for tool in "${tools_array[@]}"; do
821
- # Trim whitespace
822
- tool=$(echo "$tool" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
823
- if [[ -n "$tool" ]]; then
824
- CLAUDE_CMD_ARGS+=("$tool")
825
- fi
826
- done
827
- fi
828
-
829
- # Add session continuity flag
830
- if [[ "$CLAUDE_USE_CONTINUE" == "true" ]]; then
831
- CLAUDE_CMD_ARGS+=("--continue")
832
- fi
833
-
834
- # Add loop context as system prompt (no escaping needed - array handles it)
835
- if [[ -n "$loop_context" ]]; then
836
- CLAUDE_CMD_ARGS+=("--append-system-prompt" "$loop_context")
837
- fi
838
-
839
- # Read prompt file content and use -p flag
840
- # Note: Claude CLI uses -p for prompts, not --prompt-file (which doesn't exist)
841
- # Array-based approach maintains shell injection safety
842
- local prompt_content
843
- prompt_content=$(cat "$prompt_file")
844
- CLAUDE_CMD_ARGS+=("-p" "$prompt_content")
845
- }
846
-
847
- # Main execution function
848
- execute_claude_code() {
849
- local timestamp=$(date '+%Y-%m-%d_%H-%M-%S')
850
- local output_file="$LOG_DIR/claude_output_${timestamp}.log"
851
- local loop_count=$1
852
- local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
853
- calls_made=$((calls_made + 1))
854
-
855
- log_status "LOOP" "Executing Claude Code (Call $calls_made/$MAX_CALLS_PER_HOUR)"
856
- local timeout_seconds=$((CLAUDE_TIMEOUT_MINUTES * 60))
857
- log_status "INFO" "⏳ Starting Claude Code execution... (timeout: ${CLAUDE_TIMEOUT_MINUTES}m)"
858
-
859
- # Build loop context for session continuity
860
- local loop_context=""
861
- if [[ "$CLAUDE_USE_CONTINUE" == "true" ]]; then
862
- loop_context=$(build_loop_context "$loop_count")
863
- if [[ -n "$loop_context" && "$VERBOSE_PROGRESS" == "true" ]]; then
864
- log_status "INFO" "Loop context: $loop_context"
865
- fi
866
- fi
867
-
868
- # Initialize or resume session
869
- local session_id=""
870
- if [[ "$CLAUDE_USE_CONTINUE" == "true" ]]; then
871
- session_id=$(init_claude_session)
872
- fi
873
-
874
- # Build the Claude CLI command with modern flags
875
- # Note: We use the modern CLI with -p flag when CLAUDE_OUTPUT_FORMAT is "json"
876
- # For backward compatibility, fall back to stdin piping for text mode
877
- local use_modern_cli=false
878
-
879
- if [[ "$CLAUDE_OUTPUT_FORMAT" == "json" ]]; then
880
- # Modern approach: use CLI flags (builds CLAUDE_CMD_ARGS array)
881
- if build_claude_command "$PROMPT_FILE" "$loop_context" "$session_id"; then
882
- use_modern_cli=true
883
- log_status "INFO" "Using modern CLI mode (JSON output)"
884
- else
885
- log_status "WARN" "Failed to build modern CLI command, falling back to legacy mode"
886
- fi
887
- else
888
- log_status "INFO" "Using legacy CLI mode (text output)"
889
- fi
890
-
891
- # Execute Claude Code
892
- if [[ "$use_modern_cli" == "true" ]]; then
893
- # Modern execution with command array (shell-injection safe)
894
- # Execute array directly without bash -c to prevent shell metacharacter interpretation
895
- if portable_timeout ${timeout_seconds}s "${CLAUDE_CMD_ARGS[@]}" > "$output_file" 2>&1 &
896
- then
897
- : # Continue to wait loop
898
- else
899
- log_status "ERROR" "❌ Failed to start Claude Code process (modern mode)"
900
- # Fall back to legacy mode
901
- log_status "INFO" "Falling back to legacy mode..."
902
- use_modern_cli=false
903
- fi
904
- fi
905
-
906
- # Fall back to legacy stdin piping if modern mode failed or not enabled
907
- if [[ "$use_modern_cli" == "false" ]]; then
908
- if portable_timeout ${timeout_seconds}s $CLAUDE_CODE_CMD < "$PROMPT_FILE" > "$output_file" 2>&1 &
909
- then
910
- : # Continue to wait loop
911
- else
912
- log_status "ERROR" "❌ Failed to start Claude Code process"
913
- return 1
914
- fi
915
- fi
916
-
917
- # Get PID and monitor progress
918
- local claude_pid=$!
919
- local progress_counter=0
920
-
921
- # Show progress while Claude Code is running
922
- while kill -0 $claude_pid 2>/dev/null; do
923
- progress_counter=$((progress_counter + 1))
924
- case $((progress_counter % 4)) in
925
- 1) progress_indicator="⠋" ;;
926
- 2) progress_indicator="" ;;
927
- 3) progress_indicator="⠹" ;;
928
- 0) progress_indicator="⠸" ;;
929
- esac
930
-
931
- # Get last line from output if available
932
- local last_line=""
933
- if [[ -f "$output_file" && -s "$output_file" ]]; then
934
- last_line=$(tail -1 "$output_file" 2>/dev/null | head -c 80)
935
- fi
936
-
937
- # Update progress file for monitor
938
- cat > "$PROGRESS_FILE" << EOF
939
- {
940
- "status": "executing",
941
- "indicator": "$progress_indicator",
942
- "elapsed_seconds": $((progress_counter * 10)),
943
- "last_output": "$last_line",
944
- "timestamp": "$(date '+%Y-%m-%d %H:%M:%S')"
945
- }
946
- EOF
947
-
948
- # Only log if verbose mode is enabled
949
- if [[ "$VERBOSE_PROGRESS" == "true" ]]; then
950
- if [[ -n "$last_line" ]]; then
951
- log_status "INFO" "$progress_indicator Claude Code: $last_line... (${progress_counter}0s)"
952
- else
953
- log_status "INFO" "$progress_indicator Claude Code working... (${progress_counter}0s elapsed)"
954
- fi
955
- fi
956
-
957
- sleep 10
958
- done
959
-
960
- # Wait for the process to finish and get exit code
961
- # Use || to prevent set -e from killing the script on non-zero (e.g. timeout 124)
962
- local exit_code=0
963
- wait $claude_pid || exit_code=$?
964
-
965
- if [ $exit_code -eq 124 ]; then
966
- # Timeout - not a failure, just took too long
967
- log_status "WARN" "⏰ Claude Code timed out after ${CLAUDE_TIMEOUT_MINUTES} minutes"
968
- log_status "INFO" "Session will be reset and retried in next loop"
969
- rm -f "$CLAUDE_SESSION_FILE"
970
- echo '{"status": "timeout", "timestamp": "'$(date '+%Y-%m-%d %H:%M:%S')'"}' > "$PROGRESS_FILE"
971
- return 4 # Specific return code for timeout
972
- elif [ $exit_code -eq 0 ]; then
973
- # Only increment counter on successful execution
974
- echo "$calls_made" > "$CALL_COUNT_FILE"
975
-
976
- # Clear progress file
977
- echo '{"status": "completed", "timestamp": "'$(date '+%Y-%m-%d %H:%M:%S')'"}' > "$PROGRESS_FILE"
978
-
979
- log_status "SUCCESS" "✅ Claude Code execution completed successfully"
980
-
981
- # Save session ID from JSON output (Phase 1.1)
982
- if [[ "$CLAUDE_USE_CONTINUE" == "true" ]]; then
983
- save_claude_session "$output_file"
984
- fi
985
-
986
- # Analyze the response
987
- log_status "INFO" "🔍 Analyzing Claude Code response..."
988
- analyze_response "$output_file" "$loop_count"
989
- local analysis_exit_code=$?
990
-
991
- # Update exit signals based on analysis
992
- update_exit_signals
993
-
994
- # Log analysis summary
995
- log_analysis_summary
996
-
997
- # Get file change count for circuit breaker
998
- local files_changed=$(git diff --name-only 2>/dev/null | wc -l || echo 0)
999
- local has_errors="false"
1000
-
1001
- # Two-stage error detection to avoid JSON field false positives
1002
- # Stage 1: Filter out JSON field patterns like "is_error": false
1003
- # Stage 2: Look for actual error messages in specific contexts
1004
- # Avoid type annotations like "error: Error" by requiring lowercase after ": error"
1005
- if grep -v '"[^"]*error[^"]*":' "$output_file" 2>/dev/null | \
1006
- grep -qE '(^Error:|^ERROR:|^error:|\]: error|Link: error|Error occurred|failed with error|[Ee]xception|Fatal|FATAL)'; then
1007
- has_errors="true"
1008
-
1009
- # Debug logging: show what triggered error detection
1010
- if [[ "$VERBOSE_PROGRESS" == "true" ]]; then
1011
- log_status "DEBUG" "Error patterns found:"
1012
- grep -v '"[^"]*error[^"]*":' "$output_file" 2>/dev/null | \
1013
- grep -nE '(^Error:|^ERROR:|^error:|\]: error|Link: error|Error occurred|failed with error|[Ee]xception|Fatal|FATAL)' | \
1014
- head -3 | while IFS= read -r line; do
1015
- log_status "DEBUG" " $line"
1016
- done
1017
- fi
1018
-
1019
- log_status "WARN" "Errors detected in output, check: $output_file"
1020
- fi
1021
- local output_length=$(wc -c < "$output_file" 2>/dev/null || echo 0)
1022
-
1023
- # Record result in circuit breaker
1024
- record_loop_result "$loop_count" "$files_changed" "$has_errors" "$output_length"
1025
- local circuit_result=$?
1026
-
1027
- if [[ $circuit_result -ne 0 ]]; then
1028
- log_status "WARN" "Circuit breaker opened - halting execution"
1029
- return 3 # Special code for circuit breaker trip
1030
- fi
1031
-
1032
- return 0
1033
- else
1034
- # Clear progress file on failure
1035
- echo '{"status": "failed", "timestamp": "'$(date '+%Y-%m-%d %H:%M:%S')'"}' > "$PROGRESS_FILE"
1036
-
1037
- # Check if the failure is due to API 5-hour limit
1038
- if grep -qi "5.*hour.*limit\|limit.*reached.*try.*back\|usage.*limit.*reached" "$output_file"; then
1039
- log_status "ERROR" "🚫 Claude API 5-hour usage limit reached"
1040
- return 2 # Special return code for API limit
1041
- else
1042
- log_status "ERROR" "❌ Claude Code execution failed, check: $output_file"
1043
- return 1
1044
- fi
1045
- fi
1046
- }
1047
-
1048
- # Cleanup function
1049
- cleanup() {
1050
- log_status "INFO" "Ralph loop interrupted. Cleaning up..."
1051
- reset_session "manual_interrupt"
1052
- update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "interrupted" "stopped"
1053
- exit 0
1054
- }
1055
-
1056
- # Set up signal handlers
1057
- trap cleanup SIGINT SIGTERM
1058
-
1059
- # Global variable for loop count (needed by cleanup function)
1060
- loop_count=0
1061
-
1062
- # Main loop
1063
- main() {
1064
-
1065
- log_status "SUCCESS" "🚀 Ralph loop starting with Claude Code"
1066
- log_status "INFO" "Max calls per hour: $MAX_CALLS_PER_HOUR"
1067
- log_status "INFO" "Logs: $LOG_DIR/ | Docs: $DOCS_DIR/ | Status: $STATUS_FILE"
1068
-
1069
- # Check if project uses old flat structure and needs migration
1070
- if [[ -f "PROMPT.md" ]] && [[ ! -d ".ralph" ]]; then
1071
- log_status "ERROR" "This project uses the old flat structure."
1072
- echo ""
1073
- echo "Ralph v0.10.0+ uses a .ralph/ subfolder to keep your project root clean."
1074
- echo ""
1075
- echo "To upgrade your project, run:"
1076
- echo " ralph-migrate"
1077
- echo ""
1078
- echo "This will move Ralph-specific files to .ralph/ while preserving src/ at root."
1079
- echo "A backup will be created before migration."
1080
- exit 1
1081
- fi
1082
-
1083
- # Check if this is a Ralph project directory
1084
- if [[ ! -f "$PROMPT_FILE" ]]; then
1085
- log_status "ERROR" "Prompt file '$PROMPT_FILE' not found!"
1086
- echo ""
1087
-
1088
- # Check if this looks like a partial Ralph project
1089
- if [[ -f "$RALPH_DIR/@fix_plan.md" ]] || [[ -d "$RALPH_DIR/specs" ]] || [[ -f "$RALPH_DIR/@AGENT.md" ]]; then
1090
- echo "This appears to be a Ralph project but is missing .ralph/PROMPT.md."
1091
- echo "You may need to create or restore the PROMPT.md file."
1092
- else
1093
- echo "This directory is not a Ralph project."
1094
- fi
1095
-
1096
- echo ""
1097
- echo "To fix this:"
1098
- echo " 1. Create a new project: ralph-setup my-project"
1099
- echo " 2. Import existing requirements: ralph-import requirements.md"
1100
- echo " 3. Navigate to an existing Ralph project directory"
1101
- echo " 4. Or create .ralph/PROMPT.md manually in this directory"
1102
- echo ""
1103
- echo "Ralph projects should contain: .ralph/PROMPT.md, .ralph/@fix_plan.md, .ralph/specs/, src/, etc."
1104
- exit 1
1105
- fi
1106
-
1107
- # Initialize session tracking before entering the loop
1108
- init_session_tracking
1109
-
1110
- log_status "INFO" "Starting main loop..."
1111
- log_status "INFO" "DEBUG: About to enter while loop, loop_count=$loop_count"
1112
-
1113
- while true; do
1114
- loop_count=$((loop_count + 1))
1115
- log_status "INFO" "DEBUG: Successfully incremented loop_count to $loop_count"
1116
-
1117
- # Update session last_used timestamp
1118
- update_session_last_used
1119
-
1120
- log_status "INFO" "Loop #$loop_count - calling init_call_tracking..."
1121
- init_call_tracking
1122
-
1123
- log_status "LOOP" "=== Starting Loop #$loop_count ==="
1124
-
1125
- # Check circuit breaker before attempting execution
1126
- if should_halt_execution; then
1127
- reset_session "circuit_breaker_open"
1128
- update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "circuit_breaker_open" "halted" "stagnation_detected"
1129
- log_status "ERROR" "🛑 Circuit breaker has opened - execution halted"
1130
- break
1131
- fi
1132
-
1133
- # Check rate limits
1134
- if ! can_make_call; then
1135
- wait_for_reset
1136
- continue
1137
- fi
1138
-
1139
- # Check for graceful exit conditions
1140
- local exit_reason=$(should_exit_gracefully)
1141
- if [[ "$exit_reason" != "" ]]; then
1142
- log_status "SUCCESS" "🏁 Graceful exit triggered: $exit_reason"
1143
- reset_session "project_complete"
1144
- update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "graceful_exit" "completed" "$exit_reason"
1145
-
1146
- log_status "SUCCESS" "🎉 Ralph has completed the project! Final stats:"
1147
- log_status "INFO" " - Total loops: $loop_count"
1148
- log_status "INFO" " - API calls used: $(cat "$CALL_COUNT_FILE")"
1149
- log_status "INFO" " - Exit reason: $exit_reason"
1150
-
1151
- break
1152
- fi
1153
-
1154
- # Update status
1155
- local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
1156
- update_status "$loop_count" "$calls_made" "executing" "running"
1157
-
1158
- # Execute Claude Code
1159
- local exec_result=0
1160
- execute_claude_code "$loop_count" || exec_result=$?
1161
-
1162
- if [ $exec_result -eq 0 ]; then
1163
- update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "completed" "success"
1164
-
1165
- # Brief pause between successful executions
1166
- sleep 5
1167
- elif [ $exec_result -eq 3 ]; then
1168
- # Circuit breaker opened
1169
- reset_session "circuit_breaker_trip"
1170
- update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "circuit_breaker_open" "halted" "stagnation_detected"
1171
- log_status "ERROR" "🛑 Circuit breaker has opened - halting loop"
1172
- log_status "INFO" "Run 'ralph --reset-circuit' to reset the circuit breaker after addressing issues"
1173
- break
1174
- elif [ $exec_result -eq 2 ]; then
1175
- # API 5-hour limit reached - handle specially
1176
- update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "api_limit" "paused"
1177
- log_status "WARN" "🛑 Claude API 5-hour limit reached!"
1178
-
1179
- # Ask user whether to wait or exit
1180
- echo -e "\n${YELLOW}The Claude API 5-hour usage limit has been reached.${NC}"
1181
- echo -e "${YELLOW}You can either:${NC}"
1182
- echo -e " ${GREEN}1)${NC} Wait for the limit to reset (usually within an hour)"
1183
- echo -e " ${GREEN}2)${NC} Exit the loop and try again later"
1184
- echo -e "\n${BLUE}Choose an option (1 or 2):${NC} "
1185
-
1186
- # Read user input with timeout
1187
- read -t 30 -n 1 user_choice
1188
- echo # New line after input
1189
-
1190
- if [[ "$user_choice" == "2" ]] || [[ -z "$user_choice" ]]; then
1191
- log_status "INFO" "User chose to exit (or timed out). Exiting loop..."
1192
- update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "api_limit_exit" "stopped" "api_5hour_limit"
1193
- break
1194
- else
1195
- log_status "INFO" "User chose to wait. Waiting for API limit reset..."
1196
- # Wait for longer period when API limit is hit
1197
- local wait_minutes=60
1198
- log_status "INFO" "Waiting $wait_minutes minutes before retrying..."
1199
-
1200
- # Countdown display
1201
- local wait_seconds=$((wait_minutes * 60))
1202
- while [[ $wait_seconds -gt 0 ]]; do
1203
- local minutes=$((wait_seconds / 60))
1204
- local seconds=$((wait_seconds % 60))
1205
- printf "\r${YELLOW}Time until retry: %02d:%02d${NC}" $minutes $seconds
1206
- sleep 1
1207
- ((wait_seconds--))
1208
- done
1209
- printf "\n"
1210
- fi
1211
- elif [ $exec_result -eq 4 ]; then
1212
- # Timeout - non-fatal, session already reset in execute_claude_code
1213
- update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "timeout" "retrying"
1214
- log_status "INFO" "Retrying with fresh session in 10 seconds..."
1215
- sleep 10
1216
- else
1217
- update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "failed" "error"
1218
- log_status "WARN" "Execution failed, waiting 30 seconds before retry..."
1219
- sleep 30
1220
- fi
1221
-
1222
- log_status "LOOP" "=== Completed Loop #$loop_count ==="
1223
- done
1224
- }
1225
-
1226
- # Help function
1227
- show_help() {
1228
- cat << HELPEOF
1229
- Ralph Loop for Claude Code
1230
-
1231
- Usage: $0 [OPTIONS]
1232
-
1233
- IMPORTANT: This command must be run from a Ralph project directory.
1234
- Use 'ralph-setup project-name' to create a new project first.
1235
-
1236
- Options:
1237
- -h, --help Show this help message
1238
- -c, --calls NUM Set max calls per hour (default: $MAX_CALLS_PER_HOUR)
1239
- -p, --prompt FILE Set prompt file (default: $PROMPT_FILE)
1240
- -s, --status Show current status and exit
1241
- -m, --monitor Start with tmux session and live monitor (requires tmux)
1242
- -v, --verbose Show detailed progress updates during execution
1243
- -t, --timeout MIN Set Claude Code execution timeout in minutes (default: $CLAUDE_TIMEOUT_MINUTES)
1244
- --reset-circuit Reset circuit breaker to CLOSED state
1245
- --circuit-status Show circuit breaker status and exit
1246
- --reset-session Reset session state and exit (clears session continuity)
1247
-
1248
- Modern CLI Options (Phase 1.1):
1249
- --output-format FORMAT Set Claude output format: json or text (default: $CLAUDE_OUTPUT_FORMAT)
1250
- --allowed-tools TOOLS Comma-separated list of allowed tools (default: $CLAUDE_ALLOWED_TOOLS)
1251
- --no-continue Disable session continuity across loops
1252
- --session-expiry HOURS Set session expiration time in hours (default: $CLAUDE_SESSION_EXPIRY_HOURS)
1253
-
1254
- Files created:
1255
- - $LOG_DIR/: All execution logs
1256
- - $DOCS_DIR/: Generated documentation
1257
- - $STATUS_FILE: Current status (JSON)
1258
- - .ralph/.ralph_session: Session lifecycle tracking
1259
- - .ralph/.ralph_session_history: Session transition history (last 50)
1260
- - .ralph/.call_count: API call counter for rate limiting
1261
- - .ralph/.last_reset: Timestamp of last rate limit reset
1262
-
1263
- Example workflow:
1264
- ralph-setup my-project # Create project
1265
- cd my-project # Enter project directory
1266
- $0 --monitor # Start Ralph with monitoring
1267
-
1268
- Examples:
1269
- $0 --calls 50 --prompt my_prompt.md
1270
- $0 --monitor # Start with integrated tmux monitoring
1271
- $0 --monitor --timeout 30 # 30-minute timeout for complex tasks
1272
- $0 --verbose --timeout 5 # 5-minute timeout with detailed progress
1273
- $0 --output-format text # Use legacy text output format
1274
- $0 --no-continue # Disable session continuity
1275
- $0 --session-expiry 48 # 48-hour session expiration
1276
-
1277
- HELPEOF
1278
- }
1279
-
1280
- # Parse command line arguments
1281
- while [[ $# -gt 0 ]]; do
1282
- case $1 in
1283
- -h|--help)
1284
- show_help
1285
- exit 0
1286
- ;;
1287
- -c|--calls)
1288
- MAX_CALLS_PER_HOUR="$2"
1289
- shift 2
1290
- ;;
1291
- -p|--prompt)
1292
- PROMPT_FILE="$2"
1293
- shift 2
1294
- ;;
1295
- -s|--status)
1296
- if [[ -f "$STATUS_FILE" ]]; then
1297
- echo "Current Status:"
1298
- cat "$STATUS_FILE" | jq . 2>/dev/null || cat "$STATUS_FILE"
1299
- else
1300
- echo "No status file found. Ralph may not be running."
1301
- fi
1302
- exit 0
1303
- ;;
1304
- -m|--monitor)
1305
- USE_TMUX=true
1306
- shift
1307
- ;;
1308
- -v|--verbose)
1309
- VERBOSE_PROGRESS=true
1310
- shift
1311
- ;;
1312
- -t|--timeout)
1313
- if [[ "$2" =~ ^[1-9][0-9]*$ ]] && [[ "$2" -le 120 ]]; then
1314
- CLAUDE_TIMEOUT_MINUTES="$2"
1315
- else
1316
- echo "Error: Timeout must be a positive integer between 1 and 120 minutes"
1317
- exit 1
1318
- fi
1319
- shift 2
1320
- ;;
1321
- --reset-circuit)
1322
- # Source the circuit breaker library
1323
- SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
1324
- source "$SCRIPT_DIR/lib/circuit_breaker.sh"
1325
- source "$SCRIPT_DIR/lib/date_utils.sh"
1326
- reset_circuit_breaker "Manual reset via command line"
1327
- reset_session "manual_circuit_reset"
1328
- exit 0
1329
- ;;
1330
- --reset-session)
1331
- # Reset session state only
1332
- SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
1333
- source "$SCRIPT_DIR/lib/date_utils.sh"
1334
- reset_session "manual_reset_flag"
1335
- echo -e "\033[0;32m✅ Session state reset successfully\033[0m"
1336
- exit 0
1337
- ;;
1338
- --circuit-status)
1339
- # Source the circuit breaker library
1340
- SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
1341
- source "$SCRIPT_DIR/lib/circuit_breaker.sh"
1342
- show_circuit_status
1343
- exit 0
1344
- ;;
1345
- --output-format)
1346
- if [[ "$2" == "json" || "$2" == "text" ]]; then
1347
- CLAUDE_OUTPUT_FORMAT="$2"
1348
- else
1349
- echo "Error: --output-format must be 'json' or 'text'"
1350
- exit 1
1351
- fi
1352
- shift 2
1353
- ;;
1354
- --allowed-tools)
1355
- if ! validate_allowed_tools "$2"; then
1356
- exit 1
1357
- fi
1358
- CLAUDE_ALLOWED_TOOLS="$2"
1359
- shift 2
1360
- ;;
1361
- --no-continue)
1362
- CLAUDE_USE_CONTINUE=false
1363
- shift
1364
- ;;
1365
- --session-expiry)
1366
- if [[ -z "$2" || ! "$2" =~ ^[1-9][0-9]*$ ]]; then
1367
- echo "Error: --session-expiry requires a positive integer (hours)"
1368
- exit 1
1369
- fi
1370
- CLAUDE_SESSION_EXPIRY_HOURS="$2"
1371
- shift 2
1372
- ;;
1373
- *)
1374
- echo "Unknown option: $1"
1375
- show_help
1376
- exit 1
1377
- ;;
1378
- esac
1379
- done
1380
-
1381
- # Only execute when run directly, not when sourced
1382
- if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
1383
- # If tmux mode requested, set it up
1384
- if [[ "$USE_TMUX" == "true" ]]; then
1385
- check_tmux_available
1386
- setup_tmux_session
1387
- fi
1388
-
1389
- # Start the main loop
1390
- main
1391
- fi
1
+ #!/bin/bash
2
+
3
+ # Claude Code Ralph Loop with Rate Limiting and Documentation
4
+ # Adaptation of the Ralph technique for Claude Code with usage management
5
+
6
+ set -e # Exit on any error
7
+
8
+ # Note: CLAUDE_CODE_ENABLE_DANGEROUS_PERMISSIONS_IN_SANDBOX and IS_SANDBOX
9
+ # environment variables are NOT exported here. Tool restrictions are handled
10
+ # via --allowedTools flag in CLAUDE_CMD_ARGS, which is the proper approach.
11
+ # Exporting sandbox variables without a verified sandbox would be misleading.
12
+
13
+ # Source library components
14
+ SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
15
+ source "$SCRIPT_DIR/lib/date_utils.sh"
16
+ source "$SCRIPT_DIR/lib/timeout_utils.sh"
17
+ source "$SCRIPT_DIR/lib/response_analyzer.sh"
18
+ source "$SCRIPT_DIR/lib/circuit_breaker.sh"
19
+
20
+ # Configuration
21
+ # Ralph-specific files live in .ralph/ subfolder
22
+ RALPH_DIR=".ralph"
23
+ PROMPT_FILE="$RALPH_DIR/PROMPT.md"
24
+ LOG_DIR="$RALPH_DIR/logs"
25
+ DOCS_DIR="$RALPH_DIR/docs/generated"
26
+ STATUS_FILE="$RALPH_DIR/status.json"
27
+ PROGRESS_FILE="$RALPH_DIR/progress.json"
28
+ CLAUDE_CODE_CMD="claude"
29
+ SLEEP_DURATION=3600 # 1 hour in seconds
30
+ LIVE_OUTPUT=false # Show Claude Code output in real-time (streaming)
31
+ LIVE_LOG_FILE="$RALPH_DIR/live.log" # Fixed file for live output monitoring
32
+ CALL_COUNT_FILE="$RALPH_DIR/.call_count"
33
+ TIMESTAMP_FILE="$RALPH_DIR/.last_reset"
34
+ USE_TMUX=false
35
+
36
+ # Save environment variable state BEFORE setting defaults
37
+ # These are used by load_ralphrc() to determine which values came from environment
38
+ _env_MAX_CALLS_PER_HOUR="${MAX_CALLS_PER_HOUR:-}"
39
+ _env_CLAUDE_TIMEOUT_MINUTES="${CLAUDE_TIMEOUT_MINUTES:-}"
40
+ _env_CLAUDE_OUTPUT_FORMAT="${CLAUDE_OUTPUT_FORMAT:-}"
41
+ _env_CLAUDE_ALLOWED_TOOLS="${CLAUDE_ALLOWED_TOOLS:-}"
42
+ _env_CLAUDE_USE_CONTINUE="${CLAUDE_USE_CONTINUE:-}"
43
+ _env_CLAUDE_SESSION_EXPIRY_HOURS="${CLAUDE_SESSION_EXPIRY_HOURS:-}"
44
+ _env_VERBOSE_PROGRESS="${VERBOSE_PROGRESS:-}"
45
+ _env_CB_COOLDOWN_MINUTES="${CB_COOLDOWN_MINUTES:-}"
46
+ _env_CB_AUTO_RESET="${CB_AUTO_RESET:-}"
47
+
48
+ # Now set defaults (only if not already set by environment)
49
+ MAX_CALLS_PER_HOUR="${MAX_CALLS_PER_HOUR:-100}"
50
+ VERBOSE_PROGRESS="${VERBOSE_PROGRESS:-false}"
51
+ CLAUDE_TIMEOUT_MINUTES="${CLAUDE_TIMEOUT_MINUTES:-15}"
52
+
53
+ # Modern Claude CLI configuration (Phase 1.1)
54
+ CLAUDE_OUTPUT_FORMAT="${CLAUDE_OUTPUT_FORMAT:-json}"
55
+ CLAUDE_ALLOWED_TOOLS="${CLAUDE_ALLOWED_TOOLS:-Write,Read,Edit,Bash(git *),Bash(npm *),Bash(pytest)}"
56
+ CLAUDE_USE_CONTINUE="${CLAUDE_USE_CONTINUE:-true}"
57
+ CLAUDE_SESSION_FILE="$RALPH_DIR/.claude_session_id" # Session ID persistence file
58
+ CLAUDE_MIN_VERSION="2.0.76" # Minimum required Claude CLI version
59
+
60
+ # Session management configuration (Phase 1.2)
61
+ # Note: SESSION_EXPIRATION_SECONDS is defined in lib/response_analyzer.sh (86400 = 24 hours)
62
+ RALPH_SESSION_FILE="$RALPH_DIR/.ralph_session" # Ralph-specific session tracking (lifecycle)
63
+ RALPH_SESSION_HISTORY_FILE="$RALPH_DIR/.ralph_session_history" # Session transition history
64
+ # Session expiration: 24 hours default balances project continuity with fresh context
65
+ # Too short = frequent context loss; Too long = stale context causes unpredictable behavior
66
+ CLAUDE_SESSION_EXPIRY_HOURS=${CLAUDE_SESSION_EXPIRY_HOURS:-24}
67
+
68
+ # Valid tool patterns for --allowed-tools validation
69
+ # Tools can be exact matches or pattern matches with wildcards in parentheses
70
+ VALID_TOOL_PATTERNS=(
71
+ "Write"
72
+ "Read"
73
+ "Edit"
74
+ "MultiEdit"
75
+ "Glob"
76
+ "Grep"
77
+ "Task"
78
+ "TodoWrite"
79
+ "WebFetch"
80
+ "WebSearch"
81
+ "Bash"
82
+ "Bash(git *)"
83
+ "Bash(npm *)"
84
+ "Bash(bats *)"
85
+ "Bash(python *)"
86
+ "Bash(node *)"
87
+ "NotebookEdit"
88
+ )
89
+
90
+ # Exit detection configuration
91
+ EXIT_SIGNALS_FILE="$RALPH_DIR/.exit_signals"
92
+ RESPONSE_ANALYSIS_FILE="$RALPH_DIR/.response_analysis"
93
+ MAX_CONSECUTIVE_TEST_LOOPS=3
94
+ MAX_CONSECUTIVE_DONE_SIGNALS=2
95
+ TEST_PERCENTAGE_THRESHOLD=30 # If more than 30% of recent loops are test-only, flag it
96
+
97
+ # .ralphrc configuration file
98
+ RALPHRC_FILE=".ralphrc"
99
+ RALPHRC_LOADED=false
100
+
101
+ # load_ralphrc - Load project-specific configuration from .ralphrc
102
+ #
103
+ # This function sources .ralphrc if it exists, applying project-specific
104
+ # settings. Environment variables take precedence over .ralphrc values.
105
+ #
106
+ # Configuration values that can be overridden:
107
+ # - MAX_CALLS_PER_HOUR
108
+ # - CLAUDE_TIMEOUT_MINUTES
109
+ # - CLAUDE_OUTPUT_FORMAT
110
+ # - ALLOWED_TOOLS (mapped to CLAUDE_ALLOWED_TOOLS)
111
+ # - SESSION_CONTINUITY (mapped to CLAUDE_USE_CONTINUE)
112
+ # - SESSION_EXPIRY_HOURS (mapped to CLAUDE_SESSION_EXPIRY_HOURS)
113
+ # - CB_NO_PROGRESS_THRESHOLD
114
+ # - CB_SAME_ERROR_THRESHOLD
115
+ # - CB_OUTPUT_DECLINE_THRESHOLD
116
+ # - RALPH_VERBOSE
117
+ #
118
+ load_ralphrc() {
119
+ if [[ ! -f "$RALPHRC_FILE" ]]; then
120
+ return 0
121
+ fi
122
+
123
+ # Source .ralphrc (this may override default values)
124
+ # shellcheck source=/dev/null
125
+ source "$RALPHRC_FILE"
126
+
127
+ # Map .ralphrc variable names to internal names
128
+ if [[ -n "${ALLOWED_TOOLS:-}" ]]; then
129
+ CLAUDE_ALLOWED_TOOLS="$ALLOWED_TOOLS"
130
+ fi
131
+ if [[ -n "${SESSION_CONTINUITY:-}" ]]; then
132
+ CLAUDE_USE_CONTINUE="$SESSION_CONTINUITY"
133
+ fi
134
+ if [[ -n "${SESSION_EXPIRY_HOURS:-}" ]]; then
135
+ CLAUDE_SESSION_EXPIRY_HOURS="$SESSION_EXPIRY_HOURS"
136
+ fi
137
+ if [[ -n "${RALPH_VERBOSE:-}" ]]; then
138
+ VERBOSE_PROGRESS="$RALPH_VERBOSE"
139
+ fi
140
+
141
+ # Restore ONLY values that were explicitly set via environment variables
142
+ # (not script defaults). The _env_* variables were captured BEFORE defaults were set.
143
+ # If _env_* is non-empty, the user explicitly set it in their environment.
144
+ [[ -n "$_env_MAX_CALLS_PER_HOUR" ]] && MAX_CALLS_PER_HOUR="$_env_MAX_CALLS_PER_HOUR"
145
+ [[ -n "$_env_CLAUDE_TIMEOUT_MINUTES" ]] && CLAUDE_TIMEOUT_MINUTES="$_env_CLAUDE_TIMEOUT_MINUTES"
146
+ [[ -n "$_env_CLAUDE_OUTPUT_FORMAT" ]] && CLAUDE_OUTPUT_FORMAT="$_env_CLAUDE_OUTPUT_FORMAT"
147
+ [[ -n "$_env_CLAUDE_ALLOWED_TOOLS" ]] && CLAUDE_ALLOWED_TOOLS="$_env_CLAUDE_ALLOWED_TOOLS"
148
+ [[ -n "$_env_CLAUDE_USE_CONTINUE" ]] && CLAUDE_USE_CONTINUE="$_env_CLAUDE_USE_CONTINUE"
149
+ [[ -n "$_env_CLAUDE_SESSION_EXPIRY_HOURS" ]] && CLAUDE_SESSION_EXPIRY_HOURS="$_env_CLAUDE_SESSION_EXPIRY_HOURS"
150
+ [[ -n "$_env_VERBOSE_PROGRESS" ]] && VERBOSE_PROGRESS="$_env_VERBOSE_PROGRESS"
151
+ [[ -n "$_env_CB_COOLDOWN_MINUTES" ]] && CB_COOLDOWN_MINUTES="$_env_CB_COOLDOWN_MINUTES"
152
+ [[ -n "$_env_CB_AUTO_RESET" ]] && CB_AUTO_RESET="$_env_CB_AUTO_RESET"
153
+
154
+ RALPHRC_LOADED=true
155
+ return 0
156
+ }
157
+
158
+ # Colors for terminal output
159
+ RED='\033[0;31m'
160
+ GREEN='\033[0;32m'
161
+ YELLOW='\033[1;33m'
162
+ BLUE='\033[0;34m'
163
+ PURPLE='\033[0;35m'
164
+ NC='\033[0m' # No Color
165
+
166
+ # Initialize directories
167
+ mkdir -p "$LOG_DIR" "$DOCS_DIR"
168
+
169
+ # Check if tmux is available
170
+ check_tmux_available() {
171
+ if ! command -v tmux &> /dev/null; then
172
+ log_status "ERROR" "tmux is not installed. Please install tmux or run without --monitor flag."
173
+ echo "Install tmux:"
174
+ echo " Ubuntu/Debian: sudo apt-get install tmux"
175
+ echo " macOS: brew install tmux"
176
+ echo " CentOS/RHEL: sudo yum install tmux"
177
+ exit 1
178
+ fi
179
+ }
180
+
181
+ # Get the tmux base-index for windows (handles custom tmux configurations)
182
+ # Returns: the base window index (typically 0 or 1)
183
+ get_tmux_base_index() {
184
+ local base_index
185
+ base_index=$(tmux show-options -gv base-index 2>/dev/null)
186
+ # Default to 0 if not set or tmux command fails
187
+ echo "${base_index:-0}"
188
+ }
189
+
190
+ # Setup tmux session with monitor
191
+ setup_tmux_session() {
192
+ local session_name="ralph-$(date +%s)"
193
+ local ralph_home="${RALPH_HOME:-$HOME/.ralph}"
194
+ local project_dir="$(pwd)"
195
+
196
+ # Get the tmux base-index to handle custom configurations (e.g., base-index 1)
197
+ local base_win
198
+ base_win=$(get_tmux_base_index)
199
+
200
+ log_status "INFO" "Setting up tmux session: $session_name"
201
+
202
+ # Initialize live.log file
203
+ echo "=== Ralph Live Output - Waiting for first loop... ===" > "$LIVE_LOG_FILE"
204
+
205
+ # Create new tmux session detached (left pane - Ralph loop)
206
+ tmux new-session -d -s "$session_name" -c "$project_dir"
207
+
208
+ # Split window vertically (right side)
209
+ tmux split-window -h -t "$session_name" -c "$project_dir"
210
+
211
+ # Split right pane horizontally (top: Claude output, bottom: status)
212
+ tmux split-window -v -t "$session_name:${base_win}.1" -c "$project_dir"
213
+
214
+ # Right-top pane (pane 1): Live Claude Code output
215
+ tmux send-keys -t "$session_name:${base_win}.1" "tail -f '$project_dir/$LIVE_LOG_FILE'" Enter
216
+
217
+ # Right-bottom pane (pane 2): Ralph status monitor
218
+ if command -v ralph-monitor &> /dev/null; then
219
+ tmux send-keys -t "$session_name:${base_win}.2" "ralph-monitor" Enter
220
+ else
221
+ tmux send-keys -t "$session_name:${base_win}.2" "'$ralph_home/ralph_monitor.sh'" Enter
222
+ fi
223
+
224
+ # Start ralph loop in the left pane (exclude tmux flag to avoid recursion)
225
+ # Forward all CLI parameters that were set by the user
226
+ local ralph_cmd
227
+ if command -v ralph &> /dev/null; then
228
+ ralph_cmd="ralph"
229
+ else
230
+ ralph_cmd="'$ralph_home/ralph_loop.sh'"
231
+ fi
232
+
233
+ # Always use --live mode in tmux for real-time streaming
234
+ ralph_cmd="$ralph_cmd --live"
235
+
236
+ # Forward --calls if non-default
237
+ if [[ "$MAX_CALLS_PER_HOUR" != "100" ]]; then
238
+ ralph_cmd="$ralph_cmd --calls $MAX_CALLS_PER_HOUR"
239
+ fi
240
+ # Forward --prompt if non-default
241
+ if [[ "$PROMPT_FILE" != "$RALPH_DIR/PROMPT.md" ]]; then
242
+ ralph_cmd="$ralph_cmd --prompt '$PROMPT_FILE'"
243
+ fi
244
+ # Forward --output-format if non-default (default is json)
245
+ if [[ "$CLAUDE_OUTPUT_FORMAT" != "json" ]]; then
246
+ ralph_cmd="$ralph_cmd --output-format $CLAUDE_OUTPUT_FORMAT"
247
+ fi
248
+ # Forward --verbose if enabled
249
+ if [[ "$VERBOSE_PROGRESS" == "true" ]]; then
250
+ ralph_cmd="$ralph_cmd --verbose"
251
+ fi
252
+ # Forward --timeout if non-default (default is 15)
253
+ if [[ "$CLAUDE_TIMEOUT_MINUTES" != "15" ]]; then
254
+ ralph_cmd="$ralph_cmd --timeout $CLAUDE_TIMEOUT_MINUTES"
255
+ fi
256
+ # Forward --allowed-tools if non-default
257
+ if [[ "$CLAUDE_ALLOWED_TOOLS" != "Write,Read,Edit,Bash(git *),Bash(npm *),Bash(pytest)" ]]; then
258
+ ralph_cmd="$ralph_cmd --allowed-tools '$CLAUDE_ALLOWED_TOOLS'"
259
+ fi
260
+ # Forward --no-continue if session continuity disabled
261
+ if [[ "$CLAUDE_USE_CONTINUE" == "false" ]]; then
262
+ ralph_cmd="$ralph_cmd --no-continue"
263
+ fi
264
+ # Forward --session-expiry if non-default (default is 24)
265
+ if [[ "$CLAUDE_SESSION_EXPIRY_HOURS" != "24" ]]; then
266
+ ralph_cmd="$ralph_cmd --session-expiry $CLAUDE_SESSION_EXPIRY_HOURS"
267
+ fi
268
+ # Forward --auto-reset-circuit if enabled
269
+ if [[ "$CB_AUTO_RESET" == "true" ]]; then
270
+ ralph_cmd="$ralph_cmd --auto-reset-circuit"
271
+ fi
272
+
273
+ tmux send-keys -t "$session_name:${base_win}.0" "$ralph_cmd" Enter
274
+
275
+ # Focus on left pane (main ralph loop)
276
+ tmux select-pane -t "$session_name:${base_win}.0"
277
+
278
+ # Set pane titles (requires tmux 2.6+)
279
+ tmux select-pane -t "$session_name:${base_win}.0" -T "Ralph Loop"
280
+ tmux select-pane -t "$session_name:${base_win}.1" -T "Claude Output"
281
+ tmux select-pane -t "$session_name:${base_win}.2" -T "Status"
282
+
283
+ # Set window title
284
+ tmux rename-window -t "$session_name:${base_win}" "Ralph: Loop | Output | Status"
285
+
286
+ log_status "SUCCESS" "Tmux session created with 3 panes:"
287
+ log_status "INFO" " Left: Ralph loop"
288
+ log_status "INFO" " Right-top: Claude Code live output"
289
+ log_status "INFO" " Right-bottom: Status monitor"
290
+ log_status "INFO" ""
291
+ log_status "INFO" "Use Ctrl+B then D to detach from session"
292
+ log_status "INFO" "Use 'tmux attach -t $session_name' to reattach"
293
+
294
+ # Attach to session (this will block until session ends)
295
+ tmux attach-session -t "$session_name"
296
+
297
+ exit 0
298
+ }
299
+
300
+ # Initialize call tracking
301
+ init_call_tracking() {
302
+ # Debug logging removed for cleaner output
303
+ local current_hour=$(date +%Y%m%d%H)
304
+ local last_reset_hour=""
305
+
306
+ if [[ -f "$TIMESTAMP_FILE" ]]; then
307
+ last_reset_hour=$(cat "$TIMESTAMP_FILE")
308
+ fi
309
+
310
+ # Reset counter if it's a new hour
311
+ if [[ "$current_hour" != "$last_reset_hour" ]]; then
312
+ echo "0" > "$CALL_COUNT_FILE"
313
+ echo "$current_hour" > "$TIMESTAMP_FILE"
314
+ log_status "INFO" "Call counter reset for new hour: $current_hour"
315
+ fi
316
+
317
+ # Initialize exit signals tracking if it doesn't exist
318
+ if [[ ! -f "$EXIT_SIGNALS_FILE" ]]; then
319
+ echo '{"test_only_loops": [], "done_signals": [], "completion_indicators": []}' > "$EXIT_SIGNALS_FILE"
320
+ fi
321
+
322
+ # Initialize circuit breaker
323
+ init_circuit_breaker
324
+
325
+ }
326
+
327
+ # Log function with timestamps and colors
328
+ log_status() {
329
+ local level=$1
330
+ local message=$2
331
+ local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
332
+ local color=""
333
+
334
+ case $level in
335
+ "INFO") color=$BLUE ;;
336
+ "WARN") color=$YELLOW ;;
337
+ "ERROR") color=$RED ;;
338
+ "SUCCESS") color=$GREEN ;;
339
+ "LOOP") color=$PURPLE ;;
340
+ esac
341
+
342
+ # Write to stderr so log messages don't interfere with function return values
343
+ echo -e "${color}[$timestamp] [$level] $message${NC}" >&2
344
+ echo "[$timestamp] [$level] $message" >> "$LOG_DIR/ralph.log"
345
+ }
346
+
347
+ # Update status JSON for external monitoring
348
+ update_status() {
349
+ local loop_count=$1
350
+ local calls_made=$2
351
+ local last_action=$3
352
+ local status=$4
353
+ local exit_reason=${5:-""}
354
+
355
+ cat > "$STATUS_FILE" << STATUSEOF
356
+ {
357
+ "timestamp": "$(get_iso_timestamp)",
358
+ "loop_count": $loop_count,
359
+ "calls_made_this_hour": $calls_made,
360
+ "max_calls_per_hour": $MAX_CALLS_PER_HOUR,
361
+ "last_action": "$last_action",
362
+ "status": "$status",
363
+ "exit_reason": "$exit_reason",
364
+ "next_reset": "$(get_next_hour_time)"
365
+ }
366
+ STATUSEOF
367
+ }
368
+
369
+ # Check if we can make another call
370
+ can_make_call() {
371
+ local calls_made=0
372
+ if [[ -f "$CALL_COUNT_FILE" ]]; then
373
+ calls_made=$(cat "$CALL_COUNT_FILE")
374
+ fi
375
+
376
+ if [[ $calls_made -ge $MAX_CALLS_PER_HOUR ]]; then
377
+ return 1 # Cannot make call
378
+ else
379
+ return 0 # Can make call
380
+ fi
381
+ }
382
+
383
+ # Increment call counter
384
+ increment_call_counter() {
385
+ local calls_made=0
386
+ if [[ -f "$CALL_COUNT_FILE" ]]; then
387
+ calls_made=$(cat "$CALL_COUNT_FILE")
388
+ fi
389
+
390
+ ((calls_made++))
391
+ echo "$calls_made" > "$CALL_COUNT_FILE"
392
+ echo "$calls_made"
393
+ }
394
+
395
+ # Wait for rate limit reset with countdown
396
+ wait_for_reset() {
397
+ local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
398
+ log_status "WARN" "Rate limit reached ($calls_made/$MAX_CALLS_PER_HOUR). Waiting for reset..."
399
+
400
+ # Calculate time until next hour
401
+ local current_minute=$(date +%M)
402
+ local current_second=$(date +%S)
403
+ local wait_time=$(((60 - current_minute - 1) * 60 + (60 - current_second)))
404
+
405
+ log_status "INFO" "Sleeping for $wait_time seconds until next hour..."
406
+
407
+ # Countdown display
408
+ while [[ $wait_time -gt 0 ]]; do
409
+ local hours=$((wait_time / 3600))
410
+ local minutes=$(((wait_time % 3600) / 60))
411
+ local seconds=$((wait_time % 60))
412
+
413
+ printf "\r${YELLOW}Time until reset: %02d:%02d:%02d${NC}" $hours $minutes $seconds
414
+ sleep 1
415
+ ((wait_time--))
416
+ done
417
+ printf "\n"
418
+
419
+ # Reset counter
420
+ echo "0" > "$CALL_COUNT_FILE"
421
+ echo "$(date +%Y%m%d%H)" > "$TIMESTAMP_FILE"
422
+ log_status "SUCCESS" "Rate limit reset! Ready for new calls."
423
+ }
424
+
425
+ # Check if we should gracefully exit
426
+ should_exit_gracefully() {
427
+
428
+ if [[ ! -f "$EXIT_SIGNALS_FILE" ]]; then
429
+ return 1 # Don't exit, file doesn't exist
430
+ fi
431
+
432
+ local signals=$(cat "$EXIT_SIGNALS_FILE")
433
+
434
+ # Count recent signals (last 5 loops) - with error handling
435
+ local recent_test_loops
436
+ local recent_done_signals
437
+ local recent_completion_indicators
438
+
439
+ recent_test_loops=$(echo "$signals" | jq '.test_only_loops | length' 2>/dev/null || echo "0")
440
+ recent_done_signals=$(echo "$signals" | jq '.done_signals | length' 2>/dev/null || echo "0")
441
+ recent_completion_indicators=$(echo "$signals" | jq '.completion_indicators | length' 2>/dev/null || echo "0")
442
+
443
+
444
+ # Check for exit conditions
445
+
446
+ # 0. Permission denials (highest priority - Issue #101)
447
+ # When Claude Code is denied permission to run commands, halt immediately
448
+ # to allow user to update .ralphrc ALLOWED_TOOLS configuration
449
+ if [[ -f "$RESPONSE_ANALYSIS_FILE" ]]; then
450
+ local has_permission_denials=$(jq -r '.analysis.has_permission_denials // false' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "false")
451
+ if [[ "$has_permission_denials" == "true" ]]; then
452
+ local denied_count=$(jq -r '.analysis.permission_denial_count // 0' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "0")
453
+ local denied_cmds=$(jq -r '.analysis.denied_commands | join(", ")' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "unknown")
454
+ log_status "WARN" "🚫 Permission denied for $denied_count command(s): $denied_cmds"
455
+ log_status "WARN" "Update ALLOWED_TOOLS in .ralphrc to include the required tools"
456
+ echo "permission_denied"
457
+ return 0
458
+ fi
459
+ fi
460
+
461
+ # 1. Too many consecutive test-only loops
462
+ if [[ $recent_test_loops -ge $MAX_CONSECUTIVE_TEST_LOOPS ]]; then
463
+ log_status "WARN" "Exit condition: Too many test-focused loops ($recent_test_loops >= $MAX_CONSECUTIVE_TEST_LOOPS)"
464
+ echo "test_saturation"
465
+ return 0
466
+ fi
467
+
468
+ # 2. Multiple "done" signals
469
+ if [[ $recent_done_signals -ge $MAX_CONSECUTIVE_DONE_SIGNALS ]]; then
470
+ log_status "WARN" "Exit condition: Multiple completion signals ($recent_done_signals >= $MAX_CONSECUTIVE_DONE_SIGNALS)"
471
+ echo "completion_signals"
472
+ return 0
473
+ fi
474
+
475
+ # 3. Safety circuit breaker - force exit after 5 consecutive EXIT_SIGNAL=true responses
476
+ # Note: completion_indicators only accumulates when Claude explicitly sets EXIT_SIGNAL=true
477
+ # (not based on confidence score). This safety breaker catches cases where Claude signals
478
+ # completion 5+ times but the normal exit path (completion_indicators >= 2 + EXIT_SIGNAL=true)
479
+ # didn't trigger for some reason. Threshold of 5 prevents API waste while being higher than
480
+ # the normal threshold (2) to avoid false positives.
481
+ if [[ $recent_completion_indicators -ge 5 ]]; then
482
+ log_status "WARN" "🚨 SAFETY CIRCUIT BREAKER: Force exit after 5 consecutive EXIT_SIGNAL=true responses ($recent_completion_indicators)" >&2
483
+ echo "safety_circuit_breaker"
484
+ return 0
485
+ fi
486
+
487
+ # 4. Strong completion indicators (only if Claude's EXIT_SIGNAL is true)
488
+ # This prevents premature exits when heuristics detect completion patterns
489
+ # but Claude explicitly indicates work is still in progress via RALPH_STATUS block.
490
+ # The exit_signal in .response_analysis represents Claude's explicit intent.
491
+ local claude_exit_signal="false"
492
+ if [[ -f "$RESPONSE_ANALYSIS_FILE" ]]; then
493
+ claude_exit_signal=$(jq -r '.analysis.exit_signal // false' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null || echo "false")
494
+ fi
495
+
496
+ if [[ $recent_completion_indicators -ge 2 ]] && [[ "$claude_exit_signal" == "true" ]]; then
497
+ log_status "WARN" "Exit condition: Strong completion indicators ($recent_completion_indicators) with EXIT_SIGNAL=true" >&2
498
+ echo "project_complete"
499
+ return 0
500
+ fi
501
+
502
+ # 5. Check @fix_plan.md for completion
503
+ # Fix #144: Only match valid markdown checkboxes, not date entries like [2026-01-29]
504
+ # Valid patterns: "- [ ]" (uncompleted) and "- [x]" or "- [X]" (completed)
505
+ if [[ -f "$RALPH_DIR/@fix_plan.md" ]]; then
506
+ local uncompleted_items=$(grep -cE "^[[:space:]]*- \[ \]" "$RALPH_DIR/@fix_plan.md" 2>/dev/null || true)
507
+ [[ -z "$uncompleted_items" ]] && uncompleted_items=0
508
+ local completed_items=$(grep -cE "^[[:space:]]*- \[[xX]\]" "$RALPH_DIR/@fix_plan.md" 2>/dev/null || true)
509
+ [[ -z "$completed_items" ]] && completed_items=0
510
+ local total_items=$((uncompleted_items + completed_items))
511
+
512
+ if [[ $total_items -gt 0 ]] && [[ $completed_items -eq $total_items ]]; then
513
+ log_status "WARN" "Exit condition: All @fix_plan.md items completed ($completed_items/$total_items)" >&2
514
+ echo "plan_complete"
515
+ return 0
516
+ fi
517
+ fi
518
+
519
+ echo "" # Return empty string instead of using return code
520
+ }
521
+
522
+ # =============================================================================
523
+ # MODERN CLI HELPER FUNCTIONS (Phase 1.1)
524
+ # =============================================================================
525
+
526
+ # Check Claude CLI version for compatibility with modern flags
527
+ check_claude_version() {
528
+ local version=$($CLAUDE_CODE_CMD --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
529
+
530
+ if [[ -z "$version" ]]; then
531
+ log_status "WARN" "Cannot detect Claude CLI version, assuming compatible"
532
+ return 0
533
+ fi
534
+
535
+ # Compare versions (simplified semver comparison)
536
+ local required="$CLAUDE_MIN_VERSION"
537
+
538
+ # Convert to comparable integers (major * 10000 + minor * 100 + patch)
539
+ local ver_parts=(${version//./ })
540
+ local req_parts=(${required//./ })
541
+
542
+ local ver_num=$((${ver_parts[0]:-0} * 10000 + ${ver_parts[1]:-0} * 100 + ${ver_parts[2]:-0}))
543
+ local req_num=$((${req_parts[0]:-0} * 10000 + ${req_parts[1]:-0} * 100 + ${req_parts[2]:-0}))
544
+
545
+ if [[ $ver_num -lt $req_num ]]; then
546
+ log_status "WARN" "Claude CLI version $version < $required. Some modern features may not work."
547
+ log_status "WARN" "Consider upgrading: npm update -g @anthropic-ai/claude-code"
548
+ return 1
549
+ fi
550
+
551
+ log_status "INFO" "Claude CLI version $version (>= $required) - modern features enabled"
552
+ return 0
553
+ }
554
+
555
+ # Validate allowed tools against whitelist
556
+ # Returns 0 if valid, 1 if invalid with error message
557
+ validate_allowed_tools() {
558
+ local tools_input=$1
559
+
560
+ if [[ -z "$tools_input" ]]; then
561
+ return 0 # Empty is valid (uses defaults)
562
+ fi
563
+
564
+ # Split by comma
565
+ local IFS=','
566
+ read -ra tools <<< "$tools_input"
567
+
568
+ for tool in "${tools[@]}"; do
569
+ # Trim whitespace
570
+ tool=$(echo "$tool" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
571
+
572
+ if [[ -z "$tool" ]]; then
573
+ continue
574
+ fi
575
+
576
+ local valid=false
577
+
578
+ # Check against valid patterns
579
+ for pattern in "${VALID_TOOL_PATTERNS[@]}"; do
580
+ if [[ "$tool" == "$pattern" ]]; then
581
+ valid=true
582
+ break
583
+ fi
584
+
585
+ # Check for Bash(*) pattern - any Bash with parentheses is allowed
586
+ if [[ "$tool" =~ ^Bash\(.+\)$ ]]; then
587
+ valid=true
588
+ break
589
+ fi
590
+ done
591
+
592
+ if [[ "$valid" == "false" ]]; then
593
+ echo "Error: Invalid tool in --allowed-tools: '$tool'"
594
+ echo "Valid tools: ${VALID_TOOL_PATTERNS[*]}"
595
+ echo "Note: Bash(...) patterns with any content are allowed (e.g., 'Bash(git *)')"
596
+ return 1
597
+ fi
598
+ done
599
+
600
+ return 0
601
+ }
602
+
603
+ # Build loop context for Claude Code session
604
+ # Provides loop-specific context via --append-system-prompt
605
+ build_loop_context() {
606
+ local loop_count=$1
607
+ local context=""
608
+
609
+ # Add loop number
610
+ context="Loop #${loop_count}. "
611
+
612
+ # Extract incomplete tasks from @fix_plan.md
613
+ # Bug #3 Fix: Support indented markdown checkboxes with [[:space:]]* pattern
614
+ if [[ -f "$RALPH_DIR/@fix_plan.md" ]]; then
615
+ local incomplete_tasks=$(grep -cE "^[[:space:]]*- \[ \]" "$RALPH_DIR/@fix_plan.md" 2>/dev/null || true)
616
+ [[ -z "$incomplete_tasks" ]] && incomplete_tasks=0
617
+ context+="Remaining tasks: ${incomplete_tasks}. "
618
+ fi
619
+
620
+ # Add circuit breaker state
621
+ if [[ -f "$RALPH_DIR/.circuit_breaker_state" ]]; then
622
+ local cb_state=$(jq -r '.state // "UNKNOWN"' "$RALPH_DIR/.circuit_breaker_state" 2>/dev/null)
623
+ if [[ "$cb_state" != "CLOSED" && "$cb_state" != "null" && -n "$cb_state" ]]; then
624
+ context+="Circuit breaker: ${cb_state}. "
625
+ fi
626
+ fi
627
+
628
+ # Add previous loop summary (truncated)
629
+ if [[ -f "$RESPONSE_ANALYSIS_FILE" ]]; then
630
+ local prev_summary=$(jq -r '.analysis.work_summary // ""' "$RESPONSE_ANALYSIS_FILE" 2>/dev/null | head -c 200)
631
+ if [[ -n "$prev_summary" && "$prev_summary" != "null" ]]; then
632
+ context+="Previous: ${prev_summary}"
633
+ fi
634
+ fi
635
+
636
+ # Limit total length to ~500 chars
637
+ echo "${context:0:500}"
638
+ }
639
+
640
+ # Get session file age in hours (cross-platform)
641
+ # Returns: age in hours on stdout, or -1 if stat fails
642
+ # Note: Returns 0 for files less than 1 hour old
643
+ get_session_file_age_hours() {
644
+ local file=$1
645
+
646
+ if [[ ! -f "$file" ]]; then
647
+ echo "0"
648
+ return
649
+ fi
650
+
651
+ # Get file modification time using capability detection
652
+ # Handles macOS with Homebrew coreutils where stat flags differ
653
+ local file_mtime
654
+
655
+ # Try GNU stat first (Linux, macOS with Homebrew coreutils)
656
+ if file_mtime=$(stat -c %Y "$file" 2>/dev/null) && [[ -n "$file_mtime" && "$file_mtime" =~ ^[0-9]+$ ]]; then
657
+ : # success
658
+ # Try BSD stat (native macOS)
659
+ elif file_mtime=$(stat -f %m "$file" 2>/dev/null) && [[ -n "$file_mtime" && "$file_mtime" =~ ^[0-9]+$ ]]; then
660
+ : # success
661
+ # Fallback to date -r (most portable)
662
+ elif file_mtime=$(date -r "$file" +%s 2>/dev/null) && [[ -n "$file_mtime" && "$file_mtime" =~ ^[0-9]+$ ]]; then
663
+ : # success
664
+ else
665
+ file_mtime=""
666
+ fi
667
+
668
+ # Handle stat failure - return -1 to indicate error
669
+ # This prevents false expiration when stat fails
670
+ if [[ -z "$file_mtime" || "$file_mtime" == "0" ]]; then
671
+ echo "-1"
672
+ return
673
+ fi
674
+
675
+ local current_time
676
+ current_time=$(date +%s)
677
+
678
+ local age_seconds=$((current_time - file_mtime))
679
+ local age_hours=$((age_seconds / 3600))
680
+
681
+ echo "$age_hours"
682
+ }
683
+
684
+ # Initialize or resume Claude session (with expiration check)
685
+ #
686
+ # Session Expiration Strategy:
687
+ # - Default expiration: 24 hours (configurable via CLAUDE_SESSION_EXPIRY_HOURS)
688
+ # - 24 hours chosen because: long enough for multi-day projects, short enough
689
+ # to prevent stale context from causing unpredictable behavior
690
+ # - Sessions auto-expire to ensure Claude starts fresh periodically
691
+ #
692
+ # Returns (stdout):
693
+ # - Session ID string: when resuming a valid, non-expired session
694
+ # - Empty string: when starting new session (no file, expired, or stat error)
695
+ #
696
+ # Return codes:
697
+ # - 0: Always returns success (caller should check stdout for session ID)
698
+ #
699
+ init_claude_session() {
700
+ if [[ -f "$CLAUDE_SESSION_FILE" ]]; then
701
+ # Check session age
702
+ local age_hours
703
+ age_hours=$(get_session_file_age_hours "$CLAUDE_SESSION_FILE")
704
+
705
+ # Handle stat failure (-1) - treat as needing new session
706
+ # Don't expire sessions when we can't determine age
707
+ if [[ $age_hours -eq -1 ]]; then
708
+ log_status "WARN" "Could not determine session age, starting new session"
709
+ rm -f "$CLAUDE_SESSION_FILE"
710
+ echo ""
711
+ return 0
712
+ fi
713
+
714
+ # Check if session has expired
715
+ if [[ $age_hours -ge $CLAUDE_SESSION_EXPIRY_HOURS ]]; then
716
+ log_status "INFO" "Session expired (${age_hours}h old, max ${CLAUDE_SESSION_EXPIRY_HOURS}h), starting new session"
717
+ rm -f "$CLAUDE_SESSION_FILE"
718
+ echo ""
719
+ return 0
720
+ fi
721
+
722
+ # Session is valid, try to read it
723
+ local session_id=$(cat "$CLAUDE_SESSION_FILE" 2>/dev/null)
724
+ if [[ -n "$session_id" ]]; then
725
+ log_status "INFO" "Resuming Claude session: ${session_id:0:20}... (${age_hours}h old)"
726
+ echo "$session_id"
727
+ return 0
728
+ fi
729
+ fi
730
+
731
+ log_status "INFO" "Starting new Claude session"
732
+ echo ""
733
+ }
734
+
735
+ # Save session ID after successful execution
736
+ save_claude_session() {
737
+ local output_file=$1
738
+
739
+ # Try to extract session ID from JSON output
740
+ if [[ -f "$output_file" ]]; then
741
+ local session_id=$(jq -r '.metadata.session_id // .session_id // empty' "$output_file" 2>/dev/null)
742
+ if [[ -n "$session_id" && "$session_id" != "null" ]]; then
743
+ echo "$session_id" > "$CLAUDE_SESSION_FILE"
744
+ log_status "INFO" "Saved Claude session: ${session_id:0:20}..."
745
+ fi
746
+ fi
747
+ }
748
+
749
+ # =============================================================================
750
+ # SESSION LIFECYCLE MANAGEMENT FUNCTIONS (Phase 1.2)
751
+ # =============================================================================
752
+
753
+ # Get current session ID from Ralph session file
754
+ # Returns: session ID string or empty if not found
755
+ get_session_id() {
756
+ if [[ ! -f "$RALPH_SESSION_FILE" ]]; then
757
+ echo ""
758
+ return 0
759
+ fi
760
+
761
+ # Extract session_id from JSON file (SC2155: separate declare from assign)
762
+ local session_id
763
+ session_id=$(jq -r '.session_id // ""' "$RALPH_SESSION_FILE" 2>/dev/null)
764
+ local jq_status=$?
765
+
766
+ # Handle jq failure or null/empty results
767
+ if [[ $jq_status -ne 0 || -z "$session_id" || "$session_id" == "null" ]]; then
768
+ session_id=""
769
+ fi
770
+ echo "$session_id"
771
+ return 0
772
+ }
773
+
774
+ # Reset session with reason logging
775
+ # Usage: reset_session "reason_for_reset"
776
+ reset_session() {
777
+ local reason=${1:-"manual_reset"}
778
+
779
+ # Get current timestamp
780
+ local reset_timestamp
781
+ reset_timestamp=$(get_iso_timestamp)
782
+
783
+ # Always create/overwrite the session file using jq for safe JSON escaping
784
+ jq -n \
785
+ --arg session_id "" \
786
+ --arg created_at "" \
787
+ --arg last_used "" \
788
+ --arg reset_at "$reset_timestamp" \
789
+ --arg reset_reason "$reason" \
790
+ '{
791
+ session_id: $session_id,
792
+ created_at: $created_at,
793
+ last_used: $last_used,
794
+ reset_at: $reset_at,
795
+ reset_reason: $reset_reason
796
+ }' > "$RALPH_SESSION_FILE"
797
+
798
+ # Also clear the Claude session file for consistency
799
+ rm -f "$CLAUDE_SESSION_FILE" 2>/dev/null
800
+
801
+ # Clear exit signals to prevent stale completion indicators from causing premature exit (issue #91)
802
+ # This ensures a fresh start without leftover state from previous sessions
803
+ if [[ -f "$EXIT_SIGNALS_FILE" ]]; then
804
+ echo '{"test_only_loops": [], "done_signals": [], "completion_indicators": []}' > "$EXIT_SIGNALS_FILE"
805
+ [[ "${VERBOSE_PROGRESS:-}" == "true" ]] && log_status "INFO" "Cleared exit signals file"
806
+ fi
807
+
808
+ # Clear response analysis to prevent stale EXIT_SIGNAL from previous session
809
+ rm -f "$RESPONSE_ANALYSIS_FILE" 2>/dev/null
810
+
811
+ # Log the session transition (non-fatal to prevent script exit under set -e)
812
+ log_session_transition "active" "reset" "$reason" "${loop_count:-0}" || true
813
+
814
+ log_status "INFO" "Session reset: $reason"
815
+ }
816
+
817
+ # Log session state transitions to history file
818
+ # Usage: log_session_transition from_state to_state reason loop_number
819
+ log_session_transition() {
820
+ local from_state=$1
821
+ local to_state=$2
822
+ local reason=$3
823
+ local loop_number=${4:-0}
824
+
825
+ # Get timestamp once (SC2155: separate declare from assign)
826
+ local ts
827
+ ts=$(get_iso_timestamp)
828
+
829
+ # Create transition entry using jq for safe JSON (SC2155: separate declare from assign)
830
+ local transition
831
+ transition=$(jq -n -c \
832
+ --arg timestamp "$ts" \
833
+ --arg from_state "$from_state" \
834
+ --arg to_state "$to_state" \
835
+ --arg reason "$reason" \
836
+ --argjson loop_number "$loop_number" \
837
+ '{
838
+ timestamp: $timestamp,
839
+ from_state: $from_state,
840
+ to_state: $to_state,
841
+ reason: $reason,
842
+ loop_number: $loop_number
843
+ }')
844
+
845
+ # Read history file defensively - fallback to empty array on any failure
846
+ local history
847
+ if [[ -f "$RALPH_SESSION_HISTORY_FILE" ]]; then
848
+ history=$(cat "$RALPH_SESSION_HISTORY_FILE" 2>/dev/null)
849
+ # Validate JSON, fallback to empty array if corrupted
850
+ if ! echo "$history" | jq empty 2>/dev/null; then
851
+ history='[]'
852
+ fi
853
+ else
854
+ history='[]'
855
+ fi
856
+
857
+ # Append transition and keep only last 50 entries
858
+ local updated_history
859
+ updated_history=$(echo "$history" | jq ". += [$transition] | .[-50:]" 2>/dev/null)
860
+ local jq_status=$?
861
+
862
+ # Only write if jq succeeded
863
+ if [[ $jq_status -eq 0 && -n "$updated_history" ]]; then
864
+ echo "$updated_history" > "$RALPH_SESSION_HISTORY_FILE"
865
+ else
866
+ # Fallback: start fresh with just this transition
867
+ echo "[$transition]" > "$RALPH_SESSION_HISTORY_FILE"
868
+ fi
869
+ }
870
+
871
+ # Generate a unique session ID using timestamp and random component
872
+ generate_session_id() {
873
+ local ts
874
+ ts=$(date +%s)
875
+ local rand
876
+ rand=$RANDOM
877
+ echo "ralph-${ts}-${rand}"
878
+ }
879
+
880
+ # Initialize session tracking (called at loop start)
881
+ init_session_tracking() {
882
+ local ts
883
+ ts=$(get_iso_timestamp)
884
+
885
+ # Create session file if it doesn't exist
886
+ if [[ ! -f "$RALPH_SESSION_FILE" ]]; then
887
+ local new_session_id
888
+ new_session_id=$(generate_session_id)
889
+
890
+ jq -n \
891
+ --arg session_id "$new_session_id" \
892
+ --arg created_at "$ts" \
893
+ --arg last_used "$ts" \
894
+ --arg reset_at "" \
895
+ --arg reset_reason "" \
896
+ '{
897
+ session_id: $session_id,
898
+ created_at: $created_at,
899
+ last_used: $last_used,
900
+ reset_at: $reset_at,
901
+ reset_reason: $reset_reason
902
+ }' > "$RALPH_SESSION_FILE"
903
+
904
+ log_status "INFO" "Initialized session tracking (session: $new_session_id)"
905
+ return 0
906
+ fi
907
+
908
+ # Validate existing session file
909
+ if ! jq empty "$RALPH_SESSION_FILE" 2>/dev/null; then
910
+ log_status "WARN" "Corrupted session file detected, recreating..."
911
+ local new_session_id
912
+ new_session_id=$(generate_session_id)
913
+
914
+ jq -n \
915
+ --arg session_id "$new_session_id" \
916
+ --arg created_at "$ts" \
917
+ --arg last_used "$ts" \
918
+ --arg reset_at "$ts" \
919
+ --arg reset_reason "corrupted_file_recovery" \
920
+ '{
921
+ session_id: $session_id,
922
+ created_at: $created_at,
923
+ last_used: $last_used,
924
+ reset_at: $reset_at,
925
+ reset_reason: $reset_reason
926
+ }' > "$RALPH_SESSION_FILE"
927
+ fi
928
+ }
929
+
930
+ # Update last_used timestamp in session file (called on each loop iteration)
931
+ update_session_last_used() {
932
+ if [[ ! -f "$RALPH_SESSION_FILE" ]]; then
933
+ return 0
934
+ fi
935
+
936
+ local ts
937
+ ts=$(get_iso_timestamp)
938
+
939
+ # Update last_used in existing session file
940
+ local updated
941
+ updated=$(jq --arg last_used "$ts" '.last_used = $last_used' "$RALPH_SESSION_FILE" 2>/dev/null)
942
+ local jq_status=$?
943
+
944
+ if [[ $jq_status -eq 0 && -n "$updated" ]]; then
945
+ echo "$updated" > "$RALPH_SESSION_FILE"
946
+ fi
947
+ }
948
+
949
+ # Global array for Claude command arguments (avoids shell injection)
950
+ declare -a CLAUDE_CMD_ARGS=()
951
+
952
+ # Build Claude CLI command with modern flags using array (shell-injection safe)
953
+ # Populates global CLAUDE_CMD_ARGS array for direct execution
954
+ # Uses -p flag with prompt content (Claude CLI does not have --prompt-file)
955
+ build_claude_command() {
956
+ local prompt_file=$1
957
+ local loop_context=$2
958
+ local session_id=$3
959
+
960
+ # Reset global array
961
+ # Note: We do NOT use --dangerously-skip-permissions here. Tool permissions
962
+ # are controlled via --allowedTools from CLAUDE_ALLOWED_TOOLS in .ralphrc.
963
+ # This preserves the permission denial circuit breaker (Issue #101).
964
+ CLAUDE_CMD_ARGS=("$CLAUDE_CODE_CMD")
965
+
966
+ # Check if prompt file exists
967
+ if [[ ! -f "$prompt_file" ]]; then
968
+ log_status "ERROR" "Prompt file not found: $prompt_file"
969
+ return 1
970
+ fi
971
+
972
+ # Add output format flag
973
+ if [[ "$CLAUDE_OUTPUT_FORMAT" == "json" ]]; then
974
+ CLAUDE_CMD_ARGS+=("--output-format" "json")
975
+ fi
976
+
977
+ # Add allowed tools (each tool as separate array element)
978
+ if [[ -n "$CLAUDE_ALLOWED_TOOLS" ]]; then
979
+ CLAUDE_CMD_ARGS+=("--allowedTools")
980
+ # Split by comma and add each tool
981
+ local IFS=','
982
+ read -ra tools_array <<< "$CLAUDE_ALLOWED_TOOLS"
983
+ for tool in "${tools_array[@]}"; do
984
+ # Trim whitespace
985
+ tool=$(echo "$tool" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
986
+ if [[ -n "$tool" ]]; then
987
+ CLAUDE_CMD_ARGS+=("$tool")
988
+ fi
989
+ done
990
+ fi
991
+
992
+ # Add session continuity flag
993
+ # IMPORTANT: Use --resume with explicit session ID instead of --continue
994
+ # --continue resumes the "most recent session in current directory" which
995
+ # can hijack active Claude Code sessions. --resume with a specific session ID
996
+ # ensures we only resume Ralph's own sessions. (Issue #151)
997
+ if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
998
+ CLAUDE_CMD_ARGS+=("--resume" "$session_id")
999
+ fi
1000
+ # If no session_id, start fresh - Claude will generate a new session ID
1001
+ # which we'll capture via save_claude_session() for future loops
1002
+
1003
+ # Add loop context as system prompt (no escaping needed - array handles it)
1004
+ if [[ -n "$loop_context" ]]; then
1005
+ CLAUDE_CMD_ARGS+=("--append-system-prompt" "$loop_context")
1006
+ fi
1007
+
1008
+ # Read prompt file content and use -p flag
1009
+ # Note: Claude CLI uses -p for prompts, not --prompt-file (which doesn't exist)
1010
+ # Array-based approach maintains shell injection safety
1011
+ local prompt_content
1012
+ prompt_content=$(cat "$prompt_file")
1013
+ CLAUDE_CMD_ARGS+=("-p" "$prompt_content")
1014
+ }
1015
+
1016
+ # Main execution function
1017
+ execute_claude_code() {
1018
+ local timestamp=$(date '+%Y-%m-%d_%H-%M-%S')
1019
+ local output_file="$LOG_DIR/claude_output_${timestamp}.log"
1020
+ local loop_count=$1
1021
+ local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
1022
+ calls_made=$((calls_made + 1))
1023
+
1024
+ # Fix #141: Capture git HEAD SHA at loop start to detect commits as progress
1025
+ # Store in file for access by progress detection after Claude execution
1026
+ local loop_start_sha=""
1027
+ if command -v git &>/dev/null && git rev-parse --git-dir &>/dev/null 2>&1; then
1028
+ loop_start_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
1029
+ fi
1030
+ echo "$loop_start_sha" > "$RALPH_DIR/.loop_start_sha"
1031
+
1032
+ log_status "LOOP" "Executing Claude Code (Call $calls_made/$MAX_CALLS_PER_HOUR)"
1033
+ local timeout_seconds=$((CLAUDE_TIMEOUT_MINUTES * 60))
1034
+ log_status "INFO" "⏳ Starting Claude Code execution... (timeout: ${CLAUDE_TIMEOUT_MINUTES}m)"
1035
+
1036
+ # Build loop context for session continuity
1037
+ local loop_context=""
1038
+ if [[ "$CLAUDE_USE_CONTINUE" == "true" ]]; then
1039
+ loop_context=$(build_loop_context "$loop_count")
1040
+ if [[ -n "$loop_context" && "$VERBOSE_PROGRESS" == "true" ]]; then
1041
+ log_status "INFO" "Loop context: $loop_context"
1042
+ fi
1043
+ fi
1044
+
1045
+ # Initialize or resume session
1046
+ local session_id=""
1047
+ if [[ "$CLAUDE_USE_CONTINUE" == "true" ]]; then
1048
+ session_id=$(init_claude_session)
1049
+ fi
1050
+
1051
+ # Live mode requires JSON output (stream-json) — override text format
1052
+ if [[ "$LIVE_OUTPUT" == "true" && "$CLAUDE_OUTPUT_FORMAT" == "text" ]]; then
1053
+ log_status "WARN" "Live mode requires JSON output format. Overriding text → json for this session."
1054
+ CLAUDE_OUTPUT_FORMAT="json"
1055
+ fi
1056
+
1057
+ # Build the Claude CLI command with modern flags
1058
+ local use_modern_cli=false
1059
+
1060
+ if build_claude_command "$PROMPT_FILE" "$loop_context" "$session_id"; then
1061
+ use_modern_cli=true
1062
+ log_status "INFO" "Using modern CLI mode (${CLAUDE_OUTPUT_FORMAT} output)"
1063
+ else
1064
+ log_status "WARN" "Failed to build modern CLI command, falling back to legacy mode"
1065
+ if [[ "$LIVE_OUTPUT" == "true" ]]; then
1066
+ log_status "ERROR" "Live mode requires a built Claude command. Falling back to background mode."
1067
+ LIVE_OUTPUT=false
1068
+ fi
1069
+ fi
1070
+
1071
+ # Execute Claude Code
1072
+ local exit_code=0
1073
+
1074
+ # Initialize live.log for this execution
1075
+ echo -e "\n\n=== Loop #$loop_count - $(date '+%Y-%m-%d %H:%M:%S') ===" > "$LIVE_LOG_FILE"
1076
+
1077
+ if [[ "$LIVE_OUTPUT" == "true" ]]; then
1078
+ # LIVE MODE: Show streaming output in real-time using stream-json + jq
1079
+ # Based on: https://www.ytyng.com/en/blog/claude-stream-json-jq/
1080
+ #
1081
+ # Uses CLAUDE_CMD_ARGS from build_claude_command() to preserve:
1082
+ # - --allowedTools (tool permissions)
1083
+ # - --append-system-prompt (loop context)
1084
+ # - --continue (session continuity)
1085
+ # - -p (prompt content)
1086
+
1087
+ # Check dependencies for live mode
1088
+ if ! command -v jq &> /dev/null; then
1089
+ log_status "ERROR" "Live mode requires 'jq' but it's not installed. Falling back to background mode."
1090
+ LIVE_OUTPUT=false
1091
+ elif ! command -v stdbuf &> /dev/null; then
1092
+ log_status "ERROR" "Live mode requires 'stdbuf' (from coreutils) but it's not installed. Falling back to background mode."
1093
+ LIVE_OUTPUT=false
1094
+ fi
1095
+ fi
1096
+
1097
+ if [[ "$LIVE_OUTPUT" == "true" ]]; then
1098
+ # Safety check: live mode requires a successfully built modern command
1099
+ if [[ "$use_modern_cli" != "true" || ${#CLAUDE_CMD_ARGS[@]} -eq 0 ]]; then
1100
+ log_status "ERROR" "Live mode requires a built Claude command. Falling back to background mode."
1101
+ LIVE_OUTPUT=false
1102
+ fi
1103
+ fi
1104
+
1105
+ if [[ "$LIVE_OUTPUT" == "true" ]]; then
1106
+ log_status "INFO" "📺 Live output mode enabled - showing Claude Code streaming..."
1107
+ echo -e "${PURPLE}━━━━━━━━━━━━━━━━ Claude Code Output ━━━━━━━━━━━━━━━━${NC}"
1108
+
1109
+ # Modify CLAUDE_CMD_ARGS: replace --output-format value with stream-json
1110
+ # and add streaming-specific flags
1111
+ local -a LIVE_CMD_ARGS=()
1112
+ local skip_next=false
1113
+ for arg in "${CLAUDE_CMD_ARGS[@]}"; do
1114
+ if [[ "$skip_next" == "true" ]]; then
1115
+ # Replace "json" with "stream-json" for output format
1116
+ LIVE_CMD_ARGS+=("stream-json")
1117
+ skip_next=false
1118
+ elif [[ "$arg" == "--output-format" ]]; then
1119
+ LIVE_CMD_ARGS+=("$arg")
1120
+ skip_next=true
1121
+ else
1122
+ LIVE_CMD_ARGS+=("$arg")
1123
+ fi
1124
+ done
1125
+
1126
+ # Add streaming-specific flags (--verbose and --include-partial-messages)
1127
+ # These are required for stream-json to work properly
1128
+ LIVE_CMD_ARGS+=("--verbose" "--include-partial-messages")
1129
+
1130
+ # jq filter: show text + tool names + newlines for readability
1131
+ local jq_filter='
1132
+ if .type == "stream_event" then
1133
+ if .event.type == "content_block_delta" and .event.delta.type == "text_delta" then
1134
+ .event.delta.text
1135
+ elif .event.type == "content_block_start" and .event.content_block.type == "tool_use" then
1136
+ "\n\n⚡ [" + .event.content_block.name + "]\n"
1137
+ elif .event.type == "content_block_stop" then
1138
+ "\n"
1139
+ else
1140
+ empty
1141
+ end
1142
+ else
1143
+ empty
1144
+ end'
1145
+
1146
+ # Execute with streaming, preserving all flags from build_claude_command()
1147
+ # Use stdbuf to disable buffering for real-time output
1148
+ # Use portable_timeout for consistent timeout protection (Issue: missing timeout)
1149
+ # Capture all pipeline exit codes for proper error handling
1150
+ # stdin must be redirected from /dev/null because newer Claude CLI versions
1151
+ # read from stdin even in -p (print) mode, causing the process to hang
1152
+ set -o pipefail
1153
+ portable_timeout ${timeout_seconds}s stdbuf -oL "${LIVE_CMD_ARGS[@]}" \
1154
+ < /dev/null 2>&1 | stdbuf -oL tee "$output_file" | stdbuf -oL jq --unbuffered -j "$jq_filter" 2>/dev/null | tee "$LIVE_LOG_FILE"
1155
+
1156
+ # Capture exit codes from pipeline
1157
+ local -a pipe_status=("${PIPESTATUS[@]}")
1158
+ set +o pipefail
1159
+
1160
+ # Primary exit code is from Claude/timeout (first command in pipeline)
1161
+ exit_code=${pipe_status[0]}
1162
+
1163
+ # Check for tee failures (second command) - could break logging/session
1164
+ if [[ ${pipe_status[1]} -ne 0 ]]; then
1165
+ log_status "WARN" "Failed to write stream output to log file (exit code ${pipe_status[1]})"
1166
+ fi
1167
+
1168
+ # Check for jq failures (third command) - warn but don't fail
1169
+ if [[ ${pipe_status[2]} -ne 0 ]]; then
1170
+ log_status "WARN" "jq filter had issues parsing some stream events (exit code ${pipe_status[2]})"
1171
+ fi
1172
+
1173
+ echo ""
1174
+ echo -e "${PURPLE}━━━━━━━━━━━━━━━━ End of Output ━━━━━━━━━━━━━━━━━━━${NC}"
1175
+
1176
+ # Extract session ID from stream-json output for session continuity
1177
+ # Stream-json format has session_id in the final "result" type message
1178
+ # Keep full stream output in _stream.log, extract session data separately
1179
+ if [[ "$CLAUDE_USE_CONTINUE" == "true" && -f "$output_file" ]]; then
1180
+ # Preserve full stream output for analysis (don't overwrite output_file)
1181
+ local stream_output_file="${output_file%.log}_stream.log"
1182
+ cp "$output_file" "$stream_output_file"
1183
+
1184
+ # Extract the result message and convert to standard JSON format
1185
+ # Use flexible regex to match various JSON formatting styles
1186
+ # Matches: "type":"result", "type": "result", "type" : "result"
1187
+ local result_line=$(grep -E '"type"[[:space:]]*:[[:space:]]*"result"' "$output_file" 2>/dev/null | tail -1)
1188
+
1189
+ if [[ -n "$result_line" ]]; then
1190
+ # Validate that extracted line is valid JSON before using it
1191
+ if echo "$result_line" | jq -e . >/dev/null 2>&1; then
1192
+ # Write validated result as the output_file for downstream processing
1193
+ # (save_claude_session and analyze_response expect JSON format)
1194
+ echo "$result_line" > "$output_file"
1195
+ log_status "INFO" "Extracted and validated session data from stream output"
1196
+ else
1197
+ log_status "WARN" "Extracted result line is not valid JSON, keeping stream output"
1198
+ # Restore original stream output
1199
+ cp "$stream_output_file" "$output_file"
1200
+ fi
1201
+ else
1202
+ log_status "WARN" "Could not find result message in stream output"
1203
+ # Keep stream output as-is for debugging
1204
+ fi
1205
+ fi
1206
+ else
1207
+ # BACKGROUND MODE: Original behavior with progress monitoring
1208
+ if [[ "$use_modern_cli" == "true" ]]; then
1209
+ # Modern execution with command array (shell-injection safe)
1210
+ # Execute array directly without bash -c to prevent shell metacharacter interpretation
1211
+ # stdin must be redirected from /dev/null because newer Claude CLI versions
1212
+ # read from stdin even in -p (print) mode, causing SIGTTIN suspension
1213
+ # when the process is backgrounded
1214
+ if portable_timeout ${timeout_seconds}s "${CLAUDE_CMD_ARGS[@]}" < /dev/null > "$output_file" 2>&1 &
1215
+ then
1216
+ : # Continue to wait loop
1217
+ else
1218
+ log_status "ERROR" " Failed to start Claude Code process (modern mode)"
1219
+ # Fall back to legacy mode
1220
+ log_status "INFO" "Falling back to legacy mode..."
1221
+ use_modern_cli=false
1222
+ fi
1223
+ fi
1224
+
1225
+ # Fall back to legacy stdin piping if modern mode failed or not enabled
1226
+ # Note: Legacy mode doesn't use --allowedTools, so tool permissions
1227
+ # will be handled by Claude Code's default permission system
1228
+ if [[ "$use_modern_cli" == "false" ]]; then
1229
+ if portable_timeout ${timeout_seconds}s $CLAUDE_CODE_CMD < "$PROMPT_FILE" > "$output_file" 2>&1 &
1230
+ then
1231
+ : # Continue to wait loop
1232
+ else
1233
+ log_status "ERROR" "❌ Failed to start Claude Code process"
1234
+ return 1
1235
+ fi
1236
+ fi
1237
+
1238
+ # Get PID and monitor progress
1239
+ local claude_pid=$!
1240
+ local progress_counter=0
1241
+
1242
+ # Show progress while Claude Code is running
1243
+ while kill -0 $claude_pid 2>/dev/null; do
1244
+ progress_counter=$((progress_counter + 1))
1245
+ case $((progress_counter % 4)) in
1246
+ 1) progress_indicator="⠋" ;;
1247
+ 2) progress_indicator="⠙" ;;
1248
+ 3) progress_indicator="⠹" ;;
1249
+ 0) progress_indicator="⠸" ;;
1250
+ esac
1251
+
1252
+ # Get last line from output if available
1253
+ local last_line=""
1254
+ if [[ -f "$output_file" && -s "$output_file" ]]; then
1255
+ last_line=$(tail -1 "$output_file" 2>/dev/null | head -c 80)
1256
+ # Copy to live.log for tmux monitoring
1257
+ cp "$output_file" "$LIVE_LOG_FILE" 2>/dev/null
1258
+ fi
1259
+
1260
+ # Update progress file for monitor
1261
+ cat > "$PROGRESS_FILE" << EOF
1262
+ {
1263
+ "status": "executing",
1264
+ "indicator": "$progress_indicator",
1265
+ "elapsed_seconds": $((progress_counter * 10)),
1266
+ "last_output": "$last_line",
1267
+ "timestamp": "$(date '+%Y-%m-%d %H:%M:%S')"
1268
+ }
1269
+ EOF
1270
+
1271
+ # Only log if verbose mode is enabled
1272
+ if [[ "$VERBOSE_PROGRESS" == "true" ]]; then
1273
+ if [[ -n "$last_line" ]]; then
1274
+ log_status "INFO" "$progress_indicator Claude Code: $last_line... (${progress_counter}0s)"
1275
+ else
1276
+ log_status "INFO" "$progress_indicator Claude Code working... (${progress_counter}0s elapsed)"
1277
+ fi
1278
+ fi
1279
+
1280
+ sleep 10
1281
+ done
1282
+
1283
+ # Wait for the process to finish and get exit code
1284
+ wait $claude_pid
1285
+ exit_code=$?
1286
+ fi
1287
+
1288
+ if [ $exit_code -eq 0 ]; then
1289
+ # Only increment counter on successful execution
1290
+ echo "$calls_made" > "$CALL_COUNT_FILE"
1291
+
1292
+ # Clear progress file
1293
+ echo '{"status": "completed", "timestamp": "'$(date '+%Y-%m-%d %H:%M:%S')'"}' > "$PROGRESS_FILE"
1294
+
1295
+ log_status "SUCCESS" "✅ Claude Code execution completed successfully"
1296
+
1297
+ # Save session ID from JSON output (Phase 1.1)
1298
+ if [[ "$CLAUDE_USE_CONTINUE" == "true" ]]; then
1299
+ save_claude_session "$output_file"
1300
+ fi
1301
+
1302
+ # Analyze the response
1303
+ log_status "INFO" "🔍 Analyzing Claude Code response..."
1304
+ analyze_response "$output_file" "$loop_count"
1305
+ local analysis_exit_code=$?
1306
+
1307
+ # Update exit signals based on analysis
1308
+ update_exit_signals
1309
+
1310
+ # Log analysis summary
1311
+ log_analysis_summary
1312
+
1313
+ # Get file change count for circuit breaker
1314
+ # Fix #141: Detect both uncommitted changes AND committed changes
1315
+ local files_changed=0
1316
+ local loop_start_sha=""
1317
+ local current_sha=""
1318
+
1319
+ if [[ -f "$RALPH_DIR/.loop_start_sha" ]]; then
1320
+ loop_start_sha=$(cat "$RALPH_DIR/.loop_start_sha" 2>/dev/null || echo "")
1321
+ fi
1322
+
1323
+ if command -v git &>/dev/null && git rev-parse --git-dir &>/dev/null 2>&1; then
1324
+ current_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
1325
+
1326
+ # Check if commits were made (HEAD changed)
1327
+ if [[ -n "$loop_start_sha" && -n "$current_sha" && "$loop_start_sha" != "$current_sha" ]]; then
1328
+ # Commits were made - count union of committed files AND working tree changes
1329
+ # This catches cases where Claude commits some files but still has other modified files
1330
+ files_changed=$(
1331
+ {
1332
+ git diff --name-only "$loop_start_sha" "$current_sha" 2>/dev/null
1333
+ git diff --name-only HEAD 2>/dev/null # unstaged changes
1334
+ git diff --name-only --cached 2>/dev/null # staged changes
1335
+ } | sort -u | wc -l
1336
+ )
1337
+ [[ "$VERBOSE_PROGRESS" == "true" ]] && log_status "DEBUG" "Detected $files_changed unique files changed (commits + working tree) since loop start"
1338
+ else
1339
+ # No commits - check for uncommitted changes (staged + unstaged)
1340
+ files_changed=$(
1341
+ {
1342
+ git diff --name-only 2>/dev/null # unstaged changes
1343
+ git diff --name-only --cached 2>/dev/null # staged changes
1344
+ } | sort -u | wc -l
1345
+ )
1346
+ fi
1347
+ fi
1348
+
1349
+ local has_errors="false"
1350
+
1351
+ # Two-stage error detection to avoid JSON field false positives
1352
+ # Stage 1: Filter out JSON field patterns like "is_error": false
1353
+ # Stage 2: Look for actual error messages in specific contexts
1354
+ # Avoid type annotations like "error: Error" by requiring lowercase after ": error"
1355
+ if grep -v '"[^"]*error[^"]*":' "$output_file" 2>/dev/null | \
1356
+ grep -qE '(^Error:|^ERROR:|^error:|\]: error|Link: error|Error occurred|failed with error|[Ee]xception|Fatal|FATAL)'; then
1357
+ has_errors="true"
1358
+
1359
+ # Debug logging: show what triggered error detection
1360
+ if [[ "$VERBOSE_PROGRESS" == "true" ]]; then
1361
+ log_status "DEBUG" "Error patterns found:"
1362
+ grep -v '"[^"]*error[^"]*":' "$output_file" 2>/dev/null | \
1363
+ grep -nE '(^Error:|^ERROR:|^error:|\]: error|Link: error|Error occurred|failed with error|[Ee]xception|Fatal|FATAL)' | \
1364
+ head -3 | while IFS= read -r line; do
1365
+ log_status "DEBUG" " $line"
1366
+ done
1367
+ fi
1368
+
1369
+ log_status "WARN" "Errors detected in output, check: $output_file"
1370
+ fi
1371
+ local output_length=$(wc -c < "$output_file" 2>/dev/null || echo 0)
1372
+
1373
+ # Record result in circuit breaker
1374
+ record_loop_result "$loop_count" "$files_changed" "$has_errors" "$output_length"
1375
+ local circuit_result=$?
1376
+
1377
+ if [[ $circuit_result -ne 0 ]]; then
1378
+ log_status "WARN" "Circuit breaker opened - halting execution"
1379
+ return 3 # Special code for circuit breaker trip
1380
+ fi
1381
+
1382
+ return 0
1383
+ else
1384
+ # Clear progress file on failure
1385
+ echo '{"status": "failed", "timestamp": "'$(date '+%Y-%m-%d %H:%M:%S')'"}' > "$PROGRESS_FILE"
1386
+
1387
+ # Check if the failure is due to API 5-hour limit
1388
+ if grep -qi "5.*hour.*limit\|limit.*reached.*try.*back\|usage.*limit.*reached" "$output_file"; then
1389
+ log_status "ERROR" "🚫 Claude API 5-hour usage limit reached"
1390
+ return 2 # Special return code for API limit
1391
+ else
1392
+ log_status "ERROR" "❌ Claude Code execution failed, check: $output_file"
1393
+ return 1
1394
+ fi
1395
+ fi
1396
+ }
1397
+
1398
+ # Cleanup function
1399
+ cleanup() {
1400
+ log_status "INFO" "Ralph loop interrupted. Cleaning up..."
1401
+ reset_session "manual_interrupt"
1402
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")" "interrupted" "stopped"
1403
+ exit 0
1404
+ }
1405
+
1406
+ # Set up signal handlers
1407
+ trap cleanup SIGINT SIGTERM
1408
+
1409
+ # Global variable for loop count (needed by cleanup function)
1410
+ loop_count=0
1411
+
1412
+ # Main loop
1413
+ main() {
1414
+ # Load project-specific configuration from .ralphrc
1415
+ if load_ralphrc; then
1416
+ if [[ "$RALPHRC_LOADED" == "true" ]]; then
1417
+ log_status "INFO" "Loaded configuration from .ralphrc"
1418
+ fi
1419
+ fi
1420
+
1421
+ log_status "SUCCESS" "🚀 Ralph loop starting with Claude Code"
1422
+ log_status "INFO" "Max calls per hour: $MAX_CALLS_PER_HOUR"
1423
+ log_status "INFO" "Logs: $LOG_DIR/ | Docs: $DOCS_DIR/ | Status: $STATUS_FILE"
1424
+
1425
+ # Check if project uses old flat structure and needs migration
1426
+ if [[ -f "PROMPT.md" ]] && [[ ! -d ".ralph" ]]; then
1427
+ log_status "ERROR" "This project uses the old flat structure."
1428
+ echo ""
1429
+ echo "Ralph v0.10.0+ uses a .ralph/ subfolder to keep your project root clean."
1430
+ echo ""
1431
+ echo "To upgrade your project, run:"
1432
+ echo " ralph-migrate"
1433
+ echo ""
1434
+ echo "This will move Ralph-specific files to .ralph/ while preserving src/ at root."
1435
+ echo "A backup will be created before migration."
1436
+ exit 1
1437
+ fi
1438
+
1439
+ # Check if this is a Ralph project directory
1440
+ if [[ ! -f "$PROMPT_FILE" ]]; then
1441
+ log_status "ERROR" "Prompt file '$PROMPT_FILE' not found!"
1442
+ echo ""
1443
+
1444
+ # Check if this looks like a partial Ralph project
1445
+ if [[ -f "$RALPH_DIR/@fix_plan.md" ]] || [[ -d "$RALPH_DIR/specs" ]] || [[ -f "$RALPH_DIR/AGENT.md" ]]; then
1446
+ echo "This appears to be a Ralph project but is missing .ralph/PROMPT.md."
1447
+ echo "You may need to create or restore the PROMPT.md file."
1448
+ else
1449
+ echo "This directory is not a Ralph project."
1450
+ fi
1451
+
1452
+ echo ""
1453
+ echo "To fix this:"
1454
+ echo " 1. Enable Ralph in existing project: ralph-enable"
1455
+ echo " 2. Create a new project: ralph-setup my-project"
1456
+ echo " 3. Import existing requirements: ralph-import requirements.md"
1457
+ echo " 4. Navigate to an existing Ralph project directory"
1458
+ echo " 5. Or create .ralph/PROMPT.md manually in this directory"
1459
+ echo ""
1460
+ echo "Ralph projects should contain: .ralph/PROMPT.md, .ralph/@fix_plan.md, .ralph/specs/, src/, etc."
1461
+ exit 1
1462
+ fi
1463
+
1464
+ # Initialize session tracking before entering the loop
1465
+ init_session_tracking
1466
+
1467
+ log_status "INFO" "Starting main loop..."
1468
+
1469
+ while true; do
1470
+ loop_count=$((loop_count + 1))
1471
+
1472
+ # Update session last_used timestamp
1473
+ update_session_last_used
1474
+
1475
+ log_status "INFO" "Loop #$loop_count - calling init_call_tracking..."
1476
+ init_call_tracking
1477
+
1478
+ log_status "LOOP" "=== Starting Loop #$loop_count ==="
1479
+
1480
+ # Check circuit breaker before attempting execution
1481
+ if should_halt_execution; then
1482
+ reset_session "circuit_breaker_open"
1483
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "circuit_breaker_open" "halted" "stagnation_detected"
1484
+ log_status "ERROR" "🛑 Circuit breaker has opened - execution halted"
1485
+ break
1486
+ fi
1487
+
1488
+ # Check rate limits
1489
+ if ! can_make_call; then
1490
+ wait_for_reset
1491
+ continue
1492
+ fi
1493
+
1494
+ # Check for graceful exit conditions
1495
+ local exit_reason=$(should_exit_gracefully)
1496
+ if [[ "$exit_reason" != "" ]]; then
1497
+ # Handle permission_denied specially (Issue #101)
1498
+ if [[ "$exit_reason" == "permission_denied" ]]; then
1499
+ log_status "ERROR" "🚫 Permission denied - halting loop"
1500
+ reset_session "permission_denied"
1501
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "permission_denied" "halted" "permission_denied"
1502
+
1503
+ # Display helpful guidance for resolving permission issues
1504
+ echo ""
1505
+ echo -e "${RED}╔════════════════════════════════════════════════════════════╗${NC}"
1506
+ echo -e "${RED}║ PERMISSION DENIED - Loop Halted ║${NC}"
1507
+ echo -e "${RED}╚════════════════════════════════════════════════════════════╝${NC}"
1508
+ echo ""
1509
+ echo -e "${YELLOW}Claude Code was denied permission to execute commands.${NC}"
1510
+ echo ""
1511
+ echo -e "${YELLOW}To fix this:${NC}"
1512
+ echo " 1. Edit .ralphrc and update ALLOWED_TOOLS to include the required tools"
1513
+ echo " 2. Common patterns:"
1514
+ echo " - Bash(npm *) - All npm commands"
1515
+ echo " - Bash(npm install) - Only npm install"
1516
+ echo " - Bash(pnpm *) - All pnpm commands"
1517
+ echo " - Bash(yarn *) - All yarn commands"
1518
+ echo ""
1519
+ echo -e "${YELLOW}After updating .ralphrc:${NC}"
1520
+ echo " ralph --reset-session # Clear stale session state"
1521
+ echo " ralph --monitor # Restart the loop"
1522
+ echo ""
1523
+
1524
+ # Show current ALLOWED_TOOLS if .ralphrc exists
1525
+ if [[ -f ".ralphrc" ]]; then
1526
+ local current_tools=$(grep "^ALLOWED_TOOLS=" ".ralphrc" 2>/dev/null | cut -d= -f2- | tr -d '"')
1527
+ if [[ -n "$current_tools" ]]; then
1528
+ echo -e "${BLUE}Current ALLOWED_TOOLS:${NC} $current_tools"
1529
+ echo ""
1530
+ fi
1531
+ fi
1532
+
1533
+ break
1534
+ fi
1535
+
1536
+ log_status "SUCCESS" "🏁 Graceful exit triggered: $exit_reason"
1537
+ reset_session "project_complete"
1538
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "graceful_exit" "completed" "$exit_reason"
1539
+
1540
+ log_status "SUCCESS" "🎉 Ralph has completed the project! Final stats:"
1541
+ log_status "INFO" " - Total loops: $loop_count"
1542
+ log_status "INFO" " - API calls used: $(cat "$CALL_COUNT_FILE")"
1543
+ log_status "INFO" " - Exit reason: $exit_reason"
1544
+
1545
+ break
1546
+ fi
1547
+
1548
+ # Update status
1549
+ local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
1550
+ update_status "$loop_count" "$calls_made" "executing" "running"
1551
+
1552
+ # Execute Claude Code
1553
+ execute_claude_code "$loop_count"
1554
+ local exec_result=$?
1555
+
1556
+ if [ $exec_result -eq 0 ]; then
1557
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "completed" "success"
1558
+
1559
+ # Brief pause between successful executions
1560
+ sleep 5
1561
+ elif [ $exec_result -eq 3 ]; then
1562
+ # Circuit breaker opened
1563
+ reset_session "circuit_breaker_trip"
1564
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "circuit_breaker_open" "halted" "stagnation_detected"
1565
+ log_status "ERROR" "🛑 Circuit breaker has opened - halting loop"
1566
+ log_status "INFO" "Run 'ralph --reset-circuit' to reset the circuit breaker after addressing issues"
1567
+ break
1568
+ elif [ $exec_result -eq 2 ]; then
1569
+ # API 5-hour limit reached - handle specially
1570
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "api_limit" "paused"
1571
+ log_status "WARN" "🛑 Claude API 5-hour limit reached!"
1572
+
1573
+ # Ask user whether to wait or exit
1574
+ echo -e "\n${YELLOW}The Claude API 5-hour usage limit has been reached.${NC}"
1575
+ echo -e "${YELLOW}You can either:${NC}"
1576
+ echo -e " ${GREEN}1)${NC} Wait for the limit to reset (usually within an hour)"
1577
+ echo -e " ${GREEN}2)${NC} Exit the loop and try again later"
1578
+ echo -e "\n${BLUE}Choose an option (1 or 2):${NC} "
1579
+
1580
+ # Read user input with timeout
1581
+ read -t 30 -n 1 user_choice
1582
+ echo # New line after input
1583
+
1584
+ if [[ "$user_choice" == "2" ]] || [[ -z "$user_choice" ]]; then
1585
+ log_status "INFO" "User chose to exit (or timed out). Exiting loop..."
1586
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "api_limit_exit" "stopped" "api_5hour_limit"
1587
+ break
1588
+ else
1589
+ log_status "INFO" "User chose to wait. Waiting for API limit reset..."
1590
+ # Wait for longer period when API limit is hit
1591
+ local wait_minutes=60
1592
+ log_status "INFO" "Waiting $wait_minutes minutes before retrying..."
1593
+
1594
+ # Countdown display
1595
+ local wait_seconds=$((wait_minutes * 60))
1596
+ while [[ $wait_seconds -gt 0 ]]; do
1597
+ local minutes=$((wait_seconds / 60))
1598
+ local seconds=$((wait_seconds % 60))
1599
+ printf "\r${YELLOW}Time until retry: %02d:%02d${NC}" $minutes $seconds
1600
+ sleep 1
1601
+ ((wait_seconds--))
1602
+ done
1603
+ printf "\n"
1604
+ fi
1605
+ else
1606
+ update_status "$loop_count" "$(cat "$CALL_COUNT_FILE")" "failed" "error"
1607
+ log_status "WARN" "Execution failed, waiting 30 seconds before retry..."
1608
+ sleep 30
1609
+ fi
1610
+
1611
+ log_status "LOOP" "=== Completed Loop #$loop_count ==="
1612
+ done
1613
+ }
1614
+
1615
+ # Help function
1616
+ show_help() {
1617
+ cat << HELPEOF
1618
+ Ralph Loop for Claude Code
1619
+
1620
+ Usage: $0 [OPTIONS]
1621
+
1622
+ IMPORTANT: This command must be run from a Ralph project directory.
1623
+ Use 'ralph-setup project-name' to create a new project first.
1624
+
1625
+ Options:
1626
+ -h, --help Show this help message
1627
+ -c, --calls NUM Set max calls per hour (default: $MAX_CALLS_PER_HOUR)
1628
+ -p, --prompt FILE Set prompt file (default: $PROMPT_FILE)
1629
+ -s, --status Show current status and exit
1630
+ -m, --monitor Start with tmux session and live monitor (requires tmux)
1631
+ -v, --verbose Show detailed progress updates during execution
1632
+ -l, --live Show Claude Code output in real-time (auto-switches to JSON output)
1633
+ -t, --timeout MIN Set Claude Code execution timeout in minutes (default: $CLAUDE_TIMEOUT_MINUTES)
1634
+ --reset-circuit Reset circuit breaker to CLOSED state
1635
+ --circuit-status Show circuit breaker status and exit
1636
+ --auto-reset-circuit Auto-reset circuit breaker on startup (bypasses cooldown)
1637
+ --reset-session Reset session state and exit (clears session continuity)
1638
+
1639
+ Modern CLI Options (Phase 1.1):
1640
+ --output-format FORMAT Set Claude output format: json or text (default: $CLAUDE_OUTPUT_FORMAT)
1641
+ Note: --live mode requires JSON and will auto-switch
1642
+ --allowed-tools TOOLS Comma-separated list of allowed tools (default: $CLAUDE_ALLOWED_TOOLS)
1643
+ --no-continue Disable session continuity across loops
1644
+ --session-expiry HOURS Set session expiration time in hours (default: $CLAUDE_SESSION_EXPIRY_HOURS)
1645
+
1646
+ Files created:
1647
+ - $LOG_DIR/: All execution logs
1648
+ - $DOCS_DIR/: Generated documentation
1649
+ - $STATUS_FILE: Current status (JSON)
1650
+ - .ralph/.ralph_session: Session lifecycle tracking
1651
+ - .ralph/.ralph_session_history: Session transition history (last 50)
1652
+ - .ralph/.call_count: API call counter for rate limiting
1653
+ - .ralph/.last_reset: Timestamp of last rate limit reset
1654
+
1655
+ Example workflow:
1656
+ ralph-setup my-project # Create project
1657
+ cd my-project # Enter project directory
1658
+ $0 --monitor # Start Ralph with monitoring
1659
+
1660
+ Examples:
1661
+ $0 --calls 50 --prompt my_prompt.md
1662
+ $0 --monitor # Start with integrated tmux monitoring
1663
+ $0 --live # Show Claude Code output in real-time (streaming)
1664
+ $0 --live --verbose # Live streaming + verbose logging
1665
+ $0 --monitor --timeout 30 # 30-minute timeout for complex tasks
1666
+ $0 --verbose --timeout 5 # 5-minute timeout with detailed progress
1667
+ $0 --output-format text # Use legacy text output format
1668
+ $0 --no-continue # Disable session continuity
1669
+ $0 --session-expiry 48 # 48-hour session expiration
1670
+
1671
+ HELPEOF
1672
+ }
1673
+
1674
+ # Parse command line arguments
1675
+ while [[ $# -gt 0 ]]; do
1676
+ case $1 in
1677
+ -h|--help)
1678
+ show_help
1679
+ exit 0
1680
+ ;;
1681
+ -c|--calls)
1682
+ MAX_CALLS_PER_HOUR="$2"
1683
+ shift 2
1684
+ ;;
1685
+ -p|--prompt)
1686
+ PROMPT_FILE="$2"
1687
+ shift 2
1688
+ ;;
1689
+ -s|--status)
1690
+ if [[ -f "$STATUS_FILE" ]]; then
1691
+ echo "Current Status:"
1692
+ cat "$STATUS_FILE" | jq . 2>/dev/null || cat "$STATUS_FILE"
1693
+ else
1694
+ echo "No status file found. Ralph may not be running."
1695
+ fi
1696
+ exit 0
1697
+ ;;
1698
+ -m|--monitor)
1699
+ USE_TMUX=true
1700
+ shift
1701
+ ;;
1702
+ -v|--verbose)
1703
+ VERBOSE_PROGRESS=true
1704
+ shift
1705
+ ;;
1706
+ -l|--live)
1707
+ LIVE_OUTPUT=true
1708
+ shift
1709
+ ;;
1710
+ -t|--timeout)
1711
+ if [[ "$2" =~ ^[1-9][0-9]*$ ]] && [[ "$2" -le 120 ]]; then
1712
+ CLAUDE_TIMEOUT_MINUTES="$2"
1713
+ else
1714
+ echo "Error: Timeout must be a positive integer between 1 and 120 minutes"
1715
+ exit 1
1716
+ fi
1717
+ shift 2
1718
+ ;;
1719
+ --reset-circuit)
1720
+ # Source the circuit breaker library
1721
+ SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
1722
+ source "$SCRIPT_DIR/lib/circuit_breaker.sh"
1723
+ source "$SCRIPT_DIR/lib/date_utils.sh"
1724
+ reset_circuit_breaker "Manual reset via command line"
1725
+ reset_session "manual_circuit_reset"
1726
+ exit 0
1727
+ ;;
1728
+ --reset-session)
1729
+ # Reset session state only
1730
+ SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
1731
+ source "$SCRIPT_DIR/lib/date_utils.sh"
1732
+ reset_session "manual_reset_flag"
1733
+ echo -e "\033[0;32m✅ Session state reset successfully\033[0m"
1734
+ exit 0
1735
+ ;;
1736
+ --circuit-status)
1737
+ # Source the circuit breaker library
1738
+ SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
1739
+ source "$SCRIPT_DIR/lib/circuit_breaker.sh"
1740
+ show_circuit_status
1741
+ exit 0
1742
+ ;;
1743
+ --output-format)
1744
+ if [[ "$2" == "json" || "$2" == "text" ]]; then
1745
+ CLAUDE_OUTPUT_FORMAT="$2"
1746
+ else
1747
+ echo "Error: --output-format must be 'json' or 'text'"
1748
+ exit 1
1749
+ fi
1750
+ shift 2
1751
+ ;;
1752
+ --allowed-tools)
1753
+ if ! validate_allowed_tools "$2"; then
1754
+ exit 1
1755
+ fi
1756
+ CLAUDE_ALLOWED_TOOLS="$2"
1757
+ shift 2
1758
+ ;;
1759
+ --no-continue)
1760
+ CLAUDE_USE_CONTINUE=false
1761
+ shift
1762
+ ;;
1763
+ --session-expiry)
1764
+ if [[ -z "$2" || ! "$2" =~ ^[1-9][0-9]*$ ]]; then
1765
+ echo "Error: --session-expiry requires a positive integer (hours)"
1766
+ exit 1
1767
+ fi
1768
+ CLAUDE_SESSION_EXPIRY_HOURS="$2"
1769
+ shift 2
1770
+ ;;
1771
+ --auto-reset-circuit)
1772
+ CB_AUTO_RESET=true
1773
+ shift
1774
+ ;;
1775
+ *)
1776
+ echo "Unknown option: $1"
1777
+ show_help
1778
+ exit 1
1779
+ ;;
1780
+ esac
1781
+ done
1782
+
1783
+ # Only execute when run directly, not when sourced
1784
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
1785
+ # If tmux mode requested, set it up
1786
+ if [[ "$USE_TMUX" == "true" ]]; then
1787
+ check_tmux_available
1788
+ setup_tmux_session
1789
+ fi
1790
+
1791
+ # Start the main loop
1792
+ main
1793
+ fi