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 @@
|
|
|
1
|
+
"""Commandsmith-discoverable command modules."""
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-generated by Commandsmith — do NOT hand-edit.
|
|
3
|
+
Run ``atomadic-forge commandsmith sync`` to refresh.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register_all(app: typer.Typer) -> None:
|
|
12
|
+
"""Register every Commandsmith-discovered command on ``app``."""
|
|
13
|
+
from atomadic_forge.commands.chat import app as _app_chat
|
|
14
|
+
app.add_typer(_app_chat, name='chat', help='Chat with a Forge-aware AI copilot over optional repo context.', hidden=False)
|
|
15
|
+
from atomadic_forge.commands.commandsmith import app as _app_commandsmith
|
|
16
|
+
app.add_typer(_app_commandsmith, name='commandsmith', help='``atomadic-forge commandsmith`` — manage the auto-wired CLI command registry.', hidden=False)
|
|
17
|
+
from atomadic_forge.commands.config_cmd import app as _app_config
|
|
18
|
+
app.add_typer(_app_config, name='config', help='Configure Atomadic Forge — setup wizard + config management.', hidden=False)
|
|
19
|
+
from atomadic_forge.commands.demo import app as _app_demo
|
|
20
|
+
app.add_typer(_app_demo, name='demo', help='One-shot launch-video verb: preset evolve + DEMO.md artifact.', hidden=False)
|
|
21
|
+
from atomadic_forge.commands.emergent import app as _app_emergent
|
|
22
|
+
app.add_typer(_app_emergent, name='emergent', help='Synthesise new feature candidates from existing components.', hidden=False)
|
|
23
|
+
from atomadic_forge.commands.emergent_then_synergy import app as _app_emergent_then_synergy
|
|
24
|
+
app.add_typer(_app_emergent_then_synergy, name='emergent-then-synergy', help='Run emergent to emit a JSON artifact, then feed it to synergy for the next phase.', hidden=False)
|
|
25
|
+
from atomadic_forge.commands.evolve import app as _app_evolve
|
|
26
|
+
app.add_typer(_app_evolve, name='evolve', help="Recursive self-improvement: run iterate N times, each round seeded by the previous round's growing catalog.", hidden=False)
|
|
27
|
+
from atomadic_forge.commands.evolve_then_iterate import app as _app_evolve_then_iterate
|
|
28
|
+
app.add_typer(_app_evolve_then_iterate, name='evolve-then-iterate', help='Run evolve to emit a JSON artifact, then feed it to iterate for the next phase.', hidden=False)
|
|
29
|
+
from atomadic_forge.commands.feature_then_emergent import app as _app_feature_then_emergent
|
|
30
|
+
app.add_typer(_app_feature_then_emergent, name='feature-then-emergent', help='Run any feature, then fan its JSON output into emergent scan to surface novel compositions.', hidden=False)
|
|
31
|
+
from atomadic_forge.commands.iterate import app as _app_iterate
|
|
32
|
+
app.add_typer(_app_iterate, name='iterate', help='Architecturally-coherent code generation: LLM emits, Forge enforces, loop iterates until certify clears.', hidden=False)
|
|
33
|
+
from atomadic_forge.commands.synergy import app as _app_synergy
|
|
34
|
+
app.add_typer(_app_synergy, name='synergy', help="Find feature/CLI synergies (producer-consumer pairs that aren't wired together) and optionally implement adapters.", hidden=False)
|
|
35
|
+
from atomadic_forge.commands.synergy_then_emergent import app as _app_synergy_then_emergent
|
|
36
|
+
app.add_typer(_app_synergy_then_emergent, name='synergy-then-emergent', help='Run synergy to emit a JSON artifact, then feed it to emergent for the next phase.', hidden=False)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Tier a4 — `forge audit` verb: surface .atomadic-forge lineage.
|
|
2
|
+
|
|
3
|
+
Lane D1 of the post-audit plan. Closes the 'lineage exists but is
|
|
4
|
+
invisible' friction point: every --apply already writes to
|
|
5
|
+
.atomadic-forge/lineage.jsonl, but until now there was no verb to
|
|
6
|
+
query it short of `cat | jq`.
|
|
7
|
+
|
|
8
|
+
Subcommands:
|
|
9
|
+
forge audit list <project> [--last N] [--json]
|
|
10
|
+
Summary of distinct artifacts, run counts, and latest write times.
|
|
11
|
+
|
|
12
|
+
forge audit show <project> <artifact> [--json]
|
|
13
|
+
Pretty-print the named manifest (scout, cherry, wire, certify, …).
|
|
14
|
+
|
|
15
|
+
forge audit log <project> [--last N] [--json]
|
|
16
|
+
Raw lineage entries (newest-last).
|
|
17
|
+
|
|
18
|
+
Future subcommands ('trend', 'replay') need lineage-shape extension
|
|
19
|
+
and are reserved for their own lane.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Annotated
|
|
26
|
+
|
|
27
|
+
import typer
|
|
28
|
+
|
|
29
|
+
from ..a1_at_functions.error_hints import format_hint
|
|
30
|
+
from ..a1_at_functions.lineage_reader import (
|
|
31
|
+
list_artifacts,
|
|
32
|
+
load_manifest,
|
|
33
|
+
read_lineage,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
COMMAND_NAME = "audit"
|
|
37
|
+
COMMAND_HELP = (
|
|
38
|
+
"Surface the .atomadic-forge lineage log: list runs, inspect "
|
|
39
|
+
"saved manifests, replay history."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
app = typer.Typer(no_args_is_help=True, help=COMMAND_HELP)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.command("list")
|
|
46
|
+
def list_cmd(
|
|
47
|
+
project: Annotated[Path, typer.Argument(
|
|
48
|
+
exists=True, file_okay=False, dir_okay=True, resolve_path=True,
|
|
49
|
+
help="Project root containing .atomadic-forge/.")] = Path("."),
|
|
50
|
+
json_out: Annotated[bool, typer.Option("--json")] = False,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Summarize each artifact: run count, latest write time, path."""
|
|
53
|
+
entries = list_artifacts(project)
|
|
54
|
+
if json_out:
|
|
55
|
+
typer.echo(json.dumps(
|
|
56
|
+
{"schema_version": "atomadic-forge.audit.list/v1",
|
|
57
|
+
"project": str(project),
|
|
58
|
+
"artifact_count": len(entries),
|
|
59
|
+
"artifacts": entries},
|
|
60
|
+
indent=2, default=str))
|
|
61
|
+
return
|
|
62
|
+
if not entries:
|
|
63
|
+
typer.echo(f"\nNo lineage found at {project}/.atomadic-forge/lineage.jsonl")
|
|
64
|
+
typer.echo("\nLineage is recorded automatically when you run any verb")
|
|
65
|
+
typer.echo("with --apply (forge auto, forge cherry, forge finalize).")
|
|
66
|
+
return
|
|
67
|
+
typer.echo(f"\nForge audit — artifacts under {project}/.atomadic-forge/")
|
|
68
|
+
typer.echo("-" * 60)
|
|
69
|
+
for e in entries:
|
|
70
|
+
typer.echo(
|
|
71
|
+
f" {e['artifact']:<20} runs={e['run_count']:<3} "
|
|
72
|
+
f"latest={e['latest_ts_utc']} ({e['path']})"
|
|
73
|
+
)
|
|
74
|
+
typer.echo(f"\n {len(entries)} distinct artifact(s).")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.command("show")
|
|
78
|
+
def show_cmd(
|
|
79
|
+
project: Annotated[Path, typer.Argument(
|
|
80
|
+
exists=True, file_okay=False, dir_okay=True, resolve_path=True)],
|
|
81
|
+
artifact: Annotated[str, typer.Argument(
|
|
82
|
+
help="Manifest name: scout | cherry | assimilate | wire | certify | "
|
|
83
|
+
"auto | (custom).")],
|
|
84
|
+
json_out: Annotated[bool, typer.Option("--json")] = False,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Pretty-print the JSON manifest for ``artifact``."""
|
|
87
|
+
data = load_manifest(project, artifact)
|
|
88
|
+
if data is None:
|
|
89
|
+
raise typer.BadParameter(
|
|
90
|
+
format_hint("not_a_forge_manifest",
|
|
91
|
+
path=project / ".atomadic-forge" / f"{artifact}.json")
|
|
92
|
+
)
|
|
93
|
+
if json_out:
|
|
94
|
+
typer.echo(json.dumps(data, indent=2, default=str))
|
|
95
|
+
return
|
|
96
|
+
schema = data.get("schema_version", "(unknown)")
|
|
97
|
+
typer.echo(f"\nForge audit — {artifact} ({schema})")
|
|
98
|
+
typer.echo("-" * 60)
|
|
99
|
+
# Compact summary lines: top-level keys with non-collection values,
|
|
100
|
+
# plus collection sizes. Verbose dump available via --json.
|
|
101
|
+
for key, value in data.items():
|
|
102
|
+
if isinstance(value, dict):
|
|
103
|
+
typer.echo(f" {key}: <dict, {len(value)} keys>")
|
|
104
|
+
elif isinstance(value, list):
|
|
105
|
+
typer.echo(f" {key}: <list, {len(value)} items>")
|
|
106
|
+
else:
|
|
107
|
+
typer.echo(f" {key}: {value}")
|
|
108
|
+
typer.echo("\n (re-run with --json for full payload)")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command("log")
|
|
112
|
+
def log_cmd(
|
|
113
|
+
project: Annotated[Path, typer.Argument(
|
|
114
|
+
exists=True, file_okay=False, dir_okay=True, resolve_path=True)] = Path("."),
|
|
115
|
+
last: Annotated[int, typer.Option("--last",
|
|
116
|
+
help="Show only the most recent N entries.")] = 20,
|
|
117
|
+
json_out: Annotated[bool, typer.Option("--json")] = False,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Show raw lineage entries (newest-last)."""
|
|
120
|
+
entries = read_lineage(project, last=last)
|
|
121
|
+
if json_out:
|
|
122
|
+
typer.echo(json.dumps(
|
|
123
|
+
{"schema_version": "atomadic-forge.audit.log/v1",
|
|
124
|
+
"project": str(project),
|
|
125
|
+
"entry_count": len(entries),
|
|
126
|
+
"entries": entries},
|
|
127
|
+
indent=2, default=str))
|
|
128
|
+
return
|
|
129
|
+
if not entries:
|
|
130
|
+
typer.echo(f"\nNo lineage entries at {project}/.atomadic-forge/lineage.jsonl")
|
|
131
|
+
return
|
|
132
|
+
typer.echo(f"\nForge audit — last {len(entries)} lineage entries")
|
|
133
|
+
typer.echo("-" * 60)
|
|
134
|
+
for e in entries:
|
|
135
|
+
typer.echo(f" {e.get('ts_utc', '?')} "
|
|
136
|
+
f"{e.get('artifact', '?'):<20} "
|
|
137
|
+
f"{e.get('path', '?')}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def register(parent: typer.Typer) -> None:
|
|
141
|
+
"""Hook used by the unified CLI to mount this sub-app."""
|
|
142
|
+
parent.add_typer(app, name=COMMAND_NAME, help=COMMAND_HELP)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""``forge chat`` — chat copilot over Forge + optional repo context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from atomadic_forge.a1_at_functions.chat_context import (
|
|
13
|
+
build_chat_context,
|
|
14
|
+
chat_system_prompt,
|
|
15
|
+
render_chat_prompt,
|
|
16
|
+
)
|
|
17
|
+
from atomadic_forge.a1_at_functions.provider_resolver import (
|
|
18
|
+
PROVIDER_HELP,
|
|
19
|
+
resolve_provider,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
COMMAND_NAME = "chat"
|
|
23
|
+
COMMAND_HELP = "Chat with a Forge-aware AI copilot over optional repo context."
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(no_args_is_help=True, help=COMMAND_HELP)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _provider(name: str):
|
|
30
|
+
try:
|
|
31
|
+
return resolve_provider(name)
|
|
32
|
+
except ValueError as exc:
|
|
33
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _context_paths(context: list[Path] | None, cwd_context: bool) -> list[Path]:
|
|
37
|
+
if context:
|
|
38
|
+
return context
|
|
39
|
+
return [Path.cwd()] if cwd_context else []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("ask")
|
|
43
|
+
def ask_cmd(
|
|
44
|
+
message: Annotated[str, typer.Argument(help="Question or request for the copilot.")],
|
|
45
|
+
provider: Annotated[str, typer.Option("--provider",
|
|
46
|
+
help=PROVIDER_HELP)] = "auto",
|
|
47
|
+
context: Annotated[list[Path] | None, typer.Option("--context", "-c",
|
|
48
|
+
exists=True, file_okay=True, dir_okay=True, resolve_path=True,
|
|
49
|
+
help="File or directory to include as bounded context. Repeatable.")] = None,
|
|
50
|
+
cwd_context: Annotated[bool, typer.Option("--cwd-context/--no-cwd-context",
|
|
51
|
+
help="Use the current directory as context when --context is omitted.")] = True,
|
|
52
|
+
max_files: Annotated[int, typer.Option("--max-files",
|
|
53
|
+
help="Maximum files to pack into the chat context.")] = 12,
|
|
54
|
+
max_chars: Annotated[int, typer.Option("--max-chars",
|
|
55
|
+
help="Maximum context characters sent to the provider.")] = 16_000,
|
|
56
|
+
json_out: Annotated[bool, typer.Option("--json")] = False,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Ask one copilot question and print the answer."""
|
|
59
|
+
llm = _provider(provider)
|
|
60
|
+
ctx = build_chat_context(
|
|
61
|
+
_context_paths(context, cwd_context),
|
|
62
|
+
cwd=Path.cwd(),
|
|
63
|
+
max_files=max_files,
|
|
64
|
+
max_chars=max_chars,
|
|
65
|
+
)
|
|
66
|
+
prompt = render_chat_prompt(message, context=ctx["context"])
|
|
67
|
+
try:
|
|
68
|
+
answer = llm.call(prompt, system=chat_system_prompt())
|
|
69
|
+
except RuntimeError as exc:
|
|
70
|
+
raise click.ClickException(str(exc)) from exc
|
|
71
|
+
if json_out:
|
|
72
|
+
typer.echo(json.dumps({
|
|
73
|
+
"schema_version": "atomadic-forge.chat/v1",
|
|
74
|
+
"provider": llm.name,
|
|
75
|
+
"message": message,
|
|
76
|
+
"answer": answer,
|
|
77
|
+
"context": {
|
|
78
|
+
"file_count": ctx["file_count"],
|
|
79
|
+
"char_count": ctx["char_count"],
|
|
80
|
+
"files": ctx["files"],
|
|
81
|
+
},
|
|
82
|
+
}, indent=2, default=str))
|
|
83
|
+
return
|
|
84
|
+
typer.echo(answer.rstrip())
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command("repl")
|
|
88
|
+
def repl_cmd(
|
|
89
|
+
provider: Annotated[str, typer.Option("--provider",
|
|
90
|
+
help=PROVIDER_HELP)] = "auto",
|
|
91
|
+
context: Annotated[list[Path] | None, typer.Option("--context", "-c",
|
|
92
|
+
exists=True, file_okay=True, dir_okay=True, resolve_path=True,
|
|
93
|
+
help="File or directory to include as bounded context. Repeatable.")] = None,
|
|
94
|
+
cwd_context: Annotated[bool, typer.Option("--cwd-context/--no-cwd-context",
|
|
95
|
+
help="Use the current directory as context when --context is omitted.")] = True,
|
|
96
|
+
max_files: Annotated[int, typer.Option("--max-files")] = 12,
|
|
97
|
+
max_chars: Annotated[int, typer.Option("--max-chars")] = 16_000,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Start an interactive copilot session. Type ``/exit`` to leave."""
|
|
100
|
+
llm = _provider(provider)
|
|
101
|
+
ctx = build_chat_context(
|
|
102
|
+
_context_paths(context, cwd_context),
|
|
103
|
+
cwd=Path.cwd(),
|
|
104
|
+
max_files=max_files,
|
|
105
|
+
max_chars=max_chars,
|
|
106
|
+
)
|
|
107
|
+
typer.echo(f"Forge chat copilot ({llm.name}). Type /exit to leave.")
|
|
108
|
+
if ctx["file_count"]:
|
|
109
|
+
typer.echo(f"Context: {ctx['file_count']} file(s), {ctx['char_count']} chars.")
|
|
110
|
+
history: list[dict[str, str]] = []
|
|
111
|
+
while True:
|
|
112
|
+
try:
|
|
113
|
+
message = typer.prompt("you").strip()
|
|
114
|
+
except (EOFError, KeyboardInterrupt):
|
|
115
|
+
typer.echo("")
|
|
116
|
+
break
|
|
117
|
+
if message.lower() in {"/exit", "/quit", "exit", "quit", ":q"}:
|
|
118
|
+
break
|
|
119
|
+
if not message:
|
|
120
|
+
continue
|
|
121
|
+
prompt = render_chat_prompt(message, context=ctx["context"], history=history)
|
|
122
|
+
try:
|
|
123
|
+
answer = llm.call(prompt, system=chat_system_prompt()).rstrip()
|
|
124
|
+
except RuntimeError as exc:
|
|
125
|
+
typer.secho(f"Provider error: {exc}", fg=typer.colors.RED, err=True)
|
|
126
|
+
continue
|
|
127
|
+
typer.echo("")
|
|
128
|
+
typer.echo(answer)
|
|
129
|
+
typer.echo("")
|
|
130
|
+
history.extend([
|
|
131
|
+
{"role": "user", "content": message},
|
|
132
|
+
{"role": "assistant", "content": answer},
|
|
133
|
+
])
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""``atomadic-forge commandsmith`` — manage the auto-wired CLI command registry.
|
|
2
|
+
|
|
3
|
+
This is the operator-facing surface for the Commandsmith feature. Every
|
|
4
|
+
ASS-ADE assimilation cycle ends by adding new symbols (classes, functions)
|
|
5
|
+
under ``atomadic_forge/``. Commandsmith turns those symbols into invocable CLI
|
|
6
|
+
verbs without any hand-editing of ``unified_cli.py``.
|
|
7
|
+
|
|
8
|
+
Sub-commands:
|
|
9
|
+
|
|
10
|
+
* ``atomadic-forge commandsmith discover`` — list discoverable command modules.
|
|
11
|
+
* ``atomadic-forge commandsmith sync`` — regenerate registry + docs + manifest.
|
|
12
|
+
* ``atomadic-forge commandsmith wrap`` — generate a Typer wrapper around a class.
|
|
13
|
+
* ``atomadic-forge commandsmith smoke`` — run ``--help`` against every command.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Annotated
|
|
21
|
+
|
|
22
|
+
import typer
|
|
23
|
+
|
|
24
|
+
from atomadic_forge.a3_og_features.commandsmith_feature import Commandsmith
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(
|
|
27
|
+
no_args_is_help=True,
|
|
28
|
+
help="Auto-register, document, and smoke-test ASS-ADE CLI commands.",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _resolve_package_root() -> Path:
|
|
33
|
+
"""Locate ``src/atomadic_forge`` from this file's location."""
|
|
34
|
+
here = Path(__file__).resolve()
|
|
35
|
+
return here.parent.parent # commands/ -> atomadic_forge/
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command("discover")
|
|
39
|
+
def discover_cmd(
|
|
40
|
+
json_out: Annotated[bool, typer.Option("--json", help="Emit JSON.")] = False,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""List discoverable command modules under ``atomadic_forge/commands/``."""
|
|
43
|
+
cs = Commandsmith(package_root=_resolve_package_root())
|
|
44
|
+
cards = cs.discover()
|
|
45
|
+
if json_out:
|
|
46
|
+
typer.echo(json.dumps(cards, indent=2))
|
|
47
|
+
return
|
|
48
|
+
typer.echo(f"Discovered {len(cards)} command module(s):")
|
|
49
|
+
for c in cards:
|
|
50
|
+
flag = " [hidden]" if c["hidden"] else ""
|
|
51
|
+
typer.echo(f" - {c['name']:24s} {c['surface']:14s} {c['module']}{flag}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command("sync")
|
|
55
|
+
def sync_cmd(
|
|
56
|
+
smoke: Annotated[bool, typer.Option("--smoke",
|
|
57
|
+
help="Also smoke-test every command's --help.")] = False,
|
|
58
|
+
json_out: Annotated[bool, typer.Option("--json")] = False,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Rebuild registry, per-command docs, and the manifest."""
|
|
61
|
+
cs = Commandsmith(package_root=_resolve_package_root())
|
|
62
|
+
manifest = cs.sync(smoke=smoke)
|
|
63
|
+
if json_out:
|
|
64
|
+
typer.echo(json.dumps(manifest, indent=2))
|
|
65
|
+
return
|
|
66
|
+
typer.echo(f"Wrote registry → {cs.commands_dir / '_registry.py'}")
|
|
67
|
+
typer.echo(f"Wrote docs → {cs.docs_root}")
|
|
68
|
+
typer.echo(f"Manifest → {cs.manifest_dir / 'commandsmith_manifest.json'}")
|
|
69
|
+
typer.echo(f"Total verbs: {len(manifest['commands'])}")
|
|
70
|
+
if smoke:
|
|
71
|
+
passed = sum(1 for v in manifest["smoke_results"].values() if v)
|
|
72
|
+
typer.echo(f"Smoke: {passed}/{len(manifest['smoke_results'])} passed")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.command("wrap")
|
|
76
|
+
def wrap_cmd(
|
|
77
|
+
target_module: Annotated[str, typer.Argument(
|
|
78
|
+
help="Importable module path of the class to wrap.")],
|
|
79
|
+
target_class: Annotated[str, typer.Argument(
|
|
80
|
+
help="Class name to wrap (must be importable).")],
|
|
81
|
+
command_name: Annotated[str, typer.Option("--name", "-n",
|
|
82
|
+
help="CLI verb to expose (kebab-case).")] = "",
|
|
83
|
+
help_text: Annotated[str, typer.Option("--help-text",
|
|
84
|
+
help="One-line help string for the generated subcommand group.")] = "",
|
|
85
|
+
out_dir: Annotated[Path | None, typer.Option("--out-dir",
|
|
86
|
+
help="Where to write the wrapper module (defaults to commands/).",
|
|
87
|
+
file_okay=False, dir_okay=True, resolve_path=True)] = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Generate a Typer wrapper around an importable class."""
|
|
90
|
+
cs = Commandsmith(package_root=_resolve_package_root())
|
|
91
|
+
name = command_name or target_class.lower()
|
|
92
|
+
name = "".join("-" + c.lower() if c.isupper() and i > 0 else c.lower()
|
|
93
|
+
for i, c in enumerate(target_class)).lstrip("-")
|
|
94
|
+
name = command_name or name
|
|
95
|
+
out = cs.wrap_class(
|
|
96
|
+
target_module=target_module,
|
|
97
|
+
target_class=target_class,
|
|
98
|
+
command_name=name,
|
|
99
|
+
help_text=help_text or f"Auto-wrapped {target_class}",
|
|
100
|
+
out_dir=out_dir,
|
|
101
|
+
)
|
|
102
|
+
typer.echo(f"Wrote wrapper → {out}")
|
|
103
|
+
typer.echo("Run ``atomadic-forge commandsmith sync`` to register it.")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
_CORE_VERBS = [
|
|
107
|
+
"init", "auto", "recon", "cherry", "finalize", "wire", "plan",
|
|
108
|
+
"context-pack", "preflight", "recipes", "plan-list", "plan-show",
|
|
109
|
+
"plan-step", "plan-apply", "enforce", "certify", "sbom", "diff",
|
|
110
|
+
"doctor", "cs1", "sidecar", "mcp", "status",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@app.command("smoke")
|
|
115
|
+
def smoke_cmd(
|
|
116
|
+
include_core: Annotated[bool, typer.Option(
|
|
117
|
+
"--include-core/--no-core",
|
|
118
|
+
help="Also smoke-test core verbs (init, wire, certify, etc.).")] = True,
|
|
119
|
+
json_out: Annotated[bool, typer.Option("--json")] = False,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Run ``<verb> --help`` for every registered command and report."""
|
|
122
|
+
import os
|
|
123
|
+
import subprocess
|
|
124
|
+
cs = Commandsmith(package_root=_resolve_package_root())
|
|
125
|
+
cards = cs.discover()
|
|
126
|
+
results = cs.smoke(cards)
|
|
127
|
+
if include_core:
|
|
128
|
+
env = os.environ.copy()
|
|
129
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
130
|
+
env["PYTHONUTF8"] = "1"
|
|
131
|
+
cli = [sys.executable, "-m", "atomadic_forge.a4_sy_orchestration.cli"]
|
|
132
|
+
for verb in _CORE_VERBS:
|
|
133
|
+
if verb in results:
|
|
134
|
+
continue
|
|
135
|
+
try:
|
|
136
|
+
rc = subprocess.run(
|
|
137
|
+
cli + [verb, "--help"],
|
|
138
|
+
capture_output=True, timeout=20, env=env,
|
|
139
|
+
).returncode
|
|
140
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
141
|
+
rc = 1
|
|
142
|
+
results[verb] = rc == 0
|
|
143
|
+
if json_out:
|
|
144
|
+
typer.echo(json.dumps(results, indent=2))
|
|
145
|
+
return
|
|
146
|
+
pad = max((len(n) for n in results), default=0)
|
|
147
|
+
for name, ok in sorted(results.items()):
|
|
148
|
+
mark = "PASS" if ok else "FAIL"
|
|
149
|
+
typer.echo(f" {mark:5s} {name:<{pad}}")
|
|
150
|
+
failed = [n for n, ok in results.items() if not ok]
|
|
151
|
+
if failed:
|
|
152
|
+
typer.echo(f"\n{len(failed)} verb(s) failed --help:", err=True)
|
|
153
|
+
raise typer.Exit(1)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.command("repair-imports")
|
|
157
|
+
def repair_imports_cmd(
|
|
158
|
+
package_root: Annotated[Path, typer.Argument(
|
|
159
|
+
help="Path to <output>/src/<package>/ produced by atomadic-forge assimilate.",
|
|
160
|
+
exists=True, file_okay=False, dir_okay=True, resolve_path=True)],
|
|
161
|
+
apply: Annotated[bool, typer.Option("--apply",
|
|
162
|
+
help="Write changes (default: dry-run).")] = False,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Repair broken cross-symbol imports in an assimilated tier tree.
|
|
165
|
+
|
|
166
|
+
The assimilator emits one file per symbol but does not always rewrite
|
|
167
|
+
intra-package references (``from <flat-name> import X``). This pass
|
|
168
|
+
looks each name up in the tier-local sibling files and rewrites the
|
|
169
|
+
import to a relative sibling reference.
|
|
170
|
+
"""
|
|
171
|
+
from atomadic_forge.a1_at_functions.commandsmith_import_repair import (
|
|
172
|
+
repair_assimilation_output,
|
|
173
|
+
)
|
|
174
|
+
diffs = repair_assimilation_output(package_root, dry_run=not apply)
|
|
175
|
+
total = sum(len(v) for v in diffs.values())
|
|
176
|
+
for tier, items in diffs.items():
|
|
177
|
+
typer.echo(f" {tier}: {len(items)} files {'rewritten' if apply else 'would change'}")
|
|
178
|
+
typer.echo(f"TOTAL: {total} files {'rewritten' if apply else 'would change'} ({'applied' if apply else 'dry-run'})")
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Tier a4 — config management CLI commands (show, set, test, wizard)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from atomadic_forge.a0_qk_constants.config_defaults import (
|
|
12
|
+
CONFIG_FILE_NAME,
|
|
13
|
+
GLOBAL_CONFIG_DIR,
|
|
14
|
+
LOCAL_CONFIG_DIR,
|
|
15
|
+
)
|
|
16
|
+
from atomadic_forge.a1_at_functions.config_io import (
|
|
17
|
+
load_config,
|
|
18
|
+
read_config_file,
|
|
19
|
+
save_config,
|
|
20
|
+
validate_config,
|
|
21
|
+
)
|
|
22
|
+
from atomadic_forge.a1_at_functions.provider_detect import test_provider
|
|
23
|
+
|
|
24
|
+
COMMAND_NAME = "config"
|
|
25
|
+
COMMAND_HELP = "Configure Atomadic Forge — setup wizard + config management."
|
|
26
|
+
|
|
27
|
+
app = typer.Typer(no_args_is_help=True, help=COMMAND_HELP)
|
|
28
|
+
|
|
29
|
+
_CWD = Path(".")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.command("wizard")
|
|
33
|
+
def wizard_cmd(
|
|
34
|
+
project_dir: Annotated[
|
|
35
|
+
Path,
|
|
36
|
+
typer.Option("--project", file_okay=False, dir_okay=True, resolve_path=True,
|
|
37
|
+
help="Project directory (default: cwd)."),
|
|
38
|
+
] = _CWD,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Interactive setup wizard — configure LLM, defaults, and workspace."""
|
|
41
|
+
from atomadic_forge.a3_og_features.setup_wizard import run_wizard
|
|
42
|
+
run_wizard(project_dir.resolve())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.command("show")
|
|
46
|
+
def show_cmd(
|
|
47
|
+
project_dir: Annotated[
|
|
48
|
+
Path,
|
|
49
|
+
typer.Option("--project", file_okay=False, dir_okay=True, resolve_path=True,
|
|
50
|
+
help="Project directory (default: cwd)."),
|
|
51
|
+
] = _CWD,
|
|
52
|
+
json_out: Annotated[bool, typer.Option("--json")] = False,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Print the current merged config (local → global → defaults)."""
|
|
55
|
+
config = load_config(project_dir.resolve())
|
|
56
|
+
issues = validate_config(config)
|
|
57
|
+
|
|
58
|
+
if json_out:
|
|
59
|
+
typer.echo(json.dumps({"config": config, "issues": issues}, indent=2))
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
typer.echo("\nAtomadic Forge — config")
|
|
63
|
+
typer.echo("-" * 44)
|
|
64
|
+
for k, v in config.items():
|
|
65
|
+
if "key" in k and v:
|
|
66
|
+
s = str(v)
|
|
67
|
+
display = s[:8] + "..." + s[-4:] if len(s) > 12 else "***"
|
|
68
|
+
else:
|
|
69
|
+
display = str(v) if v is not None else "(not set)"
|
|
70
|
+
typer.echo(f" {k:28s} {display}")
|
|
71
|
+
|
|
72
|
+
if issues:
|
|
73
|
+
typer.echo("\nValidation issues:")
|
|
74
|
+
for issue in issues:
|
|
75
|
+
typer.secho(f" - {issue}", fg="yellow")
|
|
76
|
+
else:
|
|
77
|
+
typer.secho("\n Config is valid.", fg="green")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@app.command("set")
|
|
81
|
+
def set_cmd(
|
|
82
|
+
key: Annotated[str, typer.Argument(help="Config key to set.")],
|
|
83
|
+
value: Annotated[str, typer.Argument(help="Value to assign.")],
|
|
84
|
+
project_dir: Annotated[
|
|
85
|
+
Path,
|
|
86
|
+
typer.Option("--project", file_okay=False, dir_okay=True, resolve_path=True,
|
|
87
|
+
help="Project directory (default: cwd)."),
|
|
88
|
+
] = _CWD,
|
|
89
|
+
global_: Annotated[
|
|
90
|
+
bool,
|
|
91
|
+
typer.Option("--global", help="Write to global config (~/.atomadic-forge/config.json)."),
|
|
92
|
+
] = False,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Set a single config key in the local (or --global) config file."""
|
|
95
|
+
if global_:
|
|
96
|
+
config_path = Path(GLOBAL_CONFIG_DIR).expanduser() / CONFIG_FILE_NAME
|
|
97
|
+
else:
|
|
98
|
+
config_path = project_dir.resolve() / LOCAL_CONFIG_DIR / CONFIG_FILE_NAME
|
|
99
|
+
|
|
100
|
+
current = read_config_file(config_path)
|
|
101
|
+
|
|
102
|
+
# Coerce obvious types so booleans and numbers round-trip cleanly.
|
|
103
|
+
coerced: object = value
|
|
104
|
+
if value.lower() in ("true", "false"):
|
|
105
|
+
coerced = value.lower() == "true"
|
|
106
|
+
else:
|
|
107
|
+
try:
|
|
108
|
+
coerced = float(value) if "." in value else int(value)
|
|
109
|
+
except ValueError:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
current[key] = coerced
|
|
113
|
+
save_config(current, config_path)
|
|
114
|
+
typer.echo(f" Set {key!r} = {coerced!r} → {config_path}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.command("test")
|
|
118
|
+
def test_cmd(
|
|
119
|
+
project_dir: Annotated[
|
|
120
|
+
Path,
|
|
121
|
+
typer.Option("--project", file_okay=False, dir_okay=True, resolve_path=True,
|
|
122
|
+
help="Project directory (default: cwd)."),
|
|
123
|
+
] = _CWD,
|
|
124
|
+
provider: Annotated[
|
|
125
|
+
str | None,
|
|
126
|
+
typer.Option("--provider", help="Override provider to test."),
|
|
127
|
+
] = None,
|
|
128
|
+
json_out: Annotated[bool, typer.Option("--json")] = False,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Test the configured LLM provider connection."""
|
|
131
|
+
config = load_config(project_dir.resolve())
|
|
132
|
+
p = provider or config.get("provider", "auto")
|
|
133
|
+
result = test_provider(p, config)
|
|
134
|
+
|
|
135
|
+
if json_out:
|
|
136
|
+
typer.echo(json.dumps(result, indent=2))
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
color = "green" if result["ok"] else "red"
|
|
140
|
+
typer.secho(f"\nProvider test — {p}", bold=True)
|
|
141
|
+
typer.secho(f" Status: {'OK' if result['ok'] else 'FAIL'}", fg=color)
|
|
142
|
+
typer.echo(f" Model: {result['model']}")
|
|
143
|
+
typer.echo(f" Latency: {result['latency_ms']}ms")
|
|
144
|
+
if result["error"]:
|
|
145
|
+
typer.secho(f" Error: {result['error']}", fg="yellow")
|