@heretek-ai/epistemic-swarm 0.7.6 → 0.7.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.
Files changed (54) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/config/opencode-snippet.json +102 -149
  3. package/install.sh +81 -58
  4. package/package.json +1 -1
  5. package/plugins/opencode/index.js +72 -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__/path_safety.cpython-311.pyc +0 -0
  15. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  16. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  17. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  18. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  19. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  20. package/runner/mcp_server.py +7 -1
  21. package/runner/research_swarm.py +65 -3
  22. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  23. package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
  24. package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
  25. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  26. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  27. package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
  28. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  29. package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
  30. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  31. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  32. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  33. package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
  34. package/runner/tests/__pycache__/test_opencode_v2_registrars.cpython-311.pyc +0 -0
  35. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  36. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  37. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  38. package/runner/tests/__pycache__/test_swarm_config.cpython-311.pyc +0 -0
  39. package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
  40. package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
  41. package/runner/tests/test_factory.py +15 -7
  42. package/runner/tests/test_opencode_ux.py +37 -27
  43. package/runner/tests/test_opencode_v2_registrars.py +113 -16
  44. package/runner/tests/test_swarm_config.py +263 -0
  45. package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
  46. package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
  47. package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
  48. package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
  49. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  50. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  51. package/skills/swarm_config/__init__.py +15 -2
  52. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  53. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
  54. package/skills/swarm_config/configure.py +103 -6
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epistemic-swarm",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
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",
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "$schema": "https://opencode.ai/config.json",
3
3
  "mcp": {
4
- "iumbtems": {
5
- "type": "local",
6
- "command": ["python3", "<IUMBTEMS_ROOT>/runner/mcp_server.py"],
7
- "enabled": true
4
+ "servers": {
5
+ "iumbtems": {
6
+ "type": "local",
7
+ "command": ["python3", "<IUMBTEMS_ROOT>/runner/mcp_server.py"],
8
+ "enabled": true
9
+ }
8
10
  }
9
11
  },
10
12
  "plugins": [
@@ -17,187 +19,138 @@
17
19
  }
18
20
  }
19
21
  ],
