@heretek-ai/epistemic-swarm 0.7.24 → 1.0.2

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 (350) hide show
  1. package/index.ts +1 -0
  2. package/package.json +63 -139
  3. package/rpc.ts +1 -0
  4. package/server.ts +1 -0
  5. package/src/agents.ts +69 -0
  6. package/src/continue.ts +40 -0
  7. package/src/hooks.ts +283 -0
  8. package/src/panels.tsx +220 -0
  9. package/src/policy.ts +221 -0
  10. package/src/pr.ts +37 -0
  11. package/src/research.ts +63 -0
  12. package/src/rpc-def.ts +136 -0
  13. package/src/rpc.ts +331 -0
  14. package/src/runtime.ts +112 -0
  15. package/src/server.ts +311 -0
  16. package/src/session.ts +61 -0
  17. package/src/tools.ts +61 -0
  18. package/src/tui.tsx +206 -0
  19. package/tui.tsx +1 -0
  20. package/.agents/skills/brainstorming/SKILL.md +0 -19
  21. package/.agents/skills/code_audit/SKILL.md +0 -19
  22. package/.agents/skills/darkharvest/SKILL.md +0 -19
  23. package/.agents/skills/epistemic_search/SKILL.md +0 -19
  24. package/.agents/skills/factory/SKILL.md +0 -19
  25. package/.agents/skills/grilling/SKILL.md +0 -19
  26. package/.agents/skills/oss_scout/SKILL.md +0 -19
  27. package/.agents/skills/research_cache/SKILL.md +0 -19
  28. package/.agents/skills/swarm_config/SKILL.md +0 -19
  29. package/.claude-plugin/marketplace.json +0 -61
  30. package/.claude-plugin/plugin.json +0 -76
  31. package/.omp/README.md +0 -39
  32. package/.omp/SYSTEM.md +0 -17
  33. package/.omp/commands/audit.md +0 -10
  34. package/.omp/commands/brainstorming.md +0 -12
  35. package/.omp/commands/darkharvest.md +0 -11
  36. package/.omp/commands/domainexpansion.md +0 -9
  37. package/.omp/commands/factory.md +0 -9
  38. package/.omp/commands/grill.md +0 -9
  39. package/.omp/commands/scout.md +0 -10
  40. package/.omp/commands/swarm-config.md +0 -10
  41. package/.omp/commands/swarm.md +0 -9
  42. package/.omp/hooks/post/epistemic-audit.ts +0 -16
  43. package/.omp/hooks/pre/epistemic-redirect.ts +0 -21
  44. package/.omp/prompts/brainstorming.md +0 -8
  45. package/.omp/prompts/swarm.md +0 -8
  46. package/LICENSE +0 -126
  47. package/MARKETPLACE.md +0 -130
  48. package/README.md +0 -403
  49. package/bin/cli.js +0 -297
  50. package/config/claude-settings-patch.json +0 -10
  51. package/config/docker-compose.infra.yml +0 -23
  52. package/config/domain_packs/biopharma.json +0 -23
  53. package/config/domain_packs/legal.json +0 -19
  54. package/config/domain_packs/quant.json +0 -19
  55. package/config/mcp-research-servers.json +0 -33
  56. package/config/mcp_launcher.py +0 -153
  57. package/config/opencode-snippet.json +0 -156
  58. package/config/searxng_mcp.py +0 -92
  59. package/extensions/pi/index.js +0 -357
  60. package/hooks/hooks.json +0 -45
  61. package/install.sh +0 -473
  62. package/plugins/antigravity/README.md +0 -72
  63. package/plugins/antigravity/agents/alpha-thesis.md +0 -36
  64. package/plugins/antigravity/agents/beta-antithesis.md +0 -35
  65. package/plugins/antigravity/agents/brainstormer.md +0 -27
  66. package/plugins/antigravity/agents/code-auditor.md +0 -28
  67. package/plugins/antigravity/agents/darkharvester.md +0 -30
  68. package/plugins/antigravity/agents/epistemic-auditor.md +0 -28
  69. package/plugins/antigravity/agents/manager.md +0 -26
  70. package/plugins/antigravity/agents/oss-scout.md +0 -27
  71. package/plugins/antigravity/agents/programmer.md +0 -25
  72. package/plugins/antigravity/agents/qa-a.md +0 -22
  73. package/plugins/antigravity/agents/qa-b.md +0 -22
  74. package/plugins/antigravity/antigravity_hooks.py +0 -265
  75. package/plugins/antigravity/commands/audit.md +0 -11
  76. package/plugins/antigravity/commands/brainstorm.md +0 -9
  77. package/plugins/antigravity/commands/brainstorming.md +0 -10
  78. package/plugins/antigravity/commands/darkharvest.md +0 -12
  79. package/plugins/antigravity/commands/domainexpansion.md +0 -12
  80. package/plugins/antigravity/commands/factory.md +0 -14
  81. package/plugins/antigravity/commands/grill.md +0 -10
  82. package/plugins/antigravity/commands/scout.md +0 -11
  83. package/plugins/antigravity/commands/swarm-config.md +0 -9
  84. package/plugins/antigravity/commands/swarm.md +0 -12
  85. package/plugins/antigravity/hook_bus_bridge.js +0 -352
  86. package/plugins/antigravity/hooks.json +0 -44
  87. package/plugins/antigravity/mcp_config.json +0 -33
  88. package/plugins/antigravity/mcp_launcher.py +0 -58
  89. package/plugins/antigravity/plugin.json +0 -13
  90. package/plugins/antigravity/rules/AGENTS.md +0 -127
  91. package/plugins/antigravity/rules/epistemic-integrity.md +0 -16
  92. package/plugins/antigravity/skills/brainstorming/SKILL.md +0 -19
  93. package/plugins/antigravity/skills/code_audit/SKILL.md +0 -19
  94. package/plugins/antigravity/skills/darkharvest/SKILL.md +0 -19
  95. package/plugins/antigravity/skills/epistemic_search/SKILL.md +0 -19
  96. package/plugins/antigravity/skills/factory/SKILL.md +0 -19
  97. package/plugins/antigravity/skills/grilling/SKILL.md +0 -19
  98. package/plugins/antigravity/skills/oss_scout/SKILL.md +0 -19
  99. package/plugins/antigravity/skills/research_cache/SKILL.md +0 -19
  100. package/plugins/antigravity/skills/swarm_config/SKILL.md +0 -19
  101. package/plugins/codex/AGENTS.md.snippet +0 -10
  102. package/plugins/codex/README.md +0 -37
  103. package/plugins/codex/config.toml.snippet +0 -28
  104. package/plugins/codex/openai.yaml +0 -24
  105. package/plugins/codex/skills/brainstorming/SKILL.md +0 -19
  106. package/plugins/codex/skills/code_audit/SKILL.md +0 -19
  107. package/plugins/codex/skills/darkharvest/SKILL.md +0 -19
  108. package/plugins/codex/skills/epistemic_search/SKILL.md +0 -19
  109. package/plugins/codex/skills/factory/SKILL.md +0 -19
  110. package/plugins/codex/skills/grilling/SKILL.md +0 -19
  111. package/plugins/codex/skills/oss_scout/SKILL.md +0 -19
  112. package/plugins/codex/skills/research_cache/SKILL.md +0 -19
  113. package/plugins/codex/skills/swarm_config/SKILL.md +0 -19
  114. package/plugins/darkharvest/.claude-plugin/plugin.json +0 -15
  115. package/plugins/darkharvest/agents/harvest-proponent.md +0 -22
  116. package/plugins/darkharvest/agents/harvest-redteam.md +0 -20
  117. package/plugins/darkharvest/evals/teardown-verdict/graders/license-line.md +0 -6
  118. package/plugins/darkharvest/evals/teardown-verdict/graders/skill-fired.md +0 -5
  119. package/plugins/darkharvest/evals/teardown-verdict/prompt.md +0 -6
  120. package/plugins/darkharvest/skills/darkharvest/SKILL.md +0 -70
  121. package/plugins/darkharvest/skills/darkharvest/scripts/harvest.py +0 -289
  122. package/plugins/factory/.claude-plugin/plugin.json +0 -15
  123. package/plugins/factory/agents/factory-manager.md +0 -22
  124. package/plugins/factory/agents/programmer.md +0 -16
  125. package/plugins/factory/agents/qa-adversarial.md +0 -17
  126. package/plugins/factory/agents/qa-functional.md +0 -17
  127. package/plugins/factory/evals/gate-halt/graders/gates-first.md +0 -6
  128. package/plugins/factory/evals/gate-halt/graders/skill-fired.md +0 -5
  129. package/plugins/factory/evals/gate-halt/prompt.md +0 -6
  130. package/plugins/factory/skills/factory/SKILL.md +0 -61
  131. package/plugins/factory/skills/factory/scripts/factory.py +0 -300
  132. package/plugins/gemini/GEMINI.md +0 -16
  133. package/plugins/gemini/README.md +0 -20
  134. package/plugins/gemini/commands/audit.toml +0 -6
  135. package/plugins/gemini/commands/brainstorming.toml +0 -10
  136. package/plugins/gemini/commands/darkharvest.toml +0 -8
  137. package/plugins/gemini/commands/domainexpansion.toml +0 -7
  138. package/plugins/gemini/commands/factory.toml +0 -8
  139. package/plugins/gemini/commands/grill.toml +0 -6
  140. package/plugins/gemini/commands/scout.toml +0 -7
  141. package/plugins/gemini/commands/swarm-config.toml +0 -7
  142. package/plugins/gemini/commands/swarm.toml +0 -8
  143. package/plugins/gemini/gemini-extension.json +0 -38
  144. package/plugins/gemini/hooks/hooks.json +0 -11
  145. package/plugins/gemini/skills/brainstorming/SKILL.md +0 -19
  146. package/plugins/gemini/skills/code_audit/SKILL.md +0 -19
  147. package/plugins/gemini/skills/darkharvest/SKILL.md +0 -19
  148. package/plugins/gemini/skills/epistemic_search/SKILL.md +0 -19
  149. package/plugins/gemini/skills/factory/SKILL.md +0 -19
  150. package/plugins/gemini/skills/grilling/SKILL.md +0 -19
  151. package/plugins/gemini/skills/oss_scout/SKILL.md +0 -19
  152. package/plugins/gemini/skills/research_cache/SKILL.md +0 -19
  153. package/plugins/gemini/skills/swarm_config/SKILL.md +0 -19
  154. package/plugins/opencode/analysis-bridge.js +0 -620
  155. package/plugins/opencode/config-io.js +0 -1651
  156. package/plugins/opencode/dep-health.js +0 -1039
  157. package/plugins/opencode/hook-bus.js +0 -496
  158. package/plugins/opencode/index.js +0 -2992
  159. package/plugins/opencode/tui.js +0 -1551
  160. package/plugins/opencode/weight-baseline.json +0 -16
  161. package/plugins/opencode/weight-signals.js +0 -769
  162. package/plugins/research-cache/.claude-plugin/plugin.json +0 -15
  163. package/plugins/research-cache/skills/research_cache/SKILL.md +0 -36
  164. package/plugins/research-cache/skills/research_cache/__init__.py +0 -0
  165. package/plugins/research-cache/skills/research_cache/hasher.py +0 -302
  166. package/plugins/socratic-grilling/.claude-plugin/plugin.json +0 -15
  167. package/plugins/socratic-grilling/skills/grilling/SKILL.md +0 -61
  168. package/plugins/socratic-grilling/skills/grilling/__init__.py +0 -0
  169. package/plugins/socratic-grilling/skills/grilling/socratic_tree.py +0 -363
  170. package/prompts/agent_alpha_thesis.md +0 -70
  171. package/prompts/agent_beta_antithesis.md +0 -78
  172. package/prompts/agent_brainstormer.md +0 -143
  173. package/prompts/agent_code_auditor.md +0 -88
  174. package/prompts/agent_darkharvest.md +0 -140
  175. package/prompts/agent_oss_scout.md +0 -78
  176. package/prompts/antigravity_repo_factcheck.md +0 -143
  177. package/prompts/base_epistemic_system.md +0 -50
  178. package/prompts/epistemic_auditor.md +0 -74
  179. package/prompts/orchestrator.md +0 -70
  180. package/runner/__init__.py +0 -0
  181. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  182. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  183. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  184. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  185. package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
  186. package/runner/__pycache__/darkharvest_claims.cpython-311.pyc +0 -0
  187. package/runner/__pycache__/errors.cpython-311.pyc +0 -0
  188. package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
  189. package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
  190. package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
  191. package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
  192. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  193. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  194. package/runner/__pycache__/preflight.cpython-311.pyc +0 -0
  195. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  196. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  197. package/runner/__pycache__/retrieval_log.cpython-311.pyc +0 -0
  198. package/runner/__pycache__/schema_validate.cpython-311.pyc +0 -0
  199. package/runner/__pycache__/schemas.cpython-311.pyc +0 -0
  200. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  201. package/runner/antigravity_hooks.py +0 -265
  202. package/runner/auctioneer.py +0 -169
  203. package/runner/auditor_engine.py +0 -711
  204. package/runner/claim_store.py +0 -417
  205. package/runner/claim_witness.py +0 -412
  206. package/runner/darkharvest_claims.py +0 -358
  207. package/runner/errors.py +0 -51
  208. package/runner/living_dossiers.py +0 -395
  209. package/runner/mcp_protocol.py +0 -188
  210. package/runner/mcp_server.py +0 -1447
  211. package/runner/path_safety.py +0 -62
  212. package/runner/pcrb.py +0 -290
  213. package/runner/pcrb_verify.py +0 -267
  214. package/runner/preflight.py +0 -1011
  215. package/runner/refinement.py +0 -580
  216. package/runner/research_swarm.py +0 -1848
  217. package/runner/retrieval_log.py +0 -90
  218. package/runner/schema_validate.py +0 -193
  219. package/runner/schemas.py +0 -630
  220. package/runner/state_machine.py +0 -463
  221. package/runner/tests/__pycache__/test_analysis_bridge.cpython-311.pyc +0 -0
  222. package/runner/tests/__pycache__/test_antigravity_plugin.cpython-311.pyc +0 -0
  223. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  224. package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
  225. package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
  226. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  227. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  228. package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
  229. package/runner/tests/__pycache__/test_config_schema.cpython-311.pyc +0 -0
  230. package/runner/tests/__pycache__/test_contract_tooling.cpython-311.pyc +0 -0
  231. package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
  232. package/runner/tests/__pycache__/test_dep_health.cpython-311.pyc +0 -0
  233. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  234. package/runner/tests/__pycache__/test_dossier_contract.cpython-311.pyc +0 -0
  235. package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
  236. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  237. package/runner/tests/__pycache__/test_frontier_concurrency.cpython-311.pyc +0 -0
  238. package/runner/tests/__pycache__/test_frontier_mirror_standalone.cpython-311.pyc +0 -0
  239. package/runner/tests/__pycache__/test_grounding.cpython-311.pyc +0 -0
  240. package/runner/tests/__pycache__/test_hardening_contract.cpython-311.pyc +0 -0
  241. package/runner/tests/__pycache__/test_hook_bus.cpython-311.pyc +0 -0
  242. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  243. package/runner/tests/__pycache__/test_manifest_concurrency.cpython-311.pyc +0 -0
  244. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  245. package/runner/tests/__pycache__/test_nk_hardening.cpython-311.pyc +0 -0
  246. package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
  247. package/runner/tests/__pycache__/test_opencode_v2_registrars.cpython-311.pyc +0 -0
  248. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  249. package/runner/tests/__pycache__/test_phase08_waiver_reopens.cpython-311.pyc +0 -0
  250. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  251. package/runner/tests/__pycache__/test_search_anomaly.cpython-311.pyc +0 -0
  252. package/runner/tests/__pycache__/test_search_provider.cpython-311.pyc +0 -0
  253. package/runner/tests/__pycache__/test_search_provider_plugin.cpython-311.pyc +0 -0
  254. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  255. package/runner/tests/__pycache__/test_swarm_config.cpython-311.pyc +0 -0
  256. package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
  257. package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
  258. package/runner/tests/__pycache__/test_weight_signals.cpython-311.pyc +0 -0
  259. package/runner/tests/fixtures/auction_objective.json +0 -35
  260. package/runner/tests/fixtures/borderline_claims.json +0 -27
  261. package/runner/tests/fixtures/config_validation_cases.json +0 -276
  262. package/runner/tests/fixtures/ddg_anomaly_page.html +0 -176
  263. package/runner/tests/fixtures/ddg_results_page.html +0 -28
  264. package/runner/tests/fixtures/divergence_objectives.json +0 -31
  265. package/runner/tests/test_analysis_bridge.py +0 -450
  266. package/runner/tests/test_antigravity_plugin.py +0 -361
  267. package/runner/tests/test_auction_order.py +0 -173
  268. package/runner/tests/test_backends.py +0 -457
  269. package/runner/tests/test_bet1_spike.py +0 -95
  270. package/runner/tests/test_claim_store.py +0 -162
  271. package/runner/tests/test_claim_witness.py +0 -186
  272. package/runner/tests/test_claude_plugin.py +0 -170
  273. package/runner/tests/test_config_schema.py +0 -879
  274. package/runner/tests/test_contract_tooling.py +0 -312
  275. package/runner/tests/test_darkharvest_audit.py +0 -395
  276. package/runner/tests/test_dep_health.py +0 -595
  277. package/runner/tests/test_domain_packs.py +0 -217
  278. package/runner/tests/test_dossier_contract.py +0 -196
  279. package/runner/tests/test_factory.py +0 -258
  280. package/runner/tests/test_fleet_seam.py +0 -113
  281. package/runner/tests/test_frontier_concurrency.py +0 -288
  282. package/runner/tests/test_frontier_mirror_standalone.py +0 -156
  283. package/runner/tests/test_grounding.py +0 -183
  284. package/runner/tests/test_hardening_contract.py +0 -391
  285. package/runner/tests/test_hook_bus.py +0 -956
  286. package/runner/tests/test_living_dossiers.py +0 -204
  287. package/runner/tests/test_manifest_concurrency.py +0 -187
  288. package/runner/tests/test_mcp_server.py +0 -257
  289. package/runner/tests/test_nk_hardening.py +0 -1046
  290. package/runner/tests/test_opencode_ux.py +0 -2438
  291. package/runner/tests/test_opencode_v2_registrars.py +0 -2157
  292. package/runner/tests/test_pcrb.py +0 -240
  293. package/runner/tests/test_phase08_waiver_reopens.py +0 -539
  294. package/runner/tests/test_refinement.py +0 -255
  295. package/runner/tests/test_search_anomaly.py +0 -342
  296. package/runner/tests/test_search_provider.py +0 -1534
  297. package/runner/tests/test_search_provider_plugin.py +0 -401
  298. package/runner/tests/test_swarm.py +0 -444
  299. package/runner/tests/test_swarm_config.py +0 -520
  300. package/runner/tests/test_sweep_regressions.py +0 -453
  301. package/runner/tests/test_webcache.py +0 -271
  302. package/runner/tests/test_weight_signals.py +0 -401
  303. package/schemas/alpha_dossier.schema.json +0 -204
  304. package/schemas/audit_report.schema.json +0 -79
  305. package/schemas/beta_dossier.schema.json +0 -214
  306. package/schemas/config.schema.json +0 -311
  307. package/schemas/frontier.schema.json +0 -61
  308. package/schemas/manifest.schema.json +0 -49
  309. package/schemas/scope_manifest.schema.json +0 -47
  310. package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
  311. package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
  312. package/scripts/__pycache__/check_docs.cpython-311.pyc +0 -0
  313. package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
  314. package/scripts/__pycache__/gen_schemas.cpython-311.pyc +0 -0
  315. package/scripts/auction_experiment.py +0 -188
  316. package/scripts/bet1_advisory_spike.py +0 -442
  317. package/scripts/build_adapters.py +0 -417
  318. package/scripts/check_docs.py +0 -66
  319. package/scripts/divergence_experiment.py +0 -214
  320. package/scripts/gen_schemas.py +0 -69
  321. package/skills/brainstorming/SKILL.md +0 -106
  322. package/skills/brainstorming/__init__.py +0 -1
  323. package/skills/brainstorming/scripts/brainstorm.py +0 -201
  324. package/skills/code_audit/SKILL.md +0 -45
  325. package/skills/darkharvest/SKILL.md +0 -70
  326. package/skills/darkharvest/scripts/harvest.py +0 -289
  327. package/skills/epistemic_search/SKILL.md +0 -67
  328. package/skills/epistemic_search/__init__.py +0 -1
  329. package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
  330. package/skills/epistemic_search/scripts/fetch.py +0 -156
  331. package/skills/epistemic_search/scripts/search.py +0 -551
  332. package/skills/epistemic_search/scripts/webcache.py +0 -339
  333. package/skills/factory/SKILL.md +0 -61
  334. package/skills/factory/scripts/factory.py +0 -300
  335. package/skills/grilling/SKILL.md +0 -61
  336. package/skills/grilling/__init__.py +0 -0
  337. package/skills/grilling/__pycache__/__init__.cpython-311.pyc +0 -0
  338. package/skills/grilling/__pycache__/socratic_tree.cpython-311.pyc +0 -0
  339. package/skills/grilling/socratic_tree.py +0 -363
  340. package/skills/oss_scout/SKILL.md +0 -49
  341. package/skills/research_cache/SKILL.md +0 -36
  342. package/skills/research_cache/__init__.py +0 -0
  343. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  344. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  345. package/skills/research_cache/hasher.py +0 -302
  346. package/skills/swarm_config/SKILL.md +0 -178
  347. package/skills/swarm_config/__init__.py +0 -35
  348. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  349. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
  350. package/skills/swarm_config/configure.py +0 -1002
