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