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,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