@heretek-ai/epistemic-swarm 0.2.2 → 0.3.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.
- package/.agents/skills/brainstorming/SKILL.md +13 -0
- package/.agents/skills/code_audit/SKILL.md +13 -0
- package/.agents/skills/epistemic_search/SKILL.md +13 -0
- package/.agents/skills/grilling/SKILL.md +13 -0
- package/.agents/skills/oss_scout/SKILL.md +13 -0
- package/.agents/skills/research_cache/SKILL.md +13 -0
- package/.agents/skills/swarm_config/SKILL.md +13 -0
- package/.claude-plugin/plugin.json +15 -5
- package/.omp/README.md +39 -0
- package/.omp/SYSTEM.md +12 -0
- package/.omp/commands/audit.md +10 -0
- package/.omp/commands/brainstorming.md +12 -0
- package/.omp/commands/grill.md +9 -0
- package/.omp/commands/scout.md +10 -0
- package/.omp/commands/swarm-config.md +10 -0
- package/.omp/commands/swarm.md +9 -0
- package/.omp/hooks/post/epistemic-audit.ts +16 -0
- package/.omp/hooks/pre/epistemic-redirect.ts +21 -0
- package/.omp/prompts/brainstorming.md +8 -0
- package/.omp/prompts/swarm.md +8 -0
- package/MARKETPLACE.md +8 -0
- package/README.md +53 -8
- package/bin/cli.js +27 -3
- package/config/domain_packs/biopharma.json +23 -0
- package/config/domain_packs/legal.json +19 -0
- package/config/domain_packs/quant.json +19 -0
- package/config/mcp-research-servers.json +7 -0
- package/config/mcp_launcher.py +48 -137
- package/config/opencode-snippet.json +58 -3
- package/config/searxng_mcp.py +42 -83
- package/extensions/pi/index.js +196 -28
- package/install.sh +20 -4
- package/package.json +40 -5
- package/plugins/antigravity/README.md +28 -0
- package/plugins/antigravity/agents/alpha-thesis.md +6 -0
- package/plugins/antigravity/agents/beta-antithesis.md +7 -0
- package/plugins/antigravity/agents/brainstormer.md +7 -0
- package/plugins/antigravity/agents/epistemic-auditor.md +5 -0
- package/plugins/antigravity/hooks.json +23 -0
- package/plugins/antigravity/mcp_config.json +33 -0
- package/plugins/antigravity/plugin.json +21 -0
- package/plugins/antigravity/rules/epistemic-integrity.md +6 -0
- package/plugins/antigravity/skills/brainstorming/SKILL.md +13 -0
- package/plugins/antigravity/skills/code_audit/SKILL.md +13 -0
- package/plugins/antigravity/skills/epistemic_search/SKILL.md +13 -0
- package/plugins/antigravity/skills/grilling/SKILL.md +13 -0
- package/plugins/antigravity/skills/oss_scout/SKILL.md +13 -0
- package/plugins/antigravity/skills/research_cache/SKILL.md +13 -0
- package/plugins/antigravity/skills/swarm_config/SKILL.md +13 -0
- package/plugins/codex/AGENTS.md.snippet +10 -0
- package/plugins/codex/README.md +37 -0
- package/plugins/codex/config.toml.snippet +28 -0
- package/plugins/codex/openai.yaml +24 -0
- package/plugins/codex/skills/brainstorming/SKILL.md +13 -0
- package/plugins/codex/skills/code_audit/SKILL.md +13 -0
- package/plugins/codex/skills/epistemic_search/SKILL.md +13 -0
- package/plugins/codex/skills/grilling/SKILL.md +13 -0
- package/plugins/codex/skills/oss_scout/SKILL.md +13 -0
- package/plugins/codex/skills/research_cache/SKILL.md +13 -0
- package/plugins/codex/skills/swarm_config/SKILL.md +13 -0
- package/plugins/gemini/GEMINI.md +15 -0
- package/plugins/gemini/README.md +19 -0
- package/plugins/gemini/commands/audit.toml +6 -0
- package/plugins/gemini/commands/brainstorming.toml +10 -0
- package/plugins/gemini/commands/grill.toml +6 -0
- package/plugins/gemini/commands/scout.toml +7 -0
- package/plugins/gemini/commands/swarm-config.toml +7 -0
- package/plugins/gemini/commands/swarm.toml +8 -0
- package/plugins/gemini/gemini-extension.json +38 -0
- package/plugins/gemini/hooks/hooks.json +11 -0
- package/plugins/gemini/skills/brainstorming/SKILL.md +13 -0
- package/plugins/gemini/skills/code_audit/SKILL.md +13 -0
- package/plugins/gemini/skills/epistemic_search/SKILL.md +13 -0
- package/plugins/gemini/skills/grilling/SKILL.md +13 -0
- package/plugins/gemini/skills/oss_scout/SKILL.md +13 -0
- package/plugins/gemini/skills/research_cache/SKILL.md +13 -0
- package/plugins/gemini/skills/swarm_config/SKILL.md +13 -0
- package/plugins/opencode/index.js +335 -118
- package/prompts/agent_brainstormer.md +97 -0
- 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__/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/auctioneer.py +169 -0
- package/runner/auditor_engine.py +127 -12
- package/runner/claim_store.py +361 -0
- package/runner/claim_witness.py +183 -0
- package/runner/living_dossiers.py +355 -0
- package/runner/mcp_protocol.py +188 -0
- package/runner/mcp_server.py +567 -0
- package/runner/pcrb.py +212 -0
- package/runner/pcrb_verify.py +237 -0
- package/runner/refinement.py +335 -0
- package/runner/research_swarm.py +378 -94
- package/runner/tests/__pycache__/test_auction_order.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_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/fixtures/auction_objective.json +35 -0
- package/runner/tests/fixtures/borderline_claims.json +27 -0
- package/runner/tests/fixtures/divergence_objectives.json +31 -0
- package/runner/tests/test_auction_order.py +173 -0
- package/runner/tests/test_claim_store.py +162 -0
- package/runner/tests/test_claim_witness.py +186 -0
- package/runner/tests/test_domain_packs.py +217 -0
- package/runner/tests/test_fleet_seam.py +113 -0
- package/runner/tests/test_living_dossiers.py +204 -0
- package/runner/tests/test_mcp_server.py +212 -0
- package/runner/tests/test_pcrb.py +240 -0
- package/runner/tests/test_refinement.py +255 -0
- package/runner/tests/test_swarm.py +173 -15
- package/scripts/auction_experiment.py +180 -0
- package/scripts/build_adapters.py +183 -0
- package/scripts/divergence_experiment.py +184 -0
- package/skills/brainstorming/SKILL.md +106 -0
- package/skills/brainstorming/__init__.py +1 -0
- package/skills/brainstorming/scripts/brainstorm.py +200 -0
- package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
- package/skills/swarm_config/SKILL.md +1 -1
- 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 +45 -11
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Living Dossiers: claim degradation ledger (Stream C).
|
|
4
|
+
|
|
5
|
+
Epistemic core idea: a `[VERIFIED]` claim is a time-bounded LOAN against its
|
|
6
|
+
cached source, not a permanent fact. When a source is retracted or revised,
|
|
7
|
+
the loan is called — regardless of whether the local cache still contains a
|
|
8
|
+
matching quote.
|
|
9
|
+
|
|
10
|
+
Source-of-truth discipline (same as the rest of IUMBTEMS):
|
|
11
|
+
- Retraction events are FILES: `.research/retractions/<hash>.json`
|
|
12
|
+
`{hash, event: RETRACTED|REVISED, at, note, supersedes?}`
|
|
13
|
+
- Status transitions are an append-only ledger: `.research/ledger/claim_status.json`
|
|
14
|
+
- Dossiers are NEVER mutated in place — they are the historical record.
|
|
15
|
+
- Requeue instructions land in `.research/requeue.json` for the next swarm pass.
|
|
16
|
+
|
|
17
|
+
Degradation rules (applied after quote verification):
|
|
18
|
+
RETRACTED source -> claim.status = STALE (loan called; quote match irrelevant)
|
|
19
|
+
REVISED source -> claim.status = SUSPECT (needs human/agent re-review + requeue)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
from dataclasses import dataclass, asdict
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
29
|
+
|
|
30
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
31
|
+
PROJECT_ROOT = SCRIPT_DIR.parent
|
|
32
|
+
if str(PROJECT_ROOT) not in sys.path:
|
|
33
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
34
|
+
|
|
35
|
+
from runner.claim_witness import ( # noqa: E402
|
|
36
|
+
STATUS_LIVE,
|
|
37
|
+
STATUS_STALE,
|
|
38
|
+
STATUS_SUSPECT,
|
|
39
|
+
ClaimWitness,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
EVENT_RETRACTED = "RETRACTED"
|
|
43
|
+
EVENT_REVISED = "REVISED"
|
|
44
|
+
VALID_EVENTS = (EVENT_RETRACTED, EVENT_REVISED)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class RetractionEvent:
|
|
49
|
+
hash: str
|
|
50
|
+
event: str
|
|
51
|
+
at: str
|
|
52
|
+
note: str = ""
|
|
53
|
+
supersedes: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
56
|
+
return asdict(self)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class StatusEvent:
|
|
61
|
+
scope_id: str
|
|
62
|
+
claim_id: str
|
|
63
|
+
from_status: str
|
|
64
|
+
to_status: str
|
|
65
|
+
reason: str
|
|
66
|
+
at: str
|
|
67
|
+
|
|
68
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
69
|
+
return asdict(self)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---- retraction events (files are the record) ---------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def write_retraction(
|
|
76
|
+
base_dir: Path,
|
|
77
|
+
source_hash: str,
|
|
78
|
+
event: str,
|
|
79
|
+
note: str = "",
|
|
80
|
+
at: Optional[str] = None,
|
|
81
|
+
supersedes: Optional[str] = None,
|
|
82
|
+
) -> Path:
|
|
83
|
+
"""Record a RETRACTED/REVISED event for a cached source."""
|
|
84
|
+
if event not in VALID_EVENTS:
|
|
85
|
+
raise ValueError(f"event must be one of {VALID_EVENTS}, got {event!r}")
|
|
86
|
+
retr_dir = Path(base_dir) / "retractions"
|
|
87
|
+
retr_dir.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
payload = RetractionEvent(
|
|
89
|
+
hash=source_hash,
|
|
90
|
+
event=event,
|
|
91
|
+
at=at or datetime.now(timezone.utc).isoformat(),
|
|
92
|
+
note=note,
|
|
93
|
+
supersedes=supersedes,
|
|
94
|
+
)
|
|
95
|
+
path = retr_dir / f"{source_hash}.json"
|
|
96
|
+
# Last write wins per hash (a REVISED may follow a RETRACTED); the ledger
|
|
97
|
+
# keeps the full history of claim transitions either way.
|
|
98
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
99
|
+
json.dump(payload.to_dict(), f, indent=2)
|
|
100
|
+
return path
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def load_retractions(base_dir: Path) -> Dict[str, RetractionEvent]:
|
|
104
|
+
"""Load all retraction events, keyed by source hash."""
|
|
105
|
+
retr_dir = Path(base_dir) / "retractions"
|
|
106
|
+
out: Dict[str, RetractionEvent] = {}
|
|
107
|
+
if not retr_dir.exists():
|
|
108
|
+
return out
|
|
109
|
+
for path in sorted(retr_dir.glob("*.json")):
|
|
110
|
+
try:
|
|
111
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
112
|
+
data = json.load(f)
|
|
113
|
+
except (OSError, json.JSONDecodeError):
|
|
114
|
+
continue
|
|
115
|
+
src_hash = data.get("hash") or path.stem
|
|
116
|
+
event = str(data.get("event", "")).upper()
|
|
117
|
+
if event not in VALID_EVENTS:
|
|
118
|
+
continue
|
|
119
|
+
out[src_hash] = RetractionEvent(
|
|
120
|
+
hash=src_hash,
|
|
121
|
+
event=event,
|
|
122
|
+
at=data.get("at") or "",
|
|
123
|
+
note=data.get("note") or "",
|
|
124
|
+
supersedes=data.get("supersedes"),
|
|
125
|
+
)
|
|
126
|
+
return out
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---- degradation -------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def apply_degradation(
|
|
133
|
+
claims: Iterable[ClaimWitness],
|
|
134
|
+
retractions: Dict[str, RetractionEvent],
|
|
135
|
+
scope_id: str = "unknown",
|
|
136
|
+
now: Optional[str] = None,
|
|
137
|
+
) -> Tuple[List[ClaimWitness], List[StatusEvent]]:
|
|
138
|
+
"""Mark claims STALE/SUSPECT per retraction events.
|
|
139
|
+
|
|
140
|
+
Returns (degraded_claims, status_events). ClaimWitness objects are
|
|
141
|
+
updated in memory only — callers persist to the ledger, never into the
|
|
142
|
+
source dossier files.
|
|
143
|
+
"""
|
|
144
|
+
now = now or datetime.now(timezone.utc).isoformat()
|
|
145
|
+
events: List[StatusEvent] = []
|
|
146
|
+
out: List[ClaimWitness] = []
|
|
147
|
+
|
|
148
|
+
for claim in claims:
|
|
149
|
+
prior = claim.status or STATUS_LIVE
|
|
150
|
+
src_hash = claim.source_hash
|
|
151
|
+
ev = retractions.get(src_hash) if src_hash else None
|
|
152
|
+
|
|
153
|
+
new_status = prior
|
|
154
|
+
reason = ""
|
|
155
|
+
if ev is not None:
|
|
156
|
+
if ev.event == EVENT_RETRACTED:
|
|
157
|
+
new_status = STATUS_STALE
|
|
158
|
+
reason = f"source {src_hash} RETRACTED: {ev.note or 'no note'}"
|
|
159
|
+
elif ev.event == EVENT_REVISED:
|
|
160
|
+
new_status = STATUS_SUSPECT
|
|
161
|
+
reason = f"source {src_hash} REVISED: {ev.note or 'no note'}"
|
|
162
|
+
|
|
163
|
+
if new_status != prior:
|
|
164
|
+
claim.status = new_status
|
|
165
|
+
events.append(
|
|
166
|
+
StatusEvent(
|
|
167
|
+
scope_id=scope_id,
|
|
168
|
+
claim_id=claim.claim_id,
|
|
169
|
+
from_status=prior,
|
|
170
|
+
to_status=new_status,
|
|
171
|
+
reason=reason,
|
|
172
|
+
at=now,
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
out.append(claim)
|
|
176
|
+
|
|
177
|
+
return out, events
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---- ledger + requeue sidecars -----------------------------------------
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def append_ledger(base_dir: Path, events: List[StatusEvent]) -> Path:
|
|
184
|
+
"""Append status transitions to the append-only ledger (never rewrite)."""
|
|
185
|
+
ledger_dir = Path(base_dir) / "ledger"
|
|
186
|
+
ledger_dir.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
path = ledger_dir / "claim_status.json"
|
|
188
|
+
existing: List[Dict[str, Any]] = []
|
|
189
|
+
if path.exists():
|
|
190
|
+
try:
|
|
191
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
192
|
+
existing = json.load(f)
|
|
193
|
+
if not isinstance(existing, list):
|
|
194
|
+
existing = []
|
|
195
|
+
except (OSError, json.JSONDecodeError):
|
|
196
|
+
existing = []
|
|
197
|
+
existing.extend(e.to_dict() for e in events)
|
|
198
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
199
|
+
json.dump(existing, f, indent=2)
|
|
200
|
+
return path
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def queue_requeue(base_dir: Path, scope_id: str, reason: str = "claim degradation") -> Path:
|
|
204
|
+
"""Record that a scope needs a re-run on the next swarm pass."""
|
|
205
|
+
path = Path(base_dir) / "requeue.json"
|
|
206
|
+
entries: List[Dict[str, Any]] = []
|
|
207
|
+
if path.exists():
|
|
208
|
+
try:
|
|
209
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
210
|
+
entries = json.load(f)
|
|
211
|
+
if not isinstance(entries, list):
|
|
212
|
+
entries = []
|
|
213
|
+
except (OSError, json.JSONDecodeError):
|
|
214
|
+
entries = []
|
|
215
|
+
if not any(e.get("scope_id") == scope_id for e in entries):
|
|
216
|
+
entries.append(
|
|
217
|
+
{
|
|
218
|
+
"scope_id": scope_id,
|
|
219
|
+
"reason": reason,
|
|
220
|
+
"queued_at": datetime.now(timezone.utc).isoformat(),
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
224
|
+
json.dump(entries, f, indent=2)
|
|
225
|
+
return path
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def load_requeue(base_dir: Path) -> List[Dict[str, Any]]:
|
|
229
|
+
path = Path(base_dir) / "requeue.json"
|
|
230
|
+
if not path.exists():
|
|
231
|
+
return []
|
|
232
|
+
try:
|
|
233
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
234
|
+
data = json.load(f)
|
|
235
|
+
return data if isinstance(data, list) else []
|
|
236
|
+
except (OSError, json.JSONDecodeError):
|
|
237
|
+
return []
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ---- one-pass staleness check (the probe's unit of work) ----------------
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def check_staleness(base_dir: Path, scope_ids: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
244
|
+
"""Run one degradation pass over scope dossiers.
|
|
245
|
+
|
|
246
|
+
For each scope with alpha/beta dossiers, fold claims, join against
|
|
247
|
+
retraction events, write the ledger, mirror into the claim store, and
|
|
248
|
+
queue re-runs for scopes that degraded. Dossiers themselves are left
|
|
249
|
+
untouched.
|
|
250
|
+
|
|
251
|
+
Returns a summary dict suitable for JSON tool output.
|
|
252
|
+
"""
|
|
253
|
+
from runner.claim_witness import claims_from_dossier, load_dossier
|
|
254
|
+
|
|
255
|
+
base_dir = Path(base_dir)
|
|
256
|
+
retractions = load_retractions(base_dir)
|
|
257
|
+
scratch = base_dir / "scratchpads"
|
|
258
|
+
|
|
259
|
+
if scope_ids is None:
|
|
260
|
+
if not scratch.exists():
|
|
261
|
+
return {"scopes": [], "retractions": len(retractions), "degraded": 0}
|
|
262
|
+
scope_dirs = sorted(p for p in scratch.iterdir() if p.is_dir())
|
|
263
|
+
else:
|
|
264
|
+
scope_dirs = [scratch / s for s in scope_ids]
|
|
265
|
+
|
|
266
|
+
all_events: List[StatusEvent] = []
|
|
267
|
+
per_scope: List[Dict[str, Any]] = []
|
|
268
|
+
degraded_scopes = set()
|
|
269
|
+
|
|
270
|
+
for scope_dir in scope_dirs:
|
|
271
|
+
if not scope_dir.is_dir():
|
|
272
|
+
continue
|
|
273
|
+
scope_id = scope_dir.name
|
|
274
|
+
scope_event_count = 0
|
|
275
|
+
claim_sets = 0
|
|
276
|
+
|
|
277
|
+
for dossier_name in ("alpha_dossier.json", "beta_dossier.json"):
|
|
278
|
+
dpath = scope_dir / dossier_name
|
|
279
|
+
if not dpath.exists():
|
|
280
|
+
continue
|
|
281
|
+
claim_sets += 1
|
|
282
|
+
dossier = load_dossier(dpath)
|
|
283
|
+
claims = claims_from_dossier(dossier)
|
|
284
|
+
degraded, events = apply_degradation(
|
|
285
|
+
claims, retractions, scope_id=scope_id
|
|
286
|
+
)
|
|
287
|
+
all_events.extend(events)
|
|
288
|
+
scope_event_count += len(events)
|
|
289
|
+
if events:
|
|
290
|
+
degraded_scopes.add(scope_id)
|
|
291
|
+
|
|
292
|
+
per_scope.append(
|
|
293
|
+
{
|
|
294
|
+
"scope_id": scope_id,
|
|
295
|
+
"claim_sets": claim_sets,
|
|
296
|
+
"status_events": scope_event_count,
|
|
297
|
+
"degraded": scope_id in degraded_scopes,
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if all_events:
|
|
302
|
+
append_ledger(base_dir, all_events)
|
|
303
|
+
# Mirror into the derived claim store (best-effort; never fatal).
|
|
304
|
+
try:
|
|
305
|
+
from runner.claim_store import ClaimStore
|
|
306
|
+
|
|
307
|
+
store = ClaimStore(base_dir)
|
|
308
|
+
for e in all_events:
|
|
309
|
+
store.record_status_event(
|
|
310
|
+
e.scope_id, e.claim_id, e.from_status, e.to_status, e.reason, e.at
|
|
311
|
+
)
|
|
312
|
+
store.close()
|
|
313
|
+
except Exception as exc: # noqa: BLE001
|
|
314
|
+
print(f"[living-dossiers] claim store mirror skipped: {exc}", file=sys.stderr)
|
|
315
|
+
|
|
316
|
+
for scope_id in sorted(degraded_scopes):
|
|
317
|
+
queue_requeue(base_dir, scope_id, reason="claim degradation (retraction event)")
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
"retractions": len(retractions),
|
|
321
|
+
"scopes": per_scope,
|
|
322
|
+
"degraded_scopes": sorted(degraded_scopes),
|
|
323
|
+
"status_events": len(all_events),
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
328
|
+
import argparse
|
|
329
|
+
|
|
330
|
+
parser = argparse.ArgumentParser(description="IUMBTEMS living dossiers ledger")
|
|
331
|
+
parser.add_argument("--dir", default=".research")
|
|
332
|
+
sub = parser.add_subparsers(dest="command")
|
|
333
|
+
|
|
334
|
+
rep = sub.add_parser("report", help="Record a retraction/retraction event")
|
|
335
|
+
rep.add_argument("--hash", required=True)
|
|
336
|
+
rep.add_argument("--event", required=True, choices=list(VALID_EVENTS))
|
|
337
|
+
rep.add_argument("--note", default="")
|
|
338
|
+
|
|
339
|
+
sub.add_parser("check", help="Run one staleness/degradation pass")
|
|
340
|
+
args = parser.parse_args(argv)
|
|
341
|
+
|
|
342
|
+
base = Path(args.dir)
|
|
343
|
+
if args.command == "report":
|
|
344
|
+
path = write_retraction(base, args.hash, args.event, note=args.note)
|
|
345
|
+
print(json.dumps({"status": "recorded", "path": str(path)}))
|
|
346
|
+
return 0
|
|
347
|
+
if args.command == "check":
|
|
348
|
+
print(json.dumps(check_staleness(base), indent=2))
|
|
349
|
+
return 0
|
|
350
|
+
print(json.dumps(check_staleness(base), indent=2))
|
|
351
|
+
return 0
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
if __name__ == "__main__":
|
|
355
|
+
sys.exit(main())
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Shared MCP stdio JSON-RPC protocol loop for IUMBTEMS.
|
|
4
|
+
|
|
5
|
+
Extracted from the duplicated hand-rolled servers in config/mcp_launcher.py
|
|
6
|
+
(the zero-key fallback) and config/searxng_mcp.py so the first-party tool
|
|
7
|
+
server (runner/mcp_server.py) can share one framing/dispatch implementation.
|
|
8
|
+
|
|
9
|
+
Protocol contract (kept byte-compatible with the previous inline loops):
|
|
10
|
+
- protocolVersion: "2024-11-05"
|
|
11
|
+
- Methods: initialize, notifications/initialized (no reply), tools/list,
|
|
12
|
+
tools/call. Unknown methods get JSON-RPC error -32601.
|
|
13
|
+
- Framing: Content-Length header frames AND newline-delimited JSON are both
|
|
14
|
+
accepted; a reply is framed the same way its request arrived.
|
|
15
|
+
- tools/call success payload: {"content": [{"type": "text", "text": ...}]}.
|
|
16
|
+
|
|
17
|
+
Resilience note (intentional delta from the old inline loops): a tool handler
|
|
18
|
+
that raises now returns JSON-RPC error -32603 instead of killing the server.
|
|
19
|
+
Malformed messages are logged and skipped; any other stream error ends the
|
|
20
|
+
loop (Content-Length desync is not recoverable).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
from typing import Any, Callable, Dict, List, Optional, TextIO, Tuple
|
|
26
|
+
|
|
27
|
+
PROTOCOL_VERSION = "2024-11-05"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ToolSpec:
|
|
31
|
+
"""One MCP tool: public manifest fields plus the handler behind it."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
name: str,
|
|
36
|
+
description: str,
|
|
37
|
+
input_schema: Dict[str, Any],
|
|
38
|
+
handler: Callable[[Dict[str, Any]], str],
|
|
39
|
+
aliases: Tuple[str, ...] = (),
|
|
40
|
+
):
|
|
41
|
+
self.name = name
|
|
42
|
+
self.description = description
|
|
43
|
+
self.input_schema = input_schema
|
|
44
|
+
self.handler = handler
|
|
45
|
+
self.aliases = tuple(aliases)
|
|
46
|
+
|
|
47
|
+
def to_manifest(self) -> Dict[str, Any]:
|
|
48
|
+
return {
|
|
49
|
+
"name": self.name,
|
|
50
|
+
"description": self.description,
|
|
51
|
+
"inputSchema": self.input_schema,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def read_message(stream: TextIO) -> Optional[Tuple[Dict[str, Any], bool]]:
|
|
56
|
+
"""Read one JSON-RPC message. Returns (request, use_headers); None on EOF."""
|
|
57
|
+
while True:
|
|
58
|
+
line = stream.readline()
|
|
59
|
+
if not line:
|
|
60
|
+
return None
|
|
61
|
+
line = line.strip()
|
|
62
|
+
if not line:
|
|
63
|
+
continue
|
|
64
|
+
if line.startswith("Content-Length:"):
|
|
65
|
+
length = int(line.split(":", 1)[1].strip())
|
|
66
|
+
stream.readline() # blank separator line
|
|
67
|
+
body = stream.read(length)
|
|
68
|
+
return json.loads(body), True
|
|
69
|
+
return json.loads(line), False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def write_message(stream: TextIO, message: Dict[str, Any], use_headers: bool) -> None:
|
|
73
|
+
"""Write one JSON-RPC message, matching the request's framing."""
|
|
74
|
+
out = json.dumps(message)
|
|
75
|
+
if use_headers:
|
|
76
|
+
stream.write(f"Content-Length: {len(out)}\r\n\r\n{out}")
|
|
77
|
+
else:
|
|
78
|
+
stream.write(out + "\n")
|
|
79
|
+
stream.flush()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class StdioJsonRpcServer:
|
|
83
|
+
"""Dispatch loop over a fixed tool registry."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
server_name: str,
|
|
88
|
+
version: str,
|
|
89
|
+
tools: List[ToolSpec],
|
|
90
|
+
protocol_version: str = PROTOCOL_VERSION,
|
|
91
|
+
log_prefix: Optional[str] = None,
|
|
92
|
+
):
|
|
93
|
+
self.server_name = server_name
|
|
94
|
+
self.version = version
|
|
95
|
+
self.tools = list(tools)
|
|
96
|
+
self.protocol_version = protocol_version
|
|
97
|
+
self.log_prefix = log_prefix or f"[{server_name}]"
|
|
98
|
+
self._by_name: Dict[str, ToolSpec] = {}
|
|
99
|
+
for spec in self.tools:
|
|
100
|
+
self._by_name[spec.name] = spec
|
|
101
|
+
for alias in spec.aliases:
|
|
102
|
+
self._by_name[alias] = spec
|
|
103
|
+
|
|
104
|
+
# --- request handling -------------------------------------------------
|
|
105
|
+
|
|
106
|
+
def handle(self, req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
107
|
+
"""Return a response dict, or None for notifications (no reply)."""
|
|
108
|
+
req_id = req.get("id")
|
|
109
|
+
method = req.get("method")
|
|
110
|
+
params = req.get("params") or {}
|
|
111
|
+
|
|
112
|
+
if method is not None and str(method).startswith("notifications/"):
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
if method == "initialize":
|
|
116
|
+
return self._reply(
|
|
117
|
+
req_id,
|
|
118
|
+
{
|
|
119
|
+
"protocolVersion": self.protocol_version,
|
|
120
|
+
"serverInfo": {
|
|
121
|
+
"name": self.server_name,
|
|
122
|
+
"version": self.version,
|
|
123
|
+
},
|
|
124
|
+
"capabilities": {"tools": {}},
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
if method == "tools/list":
|
|
128
|
+
return self._reply(
|
|
129
|
+
req_id, {"tools": [t.to_manifest() for t in self.tools]}
|
|
130
|
+
)
|
|
131
|
+
if method == "tools/call":
|
|
132
|
+
return self._call(req_id, params)
|
|
133
|
+
return self._error(req_id, -32601, f"Method {method} not handled")
|
|
134
|
+
|
|
135
|
+
def _call(self, req_id: Any, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
136
|
+
tool_name = params.get("name")
|
|
137
|
+
args = params.get("arguments") or {}
|
|
138
|
+
spec = self._by_name.get(tool_name)
|
|
139
|
+
if spec is None:
|
|
140
|
+
return self._error(req_id, -32601, f"Tool {tool_name} not found")
|
|
141
|
+
try:
|
|
142
|
+
text = spec.handler(args)
|
|
143
|
+
except Exception as exc: # noqa: BLE001 - surface, do not kill the server
|
|
144
|
+
return self._error(req_id, -32603, f"{type(exc).__name__}: {exc}")
|
|
145
|
+
return self._reply(
|
|
146
|
+
req_id, {"content": [{"type": "text", "text": text}]}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def _reply(req_id: Any, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
151
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": result}
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def _error(req_id: Any, code: int, message: str) -> Dict[str, Any]:
|
|
155
|
+
return {
|
|
156
|
+
"jsonrpc": "2.0",
|
|
157
|
+
"id": req_id,
|
|
158
|
+
"error": {"code": code, "message": message},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# --- stdio loop -------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def serve_forever(
|
|
164
|
+
self,
|
|
165
|
+
stdin: Optional[TextIO] = None,
|
|
166
|
+
stdout: Optional[TextIO] = None,
|
|
167
|
+
stderr: Optional[TextIO] = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
stdin = stdin if stdin is not None else sys.stdin
|
|
170
|
+
stdout = stdout if stdout is not None else sys.stdout
|
|
171
|
+
stderr = stderr if stderr is not None else sys.stderr
|
|
172
|
+
while True:
|
|
173
|
+
try:
|
|
174
|
+
parsed = read_message(stdin)
|
|
175
|
+
if parsed is None:
|
|
176
|
+
break
|
|
177
|
+
req, use_headers = parsed
|
|
178
|
+
res = self.handle(req)
|
|
179
|
+
if res is not None:
|
|
180
|
+
write_message(stdout, res, use_headers)
|
|
181
|
+
except json.JSONDecodeError as exc:
|
|
182
|
+
stderr.write(f"{self.log_prefix} Skipping malformed message: {exc}\n")
|
|
183
|
+
stderr.flush()
|
|
184
|
+
continue
|
|
185
|
+
except Exception as exc: # noqa: BLE001 - stream desync ends the loop
|
|
186
|
+
stderr.write(f"{self.log_prefix} Error handling request: {exc}\n")
|
|
187
|
+
stderr.flush()
|
|
188
|
+
break
|