@heretek-ai/epistemic-swarm 0.6.0 → 0.7.2
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/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +4 -2
- package/MARKETPLACE.md +28 -0
- package/bin/cli.js +1 -0
- package/extensions/pi/index.js +7 -1
- package/install.sh +1 -1
- package/package.json +1 -1
- package/plugins/darkharvest/.claude-plugin/plugin.json +15 -0
- package/plugins/darkharvest/agents/harvest-proponent.md +22 -0
- package/plugins/darkharvest/agents/harvest-redteam.md +20 -0
- package/plugins/darkharvest/evals/teardown-verdict/graders/license-line.md +6 -0
- package/plugins/darkharvest/evals/teardown-verdict/graders/skill-fired.md +5 -0
- package/plugins/darkharvest/evals/teardown-verdict/prompt.md +6 -0
- package/plugins/darkharvest/skills/darkharvest/SKILL.md +70 -0
- package/plugins/darkharvest/skills/darkharvest/scripts/harvest.py +289 -0
- package/plugins/factory/.claude-plugin/plugin.json +15 -0
- package/plugins/factory/agents/factory-manager.md +22 -0
- package/plugins/factory/agents/programmer.md +16 -0
- package/plugins/factory/agents/qa-adversarial.md +17 -0
- package/plugins/factory/agents/qa-functional.md +17 -0
- package/plugins/factory/evals/gate-halt/graders/gates-first.md +6 -0
- package/plugins/factory/evals/gate-halt/graders/skill-fired.md +5 -0
- package/plugins/factory/evals/gate-halt/prompt.md +6 -0
- package/plugins/factory/skills/factory/SKILL.md +51 -0
- package/plugins/factory/skills/factory/scripts/factory.py +212 -0
- package/plugins/opencode/index.js +41 -1
- package/plugins/opencode/tui.js +66 -38
- 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__/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__/refinement.cpython-311.pyc +0 -0
- package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
- package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
- package/runner/claim_store.py +15 -4
- package/runner/living_dossiers.py +74 -56
- package/runner/mcp_server.py +37 -2
- package/runner/pcrb.py +53 -29
- package/runner/refinement.py +70 -27
- package/runner/research_swarm.py +336 -127
- 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_domain_packs.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_living_dossiers.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_pcrb.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm.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/test_backends.py +280 -0
- package/runner/tests/test_claude_plugin.py +170 -0
- package/runner/tests/test_sweep_regressions.py +71 -38
- package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
- package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
- package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
- package/scripts/build_adapters.py +87 -9
- package/skills/darkharvest/scripts/harvest.py +76 -39
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/epistemic_search/scripts/search.py +29 -19
- package/skills/epistemic_search/scripts/webcache.py +29 -17
- package/skills/factory/scripts/factory.py +1 -1
- 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/skills/swarm_config/configure.py +70 -37
|
@@ -43,6 +43,17 @@ CANONICAL_SKILLS = [
|
|
|
43
43
|
"factory",
|
|
44
44
|
]
|
|
45
45
|
|
|
46
|
+
# Modular Claude Code plugins mirror canonical skills as full copies (Claude
|
|
47
|
+
# Code loads plugin-local skills/, not the repo skills/ tree). Rule: copies
|
|
48
|
+
# are generated — never hand-edit under plugins/<mod>/skills/. Rebuild with
|
|
49
|
+
# `python3 scripts/build_adapters.py`; `--check` fails CI on drift.
|
|
50
|
+
MODULAR_PLUGINS = {
|
|
51
|
+
"socratic-grilling": "grilling",
|
|
52
|
+
"research-cache": "research_cache",
|
|
53
|
+
"darkharvest": "darkharvest",
|
|
54
|
+
"factory": "factory",
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
# Skill -> the MCP tool(s) that now carry its programmatic surface.
|
|
47
58
|
SKILL_TOOLS = {
|
|
48
59
|
"grilling": ["iumbtems_socratic_frontier"],
|
|
@@ -133,9 +144,70 @@ def build():
|
|
|
133
144
|
if _build_skill_stub(target_base, skill):
|
|
134
145
|
count += 1
|
|
135
146
|
print(f"✅ Built {count} thin skill stubs across {len(TARGETS)} targets.")
|
|
147
|
+
synced = sync_modular_plugins()
|
|
148
|
+
print(f"✅ Synced {synced} modular Claude Code plugin skill copies.")
|
|
136
149
|
return 0
|
|
137
150
|
|
|
138
151
|
|
|
152
|
+
def _collect_files(root: Path) -> dict:
|
|
153
|
+
"""Relative-path -> Path map for an existing tree (no __pycache__)."""
|
|
154
|
+
if not root.exists():
|
|
155
|
+
return {}
|
|
156
|
+
return {
|
|
157
|
+
p.relative_to(root): p
|
|
158
|
+
for p in sorted(root.rglob("*"))
|
|
159
|
+
if p.is_file() and "__pycache__" not in p.parts
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _iter_modular_files(src: Path, dst: Path):
|
|
164
|
+
"""Yield (relpath, src_bytes|None, dst_bytes|None) for sync/check."""
|
|
165
|
+
src_files = _collect_files(src)
|
|
166
|
+
dst_files = _collect_files(dst)
|
|
167
|
+
for rel in sorted(set(src_files) | set(dst_files)):
|
|
168
|
+
s = src_files.get(rel)
|
|
169
|
+
d = dst_files.get(rel)
|
|
170
|
+
yield rel, (s.read_bytes() if s else None), (d.read_bytes() if d else None)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def sync_modular_plugins() -> int:
|
|
174
|
+
synced = 0
|
|
175
|
+
for mod, skill in MODULAR_PLUGINS.items():
|
|
176
|
+
src = PROJECT_ROOT / "skills" / skill
|
|
177
|
+
dst = PROJECT_ROOT / "plugins" / mod / "skills" / skill
|
|
178
|
+
if not (src / SKILL_FILENAME).exists():
|
|
179
|
+
print(f"[WARN] canonical skill missing: skills/{skill}", file=sys.stderr)
|
|
180
|
+
continue
|
|
181
|
+
if dst.exists():
|
|
182
|
+
shutil.rmtree(dst)
|
|
183
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
for rel, content, _ in _iter_modular_files(src, dst):
|
|
185
|
+
if content is None:
|
|
186
|
+
continue
|
|
187
|
+
out = dst / rel
|
|
188
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
out.write_bytes(content)
|
|
190
|
+
shutil.copymode(src / rel, out)
|
|
191
|
+
synced += 1
|
|
192
|
+
return synced
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _check_modular_plugins() -> List[str]:
|
|
196
|
+
errors = []
|
|
197
|
+
for mod, skill in MODULAR_PLUGINS.items():
|
|
198
|
+
src = PROJECT_ROOT / "skills" / skill
|
|
199
|
+
dst = PROJECT_ROOT / "plugins" / mod / "skills" / skill
|
|
200
|
+
if not dst.exists():
|
|
201
|
+
errors.append(f"MISSING modular copy: plugins/{mod}/skills/{skill}/")
|
|
202
|
+
continue
|
|
203
|
+
for rel, s_bytes, d_bytes in _iter_modular_files(src, dst):
|
|
204
|
+
if s_bytes is None:
|
|
205
|
+
errors.append(f"STRAY FILE in plugins/{mod}/skills/{skill}/{rel}")
|
|
206
|
+
elif s_bytes != d_bytes:
|
|
207
|
+
errors.append(f"OUT OF SYNC: plugins/{mod}/skills/{skill}/{rel}")
|
|
208
|
+
return errors
|
|
209
|
+
|
|
210
|
+
|
|
139
211
|
def _check_skill_stub(target_rel: str, skill: str) -> List[str]:
|
|
140
212
|
errors = []
|
|
141
213
|
dst = PROJECT_ROOT / target_rel / skill / SKILL_FILENAME
|
|
@@ -153,20 +225,25 @@ def _check_skill_stub(target_rel: str, skill: str) -> List[str]:
|
|
|
153
225
|
return errors
|
|
154
226
|
|
|
155
227
|
|
|
228
|
+
def _check_manifest_path(rel: str, key: str, kind: str) -> List[str]:
|
|
229
|
+
"""Validate one pi/omp manifest path or glob entry."""
|
|
230
|
+
if "*" in rel:
|
|
231
|
+
if list(PROJECT_ROOT.glob(rel.lstrip("./"))):
|
|
232
|
+
return []
|
|
233
|
+
return [f"package.json {key}.{kind} glob matches nothing: {rel}"]
|
|
234
|
+
if (PROJECT_ROOT / rel.lstrip("./")).exists():
|
|
235
|
+
return []
|
|
236
|
+
return [f"package.json {key}.{kind} missing: {rel}"]
|
|
237
|
+
|
|
238
|
+
|
|
156
239
|
def _check_package_json() -> List[str]:
|
|
157
|
-
errors = []
|
|
158
240
|
pkg = json.loads((PROJECT_ROOT / "package.json").read_text())
|
|
241
|
+
errors: List[str] = []
|
|
159
242
|
for key in ("pi", "omp"):
|
|
160
243
|
block = pkg.get(key, {})
|
|
161
244
|
for kind in ("skills", "extensions", "prompts"):
|
|
162
|
-
for
|
|
163
|
-
|
|
164
|
-
if not list(PROJECT_ROOT.glob(p.lstrip("./"))):
|
|
165
|
-
errors.append(
|
|
166
|
-
f"package.json {key}.{kind} glob matches nothing: {p}"
|
|
167
|
-
)
|
|
168
|
-
elif not (PROJECT_ROOT / p.lstrip("./")).exists():
|
|
169
|
-
errors.append(f"package.json {key}.{kind} missing: {p}")
|
|
245
|
+
for rel in block.get(kind, []):
|
|
246
|
+
errors.extend(_check_manifest_path(rel, key, kind))
|
|
170
247
|
return errors
|
|
171
248
|
|
|
172
249
|
|
|
@@ -176,6 +253,7 @@ def check():
|
|
|
176
253
|
for target_rel, _mode in TARGETS:
|
|
177
254
|
for skill in CANONICAL_SKILLS:
|
|
178
255
|
errors.extend(_check_skill_stub(target_rel, skill))
|
|
256
|
+
errors.extend(_check_modular_plugins())
|
|
179
257
|
errors.extend(_check_package_json())
|
|
180
258
|
if errors:
|
|
181
259
|
print("❌ Adapter check failed:")
|
|
@@ -59,11 +59,19 @@ TEXT_EXTS = {".md", ".json", ".toml", ".yaml", ".yml", ".txt", ".py", ".js", ".t
|
|
|
59
59
|
DEFAULT_MAX_FILES = 120
|
|
60
60
|
DEFAULT_MAX_BYTES = 400_000
|
|
61
61
|
DEFAULT_TIMEOUT = 120
|
|
62
|
+
DEFAULT_BASE_DIR = ".research"
|
|
63
|
+
MAX_SCAN_DEPTH = 3
|
|
64
|
+
MAX_SINGLE_FILE_BYTES = 100_000
|
|
65
|
+
SNIPPET_CHARS = 4000
|
|
66
|
+
|
|
67
|
+
SPDX_APACHE2 = "Apache-2.0"
|
|
68
|
+
PERMISSIVE_SPDX = frozenset({"MIT", SPDX_APACHE2, "BSD-3-Clause", "ISC"})
|
|
69
|
+
COPYLEFT_SPDX = frozenset({"GPL", "AGPL"})
|
|
62
70
|
|
|
63
71
|
SPDX_MAP = {
|
|
64
72
|
"mit": "MIT",
|
|
65
|
-
"apache-2.0":
|
|
66
|
-
"apache 2.0":
|
|
73
|
+
"apache-2.0": SPDX_APACHE2,
|
|
74
|
+
"apache 2.0": SPDX_APACHE2,
|
|
67
75
|
"bsd-3-clause": "BSD-3-Clause",
|
|
68
76
|
"bsd 3-clause": "BSD-3-Clause",
|
|
69
77
|
"isc": "ISC",
|
|
@@ -97,43 +105,74 @@ def detect_license(text):
|
|
|
97
105
|
|
|
98
106
|
|
|
99
107
|
def scan_tree(root, max_files=DEFAULT_MAX_FILES, max_bytes=DEFAULT_MAX_BYTES):
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
manifests = {}
|
|
103
|
-
license_text = ""
|
|
104
|
-
readme_head = ""
|
|
108
|
+
root = Path(root)
|
|
109
|
+
state = _ScanState()
|
|
105
110
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
106
111
|
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
107
112
|
rel = Path(dirpath).relative_to(root)
|
|
108
|
-
|
|
109
|
-
if depth > 3:
|
|
113
|
+
if (len(rel.parts) if str(rel) != "." else 0) > MAX_SCAN_DEPTH:
|
|
110
114
|
dirnames[:] = []
|
|
111
115
|
continue
|
|
116
|
+
prefix = "" if str(rel) == "." else str(rel) + os.sep
|
|
112
117
|
for f in sorted(filenames):
|
|
113
|
-
if
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
118
|
+
if state.capped(max_files, max_bytes):
|
|
119
|
+
return state.finish(truncated=True)
|
|
120
|
+
_scan_file(dirpath, prefix + f, state)
|
|
121
|
+
return state.finish(truncated=False)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class _ScanState:
|
|
125
|
+
"""Mutable accumulator for one scan_tree pass (keeps helpers small)."""
|
|
126
|
+
|
|
127
|
+
def __init__(self):
|
|
128
|
+
self.entries = []
|
|
129
|
+
self.total_bytes = 0
|
|
130
|
+
self.manifests = {}
|
|
131
|
+
self.license_text = ""
|
|
132
|
+
self.readme_head = ""
|
|
133
|
+
|
|
134
|
+
def capped(self, max_files, max_bytes):
|
|
135
|
+
return len(self.entries) >= max_files or self.total_bytes >= max_bytes
|
|
136
|
+
|
|
137
|
+
def finish(self, truncated):
|
|
138
|
+
return (
|
|
139
|
+
self.entries,
|
|
140
|
+
self.manifests,
|
|
141
|
+
self.license_text,
|
|
142
|
+
self.readme_head,
|
|
143
|
+
truncated,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _scan_file(dirpath, rel_entry, state):
|
|
148
|
+
filename = os.path.basename(rel_entry)
|
|
149
|
+
try:
|
|
150
|
+
size = (Path(dirpath) / filename).stat().st_size
|
|
151
|
+
except OSError:
|
|
152
|
+
return
|
|
153
|
+
if size > MAX_SINGLE_FILE_BYTES:
|
|
154
|
+
return
|
|
155
|
+
state.entries.append(rel_entry)
|
|
156
|
+
if filename in MANIFEST_FILES or Path(filename).suffix in TEXT_EXTS:
|
|
157
|
+
_capture_text(dirpath, filename, state)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _capture_text(dirpath, filename, state):
|
|
161
|
+
try:
|
|
162
|
+
content = (Path(dirpath) / filename).read_text(
|
|
163
|
+
encoding="utf-8", errors="replace"
|
|
164
|
+
)
|
|
165
|
+
except OSError:
|
|
166
|
+
return
|
|
167
|
+
state.total_bytes += len(content.encode("utf-8", errors="replace"))
|
|
168
|
+
snippet = content[:SNIPPET_CHARS]
|
|
169
|
+
if filename in MANIFEST_FILES and filename not in state.manifests:
|
|
170
|
+
state.manifests[filename] = snippet
|
|
171
|
+
lowered = filename.lower()
|
|
172
|
+
if lowered.startswith("license") and not state.license_text:
|
|
173
|
+
state.license_text = snippet
|
|
174
|
+
if lowered == "readme.md" and not state.readme_head:
|
|
175
|
+
state.readme_head = snippet
|
|
137
176
|
|
|
138
177
|
|
|
139
178
|
def harvest_repo(
|
|
@@ -141,7 +180,7 @@ def harvest_repo(
|
|
|
141
180
|
max_files=DEFAULT_MAX_FILES,
|
|
142
181
|
max_bytes=DEFAULT_MAX_BYTES,
|
|
143
182
|
timeout=DEFAULT_TIMEOUT,
|
|
144
|
-
base_dir=
|
|
183
|
+
base_dir=DEFAULT_BASE_DIR,
|
|
145
184
|
):
|
|
146
185
|
started = datetime.now(timezone.utc).isoformat()
|
|
147
186
|
tmp = tempfile.mkdtemp(prefix="darkharvest-")
|
|
@@ -162,9 +201,7 @@ def harvest_repo(
|
|
|
162
201
|
)
|
|
163
202
|
spdx = detect_license(license_text + manifests.get("package.json", ""))
|
|
164
203
|
harvestable = (
|
|
165
|
-
"depend-or-vendor"
|
|
166
|
-
if spdx in ("MIT", "Apache-2.0", "BSD-3-Clause", "ISC")
|
|
167
|
-
else "clean-room-rebuild-only"
|
|
204
|
+
"depend-or-vendor" if spdx in PERMISSIVE_SPDX else "clean-room-rebuild-only"
|
|
168
205
|
)
|
|
169
206
|
hasher = SourceHasher(Path(base_dir))
|
|
170
207
|
cache_blob = (
|
|
@@ -174,7 +211,7 @@ def harvest_repo(
|
|
|
174
211
|
)
|
|
175
212
|
source_hash = hasher.store_source(url, cache_blob, f"Darkharvest {url}")
|
|
176
213
|
warnings = []
|
|
177
|
-
if spdx in
|
|
214
|
+
if spdx in COPYLEFT_SPDX:
|
|
178
215
|
warnings.append(
|
|
179
216
|
"⚠️ LICENSE: strong copyleft — spec rebuild only, never vendor"
|
|
180
217
|
)
|
|
@@ -209,7 +246,7 @@ def main():
|
|
|
209
246
|
ap.add_argument("--max-files", type=int, default=DEFAULT_MAX_FILES)
|
|
210
247
|
ap.add_argument("--max-bytes", type=int, default=DEFAULT_MAX_BYTES)
|
|
211
248
|
ap.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT)
|
|
212
|
-
ap.add_argument("--dir", default=
|
|
249
|
+
ap.add_argument("--dir", default=DEFAULT_BASE_DIR)
|
|
213
250
|
args = ap.parse_args()
|
|
214
251
|
|
|
215
252
|
if not re.match(r"^https?://", args.repo):
|
|
Binary file
|
|
@@ -79,11 +79,9 @@ class DDGLiteParser(HTMLParser):
|
|
|
79
79
|
self._row_snippet = []
|
|
80
80
|
|
|
81
81
|
def handle_endtag(self, tag: str):
|
|
82
|
-
if self._capture == "title" and tag == "a"
|
|
83
|
-
self.
|
|
84
|
-
|
|
85
|
-
self._capture = None
|
|
86
|
-
elif self._capture == "snippet" and tag == "td":
|
|
82
|
+
if (self._capture == "title" and tag == "a") or (
|
|
83
|
+
self._capture == "snippet" and tag == "td"
|
|
84
|
+
):
|
|
87
85
|
self._depth -= 1
|
|
88
86
|
if self._depth <= 0:
|
|
89
87
|
self._capture = None
|
|
@@ -99,11 +97,13 @@ class DDGLiteParser(HTMLParser):
|
|
|
99
97
|
def flush(self):
|
|
100
98
|
"""Emit the in-progress row, if any. Call after ``feed()`` completes."""
|
|
101
99
|
if self._row_href is not None:
|
|
102
|
-
self.results.append(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
self.results.append(
|
|
101
|
+
{
|
|
102
|
+
"href": self._row_href,
|
|
103
|
+
"title": "".join(self._row_title).strip(),
|
|
104
|
+
"snippet": "".join(self._row_snippet).strip(),
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
107
|
self._row_href = None
|
|
108
108
|
self._row_title = []
|
|
109
109
|
self._row_snippet = []
|
|
@@ -138,7 +138,9 @@ def _fetch_ddg_html(effective_query: str) -> str:
|
|
|
138
138
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
139
139
|
return resp.read().decode("utf-8", errors="replace")
|
|
140
140
|
except Exception as e:
|
|
141
|
-
sys.stderr.write(
|
|
141
|
+
sys.stderr.write(
|
|
142
|
+
f"[epistemic-search] Network error fetching search results: {e}\n"
|
|
143
|
+
)
|
|
142
144
|
return ""
|
|
143
145
|
|
|
144
146
|
|
|
@@ -154,9 +156,13 @@ def _is_domain_allowed(
|
|
|
154
156
|
allowed_domains: Optional[List[str]] = None,
|
|
155
157
|
blocked_domains: Optional[List[str]] = None,
|
|
156
158
|
) -> bool:
|
|
157
|
-
if allowed_domains and not any(
|
|
159
|
+
if allowed_domains and not any(
|
|
160
|
+
hostname == d or hostname.endswith("." + d) for d in allowed_domains
|
|
161
|
+
):
|
|
158
162
|
return False
|
|
159
|
-
if blocked_domains and any(
|
|
163
|
+
if blocked_domains and any(
|
|
164
|
+
hostname == d or hostname.endswith("." + d) for d in blocked_domains
|
|
165
|
+
):
|
|
160
166
|
return False
|
|
161
167
|
return True
|
|
162
168
|
|
|
@@ -188,11 +194,13 @@ def search_duckduckgo(
|
|
|
188
194
|
if not _is_domain_allowed(hostname, allowed_domains, blocked_domains):
|
|
189
195
|
continue
|
|
190
196
|
|
|
191
|
-
results.append(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
results.append(
|
|
198
|
+
{
|
|
199
|
+
"title": row["title"],
|
|
200
|
+
"url": clean_url,
|
|
201
|
+
"snippet": row["snippet"],
|
|
202
|
+
}
|
|
203
|
+
)
|
|
196
204
|
if len(results) >= max_results:
|
|
197
205
|
break
|
|
198
206
|
|
|
@@ -272,7 +280,9 @@ def main():
|
|
|
272
280
|
)
|
|
273
281
|
|
|
274
282
|
if not query:
|
|
275
|
-
sys.stderr.write(
|
|
283
|
+
sys.stderr.write(
|
|
284
|
+
'Usage: search.py [options] <query>\nOr pipe JSON: echo \'{"query":"..."}\' | search.py\n'
|
|
285
|
+
)
|
|
276
286
|
sys.exit(1)
|
|
277
287
|
|
|
278
288
|
results = search_duckduckgo(
|
|
@@ -60,7 +60,9 @@ def canonical_url(url):
|
|
|
60
60
|
query = urllib.parse.urlencode(
|
|
61
61
|
sorted(urllib.parse.parse_qsl(p.query, keep_blank_values=True))
|
|
62
62
|
)
|
|
63
|
-
return urllib.parse.urlunsplit(
|
|
63
|
+
return urllib.parse.urlunsplit(
|
|
64
|
+
(p.scheme or "https", netloc, p.path or "/", query, "")
|
|
65
|
+
)
|
|
64
66
|
except Exception:
|
|
65
67
|
return None
|
|
66
68
|
|
|
@@ -142,7 +144,9 @@ def cmd_gate(stdin_data):
|
|
|
142
144
|
return None # miss -> allow, PostToolUse archives
|
|
143
145
|
md_path, meta = found
|
|
144
146
|
if not is_fresh(meta, url):
|
|
145
|
-
log(
|
|
147
|
+
log(
|
|
148
|
+
f"stale entry for {domain_of(url)} ({meta.get('cached_at')}); allowing live fetch"
|
|
149
|
+
)
|
|
146
150
|
return None # stale -> allow, PostToolUse refreshes
|
|
147
151
|
try:
|
|
148
152
|
content = md_path.read_text(encoding="utf-8")
|
|
@@ -174,24 +178,32 @@ def cmd_gate(stdin_data):
|
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
|
|
181
|
+
def _text_from_value(val):
|
|
182
|
+
"""Recursively pull the first non-empty text out of a response value."""
|
|
183
|
+
if isinstance(val, str) and val.strip():
|
|
184
|
+
return val
|
|
185
|
+
if isinstance(val, dict):
|
|
186
|
+
for sub in ("text", "content", "output"):
|
|
187
|
+
got = _text_from_value(val.get(sub))
|
|
188
|
+
if got:
|
|
189
|
+
return got
|
|
190
|
+
if isinstance(val, list):
|
|
191
|
+
parts = [
|
|
192
|
+
p.get("text", "")
|
|
193
|
+
for p in val
|
|
194
|
+
if isinstance(p, dict) and isinstance(p.get("text"), str)
|
|
195
|
+
]
|
|
196
|
+
if parts:
|
|
197
|
+
return "\n".join(parts)
|
|
198
|
+
return ""
|
|
199
|
+
|
|
200
|
+
|
|
177
201
|
def extract_response_text(stdin_data):
|
|
178
202
|
"""Defensively locate the tool response text across field-name variants."""
|
|
179
203
|
for key in ("tool_response", "response", "tool_result", "result", "output"):
|
|
180
|
-
|
|
181
|
-
if
|
|
182
|
-
return
|
|
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)
|
|
204
|
+
got = _text_from_value(stdin_data.get(key))
|
|
205
|
+
if got:
|
|
206
|
+
return got
|
|
195
207
|
return ""
|
|
196
208
|
|
|
197
209
|
|
|
@@ -150,7 +150,7 @@ def cmd_expansion(args):
|
|
|
150
150
|
state["expansion"]["loops_planned"] = n
|
|
151
151
|
save_state(args.run, state)
|
|
152
152
|
done = 0
|
|
153
|
-
for
|
|
153
|
+
for _ in range(1, n + 1):
|
|
154
154
|
if stop_requested(args.run):
|
|
155
155
|
print(f"🛑 STOP file present — halting after {done}/{n} loops.")
|
|
156
156
|
break
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -17,6 +17,7 @@ DEFAULT_CONFIG: Dict[str, Any] = {
|
|
|
17
17
|
"max_iterations": 2,
|
|
18
18
|
"divergence_threshold": 0.75,
|
|
19
19
|
"mode": "research", # "research", "audit", "scout", "hybrid", "brainstorm", "darkharvest"
|
|
20
|
+
"backend": "auto", # "auto" (host-native) | "claude" | "opencode"
|
|
20
21
|
"cache_raw_markdown": True,
|
|
21
22
|
"license_whitelist": ["MIT", "Apache-2.0", "BSD-3-Clause", "ISC"],
|
|
22
23
|
"output_dir": DEFAULT_RESEARCH_DIR,
|
|
@@ -31,10 +32,12 @@ DEFAULT_CONFIG: Dict[str, Any] = {
|
|
|
31
32
|
# different model families/weights probe divergence that prompt-level
|
|
32
33
|
# red-teaming cannot reach. `backend` is a command list, not just a model
|
|
33
34
|
# string, because cross-FAMILY means non-Claude processes.
|
|
35
|
+
# None means "fall through to the host-native default" (claude -p on
|
|
36
|
+
# Claude Code, opencode run on OpenCode) — explicit values still win.
|
|
34
37
|
# mock mode never builds or spawns these commands.
|
|
35
38
|
"agents": {
|
|
36
|
-
"alpha": {"backend":
|
|
37
|
-
"beta": {"backend":
|
|
39
|
+
"alpha": {"backend": None, "model": None},
|
|
40
|
+
"beta": {"backend": None, "model": None},
|
|
38
41
|
},
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -78,6 +81,9 @@ def display_config(cfg: Dict[str, Any]):
|
|
|
78
81
|
print(f" 🔄 Max Iterations (Depth): {cfg.get('max_iterations', 2)}")
|
|
79
82
|
print(f" ⚖️ Divergence Threshold: {cfg.get('divergence_threshold', 0.75)}")
|
|
80
83
|
print(f" 🎯 Operating Mode: {cfg.get('mode', 'research').upper()}")
|
|
84
|
+
print(
|
|
85
|
+
f" 🖥️ Agent Backend: {cfg.get('backend', 'auto').upper()} (auto = host-native)"
|
|
86
|
+
)
|
|
81
87
|
print(
|
|
82
88
|
f" 💾 Raw Markdown Caching: {'ENABLED' if cfg.get('cache_raw_markdown', True) else 'DISABLED'}"
|
|
83
89
|
)
|
|
@@ -141,44 +147,71 @@ def run_interactive(cfg: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
141
147
|
return cfg
|
|
142
148
|
|
|
143
149
|
|
|
150
|
+
def _set_engine(cfg, value):
|
|
151
|
+
if value.lower() in ("duckduckgo", "brave", "firecrawl", "searxng"):
|
|
152
|
+
cfg["search_engine"] = value.lower()
|
|
153
|
+
return True
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _set_depth(cfg, value):
|
|
158
|
+
try:
|
|
159
|
+
cfg["max_iterations"] = int(value)
|
|
160
|
+
return True
|
|
161
|
+
except ValueError:
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _set_mode(cfg, value):
|
|
166
|
+
if value.lower() in (
|
|
167
|
+
"research",
|
|
168
|
+
"audit",
|
|
169
|
+
"scout",
|
|
170
|
+
"hybrid",
|
|
171
|
+
"brainstorm",
|
|
172
|
+
"darkharvest",
|
|
173
|
+
):
|
|
174
|
+
cfg["mode"] = value.lower()
|
|
175
|
+
return True
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _set_divergence(cfg, value):
|
|
180
|
+
try:
|
|
181
|
+
cfg["divergence_threshold"] = float(value)
|
|
182
|
+
return True
|
|
183
|
+
except ValueError:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _set_backend(cfg, value):
|
|
188
|
+
if value.lower() in ("auto", "claude", "opencode"):
|
|
189
|
+
cfg["backend"] = value.lower()
|
|
190
|
+
return True
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# flag spellings -> setter; each setter returns True when it changed config.
|
|
195
|
+
_CLI_HANDLERS = [
|
|
196
|
+
(("--engine", "-e"), _set_engine),
|
|
197
|
+
(("--depth", "-d", "--iterations"), _set_depth),
|
|
198
|
+
(("--mode", "-m"), _set_mode),
|
|
199
|
+
(("--divergence",), _set_divergence),
|
|
200
|
+
(("--backend",), _set_backend),
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
|
|
144
204
|
def _parse_cli_updates(args: list, cfg: Dict[str, Any]) -> bool:
|
|
145
|
-
idx = 0
|
|
146
205
|
modified = False
|
|
206
|
+
idx = 0
|
|
147
207
|
while idx < len(args):
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
elif arg in ("--depth", "-d", "--iterations") and idx + 1 < len(args):
|
|
156
|
-
try:
|
|
157
|
-
cfg["max_iterations"] = int(args[idx + 1])
|
|
158
|
-
modified = True
|
|
159
|
-
except ValueError:
|
|
160
|
-
pass
|
|
161
|
-
idx += 1
|
|
162
|
-
elif arg in ("--mode", "-m") and idx + 1 < len(args):
|
|
163
|
-
mode = args[idx + 1].lower()
|
|
164
|
-
if mode in (
|
|
165
|
-
"research",
|
|
166
|
-
"audit",
|
|
167
|
-
"scout",
|
|
168
|
-
"hybrid",
|
|
169
|
-
"brainstorm",
|
|
170
|
-
"darkharvest",
|
|
171
|
-
):
|
|
172
|
-
cfg["mode"] = mode
|
|
173
|
-
modified = True
|
|
174
|
-
idx += 1
|
|
175
|
-
elif arg == "--divergence" and idx + 1 < len(args):
|
|
176
|
-
try:
|
|
177
|
-
cfg["divergence_threshold"] = float(args[idx + 1])
|
|
178
|
-
modified = True
|
|
179
|
-
except ValueError:
|
|
180
|
-
pass
|
|
181
|
-
idx += 1
|
|
208
|
+
value = args[idx + 1] if idx + 1 < len(args) else None
|
|
209
|
+
for flags, handler in _CLI_HANDLERS:
|
|
210
|
+
if args[idx] in flags:
|
|
211
|
+
if value is not None and handler(cfg, value):
|
|
212
|
+
modified = True
|
|
213
|
+
idx += 1
|
|
214
|
+
break
|
|
182
215
|
idx += 1
|
|
183
216
|
return modified
|
|
184
217
|
|