aicodeman 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/README.md +91 -0
  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 +36 -0
  29. package/dist/config/server-timing.d.ts.map +1 -0
  30. package/dist/config/server-timing.js +51 -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.map +1 -1
  45. package/dist/hooks-config.js +7 -6
  46. package/dist/hooks-config.js.map +1 -1
  47. package/dist/image-watcher.d.ts +4 -4
  48. package/dist/image-watcher.d.ts.map +1 -1
  49. package/dist/image-watcher.js +17 -30
  50. package/dist/image-watcher.js.map +1 -1
  51. package/dist/index.js +1 -2
  52. package/dist/index.js.map +1 -1
  53. package/dist/plan-orchestrator.d.ts +2 -24
  54. package/dist/plan-orchestrator.d.ts.map +1 -1
  55. package/dist/plan-orchestrator.js.map +1 -1
  56. package/dist/push-store.d.ts +1 -1
  57. package/dist/push-store.d.ts.map +1 -1
  58. package/dist/push-store.js +4 -12
  59. package/dist/push-store.js.map +1 -1
  60. package/dist/ralph-fix-plan-watcher.d.ts +91 -0
  61. package/dist/ralph-fix-plan-watcher.d.ts.map +1 -0
  62. package/dist/ralph-fix-plan-watcher.js +326 -0
  63. package/dist/ralph-fix-plan-watcher.js.map +1 -0
  64. package/dist/ralph-plan-tracker.d.ts +201 -0
  65. package/dist/ralph-plan-tracker.d.ts.map +1 -0
  66. package/dist/ralph-plan-tracker.js +325 -0
  67. package/dist/ralph-plan-tracker.js.map +1 -0
  68. package/dist/ralph-stall-detector.d.ts +84 -0
  69. package/dist/ralph-stall-detector.d.ts.map +1 -0
  70. package/dist/ralph-stall-detector.js +139 -0
  71. package/dist/ralph-stall-detector.js.map +1 -0
  72. package/dist/ralph-status-parser.d.ts +141 -0
  73. package/dist/ralph-status-parser.d.ts.map +1 -0
  74. package/dist/ralph-status-parser.js +478 -0
  75. package/dist/ralph-status-parser.js.map +1 -0
  76. package/dist/ralph-tracker.d.ts +194 -685
  77. package/dist/ralph-tracker.d.ts.map +1 -1
  78. package/dist/ralph-tracker.js +349 -1713
  79. package/dist/ralph-tracker.js.map +1 -1
  80. package/dist/respawn-adaptive-timing.d.ts +61 -0
  81. package/dist/respawn-adaptive-timing.d.ts.map +1 -0
  82. package/dist/respawn-adaptive-timing.js +105 -0
  83. package/dist/respawn-adaptive-timing.js.map +1 -0
  84. package/dist/respawn-controller.d.ts +14 -101
  85. package/dist/respawn-controller.d.ts.map +1 -1
  86. package/dist/respawn-controller.js +155 -594
  87. package/dist/respawn-controller.js.map +1 -1
  88. package/dist/respawn-health.d.ts +54 -0
  89. package/dist/respawn-health.d.ts.map +1 -0
  90. package/dist/respawn-health.js +183 -0
  91. package/dist/respawn-health.js.map +1 -0
  92. package/dist/respawn-metrics.d.ts +81 -0
  93. package/dist/respawn-metrics.d.ts.map +1 -0
  94. package/dist/respawn-metrics.js +198 -0
  95. package/dist/respawn-metrics.js.map +1 -0
  96. package/dist/respawn-patterns.d.ts +45 -0
  97. package/dist/respawn-patterns.d.ts.map +1 -0
  98. package/dist/respawn-patterns.js +125 -0
  99. package/dist/respawn-patterns.js.map +1 -0
  100. package/dist/session-auto-ops.d.ts +89 -0
  101. package/dist/session-auto-ops.d.ts.map +1 -0
  102. package/dist/session-auto-ops.js +224 -0
  103. package/dist/session-auto-ops.js.map +1 -0
  104. package/dist/session-cli-builder.d.ts +62 -0
  105. package/dist/session-cli-builder.d.ts.map +1 -0
  106. package/dist/session-cli-builder.js +121 -0
  107. package/dist/session-cli-builder.js.map +1 -0
  108. package/dist/session-task-cache.d.ts +52 -0
  109. package/dist/session-task-cache.d.ts.map +1 -0
  110. package/dist/session-task-cache.js +90 -0
  111. package/dist/session-task-cache.js.map +1 -0
  112. package/dist/session.d.ts +2 -33
  113. package/dist/session.d.ts.map +1 -1
  114. package/dist/session.js +58 -309
  115. package/dist/session.js.map +1 -1
  116. package/dist/state-store.d.ts +9 -2
  117. package/dist/state-store.d.ts.map +1 -1
  118. package/dist/state-store.js +112 -39
  119. package/dist/state-store.js.map +1 -1
  120. package/dist/subagent-watcher.d.ts +16 -9
  121. package/dist/subagent-watcher.d.ts.map +1 -1
  122. package/dist/subagent-watcher.js +126 -147
  123. package/dist/subagent-watcher.js.map +1 -1
  124. package/dist/team-watcher.d.ts +3 -0
  125. package/dist/team-watcher.d.ts.map +1 -1
  126. package/dist/team-watcher.js +54 -5
  127. package/dist/team-watcher.js.map +1 -1
  128. package/dist/tmux-manager.d.ts.map +1 -1
  129. package/dist/tmux-manager.js +1 -2
  130. package/dist/tmux-manager.js.map +1 -1
  131. package/dist/tunnel-manager.d.ts +26 -0
  132. package/dist/tunnel-manager.d.ts.map +1 -1
  133. package/dist/tunnel-manager.js +127 -7
  134. package/dist/tunnel-manager.js.map +1 -1
  135. package/dist/types/api.d.ts +93 -0
  136. package/dist/types/api.d.ts.map +1 -0
  137. package/dist/types/api.js +83 -0
  138. package/dist/types/api.js.map +1 -0
  139. package/dist/types/app-state.d.ts +100 -0
  140. package/dist/types/app-state.d.ts.map +1 -0
  141. package/dist/types/app-state.js +59 -0
  142. package/dist/types/app-state.js.map +1 -0
  143. package/dist/types/common.d.ts +70 -0
  144. package/dist/types/common.d.ts.map +1 -0
  145. package/dist/types/common.js +8 -0
  146. package/dist/types/common.js.map +1 -0
  147. package/dist/types/index.d.ts +18 -0
  148. package/dist/types/index.d.ts.map +1 -0
  149. package/dist/types/index.js +18 -0
  150. package/dist/types/index.js.map +1 -0
  151. package/dist/types/lifecycle.d.ts +17 -0
  152. package/dist/types/lifecycle.d.ts.map +1 -0
  153. package/dist/types/lifecycle.js +5 -0
  154. package/dist/types/lifecycle.js.map +1 -0
  155. package/dist/types/plan.d.ts +32 -0
  156. package/dist/types/plan.d.ts.map +1 -0
  157. package/dist/types/plan.js +5 -0
  158. package/dist/types/plan.js.map +1 -0
  159. package/dist/types/push.d.ts +23 -0
  160. package/dist/types/push.d.ts.map +1 -0
  161. package/dist/types/push.js +5 -0
  162. package/dist/types/push.js.map +1 -0
  163. package/dist/types/ralph.d.ts +241 -0
  164. package/dist/types/ralph.d.ts.map +1 -0
  165. package/dist/types/ralph.js +49 -0
  166. package/dist/types/ralph.js.map +1 -0
  167. package/dist/types/respawn.d.ts +250 -0
  168. package/dist/types/respawn.d.ts.map +1 -0
  169. package/dist/types/respawn.js +5 -0
  170. package/dist/types/respawn.js.map +1 -0
  171. package/dist/types/run-summary.d.ts +81 -0
  172. package/dist/types/run-summary.d.ts.map +1 -0
  173. package/dist/types/run-summary.js +22 -0
  174. package/dist/types/run-summary.js.map +1 -0
  175. package/dist/types/session.d.ts +130 -0
  176. package/dist/types/session.d.ts.map +1 -0
  177. package/dist/types/session.js +5 -0
  178. package/dist/types/session.js.map +1 -0
  179. package/dist/types/task.d.ts +58 -0
  180. package/dist/types/task.d.ts.map +1 -0
  181. package/dist/types/task.js +5 -0
  182. package/dist/types/task.js.map +1 -0
  183. package/dist/types/teams.d.ts +55 -0
  184. package/dist/types/teams.d.ts.map +1 -0
  185. package/dist/types/teams.js +5 -0
  186. package/dist/types/teams.js.map +1 -0
  187. package/dist/types/tools.d.ts +46 -0
  188. package/dist/types/tools.d.ts.map +1 -0
  189. package/dist/types/tools.js +5 -0
  190. package/dist/types/tools.js.map +1 -0
  191. package/dist/types.d.ts +1 -1138
  192. package/dist/types.d.ts.map +1 -1
  193. package/dist/types.js +1 -214
  194. package/dist/types.js.map +1 -1
  195. package/dist/utils/claude-cli-resolver.d.ts.map +1 -1
  196. package/dist/utils/claude-cli-resolver.js +1 -2
  197. package/dist/utils/claude-cli-resolver.js.map +1 -1
  198. package/dist/utils/debouncer.d.ts +111 -0
  199. package/dist/utils/debouncer.d.ts.map +1 -0
  200. package/dist/utils/debouncer.js +162 -0
  201. package/dist/utils/debouncer.js.map +1 -0
  202. package/dist/utils/index.d.ts +3 -2
  203. package/dist/utils/index.d.ts.map +1 -1
  204. package/dist/utils/index.js +3 -2
  205. package/dist/utils/index.js.map +1 -1
  206. package/dist/utils/opencode-cli-resolver.d.ts.map +1 -1
  207. package/dist/utils/opencode-cli-resolver.js +1 -2
  208. package/dist/utils/opencode-cli-resolver.js.map +1 -1
  209. package/dist/utils/string-similarity.d.ts +0 -57
  210. package/dist/utils/string-similarity.d.ts.map +1 -1
  211. package/dist/utils/string-similarity.js +3 -18
  212. package/dist/utils/string-similarity.js.map +1 -1
  213. package/dist/web/middleware/auth.d.ts +31 -0
  214. package/dist/web/middleware/auth.d.ts.map +1 -0
  215. package/dist/web/middleware/auth.js +154 -0
  216. package/dist/web/middleware/auth.js.map +1 -0
  217. package/dist/web/ports/auth-port.d.ts +18 -0
  218. package/dist/web/ports/auth-port.d.ts.map +1 -0
  219. package/dist/web/ports/auth-port.js +6 -0
  220. package/dist/web/ports/auth-port.js.map +1 -0
  221. package/dist/web/ports/config-port.d.ts +28 -0
  222. package/dist/web/ports/config-port.d.ts.map +1 -0
  223. package/dist/web/ports/config-port.js +6 -0
  224. package/dist/web/ports/config-port.js.map +1 -0
  225. package/dist/web/ports/event-port.d.ts +13 -0
  226. package/dist/web/ports/event-port.d.ts.map +1 -0
  227. package/dist/web/ports/event-port.js +6 -0
  228. package/dist/web/ports/event-port.js.map +1 -0
  229. package/dist/web/ports/index.d.ts +14 -0
  230. package/dist/web/ports/index.d.ts.map +1 -0
  231. package/dist/web/ports/index.js +9 -0
  232. package/dist/web/ports/index.js.map +1 -0
  233. package/dist/web/ports/infra-port.d.ts +36 -0
  234. package/dist/web/ports/infra-port.d.ts.map +1 -0
  235. package/dist/web/ports/infra-port.js +6 -0
  236. package/dist/web/ports/infra-port.js.map +1 -0
  237. package/dist/web/ports/respawn-port.d.ts +20 -0
  238. package/dist/web/ports/respawn-port.d.ts.map +1 -0
  239. package/dist/web/ports/respawn-port.js +6 -0
  240. package/dist/web/ports/respawn-port.js.map +1 -0
  241. package/dist/web/ports/session-port.d.ts +15 -0
  242. package/dist/web/ports/session-port.d.ts.map +1 -0
  243. package/dist/web/ports/session-port.js +6 -0
  244. package/dist/web/ports/session-port.js.map +1 -0
  245. package/dist/web/public/api-client.js +70 -0
  246. package/dist/web/public/api-client.js.br +0 -0
  247. package/dist/web/public/api-client.js.gz +0 -0
  248. package/dist/web/public/app.js +152 -236
  249. package/dist/web/public/app.js.br +0 -0
  250. package/dist/web/public/app.js.gz +0 -0
  251. package/dist/web/public/constants.js +238 -0
  252. package/dist/web/public/constants.js.br +0 -0
  253. package/dist/web/public/constants.js.gz +0 -0
  254. package/dist/web/public/index.html +11 -3
  255. package/dist/web/public/index.html.br +0 -0
  256. package/dist/web/public/index.html.gz +0 -0
  257. package/dist/web/public/keyboard-accessory.js +279 -0
  258. package/dist/web/public/keyboard-accessory.js.br +0 -0
  259. package/dist/web/public/keyboard-accessory.js.gz +0 -0
  260. package/dist/web/public/mobile-handlers.js +467 -0
  261. package/dist/web/public/mobile-handlers.js.br +0 -0
  262. package/dist/web/public/mobile-handlers.js.gz +0 -0
  263. package/dist/web/public/mobile.css.gz +0 -0
  264. package/dist/web/public/notification-manager.js +445 -0
  265. package/dist/web/public/notification-manager.js.br +0 -0
  266. package/dist/web/public/notification-manager.js.gz +0 -0
  267. package/dist/web/public/ralph-wizard.js +3 -3
  268. package/dist/web/public/ralph-wizard.js.br +0 -0
  269. package/dist/web/public/ralph-wizard.js.gz +0 -0
  270. package/dist/web/public/styles.css.gz +0 -0
  271. package/dist/web/public/subagent-windows.js +1115 -0
  272. package/dist/web/public/subagent-windows.js.br +0 -0
  273. package/dist/web/public/subagent-windows.js.gz +0 -0
  274. package/dist/web/public/sw.js.gz +0 -0
  275. package/dist/web/public/upload.html.gz +0 -0
  276. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  277. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  278. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  279. package/dist/web/public/vendor/xterm.css.gz +0 -0
  280. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  281. package/dist/web/public/voice-input.js +858 -0
  282. package/dist/web/public/voice-input.js.br +0 -0
  283. package/dist/web/public/voice-input.js.gz +0 -0
  284. package/dist/web/route-helpers.d.ts +38 -0
  285. package/dist/web/route-helpers.d.ts.map +1 -0
  286. package/dist/web/route-helpers.js +143 -0
  287. package/dist/web/route-helpers.js.map +1 -0
  288. package/dist/web/routes/case-routes.d.ts +9 -0
  289. package/dist/web/routes/case-routes.d.ts.map +1 -0
  290. package/dist/web/routes/case-routes.js +419 -0
  291. package/dist/web/routes/case-routes.js.map +1 -0
  292. package/dist/web/routes/file-routes.d.ts +8 -0
  293. package/dist/web/routes/file-routes.d.ts.map +1 -0
  294. package/dist/web/routes/file-routes.js +337 -0
  295. package/dist/web/routes/file-routes.js.map +1 -0
  296. package/dist/web/routes/hook-event-routes.d.ts +9 -0
  297. package/dist/web/routes/hook-event-routes.d.ts.map +1 -0
  298. package/dist/web/routes/hook-event-routes.js +57 -0
  299. package/dist/web/routes/hook-event-routes.js.map +1 -0
  300. package/dist/web/routes/index.d.ts +16 -0
  301. package/dist/web/routes/index.d.ts.map +1 -0
  302. package/dist/web/routes/index.js +16 -0
  303. package/dist/web/routes/index.js.map +1 -0
  304. package/dist/web/routes/mux-routes.d.ts +8 -0
  305. package/dist/web/routes/mux-routes.d.ts.map +1 -0
  306. package/dist/web/routes/mux-routes.js +32 -0
  307. package/dist/web/routes/mux-routes.js.map +1 -0
  308. package/dist/web/routes/plan-routes.d.ts +9 -0
  309. package/dist/web/routes/plan-routes.d.ts.map +1 -0
  310. package/dist/web/routes/plan-routes.js +381 -0
  311. package/dist/web/routes/plan-routes.js.map +1 -0
  312. package/dist/web/routes/push-routes.d.ts +8 -0
  313. package/dist/web/routes/push-routes.d.ts.map +1 -0
  314. package/dist/web/routes/push-routes.js +49 -0
  315. package/dist/web/routes/push-routes.js.map +1 -0
  316. package/dist/web/routes/ralph-routes.d.ts +9 -0
  317. package/dist/web/routes/ralph-routes.d.ts.map +1 -0
  318. package/dist/web/routes/ralph-routes.js +475 -0
  319. package/dist/web/routes/ralph-routes.js.map +1 -0
  320. package/dist/web/routes/respawn-routes.d.ts +8 -0
  321. package/dist/web/routes/respawn-routes.d.ts.map +1 -0
  322. package/dist/web/routes/respawn-routes.js +260 -0
  323. package/dist/web/routes/respawn-routes.js.map +1 -0
  324. package/dist/web/routes/scheduled-routes.d.ts +8 -0
  325. package/dist/web/routes/scheduled-routes.d.ts.map +1 -0
  326. package/dist/web/routes/scheduled-routes.js +51 -0
  327. package/dist/web/routes/scheduled-routes.js.map +1 -0
  328. package/dist/web/routes/session-routes.d.ts +9 -0
  329. package/dist/web/routes/session-routes.d.ts.map +1 -0
  330. package/dist/web/routes/session-routes.js +729 -0
  331. package/dist/web/routes/session-routes.js.map +1 -0
  332. package/dist/web/routes/system-routes.d.ts +9 -0
  333. package/dist/web/routes/system-routes.d.ts.map +1 -0
  334. package/dist/web/routes/system-routes.js +678 -0
  335. package/dist/web/routes/system-routes.js.map +1 -0
  336. package/dist/web/routes/team-routes.d.ts +8 -0
  337. package/dist/web/routes/team-routes.d.ts.map +1 -0
  338. package/dist/web/routes/team-routes.js +14 -0
  339. package/dist/web/routes/team-routes.js.map +1 -0
  340. package/dist/web/schemas.d.ts +43 -3
  341. package/dist/web/schemas.d.ts.map +1 -1
  342. package/dist/web/schemas.js +6 -2
  343. package/dist/web/schemas.js.map +1 -1
  344. package/dist/web/server.d.ts +10 -9
  345. package/dist/web/server.d.ts.map +1 -1
  346. package/dist/web/server.js +342 -3829
  347. package/dist/web/server.js.map +1 -1
  348. package/package.json +1 -1
