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
forgecli/core/models.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Modular registry for all supported AI models in ForgeCLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
ModelTier = Literal["latest", "recommended", "normal", "legacy", "deprecated"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class ModelDef:
|
|
13
|
+
id: str
|
|
14
|
+
display_name: str
|
|
15
|
+
tier: ModelTier
|
|
16
|
+
provider: str # Lowercase provider name, e.g., 'openai', 'groq'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Single source of truth for the model catalog
|
|
20
|
+
MODEL_CATALOG: list[ModelDef] = [
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# OpenAI
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
ModelDef(id="gpt-5.5", display_name="GPT-5.5", tier="latest", provider="openai"),
|
|
25
|
+
ModelDef(id="gpt-5", display_name="GPT-5", tier="recommended", provider="openai"),
|
|
26
|
+
ModelDef(id="gpt-5-mini", display_name="GPT-5 Mini", tier="normal", provider="openai"),
|
|
27
|
+
ModelDef(id="gpt-4.1", display_name="GPT-4.1", tier="legacy", provider="openai"),
|
|
28
|
+
ModelDef(id="gpt-4.1-mini", display_name="GPT-4.1 Mini", tier="legacy", provider="openai"),
|
|
29
|
+
ModelDef(id="gpt-4o", display_name="GPT-4o", tier="legacy", provider="openai"),
|
|
30
|
+
ModelDef(id="gpt-4o-mini", display_name="GPT-4o Mini", tier="legacy", provider="openai"),
|
|
31
|
+
ModelDef(id="gpt-4-turbo", display_name="GPT-4 Turbo", tier="legacy", provider="openai"),
|
|
32
|
+
ModelDef(id="o1", display_name="o1", tier="legacy", provider="openai"),
|
|
33
|
+
ModelDef(id="o1-preview", display_name="o1 Preview", tier="legacy", provider="openai"),
|
|
34
|
+
ModelDef(id="o1-mini", display_name="o1 Mini", tier="legacy", provider="openai"),
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Anthropic
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
ModelDef(
|
|
39
|
+
id="claude-opus-4.8", display_name="Claude Opus 4.8", tier="latest", provider="anthropic"
|
|
40
|
+
),
|
|
41
|
+
ModelDef(
|
|
42
|
+
id="claude-opus-4.6", display_name="Claude Opus 4.6", tier="latest", provider="anthropic"
|
|
43
|
+
),
|
|
44
|
+
ModelDef(
|
|
45
|
+
id="claude-sonnet-4.6",
|
|
46
|
+
display_name="Claude Sonnet 4.6",
|
|
47
|
+
tier="recommended",
|
|
48
|
+
provider="anthropic",
|
|
49
|
+
),
|
|
50
|
+
ModelDef(
|
|
51
|
+
id="claude-sonnet-4.5",
|
|
52
|
+
display_name="Claude Sonnet 4.5",
|
|
53
|
+
tier="legacy",
|
|
54
|
+
provider="anthropic",
|
|
55
|
+
),
|
|
56
|
+
ModelDef(
|
|
57
|
+
id="claude-haiku-4.5", display_name="Claude Haiku 4.5", tier="legacy", provider="anthropic"
|
|
58
|
+
),
|
|
59
|
+
ModelDef(
|
|
60
|
+
id="claude-3-5-sonnet-latest",
|
|
61
|
+
display_name="Claude 3.5 Sonnet",
|
|
62
|
+
tier="legacy",
|
|
63
|
+
provider="anthropic",
|
|
64
|
+
),
|
|
65
|
+
ModelDef(
|
|
66
|
+
id="claude-3-5-haiku-latest",
|
|
67
|
+
display_name="Claude 3.5 Haiku",
|
|
68
|
+
tier="legacy",
|
|
69
|
+
provider="anthropic",
|
|
70
|
+
),
|
|
71
|
+
ModelDef(
|
|
72
|
+
id="claude-3-opus-latest", display_name="Claude 3 Opus", tier="legacy", provider="anthropic"
|
|
73
|
+
),
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Google Gemini
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
ModelDef(
|
|
78
|
+
id="gemini-2.5-pro", display_name="Gemini 2.5 Pro", tier="recommended", provider="google"
|
|
79
|
+
),
|
|
80
|
+
ModelDef(
|
|
81
|
+
id="gemini-2.5-flash",
|
|
82
|
+
display_name="Gemini 2.5 Flash",
|
|
83
|
+
tier="recommended",
|
|
84
|
+
provider="google",
|
|
85
|
+
),
|
|
86
|
+
ModelDef(
|
|
87
|
+
id="gemini-2.5-flash-lite",
|
|
88
|
+
display_name="Gemini 2.5 Flash Lite",
|
|
89
|
+
tier="normal",
|
|
90
|
+
provider="google",
|
|
91
|
+
),
|
|
92
|
+
ModelDef(
|
|
93
|
+
id="gemini-2.0-flash", display_name="Gemini 2.0 Flash", tier="legacy", provider="google"
|
|
94
|
+
),
|
|
95
|
+
ModelDef(id="gemini-1.5-pro", display_name="Gemini 1.5 Pro", tier="legacy", provider="google"),
|
|
96
|
+
ModelDef(
|
|
97
|
+
id="gemini-1.5-flash", display_name="Gemini 1.5 Flash", tier="legacy", provider="google"
|
|
98
|
+
),
|
|
99
|
+
ModelDef(
|
|
100
|
+
id="gemini-2.0-flash-exp",
|
|
101
|
+
display_name="Gemini 2.0 Flash Exp",
|
|
102
|
+
tier="legacy",
|
|
103
|
+
provider="google",
|
|
104
|
+
),
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# OpenRouter
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
ModelDef(id="glm-5.2", display_name="GLM 5.2", tier="normal", provider="openrouter"),
|
|
109
|
+
ModelDef(id="deepseek-v3", display_name="DeepSeek V3", tier="normal", provider="openrouter"),
|
|
110
|
+
ModelDef(id="deepseek-r1", display_name="DeepSeek R1", tier="normal", provider="openrouter"),
|
|
111
|
+
ModelDef(id="qwen3-coder", display_name="Qwen3 Coder", tier="normal", provider="openrouter"),
|
|
112
|
+
ModelDef(id="qwen3-32b", display_name="Qwen3 32B", tier="normal", provider="openrouter"),
|
|
113
|
+
ModelDef(id="kimi-k2", display_name="Kimi K2", tier="normal", provider="openrouter"),
|
|
114
|
+
ModelDef(
|
|
115
|
+
id="llama-4-maverick", display_name="Llama 4 Maverick", tier="normal", provider="openrouter"
|
|
116
|
+
),
|
|
117
|
+
ModelDef(
|
|
118
|
+
id="llama-4-scout", display_name="Llama 4 Scout", tier="normal", provider="openrouter"
|
|
119
|
+
),
|
|
120
|
+
ModelDef(
|
|
121
|
+
id="llama-3.3-70b", display_name="Llama 3.3 70B", tier="normal", provider="openrouter"
|
|
122
|
+
),
|
|
123
|
+
ModelDef(id="gemma-3", display_name="Gemma 3", tier="normal", provider="openrouter"),
|
|
124
|
+
ModelDef(id="devstral", display_name="Devstral", tier="normal", provider="openrouter"),
|
|
125
|
+
ModelDef(id="codestral", display_name="Codestral", tier="normal", provider="openrouter"),
|
|
126
|
+
ModelDef(id="phi-4", display_name="Phi-4", tier="normal", provider="openrouter"),
|
|
127
|
+
ModelDef(
|
|
128
|
+
id="mistral-large", display_name="Mistral Large", tier="normal", provider="openrouter"
|
|
129
|
+
),
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# Groq
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
ModelDef(id="llama-4-scout", display_name="Llama 4 Scout", tier="normal", provider="groq"),
|
|
134
|
+
ModelDef(id="deepseek-r1", display_name="DeepSeek R1", tier="normal", provider="groq"),
|
|
135
|
+
ModelDef(id="qwen3-32b", display_name="Qwen3 32B", tier="normal", provider="groq"),
|
|
136
|
+
ModelDef(id="gemma-3", display_name="Gemma 3", tier="normal", provider="groq"),
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# Mistral
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
ModelDef(id="mistral-large", display_name="Mistral Large", tier="normal", provider="mistral"),
|
|
141
|
+
ModelDef(id="magistral", display_name="Magistral", tier="normal", provider="mistral"),
|
|
142
|
+
ModelDef(id="mistral-small", display_name="Mistral Small", tier="normal", provider="mistral"),
|
|
143
|
+
ModelDef(id="codestral", display_name="Codestral", tier="normal", provider="mistral"),
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# MiniMax
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
ModelDef(
|
|
148
|
+
id="abab6.5g-chat", display_name="Abab 6.5G Chat", tier="recommended", provider="minimax"
|
|
149
|
+
),
|
|
150
|
+
ModelDef(id="abab6.5-chat", display_name="Abab 6.5 Chat", tier="legacy", provider="minimax"),
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# xAI (Grok)
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
ModelDef(id="grok-2", display_name="Grok 2", tier="recommended", provider="xai"),
|
|
155
|
+
ModelDef(id="grok-beta", display_name="Grok Beta", tier="latest", provider="xai"),
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Together AI
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
ModelDef(
|
|
160
|
+
id="llama-3.1-70b", display_name="Llama 3.1 70B", tier="recommended", provider="together"
|
|
161
|
+
),
|
|
162
|
+
ModelDef(
|
|
163
|
+
id="llama-3.1-405b", display_name="Llama 3.1 405B", tier="latest", provider="together"
|
|
164
|
+
),
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# Fireworks AI
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
ModelDef(
|
|
169
|
+
id="llama-3.1-70b", display_name="Llama 3.1 70B", tier="recommended", provider="fireworks"
|
|
170
|
+
),
|
|
171
|
+
ModelDef(
|
|
172
|
+
id="llama-3.1-405b", display_name="Llama 3.1 405B", tier="latest", provider="fireworks"
|
|
173
|
+
),
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# Cohere
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
ModelDef(id="command-r-plus", display_name="Command R+", tier="recommended", provider="cohere"),
|
|
178
|
+
ModelDef(id="command-r", display_name="Command R", tier="normal", provider="cohere"),
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# NVIDIA NIM
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
ModelDef(
|
|
183
|
+
id="llama-3.1-70b", display_name="Llama 3.1 70B", tier="recommended", provider="nvidia"
|
|
184
|
+
),
|
|
185
|
+
ModelDef(id="llama-3.1-405b", display_name="Llama 3.1 405B", tier="latest", provider="nvidia"),
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# Ollama
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
ModelDef(id="llama3", display_name="Llama 3", tier="normal", provider="ollama"),
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# LM Studio
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
ModelDef(id="local-model", display_name="Local Model", tier="normal", provider="lmstudio"),
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
# vLLM
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
ModelDef(id="local-model", display_name="Local Model", tier="normal", provider="vllm"),
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_model_def(model_id: str, provider: str | None = None) -> ModelDef | None:
|
|
202
|
+
"""Retrieve the model definition by model ID, optionally filtering by provider."""
|
|
203
|
+
model_id_lower = model_id.lower().strip()
|
|
204
|
+
provider_lower = provider.lower().strip() if provider else None
|
|
205
|
+
for m in MODEL_CATALOG:
|
|
206
|
+
if m.id == model_id_lower and (provider_lower is None or m.provider == provider_lower):
|
|
207
|
+
return m
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def get_display_name(model_id: str, provider: str | None = None) -> str:
|
|
212
|
+
"""Get the friendly display name for a model, defaulting to the ID itself."""
|
|
213
|
+
m = get_model_def(model_id, provider)
|
|
214
|
+
return m.display_name if m else model_id
|
forgecli/core/plugins.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Plugin discovery and lifecycle.
|
|
2
|
+
|
|
3
|
+
The plugin system is intentionally tiny for the scaffold: plugins are
|
|
4
|
+
loaded by entry point group (``forgecli.plugins``) and must implement
|
|
5
|
+
:class:`Plugin`. A real implementation may also support directory
|
|
6
|
+
plugins (``plugins_dir``) but the interface is the same.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from importlib import metadata
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from forgecli.core.context import AppContext
|
|
16
|
+
from forgecli.core.errors import PluginError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Plugin(ABC):
|
|
20
|
+
"""Base class for ForgeCLI plugins."""
|
|
21
|
+
|
|
22
|
+
name: str = "plugin"
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def configure(self, context: AppContext) -> None:
|
|
26
|
+
"""Register services, commands, providers, etc. on ``context``."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def discover_plugins(group: str = "forgecli.plugins") -> list[Plugin]:
|
|
30
|
+
"""Discover installed plugins via the ``group`` entry point."""
|
|
31
|
+
plugins: list[Plugin] = []
|
|
32
|
+
try:
|
|
33
|
+
entries = metadata.entry_points(group=group)
|
|
34
|
+
except Exception as exc:
|
|
35
|
+
raise PluginError(f"Plugin discovery failed: {exc}") from exc
|
|
36
|
+
for ep in entries:
|
|
37
|
+
try:
|
|
38
|
+
plugin_cls = ep.load()
|
|
39
|
+
plugin = plugin_cls() # type: ignore[call-arg]
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
raise PluginError(f"Failed to load plugin {ep.name!r}: {exc}") from exc
|
|
42
|
+
if not isinstance(plugin, Plugin):
|
|
43
|
+
raise PluginError(f"Plugin {ep.name!r} must subclass forgecli.plugins.Plugin")
|
|
44
|
+
plugins.append(plugin)
|
|
45
|
+
return plugins
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def install_plugins(context: AppContext, plugins: list[Plugin]) -> None:
|
|
49
|
+
"""Configure each plugin against ``context``."""
|
|
50
|
+
for plugin in plugins:
|
|
51
|
+
plugin.configure(context)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = ["Any", "Plugin", "discover_plugins", "install_plugins"]
|
forgecli/core/service.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Composable service lifecycle base class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from forgecli.core.logging import get_logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Service:
|
|
9
|
+
"""Base class for application services with structured logging."""
|
|
10
|
+
|
|
11
|
+
name: str = "service"
|
|
12
|
+
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
self.log = get_logger(f"forgecli.{self.name}")
|
|
15
|
+
|
|
16
|
+
async def start(self) -> None: # pragma: no cover - placeholder
|
|
17
|
+
"""Bring the service online. Override in subclasses."""
|
|
18
|
+
self.log.debug("start() called (no-op)")
|
|
19
|
+
|
|
20
|
+
async def stop(self) -> None: # pragma: no cover - placeholder
|
|
21
|
+
"""Tear the service down. Override in subclasses."""
|
|
22
|
+
self.log.debug("stop() called (no-op)")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Auto-generate project documentation from the Graphify knowledge graph.
|
|
2
|
+
|
|
3
|
+
The generator walks the project, asks Graphify for a snapshot, and
|
|
4
|
+
emits a Markdown report at ``docs/OVERVIEW.md`` with:
|
|
5
|
+
|
|
6
|
+
* a module-by-module summary derived from the graph nodes;
|
|
7
|
+
* the file tree of the project;
|
|
8
|
+
* a flat list of every symbol with its location.
|
|
9
|
+
|
|
10
|
+
The output is intentionally simple: the goal is to give an LLM
|
|
11
|
+
(or a human) a starting point for richer docs.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from datetime import date
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from forgecli.core.context import AppContext
|
|
20
|
+
from forgecli.graph.backend_graphify import GraphifyRepositoryGraph
|
|
21
|
+
from forgecli.utils.fs import ensure_dir
|
|
22
|
+
|
|
23
|
+
_INTRO_TEMPLATE = """\
|
|
24
|
+
# {project} — Auto-generated overview
|
|
25
|
+
|
|
26
|
+
_Generated on {today} by `forge docs`._
|
|
27
|
+
|
|
28
|
+
This document is a starting point: it summarises the module layout
|
|
29
|
+
and lists every symbol the Graphify knowledge graph knows about.
|
|
30
|
+
For deeper documentation, run `forge plan`, `forge build`, or
|
|
31
|
+
`forge explain` on individual modules.
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def generate_docs(context: AppContext, *, output: Path | None = None) -> Path:
|
|
37
|
+
"""Generate the overview file and return its path."""
|
|
38
|
+
root = context.cwd
|
|
39
|
+
target = output or (root / "docs" / "OVERVIEW.md")
|
|
40
|
+
ensure_dir(target.parent)
|
|
41
|
+
|
|
42
|
+
graph = GraphifyRepositoryGraph(root=root)
|
|
43
|
+
snapshot = graph._cached # may be None; load() if needed.
|
|
44
|
+
|
|
45
|
+
# Build a tiny ad-hoc snapshot by re-walking the project.
|
|
46
|
+
nodes = _walk_nodes(root)
|
|
47
|
+
communities = _community_buckets(nodes)
|
|
48
|
+
|
|
49
|
+
lines: list[str] = [
|
|
50
|
+
_INTRO_TEMPLATE.format(project=root.name, today=date.today().isoformat()),
|
|
51
|
+
]
|
|
52
|
+
lines.append("## Modules\n")
|
|
53
|
+
for community, members in sorted(communities.items()):
|
|
54
|
+
lines.append(f"### {community}\n")
|
|
55
|
+
for node in sorted(members, key=lambda n: str(n["path"])):
|
|
56
|
+
location = f"`{node['path']}:{node['line']}`" if node["line"] else f"`{node['path']}`"
|
|
57
|
+
lines.append(f"- {node['label']} — {location}")
|
|
58
|
+
lines.append("")
|
|
59
|
+
if not nodes:
|
|
60
|
+
lines.append("_No indexed symbols found._\n")
|
|
61
|
+
target.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
|
|
62
|
+
_ = snapshot # silence unused
|
|
63
|
+
return target
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _walk_nodes(root: Path) -> list[dict[str, object]]:
|
|
67
|
+
"""Build a small node list from the project tree.
|
|
68
|
+
|
|
69
|
+
This is a *fallback* when Graphify hasn't been run. The docs
|
|
70
|
+
generator is meant to be cheap and offline; it doesn't shell out
|
|
71
|
+
to the Graphify CLI.
|
|
72
|
+
"""
|
|
73
|
+
nodes: list[dict[str, object]] = []
|
|
74
|
+
for path in sorted(root.rglob("*.py")):
|
|
75
|
+
if any(part.startswith(".") for part in path.parts):
|
|
76
|
+
continue
|
|
77
|
+
if any(part in {"__pycache__", "node_modules", ".venv", "venv"} for part in path.parts):
|
|
78
|
+
continue
|
|
79
|
+
rel = path.relative_to(root)
|
|
80
|
+
try:
|
|
81
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
82
|
+
except OSError:
|
|
83
|
+
continue
|
|
84
|
+
nodes.append({"path": str(rel), "label": path.name, "line": 1})
|
|
85
|
+
for index, line in enumerate(text.splitlines(), start=1):
|
|
86
|
+
stripped = line.lstrip()
|
|
87
|
+
if stripped.startswith(("def ", "class ", "async def ")):
|
|
88
|
+
name = _extract_symbol_name(stripped)
|
|
89
|
+
if name:
|
|
90
|
+
nodes.append({"path": str(rel), "label": name, "line": index})
|
|
91
|
+
return nodes
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _extract_symbol_name(line: str) -> str | None:
|
|
95
|
+
"""Return the symbol name from a ``def foo(...)`` / ``class Foo:`` line."""
|
|
96
|
+
line = line.strip()
|
|
97
|
+
for prefix in ("async def ", "def ", "class "):
|
|
98
|
+
if line.startswith(prefix):
|
|
99
|
+
rest = line[len(prefix) :]
|
|
100
|
+
return rest.split("(", 1)[0].split(":", 1)[0].strip() or None
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _community_buckets(nodes: list[dict[str, object]]) -> dict[str, list[dict[str, object]]]:
|
|
105
|
+
"""Group nodes by their top-level directory (the "module")."""
|
|
106
|
+
buckets: dict[str, list[dict[str, object]]] = {}
|
|
107
|
+
for node in nodes:
|
|
108
|
+
path = str(node["path"])
|
|
109
|
+
parts = path.split("/")
|
|
110
|
+
module = parts[0] if len(parts) > 1 else path
|
|
111
|
+
buckets.setdefault(module, []).append(node)
|
|
112
|
+
return buckets
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
__all__ = ["generate_docs"]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""The ForgeCLI Execution Engine.
|
|
2
|
+
|
|
3
|
+
This package defines the *contract* for every orchestration stage
|
|
4
|
+
in ForgeCLI. The pipeline is a fixed sequence of eight stages:
|
|
5
|
+
|
|
6
|
+
1. Intent Analyzer — turn the prompt into an Intent
|
|
7
|
+
2. Repository Analyzer — query Graphify for relevant context
|
|
8
|
+
3. Context Optimizer — apply Ponytail to the prompt + context
|
|
9
|
+
4. Planning Engine — produce a SoftwarePlan
|
|
10
|
+
5. Model Router — pick (provider, model) for the call
|
|
11
|
+
6. Execution Engine — invoke the LLM, extract a diff
|
|
12
|
+
7. Validation Engine — apply the diff + run tests + auto-fix
|
|
13
|
+
8. Git Engine — stage / push the changes
|
|
14
|
+
|
|
15
|
+
Every stage is an independent object behind the :class:`Stage`
|
|
16
|
+
Protocol. The :class:`ExecutionEngine` runs them in order, emits
|
|
17
|
+
structured events, supports retries, cancellation, and plugin
|
|
18
|
+
hooks. No business logic lives in this package — the actual
|
|
19
|
+
implementations live in :mod:`forgecli.engine.stages` and may be
|
|
20
|
+
replaced by plugins.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from forgecli.engine.context import (
|
|
24
|
+
EngineContext,
|
|
25
|
+
IntentAnalysis,
|
|
26
|
+
ModelSelection,
|
|
27
|
+
RetrievalResult,
|
|
28
|
+
StageLog,
|
|
29
|
+
)
|
|
30
|
+
from forgecli.engine.defaults import (
|
|
31
|
+
default_registry,
|
|
32
|
+
)
|
|
33
|
+
from forgecli.engine.events import (
|
|
34
|
+
EngineCancelledError,
|
|
35
|
+
EngineEvent,
|
|
36
|
+
EventBus,
|
|
37
|
+
LogLevel,
|
|
38
|
+
ProgressEvent,
|
|
39
|
+
StageEvent,
|
|
40
|
+
TextLogEvent,
|
|
41
|
+
)
|
|
42
|
+
from forgecli.engine.execution import (
|
|
43
|
+
EngineResult,
|
|
44
|
+
ExecutionEngine,
|
|
45
|
+
PipelineBuilder,
|
|
46
|
+
Stage,
|
|
47
|
+
StageContext,
|
|
48
|
+
StageRegistry,
|
|
49
|
+
StageResult,
|
|
50
|
+
StageStatus,
|
|
51
|
+
)
|
|
52
|
+
from forgecli.engine.plugins import (
|
|
53
|
+
EnginePluginFactory,
|
|
54
|
+
HookManager,
|
|
55
|
+
PluginHook,
|
|
56
|
+
register_plugin,
|
|
57
|
+
stage_as_plugin,
|
|
58
|
+
)
|
|
59
|
+
from forgecli.engine.stages import (
|
|
60
|
+
ContextOptimizerStage,
|
|
61
|
+
ExecutionEngineStage,
|
|
62
|
+
GitEngineStage,
|
|
63
|
+
IntentAnalyzerStage,
|
|
64
|
+
ModelRouterStage,
|
|
65
|
+
PlanningEngineStage,
|
|
66
|
+
RepositoryAnalyzerStage,
|
|
67
|
+
ValidationEngineStage,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
"ContextOptimizerStage",
|
|
72
|
+
"EngineCancelledError",
|
|
73
|
+
"EngineContext",
|
|
74
|
+
"EngineEvent",
|
|
75
|
+
"EnginePluginFactory",
|
|
76
|
+
"EngineResult",
|
|
77
|
+
"EventBus",
|
|
78
|
+
"ExecutionEngine",
|
|
79
|
+
"ExecutionEngineStage",
|
|
80
|
+
"GitEngineStage",
|
|
81
|
+
"HookManager",
|
|
82
|
+
"IntentAnalysis",
|
|
83
|
+
"IntentAnalyzerStage",
|
|
84
|
+
"LogLevel",
|
|
85
|
+
"ModelRouterStage",
|
|
86
|
+
"ModelSelection",
|
|
87
|
+
"PipelineBuilder",
|
|
88
|
+
"PlanningEngineStage",
|
|
89
|
+
"PluginHook",
|
|
90
|
+
"ProgressEvent",
|
|
91
|
+
"RepositoryAnalyzerStage",
|
|
92
|
+
"RetrievalResult",
|
|
93
|
+
"Stage",
|
|
94
|
+
"StageContext",
|
|
95
|
+
"StageEvent",
|
|
96
|
+
"StageLog",
|
|
97
|
+
"StageRegistry",
|
|
98
|
+
"StageResult",
|
|
99
|
+
"StageStatus",
|
|
100
|
+
"TextLogEvent",
|
|
101
|
+
"ValidationEngineStage",
|
|
102
|
+
"default_registry",
|
|
103
|
+
"register_plugin",
|
|
104
|
+
"stage_as_plugin",
|
|
105
|
+
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Typed context that flows through the engine's eight stages.
|
|
2
|
+
|
|
3
|
+
Each stage reads from the context, mutates one or two fields, and
|
|
4
|
+
returns a :class:`~forgecli.engine.execution.StageResult`. The
|
|
5
|
+
context is the *only* mechanism stages use to share data — there
|
|
6
|
+
are no hidden globals.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from forgecli.plugins import Intent
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Per-stage payloads
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class IntentAnalysis:
|
|
27
|
+
"""The output of the Intent Analyzer stage."""
|
|
28
|
+
|
|
29
|
+
intent: Intent
|
|
30
|
+
confidence: float
|
|
31
|
+
rationale: tuple[str, ...] = ()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class RetrievalResult:
|
|
36
|
+
"""The output of the Repository Analyzer stage."""
|
|
37
|
+
|
|
38
|
+
query: str
|
|
39
|
+
matched_nodes: tuple[Mapping[str, Any], ...] = ()
|
|
40
|
+
context_text: str = ""
|
|
41
|
+
artifacts: dict[str, Path] = field(default_factory=dict)
|
|
42
|
+
notes: tuple[str, ...] = ()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class ModelSelection:
|
|
47
|
+
"""The output of the Model Router stage."""
|
|
48
|
+
|
|
49
|
+
provider: str
|
|
50
|
+
model: str
|
|
51
|
+
mode: str = "explicit" # "explicit" | "cheapest" | "fallback"
|
|
52
|
+
cost_in: float = 0.0
|
|
53
|
+
cost_out: float = 0.0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class StageLog:
|
|
58
|
+
"""A single record of how a stage performed.
|
|
59
|
+
|
|
60
|
+
The engine accumulates these on :class:`EngineContext` so
|
|
61
|
+
callers can render a final report.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
stage: str
|
|
65
|
+
status: str
|
|
66
|
+
started_at: float
|
|
67
|
+
finished_at: float | None = None
|
|
68
|
+
notes: tuple[str, ...] = ()
|
|
69
|
+
error: str | None = None
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def duration_seconds(self) -> float:
|
|
73
|
+
if self.finished_at is None:
|
|
74
|
+
return 0.0
|
|
75
|
+
return max(0.0, self.finished_at - self.started_at)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# Engine context
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class EngineContext:
|
|
85
|
+
"""Mutable state shared by every stage of the engine.
|
|
86
|
+
|
|
87
|
+
The context is constructed by :class:`EngineBuilder` and
|
|
88
|
+
passed to each :class:`Stage`. Stages may set the
|
|
89
|
+
``intent_analysis`` / ``retrieval`` / ``model_selection`` /
|
|
90
|
+
``plan`` / ``response`` / ``diff_text`` / ``applied_files`` /
|
|
91
|
+
``test_*`` fields; downstream stages read them.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
prompt: str
|
|
95
|
+
cwd: Path
|
|
96
|
+
run_id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
|
|
97
|
+
started_at: float = field(default_factory=time.time)
|
|
98
|
+
|
|
99
|
+
# Stage 1: Intent
|
|
100
|
+
intent_analysis: IntentAnalysis | None = None
|
|
101
|
+
# Stage 2: Retrieval
|
|
102
|
+
retrieval: RetrievalResult | None = None
|
|
103
|
+
# Stage 3: Caveman optimization (runs before context optimization)
|
|
104
|
+
caveman_optimized_request: Any = None
|
|
105
|
+
caveman_optimized_notes: tuple[str, ...] = ()
|
|
106
|
+
# Stage 4: Context optimization
|
|
107
|
+
optimized_request: Any = None
|
|
108
|
+
optimized_notes: tuple[str, ...] = ()
|
|
109
|
+
# Stage 5: Plan
|
|
110
|
+
plan: Any = None # SoftwarePlan; kept loose to avoid an import cycle
|
|
111
|
+
# Stage 5: Model
|
|
112
|
+
model_selection: ModelSelection | None = None
|
|
113
|
+
# Stage 6: Execution
|
|
114
|
+
response: Any = None
|
|
115
|
+
# Stage 7: Validation
|
|
116
|
+
diff_text: str = ""
|
|
117
|
+
applied_files: list[Path] = field(default_factory=list)
|
|
118
|
+
test_stdout: str = ""
|
|
119
|
+
test_stderr: str = ""
|
|
120
|
+
test_returncode: int | None = None
|
|
121
|
+
fix_attempts: int = 0
|
|
122
|
+
# Stage 8: Git
|
|
123
|
+
staged: bool = False
|
|
124
|
+
pushed: bool = False
|
|
125
|
+
|
|
126
|
+
# Free-form extras for plugins / advanced stages.
|
|
127
|
+
extras: dict[str, Any] = field(default_factory=dict)
|
|
128
|
+
|
|
129
|
+
# Per-stage log (append-only).
|
|
130
|
+
log: list[StageLog] = field(default_factory=list)
|
|
131
|
+
|
|
132
|
+
def to_log_dict(self) -> dict[str, Any]:
|
|
133
|
+
"""Return a JSON-serializable snapshot of the context (no Paths)."""
|
|
134
|
+
return {
|
|
135
|
+
"run_id": self.run_id,
|
|
136
|
+
"started_at": self.started_at,
|
|
137
|
+
"intent": self.intent_analysis.intent.value if self.intent_analysis else None,
|
|
138
|
+
"intent_confidence": self.intent_analysis.confidence if self.intent_analysis else 0.0,
|
|
139
|
+
"retrieval_query": self.retrieval.query if self.retrieval else None,
|
|
140
|
+
"retrieval_match_count": len(self.retrieval.matched_nodes) if self.retrieval else 0,
|
|
141
|
+
"caveman_optimized_notes": list(self.caveman_optimized_notes),
|
|
142
|
+
"optimized_notes": list(self.optimized_notes),
|
|
143
|
+
"has_plan": self.plan is not None,
|
|
144
|
+
"model": self.model_selection.model if self.model_selection else None,
|
|
145
|
+
"provider": self.model_selection.provider if self.model_selection else None,
|
|
146
|
+
"response_present": self.response is not None,
|
|
147
|
+
"diff_length": len(self.diff_text),
|
|
148
|
+
"applied_files": [str(p) for p in self.applied_files],
|
|
149
|
+
"test_returncode": self.test_returncode,
|
|
150
|
+
"fix_attempts": self.fix_attempts,
|
|
151
|
+
"staged": self.staged,
|
|
152
|
+
"pushed": self.pushed,
|
|
153
|
+
"stage_count": len(self.log),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
__all__ = [
|
|
158
|
+
"EngineContext",
|
|
159
|
+
"IntentAnalysis",
|
|
160
|
+
"ModelSelection",
|
|
161
|
+
"RetrievalResult",
|
|
162
|
+
"StageLog",
|
|
163
|
+
]
|