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,56 @@
1
+ """Planning and agentic execution strategies."""
2
+
3
+ from forgecli.planner.agent import Agent
4
+ from forgecli.planner.plan import Plan, Step, StepStatus
5
+ from forgecli.planner.planner import Planner
6
+ from forgecli.planner.render import print_plan, render_plan
7
+ from forgecli.planner.serialize import plan_to_dict, plan_to_json, plan_to_markdown
8
+ from forgecli.planner.software import (
9
+ Architecture,
10
+ Component,
11
+ ComponentKind,
12
+ DataFlow,
13
+ FolderNode,
14
+ FolderStructure,
15
+ Milestone,
16
+ PlannerOptions,
17
+ Priority,
18
+ PromptSequence,
19
+ PromptTurn,
20
+ Risk,
21
+ RiskSeverity,
22
+ SoftwarePlan,
23
+ Task,
24
+ TaskStatus,
25
+ build_software_plan,
26
+ )
27
+
28
+ __all__ = [
29
+ "Agent",
30
+ "Architecture",
31
+ "Component",
32
+ "ComponentKind",
33
+ "DataFlow",
34
+ "FolderNode",
35
+ "FolderStructure",
36
+ "Milestone",
37
+ "Plan",
38
+ "Planner",
39
+ "PlannerOptions",
40
+ "Priority",
41
+ "PromptSequence",
42
+ "PromptTurn",
43
+ "Risk",
44
+ "RiskSeverity",
45
+ "SoftwarePlan",
46
+ "Step",
47
+ "StepStatus",
48
+ "Task",
49
+ "TaskStatus",
50
+ "build_software_plan",
51
+ "plan_to_dict",
52
+ "plan_to_json",
53
+ "plan_to_markdown",
54
+ "print_plan",
55
+ "render_plan",
56
+ ]
@@ -0,0 +1,61 @@
1
+ """Executes a :class:`Plan` step-by-step using registered tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from typing import Any
7
+
8
+ from forgecli.core.service import Service
9
+ from forgecli.planner.plan import Plan, Step
10
+ from forgecli.planner.planner import Planner
11
+
12
+ ToolFn = Callable[[Step], Awaitable[Any]]
13
+
14
+
15
+ class Agent(Service):
16
+ """Drives a plan through a registry of tool callables.
17
+
18
+ Tools are registered as async callables taking a :class:`Step` and
19
+ returning an arbitrary result. This keeps the agent implementation
20
+ small and lets us add tools (read_file, edit_file, run_shell, ...)
21
+ without subclassing.
22
+ """
23
+
24
+ name = "planner.agent"
25
+
26
+ def __init__(self, planner: Planner) -> None:
27
+ super().__init__()
28
+ self._planner = planner
29
+ self._tools: dict[str, ToolFn] = {}
30
+
31
+ @property
32
+ def planner(self) -> Planner:
33
+ return self._planner
34
+
35
+ def register_tool(self, name: str, fn: ToolFn) -> None:
36
+ """Register ``fn`` as a callable for steps tagged with ``name``."""
37
+ self._tools[name] = fn
38
+
39
+ def tool_names(self) -> list[str]:
40
+ return sorted(self._tools)
41
+
42
+ async def run(self, goal: str, *, context: dict | None = None) -> Plan:
43
+ """Plan and execute ``goal``; returns the executed :class:`Plan`."""
44
+ plan = await self._planner.make_plan(goal, context=context)
45
+ for step in plan.steps:
46
+ await self._execute_step(step)
47
+ return plan
48
+
49
+ async def _execute_step(self, step: Step) -> None:
50
+ if step.tool is None:
51
+ step.mark_done()
52
+ return
53
+ tool = self._tools.get(step.tool)
54
+ if tool is None:
55
+ step.mark_done(error=f"Unknown tool: {step.tool}")
56
+ return
57
+ step.mark_running()
58
+ try:
59
+ step.mark_done(result=await tool(step))
60
+ except Exception as exc:
61
+ step.mark_done(error=str(exc))
@@ -0,0 +1,65 @@
1
+ """Plan/Step value types for the planner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+ from typing import Any
9
+ from uuid import uuid4
10
+
11
+
12
+ class StepStatus(str, Enum):
13
+ """Lifecycle status of a single :class:`Step`."""
14
+
15
+ PENDING = "pending"
16
+ RUNNING = "running"
17
+ SUCCEEDED = "succeeded"
18
+ FAILED = "failed"
19
+ SKIPPED = "skipped"
20
+
21
+
22
+ @dataclass
23
+ class Step:
24
+ """A unit of work in a :class:`Plan`."""
25
+
26
+ id: str = field(default_factory=lambda: str(uuid4()))
27
+ description: str = ""
28
+ tool: str | None = None
29
+ inputs: dict[str, Any] = field(default_factory=dict)
30
+ status: StepStatus = StepStatus.PENDING
31
+ result: Any = None
32
+ error: str | None = None
33
+ started_at: float | None = None
34
+ finished_at: float | None = None
35
+
36
+ def mark_running(self) -> None:
37
+ self.status = StepStatus.RUNNING
38
+ self.started_at = time.time()
39
+
40
+ def mark_done(self, *, result: Any = None, error: str | None = None) -> None:
41
+ self.finished_at = time.time()
42
+ if error is None:
43
+ self.status = StepStatus.SUCCEEDED
44
+ else:
45
+ self.status = StepStatus.FAILED
46
+ self.error = error
47
+ self.result = result
48
+
49
+
50
+ @dataclass
51
+ class Plan:
52
+ """An ordered, named collection of :class:`Step` objects."""
53
+
54
+ id: str = field(default_factory=lambda: str(uuid4()))
55
+ name: str = "plan"
56
+ goal: str = ""
57
+ steps: list[Step] = field(default_factory=list)
58
+ metadata: dict[str, Any] = field(default_factory=dict)
59
+
60
+ def add_step(self, step: Step) -> Step:
61
+ self.steps.append(step)
62
+ return step
63
+
64
+ def pending_steps(self) -> list[Step]:
65
+ return [s for s in self.steps if s.status is StepStatus.PENDING]
@@ -0,0 +1,17 @@
1
+ """High-level planner interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+ from forgecli.planner.plan import Plan
8
+
9
+
10
+ class Planner(ABC):
11
+ """Strategy interface for turning a goal into a :class:`Plan`."""
12
+
13
+ name: str = "abstract"
14
+
15
+ @abstractmethod
16
+ async def make_plan(self, goal: str, *, context: dict | None = None) -> Plan:
17
+ """Build a :class:`Plan` that aims to satisfy ``goal``."""
@@ -0,0 +1,267 @@
1
+ """Rich-based renderer for :class:`SoftwarePlan`.
2
+
3
+ The renderer is pure: it accepts a :class:`SoftwarePlan` and a Rich
4
+ :class:`Console` and emits the plan as a sequence of sections. Callers
5
+ can override the console, choose a different layout, or post-process
6
+ the output before display.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from rich.console import Console, Group
12
+ from rich.panel import Panel
13
+ from rich.table import Table
14
+ from rich.text import Text
15
+ from rich.tree import Tree
16
+
17
+ from forgecli.planner.software import (
18
+ FolderNode,
19
+ Priority,
20
+ RiskSeverity,
21
+ SoftwarePlan,
22
+ )
23
+
24
+ _PRIORITY_STYLE: dict[Priority, str] = {
25
+ Priority.P0: "bold red",
26
+ Priority.P1: "bold yellow",
27
+ Priority.P2: "cyan",
28
+ Priority.P3: "muted",
29
+ }
30
+
31
+ _RISK_STYLE: dict[RiskSeverity, str] = {
32
+ RiskSeverity.LOW: "green",
33
+ RiskSeverity.MEDIUM: "yellow",
34
+ RiskSeverity.HIGH: "bold red",
35
+ }
36
+
37
+
38
+ def print_plan(plan: SoftwarePlan, console: Console | None = None) -> None:
39
+ """Print a :class:`SoftwarePlan` using the given Rich console."""
40
+ console = console or Console()
41
+ for renderable in render_plan(plan):
42
+ console.print(renderable)
43
+
44
+
45
+ def render_plan(plan: SoftwarePlan) -> list:
46
+ """Return a list of Rich renderables that visualize ``plan``."""
47
+ renderables: list = []
48
+ renderables.append(_render_header(plan))
49
+ renderables.append(_render_summary(plan))
50
+ renderables.append(_render_architecture(plan))
51
+ renderables.append(_render_folder_structure(plan))
52
+ renderables.append(_render_milestones(plan))
53
+ renderables.append(_render_tasks(plan))
54
+ renderables.append(_render_risks(plan))
55
+ renderables.extend(_render_prompt_sequences(plan))
56
+ if plan.notes:
57
+ renderables.append(_render_notes(plan))
58
+ return renderables
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Sections
63
+ # ---------------------------------------------------------------------------
64
+
65
+
66
+ def _render_header(plan: SoftwarePlan) -> Panel:
67
+ title = Text("Forge Plan", style="bold accent")
68
+ title.append(" — ", style="muted")
69
+ title.append(plan.goal, style="bold")
70
+ return Panel(
71
+ title,
72
+ border_style="magenta",
73
+ title=Text("forge plan", style="bold magenta"),
74
+ title_align="left",
75
+ padding=(0, 1),
76
+ )
77
+
78
+
79
+ def _render_summary(plan: SoftwarePlan) -> Panel:
80
+ body = Text(plan.summary, style="white")
81
+ stats = Table.grid(padding=(0, 2))
82
+ stats.add_column(style="bold")
83
+ stats.add_column()
84
+ stats.add_row("milestones", str(len(plan.milestones)))
85
+ stats.add_row("tasks", str(len(plan.tasks)))
86
+ stats.add_row("risks", str(len(plan.risks)))
87
+ stats.add_row("prompts", str(len(plan.prompt_sequences)))
88
+ layout = Group(
89
+ body,
90
+ Text(""),
91
+ stats,
92
+ )
93
+ return Panel(layout, title="Summary", border_style="cyan", padding=(0, 1))
94
+
95
+
96
+ def _render_architecture(plan: SoftwarePlan) -> Panel:
97
+ table = Table(
98
+ title="Components",
99
+ title_style="bold cyan",
100
+ header_style="bold magenta",
101
+ show_lines=False,
102
+ expand=True,
103
+ )
104
+ table.add_column("Name", style="bold")
105
+ table.add_column("Kind")
106
+ table.add_column("Purpose", overflow="fold")
107
+ table.add_column("Depends on", style="muted")
108
+ for component in plan.architecture.components:
109
+ table.add_row(
110
+ component.name,
111
+ component.kind.value,
112
+ component.purpose,
113
+ ", ".join(component.depends_on) or "—",
114
+ )
115
+ flows = Table(
116
+ title="Data flow",
117
+ title_style="bold cyan",
118
+ header_style="bold magenta",
119
+ show_lines=False,
120
+ expand=True,
121
+ )
122
+ flows.add_column("Source", style="bold")
123
+ flows.add_column("Target", style="bold")
124
+ flows.add_column("Contract", overflow="fold")
125
+ for flow in plan.architecture.flows:
126
+ flows.add_row(flow.source, flow.target, flow.contract)
127
+ body: list = [
128
+ Text(plan.architecture.summary, style="white"),
129
+ Text(""),
130
+ table,
131
+ Text(""),
132
+ flows,
133
+ ]
134
+ return Panel(Group(*body), title="Architecture", border_style="cyan", padding=(0, 1))
135
+
136
+
137
+ def _render_folder_structure(plan: SoftwarePlan) -> Panel:
138
+ root = plan.folder_structure.root
139
+ tree = Tree(Text(root, style="bold magenta"), guide_style="cyan")
140
+ _populate_tree(tree, plan.folder_structure.tree)
141
+ return Panel(tree, title=f"Folder structure ({root}/)", border_style="cyan", padding=(0, 1))
142
+
143
+
144
+ def _populate_tree(parent: Tree, nodes: list[FolderNode]) -> None:
145
+ for node in nodes:
146
+ label = Text(node.path, style="bold")
147
+ if node.purpose:
148
+ label.append(" ")
149
+ label.append(Text(f"({node.purpose})", style="muted"))
150
+ branch = parent.add(label)
151
+ if node.children:
152
+ _populate_tree(branch, node.children)
153
+
154
+
155
+ def _render_milestones(plan: SoftwarePlan) -> Panel:
156
+ table = Table(
157
+ title="Milestones",
158
+ title_style="bold cyan",
159
+ header_style="bold magenta",
160
+ show_lines=False,
161
+ expand=True,
162
+ )
163
+ table.add_column("ID", style="bold", no_wrap=True)
164
+ table.add_column("Priority", no_wrap=True)
165
+ table.add_column("Title", style="bold")
166
+ table.add_column("Description", overflow="fold")
167
+ table.add_column("Tasks", justify="right", style="muted")
168
+ for milestone in plan.milestones:
169
+ table.add_row(
170
+ milestone.id,
171
+ Text(milestone.priority.value, style=_PRIORITY_STYLE[milestone.priority]),
172
+ milestone.title,
173
+ milestone.description,
174
+ str(len(milestone.task_ids)),
175
+ )
176
+ return Panel(table, title="Roadmap", border_style="cyan", padding=(0, 1))
177
+
178
+
179
+ def _render_tasks(plan: SoftwarePlan) -> Panel:
180
+ table = Table(
181
+ title="Tasks",
182
+ title_style="bold cyan",
183
+ header_style="bold magenta",
184
+ show_lines=True,
185
+ expand=True,
186
+ )
187
+ table.add_column("ID", style="bold", no_wrap=True)
188
+ table.add_column("M", no_wrap=True, style="muted")
189
+ table.add_column("Priority", no_wrap=True)
190
+ table.add_column("Size", no_wrap=True, justify="right")
191
+ table.add_column("Title", style="bold")
192
+ table.add_column("Acceptance", overflow="fold", style="muted")
193
+ for task in plan.tasks:
194
+ acceptance = "\n".join(f"• {c}" for c in task.acceptance) or "—"
195
+ table.add_row(
196
+ task.id,
197
+ task.milestone_id,
198
+ Text(task.priority.value, style=_PRIORITY_STYLE[task.priority]),
199
+ task.estimate,
200
+ task.title,
201
+ acceptance,
202
+ )
203
+ return Panel(table, title="Task list", border_style="cyan", padding=(0, 1))
204
+
205
+
206
+ def _render_risks(plan: SoftwarePlan) -> Panel:
207
+ table = Table(
208
+ title="Risks",
209
+ title_style="bold cyan",
210
+ header_style="bold magenta",
211
+ show_lines=True,
212
+ expand=True,
213
+ )
214
+ table.add_column("ID", style="bold", no_wrap=True)
215
+ table.add_column("Severity", no_wrap=True)
216
+ table.add_column("Likelihood", no_wrap=True)
217
+ table.add_column("Description", overflow="fold")
218
+ table.add_column("Mitigation", overflow="fold", style="muted")
219
+ for risk in plan.risks:
220
+ table.add_row(
221
+ risk.id,
222
+ Text(risk.severity.value, style=_RISK_STYLE[risk.severity]),
223
+ Text(risk.likelihood.value, style=_RISK_STYLE[risk.likelihood]),
224
+ risk.description,
225
+ risk.mitigation or "—",
226
+ )
227
+ return Panel(table, title="Risk register", border_style="cyan", padding=(0, 1))
228
+
229
+
230
+ def _render_prompt_sequences(plan: SoftwarePlan) -> list[Panel]:
231
+ panels: list[Panel] = []
232
+ for sequence in plan.prompt_sequences[:3]:
233
+ body = Text()
234
+ body.append("system: ", style="bold magenta")
235
+ body.append(sequence.system, style="white")
236
+ body.append("\n\n")
237
+ body.append("user: ", style="bold cyan")
238
+ body.append(sequence.user, style="white")
239
+ panels.append(
240
+ Panel(
241
+ body,
242
+ title=f"Prompt — task {sequence.task_id}",
243
+ border_style="magenta",
244
+ padding=(0, 1),
245
+ )
246
+ )
247
+ remaining = len(plan.prompt_sequences) - 3
248
+ if remaining > 0:
249
+ panels.append(
250
+ Panel(
251
+ Text(
252
+ f"+ {remaining} more prompt sequences (use --json or --md to view all).",
253
+ style="muted",
254
+ ),
255
+ border_style="magenta",
256
+ padding=(0, 1),
257
+ )
258
+ )
259
+ return panels
260
+
261
+
262
+ def _render_notes(plan: SoftwarePlan) -> Panel:
263
+ body = Text("\n".join(f"• {note}" for note in plan.notes), style="muted")
264
+ return Panel(body, title="Notes", border_style="cyan", padding=(0, 1))
265
+
266
+
267
+ __all__ = ["print_plan", "render_plan"]
@@ -0,0 +1,104 @@
1
+ """Serialize :class:`SoftwarePlan` to JSON or Markdown."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from forgecli.planner.software import SoftwarePlan
8
+
9
+
10
+ def plan_to_json(plan: SoftwarePlan, *, indent: int = 2) -> str:
11
+ """Return a JSON string representation of ``plan``."""
12
+ return plan.model_dump_json(indent=indent)
13
+
14
+
15
+ def plan_to_dict(plan: SoftwarePlan) -> dict[str, Any]:
16
+ """Return the plan as a JSON-serializable dict."""
17
+ return plan.model_dump(mode="json")
18
+
19
+
20
+ def plan_to_markdown(plan: SoftwarePlan) -> str:
21
+ """Return a Markdown rendering of ``plan``."""
22
+ parts: list[str] = []
23
+ parts.append(f"# Plan: {plan.goal}\n")
24
+ parts.append(f"_{plan.summary}_\n")
25
+
26
+ parts.append("## Architecture\n")
27
+ parts.append(plan.architecture.summary + "\n")
28
+ parts.append("### Components\n")
29
+ parts.append("| Name | Kind | Purpose | Depends on |")
30
+ parts.append("| --- | --- | --- | --- |")
31
+ for component in plan.architecture.components:
32
+ deps = ", ".join(component.depends_on) or "—"
33
+ parts.append(
34
+ f"| {component.name} | {component.kind.value} | {component.purpose} | {deps} |"
35
+ )
36
+ parts.append("\n### Data flow\n")
37
+ parts.append("| Source | Target | Contract |")
38
+ parts.append("| --- | --- | --- |")
39
+ for flow in plan.architecture.flows:
40
+ parts.append(f"| {flow.source} | {flow.target} | {flow.contract} |")
41
+
42
+ parts.append("\n## Folder structure\n")
43
+ parts.append(f"Root: `{plan.folder_structure.root}/`\n")
44
+ parts.append("```")
45
+ for line in _format_tree(plan.folder_structure.tree):
46
+ parts.append(line)
47
+ parts.append("```")
48
+
49
+ parts.append("\n## Roadmap\n")
50
+ for milestone in plan.milestones:
51
+ parts.append(f"### {milestone.id} — {milestone.title} ({milestone.priority.value})")
52
+ parts.append(milestone.description)
53
+ parts.append("\n**Deliverables:**")
54
+ for deliverable in milestone.deliverables:
55
+ parts.append(f"- {deliverable}")
56
+
57
+ parts.append("\n## Tasks\n")
58
+ parts.append("| ID | M | Priority | Size | Title | Owner |")
59
+ parts.append("| --- | --- | --- | --- | --- | --- |")
60
+ for task in plan.tasks:
61
+ parts.append(
62
+ f"| {task.id} | {task.milestone_id} | {task.priority.value} | "
63
+ f"{task.estimate} | {task.title} | {task.owner} |"
64
+ )
65
+
66
+ parts.append("\n## Risks\n")
67
+ parts.append("| ID | Severity | Likelihood | Description | Mitigation |")
68
+ parts.append("| --- | --- | --- | --- | --- |")
69
+ for risk in plan.risks:
70
+ parts.append(
71
+ f"| {risk.id} | {risk.severity.value} | {risk.likelihood.value} | "
72
+ f"{risk.description} | {risk.mitigation or '—'} |"
73
+ )
74
+
75
+ parts.append("\n## Prompt sequences\n")
76
+ for sequence in plan.prompt_sequences:
77
+ parts.append(f"### Task {sequence.task_id}\n")
78
+ parts.append("**System**\n")
79
+ parts.append(f"```\n{sequence.system}\n```\n")
80
+ parts.append("**User**\n")
81
+ parts.append(f"```\n{sequence.user}\n```\n")
82
+
83
+ if plan.notes:
84
+ parts.append("\n## Notes\n")
85
+ for note in plan.notes:
86
+ parts.append(f"- {note}")
87
+
88
+ return "\n".join(parts) + "\n"
89
+
90
+
91
+ def _format_tree(nodes, prefix: str = "") -> list[str]:
92
+ lines: list[str] = []
93
+ for index, node in enumerate(nodes):
94
+ last = index == len(nodes) - 1
95
+ connector = "└── " if last else "├── "
96
+ annotation = f" ({node.purpose})" if node.purpose else ""
97
+ lines.append(f"{prefix}{connector}{node.path}{annotation}")
98
+ if node.children:
99
+ extension = " " if last else "│ "
100
+ lines.extend(_format_tree(node.children, prefix + extension))
101
+ return lines
102
+
103
+
104
+ __all__ = ["plan_to_dict", "plan_to_json", "plan_to_markdown"]