aicodeman 0.2.9 → 0.3.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 (374) hide show
  1. package/README.md +118 -4
  2. package/dist/ai-idle-checker.d.ts.map +1 -1
  3. package/dist/ai-idle-checker.js +3 -2
  4. package/dist/ai-idle-checker.js.map +1 -1
  5. package/dist/ai-plan-checker.d.ts.map +1 -1
  6. package/dist/ai-plan-checker.js +3 -2
  7. package/dist/ai-plan-checker.js.map +1 -1
  8. package/dist/bash-tool-parser.d.ts +2 -3
  9. package/dist/bash-tool-parser.d.ts.map +1 -1
  10. package/dist/bash-tool-parser.js +14 -31
  11. package/dist/bash-tool-parser.js.map +1 -1
  12. package/dist/config/ai-defaults.d.ts +16 -0
  13. package/dist/config/ai-defaults.d.ts.map +1 -0
  14. package/dist/config/ai-defaults.js +16 -0
  15. package/dist/config/ai-defaults.js.map +1 -0
  16. package/dist/config/auth-config.d.ts +19 -0
  17. package/dist/config/auth-config.d.ts.map +1 -0
  18. package/dist/config/auth-config.js +28 -0
  19. package/dist/config/auth-config.js.map +1 -0
  20. package/dist/config/exec-timeout.d.ts +10 -0
  21. package/dist/config/exec-timeout.d.ts.map +1 -0
  22. package/dist/config/exec-timeout.js +10 -0
  23. package/dist/config/exec-timeout.js.map +1 -0
  24. package/dist/config/map-limits.d.ts +4 -0
  25. package/dist/config/map-limits.d.ts.map +1 -1
  26. package/dist/config/map-limits.js +7 -0
  27. package/dist/config/map-limits.js.map +1 -1
  28. package/dist/config/server-timing.d.ts +42 -0
  29. package/dist/config/server-timing.d.ts.map +1 -0
  30. package/dist/config/server-timing.js +57 -0
  31. package/dist/config/server-timing.js.map +1 -0
  32. package/dist/config/team-config.d.ts +16 -0
  33. package/dist/config/team-config.d.ts.map +1 -0
  34. package/dist/config/team-config.js +16 -0
  35. package/dist/config/team-config.js.map +1 -0
  36. package/dist/config/terminal-limits.d.ts +18 -0
  37. package/dist/config/terminal-limits.d.ts.map +1 -0
  38. package/dist/config/terminal-limits.js +18 -0
  39. package/dist/config/terminal-limits.js.map +1 -0
  40. package/dist/config/tunnel-config.d.ts +27 -0
  41. package/dist/config/tunnel-config.d.ts.map +1 -0
  42. package/dist/config/tunnel-config.js +36 -0
  43. package/dist/config/tunnel-config.js.map +1 -0
  44. package/dist/hooks-config.d.ts +21 -6
  45. package/dist/hooks-config.d.ts.map +1 -1
  46. package/dist/hooks-config.js +28 -12
  47. package/dist/hooks-config.js.map +1 -1
  48. package/dist/image-watcher.d.ts +4 -4
  49. package/dist/image-watcher.d.ts.map +1 -1
  50. package/dist/image-watcher.js +17 -30
  51. package/dist/image-watcher.js.map +1 -1
  52. package/dist/index.js +1 -2
  53. package/dist/index.js.map +1 -1
  54. package/dist/plan-orchestrator.d.ts +2 -24
  55. package/dist/plan-orchestrator.d.ts.map +1 -1
  56. package/dist/plan-orchestrator.js.map +1 -1
  57. package/dist/prompts/planner.d.ts +7 -8
  58. package/dist/prompts/planner.d.ts.map +1 -1
  59. package/dist/prompts/planner.js +7 -8
  60. package/dist/prompts/planner.js.map +1 -1
  61. package/dist/prompts/research-agent.d.ts +6 -4
  62. package/dist/prompts/research-agent.d.ts.map +1 -1
  63. package/dist/prompts/research-agent.js +6 -4
  64. package/dist/prompts/research-agent.js.map +1 -1
  65. package/dist/push-store.d.ts +1 -1
  66. package/dist/push-store.d.ts.map +1 -1
  67. package/dist/push-store.js +4 -12
  68. package/dist/push-store.js.map +1 -1
  69. package/dist/ralph-fix-plan-watcher.d.ts +91 -0
  70. package/dist/ralph-fix-plan-watcher.d.ts.map +1 -0
  71. package/dist/ralph-fix-plan-watcher.js +326 -0
  72. package/dist/ralph-fix-plan-watcher.js.map +1 -0
  73. package/dist/ralph-loop.d.ts +14 -4
  74. package/dist/ralph-loop.d.ts.map +1 -1
  75. package/dist/ralph-loop.js +14 -4
  76. package/dist/ralph-loop.js.map +1 -1
  77. package/dist/ralph-plan-tracker.d.ts +201 -0
  78. package/dist/ralph-plan-tracker.d.ts.map +1 -0
  79. package/dist/ralph-plan-tracker.js +325 -0
  80. package/dist/ralph-plan-tracker.js.map +1 -0
  81. package/dist/ralph-stall-detector.d.ts +84 -0
  82. package/dist/ralph-stall-detector.d.ts.map +1 -0
  83. package/dist/ralph-stall-detector.js +139 -0
  84. package/dist/ralph-stall-detector.js.map +1 -0
  85. package/dist/ralph-status-parser.d.ts +141 -0
  86. package/dist/ralph-status-parser.d.ts.map +1 -0
  87. package/dist/ralph-status-parser.js +478 -0
  88. package/dist/ralph-status-parser.js.map +1 -0
  89. package/dist/ralph-tracker.d.ts +218 -692
  90. package/dist/ralph-tracker.d.ts.map +1 -1
  91. package/dist/ralph-tracker.js +389 -1723
  92. package/dist/ralph-tracker.js.map +1 -1
  93. package/dist/respawn-adaptive-timing.d.ts +61 -0
  94. package/dist/respawn-adaptive-timing.d.ts.map +1 -0
  95. package/dist/respawn-adaptive-timing.js +105 -0
  96. package/dist/respawn-adaptive-timing.js.map +1 -0
  97. package/dist/respawn-controller.d.ts +35 -115
  98. package/dist/respawn-controller.d.ts.map +1 -1
  99. package/dist/respawn-controller.js +167 -607
  100. package/dist/respawn-controller.js.map +1 -1
  101. package/dist/respawn-health.d.ts +54 -0
  102. package/dist/respawn-health.d.ts.map +1 -0
  103. package/dist/respawn-health.js +183 -0
  104. package/dist/respawn-health.js.map +1 -0
  105. package/dist/respawn-metrics.d.ts +81 -0
  106. package/dist/respawn-metrics.d.ts.map +1 -0
  107. package/dist/respawn-metrics.js +198 -0
  108. package/dist/respawn-metrics.js.map +1 -0
  109. package/dist/respawn-patterns.d.ts +45 -0
  110. package/dist/respawn-patterns.d.ts.map +1 -0
  111. package/dist/respawn-patterns.js +125 -0
  112. package/dist/respawn-patterns.js.map +1 -0
  113. package/dist/session-auto-ops.d.ts +89 -0
  114. package/dist/session-auto-ops.d.ts.map +1 -0
  115. package/dist/session-auto-ops.js +224 -0
  116. package/dist/session-auto-ops.js.map +1 -0
  117. package/dist/session-cli-builder.d.ts +62 -0
  118. package/dist/session-cli-builder.d.ts.map +1 -0
  119. package/dist/session-cli-builder.js +121 -0
  120. package/dist/session-cli-builder.js.map +1 -0
  121. package/dist/session-manager.d.ts +17 -5
  122. package/dist/session-manager.d.ts.map +1 -1
  123. package/dist/session-manager.js +17 -5
  124. package/dist/session-manager.js.map +1 -1
  125. package/dist/session-task-cache.d.ts +52 -0
  126. package/dist/session-task-cache.d.ts.map +1 -0
  127. package/dist/session-task-cache.js +90 -0
  128. package/dist/session-task-cache.js.map +1 -0
  129. package/dist/session.d.ts +23 -41
  130. package/dist/session.d.ts.map +1 -1
  131. package/dist/session.js +79 -317
  132. package/dist/session.js.map +1 -1
  133. package/dist/state-store.d.ts +19 -9
  134. package/dist/state-store.d.ts.map +1 -1
  135. package/dist/state-store.js +29 -30
  136. package/dist/state-store.js.map +1 -1
  137. package/dist/subagent-watcher.d.ts +26 -7
  138. package/dist/subagent-watcher.d.ts.map +1 -1
  139. package/dist/subagent-watcher.js +47 -64
  140. package/dist/subagent-watcher.js.map +1 -1
  141. package/dist/team-watcher.d.ts.map +1 -1
  142. package/dist/team-watcher.js +2 -5
  143. package/dist/team-watcher.js.map +1 -1
  144. package/dist/tmux-manager.d.ts.map +1 -1
  145. package/dist/tmux-manager.js +1 -2
  146. package/dist/tmux-manager.js.map +1 -1
  147. package/dist/tunnel-manager.d.ts +26 -0
  148. package/dist/tunnel-manager.d.ts.map +1 -1
  149. package/dist/tunnel-manager.js +126 -7
  150. package/dist/tunnel-manager.js.map +1 -1
  151. package/dist/types/api.d.ts +108 -0
  152. package/dist/types/api.d.ts.map +1 -0
  153. package/dist/types/api.js +98 -0
  154. package/dist/types/api.js.map +1 -0
  155. package/dist/types/app-state.d.ts +117 -0
  156. package/dist/types/app-state.d.ts.map +1 -0
  157. package/dist/types/app-state.js +76 -0
  158. package/dist/types/app-state.js.map +1 -0
  159. package/dist/types/common.d.ts +79 -0
  160. package/dist/types/common.d.ts.map +1 -0
  161. package/dist/types/common.js +17 -0
  162. package/dist/types/common.js.map +1 -0
  163. package/dist/types/index.d.ts +66 -0
  164. package/dist/types/index.d.ts.map +1 -0
  165. package/dist/types/index.js +66 -0
  166. package/dist/types/index.js.map +1 -0
  167. package/dist/types/lifecycle.d.ts +28 -0
  168. package/dist/types/lifecycle.d.ts.map +1 -0
  169. package/dist/types/lifecycle.js +16 -0
  170. package/dist/types/lifecycle.js.map +1 -0
  171. package/dist/types/plan.d.ts +45 -0
  172. package/dist/types/plan.d.ts.map +1 -0
  173. package/dist/types/plan.js +18 -0
  174. package/dist/types/plan.js.map +1 -0
  175. package/dist/types/push.d.ts +36 -0
  176. package/dist/types/push.d.ts.map +1 -0
  177. package/dist/types/push.js +18 -0
  178. package/dist/types/push.js.map +1 -0
  179. package/dist/types/ralph.d.ts +262 -0
  180. package/dist/types/ralph.d.ts.map +1 -0
  181. package/dist/types/ralph.js +70 -0
  182. package/dist/types/ralph.js.map +1 -0
  183. package/dist/types/respawn.d.ts +271 -0
  184. package/dist/types/respawn.d.ts.map +1 -0
  185. package/dist/types/respawn.js +26 -0
  186. package/dist/types/respawn.js.map +1 -0
  187. package/dist/types/run-summary.d.ts +96 -0
  188. package/dist/types/run-summary.d.ts.map +1 -0
  189. package/dist/types/run-summary.js +37 -0
  190. package/dist/types/run-summary.js.map +1 -0
  191. package/dist/types/session.d.ts +152 -0
  192. package/dist/types/session.d.ts.map +1 -0
  193. package/dist/types/session.js +27 -0
  194. package/dist/types/session.js.map +1 -0
  195. package/dist/types/task.d.ts +72 -0
  196. package/dist/types/task.d.ts.map +1 -0
  197. package/dist/types/task.js +19 -0
  198. package/dist/types/task.js.map +1 -0
  199. package/dist/types/teams.d.ts +73 -0
  200. package/dist/types/teams.d.ts.map +1 -0
  201. package/dist/types/teams.js +23 -0
  202. package/dist/types/teams.js.map +1 -0
  203. package/dist/types/tools.d.ts +61 -0
  204. package/dist/types/tools.d.ts.map +1 -0
  205. package/dist/types/tools.js +20 -0
  206. package/dist/types/tools.js.map +1 -0
  207. package/dist/types.d.ts +8 -1134
  208. package/dist/types.d.ts.map +1 -1
  209. package/dist/types.js +8 -210
  210. package/dist/types.js.map +1 -1
  211. package/dist/utils/claude-cli-resolver.d.ts.map +1 -1
  212. package/dist/utils/claude-cli-resolver.js +1 -2
  213. package/dist/utils/claude-cli-resolver.js.map +1 -1
  214. package/dist/utils/debouncer.d.ts +111 -0
  215. package/dist/utils/debouncer.d.ts.map +1 -0
  216. package/dist/utils/debouncer.js +162 -0
  217. package/dist/utils/debouncer.js.map +1 -0
  218. package/dist/utils/index.d.ts +3 -2
  219. package/dist/utils/index.d.ts.map +1 -1
  220. package/dist/utils/index.js +3 -2
  221. package/dist/utils/index.js.map +1 -1
  222. package/dist/utils/opencode-cli-resolver.d.ts.map +1 -1
  223. package/dist/utils/opencode-cli-resolver.js +1 -2
  224. package/dist/utils/opencode-cli-resolver.js.map +1 -1
  225. package/dist/utils/string-similarity.d.ts +0 -57
  226. package/dist/utils/string-similarity.d.ts.map +1 -1
  227. package/dist/utils/string-similarity.js +3 -18
  228. package/dist/utils/string-similarity.js.map +1 -1
  229. package/dist/web/middleware/auth.d.ts +31 -0
  230. package/dist/web/middleware/auth.d.ts.map +1 -0
  231. package/dist/web/middleware/auth.js +154 -0
  232. package/dist/web/middleware/auth.js.map +1 -0
  233. package/dist/web/ports/auth-port.d.ts +18 -0
  234. package/dist/web/ports/auth-port.d.ts.map +1 -0
  235. package/dist/web/ports/auth-port.js +6 -0
  236. package/dist/web/ports/auth-port.js.map +1 -0
  237. package/dist/web/ports/config-port.d.ts +28 -0
  238. package/dist/web/ports/config-port.d.ts.map +1 -0
  239. package/dist/web/ports/config-port.js +6 -0
  240. package/dist/web/ports/config-port.js.map +1 -0
  241. package/dist/web/ports/event-port.d.ts +13 -0
  242. package/dist/web/ports/event-port.d.ts.map +1 -0
  243. package/dist/web/ports/event-port.js +6 -0
  244. package/dist/web/ports/event-port.js.map +1 -0
  245. package/dist/web/ports/index.d.ts +14 -0
  246. package/dist/web/ports/index.d.ts.map +1 -0
  247. package/dist/web/ports/index.js +9 -0
  248. package/dist/web/ports/index.js.map +1 -0
  249. package/dist/web/ports/infra-port.d.ts +36 -0
  250. package/dist/web/ports/infra-port.d.ts.map +1 -0
  251. package/dist/web/ports/infra-port.js +6 -0
  252. package/dist/web/ports/infra-port.js.map +1 -0
  253. package/dist/web/ports/respawn-port.d.ts +20 -0
  254. package/dist/web/ports/respawn-port.d.ts.map +1 -0
  255. package/dist/web/ports/respawn-port.js +6 -0
  256. package/dist/web/ports/respawn-port.js.map +1 -0
  257. package/dist/web/ports/session-port.d.ts +15 -0
  258. package/dist/web/ports/session-port.d.ts.map +1 -0
  259. package/dist/web/ports/session-port.js +6 -0
  260. package/dist/web/ports/session-port.js.map +1 -0
  261. package/dist/web/public/api-client.js +82 -0
  262. package/dist/web/public/api-client.js.br +0 -0
  263. package/dist/web/public/api-client.js.gz +0 -0
  264. package/dist/web/public/app.js +117 -201
  265. package/dist/web/public/app.js.br +0 -0
  266. package/dist/web/public/app.js.gz +0 -0
  267. package/dist/web/public/constants.js +365 -0
  268. package/dist/web/public/constants.js.br +0 -0
  269. package/dist/web/public/constants.js.gz +0 -0
  270. package/dist/web/public/index.html +15 -3
  271. package/dist/web/public/index.html.br +0 -0
  272. package/dist/web/public/index.html.gz +0 -0
  273. package/dist/web/public/keyboard-accessory.js +302 -0
  274. package/dist/web/public/keyboard-accessory.js.br +0 -0
  275. package/dist/web/public/keyboard-accessory.js.gz +0 -0
  276. package/dist/web/public/mobile-handlers.js +491 -0
  277. package/dist/web/public/mobile-handlers.js.br +0 -0
  278. package/dist/web/public/mobile-handlers.js.gz +0 -0
  279. package/dist/web/public/mobile.css.gz +0 -0
  280. package/dist/web/public/notification-manager.js +472 -0
  281. package/dist/web/public/notification-manager.js.br +0 -0
  282. package/dist/web/public/notification-manager.js.gz +0 -0
  283. package/dist/web/public/ralph-wizard.js +33 -9
  284. package/dist/web/public/ralph-wizard.js.br +0 -0
  285. package/dist/web/public/ralph-wizard.js.gz +0 -0
  286. package/dist/web/public/styles.css.gz +0 -0
  287. package/dist/web/public/subagent-windows.js +1149 -0
  288. package/dist/web/public/subagent-windows.js.br +0 -0
  289. package/dist/web/public/subagent-windows.js.gz +0 -0
  290. package/dist/web/public/sw.js +15 -0
  291. package/dist/web/public/sw.js.br +0 -0
  292. package/dist/web/public/sw.js.gz +0 -0
  293. package/dist/web/public/upload.html.gz +0 -0
  294. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  295. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  296. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  297. package/dist/web/public/vendor/xterm-zerolag-input.js +4 -0
  298. package/dist/web/public/vendor/xterm-zerolag-input.js.br +0 -0
  299. package/dist/web/public/vendor/xterm-zerolag-input.js.gz +0 -0
  300. package/dist/web/public/vendor/xterm.css.gz +0 -0
  301. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  302. package/dist/web/public/voice-input.js +882 -0
  303. package/dist/web/public/voice-input.js.br +0 -0
  304. package/dist/web/public/voice-input.js.gz +0 -0
  305. package/dist/web/route-helpers.d.ts +38 -0
  306. package/dist/web/route-helpers.d.ts.map +1 -0
  307. package/dist/web/route-helpers.js +144 -0
  308. package/dist/web/route-helpers.js.map +1 -0
  309. package/dist/web/routes/case-routes.d.ts +9 -0
  310. package/dist/web/routes/case-routes.d.ts.map +1 -0
  311. package/dist/web/routes/case-routes.js +426 -0
  312. package/dist/web/routes/case-routes.js.map +1 -0
  313. package/dist/web/routes/file-routes.d.ts +8 -0
  314. package/dist/web/routes/file-routes.d.ts.map +1 -0
  315. package/dist/web/routes/file-routes.js +337 -0
  316. package/dist/web/routes/file-routes.js.map +1 -0
  317. package/dist/web/routes/hook-event-routes.d.ts +9 -0
  318. package/dist/web/routes/hook-event-routes.d.ts.map +1 -0
  319. package/dist/web/routes/hook-event-routes.js +57 -0
  320. package/dist/web/routes/hook-event-routes.js.map +1 -0
  321. package/dist/web/routes/index.d.ts +16 -0
  322. package/dist/web/routes/index.d.ts.map +1 -0
  323. package/dist/web/routes/index.js +16 -0
  324. package/dist/web/routes/index.js.map +1 -0
  325. package/dist/web/routes/mux-routes.d.ts +8 -0
  326. package/dist/web/routes/mux-routes.d.ts.map +1 -0
  327. package/dist/web/routes/mux-routes.js +32 -0
  328. package/dist/web/routes/mux-routes.js.map +1 -0
  329. package/dist/web/routes/plan-routes.d.ts +9 -0
  330. package/dist/web/routes/plan-routes.d.ts.map +1 -0
  331. package/dist/web/routes/plan-routes.js +385 -0
  332. package/dist/web/routes/plan-routes.js.map +1 -0
  333. package/dist/web/routes/push-routes.d.ts +8 -0
  334. package/dist/web/routes/push-routes.d.ts.map +1 -0
  335. package/dist/web/routes/push-routes.js +49 -0
  336. package/dist/web/routes/push-routes.js.map +1 -0
  337. package/dist/web/routes/ralph-routes.d.ts +9 -0
  338. package/dist/web/routes/ralph-routes.d.ts.map +1 -0
  339. package/dist/web/routes/ralph-routes.js +485 -0
  340. package/dist/web/routes/ralph-routes.js.map +1 -0
  341. package/dist/web/routes/respawn-routes.d.ts +8 -0
  342. package/dist/web/routes/respawn-routes.d.ts.map +1 -0
  343. package/dist/web/routes/respawn-routes.js +270 -0
  344. package/dist/web/routes/respawn-routes.js.map +1 -0
  345. package/dist/web/routes/scheduled-routes.d.ts +8 -0
  346. package/dist/web/routes/scheduled-routes.d.ts.map +1 -0
  347. package/dist/web/routes/scheduled-routes.js +51 -0
  348. package/dist/web/routes/scheduled-routes.js.map +1 -0
  349. package/dist/web/routes/session-routes.d.ts +9 -0
  350. package/dist/web/routes/session-routes.d.ts.map +1 -0
  351. package/dist/web/routes/session-routes.js +751 -0
  352. package/dist/web/routes/session-routes.js.map +1 -0
  353. package/dist/web/routes/system-routes.d.ts +9 -0
  354. package/dist/web/routes/system-routes.d.ts.map +1 -0
  355. package/dist/web/routes/system-routes.js +699 -0
  356. package/dist/web/routes/system-routes.js.map +1 -0
  357. package/dist/web/routes/team-routes.d.ts +8 -0
  358. package/dist/web/routes/team-routes.d.ts.map +1 -0
  359. package/dist/web/routes/team-routes.js +14 -0
  360. package/dist/web/routes/team-routes.js.map +1 -0
  361. package/dist/web/schemas.d.ts +43 -3
  362. package/dist/web/schemas.d.ts.map +1 -1
  363. package/dist/web/schemas.js +6 -2
  364. package/dist/web/schemas.js.map +1 -1
  365. package/dist/web/server.d.ts +35 -15
  366. package/dist/web/server.d.ts.map +1 -1
  367. package/dist/web/server.js +563 -3971
  368. package/dist/web/server.js.map +1 -1
  369. package/dist/web/sse-events.d.ts +361 -0
  370. package/dist/web/sse-events.d.ts.map +1 -0
  371. package/dist/web/sse-events.js +396 -0
  372. package/dist/web/sse-events.js.map +1 -0
  373. package/package.json +2 -1
  374. package/scripts/postinstall.js +58 -0
