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.
Files changed (131) hide show
  1. atomadic_forge/__init__.py +12 -0
  2. atomadic_forge/__main__.py +5 -0
  3. atomadic_forge/a0_qk_constants/__init__.py +1 -0
  4. atomadic_forge/a0_qk_constants/agent_plan_schema.py +120 -0
  5. atomadic_forge/a0_qk_constants/commandsmith_types.py +49 -0
  6. atomadic_forge/a0_qk_constants/config_defaults.py +38 -0
  7. atomadic_forge/a0_qk_constants/emergent_types.py +77 -0
  8. atomadic_forge/a0_qk_constants/error_codes.py +296 -0
  9. atomadic_forge/a0_qk_constants/forge_types.py +89 -0
  10. atomadic_forge/a0_qk_constants/gen_language.py +116 -0
  11. atomadic_forge/a0_qk_constants/lang_extensions.py +150 -0
  12. atomadic_forge/a0_qk_constants/policy_schema.py +48 -0
  13. atomadic_forge/a0_qk_constants/receipt_schema.py +311 -0
  14. atomadic_forge/a0_qk_constants/roi_constants.py +96 -0
  15. atomadic_forge/a0_qk_constants/semantic_types.py +61 -0
  16. atomadic_forge/a0_qk_constants/sidecar_schema.py +81 -0
  17. atomadic_forge/a0_qk_constants/synergy_types.py +62 -0
  18. atomadic_forge/a0_qk_constants/tier_names.py +47 -0
  19. atomadic_forge/a1_at_functions/__init__.py +1 -0
  20. atomadic_forge/a1_at_functions/agent_context_pack.py +193 -0
  21. atomadic_forge/a1_at_functions/agent_memory.py +139 -0
  22. atomadic_forge/a1_at_functions/agent_plan_emitter.py +324 -0
  23. atomadic_forge/a1_at_functions/agent_summary.py +277 -0
  24. atomadic_forge/a1_at_functions/body_extractor.py +306 -0
  25. atomadic_forge/a1_at_functions/card_renderer.py +210 -0
  26. atomadic_forge/a1_at_functions/certify_checks.py +445 -0
  27. atomadic_forge/a1_at_functions/chat_context.py +170 -0
  28. atomadic_forge/a1_at_functions/cherry_pick.py +71 -0
  29. atomadic_forge/a1_at_functions/classify_tier.py +115 -0
  30. atomadic_forge/a1_at_functions/commandsmith_discover.py +167 -0
  31. atomadic_forge/a1_at_functions/commandsmith_render.py +267 -0
  32. atomadic_forge/a1_at_functions/compiler_feedback.py +94 -0
  33. atomadic_forge/a1_at_functions/compliance_checker.py +228 -0
  34. atomadic_forge/a1_at_functions/config_io.py +68 -0
  35. atomadic_forge/a1_at_functions/cs1_renderer.py +588 -0
  36. atomadic_forge/a1_at_functions/doc_synthesizer.py +205 -0
  37. atomadic_forge/a1_at_functions/emergent_compose.py +192 -0
  38. atomadic_forge/a1_at_functions/emergent_rank.py +116 -0
  39. atomadic_forge/a1_at_functions/emergent_signature_extract.py +242 -0
  40. atomadic_forge/a1_at_functions/emergent_synthesize.py +88 -0
  41. atomadic_forge/a1_at_functions/enforce_planner.py +208 -0
  42. atomadic_forge/a1_at_functions/error_hints.py +105 -0
  43. atomadic_forge/a1_at_functions/evolution_log.py +94 -0
  44. atomadic_forge/a1_at_functions/forge_feedback.py +433 -0
  45. atomadic_forge/a1_at_functions/generation_quality.py +322 -0
  46. atomadic_forge/a1_at_functions/import_repair.py +211 -0
  47. atomadic_forge/a1_at_functions/import_smoke.py +102 -0
  48. atomadic_forge/a1_at_functions/js_parser.py +539 -0
  49. atomadic_forge/a1_at_functions/lineage_chain.py +144 -0
  50. atomadic_forge/a1_at_functions/lineage_reader.py +107 -0
  51. atomadic_forge/a1_at_functions/llm_client.py +554 -0
  52. atomadic_forge/a1_at_functions/local_signer.py +134 -0
  53. atomadic_forge/a1_at_functions/lsp_protocol.py +379 -0
  54. atomadic_forge/a1_at_functions/manifest_diff.py +314 -0
  55. atomadic_forge/a1_at_functions/mcp_protocol.py +1066 -0
  56. atomadic_forge/a1_at_functions/patch_scorer.py +267 -0
  57. atomadic_forge/a1_at_functions/plan_adapter.py +75 -0
  58. atomadic_forge/a1_at_functions/policy_loader.py +107 -0
  59. atomadic_forge/a1_at_functions/preflight_change.py +227 -0
  60. atomadic_forge/a1_at_functions/progress_reporter.py +81 -0
  61. atomadic_forge/a1_at_functions/provider_detect.py +157 -0
  62. atomadic_forge/a1_at_functions/provider_resolver.py +48 -0
  63. atomadic_forge/a1_at_functions/receipt_emitter.py +291 -0
  64. atomadic_forge/a1_at_functions/recipes.py +186 -0
  65. atomadic_forge/a1_at_functions/repo_explainer.py +124 -0
  66. atomadic_forge/a1_at_functions/roi_calculator.py +265 -0
  67. atomadic_forge/a1_at_functions/rollback_planner.py +147 -0
  68. atomadic_forge/a1_at_functions/sbom_emitter.py +155 -0
  69. atomadic_forge/a1_at_functions/scaffold_js.py +55 -0
  70. atomadic_forge/a1_at_functions/scaffold_pyproject.py +62 -0
  71. atomadic_forge/a1_at_functions/scaffold_starter.py +94 -0
  72. atomadic_forge/a1_at_functions/scout_walk.py +309 -0
  73. atomadic_forge/a1_at_functions/sidecar_parser.py +161 -0
  74. atomadic_forge/a1_at_functions/sidecar_validator.py +202 -0
  75. atomadic_forge/a1_at_functions/stub_detector.py +158 -0
  76. atomadic_forge/a1_at_functions/synergy_detect.py +166 -0
  77. atomadic_forge/a1_at_functions/synergy_render.py +252 -0
  78. atomadic_forge/a1_at_functions/synergy_surface_extract.py +163 -0
  79. atomadic_forge/a1_at_functions/test_runner.py +196 -0
  80. atomadic_forge/a1_at_functions/test_selector.py +122 -0
  81. atomadic_forge/a1_at_functions/tier_init_rebuild.py +122 -0
  82. atomadic_forge/a1_at_functions/tool_composer.py +130 -0
  83. atomadic_forge/a1_at_functions/transcript_log.py +70 -0
  84. atomadic_forge/a1_at_functions/wire_check.py +260 -0
  85. atomadic_forge/a2_mo_composites/__init__.py +1 -0
  86. atomadic_forge/a2_mo_composites/lineage_chain_store.py +122 -0
  87. atomadic_forge/a2_mo_composites/manifest_store.py +46 -0
  88. atomadic_forge/a2_mo_composites/plan_store.py +164 -0
  89. atomadic_forge/a2_mo_composites/receipt_signer.py +231 -0
  90. atomadic_forge/a3_og_features/__init__.py +1 -0
  91. atomadic_forge/a3_og_features/commandsmith_feature.py +267 -0
  92. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/__init__.py +3 -0
  93. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a0_qk_constants/__init__.py +4 -0
  94. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a1_at_functions/__init__.py +14 -0
  95. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/conftest.py +10 -0
  96. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/test_mixed.py +18 -0
  97. atomadic_forge/a3_og_features/demo_runner.py +502 -0
  98. atomadic_forge/a3_og_features/emergent_feature.py +95 -0
  99. atomadic_forge/a3_og_features/emergent_pipeline_integration.py +154 -0
  100. atomadic_forge/a3_og_features/forge_enforce.py +107 -0
  101. atomadic_forge/a3_og_features/forge_evolve.py +176 -0
  102. atomadic_forge/a3_og_features/forge_loop.py +528 -0
  103. atomadic_forge/a3_og_features/forge_pipeline.py +295 -0
  104. atomadic_forge/a3_og_features/forge_plan_apply.py +222 -0
  105. atomadic_forge/a3_og_features/lsp_server.py +98 -0
  106. atomadic_forge/a3_og_features/mcp_server.py +160 -0
  107. atomadic_forge/a3_og_features/setup_wizard.py +337 -0
  108. atomadic_forge/a3_og_features/synergy_feature.py +65 -0
  109. atomadic_forge/a4_sy_orchestration/__init__.py +1 -0
  110. atomadic_forge/a4_sy_orchestration/cli.py +1284 -0
  111. atomadic_forge/commands/__init__.py +1 -0
  112. atomadic_forge/commands/_registry.py +36 -0
  113. atomadic_forge/commands/audit.py +142 -0
  114. atomadic_forge/commands/chat.py +133 -0
  115. atomadic_forge/commands/commandsmith.py +178 -0
  116. atomadic_forge/commands/config_cmd.py +145 -0
  117. atomadic_forge/commands/demo.py +142 -0
  118. atomadic_forge/commands/emergent.py +124 -0
  119. atomadic_forge/commands/emergent_then_synergy.py +70 -0
  120. atomadic_forge/commands/evolve.py +122 -0
  121. atomadic_forge/commands/evolve_then_iterate.py +70 -0
  122. atomadic_forge/commands/feature_then_emergent.py +111 -0
  123. atomadic_forge/commands/iterate.py +140 -0
  124. atomadic_forge/commands/synergy.py +96 -0
  125. atomadic_forge/commands/synergy_then_emergent.py +70 -0
  126. atomadic_forge-0.3.2.dist-info/METADATA +471 -0
  127. atomadic_forge-0.3.2.dist-info/RECORD +131 -0
  128. atomadic_forge-0.3.2.dist-info/WHEEL +5 -0
  129. atomadic_forge-0.3.2.dist-info/entry_points.txt +3 -0
  130. atomadic_forge-0.3.2.dist-info/licenses/LICENSE +15 -0
  131. 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")