From b3f808b5f585eff85d4147e51f67f822833f8950 Mon Sep 17 00:00:00 2001 From: arkon Date: Tue, 27 Jan 2026 18:05:03 +0100 Subject: [PATCH] chore: bump version to 0.1399 --- CLAUDE.md | 10 +++- package.json | 2 +- src/ralph-tracker.ts | 121 ++++++++++++++++++++++++++++++++++++++++++ src/web/public/app.js | 47 ++++++++++++---- src/web/server.ts | 8 +++ 5 files changed, 177 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e9f7f226..be191af3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,7 +38,7 @@ Claudeman is a Claude Code session manager with a web interface and autonomous R When adding new features, always ask: "Will this maintain responsiveness with 20 sessions and 50 agent windows?" -**Version**: 0.1398 (must match `package.json`) +**Version**: 0.1399 (must match `package.json`) **Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty @@ -610,6 +610,14 @@ Both wait for idle. Configure via `session.setAutoCompact()` / `session.setAutoC Detects Ralph loops and todos inside Claude sessions. **Disabled by default** but auto-enables when Ralph-related patterns are detected (promise tags, TodoWrite, iteration patterns, etc.). See `ralph-tracker.ts:shouldAutoEnable()` for the full pattern list. +**Auto-Loading @fix_plan.md**: When a session starts, the Ralph tracker automatically: +1. Loads existing `@fix_plan.md` from the working directory (if present) +2. Imports todos and shows them in the Ralph panel +3. Watches the file for changes and reloads when modified by Claude +4. Auto-enables the tracker if todos are found + +This means you can create a session in a folder with an existing `@fix_plan.md` and the tracker will immediately show and track those tasks. + **Auto-Configuration from Ralph Plugin State**: When a session starts, Claudeman reads `.claude/ralph-loop.local.md` to auto-configure: ```yaml diff --git a/package.json b/package.json index 8c83f872..50b0e97d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1398", + "version": "0.1399", "description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js", diff --git a/src/ralph-tracker.ts b/src/ralph-tracker.ts index e99a2af9..0038ee6d 100644 --- a/src/ralph-tracker.ts +++ b/src/ralph-tracker.ts @@ -14,6 +14,9 @@ */ import { EventEmitter } from 'node:events'; +import { readFile } from 'node:fs/promises'; +import { existsSync, FSWatcher, watch as fsWatch } from 'node:fs'; +import { join } from 'node:path'; import { RalphTrackerState, RalphTodoItem, @@ -429,6 +432,18 @@ export class RalphTracker extends EventEmitter { /** Cumulative tasks completed across all iterations */ private _totalTasksCompleted: number = 0; + /** Working directory for @fix_plan.md watching */ + private _workingDir: string | null = null; + + /** File watcher for @fix_plan.md */ + private _fixPlanWatcher: FSWatcher | null = null; + + /** Debounce timer for file change events */ + private _fixPlanReloadTimer: NodeJS.Timeout | null = null; + + /** Path to the @fix_plan.md file being watched */ + private _fixPlanPath: string | null = null; + /** * Creates a new RalphTracker instance. * Starts in disabled state until Ralph patterns are detected. @@ -461,6 +476,112 @@ export class RalphTracker extends EventEmitter { return this._autoEnableDisabled; } + /** + * Set the working directory and start watching @fix_plan.md. + * Automatically loads existing @fix_plan.md if present. + * @param workingDir - The session's working directory + */ + setWorkingDir(workingDir: string): void { + this._workingDir = workingDir; + this._fixPlanPath = join(workingDir, '@fix_plan.md'); + + // Try to load existing @fix_plan.md + this.loadFixPlanFromDisk(); + + // Start watching for changes + this.startWatchingFixPlan(); + } + + /** + * Load @fix_plan.md from disk if it exists. + * Called on initialization and when file changes are detected. + */ + async loadFixPlanFromDisk(): Promise { + if (!this._fixPlanPath) return 0; + + try { + if (!existsSync(this._fixPlanPath)) { + return 0; + } + + const content = await readFile(this._fixPlanPath, 'utf-8'); + const count = this.importFixPlanMarkdown(content); + + if (count > 0) { + // Auto-enable tracker when we have todos from @fix_plan.md + if (!this._loopState.enabled) { + this.enable(); + } + console.log(`[RalphTracker] Loaded ${count} todos from @fix_plan.md`); + } + + return count; + } catch (err) { + // File doesn't exist or can't be read - that's OK + console.log(`[RalphTracker] Could not load @fix_plan.md: ${err}`); + return 0; + } + } + + /** + * Start watching @fix_plan.md for changes. + * Reloads todos when the file is modified. + */ + private startWatchingFixPlan(): void { + if (!this._fixPlanPath || !this._workingDir) return; + + // Stop existing watcher if any + this.stopWatchingFixPlan(); + + try { + // Only watch if the file exists + if (!existsSync(this._fixPlanPath)) { + // Watch the directory instead for file creation + this._fixPlanWatcher = fsWatch(this._workingDir, (_eventType, filename) => { + if (filename === '@fix_plan.md') { + this.handleFixPlanChange(); + } + }); + } else { + // Watch the file directly + this._fixPlanWatcher = fsWatch(this._fixPlanPath, () => { + this.handleFixPlanChange(); + }); + } + } catch (err) { + console.log(`[RalphTracker] Could not watch @fix_plan.md: ${err}`); + } + } + + /** + * Handle @fix_plan.md file change with debouncing. + */ + private handleFixPlanChange(): void { + // Debounce rapid changes (e.g., multiple writes) + if (this._fixPlanReloadTimer) { + clearTimeout(this._fixPlanReloadTimer); + } + + this._fixPlanReloadTimer = setTimeout(() => { + this._fixPlanReloadTimer = null; + this.loadFixPlanFromDisk(); + }, 500); // 500ms debounce + } + + /** + * Stop watching @fix_plan.md. + */ + stopWatchingFixPlan(): void { + if (this._fixPlanWatcher) { + this._fixPlanWatcher.close(); + this._fixPlanWatcher = null; + } + if (this._fixPlanReloadTimer) { + clearTimeout(this._fixPlanReloadTimer); + this._fixPlanReloadTimer = null; + } + } + /** * Whether the tracker is enabled and actively monitoring output. * Disabled by default; auto-enables when Ralph patterns detected. diff --git a/src/web/public/app.js b/src/web/public/app.js index 6802b86f..c1b805bd 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -2662,6 +2662,9 @@ class ClaudemanApp { const plan = this.ralphWizardConfig.existingPlan; if (!plan) return; + // Stop any ongoing plan generation + this.stopPlanGeneration(); + // Convert existing todos to generatedPlan format (only pending items) const pendingTodos = plan.todos.filter(t => t.status === 'pending' || t.status === 'in_progress'); this.ralphWizardConfig.generatedPlan = pendingTodos.map((todo, idx) => ({ @@ -2678,6 +2681,28 @@ class ClaudemanApp { this.updateDetailLevelButtons(); } + // Stop any ongoing plan generation (abort fetch, clear timers, hide spinner) + stopPlanGeneration() { + // Abort ongoing fetch + if (this.planGenerationAbortController) { + this.planGenerationAbortController.abort(); + this.planGenerationAbortController = null; + } + + // Clear timers + if (this.planLoadingTimer) { + clearInterval(this.planLoadingTimer); + this.planLoadingTimer = null; + } + if (this.planPhaseTimer) { + clearInterval(this.planPhaseTimer); + this.planPhaseTimer = null; + } + + // Hide loading spinner + document.getElementById('planGenerationLoading')?.classList.add('hidden'); + } + // Generate a new plan (ignore existing) generateNewPlan() { this.ralphWizardConfig.useExistingPlan = false; @@ -2832,15 +2857,11 @@ class ClaudemanApp { async generatePlan() { const config = this.ralphWizardConfig; - // Stop any existing timers - if (this.planLoadingTimer) { - clearInterval(this.planLoadingTimer); - this.planLoadingTimer = null; - } - if (this.planPhaseTimer) { - clearInterval(this.planPhaseTimer); - this.planPhaseTimer = null; - } + // Stop any existing generation first + this.stopPlanGeneration(); + + // Create abort controller for this generation + this.planGenerationAbortController = new AbortController(); // Show loading state, hide other sections document.getElementById('existingPlanSection')?.classList.add('hidden'); @@ -2895,6 +2916,7 @@ class ClaudemanApp { taskDescription: config.taskDescription, detailLevel: config.planDetailLevel, }), + signal: this.planGenerationAbortController?.signal, }); const data = await res.json(); @@ -2946,6 +2968,13 @@ class ClaudemanApp { clearInterval(this.planLoadingTimer); this.planLoadingTimer = null; } + + // Ignore abort errors (user cancelled, e.g., clicked "Use Existing Plan") + if (err.name === 'AbortError') { + console.log('Plan generation aborted by user'); + return; + } + console.error('Plan generation failed:', err); this.showPlanError('Network error: ' + err.message); } diff --git a/src/web/server.ts b/src/web/server.ts index a65103e7..17bd4cf7 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -2917,6 +2917,11 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; private async _doCleanupSession(sessionId: string, killScreen: boolean): Promise { const session = this.sessions.get(sessionId); + // Stop watching @fix_plan.md for this session + if (session) { + session.ralphTracker.stopWatchingFixPlan(); + } + // Kill all subagents spawned by this session if (session && killScreen) { try { @@ -3032,6 +3037,9 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; this.runSummaryTrackers.set(session.id, summaryTracker); summaryTracker.recordSessionStarted(session.mode, session.workingDir); + // Set working directory for Ralph tracker to auto-load @fix_plan.md + session.ralphTracker.setWorkingDir(session.workingDir); + session.on('output', (data) => { // Use batching for better performance at high throughput this.batchOutputData(session.id, data);