atomadic-forge 0.3.2__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.
- atomadic_forge/__init__.py +12 -0
- atomadic_forge/__main__.py +5 -0
- atomadic_forge/a0_qk_constants/__init__.py +1 -0
- atomadic_forge/a0_qk_constants/agent_plan_schema.py +120 -0
- atomadic_forge/a0_qk_constants/commandsmith_types.py +49 -0
- atomadic_forge/a0_qk_constants/config_defaults.py +38 -0
- atomadic_forge/a0_qk_constants/emergent_types.py +77 -0
- atomadic_forge/a0_qk_constants/error_codes.py +296 -0
- atomadic_forge/a0_qk_constants/forge_types.py +89 -0
- atomadic_forge/a0_qk_constants/gen_language.py +116 -0
- atomadic_forge/a0_qk_constants/lang_extensions.py +150 -0
- atomadic_forge/a0_qk_constants/policy_schema.py +48 -0
- atomadic_forge/a0_qk_constants/receipt_schema.py +311 -0
- atomadic_forge/a0_qk_constants/roi_constants.py +96 -0
- atomadic_forge/a0_qk_constants/semantic_types.py +61 -0
- atomadic_forge/a0_qk_constants/sidecar_schema.py +81 -0
- atomadic_forge/a0_qk_constants/synergy_types.py +62 -0
- atomadic_forge/a0_qk_constants/tier_names.py +47 -0
- atomadic_forge/a1_at_functions/__init__.py +1 -0
- atomadic_forge/a1_at_functions/agent_context_pack.py +193 -0
- atomadic_forge/a1_at_functions/agent_memory.py +139 -0
- atomadic_forge/a1_at_functions/agent_plan_emitter.py +324 -0
- atomadic_forge/a1_at_functions/agent_summary.py +277 -0
- atomadic_forge/a1_at_functions/body_extractor.py +306 -0
- atomadic_forge/a1_at_functions/card_renderer.py +210 -0
- atomadic_forge/a1_at_functions/certify_checks.py +445 -0
- atomadic_forge/a1_at_functions/chat_context.py +170 -0
- atomadic_forge/a1_at_functions/cherry_pick.py +71 -0
- atomadic_forge/a1_at_functions/classify_tier.py +115 -0
- atomadic_forge/a1_at_functions/commandsmith_discover.py +167 -0
- atomadic_forge/a1_at_functions/commandsmith_render.py +267 -0
- atomadic_forge/a1_at_functions/compiler_feedback.py +94 -0
- atomadic_forge/a1_at_functions/compliance_checker.py +228 -0
- atomadic_forge/a1_at_functions/config_io.py +68 -0
- atomadic_forge/a1_at_functions/cs1_renderer.py +588 -0
- atomadic_forge/a1_at_functions/doc_synthesizer.py +205 -0
- atomadic_forge/a1_at_functions/emergent_compose.py +192 -0
- atomadic_forge/a1_at_functions/emergent_rank.py +116 -0
- atomadic_forge/a1_at_functions/emergent_signature_extract.py +242 -0
- atomadic_forge/a1_at_functions/emergent_synthesize.py +88 -0
- atomadic_forge/a1_at_functions/enforce_planner.py +208 -0
- atomadic_forge/a1_at_functions/error_hints.py +105 -0
- atomadic_forge/a1_at_functions/evolution_log.py +94 -0
- atomadic_forge/a1_at_functions/forge_feedback.py +433 -0
- atomadic_forge/a1_at_functions/generation_quality.py +322 -0
- atomadic_forge/a1_at_functions/import_repair.py +211 -0
- atomadic_forge/a1_at_functions/import_smoke.py +102 -0
- atomadic_forge/a1_at_functions/js_parser.py +539 -0
- atomadic_forge/a1_at_functions/lineage_chain.py +144 -0
- atomadic_forge/a1_at_functions/lineage_reader.py +107 -0
- atomadic_forge/a1_at_functions/llm_client.py +554 -0
- atomadic_forge/a1_at_functions/local_signer.py +134 -0
- atomadic_forge/a1_at_functions/lsp_protocol.py +379 -0
- atomadic_forge/a1_at_functions/manifest_diff.py +314 -0
- atomadic_forge/a1_at_functions/mcp_protocol.py +1066 -0
- atomadic_forge/a1_at_functions/patch_scorer.py +267 -0
- atomadic_forge/a1_at_functions/plan_adapter.py +75 -0
- atomadic_forge/a1_at_functions/policy_loader.py +107 -0
- atomadic_forge/a1_at_functions/preflight_change.py +227 -0
- atomadic_forge/a1_at_functions/progress_reporter.py +81 -0
- atomadic_forge/a1_at_functions/provider_detect.py +157 -0
- atomadic_forge/a1_at_functions/provider_resolver.py +48 -0
- atomadic_forge/a1_at_functions/receipt_emitter.py +291 -0
- atomadic_forge/a1_at_functions/recipes.py +186 -0
- atomadic_forge/a1_at_functions/repo_explainer.py +124 -0
- atomadic_forge/a1_at_functions/roi_calculator.py +265 -0
- atomadic_forge/a1_at_functions/rollback_planner.py +147 -0
- atomadic_forge/a1_at_functions/sbom_emitter.py +155 -0
- atomadic_forge/a1_at_functions/scaffold_js.py +55 -0
- atomadic_forge/a1_at_functions/scaffold_pyproject.py +62 -0
- atomadic_forge/a1_at_functions/scaffold_starter.py +94 -0
- atomadic_forge/a1_at_functions/scout_walk.py +309 -0
- atomadic_forge/a1_at_functions/sidecar_parser.py +161 -0
- atomadic_forge/a1_at_functions/sidecar_validator.py +202 -0
- atomadic_forge/a1_at_functions/stub_detector.py +158 -0
- atomadic_forge/a1_at_functions/synergy_detect.py +166 -0
- atomadic_forge/a1_at_functions/synergy_render.py +252 -0
- atomadic_forge/a1_at_functions/synergy_surface_extract.py +163 -0
- atomadic_forge/a1_at_functions/test_runner.py +196 -0
- atomadic_forge/a1_at_functions/test_selector.py +122 -0
- atomadic_forge/a1_at_functions/tier_init_rebuild.py +122 -0
- atomadic_forge/a1_at_functions/tool_composer.py +130 -0
- atomadic_forge/a1_at_functions/transcript_log.py +70 -0
- atomadic_forge/a1_at_functions/wire_check.py +260 -0
- atomadic_forge/a2_mo_composites/__init__.py +1 -0
- atomadic_forge/a2_mo_composites/lineage_chain_store.py +122 -0
- atomadic_forge/a2_mo_composites/manifest_store.py +46 -0
- atomadic_forge/a2_mo_composites/plan_store.py +164 -0
- atomadic_forge/a2_mo_composites/receipt_signer.py +231 -0
- atomadic_forge/a3_og_features/__init__.py +1 -0
- atomadic_forge/a3_og_features/commandsmith_feature.py +267 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/__init__.py +3 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a0_qk_constants/__init__.py +4 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a1_at_functions/__init__.py +14 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/conftest.py +10 -0
- atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/test_mixed.py +18 -0
- atomadic_forge/a3_og_features/demo_runner.py +502 -0
- atomadic_forge/a3_og_features/emergent_feature.py +95 -0
- atomadic_forge/a3_og_features/emergent_pipeline_integration.py +154 -0
- atomadic_forge/a3_og_features/forge_enforce.py +107 -0
- atomadic_forge/a3_og_features/forge_evolve.py +176 -0
- atomadic_forge/a3_og_features/forge_loop.py +528 -0
- atomadic_forge/a3_og_features/forge_pipeline.py +295 -0
- atomadic_forge/a3_og_features/forge_plan_apply.py +222 -0
- atomadic_forge/a3_og_features/lsp_server.py +98 -0
- atomadic_forge/a3_og_features/mcp_server.py +160 -0
- atomadic_forge/a3_og_features/setup_wizard.py +337 -0
- atomadic_forge/a3_og_features/synergy_feature.py +65 -0
- atomadic_forge/a4_sy_orchestration/__init__.py +1 -0
- atomadic_forge/a4_sy_orchestration/cli.py +1284 -0
- atomadic_forge/commands/__init__.py +1 -0
- atomadic_forge/commands/_registry.py +36 -0
- atomadic_forge/commands/audit.py +142 -0
- atomadic_forge/commands/chat.py +133 -0
- atomadic_forge/commands/commandsmith.py +178 -0
- atomadic_forge/commands/config_cmd.py +145 -0
- atomadic_forge/commands/demo.py +142 -0
- atomadic_forge/commands/emergent.py +124 -0
- atomadic_forge/commands/emergent_then_synergy.py +70 -0
- atomadic_forge/commands/evolve.py +122 -0
- atomadic_forge/commands/evolve_then_iterate.py +70 -0
- atomadic_forge/commands/feature_then_emergent.py +111 -0
- atomadic_forge/commands/iterate.py +140 -0
- atomadic_forge/commands/synergy.py +96 -0
- atomadic_forge/commands/synergy_then_emergent.py +70 -0
- atomadic_forge-0.3.2.dist-info/METADATA +471 -0
- atomadic_forge-0.3.2.dist-info/RECORD +131 -0
- atomadic_forge-0.3.2.dist-info/WHEEL +5 -0
- atomadic_forge-0.3.2.dist-info/entry_points.txt +3 -0
- atomadic_forge-0.3.2.dist-info/licenses/LICENSE +15 -0
- atomadic_forge-0.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Tier a1 — pure tier classification.
|
|
2
|
+
|
|
3
|
+
Word-boundary token matching (the substring-bug-fixed version) plus optional
|
|
4
|
+
body-aware promotion: any class with mutable instance state (``self.<attr>
|
|
5
|
+
= …``) is promoted to ``a2_mo_composites`` regardless of name signals.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
_WORD_RE = re.compile(r"[A-Za-z][A-Za-z0-9]*")
|
|
14
|
+
|
|
15
|
+
_CONTRACT = frozenset({"constant", "const", "schema", "manifest", "token",
|
|
16
|
+
"invariant", "proof"})
|
|
17
|
+
_ATOM = frozenset({"button", "textarea", "input", "validator", "validate",
|
|
18
|
+
"format", "parse", "atom"})
|
|
19
|
+
_COMPOSITE = frozenset({"engine", "service", "client", "manager", "store",
|
|
20
|
+
"hook", "calculator", "molecule"})
|
|
21
|
+
_FEATURE = frozenset({"registry", "gateway", "vault", "module", "feature",
|
|
22
|
+
"workflow", "page", "screen", "organism"})
|
|
23
|
+
_ORCHESTRATION = frozenset({"server", "router", "orchestrator", "bridge",
|
|
24
|
+
"runtime", "system", "main", "mcp", "cli"})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def word_tokens(text: str) -> set[str]:
|
|
28
|
+
"""Tokenize on word boundaries and split camelCase.
|
|
29
|
+
|
|
30
|
+
``"AgenticSwarm"`` → ``{"agentic", "swarm"}``; ``"atomadic-v2"`` →
|
|
31
|
+
``{"atomadic", "v2"}`` — no false-positive on ``"atom"``.
|
|
32
|
+
"""
|
|
33
|
+
raw = _WORD_RE.findall(text)
|
|
34
|
+
out: set[str] = set()
|
|
35
|
+
for word in raw:
|
|
36
|
+
for piece in re.findall(r"[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z0-9]+|[A-Z]+", word):
|
|
37
|
+
if piece:
|
|
38
|
+
out.add(piece.lower())
|
|
39
|
+
out.add(word.lower())
|
|
40
|
+
return out
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def classify_tier(*, name: str, kind: str, path: str = "",
|
|
44
|
+
body_signals: dict[str, Any] | None = None) -> str:
|
|
45
|
+
"""Return the canonical tier directory for a symbol.
|
|
46
|
+
|
|
47
|
+
``body_signals`` (optional) carries ``has_self_assign`` /
|
|
48
|
+
``has_class_attr_collections`` flags from the body extractor. When set,
|
|
49
|
+
a class with mutable instance state is forced to a2.
|
|
50
|
+
"""
|
|
51
|
+
tokens = word_tokens(f"{path} {name} {kind}")
|
|
52
|
+
stateful = bool(body_signals and (
|
|
53
|
+
body_signals.get("has_self_assign")
|
|
54
|
+
or body_signals.get("has_class_attr_collections")
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
if kind in ("class", "type") and stateful:
|
|
58
|
+
if tokens & _ORCHESTRATION:
|
|
59
|
+
return "a4_sy_orchestration"
|
|
60
|
+
if tokens & _FEATURE:
|
|
61
|
+
return "a3_og_features"
|
|
62
|
+
return "a2_mo_composites"
|
|
63
|
+
|
|
64
|
+
if tokens & _CONTRACT:
|
|
65
|
+
return "a0_qk_constants"
|
|
66
|
+
if tokens & _ATOM:
|
|
67
|
+
return "a1_at_functions"
|
|
68
|
+
if tokens & _COMPOSITE:
|
|
69
|
+
return "a2_mo_composites"
|
|
70
|
+
if tokens & _FEATURE:
|
|
71
|
+
return "a3_og_features"
|
|
72
|
+
if tokens & _ORCHESTRATION:
|
|
73
|
+
return "a4_sy_orchestration"
|
|
74
|
+
|
|
75
|
+
if kind == "function":
|
|
76
|
+
return "a1_at_functions"
|
|
77
|
+
if kind in ("class", "type"):
|
|
78
|
+
return "a2_mo_composites"
|
|
79
|
+
return "a1_at_functions"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
_IO_ROOT_MODULES = frozenset({"urllib", "requests", "httpx", "socket",
|
|
83
|
+
"subprocess", "shutil", "os", "sqlite3",
|
|
84
|
+
"boto3", "smtplib"})
|
|
85
|
+
_IO_BUILTINS = frozenset({"open", "print", "input", "connect"})
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def detect_effects(node: Any) -> list[str]:
|
|
89
|
+
"""Cheap effect inference on an ``ast.AST`` node body."""
|
|
90
|
+
import ast
|
|
91
|
+
|
|
92
|
+
effects: list[str] = []
|
|
93
|
+
for child in ast.walk(node):
|
|
94
|
+
if isinstance(child, ast.Call):
|
|
95
|
+
f = child.func
|
|
96
|
+
if isinstance(f, ast.Name) and f.id in _IO_BUILTINS:
|
|
97
|
+
effects.append("io")
|
|
98
|
+
elif isinstance(f, ast.Attribute):
|
|
99
|
+
root = f
|
|
100
|
+
while isinstance(root, ast.Attribute):
|
|
101
|
+
root = root.value
|
|
102
|
+
if isinstance(root, ast.Name) and root.id in _IO_ROOT_MODULES:
|
|
103
|
+
effects.append("io")
|
|
104
|
+
if f.attr in ("write", "send", "post", "put", "delete",
|
|
105
|
+
"execute", "commit"):
|
|
106
|
+
effects.append("state")
|
|
107
|
+
if isinstance(child, ast.Global | ast.Nonlocal):
|
|
108
|
+
effects.append("state")
|
|
109
|
+
if not effects:
|
|
110
|
+
return ["pure"]
|
|
111
|
+
seen: list[str] = []
|
|
112
|
+
for e in effects:
|
|
113
|
+
if e not in seen:
|
|
114
|
+
seen.append(e)
|
|
115
|
+
return seen
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Tier a1 — pure discovery for the Commandsmith CLI registry.
|
|
2
|
+
|
|
3
|
+
Walks a directory tree of would-be command modules and returns a list of
|
|
4
|
+
:class:`RegisteredCommandCard` describing what each module exports.
|
|
5
|
+
|
|
6
|
+
Three recognised surfaces:
|
|
7
|
+
|
|
8
|
+
* **typer_app** — module exposes a top-level ``app: typer.Typer``.
|
|
9
|
+
* **register_fn** — module exposes ``register(app: typer.Typer) -> None``.
|
|
10
|
+
* **wrapped_class** — module exposes a public class plus a class-level
|
|
11
|
+
``COMMAND_NAME`` attribute, indicating it should be wrapped by the
|
|
12
|
+
generator (see ``commandsmith_render``).
|
|
13
|
+
|
|
14
|
+
This module is pure: it only reads files and parses AST. It NEVER imports
|
|
15
|
+
the modules it describes — that lives in tier a2 / a3 to keep a1 import-safe.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import ast
|
|
21
|
+
from collections.abc import Iterable
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from ..a0_qk_constants.commandsmith_types import (
|
|
25
|
+
CommandSignatureCard,
|
|
26
|
+
RegisteredCommandCard,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_PUBLIC = lambda name: not name.startswith("_") # noqa: E731 — short pure helper
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _module_for(path: Path, package_root: Path, package: str) -> str:
|
|
33
|
+
rel = path.relative_to(package_root)
|
|
34
|
+
parts = list(rel.with_suffix("").parts)
|
|
35
|
+
if parts and parts[-1] == "__init__":
|
|
36
|
+
parts.pop()
|
|
37
|
+
return ".".join([package, *parts]) if parts else package
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _docstring_first_line(node: ast.AST) -> str:
|
|
41
|
+
doc = ast.get_docstring(node) or ""
|
|
42
|
+
first = doc.strip().split("\n", 1)[0] if doc else ""
|
|
43
|
+
return first.strip()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _collect_signature(fn: ast.FunctionDef) -> CommandSignatureCard:
|
|
47
|
+
params: list[str] = []
|
|
48
|
+
for arg in fn.args.args:
|
|
49
|
+
ann = ast.unparse(arg.annotation) if arg.annotation else "Any"
|
|
50
|
+
params.append(f"{arg.arg}: {ann}")
|
|
51
|
+
ret = ast.unparse(fn.returns) if fn.returns else "Any"
|
|
52
|
+
return CommandSignatureCard(
|
|
53
|
+
name=fn.name,
|
|
54
|
+
parameters=params,
|
|
55
|
+
return_type=ret,
|
|
56
|
+
docstring=_docstring_first_line(fn),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _detect_surface(tree: ast.Module) -> tuple[str, str, bool, list[ast.AST], str]:
|
|
61
|
+
"""Return (surface, help_text, hidden, sub_command_nodes, command_name)."""
|
|
62
|
+
has_typer_app = False
|
|
63
|
+
has_register_fn = False
|
|
64
|
+
has_command_name_const = False
|
|
65
|
+
command_name = ""
|
|
66
|
+
help_text = _docstring_first_line(tree)
|
|
67
|
+
hidden = False
|
|
68
|
+
classes: list[ast.ClassDef] = []
|
|
69
|
+
register_fn: ast.FunctionDef | None = None
|
|
70
|
+
|
|
71
|
+
for node in tree.body:
|
|
72
|
+
if isinstance(node, ast.Assign):
|
|
73
|
+
for target in node.targets:
|
|
74
|
+
if isinstance(target, ast.Name):
|
|
75
|
+
if target.id == "app":
|
|
76
|
+
has_typer_app = True
|
|
77
|
+
elif target.id == "COMMAND_NAME":
|
|
78
|
+
has_command_name_const = True
|
|
79
|
+
if isinstance(node.value, ast.Constant) and isinstance(
|
|
80
|
+
node.value.value, str
|
|
81
|
+
):
|
|
82
|
+
command_name = node.value.value
|
|
83
|
+
elif target.id == "COMMAND_HELP":
|
|
84
|
+
if isinstance(node.value, ast.Constant) and isinstance(
|
|
85
|
+
node.value.value, str
|
|
86
|
+
):
|
|
87
|
+
help_text = node.value.value
|
|
88
|
+
elif target.id == "COMMAND_HIDDEN":
|
|
89
|
+
if isinstance(node.value, ast.Constant) and isinstance(
|
|
90
|
+
node.value.value, bool
|
|
91
|
+
):
|
|
92
|
+
hidden = node.value.value
|
|
93
|
+
elif isinstance(node, ast.FunctionDef) and node.name == "register":
|
|
94
|
+
has_register_fn = True
|
|
95
|
+
register_fn = node
|
|
96
|
+
elif isinstance(node, ast.ClassDef) and _PUBLIC(node.name):
|
|
97
|
+
classes.append(node)
|
|
98
|
+
|
|
99
|
+
if has_typer_app:
|
|
100
|
+
return ("typer_app", help_text, hidden, [], command_name)
|
|
101
|
+
if has_register_fn:
|
|
102
|
+
nodes: list[ast.AST] = [register_fn] if register_fn else []
|
|
103
|
+
return ("register_fn", help_text, hidden, nodes, command_name)
|
|
104
|
+
if has_command_name_const and classes:
|
|
105
|
+
classes[0]._commandsmith_name = command_name # type: ignore[attr-defined]
|
|
106
|
+
return ("wrapped_class", help_text, hidden, classes, command_name)
|
|
107
|
+
return ("", "", False, [], "")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def discover_command_modules(
|
|
111
|
+
package_root: Path,
|
|
112
|
+
package: str,
|
|
113
|
+
sub_dirs: Iterable[str] = ("commands",),
|
|
114
|
+
source_root: str = "atomadic_forge_seed",
|
|
115
|
+
) -> list[RegisteredCommandCard]:
|
|
116
|
+
"""Scan ``<package_root>/<sub_dir>`` files for command surfaces.
|
|
117
|
+
|
|
118
|
+
Returns a sorted list of :class:`RegisteredCommandCard`.
|
|
119
|
+
Files starting with ``_`` (incl. ``__init__.py``) are ignored.
|
|
120
|
+
"""
|
|
121
|
+
cards: list[RegisteredCommandCard] = []
|
|
122
|
+
for sub in sub_dirs:
|
|
123
|
+
base = package_root / sub
|
|
124
|
+
if not base.exists():
|
|
125
|
+
continue
|
|
126
|
+
for py_file in sorted(base.rglob("*.py")):
|
|
127
|
+
if py_file.name.startswith("_"):
|
|
128
|
+
continue
|
|
129
|
+
try:
|
|
130
|
+
tree = ast.parse(py_file.read_text(encoding="utf-8"))
|
|
131
|
+
except (SyntaxError, OSError):
|
|
132
|
+
continue
|
|
133
|
+
surface, help_text, hidden, sub_nodes, explicit_name = _detect_surface(tree)
|
|
134
|
+
if not surface:
|
|
135
|
+
continue
|
|
136
|
+
sub_cmds: list[CommandSignatureCard] = []
|
|
137
|
+
for node in sub_nodes:
|
|
138
|
+
if isinstance(node, ast.FunctionDef):
|
|
139
|
+
sub_cmds.append(_collect_signature(node))
|
|
140
|
+
elif isinstance(node, ast.ClassDef):
|
|
141
|
+
for body in node.body:
|
|
142
|
+
if isinstance(body, ast.FunctionDef) and _PUBLIC(body.name):
|
|
143
|
+
sub_cmds.append(_collect_signature(body))
|
|
144
|
+
name = py_file.stem
|
|
145
|
+
if surface == "wrapped_class":
|
|
146
|
+
cls = next(
|
|
147
|
+
(c for c in sub_nodes if isinstance(c, ast.ClassDef)), None
|
|
148
|
+
)
|
|
149
|
+
custom = getattr(cls, "_commandsmith_name", "") if cls else ""
|
|
150
|
+
if custom:
|
|
151
|
+
name = custom
|
|
152
|
+
# Explicit COMMAND_NAME wins for any surface (e.g. wrappers strip
|
|
153
|
+
# the trailing ``_cli`` suffix to expose a cleaner verb).
|
|
154
|
+
if explicit_name:
|
|
155
|
+
name = explicit_name
|
|
156
|
+
cards.append(
|
|
157
|
+
RegisteredCommandCard(
|
|
158
|
+
name=name.replace("_", "-"),
|
|
159
|
+
module=_module_for(py_file, package_root, package),
|
|
160
|
+
surface=surface, # type: ignore[arg-type]
|
|
161
|
+
help_text=help_text,
|
|
162
|
+
hidden=hidden,
|
|
163
|
+
sub_commands=sub_cmds,
|
|
164
|
+
source_root=source_root,
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
return sorted(cards, key=lambda c: c["name"])
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Tier a1 — pure renderers for the Commandsmith CLI registry.
|
|
2
|
+
|
|
3
|
+
Three renderers:
|
|
4
|
+
|
|
5
|
+
* :func:`render_registry_module` — emits the ``atomadic_forge.commands._registry``
|
|
6
|
+
Python source that auto-imports each discovered command module and wires
|
|
7
|
+
it onto the supplied Typer app.
|
|
8
|
+
|
|
9
|
+
* :func:`render_command_doc` — emits a Markdown page for a single command
|
|
10
|
+
card.
|
|
11
|
+
|
|
12
|
+
* :func:`render_command_index` — emits ``docs/commands/INDEX.md`` listing
|
|
13
|
+
every registered command.
|
|
14
|
+
|
|
15
|
+
* :func:`render_wrapper_module` — emits a Python source file that wraps a
|
|
16
|
+
bare class (e.g. an assimilated ``CherryPicker``) in a Typer subcommand
|
|
17
|
+
group, so newly assimilated features can be invoked from the CLI without
|
|
18
|
+
hand-coded glue.
|
|
19
|
+
|
|
20
|
+
All four are pure: they take cards and return strings. No I/O.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import Iterable
|
|
26
|
+
|
|
27
|
+
from ..a0_qk_constants.commandsmith_types import (
|
|
28
|
+
CommandSignatureCard,
|
|
29
|
+
RegisteredCommandCard,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_REGISTRY_HEADER = '''"""
|
|
33
|
+
Auto-generated by Commandsmith — do NOT hand-edit.
|
|
34
|
+
Run ``atomadic-forge commandsmith sync`` to refresh.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import typer
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
'''
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def render_registry_module(cards: Iterable[RegisteredCommandCard]) -> str:
|
|
46
|
+
"""Render an importable module that registers every card on a Typer app."""
|
|
47
|
+
cards = list(cards)
|
|
48
|
+
imports: list[str] = []
|
|
49
|
+
body: list[str] = []
|
|
50
|
+
for card in cards:
|
|
51
|
+
mod = card["module"]
|
|
52
|
+
imports.append(f"from {mod} import * # noqa: F401,F403 — exported via {card['surface']}")
|
|
53
|
+
# We import the module by full name as alias so different surfaces work
|
|
54
|
+
# together; collisions are avoided because names are distinct verbs.
|
|
55
|
+
if not cards:
|
|
56
|
+
body.append(" pass # no commands discovered")
|
|
57
|
+
else:
|
|
58
|
+
body.append(" \"\"\"Register every Commandsmith-discovered command on ``app``.\"\"\"")
|
|
59
|
+
for card in cards:
|
|
60
|
+
mod = card["module"]
|
|
61
|
+
verb = card["name"]
|
|
62
|
+
help_text = card["help_text"].replace('"""', "'''")
|
|
63
|
+
hidden = card["hidden"]
|
|
64
|
+
if card["surface"] == "typer_app":
|
|
65
|
+
body.append(
|
|
66
|
+
f" from {mod} import app as _app_{verb.replace('-', '_')}"
|
|
67
|
+
)
|
|
68
|
+
body.append(
|
|
69
|
+
f" app.add_typer(_app_{verb.replace('-', '_')}, name={verb!r}, "
|
|
70
|
+
f"help={help_text!r}, hidden={hidden!r})"
|
|
71
|
+
)
|
|
72
|
+
elif card["surface"] == "register_fn":
|
|
73
|
+
body.append(f" from {mod} import register as _reg_{verb.replace('-', '_')}")
|
|
74
|
+
body.append(
|
|
75
|
+
f" _sub_{verb.replace('-', '_')} = typer.Typer(name={verb!r}, "
|
|
76
|
+
f"help={help_text!r})"
|
|
77
|
+
)
|
|
78
|
+
body.append(
|
|
79
|
+
f" _reg_{verb.replace('-', '_')}(_sub_{verb.replace('-', '_')})"
|
|
80
|
+
)
|
|
81
|
+
body.append(
|
|
82
|
+
f" app.add_typer(_sub_{verb.replace('-', '_')}, name={verb!r}, "
|
|
83
|
+
f"hidden={hidden!r})"
|
|
84
|
+
)
|
|
85
|
+
elif card["surface"] == "wrapped_class":
|
|
86
|
+
wrapper_mod = f"{mod}_cli"
|
|
87
|
+
body.append(
|
|
88
|
+
f" from {wrapper_mod} import app as _wrap_{verb.replace('-', '_')}"
|
|
89
|
+
)
|
|
90
|
+
body.append(
|
|
91
|
+
f" app.add_typer(_wrap_{verb.replace('-', '_')}, name={verb!r}, "
|
|
92
|
+
f"help={help_text!r}, hidden={hidden!r})"
|
|
93
|
+
)
|
|
94
|
+
src = _REGISTRY_HEADER
|
|
95
|
+
src += "def register_all(app: typer.Typer) -> None:\n"
|
|
96
|
+
src += "\n".join(body) + "\n"
|
|
97
|
+
return src
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def render_command_doc(card: RegisteredCommandCard) -> str:
|
|
101
|
+
"""Render a Markdown page describing one command."""
|
|
102
|
+
lines = [
|
|
103
|
+
f"# `atomadic-forge {card['name']}`",
|
|
104
|
+
"",
|
|
105
|
+
f"_{card['help_text']}_" if card["help_text"] else "",
|
|
106
|
+
"",
|
|
107
|
+
f"- **Module**: `{card['module']}`",
|
|
108
|
+
f"- **Surface**: `{card['surface']}`",
|
|
109
|
+
f"- **Source root**: `{card['source_root']}`",
|
|
110
|
+
f"- **Hidden**: `{card['hidden']}`",
|
|
111
|
+
"",
|
|
112
|
+
]
|
|
113
|
+
if card["sub_commands"]:
|
|
114
|
+
lines.append("## Sub-commands")
|
|
115
|
+
lines.append("")
|
|
116
|
+
for sc in card["sub_commands"]:
|
|
117
|
+
lines.append(f"### `{sc['name']}`")
|
|
118
|
+
lines.append("")
|
|
119
|
+
if sc["docstring"]:
|
|
120
|
+
lines.append(sc["docstring"])
|
|
121
|
+
lines.append("")
|
|
122
|
+
if sc["parameters"]:
|
|
123
|
+
lines.append("Parameters:")
|
|
124
|
+
lines.append("")
|
|
125
|
+
for p in sc["parameters"]:
|
|
126
|
+
lines.append(f"- `{p}`")
|
|
127
|
+
lines.append("")
|
|
128
|
+
lines.append(f"Returns: `{sc['return_type']}`")
|
|
129
|
+
lines.append("")
|
|
130
|
+
return "\n".join(line for line in lines if line is not None) + "\n"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def render_command_index(cards: Iterable[RegisteredCommandCard]) -> str:
|
|
134
|
+
"""Render docs/commands/INDEX.md."""
|
|
135
|
+
cards = sorted(cards, key=lambda c: c["name"])
|
|
136
|
+
lines = ["# ASS-ADE Commands", "", "Auto-generated index. One row per top-level CLI verb.", "",
|
|
137
|
+
"| Verb | Help | Source | Surface |",
|
|
138
|
+
"|------|------|--------|---------|"]
|
|
139
|
+
for c in cards:
|
|
140
|
+
help_short = c["help_text"].replace("|", "\\|") or "_(no help)_"
|
|
141
|
+
lines.append(
|
|
142
|
+
f"| `{c['name']}` | {help_short} | `{c['source_root']}` | `{c['surface']}` |"
|
|
143
|
+
)
|
|
144
|
+
return "\n".join(lines) + "\n"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def render_wrapper_module(
|
|
148
|
+
*,
|
|
149
|
+
target_module: str,
|
|
150
|
+
target_class: str,
|
|
151
|
+
command_name: str,
|
|
152
|
+
sub_commands: list[CommandSignatureCard],
|
|
153
|
+
help_text: str = "",
|
|
154
|
+
init_params: list[str] | None = None,
|
|
155
|
+
auto_scan: str | None = None,
|
|
156
|
+
) -> str:
|
|
157
|
+
"""Emit a wrapper Python module wrapping a class as a Typer subcommand group.
|
|
158
|
+
|
|
159
|
+
Generated app structure::
|
|
160
|
+
|
|
161
|
+
app = typer.Typer()
|
|
162
|
+
@app.callback() — accepts __init__ params, builds an instance, stashes it
|
|
163
|
+
on ``ctx.obj``. Optionally auto-invokes a "warm-up"
|
|
164
|
+
method (e.g. ``scan_repo``) before subcommands.
|
|
165
|
+
@app.command("verb") — pulls instance off ctx.obj, calls the method.
|
|
166
|
+
"""
|
|
167
|
+
safe_help = help_text.replace('"""', "'''")
|
|
168
|
+
init_params = init_params or []
|
|
169
|
+
init_sig, init_fwd = _build_typer_args(init_params)
|
|
170
|
+
has_init_args = bool(init_fwd)
|
|
171
|
+
|
|
172
|
+
lines = [
|
|
173
|
+
'"""',
|
|
174
|
+
f"Auto-generated wrapper for {target_module}.{target_class}.",
|
|
175
|
+
"",
|
|
176
|
+
f"Run ``atomadic-forge {command_name} <method>`` to invoke methods on this class.",
|
|
177
|
+
"Re-emit with ``atomadic-forge commandsmith wrap`` after the source changes.",
|
|
178
|
+
'"""',
|
|
179
|
+
"",
|
|
180
|
+
"from __future__ import annotations",
|
|
181
|
+
"",
|
|
182
|
+
"import json",
|
|
183
|
+
"from pathlib import Path # noqa: F401 — used by some auto-generated annotations",
|
|
184
|
+
"import typer",
|
|
185
|
+
"",
|
|
186
|
+
f"from {target_module} import {target_class}",
|
|
187
|
+
"",
|
|
188
|
+
f"COMMAND_NAME = {command_name!r}",
|
|
189
|
+
f"COMMAND_HELP = {safe_help!r}",
|
|
190
|
+
"",
|
|
191
|
+
f'app = typer.Typer(no_args_is_help=True, help={safe_help!r})',
|
|
192
|
+
"",
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
# Callback that builds the instance. Always emitted so subcommands can
|
|
196
|
+
# share the instance via ctx.obj.
|
|
197
|
+
cb_args = "ctx: typer.Context"
|
|
198
|
+
if init_sig:
|
|
199
|
+
cb_args += ", " + init_sig
|
|
200
|
+
lines.extend([
|
|
201
|
+
"@app.callback()",
|
|
202
|
+
f"def _build({cb_args}) -> None:",
|
|
203
|
+
f' """Construct an instance of {target_class} for this invocation."""',
|
|
204
|
+
])
|
|
205
|
+
if has_init_args:
|
|
206
|
+
lines.append(f" instance = {target_class}({init_fwd})")
|
|
207
|
+
else:
|
|
208
|
+
lines.append(f" instance = {target_class}()")
|
|
209
|
+
if auto_scan:
|
|
210
|
+
lines.append(f" # auto-warm-up: call {auto_scan}() so subcommands see populated state")
|
|
211
|
+
lines.append(" try:")
|
|
212
|
+
lines.append(f" instance.{auto_scan}()")
|
|
213
|
+
lines.append(" except Exception:")
|
|
214
|
+
lines.append(" pass")
|
|
215
|
+
lines.append(" ctx.obj = instance")
|
|
216
|
+
lines.append("")
|
|
217
|
+
|
|
218
|
+
if not sub_commands:
|
|
219
|
+
lines.extend([
|
|
220
|
+
"@app.command('show')",
|
|
221
|
+
"def _show(ctx: typer.Context) -> None:",
|
|
222
|
+
f' """No public methods detected — show the {target_class} repr."""',
|
|
223
|
+
" typer.echo(repr(ctx.obj))",
|
|
224
|
+
])
|
|
225
|
+
|
|
226
|
+
for sc in sub_commands:
|
|
227
|
+
if sc["name"] in {"__init__"}:
|
|
228
|
+
continue
|
|
229
|
+
verb = sc["name"].replace("_", "-")
|
|
230
|
+
py_args, fwd_args = _build_typer_args(sc["parameters"])
|
|
231
|
+
full_sig = "ctx: typer.Context"
|
|
232
|
+
if py_args:
|
|
233
|
+
full_sig += ", " + py_args
|
|
234
|
+
doc = sc["docstring"] or f"Invoke {target_class}.{sc['name']}."
|
|
235
|
+
lines.extend([
|
|
236
|
+
f"@app.command({verb!r})",
|
|
237
|
+
f"def _{sc['name']}({full_sig}) -> None:",
|
|
238
|
+
f' """{doc}"""',
|
|
239
|
+
f" result = ctx.obj.{sc['name']}({fwd_args})",
|
|
240
|
+
" if result is not None:",
|
|
241
|
+
" try:",
|
|
242
|
+
" typer.echo(json.dumps(result, indent=2, default=str))",
|
|
243
|
+
" except (TypeError, ValueError):",
|
|
244
|
+
" typer.echo(str(result))",
|
|
245
|
+
"",
|
|
246
|
+
])
|
|
247
|
+
return "\n".join(lines) + "\n"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _build_typer_args(parameters: list[str]) -> tuple[str, str]:
|
|
251
|
+
"""Build (python signature args, forward-call args) from parameter strings.
|
|
252
|
+
|
|
253
|
+
Drops ``self``. Falls back to ``str`` for unknown annotations.
|
|
254
|
+
"""
|
|
255
|
+
sig_parts: list[str] = []
|
|
256
|
+
fwd_parts: list[str] = []
|
|
257
|
+
for p in parameters:
|
|
258
|
+
name, _, ann = p.partition(":")
|
|
259
|
+
name = name.strip()
|
|
260
|
+
ann = ann.strip() or "str"
|
|
261
|
+
if name in ("self", "cls"):
|
|
262
|
+
continue
|
|
263
|
+
if ann not in ("int", "float", "bool", "str", "Path", "list", "dict"):
|
|
264
|
+
ann = "str"
|
|
265
|
+
sig_parts.append(f"{name}: {ann}")
|
|
266
|
+
fwd_parts.append(name)
|
|
267
|
+
return ", ".join(sig_parts), ", ".join(fwd_parts)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Tier a1 — pure compiler-feedback prompt builder for the fix-round loop.
|
|
2
|
+
|
|
3
|
+
Golden Path Lane A W3 deliverable. Distinct from the regular iterate
|
|
4
|
+
turn:
|
|
5
|
+
|
|
6
|
+
* regular turn — wire + certify scores → LLM is asked to *improve
|
|
7
|
+
quality* (raise the score, fix violations,
|
|
8
|
+
etc.). Bounded by ``max_iterations``.
|
|
9
|
+
* fix-round turn — import_smoke FAIL (or pytest collection error)
|
|
10
|
+
→ LLM is asked to *fix the broken code* without
|
|
11
|
+
adding features. Bounded by ``max_fix_rounds``
|
|
12
|
+
PER iterate turn.
|
|
13
|
+
|
|
14
|
+
A fix-round is much cheaper (one error trace, no quality scoring) and
|
|
15
|
+
should always run before the next regular turn — otherwise the
|
|
16
|
+
regular turn shows the LLM a ``score=0`` because the package didn't
|
|
17
|
+
even import, and the LLM wastes the round chasing scoring instead of
|
|
18
|
+
the actual blocker.
|
|
19
|
+
|
|
20
|
+
Pure: takes an ``ImportSmokeReport`` (or ImportSmoke-shaped dict) and
|
|
21
|
+
returns a string. No I/O, no LLM call. The forge_loop orchestrator
|
|
22
|
+
owns when to invoke this.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
_FIX_ROUND_HEADER = (
|
|
27
|
+
"FIX ROUND — your previous emission did not import. Do NOT add "
|
|
28
|
+
"features; do NOT touch unrelated files. Output ONLY the minimum "
|
|
29
|
+
"set of files needed to make the package importable.\n"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_FIX_ROUND_FOOTER = (
|
|
33
|
+
"\nRespond with the standard JSON array of `{path, content}` "
|
|
34
|
+
"objects. If you believe the package is already correct and the "
|
|
35
|
+
"error is environmental (e.g. a missing 3rd-party dep), output "
|
|
36
|
+
"the exact two characters `[]` and the loop will exit.\n"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def pack_compile_feedback(
|
|
41
|
+
smoke_report: dict,
|
|
42
|
+
*,
|
|
43
|
+
package: str,
|
|
44
|
+
fix_round_index: int,
|
|
45
|
+
max_fix_rounds: int,
|
|
46
|
+
) -> str:
|
|
47
|
+
"""Build the fix-round prompt from an ImportSmokeReport-shape dict.
|
|
48
|
+
|
|
49
|
+
Arguments:
|
|
50
|
+
smoke_report — dict with at least ``error_kind``,
|
|
51
|
+
``error_message``, ``traceback_excerpt``.
|
|
52
|
+
package — the package name being iterated.
|
|
53
|
+
fix_round_index — 1-based current attempt within this turn.
|
|
54
|
+
max_fix_rounds — total budget so the LLM knows whether to
|
|
55
|
+
minimise the fix or give up cleanly.
|
|
56
|
+
"""
|
|
57
|
+
if fix_round_index < 1:
|
|
58
|
+
raise ValueError("fix_round_index must be >= 1")
|
|
59
|
+
if max_fix_rounds < 1:
|
|
60
|
+
raise ValueError("max_fix_rounds must be >= 1")
|
|
61
|
+
|
|
62
|
+
kind = str(smoke_report.get("error_kind") or "ImportFailure")
|
|
63
|
+
message = str(smoke_report.get("error_message") or "(no message)")
|
|
64
|
+
trace = str(smoke_report.get("traceback_excerpt") or "").strip()
|
|
65
|
+
|
|
66
|
+
parts: list[str] = [
|
|
67
|
+
_FIX_ROUND_HEADER,
|
|
68
|
+
f" package: {package}",
|
|
69
|
+
f" fix-round attempt: {fix_round_index} of {max_fix_rounds}",
|
|
70
|
+
f" error_kind: {kind}",
|
|
71
|
+
f" error_message: {message}",
|
|
72
|
+
]
|
|
73
|
+
if trace:
|
|
74
|
+
# Cap to avoid blowing the context window — the smoke
|
|
75
|
+
# collector already trims stderr to ~600 chars.
|
|
76
|
+
parts.append("\nTraceback excerpt:\n```\n" + trace[:1200] + "\n```")
|
|
77
|
+
parts.append(_FIX_ROUND_FOOTER)
|
|
78
|
+
return "\n".join(parts)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def should_fix_round(smoke_report: dict) -> bool:
|
|
82
|
+
"""Return True iff the smoke report names a fixable runtime error.
|
|
83
|
+
|
|
84
|
+
'Fixable' means: there's a concrete error_kind and non-empty
|
|
85
|
+
message we can show the LLM. Empty smoke (importable=True) returns
|
|
86
|
+
False; environmental failures with no useful trace also return
|
|
87
|
+
False so the orchestrator skips the round rather than re-prompting
|
|
88
|
+
the LLM with nothing.
|
|
89
|
+
"""
|
|
90
|
+
if smoke_report.get("importable"):
|
|
91
|
+
return False
|
|
92
|
+
kind = smoke_report.get("error_kind") or ""
|
|
93
|
+
message = smoke_report.get("error_message") or ""
|
|
94
|
+
return bool(kind and message)
|