@heretek-ai/epistemic-swarm 0.4.2 → 0.4.4

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 (44) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/hooks/hooks.json +22 -1
  3. package/install.sh +25 -0
  4. package/package.json +1 -1
  5. package/plugins/antigravity/plugin.json +1 -1
  6. package/plugins/gemini/gemini-extension.json +1 -1
  7. package/plugins/opencode/index.js +44 -15
  8. package/plugins/opencode/tui.js +40 -18
  9. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  10. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  11. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  12. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  13. package/runner/__pycache__/claim_witness.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__/pcrb.cpython-311.pyc +0 -0
  18. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  19. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  20. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  21. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  22. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  23. package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
  24. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  25. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  26. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  27. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  28. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  29. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  30. package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
  31. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  32. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  33. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  34. package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
  35. package/runner/tests/test_bet1_spike.py +95 -0
  36. package/runner/tests/test_opencode_ux.py +76 -0
  37. package/runner/tests/test_webcache.py +222 -0
  38. package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
  39. package/scripts/bet1_advisory_spike.py +426 -0
  40. package/skills/epistemic_search/scripts/webcache.py +292 -0
  41. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  42. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  43. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  44. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env python3
2
+ """Content-addressed WebFetch cache-through gate for Claude Code hooks.
3
+
4
+ Replaces the blanket PreToolUse deny with the cache-through pattern
5
+ (proven by theYahia/claude-webcache):
6
+
7
+ gate (PreToolUse WebFetch): serve `.research/sources/<sha256>.md` hits
8
+ via deny + additionalContext (no network); allow misses/stale.
9
+ archive (PostToolUse WebFetch): store the live response as md + json
10
+ sidecar, same layout as SourceHasher (content-hash filename).
11
+ stats (SessionStart): one-line cache summary via additionalContext.
12
+
13
+ Fail-open everywhere: any error exits 0 with no output (allow), logging to
14
+ stderr (visible with --debug). This is a citation-integrity control, not a
15
+ security boundary.
16
+
17
+ URL canonicalization (borrowed from webcache): lowercase host, strip default
18
+ ports and fragments, sort query parameters — so formatting variance does not
19
+ cause silent misses.
20
+
21
+ TTL: sidecar `cached_at`; default 7 days (IUMBTEMS_FETCH_TTL_DAYS), 30 days
22
+ for stable documentation domains. Inline cap for served hits: 12000 chars
23
+ (IUMBTEMS_FETCH_MAX_INLINE), remainder via file pointer.
24
+ """
25
+
26
+ import hashlib
27
+ import json
28
+ import os
29
+ import sys
30
+ import urllib.parse
31
+ from datetime import datetime, timezone
32
+ from pathlib import Path
33
+
34
+ DEFAULT_TTL_DAYS = 7
35
+ DOC_DOMAIN_TTL_DAYS = 30
36
+ DOC_DOMAINS = (
37
+ "code.claude.com",
38
+ "docs.anthropic.com",
39
+ "platform.claude.com",
40
+ "docs.python.org",
41
+ "developer.mozilla.org",
42
+ )
43
+ DEFAULT_MAX_INLINE = 12000
44
+
45
+
46
+ def log(msg):
47
+ sys.stderr.write(f"[webcache] {msg}\n")
48
+
49
+
50
+ def canonical_url(url):
51
+ try:
52
+ p = urllib.parse.urlsplit(url.strip())
53
+ host = (p.hostname or "").lower()
54
+ if not host:
55
+ return None
56
+ port = p.port
57
+ if (p.scheme == "http" and port == 80) or (p.scheme == "https" and port == 443):
58
+ port = None
59
+ netloc = f"{host}:{port}" if port else host
60
+ query = urllib.parse.urlencode(
61
+ sorted(urllib.parse.parse_qsl(p.query, keep_blank_values=True))
62
+ )
63
+ return urllib.parse.urlunsplit((p.scheme or "https", netloc, p.path or "/", query, ""))
64
+ except Exception:
65
+ return None
66
+
67
+
68
+ def domain_of(url):
69
+ try:
70
+ return (urllib.parse.urlsplit(url).hostname or "").lower()
71
+ except Exception:
72
+ return ""
73
+
74
+
75
+ def ttl_days(url):
76
+ try:
77
+ override = int(os.environ.get("IUMBTEMS_FETCH_TTL_DAYS", ""))
78
+ return max(override, 0)
79
+ except ValueError:
80
+ pass
81
+ domain = domain_of(url)
82
+ if any(domain == d or domain.endswith("." + d) for d in DOC_DOMAINS):
83
+ return DOC_DOMAIN_TTL_DAYS
84
+ return DEFAULT_TTL_DAYS
85
+
86
+
87
+ def max_inline():
88
+ try:
89
+ return max(int(os.environ.get("IUMBTEMS_FETCH_MAX_INLINE", "")), 1000)
90
+ except ValueError:
91
+ return DEFAULT_MAX_INLINE
92
+
93
+
94
+ def research_dir(stdin_data):
95
+ base = stdin_data.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()
96
+ return Path(base) / ".research"
97
+
98
+
99
+ def find_cached(sources_dir, url):
100
+ """Scan json sidecars for a matching canonical URL. Returns (md_path, meta) or None."""
101
+ canon = canonical_url(url)
102
+ if not canon or not sources_dir.is_dir():
103
+ return None
104
+ for sidecar in sources_dir.glob("*.json"):
105
+ try:
106
+ meta = json.loads(sidecar.read_text(encoding="utf-8"))
107
+ except (OSError, ValueError):
108
+ continue
109
+ if not isinstance(meta, dict):
110
+ continue
111
+ if canonical_url(str(meta.get("url", ""))) == canon:
112
+ md_path = sidecar.with_suffix(".md")
113
+ if md_path.exists():
114
+ return md_path, meta
115
+ return None
116
+
117
+
118
+ def is_fresh(meta, url):
119
+ try:
120
+ cached_at = datetime.fromisoformat(str(meta.get("cached_at", "")))
121
+ if cached_at.tzinfo is None:
122
+ cached_at = cached_at.replace(tzinfo=timezone.utc)
123
+ age_days = (datetime.now(timezone.utc) - cached_at).total_seconds() / 86400
124
+ return age_days <= ttl_days(url)
125
+ except (ValueError, TypeError):
126
+ return False
127
+
128
+
129
+ def cmd_gate(stdin_data):
130
+ """PreToolUse: serve fresh hits, allow everything else (fail-open)."""
131
+ tool_input = stdin_data.get("tool_input") or {}
132
+ url = tool_input.get("url", "")
133
+ if not url:
134
+ return None # allow
135
+ sources_dir = research_dir(stdin_data) / "sources"
136
+ try:
137
+ found = find_cached(sources_dir, url)
138
+ except Exception as e:
139
+ log(f"cache lookup failed for {domain_of(url)}: {e}")
140
+ return None # allow
141
+ if not found:
142
+ return None # miss -> allow, PostToolUse archives
143
+ md_path, meta = found
144
+ if not is_fresh(meta, url):
145
+ log(f"stale entry for {domain_of(url)} ({meta.get('cached_at')}); allowing live fetch")
146
+ return None # stale -> allow, PostToolUse refreshes
147
+ try:
148
+ content = md_path.read_text(encoding="utf-8")
149
+ except OSError as e:
150
+ log(f"cache read failed: {e}")
151
+ return None # allow
152
+ cap = max_inline()
153
+ served = content if len(content) <= cap else content[:cap] + "\n\n[... truncated]"
154
+ body = (
155
+ f"<!-- EPISTEMIC_CACHE_HIT: {md_path.name} -->\n"
156
+ f"**Source URL:** {url}\n"
157
+ f"**Cached:** {meta.get('cached_at')} **Content SHA-256:** `{meta.get('hash')}`\n"
158
+ f"**Verification Tag:** `[VERIFIED: {str(meta.get('hash', ''))[:16]}]`\n\n"
159
+ f"{served}\n"
160
+ )
161
+ if len(content) > cap:
162
+ body += f"\n(Full cached text in {md_path}; cite the file for passages beyond the excerpt.)\n"
163
+ log(f"cache HIT for {domain_of(url)} ({len(content)} chars, {md_path.name})")
164
+ return {
165
+ "hookSpecificOutput": {
166
+ "hookEventName": "PreToolUse",
167
+ "permissionDecision": "deny",
168
+ "permissionDecisionReason": (
169
+ "Served from the epistemic content cache (.research/sources); "
170
+ "no network fetch needed. Cite the cached file."
171
+ ),
172
+ "additionalContext": body,
173
+ }
174
+ }
175
+
176
+
177
+ def extract_response_text(stdin_data):
178
+ """Defensively locate the tool response text across field-name variants."""
179
+ for key in ("tool_response", "response", "tool_result", "result", "output"):
180
+ val = stdin_data.get(key)
181
+ if isinstance(val, str) and val.strip():
182
+ return val
183
+ if isinstance(val, dict):
184
+ for sub in ("text", "content", "output"):
185
+ inner = val.get(sub)
186
+ if isinstance(inner, str) and inner.strip():
187
+ return inner
188
+ if isinstance(inner, list):
189
+ parts = [
190
+ p.get("text", "") for p in inner
191
+ if isinstance(p, dict) and isinstance(p.get("text"), str)
192
+ ]
193
+ if parts:
194
+ return "\n".join(parts)
195
+ return ""
196
+
197
+
198
+ def extract_url(stdin_data):
199
+ tool_input = stdin_data.get("tool_input") or stdin_data.get("input") or {}
200
+ if isinstance(tool_input, dict):
201
+ url = tool_input.get("url", "")
202
+ if url:
203
+ return url
204
+ # Fall back to a URL already recorded in a response envelope.
205
+ for key in ("tool_response", "response", "tool_result"):
206
+ val = stdin_data.get(key)
207
+ if isinstance(val, dict) and val.get("url"):
208
+ return val["url"]
209
+ return ""
210
+
211
+
212
+ def cmd_archive(stdin_data):
213
+ """PostToolUse: store the live response as md + json sidecar."""
214
+ url = extract_url(stdin_data)
215
+ text = extract_response_text(stdin_data)
216
+ if not url or not text:
217
+ return None
218
+ try:
219
+ sources_dir = research_dir(stdin_data) / "sources"
220
+ sources_dir.mkdir(parents=True, exist_ok=True)
221
+ digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
222
+ sources_dir.joinpath(f"{digest}.md").write_text(text, encoding="utf-8")
223
+ sources_dir.joinpath(f"{digest}.json").write_text(
224
+ json.dumps(
225
+ {
226
+ "hash": digest,
227
+ "url": url,
228
+ "title": url,
229
+ "tier": "WEB_DOCUMENT",
230
+ "cached_at": datetime.now(timezone.utc).isoformat(),
231
+ "byte_size": len(text.encode("utf-8")),
232
+ "char_count": len(text),
233
+ "custom_metadata": {"via": "webfetch-hook"},
234
+ },
235
+ indent=2,
236
+ ),
237
+ encoding="utf-8",
238
+ )
239
+ log(f"archived {domain_of(url)} -> {digest[:16]} ({len(text)} chars)")
240
+ except Exception as e:
241
+ log(f"archive failed for {domain_of(url)}: {e}")
242
+ return None
243
+
244
+
245
+ def cmd_stats(stdin_data):
246
+ """SessionStart: one-line cache summary; silent when empty."""
247
+ try:
248
+ sources_dir = research_dir(stdin_data) / "sources"
249
+ if not sources_dir.is_dir():
250
+ return None
251
+ md_files = list(sources_dir.glob("*.md"))
252
+ if not md_files:
253
+ return None
254
+ return {
255
+ "hookSpecificOutput": {
256
+ "hookEventName": "SessionStart",
257
+ "additionalContext": (
258
+ f"epistemic-cache: {len(md_files)} page(s) cached in "
259
+ f".research/sources — WebFetch hits are served from cache."
260
+ ),
261
+ }
262
+ }
263
+ except Exception as e:
264
+ log(f"stats failed: {e}")
265
+ return None
266
+
267
+
268
+ def main(argv):
269
+ mode = argv[1] if len(argv) > 1 else "gate"
270
+ try:
271
+ raw = sys.stdin.read() if not sys.stdin.isatty() else ""
272
+ stdin_data = json.loads(raw) if raw.strip() else {}
273
+ if not isinstance(stdin_data, dict):
274
+ stdin_data = {}
275
+ except (ValueError, OSError):
276
+ stdin_data = {}
277
+ handler = {"gate": cmd_gate, "archive": cmd_archive, "stats": cmd_stats}.get(mode)
278
+ if handler is None:
279
+ log(f"unknown mode: {mode}")
280
+ return 0
281
+ try:
282
+ out = handler(stdin_data)
283
+ except Exception as e:
284
+ log(f"{mode} failed: {e}")
285
+ return 0 # fail-open
286
+ if out:
287
+ sys.stdout.write(json.dumps(out))
288
+ return 0
289
+
290
+
291
+ if __name__ == "__main__":
292
+ sys.exit(main(sys.argv))