@@ -1,25 +1,47 @@
1
1
  /**
2
- * @fileoverview Ralph Tracker - Detects Ralph Wiggum loops, todos, and completion phrases
2
+ * @fileoverview Ralph Tracker - Detects Ralph Wiggum loops, todos, and completion phrases.
3
3
  *
4
- * This module parses terminal output from Claude Code sessions to detect:
5
- * - Ralph Wiggum loop state (active, completion phrase, iteration count)
6
- * - Todo list items from the TodoWrite tool
4
+ * Parses terminal output from Claude Code sessions to detect:
5
+ * - Ralph loop state (active, completion phrase, iteration count)
6
+ * - Todo items from the TodoWrite tool (with deduplication and expiry)
7
7
  * - Completion phrases signaling loop completion
8
+ * - Circuit breaker state (CLOSED/HALF_OPEN/OPEN)
8
9
  *
9
- * The tracker is DISABLED by default and auto-enables when Ralph-related
10
- * patterns are detected in the output stream, reducing overhead for
11
- * sessions not using autonomous loops.
10
+ * DISABLED by default auto-enables when Ralph-related patterns appear,
11
+ * reducing overhead for non-autonomous sessions.
12
+ *
13
+ * Composed of four sub-modules:
14
+ * - `RalphPlanTracker`: Plan task management, checkpoints, versioning
15
+ * - `RalphFixPlanWatcher`: @fix_plan.md file watching and parsing
16
+ * - `RalphStallDetector`: Iteration stall detection
17
+ * - `RalphStatusParser`: RALPH_STATUS block parsing, circuit breaker
18
+ *
19
+ * Key exports:
20
+ * - `RalphTracker` class — main tracker, extends EventEmitter
21
+ * - `RalphTrackerEvents` interface — typed event map
22
+ * - Re-exports: `EnhancedPlanTask`, `CheckpointReview` from ralph-plan-tracker
23
+ *
24
+ * Key methods: `processData(data)` — feed terminal output, `getState()`,
25
+ * `getTodos()`, `getCompletionHistory()`, `getPlanTasks()`, `reset()`
26
+ *
27
+ * @dependencies types (RalphTrackerState, RalphTodoItem, CircuitBreakerStatus),
28
+ * ralph-plan-tracker, ralph-fix-plan-watcher, ralph-stall-detector, ralph-status-parser,
29
+ * config/buffer-limits, config/map-limits
30
+ * @consumedby session (owns one RalphTracker per session), web/server (SSE events)
31
+ * @emits ralphStateChanged, todoUpdated, completionDetected, statusBlockParsed,
32
+ * circuitBreakerChanged, exitGateMet, planTaskUpdated, planCheckpoint
12
33
  *
13
34
  * @module ralph-tracker
14
35
  */
15
36
  import { EventEmitter } from 'node:events';
16
- import { readFile } from 'node:fs/promises';
17
- import { existsSync, watch as fsWatch } from 'node:fs';
18
- import { join } from 'node:path';
19
- import { createInitialRalphTrackerState, createInitialCircuitBreakerStatus, } from './types.js';
20
- import { ANSI_ESCAPE_PATTERN_SIMPLE, fuzzyPhraseMatch, todoContentHash, stringSimilarity } from './utils/index.js';
37
+ import { createInitialRalphTrackerState, } from './types.js';
38
+ import { ANSI_ESCAPE_PATTERN_SIMPLE, fuzzyPhraseMatch, todoContentHash, stringSimilarity, Debouncer, CleanupManager, } from './utils/index.js';
21
39
  import { MAX_LINE_BUFFER_SIZE } from './config/buffer-limits.js';
22
40
  import { MAX_TODOS_PER_SESSION } from './config/map-limits.js';
41
+ import { RalphPlanTracker } from './ralph-plan-tracker.js';
42
+ import { RalphFixPlanWatcher, generateFixPlanMarkdown, importFixPlanMarkdown } from './ralph-fix-plan-watcher.js';
43
+ import { RalphStallDetector } from './ralph-stall-detector.js';
44
+ import { RalphStatusParser } from './ralph-status-parser.js';
23
45
  // ========== Configuration Constants ==========
24
46
  // Note: MAX_TODOS_PER_SESSION and MAX_LINE_BUFFER_SIZE are imported from config modules
25
47
  /**
@@ -28,11 +50,17 @@ import { MAX_TODOS_PER_SESSION } from './config/map-limits.js';
28
50
  */
29
51
  const TODO_EXPIRY_MS = 60 * 60 * 1000;
30
52
  /**
31
- * Minimum interval between cleanup checks (in milliseconds).
53
+ * Minimum interval between on-demand cleanup checks (in milliseconds).
32
54
  * Prevents running cleanup on every data chunk.
33
55
  * Default: 30 seconds
34
56
  */
35
57
  const CLEANUP_THROTTLE_MS = 30 * 1000;
58
+ /**
59
+ * Interval for periodic todo expiry cleanup (in milliseconds).
60
+ * Actively purges expired todos even when no terminal data is flowing.
61
+ * Default: 5 minutes
62
+ */
63
+ const TODO_CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
36
64
  /**
37
65
  * Similarity threshold for todo deduplication.
38
66
  * Todos with similarity >= this value are considered duplicates.
@@ -51,7 +79,6 @@ const EVENT_DEBOUNCE_MS = 50;
51
79
  * Prevents unbounded growth if many unique phrases are seen.
52
80
  */
53
81
  const MAX_COMPLETION_PHRASE_ENTRIES = 50;
54
- const MAX_PLAN_HISTORY = 10;
55
82
  /**
56
83
  * Common/generic completion phrases that may cause false positives.
57
84
  * These phrases are likely to appear in Claude's natural output,
@@ -226,66 +253,6 @@ const TASK_DONE_PATTERN = /(?:task|item|todo)\s*(?:#?\d+|"\s*[^"]+\s*")?\s*(?:is
226
253
  // ---------- Utility Patterns ----------
227
254
  /** Maximum number of task number to content mappings to track */
228
255
  const MAX_TASK_MAPPINGS = 100;
229
- // ---------- RALPH_STATUS Block Patterns ----------
230
- // Based on Ralph Claude Code structured status reporting
231
- /**
232
- * Matches the start of a RALPH_STATUS block
233
- * Pattern: ---RALPH_STATUS---
234
- */
235
- const RALPH_STATUS_START_PATTERN = /^---RALPH_STATUS---\s*$/;
236
- /**
237
- * Matches the end of a RALPH_STATUS block
238
- * Pattern: ---END_RALPH_STATUS---
239
- */
240
- const RALPH_STATUS_END_PATTERN = /^---END_RALPH_STATUS---\s*$/;
241
- /**
242
- * Matches STATUS field in RALPH_STATUS block
243
- * Captures: IN_PROGRESS | COMPLETE | BLOCKED
244
- */
245
- const RALPH_STATUS_FIELD_PATTERN = /^STATUS:\s*(IN_PROGRESS|COMPLETE|BLOCKED)\s*$/i;
246
- /**
247
- * Matches TASKS_COMPLETED_THIS_LOOP field
248
- * Captures: number
249
- */
250
- const RALPH_TASKS_COMPLETED_PATTERN = /^TASKS_COMPLETED_THIS_LOOP:\s*(\d+)\s*$/i;
251
- /**
252
- * Matches FILES_MODIFIED field
253
- * Captures: number
254
- */
255
- const RALPH_FILES_MODIFIED_PATTERN = /^FILES_MODIFIED:\s*(\d+)\s*$/i;
256
- /**
257
- * Matches TESTS_STATUS field
258
- * Captures: PASSING | FAILING | NOT_RUN
259
- */
260
- const RALPH_TESTS_STATUS_PATTERN = /^TESTS_STATUS:\s*(PASSING|FAILING|NOT_RUN)\s*$/i;
261
- /**
262
- * Matches WORK_TYPE field
263
- * Captures: IMPLEMENTATION | TESTING | DOCUMENTATION | REFACTORING
264
- */
265
- const RALPH_WORK_TYPE_PATTERN = /^WORK_TYPE:\s*(IMPLEMENTATION|TESTING|DOCUMENTATION|REFACTORING)\s*$/i;
266
- /**
267
- * Matches EXIT_SIGNAL field
268
- * Captures: true | false
269
- */
270
- const RALPH_EXIT_SIGNAL_PATTERN = /^EXIT_SIGNAL:\s*(true|false)\s*$/i;
271
- /**
272
- * Matches RECOMMENDATION field
273
- * Captures: any text
274
- */
275
- const RALPH_RECOMMENDATION_PATTERN = /^RECOMMENDATION:\s*(.+)$/i;
276
- // ---------- Completion Indicator Patterns (for dual-condition exit) ----------
277
- /**
278
- * Patterns that indicate potential completion (natural language)
279
- * Count >= 2 along with EXIT_SIGNAL: true triggers exit
280
- */
281
- const COMPLETION_INDICATOR_PATTERNS = [
282
- /all\s+(?:tasks?|items?|work)\s+(?:are\s+)?(?:completed?|done|finished)/i,
283
- /(?:completed?|finished)\s+all\s+(?:tasks?|items?|work)/i,
284
- /nothing\s+(?:left|remaining)\s+to\s+do/i,
285
- /no\s+more\s+(?:tasks?|items?|work)/i,
286
- /everything\s+(?:is\s+)?(?:completed?|done)/i,
287
- /project\s+(?:is\s+)?(?:completed?|done|finished)/i,
288
- ];
289
256
  // ---------- Priority Detection Patterns ----------
290
257
  // Pre-compiled for performance; avoids repeated allocation in parsePriority()
291
258
  /** P0 (Critical) priority patterns - highest severity issues */
