Compare commits

..

8 Commits

Author SHA1 Message Date
Auto
6d15d020ec 0.1.1 2026-02-04 15:39:46 +02:00
Auto
196038fa26 refactor: extract docs to standalone site at autoforge.cc
- Remove embedded documentation system (18 files) from main UI:
  - Delete ui/src/components/docs/ (DocsPage, DocsContent, DocsSidebar,
    DocsSearch, docsData, and all 13 section components)
  - Delete ui/src/hooks/useHashRoute.ts (only used for docs routing)
- Simplify ui/src/main.tsx: remove Router component, render App directly
  inside QueryClientProvider (no more hash-based routing)
- Update docs button in App.tsx header to open https://autoforge.cc in
  a new tab instead of navigating to #/docs hash route
- Add logo to header
- Add temp-docs/ to .gitignore
- Update CLAUDE.md with current architecture documentation

The documentation has been extracted into a separate repository and
deployed as a standalone Vite + React site at https://autoforge.cc.
This reduces the main UI bundle and decouples docs from app releases.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 15:36:55 +02:00
Auto
4549840330 feat: add npm global package for one-command install
Add a Node.js CLI wrapper that allows installing AutoForge globally via
`npm install -g autoforge-ai` and running it with a single `autoforge`
command. The CLI handles Python detection, venv management, config
loading, and uvicorn server lifecycle automatically.

New files:
- package.json: npm package config with bin entry, files whitelist,
  and prepublishOnly script that builds the UI
- bin/autoforge.js: thin entry point that imports lib/cli.js
- lib/cli.js: main CLI module (~790 lines) with cross-platform Python
  3.11+ detection, composite venv marker for smart invalidation
  (requirements hash + Python version + path), .env config management
  at ~/.autoforge/.env, server startup with PID file and port detection,
  and signal handling with process tree cleanup
- requirements-prod.txt: runtime-only deps (excludes ruff, mypy, pytest)
- .npmignore: excludes dev files, tests, __pycache__, UI source

Modified files:
- ui/package.json: rename to autoforge-ui to avoid confusion with root
- .gitignore: add *.tgz for npm pack output
- README.md: add npm install as primary quick start method, document
  CLI commands, add Ollama/Vertex AI config sections, new troubleshooting
  entries for Python/venv issues
- GettingStarted.tsx: add Installation, Quick Start, and CLI Commands
  sections to in-app documentation with command reference table
- docsData.ts: add installation and cli-commands sidebar entries

Published as autoforge-ai@0.1.0 on npm.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 14:48:00 +02:00
Auto
451a5a9d05 fix: prevent dialog modal from being clipped on zoom
Add max-h-[calc(100vh-2rem)] and overflow-y-auto to the shared
DialogContent component so modals scroll vertically when their
content exceeds the viewport height (e.g., Settings modal when
browser is zoomed in).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 12:42:04 +02:00
Auto
c2ad993e75 rebrand: rename AutoCoder to AutoForge across entire codebase
Complete project rebrand from AutoCoder to AutoForge, touching 62 files
across Python backend, FastAPI server, React UI, documentation, config,
and CI/CD.

Key changes:
- Rename autocoder_paths.py -> autoforge_paths.py with backward-compat
  migration from .autocoder/ -> .autoforge/ directories
- Update registry.py to migrate ~/.autocoder/ -> ~/.autoforge/ global
  config directory with fallback support
- Update security.py with fallback reads from legacy .autocoder/ paths
- Rename .claude/commands and skills from gsd-to-autocoder-spec to
  gsd-to-autoforge-spec
- Update all Python modules: client, prompts, progress, agent,
  orchestrator, server routers and services
- Update React UI: package.json name, index.html title, localStorage
  keys, all documentation sections, component references
- Update start scripts (bat/sh/py), examples, and .env.example
- Update CLAUDE.md and README.md with new branding and paths
- Update test files for new .autoforge/ directory structure
- Transfer git remote from leonvanzyl/autocoder to
  AutoForgeAI/autoforge

Backward compatibility preserved: legacy .autocoder/ directories are
auto-detected and migrated on next agent start. Config fallback chain
checks both new and old paths.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 12:02:06 +02:00
Leon van Zyl
f6510b4dd8 Merge pull request #154 from paperlinguist/master
Adding Scrollbar and Improving vite config
2026-02-04 08:55:15 +02:00
Auto
2507bfd5f0 fix: use project dir as cwd for parallel orchestrator subprocesses
The parallel orchestrator was using AUTOCODER_ROOT as the working
directory when spawning coding, batch, and testing agent subprocesses.
This caused the Claude Code CLI to create .claude/ and .claude_worktrees/
directories in the autocoder installation folder instead of the project
directory, scattering output files across multiple locations.

Changed all 3 subprocess spawn sites (coding agent, batch agent, testing
agent) to use self.project_dir as cwd, matching the behavior of the
server's process_manager.py. The subprocess commands already use absolute
paths to autonomous_agent_demo.py, so Python imports are unaffected.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 08:50:36 +02:00
Abigail Green
f32e7efda6 Adding Scrollbar and Improving vite config
Add scrollbar to Mission Control panel
Improve code-splitting to reduce bundle size
2026-02-03 12:18:53 -07:00
81 changed files with 1459 additions and 3382 deletions

View File

@@ -97,7 +97,7 @@ Fix ALL issues before considering the implementation complete. Never leave linti
## Project-Specific Context ## Project-Specific Context
For this project (autocoder): For this project (autoforge):
- **Python Backend**: Uses SQLAlchemy, FastAPI, follows patterns in `api/`, `mcp_server/` - **Python Backend**: Uses SQLAlchemy, FastAPI, follows patterns in `api/`, `mcp_server/`
- **React UI**: Uses React 18, TypeScript, TanStack Query, Tailwind CSS v4, Radix UI - **React UI**: Uses React 18, TypeScript, TanStack Query, Tailwind CSS v4, Radix UI
- **Design System**: Neobrutalism style with specific color tokens and animations - **Design System**: Neobrutalism style with specific color tokens and animations

View File

