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