forgeoptimizer 1.0.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.
- forgecli/__init__.py +4 -0
- forgecli/build/__init__.py +135 -0
- forgecli/build/apply.py +218 -0
- forgecli/build/caveman_optimize.py +37 -0
- forgecli/build/diff_extract.py +218 -0
- forgecli/build/llm.py +167 -0
- forgecli/build/optimize.py +37 -0
- forgecli/build/pipeline.py +76 -0
- forgecli/build/retrieval.py +157 -0
- forgecli/build/summarize.py +76 -0
- forgecli/build/test_run.py +75 -0
- forgecli/builder/__init__.py +11 -0
- forgecli/builder/builder.py +53 -0
- forgecli/builder/editor.py +36 -0
- forgecli/builder/formatter.py +31 -0
- forgecli/cli/__init__.py +5 -0
- forgecli/cli/bootstrap.py +281 -0
- forgecli/cli/commands_graph.py +137 -0
- forgecli/cli/commands_wrappers.py +63 -0
- forgecli/cli/main.py +122 -0
- forgecli/cli/ui.py +56 -0
- forgecli/config/__init__.py +28 -0
- forgecli/config/loader.py +85 -0
- forgecli/config/settings.py +204 -0
- forgecli/config/writer.py +90 -0
- forgecli/core/__init__.py +35 -0
- forgecli/core/container.py +68 -0
- forgecli/core/context.py +54 -0
- forgecli/core/credentials.py +114 -0
- forgecli/core/errors.py +27 -0
- forgecli/core/events.py +67 -0
- forgecli/core/logging.py +47 -0
- forgecli/core/models.py +159 -0
- forgecli/core/plugins.py +56 -0
- forgecli/core/service.py +22 -0
- forgecli/docs/__init__.py +5 -0
- forgecli/docs/generator.py +119 -0
- forgecli/engine/__init__.py +105 -0
- forgecli/engine/context.py +171 -0
- forgecli/engine/defaults.py +89 -0
- forgecli/engine/events.py +185 -0
- forgecli/engine/execution.py +526 -0
- forgecli/engine/plugins.py +146 -0
- forgecli/engine/runner.py +155 -0
- forgecli/engine/stages/__init__.py +33 -0
- forgecli/engine/stages/caveman_optimizer.py +47 -0
- forgecli/engine/stages/context_optimizer.py +44 -0
- forgecli/engine/stages/execution_engine_stage.py +108 -0
- forgecli/engine/stages/git_engine.py +30 -0
- forgecli/engine/stages/intent_analyzer.py +38 -0
- forgecli/engine/stages/model_router.py +66 -0
- forgecli/engine/stages/planning_engine.py +45 -0
- forgecli/engine/stages/repository_analyzer.py +54 -0
- forgecli/engine/stages/validation_engine.py +89 -0
- forgecli/git/__init__.py +9 -0
- forgecli/git/repo.py +55 -0
- forgecli/git/service.py +45 -0
- forgecli/graph/__init__.py +56 -0
- forgecli/graph/backend_graphify.py +453 -0
- forgecli/graph/edge.py +19 -0
- forgecli/graph/graph.py +85 -0
- forgecli/graph/graphify.py +412 -0
- forgecli/graph/indexer.py +82 -0
- forgecli/graph/node.py +47 -0
- forgecli/graph/repository.py +164 -0
- forgecli/memory/__init__.py +12 -0
- forgecli/memory/cache.py +61 -0
- forgecli/memory/history.py +138 -0
- forgecli/memory/store.py +87 -0
- forgecli/optimizer/__init__.py +14 -0
- forgecli/optimizer/caveman/__init__.py +173 -0
- forgecli/optimizer/caveman/cli.py +64 -0
- forgecli/optimizer/caveman/decorator.py +67 -0
- forgecli/optimizer/caveman/factory.py +51 -0
- forgecli/optimizer/caveman/ruleset.py +156 -0
- forgecli/optimizer/caveman/state.py +52 -0
- forgecli/optimizer/chunker.py +85 -0
- forgecli/optimizer/optimizer.py +81 -0
- forgecli/optimizer/ponytail/__init__.py +183 -0
- forgecli/optimizer/ponytail/cli.py +168 -0
- forgecli/optimizer/ponytail/decorator.py +70 -0
- forgecli/optimizer/ponytail/factory.py +51 -0
- forgecli/optimizer/ponytail/ruleset.py +168 -0
- forgecli/optimizer/ponytail/state.py +53 -0
- forgecli/optimizer/ranker.py +37 -0
- forgecli/optimizer/summarizer.py +44 -0
- forgecli/orchestrator/__init__.py +707 -0
- forgecli/planner/__init__.py +56 -0
- forgecli/planner/agent.py +61 -0
- forgecli/planner/plan.py +65 -0
- forgecli/planner/planner.py +17 -0
- forgecli/planner/render.py +271 -0
- forgecli/planner/serialize.py +106 -0
- forgecli/planner/software.py +834 -0
- forgecli/platform/__init__.py +91 -0
- forgecli/platform/core.py +201 -0
- forgecli/platform/deps.py +354 -0
- forgecli/platform/paths.py +236 -0
- forgecli/platform/shell.py +176 -0
- forgecli/platform/update.py +252 -0
- forgecli/plugins/__init__.py +251 -0
- forgecli/prompts/__init__.py +11 -0
- forgecli/prompts/loader.py +28 -0
- forgecli/prompts/registry.py +32 -0
- forgecli/prompts/renderer.py +23 -0
- forgecli/providers/__init__.py +39 -0
- forgecli/providers/anthropic.py +206 -0
- forgecli/providers/base.py +206 -0
- forgecli/providers/builtin.py +26 -0
- forgecli/providers/conversation.py +78 -0
- forgecli/providers/google.py +295 -0
- forgecli/providers/http_base.py +211 -0
- forgecli/providers/mock.py +113 -0
- forgecli/providers/openai.py +202 -0
- forgecli/providers/openai_compatible.py +728 -0
- forgecli/providers/router.py +345 -0
- forgecli/providers/router_state.py +89 -0
- forgecli/review/__init__.py +45 -0
- forgecli/review/analyzer.py +124 -0
- forgecli/review/analyzers/__init__.py +1 -0
- forgecli/review/analyzers/architecture.py +258 -0
- forgecli/review/analyzers/complexity.py +167 -0
- forgecli/review/analyzers/dead_code.py +262 -0
- forgecli/review/analyzers/duplicates.py +163 -0
- forgecli/review/analyzers/performance.py +165 -0
- forgecli/review/analyzers/security.py +251 -0
- forgecli/review/finding.py +68 -0
- forgecli/review/report.py +311 -0
- forgecli/review/repository.py +131 -0
- forgecli/review/suggestions.py +98 -0
- forgecli/runtime/__init__.py +6 -0
- forgecli/runtime/cache_store.py +77 -0
- forgecli/runtime/prepare.py +199 -0
- forgecli/runtime/wrappers.py +152 -0
- forgecli/sdk/__init__.py +132 -0
- forgecli/sdk/events.py +206 -0
- forgecli/sdk/interfaces.py +282 -0
- forgecli/sdk/loader.py +243 -0
- forgecli/sdk/manager.py +693 -0
- forgecli/sdk/manifest.py +397 -0
- forgecli/sdk/sandbox.py +197 -0
- forgecli/sdk/version.py +316 -0
- forgecli/templates/__init__.py +9 -0
- forgecli/templates/engine.py +37 -0
- forgecli/templates/registry.py +32 -0
- forgecli/utils/__init__.py +19 -0
- forgecli/utils/fs.py +91 -0
- forgecli/utils/ids.py +11 -0
- forgecli/utils/io.py +27 -0
- forgecli/utils/paths.py +70 -0
- forgecli/utils/timing.py +25 -0
- forgeoptimizer-1.0.0.dist-info/METADATA +169 -0
- forgeoptimizer-1.0.0.dist-info/RECORD +156 -0
- forgeoptimizer-1.0.0.dist-info/WHEEL +4 -0
- forgeoptimizer-1.0.0.dist-info/entry_points.txt +2 -0
- forgeoptimizer-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,834 @@
|
|
|
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(
|
|
504
|
+
milestones: list[Milestone], kind: str, options: PlannerOptions
|
|
505
|
+
) -> list[Task]:
|
|
506
|
+
tasks: list[Task] = []
|
|
507
|
+
counter = 0
|
|
508
|
+
|
|
509
|
+
def _next_id() -> str:
|
|
510
|
+
nonlocal counter
|
|
511
|
+
counter += 1
|
|
512
|
+
return f"T{counter:02d}"
|
|
513
|
+
|
|
514
|
+
for milestone in milestones:
|
|
515
|
+
per_milestone = _tasks_for_milestone(milestone, kind, options)
|
|
516
|
+
for title, description, acceptance, estimate, owner in per_milestone:
|
|
517
|
+
tid = _next_id()
|
|
518
|
+
task = Task(
|
|
519
|
+
id=tid,
|
|
520
|
+
title=title,
|
|
521
|
+
description=description,
|
|
522
|
+
milestone_id=milestone.id,
|
|
523
|
+
priority=milestone.priority,
|
|
524
|
+
estimate=estimate,
|
|
525
|
+
owner=owner,
|
|
526
|
+
acceptance=list(acceptance),
|
|
527
|
+
depends_on=[],
|
|
528
|
+
)
|
|
529
|
+
tasks.append(task)
|
|
530
|
+
milestone.task_ids.append(tid)
|
|
531
|
+
# Wire intra-milestone dependencies: each task depends on the previous.
|
|
532
|
+
from itertools import pairwise
|
|
533
|
+
|
|
534
|
+
for milestone in milestones:
|
|
535
|
+
milestone_tasks = [t for t in tasks if t.milestone_id == milestone.id]
|
|
536
|
+
for prev, current in pairwise(milestone_tasks):
|
|
537
|
+
current.depends_on.append(prev.id)
|
|
538
|
+
return tasks
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _tasks_for_milestone(
|
|
542
|
+
milestone: Milestone, kind: str, options: PlannerOptions
|
|
543
|
+
) -> list[tuple[str, str, tuple[str, ...], str, str]]:
|
|
544
|
+
if milestone.id == "M0":
|
|
545
|
+
return [
|
|
546
|
+
(
|
|
547
|
+
"Write a one-paragraph problem statement",
|
|
548
|
+
"Capture the user need and the success criteria in plain English.",
|
|
549
|
+
("Reviewed by a stakeholder", "No technical jargon"),
|
|
550
|
+
"S",
|
|
551
|
+
"human",
|
|
552
|
+
),
|
|
553
|
+
(
|
|
554
|
+
"Draft three user stories",
|
|
555
|
+
"Each story follows: As a ___, I want ___, so that ___.",
|
|
556
|
+
("Three stories", "All have acceptance criteria"),
|
|
557
|
+
"S",
|
|
558
|
+
"human",
|
|
559
|
+
),
|
|
560
|
+
]
|
|
561
|
+
if milestone.id == "M1":
|
|
562
|
+
return [
|
|
563
|
+
(
|
|
564
|
+
"Scaffold the repository",
|
|
565
|
+
"Create pyproject.toml, src/ tree, README, and a Makefile.",
|
|
566
|
+
("`pytest` runs green", "`ruff check` is clean"),
|
|
567
|
+
"S",
|
|
568
|
+
"agent",
|
|
569
|
+
),
|
|
570
|
+
(
|
|
571
|
+
"Configure CI",
|
|
572
|
+
"Add a GitHub Actions workflow that runs lint + tests on PRs.",
|
|
573
|
+
("Workflow visible in .github/workflows", "PR check is green"),
|
|
574
|
+
"S",
|
|
575
|
+
"agent",
|
|
576
|
+
),
|
|
577
|
+
(
|
|
578
|
+
"Add structured logging",
|
|
579
|
+
"Configure the project's logging format and level.",
|
|
580
|
+
(),
|
|
581
|
+
"S",
|
|
582
|
+
"agent",
|
|
583
|
+
),
|
|
584
|
+
]
|
|
585
|
+
if milestone.id == "M2":
|
|
586
|
+
return [
|
|
587
|
+
(
|
|
588
|
+
"Define domain types",
|
|
589
|
+
"Add Pydantic/dataclass types for the core entities.",
|
|
590
|
+
("Types are immutable", "Round-trip JSON serialization works"),
|
|
591
|
+
"M",
|
|
592
|
+
"agent",
|
|
593
|
+
),
|
|
594
|
+
(
|
|
595
|
+
"Implement domain operations",
|
|
596
|
+
"Write the business rules as pure functions.",
|
|
597
|
+
("Unit tests cover edge cases", "No I/O in this layer"),
|
|
598
|
+
"M",
|
|
599
|
+
"agent",
|
|
600
|
+
),
|
|
601
|
+
(
|
|
602
|
+
"Build the persistence layer",
|
|
603
|
+
"Wrap the chosen datastore behind repository interfaces.",
|
|
604
|
+
("In-memory implementation for tests", "Real implementation behind an env flag"),
|
|
605
|
+
"M",
|
|
606
|
+
"agent",
|
|
607
|
+
),
|
|
608
|
+
]
|
|
609
|
+
if milestone.id == "M3":
|
|
610
|
+
if kind in {"api", "service", "backend"}:
|
|
611
|
+
return [
|
|
612
|
+
(
|
|
613
|
+
"Add HTTP routing",
|
|
614
|
+
"Define routes and request/response models.",
|
|
615
|
+
("OpenAPI schema is generated", "Validation errors are 4xx with detail"),
|
|
616
|
+
"M",
|
|
617
|
+
"agent",
|
|
618
|
+
),
|
|
619
|
+
(
|
|
620
|
+
"Add error handling",
|
|
621
|
+
"Translate exceptions to typed HTTP responses.",
|
|
622
|
+
("No 500s for known error types", "All errors logged with context"),
|
|
623
|
+
"S",
|
|
624
|
+
"agent",
|
|
625
|
+
),
|
|
626
|
+
]
|
|
627
|
+
if kind == "cli":
|
|
628
|
+
return [
|
|
629
|
+
(
|
|
630
|
+
"Define subcommands",
|
|
631
|
+
"Use Typer/Click to declare the command surface.",
|
|
632
|
+
("`--help` is helpful", "Subcommands have unit tests"),
|
|
633
|
+
"M",
|
|
634
|
+
"agent",
|
|
635
|
+
),
|
|
636
|
+
(
|
|
637
|
+
"Add progress + error reporting",
|
|
638
|
+
"Use Rich for progress bars and styled errors.",
|
|
639
|
+
("Errors exit non-zero", "No stack traces in default mode"),
|
|
640
|
+
"S",
|
|
641
|
+
"agent",
|
|
642
|
+
),
|
|
643
|
+
]
|
|
644
|
+
return [
|
|
645
|
+
(
|
|
646
|
+
"Design the public API",
|
|
647
|
+
"Sketch the public functions/classes and their contracts.",
|
|
648
|
+
("Docstrings on every public symbol", "At least one usage example per entry point"),
|
|
649
|
+
"M",
|
|
650
|
+
"agent",
|
|
651
|
+
),
|
|
652
|
+
(
|
|
653
|
+
"Document the public API",
|
|
654
|
+
"Generate API reference docs (mkdocs/sphinx).",
|
|
655
|
+
("Docs build green", "Cross-links to guides"),
|
|
656
|
+
"S",
|
|
657
|
+
"agent",
|
|
658
|
+
),
|
|
659
|
+
]
|
|
660
|
+
if milestone.id == "M4":
|
|
661
|
+
items: list[tuple[str, str, tuple[str, ...], str, str]] = [
|
|
662
|
+
(
|
|
663
|
+
"Add authentication",
|
|
664
|
+
"Choose an auth model and apply it at the interface boundary.",
|
|
665
|
+
("Auth failures are 401/403", "No credentials in logs"),
|
|
666
|
+
"M",
|
|
667
|
+
"agent",
|
|
668
|
+
),
|
|
669
|
+
(
|
|
670
|
+
"Add error reporting",
|
|
671
|
+
"Translate exceptions to typed responses and structured logs.",
|
|
672
|
+
(),
|
|
673
|
+
"S",
|
|
674
|
+
"agent",
|
|
675
|
+
),
|
|
676
|
+
]
|
|
677
|
+
if options.include_observability:
|
|
678
|
+
items.append(
|
|
679
|
+
(
|
|
680
|
+
"Add metrics + tracing",
|
|
681
|
+
"Instrument key request paths with counters and spans.",
|
|
682
|
+
("Metrics endpoint exposed", "Traces sampled at >= 1%"),
|
|
683
|
+
"M",
|
|
684
|
+
"agent",
|
|
685
|
+
)
|
|
686
|
+
)
|
|
687
|
+
return items
|
|
688
|
+
if milestone.id == "M5":
|
|
689
|
+
items = [
|
|
690
|
+
(
|
|
691
|
+
"Write the README",
|
|
692
|
+
"Document install, quickstart, and architecture overview.",
|
|
693
|
+
("Badges are green", "New contributor can follow the steps"),
|
|
694
|
+
"M",
|
|
695
|
+
"human",
|
|
696
|
+
),
|
|
697
|
+
(
|
|
698
|
+
"Add a release checklist",
|
|
699
|
+
"Define pre-release steps: version bump, changelog, tag.",
|
|
700
|
+
(),
|
|
701
|
+
"S",
|
|
702
|
+
"agent",
|
|
703
|
+
),
|
|
704
|
+
]
|
|
705
|
+
if options.include_tests:
|
|
706
|
+
items.append(
|
|
707
|
+
(
|
|
708
|
+
"Audit test coverage",
|
|
709
|
+
"Identify gaps and add tests for the highest-risk paths.",
|
|
710
|
+
("Coverage >= 80%", "No flaky tests on CI"),
|
|
711
|
+
"M",
|
|
712
|
+
"agent",
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
return items
|
|
716
|
+
return []
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _build_risks(
|
|
720
|
+
goal: str, kind: str, stack: tuple[str, ...], options: PlannerOptions
|
|
721
|
+
) -> list[Risk]:
|
|
722
|
+
risks: list[Risk] = [
|
|
723
|
+
Risk(
|
|
724
|
+
id="R1",
|
|
725
|
+
description="Scope creep beyond the initial milestone set.",
|
|
726
|
+
severity=RiskSeverity.MEDIUM,
|
|
727
|
+
likelihood=RiskSeverity.MEDIUM,
|
|
728
|
+
mitigation="Lock the M0 problem statement; require a new milestone to expand scope.",
|
|
729
|
+
),
|
|
730
|
+
Risk(
|
|
731
|
+
id="R2",
|
|
732
|
+
description="Premature optimization of hot paths.",
|
|
733
|
+
severity=RiskSeverity.LOW,
|
|
734
|
+
likelihood=RiskSeverity.MEDIUM,
|
|
735
|
+
mitigation="Measure first; the optimizer module already records call stats.",
|
|
736
|
+
),
|
|
737
|
+
Risk(
|
|
738
|
+
id="R3",
|
|
739
|
+
description="External API rate limits or schema changes.",
|
|
740
|
+
severity=RiskSeverity.MEDIUM,
|
|
741
|
+
likelihood=RiskSeverity.MEDIUM,
|
|
742
|
+
mitigation="Wrap all external calls behind interfaces; ship a mock for tests.",
|
|
743
|
+
),
|
|
744
|
+
]
|
|
745
|
+
if "api" in goal.lower() or kind in {"api", "service", "backend"}:
|
|
746
|
+
risks.append(
|
|
747
|
+
Risk(
|
|
748
|
+
id="R4",
|
|
749
|
+
description="Authentication misconfiguration leaks data.",
|
|
750
|
+
severity=RiskSeverity.HIGH,
|
|
751
|
+
likelihood=RiskSeverity.LOW,
|
|
752
|
+
mitigation="Test auth failure paths in CI; deny by default in the router.",
|
|
753
|
+
)
|
|
754
|
+
)
|
|
755
|
+
if options.include_observability:
|
|
756
|
+
risks.append(
|
|
757
|
+
Risk(
|
|
758
|
+
id="R5",
|
|
759
|
+
description="Missing observability hides production incidents.",
|
|
760
|
+
severity=RiskSeverity.MEDIUM,
|
|
761
|
+
likelihood=RiskSeverity.MEDIUM,
|
|
762
|
+
mitigation="Adopt structured logging + metrics before M4 ships.",
|
|
763
|
+
)
|
|
764
|
+
)
|
|
765
|
+
return risks
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _build_prompt_sequences(
|
|
769
|
+
tasks: list[Task], goal: str, stack: tuple[str, ...]
|
|
770
|
+
) -> list[PromptSequence]:
|
|
771
|
+
stack_line = ", ".join(stack[:4]) if stack else "the chosen stack"
|
|
772
|
+
out: list[PromptSequence] = []
|
|
773
|
+
for task in tasks:
|
|
774
|
+
system = (
|
|
775
|
+
f"You are a senior engineer working on a project to {goal}. "
|
|
776
|
+
f"The project uses {stack_line}. Apply the Ponytail ruleset: "
|
|
777
|
+
f"ship the smallest correct change, reuse existing helpers, prefer "
|
|
778
|
+
f"the standard library, and avoid speculative abstractions."
|
|
779
|
+
)
|
|
780
|
+
user = (
|
|
781
|
+
f"Complete this task:\n\n"
|
|
782
|
+
f"Title: {task.title}\n"
|
|
783
|
+
f"Description: {task.description}\n"
|
|
784
|
+
f"Acceptance criteria:\n"
|
|
785
|
+
+ "\n".join(f"- {c}" for c in task.acceptance or ["(none specified)"])
|
|
786
|
+
+ "\n\nReturn a unified diff, not free-form prose. "
|
|
787
|
+
f"Keep the change to {task.estimate} of work."
|
|
788
|
+
)
|
|
789
|
+
out.append(
|
|
790
|
+
PromptSequence(
|
|
791
|
+
task_id=task.id,
|
|
792
|
+
system=system,
|
|
793
|
+
user=user,
|
|
794
|
+
turns=[
|
|
795
|
+
PromptTurn(role="system", content=system),
|
|
796
|
+
PromptTurn(role="user", content=user),
|
|
797
|
+
],
|
|
798
|
+
)
|
|
799
|
+
)
|
|
800
|
+
return out
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _build_notes(options: PlannerOptions) -> list[str]:
|
|
804
|
+
notes: list[str] = []
|
|
805
|
+
if options.max_milestones != 6:
|
|
806
|
+
notes.append(f"max_milestones={options.max_milestones}")
|
|
807
|
+
if not options.include_tests:
|
|
808
|
+
notes.append("test task omitted by configuration")
|
|
809
|
+
if not options.include_observability:
|
|
810
|
+
notes.append("observability tasks omitted by configuration")
|
|
811
|
+
if options.extra_goals:
|
|
812
|
+
notes.append(f"extra goals: {', '.join(options.extra_goals)}")
|
|
813
|
+
return notes
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
__all__ = [
|
|
817
|
+
"Architecture",
|
|
818
|
+
"Component",
|
|
819
|
+
"ComponentKind",
|
|
820
|
+
"DataFlow",
|
|
821
|
+
"FolderNode",
|
|
822
|
+
"FolderStructure",
|
|
823
|
+
"Milestone",
|
|
824
|
+
"PlannerOptions",
|
|
825
|
+
"Priority",
|
|
826
|
+
"PromptSequence",
|
|
827
|
+
"PromptTurn",
|
|
828
|
+
"Risk",
|
|
829
|
+
"RiskSeverity",
|
|
830
|
+
"SoftwarePlan",
|
|
831
|
+
"Task",
|
|
832
|
+
"TaskStatus",
|
|
833
|
+
"build_software_plan",
|
|
834
|
+
]
|