@heretek-ai/epistemic-swarm 0.7.15 → 0.7.17

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 (80) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +20 -0
  3. package/install.sh +233 -14
  4. package/package.json +4 -1
  5. package/plugins/opencode/index.js +598 -1
  6. package/plugins/socratic-grilling/skills/grilling/socratic_tree.py +159 -24
  7. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  8. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  9. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  10. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  11. package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
  12. package/runner/__pycache__/darkharvest_claims.cpython-311.pyc +0 -0
  13. package/runner/__pycache__/errors.cpython-311.pyc +0 -0
  14. package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
  15. package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
  16. package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
  17. package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
  18. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  19. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  20. package/runner/__pycache__/preflight.cpython-311.pyc +0 -0
  21. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  22. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  23. package/runner/__pycache__/retrieval_log.cpython-311.pyc +0 -0
  24. package/runner/__pycache__/schema_validate.cpython-311.pyc +0 -0
  25. package/runner/__pycache__/schemas.cpython-311.pyc +0 -0
  26. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  27. package/runner/mcp_server.py +79 -28
  28. package/runner/preflight.py +829 -12
  29. package/runner/research_swarm.py +68 -5
  30. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  31. package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
  32. package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
  33. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  34. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  35. package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
  36. package/runner/tests/__pycache__/test_contract_tooling.cpython-311.pyc +0 -0
  37. package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
  38. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  39. package/runner/tests/__pycache__/test_dossier_contract.cpython-311.pyc +0 -0
  40. package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
  41. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  42. package/runner/tests/__pycache__/test_frontier_concurrency.cpython-311.pyc +0 -0
  43. package/runner/tests/__pycache__/test_frontier_mirror_standalone.cpython-311.pyc +0 -0
  44. package/runner/tests/__pycache__/test_grounding.cpython-311.pyc +0 -0
  45. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  46. package/runner/tests/__pycache__/test_manifest_concurrency.cpython-311.pyc +0 -0
  47. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  48. package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
  49. package/runner/tests/__pycache__/test_opencode_v2_registrars.cpython-311.pyc +0 -0
  50. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  51. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  52. package/runner/tests/__pycache__/test_search_anomaly.cpython-311.pyc +0 -0
  53. package/runner/tests/__pycache__/test_search_provider.cpython-311.pyc +0 -0
  54. package/runner/tests/__pycache__/test_search_provider_plugin.cpython-311.pyc +0 -0
  55. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  56. package/runner/tests/__pycache__/test_swarm_config.cpython-311.pyc +0 -0
  57. package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
  58. package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
  59. package/runner/tests/fixtures/ddg_anomaly_page.html +176 -0
  60. package/runner/tests/fixtures/ddg_results_page.html +28 -0
  61. package/runner/tests/test_frontier_concurrency.py +288 -0
  62. package/runner/tests/test_frontier_mirror_standalone.py +156 -0
  63. package/runner/tests/test_search_anomaly.py +277 -0
  64. package/runner/tests/test_search_provider.py +1464 -0
  65. package/runner/tests/test_search_provider_plugin.py +401 -0
  66. package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
  67. package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
  68. package/scripts/__pycache__/check_docs.cpython-311.pyc +0 -0
  69. package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
  70. package/scripts/__pycache__/gen_schemas.cpython-311.pyc +0 -0
  71. package/skills/epistemic_search/SKILL.md +32 -0
  72. package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
  73. package/skills/epistemic_search/scripts/search.py +232 -19
  74. package/skills/grilling/__pycache__/__init__.cpython-311.pyc +0 -0
  75. package/skills/grilling/__pycache__/socratic_tree.cpython-311.pyc +0 -0
  76. package/skills/grilling/socratic_tree.py +159 -24
  77. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  78. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  79. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  80. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epistemic-swarm",
3
- "version": "0.7.15",
3
+ "version": "0.7.17",
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",
package/README.md CHANGED
@@ -316,6 +316,26 @@ Every factual claim carries an explicit evidentiary tag:
316
316
  3. **Academic Tier**: Semantic Scholar / arXiv MCPs for DOI citation resolution.
