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,528 @@
|
|
|
1
|
+
"""Tier a3 — the LLM ↔ Forge iteration loop.
|
|
2
|
+
|
|
3
|
+
This is the headline pipeline: intent → LLM emits code → Forge enforces
|
|
4
|
+
tier law → structured feedback → LLM iterates → … → certify ≥ threshold.
|
|
5
|
+
|
|
6
|
+
Forge is the architecture substrate. An LLM (any provider) is the
|
|
7
|
+
generator. Together they produce **architecturally-coherent** code in a
|
|
8
|
+
controlled loop where the LLM is held accountable to a strict layered
|
|
9
|
+
discipline.
|
|
10
|
+
|
|
11
|
+
Public API:
|
|
12
|
+
run_iterate(intent, *, output, package, llm, ...) -> IterateReport
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import datetime as _dt
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from ..a0_qk_constants.gen_language import (
|
|
22
|
+
ALLOWED_FILE_EXTS,
|
|
23
|
+
EMITS_INIT_FILES,
|
|
24
|
+
EMITS_PYPROJECT,
|
|
25
|
+
Language,
|
|
26
|
+
normalize_language,
|
|
27
|
+
pkg_root_for,
|
|
28
|
+
)
|
|
29
|
+
from ..a0_qk_constants.tier_names import TIER_NAMES
|
|
30
|
+
from ..a1_at_functions.certify_checks import certify
|
|
31
|
+
from ..a1_at_functions.compiler_feedback import (
|
|
32
|
+
pack_compile_feedback,
|
|
33
|
+
should_fix_round,
|
|
34
|
+
)
|
|
35
|
+
from ..a1_at_functions.forge_feedback import (
|
|
36
|
+
compute_reuse_stats,
|
|
37
|
+
pack_feedback,
|
|
38
|
+
pack_initial_intent,
|
|
39
|
+
parse_files_from_response,
|
|
40
|
+
system_prompt,
|
|
41
|
+
)
|
|
42
|
+
from ..a1_at_functions.generation_quality import (
|
|
43
|
+
apply_docs_phase,
|
|
44
|
+
apply_docstring_phase,
|
|
45
|
+
apply_test_phase,
|
|
46
|
+
)
|
|
47
|
+
from ..a1_at_functions.import_smoke import import_smoke
|
|
48
|
+
from ..a1_at_functions.llm_client import LLMClient, resolve_default_client
|
|
49
|
+
from ..a1_at_functions.scaffold_js import render_js_readme, render_package_json
|
|
50
|
+
from ..a1_at_functions.scaffold_pyproject import render_pyproject
|
|
51
|
+
from ..a1_at_functions.scaffold_starter import (
|
|
52
|
+
render_gitignore,
|
|
53
|
+
render_readme,
|
|
54
|
+
render_tests_conftest,
|
|
55
|
+
render_tests_init,
|
|
56
|
+
)
|
|
57
|
+
from ..a1_at_functions.scout_walk import harvest_repo
|
|
58
|
+
from ..a1_at_functions.tier_init_rebuild import rebuild_tier_inits
|
|
59
|
+
from ..a1_at_functions.transcript_log import TranscriptLog
|
|
60
|
+
from ..a1_at_functions.wire_check import scan_violations
|
|
61
|
+
from ..a2_mo_composites.manifest_store import ManifestStore
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
from .emergent_pipeline_integration import emergent_overlay_for_path
|
|
65
|
+
_HAS_EMERGENT = True
|
|
66
|
+
except Exception: # noqa: BLE001
|
|
67
|
+
_HAS_EMERGENT = False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _scaffold_package(output: Path, package: str, *,
|
|
71
|
+
intent: str = "",
|
|
72
|
+
language: Language = "python") -> Path:
|
|
73
|
+
"""Scaffold the surrounding package skeleton for the LLM to fill in.
|
|
74
|
+
|
|
75
|
+
For Python, this is a complete pip-installable layout: 5 tier
|
|
76
|
+
directories with ``__init__.py``s, a real ``pyproject.toml`` rooted
|
|
77
|
+
at ``output/``, ``README.md``, ``.gitignore``, and ``tests/`` with a
|
|
78
|
+
conftest that adds ``src/`` to ``sys.path``. After this runs,
|
|
79
|
+
``pip install -e <output>`` works.
|
|
80
|
+
|
|
81
|
+
For JavaScript / TypeScript, the layout is simpler: 5 tier
|
|
82
|
+
directories at ``output/<package>/`` (no ``src/`` wrapper), a
|
|
83
|
+
minimal ``package.json`` with ``"type": "module"``, and a README.
|
|
84
|
+
No ``__init__.py`` (ES modules don't need one), no test framework
|
|
85
|
+
config (Forge doesn't currently run JS tests in certify).
|
|
86
|
+
"""
|
|
87
|
+
pkg_root = output / pkg_root_for(language, package)
|
|
88
|
+
for tier in TIER_NAMES:
|
|
89
|
+
d = pkg_root / tier
|
|
90
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
if EMITS_INIT_FILES[language]:
|
|
92
|
+
(d / "__init__.py").write_text(
|
|
93
|
+
f'"""{tier}."""\n', encoding="utf-8"
|
|
94
|
+
)
|
|
95
|
+
if EMITS_INIT_FILES[language]:
|
|
96
|
+
(pkg_root / "__init__.py").write_text(
|
|
97
|
+
f'"""{package} — generated by atomadic-forge iterate."""\n'
|
|
98
|
+
f'__version__ = "0.0.1"\n',
|
|
99
|
+
encoding="utf-8",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Top-level scaffolds — only write if absent so re-runs don't clobber
|
|
103
|
+
# an LLM-improved README.
|
|
104
|
+
if EMITS_PYPROJECT[language]:
|
|
105
|
+
pyproject = output / "pyproject.toml"
|
|
106
|
+
if not pyproject.exists():
|
|
107
|
+
pyproject.write_text(
|
|
108
|
+
render_pyproject(
|
|
109
|
+
package=package,
|
|
110
|
+
description=(intent or "")[:120].strip() or f"{package} package",
|
|
111
|
+
console_script_target="a4_sy_orchestration.cli:main",
|
|
112
|
+
),
|
|
113
|
+
encoding="utf-8",
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
# JS/TS: minimal package.json with type: module so ES6 imports resolve.
|
|
117
|
+
pj = output / "package.json"
|
|
118
|
+
if not pj.exists():
|
|
119
|
+
pj.write_text(
|
|
120
|
+
render_package_json(
|
|
121
|
+
package=package,
|
|
122
|
+
description=(intent or "")[:120].strip(),
|
|
123
|
+
language=language,
|
|
124
|
+
),
|
|
125
|
+
encoding="utf-8",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
readme = output / "README.md"
|
|
129
|
+
if not readme.exists():
|
|
130
|
+
if language == "python":
|
|
131
|
+
readme.write_text(
|
|
132
|
+
render_readme(package=package, intent=intent or "(unspecified)"),
|
|
133
|
+
encoding="utf-8",
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
readme.write_text(
|
|
137
|
+
render_js_readme(package=package,
|
|
138
|
+
intent=intent or "(unspecified)",
|
|
139
|
+
language=language),
|
|
140
|
+
encoding="utf-8",
|
|
141
|
+
)
|
|
142
|
+
gi = output / ".gitignore"
|
|
143
|
+
if not gi.exists():
|
|
144
|
+
gi.write_text(render_gitignore(), encoding="utf-8")
|
|
145
|
+
|
|
146
|
+
# Python-only test scaffolding. JS/TS test runners (vitest/jest/node:test)
|
|
147
|
+
# don't need a conftest.py; that's a 0.3 roadmap item.
|
|
148
|
+
if language == "python":
|
|
149
|
+
tests_dir = output / "tests"
|
|
150
|
+
tests_dir.mkdir(exist_ok=True)
|
|
151
|
+
init = tests_dir / "__init__.py"
|
|
152
|
+
if not init.exists():
|
|
153
|
+
init.write_text(render_tests_init(), encoding="utf-8")
|
|
154
|
+
cf = tests_dir / "conftest.py"
|
|
155
|
+
if not cf.exists():
|
|
156
|
+
cf.write_text(render_tests_conftest(package=package), encoding="utf-8")
|
|
157
|
+
|
|
158
|
+
return pkg_root
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
_PATH_REJECT_CHARS = ("<", ">", "|", "*", "?", '"', "\x00")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _safe_path(rel: str, language: Language = "python") -> bool:
|
|
165
|
+
"""Reject placeholder/metachar/traversal paths the LLM may have emitted.
|
|
166
|
+
|
|
167
|
+
Small models often echo template tokens like ``<pkg>`` or ``${name}``
|
|
168
|
+
verbatim instead of substituting. We refuse those instead of crashing
|
|
169
|
+
on Windows or writing garbage files.
|
|
170
|
+
|
|
171
|
+
File-suffix allow-list is language-aware: a Python evolve refuses
|
|
172
|
+
LLM-emitted ``.js`` files (and vice-versa) so the output tree stays
|
|
173
|
+
monolingual. ``.md`` is allowed in every language.
|
|
174
|
+
"""
|
|
175
|
+
rel = rel.strip()
|
|
176
|
+
if not rel:
|
|
177
|
+
return False
|
|
178
|
+
if any(ch in rel for ch in _PATH_REJECT_CHARS):
|
|
179
|
+
return False
|
|
180
|
+
if ".." in rel.replace("\\", "/").split("/"):
|
|
181
|
+
return False
|
|
182
|
+
rel_lower = rel.lower()
|
|
183
|
+
allowed = ALLOWED_FILE_EXTS[language]
|
|
184
|
+
if not any(rel_lower.endswith(ext) for ext in allowed):
|
|
185
|
+
return False
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _write_files(output: Path, files: list[dict[str, str]],
|
|
190
|
+
language: Language = "python") -> list[str]:
|
|
191
|
+
written: list[str] = []
|
|
192
|
+
output_resolved = output.resolve()
|
|
193
|
+
for f in files:
|
|
194
|
+
rel = f.get("path", "").lstrip("/\\")
|
|
195
|
+
if not _safe_path(rel, language=language):
|
|
196
|
+
continue
|
|
197
|
+
try:
|
|
198
|
+
target = (output / rel).resolve()
|
|
199
|
+
target.relative_to(output_resolved) # prompt-injection / traversal guard
|
|
200
|
+
except (ValueError, OSError):
|
|
201
|
+
continue
|
|
202
|
+
try:
|
|
203
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
target.write_text(f.get("content", ""), encoding="utf-8")
|
|
205
|
+
except OSError:
|
|
206
|
+
continue
|
|
207
|
+
written.append(str(target.relative_to(output_resolved).as_posix()))
|
|
208
|
+
return written
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _run_fix_rounds(
|
|
212
|
+
*,
|
|
213
|
+
pkg_root: Path,
|
|
214
|
+
output: Path,
|
|
215
|
+
package: str,
|
|
216
|
+
language: Language,
|
|
217
|
+
llm: LLMClient,
|
|
218
|
+
sys_prompt: str,
|
|
219
|
+
max_fix_rounds: int,
|
|
220
|
+
transcript: TranscriptLog | None,
|
|
221
|
+
turn: int,
|
|
222
|
+
) -> list[dict[str, Any]]:
|
|
223
|
+
"""Run up to ``max_fix_rounds`` import-smoke fix rounds.
|
|
224
|
+
|
|
225
|
+
Each round:
|
|
226
|
+
1. Run import_smoke against the just-written package.
|
|
227
|
+
2. If importable → return what we did so far; the regular turn
|
|
228
|
+
continues.
|
|
229
|
+
3. Otherwise pack a fix-round prompt with the error trace and
|
|
230
|
+
re-call the LLM, write the response files, and loop.
|
|
231
|
+
|
|
232
|
+
Lane A W3 contract — the fix-round NEVER scores quality; it only
|
|
233
|
+
asks for the minimum patch to make the package import. This keeps
|
|
234
|
+
the regular --max-iterations budget for actual quality work.
|
|
235
|
+
"""
|
|
236
|
+
rounds: list[dict[str, Any]] = []
|
|
237
|
+
if max_fix_rounds < 1 or language != "python":
|
|
238
|
+
# Smoke is Python-only; JS/TS skip fix rounds today.
|
|
239
|
+
return rounds
|
|
240
|
+
for attempt in range(1, max_fix_rounds + 1):
|
|
241
|
+
smoke = import_smoke(output_root=output, package=package)
|
|
242
|
+
if smoke.get("importable"):
|
|
243
|
+
return rounds
|
|
244
|
+
if not should_fix_round(smoke):
|
|
245
|
+
return rounds
|
|
246
|
+
prompt = pack_compile_feedback(
|
|
247
|
+
smoke, package=package,
|
|
248
|
+
fix_round_index=attempt,
|
|
249
|
+
max_fix_rounds=max_fix_rounds,
|
|
250
|
+
)
|
|
251
|
+
if transcript:
|
|
252
|
+
transcript.append(
|
|
253
|
+
"prompt", prompt, role="user", llm=llm.name,
|
|
254
|
+
extra={"turn": turn, "phase": "fix-round",
|
|
255
|
+
"fix_round": attempt},
|
|
256
|
+
)
|
|
257
|
+
response = llm.call(prompt, system=sys_prompt)
|
|
258
|
+
if transcript:
|
|
259
|
+
transcript.append(
|
|
260
|
+
"response", response, role="assistant", llm=llm.name,
|
|
261
|
+
extra={"turn": turn, "phase": "fix-round",
|
|
262
|
+
"fix_round": attempt},
|
|
263
|
+
)
|
|
264
|
+
files = parse_files_from_response(response)
|
|
265
|
+
# The contract said: emit `[]` to signal 'env error, give up'.
|
|
266
|
+
if not files and response.strip().endswith("[]"):
|
|
267
|
+
rounds.append({
|
|
268
|
+
"fix_round": attempt,
|
|
269
|
+
"files_written": [],
|
|
270
|
+
"halted_reason": "llm_signaled_environment_error",
|
|
271
|
+
"smoke_error_kind": smoke.get("error_kind"),
|
|
272
|
+
})
|
|
273
|
+
return rounds
|
|
274
|
+
written = _write_files(output, files, language=language)
|
|
275
|
+
if pkg_root.exists() and EMITS_INIT_FILES[language]:
|
|
276
|
+
rebuild_tier_inits(pkg_root)
|
|
277
|
+
rounds.append({
|
|
278
|
+
"fix_round": attempt,
|
|
279
|
+
"files_written": written,
|
|
280
|
+
"smoke_error_kind": smoke.get("error_kind"),
|
|
281
|
+
"smoke_error_message": smoke.get("error_message"),
|
|
282
|
+
})
|
|
283
|
+
return rounds
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def run_iterate(
|
|
287
|
+
intent: str,
|
|
288
|
+
*,
|
|
289
|
+
output: Path,
|
|
290
|
+
package: str = "generated",
|
|
291
|
+
seed_repo: Path | list[Path] | None = None,
|
|
292
|
+
llm: LLMClient | None = None,
|
|
293
|
+
max_iterations: int = 5,
|
|
294
|
+
max_fix_rounds: int = 0,
|
|
295
|
+
target_score: float = 75.0,
|
|
296
|
+
apply: bool = True,
|
|
297
|
+
language: Language | str = "python",
|
|
298
|
+
) -> dict[str, Any]:
|
|
299
|
+
"""Run the LLM ↔ Forge loop.
|
|
300
|
+
|
|
301
|
+
Termination: certify ≥ ``target_score`` AND wire PASS, OR ``max_iterations``
|
|
302
|
+
exhausted, OR LLM emits an empty file list (signals "done").
|
|
303
|
+
|
|
304
|
+
``apply=False`` returns the planned prompt + initial response without
|
|
305
|
+
writing files (useful for sanity-checking the prompt shape).
|
|
306
|
+
|
|
307
|
+
``language`` selects the target output language: ``"python"`` (default,
|
|
308
|
+
pip-installable layout under ``src/<package>/``), ``"javascript"`` or
|
|
309
|
+
``"typescript"`` (ES-module layout under ``<package>/``).
|
|
310
|
+
"""
|
|
311
|
+
output = Path(output).resolve()
|
|
312
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
313
|
+
llm = llm or resolve_default_client()
|
|
314
|
+
lang: Language = normalize_language(language)
|
|
315
|
+
pkg_root = (_scaffold_package(output, package, intent=intent, language=lang)
|
|
316
|
+
if apply else output / pkg_root_for(lang, package))
|
|
317
|
+
transcript = TranscriptLog(output) if apply else None
|
|
318
|
+
|
|
319
|
+
# Optional seed catalog from one or more sibling repos.
|
|
320
|
+
seed_catalog: list[dict] = []
|
|
321
|
+
seed_repos = ([seed_repo] if isinstance(seed_repo, Path) else
|
|
322
|
+
(list(seed_repo) if seed_repo else []))
|
|
323
|
+
for sr in seed_repos:
|
|
324
|
+
sr = Path(sr)
|
|
325
|
+
if sr.exists():
|
|
326
|
+
scout = harvest_repo(sr)
|
|
327
|
+
seed_catalog.extend(scout.get("symbols", []))
|
|
328
|
+
|
|
329
|
+
turn_log: list[dict[str, Any]] = []
|
|
330
|
+
history_files: list[str] = []
|
|
331
|
+
sys_prompt = system_prompt(language=lang)
|
|
332
|
+
|
|
333
|
+
# ── Turn 0: initial intent ────────────────────────────────────────────
|
|
334
|
+
prompt = pack_initial_intent(intent, package=package,
|
|
335
|
+
seed_catalog=seed_catalog, language=lang)
|
|
336
|
+
if not apply:
|
|
337
|
+
return {
|
|
338
|
+
"schema_version": "atomadic-forge.iterate/v1",
|
|
339
|
+
"applied": False,
|
|
340
|
+
"package": package,
|
|
341
|
+
"language": lang,
|
|
342
|
+
"system_prompt": sys_prompt,
|
|
343
|
+
"first_prompt": prompt,
|
|
344
|
+
"llm": llm.name,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if transcript:
|
|
348
|
+
transcript.append("system", sys_prompt, role="system", llm=llm.name)
|
|
349
|
+
transcript.append("prompt", prompt, role="user", llm=llm.name,
|
|
350
|
+
extra={"turn": 0, "phase": "initial"})
|
|
351
|
+
response = llm.call(prompt, system=sys_prompt)
|
|
352
|
+
if transcript:
|
|
353
|
+
transcript.append("response", response, role="assistant",
|
|
354
|
+
llm=llm.name, extra={"turn": 0})
|
|
355
|
+
files = parse_files_from_response(response)
|
|
356
|
+
parse_retried = False
|
|
357
|
+
if not files and response.strip() and not response.strip().endswith("[]"):
|
|
358
|
+
retry_prompt = (
|
|
359
|
+
"Your previous response did not parse as a JSON array of "
|
|
360
|
+
"`{path, content}` objects. Output ONLY the JSON array "
|
|
361
|
+
"literal, no prose, no markdown fences, no comments. If you "
|
|
362
|
+
"need to indicate completion, output the exact two characters "
|
|
363
|
+
"`[]`.\n\n"
|
|
364
|
+
"Re-emit your output now."
|
|
365
|
+
)
|
|
366
|
+
if transcript:
|
|
367
|
+
transcript.append("prompt", retry_prompt, role="user",
|
|
368
|
+
llm=llm.name,
|
|
369
|
+
extra={"turn": 0, "phase": "parse-retry"})
|
|
370
|
+
response = llm.call(retry_prompt, system=sys_prompt)
|
|
371
|
+
if transcript:
|
|
372
|
+
transcript.append("response", response, role="assistant",
|
|
373
|
+
llm=llm.name,
|
|
374
|
+
extra={"turn": 0, "phase": "parse-retry"})
|
|
375
|
+
files = parse_files_from_response(response)
|
|
376
|
+
parse_retried = True
|
|
377
|
+
written = _write_files(output, files, language=lang)
|
|
378
|
+
if pkg_root.exists() and EMITS_INIT_FILES[lang]:
|
|
379
|
+
rebuild_tier_inits(pkg_root)
|
|
380
|
+
history_files.extend(written)
|
|
381
|
+
turn_log.append({"turn": 0, "prompt": prompt, "files_written": written,
|
|
382
|
+
"raw_response_chars": len(response),
|
|
383
|
+
"parse_retried": parse_retried})
|
|
384
|
+
|
|
385
|
+
# ── Lane A W3 — Compiler Feedback Loop, after turn 0 ────────────────
|
|
386
|
+
fix_rounds_log: list[dict[str, Any]] = []
|
|
387
|
+
if max_fix_rounds > 0 and apply:
|
|
388
|
+
rounds = _run_fix_rounds(
|
|
389
|
+
pkg_root=pkg_root, output=output, package=package,
|
|
390
|
+
language=lang, llm=llm, sys_prompt=sys_prompt,
|
|
391
|
+
max_fix_rounds=max_fix_rounds, transcript=transcript, turn=0,
|
|
392
|
+
)
|
|
393
|
+
for r in rounds:
|
|
394
|
+
history_files.extend(r.get("files_written", []))
|
|
395
|
+
fix_rounds_log.extend({**r, "turn": 0} for r in rounds)
|
|
396
|
+
|
|
397
|
+
# ── Iteration turns ────────────────────────────────────────────────────
|
|
398
|
+
final_wire: dict[str, Any] | None = None
|
|
399
|
+
final_cert: dict[str, Any] | None = None
|
|
400
|
+
converged = False
|
|
401
|
+
|
|
402
|
+
for turn in range(1, max_iterations + 1):
|
|
403
|
+
wire = scan_violations(pkg_root)
|
|
404
|
+
cert = certify(output, project=package, package=package)
|
|
405
|
+
# Walk the *generated* package to compute reuse + emergent context.
|
|
406
|
+
emitted_scout = harvest_repo(pkg_root) if pkg_root.exists() else None
|
|
407
|
+
reuse = compute_reuse_stats(emitted_scout, seed_catalog)
|
|
408
|
+
emergent: dict[str, Any] | None = None
|
|
409
|
+
if _HAS_EMERGENT and pkg_root.exists():
|
|
410
|
+
try:
|
|
411
|
+
emergent = emergent_overlay_for_path(
|
|
412
|
+
output, phase="iterate", package=package,
|
|
413
|
+
)
|
|
414
|
+
except Exception: # noqa: BLE001
|
|
415
|
+
emergent = None
|
|
416
|
+
final_wire, final_cert = wire, cert
|
|
417
|
+
|
|
418
|
+
if wire["verdict"] == "PASS" and cert["score"] >= target_score:
|
|
419
|
+
converged = True
|
|
420
|
+
break
|
|
421
|
+
|
|
422
|
+
feedback = pack_feedback(wire_report=wire, certify_report=cert,
|
|
423
|
+
emergent_overlay=emergent,
|
|
424
|
+
reuse_stats=reuse,
|
|
425
|
+
iteration=turn)
|
|
426
|
+
if transcript:
|
|
427
|
+
transcript.append("prompt", feedback, role="user", llm=llm.name,
|
|
428
|
+
extra={"turn": turn, "phase": "iterate"})
|
|
429
|
+
response = llm.call(feedback, system=sys_prompt)
|
|
430
|
+
if transcript:
|
|
431
|
+
transcript.append("response", response, role="assistant",
|
|
432
|
+
llm=llm.name, extra={"turn": turn})
|
|
433
|
+
files = parse_files_from_response(response)
|
|
434
|
+
if not files:
|
|
435
|
+
turn_log.append({"turn": turn, "prompt_chars": len(feedback),
|
|
436
|
+
"files_written": [], "signal": "llm_done"})
|
|
437
|
+
break
|
|
438
|
+
written = _write_files(output, files, language=lang)
|
|
439
|
+
if pkg_root.exists() and EMITS_INIT_FILES[lang]:
|
|
440
|
+
rebuild_tier_inits(pkg_root)
|
|
441
|
+
history_files.extend(written)
|
|
442
|
+
turn_log.append({"turn": turn, "prompt_chars": len(feedback),
|
|
443
|
+
"files_written": written,
|
|
444
|
+
"raw_response_chars": len(response)})
|
|
445
|
+
# Lane A W3 — fix-round budget per turn.
|
|
446
|
+
if max_fix_rounds > 0:
|
|
447
|
+
rounds = _run_fix_rounds(
|
|
448
|
+
pkg_root=pkg_root, output=output, package=package,
|
|
449
|
+
language=lang, llm=llm, sys_prompt=sys_prompt,
|
|
450
|
+
max_fix_rounds=max_fix_rounds, transcript=transcript,
|
|
451
|
+
turn=turn,
|
|
452
|
+
)
|
|
453
|
+
for r in rounds:
|
|
454
|
+
history_files.extend(r.get("files_written", []))
|
|
455
|
+
fix_rounds_log.extend({**r, "turn": turn} for r in rounds)
|
|
456
|
+
|
|
457
|
+
quality_phases: list[dict[str, Any]] = []
|
|
458
|
+
if lang == "python":
|
|
459
|
+
docstrings = apply_docstring_phase(pkg_root)
|
|
460
|
+
quality_phases.append(docstrings)
|
|
461
|
+
for rel in docstrings.get("files_changed", []):
|
|
462
|
+
try:
|
|
463
|
+
history_files.append(str((pkg_root / rel).relative_to(output).as_posix()))
|
|
464
|
+
except ValueError:
|
|
465
|
+
pass
|
|
466
|
+
if docstrings.get("files_changed") and pkg_root.exists():
|
|
467
|
+
rebuild_tier_inits(pkg_root)
|
|
468
|
+
|
|
469
|
+
docs = apply_docs_phase(
|
|
470
|
+
output_root=output,
|
|
471
|
+
package_root=pkg_root,
|
|
472
|
+
package=package,
|
|
473
|
+
intent=intent,
|
|
474
|
+
)
|
|
475
|
+
quality_phases.append(docs)
|
|
476
|
+
history_files.extend(docs.get("files_written", []))
|
|
477
|
+
|
|
478
|
+
tests = apply_test_phase(
|
|
479
|
+
output_root=output,
|
|
480
|
+
package_root=pkg_root,
|
|
481
|
+
package=package,
|
|
482
|
+
)
|
|
483
|
+
quality_phases.append(tests)
|
|
484
|
+
history_files.extend(tests.get("files_written", []))
|
|
485
|
+
|
|
486
|
+
final_wire = scan_violations(pkg_root)
|
|
487
|
+
final_cert = certify(output, project=package, package=package)
|
|
488
|
+
else:
|
|
489
|
+
quality_phases.append({
|
|
490
|
+
"phase": "quality",
|
|
491
|
+
"language": lang,
|
|
492
|
+
"skipped": "deterministic doc/test phase is Python-only today",
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
ManifestStore(output).save("quality", {
|
|
496
|
+
"schema_version": "atomadic-forge.quality/v1",
|
|
497
|
+
"package": package,
|
|
498
|
+
"language": lang,
|
|
499
|
+
"phases": quality_phases,
|
|
500
|
+
"generated_at_utc": _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds"),
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
report: dict[str, Any] = {
|
|
504
|
+
"schema_version": "atomadic-forge.iterate/v1",
|
|
505
|
+
"applied": True,
|
|
506
|
+
"intent": intent,
|
|
507
|
+
"package": package,
|
|
508
|
+
"language": lang,
|
|
509
|
+
"output_root": str(output),
|
|
510
|
+
"llm": llm.name,
|
|
511
|
+
"iterations": len(turn_log),
|
|
512
|
+
"files_written_total": len(set(history_files)),
|
|
513
|
+
"converged": converged,
|
|
514
|
+
"quality_phases": quality_phases,
|
|
515
|
+
"final_wire": final_wire,
|
|
516
|
+
"final_certify": {
|
|
517
|
+
"score": (final_cert or {}).get("score", 0),
|
|
518
|
+
"issues": (final_cert or {}).get("issues", []),
|
|
519
|
+
},
|
|
520
|
+
"transcript": turn_log,
|
|
521
|
+
"fix_rounds": fix_rounds_log,
|
|
522
|
+
"fix_round_count": len(fix_rounds_log),
|
|
523
|
+
"max_fix_rounds": max_fix_rounds,
|
|
524
|
+
"transcript_log_path": (str(transcript.path) if transcript else None),
|
|
525
|
+
"generated_at_utc": _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds"),
|
|
526
|
+
}
|
|
527
|
+
ManifestStore(output).save("iterate", report)
|
|
528
|
+
return report
|