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.
Files changed (84) hide show
  1. package/.claude/commands/check-code.md +32 -0
  2. package/.claude/commands/checkpoint.md +40 -0
  3. package/.claude/commands/create-spec.md +613 -0
  4. package/.claude/commands/expand-project.md +234 -0
  5. package/.claude/commands/gsd-to-autoforge-spec.md +10 -0
  6. package/.claude/commands/review-pr.md +75 -0
  7. package/.claude/templates/app_spec.template.txt +331 -0
  8. package/.claude/templates/coding_prompt.template.md +265 -0
  9. package/.claude/templates/initializer_prompt.template.md +354 -0
  10. package/.claude/templates/testing_prompt.template.md +146 -0
  11. package/.env.example +64 -0
  12. package/LICENSE.md +676 -0
  13. package/README.md +423 -0
  14. package/agent.py +444 -0
  15. package/api/__init__.py +10 -0
  16. package/api/database.py +536 -0
  17. package/api/dependency_resolver.py +449 -0
  18. package/api/migration.py +156 -0
  19. package/auth.py +83 -0
  20. package/autoforge_paths.py +315 -0
  21. package/autonomous_agent_demo.py +293 -0
  22. package/bin/autoforge.js +3 -0
  23. package/client.py +607 -0
  24. package/env_constants.py +27 -0
  25. package/examples/OPTIMIZE_CONFIG.md +230 -0
  26. package/examples/README.md +531 -0
  27. package/examples/org_config.yaml +172 -0
  28. package/examples/project_allowed_commands.yaml +139 -0
  29. package/lib/cli.js +791 -0
  30. package/mcp_server/__init__.py +1 -0
  31. package/mcp_server/feature_mcp.py +988 -0
  32. package/package.json +53 -0
  33. package/parallel_orchestrator.py +1800 -0
  34. package/progress.py +247 -0
  35. package/prompts.py +427 -0
  36. package/pyproject.toml +17 -0
  37. package/rate_limit_utils.py +132 -0
  38. package/registry.py +614 -0
  39. package/requirements-prod.txt +14 -0
  40. package/security.py +959 -0
  41. package/server/__init__.py +17 -0
  42. package/server/main.py +261 -0
  43. package/server/routers/__init__.py +32 -0
  44. package/server/routers/agent.py +177 -0
  45. package/server/routers/assistant_chat.py +327 -0
  46. package/server/routers/devserver.py +309 -0
  47. package/server/routers/expand_project.py +239 -0
  48. package/server/routers/features.py +746 -0
  49. package/server/routers/filesystem.py +514 -0
  50. package/server/routers/projects.py +524 -0
  51. package/server/routers/schedules.py +356 -0
  52. package/server/routers/settings.py +127 -0
  53. package/server/routers/spec_creation.py +357 -0
  54. package/server/routers/terminal.py +453 -0
  55. package/server/schemas.py +593 -0
  56. package/server/services/__init__.py +36 -0
  57. package/server/services/assistant_chat_session.py +496 -0
  58. package/server/services/assistant_database.py +304 -0
  59. package/server/services/chat_constants.py +57 -0
  60. package/server/services/dev_server_manager.py +557 -0
  61. package/server/services/expand_chat_session.py +399 -0
  62. package/server/services/process_manager.py +657 -0
  63. package/server/services/project_config.py +475 -0
  64. package/server/services/scheduler_service.py +683 -0
  65. package/server/services/spec_chat_session.py +502 -0
  66. package/server/services/terminal_manager.py +756 -0
  67. package/server/utils/__init__.py +1 -0
  68. package/server/utils/process_utils.py +134 -0
  69. package/server/utils/project_helpers.py +32 -0
  70. package/server/utils/validation.py +54 -0
  71. package/server/websocket.py +903 -0
  72. package/start.py +456 -0
  73. package/ui/dist/assets/index-8W_wmZzz.js +168 -0
  74. package/ui/dist/assets/index-B47Ubhox.css +1 -0
  75. package/ui/dist/assets/vendor-flow-CVNK-_lx.js +7 -0
  76. package/ui/dist/assets/vendor-query-BUABzP5o.js +1 -0
  77. package/ui/dist/assets/vendor-radix-DTNNCg2d.js +45 -0
  78. package/ui/dist/assets/vendor-react-qkC6yhPU.js +1 -0
  79. package/ui/dist/assets/vendor-utils-COeKbHgx.js +2 -0
  80. package/ui/dist/assets/vendor-xterm-DP_gxef0.js +16 -0
  81. package/ui/dist/index.html +23 -0
  82. package/ui/dist/ollama.png +0 -0
  83. package/ui/dist/vite.svg +6 -0
  84. 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()
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../lib/cli.js';
3
+ run(process.argv.slice(2));