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,324 @@
|
|
|
1
|
+
"""Tier a1 — pure agent_plan/v1 emitter.
|
|
2
|
+
|
|
3
|
+
Consumes one or more Forge reports and emits an ordered
|
|
4
|
+
``AgentPlan``. Codex's prescription:
|
|
5
|
+
|
|
6
|
+
> 'observe → propose cards → agent chooses/edits → apply bounded
|
|
7
|
+
> change → certify → next card'
|
|
8
|
+
|
|
9
|
+
This module owns the 'propose cards' step. The agent owns 'choose
|
|
10
|
+
/ edit / apply'. Forge provides the bounded-change verbs (auto,
|
|
11
|
+
enforce, certify, etc.) the cards point at.
|
|
12
|
+
|
|
13
|
+
Ranking: same deterministic order as ``agent_summary``:
|
|
14
|
+
1. operational + applyable + low risk (cheapest wins first)
|
|
15
|
+
2. architectural + auto_fixable (forge enforce path)
|
|
16
|
+
3. release-blocking certify axes
|
|
17
|
+
4. high-confidence synthesis (synergy adapters)
|
|
18
|
+
5. composition opportunities (emergent chains)
|
|
19
|
+
6. medium-risk architectural (review_manually)
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import datetime as _dt
|
|
24
|
+
|
|
25
|
+
from ..a0_qk_constants.agent_plan_schema import (
|
|
26
|
+
REQUIRED_PLAN_FIELDS,
|
|
27
|
+
SCHEMA_VERSION_AGENT_ACTION_V1,
|
|
28
|
+
SCHEMA_VERSION_AGENT_PLAN_V1,
|
|
29
|
+
AgentActionCard,
|
|
30
|
+
AgentPlan,
|
|
31
|
+
)
|
|
32
|
+
from ..a0_qk_constants.error_codes import (
|
|
33
|
+
fcode_for_certify_axis,
|
|
34
|
+
fcode_for_tier_violation,
|
|
35
|
+
get_fcode,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
_KIND_RANK = {
|
|
39
|
+
"operational": 0,
|
|
40
|
+
"architectural": 1,
|
|
41
|
+
"release": 2,
|
|
42
|
+
"synthesis": 3,
|
|
43
|
+
"composition": 4,
|
|
44
|
+
}
|
|
45
|
+
_RISK_RANK = {"low": 0, "medium": 1, "high": 2}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _now_utc_iso() -> str:
|
|
49
|
+
return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _slug(*parts: str) -> str:
|
|
53
|
+
"""Build a stable card id from its parts. Lowercased; non-alnum
|
|
54
|
+
collapsed to '-'."""
|
|
55
|
+
raw = "-".join(p for p in parts if p)
|
|
56
|
+
out: list[str] = []
|
|
57
|
+
prev_dash = False
|
|
58
|
+
for ch in raw.lower():
|
|
59
|
+
if ch.isalnum() or ch == ".":
|
|
60
|
+
out.append(ch)
|
|
61
|
+
prev_dash = False
|
|
62
|
+
elif not prev_dash:
|
|
63
|
+
out.append("-")
|
|
64
|
+
prev_dash = True
|
|
65
|
+
return "".join(out).strip("-")[:80] or "action"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _operational_cards(certify_report: dict | None,
|
|
69
|
+
*, package: str | None) -> list[AgentActionCard]:
|
|
70
|
+
cards: list[AgentActionCard] = []
|
|
71
|
+
if not certify_report:
|
|
72
|
+
return cards
|
|
73
|
+
pkg_label = package or "your_pkg"
|
|
74
|
+
|
|
75
|
+
if not certify_report.get("documentation_complete", True):
|
|
76
|
+
cards.append(AgentActionCard(
|
|
77
|
+
schema_version=SCHEMA_VERSION_AGENT_ACTION_V1,
|
|
78
|
+
id=_slug("docs", pkg_label),
|
|
79
|
+
kind="operational",
|
|
80
|
+
title="Add a README so the documentation axis passes",
|
|
81
|
+
why="forge certify documentation_complete=False blocks any "
|
|
82
|
+
"score above 75/100; a one-paragraph README is worth +25.",
|
|
83
|
+
risk="low",
|
|
84
|
+
# forge plan-apply has a bounded F0050 handler that writes
|
|
85
|
+
# a stub README; the agent should replace it with real
|
|
86
|
+
# content before shipping.
|
|
87
|
+
applyable=True,
|
|
88
|
+
write_scope=["README.md"],
|
|
89
|
+
commands=[f"forge certify . --package {pkg_label}"],
|
|
90
|
+
related_fcodes=[fcode_for_certify_axis("documentation_complete")],
|
|
91
|
+
next_command=f"echo '# {pkg_label}' > README.md",
|
|
92
|
+
sample_path=None,
|
|
93
|
+
score_delta_estimate=25,
|
|
94
|
+
))
|
|
95
|
+
if not certify_report.get("tests_present", True):
|
|
96
|
+
cards.append(AgentActionCard(
|
|
97
|
+
schema_version=SCHEMA_VERSION_AGENT_ACTION_V1,
|
|
98
|
+
id=_slug("tests", pkg_label),
|
|
99
|
+
kind="operational",
|
|
100
|
+
title="Add a tests/ directory so the tests axis passes",
|
|
101
|
+
why="forge certify tests_present=False; even one smoke test "
|
|
102
|
+
"(import-only) clears the axis and unlocks +25.",
|
|
103
|
+
risk="low",
|
|
104
|
+
applyable=False,
|
|
105
|
+
write_scope=["tests/test_smoke.py", "tests/__init__.py"],
|
|
106
|
+
commands=[f"forge certify . --package {pkg_label}",
|
|
107
|
+
"python -m pytest"],
|
|
108
|
+
related_fcodes=[fcode_for_certify_axis("tests_present")],
|
|
109
|
+
next_command=(
|
|
110
|
+
f"mkdir -p tests && printf 'import {pkg_label}\\n' > "
|
|
111
|
+
"tests/test_smoke.py"
|
|
112
|
+
),
|
|
113
|
+
sample_path=None,
|
|
114
|
+
score_delta_estimate=25,
|
|
115
|
+
))
|
|
116
|
+
return cards
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _architectural_cards(wire_report: dict | None,
|
|
120
|
+
*, package_root: str | None) -> list[AgentActionCard]:
|
|
121
|
+
cards: list[AgentActionCard] = []
|
|
122
|
+
if not wire_report:
|
|
123
|
+
return cards
|
|
124
|
+
violations = wire_report.get("violations") or []
|
|
125
|
+
if not violations:
|
|
126
|
+
return cards
|
|
127
|
+
by_fcode_file: dict[tuple[str, str], list[dict]] = {}
|
|
128
|
+
for v in violations:
|
|
129
|
+
code = v.get("f_code") or fcode_for_tier_violation(
|
|
130
|
+
v.get("from_tier", ""), v.get("to_tier", ""))
|
|
131
|
+
key = (code, v.get("file", ""))
|
|
132
|
+
by_fcode_file.setdefault(key, []).append(v)
|
|
133
|
+
for (code, file_path), group in by_fcode_file.items():
|
|
134
|
+
entry = get_fcode(code)
|
|
135
|
+
applyable = bool(entry and entry.get("auto_fixable"))
|
|
136
|
+
if applyable:
|
|
137
|
+
risk = "low"
|
|
138
|
+
cmd = (f"forge enforce {package_root or 'src/your_pkg'} "
|
|
139
|
+
"--apply # rolls back if violations rise")
|
|
140
|
+
why = (
|
|
141
|
+
f"{file_path} carries {len(group)} {code} violation(s). "
|
|
142
|
+
"auto_fixable=True; forge enforce can move the file up "
|
|
143
|
+
"to the higher tier and atomically roll back if the "
|
|
144
|
+
"move increases violations."
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
risk = "medium"
|
|
148
|
+
cmd = (f"# review {file_path}: invert the import direction "
|
|
149
|
+
"or extract the symbol down to a lower tier.")
|
|
150
|
+
why = (
|
|
151
|
+
f"{file_path} carries {len(group)} {code} violation(s). "
|
|
152
|
+
"Mechanical fix is unsafe (a0 special-case or "
|
|
153
|
+
"non-canonical tier shape); requires agent judgement."
|
|
154
|
+
)
|
|
155
|
+
cards.append(AgentActionCard(
|
|
156
|
+
schema_version=SCHEMA_VERSION_AGENT_ACTION_V1,
|
|
157
|
+
id=_slug("fix", code, file_path),
|
|
158
|
+
kind="architectural",
|
|
159
|
+
title=(entry["title"] if entry else f"{code} on {file_path}"),
|
|
160
|
+
why=why,
|
|
161
|
+
risk=risk,
|
|
162
|
+
applyable=applyable,
|
|
163
|
+
write_scope=[file_path],
|
|
164
|
+
commands=[
|
|
165
|
+
f"forge wire {package_root or 'src/your_pkg'} "
|
|
166
|
+
"--suggest-repairs",
|
|
167
|
+
"python -m pytest",
|
|
168
|
+
],
|
|
169
|
+
related_fcodes=[code],
|
|
170
|
+
next_command=cmd,
|
|
171
|
+
sample_path=file_path,
|
|
172
|
+
score_delta_estimate=10 if applyable else 5,
|
|
173
|
+
))
|
|
174
|
+
return cards
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _synthesis_cards(synergy_report: dict | None,
|
|
178
|
+
*, top_n: int = 3) -> list[AgentActionCard]:
|
|
179
|
+
cards: list[AgentActionCard] = []
|
|
180
|
+
if not synergy_report:
|
|
181
|
+
return cards
|
|
182
|
+
candidates = synergy_report.get("candidates") or []
|
|
183
|
+
for c in candidates[:top_n]:
|
|
184
|
+
cid = c.get("id") or _slug("syn",
|
|
185
|
+
c.get("producer", ""),
|
|
186
|
+
c.get("consumer", ""))
|
|
187
|
+
cards.append(AgentActionCard(
|
|
188
|
+
schema_version=SCHEMA_VERSION_AGENT_ACTION_V1,
|
|
189
|
+
id=_slug("synergy", cid),
|
|
190
|
+
kind="synthesis",
|
|
191
|
+
title=f"Synergy adapter: {c.get('producer', '?')} → "
|
|
192
|
+
f"{c.get('consumer', '?')}",
|
|
193
|
+
why=(c.get("reason")
|
|
194
|
+
or "forge synergy detected a high-confidence "
|
|
195
|
+
"feature-pair compose-by-law match."),
|
|
196
|
+
risk="medium",
|
|
197
|
+
applyable=True,
|
|
198
|
+
write_scope=[],
|
|
199
|
+
commands=[f"forge synergy implement {cid}",
|
|
200
|
+
"python -m pytest"],
|
|
201
|
+
related_fcodes=[],
|
|
202
|
+
next_command=f"forge synergy implement {cid}",
|
|
203
|
+
sample_path=None,
|
|
204
|
+
score_delta_estimate=0,
|
|
205
|
+
))
|
|
206
|
+
return cards
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _composition_cards(emergent_report: dict | None,
|
|
210
|
+
*, top_n: int = 3) -> list[AgentActionCard]:
|
|
211
|
+
cards: list[AgentActionCard] = []
|
|
212
|
+
if not emergent_report:
|
|
213
|
+
return cards
|
|
214
|
+
candidates = emergent_report.get("candidates") or []
|
|
215
|
+
for c in candidates[:top_n]:
|
|
216
|
+
cid = c.get("id") or _slug("emg", *(s for s in c.get("chain", [])))
|
|
217
|
+
chain = c.get("chain") or []
|
|
218
|
+
cards.append(AgentActionCard(
|
|
219
|
+
schema_version=SCHEMA_VERSION_AGENT_ACTION_V1,
|
|
220
|
+
id=_slug("emergent", cid),
|
|
221
|
+
kind="composition",
|
|
222
|
+
title=f"Compose: {' → '.join(chain) if chain else cid}",
|
|
223
|
+
why=(c.get("reason")
|
|
224
|
+
or "forge emergent surfaced a hidden composition chain."),
|
|
225
|
+
risk="medium",
|
|
226
|
+
applyable=True,
|
|
227
|
+
write_scope=[],
|
|
228
|
+
commands=[f"forge emergent synthesize {cid}",
|
|
229
|
+
"python -m pytest"],
|
|
230
|
+
related_fcodes=[],
|
|
231
|
+
next_command=f"forge emergent synthesize {cid}",
|
|
232
|
+
sample_path=None,
|
|
233
|
+
score_delta_estimate=0,
|
|
234
|
+
))
|
|
235
|
+
return cards
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _rank(card: AgentActionCard) -> tuple[int, int, int, str]:
|
|
239
|
+
"""Stable card rank — see module docstring."""
|
|
240
|
+
return (
|
|
241
|
+
0 if card.get("applyable") else 1,
|
|
242
|
+
_KIND_RANK.get(card.get("kind", "operational"), 99),
|
|
243
|
+
_RISK_RANK.get(card.get("risk", "high"), 99),
|
|
244
|
+
card.get("id", ""),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def emit_agent_plan(
|
|
249
|
+
*,
|
|
250
|
+
project_root: str,
|
|
251
|
+
goal: str,
|
|
252
|
+
mode: str = "improve",
|
|
253
|
+
wire_report: dict | None = None,
|
|
254
|
+
certify_report: dict | None = None,
|
|
255
|
+
emergent_report: dict | None = None,
|
|
256
|
+
synergy_report: dict | None = None,
|
|
257
|
+
package: str | None = None,
|
|
258
|
+
top_n: int = 7,
|
|
259
|
+
) -> AgentPlan:
|
|
260
|
+
"""Build an agent_plan/v1 from any combination of Forge reports.
|
|
261
|
+
|
|
262
|
+
Caller is responsible for running the source scans (cheap to do
|
|
263
|
+
in parallel; a3 ``run_auto_plan`` wraps the orchestration).
|
|
264
|
+
"""
|
|
265
|
+
if top_n < 1:
|
|
266
|
+
raise ValueError("top_n must be >= 1")
|
|
267
|
+
if mode not in ("improve", "absorb"):
|
|
268
|
+
raise ValueError(f"mode must be 'improve' | 'absorb', got {mode!r}")
|
|
269
|
+
|
|
270
|
+
cards: list[AgentActionCard] = []
|
|
271
|
+
cards.extend(_operational_cards(certify_report, package=package))
|
|
272
|
+
cards.extend(_architectural_cards(wire_report,
|
|
273
|
+
package_root=package or project_root))
|
|
274
|
+
cards.extend(_synthesis_cards(synergy_report))
|
|
275
|
+
cards.extend(_composition_cards(emergent_report))
|
|
276
|
+
cards.sort(key=_rank)
|
|
277
|
+
|
|
278
|
+
applyable = sum(1 for c in cards if c.get("applyable"))
|
|
279
|
+
score = float((certify_report or {}).get("score", 0.0))
|
|
280
|
+
if wire_report and wire_report.get("verdict") == "PASS" and score >= 100:
|
|
281
|
+
verdict = "PASS"
|
|
282
|
+
elif not cards:
|
|
283
|
+
verdict = "PASS"
|
|
284
|
+
else:
|
|
285
|
+
verdict = "FAIL"
|
|
286
|
+
|
|
287
|
+
next_command = (cards[0]["next_command"] if cards
|
|
288
|
+
else "# already PASSing — no next action.")
|
|
289
|
+
|
|
290
|
+
sources: dict[str, str] = {}
|
|
291
|
+
if wire_report:
|
|
292
|
+
sources["wire"] = wire_report.get("schema_version", "")
|
|
293
|
+
if certify_report:
|
|
294
|
+
sources["certify"] = certify_report.get("schema_version", "")
|
|
295
|
+
if emergent_report:
|
|
296
|
+
sources["emergent"] = emergent_report.get("schema_version", "")
|
|
297
|
+
if synergy_report:
|
|
298
|
+
sources["synergy"] = synergy_report.get("schema_version", "")
|
|
299
|
+
|
|
300
|
+
plan: AgentPlan = AgentPlan(
|
|
301
|
+
schema_version=SCHEMA_VERSION_AGENT_PLAN_V1,
|
|
302
|
+
generated_at_utc=_now_utc_iso(),
|
|
303
|
+
verdict=verdict, # type: ignore[typeddict-item]
|
|
304
|
+
goal=goal,
|
|
305
|
+
mode=mode,
|
|
306
|
+
project_root=str(project_root),
|
|
307
|
+
top_actions=list(cards[:top_n]),
|
|
308
|
+
action_count=len(cards),
|
|
309
|
+
applyable_count=applyable,
|
|
310
|
+
next_command=next_command,
|
|
311
|
+
# Codex feedback (round 3): surface the certify score on the
|
|
312
|
+
# plan envelope so MCP _summary digests inherit the real number.
|
|
313
|
+
score=(float(certify_report["score"])
|
|
314
|
+
if certify_report and "score" in certify_report else None),
|
|
315
|
+
sources=sources,
|
|
316
|
+
)
|
|
317
|
+
# Defensive — TypedDict isn't enforced at runtime.
|
|
318
|
+
for f in REQUIRED_PLAN_FIELDS:
|
|
319
|
+
if f not in plan:
|
|
320
|
+
raise RuntimeError(
|
|
321
|
+
f"agent_plan emitter built a plan missing required "
|
|
322
|
+
f"field {f!r} — schema/emitter drift"
|
|
323
|
+
)
|
|
324
|
+
return plan
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Tier a1 — agent-native compact-blocker summaries.
|
|
2
|
+
|
|
3
|
+
Direct response to field feedback from Codex (the Atomadic-Lang
|
|
4
|
+
agent) after using earlier Forge releases:
|
|
5
|
+
|
|
6
|
+
> "Make tool outputs compact and explicitly actionable, because
|
|
7
|
+
> agents thrive on 'here are the 2 things blocking release' more
|
|
8
|
+
> than huge manifests."
|
|
9
|
+
|
|
10
|
+
Every Forge report (wire / certify / enforce) is structurally
|
|
11
|
+
correct but verbose. An agent consuming them via ``forge mcp serve``
|
|
12
|
+
or ``--json`` has to wade through dozens of KB to find the next
|
|
13
|
+
action. This module condenses any combination of those reports
|
|
14
|
+
into a 3-key shape:
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
"schema_version": "atomadic-forge.summary/v1",
|
|
18
|
+
"verdict": "PASS" | "FAIL" | ...,
|
|
19
|
+
"score": 0-100,
|
|
20
|
+
"blockers": [
|
|
21
|
+
{"f_code": "F0050", "title": "...",
|
|
22
|
+
"next_command": "echo '# my-pkg' > README.md",
|
|
23
|
+
"severity": "error", "auto_fixable": true},
|
|
24
|
+
...
|
|
25
|
+
],
|
|
26
|
+
"next_command": "<the single fastest unblock>",
|
|
27
|
+
"blocker_count": N,
|
|
28
|
+
"auto_fixable_count": M,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Ranking is deterministic (so the same input always yields the same
|
|
32
|
+
ordered blockers): auto_fixable first (cheap wins), then by F-code
|
|
33
|
+
severity (error > warn > info), then by frequency in the report,
|
|
34
|
+
then by F-code value (lexicographic) as a tiebreak.
|
|
35
|
+
|
|
36
|
+
Pure: no I/O, no LLM call. The caller decides whether to print the
|
|
37
|
+
summary, embed it in an MCP response, or fold it into a Receipt's
|
|
38
|
+
notes field.
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
from typing import Any, TypedDict
|
|
43
|
+
|
|
44
|
+
from ..a0_qk_constants.error_codes import (
|
|
45
|
+
fcode_for_tier_violation,
|
|
46
|
+
get_fcode,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
_SEVERITY_RANK = {"error": 0, "warn": 1, "info": 2}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Blocker(TypedDict, total=False):
|
|
53
|
+
f_code: str
|
|
54
|
+
title: str
|
|
55
|
+
next_command: str
|
|
56
|
+
severity: str
|
|
57
|
+
auto_fixable: bool
|
|
58
|
+
occurrences: int # how many wire violations / issues fed this
|
|
59
|
+
sample_path: str | None # representative file (when applicable)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _certify_blockers(
|
|
63
|
+
certify_report: dict | None,
|
|
64
|
+
*,
|
|
65
|
+
package_root: str | None,
|
|
66
|
+
) -> list[Blocker]:
|
|
67
|
+
if not certify_report:
|
|
68
|
+
return []
|
|
69
|
+
out: list[Blocker] = []
|
|
70
|
+
if not certify_report.get("documentation_complete", True):
|
|
71
|
+
out.append(_blocker(
|
|
72
|
+
"F0050",
|
|
73
|
+
next_command=(
|
|
74
|
+
f"echo '# {package_root or 'your_package'}' > README.md"
|
|
75
|
+
if package_root else "add a README.md with at least an H1 + a one-paragraph intro"
|
|
76
|
+
),
|
|
77
|
+
))
|
|
78
|
+
if not certify_report.get("tests_present", True):
|
|
79
|
+
out.append(_blocker(
|
|
80
|
+
"F0051",
|
|
81
|
+
next_command="mkdir -p tests && cat > tests/test_smoke.py "
|
|
82
|
+
"<<'PY'\nimport <pkg>\nPY",
|
|
83
|
+
))
|
|
84
|
+
if not certify_report.get("tier_layout_present", True):
|
|
85
|
+
out.append(_blocker(
|
|
86
|
+
"F0052",
|
|
87
|
+
next_command=(
|
|
88
|
+
f"forge auto . ./out --apply --package "
|
|
89
|
+
f"{package_root or 'your_pkg'}"
|
|
90
|
+
),
|
|
91
|
+
))
|
|
92
|
+
if not certify_report.get("no_upward_imports", True):
|
|
93
|
+
out.append(_blocker(
|
|
94
|
+
"F0053",
|
|
95
|
+
next_command=(
|
|
96
|
+
f"forge wire {package_root or 'src/your_pkg'} "
|
|
97
|
+
"--suggest-repairs && forge enforce "
|
|
98
|
+
f"{package_root or 'src/your_pkg'} --apply"
|
|
99
|
+
),
|
|
100
|
+
))
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _wire_blockers(
|
|
105
|
+
wire_report: dict | None,
|
|
106
|
+
*,
|
|
107
|
+
package_root: str | None,
|
|
108
|
+
) -> list[Blocker]:
|
|
109
|
+
if not wire_report:
|
|
110
|
+
return []
|
|
111
|
+
violations = wire_report.get("violations") or []
|
|
112
|
+
if not violations:
|
|
113
|
+
return []
|
|
114
|
+
# Group by F-code so duplicate violations of the same kind collapse
|
|
115
|
+
# into one blocker with a sample path + occurrence count.
|
|
116
|
+
by_fcode: dict[str, list[dict]] = {}
|
|
117
|
+
for v in violations:
|
|
118
|
+
code = v.get("f_code") or fcode_for_tier_violation(
|
|
119
|
+
v.get("from_tier", ""), v.get("to_tier", ""))
|
|
120
|
+
by_fcode.setdefault(code, []).append(v)
|
|
121
|
+
out: list[Blocker] = []
|
|
122
|
+
for code, group in by_fcode.items():
|
|
123
|
+
sample = group[0]
|
|
124
|
+
sample_file = sample.get("file", "")
|
|
125
|
+
if get_fcode(code) and get_fcode(code).get("auto_fixable"): # type: ignore[union-attr]
|
|
126
|
+
cmd = (
|
|
127
|
+
f"forge enforce {package_root or 'src/your_pkg'} --apply "
|
|
128
|
+
f"# resolves {code}"
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
cmd = (
|
|
132
|
+
f"# review manually: {sample_file} cannot move mechanically; "
|
|
133
|
+
"either invert the import direction or extract the symbol "
|
|
134
|
+
"down to a lower tier."
|
|
135
|
+
)
|
|
136
|
+
out.append(_blocker(
|
|
137
|
+
code,
|
|
138
|
+
next_command=cmd,
|
|
139
|
+
occurrences=len(group),
|
|
140
|
+
sample_path=sample_file,
|
|
141
|
+
))
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _blocker(
|
|
146
|
+
f_code: str,
|
|
147
|
+
*,
|
|
148
|
+
next_command: str,
|
|
149
|
+
occurrences: int = 1,
|
|
150
|
+
sample_path: str | None = None,
|
|
151
|
+
) -> Blocker:
|
|
152
|
+
entry = get_fcode(f_code)
|
|
153
|
+
if entry is None:
|
|
154
|
+
return Blocker(
|
|
155
|
+
f_code=f_code,
|
|
156
|
+
title=f"unregistered F-code: {f_code}",
|
|
157
|
+
next_command=next_command,
|
|
158
|
+
severity="error",
|
|
159
|
+
auto_fixable=False,
|
|
160
|
+
occurrences=occurrences,
|
|
161
|
+
sample_path=sample_path,
|
|
162
|
+
)
|
|
163
|
+
return Blocker(
|
|
164
|
+
f_code=f_code,
|
|
165
|
+
title=entry["title"],
|
|
166
|
+
next_command=next_command,
|
|
167
|
+
severity=entry["severity"],
|
|
168
|
+
auto_fixable=entry["auto_fixable"],
|
|
169
|
+
occurrences=occurrences,
|
|
170
|
+
sample_path=sample_path,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _rank(blocker: Blocker) -> tuple[int, int, int, str]:
|
|
175
|
+
"""Stable ranking: auto_fixable first, then severity, then frequency
|
|
176
|
+
(descending), then F-code lex order."""
|
|
177
|
+
return (
|
|
178
|
+
0 if blocker.get("auto_fixable") else 1,
|
|
179
|
+
_SEVERITY_RANK.get(blocker.get("severity", "error"), 0),
|
|
180
|
+
-int(blocker.get("occurrences", 1)),
|
|
181
|
+
blocker.get("f_code", ""),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def summarize_blockers(
|
|
186
|
+
*,
|
|
187
|
+
wire_report: dict | None = None,
|
|
188
|
+
certify_report: dict | None = None,
|
|
189
|
+
package_root: str | None = None,
|
|
190
|
+
top_n: int = 5,
|
|
191
|
+
) -> dict[str, Any]:
|
|
192
|
+
"""Compact-blocker summary for agent consumption (Codex feedback).
|
|
193
|
+
|
|
194
|
+
Either ``wire_report`` or ``certify_report`` (or both) may be
|
|
195
|
+
None; the function returns whatever blockers it can derive.
|
|
196
|
+
``top_n`` (default 5) caps the returned blocker list — the
|
|
197
|
+
blocker_count field always reflects the FULL count regardless.
|
|
198
|
+
"""
|
|
199
|
+
if top_n < 1:
|
|
200
|
+
raise ValueError("top_n must be >= 1")
|
|
201
|
+
|
|
202
|
+
blockers = _certify_blockers(certify_report, package_root=package_root)
|
|
203
|
+
blockers.extend(_wire_blockers(wire_report, package_root=package_root))
|
|
204
|
+
blockers.sort(key=_rank)
|
|
205
|
+
|
|
206
|
+
auto = sum(1 for b in blockers if b.get("auto_fixable"))
|
|
207
|
+
# Codex feedback: bare-wire callers have no certify score; render
|
|
208
|
+
# 'no score' instead of a misleading 0/100. None signals 'unknown'.
|
|
209
|
+
score = (certify_report or {}).get("score") if certify_report is not None else None
|
|
210
|
+
if certify_report is None and wire_report is not None:
|
|
211
|
+
# Bare-wire callers: derive a coarse verdict from wire alone.
|
|
212
|
+
verdict = wire_report.get("verdict", "FAIL")
|
|
213
|
+
elif wire_report is not None and (certify_report or {}).get("score", 0) >= 100 \
|
|
214
|
+
and wire_report.get("verdict") == "PASS":
|
|
215
|
+
verdict = "PASS"
|
|
216
|
+
elif certify_report is not None and not blockers:
|
|
217
|
+
verdict = "PASS"
|
|
218
|
+
else:
|
|
219
|
+
verdict = "FAIL"
|
|
220
|
+
|
|
221
|
+
next_command = blockers[0]["next_command"] if blockers else (
|
|
222
|
+
"# already PASSing — re-run on the next change."
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
"schema_version": "atomadic-forge.summary/v1",
|
|
227
|
+
"verdict": verdict,
|
|
228
|
+
"score": score,
|
|
229
|
+
"blocker_count": len(blockers),
|
|
230
|
+
"auto_fixable_count": auto,
|
|
231
|
+
"blockers": list(blockers[:top_n]),
|
|
232
|
+
"next_command": next_command,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def render_summary_text(summary: dict, *, width: int = 60) -> str:
|
|
237
|
+
"""Single-screen plain-text rendering of the summary.
|
|
238
|
+
|
|
239
|
+
Designed for human terminals AND for agent consumption: every
|
|
240
|
+
line is short, every blocker fits on 2 lines, the next-command
|
|
241
|
+
is the very last visible row.
|
|
242
|
+
"""
|
|
243
|
+
if width < 30:
|
|
244
|
+
raise ValueError("width must be >= 30")
|
|
245
|
+
lines: list[str] = []
|
|
246
|
+
verdict = summary.get("verdict", "?")
|
|
247
|
+
score = summary.get("score", 0)
|
|
248
|
+
n = summary.get("blocker_count", 0)
|
|
249
|
+
auto = summary.get("auto_fixable_count", 0)
|
|
250
|
+
glyph = {"PASS": "✓", "FAIL": "✗", "REFINE": "↻", "QUARANTINE": "⏸"}.get(
|
|
251
|
+
verdict, "?")
|
|
252
|
+
score_part = (f"score {score:.0f}/100 "
|
|
253
|
+
if isinstance(score, int | float) else "")
|
|
254
|
+
lines.append(f"{glyph} {verdict} {score_part}"
|
|
255
|
+
f"{n} blocker{'' if n == 1 else 's'} "
|
|
256
|
+
f"({auto} auto-fixable)")
|
|
257
|
+
lines.append("─" * width)
|
|
258
|
+
if not summary.get("blockers"):
|
|
259
|
+
lines.append("(no blockers)")
|
|
260
|
+
for i, b in enumerate(summary.get("blockers", []), 1):
|
|
261
|
+
tag = "AUTO" if b.get("auto_fixable") else "REVIEW"
|
|
262
|
+
title = b.get("title", "")
|
|
263
|
+
prefix = f" {i}. [{b.get('f_code', 'F????')}] [{tag}] "
|
|
264
|
+
# Cap the WHOLE row at width, not just the title.
|
|
265
|
+
budget = max(0, width - len(prefix))
|
|
266
|
+
if len(title) > budget:
|
|
267
|
+
title = title[: max(0, budget - 1)] + "…"
|
|
268
|
+
lines.append(prefix + title)
|
|
269
|
+
cmd = b.get("next_command", "").strip()
|
|
270
|
+
if cmd:
|
|
271
|
+
short = cmd if len(cmd) < width - 6 else cmd[: width - 7] + "…"
|
|
272
|
+
lines.append(f" → {short}")
|
|
273
|
+
lines.append("─" * width)
|
|
274
|
+
nc = summary.get("next_command", "").strip()
|
|
275
|
+
if nc:
|
|
276
|
+
lines.append(f"NEXT: {nc[: width - 6]}")
|
|
277
|
+
return "\n".join(lines)
|