@@ -1,1447 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- IUMBTEMS first-party MCP server: the canonical tool surface.
4
-
5
- Wraps the existing Python APIs in-process (no spawnSync, no shell-outs):
6
- - SwarmRunner (runner/research_swarm.py)
7
- - SourceHasher (skills/research_cache/hasher.py)
8
- - load_config / save_config (skills/swarm_config/configure.py)
9
- - DesignTree (skills/grilling/socratic_tree.py)
10
-
11
- Exposes the 7 canonical iumbtems_* tool names already registered by the
12
- OpenCode plugin and Pi extension, so harness adapters can thin out to a
13
- pointer at this server.
14
-
15
- Two entry points:
16
- python3 runner/mcp_server.py # stdio MCP server
17
- python3 runner/mcp_server.py call <tool> '{}' # one-shot dispatch (JSON out)
18
-
19
- Zero external dependencies by design: protocol loop is runner/mcp_protocol.py.
20
- """
21
-
22
- import argparse
23
- import copy
24
- import io
25
- import json
26
- import os
27
- import sys
28
- from contextlib import redirect_stdout
29
- from pathlib import Path
30
- from typing import Any, Callable, Dict, List, Optional
31
-
32
- # Bootstrap imports when launched as `python3 runner/mcp_server.py`
33
- SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
34
- PROJECT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
35
- if PROJECT_ROOT not in sys.path:
36
- sys.path.insert(0, PROJECT_ROOT)
37
-
38
- from runner.mcp_protocol import PROTOCOL_VERSION, StdioJsonRpcServer, ToolSpec # noqa: E402
39
-
40
- SERVER_NAME = "iumbtems"
41
- SERVER_VERSION = "0.3.0"
42
-
43
-
44
- # ---------------------------------------------------------------------------
45
- # helpers
46
- # ---------------------------------------------------------------------------
47
-
48
-
49
- def _tool_text(payload: Any) -> str:
50
- """Normalize a handler return into the MCP text content block body."""
51
- if isinstance(payload, str):
52
- return payload
53
- return json.dumps(payload, indent=2, default=str)
54
-
55
-
56
- def _capture_stdout(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> str:
57
- """Run a print-based CLI function and return what it wrote to stdout."""
58
- buf = io.StringIO()
59
- with redirect_stdout(buf):
60
- fn(*args, **kwargs)
61
- return buf.getvalue()
62
-
63
-
64
- # ---------------------------------------------------------------------------
65
- # tool handlers
66
- # ---------------------------------------------------------------------------
67
-
68
- DEFAULT_RESEARCH_DIR = ".research"
69
-
70
- # `iumbtems_config` arguments that are not config keys.
71
- CONFIG_TOOL_CONTROL_ARGS = ("base_dir", "dir", "show", "expected_hash", "expectedHash")
72
-
73
- # Legacy aliases accepted for the corresponding canonical key (kept for the
74
- # slash-command/CLI vocabulary; the advertised schema uses canonical names).
75
- CONFIG_KEY_ALIASES = {
76
- "engine": "search_engine",
77
- "depth": "max_iterations",
78
- "divergence": "divergence_threshold",
79
- }
80
-
81
-
82
- def config_tool_input_schema() -> Dict[str, Any]:
83
- """Advertised `iumbtems_config` input schema, derived from the canonical CONFIG.
84
-
85
- Generated from `runner.schemas.CONFIG` so the MCP surface cannot drift from
86
- the persisted contract (a parity fixture asserts this).
87
- """
88
- from runner.schemas import CONFIG
89
-
90
- props: Dict[str, Any] = {
91
- "base_dir": {
92
- "type": "string",
93
- "description": "Path to .research workspace (default .research)",
94
- },
95
- "show": {
96
- "type": "boolean",
97
- "description": (
98
- "Inspect only; equivalent to calling with no update keys "
99
- "(reads are always safe). When true, the call is read-only "
100
- "even if update keys are present: they are listed under "
101
- "`ignored_updates` and nothing is written (the expected_hash "
102
- "guard, including the dual-spelling conflict check, is not "
103
- "evaluated)."
104
- ),
105
- },
106
- "expected_hash": {
107
- "type": "string",
108
- "description": (
109
- "Optimistic-concurrency guard: SHA-256 (or a unique prefix of at "
110
- "least 8 hex chars) of the config file you read. A mismatch "
111
- "returns a structured stale error with a fresh snapshot and "
112
- "writes nothing. Empty/whitespace-only spellings count as absent. "
113
- "Alias: `expectedHash`; if both spellings are "
114
- "present they must be equal after normalization (strip + "
115
- "lowercase) or the call is rejected."
116
- ),
117
- },
118
- "expectedHash": {
119
- "type": "string",
120
- "description": "Alias of `expected_hash` (camelCase); must not conflict with it.",
121
- },
122
- }
123
- for key, subschema in CONFIG["properties"].items():
124
- props[key] = copy.deepcopy(subschema)
125
- return {"type": "object", "properties": props}
126
-
127
-
128
- def _resolve_base_dir(args: Dict[str, Any]) -> Path:
129
- raw = args.get("base_dir") or args.get("dir")
130
- if raw:
131
- return Path(os.path.realpath(str(raw)))
132
- # Project root wins over process cwd: a relative ".research" resolved
133
- # against the MCP server's cwd escapes the project (observed live:
134
- # evidence landed in ~/.research instead of the project tree).
135
- project = os.environ.get("IUMBTEMS_PROJECT_DIR", "").strip()
136
- if project and Path(project).is_dir():
137
- return Path(os.path.realpath(os.path.join(project, DEFAULT_RESEARCH_DIR)))
138
- return Path(os.path.realpath(DEFAULT_RESEARCH_DIR))
139
-
140
-
141
- def _canonical_config_keys() -> List[str]:
142
- from runner.schemas import CONFIG
143
-
144
- return list(CONFIG["properties"].keys())
145
-
146
-
147
- def _allows_null(schema: Dict[str, Any]) -> bool:
148
- types = schema.get("type")
149
- if isinstance(types, list):
150
- return "null" in types
151
- return types == "null"
152
-
153
-
154
- def _normalize_hash_spelling(value: Any) -> str:
155
- """Strip + lowercase a hash spelling for conflict comparison (H2).
156
-
157
- Mirrors `normalize_expected_hash` in `skills/swarm_config/configure.py`
158
- WITHOUT raising: malformed spellings still compare (so genuinely different
159
- malformed guards conflict), and the single `save_config` writer later
160
- validates the winner (malformed -> CONFIG_INVALID_EXPECTED_HASH).
161
- Empty/whitespace-only spellings normalize to "" and are treated as absent
162
- by the caller (R3: "" never conflicts).
163
- """
164
- return str(value).strip().lower()
165
-
166
-
167
- def _hash_spelling_present(value: Any) -> bool:
168
- """True when an expected-hash spelling counts as provided (R3).
169
-
170
- `None` and empty/whitespace-only strings mean "no guard" (mirroring
171
- `normalize_expected_hash`); everything else is a guard spelling even if
172
- malformed (malformed -> CONFIG_INVALID_EXPECTED_HASH on the write leg).
173
- """
174
- if value is None:
175
- return False
176
- return str(value).strip() != ""
177
-
178
-
179
- def _find_nested_null_violations(updates: Dict[str, Any]) -> List[str]:
180
- """Dotted paths in `updates` carrying None where the schema bans null (H4).
181
-
182
- The top-level loop in `_handle_config` already catches a direct
183
- `{"mode": None}`; this extends the same `NULL_FOR_NON_NULLABLE_KEY` code
184
- to nested nulls such as `{"verify": {"min_fuzzy_confidence": None}}`,
185
- which previously surfaced as the generic `CONFIG_VALIDATION_FAILED`.
186
- Contract: `NULL_FOR_NON_NULLABLE_KEY` wins for ANY caller-supplied null
187
- at any depth; `CONFIG_VALIDATION_FAILED` covers every other schema
188
- violation (including nulls already persisted on disk by a hand edit,
189
- which the caller did not supply in this call).
190
- """
191
- from runner.schemas import CONFIG
192
-
193
- found: List[str] = []
194
-
195
- def walk(value: Any, subschema: Any, path: str) -> None:
196
- if value is None:
197
- if isinstance(subschema, dict) and not _allows_null(subschema):
198
- found.append(path)
199
- return
200
- if isinstance(value, dict) and isinstance(subschema, dict):
201
- props = subschema.get("properties") or {}
202
- additional = subschema.get("additionalProperties")
203
- for key, child in value.items():
204
- if key in props:
205
- walk(child, props[key], f"{path}.{key}" if path else str(key))
206
- elif isinstance(additional, dict):
207
- # e.g. `mcp_servers` values must be boolean; a null there
208
- # is a null-violation too, with the full dotted path.
209
- walk(
210
- child,
211
- additional,
212
- f"{path}.{key}" if path else str(key),
213
- )
214
- # Unknown keys with no `additionalProperties` schema carry no
215
- # nullability info; generic validation decides them (unknown
216
- # object keys are preserved verbatim).
217
- return
218
- if isinstance(value, list) and isinstance(subschema, dict):
219
- items = subschema.get("items")
220
- if isinstance(items, dict):
221
- for idx, child in enumerate(value):
222
- walk(child, items, f"{path}[{idx}]")
223
-
224
- props = CONFIG.get("properties") or {}
225
- for key, value in updates.items():
226
- if value is None or not isinstance(value, (dict, list)):
227
- continue # top-level nulls are handled by the caller loop
228
- canon = CONFIG_KEY_ALIASES.get(key, key)
229
- if canon in props:
230
- walk(value, props[canon], str(canon))
231
- return sorted(found)
232
-
233
-
234
- def _config_updated_payload(
235
- cfg_file: Path, candidate: Dict[str, Any], default_config: Dict[str, Any]
236
- ) -> Dict[str, Any]:
237
- """Success payload for the config write leg (H6).
238
-
239
- `written_to` is ALWAYS an absolute path (via realpath, so symlinked
240
- workspaces resolve to the true location). Both updated legs — the normal
241
- single-read leg and the defensive branch where the just-written bytes
242
- cannot be re-read — share this helper so the shape cannot diverge.
243
- """
244
- import hashlib
245
-
246
- from skills.swarm_config.configure import _read_bounded_bytes, merge_config
247
-
248
- abs_path = str(Path(os.path.realpath(str(cfg_file))))
249
- try:
250
- # Phase-08 R2 (W17): bounded re-read — the just-written file is
251
- # regular by construction, but a single read primitive keeps the
252
- # FIFO-hardening total (a concurrent swap can only fail into the
253
- # defensive branch below, never hang the writer).
254
- raw_bytes = _read_bounded_bytes(Path(cfg_file))
255
- except OSError:
256
- raw_bytes = None
257
- if raw_bytes is None:
258
- # A successful save always leaves bytes; keep the payload shape without
259
- # a second read in this (unreachable) defensive branch.
260
- return {
261
- "status": "updated",
262
- "config": candidate,
263
- "written_to": abs_path,
264
- "hash": None,
265
- }
266
- try:
267
- parsed = json.loads(raw_bytes.decode("utf-8"))
268
- except (ValueError, UnicodeDecodeError):
269
- parsed = None
270
- effective = (
271
- merge_config(default_config, parsed)
272
- if isinstance(parsed, dict)
273
- else merge_config(default_config, {})
274
- )
275
- return {
276
- "status": "updated",
277
- "config": effective,
278
- "written_to": abs_path,
279
- "hash": hashlib.sha256(raw_bytes).hexdigest(),
280
- }
281
-
282
-
283
- def _legacy_pin_migration_pending(base_dir: str) -> bool:
284
- """Would the legacy-pin migration apply on the next write? (read-only)
285
-
286
- Phase-04 R11: the `show` leg reports this WITHOUT persisting, so a read API
287
- stays side-effect free. Mirrors `heal_config`'s decision — the on-disk file
288
- carries a legacy ``["claude", "-p"]`` pin AND the would-be-persisted config
289
- passes canonical validation (an invalid file is never healed, R1) — but
290
- never writes.
291
- """
292
- try:
293
- from skills.swarm_config.configure import (
294
- _read_bounded_bytes,
295
- load_config,
296
- migrate_legacy_agent_backends,
297
- validate_config,
298
- )
299
-
300
- cfg_file = Path(base_dir) / "config.json"
301
- # Phase-08 R2 (W17): bounded pre-read — a FIFO here hung the show leg
302
- # on a plain `read_bytes`. Unreadable/non-regular yields "no pending
303
- # migration" (fail-open read, matching `load_config` defaults).
304
- raw_bytes = _read_bounded_bytes(cfg_file)
305
- if raw_bytes is None:
306
- return False
307
- raw = json.loads(raw_bytes.decode("utf-8"))
308
- if not isinstance(raw, dict):
309
- return False
310
- if not migrate_legacy_agent_backends(raw):
311
- return False
312
- return not validate_config(load_config(base_dir))
313
- except Exception:
314
- return False
315
-
316
-
317
- def _handle_config(args: Dict[str, Any]) -> str:
318
- """Inspect or modify swarm parameters in .research/config.json.
319
-
320
- Every canonical key is accepted; invalid values, unknown keys, and `null`
321
- for non-nullable keys are rejected with a structured error before anything
322
- is written. Writes go through the single canonical writer (`save_config`:
323
- locked, validated, atomic, unknown-key-preserving) with an optional
324
- `expected_hash` guard (both `expected_hash` and `expectedHash` spellings).
325
-
326
- Phase-05 H2 decisions: the two hash spellings compare NORMALIZED
327
- (strip + lowercase, mirroring `normalize_expected_hash`), so
328
- `"ABCDEF12"` vs `"abcdef12"` is one guard, not a conflict; empty or
329
- whitespace-only spellings count as absent on both sides (R3), so `""`
330
- never conflicts; and `show` forces the read-only leg even when update
331
- keys are present (they are reported under `ignored_updates` and nothing
332
- is written, without evaluating the guard or the dual-spelling conflict
333
- check — R1 ordering).
334
- """
335
- from skills.swarm_config.configure import (
336
- DEFAULT_CONFIG,
337
- ConfigError,
338
- load_config,
339
- merge_config,
340
- save_config,
341
- validate_config,
342
- )
343
- from runner.schemas import CONFIG
344
-
345
- base_dir = str(_resolve_base_dir(args))
346
- cfg = load_config(base_dir)
347
-
348
- snake = args.get("expected_hash")
349
- camel = args.get("expectedHash")
350
- snake_present = _hash_spelling_present(snake)
351
- camel_present = _hash_spelling_present(camel)
352
-
353
- accepted = _canonical_config_keys()
354
- nullable = [key for key in accepted if _allows_null(CONFIG["properties"][key])]
355
- unknown: List[str] = []
356
- null_for_non_nullable: List[str] = []
357
- updates: Dict[str, Any] = {}
358
- for key, value in args.items():
359
- if key in CONFIG_TOOL_CONTROL_ARGS:
360
- continue
361
- canon = CONFIG_KEY_ALIASES.get(key, key)
362
- if canon not in accepted:
363
- unknown.append(str(key))
364
- continue
365
- if value is None:
366
- if _allows_null(CONFIG["properties"][canon]):
367
- # Nullable key: explicit clear / follow-the-default-policy.
368
- updates[canon] = None
369
- else:
370
- # R8: never mask a null for a non-nullable key as a no-op.
371
- null_for_non_nullable.append(str(canon))
372
- continue
373
- updates[canon] = value
374
-
375
- if unknown:
376
- return _tool_text(
377
- {
378
- "status": "error",
379
- "code": "UNKNOWN_CONFIG_KEY",
380
- "written": False,
381
- "errors": [f"unknown config key(s): {', '.join(sorted(unknown))}"],
382
- "accepted_keys": accepted,
383
- }
384
- )
385
- # H4: nested caller-supplied nulls share the top-level code, with dotted
386
- # paths (e.g. `verify.min_fuzzy_confidence`).
387
- null_for_non_nullable.extend(_find_nested_null_violations(updates))
388
- if null_for_non_nullable:
389
- return _tool_text(
390
- {
391
- "status": "error",
392
- "code": "NULL_FOR_NON_NULLABLE_KEY",
393
- "written": False,
394
- "errors": [
395
- f"null is not accepted for non-nullable key(s): "
396
- f"{', '.join(sorted(set(null_for_non_nullable)))}; omit the key to "
397
- "leave it unchanged, or pass a valid value"
398
- ],
399
- "nullable_keys": nullable,
400
- "accepted_keys": accepted,
401
- }
402
- )
403
-
404
- if args.get("show") or not updates:
405
- # R11/H2/R1: the show leg is strictly read-only — a settings panel reads
406
- # through it, so it must never persist the legacy-pin migration nor
407
- # evaluate the expected_hash guard (including the dual-spelling
408
- # conflict check, which lives BELOW this return). Report whether a
409
- # migration is pending; only the write leg below heals. When `show`
410
- # overrides present update keys, name them so the caller knows nothing
411
- # was written.
412
- payload: Dict[str, Any] = {
413
- "status": "current",
414
- "config": cfg,
415
- "migrated": _legacy_pin_migration_pending(base_dir),
416
- }
417
- if args.get("show") and updates:
418
- payload["ignored_updates"] = sorted(updates)
419
- return _tool_text(payload)
420
-
421
- # R1 ordering: the dual-spelling conflict check lives BELOW the show
422
- # early-return so the show leg is fully read-only. R4/H2: both spellings
423
- # must agree AFTER normalization; never silently prefer one.
424
- # Case/whitespace-only differences are one guard; empty/whitespace-only
425
- # spellings count as absent (R3).
426
- if (
427
- snake_present
428
- and camel_present
429
- and _normalize_hash_spelling(snake) != _normalize_hash_spelling(camel)
430
- ):
431
- return _tool_text(
432
- {
433
- "status": "error",
434
- "code": "CONFLICTING_EXPECTED_HASH",
435
- "written": False,
436
- "errors": [
437
- "expected_hash and expectedHash were both provided with "
438
- "different values; provide one spelling (or two equal values)."
439
- ],
440
- "expected_hash": str(snake),
441
- "expectedHash": str(camel),
442
- }
443
- )
444
- if snake_present:
445
- expected: Any = snake
446
- elif camel_present:
447
- expected = camel
448
- else:
449
- expected = None
450
-
451
- candidate = merge_config(cfg, updates)
452
- problems = validate_config(candidate)
453
- if problems:
454
- return _tool_text(
455
- {
456
- "status": "error",
457
- "code": "CONFIG_VALIDATION_FAILED",
458
- "written": False,
459
- "errors": problems,
460
- "config": cfg,
461
- }
462
- )
463
-
464
- # R12 (F1): do NOT heal before saving. The candidate is already migrated in
465
- # memory (`load_config`) and `save_config` merges it over the on-disk bytes,
466
- # so the legacy-pin migration persists as a side effect — while the caller's
467
- # `expected_hash` guard still matches the bytes they read. A heal-before-save
468
- # rewrote the file and turned a valid guarded write into a false `stale`.
469
- try:
470
- save_config(
471
- candidate,
472
- base_dir,
473
- expected_hash=str(expected) if expected is not None else None,
474
- )
475
- except ConfigError as exc:
476
- payload: Dict[str, Any] = {"status": "error", "written": False}
477
- payload.update(exc.to_dict())
478
- if exc.code == "CONFIG_STALE":
479
- payload["status"] = "stale"
480
- payload.update(exc.details)
481
- return _tool_text(payload)
482
-
483
- # R10 (single read): derive BOTH the effective config and the hash from ONE
484
- # read of the just-published bytes, so the storage mirror and the `changed`
485
- # event can never receive an era-A config paired with an era-B hash.
486
- # `save_config` publishes a fully defaults-merged, validated object
487
- # atomically, so the bytes on disk are already the effective config; the
488
- # earlier `load_config()` + `config_hash()` were two independent reads.
489
- # H6: the shared helper guarantees an absolute `written_to` on both legs.
490
- cfg_file = Path(base_dir) / "config.json"
491
- return _tool_text(_config_updated_payload(cfg_file, candidate, DEFAULT_CONFIG))
492
-
493
-
494
- def _run_swarm_mode(mode: Optional[str], args: Dict[str, Any]) -> str:
495
- """Shared dispatch for the four swarm-shaped tools."""
496
- from runner.research_swarm import SwarmRunner
497
-
498
- objective = args.get("objective") or args.get("target") or args.get("feature")
499
- if not objective:
500
- raise ValueError("objective is required")
501
-
502
- frontier = args.get("frontier") or args.get("frontier_file")
503
- overrides = None
504
- backend = (args.get("backend") or "").strip().lower()
505
- if backend in ("claude", "opencode"):
506
- argv = ["claude", "-p"] if backend == "claude" else ["opencode", "run"]
507
- overrides = {"alpha": {"backend": argv}, "beta": {"backend": argv}}
508
- runner = SwarmRunner(
509
- base_dir=_resolve_base_dir(args),
510
- mock_mode=bool(args.get("mock_claude") or args.get("mock_mode")),
511
- mode=mode or args.get("mode"),
512
- engine=args.get("engine"),
513
- depth=args.get("depth"),
514
- domain_pack=args.get("domain_pack"),
515
- agent_overrides=overrides,
516
- resume=bool(args.get("resume")),
517
- dry_run=bool(args.get("dry_run")),
518
- )
519
- buf = io.StringIO()
520
- status = None
521
- with redirect_stdout(buf):
522
- status = runner.run_swarm(
523
- objective, frontier_file=Path(frontier) if frontier else None
524
- )
525
-
526
- mode_name = mode or args.get("mode") or "research"
527
- log = buf.getvalue()[-4000:]
528
-
529
- # Decide from the ACTUAL run outcome, not the raw gate verdict: mock and dry
530
- # runs are exempt from the fail-fast halt (they complete/validate), and only
531
- # a real halt sets `_halted`. Reading `_search_gate.action` alone mislabelled
532
- # every blocked-but-exempt run as `halted`.
533
- if status == "halted" or getattr(runner, "_halted", False):
534
- gate = getattr(runner, "_search_gate", None) or {}
535
- return _tool_text(
536
- {
537
- "status": "halted",
538
- "mode": mode_name,
539
- "reason": gate.get("reason"),
540
- "message": gate.get("message"),
541
- "log": log,
542
- }
543
- )
544
- if status == "dry-run":
545
- return _tool_text({"status": "dry-run", "mode": mode_name, "log": log})
546
- if status == "incomplete":
547
- return _tool_text({"status": "incomplete", "mode": mode_name, "log": log})
548
-
549
- report = runner.base_dir / (
550
- {
551
- "audit": "code_audit_report.md",
552
- "scout": "oss_scout_report.md",
553
- "brainstorm": "brainstorm_report.md",
554
- "darkharvest": "darkharvest_report.md",
555
- }.get(mode_name, "final_synthesis.md")
556
- )
557
- return _tool_text(
558
- {
559
- "status": "completed",
560
- "mode": mode_name,
561
- "report": str(report),
562
- "log": log,
563
- }
564
- )
565
-
566
-
567
- def _handle_swarm_research(args: Dict[str, Any]) -> str:
568
- """Run the dialectic multi-agent research swarm (thesis vs antithesis)."""
569
- return _run_swarm_mode(None, args)
570
-
571
-
572
- def _handle_code_audit(args: Dict[str, Any]) -> str:
573
- """Run a dialectic codebase architecture & security audit."""
574
- return _run_swarm_mode("audit", args)
575
-
576
-
577
- def _handle_oss_scout(args: Dict[str, Any]) -> str:
578
- """Scout open-source software, libraries, and clean-room blueprints."""
579
- return _run_swarm_mode("scout", args)
580
-
581
-
582
- def _handle_brainstorm(args: Dict[str, Any]) -> str:
583
- """Run lateral brainstorming (feature vectors + paradigm moves + spikes)."""
584
- return _run_swarm_mode("brainstorm", args)
585
-
586
-
587
- def _handle_darkharvest(args: Dict[str, Any]) -> str:
588
- """Run product competitor teardown (seed + expand, harvest verdicts)."""
589
- return _run_swarm_mode("darkharvest", args)
590
-
591
-
592
- def _handle_factory(args: Dict[str, Any]) -> str:
593
- """Drive factory run state (init / phase-add / qa-record / expansion / stop / gate).
594
-
595
- Wraps skills/factory/scripts/factory.py so agents never need a filesystem
596
- path to the helper: the npm-installed toolchain lives outside the project,
597
- and relative `skills/...` paths broke in consuming projects (observed
598
- live: the manager agent ran `find / -name factory.py`).
599
-
600
- Phase-08 R4 (W6): the `gate` command forwards `--run/--phase/--action`
601
- (+ optional `--reason`) to the helper's `gate` subcommand. Before this
602
- fix the handler accepted `gate` but forwarded no gate args, so the helper
603
- exited 2 (missing required `--run/--phase/--action`) and gate was
604
- unreachable through the tool surface.
605
- """
606
- import subprocess
607
-
608
- command = str(args.get("command") or "").strip()
609
- if command not in ("init", "phase-add", "qa-record", "expansion", "stop", "gate"):
610
- raise ValueError(
611
- "command must be one of: init, phase-add, qa-record, expansion, stop, gate"
612
- )
613
- script = Path(PROJECT_ROOT) / "skills" / "factory" / "scripts" / "factory.py"
614
- if not script.is_file():
615
- raise FileNotFoundError(f"factory helper not found at {script}")
616
-
617
- project = (
618
- args.get("project_dir") or os.environ.get("IUMBTEMS_PROJECT_DIR") or os.getcwd()
619
- )
620
- cmd = [sys.executable, str(script), command, "--project-dir", str(project)]
621
- if command == "init":
622
- cmd += ["--run", str(args.get("run") or "")]
623
- elif command == "phase-add":
624
- cmd += [
625
- "--run",
626
- str(args.get("run") or ""),
627
- "--phase",
628
- str(args.get("phase") or ""),
629
- "--goal",
630
- str(args.get("goal") or ""),
631
- "--accept",
632
- str(args.get("accept") or ""),
633
- ]
634
- elif command == "qa-record":
635
- cmd += [
636
- "--run",
637
- str(args.get("run") or ""),
638
- "--phase",
639
- str(args.get("phase") or ""),
640
- "--seat",
641
- str(args.get("seat") or ""),
642
- "--verdict",
643
- str(args.get("verdict") or ""),
644
- ]
645
- if args.get("reason"):
646
- cmd += ["--reason", str(args["reason"])]
647
- elif command == "expansion":
648
- cmd += [
649
- "--run",
650
- str(args.get("run") or ""),
651
- "--loops",
652
- str(int(args.get("loops") or 1)),
653
- ]
654
- if args.get("max_loops"):
655
- cmd += ["--max-loops", str(int(args["max_loops"]))]
656
- elif command == "stop":
657
- cmd += ["--run", str(args.get("run") or "")]
658
- elif command == "gate":
659
- cmd += [
660
- "--run",
661
- str(args.get("run") or ""),
662
- "--phase",
663
- str(args.get("phase") or ""),
664
- "--action",
665
- str(args.get("action") or ""),
666
- ]
667
- if args.get("reason"):
668
- cmd += ["--reason", str(args["reason"])]
669
-
670
- proc = subprocess.run(
671
- cmd,
672
- capture_output=True,
673
- text=True,
674
- shell=False,
675
- stdin=subprocess.DEVNULL,
676
- cwd=str(project),
677
- )
678
- payload = {
679
- "status": "ok" if proc.returncode == 0 else "error",
680
- "command": command,
681
- "returncode": proc.returncode,
682
- "project_dir": str(project),
683
- "output": (proc.stdout or proc.stderr or "").strip()[-4000:],
684
- }
685
- if proc.returncode == 2:
686
- payload["status"] = "escalated"
687
- return _tool_text(payload)
688
-
689
-
690
- def _verify_min_fuzzy_confidence(cfg: Any) -> Any:
691
- """Configured `verify.min_fuzzy_confidence`, defaulting to 0.88 (H5).
692
-
693
- SINGLE-ENFORCEMENT-POINT CONTRACT: this returns the configured value
694
- UNTOUCHED (no `float()` parse, no clamp) and `SourceHasher.verify_quote`
695
- enforces it via `coerce_min_fuzzy_confidence` at call time. The persisted
696
- schema stays strict (`type: number`); coercion exists only for
697
- hand-edited files and direct library callers. The response echoes the
698
- EFFECTIVE threshold (`coerce_min_fuzzy_confidence(configured)`), so the
699
- reported value is always the one that governed the decision.
700
- """
701
- verify = cfg.get("verify") if isinstance(cfg, dict) else None
702
- raw = verify.get("min_fuzzy_confidence") if isinstance(verify, dict) else None
703
- return 0.88 if raw is None else raw
704
-
705
-
706
- def _handle_verify_quote(args: Dict[str, Any]) -> str:
707
- """Verify a verbatim quote against the content-addressed source cache."""
708
- from skills.research_cache.hasher import (
709
- SourceHasher,
710
- coerce_min_fuzzy_confidence,
711
- )
712
- from skills.swarm_config.configure import load_config
713
-
714
- content_hash = (
715
- args.get("hash") or args.get("content_hash") or args.get("source_hash")
716
- )
717
- quote = args.get("quote")
718
- if not content_hash or quote is None:
719
- raise ValueError("hash and quote are required")
720
-
721
- base_dir = _resolve_base_dir(args)
722
- configured = _verify_min_fuzzy_confidence(load_config(str(base_dir)))
723
- hasher = SourceHasher(base_dir)
724
- is_verified, confidence, message = hasher.verify_quote(
725
- content_hash, quote, min_fuzzy_confidence=configured
726
- )
727
- return _tool_text(
728
- {
729
- "verified": bool(is_verified),
730
- "confidence": float(confidence),
731
- "message": message,
732
- "content_hash": content_hash,
733
- "min_fuzzy_confidence": coerce_min_fuzzy_confidence(configured),
734
- }
735
- )
736
-
737
-
738
- def _handle_report_retraction(args: Dict[str, Any]) -> str:
739
- """Record a RETRACTED/REVISED event for a cached source (Living Dossiers)."""
740
- from runner.living_dossiers import write_retraction
741
-
742
- source_hash = args.get("hash") or args.get("source_hash")
743
- event = args.get("event")
744
- if not source_hash or not event:
745
- raise ValueError("hash and event are required")
746
- base_dir = _resolve_base_dir(args)
747
- path = write_retraction(
748
- base_dir,
749
- source_hash,
750
- str(event).upper(),
751
- note=args.get("note") or "",
752
- supersedes=args.get("supersedes"),
753
- )
754
- return _tool_text({"status": "recorded", "path": str(path)})
755
-
756
-
757
- def _handle_check_staleness(args: Dict[str, Any]) -> str:
758
- """Run one claim-degradation pass (retractions -> STALE/SUSPECT + requeue)."""
759
- from runner.living_dossiers import check_staleness
760
-
761
- base_dir = _resolve_base_dir(args)
762
- scope_ids = args.get("scope_ids")
763
- return _tool_text(check_staleness(base_dir, scope_ids=scope_ids))
764
-
765
-
766
- def _handle_set_domain_pack(args: Dict[str, Any]) -> str:
767
- """Activate a Regulated Domain Pack (epistemic constitution) in config.
768
-
769
- H3: surrounding whitespace is stripped before resolving — a blank or
770
- whitespace-only id is rejected (the canonical schema additionally rejects
771
- blank/padded ids at validation, so a padded id can never persist).
772
- """
773
- from runner.refinement import load_domain_pack
774
- from skills.swarm_config.configure import load_config, save_config
775
-
776
- raw_pack = args.get("pack") or args.get("domain_pack") or args.get("pack_id")
777
- pack = raw_pack.strip() if isinstance(raw_pack, str) else raw_pack
778
- if not pack or (isinstance(pack, str) and not pack.strip()):
779
- raise ValueError("pack is required (blank/whitespace-only ids are rejected)")
780
- constitution = load_domain_pack(pack) # raises if unknown — fail loudly
781
- base_dir = str(_resolve_base_dir(args))
782
- cfg = load_config(base_dir)
783
- cfg["domain_pack"] = pack
784
- save_config(cfg, base_dir)
785
- return _tool_text(
786
- {
787
- "status": "activated",
788
- "domain_pack": pack,
789
- "accept_threshold": constitution.accept_threshold,
790
- "retraction_policy": constitution.retraction_policy,
791
- "banned_domains": constitution.banned_domains,
792
- }
793
- )
794
-
795
-
796
- def _handle_export_brief(args: Dict[str, Any]) -> str:
797
- """Export a proof-carrying research brief (signed, self-contained bundle)."""
798
- from runner.pcrb import export_brief
799
-
800
- base_dir = _resolve_base_dir(args)
801
- key = args.get("key")
802
- key_path = Path(args["key_file"]) if args.get("key_file") else None
803
- path = export_brief(
804
- base_dir,
805
- out_path=Path(args["out"]) if args.get("out") else None,
806
- scope_ids=args.get("scope_ids"),
807
- objective=args.get("objective"),
808
- key=key,
809
- key_path=key_path,
810
- key_id=args.get("key_id") or "local",
811
- )
812
- return _tool_text({"status": "exported", "path": str(path)})
813
-
814
-
815
- def _handle_verify_brief(args: Dict[str, Any]) -> str:
816
- """Verify a proof-carrying brief against its bundled sources (no trust in the LLM)."""
817
- import os
818
-
819
- from runner.pcrb import ENV_KEY
820
- from runner.pcrb_verify import verify_brief
821
-
822
- brief = args.get("brief") or args.get("path")
823
- if not brief:
824
- raise ValueError("brief path is required")
825
- p = Path(os.path.realpath(str(brief)))
826
- if not p.exists():
827
- raise FileNotFoundError(f"no such brief: {p}")
828
- bundle = json.loads(p.read_text(encoding="utf-8"))
829
-
830
- key = None
831
- if args.get("key"):
832
- key = args["key"].encode("utf-8")
833
- elif args.get("key_file"):
834
- kf = Path(os.path.realpath(str(args["key_file"])))
835
- # An explicit key_file that cannot be read must surface as an error, not
836
- # a silent fallthrough to `alg: "none"`.
837
- if not kf.is_file():
838
- raise ValueError(f"key file is not a readable file: {args['key_file']!r}")
839
- key = kf.read_bytes().strip()
840
- elif os.environ.get(ENV_KEY):
841
- key = os.environ[ENV_KEY].encode("utf-8")
842
-
843
- return _tool_text(verify_brief(bundle, key=key))
844
-
845
-
846
- def _handle_reindex_claims(args: Dict[str, Any]) -> str:
847
- """Rebuild the derived claims.sqlite index from .research flat files."""
848
- from runner.claim_store import reindex
849
-
850
- base_dir = _resolve_base_dir(args)
851
- return _tool_text(reindex(base_dir))
852
-
853
-
854
- def _handle_doctor(args: Dict[str, Any]) -> str:
855
- """Preflight for the resolved workspace: backend, engine, write test, version."""
856
- from runner.preflight import format_report, host_websearch_provider, preflight
857
- from skills.swarm_config.configure import load_config
858
-
859
- base_dir = _resolve_base_dir(args)
860
- cfg = load_config(str(base_dir))
861
- report = preflight(
862
- base_dir,
863
- mode=args.get("mode") or cfg.get("mode") or "research",
864
- config=cfg,
865
- # Never spawn in a health check; report resolution only.
866
- mock_mode=True,
867
- probe=bool(args.get("probe", True)),
868
- # Feed the same host-reported provider/since the run path uses, or the
869
- # doctor names a different provider than the run banner (observed:
870
- # IUMBTEMS_WEBSEARCH_PROVIDER=tavily -> doctor said duckduckgo).
871
- host_provider=host_websearch_provider(cfg),
872
- since=args.get("since"),
873
- )
874
- return _tool_text({"report": report, "text": format_report(report)})
875
-
876
-
877
- def _handle_test(args: Dict[str, Any]) -> str:
878
- """Run the packaged test suite in a subprocess (timeout-bounded)."""
879
- import subprocess
880
-
881
- timeout = float(os.environ.get("IUMBTEMS_TEST_TIMEOUT", "300"))
882
- try:
883
- result = subprocess.run(
884
- [sys.executable, "-m", "unittest", "discover", "-s", "runner/tests"],
885
- cwd=str(PROJECT_ROOT),
886
- capture_output=True,
887
- text=True,
888
- timeout=timeout,
889
- stdin=subprocess.DEVNULL,
890
- )
891
- except subprocess.TimeoutExpired:
892
- return _tool_text({"status": "timeout", "timeout_s": timeout})
893
- tail = "\n".join((result.stderr or "").strip().splitlines()[-12:])
894
- passed = result.returncode == 0
895
- return _tool_text(
896
- {
897
- "status": "pass" if passed else "fail",
898
- "returncode": result.returncode,
899
- "tail": tail,
900
- }
901
- )
902
-
903
-
904
- def _handle_socratic_frontier(args: Dict[str, Any]) -> str:
905
- """Inspect, add to, settle, or export the Socratic decision-tree frontier."""
906
- from skills.grilling.socratic_tree import (
907
- DecisionNode,
908
- DesignTree,
909
- frontier_transaction,
910
- )
911
-
912
- file_path = Path(
913
- args.get("file") or args.get("frontier_file") or ".research/frontier.json"
914
- )
915
- action = str(args.get("action") or "").strip().lower()
916
- node_spec = args.get("node") if isinstance(args.get("node"), dict) else {}
917
- objective = args.get("objective", "")
918
-
919
- # Authoring: add a node (tool-drivable, previously hand-written JSON only).
920
- # Mutations run as a locked read-modify-write so concurrent add/settle calls
921
- # are serialized across processes instead of racing on a bare open("w").
922
- if action == "add" or node_spec:
923
- if not node_spec.get("id") or not node_spec.get("question"):
924
- return _tool_text(
925
- {"status": "error", "message": "'node' needs 'id' and 'question'"}
926
- )
927
- with frontier_transaction(file_path, objective) as tree:
928
- tree.add_node(
929
- DecisionNode(
930
- node_id=node_spec["id"],
931
- title=node_spec.get("title") or node_spec["id"],
932
- question=node_spec["question"],
933
- recommended=node_spec.get("recommended", ""),
934
- options=node_spec.get("options") or [],
935
- prerequisites=node_spec.get("depends_on") or [],
936
- )
937
- )
938
- return _tool_text(
939
- {"status": "added", "node_id": node_spec["id"], "file": str(file_path)}
940
- )
941
-
942
- if action == "export":
943
- with frontier_transaction(file_path, objective):
944
- pass
945
- return _tool_text({"status": "exported", "file": str(file_path)})
946
-
947
- if args.get("settle"):
948
- node_id, answer = args["settle"][0], args["settle"][1]
949
- with frontier_transaction(file_path, objective) as tree:
950
- tree.settle_node(node_id, answer)
951
- return _tool_text(
952
- {"status": "settled", "node_id": node_id, "file": str(file_path)}
953
- )
954
-
955
- if not file_path.exists():
956
- return _tool_text(
957
- {
958
- "status": "no_frontier",
959
- "message": f"No frontier file at {file_path}. Run /grilling first.",
960
- "file": str(file_path),
961
- }
962
- )
963
-
964
- tree = DesignTree.load_from_json(file_path)
965
- frontier = tree.compute_frontier()
966
- return _tool_text(
967
- {
968
- "status": "frontier",
969
- "objective": tree.objective,
970
- "is_complete": tree.is_complete(),
971
- "total_nodes": len(tree.nodes),
972
- "settled": sum(1 for n in tree.nodes.values() if n.is_settled()),
973
- "frontier": [
974
- {"node_id": n.node_id, "question": n.question} for n in frontier
975
- ],
976
- "file": str(file_path),
977
- }
978
- )
979
-
980
-
981
- # ---------------------------------------------------------------------------
982
- # registry
983
- # ---------------------------------------------------------------------------
984
-
985
-
986
- def build_tools() -> List[ToolSpec]:
987
- """The 7 canonical iumbtems_* tools, in-process wrappers only."""
988
- return [
989
- ToolSpec(
990
- name="iumbtems_config",
991
- description=(
992
- "Inspect or modify the active Epistemic Swarm configuration "
993
- "(.research/config.json). Accepts every canonical key; writes are "
994
- "schema-validated, locked, atomic, and preserve unknown keys. "
995
- "null is accepted only for nullable keys (it clears them / "
996
- "follows the default policy); null for a non-nullable key is "
997
- "rejected. Pass expected_hash (alias expectedHash; min 8 hex "
998
- "chars) to guard against a stale write."
999
- ),
1000
- input_schema=config_tool_input_schema(),
1001
- handler=_handle_config,
1002
- ),
1003
- ToolSpec(
1004
- name="iumbtems_swarm_research",
1005
- description="Dispatch the dialectic researcher pair (Agent Alpha thesis vs Agent Beta antithesis) with epistemic auditing. Long-running; blocks until the swarm completes.",
1006
- input_schema={
1007
- "type": "object",
1008
- "properties": {
1009
- "objective": {
1010
- "type": "string",
1011
- "description": "Research question or objective",
1012
- },
1013
- "base_dir": {"type": "string"},
1014
- "frontier_file": {
1015
- "type": "string",
1016
- "description": "Settled frontier.json from grilling",
1017
- },
1018
- "engine": {
1019
- "type": "string",
1020
- "enum": ["duckduckgo", "brave", "firecrawl", "searxng"],
1021
- },
1022
- "depth": {"type": "integer"},
1023
- "mock_claude": {
1024
- "type": "boolean",
1025
- "description": "Synthetic run, zero API cost",
1026
- },
1027
- "backend": {
1028
- "type": "string",
1029
- "enum": ["auto", "claude", "opencode"],
1030
- "description": "Agent runtime backend (default auto = host-native)",
1031
- },
1032
- },
1033
- "required": ["objective"],
1034
- },
1035
- handler=_handle_swarm_research,
1036
- ),
1037
- ToolSpec(
1038
- name="iumbtems_code_audit",
1039
- description="Dispatch a dialectic codebase review (structural architect vs vulnerability red-team).",
1040
- input_schema={
1041
- "type": "object",
1042
- "properties": {
1043
- "target": {
1044
- "type": "string",
1045
- "description": "Codebase target or audit focus",
1046
- },
1047
- "base_dir": {"type": "string"},
1048
- "frontier_file": {
1049
- "type": "string",
1050
- "description": "Settled frontier.json from /grilling",
1051
- },
1052
- "mock_claude": {"type": "boolean"},
1053
- "backend": {
1054
- "type": "string",
1055
- "enum": ["auto", "claude", "opencode"],
1056
- },
1057
- },
1058
- "required": ["target"],
1059
- },
1060
- handler=_handle_code_audit,
1061
- ),
1062
- ToolSpec(
1063
- name="iumbtems_oss_scout",
1064
- description="Scout open-source libraries, audit licenses, and build clean-room blueprints.",
1065
- input_schema={
1066
- "type": "object",
1067
- "properties": {
1068
- "feature": {
1069
- "type": "string",
1070
- "description": "Feature or library to scout",
1071
- },
1072
- "base_dir": {"type": "string"},
1073
- "frontier_file": {
1074
- "type": "string",
1075
- "description": "Settled frontier.json from /grilling",
1076
- },
1077
- "mock_claude": {"type": "boolean"},
1078
- "backend": {
1079
- "type": "string",
1080
- "enum": ["auto", "claude", "opencode"],
1081
- },
1082
- },
1083
- "required": ["feature"],
1084
- },
1085
- handler=_handle_oss_scout,
1086
- ),
1087
- ToolSpec(
1088
- name="iumbtems_brainstorm",
1089
- description="Lateral ideation portfolio (feature vectors + paradigm moves + falsifiable spikes). Never bug fixes.",
1090
- input_schema={
1091
- "type": "object",
1092
- "properties": {
1093
- "objective": {
1094
- "type": "string",
1095
- "description": "Ambiguous prompt to ideate on",
1096
- },
1097
- "base_dir": {"type": "string"},
1098
- "frontier_file": {
1099
- "type": "string",
1100
- "description": "Settled frontier.json from /grilling",
1101
- },
1102
- "mock_claude": {"type": "boolean"},
1103
- "backend": {
1104
- "type": "string",
1105
- "enum": ["auto", "claude", "opencode"],
1106
- },
1107
- },
1108
- "required": ["objective"],
1109
- },
1110
- handler=_handle_brainstorm,
1111
- ),
1112
- ToolSpec(
1113
- name="iumbtems_darkharvest",
1114
- description="Product competitor teardown: seed inspirations plus prompt, expand to adjacents, emit per-feature depend/vendor/clean-room/skip verdicts with SPDX attribution. Permissive-only vendoring; GPL/AGPL spec-rebuild only.",
1115
- input_schema={
1116
- "type": "object",
1117
- "properties": {
1118
- "objective": {
1119
- "type": "string",
1120
- "description": "Product arena to tear down (e.g. Paseo-class agent harness competitor)",
1121
- },
1122
- "seeds": {
1123
- "type": "array",
1124
- "items": {"type": "string"},
1125
- "description": "Seed inspiration repo URLs",
1126
- },
1127
- "maxRepos": {
1128
- "type": "integer",
1129
- "description": "Cap on total competitors (default 10)",
1130
- },
1131
- "depth": {"type": "integer", "description": "Dialectic depth 1-4"},
1132
- "base_dir": {"type": "string"},
1133
- "frontier_file": {
1134
- "type": "string",
1135
- "description": "Settled frontier.json from /grilling",
1136
- },
1137
- "mock_claude": {"type": "boolean"},
1138
- "backend": {
1139
- "type": "string",
1140
- "enum": ["auto", "claude", "opencode"],
1141
- },
1142
- },
1143
- "required": ["objective"],
1144
- },
1145
- handler=_handle_darkharvest,
1146
- ),
1147
- ToolSpec(
1148
- name="iumbtems_factory",
1149
- description="Drive factory run state: init / phase-add / qa-record / expansion / stop / gate. State goes to <project>/.factory and <project>/.roadmap; no filesystem path to helper scripts required.",
1150
- input_schema={
1151
- "type": "object",
1152
- "properties": {
1153
- "command": {
1154
- "type": "string",
1155
- "enum": [
1156
- "init",
1157
- "phase-add",
1158
- "qa-record",
1159
- "expansion",
1160
- "stop",
1161
- "gate",
1162
- ],
1163
- },
1164
- "action": {
1165
- "type": "string",
1166
- "enum": [
1167
- "open",
1168
- "settle",
1169
- "approve",
1170
- "waive",
1171
- "escalate",
1172
- "count",
1173
- ],
1174
- "description": "Gate action (with command: gate)",
1175
- },
1176
- "run": {"type": "string", "description": "Factory run name"},
1177
- "phase": {
1178
- "type": "string",
1179
- "description": "Phase id (e.g. 01-auth)",
1180
- },
1181
- "goal": {"type": "string"},
1182
- "accept": {
1183
- "type": "string",
1184
- "description": "Semicolon-separated acceptance criteria",
1185
- },
1186
- "seat": {"type": "string", "description": "QA seat (qa-a | qa-b)"},
1187
- "verdict": {
1188
- "type": "string",
1189
- "enum": ["pass", "fail", "conditional"],
1190
- },
1191
- "reason": {"type": "string"},
1192
- "loops": {
1193
- "type": "integer",
1194
- "description": "Expansion loop count (1-10)",
1195
- },
1196
- "max_loops": {"type": "integer"},
1197
- "project_dir": {
1198
- "type": "string",
1199
- "description": "Project root (default: IUMBTEMS_PROJECT_DIR or cwd)",
1200
- },
1201
- },
1202
- "required": ["command"],
1203
- },
1204
- handler=_handle_factory,
1205
- ),
1206
- ToolSpec(
1207
- name="iumbtems_verify_quote",
1208
- description="Audit a verbatim citation against the SHA-256 source cache (.research/sources/<hash>.md).",
1209
- input_schema={
1210
- "type": "object",
1211
- "properties": {
1212
- "hash": {
1213
- "type": "string",
1214
- "description": "Content-addressed SHA-256 of the cached source",
1215
- },
1216
- "quote": {
1217
- "type": "string",
1218
- "description": "Verbatim quote to verify",
1219
- },
1220
- "base_dir": {"type": "string"},
1221
- },
1222
- "required": ["hash", "quote"],
1223
- },
1224
- handler=_handle_verify_quote,
1225
- ),
1226
- ToolSpec(
1227
- name="iumbtems_reindex_claims",
1228
- description="Rebuild the derived claims.sqlite index from .research flat files (idempotent; flat files are source of truth).",
1229
- input_schema={
1230
- "type": "object",
1231
- "properties": {
1232
- "base_dir": {
1233
- "type": "string",
1234
- "description": "Path to .research workspace",
1235
- },
1236
- },
1237
- },
1238
- handler=_handle_reindex_claims,
1239
- ),
1240
- ToolSpec(
1241
- name="iumbtems_report_retraction",
1242
- description="Record a RETRACTED/REVISED event for a cached source; dependent VERIFIED claims degrade to STALE/SUSPECT on the next audit (Living Dossiers).",
1243
- input_schema={
1244
- "type": "object",
1245
- "properties": {
1246
- "hash": {
1247
- "type": "string",
1248
- "description": "Content-addressed SHA-256 of the affected source",
1249
- },
1250
- "event": {"type": "string", "enum": ["RETRACTED", "REVISED"]},
1251
- "note": {
1252
- "type": "string",
1253
- "description": "Why the source was retracted/revised",
1254
- },
1255
- "base_dir": {"type": "string"},
1256
- },
1257
- "required": ["hash", "event"],
1258
- },
1259
- handler=_handle_report_retraction,
1260
- ),
1261
- ToolSpec(
1262
- name="iumbtems_check_staleness",
1263
- description="Run one claim-degradation pass: join claims against retraction events, write the status ledger, and queue re-runs for degraded scopes. Never mutates dossiers.",
1264
- input_schema={
1265
- "type": "object",
1266
- "properties": {
1267
- "base_dir": {"type": "string"},
1268
- "scope_ids": {
1269
- "type": "array",
1270
- "items": {"type": "string"},
1271
- "description": "Limit the pass to specific scopes",
1272
- },
1273
- },
1274
- },
1275
- handler=_handle_check_staleness,
1276
- ),
1277
- ToolSpec(
1278
- name="iumbtems_set_domain_pack",
1279
- description="Activate a Regulated Domain Pack (biopharma/quant/legal epistemic constitution) for subsequent audits.",
1280
- input_schema={
1281
- "type": "object",
1282
- "properties": {
1283
- "pack": {
1284
- "type": "string",
1285
- "description": "Pack id: biopharma | quant | legal (or a path)",
1286
- },
1287
- "base_dir": {"type": "string"},
1288
- },
1289
- "required": ["pack"],
1290
- },
1291
- handler=_handle_set_domain_pack,
1292
- ),
1293
- ToolSpec(
1294
- name="iumbtems_export_brief",
1295
- description="Export a proof-carrying research brief (PCRB): self-contained signed bundle of synthesis, claims, quote witnesses, and full source texts.",
1296
- input_schema={
1297
- "type": "object",
1298
- "properties": {
1299
- "base_dir": {"type": "string"},
1300
- "out": {
1301
- "type": "string",
1302
- "description": "Output path (default <base_dir>/brief.pcrb.json)",
1303
- },
1304
- "objective": {"type": "string"},
1305
- "key": {
1306
- "type": "string",
1307
- "description": "HMAC key (prefer key_file or env IUMBTEMS_PCRB_KEY)",
1308
- },
1309
- "key_file": {"type": "string"},
1310
- },
1311
- },
1312
- handler=_handle_export_brief,
1313
- ),
1314
- ToolSpec(
1315
- name="iumbtems_verify_brief",
1316
- description="Verify a proof-carrying brief against its BUNDLED sources: manifest integrity, HMAC signature, and every quote re-checked. Exit-fail semantics; no trust in the producing LLM.",
1317
- input_schema={
1318
- "type": "object",
1319
- "properties": {
1320
- "brief": {
1321
- "type": "string",
1322
- "description": "Path to brief.pcrb.json",
1323
- },
1324
- "key": {"type": "string"},
1325
- "key_file": {"type": "string"},
1326
- },
1327
- "required": ["brief"],
1328
- },
1329
- handler=_handle_verify_brief,
1330
- ),
1331
- ToolSpec(
1332
- name="iumbtems_socratic_frontier",
1333
- description="Inspect or advance the Socratic decision-tree frontier (.research/frontier.json).",
1334
- input_schema={
1335
- "type": "object",
1336
- "properties": {
1337
- "file": {"type": "string", "description": "Path to frontier.json"},
1338
- "objective": {"type": "string"},
1339
- "action": {
1340
- "type": "string",
1341
- "enum": ["inspect", "add", "settle", "export"],
1342
- "description": "inspect (default) | add a node | settle a node | export the file",
1343
- },
1344
- "node": {
1345
- "type": "object",
1346
- "description": "Node to add: {id, question, title?, recommended?, depends_on?[]}",
1347
- },
1348
- "settle": {
1349
- "type": "array",
1350
- "items": {"type": "string"},
1351
- "description": "Two-element [node_id, answer] to settle a frontier node",
1352
- },
1353
- },
1354
- },
1355
- handler=_handle_socratic_frontier,
1356
- ),
1357
- ToolSpec(
1358
- name="iumbtems_doctor",
1359
- description="Preflight health check for the resolved workspace: plugin version, backend binary, search-engine reachability, and workspace writability. Run this first when anything looks wrong.",
1360
- input_schema={
1361
- "type": "object",
1362
- "properties": {
1363
- "base_dir": {"type": "string"},
1364
- "mode": {"type": "string"},
1365
- "probe": {
1366
- "type": "boolean",
1367
- "description": "Run a live 1-query search probe (default true)",
1368
- },
1369
- },
1370
- },
1371
- handler=_handle_doctor,
1372
- ),
1373
- ToolSpec(
1374
- name="iumbtems_test",
1375
- description="Run the packaged IUMBTEMS test suite (subprocess, timeout-bounded) and report pass/fail with the tail of the output.",
1376
- input_schema={"type": "object", "properties": {}},
1377
- handler=_handle_test,
1378
- ),
1379
- ]
1380
-
1381
-
1382
- def build_server() -> StdioJsonRpcServer:
1383
- return StdioJsonRpcServer(
1384
- server_name=SERVER_NAME,
1385
- version=SERVER_VERSION,
1386
- tools=build_tools(),
1387
- protocol_version=PROTOCOL_VERSION,
1388
- log_prefix="[iumbtems-mcp]",
1389
- )
1390
-
1391
-
1392
- # ---------------------------------------------------------------------------
1393
- # entry points
1394
- # ---------------------------------------------------------------------------
1395
-
1396
-
1397
- def call_once(tool_name: str, arguments: Dict[str, Any]) -> int:
1398
- """One-shot dispatch: run a single tool and print its text payload."""
1399
- server = build_server()
1400
- spec = server._by_name.get(tool_name)
1401
- if spec is None:
1402
- json.dump(
1403
- {
1404
- "error": f"Tool {tool_name} not found",
1405
- "available": [t.name for t in server.tools],
1406
- },
1407
- sys.stdout,
1408
- indent=2,
1409
- )
1410
- sys.stdout.write("\n")
1411
- return 1
1412
- try:
1413
- text = spec.handler(arguments)
1414
- except Exception as exc: # noqa: BLE001 - surface as a JSON error payload
1415
- json.dump({"error": f"{type(exc).__name__}: {exc}"}, sys.stdout, indent=2)
1416
- sys.stdout.write("\n")
1417
- return 1
1418
- sys.stdout.write(text if text.endswith("\n") else text + "\n")
1419
- return 0
1420
-
1421
-
1422
- def main(argv: Optional[List[str]] = None) -> int:
1423
- parser = argparse.ArgumentParser(description="IUMBTEMS MCP server")
1424
- sub = parser.add_subparsers(dest="command")
1425
-
1426
- call_p = sub.add_parser("call", help="One-shot tool dispatch (JSON out)")
1427
- call_p.add_argument("tool", help="Tool name, e.g. iumbtems_verify_quote")
1428
- call_p.add_argument("arguments", help="JSON object of tool arguments")
1429
-
1430
- sub.add_parser("serve", help="Run stdio MCP server (default)")
1431
-
1432
- args = parser.parse_args(argv)
1433
-
1434
- if args.command == "call":
1435
- try:
1436
- arguments = json.loads(args.arguments) if args.arguments else {}
1437
- except json.JSONDecodeError as exc:
1438
- print(json.dumps({"error": f"arguments must be valid JSON: {exc}"}))
1439
- return 1
1440
- return call_once(args.tool, arguments)
1441
-
1442
- build_server().serve_forever()
1443
- return 0
1444
-
1445
-
1446
- if __name__ == "__main__":
1447
- sys.exit(main())