Rename to Auto Claude

This commit is contained in:
AndyMik90
2025-12-11 19:53:55 +01:00
parent 22be3ad69d
commit bbf602174f
156 changed files with 2473 additions and 426 deletions
+192
View File
@@ -0,0 +1,192 @@
{
"base_commands": [
".",
"[",
"[[",
"ag",
"awk",
"basename",
"bash",
"bc",
"break",
"cat",
"cd",
"chmod",
"clear",
"cmp",
"column",
"comm",
"command",
"continue",
"cp",
"curl",
"cut",
"date",
"df",
"diff",
"dig",
"dirname",
"du",
"echo",
"egrep",
"env",
"eval",
"exec",
"exit",
"expand",
"export",
"expr",
"false",
"fd",
"fgrep",
"file",
"find",
"fmt",
"fold",
"gawk",
"gh",
"git",
"grep",
"gunzip",
"gzip",
"head",
"help",
"host",
"iconv",
"id",
"jobs",
"join",
"jq",
"kill",
"killall",
"less",
"let",
"ln",
"ls",
"lsof",
"man",
"mkdir",
"mktemp",
"more",
"mv",
"nl",
"paste",
"pgrep",
"ping",
"pkill",
"popd",
"printenv",
"printf",
"ps",
"pushd",
"pwd",
"read",
"readlink",
"realpath",
"reset",
"return",
"rev",
"rg",
"rm",
"rmdir",
"sed",
"seq",
"set",
"sh",
"shuf",
"sleep",
"sort",
"source",
"split",
"stat",
"tail",
"tar",
"tee",
"test",
"time",
"timeout",
"touch",
"tr",
"tree",
"true",
"type",
"uname",
"unexpand",
"uniq",
"unset",
"unzip",
"watch",
"wc",
"wget",
"whereis",
"which",
"whoami",
"xargs",
"yes",
"yq",
"zip",
"zsh"
],
"stack_commands": [
"ar",
"clang",
"clang++",
"cmake",
"dive",
"docker",
"docker-buildx",
"docker-compose",
"dockerfile",
"g++",
"gcc",
"ipython",
"jupyter",
"ld",
"make",
"meson",
"ninja",
"nm",
"notebook",
"objdump",
"pdb",
"pip",
"pip3",
"pipx",
"pudb",
"python",
"python3",
"redis-benchmark",
"redis-cli",
"redis-server",
"strip"
],
"script_commands": [],
"custom_commands": [],
"detected_stack": {
"languages": [
"python",
"c"
],
"package_managers": [],
"frameworks": [],
"databases": [
"redis"
],
"infrastructure": [
"docker"
],
"cloud_providers": [],
"code_quality_tools": [],
"version_managers": []
},
"custom_scripts": {
"npm_scripts": [],
"make_targets": [],
"poetry_scripts": [],
"cargo_aliases": [],
"shell_scripts": []
},
"project_dir": "/Users/andremikalsen/Documents/Coding/autonomous-coding",
"created_at": "2025-12-10T15:55:09.016762",
"project_hash": "983ec2eb75cd87788b47de48294e8bf8"
}
+10 -10
View File
@@ -1,11 +1,11 @@
# Setup ccstatusline Integration for Auto-Build
Configure ccstatusline to display real-time auto-build progress in your Claude Code status bar.
Configure ccstatusline to display real-time auto-claude progress in your Claude Code status bar.
## Prerequisites
1. **ccstatusline** must be installed and configured
2. **auto-build** must be in your project
2. **auto-claude** must be in your project
## Installation
@@ -26,7 +26,7 @@ This launches the interactive TUI to configure your status line.
In the ccstatusline TUI config, add a **Custom Command** widget with:
```
Command: python /path/to/your/project/auto-build/statusline.py --format compact
Command: python /path/to/your/project/auto-claude/statusline.py --format compact
```
**Recommended widget settings:**
@@ -41,7 +41,7 @@ Edit `~/.config/ccstatusline/settings.json` and add to your widgets array:
```json
{
"type": "custom",
"command": "python /path/to/your/project/auto-build/statusline.py --format compact",
"command": "python /path/to/your/project/auto-claude/statusline.py --format compact",
"interval": 5,
"showWhenEmpty": false
}
@@ -80,7 +80,7 @@ Output: Raw JSON status data
## Status File
Auto-build writes status to `.auto-build-status` in your project root:
Auto-build writes status to `.auto-claude-status` in your project root:
```json
{
@@ -121,9 +121,9 @@ When active, you'll see these indicators:
## Troubleshooting
### Status not showing?
1. Check if `.auto-build-status` exists in your project root
1. Check if `.auto-claude-status` exists in your project root
2. Verify the path to `statusline.py` is correct
3. Try running the command manually: `python auto-build/statusline.py --format compact`
3. Try running the command manually: `python auto-claude/statusline.py --format compact`
### Updates too slow?
- Decrease the polling interval in ccstatusline config (minimum 1 second)
@@ -136,16 +136,16 @@ When active, you'll see these indicators:
### Minimal Status Line
Just chunks and phase:
```
python auto-build/statusline.py --format compact
python auto-claude/statusline.py --format compact
```
### With Specific Spec
Monitor a specific spec:
```
python auto-build/statusline.py --format compact --spec 001-my-feature
python auto-claude/statusline.py --format compact --spec 001-my-feature
```
### Full Path for Global Use
```
python ~/projects/my-app/auto-build/statusline.py --format compact --project-dir ~/projects/my-app
python ~/projects/my-app/auto-claude/statusline.py --format compact --project-dir ~/projects/my-app
```
+21 -21
View File
@@ -4,61 +4,61 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
Auto-Build is a multi-agent autonomous coding framework that builds software through coordinated AI agent sessions. It uses the Claude Code SDK to run agents in isolated workspaces with security controls.
Auto Claude is a multi-agent autonomous coding framework that builds software through coordinated AI agent sessions. It uses the Claude Code SDK to run agents in isolated workspaces with security controls.
## Commands
### Setup
```bash
# Install dependencies (from auto-build/)
# Install dependencies (from auto-claude/)
uv venv && uv pip install -r requirements.txt
# Or: python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt
# Set up OAuth token
claude setup-token
# Add to auto-build/.env: CLAUDE_CODE_OAUTH_TOKEN=your-token
# Add to auto-claude/.env: CLAUDE_CODE_OAUTH_TOKEN=your-token
```
### Creating and Running Specs
```bash
# Create a spec interactively
python auto-build/spec_runner.py --interactive
python auto-claude/spec_runner.py --interactive
# Create spec from task description
python auto-build/spec_runner.py --task "Add user authentication"
python auto-claude/spec_runner.py --task "Add user authentication"
# Force complexity level (simple/standard/complex)
python auto-build/spec_runner.py --task "Fix button" --complexity simple
python auto-claude/spec_runner.py --task "Fix button" --complexity simple
# Run autonomous build
python auto-build/run.py --spec 001
python auto-claude/run.py --spec 001
# Run with parallel workers
python auto-build/run.py --spec 001 --parallel 2
python auto-claude/run.py --spec 001 --parallel 2
# List all specs
python auto-build/run.py --list
python auto-claude/run.py --list
```
### Workspace Management
```bash
# Review changes in isolated worktree
python auto-build/run.py --spec 001 --review
python auto-claude/run.py --spec 001 --review
# Merge completed build into project
python auto-build/run.py --spec 001 --merge
python auto-claude/run.py --spec 001 --merge
# Discard build
python auto-build/run.py --spec 001 --discard
python auto-claude/run.py --spec 001 --discard
```
### QA Validation
```bash
# Run QA manually
python auto-build/run.py --spec 001 --qa
python auto-claude/run.py --spec 001 --qa
# Check QA status
python auto-build/run.py --spec 001 --qa-status
python auto-claude/run.py --spec 001 --qa-status
```
### Testing
@@ -78,7 +78,7 @@ pytest tests/ -m "not slow"
### Spec Validation
```bash
python auto-build/validate_spec.py --spec-dir auto-build/specs/001-feature --checkpoint all
python auto-claude/validate_spec.py --spec-dir auto-claude/specs/001-feature --checkpoint all
```
## Architecture
@@ -106,7 +106,7 @@ python auto-build/validate_spec.py --spec-dir auto-build/specs/001-feature --che
- **graphiti_memory.py** - Optional graph-based cross-session memory (requires FalkorDB)
- **linear_updater.py** - Optional Linear integration for progress tracking
### Agent Prompts (auto-build/prompts/)
### Agent Prompts (auto-claude/prompts/)
| Prompt | Purpose |
|--------|---------|
@@ -123,7 +123,7 @@ python auto-build/validate_spec.py --spec-dir auto-build/specs/001-feature --che
### Spec Directory Structure
Each spec in `auto-build/specs/XXX-name/` contains:
Each spec in `auto-claude/specs/XXX-name/` contains:
- `spec.md` - Feature specification
- `requirements.json` - Structured user requirements
- `context.json` - Discovered codebase context
@@ -138,12 +138,12 @@ Three-layer defense:
2. **Filesystem Permissions** - Operations restricted to project directory
3. **Command Allowlist** - Dynamic allowlist from project analysis (security.py + project_analyzer.py)
Security profile cached in `.auto-build-security.json`.
Security profile cached in `.auto-claude-security.json`.
## Development Mode
Use `--dev` flag to work in `dev/auto-build/specs/` (gitignored) when developing the framework itself:
Use `--dev` flag to work in `dev/auto-claude/specs/` (gitignored) when developing the framework itself:
```bash
python auto-build/spec_runner.py --dev --task "Test feature"
python auto-build/run.py --dev --spec 001
python auto-claude/spec_runner.py --dev --task "Test feature"
python auto-claude/run.py --dev --spec 001
```
+42 -42
View File
@@ -1,10 +1,10 @@
# Auto-Build Framework
# Auto Claude
A production-ready framework for autonomous multi-session AI coding. Build complete applications or add features to existing projects through coordinated AI agent sessions.
## What It Does
Auto-Build uses a **multi-agent pattern** to build software autonomously:
Auto Claude uses a **multi-agent pattern** to build software autonomously:
### Spec Creation Pipeline (8 phases)
1. **Discovery** - Analyzes project structure
@@ -33,18 +33,18 @@ Each session runs with a fresh context window. Progress is tracked via `implemen
### Setup
**Step 1:** Copy the `auto-build` folder into your project
**Step 1:** Copy the `auto-claude` folder into your project
```bash
# Copy the auto-build folder to your project root
cp -r auto-build /path/to/your/project/
# Copy the auto-claude folder to your project root
cp -r auto-claude /path/to/your/project/
```
**Step 2:** Set up Python environment
```bash
cd your-project
cd auto-build
cd auto-claude
# Using uv (recommended)
uv venv && uv pip install -r requirements.txt
@@ -69,13 +69,13 @@ claude setup-token
```bash
# Activate the virtual environment
source auto-build/.venv/bin/activate
source auto-claude/.venv/bin/activate
# Create a spec interactively
python auto-build/spec_runner.py --interactive
python auto-claude/spec_runner.py --interactive
# Or with a task description
python auto-build/spec_runner.py --task "Add user authentication with OAuth"
python auto-claude/spec_runner.py --task "Add user authentication with OAuth"
```
The spec orchestrator will:
@@ -91,25 +91,25 @@ The spec orchestrator will:
**Step 5:** Run the autonomous build
```bash
python auto-build/run.py --spec 001
python auto-claude/run.py --spec 001
```
### Managing Specs
```bash
# List all specs and their status
python auto-build/run.py --list
python auto-claude/run.py --list
# Run a specific spec
python auto-build/run.py --spec 001
python auto-build/run.py --spec 001-feature-name
python auto-claude/run.py --spec 001
python auto-claude/run.py --spec 001-feature-name
# Run with parallel workers (2-3x speedup for independent phases)
python auto-build/run.py --spec 001 --parallel 2
python auto-build/run.py --spec 001 --parallel 3
python auto-claude/run.py --spec 001 --parallel 2
python auto-claude/run.py --spec 001 --parallel 3
# Limit iterations for testing
python auto-build/run.py --spec 001 --max-iterations 5
python auto-claude/run.py --spec 001 --max-iterations 5
```
### QA Validation
@@ -119,13 +119,13 @@ After all chunks are complete, QA validation runs automatically:
```bash
# QA runs automatically after build completes
# To skip automatic QA:
python auto-build/run.py --spec 001 --skip-qa
python auto-claude/run.py --spec 001 --skip-qa
# Run QA validation manually on a completed build
python auto-build/run.py --spec 001 --qa
python auto-claude/run.py --spec 001 --qa
# Check QA status
python auto-build/run.py --spec 001 --qa-status
python auto-claude/run.py --spec 001 --qa-status
```
The QA validation loop:
@@ -141,19 +141,19 @@ The `spec_runner.py` orchestrator **automatically assesses task complexity** and
```bash
# Simple task (auto-detected) - runs 3 phases
python auto-build/spec_runner.py --task "Fix button color in Header"
python auto-claude/spec_runner.py --task "Fix button color in Header"
# Complex task (auto-detected) - runs 8 phases
python auto-build/spec_runner.py --task "Add Graphiti memory integration with FalkorDB"
python auto-claude/spec_runner.py --task "Add Graphiti memory integration with FalkorDB"
# Force a specific complexity level
python auto-build/spec_runner.py --task "Update text" --complexity simple
python auto-claude/spec_runner.py --task "Update text" --complexity simple
# Interactive mode
python auto-build/spec_runner.py --interactive
python auto-claude/spec_runner.py --interactive
# Continue an interrupted spec
python auto-build/spec_runner.py --continue 001-feature
python auto-claude/spec_runner.py --continue 001-feature
```
**Complexity Tiers:**
@@ -185,17 +185,17 @@ python auto-build/spec_runner.py --continue 001-feature
**Manual validation:**
```bash
python auto-build/validate_spec.py --spec-dir auto-build/specs/001-feature --checkpoint all
python auto-claude/validate_spec.py --spec-dir auto-claude/specs/001-feature --checkpoint all
```
### Isolated Worktrees (Safe by Default)
Auto-Build uses Git worktrees to keep your work completely safe. All AI-generated code is built in a separate workspace (`.worktrees/auto-build/`) - your current files are never touched until you explicitly merge.
Auto Claude uses Git worktrees to keep your work completely safe. All AI-generated code is built in a separate workspace (`.worktrees/auto-claude/`) - your current files are never touched until you explicitly merge.
**How it works:**
1. When you run auto-build, it creates an isolated workspace
2. All coding happens in `.worktrees/auto-build/` on its own branch
1. When you run auto-claude, it creates an isolated workspace
2. All coding happens in `.worktrees/auto-claude/` on its own branch
3. You can `cd` into the worktree to test the feature before accepting
4. Only when you're satisfied, merge the changes into your project
@@ -203,27 +203,27 @@ Auto-Build uses Git worktrees to keep your work completely safe. All AI-generate
```bash
# Test the feature in the isolated workspace
cd .worktrees/auto-build/
cd .worktrees/auto-claude/
npm run dev # or your project's run command
# See what was changed
python auto-build/run.py --spec 001 --review
python auto-claude/run.py --spec 001 --review
# Add changes to your project
python auto-build/run.py --spec 001 --merge
python auto-claude/run.py --spec 001 --merge
# Discard if you don't like it (requires confirmation)
python auto-build/run.py --spec 001 --discard
python auto-claude/run.py --spec 001 --discard
```
**Key benefits:**
- **Safety**: Your uncommitted work is protected - auto-build won't touch it
- **Safety**: Your uncommitted work is protected - auto-claude won't touch it
- **Testability**: Run and test the feature before committing to it
- **Easy rollback**: Don't like it? Just discard the worktree
- **Parallel-safe**: Multiple workers can build without conflicts
If you have uncommitted changes, auto-build automatically uses isolated mode. With a clean working directory, you can choose between isolated (recommended) or direct mode.
If you have uncommitted changes, auto-claude automatically uses isolated mode. With a clean working directory, you can choose between isolated (recommended) or direct mode.
### Interactive Controls
@@ -243,10 +243,10 @@ Ctrl+C (twice)
**Alternative (file-based):**
```bash
# Create PAUSE file to pause after current session
touch auto-build/specs/001-name/PAUSE
touch auto-claude/specs/001-name/PAUSE
# Manually edit instructions file
echo "Focus on fixing the login bug first" > auto-build/specs/001-name/HUMAN_INPUT.md
echo "Focus on fixing the login bug first" > auto-claude/specs/001-name/HUMAN_INPUT.md
```
## Project Structure
@@ -254,8 +254,8 @@ echo "Focus on fixing the login bug first" > auto-build/specs/001-name/HUMAN_INP
```
your-project/
├── .worktrees/ # Created during build (git-ignored)
│ └── auto-build/ # Isolated workspace for AI coding
├── auto-build/
│ └── auto-claude/ # Isolated workspace for AI coding
├── auto-claude/
│ ├── run.py # Build entry point
│ ├── spec_runner.py # Spec creation orchestrator (8-phase pipeline)
│ ├── validate_spec.py # Spec validation with JSON schemas
@@ -312,7 +312,7 @@ your-project/
## Graphiti Memory Integration (Optional)
Auto-Build includes an optional **Graphiti-based persistent memory layer** that enables context retention across coding sessions. This uses FalkorDB as a graph database to store codebase patterns, session insights, and cross-session learnings.
Auto Claude includes an optional **Graphiti-based persistent memory layer** that enables context retention across coding sessions. This uses FalkorDB as a graph database to store codebase patterns, session insights, and cross-session learnings.
### Why Use Graphiti Memory?
@@ -350,13 +350,13 @@ OPENAI_API_KEY=sk-your-openai-key-here
**Step 4:** Verify it's working
```bash
python auto-build/run.py --list
python auto-claude/run.py --list
# Should show: "Graphiti memory: ENABLED"
```
### When Disabled
When `GRAPHITI_ENABLED` is not set (default), Auto-Build uses file-based memory only. This is the zero-dependency default that works out of the box.
When `GRAPHITI_ENABLED` is not set (default), Auto Claude uses file-based memory only. This is the zero-dependency default that works out of the box.
## Environment Variables
@@ -375,7 +375,7 @@ For parallel execution details:
- Best practices
- Troubleshooting
See [auto-build/PARALLEL_EXECUTION.md](auto-build/PARALLEL_EXECUTION.md)
See [auto-claude/PARALLEL_EXECUTION.md](auto-claude/PARALLEL_EXECUTION.md)
## Acknowledgments
@@ -1,10 +1,10 @@
# Auto-Build UI
# Auto Claude UI
A desktop application for managing AI-driven development tasks using the auto-build autonomous coding framework.
A desktop application for managing AI-driven development tasks using the Auto Claude autonomous coding framework.
## Overview
Auto-Build 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.
Auto Claude UI provides a visual Kanban board interface for creating, monitoring, and managing auto-claude tasks. It replaces the terminal-based workflow with an intuitive GUI while preserving all CLI functionality.
## Features
@@ -28,7 +28,7 @@ Auto-Build UI provides a visual Kanban board interface for creating, monitoring,
## Project Structure
```
auto-build-ui/
auto-claude-ui/
├── src/
│ ├── main/ # Electron main process
│ │ ├── index.ts # App entry point
@@ -68,13 +68,13 @@ auto-build-ui/
- Node.js 18+
- npm or pnpm
- Python 3.10+ (for auto-build backend)
- Python 3.10+ (for auto-claude backend)
### Installation
```bash
# Navigate to auto-build-ui directory
cd auto-build-ui
# Navigate to auto-claude-ui directory
cd auto-claude-ui
# Install dependencies
npm install
@@ -154,7 +154,7 @@ The application follows Electron security best practices:
## Environment Variables
- `CLAUDE_CODE_OAUTH_TOKEN`: OAuth token for Claude Code SDK (from auto-build/.env)
- `CLAUDE_CODE_OAUTH_TOKEN`: OAuth token for Claude Code SDK (from auto-claude/.env)
- `FALKORDB_URL`: FalkorDB connection URL (optional, defaults to localhost:6379)
## License
@@ -23,7 +23,7 @@ export async function launchElectronApp(): Promise<ElectronTestContext> {
...process.env,
NODE_ENV: 'test',
// Use test-specific user data directory
ELECTRON_USER_DATA_PATH: '/tmp/auto-build-ui-e2e'
ELECTRON_USER_DATA_PATH: '/tmp/auto-claude-ui-e2e'
}
});
@@ -13,7 +13,7 @@ import { mkdirSync, rmSync, existsSync, writeFileSync } from 'fs';
import path from 'path';
// Test data directory
const TEST_DATA_DIR = '/tmp/auto-build-ui-e2e';
const TEST_DATA_DIR = '/tmp/auto-claude-ui-e2e';
const TEST_PROJECT_DIR = path.join(TEST_DATA_DIR, 'test-project');
// Setup test environment
@@ -23,7 +23,7 @@ function setupTestEnvironment(): void {
}
mkdirSync(TEST_DATA_DIR, { recursive: true });
mkdirSync(TEST_PROJECT_DIR, { recursive: true });
mkdirSync(path.join(TEST_PROJECT_DIR, 'auto-build', 'specs'), { recursive: true });
mkdirSync(path.join(TEST_PROJECT_DIR, 'auto-claude', 'specs'), { recursive: true });
}
// Cleanup test environment
@@ -35,7 +35,7 @@ function cleanupTestEnvironment(): void {
// Helper to create a test spec
function createTestSpec(specId: string, status: 'pending' | 'in_progress' | 'completed' = 'pending'): void {
const specDir = path.join(TEST_PROJECT_DIR, 'auto-build', 'specs', specId);
const specDir = path.join(TEST_PROJECT_DIR, 'auto-claude', 'specs', specId);
mkdirSync(specDir, { recursive: true });
const chunkStatus = status === 'completed' ? 'completed' : status === 'in_progress' ? 'in_progress' : 'pending';
@@ -123,7 +123,7 @@ test.describe('Add Project Flow', () => {
await app.evaluate(({ dialog }) => {
dialog.showOpenDialog = async () => ({
canceled: false,
filePaths: ['/tmp/auto-build-ui-e2e/test-project']
filePaths: ['/tmp/auto-claude-ui-e2e/test-project']
});
});
@@ -199,7 +199,7 @@ test.describe('E2E Test Infrastructure', () => {
setupTestEnvironment();
createTestSpec('001-test-spec');
const specDir = path.join(TEST_PROJECT_DIR, 'auto-build', 'specs', '001-test-spec');
const specDir = path.join(TEST_PROJECT_DIR, 'auto-claude', 'specs', '001-test-spec');
expect(existsSync(specDir)).toBe(true);
expect(existsSync(path.join(specDir, 'implementation_plan.json'))).toBe(true);
expect(existsSync(path.join(specDir, 'spec.md'))).toBe(true);
@@ -214,7 +214,7 @@ test.describe('E2E Test Infrastructure', () => {
createTestSpec('002-in-progress', 'in_progress');
createTestSpec('003-completed', 'completed');
const specsDir = path.join(TEST_PROJECT_DIR, 'auto-build', 'specs');
const specsDir = path.join(TEST_PROJECT_DIR, 'auto-claude', 'specs');
expect(existsSync(path.join(specsDir, '001-pending'))).toBe(true);
expect(existsSync(path.join(specsDir, '002-in-progress'))).toBe(true);
expect(existsSync(path.join(specsDir, '003-completed'))).toBe(true);
@@ -232,8 +232,8 @@ test.describe('E2E Flow Verification (Mock-based)', () => {
const projectPath = TEST_PROJECT_DIR;
expect(existsSync(projectPath)).toBe(true);
// Check for auto-build directory detection
const autoBuildPath = path.join(projectPath, 'auto-build');
// Check for auto-claude directory detection
const autoBuildPath = path.join(projectPath, 'auto-claude');
expect(existsSync(autoBuildPath)).toBe(true);
cleanupTestEnvironment();
@@ -244,7 +244,7 @@ test.describe('E2E Flow Verification (Mock-based)', () => {
// Simulate what would happen when creating a task
const specId = '001-new-task';
const specDir = path.join(TEST_PROJECT_DIR, 'auto-build', 'specs', specId);
const specDir = path.join(TEST_PROJECT_DIR, 'auto-claude', 'specs', specId);
mkdirSync(specDir, { recursive: true });
// Write spec file
@@ -263,7 +263,7 @@ test.describe('E2E Flow Verification (Mock-based)', () => {
// Simulate status update when task starts
const planPath = path.join(
TEST_PROJECT_DIR,
'auto-build',
'auto-claude',
'specs',
'001-task',
'implementation_plan.json'
@@ -288,7 +288,7 @@ test.describe('E2E Flow Verification (Mock-based)', () => {
// Simulate approval
const qaReportPath = path.join(
TEST_PROJECT_DIR,
'auto-build',
'auto-claude',
'specs',
'001-review',
'qa_report.md'
@@ -311,7 +311,7 @@ test.describe('E2E Flow Verification (Mock-based)', () => {
// Simulate rejection
const fixRequestPath = path.join(
TEST_PROJECT_DIR,
'auto-build',
'auto-claude',
'specs',
'001-reject',
'QA_FIX_REQUEST.md'
@@ -1,9 +1,9 @@
{
"name": "auto-build-ui",
"name": "auto-claude-ui",
"version": "0.1.0",
"description": "Desktop UI for Auto-Build autonomous coding framework",
"description": "Desktop UI for Auto Claude autonomous coding framework",
"main": "./out/main/index.js",
"author": "Auto-Build Team",
"author": "Auto Claude Team",
"license": "MIT",
"scripts": {
"postinstall": "electron-rebuild",
@@ -26,6 +26,8 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-progress": "^1.1.8",
@@ -91,8 +93,8 @@
]
},
"build": {
"appId": "com.autobuild.ui",
"productName": "Auto-Build UI",
"appId": "com.autoclaude.ui",
"productName": "Auto Claude",
"directories": {
"output": "dist",
"buildResources": "resources"
@@ -21,6 +21,12 @@ importers:
'@dnd-kit/utilities':
specifier: ^3.2.2
version: 3.2.2([email protected])
'@radix-ui/react-checkbox':
specifier: ^1.1.4
version: 1.3.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-collapsible':
specifier: ^1.1.3
version: 1.1.12(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-dialog':
specifier: ^1.1.15
version: 1.1.15(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
@@ -660,6 +666,32 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/[email protected]':
resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/[email protected]':
resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/[email protected]':
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
peerDependencies:
@@ -4176,6 +4208,38 @@ snapshots:
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/[email protected])
'@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
'@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
'@radix-ui/react-presence': 1.1.5(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
'@radix-ui/react-use-previous': 1.1.1(@types/[email protected])([email protected])
'@radix-ui/react-use-size': 1.1.1(@types/[email protected])([email protected])
react: 19.2.1
react-dom: 19.2.1([email protected])
optionalDependencies:
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/[email protected])
'@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
'@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
'@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
'@radix-ui/react-presence': 1.1.5(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
'@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
react: 19.2.1
react-dom: 19.2.1([email protected])
optionalDependencies:
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/[email protected])
'@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
@@ -32,15 +32,15 @@ vi.mock('child_process', () => ({
// Setup test directories
function setupTestDirs(): void {
mkdirSync(TEST_PROJECT_PATH, { recursive: true });
mkdirSync(path.join(TEST_PROJECT_PATH, 'auto-build'), { recursive: true });
mkdirSync(path.join(TEST_PROJECT_PATH, 'auto-claude'), { recursive: true });
// Create mock spec_runner.py
writeFileSync(
path.join(TEST_PROJECT_PATH, 'auto-build', 'spec_runner.py'),
path.join(TEST_PROJECT_PATH, 'auto-claude', 'spec_runner.py'),
'# Mock spec runner\nprint("Starting spec creation")'
);
// Create mock run.py
writeFileSync(
path.join(TEST_PROJECT_PATH, 'auto-build', 'run.py'),
path.join(TEST_PROJECT_PATH, 'auto-claude', 'run.py'),
'# Mock run.py\nprint("Starting task execution")'
);
}
@@ -6,7 +6,7 @@ import { mkdirSync, rmSync, existsSync } from 'fs';
import path from 'path';
// Test data directory for isolated file operations
export const TEST_DATA_DIR = '/tmp/auto-build-ui-tests';
export const TEST_DATA_DIR = '/tmp/auto-claude-ui-tests';
// Create fresh test directory before each test
beforeEach(() => {
@@ -59,7 +59,7 @@ vi.mock('electron', () => {
// Setup test project structure
function setupTestProject(): void {
mkdirSync(TEST_PROJECT_PATH, { recursive: true });
mkdirSync(path.join(TEST_PROJECT_PATH, 'auto-build', 'specs'), { recursive: true });
mkdirSync(path.join(TEST_PROJECT_PATH, 'auto-claude', 'specs'), { recursive: true });
}
// Cleanup test directories
@@ -296,7 +296,7 @@ describe('IPC Handlers', () => {
const projectId = (addResult as { data: { id: string } }).data.id;
// Create a spec directory with implementation plan
const specDir = path.join(TEST_PROJECT_PATH, 'auto-build', 'specs', '001-test-feature');
const specDir = path.join(TEST_PROJECT_PATH, 'auto-claude', 'specs', '001-test-feature');
mkdirSync(specDir, { recursive: true });
writeFileSync(path.join(specDir, 'implementation_plan.json'), JSON.stringify({
feature: 'Test Feature',
@@ -82,16 +82,16 @@ describe('ProjectStore', () => {
expect(project1.id).toBe(project2.id);
});
it('should detect auto-build directory if present', async () => {
// Create auto-build directory
mkdirSync(path.join(TEST_PROJECT_PATH, 'auto-build'), { recursive: true });
it('should detect auto-claude directory if present', async () => {
// Create auto-claude directory
mkdirSync(path.join(TEST_PROJECT_PATH, 'auto-claude'), { recursive: true });
const { ProjectStore } = await import('../project-store');
const store = new ProjectStore();
const project = store.addProject(TEST_PROJECT_PATH);
expect(project.autoBuildPath).toBe('auto-build');
expect(project.autoBuildPath).toBe('auto-claude');
});
it('should set empty autoBuildPath if not present', async () => {
@@ -279,7 +279,7 @@ describe('ProjectStore', () => {
it('should read tasks from filesystem correctly', async () => {
// Create spec directory structure
const specsDir = path.join(TEST_PROJECT_PATH, 'auto-build', 'specs', '001-test-feature');
const specsDir = path.join(TEST_PROJECT_PATH, 'auto-claude', 'specs', '001-test-feature');
mkdirSync(specsDir, { recursive: true });
const plan = {
@@ -325,7 +325,7 @@ describe('ProjectStore', () => {
});
it('should determine status as backlog when no chunks completed', async () => {
const specsDir = path.join(TEST_PROJECT_PATH, 'auto-build', 'specs', '002-pending');
const specsDir = path.join(TEST_PROJECT_PATH, 'auto-claude', 'specs', '002-pending');
mkdirSync(specsDir, { recursive: true });
const plan = {
@@ -364,7 +364,7 @@ describe('ProjectStore', () => {
});
it('should determine status as ai_review when all chunks completed', async () => {
const specsDir = path.join(TEST_PROJECT_PATH, 'auto-build', 'specs', '003-complete');
const specsDir = path.join(TEST_PROJECT_PATH, 'auto-claude', 'specs', '003-complete');
mkdirSync(specsDir, { recursive: true });
const plan = {
@@ -403,7 +403,7 @@ describe('ProjectStore', () => {
});
it('should determine status as human_review when QA report rejected', async () => {
const specsDir = path.join(TEST_PROJECT_PATH, 'auto-build', 'specs', '004-rejected');
const specsDir = path.join(TEST_PROJECT_PATH, 'auto-claude', 'specs', '004-rejected');
mkdirSync(specsDir, { recursive: true });
const plan = {
@@ -446,7 +446,7 @@ describe('ProjectStore', () => {
});
it('should determine status as done when QA report approved', async () => {
const specsDir = path.join(TEST_PROJECT_PATH, 'auto-build', 'specs', '005-approved');
const specsDir = path.join(TEST_PROJECT_PATH, 'auto-claude', 'specs', '005-approved');
mkdirSync(specsDir, { recursive: true });
const plan = {
@@ -17,19 +17,19 @@ export interface AgentManagerEvents {
}
/**
* Manages Python subprocess spawning for auto-build agents
* Manages Python subprocess spawning for auto-claude agents
*/
export class AgentManager extends EventEmitter {
private processes: Map<string, AgentProcess> = new Map();
private pythonPath: string = 'python3';
private autoBuildSourcePath: string = ''; // Source auto-build repo location
private autoBuildSourcePath: string = ''; // Source auto-claude repo location
constructor() {
super();
}
/**
* Configure paths for Python and auto-build source
* Configure paths for Python and auto-claude source
*/
configure(pythonPath?: string, autoBuildSourcePath?: string): void {
if (pythonPath) {
@@ -41,7 +41,7 @@ export class AgentManager extends EventEmitter {
}
/**
* Get the auto-build source path (detects automatically if not configured)
* Get the auto-claude source path (detects automatically if not configured)
*/
private getAutoBuildSourcePath(): string | null {
// If manually configured, use that
@@ -51,12 +51,12 @@ export class AgentManager extends EventEmitter {
// Auto-detect from app location
const possiblePaths = [
// Dev mode: from dist/main -> ../../auto-build (sibling to auto-build-ui)
path.resolve(__dirname, '..', '..', '..', 'auto-build'),
// Dev mode: from dist/main -> ../../auto-claude (sibling to auto-claude-ui)
path.resolve(__dirname, '..', '..', '..', 'auto-claude'),
// Alternative: from app root
path.resolve(app.getAppPath(), '..', 'auto-build'),
path.resolve(app.getAppPath(), '..', 'auto-claude'),
// If running from repo root
path.resolve(process.cwd(), 'auto-build')
path.resolve(process.cwd(), 'auto-claude')
];
for (const p of possiblePaths) {
@@ -68,7 +68,7 @@ export class AgentManager extends EventEmitter {
}
/**
* Load environment variables from auto-build .env file
* Load environment variables from auto-claude .env file
*/
private loadAutoBuildEnv(): Record<string, string> {
const autoBuildSource = this.getAutoBuildSourcePath();
@@ -121,7 +121,7 @@ export class AgentManager extends EventEmitter {
projectPath: string,
taskDescription: string
): void {
const autoBuildDir = path.join(projectPath, 'auto-build');
const autoBuildDir = path.join(projectPath, 'auto-claude');
const specRunnerPath = path.join(autoBuildDir, 'spec_runner.py');
const args = [specRunnerPath, '--task', taskDescription];
@@ -138,7 +138,7 @@ export class AgentManager extends EventEmitter {
specId: string,
options: { parallel?: boolean; workers?: number } = {}
): void {
const autoBuildDir = path.join(projectPath, 'auto-build');
const autoBuildDir = path.join(projectPath, 'auto-claude');
const runPath = path.join(autoBuildDir, 'run.py');
const args = [runPath, '--spec', specId];
@@ -158,7 +158,7 @@ export class AgentManager extends EventEmitter {
projectPath: string,
specId: string
): void {
const autoBuildDir = path.join(projectPath, 'auto-build');
const autoBuildDir = path.join(projectPath, 'auto-claude');
const runPath = path.join(autoBuildDir, 'run.py');
const args = [runPath, '--spec', specId, '--qa'];
@@ -174,7 +174,7 @@ export class AgentManager extends EventEmitter {
projectPath: string,
refresh: boolean = false
): void {
// Use source auto-build path (the repo), not the project's auto-build
// Use source auto-claude path (the repo), not the project's auto-claude
const autoBuildSource = this.getAutoBuildSourcePath();
if (!autoBuildSource) {
@@ -213,7 +213,7 @@ export class AgentManager extends EventEmitter {
},
refresh: boolean = false
): void {
// Use source auto-build path (the repo), not the project's auto-build
// Use source auto-claude path (the repo), not the project's auto-claude
const autoBuildSource = this.getAutoBuildSourcePath();
if (!autoBuildSource) {
@@ -267,18 +267,18 @@ export class AgentManager extends EventEmitter {
// Kill existing process for this project if any
this.killTask(projectId);
// Run from auto-build source directory so imports work correctly
// Run from auto-claude source directory so imports work correctly
const autoBuildSource = this.getAutoBuildSourcePath();
const cwd = autoBuildSource || process.cwd();
// Load environment variables from auto-build .env file
// Load environment variables from auto-claude .env file
const autoBuildEnv = this.loadAutoBuildEnv();
const childProcess = spawn(this.pythonPath, args, {
cwd,
env: {
...process.env,
...autoBuildEnv, // Include auto-build .env variables (like CLAUDE_CODE_OAUTH_TOKEN)
...autoBuildEnv, // Include auto-claude .env variables (like CLAUDE_CODE_OAUTH_TOKEN)
PYTHONUNBUFFERED: '1'
}
});
@@ -397,18 +397,18 @@ export class AgentManager extends EventEmitter {
// Kill existing process for this project if any
this.killTask(projectId);
// Run from auto-build source directory so imports work correctly
// Run from auto-claude source directory so imports work correctly
const autoBuildSource = this.getAutoBuildSourcePath();
const cwd = autoBuildSource || process.cwd();
// Load environment variables from auto-build .env file
// Load environment variables from auto-claude .env file
const autoBuildEnv = this.loadAutoBuildEnv();
const childProcess = spawn(this.pythonPath, args, {
cwd,
env: {
...process.env,
...autoBuildEnv, // Include auto-build .env variables (like CLAUDE_CODE_OAUTH_TOKEN)
...autoBuildEnv, // Include auto-claude .env variables (like CLAUDE_CODE_OAUTH_TOKEN)
PYTHONUNBUFFERED: '1'
}
});
@@ -1,8 +1,8 @@
/**
* Auto-Build Source Updater
*
* Checks GitHub for updates to the auto-build framework and downloads them.
* This allows users to get new auto-build features without requiring a full app update.
* Checks GitHub for updates to the auto-claude framework and downloads them.
* This allows users to get new auto-claude features without requiring a full app update.
*
* Update flow:
* 1. Check GitHub for latest VERSION file
@@ -26,9 +26,9 @@ const execAsync = promisify(exec);
*/
const GITHUB_CONFIG = {
owner: 'anthropics', // Update to actual repo owner
repo: 'auto-build', // Update to actual repo name
repo: 'auto-claude', // Update to actual repo name
branch: 'main',
autoBuildPath: 'auto-build' // Path within repo
autoBuildPath: 'auto-claude' // Path within repo
};
/**
@@ -61,21 +61,21 @@ export type UpdateProgressCallback = (progress: {
}) => void;
/**
* Get the path to the bundled auto-build source
* Get the path to the bundled auto-claude source
*/
export function getBundledSourcePath(): string {
// In production, use app resources
// In development, use the repo's auto-build folder
// In development, use the repo's auto-claude folder
if (app.isPackaged) {
return path.join(process.resourcesPath, 'auto-build');
return path.join(process.resourcesPath, 'auto-claude');
}
// Development mode - look for auto-build in various locations
// Development mode - look for auto-claude in various locations
const possiblePaths = [
path.join(app.getAppPath(), '..', 'auto-build'),
path.join(app.getAppPath(), '..', '..', 'auto-build'),
path.join(process.cwd(), 'auto-build'),
path.join(process.cwd(), '..', 'auto-build')
path.join(app.getAppPath(), '..', 'auto-claude'),
path.join(app.getAppPath(), '..', '..', 'auto-claude'),
path.join(process.cwd(), 'auto-claude'),
path.join(process.cwd(), '..', 'auto-claude')
];
for (const p of possiblePaths) {
@@ -85,14 +85,14 @@ export function getBundledSourcePath(): string {
}
// Fallback
return path.join(app.getAppPath(), '..', 'auto-build');
return path.join(app.getAppPath(), '..', 'auto-claude');
}
/**
* Get the path for storing downloaded updates
*/
function getUpdateCachePath(): string {
return path.join(app.getPath('userData'), 'auto-build-updates');
return path.join(app.getPath('userData'), 'auto-claude-updates');
}
/**
@@ -276,7 +276,7 @@ function downloadFile(
}
/**
* Download and apply the latest auto-build update
* Download and apply the latest auto-claude update
*
* Note: In production, this updates the bundled source in userData.
* For packaged apps, we can't modify resourcesPath directly,
@@ -301,7 +301,7 @@ export async function downloadAndApplyUpdate(
// Get download URL for the tarball
const tarballUrl = `https://api.github.com/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/tarball/${GITHUB_CONFIG.branch}`;
const tarballPath = path.join(cachePath, 'auto-build-update.tar.gz');
const tarballPath = path.join(cachePath, 'auto-claude-update.tar.gz');
const extractPath = path.join(cachePath, 'extracted');
// Clean up previous extraction
@@ -333,7 +333,7 @@ export async function downloadAndApplyUpdate(
// Extract the tarball
await extractTarball(tarballPath, extractPath);
// Find the auto-build folder in extracted content
// Find the auto-claude folder in extracted content
// GitHub tarballs have a root folder like "owner-repo-hash/"
const extractedDirs = readdirSync(extractPath);
if (extractedDirs.length === 0) {
@@ -344,7 +344,7 @@ export async function downloadAndApplyUpdate(
const autoBuildSource = path.join(rootDir, GITHUB_CONFIG.autoBuildPath);
if (!existsSync(autoBuildSource)) {
throw new Error('auto-build folder not found in download');
throw new Error('auto-claude folder not found in download');
}
// Determine where to install the update
@@ -352,7 +352,7 @@ export async function downloadAndApplyUpdate(
if (app.isPackaged) {
// For packaged apps, store in userData as a source override
targetPath = path.join(app.getPath('userData'), 'auto-build-source');
targetPath = path.join(app.getPath('userData'), 'auto-claude-source');
} else {
// In development, update the actual source
targetPath = getBundledSourcePath();
@@ -503,7 +503,7 @@ function copyDirectoryRecursive(
export function getEffectiveSourcePath(): string {
if (app.isPackaged) {
// Check for user-updated source first
const overridePath = path.join(app.getPath('userData'), 'auto-build-source');
const overridePath = path.join(app.getPath('userData'), 'auto-claude-source');
if (existsSync(overridePath)) {
return overridePath;
}
@@ -520,7 +520,7 @@ export function hasPendingSourceUpdate(): boolean {
return false;
}
const overridePath = path.join(app.getPath('userData'), 'auto-build-source');
const overridePath = path.join(app.getPath('userData'), 'auto-claude-source');
const metadataPath = path.join(overridePath, '.update-metadata.json');
if (!existsSync(metadataPath)) {
@@ -0,0 +1,582 @@
import { EventEmitter } from 'events';
import path from 'path';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { spawn } from 'child_process';
import { app } from 'electron';
import { AUTO_BUILD_PATHS, DEFAULT_CHANGELOG_PATH } from '../shared/constants';
import type {
ChangelogTask,
TaskSpecContent,
ChangelogGenerationRequest,
ChangelogGenerationResult,
ChangelogSaveRequest,
ChangelogSaveResult,
ChangelogGenerationProgress,
ExistingChangelog,
Task,
ImplementationPlan
} from '../shared/types';
/**
* Service for generating changelogs from completed tasks
*/
export class ChangelogService extends EventEmitter {
private pythonPath: string = 'python3';
private autoBuildSourcePath: string = '';
private generationProcesses: Map<string, ReturnType<typeof spawn>> = new Map();
constructor() {
super();
}
/**
* Configure paths for Python and auto-claude source
*/
configure(pythonPath?: string, autoBuildSourcePath?: string): void {
if (pythonPath) {
this.pythonPath = pythonPath;
}
if (autoBuildSourcePath) {
this.autoBuildSourcePath = autoBuildSourcePath;
}
}
/**
* Get the auto-claude source path (detects automatically if not configured)
*/
private getAutoBuildSourcePath(): string | null {
if (this.autoBuildSourcePath && existsSync(this.autoBuildSourcePath)) {
return this.autoBuildSourcePath;
}
const possiblePaths = [
path.resolve(__dirname, '..', '..', '..', 'auto-claude'),
path.resolve(app.getAppPath(), '..', 'auto-claude'),
path.resolve(process.cwd(), 'auto-claude')
];
for (const p of possiblePaths) {
if (existsSync(p) && existsSync(path.join(p, 'VERSION'))) {
return p;
}
}
return null;
}
/**
* Load environment variables from auto-claude .env file
*/
private loadAutoBuildEnv(): Record<string, string> {
const autoBuildSource = this.getAutoBuildSourcePath();
if (!autoBuildSource) return {};
const envPath = path.join(autoBuildSource, '.env');
if (!existsSync(envPath)) return {};
try {
const envContent = readFileSync(envPath, 'utf-8');
const envVars: Record<string, string> = {};
for (const line of envContent.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIndex = trimmed.indexOf('=');
if (eqIndex > 0) {
const key = trimmed.substring(0, eqIndex).trim();
let value = trimmed.substring(eqIndex + 1).trim();
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
envVars[key] = value;
}
}
return envVars;
} catch {
return {};
}
}
/**
* Get completed tasks from a project
*/
getCompletedTasks(projectPath: string, tasks: Task[]): ChangelogTask[] {
const specsDir = path.join(projectPath, AUTO_BUILD_PATHS.SPECS_DIR);
return tasks
.filter(task => task.status === 'done')
.map(task => {
const specDir = path.join(specsDir, task.specId);
const hasSpecs = existsSync(specDir) && existsSync(path.join(specDir, AUTO_BUILD_PATHS.SPEC_FILE));
return {
id: task.id,
specId: task.specId,
title: task.title,
description: task.description,
completedAt: task.updatedAt,
hasSpecs
};
})
.sort((a, b) => new Date(b.completedAt).getTime() - new Date(a.completedAt).getTime());
}
/**
* Load spec files for given tasks
*/
async loadTaskSpecs(projectPath: string, taskIds: string[], tasks: Task[]): Promise<TaskSpecContent[]> {
const specsDir = path.join(projectPath, AUTO_BUILD_PATHS.SPECS_DIR);
const results: TaskSpecContent[] = [];
for (const taskId of taskIds) {
const task = tasks.find(t => t.id === taskId);
if (!task) continue;
const specDir = path.join(specsDir, task.specId);
const content: TaskSpecContent = {
taskId,
specId: task.specId
};
try {
// Load spec.md
const specPath = path.join(specDir, AUTO_BUILD_PATHS.SPEC_FILE);
if (existsSync(specPath)) {
content.spec = readFileSync(specPath, 'utf-8');
}
// Load requirements.json
const requirementsPath = path.join(specDir, AUTO_BUILD_PATHS.REQUIREMENTS);
if (existsSync(requirementsPath)) {
content.requirements = JSON.parse(readFileSync(requirementsPath, 'utf-8'));
}
// Load qa_report.md
const qaReportPath = path.join(specDir, AUTO_BUILD_PATHS.QA_REPORT);
if (existsSync(qaReportPath)) {
content.qaReport = readFileSync(qaReportPath, 'utf-8');
}
// Load implementation_plan.json
const planPath = path.join(specDir, AUTO_BUILD_PATHS.IMPLEMENTATION_PLAN);
if (existsSync(planPath)) {
content.implementationPlan = JSON.parse(readFileSync(planPath, 'utf-8')) as ImplementationPlan;
}
} catch (error) {
content.error = error instanceof Error ? error.message : 'Failed to load spec files';
}
results.push(content);
}
return results;
}
/**
* Generate changelog using Claude AI
*/
generateChangelog(
projectId: string,
projectPath: string,
request: ChangelogGenerationRequest,
specs: TaskSpecContent[]
): void {
// Kill existing process if any
this.cancelGeneration(projectId);
// Emit initial progress
this.emitProgress(projectId, {
stage: 'loading_specs',
progress: 10,
message: 'Preparing changelog generation...'
});
// Build the prompt for Claude
const prompt = this.buildChangelogPrompt(request, specs);
// Use Claude Code SDK via subprocess
const autoBuildSource = this.getAutoBuildSourcePath();
if (!autoBuildSource) {
this.emitError(projectId, 'Auto-build source path not found');
return;
}
// Create a temporary Python script to call Claude
const script = this.createGenerationScript(prompt, request);
this.emitProgress(projectId, {
stage: 'generating',
progress: 30,
message: 'Generating changelog with Claude AI...'
});
const autoBuildEnv = this.loadAutoBuildEnv();
const childProcess = spawn(this.pythonPath, ['-c', script], {
cwd: autoBuildSource,
env: {
...process.env,
...autoBuildEnv,
PYTHONUNBUFFERED: '1'
}
});
this.generationProcesses.set(projectId, childProcess);
let output = '';
let errorOutput = '';
childProcess.stdout?.on('data', (data: Buffer) => {
output += data.toString();
this.emitProgress(projectId, {
stage: 'generating',
progress: 50,
message: 'Generating changelog content...'
});
});
childProcess.stderr?.on('data', (data: Buffer) => {
errorOutput += data.toString();
});
childProcess.on('exit', (code: number | null) => {
this.generationProcesses.delete(projectId);
if (code === 0 && output.trim()) {
this.emitProgress(projectId, {
stage: 'formatting',
progress: 90,
message: 'Formatting changelog...'
});
// Extract changelog from output
const changelog = this.extractChangelog(output.trim());
this.emitProgress(projectId, {
stage: 'complete',
progress: 100,
message: 'Changelog generation complete'
});
const result: ChangelogGenerationResult = {
success: true,
changelog,
version: request.version,
tasksIncluded: request.taskIds.length
};
this.emit('generation-complete', projectId, result);
} else {
const error = errorOutput || `Generation failed with exit code ${code}`;
this.emitError(projectId, error);
}
});
childProcess.on('error', (err: Error) => {
this.generationProcesses.delete(projectId);
this.emitError(projectId, err.message);
});
}
/**
* Build the prompt for changelog generation
*/
private buildChangelogPrompt(
request: ChangelogGenerationRequest,
specs: TaskSpecContent[]
): string {
const audienceInstructions = {
'technical': `You are a technical documentation specialist creating a changelog for software developers.
Use precise technical language. Include API changes, architecture details, affected modules.
Note any breaking changes explicitly. Include migration steps if needed.`,
'user-facing': `You are a product manager writing release notes for end users who may not be technical.
Use clear, non-technical language. Focus on user benefits and value.
Explain "what" changed, not "how". Use active voice and positive framing.`,
'marketing': `You are a marketing specialist writing release notes that emphasize value and benefits.
Focus on outcomes and user impact. Use compelling language that highlights improvements.
Emphasize competitive advantages and user success stories.`
};
const formatInstructions = {
'keep-a-changelog': `Use Keep-a-Changelog format with these sections:
## [${request.version}] - ${request.date}
### Added
- [New features]
### Changed
- [Modifications]
### Fixed
- [Bug fixes]
### Removed
- [Deprecations/removals]`,
'simple-list': `Use a simple, clean format:
# Release v${request.version} (${request.date})
**New Features:**
- [List features]
**Improvements:**
- [List improvements]
**Bug Fixes:**
- [List fixes]`,
'github-release': `Use GitHub Release format with emojis:
## 🎉 What's New in v${request.version}
### ✨ New Features
- 🚀 **Feature Name**: Description
### 🔧 Improvements
- ⚡ **Improvement**: Description
### 🐛 Bug Fixes
- Fixed [issue description]`
};
// Build task context
const taskContext = specs.map(spec => {
let context = `## Task: ${spec.specId}\n`;
if (spec.spec) {
// Extract key parts from spec.md
context += `### Specification:\n${spec.spec.substring(0, 2000)}...\n\n`;
}
if (spec.qaReport) {
context += `### QA Validation:\n${spec.qaReport.substring(0, 500)}...\n\n`;
}
if (spec.implementationPlan) {
context += `### Implementation: ${spec.implementationPlan.feature}\n`;
context += `Type: ${spec.implementationPlan.workflow_type}\n`;
}
return context;
}).join('\n---\n');
return `${audienceInstructions[request.audience]}
Generate a changelog entry in the following format:
${formatInstructions[request.format]}
CONTEXT - The following tasks have been completed:
${taskContext}
${request.customInstructions ? `ADDITIONAL INSTRUCTIONS: ${request.customInstructions}` : ''}
Generate only the changelog content. Be comprehensive but concise. Do not include any explanation or preamble - just the formatted changelog.`;
}
/**
* Create Python script for Claude generation
*/
private createGenerationScript(prompt: string, _request: ChangelogGenerationRequest): string {
// Escape the prompt for Python string
const escapedPrompt = prompt
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n');
return `
import subprocess
import sys
prompt = """${escapedPrompt}"""
# Use Claude Code CLI to generate
result = subprocess.run(
['claude', '-p', prompt, '--output-format', 'text'],
capture_output=True,
text=True,
timeout=120
)
if result.returncode == 0:
print(result.stdout)
else:
print(result.stderr, file=sys.stderr)
sys.exit(1)
`;
}
/**
* Extract changelog content from Claude output
*/
private extractChangelog(output: string): string {
// Claude output should be the changelog directly
// Clean up any potential wrapper text
let changelog = output.trim();
// Remove any "Here's the changelog:" or similar prefixes
const prefixes = [
/^Here['']s the changelog[:\s]*/i,
/^The changelog[:\s]*/i,
/^Changelog[:\s]*/i
];
for (const prefix of prefixes) {
changelog = changelog.replace(prefix, '');
}
return changelog.trim();
}
/**
* Save changelog to file
*/
saveChangelog(
projectPath: string,
request: ChangelogSaveRequest
): ChangelogSaveResult {
const filePath = request.filePath
? path.join(projectPath, request.filePath)
: path.join(projectPath, DEFAULT_CHANGELOG_PATH);
let finalContent = request.content;
if (request.mode === 'prepend' && existsSync(filePath)) {
const existing = readFileSync(filePath, 'utf-8');
// Add separator between new and existing content
finalContent = `${request.content}\n\n${existing}`;
} else if (request.mode === 'append' && existsSync(filePath)) {
const existing = readFileSync(filePath, 'utf-8');
finalContent = `${existing}\n\n${request.content}`;
}
writeFileSync(filePath, finalContent, 'utf-8');
return {
filePath,
bytesWritten: Buffer.byteLength(finalContent, 'utf-8')
};
}
/**
* Read existing changelog file
*/
readExistingChangelog(projectPath: string): ExistingChangelog {
const filePath = path.join(projectPath, DEFAULT_CHANGELOG_PATH);
if (!existsSync(filePath)) {
return { exists: false };
}
try {
const content = readFileSync(filePath, 'utf-8');
// Try to extract last version using common patterns
const versionPatterns = [
/##\s*\[(\d+\.\d+\.\d+)\]/, // Keep-a-changelog format
/v(\d+\.\d+\.\d+)/, // v1.2.3 format
/Version\s+(\d+\.\d+\.\d+)/i // Version 1.2.3 format
];
let lastVersion: string | undefined;
for (const pattern of versionPatterns) {
const match = content.match(pattern);
if (match) {
lastVersion = match[1];
break;
}
}
return {
exists: true,
content,
lastVersion
};
} catch (error) {
return {
exists: true,
error: error instanceof Error ? error.message : 'Failed to read changelog'
};
}
}
/**
* Suggest next version based on task types
*/
suggestVersion(specs: TaskSpecContent[], currentVersion?: string): string {
// Default starting version
if (!currentVersion) {
return '1.0.0';
}
const parts = currentVersion.split('.').map(Number);
if (parts.length !== 3 || parts.some(isNaN)) {
return '1.0.0';
}
let [major, minor, patch] = parts;
// Analyze specs for version increment decision
let hasBreakingChanges = false;
let hasNewFeatures = false;
for (const spec of specs) {
const content = (spec.spec || '').toLowerCase();
if (content.includes('breaking change') || content.includes('breaking:')) {
hasBreakingChanges = true;
}
if (spec.implementationPlan?.workflow_type === 'new_feature' ||
content.includes('new feature') ||
content.includes('## added')) {
hasNewFeatures = true;
}
}
if (hasBreakingChanges) {
return `${major + 1}.0.0`;
} else if (hasNewFeatures) {
return `${major}.${minor + 1}.0`;
} else {
return `${major}.${minor}.${patch + 1}`;
}
}
/**
* Cancel ongoing generation
*/
cancelGeneration(projectId: string): boolean {
const process = this.generationProcesses.get(projectId);
if (process) {
process.kill('SIGTERM');
this.generationProcesses.delete(projectId);
return true;
}
return false;
}
/**
* Emit progress update
*/
private emitProgress(projectId: string, progress: ChangelogGenerationProgress): void {
this.emit('generation-progress', projectId, progress);
}
/**
* Emit error
*/
private emitError(projectId: string, error: string): void {
this.emit('generation-progress', projectId, {
stage: 'error',
progress: 0,
message: error,
error
});
this.emit('generation-error', projectId, error);
}
}
// Export singleton instance
export const changelogService = new ChangelogService();
@@ -58,7 +58,8 @@ import {
downloadAndApplyUpdate,
getBundledVersion,
getEffectiveSourcePath
} from './auto-build-updater';
} from './auto-claude-updater';
import { changelogService } from './changelog-service';
import type { AutoBuildSourceUpdateProgress } from '../shared/types';
/**
@@ -131,22 +132,22 @@ export function setupIpcHandlers(
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
/**
* Auto-detect the auto-build source path relative to the app location
* In dev: auto-build-ui/../auto-build
* Auto-detect the auto-claude source path relative to the app location
* In dev: auto-claude-ui/../auto-claude
* In prod: Could be bundled or configured
*/
const detectAutoBuildSourcePath = (): string | null => {
// Try relative to app directory (works in dev and if repo structure is maintained)
// __dirname in main process points to out/main in dev
const possiblePaths = [
// Dev mode: from out/main -> ../../../auto-build (sibling to auto-build-ui)
path.resolve(__dirname, '..', '..', '..', 'auto-build'),
// Dev mode: from out/main -> ../../../auto-claude (sibling to auto-claude-ui)
path.resolve(__dirname, '..', '..', '..', 'auto-claude'),
// Alternative: from app root (useful in some packaged scenarios)
path.resolve(app.getAppPath(), '..', 'auto-build'),
path.resolve(app.getAppPath(), '..', 'auto-claude'),
// If running from repo root
path.resolve(process.cwd(), 'auto-build'),
path.resolve(process.cwd(), 'auto-claude'),
// Try one more level up (in case of different build output structure)
path.resolve(__dirname, '..', '..', 'auto-build')
path.resolve(__dirname, '..', '..', 'auto-claude')
];
console.log('[Auto-Build] Detecting source path, checking:', possiblePaths);
@@ -162,7 +163,7 @@ export function setupIpcHandlers(
};
/**
* Get the configured auto-build source path from settings, or auto-detect
* Get the configured auto-claude source path from settings, or auto-detect
*/
const getAutoBuildSourcePath = (): string | null => {
// First check if manually configured
@@ -203,7 +204,7 @@ export function setupIpcHandlers(
if (result.success) {
// Update project's autoBuildPath
projectStore.updateAutoBuildPath(projectId, '.auto-build');
projectStore.updateAutoBuildPath(projectId, '.auto-claude');
}
return { success: result.success, data: result, error: result.error };
@@ -3180,4 +3181,121 @@ ${issue.body || 'No description provided.'}
mainWindow.webContents.send(IPC_CHANNELS.IDEATION_ERROR, projectId, error);
}
});
// ============================================
// Changelog Operations
// ============================================
ipcMain.handle(
IPC_CHANNELS.CHANGELOG_GET_DONE_TASKS,
async (_, projectId: string): Promise<IPCResult<import('../shared/types').ChangelogTask[]>> => {
const project = projectStore.getProject(projectId);
if (!project) {
return { success: false, error: 'Project not found' };
}
const tasks = projectStore.getTasks(projectId);
const doneTasks = changelogService.getCompletedTasks(project.path, tasks);
return { success: true, data: doneTasks };
}
);
ipcMain.handle(
IPC_CHANNELS.CHANGELOG_LOAD_TASK_SPECS,
async (_, projectId: string, taskIds: string[]): Promise<IPCResult<import('../shared/types').TaskSpecContent[]>> => {
const project = projectStore.getProject(projectId);
if (!project) {
return { success: false, error: 'Project not found' };
}
const tasks = projectStore.getTasks(projectId);
const specs = await changelogService.loadTaskSpecs(project.path, taskIds, tasks);
return { success: true, data: specs };
}
);
ipcMain.on(
IPC_CHANNELS.CHANGELOG_GENERATE,
async (_, request: import('../shared/types').ChangelogGenerationRequest) => {
const mainWindow = getMainWindow();
if (!mainWindow) return;
const project = projectStore.getProject(request.projectId);
if (!project) {
mainWindow.webContents.send(
IPC_CHANNELS.CHANGELOG_GENERATION_ERROR,
request.projectId,
'Project not found'
);
return;
}
// Load specs for selected tasks
const tasks = projectStore.getTasks(request.projectId);
const specs = await changelogService.loadTaskSpecs(project.path, request.taskIds, tasks);
// Start generation
changelogService.generateChangelog(request.projectId, project.path, request, specs);
}
);
ipcMain.handle(
IPC_CHANNELS.CHANGELOG_SAVE,
async (_, request: import('../shared/types').ChangelogSaveRequest): Promise<IPCResult<import('../shared/types').ChangelogSaveResult>> => {
const project = projectStore.getProject(request.projectId);
if (!project) {
return { success: false, error: 'Project not found' };
}
try {
const result = changelogService.saveChangelog(project.path, request);
return { success: true, data: result };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to save changelog'
};
}
}
);
ipcMain.handle(
IPC_CHANNELS.CHANGELOG_READ_EXISTING,
async (_, projectId: string): Promise<IPCResult<import('../shared/types').ExistingChangelog>> => {
const project = projectStore.getProject(projectId);
if (!project) {
return { success: false, error: 'Project not found' };
}
const result = changelogService.readExistingChangelog(project.path);
return { success: true, data: result };
}
);
// ============================================
// Changelog Agent Events → Renderer
// ============================================
changelogService.on('generation-progress', (projectId: string, progress: import('../shared/types').ChangelogGenerationProgress) => {
const mainWindow = getMainWindow();
if (mainWindow) {
mainWindow.webContents.send(IPC_CHANNELS.CHANGELOG_GENERATION_PROGRESS, projectId, progress);
}
});
changelogService.on('generation-complete', (projectId: string, result: import('../shared/types').ChangelogGenerationResult) => {
const mainWindow = getMainWindow();
if (mainWindow) {
mainWindow.webContents.send(IPC_CHANNELS.CHANGELOG_GENERATION_COMPLETE, projectId, result);
}
});
changelogService.on('generation-error', (projectId: string, error: string) => {
const mainWindow = getMainWindow();
if (mainWindow) {
mainWindow.webContents.send(IPC_CHANNELS.CHANGELOG_GENERATION_ERROR, projectId, error);
}
});
}
@@ -3,7 +3,7 @@ import path from 'path';
import crypto from 'crypto';
/**
* Files and directories to exclude when copying auto-build
* Files and directories to exclude when copying auto-claude
*/
const EXCLUDE_PATTERNS = [
'__pycache__',
@@ -23,7 +23,7 @@ const PRESERVE_ON_UPDATE = [
];
/**
* Version metadata stored in .auto-build/.version.json
* Version metadata stored in .auto-claude/.version.json
*/
export interface VersionMetadata {
version: string;
@@ -151,7 +151,7 @@ function calculateDirectoryHash(dirPath: string): string {
}
/**
* Read version from VERSION file in auto-build source
* Read version from VERSION file in auto-claude source
*/
function readSourceVersion(sourcePath: string): string {
const versionFile = path.join(sourcePath, 'VERSION');
@@ -211,16 +211,16 @@ export function hasCustomEnv(autoBuildPath: string): boolean {
/**
* Check version status for a project
* If an existing auto-build folder is found without version metadata,
* If an existing auto-claude folder is found without version metadata,
* create a retroactive .version.json to enable future update tracking.
*/
export function checkVersion(
projectPath: string,
sourcePath: string
): VersionCheckResult {
// Check for both .auto-build and auto-build folders
const dotAutoBuildPath = path.join(projectPath, '.auto-build');
const autoBuildPath = path.join(projectPath, 'auto-build');
// Check for both .auto-claude and auto-claude folders
const dotAutoBuildPath = path.join(projectPath, '.auto-claude');
const autoBuildPath = path.join(projectPath, 'auto-claude');
let installedPath: string | null = null;
if (existsSync(dotAutoBuildPath)) {
@@ -279,17 +279,24 @@ export function checkVersion(
const sourceVersion = readSourceVersion(sourcePath);
const sourceHash = calculateDirectoryHash(sourcePath);
// Only show update available if hash differs AND version is actually different
// This prevents false positives when versions match but hashes differ due to
// metadata files or other non-functional differences
const hashDiffers = metadata.sourceHash !== sourceHash;
const versionDiffers = metadata.version !== sourceVersion;
const updateAvailable = hashDiffers && versionDiffers;
return {
isInitialized: true,
currentVersion: metadata.version,
sourceVersion,
updateAvailable: metadata.sourceHash !== sourceHash,
updateAvailable,
sourcePath: installedPath
};
}
/**
* Initialize auto-build in a project
* Initialize auto-claude in a project
*/
export function initializeProject(
projectPath: string,
@@ -312,18 +319,18 @@ export function initializeProject(
}
// Check if already initialized
const dotAutoBuildPath = path.join(projectPath, '.auto-build');
const autoBuildPath = path.join(projectPath, 'auto-build');
const dotAutoBuildPath = path.join(projectPath, '.auto-claude');
const autoBuildPath = path.join(projectPath, 'auto-claude');
if (existsSync(dotAutoBuildPath) || existsSync(autoBuildPath)) {
return {
success: false,
error: 'Project already has auto-build initialized'
error: 'Project already has auto-claude initialized'
};
}
try {
// Copy files to .auto-build
// Copy files to .auto-claude
copyDirectoryRecursive(sourcePath, dotAutoBuildPath, false);
// Create specs directory
@@ -369,7 +376,7 @@ export function initializeProject(
}
/**
* Update auto-build in a project
* Update auto-claude in a project
*/
export function updateProject(
projectPath: string,
@@ -383,9 +390,9 @@ export function updateProject(
};
}
// Find existing auto-build folder
const dotAutoBuildPath = path.join(projectPath, '.auto-build');
const autoBuildPath = path.join(projectPath, 'auto-build');
// Find existing auto-claude folder
const dotAutoBuildPath = path.join(projectPath, '.auto-claude');
const autoBuildPath = path.join(projectPath, 'auto-claude');
let targetPath: string;
if (existsSync(dotAutoBuildPath)) {
@@ -395,7 +402,7 @@ export function updateProject(
} else {
return {
success: false,
error: 'No auto-build folder found to update'
error: 'No auto-claude folder found to update'
};
}
@@ -431,16 +438,16 @@ export function updateProject(
}
/**
* Get the auto-build folder path for a project (either .auto-build or auto-build)
* Get the auto-claude folder path for a project (either .auto-claude or auto-claude)
*/
export function getAutoBuildPath(projectPath: string): string | null {
const dotAutoBuildPath = path.join(projectPath, '.auto-build');
const autoBuildPath = path.join(projectPath, 'auto-build');
const dotAutoBuildPath = path.join(projectPath, '.auto-claude');
const autoBuildPath = path.join(projectPath, 'auto-claude');
if (existsSync(dotAutoBuildPath)) {
return '.auto-build';
return '.auto-claude';
} else if (existsSync(autoBuildPath)) {
return 'auto-build';
return 'auto-claude';
}
return null;
}
@@ -74,7 +74,7 @@ export class ProjectStore {
// Derive name from path if not provided
const projectName = name || path.basename(projectPath);
// Determine auto-build path (supports both 'auto-build' and '.auto-build')
// Determine auto-claude path (supports both 'auto-claude' and '.auto-claude')
const autoBuildPath = getAutoBuildPath(projectPath) || '';
const project: Project = {
@@ -157,7 +157,7 @@ export class ProjectStore {
if (!project) return [];
// Use project's autoBuildPath if set, otherwise fallback to default
const autoBuildDir = project.autoBuildPath || 'auto-build';
const autoBuildDir = project.autoBuildPath || 'auto-claude';
const specsDir = path.join(project.path, autoBuildDir, 'specs');
if (!existsSync(specsDir)) return [];
@@ -39,7 +39,15 @@ import type {
IdeationStatus,
IdeationGenerationStatus,
AutoBuildSourceUpdateCheck,
AutoBuildSourceUpdateProgress
AutoBuildSourceUpdateProgress,
ChangelogTask,
TaskSpecContent,
ChangelogGenerationRequest,
ChangelogGenerationResult,
ChangelogSaveRequest,
ChangelogSaveResult,
ChangelogGenerationProgress,
ExistingChangelog
} from '../shared/types';
// Expose a secure API to the renderer process
@@ -585,6 +593,77 @@ const electronAPI: ElectronAPI = {
return () => {
ipcRenderer.removeListener(IPC_CHANNELS.AUTOBUILD_SOURCE_PROGRESS, handler);
};
},
// ============================================
// Changelog Operations
// ============================================
getChangelogDoneTasks: (projectId: string): Promise<IPCResult<ChangelogTask[]>> =>
ipcRenderer.invoke(IPC_CHANNELS.CHANGELOG_GET_DONE_TASKS, projectId),
loadTaskSpecs: (projectId: string, taskIds: string[]): Promise<IPCResult<TaskSpecContent[]>> =>
ipcRenderer.invoke(IPC_CHANNELS.CHANGELOG_LOAD_TASK_SPECS, projectId, taskIds),
generateChangelog: (request: ChangelogGenerationRequest): void =>
ipcRenderer.send(IPC_CHANNELS.CHANGELOG_GENERATE, request),
saveChangelog: (request: ChangelogSaveRequest): Promise<IPCResult<ChangelogSaveResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.CHANGELOG_SAVE, request),
readExistingChangelog: (projectId: string): Promise<IPCResult<ExistingChangelog>> =>
ipcRenderer.invoke(IPC_CHANNELS.CHANGELOG_READ_EXISTING, projectId),
// ============================================
// Changelog Event Listeners
// ============================================
onChangelogGenerationProgress: (
callback: (projectId: string, progress: ChangelogGenerationProgress) => void
): (() => void) => {
const handler = (
_event: Electron.IpcRendererEvent,
projectId: string,
progress: ChangelogGenerationProgress
): void => {
callback(projectId, progress);
};
ipcRenderer.on(IPC_CHANNELS.CHANGELOG_GENERATION_PROGRESS, handler);
return () => {
ipcRenderer.removeListener(IPC_CHANNELS.CHANGELOG_GENERATION_PROGRESS, handler);
};
},
onChangelogGenerationComplete: (
callback: (projectId: string, result: ChangelogGenerationResult) => void
): (() => void) => {
const handler = (
_event: Electron.IpcRendererEvent,
projectId: string,
result: ChangelogGenerationResult
): void => {
callback(projectId, result);
};
ipcRenderer.on(IPC_CHANNELS.CHANGELOG_GENERATION_COMPLETE, handler);
return () => {
ipcRenderer.removeListener(IPC_CHANNELS.CHANGELOG_GENERATION_COMPLETE, handler);
};
},
onChangelogGenerationError: (
callback: (projectId: string, error: string) => void
): (() => void) => {
const handler = (
_event: Electron.IpcRendererEvent,
projectId: string,
error: string
): void => {
callback(projectId, error);
};
ipcRenderer.on(IPC_CHANNELS.CHANGELOG_GENERATION_ERROR, handler);
return () => {
ipcRenderer.removeListener(IPC_CHANNELS.CHANGELOG_GENERATION_ERROR, handler);
};
}
};
@@ -18,6 +18,7 @@ import { Roadmap } from './components/Roadmap';
import { Context } from './components/Context';
import { Ideation } from './components/Ideation';
import { GitHubIssues } from './components/GitHubIssues';
import { Changelog } from './components/Changelog';
import { useProjectStore, loadProjects } from './stores/project-store';
import { useTaskStore, loadTasks } from './stores/task-store';
import { useSettingsStore, loadSettings } from './stores/settings-store';
@@ -193,6 +194,9 @@ export function App() {
{activeView === 'github-issues' && selectedProjectId && (
<GitHubIssues onOpenSettings={() => setIsProjectSettingsOpen(true)} />
)}
{activeView === 'changelog' && selectedProjectId && (
<Changelog />
)}
{activeView === 'agent-tools' && (
<div className="flex h-full items-center justify-center">
<div className="text-center">
@@ -65,7 +65,7 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps
loadSettings();
window.electronAPI.getAppVersion().then(setVersion);
// Check for auto-build source updates
// Check for auto-claude source updates
checkForSourceUpdates();
}, []);
@@ -284,14 +284,14 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps
<Label htmlFor="autoBuildPath" className="text-sm font-medium text-foreground">Auto-Build Path</Label>
<Input
id="autoBuildPath"
placeholder="auto-build (default)"
placeholder="auto-claude (default)"
value={settings.autoBuildPath || ''}
onChange={(e) =>
setSettings({ ...settings, autoBuildPath: e.target.value })
}
/>
<p className="text-xs text-muted-foreground">
Relative path to auto-build directory in projects
Relative path to auto-claude directory in projects
</p>
</div>
</section>
@@ -0,0 +1,552 @@
import { useEffect, useState } from 'react';
import {
FileText,
RefreshCw,
Copy,
Save,
AlertCircle,
CheckCircle,
Sparkles,
ChevronDown,
ChevronUp
} from 'lucide-react';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Textarea } from './ui/textarea';
import { Checkbox } from './ui/checkbox';
import { Badge } from './ui/badge';
import { Progress } from './ui/progress';
import { ScrollArea } from './ui/scroll-area';
import { Separator } from './ui/separator';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from './ui/select';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from './ui/tooltip';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from './ui/collapsible';
import { useProjectStore } from '../stores/project-store';
import {
useChangelogStore,
loadChangelogData,
generateChangelog,
saveChangelog,
copyChangelogToClipboard
} from '../stores/changelog-store';
import {
CHANGELOG_FORMAT_LABELS,
CHANGELOG_FORMAT_DESCRIPTIONS,
CHANGELOG_AUDIENCE_LABELS,
CHANGELOG_AUDIENCE_DESCRIPTIONS,
CHANGELOG_STAGE_LABELS
} from '../../shared/constants';
import type {
ChangelogFormat,
ChangelogAudience,
ChangelogTask
} from '../../shared/types';
import { cn } from '../lib/utils';
export function Changelog() {
const selectedProjectId = useProjectStore((state) => state.selectedProjectId);
const doneTasks = useChangelogStore((state) => state.doneTasks);
const selectedTaskIds = useChangelogStore((state) => state.selectedTaskIds);
const existingChangelog = useChangelogStore((state) => state.existingChangelog);
const version = useChangelogStore((state) => state.version);
const date = useChangelogStore((state) => state.date);
const format = useChangelogStore((state) => state.format);
const audience = useChangelogStore((state) => state.audience);
const customInstructions = useChangelogStore((state) => state.customInstructions);
const generationProgress = useChangelogStore((state) => state.generationProgress);
const generatedChangelog = useChangelogStore((state) => state.generatedChangelog);
const isGenerating = useChangelogStore((state) => state.isGenerating);
const error = useChangelogStore((state) => state.error);
const toggleTaskSelection = useChangelogStore((state) => state.toggleTaskSelection);
const selectAllTasks = useChangelogStore((state) => state.selectAllTasks);
const deselectAllTasks = useChangelogStore((state) => state.deselectAllTasks);
const setVersion = useChangelogStore((state) => state.setVersion);
const setDate = useChangelogStore((state) => state.setDate);
const setFormat = useChangelogStore((state) => state.setFormat);
const setAudience = useChangelogStore((state) => state.setAudience);
const setCustomInstructions = useChangelogStore((state) => state.setCustomInstructions);
const updateGeneratedChangelog = useChangelogStore((state) => state.updateGeneratedChangelog);
const setError = useChangelogStore((state) => state.setError);
const setIsGenerating = useChangelogStore((state) => state.setIsGenerating);
const setGenerationProgress = useChangelogStore((state) => state.setGenerationProgress);
const [showAdvanced, setShowAdvanced] = useState(false);
const [saveSuccess, setSaveSuccess] = useState(false);
const [copySuccess, setCopySuccess] = useState(false);
// Load data when project changes
useEffect(() => {
if (selectedProjectId) {
loadChangelogData(selectedProjectId);
}
}, [selectedProjectId]);
// Set up event listeners for generation
useEffect(() => {
const cleanupProgress = window.electronAPI.onChangelogGenerationProgress(
(projectId, progress) => {
if (projectId === selectedProjectId) {
setGenerationProgress(progress);
}
}
);
const cleanupComplete = window.electronAPI.onChangelogGenerationComplete(
(projectId, result) => {
if (projectId === selectedProjectId) {
setIsGenerating(false);
if (result.success) {
updateGeneratedChangelog(result.changelog);
setGenerationProgress({
stage: 'complete',
progress: 100,
message: 'Changelog generated successfully!'
});
} else {
setError(result.error || 'Generation failed');
}
}
}
);
const cleanupError = window.electronAPI.onChangelogGenerationError(
(projectId, errorMsg) => {
if (projectId === selectedProjectId) {
setIsGenerating(false);
setError(errorMsg);
setGenerationProgress({
stage: 'error',
progress: 0,
message: errorMsg,
error: errorMsg
});
}
}
);
return () => {
cleanupProgress();
cleanupComplete();
cleanupError();
};
}, [selectedProjectId]);
const handleGenerate = () => {
if (selectedProjectId) {
generateChangelog(selectedProjectId);
}
};
const handleSave = async () => {
if (selectedProjectId) {
const success = await saveChangelog(selectedProjectId, 'prepend');
if (success) {
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 2000);
}
}
};
const handleCopy = () => {
const success = copyChangelogToClipboard();
if (success) {
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
}
};
const canGenerate = selectedTaskIds.length > 0 && !isGenerating;
const canSave = generatedChangelog.length > 0 && !isGenerating;
if (!selectedProjectId) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<FileText className="mx-auto h-12 w-12 text-muted-foreground/50" />
<h3 className="mt-4 text-lg font-medium">No Project Selected</h3>
<p className="mt-2 text-sm text-muted-foreground">
Select a project from the sidebar to generate changelogs.
</p>
</div>
</div>
);
}
return (
<TooltipProvider>
<div className="flex h-full flex-col">
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-6 py-4">
<div>
<h1 className="text-xl font-semibold">Changelog Generator</h1>
<p className="text-sm text-muted-foreground">
Generate release notes from completed tasks
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => selectedProjectId && loadChangelogData(selectedProjectId)}
>
<RefreshCw className="mr-2 h-4 w-4" />
Refresh
</Button>
</div>
<div className="flex flex-1 overflow-hidden">
{/* Left Panel - Configuration */}
<div className="w-96 flex-shrink-0 border-r border-border overflow-y-auto">
<div className="p-6 space-y-6">
{/* Version & Date */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm">Release Info</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="version">Version</Label>
<Input
id="version"
value={version}
onChange={(e) => setVersion(e.target.value)}
placeholder="1.0.0"
/>
</div>
<div className="space-y-2">
<Label htmlFor="date">Date</Label>
<Input
id="date"
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
</div>
</div>
{existingChangelog?.lastVersion && (
<p className="text-xs text-muted-foreground">
Previous version: {existingChangelog.lastVersion}
</p>
)}
</CardContent>
</Card>
{/* Format & Audience */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm">Output Style</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Format</Label>
<Select
value={format}
onValueChange={(value) => setFormat(value as ChangelogFormat)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(CHANGELOG_FORMAT_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>
<div>
<div>{label}</div>
<div className="text-xs text-muted-foreground">
{CHANGELOG_FORMAT_DESCRIPTIONS[value]}
</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Audience</Label>
<Select
value={audience}
onValueChange={(value) => setAudience(value as ChangelogAudience)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(CHANGELOG_AUDIENCE_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>
<div>
<div>{label}</div>
<div className="text-xs text-muted-foreground">
{CHANGELOG_AUDIENCE_DESCRIPTIONS[value]}
</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Task Selection */}
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm">
Tasks to Include ({selectedTaskIds.length}/{doneTasks.length})
</CardTitle>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={selectAllTasks}
className="h-7 px-2 text-xs"
>
All
</Button>
<Button
variant="ghost"
size="sm"
onClick={deselectAllTasks}
className="h-7 px-2 text-xs"
>
None
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<ScrollArea className="h-48">
{doneTasks.length === 0 ? (
<div className="text-center py-4 text-sm text-muted-foreground">
No completed tasks found.
<br />
Complete tasks in the Kanban board to include them here.
</div>
) : (
<div className="space-y-2">
{doneTasks.map((task) => (
<TaskItem
key={task.id}
task={task}
isSelected={selectedTaskIds.includes(task.id)}
onToggle={() => toggleTaskSelection(task.id)}
/>
))}
</div>
)}
</ScrollArea>
</CardContent>
</Card>
{/* Advanced Options */}
<Collapsible open={showAdvanced} onOpenChange={setShowAdvanced}>
<CollapsibleTrigger asChild>
<Button variant="ghost" className="w-full justify-between">
Advanced Options
{showAdvanced ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="pt-2">
<Card>
<CardContent className="pt-4">
<div className="space-y-2">
<Label htmlFor="instructions">Custom Instructions</Label>
<Textarea
id="instructions"
value={customInstructions}
onChange={(e) => setCustomInstructions(e.target.value)}
placeholder="Add any special instructions for the AI..."
rows={3}
/>
<p className="text-xs text-muted-foreground">
Optional. Guide the AI on tone, specific details to include, etc.
</p>
</div>
</CardContent>
</Card>
</CollapsibleContent>
</Collapsible>
{/* Generate Button */}
<Button
className="w-full"
onClick={handleGenerate}
disabled={!canGenerate}
>
{isGenerating ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
Generating...
</>
) : (
<>
<Sparkles className="mr-2 h-4 w-4" />
Generate Changelog
</>
)}
</Button>
{/* Progress */}
{generationProgress && isGenerating && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span>{CHANGELOG_STAGE_LABELS[generationProgress.stage]}</span>
<span>{generationProgress.progress}%</span>
</div>
<Progress value={generationProgress.progress} />
</div>
)}
{/* Error */}
{error && (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm">
<div className="flex items-start gap-2">
<AlertCircle className="h-4 w-4 text-destructive mt-0.5 shrink-0" />
<span className="text-destructive">{error}</span>
</div>
</div>
)}
</div>
</div>
{/* Right Panel - Preview */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Preview Header */}
<div className="flex items-center justify-between border-b border-border px-6 py-3">
<h2 className="font-medium">Preview</h2>
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={handleCopy}
disabled={!canSave}
>
{copySuccess ? (
<CheckCircle className="mr-2 h-4 w-4 text-success" />
) : (
<Copy className="mr-2 h-4 w-4" />
)}
{copySuccess ? 'Copied!' : 'Copy'}
</Button>
</TooltipTrigger>
<TooltipContent>Copy to clipboard</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="default"
size="sm"
onClick={handleSave}
disabled={!canSave}
>
{saveSuccess ? (
<CheckCircle className="mr-2 h-4 w-4" />
) : (
<Save className="mr-2 h-4 w-4" />
)}
{saveSuccess ? 'Saved!' : 'Save to CHANGELOG.md'}
</Button>
</TooltipTrigger>
<TooltipContent>
Prepend to CHANGELOG.md in project root
</TooltipContent>
</Tooltip>
</div>
</div>
{/* Preview Content */}
<div className="flex-1 overflow-hidden p-6">
{generatedChangelog ? (
<Textarea
className="h-full w-full resize-none font-mono text-sm"
value={generatedChangelog}
onChange={(e) => updateGeneratedChangelog(e.target.value)}
placeholder="Generated changelog will appear here..."
/>
) : (
<div className="flex h-full items-center justify-center">
<div className="text-center">
<FileText className="mx-auto h-12 w-12 text-muted-foreground/30" />
<p className="mt-4 text-sm text-muted-foreground">
Select tasks and click "Generate Changelog" to create release notes.
</p>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</TooltipProvider>
);
}
interface TaskItemProps {
task: ChangelogTask;
isSelected: boolean;
onToggle: () => void;
}
function TaskItem({ task, isSelected, onToggle }: TaskItemProps) {
const completedDate = new Date(task.completedAt).toLocaleDateString();
return (
<label
className={cn(
'flex items-start gap-3 rounded-lg border p-3 cursor-pointer transition-colors',
isSelected
? 'border-primary bg-primary/5'
: 'border-border hover:border-primary/50'
)}
>
<Checkbox
checked={isSelected}
onCheckedChange={onToggle}
className="mt-0.5"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm truncate">{task.title}</span>
{task.hasSpecs && (
<Badge variant="secondary" className="text-xs shrink-0">
Has Specs
</Badge>
)}
</div>
{task.description && (
<p className="text-xs text-muted-foreground truncate mt-1">
{task.description}
</p>
)}
<p className="text-xs text-muted-foreground mt-1">
Completed: {completedDate}
</p>
</div>
</label>
);
}
@@ -15,7 +15,8 @@ import {
AlertCircle,
Download,
RefreshCw,
Github
Github,
FileText
} from 'lucide-react';
import { Button } from './ui/button';
import { ScrollArea } from './ui/scroll-area';
@@ -53,7 +54,7 @@ import {
import { useSettingsStore, saveSettings } from '../stores/settings-store';
import type { Project, AutoBuildVersionInfo } from '../../shared/types';
export type SidebarView = 'kanban' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'agent-tools' | 'github-issues';
export type SidebarView = 'kanban' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'agent-tools' | 'github-issues' | 'changelog';
interface SidebarProps {
onSettingsClick: () => void;
@@ -77,6 +78,7 @@ const projectNavItems: NavItem[] = [
const toolsNavItems: NavItem[] = [
{ id: 'roadmap', label: 'Roadmap', icon: Map, shortcut: 'D' },
{ id: 'ideation', label: 'Ideation', icon: Lightbulb, shortcut: 'I' },
{ id: 'changelog', label: 'Changelog', icon: FileText, shortcut: 'L' },
{ id: 'github-issues', label: 'GitHub Issues', icon: Github, shortcut: 'G' },
{ id: 'context', label: 'Context', icon: BookOpen, shortcut: 'C' },
{ id: 'agent-tools', label: 'Agent Tools', icon: Wrench, shortcut: 'T' }
@@ -158,7 +160,7 @@ export function Sidebar({
if (path) {
const project = await addProject(path);
if (project && !project.autoBuildPath) {
// Project doesn't have auto-build, show init dialog
// Project doesn't have auto-claude, show init dialog
setPendingProject(project);
setShowInitDialog(true);
}
@@ -421,7 +423,7 @@ export function Sidebar({
<div className="rounded-lg bg-muted p-4 text-sm">
<p className="font-medium mb-2">This will:</p>
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
<li>Create a <code className="text-xs bg-background px-1 py-0.5 rounded">.auto-build</code> folder in your project</li>
<li>Create a <code className="text-xs bg-background px-1 py-0.5 rounded">.auto-claude</code> folder in your project</li>
<li>Copy the Auto-Build framework files</li>
<li>Set up the specs directory for your tasks</li>
</ul>
@@ -9,3 +9,4 @@ export * from './AppSettings';
export * from './Context';
export * from './Ideation';
export * from './GitHubIssues';
export * from './Changelog';
@@ -0,0 +1,30 @@
import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { Check } from 'lucide-react';
import { cn } from '../../lib/utils';
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'disabled:cursor-not-allowed disabled:opacity-50',
'data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn('flex items-center justify-center text-current')}
>
<Check className="h-3 w-3" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox };
@@ -0,0 +1,9 @@
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
const Collapsible = CollapsiblePrimitive.Root;
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
@@ -0,0 +1,263 @@
import { create } from 'zustand';
import type {
ChangelogTask,
TaskSpecContent,
ChangelogFormat,
ChangelogAudience,
ChangelogGenerationProgress,
ChangelogGenerationResult,
ExistingChangelog
} from '../../shared/types';
interface ChangelogState {
// Data
doneTasks: ChangelogTask[];
selectedTaskIds: string[];
loadedSpecs: TaskSpecContent[];
existingChangelog: ExistingChangelog | null;
// Generation config
version: string;
date: string;
format: ChangelogFormat;
audience: ChangelogAudience;
customInstructions: string;
// Generation state
generationProgress: ChangelogGenerationProgress | null;
generatedChangelog: string;
isGenerating: boolean;
error: string | null;
// Actions
setDoneTasks: (tasks: ChangelogTask[]) => void;
setSelectedTaskIds: (ids: string[]) => void;
toggleTaskSelection: (taskId: string) => void;
selectAllTasks: () => void;
deselectAllTasks: () => void;
setLoadedSpecs: (specs: TaskSpecContent[]) => void;
setExistingChangelog: (changelog: ExistingChangelog | null) => void;
// Config actions
setVersion: (version: string) => void;
setDate: (date: string) => void;
setFormat: (format: ChangelogFormat) => void;
setAudience: (audience: ChangelogAudience) => void;
setCustomInstructions: (instructions: string) => void;
// Generation actions
setGenerationProgress: (progress: ChangelogGenerationProgress | null) => void;
setGeneratedChangelog: (changelog: string) => void;
setIsGenerating: (isGenerating: boolean) => void;
setError: (error: string | null) => void;
// Compound actions
reset: () => void;
updateGeneratedChangelog: (changelog: string) => void;
}
const getDefaultDate = (): string => {
return new Date().toISOString().split('T')[0];
};
const initialState = {
doneTasks: [],
selectedTaskIds: [],
loadedSpecs: [],
existingChangelog: null,
version: '1.0.0',
date: getDefaultDate(),
format: 'keep-a-changelog' as ChangelogFormat,
audience: 'user-facing' as ChangelogAudience,
customInstructions: '',
generationProgress: null,
generatedChangelog: '',
isGenerating: false,
error: null
};
export const useChangelogStore = create<ChangelogState>((set, get) => ({
...initialState,
// Data actions
setDoneTasks: (tasks) => set({ doneTasks: tasks }),
setSelectedTaskIds: (ids) => set({ selectedTaskIds: ids }),
toggleTaskSelection: (taskId) =>
set((state) => ({
selectedTaskIds: state.selectedTaskIds.includes(taskId)
? state.selectedTaskIds.filter((id) => id !== taskId)
: [...state.selectedTaskIds, taskId]
})),
selectAllTasks: () =>
set((state) => ({
selectedTaskIds: state.doneTasks.map((task) => task.id)
})),
deselectAllTasks: () => set({ selectedTaskIds: [] }),
setLoadedSpecs: (specs) => set({ loadedSpecs: specs }),
setExistingChangelog: (changelog) => {
set({ existingChangelog: changelog });
// Auto-suggest next version if we found a previous version
if (changelog?.lastVersion) {
const parts = changelog.lastVersion.split('.').map(Number);
if (parts.length === 3 && !parts.some(isNaN)) {
const [major, minor, patch] = parts;
set({ version: `${major}.${minor}.${patch + 1}` });
}
}
},
// Config actions
setVersion: (version) => set({ version }),
setDate: (date) => set({ date }),
setFormat: (format) => set({ format }),
setAudience: (audience) => set({ audience }),
setCustomInstructions: (instructions) => set({ customInstructions: instructions }),
// Generation actions
setGenerationProgress: (progress) => set({ generationProgress: progress }),
setGeneratedChangelog: (changelog) => set({ generatedChangelog: changelog }),
setIsGenerating: (isGenerating) => set({ isGenerating }),
setError: (error) => set({ error }),
// Compound actions
reset: () => set({ ...initialState, date: getDefaultDate() }),
updateGeneratedChangelog: (changelog) => set({ generatedChangelog: changelog })
}));
// Helper functions for loading data
export async function loadChangelogData(projectId: string): Promise<void> {
const store = useChangelogStore.getState();
try {
// Load done tasks
const tasksResult = await window.electronAPI.getChangelogDoneTasks(projectId);
if (tasksResult.success && tasksResult.data) {
store.setDoneTasks(tasksResult.data);
}
// Load existing changelog
const changelogResult = await window.electronAPI.readExistingChangelog(projectId);
if (changelogResult.success && changelogResult.data) {
store.setExistingChangelog(changelogResult.data);
}
} catch (error) {
store.setError(error instanceof Error ? error.message : 'Failed to load changelog data');
}
}
export async function loadTaskSpecs(projectId: string, taskIds: string[]): Promise<void> {
const store = useChangelogStore.getState();
try {
const result = await window.electronAPI.loadTaskSpecs(projectId, taskIds);
if (result.success && result.data) {
store.setLoadedSpecs(result.data);
}
} catch (error) {
store.setError(error instanceof Error ? error.message : 'Failed to load task specs');
}
}
export function generateChangelog(projectId: string): void {
const store = useChangelogStore.getState();
if (store.selectedTaskIds.length === 0) {
store.setError('Please select at least one task to include in the changelog');
return;
}
store.setIsGenerating(true);
store.setError(null);
store.setGenerationProgress({
stage: 'loading_specs',
progress: 0,
message: 'Starting changelog generation...'
});
window.electronAPI.generateChangelog({
projectId,
taskIds: store.selectedTaskIds,
version: store.version,
date: store.date,
format: store.format,
audience: store.audience,
customInstructions: store.customInstructions || undefined
});
}
export async function saveChangelog(
projectId: string,
mode: 'prepend' | 'overwrite' | 'append' = 'prepend'
): Promise<boolean> {
const store = useChangelogStore.getState();
if (!store.generatedChangelog) {
store.setError('No changelog to save');
return false;
}
try {
const result = await window.electronAPI.saveChangelog({
projectId,
content: store.generatedChangelog,
mode
});
if (result.success) {
return true;
} else {
store.setError(result.error || 'Failed to save changelog');
return false;
}
} catch (error) {
store.setError(error instanceof Error ? error.message : 'Failed to save changelog');
return false;
}
}
export function copyChangelogToClipboard(): boolean {
const store = useChangelogStore.getState();
if (!store.generatedChangelog) {
store.setError('No changelog to copy');
return false;
}
try {
navigator.clipboard.writeText(store.generatedChangelog);
return true;
} catch (error) {
store.setError('Failed to copy to clipboard');
return false;
}
}
// Selectors
export function getSelectedTasks(): ChangelogTask[] {
const store = useChangelogStore.getState();
return store.doneTasks.filter((task) => store.selectedTaskIds.includes(task.id));
}
export function getTasksWithSpecs(): ChangelogTask[] {
const store = useChangelogStore.getState();
return store.doneTasks.filter((task) => task.hasSpecs);
}
export function canGenerate(): boolean {
const store = useChangelogStore.getState();
return store.selectedTaskIds.length > 0 && !store.isGenerating;
}
export function canSave(): boolean {
const store = useChangelogStore.getState();
return store.generatedChangelog.length > 0 && !store.isGenerating;
}
@@ -155,7 +155,7 @@ export async function updateProjectSettings(
}
/**
* Check auto-build version status for a project
* Check auto-claude version status for a project
*/
export async function checkProjectVersion(
projectId: string
@@ -172,7 +172,7 @@ export async function checkProjectVersion(
}
/**
* Initialize auto-build in a project
* Initialize auto-claude in a project
*/
export async function initializeProject(
projectId: string
@@ -184,7 +184,7 @@ export async function initializeProject(
if (result.success && result.data) {
// Update the project's autoBuildPath in local state
if (result.data.success) {
store.updateProject(projectId, { autoBuildPath: '.auto-build' });
store.updateProject(projectId, { autoBuildPath: '.auto-claude' });
}
return result.data;
}
@@ -197,7 +197,7 @@ export async function initializeProject(
}
/**
* Update auto-build in a project
* Update auto-claude in a project
*/
export async function updateProjectAutoBuild(
projectId: string
@@ -209,7 +209,7 @@ export async function updateProjectAutoBuild(
if (result.success && result.data) {
return result.data;
}
store.setError(result.error || 'Failed to update auto-build');
store.setError(result.error || 'Failed to update auto-claude');
return null;
} catch (error) {
store.setError(error instanceof Error ? error.message : 'Unknown error');
@@ -175,14 +175,26 @@ export const IPC_CHANNELS = {
AUTOBUILD_SOURCE_CHECK: 'autobuild:source:check',
AUTOBUILD_SOURCE_DOWNLOAD: 'autobuild:source:download',
AUTOBUILD_SOURCE_VERSION: 'autobuild:source:version',
AUTOBUILD_SOURCE_PROGRESS: 'autobuild:source:progress'
AUTOBUILD_SOURCE_PROGRESS: 'autobuild:source:progress',
// Changelog operations
CHANGELOG_GET_DONE_TASKS: 'changelog:getDoneTasks',
CHANGELOG_LOAD_TASK_SPECS: 'changelog:loadTaskSpecs',
CHANGELOG_GENERATE: 'changelog:generate',
CHANGELOG_SAVE: 'changelog:save',
CHANGELOG_READ_EXISTING: 'changelog:readExisting',
// Changelog events (main -> renderer)
CHANGELOG_GENERATION_PROGRESS: 'changelog:generationProgress',
CHANGELOG_GENERATION_COMPLETE: 'changelog:generationComplete',
CHANGELOG_GENERATION_ERROR: 'changelog:generationError'
} as const;
// File paths relative to project
export const AUTO_BUILD_PATHS = {
SPECS_DIR: 'auto-build/specs',
ROADMAP_DIR: 'auto-build/roadmap',
IDEATION_DIR: 'auto-build/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',
@@ -193,7 +205,7 @@ export const AUTO_BUILD_PATHS = {
ROADMAP_DISCOVERY: 'roadmap_discovery.json',
IDEATION_FILE: 'ideation.json',
IDEATION_CONTEXT: 'ideation_context.json',
PROJECT_INDEX: 'auto-build/project_index.json',
PROJECT_INDEX: 'auto-claude/project_index.json',
GRAPHITI_STATE: '.graphiti_state.json'
} as const;
@@ -386,3 +398,45 @@ export const GITHUB_COMPLEXITY_COLORS: Record<string, string> = {
standard: 'bg-warning/10 text-warning',
complex: 'bg-destructive/10 text-destructive'
};
// ============================================
// Changelog Constants
// ============================================
// Changelog format labels and descriptions
export const CHANGELOG_FORMAT_LABELS: Record<string, string> = {
'keep-a-changelog': 'Keep a Changelog',
'simple-list': 'Simple List',
'github-release': 'GitHub Release'
};
export const CHANGELOG_FORMAT_DESCRIPTIONS: Record<string, string> = {
'keep-a-changelog': 'Structured format with Added/Changed/Fixed/Removed sections',
'simple-list': 'Clean bulleted list with categories',
'github-release': 'GitHub-style release notes with emojis'
};
// Changelog audience labels and descriptions
export const CHANGELOG_AUDIENCE_LABELS: Record<string, string> = {
'technical': 'Technical',
'user-facing': 'User-Facing',
'marketing': 'Marketing'
};
export const CHANGELOG_AUDIENCE_DESCRIPTIONS: Record<string, string> = {
'technical': 'Detailed technical changes for developers',
'user-facing': 'Clear, non-technical descriptions for end users',
'marketing': 'Value-focused copy emphasizing benefits'
};
// Changelog generation stage labels
export const CHANGELOG_STAGE_LABELS: Record<string, string> = {
'loading_specs': 'Loading spec files...',
'generating': 'Generating changelog...',
'formatting': 'Formatting output...',
'complete': 'Complete',
'error': 'Error'
};
// Default changelog file path
export const DEFAULT_CHANGELOG_PATH = 'CHANGELOG.md';
@@ -76,7 +76,7 @@ export interface Task {
updatedAt: Date;
}
// Implementation Plan (from auto-build)
// Implementation Plan (from auto-claude)
export interface ImplementationPlan {
feature: string;
workflow_type: string;
@@ -685,6 +685,76 @@ export interface GitHubInvestigationStatus {
error?: string;
}
// ============================================
// Changelog Types
// ============================================
export type ChangelogFormat = 'keep-a-changelog' | 'simple-list' | 'github-release';
export type ChangelogAudience = 'technical' | 'user-facing' | 'marketing';
export interface ChangelogTask {
id: string;
specId: string;
title: string;
description: string;
completedAt: Date;
hasSpecs: boolean;
}
export interface TaskSpecContent {
taskId: string;
specId: string;
spec?: string; // Content of spec.md
requirements?: Record<string, unknown>; // Parsed requirements.json
qaReport?: string; // Content of qa_report.md
implementationPlan?: ImplementationPlan; // Parsed implementation_plan.json
error?: string; // Error message if loading failed
}
export interface ChangelogGenerationRequest {
projectId: string;
taskIds: string[];
version: string;
date: string; // ISO format
format: ChangelogFormat;
audience: ChangelogAudience;
customInstructions?: string;
}
export interface ChangelogGenerationResult {
success: boolean;
changelog: string;
version: string;
tasksIncluded: number;
error?: string;
}
export interface ChangelogSaveRequest {
projectId: string;
content: string;
filePath?: string; // Optional custom path, defaults to CHANGELOG.md
mode: 'prepend' | 'overwrite' | 'append';
}
export interface ChangelogSaveResult {
filePath: string;
bytesWritten: number;
}
export interface ChangelogGenerationProgress {
stage: 'loading_specs' | 'generating' | 'formatting' | 'complete' | 'error';
progress: number; // 0-100
message: string;
error?: string;
}
export interface ExistingChangelog {
exists: boolean;
content?: string;
lastVersion?: string;
error?: string;
}
// ============================================
// Auto-Build Source Update Types
// ============================================
@@ -850,6 +920,24 @@ export interface ElectronAPI {
onAutoBuildSourceUpdateProgress: (
callback: (progress: AutoBuildSourceUpdateProgress) => void
) => () => void;
// Changelog operations
getChangelogDoneTasks: (projectId: string) => Promise<IPCResult<ChangelogTask[]>>;
loadTaskSpecs: (projectId: string, taskIds: string[]) => Promise<IPCResult<TaskSpecContent[]>>;
generateChangelog: (request: ChangelogGenerationRequest) => void; // Async with progress events
saveChangelog: (request: ChangelogSaveRequest) => Promise<IPCResult<ChangelogSaveResult>>;
readExistingChangelog: (projectId: string) => Promise<IPCResult<ExistingChangelog>>;
// Changelog event listeners
onChangelogGenerationProgress: (
callback: (projectId: string, progress: ChangelogGenerationProgress) => void
) => () => void;
onChangelogGenerationComplete: (
callback: (projectId: string, result: ChangelogGenerationResult) => void
) => () => void;
onChangelogGenerationError: (
callback: (projectId: string, error: string) => void
) => () => void;
}
declare global {
+4 -4
View File
@@ -556,7 +556,7 @@ async def run_autonomous_agent(
Args:
project_dir: Root directory for the project
spec_dir: Directory containing the spec (auto-build/specs/001-name/)
spec_dir: Directory containing the spec (auto-claude/specs/001-name/)
model: Claude model to use
max_iterations: Maximum number of iterations (None for unlimited)
verbose: Whether to show detailed output
@@ -651,7 +651,7 @@ async def run_autonomous_agent(
print(f"\nTo resume, delete the PAUSE file:")
print(f" rm {pause_file}")
print(f"\nThen run again:")
print(f" python auto-build/run.py --spec {spec_dir.name}")
print(f" python auto-claude/run.py --spec {spec_dir.name}")
return
# Check max iterations
@@ -849,14 +849,14 @@ async def run_autonomous_agent(
bold(f"{icon(Icons.PLAY)} NEXT STEPS"),
"",
f"{total - completed} chunks remaining.",
f"Run again: {highlight(f'python auto-build/run.py --spec {spec_dir.name}')}",
f"Run again: {highlight(f'python auto-claude/run.py --spec {spec_dir.name}')}",
]
else:
content = [
bold(f"{icon(Icons.SUCCESS)} NEXT STEPS"),
"",
"All chunks completed!",
" 1. Review the auto-build/* branch",
" 1. Review the auto-claude/* branch",
" 2. Run manual tests",
" 3. Merge to main",
]
@@ -8,13 +8,13 @@ Supports monorepos with multiple services.
Usage:
# Index entire project (creates project_index.json)
python auto-build/analyzer.py --index
python auto-claude/analyzer.py --index
# Analyze specific service
python auto-build/analyzer.py --service backend
python auto-claude/analyzer.py --service backend
# Output to specific file
python auto-build/analyzer.py --index --output path/to/output.json
python auto-claude/analyzer.py --index --output path/to/output.json
The analyzer will:
1. Detect if this is a monorepo or single project
@@ -48,7 +48,7 @@ SKIP_DIRS = {
"vendor",
".idea",
".vscode",
"auto-build",
"auto-claude",
".pytest_cache",
".mypy_cache",
"coverage",
@@ -8,14 +8,14 @@ This is the "RAG-like" component that finds what files matter for THIS task.
Usage:
# Find context for a task across specific services
python auto-build/context.py \
python auto-claude/context.py \
--services backend,scraper \
--keywords "retry,error,proxy" \
--task "Add retry logic when proxies fail" \
--output auto-build/specs/001-retry/context.json
--output auto-claude/specs/001-retry/context.json
# Use project index to auto-suggest services
python auto-build/context.py \
python auto-claude/context.py \
--task "Add retry logic when proxies fail" \
--output context.json
@@ -37,7 +37,7 @@ from dataclasses import dataclass, field, asdict
# Directories to skip
SKIP_DIRS = {
"node_modules", ".git", "__pycache__", ".venv", "venv", "dist", "build",
".next", ".nuxt", "target", "vendor", ".idea", ".vscode", "auto-build",
".next", ".nuxt", "target", "vendor", ".idea", ".vscode", "auto-claude",
".pytest_cache", ".mypy_cache", "coverage", ".turbo", ".cache",
}
@@ -78,7 +78,7 @@ class ContextBuilder:
def _load_project_index(self) -> dict:
"""Load project index from file or create new one."""
index_file = self.project_dir / "auto-build" / "project_index.json"
index_file = self.project_dir / "auto-claude" / "project_index.json"
if index_file.exists():
with open(index_file) as f:
return json.load(f)
@@ -7,7 +7,7 @@ Implements a swarm coordination pattern for parallel execution of independent ch
All work is collected in a single STAGING worktree that the user can test before merging.
Architecture:
1. Create ONE staging worktree: .worktrees/auto-build/
1. Create ONE staging worktree: .worktrees/auto-claude/
2. Each worker gets a temporary worktree for isolation during work
3. Workers merge INTO staging (not base branch)
4. User can cd into staging, run the app, test the feature
@@ -366,7 +366,7 @@ class SwarmCoordinator:
# Merge the worker branch into staging
result = subprocess.run(
["git", "merge", "--no-ff", branch_name,
"-m", f"auto-build: Merge {branch_name}"],
"-m", f"auto-claude: Merge {branch_name}"],
cwd=staging_path,
capture_output=True,
text=True,
@@ -499,7 +499,7 @@ class SwarmCoordinator:
check=False,
)
result = subprocess.run(
["git", "commit", "-m", f"auto-build: Complete {chunk.id}\n\n{chunk.description}"],
["git", "commit", "-m", f"auto-claude: Complete {chunk.id}\n\n{chunk.description}"],
cwd=worktree_path,
capture_output=True,
text=True,
@@ -767,11 +767,11 @@ class SwarmCoordinator:
print()
print("When you're happy with it:")
print(highlight(f" python auto-build/run.py --spec {spec_name} --merge"))
print(highlight(f" python auto-claude/run.py --spec {spec_name} --merge"))
print()
print("To see what changed:")
print(muted(f" python auto-build/run.py --spec {spec_name} --review"))
print(muted(f" python auto-claude/run.py --spec {spec_name} --review"))
print()
return staging_path
@@ -109,7 +109,7 @@ class GraphitiConfig:
@dataclass
class GraphitiState:
"""State of Graphiti integration for an auto-build spec."""
"""State of Graphiti integration for an auto-claude spec."""
initialized: bool = False
database: Optional[str] = None
indices_built: bool = False
@@ -66,7 +66,7 @@ RETRY_DELAY_SECONDS = 1
class GraphitiMemory:
"""
Manages Graphiti-based persistent memory for auto-build sessions.
Manages Graphiti-based persistent memory for auto-claude sessions.
This class provides a high-level interface for:
- Storing session insights as episodes

Some files were not shown because too many files have changed in this diff Show More