diff --git a/.auto-claude-status b/.auto-claude-status index 84f49ee8..031b2218 100644 --- a/.auto-claude-status +++ b/.auto-claude-status @@ -1,25 +1,25 @@ { "active": true, - "spec": "005-auto-electron-product-requirements", + "spec": "004-improve-ux-ui-for-kanban-tasks", "state": "complete", "chunks": { - "completed": 5, + "completed": 1, "total": 5, "in_progress": 0, "failed": 0 }, "phase": { - "current": "Polish", - "id": 4, - "total": 2 + "current": "", + "id": 0, + "total": 0 }, "workers": { "active": 0, - "max": 1 + "max": 4 }, "session": { - "number": 5, - "started_at": "2025-12-11T11:10:36.896177" + "number": 0, + "started_at": "2025-12-12T13:34:07.312351" }, - "last_update": "2025-12-11T11:28:02.574839" + "last_update": "2025-12-12T13:48:17.101566" } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7944f924..ee079bd5 100644 --- a/.gitignore +++ b/.gitignore @@ -70,4 +70,6 @@ dmypy.json .claude_settings.json # Development of Auto Build with Auto Build -dev/ \ No newline at end of file +dev/ + +.auto-claude/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 902912cc..83057492 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -131,6 +131,39 @@ Each spec in `auto-claude/specs/XXX-name/` contains: - `qa_report.md` - QA validation results - `QA_FIX_REQUEST.md` - Issues to fix (when rejected) +### Branching & Worktree Strategy + +Auto Claude uses git worktrees for isolated builds. All branches stay LOCAL until user explicitly pushes: + +**Single Worker Mode:** +``` +main (user's branch) +└── auto-claude/{spec-name} ← staging branch (isolated worktree) +``` + +**Parallel Worker Mode:** +``` +main (user's branch) +└── auto-claude/{spec-name} ← staging branch (accumulates all work) + ├── worker-1/{chunk-id} ← temporary (merges to staging, then deleted) + ├── worker-2/{chunk-id} ← temporary (merges to staging, then deleted) + └── worker-3/{chunk-id} ← temporary (merges to staging, then deleted) +``` + +**Key principles:** +- ONE unified staging branch per spec (`auto-claude/{spec-name}`) +- Worker branches are temporary and LOCAL only +- NO automatic pushes to GitHub - user controls when to push +- User reviews in staging worktree (`.worktrees/auto-claude/`) +- Final merge: staging → main (after user approval) + +**Workflow:** +1. Build runs in isolated worktree on staging branch +2. Parallel workers create temp branches, merge to staging, then delete +3. User tests feature in `.worktrees/auto-claude/` +4. User runs `--merge` to add to their project +5. User pushes to remote when ready + ### Security Model Three-layer defense: diff --git a/RECOVERY_AND_RESUMPTION.md b/RECOVERY_AND_RESUMPTION.md new file mode 100644 index 00000000..473691c2 --- /dev/null +++ b/RECOVERY_AND_RESUMPTION.md @@ -0,0 +1,268 @@ +# Auto-Claude Recovery and Resumption System + +## Overview + +This document explains how Auto-Claude detects completed planning phases, identifies existing worktrees, and resumes execution after crashes or user interruptions. + +## Key State Detection Mechanisms + +### 1. Planning Phase Completion Detection + +**Primary Indicator: `implementation_plan.json`** + +The system determines if planning is complete by checking for the existence and validity of `implementation_plan.json`: + +- **Location**: `.auto-claude/specs/{spec-name}/implementation_plan.json` +- **Detection Logic**: + - `prompts.py::is_first_run()` checks if `implementation_plan.json` exists + - `coordinator.py::load_implementation_plan()` raises `FileNotFoundError` if missing + - `agent.py::load_implementation_plan()` returns `None` if file doesn't exist + +**Code References:** +```python +# auto-claude/prompts.py:181-192 +def is_first_run(spec_dir: Path) -> bool: + plan_file = spec_dir / "implementation_plan.json" + return not plan_file.exists() + +# auto-claude/coordinator.py:175-186 +def load_implementation_plan(self) -> ImplementationPlan: + plan_file = self.spec_dir / "implementation_plan.json" + if not plan_file.exists(): + raise FileNotFoundError("Implementation plan not found") + # ... +``` + +**Planning Phase Flow:** +1. `spec_runner.py` runs phases: discovery → requirements → complexity → context → spec → planning +2. `phase_planning()` creates `implementation_plan.json` via `planner.py` or planner agent +3. Once `implementation_plan.json` exists and is valid, planning is considered complete + +### 2. Worktree Detection + +**Staging Worktree Detection** + +The system checks for existing worktrees using: + +- **Location**: `.worktrees/auto-claude/` (staging worktree) +- **Detection Functions**: + - `workspace.py::get_existing_build_worktree()` - checks if staging worktree exists + - `worktree.py::get_or_create_staging()` - gets existing or creates new staging worktree + - `worktree.py::staging_exists()` - boolean check for staging worktree + +**Code References:** +```python +# auto-claude/workspace.py:88-93 +def get_existing_build_worktree(project_dir: Path, spec_name: str) -> Optional[Path]: + worktree_path = project_dir / ".worktrees" / STAGING_WORKTREE_NAME + if worktree_path.exists(): + return worktree_path + return None + +# auto-claude/worktree.py:173-206 +def get_or_create_staging(self, spec_name: str) -> WorktreeInfo: + staging_path = self.worktrees_dir / STAGING_WORKTREE_NAME + branch_name = f"auto-claude/{spec_name}" + + if staging_path.exists(): + # Load existing worktree info + # ... + return info + # Create new staging worktree + return self.create(STAGING_WORKTREE_NAME, branch_name) +``` + +**Worktree Persistence:** +- Worktrees persist across crashes and app restarts +- Git worktrees are stored in `.worktrees/` directory +- Each worktree has its own branch: `auto-claude/{spec-name}` +- Staging worktree is preserved until explicitly merged or discarded + +### 3. Resumption Logic + +**Entry Point: `run.py::main()`** + +When `run.py` starts, it follows this resumption flow: + +1. **Find Spec**: Locates spec directory via `find_spec()` +2. **Check Existing Build**: Calls `get_existing_build_worktree()` to detect staging worktree +3. **User Choice** (if not `--auto-continue`): + - Continue where it left off + - Review what was built + - Merge existing build + - Start fresh (discard) +4. **Workspace Setup**: + - If existing worktree found → uses it + - If not → creates new staging worktree +5. **Execution**: + - **Sequential Mode**: `run_autonomous_agent()` checks for `implementation_plan.json` + - **Parallel Mode**: `SwarmCoordinator` checks for plan, runs planner if missing + +**Code Flow:** +```python +# auto-claude/run.py:618-631 +if get_existing_build_worktree(project_dir, spec_dir.name): + if args.auto_continue: + print("Auto-continue: Resuming existing build...") + else: + continue_existing = check_existing_build(project_dir, spec_dir.name) + if continue_existing: + # Continue with existing worktree + pass + else: + # User chose to start fresh or merged existing + pass +``` + +### 4. Planner vs Coder Detection + +**Sequential Mode (`agent.py`):** + +```python +# auto-claude/agent.py:547-691 +async def run_autonomous_agent(...): + # ... + first_run = is_first_run(spec_dir) # Checks for implementation_plan.json + + if first_run: + # Run planner session + prompt = generate_planner_prompt(spec_dir) + else: + # Run coder session + next_chunk = get_next_chunk(spec_dir) + prompt = generate_coder_prompt(spec_dir, next_chunk) +``` + +**Parallel Mode (`coordinator.py`):** + +```python +# auto-claude/coordinator.py:304-350 +async def run_planner_session(self) -> bool: + """Run planner if implementation_plan.json doesn't exist""" + plan_file = self.spec_dir / "implementation_plan.json" + if not plan_file.exists(): + # Run planner agent session + # ... +``` + +### 5. Chunk Progress Tracking + +**State Storage: `implementation_plan.json`** + +Chunk statuses are stored in the plan file: +- `pending` - Not started +- `in_progress` - Currently being worked on +- `completed` - Finished successfully +- `blocked` - Stuck, needs attention +- `failed` - Failed after attempts + +**Resumption Logic:** +```python +# auto-claude/progress.py:399-452 +def get_next_chunk(spec_dir: Path) -> dict | None: + plan_file = spec_dir / "implementation_plan.json" + # ... + # Find first pending chunk in available phase + for chunk in phase.get("chunks", []): + if chunk.get("status") == "pending": + return chunk +``` + +**Recovery Handling:** +- `in_progress` chunks are reset to `pending` on recovery (see `ipc-handlers.ts:926`) +- Completed chunks remain `completed` to preserve progress +- Failed chunks can be reset to `pending` for retry + +## Recovery Scenarios + +### Scenario 1: Planning Complete, No Worktree + +**State:** +- ✅ `implementation_plan.json` exists +- ❌ No staging worktree exists + +**Behavior:** +- System detects plan exists → skips planner +- Creates new staging worktree +- Starts coding phase with first pending chunk + +### Scenario 2: Planning Complete, Worktree Exists + +**State:** +- ✅ `implementation_plan.json` exists +- ✅ Staging worktree exists at `.worktrees/auto-claude/` + +**Behavior:** +- System detects both exist +- Prompts user (or auto-continues) to resume +- Uses existing worktree +- Resumes from last incomplete chunk + +### Scenario 3: Planning Incomplete + +**State:** +- ❌ `implementation_plan.json` missing or invalid +- ❌ No staging worktree (or exists but no plan) + +**Behavior:** +- System detects missing plan +- Runs planner session first +- Creates `implementation_plan.json` +- Then proceeds to coding phase + +### Scenario 4: Crash During Coding + +**State:** +- ✅ `implementation_plan.json` exists +- ✅ Staging worktree exists +- ⚠️ Some chunks `in_progress` or `failed` + +**Behavior:** +- On restart, `in_progress` chunks are reset to `pending` +- System resumes from first `pending` chunk +- Worktree preserves all committed work +- Uncommitted changes may be lost (depends on git state) + +## Frontend Integration + +**UI State Detection (`auto-claude-ui`):** + +The frontend tracks state via: +1. **File Watcher**: Monitors `implementation_plan.json` for changes +2. **Task Store**: Reads plan file to determine task status +3. **Agent Manager**: Parses logs to detect phase transitions + +**Recovery Actions:** +- `ipc-handlers.ts::recoverTask()` - Resets stuck tasks +- Resets `in_progress` → `pending` for interrupted chunks +- Preserves `completed` chunks + +## Key Files for State + +**State Files:** +- `.auto-claude/specs/{spec}/implementation_plan.json` - Main state file +- `.auto-claude/specs/{spec}/memory/attempt_history.json` - Recovery history +- `.auto-claude/specs/{spec}/memory/build_commits.json` - Git commit tracking +- `.worktrees/auto-claude/` - Staging worktree (git-managed) + +**Detection Functions:** +- `is_first_run()` - Checks if planning needed +- `get_existing_build_worktree()` - Checks for worktree +- `get_next_chunk()` - Finds next work item +- `load_implementation_plan()` - Loads plan state + +## Summary + +The system uses a **file-based state detection** approach: + +1. **Planning Complete?** → Check for `implementation_plan.json` +2. **Worktree Exists?** → Check for `.worktrees/auto-claude/` +3. **What's Next?** → Read chunk statuses from `implementation_plan.json` +4. **Resume Point** → First `pending` chunk in available phase + +This design ensures: +- ✅ No duplicate planning if plan already exists +- ✅ Work preservation via git worktrees +- ✅ Progress tracking via chunk statuses +- ✅ Safe resumption after crashes +- ✅ User control over continuation vs fresh start diff --git a/auto-build-ui/README.md b/auto-build-ui/README.md deleted file mode 100644 index a9083320..00000000 --- a/auto-build-ui/README.md +++ /dev/null @@ -1,162 +0,0 @@ -# Auto Claude UI - -A desktop application for managing AI-driven development tasks using the Auto Claude autonomous coding framework. - -## Overview - -Auto Claude UI provides a visual Kanban board interface for creating, monitoring, and managing auto-build tasks. It replaces the terminal-based workflow with an intuitive GUI while preserving all CLI functionality. - -## Features - -- **Project Management**: Add, configure, and switch between multiple projects -- **Kanban Board**: Visual task board with columns for Backlog, In Progress, AI Review, Human Review, and Done -- **Task Creation Wizard**: Form-based interface for creating new tasks -- **Real-Time Progress**: Live updates from implementation_plan.json during agent execution -- **Human Review Workflow**: Review QA results and provide feedback -- **Theme Support**: Light and dark mode with system preference detection -- **Settings**: Per-project and app-wide configuration options - -## Tech Stack - -- **Framework**: Electron with React 18 (TypeScript) -- **Build Tool**: electron-vite with electron-builder -- **UI Components**: Radix UI primitives (shadcn/ui pattern) -- **Styling**: TailwindCSS with dark mode support -- **State Management**: Zustand -- **File Watching**: chokidar - -## Project Structure - -``` -auto-build-ui/ -├── src/ -│ ├── main/ # Electron main process -│ │ ├── index.ts # App entry point -│ │ ├── agent-manager.ts # Python subprocess management -│ │ ├── file-watcher.ts # Implementation plan watching -│ │ ├── ipc-handlers.ts # IPC message handlers -│ │ └── project-store.ts # JSON project persistence -│ ├── preload/ # Preload scripts -│ │ └── index.ts # Secure contextBridge API -│ ├── renderer/ # React application -│ │ ├── components/ # React components -│ │ │ ├── ui/ # Wrapped Radix UI components -│ │ │ ├── Sidebar.tsx -│ │ │ ├── KanbanBoard.tsx -│ │ │ ├── TaskCard.tsx -│ │ │ ├── TaskDetailPanel.tsx -│ │ │ ├── TaskCreationWizard.tsx -│ │ │ ├── ProjectSettings.tsx -│ │ │ └── AppSettings.tsx -│ │ ├── stores/ # Zustand state stores -│ │ ├── hooks/ # Custom React hooks -│ │ ├── styles/ # Global CSS -│ │ └── App.tsx # Root component -│ └── shared/ # Shared code -│ ├── types.ts # TypeScript interfaces -│ └── constants.ts # Constants and IPC channels -├── electron.vite.config.ts # Build configuration -├── package.json -├── tsconfig.json -├── tailwind.config.js -└── postcss.config.js -``` - -## Getting Started - -### Prerequisites - -- Node.js 18+ -- npm or pnpm -- Python 3.10+ (for auto-build backend) - -### Installation - -```bash -# Navigate to auto-build-ui directory -cd auto-build-ui - -# Install dependencies -npm install -``` - -### Development - -```bash -# Start development server with hot reload -npm run dev -``` - -### Build - -```bash -# Build for production -npm run build - -# Package for macOS -npm run package:mac - -# Package for Windows -npm run package:win - -# Package for Linux -npm run package:linux -``` - -### Type Checking - -```bash -npm run typecheck -``` - -### Linting - -```bash -npm run lint -``` - -### Testing - -```bash -npm run test -``` - -## Architecture - -### Main Process - -The main process handles: -- Window management -- Python subprocess spawning (agent-manager.ts) -- File system watching (file-watcher.ts) -- Project data persistence (project-store.ts) -- IPC communication with renderer - -### Preload Script - -Provides a secure bridge between main and renderer processes using Electron's contextBridge. All IPC channels are explicitly defined and typed. - -### Renderer Process - -A React application with: -- Zustand stores for state management -- Custom hooks for IPC event handling -- Radix UI components wrapped in the shadcn/ui pattern -- TailwindCSS for styling - -## Security - -The application follows Electron security best practices: -- `contextIsolation: true` -- `nodeIntegration: false` -- Minimal API surface via contextBridge -- No direct ipcRenderer exposure - -## Environment Variables - -- `CLAUDE_CODE_OAUTH_TOKEN`: OAuth token for Claude Code SDK (from auto-build/.env) -- `FALKORDB_URL`: FalkorDB connection URL (optional, defaults to localhost:6379) - -## License - -MIT diff --git a/auto-claude-ui/src/main/agent-manager.ts b/auto-claude-ui/src/main/agent-manager.ts index 833ef911..f321bafb 100644 --- a/auto-claude-ui/src/main/agent-manager.ts +++ b/auto-claude-ui/src/main/agent-manager.ts @@ -134,7 +134,9 @@ export class AgentManager extends EventEmitter { startSpecCreation( taskId: string, projectPath: string, - taskDescription: string + taskDescription: string, + specDir?: string, // Optional spec directory (when task already has a directory created by UI) + devMode: boolean = false // Dev mode: use dev/auto-claude/specs/ for framework development ): void { // Use source auto-claude path (the repo), not the project's auto-claude const autoBuildSource = this.getAutoBuildSourcePath(); @@ -157,6 +159,16 @@ export class AgentManager extends EventEmitter { // spec_runner.py will auto-start run.py after spec creation completes const args = [specRunnerPath, '--task', taskDescription, '--project-dir', projectPath]; + // Pass spec directory if provided (for UI-created tasks that already have a directory) + if (specDir) { + args.push('--spec-dir', specDir); + } + + // Pass --dev flag for framework development mode + if (devMode) { + args.push('--dev'); + } + // Note: This is spec-creation but it chains to task-execution via run.py // So we treat the whole thing as task-execution for status purposes this.spawnProcess(taskId, autoBuildSource, args, autoBuildEnv, 'task-execution'); @@ -169,7 +181,7 @@ export class AgentManager extends EventEmitter { taskId: string, projectPath: string, specId: string, - options: { parallel?: boolean; workers?: number } = {} + options: { parallel?: boolean; workers?: number; devMode?: boolean } = {} ): void { console.log('[AgentManager] startTaskExecution called for:', taskId, specId); // Use source auto-claude path (the repo), not the project's auto-claude @@ -195,10 +207,18 @@ export class AgentManager extends EventEmitter { const args = [runPath, '--spec', specId, '--project-dir', projectPath]; + // Always use auto-continue when running from UI (non-interactive) + args.push('--auto-continue'); + if (options.parallel && options.workers) { args.push('--parallel', options.workers.toString()); } + // Pass --dev flag for framework development mode + if (options.devMode) { + args.push('--dev'); + } + console.log('[AgentManager] Spawning process with args:', args); this.spawnProcess(taskId, autoBuildSource, args, autoBuildEnv, 'task-execution'); } @@ -209,7 +229,8 @@ export class AgentManager extends EventEmitter { startQAProcess( taskId: string, projectPath: string, - specId: string + specId: string, + devMode: boolean = false ): void { // Use source auto-claude path (the repo), not the project's auto-claude const autoBuildSource = this.getAutoBuildSourcePath(); @@ -231,6 +252,11 @@ export class AgentManager extends EventEmitter { const args = [runPath, '--spec', specId, '--project-dir', projectPath, '--qa']; + // Pass --dev flag for framework development mode + if (devMode) { + args.push('--dev'); + } + this.spawnProcess(taskId, autoBuildSource, args, autoBuildEnv, 'qa-process'); } diff --git a/auto-claude-ui/src/main/changelog-service.ts b/auto-claude-ui/src/main/changelog-service.ts index 9296f248..412b4d06 100644 --- a/auto-claude-ui/src/main/changelog-service.ts +++ b/auto-claude-ui/src/main/changelog-service.ts @@ -103,9 +103,12 @@ export class ChangelogService extends EventEmitter { /** * Get completed tasks from a project + * @param projectPath - Project root path + * @param tasks - List of tasks + * @param specsBaseDir - Base specs directory (e.g., 'auto-claude/specs' or 'dev/auto-claude/specs') */ - getCompletedTasks(projectPath: string, tasks: Task[]): ChangelogTask[] { - const specsDir = path.join(projectPath, AUTO_BUILD_PATHS.SPECS_DIR); + getCompletedTasks(projectPath: string, tasks: Task[], specsBaseDir?: string): ChangelogTask[] { + const specsDir = path.join(projectPath, specsBaseDir || AUTO_BUILD_PATHS.SPECS_DIR); return tasks .filter(task => task.status === 'done') @@ -127,9 +130,13 @@ export class ChangelogService extends EventEmitter { /** * Load spec files for given tasks + * @param projectPath - Project root path + * @param taskIds - IDs of tasks to load specs for + * @param tasks - List of all tasks + * @param specsBaseDir - Base specs directory (e.g., 'auto-claude/specs' or 'dev/auto-claude/specs') */ - async loadTaskSpecs(projectPath: string, taskIds: string[], tasks: Task[]): Promise { - const specsDir = path.join(projectPath, AUTO_BUILD_PATHS.SPECS_DIR); + async loadTaskSpecs(projectPath: string, taskIds: string[], tasks: Task[], specsBaseDir?: string): Promise { + const specsDir = path.join(projectPath, specsBaseDir || AUTO_BUILD_PATHS.SPECS_DIR); const results: TaskSpecContent[] = []; for (const taskId of taskIds) { diff --git a/auto-claude-ui/src/main/ipc-handlers.ts b/auto-claude-ui/src/main/ipc-handlers.ts index 19b8355e..8a8c010a 100644 --- a/auto-claude-ui/src/main/ipc-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers.ts @@ -2,7 +2,7 @@ import { ipcMain, dialog, BrowserWindow, app } from 'electron'; import path from 'path'; import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, mkdirSync } from 'fs'; import { spawn } from 'child_process'; -import { IPC_CHANNELS, DEFAULT_APP_SETTINGS, AUTO_BUILD_PATHS } from '../shared/constants'; +import { IPC_CHANNELS, DEFAULT_APP_SETTINGS, AUTO_BUILD_PATHS, getSpecsDir } from '../shared/constants'; import type { Project, ProjectSettings, @@ -325,8 +325,10 @@ export function setupIpcHandlers( } // Generate a unique spec ID based on existing specs - const autoBuildDir = project.autoBuildPath || 'auto-claude'; - const specsDir = path.join(project.path, autoBuildDir, 'specs'); + // Use devMode-aware path for specs directory + const devMode = project.settings.devMode ?? false; + const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode); + const specsDir = path.join(project.path, specsBaseDir); // Find next available spec number let specNumber = 1; @@ -405,6 +407,56 @@ export function setupIpcHandlers( } ); + ipcMain.handle( + IPC_CHANNELS.TASK_DELETE, + async (_, taskId: string): Promise => { + const { rm } = await import('fs/promises'); + + // Find task and project + const projects = projectStore.getProjects(); + let task: Task | undefined; + let project: Project | undefined; + + for (const p of projects) { + const tasks = projectStore.getTasks(p.id); + task = tasks.find((t) => t.id === taskId || t.specId === taskId); + if (task) { + project = p; + break; + } + } + + if (!task || !project) { + return { success: false, error: 'Task or project not found' }; + } + + // Check if task is currently running + const isRunning = agentManager.isRunning(taskId); + if (isRunning) { + return { success: false, error: 'Cannot delete a running task. Stop the task first.' }; + } + + // Delete the spec directory - use devMode-aware path + const devMode = project.settings.devMode ?? false; + const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode); + const specDir = path.join(project.path, specsBaseDir, task.specId); + + try { + if (existsSync(specDir)) { + await rm(specDir, { recursive: true, force: true }); + console.log(`[TASK_DELETE] Deleted spec directory: ${specDir}`); + } + return { success: true }; + } catch (error) { + console.error('[TASK_DELETE] Error deleting spec directory:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to delete task files' + }; + } + } + ); + ipcMain.on( IPC_CHANNELS.TASK_START, (_, taskId: string, options?: TaskStartOptions) => { @@ -441,12 +493,16 @@ export function setupIpcHandlers( console.log('[TASK_START] Found task:', task.specId, 'status:', task.status, 'chunks:', task.chunks.length); + // Check if dev mode is enabled for this project + const devMode = project.settings.devMode ?? false; + console.log('[TASK_START] Dev mode:', devMode); + // Start file watcher for this task - const autoBuildDir = project.autoBuildPath || 'auto-claude'; + // Use getSpecsDir helper to get correct path based on dev mode + const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode); const specDir = path.join( project.path, - autoBuildDir, - 'specs', + specsBaseDir, task.specId ); fileWatcher.watch(taskId, specDir); @@ -465,10 +521,11 @@ export function setupIpcHandlers( if (needsSpecCreation) { // No spec file - need to run spec_runner.py to create the spec const taskDescription = task.description || task.title; - console.log('[TASK_START] Starting spec creation for:', task.specId); + console.log('[TASK_START] Starting spec creation for:', task.specId, 'in:', specDir); - // Start spec creation process - agentManager.startSpecCreation(task.specId, project.path, taskDescription); + // Start spec creation process - pass the existing spec directory + // so spec_runner uses it instead of creating a new one + agentManager.startSpecCreation(task.specId, project.path, taskDescription, specDir, devMode); } else if (needsImplementation) { // Spec exists but no chunks - run run.py to create implementation plan and execute // Read the spec.md to get the task description @@ -481,25 +538,43 @@ export function setupIpcHandlers( console.log('[TASK_START] Starting task execution (no chunks) for:', task.specId); // Start task execution which will create the implementation plan + // Note: No parallel mode for planning phase - parallel only makes sense with multiple chunks agentManager.startTaskExecution( taskId, project.path, task.specId, { - parallel: options?.parallel ?? project.settings.parallelEnabled, - workers: options?.workers ?? project.settings.maxWorkers + parallel: false, // Sequential for planning phase + workers: 1, + devMode } ); } else { // Task has chunks, start normal execution + // Only enable parallel if there are multiple chunks AND user has parallel enabled + const hasMultipleChunks = task.chunks.length > 1; + const pendingChunks = task.chunks.filter(c => c.status === 'pending' || c.status === 'in_progress').length; + const parallelEnabled = options?.parallel ?? project.settings.parallelEnabled; + const useParallel = parallelEnabled && hasMultipleChunks && pendingChunks > 1; + const workers = useParallel ? (options?.workers ?? project.settings.maxWorkers) : 1; + console.log('[TASK_START] Starting task execution (has chunks) for:', task.specId); + console.log('[TASK_START] Parallel decision:', { + hasMultipleChunks, + pendingChunks, + parallelEnabled, + useParallel, + workers + }); + agentManager.startTaskExecution( taskId, project.path, task.specId, { - parallel: options?.parallel ?? project.settings.parallelEnabled, - workers: options?.workers ?? project.settings.maxWorkers + parallel: useParallel, + workers, + devMode } ); } @@ -553,9 +628,12 @@ export function setupIpcHandlers( return { success: false, error: 'Task not found' }; } + // Check if dev mode is enabled for this project + const devMode = project.settings.devMode ?? false; + const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode); const specDir = path.join( project.path, - AUTO_BUILD_PATHS.SPECS_DIR, + specsBaseDir, task.specId ); @@ -583,8 +661,8 @@ export function setupIpcHandlers( `# QA Fix Request\n\nStatus: REJECTED\n\n## Feedback\n\n${feedback || 'No feedback provided'}\n\nCreated at: ${new Date().toISOString()}\n` ); - // Restart QA process - agentManager.startQAProcess(taskId, project.path, task.specId); + // Restart QA process with dev mode + agentManager.startQAProcess(taskId, project.path, task.specId, devMode); const mainWindow = getMainWindow(); if (mainWindow) { @@ -625,12 +703,12 @@ export function setupIpcHandlers( return { success: false, error: 'Task not found' }; } - // Get the spec directory - check both .auto-claude and auto-claude - const autoBuildDir = project.autoBuildPath || 'auto-claude'; + // Get the spec directory - use devMode-aware path + const devMode = project.settings.devMode ?? false; + const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode); const specDir = path.join( project.path, - autoBuildDir, - 'specs', + specsBaseDir, task.specId ); @@ -677,6 +755,71 @@ export function setupIpcHandlers( writeFileSync(planPath, JSON.stringify(plan, null, 2)); } + // Auto-start task when status changes to 'in_progress' and no process is running + if (status === 'in_progress' && !agentManager.isRunning(taskId)) { + const mainWindow = getMainWindow(); + console.log('[TASK_UPDATE_STATUS] Auto-starting task:', taskId); + + // Start file watcher for this task + fileWatcher.watch(taskId, specDir); + + // Check if spec.md exists + const specFilePath = path.join(specDir, AUTO_BUILD_PATHS.SPEC_FILE); + const hasSpec = existsSync(specFilePath); + const needsSpecCreation = !hasSpec; + const needsImplementation = hasSpec && task.chunks.length === 0; + + console.log('[TASK_UPDATE_STATUS] hasSpec:', hasSpec, 'needsSpecCreation:', needsSpecCreation, 'needsImplementation:', needsImplementation); + + if (needsSpecCreation) { + // No spec file - need to run spec_runner.py to create the spec + const taskDescription = task.description || task.title; + console.log('[TASK_UPDATE_STATUS] Starting spec creation for:', task.specId); + agentManager.startSpecCreation(task.specId, project.path, taskDescription, specDir, devMode); + } else if (needsImplementation) { + // Spec exists but no chunks - run run.py to create implementation plan and execute + console.log('[TASK_UPDATE_STATUS] Starting task execution (no chunks) for:', task.specId); + agentManager.startTaskExecution( + taskId, + project.path, + task.specId, + { + parallel: false, + workers: 1, + devMode + } + ); + } else { + // Task has chunks, start normal execution + const hasMultipleChunks = task.chunks.length > 1; + const pendingChunks = task.chunks.filter(c => c.status === 'pending' || c.status === 'in_progress').length; + const parallelEnabled = project.settings.parallelEnabled; + const useParallel = parallelEnabled && hasMultipleChunks && pendingChunks > 1; + const workers = useParallel ? project.settings.maxWorkers : 1; + + console.log('[TASK_UPDATE_STATUS] Starting task execution (has chunks) for:', task.specId); + agentManager.startTaskExecution( + taskId, + project.path, + task.specId, + { + parallel: useParallel, + workers, + devMode + } + ); + } + + // Notify renderer about status change + if (mainWindow) { + mainWindow.webContents.send( + IPC_CHANNELS.TASK_STATUS_CHANGE, + taskId, + 'in_progress' + ); + } + } + return { success: true }; } catch (error) { console.error('Failed to update task status:', error); @@ -739,10 +882,6 @@ export function setupIpcHandlers( return { success: false, error: 'Task not found' }; } - // Determine the target status - default to 'backlog' if not specified - // If task had some chunks completed, maybe we should go to 'human_review' - const newStatus: TaskStatus = targetStatus || 'backlog'; - // Get the spec directory const autoBuildDir = project.autoBuildPath || 'auto-claude'; const specDir = path.join( @@ -756,10 +895,43 @@ export function setupIpcHandlers( const planPath = path.join(specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN); try { + // Read the plan to analyze chunk progress + let plan: Record | null = null; if (existsSync(planPath)) { const planContent = readFileSync(planPath, 'utf-8'); - const plan = JSON.parse(planContent); + plan = JSON.parse(planContent); + } + // Determine the target status intelligently based on chunk progress + // If targetStatus is explicitly provided, use it; otherwise calculate from chunks + let newStatus: TaskStatus = targetStatus || 'backlog'; + + if (!targetStatus && plan?.phases && Array.isArray(plan.phases)) { + // Analyze chunk statuses to determine appropriate recovery status + const allChunks: Array<{ status: string }> = []; + for (const phase of plan.phases as Array<{ chunks?: Array<{ status: string }> }>) { + if (phase.chunks && Array.isArray(phase.chunks)) { + allChunks.push(...phase.chunks); + } + } + + if (allChunks.length > 0) { + const completedCount = allChunks.filter(c => c.status === 'completed').length; + const allCompleted = completedCount === allChunks.length; + + if (allCompleted) { + // All chunks completed - should go to review (ai_review or human_review based on source) + // For recovery, human_review is safer as it requires manual verification + newStatus = 'human_review'; + } else if (completedCount > 0) { + // Some chunks completed, some still pending - task is in progress + newStatus = 'in_progress'; + } + // else: no chunks completed, stay with 'backlog' + } + } + + if (plan) { // Update status plan.status = newStatus; plan.planStatus = newStatus === 'done' ? 'completed' @@ -772,20 +944,28 @@ export function setupIpcHandlers( // Add recovery note plan.recoveryNote = `Task recovered from stuck state at ${new Date().toISOString()}`; - // Reset all chunk statuses to 'pending' so the task can be restarted - // This allows run.py to pick up from where it left off or restart + // Reset in_progress and failed chunk statuses to 'pending' so they can be retried + // Keep completed chunks as-is so run.py can resume from where it left off if (plan.phases && Array.isArray(plan.phases)) { - for (const phase of plan.phases) { + for (const phase of plan.phases as Array<{ chunks?: Array<{ status: string; actual_output?: string; started_at?: string; completed_at?: string }> }>) { if (phase.chunks && Array.isArray(phase.chunks)) { for (const chunk of phase.chunks) { // Reset in_progress chunks to pending (they were interrupted) // Keep completed chunks as-is so run.py can resume if (chunk.status === 'in_progress') { chunk.status = 'pending'; + // Clear execution data to maintain consistency + delete chunk.actual_output; + delete chunk.started_at; + delete chunk.completed_at; } // Also reset failed chunks so they can be retried if (chunk.status === 'failed') { chunk.status = 'pending'; + // Clear execution data to maintain consistency + delete chunk.actual_output; + delete chunk.started_at; + delete chunk.completed_at; } } } @@ -827,6 +1007,411 @@ export function setupIpcHandlers( } ); + // ============================================ + // Workspace Management Operations (for human review) + // ============================================ + + /** + * Helper function to find task and project by taskId + */ + const findTaskAndProject = (taskId: string): { task: Task | undefined; project: Project | undefined } => { + const projects = projectStore.getProjects(); + let task: Task | undefined; + let project: Project | undefined; + + for (const p of projects) { + const tasks = projectStore.getTasks(p.id); + task = tasks.find((t) => t.id === taskId || t.specId === taskId); + if (task) { + project = p; + break; + } + } + + return { task, project }; + }; + + /** + * Get the worktree status for a task + */ + ipcMain.handle( + IPC_CHANNELS.TASK_WORKTREE_STATUS, + async (_, taskId: string): Promise> => { + try { + const { task, project } = findTaskAndProject(taskId); + if (!task || !project) { + return { success: false, error: 'Task not found' }; + } + + const worktreePath = path.join(project.path, '.worktrees', 'auto-claude-staging'); + + if (!existsSync(worktreePath)) { + return { + success: true, + data: { exists: false } + }; + } + + // Get branch info from git + const { execSync } = require('child_process'); + + try { + // Get current branch in worktree + const branch = execSync('git rev-parse --abbrev-ref HEAD', { + cwd: worktreePath, + encoding: 'utf-8' + }).trim(); + + // Get base branch (usually main or master) + let baseBranch = 'main'; + try { + // Try to get the default branch + baseBranch = execSync('git rev-parse --abbrev-ref origin/HEAD 2>/dev/null || echo main', { + cwd: project.path, + encoding: 'utf-8' + }).trim().replace('origin/', ''); + } catch { + baseBranch = 'main'; + } + + // Get commit count + let commitCount = 0; + try { + const countOutput = execSync(`git rev-list --count ${baseBranch}..HEAD 2>/dev/null || echo 0`, { + cwd: worktreePath, + encoding: 'utf-8' + }).trim(); + commitCount = parseInt(countOutput, 10) || 0; + } catch { + commitCount = 0; + } + + // Get diff stats + let filesChanged = 0; + let additions = 0; + let deletions = 0; + + try { + const diffStat = execSync(`git diff --stat ${baseBranch}...HEAD 2>/dev/null || echo ""`, { + cwd: worktreePath, + encoding: 'utf-8' + }).trim(); + + // Parse the summary line (e.g., "3 files changed, 50 insertions(+), 10 deletions(-)") + const summaryMatch = diffStat.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/); + if (summaryMatch) { + filesChanged = parseInt(summaryMatch[1], 10) || 0; + additions = parseInt(summaryMatch[2], 10) || 0; + deletions = parseInt(summaryMatch[3], 10) || 0; + } + } catch { + // Ignore diff errors + } + + return { + success: true, + data: { + exists: true, + worktreePath, + branch, + baseBranch, + commitCount, + filesChanged, + additions, + deletions + } + }; + } catch (gitError) { + console.error('Git error getting worktree status:', gitError); + return { + success: true, + data: { exists: true, worktreePath } + }; + } + } catch (error) { + console.error('Failed to get worktree status:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get worktree status' + }; + } + } + ); + + /** + * Get the diff for a task's worktree + */ + ipcMain.handle( + IPC_CHANNELS.TASK_WORKTREE_DIFF, + async (_, taskId: string): Promise> => { + try { + const { task, project } = findTaskAndProject(taskId); + if (!task || !project) { + return { success: false, error: 'Task not found' }; + } + + const worktreePath = path.join(project.path, '.worktrees', 'auto-claude-staging'); + + if (!existsSync(worktreePath)) { + return { success: false, error: 'No worktree found for this task' }; + } + + const { execSync } = require('child_process'); + + // Get base branch + let baseBranch = 'main'; + try { + baseBranch = execSync('git rev-parse --abbrev-ref origin/HEAD 2>/dev/null || echo main', { + cwd: project.path, + encoding: 'utf-8' + }).trim().replace('origin/', ''); + } catch { + baseBranch = 'main'; + } + + // Get the diff with file stats + const files: import('../shared/types').WorktreeDiffFile[] = []; + + try { + // Get numstat for additions/deletions per file + const numstat = execSync(`git diff --numstat ${baseBranch}...HEAD 2>/dev/null || echo ""`, { + cwd: worktreePath, + encoding: 'utf-8' + }).trim(); + + // Get name-status for file status + const nameStatus = execSync(`git diff --name-status ${baseBranch}...HEAD 2>/dev/null || echo ""`, { + cwd: worktreePath, + encoding: 'utf-8' + }).trim(); + + // Parse name-status to get file statuses + const statusMap: Record = {}; + nameStatus.split('\n').filter(Boolean).forEach(line => { + const [status, ...pathParts] = line.split('\t'); + const filePath = pathParts.join('\t'); // Handle files with tabs in name + switch (status[0]) { + case 'A': statusMap[filePath] = 'added'; break; + case 'M': statusMap[filePath] = 'modified'; break; + case 'D': statusMap[filePath] = 'deleted'; break; + case 'R': statusMap[pathParts[1] || filePath] = 'renamed'; break; + default: statusMap[filePath] = 'modified'; + } + }); + + // Parse numstat for additions/deletions + numstat.split('\n').filter(Boolean).forEach(line => { + const [adds, dels, filePath] = line.split('\t'); + files.push({ + path: filePath, + status: statusMap[filePath] || 'modified', + additions: parseInt(adds, 10) || 0, + deletions: parseInt(dels, 10) || 0 + }); + }); + } catch (diffError) { + console.error('Error getting diff:', diffError); + } + + // Generate summary + const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0); + const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0); + const summary = `${files.length} files changed, ${totalAdditions} insertions(+), ${totalDeletions} deletions(-)`; + + return { + success: true, + data: { files, summary } + }; + } catch (error) { + console.error('Failed to get worktree diff:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get worktree diff' + }; + } + } + ); + + /** + * Merge the worktree changes into the main branch + */ + ipcMain.handle( + IPC_CHANNELS.TASK_WORKTREE_MERGE, + async (_, taskId: string): Promise> => { + try { + const { task, project } = findTaskAndProject(taskId); + if (!task || !project) { + return { success: false, error: 'Task not found' }; + } + + // Use run.py --merge to handle the merge + const sourcePath = getEffectiveSourcePath(); + if (!sourcePath) { + return { success: false, error: 'Auto Claude source not found' }; + } + + const runScript = path.join(sourcePath, 'run.py'); + const specDir = path.join(project.path, project.autoBuildPath || '.auto-claude', 'specs', task.specId); + + if (!existsSync(specDir)) { + return { success: false, error: 'Spec directory not found' }; + } + + return new Promise((resolve) => { + const mergeProcess = spawn('python3', [ + runScript, + '--spec', task.specId, + '--project-dir', project.path, + '--merge' + ], { + cwd: sourcePath, + env: { + ...process.env, + PYTHONUNBUFFERED: '1' + } + }); + + let stdout = ''; + let stderr = ''; + + mergeProcess.stdout.on('data', (data: Buffer) => { + stdout += data.toString(); + }); + + mergeProcess.stderr.on('data', (data: Buffer) => { + stderr += data.toString(); + }); + + mergeProcess.on('close', (code: number) => { + if (code === 0) { + // Update task status to done after successful merge + projectStore.updateTaskStatus(taskId, 'done'); + + const mainWindow = getMainWindow(); + if (mainWindow) { + mainWindow.webContents.send(IPC_CHANNELS.TASK_STATUS_CHANGE, taskId, 'done'); + } + + resolve({ + success: true, + data: { + success: true, + message: 'Changes merged successfully' + } + }); + } else { + // Check if there were conflicts + const hasConflicts = stdout.includes('conflict') || stderr.includes('conflict'); + + resolve({ + success: true, + data: { + success: false, + message: hasConflicts ? 'Merge conflicts detected' : `Merge failed: ${stderr || stdout}`, + conflictFiles: hasConflicts ? [] : undefined + } + }); + } + }); + + mergeProcess.on('error', (err: Error) => { + resolve({ + success: false, + error: `Failed to run merge: ${err.message}` + }); + }); + }); + } catch (error) { + console.error('Failed to merge worktree:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to merge worktree' + }; + } + } + ); + + /** + * Discard the worktree changes + */ + ipcMain.handle( + IPC_CHANNELS.TASK_WORKTREE_DISCARD, + async (_, taskId: string): Promise> => { + try { + const { task, project } = findTaskAndProject(taskId); + if (!task || !project) { + return { success: false, error: 'Task not found' }; + } + + const worktreePath = path.join(project.path, '.worktrees', 'auto-claude-staging'); + + if (!existsSync(worktreePath)) { + return { + success: true, + data: { + success: true, + message: 'No worktree to discard' + } + }; + } + + const { execSync } = require('child_process'); + + try { + // Get the branch name before removing + const branch = execSync('git rev-parse --abbrev-ref HEAD', { + cwd: worktreePath, + encoding: 'utf-8' + }).trim(); + + // Remove the worktree + execSync(`git worktree remove --force "${worktreePath}"`, { + cwd: project.path, + encoding: 'utf-8' + }); + + // Delete the branch + try { + execSync(`git branch -D "${branch}"`, { + cwd: project.path, + encoding: 'utf-8' + }); + } catch { + // Branch might already be deleted or not exist + } + + // Update task status back to backlog + projectStore.updateTaskStatus(taskId, 'backlog'); + + const mainWindow = getMainWindow(); + if (mainWindow) { + mainWindow.webContents.send(IPC_CHANNELS.TASK_STATUS_CHANGE, taskId, 'backlog'); + } + + return { + success: true, + data: { + success: true, + message: 'Worktree discarded successfully' + } + }; + } catch (gitError) { + console.error('Git error discarding worktree:', gitError); + return { + success: false, + error: `Failed to discard worktree: ${gitError instanceof Error ? gitError.message : 'Unknown error'}` + }; + } + } catch (error) { + console.error('Failed to discard worktree:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to discard worktree' + }; + } + } + ); + // ============================================ // Settings Operations // ============================================ @@ -1377,8 +1962,10 @@ ${(feature.acceptance_criteria || []).map((c: string) => `- [ ] ${c}`).join('\n' reason: 'Graphiti not configured' }; - // Check for graphiti state in specs - const specsDir = path.join(project.path, AUTO_BUILD_PATHS.SPECS_DIR); + // Check for graphiti state in specs (use devMode-aware path) + const devMode = project.settings.devMode ?? false; + const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode); + const specsDir = path.join(project.path, specsBaseDir); if (existsSync(specsDir)) { const specDirs = readdirSync(specsDir) .filter((f: string) => { @@ -1720,7 +2307,10 @@ ${(feature.acceptance_criteria || []).map((c: string) => `- [ ] ${c}`).join('\n' const results: ContextSearchResult[] = []; const queryLower = query.toLowerCase(); - const specsDir = path.join(project.path, AUTO_BUILD_PATHS.SPECS_DIR); + // Use devMode-aware path + const devMode = project.settings.devMode ?? false; + const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode); + const specsDir = path.join(project.path, specsBaseDir); if (existsSync(specsDir)) { const allSpecDirs = readdirSync(specsDir) .filter((f: string) => { @@ -1768,7 +2358,11 @@ ${(feature.acceptance_criteria || []).map((c: string) => `- [ ] ${c}`).join('\n' } const memories: MemoryEpisode[] = []; - const specsDir = path.join(project.path, AUTO_BUILD_PATHS.SPECS_DIR); + + // Use devMode-aware path + const devMode = project.settings.devMode ?? false; + const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode); + const specsDir = path.join(project.path, specsBaseDir); if (existsSync(specsDir)) { const sortedSpecDirs = readdirSync(specsDir) @@ -3791,8 +4385,10 @@ ${issue.body || 'No description provided.'} } // Generate spec ID by finding next available number - const autoBuildDir = project.autoBuildPath || 'auto-claude'; - const specsDir = path.join(project.path, autoBuildDir, 'specs'); + // Use devMode-aware path for specs directory + const devMode = project.settings.devMode ?? false; + const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode); + const specsDir = path.join(project.path, specsBaseDir); // Ensure specs directory exists if (!existsSync(specsDir)) { @@ -4082,7 +4678,11 @@ ${idea.rationale} // Use renderer tasks if provided (they have the correct UI status), // otherwise fall back to reading from filesystem const tasks = rendererTasks || projectStore.getTasks(projectId); - const doneTasks = changelogService.getCompletedTasks(project.path, tasks); + + // Use devMode-aware path for specs + const devMode = project.settings.devMode ?? false; + const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode); + const doneTasks = changelogService.getCompletedTasks(project.path, tasks, specsBaseDir); return { success: true, data: doneTasks }; } @@ -4097,7 +4697,11 @@ ${idea.rationale} } const tasks = projectStore.getTasks(projectId); - const specs = await changelogService.loadTaskSpecs(project.path, taskIds, tasks); + + // Use devMode-aware path for specs + const devMode = project.settings.devMode ?? false; + const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode); + const specs = await changelogService.loadTaskSpecs(project.path, taskIds, tasks, specsBaseDir); return { success: true, data: specs }; } @@ -4119,9 +4723,11 @@ ${idea.rationale} return; } - // Load specs for selected tasks + // Load specs for selected tasks (use devMode-aware path) const tasks = projectStore.getTasks(request.projectId); - const specs = await changelogService.loadTaskSpecs(project.path, request.taskIds, tasks); + const devMode = project.settings.devMode ?? false; + const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode); + const specs = await changelogService.loadTaskSpecs(project.path, request.taskIds, tasks, specsBaseDir); // Start generation changelogService.generateChangelog(request.projectId, project.path, request, specs); @@ -4258,8 +4864,10 @@ ${idea.rationale} try { // Generate a unique spec ID based on existing specs - const autoBuildDir = project.autoBuildPath || 'auto-claude'; - const specsDir = path.join(project.path, autoBuildDir, 'specs'); + // Use devMode-aware path for specs directory + const devMode = project.settings.devMode ?? false; + const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode); + const specsDir = path.join(project.path, specsBaseDir); // Find next available spec number let specNumber = 1; diff --git a/auto-claude-ui/src/main/project-initializer.ts b/auto-claude-ui/src/main/project-initializer.ts index fb5a639b..1962e804 100644 --- a/auto-claude-ui/src/main/project-initializer.ts +++ b/auto-claude-ui/src/main/project-initializer.ts @@ -2,6 +2,21 @@ import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSyn import path from 'path'; import crypto from 'crypto'; +/** + * Debug logging - only logs when AUTO_CLAUDE_DEBUG env var is set + */ +const DEBUG = process.env.AUTO_CLAUDE_DEBUG === 'true' || process.env.AUTO_CLAUDE_DEBUG === '1'; + +function debug(message: string, data?: Record): void { + if (DEBUG) { + if (data) { + console.log(`[ProjectInitializer] ${message}`, JSON.stringify(data, null, 2)); + } else { + console.log(`[ProjectInitializer] ${message}`); + } + } +} + /** * Files and directories to exclude when copying auto-claude */ @@ -218,18 +233,19 @@ export function checkVersion( projectPath: string, sourcePath: string ): VersionCheckResult { - // Check for both .auto-claude and auto-claude folders + debug('checkVersion called', { projectPath, sourcePath }); + + // Only .auto-claude counts as "installed" - auto-claude/ is source code const dotAutoBuildPath = path.join(projectPath, '.auto-claude'); - const autoBuildPath = path.join(projectPath, 'auto-claude'); let installedPath: string | null = null; if (existsSync(dotAutoBuildPath)) { installedPath = dotAutoBuildPath; - } else if (existsSync(autoBuildPath)) { - installedPath = autoBuildPath; + debug('Found .auto-claude folder (installed)'); } if (!installedPath) { + debug('No .auto-claude folder found - not initialized'); return { isInitialized: false, updateAvailable: false @@ -302,8 +318,11 @@ export function initializeProject( projectPath: string, sourcePath: string ): InitializationResult { + debug('initializeProject called', { projectPath, sourcePath }); + // Validate source exists if (!existsSync(sourcePath)) { + debug('Source path does not exist', { sourcePath }); return { success: false, error: `Auto-build source not found at: ${sourcePath}` @@ -312,6 +331,7 @@ export function initializeProject( // Validate project path exists if (!existsSync(projectPath)) { + debug('Project path does not exist', { projectPath }); return { success: false, error: `Project directory not found: ${projectPath}` @@ -322,30 +342,59 @@ export function initializeProject( const dotAutoBuildPath = path.join(projectPath, '.auto-claude'); const autoBuildPath = path.join(projectPath, 'auto-claude'); - if (existsSync(dotAutoBuildPath) || existsSync(autoBuildPath)) { + const dotExists = existsSync(dotAutoBuildPath); + const sourceExists = existsSync(autoBuildPath); + debug('Checking existing paths', { + dotAutoBuildPath, + dotExists, + autoBuildPath, + sourceExists + }); + + // Only .auto-claude counts as "initialized" - auto-claude/ is the source folder + // This allows initializing the Auto Claude project itself (which has auto-claude/ source) + if (dotExists) { + debug('Already initialized - .auto-claude exists'); return { success: false, - error: 'Project already has auto-claude initialized' + error: 'Project already has auto-claude initialized (.auto-claude exists)' }; } try { + debug('Copying files to .auto-claude', { from: sourcePath, to: dotAutoBuildPath }); // Copy files to .auto-claude copyDirectoryRecursive(sourcePath, dotAutoBuildPath, false); // Create specs directory const specsDir = path.join(dotAutoBuildPath, 'specs'); if (!existsSync(specsDir)) { + debug('Creating specs directory', { specsDir }); mkdirSync(specsDir, { recursive: true }); } - - // Create .gitkeep in specs writeFileSync(path.join(specsDir, '.gitkeep'), ''); + // Create roadmap directory + const roadmapDir = path.join(dotAutoBuildPath, 'roadmap'); + if (!existsSync(roadmapDir)) { + debug('Creating roadmap directory', { roadmapDir }); + mkdirSync(roadmapDir, { recursive: true }); + } + writeFileSync(path.join(roadmapDir, '.gitkeep'), ''); + + // Create ideation directory + const ideationDir = path.join(dotAutoBuildPath, 'ideation'); + if (!existsSync(ideationDir)) { + debug('Creating ideation directory', { ideationDir }); + mkdirSync(ideationDir, { recursive: true }); + } + writeFileSync(path.join(ideationDir, '.gitkeep'), ''); + // Copy .env.example to .env if .env doesn't exist const envExamplePath = path.join(dotAutoBuildPath, '.env.example'); const envPath = path.join(dotAutoBuildPath, '.env'); if (existsSync(envExamplePath) && !existsSync(envPath)) { + debug('Copying .env.example to .env'); copyFileSync(envExamplePath, envPath); } @@ -354,6 +403,7 @@ export function initializeProject( const sourceHash = calculateDirectoryHash(sourcePath); const now = new Date().toISOString(); + debug('Writing version metadata', { version, sourceHash }); writeVersionMetadata(dotAutoBuildPath, { version, sourceHash, @@ -362,15 +412,18 @@ export function initializeProject( updatedAt: now }); + debug('Initialization complete', { version }); return { success: true, version, wasUpdate: false }; } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error during initialization'; + debug('Initialization failed', { error: errorMessage }); return { success: false, - error: error instanceof Error ? error.message : 'Unknown error during initialization' + error: errorMessage }; } } @@ -382,30 +435,31 @@ export function updateProject( projectPath: string, sourcePath: string ): InitializationResult { + debug('updateProject called', { projectPath, sourcePath }); + // Validate source exists if (!existsSync(sourcePath)) { + debug('Source path does not exist'); return { success: false, error: `Auto-build source not found at: ${sourcePath}` }; } - // Find existing auto-claude folder + // Only .auto-claude is considered an installed instance const dotAutoBuildPath = path.join(projectPath, '.auto-claude'); - const autoBuildPath = path.join(projectPath, 'auto-claude'); - let targetPath: string; - if (existsSync(dotAutoBuildPath)) { - targetPath = dotAutoBuildPath; - } else if (existsSync(autoBuildPath)) { - targetPath = autoBuildPath; - } else { + if (!existsSync(dotAutoBuildPath)) { + debug('No .auto-claude folder found to update'); return { success: false, - error: 'No auto-claude folder found to update' + error: 'No .auto-claude folder found to update. Initialize the project first.' }; } + const targetPath = dotAutoBuildPath; + debug('Updating .auto-claude folder', { targetPath }); + try { // Copy files with preservation of specs/ and .env copyDirectoryRecursive(sourcePath, targetPath, true); @@ -438,16 +492,36 @@ export function updateProject( } /** - * Get the auto-claude folder path for a project (either .auto-claude or auto-claude) + * Get the auto-claude folder path for a project. + * + * IMPORTANT: Only .auto-claude/ is considered a valid "installed" auto-claude. + * The auto-claude/ folder (if it exists) is the SOURCE CODE being developed, + * not an installation. This allows Auto Claude to be used to develop itself. */ export function getAutoBuildPath(projectPath: string): string | null { const dotAutoBuildPath = path.join(projectPath, '.auto-claude'); const autoBuildPath = path.join(projectPath, 'auto-claude'); - if (existsSync(dotAutoBuildPath)) { + const dotExists = existsSync(dotAutoBuildPath); + const sourceExists = existsSync(autoBuildPath); + + debug('getAutoBuildPath called', { + projectPath, + dotAutoBuildPath, + dotExists, + autoBuildPath, + sourceExists + }); + + // Only .auto-claude counts as an "installed" auto-claude + // auto-claude/ is the source code folder, not an installation + if (dotExists) { + debug('Returning .auto-claude (installed version)'); return '.auto-claude'; - } else if (existsSync(autoBuildPath)) { - return 'auto-claude'; } + + // Don't return auto-claude/ - that's source code, not an installation + // The project needs to be initialized to create .auto-claude/ + debug('No .auto-claude folder found - project not initialized'); return null; } diff --git a/auto-claude-ui/src/main/project-store.ts b/auto-claude-ui/src/main/project-store.ts index 30edbfc8..1f18ed4a 100644 --- a/auto-claude-ui/src/main/project-store.ts +++ b/auto-claude-ui/src/main/project-store.ts @@ -3,7 +3,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from import path from 'path'; import { v4 as uuidv4 } from 'uuid'; import type { Project, ProjectSettings, Task, TaskStatus, TaskMetadata, ImplementationPlan } from '../shared/types'; -import { DEFAULT_PROJECT_SETTINGS, AUTO_BUILD_PATHS } from '../shared/constants'; +import { DEFAULT_PROJECT_SETTINGS, AUTO_BUILD_PATHS, getSpecsDir } from '../shared/constants'; import { getAutoBuildPath } from './project-initializer'; interface StoreData { @@ -156,9 +156,10 @@ export class ProjectStore { const project = this.getProject(projectId); if (!project) return []; - // Use project's autoBuildPath if set, otherwise fallback to default - const autoBuildDir = project.autoBuildPath || 'auto-claude'; - const specsDir = path.join(project.path, autoBuildDir, 'specs'); + // Use devMode-aware path for specs directory + const devMode = project.settings.devMode ?? false; + const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode); + const specsDir = path.join(project.path, specsBaseDir); if (!existsSync(specsDir)) return []; const tasks: Task[] = []; diff --git a/auto-claude-ui/src/preload/index.ts b/auto-claude-ui/src/preload/index.ts index 851384ea..8737c7c9 100644 --- a/auto-claude-ui/src/preload/index.ts +++ b/auto-claude-ui/src/preload/index.ts @@ -103,6 +103,9 @@ const electronAPI: ElectronAPI = { ): Promise> => ipcRenderer.invoke(IPC_CHANNELS.TASK_CREATE, projectId, title, description, metadata), + deleteTask: (taskId: string): Promise => + ipcRenderer.invoke(IPC_CHANNELS.TASK_DELETE, taskId), + startTask: (taskId: string, options?: TaskStartOptions): void => ipcRenderer.send(IPC_CHANNELS.TASK_START, taskId, options), @@ -131,6 +134,22 @@ const electronAPI: ElectronAPI = { checkTaskRunning: (taskId: string): Promise> => ipcRenderer.invoke(IPC_CHANNELS.TASK_CHECK_RUNNING, taskId), + // ============================================ + // Workspace Management (for human review) + // ============================================ + + getWorktreeStatus: (taskId: string): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_STATUS, taskId), + + getWorktreeDiff: (taskId: string): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_DIFF, taskId), + + mergeWorktree: (taskId: string): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_MERGE, taskId), + + discardWorktree: (taskId: string): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_DISCARD, taskId), + // ============================================ // Event Listeners (main → renderer) // ============================================ diff --git a/auto-claude-ui/src/renderer/components/Ideation.tsx b/auto-claude-ui/src/renderer/components/Ideation.tsx index 2f941d4d..afb6e3fb 100644 --- a/auto-claude-ui/src/renderer/components/Ideation.tsx +++ b/auto-claude-ui/src/renderer/components/Ideation.tsx @@ -1150,13 +1150,13 @@ function LowHangingFruitDetails({ idea }: { idea: LowHangingFruitIdea }) {
Effort
-
{idea.affectedFiles.length}
+
{idea.affectedFiles?.length ?? 0}
Files
{/* Builds Upon */} - {idea.buildsUpon.length > 0 && ( + {idea.buildsUpon && idea.buildsUpon.length > 0 && (

@@ -1173,7 +1173,7 @@ function LowHangingFruitDetails({ idea }: { idea: LowHangingFruitIdea }) { )} {/* Affected Files */} - {idea.affectedFiles.length > 0 && ( + {idea.affectedFiles && idea.affectedFiles.length > 0 && (

@@ -1190,7 +1190,7 @@ function LowHangingFruitDetails({ idea }: { idea: LowHangingFruitIdea }) { )} {/* Existing Patterns */} - {idea.existingPatterns.length > 0 && ( + {idea.existingPatterns && idea.existingPatterns.length > 0 && (

Patterns to Follow

    @@ -1245,7 +1245,7 @@ function UIUXDetails({ idea }: { idea: UIUXImprovementIdea }) {
{/* Affected Components */} - {idea.affectedComponents.length > 0 && ( + {idea.affectedComponents && idea.affectedComponents.length > 0 && (

@@ -1314,7 +1314,7 @@ function HighValueDetails({ idea }: { idea: HighValueFeatureIdea }) { )} {/* Acceptance Criteria */} - {idea.acceptanceCriteria.length > 0 && ( + {idea.acceptanceCriteria && idea.acceptanceCriteria.length > 0 && (

@@ -1332,7 +1332,7 @@ function HighValueDetails({ idea }: { idea: HighValueFeatureIdea }) { )} {/* Dependencies */} - {idea.dependencies.length > 0 && ( + {idea.dependencies && idea.dependencies.length > 0 && (

Dependencies

@@ -1399,7 +1399,7 @@ function DocumentationGapDetails({ idea }: { idea: DocumentationGapIdea }) {
{/* Affected Areas */} - {idea.affectedAreas.length > 0 && ( + {idea.affectedAreas && idea.affectedAreas.length > 0 && (

@@ -1439,7 +1439,7 @@ function SecurityHardeningDetails({ idea }: { idea: SecurityHardeningIdea }) {
- {idea.affectedFiles.length} + {idea.affectedFiles?.length ?? 0}
Files
@@ -1486,7 +1486,7 @@ function SecurityHardeningDetails({ idea }: { idea: SecurityHardeningIdea }) {

{/* Affected Files */} - {idea.affectedFiles.length > 0 && ( + {idea.affectedFiles && idea.affectedFiles.length > 0 && (

@@ -1612,7 +1612,7 @@ function PerformanceOptimizationDetails({ idea }: { idea: PerformanceOptimizatio

{/* Affected Areas */} - {idea.affectedAreas.length > 0 && ( + {idea.affectedAreas && idea.affectedAreas.length > 0 && (

@@ -1750,7 +1750,7 @@ function CodeQualityDetails({ idea }: { idea: CodeQualityIdea }) { )} {/* Affected Files */} - {idea.affectedFiles.length > 0 && ( + {idea.affectedFiles && idea.affectedFiles.length > 0 && (

diff --git a/auto-claude-ui/src/renderer/components/Insights.tsx b/auto-claude-ui/src/renderer/components/Insights.tsx index 8646056e..d525f82a 100644 --- a/auto-claude-ui/src/renderer/components/Insights.tsx +++ b/auto-claude-ui/src/renderer/components/Insights.tsx @@ -339,6 +339,11 @@ function MessageBubble({

{message.content}

+ {/* Tool usage history for assistant messages */} + {!isUser && message.toolsUsed && message.toolsUsed.length > 0 && ( + + )} + {/* Task suggestion card */} {message.suggestedTask && ( @@ -413,6 +418,98 @@ function MessageBubble({ ); } +// Tool usage history component for showing tools used in completed messages +interface ToolUsageHistoryProps { + tools: Array<{ + name: string; + input?: string; + timestamp: Date; + }>; +} + +function ToolUsageHistory({ tools }: ToolUsageHistoryProps) { + const [expanded, setExpanded] = useState(false); + + if (tools.length === 0) return null; + + // Group tools by name for summary + const toolCounts = tools.reduce((acc, tool) => { + acc[tool.name] = (acc[tool.name] || 0) + 1; + return acc; + }, {} as Record); + + const getToolIcon = (toolName: string) => { + switch (toolName) { + case 'Read': + return FileText; + case 'Glob': + return FolderSearch; + case 'Grep': + return Search; + default: + return FileText; + } + }; + + const getToolColor = (toolName: string) => { + switch (toolName) { + case 'Read': + return 'text-blue-500'; + case 'Glob': + return 'text-amber-500'; + case 'Grep': + return 'text-green-500'; + default: + return 'text-muted-foreground'; + } + }; + + return ( +
+ + + {expanded && ( +
+ {tools.map((tool, index) => { + const Icon = getToolIcon(tool.name); + return ( +
+ + {tool.name} + {tool.input && ( + + {tool.input} + + )} +
+ ); + })} +
+ )} +
+ ); +} + // Tool indicator component for showing what the AI is currently doing interface ToolIndicatorProps { name: string; diff --git a/auto-claude-ui/src/renderer/components/KanbanBoard.tsx b/auto-claude-ui/src/renderer/components/KanbanBoard.tsx index f3de4cab..2feedcc0 100644 --- a/auto-claude-ui/src/renderer/components/KanbanBoard.tsx +++ b/auto-claude-ui/src/renderer/components/KanbanBoard.tsx @@ -17,7 +17,7 @@ import { sortableKeyboardCoordinates, verticalListSortingStrategy } from '@dnd-kit/sortable'; -import { Plus } from 'lucide-react'; +import { Plus, Inbox, Loader2, Eye, CheckCircle2 } from 'lucide-react'; import { ScrollArea } from './ui/scroll-area'; import { Button } from './ui/button'; import { TaskCard } from './TaskCard'; @@ -41,6 +41,47 @@ interface DroppableColumnProps { onAddClick?: () => void; } +// Empty state content for each column +const getEmptyStateContent = (status: TaskStatus): { icon: React.ReactNode; message: string; subtext?: string } => { + switch (status) { + case 'backlog': + return { + icon: , + message: 'No tasks planned', + subtext: 'Add a task to get started' + }; + case 'in_progress': + return { + icon: , + message: 'Nothing running', + subtext: 'Start a task from Planning' + }; + case 'ai_review': + return { + icon: , + message: 'No tasks in review', + subtext: 'AI will review completed tasks' + }; + case 'human_review': + return { + icon: , + message: 'Nothing to review', + subtext: 'Tasks await your approval here' + }; + case 'done': + return { + icon: , + message: 'No completed tasks', + subtext: 'Approved tasks appear here' + }; + default: + return { + icon: , + message: 'No tasks' + }; + } +}; + function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick }: DroppableColumnProps) { const { setNodeRef } = useDroppable({ id: status @@ -65,22 +106,24 @@ function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick }: Dro } }; + const emptyState = getEmptyStateContent(status); + return (
- {/* Column header */} -
-
+ {/* Column header - enhanced styling */} +
+

{TASK_STATUS_LABELS[status]}

- + {tasks.length}
@@ -88,7 +131,7 @@ function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick }: Dro
- -
@@ -290,20 +404,41 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) { {/* Progress */}
+
+ + Progress +
- - {hasActiveExecution ? 'Overall Progress' : 'Progress'} + + {hasActiveExecution && task.executionProgress?.message + ? task.executionProgress.message + : task.chunks.length > 0 + ? `${task.chunks.filter(c => c.status === 'completed').length}/${task.chunks.length} chunks completed` + : 'No chunks yet'} - + {hasActiveExecution ? `${task.executionProgress?.overallProgress || 0}%` : `${progress}%`}
- +
+ div]:bg-success', + hasActiveExecution && '[&>div]:bg-info' + )} + animated={isRunning || task.status === 'ai_review'} + /> +
{/* Phase Progress Bar Segments */} {hasActiveExecution && (
@@ -345,7 +480,12 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) { {/* Classification Badges */} {task.metadata && ( -
+
+
+ + Classification +
+
{/* Category */} {task.metadata.category && ( )} +
)} {/* Description */} {task.description && (
-

Description

-

{task.description}

+
+ + Description +
+

{task.description}

)} @@ -490,9 +634,16 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) {

{task.metadata.affectedFiles.map((file, idx) => ( - - {file.split('/').pop()} - + + + + {file.split('/').pop()} + + + + {file} + + ))}
@@ -501,52 +652,189 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) { )} {/* Timestamps */} -
-
- Created - {formatRelativeTime(task.createdAt)} +
+
+ + Timeline
-
- Updated - {formatRelativeTime(task.updatedAt)} +
+
+ Created + {formatRelativeTime(task.createdAt)} +
+
+ Updated + {formatRelativeTime(task.updatedAt)} +
- {/* Human Review Section */} + {/* Human Review Section - Enhanced styling */} {needsReview && ( -
-

- - Review Required -

-

- Please review the changes and provide feedback if needed. -

-