@heretek-ai/epistemic-swarm 0.7.25 → 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,879 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Canonical CONFIG schema + validated single-writer persistence (phase 01).
3
-
4
- Covers the phase-01 acceptance criteria:
5
-
6
- * A1 — `schemas/config.schema.json` is generated from `runner/schemas.py`, the
7
- default config validates, and the shared fixture corpus (rejecting enum,
8
- type, range, minLength and pattern violations) yields the expected verdicts.
9
- * A2 — crash-safety (a failure between temp-write and rename leaves the
10
- original byte-identical) and lock contention (two concurrent writers never
11
- tear the file or lose each other's keys). NOTE: the full `discover` run is
12
- blocked in this sandbox by pre-existing failures/hangs in
13
- `test_search_provider` (`TestRunPathFailFast`/`TestMcpRunStatus`), identical
14
- on pristine HEAD; phase-owned suites are the verification bar.
15
- * A3 — `expected_hash` mismatch is rejected with a structured error and no
16
- write; a match (full hash or ≥8-hex prefix) proceeds.
17
- * A4 — MCP `iumbtems_config` accepts every canonical key, rejects invalid
18
- values before writing, and its advertised input schema matches the GENERATED
19
- artifact `schemas/config.schema.json` (R2; not the deepcopy source).
20
- * A5 — honest field wiring: `verify.min_fuzzy_confidence` changes verification
21
- through the MCP surface; `divergence_threshold` is advisory and emitted only
22
- for non-default values (R3); `output_dir` is deprecated and inert.
23
- * A6 — legacy pin migration is covered in `test_swarm_config.py` (unchanged),
24
- including R1's refusal to persist schema-rejected files.
25
-
26
- Rework (retry 1) regressions: R2/R3/R5/R6/R7/R8 live here; R1/R4 also have
27
- writer-level regressions in `test_swarm_config.py`.
28
- """
29
-
30
- import io
31
- import json
32
- import math
33
- import os
34
- import re
35
- import sys
36
- import tempfile
37
- import unittest
38
- from contextlib import redirect_stdout
39
- from pathlib import Path
40
-
41
- PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
42
- if str(PROJECT_ROOT) not in sys.path:
43
- sys.path.insert(0, str(PROJECT_ROOT))
44
-
45
- from runner.mcp_server import CONFIG_TOOL_CONTROL_ARGS # noqa: E402
46
- from runner.schemas import CONFIG, CONFIG_DEFAULTS # noqa: E402
47
- from runner.schema_validate import validate # noqa: E402
48
- from skills.research_cache.hasher import SourceHasher # noqa: E402
49
- from skills.swarm_config.configure import ( # noqa: E402
50
- DEFAULT_CONFIG,
51
- config_hash,
52
- load_config,
53
- validate_config,
54
- )
55
-
56
- FIXTURES = Path(__file__).resolve().parent / "fixtures"
57
- VALIDATION_CASES = json.loads(
58
- (FIXTURES / "config_validation_cases.json").read_text(encoding="utf-8")
59
- )["cases"]
60
-
61
- CONTROL_ARGS = set(CONFIG_TOOL_CONTROL_ARGS)
62
-
63
- # H1: every keyword the parity comparator deep-compares at each schema node.
64
- # `items` / `additionalProperties` recurse as schema nodes; `default` compares
65
- # by value (deep: dict/list defaults compare element-wise via !=).
66
- COMPARE_KEYWORDS = (
67
- "type",
68
- "enum",
69
- "minimum",
70
- "maximum",
71
- "minLength",
72
- "pattern",
73
- "default",
74
- )
75
-
76
- # One representative valid value per canonical key; the test asserts this map
77
- # covers the schema exactly, so a new key cannot slip in without a test.
78
- VALID_UPDATES = {
79
- "search_engine": "brave",
80
- "max_iterations": 3,
81
- "divergence_threshold": 0.5,
82
- "mode": "audit",
83
- "backend": "opencode",
84
- "cache_raw_markdown": False,
85
- "cache_ttl_days": 14,
86
- "search_timeout_s": 3.5,
87
- "searxng_url": "http://localhost:8080",
88
- "license_whitelist": ["MIT"],
89
- "output_dir": "/tmp/renamed-workspace",
90
- "allocation": "auction",
91
- "domain_pack": "biopharma",
92
- "verify": {"min_fuzzy_confidence": 0.9},
93
- "agents": {
94
- "alpha": {
95
- "backend": ["claude", "-p"],
96
- "model": "example/model",
97
- "opencode_agent": "brainstormer",
98
- }
99
- },
100
- "opencode_auto": False,
101
- "opencode_agent": "darkharvester",
102
- "mcp_servers": {"searxng": False, "firecrawl": True},
103
- }
104
-
105
- # One clearly invalid value per canonical key.
106
- INVALID_UPDATES = {
107
- "search_engine": "google",
108
- "max_iterations": 9,
109
- "divergence_threshold": "high",
110
- "mode": "swarm",
111
- "backend": "gemini",
112
- "cache_raw_markdown": "yes",
113
- "cache_ttl_days": -1,
114
- "search_timeout_s": 0,
115
- "searxng_url": 42,
116
- "license_whitelist": "MIT",
117
- "output_dir": 42,
118
- "allocation": "lottery",
119
- "domain_pack": 7,
120
- "verify": {"min_fuzzy_confidence": 2.0},
121
- "agents": "claude -p",
122
- "opencode_auto": "yes",
123
- "opencode_agent": 5,
124
- "mcp_servers": "enabled",
125
- }
126
-
127
-
128
- def _write_config(base: Path, payload: dict) -> Path:
129
- base.mkdir(parents=True, exist_ok=True)
130
- cfg_file = base / "config.json"
131
- cfg_file.write_text(json.dumps(payload, indent=2), encoding="utf-8")
132
- return cfg_file
133
-
134
-
135
- class TestCanonicalSchema(unittest.TestCase):
136
- def test_generated_config_schema_in_sync(self):
137
- from scripts.gen_schemas import check
138
-
139
- self.assertEqual(check(), 0)
140
-
141
- def test_default_config_matches_canonical_defaults(self):
142
- self.assertEqual(DEFAULT_CONFIG, CONFIG_DEFAULTS)
143
- self.assertEqual(validate(CONFIG_DEFAULTS, CONFIG), [])
144
- self.assertEqual(validate_config(DEFAULT_CONFIG), [])
145
-
146
- def test_every_default_is_declared_in_the_schema(self):
147
- self.assertEqual(set(CONFIG_DEFAULTS), set(CONFIG["properties"]))
148
- for key, value in CONFIG_DEFAULTS.items():
149
- self.assertEqual(CONFIG["properties"][key].get("default"), value, key)
150
-
151
- def test_per_key_annotations_present(self):
152
- scopes = {"run", "retrieval", "dispatch", "audit", "cache", "layout"}
153
- restarts = {"next-run", "none"}
154
- for key, subschema in CONFIG["properties"].items():
155
- self.assertIsInstance(subschema.get("x-label"), str, key)
156
- self.assertIn(subschema.get("x-scope"), scopes, key)
157
- self.assertIn(subschema.get("x-restart"), restarts, key)
158
- self.assertIsInstance(subschema.get("description"), str, key)
159
-
160
- def test_output_dir_is_flagged_deprecated(self):
161
- self.assertTrue(CONFIG["properties"]["output_dir"].get("deprecated"))
162
- self.assertEqual(CONFIG["properties"]["output_dir"]["x-restart"], "none")
163
-
164
- def test_fixture_corpus_verdicts(self):
165
- for case in VALIDATION_CASES:
166
- with self.subTest(case=case["name"]):
167
- problems = validate(case["config"], CONFIG)
168
- self.assertEqual(
169
- not problems,
170
- case["valid"],
171
- f"{case['name']}: {problems}",
172
- )
173
- if not case["valid"] and case.get("expect"):
174
- self.assertTrue(
175
- any(case["expect"] in p for p in problems),
176
- f"{case['name']}: {case['expect']!r} not in {problems}",
177
- )
178
-
179
- def test_non_finite_numbers_are_rejected(self):
180
- # Python's json accepts bare NaN/Infinity; neither can satisfy a bounded
181
- # contract, and NaN would slip past ordinary comparisons.
182
- for bad in (math.nan, math.inf, -math.inf):
183
- with self.subTest(value=bad):
184
- problems = validate(
185
- {**CONFIG_DEFAULTS, "divergence_threshold": bad}, CONFIG
186
- )
187
- self.assertTrue(problems)
188
-
189
- def test_searxng_url_and_domain_pack_are_constrained(self):
190
- """R7 choice: constrain (non-empty pack id; http(s) scheme for SearXNG)."""
191
- for bad in ("localhost:8080", "ftp://host/", " "):
192
- with self.subTest(value=bad):
193
- self.assertTrue(validate({"searxng_url": bad}, CONFIG))
194
- self.assertTrue(validate({"domain_pack": ""}, CONFIG))
195
- self.assertEqual(
196
- validate({"searxng_url": "https://searx.internal:8080"}, CONFIG), []
197
- )
198
- self.assertEqual(validate({"domain_pack": "biopharma"}, CONFIG), [])
199
- self.assertEqual(
200
- validate({"searxng_url": None, "domain_pack": None}, CONFIG), []
201
- )
202
-
203
-
204
- class TestMcpConfigSurface(unittest.TestCase):
205
- """`iumbtems_config` accepts the full canonical key set, rejects garbage."""
206
-
207
- def _call(self, args):
208
- from runner.mcp_server import _handle_config
209
-
210
- return json.loads(_handle_config(args))
211
-
212
- def test_advertised_schema_parity_with_generated_artifact(self):
213
- """R2: compare against the GENERATED artifact, never the deepcopy source."""
214
- from runner.mcp_server import build_tools, config_tool_input_schema
215
-
216
- artifact_path = PROJECT_ROOT / "schemas" / "config.schema.json"
217
- artifact = json.loads(artifact_path.read_text(encoding="utf-8"))
218
- advertised = config_tool_input_schema()
219
- spec = next(t for t in build_tools() if t.name == "iumbtems_config")
220
- self.assertEqual(spec.input_schema, advertised)
221
-
222
- problems = []
223
- self._compare_properties(artifact, advertised, "", problems)
224
- self.assertEqual(problems, [])
225
- advertised_keys = set(advertised["properties"]) - CONTROL_ARGS
226
- self.assertEqual(advertised_keys, set(artifact["properties"]))
227
- # The artifact is the generated mirror of the canonical module.
228
- self.assertEqual(set(artifact["properties"]), set(CONFIG["properties"]))
229
- # R4: both spellings are advertised and documented.
230
- self.assertIn("expected_hash", advertised["properties"])
231
- self.assertIn("expectedHash", advertised["properties"])
232
- self.assertIn(
233
- "expected_hash",
234
- advertised["properties"]["expectedHash"]["description"],
235
- )
236
-
237
- def _compare_schema_node(self, sub, advertised_node, path, problems):
238
- """Deep-compare one schema node, recursing into subschemas.
239
-
240
- H1: besides the scalar keywords, `properties` recurses per key,
241
- `items` recurses as `{path}[]`, and `additionalProperties` recurses
242
- as `{path}.*`. A canonical subschema missing on the advertised side
243
- is a drift report, never a silent skip.
244
- """
245
- for keyword in COMPARE_KEYWORDS:
246
- if sub.get(keyword) != advertised_node.get(keyword):
247
- problems.append(
248
- f"{path}: {keyword} {sub.get(keyword)!r} != "
249
- f"{advertised_node.get(keyword)!r}"
250
- )
251
- if isinstance(sub.get("properties"), dict):
252
- if not isinstance(advertised_node.get("properties"), dict):
253
- problems.append(f"{path}: properties schema missing on advertised side")
254
- else:
255
- self._compare_properties(sub, advertised_node, f"{path}.", problems)
256
- for keyword, suffix in (("items", "[]"), ("additionalProperties", ".*")):
257
- sub_node = sub.get(keyword)
258
- if isinstance(sub_node, dict):
259
- adv_node = advertised_node.get(keyword)
260
- if not isinstance(adv_node, dict):
261
- problems.append(
262
- f"{path}{suffix}: {keyword} schema missing on advertised side"
263
- )
264
- else:
265
- self._compare_schema_node(
266
- sub_node, adv_node, f"{path}{suffix}", problems
267
- )
268
-
269
- def _compare_properties(self, canonical, advertised, path, problems):
270
- for key, sub in (canonical.get("properties") or {}).items():
271
- adv = (advertised.get("properties") or {}).get(key)
272
- if adv is None:
273
- problems.append(f"missing advertised key: {path}{key}")
274
- continue
275
- self._compare_schema_node(sub, adv, f"{path}{key}", problems)
276
-
277
- def test_parity_comparator_is_not_vacuous(self):
278
- """R2: the comparator must bite when the advertised surface drifts."""
279
- from runner.mcp_server import config_tool_input_schema
280
-
281
- artifact = json.loads(
282
- (PROJECT_ROOT / "schemas" / "config.schema.json").read_text(
283
- encoding="utf-8"
284
- )
285
- )
286
- dropped = config_tool_input_schema()
287
- dropped["properties"].pop("verify")
288
- problems = []
289
- self._compare_properties(artifact, dropped, "", problems)
290
- self.assertTrue(any("missing advertised key" in p for p in problems))
291
-
292
- drifted = config_tool_input_schema()
293
- drifted["properties"]["searxng_url"].pop("pattern")
294
- problems = []
295
- self._compare_properties(artifact, drifted, "", problems)
296
- self.assertTrue(any("searxng_url: pattern" in p for p in problems))
297
-
298
- def test_parity_comparator_catches_items_additionalProperties_default_drift(self):
299
- """H1: each newly walked keyword bites when the advertised side drifts.
300
-
301
- Every sub-case mutates a FRESH advertised schema exactly once, so a
302
- green sub-case proves its own keyword is compared (never vacuous via
303
- a shared broken fixture).
304
- """
305
- from runner.mcp_server import config_tool_input_schema
306
-
307
- artifact = json.loads(
308
- (PROJECT_ROOT / "schemas" / "config.schema.json").read_text(
309
- encoding="utf-8"
310
- )
311
- )
312
-
313
- def complained_of(mutator, needle):
314
- advertised = config_tool_input_schema()
315
- mutator(advertised["properties"])
316
- problems = []
317
- self._compare_properties(artifact, advertised, "", problems)
318
- self.assertTrue(
319
- any(needle in p for p in problems),
320
- f"{needle!r} not in {problems}",
321
- )
322
-
323
- # `items` drift: top-level (license_whitelist) and nested
324
- # (agents.alpha.backend); then a dropped `items` subschema.
325
- complained_of(
326
- lambda props: props["license_whitelist"].__setitem__(
327
- "items", {"type": "integer"}
328
- ),
329
- "license_whitelist[]: type",
330
- )
331
- complained_of(
332
- lambda props: props["agents"]["properties"]["alpha"]["properties"][
333
- "backend"
334
- ].__setitem__("items", {"type": "integer"}),
335
- "agents.alpha.backend[]: type",
336
- )
337
- complained_of(
338
- lambda props: props["license_whitelist"].pop("items"),
339
- "license_whitelist[]: items schema missing",
340
- )
341
-
342
- # `additionalProperties` drift (mcp_servers) and a dropped subschema.
343
- complained_of(
344
- lambda props: props["mcp_servers"].__setitem__(
345
- "additionalProperties", {"type": "string"}
346
- ),
347
- "mcp_servers.*: type",
348
- )
349
- complained_of(
350
- lambda props: props["mcp_servers"].pop("additionalProperties"),
351
- "mcp_servers.*: additionalProperties schema missing",
352
- )
353
-
354
- # `default` drift: top-level (mode) and nested
355
- # (verify.min_fuzzy_confidence); then a dropped `default`.
356
- complained_of(
357
- lambda props: props["mode"].__setitem__("default", "scout"),
358
- "mode: default",
359
- )
360
- complained_of(
361
- lambda props: props["verify"]["properties"][
362
- "min_fuzzy_confidence"
363
- ].__setitem__("default", 0.5),
364
- "verify.min_fuzzy_confidence: default",
365
- )
366
- complained_of(
367
- lambda props: props["mode"].pop("default"),
368
- "mode: default",
369
- )
370
-
371
- def test_valid_update_map_covers_every_canonical_key(self):
372
- self.assertEqual(set(VALID_UPDATES), set(CONFIG["properties"]))
373
- self.assertEqual(set(INVALID_UPDATES), set(CONFIG["properties"]))
374
-
375
- def test_every_canonical_key_is_accepted_and_persisted(self):
376
- for key, value in VALID_UPDATES.items():
377
- with self.subTest(key=key):
378
- with tempfile.TemporaryDirectory() as tmp:
379
- payload = self._call({key: value, "base_dir": tmp})
380
- self.assertEqual(payload["status"], "updated", payload)
381
- on_disk = load_config(tmp)
382
- if key == "agents":
383
- # Deep-merge: the provided role wins, defaults survive.
384
- self.assertEqual(
385
- on_disk["agents"]["alpha"]["model"], "example/model"
386
- )
387
- else:
388
- self.assertEqual(on_disk[key], value, key)
389
-
390
- def test_invalid_values_rejected_before_write(self):
391
- for key, value in INVALID_UPDATES.items():
392
- with self.subTest(key=key):
393
- with tempfile.TemporaryDirectory() as tmp:
394
- payload = self._call({key: value, "base_dir": tmp})
395
- self.assertEqual(payload["status"], "error", payload)
396
- self.assertEqual(payload["code"], "CONFIG_VALIDATION_FAILED")
397
- self.assertFalse(payload["written"])
398
- self.assertTrue(payload["errors"])
399
- self.assertFalse((Path(tmp) / "config.json").exists())
400
-
401
- def test_unknown_key_rejected_with_accepted_list(self):
402
- with tempfile.TemporaryDirectory() as tmp:
403
- payload = self._call({"not_a_key": 1, "base_dir": tmp})
404
- self.assertEqual(payload["status"], "error")
405
- self.assertEqual(payload["code"], "UNKNOWN_CONFIG_KEY")
406
- self.assertFalse(payload["written"])
407
- self.assertIn("not_a_key", payload["errors"][0])
408
- self.assertEqual(set(payload["accepted_keys"]), set(CONFIG["properties"]))
409
- self.assertFalse((Path(tmp) / "config.json").exists())
410
-
411
- def test_legacy_aliases_still_work(self):
412
- with tempfile.TemporaryDirectory() as tmp:
413
- payload = self._call({"engine": "firecrawl", "depth": 4, "base_dir": tmp})
414
- self.assertEqual(payload["status"], "updated")
415
- on_disk = load_config(tmp)
416
- self.assertEqual(on_disk["search_engine"], "firecrawl")
417
- self.assertEqual(on_disk["max_iterations"], 4)
418
-
419
- def test_show_path_is_unchanged_and_writes_nothing(self):
420
- with tempfile.TemporaryDirectory() as tmp:
421
- payload = self._call({"base_dir": tmp})
422
- self.assertEqual(payload["status"], "current")
423
- self.assertFalse(payload["migrated"])
424
- self.assertEqual(payload["config"], load_config(tmp))
425
- self.assertFalse((Path(tmp) / "config.json").exists())
426
- shown = self._call({"base_dir": tmp, "show": True})
427
- self.assertEqual(shown["status"], "current")
428
- # R11: the legacy-pin case must NOT write (the old test was vacuous
429
- # here). Seed a pin, snapshot the bytes, and prove the show leg
430
- # leaves them byte-identical while still reporting the migration.
431
- cfg_file = _write_config(
432
- Path(tmp), {"agents": {"alpha": {"backend": ["claude", "-p"]}}}
433
- )
434
- before = cfg_file.read_bytes()
435
- pinned = self._call({"base_dir": tmp})
436
- self.assertEqual(pinned["status"], "current")
437
- self.assertTrue(pinned["migrated"])
438
- self.assertEqual(cfg_file.read_bytes(), before)
439
-
440
- def test_write_leg_heals_legacy_pin_and_persists(self):
441
- # R11 preserves phase-01 A6: a write still migrates the legacy pin on disk.
442
- with tempfile.TemporaryDirectory() as tmp:
443
- base = Path(tmp)
444
- cfg_file = _write_config(
445
- base, {"agents": {"alpha": {"backend": ["claude", "-p"]}}}
446
- )
447
- payload = self._call({"max_iterations": 2, "base_dir": tmp})
448
- self.assertEqual(payload["status"], "updated", payload)
449
- healed = json.loads(cfg_file.read_text(encoding="utf-8"))
450
- self.assertIsNone(healed["agents"]["alpha"]["backend"])
451
- self.assertEqual(healed["max_iterations"], 2)
452
-
453
- def test_guarded_write_on_legacy_pin_succeeds_and_heals(self):
454
- # F1/R12: the wizard always sends `expected_hash`. With no heal-before-
455
- # save, the guard still matches so the write is `updated` (never a false
456
- # `stale`) and the pin is migrated on disk.
457
- with tempfile.TemporaryDirectory() as tmp:
458
- base = Path(tmp)
459
- cfg_file = _write_config(
460
- base, {"agents": {"alpha": {"backend": ["claude", "-p"]}}}
461
- )
462
- start_hash = config_hash(tmp)
463
- payload = self._call(
464
- {"max_iterations": 3, "expected_hash": start_hash, "base_dir": tmp}
465
- )
466
- self.assertEqual(payload["status"], "updated", payload)
467
- healed = json.loads(cfg_file.read_text(encoding="utf-8"))
468
- self.assertIsNone(healed["agents"]["alpha"]["backend"])
469
- self.assertEqual(healed["max_iterations"], 3)
470
-
471
- def test_invalid_pin_file_show_reports_no_migration_and_write_blocked(self):
472
- # R12/R11: an invalid file is never healed, and a write that would leave
473
- # it invalid is rejected without touching the bytes.
474
- with tempfile.TemporaryDirectory() as tmp:
475
- base = Path(tmp)
476
- cfg_file = _write_config(
477
- base,
478
- {
479
- "max_iterations": 99,
480
- "agents": {"alpha": {"backend": ["claude", "-p"]}},
481
- },
482
- )
483
- before = cfg_file.read_bytes()
484
- shown = self._call({"base_dir": tmp})
485
- self.assertEqual(shown["status"], "current")
486
- self.assertFalse(shown["migrated"])
487
- self.assertEqual(cfg_file.read_bytes(), before)
488
- blocked = self._call({"mode": "audit", "base_dir": tmp})
489
- self.assertEqual(blocked["status"], "error")
490
- self.assertEqual(blocked["code"], "CONFIG_VALIDATION_FAILED")
491
- self.assertFalse(blocked["written"])
492
- self.assertEqual(cfg_file.read_bytes(), before)
493
-
494
- def test_null_clears_a_nullable_key(self):
495
- with tempfile.TemporaryDirectory() as tmp:
496
- self._call({"searxng_url": "http://localhost:8080", "base_dir": tmp})
497
- payload = self._call({"searxng_url": None, "base_dir": tmp})
498
- self.assertEqual(payload["status"], "updated")
499
- self.assertIsNone(load_config(tmp)["searxng_url"])
500
-
501
- def test_searxng_url_scheme_and_domain_pack_nonempty_enforced(self):
502
- """R7 constraints are enforced before any write at the MCP surface."""
503
- with tempfile.TemporaryDirectory() as tmp:
504
- payload = self._call({"searxng_url": "localhost:8080", "base_dir": tmp})
505
- self.assertEqual(payload["status"], "error")
506
- self.assertEqual(payload["code"], "CONFIG_VALIDATION_FAILED")
507
- payload = self._call({"domain_pack": "", "base_dir": tmp})
508
- self.assertEqual(payload["status"], "error")
509
- self.assertEqual(payload["code"], "CONFIG_VALIDATION_FAILED")
510
- self.assertFalse((Path(tmp) / "config.json").exists())
511
-
512
- def test_stale_expected_hash_returns_snapshot_without_write(self):
513
- with tempfile.TemporaryDirectory() as tmp:
514
- updated = self._call({"mode": "scout", "base_dir": tmp})
515
- stale = updated["hash"]
516
- self.assertEqual(len(stale), 64)
517
- # External writer lands a newer version.
518
- _write_config(Path(tmp), {"mode": "audit"})
519
- before = (Path(tmp) / "config.json").read_bytes()
520
- payload = self._call(
521
- {"mode": "research", "expected_hash": stale, "base_dir": tmp}
522
- )
523
- self.assertEqual(payload["status"], "stale")
524
- self.assertFalse(payload["written"])
525
- self.assertEqual(payload["expected_hash"], stale)
526
- self.assertEqual(payload["current_hash"], config_hash(tmp))
527
- self.assertEqual(payload["config"]["mode"], "audit")
528
- # R5: no absolute workspace path in the stale payload.
529
- self.assertNotIn("path", payload)
530
- self.assertEqual((Path(tmp) / "config.json").read_bytes(), before)
531
-
532
- def test_conflicting_expected_hash_spellings_are_rejected(self):
533
- """R4: never silently prefer one spelling over the other."""
534
- with tempfile.TemporaryDirectory() as tmp:
535
- created = self._call({"mode": "scout", "base_dir": tmp})
536
- live = created["hash"]
537
- before = (Path(tmp) / "config.json").read_bytes()
538
- payload = self._call(
539
- {
540
- "mode": "research",
541
- "expected_hash": live,
542
- "expectedHash": "0" * 8,
543
- "base_dir": tmp,
544
- }
545
- )
546
- self.assertEqual(payload["status"], "error")
547
- self.assertEqual(payload["code"], "CONFLICTING_EXPECTED_HASH")
548
- self.assertFalse(payload["written"])
549
- self.assertEqual((Path(tmp) / "config.json").read_bytes(), before)
550
- # Equal spellings are accepted and apply the guard.
551
- ok = self._call(
552
- {
553
- "mode": "research",
554
- "expected_hash": live,
555
- "expectedHash": live,
556
- "base_dir": tmp,
557
- }
558
- )
559
- self.assertEqual(ok["status"], "updated")
560
-
561
- def test_four_hex_expected_hash_is_rejected(self):
562
- """R4: minimum guard prefix is 8 hex chars."""
563
- with tempfile.TemporaryDirectory() as tmp:
564
- created = self._call({"mode": "scout", "base_dir": tmp})
565
- four_hex = created["hash"][:4]
566
- before = (Path(tmp) / "config.json").read_bytes()
567
- payload = self._call(
568
- {"mode": "research", "expected_hash": four_hex, "base_dir": tmp}
569
- )
570
- self.assertEqual(payload["status"], "error")
571
- self.assertEqual(payload["code"], "CONFIG_INVALID_EXPECTED_HASH")
572
- self.assertFalse(payload["written"])
573
- self.assertEqual((Path(tmp) / "config.json").read_bytes(), before)
574
-
575
- def test_null_for_non_nullable_keys_is_rejected(self):
576
- """R8: null is an explicit error, never a masked no-op."""
577
- for key in ("search_engine", "max_iterations", "agents", "verify"):
578
- with self.subTest(key=key):
579
- with tempfile.TemporaryDirectory() as tmp:
580
- payload = self._call({key: None, "base_dir": tmp})
581
- self.assertEqual(payload["status"], "error", payload)
582
- self.assertEqual(payload["code"], "NULL_FOR_NON_NULLABLE_KEY")
583
- self.assertFalse(payload["written"])
584
- self.assertIn(key, payload["errors"][0])
585
- self.assertIn("cache_ttl_days", payload["nullable_keys"])
586
- self.assertNotIn(key, payload["nullable_keys"])
587
- self.assertFalse((Path(tmp) / "config.json").exists())
588
-
589
- def test_invalid_file_is_not_healed_and_blocks_writes(self):
590
- """R1 at the MCP surface: a schema-rejected file is never persisted."""
591
- with tempfile.TemporaryDirectory() as tmp:
592
- base = Path(tmp)
593
- cfg_file = _write_config(
594
- base,
595
- {
596
- "max_iterations": 99,
597
- "agents": {"alpha": {"backend": ["claude", "-p"]}},
598
- },
599
- )
600
- before = cfg_file.read_bytes()
601
- current = self._call({"base_dir": tmp})
602
- self.assertEqual(current["status"], "current")
603
- self.assertFalse(current["migrated"])
604
- self.assertEqual(cfg_file.read_bytes(), before)
605
-
606
- # One valid write fixes both the invalid value and the pin.
607
- fixed = self._call({"max_iterations": 2, "base_dir": tmp})
608
- self.assertEqual(fixed["status"], "updated")
609
- healed = json.loads(cfg_file.read_text(encoding="utf-8"))
610
- self.assertEqual(healed["max_iterations"], 2)
611
- self.assertIsNone(healed["agents"]["alpha"]["backend"])
612
-
613
- def test_updated_payload_is_single_read_coherent(self):
614
- """R10: `updated` config + hash come from ONE read of the written bytes.
615
-
616
- Regression against the pre-R10 shape (`load_config()` then
617
- `config_hash()` as two independent reads): a write landing between the
618
- two would pair an era-A config with an era-B hash. We inject exactly
619
- that write into `config_hash`; a single-read implementation never calls
620
- it, so the returned config/hash stay coherent with the bytes on disk.
621
- """
622
- import hashlib
623
-
624
- from skills.swarm_config import configure
625
-
626
- with tempfile.TemporaryDirectory() as tmp:
627
- original = configure.config_hash
628
-
629
- def torn(base):
630
- # Simulate a concurrent writer landing era-B between the old
631
- # shape's two reads, immediately before the hash read.
632
- cfg = Path(base) / "config.json"
633
- data = json.loads(cfg.read_text(encoding="utf-8"))
634
- data["mode"] = "audit"
635
- cfg.write_text(json.dumps(data), encoding="utf-8")
636
- return original(base)
637
-
638
- configure.config_hash = torn
639
- try:
640
- payload = self._call({"mode": "scout", "base_dir": tmp})
641
- finally:
642
- configure.config_hash = original
643
-
644
- self.assertEqual(payload["status"], "updated")
645
- written = (Path(tmp) / "config.json").read_bytes()
646
- # The hash covers the bytes the config was derived from ...
647
- self.assertEqual(payload["hash"], hashlib.sha256(written).hexdigest())
648
- # ... and the config is that same era (never scout + audit-hash).
649
- self.assertEqual(payload["config"]["mode"], json.loads(written)["mode"])
650
- self.assertEqual(payload["config"], load_config(tmp))
651
-
652
- def test_output_dir_deprecation_is_inert(self):
653
- with tempfile.TemporaryDirectory() as tmp:
654
- base = Path(tmp)
655
- elsewhere = base / "some-other-workspace"
656
- payload = self._call({"output_dir": str(elsewhere), "base_dir": str(base)})
657
- self.assertEqual(payload["status"], "updated")
658
- # The value persists for backward compatibility ...
659
- self.assertEqual(load_config(str(base))["output_dir"], str(elsewhere))
660
- # ... but nothing moves: the workspace is still base_dir.
661
- self.assertTrue((base / "config.json").exists())
662
- self.assertFalse(elsewhere.exists())
663
-
664
-
665
- class TestVerifyThresholdConsumer(unittest.TestCase):
666
- """`verify.min_fuzzy_confidence` is a real parameter behind a real consumer."""
667
-
668
- @staticmethod
669
- def _seed(base: Path):
670
- hasher = SourceHasher(base)
671
- words = [f"w{i}" for i in range(1, 21)]
672
- content = " ".join(words)
673
- digest = hasher.store_source(url="https://example.test/q", content=content)
674
- # 9 of 10 quote words match a contiguous source window -> 0.90 overlap.
675
- quote = " ".join(words[:9]) + " nomatch"
676
- return digest, quote
677
-
678
- def _verify(self, base: Path, digest: str, quote: str):
679
- from runner.mcp_server import _handle_verify_quote
680
-
681
- return json.loads(
682
- _handle_verify_quote(
683
- {"hash": digest, "quote": quote, "base_dir": str(base)}
684
- )
685
- )
686
-
687
- def test_default_threshold_behavior_is_unchanged(self):
688
- with tempfile.TemporaryDirectory() as tmp:
689
- base = Path(tmp)
690
- digest, quote = self._seed(base)
691
- payload = self._verify(base, digest, quote)
692
- # Byte-identical decision to the unconfigured hasher default.
693
- expected = SourceHasher(base).verify_quote(digest, quote)
694
- self.assertEqual(payload["verified"], expected[0])
695
- self.assertEqual(payload["confidence"], expected[1])
696
- self.assertEqual(payload["min_fuzzy_confidence"], 0.88)
697
-
698
- def test_configured_threshold_changes_the_decision(self):
699
- with tempfile.TemporaryDirectory() as tmp:
700
- base = Path(tmp)
701
- digest, quote = self._seed(base)
702
- self.assertTrue(self._verify(base, digest, quote)["verified"])
703
-
704
- from runner.mcp_server import _handle_config, _handle_verify_quote
705
-
706
- json.loads(
707
- _handle_config(
708
- {
709
- "verify": {"min_fuzzy_confidence": 0.95},
710
- "base_dir": str(base),
711
- }
712
- )
713
- )
714
- strict_payload = json.loads(
715
- _handle_verify_quote(
716
- {"hash": digest, "quote": quote, "base_dir": str(base)}
717
- )
718
- )
719
- self.assertFalse(strict_payload["verified"])
720
- self.assertEqual(strict_payload["min_fuzzy_confidence"], 0.95)
721
-
722
- # Back to the default: the same quote verifies again.
723
- json.loads(
724
- _handle_config(
725
- {
726
- "verify": {"min_fuzzy_confidence": 0.88},
727
- "base_dir": str(base),
728
- }
729
- )
730
- )
731
- self.assertTrue(self._verify(base, digest, quote)["verified"])
732
-
733
- def test_unreachable_threshold_can_never_fabricate_a_pass(self):
734
- with tempfile.TemporaryDirectory() as tmp:
735
- base = Path(tmp)
736
- digest, quote = self._seed(base)
737
- passed, _, _ = SourceHasher(base).verify_quote(
738
- digest, quote, min_fuzzy_confidence=1.5
739
- )
740
- self.assertFalse(passed)
741
-
742
- def test_verify_quote_clamps_the_threshold(self):
743
- """R6: the clamp lives in `verify_quote` (single enforcement point)."""
744
- with tempfile.TemporaryDirectory() as tmp:
745
- base = Path(tmp)
746
- hasher = SourceHasher(base)
747
- words = [f"w{i}" for i in range(1, 21)]
748
- digest = hasher.store_source(
749
- url="https://example.test/c", content=" ".join(words)
750
- )
751
- # 3 of 4 quote words match a source window -> best overlap 0.75.
752
- partial = "w1 w2 w3 nomatch"
753
- self.assertFalse(hasher.verify_quote(digest, partial)[0])
754
- # Above 1.0 clamps down to 1.0: still no fabricated pass.
755
- self.assertFalse(
756
- hasher.verify_quote(digest, partial, min_fuzzy_confidence=1.5)[0]
757
- )
758
- # Below 0 clamps up to 0.0: a sub-default overlap then passes.
759
- self.assertTrue(
760
- hasher.verify_quote(digest, partial, min_fuzzy_confidence=-5)[0]
761
- )
762
- # Malformed values fall back to the strict default.
763
- self.assertFalse(
764
- hasher.verify_quote(digest, partial, min_fuzzy_confidence="high")[0]
765
- )
766
-
767
-
768
- class TestAuditThresholdConsumer(unittest.TestCase):
769
- """The auditor (a real swarm consumer) honors the configured threshold."""
770
-
771
- def _engine(self, base, threshold):
772
- from runner.auditor_engine import EpistemicAuditorEngine
773
-
774
- return EpistemicAuditorEngine(
775
- base_dir=base, min_fuzzy_confidence=threshold, repo_validator=None
776
- )
777
-
778
- def test_auditor_rejects_at_a_stricter_threshold(self):
779
- from runner.state_machine import ResearchStateMachine
780
-
781
- with tempfile.TemporaryDirectory() as tmp:
782
- base = Path(tmp)
783
- words = [f"w{i}" for i in range(1, 21)]
784
- hasher = SourceHasher(base)
785
- digest = hasher.store_source(
786
- url="https://example.test/a", content=" ".join(words)
787
- )
788
- quote = " ".join(words[:9]) + " nomatch"
789
- sm = ResearchStateMachine(base_dir=base)
790
- sm.init_session("objective", session_id="s1")
791
- sm.set_scopes([{"scope_id": "sc1", "title": "t", "objective": "o"}])
792
- dossier = {
793
- "scope_id": "sc1",
794
- "affirmative_claims": [
795
- {
796
- "claim_id": "C1",
797
- "tag": "VERIFIED",
798
- "statement": "s",
799
- "source_hash": digest,
800
- "verbatim_quote": quote,
801
- }
802
- ],
803
- }
804
- sm.record_agent_completion("sc1", "alpha", dossier)
805
- sm.record_agent_completion("sc1", "beta", {"scope_id": "sc1"})
806
-
807
- lenient = self._engine(base, 0.88).audit_scope("sc1")
808
- self.assertEqual(lenient["summary"]["verified_passed"], 1)
809
- strict = self._engine(base, 0.95).audit_scope("sc1")
810
- self.assertEqual(strict["summary"]["verified_passed"], 0)
811
- self.assertEqual(strict["summary"]["unverified_rejected"], 1)
812
-
813
-
814
- class TestDivergenceAdvisory(unittest.TestCase):
815
- """`divergence_threshold`: emitted only when non-default, never a decision.
816
-
817
- R3: default/absent threshold runs must stay byte-identical to before the key
818
- existed — no manifest field, no synthesis bullet, no banner line.
819
- """
820
-
821
- def _run(self, threshold):
822
- from runner.research_swarm import SwarmRunner
823
-
824
- tmp = tempfile.mkdtemp()
825
- base = Path(tmp) / ".research"
826
- base.mkdir(parents=True)
827
- if threshold is not None:
828
- _write_config(base, {"divergence_threshold": threshold})
829
- runner = SwarmRunner(base_dir=base, mock_mode=True, mode="research")
830
- buf = io.StringIO()
831
- with redirect_stdout(buf):
832
- runner.run_swarm("Evaluate hardware prover latency")
833
- manifest = runner.state_machine.load_global_manifest()
834
- audit = json.loads(
835
- (base / "scratchpads" / "scope_01_latency" / "audit_report.json").read_text(
836
- encoding="utf-8"
837
- )
838
- )
839
- report = (base / "final_synthesis.md").read_text(encoding="utf-8")
840
- return manifest, audit["summary"], report, buf.getvalue()
841
-
842
- def test_default_threshold_emits_nothing(self):
843
- for threshold in (None, 0.75):
844
- with self.subTest(threshold=threshold):
845
- manifest, _, report, output = self._run(threshold)
846
- self.assertNotIn("divergence_threshold", manifest)
847
- self.assertNotIn("Advisory Divergence Threshold", report)
848
- self.assertNotIn("Divergence threshold (advisory)", output)
849
-
850
- def test_configured_threshold_is_recorded_but_changes_no_decision(self):
851
- manifest_default, summary_default, report_default, out_default = self._run(None)
852
- manifest_custom, summary_custom, report_custom, out_custom = self._run(0.95)
853
-
854
- # Recorded in the manifest, the run summary, and the banner.
855
- self.assertEqual(manifest_custom["divergence_threshold"], 0.95)
856
- self.assertIn("Advisory Divergence Threshold", report_custom)
857
- self.assertIn("0.95", report_custom)
858
- self.assertIn("Divergence threshold (advisory): 0.95", out_custom)
859
-
860
- # No decision changed: the audit summaries are equal.
861
- self.assertEqual(summary_default, summary_custom)
862
-
863
- # Removing the (only) non-default bullet makes the reports identical
864
- # (modulo wall-clock stamps).
865
- stripped = "\n".join(
866
- line
867
- for line in report_custom.splitlines()
868
- if "Advisory Divergence Threshold" not in line
869
- )
870
-
871
- def _normalized(report):
872
- report = re.sub(r"`session-\d{8}-\d{6}`", "`session-<ts>`", report)
873
- return re.sub(r"`\d{4}-\d{2}-\d{2}T[^`]*`", "`<ts>`", report)
874
-
875
- self.assertEqual(_normalized(report_default), _normalized(stripped))
876
-
877
-
878
- if __name__ == "__main__":
879
- unittest.main()