@heretek-ai/epistemic-swarm 0.4.6 → 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 +310 -134
- 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 +398 -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,6 +5,7 @@ Implements Matt Pocock-style design tree traversal to isolate the active decisio
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import sys
|
|
8
|
+
import os
|
|
8
9
|
import json
|
|
9
10
|
import argparse
|
|
10
11
|
from pathlib import Path
|
|
@@ -84,7 +85,8 @@ class DesignTree:
|
|
|
84
85
|
return len(self.compute_frontier()) == 0 and all(n.is_settled() for n in self.nodes.values())
|
|
85
86
|
|
|
86
87
|
def export_frontier_json(self, output_path: Path):
|
|
87
|
-
|
|
88
|
+
safe_path = Path(os.path.realpath(str(output_path)))
|
|
89
|
+
safe_path.parent.mkdir(parents=True, exist_ok=True)
|
|
88
90
|
data = {
|
|
89
91
|
"objective": self.objective,
|
|
90
92
|
"is_complete": self.is_complete(),
|
|
@@ -93,12 +95,13 @@ class DesignTree:
|
|
|
93
95
|
k: v.settled_answer for k, v in self.nodes.items() if v.is_settled()
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
|
-
with open(
|
|
98
|
+
with open(safe_path, "w", encoding="utf-8") as f:
|
|
97
99
|
json.dump(data, f, indent=2)
|
|
98
100
|
|
|
99
101
|
@classmethod
|
|
100
102
|
def load_from_json(cls, file_path: Path) -> 'DesignTree':
|
|
101
|
-
|
|
103
|
+
safe_path = Path(os.path.realpath(str(file_path)))
|
|
104
|
+
with open(safe_path, "r", encoding="utf-8") as f:
|
|
102
105
|
data = json.load(f)
|
|
103
106
|
tree = cls(objective=data["objective"])
|
|
104
107
|
for k, v in data.get("nodes", {}).items():
|
|
@@ -106,15 +109,31 @@ class DesignTree:
|
|
|
106
109
|
return tree
|
|
107
110
|
|
|
108
111
|
|
|
112
|
+
def _display_frontier(tree: DesignTree, frontier: list) -> None:
|
|
113
|
+
print(f"\n🎯 Objective: {tree.objective}")
|
|
114
|
+
print(f"📊 Total Nodes: {len(tree.nodes)} | Settled: {sum(1 for n in tree.nodes.values() if n.is_settled())}")
|
|
115
|
+
if not frontier:
|
|
116
|
+
if tree.nodes and tree.is_complete():
|
|
117
|
+
print("✅ Frontier is EMPTY. All prerequisite branches are fully settled!")
|
|
118
|
+
else:
|
|
119
|
+
print("ℹ️ No active frontier nodes. Define new decision nodes to begin grilling.")
|
|
120
|
+
else:
|
|
121
|
+
print(f"\n⚡ Current Active Frontier ({len(frontier)} questions ready):")
|
|
122
|
+
for idx, node in enumerate(frontier, 1):
|
|
123
|
+
print(f"\n❓ Q{idx} [{node.node_id}] - {node.title}")
|
|
124
|
+
print(f" {node.question}")
|
|
125
|
+
print(f" ➡️ Recommended: {node.recommended}")
|
|
126
|
+
|
|
127
|
+
|
|
109
128
|
def main():
|
|
110
129
|
parser = argparse.ArgumentParser(description="Epistemic Swarm Socratic Decision Tree")
|
|
111
130
|
parser.add_argument("--objective", type=str, help="Research objective")
|
|
112
131
|
parser.add_argument("--file", type=str, default=".research/frontier.json", help="Path to frontier.json")
|
|
113
132
|
parser.add_argument("--show-frontier", action="store_true", help="Print the current decision frontier")
|
|
114
133
|
parser.add_argument("--settle", nargs=2, metavar=("NODE_ID", "ANSWER"), help="Settle a decision node")
|
|
115
|
-
|
|
134
|
+
|
|
116
135
|
args = parser.parse_args()
|
|
117
|
-
frontier_path = Path(args.file)
|
|
136
|
+
frontier_path = Path(os.path.realpath(str(args.file)))
|
|
118
137
|
|
|
119
138
|
if frontier_path.exists():
|
|
120
139
|
tree = DesignTree.load_from_json(frontier_path)
|
|
@@ -130,19 +149,8 @@ def main():
|
|
|
130
149
|
|
|
131
150
|
frontier = tree.compute_frontier()
|
|
132
151
|
if args.show_frontier or not args.settle:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if not frontier:
|
|
136
|
-
if tree.nodes and tree.is_complete():
|
|
137
|
-
print("✅ Frontier is EMPTY. All prerequisite branches are fully settled!")
|
|
138
|
-
else:
|
|
139
|
-
print("ℹ️ No active frontier nodes. Define new decision nodes to begin grilling.")
|
|
140
|
-
else:
|
|
141
|
-
print(f"\n⚡ Current Active Frontier ({len(frontier)} questions ready):")
|
|
142
|
-
for idx, node in enumerate(frontier, 1):
|
|
143
|
-
print(f"\n❓ Q{idx} [{node.node_id}] - {node.title}")
|
|
144
|
-
print(f" {node.question}")
|
|
145
|
-
print(f" ➡️ Recommended: {node.recommended}")
|
|
152
|
+
_display_frontier(tree, frontier)
|
|
153
|
+
|
|
146
154
|
|
|
147
155
|
if __name__ == "__main__":
|
|
148
156
|
main()
|
|
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
|
package/runner/auditor_engine.py
CHANGED
|
@@ -17,6 +17,68 @@ from skills.research_cache.hasher import SourceHasher
|
|
|
17
17
|
from runner.state_machine import ResearchStateMachine, ScopeStatus
|
|
18
18
|
from runner.refinement import compute_epistemic_score
|
|
19
19
|
|
|
20
|
+
def _compute_scope_epistemic_score(
|
|
21
|
+
constitution: Any,
|
|
22
|
+
alpha_dossier: Dict[str, Any],
|
|
23
|
+
beta_dossier: Dict[str, Any],
|
|
24
|
+
alpha_results: list,
|
|
25
|
+
beta_results: list,
|
|
26
|
+
counts: Dict[str, int],
|
|
27
|
+
) -> float:
|
|
28
|
+
from runner.refinement import LEGACY_CONSTITUTION
|
|
29
|
+
|
|
30
|
+
if constitution is not LEGACY_CONSTITUTION:
|
|
31
|
+
from runner.claim_witness import STATUS_REJECTED, claims_from_dossier
|
|
32
|
+
from runner.refinement import compute_epistemic_score_from_claims
|
|
33
|
+
|
|
34
|
+
results_by_id = {r["claim_id"]: r for r in (alpha_results + beta_results)}
|
|
35
|
+
score_claims = claims_from_dossier(alpha_dossier) + claims_from_dossier(beta_dossier)
|
|
36
|
+
for c in score_claims:
|
|
37
|
+
r = results_by_id.get(c.claim_id)
|
|
38
|
+
if r is not None and r.get("audited_tag") == "UNVERIFIED_REJECTED":
|
|
39
|
+
c.tag = "UNVERIFIED_REJECTED"
|
|
40
|
+
c.status = STATUS_REJECTED
|
|
41
|
+
epistemic_score, _breakdown = compute_epistemic_score_from_claims(
|
|
42
|
+
score_claims, constitution=constitution
|
|
43
|
+
)
|
|
44
|
+
return epistemic_score
|
|
45
|
+
|
|
46
|
+
epistemic_score, _breakdown = compute_epistemic_score(counts)
|
|
47
|
+
return epistemic_score
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _apply_living_dossiers_degradation(
|
|
51
|
+
base_dir: Path,
|
|
52
|
+
scope_id: str,
|
|
53
|
+
alpha_dossier: Dict[str, Any],
|
|
54
|
+
beta_dossier: Dict[str, Any],
|
|
55
|
+
) -> Dict[str, Any]:
|
|
56
|
+
degradation = {"events": [], "degraded_scopes": []}
|
|
57
|
+
try:
|
|
58
|
+
from runner.living_dossiers import (
|
|
59
|
+
apply_degradation,
|
|
60
|
+
append_ledger,
|
|
61
|
+
load_retractions,
|
|
62
|
+
queue_requeue,
|
|
63
|
+
)
|
|
64
|
+
from runner.claim_witness import claims_from_dossier
|
|
65
|
+
|
|
66
|
+
retractions = load_retractions(base_dir)
|
|
67
|
+
if retractions:
|
|
68
|
+
all_claims = claims_from_dossier(alpha_dossier) + claims_from_dossier(beta_dossier)
|
|
69
|
+
_degraded, events = apply_degradation(all_claims, retractions, scope_id=scope_id)
|
|
70
|
+
if events:
|
|
71
|
+
append_ledger(base_dir, events)
|
|
72
|
+
queue_requeue(base_dir, scope_id, reason="claim degradation (retraction event)")
|
|
73
|
+
degradation = {
|
|
74
|
+
"events": [e.to_dict() for e in events],
|
|
75
|
+
"degraded_scopes": [scope_id] if events else [],
|
|
76
|
+
}
|
|
77
|
+
except Exception as exc: # noqa: BLE001 - degradation must not break audit
|
|
78
|
+
print(f"[auditor] living-dossiers pass skipped: {exc}", file=sys.stderr)
|
|
79
|
+
return degradation
|
|
80
|
+
|
|
81
|
+
|
|
20
82
|
class EpistemicAuditorEngine:
|
|
21
83
|
def __init__(self, base_dir: Optional[Path] = None):
|
|
22
84
|
self.base_dir = base_dir or Path(".research")
|
|
@@ -59,82 +121,24 @@ class EpistemicAuditorEngine:
|
|
|
59
121
|
beta_dossier.get("falsification_claims", []), constitution=constitution
|
|
60
122
|
)
|
|
61
123
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
total_inferred = len(alpha_dossier.get("inferred_implications", []))
|
|
71
|
-
total_hypotheses = len(beta_dossier.get("hypotheses", []))
|
|
124
|
+
counts = {
|
|
125
|
+
"verified": alpha_verified + beta_verified,
|
|
126
|
+
"rejected": alpha_rejected + beta_rejected,
|
|
127
|
+
"inferred": len(alpha_dossier.get("inferred_implications", [])),
|
|
128
|
+
"hypotheses": len(beta_dossier.get("hypotheses", [])),
|
|
129
|
+
"neg_knowledge": len(alpha_dossier.get("negative_knowledge", [])) + len(beta_dossier.get("negative_knowledge", [])),
|
|
130
|
+
}
|
|
72
131
|
|
|
73
132
|
# 3. Calculate Epistemic Score via the pure refinement function.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
# tier weighting is opt-in via Domain Packs (Stream G).
|
|
78
|
-
if constitution is not LEGACY_CONSTITUTION:
|
|
79
|
-
from runner.claim_witness import STATUS_REJECTED, claims_from_dossier
|
|
80
|
-
from runner.refinement import compute_epistemic_score_from_claims
|
|
81
|
-
|
|
82
|
-
results_by_id = {
|
|
83
|
-
r["claim_id"]: r for r in (alpha_results + beta_results)
|
|
84
|
-
}
|
|
85
|
-
score_claims = claims_from_dossier(alpha_dossier) + claims_from_dossier(beta_dossier)
|
|
86
|
-
for c in score_claims:
|
|
87
|
-
r = results_by_id.get(c.claim_id)
|
|
88
|
-
if r is not None and r.get("audited_tag") == "UNVERIFIED_REJECTED":
|
|
89
|
-
# Reflect the auditor's verdict, not the dossier's claim.
|
|
90
|
-
c.tag = "UNVERIFIED_REJECTED"
|
|
91
|
-
c.status = STATUS_REJECTED
|
|
92
|
-
epistemic_score, _breakdown = compute_epistemic_score_from_claims(
|
|
93
|
-
score_claims, constitution=constitution
|
|
94
|
-
)
|
|
95
|
-
else:
|
|
96
|
-
epistemic_score, _breakdown = compute_epistemic_score({
|
|
97
|
-
"verified": total_verified,
|
|
98
|
-
"rejected": total_rejected,
|
|
99
|
-
"inferred": total_inferred,
|
|
100
|
-
"hypotheses": total_hypotheses,
|
|
101
|
-
"neg_knowledge": total_neg_knowledge,
|
|
102
|
-
})
|
|
133
|
+
epistemic_score = _compute_scope_epistemic_score(
|
|
134
|
+
constitution, alpha_dossier, beta_dossier, alpha_results, beta_results, counts
|
|
135
|
+
)
|
|
103
136
|
accept_threshold = constitution.accept_threshold
|
|
104
137
|
|
|
105
|
-
# 3b. Living Dossiers (Stream C):
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
try:
|
|
110
|
-
from runner.living_dossiers import (
|
|
111
|
-
apply_degradation,
|
|
112
|
-
append_ledger,
|
|
113
|
-
load_retractions,
|
|
114
|
-
queue_requeue,
|
|
115
|
-
)
|
|
116
|
-
from runner.claim_witness import claims_from_dossier
|
|
117
|
-
|
|
118
|
-
retractions = load_retractions(self.base_dir)
|
|
119
|
-
if retractions:
|
|
120
|
-
all_claims = (
|
|
121
|
-
claims_from_dossier(alpha_dossier)
|
|
122
|
-
+ claims_from_dossier(beta_dossier)
|
|
123
|
-
)
|
|
124
|
-
_degraded, events = apply_degradation(
|
|
125
|
-
all_claims, retractions, scope_id=scope_id
|
|
126
|
-
)
|
|
127
|
-
if events:
|
|
128
|
-
append_ledger(self.base_dir, events)
|
|
129
|
-
queue_requeue(
|
|
130
|
-
self.base_dir, scope_id, reason="claim degradation (retraction event)"
|
|
131
|
-
)
|
|
132
|
-
degradation = {
|
|
133
|
-
"events": [e.to_dict() for e in events],
|
|
134
|
-
"degraded_scopes": [scope_id] if events else [],
|
|
135
|
-
}
|
|
136
|
-
except Exception as exc: # noqa: BLE001 - degradation must not break audit
|
|
137
|
-
print(f"[auditor] living-dossiers pass skipped: {exc}", file=sys.stderr)
|
|
138
|
+
# 3b. Living Dossiers (Stream C): check degradation against retraction events
|
|
139
|
+
degradation = _apply_living_dossiers_degradation(
|
|
140
|
+
self.base_dir, scope_id, alpha_dossier, beta_dossier
|
|
141
|
+
)
|
|
138
142
|
|
|
139
143
|
# 4. Calculate Divergence Score
|
|
140
144
|
divergence_score, divergence_matrix = self._compute_divergence(alpha_dossier, beta_dossier)
|
|
@@ -146,9 +150,9 @@ class EpistemicAuditorEngine:
|
|
|
146
150
|
"audited_at": datetime.now(timezone.utc).isoformat(),
|
|
147
151
|
"summary": {
|
|
148
152
|
"total_claims_audited": len(alpha_results) + len(beta_results),
|
|
149
|
-
"verified_passed":
|
|
150
|
-
"unverified_rejected":
|
|
151
|
-
"negative_knowledge_count":
|
|
153
|
+
"verified_passed": counts["verified"],
|
|
154
|
+
"unverified_rejected": counts["rejected"],
|
|
155
|
+
"negative_knowledge_count": counts["neg_knowledge"],
|
|
152
156
|
"epistemic_score": epistemic_score,
|
|
153
157
|
"divergence_score": divergence_score,
|
|
154
158
|
"verdict": "CERTIFIED" if epistemic_score >= accept_threshold else "WARNING_LOW_GROUNDING"
|
|
@@ -294,44 +298,60 @@ class EpistemicAuditorEngine:
|
|
|
294
298
|
divergence = round(min(1.0, (contradictions * 2) / total_claims), 2)
|
|
295
299
|
return divergence, matrix
|
|
296
300
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
md.append(f"# Epistemic Synthesis: Scope {scope_id}\n")
|
|
302
|
-
md.append(f"**Audit Verdict**: `{summary['verdict']}` | **Epistemic Score**: `{summary['epistemic_score']}/1.0` | **Divergence Index**: `{summary['divergence_score']}`\n")
|
|
303
|
-
|
|
304
|
-
md.append("## 1. Verified Empirical Grounding")
|
|
305
|
-
for claim in audit["alpha_claims_audit"]:
|
|
301
|
+
@staticmethod
|
|
302
|
+
def _render_verified_section(alpha_audit: list, beta_audit: list) -> List[str]:
|
|
303
|
+
lines = ["## 1. Verified Empirical Grounding"]
|
|
304
|
+
for claim in alpha_audit:
|
|
306
305
|
if claim["audited_tag"] == "VERIFIED":
|
|
307
|
-
|
|
308
|
-
for claim in
|
|
306
|
+
lines.append(f"- `[VERIFIED: {claim['source_hash'][:8]}]` {claim['statement']}")
|
|
307
|
+
for claim in beta_audit:
|
|
309
308
|
if claim["audited_tag"] == "VERIFIED":
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
309
|
+
lines.append(f"- `[VERIFIED: {claim['source_hash'][:8]}]` (Counter-Evidence) {claim['statement']}")
|
|
310
|
+
return lines
|
|
311
|
+
|
|
312
|
+
@staticmethod
|
|
313
|
+
def _render_tensions_section(matrix: list) -> List[str]:
|
|
314
|
+
lines = ["\n## 2. Dialectic Tensions & Falsification Audit"]
|
|
315
|
+
if matrix:
|
|
316
|
+
for item in matrix:
|
|
317
|
+
lines.append(f"### Tension: {item['tension_type']}")
|
|
318
|
+
lines.append(f"- **Thesis Assertion**: {item['proponent_claim']}")
|
|
319
|
+
lines.append(f"- **Adversarial Critique**: {item['adversary_critique']}")
|
|
318
320
|
else:
|
|
319
|
-
|
|
321
|
+
lines.append("No active contradictions identified between primary literature and red-team findings.")
|
|
322
|
+
return lines
|
|
320
323
|
|
|
321
|
-
|
|
322
|
-
|
|
324
|
+
@staticmethod
|
|
325
|
+
def _render_rejected_section(claims: list) -> List[str]:
|
|
326
|
+
lines = ["\n## 3. Rejected & Unverified Assertions"]
|
|
327
|
+
rejected = [c for c in claims if c["audited_tag"] == "UNVERIFIED_REJECTED"]
|
|
323
328
|
if rejected:
|
|
324
329
|
for r in rejected:
|
|
325
|
-
|
|
330
|
+
lines.append(f"- ⚠️ **PURGED**: \"{r['statement']}\" — *Reason: {r['reason']}*")
|
|
326
331
|
else:
|
|
327
|
-
|
|
332
|
+
lines.append("Zero claims rejected. 100% of cited assertions verified against source cache.")
|
|
333
|
+
return lines
|
|
328
334
|
|
|
329
|
-
|
|
335
|
+
@staticmethod
|
|
336
|
+
def _render_negative_knowledge_section(alpha: Dict[str, Any], beta: Dict[str, Any]) -> List[str]:
|
|
337
|
+
lines = ["\n## 4. Negative Knowledge Catalog"]
|
|
330
338
|
all_neg = alpha.get("negative_knowledge", []) + beta.get("negative_knowledge", [])
|
|
331
339
|
if all_neg:
|
|
332
340
|
for n in all_neg:
|
|
333
|
-
|
|
341
|
+
lines.append(f"- `[NEGATIVE_KNOWLEDGE: {n['query']}]` {n['finding']}")
|
|
334
342
|
else:
|
|
335
|
-
|
|
343
|
+
lines.append("No negative knowledge declarations logged.")
|
|
344
|
+
return lines
|
|
336
345
|
|
|
346
|
+
def _generate_synthesis_markdown(self, scope_id: str, alpha: Dict[str, Any],
|
|
347
|
+
beta: Dict[str, Any], audit: Dict[str, Any]) -> str:
|
|
348
|
+
summary = audit["summary"]
|
|
349
|
+
md = [
|
|
350
|
+
f"# Epistemic Synthesis: Scope {scope_id}\n",
|
|
351
|
+
f"**Audit Verdict**: `{summary['verdict']}` | **Epistemic Score**: `{summary['epistemic_score']}/1.0` | **Divergence Index**: `{summary['divergence_score']}`\n",
|
|
352
|
+
]
|
|
353
|
+
md.extend(self._render_verified_section(audit["alpha_claims_audit"], audit["beta_claims_audit"]))
|
|
354
|
+
md.extend(self._render_tensions_section(audit.get("divergence_matrix", [])))
|
|
355
|
+
md.extend(self._render_rejected_section(audit["alpha_claims_audit"] + audit["beta_claims_audit"]))
|
|
356
|
+
md.extend(self._render_negative_knowledge_section(alpha, beta))
|
|
337
357
|
return "\n".join(md)
|
package/runner/claim_store.py
CHANGED
|
@@ -22,6 +22,7 @@ need an API call and are out of this phase's scope).
|
|
|
22
22
|
import argparse
|
|
23
23
|
import hashlib
|
|
24
24
|
import json
|
|
25
|
+
import os
|
|
25
26
|
import sqlite3
|
|
26
27
|
import sys
|
|
27
28
|
from pathlib import Path
|
|
@@ -50,8 +51,8 @@ def _has_fts5(conn: sqlite3.Connection) -> bool:
|
|
|
50
51
|
|
|
51
52
|
class ClaimStore:
|
|
52
53
|
def __init__(self, base_dir: Path, db_path: Optional[Path] = None):
|
|
53
|
-
self.base_dir = Path(base_dir)
|
|
54
|
-
self.db_path = db_path
|
|
54
|
+
self.base_dir = Path(os.path.realpath(str(base_dir)))
|
|
55
|
+
self.db_path = Path(os.path.realpath(str(db_path))) if db_path else (self.base_dir / "claims.sqlite")
|
|
55
56
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
56
57
|
self.conn = sqlite3.connect(str(self.db_path))
|
|
57
58
|
self.conn.row_factory = sqlite3.Row
|
|
@@ -344,7 +345,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
344
345
|
search_p.add_argument("text")
|
|
345
346
|
args = parser.parse_args(argv)
|
|
346
347
|
|
|
347
|
-
base = Path(args.dir)
|
|
348
|
+
base = Path(os.path.realpath(str(args.dir)))
|
|
348
349
|
if args.command == "reindex" or args.command is None:
|
|
349
350
|
print(json.dumps(reindex(base), indent=2))
|
|
350
351
|
return 0
|
|
@@ -354,7 +355,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
354
355
|
store.close()
|
|
355
356
|
print(json.dumps(rows, indent=2, default=str))
|
|
356
357
|
return 0
|
|
357
|
-
return
|
|
358
|
+
return 1
|
|
358
359
|
|
|
359
360
|
|
|
360
361
|
if __name__ == "__main__":
|
|
@@ -21,6 +21,7 @@ Degradation rules (applied after quote verification):
|
|
|
21
21
|
|
|
22
22
|
import json
|
|
23
23
|
import os
|
|
24
|
+
import re
|
|
24
25
|
import sys
|
|
25
26
|
from dataclasses import dataclass, asdict
|
|
26
27
|
from datetime import datetime, timezone
|
|
@@ -38,6 +39,7 @@ from runner.claim_witness import ( # noqa: E402
|
|
|
38
39
|
STATUS_SUSPECT,
|
|
39
40
|
ClaimWitness,
|
|
40
41
|
)
|
|
42
|
+
from runner.path_safety import safe_join # noqa: E402
|
|
41
43
|
|
|
42
44
|
EVENT_RETRACTED = "RETRACTED"
|
|
43
45
|
EVENT_REVISED = "REVISED"
|
|
@@ -83,6 +85,8 @@ def write_retraction(
|
|
|
83
85
|
"""Record a RETRACTED/REVISED event for a cached source."""
|
|
84
86
|
if event not in VALID_EVENTS:
|
|
85
87
|
raise ValueError(f"event must be one of {VALID_EVENTS}, got {event!r}")
|
|
88
|
+
if not re.match(r"^[a-zA-Z0-9_\-\.]+$", source_hash):
|
|
89
|
+
raise ValueError(f"Invalid source hash: {source_hash!r}")
|
|
86
90
|
retr_dir = Path(base_dir) / "retractions"
|
|
87
91
|
retr_dir.mkdir(parents=True, exist_ok=True)
|
|
88
92
|
payload = RetractionEvent(
|
|
@@ -92,7 +96,11 @@ def write_retraction(
|
|
|
92
96
|
note=note,
|
|
93
97
|
supersedes=supersedes,
|
|
94
98
|
)
|
|
95
|
-
|
|
99
|
+
# `source_hash` is untrusted tool input, so the filename is joined under
|
|
100
|
+
# `retractions/` through safe_join (no separators, no bare `..`, and a
|
|
101
|
+
# containment re-check after canonicalisation). The regex above is the
|
|
102
|
+
# input gate; safe_join is the one that holds if the gate is ever loosened.
|
|
103
|
+
path = safe_join(retr_dir, f"{source_hash}.json")
|
|
96
104
|
# Last write wins per hash (a REVISED may follow a RETRACTED); the ledger
|
|
97
105
|
# keeps the full history of claim transitions either way.
|
|
98
106
|
with open(path, "w", encoding="utf-8") as f:
|
|
@@ -258,7 +266,14 @@ def check_staleness(base_dir: Path, scope_ids: Optional[List[str]] = None) -> Di
|
|
|
258
266
|
|
|
259
267
|
if scope_ids is None:
|
|
260
268
|
if not scratch.exists():
|
|
261
|
-
return {
|
|
269
|
+
return {
|
|
270
|
+
"scopes": [],
|
|
271
|
+
"retractions": len(retractions),
|
|
272
|
+
"degraded": 0,
|
|
273
|
+
# Keep the early return shape identical to the normal one so
|
|
274
|
+
# callers can key off `degraded_scopes` unconditionally.
|
|
275
|
+
"degraded_scopes": [],
|
|
276
|
+
}
|
|
262
277
|
scope_dirs = sorted(p for p in scratch.iterdir() if p.is_dir())
|
|
263
278
|
else:
|
|
264
279
|
scope_dirs = [scratch / s for s in scope_ids]
|
|
@@ -281,7 +296,7 @@ def check_staleness(base_dir: Path, scope_ids: Optional[List[str]] = None) -> Di
|
|
|
281
296
|
claim_sets += 1
|
|
282
297
|
dossier = load_dossier(dpath)
|
|
283
298
|
claims = claims_from_dossier(dossier)
|
|
284
|
-
|
|
299
|
+
_, events = apply_degradation(
|
|
285
300
|
claims, retractions, scope_id=scope_id
|
|
286
301
|
)
|
|
287
302
|
all_events.extend(events)
|
|
@@ -336,19 +351,26 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
336
351
|
rep.add_argument("--event", required=True, choices=list(VALID_EVENTS))
|
|
337
352
|
rep.add_argument("--note", default="")
|
|
338
353
|
|
|
339
|
-
sub.add_parser(
|
|
354
|
+
sub.add_parser(
|
|
355
|
+
"check",
|
|
356
|
+
help=(
|
|
357
|
+
"Run one staleness/degradation pass. Exits 1 when any scope "
|
|
358
|
+
"degraded (the ledger reporting a problem IS the outcome), 0 "
|
|
359
|
+
"otherwise — callers under `set -e` should expect that."
|
|
360
|
+
),
|
|
361
|
+
)
|
|
340
362
|
args = parser.parse_args(argv)
|
|
341
363
|
|
|
342
|
-
base = Path(args.dir)
|
|
364
|
+
base = Path(os.path.realpath(str(args.dir)))
|
|
343
365
|
if args.command == "report":
|
|
344
366
|
path = write_retraction(base, args.hash, args.event, note=args.note)
|
|
345
367
|
print(json.dumps({"status": "recorded", "path": str(path)}))
|
|
346
368
|
return 0
|
|
347
|
-
if args.command == "check":
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return
|
|
369
|
+
if args.command == "check" or args.command is None:
|
|
370
|
+
res = check_staleness(base)
|
|
371
|
+
print(json.dumps(res, indent=2))
|
|
372
|
+
return 1 if res.get("degraded_scopes") else 0
|
|
373
|
+
return 1
|
|
352
374
|
|
|
353
375
|
|
|
354
376
|
if __name__ == "__main__":
|
package/runner/mcp_protocol.py
CHANGED
|
@@ -140,7 +140,7 @@ class StdioJsonRpcServer:
|
|
|
140
140
|
return self._error(req_id, -32601, f"Tool {tool_name} not found")
|
|
141
141
|
try:
|
|
142
142
|
text = spec.handler(args)
|
|
143
|
-
except Exception as exc: # noqa: BLE001
|
|
143
|
+
except Exception as exc: # noqa: BLE001 # surface, do not kill the server
|
|
144
144
|
return self._error(req_id, -32603, f"{type(exc).__name__}: {exc}")
|
|
145
145
|
return self._reply(
|
|
146
146
|
req_id, {"content": [{"type": "text", "text": text}]}
|
package/runner/mcp_server.py
CHANGED
|
@@ -63,6 +63,13 @@ def _capture_stdout(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> str:
|
|
|
63
63
|
# tool handlers
|
|
64
64
|
# ---------------------------------------------------------------------------
|
|
65
65
|
|
|
66
|
+
DEFAULT_RESEARCH_DIR = ".research"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _resolve_base_dir(args: Dict[str, Any]) -> Path:
|
|
70
|
+
raw = args.get("base_dir") or args.get("dir") or DEFAULT_RESEARCH_DIR
|
|
71
|
+
return Path(os.path.realpath(str(raw)))
|
|
72
|
+
|
|
66
73
|
def _handle_config(args: Dict[str, Any]) -> str:
|
|
67
74
|
"""Inspect or modify swarm parameters in .research/config.json."""
|
|
68
75
|
from skills.swarm_config.configure import (
|
|
@@ -71,7 +78,7 @@ def _handle_config(args: Dict[str, Any]) -> str:
|
|
|
71
78
|
save_config,
|
|
72
79
|
)
|
|
73
80
|
|
|
74
|
-
base_dir =
|
|
81
|
+
base_dir = str(_resolve_base_dir(args))
|
|
75
82
|
cfg = load_config(base_dir)
|
|
76
83
|
|
|
77
84
|
updates = {}
|
|
@@ -102,7 +109,7 @@ def _run_swarm_mode(mode: Optional[str], args: Dict[str, Any]) -> str:
|
|
|
102
109
|
|
|
103
110
|
frontier = args.get("frontier") or args.get("frontier_file")
|
|
104
111
|
runner = SwarmRunner(
|
|
105
|
-
base_dir=
|
|
112
|
+
base_dir=_resolve_base_dir(args),
|
|
106
113
|
mock_mode=bool(args.get("mock_claude") or args.get("mock_mode")),
|
|
107
114
|
mode=mode or args.get("mode"),
|
|
108
115
|
engine=args.get("engine"),
|
|
@@ -155,7 +162,7 @@ def _handle_verify_quote(args: Dict[str, Any]) -> str:
|
|
|
155
162
|
if not content_hash or quote is None:
|
|
156
163
|
raise ValueError("hash and quote are required")
|
|
157
164
|
|
|
158
|
-
hasher = SourceHasher(
|
|
165
|
+
hasher = SourceHasher(_resolve_base_dir(args))
|
|
159
166
|
is_verified, confidence, message = hasher.verify_quote(content_hash, quote)
|
|
160
167
|
return _tool_text({
|
|
161
168
|
"verified": bool(is_verified),
|
|
@@ -173,7 +180,7 @@ def _handle_report_retraction(args: Dict[str, Any]) -> str:
|
|
|
173
180
|
event = args.get("event")
|
|
174
181
|
if not source_hash or not event:
|
|
175
182
|
raise ValueError("hash and event are required")
|
|
176
|
-
base_dir =
|
|
183
|
+
base_dir = _resolve_base_dir(args)
|
|
177
184
|
path = write_retraction(
|
|
178
185
|
base_dir,
|
|
179
186
|
source_hash,
|
|
@@ -188,7 +195,7 @@ def _handle_check_staleness(args: Dict[str, Any]) -> str:
|
|
|
188
195
|
"""Run one claim-degradation pass (retractions -> STALE/SUSPECT + requeue)."""
|
|
189
196
|
from runner.living_dossiers import check_staleness
|
|
190
197
|
|
|
191
|
-
base_dir =
|
|
198
|
+
base_dir = _resolve_base_dir(args)
|
|
192
199
|
scope_ids = args.get("scope_ids")
|
|
193
200
|
return _tool_text(check_staleness(base_dir, scope_ids=scope_ids))
|
|
194
201
|
|
|
@@ -202,7 +209,7 @@ def _handle_set_domain_pack(args: Dict[str, Any]) -> str:
|
|
|
202
209
|
if not pack:
|
|
203
210
|
raise ValueError("pack is required")
|
|
204
211
|
constitution = load_domain_pack(pack) # raises if unknown — fail loudly
|
|
205
|
-
base_dir =
|
|
212
|
+
base_dir = str(_resolve_base_dir(args))
|
|
206
213
|
cfg = load_config(base_dir)
|
|
207
214
|
cfg["domain_pack"] = pack
|
|
208
215
|
save_config(cfg, base_dir)
|
|
@@ -219,7 +226,7 @@ def _handle_export_brief(args: Dict[str, Any]) -> str:
|
|
|
219
226
|
"""Export a proof-carrying research brief (signed, self-contained bundle)."""
|
|
220
227
|
from runner.pcrb import export_brief
|
|
221
228
|
|
|
222
|
-
base_dir =
|
|
229
|
+
base_dir = _resolve_base_dir(args)
|
|
223
230
|
key = args.get("key")
|
|
224
231
|
key_path = Path(args["key_file"]) if args.get("key_file") else None
|
|
225
232
|
path = export_brief(
|
|
@@ -244,7 +251,7 @@ def _handle_verify_brief(args: Dict[str, Any]) -> str:
|
|
|
244
251
|
brief = args.get("brief") or args.get("path")
|
|
245
252
|
if not brief:
|
|
246
253
|
raise ValueError("brief path is required")
|
|
247
|
-
p = Path(brief)
|
|
254
|
+
p = Path(os.path.realpath(str(brief)))
|
|
248
255
|
if not p.exists():
|
|
249
256
|
raise FileNotFoundError(f"no such brief: {p}")
|
|
250
257
|
bundle = json.loads(p.read_text(encoding="utf-8"))
|
|
@@ -252,8 +259,13 @@ def _handle_verify_brief(args: Dict[str, Any]) -> str:
|
|
|
252
259
|
key = None
|
|
253
260
|
if args.get("key"):
|
|
254
261
|
key = args["key"].encode("utf-8")
|
|
255
|
-
elif args.get("key_file")
|
|
256
|
-
|
|
262
|
+
elif args.get("key_file"):
|
|
263
|
+
kf = Path(os.path.realpath(str(args["key_file"])))
|
|
264
|
+
# An explicit key_file that cannot be read must surface as an error, not
|
|
265
|
+
# a silent fallthrough to `alg: "none"`.
|
|
266
|
+
if not kf.is_file():
|
|
267
|
+
raise ValueError(f"key file is not a readable file: {args['key_file']!r}")
|
|
268
|
+
key = kf.read_bytes().strip()
|
|
257
269
|
elif os.environ.get(ENV_KEY):
|
|
258
270
|
key = os.environ[ENV_KEY].encode("utf-8")
|
|
259
271
|
|
|
@@ -264,7 +276,7 @@ def _handle_reindex_claims(args: Dict[str, Any]) -> str:
|
|
|
264
276
|
"""Rebuild the derived claims.sqlite index from .research flat files."""
|
|
265
277
|
from runner.claim_store import reindex
|
|
266
278
|
|
|
267
|
-
base_dir =
|
|
279
|
+
base_dir = _resolve_base_dir(args)
|
|
268
280
|
return _tool_text(reindex(base_dir))
|
|
269
281
|
|
|
270
282
|
|