From bdf003f4befef90e82513de750d948c45cf7b62e Mon Sep 17 00:00:00 2001 From: eYdr1en <54525514+eYdr1en@users.noreply.github.com> Date: Thu, 29 Jan 2026 01:53:48 +0100 Subject: [PATCH 1/2] fix: ralph wizard agent files not found for linked cases Two bugs were causing "File not found" and "No result file found" errors when clicking on agent windows in the Ralph Loop wizard: 1. **Linked cases not checked**: The ralph-wizard file endpoints only looked in `~/claudeman-cases/{name}`, ignoring linked cases stored in `~/.claudeman/linked-cases.json`. Now all three endpoints check linked cases first: - `/api/generate-plan-detailed` (for saving files) - `/api/cases/:caseName/ralph-wizard/files` - `/api/cases/:caseName/ralph-wizard/file/:filePath` 2. **Execution optimizer agentType mismatch**: SSE events were sent with `agentType: 'execution'` but files were saved to folder `execution-optimizer/`. Fixed to use consistent naming: - SSE events now use `'execution-optimizer'` - Frontend typeLabels/typeIcons updated to match 3. **JSON parse errors from control characters**: Enhanced tryParseJSON to escape ALL control characters (0x00-0x1F), not just newlines. This fixes "Bad control character in string literal" errors when Claude's response contains backspace, form feed, or other control chars. Co-Authored-By: Claude Opus 4.5 --- src/plan-orchestrator.ts | 31 ++++++++---- src/web/public/app.js | 6 +-- src/web/server.ts | 105 +++++++++++++++++++++++++++++++-------- 3 files changed, 110 insertions(+), 32 deletions(-) diff --git a/src/plan-orchestrator.ts b/src/plan-orchestrator.ts index 54a1ef0..0eaaa77 100644 --- a/src/plan-orchestrator.ts +++ b/src/plan-orchestrator.ts @@ -219,7 +219,7 @@ export type ProgressCallback = (phase: string, detail: string) => void; export interface PlanSubagentEvent { type: 'started' | 'progress' | 'completed' | 'failed'; agentId: string; - agentType: 'research' | 'requirements' | 'architecture' | 'testing' | 'risks' | 'verification' | 'execution' | 'final-review'; + agentType: 'research' | 'requirements' | 'architecture' | 'testing' | 'risks' | 'verification' | 'execution-optimizer' | 'final-review'; model: string; status: string; detail?: string; @@ -276,10 +276,23 @@ function tryParseJSON(jsonString: string): { success: boolean; data?: any; error // 1. Remove trailing commas before ] or } repaired = repaired.replace(/,(\s*[\]}])/g, '$1'); - // 2. Fix unescaped newlines in strings (common LLM issue) - // Match strings and escape newlines within them + // 2. Fix unescaped control characters in strings (common LLM issue) + // JSON requires all control characters (0x00-0x1F) to be escaped + // Match strings and escape all control characters within them repaired = repaired.replace(/"([^"\\]|\\.)*"/g, (match) => { - return match.replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t'); + // Replace all control characters with their escaped forms + return match.replace(/[\x00-\x1F]/g, (char) => { + switch (char) { + case '\n': return '\\n'; + case '\r': return '\\r'; + case '\t': return '\\t'; + case '\b': return '\\b'; + case '\f': return '\\f'; + default: + // Escape other control characters as \uXXXX + return '\\u' + char.charCodeAt(0).toString(16).padStart(4, '0'); + } + }); }); // 3. Try to close unclosed arrays/objects (truncated output) @@ -1877,7 +1890,7 @@ Check \`${caseDir}/ralph-wizard/research/result.json\` for: onSubagent?.({ type: 'started', agentId, - agentType: 'execution', + agentType: 'execution-optimizer', model: MODEL_VERIFICATION, status: 'running', detail: 'Optimizing plan for Claude Code execution...', @@ -1911,7 +1924,7 @@ Check \`${caseDir}/ralph-wizard/research/result.json\` for: onSubagent?.({ type: 'progress', agentId, - agentType: 'execution', + agentType: 'execution-optimizer', model: MODEL_VERIFICATION, status: 'running', detail: `Optimizing execution... (${elapsedSec}s / ${timeoutSec}s)`, @@ -1941,7 +1954,7 @@ Check \`${caseDir}/ralph-wizard/research/result.json\` for: onSubagent?.({ type: 'completed', agentId, - agentType: 'execution', + agentType: 'execution-optimizer', model: MODEL_VERIFICATION, status: 'completed', detail: 'Using default execution strategy', @@ -2033,7 +2046,7 @@ Check \`${caseDir}/ralph-wizard/research/result.json\` for: onSubagent?.({ type: 'completed', agentId, - agentType: 'execution', + agentType: 'execution-optimizer', model: MODEL_VERIFICATION, status: 'completed', itemCount: optimizedPlan.length, @@ -2056,7 +2069,7 @@ Check \`${caseDir}/ralph-wizard/research/result.json\` for: onSubagent?.({ type: 'failed', agentId, - agentType: 'execution', + agentType: 'execution-optimizer', model: MODEL_VERIFICATION, status: 'failed', error: err instanceof Error ? err.message : String(err), diff --git a/src/web/public/app.js b/src/web/public/app.js index 9e5190e..a7f3811 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -4124,7 +4124,7 @@ class ClaudemanApp { testing: 'TDD Specialist', risks: 'Risk Analyst', verification: 'Verification Expert', - execution: 'Execution Optimizer', + 'execution-optimizer': 'Execution Optimizer', 'final-review': 'Final Review', }; @@ -4135,7 +4135,7 @@ class ClaudemanApp { testing: '🧪', risks: '⚠️', verification: '✓', - execution: '⚡', + 'execution-optimizer': '⚡', 'final-review': '🔍', }; @@ -4580,7 +4580,7 @@ class ClaudemanApp { testing: 'TDD Specialist', risks: 'Risk Analyst', verification: 'Verification Expert', - execution: 'Execution Optimizer', + 'execution-optimizer': 'Execution Optimizer', 'final-review': 'Final Review', }; diff --git a/src/web/server.ts b/src/web/server.ts index c04191d..bb562b4 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -2531,14 +2531,37 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; } // Determine output directory for saving wizard results + // Check linked cases first, then claudeman-cases let outputDir: string | undefined; if (caseName) { - const casesDir = join(homedir(), 'claudeman-cases'); - const casePath = join(casesDir, caseName); - // Security: Path traversal protection - const resolvedCase = resolve(casePath); - const resolvedBase = resolve(casesDir); - if (resolvedCase.startsWith(resolvedBase) && existsSync(casePath)) { + let casePath: string | undefined; + + // First check linked cases + const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json'); + try { + if (existsSync(linkedCasesFile)) { + const linkedCases: Record = JSON.parse(readFileSync(linkedCasesFile, 'utf-8')); + if (linkedCases[caseName] && existsSync(linkedCases[caseName])) { + casePath = linkedCases[caseName]; + } + } + } catch { + // Ignore linked cases errors + } + + // Fall back to claudeman-cases directory + if (!casePath) { + const casesDir = join(homedir(), 'claudeman-cases'); + const directPath = join(casesDir, caseName); + // Security: Path traversal protection + const resolvedCase = resolve(directPath); + const resolvedBase = resolve(casesDir); + if (resolvedCase.startsWith(resolvedBase) && existsSync(directPath)) { + casePath = directPath; + } + } + + if (casePath) { outputDir = join(casePath, 'ralph-wizard'); } } @@ -2643,14 +2666,35 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; // Get ralph-wizard files for a case (prompts and results) this.app.get('/api/cases/:caseName/ralph-wizard/files', async (req) => { const { caseName } = req.params as { caseName: string }; - const casesDir = join(homedir(), 'claudeman-cases'); - const casePath = join(casesDir, caseName); - // Security: Path traversal protection - const resolvedCase = resolve(casePath); - const resolvedBase = resolve(casesDir); - if (!resolvedCase.startsWith(resolvedBase)) { - return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name'); + // Check linked cases first, then claudeman-cases + let casePath: string | undefined; + + const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json'); + try { + if (existsSync(linkedCasesFile)) { + const linkedCases: Record = JSON.parse(readFileSync(linkedCasesFile, 'utf-8')); + if (linkedCases[caseName] && existsSync(linkedCases[caseName])) { + casePath = linkedCases[caseName]; + } + } + } catch { + // Ignore linked cases errors + } + + if (!casePath) { + const casesDir = join(homedir(), 'claudeman-cases'); + const directPath = join(casesDir, caseName); + // Security: Path traversal protection + const resolvedCase = resolve(directPath); + const resolvedBase = resolve(casesDir); + if (resolvedCase.startsWith(resolvedBase) && existsSync(directPath)) { + casePath = directPath; + } + } + + if (!casePath) { + return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Case not found'); } const wizardDir = join(casePath, 'ralph-wizard'); @@ -2689,14 +2733,35 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; // Read a specific ralph-wizard file this.app.get('/api/cases/:caseName/ralph-wizard/file/:filePath', async (req) => { const { caseName, filePath } = req.params as { caseName: string; filePath: string }; - const casesDir = join(homedir(), 'claudeman-cases'); - const casePath = join(casesDir, caseName); - // Security: Path traversal protection for case name - const resolvedCase = resolve(casePath); - const resolvedBase = resolve(casesDir); - if (!resolvedCase.startsWith(resolvedBase)) { - return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name'); + // Check linked cases first, then claudeman-cases + let casePath: string | undefined; + + const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json'); + try { + if (existsSync(linkedCasesFile)) { + const linkedCases: Record = JSON.parse(readFileSync(linkedCasesFile, 'utf-8')); + if (linkedCases[caseName] && existsSync(linkedCases[caseName])) { + casePath = linkedCases[caseName]; + } + } + } catch { + // Ignore linked cases errors + } + + if (!casePath) { + const casesDir = join(homedir(), 'claudeman-cases'); + const directPath = join(casesDir, caseName); + // Security: Path traversal protection + const resolvedCase = resolve(directPath); + const resolvedBase = resolve(casesDir); + if (resolvedCase.startsWith(resolvedBase) && existsSync(directPath)) { + casePath = directPath; + } + } + + if (!casePath) { + return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Case not found'); } const wizardDir = join(casePath, 'ralph-wizard'); From 44048b0c0eaa1e6d7f03e45bcf7e8bbb612c4db4 Mon Sep 17 00:00:00 2001 From: eYdr1en <54525514+eYdr1en@users.noreply.github.com> Date: Thu, 29 Jan 2026 02:43:25 +0100 Subject: [PATCH 2/2] fix: save agent prompts immediately on start (not on completion) Previously, prompt.md files were only saved when agents completed. This caused "File not found" errors when clicking on running agents in the UI. Now prompts are saved immediately when each agent starts: - Research agent - Analysis subagents (requirements, architecture, testing, risks) - Verification agent - Execution optimizer - Final review agent The result.json is still saved on completion as before. Co-Authored-By: Claude Opus 4.5 --- src/plan-orchestrator.ts | 79 +++++++++++++++++++++++++++++++++++----- src/web/server.ts | 29 +++++++++++++-- 2 files changed, 94 insertions(+), 14 deletions(-) diff --git a/src/plan-orchestrator.ts b/src/plan-orchestrator.ts index 0eaaa77..7c725bc 100644 --- a/src/plan-orchestrator.ts +++ b/src/plan-orchestrator.ts @@ -364,18 +364,13 @@ export class PlanOrchestrator { this.screenManager = screenManager; this.workingDir = workingDir; this.outputDir = outputDir; + console.log(`[PlanOrchestrator] Constructor called with outputDir: ${outputDir || 'UNDEFINED'}`); } /** - * Save agent prompt and result to the output directory. - * Creates a folder for each agent with prompt.md and result.json files. + * Save agent prompt immediately when agent starts (so UI can show it while running). */ - private saveAgentOutput( - agentType: string, - prompt: string, - result: unknown, - durationMs: number - ): void { + private saveAgentPrompt(agentType: string, prompt: string): void { if (!this.outputDir) return; try { @@ -394,6 +389,43 @@ export class PlanOrchestrator { const promptPath = join(agentDir, 'prompt.md'); const promptContent = `# ${agentType} Agent Prompt +Generated: ${new Date().toISOString()} +Status: Running... + +## Task Description +${this.taskDescription} + +## Prompt +${prompt} +`; + writeFileSync(promptPath, promptContent, 'utf-8'); + console.log(`[PlanOrchestrator] Saved ${agentType} prompt to ${agentDir}`); + } catch (err) { + console.error(`[PlanOrchestrator] Failed to save ${agentType} prompt:`, err); + } + } + + /** + * Save agent result when agent completes. + */ + private saveAgentResult( + agentType: string, + prompt: string, + result: unknown, + durationMs: number + ): void { + if (!this.outputDir) return; + + try { + const agentDir = join(this.outputDir, agentType); + if (!existsSync(agentDir)) { + mkdirSync(agentDir, { recursive: true }); + } + + // Update prompt with duration + const promptPath = join(agentDir, 'prompt.md'); + const promptContent = `# ${agentType} Agent Prompt + Generated: ${new Date().toISOString()} Duration: ${(durationMs / 1000).toFixed(1)}s @@ -409,12 +441,24 @@ ${prompt} const resultPath = join(agentDir, 'result.json'); writeFileSync(resultPath, JSON.stringify(result, null, 2), 'utf-8'); - console.log(`[PlanOrchestrator] Saved ${agentType} output to ${agentDir}`); + console.log(`[PlanOrchestrator] Saved ${agentType} result to ${agentDir}`); } catch (err) { - console.error(`[PlanOrchestrator] Failed to save ${agentType} output:`, err); + console.error(`[PlanOrchestrator] Failed to save ${agentType} result:`, err); } } + /** + * Save agent prompt and result to the output directory (legacy, calls both). + */ + private saveAgentOutput( + agentType: string, + prompt: string, + result: unknown, + durationMs: number + ): void { + this.saveAgentResult(agentType, prompt, result, durationMs); + } + /** * Save the final combined plan result. */ @@ -831,6 +875,9 @@ Check \`${caseDir}/ralph-wizard/research/result.json\` for: .replace('{TASK}', taskDescription) .replace('{WORKING_DIR}', this.workingDir); + // Save prompt immediately so UI can show it while running + this.saveAgentPrompt('research', prompt); + onProgress?.('research', 'Starting research agent (local project + web search)...'); // Periodic progress updates showing elapsed time with contextual messages @@ -1285,6 +1332,9 @@ Check \`${caseDir}/ralph-wizard/research/result.json\` for: detail: `Analyzing ${agentType}...`, }); + // Save prompt immediately so UI can show it while running + this.saveAgentPrompt(agentType, prompt); + const session = new Session({ workingDir: this.workingDir, screenManager: this.screenManager, @@ -1648,6 +1698,9 @@ Check \`${caseDir}/ralph-wizard/research/result.json\` for: .replace('{TASK}', taskDescription) .replace('{PLAN}', planText); + // Save prompt immediately so UI can show it while running + this.saveAgentPrompt('verification', prompt); + onProgress?.('verification', 'Validating plan quality...'); // Periodic progress updates showing elapsed time @@ -1914,6 +1967,9 @@ Check \`${caseDir}/ralph-wizard/research/result.json\` for: .replace('{TASK}', taskDescription) .replace('{PLAN}', planText); + // Save prompt immediately so UI can show it while running + this.saveAgentPrompt('execution-optimizer', prompt); + onProgress?.('execution-optimization', 'Analyzing parallelization opportunities...'); // Periodic progress updates showing elapsed time @@ -2142,6 +2198,9 @@ Check \`${caseDir}/ralph-wizard/research/result.json\` for: .replace('{TASK}', taskDescription) .replace('{PLAN}', planText); + // Save prompt immediately so UI can show it while running + this.saveAgentPrompt('final-review', prompt); + onProgress?.('final-review', 'Analyzing overall plan coherence...'); // Periodic progress updates showing elapsed time diff --git a/src/web/server.ts b/src/web/server.ts index bb562b4..040ff06 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -2533,24 +2533,39 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; // Determine output directory for saving wizard results // Check linked cases first, then claudeman-cases let outputDir: string | undefined; + console.log(`[API] generate-plan-detailed called with caseName: "${caseName}"`); if (caseName) { let casePath: string | undefined; // First check linked cases const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json'); + console.log(`[API] Checking linked cases file: ${linkedCasesFile}`); try { if (existsSync(linkedCasesFile)) { const linkedCases: Record = JSON.parse(readFileSync(linkedCasesFile, 'utf-8')); - if (linkedCases[caseName] && existsSync(linkedCases[caseName])) { - casePath = linkedCases[caseName]; + console.log(`[API] Linked cases:`, Object.keys(linkedCases)); + console.log(`[API] Looking for caseName "${caseName}" in linked cases`); + if (linkedCases[caseName]) { + console.log(`[API] Found linked case path: ${linkedCases[caseName]}`); + if (existsSync(linkedCases[caseName])) { + casePath = linkedCases[caseName]; + console.log(`[API] Path exists, using: ${casePath}`); + } else { + console.log(`[API] Path does NOT exist!`); + } + } else { + console.log(`[API] caseName "${caseName}" not found in linked cases`); } + } else { + console.log(`[API] Linked cases file does not exist`); } - } catch { - // Ignore linked cases errors + } catch (err) { + console.log(`[API] Error reading linked cases:`, err); } // Fall back to claudeman-cases directory if (!casePath) { + console.log(`[API] Falling back to claudeman-cases directory`); const casesDir = join(homedir(), 'claudeman-cases'); const directPath = join(casesDir, caseName); // Security: Path traversal protection @@ -2563,9 +2578,15 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; if (casePath) { outputDir = join(casePath, 'ralph-wizard'); + console.log(`[API] outputDir set to: ${outputDir}`); + } else { + console.log(`[API] WARNING: casePath is undefined, outputDir will be undefined!`); } + } else { + console.log(`[API] WARNING: caseName is falsy, outputDir will be undefined!`); } + console.log(`[API] Final outputDir: ${outputDir || 'UNDEFINED'}`); const orchestrator = new PlanOrchestrator(this.screenManager, process.cwd(), outputDir); // Store orchestrator for potential cancellation via API (not on disconnect)