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.
- 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 +149 -0
- forgecli/cli/commands_wrappers.py +80 -0
- forgecli/cli/daemon.py +744 -0
- forgecli/cli/main.py +234 -0
- forgecli/cli/ui.py +56 -0
- forgecli/config/__init__.py +28 -0
- forgecli/config/loader.py +85 -0
- forgecli/config/settings.py +206 -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 +214 -0
- forgecli/core/plugins.py +54 -0
- forgecli/core/service.py +22 -0
- forgecli/docs/__init__.py +5 -0
- forgecli/docs/generator.py +115 -0
- forgecli/engine/__init__.py +105 -0
- forgecli/engine/context.py +163 -0
- forgecli/engine/defaults.py +89 -0
- forgecli/engine/events.py +185 -0
- forgecli/engine/execution.py +519 -0
- forgecli/engine/plugins.py +145 -0
- forgecli/engine/runner.py +158 -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 +102 -0
- forgecli/engine/stages/git_engine.py +30 -0
- forgecli/engine/stages/intent_analyzer.py +38 -0
- forgecli/engine/stages/model_router.py +65 -0
- forgecli/engine/stages/planning_engine.py +45 -0
- forgecli/engine/stages/repository_analyzer.py +54 -0
- forgecli/engine/stages/validation_engine.py +87 -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 +448 -0
- forgecli/graph/edge.py +19 -0
- forgecli/graph/graph.py +85 -0
- forgecli/graph/graphify.py +405 -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 +136 -0
- forgecli/memory/store.py +85 -0
- forgecli/optimizer/__init__.py +13 -0
- forgecli/optimizer/caveman/__init__.py +169 -0
- forgecli/optimizer/caveman/cli.py +62 -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 +50 -0
- forgecli/optimizer/chunker.py +85 -0
- forgecli/optimizer/optimizer.py +81 -0
- forgecli/optimizer/ponytail/__init__.py +181 -0
- forgecli/optimizer/ponytail/cli.py +159 -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 +51 -0
- forgecli/optimizer/ranker.py +37 -0
- forgecli/optimizer/summarizer.py +44 -0
- forgecli/orchestrator/__init__.py +706 -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 +267 -0
- forgecli/planner/serialize.py +104 -0
- forgecli/planner/software.py +832 -0
- forgecli/platform/__init__.py +91 -0
- forgecli/platform/core.py +201 -0
- forgecli/platform/deps.py +361 -0
- forgecli/platform/paths.py +234 -0
- forgecli/platform/shell.py +176 -0
- forgecli/platform/update.py +253 -0
- forgecli/plugins/__init__.py +249 -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 +207 -0
- forgecli/providers/base.py +204 -0
- forgecli/providers/builtin.py +26 -0
- forgecli/providers/conversation.py +74 -0
- forgecli/providers/google.py +290 -0
- forgecli/providers/http_base.py +207 -0
- forgecli/providers/mock.py +111 -0
- forgecli/providers/openai.py +206 -0
- forgecli/providers/openai_compatible.py +787 -0
- forgecli/providers/router.py +340 -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 +255 -0
- forgecli/review/analyzers/complexity.py +162 -0
- forgecli/review/analyzers/dead_code.py +255 -0
- forgecli/review/analyzers/duplicates.py +161 -0
- forgecli/review/analyzers/performance.py +161 -0
- forgecli/review/analyzers/security.py +244 -0
- forgecli/review/finding.py +68 -0
- forgecli/review/report.py +321 -0
- forgecli/review/repository.py +130 -0
- forgecli/review/suggestions.py +98 -0
- forgecli/runtime/__init__.py +6 -0
- forgecli/runtime/cache_store.py +75 -0
- forgecli/runtime/mcp_config.py +118 -0
- forgecli/runtime/prepare.py +203 -0
- forgecli/runtime/wrappers.py +150 -0
- forgecli/sdk/__init__.py +132 -0
- forgecli/sdk/events.py +206 -0
- forgecli/sdk/interfaces.py +282 -0
- forgecli/sdk/loader.py +239 -0
- forgecli/sdk/manager.py +678 -0
- forgecli/sdk/manifest.py +395 -0
- forgecli/sdk/sandbox.py +247 -0
- forgecli/sdk/version.py +313 -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 +121 -0
- forgecli/utils/ids.py +11 -0
- forgecli/utils/io.py +27 -0
- forgecli/utils/paths.py +78 -0
- forgecli/utils/stats.py +179 -0
- forgecli/utils/timing.py +25 -0
- forgeoptimizer-0.1.0.dist-info/METADATA +177 -0
- forgeoptimizer-0.1.0.dist-info/RECORD +159 -0
- forgeoptimizer-0.1.0.dist-info/WHEEL +4 -0
- forgeoptimizer-0.1.0.dist-info/entry_points.txt +2 -0
- 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))
|
forgecli/planner/plan.py
ADDED
|
@@ -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"]
|