@@ -352,6 +319,13 @@ const P2_PRIORITY_PATTERNS = [
352
319
  * - 2nd occurrence: Emits `completionDetected` event (actual completion)
353
320
  * - If loop already active: Emits immediately on first occurrence
354
321
  *
322
+ * ## Sub-modules
323
+ *
324
+ * - `planTracker` - Plan task management, checkpoints, versioning
325
+ * - `fixPlanWatcher` - @fix_plan.md file watching and parsing
326
+ * - `stallDetector` - Iteration stall detection
327
+ * - `statusParser` - RALPH_STATUS block parsing, circuit breaker
328
+ *
355
329
  * ## Events
356
330
  *
357
331
  * - `loopUpdate` - Loop state changed (status, iteration, phrase)
@@ -370,6 +344,16 @@ const P2_PRIORITY_PATTERNS = [
370
344
  * ```
371
345
  */
372
346
  export class RalphTracker extends EventEmitter {
347
+ // ========== Sub-modules ==========
348
+ /** Plan task management sub-module */
349
+ planTracker = new RalphPlanTracker();
350
+ /** @fix_plan.md file watcher sub-module */
351
+ fixPlanWatcher;
352
+ /** Iteration stall detector sub-module */
353
+ stallDetector = new RalphStallDetector();
354
+ /** RALPH_STATUS block parser and circuit breaker sub-module */
355
+ statusParser = new RalphStatusParser();
356
+ // ========== Core State ==========
373
357
  /** Current state of the detected loop */
374
358
  _loopState;
375
359
  /** Map of todo items by ID for O(1) lookup */
@@ -383,14 +367,10 @@ export class RalphTracker extends EventEmitter {
383
367
  _completionPhraseCount = new Map();
384
368
  /** Timestamp of last cleanup check for throttling */
385
369
  _lastCleanupTime = 0;
386
- /** Debounce timer for todoUpdate events */
387
- _todoUpdateTimer = null;
388
- /** Debounce timer for loopUpdate events */
389
- _loopUpdateTimer = null;
390
- /** Flag indicating pending todoUpdate emission */
391
- _todoUpdatePending = false;
392
- /** Flag indicating pending loopUpdate emission */
393
- _loopUpdatePending = false;
370
+ /** Debouncer for todoUpdate events */
371
+ _todoDeb = new Debouncer(EVENT_DEBOUNCE_MS);
372
+ /** Debouncer for loopUpdate events */
373
+ _loopDeb = new Debouncer(EVENT_DEBOUNCE_MS);
394
374
  /** When true, prevents auto-enable on pattern detection */
395
375
  _autoEnableDisabled = true;
396
376
  /** Maps task numbers from "✔ Task #N" format to their content for status updates */
@@ -403,64 +383,6 @@ export class RalphTracker extends EventEmitter {
403
383
  _partialPromiseBuffer = '';
404
384
  /** Maximum size of partial promise buffer */
405
385
  static MAX_PARTIAL_PROMISE_SIZE = 256;
406
- // ========== RALPH_STATUS Block State ==========
407
- /** Circuit breaker state tracking */
408
- _circuitBreaker;
409
- /** Buffer for RALPH_STATUS block lines */
410
- _statusBlockBuffer = [];
411
- /** Flag indicating we're inside a RALPH_STATUS block */
412
- _inStatusBlock = false;
413
- /** Last parsed RALPH_STATUS block */
414
- _lastStatusBlock = null;
415
- /** Count of completion indicators detected (for dual-condition exit) */
416
- _completionIndicators = 0;
417
- /** Whether dual-condition exit gate has been met */
418
- _exitGateMet = false;
419
- /** Cumulative files modified across all iterations */
420
- _totalFilesModified = 0;
421
- /** Cumulative tasks completed across all iterations */
422
- _totalTasksCompleted = 0;
423
- /** Working directory for @fix_plan.md watching */
424
- _workingDir = null;
425
- /** File watcher for @fix_plan.md */
426
- _fixPlanWatcher = null;
427
- /** Error handler for FSWatcher (stored for cleanup to prevent memory leak) */
428
- _fixPlanWatcherErrorHandler = null;
429
- /** Debounce timer for file change events */
430
- _fixPlanReloadTimer = null;
431
- /** Path to the @fix_plan.md file being watched */
432
- _fixPlanPath = null;
433
- /**
434
- * When @fix_plan.md is active, treat it as the source of truth for todo status.
435
- * This prevents output-based detection from overriding file-based status.
436
- */
437
- get isFileAuthoritative() {
438
- return this._fixPlanPath !== null;
439
- }
440
- // ========== Enhanced Plan Management ==========
441
- /** Current version of the plan (incremented on changes) */
442
- _planVersion = 1;
443
- /** History of plan versions for rollback support */
444
- _planHistory = [];
445
- /** Enhanced plan tasks with execution tracking */
446
- _planTasks = new Map();
447
- /** Checkpoint intervals (iterations at which to trigger review) */
448
- _checkpointIterations = [5, 10, 20, 30, 50, 75, 100];
449
- /** Last checkpoint iteration */
450
- _lastCheckpointIteration = 0;
451
- // ========== Iteration Stall Detection ==========
452
- /** Timestamp when iteration count last changed */
453
- _lastIterationChangeTime = 0;
454
- /** Last observed iteration count for stall detection */
455
- _lastObservedIteration = 0;
456
- /** Timer for iteration stall detection */
457
- _iterationStallTimer = null;
458
- /** Iteration stall warning threshold (ms) - default 10 minutes */
459
- _iterationStallWarningMs = 10 * 60 * 1000;
460
- /** Iteration stall critical threshold (ms) - default 20 minutes */
461
- _iterationStallCriticalMs = 20 * 60 * 1000;
462
- /** Whether stall warning has been emitted */
463
- _iterationStallWarned = false;
464
386
  /** Alternate completion phrases (P1-003: multi-phrase support) - Set for O(1) lookup */
465
387
  _alternateCompletionPhrases = new Set();
466
388
  // ========== P1-009: Progress Estimation ==========
@@ -472,6 +394,12 @@ export class RalphTracker extends EventEmitter {
472
394
  _todosStartedAt = 0;
473
395
  /** Map of todo ID to timestamp when it started (for duration tracking) */
474
396
  _todoStartTimes = new Map();
397
+ /** Last calculated completion confidence */
398
+ _lastCompletionConfidence;
399
+ /** Manages periodic cleanup timers (todo expiry) */
400
+ cleanup = new CleanupManager();
401
+ /** Confidence threshold for triggering completion (0-100) */
402
+ static COMPLETION_CONFIDENCE_THRESHOLD = 70;
475
403
  /**
476
404
  * Creates a new RalphTracker instance.
477
405
  * Starts in disabled state until Ralph patterns are detected.
@@ -479,13 +407,235 @@ export class RalphTracker extends EventEmitter {
479
407
  constructor() {
480
408
  super();
481
409
  this._loopState = createInitialRalphTrackerState();
482
- this._circuitBreaker = createInitialCircuitBreakerStatus();
483
- this._lastIterationChangeTime = Date.now();
410
+ // Initialize fix plan watcher with callbacks to parent methods
411
+ this.fixPlanWatcher = new RalphFixPlanWatcher((content) => this.parsePriority(content), (content) => this.generateTodoId(content));
412
+ // Wire sub-module events
413
+ this._wireSubModuleEvents();
414
+ // Periodic cleanup of expired todos — ensures stale entries are purged
415
+ // even when no terminal data is flowing (e.g., idle sessions)
416
+ this.cleanup.setInterval(() => this.cleanupExpiredTodos(), TODO_CLEANUP_INTERVAL_MS, {
417
+ description: 'ralph todo expiry cleanup',
418
+ });
419
+ }
420
+ /**
421
+ * Forward all sub-module events through RalphTracker
422
+ * so external consumers don't need to know about the split.
423
+ */
424
+ _wireSubModuleEvents() {
425
+ // Forward plan tracker events
426
+ for (const event of [
427
+ 'planInitialized',
428
+ 'planTaskUpdate',
429
+ 'taskBlocked',
430
+ 'taskUnblocked',
431
+ 'planCheckpoint',
432
+ 'planTaskAdded',
433
+ 'planRollback',
434
+ ]) {
435
+ this.planTracker.on(event, (...args) => this.emit(event, ...args));
436
+ }
437
+ // Forward status parser events
438
+ this.statusParser.on('statusBlockDetected', (block) => {
439
+ // Auto-enable tracker when we see a status block
440
+ if (!this._loopState.enabled && !this._autoEnableDisabled) {
441
+ this.enable();
442
+ }
443
+ this._loopState.lastActivity = Date.now();
444
+ this.emit('statusBlockDetected', block);
445
+ this.emitLoopUpdateDebounced();
446
+ });
447
+ this.statusParser.on('circuitBreakerUpdate', (status) => {
448
+ this.emit('circuitBreakerUpdate', status);
449
+ });
450
+ this.statusParser.on('exitGateMet', (data) => {
451
+ this.emit('exitGateMet', data);
452
+ });
453
+ // Forward stall detector events
454
+ this.stallDetector.on('iterationStallWarning', (data) => {
455
+ this.emit('iterationStallWarning', data);
456
+ });
457
+ this.stallDetector.on('iterationStallCritical', (data) => {
458
+ this.emit('iterationStallCritical', data);
459
+ });
460
+ // Forward fix plan watcher events
461
+ this.fixPlanWatcher.on('todosLoaded', (items) => {
462
+ // Replace _todos with file-based items
463
+ this._todos.clear();
464
+ for (const item of items) {
465
+ this._todos.set(item.id, item);
466
+ }
467
+ // Auto-enable tracker when we have todos from @fix_plan.md
468
+ if (!this._loopState.enabled) {
469
+ this.enable();
470
+ }
471
+ this.emit('todoUpdate', this.todos);
472
+ });
473
+ }
474
+ // ========== Delegated Plan Tracker Methods ==========
475
+ /**
476
+ * Initialize plan tasks from generated plan items.
477
+ */
478
+ initializePlanTasks(items) {
479
+ this.planTracker.initializePlanTasks(items);
480
+ }
481
+ /**
482
+ * Update a specific plan task's status, attempts, or error.
483
+ */
484
+ updatePlanTask(taskId, update) {
485
+ return this.planTracker.updatePlanTask(taskId, update);
486
+ }
487
+ /**
488
+ * Add a new task to the plan.
489
+ */
490
+ addPlanTask(task) {
491
+ return this.planTracker.addPlanTask(task);
492
+ }
493
+ /**
494
+ * Get all plan tasks.
495
+ */
496
+ getPlanTasks() {
497
+ return this.planTracker.getPlanTasks();
498
+ }
499
+ /**
500
+ * Generate a checkpoint review.
501
+ */
502
+ generateCheckpointReview() {
503
+ return this.planTracker.generateCheckpointReview();
504
+ }
505
+ /**
506
+ * Get plan version history.
507
+ */
508
+ getPlanHistory() {
509
+ return this.planTracker.getPlanHistory();
510
+ }
511
+ /**
512
+ * Rollback to a previous plan version.
513
+ */
514
+ rollbackToVersion(version) {
515
+ return this.planTracker.rollbackToVersion(version);
516
+ }
517
+ /**
518
+ * Check if checkpoint review is due.
519
+ */
520
+ isCheckpointDue() {
521
+ return this.planTracker.isCheckpointDue();
522
+ }
523
+ /**
524
+ * Get current plan version.
525
+ */
526
+ get planVersion() {
527
+ return this.planTracker.planVersion;
528
+ }
529
+ // ========== Delegated Fix Plan Watcher Methods ==========
530
+ /**
531
+ * Set the working directory and start watching @fix_plan.md.
532
+ * @param workingDir - The session's working directory
533
+ */
534
+ setWorkingDir(workingDir) {
535
+ this.fixPlanWatcher.setWorkingDir(workingDir);
536
+ }
537
+ /**
538
+ * Load @fix_plan.md from disk if it exists.
539
+ */
540
+ async loadFixPlanFromDisk() {
541
+ return this.fixPlanWatcher.loadFixPlanFromDisk();
542
+ }
543
+ /**
544
+ * Stop watching @fix_plan.md.
545
+ */
546
+ stopWatchingFixPlan() {
547
+ this.fixPlanWatcher.stopWatchingFixPlan();
548
+ }
549
+ /**
550
+ * When @fix_plan.md is active, treat it as the source of truth for todo status.
551
+ */
552
+ get isFileAuthoritative() {
553
+ return this.fixPlanWatcher.isFileAuthoritative;
554
+ }
555
+ /**
556
+ * Generate @fix_plan.md content from current todos.
557
+ */
558
+ generateFixPlanMarkdown() {
559
+ return generateFixPlanMarkdown(this.todos);
560
+ }
561
+ /**
562
+ * Parse @fix_plan.md content and import todos.
563
+ * Replaces current todos with imported ones.
564
+ *
565
+ * @param content - Markdown content from @fix_plan.md
566
+ * @returns Number of todos imported
567
+ */
568
+ importFixPlanMarkdown(content) {
569
+ const newTodos = importFixPlanMarkdown(content, (c) => this.parsePriority(c), (c) => this.generateTodoId(c));
570
+ // Replace current todos with imported ones
571
+ this._todos.clear();
572
+ for (const todo of newTodos) {
573
+ this._todos.set(todo.id, todo);
574
+ }
575
+ // Emit update
576
+ this.emit('todoUpdate', this.todos);
577
+ return newTodos.length;
578
+ }
579
+ // ========== Delegated Stall Detector Methods ==========
580
+ /**
581
+ * Start iteration stall detection timer.
582
+ */
583
+ startIterationStallDetection() {
584
+ this.stallDetector.startIterationStallDetection();
585
+ }
586
+ /**
587
+ * Stop iteration stall detection timer.
588
+ */
589
+ stopIterationStallDetection() {
590
+ this.stallDetector.stopIterationStallDetection();
591
+ }
592
+ /**
593
+ * Get iteration stall metrics for monitoring.
594
+ */
595
+ getIterationStallMetrics() {
596
+ return this.stallDetector.getIterationStallMetrics();
597
+ }
598
+ /**
599
+ * Configure iteration stall thresholds.
600
+ */
601
+ configureIterationStallThresholds(warningMs, criticalMs) {
602
+ this.stallDetector.configureIterationStallThresholds(warningMs, criticalMs);
603
+ }
604
+ // ========== Delegated Status Parser Methods ==========
605
+ /**
606
+ * Manually reset circuit breaker to CLOSED state.
607
+ * @fires circuitBreakerUpdate
608
+ */
609
+ resetCircuitBreaker() {
610
+ this.statusParser.resetCircuitBreaker();
611
+ }
612
+ /**
613
+ * Get current circuit breaker status.
614
+ */
615
+ get circuitBreakerStatus() {
616
+ return this.statusParser.circuitBreakerStatus;
617
+ }
618
+ /**
619
+ * Get last parsed RALPH_STATUS block.
620
+ */
621
+ get lastStatusBlock() {
622
+ return this.statusParser.lastStatusBlock;
623
+ }
624
+ /**
625
+ * Get cumulative stats from status blocks.
626
+ */
627
+ get cumulativeStats() {
628
+ return this.statusParser.cumulativeStats;
629
+ }
630
+ /**
631
+ * Whether dual-condition exit gate has been met.
632
+ */
633
+ get exitGateMet() {
634
+ return this.statusParser.exitGateMet;
484
635
  }
636
+ // ========== Core Methods ==========
485
637
  /**
486
638
  * Add an alternate completion phrase (P1-003: multi-phrase support).
487
- * Multiple phrases can trigger completion (useful for complex workflows).
488
- * @param phrase - Additional phrase that can trigger completion
489
639
  */
490
640
  addAlternateCompletionPhrase(phrase) {
491
641
  if (!this._alternateCompletionPhrases.has(phrase)) {
@@ -496,7 +646,6 @@ export class RalphTracker extends EventEmitter {
496
646
  }
497
647
  /**
498
648
  * Remove an alternate completion phrase.
499
- * @param phrase - Phrase to remove
500
649
  */
501
650
  removeAlternateCompletionPhrase(phrase) {
502
651
  if (this._alternateCompletionPhrases.delete(phrase)) {
@@ -506,16 +655,12 @@ export class RalphTracker extends EventEmitter {
506
655
  }
507
656
  /**
508
657
  * Check if a phrase matches any valid completion phrase (primary or alternate).
509
- * @param phrase - Phrase to check
510
- * @returns True if phrase matches any valid completion phrase
511
658
  */
512
659
  isValidCompletionPhrase(phrase) {
513
660
  return this.findMatchingCompletionPhrase(phrase) !== null;
514
661
  }
515
662
  /**
516
663
  * Find which completion phrase (primary or alternate) matches the given phrase.
517
- * @param phrase - Phrase to check
518
- * @returns The matched canonical phrase, or null if no match
519
664
  */
520
665
  findMatchingCompletionPhrase(phrase) {
521
666
  const primary = this._loopState.completionPhrase;
@@ -531,7 +676,6 @@ export class RalphTracker extends EventEmitter {
531
676
  }
532
677
  /**
533
678
  * Prevent auto-enable from pattern detection.
534
- * Use this when the user has explicitly disabled the Ralph tracker.
535
679
  */
536
680
  disableAutoEnable() {
537
681
  this._autoEnableDisabled = true;
@@ -548,129 +692,14 @@ export class RalphTracker extends EventEmitter {
548
692
  get autoEnableDisabled() {
549
693
  return this._autoEnableDisabled;
550
694
  }
551
- /**
552
- * Set the working directory and start watching @fix_plan.md.
553
- * Automatically loads existing @fix_plan.md if present.
554
- * @param workingDir - The session's working directory
555
- */
556
- setWorkingDir(workingDir) {
557
- this._workingDir = workingDir;
558
- this._fixPlanPath = join(workingDir, '@fix_plan.md');
559
- // Try to load existing @fix_plan.md
560
- this.loadFixPlanFromDisk();
561
- // Start watching for changes
562
- this.startWatchingFixPlan();
563
- }
564
- /**
565
- * Load @fix_plan.md from disk if it exists.
566
- * Called on initialization and when file changes are detected.
567
- */
568
- async loadFixPlanFromDisk() {
569
- if (!this._fixPlanPath)
570
- return 0;
571
- try {
572
- if (!existsSync(this._fixPlanPath)) {
573
- return 0;
574
- }
575
- const content = await readFile(this._fixPlanPath, 'utf-8');
576
- const count = this.importFixPlanMarkdown(content);
577
- if (count > 0) {
578
- // Auto-enable tracker when we have todos from @fix_plan.md
579
- if (!this._loopState.enabled) {
580
- this.enable();
581
- }
582
- console.log(`[RalphTracker] Loaded ${count} todos from @fix_plan.md`);
583
- }
584
- return count;
585
- }
586
- catch (err) {
587
- // File doesn't exist or can't be read - that's OK
588
- console.log(`[RalphTracker] Could not load @fix_plan.md: ${err}`);
589
- return 0;
590
- }
591
- }
592
- /**
593
- * Start watching @fix_plan.md for changes.
594
- * Reloads todos when the file is modified.
595
- */
596
- startWatchingFixPlan() {
597
- if (!this._fixPlanPath || !this._workingDir)
598
- return;
599
- // Stop existing watcher if any
600
- this.stopWatchingFixPlan();
601
- try {
602
- // Only watch if the file exists
603
- if (!existsSync(this._fixPlanPath)) {
604
- // Watch the directory instead for file creation
605
- this._fixPlanWatcher = fsWatch(this._workingDir, (_eventType, filename) => {
606
- if (filename === '@fix_plan.md') {
607
- this.handleFixPlanChange();
608
- }
609
- });
610
- }
611
- else {
612
- // Watch the file directly
613
- this._fixPlanWatcher = fsWatch(this._fixPlanPath, () => {
614
- this.handleFixPlanChange();
615
- });
616
- }
617
- // Add error handler to prevent unhandled errors and clean up on failure
618
- // Store handler reference for proper cleanup in stopWatchingFixPlan()
619
- if (this._fixPlanWatcher) {
620
- this._fixPlanWatcherErrorHandler = (err) => {
621
- console.log(`[RalphTracker] FSWatcher error for @fix_plan.md: ${err.message}`);
622
- this.stopWatchingFixPlan();
623
- };
624
- this._fixPlanWatcher.on('error', this._fixPlanWatcherErrorHandler);
625
- }
626
- }
627
- catch (err) {
628
- console.log(`[RalphTracker] Could not watch @fix_plan.md: ${err}`);
629
- }
630
- }
631
- /**
632
- * Handle @fix_plan.md file change with debouncing.
633
- */
634
- handleFixPlanChange() {
635
- // Debounce rapid changes (e.g., multiple writes)
636
- if (this._fixPlanReloadTimer) {
637
- clearTimeout(this._fixPlanReloadTimer);
638
- }
639
- this._fixPlanReloadTimer = setTimeout(() => {
640
- this._fixPlanReloadTimer = null;
641
- this.loadFixPlanFromDisk();
642
- }, 500); // 500ms debounce
643
- }
644
- /**
645
- * Stop watching @fix_plan.md.
646
- */
647
- stopWatchingFixPlan() {
648
- if (this._fixPlanWatcher) {
649
- // Remove error handler before closing to prevent memory leak
650
- if (this._fixPlanWatcherErrorHandler) {
651
- this._fixPlanWatcher.off('error', this._fixPlanWatcherErrorHandler);
652
- this._fixPlanWatcherErrorHandler = null;
653
- }
654
- this._fixPlanWatcher.close();
655
- this._fixPlanWatcher = null;
656
- }
657
- if (this._fixPlanReloadTimer) {
658
- clearTimeout(this._fixPlanReloadTimer);
659
- this._fixPlanReloadTimer = null;
660
- }
661
- }
662
695
  /**
663
696
  * Whether the tracker is enabled and actively monitoring output.
664
- * Disabled by default; auto-enables when Ralph patterns detected.
665
- * @returns True if tracker is processing terminal data
666
697
  */
667
698
  get enabled() {
668
699
  return this._loopState.enabled;
669
700
  }
670
701
  /**
671
702
  * Enable the tracker to start monitoring terminal output.
672
- * Called automatically when Ralph patterns are detected.
673
- * Emits 'enabled' event when transitioning from disabled state.
674
703
  * @fires enabled
675
704
  * @fires loopUpdate
676
705
  */
@@ -684,7 +713,6 @@ export class RalphTracker extends EventEmitter {
684
713
  }
685
714
  /**
686
715
  * Disable the tracker to stop monitoring terminal output.
687
- * Terminal data will be ignored until re-enabled.
688
716
  * @fires loopUpdate
689
717
  */
690
718
  disable() {
@@ -696,17 +724,6 @@ export class RalphTracker extends EventEmitter {
696
724
  }
697
725
  /**
698
726
  * Soft reset - clears state but keeps enabled status.
699
- * Use when a new task/loop starts within the same session.
700
- *
701
- * Clears:
702
- * - All todo items
703
- * - Completion phrase tracking
704
- * - Loop state (active, iterations)
705
- * - Line buffer
706
- *
707
- * Preserves:
708
- * - Enabled status
709
- *
710
727
  * @fires loopUpdate
711
728
  * @fires todoUpdate
712
729
  */
@@ -721,15 +738,10 @@ export class RalphTracker extends EventEmitter {
721
738
  this._taskNumberToContent.clear();
722
739
  this._lineBuffer = '';
723
740
  this._partialPromiseBuffer = '';
724
- // Reset RALPH_STATUS block state
725
- this._statusBlockBuffer = [];
726
- this._inStatusBlock = false;
727
- this._lastStatusBlock = null;
728
- this._completionIndicators = 0;
729
- this._exitGateMet = false;
730
- this._totalFilesModified = 0;
731
- this._totalTasksCompleted = 0;
732
- // Keep circuit breaker state on soft reset (it tracks across iterations)
741
+ // Reset sub-modules
742
+ this.statusParser.reset();
743
+ this.planTracker.reset();
744
+ this.stallDetector.reset();
733
745
  // Emit on next tick to prevent listeners from modifying state during reset (non-reentrant)
734
746
  const loopState = this.loopState;
735
747
  const todos = this.todos;
@@ -740,8 +752,6 @@ export class RalphTracker extends EventEmitter {
740
752
  }
741
753
  /**
742
754
  * Full reset - clears all state including enabled status.
743
- * Use when session is closed or completely cleared.
744
- * Returns tracker to initial disabled state.
745
755
  * @fires loopUpdate
746
756
  * @fires todoUpdate
747
757
  */
@@ -755,15 +765,11 @@ export class RalphTracker extends EventEmitter {
755
765
  this._todoStartTimes.clear();
756
766
  this._alternateCompletionPhrases.clear();
757
767
  this._lineBuffer = '';
758
- // Reset all RALPH_STATUS block and circuit breaker state
759
- this._statusBlockBuffer = [];
760
- this._inStatusBlock = false;
761
- this._lastStatusBlock = null;
762
- this._completionIndicators = 0;
763
- this._exitGateMet = false;
764
- this._totalFilesModified = 0;
765
- this._totalTasksCompleted = 0;
766
- this._circuitBreaker = createInitialCircuitBreakerStatus();
768
+ this._partialPromiseBuffer = '';
769
+ // Full reset sub-modules
770
+ this.statusParser.fullReset();
771
+ this.planTracker.fullReset();
772
+ this.stallDetector.reset();
767
773
  // Emit on next tick to prevent listeners from modifying state during reset (non-reentrant)
768
774
  const loopState = this.loopState;
769
775
  const todos = this.todos;
@@ -774,178 +780,49 @@ export class RalphTracker extends EventEmitter {
774
780
  }
775
781
  /**
776
782
  * Clear all debounce timers.
777
- * Called during reset/fullReset to prevent stale emissions.
778
783
  */
779
784
  clearDebounceTimers() {
780
- if (this._todoUpdateTimer) {
781
- clearTimeout(this._todoUpdateTimer);
782
- this._todoUpdateTimer = null;
783
- }
784
- if (this._loopUpdateTimer) {
785
- clearTimeout(this._loopUpdateTimer);
786
- this._loopUpdateTimer = null;
787
- }
788
- this._todoUpdatePending = false;
789
- this._loopUpdatePending = false;
785
+ this._todoDeb.cancel();
786
+ this._loopDeb.cancel();
790
787
  }
791
788
  /**
792
789
  * Emit todoUpdate event with debouncing.
793
- * Batches rapid consecutive calls to reduce UI jitter.
794
- * The event fires after EVENT_DEBOUNCE_MS of inactivity.
795
790
  */
796
791
  emitTodoUpdateDebounced() {
797
- this._todoUpdatePending = true;
798
- if (this._todoUpdateTimer) {
799
- clearTimeout(this._todoUpdateTimer);
800
- }
801
- this._todoUpdateTimer = setTimeout(() => {
802
- if (this._todoUpdatePending) {
803
- this._todoUpdatePending = false;
804
- this._todoUpdateTimer = null;
805
- this.emit('todoUpdate', this.todos);
806
- }
807
- }, EVENT_DEBOUNCE_MS);
792
+ this._todoDeb.schedule(() => this.emit('todoUpdate', this.todos));
808
793
  }
809
794
  /**
810
795
  * Emit loopUpdate event with debouncing.
811
- * Batches rapid consecutive calls to reduce UI jitter.
812
- * The event fires after EVENT_DEBOUNCE_MS of inactivity.
813
796
  */
814
797
  emitLoopUpdateDebounced() {
815
- this._loopUpdatePending = true;
816
- if (this._loopUpdateTimer) {
817
- clearTimeout(this._loopUpdateTimer);
818
- }
819
- this._loopUpdateTimer = setTimeout(() => {
820
- if (this._loopUpdatePending) {
821
- this._loopUpdatePending = false;
822
- this._loopUpdateTimer = null;
823
- this.emit('loopUpdate', this.loopState);
824
- }
825
- }, EVENT_DEBOUNCE_MS);
798
+ this._loopDeb.schedule(() => this.emit('loopUpdate', this.loopState));
826
799
  }
827
800
  /**
828
801
  * Flush all pending debounced events immediately.
829
- * Useful for testing or when immediate state sync is needed.
830
802
  */
831
803
  flushPendingEvents() {
832
- if (this._todoUpdatePending) {
833
- this._todoUpdatePending = false;
834
- if (this._todoUpdateTimer) {
835
- clearTimeout(this._todoUpdateTimer);
836
- this._todoUpdateTimer = null;
837
- }
804
+ if (this._todoDeb.isPending) {
805
+ this._todoDeb.cancel();
838
806
  this.emit('todoUpdate', this.todos);
839
807
  }
840
- if (this._loopUpdatePending) {
841
- this._loopUpdatePending = false;
842
- if (this._loopUpdateTimer) {
843
- clearTimeout(this._loopUpdateTimer);
844
- this._loopUpdateTimer = null;
845
- }
808
+ if (this._loopDeb.isPending) {
809
+ this._loopDeb.cancel();
846
810
  this.emit('loopUpdate', this.loopState);
847
811
  }
848
812
  }
849
813
  /**
850
814
  * Get a copy of the current loop state.
851
- * @returns Shallow copy of loop state (safe to modify)
852
- */
853
- // ========== Iteration Stall Detection Methods ==========
854
- /**
855
- * Start iteration stall detection timer.
856
- * Should be called when the loop becomes active.
857
- */
858
- startIterationStallDetection() {
859
- this.stopIterationStallDetection();
860
- this._lastIterationChangeTime = Date.now();
861
- this._iterationStallWarned = false;
862
- // Check every minute
863
- this._iterationStallTimer = setInterval(() => {
864
- this.checkIterationStall();
865
- }, 60 * 1000);
866
- }
867
- /**
868
- * Stop iteration stall detection timer.
869
815
  */
870
- stopIterationStallDetection() {
871
- if (this._iterationStallTimer) {
872
- clearInterval(this._iterationStallTimer);
873
- this._iterationStallTimer = null;
874
- }
816
+ get loopState() {
817
+ return {
818
+ ...this._loopState,
819
+ planVersion: this.planTracker.planVersion,
820
+ planHistoryLength: this.planTracker.getPlanHistory().length,
821
+ completionConfidence: this._lastCompletionConfidence,
822
+ };
875
823
  }
876
824
  /**
877
- * Check for iteration stall and emit appropriate events.
878
- */
879
- checkIterationStall() {
880
- if (!this._loopState.active)
881
- return;
882
- const stallDurationMs = Date.now() - this._lastIterationChangeTime;
883
- // Critical stall (longer duration)
884
- if (stallDurationMs >= this._iterationStallCriticalMs) {
885
- this.emit('iterationStallCritical', {
886
- iteration: this._loopState.cycleCount,
887
- stallDurationMs,
888
- });
889
- return;
890
- }
891
- // Warning stall
892
- if (stallDurationMs >= this._iterationStallWarningMs && !this._iterationStallWarned) {
893
- this._iterationStallWarned = true;
894
- this.emit('iterationStallWarning', {
895
- iteration: this._loopState.cycleCount,
896
- stallDurationMs,
897
- });
898
- }
899
- }
900
- /**
901
- * Get iteration stall metrics for monitoring.
902
- */
903
- getIterationStallMetrics() {
904
- return {
905
- lastIterationChangeTime: this._lastIterationChangeTime,
906
- stallDurationMs: Date.now() - this._lastIterationChangeTime,
907
- warningThresholdMs: this._iterationStallWarningMs,
908
- criticalThresholdMs: this._iterationStallCriticalMs,
909
- isWarned: this._iterationStallWarned,
910
- currentIteration: this._loopState.cycleCount,
911
- };
912
- }
913
- /**
914
- * Configure iteration stall thresholds.
915
- * @param warningMs - Warning threshold in milliseconds
916
- * @param criticalMs - Critical threshold in milliseconds
917
- */
918
- configureIterationStallThresholds(warningMs, criticalMs) {
919
- this._iterationStallWarningMs = warningMs;
920
- this._iterationStallCriticalMs = criticalMs;
921
- }
922
- get loopState() {
923
- return {
924
- ...this._loopState,
925
- planVersion: this._planVersion,
926
- planHistoryLength: this._planHistory.length,
927
- completionConfidence: this._lastCompletionConfidence,
928
- };
929
- }
930
- /** Last calculated completion confidence */
931
- _lastCompletionConfidence;
932
- /** Confidence threshold for triggering completion (0-100) */
933
- static COMPLETION_CONFIDENCE_THRESHOLD = 70;
934
- /**
935
- * Calculate confidence score for a potential completion signal.
936
- *
937
- * Scoring weights:
938
- * - Promise tag with proper format: +30
939
- * - Matches expected phrase: +25
940
- * - All todos complete: +20
941
- * - EXIT_SIGNAL: true: +15
942
- * - Multiple completion indicators (>=2): +10
943
- * - Context appropriate (not in prompt/explanation): +10
944
- * - Loop was explicitly active: +10
945
- *
946
- * @param phrase - The detected phrase to evaluate
947
- * @param context - Optional surrounding context for the phrase
948
- * @returns CompletionConfidence assessment
825
+ * Calculate confidence score for a potential completion signal.
949
826
  */
950
827
  calculateCompletionConfidence(phrase, context) {
951
828
  let score = 0;
@@ -978,19 +855,19 @@ export class RalphTracker extends EventEmitter {
978
855
  score += 20;
979
856
  }
980
857
  // Check for EXIT_SIGNAL from RALPH_STATUS block (adds 15 points)
981
- if (this._lastStatusBlock?.exitSignal === true) {
858
+ const lastBlock = this.statusParser.lastStatusBlock;
859
+ if (lastBlock?.exitSignal === true) {
982
860
  signals.hasExitSignal = true;
983
861
  score += 15;
984
862
  }
985
863
  // Check for multiple completion indicators (adds 10 points)
986
- if (this._completionIndicators >= 2) {
864
+ if (this.statusParser.cumulativeStats.completionIndicators >= 2) {
987
865
  signals.multipleIndicators = true;
988
866
  score += 10;
989
867
  }
990
868
  // Check context appropriateness (deduct if inappropriate)
991
869
  if (context) {
992
870
  const lowerContext = context.toLowerCase();
993
- // Deduct points if phrase appears in prompt-like context
994
871
  if (lowerContext.includes('output:') ||
995
872
  lowerContext.includes('completion phrase') ||
996
873
  lowerContext.includes('output exactly') ||
@@ -1024,28 +901,12 @@ export class RalphTracker extends EventEmitter {
1024
901
  }
1025
902
  /**
1026
903
  * Get all tracked todo items as an array.
1027
- * @returns Array of todo items (copy, safe to modify)
1028
904
  */
1029
905
  get todos() {
1030
906
  return Array.from(this._todos.values());
1031
907
  }
1032
908
  /**
1033
909
  * Process raw terminal data to detect inner loop patterns.
1034
- *
1035
- * This is the main entry point for parsing output. Call this with each
1036
- * chunk of data from the PTY. The tracker will:
1037
- *
1038
- * 1. Strip ANSI escape codes
1039
- * 2. Auto-enable if disabled and Ralph patterns detected
1040
- * 3. Buffer data and process complete lines
1041
- * 4. Detect loop status, todos, and completion phrases
1042
- * 5. Periodically clean up expired todos
1043
- *
1044
- * @param data - Raw terminal data (may include ANSI codes)
1045
- * @fires loopUpdate - When loop state changes
1046
- * @fires todoUpdate - When todos are detected or updated
1047
- * @fires completionDetected - When completion phrase found
1048
- * @fires enabled - When tracker auto-enables
1049
910
  */
1050
911
  processTerminalData(data) {
1051
912
  // Remove ANSI escape codes for cleaner parsing
@@ -1054,33 +915,29 @@ export class RalphTracker extends EventEmitter {
1054
915
  }
1055
916
  /**
1056
917
  * Process pre-stripped terminal data (ANSI codes already removed).
1057
- * Use this when the caller has already stripped ANSI to avoid redundant regex work.
1058
918
  */
1059
919
  processCleanData(cleanData) {
1060
920
  // If tracker is disabled, only check for patterns that should auto-enable it
1061
921
  if (!this._loopState.enabled) {
1062
- // Don't auto-enable if explicitly disabled by user setting
1063
922
  if (this._autoEnableDisabled) {
1064
923
  return;
1065
924
  }
1066
925
  if (this.shouldAutoEnable(cleanData)) {
1067
926
  this.enable();
1068
- // Continue processing now that we're enabled
1069
927
  }
1070
928
  else {
1071
- return; // Don't process further when disabled
929
+ return;
1072
930
  }
1073
931
  }
1074
932
  // Buffer data for line-based processing
1075
933
  this._lineBuffer += cleanData;
1076
934
  // Prevent unbounded line buffer growth from very long lines
1077
935
  if (this._lineBuffer.length > MAX_LINE_BUFFER_SIZE) {
1078
- // Truncate to last portion to preserve recent data
1079
936
  this._lineBuffer = this._lineBuffer.slice(-Math.floor(MAX_LINE_BUFFER_SIZE / 2));
1080
937
  }
1081
938
  // Process complete lines
1082
939
  const lines = this._lineBuffer.split('\n');
1083
- this._lineBuffer = lines.pop() || ''; // Keep incomplete line in buffer
940
+ this._lineBuffer = lines.pop() || '';
1084
941
  for (const line of lines) {
1085
942
  this.processLine(line);
1086
943
  }
@@ -1091,26 +948,11 @@ export class RalphTracker extends EventEmitter {
1091
948
  }
1092
949
  /**
1093
950
  * Check if data contains patterns that should auto-enable the tracker.
1094
- *
1095
- * The tracker auto-enables when any of these patterns are detected:
1096
- * - `/ralph-loop:ralph-loop` command
1097
- * - `<promise>PHRASE</promise>` completion tags
1098
- * - TodoWrite tool usage indicators
1099
- * - Iteration patterns (`Iteration 5/50`, `[5/50]`)
1100
- * - Todo checkboxes (`- [ ]`, `- [x]`)
1101
- * - Todo indicator icons (`☐`, `◐`, `☒`)
1102
- * - Loop start messages (`Loop started at`)
1103
- * - All tasks complete announcements
1104
- * - Task completion signals
1105
- *
1106
- * @param data - ANSI-cleaned terminal data
1107
- * @returns True if any Ralph-related pattern is detected
1108
951
  */
1109
952
  shouldAutoEnable(data) {
1110
953
  // Cheap pre-filter: skip the full regex battery if none of the key
1111
954
  // substrings that any pattern could match are present in the data.
1112
- // This avoids 12 regex tests on every PTY chunk (the common case).
1113
- if (!data.includes('<') && // <promise>, TodoWrite
955
+ if (!data.includes('<') &&
1114
956
  !data.includes('ralph') &&
1115
957
  !data.includes('Ralph') &&
1116
958
  !data.includes('Todo') &&
@@ -1118,8 +960,8 @@ export class RalphTracker extends EventEmitter {
1118
960
  !data.includes('Iteration') &&
1119
961
  !data.includes('[') &&
1120
962
  !data.includes('\u2610') &&
1121
- !data.includes('\u2612') && // ☐ ☒
1122
- !data.includes('\u2714') && // ✔
963
+ !data.includes('\u2612') &&
964
+ !data.includes('\u2714') &&
1123
965
  !data.includes('Loop') &&
1124
966
  !data.includes('complete') &&
1125
967
  !data.includes('COMPLETE') &&
@@ -1127,74 +969,46 @@ export class RalphTracker extends EventEmitter {
1127
969
  !data.includes('DONE')) {
1128
970
  return false;
1129
971
  }
1130
- // Ralph loop command: /ralph-loop:ralph-loop
1131
- if (RALPH_START_PATTERN.test(data)) {
972
+ if (RALPH_START_PATTERN.test(data))
1132
973
  return true;
1133
- }
1134
- // Completion phrase: <promise>...</promise>
1135
- if (PROMISE_PATTERN.test(data)) {
974
+ if (PROMISE_PATTERN.test(data))
1136
975
  return true;
1137
- }
1138
- // TodoWrite tool usage
1139
- if (TODOWRITE_PATTERN.test(data)) {
976
+ if (TODOWRITE_PATTERN.test(data))
1140
977
  return true;
1141
- }
1142
- // Iteration patterns from Ralph loop: "Iteration 5/50", "[5/50]"
1143
- if (ITERATION_PATTERN.test(data)) {
978
+ if (ITERATION_PATTERN.test(data))
1144
979
  return true;
1145
- }
1146
- // Todo checkboxes: "- [ ] Task" or "- [x] Task"
1147
- // Reset lastIndex BEFORE test to ensure consistent matching with /g flag patterns
1148
980
  TODO_CHECKBOX_PATTERN.lastIndex = 0;
1149
- if (TODO_CHECKBOX_PATTERN.test(data)) {
981
+ if (TODO_CHECKBOX_PATTERN.test(data))
1150
982
  return true;
1151
- }
1152
- // Todo indicator icons: "Todo: ☐", "Todo: ◐", etc.
1153
983
  TODO_INDICATOR_PATTERN.lastIndex = 0;
1154
- if (TODO_INDICATOR_PATTERN.test(data)) {
984
+ if (TODO_INDICATOR_PATTERN.test(data))
1155
985
  return true;
1156
- }
1157
- // Claude Code native todo format: "☐ Task", "☒ Task"
1158
986
  TODO_NATIVE_PATTERN.lastIndex = 0;
1159
- if (TODO_NATIVE_PATTERN.test(data)) {
987
+ if (TODO_NATIVE_PATTERN.test(data))
1160
988
  return true;
1161
- }
1162
- // Claude Code checkmark-based TodoWrite: "✔ Task #N created:", "✔ Task #N updated:"
1163
989
  TODO_TASK_CREATED_PATTERN.lastIndex = 0;
1164
- if (TODO_TASK_CREATED_PATTERN.test(data)) {
990
+ if (TODO_TASK_CREATED_PATTERN.test(data))
1165
991
  return true;
1166
- }
1167
992
  TODO_TASK_STATUS_PATTERN.lastIndex = 0;
1168
- if (TODO_TASK_STATUS_PATTERN.test(data)) {
993
+ if (TODO_TASK_STATUS_PATTERN.test(data))
1169
994
  return true;
1170
- }
1171
- // Loop start patterns (e.g., "Loop started at", "Starting Ralph loop")
1172
- if (LOOP_START_PATTERN.test(data)) {
995
+ if (LOOP_START_PATTERN.test(data))
1173
996
  return true;
1174
- }
1175
- // All tasks complete signals
1176
- if (ALL_COMPLETE_PATTERN.test(data)) {
997
+ if (ALL_COMPLETE_PATTERN.test(data))
1177
998
  return true;
1178
- }
1179
- // Task completion signals
1180
- if (TASK_DONE_PATTERN.test(data)) {
999
+ if (TASK_DONE_PATTERN.test(data))
1181
1000
  return true;
1182
- }
1183
1001
  return false;
1184
1002
  }
1185
1003
  /**
1186
1004
  * Process a single line of terminal output.
1187
- * Runs all detection methods in sequence.
1188
- * @param line - Single line of ANSI-cleaned terminal output
1189
1005
  */
1190
1006
  processLine(line) {
1191
1007
  const trimmed = line.trim();
1192
1008
  if (!trimmed)
1193
1009
  return;
1194
- // Check for RALPH_STATUS block (structured status reporting)
1195
- this.processStatusBlockLine(trimmed);
1196
- // Check for completion indicators (for dual-condition exit gate)
1197
- this.detectCompletionIndicators(trimmed);
1010
+ // Delegate RALPH_STATUS block and completion indicator detection to sub-module
1011
+ this.statusParser.processLine(trimmed);
1198
1012
  // Check for completion phrase
1199
1013
  this.detectCompletionPhrase(trimmed);
1200
1014
  // Check for "all tasks complete" signals
@@ -1208,52 +1022,25 @@ export class RalphTracker extends EventEmitter {
1208
1022
  }
1209
1023
  /**
1210
1024
  * Detect "all tasks complete" messages.
1211
- *
1212
- * When a valid "all complete" message is detected:
1213
- * 1. Marks all tracked todos as completed
1214
- * 2. Emits completion event if a completion phrase is set
1215
- *
1216
- * Validation criteria:
1217
- * - Line must match ALL_COMPLETE_PATTERN
1218
- * - Line must be reasonably short (<100 chars) to avoid matching commentary
1219
- * - Must not look like prompt text (no "output:" or `<promise>`)
1220
- * - Must have at least one tracked todo
1221
- * - If count is mentioned, should roughly match tracked todo count
1222
- *
1223
- * @param line - Single line to check
1224
- * @fires todoUpdate - If any todos marked complete
1225
- * @fires completionDetected - If completion phrase was set
1226
- * @fires loopUpdate - If loop state changes
1227
1025
  */
1228
1026
  detectAllTasksComplete(line) {
1229
- // When @fix_plan.md is active, only trust the file for todo status
1230
- // This prevents false positives from Claude saying "all done" in conversation
1231
1027
  if (this.isFileAuthoritative)
1232
1028
  return;
1233
- // Only trigger if line is a clear standalone completion message
1234
- // Avoid matching commentary like "once all tasks are complete..."
1235
1029
  if (!ALL_COMPLETE_PATTERN.test(line))
1236
1030
  return;
1237
- // Must be a reasonably short line (< 100 chars) to be a completion signal, not commentary
1238
1031
  if (line.length > 100)
1239
1032
  return;
1240
- // Skip if this looks like it's part of the original prompt (contains "output:")
1241
1033
  if (line.toLowerCase().includes('output:') || line.includes('<promise>'))
1242
1034
  return;
1243
- // Don't trigger if we haven't seen any todos yet
1244
1035
  if (this._todos.size === 0)
1245
1036
  return;
1246
- // Check if the count matches our todo count (e.g., "All 8 files created")
1247
1037
  const countMatch = line.match(ALL_COUNT_PATTERN);
1248
1038
  const parsedCount = countMatch ? parseInt(countMatch[1], 10) : NaN;
1249
1039
  const mentionedCount = Number.isNaN(parsedCount) ? null : parsedCount;
1250
1040
  const todoCount = this._todos.size;
1251
- // If a count is mentioned, it should match our todo count (within reason)
1252
1041
  if (mentionedCount !== null && Math.abs(mentionedCount - todoCount) > 2) {
1253
- // Count doesn't match our todos, might be unrelated
1254
1042
  return;
1255
1043
  }
1256
- // Mark all todos as complete
1257
1044
  let updated = false;
1258
1045
  for (const todo of this._todos.values()) {
1259
1046
  if (todo.status !== 'completed') {
@@ -1264,7 +1051,6 @@ export class RalphTracker extends EventEmitter {
1264
1051
  if (updated) {
1265
1052
  this.emit('todoUpdate', this.todos);
1266
1053
  }
1267
- // Emit completion if we have an expected phrase
1268
1054
  if (this._loopState.completionPhrase) {
1269
1055
  this._loopState.active = false;
1270
1056
  this._loopState.lastActivity = Date.now();
@@ -1273,25 +1059,18 @@ export class RalphTracker extends EventEmitter {
1273
1059
  }
1274
1060
  }
1275
1061
  /**
1276
- * Detect individual task completion signals
1277
- * e.g., "Task 8 is done", "marked as completed"
1278
- *
1279
- * NOTE: This is intentionally conservative to avoid jitter.
1280
- * Only marks a todo complete if we can match it by task number.
1062
+ * Detect individual task completion signals.
1281
1063
  */
1282
1064
  detectTaskCompletion(line) {
1283
- // When @fix_plan.md is active, only trust the file for todo status
1284
1065
  if (this.isFileAuthoritative)
1285
1066
  return;
1286
1067
  if (!TASK_DONE_PATTERN.test(line))
1287
1068
  return;
1288
- // Only act on explicit task number references like "Task 8 is done"
1289
1069
  const taskNumMatch = line.match(/task\s*#?(\d+)/i);
1290
1070
  if (taskNumMatch) {
1291
1071
  const taskNum = parseInt(taskNumMatch[1], 10);
1292
1072
  if (Number.isNaN(taskNum))
1293
1073
  return;
1294
- // Find the nth todo (by order) and mark it complete
1295
1074
  let count = 0;
1296
1075
  for (const [_id, todo] of this._todos) {
1297
1076
  count++;
@@ -1302,23 +1081,11 @@ export class RalphTracker extends EventEmitter {
1302
1081
  }
1303
1082
  }
1304
1083
  }
1305
- // Don't guess which todo to mark - let the checkbox detection handle it
1306
1084
  }
1307
1085
  /**
1308
1086
  * Check for multi-line patterns that might span line boundaries.
1309
- * Completion phrases can be split across PTY chunks.
1310
- *
1311
- * Handles cross-chunk promise tags by:
1312
- * 1. Checking combined buffer + new data for complete tags
1313
- * 2. Detecting partial tags at end of chunk and buffering
1314
- * 3. Clearing buffer when complete tag found or buffer gets stale
1315
- *
1316
- * @param data - The full data chunk (may contain multiple lines)
1317
1087
  */
1318
1088
  checkMultiLinePatterns(data) {
1319
- // Only try to complete a cross-chunk promise if we have a partial buffer.
1320
- // Without a partial buffer, complete tags are already handled by processLine
1321
- // via detectCompletionPhrase — re-detecting here would double-count.
1322
1089
  if (this._partialPromiseBuffer) {
1323
1090
  const combinedData = this._partialPromiseBuffer + data;
1324
1091
  const promiseMatch = combinedData.match(PROMISE_PATTERN);
@@ -1329,7 +1096,6 @@ export class RalphTracker extends EventEmitter {
1329
1096
  return;
1330
1097
  }
1331
1098
  }
1332
- // Check for partial promise tag at end of data (for next chunk)
1333
1099
  const partialMatch = data.match(PROMISE_PARTIAL_PATTERN);
1334
1100
  if (partialMatch) {
1335
1101
  const partialContent = partialMatch[0];
@@ -1346,32 +1112,16 @@ export class RalphTracker extends EventEmitter {
1346
1112
  }
1347
1113
  /**
1348
1114
  * Detect completion phrases in a line.
1349
- *
1350
- * Handles two formats:
1351
- * 1. Tagged: `<promise>PHRASE</promise>` - Processed via handleCompletionPhrase
1352
- * 2. Bare: Just `PHRASE` - Only if we already know the expected phrase
1353
- *
1354
- * Bare phrase detection avoids false positives by requiring:
1355
- * - The phrase was previously seen in tagged form
1356
- * - Line is standalone or ends with the phrase
1357
- * - Line doesn't look like prompt context
1358
- *
1359
- * @param line - Single line to check
1360
1115
  */
1361
1116
  detectCompletionPhrase(line) {
1362
- // First check for tagged phrase: <promise>PHRASE</promise>
1363
1117
  const match = line.match(PROMISE_PATTERN);
1364
1118
  if (match) {
1365
1119
  this.handleCompletionPhrase(match[1]);
1366
1120
  return;
1367
1121
  }
1368
- // If we have an expected completion phrase, also check for bare phrase
1369
- // This handles cases where Claude outputs "ALL_TASKS_DONE" without the tags
1370
1122
  const expectedPhrase = this._loopState.completionPhrase;
1371
1123
  if (expectedPhrase && line.toUpperCase().includes(expectedPhrase.toUpperCase())) {
1372
- // Avoid false positives: don't trigger on prompt context
1373
1124
  const isNotInPromptContext = !line.includes('<promise>') && !line.includes('output:');
1374
- // Also avoid triggering on "completion phrase is X" explanatory text
1375
1125
  const isNotExplanation = !line.toLowerCase().includes('completion phrase') && !line.toLowerCase().includes('output exactly');
1376
1126
  if (isNotInPromptContext && isNotExplanation) {
1377
1127
  this.handleBareCompletionPhrase(expectedPhrase);
@@ -1380,37 +1130,17 @@ export class RalphTracker extends EventEmitter {
1380
1130
  }
1381
1131
  /**
1382
1132
  * Handle a bare completion phrase (without XML tags).
1383
- *
1384
- * Only fires completion if:
1385
- * 1. The phrase was previously seen in tagged form (from prompt)
1386
- * 2. This is the first bare occurrence (prevents double-firing)
1387
- *
1388
- * When triggered:
1389
- * - Marks all todos as complete
1390
- * - Emits completionDetected event
1391
- * - Sets loop to inactive
1392
- *
1393
- * @param phrase - The completion phrase text
1394
- * @fires todoUpdate - If any todos marked complete
1395
- * @fires completionDetected - When completion triggered
1396
- * @fires loopUpdate - When loop state changes
1397
1133
  */
1398
1134
  handleBareCompletionPhrase(phrase) {
1399
- // Allow bare phrase detection if:
1400
- // 1. Loop is explicitly active (via startLoop()) - phrase was set programmatically
1401
- // 2. OR phrase was seen in tagged form (from terminal output)
1402
1135
  const taggedCount = this._completionPhraseCount.get(phrase) || 0;
1403
1136
  const loopExplicitlyActive = this._loopState.active;
1404
1137
  if (taggedCount === 0 && !loopExplicitlyActive)
1405
1138
  return;
1406
- // Track bare occurrences to avoid double-firing
1407
1139
  const bareKey = `bare:${phrase}`;
1408
1140
  const bareCount = (this._completionPhraseCount.get(bareKey) || 0) + 1;
1409
1141
  this._completionPhraseCount.set(bareKey, bareCount);
1410
- // Only fire once for bare phrase
1411
1142
  if (bareCount > 1)
1412
1143
  return;
1413
- // Mark all todos as complete (since we've reached the completion phrase)
1414
1144
  let updated = false;
1415
1145
  for (const todo of this._todos.values()) {
1416
1146
  if (todo.status !== 'completed') {
@@ -1421,36 +1151,26 @@ export class RalphTracker extends EventEmitter {
1421
1151
  if (updated) {
1422
1152
  this.emit('todoUpdate', this.todos);
1423
1153
  }
1424
- // Emit completion event
1425
1154
  this._loopState.active = false;
1426
1155
  this._loopState.lastActivity = Date.now();
1427
1156
  this.emit('completionDetected', phrase);
1428
1157
  this.emit('loopUpdate', this.loopState);
1429
1158
  }
1430
1159
  /**
1431
- * Handle a detected completion phrase
1432
- *
1433
- * Uses occurrence-based detection combined with confidence scoring
1434
- * to distinguish prompt from actual completion:
1435
- * - 1st occurrence: Store as expected phrase (likely in prompt)
1436
- * - 2nd occurrence OR high confidence: Emit completionDetected (actual completion)
1437
- * - If loop already active: Emit immediately (explicit loop start)
1160
+ * Handle a detected completion phrase.
1438
1161
  */
1439
1162
  handleCompletionPhrase(phrase) {
1440
1163
  const count = (this._completionPhraseCount.get(phrase) || 0) + 1;
1441
1164
  this._completionPhraseCount.set(phrase, count);
1442
1165
  // Trim completion phrase map if it exceeds the limit
1443
1166
  if (this._completionPhraseCount.size > MAX_COMPLETION_PHRASE_ENTRIES) {
1444
- // Keep only the most important entries (current expected phrase and highest counts)
1445
1167
  const entries = Array.from(this._completionPhraseCount.entries());
1446
- entries.sort((a, b) => b[1] - a[1]); // Sort by count descending
1168
+ entries.sort((a, b) => b[1] - a[1]);
1447
1169
  this._completionPhraseCount.clear();
1448
- // Keep top half of entries
1449
1170
  const keepCount = Math.floor(MAX_COMPLETION_PHRASE_ENTRIES / 2);
1450
1171
  for (let i = 0; i < Math.min(keepCount, entries.length); i++) {
1451
1172
  this._completionPhraseCount.set(entries[i][0], entries[i][1]);
1452
1173
  }
1453
- // Always keep the expected phrase if set
1454
1174
  if (this._loopState.completionPhrase && !this._completionPhraseCount.has(this._loopState.completionPhrase)) {
1455
1175
  this._completionPhraseCount.set(this._loopState.completionPhrase, 1);
1456
1176
  }
@@ -1459,23 +1179,16 @@ export class RalphTracker extends EventEmitter {
1459
1179
  if (!this._loopState.completionPhrase) {
1460
1180
  this._loopState.completionPhrase = phrase;
1461
1181
  this._loopState.lastActivity = Date.now();
1462
- // P1-002: Validate phrase and emit warning if risky
1463
1182
  this.validateCompletionPhrase(phrase);
1464
1183
  this.emit('loopUpdate', this.loopState);
1465
1184
  }
1466
- // Check for fuzzy match with primary phrase or any alternate phrase (P1-003)
1467
- // This handles minor variations like whitespace, case, underscores vs hyphens
1185
+ // Check for fuzzy match with primary phrase or any alternate phrase
1468
1186
  const matchedPhrase = this.findMatchingCompletionPhrase(phrase);
1469
1187
  if (matchedPhrase) {
1470
- // Use the matched phrase (canonical) for tracking
1471
1188
  const canonicalCount = this._completionPhraseCount.get(matchedPhrase) || 0;
1472
- // Require 2nd+ occurrence of canonical phrase OR explicitly active loop.
1473
- // First occurrence (count=1) is the prompt echo — not actual completion.
1474
1189
  if (canonicalCount >= 2 || this._loopState.active) {
1475
- // Mark as completion
1476
1190
  this._loopState.active = false;
1477
1191
  this._loopState.lastActivity = Date.now();
1478
- // Mark all todos as complete
1479
1192
  let updated = false;
1480
1193
  for (const todo of this._todos.values()) {
1481
1194
  if (todo.status !== 'completed') {
@@ -1491,9 +1204,7 @@ export class RalphTracker extends EventEmitter {
1491
1204
  return;
1492
1205
  }
1493
1206
  }
1494
- // Emit completion if loop is active OR this is 2nd+ occurrence
1495
1207
  if (this._loopState.active || count >= 2) {
1496
- // Mark all todos as complete when completion phrase is detected
1497
1208
  let updated = false;
1498
1209
  for (const todo of this._todos.values()) {
1499
1210
  if (todo.status !== 'completed') {
@@ -1512,39 +1223,17 @@ export class RalphTracker extends EventEmitter {
1512
1223
  }
1513
1224
  /**
1514
1225
  * Check if two phrases match with fuzzy tolerance.
1515
- * Handles variations in:
1516
- * - Case (COMPLETE vs Complete)
1517
- * - Whitespace (TASK_DONE vs TASK DONE)
1518
- * - Separators (TASK_DONE vs TASK-DONE)
1519
- * - Minor typos with Levenshtein distance (COMPLET vs COMPLETE)
1520
- *
1521
- * @param phrase1 - First phrase to compare
1522
- * @param phrase2 - Second phrase to compare
1523
- * @param maxDistance - Maximum edit distance for fuzzy match (default: 2)
1524
- * @returns True if phrases are fuzzy-equal
1525
1226
  */
1526
1227
  isFuzzyPhraseMatch(phrase1, phrase2, maxDistance = 2) {
1527
1228
  return fuzzyPhraseMatch(phrase1, phrase2, maxDistance);
1528
1229
  }
1529
1230
  /**
1530
1231
  * Validate a completion phrase and emit warnings if it's risky.
1531
- *
1532
- * P1-002: Configurable false positive prevention
1533
- *
1534
- * Checks for:
1535
- * - Common/generic phrases (DONE, COMPLETE, etc.)
1536
- * - Short phrases (< MIN_RECOMMENDED_PHRASE_LENGTH)
1537
- * - Numeric-only phrases
1538
- *
1539
- * @param phrase - The completion phrase to validate
1540
- * @fires phraseValidationWarning - When a risky phrase is detected
1541
1232
  */
1542
1233
  validateCompletionPhrase(phrase) {
1543
1234
  const normalized = phrase.toUpperCase().replace(/[\s_\-.]+/g, '');
1544
- // Generate a suggested unique phrase
1545
1235
  const uniqueSuffix = Date.now().toString(36).slice(-4).toUpperCase();
1546
1236
  const suggestedPhrase = `${phrase}_${uniqueSuffix}`;
1547
- // Check for common phrases
1548
1237
  if (COMMON_COMPLETION_PHRASES.has(normalized)) {
1549
1238
  console.warn(`[RalphTracker] Warning: Completion phrase "${phrase}" is very common and may cause false positives. Consider using: "${suggestedPhrase}"`);
1550
1239
  this.emit('phraseValidationWarning', {
@@ -1554,7 +1243,6 @@ export class RalphTracker extends EventEmitter {
1554
1243
  });
1555
1244
  return;
1556
1245
  }
1557
- // Check for short phrases
1558
1246
  if (normalized.length < MIN_RECOMMENDED_PHRASE_LENGTH) {
1559
1247
  console.warn(`[RalphTracker] Warning: Completion phrase "${phrase}" is too short (${normalized.length} chars). Consider using: "${suggestedPhrase}"`);
1560
1248
  this.emit('phraseValidationWarning', {
@@ -1564,7 +1252,6 @@ export class RalphTracker extends EventEmitter {
1564
1252
  });
1565
1253
  return;
1566
1254
  }
1567
- // Check for numeric-only phrases
1568
1255
  if (/^\d+$/.test(normalized)) {
1569
1256
  console.warn(`[RalphTracker] Warning: Completion phrase "${phrase}" is numeric-only and may cause false positives. Consider using: "${suggestedPhrase}"`);
1570
1257
  this.emit('phraseValidationWarning', {
@@ -1576,12 +1263,6 @@ export class RalphTracker extends EventEmitter {
1576
1263
  }
1577
1264
  /**
1578
1265
  * Activate the loop if not already active.
1579
- *
1580
- * Sets loop state to active and initializes counters.
1581
- * No-op if loop is already active.
1582
- *
1583
- * @returns True if loop was activated, false if already active
1584
- * @fires loopUpdate - When loop state changes
1585
1266
  */
1586
1267
  activateLoopIfNeeded() {
1587
1268
  if (this._loopState.active)
@@ -1597,131 +1278,77 @@ export class RalphTracker extends EventEmitter {
1597
1278
  }
1598
1279
  /**
1599
1280
  * Detect loop start and status indicators.
1600
- *
1601
- * Patterns detected:
1602
- * - Ralph loop start commands (`/ralph-loop:ralph-loop`)
1603
- * - Loop start messages (`Loop started at`, `Starting Ralph loop`)
1604
- * - Max iterations setting (`max-iterations 50`)
1605
- * - Iteration progress (`Iteration 5/50`, `[5/50]`)
1606
- * - Elapsed time (`Elapsed: 2.5 hours`)
1607
- * - Cycle count (`cycle #5`, `respawn cycle #3`)
1608
- * - TodoWrite tool usage
1609
- *
1610
- * @param line - Single line to check
1611
- * @fires loopUpdate - When any loop state changes
1612
1281
  */
1613
1282
  detectLoopStatus(line) {
1614
- // Check for Ralph loop start command (/ralph-loop:ralph-loop)
1615
- // or generic loop start patterns ("Loop started at", "Starting Ralph loop")
1616
1283
  if (RALPH_START_PATTERN.test(line) || LOOP_START_PATTERN.test(line)) {
1617
1284
  this.activateLoopIfNeeded();
1618
1285
  }
1619
- // Check for max iterations setting
1620
1286
  const maxIterMatch = line.match(MAX_ITERATIONS_PATTERN);
1621
1287
  if (maxIterMatch) {
1622
1288
  const maxIter = parseInt(maxIterMatch[1], 10);
1623
1289
  if (!Number.isNaN(maxIter) && maxIter > 0) {
1624
1290
  this._loopState.maxIterations = maxIter;
1625
1291
  this._loopState.lastActivity = Date.now();
1626
- // Use debounced emit for settings changes
1627
1292
  this.emitLoopUpdateDebounced();
1628
1293
  }
1629
1294
  }
1630
- // Check for iteration patterns: "Iteration 5/50", "[5/50]"
1631
1295
  const iterMatch = line.match(ITERATION_PATTERN);
1632
1296
  if (iterMatch) {
1633
- // Pattern captures: group 1&2 for "Iteration X/Y", group 3&4 for "[X/Y]"
1634
1297
  const currentIter = parseInt(iterMatch[1] || iterMatch[3], 10);
1635
1298
  const maxIterStr = iterMatch[2] || iterMatch[4];
1636
1299
  const maxIter = maxIterStr ? parseInt(maxIterStr, 10) : null;
1637
1300
  if (!Number.isNaN(currentIter)) {
1638
1301
  this.activateLoopIfNeeded();
1639
- // Track iteration changes for stall detection
1640
- if (currentIter !== this._lastObservedIteration) {
1641
- this._lastIterationChangeTime = Date.now();
1642
- this._lastObservedIteration = currentIter;
1643
- this._iterationStallWarned = false; // Reset warning on iteration change
1644
- // P1-004: Reset circuit breaker on successful iteration progress
1645
- // If we're making progress, the loop is healthy
1646
- if (this._circuitBreaker.state === 'HALF_OPEN' ||
1647
- this._circuitBreaker.consecutiveNoProgress > 0 ||
1648
- this._circuitBreaker.consecutiveSameError > 0 ||
1649
- this._circuitBreaker.consecutiveTestsFailure > 0) {
1650
- this._circuitBreaker.consecutiveNoProgress = 0;
1651
- this._circuitBreaker.consecutiveSameError = 0;
1652
- this._circuitBreaker.lastProgressIteration = currentIter;
1653
- if (this._circuitBreaker.state === 'HALF_OPEN') {
1654
- this._circuitBreaker.state = 'CLOSED';
1655
- this._circuitBreaker.reason = 'Iteration progress detected';
1656
- this._circuitBreaker.reasonCode = 'progress_detected';
1657
- this.emit('circuitBreakerUpdate', { ...this._circuitBreaker });
1658
- }
1659
- }
1302
+ // Track iteration changes for stall detection and circuit breaker
1303
+ if (currentIter !== this.stallDetector.getIterationStallMetrics().currentIteration) {
1304
+ this.stallDetector.notifyIterationChanged(currentIter);
1305
+ this.statusParser.notifyIterationProgress(currentIter);
1660
1306
  }
1661
1307
  this._loopState.cycleCount = currentIter;
1308
+ // Notify sub-modules of cycle count
1309
+ this.planTracker.notifyCycleCount(currentIter);
1310
+ this.statusParser.setCycleCount(currentIter);
1311
+ this.stallDetector.setLoopActive(true);
1662
1312
  if (maxIter !== null && !Number.isNaN(maxIter)) {
1663
1313
  this._loopState.maxIterations = maxIter;
1664
1314
  }
1665
1315
  this._loopState.lastActivity = Date.now();
1666
- // Use debounced emit for rapid iteration updates
1667
1316
  this.emitLoopUpdateDebounced();
1668
1317
  }
1669
1318
  }
1670
- // Check for elapsed time
1671
1319
  const elapsedMatch = line.match(ELAPSED_TIME_PATTERN);
1672
1320
  if (elapsedMatch) {
1673
1321
  this._loopState.elapsedHours = parseFloat(elapsedMatch[1]);
1674
1322
  this._loopState.lastActivity = Date.now();
1675
- // Use debounced emit for elapsed time updates
1676
1323
  this.emitLoopUpdateDebounced();
1677
1324
  }
1678
- // Check for cycle count (legacy pattern)
1679
1325
  const cycleMatch = line.match(CYCLE_PATTERN);
1680
1326
  if (cycleMatch) {
1681
1327
  const cycleNum = parseInt(cycleMatch[1] || cycleMatch[2], 10);
1682
1328
  if (!Number.isNaN(cycleNum) && cycleNum > this._loopState.cycleCount) {
1683
1329
  this._loopState.cycleCount = cycleNum;
1684
1330
  this._loopState.lastActivity = Date.now();
1685
- // Use debounced emit for cycle updates
1686
1331
  this.emitLoopUpdateDebounced();
1687
1332
  }
1688
1333
  }
1689
- // Check for TodoWrite tool usage - indicates active task tracking
1690
1334
  if (TODOWRITE_PATTERN.test(line)) {
1691
1335
  this._loopState.lastActivity = Date.now();
1692
- // Don't emit update just for activity, let todo detection handle it
1693
1336
  }
1694
1337
  }
1695
1338
  /**
1696
1339
  * Detect todo items in various formats from Claude Code output.
1697
- *
1698
- * Supported formats:
1699
- * - Format 1: Checkbox markdown (`- [ ] Task`, `- [x] Task`)
1700
- * - Format 2: Indicator icons (`Todo: ☐ Task`, `Todo: ✓ Task`)
1701
- * - Format 3: Status in parentheses (`- Task (pending)`)
1702
- * - Format 4: Native TodoWrite (`☐ Task`, `☒ Task`, `◐ Task`)
1703
- *
1704
- * Uses quick pre-check to skip lines that can't contain todos.
1705
- * Excludes tool invocations and Claude commentary patterns.
1706
- *
1707
- * @param line - Single line to check
1708
- * @fires todoUpdate - When any todos are detected or updated
1709
1340
  */
1710
1341
  detectTodoItems(line) {
1711
- // Pre-compute which pattern categories might match (60-75% faster)
1712
1342
  const hasCheckbox = line.includes('[');
1713
1343
  const hasTodoIndicator = line.includes('Todo:');
1714
1344
  const hasNativeCheckbox = line.includes('☐') || line.includes('☒') || line.includes('◐') || line.includes('✓');
1715
1345
  const hasStatus = line.includes('(pending)') || line.includes('(in_progress)') || line.includes('(completed)');
1716
1346
  const hasCheckmark = line.includes('✔');
1717
- // Quick check: skip lines that can't possibly contain todos
1718
1347
  if (!hasCheckbox && !hasTodoIndicator && !hasNativeCheckbox && !hasStatus && !hasCheckmark) {
1719
1348
  return;
1720
1349
  }
1721
1350
  let updated = false;
1722
1351
  let match;
1723
- // Format 1: Checkbox format "- [ ] Task" or "- [x] Task"
1724
- // Only scan if line contains '[' character
1725
1352
  if (hasCheckbox) {
1726
1353
  TODO_CHECKBOX_PATTERN.lastIndex = 0;
1727
1354
  while ((match = TODO_CHECKBOX_PATTERN.exec(line)) !== null) {
@@ -1732,8 +1359,6 @@ export class RalphTracker extends EventEmitter {
1732
1359
  updated = true;
1733
1360
  }
1734
1361
  }
1735
- // Format 2: Todo with indicator icons
1736
- // Only scan if line contains 'Todo:' prefix
1737
1362
  if (hasTodoIndicator) {
1738
1363
  TODO_INDICATOR_PATTERN.lastIndex = 0;
1739
1364
  while ((match = TODO_INDICATOR_PATTERN.exec(line)) !== null) {
@@ -1744,8 +1369,6 @@ export class RalphTracker extends EventEmitter {
1744
1369
  updated = true;
1745
1370
  }
1746
1371
  }
1747
- // Format 3: Status in parentheses
1748
- // Only scan if line contains status in parentheses
1749
1372
  if (hasStatus) {
1750
1373
  TODO_STATUS_PATTERN.lastIndex = 0;
1751
1374
  while ((match = TODO_STATUS_PATTERN.exec(line)) !== null) {
@@ -1755,18 +1378,14 @@ export class RalphTracker extends EventEmitter {
1755
1378
  updated = true;
1756
1379
  }
1757
1380
  }
1758
- // Format 4: Claude Code native TodoWrite output (☐, ☒, ◐)
1759
- // Only scan if line contains native checkbox icons
1760
1381
  if (hasNativeCheckbox) {
1761
1382
  TODO_NATIVE_PATTERN.lastIndex = 0;
1762
1383
  while ((match = TODO_NATIVE_PATTERN.exec(line)) !== null) {
1763
1384
  const icon = match[1];
1764
1385
  const content = match[2].trim();
1765
- // Skip if content matches exclude patterns (tool invocations, commentary)
1766
1386
  const shouldExclude = TODO_EXCLUDE_PATTERNS.some((pattern) => pattern.test(content));
1767
1387
  if (shouldExclude)
1768
1388
  continue;
1769
- // Skip if content is too short or looks like partial garbage
1770
1389
  if (content.length < 5)
1771
1390
  continue;
1772
1391
  const status = this.iconToStatus(icon);
@@ -1774,10 +1393,7 @@ export class RalphTracker extends EventEmitter {
1774
1393
  updated = true;
1775
1394
  }
1776
1395
  }
1777
- // Format 5: Claude Code checkmark-based TodoWrite output (✔ Task #N)
1778
- // Handles: "✔ Task #N created: content", "✔ #N content", "✔ Task #N updated: status → X"
1779
1396
  if (hasCheckmark) {
1780
- // Task creation: "✔ Task #1 created: Fix the bug"
1781
1397
  TODO_TASK_CREATED_PATTERN.lastIndex = 0;
1782
1398
  while ((match = TODO_TASK_CREATED_PATTERN.exec(line)) !== null) {
1783
1399
  const taskNum = parseInt(match[1], 10);
@@ -1789,13 +1405,11 @@ export class RalphTracker extends EventEmitter {
1789
1405
  updated = true;
1790
1406
  }
1791
1407
  }
1792
- // Task summary: "✔ #1 Fix the bug"
1793
1408
  TODO_TASK_SUMMARY_PATTERN.lastIndex = 0;
1794
1409
  while ((match = TODO_TASK_SUMMARY_PATTERN.exec(line)) !== null) {
1795
1410
  const taskNum = parseInt(match[1], 10);
1796
1411
  const content = match[2].trim();
1797
1412
  if (content.length >= 5) {
1798
- // Only register if not already known from a "created" line
1799
1413
  if (!this._taskNumberToContent.has(taskNum)) {
1800
1414
  this._taskNumberToContent.set(taskNum, content);
1801
1415
  this.enforceTaskMappingLimit();
@@ -1804,7 +1418,6 @@ export class RalphTracker extends EventEmitter {
1804
1418
  updated = true;
1805
1419
  }
1806
1420
  }
1807
- // Status update: "✔ Task #1 updated: status → completed"
1808
1421
  TODO_TASK_STATUS_PATTERN.lastIndex = 0;
1809
1422
  while ((match = TODO_TASK_STATUS_PATTERN.exec(line)) !== null) {
1810
1423
  const taskNum = parseInt(match[1], 10);
@@ -1816,19 +1429,15 @@ export class RalphTracker extends EventEmitter {
1816
1429
  updated = true;
1817
1430
  }
1818
1431
  }
1819
- // Plain checkmark: "✔ Create hello.txt" (no task number)
1820
- // Only match if numbered patterns didn't already match on this line
1821
1432
  if (!updated) {
1822
1433
  TODO_PLAIN_CHECKMARK_PATTERN.lastIndex = 0;
1823
1434
  while ((match = TODO_PLAIN_CHECKMARK_PATTERN.exec(line)) !== null) {
1824
1435
  const content = match[1].trim();
1825
- // Skip if content matches exclude patterns
1826
1436
  const shouldExclude = TODO_EXCLUDE_PATTERNS.some((pattern) => pattern.test(content));
1827
1437
  if (shouldExclude)
1828
1438
  continue;
1829
1439
  if (content.length < 5)
1830
1440
  continue;
1831
- // Skip status/created/updated prefixed content (already handled above)
1832
1441
  if (/^(Task\s*#\d+|#\d+)\s/.test(content))
1833
1442
  continue;
1834
1443
  this.upsertTodo(content, 'completed');
@@ -1837,68 +1446,46 @@ export class RalphTracker extends EventEmitter {
1837
1446
  }
1838
1447
  }
1839
1448
  if (updated) {
1840
- // Use debounced emit to batch rapid todo updates and reduce UI jitter
1841
1449
  this.emitTodoUpdateDebounced();
1842
1450
  }
1843
1451
  }
1844
1452
  /**
1845
1453
  * Convert a todo icon character to its corresponding status.
1846
- *
1847
- * Icon mappings:
1848
- * - Completed: `✓`, `✅`, `☒`, `◉`, `●`
1849
- * - In Progress: `◐`, `⏳`, `⌛`, `🔄`
1850
- * - Pending: `☐`, `○`, and anything else (default)
1851
- *
1852
- * @param icon - Single character icon
1853
- * @returns Corresponding RalphTodoStatus
1854
1454
  */
1855
1455
  iconToStatus(icon) {
1856
1456
  switch (icon) {
1857
1457
  case '✓':
1858
1458
  case '✅':
1859
- case '☒': // Claude Code checked checkbox
1860
- case '◉': // Filled circle (completed)
1861
- case '●': // Solid circle (completed)
1459
+ case '☒':
1460
+ case '◉':
1461
+ case '●':
1862
1462
  return 'completed';
1863
- case '◐': // Half-filled circle (in progress)
1463
+ case '◐':
1864
1464
  case '⏳':
1865
1465
  case '⌛':
1866
1466
  case '🔄':
1867
1467
  return 'in_progress';
1868
- case '☐': // Claude Code empty checkbox
1869
- case '○': // Empty circle
1468
+ case '☐':
1469
+ case '○':
1870
1470
  default:
1871
1471
  return 'pending';
1872
1472
  }
1873
1473
  }
1874
1474
  /**
1875
1475
  * Parse priority from todo content.
1876
- * P1-008: Enhanced keyword-based priority inference.
1877
- *
1878
- * Priority levels:
1879
- * - P0 (Critical): Explicit P0, "critical", "blocker", "urgent", "security", "crash", "broken"
1880
- * - P1 (High): Explicit P1, "important", "high priority", "bug", "fix", "error", "fail"
1881
- * - P2 (Medium): Explicit P2, "nice to have", "low priority", "refactor", "cleanup", "improve"
1882
- *
1883
- * @param content - Todo content text
1884
- * @returns Parsed priority level or null
1885
1476
  */
1886
1477
  parsePriority(content) {
1887
1478
  const upper = content.toUpperCase();
1888
- // Check P0 first (highest priority wins)
1889
- // Uses pre-compiled module-level patterns for performance
1890
1479
  for (const pattern of P0_PRIORITY_PATTERNS) {
1891
1480
  if (pattern.test(upper)) {
1892
1481
  return 'P0';
1893
1482
  }
1894
1483
  }
1895
- // Check P1
1896
1484
  for (const pattern of P1_PRIORITY_PATTERNS) {
1897
1485
  if (pattern.test(upper)) {
1898
1486
  return 'P1';
1899
1487
  }
1900
1488
  }
1901
- // Check P2
1902
1489
  for (const pattern of P2_PRIORITY_PATTERNS) {
1903
1490
  if (pattern.test(upper)) {
1904
1491
  return 'P2';
@@ -1908,82 +1495,51 @@ export class RalphTracker extends EventEmitter {
1908
1495
  }
1909
1496
  /**
1910
1497
  * Add a new todo item or update an existing one.
1911
- *
1912
- * Behavior:
1913
- * - Content is cleaned (ANSI removed, whitespace collapsed)
1914
- * - Content under 5 chars is skipped
1915
- * - ID is generated from normalized content (stable hash)
1916
- * - Priority is parsed from content (P0/P1/P2, Critical, High Priority, etc.)
1917
- * - Existing item: Updates status and timestamp
1918
- * - New item: Adds to map, evicts oldest if at MAX_TODOS_PER_SESSION
1919
- *
1920
- * @param content - Raw todo content text
1921
- * @param status - Status to set
1922
1498
  */
1923
1499
  upsertTodo(content, status) {
1924
- // Skip empty or whitespace-only content
1925
1500
  if (!content || !content.trim())
1926
1501
  return;
1927
- // Clean content: remove ANSI codes, collapse whitespace, trim
1928
- const cleanContent = content
1929
- .replace(ANSI_ESCAPE_PATTERN_SIMPLE, '') // Remove ANSI escape codes
1930
- .replace(/\s+/g, ' ') // Collapse whitespace
1931
- .trim();
1502
+ const cleanContent = content.replace(ANSI_ESCAPE_PATTERN_SIMPLE, '').replace(/\s+/g, ' ').trim();
1932
1503
  if (cleanContent.length < 5)
1933
- return; // Skip very short content
1934
- // Parse priority from content
1504
+ return;
1935
1505
  const priority = this.parsePriority(cleanContent);
1936
- // P1-009: Estimate complexity for duration tracking
1937
1506
  const estimatedComplexity = this.estimateComplexity(cleanContent);
1938
- // Generate a stable ID from normalized content
1939
1507
  const id = this.generateTodoId(cleanContent);
1940
1508
  const existing = this._todos.get(id);
1941
1509
  if (existing) {
1942
- // P1-009: Track status transitions for progress estimation
1943
1510
  const wasCompleted = existing.status === 'completed';
1944
1511
  const isNowCompleted = status === 'completed';
1945
1512
  const wasInProgress = existing.status === 'in_progress';
1946
1513
  const isNowInProgress = status === 'in_progress';
1947
- // Update existing todo (exact match by ID)
1948
1514
  existing.status = status;
1949
1515
  existing.detectedAt = Date.now();
1950
- // Update priority if parsed (don't overwrite with null)
1951
1516
  if (priority)
1952
1517
  existing.priority = priority;
1953
- // Update complexity estimate if not already set
1954
1518
  if (!existing.estimatedComplexity) {
1955
1519
  existing.estimatedComplexity = estimatedComplexity;
1956
1520
  }
1957
- // P1-009: Track completion time
1958
1521
  if (!wasCompleted && isNowCompleted) {
1959
1522
  this.recordTodoCompletion(id);
1960
1523
  }
1961
- // P1-009: Start tracking when status changes to in_progress
1962
1524
  if (!wasInProgress && isNowInProgress) {
1963
1525
  this.startTrackingTodo(id);
1964
1526
  }
1965
1527
  }
1966
1528
  else {
1967
- // P1-007: Check for similar existing todo (deduplication)
1968
1529
  const similar = this.findSimilarTodo(cleanContent);
1969
1530
  if (similar) {
1970
- // P1-009: Track status transitions on similar todo
1971
1531
  const wasCompleted = similar.status === 'completed';
1972
1532
  const isNowCompleted = status === 'completed';
1973
1533
  const wasInProgress = similar.status === 'in_progress';
1974
1534
  const isNowInProgress = status === 'in_progress';
1975
- // Update similar todo instead of creating duplicate
1976
1535
  similar.status = status;
1977
1536
  similar.detectedAt = Date.now();
1978
- // Update priority if new content has priority and existing doesn't
1979
1537
  if (priority && !similar.priority) {
1980
1538
  similar.priority = priority;
1981
1539
  }
1982
- // Keep the longer/more descriptive content
1983
1540
  if (cleanContent.length > similar.content.length) {
1984
1541
  similar.content = cleanContent;
1985
1542
  }
1986
- // P1-009: Track completion time
1987
1543
  if (!wasCompleted && isNowCompleted) {
1988
1544
  this.recordTodoCompletion(similar.id);
1989
1545
  }
@@ -1992,20 +1548,17 @@ export class RalphTracker extends EventEmitter {
1992
1548
  }
1993
1549
  return;
1994
1550
  }
1995
- // Add new todo with guaranteed eviction if at capacity
1996
- // Use while loop to ensure we always have room (handles edge case where findOldestTodo returns undefined)
1997
1551
  while (this._todos.size >= MAX_TODOS_PER_SESSION) {
1998
1552
  const oldest = this.findOldestTodo();
1999
1553
  if (oldest) {
2000
1554
  this._todos.delete(oldest.id);
2001
1555
  }
2002
1556
  else {
2003
- // Safety valve: if somehow no oldest found, clear a random entry
2004
1557
  const firstKey = this._todos.keys().next().value;
2005
1558
  if (firstKey)
2006
1559
  this._todos.delete(firstKey);
2007
1560
  else
2008
- break; // Map is empty somehow, exit loop
1561
+ break;
2009
1562
  }
2010
1563
  }
2011
1564
  const estimatedDurationMs = this.getEstimatedDuration(estimatedComplexity);
@@ -2018,7 +1571,6 @@ export class RalphTracker extends EventEmitter {
2018
1571
  estimatedComplexity,
2019
1572
  estimatedDurationMs,
2020
1573
  });
2021
- // P1-009: Start tracking if already in_progress
2022
1574
  if (status === 'in_progress') {
2023
1575
  this.startTrackingTodo(id);
2024
1576
  }
@@ -2026,71 +1578,39 @@ export class RalphTracker extends EventEmitter {
2026
1578
  }
2027
1579
  /**
2028
1580
  * Normalize todo content for consistent matching.
2029
- *
2030
- * Normalization steps:
2031
- * 1. Collapse multiple whitespace to single space
2032
- * 2. Remove special characters (keep alphanumeric + basic punctuation)
2033
- * 3. Trim whitespace
2034
- * 4. Convert to lowercase
2035
- *
2036
- * This prevents duplicate todos from terminal rendering artifacts.
2037
- *
2038
- * @param content - Raw todo content
2039
- * @returns Normalized lowercase string
2040
1581
  */
2041
1582
  normalizeTodoContent(content) {
2042
1583
  if (!content)
2043
1584
  return '';
2044
1585
  return content
2045
- .replace(/\s+/g, ' ') // Collapse whitespace
2046
- .replace(/[^a-zA-Z0-9\s.,!?'"-]/g, '') // Remove special chars (keep punctuation)
1586
+ .replace(/\s+/g, ' ')
1587
+ .replace(/[^a-zA-Z0-9\s.,!?'"-]/g, '')
2047
1588
  .trim()
2048
1589
  .toLowerCase();
2049
1590
  }
2050
1591
  /**
2051
1592
  * Calculate similarity between two strings.
2052
- *
2053
- * P1-007: Uses a hybrid approach combining:
2054
- * 1. Levenshtein-based similarity for edit-distance tolerance
2055
- * 2. Bigram (Dice coefficient) for reordering tolerance
2056
- * Returns the maximum of both methods.
2057
- *
2058
- * @param str1 - First string (will be normalized)
2059
- * @param str2 - Second string (will be normalized)
2060
- * @returns Similarity score from 0.0 (no similarity) to 1.0 (identical)
2061
1593
  */
2062
1594
  calculateSimilarity(str1, str2) {
2063
1595
  const norm1 = this.normalizeTodoContent(str1);
2064
1596
  const norm2 = this.normalizeTodoContent(str2);
2065
- // Identical after normalization
2066
1597
  if (norm1 === norm2)
2067
1598
  return 1.0;
2068
- // If either is empty, no similarity
2069
1599
  if (!norm1 || !norm2)
2070
1600
  return 0.0;
2071
- // Method 1: Levenshtein-based similarity (good for typos/minor edits)
2072
1601
  const levenshteinSim = stringSimilarity(norm1, norm2);
2073
- // Method 2: Bigram/Dice similarity (good for word reordering)
2074
1602
  const bigramSim = this.calculateBigramSimilarity(norm1, norm2);
2075
- // Return the higher of the two scores
2076
1603
  return Math.max(levenshteinSim, bigramSim);
2077
1604
  }
2078
1605
  /**
2079
1606
  * Calculate bigram (Dice coefficient) similarity.
2080
- * Good for detecting near-duplicates with word reordering.
2081
- *
2082
- * @param norm1 - First normalized string
2083
- * @param norm2 - Second normalized string
2084
- * @returns Similarity score from 0.0 to 1.0
2085
1607
  */
2086
1608
  calculateBigramSimilarity(norm1, norm2) {
2087
- // Short strings: use simple character overlap
2088
1609
  if (norm1.length < 3 || norm2.length < 3) {
2089
1610
  const shorter = norm1.length <= norm2.length ? norm1 : norm2;
2090
1611
  const longer = norm1.length > norm2.length ? norm1 : norm2;
2091
1612
  return longer.includes(shorter) ? 0.9 : 0.0;
2092
1613
  }
2093
- // Extract bigrams (pairs of consecutive characters)
2094
1614
  const getBigrams = (s) => {
2095
1615
  const bigrams = new Set();
2096
1616
  for (let i = 0; i < s.length - 1; i++) {
@@ -2100,14 +1620,12 @@ export class RalphTracker extends EventEmitter {
2100
1620
  };
2101
1621
  const bigrams1 = getBigrams(norm1);
2102
1622
  const bigrams2 = getBigrams(norm2);
2103
- // Count intersection
2104
1623
  let intersection = 0;
2105
1624
  for (const bigram of bigrams1) {
2106
1625
  if (bigrams2.has(bigram)) {
2107
1626
  intersection++;
2108
1627
  }
2109
1628
  }
2110
- // Dice coefficient: 2 * intersection / (total bigrams)
2111
1629
  const totalBigrams = bigrams1.size + bigrams2.size;
2112
1630
  if (totalBigrams === 0)
2113
1631
  return 0.0;
@@ -2115,32 +1633,18 @@ export class RalphTracker extends EventEmitter {
2115
1633
  }
2116
1634
  /**
2117
1635
  * Find an existing todo that is similar to the given content.
2118
- * Returns the most similar todo if similarity >= threshold.
2119
- *
2120
- * Deduplication is intentionally conservative:
2121
- * - Short strings (< 30 chars): require 95% similarity (nearly identical)
2122
- * - Medium strings (30-60 chars): require 90% similarity
2123
- * - Longer strings: use default 85% threshold
2124
- *
2125
- * This prevents over-aggressive deduplication of brief, numbered items
2126
- * like "Task 1", "Task 2" while still catching true duplicates.
2127
- *
2128
- * @param content - New todo content to check against existing todos
2129
- * @returns Similar todo item if found, undefined otherwise
2130
1636
  */
2131
1637
  findSimilarTodo(content) {
2132
1638
  const normalized = this.normalizeTodoContent(content);
2133
- // Determine appropriate threshold based on string length
2134
- // Shorter strings need higher threshold to avoid false positives
2135
1639
  let threshold;
2136
1640
  if (normalized.length < 30) {
2137
- threshold = 0.95; // Very strict for short strings
1641
+ threshold = 0.95;
2138
1642
  }
2139
1643
  else if (normalized.length < 60) {
2140
- threshold = 0.9; // Strict for medium strings
1644
+ threshold = 0.9;
2141
1645
  }
2142
1646
  else {
2143
- threshold = TODO_SIMILARITY_THRESHOLD; // 0.85 for longer strings
1647
+ threshold = TODO_SIMILARITY_THRESHOLD;
2144
1648
  }
2145
1649
  let bestMatch;
2146
1650
  let bestSimilarity = 0;
@@ -2156,14 +1660,9 @@ export class RalphTracker extends EventEmitter {
2156
1660
  // ========== P1-009: Progress Estimation Methods ==========
2157
1661
  /**
2158
1662
  * Estimate complexity of a todo based on content keywords.
2159
- * Used for duration estimation.
2160
- *
2161
- * @param content - Todo content text
2162
- * @returns Complexity category
2163
1663
  */
2164
1664
  estimateComplexity(content) {
2165
1665
  const lower = content.toLowerCase();
2166
- // Trivial: Simple fixes, typos, documentation
2167
1666
  const trivialPatterns = [
2168
1667
  /\btypo\b/,
2169
1668
  /\bspelling\b/,
@@ -2172,7 +1671,6 @@ export class RalphTracker extends EventEmitter {
2172
1671
  /\brename\b/,
2173
1672
  /\bformat(?:ting)?\b/,
2174
1673
  ];
2175
- // Complex: Architecture, refactoring, security, testing
2176
1674
  const complexPatterns = [
2177
1675
  /\barchitect(?:ure)?\b/,
2178
1676
  /\brefactor\b/,
@@ -2185,7 +1683,6 @@ export class RalphTracker extends EventEmitter {
2185
1683
  /\boptimiz(?:e|ation)\b/,
2186
1684
  /\bmultiple\s+files?\b/,
2187
1685
  ];
2188
- // Moderate: Bugs, features, enhancements
2189
1686
  const moderatePatterns = [/\bbug\b/, /\bfeature\b/, /\benhance(?:ment)?\b/, /\bimplement\b/, /\badd\b/, /\bfix\b/];
2190
1687
  for (const pattern of complexPatterns) {
2191
1688
  if (pattern.test(lower))
@@ -2203,13 +1700,8 @@ export class RalphTracker extends EventEmitter {
2203
1700
  }
2204
1701
  /**
2205
1702
  * Get estimated duration for a complexity level (ms).
2206
- * Based on historical patterns from similar tasks.
2207
- *
2208
- * @param complexity - Complexity category
2209
- * @returns Estimated duration in milliseconds
2210
1703
  */
2211
1704
  getEstimatedDuration(complexity) {
2212
- // If we have historical data, use average adjusted by complexity
2213
1705
  const avgTime = this.getAverageCompletionTime();
2214
1706
  if (avgTime !== null) {
2215
1707
  const multipliers = {
@@ -2220,18 +1712,16 @@ export class RalphTracker extends EventEmitter {
2220
1712
  };
2221
1713
  return Math.round(avgTime * multipliers[complexity]);
2222
1714
  }
2223
- // Default estimates (in ms) based on typical task durations
2224
1715
  const defaults = {
2225
- trivial: 1 * 60 * 1000, // 1 minute
2226
- simple: 3 * 60 * 1000, // 3 minutes
2227
- moderate: 10 * 60 * 1000, // 10 minutes
2228
- complex: 30 * 60 * 1000, // 30 minutes
1716
+ trivial: 1 * 60 * 1000,
1717
+ simple: 3 * 60 * 1000,
1718
+ moderate: 10 * 60 * 1000,
1719
+ complex: 30 * 60 * 1000,
2229
1720
  };
2230
1721
  return defaults[complexity];
2231
1722
  }
2232
1723
  /**
2233
1724
  * Get average completion time from historical data.
2234
- * @returns Average time in ms, or null if no data
2235
1725
  */
2236
1726
  getAverageCompletionTime() {
2237
1727
  if (this._completionTimes.length === 0)
@@ -2241,14 +1731,12 @@ export class RalphTracker extends EventEmitter {
2241
1731
  }
2242
1732
  /**
2243
1733
  * Record a todo completion for progress tracking.
2244
- * @param todoId - ID of the completed todo
2245
1734
  */
2246
1735
  recordTodoCompletion(todoId) {
2247
1736
  const startTime = this._todoStartTimes.get(todoId);
2248
1737
  if (startTime) {
2249
1738
  const duration = Date.now() - startTime;
2250
1739
  this._completionTimes.push(duration);
2251
- // Keep only recent completion times
2252
1740
  while (this._completionTimes.length > RalphTracker.MAX_COMPLETION_TIMES) {
2253
1741
  this._completionTimes.shift();
2254
1742
  }
@@ -2257,23 +1745,17 @@ export class RalphTracker extends EventEmitter {
2257
1745
  }
2258
1746
  /**
2259
1747
  * Start tracking a todo for duration estimation.
2260
- * @param todoId - ID of the todo being started
2261
1748
  */
2262
1749
  startTrackingTodo(todoId) {
2263
1750
  if (!this._todoStartTimes.has(todoId)) {
2264
1751
  this._todoStartTimes.set(todoId, Date.now());
2265
1752
  }
2266
- // Initialize session tracking if needed
2267
1753
  if (this._todosStartedAt === 0) {
2268
1754
  this._todosStartedAt = Date.now();
2269
1755
  }
2270
1756
  }
2271
1757
  /**
2272
1758
  * Get progress estimation for the todo list.
2273
- * P1-009: Provides completion percentage, estimated remaining time,
2274
- * and projected completion timestamp.
2275
- *
2276
- * @returns Progress estimation object
2277
1759
  */
2278
1760
  getTodoProgress() {
2279
1761
  const todos = Array.from(this._todos.values());
@@ -2282,19 +1764,16 @@ export class RalphTracker extends EventEmitter {
2282
1764
  const inProgress = todos.filter((t) => t.status === 'in_progress').length;
2283
1765
  const pending = todos.filter((t) => t.status === 'pending').length;
2284
1766
  const percentComplete = total > 0 ? Math.round((completed / total) * 100) : 0;
2285
- // Calculate estimated remaining time
2286
1767
  let estimatedRemainingMs = null;
2287
1768
  let avgCompletionTimeMs = null;
2288
1769
  let projectedCompletionAt = null;
2289
1770
  avgCompletionTimeMs = this.getAverageCompletionTime();
2290
1771
  if (total > 0 && completed > 0) {
2291
- // Method 1: Use historical average if available
2292
1772
  if (avgCompletionTimeMs !== null) {
2293
1773
  const remaining = total - completed;
2294
1774
  estimatedRemainingMs = remaining * avgCompletionTimeMs;
2295
1775
  }
2296
1776
  else {
2297
- // Method 2: Calculate based on elapsed time and progress
2298
1777
  const elapsed = Date.now() - this._todosStartedAt;
2299
1778
  if (elapsed > 0 && completed > 0) {
2300
1779
  const timePerTodo = elapsed / completed;
@@ -2303,13 +1782,11 @@ export class RalphTracker extends EventEmitter {
2303
1782
  estimatedRemainingMs = Math.round(remaining * timePerTodo);
2304
1783
  }
2305
1784
  }
2306
- // Calculate projected completion timestamp
2307
1785
  if (estimatedRemainingMs !== null) {
2308
1786
  projectedCompletionAt = Date.now() + estimatedRemainingMs;
2309
1787
  }
2310
1788
  }
2311
1789
  else if (total > 0 && completed === 0) {
2312
- // No completions yet - use complexity-based estimates
2313
1790
  let totalEstimate = 0;
2314
1791
  for (const todo of todos) {
2315
1792
  if (todo.status !== 'completed') {
@@ -2331,35 +1808,17 @@ export class RalphTracker extends EventEmitter {
2331
1808
  projectedCompletionAt,
2332
1809
  };
2333
1810
  }
2334
- /**
2335
- * Generate a stable ID from todo content using djb2 hash.
2336
- *
2337
- * Uses the djb2 hash algorithm for good distribution across strings.
2338
- * Content is normalized first to prevent duplicates from terminal artifacts.
2339
- *
2340
- * @param content - Todo content text
2341
- * @returns Stable ID in format `todo-{hash}` (base36 encoded)
2342
- */
2343
1811
  /**
2344
1812
  * Generate a stable ID from todo content using content hashing.
2345
- *
2346
- * P1-007: Uses centralized todoContentHash utility for consistency
2347
- * with deduplication logic.
2348
- *
2349
- * @param content - Todo content text
2350
- * @returns Unique ID string prefixed with "todo-"
2351
1813
  */
2352
1814
  generateTodoId(content) {
2353
1815
  if (!content)
2354
1816
  return 'todo-empty';
2355
- // Use centralized hashing utility
2356
1817
  const hash = todoContentHash(content);
2357
1818
  return `todo-${hash}`;
2358
1819
  }
2359
1820
  /**
2360
1821
  * Find the todo item with the oldest detectedAt timestamp.
2361
- * Used for LRU eviction when at MAX_TODOS_PER_SESSION limit.
2362
- * @returns Oldest todo item, or undefined if map is empty
2363
1822
  */
2364
1823
  findOldestTodo() {
2365
1824
  let oldest;
@@ -2372,7 +1831,6 @@ export class RalphTracker extends EventEmitter {
2372
1831
  }
2373
1832
  /**
2374
1833
  * Conditionally run cleanup, throttled to CLEANUP_THROTTLE_MS.
2375
- * Prevents cleanup from running on every data chunk (performance).
2376
1834
  */
2377
1835
  maybeCleanupExpiredTodos() {
2378
1836
  const now = Date.now();
@@ -2384,8 +1842,6 @@ export class RalphTracker extends EventEmitter {
2384
1842
  }
2385
1843
  /**
2386
1844
  * Remove todo items older than TODO_EXPIRY_MS.
2387
- * Emits todoUpdate if any items were removed.
2388
- * @fires todoUpdate - When expired items are removed
2389
1845
  */
2390
1846
  cleanupExpiredTodos() {
2391
1847
  const now = Date.now();
@@ -2398,25 +1854,16 @@ export class RalphTracker extends EventEmitter {
2398
1854
  if (toDelete.length > 0) {
2399
1855
  for (const id of toDelete) {
2400
1856
  this._todos.delete(id);
1857
+ this._todoStartTimes.delete(id);
2401
1858
  }
2402
1859
  this.emit('todoUpdate', this.todos);
2403
1860
  }
2404
1861
  }
2405
1862
  /**
2406
1863
  * Programmatically start a loop (external API).
2407
- *
2408
- * Use when starting a loop from outside terminal detection,
2409
- * such as from a user action or API call.
2410
- *
2411
- * Automatically enables the tracker if not already enabled.
2412
- *
2413
- * @param completionPhrase - Optional phrase that signals completion
2414
- * @param maxIterations - Optional maximum iteration count
2415
- * @fires enabled - If tracker was disabled
2416
- * @fires loopUpdate - When loop state changes
2417
1864
  */
2418
1865
  startLoop(completionPhrase, maxIterations) {
2419
- this.enable(); // Ensure tracker is enabled
1866
+ this.enable();
2420
1867
  this._loopState.active = true;
2421
1868
  this._loopState.startedAt = Date.now();
2422
1869
  this._loopState.cycleCount = 0;
@@ -2430,9 +1877,6 @@ export class RalphTracker extends EventEmitter {
2430
1877
  }
2431
1878
  /**
2432
1879
  * Update the maximum iteration count (external API).
2433
- *
2434
- * @param maxIterations - New max iterations, or null to remove limit
2435
- * @fires loopUpdate - When loop state changes
2436
1880
  */
2437
1881
  setMaxIterations(maxIterations) {
2438
1882
  this._loopState.maxIterations = maxIterations;
@@ -2440,11 +1884,7 @@ export class RalphTracker extends EventEmitter {
2440
1884
  this.emit('loopUpdate', this.loopState);
2441
1885
  }
2442
1886
  /**
2443
- * Configure the tracker from external state (e.g. ralph plugin config).
2444
- * Only updates fields that are provided, leaving others unchanged.
2445
- *
2446
- * @param config - Partial configuration to apply
2447
- * @fires loopUpdate - When loop state changes
1887
+ * Configure the tracker from external state.
2448
1888
  */
2449
1889
  configure(config) {
2450
1890
  if (config.enabled !== undefined) {
@@ -2461,11 +1901,6 @@ export class RalphTracker extends EventEmitter {
2461
1901
  }
2462
1902
  /**
2463
1903
  * Programmatically stop the loop (external API).
2464
- *
2465
- * Sets loop to inactive. Does not disable the tracker
2466
- * or clear todos - use reset() or clear() for that.
2467
- *
2468
- * @fires loopUpdate - When loop state changes
2469
1904
  */
2470
1905
  stopLoop() {
2471
1906
  this._loopState.active = false;
@@ -2474,12 +1909,10 @@ export class RalphTracker extends EventEmitter {
2474
1909
  }
2475
1910
  /**
2476
1911
  * Enforce size limit on _taskNumberToContent map.
2477
- * Removes lowest task numbers (oldest tasks) when limit exceeded.
2478
1912
  */
2479
1913
  enforceTaskMappingLimit() {
2480
1914
  if (this._taskNumberToContent.size <= MAX_TASK_MAPPINGS)
2481
1915
  return;
2482
- // Sort keys and remove lowest (oldest) task numbers
2483
1916
  const sortedKeys = Array.from(this._taskNumberToContent.keys()).sort((a, b) => a - b);
2484
1917
  const keysToRemove = sortedKeys.slice(0, this._taskNumberToContent.size - MAX_TASK_MAPPINGS);
2485
1918
  for (const key of keysToRemove) {
@@ -2488,21 +1921,12 @@ export class RalphTracker extends EventEmitter {
2488
1921
  }
2489
1922
  /**
2490
1923
  * Clear all state and disable the tracker.
2491
- *
2492
- * Use when the session is cleared or closed.
2493
- * Resets everything to initial disabled state.
2494
- *
2495
- * @fires loopUpdate - With initial state
2496
- * @fires todoUpdate - With empty array
2497
1924
  */
2498
1925
  clear() {
2499
- // Clear debounce timers to prevent stale emissions after clear
2500
1926
  this.clearDebounceTimers();
2501
- // Stop fix plan file watcher to prevent memory leak
2502
- this.stopWatchingFixPlan();
2503
- // Stop iteration stall detection timer to prevent leak
2504
- this.stopIterationStallDetection();
2505
- this._loopState = createInitialRalphTrackerState(); // This sets enabled: false
1927
+ this.fixPlanWatcher.stop();
1928
+ this.stallDetector.stopIterationStallDetection();
1929
+ this._loopState = createInitialRalphTrackerState();
2506
1930
  this._todos.clear();
2507
1931
  this._taskNumberToContent.clear();
2508
1932
  this._todoStartTimes.clear();
@@ -2510,26 +1934,15 @@ export class RalphTracker extends EventEmitter {
2510
1934
  this._lineBuffer = '';
2511
1935
  this._partialPromiseBuffer = '';
2512
1936
  this._completionPhraseCount.clear();
2513
- // Clear RALPH_STATUS block and circuit breaker state
2514
- this._statusBlockBuffer = [];
2515
- this._inStatusBlock = false;
2516
- this._lastStatusBlock = null;
2517
- this._completionIndicators = 0;
2518
- this._exitGateMet = false;
2519
- this._totalFilesModified = 0;
2520
- this._totalTasksCompleted = 0;
2521
- this._circuitBreaker = createInitialCircuitBreakerStatus();
1937
+ // Clear sub-module state
1938
+ this.statusParser.fullReset();
1939
+ this.planTracker.fullReset();
1940
+ this.stallDetector.reset();
2522
1941
  this.emit('loopUpdate', this.loopState);
2523
1942
  this.emit('todoUpdate', this.todos);
2524
1943
  }
2525
1944
  /**
2526
1945
  * Get aggregated statistics about tracked todos.
2527
- *
2528
- * @returns Object with counts by status:
2529
- * - total: Total number of tracked todos
2530
- * - pending: Todos not yet started
2531
- * - inProgress: Todos currently in progress
2532
- * - completed: Finished todos
2533
1946
  */
2534
1947
  getTodoStats() {
2535
1948
  let pending = 0;
@@ -2557,786 +1970,39 @@ export class RalphTracker extends EventEmitter {
2557
1970
  }
2558
1971
  /**
2559
1972
  * Restore tracker state from persisted data.
2560
- *
2561
- * Use after loading state from StateStore. Handles backwards
2562
- * compatibility by defaulting missing `enabled` flag to false.
2563
- *
2564
- * Note: Does not emit events (caller should handle if needed).
2565
- *
2566
- * @param loopState - Persisted loop state object
2567
- * @param todos - Persisted todo items array
2568
1973
  */
2569
1974
  restoreState(loopState, todos) {
2570
- // Ensure enabled flag exists (backwards compatibility)
2571
1975
  this._loopState = {
2572
1976
  ...loopState,
2573
- enabled: loopState.enabled ?? false, // Override after spread for backwards compat
1977
+ enabled: loopState.enabled ?? false,
2574
1978
  };
2575
1979
  this._todos.clear();
2576
1980
  for (const todo of todos) {
2577
- // Backwards compatibility: ensure priority field exists
2578
1981
  this._todos.set(todo.id, {
2579
1982
  ...todo,
2580
1983
  priority: todo.priority ?? null,
2581
1984
  });
2582
1985
  }
2583
1986
  }
2584
- // ========== RALPH_STATUS Block Detection ==========
2585
- /**
2586
- * Process a line for RALPH_STATUS block detection.
2587
- * Buffers lines between ---RALPH_STATUS--- and ---END_RALPH_STATUS---
2588
- * then parses the complete block.
2589
- *
2590
- * @param line - Single line to process (already trimmed)
2591
- * @fires statusBlockDetected - When a complete block is parsed
2592
- */
2593
- processStatusBlockLine(line) {
2594
- // Check for block start
2595
- if (RALPH_STATUS_START_PATTERN.test(line)) {
2596
- this._inStatusBlock = true;
2597
- this._statusBlockBuffer = [];
2598
- return;
2599
- }
2600
- // Check for block end
2601
- if (this._inStatusBlock && RALPH_STATUS_END_PATTERN.test(line)) {
2602
- this._inStatusBlock = false;
2603
- this.parseStatusBlock(this._statusBlockBuffer);
2604
- this._statusBlockBuffer = [];
2605
- return;
2606
- }
2607
- // Buffer lines while in block
2608
- if (this._inStatusBlock) {
2609
- this._statusBlockBuffer.push(line);
2610
- }
2611
- }
2612
- /**
2613
- * Parse buffered RALPH_STATUS block lines into structured data.
2614
- *
2615
- * P1-004: Enhanced with schema validation and error recovery
2616
- *
2617
- * @param lines - Array of lines between block markers
2618
- * @fires statusBlockDetected - When parsing succeeds
2619
- */
2620
- parseStatusBlock(lines) {
2621
- const block = {
2622
- parsedAt: Date.now(),
2623
- };
2624
- const parseErrors = [];
2625
- const unknownFields = [];
2626
- for (const line of lines) {
2627
- const trimmedLine = line.trim();
2628
- if (!trimmedLine)
2629
- continue;
2630
- // Track whether this line matched any known field
2631
- let matched = false;
2632
- // STATUS field (required)
2633
- const statusMatch = trimmedLine.match(RALPH_STATUS_FIELD_PATTERN);
2634
- if (statusMatch) {
2635
- const value = statusMatch[1].toUpperCase();
2636
- if (['IN_PROGRESS', 'COMPLETE', 'BLOCKED'].includes(value)) {
2637
- block.status = value;
2638
- }
2639
- else {
2640
- parseErrors.push(`Invalid STATUS value: "${value}". Expected: IN_PROGRESS, COMPLETE, or BLOCKED`);
2641
- }
2642
- matched = true;
2643
- }
2644
- // TASKS_COMPLETED_THIS_LOOP field
2645
- const tasksMatch = trimmedLine.match(RALPH_TASKS_COMPLETED_PATTERN);
2646
- if (tasksMatch) {
2647
- const value = parseInt(tasksMatch[1], 10);
2648
- if (!Number.isNaN(value) && value >= 0) {
2649
- block.tasksCompletedThisLoop = value;
2650
- }
2651
- else {
2652
- parseErrors.push(`Invalid TASKS_COMPLETED_THIS_LOOP value: "${tasksMatch[1]}". Expected: non-negative integer`);
2653
- }
2654
- matched = true;
2655
- }
2656
- // FILES_MODIFIED field
2657
- const filesMatch = trimmedLine.match(RALPH_FILES_MODIFIED_PATTERN);
2658
- if (filesMatch) {
2659
- const value = parseInt(filesMatch[1], 10);
2660
- if (!Number.isNaN(value) && value >= 0) {
2661
- block.filesModified = value;
2662
- }
2663
- else {
2664
- parseErrors.push(`Invalid FILES_MODIFIED value: "${filesMatch[1]}". Expected: non-negative integer`);
2665
- }
2666
- matched = true;
2667
- }
2668
- // TESTS_STATUS field
2669
- const testsMatch = trimmedLine.match(RALPH_TESTS_STATUS_PATTERN);
2670
- if (testsMatch) {
2671
- const value = testsMatch[1].toUpperCase();
2672
- if (['PASSING', 'FAILING', 'NOT_RUN'].includes(value)) {
2673
- block.testsStatus = value;
2674
- }
2675
- else {
2676
- parseErrors.push(`Invalid TESTS_STATUS value: "${value}". Expected: PASSING, FAILING, or NOT_RUN`);
2677
- }
2678
- matched = true;
2679
- }
2680
- // WORK_TYPE field
2681
- const workMatch = trimmedLine.match(RALPH_WORK_TYPE_PATTERN);
2682
- if (workMatch) {
2683
- const value = workMatch[1].toUpperCase();
2684
- if (['IMPLEMENTATION', 'TESTING', 'DOCUMENTATION', 'REFACTORING'].includes(value)) {
2685
- block.workType = value;
2686
- }
2687
- else {
2688
- parseErrors.push(`Invalid WORK_TYPE value: "${value}". Expected: IMPLEMENTATION, TESTING, DOCUMENTATION, or REFACTORING`);
2689
- }
2690
- matched = true;
2691
- }
2692
- // EXIT_SIGNAL field
2693
- const exitMatch = trimmedLine.match(RALPH_EXIT_SIGNAL_PATTERN);
2694
- if (exitMatch) {
2695
- block.exitSignal = exitMatch[1].toLowerCase() === 'true';
2696
- matched = true;
2697
- }
2698
- // RECOMMENDATION field
2699
- const recMatch = trimmedLine.match(RALPH_RECOMMENDATION_PATTERN);
2700
- if (recMatch) {
2701
- block.recommendation = recMatch[1].trim();
2702
- matched = true;
2703
- }
2704
- // Track unknown fields for debugging (only if looks like a field)
2705
- if (!matched && trimmedLine.includes(':')) {
2706
- const fieldName = trimmedLine.split(':')[0].trim().toUpperCase();
2707
- if (fieldName && !['#', '//'].some((c) => fieldName.startsWith(c))) {
2708
- unknownFields.push(fieldName);
2709
- }
2710
- }
2711
- }
2712
- // Log parse errors if any
2713
- if (parseErrors.length > 0) {
2714
- console.warn(`[RalphTracker] RALPH_STATUS parse errors:\n - ${parseErrors.join('\n - ')}`);
2715
- }
2716
- // Log unknown fields if any
2717
- if (unknownFields.length > 0) {
2718
- console.warn(`[RalphTracker] RALPH_STATUS unknown fields: ${unknownFields.join(', ')}`);
2719
- }
2720
- // Validate required field: STATUS
2721
- if (block.status === undefined) {
2722
- console.warn('[RalphTracker] RALPH_STATUS block missing required STATUS field, skipping');
2723
- return;
2724
- }
2725
- // Fill in defaults for missing optional fields
2726
- const fullBlock = {
2727
- status: block.status,
2728
- tasksCompletedThisLoop: block.tasksCompletedThisLoop ?? 0,
2729
- filesModified: block.filesModified ?? 0,
2730
- testsStatus: block.testsStatus ?? 'NOT_RUN',
2731
- workType: block.workType ?? 'IMPLEMENTATION',
2732
- exitSignal: block.exitSignal ?? false,
2733
- recommendation: block.recommendation ?? '',
2734
- parsedAt: block.parsedAt,
2735
- };
2736
- this._lastStatusBlock = fullBlock;
2737
- this.handleStatusBlock(fullBlock);
2738
- }
2739
- /**
2740
- * Handle a parsed RALPH_STATUS block.
2741
- * Updates circuit breaker, checks exit conditions.
2742
- *
2743
- * @param block - Parsed status block
2744
- * @fires statusBlockDetected - With the block data
2745
- * @fires circuitBreakerUpdate - If state changes
2746
- * @fires exitGateMet - If dual-condition exit triggered
2747
- */
2748
- handleStatusBlock(block) {
2749
- // Auto-enable tracker when we see a status block
2750
- if (!this._loopState.enabled && !this._autoEnableDisabled) {
2751
- this.enable();
2752
- }
2753
- // Update cumulative counts
2754
- this._totalFilesModified += block.filesModified;
2755
- this._totalTasksCompleted += block.tasksCompletedThisLoop;
2756
- // Check for progress (for circuit breaker)
2757
- const hasProgress = block.filesModified > 0 || block.tasksCompletedThisLoop > 0;
2758
- // Update circuit breaker
2759
- this.updateCircuitBreaker(hasProgress, block.testsStatus, block.status);
2760
- // Check completion indicators
2761
- if (block.status === 'COMPLETE') {
2762
- this._completionIndicators++;
2763
- }
2764
- // Check dual-condition exit gate
2765
- if (block.exitSignal && this._completionIndicators >= 2 && !this._exitGateMet) {
2766
- this._exitGateMet = true;
2767
- this.emit('exitGateMet', {
2768
- completionIndicators: this._completionIndicators,
2769
- exitSignal: true,
2770
- });
2771
- }
2772
- // Update loop state
2773
- this._loopState.lastActivity = Date.now();
2774
- // Emit the status block
2775
- this.emit('statusBlockDetected', block);
2776
- this.emitLoopUpdateDebounced();
2777
- }
2778
- // ========== Circuit Breaker ==========
2779
- /**
2780
- * Update circuit breaker state based on iteration results.
2781
- *
2782
- * @param hasProgress - Whether this iteration made progress
2783
- * @param testsStatus - Current test status
2784
- * @param status - Overall status from RALPH_STATUS
2785
- * @fires circuitBreakerUpdate - If state changes
2786
- */
2787
- updateCircuitBreaker(hasProgress, testsStatus, status) {
2788
- const prevState = this._circuitBreaker.state;
2789
- if (hasProgress) {
2790
- // Progress detected - reset counters, possibly close circuit
2791
- this._circuitBreaker.consecutiveNoProgress = 0;
2792
- this._circuitBreaker.consecutiveSameError = 0;
2793
- this._circuitBreaker.lastProgressIteration = this._loopState.cycleCount;
2794
- if (this._circuitBreaker.state === 'HALF_OPEN') {
2795
- this._circuitBreaker.state = 'CLOSED';
2796
- this._circuitBreaker.reason = 'Progress detected, circuit closed';
2797
- this._circuitBreaker.reasonCode = 'progress_detected';
2798
- }
2799
- }
2800
- else {
2801
- // No progress
2802
- this._circuitBreaker.consecutiveNoProgress++;
2803
- // State transitions based on consecutive no-progress
2804
- if (this._circuitBreaker.state === 'CLOSED') {
2805
- if (this._circuitBreaker.consecutiveNoProgress >= 3) {
2806
- this._circuitBreaker.state = 'OPEN';
2807
- this._circuitBreaker.reason = `No progress for ${this._circuitBreaker.consecutiveNoProgress} iterations`;
2808
- this._circuitBreaker.reasonCode = 'no_progress_open';
2809
- }
2810
- else if (this._circuitBreaker.consecutiveNoProgress >= 2) {
2811
- this._circuitBreaker.state = 'HALF_OPEN';
2812
- this._circuitBreaker.reason = 'Warning: no progress detected';
2813
- this._circuitBreaker.reasonCode = 'no_progress_warning';
2814
- }
2815
- }
2816
- else if (this._circuitBreaker.state === 'HALF_OPEN') {
2817
- if (this._circuitBreaker.consecutiveNoProgress >= 3) {
2818
- this._circuitBreaker.state = 'OPEN';
2819
- this._circuitBreaker.reason = `No progress for ${this._circuitBreaker.consecutiveNoProgress} iterations`;
2820
- this._circuitBreaker.reasonCode = 'no_progress_open';
2821
- }
2822
- }
2823
- }
2824
- // Track tests failure
2825
- if (testsStatus === 'FAILING') {
2826
- this._circuitBreaker.consecutiveTestsFailure++;
2827
- if (this._circuitBreaker.consecutiveTestsFailure >= 5 && this._circuitBreaker.state !== 'OPEN') {
2828
- this._circuitBreaker.state = 'OPEN';
2829
- this._circuitBreaker.reason = `Tests failing for ${this._circuitBreaker.consecutiveTestsFailure} iterations`;
2830
- this._circuitBreaker.reasonCode = 'tests_failing_too_long';
2831
- }
2832
- }
2833
- else {
2834
- this._circuitBreaker.consecutiveTestsFailure = 0;
2835
- }
2836
- // Track blocked status
2837
- if (status === 'BLOCKED' && this._circuitBreaker.state !== 'OPEN') {
2838
- this._circuitBreaker.state = 'OPEN';
2839
- this._circuitBreaker.reason = 'Claude reported BLOCKED status';
2840
- this._circuitBreaker.reasonCode = 'same_error_repeated';
2841
- }
2842
- // Emit if state changed
2843
- if (prevState !== this._circuitBreaker.state) {
2844
- this._circuitBreaker.lastTransitionAt = Date.now();
2845
- this.emit('circuitBreakerUpdate', { ...this._circuitBreaker });
2846
- }
2847
- }
2848
- /**
2849
- * Manually reset circuit breaker to CLOSED state.
2850
- * Use when user acknowledges the issue is resolved.
2851
- *
2852
- * @fires circuitBreakerUpdate
2853
- */
2854
- resetCircuitBreaker() {
2855
- this._circuitBreaker = createInitialCircuitBreakerStatus();
2856
- this._circuitBreaker.reason = 'Manual reset';
2857
- this._circuitBreaker.reasonCode = 'manual_reset';
2858
- this.emit('circuitBreakerUpdate', { ...this._circuitBreaker });
2859
- }
2860
- /**
2861
- * Get current circuit breaker status.
2862
- */
2863
- get circuitBreakerStatus() {
2864
- return { ...this._circuitBreaker };
2865
- }
2866
- /**
2867
- * Get last parsed RALPH_STATUS block.
2868
- */
2869
- get lastStatusBlock() {
2870
- return this._lastStatusBlock ? { ...this._lastStatusBlock } : null;
2871
- }
2872
- /**
2873
- * Get cumulative stats from status blocks.
2874
- */
2875
- get cumulativeStats() {
2876
- return {
2877
- filesModified: this._totalFilesModified,
2878
- tasksCompleted: this._totalTasksCompleted,
2879
- completionIndicators: this._completionIndicators,
2880
- };
2881
- }
2882
- /**
2883
- * Whether dual-condition exit gate has been met.
2884
- */
2885
- get exitGateMet() {
2886
- return this._exitGateMet;
2887
- }
2888
- // ========== Completion Indicator Detection ==========
2889
- /**
2890
- * Check line for completion indicators (natural language patterns).
2891
- * Used for dual-condition exit gate.
2892
- *
2893
- * @param line - Line to check
2894
- */
2895
- detectCompletionIndicators(line) {
2896
- for (const pattern of COMPLETION_INDICATOR_PATTERNS) {
2897
- if (pattern.test(line)) {
2898
- this._completionIndicators++;
2899
- break; // Only count once per line
2900
- }
2901
- }
2902
- }
2903
- // ========== @fix_plan.md Generation & Import ==========
2904
- /**
2905
- * Generate @fix_plan.md content from current todos.
2906
- * Groups todos by priority and status.
2907
- *
2908
- * @returns Markdown content for @fix_plan.md
2909
- */
2910
- generateFixPlanMarkdown() {
2911
- const todos = this.todos;
2912
- const lines = ['# Fix Plan', ''];
2913
- // Group by priority
2914
- const p0 = [];
2915
- const p1 = [];
2916
- const p2 = [];
2917
- const noPriority = [];
2918
- const completed = [];
2919
- for (const todo of todos) {
2920
- if (todo.status === 'completed') {
2921
- completed.push(todo);
2922
- }
2923
- else if (todo.priority === 'P0') {
2924
- p0.push(todo);
2925
- }
2926
- else if (todo.priority === 'P1') {
2927
- p1.push(todo);
2928
- }
2929
- else if (todo.priority === 'P2') {
2930
- p2.push(todo);
2931
- }
2932
- else {
2933
- noPriority.push(todo);
2934
- }
2935
- }
2936
- // High Priority (P0)
2937
- if (p0.length > 0) {
2938
- lines.push('## High Priority (P0)');
2939
- for (const todo of p0) {
2940
- const checkbox = todo.status === 'in_progress' ? '[-]' : '[ ]';
2941
- lines.push(`- ${checkbox} ${todo.content}`);
2942
- }
2943
- lines.push('');
2944
- }
2945
- // Standard (P1)
2946
- if (p1.length > 0) {
2947
- lines.push('## Standard (P1)');
2948
- for (const todo of p1) {
2949
- const checkbox = todo.status === 'in_progress' ? '[-]' : '[ ]';
2950
- lines.push(`- ${checkbox} ${todo.content}`);
2951
- }
2952
- lines.push('');
2953
- }
2954
- // Nice to Have (P2)
2955
- if (p2.length > 0) {
2956
- lines.push('## Nice to Have (P2)');
2957
- for (const todo of p2) {
2958
- const checkbox = todo.status === 'in_progress' ? '[-]' : '[ ]';
2959
- lines.push(`- ${checkbox} ${todo.content}`);
2960
- }
2961
- lines.push('');
2962
- }
2963
- // Tasks (no priority)
2964
- if (noPriority.length > 0) {
2965
- lines.push('## Tasks');
2966
- for (const todo of noPriority) {
2967
- const checkbox = todo.status === 'in_progress' ? '[-]' : '[ ]';
2968
- lines.push(`- ${checkbox} ${todo.content}`);
2969
- }
2970
- lines.push('');
2971
- }
2972
- // Completed
2973
- if (completed.length > 0) {
2974
- lines.push('## Completed');
2975
- for (const todo of completed) {
2976
- lines.push(`- [x] ${todo.content}`);
2977
- }
2978
- lines.push('');
2979
- }
2980
- return lines.join('\n');
2981
- }
2982
- /**
2983
- * Parse @fix_plan.md content and import todos.
2984
- * Replaces current todos with imported ones.
2985
- *
2986
- * @param content - Markdown content from @fix_plan.md
2987
- * @returns Number of todos imported
2988
- */
2989
- importFixPlanMarkdown(content) {
2990
- const lines = content.split('\n');
2991
- const newTodos = [];
2992
- let currentPriority = null;
2993
- // Patterns for section headers
2994
- const p0HeaderPattern = /^##\s*(High Priority|Critical|P0)/i;
2995
- const p1HeaderPattern = /^##\s*(Standard|P1|Medium Priority)/i;
2996
- const p2HeaderPattern = /^##\s*(Nice to Have|P2|Low Priority)/i;
2997
- const completedHeaderPattern = /^##\s*Completed/i;
2998
- const tasksHeaderPattern = /^##\s*Tasks/i;
2999
- // Pattern for todo items
3000
- const todoPattern = /^-\s*\[([ x-])\]\s*(.+)$/;
3001
- let inCompletedSection = false;
3002
- for (const line of lines) {
3003
- const trimmed = line.trim();
3004
- // Check for section headers
3005
- if (p0HeaderPattern.test(trimmed)) {
3006
- currentPriority = 'P0';
3007
- inCompletedSection = false;
3008
- continue;
3009
- }
3010
- if (p1HeaderPattern.test(trimmed)) {
3011
- currentPriority = 'P1';
3012
- inCompletedSection = false;
3013
- continue;
3014
- }
3015
- if (p2HeaderPattern.test(trimmed)) {
3016
- currentPriority = 'P2';
3017
- inCompletedSection = false;
3018
- continue;
3019
- }
3020
- if (completedHeaderPattern.test(trimmed)) {
3021
- inCompletedSection = true;
3022
- continue;
3023
- }
3024
- if (tasksHeaderPattern.test(trimmed)) {
3025
- currentPriority = null;
3026
- inCompletedSection = false;
3027
- continue;
3028
- }
3029
- // Parse todo item
3030
- const match = trimmed.match(todoPattern);
3031
- if (match) {
3032
- const [, checkboxState, content] = match;
3033
- let status;
3034
- if (inCompletedSection || checkboxState === 'x' || checkboxState === 'X') {
3035
- status = 'completed';
3036
- }
3037
- else if (checkboxState === '-') {
3038
- status = 'in_progress';
3039
- }
3040
- else {
3041
- status = 'pending';
3042
- }
3043
- // Parse priority from content if not in a priority section
3044
- const parsedPriority = inCompletedSection ? null : currentPriority || this.parsePriority(content);
3045
- const id = this.generateTodoId(content);
3046
- newTodos.push({
3047
- id,
3048
- content: content.trim(),
3049
- status,
3050
- detectedAt: Date.now(),
3051
- priority: parsedPriority,
3052
- });
3053
- }
3054
- }
3055
- // Replace current todos with imported ones
3056
- this._todos.clear();
3057
- for (const todo of newTodos) {
3058
- this._todos.set(todo.id, todo);
3059
- }
3060
- // Emit update
3061
- this.emit('todoUpdate', this.todos);
3062
- return newTodos.length;
3063
- }
3064
- // ========== Enhanced Plan Management Methods ==========
3065
- /**
3066
- * Initialize plan tasks from generated plan items.
3067
- * Called when wizard generates a new plan.
3068
- */
3069
- initializePlanTasks(items) {
3070
- // Save current plan to history before replacing
3071
- if (this._planTasks.size > 0) {
3072
- this._savePlanToHistory('Plan replaced with new generation');
3073
- }
3074
- // Clear and rebuild
3075
- this._planTasks.clear();
3076
- this._planVersion++;
3077
- items.forEach((item, idx) => {
3078
- const id = item.id || `task-${idx}`;
3079
- const task = {
3080
- id,
3081
- content: item.content,
3082
- priority: item.priority || null,
3083
- verificationCriteria: item.verificationCriteria,
3084
- testCommand: item.testCommand,
3085
- dependencies: item.dependencies || [],
3086
- status: 'pending',
3087
- attempts: 0,
3088
- version: this._planVersion,
3089
- tddPhase: item.tddPhase,
3090
- pairedWith: item.pairedWith,
3091
- complexity: item.complexity,
3092
- };
3093
- this._planTasks.set(id, task);
3094
- });
3095
- this.emit('planInitialized', { version: this._planVersion, taskCount: this._planTasks.size });
3096
- }
3097
- /**
3098
- * Update a specific plan task's status, attempts, or error.
3099
- */
3100
- updatePlanTask(taskId, update) {
3101
- const task = this._planTasks.get(taskId);
3102
- if (!task) {
3103
- return { success: false, error: 'Task not found' };
3104
- }
3105
- if (update.status) {
3106
- task.status = update.status;
3107
- if (update.status === 'completed') {
3108
- task.completedAt = Date.now();
3109
- }
3110
- }
3111
- if (update.error) {
3112
- task.lastError = update.error;
3113
- }
3114
- if (update.incrementAttempts) {
3115
- task.attempts++;
3116
- // After 3 failed attempts, mark as blocked and emit warning
3117
- if (task.attempts >= 3 && task.status === 'failed') {
3118
- task.status = 'blocked';
3119
- this.emit('taskBlocked', {
3120
- taskId,
3121
- content: task.content,
3122
- attempts: task.attempts,
3123
- lastError: task.lastError,
3124
- });
3125
- }
3126
- }
3127
- // Update blocked tasks when a dependency completes
3128
- if (update.status === 'completed') {
3129
- this._unblockDependentTasks(taskId);
3130
- }
3131
- // Check for checkpoint
3132
- this._checkForCheckpoint();
3133
- this.emit('planTaskUpdate', { taskId, task });
3134
- return { success: true, task };
3135
- }
3136
- /**
3137
- * Unblock tasks that were waiting on a completed dependency.
3138
- */
3139
- _unblockDependentTasks(completedTaskId) {
3140
- for (const [_, task] of this._planTasks) {
3141
- if (task.dependencies.includes(completedTaskId)) {
3142
- // Check if all dependencies are now complete
3143
- const allDepsComplete = task.dependencies.every((depId) => {
3144
- const dep = this._planTasks.get(depId);
3145
- return dep && dep.status === 'completed';
3146
- });
3147
- if (allDepsComplete && task.status === 'blocked') {
3148
- task.status = 'pending';
3149
- this.emit('taskUnblocked', { taskId: task.id });
3150
- }
3151
- }
3152
- }
3153
- }
3154
- /**
3155
- * Check if current iteration is a checkpoint and emit review if so.
3156
- */
3157
- _checkForCheckpoint() {
3158
- const currentIteration = this._loopState.cycleCount;
3159
- if (this._checkpointIterations.includes(currentIteration) && currentIteration > this._lastCheckpointIteration) {
3160
- this._lastCheckpointIteration = currentIteration;
3161
- const checkpoint = this.generateCheckpointReview();
3162
- this.emit('planCheckpoint', checkpoint);
3163
- }
3164
- }
3165
- /**
3166
- * Generate a checkpoint review summarizing plan progress and stuck tasks.
3167
- */
3168
- generateCheckpointReview() {
3169
- const tasks = Array.from(this._planTasks.values());
3170
- const summary = {
3171
- total: tasks.length,
3172
- completed: tasks.filter((t) => t.status === 'completed').length,
3173
- failed: tasks.filter((t) => t.status === 'failed').length,
3174
- blocked: tasks.filter((t) => t.status === 'blocked').length,
3175
- pending: tasks.filter((t) => t.status === 'pending').length,
3176
- inProgress: tasks.filter((t) => t.status === 'in_progress').length,
3177
- };
3178
- // Find stuck tasks (3+ attempts or blocked)
3179
- const stuckTasks = tasks
3180
- .filter((t) => t.attempts >= 3 || t.status === 'blocked')
3181
- .map((t) => ({
3182
- id: t.id,
3183
- content: t.content,
3184
- attempts: t.attempts,
3185
- lastError: t.lastError,
3186
- }));
3187
- // Generate recommendations
3188
- const recommendations = [];
3189
- if (stuckTasks.length > 0) {
3190
- recommendations.push(`${stuckTasks.length} task(s) are stuck. Consider breaking them into smaller steps.`);
3191
- }
3192
- if (summary.failed > summary.completed && summary.total > 5) {
3193
- recommendations.push('More tasks have failed than completed. Review approach and consider plan adjustment.');
3194
- }
3195
- const progressPercent = summary.total > 0 ? Math.round((summary.completed / summary.total) * 100) : 0;
3196
- if (progressPercent < 20 && this._loopState.cycleCount > 10) {
3197
- recommendations.push('Progress is slow. Consider simplifying tasks or reviewing dependencies.');
3198
- }
3199
- if (summary.total > 0 && summary.blocked > summary.total / 3) {
3200
- recommendations.push('Many tasks are blocked. Review dependency chain for bottlenecks.');
3201
- }
3202
- return {
3203
- iteration: this._loopState.cycleCount,
3204
- timestamp: Date.now(),
3205
- summary,
3206
- stuckTasks,
3207
- recommendations,
3208
- };
3209
- }
3210
- /**
3211
- * Save current plan state to history.
3212
- */
3213
- _savePlanToHistory(summary) {
3214
- // Clone current tasks
3215
- const tasksCopy = new Map();
3216
- for (const [id, task] of this._planTasks) {
3217
- tasksCopy.set(id, { ...task });
3218
- }
3219
- this._planHistory.push({
3220
- version: this._planVersion,
3221
- timestamp: Date.now(),
3222
- tasks: tasksCopy,
3223
- summary,
3224
- });
3225
- // Limit history size
3226
- if (this._planHistory.length > MAX_PLAN_HISTORY) {
3227
- this._planHistory.shift();
3228
- }
3229
- }
3230
- /**
3231
- * Get plan version history.
3232
- */
3233
- getPlanHistory() {
3234
- return this._planHistory.map((h) => {
3235
- const tasks = Array.from(h.tasks.values());
3236
- return {
3237
- version: h.version,
3238
- timestamp: h.timestamp,
3239
- summary: h.summary,
3240
- stats: {
3241
- total: tasks.length,
3242
- completed: tasks.filter((t) => t.status === 'completed').length,
3243
- failed: tasks.filter((t) => t.status === 'failed').length,
3244
- },
3245
- };
3246
- });
3247
- }
3248
- /**
3249
- * Rollback to a previous plan version.
3250
- */
3251
- rollbackToVersion(version) {
3252
- const historyEntry = this._planHistory.find((h) => h.version === version);
3253
- if (!historyEntry) {
3254
- return { success: false, error: `Version ${version} not found in history` };
3255
- }
3256
- // Save current state first
3257
- this._savePlanToHistory(`Rolled back from v${this._planVersion} to v${version}`);
3258
- // Restore the historical version
3259
- this._planTasks.clear();
3260
- for (const [id, task] of historyEntry.tasks) {
3261
- // Reset execution state for retry
3262
- this._planTasks.set(id, {
3263
- ...task,
3264
- status: task.status === 'completed' ? 'completed' : 'pending',
3265
- attempts: task.status === 'completed' ? task.attempts : 0,
3266
- lastError: undefined,
3267
- });
3268
- }
3269
- this._planVersion++;
3270
- this.emit('planRollback', { version, newVersion: this._planVersion });
3271
- return { success: true, plan: Array.from(this._planTasks.values()) };
3272
- }
3273
- /**
3274
- * Add a new task to the plan (for runtime adaptation).
3275
- */
3276
- addPlanTask(task) {
3277
- // Generate unique ID
3278
- const existingIds = Array.from(this._planTasks.keys());
3279
- const prefix = task.priority || 'P1';
3280
- let counter = existingIds.filter((id) => id.startsWith(prefix)).length + 1;
3281
- let id = `${prefix}-${String(counter).padStart(3, '0')}`;
3282
- while (this._planTasks.has(id)) {
3283
- counter++;
3284
- id = `${prefix}-${String(counter).padStart(3, '0')}`;
3285
- }
3286
- const newTask = {
3287
- id,
3288
- content: task.content,
3289
- priority: task.priority || null,
3290
- verificationCriteria: task.verificationCriteria || 'Task completed successfully',
3291
- dependencies: task.dependencies || [],
3292
- status: 'pending',
3293
- attempts: 0,
3294
- version: this._planVersion,
3295
- };
3296
- this._planTasks.set(id, newTask);
3297
- this.emit('planTaskAdded', { task: newTask });
3298
- return { task: newTask };
3299
- }
3300
- /**
3301
- * Get all plan tasks.
3302
- */
3303
- getPlanTasks() {
3304
- return Array.from(this._planTasks.values());
3305
- }
3306
- /**
3307
- * Get current plan version.
3308
- */
3309
- get planVersion() {
3310
- return this._planVersion;
3311
- }
3312
- /**
3313
- * Check if checkpoint review is due for current iteration.
3314
- */
3315
- isCheckpointDue() {
3316
- const currentIteration = this._loopState.cycleCount;
3317
- return this._checkpointIterations.includes(currentIteration) && currentIteration > this._lastCheckpointIteration;
3318
- }
3319
1987
  /**
3320
1988
  * Clean up all resources and release memory.
3321
- *
3322
- * Call this when the session is being destroyed to prevent memory leaks.
3323
- * Stops file watchers, clears all timers, data, and removes event listeners.
3324
1989
  */
3325
1990
  destroy() {
3326
- this.clearDebounceTimers();
3327
- this.stopWatchingFixPlan();
3328
- this.stopIterationStallDetection();
1991
+ this.cleanup.dispose();
1992
+ this._todoDeb.dispose();
1993
+ this._loopDeb.dispose();
1994
+ this.fixPlanWatcher.destroy();
1995
+ this.stallDetector.destroy();
1996
+ this.statusParser.destroy();
1997
+ this.planTracker.destroy();
3329
1998
  this._todos.clear();
3330
1999
  this._taskNumberToContent.clear();
3331
2000
  this._todoStartTimes.clear();
3332
2001
  this._alternateCompletionPhrases.clear();
3333
2002
  this._completionPhraseCount.clear();
3334
- this._planTasks.clear();
3335
2003
  this._completionTimes.length = 0;
3336
2004
  this._lineBuffer = '';
3337
2005
  this._partialPromiseBuffer = '';
3338
- this._statusBlockBuffer.length = 0;
3339
- this._planHistory.length = 0;
3340
2006
  this.removeAllListeners();
3341
2007
  }
3342
2008
  }