forgeoptimizer 0.1.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 (159) hide show
  1. forgecli/__init__.py +4 -0
  2. forgecli/build/__init__.py +135 -0
  3. forgecli/build/apply.py +218 -0
  4. forgecli/build/caveman_optimize.py +37 -0
  5. forgecli/build/diff_extract.py +218 -0
  6. forgecli/build/llm.py +167 -0
  7. forgecli/build/optimize.py +37 -0
  8. forgecli/build/pipeline.py +76 -0
  9. forgecli/build/retrieval.py +157 -0
  10. forgecli/build/summarize.py +76 -0
  11. forgecli/build/test_run.py +75 -0
  12. forgecli/builder/__init__.py +11 -0
  13. forgecli/builder/builder.py +53 -0
  14. forgecli/builder/editor.py +36 -0
  15. forgecli/builder/formatter.py +31 -0
  16. forgecli/cli/__init__.py +5 -0
  17. forgecli/cli/bootstrap.py +281 -0
  18. forgecli/cli/commands_graph.py +149 -0
  19. forgecli/cli/commands_wrappers.py +80 -0
  20. forgecli/cli/daemon.py +744 -0
  21. forgecli/cli/main.py +234 -0
  22. forgecli/cli/ui.py +56 -0
  23. forgecli/config/__init__.py +28 -0
  24. forgecli/config/loader.py +85 -0
  25. forgecli/config/settings.py +206 -0
  26. forgecli/config/writer.py +90 -0
  27. forgecli/core/__init__.py +35 -0
  28. forgecli/core/container.py +68 -0
  29. forgecli/core/context.py +54 -0
  30. forgecli/core/credentials.py +114 -0
  31. forgecli/core/errors.py +27 -0
  32. forgecli/core/events.py +67 -0
  33. forgecli/core/logging.py +47 -0
  34. forgecli/core/models.py +214 -0
  35. forgecli/core/plugins.py +54 -0
  36. forgecli/core/service.py +22 -0
  37. forgecli/docs/__init__.py +5 -0
  38. forgecli/docs/generator.py +115 -0
  39. forgecli/engine/__init__.py +105 -0
  40. forgecli/engine/context.py +163 -0
  41. forgecli/engine/defaults.py +89 -0
  42. forgecli/engine/events.py +185 -0
  43. forgecli/engine/execution.py +519 -0
  44. forgecli/engine/plugins.py +145 -0
  45. forgecli/engine/runner.py +158 -0
  46. forgecli/engine/stages/__init__.py +33 -0
  47. forgecli/engine/stages/caveman_optimizer.py +47 -0
  48. forgecli/engine/stages/context_optimizer.py +44 -0
  49. forgecli/engine/stages/execution_engine_stage.py +102 -0
  50. forgecli/engine/stages/git_engine.py +30 -0
  51. forgecli/engine/stages/intent_analyzer.py +38 -0
  52. forgecli/engine/stages/model_router.py +65 -0
  53. forgecli/engine/stages/planning_engine.py +45 -0
  54. forgecli/engine/stages/repository_analyzer.py +54 -0
  55. forgecli/engine/stages/validation_engine.py +87 -0
  56. forgecli/git/__init__.py +9 -0
  57. forgecli/git/repo.py +55 -0
  58. forgecli/git/service.py +45 -0
  59. forgecli/graph/__init__.py +56 -0
  60. forgecli/graph/backend_graphify.py +448 -0
  61. forgecli/graph/edge.py +19 -0
  62. forgecli/graph/graph.py +85 -0
  63. forgecli/graph/graphify.py +405 -0
  64. forgecli/graph/indexer.py +82 -0
  65. forgecli/graph/node.py +47 -0
  66. forgecli/graph/repository.py +164 -0
  67. forgecli/memory/__init__.py +12 -0
  68. forgecli/memory/cache.py +61 -0
  69. forgecli/memory/history.py +136 -0
  70. forgecli/memory/store.py +85 -0
  71. forgecli/optimizer/__init__.py +13 -0
  72. forgecli/optimizer/caveman/__init__.py +169 -0
  73. forgecli/optimizer/caveman/cli.py +62 -0
  74. forgecli/optimizer/caveman/decorator.py +67 -0
  75. forgecli/optimizer/caveman/factory.py +51 -0
  76. forgecli/optimizer/caveman/ruleset.py +156 -0
  77. forgecli/optimizer/caveman/state.py +50 -0
  78. forgecli/optimizer/chunker.py +85 -0
  79. forgecli/optimizer/optimizer.py +81 -0
  80. forgecli/optimizer/ponytail/__init__.py +181 -0
  81. forgecli/optimizer/ponytail/cli.py +159 -0
  82. forgecli/optimizer/ponytail/decorator.py +70 -0
  83. forgecli/optimizer/ponytail/factory.py +51 -0
  84. forgecli/optimizer/ponytail/ruleset.py +168 -0
  85. forgecli/optimizer/ponytail/state.py +51 -0
  86. forgecli/optimizer/ranker.py +37 -0
  87. forgecli/optimizer/summarizer.py +44 -0
  88. forgecli/orchestrator/__init__.py +706 -0
  89. forgecli/planner/__init__.py +56 -0
  90. forgecli/planner/agent.py +61 -0
  91. forgecli/planner/plan.py +65 -0
  92. forgecli/planner/planner.py +17 -0
  93. forgecli/planner/render.py +267 -0
  94. forgecli/planner/serialize.py +104 -0
  95. forgecli/planner/software.py +832 -0
  96. forgecli/platform/__init__.py +91 -0
  97. forgecli/platform/core.py +201 -0
  98. forgecli/platform/deps.py +361 -0
  99. forgecli/platform/paths.py +234 -0
  100. forgecli/platform/shell.py +176 -0
  101. forgecli/platform/update.py +253 -0
  102. forgecli/plugins/__init__.py +249 -0
  103. forgecli/prompts/__init__.py +11 -0
  104. forgecli/prompts/loader.py +28 -0
  105. forgecli/prompts/registry.py +32 -0
  106. forgecli/prompts/renderer.py +23 -0
  107. forgecli/providers/__init__.py +39 -0
  108. forgecli/providers/anthropic.py +207 -0
  109. forgecli/providers/base.py +204 -0
  110. forgecli/providers/builtin.py +26 -0
  111. forgecli/providers/conversation.py +74 -0
  112. forgecli/providers/google.py +290 -0
  113. forgecli/providers/http_base.py +207 -0
  114. forgecli/providers/mock.py +111 -0
  115. forgecli/providers/openai.py +206 -0
  116. forgecli/providers/openai_compatible.py +787 -0
  117. forgecli/providers/router.py +340 -0
  118. forgecli/providers/router_state.py +89 -0
  119. forgecli/review/__init__.py +45 -0
  120. forgecli/review/analyzer.py +124 -0
  121. forgecli/review/analyzers/__init__.py +1 -0
  122. forgecli/review/analyzers/architecture.py +255 -0
  123. forgecli/review/analyzers/complexity.py +162 -0
  124. forgecli/review/analyzers/dead_code.py +255 -0
  125. forgecli/review/analyzers/duplicates.py +161 -0
  126. forgecli/review/analyzers/performance.py +161 -0
  127. forgecli/review/analyzers/security.py +244 -0
  128. forgecli/review/finding.py +68 -0
  129. forgecli/review/report.py +321 -0
  130. forgecli/review/repository.py +130 -0
  131. forgecli/review/suggestions.py +98 -0
  132. forgecli/runtime/__init__.py +6 -0
  133. forgecli/runtime/cache_store.py +75 -0
  134. forgecli/runtime/mcp_config.py +118 -0
  135. forgecli/runtime/prepare.py +203 -0
  136. forgecli/runtime/wrappers.py +150 -0
  137. forgecli/sdk/__init__.py +132 -0
  138. forgecli/sdk/events.py +206 -0
  139. forgecli/sdk/interfaces.py +282 -0
  140. forgecli/sdk/loader.py +239 -0
  141. forgecli/sdk/manager.py +678 -0
  142. forgecli/sdk/manifest.py +395 -0
  143. forgecli/sdk/sandbox.py +247 -0
  144. forgecli/sdk/version.py +313 -0
  145. forgecli/templates/__init__.py +9 -0
  146. forgecli/templates/engine.py +37 -0
  147. forgecli/templates/registry.py +32 -0
  148. forgecli/utils/__init__.py +19 -0
  149. forgecli/utils/fs.py +121 -0
  150. forgecli/utils/ids.py +11 -0
  151. forgecli/utils/io.py +27 -0
  152. forgecli/utils/paths.py +78 -0
  153. forgecli/utils/stats.py +179 -0
  154. forgecli/utils/timing.py +25 -0
  155. forgeoptimizer-0.1.0.dist-info/METADATA +177 -0
  156. forgeoptimizer-0.1.0.dist-info/RECORD +159 -0
  157. forgeoptimizer-0.1.0.dist-info/WHEEL +4 -0
  158. forgeoptimizer-0.1.0.dist-info/entry_points.txt +2 -0
  159. forgeoptimizer-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,832 @@
