@heretek-ai/epistemic-swarm 0.4.8 → 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 (59) hide show
  1. package/.agents/skills/darkharvest/SKILL.md +13 -0
  2. package/.omp/commands/darkharvest.md +11 -0
  3. package/README.md +4 -0
  4. package/bin/cli.js +14 -0
  5. package/config/opencode-snippet.json +14 -1
  6. package/extensions/pi/index.js +47 -1
  7. package/package.json +5 -3
  8. package/plugins/antigravity/skills/darkharvest/SKILL.md +13 -0
  9. package/plugins/codex/skills/darkharvest/SKILL.md +13 -0
  10. package/plugins/gemini/commands/darkharvest.toml +8 -0
  11. package/plugins/gemini/skills/darkharvest/SKILL.md +13 -0
  12. package/plugins/opencode/index.js +49 -2
  13. package/prompts/agent_darkharvest.md +91 -0
  14. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  15. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  16. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  17. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  18. package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
  19. package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
  20. package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
  21. package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
  22. package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
  23. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  24. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  25. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  26. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  27. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  28. package/runner/mcp_server.py +202 -62
  29. package/runner/research_swarm.py +138 -16
  30. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  31. package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
  32. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  33. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  34. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  35. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  36. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  37. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  38. package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
  39. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  40. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  41. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  42. package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
  43. package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
  44. package/runner/tests/test_mcp_server.py +80 -37
  45. package/runner/tests/test_opencode_ux.py +27 -8
  46. package/runner/tests/test_swarm.py +12 -0
  47. package/runner/tests/test_sweep_regressions.py +47 -0
  48. package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
  49. package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
  50. package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
  51. package/scripts/build_adapters.py +9 -2
  52. package/skills/darkharvest/SKILL.md +70 -0
  53. package/skills/darkharvest/scripts/harvest.py +252 -0
  54. package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
  55. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  56. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  57. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  58. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
  59. package/skills/swarm_config/configure.py +12 -2
@@ -44,6 +44,7 @@ SERVER_VERSION = "0.3.0"
44
44
  # helpers
45
45
  # ---------------------------------------------------------------------------
46
46
 
47
+
47
48
  def _tool_text(payload: Any) -> str:
48
49
  """Normalize a handler return into the MCP text content block body."""
49
50
  if isinstance(payload, str):
@@ -70,6 +71,7 @@ def _resolve_base_dir(args: Dict[str, Any]) -> Path:
70
71
  raw = args.get("base_dir") or args.get("dir") or DEFAULT_RESEARCH_DIR
71
72
  return Path(os.path.realpath(str(raw)))
72
73
 
74
+
73
75
  def _handle_config(args: Dict[str, Any]) -> str:
74
76
  """Inspect or modify swarm parameters in .research/config.json."""
