@heretek-ai/epistemic-swarm 0.4.7 → 0.5.0
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/.agents/skills/darkharvest/SKILL.md +13 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.omp/commands/darkharvest.md +11 -0
- package/README.md +4 -0
- package/bin/cli.js +73 -7
- package/config/mcp_launcher.py +75 -48
- package/config/opencode-snippet.json +14 -1
- package/extensions/pi/index.js +72 -6
- package/install.sh +5 -5
- package/package.json +5 -3
- package/plugins/antigravity/plugin.json +1 -1
- package/plugins/antigravity/skills/darkharvest/SKILL.md +13 -0
- package/plugins/codex/skills/darkharvest/SKILL.md +13 -0
- package/plugins/gemini/commands/darkharvest.toml +8 -0
- package/plugins/gemini/gemini-extension.json +1 -1
- package/plugins/gemini/skills/darkharvest/SKILL.md +13 -0
- package/plugins/opencode/index.js +357 -144
- 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/prompts/agent_darkharvest.md +91 -0
- 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 +225 -73
- 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 +198 -22
- 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 +80 -38
- package/runner/tests/test_opencode_ux.py +389 -35
- package/runner/tests/test_swarm.py +32 -9
- package/runner/tests/test_sweep_regressions.py +420 -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 +57 -35
- package/scripts/divergence_experiment.py +61 -31
- package/skills/brainstorming/scripts/brainstorm.py +3 -2
- package/skills/darkharvest/SKILL.md +70 -0
- package/skills/darkharvest/scripts/harvest.py +252 -0
- 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 +44 -30
|
@@ -11,16 +11,35 @@
|
|
|
11
11
|
* catalog. Schemas mirror runner/mcp_server.py build_tools() — keep in sync.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { spawn } from 'child_process';
|
|
15
|
-
import { readFileSync } from 'fs';
|
|
16
|
-
import path from 'path';
|
|
17
|
-
import { fileURLToPath } from 'url';
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { accessSync, constants, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
18
|
|
|
19
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
20
|
const __dirname = path.dirname(__filename);
|
|
21
21
|
const PKG_ROOT = path.resolve(__dirname, '../..');
|
|
22
22
|
const MCP_SERVER = path.join(PKG_ROOT, 'runner', 'mcp_server.py');
|
|
23
23
|
|
|
24
|
+
function resolveBinary(name) {
|
|
25
|
+
const pathDirs = (process.env.PATH || '').split(path.delimiter);
|
|
26
|
+
for (const dir of pathDirs) {
|
|
27
|
+
if (!dir || dir === '.' || dir.startsWith('./') || dir.startsWith('../')) continue;
|
|
28
|
+
const full = path.join(dir, name);
|
|
29
|
+
try {
|
|
30
|
+
// Match Node's own PATH search: skip non-files and non-executables so a
|
|
31
|
+
// directory or a non-executable named `python3` earlier on PATH cannot
|
|
32
|
+
// shadow the real binary.
|
|
33
|
+
if (!statSync(full).isFile()) continue;
|
|
34
|
+
accessSync(full, constants.X_OK);
|
|
35
|
+
return realpathSync(full);
|
|
36
|
+
} catch {
|
|
37
|
+
/* not present, not a file, or not executable — keep searching */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return name;
|
|
41
|
+
}
|
|
42
|
+
|
|
24
43
|
/** Plugin version tracks package.json so it cannot drift across releases. */
|
|
25
44
|
let PKG_VERSION = '0.0.0';
|
|
26
45
|
try {
|
|
@@ -35,7 +54,7 @@ try {
|
|
|
35
54
|
* Async (non-blocking spawn) so long swarms/audits never freeze the host
|
|
36
55
|
* event loop; cancellation remains the host's prerogative.
|
|
37
56
|
*/
|
|
38
|
-
function callMcp(tool, args = {}, cwd) {
|
|
57
|
+
function callMcp(tool, args = {}, cwd = undefined) {
|
|
39
58
|
return new Promise((resolve) => {
|
|
40
59
|
let stdout = '';
|
|
41
60
|
let stderr = '';
|
|
@@ -48,7 +67,7 @@ function callMcp(tool, args = {}, cwd) {
|
|
|
48
67
|
let child;
|
|
49
68
|
try {
|
|
50
69
|
child = spawn(
|
|
51
|
-
'python3',
|
|
70
|
+
resolveBinary('python3'),
|
|
52
71
|
[MCP_SERVER, 'call', tool, JSON.stringify(args || {})],
|
|
53
72
|
{
|
|
54
73
|
cwd: cwd || process.cwd(),
|
|
@@ -56,12 +75,12 @@ function callMcp(tool, args = {}, cwd) {
|
|
|
56
75
|
}
|
|
57
76
|
);
|
|
58
77
|
} catch (err) {
|
|
59
|
-
done(String(
|
|
78
|
+
done(String(err?.message || err), 'error');
|
|
60
79
|
return;
|
|
61
80
|
}
|
|
62
81
|
child.stdout.on('data', (d) => { stdout += String(d); });
|
|
63
82
|
child.stderr.on('data', (d) => { stderr += String(d); });
|
|
64
|
-
child.on('error', (err) => done(String(
|
|
83
|
+
child.on('error', (err) => done(String(err?.message || err), 'error'));
|
|
65
84
|
child.on('close', (code) => done(stdout || stderr, code === 0 ? 'success' : 'error'));
|
|
66
85
|
});
|
|
67
86
|
}
|
|
@@ -73,6 +92,7 @@ export const IUMBTEMS_TOOL_NAMES = [
|
|
|
73
92
|
'iumbtems_code_audit',
|
|
74
93
|
'iumbtems_oss_scout',
|
|
75
94
|
'iumbtems_brainstorm',
|
|
95
|
+
'iumbtems_darkharvest',
|
|
76
96
|
'iumbtems_verify_quote',
|
|
77
97
|
'iumbtems_socratic_frontier',
|
|
78
98
|
'iumbtems_reindex_claims',
|
|
@@ -107,8 +127,8 @@ const TOOL_CATALOG = [
|
|
|
107
127
|
},
|
|
108
128
|
mode: {
|
|
109
129
|
type: 'string',
|
|
110
|
-
enum: ['research', 'audit', 'scout', 'hybrid', 'brainstorm'],
|
|
111
|
-
description: 'Operating mode: research (literature), audit (codebase), scout (OSS), hybrid, brainstorm (lateral ideation)',
|
|
130
|
+
enum: ['research', 'audit', 'scout', 'hybrid', 'brainstorm', 'darkharvest'],
|
|
131
|
+
description: 'Operating mode: research (literature), audit (codebase), scout (OSS), hybrid, brainstorm (lateral ideation), darkharvest (competitor teardown)',
|
|
112
132
|
},
|
|
113
133
|
divergence_threshold: {
|
|
114
134
|
type: 'number',
|
|
@@ -204,6 +224,39 @@ const TOOL_CATALOG = [
|
|
|
204
224
|
required: ['objective'],
|
|
205
225
|
},
|
|
206
226
|
},
|
|
227
|
+
{
|
|
228
|
+
name: 'iumbtems_darkharvest',
|
|
229
|
+
description:
|
|
230
|
+
'Product competitor teardown: seed inspirations plus prompt, expand to adjacents, emit per-feature depend/vendor/clean-room/skip verdicts with SPDX attribution.',
|
|
231
|
+
input: {
|
|
232
|
+
type: 'object',
|
|
233
|
+
properties: {
|
|
234
|
+
objective: {
|
|
235
|
+
type: 'string',
|
|
236
|
+
description: 'Product arena to tear down (e.g. Paseo-class agent harness competitor)',
|
|
237
|
+
},
|
|
238
|
+
seeds: {
|
|
239
|
+
type: 'array',
|
|
240
|
+
items: { type: 'string' },
|
|
241
|
+
description: 'Seed inspiration repo URLs',
|
|
242
|
+
},
|
|
243
|
+
maxRepos: {
|
|
244
|
+
type: 'integer',
|
|
245
|
+
description: 'Cap on total competitors (default 10)',
|
|
246
|
+
},
|
|
247
|
+
depth: {
|
|
248
|
+
type: 'integer',
|
|
249
|
+
description: 'Dialectic depth 1-4',
|
|
250
|
+
},
|
|
251
|
+
mock_mode: {
|
|
252
|
+
type: 'boolean',
|
|
253
|
+
description: 'Run in mock mode without invoking LLM tokens',
|
|
254
|
+
default: false,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
required: ['objective'],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
207
260
|
{
|
|
208
261
|
name: 'iumbtems_verify_quote',
|
|
209
262
|
description:
|
|
@@ -446,19 +499,36 @@ export const OPENCODE_COMMANDS = [
|
|
|
446
499
|
'Execute iumbtems_brainstorm with {"objective": "<prompt>"} and report feature vectors, paradigm inversions, and falsifiable spikes.',
|
|
447
500
|
].join('\n'),
|
|
448
501
|
},
|
|
502
|
+
{
|
|
503
|
+
name: 'darkharvest',
|
|
504
|
+
description: 'Product competitor teardown with per-feature harvest verdicts',
|
|
505
|
+
usage: '/darkharvest <objective> [--seeds <urls>] [--max-repos <n>]',
|
|
506
|
+
template: [
|
|
507
|
+
'Run a product competitor teardown (seed inspirations plus prompt, expand to adjacents).',
|
|
508
|
+
'Objective: $ARGUMENTS (product arena; seeds may be embedded as URLs)',
|
|
509
|
+
'If $ARGUMENTS is empty, ask the user for the product arena and seed URLs first; never call the tool with placeholder arguments.',
|
|
510
|
+
'1. Execute iumbtems_darkharvest with {"objective": "<objective>", "seeds": ["<url>", "..."]} (pass "mock_mode": true only for dry runs).',
|
|
511
|
+
'2. Report the competitor x capability matrix, white-space gaps both ways, and the SPDX-attributed harvest backlog in .research/darkharvest_report.md.',
|
|
512
|
+
'3. Legal rule: permissive-only vendor; GPL/AGPL clean-room-rebuild only; workflows clonable, assets never.',
|
|
513
|
+
].join('\n'),
|
|
514
|
+
},
|
|
449
515
|
];
|
|
450
516
|
|
|
451
517
|
/**
|
|
452
|
-
*
|
|
453
|
-
*
|
|
518
|
+
* Command catalog shaped for a declarative opencode config.
|
|
519
|
+
*
|
|
520
|
+
* V2's schema key is `commands` (plural) — `command` is the deprecated V1 key
|
|
521
|
+
* and is ignored by a V2 host. Registration on a live V2 host goes through
|
|
522
|
+
* `command.transform` instead of this; this exists so tests and docs can assert
|
|
523
|
+
* the catalog shape and the correct key.
|
|
454
524
|
*/
|
|
455
|
-
export function
|
|
456
|
-
|
|
525
|
+
export function commandCatalog() {
|
|
526
|
+
const out = {};
|
|
457
527
|
for (const cmd of OPENCODE_COMMANDS) {
|
|
458
|
-
if (
|
|
459
|
-
|
|
528
|
+
if (!cmd?.name) continue;
|
|
529
|
+
out[cmd.name] = { description: cmd.description, template: cmd.template };
|
|
460
530
|
}
|
|
461
|
-
return
|
|
531
|
+
return out;
|
|
462
532
|
}
|
|
463
533
|
|
|
464
534
|
/** Tool map (object form) for the server hook; array catalog stays canonical. */
|
|
@@ -469,7 +539,7 @@ function buildToolMap() {
|
|
|
469
539
|
{
|
|
470
540
|
...tool,
|
|
471
541
|
options: { codemode: false },
|
|
472
|
-
execute: async (args = {}, toolContext) =>
|
|
542
|
+
execute: async (args = {}, toolContext = undefined) =>
|
|
473
543
|
callMcp(tool.name, normalizeArgs(tool.name, args), toolContext?.cwd),
|
|
474
544
|
},
|
|
475
545
|
])
|
|
@@ -482,7 +552,7 @@ function buildToolMap() {
|
|
|
482
552
|
* Reference pattern: @prevalentware/opencode-goal-plugin subscribes via
|
|
483
553
|
* `context.event.subscribe({signal})`, detects idle as `session.idle` or an
|
|
484
554
|
* idle `session.status`, continues via `context.session.prompt`, and preserves
|
|
485
|
-
* state across compaction with
|
|
555
|
+
* state across compaction with `context.session.hook("compaction", ...)`.
|
|
486
556
|
* Every hook here is best-effort and never throws into the host.
|
|
487
557
|
*/
|
|
488
558
|
|
|
@@ -507,22 +577,31 @@ function readJsonFile(file) {
|
|
|
507
577
|
}
|
|
508
578
|
}
|
|
509
579
|
|
|
580
|
+
function parseLedgerEntries(ledger) {
|
|
581
|
+
const stale = [];
|
|
582
|
+
const suspect = [];
|
|
583
|
+
const last = new Map();
|
|
584
|
+
for (const e of ledger) {
|
|
585
|
+
if (typeof e?.claim_id === 'string') last.set(e.claim_id, e.to_status);
|
|
586
|
+
}
|
|
587
|
+
for (const [id, s] of last) {
|
|
588
|
+
if (s === 'STALE') stale.push(id);
|
|
589
|
+
else if (s === 'SUSPECT') suspect.push(id);
|
|
590
|
+
}
|
|
591
|
+
const lastAt = ledger.at(-1)?.at ?? '';
|
|
592
|
+
return { stale, suspect, fingerprint: `${ledger.length}:${lastAt}` };
|
|
593
|
+
}
|
|
594
|
+
|
|
510
595
|
/** Last-write-wins degraded claims + requeue list. Pure fs reads. */
|
|
511
596
|
export function readLedgerDegraded(root) {
|
|
512
597
|
const out = { stale: [], suspect: [], requeued: [], fingerprint: 'empty' };
|
|
513
598
|
try {
|
|
514
599
|
const ledger = readJsonFile(path.join(root, '.research', 'ledger', 'claim_status.json'));
|
|
515
600
|
if (Array.isArray(ledger)) {
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
for (const [id, s] of last) {
|
|
521
|
-
if (s === 'STALE') out.stale.push(id);
|
|
522
|
-
else if (s === 'SUSPECT') out.suspect.push(id);
|
|
523
|
-
}
|
|
524
|
-
const lastAt = ledger.length > 0 ? ledger[ledger.length - 1]?.at : '';
|
|
525
|
-
out.fingerprint = `${ledger.length}:${lastAt}`;
|
|
601
|
+
const parsed = parseLedgerEntries(ledger);
|
|
602
|
+
out.stale = parsed.stale;
|
|
603
|
+
out.suspect = parsed.suspect;
|
|
604
|
+
out.fingerprint = parsed.fingerprint;
|
|
526
605
|
}
|
|
527
606
|
const requeue = readJsonFile(path.join(root, '.research', 'requeue.json'));
|
|
528
607
|
if (Array.isArray(requeue)) {
|
|
@@ -635,6 +714,200 @@ export function startStalenessNudge(host, root) {
|
|
|
635
714
|
}
|
|
636
715
|
}
|
|
637
716
|
|
|
717
|
+
/**
|
|
718
|
+
* Plugin log. Never throws.
|
|
719
|
+
*
|
|
720
|
+
* Before this existed every setup block swallowed errors with a bare
|
|
721
|
+
* `catch {}`, so `--log-level all` showed only host-side symptoms and a
|
|
722
|
+
* registration failure was indistinguishable from success.
|
|
723
|
+
*/
|
|
724
|
+
function log(host, level, message, detail) {
|
|
725
|
+
let text = message;
|
|
726
|
+
if (detail !== undefined) {
|
|
727
|
+
try {
|
|
728
|
+
text = `${message} ${JSON.stringify(detail)}`;
|
|
729
|
+
} catch {
|
|
730
|
+
text = `${message} [unserialisable detail]`;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
const sink = host?.client?.app?.log;
|
|
735
|
+
if (typeof sink === 'function') {
|
|
736
|
+
Promise.resolve(sink.call(host.client.app, {
|
|
737
|
+
body: { service: 'iumbtems', level, message: text },
|
|
738
|
+
})).catch(() => {});
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
} catch {
|
|
742
|
+
/* fall through to console */
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
(console.error || console.log)(`[iumbtems] ${level}: ${text}`);
|
|
746
|
+
} catch {
|
|
747
|
+
/* logging must never throw into the host */
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function errDetail(err) {
|
|
752
|
+
return { error: String(err?.message ?? err) };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function registerHostCommands(host) {
|
|
756
|
+
if (typeof host?.command?.transform !== 'function') return [];
|
|
757
|
+
|
|
758
|
+
// Snapshot which names are already taken *at setup time*. This snapshot is
|
|
759
|
+
// COPIED into the transform callback on every invocation and never grown.
|
|
760
|
+
//
|
|
761
|
+
// The host replays this callback on each command-registry rebuild, and the
|
|
762
|
+
// rebuilt registry starts from base config — so if the callback mutates a
|
|
763
|
+
// shared "already added" set, the second replay sees all six names as taken
|
|
764
|
+
// and adds nothing. That is exactly why the slash commands appeared at
|
|
765
|
+
// startup and vanished seconds later, and why only a full process restart
|
|
766
|
+
// brought them back (fresh setup -> fresh set).
|
|
767
|
+
//
|
|
768
|
+
// The reference plugin (@prevalentware/opencode-goal-plugin) gets this right
|
|
769
|
+
// with `const claimed = new Set(existingCommands)` inside the callback.
|
|
770
|
+
let existingNames = new Set();
|
|
771
|
+
try {
|
|
772
|
+
const listed = await host.command.list();
|
|
773
|
+
existingNames = new Set((listed?.data || []).map((c) => c?.name));
|
|
774
|
+
} catch (err) {
|
|
775
|
+
log(host, 'warn', 'command.list() failed; assuming an empty registry', errDetail(err));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const registration = await host.command.transform((draft) => {
|
|
779
|
+
const claimed = new Set(existingNames); // fresh copy per replay
|
|
780
|
+
const added = [];
|
|
781
|
+
for (const cmd of OPENCODE_COMMANDS) {
|
|
782
|
+
if (!cmd?.name || claimed.has(cmd.name)) continue;
|
|
783
|
+
claimed.add(cmd.name);
|
|
784
|
+
added.push(cmd.name);
|
|
785
|
+
draft.add({
|
|
786
|
+
name: cmd.name,
|
|
787
|
+
description: cmd.description,
|
|
788
|
+
execute: async (input) => {
|
|
789
|
+
const args = input?.prompt?.text || '';
|
|
790
|
+
const prompt = (typeof input?.prompt === 'object' && input?.prompt !== null) ? input.prompt : {};
|
|
791
|
+
await host.session.prompt({
|
|
792
|
+
...prompt,
|
|
793
|
+
sessionID: input?.sessionID,
|
|
794
|
+
text: cmd.template.split('$ARGUMENTS').join(String(args).trim()),
|
|
795
|
+
delivery: input?.delivery,
|
|
796
|
+
});
|
|
797
|
+
},
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
log(host, 'debug', 'command.transform pass', {
|
|
801
|
+
added,
|
|
802
|
+
skippedAsPreexisting: [...existingNames].filter((n) => !added.includes(n)),
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// Force materialisation now instead of waiting for the host's own later
|
|
807
|
+
// reconcile (which is the pass that used to wipe them).
|
|
808
|
+
try {
|
|
809
|
+
if (typeof host.command.reload === 'function') await host.command.reload();
|
|
810
|
+
log(host, 'info', 'slash commands registered', { count: OPENCODE_COMMANDS.length });
|
|
811
|
+
} catch (err) {
|
|
812
|
+
log(host, 'warn', 'command.reload() failed', errDetail(err));
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return registration ? [registration] : [];
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
async function registerHostTools(host) {
|
|
819
|
+
if (typeof host?.tool?.transform !== 'function') return [];
|
|
820
|
+
const registration = await host.tool.transform((draft) => {
|
|
821
|
+
for (const [name, spec] of Object.entries(buildToolMap())) {
|
|
822
|
+
draft.add({
|
|
823
|
+
name,
|
|
824
|
+
description: spec.description,
|
|
825
|
+
input: spec.input,
|
|
826
|
+
options: { codemode: false },
|
|
827
|
+
execute: async (args = {}, toolContext = undefined) => {
|
|
828
|
+
const r = await callMcp(name, normalizeArgs(name, args), toolContext?.cwd);
|
|
829
|
+
return { content: r.content };
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
log(host, 'debug', 'tool.transform pass', { count: Object.keys(buildToolMap()).length });
|
|
834
|
+
});
|
|
835
|
+
try {
|
|
836
|
+
if (typeof host.tool.reload === 'function') await host.tool.reload();
|
|
837
|
+
} catch (err) {
|
|
838
|
+
log(host, 'warn', 'tool.reload() failed', errDetail(err));
|
|
839
|
+
}
|
|
840
|
+
return registration ? [registration] : [];
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* V2 compaction hook: keep .research state across context compaction.
|
|
845
|
+
*
|
|
846
|
+
* The V1 equivalent (`experimental.session.compacting`) lived in the old
|
|
847
|
+
* `server()` hook and was dead code on V2 — compaction silently lost the
|
|
848
|
+
* swarm state. Shape follows the reference plugin: push a text part onto
|
|
849
|
+
* `event.system`.
|
|
850
|
+
*/
|
|
851
|
+
async function registerCompactionHook(host, context) {
|
|
852
|
+
const hook = host?.session?.hook || context?.session?.hook;
|
|
853
|
+
if (typeof hook !== 'function') return [];
|
|
854
|
+
try {
|
|
855
|
+
const registration = await hook.call(host.session || context.session, 'compaction', async (event) => {
|
|
856
|
+
try {
|
|
857
|
+
const root =
|
|
858
|
+
event?.directory || host.location?.directory || context.location?.directory || event?.cwd;
|
|
859
|
+
if (!root) return;
|
|
860
|
+
const text = buildCompactionContext(root);
|
|
861
|
+
if (!text) return;
|
|
862
|
+
const system = Array.isArray(event?.system) ? event.system : null;
|
|
863
|
+
if (!system) return;
|
|
864
|
+
// Idempotent: the host may invoke this more than once per session.
|
|
865
|
+
if (system.some((part) => part?.type === 'text' && part?.text === text)) return;
|
|
866
|
+
system.push({ type: 'text', text });
|
|
867
|
+
} catch (err) {
|
|
868
|
+
log(host, 'warn', 'compaction hook failed', errDetail(err));
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
return registration ? [registration] : [];
|
|
872
|
+
} catch (err) {
|
|
873
|
+
log(host, 'warn', 'session.hook("compaction") unavailable', errDetail(err));
|
|
874
|
+
return [];
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async function applyConfigOptions(host, opts, context) {
|
|
879
|
+
if (!opts || typeof opts !== 'object') return;
|
|
880
|
+
const updates = {};
|
|
881
|
+
if (opts.search_engine) updates.search_engine = opts.search_engine;
|
|
882
|
+
if (opts.max_iterations) updates.max_iterations = opts.max_iterations;
|
|
883
|
+
if (opts.mode) updates.mode = opts.mode;
|
|
884
|
+
if (opts.divergence_threshold !== undefined) {
|
|
885
|
+
updates.divergence_threshold = opts.divergence_threshold;
|
|
886
|
+
}
|
|
887
|
+
if (Object.keys(updates).length > 0) {
|
|
888
|
+
await callMcp(
|
|
889
|
+
'iumbtems_config',
|
|
890
|
+
updates,
|
|
891
|
+
host.location?.directory || context.location?.directory || host.cwd
|
|
892
|
+
);
|
|
893
|
+
log(host, 'debug', 'applied tuple options via iumbtems_config', updates);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function startNudgeSubscription(host, opts, context, cleanups) {
|
|
898
|
+
if (!(opts?.staleness_nudge ?? true)) return;
|
|
899
|
+
const nudgeHost = {
|
|
900
|
+
event: host.event || context.event,
|
|
901
|
+
session: host.session || context.session,
|
|
902
|
+
};
|
|
903
|
+
const root = host.location?.directory || context.location?.directory;
|
|
904
|
+
if (root) {
|
|
905
|
+
const stop = startStalenessNudge(nudgeHost, root);
|
|
906
|
+
if (typeof stop === 'function') cleanups.add(stop);
|
|
907
|
+
log(host, 'debug', 'staleness nudge subscription', { root, started: typeof stop === 'function' });
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
638
911
|
export function createOpenCodePlugin(context = {}) {
|
|
639
912
|
return {
|
|
640
913
|
id: 'heretek.iumbtems.epistemic-swarm',
|
|
@@ -643,137 +916,77 @@ export function createOpenCodePlugin(context = {}) {
|
|
|
643
916
|
description:
|
|
644
917
|
'I Use My Brain To Express My Self: High-integrity dialectic research, code audits, and open-source scouting',
|
|
645
918
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
// Tool map (object form, as in the reference implementation).
|
|
651
|
-
tool: buildToolMap(),
|
|
652
|
-
// Compaction hook: preserve .research state (best-effort, silent skip
|
|
653
|
-
// when no workspace is present so compaction never breaks).
|
|
654
|
-
'experimental.session.compacting': async (input, output) => {
|
|
655
|
-
try {
|
|
656
|
-
const root = input?.directory ?? input?.location?.directory ?? process.cwd();
|
|
657
|
-
const ctx = buildCompactionContext(root);
|
|
658
|
-
if (ctx && output && Array.isArray(output.context)) output.context.push(ctx);
|
|
659
|
-
} catch {
|
|
660
|
-
/* never break compaction */
|
|
661
|
-
}
|
|
662
|
-
},
|
|
663
|
-
}),
|
|
664
|
-
|
|
919
|
+
// V2 only. The V1 `server()` factory and its `config` hook wrote the
|
|
920
|
+
// deprecated `command` key (V2's schema is `commands`), so they registered
|
|
921
|
+
// nothing on a V2 host — and carrying both shapes meant the real
|
|
922
|
+
// registration path had to guess which host it was talking to.
|
|
665
923
|
setup: async (appContext) => {
|
|
666
924
|
const host = appContext || {};
|
|
667
925
|
const opts = context.options || host.options;
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
|
|
926
|
+
// Transform registrations must precede any slow work: the host may
|
|
927
|
+
// reconcile past a slow setup and never see the commands.
|
|
928
|
+
const registrations = [];
|
|
929
|
+
const adopt = (list) => {
|
|
930
|
+
for (const r of list || []) if (r) registrations.push(r);
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
log(host, 'info', 'iumbtems setup', {
|
|
934
|
+
version: PKG_VERSION,
|
|
935
|
+
directory: host.location?.directory || context.location?.directory || null,
|
|
936
|
+
});
|
|
937
|
+
|
|
671
938
|
try {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const listed = await host.command.list();
|
|
676
|
-
existing = new Set(((listed && listed.data) || []).map((c) => c && c.name));
|
|
677
|
-
} catch {
|
|
678
|
-
/* treat as empty */
|
|
679
|
-
}
|
|
680
|
-
await host.command.transform((draft) => {
|
|
681
|
-
for (const cmd of OPENCODE_COMMANDS) {
|
|
682
|
-
if (!cmd || !cmd.name || existing.has(cmd.name)) continue;
|
|
683
|
-
existing.add(cmd.name);
|
|
684
|
-
draft.add({
|
|
685
|
-
name: cmd.name,
|
|
686
|
-
description: cmd.description,
|
|
687
|
-
execute: async (input) => {
|
|
688
|
-
const args = (input && input.prompt && input.prompt.text) || '';
|
|
689
|
-
const prompt = (input && input.prompt && typeof input.prompt === 'object') ? input.prompt : {};
|
|
690
|
-
await host.session.prompt({
|
|
691
|
-
...prompt,
|
|
692
|
-
sessionID: input && input.sessionID,
|
|
693
|
-
text: cmd.template.split('$ARGUMENTS').join(String(args).trim()),
|
|
694
|
-
delivery: input && input.delivery,
|
|
695
|
-
});
|
|
696
|
-
},
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
} catch {
|
|
702
|
-
/* command transform is best-effort */
|
|
939
|
+
adopt(await registerHostCommands(host));
|
|
940
|
+
} catch (err) {
|
|
941
|
+
log(host, 'error', 'registering slash commands failed', errDetail(err));
|
|
703
942
|
}
|
|
704
943
|
try {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
draft.add({
|
|
709
|
-
name,
|
|
710
|
-
description: spec.description,
|
|
711
|
-
input: spec.input,
|
|
712
|
-
options: { codemode: false },
|
|
713
|
-
execute: async (args = {}, toolContext) => {
|
|
714
|
-
const r = await callMcp(name, normalizeArgs(name, args), toolContext?.cwd);
|
|
715
|
-
return { content: r.content };
|
|
716
|
-
},
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
} catch {
|
|
722
|
-
/* tool transform is best-effort */
|
|
944
|
+
adopt(await registerHostTools(host));
|
|
945
|
+
} catch (err) {
|
|
946
|
+
log(host, 'error', 'registering tools failed', errDetail(err));
|
|
723
947
|
}
|
|
724
|
-
// Slow work AFTER registration: apply tuple options via MCP config
|
|
725
|
-
// write, scoped to the plugin location (never the service cwd).
|
|
726
948
|
try {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
if (opts.max_iterations) updates.max_iterations = opts.max_iterations;
|
|
731
|
-
if (opts.mode) updates.mode = opts.mode;
|
|
732
|
-
if (opts.divergence_threshold !== undefined) {
|
|
733
|
-
updates.divergence_threshold = opts.divergence_threshold;
|
|
734
|
-
}
|
|
735
|
-
if (Object.keys(updates).length > 0) {
|
|
736
|
-
await callMcp(
|
|
737
|
-
'iumbtems_config',
|
|
738
|
-
updates,
|
|
739
|
-
host.location?.directory || context.location?.directory || host.cwd
|
|
740
|
-
);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
} catch {
|
|
744
|
-
/* options apply is best-effort */
|
|
949
|
+
adopt(await registerCompactionHook(host, context));
|
|
950
|
+
} catch (err) {
|
|
951
|
+
log(host, 'error', 'registering compaction hook failed', errDetail(err));
|
|
745
952
|
}
|
|
746
|
-
//
|
|
747
|
-
// Subscribes only when the host exposes event/session; one per root.
|
|
953
|
+
// Slow work AFTER registration.
|
|
748
954
|
try {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
955
|
+
await applyConfigOptions(host, opts, context);
|
|
956
|
+
} catch (err) {
|
|
957
|
+
log(host, 'warn', 'applying tuple options failed', errDetail(err));
|
|
958
|
+
}
|
|
959
|
+
try {
|
|
960
|
+
startNudgeSubscription(host, opts, context, setupCleanups);
|
|
961
|
+
} catch (err) {
|
|
962
|
+
log(host, 'warn', 'starting staleness nudge failed', errDetail(err));
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// The host calls this on unload/reload. It MUST be a function: returning
|
|
966
|
+
// a plain object used to abort the whole plugin reload with
|
|
967
|
+
// `TypeError: de is not a function`, dropping every registration.
|
|
968
|
+
return async () => {
|
|
969
|
+
let disposed = 0;
|
|
970
|
+
for (const registration of registrations) {
|
|
971
|
+
try {
|
|
972
|
+
await registration.dispose();
|
|
973
|
+
disposed += 1;
|
|
974
|
+
} catch (err) {
|
|
975
|
+
log(host, 'warn', 'registration.dispose() failed', errDetail(err));
|
|
759
976
|
}
|
|
760
977
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
// Disposer: the host calls setup's return value as a cleanup function
|
|
765
|
-
// on unload/reload. It MUST be a function (returning a plain object
|
|
766
|
-
// crashes host reload with "not a function"). Aborts nudge streams so
|
|
767
|
-
// reloads never stack duplicate subscriptions.
|
|
768
|
-
return () => {
|
|
769
|
-
for (const stop of [...setupCleanups]) {
|
|
770
|
-
setupCleanups.delete(stop);
|
|
978
|
+
registrations.length = 0;
|
|
979
|
+
let stopped = 0;
|
|
980
|
+
for (const stop of setupCleanups) {
|
|
771
981
|
try {
|
|
772
982
|
stop();
|
|
773
|
-
|
|
774
|
-
|
|
983
|
+
stopped += 1;
|
|
984
|
+
} catch (err) {
|
|
985
|
+
log(host, 'warn', 'cleanup failed', errDetail(err));
|
|
775
986
|
}
|
|
776
987
|
}
|
|
988
|
+
setupCleanups.clear();
|
|
989
|
+
log(host, 'info', 'iumbtems disposed', { registrations: disposed, nudges: stopped });
|
|
777
990
|
};
|
|
778
991
|
},
|
|
779
992
|
};
|
package/plugins/opencode/tui.js
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* `keymap.layer` (inside a component scope), `api.theme`.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
15
|
-
import path from 'path';
|
|
14
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
16
|
import { createElement, insert, setProp } from '@opentui/solid';
|
|
17
17
|
import { createEffect, createSignal, onCleanup } from 'solid-js';
|
|
18
18
|
import { OPENCODE_COMMANDS } from './index.js';
|
|
@@ -65,9 +65,8 @@ export function readSwarmStatus(root) {
|
|
|
65
65
|
suspect: 0,
|
|
66
66
|
requeued: 0,
|
|
67
67
|
};
|
|
68
|
-
let entries = [];
|
|
69
68
|
try {
|
|
70
|
-
|
|
69
|
+
readdirSync(research);
|
|
71
70
|
status.workspace = true;
|
|
72
71
|
} catch {
|
|
73
72
|
return status;
|
|
@@ -149,7 +148,7 @@ function summarize(status) {
|
|
|
149
148
|
return `IUMBTEMS: ${bits.join(' · ')}`;
|
|
150
149
|
}
|
|
151
150
|
|
|
152
|
-
function SwarmSidebar(api, root
|
|
151
|
+
function SwarmSidebar(api, root) {
|
|
153
152
|
const colors = themeFg(api?.theme);
|
|
154
153
|
const [snapshot, setSnapshot] = createSignal(readSwarmStatus(root));
|
|
155
154
|
createEffect(() => {
|
|
@@ -162,7 +161,6 @@ function SwarmSidebar(api, root, sessionID) {
|
|
|
162
161
|
}, REFRESH_MS);
|
|
163
162
|
onCleanup(() => clearInterval(timer));
|
|
164
163
|
});
|
|
165
|
-
void sessionID;
|
|
166
164
|
return box({}, [
|
|
167
165
|
() => {
|
|
168
166
|
const s = snapshot();
|
|
@@ -196,7 +194,7 @@ function SwarmKeymapLayer(api) {
|
|
|
196
194
|
// Every slash command gets a palette entry (usage toast; the host has no
|
|
197
195
|
// programmatic slash-invoke API, so discovery + usage guidance is the win).
|
|
198
196
|
for (const cmd of OPENCODE_COMMANDS || []) {
|
|
199
|
-
if (!cmd
|
|
197
|
+
if (!cmd?.name) continue;
|
|
200
198
|
commands.push({
|
|
201
199
|
id: `iumbtems.command.${cmd.name}`,
|
|
202
200
|
title: `/${cmd.name}`,
|
|
@@ -247,8 +245,8 @@ function registerSlot(api, name, render) {
|
|
|
247
245
|
export function setupTui(api) {
|
|
248
246
|
try {
|
|
249
247
|
const root = resolveRoot(api);
|
|
250
|
-
const offSidebar = registerSlot(api, 'sidebar.content', (
|
|
251
|
-
SwarmSidebar(api, root
|
|
248
|
+
const offSidebar = registerSlot(api, 'sidebar.content', () =>
|
|
249
|
+
SwarmSidebar(api, root)
|
|
252
250
|
);
|
|
253
251
|
const offApp = registerSlot(api, 'app', () => SwarmKeymapLayer(api));
|
|
254
252
|
return () => {
|