@heretek-ai/epistemic-swarm 0.4.7 → 0.4.8
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/.claude-plugin/plugin.json +1 -1
- package/bin/cli.js +59 -7
- package/config/mcp_launcher.py +75 -48
- package/extensions/pi/index.js +25 -5
- package/install.sh +5 -5
- package/package.json +1 -1
- package/plugins/antigravity/plugin.json +1 -1
- package/plugins/gemini/gemini-extension.json +1 -1
- package/plugins/opencode/index.js +308 -142
- package/plugins/opencode/tui.js +7 -9
- package/plugins/research-cache/skills/research_cache/hasher.py +27 -9
- package/plugins/socratic-grilling/skills/grilling/socratic_tree.py +26 -18
- 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__/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__/refinement.cpython-311.pyc +0 -0
- package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
- package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
- package/runner/auditor_engine.py +122 -102
- package/runner/claim_store.py +5 -4
- package/runner/living_dossiers.py +32 -10
- package/runner/mcp_protocol.py +1 -1
- package/runner/mcp_server.py +23 -11
- package/runner/path_safety.py +62 -0
- package/runner/pcrb.py +68 -39
- package/runner/pcrb_verify.py +82 -52
- package/runner/refinement.py +61 -47
- package/runner/research_swarm.py +61 -7
- package/runner/state_machine.py +9 -6
- package/runner/tests/__pycache__/test_auction_order.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_domain_packs.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm.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/test_mcp_server.py +0 -1
- package/runner/tests/test_opencode_ux.py +368 -33
- package/runner/tests/test_swarm.py +20 -9
- package/runner/tests/test_sweep_regressions.py +373 -0
- package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
- package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
- package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
- package/scripts/auction_experiment.py +38 -30
- package/scripts/bet1_advisory_spike.py +41 -25
- package/scripts/build_adapters.py +52 -37
- package/scripts/divergence_experiment.py +61 -31
- package/skills/brainstorming/scripts/brainstorm.py +3 -2
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/epistemic_search/scripts/fetch.py +0 -1
- package/skills/epistemic_search/scripts/search.py +202 -73
- package/skills/epistemic_search/scripts/webcache.py +1 -1
- package/skills/grilling/socratic_tree.py +26 -18
- 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 +27 -9
- 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 +32 -28
|
@@ -42,18 +42,47 @@ def last_json_object(stdout):
|
|
|
42
42
|
return json.loads(lines[-1])
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
# Shared V2 mock: captures the tool map produced by `tool.transform` without a
|
|
46
|
+
# real host. The old tests drove `plugin.server()` — a V1 hook that no longer
|
|
47
|
+
# exists — so every tool assertion has to go through the V2 domain instead.
|
|
48
|
+
V2_TOOL_SETUP = """
|
|
49
|
+
import plugin from "./plugins/opencode/index.js";
|
|
50
|
+
const toolMap = {};
|
|
51
|
+
const host = {
|
|
52
|
+
options: {},
|
|
53
|
+
command: {
|
|
54
|
+
list: async () => ({data: []}),
|
|
55
|
+
transform: async () => ({dispose: () => {}}),
|
|
56
|
+
reload: async () => {}
|
|
57
|
+
},
|
|
58
|
+
tool: {
|
|
59
|
+
transform: async (fn) => {
|
|
60
|
+
fn({add: (t) => { toolMap[t.name] = t; }});
|
|
61
|
+
return {dispose: () => {}};
|
|
62
|
+
},
|
|
63
|
+
reload: async () => {}
|
|
64
|
+
},
|
|
65
|
+
session: { prompt: async () => ({}) }
|
|
66
|
+
};
|
|
67
|
+
await plugin.setup(host);
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
|
|
45
71
|
class TestOpenCodeCommandCatalog(unittest.TestCase):
|
|
46
72
|
def test_catalog_shape_and_registration(self):
|
|
73
|
+
# V2 only: the V1 `server()` config hook wrote the deprecated `command`
|
|
74
|
+
# key and registered nothing on a v2 host. The declarative catalog is
|
|
75
|
+
# now exposed as `commandCatalog()` (V2 key: `commands`); live hosts
|
|
76
|
+
# register through `command.transform` — covered in
|
|
77
|
+
# TestOpenCodeV2Transforms.
|
|
47
78
|
res = run_node(
|
|
48
79
|
"""
|
|
49
|
-
import
|
|
50
|
-
const
|
|
51
|
-
const cfg = {};
|
|
52
|
-
shell.config(cfg);
|
|
80
|
+
import { OPENCODE_COMMANDS, commandCatalog } from "./plugins/opencode/index.js";
|
|
81
|
+
const catalog = commandCatalog();
|
|
53
82
|
console.log(JSON.stringify({
|
|
54
83
|
catalog: OPENCODE_COMMANDS.map(c => ({name: c.name, hasDesc: !!c.description, hasArgs: c.template.includes("$ARGUMENTS")})),
|
|
55
|
-
registered: Object.keys(
|
|
56
|
-
templates: Object.fromEntries(Object.entries(
|
|
84
|
+
registered: Object.keys(catalog),
|
|
85
|
+
templates: Object.fromEntries(Object.entries(catalog).map(([k, v]) => [k, !!v.description && !!v.template]))
|
|
57
86
|
}));
|
|
58
87
|
"""
|
|
59
88
|
)
|
|
@@ -75,6 +104,25 @@ class TestOpenCodeCommandCatalog(unittest.TestCase):
|
|
|
75
104
|
self.assertIn(c, data["registered"])
|
|
76
105
|
self.assertTrue(data["templates"][c])
|
|
77
106
|
|
|
107
|
+
def test_catalog_uses_v2_commands_key(self):
|
|
108
|
+
"""`command` is the deprecated V1 key and is ignored by a V2 host."""
|
|
109
|
+
res = run_node(
|
|
110
|
+
"""
|
|
111
|
+
import plugin from "./plugins/opencode/index.js";
|
|
112
|
+
console.log(JSON.stringify({
|
|
113
|
+
hasServer: typeof plugin.server,
|
|
114
|
+
hasSetup: typeof plugin.setup,
|
|
115
|
+
hasConfigHook: false
|
|
116
|
+
}));
|
|
117
|
+
"""
|
|
118
|
+
)
|
|
119
|
+
self.assertEqual(res.returncode, 0, f"entrypoint shape test failed: {res.stderr}")
|
|
120
|
+
data = last_json_object(res.stdout)
|
|
121
|
+
# V1 `server()` is gone: a v2 host never calls it, and keeping it meant
|
|
122
|
+
# the real registration path had to guess which host it was talking to.
|
|
123
|
+
self.assertNotEqual(data["hasServer"], "function")
|
|
124
|
+
self.assertEqual(data["hasSetup"], "function")
|
|
125
|
+
|
|
78
126
|
def test_empty_arg_guards(self):
|
|
79
127
|
res = run_node(
|
|
80
128
|
"""
|
|
@@ -91,43 +139,64 @@ class TestOpenCodeCommandCatalog(unittest.TestCase):
|
|
|
91
139
|
self.assertEqual(len(data["withUsage"]), data["total"])
|
|
92
140
|
|
|
93
141
|
def test_async_execute_resolves(self):
|
|
142
|
+
# V2 tool results are `{content}` only (the V1 map added a `status`
|
|
143
|
+
# field, which the V2 domain does not have).
|
|
94
144
|
res = run_node(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const shell = await plugin.server();
|
|
145
|
+
V2_TOOL_SETUP
|
|
146
|
+
+ """
|
|
98
147
|
let ticked = false;
|
|
99
148
|
const timer = setInterval(() => { ticked = true; }, 5);
|
|
100
|
-
const r = await
|
|
149
|
+
const r = await toolMap.iumbtems_config.execute({});
|
|
101
150
|
clearInterval(timer);
|
|
102
|
-
console.log(JSON.stringify({
|
|
151
|
+
console.log(JSON.stringify({
|
|
152
|
+
hasContent: typeof r.content === "string" && r.content.length > 0,
|
|
153
|
+
shapeOk: Object.keys(r).sort().join(",") === "content",
|
|
154
|
+
loopAlive: ticked
|
|
155
|
+
}));
|
|
103
156
|
"""
|
|
104
157
|
)
|
|
105
158
|
self.assertEqual(res.returncode, 0, f"async execute test failed: {res.stderr}")
|
|
106
159
|
data = last_json_object(res.stdout)
|
|
107
|
-
self.assertEqual(data["status"], "success")
|
|
108
160
|
self.assertTrue(data["hasContent"])
|
|
161
|
+
self.assertTrue(data["shapeOk"])
|
|
109
162
|
self.assertTrue(data["loopAlive"])
|
|
110
163
|
|
|
111
|
-
def
|
|
164
|
+
def test_v2_skips_preexisting_command_names(self):
|
|
165
|
+
"""Names already present at setup time are respected, not overwritten.
|
|
166
|
+
|
|
167
|
+
This is the dedupe half of the replay fix: the skip set must be a
|
|
168
|
+
snapshot taken at setup, not a set that grows across transform replays.
|
|
169
|
+
"""
|
|
112
170
|
res = run_node(
|
|
113
171
|
"""
|
|
114
|
-
import
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
172
|
+
import plugin from "./plugins/opencode/index.js";
|
|
173
|
+
const added = [];
|
|
174
|
+
const host = {
|
|
175
|
+
options: {},
|
|
176
|
+
command: {
|
|
177
|
+
list: async () => ({data: [{name: "swarm"}]}),
|
|
178
|
+
transform: async (fn) => { fn({add: (d) => added.push(d)}); return {dispose: () => {}}; },
|
|
179
|
+
reload: async () => {}
|
|
180
|
+
},
|
|
181
|
+
tool: { transform: async () => ({dispose: () => {}}) },
|
|
182
|
+
session: { prompt: async () => ({}) }
|
|
183
|
+
};
|
|
184
|
+
await plugin.setup(host);
|
|
185
|
+
console.log(JSON.stringify({names: added.map(c => c.name)}));
|
|
118
186
|
"""
|
|
119
187
|
)
|
|
120
|
-
self.assertEqual(res.returncode, 0, f"
|
|
188
|
+
self.assertEqual(res.returncode, 0, f"preexisting-name test failed: {res.stderr}")
|
|
121
189
|
data = last_json_object(res.stdout)
|
|
122
|
-
|
|
123
|
-
self.
|
|
190
|
+
# `swarm` was already registered by the user, so we add the other six.
|
|
191
|
+
self.assertNotIn("swarm", data["names"])
|
|
192
|
+
self.assertEqual(len(data["names"]), len(EXPECTED_COMMANDS) - 1)
|
|
124
193
|
|
|
125
194
|
def test_tool_map_shape(self):
|
|
126
195
|
res = run_node(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const entries = Object.entries(
|
|
196
|
+
V2_TOOL_SETUP
|
|
197
|
+
+ """
|
|
198
|
+
import { IUMBTEMS_TOOL_NAMES } from "./plugins/opencode/index.js";
|
|
199
|
+
const entries = Object.entries(toolMap);
|
|
131
200
|
console.log(JSON.stringify({
|
|
132
201
|
keys: entries.map(([k]) => k),
|
|
133
202
|
canonical: IUMBTEMS_TOOL_NAMES,
|
|
@@ -416,18 +485,36 @@ class TestOpenCodeLifecycle(unittest.TestCase):
|
|
|
416
485
|
self.assertFalse(data["empty"])
|
|
417
486
|
|
|
418
487
|
def test_compaction_hook_pushes_state(self):
|
|
488
|
+
# V2 shape: `session.hook("compaction", fn)` pushes a text part onto
|
|
489
|
+
# `event.system`. The old `experimental.session.compacting` hook lived
|
|
490
|
+
# in the V1 `server()` factory and was dead code on a V2 host.
|
|
419
491
|
with tempfile.TemporaryDirectory() as tmp:
|
|
420
492
|
root = self._fixture(tmp)
|
|
421
493
|
res = run_node(
|
|
422
494
|
f"""
|
|
423
495
|
import plugin from "./plugins/opencode/index.js";
|
|
424
|
-
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
496
|
+
let handler = null;
|
|
497
|
+
const host = {{
|
|
498
|
+
options: {{}},
|
|
499
|
+
location: {{directory: {root!r}}},
|
|
500
|
+
command: {{list: async () => ({{data: []}}), transform: async () => ({{dispose: async () => {{}}}}), reload: async () => {{}}}},
|
|
501
|
+
tool: {{transform: async () => ({{dispose: async () => {{}}}}), reload: async () => {{}}}},
|
|
502
|
+
session: {{
|
|
503
|
+
prompt: async () => ({{}}),
|
|
504
|
+
hook: async (name, fn) => {{ if (name === "compaction") handler = fn; return {{dispose: async () => {{}}}}; }}
|
|
505
|
+
}}
|
|
506
|
+
}};
|
|
507
|
+
await plugin.setup(host);
|
|
508
|
+
const out = {{system: [], directory: {root!r}}};
|
|
509
|
+
if (handler) await handler(out);
|
|
510
|
+
const empty = {{system: [], directory: "/tmp/oc-lifecycle-empty-nonexistent"}};
|
|
511
|
+
if (handler) await handler(empty);
|
|
512
|
+
console.log(JSON.stringify({{
|
|
513
|
+
hook: typeof handler,
|
|
514
|
+
pushed: out.system.length,
|
|
515
|
+
text: (out.system[0] && out.system[0].text) || "",
|
|
516
|
+
skipped: empty.system.length
|
|
517
|
+
}}));
|
|
431
518
|
"""
|
|
432
519
|
)
|
|
433
520
|
self.assertEqual(res.returncode, 0, f"compaction test failed: {res.stderr}")
|
|
@@ -503,7 +590,7 @@ class TestOpenCodeV2Transforms(unittest.TestCase):
|
|
|
503
590
|
)
|
|
504
591
|
self.assertEqual(res.returncode, 0, f"v2 transform test failed: {res.stderr}")
|
|
505
592
|
data = last_json_object(res.stdout)
|
|
506
|
-
self.
|
|
593
|
+
self.assertTrue(data["cleanupFn"])
|
|
507
594
|
for c in [
|
|
508
595
|
"swarm",
|
|
509
596
|
"grill",
|
|
@@ -563,7 +650,7 @@ class TestOpenCodeV2Transforms(unittest.TestCase):
|
|
|
563
650
|
)
|
|
564
651
|
self.assertEqual(res.returncode, 0, f"bare setup test failed: {res.stderr}")
|
|
565
652
|
data = last_json_object(res.stdout)
|
|
566
|
-
self.
|
|
653
|
+
self.assertTrue(data["cleanupFn"])
|
|
567
654
|
|
|
568
655
|
def test_options_applied_to_location_dir_after_transforms(self):
|
|
569
656
|
import tempfile
|
|
@@ -618,6 +705,254 @@ class TestOpenCodeV2Transforms(unittest.TestCase):
|
|
|
618
705
|
self.assertTrue(data["cleanupFn"])
|
|
619
706
|
self.assertTrue(data["aborted"])
|
|
620
707
|
|
|
708
|
+
# ------------------------------------------------------------------
|
|
709
|
+
# Regression tests for the vanishing-slash-commands defect.
|
|
710
|
+
#
|
|
711
|
+
# The host REPLAYS a command transform callback on every registry rebuild.
|
|
712
|
+
# The old code kept one `existing` Set closed over from setup and mutated it
|
|
713
|
+
# inside the callback, so replay 2 saw every name as "already added" and
|
|
714
|
+
# registered nothing — commands appeared at startup and vanished seconds
|
|
715
|
+
# later. The mock used to invoke the callback exactly once, so the suite was
|
|
716
|
+
# structurally blind to it.
|
|
717
|
+
# ------------------------------------------------------------------
|
|
718
|
+
|
|
719
|
+
REPLAY_SETUP = """
|
|
720
|
+
import plugin from "./plugins/opencode/index.js";
|
|
721
|
+
const passes = [];
|
|
722
|
+
let captured = null;
|
|
723
|
+
const host = {
|
|
724
|
+
options: {},
|
|
725
|
+
command: {
|
|
726
|
+
// Emulate a host whose registry is rebuilt from base config on
|
|
727
|
+
// each pass: our commands are NOT pre-existing at setup time,
|
|
728
|
+
// and are wiped before every replay.
|
|
729
|
+
list: async () => ({data: [{name: "goal"}]}),
|
|
730
|
+
transform: async (fn) => { captured = fn; return {dispose: () => {}}; },
|
|
731
|
+
reload: async () => {}
|
|
732
|
+
},
|
|
733
|
+
tool: {
|
|
734
|
+
transform: async () => ({dispose: () => {}}),
|
|
735
|
+
reload: async () => {}
|
|
736
|
+
},
|
|
737
|
+
session: { prompt: async () => ({}) }
|
|
738
|
+
};
|
|
739
|
+
const cleanup = await plugin.setup(host);
|
|
740
|
+
for (let i = 0; i < 3; i++) {
|
|
741
|
+
const draftAdds = [];
|
|
742
|
+
captured({add: (d) => draftAdds.push(d)});
|
|
743
|
+
passes.push(draftAdds.map((d) => d.name));
|
|
744
|
+
}
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
def test_v2_command_transform_is_replay_safe(self):
|
|
748
|
+
"""Every replay must re-add all commands. This is the vanish bug."""
|
|
749
|
+
res = run_node(
|
|
750
|
+
self.REPLAY_SETUP
|
|
751
|
+
+ """
|
|
752
|
+
console.log(JSON.stringify({passes}));
|
|
753
|
+
"""
|
|
754
|
+
)
|
|
755
|
+
self.assertEqual(res.returncode, 0, f"replay test failed: {res.stderr}")
|
|
756
|
+
passes = last_json_object(res.stdout)["passes"]
|
|
757
|
+
self.assertEqual(len(passes), 3)
|
|
758
|
+
for i, added in enumerate(passes):
|
|
759
|
+
self.assertEqual(
|
|
760
|
+
len(added),
|
|
761
|
+
len(EXPECTED_COMMANDS),
|
|
762
|
+
f"transform pass {i} added {len(added)} commands, expected "
|
|
763
|
+
f"{len(EXPECTED_COMMANDS)} — a replay must not skip our names "
|
|
764
|
+
f"({added})",
|
|
765
|
+
)
|
|
766
|
+
for c in EXPECTED_COMMANDS:
|
|
767
|
+
self.assertIn(c, added)
|
|
768
|
+
|
|
769
|
+
def test_v2_setup_return_is_a_function(self):
|
|
770
|
+
"""Returning a plain object aborts host reload: `de is not a function`."""
|
|
771
|
+
res = run_node(
|
|
772
|
+
REPLAY_SINGLE
|
|
773
|
+
+ """
|
|
774
|
+
console.log(JSON.stringify({type: typeof cleanup}));
|
|
775
|
+
"""
|
|
776
|
+
)
|
|
777
|
+
self.assertEqual(res.returncode, 0, f"setup-return test failed: {res.stderr}")
|
|
778
|
+
self.assertEqual(last_json_object(res.stdout)["type"], "function")
|
|
779
|
+
|
|
780
|
+
def test_v2_cleanup_disposes_registrations(self):
|
|
781
|
+
res = run_node(
|
|
782
|
+
"""
|
|
783
|
+
import plugin from "./plugins/opencode/index.js";
|
|
784
|
+
const disposed = [];
|
|
785
|
+
const mk = (label) => ({dispose: async () => { disposed.push(label); }});
|
|
786
|
+
const host = {
|
|
787
|
+
options: {},
|
|
788
|
+
command: {
|
|
789
|
+
list: async () => ({data: []}),
|
|
790
|
+
transform: async () => mk("command"),
|
|
791
|
+
reload: async () => {}
|
|
792
|
+
},
|
|
793
|
+
tool: {
|
|
794
|
+
transform: async () => mk("tool"),
|
|
795
|
+
reload: async () => {}
|
|
796
|
+
},
|
|
797
|
+
session: {
|
|
798
|
+
prompt: async () => ({}),
|
|
799
|
+
hook: async () => mk("compaction")
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
const cleanup = await plugin.setup(host);
|
|
803
|
+
await cleanup();
|
|
804
|
+
console.log(JSON.stringify({disposed}));
|
|
805
|
+
"""
|
|
806
|
+
)
|
|
807
|
+
self.assertEqual(res.returncode, 0, f"dispose test failed: {res.stderr}")
|
|
808
|
+
disposed = last_json_object(res.stdout)["disposed"]
|
|
809
|
+
# Every transform/hook handle must be disposed on teardown, otherwise a
|
|
810
|
+
# stale registration can make command.list() report names with no live
|
|
811
|
+
# handler — which is what makes the skip-check fire on the next setup.
|
|
812
|
+
self.assertIn("command", disposed)
|
|
813
|
+
self.assertIn("tool", disposed)
|
|
814
|
+
self.assertIn("compaction", disposed)
|
|
815
|
+
|
|
816
|
+
def test_v2_repeated_setup_cycles_register_both_times(self):
|
|
817
|
+
"""setup -> cleanup -> setup must re-register (the reload path)."""
|
|
818
|
+
res = run_node(
|
|
819
|
+
"""
|
|
820
|
+
import plugin from "./plugins/opencode/index.js";
|
|
821
|
+
const perSetup = [];
|
|
822
|
+
for (let cycle = 0; cycle < 2; cycle++) {
|
|
823
|
+
const added = [];
|
|
824
|
+
let captured = null;
|
|
825
|
+
const host = {
|
|
826
|
+
options: {},
|
|
827
|
+
command: {
|
|
828
|
+
list: async () => ({data: []}),
|
|
829
|
+
transform: async (fn) => { captured = fn; return {dispose: async () => {}}; },
|
|
830
|
+
reload: async () => {}
|
|
831
|
+
},
|
|
832
|
+
tool: { transform: async () => ({dispose: async () => {}}), reload: async () => {} },
|
|
833
|
+
session: { prompt: async () => ({}) }
|
|
834
|
+
};
|
|
835
|
+
const cleanup = await plugin.setup(host);
|
|
836
|
+
captured({add: (d) => added.push(d.name)});
|
|
837
|
+
perSetup.push(added);
|
|
838
|
+
await cleanup();
|
|
839
|
+
}
|
|
840
|
+
console.log(JSON.stringify({perSetup}));
|
|
841
|
+
"""
|
|
842
|
+
)
|
|
843
|
+
self.assertEqual(res.returncode, 0, f"setup-cycle test failed: {res.stderr}")
|
|
844
|
+
per_setup = last_json_object(res.stdout)["perSetup"]
|
|
845
|
+
self.assertEqual(len(per_setup), 2)
|
|
846
|
+
for i, added in enumerate(per_setup):
|
|
847
|
+
self.assertEqual(
|
|
848
|
+
len(added), len(EXPECTED_COMMANDS),
|
|
849
|
+
f"setup cycle {i} registered {len(added)} commands",
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
def test_v2_compaction_hook_preserves_state(self):
|
|
853
|
+
"""The V1 `experimental.session.compacting` hook was dead code on V2.
|
|
854
|
+
|
|
855
|
+
Uses a real fixture workspace: `buildCompactionContext` returns null
|
|
856
|
+
without `.research/config.json`, and pointing at `process.cwd()` made
|
|
857
|
+
this pass only where a developer happened to have one.
|
|
858
|
+
"""
|
|
859
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
860
|
+
root = Path(tmp)
|
|
861
|
+
rd = root / ".research"
|
|
862
|
+
rd.mkdir()
|
|
863
|
+
(rd / "config.json").write_text(
|
|
864
|
+
json.dumps({"mode": "audit", "search_engine": "duckduckgo", "max_iterations": 2}),
|
|
865
|
+
encoding="utf-8",
|
|
866
|
+
)
|
|
867
|
+
res = run_node(
|
|
868
|
+
f"""
|
|
869
|
+
import plugin from "./plugins/opencode/index.js";
|
|
870
|
+
let hookName = null;
|
|
871
|
+
let handler = null;
|
|
872
|
+
const host = {{
|
|
873
|
+
options: {{}},
|
|
874
|
+
location: {{directory: {str(root)!r}}},
|
|
875
|
+
command: {{list: async () => ({{data: []}}), transform: async () => ({{dispose: async () => {{}}}}), reload: async () => {{}}}},
|
|
876
|
+
tool: {{transform: async () => ({{dispose: async () => {{}}}}), reload: async () => {{}}}},
|
|
877
|
+
session: {{
|
|
878
|
+
prompt: async () => ({{}}),
|
|
879
|
+
hook: async (name, fn) => {{ hookName = name; handler = fn; return {{dispose: async () => {{}}}}; }}
|
|
880
|
+
}}
|
|
881
|
+
}};
|
|
882
|
+
await plugin.setup(host);
|
|
883
|
+
const event = {{system: [], directory: {str(root)!r}}};
|
|
884
|
+
if (handler) await handler(event);
|
|
885
|
+
const emptyEvent = {{system: [], directory: "/tmp/oc-v2-compaction-empty-nonexistent"}};
|
|
886
|
+
if (handler) await handler(emptyEvent);
|
|
887
|
+
console.log(JSON.stringify({{
|
|
888
|
+
hookName,
|
|
889
|
+
parts: event.system.map((p) => ({{type: p.type, text: p.text || ""}})),
|
|
890
|
+
emptySkipped: emptyEvent.system.length
|
|
891
|
+
}}));
|
|
892
|
+
"""
|
|
893
|
+
)
|
|
894
|
+
self.assertEqual(res.returncode, 0, f"compaction hook test failed: {res.stderr}")
|
|
895
|
+
data = last_json_object(res.stdout)
|
|
896
|
+
self.assertEqual(data["hookName"], "compaction")
|
|
897
|
+
self.assertEqual(data["emptySkipped"], 0, "empty workspace must not push state")
|
|
898
|
+
self.assertTrue(
|
|
899
|
+
data["parts"]
|
|
900
|
+
and data["parts"][0]["type"] == "text"
|
|
901
|
+
and "mode=audit" in data["parts"][0]["text"],
|
|
902
|
+
f"compaction hook did not push state onto event.system: {data}",
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
def test_v2_logs_never_throw_and_surface_errors(self):
|
|
906
|
+
"""Bare `catch {}` made failures invisible under --log-level all."""
|
|
907
|
+
res = run_node(
|
|
908
|
+
"""
|
|
909
|
+
import plugin from "./plugins/opencode/index.js";
|
|
910
|
+
const logged = [];
|
|
911
|
+
const origError = console.error;
|
|
912
|
+
console.error = (...a) => { logged.push(a.join(" ")); };
|
|
913
|
+
const host = {
|
|
914
|
+
options: {},
|
|
915
|
+
command: {
|
|
916
|
+
list: async () => { throw new Error("registry unavailable"); },
|
|
917
|
+
transform: async (fn) => { fn({add: () => {}}); return {dispose: async () => {}}; },
|
|
918
|
+
reload: async () => {}
|
|
919
|
+
},
|
|
920
|
+
tool: {transform: async () => ({dispose: async () => {}}), reload: async () => {}},
|
|
921
|
+
session: {prompt: async () => ({})}
|
|
922
|
+
};
|
|
923
|
+
const cleanup = await plugin.setup(host);
|
|
924
|
+
await cleanup();
|
|
925
|
+
console.error = origError;
|
|
926
|
+
console.log(JSON.stringify({
|
|
927
|
+
logged,
|
|
928
|
+
mentionsFailure: logged.some((l) => /command\\.list|registry unavailable/.test(l))
|
|
929
|
+
}));
|
|
930
|
+
"""
|
|
931
|
+
)
|
|
932
|
+
self.assertEqual(res.returncode, 0, f"logging test failed: {res.stderr}")
|
|
933
|
+
data = last_json_object(res.stdout)
|
|
934
|
+
self.assertTrue(
|
|
935
|
+
data["mentionsFailure"],
|
|
936
|
+
f"a swallowed error must be logged, not dropped: {data['logged']}",
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
# One-shot REPLAY_SETUP equivalent that also yields the cleanup function.
|
|
941
|
+
REPLAY_SINGLE = """
|
|
942
|
+
import plugin from "./plugins/opencode/index.js";
|
|
943
|
+
const host = {
|
|
944
|
+
options: {},
|
|
945
|
+
command: {
|
|
946
|
+
list: async () => ({data: []}),
|
|
947
|
+
transform: async () => ({dispose: async () => {}}),
|
|
948
|
+
reload: async () => {}
|
|
949
|
+
},
|
|
950
|
+
tool: {transform: async () => ({dispose: async () => {}}), reload: async () => {}},
|
|
951
|
+
session: {prompt: async () => ({})}
|
|
952
|
+
};
|
|
953
|
+
const cleanup = await plugin.setup(host);
|
|
954
|
+
"""
|
|
955
|
+
|
|
621
956
|
|
|
622
957
|
if __name__ == "__main__":
|
|
623
958
|
unittest.main()
|
|
@@ -41,7 +41,7 @@ In our experiments, the 70B parameter model was trained on 15.0 trillion tokens.
|
|
|
41
41
|
content=content,
|
|
42
42
|
title="Chinchilla Scaling Laws",
|
|
43
43
|
)
|
|
44
|
-
self.
|
|
44
|
+
self.assertEqual(len(shash), 64)
|
|
45
45
|
|
|
46
46
|
# 1. Exact quote match
|
|
47
47
|
verified, conf, msg = self.hasher.verify_quote(
|
|
@@ -319,19 +319,30 @@ In our experiments, the 70B parameter model was trained on 15.0 trillion tokens.
|
|
|
319
319
|
# 1. Test OpenCode Plugin registration (A2b: thin forwarders + both
|
|
320
320
|
# name exports — canonical IUMBTEMS_TOOL_NAMES and the deprecated
|
|
321
321
|
# IUMBEMS_TOOL_NAMES alias). Object form: server() resolves to
|
|
322
|
-
#
|
|
322
|
+
# V2 shape: tools + commands are registered through the owning
|
|
323
|
+
# domains inside setup(); the declarative catalog is commandCatalog().
|
|
324
|
+
# (The V1 `server()` config hook is gone — it wrote the deprecated
|
|
325
|
+
# `command` key and registered nothing on a v2 host.)
|
|
323
326
|
node_code_oc = """
|
|
324
|
-
import plugin, { IUMBTEMS_TOOL_NAMES, IUMBEMS_TOOL_NAMES } from "./plugins/opencode/index.js";
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
+
import plugin, { IUMBTEMS_TOOL_NAMES, IUMBEMS_TOOL_NAMES, commandCatalog } from "./plugins/opencode/index.js";
|
|
328
|
+
const toolMap = {};
|
|
329
|
+
const host = {
|
|
330
|
+
options: {},
|
|
331
|
+
command: {list: async () => ({data: []}), transform: async () => ({dispose: async () => {}}), reload: async () => {}},
|
|
332
|
+
tool: {
|
|
333
|
+
transform: async (fn) => { fn({add: (t) => { toolMap[t.name] = t; }}); return {dispose: async () => {}}; },
|
|
334
|
+
reload: async () => {}
|
|
335
|
+
},
|
|
336
|
+
session: {prompt: async () => ({})}
|
|
337
|
+
};
|
|
338
|
+
await plugin.setup(host);
|
|
339
|
+
const tools = Object.values(toolMap);
|
|
327
340
|
const names = tools.map(t => t.name);
|
|
328
|
-
const cfg = {};
|
|
329
|
-
shell.config(cfg);
|
|
330
341
|
console.log(JSON.stringify({
|
|
331
342
|
names,
|
|
332
343
|
canonical: IUMBTEMS_TOOL_NAMES,
|
|
333
344
|
alias: IUMBEMS_TOOL_NAMES,
|
|
334
|
-
commands: Object.keys(
|
|
345
|
+
commands: Object.keys(commandCatalog())
|
|
335
346
|
}));
|
|
336
347
|
"""
|
|
337
348
|
res_oc = subprocess.run(
|
|
@@ -370,7 +381,7 @@ In our experiments, the 70B parameter model was trained on 15.0 trillion tokens.
|
|
|
370
381
|
]
|
|
371
382
|
for t in expected_tools:
|
|
372
383
|
self.assertIn(t, tools)
|
|
373
|
-
# Slash-command catalog
|
|
384
|
+
# Slash-command catalog (V2 `commands` shape).
|
|
374
385
|
for c in [
|
|
375
386
|
"swarm",
|
|
376
387
|
"grill",
|