Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 67 additions & 41 deletions apps/frontend/src/main/file-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,64 +15,81 @@ interface WatcherInfo {
*/
export class FileWatcher extends EventEmitter {
private watchers: Map<string, WatcherInfo> = new Map();
private pendingWatches: Set<string> = new Set();

/**
* Start watching a task's implementation plan
*/
async watch(taskId: string, specDir: string): Promise<void> {
// Stop any existing watcher for this task
await this.unwatch(taskId);

const planPath = path.join(specDir, 'implementation_plan.json');

// Check if plan file exists
if (!existsSync(planPath)) {
this.emit('error', taskId, `Plan file not found: ${planPath}`);
// Prevent overlapping watch() calls for the same taskId.
// Since watch() is async, rapid-fire callers could enter concurrently
// before the first call updates state, creating duplicate watchers.
if (this.pendingWatches.has(taskId)) {
return;
}
this.pendingWatches.add(taskId);

// Create watcher with settings to handle frequent writes
const watcher = chokidar.watch(planPath, {
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 300,
pollInterval: 100
try {
// Close any existing watcher for this task
const existing = this.watchers.get(taskId);
if (existing) {
await existing.watcher.close();
this.watchers.delete(taskId);
}
});

// Store watcher info
this.watchers.set(taskId, {
taskId,
watcher,
planPath
});
const planPath = path.join(specDir, 'implementation_plan.json');

// Check if plan file exists
if (!existsSync(planPath)) {
this.emit('error', taskId, `Plan file not found: ${planPath}`);
return;
}
Comment on lines +66 to +70
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, locate the file-watcher.ts file
find . -name "file-watcher.ts" -type f

Repository: AndyMik90/Auto-Claude

Length of output: 105


🏁 Script executed:

#!/bin/bash
# Read the file-watcher.ts to understand the context
cd apps/frontend/src/main
wc -l file-watcher.ts
head -80 file-watcher.ts

Repository: AndyMik90/Auto-Claude

Length of output: 2543


🏁 Script executed:

#!/bin/bash
# Search for error event listeners on fileWatcher instances
rg -n "fileWatcher\.on\(\s*['\"]error['\"]" --type=ts -C5
rg -n "\.on\(\s*['\"]error['\"]" --type=ts -C5 | head -100

Repository: AndyMik90/Auto-Claude

Length of output: 8240


🏁 Script executed:

#!/bin/bash
# Search for where FileWatcher is instantiated and used
rg -n "new FileWatcher|new file-watcher|FileWatcher" --type=ts | head -20

Repository: AndyMik90/Auto-Claude

Length of output: 2447


🏁 Script executed:

#!/bin/bash
# Search for 'not-ready' or 'warn' event patterns to see if already used
rg -n "emit\(['\"]warn|emit\(['\"]not-ready|emit\(['\"]error" --type=ts apps/frontend/src/main/

Repository: AndyMik90/Auto-Claude

Length of output: 4681


Emitting 'error' for a missing plan file creates unnecessary UI noise during normal retry flow.

The error listener in agent-events-handlers.ts (line 309) sends all 'error' events to the renderer via IPC_CHANNELS.TASK_ERROR, surfacing them in the UI. During normal operation—when the worktree and implementation_plan.json are created after the initial watch() call—this emits alarming error messages even though callers already handle retries silently.

Remove the error emission for missing files and return silently:

Proposed fix
       // Check if plan file exists
       if (!existsSync(planPath)) {
-        this.emit('error', taskId, `Plan file not found: ${planPath}`);
+        // Plan file may not exist yet (e.g. worktree still being created).
+        // Callers handle re-watching, so a silent return avoids noisy error events.
         return;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check if plan file exists
if (!existsSync(planPath)) {
this.emit('error', taskId, `Plan file not found: ${planPath}`);
return;
}
// Check if plan file exists
if (!existsSync(planPath)) {
// Plan file may not exist yet (e.g. worktree still being created).
// Callers handle re-watching, so a silent return avoids noisy error events.
return;
}
🤖 Prompt for AI Agents
In `@apps/frontend/src/main/file-watcher.ts` around lines 42 - 46, The current
check in the file-watcher that calls this.emit('error', taskId, `Plan file not
found: ${planPath}`) when existsSync(planPath) is false should be changed to
silently return instead of emitting an error to avoid UI noise during normal
watch() retry flows; locate the block that references existsSync and planPath
and remove the emit('error', ...) call so the function simply returns when the
plan file is missing (optionally keep a non-error debug/log statement if you
need visibility).


// Handle file changes
watcher.on('change', () => {
// Create watcher with settings to handle frequent writes
const watcher = chokidar.watch(planPath, {
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 300,
pollInterval: 100
}
});

// Store watcher info
this.watchers.set(taskId, {
taskId,
watcher,
planPath
});

// Handle file changes
watcher.on('change', () => {
try {
const content = readFileSync(planPath, 'utf-8');
const plan: ImplementationPlan = JSON.parse(content);
this.emit('progress', taskId, plan);
} catch {
// File might be in the middle of being written
// Ignore parse errors, next change event will have complete file
}
});

// Handle errors
watcher.on('error', (error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
this.emit('error', taskId, message);
});

// Read and emit initial state
try {
const content = readFileSync(planPath, 'utf-8');
const plan: ImplementationPlan = JSON.parse(content);
this.emit('progress', taskId, plan);
} catch {
// File might be in the middle of being written
// Ignore parse errors, next change event will have complete file
// Initial read failed - not critical
}
});

// Handle errors
watcher.on('error', (error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
this.emit('error', taskId, message);
});

// Read and emit initial state
try {
const content = readFileSync(planPath, 'utf-8');
const plan: ImplementationPlan = JSON.parse(content);
this.emit('progress', taskId, plan);
} catch {
// Initial read failed - not critical
} finally {
this.pendingWatches.delete(taskId);
}
}

Expand Down Expand Up @@ -107,6 +124,15 @@ export class FileWatcher extends EventEmitter {
return this.watchers.has(taskId);
}

/**
* Get the spec directory currently being watched for a task
*/
getWatchedSpecDir(taskId: string): string | null {
const watcherInfo = this.watchers.get(taskId);
if (!watcherInfo) return null;
return path.dirname(watcherInfo.planPath);
}

/**
* Get current plan state for a task
*/
Expand Down
35 changes: 31 additions & 4 deletions apps/frontend/src/main/ipc-handlers/agent-events-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,23 @@ export function registerAgenteventsHandlers(

// Send final plan state to renderer BEFORE unwatching
// This ensures the renderer has the final subtask data (fixes 0/0 subtask bug)
const finalPlan = fileWatcher.getCurrentPlan(taskId);
// Try the file watcher's current path first, then fall back to worktree path
let finalPlan = fileWatcher.getCurrentPlan(taskId);
if (!finalPlan && exitTask && exitProject) {
// File watcher may have been watching the wrong path (main vs worktree)
// Try reading directly from the worktree
const worktreePath = findTaskWorktree(exitProject.path, exitTask.specId);
if (worktreePath) {
const specsBaseDir = getSpecsDir(exitProject.autoBuildPath);
const worktreePlanPath = path.join(worktreePath, specsBaseDir, exitTask.specId, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
try {
const content = readFileSync(worktreePlanPath, 'utf-8');
finalPlan = JSON.parse(content);
} catch {
// Worktree plan file not readable - not critical
}
}
}
if (finalPlan) {
safeSendToRenderer(
getMainWindow,
Expand Down Expand Up @@ -211,15 +227,26 @@ export function registerAgenteventsHandlers(
const worktreePath = findTaskWorktree(project.path, task.specId);
if (worktreePath) {
const specsBaseDir = getSpecsDir(project.autoBuildPath);
const worktreeSpecDir = path.join(worktreePath, specsBaseDir, task.specId);
const worktreePlanPath = path.join(
worktreePath,
specsBaseDir,
task.specId,
worktreeSpecDir,
AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN
);
if (existsSync(worktreePlanPath)) {
persistPlanPhaseSync(worktreePlanPath, progress.phase, project.id);
}

// Re-watch the worktree path if the file watcher is still watching the main project path.
// This handles the case where the task started before the worktree existed:
// the initial watch fell back to the main project spec dir, but now the worktree
// is available and implementation_plan.json is being written there.
const currentWatchDir = fileWatcher.getWatchedSpecDir(taskId);
if (currentWatchDir && currentWatchDir !== worktreeSpecDir && existsSync(worktreePlanPath)) {

This comment was marked as outdated.

console.warn(`[agent-events-handlers] Re-watching worktree path for ${taskId}: ${worktreeSpecDir}`);
fileWatcher.watch(taskId, worktreeSpecDir).catch((err) => {
console.error(`[agent-events-handlers] Failed to re-watch worktree for ${taskId}:`, err);
});
}
}
} else if (xstateInTerminalState && progress.phase) {
console.debug(`[agent-events-handlers] Skipping persistPlanPhaseSync for ${taskId}: XState in '${currentXState}', not overwriting with phase '${progress.phase}'`);
Expand Down
46 changes: 34 additions & 12 deletions apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,19 @@ async function ensureProfileManagerInitialized(): Promise<
}
}

/**
* Get the spec directory for file watching, preferring the worktree path if it exists.
* When a task runs in a worktree, implementation_plan.json is written there,
* not in the main project's spec directory.
*/
function getSpecDirForWatcher(projectPath: string, specsBaseDir: string, specId: string): string {
const worktreePath = findTaskWorktree(projectPath, specId);
if (worktreePath) {
return path.join(worktreePath, specsBaseDir, specId);
}
return path.join(projectPath, specsBaseDir, specId);
}

/**
* Register task execution handlers (start, stop, review, status management, recovery)
*/
Expand Down Expand Up @@ -205,15 +218,16 @@ export function registerTaskExecutionHandlers(
}

// Start file watcher for this task
// Use worktree path if it exists, since the backend writes implementation_plan.json there
const specsBaseDir = getSpecsDir(project.autoBuildPath);
const specDir = path.join(
project.path,
specsBaseDir,
task.specId
);
fileWatcher.watch(taskId, specDir);
const watchSpecDir = getSpecDirForWatcher(project.path, specsBaseDir, task.specId);
fileWatcher.watch(taskId, watchSpecDir).catch((err) => {
console.error(`[TASK_START] Failed to watch spec dir for ${taskId}:`, err);
});

// Check if spec.md exists (indicates spec creation was already done or in progress)
// Check main project path for spec file (spec is created before worktree)
const specDir = path.join(project.path, specsBaseDir, task.specId);
const specFilePath = path.join(specDir, AUTO_BUILD_PATHS.SPEC_FILE);
const hasSpec = existsSync(specFilePath);

Expand Down Expand Up @@ -710,7 +724,11 @@ export function registerTaskExecutionHandlers(
}

// Start file watcher for this task
fileWatcher.watch(taskId, specDir);
// Use worktree path if it exists, since the backend writes implementation_plan.json there
const watchSpecDir = getSpecDirForWatcher(project.path, specsBaseDir, task.specId);
fileWatcher.watch(taskId, watchSpecDir).catch((err) => {
console.error(`[TASK_UPDATE_STATUS] Failed to watch spec dir for ${taskId}:`, err);
});

// Check if spec.md exists
const specFilePath = path.join(specDir, AUTO_BUILD_PATHS.SPEC_FILE);
Expand Down Expand Up @@ -1146,12 +1164,16 @@ export function registerTaskExecutionHandlers(

// Start the task execution
// Start file watcher for this task
const specsBaseDir = getSpecsDir(project.autoBuildPath);
const specDirForWatcher = path.join(project.path, specsBaseDir, task.specId);
fileWatcher.watch(taskId, specDirForWatcher);
// Use worktree path if it exists, since the backend writes implementation_plan.json there
const watchSpecDir = getSpecDirForWatcher(project.path, specsBaseDir, task.specId);
fileWatcher.watch(taskId, watchSpecDir).catch((err) => {
console.error(`[Recovery] Failed to watch spec dir for ${taskId}:`, err);
});

// Check if spec.md exists to determine whether to run spec creation or task execution
const specFilePath = path.join(specDirForWatcher, AUTO_BUILD_PATHS.SPEC_FILE);
// Check main project path for spec file (spec is created before worktree)
const mainSpecDir = path.join(project.path, specsBaseDir, task.specId);
const specFilePath = path.join(mainSpecDir, AUTO_BUILD_PATHS.SPEC_FILE);
Comment on lines 1180 to 1183
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Inaccurate comment: mainSpecDir is declared outside the try block, not inside it.

mainSpecDir is declared at Line 937, before the try block that starts at Line 951. The comment on Line 1179 says "declared in the outer try block above" which is misleading.

📝 Fix the comment
             // Check if spec.md exists to determine whether to run spec creation or task execution
             // Check main project path for spec file (spec is created before worktree)
-            // mainSpecDir is declared in the outer try block above
+            // mainSpecDir is declared in the outer scope above (line ~937)
             const specFilePath = path.join(mainSpecDir, AUTO_BUILD_PATHS.SPEC_FILE);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check if spec.md exists to determine whether to run spec creation or task execution
const specFilePath = path.join(specDirForWatcher, AUTO_BUILD_PATHS.SPEC_FILE);
// Check main project path for spec file (spec is created before worktree)
// mainSpecDir is declared in the outer try block above
const specFilePath = path.join(mainSpecDir, AUTO_BUILD_PATHS.SPEC_FILE);
// Check if spec.md exists to determine whether to run spec creation or task execution
// Check main project path for spec file (spec is created before worktree)
// mainSpecDir is declared in the outer scope above (line ~937)
const specFilePath = path.join(mainSpecDir, AUTO_BUILD_PATHS.SPEC_FILE);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts` around lines
1177 - 1180, The comment incorrectly states that mainSpecDir is declared in the
outer try block; update the comment to accurately state that mainSpecDir is
declared earlier/outside the try block (it lives before the try), so change the
remark near the specFilePath calculation to reflect that mainSpecDir is defined
outside the try scope (reference variable name mainSpecDir and
AUTO_BUILD_PATHS.SPEC_FILE to locate the line).

const hasSpec = existsSync(specFilePath);
const needsSpecCreation = !hasSpec;

Expand All @@ -1162,7 +1184,7 @@ export function registerTaskExecutionHandlers(
// No spec file - need to run spec_runner.py to create the spec
const taskDescription = task.description || task.title;
console.warn(`[Recovery] Starting spec creation for: ${task.specId}`);
agentManager.startSpecCreation(taskId, project.path, taskDescription, specDirForWatcher, task.metadata, baseBranchForRecovery, project.id);
agentManager.startSpecCreation(taskId, project.path, taskDescription, mainSpecDir, task.metadata, baseBranchForRecovery, project.id);
} else {
// Spec exists - run task execution
console.warn(`[Recovery] Starting task execution for: ${task.specId}`);
Expand Down
Loading