@heretek-ai/epistemic-swarm 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/install.sh +88 -0
  3. package/package.json +6 -1
  4. package/plugins/antigravity/plugin.json +1 -1
  5. package/plugins/gemini/gemini-extension.json +1 -1
  6. package/plugins/opencode/index.js +318 -17
  7. package/plugins/opencode/tui.js +247 -0
  8. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  9. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  10. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  11. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  12. package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
  13. package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
  14. package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
  15. package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
  16. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  17. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  18. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  19. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  20. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  21. package/runner/tests/__pycache__/test_auction_order.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_domain_packs.cpython-311.pyc +0 -0
  25. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  26. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  27. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  28. package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
  29. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  30. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  31. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  32. package/runner/tests/test_opencode_ux.py +393 -0
  33. package/runner/tests/test_swarm.py +19 -3
  34. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  35. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  36. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  37. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for the OpenCode v2 end-user UX surface (goal Phase A).
3
+
4
+ Covers the slash-command catalog, the server object-form contract, and
5
+ install.sh OpenCode handling (idempotency + bare-object repair).
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+ import unittest
14
+ from pathlib import Path
15
+
16
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
17
+ if str(PROJECT_ROOT) not in sys.path:
18
+ sys.path.insert(0, str(PROJECT_ROOT))
19
+
20
+ EXPECTED_COMMANDS = [
21
+ "swarm",
22
+ "grill",
23
+ "swarm-config",
24
+ "audit",
25
+ "scout",
26
+ "brainstorming",
27
+ "brainstorm",
28
+ ]
29
+
30
+
31
+ def run_node(code):
32
+ return subprocess.run(
33
+ ["node", "--input-type=module", "-e", code],
34
+ capture_output=True,
35
+ text=True,
36
+ cwd=str(PROJECT_ROOT),
37
+ )
38
+
39
+
40
+ def last_json_object(stdout):
41
+ lines = [l.strip() for l in stdout.strip().split("\n") if l.strip().startswith("{")]
42
+ return json.loads(lines[-1])
43
+
44
+
45
+ class TestOpenCodeCommandCatalog(unittest.TestCase):
46
+ def test_catalog_shape_and_registration(self):
47
+ res = run_node(
48
+ """
49
+ import plugin, { OPENCODE_COMMANDS, registerOpenCodeCommands } from "./plugins/opencode/index.js";
50
+ const shell = await plugin.server();
51
+ const cfg = {};
52
+ shell.config(cfg);
53
+ console.log(JSON.stringify({
54
+ catalog: OPENCODE_COMMANDS.map(c => ({name: c.name, hasDesc: !!c.description, hasArgs: c.template.includes("$ARGUMENTS")})),
55
+ registered: Object.keys(cfg.command || {}),
56
+ templates: Object.fromEntries(Object.entries(cfg.command || {}).map(([k, v]) => [k, !!v.description && !!v.template]))
57
+ }));
58
+ """
59
+ )
60
+ self.assertEqual(
61
+ res.returncode, 0, f"command catalog test failed: {res.stderr}"
62
+ )
63
+ data = last_json_object(res.stdout)
64
+ names = [c["name"] for c in data["catalog"]]
65
+ for c in EXPECTED_COMMANDS:
66
+ self.assertIn(c, names)
67
+ for entry in data["catalog"]:
68
+ self.assertTrue(
69
+ entry["hasDesc"], f"command lacks description: {entry['name']}"
70
+ )
71
+ self.assertTrue(
72
+ entry["hasArgs"], f"template lacks $ARGUMENTS: {entry['name']}"
73
+ )
74
+ for c in EXPECTED_COMMANDS:
75
+ self.assertIn(c, data["registered"])
76
+ self.assertTrue(data["templates"][c])
77
+
78
+ def test_registration_never_overwrites(self):
79
+ res = run_node(
80
+ """
81
+ import { registerOpenCodeCommands } from "./plugins/opencode/index.js";
82
+ const cfg = {command: {swarm: {description: "mine", template: "mine"}}};
83
+ registerOpenCodeCommands(cfg);
84
+ console.log(JSON.stringify({swarm: cfg.command.swarm, count: Object.keys(cfg.command).length}));
85
+ """
86
+ )
87
+ self.assertEqual(res.returncode, 0, f"no-overwrite test failed: {res.stderr}")
88
+ data = last_json_object(res.stdout)
89
+ self.assertEqual(data["swarm"], {"description": "mine", "template": "mine"})
90
+ self.assertEqual(data["count"], len(EXPECTED_COMMANDS))
91
+
92
+ def test_tool_map_shape(self):
93
+ res = run_node(
94
+ """
95
+ import plugin, { IUMBTEMS_TOOL_NAMES } from "./plugins/opencode/index.js";
96
+ const shell = await plugin.server();
97
+ const entries = Object.entries(shell.tool);
98
+ console.log(JSON.stringify({
99
+ keys: entries.map(([k]) => k),
100
+ canonical: IUMBTEMS_TOOL_NAMES,
101
+ ok: entries.map(([k, t]) => (!!t.description && !!t.input && typeof t.execute === "function" && t.options && t.options.codemode === false))
102
+ }));
103
+ """
104
+ )
105
+ self.assertEqual(res.returncode, 0, f"tool map test failed: {res.stderr}")
106
+ data = last_json_object(res.stdout)
107
+ self.assertEqual(sorted(data["keys"]), sorted(data["canonical"]))
108
+ self.assertEqual(len(data["keys"]), 13)
109
+ self.assertTrue(all(data["ok"]))
110
+
111
+
112
+ class TestInstallOpenCode(unittest.TestCase):
113
+ def _run_install(self, home):
114
+ env = dict(os.environ, HOME=str(home))
115
+ return subprocess.run(
116
+ ["bash", str(PROJECT_ROOT / "install.sh")],
117
+ capture_output=True,
118
+ text=True,
119
+ cwd=str(PROJECT_ROOT),
120
+ env=env,
121
+ )
122
+
123
+ def _seed_malformed(self, home):
124
+ oc = home / ".config" / "opencode"
125
+ oc.mkdir(parents=True, exist_ok=True)
126
+ # Exact malformation seen in the wild: bare options object after the string.
127
+ oc.joinpath("opencode.json").write_text(
128
+ json.dumps(
129
+ {
130
+ "$schema": "https://opencode.ai/config.json",
131
+ "plugin": [
132
+ "@prevalentware/opencode-goal-plugin",
133
+ "@heretek-ai/epistemic-swarm",
134
+ {
135
+ "search_engine": "duckduckgo",
136
+ "max_iterations": 10,
137
+ "mode": "research",
138
+ },
139
+ ],
140
+ }
141
+ ),
142
+ encoding="utf-8",
143
+ )
144
+
145
+ def test_idempotent_and_repairs_bare_object(self):
146
+ with tempfile.TemporaryDirectory() as tmp:
147
+ home = Path(tmp)
148
+ self._seed_malformed(home)
149
+ first = self._run_install(home)
150
+ self.assertEqual(first.returncode, 0, f"install failed: {first.stderr}")
151
+ oc_path = home / ".config" / "opencode" / "opencode.json"
152
+ cfg = json.loads(oc_path.read_text(encoding="utf-8"))
153
+ # Bare object repaired into tuple form, preserving user options.
154
+ tuples = [
155
+ p
156
+ for p in cfg["plugin"]
157
+ if isinstance(p, list) and p and p[0] == "@heretek-ai/epistemic-swarm"
158
+ ]
159
+ self.assertEqual(len(tuples), 1)
160
+ self.assertEqual(
161
+ tuples[0][1],
162
+ {
163
+ "search_engine": "duckduckgo",
164
+ "max_iterations": 10,
165
+ "mode": "research",
166
+ },
167
+ )
168
+ self.assertFalse(any(isinstance(p, dict) for p in cfg["plugin"]))
169
+ # Agents merged from the snippet, MCP server absolute.
170
+ for agent in [
171
+ "code-auditor",
172
+ "oss-scout",
173
+ "alpha-thesis",
174
+ "beta-redteam",
175
+ "epistemic-auditor",
176
+ "brainstormer",
177
+ ]:
178
+ self.assertIn(agent, cfg.get("agent", {}))
179
+ self.assertIn("iumbtems", cfg.get("mcp", {}))
180
+ cmd = cfg["mcp"]["iumbtems"]["command"]
181
+ self.assertTrue(Path(cmd[-1]).is_absolute())
182
+ self.assertEqual(cmd[-1], str(PROJECT_ROOT / "runner" / "mcp_server.py"))
183
+ # Second run changes nothing.
184
+ before = oc_path.read_bytes()
185
+ second = self._run_install(home)
186
+ self.assertEqual(second.returncode, 0, f"reinstall failed: {second.stderr}")
187
+ self.assertEqual(oc_path.read_bytes(), before)
188
+
189
+
190
+ class TestOpenCodeTui(unittest.TestCase):
191
+ def test_module_load_and_setup(self):
192
+ res = run_node(
193
+ """
194
+ import plugin, { readSwarmStatus, setupTui } from "./plugins/opencode/tui.js";
195
+ const calls = [];
196
+ const mock = {
197
+ theme: {}, directory: "/tmp/oc-tui-empty",
198
+ ui: { slot: (...a) => { calls.push(a.length); return () => {}; }, toast: { show: () => {} }, router: { current: () => ({type: "other"}) } },
199
+ keymap: { layer: () => {} }
200
+ };
201
+ const cleanup = setupTui(mock);
202
+ const empty = readSwarmStatus("/tmp/oc-tui-empty");
203
+ console.log(JSON.stringify({id: plugin.id, hasTui: !!plugin.tui, setupFn: typeof plugin.setup, slots: calls, cleanup: typeof cleanup, emptyWorkspace: empty.workspace}));
204
+ """
205
+ )
206
+ self.assertEqual(res.returncode, 0, f"tui load test failed: {res.stderr}")
207
+ data = last_json_object(res.stdout)
208
+ self.assertEqual(data["id"], "heretek.iumbtems.epistemic-swarm.tui")
209
+ self.assertTrue(data["hasTui"])
210
+ self.assertEqual(data["setupFn"], "function")
211
+ self.assertEqual(data["slots"], [1, 1])
212
+ self.assertEqual(data["cleanup"], "function")
213
+ self.assertFalse(data["emptyWorkspace"])
214
+
215
+ def test_status_snapshot_counts(self):
216
+ with tempfile.TemporaryDirectory() as tmp:
217
+ root = Path(tmp)
218
+ research = root / ".research"
219
+ (research / "ledger").mkdir(parents=True)
220
+ research.joinpath("config.json").write_text(
221
+ json.dumps(
222
+ {
223
+ "mode": "research",
224
+ "search_engine": "duckduckgo",
225
+ "max_iterations": 2,
226
+ }
227
+ ),
228
+ encoding="utf-8",
229
+ )
230
+ research.joinpath("final_synthesis.md").write_text(
231
+ "# synthesis\n", encoding="utf-8"
232
+ )
233
+ research.joinpath("frontier.json").write_text(
234
+ json.dumps({"nodes": [{"status": "open"}, {"status": "settled"}]}),
235
+ encoding="utf-8",
236
+ )
237
+ research.joinpath("ledger", "claim_status.json").write_text(
238
+ json.dumps(
239
+ [
240
+ {
241
+ "scope_id": "s",
242
+ "claim_id": "a",
243
+ "from_status": "UNVERIFIED",
244
+ "to_status": "VERIFIED",
245
+ "reason": "",
246
+ "at": "t1",
247
+ },
248
+ {
249
+ "scope_id": "s",
250
+ "claim_id": "a",
251
+ "from_status": "VERIFIED",
252
+ "to_status": "STALE",
253
+ "reason": "retracted",
254
+ "at": "t2",
255
+ },
256
+ {
257
+ "scope_id": "s",
258
+ "claim_id": "b",
259
+ "from_status": "VERIFIED",
260
+ "to_status": "SUSPECT",
261
+ "reason": "",
262
+ "at": "t3",
263
+ },
264
+ ]
265
+ ),
266
+ encoding="utf-8",
267
+ )
268
+ research.joinpath("requeue.json").write_text(
269
+ json.dumps([{"scope_id": "s1"}, {"scope_id": "s2"}]), encoding="utf-8"
270
+ )
271
+ res = run_node(
272
+ f"""
273
+ import {{ readSwarmStatus }} from "./plugins/opencode/tui.js";
274
+ console.log(JSON.stringify(readSwarmStatus({str(root)!r})));
275
+ """
276
+ )
277
+ self.assertEqual(res.returncode, 0, f"snapshot test failed: {res.stderr}")
278
+ data = last_json_object(res.stdout)
279
+ self.assertTrue(data["workspace"])
280
+ self.assertEqual(data["mode"], "research")
281
+ self.assertEqual(data["searchEngine"], "duckduckgo")
282
+ self.assertEqual(len(data["reports"]), 1)
283
+ self.assertEqual(data["frontierOpen"], 1)
284
+ self.assertEqual(data["stale"], 1)
285
+ self.assertEqual(data["suspect"], 1)
286
+ self.assertEqual(data["requeued"], 2)
287
+
288
+
289
+ class TestOpenCodeLifecycle(unittest.TestCase):
290
+ @staticmethod
291
+ def _fixture(tmp):
292
+ root = Path(tmp)
293
+ research = root / ".research"
294
+ (research / "ledger").mkdir(parents=True)
295
+ research.joinpath("config.json").write_text(
296
+ json.dumps(
297
+ {"mode": "audit", "search_engine": "brave", "max_iterations": 3}
298
+ ),
299
+ encoding="utf-8",
300
+ )
301
+ research.joinpath("ledger", "claim_status.json").write_text(
302
+ json.dumps(
303
+ [
304
+ {
305
+ "scope_id": "s",
306
+ "claim_id": "x1",
307
+ "from_status": "VERIFIED",
308
+ "to_status": "STALE",
309
+ "reason": "retracted",
310
+ "at": "t1",
311
+ },
312
+ ]
313
+ ),
314
+ encoding="utf-8",
315
+ )
316
+ research.joinpath("requeue.json").write_text(
317
+ json.dumps([{"scope_id": "s9"}]), encoding="utf-8"
318
+ )
319
+ return str(root)
320
+
321
+ def test_idle_detection(self):
322
+ res = run_node(
323
+ """
324
+ import { isIdleEvent } from "./plugins/opencode/index.js";
325
+ console.log(JSON.stringify({
326
+ idle: isIdleEvent({type: "session.idle"}),
327
+ statusIdle: isIdleEvent({type: "session.status", properties: {status: {type: "idle"}}}),
328
+ busy: isIdleEvent({type: "session.status", properties: {status: {type: "busy"}}}),
329
+ other: isIdleEvent({type: "message.updated"}),
330
+ empty: isIdleEvent(null)
331
+ }));
332
+ """
333
+ )
334
+ self.assertEqual(res.returncode, 0, f"idle test failed: {res.stderr}")
335
+ data = last_json_object(res.stdout)
336
+ self.assertTrue(data["idle"])
337
+ self.assertTrue(data["statusIdle"])
338
+ self.assertFalse(data["busy"])
339
+ self.assertFalse(data["other"])
340
+ self.assertFalse(data["empty"])
341
+
342
+ def test_compaction_hook_pushes_state(self):
343
+ with tempfile.TemporaryDirectory() as tmp:
344
+ root = self._fixture(tmp)
345
+ res = run_node(
346
+ f"""
347
+ import plugin from "./plugins/opencode/index.js";
348
+ const shell = await plugin.server();
349
+ const hook = shell["experimental.session.compacting"];
350
+ const out = {{context: []}};
351
+ await hook({{directory: {root!r}}}, out);
352
+ const empty = {{context: []}};
353
+ await hook({{directory: "/tmp/oc-lifecycle-empty"}}, empty);
354
+ console.log(JSON.stringify({{hook: typeof hook, pushed: out.context.length, text: out.context[0] || "", skipped: empty.context.length}}));
355
+ """
356
+ )
357
+ self.assertEqual(res.returncode, 0, f"compaction test failed: {res.stderr}")
358
+ data = last_json_object(res.stdout)
359
+ self.assertEqual(data["hook"], "function")
360
+ self.assertEqual(data["pushed"], 1)
361
+ self.assertIn("mode=audit", data["text"])
362
+ self.assertIn("x1", data["text"])
363
+ self.assertIn("s9", data["text"])
364
+ self.assertEqual(data["skipped"], 0)
365
+
366
+ def test_nudge_once_per_ledger_state(self):
367
+ with tempfile.TemporaryDirectory() as tmp:
368
+ root = self._fixture(tmp)
369
+ res = run_node(
370
+ f"""
371
+ import {{ handleIdleEvent }} from "./plugins/opencode/index.js";
372
+ const prompts = [];
373
+ const host = {{session: {{prompt: async (p) => {{ prompts.push(p); return {{}}; }}}}}};
374
+ const state = {{}};
375
+ const evt = {{type: "session.idle", properties: {{sessionID: "ses_1"}}}};
376
+ const first = handleIdleEvent(host, {root!r}, state, evt);
377
+ await new Promise(r => setTimeout(r, 20));
378
+ const second = handleIdleEvent(host, {root!r}, state, evt);
379
+ const busy = handleIdleEvent(host, {root!r}, state, {{type: "message.updated"}});
380
+ console.log(JSON.stringify({{first, second, busy, prompts: prompts.length, sid: prompts[0]?.sessionID || null}}));
381
+ """
382
+ )
383
+ self.assertEqual(res.returncode, 0, f"nudge test failed: {res.stderr}")
384
+ data = last_json_object(res.stdout)
385
+ self.assertTrue(data["first"])
386
+ self.assertFalse(data["second"])
387
+ self.assertFalse(data["busy"])
388
+ self.assertEqual(data["prompts"], 1)
389
+ self.assertEqual(data["sid"], "ses_1")
390
+
391
+
392
+ if __name__ == "__main__":
393
+ unittest.main()
@@ -318,15 +318,20 @@ In our experiments, the 70B parameter model was trained on 15.0 trillion tokens.
318
318
 