20
- "agent": {
22
+ "agents": {
21
23
  "code-auditor": {
22
24
  "description": "IUMBTEMS Codebase Auditor: Dialectic architecture mapping and vulnerability red-teaming with line-level proof.",
23
- "tools": {
24
- "read": true,
25
- "write": true,
26
- "grep": true,
27
- "glob": true,
28
- "iumbtems_code_audit": true,
29
- "iumbtems_config": true
30
- }
25
+ "mode": "all",
26
+ "permissions": [
27
+ { "action": "read", "resource": "*", "effect": "allow" },
28
+ { "action": "grep", "resource": "*", "effect": "allow" },
29
+ { "action": "glob", "resource": "*", "effect": "allow" },
30
+ { "action": "edit", "resource": "*", "effect": "allow" },
31
+ { "action": "shell", "resource": "*", "effect": "ask" }
32
+ ]
31
33
  },
32
34
  "oss-scout": {
33
35
  "description": "IUMBTEMS Open Source Scout: Discovers mature OSS packages, audits copyleft licensing, and generates clean-room blueprints.",
34
- "tools": {
35
- "read": true,
36
- "write": true,
37
- "grep": true,
38
- "glob": true,
39
- "iumbtems_oss_scout": true,
40
- "iumbtems_config": true
41
- }
36
+ "mode": "all",
37
+ "permissions": [
38
+ { "action": "read", "resource": "*", "effect": "allow" },
39
+ { "action": "grep", "resource": "*", "effect": "allow" },
40
+ { "action": "glob", "resource": "*", "effect": "allow" },
41
+ { "action": "edit", "resource": "*", "effect": "allow" },
42
+ { "action": "shell", "resource": "*", "effect": "ask" }
43
+ ]
42
44
  },
43
45
  "alpha-thesis": {
44
46
  "description": "IUMBTEMS Proponent Agent: Gathers corroborating primary sources, benchmark results, and empirical proofs.",
45
- "tools": {
46
- "bash": true,
47
- "read": true,
48
- "write": true,
49
- "grep": true,
50
- "glob": true,
51
- "task": true,
52
- "iumbtems_swarm_research": true,
53
- "iumbtems_verify_quote": true,
54
- "iumbtems_config": true
55
- }
47
+ "mode": "all",
48
+ "permissions": [
49
+ { "action": "read", "resource": "*", "effect": "allow" },
50
+ { "action": "grep", "resource": "*", "effect": "allow" },
51
+ { "action": "glob", "resource": "*", "effect": "allow" },
52
+ { "action": "edit", "resource": "*", "effect": "allow" },
53
+ { "action": "shell", "resource": "*", "effect": "ask" },
54
+ { "action": "subagent", "resource": "*", "effect": "allow" }
55
+ ]
56
56
  },
57
57
  "beta-redteam": {
58
58
  "description": "IUMBTEMS Adversary Agent: Active falsification, red-teaming, hunting for counter-benchmarks, and edge-case failure modes.",
59
- "tools": {
60
- "bash": true,
61
- "read": true,
62
- "write": true,
63
- "grep": true,
64
- "glob": true,
65
- "task": true,
66
- "iumbtems_swarm_research": true,
67
- "iumbtems_verify_quote": true
68
- }
59
+ "mode": "all",
60
+ "permissions": [
61
+ { "action": "read", "resource": "*", "effect": "allow" },
62
+ { "action": "grep", "resource": "*", "effect": "allow" },
63
+ { "action": "glob", "resource": "*", "effect": "allow" },
64
+ { "action": "edit", "resource": "*", "effect": "allow" },
65
+ { "action": "shell", "resource": "*", "effect": "ask" },
66
+ { "action": "subagent", "resource": "*", "effect": "allow" }
67
+ ]
69
68
  },
70
69
  "epistemic-auditor": {
71
70
  "description": "IUMBTEMS Synthesizer & Auditor: Verifies verbatim source quotes, downgrades unverified claims, and calculates divergence.",
72
- "tools": {
73
- "read": true,
74
- "write": true,
75
- "grep": true,
76
- "glob": true,
77
- "iumbtems_verify_quote": true
78
- }
71
+ "mode": "all",
72
+ "permissions": [
73
+ { "action": "read", "resource": "*", "effect": "allow" },
74
+ { "action": "grep", "resource": "*", "effect": "allow" },
75
+ { "action": "glob", "resource": "*", "effect": "allow" },
76
+ { "action": "edit", "resource": "*", "effect": "allow" }
77
+ ]
79
78
  },
80
79
  "brainstormer": {
81
80
  "description": "IUMBTEMS Lateral Brainstormer: divergent what-if features, paradigm inversions, falsifiable spikes (never bug fixes).",
82
- "tools": {
83
- "read": true,
84
- "write": true,
85
- "grep": true,
86
- "glob": true,
87
- "iumbtems_brainstorm": true,
88
- "iumbtems_config": true
89
- }
81
+ "mode": "all",
82
+ "permissions": [
83
+ { "action": "read", "resource": "*", "effect": "allow" },
84
+ { "action": "grep", "resource": "*", "effect": "allow" },
85
+ { "action": "glob", "resource": "*", "effect": "allow" },
86
+ { "action": "edit", "resource": "*", "effect": "allow" }
87
+ ]
90
88
  },
91
89
  "darkharvester": {
92
90
  "description": "IUMBTEMS Competitor Teardown: seed plus expand, product plus code matrix, per-feature harvest verdicts with SPDX attribution.",
93
- "tools": {
94
- "read": true,
95
- "write": true,
96
- "grep": true,
97
- "glob": true,
98
- "iumbtems_darkharvest": true,
99
- "iumbtems_verify_quote": true,
100
- "iumbtems_config": true
101
- }
91
+ "mode": "all",
92
+ "permissions": [
93
+ { "action": "read", "resource": "*", "effect": "allow" },
94
+ { "action": "grep", "resource": "*", "effect": "allow" },
95
+ { "action": "glob", "resource": "*", "effect": "allow" },
96
+ { "action": "edit", "resource": "*", "effect": "allow" }
97
+ ]
102
98
  },
103
99
  "manager": {
104
100
  "description": "IUMBTEMS Factory Manager: grill-gated phased builds. Owns gates, swarm dispatch, roadmap synthesis, QA tiebreaks. Never writes code.",
105
101
  "mode": "primary",
106
- "temperature": 0.2,
107
- "permission": {
108
- "edit": "deny",
109
- "bash": "ask",
110
- "task": {
111
- "*": "deny",
112
- "factory-*": "allow",
113
- "programmer": "allow",
114
- "qa-a": "allow",
115
- "qa-b": "allow",
116
- "brainstormer": "allow",
117
- "darkharvester": "allow"
118
- }
119
- },
120
- "tools": {
121
- "read": true,
122
- "write": true,
123
- "grep": true,
124
- "glob": true,
125
- "task": true,
126
- "iumbtems_brainstorm": true,
127
- "iumbtems_darkharvest": true,
128
- "iumbtems_verify_quote": true,
129
- "iumbtems_socratic_frontier": true,
130
- "iumbtems_config": true
131
- }
102
+ "system": "You are the IUMBTEMS Factory Manager. Own gate discipline: grill until the frontier is settled, run the brainstorm and darkharvest swarms, synthesize .roadmap phase dossiers, spawn the programmer per phase, run qa-a and qa-b, and tiebreak their disagreements. Never write implementation code. Use the iumbtems_factory tool for run state. Explicit user approval advances each gate.",
103
+ "permissions": [
104
+ { "action": "edit", "resource": "*", "effect": "deny" },
105
+ { "action": "shell", "resource": "*", "effect": "ask" },
106
+ { "action": "subagent", "resource": "*", "effect": "deny" },
107
+ { "action": "subagent", "resource": "programmer", "effect": "allow" },
108
+ { "action": "subagent", "resource": "qa-a", "effect": "allow" },
109
+ { "action": "subagent", "resource": "qa-b", "effect": "allow" },
110
+ { "action": "subagent", "resource": "brainstormer", "effect": "allow" },
111
+ { "action": "subagent", "resource": "darkharvester", "effect": "allow" }
112
+ ]
132
113
  },
133
114
  "programmer": {
134
115
  "description": "IUMBTEMS Factory Programmer: implements exactly one phase brief per spawn. Cites phase evidence hashes. Never invokes swarms or other programmers.",
135
116
  "mode": "subagent",
136
- "temperature": 0.3,
137
- "permission": {
138
- "edit": "allow",
139
- "bash": "ask",
140
- "task": {
141
- "*": "deny"
142
- }
143
- },
144
- "tools": {
145
- "read": true,
146
- "write": true,
147
- "bash": true,
148
- "grep": true,
149
- "glob": true,
150
- "iumbtems_verify_quote": true
151
- }
117
+ "system": "You are the IUMBTEMS Factory Programmer. Implement exactly one phase brief per spawn. Cite phase evidence hashes. Never invoke swarms or other programmers. If the brief is ambiguous or untestable, stop and ask the manager.",
118
+ "permissions": [
119
+ { "action": "edit", "resource": "*", "effect": "allow" },
120
+ { "action": "shell", "resource": "*", "effect": "ask" },
121
+ { "action": "subagent", "resource": "*", "effect": "deny" }
122
+ ]
152
123
  },
153
124
  "qa-a": {
154
125
  "description": "IUMBTEMS Factory QA (functional): verifies phase acceptance criteria pass on the real surface. Read-only plus test execution. Diverged prompt from qa-b.",
155
126
  "mode": "subagent",
156
- "temperature": 0.1,
157
- "permission": {
158
- "edit": "deny",
159
- "bash": "ask",
160
- "task": {
161
- "*": "deny"
162
- }
163
- },
164
- "tools": {
165
- "read": true,
166
- "bash": true,
167
- "grep": true,
168
- "glob": true
169
- }
127
+ "system": "You are the IUMBTEMS Factory functional QA. Verify each phase acceptance criterion on the real surface with tests and inspection. Read-only plus test execution; never edit code. Return pass|fail(reason)|conditional(note).",
128
+ "permissions": [
129
+ { "action": "edit", "resource": "*", "effect": "deny" },
130
+ { "action": "shell", "resource": "*", "effect": "ask" },
131
+ { "action": "subagent", "resource": "*", "effect": "deny" }
132
+ ]
170
133
  },
171
134
  "qa-b": {
172
135
  "description": "IUMBTEMS Factory QA (adversarial): hunts edge cases, regressions, and acceptance loopholes the functional pass missed. Read-only plus test execution. Diverged prompt from qa-a.",
173
136
  "mode": "subagent",
174
- "temperature": 0.4,
175
- "permission": {
176
- "edit": "deny",
177
- "bash": "ask",
178
- "task": {
179
- "*": "deny"
180
- }
181
- },
182
- "tools": {
183
- "read": true,
184
- "bash": true,
185
- "grep": true,
186
- "glob": true
187
- }
137
+ "system": "You are the IUMBTEMS Factory adversarial QA. Attack the phase: edge cases, regressions, vacuous acceptance criteria, error paths, resource limits. Read-only plus test execution; never edit code. Return pass|fail(reason)|conditional(note) with reproductions.",
138
+ "permissions": [
139
+ { "action": "edit", "resource": "*", "effect": "deny" },
140
+ { "action": "shell", "resource": "*", "effect": "ask" },
141
+ { "action": "subagent", "resource": "*", "effect": "deny" }
142
+ ]
188
143
  }
189
144
  },
190
- "skills": {
191
- "paths": [
192
- "./skills/grilling",
193
- "./skills/research_cache",
194
- "./skills/epistemic_search",
195
- "./skills/swarm_config",
196
- "./skills/code_audit",
197
- "./skills/oss_scout",
198
- "./skills/brainstorming",
199
- "./skills/darkharvest",
200
- "./skills/factory"
201
- ]
202
- }
145
+ "skills": [
146
+ "./skills/grilling",
147
+ "./skills/research_cache",
148
+ "./skills/epistemic_search",
149
+ "./skills/swarm_config",
150
+ "./skills/code_audit",
151
+ "./skills/oss_scout",
152
+ "./skills/brainstorming",
153
+ "./skills/darkharvest",
154
+ "./skills/factory"
155
+ ]
203
156
  }
