Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
Thumbs.db
ehthumbs.db
Desktop.ini
nul
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:

cat -n .gitignore | head -20

Repository: AndyMik90/Auto-Claude

Length of output: 527


🏁 Script executed:

git log --oneline -p -- .gitignore | head -100

Repository: AndyMik90/Auto-Claude

Length of output: 1681


🏁 Script executed:

# Search for any references to "nul" or ".bill" in the codebase
rg "nul|\.bill" --type-list | head -20

Repository: AndyMik90/Auto-Claude

Length of output: 454


🏁 Script executed:

# Search for any documentation or comments about these entries
rg -i "nul|bill" --type md --type txt | head -50

Repository: AndyMik90/Auto-Claude

Length of output: 5472


🏁 Script executed:

# Search for any actual "nul" files or references in the codebase
fd nul

Repository: AndyMik90/Auto-Claude

Length of output: 47


🏁 Script executed:

# Search for ".bill" directory or references
fd bill

Repository: AndyMik90/Auto-Claude

Length of output: 47


🏁 Script executed:

# Check if there are any comments or context in the PR/commit about these specific entries
git log --all --oneline --grep="bill\|nul" 2>/dev/null

Repository: AndyMik90/Auto-Claude

Length of output: 1965


🏁 Script executed:

# Look for any README or documentation about .bill directory
find . -type f \( -name "README*" -o -name "*.md" \) -exec grep -l "\.bill\|bill" {} \; 2>/dev/null

Repository: AndyMik90/Auto-Claude

Length of output: 47


🏁 Script executed:

# Check if there's any issue or PR description that might explain these entries
git show 1670dd5 --format=fuller 2>/dev/null | head -50

Repository: AndyMik90/Auto-Claude

Length of output: 2317


🏁 Script executed:

# Check if "nul" appears in any other repositories or gitignore best practices
# Also check if this was copied from somewhere
rg "nul" --type-list 2>&1 | head -5

Repository: AndyMik90/Auto-Claude

Length of output: 179


🏁 Script executed:

# Search for any references to this being a Windows-specific issue or tool
rg -i "windows|device|reserved" -- .gitignore

Repository: AndyMik90/Auto-Claude

Length of output: 47


🏁 Script executed:

# Check the full context of the .gitignore to see the coding guideline compliance
cat -n .gitignore | grep -A 5 -B 5 "auto-claude"

Repository: AndyMik90/Auto-Claude

Length of output: 445


Clarify the purpose of nul and .bill/ entries.

The nul entry 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
In @.gitignore at line 10, The .gitignore contains unusual entries "nul" and
".bill/" that lack context; update the repository-wide .gitignore by either
removing these entries if they are not needed globally, or add a short comment
above each entry explaining its purpose and scope (e.g., Windows reserved device
exclusion for "nul" or a developer-specific directory for ".bill/") so reviewers
understand why "nul" and ".bill/" remain; ensure the comment clarifies whether
the entry is global or should be moved to a local/developer-specific ignore
file.


# ===========================
# Local Development
# ===========================
.bill/

# ===========================
# Security - Environment & Secrets
Expand Down
10 changes: 8 additions & 2 deletions apps/backend/implementation_plan/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,15 @@ def update_status_from_subtasks(self):
self.planStatus = "in_progress"
else:
# All subtasks pending
# Preserve human_review/review status if it's for plan approval stage
# Preserve review status if it's for plan approval stage
# (spec is complete, waiting for user to approve before coding starts)
if self.status == "human_review" and self.planStatus == "review":
# This covers both:
# - human_review + review: legacy plan review state
# - stopped + awaiting_review: new plan review state (set by spec_runner.py --no-build)
if self.status in ("human_review", "stopped") and self.planStatus in (
"review",
"awaiting_review",
):
# Keep the plan approval status - don't reset to backlog
pass
else:
Expand Down
47 changes: 47 additions & 0 deletions apps/backend/runners/spec_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

import asyncio
import io
import json
import os
from pathlib import Path

Expand Down Expand Up @@ -372,6 +373,52 @@ def main():
# Execute run.py - replace current process
os.execv(sys.executable, run_cmd)

