auto-claude: Complete migrate-to-new
Update consumers to use new system
This commit is contained in:
+9
-9
@@ -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
@@ -70,4 +70,6 @@ dmypy.json
|
||||
.claude_settings.json
|
||||
|
||||
# Development of Auto Build with Auto Build
|
||||
dev/
|
||||
dev/
|
||||
|
||||
.auto-claude/
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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():
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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()]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user