@@ -10,16 +10,23 @@
10
10
  * patterns are detected in the output stream, reducing overhead for
11
11
  * sessions not using autonomous loops.
12
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
+ *
13
19
  * @module ralph-tracker
14
20
  */
15
21
  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';
22
+ import { createInitialRalphTrackerState, } from './types.js';
23
+ import { ANSI_ESCAPE_PATTERN_SIMPLE, fuzzyPhraseMatch, todoContentHash, stringSimilarity, Debouncer, } from './utils/index.js';
21
24
  import { MAX_LINE_BUFFER_SIZE } from './config/buffer-limits.js';
22
25
  import { MAX_TODOS_PER_SESSION } from './config/map-limits.js';
26
+ import { RalphPlanTracker } from './ralph-plan-tracker.js';
27
+ import { RalphFixPlanWatcher, generateFixPlanMarkdown, importFixPlanMarkdown } from './ralph-fix-plan-watcher.js';
28
+ import { RalphStallDetector } from './ralph-stall-detector.js';
29
+ import { RalphStatusParser } from './ralph-status-parser.js';
23
30
  // ========== Configuration Constants ==========
24
31
  // Note: MAX_TODOS_PER_SESSION and MAX_LINE_BUFFER_SIZE are imported from config modules
25
32
  /**
@@ -51,7 +58,6 @@ const EVENT_DEBOUNCE_MS = 50;
51
58
  * Prevents unbounded growth if many unique phrases are seen.
52
59
  */
53
60
  const MAX_COMPLETION_PHRASE_ENTRIES = 50;
54
- const MAX_PLAN_HISTORY = 10;
55
61
  /**
56
62
  * Common/generic completion phrases that may cause false positives.
57
63
  * These phrases are likely to appear in Claude's natural output,
@@ -226,66 +232,6 @@ const TASK_DONE_PATTERN = /(?:task|item|todo)\s*(?:#?\d+|"\s*[^"]+\s*")?\s*(?:is
226
232
  // ---------- Utility Patterns ----------
227
233
  /** Maximum number of task number to content mappings to track */
228
234
  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
235
  // ---------- Priority Detection Patterns ----------
290
236
  // Pre-compiled for performance; avoids repeated allocation in parsePriority()
291
237
  /** P0 (Critical) priority patterns - highest severity issues */
