mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-02-04 16:03:08 +00:00
Compare commits
11 Commits
1607fc8175
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
326f38b3c4 | ||
|
|
6d15d020ec | ||
|
|
196038fa26 | ||
|
|
4549840330 | ||
|
|
451a5a9d05 | ||
|
|
c2ad993e75 | ||
|
|
f6510b4dd8 | ||
|
|
2507bfd5f0 | ||
|
|
f32e7efda6 | ||
|
|
b2ab1ecc7a | ||
|
|
016eead8b4 |
@@ -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
|
||||||
228
CLAUDE.md
228
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)
|
||||||
@@ -54,6 +64,12 @@ python autonomous_agent_demo.py --project-dir my-app --yolo
|
|||||||
|
|
||||||
# Parallel mode: run multiple agents concurrently (1-5 agents)
|
# Parallel mode: run multiple agents concurrently (1-5 agents)
|
||||||
python autonomous_agent_demo.py --project-dir my-app --parallel --max-concurrency 3
|
python autonomous_agent_demo.py --project-dir my-app --parallel --max-concurrency 3
|
||||||
|
|
||||||
|
# Batch mode: implement multiple features per agent session (1-3)
|
||||||
|
python autonomous_agent_demo.py --project-dir my-app --batch-size 3
|
||||||
|
|
||||||
|
# Batch specific features by ID
|
||||||
|
python autonomous_agent_demo.py --project-dir my-app --batch-features 1,2,3
|
||||||
```
|
```
|
||||||
|
|
||||||
### YOLO Mode (Rapid Prototyping)
|
### YOLO Mode (Rapid Prototyping)
|
||||||
@@ -68,7 +84,7 @@ python autonomous_agent_demo.py --project-dir my-app --yolo
|
|||||||
```
|
```
|
||||||
|
|
||||||
**What's different in YOLO mode:**
|
**What's different in YOLO mode:**
|
||||||
- No regression testing (skips `feature_get_for_regression`)
|
- No regression testing
|
||||||
- No Playwright MCP server (browser automation disabled)
|
- No Playwright MCP server (browser automation disabled)
|
||||||
- Features marked passing after lint/type-check succeeds
|
- Features marked passing after lint/type-check succeeds
|
||||||
- Faster iteration for prototyping
|
- Faster iteration for prototyping
|
||||||
@@ -99,8 +115,11 @@ npm run lint # Run ESLint
|
|||||||
```bash
|
```bash
|
||||||
ruff check . # Lint
|
ruff check . # Lint
|
||||||
mypy . # Type check
|
mypy . # Type check
|
||||||
python test_security.py # Security unit tests (163 tests)
|
python test_security.py # Security unit tests (12 tests)
|
||||||
python test_security_integration.py # Integration tests (9 tests)
|
python test_security_integration.py # Integration tests (9 tests)
|
||||||
|
python -m pytest test_client.py # Client tests (20 tests)
|
||||||
|
python -m pytest test_dependency_resolver.py # Dependency resolver tests (12 tests)
|
||||||
|
python -m pytest test_rate_limit_utils.py # Rate limit tests (22 tests)
|
||||||
```
|
```
|
||||||
|
|
||||||
### React UI
|
### React UI
|
||||||
@@ -108,11 +127,17 @@ python test_security_integration.py # Integration tests (9 tests)
|
|||||||
```bash
|
```bash
|
||||||
cd ui
|
cd ui
|
||||||
npm run lint # ESLint
|
npm run lint # ESLint
|
||||||
npm run build # Type check + build
|
npm run build # Type check + build (Vite 7)
|
||||||
npm run test:e2e # Playwright end-to-end tests
|
npm run test:e2e # Playwright end-to-end tests
|
||||||
npm run test:e2e:ui # Playwright tests with UI
|
npm run test:e2e:ui # Playwright tests with UI
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### CI/CD
|
||||||
|
|
||||||
|
GitHub Actions (`.github/workflows/ci.yml`) runs on push/PR to master:
|
||||||
|
- **Python job**: ruff lint + security tests
|
||||||
|
- **UI job**: ESLint + TypeScript build
|
||||||
|
|
||||||
### Code Quality
|
### Code Quality
|
||||||
|
|
||||||
Configuration in `pyproject.toml`:
|
Configuration in `pyproject.toml`:
|
||||||
@@ -121,24 +146,40 @@ 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
|
- `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 and MCP servers
|
- `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)
|
||||||
- `prompts.py` - Prompt template loading with project-specific fallback
|
- `prompts.py` - Prompt template loading with project-specific fallback and batch feature prompts
|
||||||
- `progress.py` - Progress tracking, database queries, webhook notifications
|
- `progress.py` - Progress tracking, database queries, webhook notifications
|
||||||
- `registry.py` - Project registry for mapping names to paths (cross-platform)
|
- `registry.py` - Project registry for mapping names to paths (cross-platform), global settings model
|
||||||
- `parallel_orchestrator.py` - Concurrent agent execution with dependency-aware scheduling
|
- `parallel_orchestrator.py` - Concurrent agent execution with dependency-aware scheduling
|
||||||
|
- `auth.py` - Authentication error detection for Claude CLI
|
||||||
|
- `env_constants.py` - Shared environment variable constants (API_ENV_VARS) used by client.py and chat sessions
|
||||||
|
- `rate_limit_utils.py` - Rate limit detection, retry parsing, exponential backoff with jitter
|
||||||
|
- `api/database.py` - SQLAlchemy models (Feature, Schedule, ScheduleOverride)
|
||||||
- `api/dependency_resolver.py` - Cycle detection (Kahn's algorithm + DFS) and dependency validation
|
- `api/dependency_resolver.py` - Cycle detection (Kahn's algorithm + DFS) and dependency validation
|
||||||
|
- `api/migration.py` - JSON-to-SQLite migration utility
|
||||||
|
|
||||||
### 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
|
||||||
@@ -147,13 +188,36 @@ The registry uses:
|
|||||||
|
|
||||||
### Server API (server/)
|
### Server API (server/)
|
||||||
|
|
||||||
The FastAPI server provides REST endpoints for the UI:
|
The FastAPI server provides REST and WebSocket endpoints for the UI:
|
||||||
|
|
||||||
- `server/routers/projects.py` - Project CRUD with registry integration
|
**Routers** (`server/routers/`):
|
||||||
- `server/routers/features.py` - Feature management
|
- `projects.py` - Project CRUD with registry integration
|
||||||
- `server/routers/agent.py` - Agent control (start/stop/pause/resume)
|
- `features.py` - Feature management
|
||||||
- `server/routers/filesystem.py` - Filesystem browser API with security controls
|
- `agent.py` - Agent control (start/stop/pause/resume)
|
||||||
- `server/routers/spec_creation.py` - WebSocket for interactive spec creation
|
- `filesystem.py` - Filesystem browser API with security controls
|
||||||
|
- `spec_creation.py` - WebSocket for interactive spec creation
|
||||||
|
- `expand_project.py` - Interactive project expansion via natural language
|
||||||
|
- `assistant_chat.py` - Read-only project assistant chat (WebSocket/REST)
|
||||||
|
- `terminal.py` - Interactive terminal I/O with PTY support (WebSocket bidirectional)
|
||||||
|
- `devserver.py` - Dev server control (start/stop) and config
|
||||||
|
- `schedules.py` - CRUD for time-based agent scheduling
|
||||||
|
- `settings.py` - Global settings management (model selection, YOLO, batch size, headless browser)
|
||||||
|
|
||||||
|
**Services** (`server/services/`):
|
||||||
|
- `process_manager.py` - Agent process lifecycle management
|
||||||
|
- `project_config.py` - Project type detection and dev command management
|
||||||
|
- `terminal_manager.py` - Terminal session management with PTY (`pywinpty` on Windows)
|
||||||
|
- `scheduler_service.py` - APScheduler-based automated agent scheduling
|
||||||
|
- `dev_server_manager.py` - Dev server lifecycle management
|
||||||
|
- `assistant_chat_session.py` / `assistant_database.py` - Assistant chat sessions with SQLite persistence
|
||||||
|
- `spec_chat_session.py` - Spec creation chat sessions
|
||||||
|
- `expand_chat_session.py` - Expand project chat sessions
|
||||||
|
- `chat_constants.py` - Shared constants for chat services
|
||||||
|
|
||||||
|
**Utilities** (`server/utils/`):
|
||||||
|
- `process_utils.py` - Process management utilities
|
||||||
|
- `project_helpers.py` - Project path resolution helpers
|
||||||
|
- `validation.py` - Project name validation
|
||||||
|
|
||||||
### Feature Management
|
### Feature Management
|
||||||
|
|
||||||
@@ -164,18 +228,26 @@ Features are stored in SQLite (`features.db`) via SQLAlchemy. The agent interact
|
|||||||
|
|
||||||
MCP tools available to the agent:
|
MCP tools available to the agent:
|
||||||
- `feature_get_stats` - Progress statistics
|
- `feature_get_stats` - Progress statistics
|
||||||
- `feature_get_next` - Get highest-priority pending feature (respects dependencies)
|
- `feature_get_by_id` - Get a single feature by ID
|
||||||
- `feature_claim_next` - Atomically claim next available feature (for parallel mode)
|
- `feature_get_summary` - Get summary of all features
|
||||||
- `feature_get_for_regression` - Random passing features for regression testing
|
- `feature_get_ready` - Get features ready to work on (dependencies met)
|
||||||
|
- `feature_get_blocked` - Get features blocked by unmet dependencies
|
||||||
|
- `feature_get_graph` - Get full dependency graph
|
||||||
|
- `feature_claim_and_get` - Atomically claim next available feature (for parallel mode)
|
||||||
|
- `feature_mark_in_progress` - Mark feature as in progress
|
||||||
- `feature_mark_passing` - Mark feature complete
|
- `feature_mark_passing` - Mark feature complete
|
||||||
|
- `feature_mark_failing` - Mark feature as failing
|
||||||
- `feature_skip` - Move feature to end of queue
|
- `feature_skip` - Move feature to end of queue
|
||||||
|
- `feature_clear_in_progress` - Clear in-progress status
|
||||||
- `feature_create_bulk` - Initialize all features (used by initializer)
|
- `feature_create_bulk` - Initialize all features (used by initializer)
|
||||||
|
- `feature_create` - Create a single feature
|
||||||
- `feature_add_dependency` - Add dependency between features (with cycle detection)
|
- `feature_add_dependency` - Add dependency between features (with cycle detection)
|
||||||
- `feature_remove_dependency` - Remove a dependency
|
- `feature_remove_dependency` - Remove a dependency
|
||||||
|
- `feature_set_dependencies` - Set all dependencies for a feature at once
|
||||||
|
|
||||||
### React UI (ui/)
|
### React UI (ui/)
|
||||||
|
|
||||||
- Tech stack: React 19, TypeScript, TanStack Query, Tailwind CSS v4, Radix UI, dagre (graph layout)
|
- Tech stack: React 19, TypeScript, Vite 7, TanStack Query, Tailwind CSS v4, Radix UI, dagre (graph layout), xterm.js (terminal)
|
||||||
- `src/App.tsx` - Main app with project selection, kanban board, agent controls
|
- `src/App.tsx` - Main app with project selection, kanban board, agent controls
|
||||||
- `src/hooks/useWebSocket.ts` - Real-time updates via WebSocket (progress, agent status, logs, agent updates)
|
- `src/hooks/useWebSocket.ts` - Real-time updates via WebSocket (progress, agent status, logs, agent updates)
|
||||||
- `src/hooks/useProjects.ts` - React Query hooks for API calls
|
- `src/hooks/useProjects.ts` - React Query hooks for API calls
|
||||||
@@ -187,6 +259,17 @@ Key components:
|
|||||||
- `DependencyGraph.tsx` - Interactive node graph visualization with dagre layout
|
- `DependencyGraph.tsx` - Interactive node graph visualization with dagre layout
|
||||||
- `CelebrationOverlay.tsx` - Confetti animation on feature completion
|
- `CelebrationOverlay.tsx` - Confetti animation on feature completion
|
||||||
- `FolderBrowser.tsx` - Server-side filesystem browser for project folder selection
|
- `FolderBrowser.tsx` - Server-side filesystem browser for project folder selection
|
||||||
|
- `Terminal.tsx` / `TerminalTabs.tsx` - xterm.js-based multi-tab terminal
|
||||||
|
- `AssistantPanel.tsx` / `AssistantChat.tsx` - AI assistant for project Q&A
|
||||||
|
- `ExpandProjectModal.tsx` / `ExpandProjectChat.tsx` - Add features via natural language
|
||||||
|
- `DevServerControl.tsx` - Dev server start/stop control
|
||||||
|
- `ScheduleModal.tsx` - Schedule management UI
|
||||||
|
- `SettingsModal.tsx` - Global settings panel
|
||||||
|
|
||||||
|
In-app documentation (`/#/docs` route):
|
||||||
|
- `src/components/docs/sections/` - Content for each doc section (GettingStarted.tsx, AgentSystem.tsx, etc.)
|
||||||
|
- `src/components/docs/docsData.ts` - Sidebar structure, subsection IDs, search keywords
|
||||||
|
- `src/components/docs/DocsPage.tsx` - Page layout; `DocsContent.tsx` - section renderer with scroll tracking
|
||||||
|
|
||||||
Keyboard shortcuts (press `?` for help):
|
Keyboard shortcuts (press `?` for help):
|
||||||
- `D` - Toggle debug panel
|
- `D` - Toggle debug panel
|
||||||
@@ -197,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
|
||||||
|
|
||||||
@@ -248,29 +331,20 @@ The following directories (relative to home) are always blocked:
|
|||||||
- `.docker`, `.config/gcloud` - Container/cloud configs
|
- `.docker`, `.config/gcloud` - Container/cloud configs
|
||||||
- `.npmrc`, `.pypirc`, `.netrc` - Package manager credentials
|
- `.npmrc`, `.pypirc`, `.netrc` - Package manager credentials
|
||||||
|
|
||||||
**Example Output:**
|
|
||||||
|
|
||||||
```
|
|
||||||
Created security settings at /path/to/project/.claude_settings.json
|
|
||||||
- Sandbox enabled (OS-level bash isolation)
|
|
||||||
- Filesystem restricted to: /path/to/project
|
|
||||||
- Extra read paths (validated): /Users/me/docs, /opt/shared-libs
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Per-Project Allowed Commands
|
#### Per-Project Allowed Commands
|
||||||
|
|
||||||
The agent's bash command access is controlled through a hierarchical configuration system:
|
The agent's bash command access is controlled through a hierarchical configuration system:
|
||||||
|
|
||||||
**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
|
||||||
@@ -290,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
|
||||||
@@ -318,13 +392,29 @@ blocked_commands:
|
|||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- `security.py` - Command validation logic and hardcoded blocklist
|
- `security.py` - Command validation logic and hardcoded blocklist
|
||||||
- `test_security.py` - Unit tests for security system (136 tests)
|
- `test_security.py` - Unit tests for security system
|
||||||
- `test_security_integration.py` - Integration tests with real hooks (9 tests)
|
- `test_security_integration.py` - Integration tests with real hooks
|
||||||
- `TEST_SECURITY.md` - Quick testing reference guide
|
|
||||||
- `examples/project_allowed_commands.yaml` - Project config example (all commented by default)
|
- `examples/project_allowed_commands.yaml` - Project config example (all commented by default)
|
||||||
- `examples/org_config.yaml` - Org config example (all commented by default)
|
- `examples/org_config.yaml` - Org config example (all commented by default)
|
||||||
- `examples/README.md` - Comprehensive guide with use cases, testing, and troubleshooting
|
- `examples/README.md` - Comprehensive guide with use cases, testing, and troubleshooting
|
||||||
|
|
||||||
|
### Vertex AI Configuration (Optional)
|
||||||
|
|
||||||
|
Run coding agents via Google Cloud Vertex AI:
|
||||||
|
|
||||||
|
1. Install and authenticate gcloud CLI: `gcloud auth application-default login`
|
||||||
|
2. Configure `.env`:
|
||||||
|
```
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Use `@` instead of `-` in model names for Vertex AI.
|
||||||
|
|
||||||
### Ollama Local Models (Optional)
|
### Ollama Local Models (Optional)
|
||||||
|
|
||||||
Run coding agents using local models via Ollama v0.14.0+:
|
Run coding agents using local models via Ollama v0.14.0+:
|
||||||
@@ -341,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
|
||||||
@@ -360,8 +450,24 @@ Run coding agents using local models via Ollama v0.14.0+:
|
|||||||
|
|
||||||
## Claude Code Integration
|
## Claude Code Integration
|
||||||
|
|
||||||
- `.claude/commands/create-spec.md` - `/create-spec` slash command for interactive spec creation
|
**Slash commands** (`.claude/commands/`):
|
||||||
- `.claude/skills/frontend-design/SKILL.md` - Skill for distinctive UI design
|
- `/create-spec` - Interactive spec creation for new projects
|
||||||
|
- `/expand-project` - Expand existing project with new features
|
||||||
|
- `/gsd-to-autoforge-spec` - Convert GSD codebase mapping to app_spec.txt
|
||||||
|
- `/check-code` - Run lint and type-check for code quality
|
||||||
|
- `/checkpoint` - Create comprehensive checkpoint commit
|
||||||
|
- `/review-pr` - Review pull requests
|
||||||
|
|
||||||
|
**Custom agents** (`.claude/agents/`):
|
||||||
|
- `coder.md` - Elite software architect agent for code implementation (Opus)
|
||||||
|
- `code-review.md` - Code review agent for quality/security/performance analysis (Opus)
|
||||||
|
- `deep-dive.md` - Technical investigator for deep analysis and debugging (Opus)
|
||||||
|
|
||||||
|
**Skills** (`.claude/skills/`):
|
||||||
|
- `frontend-design` - Distinctive, production-grade UI design
|
||||||
|
- `gsd-to-autoforge-spec` - Convert GSD codebase mapping to AutoForge app_spec format
|
||||||
|
|
||||||
|
**Other:**
|
||||||
- `.claude/templates/` - Prompt templates copied to new projects
|
- `.claude/templates/` - Prompt templates copied to new projects
|
||||||
- `examples/` - Configuration examples and documentation for security settings
|
- `examples/` - Configuration examples and documentation for security settings
|
||||||
|
|
||||||
@@ -369,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
|
||||||
@@ -392,7 +498,7 @@ The UI receives updates via WebSocket (`/ws/projects/{project_name}`):
|
|||||||
|
|
||||||
When running with `--parallel`, the orchestrator:
|
When running with `--parallel`, the orchestrator:
|
||||||
1. Spawns multiple Claude agents as subprocesses (up to `--max-concurrency`)
|
1. Spawns multiple Claude agents as subprocesses (up to `--max-concurrency`)
|
||||||
2. Each agent claims features atomically via `feature_claim_next`
|
2. Each agent claims features atomically via `feature_claim_and_get`
|
||||||
3. Features blocked by unmet dependencies are skipped
|
3. Features blocked by unmet dependencies are skipped
|
||||||
4. Browser contexts are isolated per agent using `--isolated` flag
|
4. Browser contexts are isolated per agent using `--isolated` flag
|
||||||
5. AgentTracker parses output and emits `agent_update` messages for UI
|
5. AgentTracker parses output and emits `agent_update` messages for UI
|
||||||
@@ -405,6 +511,16 @@ The orchestrator enforces strict bounds on concurrent processes:
|
|||||||
- Testing agents are capped at `max_concurrency` (same as coding agents)
|
- Testing agents are capped at `max_concurrency` (same as coding agents)
|
||||||
- Total process count never exceeds 11 Python processes (1 orchestrator + 5 coding + 5 testing)
|
- Total process count never exceeds 11 Python processes (1 orchestrator + 5 coding + 5 testing)
|
||||||
|
|
||||||
|
### Multi-Feature Batching
|
||||||
|
|
||||||
|
Agents can implement multiple features per session using `--batch-size` (1-3, default: 3):
|
||||||
|
- `--batch-size N` - Max features per coding agent batch
|
||||||
|
- `--testing-batch-size N` - Features per testing batch (1-5, default: 3)
|
||||||
|
- `--batch-features 1,2,3` - Specific feature IDs for batch implementation
|
||||||
|
- `--testing-batch-features 1,2,3` - Specific feature IDs for batch regression testing
|
||||||
|
- `prompts.py` provides `get_batch_feature_prompt()` for multi-feature prompt generation
|
||||||
|
- Configurable in UI via settings panel
|
||||||
|
|
||||||
### Design System
|
### Design System
|
||||||
|
|
||||||
The UI uses a **neobrutalism** design with Tailwind CSS v4:
|
The UI uses a **neobrutalism** design with Tailwind CSS v4:
|
||||||
|
|||||||
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
|
||||||
|
|||||||
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.1",
|
||||||
|
"description": "Autonomous coding agent with web UI - build complete apps with AI",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"bin": {
|
||||||
|
"autoforge": "./bin/autoforge.js"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"bin/",
|
||||||
|
"lib/",
|
||||||
|
"api/",
|
||||||
|
"server/",
|
||||||
|
"mcp_server/",
|
||||||
|
"ui/dist/",
|
||||||
|
"ui/package.json",
|
||||||
|
".claude/commands/",
|
||||||
|
".claude/templates/",
|
||||||
|
"examples/",
|
||||||
|
"start.py",
|
||||||
|
"agent.py",
|
||||||
|
"auth.py",
|
||||||
|
"autoforge_paths.py",
|
||||||
|
"autonomous_agent_demo.py",
|
||||||
|
"client.py",
|
||||||
|
"env_constants.py",
|
||||||
|
"parallel_orchestrator.py",
|
||||||
|
"progress.py",
|
||||||
|
"prompts.py",
|
||||||
|
"registry.py",
|
||||||
|
"rate_limit_utils.py",
|
||||||
|
"security.py",
|
||||||
|
"requirements-prod.txt",
|
||||||
|
"pyproject.toml",
|
||||||
|
".env.example",
|
||||||
|
"!**/__pycache__/",
|
||||||
|
"!**/*.pyc"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"ai",
|
||||||
|
"coding-agent",
|
||||||
|
"claude",
|
||||||
|
"autonomous",
|
||||||
|
"code-generation"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"prepublishOnly": "npm --prefix ui install && npm --prefix ui run build"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import asyncio
|
|||||||
import atexit
|
import atexit
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -39,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:
|
||||||
@@ -822,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",
|
||||||
@@ -844,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":
|
||||||
@@ -888,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",
|
||||||
@@ -907,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":
|
||||||
@@ -991,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",
|
||||||
@@ -1011,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":
|
||||||
@@ -1052,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",
|
||||||
@@ -1072,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":
|
||||||
@@ -1116,6 +1117,11 @@ class ParallelOrchestrator:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Pattern to detect when a batch agent claims a new feature
|
||||||
|
_CLAIM_FEATURE_PATTERN = re.compile(
|
||||||
|
r"feature_claim_and_get\b.*?['\"]?feature_id['\"]?\s*[:=]\s*(\d+)"
|
||||||
|
)
|
||||||
|
|
||||||
def _read_output(
|
def _read_output(
|
||||||
self,
|
self,
|
||||||
feature_id: int | None,
|
feature_id: int | None,
|
||||||
@@ -1124,6 +1130,7 @@ class ParallelOrchestrator:
|
|||||||
agent_type: Literal["coding", "testing"] = "coding",
|
agent_type: Literal["coding", "testing"] = "coding",
|
||||||
):
|
):
|
||||||
"""Read output from subprocess and emit events."""
|
"""Read output from subprocess and emit events."""
|
||||||
|
current_feature_id = feature_id
|
||||||
try:
|
try:
|
||||||
if proc.stdout is None:
|
if proc.stdout is None:
|
||||||
proc.wait()
|
proc.wait()
|
||||||
@@ -1132,11 +1139,17 @@ class ParallelOrchestrator:
|
|||||||
if abort.is_set():
|
if abort.is_set():
|
||||||
break
|
break
|
||||||
line = line.rstrip()
|
line = line.rstrip()
|
||||||
|
# Detect when a batch agent claims a new feature
|
||||||
|
claim_match = self._CLAIM_FEATURE_PATTERN.search(line)
|
||||||
|
if claim_match:
|
||||||
|
claimed_id = int(claim_match.group(1))
|
||||||
|
if claimed_id != current_feature_id:
|
||||||
|
current_feature_id = claimed_id
|
||||||
if self.on_output is not None:
|
if self.on_output is not None:
|
||||||
self.on_output(feature_id or 0, line)
|
self.on_output(current_feature_id or 0, line)
|
||||||
else:
|
else:
|
||||||
# Both coding and testing agents now use [Feature #X] format
|
# Both coding and testing agents now use [Feature #X] format
|
||||||
print(f"[Feature #{feature_id}] {line}", flush=True)
|
print(f"[Feature #{current_feature_id}] {line}", flush=True)
|
||||||
proc.wait()
|
proc.wait()
|
||||||
finally:
|
finally:
|
||||||
# CRITICAL: Kill the process tree to clean up any child processes (e.g., Claude CLI)
|
# CRITICAL: Kill the process tree to clean up any child processes (e.g., Claude CLI)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class DevServerProcessManager:
|
|||||||
self._callbacks_lock = threading.Lock()
|
self._callbacks_lock = threading.Lock()
|
||||||
|
|
||||||
# Lock file to prevent multiple instances (stored in project directory)
|
# Lock file to prevent multiple instances (stored in project directory)
|
||||||
from autocoder_paths import get_devserver_lock_path
|
from autoforge_paths import get_devserver_lock_path
|
||||||
self.lock_file = get_devserver_lock_path(self.project_dir)
|
self.lock_file = get_devserver_lock_path(self.project_dir)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -504,10 +504,10 @@ def cleanup_orphaned_devserver_locks() -> int:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Check both legacy and new locations for lock files
|
# Check both legacy and new locations for lock files
|
||||||
from autocoder_paths import get_autocoder_dir
|
from autoforge_paths import get_autoforge_dir
|
||||||
lock_locations = [
|
lock_locations = [
|
||||||
project_path / ".devserver.lock",
|
project_path / ".devserver.lock",
|
||||||
get_autocoder_dir(project_path) / ".devserver.lock",
|
get_autoforge_dir(project_path) / ".devserver.lock",
|
||||||
]
|
]
|
||||||
lock_file = None
|
lock_file = None
|
||||||
for candidate in lock_locations:
|
for candidate in lock_locations:
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
@@ -28,14 +28,14 @@ import { ThemeSelector } from './components/ThemeSelector'
|
|||||||
import { ResetProjectModal } from './components/ResetProjectModal'
|
import { ResetProjectModal } from './components/ResetProjectModal'
|
||||||
import { ProjectSetupRequired } from './components/ProjectSetupRequired'
|
import { ProjectSetupRequired } from './components/ProjectSetupRequired'
|
||||||
import { getDependencyGraph, startAgent } from './lib/api'
|
import { getDependencyGraph, startAgent } from './lib/api'
|
||||||
import { Loader2, Settings, Moon, Sun, RotateCcw } from 'lucide-react'
|
import { Loader2, Settings, Moon, Sun, RotateCcw, BookOpen } from 'lucide-react'
|
||||||
import type { Feature } from './lib/types'
|
import type { Feature } from './lib/types'
|
||||||
import { Button } from '@/components/ui/button'
|
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">
|
||||||
@@ -335,6 +338,17 @@ function App() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Docs link */}
|
||||||
|
<Button
|
||||||
|
onClick={() => window.open('https://autoforge.cc', '_blank')}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
title="Documentation"
|
||||||
|
aria-label="Open Documentation"
|
||||||
|
>
|
||||||
|
<BookOpen size={18} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* Theme selector */}
|
{/* Theme selector */}
|
||||||
<ThemeSelector
|
<ThemeSelector
|
||||||
themes={themes}
|
themes={themes}
|
||||||
@@ -365,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">
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { createRoot } from 'react-dom/client'
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
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: {
|
||||||
|
|||||||
@@ -1134,6 +1134,143 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
Documentation Prose Typography
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
.docs-prose {
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--foreground);
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 2px solid var(--border);
|
||||||
|
scroll-margin-top: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose h2:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose h3 {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--foreground);
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
scroll-margin-top: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
max-width: 65ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose ul,
|
||||||
|
.docs-prose ol {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose li {
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose li > ul,
|
||||||
|
.docs-prose li > ol {
|
||||||
|
margin-top: 0.375rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose pre {
|
||||||
|
background: var(--muted);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose code:not(pre code) {
|
||||||
|
background: var(--muted);
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose th {
|
||||||
|
background: var(--muted);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--foreground);
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose td {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose tr:nth-child(even) td {
|
||||||
|
background: var(--muted);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose blockquote {
|
||||||
|
border-left: 4px solid var(--primary);
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose a {
|
||||||
|
color: var(--primary);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose a:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose strong {
|
||||||
|
color: var(--foreground);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-prose hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
Scrollbar Styling
|
Scrollbar Styling
|
||||||
============================================================================ */
|
============================================================================ */
|
||||||
|
|||||||
@@ -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