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,295 @@
|
|
|
1
|
+
"""Tier a3 — Forge feature orchestrators.
|
|
2
|
+
|
|
3
|
+
These wire the a1 helpers + a2 composites into the user-facing verbs:
|
|
4
|
+
``recon`` (scout + emergent overlay), ``cherry`` (cherry-pick),
|
|
5
|
+
``finalize`` (assimilate + wire + certify), and ``auto`` (the whole chain).
|
|
6
|
+
|
|
7
|
+
Every entry point returns a structured dict so downstream tools can pipe
|
|
8
|
+
output reliably. When ``apply=False`` the pipeline runs in dry-run mode —
|
|
9
|
+
nothing is written to disk except into the project's ``.atomadic-forge/``
|
|
10
|
+
manifest store.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
import shutil
|
|
18
|
+
from collections.abc import Callable, Iterable
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from .. import __version__
|
|
23
|
+
from ..a0_qk_constants.tier_names import TIER_NAMES
|
|
24
|
+
from ..a1_at_functions.agent_plan_emitter import emit_agent_plan
|
|
25
|
+
from ..a1_at_functions.certify_checks import certify
|
|
26
|
+
from ..a1_at_functions.cherry_pick import select_items
|
|
27
|
+
from ..a1_at_functions.scout_walk import harvest_repo
|
|
28
|
+
from ..a1_at_functions.wire_check import scan_violations
|
|
29
|
+
from ..a2_mo_composites.manifest_store import ManifestStore
|
|
30
|
+
|
|
31
|
+
_STATUS_TEMPLATE = """# Atomadic Forge — Assimilation Status
|
|
32
|
+
|
|
33
|
+
This directory was produced by ``forge auto`` / ``forge finalize``. It is
|
|
34
|
+
**bootstrapped material**, not a finished product.
|
|
35
|
+
|
|
36
|
+
## What's here
|
|
37
|
+
- 5-tier monadic layout (``a0_qk_constants/`` … ``a4_sy_orchestration/``)
|
|
38
|
+
- Symbols ingested from {source_count} source repo(s)
|
|
39
|
+
- {component_count} components emitted
|
|
40
|
+
- Digest: ``{digest}``
|
|
41
|
+
|
|
42
|
+
## What's still required before shipping
|
|
43
|
+
1. **Integration tests** against real inputs — the bootstrap copies code
|
|
44
|
+
verbatim where possible, but cross-symbol semantics (two ``User`` classes,
|
|
45
|
+
two ``authenticate`` flows) need human reconciliation.
|
|
46
|
+
2. **Runtime configuration** — secrets, environment variables, DB URLs.
|
|
47
|
+
3. **Observability** — logging, metrics, error reporting.
|
|
48
|
+
4. **Wire enforcement** — run ``forge wire`` and address any violations.
|
|
49
|
+
5. **Certification** — ``forge certify`` should hit ≥ 75 before public use.
|
|
50
|
+
|
|
51
|
+
## Provenance
|
|
52
|
+
Source repos: {source_list}
|
|
53
|
+
|
|
54
|
+
Generated by atomadic-forge {version}.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _current_version() -> str:
|
|
59
|
+
"""Return the source version for generated artifacts."""
|
|
60
|
+
return __version__
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _target_file_for_symbol(target_dir: Path, slug: str, source_file: Path) -> Path:
|
|
64
|
+
"""Keep absorbed source in its original language extension."""
|
|
65
|
+
suffix = source_file.suffix.lower() or ".py"
|
|
66
|
+
return target_dir / f"{slug}{suffix}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def run_recon(
|
|
70
|
+
target: Path,
|
|
71
|
+
*,
|
|
72
|
+
store_manifest: bool = True,
|
|
73
|
+
progress: Callable[[int, int, str], None] | None = None,
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
"""``forge recon`` — scout walk + persist scout.json under .atomadic-forge/.
|
|
76
|
+
|
|
77
|
+
``progress`` (optional): forwarded to ``harvest_repo`` for per-file
|
|
78
|
+
reporting on big repos. The CLI builds a stderr reporter; library
|
|
79
|
+
callers can pass any callback or ``None``.
|
|
80
|
+
"""
|
|
81
|
+
target = Path(target).resolve()
|
|
82
|
+
report = harvest_repo(target, progress=progress)
|
|
83
|
+
if store_manifest:
|
|
84
|
+
ManifestStore(target).save("scout", report)
|
|
85
|
+
return report
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def run_cherry(target: Path, *, names: Iterable[str] | None = None,
|
|
89
|
+
pick_all: bool = False, only_tier: str | None = None,
|
|
90
|
+
store_manifest: bool = True,
|
|
91
|
+
progress: Callable[[int, int, str], None] | None = None,
|
|
92
|
+
) -> dict[str, Any]:
|
|
93
|
+
"""``forge cherry`` — derive a cherry-pick manifest from the latest scout."""
|
|
94
|
+
target = Path(target).resolve()
|
|
95
|
+
store = ManifestStore(target)
|
|
96
|
+
scout = store.load("scout") or harvest_repo(target, progress=progress)
|
|
97
|
+
if not store.load("scout") and store_manifest:
|
|
98
|
+
store.save("scout", scout)
|
|
99
|
+
manifest = select_items(scout, names=names, pick_all=pick_all,
|
|
100
|
+
only_tier=only_tier)
|
|
101
|
+
if store_manifest:
|
|
102
|
+
store.save("cherry", manifest)
|
|
103
|
+
return manifest
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def run_finalize(*, target: Path, output: Path, package: str = "absorbed",
|
|
107
|
+
cherry_manifest: dict[str, Any] | None = None,
|
|
108
|
+
apply: bool = False,
|
|
109
|
+
on_conflict: str = "rename") -> dict[str, Any]:
|
|
110
|
+
"""``forge finalize`` — assimilate + wire + certify.
|
|
111
|
+
|
|
112
|
+
``on_conflict`` resolves duplicate qualnames across roots:
|
|
113
|
+
``rename`` (default) appends a suffix
|
|
114
|
+
``first`` keep first-seen
|
|
115
|
+
``last`` keep last-seen
|
|
116
|
+
``fail`` raise ValueError
|
|
117
|
+
"""
|
|
118
|
+
target = Path(target).resolve()
|
|
119
|
+
output = Path(output).resolve()
|
|
120
|
+
if cherry_manifest is None:
|
|
121
|
+
cherry_manifest = run_cherry(target, pick_all=True)
|
|
122
|
+
|
|
123
|
+
pkg_root = output / "src" / package
|
|
124
|
+
if apply:
|
|
125
|
+
for tier in TIER_NAMES:
|
|
126
|
+
(pkg_root / tier).mkdir(parents=True, exist_ok=True)
|
|
127
|
+
(pkg_root / tier / "__init__.py").write_text(
|
|
128
|
+
f'"""{tier}."""\n', encoding="utf-8",
|
|
129
|
+
)
|
|
130
|
+
(pkg_root / "__init__.py").write_text(
|
|
131
|
+
'"""Absorbed package — bootstrapped by atomadic-forge."""\n'
|
|
132
|
+
'__version__ = "0.0.1"\n',
|
|
133
|
+
encoding="utf-8",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
items = cherry_manifest.get("items", [])
|
|
137
|
+
seen_targets: dict[Path, dict] = {} # target file → originating item
|
|
138
|
+
components_emitted = 0
|
|
139
|
+
tier_dist: dict[str, int] = {}
|
|
140
|
+
skipped: list[dict] = []
|
|
141
|
+
|
|
142
|
+
for it in items:
|
|
143
|
+
qual = it["qualname"]
|
|
144
|
+
tier = it["target_tier"]
|
|
145
|
+
# Find the source file from the scout report
|
|
146
|
+
scout = ManifestStore(target).load("scout") or {}
|
|
147
|
+
sym = next((s for s in scout.get("symbols", []) if s["qualname"] == qual),
|
|
148
|
+
None)
|
|
149
|
+
if sym is None:
|
|
150
|
+
skipped.append({"qualname": qual, "reason": "not in scout"})
|
|
151
|
+
continue
|
|
152
|
+
src_file = target / sym["file"]
|
|
153
|
+
if not src_file.exists():
|
|
154
|
+
skipped.append({"qualname": qual, "reason": "source file gone"})
|
|
155
|
+
continue
|
|
156
|
+
slug = qual.replace(".", "_").lower()
|
|
157
|
+
target_dir = pkg_root / tier
|
|
158
|
+
target_file = _target_file_for_symbol(target_dir, slug, src_file)
|
|
159
|
+
|
|
160
|
+
if target_file in seen_targets:
|
|
161
|
+
if on_conflict == "fail":
|
|
162
|
+
raise ValueError(f"conflict on {target_file}: {qual} vs "
|
|
163
|
+
f"{seen_targets[target_file]['qualname']}")
|
|
164
|
+
if on_conflict == "first":
|
|
165
|
+
skipped.append({"qualname": qual, "reason": "first-wins"})
|
|
166
|
+
continue
|
|
167
|
+
if on_conflict == "last":
|
|
168
|
+
pass # fall through, overwrite
|
|
169
|
+
if on_conflict == "rename":
|
|
170
|
+
target_file = target_dir / f"{slug}__alt{target_file.suffix}"
|
|
171
|
+
|
|
172
|
+
if apply:
|
|
173
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
try:
|
|
175
|
+
shutil.copyfile(src_file, target_file)
|
|
176
|
+
except OSError as exc:
|
|
177
|
+
skipped.append({"qualname": qual, "reason": f"copy failed: {exc}"})
|
|
178
|
+
continue
|
|
179
|
+
seen_targets[target_file] = it
|
|
180
|
+
components_emitted += 1
|
|
181
|
+
tier_dist[tier] = tier_dist.get(tier, 0) + 1
|
|
182
|
+
|
|
183
|
+
digest = hashlib.sha256(
|
|
184
|
+
json.dumps([{**it, "_": "x"} for it in items], sort_keys=True).encode()
|
|
185
|
+
).hexdigest()[:16]
|
|
186
|
+
|
|
187
|
+
if apply:
|
|
188
|
+
status = _STATUS_TEMPLATE.format(
|
|
189
|
+
source_count=1,
|
|
190
|
+
component_count=components_emitted,
|
|
191
|
+
digest=digest,
|
|
192
|
+
source_list=str(target),
|
|
193
|
+
version=_current_version(),
|
|
194
|
+
)
|
|
195
|
+
(output / "STATUS.md").write_text(status, encoding="utf-8")
|
|
196
|
+
|
|
197
|
+
wire_report = scan_violations(pkg_root) if apply else {
|
|
198
|
+
"verdict": "DRY_RUN", "violation_count": 0, "violations": []
|
|
199
|
+
}
|
|
200
|
+
cert = certify(output, project=package, package=package) if apply else {
|
|
201
|
+
"score": 0, "issues": ["dry-run — re-run with --apply to certify"],
|
|
202
|
+
"schema_version": "atomadic-forge.certify/v1",
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
report = {
|
|
206
|
+
"schema_version": "atomadic-forge.assimilate/v1",
|
|
207
|
+
"target_root": str(output),
|
|
208
|
+
"source_repos": [str(target)],
|
|
209
|
+
"components_emitted": components_emitted,
|
|
210
|
+
"tier_distribution": tier_dist,
|
|
211
|
+
"skipped": skipped,
|
|
212
|
+
"digest": digest,
|
|
213
|
+
"wire": wire_report,
|
|
214
|
+
"certify": cert,
|
|
215
|
+
"applied": apply,
|
|
216
|
+
"on_conflict": on_conflict,
|
|
217
|
+
}
|
|
218
|
+
if apply:
|
|
219
|
+
ManifestStore(output).save("assimilate", report)
|
|
220
|
+
return report
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def run_auto(*, target: Path, output: Path, package: str = "absorbed",
|
|
224
|
+
apply: bool = False, on_conflict: str = "rename",
|
|
225
|
+
progress: Callable[[int, int, str], None] | None = None,
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
"""``forge auto`` — the flagship single-command pipeline.
|
|
228
|
+
|
|
229
|
+
Runs: scout → cherry-pick (all) → finalize (assimilate + wire + certify).
|
|
230
|
+
Always returns a structured report. Set ``apply=True`` to actually write
|
|
231
|
+
files; otherwise it's a deterministic dry-run.
|
|
232
|
+
|
|
233
|
+
``progress`` (optional): per-file scout-phase reporter. Cherry/finalize
|
|
234
|
+
are typically fast enough to skip; only the scout walk is plumbed.
|
|
235
|
+
"""
|
|
236
|
+
scout = run_recon(target, progress=progress)
|
|
237
|
+
cherry = run_cherry(target, pick_all=True)
|
|
238
|
+
final = run_finalize(target=target, output=output, package=package,
|
|
239
|
+
cherry_manifest=cherry, apply=apply,
|
|
240
|
+
on_conflict=on_conflict)
|
|
241
|
+
return {
|
|
242
|
+
"schema_version": "atomadic-forge.auto/v1",
|
|
243
|
+
"scout": {
|
|
244
|
+
"symbol_count": scout["symbol_count"],
|
|
245
|
+
"tier_distribution": scout["tier_distribution"],
|
|
246
|
+
"recommendations": scout["recommendations"],
|
|
247
|
+
},
|
|
248
|
+
"cherry": {"items": len(cherry["items"])},
|
|
249
|
+
"finalize": final,
|
|
250
|
+
"applied": apply,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def run_auto_plan(
|
|
255
|
+
*,
|
|
256
|
+
target: Path,
|
|
257
|
+
goal: str = "improve repo conformance",
|
|
258
|
+
mode: str = "improve",
|
|
259
|
+
package: str | None = None,
|
|
260
|
+
top_n: int = 7,
|
|
261
|
+
) -> dict[str, Any]:
|
|
262
|
+
"""Codex-driven 'observe → propose cards' orchestrator.
|
|
263
|
+
|
|
264
|
+
Runs scout + wire + certify (and, when available, emergent +
|
|
265
|
+
synergy) and emits a single ``agent_plan/v1`` with top-N action
|
|
266
|
+
cards. Does NOT mutate the filesystem — handoff is the agent's
|
|
267
|
+
responsibility.
|
|
268
|
+
|
|
269
|
+
Mode 'improve' (default): the agent operates on the repo at
|
|
270
|
+
``target`` in-place; certify runs against ``target`` and the
|
|
271
|
+
generated cards reference its layout.
|
|
272
|
+
Mode 'absorb': used by the legacy ``forge auto`` flow when the
|
|
273
|
+
target is a flat repo and the next action is to scaffold a new
|
|
274
|
+
tier-organized output.
|
|
275
|
+
|
|
276
|
+
The returned dict matches AgentPlan exactly (schema_version
|
|
277
|
+
'atomadic-forge.agent_plan/v1') so MCP clients can round-trip
|
|
278
|
+
it without unwrapping.
|
|
279
|
+
"""
|
|
280
|
+
target = Path(target).resolve()
|
|
281
|
+
wire = scan_violations(target, suggest_repairs=True)
|
|
282
|
+
try:
|
|
283
|
+
cert = certify(target, project=target.name, package=package)
|
|
284
|
+
except (OSError, RuntimeError, ValueError):
|
|
285
|
+
cert = None
|
|
286
|
+
# emergent / synergy reports are optional; the emitter degrades
|
|
287
|
+
# gracefully when they're absent. Wiring them in here would
|
|
288
|
+
# require a3→a3 imports (allowed) but would slow the plan call.
|
|
289
|
+
plan = emit_agent_plan(
|
|
290
|
+
project_root=str(target),
|
|
291
|
+
goal=goal, mode=mode,
|
|
292
|
+
wire_report=wire, certify_report=cert,
|
|
293
|
+
package=package, top_n=top_n,
|
|
294
|
+
)
|
|
295
|
+
return plan
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Tier a3 — apply ONE card / ALL applyable cards from an agent_plan.
|
|
2
|
+
|
|
3
|
+
Codex's prescription: 'agent picks one card, runs its next_command'.
|
|
4
|
+
This module is the bounded-execution helper Forge supplies so the
|
|
5
|
+
agent doesn't have to reimplement card-routing.
|
|
6
|
+
|
|
7
|
+
Routing strategy:
|
|
8
|
+
* architectural cards with auto_fixable F-codes → forge enforce
|
|
9
|
+
--apply against the card's write_scope; Forge enforce already
|
|
10
|
+
rolls back if violations rise.
|
|
11
|
+
* operational F0050 (docs missing) → write a minimal README.md
|
|
12
|
+
using the card's write_scope[0].
|
|
13
|
+
* operational F0051 (tests missing) → not yet implemented; the
|
|
14
|
+
apply call records 'skipped' with a 'manual_required' reason.
|
|
15
|
+
* synthesis / composition cards → not yet implemented; require
|
|
16
|
+
forge synergy implement / forge emergent synthesize integration.
|
|
17
|
+
|
|
18
|
+
Every apply attempt records a per-card event in the plan store
|
|
19
|
+
(applied / skipped / rolled_back / failed) so the agent (and Lane F
|
|
20
|
+
W26 audit trail) can reason about what was done.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from ..a0_qk_constants.agent_plan_schema import AgentActionCard
|
|
28
|
+
from ..a2_mo_composites.plan_store import PlanStore
|
|
29
|
+
from .forge_enforce import run_enforce
|
|
30
|
+
|
|
31
|
+
_ARCHITECTURAL_AUTO_FIX_FCODES: frozenset[str] = frozenset({
|
|
32
|
+
"F0041", "F0042", "F0043", "F0044", "F0045", "F0046",
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _find_card(plan: dict, card_id: str) -> AgentActionCard | None:
|
|
37
|
+
for card in plan.get("top_actions", []) or []:
|
|
38
|
+
if card.get("id") == card_id:
|
|
39
|
+
return card
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _apply_architectural_card(
|
|
44
|
+
project_root: Path,
|
|
45
|
+
card: AgentActionCard,
|
|
46
|
+
*,
|
|
47
|
+
apply: bool,
|
|
48
|
+
) -> dict[str, Any]:
|
|
49
|
+
"""Route an F0041..F0046 card through forge enforce.
|
|
50
|
+
|
|
51
|
+
The card's write_scope identifies the file under threat; we run
|
|
52
|
+
enforce against its package root so the rollback-safe orchestrator
|
|
53
|
+
handles it. apply=False -> dry-run; apply=True -> actually move.
|
|
54
|
+
"""
|
|
55
|
+
write_scope = card.get("write_scope") or []
|
|
56
|
+
if not write_scope:
|
|
57
|
+
return {"status": "failed",
|
|
58
|
+
"detail": {"reason": "card has empty write_scope"}}
|
|
59
|
+
rel = write_scope[0]
|
|
60
|
+
# Walk up from the file to the nearest tier-organized package.
|
|
61
|
+
file_path = (project_root / rel).resolve()
|
|
62
|
+
candidate = file_path.parent
|
|
63
|
+
pkg_root: Path | None = None
|
|
64
|
+
while candidate != candidate.parent:
|
|
65
|
+
# A package root contains tier directories.
|
|
66
|
+
if any((candidate / t).is_dir() for t in (
|
|
67
|
+
"a0_qk_constants", "a1_at_functions", "a2_mo_composites",
|
|
68
|
+
"a3_og_features", "a4_sy_orchestration",
|
|
69
|
+
)):
|
|
70
|
+
pkg_root = candidate
|
|
71
|
+
break
|
|
72
|
+
candidate = candidate.parent
|
|
73
|
+
if pkg_root is None:
|
|
74
|
+
return {"status": "failed",
|
|
75
|
+
"detail": {"reason": "no tier-organized package root above "
|
|
76
|
+
f"{rel}"}}
|
|
77
|
+
report = run_enforce(pkg_root, apply=apply)
|
|
78
|
+
pre, post = report["pre_violations"], report["post_violations"]
|
|
79
|
+
if not apply:
|
|
80
|
+
return {"status": "dry_run",
|
|
81
|
+
"detail": {"pre_violations": pre, "post_violations": post,
|
|
82
|
+
"plan": report["plan"]}}
|
|
83
|
+
if post < pre:
|
|
84
|
+
return {"status": "applied",
|
|
85
|
+
"detail": {"pre_violations": pre, "post_violations": post,
|
|
86
|
+
"applied": report["applied"]}}
|
|
87
|
+
if post > pre:
|
|
88
|
+
return {"status": "rolled_back",
|
|
89
|
+
"detail": {"pre_violations": pre, "post_violations": post,
|
|
90
|
+
"rollbacks": report["rollbacks"]}}
|
|
91
|
+
return {"status": "noop",
|
|
92
|
+
"detail": {"pre_violations": pre, "post_violations": post}}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _apply_docs_card(
|
|
96
|
+
project_root: Path,
|
|
97
|
+
card: AgentActionCard,
|
|
98
|
+
*,
|
|
99
|
+
apply: bool,
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
"""F0050 minimal-README writer.
|
|
102
|
+
|
|
103
|
+
The card's write_scope[0] tells us where the README belongs.
|
|
104
|
+
apply=True writes a one-line H1 + paragraph; apply=False is a
|
|
105
|
+
no-op dry-run.
|
|
106
|
+
"""
|
|
107
|
+
rel = (card.get("write_scope") or ["README.md"])[0]
|
|
108
|
+
target = (project_root / rel).resolve()
|
|
109
|
+
if target.exists():
|
|
110
|
+
return {"status": "skipped",
|
|
111
|
+
"detail": {"reason": f"{rel} already exists"}}
|
|
112
|
+
if not apply:
|
|
113
|
+
return {"status": "dry_run",
|
|
114
|
+
"detail": {"would_write": str(target.relative_to(project_root))}}
|
|
115
|
+
pkg = card.get("sample_path") or project_root.name
|
|
116
|
+
body = (
|
|
117
|
+
f"# {pkg}\n\n"
|
|
118
|
+
f"_Stub README written by `forge plan-apply` "
|
|
119
|
+
f"({card.get('id')})._\n\n"
|
|
120
|
+
f"Replace this with a real overview before shipping.\n"
|
|
121
|
+
)
|
|
122
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
target.write_text(body, encoding="utf-8")
|
|
124
|
+
return {"status": "applied",
|
|
125
|
+
"detail": {"written": str(target.relative_to(project_root))}}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _route_card(
|
|
129
|
+
project_root: Path,
|
|
130
|
+
card: AgentActionCard,
|
|
131
|
+
*,
|
|
132
|
+
apply: bool,
|
|
133
|
+
) -> dict[str, Any]:
|
|
134
|
+
if not card.get("applyable"):
|
|
135
|
+
return {"status": "skipped",
|
|
136
|
+
"detail": {"reason": "card.applyable=False (review_manually)"}}
|
|
137
|
+
related = set(card.get("related_fcodes") or [])
|
|
138
|
+
if related & _ARCHITECTURAL_AUTO_FIX_FCODES:
|
|
139
|
+
return _apply_architectural_card(project_root, card, apply=apply)
|
|
140
|
+
if "F0050" in related:
|
|
141
|
+
return _apply_docs_card(project_root, card, apply=apply)
|
|
142
|
+
# F0051 / synthesis / composition: not yet implementable.
|
|
143
|
+
return {
|
|
144
|
+
"status": "skipped",
|
|
145
|
+
"detail": {
|
|
146
|
+
"reason": "card kind not yet implementable in plan-apply v1",
|
|
147
|
+
"kind": card.get("kind"),
|
|
148
|
+
"related_fcodes": list(related),
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def apply_card(
|
|
154
|
+
project_root: Path,
|
|
155
|
+
plan: dict,
|
|
156
|
+
card_id: str,
|
|
157
|
+
*,
|
|
158
|
+
apply: bool = False,
|
|
159
|
+
) -> dict[str, Any]:
|
|
160
|
+
"""Apply a single card from a plan.
|
|
161
|
+
|
|
162
|
+
Returns a result dict with shape:
|
|
163
|
+
{schema_version, plan_id, card_id, apply, status, detail}
|
|
164
|
+
|
|
165
|
+
Always records the event to the plan store regardless of apply
|
|
166
|
+
flag (dry runs included) so the audit trail is complete.
|
|
167
|
+
"""
|
|
168
|
+
project_root = Path(project_root).resolve()
|
|
169
|
+
plan_id = plan.get("id") or "<unsaved>"
|
|
170
|
+
card = _find_card(plan, card_id)
|
|
171
|
+
if card is None:
|
|
172
|
+
return {
|
|
173
|
+
"schema_version": "atomadic-forge.plan_apply/v1",
|
|
174
|
+
"plan_id": plan_id, "card_id": card_id, "apply": apply,
|
|
175
|
+
"status": "failed",
|
|
176
|
+
"detail": {"reason": f"card_id {card_id!r} not in plan"},
|
|
177
|
+
}
|
|
178
|
+
outcome = _route_card(project_root, card, apply=apply)
|
|
179
|
+
PlanStore(project_root).record_card_event(
|
|
180
|
+
plan_id, card_id=card_id,
|
|
181
|
+
status=outcome["status"],
|
|
182
|
+
detail={"apply": apply, **outcome.get("detail", {})},
|
|
183
|
+
)
|
|
184
|
+
return {
|
|
185
|
+
"schema_version": "atomadic-forge.plan_apply/v1",
|
|
186
|
+
"plan_id": plan_id, "card_id": card_id, "apply": apply,
|
|
187
|
+
**outcome,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def apply_all_applyable(
|
|
192
|
+
project_root: Path,
|
|
193
|
+
plan: dict,
|
|
194
|
+
*,
|
|
195
|
+
apply: bool = False,
|
|
196
|
+
) -> dict[str, Any]:
|
|
197
|
+
"""Iterate applyable cards in plan order; collect results.
|
|
198
|
+
|
|
199
|
+
Stops on the first ``rolled_back`` or ``failed`` outcome — the
|
|
200
|
+
agent should inspect that result before proceeding rather than
|
|
201
|
+
cascading further mutations against a now-suspect repo.
|
|
202
|
+
"""
|
|
203
|
+
project_root = Path(project_root).resolve()
|
|
204
|
+
plan_id = plan.get("id") or "<unsaved>"
|
|
205
|
+
results: list[dict] = []
|
|
206
|
+
halted = None
|
|
207
|
+
for card in plan.get("top_actions", []) or []:
|
|
208
|
+
if not card.get("applyable"):
|
|
209
|
+
continue
|
|
210
|
+
outcome = apply_card(project_root, plan, card["id"], apply=apply)
|
|
211
|
+
results.append(outcome)
|
|
212
|
+
if outcome["status"] in {"rolled_back", "failed"}:
|
|
213
|
+
halted = outcome["status"]
|
|
214
|
+
break
|
|
215
|
+
return {
|
|
216
|
+
"schema_version": "atomadic-forge.plan_apply_all/v1",
|
|
217
|
+
"plan_id": plan_id, "apply": apply,
|
|
218
|
+
"results": results,
|
|
219
|
+
"halted_on": halted,
|
|
220
|
+
"applied_count": sum(1 for r in results if r["status"] == "applied"),
|
|
221
|
+
"skipped_count": sum(1 for r in results if r["status"] == "skipped"),
|
|
222
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Tier a3 — LSP stdio loop wrapping the pure dispatcher.
|
|
2
|
+
|
|
3
|
+
Golden Path Lane D W12 deliverable. The pure dispatcher lives at
|
|
4
|
+
``a1_at_functions.lsp_protocol``; this module owns the LSP framing
|
|
5
|
+
(Content-Length headers + JSON body) over stdin/stdout, exactly the
|
|
6
|
+
shape every LSP client (VS Code, Neovim, Helix, Sublime, IntelliJ)
|
|
7
|
+
expects on first connect.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from typing import IO
|
|
14
|
+
|
|
15
|
+
from ..a1_at_functions.lsp_protocol import (
|
|
16
|
+
LspState,
|
|
17
|
+
dispatch_request,
|
|
18
|
+
new_state,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def serve_stdio(
|
|
23
|
+
*,
|
|
24
|
+
stdin: IO[bytes] | None = None,
|
|
25
|
+
stdout: IO[bytes] | None = None,
|
|
26
|
+
stderr: IO[str] | None = None,
|
|
27
|
+
) -> int:
|
|
28
|
+
"""Read LSP messages from stdin (Content-Length framed) and write
|
|
29
|
+
responses + notifications to stdout. Exits 0 on clean shutdown."""
|
|
30
|
+
src_in = stdin if stdin is not None else sys.stdin.buffer
|
|
31
|
+
src_out = stdout if stdout is not None else sys.stdout.buffer
|
|
32
|
+
src_err = stderr if stderr is not None else sys.stderr
|
|
33
|
+
|
|
34
|
+
state: LspState = new_state()
|
|
35
|
+
src_err.write("forge-lsp: ready (Content-Length framed JSON-RPC)\n")
|
|
36
|
+
src_err.flush()
|
|
37
|
+
|
|
38
|
+
while True:
|
|
39
|
+
msg = _read_message(src_in)
|
|
40
|
+
if msg is None:
|
|
41
|
+
break # client closed stdin
|
|
42
|
+
try:
|
|
43
|
+
request = json.loads(msg.decode("utf-8"))
|
|
44
|
+
except json.JSONDecodeError:
|
|
45
|
+
_write_message(src_out, {
|
|
46
|
+
"jsonrpc": "2.0", "id": None,
|
|
47
|
+
"error": {"code": -32700, "message": "Parse error"},
|
|
48
|
+
})
|
|
49
|
+
continue
|
|
50
|
+
responses, notifications = dispatch_request(request, state=state)
|
|
51
|
+
for resp in responses:
|
|
52
|
+
_write_message(src_out, resp)
|
|
53
|
+
for note in notifications:
|
|
54
|
+
_write_message(src_out, note)
|
|
55
|
+
if request.get("method") == "exit":
|
|
56
|
+
return 0 if state.get("shutdown_requested") else 1
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _read_message(stream: IO[bytes]) -> bytes | None:
|
|
61
|
+
"""Read one LSP message frame: headers terminated by \\r\\n\\r\\n
|
|
62
|
+
followed by Content-Length bytes of body."""
|
|
63
|
+
headers: dict[str, str] = {}
|
|
64
|
+
while True:
|
|
65
|
+
line = stream.readline()
|
|
66
|
+
if not line:
|
|
67
|
+
return None # EOF
|
|
68
|
+
s = line.decode("ascii").rstrip("\r\n")
|
|
69
|
+
if s == "":
|
|
70
|
+
break
|
|
71
|
+
if ":" not in s:
|
|
72
|
+
continue
|
|
73
|
+
key, _, value = s.partition(":")
|
|
74
|
+
headers[key.strip().lower()] = value.strip()
|
|
75
|
+
length_str = headers.get("content-length", "0")
|
|
76
|
+
try:
|
|
77
|
+
length = int(length_str)
|
|
78
|
+
except ValueError:
|
|
79
|
+
return None
|
|
80
|
+
if length <= 0:
|
|
81
|
+
return b""
|
|
82
|
+
body = b""
|
|
83
|
+
remaining = length
|
|
84
|
+
while remaining > 0:
|
|
85
|
+
chunk = stream.read(remaining)
|
|
86
|
+
if not chunk:
|
|
87
|
+
return None # EOF mid-message
|
|
88
|
+
body += chunk
|
|
89
|
+
remaining -= len(chunk)
|
|
90
|
+
return body
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _write_message(stream: IO[bytes], payload: dict) -> None:
|
|
94
|
+
body = json.dumps(payload, default=str).encode("utf-8")
|
|
95
|
+
header = f"Content-Length: {len(body)}\r\n\r\n".encode("ascii")
|
|
96
|
+
stream.write(header)
|
|
97
|
+
stream.write(body)
|
|
98
|
+
stream.flush()
|