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.
Files changed (156) 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 +137 -0
  19. forgecli/cli/commands_wrappers.py +63 -0
  20. forgecli/cli/main.py +122 -0
  21. forgecli/cli/ui.py +56 -0
  22. forgecli/config/__init__.py +28 -0
  23. forgecli/config/loader.py +85 -0
  24. forgecli/config/settings.py +204 -0
  25. forgecli/config/writer.py +90 -0
  26. forgecli/core/__init__.py +35 -0
  27. forgecli/core/container.py +68 -0
  28. forgecli/core/context.py +54 -0
  29. forgecli/core/credentials.py +114 -0
  30. forgecli/core/errors.py +27 -0
  31. forgecli/core/events.py +67 -0
  32. forgecli/core/logging.py +47 -0
  33. forgecli/core/models.py +159 -0
  34. forgecli/core/plugins.py +56 -0
  35. forgecli/core/service.py +22 -0
  36. forgecli/docs/__init__.py +5 -0
  37. forgecli/docs/generator.py +119 -0
  38. forgecli/engine/__init__.py +105 -0
  39. forgecli/engine/context.py +171 -0
  40. forgecli/engine/defaults.py +89 -0
  41. forgecli/engine/events.py +185 -0
  42. forgecli/engine/execution.py +526 -0
  43. forgecli/engine/plugins.py +146 -0
  44. forgecli/engine/runner.py +155 -0
  45. forgecli/engine/stages/__init__.py +33 -0
  46. forgecli/engine/stages/caveman_optimizer.py +47 -0
  47. forgecli/engine/stages/context_optimizer.py +44 -0
  48. forgecli/engine/stages/execution_engine_stage.py +108 -0
  49. forgecli/engine/stages/git_engine.py +30 -0
  50. forgecli/engine/stages/intent_analyzer.py +38 -0
  51. forgecli/engine/stages/model_router.py +66 -0
  52. forgecli/engine/stages/planning_engine.py +45 -0
  53. forgecli/engine/stages/repository_analyzer.py +54 -0
  54. forgecli/engine/stages/validation_engine.py +89 -0
  55. forgecli/git/__init__.py +9 -0
  56. forgecli/git/repo.py +55 -0
  57. forgecli/git/service.py +45 -0
  58. forgecli/graph/__init__.py +56 -0
  59. forgecli/graph/backend_graphify.py +453 -0
  60. forgecli/graph/edge.py +19 -0
  61. forgecli/graph/graph.py +85 -0
  62. forgecli/graph/graphify.py +412 -0
  63. forgecli/graph/indexer.py +82 -0
  64. forgecli/graph/node.py +47 -0
  65. forgecli/graph/repository.py +164 -0
  66. forgecli/memory/__init__.py +12 -0
  67. forgecli/memory/cache.py +61 -0
  68. forgecli/memory/history.py +138 -0
  69. forgecli/memory/store.py +87 -0
  70. forgecli/optimizer/__init__.py +14 -0
  71. forgecli/optimizer/caveman/__init__.py +173 -0
  72. forgecli/optimizer/caveman/cli.py +64 -0
  73. forgecli/optimizer/caveman/decorator.py +67 -0
  74. forgecli/optimizer/caveman/factory.py +51 -0
  75. forgecli/optimizer/caveman/ruleset.py +156 -0
  76. forgecli/optimizer/caveman/state.py +52 -0
  77. forgecli/optimizer/chunker.py +85 -0
  78. forgecli/optimizer/optimizer.py +81 -0
  79. forgecli/optimizer/ponytail/__init__.py +183 -0
  80. forgecli/optimizer/ponytail/cli.py +168 -0
  81. forgecli/optimizer/ponytail/decorator.py +70 -0
  82. forgecli/optimizer/ponytail/factory.py +51 -0
  83. forgecli/optimizer/ponytail/ruleset.py +168 -0
  84. forgecli/optimizer/ponytail/state.py +53 -0
  85. forgecli/optimizer/ranker.py +37 -0
  86. forgecli/optimizer/summarizer.py +44 -0
  87. forgecli/orchestrator/__init__.py +707 -0
  88. forgecli/planner/__init__.py +56 -0
  89. forgecli/planner/agent.py +61 -0
  90. forgecli/planner/plan.py +65 -0
  91. forgecli/planner/planner.py +17 -0
  92. forgecli/planner/render.py +271 -0
  93. forgecli/planner/serialize.py +106 -0
  94. forgecli/planner/software.py +834 -0
  95. forgecli/platform/__init__.py +91 -0
  96. forgecli/platform/core.py +201 -0
  97. forgecli/platform/deps.py +354 -0
  98. forgecli/platform/paths.py +236 -0
  99. forgecli/platform/shell.py +176 -0
  100. forgecli/platform/update.py +252 -0
  101. forgecli/plugins/__init__.py +251 -0
  102. forgecli/prompts/__init__.py +11 -0
  103. forgecli/prompts/loader.py +28 -0
  104. forgecli/prompts/registry.py +32 -0
  105. forgecli/prompts/renderer.py +23 -0
  106. forgecli/providers/__init__.py +39 -0
  107. forgecli/providers/anthropic.py +206 -0
  108. forgecli/providers/base.py +206 -0
  109. forgecli/providers/builtin.py +26 -0
  110. forgecli/providers/conversation.py +78 -0
  111. forgecli/providers/google.py +295 -0
  112. forgecli/providers/http_base.py +211 -0
  113. forgecli/providers/mock.py +113 -0
  114. forgecli/providers/openai.py +202 -0
  115. forgecli/providers/openai_compatible.py +728 -0
  116. forgecli/providers/router.py +345 -0
  117. forgecli/providers/router_state.py +89 -0
  118. forgecli/review/__init__.py +45 -0
  119. forgecli/review/analyzer.py +124 -0
  120. forgecli/review/analyzers/__init__.py +1 -0
  121. forgecli/review/analyzers/architecture.py +258 -0
  122. forgecli/review/analyzers/complexity.py +167 -0
  123. forgecli/review/analyzers/dead_code.py +262 -0
  124. forgecli/review/analyzers/duplicates.py +163 -0
  125. forgecli/review/analyzers/performance.py +165 -0
  126. forgecli/review/analyzers/security.py +251 -0
  127. forgecli/review/finding.py +68 -0
  128. forgecli/review/report.py +311 -0
  129. forgecli/review/repository.py +131 -0
  130. forgecli/review/suggestions.py +98 -0
  131. forgecli/runtime/__init__.py +6 -0
  132. forgecli/runtime/cache_store.py +77 -0
  133. forgecli/runtime/prepare.py +199 -0
  134. forgecli/runtime/wrappers.py +152 -0
  135. forgecli/sdk/__init__.py +132 -0
  136. forgecli/sdk/events.py +206 -0
  137. forgecli/sdk/interfaces.py +282 -0
  138. forgecli/sdk/loader.py +243 -0
  139. forgecli/sdk/manager.py +693 -0
  140. forgecli/sdk/manifest.py +397 -0
  141. forgecli/sdk/sandbox.py +197 -0
  142. forgecli/sdk/version.py +316 -0
  143. forgecli/templates/__init__.py +9 -0
  144. forgecli/templates/engine.py +37 -0
  145. forgecli/templates/registry.py +32 -0
  146. forgecli/utils/__init__.py +19 -0
  147. forgecli/utils/fs.py +91 -0
  148. forgecli/utils/ids.py +11 -0
  149. forgecli/utils/io.py +27 -0
  150. forgecli/utils/paths.py +70 -0
  151. forgecli/utils/timing.py +25 -0
  152. forgeoptimizer-1.0.0.dist-info/METADATA +169 -0
  153. forgeoptimizer-1.0.0.dist-info/RECORD +156 -0
  154. forgeoptimizer-1.0.0.dist-info/WHEEL +4 -0
  155. forgeoptimizer-1.0.0.dist-info/entry_points.txt +2 -0
  156. forgeoptimizer-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,27 @@
