@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.
Files changed (85) hide show
  1. package/.agents/skills/darkharvest/SKILL.md +13 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.omp/commands/darkharvest.md +11 -0
  4. package/README.md +4 -0
  5. package/bin/cli.js +73 -7
  6. package/config/mcp_launcher.py +75 -48
  7. package/config/opencode-snippet.json +14 -1
  8. package/extensions/pi/index.js +72 -6
  9. package/install.sh +5 -5
  10. package/package.json +5 -3
  11. package/plugins/antigravity/plugin.json +1 -1
  12. package/plugins/antigravity/skills/darkharvest/SKILL.md +13 -0
  13. package/plugins/codex/skills/darkharvest/SKILL.md +13 -0
  14. package/plugins/gemini/commands/darkharvest.toml +8 -0
  15. package/plugins/gemini/gemini-extension.json +1 -1
  16. package/plugins/gemini/skills/darkharvest/SKILL.md +13 -0
  17. package/plugins/opencode/index.js +357 -144
  18. package/plugins/opencode/tui.js +7 -9
  19. package/plugins/research-cache/skills/research_cache/hasher.py +27 -9
  20. package/plugins/socratic-grilling/skills/grilling/socratic_tree.py +26 -18
  21. package/prompts/agent_darkharvest.md +91 -0
  22. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  23. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  24. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  25. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  26. package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
  27. package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
  28. package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
  29. package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
  30. package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
  31. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  32. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  33. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  34. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  35. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  36. package/runner/auditor_engine.py +122 -102
  37. package/runner/claim_store.py +5 -4
  38. package/runner/living_dossiers.py +32 -10
  39. package/runner/mcp_protocol.py +1 -1
  40. package/runner/mcp_server.py +225 -73
  41. package/runner/path_safety.py +62 -0
  42. package/runner/pcrb.py +68 -39
  43. package/runner/pcrb_verify.py +82 -52
  44. package/runner/refinement.py +61 -47
  45. package/runner/research_swarm.py +198 -22
  46. package/runner/state_machine.py +9 -6
  47. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  48. package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
  49. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  50. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  51. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  52. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  53. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  54. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  55. package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
  56. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  57. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  58. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  59. package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
  60. package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
  61. package/runner/tests/test_mcp_server.py +80 -38
  62. package/runner/tests/test_opencode_ux.py +389 -35
  63. package/runner/tests/test_swarm.py +32 -9
  64. package/runner/tests/test_sweep_regressions.py +420 -0
  65. package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
  66. package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
  67. package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
  68. package/scripts/auction_experiment.py +38 -30
  69. package/scripts/bet1_advisory_spike.py +41 -25
  70. package/scripts/build_adapters.py +57 -35
  71. package/scripts/divergence_experiment.py +61 -31
  72. package/skills/brainstorming/scripts/brainstorm.py +3 -2
  73. package/skills/darkharvest/SKILL.md +70 -0
  74. package/skills/darkharvest/scripts/harvest.py +252 -0
  75. package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
  76. package/skills/epistemic_search/scripts/fetch.py +0 -1
  77. package/skills/epistemic_search/scripts/search.py +202 -73
  78. package/skills/epistemic_search/scripts/webcache.py +1 -1
  79. package/skills/grilling/socratic_tree.py +26 -18
  80. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  81. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  82. package/skills/research_cache/hasher.py +27 -9
  83. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  84. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
  85. 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/`.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epistemic-swarm",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "High-Integrity Dialectic Research Agent Harness for Claude Code enforcing empirical evidence over parametric hallucination.",
5
5
  "author": {
6
6
  "name": "Heretek AI",
@@ -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
- const claudeCheck = spawnSync('claude', ['--version'], { encoding: 'utf-8' });
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
 
@@ -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 subprocess
12
+ import os
15
13
  import shutil
16
- from typing import Dict, Any, List
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
- def run_zero_key_mcp_server(service: str):
36
- """Run lightweight JSON-RPC MCP server handling search and fetch without API keys."""
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
- tools = []
42
- if service == "brave-search":
43
- def handle_brave_search(args: Dict[str, Any]) -> str:
44
- results = search_duckduckgo(
45
- query=args.get("query", ""), max_results=args.get("count", 10)
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
- tools.append(ToolSpec(
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
- elif service == "firecrawl":
64
- def handle_firecrawl_scrape(args: Dict[str, Any]) -> str:
65
- return fetch_and_cache(url=args.get("url", ""))
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
- tools.append(ToolSpec(
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
- api_key = get_config_val("BRAVE_SEARCH_API_KEY")
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
- api_key = get_config_val("FIRECRAWL_API_KEY")
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
- searxng_url = get_config_val("SEARXNG_URL", "http://localhost:8080")
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
  }
@@ -12,19 +12,39 @@
12
12
  * not a duplicate tool implementation.
13
13
  */
14
14
 
15
- import { spawnSync } from 'child_process';
16
- import path from 'path';
17
- import { fileURLToPath } from 'url';
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 -sf "$REPO_DIR/skills/$skill" "$CLAUDE_DIR/skills/$skill"
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 -sf "$REPO_DIR/skills/research_cache" "$CLAUDE_DIR/skills/research-cache"
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.4.7",
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.7",
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
+ """
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epistemic-swarm",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "IUMBTEMS Epistemic Swarm: dialectic research, code audit, OSS scout, and lateral brainstorming for Gemini CLI.",
5
5
  "mcpServers": {
6
6
  "brave-search": {
@@ -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/`.