Compare commits

..

11 Commits

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

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

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

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

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

Published as autoforge-ai@0.1.0 on npm.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 08:50:36 +02:00
Abigail Green
f32e7efda6 Adding Scrollbar and Improving vite config
Add scrollbar to Mission Control panel
Improve code-splitting to reduce bundle size
2026-02-03 12:18:53 -07:00
Auto
b2ab1ecc7a add documentation 2026-02-01 17:34:07 +02:00
Auto
016eead8b4 fix: update log prefix when batch agent moves between features
The output reader was stamping every line with the primary feature ID
(e.g., [Feature #24]) even after the agent claimed a new feature in the
batch. Now parses feature_claim_and_get calls in the output stream and
switches the prefix to the newly claimed feature ID, so logs correctly
show [Feature #30] once the agent moves on.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 16:42:56 +02:00
63 changed files with 1749 additions and 401 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
.gitignore vendored
View File

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

32
.npmignore Normal file
View File

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

234
CLAUDE.md
View File

@@ -17,18 +17,28 @@ This is an autonomous coding agent system with a React-based UI. It uses the Cla
## Commands
### Quick Start (Recommended)
### npm Global Install (Recommended)
```bash
# Windows - launches CLI menu
start.bat
npm install -g autoforge-ai
autoforge # Start server (first run sets up Python venv)
autoforge config # Edit ~/.autoforge/.env in $EDITOR
autoforge config --show # Print active configuration
autoforge --port 9999 # Custom port
autoforge --no-browser # Don't auto-open browser
autoforge --repair # Delete and recreate ~/.autoforge/venv/
```
# macOS/Linux
./start.sh
### From Source (Development)
```bash
# Launch Web UI (serves pre-built React app)
start_ui.bat # Windows
./start_ui.sh # macOS/Linux
# CLI menu
start.bat # Windows
./start.sh # macOS/Linux
```
### Python Backend (Manual)
@@ -54,6 +64,12 @@ python autonomous_agent_demo.py --project-dir my-app --yolo
# Parallel mode: run multiple agents concurrently (1-5 agents)
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)
@@ -68,7 +84,7 @@ python autonomous_agent_demo.py --project-dir my-app --yolo
```
**What's different in YOLO mode:**
- No regression testing (skips `feature_get_for_regression`)
- No regression testing
- No Playwright MCP server (browser automation disabled)
- Features marked passing after lint/type-check succeeds
- Faster iteration for prototyping
@@ -97,10 +113,13 @@ npm run lint # Run ESLint
### Python
```bash
ruff check . # Lint
mypy . # Type check
python test_security.py # Security unit tests (163 tests)
python test_security_integration.py # Integration tests (9 tests)
ruff check . # Lint
mypy . # Type check
python test_security.py # Security unit tests (12 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
@@ -108,11 +127,17 @@ python test_security_integration.py # Integration tests (9 tests)
```bash
cd ui
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: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
Configuration in `pyproject.toml`:
@@ -121,24 +146,40 @@ Configuration in `pyproject.toml`:
## Architecture
### npm CLI (bin/, lib/)
The `autoforge` command is a Node.js wrapper that manages the Python environment and server lifecycle:
- `bin/autoforge.js` - Entry point (shebang script)
- `lib/cli.js` - Main CLI logic: Python 3.11+ detection (cross-platform), venv management at `~/.autoforge/venv/` with composite marker (requirements hash + Python version), `.env` config loading from `~/.autoforge/.env`, uvicorn server startup with PID file, and signal handling
- `package.json` - npm package config (`autoforge-ai` on npm), `files` whitelist with `__pycache__` exclusions, `prepublishOnly` builds the UI
- `requirements-prod.txt` - Runtime-only Python deps (excludes ruff, mypy, pytest)
- `.npmignore` - Excludes dev files, tests, UI source from the published tarball
Publishing: `npm publish` (triggers `prepublishOnly` which builds UI, then publishes ~600KB tarball with 84 files)
### Core Python Modules
- `start.py` - CLI launcher with project creation/selection menu
- `autonomous_agent_demo.py` - Entry point for running the agent
- `autocoder_paths.py` - Central path resolution with dual-path backward compatibility and migration
- `autonomous_agent_demo.py` - Entry point for running the agent (supports `--yolo`, `--parallel`, `--batch-size`, `--batch-features`)
- `autoforge_paths.py` - Central path resolution with dual-path backward compatibility and migration
- `agent.py` - Agent session loop using Claude Agent SDK
- `client.py` - ClaudeSDKClient configuration with security hooks 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)
- `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
- `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
- `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/migration.py` - JSON-to-SQLite migration utility
### Project Registry
Projects can be stored in any directory. The registry maps project names to paths using SQLite:
- **All platforms**: `~/.autocoder/registry.db`
- **All platforms**: `~/.autoforge/registry.db`
The registry uses:
- SQLite database with SQLAlchemy ORM
@@ -147,13 +188,36 @@ The registry uses:
### 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
- `server/routers/features.py` - Feature management
- `server/routers/agent.py` - Agent control (start/stop/pause/resume)
- `server/routers/filesystem.py` - Filesystem browser API with security controls
- `server/routers/spec_creation.py` - WebSocket for interactive spec creation
**Routers** (`server/routers/`):
- `projects.py` - Project CRUD with registry integration
- `features.py` - Feature management
- `agent.py` - Agent control (start/stop/pause/resume)
- `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
@@ -164,18 +228,26 @@ Features are stored in SQLite (`features.db`) via SQLAlchemy. The agent interact
MCP tools available to the agent:
- `feature_get_stats` - Progress statistics
- `feature_get_next` - Get highest-priority pending feature (respects dependencies)
- `feature_claim_next` - Atomically claim next available feature (for parallel mode)
- `feature_get_for_regression` - Random passing features for regression testing
- `feature_get_by_id` - Get a single feature by ID
- `feature_get_summary` - Get summary of all features
- `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_failing` - Mark feature as failing
- `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` - Create a single feature
- `feature_add_dependency` - Add dependency between features (with cycle detection)
- `feature_remove_dependency` - Remove a dependency
- `feature_set_dependencies` - Set all dependencies for a feature at once
### 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/hooks/useWebSocket.ts` - Real-time updates via WebSocket (progress, agent status, logs, agent updates)
- `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
- `CelebrationOverlay.tsx` - Confetti animation on feature completion
- `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):
- `D` - Toggle debug panel
@@ -197,18 +280,18 @@ Keyboard shortcuts (press `?` for help):
### Project Structure for Generated Apps
Projects can be stored in any directory (registered in `~/.autocoder/registry.db`). Each project contains:
- `.autocoder/prompts/app_spec.txt` - Application specification (XML format)
- `.autocoder/prompts/initializer_prompt.md` - First session prompt
- `.autocoder/prompts/coding_prompt.md` - Continuation session prompt
- `.autocoder/features.db` - SQLite database with feature test cases
- `.autocoder/.agent.lock` - Lock file to prevent multiple agent instances
- `.autocoder/allowed_commands.yaml` - Project-specific bash command allowlist (optional)
- `.autocoder/.gitignore` - Ignores runtime files
Projects can be stored in any directory (registered in `~/.autoforge/registry.db`). Each project contains:
- `.autoforge/prompts/app_spec.txt` - Application specification (XML format)
- `.autoforge/prompts/initializer_prompt.md` - First session prompt
- `.autoforge/prompts/coding_prompt.md` - Continuation session prompt
- `.autoforge/features.db` - SQLite database with feature test cases
- `.autoforge/.agent.lock` - Lock file to prevent multiple agent instances
- `.autoforge/allowed_commands.yaml` - Project-specific bash command allowlist (optional)
- `.autoforge/.gitignore` - Ignores runtime files
- `CLAUDE.md` - Stays at project root (SDK convention)
- `app_spec.txt` - Root copy for agent template compatibility
Legacy projects with files at root level (e.g., `features.db`, `prompts/`) are auto-migrated to `.autocoder/` on next agent start. Dual-path resolution ensures old and new layouts work transparently.
Legacy projects with files at root level (e.g., `features.db`, `prompts/`) are auto-migrated to `.autoforge/` on next agent start. Dual-path resolution ensures old and new layouts work transparently.
### Security Model
@@ -248,29 +331,20 @@ The following directories (relative to home) are always blocked:
- `.docker`, `.config/gcloud` - Container/cloud configs
- `.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
The agent's bash command access is controlled through a hierarchical configuration system:
**Command Hierarchy (highest to lowest priority):**
1. **Hardcoded Blocklist** (`security.py`) - NEVER allowed (dd, sudo, shutdown, etc.)
2. **Org Blocklist** (`~/.autocoder/config.yaml`) - Cannot be overridden by projects
3. **Org Allowlist** (`~/.autocoder/config.yaml`) - Available to all projects
2. **Org Blocklist** (`~/.autoforge/config.yaml`) - Cannot be overridden by projects
3. **Org Allowlist** (`~/.autoforge/config.yaml`) - Available to all projects
4. **Global Allowlist** (`security.py`) - Default commands (npm, git, curl, etc.)
5. **Project Allowlist** (`.autocoder/allowed_commands.yaml`) - Project-specific commands
5. **Project Allowlist** (`.autoforge/allowed_commands.yaml`) - Project-specific commands
**Project Configuration:**
Each project can define custom allowed commands in `.autocoder/allowed_commands.yaml`:
Each project can define custom allowed commands in `.autoforge/allowed_commands.yaml`:
```yaml
version: 1
@@ -290,7 +364,7 @@ commands:
**Organization Configuration:**
System administrators can set org-wide policies in `~/.autocoder/config.yaml`:
System administrators can set org-wide policies in `~/.autoforge/config.yaml`:
```yaml
version: 1
@@ -318,13 +392,29 @@ blocked_commands:
**Files:**
- `security.py` - Command validation logic and hardcoded blocklist
- `test_security.py` - Unit tests for security system (136 tests)
- `test_security_integration.py` - Integration tests with real hooks (9 tests)
- `TEST_SECURITY.md` - Quick testing reference guide
- `test_security.py` - Unit tests for security system
- `test_security_integration.py` - Integration tests with real hooks
- `examples/project_allowed_commands.yaml` - Project 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
### 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)
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_HAIKU_MODEL=qwen3-coder
```
5. Run autocoder normally - it will use your local Ollama models
5. Run AutoForge normally - it will use your local Ollama models
**Recommended coding models:**
- `qwen3-coder` - Good balance of speed and capability
@@ -360,8 +450,24 @@ Run coding agents using local models via Ollama v0.14.0+:
## Claude Code Integration
- `.claude/commands/create-spec.md` - `/create-spec` slash command for interactive spec creation
- `.claude/skills/frontend-design/SKILL.md` - Skill for distinctive UI design
**Slash commands** (`.claude/commands/`):
- `/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
- `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
1. Project-specific: `{project_dir}/.autocoder/prompts/{name}.md` (or legacy `{project_dir}/prompts/{name}.md`)
1. Project-specific: `{project_dir}/.autoforge/prompts/{name}.md` (or legacy `{project_dir}/prompts/{name}.md`)
2. Base template: `.claude/templates/{name}.template.md`
### Agent Session Flow
1. Check if `.autocoder/features.db` has features (determines initializer vs coding agent)
1. Check if `.autoforge/features.db` has features (determines initializer vs coding agent)
2. Create ClaudeSDKClient with security settings
3. Send prompt and stream response
4. Auto-continue with 3-second delay between sessions
@@ -392,7 +498,7 @@ The UI receives updates via WebSocket (`/ws/projects/{project_name}`):
When running with `--parallel`, the orchestrator:
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
4. Browser contexts are isolated per agent using `--isolated` flag
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)
- 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
The UI uses a **neobrutalism** design with Tailwind CSS v4:

