@heretek-ai/epistemic-swarm 0.4.2 → 0.4.4
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/hooks/hooks.json +22 -1
- package/install.sh +25 -0
- 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 +44 -15
- package/plugins/opencode/tui.js +40 -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__/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/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_webcache.cpython-311.pyc +0 -0
- package/runner/tests/test_bet1_spike.py +95 -0
- package/runner/tests/test_opencode_ux.py +76 -0
- package/runner/tests/test_webcache.py +222 -0
- package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
- package/scripts/bet1_advisory_spike.py +426 -0
- package/skills/epistemic_search/scripts/webcache.py +292 -0
- package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
package/hooks/hooks.json
CHANGED
|
@@ -15,7 +15,28 @@
|
|
|
15
15
|
"hooks": [
|
|
16
16
|
{
|
|
17
17
|
"type": "command",
|
|
18
|
-
"command": "
|
|
18
|
+
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/skills/epistemic_search/scripts/webcache.py\" gate"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"PostToolUse": [
|
|
24
|
+
{
|
|
25
|
+
"matcher": "WebFetch",
|
|
26
|
+
"hooks": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/skills/epistemic_search/scripts/webcache.py\" archive"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"SessionStart": [
|
|
35
|
+
{
|
|
36
|
+
"hooks": [
|
|
37
|
+
{
|
|
38
|
+
"type": "command",
|
|
39
|
+
"command": "python3 \"${CLAUDE_PLUGIN_ROOT}/skills/epistemic_search/scripts/webcache.py\" stats"
|
|
19
40
|
}
|
|
20
41
|
]
|
|
21
42
|
}
|
package/install.sh
CHANGED
|
@@ -177,6 +177,31 @@ try:
|
|
|
177
177
|
print(f" + Added agent: {name}")
|
|
178
178
|
else:
|
|
179
179
|
print(f" ℹ️ Agent already configured: {name}")
|
|
180
|
+
|
|
181
|
+
# Skills: snippet paths are repo-relative ("./skills/x") which breaks
|
|
182
|
+
# from ~/.config — merge as absolute paths, repairing relative entries.
|
|
183
|
+
import os as _os
|
|
184
|
+
wanted = []
|
|
185
|
+
for sp in snippet.get("skills", {}).get("paths", []):
|
|
186
|
+
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)):
|
|
191
|
+
for abs_sp in wanted:
|
|
192
|
+
if existing == abs_sp:
|
|
193
|
+
break
|
|
194
|
+
rel = "./" + str(Path(abs_sp).relative_to(repo)) if str(abs_sp).startswith(str(repo)) else None
|
|
195
|
+
if rel and existing == rel:
|
|
196
|
+
paths[i] = abs_sp
|
|
197
|
+
print(f" ~ Repaired relative skill path: {rel} -> {abs_sp}")
|
|
198
|
+
break
|
|
199
|
+
for abs_sp in wanted:
|
|
200
|
+
if abs_sp not in paths:
|
|
201
|
+
paths.append(abs_sp)
|
|
202
|
+
print(f" + Added skill path: {abs_sp}")
|
|
203
|
+
else:
|
|
204
|
+
print(f" ℹ️ Skill path already configured: {abs_sp}")
|
|
180
205
|
except Exception as e:
|
|
181
206
|
print(f" ⚠️ Non-critical warning merging agents: {e}")
|
|
182
207
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heretek-ai/epistemic-swarm",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
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": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://antigravity.google/schemas/v1/plugin.json",
|
|
3
3
|
"name": "epistemic-swarm",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.4",
|
|
5
5
|
"description": "IUMBTEMS Epistemic Swarm: dialectic research, code audit, OSS scout, and lateral brainstorming for AntiGravity.",
|
|
6
6
|
"author": "Heretek AI",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* catalog. Schemas mirror runner/mcp_server.py build_tools() — keep in sync.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { spawn } from 'child_process';
|
|
15
15
|
import { readFileSync } from 'fs';
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import { fileURLToPath } from 'url';
|
|
@@ -30,21 +30,40 @@ try {
|
|
|
30
30
|
// keep fallback; version is informational only
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Uniform dispatch: one-shot MCP call, return OpenCode's {content, status}.
|
|
35
|
+
* Async (non-blocking spawn) so long swarms/audits never freeze the host
|
|
36
|
+
* event loop; cancellation remains the host's prerogative.
|
|
37
|
+
*/
|
|
34
38
|
function callMcp(tool, args = {}, cwd) {
|
|
35
|
-
|
|
36
|
-
'
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
let stdout = '';
|
|
41
|
+
let stderr = '';
|
|
42
|
+
let settled = false;
|
|
43
|
+
const done = (content, status) => {
|
|
44
|
+
if (settled) return;
|
|
45
|
+
settled = true;
|
|
46
|
+
resolve({ content, status });
|
|
47
|
+
};
|
|
48
|
+
let child;
|
|
49
|
+
try {
|
|
50
|
+
child = spawn(
|
|
51
|
+
'python3',
|
|
52
|
+
[MCP_SERVER, 'call', tool, JSON.stringify(args || {})],
|
|
53
|
+
{
|
|
54
|
+
cwd: cwd || process.cwd(),
|
|
55
|
+
env: { ...process.env, PYTHONPATH: PKG_ROOT },
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
done(String((err && err.message) || err), 'error');
|
|
60
|
+
return;
|
|
42
61
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
};
|
|
62
|
+
child.stdout.on('data', (d) => { stdout += String(d); });
|
|
63
|
+
child.stderr.on('data', (d) => { stderr += String(d); });
|
|
64
|
+
child.on('error', (err) => done(String((err && err.message) || err), 'error'));
|
|
65
|
+
child.on('close', (code) => done(stdout || stderr, code === 0 ? 'success' : 'error'));
|
|
66
|
+
});
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
/** Catalog mirrors runner/mcp_server.py build_tools(). */
|
|
@@ -351,9 +370,11 @@ export const OPENCODE_COMMANDS = [
|
|
|
351
370
|
{
|
|
352
371
|
name: 'swarm',
|
|
353
372
|
description: 'Run IUMBTEMS dialectic research swarm on an objective',
|
|
373
|
+
usage: '/swarm <objective>',
|
|
354
374
|
template: [
|
|
355
375
|
'Run an IUMBTEMS dialectic research swarm.',
|
|
356
376
|
'Objective: $ARGUMENTS',
|
|
377
|
+
'If $ARGUMENTS is empty, ask the user for the research objective first; never call the tool with placeholder, empty, or literal "<objective>" arguments.',
|
|
357
378
|
'1. If the objective is ambiguous, frame it first via iumbtems_socratic_frontier.',
|
|
358
379
|
'2. Execute iumbtems_swarm_research with {"objective": "<objective>"} (pass "mock_mode": true only for dry runs).',
|
|
359
380
|
'3. Summarize .research/final_synthesis.md, preserving [VERIFIED:<hash>] pointers.',
|
|
@@ -362,6 +383,7 @@ export const OPENCODE_COMMANDS = [
|
|
|
362
383
|
{
|
|
363
384
|
name: 'grill',
|
|
364
385
|
description: 'Launch Socratic grilling and decision tree frontier exploration',
|
|
386
|
+
usage: '/grill [--objective <text>]',
|
|
365
387
|
template: [
|
|
366
388
|
'Launch Socratic grilling on the decision frontier.',
|
|
367
389
|
'Objective: $ARGUMENTS (may be empty to inspect the current frontier)',
|
|
@@ -372,6 +394,7 @@ export const OPENCODE_COMMANDS = [
|
|
|
372
394
|
{
|
|
373
395
|
name: 'swarm-config',
|
|
374
396
|
description: 'Inspect or update Epistemic Swarm parameters (engine, depth, mode)',
|
|
397
|
+
usage: '/swarm-config [--engine <e>] [--depth <d>] [--mode <m>] [--show]',
|
|
375
398
|
template: [
|
|
376
399
|
'Inspect or update the Epistemic Swarm configuration.',
|
|
377
400
|
'Arguments: $ARGUMENTS (may be empty to inspect current settings)',
|
|
@@ -381,9 +404,11 @@ export const OPENCODE_COMMANDS = [
|
|
|
381
404
|
{
|
|
382
405
|
name: 'audit',
|
|
383
406
|
description: 'Run dialectic codebase architectural and security audit with line-level proof',
|
|
407
|
+
usage: '/audit <target_path_or_scope>',
|
|
384
408
|
template: [
|
|
385
409
|
'Run an IUMBTEMS dialectic codebase audit (structural architect vs adversarial red-teamer).',
|
|
386
410
|
'Target: $ARGUMENTS (path, component, or empty for full-repository architecture and vulnerability audit)',
|
|
411
|
+
'If $ARGUMENTS names a path that does not exist, ask the user to clarify the target first; never audit a placeholder path.',
|
|
387
412
|
'1. Execute iumbtems_code_audit with {"target": "<target>"} (pass "mock_mode": true only for dry runs).',
|
|
388
413
|
'2. Summarize .research/code_audit_report.md with line-level proof pointers.',
|
|
389
414
|
].join('\n'),
|
|
@@ -391,9 +416,11 @@ export const OPENCODE_COMMANDS = [
|
|
|
391
416
|
{
|
|
392
417
|
name: 'scout',
|
|
393
418
|
description: 'Scout open-source libraries, audit copyleft licenses, generate clean-room blueprints',
|
|
419
|
+
usage: '/scout <feature_or_algorithm>',
|
|
394
420
|
template: [
|
|
395
421
|
'Scout open-source solutions for the requested capability.',
|
|
396
422
|
'Feature: $ARGUMENTS',
|
|
423
|
+
'If $ARGUMENTS is empty, ask the user for the feature or algorithm first; never call the tool with placeholder, empty, or literal "<feature>" arguments.',
|
|
397
424
|
'1. Execute iumbtems_oss_scout with {"feature": "<feature>"} (pass "mock_mode": true only for dry runs).',
|
|
398
425
|
'2. Report mature candidates, GPL/AGPL copyleft risks, and the clean-room blueprint in .research/oss_scout_report.md.',
|
|
399
426
|
].join('\n'),
|
|
@@ -401,6 +428,7 @@ export const OPENCODE_COMMANDS = [
|
|
|
401
428
|
{
|
|
402
429
|
name: 'brainstorming',
|
|
403
430
|
description: 'Lateral brainstorming: novel feature vectors, paradigm inversions, falsifiable spikes',
|
|
431
|
+
usage: '/brainstorming <ambiguous-prompt>',
|
|
404
432
|
template: [
|
|
405
433
|
'Run lateral brainstorming (divergent what-if ideation, never bug-fix lists).',
|
|
406
434
|
'Prompt: $ARGUMENTS (defaults to "Where do we go from here?" when empty)',
|
|
@@ -411,6 +439,7 @@ export const OPENCODE_COMMANDS = [
|
|
|
411
439
|
{
|
|
412
440
|
name: 'brainstorm',
|
|
413
441
|
description: 'Alias for /brainstorming',
|
|
442
|
+
usage: '/brainstorm <ambiguous-prompt>',
|
|
414
443
|
template: [
|
|
415
444
|
'Alias for /brainstorming: run lateral brainstorming (divergent what-if ideation, never bug-fix lists).',
|
|
416
445
|
'Prompt: $ARGUMENTS (defaults to "Where do we go from here?" when empty)',
|
|
@@ -640,7 +669,7 @@ export function createOpenCodePlugin(context = {}) {
|
|
|
640
669
|
updates.divergence_threshold = opts.divergence_threshold;
|
|
641
670
|
}
|
|
642
671
|
if (Object.keys(updates).length > 0) {
|
|
643
|
-
callMcp('iumbtems_config', updates, appContext?.cwd);
|
|
672
|
+
await callMcp('iumbtems_config', updates, appContext?.cwd);
|
|
644
673
|
}
|
|
645
674
|
}
|
|
646
675
|
// Phase C: idle staleness nudge (opt out with {staleness_nudge: false}).
|
package/plugins/opencode/tui.js
CHANGED
|
@@ -15,6 +15,7 @@ import { readFileSync, readdirSync, statSync } from 'fs';
|
|
|
15
15
|
import path from 'path';
|
|
16
16
|
import { createElement, insert, setProp } from '@opentui/solid';
|
|
17
17
|
import { createEffect, createSignal, onCleanup } from 'solid-js';
|
|
18
|
+
import { OPENCODE_COMMANDS } from './index.js';
|
|
18
19
|
|
|
19
20
|
const PLUGIN_ID = 'heretek.iumbtems.epistemic-swarm.tui';
|
|
20
21
|
const REFRESH_MS = 5000;
|
|
@@ -172,26 +173,47 @@ function SwarmSidebar(api, root, sessionID) {
|
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
function SwarmKeymapLayer(api) {
|
|
176
|
+
const toast = (title, message) => {
|
|
177
|
+
try {
|
|
178
|
+
api.ui.toast.show({ title, message, variant: 'info', duration: 4000 });
|
|
179
|
+
} catch {
|
|
180
|
+
/* toast is best-effort */
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const commands = [
|
|
184
|
+
{
|
|
185
|
+
id: 'iumbtems.swarm-status',
|
|
186
|
+
title: 'Swarm status',
|
|
187
|
+
description: 'Show IUMBTEMS workspace status (mode, reports, frontier, STALE claims)',
|
|
188
|
+
group: 'IUMBTEMS',
|
|
189
|
+
palette: true,
|
|
190
|
+
run: () => {
|
|
191
|
+
const root = resolveRoot(api);
|
|
192
|
+
toast('IUMBTEMS', summarize(readSwarmStatus(root)));
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
// Every slash command gets a palette entry (usage toast; the host has no
|
|
197
|
+
// programmatic slash-invoke API, so discovery + usage guidance is the win).
|
|
198
|
+
for (const cmd of OPENCODE_COMMANDS || []) {
|
|
199
|
+
if (!cmd || !cmd.name) continue;
|
|
200
|
+
commands.push({
|
|
201
|
+
id: `iumbtems.command.${cmd.name}`,
|
|
202
|
+
title: `/${cmd.name}`,
|
|
203
|
+
description: cmd.description || cmd.name,
|
|
204
|
+
group: 'IUMBTEMS',
|
|
205
|
+
palette: true,
|
|
206
|
+
run: () => {
|
|
207
|
+
toast(
|
|
208
|
+
`IUMBTEMS /${cmd.name}`,
|
|
209
|
+
`${cmd.usage || cmd.description || ''} — type /${cmd.name} in the prompt to run.`
|
|
210
|
+
);
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
175
214
|
api.keymap.layer(() => ({
|
|
176
215
|
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
|
-
],
|
|
216
|
+
commands,
|
|
195
217
|
}));
|
|
196
218
|
return null;
|
|
197
219
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CI-safe tests for the Bet-1 advisory spike harness (no LLM, no network)."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
12
|
+
if str(PROJECT_ROOT) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
14
|
+
|
|
15
|
+
from scripts.bet1_advisory_spike import ( # noqa: E402
|
|
16
|
+
advisory_comment,
|
|
17
|
+
generate,
|
|
18
|
+
score_fixture,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestBet1Harness(unittest.TestCase):
|
|
23
|
+
def test_generate_counts_and_layout(self):
|
|
24
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
25
|
+
manifest = generate(Path(tmp))
|
|
26
|
+
self.assertEqual(len(manifest), 20)
|
|
27
|
+
defective = [e for e in manifest if not e["clean"]]
|
|
28
|
+
clean = [e for e in manifest if e["clean"]]
|
|
29
|
+
self.assertEqual(len(defective), 16)
|
|
30
|
+
self.assertEqual(len(clean), 4)
|
|
31
|
+
for entry in manifest:
|
|
32
|
+
self.assertTrue((Path(tmp) / entry["id"] / entry["file"]).exists())
|
|
33
|
+
|
|
34
|
+
def test_generate_deterministic(self):
|
|
35
|
+
with tempfile.TemporaryDirectory() as t1, tempfile.TemporaryDirectory() as t2:
|
|
36
|
+
m1 = generate(Path(t1))
|
|
37
|
+
m2 = generate(Path(t2))
|
|
38
|
+
self.assertEqual(m1, m2)
|
|
39
|
+
|
|
40
|
+
def test_scorer_hit_and_tolerance(self):
|
|
41
|
+
entry = {"id": "x", "file": "users.py", "line": 6, "clean": False}
|
|
42
|
+
self.assertTrue(score_fixture(entry, "see users.py:6 SQLi")["hit"])
|
|
43
|
+
self.assertTrue(score_fixture(entry, "users.py L8 concat")["hit"])
|
|
44
|
+
self.assertFalse(score_fixture(entry, "users.py:40 unrelated")["hit"])
|
|
45
|
+
self.assertFalse(score_fixture(entry, "no file cited")["hit"])
|
|
46
|
+
|
|
47
|
+
def test_scorer_clean_flags_fp(self):
|
|
48
|
+
entry = {"id": "c", "file": "util.py", "line": None, "clean": True}
|
|
49
|
+
self.assertTrue(score_fixture(entry, "issue in util.py:2")["false_positive"])
|
|
50
|
+
self.assertFalse(
|
|
51
|
+
score_fixture(entry, "all clear, nothing found")["false_positive"]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def test_advisory_is_non_blocking_shape(self):
|
|
55
|
+
comment = advisory_comment("bet1-01", "findings text")
|
|
56
|
+
self.assertIn("non-blocking", comment)
|
|
57
|
+
self.assertIn("bet1-01", comment)
|
|
58
|
+
self.assertIn("findings text", comment)
|
|
59
|
+
|
|
60
|
+
def test_cli_generate_and_score_exit_zero(self):
|
|
61
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
62
|
+
for argv in (["--generate", "--dir", tmp], ["--score", "--dir", tmp]):
|
|
63
|
+
res = subprocess.run(
|
|
64
|
+
[
|
|
65
|
+
sys.executable,
|
|
66
|
+
str(PROJECT_ROOT / "scripts" / "bet1_advisory_spike.py"),
|
|
67
|
+
]
|
|
68
|
+
+ argv,
|
|
69
|
+
capture_output=True,
|
|
70
|
+
text=True,
|
|
71
|
+
timeout=120,
|
|
72
|
+
)
|
|
73
|
+
self.assertEqual(res.returncode, 0, res.stderr)
|
|
74
|
+
summary = json.loads(
|
|
75
|
+
subprocess.run(
|
|
76
|
+
[
|
|
77
|
+
sys.executable,
|
|
78
|
+
str(PROJECT_ROOT / "scripts" / "bet1_advisory_spike.py"),
|
|
79
|
+
"--score",
|
|
80
|
+
"--dir",
|
|
81
|
+
tmp,
|
|
82
|
+
],
|
|
83
|
+
capture_output=True,
|
|
84
|
+
text=True,
|
|
85
|
+
timeout=120,
|
|
86
|
+
).stdout
|
|
87
|
+
)
|
|
88
|
+
self.assertEqual(summary["fixtures"], 20)
|
|
89
|
+
# No advisories written yet -> all misses, no false positives.
|
|
90
|
+
self.assertEqual(summary["hits"], 0)
|
|
91
|
+
self.assertEqual(summary["false_positives"], 0)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
unittest.main()
|
|
@@ -75,6 +75,39 @@ class TestOpenCodeCommandCatalog(unittest.TestCase):
|
|
|
75
75
|
self.assertIn(c, data["registered"])
|
|
76
76
|
self.assertTrue(data["templates"][c])
|
|
77
77
|
|
|
78
|
+
def test_empty_arg_guards(self):
|
|
79
|
+
res = run_node(
|
|
80
|
+
"""
|
|
81
|
+
import { OPENCODE_COMMANDS } from "./plugins/opencode/index.js";
|
|
82
|
+
const guarded = OPENCODE_COMMANDS.filter(c => /empty/i.test(c.template) && /ask the user/i.test(c.template)).map(c => c.name);
|
|
83
|
+
const withUsage = OPENCODE_COMMANDS.filter(c => !!c.usage).map(c => c.name);
|
|
84
|
+
console.log(JSON.stringify({guarded, withUsage, total: OPENCODE_COMMANDS.length}));
|
|
85
|
+
"""
|
|
86
|
+
)
|
|
87
|
+
self.assertEqual(res.returncode, 0, f"guard test failed: {res.stderr}")
|
|
88
|
+
data = last_json_object(res.stdout)
|
|
89
|
+
for c in ["swarm", "scout", "audit"]:
|
|
90
|
+
self.assertIn(c, data["guarded"])
|
|
91
|
+
self.assertEqual(len(data["withUsage"]), data["total"])
|
|
92
|
+
|
|
93
|
+
def test_async_execute_resolves(self):
|
|
94
|
+
res = run_node(
|
|
95
|
+
"""
|
|
96
|
+
import plugin from "./plugins/opencode/index.js";
|
|
97
|
+
const shell = await plugin.server();
|
|
98
|
+
let ticked = false;
|
|
99
|
+
const timer = setInterval(() => { ticked = true; }, 5);
|
|
100
|
+
const r = await shell.tool.iumbtems_config.execute({});
|
|
101
|
+
clearInterval(timer);
|
|
102
|
+
console.log(JSON.stringify({status: r.status, hasContent: typeof r.content === "string" && r.content.length > 0, loopAlive: ticked}));
|
|
103
|
+
"""
|
|
104
|
+
)
|
|
105
|
+
self.assertEqual(res.returncode, 0, f"async execute test failed: {res.stderr}")
|
|
106
|
+
data = last_json_object(res.stdout)
|
|
107
|
+
self.assertEqual(data["status"], "success")
|
|
108
|
+
self.assertTrue(data["hasContent"])
|
|
109
|
+
self.assertTrue(data["loopAlive"])
|
|
110
|
+
|
|
78
111
|
def test_registration_never_overwrites(self):
|
|
79
112
|
res = run_node(
|
|
80
113
|
"""
|
|
@@ -180,6 +213,13 @@ class TestInstallOpenCode(unittest.TestCase):
|
|
|
180
213
|
cmd = cfg["mcp"]["iumbtems"]["command"]
|
|
181
214
|
self.assertTrue(Path(cmd[-1]).is_absolute())
|
|
182
215
|
self.assertEqual(cmd[-1], str(PROJECT_ROOT / "runner" / "mcp_server.py"))
|
|
216
|
+
# Skills merged as absolute repo paths (relative entries repaired).
|
|
217
|
+
skill_paths = cfg.get("skills", {}).get("paths", [])
|
|
218
|
+
self.assertTrue(skill_paths)
|
|
219
|
+
for sp in skill_paths:
|
|
220
|
+
self.assertTrue(Path(sp).is_absolute(), f"relative skill path: {sp}")
|
|
221
|
+
self.assertIn(str(PROJECT_ROOT / "skills" / "grilling"), skill_paths)
|
|
222
|
+
self.assertFalse(any(not Path(sp).is_absolute() for sp in skill_paths))
|
|
183
223
|
# Second run changes nothing.
|
|
184
224
|
before = oc_path.read_bytes()
|
|
185
225
|
second = self._run_install(home)
|
|
@@ -285,6 +325,42 @@ class TestOpenCodeTui(unittest.TestCase):
|
|
|
285
325
|
self.assertEqual(data["suspect"], 1)
|
|
286
326
|
self.assertEqual(data["requeued"], 2)
|
|
287
327
|
|
|
328
|
+
def test_palette_entries_for_all_commands(self):
|
|
329
|
+
res = run_node(
|
|
330
|
+
"""
|
|
331
|
+
import { setupTui } from "./plugins/opencode/tui.js";
|
|
332
|
+
import { OPENCODE_COMMANDS } from "./plugins/opencode/index.js";
|
|
333
|
+
const renders = [];
|
|
334
|
+
let factory = null;
|
|
335
|
+
const toasts = [];
|
|
336
|
+
const mock = {
|
|
337
|
+
theme: {}, directory: process.cwd(),
|
|
338
|
+
ui: { slot: (arg) => { renders.push(arg); return () => {}; }, toast: { show: (t) => toasts.push(t) }, router: { current: () => ({type: "other"}) } },
|
|
339
|
+
keymap: { layer: (fn) => { factory = fn; } }
|
|
340
|
+
};
|
|
341
|
+
setupTui(mock);
|
|
342
|
+
const appSlot = renders.find(r => r && r.append === "app");
|
|
343
|
+
if (!appSlot) throw new Error("app slot not registered");
|
|
344
|
+
appSlot.render();
|
|
345
|
+
const spec = factory();
|
|
346
|
+
const palette = spec.commands.filter(c => c.palette);
|
|
347
|
+
for (const c of palette) c.run();
|
|
348
|
+
console.log(JSON.stringify({
|
|
349
|
+
total: spec.commands.length,
|
|
350
|
+
palette: palette.length,
|
|
351
|
+
groups: [...new Set(palette.map(c => c.group))],
|
|
352
|
+
toasts: toasts.length,
|
|
353
|
+
expected: OPENCODE_COMMANDS.length + 1
|
|
354
|
+
}));
|
|
355
|
+
"""
|
|
356
|
+
)
|
|
357
|
+
self.assertEqual(res.returncode, 0, f"palette test failed: {res.stderr}")
|
|
358
|
+
data = last_json_object(res.stdout)
|
|
359
|
+
self.assertEqual(data["total"], data["expected"])
|
|
360
|
+
self.assertEqual(data["palette"], data["expected"])
|
|
361
|
+
self.assertEqual(data["groups"], ["IUMBTEMS"])
|
|
362
|
+
self.assertEqual(data["toasts"], data["expected"])
|
|
363
|
+
|
|
288
364
|
|
|
289
365
|
class TestOpenCodeLifecycle(unittest.TestCase):
|
|
290
366
|
@staticmethod
|