atomadic-forge 0.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. atomadic_forge/__init__.py +12 -0
  2. atomadic_forge/__main__.py +5 -0
  3. atomadic_forge/a0_qk_constants/__init__.py +1 -0
  4. atomadic_forge/a0_qk_constants/agent_plan_schema.py +120 -0
  5. atomadic_forge/a0_qk_constants/commandsmith_types.py +49 -0
  6. atomadic_forge/a0_qk_constants/config_defaults.py +38 -0
  7. atomadic_forge/a0_qk_constants/emergent_types.py +77 -0
  8. atomadic_forge/a0_qk_constants/error_codes.py +296 -0
  9. atomadic_forge/a0_qk_constants/forge_types.py +89 -0
  10. atomadic_forge/a0_qk_constants/gen_language.py +116 -0
  11. atomadic_forge/a0_qk_constants/lang_extensions.py +150 -0
  12. atomadic_forge/a0_qk_constants/policy_schema.py +48 -0
  13. atomadic_forge/a0_qk_constants/receipt_schema.py +311 -0
  14. atomadic_forge/a0_qk_constants/roi_constants.py +96 -0
  15. atomadic_forge/a0_qk_constants/semantic_types.py +61 -0
  16. atomadic_forge/a0_qk_constants/sidecar_schema.py +81 -0
  17. atomadic_forge/a0_qk_constants/synergy_types.py +62 -0
  18. atomadic_forge/a0_qk_constants/tier_names.py +47 -0
  19. atomadic_forge/a1_at_functions/__init__.py +1 -0
  20. atomadic_forge/a1_at_functions/agent_context_pack.py +193 -0
  21. atomadic_forge/a1_at_functions/agent_memory.py +139 -0
  22. atomadic_forge/a1_at_functions/agent_plan_emitter.py +324 -0
  23. atomadic_forge/a1_at_functions/agent_summary.py +277 -0
  24. atomadic_forge/a1_at_functions/body_extractor.py +306 -0
  25. atomadic_forge/a1_at_functions/card_renderer.py +210 -0
  26. atomadic_forge/a1_at_functions/certify_checks.py +445 -0
  27. atomadic_forge/a1_at_functions/chat_context.py +170 -0
  28. atomadic_forge/a1_at_functions/cherry_pick.py +71 -0
  29. atomadic_forge/a1_at_functions/classify_tier.py +115 -0
  30. atomadic_forge/a1_at_functions/commandsmith_discover.py +167 -0
  31. atomadic_forge/a1_at_functions/commandsmith_render.py +267 -0
  32. atomadic_forge/a1_at_functions/compiler_feedback.py +94 -0
  33. atomadic_forge/a1_at_functions/compliance_checker.py +228 -0
  34. atomadic_forge/a1_at_functions/config_io.py +68 -0
  35. atomadic_forge/a1_at_functions/cs1_renderer.py +588 -0
  36. atomadic_forge/a1_at_functions/doc_synthesizer.py +205 -0
  37. atomadic_forge/a1_at_functions/emergent_compose.py +192 -0
  38. atomadic_forge/a1_at_functions/emergent_rank.py +116 -0
  39. atomadic_forge/a1_at_functions/emergent_signature_extract.py +242 -0
  40. atomadic_forge/a1_at_functions/emergent_synthesize.py +88 -0
  41. atomadic_forge/a1_at_functions/enforce_planner.py +208 -0
  42. atomadic_forge/a1_at_functions/error_hints.py +105 -0
  43. atomadic_forge/a1_at_functions/evolution_log.py +94 -0
  44. atomadic_forge/a1_at_functions/forge_feedback.py +433 -0
  45. atomadic_forge/a1_at_functions/generation_quality.py +322 -0
  46. atomadic_forge/a1_at_functions/import_repair.py +211 -0
  47. atomadic_forge/a1_at_functions/import_smoke.py +102 -0
  48. atomadic_forge/a1_at_functions/js_parser.py +539 -0
  49. atomadic_forge/a1_at_functions/lineage_chain.py +144 -0
  50. atomadic_forge/a1_at_functions/lineage_reader.py +107 -0
  51. atomadic_forge/a1_at_functions/llm_client.py +554 -0
  52. atomadic_forge/a1_at_functions/local_signer.py +134 -0
  53. atomadic_forge/a1_at_functions/lsp_protocol.py +379 -0
  54. atomadic_forge/a1_at_functions/manifest_diff.py +314 -0
  55. atomadic_forge/a1_at_functions/mcp_protocol.py +1066 -0
  56. atomadic_forge/a1_at_functions/patch_scorer.py +267 -0
  57. atomadic_forge/a1_at_functions/plan_adapter.py +75 -0
  58. atomadic_forge/a1_at_functions/policy_loader.py +107 -0
  59. atomadic_forge/a1_at_functions/preflight_change.py +227 -0
  60. atomadic_forge/a1_at_functions/progress_reporter.py +81 -0
  61. atomadic_forge/a1_at_functions/provider_detect.py +157 -0
  62. atomadic_forge/a1_at_functions/provider_resolver.py +48 -0
  63. atomadic_forge/a1_at_functions/receipt_emitter.py +291 -0
  64. atomadic_forge/a1_at_functions/recipes.py +186 -0
  65. atomadic_forge/a1_at_functions/repo_explainer.py +124 -0
  66. atomadic_forge/a1_at_functions/roi_calculator.py +265 -0
  67. atomadic_forge/a1_at_functions/rollback_planner.py +147 -0
  68. atomadic_forge/a1_at_functions/sbom_emitter.py +155 -0
  69. atomadic_forge/a1_at_functions/scaffold_js.py +55 -0
  70. atomadic_forge/a1_at_functions/scaffold_pyproject.py +62 -0
  71. atomadic_forge/a1_at_functions/scaffold_starter.py +94 -0
  72. atomadic_forge/a1_at_functions/scout_walk.py +309 -0
  73. atomadic_forge/a1_at_functions/sidecar_parser.py +161 -0
  74. atomadic_forge/a1_at_functions/sidecar_validator.py +202 -0
  75. atomadic_forge/a1_at_functions/stub_detector.py +158 -0
  76. atomadic_forge/a1_at_functions/synergy_detect.py +166 -0
  77. atomadic_forge/a1_at_functions/synergy_render.py +252 -0
  78. atomadic_forge/a1_at_functions/synergy_surface_extract.py +163 -0
  79. atomadic_forge/a1_at_functions/test_runner.py +196 -0
  80. atomadic_forge/a1_at_functions/test_selector.py +122 -0
  81. atomadic_forge/a1_at_functions/tier_init_rebuild.py +122 -0
  82. atomadic_forge/a1_at_functions/tool_composer.py +130 -0
  83. atomadic_forge/a1_at_functions/transcript_log.py +70 -0
  84. atomadic_forge/a1_at_functions/wire_check.py +260 -0
  85. atomadic_forge/a2_mo_composites/__init__.py +1 -0
  86. atomadic_forge/a2_mo_composites/lineage_chain_store.py +122 -0
  87. atomadic_forge/a2_mo_composites/manifest_store.py +46 -0
  88. atomadic_forge/a2_mo_composites/plan_store.py +164 -0
  89. atomadic_forge/a2_mo_composites/receipt_signer.py +231 -0
  90. atomadic_forge/a3_og_features/__init__.py +1 -0
  91. atomadic_forge/a3_og_features/commandsmith_feature.py +267 -0
  92. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/__init__.py +3 -0
  93. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a0_qk_constants/__init__.py +4 -0
  94. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/src/mixed_pkg/a1_at_functions/__init__.py +14 -0
  95. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/conftest.py +10 -0
  96. atomadic_forge/a3_og_features/demo_packages/mixed_py_js/tests/test_mixed.py +18 -0
  97. atomadic_forge/a3_og_features/demo_runner.py +502 -0
  98. atomadic_forge/a3_og_features/emergent_feature.py +95 -0
  99. atomadic_forge/a3_og_features/emergent_pipeline_integration.py +154 -0
  100. atomadic_forge/a3_og_features/forge_enforce.py +107 -0
  101. atomadic_forge/a3_og_features/forge_evolve.py +176 -0
  102. atomadic_forge/a3_og_features/forge_loop.py +528 -0
  103. atomadic_forge/a3_og_features/forge_pipeline.py +295 -0
  104. atomadic_forge/a3_og_features/forge_plan_apply.py +222 -0
  105. atomadic_forge/a3_og_features/lsp_server.py +98 -0
  106. atomadic_forge/a3_og_features/mcp_server.py +160 -0
  107. atomadic_forge/a3_og_features/setup_wizard.py +337 -0
  108. atomadic_forge/a3_og_features/synergy_feature.py +65 -0
  109. atomadic_forge/a4_sy_orchestration/__init__.py +1 -0
  110. atomadic_forge/a4_sy_orchestration/cli.py +1284 -0
  111. atomadic_forge/commands/__init__.py +1 -0
  112. atomadic_forge/commands/_registry.py +36 -0
  113. atomadic_forge/commands/audit.py +142 -0
  114. atomadic_forge/commands/chat.py +133 -0
  115. atomadic_forge/commands/commandsmith.py +178 -0
  116. atomadic_forge/commands/config_cmd.py +145 -0
  117. atomadic_forge/commands/demo.py +142 -0
  118. atomadic_forge/commands/emergent.py +124 -0
  119. atomadic_forge/commands/emergent_then_synergy.py +70 -0
  120. atomadic_forge/commands/evolve.py +122 -0
  121. atomadic_forge/commands/evolve_then_iterate.py +70 -0
  122. atomadic_forge/commands/feature_then_emergent.py +111 -0
  123. atomadic_forge/commands/iterate.py +140 -0
  124. atomadic_forge/commands/synergy.py +96 -0
  125. atomadic_forge/commands/synergy_then_emergent.py +70 -0
  126. atomadic_forge-0.3.2.dist-info/METADATA +471 -0
  127. atomadic_forge-0.3.2.dist-info/RECORD +131 -0
  128. atomadic_forge-0.3.2.dist-info/WHEEL +5 -0
  129. atomadic_forge-0.3.2.dist-info/entry_points.txt +3 -0
  130. atomadic_forge-0.3.2.dist-info/licenses/LICENSE +15 -0
  131. atomadic_forge-0.3.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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
+ ]