@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
package/runner/refinement.py
CHANGED
|
@@ -18,6 +18,7 @@ V(c_i) has never been what the runtime computes. Tier weighting becomes an
|
|
|
18
18
|
opt-in constitution — nothing flips without an explicit pack.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
+
import math
|
|
21
22
|
from dataclasses import dataclass, field
|
|
22
23
|
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
23
24
|
|
|
@@ -154,6 +155,59 @@ def claim_verdict(
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
|
|
158
|
+
def _check_verified_and_inferred(c: ClaimWitness, hasher: Any, out: List[Violation]) -> None:
|
|
159
|
+
if c.tag == TAG_VERIFIED or c.kind == KIND_CLAIM:
|
|
160
|
+
if not c.source_hash:
|
|
161
|
+
out.append(Violation(c.claim_id, "VERIFIED_REQUIRES_HASH", "source_hash missing"))
|
|
162
|
+
if not c.verbatim_quote:
|
|
163
|
+
out.append(Violation(c.claim_id, "VERIFIED_REQUIRES_QUOTE", "verbatim_quote missing"))
|
|
164
|
+
if hasher is not None and c.source_hash and c.verbatim_quote:
|
|
165
|
+
passed, _conf, msg = witness_check(c, hasher)
|
|
166
|
+
if not passed:
|
|
167
|
+
out.append(Violation(c.claim_id, "WITNESS_CHECK_FAILED", msg or "quote not in source"))
|
|
168
|
+
|
|
169
|
+
if c.tag == TAG_INFERRED or c.kind == KIND_INFERENCE:
|
|
170
|
+
if not c.parent_claims:
|
|
171
|
+
out.append(Violation(c.claim_id, "INFERRED_REQUIRES_PARENTS", "parent_claims empty"))
|
|
172
|
+
if not c.deductive_logic:
|
|
173
|
+
out.append(Violation(c.claim_id, "INFERRED_REQUIRES_LOGIC", "deductive_logic missing"))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _check_hypotheses_and_neg_knowledge(c: ClaimWitness, out: List[Violation]) -> None:
|
|
177
|
+
if (c.tag == TAG_HYPOTHESIS or c.kind == KIND_HYPOTHESIS) and not c.falsification:
|
|
178
|
+
out.append(Violation(c.claim_id, "HYPOTHESIS_REQUIRES_FALSIFICATION", "falsification missing"))
|
|
179
|
+
|
|
180
|
+
if c.tag == TAG_NEGATIVE_KNOWLEDGE or c.kind == KIND_NEGATIVE_KNOWLEDGE:
|
|
181
|
+
if not c.query:
|
|
182
|
+
out.append(Violation(c.claim_id, "NEG_KNOWLEDGE_REQUIRES_QUERY", "query missing"))
|
|
183
|
+
if not c.finding:
|
|
184
|
+
out.append(Violation(c.claim_id, "NEG_KNOWLEDGE_REQUIRES_FINDING", "finding missing"))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _check_constitution_and_parents(
|
|
188
|
+
c: ClaimWitness,
|
|
189
|
+
known_ids: set,
|
|
190
|
+
constitution: Constitution,
|
|
191
|
+
out: List[Violation],
|
|
192
|
+
) -> None:
|
|
193
|
+
for parent in c.parent_claims:
|
|
194
|
+
if parent not in known_ids:
|
|
195
|
+
out.append(
|
|
196
|
+
Violation(c.claim_id, "PARENT_UNRESOLVED", f"parent_claims '{parent}' not in dossier set")
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if constitution.mandatory_tags and not c.tag:
|
|
200
|
+
out.append(Violation(c.claim_id, "MISSING_TAG", "tag required by constitution"))
|
|
201
|
+
|
|
202
|
+
if constitution.banned_domains and c.source_url:
|
|
203
|
+
src_lower = c.source_url.lower()
|
|
204
|
+
for dom in constitution.banned_domains:
|
|
205
|
+
if dom.lower() in src_lower:
|
|
206
|
+
out.append(
|
|
207
|
+
Violation(c.claim_id, "BANNED_DOMAIN", f"source_url on banned domain '{dom}'")
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
157
211
|
def check_invariants(
|
|
158
212
|
claims: Iterable[ClaimWitness],
|
|
159
213
|
hasher: Any = None,
|
|
@@ -165,54 +219,14 @@ def check_invariants(
|
|
|
165
219
|
VERIFIED claims; without it, only structural rules are enforced.
|
|
166
220
|
"""
|
|
167
221
|
constitution = constitution or LEGACY_CONSTITUTION
|
|
168
|
-
|
|
169
|
-
known_ids = {c.claim_id for c in
|
|
222
|
+
claim_list = list(claims)
|
|
223
|
+
known_ids = {c.claim_id for c in claim_list}
|
|
170
224
|
out: List[Violation] = []
|
|
171
225
|
|
|
172
|
-
for c in
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
out.append(Violation(c.claim_id, "VERIFIED_REQUIRES_HASH", "source_hash missing"))
|
|
177
|
-
if not c.verbatim_quote:
|
|
178
|
-
out.append(Violation(c.claim_id, "VERIFIED_REQUIRES_QUOTE", "verbatim_quote missing"))
|
|
179
|
-
if hasher is not None and c.source_hash and c.verbatim_quote:
|
|
180
|
-
passed, _conf, msg = witness_check(c, hasher)
|
|
181
|
-
if not passed:
|
|
182
|
-
out.append(Violation(c.claim_id, "WITNESS_CHECK_FAILED", msg or "quote not in source"))
|
|
183
|
-
|
|
184
|
-
if c.tag == TAG_INFERRED or c.kind == KIND_INFERENCE:
|
|
185
|
-
if not c.parent_claims:
|
|
186
|
-
out.append(Violation(c.claim_id, "INFERRED_REQUIRES_PARENTS", "parent_claims empty"))
|
|
187
|
-
if not c.deductive_logic:
|
|
188
|
-
out.append(Violation(c.claim_id, "INFERRED_REQUIRES_LOGIC", "deductive_logic missing"))
|
|
189
|
-
|
|
190
|
-
if c.tag == TAG_HYPOTHESIS or c.kind == KIND_HYPOTHESIS:
|
|
191
|
-
if not c.falsification:
|
|
192
|
-
out.append(Violation(c.claim_id, "HYPOTHESIS_REQUIRES_FALSIFICATION", "falsification missing"))
|
|
193
|
-
|
|
194
|
-
if c.tag == TAG_NEGATIVE_KNOWLEDGE or c.kind == KIND_NEGATIVE_KNOWLEDGE:
|
|
195
|
-
if not c.query:
|
|
196
|
-
out.append(Violation(c.claim_id, "NEG_KNOWLEDGE_REQUIRES_QUERY", "query missing"))
|
|
197
|
-
if not c.finding:
|
|
198
|
-
out.append(Violation(c.claim_id, "NEG_KNOWLEDGE_REQUIRES_FINDING", "finding missing"))
|
|
199
|
-
|
|
200
|
-
# --- cross-reference resolution ---
|
|
201
|
-
for parent in c.parent_claims:
|
|
202
|
-
if parent not in known_ids:
|
|
203
|
-
out.append(
|
|
204
|
-
Violation(c.claim_id, "PARENT_UNRESOLVED", f"parent_claims '{parent}' not in dossier set")
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
# --- domain-pack rules ---
|
|
208
|
-
if constitution.mandatory_tags and not c.tag:
|
|
209
|
-
out.append(Violation(c.claim_id, "MISSING_TAG", "tag required by constitution"))
|
|
210
|
-
if constitution.banned_domains and c.source_url:
|
|
211
|
-
for dom in constitution.banned_domains:
|
|
212
|
-
if dom.lower() in c.source_url.lower():
|
|
213
|
-
out.append(
|
|
214
|
-
Violation(c.claim_id, "BANNED_DOMAIN", f"source_url on banned domain '{dom}'")
|
|
215
|
-
)
|
|
226
|
+
for c in claim_list:
|
|
227
|
+
_check_verified_and_inferred(c, hasher, out)
|
|
228
|
+
_check_hypotheses_and_neg_knowledge(c, out)
|
|
229
|
+
_check_constitution_and_parents(c, known_ids, constitution, out)
|
|
216
230
|
|
|
217
231
|
return out
|
|
218
232
|
|
|
@@ -294,7 +308,7 @@ def compute_epistemic_score_from_claims(
|
|
|
294
308
|
}
|
|
295
309
|
weight_sum = 0.0
|
|
296
310
|
tier_weighted = any(
|
|
297
|
-
w
|
|
311
|
+
not math.isclose(w, 1.0, rel_tol=1e-7) for w in constitution.tier_weights.values()
|
|
298
312
|
)
|
|
299
313
|
|
|
300
314
|
for c in claims:
|
package/runner/research_swarm.py
CHANGED
|
@@ -26,6 +26,47 @@ from runner.auditor_engine import EpistemicAuditorEngine
|
|
|
26
26
|
from skills.research_cache.hasher import SourceHasher
|
|
27
27
|
from skills.swarm_config.configure import load_config
|
|
28
28
|
|
|
29
|
+
EXAMPLE_RUST_RAFT_URL = "https://github.com/example/rust-raft"
|
|
30
|
+
# Common backends we can resolve without touching PATH. This is a convenience
|
|
31
|
+
# fast-path, NOT an allowlist: backend resolution is a documented extension
|
|
32
|
+
# point (CLI > IUMBTEMS_BACKEND_* > config), so any executable the OS can
|
|
33
|
+
# resolve must work. The security property is argv-list + shell=False, which
|
|
34
|
+
# run_claude_process already enforces; injection is not possible here.
|
|
35
|
+
KNOWN_BACKEND_BINARIES = {"claude", "opencode", "python", "python3", "node"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _validate_backend(backend: List[str]) -> List[str]:
|
|
39
|
+
"""Resolve-check a backend argv. Raises ValueError only if nothing can run it.
|
|
40
|
+
|
|
41
|
+
Resolution mirrors what subprocess does with shell=False: an absolute/relative
|
|
42
|
+
path is taken as-is (checked against the spawn cwd, PROJECT_ROOT), otherwise
|
|
43
|
+
the name is looked up on PATH. Previously this rejected anything outside a
|
|
44
|
+
five-name allowlist, which broke `python3.11`, `aider`, `codex exec`,
|
|
45
|
+
`npx -y ...` and any config-supplied backend.
|
|
46
|
+
"""
|
|
47
|
+
if not backend:
|
|
48
|
+
return ["claude", "-p"]
|
|
49
|
+
candidate = backend[0]
|
|
50
|
+
bin_name = Path(candidate).name
|
|
51
|
+
if bin_name in KNOWN_BACKEND_BINARIES:
|
|
52
|
+
return list(backend)
|
|
53
|
+
# Path form: resolve the same way the spawn will (cwd=PROJECT_ROOT).
|
|
54
|
+
if os.sep in candidate or (os.altsep and os.altsep in candidate):
|
|
55
|
+
resolved = (PROJECT_ROOT / candidate).resolve() if not os.path.isabs(candidate) else Path(candidate)
|
|
56
|
+
if resolved.is_file():
|
|
57
|
+
return list(backend)
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"Backend binary not found at {candidate!r} (resolved {resolved}); "
|
|
60
|
+
f"expected an executable file relative to {PROJECT_ROOT}"
|
|
61
|
+
)
|
|
62
|
+
# Bare name: ask PATH, exactly as subprocess will.
|
|
63
|
+
if shutil.which(candidate) is None:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"Backend binary {candidate!r} is not on PATH and is not a known backend "
|
|
66
|
+
f"({', '.join(sorted(KNOWN_BACKEND_BINARIES))})"
|
|
67
|
+
)
|
|
68
|
+
return list(backend)
|
|
69
|
+
|
|
29
70
|
|
|
30
71
|
class SwarmRunner:
|
|
31
72
|
def __init__(
|
|
@@ -115,13 +156,26 @@ class SwarmRunner:
|
|
|
115
156
|
cmd = self.build_agent_cmd(prompt, system_prompt_file, tools, role=role)
|
|
116
157
|
|
|
117
158
|
try:
|
|
159
|
+
if cmd:
|
|
160
|
+
_validate_backend([cmd[0]])
|
|
118
161
|
res = subprocess.run(
|
|
119
|
-
cmd,
|
|
162
|
+
cmd,
|
|
163
|
+
capture_output=True,
|
|
164
|
+
text=True,
|
|
165
|
+
check=True,
|
|
166
|
+
cwd=str(PROJECT_ROOT),
|
|
167
|
+
shell=False,
|
|
120
168
|
)
|
|
121
169
|
return res.stdout.strip()
|
|
122
170
|
except subprocess.CalledProcessError as e:
|
|
123
171
|
print(f"[ERROR] Claude process failed: {e.stderr}", file=sys.stderr)
|
|
124
172
|
raise RuntimeError(f"Claude execution failed: {e.stderr}")
|
|
173
|
+
except ValueError as e:
|
|
174
|
+
# Bad backend config: report it as a run failure, not an abort of the
|
|
175
|
+
# whole swarm. Matches the pre-allowlist behavior of surfacing the
|
|
176
|
+
# error from inside the try.
|
|
177
|
+
print(f"[ERROR] Invalid agent backend: {e}", file=sys.stderr)
|
|
178
|
+
raise RuntimeError(f"Invalid agent backend: {e}")
|
|
125
179
|
|
|
126
180
|
def _mock_claude_response(self, prompt: str) -> str:
|
|
127
181
|
"""Mock response generator for unit testing without live API keys."""
|
|
@@ -243,7 +297,7 @@ Output ONLY valid JSON representing the scope decomposition conforming to prompt
|
|
|
243
297
|
elif self.mode == "scout":
|
|
244
298
|
sample_content = "# High Performance Raft in Rust\nZero-dependency Raft implementation with 150k ops/sec throughput under Apache-2.0."
|
|
245
299
|
shash = self.hasher.store_source(
|
|
246
|
-
|
|
300
|
+
EXAMPLE_RUST_RAFT_URL, sample_content, "Rust Raft"
|
|
247
301
|
)
|
|
248
302
|
dossier = {
|
|
249
303
|
"agent": "Agent Alpha (OSS Scout)",
|
|
@@ -256,7 +310,7 @@ Output ONLY valid JSON representing the scope decomposition conforming to prompt
|
|
|
256
310
|
"tag": "VERIFIED",
|
|
257
311
|
"statement": "Rust-Raft achieves 150k ops/sec with zero external dependencies.",
|
|
258
312
|
"source_hash": shash,
|
|
259
|
-
"source_url":
|
|
313
|
+
"source_url": EXAMPLE_RUST_RAFT_URL,
|
|
260
314
|
"verbatim_quote": "Zero-dependency Raft implementation with 150k ops/sec throughput under Apache-2.0.",
|
|
261
315
|
}
|
|
262
316
|
],
|
|
@@ -390,7 +444,7 @@ Output ONLY valid JSON representing the scope decomposition conforming to prompt
|
|
|
390
444
|
elif self.mode == "scout":
|
|
391
445
|
sample_content = "# High Performance Raft in Rust\nZero-dependency Raft implementation with 150k ops/sec throughput under Apache-2.0."
|
|
392
446
|
shash = self.hasher.store_source(
|
|
393
|
-
|
|
447
|
+
EXAMPLE_RUST_RAFT_URL, sample_content, "Rust Raft"
|
|
394
448
|
)
|
|
395
449
|
dossier = {
|
|
396
450
|
"agent": "Agent Beta (OSS Red Team)",
|
|
@@ -403,7 +457,7 @@ Output ONLY valid JSON representing the scope decomposition conforming to prompt
|
|
|
403
457
|
"tag": "VERIFIED",
|
|
404
458
|
"statement": "Candidate repository has single maintainer with 9-month lull in commit history.",
|
|
405
459
|
"source_hash": shash,
|
|
406
|
-
"source_url":
|
|
460
|
+
"source_url": EXAMPLE_RUST_RAFT_URL,
|
|
407
461
|
"verbatim_quote": "Zero-dependency Raft implementation with 150k ops/sec throughput under Apache-2.0.",
|
|
408
462
|
"severity": "LOW",
|
|
409
463
|
}
|
|
@@ -550,7 +604,7 @@ Output ONLY valid JSON representing the scope decomposition conforming to prompt
|
|
|
550
604
|
print("=" * 70)
|
|
551
605
|
|
|
552
606
|
# 1. Orchestrate
|
|
553
|
-
|
|
607
|
+
self.orchestrate_objective(objective, frontier_file)
|
|
554
608
|
|
|
555
609
|
# 2. Execute scopes according to DAG
|
|
556
610
|
while True:
|
|
@@ -659,7 +713,7 @@ Output ONLY valid JSON representing the scope decomposition conforming to prompt
|
|
|
659
713
|
synthesis_lines.append("\n---\n")
|
|
660
714
|
|
|
661
715
|
avg_div = round(sum(all_divergences) / max(1, len(all_divergences)), 2)
|
|
662
|
-
synthesis_lines.append(
|
|
716
|
+
synthesis_lines.append("\n## Swarm Epistemic Audit Totals\n")
|
|
663
717
|
synthesis_lines.append(
|
|
664
718
|
f"- **Total Verified Primary Citations**: `{total_verified}`"
|
|
665
719
|
)
|
package/runner/state_machine.py
CHANGED
|
@@ -34,12 +34,15 @@ class ScopeStatus(str, Enum):
|
|
|
34
34
|
COMPLETE = "COMPLETE"
|
|
35
35
|
FAILED = "FAILED"
|
|
36
36
|
|
|
37
|
+
MANIFEST_FILENAME = "manifest.json"
|
|
38
|
+
|
|
39
|
+
|
|
37
40
|
class ResearchStateMachine:
|
|
38
41
|
def __init__(self, base_dir: Optional[Path] = None):
|
|
39
|
-
self.base_dir = base_dir or
|
|
42
|
+
self.base_dir = Path(os.path.realpath(str(base_dir or ".research")))
|
|
40
43
|
self.scratchpads_dir = self.base_dir / "scratchpads"
|
|
41
44
|
self.sources_dir = self.base_dir / "sources"
|
|
42
|
-
self.manifest_file = self.base_dir /
|
|
45
|
+
self.manifest_file = self.base_dir / MANIFEST_FILENAME
|
|
43
46
|
self._lock = threading.Lock()
|
|
44
47
|
|
|
45
48
|
# Ensure directories exist
|
|
@@ -112,17 +115,17 @@ class ResearchStateMachine:
|
|
|
112
115
|
"audit_completed": False,
|
|
113
116
|
"created_at": datetime.now(timezone.utc).isoformat()
|
|
114
117
|
}
|
|
115
|
-
temp_scope_file = (scope_dir /
|
|
118
|
+
temp_scope_file = (scope_dir / MANIFEST_FILENAME).with_suffix(".tmp")
|
|
116
119
|
with open(temp_scope_file, "w", encoding="utf-8") as f:
|
|
117
120
|
json.dump(scope_manifest, f, indent=2)
|
|
118
|
-
os.replace(temp_scope_file, scope_dir /
|
|
121
|
+
os.replace(temp_scope_file, scope_dir / MANIFEST_FILENAME)
|
|
119
122
|
|
|
120
123
|
def get_scope_dir(self, scope_id: str) -> Path:
|
|
121
124
|
return self.scratchpads_dir / scope_id
|
|
122
125
|
|
|
123
126
|
def load_scope_manifest(self, scope_id: str) -> Dict[str, Any]:
|
|
124
127
|
with self._lock:
|
|
125
|
-
scope_manifest_file = self.get_scope_dir(scope_id) /
|
|
128
|
+
scope_manifest_file = self.get_scope_dir(scope_id) / MANIFEST_FILENAME
|
|
126
129
|
if not scope_manifest_file.exists():
|
|
127
130
|
raise FileNotFoundError(f"Scope manifest not found for {scope_id}")
|
|
128
131
|
with open(scope_manifest_file, "r", encoding="utf-8") as f:
|
|
@@ -130,7 +133,7 @@ class ResearchStateMachine:
|
|
|
130
133
|
|
|
131
134
|
def save_scope_manifest(self, scope_id: str, manifest: Dict[str, Any]):
|
|
132
135
|
with self._lock:
|
|
133
|
-
scope_manifest_file = self.get_scope_dir(scope_id) /
|
|
136
|
+
scope_manifest_file = self.get_scope_dir(scope_id) / MANIFEST_FILENAME
|
|
134
137
|
temp_path = scope_manifest_file.with_suffix(".tmp")
|
|
135
138
|
with open(temp_path, "w", encoding="utf-8") as f:
|
|
136
139
|
json.dump(manifest, f, indent=2)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -97,7 +97,6 @@ class TestMcpServerTools(unittest.TestCase):
|
|
|
97
97
|
with tempfile.TemporaryDirectory() as tmp:
|
|
98
98
|
tmp_path = Path(tmp)
|
|
99
99
|
# Seed a source so verify_quote has something real to check.
|
|
100
|
-
sys.path.insert(0, str(PROJECT_ROOT))
|
|
101
100
|
from skills.research_cache.hasher import SourceHasher
|
|
102
101
|
|
|
103
102
|
hasher = SourceHasher(tmp_path)
|