1
+ """Typed exception hierarchy for ForgeCLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class ForgeCLIError(Exception):
7
+ """Base class for all ForgeCLI-specific errors."""
8
+
9
+
10
+ class ConfigError(ForgeCLIError):
11
+ """Raised when configuration cannot be loaded or validated."""
12
+
13
+
14
+ class ProviderError(ForgeCLIError):
15
+ """Raised on AI provider failures (network, auth, schema)."""
16
+
17
+
18
+ class GitError(ForgeCLIError):
19
+ """Raised on Git-related failures (missing repo, bad ref, etc)."""
20
+
21
+
22
+ class PluginError(ForgeCLIError):
23
+ """Raised on plugin discovery or lifecycle failures."""
24
+
25
+
26
+ class PipelineError(ForgeCLIError):
27
+ """Raised when a builder/review/planner pipeline cannot complete."""
@@ -0,0 +1,67 @@
1
+ """A tiny pub/sub event bus used for decoupled logging and lifecycle hooks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import contextlib
7
+ from collections.abc import Awaitable, Callable
8
+ from dataclasses import dataclass, field
9
+ from datetime import UTC, datetime
10
+ from typing import Any
11
+
12
+ Subscriber = Callable[["Event"], "None | Awaitable[None]"]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Event:
17
+ """An immutable event payload."""
18
+
19
+ name: str
20
+ payload: dict[str, Any] = field(default_factory=dict)
21
+ timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
22
+
23
+
24
+ class EventBus:
25
+ """Asynchronous-first pub/sub bus.
26
+
27
+ Synchronous subscribers are invoked inline; async subscribers are
28
+ scheduled via :func:`asyncio.create_task`. The bus is intentionally
29
+ simple: subscribers cannot block the publisher and ordering is
30
+ best-effort.
31
+ """
32
+
33
+ def __init__(self) -> None:
34
+ self._subscribers: dict[str, list[Subscriber]] = {}
35
+ self._pending_tasks: set[asyncio.Task[Any]] = set()
36
+
37
+ def subscribe(self, name: str, callback: Subscriber) -> None:
38
+ """Register ``callback`` for events named ``name``."""
39
+ self._subscribers.setdefault(name, []).append(callback)
40
+
41
+ def unsubscribe(self, name: str, callback: Subscriber) -> None:
42
+ bucket = self._subscribers.get(name)
43
+ if not bucket:
44
+ return
45
+ with contextlib.suppress(ValueError):
46
+ bucket.remove(callback)
47
+
48
+ async def publish(self, name: str, **payload: Any) -> None:
49
+ """Publish an event, dispatching to all registered subscribers."""
50
+ event = Event(name=name, payload=dict(payload))
51
+ for callback in list(self._subscribers.get(name, ())):
52
+ result = callback(event)
53
+ if asyncio.iscoroutine(result):
54
+ await result
55
+
56
+ def emit(self, name: str, **payload: Any) -> None:
57
+ """Synchronous variant of :meth:`publish`; async subs are not awaited."""
58
+ event = Event(name=name, payload=dict(payload))
59
+ for callback in list(self._subscribers.get(name, ())):
60
+ result = callback(event)
61
+ if asyncio.iscoroutine(result):
62
+ # Fire-and-forget; require an event loop to be running.
63
+ with contextlib.suppress(RuntimeError):
64
+ loop = asyncio.get_running_loop()
65
+ task = loop.create_task(result)
66
+ self._pending_tasks.add(task)
67
+ task.add_done_callback(self._pending_tasks.discard)
@@ -0,0 +1,47 @@
1
+ """Logging configuration built on the standard library ``logging`` package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sys
7
+ from typing import Final
8
+
9
+ _LOG_FORMAT: Final[str] = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
10
+ _configured = False
11
+
12
+
13
+ def configure_logging(level: str = "INFO") -> None:
14
+ """Configure root logging once for the lifetime of the process."""
15
+ global _configured
16
+ if _configured:
17
+ new_level = _coerce_level(level)
18
+ if logging.getLogger().level != logging.DEBUG or new_level == logging.DEBUG:
19
+ logging.getLogger().setLevel(new_level)
20
+ return
21
+
22
+ handler = logging.StreamHandler(stream=sys.stderr)
23
+ handler.setFormatter(logging.Formatter(_LOG_FORMAT))
24
+
25
+ root = logging.getLogger()
26
+ root.handlers.clear()
27
+ root.addHandler(handler)
28
+ root.setLevel(_coerce_level(level))
29
+
30
+ # Quiet noisy third-party loggers.
31
+ for noisy in ("httpx", "httpcore", "git", "urllib3"):
32
+ logging.getLogger(noisy).setLevel(logging.WARNING)
33
+
34
+ _configured = True
35
+
36
+
37
+ def get_logger(name: str) -> logging.Logger:
38
+ """Return a configured logger, configuring root logging lazily."""
39
+ if not _configured:
40
+ configure_logging()
41
+ return logging.getLogger(name)
42
+
43
+
44
+ def _coerce_level(level: str) -> int:
45
+ if level.isdigit():
46
+ return int(level)
47
+ return logging.getLevelName(level.upper())
@@ -0,0 +1,159 @@
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
+ # ---------------------------------------------------------------------------
37
+ # Anthropic
38
+ # ---------------------------------------------------------------------------
39
+ ModelDef(id="claude-opus-4.8", display_name="Claude Opus 4.8", tier="latest", provider="anthropic"),
40
+ ModelDef(id="claude-opus-4.6", display_name="Claude Opus 4.6", tier="latest", provider="anthropic"),
41
+ ModelDef(id="claude-sonnet-4.6", display_name="Claude Sonnet 4.6", tier="recommended", provider="anthropic"),
42
+ ModelDef(id="claude-sonnet-4.5", display_name="Claude Sonnet 4.5", tier="legacy", provider="anthropic"),
43
+ ModelDef(id="claude-haiku-4.5", display_name="Claude Haiku 4.5", tier="legacy", provider="anthropic"),
44
+ ModelDef(id="claude-3-5-sonnet-latest", display_name="Claude 3.5 Sonnet", tier="legacy", provider="anthropic"),
45
+ ModelDef(id="claude-3-5-haiku-latest", display_name="Claude 3.5 Haiku", tier="legacy", provider="anthropic"),
46
+ ModelDef(id="claude-3-opus-latest", display_name="Claude 3 Opus", tier="legacy", provider="anthropic"),
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Google Gemini
50
+ # ---------------------------------------------------------------------------
51
+ ModelDef(id="gemini-2.5-pro", display_name="Gemini 2.5 Pro", tier="recommended", provider="google"),
52
+ ModelDef(id="gemini-2.5-flash", display_name="Gemini 2.5 Flash", tier="recommended", provider="google"),
53
+ ModelDef(id="gemini-2.5-flash-lite", display_name="Gemini 2.5 Flash Lite", tier="normal", provider="google"),
54
+ ModelDef(id="gemini-2.0-flash", display_name="Gemini 2.0 Flash", tier="legacy", provider="google"),
55
+ ModelDef(id="gemini-1.5-pro", display_name="Gemini 1.5 Pro", tier="legacy", provider="google"),
56
+ ModelDef(id="gemini-1.5-flash", display_name="Gemini 1.5 Flash", tier="legacy", provider="google"),
57
+ ModelDef(id="gemini-2.0-flash-exp", display_name="Gemini 2.0 Flash Exp", tier="legacy", provider="google"),
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # OpenRouter
61
+ # ---------------------------------------------------------------------------
62
+ ModelDef(id="glm-5.2", display_name="GLM 5.2", tier="normal", provider="openrouter"),
63
+ ModelDef(id="deepseek-v3", display_name="DeepSeek V3", tier="normal", provider="openrouter"),
64
+ ModelDef(id="deepseek-r1", display_name="DeepSeek R1", tier="normal", provider="openrouter"),
65
+ ModelDef(id="qwen3-coder", display_name="Qwen3 Coder", tier="normal", provider="openrouter"),
66
+ ModelDef(id="qwen3-32b", display_name="Qwen3 32B", tier="normal", provider="openrouter"),
67
+ ModelDef(id="kimi-k2", display_name="Kimi K2", tier="normal", provider="openrouter"),
68
+ ModelDef(id="llama-4-maverick", display_name="Llama 4 Maverick", tier="normal", provider="openrouter"),
69
+ ModelDef(id="llama-4-scout", display_name="Llama 4 Scout", tier="normal", provider="openrouter"),
70
+ ModelDef(id="llama-3.3-70b", display_name="Llama 3.3 70B", tier="normal", provider="openrouter"),
71
+ ModelDef(id="gemma-3", display_name="Gemma 3", tier="normal", provider="openrouter"),
72
+ ModelDef(id="devstral", display_name="Devstral", tier="normal", provider="openrouter"),
73
+ ModelDef(id="codestral", display_name="Codestral", tier="normal", provider="openrouter"),
74
+ ModelDef(id="phi-4", display_name="Phi-4", tier="normal", provider="openrouter"),
75
+ ModelDef(id="mistral-large", display_name="Mistral Large", tier="normal", provider="openrouter"),
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Groq
79
+ # ---------------------------------------------------------------------------
80
+ ModelDef(id="llama-4-scout", display_name="Llama 4 Scout", tier="normal", provider="groq"),
81
+ ModelDef(id="deepseek-r1", display_name="DeepSeek R1", tier="normal", provider="groq"),
82
+ ModelDef(id="qwen3-32b", display_name="Qwen3 32B", tier="normal", provider="groq"),
83
+ ModelDef(id="gemma-3", display_name="Gemma 3", tier="normal", provider="groq"),
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Mistral
87
+ # ---------------------------------------------------------------------------
88
+ ModelDef(id="mistral-large", display_name="Mistral Large", tier="normal", provider="mistral"),
89
+ ModelDef(id="magistral", display_name="Magistral", tier="normal", provider="mistral"),
90
+ ModelDef(id="mistral-small", display_name="Mistral Small", tier="normal", provider="mistral"),
91
+ ModelDef(id="codestral", display_name="Codestral", tier="normal", provider="mistral"),
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # MiniMax
95
+ # ---------------------------------------------------------------------------
96
+ ModelDef(id="abab6.5g-chat", display_name="Abab 6.5G Chat", tier="recommended", provider="minimax"),
97
+ ModelDef(id="abab6.5-chat", display_name="Abab 6.5 Chat", tier="legacy", provider="minimax"),
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # xAI (Grok)
101
+ # ---------------------------------------------------------------------------
102
+ ModelDef(id="grok-2", display_name="Grok 2", tier="recommended", provider="xai"),
103
+ ModelDef(id="grok-beta", display_name="Grok Beta", tier="latest", provider="xai"),
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Together AI
107
+ # ---------------------------------------------------------------------------
108
+ ModelDef(id="llama-3.1-70b", display_name="Llama 3.1 70B", tier="recommended", provider="together"),
109
+ ModelDef(id="llama-3.1-405b", display_name="Llama 3.1 405B", tier="latest", provider="together"),
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Fireworks AI
113
+ # ---------------------------------------------------------------------------
114
+ ModelDef(id="llama-3.1-70b", display_name="Llama 3.1 70B", tier="recommended", provider="fireworks"),
115
+ ModelDef(id="llama-3.1-405b", display_name="Llama 3.1 405B", tier="latest", provider="fireworks"),
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # Cohere
119
+ # ---------------------------------------------------------------------------
120
+ ModelDef(id="command-r-plus", display_name="Command R+", tier="recommended", provider="cohere"),
121
+ ModelDef(id="command-r", display_name="Command R", tier="normal", provider="cohere"),
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # NVIDIA NIM
125
+ # ---------------------------------------------------------------------------
126
+ ModelDef(id="llama-3.1-70b", display_name="Llama 3.1 70B", tier="recommended", provider="nvidia"),
127
+ ModelDef(id="llama-3.1-405b", display_name="Llama 3.1 405B", tier="latest", provider="nvidia"),
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # Ollama
131
+ # ---------------------------------------------------------------------------
132
+ ModelDef(id="llama3", display_name="Llama 3", tier="normal", provider="ollama"),
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # LM Studio
136
+ # ---------------------------------------------------------------------------
137
+ ModelDef(id="local-model", display_name="Local Model", tier="normal", provider="lmstudio"),
138
+
139
+ # ---------------------------------------------------------------------------
140
+ # vLLM
141
+ # ---------------------------------------------------------------------------
142
+ ModelDef(id="local-model", display_name="Local Model", tier="normal", provider="vllm"),
143
+ ]
144
+
145
+
146
+ def get_model_def(model_id: str, provider: str | None = None) -> ModelDef | None:
147
+ """Retrieve the model definition by model ID, optionally filtering by provider."""
148
+ model_id_lower = model_id.lower().strip()
149
+ provider_lower = provider.lower().strip() if provider else None
150
+ for m in MODEL_CATALOG:
151
+ if m.id == model_id_lower and (provider_lower is None or m.provider == provider_lower):
152
+ return m
153
+ return None
154
+
155
+
156
+ def get_display_name(model_id: str, provider: str | None = None) -> str:
157
+ """Get the friendly display name for a model, defaulting to the ID itself."""
158
+ m = get_model_def(model_id, provider)
159
+ return m.display_name if m else model_id
@@ -0,0 +1,56 @@
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(
44
+ f"Plugin {ep.name!r} must subclass forgecli.plugins.Plugin"
45
+ )
46
+ plugins.append(plugin)
47
+ return plugins
48
+
49
+
50
+ def install_plugins(context: AppContext, plugins: list[Plugin]) -> None:
51
+ """Configure each plugin against ``context``."""
52
+ for plugin in plugins:
53
+ plugin.configure(context)
54
+
55
+
56
+ __all__ = ["Any", "Plugin", "discover_plugins", "install_plugins"]
@@ -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,5 @@
1
+ """Auto-generated project documentation."""
2
+
3
+ from forgecli.docs.generator import generate_docs
4
+
5
+ __all__ = ["generate_docs"]
@@ -0,0 +1,119 @@
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 = (
57
+ f"`{node['path']}:{node['line']}`"
58
+ if node["line"]
59
+ else f"`{node['path']}`"
60
+ )
61
+ lines.append(f"- {node['label']} — {location}")
62
+ lines.append("")
63
+ if not nodes:
64
+ lines.append("_No indexed symbols found._\n")
65
+ target.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
66
+ _ = snapshot # silence unused
67
+ return target
68
+
69
+
70
+ def _walk_nodes(root: Path) -> list[dict[str, object]]:
71
+ """Build a small node list from the project tree.
72
+
73
+ This is a *fallback* when Graphify hasn't been run. The docs
74
+ generator is meant to be cheap and offline; it doesn't shell out
75
+ to the Graphify CLI.
76
+ """
77
+ nodes: list[dict[str, object]] = []
78
+ for path in sorted(root.rglob("*.py")):
79
+ if any(part.startswith(".") for part in path.parts):
80
+ continue
81
+ if any(part in {"__pycache__", "node_modules", ".venv", "venv"} for part in path.parts):
82
+ continue
83
+ rel = path.relative_to(root)
84
+ try:
85
+ text = path.read_text(encoding="utf-8", errors="replace")
86
+ except OSError:
87
+ continue
88
+ nodes.append({"path": str(rel), "label": path.name, "line": 1})
89
+ for index, line in enumerate(text.splitlines(), start=1):
90
+ stripped = line.lstrip()
91
+ if stripped.startswith(("def ", "class ", "async def ")):
92
+ name = _extract_symbol_name(stripped)
93
+ if name:
94
+ nodes.append({"path": str(rel), "label": name, "line": index})
95
+ return nodes
96
+
97
+
98
+ def _extract_symbol_name(line: str) -> str | None:
99
+ """Return the symbol name from a ``def foo(...)`` / ``class Foo:`` line."""
100
+ line = line.strip()
101
+ for prefix in ("async def ", "def ", "class "):
102
+ if line.startswith(prefix):
103
+ rest = line[len(prefix):]
104
+ return rest.split("(", 1)[0].split(":", 1)[0].strip() or None
105
+ return None
106
+
107
+
108
+ def _community_buckets(nodes: list[dict[str, object]]) -> dict[str, list[dict[str, object]]]:
109
+ """Group nodes by their top-level directory (the "module")."""
110
+ buckets: dict[str, list[dict[str, object]]] = {}
111
+ for node in nodes:
112
+ path = str(node["path"])
113
+ parts = path.split("/")
114
+ module = parts[0] if len(parts) > 1 else path
115
+ buckets.setdefault(module, []).append(node)
116
+ return buckets
117
+
118
+
119
+ __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
+ ]