@heretek-ai/epistemic-swarm 0.7.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/plugin.json +1 -1
- package/bin/cli.js +1 -0
- package/extensions/pi/index.js +7 -1
- package/package.json +1 -1
- package/plugins/darkharvest/skills/darkharvest/scripts/harvest.py +76 -39
- package/plugins/factory/skills/factory/scripts/factory.py +1 -1
- 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_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 +27 -18
- 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
|
@@ -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
|
|