else:
# --no-build specified: Set planStatus to 'awaiting_review' to signal frontend
# that planning is complete and user review is required before coding starts
debug(
"spec_runner",
"--no-build flag detected, setting awaiting_review status",
)
print()
print_status("--no-build flag: Setting plan to awaiting review", "info")

plan_path = orchestrator.spec_dir / "implementation_plan.json"
debug("spec_runner", f"Looking for plan file at: {plan_path}")

if plan_path.exists():
try:
plan_data = json.loads(plan_path.read_text(encoding="utf-8"))
plan_data["planStatus"] = "awaiting_review"
plan_data["status"] = "stopped" # Mark as stopped for review
plan_path.write_text(
json.dumps(plan_data, indent=2, ensure_ascii=False),
encoding="utf-8",
)
debug(
"spec_runner",
"Set planStatus to 'awaiting_review' for frontend",
)
print()
print_status(
"Planning complete. Awaiting review before coding.", "success"
)
print(
f" {muted('Review the spec at:')} {orchestrator.spec_dir / 'spec.md'}"
)
print(
f" {muted('When ready, restart the task in the UI to begin coding.')}"
)
except (json.JSONDecodeError, OSError) as e:
debug_error("spec_runner", f"Failed to update plan status: {e}")
print_status(f"Failed to update plan status: {e}", "error")
else:
debug_error("spec_runner", f"Plan file not found at: {plan_path}")
print_status(
f"Warning: implementation_plan.json not found at {plan_path}",
"warning",
)

sys.exit(0)

except KeyboardInterrupt:
Expand Down
1 change: 1 addition & 0 deletions apps/backend/task_logger/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ def update_phase_status(
self._data["phases"][phase]["status"] = status
if completed_at:
self._data["phases"][phase]["completed_at"] = completed_at
self.save()

def set_phase_started(self, phase: str, started_at: str) -> None:
"""
Expand Down
14 changes: 11 additions & 3 deletions apps/frontend/src/main/agent/agent-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,14 @@ export class AgentManager extends EventEmitter {
}

// Check if user requires review before coding
if (!metadata?.requireReviewBeforeCoding) {
if (metadata?.requireReviewBeforeCoding) {
// Human review required: Stop after spec creation, don't start build
// Frontend will handle the review gate and resume coding after approval
// NOTE: Also pass --auto-approve so the spec gets approved during creation
// (otherwise spec_runner exits early with "spec not approved" error)
args.push('--no-build');
args.push('--auto-approve');
} else {
// Auto-approve: When user starts a task from the UI without requiring review
args.push('--auto-approve');
}
Expand All @@ -175,8 +182,9 @@ export class AgentManager extends EventEmitter {
// Store context for potential restart
this.storeTaskContext(taskId, projectPath, '', {}, true, taskDescription, specDir, metadata, baseBranch);

// Note: This is spec-creation but it chains to task-execution via run.py
await this.processManager.spawnProcess(taskId, autoBuildSource, args, combinedEnv, 'task-execution');
// Use 'spec-creation' processType so exit handler can distinguish planning from coding
// This prevents infinite loop when auto-continue triggers after coding completes
await this.processManager.spawnProcess(taskId, autoBuildSource, args, combinedEnv, 'spec-creation');
}

/**
Expand Down
193 changes: 187 additions & 6 deletions apps/frontend/src/main/ipc-handlers/agent-events-handlers.ts
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,
Expand All @@ -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";
import { findTaskWorktree } from "../worktree-paths";
import { findTaskAndProject } from "./task/shared";
import { safeSendToRenderer } from "./utils";
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand All @@ -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
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

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
In `@apps/frontend/src/main/ipc-handlers/agent-events-handlers.ts` around lines
172 - 195, The reads for metadata and plan (reading metadataPath into metadata
and planPath into planContent) can both fail silently causing
requireReviewBeforeCoding and planHasSubtasks logic to be unreliable; update the
block that reads metadata and plan (the code around hasSpec/hasPlan, metadata,
planContent, metadataPath, planPath) to detect and log when either or both reads
failed (include taskId and which path(s) failed or parse errors), and emit a
clear warning that the planning->coding transition is being skipped due to read
failures so operators can debug the ambiguous task state.

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;
Expand Down
Loading