1
+ """Software planner: turn a natural-language goal into a full plan.
2
+
3
+ The output is a :class:`SoftwarePlan` — a structured, fully-typed
4
+ breakdown of the goal into:
5
+
6
+ * **architecture** — components, contracts, data flow;
7
+ * **folder structure** — the directory layout the project will use;
8
+ * **milestones** — coarse-grained phases with deliverables;
9
+ * **tasks** — fine-grained actionable units (linked to milestones);
10
+ * **risks** — known unknowns, mitigations;
11
+ * **prompt sequence** — the system + user prompts an agent would feed
12
+ to a model to execute each task.
13
+
14
+ The planner is intentionally rule-based and deterministic so it can
15
+ run offline and be tested without network access. A future
16
+ implementation can swap in an LLM-driven planner by re-implementing
17
+ :func:`build_software_plan`.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import re
23
+ from collections.abc import Iterable
24
+ from dataclasses import dataclass, field
25
+ from enum import Enum
26
+
27
+ from pydantic import BaseModel, ConfigDict, Field
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Value types
31
+ # ---------------------------------------------------------------------------
32
+
33
+
34
+ class RiskSeverity(str, Enum):
35
+ """Severity assigned to a single :class:`Risk`."""
36
+
37
+ LOW = "low"
38
+ MEDIUM = "medium"
39
+ HIGH = "high"
40
+
41
+
42
+ class ComponentKind(str, Enum):
43
+ """Role a component plays in the architecture."""
44
+
45
+ API = "api"
46
+ UI = "ui"
47
+ WORKER = "worker"
48
+ DATA = "data"
49
+ INFRA = "infra"
50
+ LIBRARY = "library"
51
+ CLI = "cli"
52
+ SERVICE = "service"
53
+
54
+
55
+ class TaskStatus(str, Enum):
56
+ """Lifecycle status of a single :class:`Task`."""
57
+
58
+ PLANNED = "planned"
59
+ IN_PROGRESS = "in_progress"
60
+ DONE = "done"
61
+ SKIPPED = "skipped"
62
+ BLOCKED = "blocked"
63
+
64
+
65
+ class Priority(str, Enum):
66
+ """Priority assigned to a milestone or task."""
67
+
68
+ P0 = "P0"
69
+ P1 = "P1"
70
+ P2 = "P2"
71
+ P3 = "P3"
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Plan schema
76
+ # ---------------------------------------------------------------------------
77
+
78
+
79
+ class Component(BaseModel):
80
+ """A logical component of the architecture."""
81
+
82
+ model_config = ConfigDict(extra="forbid")
83
+
84
+ name: str
85
+ kind: ComponentKind
86
+ purpose: str
87
+ depends_on: tuple[str, ...] = Field(default_factory=tuple)
88
+
89
+
90
+ class DataFlow(BaseModel):
91
+ """A directed edge in the architecture diagram."""
92
+
93
+ model_config = ConfigDict(extra="forbid")
94
+
95
+ source: str
96
+ target: str
97
+ contract: str
98
+
99
+
100
+ class Architecture(BaseModel):
101
+ """The architecture section of a :class:`SoftwarePlan`."""
102
+
103
+ model_config = ConfigDict(extra="forbid")
104
+
105
+ summary: str
106
+ components: list[Component] = Field(default_factory=list)
107
+ flows: list[DataFlow] = Field(default_factory=list)
108
+
109
+
110
+ class FolderNode(BaseModel):
111
+ """A single node in the project tree."""
112
+
113
+ model_config = ConfigDict(extra="forbid")
114
+
115
+ path: str
116
+ purpose: str = ""
117
+ children: list[FolderNode] = Field(default_factory=list)
118
+
119
+
120
+ FolderNode.model_rebuild()
121
+
122
+
123
+ class FolderStructure(BaseModel):
124
+ """The folder-structure section of a :class:`SoftwarePlan`."""
125
+
126
+ model_config = ConfigDict(extra="forbid")
127
+
128
+ root: str
129
+ tree: list[FolderNode] = Field(default_factory=list)
130
+
131
+
132
+ class Milestone(BaseModel):
133
+ """A coarse-grained phase of the implementation roadmap."""
134
+
135
+ model_config = ConfigDict(extra="forbid")
136
+
137
+ id: str
138
+ title: str
139
+ description: str
140
+ priority: Priority = Priority.P1
141
+ deliverables: list[str] = Field(default_factory=list)
142
+ task_ids: list[str] = Field(default_factory=list)
143
+
144
+
145
+ class Task(BaseModel):
146
+ """A fine-grained actionable unit of work."""
147
+
148
+ model_config = ConfigDict(extra="forbid")
149
+
150
+ id: str
151
+ title: str
152
+ description: str
153
+ milestone_id: str
154
+ priority: Priority = Priority.P1
155
+ estimate: str = "M" # S / M / L / XL
156
+ status: TaskStatus = TaskStatus.PLANNED
157
+ owner: str = "agent"
158
+ acceptance: list[str] = Field(default_factory=list)
159
+ depends_on: list[str] = Field(default_factory=list)
160
+
161
+
162
+ class Risk(BaseModel):
163
+ """A known unknown."""
164
+
165
+ model_config = ConfigDict(extra="forbid")
166
+
167
+ id: str
168
+ description: str
169
+ severity: RiskSeverity = RiskSeverity.MEDIUM
170
+ likelihood: RiskSeverity = RiskSeverity.MEDIUM
171
+ mitigation: str = ""
172
+
173
+
174
+ class PromptTurn(BaseModel):
175
+ """A single (role, content) pair to be fed to a model."""
176
+
177
+ model_config = ConfigDict(extra="forbid")
178
+
179
+ role: str
180
+ content: str
181
+
182
+
183
+ class PromptSequence(BaseModel):
184
+ """The prompt sequence for a single task."""
185
+
186
+ model_config = ConfigDict(extra="forbid")
187
+
188
+ task_id: str
189
+ system: str
190
+ user: str
191
+ turns: list[PromptTurn] = Field(default_factory=list)
192
+
193
+
194
+ class SoftwarePlan(BaseModel):
195
+ """A complete software plan derived from a natural-language goal."""
196
+
197
+ model_config = ConfigDict(extra="forbid")
198
+
199
+ goal: str
200
+ summary: str
201
+ architecture: Architecture
202
+ folder_structure: FolderStructure
203
+ milestones: list[Milestone] = Field(default_factory=list)
204
+ tasks: list[Task] = Field(default_factory=list)
205
+ risks: list[Risk] = Field(default_factory=list)
206
+ prompt_sequences: list[PromptSequence] = Field(default_factory=list)
207
+ notes: list[str] = Field(default_factory=list)
208
+
209
+
210
+ # ---------------------------------------------------------------------------
211
+ # Heuristics
212
+ # ---------------------------------------------------------------------------
213
+
214
+
215
+ _KEYWORD_GOALS: dict[str, str] = {
216
+ "api": "an HTTP API",
217
+ "backend": "a backend service",
218
+ "frontend": "a frontend application",
219
+ "cli": "a command-line tool",
220
+ "library": "a reusable library",
221
+ "sdk": "a software development kit",
222
+ "service": "a backend service",
223
+ "worker": "an async worker",
224
+ "pipeline": "a data pipeline",
225
+ "dashboard": "a web dashboard",
226
+ "chatbot": "a chatbot",
227
+ "agent": "an agentic system",
228
+ "scraper": "a web scraper",
229
+ "scheduler": "a job scheduler",
230
+ "bot": "a bot",
231
+ "mobile": "a mobile application",
232
+ "app": "an application",
233
+ }
234
+
235
+
236
+ _STACK_HINTS: dict[str, tuple[str, ...]] = {
237
+ "python": ("Python", "FastAPI", "SQLAlchemy", "PostgreSQL", "pytest"),
238
+ "fastapi": ("Python", "FastAPI", "Pydantic", "PostgreSQL"),
239
+ "django": ("Python", "Django", "PostgreSQL"),
240
+ "flask": ("Python", "Flask", "SQLite"),
241
+ "typescript": ("TypeScript", "Node.js", "Express", "Vitest"),
242
+ "javascript": ("JavaScript", "Node.js", "Express"),
243
+ "react": ("TypeScript", "React", "Vite", "Vitest"),
244
+ "next": ("TypeScript", "Next.js", "React", "Vitest"),
245
+ "node": ("TypeScript", "Node.js", "Express", "Vitest"),
246
+ "go": ("Go", "net/http", "PostgreSQL", "go test"),
247
+ "rust": ("Rust", "Tokio", "PostgreSQL", "cargo test"),
248
+ "java": ("Java", "Spring Boot", "Maven", "JUnit"),
249
+ "kotlin": ("Kotlin", "Spring Boot", "Gradle", "JUnit"),
250
+ "swift": ("Swift", "SwiftUI", "XCTest"),
251
+ "elixir": ("Elixir", "Phoenix", "PostgreSQL", "ExUnit"),
252
+ }
253
+
254
+
255
+ _DEFAULT_MILESTONES: tuple[tuple[str, str, Priority, tuple[str, ...]], ...] = (
256
+ (
257
+ "M0",
258
+ "Discovery",
259
+ Priority.P0,
260
+ ("Problem statement", "User stories", "Success metrics"),
261
+ ),
262
+ (
263
+ "M1",
264
+ "Foundation",
265
+ Priority.P0,
266
+ ("Repo scaffold", "Type system", "Configuration", "CI smoke test"),
267
+ ),
268
+ (
269
+ "M2",
270
+ "Core domain",
271
+ Priority.P0,
272
+ ("Domain model", "Persistence", "Repository contracts"),
273
+ ),
274
+ (
275
+ "M3",
276
+ "Interface layer",
277
+ Priority.P1,
278
+ ("API surface", "CLI surface", "Schema validation"),
279
+ ),
280
+ (
281
+ "M4",
282
+ "Cross-cutting concerns",
283
+ Priority.P1,
284
+ ("Logging", "Error handling", "Auth", "Observability"),
285
+ ),
286
+ (
287
+ "M5",
288
+ "Quality & release",
289
+ Priority.P1,
290
+ ("Tests", "Performance", "Documentation", "Packaging"),
291
+ ),
292
+ )
293
+
294
+
295
+ def _slugify(goal: str) -> str:
296
+ """Return a filesystem-friendly slug derived from ``goal``."""
297
+ slug = re.sub(r"[^a-zA-Z0-9]+", "-", goal.lower()).strip("-")
298
+ return slug or "project"
299
+
300
+
301
+ def _detect_stack(goal: str) -> tuple[str, ...]:
302
+ """Return the inferred technology stack for ``goal``."""
303
+ lower = goal.lower()
304
+ hints: list[str] = []
305
+ for needle, stack in _STACK_HINTS.items():
306
+ if re.search(rf"\b{re.escape(needle)}\b", lower):
307
+ hints.extend(stack)
308
+ # Deduplicate while preserving order.
309
+ seen: set[str] = set()
310
+ out: list[str] = []
311
+ for hint in hints:
312
+ if hint not in seen:
313
+ seen.add(hint)
314
+ out.append(hint)
315
+ if not out:
316
+ out.extend(("Python", "FastAPI", "Pydantic", "SQLite", "pytest"))
317
+ return tuple(out)
318
+
319
+
320
+ def _classify_goal(goal: str) -> tuple[str, str]:
321
+ """Return ``(goal_kind, project_kind)`` for ``goal``."""
322
+ lower = goal.lower()
323
+ for needle, label in _KEYWORD_GOALS.items():
324
+ if re.search(rf"\b{re.escape(needle)}\b", lower):
325
+ return label, needle
326
+ return "an application", "app"
327
+
328
+
329
+ # ---------------------------------------------------------------------------
330
+ # Public API
331
+ # ---------------------------------------------------------------------------
332
+
333
+
334
+ @dataclass
335
+ class PlannerOptions:
336
+ """Toggles that influence plan generation."""
337
+
338
+ include_tests: bool = True
339
+ include_observability: bool = True
340
+ max_milestones: int = 6
341
+ extra_goals: tuple[str, ...] = field(default_factory=tuple)
342
+
343
+
344
+ def build_software_plan(goal: str, options: PlannerOptions | None = None) -> SoftwarePlan:
345
+ """Build a :class:`SoftwarePlan` for ``goal``."""
346
+ options = options or PlannerOptions()
347
+ goal = goal.strip()
348
+ if not goal:
349
+ raise ValueError("goal must be a non-empty string")
350
+
351
+ slug = _slugify(goal)
352
+ label, kind = _classify_goal(goal)
353
+ stack = _detect_stack(goal)
354
+ summary = _build_summary(goal, label, stack)
355
+ architecture = _build_architecture(goal, label, stack)
356
+ folder_structure = _build_folder_structure(slug, kind, stack)
357
+ milestones = _build_milestones(goal, kind, options)
358
+ tasks = _build_tasks(milestones, kind, options)
359
+ risks = _build_risks(goal, kind, stack, options)
360
+ prompt_sequences = _build_prompt_sequences(tasks, goal, stack)
361
+
362
+ return SoftwarePlan(
363
+ goal=goal,
364
+ summary=summary,
365
+ architecture=architecture,
366
+ folder_structure=folder_structure,
367
+ milestones=milestones,
368
+ tasks=tasks,
369
+ risks=risks,
370
+ prompt_sequences=prompt_sequences,
371
+ notes=_build_notes(options),
372
+ )
373
+
374
+
375
+ # ---------------------------------------------------------------------------
376
+ # Section builders
377
+ # ---------------------------------------------------------------------------
378
+
379
+
380
+ def _build_summary(goal: str, label: str, stack: Iterable[str]) -> str:
381
+ stack_list = ", ".join(list(stack)[:4])
382
+ return (
383
+ f"Plan for {label}: {goal}. "
384
+ f"Suggested stack: {stack_list}. "
385
+ f"Each milestone is broken into tasks with explicit acceptance "
386
+ f"criteria and a prompt sequence for an AI agent."
387
+ )
388
+
389
+
390
+ def _build_architecture(goal: str, label: str, stack: tuple[str, ...]) -> Architecture:
391
+ """Derive a simple three-component architecture from the goal."""
392
+ primary = stack[0] if stack else "Python"
393
+ components: list[Component] = [
394
+ Component(
395
+ name="interface",
396
+ kind=ComponentKind.API if "api" in goal.lower() else ComponentKind.CLI,
397
+ purpose=(
398
+ "Public surface (HTTP API or CLI). Owns request validation, "
399
+ "auth, and the contract exposed to callers."
400
+ ),
401
+ depends_on=("domain",),
402
+ ),
403
+ Component(
404
+ name="domain",
405
+ kind=ComponentKind.LIBRARY,
406
+ purpose=(
407
+ "Core business logic. Pure functions where possible; "
408
+ f"implemented in {primary} with no I/O dependencies."
409
+ ),
410
+ depends_on=(),
411
+ ),
412
+ Component(
413
+ name="persistence",
414
+ kind=ComponentKind.DATA,
415
+ purpose="Repository pattern over the chosen datastore.",
416
+ depends_on=("domain",),
417
+ ),
418
+ ]
419
+ flows = [
420
+ DataFlow(source="interface", target="domain", contract="typed function calls"),
421
+ DataFlow(source="domain", target="persistence", contract="repository interfaces"),
422
+ ]
423
+ return Architecture(
424
+ summary=(
425
+ f"Three-layer architecture for {label}, built around a pure core "
426
+ f"with a thin interface layer and a swappable persistence layer."
427
+ ),
428
+ components=components,
429
+ flows=flows,
430
+ )
431
+
432
+
433
+ def _build_folder_structure(slug: str, kind: str, stack: tuple[str, ...]) -> FolderStructure:
434
+ """Return the canonical project tree for the detected kind/stack."""
435
+ children: list[FolderNode] = [
436
+ FolderNode(path="src", purpose="Production code"),
437
+ FolderNode(path="tests", purpose="Automated tests"),
438
+ FolderNode(path="docs", purpose="User-facing documentation"),
439
+ FolderNode(path="scripts", purpose="Operational scripts"),
440
+ ]
441
+ src = children[0]
442
+ if kind in {"api", "service", "backend"}:
443
+ src.children = [
444
+ FolderNode(path="src/api", purpose="HTTP handlers + routing"),
445
+ FolderNode(path="src/domain", purpose="Core business logic"),
446
+ FolderNode(path="src/persistence", purpose="Datastore access"),
447
+ FolderNode(path="src/observability", purpose="Logging, metrics, traces"),
448
+ ]
449
+ elif kind in {"cli"}:
450
+ src.children = [
451
+ FolderNode(path="src/commands", purpose="Subcommand implementations"),
452
+ FolderNode(path="src/core", purpose="Core logic shared by commands"),
453
+ FolderNode(path="src/output", purpose="Terminal output helpers"),
454
+ ]
455
+ elif kind in {"library", "sdk"}:
456
+ src.children = [
457
+ FolderNode(path="src", purpose="Public package surface"),
458
+ FolderNode(path="src/_internal", purpose="Private implementation"),
459
+ ]
460
+ else:
461
+ src.children = [
462
+ FolderNode(path="src/api", purpose="HTTP handlers"),
463
+ FolderNode(path="src/core", purpose="Core logic"),
464
+ ]
465
+ children.extend(
466
+ [
467
+ FolderNode(path=".github/workflows", purpose="CI definitions"),
468
+ FolderNode(path="pyproject.toml", purpose="Package metadata + tooling"),
469
+ FolderNode(path="README.md", purpose="Quickstart + overview"),
470
+ ]
471
+ )
472
+ # Filter out non-pertinent top-level nodes (keep the tree tidy).
473
+ return FolderStructure(root=slug, tree=children)
474
+
475
+
476
+ def _build_milestones(goal: str, kind: str, options: PlannerOptions) -> list[Milestone]:
477
+ milestones: list[Milestone] = []
478
+ for mid, title, priority, deliverables in _DEFAULT_MILESTONES[: options.max_milestones]:
479
+ milestones.append(
480
+ Milestone(
481
+ id=mid,
482
+ title=title,
483
+ description=_milestone_description(mid, title, goal, kind),
484
+ priority=priority,
485
+ deliverables=list(deliverables),
486
+ )
487
+ )
488
+ return milestones
489
+
490
+
491
+ def _milestone_description(mid: str, title: str, goal: str, kind: str) -> str:
492
+ summaries: dict[str, str] = {
493
+ "M0": f"Lock down the problem statement and success metrics for: {goal}.",
494
+ "M1": "Set up the project skeleton, tooling, and CI smoke test.",
495
+ "M2": f"Implement the core domain for the {kind}.",
496
+ "M3": f"Expose the {kind}'s surface (HTTP, CLI, or library API).",
497
+ "M4": "Wire up cross-cutting concerns: auth, logging, errors, observability.",
498
+ "M5": "Bring the project to release quality: tests, docs, packaging.",
499
+ }
500
+ return summaries.get(mid, title)
501
+
502
+
503
+ def _build_tasks(milestones: list[Milestone], kind: str, options: PlannerOptions) -> list[Task]:
504
+ tasks: list[Task] = []
505
+ counter = 0
506
+
507
+ def _next_id() -> str:
508
+ nonlocal counter
509
+ counter += 1
510
+ return f"T{counter:02d}"
511
+
512
+ for milestone in milestones:
513
+ per_milestone = _tasks_for_milestone(milestone, kind, options)
514
+ for title, description, acceptance, estimate, owner in per_milestone:
515
+ tid = _next_id()
516
+ task = Task(
517
+ id=tid,
518
+ title=title,
519
+ description=description,
520
+ milestone_id=milestone.id,
521
+ priority=milestone.priority,
522
+ estimate=estimate,
523
+ owner=owner,
524
+ acceptance=list(acceptance),
525
+ depends_on=[],
526
+ )
527
+ tasks.append(task)
528
+ milestone.task_ids.append(tid)
529
+ # Wire intra-milestone dependencies: each task depends on the previous.
530
+ from itertools import pairwise
531
+
532
+ for milestone in milestones:
533
+ milestone_tasks = [t for t in tasks if t.milestone_id == milestone.id]
534
+ for prev, current in pairwise(milestone_tasks):
535
+ current.depends_on.append(prev.id)
536
+ return tasks
537
+
538
+
539
+ def _tasks_for_milestone(
540
+ milestone: Milestone, kind: str, options: PlannerOptions
541
+ ) -> list[tuple[str, str, tuple[str, ...], str, str]]:
542
+ if milestone.id == "M0":
543
+ return [
544
+ (
545
+ "Write a one-paragraph problem statement",
546
+ "Capture the user need and the success criteria in plain English.",
547
+ ("Reviewed by a stakeholder", "No technical jargon"),
548
+ "S",
549
+ "human",
550
+ ),
551
+ (
552
+ "Draft three user stories",
553
+ "Each story follows: As a ___, I want ___, so that ___.",
554
+ ("Three stories", "All have acceptance criteria"),
555
+ "S",
556
+ "human",
557
+ ),
558
+ ]
559
+ if milestone.id == "M1":
560
+ return [
561
+ (
562
+ "Scaffold the repository",
563
+ "Create pyproject.toml, src/ tree, README, and a Makefile.",
564
+ ("`pytest` runs green", "`ruff check` is clean"),
565
+ "S",
566
+ "agent",
567
+ ),
568
+ (
569
+ "Configure CI",
570
+ "Add a GitHub Actions workflow that runs lint + tests on PRs.",
571
+ ("Workflow visible in .github/workflows", "PR check is green"),
572
+ "S",
573
+ "agent",
574
+ ),
575
+ (
576
+ "Add structured logging",
577
+ "Configure the project's logging format and level.",
578
+ (),
579
+ "S",
580
+ "agent",
581
+ ),
582
+ ]
583
+ if milestone.id == "M2":
584
+ return [
585
+ (
586
+ "Define domain types",
587
+ "Add Pydantic/dataclass types for the core entities.",
588
+ ("Types are immutable", "Round-trip JSON serialization works"),
589
+ "M",
590
+ "agent",
591
+ ),
592
+ (
593
+ "Implement domain operations",
594
+ "Write the business rules as pure functions.",
595
+ ("Unit tests cover edge cases", "No I/O in this layer"),
596
+ "M",
597
+ "agent",
598
+ ),
599
+ (
600
+ "Build the persistence layer",
601
+ "Wrap the chosen datastore behind repository interfaces.",
602
+ ("In-memory implementation for tests", "Real implementation behind an env flag"),
603
+ "M",
604
+ "agent",
605
+ ),
606
+ ]
607
+ if milestone.id == "M3":
608
+ if kind in {"api", "service", "backend"}:
609
+ return [
610
+ (
611
+ "Add HTTP routing",
612
+ "Define routes and request/response models.",
613
+ ("OpenAPI schema is generated", "Validation errors are 4xx with detail"),
614
+ "M",
615
+ "agent",
616
+ ),
617
+ (
618
+ "Add error handling",
619
+ "Translate exceptions to typed HTTP responses.",
620
+ ("No 500s for known error types", "All errors logged with context"),
621
+ "S",
622
+ "agent",
623
+ ),
624
+ ]
625
+ if kind == "cli":
626
+ return [
627
+ (
628
+ "Define subcommands",
629
+ "Use Typer/Click to declare the command surface.",
630
+ ("`--help` is helpful", "Subcommands have unit tests"),
631
+ "M",
632
+ "agent",
633
+ ),
634
+ (
635
+ "Add progress + error reporting",
636
+ "Use Rich for progress bars and styled errors.",
637
+ ("Errors exit non-zero", "No stack traces in default mode"),
638
+ "S",
639
+ "agent",
640
+ ),
641
+ ]
642
+ return [
643
+ (
644
+ "Design the public API",
645
+ "Sketch the public functions/classes and their contracts.",
646
+ ("Docstrings on every public symbol", "At least one usage example per entry point"),
647
+ "M",
648
+ "agent",
649
+ ),
650
+ (
651
+ "Document the public API",
652
+ "Generate API reference docs (mkdocs/sphinx).",
653
+ ("Docs build green", "Cross-links to guides"),
654
+ "S",
655
+ "agent",
656
+ ),
657
+ ]
658
+ if milestone.id == "M4":
659
+ items: list[tuple[str, str, tuple[str, ...], str, str]] = [
660
+ (
661
+ "Add authentication",
662
+ "Choose an auth model and apply it at the interface boundary.",
663
+ ("Auth failures are 401/403", "No credentials in logs"),
664
+ "M",
665
+ "agent",
666
+ ),
667
+ (
668
+ "Add error reporting",
669
+ "Translate exceptions to typed responses and structured logs.",
670
+ (),
671
+ "S",
672
+ "agent",
673
+ ),
674
+ ]
675
+ if options.include_observability:
676
+ items.append(
677
+ (
678
+ "Add metrics + tracing",
679
+ "Instrument key request paths with counters and spans.",
680
+ ("Metrics endpoint exposed", "Traces sampled at >= 1%"),
681
+ "M",
682
+ "agent",
683
+ )
684
+ )
685
+ return items
686
+ if milestone.id == "M5":
687
+ items = [
688
+ (
689
+ "Write the README",
690
+ "Document install, quickstart, and architecture overview.",
691
+ ("Badges are green", "New contributor can follow the steps"),
692
+ "M",
693
+ "human",
694
+ ),
695
+ (
696
+ "Add a release checklist",
697
+ "Define pre-release steps: version bump, changelog, tag.",
698
+ (),
699
+ "S",
700
+ "agent",
701
+ ),
702
+ ]
703
+ if options.include_tests:
704
+ items.append(
705
+ (
706
+ "Audit test coverage",
707
+ "Identify gaps and add tests for the highest-risk paths.",
708
+ ("Coverage >= 80%", "No flaky tests on CI"),
709
+ "M",
710
+ "agent",
711
+ )
712
+ )
713
+ return items
714
+ return []
715
+
716
+
717
+ def _build_risks(
718
+ goal: str, kind: str, stack: tuple[str, ...], options: PlannerOptions
719
+ ) -> list[Risk]:
720
+ risks: list[Risk] = [
721
+ Risk(
722
+ id="R1",
723
+ description="Scope creep beyond the initial milestone set.",
724
+ severity=RiskSeverity.MEDIUM,
725
+ likelihood=RiskSeverity.MEDIUM,
726
+ mitigation="Lock the M0 problem statement; require a new milestone to expand scope.",
727
+ ),
728
+ Risk(
729
+ id="R2",
730
+ description="Premature optimization of hot paths.",
731
+ severity=RiskSeverity.LOW,
732
+ likelihood=RiskSeverity.MEDIUM,
733
+ mitigation="Measure first; the optimizer module already records call stats.",
734
+ ),
735
+ Risk(
736
+ id="R3",
737
+ description="External API rate limits or schema changes.",
738
+ severity=RiskSeverity.MEDIUM,
739
+ likelihood=RiskSeverity.MEDIUM,
740
+ mitigation="Wrap all external calls behind interfaces; ship a mock for tests.",
741
+ ),
742
+ ]
743
+ if "api" in goal.lower() or kind in {"api", "service", "backend"}:
744
+ risks.append(
745
+ Risk(
746
+ id="R4",
747
+ description="Authentication misconfiguration leaks data.",
748
+ severity=RiskSeverity.HIGH,
749
+ likelihood=RiskSeverity.LOW,
750
+ mitigation="Test auth failure paths in CI; deny by default in the router.",
751
+ )
752
+ )
753
+ if options.include_observability:
754
+ risks.append(
755
+ Risk(
756
+ id="R5",
757
+ description="Missing observability hides production incidents.",
758
+ severity=RiskSeverity.MEDIUM,
759
+ likelihood=RiskSeverity.MEDIUM,
760
+ mitigation="Adopt structured logging + metrics before M4 ships.",
761
+ )
762
+ )
763
+ return risks
764
+
765
+
766
+ def _build_prompt_sequences(
767
+ tasks: list[Task], goal: str, stack: tuple[str, ...]
768
+ ) -> list[PromptSequence]:
769
+ stack_line = ", ".join(stack[:4]) if stack else "the chosen stack"
770
+ out: list[PromptSequence] = []
771
+ for task in tasks:
772
+ system = (
773
+ f"You are a senior engineer working on a project to {goal}. "
774
+ f"The project uses {stack_line}. Apply the Ponytail ruleset: "
775
+ f"ship the smallest correct change, reuse existing helpers, prefer "
776
+ f"the standard library, and avoid speculative abstractions."
777
+ )
778
+ user = (
779
+ f"Complete this task:\n\n"
780
+ f"Title: {task.title}\n"
781
+ f"Description: {task.description}\n"
782
+ f"Acceptance criteria:\n"
783
+ + "\n".join(f"- {c}" for c in task.acceptance or ["(none specified)"])
784
+ + "\n\nReturn a unified diff, not free-form prose. "
785
+ f"Keep the change to {task.estimate} of work."
786
+ )
787
+ out.append(
788
+ PromptSequence(
789
+ task_id=task.id,
790
+ system=system,
791
+ user=user,
792
+ turns=[
793
+ PromptTurn(role="system", content=system),
794
+ PromptTurn(role="user", content=user),
795
+ ],
796
+ )
797
+ )
798
+ return out
799
+
800
+
801
+ def _build_notes(options: PlannerOptions) -> list[str]:
802
+ notes: list[str] = []
803
+ if options.max_milestones != 6:
804
+ notes.append(f"max_milestones={options.max_milestones}")
805
+ if not options.include_tests:
806
+ notes.append("test task omitted by configuration")
807
+ if not options.include_observability:
808
+ notes.append("observability tasks omitted by configuration")
809
+ if options.extra_goals:
810
+ notes.append(f"extra goals: {', '.join(options.extra_goals)}")
811
+ return notes
812
+
813
+
814
+ __all__ = [
815
+ "Architecture",
816
+ "Component",
817
+ "ComponentKind",
818
+ "DataFlow",
819
+ "FolderNode",
820
+ "FolderStructure",
821
+ "Milestone",
822
+ "PlannerOptions",
823
+ "Priority",
824
+ "PromptSequence",
825
+ "PromptTurn",
826
+ "Risk",
827
+ "RiskSeverity",
828
+ "SoftwarePlan",
829
+ "Task",
830
+ "TaskStatus",
831
+ "build_software_plan",
832
+ ]