-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix: watch worktree path for implementation_plan.json changes (#1805) #1842
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 3 commits
df0034a
78491d2
e7875dd
10eba19
67d33aa
b30cc0d
9d959c9
8dbccac
530df03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# First, locate the file-watcher.ts file
find . -name "file-watcher.ts" -type fRepository: 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.tsRepository: 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 -100Repository: 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 -20Repository: 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 The error listener in 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // 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); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -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 | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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) | ||||||||||||||||||||
| */ | ||||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
1180
to
1183
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Inaccurate comment:
📝 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| const hasSpec = existsSync(specFilePath); | ||||||||||||||||||||
| const needsSpecCreation = !hasSpec; | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -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}`); | ||||||||||||||||||||
|
|
||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.