@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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Darkharvest (thin adapter stub)
|
|
2
|
+
|
|
3
|
+
This file is a POINTER, not the implementation. It exists so harness skill
|
|
4
|
+
discovery finds an entry; the real skill lives in the IUMBTEMS repo.
|
|
5
|
+
|
|
6
|
+
- Canonical prose & scripts: `skills/darkharvest/`
|
|
7
|
+
- Canonical programmatic surface: `python3 runner/mcp_server.py` (stdio MCP),
|
|
8
|
+
or one-shot: `python3 runner/mcp_server.py call <tool> '{...json...}'`
|
|
9
|
+
- MCP tools for this skill: `iumbtems_darkharvest`
|
|
10
|
+
|
|
11
|
+
Epistemic rules apply regardless of harness: tag claims as
|
|
12
|
+
`[VERIFIED: <hash>]`, `[INFERRED: <reasoning>]`, `[HYPOTHESIS: <test>]`, or
|
|
13
|
+
`[NEGATIVE_KNOWLEDGE: <query>]`. Writes go only to `.research/`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Product competitor teardown with per-feature harvest verdicts
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Run a product competitor teardown for `$1`:
|
|
6
|
+
|
|
7
|
+
`python3 runner/research_swarm.py --mode darkharvest --objective "$1"`
|
|
8
|
+
|
|
9
|
+
Seeds may be embedded as repo URLs; cap live runs at `--max-repos 6`.
|
|
10
|
+
Permissive-only vendor; GPL/AGPL clean-room-rebuild only; workflows clonable, assets never.
|
|
11
|
+
Report: `.research/darkharvest_report.md` (matrix + white-space gaps + SPDX backlog).
|
package/README.md
CHANGED
|
@@ -146,6 +146,9 @@ iumbtems run "Sub-millisecond ZK state updates on L1 rollups"
|
|
|
146
146
|
# Run lateral brainstorming (divergent what-if ideation, never bug fixes)
|
|
147
147
|
iumbtems brainstorm "Where do we go from here?"
|
|
148
148
|
|
|
149
|
+
# Product competitor teardown with per-feature harvest verdicts
|
|
150
|
+
iumbtems darkharvest "Paseo-class agent harness competitor" --seeds https://github.com/a/b,https://github.com/c/d --max-repos 6 --mock-claude
|
|
151
|
+
|
|
149
152
|
# Run Socratic grilling and decision frontier calculation
|
|
150
153
|
iumbtems grill --objective "L1 vs L2 state verification trade-offs"
|
|
151
154
|
|
|
@@ -160,6 +163,7 @@ iumbtems test
|
|
|
160
163
|
- **`/swarm-config`**: Interactive tuning of search engines (DuckDuckGo, Brave, Firecrawl, SearXNG), iteration depth, divergence thresholds, and operating modes.
|
|
161
164
|
- **`/code-audit`**: Dialectic codebase review pairing a Structural Architect (thesis) with a Vulnerability Red-Teamer (antithesis) enforcing line-number proofs (`file:///path#L10-25`).
|
|
162
165
|
- **`/oss-scout`**: Evaluates GitHub repositories, package ecosystems (npm, crates.io, PyPI), license contamination (GPL/AGPL copyleft vs MIT/Apache), and outputs clean-room re-implementation blueprints.
|
|
166
|
+
- **`/darkharvest`**: Product competitor teardown (seed inspirations + prompt, expand to adjacents). Competitor × capability matrix, both-ways white-space gaps, per-feature `depend|vendor|clean-room-rebuild|skip` verdicts with SPDX attribution. Permissive-only vendoring; GPL/AGPL spec-rebuild only.
|
|
163
167
|
- **`/grilling`**: Socratic assumption-inversion and Matt Pocock-style design tree frontier discovery.
|
|
164
168
|
- **`epistemic_search`**: Zero-key DuckDuckGo Lite search and content-addressed fetch with automatic SHA-256 caching.
|
|
165
169
|
|
package/bin/cli.js
CHANGED
|
@@ -5,9 +5,28 @@
|
|
|
5
5
|
* High-Integrity Dialectic Research Harness for Claude Code, Pi, OMP, OpenCode V2, Gemini CLI, Codex CLI, AntiGravity.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const { spawnSync } = require('child_process');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const fs = require('fs');
|
|
8
|
+
const { spawnSync } = require('node:child_process');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
|
|
12
|
+
function resolveBinary(name) {
|
|
13
|
+
const pathDirs = (process.env.PATH || '').split(path.delimiter);
|
|
14
|
+
for (const dir of pathDirs) {
|
|
15
|
+
if (!dir || dir === '.' || dir.startsWith('./') || dir.startsWith('../')) continue;
|
|
16
|
+
const full = path.join(dir, name);
|
|
17
|
+
try {
|
|
18
|
+
// Match Node's own PATH search: skip non-files and non-executables so a
|
|
19
|
+
// directory or a non-executable named `python3` earlier on PATH cannot
|
|
20
|
+
// shadow the real binary.
|
|
21
|
+
if (!fs.statSync(full).isFile()) continue;
|
|
22
|
+
fs.accessSync(full, fs.constants.X_OK);
|
|
23
|
+
return fs.realpathSync(full);
|
|
24
|
+
} catch {
|
|
25
|
+
/* not present, not a file, or not executable — keep searching */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return name;
|
|
29
|
+
}
|
|
11
30
|
|
|
12
31
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
13
32
|
const args = process.argv.slice(2);
|
|
@@ -30,6 +49,7 @@ Commands:
|
|
|
30
49
|
scout "<feature>" Scout open-source software, libraries & clean-room blueprints
|
|
31
50
|
run "<objective>" Run the dialectic multi-agent research swarm
|
|
32
51
|
brainstorm "<prompt>" Run lateral brainstorming (feature vectors + spikes)
|
|
52
|
+
darkharvest "<arena>" Product competitor teardown with harvest verdicts
|
|
33
53
|
grill Launch interactive Socratic decision tree framing
|
|
34
54
|
adapters Rebuild harness adapter mirrors (skills -> plugins/*, .agents)
|
|
35
55
|
install Install skills & MCP servers into ~/.claude/
|
|
@@ -61,6 +81,7 @@ Examples:
|
|
|
61
81
|
iumbtems scout "Zero-dependency Raft consensus in Rust"
|
|
62
82
|
iumbtems run "Verify sub-millisecond ZK prover latency"
|
|
63
83
|
iumbtems brainstorm "Where do we go from here?"
|
|
84
|
+
iumbtems darkharvest "Paseo-class agent harness competitor" --seeds https://github.com/a/b,https://github.com/c/d --max-repos 6 --mock-claude
|
|
64
85
|
iumbtems grill --objective "Rollup architecture trade-offs"
|
|
65
86
|
iumbtems doctor
|
|
66
87
|
`);
|
|
@@ -74,7 +95,7 @@ function runPython(scriptRelPath, extraArgs = []) {
|
|
|
74
95
|
const scriptPath = path.join(PKG_ROOT, scriptRelPath);
|
|
75
96
|
finalArgs = [scriptPath, ...extraArgs];
|
|
76
97
|
}
|
|
77
|
-
const result = spawnSync('python3', finalArgs, {
|
|
98
|
+
const result = spawnSync(resolveBinary('python3'), finalArgs, {
|
|
78
99
|
stdio: 'inherit',
|
|
79
100
|
cwd: process.cwd(),
|
|
80
101
|
env: { ...process.env, PYTHONPATH: PKG_ROOT }
|
|
@@ -84,7 +105,7 @@ function runPython(scriptRelPath, extraArgs = []) {
|
|
|
84
105
|
|
|
85
106
|
function runBash(scriptRelPath, extraArgs = []) {
|
|
86
107
|
const scriptPath = path.join(PKG_ROOT, scriptRelPath);
|
|
87
|
-
const result = spawnSync('bash', [scriptPath, ...extraArgs], {
|
|
108
|
+
const result = spawnSync(resolveBinary('bash'), [scriptPath, ...extraArgs], {
|
|
88
109
|
stdio: 'inherit',
|
|
89
110
|
cwd: process.cwd()
|
|
90
111
|
});
|
|
@@ -141,6 +162,18 @@ switch (command) {
|
|
|
141
162
|
break;
|
|
142
163
|
}
|
|
143
164
|
|
|
165
|
+
case 'darkharvest': {
|
|
166
|
+
const objective = args[1];
|
|
167
|
+
if (!objective || objective.startsWith('--')) {
|
|
168
|
+
console.error('Error: Please provide a product arena to tear down.');
|
|
169
|
+
console.error('Example: npx @heretek-ai/epistemic-swarm darkharvest "Paseo-class harness" --seeds https://github.com/a/b --max-repos 6');
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
const forwardArgs = ['--mode', 'darkharvest', '--objective', objective, ...args.slice(2)];
|
|
173
|
+
runPython('runner/research_swarm.py', forwardArgs);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
144
177
|
case 'grill': {
|
|
145
178
|
runPython('skills/grilling/socratic_tree.py', args.slice(1));
|
|
146
179
|
break;
|
|
@@ -186,18 +219,51 @@ switch (command) {
|
|
|
186
219
|
|
|
187
220
|
case 'doctor': {
|
|
188
221
|
console.log('🔍 Checking Epistemic Swarm Environment:\n');
|
|
189
|
-
|
|
222
|
+
|
|
223
|
+
let pkgVersion = 'unknown';
|
|
224
|
+
try {
|
|
225
|
+
pkgVersion = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')).version || 'unknown';
|
|
226
|
+
} catch { /* keep 'unknown' */ }
|
|
227
|
+
console.log(`✅ epistemic-swarm: ${pkgVersion}`);
|
|
228
|
+
|
|
229
|
+
const claudeCheck = spawnSync(resolveBinary('claude'), ['--version'], { encoding: 'utf-8' });
|
|
190
230
|
if (claudeCheck.status === 0) {
|
|
191
231
|
console.log(`✅ Claude Code: ${claudeCheck.stdout.trim()}`);
|
|
192
232
|
} else {
|
|
193
233
|
console.log('⚠️ Claude Code CLI not found in PATH (Install via npm i -g @anthropic-ai/claude-code)');
|
|
194
234
|
}
|
|
195
235
|
|
|
196
|
-
const pyCheck = spawnSync('python3', ['--version'], { encoding: 'utf-8' });
|
|
236
|
+
const pyCheck = spawnSync(resolveBinary('python3'), ['--version'], { encoding: 'utf-8' });
|
|
197
237
|
console.log(`✅ Python: ${pyCheck.stdout ? pyCheck.stdout.trim() : 'python3 missing'}`);
|
|
198
238
|
|
|
199
239
|
console.log(`✅ Node.js: ${process.version}`);
|
|
200
240
|
console.log(`📁 Package Root: ${PKG_ROOT}`);
|
|
241
|
+
|
|
242
|
+
// opencode resolves this package as `@latest` into a per-fetch cache and
|
|
243
|
+
// keeps every generation. A session started against an old generation keeps
|
|
244
|
+
// running the old code until it is refreshed, so report what is cached.
|
|
245
|
+
try {
|
|
246
|
+
const cacheRoot = path.join(process.env.HOME || '', '.cache', 'opencode', 'npm', '@heretek-ai');
|
|
247
|
+
const pkgDir = path.join(cacheRoot, 'epistemic-swarm@latest');
|
|
248
|
+
if (fs.existsSync(pkgDir)) {
|
|
249
|
+
const generations = fs.readdirSync(pkgDir).filter((d) => /^\d+$/.test(d)).sort();
|
|
250
|
+
const newest = generations[generations.length - 1];
|
|
251
|
+
console.log(`\n🧩 OpenCode plugin cache: ${generations.length} generation(s) in ${pkgDir}`);
|
|
252
|
+
for (const g of generations.slice(-5)) {
|
|
253
|
+
const marker = g === newest ? ' <- newest' : '';
|
|
254
|
+
console.log(` - ${g}${marker}`);
|
|
255
|
+
}
|
|
256
|
+
if (generations.length > 1) {
|
|
257
|
+
console.log(' ⚠️ Multiple generations cached. If slash commands vanish or the plugin');
|
|
258
|
+
console.log(' misbehaves, the running session may hold an older one — refresh with');
|
|
259
|
+
console.log(' `opencode plugin update` (or clear the cache dir) and relaunch.');
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
console.log('\n🧩 OpenCode plugin cache: not present (plugin not yet fetched)');
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
console.log(`\n🧩 OpenCode plugin cache: could not inspect (${err.message})`);
|
|
266
|
+
}
|
|
201
267
|
break;
|
|
202
268
|
}
|
|
203
269
|
|
package/config/mcp_launcher.py
CHANGED
|
@@ -8,12 +8,11 @@ Safely detects API keys and endpoints:
|
|
|
8
8
|
Prevents startup crashes and missing environment variable errors in Claude Code.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
import sys
|
|
12
|
-
import os
|
|
13
11
|
import json
|
|
14
|
-
import
|
|
12
|
+
import os
|
|
15
13
|
import shutil
|
|
16
|
-
|
|
14
|
+
import sys
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
17
16
|
|
|
18
17
|
# Add parent dir to path for imports
|
|
19
18
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
@@ -21,32 +20,30 @@ PROJECT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
|
|
|
21
20
|
if PROJECT_ROOT not in sys.path:
|
|
22
21
|
sys.path.insert(0, PROJECT_ROOT)
|
|
23
22
|
|
|
23
|
+
|
|
24
24
|
def get_config_val(key: str, default: str = "") -> str:
|
|
25
25
|
"""Retrieve config from env, userConfig, or defaults."""
|
|
26
26
|
env_val = os.environ.get(key)
|
|
27
27
|
if env_val:
|
|
28
28
|
return env_val
|
|
29
|
-
# Also check user_config format
|
|
30
29
|
user_cfg_val = os.environ.get(f"CLAUDE_PLUGIN_OPTION_{key.upper()}")
|
|
31
30
|
if user_cfg_val:
|
|
32
31
|
return user_cfg_val
|
|
33
32
|
return default
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
|
|
35
|
+
def _build_brave_tools() -> list:
|
|
36
|
+
from runner.mcp_protocol import ToolSpec
|
|
37
37
|
from skills.epistemic_search.scripts.search import search_duckduckgo
|
|
38
|
-
from skills.epistemic_search.scripts.fetch import fetch_and_cache
|
|
39
|
-
from runner.mcp_protocol import StdioJsonRpcServer, ToolSpec
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
)
|
|
47
|
-
return json.dumps(results, indent=2)
|
|
39
|
+
def handle_brave_search(args: Dict[str, Any]) -> str:
|
|
40
|
+
results = search_duckduckgo(
|
|
41
|
+
query=args.get("query", ""), max_results=args.get("count", 10)
|
|
42
|
+
)
|
|
43
|
+
return json.dumps(results, indent=2)
|
|
48
44
|
|
|
49
|
-
|
|
45
|
+
return [
|
|
46
|
+
ToolSpec(
|
|
50
47
|
name="brave_web_search",
|
|
51
48
|
description="Web search with DuckDuckGo fallback (zero API key required). Returns verifiable search results with titles, URLs, and snippets.",
|
|
52
49
|
input_schema={
|
|
@@ -59,12 +56,19 @@ def run_zero_key_mcp_server(service: str):
|
|
|
59
56
|
},
|
|
60
57
|
handler=handle_brave_search,
|
|
61
58
|
aliases=("search",),
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
)
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _build_firecrawl_tools() -> list:
|
|
64
|
+
from runner.mcp_protocol import ToolSpec
|
|
65
|
+
from skills.epistemic_search.scripts.fetch import fetch_and_cache
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
def handle_firecrawl_scrape(args: Dict[str, Any]) -> str:
|
|
68
|
+
return fetch_and_cache(url=args.get("url", ""))
|
|
69
|
+
|
|
70
|
+
return [
|
|
71
|
+
ToolSpec(
|
|
68
72
|
name="firecrawl_scrape",
|
|
69
73
|
description="Scrape and extract clean markdown with automatic content-addressed SHA-256 caching into .research/sources/.",
|
|
70
74
|
input_schema={
|
|
@@ -76,7 +80,20 @@ def run_zero_key_mcp_server(service: str):
|
|
|
76
80
|
},
|
|
77
81
|
handler=handle_firecrawl_scrape,
|
|
78
82
|
aliases=("scrape", "fetch"),
|
|
79
|
-
)
|
|
83
|
+
)
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run_zero_key_mcp_server(service: str):
|
|
88
|
+
"""Run lightweight JSON-RPC MCP server handling search and fetch without API keys."""
|
|
89
|
+
from runner.mcp_protocol import StdioJsonRpcServer
|
|
90
|
+
|
|
91
|
+
if service == "brave-search":
|
|
92
|
+
tools = _build_brave_tools()
|
|
93
|
+
elif service == "firecrawl":
|
|
94
|
+
tools = _build_firecrawl_tools()
|
|
95
|
+
else:
|
|
96
|
+
tools = []
|
|
80
97
|
|
|
81
98
|
server = StdioJsonRpcServer(
|
|
82
99
|
server_name=f"epistemic-{service}-fallback",
|
|
@@ -86,41 +103,51 @@ def run_zero_key_mcp_server(service: str):
|
|
|
86
103
|
)
|
|
87
104
|
server.serve_forever()
|
|
88
105
|
|
|
106
|
+
|
|
107
|
+
def _launch_brave(npx_bin: Optional[str]) -> None:
|
|
108
|
+
api_key = get_config_val("BRAVE_SEARCH_API_KEY")
|
|
109
|
+
if api_key and npx_bin:
|
|
110
|
+
os.environ["BRAVE_SEARCH_API_KEY"] = api_key
|
|
111
|
+
os.execvp(npx_bin, [npx_bin, "-y", "@modelcontextprotocol/server-brave-search"])
|
|
112
|
+
else:
|
|
113
|
+
run_zero_key_mcp_server("brave-search")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _launch_firecrawl(npx_bin: Optional[str]) -> None:
|
|
117
|
+
api_key = get_config_val("FIRECRAWL_API_KEY")
|
|
118
|
+
api_url = get_config_val("FIRECRAWL_API_URL", "https://api.firecrawl.dev")
|
|
119
|
+
if api_key and npx_bin:
|
|
120
|
+
os.environ["FIRECRAWL_API_KEY"] = api_key
|
|
121
|
+
os.environ["FIRECRAWL_API_URL"] = api_url
|
|
122
|
+
os.execvp(npx_bin, [npx_bin, "-y", "firecrawl-mcp"])
|
|
123
|
+
else:
|
|
124
|
+
run_zero_key_mcp_server("firecrawl")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _launch_searxng() -> None:
|
|
128
|
+
searxng_url = get_config_val("SEARXNG_URL", "http://localhost:8080")
|
|
129
|
+
searxng_script = os.path.join(SCRIPT_DIR, "searxng_mcp.py")
|
|
130
|
+
if os.path.exists(searxng_script):
|
|
131
|
+
os.environ["SEARXNG_URL"] = searxng_url
|
|
132
|
+
os.execvp("python3", ["python3", searxng_script])
|
|
133
|
+
else:
|
|
134
|
+
run_zero_key_mcp_server("searxng")
|
|
135
|
+
|
|
136
|
+
|
|
89
137
|
def main():
|
|
90
138
|
service = sys.argv[1] if len(sys.argv) > 1 else "brave-search"
|
|
91
139
|
npx_bin = shutil.which("npx")
|
|
92
140
|
|
|
93
141
|
if service == "brave-search":
|
|
94
|
-
|
|
95
|
-
if api_key and npx_bin:
|
|
96
|
-
os.environ["BRAVE_SEARCH_API_KEY"] = api_key
|
|
97
|
-
os.execvp(npx_bin, [npx_bin, "-y", "@modelcontextprotocol/server-brave-search"])
|
|
98
|
-
else:
|
|
99
|
-
# Fall back to zero-key search proxy
|
|
100
|
-
run_zero_key_mcp_server("brave-search")
|
|
101
|
-
|
|
142
|
+
_launch_brave(npx_bin)
|
|
102
143
|
elif service == "firecrawl":
|
|
103
|
-
|
|
104
|
-
api_url = get_config_val("FIRECRAWL_API_URL", "https://api.firecrawl.dev")
|
|
105
|
-
if api_key and npx_bin:
|
|
106
|
-
os.environ["FIRECRAWL_API_KEY"] = api_key
|
|
107
|
-
os.environ["FIRECRAWL_API_URL"] = api_url
|
|
108
|
-
os.execvp(npx_bin, [npx_bin, "-y", "firecrawl-mcp"])
|
|
109
|
-
else:
|
|
110
|
-
# Fall back to zero-key readability fetcher + SHA-256 hasher
|
|
111
|
-
run_zero_key_mcp_server("firecrawl")
|
|
112
|
-
|
|
144
|
+
_launch_firecrawl(npx_bin)
|
|
113
145
|
elif service == "searxng":
|
|
114
|
-
|
|
115
|
-
searxng_script = os.path.join(SCRIPT_DIR, "searxng_mcp.py")
|
|
116
|
-
if os.path.exists(searxng_script):
|
|
117
|
-
os.environ["SEARXNG_URL"] = searxng_url
|
|
118
|
-
os.execvp("python3", ["python3", searxng_script])
|
|
119
|
-
else:
|
|
120
|
-
run_zero_key_mcp_server("searxng")
|
|
146
|
+
_launch_searxng()
|
|
121
147
|
else:
|
|
122
148
|
sys.stderr.write(f"Unknown service: {service}\n")
|
|
123
149
|
sys.exit(1)
|
|
124
150
|
|
|
151
|
+
|
|
125
152
|
if __name__ == "__main__":
|
|
126
153
|
main()
|
|
@@ -87,6 +87,18 @@
|
|
|
87
87
|
"iumbtems_brainstorm": true,
|
|
88
88
|
"iumbtems_config": true
|
|
89
89
|
}
|
|
90
|
+
},
|
|
91
|
+
"darkharvester": {
|
|
92
|
+
"description": "IUMBTEMS Competitor Teardown: seed plus expand, product plus code matrix, per-feature harvest verdicts with SPDX attribution.",
|
|
93
|
+
"tools": {
|
|
94
|
+
"read": true,
|
|
95
|
+
"write": true,
|
|
96
|
+
"grep": true,
|
|
97
|
+
"glob": true,
|
|
98
|
+
"iumbtems_darkharvest": true,
|
|
99
|
+
"iumbtems_verify_quote": true,
|
|
100
|
+
"iumbtems_config": true
|
|
101
|
+
}
|
|
90
102
|
}
|
|
91
103
|
},
|
|
92
104
|
"skills": {
|
|
@@ -97,7 +109,8 @@
|
|
|
97
109
|
"./skills/swarm_config",
|
|
98
110
|
"./skills/code_audit",
|
|
99
111
|
"./skills/oss_scout",
|
|
100
|
-
"./skills/brainstorming"
|
|
112
|
+
"./skills/brainstorming",
|
|
113
|
+
"./skills/darkharvest"
|
|
101
114
|
]
|
|
102
115
|
}
|
|
103
116
|
}
|
package/extensions/pi/index.js
CHANGED
|
@@ -12,19 +12,39 @@
|
|
|
12
12
|
* not a duplicate tool implementation.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { spawnSync } from 'child_process';
|
|
16
|
-
import
|
|
17
|
-
import
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
18
19
|
|
|
19
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
21
|
const __dirname = path.dirname(__filename);
|
|
21
22
|
const PKG_ROOT = path.resolve(__dirname, '../..');
|
|
22
23
|
const MCP_SERVER = path.join(PKG_ROOT, 'runner', 'mcp_server.py');
|
|
23
24
|
|
|
25
|
+
function resolveBinary(name) {
|
|
26
|
+
const pathDirs = (process.env.PATH || '').split(path.delimiter);
|
|
27
|
+
for (const dir of pathDirs) {
|
|
28
|
+
if (!dir || dir === '.' || dir.startsWith('./') || dir.startsWith('../')) continue;
|
|
29
|
+
const full = path.join(dir, name);
|
|
30
|
+
try {
|
|
31
|
+
// Match Node's own PATH search: skip non-files and non-executables so a
|
|
32
|
+
// directory or a non-executable named `python3` earlier on PATH cannot
|
|
33
|
+
// shadow the real binary.
|
|
34
|
+
if (!fs.statSync(full).isFile()) continue;
|
|
35
|
+
fs.accessSync(full, fs.constants.X_OK);
|
|
36
|
+
return fs.realpathSync(full);
|
|
37
|
+
} catch {
|
|
38
|
+
/* not present, not a file, or not executable — keep searching */
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return name;
|
|
42
|
+
}
|
|
43
|
+
|
|
24
44
|
/** Uniform dispatch: one-shot MCP call. Returns {ok, text}. */
|
|
25
45
|
function callMcp(tool, args = {}, ctx = {}) {
|
|
26
46
|
const res = spawnSync(
|
|
27
|
-
'python3',
|
|
47
|
+
resolveBinary('python3'),
|
|
28
48
|
[MCP_SERVER, 'call', tool, JSON.stringify(args || {})],
|
|
29
49
|
{
|
|
30
50
|
encoding: 'utf-8',
|
|
@@ -154,7 +174,7 @@ export default function initPiExtension(pi) {
|
|
|
154
174
|
ctx.output?.(`💡 [IUMBTEMS Brainstorm] Diverging on: "${objective}"...`);
|
|
155
175
|
// Skill-local context scaffold (not a tool duplicate) -> then canonical tool.
|
|
156
176
|
const scaffold = path.join(PKG_ROOT, 'skills/brainstorming/scripts/brainstorm.py');
|
|
157
|
-
const pre = spawnSync('python3', [scaffold, '--objective', objective, '--show-context'], {
|
|
177
|
+
const pre = spawnSync(resolveBinary('python3'), [scaffold, '--objective', objective, '--show-context'], {
|
|
158
178
|
encoding: 'utf-8',
|
|
159
179
|
cwd: ctx.cwd || process.cwd()
|
|
160
180
|
});
|
|
@@ -182,6 +202,27 @@ export default function initPiExtension(pi) {
|
|
|
182
202
|
}
|
|
183
203
|
});
|
|
184
204
|
} catch { /* alias is best-effort across pi/omp versions */ }
|
|
205
|
+
|
|
206
|
+
// /darkharvest: Product competitor teardown with harvest verdicts
|
|
207
|
+
pi.registerCommand('darkharvest', {
|
|
208
|
+
description: 'Product competitor teardown: seed inspirations, expand to adjacents, emit harvest verdicts',
|
|
209
|
+
usage: '/darkharvest <product-arena> [--seeds <urls>]',
|
|
210
|
+
handler: async (args, ctx) => {
|
|
211
|
+
const objective = args.trim();
|
|
212
|
+
if (!objective) {
|
|
213
|
+
ctx.output?.('⚠️ Please provide a product arena. Example: /darkharvest "Paseo-class agent harness" --seeds https://github.com/a/b');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
ctx.output?.(`🌑 [IUMBTEMS Darkharvest] Tearing down: "${objective}"...`);
|
|
217
|
+
const r = callMcp('iumbtems_darkharvest', { objective }, ctx);
|
|
218
|
+
if (r.ok) {
|
|
219
|
+
ctx.output?.(r.text);
|
|
220
|
+
ctx.output?.('\n✅ Darkharvest complete! Matrix written to .research/darkharvest_report.md');
|
|
221
|
+
} else {
|
|
222
|
+
ctx.output?.(`❌ Darkharvest failed:\n${r.text}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
185
226
|
}
|
|
186
227
|
|
|
187
228
|
// 2. Register Agent Tools (thin forwarders)
|
|
@@ -213,7 +254,7 @@ export default function initPiExtension(pi) {
|
|
|
213
254
|
properties: {
|
|
214
255
|
search_engine: { type: 'string', enum: ['duckduckgo', 'brave', 'firecrawl', 'searxng'] },
|
|
215
256
|
max_iterations: { type: 'integer', minimum: 1, maximum: 4 },
|
|
216
|
-
mode: { type: 'string', enum: ['research', 'audit', 'scout', 'hybrid', 'brainstorm'] },
|
|
257
|
+
mode: { type: 'string', enum: ['research', 'audit', 'scout', 'hybrid', 'brainstorm', 'darkharvest'] },
|
|
217
258
|
divergence_threshold: { type: 'number', minimum: 0, maximum: 1 },
|
|
218
259
|
show: { type: 'boolean', default: false }
|
|
219
260
|
}
|
|
@@ -244,6 +285,31 @@ export default function initPiExtension(pi) {
|
|
|
244
285
|
return { content: [{ type: 'text', text: r.text }] };
|
|
245
286
|
}
|
|
246
287
|
});
|
|
288
|
+
|
|
289
|
+
// Tool: iumbtems_darkharvest (competitor teardown)
|
|
290
|
+
pi.registerTool({
|
|
291
|
+
name: 'iumbtems_darkharvest',
|
|
292
|
+
description: 'Product competitor teardown with per-feature harvest verdicts and SPDX attribution',
|
|
293
|
+
parameters: {
|
|
294
|
+
type: 'object',
|
|
295
|
+
properties: {
|
|
296
|
+
objective: { type: 'string', description: 'Product arena to tear down' },
|
|
297
|
+
seeds: { type: 'array', items: { type: 'string' }, description: 'Seed inspiration repo URLs' },
|
|
298
|
+
maxRepos: { type: 'integer', description: 'Cap on total competitors', default: 10 },
|
|
299
|
+
mock_mode: { type: 'boolean', description: 'Mock mode without LLM tokens', default: false }
|
|
300
|
+
},
|
|
301
|
+
required: ['objective']
|
|
302
|
+
},
|
|
303
|
+
execute: async (args = {}) => {
|
|
304
|
+
const r = callMcp('iumbtems_darkharvest', {
|
|
305
|
+
objective: args.objective,
|
|
306
|
+
seeds: args.seeds,
|
|
307
|
+
maxRepos: args.maxRepos,
|
|
308
|
+
mock_claude: Boolean(args.mock_mode)
|
|
309
|
+
}, {});
|
|
310
|
+
return { content: [{ type: 'text', text: r.text }] };
|
|
311
|
+
}
|
|
312
|
+
});
|
|
247
313
|
}
|
|
248
314
|
}
|
|
249
315
|
|
package/install.sh
CHANGED
|
@@ -23,16 +23,16 @@ mkdir -p "$CLAUDE_DIR/skills"
|
|
|
23
23
|
echo "🔗 Linking skills into $CLAUDE_DIR/skills/..."
|
|
24
24
|
for skill in grilling research_cache epistemic_search swarm_config code_audit oss_scout brainstorming; do
|
|
25
25
|
# legacy research-cache dir name kept as alias for older configs
|
|
26
|
-
ln -
|
|
26
|
+
ln -sfn "$REPO_DIR/skills/$skill" "$CLAUDE_DIR/skills/$skill"
|
|
27
27
|
echo " - $CLAUDE_DIR/skills/$skill -> $REPO_DIR/skills/$skill"
|
|
28
28
|
done
|
|
29
|
-
if [ ! -e "$CLAUDE_DIR/skills/research-cache" ]; then
|
|
30
|
-
ln -
|
|
29
|
+
if [[ ! -e "$CLAUDE_DIR/skills/research-cache" ]]; then
|
|
30
|
+
ln -sfn "$REPO_DIR/skills/research_cache" "$CLAUDE_DIR/skills/research-cache"
|
|
31
31
|
echo " - $CLAUDE_DIR/skills/research-cache -> $REPO_DIR/skills/research_cache (legacy alias)"
|
|
32
32
|
fi
|
|
33
33
|
|
|
34
34
|
# 3. Patch ~/.claude.json mcpServers non-destructively
|
|
35
|
-
if [ -f "$CLAUDE_JSON" ]; then
|
|
35
|
+
if [[ -f "$CLAUDE_JSON" ]]; then
|
|
36
36
|
echo "🔧 Merging research MCP servers into $CLAUDE_JSON..."
|
|
37
37
|
python3 - <<EOF
|
|
38
38
|
import json
|
|
@@ -77,7 +77,7 @@ fi
|
|
|
77
77
|
|
|
78
78
|
# 4. Patch ~/.claude/settings.json
|
|
79
79
|
SETTINGS_JSON="$CLAUDE_DIR/settings.json"
|
|
80
|
-
if [ -f "$SETTINGS_JSON" ]; then
|
|
80
|
+
if [[ -f "$SETTINGS_JSON" ]]; then
|
|
81
81
|
echo "🔧 Merging plugin settings into $SETTINGS_JSON..."
|
|
82
82
|
python3 - <<EOF
|
|
83
83
|
import json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heretek-ai/epistemic-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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": {
|
|
@@ -79,7 +79,8 @@
|
|
|
79
79
|
"./skills/swarm_config",
|
|
80
80
|
"./skills/code_audit",
|
|
81
81
|
"./skills/oss_scout",
|
|
82
|
-
"./skills/brainstorming"
|
|
82
|
+
"./skills/brainstorming",
|
|
83
|
+
"./skills/darkharvest"
|
|
83
84
|
],
|
|
84
85
|
"prompts": [
|
|
85
86
|
"./prompts/*.md"
|
|
@@ -96,7 +97,8 @@
|
|
|
96
97
|
"./skills/swarm_config",
|
|
97
98
|
"./skills/code_audit",
|
|
98
99
|
"./skills/oss_scout",
|
|
99
|
-
"./skills/brainstorming"
|
|
100
|
+
"./skills/brainstorming",
|
|
101
|
+
"./skills/darkharvest"
|
|
100
102
|
],
|
|
101
103
|
"prompts": [
|
|
102
104
|
"./prompts/*.md"
|
|
@@ -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.8",
|
|
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",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Darkharvest (thin adapter stub)
|
|
2
|
+
|
|
3
|
+
This file is a POINTER, not the implementation. It exists so harness skill
|
|
4
|
+
discovery finds an entry; the real skill lives in the IUMBTEMS repo.
|
|
5
|
+
|
|
6
|
+
- Canonical prose & scripts: `skills/darkharvest/`
|
|
7
|
+
- Canonical programmatic surface: `python3 runner/mcp_server.py` (stdio MCP),
|
|
8
|
+
or one-shot: `python3 runner/mcp_server.py call <tool> '{...json...}'`
|
|
9
|
+
- MCP tools for this skill: `iumbtems_darkharvest`
|
|
10
|
+
|
|
11
|
+
Epistemic rules apply regardless of harness: tag claims as
|
|
12
|
+
`[VERIFIED: <hash>]`, `[INFERRED: <reasoning>]`, `[HYPOTHESIS: <test>]`, or
|
|
13
|
+
`[NEGATIVE_KNOWLEDGE: <query>]`. Writes go only to `.research/`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Darkharvest (thin adapter stub)
|
|
2
|
+
|
|
3
|
+
This file is a POINTER, not the implementation. It exists so harness skill
|
|
4
|
+
discovery finds an entry; the real skill lives in the IUMBTEMS repo.
|
|
5
|
+
|
|
6
|
+
- Canonical prose & scripts: `skills/darkharvest/`
|
|
7
|
+
- Canonical programmatic surface: `python3 runner/mcp_server.py` (stdio MCP),
|
|
8
|
+
or one-shot: `python3 runner/mcp_server.py call <tool> '{...json...}'`
|
|
9
|
+
- MCP tools for this skill: `iumbtems_darkharvest`
|
|
10
|
+
|
|
11
|
+
Epistemic rules apply regardless of harness: tag claims as
|
|
12
|
+
`[VERIFIED: <hash>]`, `[INFERRED: <reasoning>]`, `[HYPOTHESIS: <test>]`, or
|
|
13
|
+
`[NEGATIVE_KNOWLEDGE: <query>]`. Writes go only to `.research/`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
description = "Product competitor teardown with per-feature harvest verdicts"
|
|
2
|
+
prompt = """Run a product competitor teardown for: {{args}}.
|
|
3
|
+
|
|
4
|
+
Execute: python3 runner/research_swarm.py --mode darkharvest --objective "{{args}}"
|
|
5
|
+
Seeds may be embedded as repo URLs in the objective; cap at --max-repos 6 live.
|
|
6
|
+
Legal rule: permissive-only vendor; GPL/AGPL clean-room-rebuild only; workflows clonable, assets never.
|
|
7
|
+
Report: .research/darkharvest_report.md (competitor x capability matrix + harvest backlog + SPDX blocks).
|
|
8
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Darkharvest (thin adapter stub)
|
|
2
|
+
|
|
3
|
+
This file is a POINTER, not the implementation. It exists so harness skill
|
|
4
|
+
discovery finds an entry; the real skill lives in the IUMBTEMS repo.
|
|
5
|
+
|
|
6
|
+
- Canonical prose & scripts: `skills/darkharvest/`
|
|
7
|
+
- Canonical programmatic surface: `python3 runner/mcp_server.py` (stdio MCP),
|
|
8
|
+
or one-shot: `python3 runner/mcp_server.py call <tool> '{...json...}'`
|
|
9
|
+
- MCP tools for this skill: `iumbtems_darkharvest`
|
|
10
|
+
|
|
11
|
+
Epistemic rules apply regardless of harness: tag claims as
|
|
12
|
+
`[VERIFIED: <hash>]`, `[INFERRED: <reasoning>]`, `[HYPOTHESIS: <test>]`, or
|
|
13
|
+
`[NEGATIVE_KNOWLEDGE: <query>]`. Writes go only to `.research/`.
|