@@ -8,7 +8,7 @@ This command **requires** the project directory as an argument via `$ARGUMENTS`.
**Example:** `/create-spec generations/my-app` **Example:** `/create-spec generations/my-app`
**Output location:** `$ARGUMENTS/.autocoder/prompts/app_spec.txt` and `$ARGUMENTS/.autocoder/prompts/initializer_prompt.md` **Output location:** `$ARGUMENTS/.autoforge/prompts/app_spec.txt` and `$ARGUMENTS/.autoforge/prompts/initializer_prompt.md`
If `$ARGUMENTS` is empty, inform the user they must provide a project path and exit. If `$ARGUMENTS` is empty, inform the user they must provide a project path and exit.
@@ -347,13 +347,13 @@ First ask in conversation if they want to make changes.
## Output Directory ## Output Directory
The output directory is: `$ARGUMENTS/.autocoder/prompts/` The output directory is: `$ARGUMENTS/.autoforge/prompts/`
Once the user approves, generate these files: Once the user approves, generate these files:
## 1. Generate `app_spec.txt` ## 1. Generate `app_spec.txt`
**Output path:** `$ARGUMENTS/.autocoder/prompts/app_spec.txt` **Output path:** `$ARGUMENTS/.autoforge/prompts/app_spec.txt`
Create a new file using this XML structure: Create a new file using this XML structure:
@@ -489,7 +489,7 @@ Create a new file using this XML structure:
## 2. Update `initializer_prompt.md` ## 2. Update `initializer_prompt.md`
**Output path:** `$ARGUMENTS/.autocoder/prompts/initializer_prompt.md` **Output path:** `$ARGUMENTS/.autoforge/prompts/initializer_prompt.md`
If the output directory has an existing `initializer_prompt.md`, read it and update the feature count. If the output directory has an existing `initializer_prompt.md`, read it and update the feature count.
If not, copy from `.claude/templates/initializer_prompt.template.md` first, then update. If not, copy from `.claude/templates/initializer_prompt.template.md` first, then update.
@@ -512,7 +512,7 @@ After: **CRITICAL:** You must create exactly **25** features using the `feature
## 3. Write Status File (REQUIRED - Do This Last) ## 3. Write Status File (REQUIRED - Do This Last)
**Output path:** `$ARGUMENTS/.autocoder/prompts/.spec_status.json` **Output path:** `$ARGUMENTS/.autoforge/prompts/.spec_status.json`
**CRITICAL:** After you have completed ALL requested file changes, write this status file to signal completion to the UI. This is required for the "Continue to Project" button to appear. **CRITICAL:** After you have completed ALL requested file changes, write this status file to signal completion to the UI. This is required for the "Continue to Project" button to appear.
@@ -524,8 +524,8 @@ Write this JSON file:
"version": 1, "version": 1,
"timestamp": "[current ISO 8601 timestamp, e.g., 2025-01-15T14:30:00.000Z]", "timestamp": "[current ISO 8601 timestamp, e.g., 2025-01-15T14:30:00.000Z]",
"files_written": [ "files_written": [
".autocoder/prompts/app_spec.txt", ".autoforge/prompts/app_spec.txt",
".autocoder/prompts/initializer_prompt.md" ".autoforge/prompts/initializer_prompt.md"
], ],
"feature_count": [the feature count from Phase 4L] "feature_count": [the feature count from Phase 4L]
} }
@@ -539,9 +539,9 @@ Write this JSON file:
"version": 1, "version": 1,
"timestamp": "2025-01-15T14:30:00.000Z", "timestamp": "2025-01-15T14:30:00.000Z",
"files_written": [ "files_written": [
".autocoder/prompts/app_spec.txt", ".autoforge/prompts/app_spec.txt",
".autocoder/prompts/initializer_prompt.md", ".autoforge/prompts/initializer_prompt.md",
".autocoder/prompts/coding_prompt.md" ".autoforge/prompts/coding_prompt.md"
], ],
"feature_count": 35 "feature_count": 35
} }
@@ -559,11 +559,11 @@ Write this JSON file:
Once files are generated, tell the user what to do next: Once files are generated, tell the user what to do next:
> "Your specification files have been created in `$ARGUMENTS/.autocoder/prompts/`! > "Your specification files have been created in `$ARGUMENTS/.autoforge/prompts/`!
> >
> **Files created:** > **Files created:**
> - `$ARGUMENTS/.autocoder/prompts/app_spec.txt` > - `$ARGUMENTS/.autoforge/prompts/app_spec.txt`
> - `$ARGUMENTS/.autocoder/prompts/initializer_prompt.md` > - `$ARGUMENTS/.autoforge/prompts/initializer_prompt.md`
> >
> The **Continue to Project** button should now appear. Click it to start the autonomous coding agent! > The **Continue to Project** button should now appear. Click it to start the autonomous coding agent!
> >

View File

@@ -42,7 +42,7 @@ You are the **Project Expansion Assistant** - an expert at understanding existin
# FIRST: Read and Understand Existing Project # FIRST: Read and Understand Existing Project
**Step 1:** Read the existing specification: **Step 1:** Read the existing specification:
- Read `$ARGUMENTS/.autocoder/prompts/app_spec.txt` - Read `$ARGUMENTS/.autoforge/prompts/app_spec.txt`
**Step 2:** Present a summary to the user: **Step 2:** Present a summary to the user:
@@ -231,4 +231,4 @@ If they want to add more, go back to Phase 1.
# BEGIN # BEGIN
Start by reading the app specification file at `$ARGUMENTS/.autocoder/prompts/app_spec.txt`, then greet the user with a summary of their existing project and ask what they want to add. Start by reading the app specification file at `$ARGUMENTS/.autoforge/prompts/app_spec.txt`, then greet the user with a summary of their existing project and ask what they want to add.

View File

@@ -1,10 +0,0 @@
---
allowed-tools: Read, Write, Bash, Glob, Grep
description: Convert GSD codebase mapping to Autocoder app_spec.txt
---
# GSD to Autocoder Spec
Convert `.planning/codebase/*.md` (from `/gsd:map-codebase`) to Autocoder's `.autocoder/prompts/app_spec.txt`.
@.claude/skills/gsd-to-autocoder-spec/SKILL.md

View File

@@ -0,0 +1,10 @@
---
allowed-tools: Read, Write, Bash, Glob, Grep
description: Convert GSD codebase mapping to AutoForge app_spec.txt
---
# GSD to AutoForge Spec
Convert `.planning/codebase/*.md` (from `/gsd:map-codebase`) to AutoForge's `.autoforge/prompts/app_spec.txt`.
@.claude/skills/gsd-to-autoforge-spec/SKILL.md

View File

@@ -1,21 +1,21 @@
--- ---
name: gsd-to-autocoder-spec name: gsd-to-autoforge-spec
description: | description: |
Convert GSD codebase mapping to Autocoder app_spec.txt. This skill should be used when Convert GSD codebase mapping to AutoForge app_spec.txt. This skill should be used when
the user has run /gsd:map-codebase and wants to use Autocoder on an existing project. the user has run /gsd:map-codebase and wants to use AutoForge on an existing project.
Triggers: "convert to autocoder", "gsd to spec", "create app_spec from codebase", Triggers: "convert to autoforge", "gsd to spec", "create app_spec from codebase",
"use autocoder on existing project", after /gsd:map-codebase completion. "use autoforge on existing project", after /gsd:map-codebase completion.
--- ---
# GSD to Autocoder Spec Converter # GSD to AutoForge Spec Converter
Converts `.planning/codebase/*.md` (GSD mapping output) to `.autocoder/prompts/app_spec.txt` (Autocoder format). Converts `.planning/codebase/*.md` (GSD mapping output) to `.autoforge/prompts/app_spec.txt` (AutoForge format).
## When to Use ## When to Use
- After running `/gsd:map-codebase` on an existing project - After running `/gsd:map-codebase` on an existing project
- When onboarding an existing codebase to Autocoder - When onboarding an existing codebase to AutoForge
- User wants Autocoder to continue development on existing code - User wants AutoForge to continue development on existing code
## Prerequisites ## Prerequisites
@@ -84,12 +84,12 @@ Extract:
Create `prompts/` directory: Create `prompts/` directory:
```bash ```bash
mkdir -p .autocoder/prompts mkdir -p .autoforge/prompts
``` ```
**Mapping GSD Documents to Autocoder Spec:** **Mapping GSD Documents to AutoForge Spec:**
| GSD Source | Autocoder Target | | GSD Source | AutoForge Target |
|------------|------------------| |------------|------------------|
| STACK.md Languages | `<technology_stack>` | | STACK.md Languages | `<technology_stack>` |
| STACK.md Frameworks | `<frontend>`, `<backend>` | | STACK.md Frameworks | `<frontend>`, `<backend>` |
@@ -114,7 +114,7 @@ mkdir -p .autocoder/prompts
**Write the spec file** using the XML format from [references/app-spec-format.md](references/app-spec-format.md): **Write the spec file** using the XML format from [references/app-spec-format.md](references/app-spec-format.md):
```bash ```bash
cat > .autocoder/prompts/app_spec.txt << 'EOF' cat > .autoforge/prompts/app_spec.txt << 'EOF'
<project_specification> <project_specification>
<project_name>{from package.json or directory}</project_name> <project_name>{from package.json or directory}</project_name>
@@ -173,9 +173,9 @@ EOF
### Step 5: Verify Generated Spec ### Step 5: Verify Generated Spec
```bash ```bash
head -100 .autocoder/prompts/app_spec.txt head -100 .autoforge/prompts/app_spec.txt
echo "---" echo "---"
grep -c "User can\|System\|API\|Feature" .autocoder/prompts/app_spec.txt || echo "0" grep -c "User can\|System\|API\|Feature" .autoforge/prompts/app_spec.txt || echo "0"
``` ```
**Validation checklist:** **Validation checklist:**
@@ -194,15 +194,15 @@ Output:
app_spec.txt generated from GSD codebase mapping. app_spec.txt generated from GSD codebase mapping.
Source: .planning/codebase/*.md Source: .planning/codebase/*.md
Output: .autocoder/prompts/app_spec.txt Output: .autoforge/prompts/app_spec.txt
Next: Start Autocoder Next: Start AutoForge
cd {project_dir} cd {project_dir}
python ~/projects/autocoder/start.py python ~/projects/autoforge/start.py
Or via UI: Or via UI:
~/projects/autocoder/start_ui.sh ~/projects/autoforge/start_ui.sh
The Initializer will create features.db from this spec. The Initializer will create features.db from this spec.
``` ```

View File

@@ -1,6 +1,6 @@
# Autocoder app_spec.txt XML Format # AutoForge app_spec.txt XML Format
Complete reference for the XML structure expected by Autocoder's Initializer agent. Complete reference for the XML structure expected by AutoForge's Initializer agent.
## Root Structure ## Root Structure
@@ -275,7 +275,7 @@ The Initializer agent expects features distributed across categories:
| Medium web app | 200-250 | 10-15 | | Medium web app | 200-250 | 10-15 |
| Complex full-stack | 300-400 | 15-20 | | Complex full-stack | 300-400 | 15-20 |
## GSD to Autocoder Mapping ## GSD to AutoForge Mapping
When converting from GSD codebase mapping: When converting from GSD codebase mapping:

View File

@@ -36,7 +36,7 @@
# GLM/Alternative API Configuration (Optional) # GLM/Alternative API Configuration (Optional)
# To use Zhipu AI's GLM models instead of Claude, uncomment and set these variables. # To use Zhipu AI's GLM models instead of Claude, uncomment and set these variables.
# This only affects AutoCoder - your global Claude Code settings remain unchanged. # This only affects AutoForge - your global Claude Code settings remain unchanged.
# Get an API key at: https://z.ai/subscribe # Get an API key at: https://z.ai/subscribe
# #
# ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic # ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic

2
.gitignore vendored
View File

@@ -2,6 +2,7 @@
generations/ generations/
automaker/ automaker/
temp/ temp/
temp-docs/
nul nul
issues/ issues/
@@ -114,6 +115,7 @@ Desktop.ini
ui/dist/ ui/dist/
ui/.vite/ ui/.vite/
.vite/ .vite/
*.tgz
# =================== # ===================
# Environment files # Environment files

32
.npmignore Normal file
View File

@@ -0,0 +1,32 @@
venv/
**/__pycache__/
**/*.pyc
.git/
.github/
node_modules/
test_*.py
tests/
generations/
*.db
.env
requirements.txt
CLAUDE.md
LICENSE.md
README.md
ui/src/
ui/node_modules/
ui/tsconfig*.json
ui/vite.config.ts
ui/eslint.config.js
ui/index.html
ui/public/
ui/playwright.config.ts
ui/tests/
start.bat
start_ui.bat
start.sh
start_ui.sh
start_ui.py
.claude/agents/
.claude/skills/
.claude/settings.json

View File

@@ -17,18 +17,28 @@ This is an autonomous coding agent system with a React-based UI. It uses the Cla
## Commands ## Commands
### Quick Start (Recommended) ### npm Global Install (Recommended)
```bash ```bash
# Windows - launches CLI menu npm install -g autoforge-ai
start.bat autoforge # Start server (first run sets up Python venv)
autoforge config # Edit ~/.autoforge/.env in $EDITOR
autoforge config --show # Print active configuration
autoforge --port 9999 # Custom port
autoforge --no-browser # Don't auto-open browser
autoforge --repair # Delete and recreate ~/.autoforge/venv/
```
# macOS/Linux ### From Source (Development)
./start.sh
```bash
# Launch Web UI (serves pre-built React app) # Launch Web UI (serves pre-built React app)
start_ui.bat # Windows start_ui.bat # Windows
./start_ui.sh # macOS/Linux ./start_ui.sh # macOS/Linux
# CLI menu
start.bat # Windows
./start.sh # macOS/Linux
``` ```
### Python Backend (Manual) ### Python Backend (Manual)
@@ -136,11 +146,22 @@ Configuration in `pyproject.toml`:
## Architecture ## Architecture
### npm CLI (bin/, lib/)
The `autoforge` command is a Node.js wrapper that manages the Python environment and server lifecycle:
- `bin/autoforge.js` - Entry point (shebang script)
- `lib/cli.js` - Main CLI logic: Python 3.11+ detection (cross-platform), venv management at `~/.autoforge/venv/` with composite marker (requirements hash + Python version), `.env` config loading from `~/.autoforge/.env`, uvicorn server startup with PID file, and signal handling
- `package.json` - npm package config (`autoforge-ai` on npm), `files` whitelist with `__pycache__` exclusions, `prepublishOnly` builds the UI
- `requirements-prod.txt` - Runtime-only Python deps (excludes ruff, mypy, pytest)
- `.npmignore` - Excludes dev files, tests, UI source from the published tarball
Publishing: `npm publish` (triggers `prepublishOnly` which builds UI, then publishes ~600KB tarball with 84 files)
### Core Python Modules ### Core Python Modules
- `start.py` - CLI launcher with project creation/selection menu - `start.py` - CLI launcher with project creation/selection menu
- `autonomous_agent_demo.py` - Entry point for running the agent (supports `--yolo`, `--parallel`, `--batch-size`, `--batch-features`) - `autonomous_agent_demo.py` - Entry point for running the agent (supports `--yolo`, `--parallel`, `--batch-size`, `--batch-features`)
- `autocoder_paths.py` - Central path resolution with dual-path backward compatibility and migration - `autoforge_paths.py` - Central path resolution with dual-path backward compatibility and migration
- `agent.py` - Agent session loop using Claude Agent SDK - `agent.py` - Agent session loop using Claude Agent SDK
- `client.py` - ClaudeSDKClient configuration with security hooks, MCP servers, and Vertex AI support - `client.py` - ClaudeSDKClient configuration with security hooks, MCP servers, and Vertex AI support
- `security.py` - Bash command allowlist validation (ALLOWED_COMMANDS whitelist) - `security.py` - Bash command allowlist validation (ALLOWED_COMMANDS whitelist)
@@ -158,7 +179,7 @@ Configuration in `pyproject.toml`:
### Project Registry ### Project Registry
Projects can be stored in any directory. The registry maps project names to paths using SQLite: Projects can be stored in any directory. The registry maps project names to paths using SQLite:
- **All platforms**: `~/.autocoder/registry.db` - **All platforms**: `~/.autoforge/registry.db`
The registry uses: The registry uses:
- SQLite database with SQLAlchemy ORM - SQLite database with SQLAlchemy ORM
@@ -245,6 +266,11 @@ Key components:
- `ScheduleModal.tsx` - Schedule management UI - `ScheduleModal.tsx` - Schedule management UI
- `SettingsModal.tsx` - Global settings panel - `SettingsModal.tsx` - Global settings panel
In-app documentation (`/#/docs` route):
- `src/components/docs/sections/` - Content for each doc section (GettingStarted.tsx, AgentSystem.tsx, etc.)
- `src/components/docs/docsData.ts` - Sidebar structure, subsection IDs, search keywords
- `src/components/docs/DocsPage.tsx` - Page layout; `DocsContent.tsx` - section renderer with scroll tracking
Keyboard shortcuts (press `?` for help): Keyboard shortcuts (press `?` for help):
- `D` - Toggle debug panel - `D` - Toggle debug panel
- `G` - Toggle Kanban/Graph view - `G` - Toggle Kanban/Graph view
@@ -254,18 +280,18 @@ Keyboard shortcuts (press `?` for help):
### Project Structure for Generated Apps ### Project Structure for Generated Apps
Projects can be stored in any directory (registered in `~/.autocoder/registry.db`). Each project contains: Projects can be stored in any directory (registered in `~/.autoforge/registry.db`). Each project contains:
- `.autocoder/prompts/app_spec.txt` - Application specification (XML format) - `.autoforge/prompts/app_spec.txt` - Application specification (XML format)
- `.autocoder/prompts/initializer_prompt.md` - First session prompt - `.autoforge/prompts/initializer_prompt.md` - First session prompt
- `.autocoder/prompts/coding_prompt.md` - Continuation session prompt - `.autoforge/prompts/coding_prompt.md` - Continuation session prompt
- `.autocoder/features.db` - SQLite database with feature test cases - `.autoforge/features.db` - SQLite database with feature test cases
- `.autocoder/.agent.lock` - Lock file to prevent multiple agent instances - `.autoforge/.agent.lock` - Lock file to prevent multiple agent instances
- `.autocoder/allowed_commands.yaml` - Project-specific bash command allowlist (optional) - `.autoforge/allowed_commands.yaml` - Project-specific bash command allowlist (optional)
- `.autocoder/.gitignore` - Ignores runtime files - `.autoforge/.gitignore` - Ignores runtime files
- `CLAUDE.md` - Stays at project root (SDK convention) - `CLAUDE.md` - Stays at project root (SDK convention)
- `app_spec.txt` - Root copy for agent template compatibility - `app_spec.txt` - Root copy for agent template compatibility
Legacy projects with files at root level (e.g., `features.db`, `prompts/`) are auto-migrated to `.autocoder/` on next agent start. Dual-path resolution ensures old and new layouts work transparently. Legacy projects with files at root level (e.g., `features.db`, `prompts/`) are auto-migrated to `.autoforge/` on next agent start. Dual-path resolution ensures old and new layouts work transparently.
### Security Model ### Security Model
@@ -311,14 +337,14 @@ The agent's bash command access is controlled through a hierarchical configurati
**Command Hierarchy (highest to lowest priority):** **Command Hierarchy (highest to lowest priority):**
1. **Hardcoded Blocklist** (`security.py`) - NEVER allowed (dd, sudo, shutdown, etc.) 1. **Hardcoded Blocklist** (`security.py`) - NEVER allowed (dd, sudo, shutdown, etc.)
2. **Org Blocklist** (`~/.autocoder/config.yaml`) - Cannot be overridden by projects 2. **Org Blocklist** (`~/.autoforge/config.yaml`) - Cannot be overridden by projects
3. **Org Allowlist** (`~/.autocoder/config.yaml`) - Available to all projects 3. **Org Allowlist** (`~/.autoforge/config.yaml`) - Available to all projects
4. **Global Allowlist** (`security.py`) - Default commands (npm, git, curl, etc.) 4. **Global Allowlist** (`security.py`) - Default commands (npm, git, curl, etc.)
5. **Project Allowlist** (`.autocoder/allowed_commands.yaml`) - Project-specific commands 5. **Project Allowlist** (`.autoforge/allowed_commands.yaml`) - Project-specific commands
**Project Configuration:** **Project Configuration:**
Each project can define custom allowed commands in `.autocoder/allowed_commands.yaml`: Each project can define custom allowed commands in `.autoforge/allowed_commands.yaml`:
```yaml ```yaml
version: 1 version: 1
@@ -338,7 +364,7 @@ commands:
**Organization Configuration:** **Organization Configuration:**
System administrators can set org-wide policies in `~/.autocoder/config.yaml`: System administrators can set org-wide policies in `~/.autoforge/config.yaml`:
```yaml ```yaml
version: 1 version: 1
@@ -405,7 +431,7 @@ Run coding agents using local models via Ollama v0.14.0+:
ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3-coder ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3-coder
ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3-coder ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3-coder
``` ```
5. Run autocoder normally - it will use your local Ollama models 5. Run AutoForge normally - it will use your local Ollama models
**Recommended coding models:** **Recommended coding models:**
- `qwen3-coder` - Good balance of speed and capability - `qwen3-coder` - Good balance of speed and capability
@@ -427,7 +453,7 @@ Run coding agents using local models via Ollama v0.14.0+:
**Slash commands** (`.claude/commands/`): **Slash commands** (`.claude/commands/`):
- `/create-spec` - Interactive spec creation for new projects - `/create-spec` - Interactive spec creation for new projects
- `/expand-project` - Expand existing project with new features - `/expand-project` - Expand existing project with new features
- `/gsd-to-autocoder-spec` - Convert GSD codebase mapping to app_spec.txt - `/gsd-to-autoforge-spec` - Convert GSD codebase mapping to app_spec.txt
- `/check-code` - Run lint and type-check for code quality - `/check-code` - Run lint and type-check for code quality
- `/checkpoint` - Create comprehensive checkpoint commit - `/checkpoint` - Create comprehensive checkpoint commit
- `/review-pr` - Review pull requests - `/review-pr` - Review pull requests
@@ -439,7 +465,7 @@ Run coding agents using local models via Ollama v0.14.0+:
**Skills** (`.claude/skills/`): **Skills** (`.claude/skills/`):
- `frontend-design` - Distinctive, production-grade UI design - `frontend-design` - Distinctive, production-grade UI design
- `gsd-to-autocoder-spec` - Convert GSD codebase mapping to Autocoder app_spec format - `gsd-to-autoforge-spec` - Convert GSD codebase mapping to AutoForge app_spec format
**Other:** **Other:**
- `.claude/templates/` - Prompt templates copied to new projects - `.claude/templates/` - Prompt templates copied to new projects
@@ -449,12 +475,12 @@ Run coding agents using local models via Ollama v0.14.0+:
### Prompt Loading Fallback Chain ### Prompt Loading Fallback Chain
1. Project-specific: `{project_dir}/.autocoder/prompts/{name}.md` (or legacy `{project_dir}/prompts/{name}.md`) 1. Project-specific: `{project_dir}/.autoforge/prompts/{name}.md` (or legacy `{project_dir}/prompts/{name}.md`)
2. Base template: `.claude/templates/{name}.template.md` 2. Base template: `.claude/templates/{name}.template.md`
### Agent Session Flow ### Agent Session Flow
1. Check if `.autocoder/features.db` has features (determines initializer vs coding agent) 1. Check if `.autoforge/features.db` has features (determines initializer vs coding agent)
2. Create ClaudeSDKClient with security settings 2. Create ClaudeSDKClient with security settings
3. Send prompt and stream response 3. Send prompt and stream response
4. Auto-continue with 3-second delay between sessions 4. Auto-continue with 3-second delay between sessions

190
README.md
View File

@@ -1,4 +1,4 @@
# AutoCoder # AutoForge
[![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-FFDD00?style=flat&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/leonvanzyl) [![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-FFDD00?style=flat&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/leonvanzyl)
@@ -14,9 +14,11 @@ A long-running autonomous coding agent powered by the Claude Agent SDK. This too
## Prerequisites ## Prerequisites
### Claude Code CLI (Required) - **Node.js 20+** - Required for the CLI
- **Python 3.11+** - Auto-detected on first run ([download](https://www.python.org/downloads/))
- **Claude Code CLI** - Install and authenticate (see below)
This project requires the Claude Code CLI to be installed. Install it using one of these methods: ### Claude Code CLI (Required)
**macOS / Linux:** **macOS / Linux:**
```bash ```bash
@@ -39,35 +41,63 @@ You need one of the following:
## Quick Start ## Quick Start
### Option 1: Web UI (Recommended) ### Option 1: npm Install (Recommended)
**Windows:**
```cmd
start_ui.bat
```
**macOS / Linux:**
```bash ```bash
./start_ui.sh npm install -g autoforge-ai
autoforge
``` ```
On first run, AutoForge automatically:
1. Checks for Python 3.11+
2. Creates a virtual environment at `~/.autoforge/venv/`
3. Installs Python dependencies
4. Copies a default config file to `~/.autoforge/.env`
5. Starts the server and opens your browser
### CLI Commands
```
autoforge Start the server (default)
autoforge config Open ~/.autoforge/.env in $EDITOR
autoforge config --path Print config file path
autoforge config --show Show active configuration values
autoforge --port PORT Custom port (default: auto from 8888)
autoforge --host HOST Custom host (default: 127.0.0.1)
autoforge --no-browser Don't auto-open browser
autoforge --repair Delete and recreate virtual environment
autoforge --version Print version
autoforge --help Show help
```
### Option 2: From Source (Development)
Clone the repository and use the start scripts directly. This is the recommended path if you want to contribute or modify AutoForge itself.
```bash
git clone https://github.com/leonvanzyl/autoforge.git
cd autoforge
```
**Web UI:**
| Platform | Command |
|---|---|
| Windows | `start_ui.bat` |
| macOS / Linux | `./start_ui.sh` |
This launches the React-based web UI at `http://localhost:5173` with: This launches the React-based web UI at `http://localhost:5173` with:
- Project selection and creation - Project selection and creation
- Kanban board view of features - Kanban board view of features
- Real-time agent output streaming - Real-time agent output streaming
- Start/pause/stop controls - Start/pause/stop controls
### Option 2: CLI Mode **CLI Mode:**
**Windows:** | Platform | Command |
```cmd |---|---|
start.bat | Windows | `start.bat` |
``` | macOS / Linux | `./start.sh` |
**macOS / Linux:**
```bash
./start.sh
```
The start script will: The start script will:
1. Check if Claude CLI is installed 1. Check if Claude CLI is installed
@@ -130,44 +160,43 @@ Features are stored in SQLite via SQLAlchemy and managed through an MCP server t
## Project Structure ## Project Structure
``` ```
autonomous-coding/ autoforge/
├── start.bat # Windows CLI start script ├── bin/ # npm CLI entry point
├── start.sh # macOS/Linux CLI start script ├── lib/ # CLI bootstrap and setup logic
├── start_ui.bat # Windows Web UI start script ├── start.py # CLI menu and project management
├── start_ui.sh # macOS/Linux Web UI start script ├── start_ui.py # Web UI backend (FastAPI server launcher)
├── start.py # CLI menu and project management ├── autonomous_agent_demo.py # Agent entry point
├── start_ui.py # Web UI backend (FastAPI server launcher) ├── agent.py # Agent session logic
├── autonomous_agent_demo.py # Agent entry point ├── client.py # Claude SDK client configuration
├── agent.py # Agent session logic ├── security.py # Bash command allowlist and validation
├── client.py # Claude SDK client configuration ├── progress.py # Progress tracking utilities
├── security.py # Bash command allowlist and validation ├── prompts.py # Prompt loading utilities
├── progress.py # Progress tracking utilities
├── prompts.py # Prompt loading utilities
├── api/ ├── api/
│ └── database.py # SQLAlchemy models (Feature table) │ └── database.py # SQLAlchemy models (Feature table)
├── mcp_server/ ├── mcp_server/
│ └── feature_mcp.py # MCP server for feature management tools │ └── feature_mcp.py # MCP server for feature management tools
├── server/ ├── server/
│ ├── main.py # FastAPI REST API server │ ├── main.py # FastAPI REST API server
│ ├── websocket.py # WebSocket handler for real-time updates │ ├── websocket.py # WebSocket handler for real-time updates
│ ├── schemas.py # Pydantic schemas │ ├── schemas.py # Pydantic schemas
│ ├── routers/ # API route handlers │ ├── routers/ # API route handlers
│ └── services/ # Business logic services │ └── services/ # Business logic services
├── ui/ # React frontend ├── ui/ # React frontend
│ ├── src/ │ ├── src/
│ │ ├── App.tsx # Main app component │ │ ├── App.tsx # Main app component
│ │ ├── hooks/ # React Query and WebSocket hooks │ │ ├── hooks/ # React Query and WebSocket hooks
│ │ └── lib/ # API client and types │ │ └── lib/ # API client and types
│ ├── package.json │ ├── package.json
│ └── vite.config.ts │ └── vite.config.ts
├── .claude/ ├── .claude/
│ ├── commands/ │ ├── commands/
│ │ └── create-spec.md # /create-spec slash command │ │ └── create-spec.md # /create-spec slash command
│ ├── skills/ # Claude Code skills │ ├── skills/ # Claude Code skills
│ └── templates/ # Prompt templates │ └── templates/ # Prompt templates
├── generations/ # Generated projects go here ├── requirements.txt # Python dependencies (development)
├── requirements.txt # Python dependencies ├── requirements-prod.txt # Python dependencies (npm install)
── .env # Optional configuration (N8N webhook) ── package.json # npm package definition
└── .env # Optional configuration
``` ```
--- ---
@@ -264,11 +293,20 @@ The UI receives live updates via WebSocket (`/ws/projects/{project_name}`):
--- ---
## Configuration (Optional) ## Configuration
AutoForge reads configuration from a `.env` file. The file location depends on how you installed AutoForge:
| Install method | Config file location | Edit command |
|---|---|---|
| npm (global) | `~/.autoforge/.env` | `autoforge config` |
| From source | `.env` in the project root | Edit directly |
A default config file is created automatically on first run. Use `autoforge config` to open it in your editor, or `autoforge config --show` to print the active values.
### N8N Webhook Integration ### N8N Webhook Integration
The agent can send progress notifications to an N8N webhook. Create a `.env` file: Add to your `.env` to send progress notifications to an N8N webhook:
```bash ```bash
# Optional: N8N webhook for progress notifications # Optional: N8N webhook for progress notifications
@@ -290,7 +328,7 @@ When test progress increases, the agent sends:
### Using GLM Models (Alternative to Claude) ### Using GLM Models (Alternative to Claude)
To use Zhipu AI's GLM models instead of Claude, add these variables to your `.env` file in the AutoCoder directory: Add these variables to your `.env` file to use Zhipu AI's GLM models:
```bash ```bash
ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
@@ -301,10 +339,40 @@ ANTHROPIC_DEFAULT_OPUS_MODEL=glm-4.7
ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-4.5-air ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-4.5-air
``` ```
This routes AutoCoder's API requests through Zhipu's Claude-compatible API, allowing you to use GLM-4.7 and other models. **This only affects AutoCoder** - your global Claude Code settings remain unchanged. This routes AutoForge's API requests through Zhipu's Claude-compatible API, allowing you to use GLM-4.7 and other models. **This only affects AutoForge** - your global Claude Code settings remain unchanged.
Get an API key at: https://z.ai/subscribe Get an API key at: https://z.ai/subscribe
### Using Ollama Local Models
Add these variables to your `.env` file to run agents with local models via Ollama v0.14.0+:
```bash
ANTHROPIC_BASE_URL=http://localhost:11434
ANTHROPIC_AUTH_TOKEN=ollama
API_TIMEOUT_MS=3000000
ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3-coder
ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3-coder
ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3-coder
```
See the [CLAUDE.md](CLAUDE.md) for recommended models and known limitations.
### Using Vertex AI
Add these variables to your `.env` file to run agents via Google Cloud Vertex AI:
```bash
CLAUDE_CODE_USE_VERTEX=1
CLOUD_ML_REGION=us-east5
ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project-id
ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-5@20251101
ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-5@20250929
ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-3-5-haiku@20241022
```
Requires `gcloud auth application-default login` first. Note the `@` separator (not `-`) in Vertex AI model names.
--- ---
## Customization ## Customization
@@ -335,6 +403,18 @@ This is normal. The initializer agent is generating detailed test cases, which t
**"Command blocked by security hook"** **"Command blocked by security hook"**
The agent tried to run a command not in the allowlist. This is the security system working as intended. If needed, add the command to `ALLOWED_COMMANDS` in `security.py`. The agent tried to run a command not in the allowlist. This is the security system working as intended. If needed, add the command to `ALLOWED_COMMANDS` in `security.py`.
**"Python 3.11+ required but not found"**
Install Python 3.11 or later from [python.org](https://www.python.org/downloads/). Make sure `python3` (or `python` on Windows) is on your PATH.
**"Python venv module not available"**
On Debian/Ubuntu, the venv module is packaged separately. Install it with `sudo apt install python3.XX-venv` (replace `XX` with your Python minor version, e.g., `python3.12-venv`).
**"AutoForge is already running"**
A server instance is already active. Use the browser URL shown in the terminal, or stop the existing instance with Ctrl+C first.
**Virtual environment issues after a Python upgrade**
Run `autoforge --repair` to delete and recreate the virtual environment from scratch.
--- ---
## License ## License

View File

@@ -183,7 +183,7 @@ class ScheduleOverride(Base):
def get_database_path(project_dir: Path) -> Path: def get_database_path(project_dir: Path) -> Path:
"""Return the path to the SQLite database for a project.""" """Return the path to the SQLite database for a project."""
from autocoder_paths import get_features_db_path from autoforge_paths import get_features_db_path
return get_features_db_path(project_dir) return get_features_db_path(project_dir)
@@ -385,7 +385,7 @@ def create_database(project_dir: Path) -> tuple:
db_url = get_database_url(project_dir) db_url = get_database_url(project_dir)
# Ensure parent directory exists (for .autocoder/ layout) # Ensure parent directory exists (for .autoforge/ layout)
db_path = get_database_path(project_dir) db_path = get_database_path(project_dir)
db_path.parent.mkdir(parents=True, exist_ok=True) db_path.parent.mkdir(parents=True, exist_ok=True)

View File

@@ -1,17 +1,19 @@
""" """
Autocoder Path Resolution AutoForge Path Resolution
========================= =========================
Central module for resolving paths to autocoder-generated files within a project. Central module for resolving paths to autoforge-generated files within a project.
Implements a dual-path resolution strategy for backward compatibility: Implements a tri-path resolution strategy for backward compatibility:
1. Check ``project_dir / ".autocoder" / X`` (new layout) 1. Check ``project_dir / ".autoforge" / X`` (current layout)
2. Check ``project_dir / X`` (legacy root-level layout) 2. Check ``project_dir / ".autocoder" / X`` (legacy layout)
3. Default to the new location for fresh projects 3. Check ``project_dir / X`` (legacy root-level layout)
4. Default to the new location for fresh projects
This allows existing projects with root-level ``features.db``, ``.agent.lock``, This allows existing projects with root-level ``features.db``, ``.agent.lock``,
etc. to keep working while new projects store everything under ``.autocoder/``. etc. to keep working while new projects store everything under ``.autoforge/``.
Projects using the old ``.autocoder/`` directory are auto-migrated on next start.
The ``migrate_project_layout`` function can move an old-layout project to the The ``migrate_project_layout`` function can move an old-layout project to the
new layout safely, with full integrity checks for SQLite databases. new layout safely, with full integrity checks for SQLite databases.
@@ -25,10 +27,10 @@ from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# .gitignore content written into every .autocoder/ directory # .gitignore content written into every .autoforge/ directory
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_GITIGNORE_CONTENT = """\ _GITIGNORE_CONTENT = """\
# Autocoder runtime files # AutoForge runtime files
features.db features.db
features.db-wal features.db-wal
features.db-shm features.db-shm
@@ -49,15 +51,18 @@ assistant.db-shm
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _resolve_path(project_dir: Path, filename: str) -> Path: def _resolve_path(project_dir: Path, filename: str) -> Path:
"""Resolve a file path using dual-path strategy. """Resolve a file path using tri-path strategy.
Checks the new ``.autocoder/`` location first, then falls back to the Checks the new ``.autoforge/`` location first, then the legacy
legacy root-level location. If neither exists, returns the new location ``.autocoder/`` location, then the root-level location. If none exist,
so that newly-created files land in ``.autocoder/``. returns the new location so that newly-created files land in ``.autoforge/``.
""" """
new = project_dir / ".autocoder" / filename new = project_dir / ".autoforge" / filename
if new.exists(): if new.exists():
return new return new
legacy = project_dir / ".autocoder" / filename
if legacy.exists():
return legacy
old = project_dir / filename old = project_dir / filename
if old.exists(): if old.exists():
return old return old
@@ -65,14 +70,17 @@ def _resolve_path(project_dir: Path, filename: str) -> Path:
def _resolve_dir(project_dir: Path, dirname: str) -> Path: def _resolve_dir(project_dir: Path, dirname: str) -> Path:
"""Resolve a directory path using dual-path strategy. """Resolve a directory path using tri-path strategy.
Same logic as ``_resolve_path`` but intended for directories such as Same logic as ``_resolve_path`` but intended for directories such as
``prompts/``. ``prompts/``.
""" """
new = project_dir / ".autocoder" / dirname new = project_dir / ".autoforge" / dirname
if new.exists(): if new.exists():
return new return new
legacy = project_dir / ".autocoder" / dirname
if legacy.exists():
return legacy
old = project_dir / dirname old = project_dir / dirname
if old.exists(): if old.exists():
return old return old
@@ -80,27 +88,27 @@ def _resolve_dir(project_dir: Path, dirname: str) -> Path:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# .autocoder directory management # .autoforge directory management
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def get_autocoder_dir(project_dir: Path) -> Path: def get_autoforge_dir(project_dir: Path) -> Path:
"""Return the ``.autocoder`` directory path. Does NOT create it.""" """Return the ``.autoforge`` directory path. Does NOT create it."""
return project_dir / ".autocoder" return project_dir / ".autoforge"
def ensure_autocoder_dir(project_dir: Path) -> Path: def ensure_autoforge_dir(project_dir: Path) -> Path:
"""Create the ``.autocoder/`` directory (if needed) and write its ``.gitignore``. """Create the ``.autoforge/`` directory (if needed) and write its ``.gitignore``.
Returns: Returns:
The path to the ``.autocoder`` directory. The path to the ``.autoforge`` directory.
""" """
autocoder_dir = get_autocoder_dir(project_dir) autoforge_dir = get_autoforge_dir(project_dir)
autocoder_dir.mkdir(parents=True, exist_ok=True) autoforge_dir.mkdir(parents=True, exist_ok=True)
gitignore_path = autocoder_dir / ".gitignore" gitignore_path = autoforge_dir / ".gitignore"
gitignore_path.write_text(_GITIGNORE_CONTENT, encoding="utf-8") gitignore_path.write_text(_GITIGNORE_CONTENT, encoding="utf-8")
return autocoder_dir return autoforge_dir
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -154,9 +162,9 @@ def get_prompts_dir(project_dir: Path) -> Path:
def get_expand_settings_path(project_dir: Path, uuid_hex: str) -> Path: def get_expand_settings_path(project_dir: Path, uuid_hex: str) -> Path:
"""Return the path for an ephemeral expand-session settings file. """Return the path for an ephemeral expand-session settings file.
These files are short-lived and always stored in ``.autocoder/``. These files are short-lived and always stored in ``.autoforge/``.
""" """
return project_dir / ".autocoder" / f".claude_settings.expand.{uuid_hex}.json" return project_dir / ".autoforge" / f".claude_settings.expand.{uuid_hex}.json"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -166,8 +174,9 @@ def get_expand_settings_path(project_dir: Path, uuid_hex: str) -> Path:
def has_agent_running(project_dir: Path) -> bool: def has_agent_running(project_dir: Path) -> bool:
"""Check whether any agent or dev-server lock file exists at either location. """Check whether any agent or dev-server lock file exists at either location.
Inspects both the legacy root-level paths and the new ``.autocoder/`` Inspects the legacy root-level paths, the old ``.autocoder/`` paths, and
paths so that a running agent is detected regardless of project layout. the new ``.autoforge/`` paths so that a running agent is detected
regardless of project layout.
Returns: Returns:
``True`` if any ``.agent.lock`` or ``.devserver.lock`` exists. ``True`` if any ``.agent.lock`` or ``.devserver.lock`` exists.
@@ -176,8 +185,11 @@ def has_agent_running(project_dir: Path) -> bool:
for name in lock_names: for name in lock_names:
if (project_dir / name).exists(): if (project_dir / name).exists():
return True return True
# Check both old and new directory names for backward compatibility
if (project_dir / ".autocoder" / name).exists(): if (project_dir / ".autocoder" / name).exists():
return True return True
if (project_dir / ".autoforge" / name).exists():
return True
return False return False
@@ -186,7 +198,7 @@ def has_agent_running(project_dir: Path) -> bool:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def migrate_project_layout(project_dir: Path) -> list[str]: def migrate_project_layout(project_dir: Path) -> list[str]:
"""Migrate a project from the legacy root-level layout to ``.autocoder/``. """Migrate a project from the legacy root-level layout to ``.autoforge/``.
The migration is incremental and safe: The migration is incremental and safe:
@@ -199,7 +211,7 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
Returns: Returns:
A list of human-readable descriptions of what was migrated, e.g. A list of human-readable descriptions of what was migrated, e.g.
``["prompts/ -> .autocoder/prompts/", "features.db -> .autocoder/features.db"]``. ``["prompts/ -> .autoforge/prompts/", "features.db -> .autoforge/features.db"]``.
An empty list means nothing was migrated (either everything is An empty list means nothing was migrated (either everything is
already migrated, or the agent is running). already migrated, or the agent is running).
""" """
@@ -208,18 +220,31 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
logger.warning("Migration skipped: agent or dev-server is running for %s", project_dir) logger.warning("Migration skipped: agent or dev-server is running for %s", project_dir)
return [] return []
autocoder_dir = ensure_autocoder_dir(project_dir) # --- 0. Migrate .autocoder/ → .autoforge/ directory -------------------
migrated: list[str] = [] old_autocoder_dir = project_dir / ".autocoder"
new_autoforge_dir = project_dir / ".autoforge"
if old_autocoder_dir.exists() and old_autocoder_dir.is_dir() and not new_autoforge_dir.exists():
try:
old_autocoder_dir.rename(new_autoforge_dir)
logger.info("Migrated .autocoder/ -> .autoforge/")
migrated: list[str] = [".autocoder/ -> .autoforge/"]
except Exception:
logger.warning("Failed to migrate .autocoder/ -> .autoforge/", exc_info=True)
migrated = []
else:
migrated = []
autoforge_dir = ensure_autoforge_dir(project_dir)
# --- 1. Migrate prompts/ directory ----------------------------------- # --- 1. Migrate prompts/ directory -----------------------------------
try: try:
old_prompts = project_dir / "prompts" old_prompts = project_dir / "prompts"
new_prompts = autocoder_dir / "prompts" new_prompts = autoforge_dir / "prompts"
if old_prompts.exists() and old_prompts.is_dir() and not new_prompts.exists(): if old_prompts.exists() and old_prompts.is_dir() and not new_prompts.exists():
shutil.copytree(str(old_prompts), str(new_prompts)) shutil.copytree(str(old_prompts), str(new_prompts))
shutil.rmtree(str(old_prompts)) shutil.rmtree(str(old_prompts))
migrated.append("prompts/ -> .autocoder/prompts/") migrated.append("prompts/ -> .autoforge/prompts/")
logger.info("Migrated prompts/ -> .autocoder/prompts/") logger.info("Migrated prompts/ -> .autoforge/prompts/")
except Exception: except Exception:
logger.warning("Failed to migrate prompts/ directory", exc_info=True) logger.warning("Failed to migrate prompts/ directory", exc_info=True)
@@ -228,7 +253,7 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
for db_name in db_names: for db_name in db_names:
try: try:
old_db = project_dir / db_name old_db = project_dir / db_name
new_db = autocoder_dir / db_name new_db = autoforge_dir / db_name
if old_db.exists() and not new_db.exists(): if old_db.exists() and not new_db.exists():
# Flush WAL to ensure all data is in the main database file # Flush WAL to ensure all data is in the main database file
conn = sqlite3.connect(str(old_db)) conn = sqlite3.connect(str(old_db))
@@ -263,8 +288,8 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
wal_file = project_dir / f"{db_name}{suffix}" wal_file = project_dir / f"{db_name}{suffix}"
wal_file.unlink(missing_ok=True) wal_file.unlink(missing_ok=True)
migrated.append(f"{db_name} -> .autocoder/{db_name}") migrated.append(f"{db_name} -> .autoforge/{db_name}")
logger.info("Migrated %s -> .autocoder/%s", db_name, db_name) logger.info("Migrated %s -> .autoforge/%s", db_name, db_name)
except Exception: except Exception:
logger.warning("Failed to migrate %s", db_name, exc_info=True) logger.warning("Failed to migrate %s", db_name, exc_info=True)
@@ -279,11 +304,11 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
for filename in simple_files: for filename in simple_files:
try: try:
old_file = project_dir / filename old_file = project_dir / filename
new_file = autocoder_dir / filename new_file = autoforge_dir / filename
if old_file.exists() and not new_file.exists(): if old_file.exists() and not new_file.exists():
shutil.move(str(old_file), str(new_file)) shutil.move(str(old_file), str(new_file))
migrated.append(f"{filename} -> .autocoder/{filename}") migrated.append(f"{filename} -> .autoforge/{filename}")
logger.info("Migrated %s -> .autocoder/%s", filename, filename) logger.info("Migrated %s -> .autoforge/%s", filename, filename)
except Exception: except Exception:
logger.warning("Failed to migrate %s", filename, exc_info=True) logger.warning("Failed to migrate %s", filename, exc_info=True)

View File

@@ -221,11 +221,11 @@ def main() -> None:
print("Use an absolute path or register the project first.") print("Use an absolute path or register the project first.")
return return
# Migrate project layout to .autocoder/ if needed (idempotent, safe) # Migrate project layout to .autoforge/ if needed (idempotent, safe)
from autocoder_paths import migrate_project_layout from autoforge_paths import migrate_project_layout
migrated = migrate_project_layout(project_dir) migrated = migrate_project_layout(project_dir)
if migrated: if migrated:
print(f"Migrated project files to .autocoder/: {', '.join(migrated)}", flush=True) print(f"Migrated project files to .autoforge/: {', '.join(migrated)}", flush=True)
# Parse batch testing feature IDs (comma-separated string -> list[int]) # Parse batch testing feature IDs (comma-separated string -> list[int])
testing_feature_ids: list[int] | None = None testing_feature_ids: list[int] | None = None

3
bin/autoforge.js Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
import { run } from '../lib/cli.js';
run(process.argv.slice(2));

View File

@@ -382,7 +382,7 @@ def create_client(
project_dir.mkdir(parents=True, exist_ok=True) project_dir.mkdir(parents=True, exist_ok=True)
# Write settings to a file in the project directory # Write settings to a file in the project directory
from autocoder_paths import get_claude_settings_path from autoforge_paths import get_claude_settings_path
settings_file = get_claude_settings_path(project_dir) settings_file = get_claude_settings_path(project_dir)
settings_file.parent.mkdir(parents=True, exist_ok=True) settings_file.parent.mkdir(parents=True, exist_ok=True)
with open(settings_file, "w") as f: with open(settings_file, "w") as f:
@@ -450,7 +450,7 @@ def create_client(
# Build environment overrides for API endpoint configuration # Build environment overrides for API endpoint configuration
# These override system env vars for the Claude CLI subprocess, # These override system env vars for the Claude CLI subprocess,
# allowing AutoCoder to use alternative APIs (e.g., GLM) without # allowing AutoForge to use alternative APIs (e.g., GLM) without
# affecting the user's global Claude Code settings # affecting the user's global Claude Code settings
sdk_env = {} sdk_env = {}
for var in API_ENV_VARS: for var in API_ENV_VARS:

View File

@@ -7,7 +7,7 @@ subprocesses. Imported by both ``client.py`` (agent sessions) and
``server/services/chat_constants.py`` (chat sessions) to avoid maintaining ``server/services/chat_constants.py`` (chat sessions) to avoid maintaining
duplicate lists. duplicate lists.
These allow autocoder to use alternative API endpoints (Ollama, GLM, These allow autoforge to use alternative API endpoints (Ollama, GLM,
Vertex AI) without affecting the user's global Claude Code settings. Vertex AI) without affecting the user's global Claude Code settings.
""" """

View File

@@ -179,7 +179,7 @@ To see what you can reduce:
```bash ```bash
# Count commands by prefix # Count commands by prefix
grep "^ - name:" .autocoder/allowed_commands.yaml | \ grep "^ - name:" .autoforge/allowed_commands.yaml | \
sed 's/^ - name: //' | \ sed 's/^ - name: //' | \
cut -d' ' -f1 | \ cut -d' ' -f1 | \
sort | uniq -c | sort -rn sort | uniq -c | sort -rn

View File

@@ -1,4 +1,4 @@
# AutoCoder Security Configuration Examples # AutoForge Security Configuration Examples
This directory contains example configuration files for controlling which bash commands the autonomous coding agent can execute. This directory contains example configuration files for controlling which bash commands the autonomous coding agent can execute.
@@ -18,11 +18,11 @@ This directory contains example configuration files for controlling which bash c
### For a Single Project (Most Common) ### For a Single Project (Most Common)
When you create a new project with AutoCoder, it automatically creates: When you create a new project with AutoForge, it automatically creates:
```text ```text
my-project/ my-project/
.autocoder/ .autoforge/
allowed_commands.yaml ← Automatically created from template allowed_commands.yaml ← Automatically created from template
``` ```
@@ -34,17 +34,17 @@ If you want commands available across **all projects**, manually create:
```bash ```bash
# Copy the example to your home directory # Copy the example to your home directory
cp examples/org_config.yaml ~/.autocoder/config.yaml cp examples/org_config.yaml ~/.autoforge/config.yaml
# Edit it to add org-wide commands # Edit it to add org-wide commands
nano ~/.autocoder/config.yaml nano ~/.autoforge/config.yaml
``` ```
--- ---
## Project-Level Configuration ## Project-Level Configuration
**File:** `{project_dir}/.autocoder/allowed_commands.yaml` **File:** `{project_dir}/.autoforge/allowed_commands.yaml`
**Purpose:** Define commands needed for THIS specific project. **Purpose:** Define commands needed for THIS specific project.
@@ -82,7 +82,7 @@ commands:
## Organization-Level Configuration ## Organization-Level Configuration
**File:** `~/.autocoder/config.yaml` **File:** `~/.autoforge/config.yaml`
**Purpose:** Define commands and policies for ALL projects. **Purpose:** Define commands and policies for ALL projects.
@@ -127,13 +127,13 @@ When the agent tries to run a command, the system checks in this order:
└─────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────┐
│ 2. ORG BLOCKLIST (~/.autocoder/config.yaml) │ │ 2. ORG BLOCKLIST (~/.autoforge/config.yaml) │
│ Commands you block organization-wide │ │ Commands you block organization-wide │
│ ❌ Projects CANNOT override these │ │ ❌ Projects CANNOT override these │
└─────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────┐
│ 3. ORG ALLOWLIST (~/.autocoder/config.yaml) │ │ 3. ORG ALLOWLIST (~/.autoforge/config.yaml) │
│ Commands available to all projects │ │ Commands available to all projects │
│ ✅ Automatically available │ │ ✅ Automatically available │
└─────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────┘
@@ -145,7 +145,7 @@ When the agent tries to run a command, the system checks in this order:
└─────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────┐
│ 5. PROJECT ALLOWLIST (.autocoder/allowed_commands) │ │ 5. PROJECT ALLOWLIST (.autoforge/allowed_commands) │
│ Project-specific commands │ │ Project-specific commands │
│ ✅ Available only to this project │ │ ✅ Available only to this project │
└─────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────┘
@@ -195,7 +195,7 @@ Matches:
### iOS Development ### iOS Development
**Project config** (`.autocoder/allowed_commands.yaml`): **Project config** (`.autoforge/allowed_commands.yaml`):
```yaml ```yaml
version: 1 version: 1
commands: commands:
@@ -245,7 +245,7 @@ commands:
### Enterprise Organization (Restrictive) ### Enterprise Organization (Restrictive)
**Org config** (`~/.autocoder/config.yaml`): **Org config** (`~/.autoforge/config.yaml`):
```yaml ```yaml
version: 1 version: 1
@@ -265,7 +265,7 @@ blocked_commands:
### Startup Team (Permissive) ### Startup Team (Permissive)
**Org config** (`~/.autocoder/config.yaml`): **Org config** (`~/.autoforge/config.yaml`):
```yaml ```yaml
version: 1 version: 1
@@ -394,7 +394,7 @@ These commands are **NEVER allowed**, even with user approval:
**Solution:** Add the command to your project config: **Solution:** Add the command to your project config:
```yaml ```yaml
# In .autocoder/allowed_commands.yaml # In .autoforge/allowed_commands.yaml
commands: commands:
- name: X - name: X
description: What this command does description: What this command does
@@ -405,7 +405,7 @@ commands:
**Cause:** The command is in the org blocklist or hardcoded blocklist. **Cause:** The command is in the org blocklist or hardcoded blocklist.
**Solution:** **Solution:**
- If in org blocklist: Edit `~/.autocoder/config.yaml` to remove it - If in org blocklist: Edit `~/.autoforge/config.yaml` to remove it
- If in hardcoded blocklist: Cannot be allowed (by design) - If in hardcoded blocklist: Cannot be allowed (by design)
### Error: "Could not parse YAML config" ### Error: "Could not parse YAML config"
@@ -422,8 +422,8 @@ commands:
**Solution:** **Solution:**
1. Restart the agent (changes are loaded on startup) 1. Restart the agent (changes are loaded on startup)
2. Verify file location: 2. Verify file location:
- Project: `{project}/.autocoder/allowed_commands.yaml` - Project: `{project}/.autoforge/allowed_commands.yaml`
- Org: `~/.autocoder/config.yaml` (must be manually created) - Org: `~/.autoforge/config.yaml` (must be manually created)
3. Check YAML is valid (run through a YAML validator) 3. Check YAML is valid (run through a YAML validator)
--- ---
@@ -432,7 +432,7 @@ commands:
### Running the Tests ### Running the Tests
AutoCoder has comprehensive tests for the security system: AutoForge has comprehensive tests for the security system:
**Unit Tests** (136 tests - fast): **Unit Tests** (136 tests - fast):
```bash ```bash
@@ -481,7 +481,7 @@ python start.py
cd path/to/security-test cd path/to/security-test
# Edit the config # Edit the config
nano .autocoder/allowed_commands.yaml nano .autoforge/allowed_commands.yaml
``` ```
**3. Add a test command (e.g., Swift):** **3. Add a test command (e.g., Swift):**
@@ -509,7 +509,7 @@ Or:
```text ```text
Command 'wget' is not allowed. Command 'wget' is not allowed.
To allow this command: To allow this command:
1. Add to .autocoder/allowed_commands.yaml for this project, OR 1. Add to .autoforge/allowed_commands.yaml for this project, OR
2. Request mid-session approval (the agent can ask) 2. Request mid-session approval (the agent can ask)
``` ```

View File

@@ -1,6 +1,6 @@
# Organization-Level AutoCoder Configuration # Organization-Level AutoForge Configuration
# ============================================ # ============================================
# Location: ~/.autocoder/config.yaml # Location: ~/.autoforge/config.yaml
# #
# IMPORTANT: This file is OPTIONAL and must be manually created by you. # IMPORTANT: This file is OPTIONAL and must be manually created by you.
# It does NOT exist by default. # It does NOT exist by default.
@@ -22,7 +22,7 @@ version: 1
# Organization-Wide Allowed Commands # Organization-Wide Allowed Commands
# ========================================== # ==========================================
# These commands become available to ALL projects automatically. # These commands become available to ALL projects automatically.
# Projects don't need to add them to their own .autocoder/allowed_commands.yaml # Projects don't need to add them to their own .autoforge/allowed_commands.yaml
# #
# By default, this is empty. Uncomment and add commands as needed. # By default, this is empty. Uncomment and add commands as needed.
@@ -122,7 +122,7 @@ approval_timeout_minutes: 5
# Default commands: npm, git, curl, ls, cat, etc. # Default commands: npm, git, curl, ls, cat, etc.
# Always available to all projects. # Always available to all projects.
# #
# 5. Project Allowed Commands (.autocoder/allowed_commands.yaml) # 5. Project Allowed Commands (.autoforge/allowed_commands.yaml)
# Project-specific commands defined in each project. # Project-specific commands defined in each project.
# LOWEST PRIORITY (can't override blocks above). # LOWEST PRIORITY (can't override blocks above).
# #
@@ -165,7 +165,7 @@ approval_timeout_minutes: 5
# ========================================== # ==========================================
# To Create This File # To Create This File
# ========================================== # ==========================================
# 1. Copy this example to: ~/.autocoder/config.yaml # 1. Copy this example to: ~/.autoforge/config.yaml
# 2. Uncomment and customize the sections you need # 2. Uncomment and customize the sections you need
# 3. Leave empty lists if you don't need org-level controls # 3. Leave empty lists if you don't need org-level controls
# #

View File

@@ -1,12 +1,12 @@
# Project-Specific Allowed Commands # Project-Specific Allowed Commands
# ================================== # ==================================
# Location: {project_dir}/.autocoder/allowed_commands.yaml # Location: {project_dir}/.autoforge/allowed_commands.yaml
# #
# This file defines bash commands that the autonomous coding agent can use # This file defines bash commands that the autonomous coding agent can use
# for THIS SPECIFIC PROJECT, beyond the default allowed commands. # for THIS SPECIFIC PROJECT, beyond the default allowed commands.
# #
# When you create a new project, AutoCoder automatically creates this file # When you create a new project, AutoForge automatically creates this file
# in your project's .autocoder/ directory. You can customize it for your # in your project's .autoforge/ directory. You can customize it for your
# project's specific needs (iOS, Rust, Python, etc.). # project's specific needs (iOS, Rust, Python, etc.).
version: 1 version: 1
@@ -115,7 +115,7 @@ commands: []
# Limits: # Limits:
# - Maximum 100 commands per project # - Maximum 100 commands per project
# - Commands in the blocklist (sudo, dd, shutdown, etc.) can NEVER be allowed # - Commands in the blocklist (sudo, dd, shutdown, etc.) can NEVER be allowed
# - Org-level blocked commands (see ~/.autocoder/config.yaml) cannot be overridden # - Org-level blocked commands (see ~/.autoforge/config.yaml) cannot be overridden
# #
# Default Allowed Commands (always available): # Default Allowed Commands (always available):
# File operations: ls, cat, head, tail, wc, grep, cp, mkdir, mv, rm, touch # File operations: ls, cat, head, tail, wc, grep, cp, mkdir, mv, rm, touch

791
lib/cli.js Normal file
View File

@@ -0,0 +1,791 @@
/**
* AutoForge CLI
* =============
*
* Main CLI module for the AutoForge npm global package.
* Handles Python detection, virtual environment management,
* config loading, and uvicorn server lifecycle.
*
* Uses only Node.js built-in modules -- no external dependencies.
*/
import { execFileSync, spawn, execSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync, copyFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { createServer } from 'node:net';
import { homedir, platform } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
// ---------------------------------------------------------------------------
// Path constants
// ---------------------------------------------------------------------------
/** Root of the autoforge npm package (one level up from lib/) */
const PKG_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
/** User config home: ~/.autoforge/ */
const CONFIG_HOME = join(homedir(), '.autoforge');
/** Virtual-environment directory managed by the CLI */
const VENV_DIR = join(CONFIG_HOME, 'venv');
/** Composite marker written after a successful pip install */
const DEPS_MARKER = join(VENV_DIR, '.deps-installed');
/** PID file for the running server */
const PID_FILE = join(CONFIG_HOME, 'server.pid');
/** Path to the production requirements file inside the package */
const REQUIREMENTS_FILE = join(PKG_DIR, 'requirements-prod.txt');
/** Path to the .env example shipped with the package */
const ENV_EXAMPLE = join(PKG_DIR, '.env.example');
/** User .env config file */
const ENV_FILE = join(CONFIG_HOME, '.env');
const IS_WIN = platform() === 'win32';
// ---------------------------------------------------------------------------
// Package version (read lazily via createRequire)
// ---------------------------------------------------------------------------
const require = createRequire(import.meta.url);
const { version: VERSION } = require(join(PKG_DIR, 'package.json'));
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Indented console output matching the spec format. */
function log(msg = '') {
console.log(` ${msg}`);
}
/** Print a fatal error and exit. */
function die(msg) {
console.error(`\n Error: ${msg}\n`);
process.exit(1);
}
/**
* Parse a Python version string like "Python 3.13.6" and return
* { major, minor, patch, raw } or null on failure.
*/
function parsePythonVersion(raw) {
const m = raw.match(/Python\s+(\d+)\.(\d+)\.(\d+)/);
if (!m) return null;
return {
major: Number(m[1]),
minor: Number(m[2]),
patch: Number(m[3]),
raw: `${m[1]}.${m[2]}.${m[3]}`,
};
}
/**
* Try a single Python candidate. Returns { exe, version } or null.
* `candidate` is either a bare name or an array of args (e.g. ['py', '-3']).
*/
function tryPythonCandidate(candidate) {
const args = Array.isArray(candidate) ? candidate : [candidate];
const exe = args[0];
const extraArgs = args.slice(1);
try {
const out = execFileSync(exe, [...extraArgs, '--version'], {
encoding: 'utf8',
timeout: 10_000,
stdio: ['pipe', 'pipe', 'pipe'],
});
const ver = parsePythonVersion(out);
if (!ver) return null;
// Require 3.11+
if (ver.major < 3 || (ver.major === 3 && ver.minor < 11)) {
return { exe: args.join(' '), version: ver, tooOld: true };
}
return { exe: args.join(' '), version: ver, tooOld: false };
} catch {
return null;
}
}
// ---------------------------------------------------------------------------
// Python detection
// ---------------------------------------------------------------------------
/**
* Find a suitable Python >= 3.11 interpreter.
*
* Search order is platform-dependent:
* Windows: python -> py -3 -> python3
* macOS/Linux: python3 -> python
*
* The AUTOFORGE_PYTHON env var overrides automatic detection.
*
* After finding a candidate we also verify that the venv module is
* available (Debian/Ubuntu strip it out of the base package).
*/
function findPython() {
// Allow explicit override via environment variable
const override = process.env.AUTOFORGE_PYTHON;
if (override) {
const result = tryPythonCandidate(override);
if (!result) {
die(`AUTOFORGE_PYTHON is set to "${override}" but it could not be executed.`);
}
if (result.tooOld) {
die(
`Python ${result.version.raw} found (via AUTOFORGE_PYTHON), but 3.11+ required.\n` +
' Install Python 3.11+ from https://python.org'
);
}
return result;
}
// Platform-specific candidate order
const candidates = IS_WIN
? ['python', ['py', '-3'], 'python3']
: ['python3', 'python'];
let bestTooOld = null;
for (const candidate of candidates) {
const result = tryPythonCandidate(candidate);
if (!result) continue;
if (result.tooOld) {
// Remember the first "too old" result for a better error message
if (!bestTooOld) bestTooOld = result;
continue;
}
// Verify venv module is available (Debian/Ubuntu may need python3-venv)
try {
const exeParts = result.exe.split(' ');
execFileSync(exeParts[0], [...exeParts.slice(1), '-c', 'import ensurepip'], {
encoding: 'utf8',
timeout: 10_000,
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch {
die(
`Python venv module not available.\n` +
` Run: sudo apt install python3.${result.version.minor}-venv`
);
}
return result;
}
// Provide the most helpful error message we can
if (bestTooOld) {
die(
`Python ${bestTooOld.version.raw} found, but 3.11+ required.\n` +
' Install Python 3.11+ from https://python.org'
);
}
die(
'Python 3.11+ required but not found.\n' +
' Install from https://python.org'
);
}
// ---------------------------------------------------------------------------
// Venv management
// ---------------------------------------------------------------------------
/** Return the path to the Python executable inside the venv. */
function venvPython() {
return IS_WIN
? join(VENV_DIR, 'Scripts', 'python.exe')
: join(VENV_DIR, 'bin', 'python');
}
/** SHA-256 hash of the requirements-prod.txt file contents. */
function requirementsHash() {
const content = readFileSync(REQUIREMENTS_FILE, 'utf8');
return createHash('sha256').update(content).digest('hex');
}
/**
* Read the composite deps marker. Returns the parsed JSON object
* or null if the file is missing / corrupt.
*/
function readMarker() {
try {
return JSON.parse(readFileSync(DEPS_MARKER, 'utf8'));
} catch {
return null;
}
}
/**
* Ensure the virtual environment exists and dependencies are installed.
* Returns true if all setup steps were already satisfied (fast path).
*
* @param {object} python - The result of findPython()
* @param {boolean} forceRecreate - If true, delete and recreate the venv
*/
function ensureVenv(python, forceRecreate) {
mkdirSync(CONFIG_HOME, { recursive: true });
const marker = readMarker();
const reqHash = requirementsHash();
const pyExe = venvPython();
// Determine if the venv itself needs to be (re)created
let needsCreate = forceRecreate || !existsSync(pyExe);
if (!needsCreate && marker) {
// Recreate if Python major.minor changed
const markerMinor = marker.python_version;
const currentMinor = `${python.version.major}.${python.version.minor}`;
if (markerMinor && markerMinor !== currentMinor) {
needsCreate = true;
}
// Recreate if the recorded python path no longer exists
if (marker.python_path && !existsSync(marker.python_path)) {
needsCreate = true;
}
}
let depsUpToDate = false;
if (!needsCreate && marker && marker.requirements_hash === reqHash) {
depsUpToDate = true;
}
// Fast path: nothing to do
if (!needsCreate && depsUpToDate) {
return true;
}
// --- Slow path: show setup progress ---
log('[2/3] Setting up environment...');
if (needsCreate) {
if (existsSync(VENV_DIR)) {
log(' Removing old virtual environment...');
rmSync(VENV_DIR, { recursive: true, force: true });
}
log(` Creating virtual environment at ~/.autoforge/venv/`);
const exeParts = python.exe.split(' ');
try {
execFileSync(exeParts[0], [...exeParts.slice(1), '-m', 'venv', VENV_DIR], {
encoding: 'utf8',
timeout: 120_000,
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch (err) {
die(`Failed to create virtual environment: ${err.message}`);
}
}
// Install / update dependencies
log(' Installing dependencies...');
try {
execFileSync(pyExe, ['-m', 'pip', 'install', '-q', '--upgrade', 'pip'], {
encoding: 'utf8',
timeout: 300_000,
stdio: ['pipe', 'pipe', 'pipe'],
});
execFileSync(pyExe, ['-m', 'pip', 'install', '-q', '-r', REQUIREMENTS_FILE], {
encoding: 'utf8',
timeout: 600_000,
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch (err) {
die(`Failed to install dependencies: ${err.message}`);
}
// Write marker only after pip succeeds to prevent partial state
const markerData = {
requirements_hash: reqHash,
python_version: `${python.version.major}.${python.version.minor}`,
python_path: pyExe,
created_at: new Date().toISOString(),
};
writeFileSync(DEPS_MARKER, JSON.stringify(markerData, null, 2), 'utf8');
log(' Done');
return false;
}
// ---------------------------------------------------------------------------
// Config (.env) management
// ---------------------------------------------------------------------------
/**
* Parse a .env file into a plain object.
* Handles comments, blank lines, and quoted values.
*/
function parseEnvFile(filePath) {
const env = {};
if (!existsSync(filePath)) return env;
const lines = readFileSync(filePath, 'utf8').split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIdx = trimmed.indexOf('=');
if (eqIdx === -1) continue;
const key = trimmed.slice(0, eqIdx).trim();
let value = trimmed.slice(eqIdx + 1).trim();
// Strip matching quotes (single or double)
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
if (key) {
env[key] = value;
}
}
return env;
}
/**
* Ensure ~/.autoforge/.env exists. On first run, copy .env.example
* from the package directory and print a notice.
*
* Returns true if the file was newly created.
*/
function ensureEnvFile() {
if (existsSync(ENV_FILE)) return false;
mkdirSync(CONFIG_HOME, { recursive: true });
if (existsSync(ENV_EXAMPLE)) {
copyFileSync(ENV_EXAMPLE, ENV_FILE);
} else {
// Fallback: create a minimal placeholder
writeFileSync(ENV_FILE, '# AutoForge configuration\n# See documentation for available options.\n', 'utf8');
}
return true;
}
// ---------------------------------------------------------------------------
// Port detection
// ---------------------------------------------------------------------------
/**
* Find an available TCP port starting from `start`.
* Tries by actually binding a socket (most reliable cross-platform approach).
*/
function findAvailablePort(start = 8888, maxAttempts = 20) {
for (let port = start; port < start + maxAttempts; port++) {
try {
const server = createServer();
// Use a synchronous-like approach: try to listen, then close immediately
const result = new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(port, '127.0.0.1', () => {
server.close(() => resolve(port));
});
});
// We cannot await here (sync context), so use the blocking approach:
// Try to bind synchronously using a different technique.
server.close();
} catch {
// fall through
}
}
// Synchronous fallback: try to connect; if connection refused, port is free.
for (let port = start; port < start + maxAttempts; port++) {
try {
execFileSync(process.execPath, [
'-e',
`const s=require("net").createServer();` +
`s.listen(${port},"127.0.0.1",()=>{s.close();process.exit(0)});` +
`s.on("error",()=>process.exit(1))`,
], { timeout: 3000, stdio: 'pipe' });
return port;
} catch {
continue;
}
}
die(`No available ports found in range ${start}-${start + maxAttempts - 1}`);
}
// ---------------------------------------------------------------------------
// PID file management
// ---------------------------------------------------------------------------
/** Read PID from the PID file. Returns the PID number or null. */
function readPid() {
try {
const content = readFileSync(PID_FILE, 'utf8').trim();
const pid = Number(content);
return Number.isFinite(pid) && pid > 0 ? pid : null;
} catch {
return null;
}
}
/** Check whether a process with the given PID is still running. */
function isProcessAlive(pid) {
try {
process.kill(pid, 0); // signal 0 = existence check
return true;
} catch {
return false;
}
}
/** Write the PID file. */
function writePid(pid) {
mkdirSync(CONFIG_HOME, { recursive: true });
writeFileSync(PID_FILE, String(pid), 'utf8');
}
/** Remove the PID file. */
function removePid() {
try {
unlinkSync(PID_FILE);
} catch {
// Ignore -- file may already be gone
}
}
// ---------------------------------------------------------------------------
// Browser opening
// ---------------------------------------------------------------------------
/** Open a URL in the user's default browser (best-effort). */
function openBrowser(url) {
try {
if (IS_WIN) {
// "start" is a cmd built-in; the empty title string avoids
// issues when the URL contains special characters.
execSync(`start "" "${url}"`, { stdio: 'ignore' });
} else if (platform() === 'darwin') {
execFileSync('open', [url], { stdio: 'ignore' });
} else {
// Linux: only attempt if a display server is available and
// we are not in an SSH session.
const hasDisplay = process.env.DISPLAY || process.env.WAYLAND_DISPLAY;
const isSSH = !!process.env.SSH_TTY;
if (hasDisplay && !isSSH) {
execFileSync('xdg-open', [url], { stdio: 'ignore' });
}
}
} catch {
// Non-fatal: user can open the URL manually
}
}
/** Detect headless / CI environments where opening a browser is pointless. */
function isHeadless() {
if (process.env.CI) return true;
if (process.env.CODESPACES) return true;
if (process.env.SSH_TTY) return true;
// Linux without a display server
if (!IS_WIN && platform() !== 'darwin' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
return true;
}
return false;
}
// ---------------------------------------------------------------------------
// Process cleanup
// ---------------------------------------------------------------------------
/** Kill a process tree. On Windows uses taskkill; elsewhere sends SIGTERM. */
function killProcess(pid) {
try {
if (IS_WIN) {
execSync(`taskkill /pid ${pid} /t /f`, { stdio: 'ignore' });
} else {
process.kill(pid, 'SIGTERM');
}
} catch {
// Process may already be gone
}
}
// ---------------------------------------------------------------------------
// CLI commands
// ---------------------------------------------------------------------------
function printVersion() {
console.log(`autoforge v${VERSION}`);
}
function printHelp() {
console.log(`
AutoForge v${VERSION}
Autonomous coding agent with web UI
Usage:
autoforge Start the server (default)
autoforge config Open ~/.autoforge/.env in $EDITOR
autoforge config --path Print config file path
autoforge config --show Show effective configuration
Options:
--port PORT Custom port (default: auto from 8888)
--host HOST Custom host (default: 127.0.0.1)
--no-browser Don't auto-open browser
--repair Delete and recreate virtual environment
--dev Development mode (requires cloned repo)
--version Print version
--help Show this help
`);
}
function handleConfig(args) {
ensureEnvFile();
if (args.includes('--path')) {
console.log(ENV_FILE);
return;
}
if (args.includes('--show')) {
if (!existsSync(ENV_FILE)) {
log('No configuration file found.');
return;
}
const lines = readFileSync(ENV_FILE, 'utf8').split('\n');
const active = lines.filter(l => {
const t = l.trim();
return t && !t.startsWith('#');
});
if (active.length === 0) {
log('No active configuration. All lines are commented out.');
log(`Edit: ${ENV_FILE}`);
} else {
for (const line of active) {
console.log(line);
}
}
return;
}
// Open in editor
const editor = process.env.EDITOR || process.env.VISUAL || (IS_WIN ? 'notepad' : 'vi');
try {
execFileSync(editor, [ENV_FILE], { stdio: 'inherit' });
} catch {
log(`Could not open editor "${editor}".`);
log(`Edit the file manually: ${ENV_FILE}`);
}
}
// ---------------------------------------------------------------------------
// Main server start
// ---------------------------------------------------------------------------
function startServer(opts) {
const { port: requestedPort, host, noBrowser, repair } = opts;
// Step 1: Find Python
const fastPath = !repair && existsSync(venvPython()) && readMarker()?.requirements_hash === requirementsHash();
let python;
if (fastPath) {
// Skip the Python search header on fast path -- we already have a working venv
python = null;
} else {
log(`[1/3] Checking Python...`);
python = findPython();
log(` Found Python ${python.version.raw} at ${python.exe}`);
}
// Step 2: Ensure venv and deps
if (!python) {
// Fast path still needs a python reference for potential repair
python = findPython();
}
const wasAlreadyReady = ensureVenv(python, repair);
// Step 3: Config file
const configCreated = ensureEnvFile();
// Load .env into process.env for the spawned server
const dotenvVars = parseEnvFile(ENV_FILE);
// Determine port
const port = requestedPort || findAvailablePort();
// Check for already-running instance
const existingPid = readPid();
if (existingPid && isProcessAlive(existingPid)) {
log(`AutoForge is already running at http://${host}:${port}`);
log('Opening browser...');
if (!noBrowser && !isHeadless()) {
openBrowser(`http://${host}:${port}`);
}
return;
}
// Clean up stale PID file
if (existingPid) {
removePid();
}
// Show server startup step only on slow path
if (!wasAlreadyReady) {
log('[3/3] Starting server...');
}
if (configCreated) {
log(` Created config file: ~/.autoforge/.env`);
log(' Edit this file to configure API providers (Ollama, Vertex AI, z.ai)');
log('');
}
// Security warning for non-localhost host
if (host !== '127.0.0.1') {
console.log('');
console.log(' !! SECURITY WARNING !!');
console.log(` Remote access enabled on host: ${host}`);
console.log(' The AutoForge UI will be accessible from other machines.');
console.log(' Ensure you understand the security implications.');
console.log('');
}
// Build environment for uvicorn
const serverEnv = { ...process.env, ...dotenvVars, PYTHONPATH: PKG_DIR };
// Enable remote access flag for the FastAPI server
if (host !== '127.0.0.1') {
serverEnv.AUTOFORGE_ALLOW_REMOTE = '1';
}
// Spawn uvicorn
const pyExe = venvPython();
const child = spawn(
pyExe,
[
'-m', 'uvicorn',
'server.main:app',
'--host', host,
'--port', String(port),
],
{
cwd: PKG_DIR,
env: serverEnv,
stdio: 'inherit',
}
);
writePid(child.pid);
// Open browser after a short delay to let the server start
if (!noBrowser && !isHeadless()) {
setTimeout(() => openBrowser(`http://${host}:${port}`), 2000);
}
const url = `http://${host}:${port}`;
console.log('');
log(`Server running at ${url}`);
log('Press Ctrl+C to stop');
// Graceful shutdown handlers
const cleanup = () => {
killProcess(child.pid);
removePid();
};
process.on('SIGINT', () => {
console.log('');
cleanup();
process.exit(0);
});
process.on('SIGTERM', () => {
cleanup();
process.exit(0);
});
// If the child exits on its own, clean up and propagate the exit code
child.on('exit', (code) => {
removePid();
process.exit(code ?? 1);
});
}
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
/**
* Main CLI entry point.
*
* @param {string[]} args - Command-line arguments (process.argv.slice(2))
*/
export function run(args) {
// --version / -v
if (args.includes('--version') || args.includes('-v')) {
printVersion();
return;
}
// --help / -h
if (args.includes('--help') || args.includes('-h')) {
printHelp();
return;
}
// --dev guard: this only works from a cloned repository
if (args.includes('--dev')) {
die(
'Dev mode requires a cloned repository.\n' +
' Clone from https://github.com/paperlinguist/autocoder and run start_ui.sh'
);
return;
}
// "config" subcommand
if (args[0] === 'config') {
handleConfig(args.slice(1));
return;
}
// Parse flags for server start
const host = getFlagValue(args, '--host') || '127.0.0.1';
const portStr = getFlagValue(args, '--port');
const port = portStr ? Number(portStr) : null;
const noBrowser = args.includes('--no-browser');
const repair = args.includes('--repair');
if (port !== null && (!Number.isFinite(port) || port < 1 || port > 65535)) {
die('Invalid port number. Must be between 1 and 65535.');
}
// Print banner
console.log('');
log(`AutoForge v${VERSION}`);
console.log('');
startServer({ port, host, noBrowser, repair });
}
// ---------------------------------------------------------------------------
// Argument parsing helpers
// ---------------------------------------------------------------------------
/**
* Extract the value following a flag from the args array.
* E.g. getFlagValue(['--port', '9000', '--host', '0.0.0.0'], '--port') => '9000'
*/
function getFlagValue(args, flag) {
const idx = args.indexOf(flag);
if (idx === -1 || idx + 1 >= args.length) return null;
return args[idx + 1];
}

53
package.json Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "autoforge-ai",
"version": "0.1.1",
"description": "Autonomous coding agent with web UI - build complete apps with AI",
"license": "AGPL-3.0",
"bin": {
"autoforge": "./bin/autoforge.js"
},
"type": "module",
"engines": {
"node": ">=20"
},
"files": [
"bin/",
"lib/",
"api/",
"server/",
"mcp_server/",
"ui/dist/",
"ui/package.json",
".claude/commands/",
".claude/templates/",
"examples/",
"start.py",
"agent.py",
"auth.py",
"autoforge_paths.py",
"autonomous_agent_demo.py",
"client.py",
"env_constants.py",
"parallel_orchestrator.py",
"progress.py",
"prompts.py",
"registry.py",
"rate_limit_utils.py",
"security.py",
"requirements-prod.txt",
"pyproject.toml",
".env.example",
"!**/__pycache__/",
"!**/*.pyc"
],
"keywords": [
"ai",
"coding-agent",
"claude",
"autonomous",
"code-generation"
],
"scripts": {
"prepublishOnly": "npm --prefix ui install && npm --prefix ui run build"
}
}

View File

@@ -40,11 +40,11 @@ from server.utils.process_utils import kill_process_tree
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Root directory of autocoder (where this script and autonomous_agent_demo.py live) # Root directory of autoforge (where this script and autonomous_agent_demo.py live)
AUTOCODER_ROOT = Path(__file__).parent.resolve() AUTOFORGE_ROOT = Path(__file__).parent.resolve()
# Debug log file path # Debug log file path
DEBUG_LOG_FILE = AUTOCODER_ROOT / "orchestrator_debug.log" DEBUG_LOG_FILE = AUTOFORGE_ROOT / "orchestrator_debug.log"
class DebugLogger: class DebugLogger:
@@ -823,7 +823,7 @@ class ParallelOrchestrator:
cmd = [ cmd = [
sys.executable, sys.executable,
"-u", # Force unbuffered stdout/stderr "-u", # Force unbuffered stdout/stderr
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"), str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
"--project-dir", str(self.project_dir), "--project-dir", str(self.project_dir),
"--max-iterations", "1", "--max-iterations", "1",
"--agent-type", "coding", "--agent-type", "coding",
@@ -845,7 +845,7 @@ class ParallelOrchestrator:
"text": True, "text": True,
"encoding": "utf-8", "encoding": "utf-8",
"errors": "replace", "errors": "replace",
"cwd": str(AUTOCODER_ROOT), # Run from autocoder root for proper imports "cwd": str(self.project_dir), # Run from project dir so CLI creates .claude/ in project
"env": {**os.environ, "PYTHONUNBUFFERED": "1"}, "env": {**os.environ, "PYTHONUNBUFFERED": "1"},
} }
if sys.platform == "win32": if sys.platform == "win32":
@@ -889,7 +889,7 @@ class ParallelOrchestrator:
cmd = [ cmd = [
sys.executable, sys.executable,
"-u", "-u",
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"), str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
"--project-dir", str(self.project_dir), "--project-dir", str(self.project_dir),
"--max-iterations", "1", "--max-iterations", "1",
"--agent-type", "coding", "--agent-type", "coding",
@@ -908,7 +908,7 @@ class ParallelOrchestrator:
"text": True, "text": True,
"encoding": "utf-8", "encoding": "utf-8",
"errors": "replace", "errors": "replace",
"cwd": str(AUTOCODER_ROOT), "cwd": str(self.project_dir), # Run from project dir so CLI creates .claude/ in project
"env": {**os.environ, "PYTHONUNBUFFERED": "1"}, "env": {**os.environ, "PYTHONUNBUFFERED": "1"},
} }
if sys.platform == "win32": if sys.platform == "win32":
@@ -992,7 +992,7 @@ class ParallelOrchestrator:
cmd = [ cmd = [
sys.executable, sys.executable,
"-u", "-u",
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"), str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
"--project-dir", str(self.project_dir), "--project-dir", str(self.project_dir),
"--max-iterations", "1", "--max-iterations", "1",
"--agent-type", "testing", "--agent-type", "testing",
@@ -1012,7 +1012,7 @@ class ParallelOrchestrator:
"text": True, "text": True,
"encoding": "utf-8", "encoding": "utf-8",
"errors": "replace", "errors": "replace",
"cwd": str(AUTOCODER_ROOT), "cwd": str(self.project_dir), # Run from project dir so CLI creates .claude/ in project
"env": {**os.environ, "PYTHONUNBUFFERED": "1"}, "env": {**os.environ, "PYTHONUNBUFFERED": "1"},
} }
if sys.platform == "win32": if sys.platform == "win32":
@@ -1053,7 +1053,7 @@ class ParallelOrchestrator:
cmd = [ cmd = [
sys.executable, "-u", sys.executable, "-u",
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"), str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
"--project-dir", str(self.project_dir), "--project-dir", str(self.project_dir),
"--agent-type", "initializer", "--agent-type", "initializer",
"--max-iterations", "1", "--max-iterations", "1",
@@ -1073,7 +1073,7 @@ class ParallelOrchestrator:
"text": True, "text": True,
"encoding": "utf-8", "encoding": "utf-8",
"errors": "replace", "errors": "replace",
"cwd": str(AUTOCODER_ROOT), "cwd": str(AUTOFORGE_ROOT),
"env": {**os.environ, "PYTHONUNBUFFERED": "1"}, "env": {**os.environ, "PYTHONUNBUFFERED": "1"},
} }
if sys.platform == "win32": if sys.platform == "win32":

View File

@@ -46,7 +46,7 @@ def has_features(project_dir: Path) -> bool:
return True return True
# Check SQLite database # Check SQLite database
from autocoder_paths import get_features_db_path from autoforge_paths import get_features_db_path
db_file = get_features_db_path(project_dir) db_file = get_features_db_path(project_dir)
if not db_file.exists(): if not db_file.exists():
return False return False
@@ -72,7 +72,7 @@ def count_passing_tests(project_dir: Path) -> tuple[int, int, int]:
Returns: Returns:
(passing_count, in_progress_count, total_count) (passing_count, in_progress_count, total_count)
""" """
from autocoder_paths import get_features_db_path from autoforge_paths import get_features_db_path
db_file = get_features_db_path(project_dir) db_file = get_features_db_path(project_dir)
if not db_file.exists(): if not db_file.exists():
return 0, 0, 0 return 0, 0, 0
@@ -122,7 +122,7 @@ def get_all_passing_features(project_dir: Path) -> list[dict]:
Returns: Returns:
List of dicts with id, category, name for each passing feature List of dicts with id, category, name for each passing feature
""" """
from autocoder_paths import get_features_db_path from autoforge_paths import get_features_db_path
db_file = get_features_db_path(project_dir) db_file = get_features_db_path(project_dir)
if not db_file.exists(): if not db_file.exists():
return [] return []
@@ -147,7 +147,7 @@ def send_progress_webhook(passing: int, total: int, project_dir: Path) -> None:
if not WEBHOOK_URL: if not WEBHOOK_URL:
return # Webhook not configured return # Webhook not configured
from autocoder_paths import get_progress_cache_path from autoforge_paths import get_progress_cache_path
cache_file = get_progress_cache_path(project_dir) cache_file = get_progress_cache_path(project_dir)
previous = 0 previous = 0
previous_passing_ids = set() previous_passing_ids = set()

View File

@@ -19,7 +19,7 @@ TEMPLATES_DIR = Path(__file__).parent / ".claude" / "templates"
def get_project_prompts_dir(project_dir: Path) -> Path: def get_project_prompts_dir(project_dir: Path) -> Path:
"""Get the prompts directory for a specific project.""" """Get the prompts directory for a specific project."""
from autocoder_paths import get_prompts_dir from autoforge_paths import get_prompts_dir
return get_prompts_dir(project_dir) return get_prompts_dir(project_dir)
@@ -315,9 +315,9 @@ def scaffold_project_prompts(project_dir: Path) -> Path:
project_prompts = get_project_prompts_dir(project_dir) project_prompts = get_project_prompts_dir(project_dir)
project_prompts.mkdir(parents=True, exist_ok=True) project_prompts.mkdir(parents=True, exist_ok=True)
# Create .autocoder directory with .gitignore for runtime files # Create .autoforge directory with .gitignore for runtime files
from autocoder_paths import ensure_autocoder_dir from autoforge_paths import ensure_autoforge_dir
autocoder_dir = ensure_autocoder_dir(project_dir) autoforge_dir = ensure_autoforge_dir(project_dir)
# Define template mappings: (source_template, destination_name) # Define template mappings: (source_template, destination_name)
templates = [ templates = [
@@ -340,14 +340,14 @@ def scaffold_project_prompts(project_dir: Path) -> Path:
except (OSError, PermissionError) as e: except (OSError, PermissionError) as e:
print(f" Warning: Could not copy {dest_name}: {e}") print(f" Warning: Could not copy {dest_name}: {e}")
# Copy allowed_commands.yaml template to .autocoder/ # Copy allowed_commands.yaml template to .autoforge/
examples_dir = Path(__file__).parent / "examples" examples_dir = Path(__file__).parent / "examples"
allowed_commands_template = examples_dir / "project_allowed_commands.yaml" allowed_commands_template = examples_dir / "project_allowed_commands.yaml"
allowed_commands_dest = autocoder_dir / "allowed_commands.yaml" allowed_commands_dest = autoforge_dir / "allowed_commands.yaml"
if allowed_commands_template.exists() and not allowed_commands_dest.exists(): if allowed_commands_template.exists() and not allowed_commands_dest.exists():
try: try:
shutil.copy(allowed_commands_template, allowed_commands_dest) shutil.copy(allowed_commands_template, allowed_commands_dest)
copied_files.append(".autocoder/allowed_commands.yaml") copied_files.append(".autoforge/allowed_commands.yaml")
except (OSError, PermissionError) as e: except (OSError, PermissionError) as e:
print(f" Warning: Could not copy allowed_commands.yaml: {e}") print(f" Warning: Could not copy allowed_commands.yaml: {e}")

View File

@@ -3,7 +3,7 @@ Project Registry Module
======================= =======================
Cross-platform project registry for storing project name to path mappings. Cross-platform project registry for storing project name to path mappings.
Uses SQLite database stored at ~/.autocoder/registry.db. Uses SQLite database stored at ~/.autoforge/registry.db.
""" """
import logging import logging
@@ -23,6 +23,22 @@ from sqlalchemy.orm import DeclarativeBase, sessionmaker
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _migrate_registry_dir() -> None:
"""Migrate ~/.autocoder/ to ~/.autoforge/ if needed.
Provides backward compatibility by automatically renaming the old
config directory to the new location on first access.
"""
old_dir = Path.home() / ".autocoder"
new_dir = Path.home() / ".autoforge"
if old_dir.exists() and not new_dir.exists():
try:
old_dir.rename(new_dir)
logger.info("Migrated registry directory: ~/.autocoder/ -> ~/.autoforge/")
except Exception:
logger.warning("Failed to migrate ~/.autocoder/ to ~/.autoforge/", exc_info=True)
# ============================================================================= # =============================================================================
# Model Configuration (Single Source of Truth) # Model Configuration (Single Source of Truth)
# ============================================================================= # =============================================================================
@@ -120,12 +136,15 @@ _engine_lock = threading.Lock()
def get_config_dir() -> Path: def get_config_dir() -> Path:
""" """
Get the config directory: ~/.autocoder/ Get the config directory: ~/.autoforge/
Automatically migrates from ~/.autocoder/ if needed.
Returns: Returns:
Path to ~/.autocoder/ (created if it doesn't exist) Path to ~/.autoforge/ (created if it doesn't exist)
""" """
config_dir = Path.home() / ".autocoder" _migrate_registry_dir()
config_dir = Path.home() / ".autoforge"
config_dir.mkdir(parents=True, exist_ok=True) config_dir.mkdir(parents=True, exist_ok=True)
return config_dir return config_dir

14
requirements-prod.txt Normal file
View File

@@ -0,0 +1,14 @@
# Production runtime dependencies only
# For development, use requirements.txt (includes ruff, mypy, pytest)
claude-agent-sdk>=0.1.0,<0.2.0
python-dotenv>=1.0.0
sqlalchemy>=2.0.0
fastapi>=0.115.0
uvicorn[standard]>=0.32.0
websockets>=13.0
python-multipart>=0.0.17
psutil>=6.0.0
aiofiles>=24.0.0
apscheduler>=3.10.0,<4.0.0
pywinpty>=2.0.0; sys_platform == "win32"
pyyaml>=6.0.0

View File

@@ -553,14 +553,23 @@ def get_org_config_path() -> Path:
Get the organization-level config file path. Get the organization-level config file path.
Returns: Returns:
Path to ~/.autocoder/config.yaml Path to ~/.autoforge/config.yaml (falls back to ~/.autocoder/config.yaml)
""" """
return Path.home() / ".autocoder" / "config.yaml" new_path = Path.home() / ".autoforge" / "config.yaml"
if new_path.exists():
return new_path
# Backward compatibility: check old location
old_path = Path.home() / ".autocoder" / "config.yaml"
if old_path.exists():
return old_path
return new_path
def load_org_config() -> Optional[dict]: def load_org_config() -> Optional[dict]:
""" """
Load organization-level config from ~/.autocoder/config.yaml. Load organization-level config from ~/.autoforge/config.yaml.
Falls back to ~/.autocoder/config.yaml for backward compatibility.
Returns: Returns:
Dict with parsed org config, or None if file doesn't exist or is invalid Dict with parsed org config, or None if file doesn't exist or is invalid
@@ -630,7 +639,10 @@ def load_project_commands(project_dir: Path) -> Optional[dict]:
Returns: Returns:
Dict with parsed YAML config, or None if file doesn't exist or is invalid Dict with parsed YAML config, or None if file doesn't exist or is invalid
""" """
config_path = project_dir.resolve() / ".autocoder" / "allowed_commands.yaml" # Check new location first, fall back to old for backward compatibility
config_path = project_dir.resolve() / ".autoforge" / "allowed_commands.yaml"
if not config_path.exists():
config_path = project_dir.resolve() / ".autocoder" / "allowed_commands.yaml"
if not config_path.exists(): if not config_path.exists():
return None return None
@@ -909,7 +921,7 @@ async def bash_security_hook(input_data, tool_use_id=None, context=None):
# Provide helpful error message with config hint # Provide helpful error message with config hint
error_msg = f"Command '{cmd}' is not allowed.\n" error_msg = f"Command '{cmd}' is not allowed.\n"
error_msg += "To allow this command:\n" error_msg += "To allow this command:\n"
error_msg += " 1. Add to .autocoder/allowed_commands.yaml for this project, OR\n" error_msg += " 1. Add to .autoforge/allowed_commands.yaml for this project, OR\n"
error_msg += " 2. Request mid-session approval (the agent can ask)\n" error_msg += " 2. Request mid-session approval (the agent can ask)\n"
error_msg += "Note: Some commands are blocked at org-level and cannot be overridden." error_msg += "Note: Some commands are blocked at org-level and cannot be overridden."
return { return {

View File

@@ -94,7 +94,7 @@ logger = logging.getLogger(__name__)
# Check if remote access is enabled via environment variable # Check if remote access is enabled via environment variable
# Set by start_ui.py when --host is not 127.0.0.1 # Set by start_ui.py when --host is not 127.0.0.1
ALLOW_REMOTE = os.environ.get("AUTOCODER_ALLOW_REMOTE", "").lower() in ("1", "true", "yes") ALLOW_REMOTE = os.environ.get("AUTOFORGE_ALLOW_REMOTE", "").lower() in ("1", "true", "yes")
if ALLOW_REMOTE: if ALLOW_REMOTE:
logger.warning( logger.warning(
@@ -133,7 +133,7 @@ else:
if not ALLOW_REMOTE: if not ALLOW_REMOTE:
@app.middleware("http") @app.middleware("http")
async def require_localhost(request: Request, call_next): async def require_localhost(request: Request, call_next):
"""Only allow requests from localhost (disabled when AUTOCODER_ALLOW_REMOTE=1).""" """Only allow requests from localhost (disabled when AUTOFORGE_ALLOW_REMOTE=1)."""
client_host = request.client.host if request.client else None client_host = request.client.host if request.client else None
# Allow localhost connections # Allow localhost connections

View File

@@ -121,7 +121,7 @@ async def expand_project_websocket(websocket: WebSocket, project_name: str):
return return
# Verify project has app_spec.txt # Verify project has app_spec.txt
from autocoder_paths import get_prompts_dir from autoforge_paths import get_prompts_dir
spec_path = get_prompts_dir(project_dir) / "app_spec.txt" spec_path = get_prompts_dir(project_dir) / "app_spec.txt"
if not spec_path.exists(): if not spec_path.exists():
await websocket.close(code=4004, reason="Project has no spec. Create spec first.") await websocket.close(code=4004, reason="Project has no spec. Create spec first.")

View File

@@ -126,7 +126,7 @@ async def list_features(project_name: str):
if not project_dir.exists(): if not project_dir.exists():
raise HTTPException(status_code=404, detail="Project directory not found") raise HTTPException(status_code=404, detail="Project directory not found")
from autocoder_paths import get_features_db_path from autoforge_paths import get_features_db_path
db_file = get_features_db_path(project_dir) db_file = get_features_db_path(project_dir)
if not db_file.exists(): if not db_file.exists():
return FeatureListResponse(pending=[], in_progress=[], done=[]) return FeatureListResponse(pending=[], in_progress=[], done=[])
@@ -322,7 +322,7 @@ async def get_dependency_graph(project_name: str):
if not project_dir.exists(): if not project_dir.exists():
raise HTTPException(status_code=404, detail="Project directory not found") raise HTTPException(status_code=404, detail="Project directory not found")
from autocoder_paths import get_features_db_path from autoforge_paths import get_features_db_path
db_file = get_features_db_path(project_dir) db_file = get_features_db_path(project_dir)
if not db_file.exists(): if not db_file.exists():
return DependencyGraphResponse(nodes=[], edges=[]) return DependencyGraphResponse(nodes=[], edges=[])
@@ -388,7 +388,7 @@ async def get_feature(project_name: str, feature_id: int):
if not project_dir.exists(): if not project_dir.exists():
raise HTTPException(status_code=404, detail="Project directory not found") raise HTTPException(status_code=404, detail="Project directory not found")
from autocoder_paths import get_features_db_path from autoforge_paths import get_features_db_path
db_file = get_features_db_path(project_dir) db_file = get_features_db_path(project_dir)
if not db_file.exists(): if not db_file.exists():
raise HTTPException(status_code=404, detail="No features database found") raise HTTPException(status_code=404, detail="No features database found")

View File

@@ -276,7 +276,7 @@ async def delete_project(name: str, delete_files: bool = False):
raise HTTPException(status_code=404, detail=f"Project '{name}' not found") raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
# Check if agent is running # Check if agent is running
from autocoder_paths import has_agent_running from autoforge_paths import has_agent_running
if has_agent_running(project_dir): if has_agent_running(project_dir):
raise HTTPException( raise HTTPException(
status_code=409, status_code=409,
@@ -407,7 +407,7 @@ async def reset_project(name: str, full_reset: bool = False):
raise HTTPException(status_code=404, detail="Project directory not found") raise HTTPException(status_code=404, detail="Project directory not found")
# Check if agent is running # Check if agent is running
from autocoder_paths import has_agent_running from autoforge_paths import has_agent_running
if has_agent_running(project_dir): if has_agent_running(project_dir):
raise HTTPException( raise HTTPException(
status_code=409, status_code=409,
@@ -424,7 +424,7 @@ async def reset_project(name: str, full_reset: bool = False):
deleted_files: list[str] = [] deleted_files: list[str] = []
from autocoder_paths import ( from autoforge_paths import (
get_assistant_db_path, get_assistant_db_path,
get_claude_assistant_settings_path, get_claude_assistant_settings_path,
get_claude_settings_path, get_claude_settings_path,
@@ -466,7 +466,7 @@ async def reset_project(name: str, full_reset: bool = False):
# Full reset: also delete prompts directory # Full reset: also delete prompts directory
if full_reset: if full_reset:
from autocoder_paths import get_prompts_dir from autoforge_paths import get_prompts_dir
# Delete prompts from both possible locations # Delete prompts from both possible locations
for prompts_dir in [get_prompts_dir(project_dir), project_dir / "prompts"]: for prompts_dir in [get_prompts_dir(project_dir), project_dir / "prompts"]:
if prompts_dir.exists(): if prompts_dir.exists():

View File

@@ -105,7 +105,7 @@ async def get_spec_file_status(project_name: str):
if not project_dir.exists(): if not project_dir.exists():
raise HTTPException(status_code=404, detail="Project directory not found") raise HTTPException(status_code=404, detail="Project directory not found")
from autocoder_paths import get_prompts_dir from autoforge_paths import get_prompts_dir
status_file = get_prompts_dir(project_dir) / ".spec_status.json" status_file = get_prompts_dir(project_dir) / ".spec_status.json"
if not status_file.exists(): if not status_file.exists():

View File

@@ -64,7 +64,7 @@ def get_system_prompt(project_name: str, project_dir: Path) -> str:
"""Generate the system prompt for the assistant with project context.""" """Generate the system prompt for the assistant with project context."""
# Try to load app_spec.txt for context # Try to load app_spec.txt for context
app_spec_content = "" app_spec_content = ""
from autocoder_paths import get_prompts_dir from autoforge_paths import get_prompts_dir
app_spec_path = get_prompts_dir(project_dir) / "app_spec.txt" app_spec_path = get_prompts_dir(project_dir) / "app_spec.txt"
if app_spec_path.exists(): if app_spec_path.exists():
try: try:
@@ -224,7 +224,7 @@ class AssistantChatSession:
"allow": permissions_list, "allow": permissions_list,
}, },
} }
from autocoder_paths import get_claude_assistant_settings_path from autoforge_paths import get_claude_assistant_settings_path
settings_file = get_claude_assistant_settings_path(self.project_dir) settings_file = get_claude_assistant_settings_path(self.project_dir)
settings_file.parent.mkdir(parents=True, exist_ok=True) settings_file.parent.mkdir(parents=True, exist_ok=True)
with open(settings_file, "w") as f: with open(settings_file, "w") as f:

View File

@@ -64,7 +64,7 @@ class ConversationMessage(Base):
def get_db_path(project_dir: Path) -> Path: def get_db_path(project_dir: Path) -> Path:
"""Get the path to the assistant database for a project.""" """Get the path to the assistant database for a project."""
from autocoder_paths import get_assistant_db_path from autoforge_paths import get_assistant_db_path
return get_assistant_db_path(project_dir) return get_assistant_db_path(project_dir)

View File

@@ -14,7 +14,7 @@ from pathlib import Path
from typing import AsyncGenerator from typing import AsyncGenerator
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# Root directory of the autocoder project (repository root). # Root directory of the autoforge project (repository root).
# Used throughout the server package whenever the repo root is needed. # Used throughout the server package whenever the repo root is needed.
# ------------------------------------------------------------------- # -------------------------------------------------------------------
ROOT_DIR = Path(__file__).parent.parent.parent ROOT_DIR = Path(__file__).parent.parent.parent

View File

@@ -115,7 +115,7 @@ class DevServerProcessManager:
self._callbacks_lock = threading.Lock() self._callbacks_lock = threading.Lock()
# Lock file to prevent multiple instances (stored in project directory) # Lock file to prevent multiple instances (stored in project directory)
from autocoder_paths import get_devserver_lock_path from autoforge_paths import get_devserver_lock_path
self.lock_file = get_devserver_lock_path(self.project_dir) self.lock_file = get_devserver_lock_path(self.project_dir)
@property @property
@@ -504,10 +504,10 @@ def cleanup_orphaned_devserver_locks() -> int:
continue continue
# Check both legacy and new locations for lock files # Check both legacy and new locations for lock files
from autocoder_paths import get_autocoder_dir from autoforge_paths import get_autoforge_dir
lock_locations = [ lock_locations = [
project_path / ".devserver.lock", project_path / ".devserver.lock",
get_autocoder_dir(project_path) / ".devserver.lock", get_autoforge_dir(project_path) / ".devserver.lock",
] ]
lock_file = None lock_file = None
for candidate in lock_locations: for candidate in lock_locations:

View File

@@ -103,7 +103,7 @@ class ExpandChatSession:
return return
# Verify project has existing spec # Verify project has existing spec
from autocoder_paths import get_prompts_dir from autoforge_paths import get_prompts_dir
spec_path = get_prompts_dir(self.project_dir) / "app_spec.txt" spec_path = get_prompts_dir(self.project_dir) / "app_spec.txt"
if not spec_path.exists(): if not spec_path.exists():
yield { yield {
@@ -142,7 +142,7 @@ class ExpandChatSession:
], ],
}, },
} }
from autocoder_paths import get_expand_settings_path from autoforge_paths import get_expand_settings_path
settings_file = get_expand_settings_path(self.project_dir, uuid.uuid4().hex) settings_file = get_expand_settings_path(self.project_dir, uuid.uuid4().hex)
settings_file.parent.mkdir(parents=True, exist_ok=True) settings_file.parent.mkdir(parents=True, exist_ok=True)
self._settings_file = settings_file self._settings_file = settings_file

View File

@@ -92,7 +92,7 @@ class AgentProcessManager:
self._callbacks_lock = threading.Lock() self._callbacks_lock = threading.Lock()
# Lock file to prevent multiple instances (stored in project directory) # Lock file to prevent multiple instances (stored in project directory)
from autocoder_paths import get_agent_lock_path from autoforge_paths import get_agent_lock_path
self.lock_file = get_agent_lock_path(self.project_dir) self.lock_file = get_agent_lock_path(self.project_dir)
@property @property
@@ -587,10 +587,10 @@ def cleanup_orphaned_locks() -> int:
continue continue
# Check both legacy and new locations for lock files # Check both legacy and new locations for lock files
from autocoder_paths import get_autocoder_dir from autoforge_paths import get_autoforge_dir
lock_locations = [ lock_locations = [
project_path / ".agent.lock", project_path / ".agent.lock",
get_autocoder_dir(project_path) / ".agent.lock", get_autoforge_dir(project_path) / ".agent.lock",
] ]
lock_file = None lock_file = None
for candidate in lock_locations: for candidate in lock_locations:

View File

@@ -6,7 +6,7 @@ Handles project type detection and dev command configuration.
Detects project types by scanning for configuration files and provides Detects project types by scanning for configuration files and provides
default or custom dev commands for each project. default or custom dev commands for each project.
Configuration is stored in {project_dir}/.autocoder/config.json. Configuration is stored in {project_dir}/.autoforge/config.json.
""" """
import json import json
@@ -88,13 +88,22 @@ def _get_config_path(project_dir: Path) -> Path:
""" """
Get the path to the project config file. Get the path to the project config file.
Checks the new .autoforge/ location first, falls back to .autocoder/
for backward compatibility.
Args: Args:
project_dir: Path to the project directory. project_dir: Path to the project directory.
Returns: Returns:
Path to the .autocoder/config.json file. Path to the config.json file in the appropriate directory.
""" """
return project_dir / ".autocoder" / "config.json" new_path = project_dir / ".autoforge" / "config.json"
if new_path.exists():
return new_path
old_path = project_dir / ".autocoder" / "config.json"
if old_path.exists():
return old_path
return new_path
def _load_config(project_dir: Path) -> dict: def _load_config(project_dir: Path) -> dict:
@@ -137,7 +146,7 @@ def _save_config(project_dir: Path, config: dict) -> None:
""" """
Save the project configuration to disk. Save the project configuration to disk.
Creates the .autocoder directory if it doesn't exist. Creates the .autoforge directory if it doesn't exist.
Args: Args:
project_dir: Path to the project directory. project_dir: Path to the project directory.
@@ -148,7 +157,7 @@ def _save_config(project_dir: Path, config: dict) -> None:
""" """
config_path = _get_config_path(project_dir) config_path = _get_config_path(project_dir)
# Ensure the .autocoder directory exists # Ensure the .autoforge directory exists
config_path.parent.mkdir(parents=True, exist_ok=True) config_path.parent.mkdir(parents=True, exist_ok=True)
try: try:
@@ -408,11 +417,11 @@ def clear_dev_command(project_dir: Path) -> None:
config_path.unlink(missing_ok=True) config_path.unlink(missing_ok=True)
logger.info("Removed empty config file for %s", project_dir.name) logger.info("Removed empty config file for %s", project_dir.name)
# Also remove .autocoder directory if empty # Also remove .autoforge directory if empty
autocoder_dir = config_path.parent autoforge_dir = config_path.parent
if autocoder_dir.exists() and not any(autocoder_dir.iterdir()): if autoforge_dir.exists() and not any(autoforge_dir.iterdir()):
autocoder_dir.rmdir() autoforge_dir.rmdir()
logger.debug("Removed empty .autocoder directory for %s", project_dir.name) logger.debug("Removed empty .autoforge directory for %s", project_dir.name)
except OSError as e: except OSError as e:
logger.warning("Failed to clean up config for %s: %s", project_dir.name, e) logger.warning("Failed to clean up config for %s: %s", project_dir.name, e)
else: else:

View File

@@ -92,7 +92,7 @@ class SchedulerService:
async def _load_project_schedules(self, project_name: str, project_dir: Path) -> int: async def _load_project_schedules(self, project_name: str, project_dir: Path) -> int:
"""Load schedules for a single project. Returns count of schedules loaded.""" """Load schedules for a single project. Returns count of schedules loaded."""
from api.database import Schedule, create_database from api.database import Schedule, create_database
from autocoder_paths import get_features_db_path from autoforge_paths import get_features_db_path
db_path = get_features_db_path(project_dir) db_path = get_features_db_path(project_dir)
if not db_path.exists(): if not db_path.exists():
@@ -568,7 +568,7 @@ class SchedulerService:
): ):
"""Check if a project should be started on server startup.""" """Check if a project should be started on server startup."""
from api.database import Schedule, ScheduleOverride, create_database from api.database import Schedule, ScheduleOverride, create_database
from autocoder_paths import get_features_db_path from autoforge_paths import get_features_db_path
db_path = get_features_db_path(project_dir) db_path = get_features_db_path(project_dir)
if not db_path.exists(): if not db_path.exists():

View File

@@ -95,7 +95,7 @@ class SpecChatSession:
# Delete app_spec.txt so Claude can create it fresh # Delete app_spec.txt so Claude can create it fresh
# The SDK requires reading existing files before writing, but app_spec.txt is created new # The SDK requires reading existing files before writing, but app_spec.txt is created new
# Note: We keep initializer_prompt.md so Claude can read and update the template # Note: We keep initializer_prompt.md so Claude can read and update the template
from autocoder_paths import get_prompts_dir from autoforge_paths import get_prompts_dir
prompts_dir = get_prompts_dir(self.project_dir) prompts_dir = get_prompts_dir(self.project_dir)
app_spec_path = prompts_dir / "app_spec.txt" app_spec_path = prompts_dir / "app_spec.txt"
if app_spec_path.exists(): if app_spec_path.exists():
@@ -116,7 +116,7 @@ class SpecChatSession:
], ],
}, },
} }
from autocoder_paths import get_claude_settings_path from autoforge_paths import get_claude_settings_path
settings_file = get_claude_settings_path(self.project_dir) settings_file = get_claude_settings_path(self.project_dir)
settings_file.parent.mkdir(parents=True, exist_ok=True) settings_file.parent.mkdir(parents=True, exist_ok=True)
with open(settings_file, "w") as f: with open(settings_file, "w") as f:

View File

@@ -3,7 +3,7 @@ cd /d "%~dp0"
echo. echo.
echo ======================================== echo ========================================
echo Autonomous Coding Agent echo AutoForge - Autonomous Coding Agent
echo ======================================== echo ========================================
echo. echo.

View File

@@ -82,7 +82,7 @@ def get_existing_projects() -> list[tuple[str, Path]]:
def display_menu(projects: list[tuple[str, Path]]) -> None: def display_menu(projects: list[tuple[str, Path]]) -> None:
"""Display the main menu.""" """Display the main menu."""
print("\n" + "=" * 50) print("\n" + "=" * 50)
print(" Autonomous Coding Agent Launcher") print(" AutoForge - Autonomous Coding Agent")
print("=" * 50) print("=" * 50)
print("\n[1] Create new project") print("\n[1] Create new project")

View File

@@ -3,7 +3,7 @@ cd "$(dirname "$0")"
echo "" echo ""
echo "========================================" echo "========================================"
echo " Autonomous Coding Agent" echo " AutoForge - Autonomous Coding Agent"
echo "========================================" echo "========================================"
echo "" echo ""

View File

@@ -1,11 +1,11 @@
@echo off @echo off
cd /d "%~dp0" cd /d "%~dp0"
REM AutoCoder UI Launcher for Windows REM AutoForge UI Launcher for Windows
REM This script launches the web UI for the autonomous coding agent. REM This script launches the web UI for the autonomous coding agent.
echo. echo.
echo ==================================== echo ====================================
echo AutoCoder UI echo AutoForge UI
echo ==================================== echo ====================================
echo. echo.

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
AutoCoder UI Launcher AutoForge UI Launcher
===================== =====================
Automated launcher that handles all setup: Automated launcher that handles all setup:
@@ -265,7 +265,7 @@ def start_dev_server(port: int, host: str = "127.0.0.1") -> tuple:
# Set environment for remote access if needed # Set environment for remote access if needed
env = os.environ.copy() env = os.environ.copy()
if host != "127.0.0.1": if host != "127.0.0.1":
env["AUTOCODER_ALLOW_REMOTE"] = "1" env["AUTOFORGE_ALLOW_REMOTE"] = "1"
# Start FastAPI # Start FastAPI
backend = subprocess.Popen([ backend = subprocess.Popen([
@@ -297,7 +297,7 @@ def start_production_server(port: int, host: str = "127.0.0.1"):
# Enable remote access in server if not localhost # Enable remote access in server if not localhost
if host != "127.0.0.1": if host != "127.0.0.1":
env["AUTOCODER_ALLOW_REMOTE"] = "1" env["AUTOFORGE_ALLOW_REMOTE"] = "1"
# NOTE: --reload is NOT used because on Windows it breaks asyncio subprocess # NOTE: --reload is NOT used because on Windows it breaks asyncio subprocess
# support (uvicorn's reload worker doesn't inherit the ProactorEventLoop policy). # support (uvicorn's reload worker doesn't inherit the ProactorEventLoop policy).
@@ -313,7 +313,7 @@ def start_production_server(port: int, host: str = "127.0.0.1"):
def main() -> None: def main() -> None:
"""Main entry point.""" """Main entry point."""
parser = argparse.ArgumentParser(description="AutoCoder UI Launcher") parser = argparse.ArgumentParser(description="AutoForge UI Launcher")
parser.add_argument("--dev", action="store_true", help="Run in development mode with Vite hot reload") parser.add_argument("--dev", action="store_true", help="Run in development mode with Vite hot reload")
parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)") parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
parser.add_argument("--port", type=int, default=None, help="Port to bind to (default: auto-detect from 8888)") parser.add_argument("--port", type=int, default=None, help="Port to bind to (default: auto-detect from 8888)")
@@ -328,7 +328,7 @@ def main() -> None:
print(" SECURITY WARNING") print(" SECURITY WARNING")
print("!" * 50) print("!" * 50)
print(f" Remote access enabled on host: {host}") print(f" Remote access enabled on host: {host}")
print(" The AutoCoder UI will be accessible from other machines.") print(" The AutoForge UI will be accessible from other machines.")
print(" Ensure you understand the security implications:") print(" Ensure you understand the security implications:")
print(" - The agent has file system access to project directories") print(" - The agent has file system access to project directories")
print(" - The API can start/stop agents and modify files") print(" - The API can start/stop agents and modify files")
@@ -336,7 +336,7 @@ def main() -> None:
print("!" * 50 + "\n") print("!" * 50 + "\n")
print("=" * 50) print("=" * 50)
print(" AutoCoder UI Setup") print(" AutoForge UI Setup")
print("=" * 50) print("=" * 50)
total_steps = 6 if not dev_mode else 5 total_steps = 6 if not dev_mode else 5

View File

@@ -1,11 +1,11 @@
#!/bin/bash #!/bin/bash
cd "$(dirname "$0")" cd "$(dirname "$0")"
# AutoCoder UI Launcher for Unix/Linux/macOS # AutoForge UI Launcher for Unix/Linux/macOS
# This script launches the web UI for the autonomous coding agent. # This script launches the web UI for the autonomous coding agent.
echo "" echo ""
echo "====================================" echo "===================================="
echo " AutoCoder UI" echo " AutoForge UI"
echo "====================================" echo "===================================="
echo "" echo ""

View File

@@ -273,11 +273,11 @@ def test_yaml_loading():
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir) project_dir = Path(tmpdir)
autocoder_dir = project_dir / ".autocoder" autoforge_dir = project_dir / ".autoforge"
autocoder_dir.mkdir() autoforge_dir.mkdir()
# Test 1: Valid YAML # Test 1: Valid YAML
config_path = autocoder_dir / "allowed_commands.yaml" config_path = autoforge_dir / "allowed_commands.yaml"
config_path.write_text("""version: 1 config_path.write_text("""version: 1
commands: commands:
- name: swift - name: swift
@@ -297,7 +297,7 @@ commands:
failed += 1 failed += 1
# Test 2: Missing file returns None # Test 2: Missing file returns None
(project_dir / ".autocoder" / "allowed_commands.yaml").unlink() (project_dir / ".autoforge" / "allowed_commands.yaml").unlink()
config = load_project_commands(project_dir) config = load_project_commands(project_dir)
if config is None: if config is None:
print(" PASS: Missing file returns None") print(" PASS: Missing file returns None")
@@ -407,11 +407,11 @@ def test_project_commands():
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
project_dir = Path(tmpdir) project_dir = Path(tmpdir)
autocoder_dir = project_dir / ".autocoder" autoforge_dir = project_dir / ".autoforge"
autocoder_dir.mkdir() autoforge_dir.mkdir()
# Create a config with Swift commands # Create a config with Swift commands
config_path = autocoder_dir / "allowed_commands.yaml" config_path = autoforge_dir / "allowed_commands.yaml"
config_path.write_text("""version: 1 config_path.write_text("""version: 1
commands: commands:
- name: swift - name: swift
@@ -482,7 +482,7 @@ def test_org_config_loading():
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
# Use temporary_home for cross-platform compatibility # Use temporary_home for cross-platform compatibility
with temporary_home(tmpdir): with temporary_home(tmpdir):
org_dir = Path(tmpdir) / ".autocoder" org_dir = Path(tmpdir) / ".autoforge"
org_dir.mkdir() org_dir.mkdir()
org_config_path = org_dir / "config.yaml" org_config_path = org_dir / "config.yaml"
@@ -576,7 +576,7 @@ def test_hierarchy_resolution():
with tempfile.TemporaryDirectory() as tmpproject: with tempfile.TemporaryDirectory() as tmpproject:
# Use temporary_home for cross-platform compatibility # Use temporary_home for cross-platform compatibility
with temporary_home(tmphome): with temporary_home(tmphome):
org_dir = Path(tmphome) / ".autocoder" org_dir = Path(tmphome) / ".autoforge"
org_dir.mkdir() org_dir.mkdir()
org_config_path = org_dir / "config.yaml" org_config_path = org_dir / "config.yaml"
@@ -593,9 +593,9 @@ blocked_commands:
""") """)
project_dir = Path(tmpproject) project_dir = Path(tmpproject)
project_autocoder = project_dir / ".autocoder" project_autoforge = project_dir / ".autoforge"
project_autocoder.mkdir() project_autoforge.mkdir()
project_config = project_autocoder / "allowed_commands.yaml" project_config = project_autoforge / "allowed_commands.yaml"
# Create project config # Create project config
project_config.write_text("""version: 1 project_config.write_text("""version: 1
@@ -660,7 +660,7 @@ def test_org_blocklist_enforcement():
with tempfile.TemporaryDirectory() as tmpproject: with tempfile.TemporaryDirectory() as tmpproject:
# Use temporary_home for cross-platform compatibility # Use temporary_home for cross-platform compatibility
with temporary_home(tmphome): with temporary_home(tmphome):
org_dir = Path(tmphome) / ".autocoder" org_dir = Path(tmphome) / ".autoforge"
org_dir.mkdir() org_dir.mkdir()
org_config_path = org_dir / "config.yaml" org_config_path = org_dir / "config.yaml"
@@ -671,8 +671,8 @@ blocked_commands:
""") """)
project_dir = Path(tmpproject) project_dir = Path(tmpproject)
project_autocoder = project_dir / ".autocoder" project_autoforge = project_dir / ".autoforge"
project_autocoder.mkdir() project_autoforge.mkdir()
# Try to use terraform (should be blocked) # Try to use terraform (should be blocked)
input_data = {"tool_name": "Bash", "tool_input": {"command": "terraform apply"}} input_data = {"tool_name": "Bash", "tool_input": {"command": "terraform apply"}}
@@ -735,7 +735,7 @@ def test_pkill_extensibility():
with tempfile.TemporaryDirectory() as tmphome: with tempfile.TemporaryDirectory() as tmphome:
with tempfile.TemporaryDirectory() as tmpproject: with tempfile.TemporaryDirectory() as tmpproject:
with temporary_home(tmphome): with temporary_home(tmphome):
org_dir = Path(tmphome) / ".autocoder" org_dir = Path(tmphome) / ".autoforge"
org_dir.mkdir() org_dir.mkdir()
org_config_path = org_dir / "config.yaml" org_config_path = org_dir / "config.yaml"
@@ -762,9 +762,9 @@ pkill_processes:
with tempfile.TemporaryDirectory() as tmpproject: with tempfile.TemporaryDirectory() as tmpproject:
with temporary_home(tmphome): with temporary_home(tmphome):
project_dir = Path(tmpproject) project_dir = Path(tmpproject)
project_autocoder = project_dir / ".autocoder" project_autoforge = project_dir / ".autoforge"
project_autocoder.mkdir() project_autoforge.mkdir()
project_config = project_autocoder / "allowed_commands.yaml" project_config = project_autoforge / "allowed_commands.yaml"
# Create project config with extra pkill processes # Create project config with extra pkill processes
project_config.write_text("""version: 1 project_config.write_text("""version: 1
@@ -804,7 +804,7 @@ pkill_processes:
with tempfile.TemporaryDirectory() as tmphome: with tempfile.TemporaryDirectory() as tmphome:
with tempfile.TemporaryDirectory() as tmpproject: with tempfile.TemporaryDirectory() as tmpproject:
with temporary_home(tmphome): with temporary_home(tmphome):
org_dir = Path(tmphome) / ".autocoder" org_dir = Path(tmphome) / ".autoforge"
org_dir.mkdir() org_dir.mkdir()
org_config_path = org_dir / "config.yaml" org_config_path = org_dir / "config.yaml"
@@ -829,7 +829,7 @@ pkill_processes:
with tempfile.TemporaryDirectory() as tmphome: with tempfile.TemporaryDirectory() as tmphome:
with tempfile.TemporaryDirectory() as tmpproject: with tempfile.TemporaryDirectory() as tmpproject:
with temporary_home(tmphome): with temporary_home(tmphome):
org_dir = Path(tmphome) / ".autocoder" org_dir = Path(tmphome) / ".autoforge"
org_dir.mkdir() org_dir.mkdir()
org_config_path = org_dir / "config.yaml" org_config_path = org_dir / "config.yaml"
@@ -851,7 +851,7 @@ pkill_processes:
with tempfile.TemporaryDirectory() as tmphome: with tempfile.TemporaryDirectory() as tmphome:
with tempfile.TemporaryDirectory() as tmpproject: with tempfile.TemporaryDirectory() as tmpproject:
with temporary_home(tmphome): with temporary_home(tmphome):
org_dir = Path(tmphome) / ".autocoder" org_dir = Path(tmphome) / ".autoforge"
org_dir.mkdir() org_dir.mkdir()
org_config_path = org_dir / "config.yaml" org_config_path = org_dir / "config.yaml"
@@ -875,7 +875,7 @@ pkill_processes:
with tempfile.TemporaryDirectory() as tmphome: with tempfile.TemporaryDirectory() as tmphome:
with tempfile.TemporaryDirectory() as tmpproject: with tempfile.TemporaryDirectory() as tmpproject:
with temporary_home(tmphome): with temporary_home(tmphome):
org_dir = Path(tmphome) / ".autocoder" org_dir = Path(tmphome) / ".autoforge"
org_dir.mkdir() org_dir.mkdir()
org_config_path = org_dir / "config.yaml" org_config_path = org_dir / "config.yaml"

View File

@@ -79,9 +79,9 @@ def test_blocked_command_via_hook():
project_dir = Path(tmpdir) project_dir = Path(tmpdir)
# Create minimal project structure # Create minimal project structure
autocoder_dir = project_dir / ".autocoder" autoforge_dir = project_dir / ".autoforge"
autocoder_dir.mkdir() autoforge_dir.mkdir()
(autocoder_dir / "allowed_commands.yaml").write_text( (autoforge_dir / "allowed_commands.yaml").write_text(
"version: 1\ncommands: []" "version: 1\ncommands: []"
) )
@@ -114,9 +114,9 @@ def test_allowed_command_via_hook():
project_dir = Path(tmpdir) project_dir = Path(tmpdir)
# Create minimal project structure # Create minimal project structure
autocoder_dir = project_dir / ".autocoder" autoforge_dir = project_dir / ".autoforge"
autocoder_dir.mkdir() autoforge_dir.mkdir()
(autocoder_dir / "allowed_commands.yaml").write_text( (autoforge_dir / "allowed_commands.yaml").write_text(
"version: 1\ncommands: []" "version: 1\ncommands: []"
) )
@@ -145,9 +145,9 @@ def test_non_allowed_command_via_hook():
project_dir = Path(tmpdir) project_dir = Path(tmpdir)
# Create minimal project structure # Create minimal project structure
autocoder_dir = project_dir / ".autocoder" autoforge_dir = project_dir / ".autoforge"
autocoder_dir.mkdir() autoforge_dir.mkdir()
(autocoder_dir / "allowed_commands.yaml").write_text( (autoforge_dir / "allowed_commands.yaml").write_text(
"version: 1\ncommands: []" "version: 1\ncommands: []"
) )
@@ -179,9 +179,9 @@ def test_project_config_allows_command():
project_dir = Path(tmpdir) project_dir = Path(tmpdir)
# Create project config with swift allowed # Create project config with swift allowed
autocoder_dir = project_dir / ".autocoder" autoforge_dir = project_dir / ".autoforge"
autocoder_dir.mkdir() autoforge_dir.mkdir()
(autocoder_dir / "allowed_commands.yaml").write_text("""version: 1 (autoforge_dir / "allowed_commands.yaml").write_text("""version: 1
commands: commands:
- name: swift - name: swift
description: Swift compiler description: Swift compiler
@@ -214,9 +214,9 @@ def test_pattern_matching():
project_dir = Path(tmpdir) project_dir = Path(tmpdir)
# Create project config with swift* pattern # Create project config with swift* pattern
autocoder_dir = project_dir / ".autocoder" autoforge_dir = project_dir / ".autoforge"
autocoder_dir.mkdir() autoforge_dir.mkdir()
(autocoder_dir / "allowed_commands.yaml").write_text("""version: 1 (autoforge_dir / "allowed_commands.yaml").write_text("""version: 1
commands: commands:
- name: swift* - name: swift*
description: All Swift tools description: All Swift tools
@@ -247,7 +247,7 @@ def test_org_blocklist_enforcement():
with tempfile.TemporaryDirectory() as tmpproject: with tempfile.TemporaryDirectory() as tmpproject:
# Use context manager to safely set and restore HOME # Use context manager to safely set and restore HOME
with temporary_home(tmphome): with temporary_home(tmphome):
org_dir = Path(tmphome) / ".autocoder" org_dir = Path(tmphome) / ".autoforge"
org_dir.mkdir() org_dir.mkdir()
(org_dir / "config.yaml").write_text("""version: 1 (org_dir / "config.yaml").write_text("""version: 1
allowed_commands: [] allowed_commands: []
@@ -257,11 +257,11 @@ blocked_commands:
""") """)
project_dir = Path(tmpproject) project_dir = Path(tmpproject)
autocoder_dir = project_dir / ".autocoder" autoforge_dir = project_dir / ".autoforge"
autocoder_dir.mkdir() autoforge_dir.mkdir()
# Try to allow terraform in project config (should fail - org blocked) # Try to allow terraform in project config (should fail - org blocked)
(autocoder_dir / "allowed_commands.yaml").write_text("""version: 1 (autoforge_dir / "allowed_commands.yaml").write_text("""version: 1
commands: commands:
- name: terraform - name: terraform
description: Infrastructure as code description: Infrastructure as code
@@ -295,7 +295,7 @@ def test_org_allowlist_inheritance():
with tempfile.TemporaryDirectory() as tmpproject: with tempfile.TemporaryDirectory() as tmpproject:
# Use context manager to safely set and restore HOME # Use context manager to safely set and restore HOME
with temporary_home(tmphome): with temporary_home(tmphome):
org_dir = Path(tmphome) / ".autocoder" org_dir = Path(tmphome) / ".autoforge"
org_dir.mkdir() org_dir.mkdir()
(org_dir / "config.yaml").write_text("""version: 1 (org_dir / "config.yaml").write_text("""version: 1
allowed_commands: allowed_commands:
@@ -305,9 +305,9 @@ blocked_commands: []
""") """)
project_dir = Path(tmpproject) project_dir = Path(tmpproject)
autocoder_dir = project_dir / ".autocoder" autoforge_dir = project_dir / ".autoforge"
autocoder_dir.mkdir() autoforge_dir.mkdir()
(autocoder_dir / "allowed_commands.yaml").write_text( (autoforge_dir / "allowed_commands.yaml").write_text(
"version: 1\ncommands: []" "version: 1\ncommands: []"
) )
@@ -336,9 +336,9 @@ def test_invalid_yaml_ignored():
project_dir = Path(tmpdir) project_dir = Path(tmpdir)
# Create invalid YAML # Create invalid YAML
autocoder_dir = project_dir / ".autocoder" autoforge_dir = project_dir / ".autoforge"
autocoder_dir.mkdir() autoforge_dir.mkdir()
(autocoder_dir / "allowed_commands.yaml").write_text("invalid: yaml: content:") (autoforge_dir / "allowed_commands.yaml").write_text("invalid: yaml: content:")
# Try to run ls (should still work - falls back to defaults) # Try to run ls (should still work - falls back to defaults)
input_data = {"tool_name": "Bash", "tool_input": {"command": "ls"}} input_data = {"tool_name": "Bash", "tool_input": {"command": "ls"}}
@@ -365,13 +365,13 @@ def test_100_command_limit():
project_dir = Path(tmpdir) project_dir = Path(tmpdir)
# Create config with 101 commands # Create config with 101 commands
autocoder_dir = project_dir / ".autocoder" autoforge_dir = project_dir / ".autoforge"
autocoder_dir.mkdir() autoforge_dir.mkdir()
commands = [ commands = [
f" - name: cmd{i}\n description: Command {i}" for i in range(101) f" - name: cmd{i}\n description: Command {i}" for i in range(101)
] ]
(autocoder_dir / "allowed_commands.yaml").write_text( (autoforge_dir / "allowed_commands.yaml").write_text(
"version: 1\ncommands:\n" + "\n".join(commands) "version: 1\ncommands:\n" + "\n".join(commands)
) )

View File

@@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AutoCoder</title> <title>AutoForge</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Work+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=DM+Sans:wght@400;500;700&family=Space+Mono:wght@400;700&family=Outfit:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Work+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=DM+Sans:wght@400;500;700&family=Space+Mono:wght@400;700&family=Outfit:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">

16
ui/package-lock.json generated
View File

@@ -1,11 +1,11 @@
{ {
"name": "autocoder", "name": "autoforge-ui",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "autocoder", "name": "autoforge-ui",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.3",
@@ -50,6 +50,18 @@
"vite": "^7.3.0" "vite": "^7.3.0"
} }
}, },
"..": {
"name": "autoforge-ai",
"version": "0.1.0",
"extraneous": true,
"license": "AGPL-3.0",
"bin": {
"autoforge": "bin/autoforge.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",

View File

@@ -1,5 +1,5 @@
{ {
"name": "autocoder", "name": "autoforge-ui",
"private": true, "private": true,
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",

BIN
ui/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

View File

@@ -34,8 +34,8 @@ import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card' import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
const STORAGE_KEY = 'autocoder-selected-project' const STORAGE_KEY = 'autoforge-selected-project'
const VIEW_MODE_KEY = 'autocoder-view-mode' const VIEW_MODE_KEY = 'autoforge-view-mode'
// Bottom padding for main content when debug panel is collapsed (40px header + 8px margin) // Bottom padding for main content when debug panel is collapsed (40px header + 8px margin)
const COLLAPSED_DEBUG_PANEL_CLEARANCE = 48 const COLLAPSED_DEBUG_PANEL_CLEARANCE = 48
@@ -263,9 +263,12 @@ function App() {
<div className="max-w-7xl mx-auto px-4 py-4"> <div className="max-w-7xl mx-auto px-4 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{/* Logo and Title */} {/* Logo and Title */}
<h1 className="font-display text-2xl font-bold tracking-tight uppercase"> <div className="flex items-center gap-3">
AutoCoder <img src="/logo.png" alt="AutoForge" className="h-9 w-9 rounded-full" />
</h1> <h1 className="font-display text-2xl font-bold tracking-tight uppercase">
AutoForge
</h1>
</div>
{/* Controls */} {/* Controls */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -337,7 +340,7 @@ function App() {
{/* Docs link */} {/* Docs link */}
<Button <Button
onClick={() => { window.location.hash = '#/docs' }} onClick={() => window.open('https://autoforge.cc', '_blank')}
variant="outline" variant="outline"
size="sm" size="sm"
title="Documentation" title="Documentation"
@@ -376,7 +379,7 @@ function App() {
{!selectedProject ? ( {!selectedProject ? (
<div className="text-center mt-12"> <div className="text-center mt-12">
<h2 className="font-display text-2xl font-bold mb-2"> <h2 className="font-display text-2xl font-bold mb-2">
Welcome to AutoCoder Welcome to AutoForge
</h2> </h2>
<p className="text-muted-foreground mb-4"> <p className="text-muted-foreground mb-4">
Select a project from the dropdown above or create a new one to get started. Select a project from the dropdown above or create a new one to get started.

View File

@@ -8,7 +8,7 @@ import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
const ACTIVITY_COLLAPSED_KEY = 'autocoder-activity-collapsed' const ACTIVITY_COLLAPSED_KEY = 'autoforge-activity-collapsed'
interface AgentMissionControlProps { interface AgentMissionControlProps {
agents: ActiveAgent[] agents: ActiveAgent[]
@@ -88,8 +88,8 @@ export function AgentMissionControl({
{/* Content */} {/* Content */}
<div <div
className={` className={`
transition-all duration-300 ease-out overflow-hidden transition-all duration-300 ease-out
${isExpanded ? 'max-h-[600px] opacity-100' : 'max-h-0 opacity-0'} ${isExpanded ? 'max-h-[600px] opacity-100 overflow-y-auto' : 'max-h-0 opacity-0 overflow-hidden'}
`} `}
> >
<CardContent className="p-4"> <CardContent className="p-4">

View File

@@ -1,130 +0,0 @@
/**
* DocsContent Component
*
* Renders all 13 documentation section components in order.
* Uses IntersectionObserver to detect which section heading is currently
* visible in the viewport, and notifies the parent so the sidebar
* can highlight the active section.
*/
import { useEffect, useRef, useCallback } from 'react'
import { DOC_SECTIONS } from './docsData'
// Section components -- lazy-load candidates in the future, but imported
// statically for now to keep the build simple and deterministic.
import { GettingStarted } from './sections/GettingStarted'
import { AppSpecSetup } from './sections/AppSpecSetup'
import { ProjectStructure } from './sections/ProjectStructure'
import { FeaturesKanban } from './sections/FeaturesKanban'
import { AgentSystem } from './sections/AgentSystem'
import { SettingsConfig } from './sections/SettingsConfig'
import { DeveloperTools } from './sections/DeveloperTools'
import { AIAssistant } from './sections/AIAssistant'
import { Scheduling } from './sections/Scheduling'
import { AppearanceThemes } from './sections/AppearanceThemes'
import { Security } from './sections/Security'
import { AdvancedConfig } from './sections/AdvancedConfig'
import { FAQ } from './sections/FAQ'
interface DocsContentProps {
activeSectionId: string | null
onSectionVisible: (id: string) => void
}
/**
* Maps each section id from docsData to its corresponding React component.
* Order matches DOC_SECTIONS so we can iterate safely.
*/
const SECTION_COMPONENTS: Record<string, React.FC> = {
'getting-started': GettingStarted,
'app-spec-setup': AppSpecSetup,
'project-structure': ProjectStructure,
'features-kanban': FeaturesKanban,
'agent-system': AgentSystem,
'settings-config': SettingsConfig,
'developer-tools': DeveloperTools,
'ai-assistant': AIAssistant,
scheduling: Scheduling,
'appearance-themes': AppearanceThemes,
security: Security,
'advanced-config': AdvancedConfig,
faq: FAQ,
}
export function DocsContent({ onSectionVisible }: DocsContentProps) {
const containerRef = useRef<HTMLDivElement>(null)
// Store refs to each section heading element so the observer can watch them
const headingRefs = useRef<Map<string, HTMLElement>>(new Map())
// Stable callback ref setter -- avoids recreating refs on every render
const setHeadingRef = useCallback((id: string, element: HTMLElement | null) => {
if (element) {
headingRefs.current.set(id, element)
} else {
headingRefs.current.delete(id)
}
}, [])
// IntersectionObserver: track which section heading is at or near the top of the viewport
useEffect(() => {
const headings = headingRefs.current
if (headings.size === 0) return
// rootMargin: trigger when a heading enters the top 20% of the viewport.
// This ensures the sidebar updates *before* the user scrolls past the heading.
const observer = new IntersectionObserver(
(entries) => {
// Find the topmost visible heading -- the one closest to the top of the viewport
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)
if (visible.length > 0) {
const topEntry = visible[0]
const sectionId = topEntry.target.getAttribute('data-section-id')
if (sectionId) {
onSectionVisible(sectionId)
}
}
},
{
// Observe from the very top of the viewport down to -60% from the bottom,
// so headings are detected while in the upper portion of the screen.
rootMargin: '0px 0px -60% 0px',
threshold: 0,
},
)
headings.forEach((element) => observer.observe(element))
return () => observer.disconnect()
}, [onSectionVisible])
return (
<div ref={containerRef} className="docs-prose">
{DOC_SECTIONS.map((section) => {
const SectionComponent = SECTION_COMPONENTS[section.id]
if (!SectionComponent) return null
const Icon = section.icon
return (
<div key={section.id} id={section.id} className="scroll-mt-24 mb-16">
{/* Section heading with anchor */}
<h2
ref={(el) => setHeadingRef(section.id, el)}
data-section-id={section.id}
className="font-display text-2xl font-bold tracking-tight mb-6 flex items-center gap-3
text-foreground border-b-2 border-border pb-3"
>
<Icon size={24} className="text-primary shrink-0" />
{section.title}
</h2>
{/* Section body */}
<SectionComponent />
</div>
)
})}
</div>
)
}

View File

@@ -1,215 +0,0 @@
/**
* DocsPage Component
*
* Main layout for the documentation route (#/docs).
* Full-page layout with a sticky header, collapsible sidebar on the left,
* and scrollable content area on the right.
*
* Mobile-responsive: sidebar collapses behind a hamburger menu that
* opens as an overlay.
*/
import { useState, useEffect, useCallback } from 'react'
import { ArrowLeft, Menu, X, Moon, Sun } from 'lucide-react'
import { useHashRoute } from '../../hooks/useHashRoute'
import { useTheme } from '../../hooks/useTheme'
import { ThemeSelector } from '../ThemeSelector'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { DocsSidebar } from './DocsSidebar'
import { DocsSearch } from './DocsSearch'
import { DocsContent } from './DocsContent'
export function DocsPage() {
const [activeSectionId, setActiveSectionId] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
const { section: initialSection } = useHashRoute()
const { theme, setTheme, darkMode, toggleDarkMode, themes } = useTheme()
// On mount, if the hash includes a section id (e.g. #/docs/getting-started),
// scroll to it and set it as active
useEffect(() => {
if (initialSection) {
setActiveSectionId(initialSection)
// Delay scroll slightly so the DOM is rendered
requestAnimationFrame(() => {
const element = document.getElementById(initialSection)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []) // Run only on mount
// When a sidebar item is clicked, scroll the corresponding element into view
const handleSectionClick = useCallback((id: string) => {
setActiveSectionId(id)
// Update hash for linkability (without triggering a route change)
history.replaceState(null, '', `#/docs/${id}`)
const element = document.getElementById(id)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}, [])
// Called by DocsContent's IntersectionObserver when a heading scrolls into view
const handleSectionVisible = useCallback((id: string) => {
setActiveSectionId(id)
}, [])
// Close mobile sidebar when pressing Escape
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && mobileSidebarOpen) {
setMobileSidebarOpen(false)
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [mobileSidebarOpen])
// Prevent body scroll when mobile sidebar overlay is open
useEffect(() => {
if (mobileSidebarOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => {
document.body.style.overflow = ''
}
}, [mobileSidebarOpen])
return (
<div className="min-h-screen bg-background">
{/* Sticky header */}
<header className="sticky top-0 z-50 bg-card/80 backdrop-blur-md text-foreground border-b-2 border-border">
<div className="max-w-7xl mx-auto px-4 py-3">
<div className="flex items-center justify-between">
{/* Left side: hamburger (mobile) + title + badge */}
<div className="flex items-center gap-3">
{/* Mobile hamburger button -- only visible below lg breakpoint */}
<Button
variant="ghost"
size="icon-sm"
className="lg:hidden"
onClick={() => setMobileSidebarOpen(!mobileSidebarOpen)}
aria-label={mobileSidebarOpen ? 'Close sidebar' : 'Open sidebar'}
>
{mobileSidebarOpen ? <X size={20} /> : <Menu size={20} />}
</Button>
<a
href="#/"
className="font-display text-xl font-bold tracking-tight uppercase text-foreground
hover:text-primary transition-colors"
>
AutoCoder
</a>
<Badge variant="secondary" className="text-xs font-medium">
Documentation
</Badge>
</div>
{/* Right side: theme controls + back button */}
<div className="flex items-center gap-2">
<ThemeSelector
themes={themes}
currentTheme={theme}
onThemeChange={setTheme}
/>
<Button
onClick={toggleDarkMode}
variant="outline"
size="sm"
title="Toggle dark mode"
aria-label="Toggle dark mode"
>
{darkMode ? <Sun size={18} /> : <Moon size={18} />}
</Button>
<Button
variant="outline"
size="sm"
asChild
>
<a href="#/" className="inline-flex items-center gap-1.5">
<ArrowLeft size={16} />
<span className="hidden sm:inline">Back to App</span>
</a>
</Button>
</div>
</div>
</div>
</header>
{/* Body: sidebar + content */}
<div className="max-w-7xl mx-auto flex">
{/* ----------------------------------------------------------------
Desktop sidebar -- visible at lg and above
Fixed width, sticky below the header, independently scrollable
---------------------------------------------------------------- */}
<aside
className="hidden lg:block w-[280px] shrink-0 sticky top-[57px] h-[calc(100vh-57px)]
overflow-y-auto border-r border-border p-4 space-y-4"
>
<DocsSearch value={searchQuery} onChange={setSearchQuery} />
<DocsSidebar
activeSectionId={activeSectionId}
onSectionClick={handleSectionClick}
searchQuery={searchQuery}
/>
</aside>
{/* ----------------------------------------------------------------
Mobile sidebar overlay -- visible below lg breakpoint
---------------------------------------------------------------- */}
{mobileSidebarOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40 bg-background/60 backdrop-blur-sm lg:hidden"
onClick={() => setMobileSidebarOpen(false)}
aria-hidden="true"
/>
{/* Sidebar panel */}
<aside
className="fixed top-[57px] left-0 z-50 w-[280px] h-[calc(100vh-57px)]
overflow-y-auto bg-card border-r-2 border-border p-4 space-y-4
animate-slide-in lg:hidden"
>
<DocsSearch value={searchQuery} onChange={setSearchQuery} />
<DocsSidebar
activeSectionId={activeSectionId}
onSectionClick={handleSectionClick}
searchQuery={searchQuery}
onMobileClose={() => setMobileSidebarOpen(false)}
/>
</aside>
</>
)}
{/* ----------------------------------------------------------------
Content area -- fills remaining space, scrollable
---------------------------------------------------------------- */}
<main className="flex-1 min-w-0 px-6 py-8 lg:px-10">
<div className="max-w-[65ch] mx-auto">
<DocsContent
activeSectionId={activeSectionId}
onSectionVisible={handleSectionVisible}
/>
</div>
</main>
</div>
</div>
)
}

View File

@@ -1,78 +0,0 @@
/**
* DocsSearch Component
*
* Search input for the documentation sidebar.
* Supports Ctrl/Cmd+K keyboard shortcut to focus,
* and shows a keyboard hint when the input is empty.
*/
import { useRef, useEffect } from 'react'
import { Search, X } from 'lucide-react'
interface DocsSearchProps {
value: string
onChange: (value: string) => void
}
export function DocsSearch({ value, onChange }: DocsSearchProps) {
const inputRef = useRef<HTMLInputElement>(null)
// Global keyboard shortcut: Ctrl/Cmd+K focuses the search input
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault()
inputRef.current?.focus()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [])
return (
<div className="relative">
{/* Search icon */}
<Search
size={16}
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none"
/>
<input
ref={inputRef}
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="Search docs..."
className="w-full pl-9 pr-16 py-2 text-sm bg-muted border border-border rounded-lg
text-foreground placeholder:text-muted-foreground
focus:outline-none focus:ring-2 focus:ring-ring/50 focus:border-ring
transition-colors"
/>
{/* Right side: clear button when has value, otherwise Ctrl+K hint */}
{value ? (
<button
onClick={() => {
onChange('')
inputRef.current?.focus()
}}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground
hover:text-foreground transition-colors"
aria-label="Clear search"
>
<X size={16} />
</button>
) : (
<kbd
className="absolute right-3 top-1/2 -translate-y-1/2
text-[10px] text-muted-foreground bg-background
border border-border rounded px-1.5 py-0.5
pointer-events-none select-none"
>
Ctrl+K
</kbd>
)}
</div>
)
}

View File

@@ -1,189 +0,0 @@
/**
* DocsSidebar Component
*
* Left sidebar navigation for the documentation page.
* Lists all sections from docsData with expandable subsections.
* Supports search filtering with auto-expansion of matching sections.
*/
import { useState, useMemo } from 'react'
import { ChevronRight } from 'lucide-react'
import { DOC_SECTIONS, type DocSection } from './docsData'
interface DocsSidebarProps {
activeSectionId: string | null
onSectionClick: (id: string) => void
searchQuery: string
onMobileClose?: () => void
}
export function DocsSidebar({
activeSectionId,
onSectionClick,
searchQuery,
onMobileClose,
}: DocsSidebarProps) {
// Track which top-level sections are manually expanded by the user
const [expandedSections, setExpandedSections] = useState<Set<string>>(() => {
// Start with the first section expanded so the sidebar is not fully collapsed
const initial = new Set<string>()
if (DOC_SECTIONS.length > 0) {
initial.add(DOC_SECTIONS[0].id)
}
return initial
})
const normalizedQuery = searchQuery.trim().toLowerCase()
// Filter sections based on search query, matching against section title,
// subsection titles, and keywords
const filteredSections = useMemo(() => {
if (!normalizedQuery) {
return DOC_SECTIONS
}
return DOC_SECTIONS.filter((section) => {
// Check section title
if (section.title.toLowerCase().includes(normalizedQuery)) return true
// Check keywords
if (section.keywords.some((kw) => kw.toLowerCase().includes(normalizedQuery))) return true
// Check subsection titles
if (section.subsections.some((sub) => sub.title.toLowerCase().includes(normalizedQuery))) {
return true
}
return false
})
}, [normalizedQuery])
// Determine which sections should appear expanded:
// - When searching: auto-expand all matching sections
// - Otherwise: use manual expanded state, plus expand whichever section contains the active item
const isSectionExpanded = (sectionId: string): boolean => {
if (normalizedQuery) return true
if (expandedSections.has(sectionId)) return true
// Also expand the section that contains the currently active subsection
if (activeSectionId) {
const section = DOC_SECTIONS.find((s) => s.id === sectionId)
if (section) {
if (section.id === activeSectionId) return true
if (section.subsections.some((sub) => sub.id === activeSectionId)) return true
}
}
return false
}
const toggleSection = (sectionId: string) => {
setExpandedSections((prev) => {
const next = new Set(prev)
if (next.has(sectionId)) {
next.delete(sectionId)
} else {
next.add(sectionId)
}
return next
})
}
/**
* Checks whether a given id (section or subsection) is the currently active item.
* Active items get a highlighted visual treatment.
*/
const isActive = (id: string): boolean => activeSectionId === id
/**
* Checks whether a section contains the active subsection.
* Used to highlight parent sections in a muted way.
*/
const sectionContainsActive = (section: DocSection): boolean => {
if (!activeSectionId) return false
return section.subsections.some((sub) => sub.id === activeSectionId)
}
const handleItemClick = (id: string) => {
onSectionClick(id)
// On mobile, close the sidebar after navigation
onMobileClose?.()
}
return (
<nav aria-label="Documentation navigation" className="space-y-1">
{filteredSections.map((section) => {
const Icon = section.icon
const expanded = isSectionExpanded(section.id)
const active = isActive(section.id)
const containsActive = sectionContainsActive(section)
return (
<div key={section.id}>
{/* Section header (clickable to expand/collapse and navigate) */}
<button
onClick={() => {
toggleSection(section.id)
handleItemClick(section.id)
}}
className={`w-full flex items-center gap-2 px-3 py-2 text-sm rounded-md
transition-colors cursor-pointer group
${active
? 'bg-primary/10 border-l-2 border-primary text-foreground font-semibold'
: containsActive
? 'text-foreground font-medium'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
}`}
aria-expanded={expanded}
>
<Icon
size={16}
className={`shrink-0 ${active ? 'text-primary' : 'text-muted-foreground group-hover:text-foreground'}`}
/>
<span className="flex-1 text-left truncate">{section.title}</span>
<ChevronRight
size={14}
className={`shrink-0 text-muted-foreground transition-transform duration-200
${expanded ? 'rotate-90' : ''}`}
/>
</button>
{/* Subsections (shown when expanded) */}
{expanded && (
<div className="ml-4 mt-0.5 space-y-0.5 border-l border-border animate-slide-in-down">
{section.subsections.map((sub) => {
const subActive = isActive(sub.id)
return (
<button
key={sub.id}
onClick={() => handleItemClick(sub.id)}
className={`w-full text-left px-3 py-1.5 text-sm rounded-r-md
transition-colors cursor-pointer
${subActive
? 'bg-primary/10 border-l-2 border-primary text-foreground font-medium -ml-px'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
}`}
>
{sub.title}
</button>
)
})}
</div>
)}
</div>
)
})}
{/* No results message when search filters everything out */}
{normalizedQuery && filteredSections.length === 0 && (
<div className="px-3 py-6 text-center text-sm text-muted-foreground">
No sections match &ldquo;{searchQuery}&rdquo;
</div>
)}
</nav>
)
}

View File

@@ -1,222 +0,0 @@
import {
Rocket,
FileText,
FolderTree,
LayoutGrid,
Bot,
Settings,
Terminal,
MessageSquare,
Clock,
Palette,
Shield,
Wrench,
HelpCircle,
type LucideIcon,
} from 'lucide-react'
export interface DocSubsection {
id: string
title: string
}
export interface DocSection {
id: string
title: string
icon: LucideIcon
subsections: DocSubsection[]
keywords: string[]
}
export const DOC_SECTIONS: DocSection[] = [
{
id: 'getting-started',
title: 'Getting Started',
icon: Rocket,
subsections: [
{ id: 'what-is-autocoder', title: 'What is AutoCoder?' },
{ id: 'quick-start', title: 'Quick Start' },
{ id: 'creating-a-project', title: 'Creating a New Project' },
{ id: 'existing-project', title: 'Adding to an Existing Project' },
{ id: 'system-requirements', title: 'System Requirements' },
],
keywords: ['install', 'setup', 'start', 'begin', 'new', 'requirements', 'prerequisites'],
},
{
id: 'app-spec-setup',
title: 'App Spec & Project Setup',
icon: FileText,
subsections: [
{ id: 'what-is-app-spec', title: 'What is an App Spec?' },
{ id: 'creating-spec-with-claude', title: 'Creating a Spec with Claude' },
{ id: 'writing-spec-manually', title: 'Writing a Spec Manually' },
{ id: 'initializer-agent', title: 'The Initializer Agent' },
{ id: 'starting-after-spec', title: 'Starting After Spec Creation' },
],
keywords: ['spec', 'specification', 'xml', 'app_spec', 'initializer', 'prompt', 'template'],
},
{
id: 'project-structure',
title: 'Target Project Structure',
icon: FolderTree,
subsections: [
{ id: 'autocoder-directory', title: '.autocoder/ Directory Layout' },
{ id: 'features-db', title: 'Features Database' },
{ id: 'prompts-directory', title: 'Prompts Directory' },
{ id: 'allowed-commands-yaml', title: 'Allowed Commands Config' },
{ id: 'claude-md', title: 'CLAUDE.md Convention' },
{ id: 'legacy-migration', title: 'Legacy Layout Migration' },
{ id: 'claude-inheritance', title: 'Claude Inheritance' },
],
keywords: ['folder', 'directory', 'structure', 'layout', 'files', 'database', 'sqlite', 'migration'],
},
{
id: 'features-kanban',
title: 'Features & Kanban Board',
icon: LayoutGrid,
subsections: [
{ id: 'kanban-overview', title: 'Kanban Board Overview' },
{ id: 'feature-cards', title: 'Feature Cards' },
{ id: 'dependency-graph', title: 'Dependency Graph View' },
{ id: 'adding-features', title: 'Adding Features' },
{ id: 'editing-features', title: 'Editing & Deleting Features' },
{ id: 'feature-dependencies', title: 'Feature Dependencies' },
{ id: 'expanding-with-ai', title: 'Expanding Project with AI' },
{ id: 'feature-priority', title: 'Priority & Ordering' },
],
keywords: ['kanban', 'board', 'feature', 'card', 'dependency', 'graph', 'priority', 'pending', 'progress', 'done'],
},
{
id: 'agent-system',
title: 'Agent System',
icon: Bot,
subsections: [
{ id: 'maestro-orchestrator', title: 'Maestro: The Orchestrator' },
{ id: 'coding-agents', title: 'Coding Agents' },
{ id: 'testing-agents', title: 'Testing Agents' },
{ id: 'agent-lifecycle', title: 'Agent Lifecycle' },
{ id: 'concurrency', title: 'Concurrency Control' },
{ id: 'mission-control', title: 'Agent Mission Control' },
{ id: 'agent-mascots', title: 'Agent Mascots & States' },
{ id: 'agent-logs', title: 'Viewing Agent Logs' },
{ id: 'process-limits', title: 'Process Limits' },
],
keywords: ['agent', 'maestro', 'orchestrator', 'coding', 'testing', 'parallel', 'concurrency', 'mascot', 'spark', 'fizz', 'octo', 'batch'],
},
{
id: 'settings-config',
title: 'Settings & Configuration',
icon: Settings,
subsections: [
{ id: 'opening-settings', title: 'Opening Settings' },
{ id: 'yolo-mode', title: 'YOLO Mode' },
{ id: 'headless-browser', title: 'Headless Browser' },
{ id: 'model-selection', title: 'Model Selection' },
{ id: 'regression-agents', title: 'Regression Agents' },
{ id: 'features-per-agent', title: 'Features per Agent (Batch Size)' },
{ id: 'concurrency-setting', title: 'Concurrency' },
{ id: 'settings-persistence', title: 'How Settings are Persisted' },
],
keywords: ['settings', 'config', 'yolo', 'headless', 'model', 'opus', 'sonnet', 'haiku', 'batch', 'regression'],
},
{
id: 'developer-tools',
title: 'Developer Tools',
icon: Terminal,
subsections: [
{ id: 'debug-panel', title: 'Debug Panel' },
{ id: 'agent-logs-tab', title: 'Agent Logs Tab' },
{ id: 'dev-server-logs', title: 'Dev Server Logs Tab' },
{ id: 'terminal', title: 'Terminal' },
{ id: 'dev-server-control', title: 'Dev Server Control' },
{ id: 'per-agent-logs', title: 'Per-Agent Logs' },
],
keywords: ['debug', 'terminal', 'logs', 'dev server', 'console', 'xterm', 'shell'],
},
{
id: 'ai-assistant',
title: 'AI Assistant',
icon: MessageSquare,
subsections: [
{ id: 'what-is-assistant', title: 'What is the Assistant?' },
{ id: 'opening-assistant', title: 'Opening the Assistant' },
{ id: 'assistant-capabilities', title: 'What It Can Do' },
{ id: 'assistant-limitations', title: 'What It Cannot Do' },
{ id: 'conversation-history', title: 'Conversation History' },
],
keywords: ['assistant', 'ai', 'chat', 'help', 'question', 'conversation'],
},
{
id: 'scheduling',
title: 'Scheduling',
icon: Clock,
subsections: [
{ id: 'what-scheduling-does', title: 'What Scheduling Does' },
{ id: 'creating-schedule', title: 'Creating a Schedule' },
{ id: 'schedule-settings', title: 'Schedule Settings' },
{ id: 'schedule-overrides', title: 'Schedule Overrides' },
{ id: 'crash-recovery', title: 'Crash Recovery' },
],
keywords: ['schedule', 'timer', 'automated', 'cron', 'run', 'recurring', 'utc'],
},
{
id: 'appearance-themes',
title: 'Appearance & Themes',
icon: Palette,
subsections: [
{ id: 'themes-overview', title: 'Themes Overview' },
{ id: 'dark-light-mode', title: 'Dark & Light Mode' },
{ id: 'theme-selector', title: 'Theme Selector' },
{ id: 'keyboard-shortcuts', title: 'Keyboard Shortcuts' },
],
keywords: ['theme', 'dark', 'light', 'color', 'appearance', 'twitter', 'claude', 'neo', 'brutalism', 'retro', 'aurora', 'business', 'keyboard', 'shortcut'],
},
{
id: 'security',
title: 'Security',
icon: Shield,
subsections: [
{ id: 'command-validation', title: 'Command Validation Overview' },
{ id: 'command-hierarchy', title: 'Command Hierarchy' },
{ id: 'hardcoded-blocklist', title: 'Hardcoded Blocklist' },
{ id: 'global-allowlist', title: 'Global Allowlist' },
{ id: 'project-allowlist', title: 'Per-Project Allowed Commands' },
{ id: 'org-config', title: 'Organization Configuration' },
{ id: 'extra-read-paths', title: 'Extra Read Paths' },
{ id: 'filesystem-sandboxing', title: 'Filesystem Sandboxing' },
],
keywords: ['security', 'sandbox', 'allowlist', 'blocklist', 'command', 'bash', 'permission', 'filesystem'],
},
{
id: 'advanced-config',
title: 'Advanced Configuration',
icon: Wrench,
subsections: [
{ id: 'vertex-ai', title: 'Vertex AI Setup' },
{ id: 'ollama', title: 'Ollama Local Models' },
{ id: 'env-variables', title: 'Environment Variables' },
{ id: 'cli-arguments', title: 'CLI Arguments' },
{ id: 'webhooks', title: 'Webhook Support' },
{ id: 'project-registry', title: 'Project Registry' },
],
keywords: ['vertex', 'gcloud', 'ollama', 'local', 'env', 'environment', 'cli', 'webhook', 'n8n', 'registry', 'api'],
},
{
id: 'faq',
title: 'FAQ & Troubleshooting',
icon: HelpCircle,
subsections: [
{ id: 'faq-new-project', title: 'Starting a New Project' },
{ id: 'faq-existing-project', title: 'Adding to Existing Project' },
{ id: 'faq-agent-crash', title: 'Agent Crashes' },
{ id: 'faq-custom-commands', title: 'Custom Bash Commands' },
{ id: 'faq-blocked-features', title: 'Blocked Features' },
{ id: 'faq-parallel', title: 'Running in Parallel' },
{ id: 'faq-local-model', title: 'Using Local Models' },
{ id: 'faq-reset', title: 'Resetting a Project' },
{ id: 'faq-agent-types', title: 'Coding vs Testing Agents' },
{ id: 'faq-real-time', title: 'Monitoring in Real Time' },
],
keywords: ['faq', 'troubleshoot', 'help', 'problem', 'issue', 'fix', 'error', 'stuck', 'reset', 'crash'],
},
]

View File

@@ -1,75 +0,0 @@
/**
* AIAssistant Documentation Section
*
* Covers the project assistant: what it is, how to open it,
* its capabilities and limitations, and conversation history.
*/
import { Badge } from '@/components/ui/badge'
export function AIAssistant() {
return (
<div>
{/* What is the Assistant? */}
<h3 id="what-is-assistant" className="text-lg font-semibold text-foreground mt-8 mb-3">
What is the Assistant?
</h3>
<p className="text-muted-foreground mb-4">
The AI Assistant is a read-only project helper that can answer questions about your project, search
code, view progress, and help you understand what&rsquo;s happening &mdash; without making any changes.
</p>
{/* Opening the Assistant */}
<h3 id="opening-assistant" className="text-lg font-semibold text-foreground mt-8 mb-3">
Opening the Assistant
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
Press <Badge variant="secondary">A</Badge> to toggle the assistant panel
</li>
<li>Or click the floating action button (chat bubble) in the bottom-right corner</li>
<li>The panel slides in from the right side</li>
</ul>
{/* What It Can Do */}
<h3 id="assistant-capabilities" className="text-lg font-semibold text-foreground mt-8 mb-3">
What It Can Do
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Read and search your project&rsquo;s source code</li>
<li>Answer questions about code architecture and implementation</li>
<li>View feature progress and status</li>
<li>Create new features based on your description</li>
<li>Explain what agents have done or are currently doing</li>
<li>Help debug issues by analyzing code and logs</li>
</ul>
{/* What It Cannot Do */}
<h3 id="assistant-limitations" className="text-lg font-semibold text-foreground mt-8 mb-3">
What It Cannot Do
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Modify files (read-only access)</li>
<li>Run bash commands</li>
<li>Mark features as passing/failing</li>
<li>Start or stop agents</li>
<li>Access external APIs or the internet</li>
</ul>
<div className="border-l-4 border-primary pl-4 italic text-muted-foreground mt-4">
This is a deliberate security design &mdash; the assistant is a safe way to interact with your project
without risk of unintended changes.
</div>
{/* Conversation History */}
<h3 id="conversation-history" className="text-lg font-semibold text-foreground mt-8 mb-3">
Conversation History
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Conversations are stored per-project in SQLite database</li>
<li>Multiple conversations supported &mdash; start new ones as needed</li>
<li>Switch between conversations using the conversation selector</li>
<li>History persists across browser sessions</li>
</ul>
</div>
)
}

View File

@@ -1,220 +0,0 @@
/**
* AdvancedConfig Documentation Section
*
* Covers Vertex AI setup, Ollama local models, environment variables,
* CLI arguments, webhook support, and the project registry.
*/
import { Badge } from '@/components/ui/badge'
/** Environment variable descriptor for the reference table. */
interface EnvVar {
name: string
description: string
}
const ENV_VARS: EnvVar[] = [
{ name: 'CLAUDE_CODE_USE_VERTEX', description: 'Enable Vertex AI (1)' },
{ name: 'CLOUD_ML_REGION', description: 'GCP region' },
{ name: 'ANTHROPIC_VERTEX_PROJECT_ID', description: 'GCP project ID' },
{ name: 'ANTHROPIC_BASE_URL', description: 'Custom API base URL (for Ollama)' },
{ name: 'ANTHROPIC_AUTH_TOKEN', description: 'API auth token' },
{ name: 'API_TIMEOUT_MS', description: 'API timeout in milliseconds' },
{ name: 'EXTRA_READ_PATHS', description: 'Comma-separated extra read directories' },
{ name: 'ANTHROPIC_DEFAULT_OPUS_MODEL', description: 'Override Opus model name' },
{ name: 'ANTHROPIC_DEFAULT_SONNET_MODEL', description: 'Override Sonnet model name' },
{ name: 'ANTHROPIC_DEFAULT_HAIKU_MODEL', description: 'Override Haiku model name' },
]
/** CLI argument descriptor for the reference table. */
interface CliArg {
name: string
description: string
}
const CLI_ARGS: CliArg[] = [
{ name: '--project-dir', description: 'Project directory path or registered name' },
{ name: '--yolo', description: 'Enable YOLO mode' },
{ name: '--parallel', description: 'Enable parallel mode' },
{ name: '--max-concurrency N', description: 'Max concurrent agents (1-5)' },
{ name: '--batch-size N', description: 'Features per coding agent (1-3)' },
{ name: '--batch-features 1,2,3', description: 'Specific feature IDs to implement' },
{ name: '--testing-batch-size N', description: 'Features per testing batch (1-5)' },
{ name: '--testing-batch-features 1,2,3', description: 'Specific testing feature IDs' },
]
export function AdvancedConfig() {
return (
<div>
{/* Vertex AI Setup */}
<h3 id="vertex-ai" className="text-lg font-semibold text-foreground mt-8 mb-3">
Vertex AI Setup
</h3>
<p className="text-muted-foreground mb-3">
Run coding agents via Google Cloud Vertex AI:
</p>
<ol className="list-decimal space-y-2 ml-4 text-muted-foreground">
<li>
Install and authenticate the gcloud CLI:{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
gcloud auth application-default login
</span>
</li>
<li>
Configure your{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">.env</span> file:
</li>
</ol>
<div className="bg-muted rounded-lg p-4 font-mono text-sm mt-3">
<pre><code>{`CLAUDE_CODE_USE_VERTEX=1
CLOUD_ML_REGION=us-east5
ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project-id
ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-5@20251101
ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-5@20250929
ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-3-5-haiku@20241022`}</code></pre>
</div>
<blockquote className="border-l-4 border-primary pl-4 italic text-muted-foreground mt-4">
Use <span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono not-italic">@</span>{' '}
instead of <span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono not-italic">-</span>{' '}
in model names for Vertex AI.
</blockquote>
{/* Ollama Local Models */}
<h3 id="ollama" className="text-lg font-semibold text-foreground mt-8 mb-3">
Ollama Local Models
</h3>
<p className="text-muted-foreground mb-3">
Run coding agents using local models via Ollama v0.14.0+:
</p>
<ol className="list-decimal space-y-2 ml-4 text-muted-foreground">
<li>
Install Ollama from{' '}
<a href="https://ollama.com" target="_blank" rel="noreferrer" className="text-primary underline">
ollama.com
</a>
</li>
<li>
Start Ollama:{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">ollama serve</span>
</li>
<li>
Pull a coding model:{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">ollama pull qwen3-coder</span>
</li>
<li>
Configure your{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">.env</span>:
</li>
</ol>
<div className="bg-muted rounded-lg p-4 font-mono text-sm mt-3">
<pre><code>{`ANTHROPIC_BASE_URL=http://localhost:11434
ANTHROPIC_AUTH_TOKEN=ollama
API_TIMEOUT_MS=3000000
ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3-coder`}</code></pre>
</div>
<p className="text-muted-foreground mt-3">
<strong className="text-foreground">Recommended models:</strong>{' '}
<Badge variant="secondary">qwen3-coder</Badge>{' '}
<Badge variant="secondary">deepseek-coder-v2</Badge>{' '}
<Badge variant="secondary">codellama</Badge>
</p>
<p className="text-muted-foreground mt-2">
<strong className="text-foreground">Limitations:</strong> Smaller context windows than Claude
(model-dependent), extended context beta disabled (not supported by Ollama), and performance
depends on local hardware (GPU recommended).
</p>
{/* Environment Variables */}
<h3 id="env-variables" className="text-lg font-semibold text-foreground mt-8 mb-3">
Environment Variables
</h3>
<p className="text-muted-foreground mb-3">
Key environment variables for configuring AutoCoder:
</p>
<table className="w-full text-sm mt-3">
<thead>
<tr className="bg-muted/50">
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Variable
</th>
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Description
</th>
</tr>
</thead>
<tbody className="text-muted-foreground">
{ENV_VARS.map((v) => (
<tr key={v.name}>
<td className="border border-border px-3 py-2">
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">{v.name}</span>
</td>
<td className="border border-border px-3 py-2">{v.description}</td>
</tr>
))}
</tbody>
</table>
{/* CLI Arguments */}
<h3 id="cli-arguments" className="text-lg font-semibold text-foreground mt-8 mb-3">
CLI Arguments
</h3>
<p className="text-muted-foreground mb-3">
Command-line arguments for{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
autonomous_agent_demo.py
</span>
:
</p>
<table className="w-full text-sm mt-3">
<thead>
<tr className="bg-muted/50">
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Argument
</th>
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Description
</th>
</tr>
</thead>
<tbody className="text-muted-foreground">
{CLI_ARGS.map((arg) => (
<tr key={arg.name}>
<td className="border border-border px-3 py-2">
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">{arg.name}</span>
</td>
<td className="border border-border px-3 py-2">{arg.description}</td>
</tr>
))}
</tbody>
</table>
{/* Webhook Support */}
<h3 id="webhooks" className="text-lg font-semibold text-foreground mt-8 mb-3">
Webhook Support
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>AutoCoder can send webhook notifications on feature completion</li>
<li>Compatible with N8N and similar automation tools</li>
<li>Configure the webhook URL in project settings</li>
<li>
Payload includes: feature name, status, and project info
</li>
</ul>
{/* Project Registry */}
<h3 id="project-registry" className="text-lg font-semibold text-foreground mt-8 mb-3">
Project Registry
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
All projects are registered in{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">~/.autocoder/registry.db</span>{' '}
(SQLite)
</li>
<li>Maps project names to filesystem paths</li>
<li>Uses POSIX path format (forward slashes) for cross-platform compatibility</li>
<li>SQLAlchemy ORM with SQLite&apos;s built-in transaction handling</li>
</ul>
</div>
)
}

View File

@@ -1,280 +0,0 @@
/**
* AgentSystem Documentation Section
*
* Covers the orchestrator (Maestro), coding agents, testing agents,
* agent lifecycle, concurrency control, mission control dashboard,
* agent mascots and states, viewing logs, and process limits.
*/
import { Badge } from '@/components/ui/badge'
export function AgentSystem() {
return (
<div>
{/* Maestro: The Orchestrator */}
<h3 id="maestro-orchestrator" className="text-lg font-semibold text-foreground mt-8 mb-3">
Maestro: The Orchestrator
</h3>
<p className="text-muted-foreground mb-3">
Maestro is the central orchestrator that coordinates all agents. It acts as the conductor,
ensuring features are implemented efficiently and in the correct order.
</p>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Manages the full lifecycle of coding and testing agents</li>
<li>Schedules which features to work on based on dependencies and priority</li>
<li>Monitors agent health and restarts crashed agents automatically</li>
<li>Reports status to the UI in real time via WebSocket</li>
</ul>
{/* Coding Agents */}
<h3 id="coding-agents" className="text-lg font-semibold text-foreground mt-8 mb-3">
Coding Agents
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Implement features one at a time, or in batches of 1&ndash;3</li>
<li>
Claim features atomically via the{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
feature_claim_and_get
</span>{' '}
MCP tool &mdash; no two agents work on the same feature
</li>
<li>Run in isolated environments with their own browser context</li>
<li>
Use the Claude Code SDK with project-specific tools and{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">CLAUDE.md</span>
</li>
</ul>
{/* Testing Agents */}
<h3 id="testing-agents" className="text-lg font-semibold text-foreground mt-8 mb-3">
Testing Agents
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Run regression tests after features are implemented</li>
<li>Verify that new code does not break existing features</li>
<li>Configurable ratio: 0&ndash;3 testing agents per coding agent</li>
<li>Can batch-test multiple features per session (1&ndash;5)</li>
</ul>
{/* Agent Lifecycle */}
<h3 id="agent-lifecycle" className="text-lg font-semibold text-foreground mt-8 mb-3">
Agent Lifecycle
</h3>
<p className="text-muted-foreground mb-3">
Agents are controlled through the UI or CLI. The lifecycle states are:
</p>
<table className="w-full text-sm mt-3">
<thead>
<tr className="bg-muted/50">
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Action
</th>
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Behavior
</th>
</tr>
</thead>
<tbody className="text-muted-foreground">
<tr>
<td className="border border-border px-3 py-2 font-medium">Start</td>
<td className="border border-border px-3 py-2">
Click the Play button or run the CLI command
</td>
</tr>
<tr>
<td className="border border-border px-3 py-2 font-medium">Stop</td>
<td className="border border-border px-3 py-2">
Gracefully terminates all running agents
</td>
</tr>
<tr>
<td className="border border-border px-3 py-2 font-medium">Pause</td>
<td className="border border-border px-3 py-2">
Temporarily halts work (agents finish their current task first)
</td>
</tr>
<tr>
<td className="border border-border px-3 py-2 font-medium">Resume</td>
<td className="border border-border px-3 py-2">
Continues from where the agents were paused
</td>
</tr>
</tbody>
</table>
<p className="text-muted-foreground mt-3">
Agents auto-continue between sessions with a 3-second delay, so they keep working until
all features are complete or they are explicitly stopped.
</p>
{/* Concurrency Control */}
<h3 id="concurrency" className="text-lg font-semibold text-foreground mt-8 mb-3">
Concurrency Control
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
A slider in the agent control bar sets the number of concurrent coding agents
(1&ndash;5)
</li>
<li>
More agents means faster progress, but also higher API usage
</li>
<li>Each agent runs as an independent subprocess</li>
<li>
Feature claiming is atomic &mdash; no two agents will ever work on the same feature
simultaneously
</li>
</ul>
{/* Agent Mission Control */}
<h3 id="mission-control" className="text-lg font-semibold text-foreground mt-8 mb-3">
Agent Mission Control
</h3>
<p className="text-muted-foreground mb-3">
The Mission Control dashboard provides a real-time overview of all active agents:
</p>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Active agent cards with mascot icons and current status</li>
<li>The feature each agent is currently working on</li>
<li>Agent state indicators (thinking, working, testing, etc.)</li>
<li>Orchestrator status and a recent activity feed</li>
</ul>
{/* Agent Mascots & States */}
<h3 id="agent-mascots" className="text-lg font-semibold text-foreground mt-8 mb-3">
Agent Mascots &amp; States
</h3>
<p className="text-muted-foreground mb-3">
Each agent is assigned a unique mascot for easy identification:{' '}
<strong className="text-foreground">Spark</strong>,{' '}
<strong className="text-foreground">Fizz</strong>,{' '}
<strong className="text-foreground">Octo</strong>,{' '}
<strong className="text-foreground">Hoot</strong>,{' '}
<strong className="text-foreground">Buzz</strong>, and more. Agent states include:
</p>
<table className="w-full text-sm mt-3">
<thead>
<tr className="bg-muted/50">
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
State
</th>
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Animation
</th>
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Description
</th>
</tr>
</thead>
<tbody className="text-muted-foreground">
<tr>
<td className="border border-border px-3 py-2">
<Badge variant="secondary">Thinking</Badge>
</td>
<td className="border border-border px-3 py-2">Bouncing</td>
<td className="border border-border px-3 py-2">Agent is planning its approach</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">
<Badge variant="secondary">Working</Badge>
</td>
<td className="border border-border px-3 py-2">Shake</td>
<td className="border border-border px-3 py-2">Actively writing code</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">
<Badge variant="secondary">Testing</Badge>
</td>
<td className="border border-border px-3 py-2">Rotating</td>
<td className="border border-border px-3 py-2">Running tests</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">
<Badge variant="default">Success</Badge>
</td>
<td className="border border-border px-3 py-2">Celebration</td>
<td className="border border-border px-3 py-2">Feature completed</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">
<Badge variant="destructive">Error</Badge>
</td>
<td className="border border-border px-3 py-2">Red shake</td>
<td className="border border-border px-3 py-2">Encountered an issue</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">
<Badge variant="outline">Struggling</Badge>
</td>
<td className="border border-border px-3 py-2">Concerned expression</td>
<td className="border border-border px-3 py-2">Multiple consecutive failures</td>
</tr>
</tbody>
</table>
{/* Viewing Agent Logs */}
<h3 id="agent-logs" className="text-lg font-semibold text-foreground mt-8 mb-3">
Viewing Agent Logs
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Click any agent card in Mission Control to see its log output</li>
<li>Logs are color-coded by level (info, warning, error)</li>
<li>Output streams in real time via WebSocket</li>
<li>Each agent&apos;s logs are isolated and filterable</li>
</ul>
{/* Process Limits */}
<h3 id="process-limits" className="text-lg font-semibold text-foreground mt-8 mb-3">
Process Limits
</h3>
<p className="text-muted-foreground mb-3">
The orchestrator enforces strict bounds on concurrent processes to prevent resource
exhaustion:
</p>
<table className="w-full text-sm mt-3">
<thead>
<tr className="bg-muted/50">
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Limit
</th>
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Value
</th>
</tr>
</thead>
<tbody className="text-muted-foreground">
<tr>
<td className="border border-border px-3 py-2">
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
MAX_PARALLEL_AGENTS
</span>
</td>
<td className="border border-border px-3 py-2">5 (maximum concurrent coding agents)</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
MAX_TOTAL_AGENTS
</span>
</td>
<td className="border border-border px-3 py-2">
10 (hard limit on coding + testing combined)
</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">Testing agents</td>
<td className="border border-border px-3 py-2">
Capped at the same count as coding agents
</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">Total Python processes</td>
<td className="border border-border px-3 py-2">
Never exceeds 11 (1 orchestrator + 5 coding + 5 testing)
</td>
</tr>
</tbody>
</table>
</div>
)
}

View File

@@ -1,130 +0,0 @@
/**
* AppSpecSetup Documentation Section
*
* Explains what an app spec is, how to create one interactively
* or manually, the initializer agent, and starting after spec creation.
*/
export function AppSpecSetup() {
return (
<div>
{/* What is an App Spec? */}
<h3 id="what-is-app-spec" className="text-lg font-semibold text-foreground mt-8 mb-3">
What is an App Spec?
</h3>
<p className="text-muted-foreground mb-3">
The app spec is an XML document that describes the application to be built. It lives at{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
.autocoder/prompts/app_spec.txt
</span>{' '}
and tells the initializer agent what features to create. The spec defines your app&apos;s name,
description, tech stack, and the features that should be implemented.
</p>
<div className="bg-muted rounded-lg p-4 font-mono text-sm">
<pre><code>{`<app>
<name>My App</name>
<description>A task management app</description>
<features>
<feature>User authentication with login/signup</feature>
<feature>Task CRUD with categories</feature>
</features>
</app>`}</code></pre>
</div>
{/* Creating a Spec with Claude */}
<h3 id="creating-spec-with-claude" className="text-lg font-semibold text-foreground mt-8 mb-3">
Creating a Spec with Claude
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
In the UI, select your project and click{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">Create Spec</span>
</li>
<li>
An interactive chat with Claude helps you define your app &mdash; it asks about
your app&apos;s purpose, features, and tech stack
</li>
<li>The spec is generated and saved automatically</li>
<li>After creation, the initializer agent can be started immediately</li>
</ul>
{/* Writing a Spec Manually */}
<h3 id="writing-spec-manually" className="text-lg font-semibold text-foreground mt-8 mb-3">
Writing a Spec Manually
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
Create{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
.autocoder/prompts/app_spec.txt
</span>{' '}
in your project directory
</li>
<li>
Use XML format with app name, description, tech stack, and a feature list
</li>
<li>
Be specific about each feature &mdash; the initializer creates test cases from these
descriptions
</li>
<li>
Include technical constraints where needed (e.g.,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
&quot;use PostgreSQL&quot;
</span>
,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
&quot;React with TypeScript&quot;
</span>
)
</li>
</ul>
{/* The Initializer Agent */}
<h3 id="initializer-agent" className="text-lg font-semibold text-foreground mt-8 mb-3">
The Initializer Agent
</h3>
<p className="text-muted-foreground mb-3">
The initializer agent is the first agent to run on a new project. It bridges the gap between
your spec and the coding agents that implement features.
</p>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Runs automatically on first agent start when no features exist in the database</li>
<li>Reads the app spec and creates features with descriptions, steps, and priorities</li>
<li>
Sets up feature dependencies (e.g., &quot;auth must be done before user profile&quot;)
</li>
<li>
Creates the feature database at{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
.autocoder/features.db
</span>
</li>
</ul>
{/* Starting After Spec Creation */}
<h3 id="starting-after-spec" className="text-lg font-semibold text-foreground mt-8 mb-3">
Starting After Spec Creation
</h3>
<p className="text-muted-foreground mb-3">
Once your spec is ready, you can kick off the agents:
</p>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
From the UI, click the <strong className="text-foreground">Play</strong> button to start
the agent
</li>
<li>
Or run from the CLI:
</li>
</ul>
<div className="bg-muted rounded-lg p-4 font-mono text-sm mt-3">
<pre><code>python autonomous_agent_demo.py --project-dir your-project</code></pre>
</div>
<p className="text-muted-foreground mt-3">
The initializer runs first to create features, then coding agents take over to implement
them. Progress is shown in real time on the Kanban board.
</p>
</div>
)
}

View File

@@ -1,185 +0,0 @@
/**
* AppearanceThemes Documentation Section
*
* Covers built-in themes with color previews, dark/light mode toggling,
* the theme selector dropdown, and global keyboard shortcuts.
*/
import { Badge } from '@/components/ui/badge'
/** Theme descriptor used to render the preview rows. */
interface ThemePreview {
name: string
description: string
colors: { label: string; hex: string }[]
}
const THEMES: ThemePreview[] = [
{
name: 'Twitter',
description: 'Clean, modern blue design. Primary: blue, Background: white/dark gray.',
colors: [
{ label: 'Background', hex: '#ffffff' },
{ label: 'Primary', hex: '#4a9eff' },
{ label: 'Accent', hex: '#e8f4ff' },
],
},
{
name: 'Claude',
description: "Warm beige/cream tones with orange accents. Inspired by Anthropic's Claude brand.",
colors: [
{ label: 'Background', hex: '#faf6f0' },
{ label: 'Primary', hex: '#c75b2a' },
{ label: 'Accent', hex: '#f5ede4' },
],
},
{
name: 'Neo Brutalism',
description: 'Bold colors, hard shadows, no border radius. High contrast, expressive design.',
colors: [
{ label: 'Background', hex: '#ffffff' },
{ label: 'Primary', hex: '#ff4d00' },
{ label: 'Accent', hex: '#ffeb00' },
],
},
{
name: 'Retro Arcade',
description: 'Vibrant pink and teal with pixel-art inspired styling.',
colors: [
{ label: 'Background', hex: '#f0e6d3' },
{ label: 'Primary', hex: '#e8457c' },
{ label: 'Accent', hex: '#4eb8a5' },
],
},
{
name: 'Aurora',
description: 'Deep violet and luminous teal, inspired by the northern lights.',
colors: [
{ label: 'Background', hex: '#faf8ff' },
{ label: 'Primary', hex: '#8b5cf6' },
{ label: 'Accent', hex: '#2dd4bf' },
],
},
{
name: 'Business',
description: 'Professional deep navy and gray monochrome palette for corporate use.',
colors: [
{ label: 'Background', hex: '#eaecef' },
{ label: 'Primary', hex: '#000e4e' },
{ label: 'Accent', hex: '#6b7280' },
],
},
]
/** Keyboard shortcut descriptor for the shortcuts table. */
interface Shortcut {
key: string
action: string
}
const SHORTCUTS: Shortcut[] = [
{ key: '?', action: 'Show keyboard shortcuts help' },
{ key: 'D', action: 'Toggle debug panel' },
{ key: 'T', action: 'Toggle terminal' },
{ key: 'G', action: 'Toggle Kanban/Graph view' },
{ key: 'N', action: 'Add new feature' },
{ key: 'E', action: 'Expand project with AI' },
{ key: 'A', action: 'Toggle AI assistant' },
{ key: ',', action: 'Open settings' },
{ key: 'R', action: 'Reset project' },
{ key: 'Escape', action: 'Close current modal' },
]
export function AppearanceThemes() {
return (
<div>
{/* Themes Overview */}
<h3 id="themes-overview" className="text-lg font-semibold text-foreground mt-8 mb-3">
Themes Overview
</h3>
<p className="text-muted-foreground mb-4">
AutoCoder comes with 6 built-in themes. Each theme provides a complete visual identity including
colors, accents, and dark mode variants.
</p>
<div className="space-y-4">
{THEMES.map((theme) => (
<div key={theme.name} className="flex items-start gap-4">
{/* Color swatches */}
<div className="flex gap-1.5 shrink-0 mt-1">
{theme.colors.map((color) => (
<div
key={color.label}
title={`${color.label}: ${color.hex}`}
className="w-6 h-6 rounded border border-border"
style={{ backgroundColor: color.hex }}
/>
))}
</div>
{/* Description */}
<div>
<strong className="text-foreground">{theme.name}</strong>
{theme.name === 'Twitter' && (
<>
{' '}
<Badge variant="secondary">Default</Badge>
</>
)}
<span className="text-muted-foreground"> &mdash; {theme.description}</span>
</div>
</div>
))}
</div>
{/* Dark & Light Mode */}
<h3 id="dark-light-mode" className="text-lg font-semibold text-foreground mt-8 mb-3">
Dark &amp; Light Mode
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Toggle with the sun/moon icon in the header</li>
<li>All 6 themes have dedicated dark mode variants</li>
<li>
Preference is saved in browser{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">localStorage</span>
</li>
<li>Dark mode affects all UI elements including the docs page</li>
</ul>
{/* Theme Selector */}
<h3 id="theme-selector" className="text-lg font-semibold text-foreground mt-8 mb-3">
Theme Selector
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Hover over the palette icon in the header to open the theme dropdown</li>
<li>Preview themes by hovering over each option (live preview)</li>
<li>Click to select &mdash; the change is applied instantly</li>
<li>Theme preference persists across sessions</li>
</ul>
{/* Keyboard Shortcuts */}
<h3 id="keyboard-shortcuts" className="text-lg font-semibold text-foreground mt-8 mb-3">
Keyboard Shortcuts
</h3>
<p className="text-muted-foreground mb-3">
Press <Badge variant="secondary">?</Badge> anywhere in the UI to see the shortcuts help overlay.
</p>
<table className="w-full text-sm mt-3">
<thead>
<tr className="bg-muted/50">
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">Key</th>
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">Action</th>
</tr>
</thead>
<tbody className="text-muted-foreground">
{SHORTCUTS.map((shortcut) => (
<tr key={shortcut.key}>
<td className="border border-border px-3 py-2">
<Badge variant="secondary">{shortcut.key}</Badge>
</td>
<td className="border border-border px-3 py-2">{shortcut.action}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}

View File

@@ -1,104 +0,0 @@
/**
* DeveloperTools Documentation Section
*
* Covers the debug panel, agent logs tab, dev server logs,
* terminal, dev server control, and per-agent logs.
*/
import { Badge } from '@/components/ui/badge'
export function DeveloperTools() {
return (
<div>
{/* Debug Panel */}
<h3 id="debug-panel" className="text-lg font-semibold text-foreground mt-8 mb-3">
Debug Panel
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
Press <Badge variant="secondary">D</Badge> to toggle the debug panel at the bottom of the screen
</li>
<li>Resizable by dragging the top edge</li>
<li>
Three tabs: <strong className="text-foreground">Agent Logs</strong>,{' '}
<strong className="text-foreground">Dev Server Logs</strong>, and{' '}
<strong className="text-foreground">Terminal</strong>
</li>
<li>Shows real-time output from agents and dev server</li>
</ul>
{/* Agent Logs Tab */}
<h3 id="agent-logs-tab" className="text-lg font-semibold text-foreground mt-8 mb-3">
Agent Logs Tab
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
Color-coded log levels:{' '}
<span className="text-[var(--color-log-error)] font-medium">Error</span>,{' '}
<span className="text-[var(--color-log-warning)] font-medium">Warning</span>,{' '}
<span className="text-[var(--color-log-info)] font-medium">Info</span>,{' '}
<span className="text-[var(--color-log-debug)] font-medium">Debug</span>,{' '}
<span className="text-[var(--color-log-success)] font-medium">Success</span>
</li>
<li>Timestamps on each log entry</li>
<li>Auto-scrolls to latest entry</li>
<li>Clear button to reset log view</li>
</ul>
{/* Dev Server Logs Tab */}
<h3 id="dev-server-logs" className="text-lg font-semibold text-foreground mt-8 mb-3">
Dev Server Logs Tab
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
Shows stdout/stderr from the project&rsquo;s dev server (e.g.,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">npm run dev</span>)
</li>
<li>Useful for seeing compilation errors, hot reload status</li>
<li>Clear button available</li>
</ul>
{/* Terminal */}
<h3 id="terminal" className="text-lg font-semibold text-foreground mt-8 mb-3">
Terminal
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
Press <Badge variant="secondary">T</Badge> to open terminal (opens debug panel on the terminal tab)
</li>
<li>Full xterm.js terminal emulator with WebSocket backend</li>
<li>Multi-tab support: create multiple terminal sessions</li>
<li>Rename tabs by double-clicking the tab title</li>
<li>Each tab runs an independent PTY (pseudo-terminal) session</li>
<li>Supports standard terminal features: colors, cursor movement, history</li>
</ul>
{/* Dev Server Control */}
<h3 id="dev-server-control" className="text-lg font-semibold text-foreground mt-8 mb-3">
Dev Server Control
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Start/stop button in the header bar</li>
<li>
Auto-detects project type (Next.js, Vite, CRA, etc.) and runs the appropriate dev command
</li>
<li>Shows the dev server URL when running</li>
<li>Automatic crash detection and restart option</li>
<li>Dev server output piped to the Dev Server Logs tab</li>
</ul>
{/* Per-Agent Logs */}
<h3 id="per-agent-logs" className="text-lg font-semibold text-foreground mt-8 mb-3">
Per-Agent Logs
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>In Agent Mission Control, click any agent card to see its individual logs</li>
<li>
Logs include: what feature the agent is working on, code changes, test results
</li>
<li>Separate logs for coding agents and testing agents</li>
<li>Real-time streaming &mdash; see agent output as it happens</li>
</ul>
</div>
)
}

View File

@@ -1,157 +0,0 @@
/**
* FAQ Documentation Section
*
* Covers frequently asked questions about project setup, agent behavior,
* customization, troubleshooting, and real-time monitoring.
*/
export function FAQ() {
return (
<div>
{/* Starting a New Project */}
<h3 id="faq-new-project" className="text-lg font-semibold text-foreground mt-8 mb-3">
Starting a New Project
</h3>
<p className="text-muted-foreground italic mb-2">
How do I use AutoCoder on a new project?
</p>
<p className="text-muted-foreground">
From the UI, select &quot;Create New Project&quot; in the project dropdown. Choose a folder and
name. Then create an app spec using the interactive chat or write one manually. Click Start to run
the initializer agent, which creates features from your spec. Coding agents then implement features
automatically.
</p>
{/* Adding to Existing Project */}
<h3 id="faq-existing-project" className="text-lg font-semibold text-foreground mt-8 mb-3">
Adding to Existing Project
</h3>
<p className="text-muted-foreground italic mb-2">
How do I add AutoCoder to an existing project?
</p>
<p className="text-muted-foreground">
Register the project folder through the UI project selector using &quot;Add Existing&quot;.
AutoCoder creates a{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">.autocoder/</span> directory
alongside your existing code. Write an app spec describing what to build (new features), and the
agent works within your existing codebase.
</p>
{/* Agent Crashes */}
<h3 id="faq-agent-crash" className="text-lg font-semibold text-foreground mt-8 mb-3">
Agent Crashes
</h3>
<p className="text-muted-foreground italic mb-2">
What happens if an agent crashes?
</p>
<p className="text-muted-foreground">
The orchestrator (Maestro) automatically detects crashed agents and can restart them. Features
claimed by a crashed agent are released back to the pending queue. Scheduled runs use exponential
backoff with up to 3 retries. Check the agent logs in the debug panel for crash details.
</p>
{/* Custom Bash Commands */}
<h3 id="faq-custom-commands" className="text-lg font-semibold text-foreground mt-8 mb-3">
Custom Bash Commands
</h3>
<p className="text-muted-foreground italic mb-2">
How do I customize which bash commands the agent can use?
</p>
<p className="text-muted-foreground">
Create{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
.autocoder/allowed_commands.yaml
</span>{' '}
in your project with a list of allowed commands. Supports exact names, wildcards (e.g.,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">swift*</span>), and local
scripts. See the Security section for full details on the command hierarchy.
</p>
{/* Blocked Features */}
<h3 id="faq-blocked-features" className="text-lg font-semibold text-foreground mt-8 mb-3">
Blocked Features
</h3>
<p className="text-muted-foreground italic mb-2">
Why are my features stuck in &quot;blocked&quot; status?
</p>
<p className="text-muted-foreground">
Features with unmet dependencies show as blocked. Check the Dependency Graph view (press{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">G</span>) to see which
features are waiting on others. A feature can only start when all its dependencies are marked as
&quot;passing&quot;. Remove or reorder dependencies if needed.
</p>
{/* Running in Parallel */}
<h3 id="faq-parallel" className="text-lg font-semibold text-foreground mt-8 mb-3">
Running in Parallel
</h3>
<p className="text-muted-foreground italic mb-2">
How do I run multiple agents in parallel?
</p>
<p className="text-muted-foreground">
Use the concurrency slider in the agent control bar (1&ndash;5 agents) or pass{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
--parallel --max-concurrency N
</span>{' '}
on the CLI. Each agent claims features atomically, so there is no conflict. More agents means
faster progress but higher API cost.
</p>
{/* Using Local Models */}
<h3 id="faq-local-model" className="text-lg font-semibold text-foreground mt-8 mb-3">
Using Local Models
</h3>
<p className="text-muted-foreground italic mb-2">
Can I use a local model instead of the Claude API?
</p>
<p className="text-muted-foreground">
Yes, via Ollama v0.14.0+. Install Ollama, pull a coding model (e.g.,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">qwen3-coder</span>), and
configure your{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">.env</span> to point to
localhost. See the Advanced Configuration section for full setup instructions.
</p>
{/* Resetting a Project */}
<h3 id="faq-reset" className="text-lg font-semibold text-foreground mt-8 mb-3">
Resetting a Project
</h3>
<p className="text-muted-foreground italic mb-2">
How do I reset a project and start over?
</p>
<p className="text-muted-foreground">
Press <span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">R</span> (when agents
are stopped) to open the Reset modal. Choose between: &quot;Reset Features&quot; (clears the
feature database, keeps the spec) or &quot;Full Reset&quot; (removes the spec too, starts fresh).
After a full reset, you will be prompted to create a new spec.
</p>
{/* Coding vs Testing Agents */}
<h3 id="faq-agent-types" className="text-lg font-semibold text-foreground mt-8 mb-3">
Coding vs Testing Agents
</h3>
<p className="text-muted-foreground italic mb-2">
What&apos;s the difference between coding and testing agents?
</p>
<p className="text-muted-foreground">
Coding agents implement features &mdash; they write code, create files, and run feature-specific
tests. Testing agents run regression tests across completed features to ensure new code does not
break existing functionality. Configure the testing agent ratio (0&ndash;3) in settings.
</p>
{/* Monitoring in Real Time */}
<h3 id="faq-real-time" className="text-lg font-semibold text-foreground mt-8 mb-3">
Monitoring in Real Time
</h3>
<p className="text-muted-foreground italic mb-2">
How do I view what an agent is doing in real time?
</p>
<p className="text-muted-foreground">
Multiple ways: (1) Watch the Kanban board for feature status changes. (2) Open the debug panel
(<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">D</span> key) for live
agent logs. (3) Click agent cards in Mission Control for per-agent logs. (4) The progress bar
updates in real time via WebSocket.
</p>
</div>
)
}

View File

@@ -1,182 +0,0 @@
/**
* FeaturesKanban Documentation Section
*
* Covers the Kanban board, feature cards, dependency graph view,
* adding/editing features, dependencies, expanding with AI,
* and priority ordering.
*/
import { Badge } from '@/components/ui/badge'
export function FeaturesKanban() {
return (
<div>
{/* Kanban Board Overview */}
<h3 id="kanban-overview" className="text-lg font-semibold text-foreground mt-8 mb-3">
Kanban Board Overview
</h3>
<p className="text-muted-foreground mb-3">
The main view organizes features into three columns representing their current status:
</p>
<table className="w-full text-sm mt-3 mb-4">
<thead>
<tr className="bg-muted/50">
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Column
</th>
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Color
</th>
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Meaning
</th>
</tr>
</thead>
<tbody className="text-muted-foreground">
<tr>
<td className="border border-border px-3 py-2 font-medium">Pending</td>
<td className="border border-border px-3 py-2">
<Badge variant="outline" className="border-yellow-500 text-yellow-600">Yellow</Badge>
</td>
<td className="border border-border px-3 py-2">Waiting to be picked up</td>
</tr>
<tr>
<td className="border border-border px-3 py-2 font-medium">In Progress</td>
<td className="border border-border px-3 py-2">
<Badge variant="outline" className="border-cyan-500 text-cyan-600">Cyan</Badge>
</td>
<td className="border border-border px-3 py-2">An agent is actively working on it</td>
</tr>
<tr>
<td className="border border-border px-3 py-2 font-medium">Done</td>
<td className="border border-border px-3 py-2">
<Badge variant="outline" className="border-green-500 text-green-600">Green</Badge>
</td>
<td className="border border-border px-3 py-2">Implemented and passing</td>
</tr>
</tbody>
</table>
<p className="text-muted-foreground">
Each feature appears as a card showing its name, priority, and category. The board updates
in real time as agents work.
</p>
{/* Feature Cards */}
<h3 id="feature-cards" className="text-lg font-semibold text-foreground mt-8 mb-3">
Feature Cards
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
Each card displays a priority badge (<Badge variant="secondary">P1</Badge> through{' '}
<Badge variant="secondary">P5</Badge>), a category tag, and the feature name
</li>
<li>Status icons indicate the current state of the feature</li>
<li>Click a card to open the detail modal with the full description and test steps</li>
<li>
Cards in the &quot;In Progress&quot; column show which agent is currently working on them
</li>
</ul>
{/* Dependency Graph View */}
<h3 id="dependency-graph" className="text-lg font-semibold text-foreground mt-8 mb-3">
Dependency Graph View
</h3>
<p className="text-muted-foreground mb-3">
An alternative to the Kanban board that visualizes feature relationships as a directed graph.
</p>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
Press <Badge variant="secondary">G</Badge> to toggle between Kanban and Graph view
</li>
<li>Uses the dagre layout engine for automatic node positioning</li>
<li>
Nodes are colored by status &mdash; pending, in-progress, and done each have
distinct colors
</li>
<li>Arrows show dependency relationships between features</li>
<li>Click any node to open the feature detail modal</li>
<li>Supports both horizontal and vertical layout orientations</li>
</ul>
{/* Adding Features */}
<h3 id="adding-features" className="text-lg font-semibold text-foreground mt-8 mb-3">
Adding Features
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
Press <Badge variant="secondary">N</Badge> to open the Add Feature form
</li>
<li>Fill in: name, description, category, and priority</li>
<li>Optionally define steps (test criteria the agent must pass to complete the feature)</li>
<li>New features are added to the Pending column immediately</li>
</ul>
{/* Editing & Deleting Features */}
<h3 id="editing-features" className="text-lg font-semibold text-foreground mt-8 mb-3">
Editing &amp; Deleting Features
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Click a feature card to open the detail modal</li>
<li>
Click <strong className="text-foreground">Edit</strong> to modify the name, description,
category, priority, or steps
</li>
<li>
<strong className="text-foreground">Delete</strong> removes the feature permanently
</li>
<li>
<strong className="text-foreground">Skip</strong> moves a feature to the end of the queue
without deleting it
</li>
</ul>
{/* Feature Dependencies */}
<h3 id="feature-dependencies" className="text-lg font-semibold text-foreground mt-8 mb-3">
Feature Dependencies
</h3>
<p className="text-muted-foreground mb-3">
Features can declare dependencies on other features, ensuring they are implemented in the
correct order.
</p>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Set dependencies in the feature edit modal</li>
<li>
Cycle detection prevents circular dependencies (uses Kahn&apos;s algorithm combined
with DFS)
</li>
<li>
Blocked features display a lock icon and cannot be claimed by agents until their
dependencies are met
</li>
<li>The Dependency Graph view makes these relationships easy to visualize</li>
</ul>
{/* Expanding Project with AI */}
<h3 id="expanding-with-ai" className="text-lg font-semibold text-foreground mt-8 mb-3">
Expanding Project with AI
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
Press <Badge variant="secondary">E</Badge> to open the Expand Project modal
</li>
<li>Chat with Claude to describe the new features you want to add</li>
<li>Supports image attachments for UI mockups or design references</li>
<li>Claude creates properly structured features with appropriate dependencies</li>
<li>New features appear on the board immediately after creation</li>
</ul>
{/* Priority & Ordering */}
<h3 id="feature-priority" className="text-lg font-semibold text-foreground mt-8 mb-3">
Priority &amp; Ordering
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
Features are ordered by priority: <Badge variant="secondary">P1</Badge> is the highest
and <Badge variant="secondary">P5</Badge> is the lowest
</li>
<li>Within the same priority level, features are ordered by creation time</li>
<li>Agents always pick up the highest-priority ready feature first</li>
</ul>
</div>
)
}

View File

@@ -1,134 +0,0 @@
/**
* GettingStarted Documentation Section
*
* Covers what AutoCoder is, quick start commands,
* creating and adding projects, and system requirements.
*/
import { Badge } from '@/components/ui/badge'
export function GettingStarted() {
return (
<div>
{/* What is AutoCoder? */}
<h3 id="what-is-autocoder" className="text-lg font-semibold text-foreground mt-8 mb-3">
What is AutoCoder?
</h3>
<p className="text-muted-foreground mb-4">
AutoCoder is an autonomous coding agent system that builds complete applications over multiple
sessions using a two-agent pattern:
</p>
<ol className="list-decimal space-y-2 ml-4 text-muted-foreground">
<li>
<strong className="text-foreground">Initializer Agent</strong> &mdash; reads your app spec
and creates features in a SQLite database
</li>
<li>
<strong className="text-foreground">Coding Agent</strong> &mdash; implements features one by
one, marking each as passing when complete
</li>
</ol>
<p className="text-muted-foreground mt-4">
It comes with a React-based UI for monitoring progress, managing features, and controlling agents
in real time.
</p>
{/* Quick Start */}
<h3 id="quick-start" className="text-lg font-semibold text-foreground mt-8 mb-3">
Quick Start
</h3>
<p className="text-muted-foreground mb-3">
Launch AutoCoder with a single command. The CLI menu lets you create or select a project,
while the Web UI provides a full dashboard experience.
</p>
<div className="bg-muted rounded-lg p-4 font-mono text-sm">
<pre><code>{`# Windows
start.bat # CLI menu
start_ui.bat # Web UI
# macOS/Linux
./start.sh # CLI menu
./start_ui.sh # Web UI`}</code></pre>
</div>
{/* Creating a New Project */}
<h3 id="creating-a-project" className="text-lg font-semibold text-foreground mt-8 mb-3">
Creating a New Project
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
From the UI, click the project dropdown and select{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">Create New Project</span>
</li>
<li>Enter a name and select or browse to a folder for the project</li>
<li>
Create an app spec interactively with Claude, or write one manually in XML format
</li>
<li>
The initializer agent reads your spec and creates features automatically
</li>
</ul>
{/* Adding to an Existing Project */}
<h3 id="existing-project" className="text-lg font-semibold text-foreground mt-8 mb-3">
Adding to an Existing Project
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Register the project folder via the UI project selector</li>
<li>
AutoCoder creates a{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">.autocoder/</span>{' '}
directory inside your project
</li>
<li>
Existing code is preserved &mdash; AutoCoder adds its configuration alongside it
</li>
<li>Write or generate an app spec describing what to build</li>
</ul>
{/* System Requirements */}
<h3 id="system-requirements" className="text-lg font-semibold text-foreground mt-8 mb-3">
System Requirements
</h3>
<table className="w-full text-sm mt-3">
<thead>
<tr className="bg-muted/50">
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Requirement
</th>
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Details
</th>
</tr>
</thead>
<tbody className="text-muted-foreground">
<tr>
<td className="border border-border px-3 py-2">Python</td>
<td className="border border-border px-3 py-2">
<Badge variant="secondary">3.11+</Badge>
</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">Node.js</td>
<td className="border border-border px-3 py-2">
<Badge variant="secondary">20+</Badge>{' '}
<span className="text-xs">(for UI development)</span>
</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">Claude Code CLI</td>
<td className="border border-border px-3 py-2">
Required for running agents
</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">Operating System</td>
<td className="border border-border px-3 py-2">
Windows, macOS, or Linux
</td>
</tr>
</tbody>
</table>
</div>
)
}

View File

@@ -1,162 +0,0 @@
/**
* ProjectStructure Documentation Section
*
* Covers the .autocoder/ directory layout, features database,
* prompts directory, allowed commands, CLAUDE.md convention,
* legacy migration, and Claude inheritance.
*/
export function ProjectStructure() {
return (
<div>
{/* .autocoder/ Directory Layout */}
<h3 id="autocoder-directory" className="text-lg font-semibold text-foreground mt-8 mb-3">
.autocoder/ Directory Layout
</h3>
<p className="text-muted-foreground mb-3">
Every AutoCoder project stores its configuration and runtime files in a{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">.autocoder/</span>{' '}
directory at the project root.
</p>
<div className="bg-muted rounded-lg p-4 font-mono text-sm">
<pre><code>{`your-project/
\u251C\u2500\u2500 .autocoder/
\u2502 \u251C\u2500\u2500 features.db # SQLite feature database
\u2502 \u251C\u2500\u2500 .agent.lock # Lock file (prevents multiple instances)
\u2502 \u251C\u2500\u2500 .gitignore # Ignores runtime files
\u2502 \u251C\u2500\u2500 allowed_commands.yaml # Per-project bash command allowlist
\u2502 \u2514\u2500\u2500 prompts/
\u2502 \u251C\u2500\u2500 app_spec.txt # Application specification (XML)
\u2502 \u251C\u2500\u2500 initializer_prompt.md # First session prompt
\u2502 \u2514\u2500\u2500 coding_prompt.md # Continuation session prompt
\u251C\u2500\u2500 CLAUDE.md # Claude Code convention file
\u2514\u2500\u2500 app_spec.txt # Root copy for template compatibility`}</code></pre>
</div>
{/* Features Database */}
<h3 id="features-db" className="text-lg font-semibold text-foreground mt-8 mb-3">
Features Database
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
SQLite database managed by SQLAlchemy, stored at{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
.autocoder/features.db
</span>
</li>
<li>
Each feature record includes: id, priority, category, name, description, steps, status
(<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">pending</span>,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">in_progress</span>,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">passing</span>,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">failing</span>),
and dependencies
</li>
<li>Agents interact with features through MCP server tools, not direct database access</li>
<li>Viewable in the UI via the Kanban board or the Dependency Graph view</li>
</ul>
{/* Prompts Directory */}
<h3 id="prompts-directory" className="text-lg font-semibold text-foreground mt-8 mb-3">
Prompts Directory
</h3>
<p className="text-muted-foreground mb-3">
Prompts control how agents behave during each session:
</p>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">app_spec.txt</span>{' '}
&mdash; your application specification in XML format
</li>
<li>
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
initializer_prompt.md
</span>{' '}
&mdash; prompt for the initializer agent (creates features from the spec)
</li>
<li>
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
coding_prompt.md
</span>{' '}
&mdash; prompt for coding agents (implements features)
</li>
</ul>
<p className="text-muted-foreground mt-3">
These can be customized per project. If not present, defaults from{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
.claude/templates/
</span>{' '}
are used as a fallback.
</p>
{/* Allowed Commands Config */}
<h3 id="allowed-commands-yaml" className="text-lg font-semibold text-foreground mt-8 mb-3">
Allowed Commands Config
</h3>
<p className="text-muted-foreground mb-3">
The optional{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
.autocoder/allowed_commands.yaml
</span>{' '}
file lets you grant project-specific bash commands to the agent. This is useful when your
project requires tools beyond the default allowlist (e.g., language-specific compilers or
custom build scripts).
</p>
<p className="text-muted-foreground">
See the <strong className="text-foreground">Security</strong> section for full details on
the command hierarchy and how project-level commands interact with global and organization
policies.
</p>
{/* CLAUDE.md Convention */}
<h3 id="claude-md" className="text-lg font-semibold text-foreground mt-8 mb-3">
CLAUDE.md Convention
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">CLAUDE.md</span>{' '}
lives at the project root, as required by the Claude Code SDK
</li>
<li>
Contains project-specific instructions that the agent follows during every coding session
</li>
<li>
Automatically inherited by all agents working on the project &mdash; no additional
configuration needed
</li>
</ul>
{/* Legacy Layout Migration */}
<h3 id="legacy-migration" className="text-lg font-semibold text-foreground mt-8 mb-3">
Legacy Layout Migration
</h3>
<p className="text-muted-foreground mb-3">
Older projects stored configuration files directly at the project root (e.g.,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">features.db</span>,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">prompts/</span>).
</p>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
On the next agent start, these files are automatically migrated into{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">.autocoder/</span>
</li>
<li>Dual-path resolution ensures both old and new layouts work transparently</li>
<li>No manual migration is needed &mdash; it happens seamlessly</li>
</ul>
{/* Claude Inheritance */}
<h3 id="claude-inheritance" className="text-lg font-semibold text-foreground mt-8 mb-3">
Claude Inheritance
</h3>
<p className="text-muted-foreground mb-3">
Agents inherit all MCP servers, tools, skills, custom commands, and{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">CLAUDE.md</span>{' '}
from the target project folder.
</p>
<div className="border-l-4 border-primary pl-4 italic text-muted-foreground">
If your project has its own MCP servers or Claude commands, the coding agent can use them.
The agent essentially runs as if Claude Code was opened in your project directory.
</div>
</div>
)
}

View File

@@ -1,102 +0,0 @@
/**
* Scheduling Documentation Section
*
* Covers schedule creation, per-schedule settings,
* overrides, and crash recovery with exponential backoff.
*/
import { Badge } from '@/components/ui/badge'
export function Scheduling() {
return (
<div>
{/* What Scheduling Does */}
<h3 id="what-scheduling-does" className="text-lg font-semibold text-foreground mt-8 mb-3">
What Scheduling Does
</h3>
<p className="text-muted-foreground mb-4">
Scheduling automates agent runs at specific times. Set up a schedule and AutoCoder will automatically
start agents on your project &mdash; useful for overnight builds, periodic maintenance, or continuous
development.
</p>
{/* Creating a Schedule */}
<h3 id="creating-schedule" className="text-lg font-semibold text-foreground mt-8 mb-3">
Creating a Schedule
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Click the clock icon in the header to open the Schedule modal</li>
<li>Set: start time, duration (how long agents run), days of the week</li>
<li>Optionally configure: YOLO mode, concurrency, model selection</li>
<li>Schedule is saved and starts at the next matching time</li>
</ul>
{/* Schedule Settings */}
<h3 id="schedule-settings" className="text-lg font-semibold text-foreground mt-8 mb-3">
Schedule Settings
</h3>
<p className="text-muted-foreground mb-3">
Each schedule can override global settings:
</p>
<table className="w-full text-sm mt-3">
<thead>
<tr className="bg-muted/50">
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">Setting</th>
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">Details</th>
</tr>
</thead>
<tbody className="text-muted-foreground">
<tr>
<td className="border border-border px-3 py-2">YOLO mode</td>
<td className="border border-border px-3 py-2">On/off per schedule</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">Concurrency</td>
<td className="border border-border px-3 py-2">
<Badge variant="secondary">1&ndash;5</Badge> agents
</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">Model tier</td>
<td className="border border-border px-3 py-2">Opus / Sonnet / Haiku</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">Duration</td>
<td className="border border-border px-3 py-2">How long the session runs before auto-stopping</td>
</tr>
</tbody>
</table>
<div className="border-l-4 border-primary pl-4 italic text-muted-foreground mt-4">
All schedule times are in UTC timezone.
</div>
{/* Schedule Overrides */}
<h3 id="schedule-overrides" className="text-lg font-semibold text-foreground mt-8 mb-3">
Schedule Overrides
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Manually skip a scheduled run (one-time override)</li>
<li>Pause a schedule temporarily (resumes on next period)</li>
<li>
View upcoming runs with{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">Running until</span> /{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">Next run</span> indicators
</li>
<li>Override without deleting the schedule</li>
</ul>
{/* Crash Recovery */}
<h3 id="crash-recovery" className="text-lg font-semibold text-foreground mt-8 mb-3">
Crash Recovery
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>If a scheduled agent crashes, it uses exponential backoff for retries</li>
<li>
Maximum <Badge variant="secondary">3</Badge> retry attempts per scheduled run
</li>
<li>Backoff prevents rapid restart loops</li>
<li>Failed runs are logged for troubleshooting</li>
</ul>
</div>
)
}

View File

@@ -1,218 +0,0 @@
/**
* Security Documentation Section
*
* Covers the defense-in-depth security model: command validation layers,
* the hierarchical allowlist/blocklist system, per-project and org-level
* configuration, extra read paths, and filesystem sandboxing.
*/
import { Badge } from '@/components/ui/badge'
export function Security() {
return (
<div>
{/* Command Validation Overview */}
<h3 id="command-validation" className="text-lg font-semibold text-foreground mt-8 mb-3">
Command Validation Overview
</h3>
<p className="text-muted-foreground mb-3">
AutoCoder uses a defense-in-depth approach for security. All three layers must pass before any
command is executed:
</p>
<ol className="list-decimal space-y-2 ml-4 text-muted-foreground">
<li>
<strong className="text-foreground">OS-level sandbox</strong> &mdash; bash commands run inside
a restricted sandbox environment
</li>
<li>
<strong className="text-foreground">Filesystem restriction</strong> &mdash; agents can only
access the project directory (plus configured extra read paths)
</li>
<li>
<strong className="text-foreground">Hierarchical allowlist</strong> &mdash; every bash command
is validated against a multi-level allowlist system
</li>
</ol>
{/* Command Hierarchy */}
<h3 id="command-hierarchy" className="text-lg font-semibold text-foreground mt-8 mb-3">
Command Hierarchy
</h3>
<p className="text-muted-foreground mb-3">
Commands are evaluated against a 5-level hierarchy, from highest to lowest priority:
</p>
<ol className="list-decimal space-y-2 ml-4 text-muted-foreground">
<li>
<strong className="text-foreground">Hardcoded Blocklist</strong>{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">security.py</span>{' '}
&mdash; NEVER allowed, cannot be overridden
</li>
<li>
<strong className="text-foreground">Org Blocklist</strong>{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">~/.autocoder/config.yaml</span>{' '}
&mdash; org-wide blocks, cannot be project-overridden
</li>
<li>
<strong className="text-foreground">Org Allowlist</strong>{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">~/.autocoder/config.yaml</span>{' '}
&mdash; available to all projects
</li>
<li>
<strong className="text-foreground">Global Allowlist</strong>{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">security.py</span>{' '}
&mdash; default commands (npm, git, curl, etc.)
</li>
<li>
<strong className="text-foreground">Project Allowlist</strong>{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
.autocoder/allowed_commands.yaml
</span>{' '}
&mdash; project-specific additions
</li>
</ol>
<blockquote className="border-l-4 border-primary pl-4 italic text-muted-foreground mt-4">
Higher priority levels always win. A command blocked at level 1 or 2 can never be allowed by
lower levels.
</blockquote>
{/* Hardcoded Blocklist */}
<h3 id="hardcoded-blocklist" className="text-lg font-semibold text-foreground mt-8 mb-3">
Hardcoded Blocklist
</h3>
<p className="text-muted-foreground mb-3">
The following commands can <strong className="text-foreground">never</strong> be allowed, regardless
of any configuration. They are hardcoded in{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">security.py</span> and
cannot be overridden:
</p>
<div className="flex flex-wrap gap-2">
{['dd', 'sudo', 'su', 'shutdown', 'reboot', 'poweroff', 'mkfs', 'fdisk', 'mount', 'umount', 'systemctl'].map(
(cmd) => (
<Badge key={cmd} variant="destructive">
{cmd}
</Badge>
),
)}
</div>
{/* Global Allowlist */}
<h3 id="global-allowlist" className="text-lg font-semibold text-foreground mt-8 mb-3">
Global Allowlist
</h3>
<p className="text-muted-foreground mb-3">
Default commands available to all projects out of the box. These are the standard development
commands needed for most projects:
</p>
<div className="flex flex-wrap gap-2">
{['npm', 'npx', 'node', 'git', 'curl', 'python', 'pip', 'cat', 'ls', 'mkdir', 'cp', 'mv', 'rm', 'grep', 'find'].map(
(cmd) => (
<Badge key={cmd} variant="secondary">
{cmd}
</Badge>
),
)}
</div>
{/* Per-Project Allowed Commands */}
<h3 id="project-allowlist" className="text-lg font-semibold text-foreground mt-8 mb-3">
Per-Project Allowed Commands
</h3>
<p className="text-muted-foreground mb-3">
Each project can define additional allowed commands in{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
.autocoder/allowed_commands.yaml
</span>
:
</p>
<div className="bg-muted rounded-lg p-4 font-mono text-sm">
<pre><code>{`# .autocoder/allowed_commands.yaml
version: 1
commands:
# Exact command name
- name: swift
description: Swift compiler
# Wildcard - matches swiftc, swiftlint, swiftformat
- name: swift*
description: All Swift tools (wildcard)
# Local project scripts
- name: ./scripts/build.sh
description: Project build script`}</code></pre>
</div>
<p className="text-muted-foreground mt-3">
<strong className="text-foreground">Pattern matching:</strong> exact match (
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">swift</span>), wildcard (
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">swift*</span> matches swiftc,
swiftlint, etc.), and scripts (
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">./scripts/build.sh</span>).
Limit: 100 commands per project.
</p>
{/* Organization Configuration */}
<h3 id="org-config" className="text-lg font-semibold text-foreground mt-8 mb-3">
Organization Configuration
</h3>
<p className="text-muted-foreground mb-3">
System administrators can set org-wide policies in{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">~/.autocoder/config.yaml</span>:
</p>
<div className="bg-muted rounded-lg p-4 font-mono text-sm">
<pre><code>{`# ~/.autocoder/config.yaml
version: 1
# Commands available to ALL projects
allowed_commands:
- name: jq
description: JSON processor
# Commands blocked across ALL projects (cannot be overridden)
blocked_commands:
- aws # Prevent accidental cloud operations
- kubectl # Block production deployments`}</code></pre>
</div>
<p className="text-muted-foreground mt-3">
Org-level blocked commands cannot be overridden by any project configuration.
</p>
{/* Extra Read Paths */}
<h3 id="extra-read-paths" className="text-lg font-semibold text-foreground mt-8 mb-3">
Extra Read Paths
</h3>
<p className="text-muted-foreground mb-3">
Allow agents to read files from directories outside the project folder via the{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">EXTRA_READ_PATHS</span>{' '}
environment variable:
</p>
<div className="bg-muted rounded-lg p-4 font-mono text-sm">
<pre><code>EXTRA_READ_PATHS=/path/to/docs,/path/to/shared-libs</code></pre>
</div>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground mt-3">
<li>Must be absolute paths and must exist as directories</li>
<li>Only read operations allowed (Read, Glob, Grep &mdash; no Write/Edit)</li>
<li>
Sensitive directories are always blocked:{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">.ssh</span>,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">.aws</span>,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">.gnupg</span>,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">.docker</span>,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">.kube</span>, etc.
</li>
</ul>
{/* Filesystem Sandboxing */}
<h3 id="filesystem-sandboxing" className="text-lg font-semibold text-foreground mt-8 mb-3">
Filesystem Sandboxing
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Agents can only write to the project directory</li>
<li>Read access is limited to the project directory plus configured extra read paths</li>
<li>
Path traversal attacks are prevented via canonicalization (
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">Path.resolve()</span>)
</li>
<li>File operations are validated before execution</li>
</ul>
</div>
)
}

View File

@@ -1,188 +0,0 @@
/**
* SettingsConfig Documentation Section
*
* Covers global settings: opening the modal, YOLO mode, headless browser,
* model selection, regression agents, batch size, concurrency, and persistence.
*/
import { Badge } from '@/components/ui/badge'
export function SettingsConfig() {
return (
<div>
{/* Opening Settings */}
<h3 id="opening-settings" className="text-lg font-semibold text-foreground mt-8 mb-3">
Opening Settings
</h3>
<p className="text-muted-foreground mb-4">
Press the <Badge variant="secondary">,</Badge> (comma) key or click the gear icon in the header bar to
open the Settings modal. Settings are global and apply to all projects.
</p>
{/* YOLO Mode */}
<h3 id="yolo-mode" className="text-lg font-semibold text-foreground mt-8 mb-3">
YOLO Mode
</h3>
<p className="text-muted-foreground mb-3">
YOLO mode is for rapid prototyping &mdash; it skips testing for faster iteration:
</p>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
<strong className="text-foreground">What&rsquo;s skipped:</strong> Regression testing, Playwright MCP
server (browser automation disabled)
</li>
<li>
<strong className="text-foreground">What still runs:</strong> Lint and type-check (to verify code
compiles), Feature MCP server for tracking
</li>
<li>
Toggle via the lightning bolt button in the UI or the{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">--yolo</span> CLI flag
</li>
<li>
<strong className="text-foreground">When to use:</strong> Early prototyping when you want to scaffold
features quickly without verification overhead
</li>
<li>Switch back to standard mode for production-quality development</li>
</ul>
{/* Headless Browser */}
<h3 id="headless-browser" className="text-lg font-semibold text-foreground mt-8 mb-3">
Headless Browser
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>When enabled, Playwright runs without a visible browser window</li>
<li>Saves CPU/GPU resources on machines running multiple agents</li>
<li>Tests still run fully &mdash; just no visible browser UI</li>
<li>Toggle in settings or via the UI button</li>
</ul>
{/* Model Selection */}
<h3 id="model-selection" className="text-lg font-semibold text-foreground mt-8 mb-3">
Model Selection
</h3>
<p className="text-muted-foreground mb-3">
Choose which Claude model tier to use for your agents:
</p>
<table className="w-full text-sm mt-3">
<thead>
<tr className="bg-muted/50">
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">Tier</th>
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Characteristics
</th>
</tr>
</thead>
<tbody className="text-muted-foreground">
<tr>
<td className="border border-border px-3 py-2">
<Badge variant="default">Opus</Badge>
</td>
<td className="border border-border px-3 py-2">Most capable, highest quality</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">
<Badge variant="secondary">Sonnet</Badge>
</td>
<td className="border border-border px-3 py-2">Balanced speed and quality</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">
<Badge variant="outline">Haiku</Badge>
</td>
<td className="border border-border px-3 py-2">Fastest, most economical</td>
</tr>
</tbody>
</table>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground mt-4">
<li>Model can be set globally in settings</li>
<li>Per-schedule model override is also available</li>
<li>
When using Vertex AI, model names use{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">@</span> instead of{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">-</span> (e.g.,{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
claude-opus-4-5@20251101
</span>
)
</li>
</ul>
{/* Regression Agents */}
<h3 id="regression-agents" className="text-lg font-semibold text-foreground mt-8 mb-3">
Regression Agents
</h3>
<p className="text-muted-foreground mb-3">
Controls how many testing agents run alongside coding agents (0&ndash;3):
</p>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
<strong className="text-foreground">0:</strong> No regression testing (like YOLO but coding agents
still test their own feature)
</li>
<li>
<strong className="text-foreground">1:</strong> One testing agent runs in background verifying
completed features
</li>
<li>
<strong className="text-foreground">2&ndash;3:</strong> Multiple testing agents for thorough
verification
</li>
<li>Testing agents batch-test 1&ndash;5 features per session</li>
</ul>
{/* Features per Agent / Batch Size */}
<h3 id="features-per-agent" className="text-lg font-semibold text-foreground mt-8 mb-3">
Features per Agent (Batch Size)
</h3>
<p className="text-muted-foreground mb-3">
Controls how many features each coding agent implements per session (1&ndash;3):
</p>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
<strong className="text-foreground">1:</strong> One feature per session (most focused, lower risk of
conflicts)
</li>
<li>
<strong className="text-foreground">2&ndash;3:</strong> Multiple features per session (more efficient,
fewer session startups)
</li>
<li>
Set via settings UI or the{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">--batch-size</span> CLI flag
</li>
<li>
Can also target specific features:{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">--batch-features 1,2,3</span>
</li>
</ul>
{/* Concurrency */}
<h3 id="concurrency-setting" className="text-lg font-semibold text-foreground mt-8 mb-3">
Concurrency
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>Per-project default concurrency saved in project settings</li>
<li>Override at runtime with the concurrency slider in agent controls</li>
<li>
Range: <Badge variant="secondary">1&ndash;5</Badge> concurrent coding agents
</li>
<li>Higher concurrency = faster progress but more API cost</li>
</ul>
{/* How Settings are Persisted */}
<h3 id="settings-persistence" className="text-lg font-semibold text-foreground mt-8 mb-3">
How Settings are Persisted
</h3>
<ul className="list-disc space-y-2 ml-4 text-muted-foreground">
<li>
Global settings stored in SQLite registry at{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">~/.autocoder/registry.db</span>
</li>
<li>Per-project settings (like default concurrency) stored in the project registry entry</li>
<li>UI settings (theme, dark mode) stored in browser localStorage</li>
<li>Settings survive app restarts and are shared across UI sessions</li>
</ul>
</div>
)
}

View File

@@ -59,7 +59,7 @@ function DialogContent({
<DialogPrimitive.Content <DialogPrimitive.Content
data-slot="dialog-content" data-slot="dialog-content"
className={cn( className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg", "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] max-h-[calc(100vh-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none overflow-y-auto sm:max-w-lg",
className className
)} )}
{...props} {...props}

View File

@@ -1,36 +0,0 @@
import { useState, useEffect, useCallback } from 'react'
export type Route = 'app' | 'docs'
interface HashRouteState {
route: Route
section: string | null
navigate: (hash: string) => void
}
function parseHash(hash: string): { route: Route; section: string | null } {
const cleaned = hash.replace(/^#\/?/, '')
if (cleaned === 'docs' || cleaned.startsWith('docs/')) {
const section = cleaned.slice(5) || null // Remove 'docs/' prefix
return { route: 'docs', section }
}
return { route: 'app', section: null }
}
export function useHashRoute(): HashRouteState {
const [state, setState] = useState(() => parseHash(window.location.hash))
useEffect(() => {
const handleHashChange = () => {
setState(parseHash(window.location.hash))
}
window.addEventListener('hashchange', handleHashChange)
return () => window.removeEventListener('hashchange', handleHashChange)
}, [])
const navigate = useCallback((hash: string) => {
window.location.hash = hash
}, [])
return { ...state, navigate }
}

View File

@@ -52,8 +52,8 @@ export const THEMES: ThemeOption[] = [
} }
] ]
const THEME_STORAGE_KEY = 'autocoder-theme' const THEME_STORAGE_KEY = 'autoforge-theme'
const DARK_MODE_STORAGE_KEY = 'autocoder-dark-mode' const DARK_MODE_STORAGE_KEY = 'autoforge-dark-mode'
function getThemeClass(themeId: ThemeId): string { function getThemeClass(themeId: ThemeId): string {
switch (themeId) { switch (themeId) {

View File

@@ -1,11 +1,8 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useHashRoute } from './hooks/useHashRoute'
import App from './App' import App from './App'
import { DocsPage } from './components/docs/DocsPage'
import './styles/globals.css' import './styles/globals.css'
// Note: Custom theme removed - using shadcn/ui theming instead
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@@ -16,16 +13,10 @@ const queryClient = new QueryClient({
}, },
}) })
function Router() {
const { route } = useHashRoute()
if (route === 'docs') return <DocsPage />
return <App />
}
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Router /> <App />
</QueryClientProvider> </QueryClientProvider>
</StrictMode>, </StrictMode>,
) )

View File

@@ -26,11 +26,23 @@ export default defineConfig({
'vendor-flow': ['@xyflow/react', 'dagre'], 'vendor-flow': ['@xyflow/react', 'dagre'],
// Terminal emulator // Terminal emulator
'vendor-xterm': ['@xterm/xterm', '@xterm/addon-fit', '@xterm/addon-web-links'], 'vendor-xterm': ['@xterm/xterm', '@xterm/addon-fit', '@xterm/addon-web-links'],
// UI components // UI components - Radix UI
'vendor-ui': [ 'vendor-radix': [
'@radix-ui/react-checkbox',
'@radix-ui/react-dialog', '@radix-ui/react-dialog',
'@radix-ui/react-dropdown-menu', '@radix-ui/react-dropdown-menu',
'@radix-ui/react-label',
'@radix-ui/react-separator',
'@radix-ui/react-slot',
'@radix-ui/react-switch',
],
// Icons and utilities
'vendor-utils': [
'lucide-react', 'lucide-react',
'canvas-confetti',
'class-variance-authority',
'clsx',
'tailwind-merge',
], ],
}, },
}, },