@heretek-ai/epistemic-swarm 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +58 -0
  3. package/LICENSE +126 -0
  4. package/README.md +130 -0
  5. package/bin/cli.js +119 -0
  6. package/config/claude-settings-patch.json +10 -0
  7. package/config/docker-compose.infra.yml +23 -0
  8. package/config/mcp-research-servers.json +26 -0
  9. package/config/searxng_mcp.py +133 -0
  10. package/install.sh +102 -0
  11. package/package.json +52 -0
  12. package/prompts/agent_alpha_thesis.md +70 -0
  13. package/prompts/agent_beta_antithesis.md +78 -0
  14. package/prompts/base_epistemic_system.md +50 -0
  15. package/prompts/epistemic_auditor.md +74 -0
  16. package/prompts/orchestrator.md +70 -0
  17. package/runner/__init__.py +0 -0
  18. package/runner/__pycache__/__init__.cpython-314.pyc +0 -0
  19. package/runner/__pycache__/auditor_engine.cpython-314.pyc +0 -0
  20. package/runner/__pycache__/research_swarm.cpython-314.pyc +0 -0
  21. package/runner/__pycache__/state_machine.cpython-314.pyc +0 -0
  22. package/runner/auditor_engine.py +222 -0
  23. package/runner/research_swarm.py +337 -0
  24. package/runner/state_machine.py +192 -0
  25. package/runner/tests/__pycache__/test_swarm.cpython-314.pyc +0 -0
  26. package/runner/tests/test_swarm.py +178 -0
  27. package/skills/grilling/SKILL.md +48 -0
  28. package/skills/grilling/__init__.py +0 -0
  29. package/skills/grilling/socratic_tree.py +148 -0
  30. package/skills/research-cache/SKILL.md +36 -0
  31. package/skills/research-cache/__init__.py +0 -0
  32. package/skills/research-cache/__pycache__/__init__.cpython-314.pyc +0 -0
  33. package/skills/research-cache/__pycache__/hasher.cpython-314.pyc +0 -0
  34. package/skills/research-cache/hasher.py +195 -0
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Algorithmic Epistemic Auditor Engine for Epistemic Swarm.
4
+ Verifies quote authenticity against content-addressed source cache,
5
+ calculates divergence metrics, prunes ungrounded claims, and generates synthesis reports.
6
+ """
7
+
8
+ import sys
9
+ import json
10
+ from pathlib import Path
11
+ from datetime import datetime, timezone
12
+ from typing import Dict, Any, List, Tuple, Optional
13
+
14
+ # Import SourceHasher from skills/research-cache
15
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
16
+ from skills.research_cache.hasher import SourceHasher
17
+ from runner.state_machine import ResearchStateMachine, ScopeStatus
18
+
19
+ class EpistemicAuditorEngine:
20
+ def __init__(self, base_dir: Optional[Path] = None):
21
+ self.base_dir = base_dir or Path(".research")
22
+ self.hasher = SourceHasher(base_dir=self.base_dir)
23
+ self.state_machine = ResearchStateMachine(base_dir=self.base_dir)
24
+
25
+ def audit_scope(self, scope_id: str) -> Dict[str, Any]:
26
+ """Runs the audit pipeline on a scope with ready dossiers."""
27
+ scope_dir = self.state_machine.get_scope_dir(scope_id)
28
+ alpha_file = scope_dir / "alpha_dossier.json"
29
+ beta_file = scope_dir / "beta_dossier.json"
30
+
31
+ if not alpha_file.exists() or not beta_file.exists():
32
+ raise FileNotFoundError(f"Both dossiers must exist to audit {scope_id}")
33
+
34
+ with open(alpha_file, "r", encoding="utf-8") as f:
35
+ alpha_dossier = json.load(f)
36
+ with open(beta_file, "r", encoding="utf-8") as f:
37
+ beta_dossier = json.load(f)
38
+
39
+ self.state_machine.update_scope_status(scope_id, ScopeStatus.AUDITING)
40
+
41
+ # 1. Audit Alpha Claims
42
+ alpha_results, alpha_verified, alpha_rejected = self._verify_claims(
43
+ alpha_dossier.get("affirmative_claims", [])
44
+ )
45
+
46
+ # 2. Audit Beta Claims
47
+ beta_results, beta_verified, beta_rejected = self._verify_claims(
48
+ beta_dossier.get("falsification_claims", [])
49
+ )
50
+
51
+ total_verified = alpha_verified + beta_verified
52
+ total_rejected = alpha_rejected + beta_rejected
53
+
54
+ # Negative knowledge counts
55
+ neg_knowledge_alpha = len(alpha_dossier.get("negative_knowledge", []))
56
+ neg_knowledge_beta = len(beta_dossier.get("negative_knowledge", []))
57
+ total_neg_knowledge = neg_knowledge_alpha + neg_knowledge_beta
58
+
59
+ total_inferred = len(alpha_dossier.get("inferred_implications", []))
60
+ total_hypotheses = len(beta_dossier.get("hypotheses", []))
61
+
62
+ # 3. Calculate Epistemic Score
63
+ total_assertions = max(1, total_verified + total_inferred + total_hypotheses + total_rejected)
64
+ raw_score = (1.0 * total_verified + 0.5 * total_neg_knowledge - 2.5 * total_rejected) / total_assertions
65
+ epistemic_score = max(0.0, min(1.0, round(raw_score, 3)))
66
+
67
+ # 4. Calculate Divergence Score
68
+ divergence_score, divergence_matrix = self._compute_divergence(alpha_dossier, beta_dossier)
69
+
70
+ # 5. Build Audit Report
71
+ audit_report = {
72
+ "auditor": "Epistemic Auditor Engine v1.0",
73
+ "scope_id": scope_id,
74
+ "audited_at": datetime.now(timezone.utc).isoformat(),
75
+ "summary": {
76
+ "total_claims_audited": len(alpha_results) + len(beta_results),
77
+ "verified_passed": total_verified,
78
+ "unverified_rejected": total_rejected,
79
+ "negative_knowledge_count": total_neg_knowledge,
80
+ "epistemic_score": epistemic_score,
81
+ "divergence_score": divergence_score,
82
+ "verdict": "CERTIFIED" if epistemic_score >= 0.65 else "WARNING_LOW_GROUNDING"
83
+ },
84
+ "alpha_claims_audit": alpha_results,
85
+ "beta_claims_audit": beta_results,
86
+ "divergence_matrix": divergence_matrix
87
+ }
88
+
89
+ # Save audit_report.json
90
+ with open(scope_dir / "audit_report.json", "w", encoding="utf-8") as f:
91
+ json.dump(audit_report, f, indent=2)
92
+
93
+ # 6. Generate Synthesis Markdown
94
+ synthesis_md = self._generate_synthesis_markdown(
95
+ scope_id, alpha_dossier, beta_dossier, audit_report
96
+ )
97
+ with open(scope_dir / "scope_synthesis.md", "w", encoding="utf-8") as f:
98
+ f.write(synthesis_md)
99
+
100
+ # Mark scope completed
101
+ sm = self.state_machine.load_scope_manifest(scope_id)
102
+ sm["status"] = ScopeStatus.COMPLETE.value
103
+ sm["audit_completed"] = True
104
+ self.state_machine.save_scope_manifest(scope_id, sm)
105
+
106
+ return audit_report
107
+
108
+ def _verify_claims(self, claims: List[Dict[str, Any]]) -> Tuple[List[Dict[str, Any]], int, int]:
109
+ audited_claims = []
110
+ verified_count = 0
111
+ rejected_count = 0
112
+
113
+ for claim in claims:
114
+ cid = claim.get("claim_id", "UNKNOWN")
115
+ shash = claim.get("source_hash", "")
116
+ quote = claim.get("verbatim_quote", "")
117
+ statement = claim.get("statement", "")
118
+
119
+ if not shash or not quote:
120
+ audited_claims.append({
121
+ "claim_id": cid,
122
+ "statement": statement,
123
+ "original_tag": claim.get("tag", "VERIFIED"),
124
+ "audited_tag": "UNVERIFIED_REJECTED",
125
+ "reason": "Missing source_hash or verbatim_quote",
126
+ "confidence": 0.0
127
+ })
128
+ rejected_count += 1
129
+ continue
130
+
131
+ passed, conf, msg = self.hasher.verify_quote(shash, quote)
132
+ if passed:
133
+ audited_claims.append({
134
+ "claim_id": cid,
135
+ "statement": statement,
136
+ "original_tag": "VERIFIED",
137
+ "audited_tag": "VERIFIED",
138
+ "source_hash": shash,
139
+ "confidence": conf,
140
+ "verification_message": msg
141
+ })
142
+ verified_count += 1
143
+ else:
144
+ audited_claims.append({
145
+ "claim_id": cid,
146
+ "statement": statement,
147
+ "original_tag": "VERIFIED",
148
+ "audited_tag": "UNVERIFIED_REJECTED",
149
+ "source_hash": shash,
150
+ "rejected_quote": quote,
151
+ "reason": msg,
152
+ "confidence": conf
153
+ })
154
+ rejected_count += 1
155
+
156
+ return audited_claims, verified_count, rejected_count
157
+
158
+ def _compute_divergence(self, alpha_dossier: Dict[str, Any],
159
+ beta_dossier: Dict[str, Any]) -> Tuple[float, List[Dict[str, Any]]]:
160
+ alpha_claims = alpha_dossier.get("affirmative_claims", [])
161
+ beta_claims = beta_dossier.get("falsification_claims", [])
162
+ critiques = beta_dossier.get("methodological_critiques", [])
163
+
164
+ matrix = []
165
+ contradictions = 0
166
+
167
+ for crit in critiques:
168
+ target = crit.get("target_assertion", "")
169
+ matrix.append({
170
+ "tension_type": "METHODOLOGICAL_CHALLENGE",
171
+ "proponent_claim": target,
172
+ "adversary_critique": crit.get("critique", ""),
173
+ "counter_evidence_hash": crit.get("evidence_hash", "NONE")
174
+ })
175
+ contradictions += 1
176
+
177
+ # Calculate divergence
178
+ total_claims = max(1, len(alpha_claims) + len(beta_claims))
179
+ divergence = round(min(1.0, (contradictions * 2) / total_claims), 2)
180
+ return divergence, matrix
181
+
182
+ def _generate_synthesis_markdown(self, scope_id: str, alpha: Dict[str, Any],
183
+ beta: Dict[str, Any], audit: Dict[str, Any]) -> str:
184
+ summary = audit["summary"]
185
+ md = []
186
+ md.append(f"# Epistemic Synthesis: Scope {scope_id}\n")
187
+ md.append(f"**Audit Verdict**: `{summary['verdict']}` | **Epistemic Score**: `{summary['epistemic_score']}/1.0` | **Divergence Index**: `{summary['divergence_score']}`\n")
188
+
189
+ md.append("## 1. Verified Empirical Grounding")
190
+ for claim in audit["alpha_claims_audit"]:
191
+ if claim["audited_tag"] == "VERIFIED":
192
+ md.append(f"- `[VERIFIED: {claim['source_hash'][:8]}]` {claim['statement']}")
193
+ for claim in audit["beta_claims_audit"]:
194
+ if claim["audited_tag"] == "VERIFIED":
195
+ md.append(f"- `[VERIFIED: {claim['source_hash'][:8]}]` (Counter-Evidence) {claim['statement']}")
196
+
197
+ md.append("\n## 2. Dialectic Tensions & Falsification Audit")
198
+ if audit["divergence_matrix"]:
199
+ for item in audit["divergence_matrix"]:
200
+ md.append(f"### Tension: {item['tension_type']}")
201
+ md.append(f"- **Thesis Assertion**: {item['proponent_claim']}")
202
+ md.append(f"- **Adversarial Critique**: {item['adversary_critique']}")
203
+ else:
204
+ md.append("No active contradictions identified between primary literature and red-team findings.")
205
+
206
+ md.append("\n## 3. Rejected & Unverified Assertions")
207
+ rejected = [c for c in audit["alpha_claims_audit"] + audit["beta_claims_audit"] if c["audited_tag"] == "UNVERIFIED_REJECTED"]
208
+ if rejected:
209
+ for r in rejected:
210
+ md.append(f"- āš ļø **PURGED**: \"{r['statement']}\" — *Reason: {r['reason']}*")
211
+ else:
212
+ md.append("Zero claims rejected. 100% of cited assertions verified against source cache.")
213
+
214
+ md.append("\n## 4. Negative Knowledge Catalog")
215
+ all_neg = alpha.get("negative_knowledge", []) + beta.get("negative_knowledge", [])
216
+ if all_neg:
217
+ for n in all_neg:
218
+ md.append(f"- `[NEGATIVE_KNOWLEDGE: {n['query']}]` {n['finding']}")
219
+ else:
220
+ md.append("No negative knowledge declarations logged.")
221
+
222
+ return "\n".join(md)
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Epistemic Swarm: Dialectic Multi-Agent Research Runner.
4
+ Executes parallel Claude Code sub-processes (claude -p) for Proponent and Adversary agents,
5
+ monitors filesystem IPC scratchpads, and invokes the Epistemic Auditor.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import json
11
+ import shutil
12
+ import argparse
13
+ import subprocess
14
+ from pathlib import Path
15
+ from datetime import datetime, timezone
16
+ from concurrent.futures import ThreadPoolExecutor, as_completed
17
+ from typing import Dict, Any, List, Optional
18
+
19
+ # Ensure project root is in sys.path
20
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
21
+ sys.path.insert(0, str(PROJECT_ROOT))
22
+
23
+ from runner.state_machine import ResearchStateMachine, SessionStatus, ScopeStatus
24
+ from runner.auditor_engine import EpistemicAuditorEngine
25
+ from skills.research_cache.hasher import SourceHasher
26
+
27
+ class SwarmRunner:
28
+ def __init__(self, base_dir: Optional[Path] = None, mock_mode: bool = False):
29
+ self.base_dir = base_dir or Path(".research")
30
+ self.mock_mode = mock_mode
31
+ self.state_machine = ResearchStateMachine(base_dir=self.base_dir)
32
+ self.auditor = EpistemicAuditorEngine(base_dir=self.base_dir)
33
+ self.hasher = SourceHasher(base_dir=self.base_dir)
34
+ self.prompts_dir = PROJECT_ROOT / "prompts"
35
+
36
+ def run_claude_process(self, prompt: str, system_prompt_file: Optional[Path] = None,
37
+ tools: str = "default") -> str:
38
+ """Executes a headless Claude Code session via `claude -p`."""
39
+ if self.mock_mode:
40
+ return self._mock_claude_response(prompt)
41
+
42
+ cmd = ["claude", "-p", prompt, "--tools", tools]
43
+ if system_prompt_file and system_prompt_file.exists():
44
+ cmd.extend(["--system-prompt", str(system_prompt_file)])
45
+
46
+ try:
47
+ res = subprocess.run(
48
+ cmd,
49
+ capture_output=True,
50
+ text=True,
51
+ check=True,
52
+ cwd=str(PROJECT_ROOT)
53
+ )
54
+ return res.stdout.strip()
55
+ except subprocess.CalledProcessError as e:
56
+ print(f"[ERROR] Claude process failed: {e.stderr}", file=sys.stderr)
57
+ raise RuntimeError(f"Claude execution failed: {e.stderr}")
58
+
59
+ def _mock_claude_response(self, prompt: str) -> str:
60
+ """Mock response generator for unit testing without live API keys."""
61
+ if "Orchestrator" in prompt or "manifest.json" in prompt:
62
+ return json.dumps({
63
+ "session_id": "mock-session-001",
64
+ "objective": "Evaluate ZK prover latency",
65
+ "scopes": [
66
+ {
67
+ "scope_id": "scope_01_latency",
68
+ "title": "Hardware Prover Latency Bounds",
69
+ "objective": "Evaluate Poseidon hash witness generation latency on FPGAs vs GPUs",
70
+ "dependencies": [],
71
+ "affirmative_targets": ["Sub-200ms witness generation on 2^20 constraints"],
72
+ "adversarial_targets": ["PCIe bus bottlenecks during batch streaming"]
73
+ }
74
+ ]
75
+ })
76
+ return "MOCK_RESPONSE"
77
+
78
+ def orchestrate_objective(self, objective: str, frontier_file: Optional[Path] = None) -> List[Dict[str, Any]]:
79
+ """Phase 1: Run Swarm Orchestrator to decompose the research question."""
80
+ print(f"\n🧠 [Phase 1: Orchestration] Decomposing objective: '{objective}'...")
81
+ self.state_machine.init_session(objective)
82
+ self.state_machine.update_session_status(SessionStatus.ORCHESTRATING)
83
+
84
+ frontier_context = ""
85
+ if frontier_file and frontier_file.exists():
86
+ with open(frontier_file, "r", encoding="utf-8") as f:
87
+ frontier_data = json.load(f)
88
+ frontier_context = f"\nSETTLED CONSTRAINTS FROM FRONTIER:\n{json.dumps(frontier_data.get('settled_constraints', {}), indent=2)}"
89
+
90
+ orchestrator_prompt = f"""
91
+ You are the Swarm Orchestrator. Read prompts/orchestrator.md.
92
+ Objective: {objective}
93
+ {frontier_context}
94
+
95
+ Output ONLY valid JSON representing the scope decomposition conforming to prompts/orchestrator.md.
96
+ """
97
+ system_prompt = self.prompts_dir / "orchestrator.md"
98
+ raw_output = self.run_claude_process(orchestrator_prompt, system_prompt_file=system_prompt)
99
+
100
+ # Parse JSON
101
+ try:
102
+ # Handle potential markdown fence blocks
103
+ clean_json = raw_output
104
+ if "```json" in clean_json:
105
+ clean_json = clean_json.split("```json")[1].split("```")[0]
106
+ elif "```" in clean_json:
107
+ clean_json = clean_json.split("```")[1].split("```")[0]
108
+ manifest_data = json.loads(clean_json.strip())
109
+ scopes = manifest_data.get("scopes", [])
110
+ except Exception as e:
111
+ print(f"[WARN] Failed to parse JSON from orchestrator output: {e}. Using fallback decomposition.")
112
+ scopes = [
113
+ {
114
+ "scope_id": "scope_01_primary_investigation",
115
+ "title": f"Investigation: {objective[:40]}",
116
+ "objective": objective,
117
+ "dependencies": [],
118
+ "affirmative_targets": ["Find corroborating empirical data"],
119
+ "adversarial_targets": ["Probe counter-arguments and failure modes"]
120
+ }
121
+ ]
122
+
123
+ self.state_machine.set_scopes(scopes)
124
+ print(f"āœ… Generated {len(scopes)} decoupled dialectic scopes.")
125
+ return scopes
126
+
127
+ def run_agent_alpha(self, scope: Dict[str, Any]):
128
+ """Executes Agent Alpha (Thesis / Proponent) for a scope."""
129
+ scope_id = scope["scope_id"]
130
+ print(f" [Alpha] šŸ›ļø Starting Agent Alpha (Thesis) on [{scope_id}]...")
131
+
132
+ if self.mock_mode:
133
+ # Create synthetic cached source
134
+ sample_content = "# FPGA Prover Benchmark\nOur FPGA pipeline executes the Poseidon round constraints in 184ms with a peak memory bandwidth of 45 GB/s."
135
+ shash = self.hasher.store_source("https://arxiv.org/abs/2405.0001", sample_content, "FPGA Benchmark")
136
+
137
+ dossier = {
138
+ "agent": "Agent Alpha (Thesis)",
139
+ "scope_id": scope_id,
140
+ "timestamp": datetime.now(timezone.utc).isoformat(),
141
+ "affirmative_claims": [
142
+ {
143
+ "claim_id": "ALPHA-C01",
144
+ "tag": "VERIFIED",
145
+ "statement": "FPGA-accelerated Poseidon provers achieve sub-200ms latency on 2^20 constraints.",
146
+ "source_hash": shash,
147
+ "source_url": "https://arxiv.org/abs/2405.0001",
148
+ "verbatim_quote": "Our FPGA pipeline executes the Poseidon round constraints in 184ms with a peak memory bandwidth of 45 GB/s."
149
+ }
150
+ ],
151
+ "inferred_implications": [
152
+ {
153
+ "inference_id": "ALPHA-I01",
154
+ "tag": "INFERRED",
155
+ "statement": "Hardware provers satisfy 1-second block finality bounds.",
156
+ "parent_claims": ["ALPHA-C01"],
157
+ "deductive_logic": "184ms << 1000ms target."
158
+ }
159
+ ],
160
+ "negative_knowledge": []
161
+ }
162
+ else:
163
+ prompt = f"Run Agent Alpha for scope: {json.dumps(scope)}. Save findings to {scope_id} scratchpad."
164
+ system_prompt = self.prompts_dir / "agent_alpha_thesis.md"
165
+ self.run_claude_process(prompt, system_prompt_file=system_prompt)
166
+ # Read created dossier from scratchpad
167
+ dossier_path = self.state_machine.get_scope_dir(scope_id) / "alpha_dossier.json"
168
+ with open(dossier_path, "r", encoding="utf-8") as f:
169
+ dossier = json.load(f)
170
+
171
+ self.state_machine.record_agent_completion(scope_id, "alpha", dossier)
172
+ print(f" [Alpha] āœ… Completed Agent Alpha for [{scope_id}].")
173
+
174
+ def run_agent_beta(self, scope: Dict[str, Any]):
175
+ """Executes Agent Beta (Antithesis / Red Team) for a scope."""
176
+ scope_id = scope["scope_id"]
177
+ print(f" [Beta] šŸŽÆ Starting Agent Beta (Red Team) on [{scope_id}]...")
178
+
179
+ if self.mock_mode:
180
+ sample_content = "# PCIe Bus Saturation Study\nIn continuous batch streaming, PCIe 4.0 transfers introduce a 650ms delay, yielding total latency > 800ms."
181
+ shash = self.hasher.store_source("https://arxiv.org/abs/2406.9999", sample_content, "PCIe Bottlenecks")
182
+
183
+ dossier = {
184
+ "agent": "Agent Beta (Red Team)",
185
+ "scope_id": scope_id,
186
+ "timestamp": datetime.now(timezone.utc).isoformat(),
187
+ "falsification_claims": [
188
+ {
189
+ "claim_id": "BETA-C01",
190
+ "tag": "VERIFIED",
191
+ "statement": "Batch streaming incurs a 650ms PCIe transfer delay under production loads.",
192
+ "source_hash": shash,
193
+ "source_url": "https://arxiv.org/abs/2406.9999",
194
+ "verbatim_quote": "In continuous batch streaming, PCIe 4.0 transfers introduce a 650ms delay, yielding total latency > 800ms."
195
+ }
196
+ ],
197
+ "methodological_critiques": [
198
+ {
199
+ "target_assertion": "FPGA-accelerated Poseidon provers achieve sub-200ms latency on 2^20 constraints.",
200
+ "critique": "Benchmark isolates compute kernel and ignores host-to-device PCIe latency in pipelined batches.",
201
+ "evidence_hash": shash
202
+ }
203
+ ],
204
+ "negative_knowledge": [
205
+ {
206
+ "query": "Zero-latency PCIe streaming ZK provers",
207
+ "finding": "No architecture eliminates bus transfer overhead without on-chip memory > 128GB."
208
+ }
209
+ ]
210
+ }
211
+ else:
212
+ prompt = f"Run Agent Beta for scope: {json.dumps(scope)}. Save findings to {scope_id} scratchpad."
213
+ system_prompt = self.prompts_dir / "agent_beta_antithesis.md"
214
+ self.run_claude_process(prompt, system_prompt_file=system_prompt)
215
+ dossier_path = self.state_machine.get_scope_dir(scope_id) / "beta_dossier.json"
216
+ with open(dossier_path, "r", encoding="utf-8") as f:
217
+ dossier = json.load(f)
218
+
219
+ self.state_machine.record_agent_completion(scope_id, "beta", dossier)
220
+ print(f" [Beta] āœ… Completed Agent Beta for [{scope_id}].")
221
+
222
+ def execute_scope_dialectic(self, scope: Dict[str, Any]):
223
+ """Dispatches Agent Alpha and Agent Beta concurrently."""
224
+ scope_id = scope["scope_id"]
225
+ print(f"\n⚔ [Swarm Dispatch] Launching Dialectic Pair for [{scope_id}]: '{scope.get('title')}'")
226
+ self.state_machine.update_scope_status(scope_id, ScopeStatus.RUNNING_PARALLEL)
227
+
228
+ with ThreadPoolExecutor(max_workers=2) as executor:
229
+ future_alpha = executor.submit(self.run_agent_alpha, scope)
230
+ future_beta = executor.submit(self.run_agent_beta, scope)
231
+
232
+ # Wait for both
233
+ future_alpha.result()
234
+ future_beta.result()
235
+
236
+ # Phase 4: Run Epistemic Auditor
237
+ print(f"āš–ļø [Auditor] Auditing evidence & computing divergence for [{scope_id}]...")
238
+ audit_report = self.auditor.audit_scope(scope_id)
239
+ summary = audit_report["summary"]
240
+ print(f" [Audit Result] Score: {summary['epistemic_score']}/1.0 | Divergence: {summary['divergence_score']} | Verified: {summary['verified_passed']} | Rejected: {summary['unverified_rejected']}")
241
+
242
+ def run_swarm(self, objective: str, frontier_file: Optional[Path] = None):
243
+ """Full end-to-end execution loop."""
244
+ start_time = datetime.now(timezone.utc)
245
+ print("=" * 70)
246
+ print("🌟 EPISTEMIC SWARM: HIGH-INTEGRITY RESEARCH HARNESS")
247
+ print("=" * 70)
248
+
249
+ # 1. Orchestrate
250
+ scopes = self.orchestrate_objective(objective, frontier_file)
251
+
252
+ # 2. Execute scopes according to DAG
253
+ while True:
254
+ ready_scopes = self.state_machine.get_ready_scopes()
255
+ if not ready_scopes:
256
+ # Check if all scopes are complete
257
+ manifest = self.state_machine.load_global_manifest()
258
+ all_complete = all(
259
+ self.state_machine.load_scope_manifest(s["scope_id"]).get("status") == ScopeStatus.COMPLETE.value
260
+ for s in manifest["scopes"]
261
+ )
262
+ if all_complete:
263
+ break
264
+ else:
265
+ print("[ERROR] Deadlock in scope dependency graph.", file=sys.stderr)
266
+ self.state_machine.update_session_status(SessionStatus.FAILED)
267
+ return
268
+
269
+ for scope in ready_scopes:
270
+ self.execute_scope_dialectic(scope)
271
+
272
+ # 3. Master Synthesis Compilation
273
+ print("\nšŸ“œ [Phase 5: Master Synthesis] Aggregating scope dossiers...")
274
+ self._compile_master_synthesis(objective)
275
+ self.state_machine.update_session_status(SessionStatus.COMPLETED)
276
+
277
+ duration = (datetime.now(timezone.utc) - start_time).total_seconds()
278
+ print(f"\nšŸŽ‰ Research swarm completed in {duration:.1f}s. Report: .research/final_synthesis.md")
279
+
280
+ def _compile_master_synthesis(self, objective: str):
281
+ manifest = self.state_machine.load_global_manifest()
282
+ synthesis_lines = [
283
+ f"# Master Epistemic Research Report: {objective}\n",
284
+ f"**Session ID**: `{manifest['session_id']}` | **Generated**: `{manifest['updated_at']}`\n",
285
+ "## Executive Summary",
286
+ "This report was compiled using the Epistemic Swarm dialectic harness. Every factual statement carries an empirical verification pointer backed by a content-addressed raw document cache.\n",
287
+ "## Scope Findings & Dialectic Balance Sheets\n"
288
+ ]
289
+
290
+ total_verified = 0
291
+ total_rejected = 0
292
+ all_divergences = []
293
+
294
+ for scope in manifest["scopes"]:
295
+ sid = scope["scope_id"]
296
+ scope_dir = self.state_machine.get_scope_dir(sid)
297
+ audit_file = scope_dir / "audit_report.json"
298
+ synth_file = scope_dir / "scope_synthesis.md"
299
+
300
+ if audit_file.exists():
301
+ with open(audit_file, "r", encoding="utf-8") as f:
302
+ ar = json.load(f)
303
+ total_verified += ar["summary"]["verified_passed"]
304
+ total_rejected += ar["summary"]["unverified_rejected"]
305
+ all_divergences.append(ar["summary"]["divergence_score"])
306
+
307
+ if synth_file.exists():
308
+ with open(synth_file, "r", encoding="utf-8") as f:
309
+ synthesis_lines.append(f.read())
310
+ synthesis_lines.append("\n---\n")
311
+
312
+ avg_div = round(sum(all_divergences) / max(1, len(all_divergences)), 2)
313
+ synthesis_lines.append(f"\n## Swarm Epistemic Audit Totals\n")
314
+ synthesis_lines.append(f"- **Total Verified Primary Citations**: `{total_verified}`")
315
+ synthesis_lines.append(f"- **Total Unverified Claims Purged**: `{total_rejected}`")
316
+ synthesis_lines.append(f"- **Mean Swarm Divergence Score**: `{avg_div}`")
317
+
318
+ final_path = self.base_dir / "final_synthesis.md"
319
+ with open(final_path, "w", encoding="utf-8") as f:
320
+ f.write("\n".join(synthesis_lines))
321
+
322
+
323
+ def main():
324
+ parser = argparse.ArgumentParser(description="Epistemic Swarm Dialectic Research Runner")
325
+ parser.add_argument("--objective", type=str, required=True, help="Research question or objective")
326
+ parser.add_argument("--frontier", type=str, help="Path to settled frontier.json from /grilling")
327
+ parser.add_argument("--mock-claude", action="store_true", help="Run with synthetic test data without invoking Claude Code")
328
+ parser.add_argument("--dir", default=".research", help="Path to .research workspace")
329
+
330
+ args = parser.parse_args()
331
+ frontier_path = Path(args.frontier) if args.frontier else None
332
+ runner = SwarmRunner(base_dir=Path(args.dir), mock_mode=args.mock_claude)
333
+ runner.run_swarm(args.objective, frontier_file=frontier_path)
334
+
335
+
336
+ if __name__ == "__main__":
337
+ main()