@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +20 -0
- package/install.sh +233 -14
- package/package.json +4 -1
- package/plugins/opencode/index.js +598 -1
- package/plugins/socratic-grilling/skills/grilling/socratic_tree.py +159 -24
- 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__/darkharvest_claims.cpython-311.pyc +0 -0
- package/runner/__pycache__/errors.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__/preflight.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__/retrieval_log.cpython-311.pyc +0 -0
- package/runner/__pycache__/schema_validate.cpython-311.pyc +0 -0
- package/runner/__pycache__/schemas.cpython-311.pyc +0 -0
- package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
- package/runner/mcp_server.py +79 -28
- package/runner/preflight.py +829 -12
- package/runner/research_swarm.py +68 -5
- package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_backends.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_claude_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_contract_tooling.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dossier_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_frontier_mirror_standalone.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_grounding.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_manifest_concurrency.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_opencode_v2_registrars.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_search_anomaly.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_search_provider_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm_config.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/fixtures/ddg_anomaly_page.html +176 -0
- package/runner/tests/fixtures/ddg_results_page.html +28 -0
- package/runner/tests/test_frontier_concurrency.py +288 -0
- package/runner/tests/test_frontier_mirror_standalone.py +156 -0
- package/runner/tests/test_search_anomaly.py +277 -0
- package/runner/tests/test_search_provider.py +1464 -0
- package/runner/tests/test_search_provider_plugin.py +401 -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__/check_docs.cpython-311.pyc +0 -0
- package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
- package/scripts/__pycache__/gen_schemas.cpython-311.pyc +0 -0
- package/skills/epistemic_search/SKILL.md +32 -0
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/epistemic_search/scripts/search.py +232 -19
- package/skills/grilling/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/grilling/__pycache__/socratic_tree.cpython-311.pyc +0 -0
- package/skills/grilling/socratic_tree.py +159 -24
- package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
package/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
|
-
|
|
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("
|
|
42
|
-
|
|
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(
|
|
95
|
+
server_def["args"] = [str(Path(repo_dir) / "runner" / "mcp_server.py")]
|
|
60
96
|
env = dict(server_def.get("env") or {})
|
|
61
|
-
env["PYTHONPATH"] =
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
120
|
-
oc_path = Path("
|
|
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
|
-
|
|
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.
|
|
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"
|