@heretek-ai/epistemic-swarm 0.7.2 → 0.7.3

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 (44) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/plugins/opencode/index.js +25 -5
  4. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  5. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  6. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  7. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  8. package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
  9. package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
  10. package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
  11. package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
  12. package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
  13. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  14. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  15. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  16. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  17. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  18. package/runner/research_swarm.py +36 -23
  19. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  20. package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
  21. package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
  22. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  23. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  24. package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
  25. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  26. package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
  27. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  28. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  29. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  30. package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
  31. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  32. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  33. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  34. package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
  35. package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
  36. package/runner/tests/test_backends.py +49 -4
  37. package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
  38. package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
  39. package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
  40. package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
  41. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  42. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  43. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  44. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epistemic-swarm",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "High-Integrity Dialectic Research Agent Harness for Claude Code enforcing empirical evidence over parametric hallucination.",
5
5
  "author": {
6
6
  "name": "Heretek AI",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heretek-ai/epistemic-swarm",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "IUMBTEMS: I Use My Brain To Express My Self — High-Integrity Dialectic Research Agent Harness for Claude Code, OpenCode V2, Pi, OMP (oh-my-pi), Gemini CLI, Codex CLI, and AntiGravity",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
@@ -598,8 +598,14 @@ export function commandCatalog() {
598
598
  return out;
599
599
  }
600
600
 
601
- /** Tool map (object form) for the server hook; array catalog stays canonical. */
602
- function buildToolMap() {
601
+ /** Tool map (object form) for the server hook; array catalog stays canonical.
602
+ *
603
+ * `hostRoot` is the host-reported project directory (host.location.directory).
604
+ * The tool context's own cwd is absent on some host builds — observed live:
605
+ * every dispatch fell back to process.cwd() (/home/john) and evidence escaped
606
+ * the project tree. Prefer toolContext.cwd, then hostRoot, then process.cwd().
607
+ */
608
+ function buildToolMap(hostRoot = undefined) {
603
609
  return Object.fromEntries(
604
610
  TOOL_CATALOG.map((tool) => [
605
611
  tool.name,
@@ -607,7 +613,11 @@ function buildToolMap() {
607
613
  ...tool,
608
614
  options: { codemode: false },
609
615
  execute: async (args = {}, toolContext = undefined) =>
610
- callMcp(tool.name, normalizeArgs(tool.name, args), toolContext?.cwd),
616
+ callMcp(
617
+ tool.name,
618
+ normalizeArgs(tool.name, args),
619
+ toolContext?.cwd || hostRoot
620
+ ),
611
621
  },
612
622
  ])
613
623
  );
@@ -892,15 +902,25 @@ async function registerHostTools(host) {
892
902
  // cannot be intercepted the way Claude Code's hooks/hooks.json does.
893
903
  // Steering lives in command templates (epistemic search first); a host API
894
904
  // for pre-execution guards would close this for real.
905
+ //
906
+ // hostRoot: the host's project directory. toolContext.cwd is absent on
907
+ // some host builds (observed live: dispatches fell back to process.cwd()
908
+ // = /home/john and evidence escaped the project tree), so capture the
909
+ // host-reported directory here as the fallback.
910
+ const hostRoot = host?.location?.directory || undefined;
895
911
  const registration = await host.tool.transform((draft) => {
896
- for (const [name, spec] of Object.entries(buildToolMap())) {
912
+ for (const [name, spec] of Object.entries(buildToolMap(hostRoot))) {
897
913
  draft.add({
898
914
  name,
899
915
  description: spec.description,
900
916
  input: spec.input,
901
917
  options: { codemode: false },
902
918
  execute: async (args = {}, toolContext = undefined) => {
903
- const r = await callMcp(name, normalizeArgs(name, args), toolContext?.cwd);
919
+ const r = await callMcp(
920
+ name,
921
+ normalizeArgs(name, args),
922
+ toolContext?.cwd || hostRoot
923
+ );
904
924
  return { content: r.content };
905
925
  },
906
926
  });
@@ -43,17 +43,6 @@ OPENCODE_RUN_BASE = ["opencode", "run"]
43
43
  # Fenced JSON block marker shared by orchestrator/dossier stdout parsers.
44
44
  _JSON_FENCE = "```json"
45
45
 
46
- # Swarm mode -> OpenCode agent carrying the equivalent system prompt
47
- # (`opencode run` has no --system-prompt flag; the prompt rides on --agent).
48
- MODE_OPENCODE_AGENT = {
49
- "research": "alpha-thesis",
50
- "audit": "code-auditor",
51
- "scout": "oss-scout",
52
- "hybrid": "alpha-thesis",
53
- "brainstorm": "brainstormer",
54
- "darkharvest": "darkharvester",
55
- }
56
-
57
46
 
58
47
  def _default_backend_cmd(config_host: Optional[str] = None) -> List[str]:
59
48
  """Host-native default backend argv.
@@ -99,7 +88,17 @@ _TEXT_EVENT_MARKERS = ("message", "text", "result", "output", "content")
99
88
 
100
89
 
101
90
  def _event_text(obj: Dict[str, Any]) -> Optional[str]:
102
- """Text payload of one parsed event line, or None."""
91
+ """Text payload of one parsed event line, or None.
92
+
93
+ Handles the real `opencode run --format json` shape
94
+ ({"type":"text", "part":{"type":"text","text":"..."}}) plus top-level
95
+ variants for tolerance.
96
+ """
97
+ part = obj.get("part")
98
+ if isinstance(part, dict):
99
+ val = part.get("text")
100
+ if isinstance(val, str) and val.strip():
101
+ return val
103
102
  kind = str(obj.get("type", "")).lower()
104
103
  if kind and not any(m in kind for m in _TEXT_EVENT_MARKERS):
105
104
  return None
@@ -237,14 +236,16 @@ class SwarmRunner:
237
236
  return list(backend), model
238
237
 
239
238
  def _resolve_opencode_agent(self, role: str) -> Optional[str]:
240
- """OpenCode agent carrying the system prompt for this mode/role."""
241
- agents_cfg = self.config.get("agents") or {}
242
- role_cfg = agents_cfg.get(role) or {}
243
- if role_cfg.get("opencode_agent"):
244
- return role_cfg["opencode_agent"]
245
- if self.mode == "research" and role == "beta":
246
- return "beta-redteam"
247
- return MODE_OPENCODE_AGENT.get(self.mode)
239
+ """Explicitly configured OpenCode agent for this role, else None.
240
+
241
+ Default is None: the system prompt is inlined into the message because
242
+ `--agent <name>` fails hard ("Agent not found") unless the user
243
+ installed the snippet agent profiles — observed live, so the backend
244
+ must not depend on them. Set `agents.<role>.opencode_agent` (or the
245
+ top-level `opencode_agent` config key) to opt in.
246
+ """
247
+ role_cfg = (self.config.get("agents") or {}).get(role) or {}
248
+ return role_cfg.get("opencode_agent") or self.config.get("opencode_agent")
248
249
 
249
250
  def build_agent_cmd(
250
251
  self,
@@ -270,9 +271,16 @@ class SwarmRunner:
270
271
  "backend (CLI flag injection)"
271
272
  )
272
273
  if _backend_family(backend) == "opencode":
273
- return _build_opencode_cmd(
274
- backend, prompt, model, self._resolve_opencode_agent(role)
275
- )
274
+ agent = self._resolve_opencode_agent(role)
275
+ # No --system-prompt flag exists on `opencode run`, and --agent
276
+ # only works when the user installed the profile. Inline the
277
+ # system prompt into the message so runs never depend on
278
+ # host-side agent configuration.
279
+ if system_prompt_file and system_prompt_file.exists():
280
+ prompt = (
281
+ system_prompt_file.read_text(encoding="utf-8") + "\n\n" + prompt
282
+ )
283
+ return _build_opencode_cmd(backend, prompt, model, agent)
276
284
  cmd = list(backend) + [prompt, "--tools", tools]
277
285
  if model:
278
286
  cmd.extend(["--model", str(model)])
@@ -303,6 +311,11 @@ class SwarmRunner:
303
311
  text=True,
304
312
  check=True,
305
313
  cwd=str(PROJECT_ROOT),
314
+ # stdin MUST be DEVNULL: `opencode run` reads piped stdin to
315
+ # EOF before starting, and the MCP server's inherited stdin
316
+ # pipe is held open by the harness — every agent hung forever
317
+ # (observed live: 10+ min, 1s CPU, no network I/O).
318
+ stdin=subprocess.DEVNULL,
306
319
  shell=False,
307
320
  )
308
321
  if family == "opencode":
@@ -103,13 +103,29 @@ class TestRunnerBackendWiring(unittest.TestCase):
103
103
  r = self._runner(mode="scout")
104
104
  cmd = r.build_agent_cmd("obj")
105
105
  self.assertEqual(cmd[:2], ["opencode", "run"])
106
- self.assertIn("oss-scout", cmd)
106
+ # No --agent by default: named agents fail hard unless the user
107
+ # installed the profile ("Agent not found", observed live).
108
+ self.assertNotIn("--agent", cmd)
109
+ self.assertIn("--format", cmd)
110
+
111
+ def test_explicit_opencode_agent_config_opt_in(self):
112
+ with mock.patch.dict(os.environ, {"IUMBTEMS_HOST": "opencode"}):
113
+ r = self._runner(agent_overrides=None)
114
+ r.config["opencode_agent"] = "brainstormer"
115
+ cmd = r.build_agent_cmd("obj")
116
+ self.assertIn("--agent", cmd)
117
+ self.assertIn("brainstormer", cmd)
118
+
119
+ def test_system_prompt_inlined_for_opencode(self):
120
+ import tempfile as _tf
107
121
 
108
- def test_research_beta_maps_to_beta_redteam(self):
109
122
  with mock.patch.dict(os.environ, {"IUMBTEMS_HOST": "opencode"}):
110
123
  r = self._runner(mode="research")
111
- self.assertIn("beta-redteam", r.build_agent_cmd("obj", role="beta"))
112
- self.assertIn("alpha-thesis", r.build_agent_cmd("obj", role="alpha"))
124
+ spf = Path(_tf.mkdtemp()) / "sys.md"
125
+ spf.write_text("SYSTEM PROMPT BODY", encoding="utf-8")
126
+ cmd = r.build_agent_cmd("obj", system_prompt_file=spf)
127
+ self.assertIn("SYSTEM PROMPT BODY", cmd[2])
128
+ self.assertNotIn("--system-prompt", cmd)
113
129
 
114
130
  def test_explicit_override_beats_host_env(self):
115
131
  with mock.patch.dict(os.environ, {"IUMBTEMS_HOST": "opencode"}):
@@ -119,6 +135,17 @@ class TestRunnerBackendWiring(unittest.TestCase):
119
135
 
120
136
 
121
137
  class TestOpencodeOutputParsing(unittest.TestCase):
138
+ def test_extracts_real_part_text_events(self):
139
+ """Live shape: {"type":"text","part":{"type":"text","text":"..."}}."""
140
+ raw = "\n".join(
141
+ [
142
+ '{"type":"step_start","timestamp":1,"sessionID":"s","part":{"type":"step-start"}}',
143
+ '{"type":"text","timestamp":2,"sessionID":"s","part":{"id":"p1","type":"text","text":"pong"}}',
144
+ '{"type":"text","timestamp":3,"sessionID":"s","part":{"id":"p2","type":"text","text":"second"}}',
145
+ ]
146
+ )
147
+ self.assertEqual(_extract_opencode_text(raw), "pong\nsecond")
148
+
122
149
  def test_extracts_text_events(self):
123
150
  raw = "\n".join(
124
151
  [
@@ -139,6 +166,24 @@ class TestOpencodeOutputParsing(unittest.TestCase):
139
166
  self.assertEqual(_extract_opencode_text(raw), "verdict: clean-room")
140
167
 
141
168
 
169
+ class TestSpawnHardening(unittest.TestCase):
170
+ def test_stdin_is_devnull_for_backend_spawns(self):
171
+ """Regression: held-open stdin pipe made `opencode run` hang forever."""
172
+ import tempfile
173
+ from runner.research_swarm import SwarmRunner
174
+
175
+ r = SwarmRunner(
176
+ base_dir=Path(tempfile.mkdtemp()), mock_mode=False, mode="research"
177
+ )
178
+ fake = mock.Mock()
179
+ fake.stdout = "ok"
180
+ with mock.patch("subprocess.run", return_value=fake) as run_mock:
181
+ r.run_claude_process("objective text")
182
+ kwargs = run_mock.call_args.kwargs
183
+ self.assertEqual(kwargs.get("stdin"), subprocess.DEVNULL)
184
+ self.assertEqual(kwargs.get("shell"), False)
185
+
186
+
142
187
  class TestDossierStdoutFallback(unittest.TestCase):
143
188
  def _runner(self):
144
189
  import tempfile