190
README.md
View File

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

View File

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

View File

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

View File

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

3
bin/autoforge.js Normal file
View File

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

View File

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

View File

@@ -7,7 +7,7 @@ subprocesses. Imported by both ``client.py`` (agent sessions) and
``server/services/chat_constants.py`` (chat sessions) to avoid maintaining
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.
"""

View File

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

View File

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

View File

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

View File

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

791
lib/cli.js Normal file
View File

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

53
package.json Normal file
View File

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

View File

@@ -22,6 +22,7 @@ import asyncio
import atexit
import logging
import os
import re
import signal
import subprocess
import sys
@@ -39,11 +40,11 @@ from server.utils.process_utils import kill_process_tree
logger = logging.getLogger(__name__)
# Root directory of autocoder (where this script and autonomous_agent_demo.py live)
AUTOCODER_ROOT = Path(__file__).parent.resolve()
# Root directory of autoforge (where this script and autonomous_agent_demo.py live)
AUTOFORGE_ROOT = Path(__file__).parent.resolve()
# Debug log file path
DEBUG_LOG_FILE = AUTOCODER_ROOT / "orchestrator_debug.log"
DEBUG_LOG_FILE = AUTOFORGE_ROOT / "orchestrator_debug.log"
class DebugLogger:
@@ -822,7 +823,7 @@ class ParallelOrchestrator:
cmd = [
sys.executable,
"-u", # Force unbuffered stdout/stderr
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
"--project-dir", str(self.project_dir),
"--max-iterations", "1",
"--agent-type", "coding",
@@ -844,7 +845,7 @@ class ParallelOrchestrator:
"text": True,
"encoding": "utf-8",
"errors": "replace",
"cwd": str(AUTOCODER_ROOT), # Run from autocoder root for proper imports
"cwd": str(self.project_dir), # Run from project dir so CLI creates .claude/ in project
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
}
if sys.platform == "win32":
@@ -888,7 +889,7 @@ class ParallelOrchestrator:
cmd = [
sys.executable,
"-u",
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
"--project-dir", str(self.project_dir),
"--max-iterations", "1",
"--agent-type", "coding",
@@ -907,7 +908,7 @@ class ParallelOrchestrator:
"text": True,
"encoding": "utf-8",
"errors": "replace",
"cwd": str(AUTOCODER_ROOT),
"cwd": str(self.project_dir), # Run from project dir so CLI creates .claude/ in project
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
}
if sys.platform == "win32":
@@ -991,7 +992,7 @@ class ParallelOrchestrator:
cmd = [
sys.executable,
"-u",
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
"--project-dir", str(self.project_dir),
"--max-iterations", "1",
"--agent-type", "testing",
@@ -1011,7 +1012,7 @@ class ParallelOrchestrator:
"text": True,
"encoding": "utf-8",
"errors": "replace",
"cwd": str(AUTOCODER_ROOT),
"cwd": str(self.project_dir), # Run from project dir so CLI creates .claude/ in project
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
}
if sys.platform == "win32":
@@ -1052,7 +1053,7 @@ class ParallelOrchestrator:
cmd = [
sys.executable, "-u",
str(AUTOCODER_ROOT / "autonomous_agent_demo.py"),
str(AUTOFORGE_ROOT / "autonomous_agent_demo.py"),
"--project-dir", str(self.project_dir),
"--agent-type", "initializer",
"--max-iterations", "1",
@@ -1072,7 +1073,7 @@ class ParallelOrchestrator:
"text": True,
"encoding": "utf-8",
"errors": "replace",
"cwd": str(AUTOCODER_ROOT),
"cwd": str(AUTOFORGE_ROOT),
"env": {**os.environ, "PYTHONUNBUFFERED": "1"},
}
if sys.platform == "win32":
@@ -1116,6 +1117,11 @@ class ParallelOrchestrator:
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(
self,
feature_id: int | None,
@@ -1124,6 +1130,7 @@ class ParallelOrchestrator:
agent_type: Literal["coding", "testing"] = "coding",
):
"""Read output from subprocess and emit events."""
current_feature_id = feature_id
try:
if proc.stdout is None:
proc.wait()
@@ -1132,11 +1139,17 @@ class ParallelOrchestrator:
if abort.is_set():
break
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:
self.on_output(feature_id or 0, line)
self.on_output(current_feature_id or 0, line)
else:
# 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()
finally:
# CRITICAL: Kill the process tree to clean up any child processes (e.g., Claude CLI)

View File

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

View File

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

View File

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

14
requirements-prod.txt Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

20
ui/package-lock.json generated
View File

@@ -1,11 +1,11 @@
{
"name": "autocoder",
"name": "autoforge-ui",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "autocoder",
"name": "autoforge-ui",
"version": "1.0.0",
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.3",
@@ -20,6 +20,7 @@
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"@xyflow/react": "^12.10.0",
"autoforge-ai": "file:..",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -50,6 +51,17 @@
"vite": "^7.3.0"
}
},
"..": {
"name": "autoforge-ai",
"version": "0.1.0",
"license": "AGPL-3.0",
"bin": {
"autoforge": "bin/autoforge.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -3136,6 +3148,10 @@
"node": ">=10"
}
},
"node_modules/autoforge-ai": {
"resolved": "..",
"link": true
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",

View File

@@ -1,5 +1,5 @@
{
"name": "autocoder",
"name": "autoforge-ui",
"private": true,
"version": "1.0.0",
"type": "module",
@@ -24,6 +24,7 @@
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"@xyflow/react": "^12.10.0",
"autoforge-ai": "file:..",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",

BIN
ui/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

View File

@@ -28,14 +28,14 @@ import { ThemeSelector } from './components/ThemeSelector'
import { ResetProjectModal } from './components/ResetProjectModal'
import { ProjectSetupRequired } from './components/ProjectSetupRequired'
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 { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
const STORAGE_KEY = 'autocoder-selected-project'
const VIEW_MODE_KEY = 'autocoder-view-mode'
const STORAGE_KEY = 'autoforge-selected-project'
const VIEW_MODE_KEY = 'autoforge-view-mode'
// Bottom padding for main content when debug panel is collapsed (40px header + 8px margin)
const COLLAPSED_DEBUG_PANEL_CLEARANCE = 48
@@ -263,9 +263,12 @@ function App() {
<div className="max-w-7xl mx-auto px-4 py-4">
<div className="flex items-center justify-between">
{/* Logo and Title */}
<h1 className="font-display text-2xl font-bold tracking-tight uppercase">
AutoCoder
</h1>
<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">
AutoForge
</h1>
</div>
{/* Controls */}
<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 */}
<ThemeSelector
themes={themes}
@@ -365,7 +379,7 @@ function App() {
{!selectedProject ? (
<div className="text-center mt-12">
<h2 className="font-display text-2xl font-bold mb-2">
Welcome to AutoCoder
Welcome to AutoForge
</h2>
<p className="text-muted-foreground mb-4">
Select a project from the dropdown above or create a new one to get started.

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App'
import './styles/globals.css'
// Note: Custom theme removed - using shadcn/ui theming instead
const queryClient = new QueryClient({
defaultOptions: {

View File

@@ -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
============================================================================ */

View File

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