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,154 @@
1
+ """Tier a3 — pipeline-agnostic Emergent Scan overlay.
2
+
3
+ Every ASS-ADE phase that walks a catalog (scout, cherry-pick, assimilate,
4
+ rebuild, enhance, evolve) can call :func:`emergent_overlay_for_path` to
5
+ surface composition candidates that single-symbol heuristics would miss.
6
+
7
+ The overlay is a thin adapter:
8
+
9
+ path → harvest_signatures → find_chains → rank_chains → top-N
10
+ → return as a list of EmergentCandidateCard plus a small summary
11
+
12
+ Output is shaped so it slots into existing report dicts under an
13
+ ``"emergent"`` key without disturbing the schema.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Iterable
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from ..a0_qk_constants.emergent_types import (
23
+ EmergentCandidateCard,
24
+ SymbolSignatureCard,
25
+ )
26
+ from ..a1_at_functions.emergent_compose import find_chains
27
+ from ..a1_at_functions.emergent_rank import rank_chains
28
+ from ..a1_at_functions.emergent_signature_extract import harvest_signatures
29
+
30
+ _CONFIG_DEFAULTS: dict[str, dict[str, Any]] = {
31
+ "scout": {"max_depth": 3, "top_n": 10, "domain_jump_required": True,
32
+ "require_pure": False, "score_floor": 30},
33
+ "cherry-pick": {"max_depth": 3, "top_n": 15, "domain_jump_required": True,
34
+ "require_pure": False, "score_floor": 0},
35
+ "assimilate": {"max_depth": 3, "top_n": 25, "domain_jump_required": True,
36
+ "require_pure": False, "score_floor": 30},
37
+ "rebuild": {"max_depth": 3, "top_n": 25, "domain_jump_required": True,
38
+ "require_pure": False, "score_floor": 30},
39
+ "enhance": {"max_depth": 4, "top_n": 20, "domain_jump_required": False,
40
+ "require_pure": False, "score_floor": 20},
41
+ "evolve": {"max_depth": 4, "top_n": 30, "domain_jump_required": True,
42
+ "require_pure": True, "score_floor": 50},
43
+ }
44
+
45
+
46
+ def _detect_package(src_root: Path) -> str:
47
+ """Find the package name under ``src_root/<package>/__init__.py``.
48
+
49
+ Returns the first directory containing an ``__init__.py``. Falls back
50
+ to ``"atomadic_forge"`` if nothing is found.
51
+ """
52
+ for child in sorted(src_root.iterdir()) if src_root.exists() else []:
53
+ if child.is_dir() and (child / "__init__.py").exists():
54
+ return child.name
55
+ return "atomadic_forge"
56
+
57
+
58
+ def _src_root_of(repo: Path) -> Path:
59
+ """Return ``<repo>/src`` if it exists, else ``repo``."""
60
+ src = repo / "src"
61
+ return src if src.exists() else repo
62
+
63
+
64
+ def emergent_overlay_for_path(
65
+ repo_root: str | Path,
66
+ *,
67
+ phase: str = "scout",
68
+ catalog: Iterable[SymbolSignatureCard] | None = None,
69
+ package: str | None = None,
70
+ ) -> dict[str, Any]:
71
+ """Run an emergent scan over a repo (or pre-harvested catalog) for a phase.
72
+
73
+ ``catalog`` lets phases that already harvested signatures pass them in
74
+ instead of re-walking the tree (assimilate / rebuild build the catalog
75
+ naturally as part of their flow).
76
+ """
77
+ cfg = dict(_CONFIG_DEFAULTS.get(phase, _CONFIG_DEFAULTS["scout"]))
78
+ repo = Path(repo_root).resolve()
79
+ src_root = _src_root_of(repo)
80
+ pkg = package or _detect_package(src_root)
81
+
82
+ if catalog is None:
83
+ catalog = harvest_signatures(src_root, pkg)
84
+ catalog_list: list[SymbolSignatureCard] = list(catalog)
85
+
86
+ chains = find_chains(
87
+ catalog_list,
88
+ max_depth=cfg["max_depth"],
89
+ max_chains=2_000,
90
+ require_pure=cfg["require_pure"],
91
+ domain_jump_required=cfg["domain_jump_required"],
92
+ )
93
+ candidates = rank_chains(chains, catalog=catalog_list, top_n=cfg["top_n"])
94
+ floor = cfg["score_floor"]
95
+ candidates = [c for c in candidates if c["score"] >= floor]
96
+
97
+ by_domain: dict[str, int] = {}
98
+ for c in catalog_list:
99
+ by_domain[c["domain"]] = by_domain.get(c["domain"], 0) + 1
100
+
101
+ return {
102
+ "schema_version": "atomadic-forge.emergent.overlay/v1",
103
+ "phase": phase,
104
+ "src_root": str(src_root),
105
+ "package": pkg,
106
+ "catalog_size": len(catalog_list),
107
+ "chain_count_considered": len(chains),
108
+ "candidates": candidates,
109
+ "config": cfg,
110
+ "domain_inventory": by_domain,
111
+ "summary_line": _summary_line(candidates, len(catalog_list), pkg, phase),
112
+ }
113
+
114
+
115
+ def _summary_line(candidates: list[EmergentCandidateCard],
116
+ catalog_size: int, package: str, phase: str) -> str:
117
+ if not candidates:
118
+ return (
119
+ f"emergent[{phase}]: 0 candidates over {package} ({catalog_size} symbols) — "
120
+ "no novel composition above score floor"
121
+ )
122
+ top = candidates[0]
123
+ return (
124
+ f"emergent[{phase}]: {len(candidates)} candidate(s) over {package} "
125
+ f"({catalog_size} symbols); top={top['name']} score={top['score']:.0f}"
126
+ )
127
+
128
+
129
+ def boost_cherry_pick_targets(
130
+ cherry_targets: list[dict[str, Any]],
131
+ *,
132
+ overlay: dict[str, Any],
133
+ boost: float = 0.1,
134
+ ) -> list[dict[str, Any]]:
135
+ """Re-score cherry-pick targets using emergent participation.
136
+
137
+ Each target gains ``boost`` per emergent chain it participates in.
138
+ The augmented list is returned; the input is not mutated.
139
+ """
140
+ chains = [c["chain"]["chain"] for c in overlay.get("candidates", [])]
141
+ flat = {q for chain in chains for q in chain}
142
+ out: list[dict[str, Any]] = []
143
+ for t in cherry_targets:
144
+ sym = t.get("symbol") or {}
145
+ qname = f"{sym.get('module','')}.{sym.get('qualname','')}"
146
+ bumped = dict(t)
147
+ if qname in flat:
148
+ current = float(bumped.get("confidence", 0.0))
149
+ bumped["confidence"] = min(1.0, current + boost)
150
+ bumped.setdefault("reasons", []).append(
151
+ "participates in emergent composition chain"
152
+ )
153
+ out.append(bumped)
154
+ return out
@@ -0,0 +1,107 @@
1
+ """Tier a3 — apply the ``forge enforce`` plan.
2
+
3
+ Composes a1.enforce_planner + a1.wire_check + filesystem moves into
4
+ one orchestrator that:
5
+ 1. runs scan_violations(suggest_repairs=True)
6
+ 2. plans actions via enforce_planner.plan_actions
7
+ 3. when --apply, executes each auto_apply action atomically:
8
+ * mkdir -p <dest_tier>/
9
+ * git-rename or shutil.move
10
+ * RE-RUN scan_violations to confirm violations actually
11
+ dropped; if violations rose, ROLL BACK the move and mark
12
+ the action 'rolled_back'.
13
+
14
+ a3 because: combines a1 helpers + filesystem state. No upward imports.
15
+ The CLI surface (``commands/enforce.py``) is the only thing that
16
+ depends on this module.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import shutil
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from ..a1_at_functions.enforce_planner import (
25
+ EnforceAction,
26
+ plan_actions,
27
+ summarize_plan,
28
+ )
29
+ from ..a1_at_functions.wire_check import scan_violations
30
+
31
+
32
+ def run_enforce(
33
+ package_root: Path,
34
+ *,
35
+ apply: bool = False,
36
+ dry_run_only: bool = False,
37
+ ) -> dict:
38
+ """Plan (and optionally apply) mechanical fixes for wire violations.
39
+
40
+ Returns a result dict that always includes:
41
+ schema_version — 'atomadic-forge.enforce/v1'
42
+ apply — whether file moves were attempted
43
+ pre_violations — violation_count before the run
44
+ post_violations — violation_count after (== pre when apply=False)
45
+ plan — summarize_plan() output (action list + counts)
46
+ applied — list of {action, status} per attempted action
47
+ rollbacks — list of action srcs that were rolled back
48
+
49
+ Caller is responsible for any version-control commit/discard. The
50
+ function never deletes, only moves; rollbacks restore the original
51
+ location.
52
+ """
53
+ package_root = Path(package_root).resolve()
54
+ pre = scan_violations(package_root, suggest_repairs=True)
55
+ actions = plan_actions(pre, package_root=package_root)
56
+
57
+ applied: list[dict[str, Any]] = []
58
+ rollbacks: list[str] = []
59
+
60
+ if apply and not dry_run_only:
61
+ for a in actions:
62
+ if not a.get("auto_apply"):
63
+ applied.append({"action": a, "status": "skipped"})
64
+ continue
65
+ try:
66
+ _apply_one(package_root, a)
67
+ except OSError as exc:
68
+ applied.append({"action": a, "status": "io_error",
69
+ "detail": str(exc)})
70
+ continue
71
+ # Confirm the fix actually reduced violations; otherwise roll back.
72
+ mid = scan_violations(package_root)
73
+ if mid["violation_count"] > pre["violation_count"]:
74
+ _rollback_one(package_root, a)
75
+ rollbacks.append(a["src"])
76
+ applied.append({"action": a, "status": "rolled_back"})
77
+ else:
78
+ applied.append({"action": a, "status": "applied"})
79
+
80
+ post = scan_violations(package_root) if apply else pre
81
+ return {
82
+ "schema_version": "atomadic-forge.enforce/v1",
83
+ "apply": apply,
84
+ "pre_violations": pre["violation_count"],
85
+ "post_violations": post["violation_count"],
86
+ "plan": summarize_plan(actions),
87
+ "applied": applied,
88
+ "rollbacks": rollbacks,
89
+ }
90
+
91
+
92
+ def _apply_one(package_root: Path, action: EnforceAction) -> None:
93
+ src = package_root / action["src"]
94
+ dest = package_root / action["dest"]
95
+ dest.parent.mkdir(parents=True, exist_ok=True)
96
+ if dest.exists():
97
+ raise OSError(f"destination already exists: {dest}")
98
+ shutil.move(str(src), str(dest))
99
+
100
+
101
+ def _rollback_one(package_root: Path, action: EnforceAction) -> None:
102
+ """Best-effort: move the file back if we still own its destination."""
103
+ src = package_root / action["src"]
104
+ dest = package_root / action["dest"]
105
+ if dest.exists() and not src.exists():
106
+ src.parent.mkdir(parents=True, exist_ok=True)
107
+ shutil.move(str(dest), str(src))
@@ -0,0 +1,176 @@
1
+ """Tier a3 — recursive self-improvement loop.
2
+
3
+ ``forge evolve`` runs N rounds of ``iterate``, each round using the GROWING
4
+ catalog as the seed for the next. Round 0 starts from the user's seed (or
5
+ empty); each accepted artifact joins the catalog; subsequent rounds see a
6
+ richer compositional context via emergent + reuse signals.
7
+
8
+ Honest scope: this is **narrow self-improvement within a defined search
9
+ space** — same shape as AlphaEvolve, AutoML-Zero, Voyager. It is NOT a
10
+ path to AGI. The catalog grows, compositions multiply, the LLM gets
11
+ richer feedback. Whether the OUTPUT improves is bounded by the underlying
12
+ LLM's quality and the realism of the certify/wire signals.
13
+
14
+ Convergence rules:
15
+ * Round halts when iterate returns ``converged=True`` AND no new symbols
16
+ were added vs. previous round (catalog stable).
17
+ * Hard cap: ``rounds`` parameter (default 5).
18
+ * Optional ``stop_on_regression``: halt if score drops between rounds.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import datetime as _dt
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from ..a0_qk_constants.gen_language import (
28
+ Language,
29
+ normalize_language,
30
+ pkg_root_for,
31
+ )
32
+ from ..a1_at_functions.evolution_log import append_evolve_run
33
+ from ..a1_at_functions.llm_client import LLMClient, resolve_default_client
34
+ from ..a1_at_functions.scout_walk import harvest_repo
35
+ from ..a2_mo_composites.manifest_store import ManifestStore
36
+ from .forge_loop import run_iterate
37
+
38
+
39
+ def run_evolve(
40
+ intent: str,
41
+ *,
42
+ output: Path,
43
+ package: str = "evolved",
44
+ seed_repo: Path | None = None,
45
+ llm: LLMClient | None = None,
46
+ rounds: int = 3,
47
+ iterations_per_round: int = 4,
48
+ target_score: float = 75.0,
49
+ stop_on_regression: bool = False,
50
+ stagnation_threshold: int = 3,
51
+ language: Language | str = "python",
52
+ ) -> dict[str, Any]:
53
+ """Recursive iterate.
54
+
55
+ The growing package itself becomes the seed for the next round, so the
56
+ LLM sees its own prior output as building blocks the next time around.
57
+
58
+ ``stagnation_threshold`` halts evolve when the score AND symbol count
59
+ are unchanged for that many consecutive rounds — saves tokens when the
60
+ LLM is stuck re-emitting the same files. Set to 0 to disable.
61
+
62
+ ``language`` is forwarded to ``run_iterate`` and determines the output
63
+ layout (``"python"`` → ``output/src/<package>/aN_*/*.py``,
64
+ ``"javascript"`` → ``output/<package>/aN_*/*.js``).
65
+ """
66
+ output = Path(output).resolve()
67
+ output.mkdir(parents=True, exist_ok=True)
68
+ llm = llm or resolve_default_client()
69
+ lang: Language = normalize_language(language)
70
+ pkg_root = output / pkg_root_for(lang, package)
71
+
72
+ rounds_log: list[dict[str, Any]] = []
73
+ last_symbol_count = 0
74
+ last_score = 0.0
75
+ current_seed = Path(seed_repo) if seed_repo else None
76
+ stagnant_streak = 0
77
+ halt_reason: str | None = None
78
+
79
+ for round_idx in range(rounds):
80
+ report = run_iterate(
81
+ intent,
82
+ output=output,
83
+ package=package,
84
+ seed_repo=current_seed,
85
+ llm=llm,
86
+ max_iterations=iterations_per_round,
87
+ target_score=target_score,
88
+ apply=True,
89
+ language=lang,
90
+ )
91
+ # Re-scout the generated package to grow the catalog.
92
+ catalog = harvest_repo(pkg_root) if pkg_root.exists() else {"symbols": [], "symbol_count": 0}
93
+ symbol_count = catalog["symbol_count"]
94
+ score = report.get("final_certify", {}).get("score", 0)
95
+ delta = symbol_count - last_symbol_count
96
+
97
+ round_record = {
98
+ "round": round_idx,
99
+ "iterations": report.get("iterations", 0),
100
+ "files_written": report.get("files_written_total", 0),
101
+ "symbol_count": symbol_count,
102
+ "delta_symbols": delta,
103
+ "score": score,
104
+ "delta_score": round(score - last_score, 1),
105
+ "wire_verdict": report.get("final_wire", {}).get("verdict", "?"),
106
+ "converged": report.get("converged", False),
107
+ }
108
+ rounds_log.append(round_record)
109
+
110
+ # Stagnation tracking — flat score AND flat catalog vs. previous round.
111
+ if round_idx > 0 and delta == 0 and abs(round_record["delta_score"]) < 0.5:
112
+ stagnant_streak += 1
113
+ else:
114
+ stagnant_streak = 0
115
+ round_record["stagnant_streak"] = stagnant_streak
116
+
117
+ # Convergence checks (in priority order)
118
+ if report.get("converged") and delta == 0:
119
+ halt_reason = "converged"
120
+ break
121
+ if stop_on_regression and round_idx > 0 and round_record["delta_score"] < 0:
122
+ halt_reason = "score regression"
123
+ round_record["halt_reason"] = halt_reason
124
+ break
125
+ if stagnation_threshold and stagnant_streak >= stagnation_threshold:
126
+ halt_reason = "stagnation"
127
+ round_record["halt_reason"] = halt_reason
128
+ break
129
+
130
+ # The growing package becomes the seed for the next round.
131
+ current_seed = pkg_root if pkg_root.exists() else current_seed
132
+ last_symbol_count = symbol_count
133
+ last_score = score
134
+
135
+ final_score = rounds_log[-1]["score"] if rounds_log else 0
136
+ final_symbols = rounds_log[-1]["symbol_count"] if rounds_log else 0
137
+
138
+ out: dict[str, Any] = {
139
+ "schema_version": "atomadic-forge.evolve/v1",
140
+ "intent": intent,
141
+ "package": package,
142
+ "language": lang,
143
+ "output_root": str(output),
144
+ "llm": llm.name,
145
+ "rounds": rounds_log,
146
+ "rounds_completed": len(rounds_log),
147
+ "rounds_requested": rounds,
148
+ "final_score": final_score,
149
+ "final_symbol_count": final_symbols,
150
+ "score_trajectory": [r["score"] for r in rounds_log],
151
+ "symbol_trajectory": [r["symbol_count"] for r in rounds_log],
152
+ "halt_reason": halt_reason or "rounds_exhausted",
153
+ "converged": rounds_log[-1].get("converged", False) if rounds_log else False,
154
+ "generated_at_utc": _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds"),
155
+ }
156
+ ManifestStore(output).save("evolve", out)
157
+
158
+ # Auto-append to the evolution log so every run is documented permanently.
159
+ try:
160
+ append_evolve_run(
161
+ project_root=output,
162
+ package=package,
163
+ intent=intent,
164
+ llm_name=llm.name,
165
+ rounds_completed=len(rounds_log),
166
+ score_trajectory=out["score_trajectory"],
167
+ final_score=final_score,
168
+ converged=out["converged"],
169
+ halt_reason=out["halt_reason"],
170
+ extra={"final_symbol_count": final_symbols,
171
+ "rounds_requested": rounds},
172
+ )
173
+ except Exception: # noqa: BLE001 — logging must never break the run
174
+ pass
175
+
176
+ return out