@heretek-ai/epistemic-swarm 0.4.4 → 0.4.6

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/package.json +1 -1
  3. package/plugins/antigravity/plugin.json +1 -1
  4. package/plugins/gemini/gemini-extension.json +1 -1
  5. package/plugins/opencode/index.js +85 -8
  6. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  7. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  8. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  9. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  10. package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
  11. package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
  12. package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
  13. package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
  14. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  15. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  16. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  17. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  18. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  19. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  20. package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
  21. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  22. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  23. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  24. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  25. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  26. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  27. package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
  28. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  29. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  30. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  31. package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
  32. package/runner/tests/test_opencode_ux.py +124 -0
  33. package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
  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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epistemic-swarm",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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.4.4",
3
+ "version": "0.4.6",
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": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://antigravity.google/schemas/v1/plugin.json",
3
3
  "name": "epistemic-swarm",
4
- "version": "0.4.4",
4
+ "version": "0.4.6",
5
5
  "description": "IUMBTEMS Epistemic Swarm: dialectic research, code audit, OSS scout, and lateral brainstorming for AntiGravity.",
6
6
  "author": "Heretek AI",
7
7
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epistemic-swarm",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "IUMBTEMS Epistemic Swarm: dialectic research, code audit, OSS scout, and lateral brainstorming for Gemini CLI.",
5
5
  "mcpServers": {
6
6
  "brave-search": {
@@ -563,6 +563,10 @@ export function buildCompactionContext(root) {
563
563
 
564
564
  const NUDGE_MIN_INTERVAL_MS = 5 * 60 * 1000;
565
565
  const activeNudges = new Map();
566
+ // Cleanup functions for the setup() disposer (a setup that returns a plain
567
+ // object crashes host reload: the host calls the return value as a cleanup
568
+ // function). All setup-owned resources register here.
569
+ const setupCleanups = new Set();
566
570
 
567
571
  /**
568
572
  * Handle one subscribed event: nudge once per ledger change when degraded
@@ -659,7 +663,8 @@ export function createOpenCodePlugin(context = {}) {
659
663
  }),
660
664
 
661
665
  setup: async (appContext) => {
662
- const opts = context.options || appContext?.options;
666
+ const host = appContext || {};
667
+ const opts = context.options || host.options;
663
668
  if (opts && typeof opts === 'object') {
664
669
  const updates = {};
665
670
  if (opts.search_engine) updates.search_engine = opts.search_engine;
@@ -669,25 +674,97 @@ export function createOpenCodePlugin(context = {}) {
669
674
  updates.divergence_threshold = opts.divergence_threshold;
670
675
  }
671
676
  if (Object.keys(updates).length > 0) {
672
- await callMcp('iumbtems_config', updates, appContext?.cwd);
677
+ await callMcp('iumbtems_config', updates, host.cwd);
673
678
  }
674
679
  }
675
680
  // Phase C: idle staleness nudge (opt out with {staleness_nudge: false}).
676
681
  // Subscribes only when the host exposes event/session; one per root.
677
682
  try {
678
683
  if ((opts?.staleness_nudge ?? true)) {
679
- const host = {
680
- event: appContext?.event || context.event,
681
- session: appContext?.session || context.session,
684
+ const nudgeHost = {
685
+ event: host.event || context.event,
686
+ session: host.session || context.session,
682
687
  };
683
688
  const root =
684
- appContext?.location?.directory || context.location?.directory;
685
- if (root) startStalenessNudge(host, root);
689
+ host.location?.directory || context.location?.directory;
690
+ if (root) {
691
+ const stop = startStalenessNudge(nudgeHost, root);
692
+ if (typeof stop === 'function') setupCleanups.add(stop);
693
+ }
686
694
  }
687
695
  } catch {
688
696
  /* lifecycle hooks are best-effort */
689
697
  }
690
- return { initialized: true, platform: 'opencode-v2' };
698
+ // V2 host transforms (official dual-package pattern: V1 calls server(),
699
+ // V2 calls setup()). Registers slash commands + tools via the owning
700
+ // domains; each guarded so V1 hosts and partial V2 hosts keep working.
701
+ try {
702
+ if (host.command && typeof host.command.transform === 'function') {
703
+ let existing = new Set();
704
+ try {
705
+ const listed = await host.command.list();
706
+ existing = new Set(((listed && listed.data) || []).map((c) => c && c.name));
707
+ } catch {
708
+ /* treat as empty */
709
+ }
710
+ await host.command.transform((draft) => {
711
+ for (const cmd of OPENCODE_COMMANDS) {
712
+ if (!cmd || !cmd.name || existing.has(cmd.name)) continue;
713
+ existing.add(cmd.name);
714
+ draft.add({
715
+ name: cmd.name,
716
+ description: cmd.description,
717
+ execute: async (input) => {
718
+ const args = (input && input.prompt && input.prompt.text) || '';
719
+ const prompt = (input && input.prompt && typeof input.prompt === 'object') ? input.prompt : {};
720
+ await host.session.prompt({
721
+ ...prompt,
722
+ sessionID: input && input.sessionID,
723
+ text: cmd.template.split('$ARGUMENTS').join(String(args).trim()),
724
+ delivery: input && input.delivery,
725
+ });
726
+ },
727
+ });
728
+ }
729
+ });
730
+ }
731
+ } catch {
732
+ /* command transform is best-effort */
733
+ }
734
+ try {
735
+ if (host.tool && typeof host.tool.transform === 'function') {
736
+ await host.tool.transform((draft) => {
737
+ for (const [name, spec] of Object.entries(buildToolMap())) {
738
+ draft.add({
739
+ name,
740
+ description: spec.description,
741
+ input: spec.input,
742
+ options: { codemode: false },
743
+ execute: async (args = {}, toolContext) => {
744
+ const r = await callMcp(name, normalizeArgs(name, args), toolContext?.cwd);
745
+ return { content: r.content };
746
+ },
747
+ });
748
+ }
749
+ });
750
+ }
751
+ } catch {
752
+ /* tool transform is best-effort */
753
+ }
754
+ // Disposer: the host calls setup's return value as a cleanup function
755
+ // on unload/reload. It MUST be a function (returning a plain object
756
+ // crashes host reload with "not a function"). Aborts nudge streams so
757
+ // reloads never stack duplicate subscriptions.
758
+ return () => {
759
+ for (const stop of [...setupCleanups]) {
760
+ setupCleanups.delete(stop);
761
+ try {
762
+ stop();
763
+ } catch {
764
+ /* cleanup is best-effort */
765
+ }
766
+ }
767
+ };
691
768
  },
692
769
  };
