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 @@
1
+ # Server utilities
@@ -0,0 +1,134 @@
1
+ """
2
+ Process Utilities
3
+ =================
4
+
5
+ Shared utilities for process management across the codebase.
6
+ """
7
+
8
+ import logging
9
+ import subprocess
10
+ from dataclasses import dataclass
11
+ from typing import Literal
12
+
13
+ import psutil
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class KillResult:
20
+ """Result of a process tree kill operation.
21
+
22
+ Attributes:
23
+ status: "success" if all processes terminated, "partial" if some required
24
+ force-kill, "failure" if parent couldn't be killed
25
+ parent_pid: PID of the parent process
26
+ children_found: Number of child processes found
27
+ children_terminated: Number of children that terminated gracefully
28
+ children_killed: Number of children that required SIGKILL
29
+ parent_forcekilled: Whether the parent required SIGKILL
30
+ """
31
+
32
+ status: Literal["success", "partial", "failure"]
33
+ parent_pid: int
34
+ children_found: int = 0
35
+ children_terminated: int = 0
36
+ children_killed: int = 0
37
+ parent_forcekilled: bool = False
38
+
39
+
40
+ def kill_process_tree(proc: subprocess.Popen, timeout: float = 5.0) -> KillResult:
41
+ """Kill a process and all its child processes.
42
+
43
+ On Windows, subprocess.terminate() only kills the immediate process, leaving
44
+ orphaned child processes (e.g., spawned browser instances, coding/testing agents).
45
+ This function uses psutil to kill the entire process tree.
46
+
47
+ Args:
48
+ proc: The subprocess.Popen object to kill
49
+ timeout: Seconds to wait for graceful termination before force-killing
50
+
51
+ Returns:
52
+ KillResult with status and statistics about the termination
53
+ """
54
+ result = KillResult(status="success", parent_pid=proc.pid)
55
+
56
+ try:
57
+ parent = psutil.Process(proc.pid)
58
+ # Get all children recursively before terminating
59
+ children = parent.children(recursive=True)
60
+ result.children_found = len(children)
61
+
62
+ logger.debug(
63
+ "Killing process tree: PID %d with %d children",
64
+ proc.pid, len(children)
65
+ )
66
+
67
+ # Terminate children first (graceful)
68
+ for child in children:
69
+ try:
70
+ logger.debug("Terminating child PID %d (%s)", child.pid, child.name())
71
+ child.terminate()
72
+ except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
73
+ # NoSuchProcess: already dead
74
+ # AccessDenied: Windows can raise this for system processes or already-exited processes
75
+ logger.debug("Child PID %d already gone or inaccessible: %s", child.pid, e)
76
+
77
+ # Wait for children to terminate
78
+ gone, still_alive = psutil.wait_procs(children, timeout=timeout)
79
+ result.children_terminated = len(gone)
80
+
81
+ logger.debug(
82
+ "Children after graceful wait: %d terminated, %d still alive",
83
+ len(gone), len(still_alive)
84
+ )
85
+
86
+ # Force kill any remaining children
87
+ for child in still_alive:
88
+ try:
89
+ logger.debug("Force-killing child PID %d", child.pid)
90
+ child.kill()
91
+ result.children_killed += 1
92
+ except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
93
+ logger.debug("Child PID %d gone during force-kill: %s", child.pid, e)
94
+
95
+ if result.children_killed > 0:
96
+ result.status = "partial"
97
+
98
+ # Now terminate the parent
99
+ logger.debug("Terminating parent PID %d", proc.pid)
100
+ proc.terminate()
101
+ try:
102
+ proc.wait(timeout=timeout)
103
+ logger.debug("Parent PID %d terminated gracefully", proc.pid)
104
+ except subprocess.TimeoutExpired:
105
+ logger.debug("Parent PID %d did not terminate, force-killing", proc.pid)
106
+ proc.kill()
107
+ proc.wait()
108
+ result.parent_forcekilled = True
109
+ result.status = "partial"
110
+
111
+ logger.debug(
112
+ "Process tree kill complete: status=%s, children=%d (terminated=%d, killed=%d)",
113
+ result.status, result.children_found,
114
+ result.children_terminated, result.children_killed
115
+ )
116
+
117
+ except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
118
+ # NoSuchProcess: Process already dead
119
+ # AccessDenied: Windows can raise this for protected/system processes
120
+ # In either case, just ensure cleanup
121
+ logger.debug("Parent PID %d inaccessible (%s), attempting direct cleanup", proc.pid, e)
122
+ try:
123
+ proc.terminate()
124
+ proc.wait(timeout=1)
125
+ logger.debug("Direct termination of PID %d succeeded", proc.pid)
126
+ except (subprocess.TimeoutExpired, OSError):
127
+ try:
128
+ proc.kill()
129
+ logger.debug("Direct force-kill of PID %d succeeded", proc.pid)
130
+ except OSError as kill_error:
131
+ logger.debug("Direct force-kill of PID %d failed: %s", proc.pid, kill_error)
132
+ result.status = "failure"
133
+
134
+ return result
@@ -0,0 +1,32 @@
1
+ """
2
+ Project Helper Utilities
3
+ ========================
4
+
5
+ Shared project path lookup used across all server routers and websocket handlers.
6
+ Consolidates the previously duplicated _get_project_path() function.
7
+ """
8
+
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ # Ensure the project root is on sys.path so `registry` can be imported.
13
+ # This is necessary because `registry.py` lives at the repository root,
14
+ # outside the `server` package.
15
+ _root = Path(__file__).parent.parent.parent
16
+ if str(_root) not in sys.path:
17
+ sys.path.insert(0, str(_root))
18
+
19
+ from registry import get_project_path as _registry_get_project_path
20
+
21
+
22
+ def get_project_path(project_name: str) -> Path | None:
23
+ """Look up a project's filesystem path from the global registry.
24
+
25
+ Args:
26
+ project_name: The registered name of the project.
27
+
28
+ Returns:
29
+ The resolved ``Path`` to the project directory, or ``None`` if the
30
+ project is not found in the registry.
31
+ """
32
+ return _registry_get_project_path(project_name)
@@ -0,0 +1,54 @@
1
+ """
2
+ Shared Validation Utilities
3
+ ============================
4
+
5
+ Project name validation used across REST endpoints and WebSocket handlers.
6
+ Two variants are provided:
7
+
8
+ * ``is_valid_project_name`` -- returns ``bool``, suitable for WebSocket
9
+ handlers where raising an HTTPException is not appropriate.
10
+ * ``validate_project_name`` -- raises ``HTTPException(400)`` on failure,
11
+ suitable for REST endpoint handlers.
12
+ """
13
+
14
+ import re
15
+
16
+ from fastapi import HTTPException
17
+
18
+ # Compiled once; reused by both variants.
19
+ _PROJECT_NAME_RE = re.compile(r'^[a-zA-Z0-9_-]{1,50}$')
20
+
21
+
22
+ def is_valid_project_name(name: str) -> bool:
23
+ """Check whether *name* is a valid project name.
24
+
25
+ Allows only ASCII letters, digits, hyphens, and underscores (1-50 chars).
26
+ Returns ``True`` if valid, ``False`` otherwise.
27
+
28
+ Use this in WebSocket handlers where you need to close the socket
29
+ yourself rather than raise an HTTP error.
30
+ """
31
+ return bool(_PROJECT_NAME_RE.match(name))
32
+
33
+
34
+ def validate_project_name(name: str) -> str:
35
+ """Validate and return *name*, or raise ``HTTPException(400)``.
36
+
37
+ Suitable for REST endpoint handlers where FastAPI will convert the
38
+ exception into an HTTP 400 response automatically.
39
+
40
+ Args:
41
+ name: Project name to validate.
42
+
43
+ Returns:
44
+ The validated project name (unchanged).
45
+
46
+ Raises:
47
+ HTTPException: If *name* is invalid.
48
+ """
49
+ if not _PROJECT_NAME_RE.match(name):
50
+ raise HTTPException(
51
+ status_code=400,
52
+ detail="Invalid project name. Use only letters, numbers, hyphens, and underscores (1-50 chars)."
53
+ )
54
+ return name