317
317
  4. **Caching Tier**: Content-addressed SHA-256 storage (`skills/research_cache/hasher.py`).
318
318
 
319
+ ### Free-first provider ladder & cost safety
320
+
321
+ OpenCode V2 hosts a plugin-registered **search provider** (`iumbtems-cached`) that resolves its upstream free-first and never overrides a deliberate `websearch.provider` or a provider you `/connect`ed. ("Cached" describes the evidence pipeline — `hasher.py` content-addresses full pages fetched via `webfetch` — not in-execute memoisation.)
322
+
323
+ 1. **`/connect`-ed provider** (integration store) — **metered**, credential lives in the host store.
324
+ 2. **BYO key** (`exa`/`firecrawl`/`parallel`/`tavily`/`tinyfish` via `EXA_API_KEY`/`FIRECRAWL_API_KEY`/`PARALLEL_API_KEY`/`TAVILY_API_KEY`/`TINYFISH_API_KEY`) — **metered** (vendor-billed).
325
+ 3. **Self-hosted SearXNG** (`SEARXNG_URL`) — free; availability requires the URL.
326
+ 4. **DuckDuckGo Lite** — free, zero-key, routed through `skills/epistemic_search/scripts/search.py` (anti-bot detection + telemetry).
327
+ 5. **Console** (hosted, `$0.01`/successful search) — **metered, never implicit**.
328
+
329
+ Provider `execute()` returns **hits only** (`title`/`url`/`content`). Full page content must be fetched explicitly with the host `webfetch` tool and cached via `python3 skills/research_cache/hasher.py cache …`; **a snippet alone never witnesses a `[VERIFIED: <hash>]` claim**.
330
+
331
+ Preflight prints the active provider, a free|metered classification, a **per-run** our-path search count + cost **estimate**, and `metered-mode: yes|no|unknown`. The reachability probe is excluded, so a zero-search run reports `our-path searches: 0`, and every metered rung logs its own event. Engine aliases are normalised before the probe (`ddg`/`DuckDuckGo`/`DDG`/whitespace all mean `duckduckgo`), so no spelling can skip the probe. It also gates availability mode-aware: `research`/`scout`/`darkharvest`/`hybrid` (and strict `brainstorm`, including `--domain-pack`) **HALT before any agent spawns** when no usable search exists (exit code 3 — deliberately distinct from argparse's usage-error 2); `audit`/internal `brainstorm` warn and proceed, and `--dry-run` validates and reports without halting. The workspace connection file (`.research/websearch-state.json`) is advisory: it is honoured only within a 24h TTL and never marks a provider usable without a resolvable credential. It is resolved in a **single read** (provider + advisory verdict together), only when the path is a **regular file**, and read with a bounded non-blocking guard — so a FIFO/device/symlink or a file swapped between reads can neither spoof host confirmation nor wedge preflight/`doctor`. Enable the opt-in per-search cost gate with `./install.sh --search-gate`, which adds:
332
+
333
+ ```jsonc
334
+ { "permissions": [ { "action": "websearch", "resource": "*", "effect": "ask" } ] }
335
+ ```
336
+
337
+ Without the flag, `install.sh` never touches your permissions. If a `websearch` `deny` already exists the gate refuses to downgrade it (exit non-zero, config untouched), and resource-scoped websearch rules are preserved and reported — never silently deleted.
338
+
319
339
  ### Local Infrastructure (Optional)
320
340
  ```bash
321
341
  docker compose -f config/docker-compose.infra.yml up -d # local SearXNG + Firecrawl
package/install.sh CHANGED
@@ -5,9 +5,40 @@ REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
5
  CLAUDE_DIR="$HOME/.claude"
6
6
  CLAUDE_JSON="$HOME/.claude.json"
7
7
 
8
+ # Opt-in cost gate. With --search-gate we add ONE rule to the user's OpenCode
9
+ # config: {action:"websearch", resource:"*", effect:"ask"}. The websearch
10
+ # permission action uses the search query as the resource
11
+ # [VERIFIED: 8bde98f3962fef9407abc4622660d9f52f0490de803151fcc0ce0f648f7c0bbe],
12
+ # so every billable search prompts. Without the flag install.sh never touches
13
+ # the permissions array. Manual equivalent (opencode.json):
14
+ # { "$schema": "https://opencode.ai/config.json",
15
+ # "permissions": [ { "action": "websearch", "resource": "*", "effect": "ask" } ] }
16
+ SEARCH_GATE=0
17
+
18
+ usage() {
19
+ cat <<'USAGE'
20
+ Usage: install.sh [--search-gate]
21
+
22
+ --search-gate Add {"action":"websearch","resource":"*","effect":"ask"} to the
23
+ OpenCode config permissions so every search prompts before it
24
+ can incur cost. Opt-in only; without it permissions are untouched.
25
+ -h, --help Show this help.
26
+ USAGE
27
+ }
28
+
29
+ while [[ $# -gt 0 ]]; do
30
+ case "$1" in
31
+ --search-gate) SEARCH_GATE=1 ;;
32
+ -h|--help) usage; exit 0 ;;
33
+ *) echo "❌ Unknown option: $1" >&2; usage >&2; exit 2 ;;
34
+ esac
35
+ shift
36
+ done
37
+
8
38
  echo "========================================================"
9
39
  echo "🚀 Installing Epistemic Swarm into Claude Code"
10
40
  echo " Source: $REPO_DIR"
41
+ [[ "$SEARCH_GATE" == "1" ]] && echo " Search cost gate: ON (websearch -> ask)"
11
42
  echo "========================================================"
12
43
 
13
44
  # 1. Dependency checks
@@ -34,17 +65,22 @@ fi
34
65
  # 3. Patch ~/.claude.json mcpServers non-destructively
35
66
  if [[ -f "$CLAUDE_JSON" ]]; then
36
67
  echo "🔧 Merging research MCP servers into $CLAUDE_JSON..."
37
- python3 - <<EOF
68
+ # Heredoc is QUOTED (<<'PY'): an unquoted heredoc runs command substitution
69
+ # on backticks in comments (e.g. "permissions") and breaks the install.
70
+ # Variables are passed through the environment instead.
71
+ CLAUDE_JSON="$CLAUDE_JSON" REPO_DIR="$REPO_DIR" python3 - <<'PY'
38
72
  import json
73
+ import os
39
74
  from pathlib import Path
40
75
 
41
- claude_json_path = Path("$CLAUDE_JSON")
42
- mcp_config_path = Path("$REPO_DIR/config/mcp-research-servers.json")
76
+ claude_json_path = Path(os.environ["CLAUDE_JSON"])
77
+ repo_dir = os.environ["REPO_DIR"]
78
+ mcp_config_path = Path(repo_dir) / "config" / "mcp-research-servers.json"
43
79
 
44
80
  try:
45
81
  with open(claude_json_path, 'r', encoding='utf-8') as f:
46
82
  claude_data = json.load(f)
47
-
83
+
48
84
  with open(mcp_config_path, 'r', encoding='utf-8') as f:
49
85
  mcp_data = json.load(f)
50
86
 
@@ -56,9 +92,9 @@ try:
56
92
  # "runner/mcp_server.py" only works when cwd is the repo root.
57
93
  if server_name == "iumbtems":
58
94
  server_def = dict(server_def)
59
- server_def["args"] = [str(Path("$REPO_DIR") / "runner" / "mcp_server.py")]
95
+ server_def["args"] = [str(Path(repo_dir) / "runner" / "mcp_server.py")]
60
96
  env = dict(server_def.get("env") or {})
61
- env["PYTHONPATH"] = "$REPO_DIR"
97
+ env["PYTHONPATH"] = repo_dir
62
98
  server_def["env"] = env
63
99
 
64
100
  if server_name not in claude_data["mcpServers"]:
@@ -72,18 +108,20 @@ try:
72
108
  print("✅ ~/.claude.json successfully updated.")
73
109
  except Exception as e:
74
110
  print(f"⚠️ Non-critical warning merging MCP servers: {e}")
75
- EOF
111
+ PY
76
112
  fi
77
113
 
78
114
  # 4. Patch ~/.claude/settings.json
79
115
  SETTINGS_JSON="$CLAUDE_DIR/settings.json"
80
116
  if [[ -f "$SETTINGS_JSON" ]]; then
81
117
  echo "🔧 Merging plugin settings into $SETTINGS_JSON..."
82
- python3 - <<EOF
118
+ # Quoted heredoc: keep shell expansion out of the Python (see note above).
119
+ SETTINGS_JSON="$SETTINGS_JSON" python3 - <<'PY'
83
120
  import json
121
+ import os
84
122
  from pathlib import Path
85
123
 
86
- settings_path = Path("$SETTINGS_JSON")
124
+ settings_path = Path(os.environ["SETTINGS_JSON"])
87
125
  try:
88
126
  with open(settings_path, 'r', encoding='utf-8') as f:
89
127
  settings = json.load(f)
@@ -106,18 +144,24 @@ try:
106
144
  print("✅ ~/.claude/settings.json successfully updated.")
107
145
  except Exception as e:
108
146
  print(f"⚠️ Non-critical warning updating settings: {e}")
109
- EOF
147
+ PY
110
148
  fi
111
149
 
112
150
  # 5. OpenCode V2 integration (plugins/agents/skills/mcp.servers — merge only)
113
151
  OPENCODE_JSON="$HOME/.config/opencode/opencode.json"
114
152
  echo "🔧 Merging OpenCode V2 configuration into $OPENCODE_JSON..."
115
- python3 - <<EOF
153
+ # Quoted heredoc (<<'PY'): the comment text below contains backticks, which an
154
+ # unquoted heredoc would execute as command substitution ("permissions: command
155
+ # not found", "plugin: command not found", …). Variables go via the environment.
156
+ REPO_DIR="$REPO_DIR" OPENCODE_JSON="$OPENCODE_JSON" SEARCH_GATE="$SEARCH_GATE" python3 - <<'PY'
157
+ import fnmatch
116
158
  import json
159
+ import os
117
160
  from pathlib import Path
118
161
 
119
- repo = Path("$REPO_DIR")
120
- oc_path = Path("$OPENCODE_JSON")
162
+ repo = Path(os.environ["REPO_DIR"])
163
+ oc_path = Path(os.environ["OPENCODE_JSON"])
164
+ search_gate = os.environ.get("SEARCH_GATE", "0")
121
165
  PKG = "@heretek-ai/epistemic-swarm"
122
166
  OPTION_KEYS = {"search_engine", "max_iterations", "mode", "divergence_threshold"}
123
167
 
@@ -240,15 +284,190 @@ if "iumbtems" not in servers:
240
284
  else:
241
285
  print(" ℹ️ MCP server already configured: iumbtems")
242
286
 
287
+ if search_gate == "1":
288
+ # Opt-in cost gate. V2 config key is `permissions` (a list of rules); the
289
+ # websearch action's resource is the search query, so "*" gates them all.
290
+ rules = cfg.get("permissions")
291
+ if rules is not None and not isinstance(rules, list):
292
+ # A non-list shape is not a V2 permissions list. Refuse LOUDLY rather
293
+ # than replacing it with a fresh list and silently dropping unrelated
294
+ # entries.
295
+ raise SystemExit(
296
+ "❌ opencode.json `permissions` is a "
297
+ f"{type(rules).__name__}, not a list; refusing to overwrite it. "
298
+ "Convert it to a list of {action, resource, effect} rules, then "
299
+ "re-run install.sh --search-gate."
300
+ )
301
+ if not isinstance(rules, list):
302
+ rules = cfg["permissions"] = []
303
+
304
+ def _is_websearch(rule):
305
+ return isinstance(rule, dict) and rule.get("action") == "websearch"
306
+
307
+ def _action_matches_websearch(rule):
308
+ # V2 actions support whole-value wildcards and the host resolves with
309
+ # last-match-wins, so `*` / `web*` also govern websearch queries.
310
+ action = rule.get("action") if isinstance(rule, dict) else None
311
+ return isinstance(action, str) and fnmatch.fnmatchcase(
312
+ "websearch", action
313
+ )
314
+
315
+ KNOWN_EFFECTS = {"allow", "ask", "deny"}
316
+
317
+ def _effect_known(rule):
318
+ effect = rule.get("effect")
319
+ return isinstance(effect, str) and effect in KNOWN_EFFECTS
320
+
321
+ # --- Safety: NEVER weaken an explicit deny. An existing websearch deny —
322
+ # literal action OR wildcard (`*`, `web*`), ANY resource — is stricter than
323
+ # the `ask` gate, and under last-match-wins our appended rule would win and
324
+ # silently downgrade it. The same holds for a matching rule whose effect is
325
+ # missing or not a known effect string: its safety cannot be determined.
326
+ # Preserve the config untouched and refuse loudly instead of rewriting a
327
+ # security decision the user made.
328
+ websearch_denies = [
329
+ r
330
+ for r in rules
331
+ if _action_matches_websearch(r)
332
+ and (r.get("effect") == "deny" or not _effect_known(r))
333
+ ]
334
+ if websearch_denies:
335
+ raise SystemExit(
336
+ "❌ opencode.json already denies websearch "
337
+ f"({json.dumps(websearch_denies)}); the --search-gate `ask` rule "
338
+ "would be WEAKER than it under last-match-wins, so install.sh "
339
+ "refuses to downgrade it and leaves your config untouched.\n"
340
+ " The existing deny is already stricter than ask. If you really "
341
+ "want the ask gate instead, remove the websearch deny rule(s) "
342
+ "manually, then re-run `install.sh --search-gate`."
343
+ )
344
+
345
+ canonical = {"action": "websearch", "resource": "*", "effect": "ask"}
346
+ global_ws = [
347
+ r for r in rules if _is_websearch(r) and r.get("resource") == "*"
348
+ ]
349
+ # Resource-scoped websearch rules (resource != "*") are the user's explicit
350
+ # per-query decisions. NEVER silently drop them: keep every one and report
351
+ # it. Effective precedence is resolved below under last-match-wins — these
352
+ # rules are preserved verbatim, never rewritten.
353
+ scoped_ws = [
354
+ r
355
+ for r in rules
356
+ if _action_matches_websearch(r) and r.get("resource") != "*"
357
+ ]
358
+ for rule in scoped_ws:
359
+ if rule.get("effect") == "allow":
360
+ print(
361
+ " ⚠️ Preserved resource-scoped websearch allow "
362
+ f"({json.dumps(rule)}): kept verbatim; effective precedence "
363
+ "is resolved below under last-match-wins."
364
+ )
365
+ else:
366
+ print(
367
+ " ℹ️ Preserved existing websearch rule "
368
+ f"({json.dumps(rule)})"
369
+ )
370
+
371
+ if global_ws:
372
+ existing_ask = [r for r in global_ws if r.get("effect") == "ask"]
373
+ if existing_ask:
374
+ # An ask gate is already present. Keep it VERBATIM — rewriting it to
375
+ # the canonical shape would silently drop unknown keys (e.g. a
376
+ # user-added "note"), so preserve and report instead.
377
+ kept = existing_ask[0]
378
+ dropped = [r for r in global_ws if r is not kept]
379
+ for r in dropped:
380
+ print(
381
+ " ~ Dropped duplicate websearch '*' rule "
382
+ f"({json.dumps(r)})"
383
+ )
384
+ drop_ids = {id(r) for r in dropped}
385
+ rules[:] = [r for r in rules if id(r) not in drop_ids]
386
+ # Ordering matters under last-match-wins: a later matching `allow`
387
+ # (scoped or wildcard-action) would override the preserved ask for
388
+ # overlapping queries. Move the gate last so it takes precedence,
389
+ # and say so loudly — this overrides an explicit per-query allow.
390
+ kept_index = next(i for i, r in enumerate(rules) if r is kept)
391
+ later_allow = [
392
+ r
393
+ for r in rules[kept_index + 1 :]
394
+ if _action_matches_websearch(r) and r.get("effect") == "allow"
395
+ ]
396
+ if later_allow:
397
+ rules.remove(kept)
398
+ rules.append(kept)
399
+ print(
400
+ " ⚠️ Moved websearch ask gate after later matching allow "
401
+ f"({json.dumps(later_allow)}): the gate now takes "
402
+ "precedence for overlapping queries under last-match-wins."
403
+ )
404
+ if kept == canonical:
405
+ print(" ℹ️ websearch ask-gate permission already configured")
406
+ else:
407
+ print(
408
+ " ℹ️ Preserved existing websearch ask rule with extra keys "
409
+ f"({json.dumps(kept)}); not rewritten."
410
+ )
411
+ else:
412
+ # Only allow effects can reach here (deny/malformed abort above).
413
+ for r in global_ws:
414
+ print(" ~ replaced websearch allow (*) with ask")
415
+ drop_ids = {id(r) for r in global_ws}
416
+ rules[:] = [r for r in rules if id(r) not in drop_ids]
417
+ rules.append(canonical)
418
+ else:
419
+ # No literal global websearch rule. A broader ask already covering
420
+ # websearch means the gate is effectively in place — don't duplicate
421
+ # it, unless a later resource-scoped allow would override it.
422
+ global_matches = [
423
+ (i, r)
424
+ for i, r in enumerate(rules)
425
+ if _action_matches_websearch(r) and r.get("resource") == "*"
426
+ ]
427
+ last_global_index, last_global = (
428
+ global_matches[-1] if global_matches else (None, None)
429
+ )
430
+ later_scoped_allow = (
431
+ last_global is not None
432
+ and any(
433
+ i > last_global_index
434
+ and _action_matches_websearch(r)
435
+ and r.get("resource") != "*"
436
+ and r.get("effect") == "allow"
437
+ for i, r in enumerate(rules)
438
+ )
439
+ )
440
+ if (
441
+ last_global is not None
442
+ and last_global.get("effect") == "ask"
443
+ and not later_scoped_allow
444
+ ):
445
+ print(
446
+ " ℹ️ websearch is already gated by a broader ask rule; "
447
+ "nothing to add"
448
+ )
449
+ else:
450
+ if later_scoped_allow:
451
+ print(
452
+ " ⚠️ Resource-scoped websearch allow follows the broader "
453
+ "ask gate; appending canonical ask so the gate takes "
454
+ "precedence for overlapping queries under last-match-wins."
455
+ )
456
+ rules.append(canonical)
457
+ print(" + Added websearch ask-gate permission (--search-gate)")
458
+ else:
459
+ print(" ℹ️ permissions untouched (pass --search-gate to add the websearch ask rule)")
460
+
243
461
  oc_path.write_text(json.dumps(cfg, indent=2) + "\n", encoding="utf-8")
244
462
  print("✅ OpenCode configuration updated.")
245
- EOF
463
+ PY
246
464
 
247
465
  echo ""
248
466
  echo "🎉 Epistemic Swarm installation complete!"
249
467
  echo " - Interactive Grilling: run /grilling inside Claude Code"
250
468
  echo " - Lateral Brainstorming: run /brainstorming inside Claude Code (skills/brainstorming)"
251
469
  echo " - OpenCode V2: /swarm /grill /audit /scout /brainstorming via plugin commands"
470
+ echo " - Cost gate (opt-in): re-run with --search-gate to add a websearch ask rule"
252
471
  echo " - Rebuild harness adapters: python3 scripts/build_adapters.py"
253
472
  echo " - Headless Research Swarm: npx @heretek-ai/epistemic-swarm run \"<objective>\""
254
473
  echo "========================================================"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heretek-ai/epistemic-swarm",
3
- "version": "0.7.15",
3
+ "version": "0.7.17",
4
4
  "description": "IUMBTEMS (Epistemic Swarm) — High-Integrity Dialectic Research Agent Harness for Claude Code, OpenCode V2, Pi (pi.dev), OMP (oh-my-pi), Gemini CLI, Codex CLI, and AntiGravity",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
@@ -22,6 +22,9 @@
22
22
  "solid-js": "1.9.12",
23
23
  "@opentui/solid": ">=0.4.0 <0.6.0"
24
24
  },
25
+ "overrides": {
26
+ "@babel/core": "^7.29.6"
27
+ },
25
28
  "repository": {
26
29
  "type": "git",
27
30
  "url": "git+https://github.com/Heretek-AI/IUMBTEMS.git"