@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
|
@@ -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
|
}
|
|
@@ -449,16 +468,20 @@ export const OPENCODE_COMMANDS = [
|
|
|
449
468
|
];
|
|
450
469
|
|
|
451
470
|
/**
|
|
452
|
-
*
|
|
453
|
-
*
|
|
471
|
+
* Command catalog shaped for a declarative opencode config.
|
|
472
|
+
*
|
|
473
|
+
* V2's schema key is `commands` (plural) — `command` is the deprecated V1 key
|
|
474
|
+
* and is ignored by a V2 host. Registration on a live V2 host goes through
|
|
475
|
+
* `command.transform` instead of this; this exists so tests and docs can assert
|
|
476
|
+
* the catalog shape and the correct key.
|
|
454
477
|
*/
|
|
455
|
-
export function
|
|
456
|
-
|
|
478
|
+
export function commandCatalog() {
|
|
479
|
+
const out = {};
|
|
457
480
|
for (const cmd of OPENCODE_COMMANDS) {
|
|
458
|
-
if (
|
|
459
|
-
|
|
481
|
+
if (!cmd?.name) continue;
|
|
482
|
+
out[cmd.name] = { description: cmd.description, template: cmd.template };
|
|
460
483
|
}
|
|
461
|
-
return
|
|
484
|
+
return out;
|
|
462
485
|
}
|
|
463
486
|
|
|
464
487
|
/** Tool map (object form) for the server hook; array catalog stays canonical. */
|
|
@@ -469,7 +492,7 @@ function buildToolMap() {
|
|
|
469
492
|
{
|
|
470
493
|
...tool,
|
|
471
494
|
options: { codemode: false },
|
|
472
|
-
execute: async (args = {}, toolContext) =>
|
|
495
|
+
execute: async (args = {}, toolContext = undefined) =>
|
|
473
496
|
callMcp(tool.name, normalizeArgs(tool.name, args), toolContext?.cwd),
|
|
474
497
|
},
|
|
475
498
|
])
|
|
@@ -482,7 +505,7 @@ function buildToolMap() {
|
|
|
482
505
|
* Reference pattern: @prevalentware/opencode-goal-plugin subscribes via
|
|
483
506
|
* `context.event.subscribe({signal})`, detects idle as `session.idle` or an
|
|
484
507
|
* idle `session.status`, continues via `context.session.prompt`, and preserves
|
|
485
|
-
* state across compaction with
|
|
508
|
+
* state across compaction with `context.session.hook("compaction", ...)`.
|
|
486
509
|
* Every hook here is best-effort and never throws into the host.
|
|
487
510
|
*/
|
|
488
511
|
|
|
@@ -507,22 +530,31 @@ function readJsonFile(file) {
|
|
|
507
530
|
}
|
|
508
531
|
}
|
|
509
532
|
|
|
533
|
+
function parseLedgerEntries(ledger) {
|
|
534
|
+
const stale = [];
|
|
535
|
+
const suspect = [];
|
|
536
|
+
const last = new Map();
|
|
537
|
+
for (const e of ledger) {
|
|
538
|
+
if (typeof e?.claim_id === 'string') last.set(e.claim_id, e.to_status);
|
|
539
|
+
}
|
|
540
|
+
for (const [id, s] of last) {
|
|
541
|
+
if (s === 'STALE') stale.push(id);
|
|
542
|
+
else if (s === 'SUSPECT') suspect.push(id);
|
|
543
|
+
}
|
|
544
|
+
const lastAt = ledger.at(-1)?.at ?? '';
|
|
545
|
+
return { stale, suspect, fingerprint: `${ledger.length}:${lastAt}` };
|
|
546
|
+
}
|
|
547
|
+
|
|
510
548
|
/** Last-write-wins degraded claims + requeue list. Pure fs reads. */
|
|
511
549
|
export function readLedgerDegraded(root) {
|
|
512
550
|
const out = { stale: [], suspect: [], requeued: [], fingerprint: 'empty' };
|
|
513
551
|
try {
|
|
514
552
|
const ledger = readJsonFile(path.join(root, '.research', 'ledger', 'claim_status.json'));
|
|
515
553
|
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}`;
|
|
554
|
+
const parsed = parseLedgerEntries(ledger);
|
|
555
|
+
out.stale = parsed.stale;
|
|
556
|
+
out.suspect = parsed.suspect;
|
|
557
|
+
out.fingerprint = parsed.fingerprint;
|
|
526
558
|
}
|
|
527
559
|
const requeue = readJsonFile(path.join(root, '.research', 'requeue.json'));
|
|
528
560
|
if (Array.isArray(requeue)) {
|
|
@@ -635,6 +667,200 @@ export function startStalenessNudge(host, root) {
|
|
|
635
667
|
}
|
|
636
668
|
}
|
|
637
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Plugin log. Never throws.
|
|
672
|
+
*
|
|
673
|
+
* Before this existed every setup block swallowed errors with a bare
|
|
674
|
+
* `catch {}`, so `--log-level all` showed only host-side symptoms and a
|
|
675
|
+
* registration failure was indistinguishable from success.
|
|
676
|
+
*/
|
|
677
|
+
function log(host, level, message, detail) {
|
|
678
|
+
let text = message;
|
|
679
|
+
if (detail !== undefined) {
|
|
680
|
+
try {
|
|
681
|
+
text = `${message} ${JSON.stringify(detail)}`;
|
|
682
|
+
} catch {
|
|
683
|
+
text = `${message} [unserialisable detail]`;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
const sink = host?.client?.app?.log;
|
|
688
|
+
if (typeof sink === 'function') {
|
|
689
|
+
Promise.resolve(sink.call(host.client.app, {
|
|
690
|
+
body: { service: 'iumbtems', level, message: text },
|
|
691
|
+
})).catch(() => {});
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
} catch {
|
|
695
|
+
/* fall through to console */
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
(console.error || console.log)(`[iumbtems] ${level}: ${text}`);
|
|
699
|
+
} catch {
|
|
700
|
+
/* logging must never throw into the host */
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function errDetail(err) {
|
|
705
|
+
return { error: String(err?.message ?? err) };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function registerHostCommands(host) {
|
|
709
|
+
if (typeof host?.command?.transform !== 'function') return [];
|
|
710
|
+
|
|
711
|
+
// Snapshot which names are already taken *at setup time*. This snapshot is
|
|
712
|
+
// COPIED into the transform callback on every invocation and never grown.
|
|
713
|
+
//
|
|
714
|
+
// The host replays this callback on each command-registry rebuild, and the
|
|
715
|
+
// rebuilt registry starts from base config — so if the callback mutates a
|
|
716
|
+
// shared "already added" set, the second replay sees all six names as taken
|
|
717
|
+
// and adds nothing. That is exactly why the slash commands appeared at
|
|
718
|
+
// startup and vanished seconds later, and why only a full process restart
|
|
719
|
+
// brought them back (fresh setup -> fresh set).
|
|
720
|
+
//
|
|
721
|
+
// The reference plugin (@prevalentware/opencode-goal-plugin) gets this right
|
|
722
|
+
// with `const claimed = new Set(existingCommands)` inside the callback.
|
|
723
|
+
let existingNames = new Set();
|
|
724
|
+
try {
|
|
725
|
+
const listed = await host.command.list();
|
|
726
|
+
existingNames = new Set((listed?.data || []).map((c) => c?.name));
|
|
727
|
+
} catch (err) {
|
|
728
|
+
log(host, 'warn', 'command.list() failed; assuming an empty registry', errDetail(err));
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const registration = await host.command.transform((draft) => {
|
|
732
|
+
const claimed = new Set(existingNames); // fresh copy per replay
|
|
733
|
+
const added = [];
|
|
734
|
+
for (const cmd of OPENCODE_COMMANDS) {
|
|
735
|
+
if (!cmd?.name || claimed.has(cmd.name)) continue;
|
|
736
|
+
claimed.add(cmd.name);
|
|
737
|
+
added.push(cmd.name);
|
|
738
|
+
draft.add({
|
|
739
|
+
name: cmd.name,
|
|
740
|
+
description: cmd.description,
|
|
741
|
+
execute: async (input) => {
|
|
742
|
+
const args = input?.prompt?.text || '';
|
|
743
|
+
const prompt = (typeof input?.prompt === 'object' && input?.prompt !== null) ? input.prompt : {};
|
|
744
|
+
await host.session.prompt({
|
|
745
|
+
...prompt,
|
|
746
|
+
sessionID: input?.sessionID,
|
|
747
|
+
text: cmd.template.split('$ARGUMENTS').join(String(args).trim()),
|
|
748
|
+
delivery: input?.delivery,
|
|
749
|
+
});
|
|
750
|
+
},
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
log(host, 'debug', 'command.transform pass', {
|
|
754
|
+
added,
|
|
755
|
+
skippedAsPreexisting: [...existingNames].filter((n) => !added.includes(n)),
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// Force materialisation now instead of waiting for the host's own later
|
|
760
|
+
// reconcile (which is the pass that used to wipe them).
|
|
761
|
+
try {
|
|
762
|
+
if (typeof host.command.reload === 'function') await host.command.reload();
|
|
763
|
+
log(host, 'info', 'slash commands registered', { count: OPENCODE_COMMANDS.length });
|
|
764
|
+
} catch (err) {
|
|
765
|
+
log(host, 'warn', 'command.reload() failed', errDetail(err));
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return registration ? [registration] : [];
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
async function registerHostTools(host) {
|
|
772
|
+
if (typeof host?.tool?.transform !== 'function') return [];
|
|
773
|
+
const registration = await host.tool.transform((draft) => {
|
|
774
|
+
for (const [name, spec] of Object.entries(buildToolMap())) {
|
|
775
|
+
draft.add({
|
|
776
|
+
name,
|
|
777
|
+
description: spec.description,
|
|
778
|
+
input: spec.input,
|
|
779
|
+
options: { codemode: false },
|
|
780
|
+
execute: async (args = {}, toolContext = undefined) => {
|
|
781
|
+
const r = await callMcp(name, normalizeArgs(name, args), toolContext?.cwd);
|
|
782
|
+
return { content: r.content };
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
log(host, 'debug', 'tool.transform pass', { count: Object.keys(buildToolMap()).length });
|
|
787
|
+
});
|
|
788
|
+
try {
|
|
789
|
+
if (typeof host.tool.reload === 'function') await host.tool.reload();
|
|
790
|
+
} catch (err) {
|
|
791
|
+
log(host, 'warn', 'tool.reload() failed', errDetail(err));
|
|
792
|
+
}
|
|
793
|
+
return registration ? [registration] : [];
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* V2 compaction hook: keep .research state across context compaction.
|
|
798
|
+
*
|
|
799
|
+
* The V1 equivalent (`experimental.session.compacting`) lived in the old
|
|
800
|
+
* `server()` hook and was dead code on V2 — compaction silently lost the
|
|
801
|
+
* swarm state. Shape follows the reference plugin: push a text part onto
|
|
802
|
+
* `event.system`.
|
|
803
|
+
*/
|
|
804
|
+
async function registerCompactionHook(host, context) {
|
|
805
|
+
const hook = host?.session?.hook || context?.session?.hook;
|
|
806
|
+
if (typeof hook !== 'function') return [];
|
|
807
|
+
try {
|
|
808
|
+
const registration = await hook.call(host.session || context.session, 'compaction', async (event) => {
|
|
809
|
+
try {
|
|
810
|
+
const root =
|
|
811
|
+
event?.directory || host.location?.directory || context.location?.directory || event?.cwd;
|
|
812
|
+
if (!root) return;
|
|
813
|
+
const text = buildCompactionContext(root);
|
|
814
|
+
if (!text) return;
|
|
815
|
+
const system = Array.isArray(event?.system) ? event.system : null;
|
|
816
|
+
if (!system) return;
|
|
817
|
+
// Idempotent: the host may invoke this more than once per session.
|
|
818
|
+
if (system.some((part) => part?.type === 'text' && part?.text === text)) return;
|
|
819
|
+
system.push({ type: 'text', text });
|
|
820
|
+
} catch (err) {
|
|
821
|
+
log(host, 'warn', 'compaction hook failed', errDetail(err));
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
return registration ? [registration] : [];
|
|
825
|
+
} catch (err) {
|
|
826
|
+
log(host, 'warn', 'session.hook("compaction") unavailable', errDetail(err));
|
|
827
|
+
return [];
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async function applyConfigOptions(host, opts, context) {
|
|
832
|
+
if (!opts || typeof opts !== 'object') return;
|
|
833
|
+
const updates = {};
|
|
834
|
+
if (opts.search_engine) updates.search_engine = opts.search_engine;
|
|
835
|
+
if (opts.max_iterations) updates.max_iterations = opts.max_iterations;
|
|
836
|
+
if (opts.mode) updates.mode = opts.mode;
|
|
837
|
+
if (opts.divergence_threshold !== undefined) {
|
|
838
|
+
updates.divergence_threshold = opts.divergence_threshold;
|
|
839
|
+
}
|
|
840
|
+
if (Object.keys(updates).length > 0) {
|
|
841
|
+
await callMcp(
|
|
842
|
+
'iumbtems_config',
|
|
843
|
+
updates,
|
|
844
|
+
host.location?.directory || context.location?.directory || host.cwd
|
|
845
|
+
);
|
|
846
|
+
log(host, 'debug', 'applied tuple options via iumbtems_config', updates);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function startNudgeSubscription(host, opts, context, cleanups) {
|
|
851
|
+
if (!(opts?.staleness_nudge ?? true)) return;
|
|
852
|
+
const nudgeHost = {
|
|
853
|
+
event: host.event || context.event,
|
|
854
|
+
session: host.session || context.session,
|
|
855
|
+
};
|
|
856
|
+
const root = host.location?.directory || context.location?.directory;
|
|
857
|
+
if (root) {
|
|
858
|
+
const stop = startStalenessNudge(nudgeHost, root);
|
|
859
|
+
if (typeof stop === 'function') cleanups.add(stop);
|
|
860
|
+
log(host, 'debug', 'staleness nudge subscription', { root, started: typeof stop === 'function' });
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
638
864
|
export function createOpenCodePlugin(context = {}) {
|
|
639
865
|
return {
|
|
640
866
|
id: 'heretek.iumbtems.epistemic-swarm',
|
|
@@ -643,137 +869,77 @@ export function createOpenCodePlugin(context = {}) {
|
|
|
643
869
|
description:
|
|
644
870
|
'I Use My Brain To Express My Self: High-integrity dialectic research, code audits, and open-source scouting',
|
|
645
871
|
|
|
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
|
-
|
|
872
|
+
// V2 only. The V1 `server()` factory and its `config` hook wrote the
|
|
873
|
+
// deprecated `command` key (V2's schema is `commands`), so they registered
|
|
874
|
+
// nothing on a V2 host — and carrying both shapes meant the real
|
|
875
|
+
// registration path had to guess which host it was talking to.
|
|
665
876
|
setup: async (appContext) => {
|
|
666
877
|
const host = appContext || {};
|
|
667
878
|
const opts = context.options || host.options;
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
|
|
879
|
+
// Transform registrations must precede any slow work: the host may
|
|
880
|
+
// reconcile past a slow setup and never see the commands.
|
|
881
|
+
const registrations = [];
|
|
882
|
+
const adopt = (list) => {
|
|
883
|
+
for (const r of list || []) if (r) registrations.push(r);
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
log(host, 'info', 'iumbtems setup', {
|
|
887
|
+
version: PKG_VERSION,
|
|
888
|
+
directory: host.location?.directory || context.location?.directory || null,
|
|
889
|
+
});
|
|
890
|
+
|
|
671
891
|
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 */
|
|
892
|
+
adopt(await registerHostCommands(host));
|
|
893
|
+
} catch (err) {
|
|
894
|
+
log(host, 'error', 'registering slash commands failed', errDetail(err));
|
|
703
895
|
}
|
|
704
896
|
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 */
|
|
897
|
+
adopt(await registerHostTools(host));
|
|
898
|
+
} catch (err) {
|
|
899
|
+
log(host, 'error', 'registering tools failed', errDetail(err));
|
|
723
900
|
}
|
|
724
|
-
// Slow work AFTER registration: apply tuple options via MCP config
|
|
725
|
-
// write, scoped to the plugin location (never the service cwd).
|
|
726
901
|
try {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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 */
|
|
902
|
+
adopt(await registerCompactionHook(host, context));
|
|
903
|
+
} catch (err) {
|
|
904
|
+
log(host, 'error', 'registering compaction hook failed', errDetail(err));
|
|
905
|
+
}
|
|
906
|
+
// Slow work AFTER registration.
|
|
907
|
+
try {
|
|
908
|
+
await applyConfigOptions(host, opts, context);
|
|
909
|
+
} catch (err) {
|
|
910
|
+
log(host, 'warn', 'applying tuple options failed', errDetail(err));
|
|
745
911
|
}
|
|
746
|
-
// Phase C: idle staleness nudge (opt out with {staleness_nudge: false}).
|
|
747
|
-
// Subscribes only when the host exposes event/session; one per root.
|
|
748
912
|
try {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
913
|
+
startNudgeSubscription(host, opts, context, setupCleanups);
|
|
914
|
+
} catch (err) {
|
|
915
|
+
log(host, 'warn', 'starting staleness nudge failed', errDetail(err));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// The host calls this on unload/reload. It MUST be a function: returning
|
|
919
|
+
// a plain object used to abort the whole plugin reload with
|
|
920
|
+
// `TypeError: de is not a function`, dropping every registration.
|
|
921
|
+
return async () => {
|
|
922
|
+
let disposed = 0;
|
|
923
|
+
for (const registration of registrations) {
|
|
924
|
+
try {
|
|
925
|
+
await registration.dispose();
|
|
926
|
+
disposed += 1;
|
|
927
|
+
} catch (err) {
|
|
928
|
+
log(host, 'warn', 'registration.dispose() failed', errDetail(err));
|
|
759
929
|
}
|
|
760
930
|
}
|
|
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);
|
|
931
|
+
registrations.length = 0;
|
|
932
|
+
let stopped = 0;
|
|
933
|
+
for (const stop of setupCleanups) {
|
|
771
934
|
try {
|
|
772
935
|
stop();
|
|
773
|
-
|
|
774
|
-
|
|
936
|
+
stopped += 1;
|
|
937
|
+
} catch (err) {
|
|
938
|
+
log(host, 'warn', 'cleanup failed', errDetail(err));
|
|
775
939
|
}
|
|
776
940
|
}
|
|
941
|
+
setupCleanups.clear();
|
|
942
|
+
log(host, 'info', 'iumbtems disposed', { registrations: disposed, nudges: stopped });
|
|
777
943
|
};
|
|
778
944
|
},
|
|
779
945
|
};
|
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 () => {
|