-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
fix: Human review checkpoint using stopped status (Issue #1231) #1334
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
Changes from 5 commits
1670dd5
f88c052
df2bdef
83e865f
8f2d07f
e4c944c
16ef603
ffe6f67
be17a96
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 |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import type { BrowserWindow } from "electron"; | ||
| import path from "path"; | ||
| import { existsSync } from "fs"; | ||
| import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync } from "fs"; | ||
| import { IPC_CHANNELS, AUTO_BUILD_PATHS, getSpecsDir } from "../../shared/constants"; | ||
| import { | ||
| wouldPhaseRegress, | ||
|
|
@@ -22,7 +22,7 @@ import { titleGenerator } from "../title-generator"; | |
| import { fileWatcher } from "../file-watcher"; | ||
| import { projectStore } from "../project-store"; | ||
| import { notificationService } from "../notification-service"; | ||
| import { persistPlanStatusSync, getPlanPath } from "./task/plan-file-utils"; | ||
| import { persistPlanStatusSync, getPlanPath, createPlanIfNotExists } from "./task/plan-file-utils"; | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| import { findTaskWorktree } from "../worktree-paths"; | ||
| import { findTaskAndProject } from "./task/shared"; | ||
| import { safeSendToRenderer } from "./utils"; | ||
|
|
@@ -100,6 +100,26 @@ function validateStatusTransition( | |
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Atomic file write to prevent corruption on crash/interrupt. | ||
| * Writes to a temporary file first, then atomically renames to target. | ||
| */ | ||
| function atomicWriteFileSync(filePath: string, content: string): void { | ||
| const tempPath = `${filePath}.${process.pid}.tmp`; | ||
| try { | ||
| writeFileSync(tempPath, content, "utf-8"); | ||
| renameSync(tempPath, filePath); | ||
| } catch (error) { | ||
| // Clean up temp file if rename failed | ||
| try { | ||
| unlinkSync(tempPath); | ||
| } catch { | ||
| // Ignore cleanup errors | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Register all agent-events-related IPC handlers | ||
| */ | ||
|
|
@@ -135,7 +155,7 @@ export function registerAgenteventsHandlers( | |
|
|
||
| agentManager.on("exit", (taskId: string, code: number | null, processType: ProcessType) => { | ||
| // Get project info early for multi-project filtering (issue #723) | ||
| const { project: exitProject } = findTaskAndProject(taskId); | ||
| const { project: exitProject, task: exitTask } = findTaskAndProject(taskId); | ||
| const exitProjectId = exitProject?.id; | ||
|
|
||
| // Send final plan state to renderer BEFORE unwatching | ||
|
|
@@ -153,9 +173,170 @@ export function registerAgenteventsHandlers( | |
|
|
||
| fileWatcher.unwatch(taskId); | ||
|
|
||
| if (processType === "spec-creation") { | ||
| console.warn(`[Task ${taskId}] Spec creation completed with code ${code}`); | ||
| return; | ||
| // FIX: Planning-to-coding transition (Issue #1231) | ||
| // Only run this logic for spec-creation process (planning phase). | ||
| // This prevents infinite loop: without this check, when coding (task-execution) completes, | ||
| // this handler would incorrectly call startTaskExecution again, causing an infinite loop. | ||
| // The processType check ensures we only auto-continue from planning → coding, not coding → coding. | ||
| if (processType === 'spec-creation' && (code === 0 || code === 1) && exitProject && exitTask) { | ||
| const specsBaseDir = getSpecsDir(exitProject.autoBuildPath); | ||
| const specDir = path.join(exitProject.path, specsBaseDir, exitTask.specId); | ||
| const specFilePath = path.join(specDir, AUTO_BUILD_PATHS.SPEC_FILE); | ||
| const planPath = path.join(specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN); | ||
| const metadataPath = path.join(specDir, "task_metadata.json"); | ||
|
|
||
| // Check if spec and plan exist (planning completed) | ||
| const hasSpec = existsSync(specFilePath); | ||
| const hasPlan = existsSync(planPath); | ||
|
|
||
| if (hasSpec && hasPlan) { | ||
| // Read metadata and plan once to avoid multiple file reads | ||
| let metadata: Record<string, unknown> = {}; | ||
| let planContent: Record<string, unknown> = {}; | ||
|
|
||
| if (existsSync(metadataPath)) { | ||
| try { | ||
| metadata = JSON.parse(readFileSync(metadataPath, "utf-8")); | ||
| } catch (err) { | ||
| console.error(`[Task ${taskId}] Failed to read metadata:`, err); | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| planContent = JSON.parse(readFileSync(planPath, "utf-8")); | ||
| } catch (err) { | ||
| console.error(`[Task ${taskId}] Failed to read plan:`, err); | ||
| } | ||
|
|
||
| const requireReviewBeforeCoding = metadata.requireReviewBeforeCoding === true; | ||
| const allSubtasks = ((planContent.phases as Array<{ subtasks?: unknown[] }>) || []) | ||
| .flatMap((p) => p.subtasks || []); | ||
| const planHasSubtasks = allSubtasks.length > 0; | ||
|
|
||
|
Comment on lines
173
to
196
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 Consider logging when planning-to-coding transition is skipped due to read failures. If both metadata and plan reads fail silently, the transition logic effectively becomes a no-op. The task continues without stopping for review or auto-continuing, which may leave it in an ambiguous state. Adding a log when the block exits without taking action improves debuggability. 💡 Optional improvement const planHasSubtasks = allSubtasks.length > 0;
+ if (!planHasSubtasks) {
+ console.warn(`[Task ${taskId}] Plan exists but has no subtasks - skipping transition logic`);
+ }
+
if (planHasSubtasks && requireReviewBeforeCoding) {🤖 Prompt for AI Agents |
||
| if (planHasSubtasks && requireReviewBeforeCoding) { | ||
| // User wants to review before coding - STOP the task | ||
| // Note: spec_runner.py with --no-build already sets these values, but we set them | ||
| // here as a fallback in case the Python process didn't complete the update | ||
| // Also persist to worktree if it exists, since getTasks() prefers worktree version | ||
| const worktreePath = findTaskWorktree(exitProject.path, exitTask.specId); | ||
| const worktreeSpecDir = worktreePath ? path.join(worktreePath, specsBaseDir, exitTask.specId) : null; | ||
|
|
||
| // Prepare updated content | ||
| metadata.stoppedForPlanReview = true; | ||
| planContent.status = "stopped"; | ||
| planContent.planStatus = "awaiting_review"; | ||
|
|
||
| const metadataJson = JSON.stringify(metadata, null, 2); | ||
| const planJson = JSON.stringify(planContent, null, 2); | ||
|
|
||
| // Write to main project (atomic write to prevent corruption) | ||
| try { | ||
| atomicWriteFileSync(metadataPath, metadataJson); | ||
| } catch (err) { | ||
| console.error(`[Task ${taskId}] Failed to update task_metadata.json:`, err); | ||
| } | ||
|
|
||
| try { | ||
| atomicWriteFileSync(planPath, planJson); | ||
| } catch (err) { | ||
| console.error(`[Task ${taskId}] Failed to update implementation_plan.json:`, err); | ||
| } | ||
|
|
||
| // Also write to worktree if it exists (for consistency with getTasks()) | ||
| if (worktreeSpecDir) { | ||
| const worktreeMetadataPath = path.join(worktreeSpecDir, "task_metadata.json"); | ||
| const worktreePlanPath = path.join(worktreeSpecDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN); | ||
|
|
||
| try { | ||
| if (existsSync(worktreeMetadataPath)) { | ||
| atomicWriteFileSync(worktreeMetadataPath, metadataJson); | ||
| } | ||
| } catch (err) { | ||
| console.error(`[Task ${taskId}] Failed to update worktree task_metadata.json:`, err); | ||
| } | ||
|
|
||
| try { | ||
| if (existsSync(worktreePlanPath)) { | ||
| atomicWriteFileSync(worktreePlanPath, planJson); | ||
| } | ||
| } catch (err) { | ||
| console.error(`[Task ${taskId}] Failed to update worktree implementation_plan.json:`, err); | ||
| } | ||
| } | ||
|
|
||
| // Invalidate cache so UI reflects the new status | ||
| projectStore.invalidateTasksCache(exitProject.id); | ||
|
|
||
| // Notify renderer of status change | ||
| safeSendToRenderer( | ||
| getMainWindow, | ||
| IPC_CHANNELS.TASK_STATUS_CHANGE, | ||
| taskId, | ||
| "stopped" as TaskStatus, | ||
| exitProjectId | ||
| ); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| if (planHasSubtasks && !requireReviewBeforeCoding) { | ||
| // Auto-continue to coding phase | ||
| // First, update the plan file status to in_progress so UI stays consistent | ||
| planContent.status = "in_progress"; | ||
| planContent.planStatus = "in_progress"; | ||
|
|
||
| const planJson = JSON.stringify(planContent, null, 2); | ||
|
|
||
| // Write to main project (atomic write to prevent corruption) | ||
| try { | ||
| atomicWriteFileSync(planPath, planJson); | ||
| } catch (err) { | ||
| console.error(`[Task ${taskId}] Failed to update implementation_plan.json for coding transition:`, err); | ||
| } | ||
|
|
||
| // Also write to worktree if it exists (for consistency with getTasks()) | ||
| const worktreePath = findTaskWorktree(exitProject.path, exitTask.specId); | ||
| if (worktreePath) { | ||
| const worktreePlanPath = path.join(worktreePath, specsBaseDir, exitTask.specId, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN); | ||
| try { | ||
| if (existsSync(worktreePlanPath)) { | ||
| atomicWriteFileSync(worktreePlanPath, planJson); | ||
| } | ||
| } catch (err) { | ||
| console.error(`[Task ${taskId}] Failed to update worktree implementation_plan.json for coding transition:`, err); | ||
| } | ||
| } | ||
|
|
||
| // Invalidate cache so UI reflects the new status | ||
| projectStore.invalidateTasksCache(exitProject.id); | ||
|
|
||
| // Start coding phase | ||
| const baseBranch = (exitTask.metadata?.baseBranch as string | undefined) || exitProject.settings?.mainBranch; | ||
| agentManager.startTaskExecution( | ||
| taskId, | ||
| exitProject.path, | ||
| exitTask.specId, | ||
| { | ||
| parallel: false, | ||
| workers: 1, | ||
| baseBranch, | ||
| useWorktree: exitTask.metadata?.useWorktree as boolean | undefined | ||
| } | ||
| ); | ||
|
|
||
| // Notify renderer that task is continuing to coding phase | ||
| // This ensures UI shows in_progress status immediately | ||
| safeSendToRenderer( | ||
| getMainWindow, | ||
| IPC_CHANNELS.TASK_STATUS_CHANGE, | ||
| taskId, | ||
| "in_progress" as TaskStatus, | ||
| exitProjectId | ||
| ); | ||
|
|
||
| return; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| let task: Task | undefined; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n .gitignore | head -20Repository: AndyMik90/Auto-Claude
Length of output: 527
🏁 Script executed:
git log --oneline -p -- .gitignore | head -100Repository: AndyMik90/Auto-Claude
Length of output: 1681
🏁 Script executed:
Repository: AndyMik90/Auto-Claude
Length of output: 454
🏁 Script executed:
Repository: AndyMik90/Auto-Claude
Length of output: 5472
🏁 Script executed:
# Search for any actual "nul" files or references in the codebase fd nulRepository: AndyMik90/Auto-Claude
Length of output: 47
🏁 Script executed:
# Search for ".bill" directory or references fd billRepository: AndyMik90/Auto-Claude
Length of output: 47
🏁 Script executed:
Repository: AndyMik90/Auto-Claude
Length of output: 1965
🏁 Script executed:
Repository: AndyMik90/Auto-Claude
Length of output: 47
🏁 Script executed:
Repository: AndyMik90/Auto-Claude
Length of output: 2317
🏁 Script executed:
Repository: AndyMik90/Auto-Claude
Length of output: 179
🏁 Script executed:
Repository: AndyMik90/Auto-Claude
Length of output: 47
🏁 Script executed:
Repository: AndyMik90/Auto-Claude
Length of output: 445
Clarify the purpose of
nuland.bill/entries.The
nulentry at line 10 is a reserved Windows device name that cannot be created as a regular file. This is an unusual .gitignore pattern that lacks explanation.Similarly, the
.bill/entry at line 15 appears to be a personal development directory but has no documented purpose or relation to the PR's stated objectives (human review checkpoint feature).Please clarify why these entries are needed and whether they should remain in the repository-wide .gitignore.
🤖 Prompt for AI Agents