auto-claude: Complete migrate-to-new

Update consumers to use new system
This commit is contained in:
AndyMik90
2025-12-12 14:31:56 +01:00
parent bda3ab816f
commit 2e52278ca0
42 changed files with 3050 additions and 551 deletions
+9 -9
View File
@@ -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"
}
+3 -1
View File
@@ -70,4 +70,6 @@ dmypy.json
.claude_settings.json
# Development of Auto Build with Auto Build
dev/
dev/
.auto-claude/
+33
View File
@@ -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:
+268
View File
@@ -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
-162
View File
@@ -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
+29 -3
View File
@@ -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');
}
+11 -4
View File
@@ -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<TaskSpecContent[]> {
const specsDir = path.join(projectPath, AUTO_BUILD_PATHS.SPECS_DIR);
async loadTaskSpecs(projectPath: string, taskIds: string[], tasks: Task[], specsBaseDir?: string): Promise<TaskSpecContent[]> {
const specsDir = path.join(projectPath, specsBaseDir || AUTO_BUILD_PATHS.SPECS_DIR);
const results: TaskSpecContent[] = [];
for (const taskId of taskIds) {
+648 -40
View File
@@ -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<IPCResult> => {
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<string, unknown> | 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<IPCResult<import('../shared/types').WorktreeStatus>> => {
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<IPCResult<import('../shared/types').WorktreeDiff>> => {
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<string, 'added' | 'modified' | 'deleted' | 'renamed'> = {};
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<IPCResult<import('../shared/types').WorktreeMergeResult>> => {
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<IPCResult<import('../shared/types').WorktreeDiscardResult>> => {
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;
+96 -22
View File
@@ -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<string, unknown>): 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;
}
+5 -4
View File
@@ -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[] = [];
+19
View File
@@ -103,6 +103,9 @@ const electronAPI: ElectronAPI = {
): Promise<IPCResult<Task>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_CREATE, projectId, title, description, metadata),
deleteTask: (taskId: string): Promise<IPCResult> =>
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<IPCResult<boolean>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_CHECK_RUNNING, taskId),
// ============================================
// Workspace Management (for human review)
// ============================================
getWorktreeStatus: (taskId: string): Promise<IPCResult<import('../shared/types').WorktreeStatus>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_STATUS, taskId),
getWorktreeDiff: (taskId: string): Promise<IPCResult<import('../shared/types').WorktreeDiff>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_DIFF, taskId),
mergeWorktree: (taskId: string): Promise<IPCResult<import('../shared/types').WorktreeMergeResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_MERGE, taskId),
discardWorktree: (taskId: string): Promise<IPCResult<import('../shared/types').WorktreeDiscardResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.TASK_WORKTREE_DISCARD, taskId),
// ============================================
// Event Listeners (main → renderer)
// ============================================
@@ -1150,13 +1150,13 @@ function LowHangingFruitDetails({ idea }: { idea: LowHangingFruitIdea }) {
<div className="text-xs text-muted-foreground">Effort</div>
</Card>
<Card className="p-3 text-center">
<div className="text-lg font-semibold">{idea.affectedFiles.length}</div>
<div className="text-lg font-semibold">{idea.affectedFiles?.length ?? 0}</div>
<div className="text-xs text-muted-foreground">Files</div>
</Card>
</div>
{/* Builds Upon */}
{idea.buildsUpon.length > 0 && (
{idea.buildsUpon && idea.buildsUpon.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<TrendingUp className="h-4 w-4" />
@@ -1173,7 +1173,7 @@ function LowHangingFruitDetails({ idea }: { idea: LowHangingFruitIdea }) {
)}
{/* Affected Files */}
{idea.affectedFiles.length > 0 && (
{idea.affectedFiles && idea.affectedFiles.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<FileCode className="h-4 w-4" />
@@ -1190,7 +1190,7 @@ function LowHangingFruitDetails({ idea }: { idea: LowHangingFruitIdea }) {
)}
{/* Existing Patterns */}
{idea.existingPatterns.length > 0 && (
{idea.existingPatterns && idea.existingPatterns.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2">Patterns to Follow</h3>
<ul className="space-y-1">
@@ -1245,7 +1245,7 @@ function UIUXDetails({ idea }: { idea: UIUXImprovementIdea }) {
</div>
{/* Affected Components */}
{idea.affectedComponents.length > 0 && (
{idea.affectedComponents && idea.affectedComponents.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<FileCode className="h-4 w-4" />
@@ -1314,7 +1314,7 @@ function HighValueDetails({ idea }: { idea: HighValueFeatureIdea }) {
)}
{/* Acceptance Criteria */}
{idea.acceptanceCriteria.length > 0 && (
{idea.acceptanceCriteria && idea.acceptanceCriteria.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<CheckCircle2 className="h-4 w-4" />
@@ -1332,7 +1332,7 @@ function HighValueDetails({ idea }: { idea: HighValueFeatureIdea }) {
)}
{/* Dependencies */}
{idea.dependencies.length > 0 && (
{idea.dependencies && idea.dependencies.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2">Dependencies</h3>
<div className="flex flex-wrap gap-1">
@@ -1399,7 +1399,7 @@ function DocumentationGapDetails({ idea }: { idea: DocumentationGapIdea }) {
</div>
{/* Affected Areas */}
{idea.affectedAreas.length > 0 && (
{idea.affectedAreas && idea.affectedAreas.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<FileCode className="h-4 w-4" />
@@ -1439,7 +1439,7 @@ function SecurityHardeningDetails({ idea }: { idea: SecurityHardeningIdea }) {
</Card>
<Card className="p-3 text-center">
<div className="text-lg font-semibold">
{idea.affectedFiles.length}
{idea.affectedFiles?.length ?? 0}
</div>
<div className="text-xs text-muted-foreground">Files</div>
</Card>
@@ -1486,7 +1486,7 @@ function SecurityHardeningDetails({ idea }: { idea: SecurityHardeningIdea }) {
</div>
{/* Affected Files */}
{idea.affectedFiles.length > 0 && (
{idea.affectedFiles && idea.affectedFiles.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<FileCode className="h-4 w-4" />
@@ -1612,7 +1612,7 @@ function PerformanceOptimizationDetails({ idea }: { idea: PerformanceOptimizatio
</div>
{/* Affected Areas */}
{idea.affectedAreas.length > 0 && (
{idea.affectedAreas && idea.affectedAreas.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<FileCode className="h-4 w-4" />
@@ -1750,7 +1750,7 @@ function CodeQualityDetails({ idea }: { idea: CodeQualityIdea }) {
)}
{/* Affected Files */}
{idea.affectedFiles.length > 0 && (
{idea.affectedFiles && idea.affectedFiles.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<FileCode className="h-4 w-4" />
@@ -339,6 +339,11 @@ function MessageBubble({
<p className="whitespace-pre-wrap">{message.content}</p>
</div>
{/* Tool usage history for assistant messages */}
{!isUser && message.toolsUsed && message.toolsUsed.length > 0 && (
<ToolUsageHistory tools={message.toolsUsed} />
)}
{/* Task suggestion card */}
{message.suggestedTask && (
<Card className="mt-3 border-primary/20 bg-primary/5">
@@ -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<string, number>);
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 (
<div className="mt-2">
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<span className="flex items-center gap-1">
{Object.entries(toolCounts).map(([name, count]) => {
const Icon = getToolIcon(name);
return (
<span key={name} className={cn('flex items-center gap-0.5', getToolColor(name))}>
<Icon className="h-3 w-3" />
<span>{count}</span>
</span>
);
})}
</span>
<span>{tools.length} tool{tools.length !== 1 ? 's' : ''} used</span>
<span className="text-[10px]">{expanded ? '' : ''}</span>
</button>
{expanded && (
<div className="mt-2 space-y-1 rounded-md border border-border bg-muted/30 p-2">
{tools.map((tool, index) => {
const Icon = getToolIcon(tool.name);
return (
<div
key={`${tool.name}-${index}`}
className="flex items-center gap-2 text-xs"
>
<Icon className={cn('h-3 w-3 shrink-0', getToolColor(tool.name))} />
<span className="font-medium">{tool.name}</span>
{tool.input && (
<span className="text-muted-foreground truncate max-w-[250px]">
{tool.input}
</span>
)}
</div>
);
})}
</div>
)}
</div>
);
}
// Tool indicator component for showing what the AI is currently doing
interface ToolIndicatorProps {
name: string;
@@ -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: <Inbox className="h-6 w-6 text-muted-foreground/50" />,
message: 'No tasks planned',
subtext: 'Add a task to get started'
};
case 'in_progress':
return {
icon: <Loader2 className="h-6 w-6 text-muted-foreground/50" />,
message: 'Nothing running',
subtext: 'Start a task from Planning'
};
case 'ai_review':
return {
icon: <Eye className="h-6 w-6 text-muted-foreground/50" />,
message: 'No tasks in review',
subtext: 'AI will review completed tasks'
};
case 'human_review':
return {
icon: <Eye className="h-6 w-6 text-muted-foreground/50" />,
message: 'Nothing to review',
subtext: 'Tasks await your approval here'
};
case 'done':
return {
icon: <CheckCircle2 className="h-6 w-6 text-muted-foreground/50" />,
message: 'No completed tasks',
subtext: 'Approved tasks appear here'
};
default:
return {
icon: <Inbox className="h-6 w-6 text-muted-foreground/50" />,
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 (
<div
className={cn(
'flex w-72 shrink-0 flex-col rounded-xl border border-white/5 bg-linear-to-b from-secondary/30 to-transparent backdrop-blur-sm transition-all duration-200',
getColumnBorderColor(),
'border-t-2',
isOver && 'bg-accent/10'
isOver && 'drop-zone-highlight'
)}
>
{/* Column header */}
<div className="flex items-center justify-between p-4">
<div className="flex items-center gap-2">
{/* Column header - enhanced styling */}
<div className="flex items-center justify-between p-4 border-b border-white/5">
<div className="flex items-center gap-2.5">
<h2 className="font-semibold text-sm text-foreground">
{TASK_STATUS_LABELS[status]}
</h2>
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-secondary text-xs font-medium text-muted-foreground">
<span className="column-count-badge">
{tasks.length}
</span>
</div>
@@ -88,7 +131,7 @@ function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick }: Dro
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
className="h-7 w-7 hover:bg-primary/10 hover:text-primary transition-colors"
onClick={onAddClick}
>
<Plus className="h-4 w-4" />
@@ -98,20 +141,39 @@ function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick }: Dro
{/* Droppable task list */}
<div ref={setNodeRef} className="flex-1 min-h-0">
<ScrollArea className="h-full px-3 pb-3">
<ScrollArea className="h-full px-3 pb-3 pt-2">
<SortableContext
items={taskIds}
strategy={verticalListSortingStrategy}
>
<div className="space-y-3 min-h-[100px]">
<div className="space-y-3 min-h-[120px]">
{tasks.length === 0 ? (
<div
className={cn(
'rounded-lg border border-dashed border-border p-4 text-center text-sm text-muted-foreground transition-all duration-200',
isOver && 'border-primary/50 bg-accent/30'
'empty-column-dropzone flex flex-col items-center justify-center py-6',
isOver && 'active'
)}
>
{isOver ? 'Drop here' : 'No tasks'}
{isOver ? (
<>
<div className="h-8 w-8 rounded-full bg-primary/20 flex items-center justify-center mb-2">
<Plus className="h-4 w-4 text-primary" />
</div>
<span className="text-sm font-medium text-primary">Drop here</span>
</>
) : (
<>
{emptyState.icon}
<span className="mt-2 text-sm font-medium text-muted-foreground/70">
{emptyState.message}
</span>
{emptyState.subtext && (
<span className="mt-0.5 text-xs text-muted-foreground/50">
{emptyState.subtext}
</span>
)}
</>
)}
</div>
) : (
tasks.map((task) => (
@@ -248,10 +310,10 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
))}
</div>
{/* Drag overlay - shows the card being dragged */}
{/* Drag overlay - enhanced visual feedback */}
<DragOverlay>
{activeTask ? (
<div className="opacity-95 rotate-2 scale-105 cursor-grabbing">
<div className="drag-overlay-card">
<TaskCard task={activeTask} onClick={() => {}} />
</div>
) : null}
@@ -18,7 +18,8 @@ import {
Import,
Radio,
Github,
Globe
Globe,
Code2
} from 'lucide-react';
import { LinearTaskImportModal } from './LinearTaskImportModal';
import {
@@ -1110,6 +1111,34 @@ export function ProjectSettings({ project, open, onOpenChange }: ProjectSettings
/>
</div>
)}
{/* Dev Mode Toggle */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<div className="flex items-center gap-2">
<Code2 className="h-4 w-4 text-info" />
<Label className="font-normal text-foreground">Framework Dev Mode</Label>
</div>
<p className="text-xs text-muted-foreground pl-6">
Use <code className="px-1 bg-muted rounded">dev/auto-claude/specs/</code> for framework development
</p>
</div>
<Switch
checked={settings.devMode ?? false}
onCheckedChange={(checked) =>
setSettings({ ...settings, devMode: checked })
}
/>
</div>
{settings.devMode && (
<div className="rounded-lg border border-info/30 bg-info/5 p-3 ml-6">
<p className="text-xs text-info">
Dev mode enabled. Tasks will be stored in the gitignored <code className="px-1 bg-info/10 rounded">dev/auto-claude/specs/</code> directory
instead of the project's <code className="px-1 bg-info/10 rounded">.auto-claude/specs/</code> directory.
Use this when developing the Auto Claude framework itself.
</p>
</div>
)}
</section>
<Separator />
@@ -16,12 +16,15 @@ export function SortableTaskCard({ task, onClick }: SortableTaskCardProps) {
setNodeRef,
transform,
transition,
isDragging
isDragging,
isOver
} = useSortable({ id: task.id });
const style = {
transform: CSS.Transform.toString(transform),
transition
transition,
// Prevent z-index stacking issues during drag
zIndex: isDragging ? 50 : undefined
};
return (
@@ -29,8 +32,9 @@ export function SortableTaskCard({ task, onClick }: SortableTaskCardProps) {
ref={setNodeRef}
style={style}
className={cn(
'touch-none',
isDragging && 'opacity-50 scale-[0.98]'
'touch-none transition-all duration-200',
isDragging && 'dragging-placeholder opacity-40 scale-[0.98]',
isOver && !isDragging && 'ring-2 ring-primary/30 ring-offset-2 ring-offset-background rounded-xl'
)}
{...attributes}
{...listeners}
@@ -114,21 +114,27 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
return (
<Card
className={cn(
'card-surface card-interactive cursor-pointer',
isRunning && 'ring-2 ring-primary border-primary'
'card-surface task-card-enhanced cursor-pointer',
isRunning && !isStuck && 'ring-2 ring-primary border-primary task-running-pulse',
isStuck && 'ring-2 ring-warning border-warning task-stuck-pulse'
)}
onClick={onClick}
>
<CardContent className="p-4">
{/* Header */}
<div className="flex items-start justify-between gap-2">
<h3 className="font-medium text-sm text-foreground line-clamp-2">{task.title}</h3>
<div className="flex items-center gap-1.5 shrink-0">
{/* Stuck indicator */}
{/* Header - improved visual hierarchy */}
<div className="flex items-start justify-between gap-3">
<h3
className="font-semibold text-sm text-foreground line-clamp-2 leading-snug flex-1 min-w-0"
title={task.title}
>
{task.title}
</h3>
<div className="flex items-center gap-1.5 shrink-0 flex-wrap justify-end max-w-[140px]">
{/* Stuck indicator - highest priority */}
{isStuck && (
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0 flex items-center gap-1 bg-warning/10 text-warning border-warning/30"
className="text-[10px] px-1.5 py-0.5 flex items-center gap-1 bg-warning/10 text-warning border-warning/30 badge-priority-urgent"
>
<AlertTriangle className="h-2.5 w-2.5" />
Stuck
@@ -139,7 +145,7 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
<Badge
variant="outline"
className={cn(
'text-[10px] px-1.5 py-0 flex items-center gap-1',
'text-[10px] px-1.5 py-0.5 flex items-center gap-1',
EXECUTION_PHASE_BADGE_COLORS[executionPhase]
)}
>
@@ -147,7 +153,10 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
{EXECUTION_PHASE_LABELS[executionPhase]}
</Badge>
)}
<Badge variant={isStuck ? 'warning' : getStatusBadgeVariant(task.status)}>
<Badge
variant={isStuck ? 'warning' : getStatusBadgeVariant(task.status)}
className="text-[10px] px-1.5 py-0.5"
>
{isStuck ? 'Needs Recovery' : getStatusLabel(task.status)}
</Badge>
</div>
@@ -235,24 +244,26 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
<Progress
value={hasActiveExecution ? (task.executionProgress?.overallProgress || 0) : progress}
className="h-1.5"
animated={isRunning || task.status === 'ai_review'}
/>
{/* Chunk indicators - only show when chunks exist */}
{/* Chunk indicators - enhanced with tooltips and animation */}
{task.chunks.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{task.chunks.slice(0, 8).map((chunk) => (
<div className="mt-2.5 flex flex-wrap gap-1.5">
{task.chunks.slice(0, 10).map((chunk) => (
<div
key={chunk.id}
className={cn(
'h-1.5 w-1.5 rounded-full',
CHUNK_STATUS_COLORS[chunk.status]
'h-2 w-2 rounded-full chunk-dot',
CHUNK_STATUS_COLORS[chunk.status],
chunk.status === 'in_progress' && 'chunk-dot-active'
)}
title={`${chunk.title}: ${chunk.status}`}
title={`${chunk.title || chunk.id}: ${chunk.status}`}
/>
))}
{task.chunks.length > 8 && (
<span className="text-xs text-muted-foreground">
+{task.chunks.length - 8}
{task.chunks.length > 10 && (
<span className="text-[10px] text-muted-foreground font-medium ml-0.5">
+{task.chunks.length - 10}
</span>
)}
</div>
@@ -21,7 +21,16 @@ import {
GitBranch,
ListChecks,
Loader2,
RotateCcw
RotateCcw,
Trash2,
GitMerge,
Eye,
FolderX,
Plus,
Minus,
ExternalLink,
Zap,
Info
} from 'lucide-react';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
@@ -30,6 +39,17 @@ import { ScrollArea } from './ui/scroll-area';
import { Separator } from './ui/separator';
import { Textarea } from './ui/textarea';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from './ui/tooltip';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from './ui/alert-dialog';
import { cn, calculateProgress, formatRelativeTime } from '../lib/utils';
import {
TASK_STATUS_LABELS,
@@ -46,8 +66,8 @@ import {
EXECUTION_PHASE_BADGE_COLORS,
EXECUTION_PHASE_COLORS
} from '../../shared/constants';
import { startTask, stopTask, submitReview, checkTaskRunning, recoverStuckTask } from '../stores/task-store';
import type { Task, TaskCategory, ExecutionPhase } from '../../shared/types';
import { startTask, stopTask, submitReview, checkTaskRunning, recoverStuckTask, deleteTask } from '../stores/task-store';
import type { Task, TaskCategory, ExecutionPhase, WorktreeStatus, WorktreeDiff } from '../../shared/types';
// Category icon mapping
const CategoryIcon: Record<TaskCategory, typeof Target> = {
@@ -75,6 +95,18 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) {
const [isStuck, setIsStuck] = useState(false);
const [isRecovering, setIsRecovering] = useState(false);
const [hasCheckedRunning, setHasCheckedRunning] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [deleteError, setDeleteError] = useState<string | null>(null);
// Workspace management state
const [worktreeStatus, setWorktreeStatus] = useState<WorktreeStatus | null>(null);
const [worktreeDiff, setWorktreeDiff] = useState<WorktreeDiff | null>(null);
const [isLoadingWorktree, setIsLoadingWorktree] = useState(false);
const [isMerging, setIsMerging] = useState(false);
const [isDiscarding, setIsDiscarding] = useState(false);
const [showDiscardDialog, setShowDiscardDialog] = useState(false);
const [workspaceError, setWorkspaceError] = useState<string | null>(null);
const [showDiffDialog, setShowDiffDialog] = useState(false);
const logsEndRef = useRef<HTMLDivElement>(null);
const logsContainerRef = useRef<HTMLDivElement>(null);
@@ -118,6 +150,33 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) {
}
}, [activeTab]);
// Load worktree status when task is in human_review
useEffect(() => {
if (needsReview) {
setIsLoadingWorktree(true);
setWorkspaceError(null);
Promise.all([
window.electronAPI.getWorktreeStatus(task.id),
window.electronAPI.getWorktreeDiff(task.id)
]).then(([statusResult, diffResult]) => {
if (statusResult.success && statusResult.data) {
setWorktreeStatus(statusResult.data);
}
if (diffResult.success && diffResult.data) {
setWorktreeDiff(diffResult.data);
}
}).catch((err) => {
console.error('Failed to load worktree info:', err);
}).finally(() => {
setIsLoadingWorktree(false);
});
} else {
setWorktreeStatus(null);
setWorktreeDiff(null);
}
}, [task.id, needsReview]);
const handleStartStop = () => {
if (isRunning && !isStuck) {
stopTask(task.id);
@@ -152,6 +211,46 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) {
setFeedback('');
};
const handleDelete = async () => {
setIsDeleting(true);
setDeleteError(null);
const result = await deleteTask(task.id);
if (result.success) {
setShowDeleteDialog(false);
onClose();
} else {
setDeleteError(result.error || 'Failed to delete task');
}
setIsDeleting(false);
};
const handleMerge = async () => {
setIsMerging(true);
setWorkspaceError(null);
const result = await window.electronAPI.mergeWorktree(task.id);
if (result.success && result.data?.success) {
// Task will be moved to 'done' by the IPC handler
onClose();
} else {
setWorkspaceError(result.data?.message || result.error || 'Failed to merge changes');
}
setIsMerging(false);
};
const handleDiscard = async () => {
setIsDiscarding(true);
setWorkspaceError(null);
const result = await window.electronAPI.discardWorktree(task.id);
if (result.success && result.data?.success) {
// Task will be moved back to 'backlog' by the IPC handler
setShowDiscardDialog(false);
onClose();
} else {
setWorkspaceError(result.data?.message || result.error || 'Failed to discard changes');
}
setIsDiscarding(false);
};
const getChunkStatusIcon = (status: string) => {
switch (status) {
case 'completed':
@@ -166,31 +265,46 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) {
};
return (
<div className="flex h-full w-96 flex-col bg-card border-l border-border">
{/* Header */}
<div className="flex items-start justify-between p-4">
<div className="flex-1 min-w-0 pr-2">
<h2 className="font-semibold text-lg text-foreground truncate">{task.title}</h2>
<div className="mt-1.5 flex items-center gap-2">
<Badge variant="outline" className="text-xs">
{task.specId}
</Badge>
{isStuck ? (
<Badge variant="warning" className="text-xs flex items-center gap-1">
<AlertTriangle className="h-3 w-3" />
Stuck
<TooltipProvider delayDuration={300}>
<div className="flex h-full w-96 flex-col bg-card border-l border-border">
{/* Header - Enhanced with better visual hierarchy */}
<div className="flex items-start justify-between p-4 pb-3">
<div className="flex-1 min-w-0 pr-2">
<Tooltip>
<TooltipTrigger asChild>
<h2 className="font-semibold text-lg text-foreground line-clamp-2 leading-snug cursor-default">
{task.title}
</h2>
</TooltipTrigger>
{task.title.length > 40 && (
<TooltipContent side="bottom" className="max-w-xs">
<p className="text-sm">{task.title}</p>
</TooltipContent>
)}
</Tooltip>
<div className="mt-2 flex items-center gap-2 flex-wrap">
<Badge variant="outline" className="text-xs font-mono">
{task.specId}
</Badge>
) : (
<span className="text-xs text-muted-foreground">
{TASK_STATUS_LABELS[task.status]}
</span>
)}
{isStuck ? (
<Badge variant="warning" className="text-xs flex items-center gap-1 animate-pulse">
<AlertTriangle className="h-3 w-3" />
Stuck
</Badge>
) : (
<Badge
variant={task.status === 'done' ? 'success' : task.status === 'human_review' ? 'purple' : task.status === 'in_progress' ? 'info' : 'secondary'}
className={cn('text-xs', (task.status === 'in_progress' && !isStuck) && 'status-running')}
>
{TASK_STATUS_LABELS[task.status]}
</Badge>
)}
</div>
</div>
<Button variant="ghost" size="icon" className="shrink-0 -mr-1 -mt-1 hover:bg-destructive/10 hover:text-destructive transition-colors" onClick={onClose}>
<X className="h-4 w-4" />
</Button>
</div>
<Button variant="ghost" size="icon" onClick={onClose}>
<X className="h-4 w-4" />
</Button>
</div>
<Separator />
@@ -290,20 +404,41 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) {
{/* Progress */}
<div>
<div className="section-divider mb-3">
<Zap className="h-3 w-3" />
Progress
</div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-foreground">
{hasActiveExecution ? 'Overall Progress' : 'Progress'}
<span className="text-xs text-muted-foreground">
{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'}
</span>
<span className="text-sm text-foreground">
<span className={cn(
'text-sm font-semibold tabular-nums',
task.status === 'done' ? 'text-success' : 'text-foreground'
)}>
{hasActiveExecution
? `${task.executionProgress?.overallProgress || 0}%`
: `${progress}%`}
</span>
</div>
<Progress
value={hasActiveExecution ? (task.executionProgress?.overallProgress || 0) : progress}
className="h-2"
/>
<div className={cn(
'rounded-full',
hasActiveExecution && 'progress-working'
)}>
<Progress
value={hasActiveExecution ? (task.executionProgress?.overallProgress || 0) : progress}
className={cn(
'h-2',
task.status === 'done' && '[&>div]:bg-success',
hasActiveExecution && '[&>div]:bg-info'
)}
animated={isRunning || task.status === 'ai_review'}
/>
</div>
{/* Phase Progress Bar Segments */}
{hasActiveExecution && (
<div className="mt-2 flex gap-0.5 h-1.5 rounded-full overflow-hidden bg-muted/30">
@@ -345,7 +480,12 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) {
{/* Classification Badges */}
{task.metadata && (
<div className="flex flex-wrap gap-1.5">
<div>
<div className="section-divider mb-3">
<Info className="h-3 w-3" />
Classification
</div>
<div className="flex flex-wrap gap-1.5">
{/* Category */}
{task.metadata.category && (
<Badge
@@ -404,14 +544,18 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) {
: task.metadata.sourceType}
</Badge>
)}
</div>
</div>
)}
{/* Description */}
{task.description && (
<div>
<h3 className="text-sm font-medium text-foreground mb-2">Description</h3>
<p className="text-sm text-muted-foreground">{task.description}</p>
<div className="section-divider mb-3">
<FileCode className="h-3 w-3" />
Description
</div>
<p className="text-sm text-muted-foreground leading-relaxed">{task.description}</p>
</div>
)}
@@ -490,9 +634,16 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) {
</h3>
<div className="flex flex-wrap gap-1">
{task.metadata.affectedFiles.map((file, idx) => (
<Badge key={idx} variant="secondary" className="text-xs font-mono">
{file.split('/').pop()}
</Badge>
<Tooltip key={idx}>
<TooltipTrigger asChild>
<Badge variant="secondary" className="text-xs font-mono cursor-help">
{file.split('/').pop()}
</Badge>
</TooltipTrigger>
<TooltipContent side="top" className="font-mono text-xs">
{file}
</TooltipContent>
</Tooltip>
))}
</div>
</div>
@@ -501,52 +652,189 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) {
)}
{/* Timestamps */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Created</span>
<span className="text-foreground">{formatRelativeTime(task.createdAt)}</span>
<div>
<div className="section-divider mb-3">
<Clock className="h-3 w-3" />
Timeline
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Updated</span>
<span className="text-foreground">{formatRelativeTime(task.updatedAt)}</span>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Created</span>
<span className="text-foreground tabular-nums">{formatRelativeTime(task.createdAt)}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Updated</span>
<span className="text-foreground tabular-nums">{formatRelativeTime(task.updatedAt)}</span>
</div>
</div>
</div>
{/* Human Review Section */}
{/* Human Review Section - Enhanced styling */}
{needsReview && (
<div className="rounded-xl border border-purple-500/30 bg-purple-500/10 p-4">
<h3 className="font-medium text-sm text-foreground mb-2 flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-purple-400" />
Review Required
</h3>
<p className="text-sm text-muted-foreground mb-3">
Please review the changes and provide feedback if needed.
</p>
<Textarea
placeholder="Enter feedback for rejection (optional for approval)..."
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
className="mb-3"
rows={3}
/>
<div className="flex gap-2">
<div className="space-y-4">
{/* Section divider */}
<div className="section-divider-gradient" />
{/* Workspace Status */}
{isLoadingWorktree ? (
<div className="rounded-xl border border-border bg-secondary/30 p-4">
<div className="flex items-center gap-2 text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm">Loading workspace info...</span>
</div>
</div>
) : worktreeStatus?.exists ? (
<div className="review-section-highlight">
<h3 className="font-medium text-sm text-foreground mb-3 flex items-center gap-2">
<GitBranch className="h-4 w-4 text-purple-400" />
Build Ready for Review
</h3>
{/* Change Summary */}
<div className="bg-background/50 rounded-lg p-3 mb-3">
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="flex items-center gap-2">
<FileCode className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">Files changed:</span>
<span className="text-foreground font-medium">{worktreeStatus.filesChanged || 0}</span>
</div>
<div className="flex items-center gap-2">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">Commits:</span>
<span className="text-foreground font-medium">{worktreeStatus.commitCount || 0}</span>
</div>
<div className="flex items-center gap-2">
<Plus className="h-4 w-4 text-success" />
<span className="text-muted-foreground">Additions:</span>
<span className="text-success font-medium">+{worktreeStatus.additions || 0}</span>
</div>
<div className="flex items-center gap-2">
<Minus className="h-4 w-4 text-destructive" />
<span className="text-muted-foreground">Deletions:</span>
<span className="text-destructive font-medium">-{worktreeStatus.deletions || 0}</span>
</div>
</div>
{worktreeStatus.branch && (
<div className="mt-2 pt-2 border-t border-border/50 text-xs text-muted-foreground">
Branch: <code className="bg-background px-1 rounded">{worktreeStatus.branch}</code>
{' → '}
<code className="bg-background px-1 rounded">{worktreeStatus.baseBranch || 'main'}</code>
</div>
)}
</div>
{/* Workspace Error */}
{workspaceError && (
<div className="bg-destructive/10 border border-destructive/30 rounded-lg p-3 mb-3">
<p className="text-sm text-destructive">{workspaceError}</p>
</div>
)}
{/* Action Buttons */}
<div className="flex gap-2 mb-3">
<Button
variant="outline"
size="sm"
onClick={() => setShowDiffDialog(true)}
className="flex-1"
>
<Eye className="mr-2 h-4 w-4" />
View Changes
</Button>
{worktreeStatus.worktreePath && (
<Button
variant="outline"
size="sm"
onClick={() => {
// Open folder in system file manager
window.electronAPI.createTerminal({
id: `open-${task.id}`,
cwd: worktreeStatus.worktreePath!,
name: 'Open Folder'
});
}}
className="flex-none"
>
<ExternalLink className="h-4 w-4" />
</Button>
)}
</div>
{/* Primary Actions */}
<div className="flex gap-2">
<Button
variant="success"
onClick={handleMerge}
disabled={isMerging || isDiscarding}
className="flex-1"
>
{isMerging ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Merging...
</>
) : (
<>
<GitMerge className="mr-2 h-4 w-4" />
Merge to Main
</>
)}
</Button>
<Button
variant="outline"
onClick={() => setShowDiscardDialog(true)}
disabled={isMerging || isDiscarding}
className="text-destructive hover:text-destructive hover:bg-destructive/10"
>
<FolderX className="h-4 w-4" />
</Button>
</div>
</div>
) : (
<div className="rounded-xl border border-border bg-secondary/30 p-4">
<h3 className="font-medium text-sm text-foreground mb-2 flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-muted-foreground" />
No Workspace Found
</h3>
<p className="text-sm text-muted-foreground">
No isolated workspace was found for this task. The changes may have been made directly in your project.
</p>
</div>
)}
{/* QA Feedback Section (for requesting changes) */}
<div className="rounded-xl border border-warning/30 bg-warning/10 p-4">
<h3 className="font-medium text-sm text-foreground mb-2 flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-warning" />
Request Changes
</h3>
<p className="text-sm text-muted-foreground mb-3">
Found issues? Describe what needs to be fixed and the AI will continue working on it.
</p>
<Textarea
placeholder="Describe the issues or changes needed..."
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
className="mb-3"
rows={3}
/>
<Button
variant="success"
onClick={handleApprove}
disabled={isSubmitting}
className="flex-1"
>
<CheckCircle2 className="mr-2 h-4 w-4" />
Approve
</Button>
<Button
variant="destructive"
variant="warning"
onClick={handleReject}
disabled={isSubmitting || !feedback.trim()}
className="flex-1"
className="w-full"
>
<XCircle className="mr-2 h-4 w-4" />
Reject
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Submitting...
</>
) : (
<>
<RotateCcw className="mr-2 h-4 w-4" />
Request Changes
</>
)}
</Button>
</div>
</div>
@@ -560,52 +848,91 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) {
<ScrollArea className="h-full">
<div className="p-4 space-y-3">
{task.chunks.length === 0 ? (
<div className="text-center text-sm text-muted-foreground py-8">
No chunks defined yet
<div className="text-center py-12">
<ListChecks className="h-10 w-10 mx-auto mb-3 text-muted-foreground/30" />
<p className="text-sm font-medium text-muted-foreground mb-1">No chunks defined</p>
<p className="text-xs text-muted-foreground/70">
Implementation chunks will appear here after planning
</p>
</div>
) : (
task.chunks.map((chunk, index) => (
<div
key={chunk.id}
className={cn(
'rounded-xl border border-border bg-secondary/30 p-3 transition-all duration-200',
chunk.status === 'in_progress' && 'border-[var(--info)]/50 bg-[var(--info-light)]',
chunk.status === 'completed' && 'border-[var(--success)]/50 bg-[var(--success-light)]',
chunk.status === 'failed' && 'border-[var(--error)]/50 bg-[var(--error-light)]'
)}
>
<div className="flex items-start gap-2">
{getChunkStatusIcon(chunk.status)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
#{index + 1}
</span>
<span className="text-sm font-medium text-foreground truncate">
{chunk.id}
</span>
</div>
<p className="mt-1 text-xs text-muted-foreground line-clamp-2">
{chunk.description}
</p>
{chunk.files && chunk.files.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{chunk.files.map((file) => (
<Badge
key={file}
variant="secondary"
className="text-xs"
>
<FileCode className="mr-1 h-3 w-3" />
{file.split('/').pop()}
</Badge>
))}
<>
{/* Progress summary */}
<div className="flex items-center justify-between text-xs text-muted-foreground pb-2 border-b border-border/50">
<span>{task.chunks.filter(c => c.status === 'completed').length} of {task.chunks.length} completed</span>
<span className="tabular-nums">{progress}%</span>
</div>
{task.chunks.map((chunk, index) => (
<div
key={chunk.id}
className={cn(
'rounded-xl border border-border bg-secondary/30 p-3 transition-all duration-200 hover:bg-secondary/50',
chunk.status === 'in_progress' && 'border-[var(--info)]/50 bg-[var(--info-light)] ring-1 ring-info/20',
chunk.status === 'completed' && 'border-[var(--success)]/50 bg-[var(--success-light)]',
chunk.status === 'failed' && 'border-[var(--error)]/50 bg-[var(--error-light)]'
)}
>
<div className="flex items-start gap-2">
{getChunkStatusIcon(chunk.status)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className={cn(
'text-[10px] font-medium px-1.5 py-0.5 rounded-full',
chunk.status === 'completed' ? 'bg-success/20 text-success' :
chunk.status === 'in_progress' ? 'bg-info/20 text-info' :
chunk.status === 'failed' ? 'bg-destructive/20 text-destructive' :
'bg-muted text-muted-foreground'
)}>
#{index + 1}
</span>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-sm font-medium text-foreground truncate cursor-default">
{chunk.id}
</span>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
<p className="font-mono text-xs">{chunk.id}</p>
</TooltipContent>
</Tooltip>
</div>
)}
<Tooltip>
<TooltipTrigger asChild>
<p className="mt-1 text-xs text-muted-foreground line-clamp-2 cursor-default">
{chunk.description}
</p>
</TooltipTrigger>
{chunk.description && chunk.description.length > 80 && (
<TooltipContent side="bottom" className="max-w-sm">
<p className="text-xs">{chunk.description}</p>
</TooltipContent>
)}
</Tooltip>
{chunk.files && chunk.files.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{chunk.files.map((file) => (
<Tooltip key={file}>
<TooltipTrigger asChild>
<Badge
variant="secondary"
className="text-xs font-mono cursor-help"
>
<FileCode className="mr-1 h-3 w-3" />
{file.split('/').pop()}
</Badge>
</TooltipTrigger>
<TooltipContent side="top" className="font-mono text-xs">
{file}
</TooltipContent>
</Tooltip>
))}
</div>
)}
</div>
</div>
</div>
</div>
))
))}
</>
)}
</div>
</ScrollArea>
@@ -679,12 +1006,191 @@ export function TaskDetailPanel({ task, onClose }: TaskDetailPanelProps) {
</Button>
)}
{task.status === 'done' && (
<div className="text-center text-sm text-[var(--success)]">
<CheckCircle2 className="mx-auto mb-1 h-6 w-6" />
Task completed successfully
<div className="completion-state text-sm">
<CheckCircle2 className="h-5 w-5" />
<span className="font-medium">Task completed successfully</span>
</div>
)}
{/* Delete Button - always visible but disabled when running */}
<Button
variant="ghost"
size="sm"
className="w-full mt-3 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
onClick={() => setShowDeleteDialog(true)}
disabled={isRunning && !isStuck}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete Task
</Button>
</div>
</div>
{/* Delete Confirmation Dialog */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-destructive" />
Delete Task
</AlertDialogTitle>
<AlertDialogDescription className="space-y-3">
<p>
Are you sure you want to delete <strong className="text-foreground">"{task.title}"</strong>?
</p>
<p className="text-destructive">
This action cannot be undone. All task files, including the spec, implementation plan, and any generated code will be permanently deleted from the project.
</p>
{deleteError && (
<p className="text-destructive bg-destructive/10 px-3 py-2 rounded-lg text-sm">
{deleteError}
</p>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.preventDefault();
handleDelete();
}}
disabled={isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deleting...
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
Delete Permanently
</>
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Discard Confirmation Dialog */}
<AlertDialog open={showDiscardDialog} onOpenChange={setShowDiscardDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<FolderX className="h-5 w-5 text-destructive" />
Discard Build
</AlertDialogTitle>
<AlertDialogDescription className="space-y-3">
<p>
Are you sure you want to discard all changes for <strong className="text-foreground">"{task.title}"</strong>?
</p>
<p className="text-destructive">
This will permanently delete the isolated workspace and all uncommitted changes.
The task will be moved back to Planning status.
</p>
{worktreeStatus?.exists && (
<div className="bg-muted/50 rounded-lg p-3 text-sm">
<div className="flex justify-between mb-1">
<span className="text-muted-foreground">Files changed:</span>
<span>{worktreeStatus.filesChanged || 0}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Lines:</span>
<span className="text-success">+{worktreeStatus.additions || 0}</span>
<span className="text-destructive">-{worktreeStatus.deletions || 0}</span>
</div>
</div>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDiscarding}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.preventDefault();
handleDiscard();
}}
disabled={isDiscarding}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDiscarding ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Discarding...
</>
) : (
<>
<FolderX className="mr-2 h-4 w-4" />
Discard Build
</>
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Diff View Dialog */}
<AlertDialog open={showDiffDialog} onOpenChange={setShowDiffDialog}>
<AlertDialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<Eye className="h-5 w-5 text-purple-400" />
Changed Files
</AlertDialogTitle>
<AlertDialogDescription>
{worktreeDiff?.summary || 'No changes found'}
</AlertDialogDescription>
</AlertDialogHeader>
<div className="flex-1 overflow-auto min-h-0 -mx-6 px-6">
{worktreeDiff?.files && worktreeDiff.files.length > 0 ? (
<div className="space-y-2">
{worktreeDiff.files.map((file, idx) => (
<div
key={idx}
className="flex items-center justify-between p-2 rounded-lg bg-secondary/30 hover:bg-secondary/50 transition-colors"
>
<div className="flex items-center gap-2 min-w-0 flex-1">
<FileCode className={cn(
'h-4 w-4 shrink-0',
file.status === 'added' && 'text-success',
file.status === 'deleted' && 'text-destructive',
file.status === 'modified' && 'text-info',
file.status === 'renamed' && 'text-warning'
)} />
<span className="text-sm font-mono truncate">{file.path}</span>
</div>
<div className="flex items-center gap-2 shrink-0 ml-2">
<Badge
variant="secondary"
className={cn(
'text-xs',
file.status === 'added' && 'bg-success/10 text-success',
file.status === 'deleted' && 'bg-destructive/10 text-destructive',
file.status === 'modified' && 'bg-info/10 text-info',
file.status === 'renamed' && 'bg-warning/10 text-warning'
)}
>
{file.status}
</Badge>
<span className="text-xs text-success">+{file.additions}</span>
<span className="text-xs text-destructive">-{file.deletions}</span>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
No changed files found
</div>
)}
</div>
<AlertDialogFooter className="mt-4">
<AlertDialogCancel>Close</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</TooltipProvider>
);
}
@@ -2,10 +2,14 @@ import * as React from 'react';
import * as ProgressPrimitive from '@radix-ui/react-progress';
import { cn } from '../../lib/utils';
interface ProgressProps extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {
animated?: boolean;
}
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
ProgressProps
>(({ className, value, animated, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
@@ -15,7 +19,10 @@ const Progress = React.forwardRef<
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all duration-300 ease-out"
className={cn(
'h-full w-full flex-1 bg-primary transition-all duration-300 ease-out',
animated && 'progress-working'
)}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
@@ -4,6 +4,7 @@ import type {
InsightsChatMessage,
InsightsChatStatus,
InsightsStreamChunk,
InsightsToolUsage,
TaskMetadata,
Task
} from '../../shared/types';
@@ -20,6 +21,7 @@ interface InsightsState {
pendingMessage: string;
streamingContent: string; // Accumulates streaming response
currentTool: ToolUsage | null; // Currently executing tool
toolsUsed: InsightsToolUsage[]; // Tools used during current response
// Actions
setSession: (session: InsightsSession | null) => void;
@@ -30,6 +32,8 @@ interface InsightsState {
appendStreamingContent: (content: string) => void;
clearStreamingContent: () => void;
setCurrentTool: (tool: ToolUsage | null) => void;
addToolUsage: (tool: ToolUsage) => void;
clearToolsUsed: () => void;
finalizeStreamingMessage: (suggestedTask?: InsightsChatMessage['suggestedTask']) => void;
clearSession: () => void;
}
@@ -46,6 +50,7 @@ export const useInsightsStore = create<InsightsState>((set, get) => ({
pendingMessage: '',
streamingContent: '',
currentTool: null,
toolsUsed: [],
// Actions
setSession: (session) => set({ session }),
@@ -108,22 +113,42 @@ export const useInsightsStore = create<InsightsState>((set, get) => ({
setCurrentTool: (tool) => set({ currentTool: tool }),
addToolUsage: (tool) =>
set((state) => ({
toolsUsed: [
...state.toolsUsed,
{
name: tool.name,
input: tool.input,
timestamp: new Date()
}
]
})),
clearToolsUsed: () => set({ toolsUsed: [] }),
finalizeStreamingMessage: (suggestedTask) =>
set((state) => {
const content = state.streamingContent;
if (!content && !suggestedTask) return { streamingContent: '' };
const toolsUsed = state.toolsUsed.length > 0 ? [...state.toolsUsed] : undefined;
if (!content && !suggestedTask && !toolsUsed) {
return { streamingContent: '', toolsUsed: [] };
}
const newMessage: InsightsChatMessage = {
id: `msg-${Date.now()}`,
role: 'assistant',
content,
timestamp: new Date(),
suggestedTask
suggestedTask,
toolsUsed
};
if (!state.session) {
return {
streamingContent: '',
toolsUsed: [],
session: {
id: `session-${Date.now()}`,
projectId: '',
@@ -136,6 +161,7 @@ export const useInsightsStore = create<InsightsState>((set, get) => ({
return {
streamingContent: '',
toolsUsed: [],
session: {
...state.session,
messages: [...state.session.messages, newMessage],
@@ -150,7 +176,8 @@ export const useInsightsStore = create<InsightsState>((set, get) => ({
status: initialStatus,
pendingMessage: '',
streamingContent: '',
currentTool: null
currentTool: null,
toolsUsed: []
})
}));
@@ -180,6 +207,7 @@ export function sendMessage(projectId: string, message: string): void {
// Clear pending and set status
store.setPendingMessage('');
store.clearStreamingContent();
store.clearToolsUsed(); // Clear tools from previous response
store.setStatus({
phase: 'thinking',
message: 'Processing your message...'
@@ -239,6 +267,11 @@ export function setupInsightsListeners(): () => void {
name: chunk.tool.name,
input: chunk.tool.input
});
// Record this tool usage for history
store().addToolUsage({
name: chunk.tool.name,
input: chunk.tool.input
});
store().setStatus({
phase: 'streaming',
message: `Using ${chunk.tool.name}...`
@@ -72,7 +72,8 @@ export const useTaskStore = create<TaskState>((set, get) => ({
);
// Determine status based on chunks
const allCompleted = chunks.every((c) => c.status === 'completed');
// Note: chunks.every() returns true for empty arrays, so we must check length > 0
const allCompleted = chunks.length > 0 && chunks.every((c) => c.status === 'completed');
const anyInProgress = chunks.some((c) => c.status === 'in_progress');
const anyFailed = chunks.some((c) => c.status === 'failed');
@@ -299,3 +300,37 @@ export async function recoverStuckTask(
};
}
}
/**
* Delete a task and its spec directory
*/
export async function deleteTask(
taskId: string
): Promise<{ success: boolean; error?: string }> {
const store = useTaskStore.getState();
try {
const result = await window.electronAPI.deleteTask(taskId);
if (result.success) {
// Remove from local state
store.setTasks(store.tasks.filter(t => t.id !== taskId && t.specId !== taskId));
// Clear selection if this task was selected
if (store.selectedTaskId === taskId) {
store.selectTask(null);
}
return { success: true };
}
return {
success: false,
error: result.error || 'Failed to delete task'
};
} catch (error) {
console.error('Error deleting task:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
@@ -370,6 +370,55 @@ body {
border-top-color: var(--success);
}
/* Progress bar working animation - subtle glow sweep */
@keyframes progress-glow-sweep {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.progress-working {
position: relative;
overflow: hidden;
}
.progress-working::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(214, 216, 118, 0.4) 25%,
rgba(214, 216, 118, 0.7) 50%,
rgba(214, 216, 118, 0.4) 75%,
transparent 100%
);
background-size: 200% 100%;
animation: progress-glow-sweep 2s ease-in-out infinite;
border-radius: inherit;
pointer-events: none;
}
/* Light mode variant */
:root .progress-working::after {
background: linear-gradient(
90deg,
transparent 0%,
rgba(165, 166, 106, 0.3) 25%,
rgba(165, 166, 106, 0.6) 50%,
rgba(165, 166, 106, 0.3) 75%,
transparent 100%
);
background-size: 200% 100%;
}
/* Animation utilities */
.transition-default {
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
@@ -402,3 +451,285 @@ body {
transition-duration: 0.01ms !important;
}
}
/* ============================================
Enhanced Task Card Styling
============================================ */
/* Task card enhanced hover effect */
.task-card-enhanced {
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.task-card-enhanced:hover {
border-color: rgba(214, 216, 118, 0.4);
box-shadow: 0 0 0 1px rgba(214, 216, 118, 0.1), 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.dark .task-card-enhanced:hover {
box-shadow: 0 0 0 1px rgba(214, 216, 118, 0.15), 0 4px 20px rgba(0, 0, 0, 0.4);
}
/* Running task pulse animation */
.task-running-pulse {
animation: task-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes task-pulse {
0%, 100% {
border-color: var(--primary);
box-shadow: 0 0 0 0 rgba(214, 216, 118, 0.4);
}
50% {
border-color: rgba(214, 216, 118, 0.8);
box-shadow: 0 0 0 4px rgba(214, 216, 118, 0.1);
}
}
/* Stuck task warning pulse */
.task-stuck-pulse {
animation: stuck-pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes stuck-pulse {
0%, 100% {
border-color: var(--warning);
box-shadow: 0 0 0 0 rgba(210, 215, 20, 0.3);
}
50% {
border-color: rgba(210, 215, 20, 0.8);
box-shadow: 0 0 8px rgba(210, 215, 20, 0.2);
}
}
/* Progress bar animated shimmer for active state */
.progress-animated-fill {
background: linear-gradient(
90deg,
var(--primary) 0%,
rgba(214, 216, 118, 0.7) 50%,
var(--primary) 100%
);
background-size: 200% 100%;
animation: progress-shimmer 2s linear infinite;
}
@keyframes progress-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* Chunk status dot styling */
.chunk-dot {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.chunk-dot:hover {
transform: scale(1.5);
}
.chunk-dot-active {
animation: chunk-dot-pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes chunk-dot-pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.2);
}
}
/* Empty column drop zone state */
.empty-column-dropzone {
border: 2px dashed var(--border);
border-radius: var(--radius-lg);
padding: 1.5rem;
text-align: center;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.empty-column-dropzone:hover,
.empty-column-dropzone.active {
border-color: rgba(214, 216, 118, 0.4);
background-color: rgba(214, 216, 118, 0.05);
}
/* Drag overlay card styling */
.drag-overlay-card {
opacity: 0.95;
transform: rotate(2deg) scale(1.02);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
cursor: grabbing;
}
.dark .drag-overlay-card {
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);
}
/* Dragging source placeholder effect */
.dragging-placeholder {
opacity: 0.4;
transform: scale(0.98);
border-style: dashed;
}
/* Drop zone highlight when dragging over */
.drop-zone-highlight {
border-color: var(--primary) !important;
background-color: rgba(214, 216, 118, 0.08) !important;
}
/* Column count badge styling */
.column-count-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.5rem;
height: 1.5rem;
padding: 0 0.375rem;
border-radius: var(--radius-full);
background: var(--secondary);
font-size: 0.75rem;
font-weight: 600;
color: var(--muted-foreground);
transition: all 0.2s ease;
}
/* Phase progress indicator bar */
.phase-indicator {
display: flex;
gap: 2px;
height: 4px;
border-radius: var(--radius-full);
overflow: hidden;
background: rgba(255, 255, 255, 0.05);
}
.phase-indicator-segment {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.phase-indicator-segment.active {
filter: brightness(1.2);
}
/* Badge priority ordering */
.badge-priority-urgent {
order: -3;
}
.badge-priority-high {
order: -2;
}
.badge-priority-medium {
order: -1;
}
/* Review section styling */
.review-section-highlight {
border: 1px solid rgba(168, 85, 247, 0.3);
background: linear-gradient(
135deg,
rgba(168, 85, 247, 0.08) 0%,
rgba(168, 85, 247, 0.02) 100%
);
border-radius: var(--radius-xl);
padding: 1rem;
}
/* Success completion state */
.completion-state {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
color: var(--success);
padding: 1rem;
border-radius: var(--radius-lg);
background: rgba(78, 190, 150, 0.1);
}
/* Section divider with gradient */
.section-divider-gradient {
height: 1px;
background: linear-gradient(
90deg,
transparent 0%,
var(--border) 20%,
var(--border) 80%,
transparent 100%
);
margin: 1rem 0;
}
/* Section divider with icon label */
.section-divider {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted-foreground);
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
:root .section-divider {
border-bottom-color: rgba(0, 0, 0, 0.08);
}
/* Status badge running animation */
.status-running {
animation: status-running-pulse 2s ease-in-out infinite;
}
@keyframes status-running-pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
/* Metadata list styling */
.metadata-list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.metadata-list-item:last-child {
border-bottom: none;
}
/* Tooltip enhanced styling */
.tooltip-enhanced {
max-width: 250px;
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
line-height: 1.4;
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.dark .tooltip-enhanced {
background: #1A1A1F;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
+28 -5
View File
@@ -112,7 +112,8 @@ export const DEFAULT_PROJECT_SETTINGS = {
onTaskFailed: true,
onReviewNeeded: true,
sound: false
}
},
devMode: false
};
// IPC Channel names
@@ -129,6 +130,7 @@ export const IPC_CHANNELS = {
// Task operations
TASK_LIST: 'task:list',
TASK_CREATE: 'task:create',
TASK_DELETE: 'task:delete',
TASK_START: 'task:start',
TASK_STOP: 'task:stop',
TASK_REVIEW: 'task:review',
@@ -136,6 +138,12 @@ export const IPC_CHANNELS = {
TASK_RECOVER_STUCK: 'task:recoverStuck',
TASK_CHECK_RUNNING: 'task:checkRunning',
// Workspace management (for human review)
TASK_WORKTREE_STATUS: 'task:worktreeStatus',
TASK_WORKTREE_DIFF: 'task:worktreeDiff',
TASK_WORKTREE_MERGE: 'task:worktreeMerge',
TASK_WORKTREE_DISCARD: 'task:worktreeDiscard',
// Task events (main -> renderer)
TASK_PROGRESS: 'task:progress',
TASK_ERROR: 'task:error',
@@ -262,10 +270,11 @@ export const IPC_CHANNELS = {
} as const;
// File paths relative to project
// IMPORTANT: All paths use .auto-claude/ (the installed instance), NOT auto-claude/ (source code)
export const AUTO_BUILD_PATHS = {
SPECS_DIR: 'auto-claude/specs',
ROADMAP_DIR: 'auto-claude/roadmap',
IDEATION_DIR: 'auto-claude/ideation',
SPECS_DIR: '.auto-claude/specs',
ROADMAP_DIR: '.auto-claude/roadmap',
IDEATION_DIR: '.auto-claude/ideation',
IMPLEMENTATION_PLAN: 'implementation_plan.json',
SPEC_FILE: 'spec.md',
QA_REPORT: 'qa_report.md',
@@ -276,10 +285,24 @@ export const AUTO_BUILD_PATHS = {
ROADMAP_DISCOVERY: 'roadmap_discovery.json',
IDEATION_FILE: 'ideation.json',
IDEATION_CONTEXT: 'ideation_context.json',
PROJECT_INDEX: 'auto-claude/project_index.json',
PROJECT_INDEX: '.auto-claude/project_index.json',
GRAPHITI_STATE: '.graphiti_state.json'
} as const;
/**
* Get the specs directory path.
*
* Note: devMode parameter is kept for API compatibility but currently
* all specs go to .auto-claude/specs/ (the installed instance).
* The auto-claude/ folder is source code and should not contain specs.
*/
export function getSpecsDir(autoBuildPath: string | undefined, _devMode: boolean): string {
// Always use .auto-claude/specs - this is the installed instance
// autoBuildPath should always be '.auto-claude' or undefined (not initialized)
const basePath = autoBuildPath || '.auto-claude';
return `${basePath}/specs`;
}
// Roadmap feature priority colors
export const ROADMAP_PRIORITY_COLORS: Record<string, string> = {
must: 'bg-destructive/10 text-destructive border-destructive/30',
+53
View File
@@ -21,6 +21,8 @@ export interface ProjectSettings {
linearSync: boolean;
linearTeamId?: string;
notifications: NotificationSettings;
/** Dev mode: use dev/auto-claude/specs/ for framework development */
devMode: boolean;
}
export interface NotificationSettings {
@@ -186,6 +188,41 @@ export interface TaskStartOptions {
model?: string;
}
// Workspace management types (for human review)
export interface WorktreeStatus {
exists: boolean;
worktreePath?: string;
branch?: string;
baseBranch?: string;
commitCount?: number;
filesChanged?: number;
additions?: number;
deletions?: number;
}
export interface WorktreeDiff {
files: WorktreeDiffFile[];
summary: string;
}
export interface WorktreeDiffFile {
path: string;
status: 'added' | 'modified' | 'deleted' | 'renamed';
additions: number;
deletions: number;
}
export interface WorktreeMergeResult {
success: boolean;
message: string;
conflictFiles?: string[];
}
export interface WorktreeDiscardResult {
success: boolean;
message: string;
}
// Stuck task recovery types
export interface StuckTaskInfo {
taskId: string;
@@ -891,6 +928,13 @@ export interface ExistingChangelog {
export type InsightsChatRole = 'user' | 'assistant';
// Tool usage record for showing what tools the AI used
export interface InsightsToolUsage {
name: string;
input?: string;
timestamp: Date;
}
export interface InsightsChatMessage {
id: string;
role: InsightsChatRole;
@@ -902,6 +946,8 @@ export interface InsightsChatMessage {
description: string;
metadata?: TaskMetadata;
};
// Tools used during this response (assistant messages only)
toolsUsed?: InsightsToolUsage[];
}
export interface InsightsSession {
@@ -971,6 +1017,7 @@ export interface ElectronAPI {
// Task operations
getTasks: (projectId: string) => Promise<IPCResult<Task[]>>;
createTask: (projectId: string, title: string, description: string, metadata?: TaskMetadata) => Promise<IPCResult<Task>>;
deleteTask: (taskId: string) => Promise<IPCResult>;
startTask: (taskId: string, options?: TaskStartOptions) => void;
stopTask: (taskId: string) => void;
submitReview: (taskId: string, approved: boolean, feedback?: string) => Promise<IPCResult>;
@@ -978,6 +1025,12 @@ export interface ElectronAPI {
recoverStuckTask: (taskId: string, targetStatus?: TaskStatus) => Promise<IPCResult<TaskRecoveryResult>>;
checkTaskRunning: (taskId: string) => Promise<IPCResult<boolean>>;
// Workspace management (for human review)
getWorktreeStatus: (taskId: string) => Promise<IPCResult<WorktreeStatus>>;
getWorktreeDiff: (taskId: string) => Promise<IPCResult<WorktreeDiff>>;
mergeWorktree: (taskId: string) => Promise<IPCResult<WorktreeMergeResult>>;
discardWorktree: (taskId: string) => Promise<IPCResult<WorktreeDiscardResult>>;
// Event listeners
onTaskProgress: (callback: (taskId: string, plan: ImplementationPlan) => void) => () => void;
onTaskError: (callback: (taskId: string, error: string) => void) => () => void;
+7
View File
@@ -0,0 +1,7 @@
{
"version": "1.0.0",
"sourceHash": "901d7898b1e236b8",
"sourcePath": "/Users/andremikalsen/Documents/Coding/autonomous-coding/auto-claude",
"initializedAt": "2025-12-12T09:20:22.555Z",
"updatedAt": "2025-12-12T09:20:22.555Z"
}
+2 -2
View File
@@ -77,8 +77,8 @@ class ContextBuilder:
self.project_index = project_index or self._load_project_index()
def _load_project_index(self) -> dict:
"""Load project index from file or create new one."""
index_file = self.project_dir / "auto-claude" / "project_index.json"
"""Load project index from file or create new one (.auto-claude is the installed instance)."""
index_file = self.project_dir / ".auto-claude" / "project_index.json"
if index_file.exists():
with open(index_file) as f:
return json.load(f)
@@ -0,0 +1,140 @@
{
"code_quality": [
{
"id": "cq-001",
"type": "code_quality",
"title": "Split monolithic spec_runner.py into focused modules",
"description": "The spec_runner.py file has grown to 1802 lines and handles multiple concerns: complexity assessment, phase orchestration, spec creation, and CLI interface. This violates single responsibility principle and makes the code difficult to navigate and maintain.",
"rationale": "Large files increase cognitive load, make code reviews harder, and often lead to merge conflicts. The file mixes high-level orchestration logic with detailed implementation, making it hard to understand the overall flow. Splitting it would improve testability and maintainability.",
"category": "large_files",
"severity": "major",
"affectedFiles": ["auto-claude/spec_runner.py"],
"currentState": "Single 1802-line file handling complexity assessment, phase orchestration, spec creation, validation, and CLI interface",
"proposedChange": "Split into: spec_runner.py (main CLI), complexity_analyzer.py (assessment logic), phase_orchestrator.py (pipeline management), and spec_creator.py (creation logic)",
"codeExample": "# Current structure (all in one file):\nclass ComplexityAnalyzer: ...\nclass SpecOrchestrator: ...\ndef main(): ...\n\n# Proposed structure:\n# spec_runner.py - CLI entry point\n# complexity/analyzer.py - ComplexityAnalyzer class\n# orchestration/phases.py - Phase management\n# creation/spec_creator.py - Spec creation logic",
"bestPractice": "Single Responsibility Principle - each module should have one well-defined purpose",
"metrics": {
"lineCount": 1802,
"complexity": null,
"duplicateLines": null,
"testCoverage": null
},
"estimatedEffort": "medium",
"breakingChange": false,
"prerequisites": ["Ensure comprehensive test coverage before refactoring", "Document current module interactions"]
},
{
"id": "cq-002",
"type": "code_quality",
"title": "Extract duplicate environment and path setup code",
"description": "Identical environment setup and path manipulation code is duplicated across 7 runner files (spec_runner.py, run.py, ideation_runner.py, roadmap_runner.py, etc.). Each file has the same 10-15 lines for .env loading and sys.path.insert operations.",
"rationale": "Code duplication leads to maintenance overhead and inconsistency. When environment setup logic needs to change, it must be updated in multiple places, increasing the risk of bugs and inconsistencies.",
"category": "duplication",
"severity": "minor",
"affectedFiles": [
"auto-claude/spec_runner.py",
"auto-claude/run.py",
"auto-claude/ideation_runner.py",
"auto-claude/roadmap_runner.py",
"auto-claude/insights_runner.py",
"auto-claude/test_graphiti_memory.py",
"auto-claude/statusline.py"
],
"currentState": "7 files each containing identical 10-15 lines for environment setup: sys.path.insert, load_dotenv with dev/auto-claude fallback",
"proposedChange": "Create auto-claude/bootstrap.py module with setup_environment() function that handles path setup and .env loading, then import and call it in each runner",
"codeExample": "# Current (repeated 7 times):\nsys.path.insert(0, str(Path(__file__).parent))\nfrom dotenv import load_dotenv\nenv_file = Path(__file__).parent / \".env\"\ndev_env_file = Path(__file__).parent.parent / \"dev\" / \"auto-claude\" / \".env\"\nif env_file.exists():\n load_dotenv(env_file)\nelif dev_env_file.exists():\n load_dotenv(dev_env_file)\n\n# Proposed:\n# bootstrap.py\ndef setup_environment() -> None: ...\n\n# In each runner:\nfrom bootstrap import setup_environment\nsetup_environment()",
"bestPractice": "DRY (Don't Repeat Yourself) - extract common initialization logic into reusable utilities",
"metrics": {
"lineCount": null,
"complexity": null,
"duplicateLines": 105,
"testCoverage": null
},
"estimatedEffort": "small",
"breakingChange": false,
"prerequisites": null
},
{
"id": "cq-003",
"type": "code_quality",
"title": "Consolidate project_analyzer.py large file with clear module boundaries",
"description": "The project_analyzer.py file at 1577 lines contains multiple distinct responsibilities: command registries, technology detection, security profiling, and file analysis. The file has grown organically and would benefit from being split into focused modules.",
"rationale": "While this file is large, it's well-structured with clear sections. However, the mixing of static data (command registries), detection logic, and security profiling makes it harder to maintain and test individual components.",
"category": "large_files",
"severity": "minor",
"affectedFiles": ["auto-claude/project_analyzer.py"],
"currentState": "1577-line file containing command registries, technology detection, security profiling, and caching logic all in one module",
"proposedChange": "Split into: security/command_registry.py (command mappings), detection/technology_detector.py (stack detection), security/profile_builder.py (security profile creation), and project_analyzer.py (main interface)",
"codeExample": "# Current structure (all in one file):\nBASE_COMMANDS = { ... } # ~200 lines\nLANGUAGE_COMMANDS = { ... } # ~300 lines\nclass ProjectAnalyzer: ... # ~800 lines\n\n# Proposed structure:\n# security/commands.py - Command registries\n# detection/stack.py - Technology detection\n# security/profile.py - Profile building\n# project_analyzer.py - Main coordinator class",
"bestPractice": "Separation of concerns - separate data definitions, business logic, and coordination",
"metrics": {
"lineCount": 1577,
"complexity": null,
"duplicateLines": null,
"testCoverage": null
},
"estimatedEffort": "medium",
"breakingChange": false,
"prerequisites": ["Maintain backward compatibility for existing imports", "Ensure comprehensive test coverage"]
},
{
"id": "cq-004",
"type": "code_quality",
"title": "Add missing Python linting and formatting configuration",
"description": "The project lacks centralized code style configuration (no .flake8, .pylintrc, pyproject.toml with tool sections, or black configuration). Print statements are used extensively (703 occurrences) instead of proper logging, and there's no apparent consistent formatting standard.",
"rationale": "Without automated linting and formatting, code quality can drift over time, leading to inconsistent style, potential bugs, and reduced readability. Print statements make debugging harder and pollute output.",
"category": "linting",
"severity": "minor",
"affectedFiles": ["multiple files across the project"],
"currentState": "No centralized linting configuration, 703 print statements across 31 files, no apparent formatting standards",
"proposedChange": "Add pyproject.toml with black, flake8, and mypy configuration. Create a logging utility to replace raw print statements with proper log levels. Add pre-commit hooks for automated formatting.",
"codeExample": "# Add to pyproject.toml:\n[tool.black]\nline-length = 100\ntarget-version = ['py39']\n\n[tool.flake8]\nmax-line-length = 100\nignore = ['E203', 'W503']\n\n# Replace print statements:\n# Before: print(f\"Processing chunk {chunk_id}\")\n# After: logger.info(f\"Processing chunk {chunk_id}\")",
"bestPractice": "Consistent code style and proper logging improve maintainability and debugging capability",
"metrics": {
"lineCount": null,
"complexity": null,
"duplicateLines": null,
"testCoverage": null
},
"estimatedEffort": "small",
"breakingChange": false,
"prerequisites": ["Decide on line length and style preferences", "Plan migration strategy for print statements"]
},
{
"id": "cq-005",
"type": "code_quality",
"title": "Improve type safety with stricter type annotations",
"description": "While the codebase has good type annotation coverage (422 functions with return type annotations), there are opportunities to improve type safety by adding more precise types for complex data structures like implementation plans, chunks, and configuration objects that are often passed around as generic dictionaries.",
"rationale": "Type safety helps catch bugs at development time and improves IDE support. The codebase uses many dictionary-based data structures that could benefit from TypedDict or dataclass definitions for better type checking and documentation.",
"category": "types",
"severity": "suggestion",
"affectedFiles": [
"auto-claude/implementation_plan.py",
"auto-claude/agent.py",
"auto-claude/planner.py",
"auto-claude/progress.py"
],
"currentState": "Good type annotation coverage but heavy use of generic dict types for structured data like chunks, specs, and configuration",
"proposedChange": "Define TypedDict classes for chunk data, spec configuration, and other frequently-used data structures. Consider converting some dictionary-heavy code to use dataclasses.",
"codeExample": "# Current:\ndef process_chunk(chunk: dict) -> dict:\n chunk_id = chunk[\"id\"]\n description = chunk[\"description\"]\n ...\n\n# Proposed:\nfrom typing import TypedDict\n\nclass ChunkData(TypedDict):\n id: str\n description: str\n status: ChunkStatus\n dependencies: List[str]\n\ndef process_chunk(chunk: ChunkData) -> ChunkResult:\n ...",
"bestPractice": "Strong typing improves code reliability and developer experience through better IDE support and compile-time error detection",
"metrics": {
"lineCount": null,
"complexity": null,
"duplicateLines": null,
"testCoverage": null
},
"estimatedEffort": "medium",
"breakingChange": false,
"prerequisites": ["Audit existing dictionary usage patterns", "Plan gradual migration strategy"]
}
],
"metadata": {
"filesAnalyzed": 35,
"largeFilesFound": 3,
"duplicateBlocksFound": 2,
"lintingConfigured": false,
"testsPresent": true,
"generatedAt": "2025-12-12T10:38:18Z"
}
}
+178
View File
@@ -0,0 +1,178 @@
{
"id": "ideation-20251212-104105",
"project_id": "/Users/andremikalsen/Documents/Coding/autonomous-coding",
"config": {
"enabled_types": [
"code_quality"
],
"include_roadmap_context": true,
"include_kanban_context": true,
"max_ideas_per_type": 5
},
"ideas": [
{
"id": "cq-001",
"type": "code_quality",
"title": "Split monolithic spec_runner.py into focused modules",
"description": "The spec_runner.py file has grown to 1802 lines and handles multiple concerns: complexity assessment, phase orchestration, spec creation, and CLI interface. This violates single responsibility principle and makes the code difficult to navigate and maintain.",
"rationale": "Large files increase cognitive load, make code reviews harder, and often lead to merge conflicts. The file mixes high-level orchestration logic with detailed implementation, making it hard to understand the overall flow. Splitting it would improve testability and maintainability.",
"category": "large_files",
"severity": "major",
"affectedFiles": [
"auto-claude/spec_runner.py"
],
"currentState": "Single 1802-line file handling complexity assessment, phase orchestration, spec creation, validation, and CLI interface",
"proposedChange": "Split into: spec_runner.py (main CLI), complexity_analyzer.py (assessment logic), phase_orchestrator.py (pipeline management), and spec_creator.py (creation logic)",
"codeExample": "# Current structure (all in one file):\nclass ComplexityAnalyzer: ...\nclass SpecOrchestrator: ...\ndef main(): ...\n\n# Proposed structure:\n# spec_runner.py - CLI entry point\n# complexity/analyzer.py - ComplexityAnalyzer class\n# orchestration/phases.py - Phase management\n# creation/spec_creator.py - Spec creation logic",
"bestPractice": "Single Responsibility Principle - each module should have one well-defined purpose",
"metrics": {
"lineCount": 1802,
"complexity": null,
"duplicateLines": null,
"testCoverage": null
},
"estimatedEffort": "medium",
"breakingChange": false,
"prerequisites": [
"Ensure comprehensive test coverage before refactoring",
"Document current module interactions"
],
"status": "dismissed"
},
{
"id": "cq-002",
"type": "code_quality",
"title": "Extract duplicate environment and path setup code",
"description": "Identical environment setup and path manipulation code is duplicated across 7 runner files (spec_runner.py, run.py, ideation_runner.py, roadmap_runner.py, etc.). Each file has the same 10-15 lines for .env loading and sys.path.insert operations.",
"rationale": "Code duplication leads to maintenance overhead and inconsistency. When environment setup logic needs to change, it must be updated in multiple places, increasing the risk of bugs and inconsistencies.",
"category": "duplication",
"severity": "minor",
"affectedFiles": [
"auto-claude/spec_runner.py",
"auto-claude/run.py",
"auto-claude/ideation_runner.py",
"auto-claude/roadmap_runner.py",
"auto-claude/insights_runner.py",
"auto-claude/test_graphiti_memory.py",
"auto-claude/statusline.py"
],
"currentState": "7 files each containing identical 10-15 lines for environment setup: sys.path.insert, load_dotenv with dev/auto-claude fallback",
"proposedChange": "Create auto-claude/bootstrap.py module with setup_environment() function that handles path setup and .env loading, then import and call it in each runner",
"codeExample": "# Current (repeated 7 times):\nsys.path.insert(0, str(Path(__file__).parent))\nfrom dotenv import load_dotenv\nenv_file = Path(__file__).parent / \".env\"\ndev_env_file = Path(__file__).parent.parent / \"dev\" / \"auto-claude\" / \".env\"\nif env_file.exists():\n load_dotenv(env_file)\nelif dev_env_file.exists():\n load_dotenv(dev_env_file)\n\n# Proposed:\n# bootstrap.py\ndef setup_environment() -> None: ...\n\n# In each runner:\nfrom bootstrap import setup_environment\nsetup_environment()",
"bestPractice": "DRY (Don't Repeat Yourself) - extract common initialization logic into reusable utilities",
"metrics": {
"lineCount": null,
"complexity": null,
"duplicateLines": 105,
"testCoverage": null
},
"estimatedEffort": "small",
"breakingChange": false,
"prerequisites": null
},
{
"id": "cq-003",
"type": "code_quality",
"title": "Consolidate project_analyzer.py large file with clear module boundaries",
"description": "The project_analyzer.py file at 1577 lines contains multiple distinct responsibilities: command registries, technology detection, security profiling, and file analysis. The file has grown organically and would benefit from being split into focused modules.",
"rationale": "While this file is large, it's well-structured with clear sections. However, the mixing of static data (command registries), detection logic, and security profiling makes it harder to maintain and test individual components.",
"category": "large_files",
"severity": "minor",
"affectedFiles": [
"auto-claude/project_analyzer.py"
],
"currentState": "1577-line file containing command registries, technology detection, security profiling, and caching logic all in one module",
"proposedChange": "Split into: security/command_registry.py (command mappings), detection/technology_detector.py (stack detection), security/profile_builder.py (security profile creation), and project_analyzer.py (main interface)",
"codeExample": "# Current structure (all in one file):\nBASE_COMMANDS = { ... } # ~200 lines\nLANGUAGE_COMMANDS = { ... } # ~300 lines\nclass ProjectAnalyzer: ... # ~800 lines\n\n# Proposed structure:\n# security/commands.py - Command registries\n# detection/stack.py - Technology detection\n# security/profile.py - Profile building\n# project_analyzer.py - Main coordinator class",
"bestPractice": "Separation of concerns - separate data definitions, business logic, and coordination",
"metrics": {
"lineCount": 1577,
"complexity": null,
"duplicateLines": null,
"testCoverage": null
},
"estimatedEffort": "medium",
"breakingChange": false,
"prerequisites": [
"Maintain backward compatibility for existing imports",
"Ensure comprehensive test coverage"
]
},
{
"id": "cq-004",
"type": "code_quality",
"title": "Add missing Python linting and formatting configuration",
"description": "The project lacks centralized code style configuration (no .flake8, .pylintrc, pyproject.toml with tool sections, or black configuration). Print statements are used extensively (703 occurrences) instead of proper logging, and there's no apparent consistent formatting standard.",
"rationale": "Without automated linting and formatting, code quality can drift over time, leading to inconsistent style, potential bugs, and reduced readability. Print statements make debugging harder and pollute output.",
"category": "linting",
"severity": "minor",
"affectedFiles": [
"multiple files across the project"
],
"currentState": "No centralized linting configuration, 703 print statements across 31 files, no apparent formatting standards",
"proposedChange": "Add pyproject.toml with black, flake8, and mypy configuration. Create a logging utility to replace raw print statements with proper log levels. Add pre-commit hooks for automated formatting.",
"codeExample": "# Add to pyproject.toml:\n[tool.black]\nline-length = 100\ntarget-version = ['py39']\n\n[tool.flake8]\nmax-line-length = 100\nignore = ['E203', 'W503']\n\n# Replace print statements:\n# Before: print(f\"Processing chunk {chunk_id}\")\n# After: logger.info(f\"Processing chunk {chunk_id}\")",
"bestPractice": "Consistent code style and proper logging improve maintainability and debugging capability",
"metrics": {
"lineCount": null,
"complexity": null,
"duplicateLines": null,
"testCoverage": null
},
"estimatedEffort": "small",
"breakingChange": false,
"prerequisites": [
"Decide on line length and style preferences",
"Plan migration strategy for print statements"
]
},
{
"id": "cq-005",
"type": "code_quality",
"title": "Improve type safety with stricter type annotations",
"description": "While the codebase has good type annotation coverage (422 functions with return type annotations), there are opportunities to improve type safety by adding more precise types for complex data structures like implementation plans, chunks, and configuration objects that are often passed around as generic dictionaries.",
"rationale": "Type safety helps catch bugs at development time and improves IDE support. The codebase uses many dictionary-based data structures that could benefit from TypedDict or dataclass definitions for better type checking and documentation.",
"category": "types",
"severity": "suggestion",
"affectedFiles": [
"auto-claude/implementation_plan.py",
"auto-claude/agent.py",
"auto-claude/planner.py",
"auto-claude/progress.py"
],
"currentState": "Good type annotation coverage but heavy use of generic dict types for structured data like chunks, specs, and configuration",
"proposedChange": "Define TypedDict classes for chunk data, spec configuration, and other frequently-used data structures. Consider converting some dictionary-heavy code to use dataclasses.",
"codeExample": "# Current:\ndef process_chunk(chunk: dict) -> dict:\n chunk_id = chunk[\"id\"]\n description = chunk[\"description\"]\n ...\n\n# Proposed:\nfrom typing import TypedDict\n\nclass ChunkData(TypedDict):\n id: str\n description: str\n status: ChunkStatus\n dependencies: List[str]\n\ndef process_chunk(chunk: ChunkData) -> ChunkResult:\n ...",
"bestPractice": "Strong typing improves code reliability and developer experience through better IDE support and compile-time error detection",
"metrics": {
"lineCount": null,
"complexity": null,
"duplicateLines": null,
"testCoverage": null
},
"estimatedEffort": "medium",
"breakingChange": false,
"prerequisites": [
"Audit existing dictionary usage patterns",
"Plan gradual migration strategy"
]
}
],
"project_context": {
"existing_features": [],
"tech_stack": [],
"target_audience": null,
"planned_features": []
},
"summary": {
"total_ideas": 5,
"by_type": {
"code_quality": 5
},
"by_status": {
"draft": 5
}
},
"generated_at": "2025-12-12T10:41:05.715013",
"updated_at": "2025-12-12T09:41:44.342Z"
}
@@ -0,0 +1,15 @@
{
"existing_features": [],
"tech_stack": [],
"target_audience": null,
"planned_features": [],
"config": {
"enabled_types": [
"code_quality"
],
"include_roadmap_context": true,
"include_kanban_context": true,
"max_ideas_per_type": 5
},
"created_at": "2025-12-12T10:38:18.376464"
}
+12
View File
@@ -0,0 +1,12 @@
{
"project_root": "/Users/andremikalsen/Documents/Coding/autonomous-coding",
"project_type": "single",
"services": {},
"infrastructure": {
"docker_compose": "docker-compose.yml",
"docker_services": [
"falkordb"
]
},
"conventions": {}
}
+9 -8
View File
@@ -147,11 +147,12 @@ class IdeationOrchestrator:
self.include_kanban_context = include_kanban_context
self.max_ideas_per_type = max_ideas_per_type
# Default output to project's auto-claude directory
# Default output to project's .auto-claude directory (installed instance)
# Note: auto-claude/ is source code, .auto-claude/ is the installed instance
if output_dir:
self.output_dir = Path(output_dir)
else:
self.output_dir = self.project_dir / "auto-claude" / "ideation"
self.output_dir = self.project_dir / ".auto-claude" / "ideation"
self.output_dir.mkdir(parents=True, exist_ok=True)
@@ -243,8 +244,8 @@ class IdeationOrchestrator:
"planned_features": [],
}
# Get project index
project_index_path = self.project_dir / "auto-claude" / "project_index.json"
# Get project index (from .auto-claude - the installed instance)
project_index_path = self.project_dir / ".auto-claude" / "project_index.json"
if project_index_path.exists():
try:
with open(project_index_path) as f:
@@ -261,7 +262,7 @@ class IdeationOrchestrator:
# Get roadmap context if enabled
if self.include_roadmap_context:
roadmap_path = self.project_dir / "auto-claude" / "roadmap" / "roadmap.json"
roadmap_path = self.project_dir / ".auto-claude" / "roadmap" / "roadmap.json"
if roadmap_path.exists():
try:
with open(roadmap_path) as f:
@@ -276,7 +277,7 @@ class IdeationOrchestrator:
pass
# Also check discovery for audience
discovery_path = self.project_dir / "auto-claude" / "roadmap" / "roadmap_discovery.json"
discovery_path = self.project_dir / ".auto-claude" / "roadmap" / "roadmap_discovery.json"
if discovery_path.exists() and not context["target_audience"]:
try:
with open(discovery_path) as f:
@@ -292,7 +293,7 @@ class IdeationOrchestrator:
# Get kanban context if enabled
if self.include_kanban_context:
specs_dir = self.project_dir / "auto-claude" / "specs"
specs_dir = self.project_dir / ".auto-claude" / "specs"
if specs_dir.exists():
for spec_dir in specs_dir.iterdir():
if spec_dir.is_dir():
@@ -357,7 +358,7 @@ class IdeationOrchestrator:
"""Ensure project index exists."""
project_index = self.output_dir / "project_index.json"
auto_build_index = self.project_dir / "auto-claude" / "project_index.json"
auto_build_index = self.project_dir / ".auto-claude" / "project_index.json"
# Check if we can copy existing index
if auto_build_index.exists():
+87 -2
View File
@@ -32,6 +32,8 @@ class WorkflowType(str, Enum):
INVESTIGATION = "investigation" # Bug hunting (investigate, hypothesize, fix)
MIGRATION = "migration" # Data migration (prepare, test, execute, cleanup)
SIMPLE = "simple" # Single-service, minimal overhead
DEVELOPMENT = "development" # General development work
ENHANCEMENT = "enhancement" # Improving existing features
class PhaseType(str, Enum):
@@ -186,6 +188,9 @@ class Chunk:
self.status = ChunkStatus.IN_PROGRESS
self.started_at = datetime.now().isoformat()
self.session_id = session_id
# Clear stale data from previous runs to ensure clean state
self.completed_at = None
self.actual_output = None
def complete(self, output: Optional[str] = None):
"""Mark chunk as done."""
@@ -197,6 +202,7 @@ class Chunk:
def fail(self, reason: Optional[str] = None):
"""Mark chunk as failed."""
self.status = ChunkStatus.FAILED
self.completed_at = None # Clear to maintain consistency (failed != completed)
if reason:
self.actual_output = f"FAILED: {reason}"
@@ -263,8 +269,16 @@ class ImplementationPlan:
updated_at: Optional[str] = None
spec_file: Optional[str] = None
# Task status (synced with UI)
# status: backlog, in_progress, ai_review, human_review, done
# planStatus: pending, in_progress, review, completed
status: Optional[str] = None
planStatus: Optional[str] = None
recoveryNote: Optional[str] = None
qa_signoff: Optional[dict] = None
def to_dict(self) -> dict:
return {
result = {
"feature": self.feature,
"workflow_type": self.workflow_type.value,
"services_involved": self.services_involved,
@@ -274,18 +288,41 @@ class ImplementationPlan:
"updated_at": self.updated_at,
"spec_file": self.spec_file,
}
# Include status fields if set (synced with UI)
if self.status:
result["status"] = self.status
if self.planStatus:
result["planStatus"] = self.planStatus
if self.recoveryNote:
result["recoveryNote"] = self.recoveryNote
if self.qa_signoff:
result["qa_signoff"] = self.qa_signoff
return result
@classmethod
def from_dict(cls, data: dict) -> "ImplementationPlan":
# Parse workflow_type with fallback for unknown types
workflow_type_str = data.get("workflow_type", "feature")
try:
workflow_type = WorkflowType(workflow_type_str)
except ValueError:
# Unknown workflow type - default to FEATURE
print(f"Warning: Unknown workflow_type '{workflow_type_str}', defaulting to 'feature'")
workflow_type = WorkflowType.FEATURE
return cls(
feature=data["feature"],
workflow_type=WorkflowType(data.get("workflow_type", "feature")),
workflow_type=workflow_type,
services_involved=data.get("services_involved", []),
phases=[Phase.from_dict(p) for p in data.get("phases", [])],
final_acceptance=data.get("final_acceptance", []),
created_at=data.get("created_at"),
updated_at=data.get("updated_at"),
spec_file=data.get("spec_file"),
status=data.get("status"),
planStatus=data.get("planStatus"),
recoveryNote=data.get("recoveryNote"),
qa_signoff=data.get("qa_signoff"),
)
def save(self, path: Path):
@@ -294,10 +331,58 @@ class ImplementationPlan:
if not self.created_at:
self.created_at = self.updated_at
# Auto-update status based on chunk completion
self.update_status_from_chunks()
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
json.dump(self.to_dict(), f, indent=2)
def update_status_from_chunks(self):
"""Update overall status and planStatus based on chunk completion state.
This syncs the task status with the UI's expected values:
- status: backlog, in_progress, ai_review, human_review, done
- planStatus: pending, in_progress, review, completed
"""
all_chunks = [c for p in self.phases for c in p.chunks]
if not all_chunks:
# No chunks yet - stay in backlog/pending
if not self.status:
self.status = "backlog"
if not self.planStatus:
self.planStatus = "pending"
return
completed_count = sum(1 for c in all_chunks if c.status == ChunkStatus.COMPLETED)
failed_count = sum(1 for c in all_chunks if c.status == ChunkStatus.FAILED)
in_progress_count = sum(1 for c in all_chunks if c.status == ChunkStatus.IN_PROGRESS)
total_count = len(all_chunks)
# Determine status based on chunk states
if completed_count == total_count:
# All chunks completed - check if QA approved
if self.qa_signoff and self.qa_signoff.get("status") == "approved":
self.status = "human_review"
self.planStatus = "review"
else:
# All chunks done, waiting for QA
self.status = "ai_review"
self.planStatus = "review"
elif failed_count > 0:
# Some chunks failed - still in progress (needs retry or fix)
self.status = "in_progress"
self.planStatus = "in_progress"
elif in_progress_count > 0 or completed_count > 0:
# Some chunks in progress or completed
self.status = "in_progress"
self.planStatus = "in_progress"
else:
# All chunks pending - backlog
self.status = "backlog"
self.planStatus = "pending"
@classmethod
def load(cls, path: Path) -> "ImplementationPlan":
"""Load plan from JSON file."""
+4 -4
View File
@@ -37,8 +37,8 @@ def load_project_context(project_dir: str) -> str:
"""Load project context for the AI."""
context_parts = []
# Load project index if available
index_path = Path(project_dir) / "auto-claude" / "project_index.json"
# Load project index if available (from .auto-claude - the installed instance)
index_path = Path(project_dir) / ".auto-claude" / "project_index.json"
if index_path.exists():
try:
with open(index_path) as f:
@@ -55,7 +55,7 @@ def load_project_context(project_dir: str) -> str:
pass
# Load roadmap if available
roadmap_path = Path(project_dir) / "auto-claude" / "roadmap" / "roadmap.json"
roadmap_path = Path(project_dir) / ".auto-claude" / "roadmap" / "roadmap.json"
if roadmap_path.exists():
try:
with open(roadmap_path) as f:
@@ -68,7 +68,7 @@ def load_project_context(project_dir: str) -> str:
pass
# Load existing tasks
tasks_path = Path(project_dir) / "auto-claude" / "specs"
tasks_path = Path(project_dir) / ".auto-claude" / "specs"
if tasks_path.exists():
try:
task_dirs = [d for d in tasks_path.iterdir() if d.is_dir()]
+9 -9
View File
@@ -642,11 +642,10 @@ git commit -m "auto-claude: Complete [chunk-id] - [chunk description]
- Phase progress: [X]/[Y] chunks complete"
```
### Push to Remote
### DO NOT Push to Remote
```bash
git push origin auto-claude/[feature-name]
```
**IMPORTANT**: Do NOT run `git push`. All work stays local until the user reviews and approves.
The user will push to remote after reviewing your changes in the isolated workspace.
**Note**: Memory files (attempt_history.json, build_commits.json) are automatically
updated by the orchestrator after each session. You don't need to update them manually.
@@ -680,7 +679,7 @@ Check complexity_assessment.json for dev_mode flag. If dev_mode is true, skip co
# Only if dev_mode is false:
git add build-progress.txt
git commit -m "auto-claude: Update progress"
git push
# Do NOT push - user will push after review
```
---
@@ -882,10 +881,11 @@ Before context fills up:
1. **Write session insights** - Document what you learned (Step 12, optional)
2. **Commit all working code** - no uncommitted changes
3. **Push to remote** - ensure progress is saved
4. **Update build-progress.txt** - document what's next
5. **Leave app working** - no broken state
6. **No half-finished chunks** - complete or revert
3. **Update build-progress.txt** - document what's next
4. **Leave app working** - no broken state
5. **No half-finished chunks** - complete or revert
**NOTE**: Do NOT push to remote. All work stays local until user reviews and approves.
The next session will:
1. Read implementation_plan.json
+3 -2
View File
@@ -660,8 +660,9 @@ Before your context fills up:
1. **Commit all work**
2. **Ensure implementation_plan.json is complete**
3. **Push to remote**: `git push -u origin auto-claude/[feature-name]`
4. **Leave clean state** - no broken code
3. **Leave clean state** - no broken code
**NOTE**: Do NOT push to remote. All work stays local until user reviews and approves.
The next agent will:
1. Read `implementation_plan.json` for chunk list
+1 -4
View File
@@ -182,10 +182,7 @@ Verified:
QA Fix Session: [N]"
```
Push:
```bash
git push origin [branch-name]
```
**NOTE**: Do NOT push to remote. All work stays local until user reviews and approves.
---
+3 -2
View File
@@ -88,11 +88,12 @@ class RoadmapOrchestrator:
self.model = model
self.refresh = refresh
# Default output to project's auto-claude directory
# Default output to project's .auto-claude directory (installed instance)
# Note: auto-claude/ is source code, .auto-claude/ is the installed instance
if output_dir:
self.output_dir = Path(output_dir)
else:
self.output_dir = self.project_dir / "auto-claude" / "roadmap"
self.output_dir = self.project_dir / ".auto-claude" / "roadmap"
self.output_dir.mkdir(parents=True, exist_ok=True)
+35 -41
View File
@@ -108,35 +108,24 @@ from qa_loop import (
# Configuration
DEFAULT_MODEL = "claude-opus-4-5-20251101"
# Dev specs directory (--dev mode) - gitignored, for developing auto-claude itself
DEV_SPECS_DIR = "dev/auto-claude/specs"
def get_specs_dir(project_dir: Path, dev_mode: bool = False) -> Path:
"""Get the specs directory path based on project and mode.
"""Get the specs directory path.
IMPORTANT: Only .auto-claude/ is considered an "installed" auto-claude.
The auto-claude/ folder (if it exists) is SOURCE CODE being developed,
not an installation. This allows Auto Claude to be used to develop itself.
Args:
project_dir: The project root directory
dev_mode: If True, use dev/auto-claude/specs/ for framework development
dev_mode: Deprecated, kept for API compatibility. Has no effect.
Returns:
Path to the specs directory within the project
Path to the specs directory within .auto-claude/
"""
if dev_mode:
return project_dir / DEV_SPECS_DIR
# Check for .auto-claude first (hidden folder for external projects)
hidden_auto_claude = project_dir / ".auto-claude"
if hidden_auto_claude.exists():
return hidden_auto_claude / "specs"
# Then check for auto-claude (visible folder)
visible_auto_claude = project_dir / "auto-claude"
if visible_auto_claude.exists():
return visible_auto_claude / "specs"
# Default to .auto-claude for external projects
return hidden_auto_claude / "specs"
# Always use .auto-claude/specs - this is the installed instance
# The auto-claude/ folder is source code, not an installation
return project_dir / ".auto-claude" / "specs"
def list_specs(project_dir: Path, dev_mode: bool = False) -> list[dict]:
@@ -419,6 +408,13 @@ Environment Variables:
help="Dev mode: use specs from dev/auto-claude/specs/ (gitignored) for framework development",
)
# Non-interactive mode (for UI/automation)
parser.add_argument(
"--auto-continue",
action="store_true",
help="Non-interactive mode: auto-continue existing builds, skip prompts (for UI integration)",
)
return parser.parse_args()
@@ -503,22 +499,14 @@ def main() -> None:
project_dir = Path.cwd()
debug("run.py", "Using current working directory", project_dir=str(project_dir))
# Auto-detect if running from within auto-claude directory
# Handle both: auto-claude/ and dev/auto-claude/
# Auto-detect if running from within auto-claude directory (the source code)
if project_dir.name == "auto-claude" and (project_dir / "run.py").exists():
parent = project_dir.parent
# Check if we're in dev/auto-claude (parent is 'dev')
if parent.name == "dev" and (parent.parent / "auto-claude" / "run.py").exists():
# Running from dev/auto-claude, go up 2 levels
project_dir = parent.parent
else:
# Running from auto-claude, go up 1 level
project_dir = parent
# Running from within auto-claude/ source directory, go up 1 level
project_dir = project_dir.parent
# Show dev mode info
# Note: --dev flag is deprecated but kept for API compatibility
if args.dev:
print(f"\n{icon(Icons.GEAR)} DEV MODE: Using specs from dev/auto-claude/specs/")
print(f" Code changes can target the entire project root\n")
print(f"\n{icon(Icons.GEAR)} Note: --dev flag is deprecated. All specs now use .auto-claude/specs/\n")
# Handle --list
if args.list:
@@ -629,13 +617,18 @@ def main() -> None:
# Check for existing build
if get_existing_build_worktree(project_dir, spec_dir.name):
continue_existing = check_existing_build(project_dir, spec_dir.name)
if continue_existing:
# Continue with existing worktree
pass
if args.auto_continue:
# Non-interactive mode: auto-continue with existing build
debug("run.py", "Auto-continue mode: continuing with existing build")
print("Auto-continue: Resuming existing build...")
else:
# User chose to start fresh or merged existing
pass
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
# Choose workspace (skip for parallel mode - it always uses worktrees)
working_dir = project_dir
@@ -646,12 +639,13 @@ def main() -> None:
workspace_mode = WorkspaceMode.ISOLATED
print("Parallel mode uses isolated workspaces automatically.")
else:
# Sequential mode - let user choose
# Sequential mode - let user choose (or auto-select if --auto-continue)
workspace_mode = choose_workspace(
project_dir,
spec_dir.name,
force_isolated=args.isolated,
force_direct=args.direct,
auto_continue=args.auto_continue,
)
if workspace_mode == WorkspaceMode.ISOLATED:
+2 -2
View File
@@ -51,8 +51,8 @@ class ServiceContextGenerator:
self.project_index = project_index or self._load_project_index()
def _load_project_index(self) -> dict:
"""Load project index from file."""
index_file = self.project_dir / "auto-claude" / "project_index.json"
"""Load project index from file (.auto-claude is the installed instance)."""
index_file = self.project_dir / ".auto-claude" / "project_index.json"
if index_file.exists():
with open(index_file) as f:
return json.load(f)
+33 -40
View File
@@ -92,36 +92,24 @@ from debug import (
MAX_RETRIES = 3
PROMPTS_DIR = Path(__file__).parent / "prompts"
# Dev specs directory (--dev mode) - gitignored, for developing auto-claude itself
DEV_SPECS_DIR = Path(__file__).parent.parent / "dev" / "auto-claude" / "specs"
def get_specs_dir(project_dir: Path, dev_mode: bool = False) -> Path:
"""Get the specs directory based on project and mode.
"""Get the specs directory path.
IMPORTANT: Only .auto-claude/ is considered an "installed" auto-claude.
The auto-claude/ folder (if it exists) is SOURCE CODE being developed,
not an installation. This allows Auto Claude to be used to develop itself.
Args:
project_dir: The project root directory
dev_mode: If True, use dev/auto-claude/specs/ for framework development
dev_mode: Deprecated, kept for API compatibility. Has no effect.
Returns:
Path to the specs directory within the project
Path to the specs directory within .auto-claude/
"""
if dev_mode:
return DEV_SPECS_DIR
# Check for .auto-claude first (hidden folder for external projects)
hidden_auto_claude = project_dir / ".auto-claude"
if hidden_auto_claude.exists():
return hidden_auto_claude / "specs"
# Then check for auto-claude (visible folder)
visible_auto_claude = project_dir / "auto-claude"
if visible_auto_claude.exists():
return visible_auto_claude / "specs"
# Default to creating .auto-claude for external projects
# (auto-claude is typically only used when the project IS auto-claude itself)
return hidden_auto_claude / "specs"
# Always use .auto-claude/specs - this is the installed instance
# The auto-claude/ folder is source code, not an installation
return project_dir / ".auto-claude" / "specs"
class Complexity(Enum):
@@ -384,6 +372,7 @@ class SpecOrchestrator:
project_dir: Path,
task_description: Optional[str] = None,
spec_name: Optional[str] = None,
spec_dir: Optional[Path] = None, # Use existing spec directory (for UI integration)
model: str = "claude-opus-4-5-20251101",
complexity_override: Optional[str] = None, # Force a specific complexity
use_ai_assessment: bool = True, # Use AI for complexity assessment (vs heuristics)
@@ -402,8 +391,11 @@ class SpecOrchestrator:
# Complexity assessment (populated during run)
self.assessment: Optional[ComplexityAssessment] = None
# Create spec directory
if spec_name:
# Create/use spec directory
if spec_dir:
# Use provided spec directory (from UI)
self.spec_dir = Path(spec_dir)
elif spec_name:
self.spec_dir = self.specs_dir / spec_name
else:
self.spec_dir = self._create_spec_dir()
@@ -1686,6 +1678,11 @@ Examples:
action="store_true",
help="Don't automatically start the build after spec creation (default: auto-start build)",
)
parser.add_argument(
"--spec-dir",
type=Path,
help="Use existing spec directory instead of creating a new one (for UI integration)",
)
args = parser.parse_args()
@@ -1711,33 +1708,27 @@ Examples:
# Find project root (look for auto-claude folder)
project_dir = args.project_dir
# Auto-detect if running from within auto-claude directory
# Handle both: auto-claude/ and dev/auto-claude/
# Auto-detect if running from within auto-claude directory (the source code)
if project_dir.name == "auto-claude" and (project_dir / "run.py").exists():
parent = project_dir.parent
# Check if we're in dev/auto-claude (parent is 'dev')
if parent.name == "dev" and (parent.parent / "auto-claude" / "run.py").exists():
# Running from dev/auto-claude, go up 2 levels
project_dir = parent.parent
else:
# Running from auto-claude, go up 1 level
project_dir = parent
elif not (project_dir / "auto-claude").exists():
# Try parent directories
# Running from within auto-claude/ source directory, go up 1 level
project_dir = project_dir.parent
elif not (project_dir / ".auto-claude").exists():
# No .auto-claude folder found - try to find project root
# First check for .auto-claude (installed instance)
for parent in project_dir.parents:
if (parent / "auto-claude").exists():
if (parent / ".auto-claude").exists():
project_dir = parent
break
# Show dev mode warning
# Note: --dev flag is deprecated but kept for API compatibility
if args.dev:
print(f"\n{icon(Icons.GEAR)} DEV MODE: Specs will be saved to dev/auto-claude/specs/ (gitignored)")
print(f" Code changes can target the entire project root\n")
print(f"\n{icon(Icons.GEAR)} Note: --dev flag is deprecated. All specs now go to .auto-claude/specs/\n")
orchestrator = SpecOrchestrator(
project_dir=project_dir,
task_description=task_description,
spec_name=args.continue_spec,
spec_dir=args.spec_dir,
model=args.model,
complexity_override=args.complexity,
use_ai_assessment=not args.no_ai_assessment,
@@ -1762,6 +1753,8 @@ Examples:
sys.executable,
str(run_script),
"--spec", orchestrator.spec_dir.name,
"--project-dir", str(orchestrator.project_dir),
"--auto-continue", # Non-interactive mode for chained execution
]
# Pass through dev mode
+6 -6
View File
@@ -50,20 +50,20 @@ from ui import (
def find_project_root() -> Path:
"""Find the project root by looking for .auto-claude-status or auto-claude/."""
"""Find the project root by looking for .auto-claude or .auto-claude-status."""
cwd = Path.cwd()
# Check current directory
if (cwd / ".auto-claude-status").exists():
# Check current directory - prioritize .auto-claude (installed instance)
if (cwd / ".auto-claude").exists():
return cwd
if (cwd / "auto-claude").exists():
if (cwd / ".auto-claude-status").exists():
return cwd
# Walk up to find project root
for parent in cwd.parents:
if (parent / ".auto-claude-status").exists():
if (parent / ".auto-claude").exists():
return parent
if (parent / "auto-claude").exists():
if (parent / ".auto-claude-status").exists():
return parent
return cwd
+10 -2
View File
@@ -98,6 +98,7 @@ def choose_workspace(
spec_name: str,
force_isolated: bool = False,
force_direct: bool = False,
auto_continue: bool = False,
) -> WorkspaceMode:
"""
Let user choose where auto-claude should work.
@@ -109,6 +110,7 @@ def choose_workspace(
spec_name: Name of the spec being built
force_isolated: Skip prompts and use isolated mode
force_direct: Skip prompts and use direct mode
auto_continue: Non-interactive mode (for UI integration) - skip all prompts
Returns:
WorkspaceMode indicating where to work
@@ -119,6 +121,11 @@ def choose_workspace(
if force_direct:
return WorkspaceMode.DIRECT
# Non-interactive mode: default to isolated for safety
if auto_continue:
print("Auto-continue: Using isolated workspace for safety.")
return WorkspaceMode.ISOLATED
# Check for unsaved work
has_unsaved = has_uncommitted_changes(project_dir)
@@ -203,8 +210,9 @@ def copy_spec_to_worktree(
Path to the spec directory inside the worktree
"""
# Determine target location inside worktree
# Use auto-claude/specs/{spec_name}/ as the standard location
target_spec_dir = worktree_path / "auto-claude" / "specs" / spec_name
# Use .auto-claude/specs/{spec_name}/ as the standard location
# Note: auto-claude/ is source code, .auto-claude/ is the installed instance
target_spec_dir = worktree_path / ".auto-claude" / "specs" / spec_name
# Create parent directories if needed
target_spec_dir.parent.mkdir(parents=True, exist_ok=True)