319
319
  # 1. Test OpenCode Plugin registration (A2b: thin forwarders + both
320
320
  # name exports — canonical IUMBTEMS_TOOL_NAMES and the deprecated
321
- # IUMBEMS_TOOL_NAMES alias).
321
+ # IUMBEMS_TOOL_NAMES alias). Object form: server() resolves to
322
+ # {config, tool} with the tool map keyed by name.
322
323
  node_code_oc = """
323
324
  import plugin, { IUMBTEMS_TOOL_NAMES, IUMBEMS_TOOL_NAMES } from "./plugins/opencode/index.js";
324
- const tools = plugin.server();
325
+ const shell = await plugin.server();
326
+ const tools = Object.values(shell.tool);
325
327
  const names = tools.map(t => t.name);
328
+ const cfg = {};
329
+ shell.config(cfg);
326
330
  console.log(JSON.stringify({
327
331
  names,
328
332
  canonical: IUMBTEMS_TOOL_NAMES,
329
- alias: IUMBEMS_TOOL_NAMES
333
+ alias: IUMBEMS_TOOL_NAMES,
334
+ commands: Object.keys(cfg.command || {})
330
335
  }));
331
336
  """
332
337
  res_oc = subprocess.run(
@@ -365,6 +370,17 @@ In our experiments, the 70B parameter model was trained on 15.0 trillion tokens.
365
370
  ]
366
371
  for t in expected_tools:
367
372
  self.assertIn(t, tools)
373
+ # Slash-command catalog registered via the server config hook.
374
+ for c in [
375
+ "swarm",
376
+ "grill",
377
+ "swarm-config",
378
+ "audit",
379
+ "scout",
380
+ "brainstorming",
381
+ "brainstorm",
382
+ ]:
383
+ self.assertIn(c, data_oc["commands"])
368
384
 
369
385
  # 2. Test Pi Extension registration
370
386
  node_code_pi = """