package/install.sh CHANGED
@@ -109,7 +109,7 @@ except Exception as e:
109
109
  EOF
110
110
  fi
111
111
 
112
- # 5. OpenCode V2 integration (plugin tuple, agents, MCP server — merge only)
112
+ # 5. OpenCode V2 integration (plugins/agents/skills/mcp.servers — merge only)
113
113
  OPENCODE_JSON="$HOME/.config/opencode/opencode.json"
114
114
  echo "🔧 Merging OpenCode V2 configuration into $OPENCODE_JSON..."
115
115
  python3 - <<EOF
@@ -124,91 +124,114 @@ OPTION_KEYS = {"search_engine", "max_iterations", "mode", "divergence_threshold"
124
124
  oc_path.parent.mkdir(parents=True, exist_ok=True)
125
125
  cfg = json.loads(oc_path.read_text(encoding="utf-8")) if oc_path.exists() else {}
126
126
 
127
- # --- plugin entry: tuple form [package, options]; repair bare-object form ---
128
- plugins = cfg.get("plugin", [])
127
+ # --- plugins (V2): object form {"package", "options"}; migrate legacy `plugin` ---
128
+ plugins = cfg.get("plugins")
129
129
  if not isinstance(plugins, list):
130
- print(" ⚠️ 'plugin' is not a list; leaving untouched")
131
- plugins = cfg.get("plugin", [])
132
- else:
133
- repaired = []
130
+ plugins = []
131
+
132
+ # Migrate any legacy V1 `plugin` entries: bare strings, [pkg, options] tuples,
133
+ # and the "wild" malformation where a bare options object trails our package.
134
+ legacy = cfg.get("plugin")
135
+ if isinstance(legacy, list):
134
136
  i = 0
135
- changed = False
136
- while i < len(plugins):
137
- item = plugins[i]
138
- # Bare options object following our package string (or standalone):
139
- # fold into a tuple instead of leaving an invalid entry.
140
- if isinstance(item, dict) and set(item) & OPTION_KEYS:
141
- prev_is_pkg = repaired and repaired[-1] == PKG
142
- prev_is_tuple = (
143
- repaired and isinstance(repaired[-1], list)
144
- and repaired[-1] and repaired[-1][0] == PKG
145
- )
146
- if prev_is_pkg:
147
- repaired[-1] = [PKG, item]
148
- changed = True
149
- print(" ~ Repaired bare options object into tuple entry")
150
- elif prev_is_tuple:
151
- merged = {**(repaired[-1][1] if len(repaired[-1]) > 1 else {}), **item}
152
- repaired[-1] = [PKG, merged]
153
- changed = True
154
- print(" ~ Folded stray options into existing tuple entry")
137
+ while i < len(legacy):
138
+ item = legacy[i]
139
+ if isinstance(item, str):
140
+ if item == PKG and i + 1 < len(legacy) and isinstance(legacy[i + 1], dict) and (set(legacy[i + 1]) & OPTION_KEYS):
141
+ plugins.append({"package": PKG, "options": legacy[i + 1]})
142
+ i += 2
143
+ continue
144
+ plugins.append(item)
145
+ elif isinstance(item, list) and item:
146
+ opts = item[1] if len(item) > 1 and isinstance(item[1], dict) else {}
147
+ plugins.append({"package": item[0], "options": opts})
148
+ elif isinstance(item, dict):
149
+ if "package" in item:
150
+ plugins.append(item)
151
+ elif set(item) & OPTION_KEYS:
152
+ plugins.append({"package": PKG, "options": item})
155
153
  else:
156
- repaired.append([PKG, item])
157
- changed = True
158
- print(" ~ Attached stray options object to new tuple entry")
159
- else:
160
- repaired.append(item)
154
+ plugins.append(item)
161
155
  i += 1
162
- if not any((p == PKG or (isinstance(p, list) and p and p[0] == PKG)) for p in repaired):
163
- repaired.append([PKG, {"search_engine": "duckduckgo", "max_iterations": 2, "mode": "research"}])
164
- changed = True
165
- print(f" + Added plugin entry: {PKG}")
166
- else:
167
- print(f" ℹ️ Plugin entry already configured: {PKG}")
168
- cfg["plugin"] = repaired
169
-
170
- # --- agents: merge snippet definitions, never overwrite user agents ---
156
+ cfg.pop("plugin", None)
157
+ print(" ~ Migrated legacy 'plugin' entries to V2 'plugins' (object form)")
158
+
159
+ # Normalize our entry to the V2 object form and ensure default options exist.
160
+ DEFAULT_OPTIONS = {"search_engine": "duckduckgo", "max_iterations": 2, "mode": "research"}
161
+ our = None
162
+ for idx, entry in enumerate(plugins):
163
+ if entry == PKG:
164
+ our = {"package": PKG, "options": {}}
165
+ plugins[idx] = our
166
+ elif isinstance(entry, dict) and entry.get("package") == PKG:
167
+ our = entry
168
+ entry["options"] = {**DEFAULT_OPTIONS, **(entry.get("options") or {})}
169
+ if our is None:
170
+ our = {"package": PKG, "options": dict(DEFAULT_OPTIONS)}
171
+ plugins.append(our)
172
+ print(f" + Added plugin entry: {PKG}")
173
+ else:
174
+ print(f" ℹ️ Plugin entry already configured: {PKG}")
175
+ cfg["plugins"] = plugins
176
+
177
+ # --- agents (V2 plural): merge snippet definitions, never overwrite users ---
171
178
  try:
172
179
  snippet = json.loads((repo / "config" / "opencode-snippet.json").read_text(encoding="utf-8"))
173
- agents = cfg.setdefault("agent", {})
174
- for name, definition in snippet.get("agent", {}).items():
180
+ agents = cfg.setdefault("agents", {})
181
+ legacy_agents = cfg.get("agent")
182
+ if isinstance(legacy_agents, dict):
183
+ for name, definition in legacy_agents.items():
184
+ agents.setdefault(name, definition)
185
+ cfg.pop("agent", None)
186
+ print(" ~ Migrated legacy 'agent' entries to V2 'agents'")
187
+ for name, definition in (snippet.get("agents") or snippet.get("agent") or {}).items():
175
188
  if name not in agents:
176
189
  agents[name] = definition
177
190
  print(f" + Added agent: {name}")
178
191
  else:
179
192
  print(f" ℹ️ Agent already configured: {name}")
180
193
 
181
- # Skills: snippet paths are repo-relative ("./skills/x") which breaks
182
- # from ~/.config — merge as absolute paths, repairing relative entries.
194
+ # Skills: V2 is a list of directories/URLs. Snippet paths are repo-relative
195
+ # ("./skills/x") which breaks from ~/.config — merge as absolute paths and
196
+ # repair a legacy `{"paths": [...]}` object or relative entries in place.
183
197
  import os as _os
184
198
  wanted = []
185
- for sp in snippet.get("skills", {}).get("paths", []):
199
+ for sp in snippet.get("skills") or []:
186
200
  wanted.append(sp if _os.path.isabs(sp) else str(repo / sp.lstrip("./")))
187
- skills_cfg = cfg.setdefault("skills", {})
188
- paths = skills_cfg.setdefault("paths", [])
189
- # Repair repo-relative entries in place, then append missing absolutes.
190
- for i, existing in enumerate(list(paths)):
201
+ skills = cfg.get("skills")
202
+ if isinstance(skills, dict):
203
+ skills = cfg["skills"] = list(skills.get("paths") or [])
204
+ print(" ~ Migrated legacy skills.paths to the V2 skills list")
205
+ elif not isinstance(skills, list):
206
+ skills = cfg["skills"] = []
207
+ for i, existing in enumerate(list(skills)):
208
+ if existing in wanted:
209
+ continue
191
210
  for abs_sp in wanted:
192
- if existing == abs_sp:
193
- break
194
211
  rel = "./" + str(Path(abs_sp).relative_to(repo)) if str(abs_sp).startswith(str(repo)) else None
195
212
  if rel and existing == rel:
196
- paths[i] = abs_sp
213
+ skills[i] = abs_sp
197
214
  print(f" ~ Repaired relative skill path: {rel} -> {abs_sp}")
198
215
  break
199
216
  for abs_sp in wanted:
200
- if abs_sp not in paths:
201
- paths.append(abs_sp)
217
+ if abs_sp not in skills:
218
+ skills.append(abs_sp)
202
219
  print(f" + Added skill path: {abs_sp}")
203
220
  else:
204
221
  print(f" ℹ️ Skill path already configured: {abs_sp}")
205
222
  except Exception as e:
206
223
  print(f" ⚠️ Non-critical warning merging agents: {e}")
207
224
 
208
- # --- MCP server: absolute runner path (relative paths only work at repo root) ---
225
+ # --- MCP server (V2 `mcp.servers`): absolute runner path ---
209
226
  mcp = cfg.setdefault("mcp", {})
210
- if "iumbtems" not in mcp:
211
- mcp["iumbtems"] = {
227
+ servers = mcp.setdefault("servers", {})
228
+ legacy_mcp = mcp.get("iumbtems")
229
+ if isinstance(legacy_mcp, dict):
230
+ servers.setdefault("iumbtems", legacy_mcp)
231
+ mcp.pop("iumbtems", None)
232
+ print(" ~ Migrated legacy mcp.iumbtems to V2 mcp.servers.iumbtems")
233
+ if "iumbtems" not in servers:
234
+ servers["iumbtems"] = {
212
235
  "type": "local",
213
236
  "command": ["python3", str(repo / "runner" / "mcp_server.py")],
214
237
  "enabled": True,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heretek-ai/epistemic-swarm",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
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": {
@@ -920,7 +920,8 @@ async function registerHostCommands(host) {
920
920
  await host.session.prompt({
921
921
  ...prompt,
922
922
  sessionID: input?.sessionID,
923
- ...(cmd.agent ? { agent: cmd.agent } : {}),
923
+ // No `agent` field here: V2's SessionPromptInput has none (agent
924
+ // mentions only). Pinning happens via switchAgent above.
924
925
  text: cmd.template.split('$ARGUMENTS').join(String(args).trim()),
925
926
  delivery: input?.delivery,
926
927
  });
@@ -1028,7 +1029,9 @@ async function registerHostAgents(host) {
1028
1029
  // every step of a factory session executed as `build`, not `manager`).
1029
1030
  if (typeof host?.agent?.transform !== 'function') return [];
1030
1031
  const snippet = readPkgJson(path.join(PKG_ROOT, 'config', 'opencode-snippet.json'));
1031
- const agentDefs = (snippet && snippet.agent) || {};
1032
+ // V2 config key is `agents` (plural); accept the legacy `agent` key for one
1033
+ // release so older snippet copies keep working.
1034
+ const agentDefs = (snippet && (snippet.agents || snippet.agent)) || {};
1032
1035
  if (Object.keys(agentDefs).length === 0) return [];
1033
1036
 
1034
1037
  let existing = new Set();
@@ -1046,7 +1049,7 @@ async function registerHostAgents(host) {
1046
1049
  if (claimed.has(name)) continue;
1047
1050
  claimed.add(name);
1048
1051
  added.push(name);
1049
- draft.add(toAgentInfo(name, def));
1052
+ registerAgent(draft, name, def);
1050
1053
  }
1051
1054
  log(host, 'debug', 'agent.transform pass', { added });
1052
1055
  });
@@ -1071,18 +1074,49 @@ const FACTORY_SYSTEM = {
1071
1074
  'You are the IUMBTEMS Factory adversarial QA. Attack the phase: edge cases, regressions, vacuous acceptance criteria, error paths, resource limits. Read-only plus test execution; never edit code. Return pass|fail(reason)|conditional(note) with reproductions.',
1072
1075
  };
1073
1076
 
1074
- /** Map one snippet agent definition to an Agent.Info shape. */
1077
+ /** V1 permission/tool action names -> V2 permission action names. */
1078
+ const ACTION_ALIASES = { bash: 'shell', task: 'subagent', patch: 'edit', write: 'edit' };
1079
+
1080
+ function normalizeAction(action) {
1081
+ const key = String(action || '').trim();
1082
+ return ACTION_ALIASES[key] || key;
1083
+ }
1084
+
1085
+ function normalizeEffect(effect) {
1086
+ return effect === 'deny' || effect === 'ask' ? effect : 'allow';
1087
+ }
1088
+
1089
+ /**
1090
+ * Map one snippet agent definition to an Agent.Info shape.
1091
+ *
1092
+ * Accepts the V2 config form (`permissions: [{action,resource,effect}]`) and
1093
+ * still understands the legacy V1 form (`tools` map + `permission` map) so an
1094
+ * older snippet copy is not silently dropped.
1095
+ */
1075
1096
  function toAgentInfo(name, def) {
1076
1097
  const permissions = [];
1098
+ for (const rule of def?.permissions || []) {
1099
+ if (rule && typeof rule === 'object' && typeof rule.action === 'string') {
1100
+ permissions.push({
1101
+ action: normalizeAction(rule.action),
1102
+ resource: typeof rule.resource === 'string' ? rule.resource : '*',
1103
+ effect: normalizeEffect(rule.effect),
1104
+ });
1105
+ }
1106
+ }
1077
1107
  for (const [tool, enabled] of Object.entries(def?.tools || {})) {
1078
- permissions.push({ action: tool, resource: '*', effect: enabled ? 'allow' : 'deny' });
1108
+ permissions.push({
1109
+ action: normalizeAction(tool),
1110
+ resource: '*',
1111
+ effect: enabled ? 'allow' : 'deny',
1112
+ });
1079
1113
  }
1080
1114
  for (const [key, value] of Object.entries(def?.permission || {})) {
1081
1115
  if (typeof value === 'string') {
1082
- permissions.push({ action: key, resource: '*', effect: value });
1116
+ permissions.push({ action: normalizeAction(key), resource: '*', effect: normalizeEffect(value) });
1083
1117
  } else if (value && typeof value === 'object') {
1084
1118
  for (const [resource, effect] of Object.entries(value)) {
1085
- permissions.push({ action: key, resource, effect });
1119
+ permissions.push({ action: normalizeAction(key), resource, effect: normalizeEffect(effect) });
1086
1120
  }
1087
1121
  }
1088
1122
  }
@@ -1097,11 +1131,41 @@ function toAgentInfo(name, def) {
1097
1131
  };
1098
1132
  if (def?.steps) info.steps = def.steps;
1099
1133
  if (def?.color) info.color = def.color;
1100
- const system = FACTORY_SYSTEM[name];
1134
+ // The canonical short role prompt wins; fall back to a snippet-provided one.
1135
+ const system = FACTORY_SYSTEM[name] || def?.system;
1101
1136
  if (system) info.system = system;
1102
1137
  return info;
1103
1138
  }
1104
1139
 
1140
+ /**
1141
+ * Register one agent profile through a V2 `AgentEditor`.
1142
+ *
1143
+ * V2's AgentEditor has NO `add()` — only `list/get/default/update/remove`
1144
+ * (verified against @opencode/plugin@2.0.18 dist/promise/agent.d.ts). Calling
1145
+ * `draft.add(...)` threw `draft.add is not a function` and the host disabled
1146
+ * the whole plugin ("disabled plugin after transform failure", state=agent).
1147
+ *
1148
+ * `update(id, mutate)` upserts: when the id is new the host seeds the draft
1149
+ * from `Agent.Info.default(id)` plus global permission rules, then hands it to
1150
+ * the callback (opencode v2.0.18 agent registry:
1151
+ * `c = agents.get(d) ?? {...default(d), permissions:[...default.permissions, ...I]}`).
1152
+ * The first-party `opencode.config.agent` plugin registers config agents the
1153
+ * same way, so this is the supported path for creating profile agents.
1154
+ *
1155
+ * Seeded permissions are kept and ours appended: rule matching is
1156
+ * last-match-wins, so an explicit `edit: deny` still beats the default allow.
1157
+ */
1158
+ function registerAgent(editor, name, def) {
1159
+ const info = toAgentInfo(name, def);
1160
+ const rules = info.permissions || [];
1161
+ const profile = { ...info };
1162
+ delete profile.permissions;
1163
+ editor.update(name, (agent) => {
1164
+ Object.assign(agent, profile);
1165
+ agent.permissions = [...(agent.permissions || []), ...rules];
1166
+ });
1167
+ }
1168
+
1105
1169
  /** Canonical skills the plugin registers so agents never hunt the filesystem. */
1106
1170
  const BUNDLED_SKILLS = [
1107
1171
  'factory',