75
77
  from skills.swarm_config.configure import (
@@ -82,7 +84,14 @@ def _handle_config(args: Dict[str, Any]) -> str:
82
84
  cfg = load_config(base_dir)
83
85
 
84
86
  updates = {}
85
- for key in ("search_engine", "engine", "max_iterations", "depth", "mode", "divergence_threshold"):
87
+ for key in (
88
+ "search_engine",
89
+ "engine",
90
+ "max_iterations",
91
+ "depth",
92
+ "mode",
93
+ "divergence_threshold",
94
+ ):
86
95
  if key in args and args[key] is not None:
87
96
  canon = {
88
97
  "engine": "search_engine",
@@ -94,7 +103,13 @@ def _handle_config(args: Dict[str, Any]) -> str:
94
103
  if updates:
95
104
  cfg.update(updates)
96
105
  save_config(cfg, base_dir)
97
- return _tool_text({"status": "updated", "config": cfg, "written_to": str(Path(base_dir) / "config.json")})
106
+ return _tool_text(
107
+ {
108
+ "status": "updated",
109
+ "config": cfg,
110
+ "written_to": str(Path(base_dir) / "config.json"),
111
+ }
112
+ )
98
113
 
99
114
  return _tool_text({"status": "current", "config": cfg})
100
115
 
@@ -121,16 +136,21 @@ def _run_swarm_mode(mode: Optional[str], args: Dict[str, Any]) -> str:
121
136
  runner.run_swarm(objective, frontier_file=Path(frontier) if frontier else None)
122
137
 
123
138
  report = runner.base_dir / (
124
- {"audit": "code_audit_report.md", "scout": "oss_scout_report.md", "brainstorm": "brainstorm_report.md"}.get(
125
- mode or args.get("mode") or "research", "final_synthesis.md"
126
- )
139
+ {
140
+ "audit": "code_audit_report.md",
141
+ "scout": "oss_scout_report.md",
142
+ "brainstorm": "brainstorm_report.md",
143
+ "darkharvest": "darkharvest_report.md",
144
+ }.get(mode or args.get("mode") or "research", "final_synthesis.md")
145
+ )
146
+ return _tool_text(
147
+ {
148
+ "status": "completed",
149
+ "mode": mode or args.get("mode") or "research",
150
+ "report": str(report),
151
+ "log": buf.getvalue()[-4000:],
152
+ }
127
153
  )
128
- return _tool_text({
129
- "status": "completed",
130
- "mode": mode or args.get("mode") or "research",
131
- "report": str(report),
132
- "log": buf.getvalue()[-4000:],
133
- })
134
154
 
135
155
 
136
156
  def _handle_swarm_research(args: Dict[str, Any]) -> str:
@@ -153,23 +173,32 @@ def _handle_brainstorm(args: Dict[str, Any]) -> str:
153
173
  return _run_swarm_mode("brainstorm", args)
154
174
 
155
175
 
176
+ def _handle_darkharvest(args: Dict[str, Any]) -> str:
177
+ """Run product competitor teardown (seed + expand, harvest verdicts)."""
178
+ return _run_swarm_mode("darkharvest", args)
179
+
180
+
156
181
  def _handle_verify_quote(args: Dict[str, Any]) -> str:
157
182
  """Verify a verbatim quote against the content-addressed source cache."""
158
183
  from skills.research_cache.hasher import SourceHasher
159
184
 
160
- content_hash = args.get("hash") or args.get("content_hash") or args.get("source_hash")
185
+ content_hash = (
186
+ args.get("hash") or args.get("content_hash") or args.get("source_hash")
187
+ )
161
188
  quote = args.get("quote")
162
189
  if not content_hash or quote is None:
163
190
  raise ValueError("hash and quote are required")
164
191
 
165
192
  hasher = SourceHasher(_resolve_base_dir(args))
166
193
  is_verified, confidence, message = hasher.verify_quote(content_hash, quote)
167
- return _tool_text({
168
- "verified": bool(is_verified),
169
- "confidence": float(confidence),
170
- "message": message,
171
- "content_hash": content_hash,
172
- })
194
+ return _tool_text(
195
+ {
196
+ "verified": bool(is_verified),
197
+ "confidence": float(confidence),
198
+ "message": message,
199
+ "content_hash": content_hash,
200
+ }
201
+ )
173
202
 
174
203
 
175
204
  def _handle_report_retraction(args: Dict[str, Any]) -> str:
@@ -213,13 +242,15 @@ def _handle_set_domain_pack(args: Dict[str, Any]) -> str:
213
242
  cfg = load_config(base_dir)
214
243
  cfg["domain_pack"] = pack
215
244
  save_config(cfg, base_dir)
216
- return _tool_text({
217
- "status": "activated",
218
- "domain_pack": pack,
219
- "accept_threshold": constitution.accept_threshold,
220
- "retraction_policy": constitution.retraction_policy,
221
- "banned_domains": constitution.banned_domains,
222
- })
245
+ return _tool_text(
246
+ {
247
+ "status": "activated",
248
+ "domain_pack": pack,
249
+ "accept_threshold": constitution.accept_threshold,
250
+ "retraction_policy": constitution.retraction_policy,
251
+ "banned_domains": constitution.banned_domains,
252
+ }
253
+ )
223
254
 
224
255
 
225
256
  def _handle_export_brief(args: Dict[str, Any]) -> str:
@@ -284,7 +315,9 @@ def _handle_socratic_frontier(args: Dict[str, Any]) -> str:
284
315
  """Advance or inspect the Socratic decision-tree frontier."""
285
316
  from skills.grilling.socratic_tree import DesignTree
286
317
 
287
- file_path = Path(args.get("file") or args.get("frontier_file") or ".research/frontier.json")
318
+ file_path = Path(
319
+ args.get("file") or args.get("frontier_file") or ".research/frontier.json"
320
+ )
288
321
 
289
322
  if args.get("settle"):
290
323
  node_id, answer = args["settle"][0], args["settle"][1]
@@ -294,32 +327,41 @@ def _handle_socratic_frontier(args: Dict[str, Any]) -> str:
294
327
  tree = DesignTree(objective=args.get("objective", ""))
295
328
  tree.settle_node(node_id, answer)
296
329
  tree.export_frontier_json(file_path)
297
- return _tool_text({"status": "settled", "node_id": node_id, "file": str(file_path)})
330
+ return _tool_text(
331
+ {"status": "settled", "node_id": node_id, "file": str(file_path)}
332
+ )
298
333
 
299
334
  if not file_path.exists():
300
- return _tool_text({
301
- "status": "no_frontier",
302
- "message": f"No frontier file at {file_path}. Run /grilling first.",
303
- "file": str(file_path),
304
- })
335
+ return _tool_text(
336
+ {
337
+ "status": "no_frontier",
338
+ "message": f"No frontier file at {file_path}. Run /grilling first.",
339
+ "file": str(file_path),
340
+ }
341
+ )
305
342
 
306
343
  tree = DesignTree.load_from_json(file_path)
307
344
  frontier = tree.compute_frontier()
308
- return _tool_text({
309
- "status": "frontier",
310
- "objective": tree.objective,
311
- "is_complete": tree.is_complete(),
312
- "total_nodes": len(tree.nodes),
313
- "settled": sum(1 for n in tree.nodes.values() if n.is_settled()),
314
- "frontier": [{"node_id": n.node_id, "question": n.question} for n in frontier],
315
- "file": str(file_path),
316
- })
345
+ return _tool_text(
346
+ {
347
+ "status": "frontier",
348
+ "objective": tree.objective,
349
+ "is_complete": tree.is_complete(),
350
+ "total_nodes": len(tree.nodes),
351
+ "settled": sum(1 for n in tree.nodes.values() if n.is_settled()),
352
+ "frontier": [
353
+ {"node_id": n.node_id, "question": n.question} for n in frontier
354
+ ],
355
+ "file": str(file_path),
356
+ }
357
+ )
317
358
 
318
359
 
319
360
  # ---------------------------------------------------------------------------
320
361
  # registry
321
362
  # ---------------------------------------------------------------------------
322
363
 
364
+
323
365
  def build_tools() -> List[ToolSpec]:
324
366
  """The 7 canonical iumbtems_* tools, in-process wrappers only."""
325
367
  return [
@@ -329,10 +371,29 @@ def build_tools() -> List[ToolSpec]:
329
371
  input_schema={
330
372
  "type": "object",
331
373
  "properties": {
332
- "base_dir": {"type": "string", "description": "Path to .research workspace (default .research)"},
333
- "search_engine": {"type": "string", "enum": ["duckduckgo", "brave", "firecrawl", "searxng"]},
334
- "max_iterations": {"type": "integer", "description": "Dialectic depth 1-4"},
335
- "mode": {"type": "string", "enum": ["research", "audit", "scout", "hybrid", "brainstorm"]},
374
+ "base_dir": {
375
+ "type": "string",
376
+ "description": "Path to .research workspace (default .research)",
377
+ },
378
+ "search_engine": {
379
+ "type": "string",
380
+ "enum": ["duckduckgo", "brave", "firecrawl", "searxng"],
381
+ },
382
+ "max_iterations": {
383
+ "type": "integer",
384
+ "description": "Dialectic depth 1-4",
385
+ },
386
+ "mode": {
387
+ "type": "string",
388
+ "enum": [
389
+ "research",
390
+ "audit",
391
+ "scout",
392
+ "hybrid",
393
+ "brainstorm",
394
+ "darkharvest",
395
+ ],
396
+ },
336
397
  "divergence_threshold": {"type": "number"},
337
398
  },
338
399
  },
@@ -344,12 +405,24 @@ def build_tools() -> List[ToolSpec]:
344
405
  input_schema={
345
406
  "type": "object",
346
407
  "properties": {
347
- "objective": {"type": "string", "description": "Research question or objective"},
408
+ "objective": {
409
+ "type": "string",
410
+ "description": "Research question or objective",
411
+ },
348
412
  "base_dir": {"type": "string"},
349
- "frontier_file": {"type": "string", "description": "Settled frontier.json from grilling"},
350
- "engine": {"type": "string", "enum": ["duckduckgo", "brave", "firecrawl", "searxng"]},
413
+ "frontier_file": {
414
+ "type": "string",
415
+ "description": "Settled frontier.json from grilling",
416
+ },
417
+ "engine": {
418
+ "type": "string",
419
+ "enum": ["duckduckgo", "brave", "firecrawl", "searxng"],
420
+ },
351
421
  "depth": {"type": "integer"},
352
- "mock_claude": {"type": "boolean", "description": "Synthetic run, zero API cost"},
422
+ "mock_claude": {
423
+ "type": "boolean",
424
+ "description": "Synthetic run, zero API cost",
425
+ },
353
426
  },
354
427
  "required": ["objective"],
355
428
  },
@@ -361,7 +434,10 @@ def build_tools() -> List[ToolSpec]:
361
434
  input_schema={
362
435
  "type": "object",
363
436
  "properties": {
364
- "target": {"type": "string", "description": "Codebase target or audit focus"},
437
+ "target": {
438
+ "type": "string",
439
+ "description": "Codebase target or audit focus",
440
+ },
365
441
  "base_dir": {"type": "string"},
366
442
  "mock_claude": {"type": "boolean"},
367
443
  },
@@ -375,7 +451,10 @@ def build_tools() -> List[ToolSpec]:
375
451
  input_schema={
376
452
  "type": "object",
377
453
  "properties": {
378
- "feature": {"type": "string", "description": "Feature or library to scout"},
454
+ "feature": {
455
+ "type": "string",
456
+ "description": "Feature or library to scout",
457
+ },
379
458
  "base_dir": {"type": "string"},
380
459
  "mock_claude": {"type": "boolean"},
381
460
  },
@@ -389,7 +468,10 @@ def build_tools() -> List[ToolSpec]:
389
468
  input_schema={
390
469
  "type": "object",
391
470
  "properties": {
392
- "objective": {"type": "string", "description": "Ambiguous prompt to ideate on"},
471
+ "objective": {
472
+ "type": "string",
473
+ "description": "Ambiguous prompt to ideate on",
474
+ },
393
475
  "base_dir": {"type": "string"},
394
476
  "mock_claude": {"type": "boolean"},
395
477
  },
@@ -397,14 +479,47 @@ def build_tools() -> List[ToolSpec]:
397
479
  },
398
480
  handler=_handle_brainstorm,
399
481
  ),
482
+ ToolSpec(
483
+ name="iumbtems_darkharvest",
484
+ description="Product competitor teardown: seed inspirations plus prompt, expand to adjacents, emit per-feature depend/vendor/clean-room/skip verdicts with SPDX attribution. Permissive-only vendoring; GPL/AGPL spec-rebuild only.",
485
+ input_schema={
486
+ "type": "object",
487
+ "properties": {
488
+ "objective": {
489
+ "type": "string",
490
+ "description": "Product arena to tear down (e.g. Paseo-class agent harness competitor)",
491
+ },
492
+ "seeds": {
493
+ "type": "array",
494
+ "items": {"type": "string"},
495
+ "description": "Seed inspiration repo URLs",
496
+ },
497
+ "maxRepos": {
498
+ "type": "integer",
499
+ "description": "Cap on total competitors (default 10)",
500
+ },
501
+ "depth": {"type": "integer", "description": "Dialectic depth 1-4"},
502
+ "base_dir": {"type": "string"},
503
+ "mock_claude": {"type": "boolean"},
504
+ },
505
+ "required": ["objective"],
506
+ },
507
+ handler=_handle_darkharvest,
508
+ ),
400
509
  ToolSpec(
401
510
  name="iumbtems_verify_quote",
402
511
  description="Audit a verbatim citation against the SHA-256 source cache (.research/sources/<hash>.md).",
403
512
  input_schema={
404
513
  "type": "object",
405
514
  "properties": {
406
- "hash": {"type": "string", "description": "Content-addressed SHA-256 of the cached source"},
407
- "quote": {"type": "string", "description": "Verbatim quote to verify"},
515
+ "hash": {
516
+ "type": "string",
517
+ "description": "Content-addressed SHA-256 of the cached source",
518
+ },
519
+ "quote": {
520
+ "type": "string",
521
+ "description": "Verbatim quote to verify",
522
+ },
408
523
  "base_dir": {"type": "string"},
409
524
  },
410
525
  "required": ["hash", "quote"],
@@ -417,7 +532,10 @@ def build_tools() -> List[ToolSpec]:
417
532
  input_schema={
418
533
  "type": "object",
419
534
  "properties": {
420
- "base_dir": {"type": "string", "description": "Path to .research workspace"},
535
+ "base_dir": {
536
+ "type": "string",
537
+ "description": "Path to .research workspace",
538
+ },
421
539
  },
422
540
  },
423
541
  handler=_handle_reindex_claims,
@@ -428,9 +546,15 @@ def build_tools() -> List[ToolSpec]:
428
546
  input_schema={
429
547
  "type": "object",
430
548
  "properties": {
431
- "hash": {"type": "string", "description": "Content-addressed SHA-256 of the affected source"},
549
+ "hash": {
550
+ "type": "string",
551
+ "description": "Content-addressed SHA-256 of the affected source",
552
+ },
432
553
  "event": {"type": "string", "enum": ["RETRACTED", "REVISED"]},
433
- "note": {"type": "string", "description": "Why the source was retracted/revised"},
554
+ "note": {
555
+ "type": "string",
556
+ "description": "Why the source was retracted/revised",
557
+ },
434
558
  "base_dir": {"type": "string"},
435
559
  },
436
560
  "required": ["hash", "event"],
@@ -459,7 +583,10 @@ def build_tools() -> List[ToolSpec]:
459
583
  input_schema={
460
584
  "type": "object",
461
585
  "properties": {
462
- "pack": {"type": "string", "description": "Pack id: biopharma | quant | legal (or a path)"},
586
+ "pack": {
587
+ "type": "string",
588
+ "description": "Pack id: biopharma | quant | legal (or a path)",
589
+ },
463
590
  "base_dir": {"type": "string"},
464
591
  },
465
592
  "required": ["pack"],
@@ -473,9 +600,15 @@ def build_tools() -> List[ToolSpec]:
473
600
  "type": "object",
474
601
  "properties": {
475
602
  "base_dir": {"type": "string"},
476
- "out": {"type": "string", "description": "Output path (default <base_dir>/brief.pcrb.json)"},
603
+ "out": {
604
+ "type": "string",
605
+ "description": "Output path (default <base_dir>/brief.pcrb.json)",
606
+ },
477
607
  "objective": {"type": "string"},
478
- "key": {"type": "string", "description": "HMAC key (prefer key_file or env IUMBTEMS_PCRB_KEY)"},
608
+ "key": {
609
+ "type": "string",
610
+ "description": "HMAC key (prefer key_file or env IUMBTEMS_PCRB_KEY)",
611
+ },
479
612
  "key_file": {"type": "string"},
480
613
  },
481
614
  },
@@ -487,7 +620,10 @@ def build_tools() -> List[ToolSpec]:
487
620
  input_schema={
488
621
  "type": "object",
489
622
  "properties": {
490
- "brief": {"type": "string", "description": "Path to brief.pcrb.json"},
623
+ "brief": {
624
+ "type": "string",
625
+ "description": "Path to brief.pcrb.json",
626
+ },
491
627
  "key": {"type": "string"},
492
628
  "key_file": {"type": "string"},
493
629
  },
@@ -529,13 +665,17 @@ def build_server() -> StdioJsonRpcServer:
529
665
  # entry points
530
666
  # ---------------------------------------------------------------------------
531
667
 
668
+
532
669
  def call_once(tool_name: str, arguments: Dict[str, Any]) -> int:
533
670
  """One-shot dispatch: run a single tool and print its text payload."""
534
671
  server = build_server()
535
672
  spec = server._by_name.get(tool_name)
536
673
  if spec is None:
537
674
  json.dump(
538
- {"error": f"Tool {tool_name} not found", "available": [t.name for t in server.tools]},
675
+ {
676
+ "error": f"Tool {tool_name} not found",
677
+ "available": [t.name for t in server.tools],
678
+ },
539
679
  sys.stdout,
540
680
  indent=2,
541
681
  )