codeframe-ai 0.9.0__py3-none-any.whl

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 (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,183 @@
1
+ """Per-workspace outbound webhook notification config (issue #560).
2
+
3
+ Stored alongside other workspace UI settings under ``.codeframe/`` as a JSON
4
+ file. Headless — no FastAPI or HTTP imports.
5
+
6
+ Schema (``.codeframe/notifications_config.json``):
7
+
8
+ {
9
+ "webhook_url": "https://hooks.example.com/...", // or null
10
+ "webhook_enabled": true
11
+ }
12
+
13
+ Defense-in-depth: ``is_webhook_active`` re-validates the URL scheme/host
14
+ because the JSON file can be hand-edited or migrated from an older
15
+ deployment that didn't enforce validation at write time. The router PUT
16
+ endpoint validates too — never trust just one layer.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import logging
23
+ import os
24
+ import tempfile
25
+ from pathlib import Path
26
+ from typing import Optional, TypedDict
27
+
28
+ from codeframe.core.workspace import Workspace
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ NOTIFICATIONS_CONFIG_FILENAME = "notifications_config.json"
33
+
34
+ # Intentionally duplicated with codeframe/ui/routers/settings_v2.py's
35
+ # ``_ALLOWED_WEBHOOK_SCHEMES``: core cannot import from the UI layer
36
+ # (architecture rule #1 — core must be headless). Keep both values in
37
+ # sync if extended.
38
+ _ALLOWED_SCHEMES = frozenset({"http", "https"})
39
+
40
+
41
+ class NotificationsConfig(TypedDict):
42
+ webhook_url: Optional[str]
43
+ webhook_enabled: bool
44
+
45
+
46
+ _DEFAULT: NotificationsConfig = {"webhook_url": None, "webhook_enabled": False}
47
+
48
+
49
+ def _config_path(workspace: Workspace) -> Path:
50
+ return workspace.state_dir / NOTIFICATIONS_CONFIG_FILENAME
51
+
52
+
53
+ def _is_safe_webhook_url(url: str) -> bool:
54
+ """Defence-in-depth: scheme/host check on a stored URL before we POST.
55
+
56
+ The router PUT validates too, but a hand-edited or pre-migration JSON
57
+ file could carry a ``file://`` or schemeless URL. Used by
58
+ ``is_webhook_active`` to fail-safe to ``None``.
59
+
60
+ TODO: This does NOT block RFC-1918 (10/8, 172.16/12, 192.168/16) or
61
+ loopback (127/8) addresses. For a self-hosted single-user tool that is
62
+ intentional — users want to point at local receivers. If CodeFRAME ever
63
+ runs as a shared / multi-tenant service, add a socket-level check here
64
+ that resolves the host and rejects private/loopback ranges.
65
+ """
66
+ from urllib.parse import urlparse
67
+
68
+ try:
69
+ parsed = urlparse(url)
70
+ except (TypeError, ValueError):
71
+ return False
72
+ return parsed.scheme.lower() in _ALLOWED_SCHEMES and bool(parsed.netloc)
73
+
74
+
75
+ def load_notifications_config(workspace: Workspace) -> NotificationsConfig:
76
+ """Read notifications config, returning defaults on missing/corrupt file.
77
+
78
+ Never raises — a broken config should not break the triggering operation.
79
+ Non-object JSON (``[]``, ``null``, a bare integer) is treated as corrupt
80
+ and falls back to defaults rather than raising ``AttributeError`` from
81
+ a downstream ``.get()`` call.
82
+ """
83
+ path = _config_path(workspace)
84
+ if not path.exists():
85
+ return dict(_DEFAULT) # type: ignore[return-value]
86
+ try:
87
+ data = json.loads(path.read_text())
88
+ if not isinstance(data, dict):
89
+ raise ValueError(
90
+ f"Expected JSON object, got {type(data).__name__}"
91
+ )
92
+ return {
93
+ "webhook_url": data.get("webhook_url") or None,
94
+ "webhook_enabled": bool(data.get("webhook_enabled", False)),
95
+ }
96
+ except (OSError, json.JSONDecodeError, ValueError) as e:
97
+ logger.warning(
98
+ "Invalid notifications_config.json — falling back to defaults: %s", e
99
+ )
100
+ return dict(_DEFAULT) # type: ignore[return-value]
101
+
102
+
103
+ def save_notifications_config(
104
+ workspace: Workspace, config: NotificationsConfig
105
+ ) -> None:
106
+ """Atomically persist notifications config to disk."""
107
+ path = _config_path(workspace)
108
+ payload = {
109
+ "webhook_url": config.get("webhook_url") or None,
110
+ "webhook_enabled": bool(config.get("webhook_enabled", False)),
111
+ }
112
+ path.parent.mkdir(parents=True, exist_ok=True)
113
+ fd, tmp_name = tempfile.mkstemp(
114
+ prefix=f".{path.name}.", suffix=".tmp", dir=path.parent
115
+ )
116
+ try:
117
+ with os.fdopen(fd, "w") as f:
118
+ f.write(json.dumps(payload, indent=2))
119
+ os.replace(tmp_name, path)
120
+ except Exception:
121
+ try:
122
+ os.unlink(tmp_name)
123
+ except OSError:
124
+ pass
125
+ raise
126
+
127
+
128
+ def is_webhook_active(workspace: Workspace) -> Optional[str]:
129
+ """Return the webhook URL if URL is set, enabled flag is on, AND the URL
130
+ passes basic safety checks (``http(s)`` scheme + non-empty host).
131
+
132
+ Returns ``None`` otherwise — callers should short-circuit on ``None`` to
133
+ avoid instantiating the webhook service for nothing. The safety check is
134
+ intentionally redundant with the PUT-endpoint validation: it protects
135
+ against hand-edited config files and pre-migration data.
136
+ """
137
+ cfg = load_notifications_config(workspace)
138
+ url = (cfg["webhook_url"] or "").strip()
139
+ if not url or not cfg["webhook_enabled"]:
140
+ return None
141
+ if not _is_safe_webhook_url(url):
142
+ logger.warning(
143
+ "Refusing to dispatch webhook to unsafe URL: %s",
144
+ _redact_url_for_log(url),
145
+ )
146
+ return None
147
+ return url
148
+
149
+
150
+ def _redact_url_for_log(url: str) -> str:
151
+ """Return a logging-safe representation of a webhook URL.
152
+
153
+ Slack/Discord/GitHub-style webhook URLs commonly embed secrets in:
154
+
155
+ * the path or query (Slack's ``T*/B*/...`` token, signed Zapier hooks)
156
+ * basic-auth credentials in ``user:password@host`` form
157
+
158
+ ``parsed.netloc`` preserves the userinfo segment, so we use
159
+ ``parsed.hostname`` (which strips auth and port) and re-attach the
160
+ port explicitly. Returns ``scheme://host[:port]`` when parsable, else
161
+ ``"<unparseable>"``.
162
+ """
163
+ from urllib.parse import urlparse
164
+
165
+ try:
166
+ parsed = urlparse(url)
167
+ except (TypeError, ValueError):
168
+ return "<unparseable>"
169
+ if not parsed.scheme or not parsed.hostname:
170
+ return "<unparseable>"
171
+ host = parsed.hostname
172
+ # ``parsed.port`` raises ValueError for malformed ports (e.g.
173
+ # ``file://host:abc/x``). Without this guard the "fail-safe" branch
174
+ # in ``is_webhook_active`` could end up raising instead of returning
175
+ # None, which would bubble through every dispatch site's broad
176
+ # ``except Exception``.
177
+ try:
178
+ port = parsed.port
179
+ except ValueError:
180
+ return "<unparseable>"
181
+ if port is not None:
182
+ host = f"{host}:{port}"
183
+ return f"{parsed.scheme}://{host}"
@@ -0,0 +1,437 @@
1
+ """Agent planning module for CodeFRAME v2.
2
+
3
+ Transforms task context into an executable implementation plan.
4
+ Uses LLM to analyze requirements and decompose into actionable steps.
5
+
6
+ This module is headless - no FastAPI or HTTP dependencies.
7
+ """
8
+
9
+ import json
10
+ import re
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+
14
+ from codeframe.core.context import TaskContext
15
+ from codeframe.adapters.llm import LLMProvider, Purpose
16
+
17
+
18
+ class StepType(str, Enum):
19
+ """Type of implementation step."""
20
+
21
+ FILE_CREATE = "file_create" # Create a new file
22
+ FILE_EDIT = "file_edit" # Edit an existing file
23
+ FILE_DELETE = "file_delete" # Delete a file
24
+ SHELL_COMMAND = "shell_command" # Run a shell command
25
+ VERIFICATION = "verification" # Run tests/linting
26
+
27
+
28
+ class Complexity(str, Enum):
29
+ """Estimated task complexity."""
30
+
31
+ LOW = "low" # Simple change, < 50 lines
32
+ MEDIUM = "medium" # Moderate change, 50-200 lines
33
+ HIGH = "high" # Complex change, > 200 lines or architectural
34
+
35
+
36
+ @dataclass
37
+ class PlanStep:
38
+ """A single step in an implementation plan.
39
+
40
+ Attributes:
41
+ index: Step number (1-based)
42
+ type: Type of operation
43
+ description: What this step accomplishes
44
+ target: File path or command target
45
+ details: Additional details (e.g., what to change)
46
+ depends_on: Indices of steps this depends on
47
+ """
48
+
49
+ index: int
50
+ type: StepType
51
+ description: str
52
+ target: str
53
+ details: str = ""
54
+ depends_on: list[int] = field(default_factory=list)
55
+
56
+ def to_dict(self) -> dict:
57
+ """Convert to dictionary."""
58
+ return {
59
+ "index": self.index,
60
+ "type": self.type.value,
61
+ "description": self.description,
62
+ "target": self.target,
63
+ "details": self.details,
64
+ "depends_on": self.depends_on,
65
+ }
66
+
67
+
68
+ @dataclass
69
+ class ImplementationPlan:
70
+ """Complete plan for implementing a task.
71
+
72
+ Attributes:
73
+ task_id: ID of the task this plan is for
74
+ summary: Brief summary of the approach
75
+ steps: Ordered list of implementation steps
76
+ files_to_create: New files that will be created
77
+ files_to_modify: Existing files that will be changed
78
+ estimated_complexity: Overall complexity estimate
79
+ considerations: Important notes or warnings
80
+ """
81
+
82
+ task_id: str
83
+ summary: str
84
+ steps: list[PlanStep]
85
+ files_to_create: list[str] = field(default_factory=list)
86
+ files_to_modify: list[str] = field(default_factory=list)
87
+ estimated_complexity: Complexity = Complexity.MEDIUM
88
+ considerations: list[str] = field(default_factory=list)
89
+
90
+ @property
91
+ def total_steps(self) -> int:
92
+ """Total number of steps."""
93
+ return len(self.steps)
94
+
95
+ @property
96
+ def file_operations(self) -> list[PlanStep]:
97
+ """Steps that involve file operations."""
98
+ return [
99
+ s for s in self.steps
100
+ if s.type in {StepType.FILE_CREATE, StepType.FILE_EDIT, StepType.FILE_DELETE}
101
+ ]
102
+
103
+ @property
104
+ def commands(self) -> list[PlanStep]:
105
+ """Steps that involve shell commands."""
106
+ return [s for s in self.steps if s.type == StepType.SHELL_COMMAND]
107
+
108
+ def to_dict(self) -> dict:
109
+ """Convert to dictionary for serialization."""
110
+ return {
111
+ "task_id": self.task_id,
112
+ "summary": self.summary,
113
+ "steps": [s.to_dict() for s in self.steps],
114
+ "files_to_create": self.files_to_create,
115
+ "files_to_modify": self.files_to_modify,
116
+ "estimated_complexity": self.estimated_complexity.value,
117
+ "considerations": self.considerations,
118
+ }
119
+
120
+ def to_markdown(self) -> str:
121
+ """Convert to markdown format for display."""
122
+ lines = [
123
+ "# Implementation Plan",
124
+ "",
125
+ f"**Task:** {self.task_id}",
126
+ f"**Complexity:** {self.estimated_complexity.value}",
127
+ "",
128
+ "## Summary",
129
+ f"{self.summary}",
130
+ "",
131
+ ]
132
+
133
+ if self.files_to_create:
134
+ lines.append("## Files to Create")
135
+ for f in self.files_to_create:
136
+ lines.append(f"- `{f}`")
137
+ lines.append("")
138
+
139
+ if self.files_to_modify:
140
+ lines.append("## Files to Modify")
141
+ for f in self.files_to_modify:
142
+ lines.append(f"- `{f}`")
143
+ lines.append("")
144
+
145
+ lines.append("## Steps")
146
+ for step in self.steps:
147
+ deps = f" (depends on: {step.depends_on})" if step.depends_on else ""
148
+ lines.append(f"{step.index}. **[{step.type.value}]** {step.description}{deps}")
149
+ lines.append(f" - Target: `{step.target}`")
150
+ if step.details:
151
+ lines.append(f" - Details: {step.details[:200]}")
152
+ lines.append("")
153
+
154
+ if self.considerations:
155
+ lines.append("## Considerations")
156
+ for c in self.considerations:
157
+ lines.append(f"- {c}")
158
+
159
+ return "\n".join(lines)
160
+
161
+
162
+ PLANNING_SYSTEM_PROMPT = """You are an expert software implementation planner. Your job is to analyze a task and its context, then create a detailed implementation plan.
163
+
164
+ ## CRITICAL: Decision-Making Autonomy
165
+
166
+ You are an agent - keep going until the task is completely resolved. Make tactical decisions independently:
167
+
168
+ ALWAYS decide autonomously (without asking):
169
+ - Choose between equivalent implementation approaches
170
+ - Decide file organization and naming conventions
171
+ - Select library versions (prefer latest stable)
172
+ - Handle existing files (overwrite, merge, or extend as appropriate)
173
+ - Choose test frameworks and configurations
174
+ - Make code style decisions following existing patterns
175
+ - Install dependencies using the project's package manager
176
+ - Create directories and files as needed
177
+ - Fix linting errors automatically
178
+
179
+ ONLY flag as a blocker when:
180
+ - Requirements genuinely conflict or are underspecified
181
+ - Security policies need clarification
182
+ - Business logic requires domain expertise the context doesn't provide
183
+ - Access credentials are missing
184
+
185
+ NEVER stop to ask about:
186
+ - Tooling choices (use project preferences or best practices)
187
+ - File handling (existing files should be updated, not blocked on)
188
+ - Configuration details (use sensible defaults)
189
+ - Minor implementation decisions
190
+ - "Which approach should I use?" - pick the best one and proceed
191
+
192
+ When multiple valid options exist, choose the simpler approach. Trust your expertise.
193
+
194
+ ## Output Format
195
+
196
+ You must return a valid JSON object with this structure:
197
+ {
198
+ "summary": "Brief description of the implementation approach",
199
+ "steps": [
200
+ {
201
+ "index": 1,
202
+ "type": "file_create|file_edit|file_delete|shell_command|verification",
203
+ "description": "What this step accomplishes",
204
+ "target": "see target rules below",
205
+ "details": "Specific changes or command arguments",
206
+ "depends_on": []
207
+ }
208
+ ],
209
+ "files_to_create": ["path/to/new/file.py"],
210
+ "files_to_modify": ["path/to/existing/file.py"],
211
+ "estimated_complexity": "low|medium|high",
212
+ "considerations": ["Important note or warning"]
213
+ }
214
+
215
+ TARGET RULES (critical - follow exactly):
216
+ - file_create: target = file path to create (e.g., "src/utils.py")
217
+ - file_edit: target = file path to edit (e.g., "main.py")
218
+ - file_delete: target = file path to delete
219
+ - shell_command: target = the actual command to run (e.g., "python main.py --help", "pytest tests/")
220
+ - verification: target = the actual command to run (e.g., "python script.py --help", "pytest -v")
221
+ DO NOT put "shell_command" as the target. Put the actual command string.
222
+
223
+ ## Planning Guidelines
224
+
225
+ 1. Break work into small, focused steps (each step should do ONE thing)
226
+ 2. Order steps by dependency (later steps can depend on earlier ones)
227
+ 3. Include verification steps after significant changes
228
+ 4. Be specific about what files to modify and what changes to make
229
+ 5. Consider edge cases and potential issues
230
+ 6. Keep the plan achievable - don't over-engineer
231
+ 7. IMPORTANT: Before choosing file_create, verify the file does not already exist in the repository structure. If it exists, use file_edit instead. Never use file_create for files listed in the Repository Structure section.
232
+ 8. Run tests after implementation to verify correctness
233
+
234
+ Return ONLY the JSON object, no additional text."""
235
+
236
+
237
+ class Planner:
238
+ """Creates implementation plans from task context.
239
+
240
+ Uses LLM to analyze requirements and generate structured plans.
241
+ """
242
+
243
+ def __init__(self, llm_provider: LLMProvider):
244
+ """Initialize the planner.
245
+
246
+ Args:
247
+ llm_provider: LLM provider for generating plans
248
+ """
249
+ self.llm = llm_provider
250
+
251
+ def create_plan(self, context: TaskContext) -> ImplementationPlan:
252
+ """Create an implementation plan from task context.
253
+
254
+ Args:
255
+ context: Loaded task context
256
+
257
+ Returns:
258
+ ImplementationPlan with steps to execute
259
+
260
+ Raises:
261
+ ValueError: If plan generation fails
262
+ """
263
+ # Build the planning prompt
264
+ prompt = self._build_prompt(context)
265
+
266
+ # Call LLM with planning purpose (uses stronger model)
267
+ response = self.llm.complete(
268
+ messages=[{"role": "user", "content": prompt}],
269
+ purpose=Purpose.PLANNING,
270
+ system=PLANNING_SYSTEM_PROMPT,
271
+ max_tokens=4096,
272
+ temperature=0.0,
273
+ )
274
+
275
+ # Parse the response into a plan
276
+ return self._parse_plan(response.content, context.task.id)
277
+
278
+ def _build_prompt(self, context: TaskContext) -> str:
279
+ """Build the planning prompt from context.
280
+
281
+ Args:
282
+ context: Task context
283
+
284
+ Returns:
285
+ Formatted prompt string
286
+ """
287
+ sections = []
288
+
289
+ # Task information
290
+ sections.append("## Task to Implement")
291
+ sections.append(f"Title: {context.task.title}")
292
+ if context.task.description:
293
+ sections.append(f"Description: {context.task.description}")
294
+ sections.append("")
295
+
296
+ # Project preferences from AGENTS.md/CLAUDE.md
297
+ if context.preferences and context.preferences.has_preferences():
298
+ pref_section = context.preferences.to_prompt_section()
299
+ if pref_section:
300
+ sections.append(pref_section)
301
+ sections.append("")
302
+
303
+ # Tech stack configuration
304
+ if context.has_tech_stack:
305
+ sections.append("## Project Tech Stack")
306
+ sections.append(f"**Technology:** {context.tech_stack}")
307
+ sections.append("")
308
+ sections.append("Use appropriate commands and patterns for this technology stack.")
309
+ sections.append("When generating shell_command steps, use the correct tools for this stack.")
310
+ sections.append("")
311
+
312
+ # PRD if available
313
+ if context.prd:
314
+ sections.append("## Product Requirements")
315
+ # Limit PRD content to avoid overwhelming the plan
316
+ prd_content = context.prd.content[:5000]
317
+ sections.append(prd_content)
318
+ sections.append("")
319
+
320
+ # Previous clarifications
321
+ if context.answered_blockers:
322
+ sections.append("## Clarifications")
323
+ for b in context.answered_blockers:
324
+ sections.append(f"Q: {b.question}")
325
+ sections.append(f"A: {b.answer}")
326
+ sections.append("")
327
+
328
+ # Repository structure
329
+ if context.file_tree:
330
+ sections.append("## Repository Structure")
331
+ sections.append(f"Total files: {len(context.file_tree)}")
332
+ # Show top relevant files
333
+ for f in context.relevant_files[:20]:
334
+ sections.append(f" - {f.path}")
335
+ sections.append("")
336
+
337
+ # Existing files warning for planner
338
+ if context.file_tree:
339
+ sections.append("## Existing Files Warning")
340
+ sections.append(
341
+ "The following files already exist in the workspace. "
342
+ "Use file_edit (NOT file_create) for these:"
343
+ )
344
+ for f_info in context.file_tree[:50]:
345
+ sections.append(f" - {f_info.path}")
346
+ sections.append("")
347
+
348
+ # Loaded file contents
349
+ if context.loaded_files:
350
+ sections.append("## Relevant Source Files")
351
+ for f in context.loaded_files[:5]: # Limit to top 5
352
+ sections.append(f"### {f.path}")
353
+ sections.append("```")
354
+ # Truncate large files in prompt
355
+ content = f.content[:3000]
356
+ sections.append(content)
357
+ if len(f.content) > 3000:
358
+ sections.append("... (truncated)")
359
+ sections.append("```")
360
+ sections.append("")
361
+
362
+ sections.append("## Instructions")
363
+ sections.append("Create an implementation plan for this task.")
364
+ sections.append("Return a JSON object with the plan structure.")
365
+
366
+ return "\n".join(sections)
367
+
368
+ def _parse_plan(self, response_text: str, task_id: str) -> ImplementationPlan:
369
+ """Parse LLM response into an ImplementationPlan.
370
+
371
+ Args:
372
+ response_text: Raw LLM response
373
+ task_id: Task ID for the plan
374
+
375
+ Returns:
376
+ Parsed ImplementationPlan
377
+
378
+ Raises:
379
+ ValueError: If parsing fails
380
+ """
381
+ # Try to extract JSON from response
382
+ try:
383
+ # Find JSON object in response
384
+ json_match = re.search(r"\{[\s\S]*\}", response_text)
385
+ if not json_match:
386
+ raise ValueError("No JSON object found in response")
387
+
388
+ data = json.loads(json_match.group())
389
+ except json.JSONDecodeError as e:
390
+ raise ValueError(f"Failed to parse plan JSON: {e}")
391
+
392
+ # Build plan from parsed data
393
+ steps = []
394
+ for step_data in data.get("steps", []):
395
+ step_type = self._parse_step_type(step_data.get("type", "file_edit"))
396
+ steps.append(PlanStep(
397
+ index=step_data.get("index", len(steps) + 1),
398
+ type=step_type,
399
+ description=step_data.get("description", ""),
400
+ target=step_data.get("target", ""),
401
+ details=step_data.get("details", ""),
402
+ depends_on=step_data.get("depends_on", []),
403
+ ))
404
+
405
+ complexity = self._parse_complexity(
406
+ data.get("estimated_complexity", "medium")
407
+ )
408
+
409
+ return ImplementationPlan(
410
+ task_id=task_id,
411
+ summary=data.get("summary", "No summary provided"),
412
+ steps=steps,
413
+ files_to_create=data.get("files_to_create", []),
414
+ files_to_modify=data.get("files_to_modify", []),
415
+ estimated_complexity=complexity,
416
+ considerations=data.get("considerations", []),
417
+ )
418
+
419
+ def _parse_step_type(self, type_str: str) -> StepType:
420
+ """Parse step type string to enum."""
421
+ type_map = {
422
+ "file_create": StepType.FILE_CREATE,
423
+ "file_edit": StepType.FILE_EDIT,
424
+ "file_delete": StepType.FILE_DELETE,
425
+ "shell_command": StepType.SHELL_COMMAND,
426
+ "verification": StepType.VERIFICATION,
427
+ }
428
+ return type_map.get(type_str.lower(), StepType.FILE_EDIT)
429
+
430
+ def _parse_complexity(self, complexity_str: str) -> Complexity:
431
+ """Parse complexity string to enum."""
432
+ complexity_map = {
433
+ "low": Complexity.LOW,
434
+ "medium": Complexity.MEDIUM,
435
+ "high": Complexity.HIGH,
436
+ }
437
+ return complexity_map.get(complexity_str.lower(), Complexity.MEDIUM)