autoforge-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/check-code.md +32 -0
- package/.claude/commands/checkpoint.md +40 -0
- package/.claude/commands/create-spec.md +613 -0
- package/.claude/commands/expand-project.md +234 -0
- package/.claude/commands/gsd-to-autoforge-spec.md +10 -0
- package/.claude/commands/review-pr.md +75 -0
- package/.claude/templates/app_spec.template.txt +331 -0
- package/.claude/templates/coding_prompt.template.md +265 -0
- package/.claude/templates/initializer_prompt.template.md +354 -0
- package/.claude/templates/testing_prompt.template.md +146 -0
- package/.env.example +64 -0
- package/LICENSE.md +676 -0
- package/README.md +423 -0
- package/agent.py +444 -0
- package/api/__init__.py +10 -0
- package/api/database.py +536 -0
- package/api/dependency_resolver.py +449 -0
- package/api/migration.py +156 -0
- package/auth.py +83 -0
- package/autoforge_paths.py +315 -0
- package/autonomous_agent_demo.py +293 -0
- package/bin/autoforge.js +3 -0
- package/client.py +607 -0
- package/env_constants.py +27 -0
- package/examples/OPTIMIZE_CONFIG.md +230 -0
- package/examples/README.md +531 -0
- package/examples/org_config.yaml +172 -0
- package/examples/project_allowed_commands.yaml +139 -0
- package/lib/cli.js +791 -0
- package/mcp_server/__init__.py +1 -0
- package/mcp_server/feature_mcp.py +988 -0
- package/package.json +53 -0
- package/parallel_orchestrator.py +1800 -0
- package/progress.py +247 -0
- package/prompts.py +427 -0
- package/pyproject.toml +17 -0
- package/rate_limit_utils.py +132 -0
- package/registry.py +614 -0
- package/requirements-prod.txt +14 -0
- package/security.py +959 -0
- package/server/__init__.py +17 -0
- package/server/main.py +261 -0
- package/server/routers/__init__.py +32 -0
- package/server/routers/agent.py +177 -0
- package/server/routers/assistant_chat.py +327 -0
- package/server/routers/devserver.py +309 -0
- package/server/routers/expand_project.py +239 -0
- package/server/routers/features.py +746 -0
- package/server/routers/filesystem.py +514 -0
- package/server/routers/projects.py +524 -0
- package/server/routers/schedules.py +356 -0
- package/server/routers/settings.py +127 -0
- package/server/routers/spec_creation.py +357 -0
- package/server/routers/terminal.py +453 -0
- package/server/schemas.py +593 -0
- package/server/services/__init__.py +36 -0
- package/server/services/assistant_chat_session.py +496 -0
- package/server/services/assistant_database.py +304 -0
- package/server/services/chat_constants.py +57 -0
- package/server/services/dev_server_manager.py +557 -0
- package/server/services/expand_chat_session.py +399 -0
- package/server/services/process_manager.py +657 -0
- package/server/services/project_config.py +475 -0
- package/server/services/scheduler_service.py +683 -0
- package/server/services/spec_chat_session.py +502 -0
- package/server/services/terminal_manager.py +756 -0
- package/server/utils/__init__.py +1 -0
- package/server/utils/process_utils.py +134 -0
- package/server/utils/project_helpers.py +32 -0
- package/server/utils/validation.py +54 -0
- package/server/websocket.py +903 -0
- package/start.py +456 -0
- package/ui/dist/assets/index-8W_wmZzz.js +168 -0
- package/ui/dist/assets/index-B47Ubhox.css +1 -0
- package/ui/dist/assets/vendor-flow-CVNK-_lx.js +7 -0
- package/ui/dist/assets/vendor-query-BUABzP5o.js +1 -0
- package/ui/dist/assets/vendor-radix-DTNNCg2d.js +45 -0
- package/ui/dist/assets/vendor-react-qkC6yhPU.js +1 -0
- package/ui/dist/assets/vendor-utils-COeKbHgx.js +2 -0
- package/ui/dist/assets/vendor-xterm-DP_gxef0.js +16 -0
- package/ui/dist/index.html +23 -0
- package/ui/dist/ollama.png +0 -0
- package/ui/dist/vite.svg +6 -0
- package/ui/package.json +57 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AutoForge Path Resolution
|
|
3
|
+
=========================
|
|
4
|
+
|
|
5
|
+
Central module for resolving paths to autoforge-generated files within a project.
|
|
6
|
+
|
|
7
|
+
Implements a tri-path resolution strategy for backward compatibility:
|
|
8
|
+
|
|
9
|
+
1. Check ``project_dir / ".autoforge" / X`` (current layout)
|
|
10
|
+
2. Check ``project_dir / ".autocoder" / X`` (legacy layout)
|
|
11
|
+
3. Check ``project_dir / X`` (legacy root-level layout)
|
|
12
|
+
4. Default to the new location for fresh projects
|
|
13
|
+
|
|
14
|
+
This allows existing projects with root-level ``features.db``, ``.agent.lock``,
|
|
15
|
+
etc. to keep working while new projects store everything under ``.autoforge/``.
|
|
16
|
+
Projects using the old ``.autocoder/`` directory are auto-migrated on next start.
|
|
17
|
+
|
|
18
|
+
The ``migrate_project_layout`` function can move an old-layout project to the
|
|
19
|
+
new layout safely, with full integrity checks for SQLite databases.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import shutil
|
|
24
|
+
import sqlite3
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# .gitignore content written into every .autoforge/ directory
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
_GITIGNORE_CONTENT = """\
|
|
33
|
+
# AutoForge runtime files
|
|
34
|
+
features.db
|
|
35
|
+
features.db-wal
|
|
36
|
+
features.db-shm
|
|
37
|
+
assistant.db
|
|
38
|
+
assistant.db-wal
|
|
39
|
+
assistant.db-shm
|
|
40
|
+
.agent.lock
|
|
41
|
+
.devserver.lock
|
|
42
|
+
.claude_settings.json
|
|
43
|
+
.claude_assistant_settings.json
|
|
44
|
+
.claude_settings.expand.*.json
|
|
45
|
+
.progress_cache
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Private helpers
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
def _resolve_path(project_dir: Path, filename: str) -> Path:
|
|
54
|
+
"""Resolve a file path using tri-path strategy.
|
|
55
|
+
|
|
56
|
+
Checks the new ``.autoforge/`` location first, then the legacy
|
|
57
|
+
``.autocoder/`` location, then the root-level location. If none exist,
|
|
58
|
+
returns the new location so that newly-created files land in ``.autoforge/``.
|
|
59
|
+
"""
|
|
60
|
+
new = project_dir / ".autoforge" / filename
|
|
61
|
+
if new.exists():
|
|
62
|
+
return new
|
|
63
|
+
legacy = project_dir / ".autocoder" / filename
|
|
64
|
+
if legacy.exists():
|
|
65
|
+
return legacy
|
|
66
|
+
old = project_dir / filename
|
|
67
|
+
if old.exists():
|
|
68
|
+
return old
|
|
69
|
+
return new # default for new projects
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _resolve_dir(project_dir: Path, dirname: str) -> Path:
|
|
73
|
+
"""Resolve a directory path using tri-path strategy.
|
|
74
|
+
|
|
75
|
+
Same logic as ``_resolve_path`` but intended for directories such as
|
|
76
|
+
``prompts/``.
|
|
77
|
+
"""
|
|
78
|
+
new = project_dir / ".autoforge" / dirname
|
|
79
|
+
if new.exists():
|
|
80
|
+
return new
|
|
81
|
+
legacy = project_dir / ".autocoder" / dirname
|
|
82
|
+
if legacy.exists():
|
|
83
|
+
return legacy
|
|
84
|
+
old = project_dir / dirname
|
|
85
|
+
if old.exists():
|
|
86
|
+
return old
|
|
87
|
+
return new
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# .autoforge directory management
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def get_autoforge_dir(project_dir: Path) -> Path:
|
|
95
|
+
"""Return the ``.autoforge`` directory path. Does NOT create it."""
|
|
96
|
+
return project_dir / ".autoforge"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def ensure_autoforge_dir(project_dir: Path) -> Path:
|
|
100
|
+
"""Create the ``.autoforge/`` directory (if needed) and write its ``.gitignore``.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
The path to the ``.autoforge`` directory.
|
|
104
|
+
"""
|
|
105
|
+
autoforge_dir = get_autoforge_dir(project_dir)
|
|
106
|
+
autoforge_dir.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
|
|
108
|
+
gitignore_path = autoforge_dir / ".gitignore"
|
|
109
|
+
gitignore_path.write_text(_GITIGNORE_CONTENT, encoding="utf-8")
|
|
110
|
+
|
|
111
|
+
return autoforge_dir
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Dual-path file helpers
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def get_features_db_path(project_dir: Path) -> Path:
|
|
119
|
+
"""Resolve the path to ``features.db``."""
|
|
120
|
+
return _resolve_path(project_dir, "features.db")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_assistant_db_path(project_dir: Path) -> Path:
|
|
124
|
+
"""Resolve the path to ``assistant.db``."""
|
|
125
|
+
return _resolve_path(project_dir, "assistant.db")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_agent_lock_path(project_dir: Path) -> Path:
|
|
129
|
+
"""Resolve the path to ``.agent.lock``."""
|
|
130
|
+
return _resolve_path(project_dir, ".agent.lock")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_devserver_lock_path(project_dir: Path) -> Path:
|
|
134
|
+
"""Resolve the path to ``.devserver.lock``."""
|
|
135
|
+
return _resolve_path(project_dir, ".devserver.lock")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_claude_settings_path(project_dir: Path) -> Path:
|
|
139
|
+
"""Resolve the path to ``.claude_settings.json``."""
|
|
140
|
+
return _resolve_path(project_dir, ".claude_settings.json")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_claude_assistant_settings_path(project_dir: Path) -> Path:
|
|
144
|
+
"""Resolve the path to ``.claude_assistant_settings.json``."""
|
|
145
|
+
return _resolve_path(project_dir, ".claude_assistant_settings.json")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_progress_cache_path(project_dir: Path) -> Path:
|
|
149
|
+
"""Resolve the path to ``.progress_cache``."""
|
|
150
|
+
return _resolve_path(project_dir, ".progress_cache")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_prompts_dir(project_dir: Path) -> Path:
|
|
154
|
+
"""Resolve the path to the ``prompts/`` directory."""
|
|
155
|
+
return _resolve_dir(project_dir, "prompts")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# Non-dual-path helpers (always use new location)
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def get_expand_settings_path(project_dir: Path, uuid_hex: str) -> Path:
|
|
163
|
+
"""Return the path for an ephemeral expand-session settings file.
|
|
164
|
+
|
|
165
|
+
These files are short-lived and always stored in ``.autoforge/``.
|
|
166
|
+
"""
|
|
167
|
+
return project_dir / ".autoforge" / f".claude_settings.expand.{uuid_hex}.json"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# Lock-file safety check
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def has_agent_running(project_dir: Path) -> bool:
|
|
175
|
+
"""Check whether any agent or dev-server lock file exists at either location.
|
|
176
|
+
|
|
177
|
+
Inspects the legacy root-level paths, the old ``.autocoder/`` paths, and
|
|
178
|
+
the new ``.autoforge/`` paths so that a running agent is detected
|
|
179
|
+
regardless of project layout.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
``True`` if any ``.agent.lock`` or ``.devserver.lock`` exists.
|
|
183
|
+
"""
|
|
184
|
+
lock_names = (".agent.lock", ".devserver.lock")
|
|
185
|
+
for name in lock_names:
|
|
186
|
+
if (project_dir / name).exists():
|
|
187
|
+
return True
|
|
188
|
+
# Check both old and new directory names for backward compatibility
|
|
189
|
+
if (project_dir / ".autocoder" / name).exists():
|
|
190
|
+
return True
|
|
191
|
+
if (project_dir / ".autoforge" / name).exists():
|
|
192
|
+
return True
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# Migration
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
def migrate_project_layout(project_dir: Path) -> list[str]:
|
|
201
|
+
"""Migrate a project from the legacy root-level layout to ``.autoforge/``.
|
|
202
|
+
|
|
203
|
+
The migration is incremental and safe:
|
|
204
|
+
|
|
205
|
+
* If the agent is running (lock files present) the migration is skipped
|
|
206
|
+
entirely to avoid corrupting in-use databases.
|
|
207
|
+
* Each file/directory is migrated independently. If any single step
|
|
208
|
+
fails the error is logged and migration continues with the remaining
|
|
209
|
+
items. Partial migration is safe because the dual-path resolution
|
|
210
|
+
strategy will find files at whichever location they ended up in.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
A list of human-readable descriptions of what was migrated, e.g.
|
|
214
|
+
``["prompts/ -> .autoforge/prompts/", "features.db -> .autoforge/features.db"]``.
|
|
215
|
+
An empty list means nothing was migrated (either everything is
|
|
216
|
+
already migrated, or the agent is running).
|
|
217
|
+
"""
|
|
218
|
+
# Safety: refuse to migrate while an agent is running
|
|
219
|
+
if has_agent_running(project_dir):
|
|
220
|
+
logger.warning("Migration skipped: agent or dev-server is running for %s", project_dir)
|
|
221
|
+
return []
|
|
222
|
+
|
|
223
|
+
# --- 0. Migrate .autocoder/ → .autoforge/ directory -------------------
|
|
224
|
+
old_autocoder_dir = project_dir / ".autocoder"
|
|
225
|
+
new_autoforge_dir = project_dir / ".autoforge"
|
|
226
|
+
if old_autocoder_dir.exists() and old_autocoder_dir.is_dir() and not new_autoforge_dir.exists():
|
|
227
|
+
try:
|
|
228
|
+
old_autocoder_dir.rename(new_autoforge_dir)
|
|
229
|
+
logger.info("Migrated .autocoder/ -> .autoforge/")
|
|
230
|
+
migrated: list[str] = [".autocoder/ -> .autoforge/"]
|
|
231
|
+
except Exception:
|
|
232
|
+
logger.warning("Failed to migrate .autocoder/ -> .autoforge/", exc_info=True)
|
|
233
|
+
migrated = []
|
|
234
|
+
else:
|
|
235
|
+
migrated = []
|
|
236
|
+
|
|
237
|
+
autoforge_dir = ensure_autoforge_dir(project_dir)
|
|
238
|
+
|
|
239
|
+
# --- 1. Migrate prompts/ directory -----------------------------------
|
|
240
|
+
try:
|
|
241
|
+
old_prompts = project_dir / "prompts"
|
|
242
|
+
new_prompts = autoforge_dir / "prompts"
|
|
243
|
+
if old_prompts.exists() and old_prompts.is_dir() and not new_prompts.exists():
|
|
244
|
+
shutil.copytree(str(old_prompts), str(new_prompts))
|
|
245
|
+
shutil.rmtree(str(old_prompts))
|
|
246
|
+
migrated.append("prompts/ -> .autoforge/prompts/")
|
|
247
|
+
logger.info("Migrated prompts/ -> .autoforge/prompts/")
|
|
248
|
+
except Exception:
|
|
249
|
+
logger.warning("Failed to migrate prompts/ directory", exc_info=True)
|
|
250
|
+
|
|
251
|
+
# --- 2. Migrate SQLite databases (features.db, assistant.db) ---------
|
|
252
|
+
db_names = ("features.db", "assistant.db")
|
|
253
|
+
for db_name in db_names:
|
|
254
|
+
try:
|
|
255
|
+
old_db = project_dir / db_name
|
|
256
|
+
new_db = autoforge_dir / db_name
|
|
257
|
+
if old_db.exists() and not new_db.exists():
|
|
258
|
+
# Flush WAL to ensure all data is in the main database file
|
|
259
|
+
conn = sqlite3.connect(str(old_db))
|
|
260
|
+
try:
|
|
261
|
+
cursor = conn.cursor()
|
|
262
|
+
cursor.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
263
|
+
finally:
|
|
264
|
+
conn.close()
|
|
265
|
+
|
|
266
|
+
# Copy the main database file (WAL is now flushed)
|
|
267
|
+
shutil.copy2(str(old_db), str(new_db))
|
|
268
|
+
|
|
269
|
+
# Verify the copy is intact
|
|
270
|
+
verify_conn = sqlite3.connect(str(new_db))
|
|
271
|
+
try:
|
|
272
|
+
verify_cursor = verify_conn.cursor()
|
|
273
|
+
result = verify_cursor.execute("PRAGMA integrity_check").fetchone()
|
|
274
|
+
if result is None or result[0] != "ok":
|
|
275
|
+
logger.error(
|
|
276
|
+
"Integrity check failed for migrated %s: %s",
|
|
277
|
+
db_name, result,
|
|
278
|
+
)
|
|
279
|
+
# Remove the broken copy; old file stays in place
|
|
280
|
+
new_db.unlink(missing_ok=True)
|
|
281
|
+
continue
|
|
282
|
+
finally:
|
|
283
|
+
verify_conn.close()
|
|
284
|
+
|
|
285
|
+
# Remove old database files (.db, .db-wal, .db-shm)
|
|
286
|
+
old_db.unlink(missing_ok=True)
|
|
287
|
+
for suffix in ("-wal", "-shm"):
|
|
288
|
+
wal_file = project_dir / f"{db_name}{suffix}"
|
|
289
|
+
wal_file.unlink(missing_ok=True)
|
|
290
|
+
|
|
291
|
+
migrated.append(f"{db_name} -> .autoforge/{db_name}")
|
|
292
|
+
logger.info("Migrated %s -> .autoforge/%s", db_name, db_name)
|
|
293
|
+
except Exception:
|
|
294
|
+
logger.warning("Failed to migrate %s", db_name, exc_info=True)
|
|
295
|
+
|
|
296
|
+
# --- 3. Migrate simple files -----------------------------------------
|
|
297
|
+
simple_files = (
|
|
298
|
+
".agent.lock",
|
|
299
|
+
".devserver.lock",
|
|
300
|
+
".claude_settings.json",
|
|
301
|
+
".claude_assistant_settings.json",
|
|
302
|
+
".progress_cache",
|
|
303
|
+
)
|
|
304
|
+
for filename in simple_files:
|
|
305
|
+
try:
|
|
306
|
+
old_file = project_dir / filename
|
|
307
|
+
new_file = autoforge_dir / filename
|
|
308
|
+
if old_file.exists() and not new_file.exists():
|
|
309
|
+
shutil.move(str(old_file), str(new_file))
|
|
310
|
+
migrated.append(f"{filename} -> .autoforge/{filename}")
|
|
311
|
+
logger.info("Migrated %s -> .autoforge/%s", filename, filename)
|
|
312
|
+
except Exception:
|
|
313
|
+
logger.warning("Failed to migrate %s", filename, exc_info=True)
|
|
314
|
+
|
|
315
|
+
return migrated
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Autonomous Coding Agent Demo
|
|
4
|
+
============================
|
|
5
|
+
|
|
6
|
+
A minimal harness demonstrating long-running autonomous coding with Claude.
|
|
7
|
+
This script implements a unified orchestrator pattern that handles:
|
|
8
|
+
- Initialization (creating features from app_spec)
|
|
9
|
+
- Coding agents (implementing features)
|
|
10
|
+
- Testing agents (regression testing)
|
|
11
|
+
|
|
12
|
+
Example Usage:
|
|
13
|
+
# Using absolute path directly
|
|
14
|
+
python autonomous_agent_demo.py --project-dir C:/Projects/my-app
|
|
15
|
+
|
|
16
|
+
# Using registered project name (looked up from registry)
|
|
17
|
+
python autonomous_agent_demo.py --project-dir my-app
|
|
18
|
+
|
|
19
|
+
# Limit iterations for testing (when running as subprocess)
|
|
20
|
+
python autonomous_agent_demo.py --project-dir my-app --max-iterations 5
|
|
21
|
+
|
|
22
|
+
# YOLO mode: rapid prototyping without testing agents
|
|
23
|
+
python autonomous_agent_demo.py --project-dir my-app --yolo
|
|
24
|
+
|
|
25
|
+
# Parallel execution with 3 concurrent coding agents
|
|
26
|
+
python autonomous_agent_demo.py --project-dir my-app --concurrency 3
|
|
27
|
+
|
|
28
|
+
# Single agent mode (orchestrator with concurrency=1, the default)
|
|
29
|
+
python autonomous_agent_demo.py --project-dir my-app
|
|
30
|
+
|
|
31
|
+
# Run as specific agent type (used by orchestrator to spawn subprocesses)
|
|
32
|
+
python autonomous_agent_demo.py --project-dir my-app --agent-type initializer
|
|
33
|
+
python autonomous_agent_demo.py --project-dir my-app --agent-type coding --feature-id 42
|
|
34
|
+
python autonomous_agent_demo.py --project-dir my-app --agent-type testing
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import argparse
|
|
38
|
+
import asyncio
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
from dotenv import load_dotenv
|
|
42
|
+
|
|
43
|
+
# Load environment variables from .env file (if it exists)
|
|
44
|
+
# IMPORTANT: Must be called BEFORE importing other modules that read env vars at load time
|
|
45
|
+
load_dotenv()
|
|
46
|
+
|
|
47
|
+
from agent import run_autonomous_agent
|
|
48
|
+
from registry import DEFAULT_MODEL, get_project_path
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_args() -> argparse.Namespace:
|
|
52
|
+
"""Parse command line arguments."""
|
|
53
|
+
parser = argparse.ArgumentParser(
|
|
54
|
+
description="Autonomous Coding Agent Demo - Unified orchestrator pattern",
|
|
55
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
56
|
+
epilog="""
|
|
57
|
+
Examples:
|
|
58
|
+
# Use absolute path directly (single agent, default)
|
|
59
|
+
python autonomous_agent_demo.py --project-dir C:/Projects/my-app
|
|
60
|
+
|
|
61
|
+
# Use registered project name (looked up from registry)
|
|
62
|
+
python autonomous_agent_demo.py --project-dir my-app
|
|
63
|
+
|
|
64
|
+
# Parallel execution with 3 concurrent agents
|
|
65
|
+
python autonomous_agent_demo.py --project-dir my-app --concurrency 3
|
|
66
|
+
|
|
67
|
+
# YOLO mode: rapid prototyping without testing agents
|
|
68
|
+
python autonomous_agent_demo.py --project-dir my-app --yolo
|
|
69
|
+
|
|
70
|
+
# Configure testing agent ratio (2 testing agents per coding agent)
|
|
71
|
+
python autonomous_agent_demo.py --project-dir my-app --testing-ratio 2
|
|
72
|
+
|
|
73
|
+
# Disable testing agents (similar to YOLO but with verification)
|
|
74
|
+
python autonomous_agent_demo.py --project-dir my-app --testing-ratio 0
|
|
75
|
+
|
|
76
|
+
Authentication:
|
|
77
|
+
Uses Claude CLI authentication (run 'claude login' if not logged in)
|
|
78
|
+
Authentication is handled by start.bat/start.sh before this runs
|
|
79
|
+
""",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--project-dir",
|
|
84
|
+
type=str,
|
|
85
|
+
required=True,
|
|
86
|
+
help="Project directory path (absolute) or registered project name",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--max-iterations",
|
|
91
|
+
type=int,
|
|
92
|
+
default=None,
|
|
93
|
+
help="Maximum number of agent iterations (default: unlimited, typically 1 for subprocesses)",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--model",
|
|
98
|
+
type=str,
|
|
99
|
+
default=DEFAULT_MODEL,
|
|
100
|
+
help=f"Claude model to use (default: {DEFAULT_MODEL})",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--yolo",
|
|
105
|
+
action="store_true",
|
|
106
|
+
default=False,
|
|
107
|
+
help="Enable YOLO mode: skip testing agents for rapid prototyping",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Unified orchestrator mode (replaces --parallel)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--concurrency", "-c",
|
|
113
|
+
type=int,
|
|
114
|
+
default=1,
|
|
115
|
+
help="Number of concurrent coding agents (default: 1, max: 5)",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Backward compatibility: --parallel is deprecated alias for --concurrency
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--parallel", "-p",
|
|
121
|
+
type=int,
|
|
122
|
+
nargs="?",
|
|
123
|
+
const=3,
|
|
124
|
+
default=None,
|
|
125
|
+
metavar="N",
|
|
126
|
+
help="DEPRECATED: Use --concurrency instead. Alias for --concurrency.",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
parser.add_argument(
|
|
130
|
+
"--feature-id",
|
|
131
|
+
type=int,
|
|
132
|
+
default=None,
|
|
133
|
+
help="Work on a specific feature ID only (used by orchestrator for coding agents)",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
"--feature-ids",
|
|
138
|
+
type=str,
|
|
139
|
+
default=None,
|
|
140
|
+
help="Comma-separated feature IDs to implement in batch (e.g., '5,8,12')",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Agent type for subprocess mode
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
"--agent-type",
|
|
146
|
+
choices=["initializer", "coding", "testing"],
|
|
147
|
+
default=None,
|
|
148
|
+
help="Agent type (used by orchestrator to spawn specialized subprocesses)",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
"--testing-feature-id",
|
|
153
|
+
type=int,
|
|
154
|
+
default=None,
|
|
155
|
+
help="Feature ID to regression test (used by orchestrator for testing agents, legacy single mode)",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"--testing-feature-ids",
|
|
160
|
+
type=str,
|
|
161
|
+
default=None,
|
|
162
|
+
help="Comma-separated feature IDs to regression test in batch (e.g., '5,12,18')",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Testing agent configuration
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"--testing-ratio",
|
|
168
|
+
type=int,
|
|
169
|
+
default=1,
|
|
170
|
+
help="Testing agents per coding agent (0-3, default: 1). Set to 0 to disable testing agents.",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
parser.add_argument(
|
|
174
|
+
"--testing-batch-size",
|
|
175
|
+
type=int,
|
|
176
|
+
default=3,
|
|
177
|
+
help="Number of features per testing batch (1-5, default: 3)",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
"--batch-size",
|
|
182
|
+
type=int,
|
|
183
|
+
default=3,
|
|
184
|
+
help="Max features per coding agent batch (1-3, default: 3)",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return parser.parse_args()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def main() -> None:
|
|
191
|
+
"""Main entry point."""
|
|
192
|
+
print("[ENTRY] autonomous_agent_demo.py starting...", flush=True)
|
|
193
|
+
args = parse_args()
|
|
194
|
+
|
|
195
|
+
# Note: Authentication is handled by start.bat/start.sh before this script runs.
|
|
196
|
+
# The Claude SDK auto-detects credentials from ~/.claude/.credentials.json
|
|
197
|
+
|
|
198
|
+
# Handle deprecated --parallel flag
|
|
199
|
+
if args.parallel is not None:
|
|
200
|
+
print("WARNING: --parallel is deprecated. Use --concurrency instead.", flush=True)
|
|
201
|
+
args.concurrency = args.parallel
|
|
202
|
+
|
|
203
|
+
# Resolve project directory:
|
|
204
|
+
# 1. If absolute path, use as-is
|
|
205
|
+
# 2. Otherwise, look up from registry by name
|
|
206
|
+
project_dir_input = args.project_dir
|
|
207
|
+
project_dir = Path(project_dir_input)
|
|
208
|
+
|
|
209
|
+
if project_dir.is_absolute():
|
|
210
|
+
# Absolute path provided - use directly
|
|
211
|
+
if not project_dir.exists():
|
|
212
|
+
print(f"Error: Project directory does not exist: {project_dir}")
|
|
213
|
+
return
|
|
214
|
+
else:
|
|
215
|
+
# Treat as a project name - look up from registry
|
|
216
|
+
registered_path = get_project_path(project_dir_input)
|
|
217
|
+
if registered_path:
|
|
218
|
+
project_dir = registered_path
|
|
219
|
+
else:
|
|
220
|
+
print(f"Error: Project '{project_dir_input}' not found in registry")
|
|
221
|
+
print("Use an absolute path or register the project first.")
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
# Migrate project layout to .autoforge/ if needed (idempotent, safe)
|
|
225
|
+
from autoforge_paths import migrate_project_layout
|
|
226
|
+
migrated = migrate_project_layout(project_dir)
|
|
227
|
+
if migrated:
|
|
228
|
+
print(f"Migrated project files to .autoforge/: {', '.join(migrated)}", flush=True)
|
|
229
|
+
|
|
230
|
+
# Parse batch testing feature IDs (comma-separated string -> list[int])
|
|
231
|
+
testing_feature_ids: list[int] | None = None
|
|
232
|
+
if args.testing_feature_ids:
|
|
233
|
+
try:
|
|
234
|
+
testing_feature_ids = [int(x.strip()) for x in args.testing_feature_ids.split(",") if x.strip()]
|
|
235
|
+
except ValueError:
|
|
236
|
+
print(f"Error: --testing-feature-ids must be comma-separated integers, got: {args.testing_feature_ids}")
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
# Parse batch coding feature IDs (comma-separated string -> list[int])
|
|
240
|
+
coding_feature_ids: list[int] | None = None
|
|
241
|
+
if args.feature_ids:
|
|
242
|
+
try:
|
|
243
|
+
coding_feature_ids = [int(x.strip()) for x in args.feature_ids.split(",") if x.strip()]
|
|
244
|
+
except ValueError:
|
|
245
|
+
print(f"Error: --feature-ids must be comma-separated integers, got: {args.feature_ids}")
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
if args.agent_type:
|
|
250
|
+
# Subprocess mode - spawned by orchestrator for a specific role
|
|
251
|
+
asyncio.run(
|
|
252
|
+
run_autonomous_agent(
|
|
253
|
+
project_dir=project_dir,
|
|
254
|
+
model=args.model,
|
|
255
|
+
max_iterations=args.max_iterations or 1,
|
|
256
|
+
yolo_mode=args.yolo,
|
|
257
|
+
feature_id=args.feature_id,
|
|
258
|
+
feature_ids=coding_feature_ids,
|
|
259
|
+
agent_type=args.agent_type,
|
|
260
|
+
testing_feature_id=args.testing_feature_id,
|
|
261
|
+
testing_feature_ids=testing_feature_ids,
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
else:
|
|
265
|
+
# Entry point mode - always use unified orchestrator
|
|
266
|
+
from parallel_orchestrator import run_parallel_orchestrator
|
|
267
|
+
|
|
268
|
+
# Clamp concurrency to valid range (1-5)
|
|
269
|
+
concurrency = max(1, min(args.concurrency, 5))
|
|
270
|
+
if concurrency != args.concurrency:
|
|
271
|
+
print(f"Clamping concurrency to valid range: {concurrency}", flush=True)
|
|
272
|
+
|
|
273
|
+
asyncio.run(
|
|
274
|
+
run_parallel_orchestrator(
|
|
275
|
+
project_dir=project_dir,
|
|
276
|
+
max_concurrency=concurrency,
|
|
277
|
+
model=args.model,
|
|
278
|
+
yolo_mode=args.yolo,
|
|
279
|
+
testing_agent_ratio=args.testing_ratio,
|
|
280
|
+
testing_batch_size=args.testing_batch_size,
|
|
281
|
+
batch_size=args.batch_size,
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
except KeyboardInterrupt:
|
|
285
|
+
print("\n\nInterrupted by user")
|
|
286
|
+
print("To resume, run the same command again")
|
|
287
|
+
except Exception as e:
|
|
288
|
+
print(f"\nFatal error: {e}")
|
|
289
|
+
raise
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
if __name__ == "__main__":
|
|
293
|
+
main()
|
package/bin/autoforge.js
ADDED