@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,247 @@
1
+ /**
2
+ * IUMBTEMS Epistemic Swarm — OpenCode TUI surface (Phase B visibility).
3
+ *
4
+ * Sidebar status panel + command-palette entry. Read-only: pure `fs` reads of
5
+ * the `.research` workspace (config, report inventory, frontier open nodes,
6
+ * claim-status ledger). Never spawns processes, never writes. Every read is
7
+ * defensive — an absent workspace renders as "no swarm state", never throws.
8
+ *
9
+ * Host API mirrors @prevalentware/opencode-goal-plugin's V2 TUI usage:
10
+ * `ui.slot({append, render})`, `ui.toast.show`, `ui.router.current`,
11
+ * `keymap.layer` (inside a component scope), `api.theme`.
12
+ */
13
+
14
+ import { readFileSync, readdirSync, statSync } from 'fs';
15
+ import path from 'path';
16
+ import { createElement, insert, setProp } from '@opentui/solid';
17
+ import { createEffect, createSignal, onCleanup } from 'solid-js';
18
+
19
+ const PLUGIN_ID = 'heretek.iumbtems.epistemic-swarm.tui';
20
+ const REFRESH_MS = 5000;
21
+
22
+ function element(tag, props = {}, children = []) {
23
+ const node = createElement(tag);
24
+ for (const [key, value] of Object.entries(props)) {
25
+ if (value !== undefined) setProp(node, key, value);
26
+ }
27
+ for (const child of children) {
28
+ if (child !== null && child !== undefined && child !== false) insert(node, child);
29
+ }
30
+ return node;
31
+ }
32
+
33
+ function text(props, children) {
34
+ return element('text', props, children);
35
+ }
36
+
37
+ function box(props, children = []) {
38
+ return element('box', props, children);
39
+ }
40
+
41
+ function readJson(file) {
42
+ try {
43
+ const parsed = JSON.parse(readFileSync(file, 'utf-8'));
44
+ return parsed;
45
+ } catch {
46
+ return undefined;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Snapshot the workspace status. Pure fs reads; safe on missing workspace.
52
+ * Exported for tests.
53
+ */
54
+ export function readSwarmStatus(root) {
55
+ const research = path.join(root || process.cwd(), '.research');
56
+ const status = {
57
+ workspace: false,
58
+ mode: null,
59
+ searchEngine: null,
60
+ maxIterations: null,
61
+ reports: [],
62
+ frontierOpen: null,
63
+ stale: 0,
64
+ suspect: 0,
65
+ requeued: 0,
66
+ };
67
+ let entries = [];
68
+ try {
69
+ entries = readdirSync(research);
70
+ status.workspace = true;
71
+ } catch {
72
+ return status;
73
+ }
74
+ const cfg = readJson(path.join(research, 'config.json'));
75
+ if (cfg && typeof cfg === 'object') {
76
+ status.mode = cfg.mode ?? null;
77
+ status.searchEngine = cfg.search_engine ?? null;
78
+ status.maxIterations = cfg.max_iterations ?? null;
79
+ }
80
+ for (const name of [
81
+ 'final_synthesis.md',
82
+ 'brainstorm_report.md',
83
+ 'code_audit_report.md',
84
+ 'oss_scout_report.md',
85
+ ]) {
86
+ try {
87
+ const st = statSync(path.join(research, name));
88
+ status.reports.push({ name, mtimeMs: st.mtimeMs });
89
+ } catch {
90
+ /* absent report */
91
+ }
92
+ }
93
+ const frontier = readJson(path.join(research, 'frontier.json'));
94
+ if (frontier && typeof frontier === 'object') {
95
+ const nodes = Array.isArray(frontier.nodes) ? frontier.nodes : Object.values(frontier.nodes || {});
96
+ const open = nodes.filter((n) => n && n.status !== 'settled' && n.status !== 'closed');
97
+ status.frontierOpen = open.length;
98
+ }
99
+ const ledger = readJson(path.join(research, 'ledger', 'claim_status.json'));
100
+ if (Array.isArray(ledger)) {
101
+ const last = new Map();
102
+ for (const e of ledger) {
103
+ if (e && typeof e.claim_id === 'string') last.set(e.claim_id, e.to_status);
104
+ }
105
+ for (const s of last.values()) {
106
+ if (s === 'STALE') status.stale += 1;
107
+ else if (s === 'SUSPECT') status.suspect += 1;
108
+ }
109
+ }
110
+ const requeue = readJson(path.join(research, 'requeue.json'));
111
+ if (Array.isArray(requeue)) status.requeued = requeue.length;
112
+ return status;
113
+ }
114
+
115
+ function themeFg(theme) {
116
+ try {
117
+ const t = theme ?? {};
118
+ const pick = (...paths) => {
119
+ for (const p of paths) {
120
+ let cur = t;
121
+ for (const k of p) {
122
+ if (cur === null || typeof cur !== 'object') { cur = undefined; break; }
123
+ cur = cur[k];
124
+ }
125
+ if (cur && typeof cur === 'object' && 'default' in cur) cur = cur.default;
126
+ if (cur !== undefined && cur !== null) return cur;
127
+ }
128
+ return undefined;
129
+ };
130
+ return {
131
+ text: pick(['text', 'default'], ['text']),
132
+ muted: pick(['text', 'subdued'], ['textMuted']),
133
+ warn: pick(['text', 'feedback', 'warning'], ['warning']),
134
+ };
135
+ } catch {
136
+ return {};
137
+ }
138
+ }
139
+
140
+ function summarize(status) {
141
+ if (!status.workspace) return 'IUMBTEMS: no swarm state';
142
+ const bits = [];
143
+ if (status.mode) bits.push(status.mode);
144
+ bits.push(`${status.reports.length} reports`);
145
+ if (status.frontierOpen !== null) bits.push(`${status.frontierOpen} open frontier`);
146
+ if (status.stale > 0 || status.suspect > 0) bits.push(`${status.stale} STALE/${status.suspect} SUSPECT`);
147
+ if (status.requeued > 0) bits.push(`${status.requeued} requeued`);
148
+ return `IUMBTEMS: ${bits.join(' · ')}`;
149
+ }
150
+
151
+ function SwarmSidebar(api, root, sessionID) {
152
+ const colors = themeFg(api?.theme);
153
+ const [snapshot, setSnapshot] = createSignal(readSwarmStatus(root));
154
+ createEffect(() => {
155
+ const timer = setInterval(() => {
156
+ try {
157
+ setSnapshot(readSwarmStatus(root));
158
+ } catch {
159
+ /* keep last snapshot */
160
+ }
161
+ }, REFRESH_MS);
162
+ onCleanup(() => clearInterval(timer));
163
+ });
164
+ void sessionID;
165
+ return box({}, [
166
+ () => {
167
+ const s = snapshot();
168
+ const degraded = s.stale > 0 || s.suspect > 0;
169
+ return text({ fg: degraded ? colors.warn : colors.muted }, [summarize(s)]);
170
+ },
171
+ ]);
172
+ }
173
+
174
+ function SwarmKeymapLayer(api) {
175
+ api.keymap.layer(() => ({
176
+ mode: 'global',
177
+ commands: [
178
+ {
179
+ id: 'iumbtems.swarm-status',
180
+ title: 'Swarm status',
181
+ description: 'Show IUMBTEMS workspace status (mode, reports, frontier, STALE claims)',
182
+ group: 'IUMBTEMS',
183
+ palette: true,
184
+ run: () => {
185
+ const root = resolveRoot(api);
186
+ api.ui.toast.show({
187
+ title: 'IUMBTEMS',
188
+ message: summarize(readSwarmStatus(root)),
189
+ variant: 'info',
190
+ duration: 4000,
191
+ });
192
+ },
193
+ },
194
+ ],
195
+ }));
196
+ return null;
197
+ }
198
+
199
+ function resolveRoot(api) {
200
+ try {
201
+ return (
202
+ api?.directory ??
203
+ api?.project?.directory ??
204
+ api?.cwd ??
205
+ process.cwd()
206
+ );
207
+ } catch {
208
+ return process.cwd();
209
+ }
210
+ }
211
+
212
+ function registerSlot(api, name, render) {
213
+ const slot = api?.ui?.slot;
214
+ if (typeof slot !== 'function') return () => {};
215
+ try {
216
+ const dispose =
217
+ slot.length <= 1 ? slot({ append: name, render }) : slot(name, render);
218
+ return typeof dispose === 'function' ? dispose : () => {};
219
+ } catch {
220
+ return () => {};
221
+ }
222
+ }
223
+
224
+ /** TUI setup: sidebar slot + palette command. Never throws. */
225
+ export function setupTui(api) {
226
+ try {
227
+ const root = resolveRoot(api);
228
+ const offSidebar = registerSlot(api, 'sidebar.content', (props) =>
229
+ SwarmSidebar(api, root, props?.sessionID)
230
+ );
231
+ const offApp = registerSlot(api, 'app', () => SwarmKeymapLayer(api));
232
+ return () => {
233
+ try { offSidebar(); } catch { /* noop */ }
234
+ try { offApp(); } catch { /* noop */ }
235
+ };
236
+ } catch {
237
+ return () => {};
238
+ }
239
+ }
240
+
241
+ const plugin = {
242
+ id: PLUGIN_ID,
243
+ tui: true,
244
+ setup: setupTui,
245
+ };
246
+
247
+ export default plugin;