@@ -352,6 +298,13 @@ const P2_PRIORITY_PATTERNS = [
352
298
  * - 2nd occurrence: Emits `completionDetected` event (actual completion)
353
299
  * - If loop already active: Emits immediately on first occurrence
354
300
  *
301
+ * ## Sub-modules
302
+ *
303
+ * - `planTracker` - Plan task management, checkpoints, versioning
304
+ * - `fixPlanWatcher` - @fix_plan.md file watching and parsing
305
+ * - `stallDetector` - Iteration stall detection
306
+ * - `statusParser` - RALPH_STATUS block parsing, circuit breaker
307
+ *
355
308
  * ## Events
356
309
  *
357
310
  * - `loopUpdate` - Loop state changed (status, iteration, phrase)
@@ -370,6 +323,16 @@ const P2_PRIORITY_PATTERNS = [
370
323
  * ```
371
324
  */
372
325
  export class RalphTracker extends EventEmitter {
326
+ // ========== Sub-modules ==========
327
+ /** Plan task management sub-module */
328
+ planTracker = new RalphPlanTracker();
329
+ /** @fix_plan.md file watcher sub-module */
330
+ fixPlanWatcher;
331
+ /** Iteration stall detector sub-module */
332
+ stallDetector = new RalphStallDetector();
333
+ /** RALPH_STATUS block parser and circuit breaker sub-module */
334
+ statusParser = new RalphStatusParser();
335
+ // ========== Core State ==========
373
336
  /** Current state of the detected loop */
374
337
  _loopState;
375
338
  /** Map of todo items by ID for O(1) lookup */
@@ -383,14 +346,10 @@ export class RalphTracker extends EventEmitter {
383
346
  _completionPhraseCount = new Map();
384
347
  /** Timestamp of last cleanup check for throttling */
385
348
  _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;
349
+ /** Debouncer for todoUpdate events */
350
+ _todoDeb = new Debouncer(EVENT_DEBOUNCE_MS);
351
+ /** Debouncer for loopUpdate events */
352
+ _loopDeb = new Debouncer(EVENT_DEBOUNCE_MS);
394
353
  /** When true, prevents auto-enable on pattern detection */
395
354
  _autoEnableDisabled = true;
396
355
  /** Maps task numbers from "✔ Task #N" format to their content for status updates */
@@ -403,64 +362,6 @@ export class RalphTracker extends EventEmitter {
403
362
  _partialPromiseBuffer = '';
404
363
  /** Maximum size of partial promise buffer */
405
364
  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
365
  /** Alternate completion phrases (P1-003: multi-phrase support) - Set for O(1) lookup */
465
366
  _alternateCompletionPhrases = new Set();
466
367
  // ========== P1-009: Progress Estimation ==========
@@ -472,6 +373,10 @@ export class RalphTracker extends EventEmitter {
472
373
  _todosStartedAt = 0;
473
374
  /** Map of todo ID to timestamp when it started (for duration tracking) */
474
375
  _todoStartTimes = new Map();
376
+ /** Last calculated completion confidence */
377
+ _lastCompletionConfidence;
378
+ /** Confidence threshold for triggering completion (0-100) */
379
+ static COMPLETION_CONFIDENCE_THRESHOLD = 70;
475
380
  /**
476
381
  * Creates a new RalphTracker instance.
477
382
  * Starts in disabled state until Ralph patterns are detected.
@@ -479,13 +384,230 @@ export class RalphTracker extends EventEmitter {
479
384
  constructor() {
480
385
  super();
481
386
  this._loopState = createInitialRalphTrackerState();
482
- this._circuitBreaker = createInitialCircuitBreakerStatus();
483
- this._lastIterationChangeTime = Date.now();
387
+ // Initialize fix plan watcher with callbacks to parent methods
388
+ this.fixPlanWatcher = new RalphFixPlanWatcher((content) => this.parsePriority(content), (content) => this.generateTodoId(content));
389
+ // Wire sub-module events
390
+ this._wireSubModuleEvents();
391
+ }
392
+ /**
393
+ * Forward all sub-module events through RalphTracker
394
+ * so external consumers don't need to know about the split.
395
+ */
396
+ _wireSubModuleEvents() {
397
+ // Forward plan tracker events
398
+ for (const event of [
399
+ 'planInitialized',
400
+ 'planTaskUpdate',
401
+ 'taskBlocked',
402
+ 'taskUnblocked',
403
+ 'planCheckpoint',
404
+ 'planTaskAdded',
405
+ 'planRollback',
406
+ ]) {
407
+ this.planTracker.on(event, (...args) => this.emit(event, ...args));
408
+ }
409
+ // Forward status parser events
410
+ this.statusParser.on('statusBlockDetected', (block) => {
411
+ // Auto-enable tracker when we see a status block
412
+ if (!this._loopState.enabled && !this._autoEnableDisabled) {
413
+ this.enable();
414
+ }
415
+ this._loopState.lastActivity = Date.now();
416
+ this.emit('statusBlockDetected', block);
417
+ this.emitLoopUpdateDebounced();
418
+ });
419
+ this.statusParser.on('circuitBreakerUpdate', (status) => {
420
+ this.emit('circuitBreakerUpdate', status);
421
+ });
422
+ this.statusParser.on('exitGateMet', (data) => {
423
+ this.emit('exitGateMet', data);
424
+ });
425
+ // Forward stall detector events
426
+ this.stallDetector.on('iterationStallWarning', (data) => {
427
+ this.emit('iterationStallWarning', data);
428
+ });
429
+ this.stallDetector.on('iterationStallCritical', (data) => {
430
+ this.emit('iterationStallCritical', data);
431
+ });
432
+ // Forward fix plan watcher events
433
+ this.fixPlanWatcher.on('todosLoaded', (items) => {
434
+ // Replace _todos with file-based items
435
+ this._todos.clear();
436
+ for (const item of items) {
437
+ this._todos.set(item.id, item);
438
+ }
439
+ // Auto-enable tracker when we have todos from @fix_plan.md
440
+ if (!this._loopState.enabled) {
441
+ this.enable();
442
+ }
443
+ this.emit('todoUpdate', this.todos);
444
+ });
445
+ }
446
+ // ========== Delegated Plan Tracker Methods ==========
447
+ /**
448
+ * Initialize plan tasks from generated plan items.
449
+ */
450
+ initializePlanTasks(items) {
451
+ this.planTracker.initializePlanTasks(items);
452
+ }
453
+ /**
454
+ * Update a specific plan task's status, attempts, or error.
455
+ */
456
+ updatePlanTask(taskId, update) {
457
+ return this.planTracker.updatePlanTask(taskId, update);
458
+ }
459
+ /**
460
+ * Add a new task to the plan.
461
+ */
462
+ addPlanTask(task) {
463
+ return this.planTracker.addPlanTask(task);
464
+ }
465
+ /**
466
+ * Get all plan tasks.
467
+ */
468
+ getPlanTasks() {
469
+ return this.planTracker.getPlanTasks();
470
+ }
471
+ /**
472
+ * Generate a checkpoint review.
473
+ */
474
+ generateCheckpointReview() {
475
+ return this.planTracker.generateCheckpointReview();
476
+ }
477
+ /**
478
+ * Get plan version history.
479
+ */
480
+ getPlanHistory() {
481
+ return this.planTracker.getPlanHistory();
482
+ }
483
+ /**
484
+ * Rollback to a previous plan version.
485
+ */
486
+ rollbackToVersion(version) {
487
+ return this.planTracker.rollbackToVersion(version);
488
+ }
489
+ /**
490
+ * Check if checkpoint review is due.
491
+ */
492
+ isCheckpointDue() {
493
+ return this.planTracker.isCheckpointDue();
494
+ }
495
+ /**
496
+ * Get current plan version.
497
+ */
498
+ get planVersion() {
499
+ return this.planTracker.planVersion;
500
+ }
501
+ // ========== Delegated Fix Plan Watcher Methods ==========
502
+ /**
503
+ * Set the working directory and start watching @fix_plan.md.
504
+ * @param workingDir - The session's working directory
505
+ */
506
+ setWorkingDir(workingDir) {
507
+ this.fixPlanWatcher.setWorkingDir(workingDir);
508
+ }
509
+ /**
510
+ * Load @fix_plan.md from disk if it exists.
511
+ */
512
+ async loadFixPlanFromDisk() {
513
+ return this.fixPlanWatcher.loadFixPlanFromDisk();
514
+ }
515
+ /**
516
+ * Stop watching @fix_plan.md.
517
+ */
518
+ stopWatchingFixPlan() {
519
+ this.fixPlanWatcher.stopWatchingFixPlan();
520
+ }
521
+ /**
522
+ * When @fix_plan.md is active, treat it as the source of truth for todo status.
523
+ */
524
+ get isFileAuthoritative() {
525
+ return this.fixPlanWatcher.isFileAuthoritative;
526
+ }
527
+ /**
528
+ * Generate @fix_plan.md content from current todos.
529
+ */
530
+ generateFixPlanMarkdown() {
531
+ return generateFixPlanMarkdown(this.todos);
532
+ }
533
+ /**
534
+ * Parse @fix_plan.md content and import todos.
535
+ * Replaces current todos with imported ones.
536
+ *
537
+ * @param content - Markdown content from @fix_plan.md
538
+ * @returns Number of todos imported
539
+ */
540
+ importFixPlanMarkdown(content) {
541
+ const newTodos = importFixPlanMarkdown(content, (c) => this.parsePriority(c), (c) => this.generateTodoId(c));
542
+ // Replace current todos with imported ones
543
+ this._todos.clear();
544
+ for (const todo of newTodos) {
545
+ this._todos.set(todo.id, todo);
546
+ }
547
+ // Emit update
548
+ this.emit('todoUpdate', this.todos);
549
+ return newTodos.length;
550
+ }
551
+ // ========== Delegated Stall Detector Methods ==========
552
+ /**
553
+ * Start iteration stall detection timer.
554
+ */
555
+ startIterationStallDetection() {
556
+ this.stallDetector.startIterationStallDetection();
557
+ }
558
+ /**
559
+ * Stop iteration stall detection timer.
560
+ */
561
+ stopIterationStallDetection() {
562
+ this.stallDetector.stopIterationStallDetection();
563
+ }
564
+ /**
565
+ * Get iteration stall metrics for monitoring.
566
+ */
567
+ getIterationStallMetrics() {
568
+ return this.stallDetector.getIterationStallMetrics();
569
+ }
570
+ /**
571
+ * Configure iteration stall thresholds.
572
+ */
573
+ configureIterationStallThresholds(warningMs, criticalMs) {
574
+ this.stallDetector.configureIterationStallThresholds(warningMs, criticalMs);
575
+ }
576
+ // ========== Delegated Status Parser Methods ==========
577
+ /**
578
+ * Manually reset circuit breaker to CLOSED state.
579
+ * @fires circuitBreakerUpdate
580
+ */
581
+ resetCircuitBreaker() {
582
+ this.statusParser.resetCircuitBreaker();
583
+ }
584
+ /**
585
+ * Get current circuit breaker status.
586
+ */
587
+ get circuitBreakerStatus() {
588
+ return this.statusParser.circuitBreakerStatus;
589
+ }
590
+ /**
591
+ * Get last parsed RALPH_STATUS block.
592
+ */
593
+ get lastStatusBlock() {
594
+ return this.statusParser.lastStatusBlock;
595
+ }
596
+ /**
597
+ * Get cumulative stats from status blocks.
598
+ */
599
+ get cumulativeStats() {
600
+ return this.statusParser.cumulativeStats;
601
+ }
602
+ /**
603
+ * Whether dual-condition exit gate has been met.
604
+ */
605
+ get exitGateMet() {
606
+ return this.statusParser.exitGateMet;
484
607
  }
608
+ // ========== Core Methods ==========
485
609
  /**
486
610
  * 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
611
  */
490
612
  addAlternateCompletionPhrase(phrase) {
491
613
  if (!this._alternateCompletionPhrases.has(phrase)) {
@@ -496,7 +618,6 @@ export class RalphTracker extends EventEmitter {
496
618
  }
497
619
  /**
498
620
  * Remove an alternate completion phrase.
499
- * @param phrase - Phrase to remove
500
621
  */
501
622
  removeAlternateCompletionPhrase(phrase) {
502
623
  if (this._alternateCompletionPhrases.delete(phrase)) {
@@ -506,16 +627,12 @@ export class RalphTracker extends EventEmitter {
506
627
  }
507
628
  /**
508
629
  * 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
630
  */
512
631
  isValidCompletionPhrase(phrase) {
513
632
  return this.findMatchingCompletionPhrase(phrase) !== null;
514
633
  }
515
634
  /**
516
635
  * 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
636
  */
520
637
  findMatchingCompletionPhrase(phrase) {
521
638
  const primary = this._loopState.completionPhrase;
@@ -531,7 +648,6 @@ export class RalphTracker extends EventEmitter {
531
648
  }
532
649
  /**
533
650
  * Prevent auto-enable from pattern detection.
534
- * Use this when the user has explicitly disabled the Ralph tracker.
535
651
  */
536
652
  disableAutoEnable() {
537
653
  this._autoEnableDisabled = true;
@@ -548,129 +664,14 @@ export class RalphTracker extends EventEmitter {
548
664
  get autoEnableDisabled() {
549
665
  return this._autoEnableDisabled;
550
666
  }
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
667
  /**
663
668
  * 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
669
  */
667
670
  get enabled() {
668
671
  return this._loopState.enabled;
669
672
  }
670
673
  /**
671
674
  * 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
675
  * @fires enabled
675
676
  * @fires loopUpdate
676
677
  */
@@ -684,7 +685,6 @@ export class RalphTracker extends EventEmitter {
684
685
  }
685
686
  /**
686
687
  * Disable the tracker to stop monitoring terminal output.
687
- * Terminal data will be ignored until re-enabled.
688
688
  * @fires loopUpdate
689
689
  */
690
690
  disable() {
@@ -696,17 +696,6 @@ export class RalphTracker extends EventEmitter {
696
696
  }
697
697
  /**
698
698
  * 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
699
  * @fires loopUpdate
711
700
  * @fires todoUpdate
712
701
  */
@@ -721,15 +710,10 @@ export class RalphTracker extends EventEmitter {
721
710
  this._taskNumberToContent.clear();
722
711
  this._lineBuffer = '';
723
712
  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)
713
+ // Reset sub-modules
714
+ this.statusParser.reset();
715
+ this.planTracker.reset();
716
+ this.stallDetector.reset();
733
717
  // Emit on next tick to prevent listeners from modifying state during reset (non-reentrant)
734
718
  const loopState = this.loopState;
735
719
  const todos = this.todos;
@@ -740,8 +724,6 @@ export class RalphTracker extends EventEmitter {
740
724
  }
741
725
  /**
742
726
  * 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
727
  * @fires loopUpdate
746
728
  * @fires todoUpdate
747
729
  */
@@ -755,15 +737,11 @@ export class RalphTracker extends EventEmitter {
755
737
  this._todoStartTimes.clear();
756
738
  this._alternateCompletionPhrases.clear();
757
739
  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();
740
+ this._partialPromiseBuffer = '';
741
+ // Full reset sub-modules
742
+ this.statusParser.fullReset();
743
+ this.planTracker.fullReset();
744
+ this.stallDetector.reset();
767
745
  // Emit on next tick to prevent listeners from modifying state during reset (non-reentrant)
768
746
  const loopState = this.loopState;
769
747
  const todos = this.todos;
@@ -774,178 +752,49 @@ export class RalphTracker extends EventEmitter {
774
752
  }
775
753
  /**
776
754
  * Clear all debounce timers.
777
- * Called during reset/fullReset to prevent stale emissions.
778
755
  */
779
756
  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;
757
+ this._todoDeb.cancel();
758
+ this._loopDeb.cancel();
790
759
  }
791
760
  /**
792
761
  * 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
762
  */
796
763
  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);
764
+ this._todoDeb.schedule(() => this.emit('todoUpdate', this.todos));
808
765
  }
809
766
  /**
810
767
  * 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
768
  */
814
769
  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);
770
+ this._loopDeb.schedule(() => this.emit('loopUpdate', this.loopState));
826
771
  }
827
772
  /**
828
773
  * Flush all pending debounced events immediately.
829
- * Useful for testing or when immediate state sync is needed.
830
774
  */
831
775
  flushPendingEvents() {
832
- if (this._todoUpdatePending) {
833
- this._todoUpdatePending = false;
834
- if (this._todoUpdateTimer) {
835
- clearTimeout(this._todoUpdateTimer);
836
- this._todoUpdateTimer = null;
837
- }
776
+ if (this._todoDeb.isPending) {
777
+ this._todoDeb.cancel();
838
778
  this.emit('todoUpdate', this.todos);
839
779
  }
840
- if (this._loopUpdatePending) {
841
- this._loopUpdatePending = false;
842
- if (this._loopUpdateTimer) {
843
- clearTimeout(this._loopUpdateTimer);
844
- this._loopUpdateTimer = null;
845
- }
780
+ if (this._loopDeb.isPending) {
781
+ this._loopDeb.cancel();
846
782
  this.emit('loopUpdate', this.loopState);
847
783
  }
848
784
  }
849
785
  /**
850
786
  * 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
- */
870
- stopIterationStallDetection() {
871
- if (this._iterationStallTimer) {
872
- clearInterval(this._iterationStallTimer);
873
- this._iterationStallTimer = null;
874
- }
875
- }
876
- /**
877
- * Check for iteration stall and emit appropriate events.
878
787
  */
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() {
788
+ get loopState() {
904
789
  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,
790
+ ...this._loopState,
791
+ planVersion: this.planTracker.planVersion,
792
+ planHistoryLength: this.planTracker.getPlanHistory().length,
793
+ completionConfidence: this._lastCompletionConfidence,
911
794
  };
912
795
  }
913
796
  /**
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
797
+ * Calculate confidence score for a potential completion signal.
949
798
  */
950
799
  calculateCompletionConfidence(phrase, context) {
951
800
  let score = 0;
@@ -978,19 +827,19 @@ export class RalphTracker extends EventEmitter {
978
827
  score += 20;
979
828
  }
980
829
  // Check for EXIT_SIGNAL from RALPH_STATUS block (adds 15 points)
981
- if (this._lastStatusBlock?.exitSignal === true) {
830
+ const lastBlock = this.statusParser.lastStatusBlock;
831
+ if (lastBlock?.exitSignal === true) {
982
832
  signals.hasExitSignal = true;
983
833
  score += 15;
984
834
  }
985
835
  // Check for multiple completion indicators (adds 10 points)
986
- if (this._completionIndicators >= 2) {
836
+ if (this.statusParser.cumulativeStats.completionIndicators >= 2) {
987
837
  signals.multipleIndicators = true;
988
838
  score += 10;
989
839
  }
990
840
  // Check context appropriateness (deduct if inappropriate)
991
841
  if (context) {
992
842
  const lowerContext = context.toLowerCase();
993
- // Deduct points if phrase appears in prompt-like context
994
843
  if (lowerContext.includes('output:') ||
995
844
  lowerContext.includes('completion phrase') ||
996
845
  lowerContext.includes('output exactly') ||
@@ -1024,28 +873,12 @@ export class RalphTracker extends EventEmitter {
1024
873
  }
1025
874
  /**
1026
875
  * Get all tracked todo items as an array.
1027
- * @returns Array of todo items (copy, safe to modify)
1028
876
  */
1029
877
  get todos() {
1030
878
  return Array.from(this._todos.values());
1031
879
  }
1032
880
  /**
1033
881
  * 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
882
  */
1050
883
  processTerminalData(data) {
1051
884
  // Remove ANSI escape codes for cleaner parsing
@@ -1054,33 +887,29 @@ export class RalphTracker extends EventEmitter {
1054
887
  }
1055
888
  /**
1056
889
  * 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
890
  */
1059
891
  processCleanData(cleanData) {
1060
892
  // If tracker is disabled, only check for patterns that should auto-enable it
1061
893
  if (!this._loopState.enabled) {
1062
- // Don't auto-enable if explicitly disabled by user setting
1063
894
  if (this._autoEnableDisabled) {
1064
895
  return;
1065
896
  }
1066
897
  if (this.shouldAutoEnable(cleanData)) {
1067
898
  this.enable();
1068
- // Continue processing now that we're enabled
1069
899
  }
1070
900
  else {
1071
- return; // Don't process further when disabled
901
+ return;
1072
902
  }
1073
903
  }
1074
904
  // Buffer data for line-based processing
1075
905
  this._lineBuffer += cleanData;
1076
906
  // Prevent unbounded line buffer growth from very long lines
1077
907
  if (this._lineBuffer.length > MAX_LINE_BUFFER_SIZE) {
1078
- // Truncate to last portion to preserve recent data
1079
908
  this._lineBuffer = this._lineBuffer.slice(-Math.floor(MAX_LINE_BUFFER_SIZE / 2));
1080
909
  }
1081
910
  // Process complete lines
1082
911
  const lines = this._lineBuffer.split('\n');
1083
- this._lineBuffer = lines.pop() || ''; // Keep incomplete line in buffer
912
+ this._lineBuffer = lines.pop() || '';
1084
913
  for (const line of lines) {
1085
914
  this.processLine(line);
1086
915
  }
@@ -1091,26 +920,11 @@ export class RalphTracker extends EventEmitter {
1091
920
  }
1092
921
  /**
1093
922
  * 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
923
  */
1109
924
  shouldAutoEnable(data) {
1110
925
  // Cheap pre-filter: skip the full regex battery if none of the key
1111
926
  // 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
927
+ if (!data.includes('<') &&
1114
928
  !data.includes('ralph') &&
1115
929
  !data.includes('Ralph') &&
1116
930
  !data.includes('Todo') &&
@@ -1118,8 +932,8 @@ export class RalphTracker extends EventEmitter {
1118
932
  !data.includes('Iteration') &&
1119
933
  !data.includes('[') &&
1120
934
  !data.includes('\u2610') &&
1121
- !data.includes('\u2612') && // ☐ ☒
1122
- !data.includes('\u2714') && // ✔
935
+ !data.includes('\u2612') &&
936
+ !data.includes('\u2714') &&
1123
937
  !data.includes('Loop') &&
1124
938
  !data.includes('complete') &&
1125
939
  !data.includes('COMPLETE') &&
@@ -1127,74 +941,46 @@ export class RalphTracker extends EventEmitter {
1127
941
  !data.includes('DONE')) {
1128
942
  return false;
1129
943
  }
1130
- // Ralph loop command: /ralph-loop:ralph-loop
1131
- if (RALPH_START_PATTERN.test(data)) {
944
+ if (RALPH_START_PATTERN.test(data))
1132
945
  return true;
1133
- }
1134
- // Completion phrase: <promise>...</promise>
1135
- if (PROMISE_PATTERN.test(data)) {
946
+ if (PROMISE_PATTERN.test(data))
1136
947
  return true;
1137
- }
1138
- // TodoWrite tool usage
1139
- if (TODOWRITE_PATTERN.test(data)) {
948
+ if (TODOWRITE_PATTERN.test(data))
1140
949
  return true;
1141
- }
1142
- // Iteration patterns from Ralph loop: "Iteration 5/50", "[5/50]"
1143
- if (ITERATION_PATTERN.test(data)) {
950
+ if (ITERATION_PATTERN.test(data))
1144
951
  return true;
1145
- }
1146
- // Todo checkboxes: "- [ ] Task" or "- [x] Task"
1147
- // Reset lastIndex BEFORE test to ensure consistent matching with /g flag patterns
1148
952
  TODO_CHECKBOX_PATTERN.lastIndex = 0;
1149
- if (TODO_CHECKBOX_PATTERN.test(data)) {
953
+ if (TODO_CHECKBOX_PATTERN.test(data))
1150
954
  return true;
1151
- }
1152
- // Todo indicator icons: "Todo: ☐", "Todo: ◐", etc.
1153
955
  TODO_INDICATOR_PATTERN.lastIndex = 0;
1154
- if (TODO_INDICATOR_PATTERN.test(data)) {
956
+ if (TODO_INDICATOR_PATTERN.test(data))
1155
957
  return true;
1156
- }
1157
- // Claude Code native todo format: "☐ Task", "☒ Task"
1158
958
  TODO_NATIVE_PATTERN.lastIndex = 0;
1159
- if (TODO_NATIVE_PATTERN.test(data)) {
959
+ if (TODO_NATIVE_PATTERN.test(data))
1160
960
  return true;
1161
- }
1162
- // Claude Code checkmark-based TodoWrite: "✔ Task #N created:", "✔ Task #N updated:"
1163
961
  TODO_TASK_CREATED_PATTERN.lastIndex = 0;
1164
- if (TODO_TASK_CREATED_PATTERN.test(data)) {
962
+ if (TODO_TASK_CREATED_PATTERN.test(data))
1165
963
  return true;
1166
- }
1167
964
  TODO_TASK_STATUS_PATTERN.lastIndex = 0;
1168
- if (TODO_TASK_STATUS_PATTERN.test(data)) {
965
+ if (TODO_TASK_STATUS_PATTERN.test(data))
1169
966
  return true;
1170
- }
1171
- // Loop start patterns (e.g., "Loop started at", "Starting Ralph loop")
1172
- if (LOOP_START_PATTERN.test(data)) {
967
+ if (LOOP_START_PATTERN.test(data))
1173
968
  return true;
1174
- }
1175
- // All tasks complete signals
1176
- if (ALL_COMPLETE_PATTERN.test(data)) {
969
+ if (ALL_COMPLETE_PATTERN.test(data))
1177
970
  return true;
1178
- }
1179
- // Task completion signals
1180
- if (TASK_DONE_PATTERN.test(data)) {
971
+ if (TASK_DONE_PATTERN.test(data))
1181
972
  return true;
1182
- }
1183
973
  return false;
1184
974
  }
1185
975
  /**
1186
976
  * 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
977
  */
1190
978
  processLine(line) {
1191
979
  const trimmed = line.trim();
1192
980
  if (!trimmed)
1193
981
  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);
982
+ // Delegate RALPH_STATUS block and completion indicator detection to sub-module
983
+ this.statusParser.processLine(trimmed);
1198
984
  // Check for completion phrase
1199
985
  this.detectCompletionPhrase(trimmed);
1200
986
  // Check for "all tasks complete" signals
@@ -1208,52 +994,25 @@ export class RalphTracker extends EventEmitter {
1208
994
  }
1209
995
  /**
1210
996
  * 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
997
  */
1228
998
  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
999
  if (this.isFileAuthoritative)
1232
1000
  return;
1233
- // Only trigger if line is a clear standalone completion message
1234
- // Avoid matching commentary like "once all tasks are complete..."
1235
1001
  if (!ALL_COMPLETE_PATTERN.test(line))
1236
1002
  return;
1237
- // Must be a reasonably short line (< 100 chars) to be a completion signal, not commentary
1238
1003
  if (line.length > 100)
1239
1004
  return;
1240
- // Skip if this looks like it's part of the original prompt (contains "output:")
1241
1005
  if (line.toLowerCase().includes('output:') || line.includes('<promise>'))
1242
1006
  return;
1243
- // Don't trigger if we haven't seen any todos yet
1244
1007
  if (this._todos.size === 0)
1245
1008
  return;
1246
- // Check if the count matches our todo count (e.g., "All 8 files created")
1247
1009
  const countMatch = line.match(ALL_COUNT_PATTERN);
1248
1010
  const parsedCount = countMatch ? parseInt(countMatch[1], 10) : NaN;
1249
1011
  const mentionedCount = Number.isNaN(parsedCount) ? null : parsedCount;
1250
1012
  const todoCount = this._todos.size;
1251
- // If a count is mentioned, it should match our todo count (within reason)
1252
1013
  if (mentionedCount !== null && Math.abs(mentionedCount - todoCount) > 2) {
1253
- // Count doesn't match our todos, might be unrelated
1254
1014
  return;
1255
1015
  }
1256
- // Mark all todos as complete
1257
1016
  let updated = false;
1258
1017
  for (const todo of this._todos.values()) {
1259
1018
  if (todo.status !== 'completed') {
@@ -1264,7 +1023,6 @@ export class RalphTracker extends EventEmitter {
1264
1023
  if (updated) {
1265
1024
  this.emit('todoUpdate', this.todos);
1266
1025
  }
1267
- // Emit completion if we have an expected phrase
1268
1026
  if (this._loopState.completionPhrase) {
1269
1027
  this._loopState.active = false;
1270
1028
  this._loopState.lastActivity = Date.now();
@@ -1273,25 +1031,18 @@ export class RalphTracker extends EventEmitter {
1273
1031
  }
1274
1032
  }
1275
1033
  /**
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.
1034
+ * Detect individual task completion signals.
1281
1035
  */
1282
1036
  detectTaskCompletion(line) {
1283
- // When @fix_plan.md is active, only trust the file for todo status
1284
1037
  if (this.isFileAuthoritative)
1285
1038
  return;
1286
1039
  if (!TASK_DONE_PATTERN.test(line))
1287
1040
  return;
1288
- // Only act on explicit task number references like "Task 8 is done"
1289
1041
  const taskNumMatch = line.match(/task\s*#?(\d+)/i);
1290
1042
  if (taskNumMatch) {
1291
1043
  const taskNum = parseInt(taskNumMatch[1], 10);
1292
1044
  if (Number.isNaN(taskNum))
1293
1045
  return;
1294
- // Find the nth todo (by order) and mark it complete
1295
1046
  let count = 0;
1296
1047
  for (const [_id, todo] of this._todos) {
1297
1048
  count++;
@@ -1302,23 +1053,11 @@ export class RalphTracker extends EventEmitter {
1302
1053
  }
1303
1054
  }
1304
1055
  }
1305
- // Don't guess which todo to mark - let the checkbox detection handle it
1306
1056
  }
1307
1057
  /**
1308
1058
  * 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
1059
  */
1318
1060
  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
1061
  if (this._partialPromiseBuffer) {
1323
1062
  const combinedData = this._partialPromiseBuffer + data;
1324
1063
  const promiseMatch = combinedData.match(PROMISE_PATTERN);
@@ -1329,7 +1068,6 @@ export class RalphTracker extends EventEmitter {
1329
1068
  return;
1330
1069
  }
1331
1070
  }
1332
- // Check for partial promise tag at end of data (for next chunk)
1333
1071
  const partialMatch = data.match(PROMISE_PARTIAL_PATTERN);
1334
1072
  if (partialMatch) {
1335
1073
  const partialContent = partialMatch[0];
@@ -1346,32 +1084,16 @@ export class RalphTracker extends EventEmitter {
1346
1084
  }
1347
1085
  /**
1348
1086
  * 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
1087
  */
1361
1088
  detectCompletionPhrase(line) {
1362
- // First check for tagged phrase: <promise>PHRASE</promise>
1363
1089
  const match = line.match(PROMISE_PATTERN);
1364
1090
  if (match) {
1365
1091
  this.handleCompletionPhrase(match[1]);
1366
1092
  return;
1367
1093
  }
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
1094
  const expectedPhrase = this._loopState.completionPhrase;
1371
1095
  if (expectedPhrase && line.toUpperCase().includes(expectedPhrase.toUpperCase())) {
1372
- // Avoid false positives: don't trigger on prompt context
1373
1096
  const isNotInPromptContext = !line.includes('<promise>') && !line.includes('output:');
1374
- // Also avoid triggering on "completion phrase is X" explanatory text
1375
1097
  const isNotExplanation = !line.toLowerCase().includes('completion phrase') && !line.toLowerCase().includes('output exactly');
1376
1098
  if (isNotInPromptContext && isNotExplanation) {
1377
1099
  this.handleBareCompletionPhrase(expectedPhrase);
@@ -1380,37 +1102,17 @@ export class RalphTracker extends EventEmitter {
1380
1102
  }
1381
1103
  /**
1382
1104
  * 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
1105
  */
1398
1106
  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
1107
  const taggedCount = this._completionPhraseCount.get(phrase) || 0;
1403
1108
  const loopExplicitlyActive = this._loopState.active;
1404
1109
  if (taggedCount === 0 && !loopExplicitlyActive)
1405
1110
  return;
1406
- // Track bare occurrences to avoid double-firing
1407
1111
  const bareKey = `bare:${phrase}`;
1408
1112
  const bareCount = (this._completionPhraseCount.get(bareKey) || 0) + 1;
1409
1113
  this._completionPhraseCount.set(bareKey, bareCount);
1410
- // Only fire once for bare phrase
1411
1114
  if (bareCount > 1)
1412
1115
  return;
1413
- // Mark all todos as complete (since we've reached the completion phrase)
1414
1116
  let updated = false;
1415
1117
  for (const todo of this._todos.values()) {
1416
1118
  if (todo.status !== 'completed') {
@@ -1421,36 +1123,26 @@ export class RalphTracker extends EventEmitter {
1421
1123
  if (updated) {
1422
1124
  this.emit('todoUpdate', this.todos);
1423
1125
  }
1424
- // Emit completion event
1425
1126
  this._loopState.active = false;
1426
1127
  this._loopState.lastActivity = Date.now();
1427
1128
  this.emit('completionDetected', phrase);
1428
1129
  this.emit('loopUpdate', this.loopState);
1429
1130
  }
1430
1131
  /**
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)
1132
+ * Handle a detected completion phrase.
1438
1133
  */
1439
1134
  handleCompletionPhrase(phrase) {
1440
1135
  const count = (this._completionPhraseCount.get(phrase) || 0) + 1;
1441
1136
  this._completionPhraseCount.set(phrase, count);
1442
1137
  // Trim completion phrase map if it exceeds the limit
1443
1138
  if (this._completionPhraseCount.size > MAX_COMPLETION_PHRASE_ENTRIES) {
1444
- // Keep only the most important entries (current expected phrase and highest counts)
1445
1139
  const entries = Array.from(this._completionPhraseCount.entries());
1446
- entries.sort((a, b) => b[1] - a[1]); // Sort by count descending
1140
+ entries.sort((a, b) => b[1] - a[1]);
1447
1141
  this._completionPhraseCount.clear();
1448
- // Keep top half of entries
1449
1142
  const keepCount = Math.floor(MAX_COMPLETION_PHRASE_ENTRIES / 2);
1450
1143
  for (let i = 0; i < Math.min(keepCount, entries.length); i++) {
1451
1144
  this._completionPhraseCount.set(entries[i][0], entries[i][1]);
1452
1145
  }
1453
- // Always keep the expected phrase if set
1454
1146
  if (this._loopState.completionPhrase && !this._completionPhraseCount.has(this._loopState.completionPhrase)) {
1455
1147
  this._completionPhraseCount.set(this._loopState.completionPhrase, 1);
1456
1148
  }
@@ -1459,23 +1151,16 @@ export class RalphTracker extends EventEmitter {
1459
1151
  if (!this._loopState.completionPhrase) {
1460
1152
  this._loopState.completionPhrase = phrase;
1461
1153
  this._loopState.lastActivity = Date.now();
1462
- // P1-002: Validate phrase and emit warning if risky
1463
1154
  this.validateCompletionPhrase(phrase);
1464
1155
  this.emit('loopUpdate', this.loopState);
1465
1156
  }
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
1157
+ // Check for fuzzy match with primary phrase or any alternate phrase
1468
1158
  const matchedPhrase = this.findMatchingCompletionPhrase(phrase);
1469
1159
  if (matchedPhrase) {
1470
- // Use the matched phrase (canonical) for tracking
1471
1160
  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
1161
  if (canonicalCount >= 2 || this._loopState.active) {
1475
- // Mark as completion
1476
1162
  this._loopState.active = false;
1477
1163
  this._loopState.lastActivity = Date.now();
1478
- // Mark all todos as complete
1479
1164
  let updated = false;
1480
1165
  for (const todo of this._todos.values()) {
1481
1166
  if (todo.status !== 'completed') {
@@ -1491,9 +1176,7 @@ export class RalphTracker extends EventEmitter {
1491
1176
  return;
1492
1177
  }
1493
1178
  }
1494
- // Emit completion if loop is active OR this is 2nd+ occurrence
1495
1179
  if (this._loopState.active || count >= 2) {
1496
- // Mark all todos as complete when completion phrase is detected
1497
1180
  let updated = false;
1498
1181
  for (const todo of this._todos.values()) {
1499
1182
  if (todo.status !== 'completed') {
@@ -1512,39 +1195,17 @@ export class RalphTracker extends EventEmitter {
1512
1195
  }
1513
1196
  /**
1514
1197
  * 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
1198
  */
1526
1199
  isFuzzyPhraseMatch(phrase1, phrase2, maxDistance = 2) {
1527
1200
  return fuzzyPhraseMatch(phrase1, phrase2, maxDistance);
1528
1201
  }
1529
1202
  /**
1530
1203
  * 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
1204
  */
1542
1205
  validateCompletionPhrase(phrase) {
1543
1206
  const normalized = phrase.toUpperCase().replace(/[\s_\-.]+/g, '');
1544
- // Generate a suggested unique phrase
1545
1207
  const uniqueSuffix = Date.now().toString(36).slice(-4).toUpperCase();
1546
1208
  const suggestedPhrase = `${phrase}_${uniqueSuffix}`;
1547
- // Check for common phrases
1548
1209
  if (COMMON_COMPLETION_PHRASES.has(normalized)) {
1549
1210
  console.warn(`[RalphTracker] Warning: Completion phrase "${phrase}" is very common and may cause false positives. Consider using: "${suggestedPhrase}"`);
1550
1211
  this.emit('phraseValidationWarning', {
@@ -1554,7 +1215,6 @@ export class RalphTracker extends EventEmitter {
1554
1215
  });
1555
1216
  return;
1556
1217
  }
1557
- // Check for short phrases
1558
1218
  if (normalized.length < MIN_RECOMMENDED_PHRASE_LENGTH) {
1559
1219
  console.warn(`[RalphTracker] Warning: Completion phrase "${phrase}" is too short (${normalized.length} chars). Consider using: "${suggestedPhrase}"`);
1560
1220
  this.emit('phraseValidationWarning', {
@@ -1564,7 +1224,6 @@ export class RalphTracker extends EventEmitter {
1564
1224
  });
1565
1225
  return;
1566
1226
  }
1567
- // Check for numeric-only phrases
1568
1227
  if (/^\d+$/.test(normalized)) {
1569
1228
  console.warn(`[RalphTracker] Warning: Completion phrase "${phrase}" is numeric-only and may cause false positives. Consider using: "${suggestedPhrase}"`);
1570
1229
  this.emit('phraseValidationWarning', {
@@ -1576,12 +1235,6 @@ export class RalphTracker extends EventEmitter {
1576
1235
  }
1577
1236
  /**
1578
1237
  * 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
1238
  */
1586
1239
  activateLoopIfNeeded() {
1587
1240
  if (this._loopState.active)
@@ -1597,131 +1250,77 @@ export class RalphTracker extends EventEmitter {
1597
1250
  }
1598
1251
  /**
1599
1252
  * 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
1253
  */
1613
1254
  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
1255
  if (RALPH_START_PATTERN.test(line) || LOOP_START_PATTERN.test(line)) {
1617
1256
  this.activateLoopIfNeeded();
1618
1257
  }
1619
- // Check for max iterations setting
1620
1258
  const maxIterMatch = line.match(MAX_ITERATIONS_PATTERN);
1621
1259
  if (maxIterMatch) {
1622
1260
  const maxIter = parseInt(maxIterMatch[1], 10);
1623
1261
  if (!Number.isNaN(maxIter) && maxIter > 0) {
1624
1262
  this._loopState.maxIterations = maxIter;
1625
1263
  this._loopState.lastActivity = Date.now();
1626
- // Use debounced emit for settings changes
1627
1264
  this.emitLoopUpdateDebounced();
1628
1265
  }
1629
1266
  }
1630
- // Check for iteration patterns: "Iteration 5/50", "[5/50]"
1631
1267
  const iterMatch = line.match(ITERATION_PATTERN);
1632
1268
  if (iterMatch) {
1633
- // Pattern captures: group 1&2 for "Iteration X/Y", group 3&4 for "[X/Y]"
1634
1269
  const currentIter = parseInt(iterMatch[1] || iterMatch[3], 10);
1635
1270
  const maxIterStr = iterMatch[2] || iterMatch[4];
1636
1271
  const maxIter = maxIterStr ? parseInt(maxIterStr, 10) : null;
1637
1272
  if (!Number.isNaN(currentIter)) {
1638
1273
  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
- }
1274
+ // Track iteration changes for stall detection and circuit breaker
1275
+ if (currentIter !== this.stallDetector.getIterationStallMetrics().currentIteration) {
1276
+ this.stallDetector.notifyIterationChanged(currentIter);
1277
+ this.statusParser.notifyIterationProgress(currentIter);
1660
1278
  }
1661
1279
  this._loopState.cycleCount = currentIter;
1280
+ // Notify sub-modules of cycle count
1281
+ this.planTracker.notifyCycleCount(currentIter);
1282
+ this.statusParser.setCycleCount(currentIter);
1283
+ this.stallDetector.setLoopActive(true);
1662
1284
  if (maxIter !== null && !Number.isNaN(maxIter)) {
1663
1285
  this._loopState.maxIterations = maxIter;
1664
1286
  }
1665
1287
  this._loopState.lastActivity = Date.now();
1666
- // Use debounced emit for rapid iteration updates
1667
1288
  this.emitLoopUpdateDebounced();
1668
1289
  }
1669
1290
  }
1670
- // Check for elapsed time
1671
1291
  const elapsedMatch = line.match(ELAPSED_TIME_PATTERN);
1672
1292
  if (elapsedMatch) {
1673
1293
  this._loopState.elapsedHours = parseFloat(elapsedMatch[1]);
1674
1294
  this._loopState.lastActivity = Date.now();
1675
- // Use debounced emit for elapsed time updates
1676
1295
  this.emitLoopUpdateDebounced();
1677
1296
  }
1678
- // Check for cycle count (legacy pattern)
1679
1297
  const cycleMatch = line.match(CYCLE_PATTERN);
1680
1298
  if (cycleMatch) {
1681
1299
  const cycleNum = parseInt(cycleMatch[1] || cycleMatch[2], 10);
1682
1300
  if (!Number.isNaN(cycleNum) && cycleNum > this._loopState.cycleCount) {
1683
1301
  this._loopState.cycleCount = cycleNum;
1684
1302
  this._loopState.lastActivity = Date.now();
1685
- // Use debounced emit for cycle updates
1686
1303
  this.emitLoopUpdateDebounced();
1687
1304
  }
1688
1305
  }
1689
- // Check for TodoWrite tool usage - indicates active task tracking
1690
1306
  if (TODOWRITE_PATTERN.test(line)) {
1691
1307
  this._loopState.lastActivity = Date.now();
1692
- // Don't emit update just for activity, let todo detection handle it
1693
1308
  }
1694
1309
  }
1695
1310
  /**
1696
1311
  * 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
1312
  */
1710
1313
  detectTodoItems(line) {
1711
- // Pre-compute which pattern categories might match (60-75% faster)
1712
1314
  const hasCheckbox = line.includes('[');
1713
1315
  const hasTodoIndicator = line.includes('Todo:');
1714
1316
  const hasNativeCheckbox = line.includes('☐') || line.includes('☒') || line.includes('◐') || line.includes('✓');
1715
1317
  const hasStatus = line.includes('(pending)') || line.includes('(in_progress)') || line.includes('(completed)');
1716
1318
  const hasCheckmark = line.includes('✔');
1717
- // Quick check: skip lines that can't possibly contain todos
1718
1319
  if (!hasCheckbox && !hasTodoIndicator && !hasNativeCheckbox && !hasStatus && !hasCheckmark) {
1719
1320
  return;
1720
1321
  }
1721
1322
  let updated = false;
1722
1323
  let match;
1723
- // Format 1: Checkbox format "- [ ] Task" or "- [x] Task"
1724
- // Only scan if line contains '[' character
1725
1324
  if (hasCheckbox) {
1726
1325
  TODO_CHECKBOX_PATTERN.lastIndex = 0;
1727
1326
  while ((match = TODO_CHECKBOX_PATTERN.exec(line)) !== null) {
@@ -1732,8 +1331,6 @@ export class RalphTracker extends EventEmitter {
1732
1331
  updated = true;
1733
1332
  }
1734
1333
  }
1735
- // Format 2: Todo with indicator icons
1736
- // Only scan if line contains 'Todo:' prefix
1737
1334
  if (hasTodoIndicator) {
1738
1335
  TODO_INDICATOR_PATTERN.lastIndex = 0;
1739
1336
  while ((match = TODO_INDICATOR_PATTERN.exec(line)) !== null) {
@@ -1744,8 +1341,6 @@ export class RalphTracker extends EventEmitter {
1744
1341
  updated = true;
1745
1342
  }
1746
1343
  }
1747
- // Format 3: Status in parentheses
1748
- // Only scan if line contains status in parentheses
1749
1344
  if (hasStatus) {
1750
1345
  TODO_STATUS_PATTERN.lastIndex = 0;
1751
1346
  while ((match = TODO_STATUS_PATTERN.exec(line)) !== null) {
@@ -1755,18 +1350,14 @@ export class RalphTracker extends EventEmitter {
1755
1350
  updated = true;
1756
1351
  }
1757
1352
  }
1758
- // Format 4: Claude Code native TodoWrite output (☐, ☒, ◐)
1759
- // Only scan if line contains native checkbox icons
1760
1353
  if (hasNativeCheckbox) {
1761
1354
  TODO_NATIVE_PATTERN.lastIndex = 0;
1762
1355
  while ((match = TODO_NATIVE_PATTERN.exec(line)) !== null) {
1763
1356
  const icon = match[1];
1764
1357
  const content = match[2].trim();
1765
- // Skip if content matches exclude patterns (tool invocations, commentary)
1766
1358
  const shouldExclude = TODO_EXCLUDE_PATTERNS.some((pattern) => pattern.test(content));
1767
1359
  if (shouldExclude)
1768
1360
  continue;
1769
- // Skip if content is too short or looks like partial garbage
1770
1361
  if (content.length < 5)
1771
1362
  continue;
1772
1363
  const status = this.iconToStatus(icon);
@@ -1774,10 +1365,7 @@ export class RalphTracker extends EventEmitter {
1774
1365
  updated = true;
1775
1366
  }
1776
1367
  }
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
1368
  if (hasCheckmark) {
1780
- // Task creation: "✔ Task #1 created: Fix the bug"
1781
1369
  TODO_TASK_CREATED_PATTERN.lastIndex = 0;
1782
1370
  while ((match = TODO_TASK_CREATED_PATTERN.exec(line)) !== null) {
1783
1371
  const taskNum = parseInt(match[1], 10);
@@ -1789,13 +1377,11 @@ export class RalphTracker extends EventEmitter {
1789
1377
  updated = true;
1790
1378
  }
1791
1379
  }
1792
- // Task summary: "✔ #1 Fix the bug"
1793
1380
  TODO_TASK_SUMMARY_PATTERN.lastIndex = 0;
1794
1381
  while ((match = TODO_TASK_SUMMARY_PATTERN.exec(line)) !== null) {
1795
1382
  const taskNum = parseInt(match[1], 10);
1796
1383
  const content = match[2].trim();
1797
1384
  if (content.length >= 5) {
1798
- // Only register if not already known from a "created" line
1799
1385
  if (!this._taskNumberToContent.has(taskNum)) {
1800
1386
  this._taskNumberToContent.set(taskNum, content);
1801
1387
  this.enforceTaskMappingLimit();
@@ -1804,7 +1390,6 @@ export class RalphTracker extends EventEmitter {
1804
1390
  updated = true;
1805
1391
  }
1806
1392
  }
1807
- // Status update: "✔ Task #1 updated: status → completed"
1808
1393
  TODO_TASK_STATUS_PATTERN.lastIndex = 0;
1809
1394
  while ((match = TODO_TASK_STATUS_PATTERN.exec(line)) !== null) {
1810
1395
  const taskNum = parseInt(match[1], 10);
@@ -1816,19 +1401,15 @@ export class RalphTracker extends EventEmitter {
1816
1401
  updated = true;
1817
1402
  }
1818
1403
  }
1819
- // Plain checkmark: "✔ Create hello.txt" (no task number)
1820
- // Only match if numbered patterns didn't already match on this line
1821
1404
  if (!updated) {
1822
1405
  TODO_PLAIN_CHECKMARK_PATTERN.lastIndex = 0;
1823
1406
  while ((match = TODO_PLAIN_CHECKMARK_PATTERN.exec(line)) !== null) {
1824
1407
  const content = match[1].trim();
1825
- // Skip if content matches exclude patterns
1826
1408
  const shouldExclude = TODO_EXCLUDE_PATTERNS.some((pattern) => pattern.test(content));
1827
1409
  if (shouldExclude)
1828
1410
  continue;
1829
1411
  if (content.length < 5)
1830
1412
  continue;
1831
- // Skip status/created/updated prefixed content (already handled above)
1832
1413
  if (/^(Task\s*#\d+|#\d+)\s/.test(content))
1833
1414
  continue;
1834
1415
  this.upsertTodo(content, 'completed');
@@ -1837,68 +1418,46 @@ export class RalphTracker extends EventEmitter {
1837
1418
  }
1838
1419
  }
1839
1420
  if (updated) {
1840
- // Use debounced emit to batch rapid todo updates and reduce UI jitter
1841
1421
  this.emitTodoUpdateDebounced();
1842
1422
  }
1843
1423
  }
1844
1424
  /**
1845
1425
  * 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
1426
  */
1855
1427
  iconToStatus(icon) {
1856
1428
  switch (icon) {
1857
1429
  case '✓':
1858
1430
  case '✅':
1859
- case '☒': // Claude Code checked checkbox
1860
- case '◉': // Filled circle (completed)
1861
- case '●': // Solid circle (completed)
1431
+ case '☒':
1432
+ case '◉':
1433
+ case '●':
1862
1434
  return 'completed';
1863
- case '◐': // Half-filled circle (in progress)
1435
+ case '◐':
1864
1436
  case '⏳':
1865
1437
  case '⌛':
1866
1438
  case '🔄':
1867
1439
  return 'in_progress';
1868
- case '☐': // Claude Code empty checkbox
1869
- case '○': // Empty circle
1440
+ case '☐':
1441
+ case '○':
1870
1442
  default:
1871
1443
  return 'pending';
1872
1444
  }
1873
1445
  }
1874
1446
  /**
1875
1447
  * 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
1448
  */
1886
1449
  parsePriority(content) {
1887
1450
  const upper = content.toUpperCase();
1888
- // Check P0 first (highest priority wins)
1889
- // Uses pre-compiled module-level patterns for performance
1890
1451
  for (const pattern of P0_PRIORITY_PATTERNS) {
1891
1452
  if (pattern.test(upper)) {
1892
1453
  return 'P0';
1893
1454
  }
1894
1455
  }
1895
- // Check P1
1896
1456
  for (const pattern of P1_PRIORITY_PATTERNS) {
1897
1457
  if (pattern.test(upper)) {
1898
1458
  return 'P1';
1899
1459
  }
1900
1460
  }
1901
- // Check P2
1902
1461
  for (const pattern of P2_PRIORITY_PATTERNS) {
1903
1462
  if (pattern.test(upper)) {
1904
1463
  return 'P2';
@@ -1908,82 +1467,51 @@ export class RalphTracker extends EventEmitter {
1908
1467
  }
1909
1468
  /**
1910
1469
  * 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
1470
  */
1923
1471
  upsertTodo(content, status) {
1924
- // Skip empty or whitespace-only content
1925
1472
  if (!content || !content.trim())
1926
1473
  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();
1474
+ const cleanContent = content.replace(ANSI_ESCAPE_PATTERN_SIMPLE, '').replace(/\s+/g, ' ').trim();
1932
1475
  if (cleanContent.length < 5)
1933
- return; // Skip very short content
1934
- // Parse priority from content
1476
+ return;
1935
1477
  const priority = this.parsePriority(cleanContent);
1936
- // P1-009: Estimate complexity for duration tracking
1937
1478
  const estimatedComplexity = this.estimateComplexity(cleanContent);
1938
- // Generate a stable ID from normalized content
1939
1479
  const id = this.generateTodoId(cleanContent);
1940
1480
  const existing = this._todos.get(id);
1941
1481
  if (existing) {
1942
- // P1-009: Track status transitions for progress estimation
1943
1482
  const wasCompleted = existing.status === 'completed';
1944
1483
  const isNowCompleted = status === 'completed';
1945
1484
  const wasInProgress = existing.status === 'in_progress';
1946
1485
  const isNowInProgress = status === 'in_progress';
1947
- // Update existing todo (exact match by ID)
1948
1486
  existing.status = status;
1949
1487
  existing.detectedAt = Date.now();
1950
- // Update priority if parsed (don't overwrite with null)
1951
1488
  if (priority)
1952
1489
  existing.priority = priority;
1953
- // Update complexity estimate if not already set
1954
1490
  if (!existing.estimatedComplexity) {
1955
1491
  existing.estimatedComplexity = estimatedComplexity;
1956
1492
  }
1957
- // P1-009: Track completion time
1958
1493
  if (!wasCompleted && isNowCompleted) {
1959
1494
  this.recordTodoCompletion(id);
1960
1495
  }
1961
- // P1-009: Start tracking when status changes to in_progress
1962
1496
  if (!wasInProgress && isNowInProgress) {
1963
1497
  this.startTrackingTodo(id);
1964
1498
  }
1965
1499
  }
1966
1500
  else {
1967
- // P1-007: Check for similar existing todo (deduplication)
1968
1501
  const similar = this.findSimilarTodo(cleanContent);
1969
1502
  if (similar) {
1970
- // P1-009: Track status transitions on similar todo
1971
1503
  const wasCompleted = similar.status === 'completed';
1972
1504
  const isNowCompleted = status === 'completed';
1973
1505
  const wasInProgress = similar.status === 'in_progress';
1974
1506
  const isNowInProgress = status === 'in_progress';
1975
- // Update similar todo instead of creating duplicate
1976
1507
  similar.status = status;
1977
1508
  similar.detectedAt = Date.now();
1978
- // Update priority if new content has priority and existing doesn't
1979
1509
  if (priority && !similar.priority) {
1980
1510
  similar.priority = priority;
1981
1511
  }
1982
- // Keep the longer/more descriptive content
1983
1512
  if (cleanContent.length > similar.content.length) {
1984
1513
  similar.content = cleanContent;
1985
1514
  }
1986
- // P1-009: Track completion time
1987
1515
  if (!wasCompleted && isNowCompleted) {
1988
1516
  this.recordTodoCompletion(similar.id);
1989
1517
  }
@@ -1992,20 +1520,17 @@ export class RalphTracker extends EventEmitter {
1992
1520
  }
1993
1521
  return;
1994
1522
  }
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
1523
  while (this._todos.size >= MAX_TODOS_PER_SESSION) {
1998
1524
  const oldest = this.findOldestTodo();
1999
1525
  if (oldest) {
2000
1526
  this._todos.delete(oldest.id);
2001
1527
  }
2002
1528
  else {
2003
- // Safety valve: if somehow no oldest found, clear a random entry
2004
1529
  const firstKey = this._todos.keys().next().value;
2005
1530
  if (firstKey)
2006
1531
  this._todos.delete(firstKey);
2007
1532
  else
2008
- break; // Map is empty somehow, exit loop
1533
+ break;
2009
1534
  }
2010
1535
  }
2011
1536
  const estimatedDurationMs = this.getEstimatedDuration(estimatedComplexity);
@@ -2018,7 +1543,6 @@ export class RalphTracker extends EventEmitter {
2018
1543
  estimatedComplexity,
2019
1544
  estimatedDurationMs,
2020
1545
  });
2021
- // P1-009: Start tracking if already in_progress
2022
1546
  if (status === 'in_progress') {
2023
1547
  this.startTrackingTodo(id);
2024
1548
  }
@@ -2026,71 +1550,39 @@ export class RalphTracker extends EventEmitter {
2026
1550
  }
2027
1551
  /**
2028
1552
  * 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
1553
  */
2041
1554
  normalizeTodoContent(content) {
2042
1555
  if (!content)
2043
1556
  return '';
2044
1557
  return content
2045
- .replace(/\s+/g, ' ') // Collapse whitespace
2046
- .replace(/[^a-zA-Z0-9\s.,!?'"-]/g, '') // Remove special chars (keep punctuation)
1558
+ .replace(/\s+/g, ' ')
1559
+ .replace(/[^a-zA-Z0-9\s.,!?'"-]/g, '')
2047
1560
  .trim()
2048
1561
  .toLowerCase();
2049
1562
  }
2050
1563
  /**
2051
1564
  * 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
1565
  */
2062
1566
  calculateSimilarity(str1, str2) {
2063
1567
  const norm1 = this.normalizeTodoContent(str1);
2064
1568
  const norm2 = this.normalizeTodoContent(str2);
2065
- // Identical after normalization
2066
1569
  if (norm1 === norm2)
2067
1570
  return 1.0;
2068
- // If either is empty, no similarity
2069
1571
  if (!norm1 || !norm2)
2070
1572
  return 0.0;
2071
- // Method 1: Levenshtein-based similarity (good for typos/minor edits)
2072
1573
  const levenshteinSim = stringSimilarity(norm1, norm2);
2073
- // Method 2: Bigram/Dice similarity (good for word reordering)
2074
1574
  const bigramSim = this.calculateBigramSimilarity(norm1, norm2);
2075
- // Return the higher of the two scores
2076
1575
  return Math.max(levenshteinSim, bigramSim);
2077
1576
  }
2078
1577
  /**
2079
1578
  * 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
1579
  */
2086
1580
  calculateBigramSimilarity(norm1, norm2) {
2087
- // Short strings: use simple character overlap
2088
1581
  if (norm1.length < 3 || norm2.length < 3) {
2089
1582
  const shorter = norm1.length <= norm2.length ? norm1 : norm2;
2090
1583
  const longer = norm1.length > norm2.length ? norm1 : norm2;
2091
1584
  return longer.includes(shorter) ? 0.9 : 0.0;
2092
1585
  }
2093
- // Extract bigrams (pairs of consecutive characters)
2094
1586
  const getBigrams = (s) => {
2095
1587
  const bigrams = new Set();
2096
1588
  for (let i = 0; i < s.length - 1; i++) {
@@ -2100,14 +1592,12 @@ export class RalphTracker extends EventEmitter {
2100
1592
  };
2101
1593
  const bigrams1 = getBigrams(norm1);
2102
1594
  const bigrams2 = getBigrams(norm2);
2103
- // Count intersection
2104
1595
  let intersection = 0;
2105
1596
  for (const bigram of bigrams1) {
2106
1597
  if (bigrams2.has(bigram)) {
2107
1598
  intersection++;
2108
1599
  }
2109
1600
  }
2110
- // Dice coefficient: 2 * intersection / (total bigrams)
2111
1601
  const totalBigrams = bigrams1.size + bigrams2.size;
2112
1602
  if (totalBigrams === 0)
2113
1603
  return 0.0;
@@ -2115,32 +1605,18 @@ export class RalphTracker extends EventEmitter {
2115
1605
  }
2116
1606
  /**
2117
1607
  * 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
1608
  */
2131
1609
  findSimilarTodo(content) {
2132
1610
  const normalized = this.normalizeTodoContent(content);
2133
- // Determine appropriate threshold based on string length
2134
- // Shorter strings need higher threshold to avoid false positives
2135
1611
  let threshold;
2136
1612
  if (normalized.length < 30) {
2137
- threshold = 0.95; // Very strict for short strings
1613
+ threshold = 0.95;
2138
1614
  }
2139
1615
  else if (normalized.length < 60) {
2140
- threshold = 0.9; // Strict for medium strings
1616
+ threshold = 0.9;
2141
1617
  }
2142
1618
  else {
2143
- threshold = TODO_SIMILARITY_THRESHOLD; // 0.85 for longer strings
1619
+ threshold = TODO_SIMILARITY_THRESHOLD;
2144
1620
  }
2145
1621
  let bestMatch;
2146
1622
  let bestSimilarity = 0;
@@ -2156,14 +1632,9 @@ export class RalphTracker extends EventEmitter {
2156
1632
  // ========== P1-009: Progress Estimation Methods ==========
2157
1633
  /**
2158
1634
  * 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
1635
  */
2164
1636
  estimateComplexity(content) {
2165
1637
  const lower = content.toLowerCase();
2166
- // Trivial: Simple fixes, typos, documentation
2167
1638
  const trivialPatterns = [
2168
1639
  /\btypo\b/,
2169
1640
  /\bspelling\b/,
@@ -2172,7 +1643,6 @@ export class RalphTracker extends EventEmitter {
2172
1643
  /\brename\b/,
2173
1644
  /\bformat(?:ting)?\b/,
2174
1645
  ];
2175
- // Complex: Architecture, refactoring, security, testing
2176
1646
  const complexPatterns = [
2177
1647
  /\barchitect(?:ure)?\b/,
2178
1648
  /\brefactor\b/,
@@ -2185,7 +1655,6 @@ export class RalphTracker extends EventEmitter {
2185
1655
  /\boptimiz(?:e|ation)\b/,
2186
1656
  /\bmultiple\s+files?\b/,
2187
1657
  ];
2188
- // Moderate: Bugs, features, enhancements
2189
1658
  const moderatePatterns = [/\bbug\b/, /\bfeature\b/, /\benhance(?:ment)?\b/, /\bimplement\b/, /\badd\b/, /\bfix\b/];
2190
1659
  for (const pattern of complexPatterns) {
2191
1660
  if (pattern.test(lower))
@@ -2203,13 +1672,8 @@ export class RalphTracker extends EventEmitter {
2203
1672
  }
2204
1673
  /**
2205
1674
  * 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
1675
  */
2211
1676
  getEstimatedDuration(complexity) {
2212
- // If we have historical data, use average adjusted by complexity
2213
1677
  const avgTime = this.getAverageCompletionTime();
2214
1678
  if (avgTime !== null) {
2215
1679
  const multipliers = {
@@ -2220,18 +1684,16 @@ export class RalphTracker extends EventEmitter {
2220
1684
  };
2221
1685
  return Math.round(avgTime * multipliers[complexity]);
2222
1686
  }
2223
- // Default estimates (in ms) based on typical task durations
2224
1687
  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
1688
+ trivial: 1 * 60 * 1000,
1689
+ simple: 3 * 60 * 1000,
1690
+ moderate: 10 * 60 * 1000,
1691
+ complex: 30 * 60 * 1000,
2229
1692
  };
2230
1693
  return defaults[complexity];
2231
1694
  }
2232
1695
  /**
2233
1696
  * Get average completion time from historical data.
2234
- * @returns Average time in ms, or null if no data
2235
1697
  */
2236
1698
  getAverageCompletionTime() {
2237
1699
  if (this._completionTimes.length === 0)
@@ -2241,14 +1703,12 @@ export class RalphTracker extends EventEmitter {
2241
1703
  }
2242
1704
  /**
2243
1705
  * Record a todo completion for progress tracking.
2244
- * @param todoId - ID of the completed todo
2245
1706
  */
2246
1707
  recordTodoCompletion(todoId) {
2247
1708
  const startTime = this._todoStartTimes.get(todoId);
2248
1709
  if (startTime) {
2249
1710
  const duration = Date.now() - startTime;
2250
1711
  this._completionTimes.push(duration);
2251
- // Keep only recent completion times
2252
1712
  while (this._completionTimes.length > RalphTracker.MAX_COMPLETION_TIMES) {
2253
1713
  this._completionTimes.shift();
2254
1714
  }
@@ -2257,23 +1717,17 @@ export class RalphTracker extends EventEmitter {
2257
1717
  }
2258
1718
  /**
2259
1719
  * Start tracking a todo for duration estimation.
2260
- * @param todoId - ID of the todo being started
2261
1720
  */
2262
1721
  startTrackingTodo(todoId) {
2263
1722
  if (!this._todoStartTimes.has(todoId)) {
2264
1723
  this._todoStartTimes.set(todoId, Date.now());
2265
1724
  }
2266
- // Initialize session tracking if needed
2267
1725
  if (this._todosStartedAt === 0) {
2268
1726
  this._todosStartedAt = Date.now();
2269
1727
  }
2270
1728
  }
2271
1729
  /**
2272
1730
  * 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
1731
  */
2278
1732
  getTodoProgress() {
2279
1733
  const todos = Array.from(this._todos.values());
@@ -2282,19 +1736,16 @@ export class RalphTracker extends EventEmitter {
2282
1736
  const inProgress = todos.filter((t) => t.status === 'in_progress').length;
2283
1737
  const pending = todos.filter((t) => t.status === 'pending').length;
2284
1738
  const percentComplete = total > 0 ? Math.round((completed / total) * 100) : 0;
2285
- // Calculate estimated remaining time
2286
1739
  let estimatedRemainingMs = null;
2287
1740
  let avgCompletionTimeMs = null;
2288
1741
  let projectedCompletionAt = null;
2289
1742
  avgCompletionTimeMs = this.getAverageCompletionTime();
2290
1743
  if (total > 0 && completed > 0) {
2291
- // Method 1: Use historical average if available
2292
1744
  if (avgCompletionTimeMs !== null) {
2293
1745
  const remaining = total - completed;
2294
1746
  estimatedRemainingMs = remaining * avgCompletionTimeMs;
2295
1747
  }
2296
1748
  else {
2297
- // Method 2: Calculate based on elapsed time and progress
2298
1749
  const elapsed = Date.now() - this._todosStartedAt;
2299
1750
  if (elapsed > 0 && completed > 0) {
2300
1751
  const timePerTodo = elapsed / completed;
@@ -2303,13 +1754,11 @@ export class RalphTracker extends EventEmitter {
2303
1754
  estimatedRemainingMs = Math.round(remaining * timePerTodo);
2304
1755
  }
2305
1756
  }
2306
- // Calculate projected completion timestamp
2307
1757
  if (estimatedRemainingMs !== null) {
2308
1758
  projectedCompletionAt = Date.now() + estimatedRemainingMs;
2309
1759
  }
2310
1760
  }
2311
1761
  else if (total > 0 && completed === 0) {
2312
- // No completions yet - use complexity-based estimates
2313
1762
  let totalEstimate = 0;
2314
1763
  for (const todo of todos) {
2315
1764
  if (todo.status !== 'completed') {
@@ -2331,35 +1780,17 @@ export class RalphTracker extends EventEmitter {
2331
1780
  projectedCompletionAt,
2332
1781
  };
2333
1782
  }
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
1783
  /**
2344
1784
  * 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
1785
  */
2352
1786
  generateTodoId(content) {
2353
1787
  if (!content)
2354
1788
  return 'todo-empty';
2355
- // Use centralized hashing utility
2356
1789
  const hash = todoContentHash(content);
2357
1790
  return `todo-${hash}`;
2358
1791
  }
2359
1792
  /**
2360
1793
  * 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
1794
  */
2364
1795
  findOldestTodo() {
2365
1796
  let oldest;
@@ -2372,7 +1803,6 @@ export class RalphTracker extends EventEmitter {
2372
1803
  }
2373
1804
  /**
2374
1805
  * Conditionally run cleanup, throttled to CLEANUP_THROTTLE_MS.
2375
- * Prevents cleanup from running on every data chunk (performance).
2376
1806
  */
2377
1807
  maybeCleanupExpiredTodos() {
2378
1808
  const now = Date.now();
@@ -2384,8 +1814,6 @@ export class RalphTracker extends EventEmitter {
2384
1814
  }
2385
1815
  /**
2386
1816
  * 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
1817
  */
2390
1818
  cleanupExpiredTodos() {
2391
1819
  const now = Date.now();
@@ -2404,19 +1832,9 @@ export class RalphTracker extends EventEmitter {
2404
1832
  }
2405
1833
  /**
2406
1834
  * 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
1835
  */
2418
1836
  startLoop(completionPhrase, maxIterations) {
2419
- this.enable(); // Ensure tracker is enabled
1837
+ this.enable();
2420
1838
  this._loopState.active = true;
2421
1839
  this._loopState.startedAt = Date.now();
2422
1840
  this._loopState.cycleCount = 0;
@@ -2430,9 +1848,6 @@ export class RalphTracker extends EventEmitter {
2430
1848
  }
2431
1849
  /**
2432
1850
  * 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
1851
  */
2437
1852
  setMaxIterations(maxIterations) {
2438
1853
  this._loopState.maxIterations = maxIterations;
@@ -2440,11 +1855,7 @@ export class RalphTracker extends EventEmitter {
2440
1855
  this.emit('loopUpdate', this.loopState);
2441
1856
  }
2442
1857
  /**
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
1858
+ * Configure the tracker from external state.
2448
1859
  */
2449
1860
  configure(config) {
2450
1861
  if (config.enabled !== undefined) {
@@ -2461,11 +1872,6 @@ export class RalphTracker extends EventEmitter {
2461
1872
  }
2462
1873
  /**
2463
1874
  * 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
1875
  */
2470
1876
  stopLoop() {
2471
1877
  this._loopState.active = false;
@@ -2474,12 +1880,10 @@ export class RalphTracker extends EventEmitter {
2474
1880
  }
2475
1881
  /**
2476
1882
  * Enforce size limit on _taskNumberToContent map.
2477
- * Removes lowest task numbers (oldest tasks) when limit exceeded.
2478
1883
  */
2479
1884
  enforceTaskMappingLimit() {
2480
1885
  if (this._taskNumberToContent.size <= MAX_TASK_MAPPINGS)
2481
1886
  return;
2482
- // Sort keys and remove lowest (oldest) task numbers
2483
1887
  const sortedKeys = Array.from(this._taskNumberToContent.keys()).sort((a, b) => a - b);
2484
1888
  const keysToRemove = sortedKeys.slice(0, this._taskNumberToContent.size - MAX_TASK_MAPPINGS);
2485
1889
  for (const key of keysToRemove) {
@@ -2488,21 +1892,12 @@ export class RalphTracker extends EventEmitter {
2488
1892
  }
2489
1893
  /**
2490
1894
  * 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
1895
  */
2498
1896
  clear() {
2499
- // Clear debounce timers to prevent stale emissions after clear
2500
1897
  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
1898
+ this.fixPlanWatcher.stop();
1899
+ this.stallDetector.stopIterationStallDetection();
1900
+ this._loopState = createInitialRalphTrackerState();
2506
1901
  this._todos.clear();
2507
1902
  this._taskNumberToContent.clear();
2508
1903
  this._todoStartTimes.clear();
@@ -2510,26 +1905,15 @@ export class RalphTracker extends EventEmitter {
2510
1905
  this._lineBuffer = '';
2511
1906
  this._partialPromiseBuffer = '';
2512
1907
  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();
1908
+ // Clear sub-module state
1909
+ this.statusParser.fullReset();
1910
+ this.planTracker.fullReset();
1911
+ this.stallDetector.reset();
2522
1912
  this.emit('loopUpdate', this.loopState);
2523
1913
  this.emit('todoUpdate', this.todos);
2524
1914
  }
2525
1915
  /**
2526
1916
  * 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
1917
  */
2534
1918
  getTodoStats() {
2535
1919
  let pending = 0;
@@ -2557,786 +1941,38 @@ export class RalphTracker extends EventEmitter {
2557
1941
  }
2558
1942
  /**
2559
1943
  * 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
1944
  */
2569
1945
  restoreState(loopState, todos) {
2570
- // Ensure enabled flag exists (backwards compatibility)
2571
1946
  this._loopState = {
2572
1947
  ...loopState,
2573
- enabled: loopState.enabled ?? false, // Override after spread for backwards compat
1948
+ enabled: loopState.enabled ?? false,
2574
1949
  };
2575
1950
  this._todos.clear();
2576
1951
  for (const todo of todos) {
2577
- // Backwards compatibility: ensure priority field exists
2578
1952
  this._todos.set(todo.id, {
2579
1953
  ...todo,
2580
1954
  priority: todo.priority ?? null,
2581
1955
  });
2582
1956
  }
2583
1957
  }
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
1958
  /**
3320
1959
  * 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
1960
  */
3325
1961
  destroy() {
3326
- this.clearDebounceTimers();
3327
- this.stopWatchingFixPlan();
3328
- this.stopIterationStallDetection();
1962
+ this._todoDeb.dispose();
1963
+ this._loopDeb.dispose();
1964
+ this.fixPlanWatcher.destroy();
1965
+ this.stallDetector.destroy();
1966
+ this.statusParser.destroy();
1967
+ this.planTracker.destroy();
3329
1968
  this._todos.clear();
3330
1969
  this._taskNumberToContent.clear();
3331
1970
  this._todoStartTimes.clear();
3332
1971
  this._alternateCompletionPhrases.clear();
3333
1972
  this._completionPhraseCount.clear();
3334
- this._planTasks.clear();
3335
1973
  this._completionTimes.length = 0;
3336
1974
  this._lineBuffer = '';
3337
1975
  this._partialPromiseBuffer = '';
3338
- this._statusBlockBuffer.length = 0;
3339
- this._planHistory.length = 0;
3340
1976
  this.removeAllListeners();
3341
1977
  }
3342
1978
  }