@heretek-ai/epistemic-swarm 0.7.0 → 0.7.3
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 +1 -0
- package/extensions/pi/index.js +7 -1
- package/package.json +1 -1
- package/plugins/darkharvest/skills/darkharvest/scripts/harvest.py +76 -39
- package/plugins/factory/skills/factory/scripts/factory.py +1 -1
- package/plugins/opencode/index.js +66 -6
- package/plugins/opencode/tui.js +66 -38
- 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/claim_store.py +15 -4
- package/runner/living_dossiers.py +74 -56
- package/runner/mcp_server.py +37 -2
- package/runner/pcrb.py +53 -29
- package/runner/refinement.py +70 -27
- package/runner/research_swarm.py +349 -127
- package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_backends.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_claude_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_factory.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_backends.py +325 -0
- package/runner/tests/test_sweep_regressions.py +71 -38
- 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/build_adapters.py +27 -18
- package/skills/darkharvest/scripts/harvest.py +76 -39
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/epistemic_search/scripts/search.py +29 -19
- package/skills/epistemic_search/scripts/webcache.py +29 -17
- package/skills/factory/scripts/factory.py +1 -1
- 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/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
- package/skills/swarm_config/configure.py +70 -37
package/bin/cli.js
CHANGED
|
@@ -74,6 +74,7 @@ Options:
|
|
|
74
74
|
--mode <mode> Operating mode (research, audit, scout, hybrid, brainstorm)
|
|
75
75
|
--engine <engine> Search engine (duckduckgo, brave, firecrawl, searxng)
|
|
76
76
|
--depth <n> Max dialectic iterations (1-4)
|
|
77
|
+
--backend <b> Agent runtime: auto (host-native), claude, opencode
|
|
77
78
|
--dir <path> Path to .research workspace directory (default: .research)
|
|
78
79
|
|
|
79
80
|
Examples:
|
package/extensions/pi/index.js
CHANGED
|
@@ -49,7 +49,13 @@ function callMcp(tool, args = {}, ctx = {}) {
|
|
|
49
49
|
{
|
|
50
50
|
encoding: 'utf-8',
|
|
51
51
|
cwd: ctx.cwd || process.cwd(),
|
|
52
|
-
|
|
52
|
+
// Project root: evidence defaults under <project>/.research instead
|
|
53
|
+
// of the server process cwd.
|
|
54
|
+
env: {
|
|
55
|
+
...process.env,
|
|
56
|
+
PYTHONPATH: PKG_ROOT,
|
|
57
|
+
IUMBTEMS_PROJECT_DIR: ctx.cwd || process.cwd(),
|
|
58
|
+
},
|
|
53
59
|
}
|
|
54
60
|
);
|
|
55
61
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heretek-ai/epistemic-swarm",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "IUMBTEMS: I Use My Brain To Express My Self — High-Integrity Dialectic Research Agent Harness for Claude Code, OpenCode V2, Pi, OMP (oh-my-pi), Gemini CLI, Codex CLI, and AntiGravity",
|
|
5
5
|
"main": "bin/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -59,11 +59,19 @@ TEXT_EXTS = {".md", ".json", ".toml", ".yaml", ".yml", ".txt", ".py", ".js", ".t
|
|
|
59
59
|
DEFAULT_MAX_FILES = 120
|
|
60
60
|
DEFAULT_MAX_BYTES = 400_000
|
|
61
61
|
DEFAULT_TIMEOUT = 120
|
|
62
|
+
DEFAULT_BASE_DIR = ".research"
|
|
63
|
+
MAX_SCAN_DEPTH = 3
|
|
64
|
+
MAX_SINGLE_FILE_BYTES = 100_000
|
|
65
|
+
SNIPPET_CHARS = 4000
|
|
66
|
+
|
|
67
|
+
SPDX_APACHE2 = "Apache-2.0"
|
|
68
|
+
PERMISSIVE_SPDX = frozenset({"MIT", SPDX_APACHE2, "BSD-3-Clause", "ISC"})
|
|
69
|
+
COPYLEFT_SPDX = frozenset({"GPL", "AGPL"})
|
|
62
70
|
|
|
63
71
|
SPDX_MAP = {
|
|
64
72
|
"mit": "MIT",
|
|
65
|
-
"apache-2.0":
|
|
66
|
-
"apache 2.0":
|
|
73
|
+
"apache-2.0": SPDX_APACHE2,
|
|
74
|
+
"apache 2.0": SPDX_APACHE2,
|
|
67
75
|
"bsd-3-clause": "BSD-3-Clause",
|
|
68
76
|
"bsd 3-clause": "BSD-3-Clause",
|
|
69
77
|
"isc": "ISC",
|
|
@@ -97,43 +105,74 @@ def detect_license(text):
|
|
|
97
105
|
|
|
98
106
|
|
|
99
107
|
def scan_tree(root, max_files=DEFAULT_MAX_FILES, max_bytes=DEFAULT_MAX_BYTES):
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
manifests = {}
|
|
103
|
-
license_text = ""
|
|
104
|
-
readme_head = ""
|
|
108
|
+
root = Path(root)
|
|
109
|
+
state = _ScanState()
|
|
105
110
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
106
111
|
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
107
112
|
rel = Path(dirpath).relative_to(root)
|
|
108
|
-
|
|
109
|
-
if depth > 3:
|
|
113
|
+
if (len(rel.parts) if str(rel) != "." else 0) > MAX_SCAN_DEPTH:
|
|
110
114
|
dirnames[:] = []
|
|
111
115
|
continue
|
|
116
|
+
prefix = "" if str(rel) == "." else str(rel) + os.sep
|
|
112
117
|
for f in sorted(filenames):
|
|
113
|
-
if
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
118
|
+
if state.capped(max_files, max_bytes):
|
|
119
|
+
return state.finish(truncated=True)
|
|
120
|
+
_scan_file(dirpath, prefix + f, state)
|
|
121
|
+
return state.finish(truncated=False)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class _ScanState:
|
|
125
|
+
"""Mutable accumulator for one scan_tree pass (keeps helpers small)."""
|
|
126
|
+
|
|
127
|
+
def __init__(self):
|
|
128
|
+
self.entries = []
|
|
129
|
+
self.total_bytes = 0
|
|
130
|
+
self.manifests = {}
|
|
131
|
+
self.license_text = ""
|
|
132
|
+
self.readme_head = ""
|
|
133
|
+
|
|
134
|
+
def capped(self, max_files, max_bytes):
|
|
135
|
+
return len(self.entries) >= max_files or self.total_bytes >= max_bytes
|
|
136
|
+
|
|
137
|
+
def finish(self, truncated):
|
|
138
|
+
return (
|
|
139
|
+
self.entries,
|
|
140
|
+
self.manifests,
|
|
141
|
+
self.license_text,
|
|
142
|
+
self.readme_head,
|
|
143
|
+
truncated,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _scan_file(dirpath, rel_entry, state):
|
|
148
|
+
filename = os.path.basename(rel_entry)
|
|
149
|
+
try:
|
|
150
|
+
size = (Path(dirpath) / filename).stat().st_size
|
|
151
|
+
except OSError:
|
|
152
|
+
return
|
|
153
|
+
if size > MAX_SINGLE_FILE_BYTES:
|
|
154
|
+
return
|
|
155
|
+
state.entries.append(rel_entry)
|
|
156
|
+
if filename in MANIFEST_FILES or Path(filename).suffix in TEXT_EXTS:
|
|
157
|
+
_capture_text(dirpath, filename, state)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _capture_text(dirpath, filename, state):
|
|
161
|
+
try:
|
|
162
|
+
content = (Path(dirpath) / filename).read_text(
|
|
163
|
+
encoding="utf-8", errors="replace"
|
|
164
|
+
)
|
|
165
|
+
except OSError:
|
|
166
|
+
return
|
|
167
|
+
state.total_bytes += len(content.encode("utf-8", errors="replace"))
|
|
168
|
+
snippet = content[:SNIPPET_CHARS]
|
|
169
|
+
if filename in MANIFEST_FILES and filename not in state.manifests:
|
|
170
|
+
state.manifests[filename] = snippet
|
|
171
|
+
lowered = filename.lower()
|
|
172
|
+
if lowered.startswith("license") and not state.license_text:
|
|
173
|
+
state.license_text = snippet
|
|
174
|
+
if lowered == "readme.md" and not state.readme_head:
|
|
175
|
+
state.readme_head = snippet
|
|
137
176
|
|
|
138
177
|
|
|
139
178
|
def harvest_repo(
|
|
@@ -141,7 +180,7 @@ def harvest_repo(
|
|
|
141
180
|
max_files=DEFAULT_MAX_FILES,
|
|
142
181
|
max_bytes=DEFAULT_MAX_BYTES,
|
|
143
182
|
timeout=DEFAULT_TIMEOUT,
|
|
144
|
-
base_dir=
|
|
183
|
+
base_dir=DEFAULT_BASE_DIR,
|
|
145
184
|
):
|
|
146
185
|
started = datetime.now(timezone.utc).isoformat()
|
|
147
186
|
tmp = tempfile.mkdtemp(prefix="darkharvest-")
|
|
@@ -162,9 +201,7 @@ def harvest_repo(
|
|
|
162
201
|
)
|
|
163
202
|
spdx = detect_license(license_text + manifests.get("package.json", ""))
|
|
164
203
|
harvestable = (
|
|
165
|
-
"depend-or-vendor"
|
|
166
|
-
if spdx in ("MIT", "Apache-2.0", "BSD-3-Clause", "ISC")
|
|
167
|
-
else "clean-room-rebuild-only"
|
|
204
|
+
"depend-or-vendor" if spdx in PERMISSIVE_SPDX else "clean-room-rebuild-only"
|
|
168
205
|
)
|
|
169
206
|
hasher = SourceHasher(Path(base_dir))
|
|
170
207
|
cache_blob = (
|
|
@@ -174,7 +211,7 @@ def harvest_repo(
|
|
|
174
211
|
)
|
|
175
212
|
source_hash = hasher.store_source(url, cache_blob, f"Darkharvest {url}")
|
|
176
213
|
warnings = []
|
|
177
|
-
if spdx in
|
|
214
|
+
if spdx in COPYLEFT_SPDX:
|
|
178
215
|
warnings.append(
|
|
179
216
|
"⚠️ LICENSE: strong copyleft — spec rebuild only, never vendor"
|
|
180
217
|
)
|
|
@@ -209,7 +246,7 @@ def main():
|
|
|
209
246
|
ap.add_argument("--max-files", type=int, default=DEFAULT_MAX_FILES)
|
|
210
247
|
ap.add_argument("--max-bytes", type=int, default=DEFAULT_MAX_BYTES)
|
|
211
248
|
ap.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT)
|
|
212
|
-
ap.add_argument("--dir", default=
|
|
249
|
+
ap.add_argument("--dir", default=DEFAULT_BASE_DIR)
|
|
213
250
|
args = ap.parse_args()
|
|
214
251
|
|
|
215
252
|
if not re.match(r"^https?://", args.repo):
|
|
@@ -150,7 +150,7 @@ def cmd_expansion(args):
|
|
|
150
150
|
state["expansion"]["loops_planned"] = n
|
|
151
151
|
save_state(args.run, state)
|
|
152
152
|
done = 0
|
|
153
|
-
for
|
|
153
|
+
for _ in range(1, n + 1):
|
|
154
154
|
if stop_requested(args.run):
|
|
155
155
|
print(f"🛑 STOP file present — halting after {done}/{n} loops.")
|
|
156
156
|
break
|
|
@@ -71,7 +71,16 @@ function callMcp(tool, args = {}, cwd = undefined) {
|
|
|
71
71
|
[MCP_SERVER, 'call', tool, JSON.stringify(args || {})],
|
|
72
72
|
{
|
|
73
73
|
cwd: cwd || process.cwd(),
|
|
74
|
-
|
|
74
|
+
// Host hint: the runner defaults to the host-native backend
|
|
75
|
+
// (opencode run here), unless the caller passes backend explicitly.
|
|
76
|
+
// Project root: evidence defaults under <project>/.research instead
|
|
77
|
+
// of the server process cwd.
|
|
78
|
+
env: {
|
|
79
|
+
...process.env,
|
|
80
|
+
PYTHONPATH: PKG_ROOT,
|
|
81
|
+
IUMBTEMS_HOST: 'opencode',
|
|
82
|
+
IUMBTEMS_PROJECT_DIR: cwd || process.cwd(),
|
|
83
|
+
},
|
|
75
84
|
}
|
|
76
85
|
);
|
|
77
86
|
} catch (err) {
|
|
@@ -160,6 +169,11 @@ const TOOL_CATALOG = [
|
|
|
160
169
|
description: 'Run in mock/dry-run mode without external API charges',
|
|
161
170
|
default: false,
|
|
162
171
|
},
|
|
172
|
+
backend: {
|
|
173
|
+
type: 'string',
|
|
174
|
+
enum: ['auto', 'claude', 'opencode'],
|
|
175
|
+
description: 'Agent runtime backend (default auto = host-native)',
|
|
176
|
+
},
|
|
163
177
|
},
|
|
164
178
|
required: ['objective'],
|
|
165
179
|
},
|
|
@@ -180,6 +194,11 @@ const TOOL_CATALOG = [
|
|
|
180
194
|
description: 'Run in mock mode without invoking LLM tokens',
|
|
181
195
|
default: false,
|
|
182
196
|
},
|
|
197
|
+
backend: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
enum: ['auto', 'claude', 'opencode'],
|
|
200
|
+
description: 'Agent runtime backend (default auto = host-native)',
|
|
201
|
+
},
|
|
183
202
|
},
|
|
184
203
|
required: ['target'],
|
|
185
204
|
},
|
|
@@ -200,6 +219,11 @@ const TOOL_CATALOG = [
|
|
|
200
219
|
description: 'Run in mock mode without invoking LLM tokens',
|
|
201
220
|
default: false,
|
|
202
221
|
},
|
|
222
|
+
backend: {
|
|
223
|
+
type: 'string',
|
|
224
|
+
enum: ['auto', 'claude', 'opencode'],
|
|
225
|
+
description: 'Agent runtime backend (default auto = host-native)',
|
|
226
|
+
},
|
|
203
227
|
},
|
|
204
228
|
required: ['feature'],
|
|
205
229
|
},
|
|
@@ -220,6 +244,11 @@ const TOOL_CATALOG = [
|
|
|
220
244
|
description: 'Run in mock/dry-run mode without external API charges',
|
|
221
245
|
default: false,
|
|
222
246
|
},
|
|
247
|
+
backend: {
|
|
248
|
+
type: 'string',
|
|
249
|
+
enum: ['auto', 'claude', 'opencode'],
|
|
250
|
+
description: 'Agent runtime backend (default auto = host-native)',
|
|
251
|
+
},
|
|
223
252
|
},
|
|
224
253
|
required: ['objective'],
|
|
225
254
|
},
|
|
@@ -253,6 +282,11 @@ const TOOL_CATALOG = [
|
|
|
253
282
|
description: 'Run in mock mode without invoking LLM tokens',
|
|
254
283
|
default: false,
|
|
255
284
|
},
|
|
285
|
+
backend: {
|
|
286
|
+
type: 'string',
|
|
287
|
+
enum: ['auto', 'claude', 'opencode'],
|
|
288
|
+
description: 'Agent runtime backend (default auto = host-native)',
|
|
289
|
+
},
|
|
256
290
|
},
|
|
257
291
|
required: ['objective'],
|
|
258
292
|
},
|
|
@@ -564,8 +598,14 @@ export function commandCatalog() {
|
|
|
564
598
|
return out;
|
|
565
599
|
}
|
|
566
600
|
|
|
567
|
-
/** Tool map (object form) for the server hook; array catalog stays canonical.
|
|
568
|
-
|
|
601
|
+
/** Tool map (object form) for the server hook; array catalog stays canonical.
|
|
602
|
+
*
|
|
603
|
+
* `hostRoot` is the host-reported project directory (host.location.directory).
|
|
604
|
+
* The tool context's own cwd is absent on some host builds — observed live:
|
|
605
|
+
* every dispatch fell back to process.cwd() (/home/john) and evidence escaped
|
|
606
|
+
* the project tree. Prefer toolContext.cwd, then hostRoot, then process.cwd().
|
|
607
|
+
*/
|
|
608
|
+
function buildToolMap(hostRoot = undefined) {
|
|
569
609
|
return Object.fromEntries(
|
|
570
610
|
TOOL_CATALOG.map((tool) => [
|
|
571
611
|
tool.name,
|
|
@@ -573,7 +613,11 @@ function buildToolMap() {
|
|
|
573
613
|
...tool,
|
|
574
614
|
options: { codemode: false },
|
|
575
615
|
execute: async (args = {}, toolContext = undefined) =>
|
|
576
|
-
callMcp(
|
|
616
|
+
callMcp(
|
|
617
|
+
tool.name,
|
|
618
|
+
normalizeArgs(tool.name, args),
|
|
619
|
+
toolContext?.cwd || hostRoot
|
|
620
|
+
),
|
|
577
621
|
},
|
|
578
622
|
])
|
|
579
623
|
);
|
|
@@ -852,15 +896,31 @@ async function registerHostCommands(host) {
|
|
|
852
896
|
|
|
853
897
|
async function registerHostTools(host) {
|
|
854
898
|
if (typeof host?.tool?.transform !== 'function') return [];
|
|
899
|
+
// Accepted asymmetry (parity spec section 5): this plugin shape
|
|
900
|
+
// (command/tool transform + session hooks + event subscriptions) has no
|
|
901
|
+
// tool.execute.before registration point, so host webfetch/websearch calls
|
|
902
|
+
// cannot be intercepted the way Claude Code's hooks/hooks.json does.
|
|
903
|
+
// Steering lives in command templates (epistemic search first); a host API
|
|
904
|
+
// for pre-execution guards would close this for real.
|
|
905
|
+
//
|
|
906
|
+
// hostRoot: the host's project directory. toolContext.cwd is absent on
|
|
907
|
+
// some host builds (observed live: dispatches fell back to process.cwd()
|
|
908
|
+
// = /home/john and evidence escaped the project tree), so capture the
|
|
909
|
+
// host-reported directory here as the fallback.
|
|
910
|
+
const hostRoot = host?.location?.directory || undefined;
|
|
855
911
|
const registration = await host.tool.transform((draft) => {
|
|
856
|
-
for (const [name, spec] of Object.entries(buildToolMap())) {
|
|
912
|
+
for (const [name, spec] of Object.entries(buildToolMap(hostRoot))) {
|
|
857
913
|
draft.add({
|
|
858
914
|
name,
|
|
859
915
|
description: spec.description,
|
|
860
916
|
input: spec.input,
|
|
861
917
|
options: { codemode: false },
|
|
862
918
|
execute: async (args = {}, toolContext = undefined) => {
|
|
863
|
-
const r = await callMcp(
|
|
919
|
+
const r = await callMcp(
|
|
920
|
+
name,
|
|
921
|
+
normalizeArgs(name, args),
|
|
922
|
+
toolContext?.cwd || hostRoot
|
|
923
|
+
);
|
|
864
924
|
return { content: r.content };
|
|
865
925
|
},
|
|
866
926
|
});
|
package/plugins/opencode/tui.js
CHANGED
|
@@ -48,6 +48,68 @@ function readJson(file) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** Config-derived status fields; nulls when config is absent/invalid. */
|
|
52
|
+
function configFields(research) {
|
|
53
|
+
const out = { mode: null, searchEngine: null, maxIterations: null };
|
|
54
|
+
const cfg = readJson(path.join(research, 'config.json'));
|
|
55
|
+
if (cfg && typeof cfg === 'object') {
|
|
56
|
+
out.mode = cfg.mode ?? null;
|
|
57
|
+
out.searchEngine = cfg.search_engine ?? null;
|
|
58
|
+
out.maxIterations = cfg.max_iterations ?? null;
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const REPORT_NAMES = [
|
|
64
|
+
'final_synthesis.md',
|
|
65
|
+
'brainstorm_report.md',
|
|
66
|
+
'code_audit_report.md',
|
|
67
|
+
'oss_scout_report.md',
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
/** Report files present in the workspace, with mtimes. */
|
|
71
|
+
function presentReports(research) {
|
|
72
|
+
const reports = [];
|
|
73
|
+
for (const name of REPORT_NAMES) {
|
|
74
|
+
try {
|
|
75
|
+
const st = statSync(path.join(research, name));
|
|
76
|
+
reports.push({ name, mtimeMs: st.mtimeMs });
|
|
77
|
+
} catch {
|
|
78
|
+
/* absent report */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return reports;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Count of frontier nodes not yet settled/closed, or null when absent. */
|
|
85
|
+
function openFrontierCount(research) {
|
|
86
|
+
const frontier = readJson(path.join(research, 'frontier.json'));
|
|
87
|
+
if (!frontier || typeof frontier !== 'object') return null;
|
|
88
|
+
const nodes = Array.isArray(frontier.nodes)
|
|
89
|
+
? frontier.nodes
|
|
90
|
+
: Object.values(frontier.nodes || {});
|
|
91
|
+
return nodes.filter((n) => n && n.status !== 'settled' && n.status !== 'closed').length;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Degraded-claim counts from the ledger + requeue sidecar. */
|
|
95
|
+
function degradedCounts(research) {
|
|
96
|
+
const out = { stale: 0, suspect: 0, requeued: 0 };
|
|
97
|
+
const ledger = readJson(path.join(research, 'ledger', 'claim_status.json'));
|
|
98
|
+
if (Array.isArray(ledger)) {
|
|
99
|
+
const last = new Map();
|
|
100
|
+
for (const e of ledger) {
|
|
101
|
+
if (e && typeof e.claim_id === 'string') last.set(e.claim_id, e.to_status);
|
|
102
|
+
}
|
|
103
|
+
for (const s of last.values()) {
|
|
104
|
+
if (s === 'STALE') out.stale += 1;
|
|
105
|
+
else if (s === 'SUSPECT') out.suspect += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const requeue = readJson(path.join(research, 'requeue.json'));
|
|
109
|
+
if (Array.isArray(requeue)) out.requeued = requeue.length;
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
51
113
|
/**
|
|
52
114
|
* Snapshot the workspace status. Pure fs reads; safe on missing workspace.
|
|
53
115
|
* Exported for tests.
|
|
@@ -71,44 +133,10 @@ export function readSwarmStatus(root) {
|
|
|
71
133
|
} catch {
|
|
72
134
|
return status;
|
|
73
135
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
status.maxIterations = cfg.max_iterations ?? null;
|
|
79
|
-
}
|
|
80
|
-
for (const name of [
|
|
81
|
-
'final_synthesis.md',
|
|
82
|
-
'brainstorm_report.md',
|
|
83
|
-
'code_audit_report.md',
|
|
84
|
-
'oss_scout_report.md',
|
|
85
|
-
]) {
|
|
86
|
-
try {
|
|
87
|
-
const st = statSync(path.join(research, name));
|
|
88
|
-
status.reports.push({ name, mtimeMs: st.mtimeMs });
|
|
89
|
-
} catch {
|
|
90
|
-
/* absent report */
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
const frontier = readJson(path.join(research, 'frontier.json'));
|
|
94
|
-
if (frontier && typeof frontier === 'object') {
|
|
95
|
-
const nodes = Array.isArray(frontier.nodes) ? frontier.nodes : Object.values(frontier.nodes || {});
|
|
96
|
-
const open = nodes.filter((n) => n && n.status !== 'settled' && n.status !== 'closed');
|
|
97
|
-
status.frontierOpen = open.length;
|
|
98
|
-
}
|
|
99
|
-
const ledger = readJson(path.join(research, 'ledger', 'claim_status.json'));
|
|
100
|
-
if (Array.isArray(ledger)) {
|
|
101
|
-
const last = new Map();
|
|
102
|
-
for (const e of ledger) {
|
|
103
|
-
if (e && typeof e.claim_id === 'string') last.set(e.claim_id, e.to_status);
|
|
104
|
-
}
|
|
105
|
-
for (const s of last.values()) {
|
|
106
|
-
if (s === 'STALE') status.stale += 1;
|
|
107
|
-
else if (s === 'SUSPECT') status.suspect += 1;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const requeue = readJson(path.join(research, 'requeue.json'));
|
|
111
|
-
if (Array.isArray(requeue)) status.requeued = requeue.length;
|
|
136
|
+
Object.assign(status, configFields(research));
|
|
137
|
+
status.reports = presentReports(research);
|
|
138
|
+
status.frontierOpen = openFrontierCount(research);
|
|
139
|
+
Object.assign(status, degradedCounts(research));
|
|
112
140
|
return status;
|
|
113
141
|
}
|
|
114
142
|
|
|
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/claim_store.py
CHANGED
|
@@ -52,9 +52,15 @@ def _has_fts5(conn: sqlite3.Connection) -> bool:
|
|
|
52
52
|
class ClaimStore:
|
|
53
53
|
def __init__(self, base_dir: Path, db_path: Optional[Path] = None):
|
|
54
54
|
self.base_dir = Path(os.path.realpath(str(base_dir)))
|
|
55
|
-
self.db_path =
|
|
55
|
+
self.db_path = (
|
|
56
|
+
Path(os.path.realpath(str(db_path)))
|
|
57
|
+
if db_path
|
|
58
|
+
else (self.base_dir / "claims.sqlite")
|
|
59
|
+
)
|
|
56
60
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
-
|
|
61
|
+
# uri=False is explicit: a plain filesystem path must never be
|
|
62
|
+
# interpreted as a SQLite URI connection string (S8706).
|
|
63
|
+
self.conn = sqlite3.connect(str(self.db_path), uri=False)
|
|
58
64
|
self.conn.row_factory = sqlite3.Row
|
|
59
65
|
self.fts5 = _has_fts5(self.conn)
|
|
60
66
|
self._create_schema()
|
|
@@ -115,7 +121,10 @@ class ClaimStore:
|
|
|
115
121
|
);
|
|
116
122
|
"""
|
|
117
123
|
)
|
|
118
|
-
cur.execute(
|
|
124
|
+
cur.execute(
|
|
125
|
+
"INSERT OR IGNORE INTO meta(key, value) VALUES ('schema_version', ?)",
|
|
126
|
+
(str(SCHEMA_VERSION),),
|
|
127
|
+
)
|
|
119
128
|
if self.fts5:
|
|
120
129
|
cur.execute(
|
|
121
130
|
"""
|
|
@@ -338,7 +347,9 @@ def reindex(base_dir: Path, store: Optional[ClaimStore] = None) -> Dict[str, Any
|
|
|
338
347
|
|
|
339
348
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
340
349
|
parser = argparse.ArgumentParser(description="IUMBTEMS derived claim index")
|
|
341
|
-
parser.add_argument(
|
|
350
|
+
parser.add_argument(
|
|
351
|
+
"--dir", default=".research", help="Path to .research workspace"
|
|
352
|
+
)
|
|
342
353
|
sub = parser.add_subparsers(dest="command")
|
|
343
354
|
sub.add_parser("reindex", help="Rebuild claims.sqlite from flat files")
|
|
344
355
|
search_p = sub.add_parser("search", help="Search claim statements")
|