@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.
- package/index.ts +1 -0
- package/package.json +63 -139
- package/rpc.ts +1 -0
- package/server.ts +1 -0
- package/src/agents.ts +69 -0
- package/src/continue.ts +40 -0
- package/src/hooks.ts +283 -0
- package/src/panels.tsx +220 -0
- package/src/policy.ts +221 -0
- package/src/pr.ts +37 -0
- package/src/research.ts +63 -0
- package/src/rpc-def.ts +136 -0
- package/src/rpc.ts +331 -0
- package/src/runtime.ts +112 -0
- package/src/server.ts +311 -0
- package/src/session.ts +61 -0
- package/src/tools.ts +61 -0
- package/src/tui.tsx +206 -0
- package/tui.tsx +1 -0
- package/.agents/skills/brainstorming/SKILL.md +0 -19
- package/.agents/skills/code_audit/SKILL.md +0 -19
- package/.agents/skills/darkharvest/SKILL.md +0 -19
- package/.agents/skills/epistemic_search/SKILL.md +0 -19
- package/.agents/skills/factory/SKILL.md +0 -19
- package/.agents/skills/grilling/SKILL.md +0 -19
- package/.agents/skills/oss_scout/SKILL.md +0 -19
- package/.agents/skills/research_cache/SKILL.md +0 -19
- package/.agents/skills/swarm_config/SKILL.md +0 -19
- package/.claude-plugin/marketplace.json +0 -61
- package/.claude-plugin/plugin.json +0 -76
- package/.omp/README.md +0 -39
- package/.omp/SYSTEM.md +0 -17
- package/.omp/commands/audit.md +0 -10
- package/.omp/commands/brainstorming.md +0 -12
- package/.omp/commands/darkharvest.md +0 -11
- package/.omp/commands/domainexpansion.md +0 -9
- package/.omp/commands/factory.md +0 -9
- package/.omp/commands/grill.md +0 -9
- package/.omp/commands/scout.md +0 -10
- package/.omp/commands/swarm-config.md +0 -10
- package/.omp/commands/swarm.md +0 -9
- package/.omp/hooks/post/epistemic-audit.ts +0 -16
- package/.omp/hooks/pre/epistemic-redirect.ts +0 -21
- package/.omp/prompts/brainstorming.md +0 -8
- package/.omp/prompts/swarm.md +0 -8
- package/LICENSE +0 -126
- package/MARKETPLACE.md +0 -130
- package/README.md +0 -403
- package/bin/cli.js +0 -297
- package/config/claude-settings-patch.json +0 -10
- package/config/docker-compose.infra.yml +0 -23
- package/config/domain_packs/biopharma.json +0 -23
- package/config/domain_packs/legal.json +0 -19
- package/config/domain_packs/quant.json +0 -19
- package/config/mcp-research-servers.json +0 -33
- package/config/mcp_launcher.py +0 -153
- package/config/opencode-snippet.json +0 -156
- package/config/searxng_mcp.py +0 -92
- package/extensions/pi/index.js +0 -357
- package/hooks/hooks.json +0 -45
- package/install.sh +0 -473
- package/plugins/antigravity/README.md +0 -72
- package/plugins/antigravity/agents/alpha-thesis.md +0 -36
- package/plugins/antigravity/agents/beta-antithesis.md +0 -35
- package/plugins/antigravity/agents/brainstormer.md +0 -27
- package/plugins/antigravity/agents/code-auditor.md +0 -28
- package/plugins/antigravity/agents/darkharvester.md +0 -30
- package/plugins/antigravity/agents/epistemic-auditor.md +0 -28
- package/plugins/antigravity/agents/manager.md +0 -26
- package/plugins/antigravity/agents/oss-scout.md +0 -27
- package/plugins/antigravity/agents/programmer.md +0 -25
- package/plugins/antigravity/agents/qa-a.md +0 -22
- package/plugins/antigravity/agents/qa-b.md +0 -22
- package/plugins/antigravity/antigravity_hooks.py +0 -265
- package/plugins/antigravity/commands/audit.md +0 -11
- package/plugins/antigravity/commands/brainstorm.md +0 -9
- package/plugins/antigravity/commands/brainstorming.md +0 -10
- package/plugins/antigravity/commands/darkharvest.md +0 -12
- package/plugins/antigravity/commands/domainexpansion.md +0 -12
- package/plugins/antigravity/commands/factory.md +0 -14
- package/plugins/antigravity/commands/grill.md +0 -10
- package/plugins/antigravity/commands/scout.md +0 -11
- package/plugins/antigravity/commands/swarm-config.md +0 -9
- package/plugins/antigravity/commands/swarm.md +0 -12
- package/plugins/antigravity/hook_bus_bridge.js +0 -352
- package/plugins/antigravity/hooks.json +0 -44
- package/plugins/antigravity/mcp_config.json +0 -33
- package/plugins/antigravity/mcp_launcher.py +0 -58
- package/plugins/antigravity/plugin.json +0 -13
- package/plugins/antigravity/rules/AGENTS.md +0 -127
- package/plugins/antigravity/rules/epistemic-integrity.md +0 -16
- package/plugins/antigravity/skills/brainstorming/SKILL.md +0 -19
- package/plugins/antigravity/skills/code_audit/SKILL.md +0 -19
- package/plugins/antigravity/skills/darkharvest/SKILL.md +0 -19
- package/plugins/antigravity/skills/epistemic_search/SKILL.md +0 -19
- package/plugins/antigravity/skills/factory/SKILL.md +0 -19
- package/plugins/antigravity/skills/grilling/SKILL.md +0 -19
- package/plugins/antigravity/skills/oss_scout/SKILL.md +0 -19
- package/plugins/antigravity/skills/research_cache/SKILL.md +0 -19
- package/plugins/antigravity/skills/swarm_config/SKILL.md +0 -19
- package/plugins/codex/AGENTS.md.snippet +0 -10
- package/plugins/codex/README.md +0 -37
- package/plugins/codex/config.toml.snippet +0 -28
- package/plugins/codex/openai.yaml +0 -24
- package/plugins/codex/skills/brainstorming/SKILL.md +0 -19
- package/plugins/codex/skills/code_audit/SKILL.md +0 -19
- package/plugins/codex/skills/darkharvest/SKILL.md +0 -19
- package/plugins/codex/skills/epistemic_search/SKILL.md +0 -19
- package/plugins/codex/skills/factory/SKILL.md +0 -19
- package/plugins/codex/skills/grilling/SKILL.md +0 -19
- package/plugins/codex/skills/oss_scout/SKILL.md +0 -19
- package/plugins/codex/skills/research_cache/SKILL.md +0 -19
- package/plugins/codex/skills/swarm_config/SKILL.md +0 -19
- package/plugins/darkharvest/.claude-plugin/plugin.json +0 -15
- package/plugins/darkharvest/agents/harvest-proponent.md +0 -22
- package/plugins/darkharvest/agents/harvest-redteam.md +0 -20
- package/plugins/darkharvest/evals/teardown-verdict/graders/license-line.md +0 -6
- package/plugins/darkharvest/evals/teardown-verdict/graders/skill-fired.md +0 -5
- package/plugins/darkharvest/evals/teardown-verdict/prompt.md +0 -6
- package/plugins/darkharvest/skills/darkharvest/SKILL.md +0 -70
- package/plugins/darkharvest/skills/darkharvest/scripts/harvest.py +0 -289
- package/plugins/factory/.claude-plugin/plugin.json +0 -15
- package/plugins/factory/agents/factory-manager.md +0 -22
- package/plugins/factory/agents/programmer.md +0 -16
- package/plugins/factory/agents/qa-adversarial.md +0 -17
- package/plugins/factory/agents/qa-functional.md +0 -17
- package/plugins/factory/evals/gate-halt/graders/gates-first.md +0 -6
- package/plugins/factory/evals/gate-halt/graders/skill-fired.md +0 -5
- package/plugins/factory/evals/gate-halt/prompt.md +0 -6
- package/plugins/factory/skills/factory/SKILL.md +0 -61
- package/plugins/factory/skills/factory/scripts/factory.py +0 -300
- package/plugins/gemini/GEMINI.md +0 -16
- package/plugins/gemini/README.md +0 -20
- package/plugins/gemini/commands/audit.toml +0 -6
- package/plugins/gemini/commands/brainstorming.toml +0 -10
- package/plugins/gemini/commands/darkharvest.toml +0 -8
- package/plugins/gemini/commands/domainexpansion.toml +0 -7
- package/plugins/gemini/commands/factory.toml +0 -8
- package/plugins/gemini/commands/grill.toml +0 -6
- package/plugins/gemini/commands/scout.toml +0 -7
- package/plugins/gemini/commands/swarm-config.toml +0 -7
- package/plugins/gemini/commands/swarm.toml +0 -8
- package/plugins/gemini/gemini-extension.json +0 -38
- package/plugins/gemini/hooks/hooks.json +0 -11
- package/plugins/gemini/skills/brainstorming/SKILL.md +0 -19
- package/plugins/gemini/skills/code_audit/SKILL.md +0 -19
- package/plugins/gemini/skills/darkharvest/SKILL.md +0 -19
- package/plugins/gemini/skills/epistemic_search/SKILL.md +0 -19
- package/plugins/gemini/skills/factory/SKILL.md +0 -19
- package/plugins/gemini/skills/grilling/SKILL.md +0 -19
- package/plugins/gemini/skills/oss_scout/SKILL.md +0 -19
- package/plugins/gemini/skills/research_cache/SKILL.md +0 -19
- package/plugins/gemini/skills/swarm_config/SKILL.md +0 -19
- package/plugins/opencode/analysis-bridge.js +0 -620
- package/plugins/opencode/config-io.js +0 -1651
- package/plugins/opencode/dep-health.js +0 -1039
- package/plugins/opencode/hook-bus.js +0 -496
- package/plugins/opencode/index.js +0 -2992
- package/plugins/opencode/tui.js +0 -1551
- package/plugins/opencode/weight-baseline.json +0 -16
- package/plugins/opencode/weight-signals.js +0 -769
- package/plugins/research-cache/.claude-plugin/plugin.json +0 -15
- package/plugins/research-cache/skills/research_cache/SKILL.md +0 -36
- package/plugins/research-cache/skills/research_cache/__init__.py +0 -0
- package/plugins/research-cache/skills/research_cache/hasher.py +0 -302
- package/plugins/socratic-grilling/.claude-plugin/plugin.json +0 -15
- package/plugins/socratic-grilling/skills/grilling/SKILL.md +0 -61
- package/plugins/socratic-grilling/skills/grilling/__init__.py +0 -0
- package/plugins/socratic-grilling/skills/grilling/socratic_tree.py +0 -363
- package/prompts/agent_alpha_thesis.md +0 -70
- package/prompts/agent_beta_antithesis.md +0 -78
- package/prompts/agent_brainstormer.md +0 -143
- package/prompts/agent_code_auditor.md +0 -88
- package/prompts/agent_darkharvest.md +0 -140
- package/prompts/agent_oss_scout.md +0 -78
- package/prompts/antigravity_repo_factcheck.md +0 -143
- package/prompts/base_epistemic_system.md +0 -50
- package/prompts/epistemic_auditor.md +0 -74
- package/prompts/orchestrator.md +0 -70
- package/runner/__init__.py +0 -0
- package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
- package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
- package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
- package/runner/__pycache__/darkharvest_claims.cpython-311.pyc +0 -0
- package/runner/__pycache__/errors.cpython-311.pyc +0 -0
- package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
- package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
- package/runner/__pycache__/preflight.cpython-311.pyc +0 -0
- package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
- package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
- package/runner/__pycache__/retrieval_log.cpython-311.pyc +0 -0
- package/runner/__pycache__/schema_validate.cpython-311.pyc +0 -0
- package/runner/__pycache__/schemas.cpython-311.pyc +0 -0
- package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
- package/runner/antigravity_hooks.py +0 -265
- package/runner/auctioneer.py +0 -169
- package/runner/auditor_engine.py +0 -711
- package/runner/claim_store.py +0 -417
- package/runner/claim_witness.py +0 -412
- package/runner/darkharvest_claims.py +0 -358
- package/runner/errors.py +0 -51
- package/runner/living_dossiers.py +0 -395
- package/runner/mcp_protocol.py +0 -188
- package/runner/mcp_server.py +0 -1447
- package/runner/path_safety.py +0 -62
- package/runner/pcrb.py +0 -290
- package/runner/pcrb_verify.py +0 -267
- package/runner/preflight.py +0 -1011
- package/runner/refinement.py +0 -580
- package/runner/research_swarm.py +0 -1848
- package/runner/retrieval_log.py +0 -90
- package/runner/schema_validate.py +0 -193
- package/runner/schemas.py +0 -630
- package/runner/state_machine.py +0 -463
- package/runner/tests/__pycache__/test_analysis_bridge.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_antigravity_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_config_schema.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_contract_tooling.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dep_health.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dossier_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_mirror_standalone.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_grounding.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_hardening_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_hook_bus.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_manifest_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_nk_hardening.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_v2_registrars.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_phase08_waiver_reopens.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_anomaly.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm_config.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_weight_signals.cpython-311.pyc +0 -0
- package/runner/tests/fixtures/auction_objective.json +0 -35
- package/runner/tests/fixtures/borderline_claims.json +0 -27
- package/runner/tests/fixtures/config_validation_cases.json +0 -276
- package/runner/tests/fixtures/ddg_anomaly_page.html +0 -176
- package/runner/tests/fixtures/ddg_results_page.html +0 -28
- package/runner/tests/fixtures/divergence_objectives.json +0 -31
- package/runner/tests/test_analysis_bridge.py +0 -450
- package/runner/tests/test_antigravity_plugin.py +0 -361
- package/runner/tests/test_auction_order.py +0 -173
- package/runner/tests/test_backends.py +0 -457
- package/runner/tests/test_bet1_spike.py +0 -95
- package/runner/tests/test_claim_store.py +0 -162
- package/runner/tests/test_claim_witness.py +0 -186
- package/runner/tests/test_claude_plugin.py +0 -170
- package/runner/tests/test_config_schema.py +0 -879
- package/runner/tests/test_contract_tooling.py +0 -312
- package/runner/tests/test_darkharvest_audit.py +0 -395
- package/runner/tests/test_dep_health.py +0 -595
- package/runner/tests/test_domain_packs.py +0 -217
- package/runner/tests/test_dossier_contract.py +0 -196
- package/runner/tests/test_factory.py +0 -258
- package/runner/tests/test_fleet_seam.py +0 -113
- package/runner/tests/test_frontier_concurrency.py +0 -288
- package/runner/tests/test_frontier_mirror_standalone.py +0 -156
- package/runner/tests/test_grounding.py +0 -183
- package/runner/tests/test_hardening_contract.py +0 -391
- package/runner/tests/test_hook_bus.py +0 -956
- package/runner/tests/test_living_dossiers.py +0 -204
- package/runner/tests/test_manifest_concurrency.py +0 -187
- package/runner/tests/test_mcp_server.py +0 -257
- package/runner/tests/test_nk_hardening.py +0 -1046
- package/runner/tests/test_opencode_ux.py +0 -2438
- package/runner/tests/test_opencode_v2_registrars.py +0 -2157
- package/runner/tests/test_pcrb.py +0 -240
- package/runner/tests/test_phase08_waiver_reopens.py +0 -539
- package/runner/tests/test_refinement.py +0 -255
- package/runner/tests/test_search_anomaly.py +0 -342
- package/runner/tests/test_search_provider.py +0 -1534
- package/runner/tests/test_search_provider_plugin.py +0 -401
- package/runner/tests/test_swarm.py +0 -444
- package/runner/tests/test_swarm_config.py +0 -520
- package/runner/tests/test_sweep_regressions.py +0 -453
- package/runner/tests/test_webcache.py +0 -271
- package/runner/tests/test_weight_signals.py +0 -401
- package/schemas/alpha_dossier.schema.json +0 -204
- package/schemas/audit_report.schema.json +0 -79
- package/schemas/beta_dossier.schema.json +0 -214
- package/schemas/config.schema.json +0 -311
- package/schemas/frontier.schema.json +0 -61
- package/schemas/manifest.schema.json +0 -49
- package/schemas/scope_manifest.schema.json +0 -47
- package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
- package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
- package/scripts/__pycache__/check_docs.cpython-311.pyc +0 -0
- package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
- package/scripts/__pycache__/gen_schemas.cpython-311.pyc +0 -0
- package/scripts/auction_experiment.py +0 -188
- package/scripts/bet1_advisory_spike.py +0 -442
- package/scripts/build_adapters.py +0 -417
- package/scripts/check_docs.py +0 -66
- package/scripts/divergence_experiment.py +0 -214
- package/scripts/gen_schemas.py +0 -69
- package/skills/brainstorming/SKILL.md +0 -106
- package/skills/brainstorming/__init__.py +0 -1
- package/skills/brainstorming/scripts/brainstorm.py +0 -201
- package/skills/code_audit/SKILL.md +0 -45
- package/skills/darkharvest/SKILL.md +0 -70
- package/skills/darkharvest/scripts/harvest.py +0 -289
- package/skills/epistemic_search/SKILL.md +0 -67
- package/skills/epistemic_search/__init__.py +0 -1
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/epistemic_search/scripts/fetch.py +0 -156
- package/skills/epistemic_search/scripts/search.py +0 -551
- package/skills/epistemic_search/scripts/webcache.py +0 -339
- package/skills/factory/SKILL.md +0 -61
- package/skills/factory/scripts/factory.py +0 -300
- package/skills/grilling/SKILL.md +0 -61
- package/skills/grilling/__init__.py +0 -0
- package/skills/grilling/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/grilling/__pycache__/socratic_tree.cpython-311.pyc +0 -0
- package/skills/grilling/socratic_tree.py +0 -363
- package/skills/oss_scout/SKILL.md +0 -49
- package/skills/research_cache/SKILL.md +0 -36
- package/skills/research_cache/__init__.py +0 -0
- package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
- package/skills/research_cache/hasher.py +0 -302
- package/skills/swarm_config/SKILL.md +0 -178
- package/skills/swarm_config/__init__.py +0 -35
- package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
- package/skills/swarm_config/configure.py +0 -1002
package/runner/mcp_server.py
DELETED
|
@@ -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())
|