@heretek-ai/epistemic-swarm 0.4.7 ā 0.4.8
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 +59 -7
- package/config/mcp_launcher.py +75 -48
- package/extensions/pi/index.js +25 -5
- package/install.sh +5 -5
- package/package.json +1 -1
- package/plugins/antigravity/plugin.json +1 -1
- package/plugins/gemini/gemini-extension.json +1 -1
- package/plugins/opencode/index.js +308 -142
- package/plugins/opencode/tui.js +7 -9
- package/plugins/research-cache/skills/research_cache/hasher.py +27 -9
- package/plugins/socratic-grilling/skills/grilling/socratic_tree.py +26 -18
- 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/auditor_engine.py +122 -102
- package/runner/claim_store.py +5 -4
- package/runner/living_dossiers.py +32 -10
- package/runner/mcp_protocol.py +1 -1
- package/runner/mcp_server.py +23 -11
- package/runner/path_safety.py +62 -0
- package/runner/pcrb.py +68 -39
- package/runner/pcrb_verify.py +82 -52
- package/runner/refinement.py +61 -47
- package/runner/research_swarm.py +61 -7
- package/runner/state_machine.py +9 -6
- package/runner/tests/__pycache__/test_auction_order.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_domain_packs.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_mcp_server.py +0 -1
- package/runner/tests/test_opencode_ux.py +368 -33
- package/runner/tests/test_swarm.py +20 -9
- package/runner/tests/test_sweep_regressions.py +373 -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__/divergence_experiment.cpython-311.pyc +0 -0
- package/scripts/auction_experiment.py +38 -30
- package/scripts/bet1_advisory_spike.py +41 -25
- package/scripts/build_adapters.py +52 -37
- package/scripts/divergence_experiment.py +61 -31
- package/skills/brainstorming/scripts/brainstorm.py +3 -2
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/epistemic_search/scripts/fetch.py +0 -1
- package/skills/epistemic_search/scripts/search.py +202 -73
- package/skills/epistemic_search/scripts/webcache.py +1 -1
- package/skills/grilling/socratic_tree.py +26 -18
- package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
- package/skills/research_cache/hasher.py +27 -9
- 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 +32 -28
|
@@ -5,30 +5,42 @@ Advisory mode ONLY: findings are formatted as PR comments, never block, and
|
|
|
5
5
|
the process always exits 0. Measures: seeded-defect hit rate (finding cites
|
|
6
6
|
seeded file + line within +/-3) and clean-fixture false-positive rate.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
The default workdir is a per-user scratch dir under the system temp dir
|
|
9
|
+
(`<tmpdir>/bet1_<uid>`), not the shared world-writable `/tmp/bet1` it used to
|
|
10
|
+
be. Pass --dir explicitly to override:
|
|
11
|
+
|
|
12
|
+
python3 scripts/bet1_advisory_spike.py --generate
|
|
13
|
+
python3 scripts/bet1_advisory_spike.py --run --mock
|
|
14
|
+
python3 scripts/bet1_advisory_spike.py --run --live --only bet1-01,bet1-02
|
|
15
|
+
python3 scripts/bet1_advisory_spike.py --score
|
|
12
16
|
|
|
13
17
|
Live runs invoke real audits (LLM spend); mock runs exercise dispatch only.
|
|
14
18
|
"""
|
|
15
19
|
|
|
16
20
|
import argparse
|
|
17
21
|
import json
|
|
22
|
+
import os
|
|
18
23
|
import re
|
|
19
24
|
import subprocess
|
|
20
25
|
import sys
|
|
26
|
+
import tempfile
|
|
21
27
|
from pathlib import Path
|
|
22
28
|
|
|
23
29
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
24
30
|
LINE_TOLERANCE = 3
|
|
25
31
|
|
|
32
|
+
IMPORT_OS = "import os"
|
|
33
|
+
IMPORT_SQLITE3 = "import sqlite3"
|
|
34
|
+
IMPORT_THREADING = "import threading"
|
|
35
|
+
SQLITE_CONNECT_DB = " conn = sqlite3.connect(db)"
|
|
36
|
+
MANIFEST_FILENAME = "manifest.json"
|
|
37
|
+
|
|
26
38
|
# name -> (filename, lines, seeded_line_1based, description)
|
|
27
39
|
FIXTURES = {
|
|
28
40
|
"hardcoded-secret-1": (
|
|
29
41
|
"auth.py",
|
|
30
42
|
[
|
|
31
|
-
'API_KEY = "
|
|
43
|
+
'API_KEY = "dummy-test-key-51H7x9Q2mZ4v8K3n" # production key',
|
|
32
44
|
"",
|
|
33
45
|
"def headers():",
|
|
34
46
|
' return {"Authorization": f"Bearer {API_KEY}"}',
|
|
@@ -39,7 +51,7 @@ FIXTURES = {
|
|
|
39
51
|
"hardcoded-secret-2": (
|
|
40
52
|
"config.py",
|
|
41
53
|
[
|
|
42
|
-
|
|
54
|
+
IMPORT_OS,
|
|
43
55
|
"",
|
|
44
56
|
'DB_PASSWORD = "P@ssw0rd-2024-prod"',
|
|
45
57
|
"",
|
|
@@ -52,10 +64,10 @@ FIXTURES = {
|
|
|
52
64
|
"sqli-concat-1": (
|
|
53
65
|
"users.py",
|
|
54
66
|
[
|
|
55
|
-
|
|
67
|
+
IMPORT_SQLITE3,
|
|
56
68
|
"",
|
|
57
69
|
"def get_user(db, username):",
|
|
58
|
-
|
|
70
|
+
SQLITE_CONNECT_DB,
|
|
59
71
|
" cur = conn.cursor()",
|
|
60
72
|
' query = "SELECT * FROM users WHERE name = \'" + username + "\'"',
|
|
61
73
|
" cur.execute(query)",
|
|
@@ -67,10 +79,10 @@ FIXTURES = {
|
|
|
67
79
|
"sqli-concat-2": (
|
|
68
80
|
"orders.py",
|
|
69
81
|
[
|
|
70
|
-
|
|
82
|
+
IMPORT_SQLITE3,
|
|
71
83
|
"",
|
|
72
84
|
"def get_orders(db, status, limit):",
|
|
73
|
-
|
|
85
|
+
SQLITE_CONNECT_DB,
|
|
74
86
|
" sql = f\"SELECT * FROM orders WHERE status='{status}' LIMIT {limit}\"",
|
|
75
87
|
" return conn.execute(sql).fetchall()",
|
|
76
88
|
],
|
|
@@ -80,7 +92,7 @@ FIXTURES = {
|
|
|
80
92
|
"race-counter-1": (
|
|
81
93
|
"counter.py",
|
|
82
94
|
[
|
|
83
|
-
|
|
95
|
+
IMPORT_THREADING,
|
|
84
96
|
"",
|
|
85
97
|
"total = 0",
|
|
86
98
|
"",
|
|
@@ -97,7 +109,7 @@ FIXTURES = {
|
|
|
97
109
|
"race-counter-2": (
|
|
98
110
|
"cache.py",
|
|
99
111
|
[
|
|
100
|
-
|
|
112
|
+
IMPORT_THREADING,
|
|
101
113
|
"",
|
|
102
114
|
"_cache = {}",
|
|
103
115
|
"",
|
|
@@ -205,7 +217,7 @@ FIXTURES = {
|
|
|
205
217
|
"path-traversal-1": (
|
|
206
218
|
"files.py",
|
|
207
219
|
[
|
|
208
|
-
|
|
220
|
+
IMPORT_OS,
|
|
209
221
|
"",
|
|
210
222
|
'BASE = "/srv/uploads"',
|
|
211
223
|
"",
|
|
@@ -219,7 +231,7 @@ FIXTURES = {
|
|
|
219
231
|
"path-traversal-2": (
|
|
220
232
|
"export.py",
|
|
221
233
|
[
|
|
222
|
-
|
|
234
|
+
IMPORT_OS,
|
|
223
235
|
"import shutil",
|
|
224
236
|
"",
|
|
225
237
|
"def export_report(name, dest_dir):",
|
|
@@ -243,10 +255,10 @@ CLEAN_FIXTURES = {
|
|
|
243
255
|
"clean-2": (
|
|
244
256
|
"queries.py",
|
|
245
257
|
[
|
|
246
|
-
|
|
258
|
+
IMPORT_SQLITE3,
|
|
247
259
|
"",
|
|
248
260
|
"def get_user(db, username):",
|
|
249
|
-
|
|
261
|
+
SQLITE_CONNECT_DB,
|
|
250
262
|
" return conn.execute(",
|
|
251
263
|
' "SELECT * FROM users WHERE name = ?", (username,)',
|
|
252
264
|
" ).fetchall()",
|
|
@@ -269,7 +281,7 @@ CLEAN_FIXTURES = {
|
|
|
269
281
|
"clean-4": (
|
|
270
282
|
"counter_safe.py",
|
|
271
283
|
[
|
|
272
|
-
|
|
284
|
+
IMPORT_THREADING,
|
|
273
285
|
"",
|
|
274
286
|
"_lock = threading.Lock()",
|
|
275
287
|
"total = 0",
|
|
@@ -285,7 +297,7 @@ CLEAN_FIXTURES = {
|
|
|
285
297
|
|
|
286
298
|
|
|
287
299
|
def generate(root):
|
|
288
|
-
root = Path(root)
|
|
300
|
+
root = Path(os.path.realpath(str(root)))
|
|
289
301
|
manifest = []
|
|
290
302
|
for fid, (fname, lines, line, desc) in FIXTURES.items():
|
|
291
303
|
d = root / fid
|
|
@@ -315,7 +327,7 @@ def generate(root):
|
|
|
315
327
|
"clean": True,
|
|
316
328
|
}
|
|
317
329
|
)
|
|
318
|
-
root.joinpath(
|
|
330
|
+
root.joinpath(MANIFEST_FILENAME).write_text(
|
|
319
331
|
json.dumps(manifest, indent=2), encoding="utf-8"
|
|
320
332
|
)
|
|
321
333
|
print(f"generated {len(manifest)} fixtures in {root}")
|
|
@@ -324,7 +336,8 @@ def generate(root):
|
|
|
324
336
|
|
|
325
337
|
def run_audit(target_dir, mock, timeout=600):
|
|
326
338
|
"""Invoke the real dispatch path. Advisory: findings only, never blocks."""
|
|
327
|
-
|
|
339
|
+
clean_target = os.path.realpath(str(target_dir))
|
|
340
|
+
args = {"target": clean_target}
|
|
328
341
|
if mock:
|
|
329
342
|
args["mock_mode"] = True
|
|
330
343
|
proc = subprocess.run(
|
|
@@ -339,6 +352,7 @@ def run_audit(target_dir, mock, timeout=600):
|
|
|
339
352
|
text=True,
|
|
340
353
|
timeout=timeout,
|
|
341
354
|
cwd=str(PROJECT_ROOT),
|
|
355
|
+
shell=False,
|
|
342
356
|
)
|
|
343
357
|
return proc.stdout + proc.stderr
|
|
344
358
|
|
|
@@ -367,10 +381,11 @@ def score_fixture(entry, findings):
|
|
|
367
381
|
|
|
368
382
|
|
|
369
383
|
def score_all(root):
|
|
370
|
-
|
|
384
|
+
canonical_root = Path(os.path.realpath(str(root)))
|
|
385
|
+
manifest = json.loads((canonical_root / MANIFEST_FILENAME).read_text(encoding="utf-8"))
|
|
371
386
|
results = []
|
|
372
387
|
for entry in manifest:
|
|
373
|
-
comment_file =
|
|
388
|
+
comment_file = canonical_root / entry["id"] / "advisory.md"
|
|
374
389
|
findings = (
|
|
375
390
|
comment_file.read_text(encoding="utf-8") if comment_file.exists() else ""
|
|
376
391
|
)
|
|
@@ -393,7 +408,8 @@ def score_all(root):
|
|
|
393
408
|
|
|
394
409
|
def main(argv=None):
|
|
395
410
|
ap = argparse.ArgumentParser()
|
|
396
|
-
|
|
411
|
+
default_dir = os.path.join(tempfile.gettempdir(), f"bet1_{os.getuid() if hasattr(os, 'getuid') else 'user'}")
|
|
412
|
+
ap.add_argument("--dir", default=default_dir)
|
|
397
413
|
ap.add_argument("--generate", action="store_true")
|
|
398
414
|
ap.add_argument("--run", action="store_true")
|
|
399
415
|
ap.add_argument("--score", action="store_true")
|
|
@@ -401,12 +417,12 @@ def main(argv=None):
|
|
|
401
417
|
ap.add_argument("--live", action="store_true")
|
|
402
418
|
ap.add_argument("--only", default="")
|
|
403
419
|
args = ap.parse_args(argv)
|
|
404
|
-
root = Path(args.dir)
|
|
420
|
+
root = Path(os.path.realpath(str(args.dir)))
|
|
405
421
|
|
|
406
422
|
if args.generate or (not args.run and not args.score):
|
|
407
423
|
generate(root)
|
|
408
424
|
if args.run:
|
|
409
|
-
manifest = json.loads((root /
|
|
425
|
+
manifest = json.loads((root / MANIFEST_FILENAME).read_text(encoding="utf-8"))
|
|
410
426
|
only = {s.strip() for s in args.only.split(",") if s.strip()}
|
|
411
427
|
for entry in manifest:
|
|
412
428
|
if only and entry["id"] not in only:
|
|
@@ -28,6 +28,7 @@ import json
|
|
|
28
28
|
import shutil
|
|
29
29
|
import sys
|
|
30
30
|
from pathlib import Path
|
|
31
|
+
from typing import List
|
|
31
32
|
|
|
32
33
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
33
34
|
CANONICAL_SKILLS = [
|
|
@@ -85,6 +86,9 @@ Epistemic rules apply regardless of harness: tag claims as
|
|
|
85
86
|
"""
|
|
86
87
|
|
|
87
88
|
|
|
89
|
+
SKILL_FILENAME = "SKILL.md"
|
|
90
|
+
|
|
91
|
+
|
|
88
92
|
def render_stub(skill: str) -> str:
|
|
89
93
|
tools = ", ".join(f"`{t}`" for t in SKILL_TOOLS.get(skill, []))
|
|
90
94
|
return STUB_TEMPLATE.format(
|
|
@@ -94,62 +98,73 @@ def render_stub(skill: str) -> str:
|
|
|
94
98
|
)
|
|
95
99
|
|
|
96
100
|
|
|
101
|
+
def _build_skill_stub(target_base: Path, skill: str) -> bool:
|
|
102
|
+
src = PROJECT_ROOT / "skills" / skill
|
|
103
|
+
if not src.exists():
|
|
104
|
+
print(f"[WARN] canonical skill missing: skills/{skill}", file=sys.stderr)
|
|
105
|
+
return False
|
|
106
|
+
if not (src / SKILL_FILENAME).exists():
|
|
107
|
+
print(f"[WARN] skills/{skill}/{SKILL_FILENAME} missing", file=sys.stderr)
|
|
108
|
+
return False
|
|
109
|
+
dst = target_base / skill
|
|
110
|
+
if dst.exists():
|
|
111
|
+
shutil.rmtree(dst)
|
|
112
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
(dst / SKILL_FILENAME).write_text(render_stub(skill), encoding="utf-8")
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
|
|
97
117
|
def build():
|
|
98
118
|
count = 0
|
|
99
|
-
for target_rel,
|
|
119
|
+
for target_rel, _mode in TARGETS:
|
|
100
120
|
target_base = PROJECT_ROOT / target_rel
|
|
101
121
|
target_base.mkdir(parents=True, exist_ok=True)
|
|
102
122
|
for skill in CANONICAL_SKILLS:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
print(f"[WARN] canonical skill missing: skills/{skill}", file=sys.stderr)
|
|
106
|
-
continue
|
|
107
|
-
if not (src / "SKILL.md").exists():
|
|
108
|
-
print(f"[WARN] skills/{skill}/SKILL.md missing", file=sys.stderr)
|
|
109
|
-
continue
|
|
110
|
-
dst = target_base / skill
|
|
111
|
-
if dst.exists():
|
|
112
|
-
shutil.rmtree(dst)
|
|
113
|
-
dst.mkdir(parents=True, exist_ok=True)
|
|
114
|
-
(dst / "SKILL.md").write_text(render_stub(skill), encoding="utf-8")
|
|
115
|
-
count += 1
|
|
123
|
+
if _build_skill_stub(target_base, skill):
|
|
124
|
+
count += 1
|
|
116
125
|
print(f"ā
Built {count} thin skill stubs across {len(TARGETS)} targets.")
|
|
117
126
|
return 0
|
|
118
127
|
|
|
119
128
|
|
|
120
|
-
def
|
|
121
|
-
|
|
129
|
+
def _check_skill_stub(target_rel: str, skill: str) -> List[str]:
|
|
130
|
+
errors = []
|
|
131
|
+
dst = PROJECT_ROOT / target_rel / skill / SKILL_FILENAME
|
|
132
|
+
if not dst.exists():
|
|
133
|
+
return [f"MISSING stub: {target_rel}/{skill}/{SKILL_FILENAME}"]
|
|
134
|
+
if dst.read_text(encoding="utf-8") != render_stub(skill):
|
|
135
|
+
errors.append(f"OUT OF SYNC: {target_rel}/{skill}/{SKILL_FILENAME}")
|
|
136
|
+
stray = [
|
|
137
|
+
p.name
|
|
138
|
+
for p in (PROJECT_ROOT / target_rel / skill).iterdir()
|
|
139
|
+
if p.name != SKILL_FILENAME
|
|
140
|
+
]
|
|
141
|
+
if stray:
|
|
142
|
+
errors.append(f"STRAY FILES in {target_rel}/{skill}: {sorted(stray)}")
|
|
143
|
+
return errors
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _check_package_json() -> List[str]:
|
|
122
147
|
errors = []
|
|
123
|
-
for target_rel, mode in TARGETS:
|
|
124
|
-
for skill in CANONICAL_SKILLS:
|
|
125
|
-
dst = PROJECT_ROOT / target_rel / skill / "SKILL.md"
|
|
126
|
-
if not dst.exists():
|
|
127
|
-
errors.append(f"MISSING stub: {target_rel}/{skill}/SKILL.md")
|
|
128
|
-
continue
|
|
129
|
-
if dst.read_text(encoding="utf-8") != render_stub(skill):
|
|
130
|
-
errors.append(f"OUT OF SYNC: {target_rel}/{skill}/SKILL.md")
|
|
131
|
-
# Stubs must not drag code bodies back in.
|
|
132
|
-
stray = [
|
|
133
|
-
p.name
|
|
134
|
-
for p in (PROJECT_ROOT / target_rel / skill).iterdir()
|
|
135
|
-
if p.name != "SKILL.md"
|
|
136
|
-
]
|
|
137
|
-
if stray:
|
|
138
|
-
errors.append(f"STRAY FILES in {target_rel}/{skill}: {sorted(stray)}")
|
|
139
|
-
# Validate package.json pi/omp blocks resolve
|
|
140
148
|
pkg = json.loads((PROJECT_ROOT / "package.json").read_text())
|
|
141
149
|
for key in ("pi", "omp"):
|
|
142
150
|
block = pkg.get(key, {})
|
|
143
151
|
for kind in ("skills", "extensions", "prompts"):
|
|
144
152
|
for p in block.get(kind, []):
|
|
145
|
-
# glob patterns allowed in prompts
|
|
146
153
|
if "*" in p:
|
|
147
154
|
if not list(PROJECT_ROOT.glob(p.lstrip("./"))):
|
|
148
|
-
errors.append(
|
|
149
|
-
f"package.json {key}.{kind} glob matches nothing: {p}"
|
|
150
|
-
)
|
|
155
|
+
errors.append(f"package.json {key}.{kind} glob matches nothing: {p}")
|
|
151
156
|
elif not (PROJECT_ROOT / p.lstrip("./")).exists():
|
|
152
157
|
errors.append(f"package.json {key}.{kind} missing: {p}")
|
|
158
|
+
return errors
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def check():
|
|
162
|
+
"""Verify stubs match the generated template (not the skill bodies)."""
|
|
163
|
+
errors = []
|
|
164
|
+
for target_rel, _mode in TARGETS:
|
|
165
|
+
for skill in CANONICAL_SKILLS:
|
|
166
|
+
errors.extend(_check_skill_stub(target_rel, skill))
|
|
167
|
+
errors.extend(_check_package_json())
|
|
153
168
|
if errors:
|
|
154
169
|
print("ā Adapter check failed:")
|
|
155
170
|
for e in errors:
|
|
@@ -23,11 +23,12 @@ Usage:
|
|
|
23
23
|
|
|
24
24
|
import argparse
|
|
25
25
|
import json
|
|
26
|
+
import os
|
|
26
27
|
import sys
|
|
27
28
|
import time
|
|
28
29
|
from datetime import datetime, timezone
|
|
29
30
|
from pathlib import Path
|
|
30
|
-
from typing import Any, Dict, List, Optional
|
|
31
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
31
32
|
|
|
32
33
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
33
34
|
if str(PROJECT_ROOT) not in sys.path:
|
|
@@ -40,7 +41,8 @@ GATE_BAR = 0.15
|
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
def load_objectives(path: Path = FIXTURE) -> List[Dict[str, Any]]:
|
|
43
|
-
|
|
44
|
+
canonical_path = Path(os.path.realpath(str(path)))
|
|
45
|
+
with open(canonical_path, "r", encoding="utf-8") as f:
|
|
44
46
|
data = json.load(f)
|
|
45
47
|
return data["objectives"]
|
|
46
48
|
|
|
@@ -50,6 +52,42 @@ def estimate_tokens(text: str) -> int:
|
|
|
50
52
|
return max(1, len(text) // 4)
|
|
51
53
|
|
|
52
54
|
|
|
55
|
+
def _extract_audit_divergence(base_dir: Path) -> Tuple[Optional[float], Optional[bool]]:
|
|
56
|
+
"""Aggregate divergence across every scored audit in a run's scratchpads.
|
|
57
|
+
|
|
58
|
+
A swarm run writes one ``audit_report.json`` per scope, so a single scalar has
|
|
59
|
+
to be reduced from N scores. The reduction is the **mean** of all scores that
|
|
60
|
+
are present, which is deterministic and independent of scope-name ordering.
|
|
61
|
+
|
|
62
|
+
``verified_passed`` is True only when every scored audit reports True; it is
|
|
63
|
+
None when no audit reports it at all.
|
|
64
|
+
|
|
65
|
+
(Earlier revisions returned whichever audit happened to be visited ā first,
|
|
66
|
+
then briefly last ā so the reported number silently tracked directory sort
|
|
67
|
+
order. Neither is a meaningful rule; the mean is.)
|
|
68
|
+
"""
|
|
69
|
+
audit_paths = sorted((base_dir / "scratchpads").glob("*/audit_report.json"))
|
|
70
|
+
scores: List[float] = []
|
|
71
|
+
verified_flags: List[bool] = []
|
|
72
|
+
for ap in audit_paths:
|
|
73
|
+
try:
|
|
74
|
+
with open(ap, "r", encoding="utf-8") as f:
|
|
75
|
+
audit = json.load(f)
|
|
76
|
+
except (OSError, json.JSONDecodeError):
|
|
77
|
+
continue
|
|
78
|
+
summary = audit.get("summary", {})
|
|
79
|
+
if summary.get("divergence_score") is not None:
|
|
80
|
+
scores.append(float(summary["divergence_score"]))
|
|
81
|
+
if summary.get("verified_passed") is not None:
|
|
82
|
+
verified_flags.append(bool(summary["verified_passed"]))
|
|
83
|
+
|
|
84
|
+
if not scores:
|
|
85
|
+
return None, (None if not verified_flags else all(verified_flags))
|
|
86
|
+
divergence = sum(scores) / len(scores)
|
|
87
|
+
verified: Optional[int] = all(verified_flags) if verified_flags else None
|
|
88
|
+
return divergence, verified
|
|
89
|
+
|
|
90
|
+
|
|
53
91
|
def run_one(
|
|
54
92
|
objective: str,
|
|
55
93
|
base_dir: Path,
|
|
@@ -57,6 +95,8 @@ def run_one(
|
|
|
57
95
|
mock: bool,
|
|
58
96
|
) -> Dict[str, Any]:
|
|
59
97
|
"""Run one swarm under one config and pull divergence out of the audit."""
|
|
98
|
+
import contextlib, io
|
|
99
|
+
|
|
60
100
|
runner = SwarmRunner(
|
|
61
101
|
base_dir=base_dir,
|
|
62
102
|
mock_mode=mock,
|
|
@@ -64,39 +104,36 @@ def run_one(
|
|
|
64
104
|
agent_overrides=agent_overrides,
|
|
65
105
|
)
|
|
66
106
|
started = time.perf_counter()
|
|
67
|
-
# Suppress the swarm's chatty stdout; the artifact is what matters.
|
|
68
|
-
import contextlib, io
|
|
69
|
-
|
|
70
107
|
buf = io.StringIO()
|
|
71
108
|
with contextlib.redirect_stdout(buf):
|
|
72
109
|
runner.run_swarm(objective)
|
|
73
110
|
elapsed = time.perf_counter() - started
|
|
74
111
|
|
|
75
112
|
report_path = base_dir / "final_synthesis.md"
|
|
76
|
-
divergence =
|
|
77
|
-
verified = None
|
|
78
|
-
audit_paths = sorted((base_dir / "scratchpads").glob("*/audit_report.json"))
|
|
79
|
-
for ap in audit_paths:
|
|
80
|
-
try:
|
|
81
|
-
with open(ap, "r", encoding="utf-8") as f:
|
|
82
|
-
audit = json.load(f)
|
|
83
|
-
summary = audit.get("summary", {})
|
|
84
|
-
if summary.get("divergence_score") is not None:
|
|
85
|
-
divergence = summary["divergence_score"]
|
|
86
|
-
verified = summary.get("verified_passed")
|
|
87
|
-
except (OSError, json.JSONDecodeError):
|
|
88
|
-
continue
|
|
89
|
-
|
|
113
|
+
divergence, verified = _extract_audit_divergence(base_dir)
|
|
90
114
|
log = buf.getvalue()
|
|
91
115
|
return {
|
|
92
116
|
"divergence_score": divergence,
|
|
93
117
|
"verified_passed": verified,
|
|
94
118
|
"wall_clock_s": round(elapsed, 3),
|
|
95
|
-
"tokens_estimate": estimate_tokens(log),
|
|
119
|
+
"tokens_estimate": estimate_tokens(log),
|
|
96
120
|
"report": str(report_path) if report_path.exists() else None,
|
|
97
121
|
}
|
|
98
122
|
|
|
99
123
|
|
|
124
|
+
def _build_overrides(args: argparse.Namespace) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
125
|
+
same_family: Dict[str, Dict[str, Any]] = {}
|
|
126
|
+
cross_family: Dict[str, Dict[str, Any]] = {}
|
|
127
|
+
if args.model_alpha:
|
|
128
|
+
same_family.setdefault("alpha", {})["model"] = args.model_alpha
|
|
129
|
+
same_family.setdefault("beta", {})["model"] = args.model_alpha
|
|
130
|
+
if args.model_beta:
|
|
131
|
+
cross_family.setdefault("beta", {})["model"] = args.model_beta
|
|
132
|
+
if args.beta_backend:
|
|
133
|
+
cross_family.setdefault("beta", {})["backend"] = args.beta_backend
|
|
134
|
+
return same_family, cross_family
|
|
135
|
+
|
|
136
|
+
|
|
100
137
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
101
138
|
parser = argparse.ArgumentParser(description="Stream E divergence experiment (G2)")
|
|
102
139
|
parser.add_argument("--mock", action="store_true", help="Mock mode, zero API cost")
|
|
@@ -107,19 +144,12 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
107
144
|
parser.add_argument("--fixture", default=str(FIXTURE))
|
|
108
145
|
args = parser.parse_args(argv)
|
|
109
146
|
|
|
110
|
-
|
|
111
|
-
|
|
147
|
+
canonical_fixture = Path(os.path.realpath(str(args.fixture)))
|
|
148
|
+
objectives = load_objectives(canonical_fixture)
|
|
149
|
+
out_dir = Path(os.path.realpath(str(args.out_dir))) if args.out_dir else (PROJECT_ROOT / ".research" / "experiments")
|
|
112
150
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
113
151
|
|
|
114
|
-
same_family
|
|
115
|
-
cross_family: Dict[str, Dict[str, Any]] = {}
|
|
116
|
-
if args.model_alpha:
|
|
117
|
-
same_family.setdefault("alpha", {})["model"] = args.model_alpha
|
|
118
|
-
same_family.setdefault("beta", {})["model"] = args.model_alpha
|
|
119
|
-
if args.model_beta:
|
|
120
|
-
cross_family.setdefault("beta", {})["model"] = args.model_beta
|
|
121
|
-
if args.beta_backend:
|
|
122
|
-
cross_family.setdefault("beta", {})["backend"] = args.beta_backend
|
|
152
|
+
same_family, cross_family = _build_overrides(args)
|
|
123
153
|
|
|
124
154
|
results = []
|
|
125
155
|
for obj in objectives:
|
|
@@ -7,6 +7,7 @@ runner/research_swarm.py --mode brainstorm.
|
|
|
7
7
|
|
|
8
8
|
import argparse
|
|
9
9
|
import json
|
|
10
|
+
import os
|
|
10
11
|
import subprocess
|
|
11
12
|
import sys
|
|
12
13
|
from datetime import datetime, timezone
|
|
@@ -167,7 +168,7 @@ def main():
|
|
|
167
168
|
)
|
|
168
169
|
args = ap.parse_args()
|
|
169
170
|
|
|
170
|
-
model,
|
|
171
|
+
model, _ = build_domain_model(args.objective, full=not args.shallow)
|
|
171
172
|
|
|
172
173
|
if args.show_context or not args.export:
|
|
173
174
|
print(f"\nš§ [Brainstorm] Objective: {args.objective}")
|
|
@@ -190,7 +191,7 @@ def main():
|
|
|
190
191
|
)
|
|
191
192
|
|
|
192
193
|
if args.export:
|
|
193
|
-
out = Path(args.export)
|
|
194
|
+
out = Path(os.path.realpath(str(args.export)))
|
|
194
195
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
195
196
|
out.write_text(json.dumps(model, indent=2), encoding="utf-8")
|
|
196
197
|
print(f"ā
Domain model exported to {out}")
|
|
@@ -79,7 +79,6 @@ def fetch_and_cache(url: str, research_dir: str = ".research") -> str:
|
|
|
79
79
|
req = urllib.request.Request(url, headers=headers)
|
|
80
80
|
try:
|
|
81
81
|
with urllib.request.urlopen(req, timeout=20) as resp:
|
|
82
|
-
content_type = resp.headers.get("Content-Type", "")
|
|
83
82
|
raw_bytes = resp.read()
|
|
84
83
|
html = raw_bytes.decode("utf-8", errors="replace")
|
|
85
84
|
except Exception as e:
|