From a41240049ee02c6ffe2380c08353c1cb99b5e8e1 Mon Sep 17 00:00:00 2001 From: arkon Date: Fri, 23 Jan 2026 22:14:00 +0100 Subject: [PATCH] feat: disable Ralph/Todo tracker auto-enable by default Ralph tracker no longer auto-enables on pattern detection. It must be explicitly enabled per-session (via API) or globally via the new `ralphEnabled` AppConfig setting. Adds GET/PUT /api/config endpoints for runtime configuration. Spawn agent ralph enable is unchanged. Co-Authored-By: Claude Opus 4.5 --- src/ralph-tracker.ts | 2 +- src/types.ts | 3 ++ src/web/server.ts | 38 +++++++++++++++++---- test/ralph-tracker.test.ts | 70 ++++++++++++++++++++++++-------------- 4 files changed, 80 insertions(+), 33 deletions(-) diff --git a/src/ralph-tracker.ts b/src/ralph-tracker.ts index 8273d254..5ad70068 100644 --- a/src/ralph-tracker.ts +++ b/src/ralph-tracker.ts @@ -310,7 +310,7 @@ export class RalphTracker extends EventEmitter { private _loopUpdatePending: boolean = false; /** When true, prevents auto-enable on pattern detection */ - private _autoEnableDisabled: boolean = false; + private _autoEnableDisabled: boolean = true; /** Maps task numbers from "✔ Task #N" format to their content for status updates */ private _taskNumberToContent: Map = new Map(); diff --git a/src/types.ts b/src/types.ts index 7902b3c5..8c801e34 100644 --- a/src/types.ts +++ b/src/types.ts @@ -229,6 +229,8 @@ export interface AppConfig { respawn: RespawnConfig; /** Last used case name (for default selection) */ lastUsedCase: string | null; + /** Whether Ralph/Todo tracker is globally enabled for all new sessions */ + ralphEnabled: boolean; } // ========== Output Types ========== @@ -564,6 +566,7 @@ export const DEFAULT_CONFIG: AppConfig = { sendInit: true, // send /init after /clear }, lastUsedCase: null, + ralphEnabled: false, }; // ========== Inner Loop Tracking Types ========== diff --git a/src/web/server.ts b/src/web/server.ts index fe096f13..8c4533a4 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -213,6 +213,19 @@ export class WebServer extends EventEmitter { // API Routes this.app.get('/api/status', async () => this.getFullState()); + this.app.get('/api/config', async () => { + return { success: true, config: this.store.getConfig() }; + }); + + this.app.put('/api/config', async (req) => { + const body = req.body as Record | null; + if (!body || typeof body !== 'object') { + return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Request body must be a JSON object'); + } + this.store.setConfig(body as Partial>); + return { success: true, config: this.store.getConfig() }; + }); + // Session management this.app.get('/api/sessions', async () => this.getSessionsState()); @@ -449,8 +462,13 @@ export class WebServer extends EventEmitter { } try { - // Auto-detect completion phrase from CLAUDE.md BEFORE starting - autoConfigureRalph(session, session.workingDir, () => {}); + // Auto-detect completion phrase from CLAUDE.md BEFORE starting (only if globally enabled) + if (this.store.getConfig().ralphEnabled) { + autoConfigureRalph(session, session.workingDir, () => {}); + if (!session.ralphTracker.enabled) { + session.ralphTracker.enable(); + } + } await session.startInteractive(); this.broadcast('session:interactive', { id }); @@ -723,8 +741,13 @@ export class WebServer extends EventEmitter { } try { - // Auto-detect completion phrase from CLAUDE.md BEFORE starting - autoConfigureRalph(session, session.workingDir, () => {}); + // Auto-detect completion phrase from CLAUDE.md BEFORE starting (only if globally enabled) + if (this.store.getConfig().ralphEnabled) { + autoConfigureRalph(session, session.workingDir, () => {}); + if (!session.ralphTracker.enabled) { + session.ralphTracker.enable(); + } + } // Start interactive session await session.startInteractive(); @@ -1165,9 +1188,12 @@ export class WebServer extends EventEmitter { }); // Auto-detect completion phrase from CLAUDE.md BEFORE broadcasting - // so the initial state already has the phrase configured - if (mode === 'claude') { + // so the initial state already has the phrase configured (only if globally enabled) + if (mode === 'claude' && this.store.getConfig().ralphEnabled) { autoConfigureRalph(session, casePath, () => {}); // no broadcast yet + if (!session.ralphTracker.enabled) { + session.ralphTracker.enable(); + } } this.sessions.set(session.id, session); diff --git a/test/ralph-tracker.test.ts b/test/ralph-tracker.test.ts index 09144c1e..8f94b849 100644 --- a/test/ralph-tracker.test.ts +++ b/test/ralph-tracker.test.ts @@ -48,10 +48,40 @@ describe('RalphTracker', () => { expect(freshTracker.loopState.elapsedHours).toBeNull(); }); - it('should auto-enable on /ralph-loop:ralph-loop command', () => { + it('should not auto-enable by default (auto-enable disabled)', () => { + const freshTracker = new RalphTracker(); + freshTracker.processTerminalData('/ralph-loop:ralph-loop\n'); + + expect(freshTracker.enabled).toBe(false); + }); + + it('should not auto-enable on completion phrase by default', () => { + const freshTracker = new RalphTracker(); + freshTracker.processTerminalData('COMPLETE\n'); + + expect(freshTracker.enabled).toBe(false); + }); + + it('should not auto-enable on TodoWrite by default', () => { + const freshTracker = new RalphTracker(); + freshTracker.processTerminalData('TodoWrite: Todos have been modified\n'); + + expect(freshTracker.enabled).toBe(false); + }); + + it('should not auto-enable on todo checkboxes by default', () => { + const freshTracker = new RalphTracker(); + freshTracker.processTerminalData('- [ ] New task\n'); + + expect(freshTracker.enabled).toBe(false); + expect(freshTracker.todos).toHaveLength(0); + }); + + it('should auto-enable when enableAutoEnable() is called', () => { const freshTracker = new RalphTracker(); const enableHandler = vi.fn(); freshTracker.on('enabled', enableHandler); + freshTracker.enableAutoEnable(); freshTracker.processTerminalData('/ralph-loop:ralph-loop\n'); @@ -59,37 +89,17 @@ describe('RalphTracker', () => { expect(enableHandler).toHaveBeenCalled(); }); - it('should auto-enable on completion phrase', () => { - const freshTracker = new RalphTracker(); - freshTracker.processTerminalData('COMPLETE\n'); - - expect(freshTracker.enabled).toBe(true); - }); - - it('should auto-enable on TodoWrite detection', () => { - const freshTracker = new RalphTracker(); - freshTracker.processTerminalData('TodoWrite: Todos have been modified\n'); - - expect(freshTracker.enabled).toBe(true); - }); - - it('should auto-enable on todo checkboxes', () => { - const freshTracker = new RalphTracker(); - freshTracker.processTerminalData('- [ ] New task\n'); - - expect(freshTracker.enabled).toBe(true); - expect(freshTracker.todos).toHaveLength(1); - }); - - it('should auto-enable on iteration patterns', () => { + it('should auto-enable on iteration patterns when auto-enable allowed', () => { const freshTracker = new RalphTracker(); + freshTracker.enableAutoEnable(); freshTracker.processTerminalData('Iteration 5/50\n'); expect(freshTracker.enabled).toBe(true); }); - it('should auto-enable on loop start patterns', () => { + it('should auto-enable on loop start patterns when auto-enable allowed', () => { const freshTracker = new RalphTracker(); + freshTracker.enableAutoEnable(); freshTracker.processTerminalData('Loop started at 2024-01-15\n'); expect(freshTracker.enabled).toBe(true); @@ -378,10 +388,18 @@ describe('RalphTracker', () => { expect(todos.filter(t => t.status === 'completed')).toHaveLength(1); }); - it('should auto-enable on native todo pattern', () => { + it('should not auto-enable on native todo pattern by default', () => { const freshTracker = new RalphTracker(); freshTracker.processTerminalData('☐ New task\n'); + expect(freshTracker.enabled).toBe(false); + }); + + it('should auto-enable on native todo pattern when auto-enable allowed', () => { + const freshTracker = new RalphTracker(); + freshTracker.enableAutoEnable(); + freshTracker.processTerminalData('☐ New task\n'); + expect(freshTracker.enabled).toBe(true); }); });