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,1066 @@
|
|
|
1
|
+
"""Tier a1 — pure MCP JSON-RPC dispatch for `forge mcp serve`.
|
|
2
|
+
|
|
3
|
+
Golden Path Lane C W4 deliverable. The dispatcher is a pure function
|
|
4
|
+
``dispatch_request(req, ctx) -> response`` — no I/O, no global state.
|
|
5
|
+
The transport (stdio loop) lives in a3 ``mcp_server.py``.
|
|
6
|
+
|
|
7
|
+
Implements the slice of the MCP spec that coding agents actually
|
|
8
|
+
consume on first connect:
|
|
9
|
+
|
|
10
|
+
* ``initialize`` — capability handshake; returns serverInfo +
|
|
11
|
+
supported protocol version + tool/resource
|
|
12
|
+
capability flags.
|
|
13
|
+
* ``ping`` — liveness check (returns ``{}``).
|
|
14
|
+
* ``tools/list`` — names + JSON Schemas for the 4 Forge tools.
|
|
15
|
+
* ``tools/call`` — runs one of the named tools and returns
|
|
16
|
+
the result wrapped in MCP's ``content`` shape.
|
|
17
|
+
* ``resources/list`` — Forge documentation + lineage URIs.
|
|
18
|
+
* ``resources/read`` — read a Forge resource (docs, lineage, schema).
|
|
19
|
+
|
|
20
|
+
Tools today (Lane C W4):
|
|
21
|
+
recon, wire, certify, enforce, audit_list
|
|
22
|
+
|
|
23
|
+
Resources today:
|
|
24
|
+
forge://docs/receipt — docs/RECEIPT.md
|
|
25
|
+
forge://docs/formalization — docs/FORMALIZATION.md (citations)
|
|
26
|
+
forge://lineage/chain — local lineage chain JSONL
|
|
27
|
+
forge://schema/receipt — Receipt v1 JSON schema reference
|
|
28
|
+
|
|
29
|
+
The dispatcher returns proper JSON-RPC 2.0 responses, including
|
|
30
|
+
``-32601`` (method not found) and ``-32602`` (invalid params) error
|
|
31
|
+
codes when it can't honor a request. Pure function — exceptions raised
|
|
32
|
+
by tool handlers are caught and converted to ``-32000`` (server error)
|
|
33
|
+
responses; callers never see a Python traceback.
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
from collections.abc import Callable
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any
|
|
41
|
+
|
|
42
|
+
from .. import __version__
|
|
43
|
+
from ..a0_qk_constants.receipt_schema import SCHEMA_VERSION_V1
|
|
44
|
+
from .agent_context_pack import emit_context_pack
|
|
45
|
+
from .agent_memory import what_failed_last_time, why_did_this_change
|
|
46
|
+
from .agent_summary import summarize_blockers
|
|
47
|
+
from .certify_checks import certify
|
|
48
|
+
from .lineage_chain import canonical_receipt_hash, verify_chain_link
|
|
49
|
+
from .lineage_reader import list_artifacts, read_lineage
|
|
50
|
+
from .patch_scorer import score_patch as _score_patch
|
|
51
|
+
from .plan_adapter import adapt_plan as _adapt_plan
|
|
52
|
+
from .policy_loader import load_policy as _load_policy
|
|
53
|
+
from .preflight_change import preflight_change as _preflight_change
|
|
54
|
+
from .receipt_emitter import build_receipt
|
|
55
|
+
from .recipes import all_recipes, get_recipe, list_recipes
|
|
56
|
+
from .repo_explainer import explain_repo as _explain_repo
|
|
57
|
+
from .scout_walk import harvest_repo
|
|
58
|
+
from .test_selector import select_tests as _select_tests
|
|
59
|
+
from .tool_composer import compose_tools as _compose_tools
|
|
60
|
+
from .wire_check import scan_violations
|
|
61
|
+
|
|
62
|
+
PROTOCOL_VERSION = "2024-11-05" # MCP spec rev the server advertises
|
|
63
|
+
SERVER_NAME = "atomadic-forge"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
_JSON_RPC_VERSION = "2.0"
|
|
67
|
+
_ERR_PARSE = -32700
|
|
68
|
+
_ERR_INVALID_REQUEST = -32600
|
|
69
|
+
_ERR_METHOD_NOT_FOUND = -32601
|
|
70
|
+
_ERR_INVALID_PARAMS = -32602
|
|
71
|
+
_ERR_INTERNAL = -32603
|
|
72
|
+
_ERR_SERVER = -32000
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --- Tool registry (pure: handlers receive a project_root path) ----------
|
|
76
|
+
|
|
77
|
+
ToolHandler = Callable[[Path, dict[str, Any]], dict[str, Any]]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _tool_recon(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
81
|
+
target = Path(args.get("target", project_root)).resolve()
|
|
82
|
+
return harvest_repo(target)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _tool_wire(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
86
|
+
src = Path(args.get("source", project_root)).resolve()
|
|
87
|
+
return scan_violations(
|
|
88
|
+
src,
|
|
89
|
+
suggest_repairs=bool(args.get("suggest_repairs", False)),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _tool_certify(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
94
|
+
root = Path(args.get("project_root", project_root)).resolve()
|
|
95
|
+
package = args.get("package")
|
|
96
|
+
cert = certify(root, project=root.name, package=package)
|
|
97
|
+
if not args.get("emit_receipt"):
|
|
98
|
+
return cert
|
|
99
|
+
# When emit_receipt is requested, build a v1 Receipt around the
|
|
100
|
+
# certify result and return both for the caller's convenience.
|
|
101
|
+
wire = scan_violations(root)
|
|
102
|
+
scout = harvest_repo(root)
|
|
103
|
+
receipt = build_receipt(
|
|
104
|
+
certify_result=cert,
|
|
105
|
+
wire_report=wire,
|
|
106
|
+
scout_report=scout,
|
|
107
|
+
project_name=root.name,
|
|
108
|
+
project_root=root,
|
|
109
|
+
forge_version=__version__,
|
|
110
|
+
package=package,
|
|
111
|
+
)
|
|
112
|
+
return {"certify": cert, "receipt": receipt}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _tool_enforce_unbound(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
116
|
+
"""Default enforce handler — a3 binds the real implementation at
|
|
117
|
+
import time via ``register_enforce_handler``. Until a3 has loaded
|
|
118
|
+
(e.g. when only a1 is imported in tests), this stub returns a
|
|
119
|
+
structured 'unwired' response so callers can detect the state.
|
|
120
|
+
"""
|
|
121
|
+
return {
|
|
122
|
+
"schema_version": "atomadic-forge.enforce/v1",
|
|
123
|
+
"wired": False,
|
|
124
|
+
"note": (
|
|
125
|
+
"forge enforce tool not yet wired — import "
|
|
126
|
+
"atomadic_forge.a3_og_features.mcp_server (or any code "
|
|
127
|
+
"under a3) to register the real handler."
|
|
128
|
+
),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
_enforce_handler: ToolHandler = _tool_enforce_unbound
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _tool_enforce(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
136
|
+
return _enforce_handler(project_root, args)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def register_enforce_handler(handler: ToolHandler) -> None:
|
|
140
|
+
"""Bind the real ``run_enforce``-backed handler from a3.
|
|
141
|
+
|
|
142
|
+
Pure module-state replacement (a global within this a1 module).
|
|
143
|
+
a3's mcp_server.py calls this at import time so when the CLI
|
|
144
|
+
surface (a4) imports a3.mcp_server, the dispatcher's enforce
|
|
145
|
+
handler is wired automatically — no upward import in a1.
|
|
146
|
+
"""
|
|
147
|
+
global _enforce_handler
|
|
148
|
+
_enforce_handler = handler
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _tool_audit_list(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
152
|
+
root = Path(args.get("project_root", project_root)).resolve()
|
|
153
|
+
return {
|
|
154
|
+
"schema_version": "atomadic-forge.audit.list/v1",
|
|
155
|
+
"project": str(root),
|
|
156
|
+
"artifacts": list_artifacts(root),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _tool_context_pack(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
161
|
+
"""Codex 'Copilot's Copilot' #1 — first-call context pack."""
|
|
162
|
+
root = Path(args.get("target", project_root)).resolve()
|
|
163
|
+
try:
|
|
164
|
+
scout = harvest_repo(root)
|
|
165
|
+
except (OSError, ValueError):
|
|
166
|
+
scout = None
|
|
167
|
+
try:
|
|
168
|
+
wire = scan_violations(root)
|
|
169
|
+
except (OSError, ValueError):
|
|
170
|
+
wire = None
|
|
171
|
+
try:
|
|
172
|
+
cert = certify(root, project=root.name)
|
|
173
|
+
except (OSError, RuntimeError, ValueError):
|
|
174
|
+
cert = None
|
|
175
|
+
return emit_context_pack(
|
|
176
|
+
project_root=root,
|
|
177
|
+
scout_report=scout, wire_report=wire, certify_report=cert,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _tool_preflight_change(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
182
|
+
"""Codex 'Copilot's Copilot' #2 — pre-edit guardrail."""
|
|
183
|
+
root = Path(args.get("project_root", project_root)).resolve()
|
|
184
|
+
intent = str(args.get("intent", ""))
|
|
185
|
+
proposed = list(args.get("proposed_files") or [])
|
|
186
|
+
threshold = int(args.get("scope_threshold", 8))
|
|
187
|
+
if not intent:
|
|
188
|
+
return {"schema_version": "atomadic-forge.preflight/v1",
|
|
189
|
+
"error": "intent is required"}
|
|
190
|
+
if not proposed:
|
|
191
|
+
return {"schema_version": "atomadic-forge.preflight/v1",
|
|
192
|
+
"error": "proposed_files must be non-empty"}
|
|
193
|
+
return _preflight_change(
|
|
194
|
+
intent=intent, proposed_files=proposed,
|
|
195
|
+
project_root=root, scope_threshold=threshold,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _tool_score_patch(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
200
|
+
"""Codex 'Copilot's Copilot' #3 — pre-merge patch risk scorer."""
|
|
201
|
+
diff = str(args.get("diff", ""))
|
|
202
|
+
return _score_patch(diff)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _tool_select_tests(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
206
|
+
"""Codex #7 — minimum + full-confidence test set per intent."""
|
|
207
|
+
root = Path(args.get("project_root", project_root)).resolve()
|
|
208
|
+
return _select_tests(
|
|
209
|
+
intent=str(args.get("intent", "")),
|
|
210
|
+
changed_files=list(args.get("changed_files") or []),
|
|
211
|
+
project_root=root,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _tool_rollback_plan(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
216
|
+
"""Codex #11 — reversible-move guidance."""
|
|
217
|
+
from .rollback_planner import rollback_plan as _rb
|
|
218
|
+
root = Path(args.get("project_root", project_root)).resolve()
|
|
219
|
+
return _rb(
|
|
220
|
+
changed_files=list(args.get("changed_files") or []),
|
|
221
|
+
project_root=root,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _tool_explain_repo(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
226
|
+
"""Codex #6 — humane operational orientation."""
|
|
227
|
+
root = Path(args.get("project_root", project_root)).resolve()
|
|
228
|
+
try:
|
|
229
|
+
scout = harvest_repo(root)
|
|
230
|
+
except (OSError, ValueError):
|
|
231
|
+
scout = None
|
|
232
|
+
try:
|
|
233
|
+
wire = scan_violations(root)
|
|
234
|
+
except (OSError, ValueError):
|
|
235
|
+
wire = None
|
|
236
|
+
try:
|
|
237
|
+
cert = certify(root, project=root.name)
|
|
238
|
+
except (OSError, RuntimeError, ValueError):
|
|
239
|
+
cert = None
|
|
240
|
+
pack = emit_context_pack(project_root=root, scout_report=scout,
|
|
241
|
+
wire_report=wire, certify_report=cert)
|
|
242
|
+
return _explain_repo(
|
|
243
|
+
project_root=root,
|
|
244
|
+
repo_purpose=pack.get("repo_purpose", ""),
|
|
245
|
+
scout_report=scout, wire_report=wire, certify_report=cert,
|
|
246
|
+
depth=str(args.get("depth", "agent")),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _tool_adapt_plan(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
251
|
+
"""Codex #8 — capability-aware card filtering."""
|
|
252
|
+
plan = args.get("plan") or {}
|
|
253
|
+
if not isinstance(plan, dict):
|
|
254
|
+
return {"error": "plan must be an agent_plan/v1 object"}
|
|
255
|
+
return _adapt_plan(
|
|
256
|
+
plan,
|
|
257
|
+
agent_capabilities=list(args.get("agent_capabilities") or []),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _tool_compose_tools(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
262
|
+
"""Codex #9 — tool-use planner."""
|
|
263
|
+
return _compose_tools(goal=str(args.get("goal", "")))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _tool_load_policy(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
267
|
+
"""Codex #10 — read [tool.forge.agent] from pyproject.toml."""
|
|
268
|
+
root = Path(args.get("project_root", project_root)).resolve()
|
|
269
|
+
return _load_policy(root)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _tool_why_did_this_change(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
273
|
+
"""Codex #5 — agent memory: lineage + plan-event lookup."""
|
|
274
|
+
root = Path(args.get("project_root", project_root)).resolve()
|
|
275
|
+
return why_did_this_change(file=str(args.get("file", "")), project_root=root)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _tool_what_failed_last_time(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
279
|
+
"""Codex #5 — failed/rolled_back plan events for an area."""
|
|
280
|
+
root = Path(args.get("project_root", project_root)).resolve()
|
|
281
|
+
return what_failed_last_time(area=str(args.get("area", "")), project_root=root)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _tool_list_recipes(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
285
|
+
"""Codex #12 — list golden-path recipes."""
|
|
286
|
+
return {
|
|
287
|
+
"schema_version": "atomadic-forge.recipe.list/v1",
|
|
288
|
+
"recipes": list_recipes(),
|
|
289
|
+
"catalogue": {n: r["description"] for n, r in all_recipes().items()},
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _tool_get_recipe(project_root: Path, args: dict[str, Any]) -> dict[str, Any]:
|
|
294
|
+
"""Codex #12 — fetch a named recipe."""
|
|
295
|
+
name = str(args.get("name", ""))
|
|
296
|
+
recipe = get_recipe(name)
|
|
297
|
+
if recipe is None:
|
|
298
|
+
return {"error": f"unknown recipe: {name!r}"}
|
|
299
|
+
return recipe
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _tool_auto_plan_unbound(project_root: Path,
|
|
303
|
+
args: dict[str, Any]) -> dict[str, Any]:
|
|
304
|
+
"""auto_plan stub — a3 binds the real ``run_auto_plan`` at import
|
|
305
|
+
time via ``register_auto_plan_handler``. Same a1↔a3 injection
|
|
306
|
+
pattern the enforce tool uses (see Lane C W4 commit msg).
|
|
307
|
+
"""
|
|
308
|
+
return {
|
|
309
|
+
"schema_version": "atomadic-forge.agent_plan/v1",
|
|
310
|
+
"wired": False,
|
|
311
|
+
"note": (
|
|
312
|
+
"auto_plan tool not yet wired — import "
|
|
313
|
+
"atomadic_forge.a3_og_features.mcp_server (or any code "
|
|
314
|
+
"under a3) to register the real handler."
|
|
315
|
+
),
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
_auto_plan_handler: ToolHandler = _tool_auto_plan_unbound
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _tool_auto_plan(project_root: Path,
|
|
323
|
+
args: dict[str, Any]) -> dict[str, Any]:
|
|
324
|
+
return _auto_plan_handler(project_root, args)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def register_auto_plan_handler(handler: ToolHandler) -> None:
|
|
328
|
+
"""Bind the real auto_plan handler from a3 (mirror of
|
|
329
|
+
register_enforce_handler)."""
|
|
330
|
+
global _auto_plan_handler
|
|
331
|
+
_auto_plan_handler = handler
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _tool_auto_step_unbound(project_root, args):
|
|
335
|
+
return {
|
|
336
|
+
"schema_version": "atomadic-forge.plan_apply/v1",
|
|
337
|
+
"wired": False,
|
|
338
|
+
"note": "auto_step not wired — import a3.mcp_server.",
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _tool_auto_apply_unbound(project_root, args):
|
|
343
|
+
return {
|
|
344
|
+
"schema_version": "atomadic-forge.plan_apply_all/v1",
|
|
345
|
+
"wired": False,
|
|
346
|
+
"note": "auto_apply not wired — import a3.mcp_server.",
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
_auto_step_handler: ToolHandler = _tool_auto_step_unbound
|
|
351
|
+
_auto_apply_handler: ToolHandler = _tool_auto_apply_unbound
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _tool_auto_step(project_root, args):
|
|
355
|
+
return _auto_step_handler(project_root, args)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _tool_auto_apply(project_root, args):
|
|
359
|
+
return _auto_apply_handler(project_root, args)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def register_auto_step_handler(handler: ToolHandler) -> None:
|
|
363
|
+
global _auto_step_handler
|
|
364
|
+
_auto_step_handler = handler
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def register_auto_apply_handler(handler: ToolHandler) -> None:
|
|
368
|
+
global _auto_apply_handler
|
|
369
|
+
_auto_apply_handler = handler
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
TOOLS: dict[str, dict[str, Any]] = {
|
|
373
|
+
"recon": {
|
|
374
|
+
"name": "recon",
|
|
375
|
+
"description": "Walk a repo and classify every public symbol "
|
|
376
|
+
"into one of the 5 monadic tiers. Returns a "
|
|
377
|
+
"scout report with tier_distribution + symbols.",
|
|
378
|
+
"inputSchema": {
|
|
379
|
+
"type": "object",
|
|
380
|
+
"properties": {
|
|
381
|
+
"target": {"type": "string",
|
|
382
|
+
"description": "Repo path; defaults to project_root."},
|
|
383
|
+
},
|
|
384
|
+
"additionalProperties": False,
|
|
385
|
+
},
|
|
386
|
+
"handler": _tool_recon,
|
|
387
|
+
},
|
|
388
|
+
"wire": {
|
|
389
|
+
"name": "wire",
|
|
390
|
+
"description": "Scan a tier-organized package for upward-import "
|
|
391
|
+
"violations. With suggest_repairs=true, emits "
|
|
392
|
+
"auto_fixable count + repair_suggestions per file.",
|
|
393
|
+
"inputSchema": {
|
|
394
|
+
"type": "object",
|
|
395
|
+
"properties": {
|
|
396
|
+
"source": {"type": "string"},
|
|
397
|
+
"suggest_repairs": {"type": "boolean", "default": False},
|
|
398
|
+
},
|
|
399
|
+
"additionalProperties": False,
|
|
400
|
+
},
|
|
401
|
+
"handler": _tool_wire,
|
|
402
|
+
},
|
|
403
|
+
"certify": {
|
|
404
|
+
"name": "certify",
|
|
405
|
+
"description": "Score documentation + tests + tier layout + import "
|
|
406
|
+
"discipline. With emit_receipt=true, also emits a "
|
|
407
|
+
"Forge Receipt v1 JSON.",
|
|
408
|
+
"inputSchema": {
|
|
409
|
+
"type": "object",
|
|
410
|
+
"properties": {
|
|
411
|
+
"project_root": {"type": "string"},
|
|
412
|
+
"package": {"type": ["string", "null"]},
|
|
413
|
+
"emit_receipt": {"type": "boolean", "default": False},
|
|
414
|
+
},
|
|
415
|
+
"additionalProperties": False,
|
|
416
|
+
},
|
|
417
|
+
"handler": _tool_certify,
|
|
418
|
+
},
|
|
419
|
+
"enforce": {
|
|
420
|
+
"name": "enforce",
|
|
421
|
+
"description": "Plan (or apply) mechanical fixes for wire "
|
|
422
|
+
"violations. F-code routed; rolls back any fix "
|
|
423
|
+
"that increases the violation count.",
|
|
424
|
+
"inputSchema": {
|
|
425
|
+
"type": "object",
|
|
426
|
+
"properties": {
|
|
427
|
+
"source": {"type": "string"},
|
|
428
|
+
"apply": {"type": "boolean", "default": False},
|
|
429
|
+
},
|
|
430
|
+
"additionalProperties": False,
|
|
431
|
+
},
|
|
432
|
+
"handler": _tool_enforce,
|
|
433
|
+
},
|
|
434
|
+
"audit_list": {
|
|
435
|
+
"name": "audit_list",
|
|
436
|
+
"description": "Summarize every artifact written under "
|
|
437
|
+
".atomadic-forge/lineage.jsonl: name, run count, "
|
|
438
|
+
"latest write timestamp, path.",
|
|
439
|
+
"inputSchema": {
|
|
440
|
+
"type": "object",
|
|
441
|
+
"properties": {"project_root": {"type": "string"}},
|
|
442
|
+
"additionalProperties": False,
|
|
443
|
+
},
|
|
444
|
+
"handler": _tool_audit_list,
|
|
445
|
+
},
|
|
446
|
+
"auto_plan": {
|
|
447
|
+
"name": "auto_plan",
|
|
448
|
+
"description": (
|
|
449
|
+
"Codex's 'next best action card' generator. Runs scout + "
|
|
450
|
+
"wire + certify and emits an agent_plan/v1 with top-N "
|
|
451
|
+
"ranked AgentActionCard entries (kind, why, write_scope, "
|
|
452
|
+
"risk, applyable, commands, next_command). The active "
|
|
453
|
+
"agent picks one card and runs its next_command; Forge "
|
|
454
|
+
"does NOT mutate the repo from this tool."
|
|
455
|
+
),
|
|
456
|
+
"inputSchema": {
|
|
457
|
+
"type": "object",
|
|
458
|
+
"properties": {
|
|
459
|
+
"target": {"type": "string"},
|
|
460
|
+
"goal": {"type": "string",
|
|
461
|
+
"default": "improve repo conformance"},
|
|
462
|
+
"mode": {"type": "string",
|
|
463
|
+
"enum": ["improve", "absorb"],
|
|
464
|
+
"default": "improve"},
|
|
465
|
+
"package": {"type": ["string", "null"]},
|
|
466
|
+
"top_n": {"type": "integer", "default": 7},
|
|
467
|
+
"save": {"type": "boolean", "default": False,
|
|
468
|
+
"description": "Persist the plan + return its id."},
|
|
469
|
+
},
|
|
470
|
+
"additionalProperties": False,
|
|
471
|
+
},
|
|
472
|
+
"handler": _tool_auto_plan,
|
|
473
|
+
},
|
|
474
|
+
"auto_step": {
|
|
475
|
+
"name": "auto_step",
|
|
476
|
+
"description": (
|
|
477
|
+
"Apply ONE card from a saved plan. apply=False is dry-run "
|
|
478
|
+
"(default); apply=True executes the bounded change. The "
|
|
479
|
+
"card's outcome (applied / rolled_back / skipped / failed) "
|
|
480
|
+
"is recorded in the plan's state file."
|
|
481
|
+
),
|
|
482
|
+
"inputSchema": {
|
|
483
|
+
"type": "object",
|
|
484
|
+
"properties": {
|
|
485
|
+
"project": {"type": "string"},
|
|
486
|
+
"plan_id": {"type": "string"},
|
|
487
|
+
"card_id": {"type": "string"},
|
|
488
|
+
"apply": {"type": "boolean", "default": False},
|
|
489
|
+
},
|
|
490
|
+
"required": ["plan_id", "card_id"],
|
|
491
|
+
"additionalProperties": False,
|
|
492
|
+
},
|
|
493
|
+
"handler": _tool_auto_step,
|
|
494
|
+
},
|
|
495
|
+
"auto_apply": {
|
|
496
|
+
"name": "auto_apply",
|
|
497
|
+
"description": (
|
|
498
|
+
"Apply ALL applyable cards from a saved plan in order. "
|
|
499
|
+
"Halts on the first rolled_back or failed outcome so the "
|
|
500
|
+
"agent can inspect before cascading further mutations."
|
|
501
|
+
),
|
|
502
|
+
"inputSchema": {
|
|
503
|
+
"type": "object",
|
|
504
|
+
"properties": {
|
|
505
|
+
"project": {"type": "string"},
|
|
506
|
+
"plan_id": {"type": "string"},
|
|
507
|
+
"apply": {"type": "boolean", "default": False},
|
|
508
|
+
},
|
|
509
|
+
"required": ["plan_id"],
|
|
510
|
+
"additionalProperties": False,
|
|
511
|
+
},
|
|
512
|
+
"handler": _tool_auto_apply,
|
|
513
|
+
},
|
|
514
|
+
"context_pack": {
|
|
515
|
+
"name": "context_pack",
|
|
516
|
+
"description": (
|
|
517
|
+
"Codex 'Copilot's Copilot' #1 — first-call context bundle. "
|
|
518
|
+
"Returns repo purpose, the architecture law, tier map, "
|
|
519
|
+
"current blockers, best next action, test commands, "
|
|
520
|
+
"release gate, risky files, and recent lineage. The single "
|
|
521
|
+
"tool every coding agent should call on connect."
|
|
522
|
+
),
|
|
523
|
+
"inputSchema": {
|
|
524
|
+
"type": "object",
|
|
525
|
+
"properties": {
|
|
526
|
+
"target": {"type": "string",
|
|
527
|
+
"description": "Project path; defaults to project_root."},
|
|
528
|
+
},
|
|
529
|
+
"additionalProperties": False,
|
|
530
|
+
},
|
|
531
|
+
"handler": _tool_context_pack,
|
|
532
|
+
},
|
|
533
|
+
"preflight_change": {
|
|
534
|
+
"name": "preflight_change",
|
|
535
|
+
"description": (
|
|
536
|
+
"Codex 'Copilot's Copilot' #2 — pre-edit guardrail. Given "
|
|
537
|
+
"an intent string + a list of proposed_files, returns each "
|
|
538
|
+
"file's detected tier, forbidden imports, likely affected "
|
|
539
|
+
"tests, sibling files to read first, and whether the "
|
|
540
|
+
"write_scope is too broad. Most agent mistakes happen "
|
|
541
|
+
"BEFORE code is written; this surfaces them in advance."
|
|
542
|
+
),
|
|
543
|
+
"inputSchema": {
|
|
544
|
+
"type": "object",
|
|
545
|
+
"properties": {
|
|
546
|
+
"project_root": {"type": "string"},
|
|
547
|
+
"intent": {"type": "string"},
|
|
548
|
+
"proposed_files": {"type": "array",
|
|
549
|
+
"items": {"type": "string"}},
|
|
550
|
+
"scope_threshold": {"type": "integer", "default": 8},
|
|
551
|
+
},
|
|
552
|
+
"required": ["intent", "proposed_files"],
|
|
553
|
+
"additionalProperties": False,
|
|
554
|
+
},
|
|
555
|
+
"handler": _tool_preflight_change,
|
|
556
|
+
},
|
|
557
|
+
"score_patch": {
|
|
558
|
+
"name": "score_patch",
|
|
559
|
+
"description": (
|
|
560
|
+
"Codex 'Copilot's Copilot' #3 — patch risk scorer. Submit "
|
|
561
|
+
"a unified-diff string and get back architecture risk, "
|
|
562
|
+
"test risk, public_API risk, release risk, a "
|
|
563
|
+
"needs_human_review boolean, and suggested validation "
|
|
564
|
+
"commands. Forge becomes a PR reviewer BEFORE the PR exists."
|
|
565
|
+
),
|
|
566
|
+
"inputSchema": {
|
|
567
|
+
"type": "object",
|
|
568
|
+
"properties": {
|
|
569
|
+
"diff": {"type": "string",
|
|
570
|
+
"description": "Unified-diff text "
|
|
571
|
+
"(git diff / patch format)."},
|
|
572
|
+
},
|
|
573
|
+
"required": ["diff"],
|
|
574
|
+
"additionalProperties": False,
|
|
575
|
+
},
|
|
576
|
+
"handler": _tool_score_patch,
|
|
577
|
+
},
|
|
578
|
+
"select_tests": {
|
|
579
|
+
"name": "select_tests",
|
|
580
|
+
"description": (
|
|
581
|
+
"Codex #7 — minimum + full-confidence test sets per "
|
|
582
|
+
"intent. Returns mirror-name matches plus tier-mate tests; "
|
|
583
|
+
"agents stop over-running or under-running tests."
|
|
584
|
+
),
|
|
585
|
+
"inputSchema": {
|
|
586
|
+
"type": "object",
|
|
587
|
+
"properties": {
|
|
588
|
+
"project_root": {"type": "string"},
|
|
589
|
+
"intent": {"type": "string"},
|
|
590
|
+
"changed_files": {"type": "array",
|
|
591
|
+
"items": {"type": "string"}},
|
|
592
|
+
},
|
|
593
|
+
"required": ["changed_files"],
|
|
594
|
+
"additionalProperties": False,
|
|
595
|
+
},
|
|
596
|
+
"handler": _tool_select_tests,
|
|
597
|
+
},
|
|
598
|
+
"rollback_plan": {
|
|
599
|
+
"name": "rollback_plan",
|
|
600
|
+
"description": (
|
|
601
|
+
"Codex #11 — structured undo plan: files to remove, caches "
|
|
602
|
+
"to clean, docs to restore, tests to rerun, risk level."
|
|
603
|
+
),
|
|
604
|
+
"inputSchema": {
|
|
605
|
+
"type": "object",
|
|
606
|
+
"properties": {
|
|
607
|
+
"project_root": {"type": "string"},
|
|
608
|
+
"changed_files": {"type": "array",
|
|
609
|
+
"items": {"type": "string"}},
|
|
610
|
+
},
|
|
611
|
+
"required": ["changed_files"],
|
|
612
|
+
"additionalProperties": False,
|
|
613
|
+
},
|
|
614
|
+
"handler": _tool_rollback_plan,
|
|
615
|
+
},
|
|
616
|
+
"explain_repo": {
|
|
617
|
+
"name": "explain_repo",
|
|
618
|
+
"description": (
|
|
619
|
+
"Codex #6 — humane operational orientation. One-liner + "
|
|
620
|
+
"core flow + do_not_break list + important tests + "
|
|
621
|
+
"release state. Different from context_pack (which is "
|
|
622
|
+
"data-rich); this is decision-oriented."
|
|
623
|
+
),
|
|
624
|
+
"inputSchema": {
|
|
625
|
+
"type": "object",
|
|
626
|
+
"properties": {
|
|
627
|
+
"project_root": {"type": "string"},
|
|
628
|
+
"depth": {"type": "string", "default": "agent"},
|
|
629
|
+
},
|
|
630
|
+
"additionalProperties": False,
|
|
631
|
+
},
|
|
632
|
+
"handler": _tool_explain_repo,
|
|
633
|
+
},
|
|
634
|
+
"adapt_plan": {
|
|
635
|
+
"name": "adapt_plan",
|
|
636
|
+
"description": (
|
|
637
|
+
"Codex #8 — capability-aware card filtering. Tag each "
|
|
638
|
+
"card with recommended_handling: apply / delegate / "
|
|
639
|
+
"ask_human / report_only based on agent_capabilities."
|
|
640
|
+
),
|
|
641
|
+
"inputSchema": {
|
|
642
|
+
"type": "object",
|
|
643
|
+
"properties": {
|
|
644
|
+
"plan": {"type": "object"},
|
|
645
|
+
"agent_capabilities": {"type": "array",
|
|
646
|
+
"items": {"type": "string"}},
|
|
647
|
+
},
|
|
648
|
+
"required": ["plan"],
|
|
649
|
+
"additionalProperties": False,
|
|
650
|
+
},
|
|
651
|
+
"handler": _tool_adapt_plan,
|
|
652
|
+
},
|
|
653
|
+
"compose_tools": {
|
|
654
|
+
"name": "compose_tools",
|
|
655
|
+
"description": (
|
|
656
|
+
"Codex #9 — tool-use planner. Match a goal keyword to a "
|
|
657
|
+
"named recipe (orient / release_check / fix_violation / "
|
|
658
|
+
"before_edit / verify_patch) and return the ordered tool "
|
|
659
|
+
"sequence the agent should run."
|
|
660
|
+
),
|
|
661
|
+
"inputSchema": {
|
|
662
|
+
"type": "object",
|
|
663
|
+
"properties": {"goal": {"type": "string"}},
|
|
664
|
+
"additionalProperties": False,
|
|
665
|
+
},
|
|
666
|
+
"handler": _tool_compose_tools,
|
|
667
|
+
},
|
|
668
|
+
"load_policy": {
|
|
669
|
+
"name": "load_policy",
|
|
670
|
+
"description": (
|
|
671
|
+
"Codex #10 — read [tool.forge.agent] from the project's "
|
|
672
|
+
"pyproject.toml. Returns the v1 policy dict with "
|
|
673
|
+
"protected_files / release_gate / max_files_per_patch / "
|
|
674
|
+
"require_human_review_for fields populated (defaults "
|
|
675
|
+
"applied where the user didn't override)."
|
|
676
|
+
),
|
|
677
|
+
"inputSchema": {
|
|
678
|
+
"type": "object",
|
|
679
|
+
"properties": {"project_root": {"type": "string"}},
|
|
680
|
+
"additionalProperties": False,
|
|
681
|
+
},
|
|
682
|
+
"handler": _tool_load_policy,
|
|
683
|
+
},
|
|
684
|
+
"why_did_this_change": {
|
|
685
|
+
"name": "why_did_this_change",
|
|
686
|
+
"description": (
|
|
687
|
+
"Codex #5 — agent memory: every lineage entry + plan "
|
|
688
|
+
"event that references the named file. Helps the next "
|
|
689
|
+
"agent see what was tried, by whom, and when."
|
|
690
|
+
),
|
|
691
|
+
"inputSchema": {
|
|
692
|
+
"type": "object",
|
|
693
|
+
"properties": {
|
|
694
|
+
"project_root": {"type": "string"},
|
|
695
|
+
"file": {"type": "string"},
|
|
696
|
+
},
|
|
697
|
+
"required": ["file"],
|
|
698
|
+
"additionalProperties": False,
|
|
699
|
+
},
|
|
700
|
+
"handler": _tool_why_did_this_change,
|
|
701
|
+
},
|
|
702
|
+
"what_failed_last_time": {
|
|
703
|
+
"name": "what_failed_last_time",
|
|
704
|
+
"description": (
|
|
705
|
+
"Codex #5 — failed / rolled_back plan events matching an "
|
|
706
|
+
"area substring. Surfaces the failures the agent should "
|
|
707
|
+
"expect to confront."
|
|
708
|
+
),
|
|
709
|
+
"inputSchema": {
|
|
710
|
+
"type": "object",
|
|
711
|
+
"properties": {
|
|
712
|
+
"project_root": {"type": "string"},
|
|
713
|
+
"area": {"type": "string"},
|
|
714
|
+
},
|
|
715
|
+
"required": ["area"],
|
|
716
|
+
"additionalProperties": False,
|
|
717
|
+
},
|
|
718
|
+
"handler": _tool_what_failed_last_time,
|
|
719
|
+
},
|
|
720
|
+
"list_recipes": {
|
|
721
|
+
"name": "list_recipes",
|
|
722
|
+
"description": (
|
|
723
|
+
"Codex #12 — list named golden-path recipes "
|
|
724
|
+
"(release_hardening, add_cli_command, fix_wire_violation, "
|
|
725
|
+
"add_feature, publish_mcp). Pair with get_recipe."
|
|
726
|
+
),
|
|
727
|
+
"inputSchema": {
|
|
728
|
+
"type": "object",
|
|
729
|
+
"properties": {},
|
|
730
|
+
"additionalProperties": False,
|
|
731
|
+
},
|
|
732
|
+
"handler": _tool_list_recipes,
|
|
733
|
+
},
|
|
734
|
+
"get_recipe": {
|
|
735
|
+
"name": "get_recipe",
|
|
736
|
+
"description": (
|
|
737
|
+
"Codex #12 — fetch one named recipe (checklist + "
|
|
738
|
+
"file_scope_hints + validation_gate)."
|
|
739
|
+
),
|
|
740
|
+
"inputSchema": {
|
|
741
|
+
"type": "object",
|
|
742
|
+
"properties": {"name": {"type": "string"}},
|
|
743
|
+
"required": ["name"],
|
|
744
|
+
"additionalProperties": False,
|
|
745
|
+
},
|
|
746
|
+
"handler": _tool_get_recipe,
|
|
747
|
+
},
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
# --- Resource registry ---------------------------------------------------
|
|
752
|
+
|
|
753
|
+
ResourceLoader = Callable[[Path], str]
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _resource_doc_receipt(project_root: Path) -> str:
|
|
757
|
+
return _read_repo_doc(project_root, "docs/RECEIPT.md")
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _resource_doc_formalization(project_root: Path) -> str:
|
|
761
|
+
return _read_repo_doc(project_root, "docs/FORMALIZATION.md")
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _resource_lineage(project_root: Path) -> str:
|
|
765
|
+
entries = read_lineage(project_root)
|
|
766
|
+
return json.dumps({
|
|
767
|
+
"schema_version": "atomadic-forge.audit.log/v1",
|
|
768
|
+
"project": str(project_root),
|
|
769
|
+
"entry_count": len(entries),
|
|
770
|
+
"entries": entries,
|
|
771
|
+
}, indent=2)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _resource_schema(project_root: Path) -> str:
|
|
775
|
+
return json.dumps({
|
|
776
|
+
"schema_version": SCHEMA_VERSION_V1,
|
|
777
|
+
"doc": "see forge://docs/receipt for the full v1 schema",
|
|
778
|
+
"valid_verdicts": ["PASS", "FAIL", "REFINE", "QUARANTINE"],
|
|
779
|
+
}, indent=2)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _resource_summary_blockers(project_root: Path) -> str:
|
|
783
|
+
"""Single-call 'what's blocking release?' — runs wire + certify
|
|
784
|
+
and returns the compact summary. Codex feedback: this is the
|
|
785
|
+
resource agents should hit FIRST on every connect."""
|
|
786
|
+
try:
|
|
787
|
+
wire = scan_violations(project_root)
|
|
788
|
+
except (OSError, ValueError):
|
|
789
|
+
wire = None
|
|
790
|
+
try:
|
|
791
|
+
cert = certify(project_root, project=project_root.name)
|
|
792
|
+
except (OSError, ValueError, RuntimeError):
|
|
793
|
+
cert = None
|
|
794
|
+
s = summarize_blockers(
|
|
795
|
+
wire_report=wire, certify_report=cert,
|
|
796
|
+
package_root=project_root.name,
|
|
797
|
+
)
|
|
798
|
+
return json.dumps(s, indent=2, default=str)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _read_repo_doc(project_root: Path, rel: str) -> str:
|
|
802
|
+
"""Read a doc that lives in this Forge install's repo, not the
|
|
803
|
+
consuming project. Falls back to '(not available)' when missing."""
|
|
804
|
+
candidate = Path(__file__).resolve().parents[3] / rel
|
|
805
|
+
if not candidate.exists():
|
|
806
|
+
candidate = Path(project_root) / rel
|
|
807
|
+
try:
|
|
808
|
+
return candidate.read_text(encoding="utf-8")
|
|
809
|
+
except OSError:
|
|
810
|
+
return f"(resource {rel!r} not available in this install)"
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
RESOURCES: dict[str, dict[str, Any]] = {
|
|
814
|
+
"forge://docs/receipt": {
|
|
815
|
+
"uri": "forge://docs/receipt",
|
|
816
|
+
"name": "Forge Receipt v1 wire-format docs",
|
|
817
|
+
"mimeType": "text/markdown",
|
|
818
|
+
"loader": _resource_doc_receipt,
|
|
819
|
+
},
|
|
820
|
+
"forge://docs/formalization": {
|
|
821
|
+
"uri": "forge://docs/formalization",
|
|
822
|
+
"name": "AAM v1.0 + BEP v1.0 theorem citations for Forge gates",
|
|
823
|
+
"mimeType": "text/markdown",
|
|
824
|
+
"loader": _resource_doc_formalization,
|
|
825
|
+
},
|
|
826
|
+
"forge://lineage/chain": {
|
|
827
|
+
"uri": "forge://lineage/chain",
|
|
828
|
+
"name": "Local Vanguard lineage chain (chronological JSONL)",
|
|
829
|
+
"mimeType": "application/json",
|
|
830
|
+
"loader": _resource_lineage,
|
|
831
|
+
},
|
|
832
|
+
"forge://schema/receipt": {
|
|
833
|
+
"uri": "forge://schema/receipt",
|
|
834
|
+
"name": "Receipt v1 schema sketch (verdicts + version constants)",
|
|
835
|
+
"mimeType": "application/json",
|
|
836
|
+
"loader": _resource_schema,
|
|
837
|
+
},
|
|
838
|
+
"forge://summary/blockers": {
|
|
839
|
+
"uri": "forge://summary/blockers",
|
|
840
|
+
"name": "Top-5 blockers (Codex feedback): wire + certify in one call",
|
|
841
|
+
"mimeType": "application/json",
|
|
842
|
+
"loader": _resource_summary_blockers,
|
|
843
|
+
},
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
# --- Dispatch ------------------------------------------------------------
|
|
848
|
+
|
|
849
|
+
def _ok(msg_id: Any, result: Any) -> dict[str, Any]:
|
|
850
|
+
return {"jsonrpc": _JSON_RPC_VERSION, "id": msg_id, "result": result}
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _err(msg_id: Any, code: int, message: str,
|
|
854
|
+
data: Any | None = None) -> dict[str, Any]:
|
|
855
|
+
error: dict[str, Any] = {"code": code, "message": message}
|
|
856
|
+
if data is not None:
|
|
857
|
+
error["data"] = data
|
|
858
|
+
return {"jsonrpc": _JSON_RPC_VERSION, "id": msg_id, "error": error}
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _summary_for_tool(name: str, result: Any) -> dict[str, Any] | None:
|
|
862
|
+
"""Compute the agent-native summary for a tool result, when applicable.
|
|
863
|
+
|
|
864
|
+
Codex feedback: agents thrive on 'here are the 2 things blocking
|
|
865
|
+
release' more than huge manifests. We compute a top-5 summary
|
|
866
|
+
inline so MCP clients can branch on a 4-line response instead of
|
|
867
|
+
parsing kilobytes of JSON.
|
|
868
|
+
"""
|
|
869
|
+
if not isinstance(result, dict):
|
|
870
|
+
return None
|
|
871
|
+
schema = result.get("schema_version", "")
|
|
872
|
+
if schema == "atomadic-forge.wire/v1":
|
|
873
|
+
return summarize_blockers(wire_report=result)
|
|
874
|
+
if schema == "atomadic-forge.certify/v1":
|
|
875
|
+
return summarize_blockers(certify_report=result)
|
|
876
|
+
if name == "certify" and isinstance(result.get("receipt"), dict):
|
|
877
|
+
# The certify-with-emit_receipt path returns a wrapped dict.
|
|
878
|
+
return summarize_blockers(certify_report=result.get("certify"))
|
|
879
|
+
if schema == "atomadic-forge.context_pack/v1":
|
|
880
|
+
# Re-surface the embedded blockers_summary.
|
|
881
|
+
return result.get("blockers_summary")
|
|
882
|
+
if schema == "atomadic-forge.preflight/v1":
|
|
883
|
+
too_broad = result.get("write_scope_too_broad", False)
|
|
884
|
+
n = result.get("write_scope_size", 0)
|
|
885
|
+
return {
|
|
886
|
+
"schema_version": "atomadic-forge.summary/v1",
|
|
887
|
+
"verdict": "REFINE" if too_broad else "PASS",
|
|
888
|
+
"score": None,
|
|
889
|
+
"blocker_count": len(result.get("overall_notes") or []),
|
|
890
|
+
"auto_fixable_count": 0,
|
|
891
|
+
"blockers": [],
|
|
892
|
+
"next_command": (
|
|
893
|
+
f"# write_scope size {n} > threshold; split the change"
|
|
894
|
+
if too_broad
|
|
895
|
+
else "# preflight clean; proceed with bounded edit"
|
|
896
|
+
),
|
|
897
|
+
}
|
|
898
|
+
if schema == "atomadic-forge.patch_score/v1":
|
|
899
|
+
return {
|
|
900
|
+
"schema_version": "atomadic-forge.summary/v1",
|
|
901
|
+
"verdict": "REFINE" if result.get("needs_human_review") else "PASS",
|
|
902
|
+
"score": None,
|
|
903
|
+
"blocker_count": (
|
|
904
|
+
int(result.get("architectural_risk", False))
|
|
905
|
+
+ int(result.get("test_risk", False))
|
|
906
|
+
+ int(result.get("public_api_risk", False))
|
|
907
|
+
+ int(result.get("release_risk", False))
|
|
908
|
+
),
|
|
909
|
+
"auto_fixable_count": 0,
|
|
910
|
+
"blockers": [],
|
|
911
|
+
"next_command": (
|
|
912
|
+
"# needs_human_review=True — block auto-merge"
|
|
913
|
+
if result.get("needs_human_review")
|
|
914
|
+
else "# score_patch clean; proceed with merge"
|
|
915
|
+
),
|
|
916
|
+
}
|
|
917
|
+
if schema == "atomadic-forge.agent_plan/v1":
|
|
918
|
+
# Plans already ARE summary-shaped — surface a tiny digest
|
|
919
|
+
# so MCP clients can branch without re-parsing the full plan.
|
|
920
|
+
# Codex feedback (round 3): the plan now carries a 'score'
|
|
921
|
+
# field; inherit it so MCP _summary matches forge://summary/blockers.
|
|
922
|
+
return {
|
|
923
|
+
"schema_version": "atomadic-forge.summary/v1",
|
|
924
|
+
"verdict": result.get("verdict", "?"),
|
|
925
|
+
"score": result.get("score"),
|
|
926
|
+
"blocker_count": result.get("action_count", 0),
|
|
927
|
+
"auto_fixable_count": result.get("applyable_count", 0),
|
|
928
|
+
"blockers": [],
|
|
929
|
+
"next_command": result.get("next_command", ""),
|
|
930
|
+
}
|
|
931
|
+
return None
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _serialize_result(value: Any, *, name: str = "") -> dict[str, Any]:
|
|
935
|
+
"""Wrap a tool result in MCP's ``content`` envelope so coding-agent
|
|
936
|
+
clients see a uniform shape (text + parsed JSON).
|
|
937
|
+
|
|
938
|
+
When we can derive an agent-native summary, prepend it as a SECOND
|
|
939
|
+
text block so a sloppy client reading only ``content[0]`` still
|
|
940
|
+
gets the full payload (back-compat) while a smart client can read
|
|
941
|
+
``content[1]`` for the compact form. Both are valid MCP shapes.
|
|
942
|
+
"""
|
|
943
|
+
full = json.dumps(value, indent=2, default=str)
|
|
944
|
+
blocks: list[dict[str, Any]] = [{"type": "text", "text": full}]
|
|
945
|
+
summary = _summary_for_tool(name, value) if name else None
|
|
946
|
+
if summary is not None:
|
|
947
|
+
blocks.append({
|
|
948
|
+
"type": "text",
|
|
949
|
+
"text": "_summary:\n" + json.dumps(summary, indent=2, default=str),
|
|
950
|
+
})
|
|
951
|
+
return {"content": blocks, "_summary": summary}
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _list_tools() -> dict[str, Any]:
|
|
955
|
+
return {
|
|
956
|
+
"tools": [
|
|
957
|
+
{"name": t["name"],
|
|
958
|
+
"description": t["description"],
|
|
959
|
+
"inputSchema": t["inputSchema"]}
|
|
960
|
+
for t in TOOLS.values()
|
|
961
|
+
],
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def _list_resources() -> dict[str, Any]:
|
|
966
|
+
return {
|
|
967
|
+
"resources": [
|
|
968
|
+
{"uri": r["uri"], "name": r["name"], "mimeType": r["mimeType"]}
|
|
969
|
+
for r in RESOURCES.values()
|
|
970
|
+
],
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def _initialize_response() -> dict[str, Any]:
|
|
975
|
+
return {
|
|
976
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
977
|
+
"serverInfo": {
|
|
978
|
+
"name": SERVER_NAME,
|
|
979
|
+
"version": __version__,
|
|
980
|
+
},
|
|
981
|
+
"capabilities": {
|
|
982
|
+
"tools": {"listChanged": False},
|
|
983
|
+
"resources": {"subscribe": False, "listChanged": False},
|
|
984
|
+
},
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def dispatch_request(
|
|
989
|
+
request: dict[str, Any],
|
|
990
|
+
*,
|
|
991
|
+
project_root: Path,
|
|
992
|
+
) -> dict[str, Any] | None:
|
|
993
|
+
"""Route one JSON-RPC request to its handler and return the response.
|
|
994
|
+
|
|
995
|
+
Returns ``None`` for valid notifications (no ``id`` field) — the
|
|
996
|
+
transport must NOT write a response in that case.
|
|
997
|
+
"""
|
|
998
|
+
if not isinstance(request, dict):
|
|
999
|
+
return _err(None, _ERR_INVALID_REQUEST, "request must be a JSON object")
|
|
1000
|
+
method = request.get("method")
|
|
1001
|
+
if not isinstance(method, str):
|
|
1002
|
+
return _err(request.get("id"), _ERR_INVALID_REQUEST,
|
|
1003
|
+
"request missing string `method`")
|
|
1004
|
+
msg_id = request.get("id")
|
|
1005
|
+
is_notification = "id" not in request
|
|
1006
|
+
params = request.get("params") or {}
|
|
1007
|
+
|
|
1008
|
+
if method == "initialize":
|
|
1009
|
+
return _ok(msg_id, _initialize_response())
|
|
1010
|
+
if method == "ping":
|
|
1011
|
+
return _ok(msg_id, {})
|
|
1012
|
+
if method == "notifications/initialized":
|
|
1013
|
+
return None # client → server; server replies nothing
|
|
1014
|
+
if method == "tools/list":
|
|
1015
|
+
return _ok(msg_id, _list_tools())
|
|
1016
|
+
if method == "resources/list":
|
|
1017
|
+
return _ok(msg_id, _list_resources())
|
|
1018
|
+
if method == "tools/call":
|
|
1019
|
+
if is_notification:
|
|
1020
|
+
return None
|
|
1021
|
+
name = params.get("name")
|
|
1022
|
+
args = params.get("arguments") or {}
|
|
1023
|
+
if not isinstance(name, str) or name not in TOOLS:
|
|
1024
|
+
return _err(msg_id, _ERR_METHOD_NOT_FOUND,
|
|
1025
|
+
f"unknown tool: {name!r}")
|
|
1026
|
+
try:
|
|
1027
|
+
result = TOOLS[name]["handler"](project_root, args)
|
|
1028
|
+
except (ValueError, OSError, RuntimeError) as exc:
|
|
1029
|
+
return _err(msg_id, _ERR_SERVER,
|
|
1030
|
+
f"{type(exc).__name__}: {exc}")
|
|
1031
|
+
return _ok(msg_id, _serialize_result(result, name=name))
|
|
1032
|
+
if method == "resources/read":
|
|
1033
|
+
if is_notification:
|
|
1034
|
+
return None
|
|
1035
|
+
uri = params.get("uri")
|
|
1036
|
+
if uri not in RESOURCES:
|
|
1037
|
+
return _err(msg_id, _ERR_INVALID_PARAMS,
|
|
1038
|
+
f"unknown resource: {uri!r}")
|
|
1039
|
+
loader = RESOURCES[uri]["loader"]
|
|
1040
|
+
try:
|
|
1041
|
+
text = loader(project_root)
|
|
1042
|
+
except OSError as exc:
|
|
1043
|
+
return _err(msg_id, _ERR_SERVER,
|
|
1044
|
+
f"could not read resource: {exc}")
|
|
1045
|
+
return _ok(msg_id, {
|
|
1046
|
+
"contents": [{
|
|
1047
|
+
"uri": uri,
|
|
1048
|
+
"mimeType": RESOURCES[uri]["mimeType"],
|
|
1049
|
+
"text": text,
|
|
1050
|
+
}],
|
|
1051
|
+
})
|
|
1052
|
+
|
|
1053
|
+
return _err(msg_id, _ERR_METHOD_NOT_FOUND,
|
|
1054
|
+
f"unknown method: {method!r}")
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
__all__ = [
|
|
1058
|
+
"PROTOCOL_VERSION",
|
|
1059
|
+
"RESOURCES",
|
|
1060
|
+
"SERVER_NAME",
|
|
1061
|
+
"TOOLS",
|
|
1062
|
+
"dispatch_request",
|
|
1063
|
+
# Lane B Studio's Topology Map renders against these helpers.
|
|
1064
|
+
"canonical_receipt_hash",
|
|
1065
|
+
"verify_chain_link",
|
|
1066
|
+
]
|