693
770
  }
@@ -465,5 +465,129 @@ class TestOpenCodeLifecycle(unittest.TestCase):
465
465
  self.assertEqual(data["sid"], "ses_1")
466
466
 
467
467
 
468
+ class TestOpenCodeV2Transforms(unittest.TestCase):
469
+ """V2 host (setup ctx transforms): commands + tools via owning domains."""
470
+
471
+ V2_SETUP = """
472
+ import plugin from "./plugins/opencode/index.js";
473
+ const addedCommands = [];
474
+ const addedTools = [];
475
+ const prompts = [];
476
+ const host = {
477
+ options: {},
478
+ command: {
479
+ list: async () => ({data: [{name: "goal"}]}),
480
+ transform: async (fn) => { fn({add: (d) => addedCommands.push(d)}); return () => {}; }
481
+ },
482
+ tool: {
483
+ transform: async (fn) => { fn({add: (t) => addedTools.push(t)}); return () => {}; }
484
+ },
485
+ session: { prompt: async (p) => { prompts.push(p); return {}; } }
486
+ };
487
+ const ret = await plugin.setup(host);
488
+ %s
489
+ """
490
+
491
+ def test_v2_registers_commands_and_tools(self):
492
+ res = run_node(
493
+ self.V2_SETUP
494
+ % """
495
+ console.log(JSON.stringify({
496
+ cleanupFn: typeof ret === "function",
497
+ commands: addedCommands.map(c => c.name),
498
+ cmdExec: typeof addedCommands.find(c => c.name === "swarm").execute,
499
+ tools: addedTools.map(t => t.name),
500
+ toolExec: typeof addedTools.find(t => t.name === "iumbtems_config").execute
501
+ }));
502
+ """
503
+ )
504
+ self.assertEqual(res.returncode, 0, f"v2 transform test failed: {res.stderr}")
505
+ data = last_json_object(res.stdout)
506
+ self.assertEqual(data["cleanupFn"], True)
507
+ for c in [
508
+ "swarm",
509
+ "grill",
510
+ "swarm-config",
511
+ "audit",
512
+ "scout",
513
+ "brainstorming",
514
+ "brainstorm",
515
+ ]:
516
+ self.assertIn(c, data["commands"])
517
+ self.assertNotIn("goal", data["commands"])
518
+ self.assertEqual(data["cmdExec"], "function")
519
+ self.assertEqual(len(data["tools"]), 13)
520
+ self.assertIn("iumbtems_brainstorm", data["tools"])
521
+ self.assertEqual(data["toolExec"], "function")
522
+
523
+ def test_v2_command_execute_forwards_prompt(self):
524
+ res = run_node(
525
+ self.V2_SETUP
526
+ % """
527
+ const swarm = addedCommands.find(c => c.name === "swarm");
528
+ await swarm.execute({sessionID: "ses_9", prompt: {text: "probe objective"}, delivery: "steer"});
529
+ console.log(JSON.stringify({prompts}));
530
+ """
531
+ )
532
+ self.assertEqual(res.returncode, 0, f"v2 execute test failed: {res.stderr}")
533
+ data = last_json_object(res.stdout)["prompts"]
534
+ self.assertEqual(len(data), 1)
535
+ self.assertEqual(data[0]["sessionID"], "ses_9")
536
+ self.assertIn("probe objective", data[0]["text"])
537
+ self.assertNotIn("$ARGUMENTS", data[0]["text"])
538
+ self.assertEqual(data[0]["delivery"], "steer")
539
+
540
+ def test_v2_tool_execute_returns_content(self):
541
+ res = run_node(
542
+ self.V2_SETUP
543
+ % """
544
+ const cfg = addedTools.find(t => t.name === "iumbtems_config");
545
+ const r = await cfg.execute({}, {});
546
+ console.log(JSON.stringify({keys: Object.keys(r), status: r.status, hasContent: typeof r.content === "string"}));
547
+ """
548
+ )
549
+ self.assertEqual(res.returncode, 0, f"v2 tool test failed: {res.stderr}")
550
+ data = last_json_object(res.stdout)
551
+ self.assertEqual(data["keys"], ["content"])
552
+ self.assertTrue(data["hasContent"])
553
+
554
+ def test_setup_without_v2_domains_still_works(self):
555
+ res = run_node(
556
+ """
557
+ import plugin from "./plugins/opencode/index.js";
558
+ const ret = await plugin.setup({});
559
+ ret();
560
+ ret();
561
+ console.log(JSON.stringify({cleanupFn: typeof ret === "function"}));
562
+ """
563
+ )
564
+ self.assertEqual(res.returncode, 0, f"bare setup test failed: {res.stderr}")
565
+ data = last_json_object(res.stdout)
566
+ self.assertEqual(data["cleanupFn"], True)
567
+
568
+ def test_setup_cleanup_aborts_nudge_streams(self):
569
+ res = run_node(
570
+ """
571
+ import plugin from "./plugins/opencode/index.js";
572
+ const seen = [];
573
+ const host = {
574
+ options: {},
575
+ location: {directory: "/tmp/oc-nudge-cleanup"},
576
+ event: {subscribe: (opts) => { seen.push(opts && opts.signal); return {[Symbol.asyncIterator]() { return {next: async () => ({done: true})}; }}; }},
577
+ session: {prompt: async () => ({})}
578
+ };
579
+ const cleanup = await plugin.setup(host);
580
+ const before = seen.length;
581
+ cleanup();
582
+ console.log(JSON.stringify({subscribed: before > 0, cleanupFn: typeof cleanup === "function", aborted: seen.length > 0 && seen[0].aborted === true}));
583
+ """
584
+ )
585
+ self.assertEqual(res.returncode, 0, f"cleanup test failed: {res.stderr}")
586
+ data = last_json_object(res.stdout)
587
+ self.assertTrue(data["subscribed"])
588
+ self.assertTrue(data["cleanupFn"])
589
+ self.assertTrue(data["aborted"])
590
+
591
+
468
592
  if __name__ == "__main__":
469
593
  unittest.main()