@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.
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/opencode/index.js +25 -5
- 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/research_swarm.py +36 -23
- 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_domain_packs.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_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_backends.py +49 -4
- 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/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +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/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heretek-ai/epistemic-swarm",
|
|
3
|
-
"version": "0.7.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/runner/research_swarm.py
CHANGED
|
@@ -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
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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":
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|