mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-02-05 16:33:08 +00:00
Compare commits
19 Commits
b2ab1ecc7a
...
v0.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
## Project-Specific Context
|
||||||
|
|
||||||
For this project (autocoder):
|
For this project (autoforge):
|
||||||
- **Python Backend**: Uses SQLAlchemy, FastAPI, follows patterns in `api/`, `mcp_server/`
|
- **Python Backend**: Uses SQLAlchemy, FastAPI, follows patterns in `api/`, `mcp_server/`
|
||||||
- **React UI**: Uses React 18, TypeScript, TanStack Query, Tailwind CSS v4, Radix UI
|
- **React UI**: Uses React 18, TypeScript, TanStack Query, Tailwind CSS v4, Radix UI
|
||||||
- **Design System**: Neobrutalism style with specific color tokens and animations
|
- **Design System**: Neobrutalism style with specific color tokens and animations
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ This command **requires** the project directory as an argument via `$ARGUMENTS`.
|
|||||||
|
|
||||||
**Example:** `/create-spec generations/my-app`
|
**Example:** `/create-spec generations/my-app`
|
||||||
|
|
||||||
**Output location:** `$ARGUMENTS/.autocoder/prompts/app_spec.txt` and `$ARGUMENTS/.autocoder/prompts/initializer_prompt.md`
|
**Output location:** `$ARGUMENTS/.autoforge/prompts/app_spec.txt` and `$ARGUMENTS/.autoforge/prompts/initializer_prompt.md`
|
||||||
|
|
||||||
If `$ARGUMENTS` is empty, inform the user they must provide a project path and exit.
|
If `$ARGUMENTS` is empty, inform the user they must provide a project path and exit.
|
||||||
|
|
||||||
@@ -347,13 +347,13 @@ First ask in conversation if they want to make changes.
|
|||||||
|
|
||||||
## Output Directory
|
## Output Directory
|
||||||
|
|
||||||
The output directory is: `$ARGUMENTS/.autocoder/prompts/`
|
The output directory is: `$ARGUMENTS/.autoforge/prompts/`
|
||||||
|
|
||||||
Once the user approves, generate these files:
|
Once the user approves, generate these files:
|
||||||
|
|
||||||
## 1. Generate `app_spec.txt`
|
## 1. Generate `app_spec.txt`
|
||||||
|
|
||||||
**Output path:** `$ARGUMENTS/.autocoder/prompts/app_spec.txt`
|
**Output path:** `$ARGUMENTS/.autoforge/prompts/app_spec.txt`
|
||||||
|
|
||||||
Create a new file using this XML structure:
|
Create a new file using this XML structure:
|
||||||
|
|
||||||
@@ -489,7 +489,7 @@ Create a new file using this XML structure:
|
|||||||
|
|
||||||
## 2. Update `initializer_prompt.md`
|
## 2. Update `initializer_prompt.md`
|
||||||
|
|
||||||
**Output path:** `$ARGUMENTS/.autocoder/prompts/initializer_prompt.md`
|
**Output path:** `$ARGUMENTS/.autoforge/prompts/initializer_prompt.md`
|
||||||
|
|
||||||
If the output directory has an existing `initializer_prompt.md`, read it and update the feature count.
|
If the output directory has an existing `initializer_prompt.md`, read it and update the feature count.
|
||||||
If not, copy from `.claude/templates/initializer_prompt.template.md` first, then update.
|
If not, copy from `.claude/templates/initializer_prompt.template.md` first, then update.
|
||||||
@@ -512,7 +512,7 @@ After: **CRITICAL:** You must create exactly **25** features using the `feature
|
|||||||
|
|
||||||
## 3. Write Status File (REQUIRED - Do This Last)
|
## 3. Write Status File (REQUIRED - Do This Last)
|
||||||
|
|
||||||
**Output path:** `$ARGUMENTS/.autocoder/prompts/.spec_status.json`
|
**Output path:** `$ARGUMENTS/.autoforge/prompts/.spec_status.json`
|
||||||
|
|
||||||
**CRITICAL:** After you have completed ALL requested file changes, write this status file to signal completion to the UI. This is required for the "Continue to Project" button to appear.
|
**CRITICAL:** After you have completed ALL requested file changes, write this status file to signal completion to the UI. This is required for the "Continue to Project" button to appear.
|
||||||
|
|
||||||
@@ -524,8 +524,8 @@ Write this JSON file:
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"timestamp": "[current ISO 8601 timestamp, e.g., 2025-01-15T14:30:00.000Z]",
|
"timestamp": "[current ISO 8601 timestamp, e.g., 2025-01-15T14:30:00.000Z]",
|
||||||
"files_written": [
|
"files_written": [
|
||||||
".autocoder/prompts/app_spec.txt",
|
".autoforge/prompts/app_spec.txt",
|
||||||
".autocoder/prompts/initializer_prompt.md"
|
".autoforge/prompts/initializer_prompt.md"
|
||||||
],
|
],
|
||||||
"feature_count": [the feature count from Phase 4L]
|
"feature_count": [the feature count from Phase 4L]
|
||||||
}
|
}
|
||||||
@@ -539,9 +539,9 @@ Write this JSON file:
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"timestamp": "2025-01-15T14:30:00.000Z",
|
"timestamp": "2025-01-15T14:30:00.000Z",
|
||||||
"files_written": [
|
"files_written": [
|
||||||
".autocoder/prompts/app_spec.txt",
|
".autoforge/prompts/app_spec.txt",
|
||||||
".autocoder/prompts/initializer_prompt.md",
|
".autoforge/prompts/initializer_prompt.md",
|
||||||
".autocoder/prompts/coding_prompt.md"
|
".autoforge/prompts/coding_prompt.md"
|
||||||
],
|
],
|
||||||
"feature_count": 35
|
"feature_count": 35
|
||||||
}
|
}
|
||||||
@@ -559,11 +559,11 @@ Write this JSON file:
|
|||||||
|
|
||||||
Once files are generated, tell the user what to do next:
|
Once files are generated, tell the user what to do next:
|
||||||
|
|
||||||
> "Your specification files have been created in `$ARGUMENTS/.autocoder/prompts/`!
|
> "Your specification files have been created in `$ARGUMENTS/.autoforge/prompts/`!
|
||||||
>
|
>
|
||||||
> **Files created:**
|
> **Files created:**
|
||||||
> - `$ARGUMENTS/.autocoder/prompts/app_spec.txt`
|
> - `$ARGUMENTS/.autoforge/prompts/app_spec.txt`
|
||||||
> - `$ARGUMENTS/.autocoder/prompts/initializer_prompt.md`
|
> - `$ARGUMENTS/.autoforge/prompts/initializer_prompt.md`
|
||||||
>
|
>
|
||||||
> The **Continue to Project** button should now appear. Click it to start the autonomous coding agent!
|
> The **Continue to Project** button should now appear. Click it to start the autonomous coding agent!
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ You are the **Project Expansion Assistant** - an expert at understanding existin
|
|||||||
# FIRST: Read and Understand Existing Project
|
# FIRST: Read and Understand Existing Project
|
||||||
|
|
||||||
**Step 1:** Read the existing specification:
|
**Step 1:** Read the existing specification:
|
||||||
- Read `$ARGUMENTS/.autocoder/prompts/app_spec.txt`
|
- Read `$ARGUMENTS/.autoforge/prompts/app_spec.txt`
|
||||||
|
|
||||||
**Step 2:** Present a summary to the user:
|
**Step 2:** Present a summary to the user:
|
||||||
|
|
||||||
@@ -231,4 +231,4 @@ If they want to add more, go back to Phase 1.
|
|||||||
|
|
||||||
# BEGIN
|
# BEGIN
|
||||||
|
|
||||||
Start by reading the app specification file at `$ARGUMENTS/.autocoder/prompts/app_spec.txt`, then greet the user with a summary of their existing project and ask what they want to add.
|
Start by reading the app specification file at `$ARGUMENTS/.autoforge/prompts/app_spec.txt`, then greet the user with a summary of their existing project and ask what they want to add.
|
||||||
|
|||||||
@@ -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: |
|
description: |
|
||||||
Convert GSD codebase mapping to Autocoder app_spec.txt. This skill should be used when
|
Convert GSD codebase mapping to AutoForge app_spec.txt. This skill should be used when
|
||||||
the user has run /gsd:map-codebase and wants to use Autocoder on an existing project.
|
the user has run /gsd:map-codebase and wants to use AutoForge on an existing project.
|
||||||
Triggers: "convert to autocoder", "gsd to spec", "create app_spec from codebase",
|
Triggers: "convert to autoforge", "gsd to spec", "create app_spec from codebase",
|
||||||
"use autocoder on existing project", after /gsd:map-codebase completion.
|
"use autoforge on existing project", after /gsd:map-codebase completion.
|
||||||
---
|
---
|
||||||
|
|
||||||
# GSD to Autocoder Spec Converter
|
# GSD to AutoForge Spec Converter
|
||||||
|
|
||||||
Converts `.planning/codebase/*.md` (GSD mapping output) to `.autocoder/prompts/app_spec.txt` (Autocoder format).
|
Converts `.planning/codebase/*.md` (GSD mapping output) to `.autoforge/prompts/app_spec.txt` (AutoForge format).
|
||||||
|
|
||||||
## When to Use
|
## When to Use
|
||||||
|
|
||||||
- After running `/gsd:map-codebase` on an existing project
|
- After running `/gsd:map-codebase` on an existing project
|
||||||
- When onboarding an existing codebase to Autocoder
|
- When onboarding an existing codebase to AutoForge
|
||||||
- User wants Autocoder to continue development on existing code
|
- User wants AutoForge to continue development on existing code
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -84,12 +84,12 @@ Extract:
|
|||||||
|
|
||||||
Create `prompts/` directory:
|
Create `prompts/` directory:
|
||||||
```bash
|
```bash
|
||||||
mkdir -p .autocoder/prompts
|
mkdir -p .autoforge/prompts
|
||||||
```
|
```
|
||||||
|
|
||||||
**Mapping GSD Documents to Autocoder Spec:**
|
**Mapping GSD Documents to AutoForge Spec:**
|
||||||
|
|
||||||
| GSD Source | Autocoder Target |
|
| GSD Source | AutoForge Target |
|
||||||
|------------|------------------|
|
|------------|------------------|
|
||||||
| STACK.md Languages | `<technology_stack>` |
|
| STACK.md Languages | `<technology_stack>` |
|
||||||
| STACK.md Frameworks | `<frontend>`, `<backend>` |
|
| STACK.md Frameworks | `<frontend>`, `<backend>` |
|
||||||
@@ -114,7 +114,7 @@ mkdir -p .autocoder/prompts
|
|||||||
**Write the spec file** using the XML format from [references/app-spec-format.md](references/app-spec-format.md):
|
**Write the spec file** using the XML format from [references/app-spec-format.md](references/app-spec-format.md):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat > .autocoder/prompts/app_spec.txt << 'EOF'
|
cat > .autoforge/prompts/app_spec.txt << 'EOF'
|
||||||
<project_specification>
|
<project_specification>
|
||||||
<project_name>{from package.json or directory}</project_name>
|
<project_name>{from package.json or directory}</project_name>
|
||||||
|
|
||||||
@@ -173,9 +173,9 @@ EOF
|
|||||||
### Step 5: Verify Generated Spec
|
### Step 5: Verify Generated Spec
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
head -100 .autocoder/prompts/app_spec.txt
|
head -100 .autoforge/prompts/app_spec.txt
|
||||||
echo "---"
|
echo "---"
|
||||||
grep -c "User can\|System\|API\|Feature" .autocoder/prompts/app_spec.txt || echo "0"
|
grep -c "User can\|System\|API\|Feature" .autoforge/prompts/app_spec.txt || echo "0"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Validation checklist:**
|
**Validation checklist:**
|
||||||
@@ -194,15 +194,15 @@ Output:
|
|||||||
app_spec.txt generated from GSD codebase mapping.
|
app_spec.txt generated from GSD codebase mapping.
|
||||||
|
|
||||||
Source: .planning/codebase/*.md
|
Source: .planning/codebase/*.md
|
||||||
Output: .autocoder/prompts/app_spec.txt
|
Output: .autoforge/prompts/app_spec.txt
|
||||||
|
|
||||||
Next: Start Autocoder
|
Next: Start AutoForge
|
||||||
|
|
||||||
cd {project_dir}
|
cd {project_dir}
|
||||||
python ~/projects/autocoder/start.py
|
python ~/projects/autoforge/start.py
|
||||||
|
|
||||||
Or via UI:
|
Or via UI:
|
||||||
~/projects/autocoder/start_ui.sh
|
~/projects/autoforge/start_ui.sh
|
||||||
|
|
||||||
The Initializer will create features.db from this spec.
|
The Initializer will create features.db from this spec.
|
||||||
```
|
```
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Autocoder app_spec.txt XML Format
|
# AutoForge app_spec.txt XML Format
|
||||||
|
|
||||||
Complete reference for the XML structure expected by Autocoder's Initializer agent.
|
Complete reference for the XML structure expected by AutoForge's Initializer agent.
|
||||||
|
|
||||||
## Root Structure
|
## Root Structure
|
||||||
|
|
||||||
@@ -275,7 +275,7 @@ The Initializer agent expects features distributed across categories:
|
|||||||
| Medium web app | 200-250 | 10-15 |
|
| Medium web app | 200-250 | 10-15 |
|
||||||
| Complex full-stack | 300-400 | 15-20 |
|
| Complex full-stack | 300-400 | 15-20 |
|
||||||
|
|
||||||
## GSD to Autocoder Mapping
|
## GSD to AutoForge Mapping
|
||||||
|
|
||||||
When converting from GSD codebase mapping:
|
When converting from GSD codebase mapping:
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
# GLM/Alternative API Configuration (Optional)
|
# GLM/Alternative API Configuration (Optional)
|
||||||
# To use Zhipu AI's GLM models instead of Claude, uncomment and set these variables.
|
# To use Zhipu AI's GLM models instead of Claude, uncomment and set these variables.
|
||||||
# This only affects AutoCoder - your global Claude Code settings remain unchanged.
|
# This only affects AutoForge - your global Claude Code settings remain unchanged.
|
||||||
# Get an API key at: https://z.ai/subscribe
|
# Get an API key at: https://z.ai/subscribe
|
||||||
#
|
#
|
||||||
# ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
|
# ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
generations/
|
generations/
|
||||||
automaker/
|
automaker/
|
||||||
temp/
|
temp/
|
||||||
|
temp-docs/
|
||||||
|
|
||||||
nul
|
nul
|
||||||
issues/
|
issues/
|
||||||
@@ -114,6 +115,7 @@ Desktop.ini
|
|||||||
ui/dist/
|
ui/dist/
|
||||||
ui/.vite/
|
ui/.vite/
|
||||||
.vite/
|
.vite/
|
||||||
|
*.tgz
|
||||||
|
|
||||||
# ===================
|
# ===================
|
||||||
# Environment files
|
# Environment files
|
||||||
|
|||||||
32
.npmignore
Normal file
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
|
## Commands
|
||||||
|
|
||||||
### Quick Start (Recommended)
|
### npm Global Install (Recommended)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Windows - launches CLI menu
|
npm install -g autoforge-ai
|
||||||
start.bat
|
autoforge # Start server (first run sets up Python venv)
|
||||||
|
autoforge config # Edit ~/.autoforge/.env in $EDITOR
|
||||||
|
autoforge config --show # Print active configuration
|
||||||
|
autoforge --port 9999 # Custom port
|
||||||
|
autoforge --no-browser # Don't auto-open browser
|
||||||
|
autoforge --repair # Delete and recreate ~/.autoforge/venv/
|
||||||
|
```
|
||||||
|
|
||||||
# macOS/Linux
|
### From Source (Development)
|
||||||
./start.sh
|
|
||||||
|
|
||||||
|
```bash
|
||||||
# Launch Web UI (serves pre-built React app)
|
# Launch Web UI (serves pre-built React app)
|
||||||
start_ui.bat # Windows
|
start_ui.bat # Windows
|
||||||
./start_ui.sh # macOS/Linux
|
./start_ui.sh # macOS/Linux
|
||||||
|
|
||||||
|
# CLI menu
|
||||||
|
start.bat # Windows
|
||||||
|
./start.sh # macOS/Linux
|
||||||
```
|
```
|
||||||
|
|
||||||
### Python Backend (Manual)
|
### Python Backend (Manual)
|
||||||
@@ -136,11 +146,22 @@ Configuration in `pyproject.toml`:
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
### npm CLI (bin/, lib/)
|
||||||
|
|
||||||
|
The `autoforge` command is a Node.js wrapper that manages the Python environment and server lifecycle:
|
||||||
|
- `bin/autoforge.js` - Entry point (shebang script)
|
||||||
|
- `lib/cli.js` - Main CLI logic: Python 3.11+ detection (cross-platform), venv management at `~/.autoforge/venv/` with composite marker (requirements hash + Python version), `.env` config loading from `~/.autoforge/.env`, uvicorn server startup with PID file, and signal handling
|
||||||
|
- `package.json` - npm package config (`autoforge-ai` on npm), `files` whitelist with `__pycache__` exclusions, `prepublishOnly` builds the UI
|
||||||
|
- `requirements-prod.txt` - Runtime-only Python deps (excludes ruff, mypy, pytest)
|
||||||
|
- `.npmignore` - Excludes dev files, tests, UI source from the published tarball
|
||||||
|
|
||||||
|
Publishing: `npm publish` (triggers `prepublishOnly` which builds UI, then publishes ~600KB tarball with 84 files)
|
||||||
|
|
||||||
### Core Python Modules
|
### Core Python Modules
|
||||||
|
|
||||||
- `start.py` - CLI launcher with project creation/selection menu
|
- `start.py` - CLI launcher with project creation/selection menu
|
||||||
- `autonomous_agent_demo.py` - Entry point for running the agent (supports `--yolo`, `--parallel`, `--batch-size`, `--batch-features`)
|
- `autonomous_agent_demo.py` - Entry point for running the agent (supports `--yolo`, `--parallel`, `--batch-size`, `--batch-features`)
|
||||||
- `autocoder_paths.py` - Central path resolution with dual-path backward compatibility and migration
|
- `autoforge_paths.py` - Central path resolution with dual-path backward compatibility and migration
|
||||||
- `agent.py` - Agent session loop using Claude Agent SDK
|
- `agent.py` - Agent session loop using Claude Agent SDK
|
||||||
- `client.py` - ClaudeSDKClient configuration with security hooks, MCP servers, and Vertex AI support
|
- `client.py` - ClaudeSDKClient configuration with security hooks, MCP servers, and Vertex AI support
|
||||||
- `security.py` - Bash command allowlist validation (ALLOWED_COMMANDS whitelist)
|
- `security.py` - Bash command allowlist validation (ALLOWED_COMMANDS whitelist)
|
||||||
@@ -158,7 +179,7 @@ Configuration in `pyproject.toml`:
|
|||||||
### Project Registry
|
### Project Registry
|
||||||
|
|
||||||
Projects can be stored in any directory. The registry maps project names to paths using SQLite:
|
Projects can be stored in any directory. The registry maps project names to paths using SQLite:
|
||||||
- **All platforms**: `~/.autocoder/registry.db`
|
- **All platforms**: `~/.autoforge/registry.db`
|
||||||
|
|
||||||
The registry uses:
|
The registry uses:
|
||||||
- SQLite database with SQLAlchemy ORM
|
- SQLite database with SQLAlchemy ORM
|
||||||
@@ -245,6 +266,11 @@ Key components:
|
|||||||
- `ScheduleModal.tsx` - Schedule management UI
|
- `ScheduleModal.tsx` - Schedule management UI
|
||||||
- `SettingsModal.tsx` - Global settings panel
|
- `SettingsModal.tsx` - Global settings panel
|
||||||
|
|
||||||
|
In-app documentation (`/#/docs` route):
|
||||||
|
- `src/components/docs/sections/` - Content for each doc section (GettingStarted.tsx, AgentSystem.tsx, etc.)
|
||||||
|
- `src/components/docs/docsData.ts` - Sidebar structure, subsection IDs, search keywords
|
||||||
|
- `src/components/docs/DocsPage.tsx` - Page layout; `DocsContent.tsx` - section renderer with scroll tracking
|
||||||
|
|
||||||
Keyboard shortcuts (press `?` for help):
|
Keyboard shortcuts (press `?` for help):
|
||||||
- `D` - Toggle debug panel
|
- `D` - Toggle debug panel
|
||||||
- `G` - Toggle Kanban/Graph view
|
- `G` - Toggle Kanban/Graph view
|
||||||
@@ -254,18 +280,18 @@ Keyboard shortcuts (press `?` for help):
|
|||||||
|
|
||||||
### Project Structure for Generated Apps
|
### Project Structure for Generated Apps
|
||||||
|
|
||||||
Projects can be stored in any directory (registered in `~/.autocoder/registry.db`). Each project contains:
|
Projects can be stored in any directory (registered in `~/.autoforge/registry.db`). Each project contains:
|
||||||
- `.autocoder/prompts/app_spec.txt` - Application specification (XML format)
|
- `.autoforge/prompts/app_spec.txt` - Application specification (XML format)
|
||||||
- `.autocoder/prompts/initializer_prompt.md` - First session prompt
|
- `.autoforge/prompts/initializer_prompt.md` - First session prompt
|
||||||
- `.autocoder/prompts/coding_prompt.md` - Continuation session prompt
|
- `.autoforge/prompts/coding_prompt.md` - Continuation session prompt
|
||||||
- `.autocoder/features.db` - SQLite database with feature test cases
|
- `.autoforge/features.db` - SQLite database with feature test cases
|
||||||
- `.autocoder/.agent.lock` - Lock file to prevent multiple agent instances
|
- `.autoforge/.agent.lock` - Lock file to prevent multiple agent instances
|
||||||
- `.autocoder/allowed_commands.yaml` - Project-specific bash command allowlist (optional)
|
- `.autoforge/allowed_commands.yaml` - Project-specific bash command allowlist (optional)
|
||||||
- `.autocoder/.gitignore` - Ignores runtime files
|
- `.autoforge/.gitignore` - Ignores runtime files
|
||||||
- `CLAUDE.md` - Stays at project root (SDK convention)
|
- `CLAUDE.md` - Stays at project root (SDK convention)
|
||||||
- `app_spec.txt` - Root copy for agent template compatibility
|
- `app_spec.txt` - Root copy for agent template compatibility
|
||||||
|
|
||||||
Legacy projects with files at root level (e.g., `features.db`, `prompts/`) are auto-migrated to `.autocoder/` on next agent start. Dual-path resolution ensures old and new layouts work transparently.
|
Legacy projects with files at root level (e.g., `features.db`, `prompts/`) are auto-migrated to `.autoforge/` on next agent start. Dual-path resolution ensures old and new layouts work transparently.
|
||||||
|
|
||||||
### Security Model
|
### Security Model
|
||||||
|
|
||||||
@@ -311,14 +337,14 @@ The agent's bash command access is controlled through a hierarchical configurati
|
|||||||
|
|
||||||
**Command Hierarchy (highest to lowest priority):**
|
**Command Hierarchy (highest to lowest priority):**
|
||||||
1. **Hardcoded Blocklist** (`security.py`) - NEVER allowed (dd, sudo, shutdown, etc.)
|
1. **Hardcoded Blocklist** (`security.py`) - NEVER allowed (dd, sudo, shutdown, etc.)
|
||||||
2. **Org Blocklist** (`~/.autocoder/config.yaml`) - Cannot be overridden by projects
|
2. **Org Blocklist** (`~/.autoforge/config.yaml`) - Cannot be overridden by projects
|
||||||
3. **Org Allowlist** (`~/.autocoder/config.yaml`) - Available to all projects
|
3. **Org Allowlist** (`~/.autoforge/config.yaml`) - Available to all projects
|
||||||
4. **Global Allowlist** (`security.py`) - Default commands (npm, git, curl, etc.)
|
4. **Global Allowlist** (`security.py`) - Default commands (npm, git, curl, etc.)
|
||||||
5. **Project Allowlist** (`.autocoder/allowed_commands.yaml`) - Project-specific commands
|
5. **Project Allowlist** (`.autoforge/allowed_commands.yaml`) - Project-specific commands
|
||||||
|
|
||||||
**Project Configuration:**
|
**Project Configuration:**
|
||||||
|
|
||||||
Each project can define custom allowed commands in `.autocoder/allowed_commands.yaml`:
|
Each project can define custom allowed commands in `.autoforge/allowed_commands.yaml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: 1
|
version: 1
|
||||||
@@ -338,7 +364,7 @@ commands:
|
|||||||
|
|
||||||
**Organization Configuration:**
|
**Organization Configuration:**
|
||||||
|
|
||||||
System administrators can set org-wide policies in `~/.autocoder/config.yaml`:
|
System administrators can set org-wide policies in `~/.autoforge/config.yaml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: 1
|
version: 1
|
||||||
@@ -405,7 +431,7 @@ Run coding agents using local models via Ollama v0.14.0+:
|
|||||||
ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3-coder
|
ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3-coder
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3-coder
|
ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3-coder
|
||||||
```
|
```
|
||||||
5. Run autocoder normally - it will use your local Ollama models
|
5. Run AutoForge normally - it will use your local Ollama models
|
||||||
|
|
||||||
**Recommended coding models:**
|
**Recommended coding models:**
|
||||||
- `qwen3-coder` - Good balance of speed and capability
|
- `qwen3-coder` - Good balance of speed and capability
|
||||||
@@ -427,7 +453,7 @@ Run coding agents using local models via Ollama v0.14.0+:
|
|||||||
**Slash commands** (`.claude/commands/`):
|
**Slash commands** (`.claude/commands/`):
|
||||||
- `/create-spec` - Interactive spec creation for new projects
|
- `/create-spec` - Interactive spec creation for new projects
|
||||||
- `/expand-project` - Expand existing project with new features
|
- `/expand-project` - Expand existing project with new features
|
||||||
- `/gsd-to-autocoder-spec` - Convert GSD codebase mapping to app_spec.txt
|
- `/gsd-to-autoforge-spec` - Convert GSD codebase mapping to app_spec.txt
|
||||||
- `/check-code` - Run lint and type-check for code quality
|
- `/check-code` - Run lint and type-check for code quality
|
||||||
- `/checkpoint` - Create comprehensive checkpoint commit
|
- `/checkpoint` - Create comprehensive checkpoint commit
|
||||||
- `/review-pr` - Review pull requests
|
- `/review-pr` - Review pull requests
|
||||||
@@ -439,7 +465,7 @@ Run coding agents using local models via Ollama v0.14.0+:
|
|||||||
|
|
||||||
**Skills** (`.claude/skills/`):
|
**Skills** (`.claude/skills/`):
|
||||||
- `frontend-design` - Distinctive, production-grade UI design
|
- `frontend-design` - Distinctive, production-grade UI design
|
||||||
- `gsd-to-autocoder-spec` - Convert GSD codebase mapping to Autocoder app_spec format
|
- `gsd-to-autoforge-spec` - Convert GSD codebase mapping to AutoForge app_spec format
|
||||||
|
|
||||||
**Other:**
|
**Other:**
|
||||||
- `.claude/templates/` - Prompt templates copied to new projects
|
- `.claude/templates/` - Prompt templates copied to new projects
|
||||||
@@ -449,12 +475,12 @@ Run coding agents using local models via Ollama v0.14.0+:
|
|||||||
|
|
||||||
### Prompt Loading Fallback Chain
|
### Prompt Loading Fallback Chain
|
||||||
|
|
||||||
1. Project-specific: `{project_dir}/.autocoder/prompts/{name}.md` (or legacy `{project_dir}/prompts/{name}.md`)
|
1. Project-specific: `{project_dir}/.autoforge/prompts/{name}.md` (or legacy `{project_dir}/prompts/{name}.md`)
|
||||||
2. Base template: `.claude/templates/{name}.template.md`
|
2. Base template: `.claude/templates/{name}.template.md`
|
||||||
|
|
||||||
### Agent Session Flow
|
### Agent Session Flow
|
||||||
|
|
||||||
1. Check if `.autocoder/features.db` has features (determines initializer vs coding agent)
|
1. Check if `.autoforge/features.db` has features (determines initializer vs coding agent)
|
||||||
2. Create ClaudeSDKClient with security settings
|
2. Create ClaudeSDKClient with security settings
|
||||||
3. Send prompt and stream response
|
3. Send prompt and stream response
|
||||||
4. Auto-continue with 3-second delay between sessions
|
4. Auto-continue with 3-second delay between sessions
|
||||||
|
|||||||
146
README.md
146
README.md
@@ -1,4 +1,4 @@
|
|||||||
# AutoCoder
|
# AutoForge
|
||||||
|
|
||||||
[](https://www.buymeacoffee.com/leonvanzyl)
|
[](https://www.buymeacoffee.com/leonvanzyl)
|
||||||
|
|
||||||
@@ -14,9 +14,11 @@ A long-running autonomous coding agent powered by the Claude Agent SDK. This too
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
### Claude Code CLI (Required)
|
- **Node.js 20+** - Required for the CLI
|
||||||
|
- **Python 3.11+** - Auto-detected on first run ([download](https://www.python.org/downloads/))
|
||||||
|
- **Claude Code CLI** - Install and authenticate (see below)
|
||||||
|
|
||||||
This project requires the Claude Code CLI to be installed. Install it using one of these methods:
|
### Claude Code CLI (Required)
|
||||||
|
|
||||||
**macOS / Linux:**
|
**macOS / Linux:**
|
||||||
```bash
|
```bash
|
||||||
@@ -39,35 +41,63 @@ You need one of the following:
|
|||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Option 1: Web UI (Recommended)
|
### Option 1: npm Install (Recommended)
|
||||||
|
|
||||||
**Windows:**
|
|
||||||
```cmd
|
|
||||||
start_ui.bat
|
|
||||||
```
|
|
||||||
|
|
||||||
**macOS / Linux:**
|
|
||||||
```bash
|
```bash
|
||||||
./start_ui.sh
|
npm install -g autoforge-ai
|
||||||
|
autoforge
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On first run, AutoForge automatically:
|
||||||
|
1. Checks for Python 3.11+
|
||||||
|
2. Creates a virtual environment at `~/.autoforge/venv/`
|
||||||
|
3. Installs Python dependencies
|
||||||
|
4. Copies a default config file to `~/.autoforge/.env`
|
||||||
|
5. Starts the server and opens your browser
|
||||||
|
|
||||||
|
### CLI Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
autoforge Start the server (default)
|
||||||
|
autoforge config Open ~/.autoforge/.env in $EDITOR
|
||||||
|
autoforge config --path Print config file path
|
||||||
|
autoforge config --show Show active configuration values
|
||||||
|
autoforge --port PORT Custom port (default: auto from 8888)
|
||||||
|
autoforge --host HOST Custom host (default: 127.0.0.1)
|
||||||
|
autoforge --no-browser Don't auto-open browser
|
||||||
|
autoforge --repair Delete and recreate virtual environment
|
||||||
|
autoforge --version Print version
|
||||||
|
autoforge --help Show help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: From Source (Development)
|
||||||
|
|
||||||
|
Clone the repository and use the start scripts directly. This is the recommended path if you want to contribute or modify AutoForge itself.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/leonvanzyl/autoforge.git
|
||||||
|
cd autoforge
|
||||||
|
```
|
||||||
|
|
||||||
|
**Web UI:**
|
||||||
|
|
||||||
|
| Platform | Command |
|
||||||
|
|---|---|
|
||||||
|
| Windows | `start_ui.bat` |
|
||||||
|
| macOS / Linux | `./start_ui.sh` |
|
||||||
|
|
||||||
This launches the React-based web UI at `http://localhost:5173` with:
|
This launches the React-based web UI at `http://localhost:5173` with:
|
||||||
- Project selection and creation
|
- Project selection and creation
|
||||||
- Kanban board view of features
|
- Kanban board view of features
|
||||||
- Real-time agent output streaming
|
- Real-time agent output streaming
|
||||||
- Start/pause/stop controls
|
- Start/pause/stop controls
|
||||||
|
|
||||||
### Option 2: CLI Mode
|
**CLI Mode:**
|
||||||
|
|
||||||
**Windows:**
|
| Platform | Command |
|
||||||
```cmd
|
|---|---|
|
||||||
start.bat
|
| Windows | `start.bat` |
|
||||||
```
|
| macOS / Linux | `./start.sh` |
|
||||||
|
|
||||||
**macOS / Linux:**
|
|
||||||
```bash
|
|
||||||
./start.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
The start script will:
|
The start script will:
|
||||||
1. Check if Claude CLI is installed
|
1. Check if Claude CLI is installed
|
||||||
@@ -130,11 +160,9 @@ Features are stored in SQLite via SQLAlchemy and managed through an MCP server t
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
autonomous-coding/
|
autoforge/
|
||||||
├── start.bat # Windows CLI start script
|
├── bin/ # npm CLI entry point
|
||||||
├── start.sh # macOS/Linux CLI start script
|
├── lib/ # CLI bootstrap and setup logic
|
||||||
├── start_ui.bat # Windows Web UI start script
|
|
||||||
├── start_ui.sh # macOS/Linux Web UI start script
|
|
||||||
├── start.py # CLI menu and project management
|
├── start.py # CLI menu and project management
|
||||||
├── start_ui.py # Web UI backend (FastAPI server launcher)
|
├── start_ui.py # Web UI backend (FastAPI server launcher)
|
||||||
├── autonomous_agent_demo.py # Agent entry point
|
├── autonomous_agent_demo.py # Agent entry point
|
||||||
@@ -165,9 +193,10 @@ autonomous-coding/
|
|||||||
│ │ └── create-spec.md # /create-spec slash command
|
│ │ └── create-spec.md # /create-spec slash command
|
||||||
│ ├── skills/ # Claude Code skills
|
│ ├── skills/ # Claude Code skills
|
||||||
│ └── templates/ # Prompt templates
|
│ └── templates/ # Prompt templates
|
||||||
├── generations/ # Generated projects go here
|
├── requirements.txt # Python dependencies (development)
|
||||||
├── requirements.txt # Python dependencies
|
├── requirements-prod.txt # Python dependencies (npm install)
|
||||||
└── .env # Optional configuration (N8N webhook)
|
├── package.json # npm package definition
|
||||||
|
└── .env # Optional configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -264,11 +293,20 @@ The UI receives live updates via WebSocket (`/ws/projects/{project_name}`):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configuration (Optional)
|
## Configuration
|
||||||
|
|
||||||
|
AutoForge reads configuration from a `.env` file. The file location depends on how you installed AutoForge:
|
||||||
|
|
||||||
|
| Install method | Config file location | Edit command |
|
||||||
|
|---|---|---|
|
||||||
|
| npm (global) | `~/.autoforge/.env` | `autoforge config` |
|
||||||
|
| From source | `.env` in the project root | Edit directly |
|
||||||
|
|
||||||
|
A default config file is created automatically on first run. Use `autoforge config` to open it in your editor, or `autoforge config --show` to print the active values.
|
||||||
|
|
||||||
### N8N Webhook Integration
|
### N8N Webhook Integration
|
||||||
|
|
||||||
The agent can send progress notifications to an N8N webhook. Create a `.env` file:
|
Add to your `.env` to send progress notifications to an N8N webhook:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Optional: N8N webhook for progress notifications
|
# Optional: N8N webhook for progress notifications
|
||||||
@@ -290,7 +328,7 @@ When test progress increases, the agent sends:
|
|||||||
|
|
||||||
### Using GLM Models (Alternative to Claude)
|
### Using GLM Models (Alternative to Claude)
|
||||||
|
|
||||||
To use Zhipu AI's GLM models instead of Claude, add these variables to your `.env` file in the AutoCoder directory:
|
Add these variables to your `.env` file to use Zhipu AI's GLM models:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
|
ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
|
||||||
@@ -301,10 +339,40 @@ ANTHROPIC_DEFAULT_OPUS_MODEL=glm-4.7
|
|||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-4.5-air
|
ANTHROPIC_DEFAULT_HAIKU_MODEL=glm-4.5-air
|
||||||
```
|
```
|
||||||
|
|
||||||
This routes AutoCoder's API requests through Zhipu's Claude-compatible API, allowing you to use GLM-4.7 and other models. **This only affects AutoCoder** - your global Claude Code settings remain unchanged.
|
This routes AutoForge's API requests through Zhipu's Claude-compatible API, allowing you to use GLM-4.7 and other models. **This only affects AutoForge** - your global Claude Code settings remain unchanged.
|
||||||
|
|
||||||
Get an API key at: https://z.ai/subscribe
|
Get an API key at: https://z.ai/subscribe
|
||||||
|
|
||||||
|
### Using Ollama Local Models
|
||||||
|
|
||||||
|
Add these variables to your `.env` file to run agents with local models via Ollama v0.14.0+:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ANTHROPIC_BASE_URL=http://localhost:11434
|
||||||
|
ANTHROPIC_AUTH_TOKEN=ollama
|
||||||
|
API_TIMEOUT_MS=3000000
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3-coder
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3-coder
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3-coder
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [CLAUDE.md](CLAUDE.md) for recommended models and known limitations.
|
||||||
|
|
||||||
|
### Using Vertex AI
|
||||||
|
|
||||||
|
Add these variables to your `.env` file to run agents via Google Cloud Vertex AI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CLAUDE_CODE_USE_VERTEX=1
|
||||||
|
CLOUD_ML_REGION=us-east5
|
||||||
|
ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project-id
|
||||||
|
ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-5@20251101
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-5@20250929
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-3-5-haiku@20241022
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires `gcloud auth application-default login` first. Note the `@` separator (not `-`) in Vertex AI model names.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
@@ -335,6 +403,18 @@ This is normal. The initializer agent is generating detailed test cases, which t
|
|||||||
**"Command blocked by security hook"**
|
**"Command blocked by security hook"**
|
||||||
The agent tried to run a command not in the allowlist. This is the security system working as intended. If needed, add the command to `ALLOWED_COMMANDS` in `security.py`.
|
The agent tried to run a command not in the allowlist. This is the security system working as intended. If needed, add the command to `ALLOWED_COMMANDS` in `security.py`.
|
||||||
|
|
||||||
|
**"Python 3.11+ required but not found"**
|
||||||
|
Install Python 3.11 or later from [python.org](https://www.python.org/downloads/). Make sure `python3` (or `python` on Windows) is on your PATH.
|
||||||
|
|
||||||
|
**"Python venv module not available"**
|
||||||
|
On Debian/Ubuntu, the venv module is packaged separately. Install it with `sudo apt install python3.XX-venv` (replace `XX` with your Python minor version, e.g., `python3.12-venv`).
|
||||||
|
|
||||||
|
**"AutoForge is already running"**
|
||||||
|
A server instance is already active. Use the browser URL shown in the terminal, or stop the existing instance with Ctrl+C first.
|
||||||
|
|
||||||
|
**Virtual environment issues after a Python upgrade**
|
||||||
|
Run `autoforge --repair` to delete and recreate the virtual environment from scratch.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ class ScheduleOverride(Base):
|
|||||||
|
|
||||||
def get_database_path(project_dir: Path) -> Path:
|
def get_database_path(project_dir: Path) -> Path:
|
||||||
"""Return the path to the SQLite database for a project."""
|
"""Return the path to the SQLite database for a project."""
|
||||||
from autocoder_paths import get_features_db_path
|
from autoforge_paths import get_features_db_path
|
||||||
return get_features_db_path(project_dir)
|
return get_features_db_path(project_dir)
|
||||||
|
|
||||||
|
|
||||||
@@ -385,7 +385,7 @@ def create_database(project_dir: Path) -> tuple:
|
|||||||
|
|
||||||
db_url = get_database_url(project_dir)
|
db_url = get_database_url(project_dir)
|
||||||
|
|
||||||
# Ensure parent directory exists (for .autocoder/ layout)
|
# Ensure parent directory exists (for .autoforge/ layout)
|
||||||
db_path = get_database_path(project_dir)
|
db_path = get_database_path(project_dir)
|
||||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
"""
|
"""
|
||||||
Autocoder Path Resolution
|
AutoForge Path Resolution
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
Central module for resolving paths to autocoder-generated files within a project.
|
Central module for resolving paths to autoforge-generated files within a project.
|
||||||
|
|
||||||
Implements a dual-path resolution strategy for backward compatibility:
|
Implements a tri-path resolution strategy for backward compatibility:
|
||||||
|
|
||||||
1. Check ``project_dir / ".autocoder" / X`` (new layout)
|
1. Check ``project_dir / ".autoforge" / X`` (current layout)
|
||||||
2. Check ``project_dir / X`` (legacy root-level layout)
|
2. Check ``project_dir / ".autocoder" / X`` (legacy layout)
|
||||||
3. Default to the new location for fresh projects
|
3. Check ``project_dir / X`` (legacy root-level layout)
|
||||||
|
4. Default to the new location for fresh projects
|
||||||
|
|
||||||
This allows existing projects with root-level ``features.db``, ``.agent.lock``,
|
This allows existing projects with root-level ``features.db``, ``.agent.lock``,
|
||||||
etc. to keep working while new projects store everything under ``.autocoder/``.
|
etc. to keep working while new projects store everything under ``.autoforge/``.
|
||||||
|
Projects using the old ``.autocoder/`` directory are auto-migrated on next start.
|
||||||
|
|
||||||
The ``migrate_project_layout`` function can move an old-layout project to the
|
The ``migrate_project_layout`` function can move an old-layout project to the
|
||||||
new layout safely, with full integrity checks for SQLite databases.
|
new layout safely, with full integrity checks for SQLite databases.
|
||||||
@@ -25,10 +27,10 @@ from pathlib import Path
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# .gitignore content written into every .autocoder/ directory
|
# .gitignore content written into every .autoforge/ directory
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
_GITIGNORE_CONTENT = """\
|
_GITIGNORE_CONTENT = """\
|
||||||
# Autocoder runtime files
|
# AutoForge runtime files
|
||||||
features.db
|
features.db
|
||||||
features.db-wal
|
features.db-wal
|
||||||
features.db-shm
|
features.db-shm
|
||||||
@@ -49,15 +51,18 @@ assistant.db-shm
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _resolve_path(project_dir: Path, filename: str) -> Path:
|
def _resolve_path(project_dir: Path, filename: str) -> Path:
|
||||||
"""Resolve a file path using dual-path strategy.
|
"""Resolve a file path using tri-path strategy.
|
||||||
|
|
||||||
Checks the new ``.autocoder/`` location first, then falls back to the
|
Checks the new ``.autoforge/`` location first, then the legacy
|
||||||
legacy root-level location. If neither exists, returns the new location
|
``.autocoder/`` location, then the root-level location. If none exist,
|
||||||
so that newly-created files land in ``.autocoder/``.
|
returns the new location so that newly-created files land in ``.autoforge/``.
|
||||||
"""
|
"""
|
||||||
new = project_dir / ".autocoder" / filename
|
new = project_dir / ".autoforge" / filename
|
||||||
if new.exists():
|
if new.exists():
|
||||||
return new
|
return new
|
||||||
|
legacy = project_dir / ".autocoder" / filename
|
||||||
|
if legacy.exists():
|
||||||
|
return legacy
|
||||||
old = project_dir / filename
|
old = project_dir / filename
|
||||||
if old.exists():
|
if old.exists():
|
||||||
return old
|
return old
|
||||||
@@ -65,14 +70,17 @@ def _resolve_path(project_dir: Path, filename: str) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_dir(project_dir: Path, dirname: str) -> Path:
|
def _resolve_dir(project_dir: Path, dirname: str) -> Path:
|
||||||
"""Resolve a directory path using dual-path strategy.
|
"""Resolve a directory path using tri-path strategy.
|
||||||
|
|
||||||
Same logic as ``_resolve_path`` but intended for directories such as
|
Same logic as ``_resolve_path`` but intended for directories such as
|
||||||
``prompts/``.
|
``prompts/``.
|
||||||
"""
|
"""
|
||||||
new = project_dir / ".autocoder" / dirname
|
new = project_dir / ".autoforge" / dirname
|
||||||
if new.exists():
|
if new.exists():
|
||||||
return new
|
return new
|
||||||
|
legacy = project_dir / ".autocoder" / dirname
|
||||||
|
if legacy.exists():
|
||||||
|
return legacy
|
||||||
old = project_dir / dirname
|
old = project_dir / dirname
|
||||||
if old.exists():
|
if old.exists():
|
||||||
return old
|
return old
|
||||||
@@ -80,27 +88,27 @@ def _resolve_dir(project_dir: Path, dirname: str) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# .autocoder directory management
|
# .autoforge directory management
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def get_autocoder_dir(project_dir: Path) -> Path:
|
def get_autoforge_dir(project_dir: Path) -> Path:
|
||||||
"""Return the ``.autocoder`` directory path. Does NOT create it."""
|
"""Return the ``.autoforge`` directory path. Does NOT create it."""
|
||||||
return project_dir / ".autocoder"
|
return project_dir / ".autoforge"
|
||||||
|
|
||||||
|
|
||||||
def ensure_autocoder_dir(project_dir: Path) -> Path:
|
def ensure_autoforge_dir(project_dir: Path) -> Path:
|
||||||
"""Create the ``.autocoder/`` directory (if needed) and write its ``.gitignore``.
|
"""Create the ``.autoforge/`` directory (if needed) and write its ``.gitignore``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The path to the ``.autocoder`` directory.
|
The path to the ``.autoforge`` directory.
|
||||||
"""
|
"""
|
||||||
autocoder_dir = get_autocoder_dir(project_dir)
|
autoforge_dir = get_autoforge_dir(project_dir)
|
||||||
autocoder_dir.mkdir(parents=True, exist_ok=True)
|
autoforge_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
gitignore_path = autocoder_dir / ".gitignore"
|
gitignore_path = autoforge_dir / ".gitignore"
|
||||||
gitignore_path.write_text(_GITIGNORE_CONTENT, encoding="utf-8")
|
gitignore_path.write_text(_GITIGNORE_CONTENT, encoding="utf-8")
|
||||||
|
|
||||||
return autocoder_dir
|
return autoforge_dir
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -154,9 +162,9 @@ def get_prompts_dir(project_dir: Path) -> Path:
|
|||||||
def get_expand_settings_path(project_dir: Path, uuid_hex: str) -> Path:
|
def get_expand_settings_path(project_dir: Path, uuid_hex: str) -> Path:
|
||||||
"""Return the path for an ephemeral expand-session settings file.
|
"""Return the path for an ephemeral expand-session settings file.
|
||||||
|
|
||||||
These files are short-lived and always stored in ``.autocoder/``.
|
These files are short-lived and always stored in ``.autoforge/``.
|
||||||
"""
|
"""
|
||||||
return project_dir / ".autocoder" / f".claude_settings.expand.{uuid_hex}.json"
|
return project_dir / ".autoforge" / f".claude_settings.expand.{uuid_hex}.json"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -166,8 +174,9 @@ def get_expand_settings_path(project_dir: Path, uuid_hex: str) -> Path:
|
|||||||
def has_agent_running(project_dir: Path) -> bool:
|
def has_agent_running(project_dir: Path) -> bool:
|
||||||
"""Check whether any agent or dev-server lock file exists at either location.
|
"""Check whether any agent or dev-server lock file exists at either location.
|
||||||
|
|
||||||
Inspects both the legacy root-level paths and the new ``.autocoder/``
|
Inspects the legacy root-level paths, the old ``.autocoder/`` paths, and
|
||||||
paths so that a running agent is detected regardless of project layout.
|
the new ``.autoforge/`` paths so that a running agent is detected
|
||||||
|
regardless of project layout.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
``True`` if any ``.agent.lock`` or ``.devserver.lock`` exists.
|
``True`` if any ``.agent.lock`` or ``.devserver.lock`` exists.
|
||||||
@@ -176,8 +185,11 @@ def has_agent_running(project_dir: Path) -> bool:
|
|||||||
for name in lock_names:
|
for name in lock_names:
|
||||||
if (project_dir / name).exists():
|
if (project_dir / name).exists():
|
||||||
return True
|
return True
|
||||||
|
# Check both old and new directory names for backward compatibility
|
||||||
if (project_dir / ".autocoder" / name).exists():
|
if (project_dir / ".autocoder" / name).exists():
|
||||||
return True
|
return True
|
||||||
|
if (project_dir / ".autoforge" / name).exists():
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -186,7 +198,7 @@ def has_agent_running(project_dir: Path) -> bool:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def migrate_project_layout(project_dir: Path) -> list[str]:
|
def migrate_project_layout(project_dir: Path) -> list[str]:
|
||||||
"""Migrate a project from the legacy root-level layout to ``.autocoder/``.
|
"""Migrate a project from the legacy root-level layout to ``.autoforge/``.
|
||||||
|
|
||||||
The migration is incremental and safe:
|
The migration is incremental and safe:
|
||||||
|
|
||||||
@@ -199,7 +211,7 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A list of human-readable descriptions of what was migrated, e.g.
|
A list of human-readable descriptions of what was migrated, e.g.
|
||||||
``["prompts/ -> .autocoder/prompts/", "features.db -> .autocoder/features.db"]``.
|
``["prompts/ -> .autoforge/prompts/", "features.db -> .autoforge/features.db"]``.
|
||||||
An empty list means nothing was migrated (either everything is
|
An empty list means nothing was migrated (either everything is
|
||||||
already migrated, or the agent is running).
|
already migrated, or the agent is running).
|
||||||
"""
|
"""
|
||||||
@@ -208,18 +220,31 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
|
|||||||
logger.warning("Migration skipped: agent or dev-server is running for %s", project_dir)
|
logger.warning("Migration skipped: agent or dev-server is running for %s", project_dir)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
autocoder_dir = ensure_autocoder_dir(project_dir)
|
# --- 0. Migrate .autocoder/ → .autoforge/ directory -------------------
|
||||||
migrated: list[str] = []
|
old_autocoder_dir = project_dir / ".autocoder"
|
||||||
|
new_autoforge_dir = project_dir / ".autoforge"
|
||||||
|
if old_autocoder_dir.exists() and old_autocoder_dir.is_dir() and not new_autoforge_dir.exists():
|
||||||
|
try:
|
||||||
|
old_autocoder_dir.rename(new_autoforge_dir)
|
||||||
|
logger.info("Migrated .autocoder/ -> .autoforge/")
|
||||||
|
migrated: list[str] = [".autocoder/ -> .autoforge/"]
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Failed to migrate .autocoder/ -> .autoforge/", exc_info=True)
|
||||||
|
migrated = []
|
||||||
|
else:
|
||||||
|
migrated = []
|
||||||
|
|
||||||
|
autoforge_dir = ensure_autoforge_dir(project_dir)
|
||||||
|
|
||||||
# --- 1. Migrate prompts/ directory -----------------------------------
|
# --- 1. Migrate prompts/ directory -----------------------------------
|
||||||
try:
|
try:
|
||||||
old_prompts = project_dir / "prompts"
|
old_prompts = project_dir / "prompts"
|
||||||
new_prompts = autocoder_dir / "prompts"
|
new_prompts = autoforge_dir / "prompts"
|
||||||
if old_prompts.exists() and old_prompts.is_dir() and not new_prompts.exists():
|
if old_prompts.exists() and old_prompts.is_dir() and not new_prompts.exists():
|
||||||
shutil.copytree(str(old_prompts), str(new_prompts))
|
shutil.copytree(str(old_prompts), str(new_prompts))
|
||||||
shutil.rmtree(str(old_prompts))
|
shutil.rmtree(str(old_prompts))
|
||||||
migrated.append("prompts/ -> .autocoder/prompts/")
|
migrated.append("prompts/ -> .autoforge/prompts/")
|
||||||
logger.info("Migrated prompts/ -> .autocoder/prompts/")
|
logger.info("Migrated prompts/ -> .autoforge/prompts/")
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Failed to migrate prompts/ directory", exc_info=True)
|
logger.warning("Failed to migrate prompts/ directory", exc_info=True)
|
||||||
|
|
||||||
@@ -228,7 +253,7 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
|
|||||||
for db_name in db_names:
|
for db_name in db_names:
|
||||||
try:
|
try:
|
||||||
old_db = project_dir / db_name
|
old_db = project_dir / db_name
|
||||||
new_db = autocoder_dir / db_name
|
new_db = autoforge_dir / db_name
|
||||||
if old_db.exists() and not new_db.exists():
|
if old_db.exists() and not new_db.exists():
|
||||||
# Flush WAL to ensure all data is in the main database file
|
# Flush WAL to ensure all data is in the main database file
|
||||||
conn = sqlite3.connect(str(old_db))
|
conn = sqlite3.connect(str(old_db))
|
||||||
@@ -263,8 +288,8 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
|
|||||||
wal_file = project_dir / f"{db_name}{suffix}"
|
wal_file = project_dir / f"{db_name}{suffix}"
|
||||||
wal_file.unlink(missing_ok=True)
|
wal_file.unlink(missing_ok=True)
|
||||||
|
|
||||||
migrated.append(f"{db_name} -> .autocoder/{db_name}")
|
migrated.append(f"{db_name} -> .autoforge/{db_name}")
|
||||||
logger.info("Migrated %s -> .autocoder/%s", db_name, db_name)
|
logger.info("Migrated %s -> .autoforge/%s", db_name, db_name)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Failed to migrate %s", db_name, exc_info=True)
|
logger.warning("Failed to migrate %s", db_name, exc_info=True)
|
||||||
|
|
||||||
@@ -279,11 +304,11 @@ def migrate_project_layout(project_dir: Path) -> list[str]:
|
|||||||
for filename in simple_files:
|
for filename in simple_files:
|
||||||
try:
|
try:
|
||||||
old_file = project_dir / filename
|
old_file = project_dir / filename
|
||||||
new_file = autocoder_dir / filename
|
new_file = autoforge_dir / filename
|
||||||
if old_file.exists() and not new_file.exists():
|
if old_file.exists() and not new_file.exists():
|
||||||
shutil.move(str(old_file), str(new_file))
|
shutil.move(str(old_file), str(new_file))
|
||||||
migrated.append(f"{filename} -> .autocoder/{filename}")
|
migrated.append(f"{filename} -> .autoforge/{filename}")
|
||||||
logger.info("Migrated %s -> .autocoder/%s", filename, filename)
|
logger.info("Migrated %s -> .autoforge/%s", filename, filename)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Failed to migrate %s", filename, exc_info=True)
|
logger.warning("Failed to migrate %s", filename, exc_info=True)
|
||||||
|
|
||||||
@@ -221,11 +221,11 @@ def main() -> None:
|
|||||||
print("Use an absolute path or register the project first.")
|
print("Use an absolute path or register the project first.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Migrate project layout to .autocoder/ if needed (idempotent, safe)
|
# Migrate project layout to .autoforge/ if needed (idempotent, safe)
|
||||||
from autocoder_paths import migrate_project_layout
|
from autoforge_paths import migrate_project_layout
|
||||||
migrated = migrate_project_layout(project_dir)
|
migrated = migrate_project_layout(project_dir)
|
||||||
if migrated:
|
if migrated:
|
||||||
print(f"Migrated project files to .autocoder/: {', '.join(migrated)}", flush=True)
|
print(f"Migrated project files to .autoforge/: {', '.join(migrated)}", flush=True)
|
||||||
|
|
||||||
# Parse batch testing feature IDs (comma-separated string -> list[int])
|
# Parse batch testing feature IDs (comma-separated string -> list[int])
|
||||||
testing_feature_ids: list[int] | None = None
|
testing_feature_ids: list[int] | None = None
|
||||||
@@ -263,6 +263,17 @@ def main() -> None:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Entry point mode - always use unified orchestrator
|
# 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
|
from parallel_orchestrator import run_parallel_orchestrator
|
||||||
|
|
||||||
# Clamp concurrency to valid range (1-5)
|
# 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)
|
project_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Write settings to a file in the project directory
|
# Write settings to a file in the project directory
|
||||||
from autocoder_paths import get_claude_settings_path
|
from autoforge_paths import get_claude_settings_path
|
||||||
settings_file = get_claude_settings_path(project_dir)
|
settings_file = get_claude_settings_path(project_dir)
|
||||||
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(settings_file, "w") as f:
|
with open(settings_file, "w") as f:
|
||||||
@@ -450,7 +450,7 @@ def create_client(
|
|||||||
|
|
||||||
# Build environment overrides for API endpoint configuration
|
# Build environment overrides for API endpoint configuration
|
||||||
# These override system env vars for the Claude CLI subprocess,
|
# These override system env vars for the Claude CLI subprocess,
|
||||||
# allowing AutoCoder to use alternative APIs (e.g., GLM) without
|
# allowing AutoForge to use alternative APIs (e.g., GLM) without
|
||||||
# affecting the user's global Claude Code settings
|
# affecting the user's global Claude Code settings
|
||||||
sdk_env = {}
|
sdk_env = {}
|
||||||
for var in API_ENV_VARS:
|
for var in API_ENV_VARS:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ subprocesses. Imported by both ``client.py`` (agent sessions) and
|
|||||||
``server/services/chat_constants.py`` (chat sessions) to avoid maintaining
|
``server/services/chat_constants.py`` (chat sessions) to avoid maintaining
|
||||||
duplicate lists.
|
duplicate lists.
|
||||||
|
|
||||||
These allow autocoder to use alternative API endpoints (Ollama, GLM,
|
These allow autoforge to use alternative API endpoints (Ollama, GLM,
|
||||||
Vertex AI) without affecting the user's global Claude Code settings.
|
Vertex AI) without affecting the user's global Claude Code settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ To see what you can reduce:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Count commands by prefix
|
# Count commands by prefix
|
||||||
grep "^ - name:" .autocoder/allowed_commands.yaml | \
|
grep "^ - name:" .autoforge/allowed_commands.yaml | \
|
||||||
sed 's/^ - name: //' | \
|
sed 's/^ - name: //' | \
|
||||||
cut -d' ' -f1 | \
|
cut -d' ' -f1 | \
|
||||||
sort | uniq -c | sort -rn
|
sort | uniq -c | sort -rn
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# AutoCoder Security Configuration Examples
|
# AutoForge Security Configuration Examples
|
||||||
|
|
||||||
This directory contains example configuration files for controlling which bash commands the autonomous coding agent can execute.
|
This directory contains example configuration files for controlling which bash commands the autonomous coding agent can execute.
|
||||||
|
|
||||||
@@ -18,11 +18,11 @@ This directory contains example configuration files for controlling which bash c
|
|||||||
|
|
||||||
### For a Single Project (Most Common)
|
### For a Single Project (Most Common)
|
||||||
|
|
||||||
When you create a new project with AutoCoder, it automatically creates:
|
When you create a new project with AutoForge, it automatically creates:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
my-project/
|
my-project/
|
||||||
.autocoder/
|
.autoforge/
|
||||||
allowed_commands.yaml ← Automatically created from template
|
allowed_commands.yaml ← Automatically created from template
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -34,17 +34,17 @@ If you want commands available across **all projects**, manually create:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Copy the example to your home directory
|
# Copy the example to your home directory
|
||||||
cp examples/org_config.yaml ~/.autocoder/config.yaml
|
cp examples/org_config.yaml ~/.autoforge/config.yaml
|
||||||
|
|
||||||
# Edit it to add org-wide commands
|
# Edit it to add org-wide commands
|
||||||
nano ~/.autocoder/config.yaml
|
nano ~/.autoforge/config.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Project-Level Configuration
|
## Project-Level Configuration
|
||||||
|
|
||||||
**File:** `{project_dir}/.autocoder/allowed_commands.yaml`
|
**File:** `{project_dir}/.autoforge/allowed_commands.yaml`
|
||||||
|
|
||||||
**Purpose:** Define commands needed for THIS specific project.
|
**Purpose:** Define commands needed for THIS specific project.
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ commands:
|
|||||||
|
|
||||||
## Organization-Level Configuration
|
## Organization-Level Configuration
|
||||||
|
|
||||||
**File:** `~/.autocoder/config.yaml`
|
**File:** `~/.autoforge/config.yaml`
|
||||||
|
|
||||||
**Purpose:** Define commands and policies for ALL projects.
|
**Purpose:** Define commands and policies for ALL projects.
|
||||||
|
|
||||||
@@ -127,13 +127,13 @@ When the agent tries to run a command, the system checks in this order:
|
|||||||
└─────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────┘
|
||||||
↓
|
↓
|
||||||
┌─────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────┐
|
||||||
│ 2. ORG BLOCKLIST (~/.autocoder/config.yaml) │
|
│ 2. ORG BLOCKLIST (~/.autoforge/config.yaml) │
|
||||||
│ Commands you block organization-wide │
|
│ Commands you block organization-wide │
|
||||||
│ ❌ Projects CANNOT override these │
|
│ ❌ Projects CANNOT override these │
|
||||||
└─────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────┘
|
||||||
↓
|
↓
|
||||||
┌─────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────┐
|
||||||
│ 3. ORG ALLOWLIST (~/.autocoder/config.yaml) │
|
│ 3. ORG ALLOWLIST (~/.autoforge/config.yaml) │
|
||||||
│ Commands available to all projects │
|
│ Commands available to all projects │
|
||||||
│ ✅ Automatically available │
|
│ ✅ Automatically available │
|
||||||
└─────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────┘
|
||||||
@@ -145,7 +145,7 @@ When the agent tries to run a command, the system checks in this order:
|
|||||||
└─────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────┘
|
||||||
↓
|
↓
|
||||||
┌─────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────┐
|
||||||
│ 5. PROJECT ALLOWLIST (.autocoder/allowed_commands) │
|
│ 5. PROJECT ALLOWLIST (.autoforge/allowed_commands) │
|
||||||
│ Project-specific commands │
|
│ Project-specific commands │
|
||||||
│ ✅ Available only to this project │
|
│ ✅ Available only to this project │
|
||||||
└─────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────┘
|
||||||
@@ -195,7 +195,7 @@ Matches:
|
|||||||
|
|
||||||
### iOS Development
|
### iOS Development
|
||||||
|
|
||||||
**Project config** (`.autocoder/allowed_commands.yaml`):
|
**Project config** (`.autoforge/allowed_commands.yaml`):
|
||||||
```yaml
|
```yaml
|
||||||
version: 1
|
version: 1
|
||||||
commands:
|
commands:
|
||||||
@@ -245,7 +245,7 @@ commands:
|
|||||||
|
|
||||||
### Enterprise Organization (Restrictive)
|
### Enterprise Organization (Restrictive)
|
||||||
|
|
||||||
**Org config** (`~/.autocoder/config.yaml`):
|
**Org config** (`~/.autoforge/config.yaml`):
|
||||||
```yaml
|
```yaml
|
||||||
version: 1
|
version: 1
|
||||||
|
|
||||||
@@ -265,7 +265,7 @@ blocked_commands:
|
|||||||
|
|
||||||
### Startup Team (Permissive)
|
### Startup Team (Permissive)
|
||||||
|
|
||||||
**Org config** (`~/.autocoder/config.yaml`):
|
**Org config** (`~/.autoforge/config.yaml`):
|
||||||
```yaml
|
```yaml
|
||||||
version: 1
|
version: 1
|
||||||
|
|
||||||
@@ -394,7 +394,7 @@ These commands are **NEVER allowed**, even with user approval:
|
|||||||
|
|
||||||
**Solution:** Add the command to your project config:
|
**Solution:** Add the command to your project config:
|
||||||
```yaml
|
```yaml
|
||||||
# In .autocoder/allowed_commands.yaml
|
# In .autoforge/allowed_commands.yaml
|
||||||
commands:
|
commands:
|
||||||
- name: X
|
- name: X
|
||||||
description: What this command does
|
description: What this command does
|
||||||
@@ -405,7 +405,7 @@ commands:
|
|||||||
**Cause:** The command is in the org blocklist or hardcoded blocklist.
|
**Cause:** The command is in the org blocklist or hardcoded blocklist.
|
||||||
|
|
||||||
**Solution:**
|
**Solution:**
|
||||||
- If in org blocklist: Edit `~/.autocoder/config.yaml` to remove it
|
- If in org blocklist: Edit `~/.autoforge/config.yaml` to remove it
|
||||||
- If in hardcoded blocklist: Cannot be allowed (by design)
|
- If in hardcoded blocklist: Cannot be allowed (by design)
|
||||||
|
|
||||||
### Error: "Could not parse YAML config"
|
### Error: "Could not parse YAML config"
|
||||||
@@ -422,8 +422,8 @@ commands:
|
|||||||
**Solution:**
|
**Solution:**
|
||||||
1. Restart the agent (changes are loaded on startup)
|
1. Restart the agent (changes are loaded on startup)
|
||||||
2. Verify file location:
|
2. Verify file location:
|
||||||
- Project: `{project}/.autocoder/allowed_commands.yaml`
|
- Project: `{project}/.autoforge/allowed_commands.yaml`
|
||||||
- Org: `~/.autocoder/config.yaml` (must be manually created)
|
- Org: `~/.autoforge/config.yaml` (must be manually created)
|
||||||
3. Check YAML is valid (run through a YAML validator)
|
3. Check YAML is valid (run through a YAML validator)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -432,7 +432,7 @@ commands:
|
|||||||
|
|
||||||
### Running the Tests
|
### Running the Tests
|
||||||
|
|
||||||
AutoCoder has comprehensive tests for the security system:
|
AutoForge has comprehensive tests for the security system:
|
||||||
|
|
||||||
**Unit Tests** (136 tests - fast):
|
**Unit Tests** (136 tests - fast):
|
||||||
```bash
|
```bash
|
||||||
@@ -481,7 +481,7 @@ python start.py
|
|||||||
cd path/to/security-test
|
cd path/to/security-test
|
||||||
|
|
||||||
# Edit the config
|
# Edit the config
|
||||||
nano .autocoder/allowed_commands.yaml
|
nano .autoforge/allowed_commands.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
**3. Add a test command (e.g., Swift):**
|
**3. Add a test command (e.g., Swift):**
|
||||||
@@ -509,7 +509,7 @@ Or:
|
|||||||
```text
|
```text
|
||||||
Command 'wget' is not allowed.
|
Command 'wget' is not allowed.
|
||||||
To allow this command:
|
To allow this command:
|
||||||
1. Add to .autocoder/allowed_commands.yaml for this project, OR
|
1. Add to .autoforge/allowed_commands.yaml for this project, OR
|
||||||
2. Request mid-session approval (the agent can ask)
|
2. Request mid-session approval (the agent can ask)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Organization-Level AutoCoder Configuration
|
# Organization-Level AutoForge Configuration
|
||||||
# ============================================
|
# ============================================
|
||||||
# Location: ~/.autocoder/config.yaml
|
# Location: ~/.autoforge/config.yaml
|
||||||
#
|
#
|
||||||
# IMPORTANT: This file is OPTIONAL and must be manually created by you.
|
# IMPORTANT: This file is OPTIONAL and must be manually created by you.
|
||||||
# It does NOT exist by default.
|
# It does NOT exist by default.
|
||||||
@@ -22,7 +22,7 @@ version: 1
|
|||||||
# Organization-Wide Allowed Commands
|
# Organization-Wide Allowed Commands
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# These commands become available to ALL projects automatically.
|
# These commands become available to ALL projects automatically.
|
||||||
# Projects don't need to add them to their own .autocoder/allowed_commands.yaml
|
# Projects don't need to add them to their own .autoforge/allowed_commands.yaml
|
||||||
#
|
#
|
||||||
# By default, this is empty. Uncomment and add commands as needed.
|
# By default, this is empty. Uncomment and add commands as needed.
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ approval_timeout_minutes: 5
|
|||||||
# Default commands: npm, git, curl, ls, cat, etc.
|
# Default commands: npm, git, curl, ls, cat, etc.
|
||||||
# Always available to all projects.
|
# Always available to all projects.
|
||||||
#
|
#
|
||||||
# 5. Project Allowed Commands (.autocoder/allowed_commands.yaml)
|
# 5. Project Allowed Commands (.autoforge/allowed_commands.yaml)
|
||||||
# Project-specific commands defined in each project.
|
# Project-specific commands defined in each project.
|
||||||
# LOWEST PRIORITY (can't override blocks above).
|
# LOWEST PRIORITY (can't override blocks above).
|
||||||
#
|
#
|
||||||
@@ -165,7 +165,7 @@ approval_timeout_minutes: 5
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# To Create This File
|
# To Create This File
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 1. Copy this example to: ~/.autocoder/config.yaml
|
# 1. Copy this example to: ~/.autoforge/config.yaml
|
||||||
# 2. Uncomment and customize the sections you need
|
# 2. Uncomment and customize the sections you need
|
||||||
# 3. Leave empty lists if you don't need org-level controls
|
# 3. Leave empty lists if you don't need org-level controls
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# Project-Specific Allowed Commands
|
# Project-Specific Allowed Commands
|
||||||
# ==================================
|
# ==================================
|
||||||
# Location: {project_dir}/.autocoder/allowed_commands.yaml
|
# Location: {project_dir}/.autoforge/allowed_commands.yaml
|
||||||
#
|
#
|
||||||
# This file defines bash commands that the autonomous coding agent can use
|
# This file defines bash commands that the autonomous coding agent can use
|
||||||
# for THIS SPECIFIC PROJECT, beyond the default allowed commands.
|
# for THIS SPECIFIC PROJECT, beyond the default allowed commands.
|
||||||
#
|
#
|
||||||
# When you create a new project, AutoCoder automatically creates this file
|
# When you create a new project, AutoForge automatically creates this file
|
||||||
# in your project's .autocoder/ directory. You can customize it for your
|
# in your project's .autoforge/ directory. You can customize it for your
|
||||||
# project's specific needs (iOS, Rust, Python, etc.).
|
# project's specific needs (iOS, Rust, Python, etc.).
|
||||||
|
|
||||||
version: 1
|
version: 1
|
||||||
@@ -115,7 +115,7 @@ commands: []
|
|||||||
# Limits:
|
# Limits:
|
||||||
# - Maximum 100 commands per project
|
# - Maximum 100 commands per project
|
||||||
# - Commands in the blocklist (sudo, dd, shutdown, etc.) can NEVER be allowed
|
# - Commands in the blocklist (sudo, dd, shutdown, etc.) can NEVER be allowed
|
||||||
# - Org-level blocked commands (see ~/.autocoder/config.yaml) cannot be overridden
|
# - Org-level blocked commands (see ~/.autoforge/config.yaml) cannot be overridden
|
||||||
#
|
#
|
||||||
# Default Allowed Commands (always available):
|
# Default Allowed Commands (always available):
|
||||||
# File operations: ls, cat, head, tail, wc, grep, cp, mkdir, mv, rm, touch
|
# File operations: ls, cat, head, tail, wc, grep, cp, mkdir, mv, rm, touch
|
||||||
|
|||||||
791
lib/cli.js
Normal file
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];
|
||||||
|
}
|
||||||
53
package.json
Normal file
53
package.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "autoforge-ai",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"description": "Autonomous coding agent with web UI - build complete apps with AI",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"bin": {
|
||||||
|
"autoforge": "./bin/autoforge.js"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"bin/",
|
||||||
|
"lib/",
|
||||||
|
"api/",
|
||||||
|
"server/",
|
||||||
|
"mcp_server/",
|
||||||
|
"ui/dist/",
|
||||||
|
"ui/package.json",
|
||||||
|
".claude/commands/",
|
||||||
|
".claude/templates/",
|
||||||
|
"examples/",
|
||||||
|
"start.py",
|
||||||
|
"agent.py",
|
||||||
|
"auth.py",
|
||||||
|
"autoforge_paths.py",
|
||||||
|
"autonomous_agent_demo.py",
|
||||||
|
"client.py",
|
||||||
|
"env_constants.py",
|
||||||
|
"parallel_orchestrator.py",
|
||||||
|
"progress.py",
|
||||||
|
"prompts.py",
|
||||||
|
"registry.py",
|
||||||
|
"rate_limit_utils.py",
|
||||||
|
"security.py",
|
||||||
|
"requirements-prod.txt",
|
||||||
|
"pyproject.toml",
|
||||||
|
".env.example",
|
||||||
|
"!**/__pycache__/",
|
||||||
|
"!**/*.pyc"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"ai",
|
||||||
|
"coding-agent",
|
||||||
|
"claude",
|
||||||
|
"autonomous",
|
||||||
|
"code-generation"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"prepublishOnly": "npm --prefix ui install && npm --prefix ui run build"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,11 +40,11 @@ from server.utils.process_utils import kill_process_tree
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Root directory of autocoder (where this script and autonomous_agent_demo.py live)
|
# Root directory of autoforge (where this script and autonomous_agent_demo.py live)
|
||||||
AUTOCODER_ROOT = Path(__file__).parent.resolve()
|
AUTOFORGE_ROOT = Path(__file__).parent.resolve()
|
||||||
|
|
||||||
# Debug log file path
|
# Debug log file path
|
||||||
DEBUG_LOG_FILE = AUTOCODER_ROOT / "orchestrator_debug.log"
|
DEBUG_LOG_FILE = AUTOFORGE_ROOT / "orchestrator_debug.log"
|
||||||
|
|
||||||
|
|
||||||
class DebugLogger:
|
class DebugLogger:
|
||||||
@@ -823,7 +823,7 @@ class ParallelOrchestrator:
|
|||||||
cmd = [
|
cmd = [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
"-u", # Force unbuffered stdout/stderr
|
"-u", # Force unbuffered stdout/stderr
|
||||||
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
|
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
|
||||||
"--project-dir", str(self.project_dir),
|
"--project-dir", str(self.project_dir),
|
||||||
"--max-iterations", "1",
|
"--max-iterations", "1",
|
||||||
"--agent-type", "coding",
|
"--agent-type", "coding",
|
||||||
@@ -845,7 +845,7 @@ class ParallelOrchestrator:
|
|||||||
"text": True,
|
"text": True,
|
||||||
"encoding": "utf-8",
|
"encoding": "utf-8",
|
||||||
"errors": "replace",
|
"errors": "replace",
|
||||||
"cwd": str(AUTOCODER_ROOT), # Run from autocoder root for proper imports
|
"cwd": str(self.project_dir), # Run from project dir so CLI creates .claude/ in project
|
||||||
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
|
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
|
||||||
}
|
}
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
@@ -889,7 +889,7 @@ class ParallelOrchestrator:
|
|||||||
cmd = [
|
cmd = [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
"-u",
|
"-u",
|
||||||
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
|
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
|
||||||
"--project-dir", str(self.project_dir),
|
"--project-dir", str(self.project_dir),
|
||||||
"--max-iterations", "1",
|
"--max-iterations", "1",
|
||||||
"--agent-type", "coding",
|
"--agent-type", "coding",
|
||||||
@@ -908,7 +908,7 @@ class ParallelOrchestrator:
|
|||||||
"text": True,
|
"text": True,
|
||||||
"encoding": "utf-8",
|
"encoding": "utf-8",
|
||||||
"errors": "replace",
|
"errors": "replace",
|
||||||
"cwd": str(AUTOCODER_ROOT),
|
"cwd": str(self.project_dir), # Run from project dir so CLI creates .claude/ in project
|
||||||
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
|
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
|
||||||
}
|
}
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
@@ -992,7 +992,7 @@ class ParallelOrchestrator:
|
|||||||
cmd = [
|
cmd = [
|
||||||
sys.executable,
|
sys.executable,
|
||||||
"-u",
|
"-u",
|
||||||
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
|
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
|
||||||
"--project-dir", str(self.project_dir),
|
"--project-dir", str(self.project_dir),
|
||||||
"--max-iterations", "1",
|
"--max-iterations", "1",
|
||||||
"--agent-type", "testing",
|
"--agent-type", "testing",
|
||||||
@@ -1012,7 +1012,7 @@ class ParallelOrchestrator:
|
|||||||
"text": True,
|
"text": True,
|
||||||
"encoding": "utf-8",
|
"encoding": "utf-8",
|
||||||
"errors": "replace",
|
"errors": "replace",
|
||||||
"cwd": str(AUTOCODER_ROOT),
|
"cwd": str(self.project_dir), # Run from project dir so CLI creates .claude/ in project
|
||||||
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
|
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
|
||||||
}
|
}
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
@@ -1053,7 +1053,7 @@ class ParallelOrchestrator:
|
|||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
sys.executable, "-u",
|
sys.executable, "-u",
|
||||||
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
|
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
|
||||||
"--project-dir", str(self.project_dir),
|
"--project-dir", str(self.project_dir),
|
||||||
"--agent-type", "initializer",
|
"--agent-type", "initializer",
|
||||||
"--max-iterations", "1",
|
"--max-iterations", "1",
|
||||||
@@ -1073,7 +1073,7 @@ class ParallelOrchestrator:
|
|||||||
"text": True,
|
"text": True,
|
||||||
"encoding": "utf-8",
|
"encoding": "utf-8",
|
||||||
"errors": "replace",
|
"errors": "replace",
|
||||||
"cwd": str(AUTOCODER_ROOT),
|
"cwd": str(AUTOFORGE_ROOT),
|
||||||
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
|
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
|
||||||
}
|
}
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ def has_features(project_dir: Path) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# Check SQLite database
|
# Check SQLite database
|
||||||
from autocoder_paths import get_features_db_path
|
from autoforge_paths import get_features_db_path
|
||||||
db_file = get_features_db_path(project_dir)
|
db_file = get_features_db_path(project_dir)
|
||||||
if not db_file.exists():
|
if not db_file.exists():
|
||||||
return False
|
return False
|
||||||
@@ -72,7 +72,7 @@ def count_passing_tests(project_dir: Path) -> tuple[int, int, int]:
|
|||||||
Returns:
|
Returns:
|
||||||
(passing_count, in_progress_count, total_count)
|
(passing_count, in_progress_count, total_count)
|
||||||
"""
|
"""
|
||||||
from autocoder_paths import get_features_db_path
|
from autoforge_paths import get_features_db_path
|
||||||
db_file = get_features_db_path(project_dir)
|
db_file = get_features_db_path(project_dir)
|
||||||
if not db_file.exists():
|
if not db_file.exists():
|
||||||
return 0, 0, 0
|
return 0, 0, 0
|
||||||
@@ -122,7 +122,7 @@ def get_all_passing_features(project_dir: Path) -> list[dict]:
|
|||||||
Returns:
|
Returns:
|
||||||
List of dicts with id, category, name for each passing feature
|
List of dicts with id, category, name for each passing feature
|
||||||
"""
|
"""
|
||||||
from autocoder_paths import get_features_db_path
|
from autoforge_paths import get_features_db_path
|
||||||
db_file = get_features_db_path(project_dir)
|
db_file = get_features_db_path(project_dir)
|
||||||
if not db_file.exists():
|
if not db_file.exists():
|
||||||
return []
|
return []
|
||||||
@@ -147,7 +147,7 @@ def send_progress_webhook(passing: int, total: int, project_dir: Path) -> None:
|
|||||||
if not WEBHOOK_URL:
|
if not WEBHOOK_URL:
|
||||||
return # Webhook not configured
|
return # Webhook not configured
|
||||||
|
|
||||||
from autocoder_paths import get_progress_cache_path
|
from autoforge_paths import get_progress_cache_path
|
||||||
cache_file = get_progress_cache_path(project_dir)
|
cache_file = get_progress_cache_path(project_dir)
|
||||||
previous = 0
|
previous = 0
|
||||||
previous_passing_ids = set()
|
previous_passing_ids = set()
|
||||||
|
|||||||
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:
|
def get_project_prompts_dir(project_dir: Path) -> Path:
|
||||||
"""Get the prompts directory for a specific project."""
|
"""Get the prompts directory for a specific project."""
|
||||||
from autocoder_paths import get_prompts_dir
|
from autoforge_paths import get_prompts_dir
|
||||||
return get_prompts_dir(project_dir)
|
return get_prompts_dir(project_dir)
|
||||||
|
|
||||||
|
|
||||||
@@ -315,9 +315,9 @@ def scaffold_project_prompts(project_dir: Path) -> Path:
|
|||||||
project_prompts = get_project_prompts_dir(project_dir)
|
project_prompts = get_project_prompts_dir(project_dir)
|
||||||
project_prompts.mkdir(parents=True, exist_ok=True)
|
project_prompts.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Create .autocoder directory with .gitignore for runtime files
|
# Create .autoforge directory with .gitignore for runtime files
|
||||||
from autocoder_paths import ensure_autocoder_dir
|
from autoforge_paths import ensure_autoforge_dir
|
||||||
autocoder_dir = ensure_autocoder_dir(project_dir)
|
autoforge_dir = ensure_autoforge_dir(project_dir)
|
||||||
|
|
||||||
# Define template mappings: (source_template, destination_name)
|
# Define template mappings: (source_template, destination_name)
|
||||||
templates = [
|
templates = [
|
||||||
@@ -340,14 +340,14 @@ def scaffold_project_prompts(project_dir: Path) -> Path:
|
|||||||
except (OSError, PermissionError) as e:
|
except (OSError, PermissionError) as e:
|
||||||
print(f" Warning: Could not copy {dest_name}: {e}")
|
print(f" Warning: Could not copy {dest_name}: {e}")
|
||||||
|
|
||||||
# Copy allowed_commands.yaml template to .autocoder/
|
# Copy allowed_commands.yaml template to .autoforge/
|
||||||
examples_dir = Path(__file__).parent / "examples"
|
examples_dir = Path(__file__).parent / "examples"
|
||||||
allowed_commands_template = examples_dir / "project_allowed_commands.yaml"
|
allowed_commands_template = examples_dir / "project_allowed_commands.yaml"
|
||||||
allowed_commands_dest = autocoder_dir / "allowed_commands.yaml"
|
allowed_commands_dest = autoforge_dir / "allowed_commands.yaml"
|
||||||
if allowed_commands_template.exists() and not allowed_commands_dest.exists():
|
if allowed_commands_template.exists() and not allowed_commands_dest.exists():
|
||||||
try:
|
try:
|
||||||
shutil.copy(allowed_commands_template, allowed_commands_dest)
|
shutil.copy(allowed_commands_template, allowed_commands_dest)
|
||||||
copied_files.append(".autocoder/allowed_commands.yaml")
|
copied_files.append(".autoforge/allowed_commands.yaml")
|
||||||
except (OSError, PermissionError) as e:
|
except (OSError, PermissionError) as e:
|
||||||
print(f" Warning: Could not copy allowed_commands.yaml: {e}")
|
print(f" Warning: Could not copy allowed_commands.yaml: {e}")
|
||||||
|
|
||||||
|
|||||||
27
registry.py
27
registry.py
@@ -3,7 +3,7 @@ Project Registry Module
|
|||||||
=======================
|
=======================
|
||||||
|
|
||||||
Cross-platform project registry for storing project name to path mappings.
|
Cross-platform project registry for storing project name to path mappings.
|
||||||
Uses SQLite database stored at ~/.autocoder/registry.db.
|
Uses SQLite database stored at ~/.autoforge/registry.db.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -23,6 +23,22 @@ from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_registry_dir() -> None:
|
||||||
|
"""Migrate ~/.autocoder/ to ~/.autoforge/ if needed.
|
||||||
|
|
||||||
|
Provides backward compatibility by automatically renaming the old
|
||||||
|
config directory to the new location on first access.
|
||||||
|
"""
|
||||||
|
old_dir = Path.home() / ".autocoder"
|
||||||
|
new_dir = Path.home() / ".autoforge"
|
||||||
|
if old_dir.exists() and not new_dir.exists():
|
||||||
|
try:
|
||||||
|
old_dir.rename(new_dir)
|
||||||
|
logger.info("Migrated registry directory: ~/.autocoder/ -> ~/.autoforge/")
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Failed to migrate ~/.autocoder/ to ~/.autoforge/", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Model Configuration (Single Source of Truth)
|
# Model Configuration (Single Source of Truth)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -120,12 +136,15 @@ _engine_lock = threading.Lock()
|
|||||||
|
|
||||||
def get_config_dir() -> Path:
|
def get_config_dir() -> Path:
|
||||||
"""
|
"""
|
||||||
Get the config directory: ~/.autocoder/
|
Get the config directory: ~/.autoforge/
|
||||||
|
|
||||||
|
Automatically migrates from ~/.autocoder/ if needed.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to ~/.autocoder/ (created if it doesn't exist)
|
Path to ~/.autoforge/ (created if it doesn't exist)
|
||||||
"""
|
"""
|
||||||
config_dir = Path.home() / ".autocoder"
|
_migrate_registry_dir()
|
||||||
|
config_dir = Path.home() / ".autoforge"
|
||||||
config_dir.mkdir(parents=True, exist_ok=True)
|
config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
return config_dir
|
return config_dir
|
||||||
|
|
||||||
|
|||||||
14
requirements-prod.txt
Normal file
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.
|
Get the organization-level config file path.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to ~/.autocoder/config.yaml
|
Path to ~/.autoforge/config.yaml (falls back to ~/.autocoder/config.yaml)
|
||||||
"""
|
"""
|
||||||
return Path.home() / ".autocoder" / "config.yaml"
|
new_path = Path.home() / ".autoforge" / "config.yaml"
|
||||||
|
if new_path.exists():
|
||||||
|
return new_path
|
||||||
|
# Backward compatibility: check old location
|
||||||
|
old_path = Path.home() / ".autocoder" / "config.yaml"
|
||||||
|
if old_path.exists():
|
||||||
|
return old_path
|
||||||
|
return new_path
|
||||||
|
|
||||||
|
|
||||||
def load_org_config() -> Optional[dict]:
|
def load_org_config() -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
Load organization-level config from ~/.autocoder/config.yaml.
|
Load organization-level config from ~/.autoforge/config.yaml.
|
||||||
|
|
||||||
|
Falls back to ~/.autocoder/config.yaml for backward compatibility.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with parsed org config, or None if file doesn't exist or is invalid
|
Dict with parsed org config, or None if file doesn't exist or is invalid
|
||||||
@@ -630,6 +639,9 @@ def load_project_commands(project_dir: Path) -> Optional[dict]:
|
|||||||
Returns:
|
Returns:
|
||||||
Dict with parsed YAML config, or None if file doesn't exist or is invalid
|
Dict with parsed YAML config, or None if file doesn't exist or is invalid
|
||||||
"""
|
"""
|
||||||
|
# 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"
|
config_path = project_dir.resolve() / ".autocoder" / "allowed_commands.yaml"
|
||||||
|
|
||||||
if not config_path.exists():
|
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
|
# Provide helpful error message with config hint
|
||||||
error_msg = f"Command '{cmd}' is not allowed.\n"
|
error_msg = f"Command '{cmd}' is not allowed.\n"
|
||||||
error_msg += "To allow this command:\n"
|
error_msg += "To allow this command:\n"
|
||||||
error_msg += " 1. Add to .autocoder/allowed_commands.yaml for this project, OR\n"
|
error_msg += " 1. Add to .autoforge/allowed_commands.yaml for this project, OR\n"
|
||||||
error_msg += " 2. Request mid-session approval (the agent can ask)\n"
|
error_msg += " 2. Request mid-session approval (the agent can ask)\n"
|
||||||
error_msg += "Note: Some commands are blocked at org-level and cannot be overridden."
|
error_msg += "Note: Some commands are blocked at org-level and cannot be overridden."
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# Check if remote access is enabled via environment variable
|
# Check if remote access is enabled via environment variable
|
||||||
# Set by start_ui.py when --host is not 127.0.0.1
|
# Set by start_ui.py when --host is not 127.0.0.1
|
||||||
ALLOW_REMOTE = os.environ.get("AUTOCODER_ALLOW_REMOTE", "").lower() in ("1", "true", "yes")
|
ALLOW_REMOTE = os.environ.get("AUTOFORGE_ALLOW_REMOTE", "").lower() in ("1", "true", "yes")
|
||||||
|
|
||||||
if ALLOW_REMOTE:
|
if ALLOW_REMOTE:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -133,7 +133,7 @@ else:
|
|||||||
if not ALLOW_REMOTE:
|
if not ALLOW_REMOTE:
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def require_localhost(request: Request, call_next):
|
async def require_localhost(request: Request, call_next):
|
||||||
"""Only allow requests from localhost (disabled when AUTOCODER_ALLOW_REMOTE=1)."""
|
"""Only allow requests from localhost (disabled when AUTOFORGE_ALLOW_REMOTE=1)."""
|
||||||
client_host = request.client.host if request.client else None
|
client_host = request.client.host if request.client else None
|
||||||
|
|
||||||
# Allow localhost connections
|
# Allow localhost connections
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Uses project registry for path lookups and project_config for command detection.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -72,6 +73,116 @@ def get_project_dir(project_name: str) -> Path:
|
|||||||
|
|
||||||
return project_dir
|
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):
|
def get_project_devserver_manager(project_name: str):
|
||||||
"""
|
"""
|
||||||
@@ -180,8 +291,11 @@ async def start_devserver(
|
|||||||
# Determine which command to use
|
# Determine which command to use
|
||||||
command: str | None
|
command: str | None
|
||||||
if request.command:
|
if request.command:
|
||||||
command = request.command
|
raise HTTPException(
|
||||||
else:
|
status_code=400,
|
||||||
|
detail="Direct command execution is disabled. Use /config to set a safe custom_command."
|
||||||
|
)
|
||||||
|
|
||||||
command = get_dev_command(project_dir)
|
command = get_dev_command(project_dir)
|
||||||
|
|
||||||
if not command:
|
if not command:
|
||||||
@@ -193,6 +307,13 @@ async def start_devserver(
|
|||||||
# Validate command against security allowlist before execution
|
# Validate command against security allowlist before execution
|
||||||
validate_dev_command(command, project_dir)
|
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
|
# Now command is definitely str and validated
|
||||||
success, message = await manager.start(command)
|
success, message = await manager.start(command)
|
||||||
|
|
||||||
@@ -284,7 +405,13 @@ async def update_devserver_config(
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
else:
|
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)
|
validate_dev_command(update.custom_command, project_dir)
|
||||||
|
|
||||||
# Set the custom command
|
# Set the custom command
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ async def expand_project_websocket(websocket: WebSocket, project_name: str):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Verify project has app_spec.txt
|
# Verify project has app_spec.txt
|
||||||
from autocoder_paths import get_prompts_dir
|
from autoforge_paths import get_prompts_dir
|
||||||
spec_path = get_prompts_dir(project_dir) / "app_spec.txt"
|
spec_path = get_prompts_dir(project_dir) / "app_spec.txt"
|
||||||
if not spec_path.exists():
|
if not spec_path.exists():
|
||||||
await websocket.close(code=4004, reason="Project has no spec. Create spec first.")
|
await websocket.close(code=4004, reason="Project has no spec. Create spec first.")
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ async def list_features(project_name: str):
|
|||||||
if not project_dir.exists():
|
if not project_dir.exists():
|
||||||
raise HTTPException(status_code=404, detail="Project directory not found")
|
raise HTTPException(status_code=404, detail="Project directory not found")
|
||||||
|
|
||||||
from autocoder_paths import get_features_db_path
|
from autoforge_paths import get_features_db_path
|
||||||
db_file = get_features_db_path(project_dir)
|
db_file = get_features_db_path(project_dir)
|
||||||
if not db_file.exists():
|
if not db_file.exists():
|
||||||
return FeatureListResponse(pending=[], in_progress=[], done=[])
|
return FeatureListResponse(pending=[], in_progress=[], done=[])
|
||||||
@@ -322,7 +322,7 @@ async def get_dependency_graph(project_name: str):
|
|||||||
if not project_dir.exists():
|
if not project_dir.exists():
|
||||||
raise HTTPException(status_code=404, detail="Project directory not found")
|
raise HTTPException(status_code=404, detail="Project directory not found")
|
||||||
|
|
||||||
from autocoder_paths import get_features_db_path
|
from autoforge_paths import get_features_db_path
|
||||||
db_file = get_features_db_path(project_dir)
|
db_file = get_features_db_path(project_dir)
|
||||||
if not db_file.exists():
|
if not db_file.exists():
|
||||||
return DependencyGraphResponse(nodes=[], edges=[])
|
return DependencyGraphResponse(nodes=[], edges=[])
|
||||||
@@ -388,7 +388,7 @@ async def get_feature(project_name: str, feature_id: int):
|
|||||||
if not project_dir.exists():
|
if not project_dir.exists():
|
||||||
raise HTTPException(status_code=404, detail="Project directory not found")
|
raise HTTPException(status_code=404, detail="Project directory not found")
|
||||||
|
|
||||||
from autocoder_paths import get_features_db_path
|
from autoforge_paths import get_features_db_path
|
||||||
db_file = get_features_db_path(project_dir)
|
db_file = get_features_db_path(project_dir)
|
||||||
if not db_file.exists():
|
if not db_file.exists():
|
||||||
raise HTTPException(status_code=404, detail="No features database found")
|
raise HTTPException(status_code=404, detail="No features database found")
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ async def delete_project(name: str, delete_files: bool = False):
|
|||||||
raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
|
raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
|
||||||
|
|
||||||
# Check if agent is running
|
# Check if agent is running
|
||||||
from autocoder_paths import has_agent_running
|
from autoforge_paths import has_agent_running
|
||||||
if has_agent_running(project_dir):
|
if has_agent_running(project_dir):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=409,
|
status_code=409,
|
||||||
@@ -407,7 +407,7 @@ async def reset_project(name: str, full_reset: bool = False):
|
|||||||
raise HTTPException(status_code=404, detail="Project directory not found")
|
raise HTTPException(status_code=404, detail="Project directory not found")
|
||||||
|
|
||||||
# Check if agent is running
|
# Check if agent is running
|
||||||
from autocoder_paths import has_agent_running
|
from autoforge_paths import has_agent_running
|
||||||
if has_agent_running(project_dir):
|
if has_agent_running(project_dir):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=409,
|
status_code=409,
|
||||||
@@ -424,7 +424,7 @@ async def reset_project(name: str, full_reset: bool = False):
|
|||||||
|
|
||||||
deleted_files: list[str] = []
|
deleted_files: list[str] = []
|
||||||
|
|
||||||
from autocoder_paths import (
|
from autoforge_paths import (
|
||||||
get_assistant_db_path,
|
get_assistant_db_path,
|
||||||
get_claude_assistant_settings_path,
|
get_claude_assistant_settings_path,
|
||||||
get_claude_settings_path,
|
get_claude_settings_path,
|
||||||
@@ -466,7 +466,7 @@ async def reset_project(name: str, full_reset: bool = False):
|
|||||||
|
|
||||||
# Full reset: also delete prompts directory
|
# Full reset: also delete prompts directory
|
||||||
if full_reset:
|
if full_reset:
|
||||||
from autocoder_paths import get_prompts_dir
|
from autoforge_paths import get_prompts_dir
|
||||||
# Delete prompts from both possible locations
|
# Delete prompts from both possible locations
|
||||||
for prompts_dir in [get_prompts_dir(project_dir), project_dir / "prompts"]:
|
for prompts_dir in [get_prompts_dir(project_dir), project_dir / "prompts"]:
|
||||||
if prompts_dir.exists():
|
if prompts_dir.exists():
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ async def get_spec_file_status(project_name: str):
|
|||||||
if not project_dir.exists():
|
if not project_dir.exists():
|
||||||
raise HTTPException(status_code=404, detail="Project directory not found")
|
raise HTTPException(status_code=404, detail="Project directory not found")
|
||||||
|
|
||||||
from autocoder_paths import get_prompts_dir
|
from autoforge_paths import get_prompts_dir
|
||||||
status_file = get_prompts_dir(project_dir) / ".spec_status.json"
|
status_file = get_prompts_dir(project_dir) / ".spec_status.json"
|
||||||
|
|
||||||
if not status_file.exists():
|
if not status_file.exists():
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ def get_system_prompt(project_name: str, project_dir: Path) -> str:
|
|||||||
"""Generate the system prompt for the assistant with project context."""
|
"""Generate the system prompt for the assistant with project context."""
|
||||||
# Try to load app_spec.txt for context
|
# Try to load app_spec.txt for context
|
||||||
app_spec_content = ""
|
app_spec_content = ""
|
||||||
from autocoder_paths import get_prompts_dir
|
from autoforge_paths import get_prompts_dir
|
||||||
app_spec_path = get_prompts_dir(project_dir) / "app_spec.txt"
|
app_spec_path = get_prompts_dir(project_dir) / "app_spec.txt"
|
||||||
if app_spec_path.exists():
|
if app_spec_path.exists():
|
||||||
try:
|
try:
|
||||||
@@ -224,7 +224,7 @@ class AssistantChatSession:
|
|||||||
"allow": permissions_list,
|
"allow": permissions_list,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
from autocoder_paths import get_claude_assistant_settings_path
|
from autoforge_paths import get_claude_assistant_settings_path
|
||||||
settings_file = get_claude_assistant_settings_path(self.project_dir)
|
settings_file = get_claude_assistant_settings_path(self.project_dir)
|
||||||
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(settings_file, "w") as f:
|
with open(settings_file, "w") as f:
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class ConversationMessage(Base):
|
|||||||
|
|
||||||
def get_db_path(project_dir: Path) -> Path:
|
def get_db_path(project_dir: Path) -> Path:
|
||||||
"""Get the path to the assistant database for a project."""
|
"""Get the path to the assistant database for a project."""
|
||||||
from autocoder_paths import get_assistant_db_path
|
from autoforge_paths import get_assistant_db_path
|
||||||
return get_assistant_db_path(project_dir)
|
return get_assistant_db_path(project_dir)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from pathlib import Path
|
|||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
# Root directory of the autocoder project (repository root).
|
# Root directory of the autoforge project (repository root).
|
||||||
# Used throughout the server package whenever the repo root is needed.
|
# Used throughout the server package whenever the repo root is needed.
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
ROOT_DIR = Path(__file__).parent.parent.parent
|
ROOT_DIR = Path(__file__).parent.parent.parent
|
||||||
|
|||||||
@@ -14,17 +14,17 @@ This is a simplified version of AgentProcessManager, tailored for dev servers:
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Awaitable, Callable, Literal, Set
|
from typing import Awaitable, Callable, Literal, Set
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
from registry import list_registered_projects
|
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
|
from server.utils.process_utils import kill_process_tree
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -115,7 +115,7 @@ class DevServerProcessManager:
|
|||||||
self._callbacks_lock = threading.Lock()
|
self._callbacks_lock = threading.Lock()
|
||||||
|
|
||||||
# Lock file to prevent multiple instances (stored in project directory)
|
# Lock file to prevent multiple instances (stored in project directory)
|
||||||
from autocoder_paths import get_devserver_lock_path
|
from autoforge_paths import get_devserver_lock_path
|
||||||
self.lock_file = get_devserver_lock_path(self.project_dir)
|
self.lock_file = get_devserver_lock_path(self.project_dir)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -291,53 +291,54 @@ class DevServerProcessManager:
|
|||||||
Start the dev server as a subprocess.
|
Start the dev server as a subprocess.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
command: The shell command to run (e.g., "npm run dev")
|
command: The command to run (e.g., "npm run dev")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (success, message)
|
Tuple of (success, message)
|
||||||
"""
|
"""
|
||||||
if self.status == "running":
|
# Already running?
|
||||||
|
if self.process and self.status == "running":
|
||||||
return False, "Dev server is already running"
|
return False, "Dev server is already running"
|
||||||
|
|
||||||
|
# Lock check (prevents double-start)
|
||||||
if not self._check_lock():
|
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
|
command = (command or "").strip()
|
||||||
if not self.project_dir.exists():
|
if not command:
|
||||||
return False, f"Project directory does not exist: {self.project_dir}"
|
return False, "Empty dev server command"
|
||||||
|
|
||||||
# Defense-in-depth: validate command against security allowlist
|
# SECURITY: block shell operators/metacharacters (defense-in-depth)
|
||||||
commands = extract_commands(command)
|
# NOTE: On Windows, .cmd/.bat files are executed via cmd.exe even with
|
||||||
if not commands:
|
# shell=False (CPython limitation), so metacharacter blocking is critical.
|
||||||
return False, "Could not parse command for security validation"
|
# 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)
|
# Parse into argv and execute without shell
|
||||||
for cmd in commands:
|
argv = shlex.split(command, posix=(sys.platform != "win32"))
|
||||||
if cmd in blocked_commands:
|
if not argv:
|
||||||
logger.warning("Blocked dev server command '%s' (in blocklist) for %s", cmd, self.project_name)
|
return False, "Empty dev server command"
|
||||||
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"
|
|
||||||
|
|
||||||
self._command = command
|
base = Path(argv[0]).name.lower()
|
||||||
self._detected_url = None # Reset URL detection
|
|
||||||
|
# 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:
|
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":
|
if sys.platform == "win32":
|
||||||
self.process = subprocess.Popen(
|
self.process = subprocess.Popen(
|
||||||
shell_cmd,
|
argv,
|
||||||
stdin=subprocess.DEVNULL,
|
stdin=subprocess.DEVNULL,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
@@ -346,23 +347,33 @@ class DevServerProcessManager:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.process = subprocess.Popen(
|
self.process = subprocess.Popen(
|
||||||
shell_cmd,
|
argv,
|
||||||
stdin=subprocess.DEVNULL,
|
stdin=subprocess.DEVNULL,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
cwd=str(self.project_dir),
|
cwd=str(self.project_dir),
|
||||||
)
|
)
|
||||||
|
|
||||||
self._create_lock()
|
self._command = command
|
||||||
self.started_at = datetime.now()
|
self.started_at = datetime.now(timezone.utc)
|
||||||
self.status = "running"
|
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())
|
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:
|
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}"
|
return False, f"Failed to start dev server: {e}"
|
||||||
|
|
||||||
async def stop(self) -> tuple[bool, str]:
|
async def stop(self) -> tuple[bool, str]:
|
||||||
@@ -504,10 +515,10 @@ def cleanup_orphaned_devserver_locks() -> int:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Check both legacy and new locations for lock files
|
# Check both legacy and new locations for lock files
|
||||||
from autocoder_paths import get_autocoder_dir
|
from autoforge_paths import get_autoforge_dir
|
||||||
lock_locations = [
|
lock_locations = [
|
||||||
project_path / ".devserver.lock",
|
project_path / ".devserver.lock",
|
||||||
get_autocoder_dir(project_path) / ".devserver.lock",
|
get_autoforge_dir(project_path) / ".devserver.lock",
|
||||||
]
|
]
|
||||||
lock_file = None
|
lock_file = None
|
||||||
for candidate in lock_locations:
|
for candidate in lock_locations:
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class ExpandChatSession:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Verify project has existing spec
|
# Verify project has existing spec
|
||||||
from autocoder_paths import get_prompts_dir
|
from autoforge_paths import get_prompts_dir
|
||||||
spec_path = get_prompts_dir(self.project_dir) / "app_spec.txt"
|
spec_path = get_prompts_dir(self.project_dir) / "app_spec.txt"
|
||||||
if not spec_path.exists():
|
if not spec_path.exists():
|
||||||
yield {
|
yield {
|
||||||
@@ -142,7 +142,7 @@ class ExpandChatSession:
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
from autocoder_paths import get_expand_settings_path
|
from autoforge_paths import get_expand_settings_path
|
||||||
settings_file = get_expand_settings_path(self.project_dir, uuid.uuid4().hex)
|
settings_file = get_expand_settings_path(self.project_dir, uuid.uuid4().hex)
|
||||||
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
self._settings_file = settings_file
|
self._settings_file = settings_file
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class AgentProcessManager:
|
|||||||
self._callbacks_lock = threading.Lock()
|
self._callbacks_lock = threading.Lock()
|
||||||
|
|
||||||
# Lock file to prevent multiple instances (stored in project directory)
|
# Lock file to prevent multiple instances (stored in project directory)
|
||||||
from autocoder_paths import get_agent_lock_path
|
from autoforge_paths import get_agent_lock_path
|
||||||
self.lock_file = get_agent_lock_path(self.project_dir)
|
self.lock_file = get_agent_lock_path(self.project_dir)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -587,10 +587,10 @@ def cleanup_orphaned_locks() -> int:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Check both legacy and new locations for lock files
|
# Check both legacy and new locations for lock files
|
||||||
from autocoder_paths import get_autocoder_dir
|
from autoforge_paths import get_autoforge_dir
|
||||||
lock_locations = [
|
lock_locations = [
|
||||||
project_path / ".agent.lock",
|
project_path / ".agent.lock",
|
||||||
get_autocoder_dir(project_path) / ".agent.lock",
|
get_autoforge_dir(project_path) / ".agent.lock",
|
||||||
]
|
]
|
||||||
lock_file = None
|
lock_file = None
|
||||||
for candidate in lock_locations:
|
for candidate in lock_locations:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Handles project type detection and dev command configuration.
|
|||||||
Detects project types by scanning for configuration files and provides
|
Detects project types by scanning for configuration files and provides
|
||||||
default or custom dev commands for each project.
|
default or custom dev commands for each project.
|
||||||
|
|
||||||
Configuration is stored in {project_dir}/.autocoder/config.json.
|
Configuration is stored in {project_dir}/.autoforge/config.json.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -88,13 +88,22 @@ def _get_config_path(project_dir: Path) -> Path:
|
|||||||
"""
|
"""
|
||||||
Get the path to the project config file.
|
Get the path to the project config file.
|
||||||
|
|
||||||
|
Checks the new .autoforge/ location first, falls back to .autocoder/
|
||||||
|
for backward compatibility.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
project_dir: Path to the project directory.
|
project_dir: Path to the project directory.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Path to the .autocoder/config.json file.
|
Path to the config.json file in the appropriate directory.
|
||||||
"""
|
"""
|
||||||
return project_dir / ".autocoder" / "config.json"
|
new_path = project_dir / ".autoforge" / "config.json"
|
||||||
|
if new_path.exists():
|
||||||
|
return new_path
|
||||||
|
old_path = project_dir / ".autocoder" / "config.json"
|
||||||
|
if old_path.exists():
|
||||||
|
return old_path
|
||||||
|
return new_path
|
||||||
|
|
||||||
|
|
||||||
def _load_config(project_dir: Path) -> dict:
|
def _load_config(project_dir: Path) -> dict:
|
||||||
@@ -137,7 +146,7 @@ def _save_config(project_dir: Path, config: dict) -> None:
|
|||||||
"""
|
"""
|
||||||
Save the project configuration to disk.
|
Save the project configuration to disk.
|
||||||
|
|
||||||
Creates the .autocoder directory if it doesn't exist.
|
Creates the .autoforge directory if it doesn't exist.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
project_dir: Path to the project directory.
|
project_dir: Path to the project directory.
|
||||||
@@ -148,7 +157,7 @@ def _save_config(project_dir: Path, config: dict) -> None:
|
|||||||
"""
|
"""
|
||||||
config_path = _get_config_path(project_dir)
|
config_path = _get_config_path(project_dir)
|
||||||
|
|
||||||
# Ensure the .autocoder directory exists
|
# Ensure the .autoforge directory exists
|
||||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -408,11 +417,11 @@ def clear_dev_command(project_dir: Path) -> None:
|
|||||||
config_path.unlink(missing_ok=True)
|
config_path.unlink(missing_ok=True)
|
||||||
logger.info("Removed empty config file for %s", project_dir.name)
|
logger.info("Removed empty config file for %s", project_dir.name)
|
||||||
|
|
||||||
# Also remove .autocoder directory if empty
|
# Also remove .autoforge directory if empty
|
||||||
autocoder_dir = config_path.parent
|
autoforge_dir = config_path.parent
|
||||||
if autocoder_dir.exists() and not any(autocoder_dir.iterdir()):
|
if autoforge_dir.exists() and not any(autoforge_dir.iterdir()):
|
||||||
autocoder_dir.rmdir()
|
autoforge_dir.rmdir()
|
||||||
logger.debug("Removed empty .autocoder directory for %s", project_dir.name)
|
logger.debug("Removed empty .autoforge directory for %s", project_dir.name)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.warning("Failed to clean up config for %s: %s", project_dir.name, e)
|
logger.warning("Failed to clean up config for %s: %s", project_dir.name, e)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class SchedulerService:
|
|||||||
async def _load_project_schedules(self, project_name: str, project_dir: Path) -> int:
|
async def _load_project_schedules(self, project_name: str, project_dir: Path) -> int:
|
||||||
"""Load schedules for a single project. Returns count of schedules loaded."""
|
"""Load schedules for a single project. Returns count of schedules loaded."""
|
||||||
from api.database import Schedule, create_database
|
from api.database import Schedule, create_database
|
||||||
from autocoder_paths import get_features_db_path
|
from autoforge_paths import get_features_db_path
|
||||||
|
|
||||||
db_path = get_features_db_path(project_dir)
|
db_path = get_features_db_path(project_dir)
|
||||||
if not db_path.exists():
|
if not db_path.exists():
|
||||||
@@ -568,7 +568,7 @@ class SchedulerService:
|
|||||||
):
|
):
|
||||||
"""Check if a project should be started on server startup."""
|
"""Check if a project should be started on server startup."""
|
||||||
from api.database import Schedule, ScheduleOverride, create_database
|
from api.database import Schedule, ScheduleOverride, create_database
|
||||||
from autocoder_paths import get_features_db_path
|
from autoforge_paths import get_features_db_path
|
||||||
|
|
||||||
db_path = get_features_db_path(project_dir)
|
db_path = get_features_db_path(project_dir)
|
||||||
if not db_path.exists():
|
if not db_path.exists():
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class SpecChatSession:
|
|||||||
# Delete app_spec.txt so Claude can create it fresh
|
# Delete app_spec.txt so Claude can create it fresh
|
||||||
# The SDK requires reading existing files before writing, but app_spec.txt is created new
|
# The SDK requires reading existing files before writing, but app_spec.txt is created new
|
||||||
# Note: We keep initializer_prompt.md so Claude can read and update the template
|
# Note: We keep initializer_prompt.md so Claude can read and update the template
|
||||||
from autocoder_paths import get_prompts_dir
|
from autoforge_paths import get_prompts_dir
|
||||||
prompts_dir = get_prompts_dir(self.project_dir)
|
prompts_dir = get_prompts_dir(self.project_dir)
|
||||||
app_spec_path = prompts_dir / "app_spec.txt"
|
app_spec_path = prompts_dir / "app_spec.txt"
|
||||||
if app_spec_path.exists():
|
if app_spec_path.exists():
|
||||||
@@ -116,7 +116,7 @@ class SpecChatSession:
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
from autocoder_paths import get_claude_settings_path
|
from autoforge_paths import get_claude_settings_path
|
||||||
settings_file = get_claude_settings_path(self.project_dir)
|
settings_file = get_claude_settings_path(self.project_dir)
|
||||||
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(settings_file, "w") as f:
|
with open(settings_file, "w") as f:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ cd /d "%~dp0"
|
|||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ========================================
|
echo ========================================
|
||||||
echo Autonomous Coding Agent
|
echo AutoForge - Autonomous Coding Agent
|
||||||
echo ========================================
|
echo ========================================
|
||||||
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:
|
def display_menu(projects: list[tuple[str, Path]]) -> None:
|
||||||
"""Display the main menu."""
|
"""Display the main menu."""
|
||||||
print("\n" + "=" * 50)
|
print("\n" + "=" * 50)
|
||||||
print(" Autonomous Coding Agent Launcher")
|
print(" AutoForge - Autonomous Coding Agent")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print("\n[1] Create new project")
|
print("\n[1] Create new project")
|
||||||
|
|
||||||
|
|||||||
2
start.sh
2
start.sh
@@ -3,7 +3,7 @@ cd "$(dirname "$0")"
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "========================================"
|
echo "========================================"
|
||||||
echo " Autonomous Coding Agent"
|
echo " AutoForge - Autonomous Coding Agent"
|
||||||
echo "========================================"
|
echo "========================================"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
@echo off
|
@echo off
|
||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
REM AutoCoder UI Launcher for Windows
|
REM AutoForge UI Launcher for Windows
|
||||||
REM This script launches the web UI for the autonomous coding agent.
|
REM This script launches the web UI for the autonomous coding agent.
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ====================================
|
echo ====================================
|
||||||
echo AutoCoder UI
|
echo AutoForge UI
|
||||||
echo ====================================
|
echo ====================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
|
|||||||
12
start_ui.py
12
start_ui.py
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
AutoCoder UI Launcher
|
AutoForge UI Launcher
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
Automated launcher that handles all setup:
|
Automated launcher that handles all setup:
|
||||||
@@ -265,7 +265,7 @@ def start_dev_server(port: int, host: str = "127.0.0.1") -> tuple:
|
|||||||
# Set environment for remote access if needed
|
# Set environment for remote access if needed
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
if host != "127.0.0.1":
|
if host != "127.0.0.1":
|
||||||
env["AUTOCODER_ALLOW_REMOTE"] = "1"
|
env["AUTOFORGE_ALLOW_REMOTE"] = "1"
|
||||||
|
|
||||||
# Start FastAPI
|
# Start FastAPI
|
||||||
backend = subprocess.Popen([
|
backend = subprocess.Popen([
|
||||||
@@ -297,7 +297,7 @@ def start_production_server(port: int, host: str = "127.0.0.1"):
|
|||||||
|
|
||||||
# Enable remote access in server if not localhost
|
# Enable remote access in server if not localhost
|
||||||
if host != "127.0.0.1":
|
if host != "127.0.0.1":
|
||||||
env["AUTOCODER_ALLOW_REMOTE"] = "1"
|
env["AUTOFORGE_ALLOW_REMOTE"] = "1"
|
||||||
|
|
||||||
# NOTE: --reload is NOT used because on Windows it breaks asyncio subprocess
|
# NOTE: --reload is NOT used because on Windows it breaks asyncio subprocess
|
||||||
# support (uvicorn's reload worker doesn't inherit the ProactorEventLoop policy).
|
# support (uvicorn's reload worker doesn't inherit the ProactorEventLoop policy).
|
||||||
@@ -313,7 +313,7 @@ def start_production_server(port: int, host: str = "127.0.0.1"):
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Main entry point."""
|
"""Main entry point."""
|
||||||
parser = argparse.ArgumentParser(description="AutoCoder UI Launcher")
|
parser = argparse.ArgumentParser(description="AutoForge UI Launcher")
|
||||||
parser.add_argument("--dev", action="store_true", help="Run in development mode with Vite hot reload")
|
parser.add_argument("--dev", action="store_true", help="Run in development mode with Vite hot reload")
|
||||||
parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
|
parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
|
||||||
parser.add_argument("--port", type=int, default=None, help="Port to bind to (default: auto-detect from 8888)")
|
parser.add_argument("--port", type=int, default=None, help="Port to bind to (default: auto-detect from 8888)")
|
||||||
@@ -328,7 +328,7 @@ def main() -> None:
|
|||||||
print(" SECURITY WARNING")
|
print(" SECURITY WARNING")
|
||||||
print("!" * 50)
|
print("!" * 50)
|
||||||
print(f" Remote access enabled on host: {host}")
|
print(f" Remote access enabled on host: {host}")
|
||||||
print(" The AutoCoder UI will be accessible from other machines.")
|
print(" The AutoForge UI will be accessible from other machines.")
|
||||||
print(" Ensure you understand the security implications:")
|
print(" Ensure you understand the security implications:")
|
||||||
print(" - The agent has file system access to project directories")
|
print(" - The agent has file system access to project directories")
|
||||||
print(" - The API can start/stop agents and modify files")
|
print(" - The API can start/stop agents and modify files")
|
||||||
@@ -336,7 +336,7 @@ def main() -> None:
|
|||||||
print("!" * 50 + "\n")
|
print("!" * 50 + "\n")
|
||||||
|
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print(" AutoCoder UI Setup")
|
print(" AutoForge UI Setup")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
total_steps = 6 if not dev_mode else 5
|
total_steps = 6 if not dev_mode else 5
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
# AutoCoder UI Launcher for Unix/Linux/macOS
|
# AutoForge UI Launcher for Unix/Linux/macOS
|
||||||
# This script launches the web UI for the autonomous coding agent.
|
# This script launches the web UI for the autonomous coding agent.
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "===================================="
|
echo "===================================="
|
||||||
echo " AutoCoder UI"
|
echo " AutoForge UI"
|
||||||
echo "===================================="
|
echo "===================================="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|||||||
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:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
project_dir = Path(tmpdir)
|
project_dir = Path(tmpdir)
|
||||||
autocoder_dir = project_dir / ".autocoder"
|
autoforge_dir = project_dir / ".autoforge"
|
||||||
autocoder_dir.mkdir()
|
autoforge_dir.mkdir()
|
||||||
|
|
||||||
# Test 1: Valid YAML
|
# Test 1: Valid YAML
|
||||||
config_path = autocoder_dir / "allowed_commands.yaml"
|
config_path = autoforge_dir / "allowed_commands.yaml"
|
||||||
config_path.write_text("""version: 1
|
config_path.write_text("""version: 1
|
||||||
commands:
|
commands:
|
||||||
- name: swift
|
- name: swift
|
||||||
@@ -297,7 +297,7 @@ commands:
|
|||||||
failed += 1
|
failed += 1
|
||||||
|
|
||||||
# Test 2: Missing file returns None
|
# Test 2: Missing file returns None
|
||||||
(project_dir / ".autocoder" / "allowed_commands.yaml").unlink()
|
(project_dir / ".autoforge" / "allowed_commands.yaml").unlink()
|
||||||
config = load_project_commands(project_dir)
|
config = load_project_commands(project_dir)
|
||||||
if config is None:
|
if config is None:
|
||||||
print(" PASS: Missing file returns None")
|
print(" PASS: Missing file returns None")
|
||||||
@@ -407,11 +407,11 @@ def test_project_commands():
|
|||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
project_dir = Path(tmpdir)
|
project_dir = Path(tmpdir)
|
||||||
autocoder_dir = project_dir / ".autocoder"
|
autoforge_dir = project_dir / ".autoforge"
|
||||||
autocoder_dir.mkdir()
|
autoforge_dir.mkdir()
|
||||||
|
|
||||||
# Create a config with Swift commands
|
# Create a config with Swift commands
|
||||||
config_path = autocoder_dir / "allowed_commands.yaml"
|
config_path = autoforge_dir / "allowed_commands.yaml"
|
||||||
config_path.write_text("""version: 1
|
config_path.write_text("""version: 1
|
||||||
commands:
|
commands:
|
||||||
- name: swift
|
- name: swift
|
||||||
@@ -482,7 +482,7 @@ def test_org_config_loading():
|
|||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
# Use temporary_home for cross-platform compatibility
|
# Use temporary_home for cross-platform compatibility
|
||||||
with temporary_home(tmpdir):
|
with temporary_home(tmpdir):
|
||||||
org_dir = Path(tmpdir) / ".autocoder"
|
org_dir = Path(tmpdir) / ".autoforge"
|
||||||
org_dir.mkdir()
|
org_dir.mkdir()
|
||||||
org_config_path = org_dir / "config.yaml"
|
org_config_path = org_dir / "config.yaml"
|
||||||
|
|
||||||
@@ -576,7 +576,7 @@ def test_hierarchy_resolution():
|
|||||||
with tempfile.TemporaryDirectory() as tmpproject:
|
with tempfile.TemporaryDirectory() as tmpproject:
|
||||||
# Use temporary_home for cross-platform compatibility
|
# Use temporary_home for cross-platform compatibility
|
||||||
with temporary_home(tmphome):
|
with temporary_home(tmphome):
|
||||||
org_dir = Path(tmphome) / ".autocoder"
|
org_dir = Path(tmphome) / ".autoforge"
|
||||||
org_dir.mkdir()
|
org_dir.mkdir()
|
||||||
org_config_path = org_dir / "config.yaml"
|
org_config_path = org_dir / "config.yaml"
|
||||||
|
|
||||||
@@ -593,9 +593,9 @@ blocked_commands:
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
project_dir = Path(tmpproject)
|
project_dir = Path(tmpproject)
|
||||||
project_autocoder = project_dir / ".autocoder"
|
project_autoforge = project_dir / ".autoforge"
|
||||||
project_autocoder.mkdir()
|
project_autoforge.mkdir()
|
||||||
project_config = project_autocoder / "allowed_commands.yaml"
|
project_config = project_autoforge / "allowed_commands.yaml"
|
||||||
|
|
||||||
# Create project config
|
# Create project config
|
||||||
project_config.write_text("""version: 1
|
project_config.write_text("""version: 1
|
||||||
@@ -660,7 +660,7 @@ def test_org_blocklist_enforcement():
|
|||||||
with tempfile.TemporaryDirectory() as tmpproject:
|
with tempfile.TemporaryDirectory() as tmpproject:
|
||||||
# Use temporary_home for cross-platform compatibility
|
# Use temporary_home for cross-platform compatibility
|
||||||
with temporary_home(tmphome):
|
with temporary_home(tmphome):
|
||||||
org_dir = Path(tmphome) / ".autocoder"
|
org_dir = Path(tmphome) / ".autoforge"
|
||||||
org_dir.mkdir()
|
org_dir.mkdir()
|
||||||
org_config_path = org_dir / "config.yaml"
|
org_config_path = org_dir / "config.yaml"
|
||||||
|
|
||||||
@@ -671,8 +671,8 @@ blocked_commands:
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
project_dir = Path(tmpproject)
|
project_dir = Path(tmpproject)
|
||||||
project_autocoder = project_dir / ".autocoder"
|
project_autoforge = project_dir / ".autoforge"
|
||||||
project_autocoder.mkdir()
|
project_autoforge.mkdir()
|
||||||
|
|
||||||
# Try to use terraform (should be blocked)
|
# Try to use terraform (should be blocked)
|
||||||
input_data = {"tool_name": "Bash", "tool_input": {"command": "terraform apply"}}
|
input_data = {"tool_name": "Bash", "tool_input": {"command": "terraform apply"}}
|
||||||
@@ -735,7 +735,7 @@ def test_pkill_extensibility():
|
|||||||
with tempfile.TemporaryDirectory() as tmphome:
|
with tempfile.TemporaryDirectory() as tmphome:
|
||||||
with tempfile.TemporaryDirectory() as tmpproject:
|
with tempfile.TemporaryDirectory() as tmpproject:
|
||||||
with temporary_home(tmphome):
|
with temporary_home(tmphome):
|
||||||
org_dir = Path(tmphome) / ".autocoder"
|
org_dir = Path(tmphome) / ".autoforge"
|
||||||
org_dir.mkdir()
|
org_dir.mkdir()
|
||||||
org_config_path = org_dir / "config.yaml"
|
org_config_path = org_dir / "config.yaml"
|
||||||
|
|
||||||
@@ -762,9 +762,9 @@ pkill_processes:
|
|||||||
with tempfile.TemporaryDirectory() as tmpproject:
|
with tempfile.TemporaryDirectory() as tmpproject:
|
||||||
with temporary_home(tmphome):
|
with temporary_home(tmphome):
|
||||||
project_dir = Path(tmpproject)
|
project_dir = Path(tmpproject)
|
||||||
project_autocoder = project_dir / ".autocoder"
|
project_autoforge = project_dir / ".autoforge"
|
||||||
project_autocoder.mkdir()
|
project_autoforge.mkdir()
|
||||||
project_config = project_autocoder / "allowed_commands.yaml"
|
project_config = project_autoforge / "allowed_commands.yaml"
|
||||||
|
|
||||||
# Create project config with extra pkill processes
|
# Create project config with extra pkill processes
|
||||||
project_config.write_text("""version: 1
|
project_config.write_text("""version: 1
|
||||||
@@ -804,7 +804,7 @@ pkill_processes:
|
|||||||
with tempfile.TemporaryDirectory() as tmphome:
|
with tempfile.TemporaryDirectory() as tmphome:
|
||||||
with tempfile.TemporaryDirectory() as tmpproject:
|
with tempfile.TemporaryDirectory() as tmpproject:
|
||||||
with temporary_home(tmphome):
|
with temporary_home(tmphome):
|
||||||
org_dir = Path(tmphome) / ".autocoder"
|
org_dir = Path(tmphome) / ".autoforge"
|
||||||
org_dir.mkdir()
|
org_dir.mkdir()
|
||||||
org_config_path = org_dir / "config.yaml"
|
org_config_path = org_dir / "config.yaml"
|
||||||
|
|
||||||
@@ -829,7 +829,7 @@ pkill_processes:
|
|||||||
with tempfile.TemporaryDirectory() as tmphome:
|
with tempfile.TemporaryDirectory() as tmphome:
|
||||||
with tempfile.TemporaryDirectory() as tmpproject:
|
with tempfile.TemporaryDirectory() as tmpproject:
|
||||||
with temporary_home(tmphome):
|
with temporary_home(tmphome):
|
||||||
org_dir = Path(tmphome) / ".autocoder"
|
org_dir = Path(tmphome) / ".autoforge"
|
||||||
org_dir.mkdir()
|
org_dir.mkdir()
|
||||||
org_config_path = org_dir / "config.yaml"
|
org_config_path = org_dir / "config.yaml"
|
||||||
|
|
||||||
@@ -851,7 +851,7 @@ pkill_processes:
|
|||||||
with tempfile.TemporaryDirectory() as tmphome:
|
with tempfile.TemporaryDirectory() as tmphome:
|
||||||
with tempfile.TemporaryDirectory() as tmpproject:
|
with tempfile.TemporaryDirectory() as tmpproject:
|
||||||
with temporary_home(tmphome):
|
with temporary_home(tmphome):
|
||||||
org_dir = Path(tmphome) / ".autocoder"
|
org_dir = Path(tmphome) / ".autoforge"
|
||||||
org_dir.mkdir()
|
org_dir.mkdir()
|
||||||
org_config_path = org_dir / "config.yaml"
|
org_config_path = org_dir / "config.yaml"
|
||||||
|
|
||||||
@@ -875,7 +875,7 @@ pkill_processes:
|
|||||||
with tempfile.TemporaryDirectory() as tmphome:
|
with tempfile.TemporaryDirectory() as tmphome:
|
||||||
with tempfile.TemporaryDirectory() as tmpproject:
|
with tempfile.TemporaryDirectory() as tmpproject:
|
||||||
with temporary_home(tmphome):
|
with temporary_home(tmphome):
|
||||||
org_dir = Path(tmphome) / ".autocoder"
|
org_dir = Path(tmphome) / ".autoforge"
|
||||||
org_dir.mkdir()
|
org_dir.mkdir()
|
||||||
org_config_path = org_dir / "config.yaml"
|
org_config_path = org_dir / "config.yaml"
|
||||||
|
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ def test_blocked_command_via_hook():
|
|||||||
project_dir = Path(tmpdir)
|
project_dir = Path(tmpdir)
|
||||||
|
|
||||||
# Create minimal project structure
|
# Create minimal project structure
|
||||||
autocoder_dir = project_dir / ".autocoder"
|
autoforge_dir = project_dir / ".autoforge"
|
||||||
autocoder_dir.mkdir()
|
autoforge_dir.mkdir()
|
||||||
(autocoder_dir / "allowed_commands.yaml").write_text(
|
(autoforge_dir / "allowed_commands.yaml").write_text(
|
||||||
"version: 1\ncommands: []"
|
"version: 1\ncommands: []"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -114,9 +114,9 @@ def test_allowed_command_via_hook():
|
|||||||
project_dir = Path(tmpdir)
|
project_dir = Path(tmpdir)
|
||||||
|
|
||||||
# Create minimal project structure
|
# Create minimal project structure
|
||||||
autocoder_dir = project_dir / ".autocoder"
|
autoforge_dir = project_dir / ".autoforge"
|
||||||
autocoder_dir.mkdir()
|
autoforge_dir.mkdir()
|
||||||
(autocoder_dir / "allowed_commands.yaml").write_text(
|
(autoforge_dir / "allowed_commands.yaml").write_text(
|
||||||
"version: 1\ncommands: []"
|
"version: 1\ncommands: []"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -145,9 +145,9 @@ def test_non_allowed_command_via_hook():
|
|||||||
project_dir = Path(tmpdir)
|
project_dir = Path(tmpdir)
|
||||||
|
|
||||||
# Create minimal project structure
|
# Create minimal project structure
|
||||||
autocoder_dir = project_dir / ".autocoder"
|
autoforge_dir = project_dir / ".autoforge"
|
||||||
autocoder_dir.mkdir()
|
autoforge_dir.mkdir()
|
||||||
(autocoder_dir / "allowed_commands.yaml").write_text(
|
(autoforge_dir / "allowed_commands.yaml").write_text(
|
||||||
"version: 1\ncommands: []"
|
"version: 1\ncommands: []"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -179,9 +179,9 @@ def test_project_config_allows_command():
|
|||||||
project_dir = Path(tmpdir)
|
project_dir = Path(tmpdir)
|
||||||
|
|
||||||
# Create project config with swift allowed
|
# Create project config with swift allowed
|
||||||
autocoder_dir = project_dir / ".autocoder"
|
autoforge_dir = project_dir / ".autoforge"
|
||||||
autocoder_dir.mkdir()
|
autoforge_dir.mkdir()
|
||||||
(autocoder_dir / "allowed_commands.yaml").write_text("""version: 1
|
(autoforge_dir / "allowed_commands.yaml").write_text("""version: 1
|
||||||
commands:
|
commands:
|
||||||
- name: swift
|
- name: swift
|
||||||
description: Swift compiler
|
description: Swift compiler
|
||||||
@@ -214,9 +214,9 @@ def test_pattern_matching():
|
|||||||
project_dir = Path(tmpdir)
|
project_dir = Path(tmpdir)
|
||||||
|
|
||||||
# Create project config with swift* pattern
|
# Create project config with swift* pattern
|
||||||
autocoder_dir = project_dir / ".autocoder"
|
autoforge_dir = project_dir / ".autoforge"
|
||||||
autocoder_dir.mkdir()
|
autoforge_dir.mkdir()
|
||||||
(autocoder_dir / "allowed_commands.yaml").write_text("""version: 1
|
(autoforge_dir / "allowed_commands.yaml").write_text("""version: 1
|
||||||
commands:
|
commands:
|
||||||
- name: swift*
|
- name: swift*
|
||||||
description: All Swift tools
|
description: All Swift tools
|
||||||
@@ -247,7 +247,7 @@ def test_org_blocklist_enforcement():
|
|||||||
with tempfile.TemporaryDirectory() as tmpproject:
|
with tempfile.TemporaryDirectory() as tmpproject:
|
||||||
# Use context manager to safely set and restore HOME
|
# Use context manager to safely set and restore HOME
|
||||||
with temporary_home(tmphome):
|
with temporary_home(tmphome):
|
||||||
org_dir = Path(tmphome) / ".autocoder"
|
org_dir = Path(tmphome) / ".autoforge"
|
||||||
org_dir.mkdir()
|
org_dir.mkdir()
|
||||||
(org_dir / "config.yaml").write_text("""version: 1
|
(org_dir / "config.yaml").write_text("""version: 1
|
||||||
allowed_commands: []
|
allowed_commands: []
|
||||||
@@ -257,11 +257,11 @@ blocked_commands:
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
project_dir = Path(tmpproject)
|
project_dir = Path(tmpproject)
|
||||||
autocoder_dir = project_dir / ".autocoder"
|
autoforge_dir = project_dir / ".autoforge"
|
||||||
autocoder_dir.mkdir()
|
autoforge_dir.mkdir()
|
||||||
|
|
||||||
# Try to allow terraform in project config (should fail - org blocked)
|
# Try to allow terraform in project config (should fail - org blocked)
|
||||||
(autocoder_dir / "allowed_commands.yaml").write_text("""version: 1
|
(autoforge_dir / "allowed_commands.yaml").write_text("""version: 1
|
||||||
commands:
|
commands:
|
||||||
- name: terraform
|
- name: terraform
|
||||||
description: Infrastructure as code
|
description: Infrastructure as code
|
||||||
@@ -295,7 +295,7 @@ def test_org_allowlist_inheritance():
|
|||||||
with tempfile.TemporaryDirectory() as tmpproject:
|
with tempfile.TemporaryDirectory() as tmpproject:
|
||||||
# Use context manager to safely set and restore HOME
|
# Use context manager to safely set and restore HOME
|
||||||
with temporary_home(tmphome):
|
with temporary_home(tmphome):
|
||||||
org_dir = Path(tmphome) / ".autocoder"
|
org_dir = Path(tmphome) / ".autoforge"
|
||||||
org_dir.mkdir()
|
org_dir.mkdir()
|
||||||
(org_dir / "config.yaml").write_text("""version: 1
|
(org_dir / "config.yaml").write_text("""version: 1
|
||||||
allowed_commands:
|
allowed_commands:
|
||||||
@@ -305,9 +305,9 @@ blocked_commands: []
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
project_dir = Path(tmpproject)
|
project_dir = Path(tmpproject)
|
||||||
autocoder_dir = project_dir / ".autocoder"
|
autoforge_dir = project_dir / ".autoforge"
|
||||||
autocoder_dir.mkdir()
|
autoforge_dir.mkdir()
|
||||||
(autocoder_dir / "allowed_commands.yaml").write_text(
|
(autoforge_dir / "allowed_commands.yaml").write_text(
|
||||||
"version: 1\ncommands: []"
|
"version: 1\ncommands: []"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -336,9 +336,9 @@ def test_invalid_yaml_ignored():
|
|||||||
project_dir = Path(tmpdir)
|
project_dir = Path(tmpdir)
|
||||||
|
|
||||||
# Create invalid YAML
|
# Create invalid YAML
|
||||||
autocoder_dir = project_dir / ".autocoder"
|
autoforge_dir = project_dir / ".autoforge"
|
||||||
autocoder_dir.mkdir()
|
autoforge_dir.mkdir()
|
||||||
(autocoder_dir / "allowed_commands.yaml").write_text("invalid: yaml: content:")
|
(autoforge_dir / "allowed_commands.yaml").write_text("invalid: yaml: content:")
|
||||||
|
|
||||||
# Try to run ls (should still work - falls back to defaults)
|
# Try to run ls (should still work - falls back to defaults)
|
||||||
input_data = {"tool_name": "Bash", "tool_input": {"command": "ls"}}
|
input_data = {"tool_name": "Bash", "tool_input": {"command": "ls"}}
|
||||||
@@ -365,13 +365,13 @@ def test_100_command_limit():
|
|||||||
project_dir = Path(tmpdir)
|
project_dir = Path(tmpdir)
|
||||||
|
|
||||||
# Create config with 101 commands
|
# Create config with 101 commands
|
||||||
autocoder_dir = project_dir / ".autocoder"
|
autoforge_dir = project_dir / ".autoforge"
|
||||||
autocoder_dir.mkdir()
|
autoforge_dir.mkdir()
|
||||||
|
|
||||||
commands = [
|
commands = [
|
||||||
f" - name: cmd{i}\n description: Command {i}" for i in range(101)
|
f" - name: cmd{i}\n description: Command {i}" for i in range(101)
|
||||||
]
|
]
|
||||||
(autocoder_dir / "allowed_commands.yaml").write_text(
|
(autoforge_dir / "allowed_commands.yaml").write_text(
|
||||||
"version: 1\ncommands:\n" + "\n".join(commands)
|
"version: 1\ncommands:\n" + "\n".join(commands)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/png" href="/logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>AutoCoder</title>
|
<title>AutoForge</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Work+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=DM+Sans:wght@400;500;700&family=Space+Mono:wght@400;700&family=Outfit:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Work+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=DM+Sans:wght@400;500;700&family=Space+Mono:wght@400;700&family=Outfit:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
|||||||
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",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "autocoder",
|
"name": "autoforge-ui",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"@xyflow/react": "^12.10.0",
|
"@xyflow/react": "^12.10.0",
|
||||||
|
"autoforge-ai": "file:..",
|
||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -50,6 +51,17 @@
|
|||||||
"vite": "^7.3.0"
|
"vite": "^7.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"..": {
|
||||||
|
"name": "autoforge-ai",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"bin": {
|
||||||
|
"autoforge": "bin/autoforge.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
@@ -3136,6 +3148,10 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/autoforge-ai": {
|
||||||
|
"resolved": "..",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "autocoder",
|
"name": "autoforge-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"@xyflow/react": "^12.10.0",
|
"@xyflow/react": "^12.10.0",
|
||||||
|
"autoforge-ai": "file:..",
|
||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.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 { Card, CardContent } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
|
||||||
const STORAGE_KEY = 'autocoder-selected-project'
|
const STORAGE_KEY = 'autoforge-selected-project'
|
||||||
const VIEW_MODE_KEY = 'autocoder-view-mode'
|
const VIEW_MODE_KEY = 'autoforge-view-mode'
|
||||||
|
|
||||||
// Bottom padding for main content when debug panel is collapsed (40px header + 8px margin)
|
// Bottom padding for main content when debug panel is collapsed (40px header + 8px margin)
|
||||||
const COLLAPSED_DEBUG_PANEL_CLEARANCE = 48
|
const COLLAPSED_DEBUG_PANEL_CLEARANCE = 48
|
||||||
@@ -263,9 +263,12 @@ function App() {
|
|||||||
<div className="max-w-7xl mx-auto px-4 py-4">
|
<div className="max-w-7xl mx-auto px-4 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Logo and Title */}
|
{/* Logo and Title */}
|
||||||
|
<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">
|
<h1 className="font-display text-2xl font-bold tracking-tight uppercase">
|
||||||
AutoCoder
|
AutoForge
|
||||||
</h1>
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -337,7 +340,7 @@ function App() {
|
|||||||
|
|
||||||
{/* Docs link */}
|
{/* Docs link */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => { window.location.hash = '#/docs' }}
|
onClick={() => window.open('https://autoforge.cc', '_blank')}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
title="Documentation"
|
title="Documentation"
|
||||||
@@ -376,7 +379,7 @@ function App() {
|
|||||||
{!selectedProject ? (
|
{!selectedProject ? (
|
||||||
<div className="text-center mt-12">
|
<div className="text-center mt-12">
|
||||||
<h2 className="font-display text-2xl font-bold mb-2">
|
<h2 className="font-display text-2xl font-bold mb-2">
|
||||||
Welcome to AutoCoder
|
Welcome to AutoForge
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground mb-4">
|
<p className="text-muted-foreground mb-4">
|
||||||
Select a project from the dropdown above or create a new one to get started.
|
Select a project from the dropdown above or create a new one to get started.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Card, CardContent } from '@/components/ui/card'
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
const ACTIVITY_COLLAPSED_KEY = 'autocoder-activity-collapsed'
|
const ACTIVITY_COLLAPSED_KEY = 'autoforge-activity-collapsed'
|
||||||
|
|
||||||
interface AgentMissionControlProps {
|
interface AgentMissionControlProps {
|
||||||
agents: ActiveAgent[]
|
agents: ActiveAgent[]
|
||||||
@@ -88,8 +88,8 @@ export function AgentMissionControl({
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
transition-all duration-300 ease-out overflow-hidden
|
transition-all duration-300 ease-out
|
||||||
${isExpanded ? 'max-h-[600px] opacity-100' : 'max-h-0 opacity-0'}
|
${isExpanded ? 'max-h-[600px] opacity-100 overflow-y-auto' : 'max-h-0 opacity-0 overflow-hidden'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
|
|||||||
@@ -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
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] max-h-[calc(100vh-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none overflow-y-auto sm:max-w-lg",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -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 THEME_STORAGE_KEY = 'autoforge-theme'
|
||||||
const DARK_MODE_STORAGE_KEY = 'autocoder-dark-mode'
|
const DARK_MODE_STORAGE_KEY = 'autoforge-dark-mode'
|
||||||
|
|
||||||
function getThemeClass(themeId: ThemeId): string {
|
function getThemeClass(themeId: ThemeId): string {
|
||||||
switch (themeId) {
|
switch (themeId) {
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { useHashRoute } from './hooks/useHashRoute'
|
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import { DocsPage } from './components/docs/DocsPage'
|
|
||||||
import './styles/globals.css'
|
import './styles/globals.css'
|
||||||
// Note: Custom theme removed - using shadcn/ui theming instead
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -16,16 +13,10 @@ const queryClient = new QueryClient({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function Router() {
|
|
||||||
const { route } = useHashRoute()
|
|
||||||
if (route === 'docs') return <DocsPage />
|
|
||||||
return <App />
|
|
||||||
}
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Router />
|
<App />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,11 +26,23 @@ export default defineConfig({
|
|||||||
'vendor-flow': ['@xyflow/react', 'dagre'],
|
'vendor-flow': ['@xyflow/react', 'dagre'],
|
||||||
// Terminal emulator
|
// Terminal emulator
|
||||||
'vendor-xterm': ['@xterm/xterm', '@xterm/addon-fit', '@xterm/addon-web-links'],
|
'vendor-xterm': ['@xterm/xterm', '@xterm/addon-fit', '@xterm/addon-web-links'],
|
||||||
// UI components
|
// UI components - Radix UI
|
||||||
'vendor-ui': [
|
'vendor-radix': [
|
||||||
|
'@radix-ui/react-checkbox',
|
||||||
'@radix-ui/react-dialog',
|
'@radix-ui/react-dialog',
|
||||||
'@radix-ui/react-dropdown-menu',
|
'@radix-ui/react-dropdown-menu',
|
||||||
|
'@radix-ui/react-label',
|
||||||
|
'@radix-ui/react-separator',
|
||||||
|
'@radix-ui/react-slot',
|
||||||
|
'@radix-ui/react-switch',
|
||||||
|
],
|
||||||
|
// Icons and utilities
|
||||||
|
'vendor-utils': [
|
||||||
'lucide-react',
|
'lucide-react',
|
||||||
|
'canvas-confetti',
|
||||||
|
'class-variance-authority',
|
||||||
|
'clsx',
|
||||||
|
'tailwind-merge',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user