@heretek-ai/epistemic-swarm 0.7.13 → 0.7.15
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/README.md +6 -2
- package/package.json +2 -1
- package/plugins/factory/skills/factory/scripts/factory.py +68 -14
- package/plugins/opencode/index.js +21 -0
- package/plugins/socratic-grilling/skills/grilling/SKILL.md +14 -1
- package/plugins/socratic-grilling/skills/grilling/socratic_tree.py +88 -16
- 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__/darkharvest_claims.cpython-311.pyc +0 -0
- package/runner/__pycache__/errors.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__/preflight.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__/retrieval_log.cpython-311.pyc +0 -0
- package/runner/__pycache__/schema_validate.cpython-311.pyc +0 -0
- package/runner/__pycache__/schemas.cpython-311.pyc +0 -0
- package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
- package/runner/errors.py +51 -0
- package/runner/mcp_server.py +132 -8
- package/runner/preflight.py +157 -0
- package/runner/research_swarm.py +148 -4
- package/runner/schema_validate.py +93 -0
- package/runner/schemas.py +270 -0
- package/runner/state_machine.py +96 -10
- 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_contract_tooling.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dossier_contract.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_grounding.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_manifest_concurrency.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_opencode_v2_registrars.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_swarm_config.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_contract_tooling.py +312 -0
- package/runner/tests/test_mcp_server.py +2 -0
- package/runner/tests/test_opencode_ux.py +4 -2
- package/schemas/alpha_dossier.schema.json +204 -0
- package/schemas/audit_report.schema.json +76 -0
- package/schemas/beta_dossier.schema.json +214 -0
- package/schemas/frontier.schema.json +61 -0
- package/schemas/manifest.schema.json +43 -0
- package/schemas/scope_manifest.schema.json +47 -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__/check_docs.cpython-311.pyc +0 -0
- package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
- package/scripts/__pycache__/gen_schemas.cpython-311.pyc +0 -0
- package/scripts/check_docs.py +66 -0
- package/scripts/gen_schemas.py +69 -0
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/factory/scripts/factory.py +68 -14
- package/skills/grilling/SKILL.md +14 -1
- package/skills/grilling/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/grilling/__pycache__/socratic_tree.cpython-311.pyc +0 -0
- package/skills/grilling/socratic_tree.py +88 -16
- 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/README.md
CHANGED
|
@@ -199,11 +199,13 @@ iumbtems adapters
|
|
|
199
199
|
# Other utility commands
|
|
200
200
|
iumbtems install # install skills & MCP servers into ~/.claude/
|
|
201
201
|
iumbtems marketplace # print the Claude Code marketplace catalog
|
|
202
|
-
iumbtems doctor #
|
|
202
|
+
iumbtems doctor # environment + preflight health check
|
|
203
203
|
iumbtems test # run the automated test suite
|
|
204
204
|
iumbtems help # full usage text
|
|
205
205
|
```
|
|
206
206
|
|
|
207
|
+
Run-shaping flags: `--resume` (continue a session instead of re-orchestrating) and `--dry-run` (validate config/backends/engine without spawning agents). Frontier authoring is fully tool-driven: `socratic_tree.py --add-node/--settle/--export`.
|
|
208
|
+
|
|
207
209
|
Common options: `--mock-claude` (synthetic responses, zero API cost), `--frontier <file>` (settled `frontier.json`), `--mode <research|audit|scout|hybrid|brainstorm|darkharvest>`, `--engine <duckduckgo|brave|firecrawl|searxng>`, `--depth <1-4>`, `--backend <auto|claude|opencode>`, `--dir <path>`.
|
|
208
210
|
|
|
209
211
|
The `epistemic-swarm` binary is an alias of `iumbtems`.
|
|
@@ -231,6 +233,8 @@ The canonical MCP server (`python3 runner/mcp_server.py`, stdio) exposes these t
|
|
|
231
233
|
| `iumbtems_report_retraction` | Record a RETRACTED/REVISED event for a cached source; dependent `VERIFIED` claims degrade to STALE/SUSPECT on the next audit. |
|
|
232
234
|
| `iumbtems_check_staleness` | One claim-degradation pass: join claims against retraction events, write the status ledger, queue re-runs for degraded scopes. Never mutates dossiers. |
|
|
233
235
|
| `iumbtems_set_domain_pack` | Activate a Regulated Domain Pack (`biopharma` / `quant` / `legal` epistemic constitution) for subsequent audits. |
|
|
236
|
+
| `iumbtems_doctor` | Preflight health check: plugin version, backend + resolved binary, search-engine live probe, workspace write test. Run first when something looks wrong. |
|
|
237
|
+
| `iumbtems_test` | Run the packaged test suite in a subprocess and report pass/fail with the tail. |
|
|
234
238
|
|
|
235
239
|
---
|
|
236
240
|
|
|
@@ -332,7 +336,7 @@ python3 scripts/build_adapters.py --check # CI gate: fails on drift
|
|
|
332
336
|
python3 -m unittest discover -s runner/tests
|
|
333
337
|
```
|
|
334
338
|
|
|
335
|
-
|
|
339
|
+
The legacy hyphenated skill name is served by an alias the overlay installer creates under the user's `~/.claude/skills/` directory. It is deliberately **not** committed as an in-repo symlink: `claude plugin validate --strict` treats a symlink entry as a warning (→ error), which fails marketplace and eval validation.
|
|
336
340
|
|
|
337
341
|
---
|
|
338
342
|
|
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.15",
|
|
4
4
|
"description": "IUMBTEMS (Epistemic Swarm) — High-Integrity Dialectic Research Agent Harness for Claude Code, OpenCode V2, Pi (pi.dev), OMP (oh-my-pi), Gemini CLI, Codex CLI, and AntiGravity",
|
|
5
5
|
"main": "bin/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -118,6 +118,7 @@
|
|
|
118
118
|
"bin",
|
|
119
119
|
"prompts",
|
|
120
120
|
"skills",
|
|
121
|
+
"schemas",
|
|
121
122
|
"hooks",
|
|
122
123
|
"config",
|
|
123
124
|
"runner",
|
|
@@ -55,7 +55,10 @@ def run_dir(run):
|
|
|
55
55
|
def load_state(run):
|
|
56
56
|
p = run_dir(run) / "state.json"
|
|
57
57
|
if not p.exists():
|
|
58
|
-
raise SystemExit(
|
|
58
|
+
raise SystemExit(
|
|
59
|
+
f"No factory run '{run}'. Pass --run <name> (or run "
|
|
60
|
+
f"`factory.py init --run <name>` first)."
|
|
61
|
+
)
|
|
59
62
|
return json.loads(p.read_text(encoding="utf-8"))
|
|
60
63
|
|
|
61
64
|
|
|
@@ -82,6 +85,19 @@ def cmd_phase_add(args):
|
|
|
82
85
|
state = load_state(args.run)
|
|
83
86
|
if args.phase in state["phases"]:
|
|
84
87
|
raise SystemExit(f"Phase '{args.phase}' already exists.")
|
|
88
|
+
# Phase output goes to phase.json — a run-state helper must never clobber the
|
|
89
|
+
# author-written GOAL.md / dossier.json it is meant to track (#6 item 2.3).
|
|
90
|
+
# This check runs BEFORE any state mutation so a refusal leaves no trace.
|
|
91
|
+
phase_dir = ROADMAP_DIR / args.phase
|
|
92
|
+
authored = [
|
|
93
|
+
name for name in ("GOAL.md", "dossier.json") if (phase_dir / name).exists()
|
|
94
|
+
]
|
|
95
|
+
if authored and not getattr(args, "force", False):
|
|
96
|
+
raise SystemExit(
|
|
97
|
+
f"Refusing to touch author-written file(s) in {phase_dir}: "
|
|
98
|
+
f"{', '.join(authored)}. phase-add now writes phase.json only; "
|
|
99
|
+
f"re-run with --force to overwrite."
|
|
100
|
+
)
|
|
85
101
|
state["phases"][args.phase] = {
|
|
86
102
|
"goal": args.goal,
|
|
87
103
|
"acceptance": [a.strip() for a in args.accept.split(";") if a.strip()],
|
|
@@ -90,30 +106,52 @@ def cmd_phase_add(args):
|
|
|
90
106
|
"qa_reports": [],
|
|
91
107
|
}
|
|
92
108
|
save_state(args.run, state)
|
|
93
|
-
# Phase output: GOAL.md + dossier.json skeleton (evidence filled by manager).
|
|
94
|
-
phase_dir = ROADMAP_DIR / args.phase
|
|
95
109
|
phase_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
-
(phase_dir / "
|
|
97
|
-
f"# {args.phase}: {args.goal}\n\n## Acceptance\n"
|
|
98
|
-
+ "".join(f"- [ ] {a}\n" for a in state["phases"][args.phase]["acceptance"]),
|
|
99
|
-
encoding="utf-8",
|
|
100
|
-
)
|
|
101
|
-
(phase_dir / "dossier.json").write_text(
|
|
110
|
+
(phase_dir / "phase.json").write_text(
|
|
102
111
|
json.dumps(
|
|
103
112
|
{
|
|
104
113
|
"phase": args.phase,
|
|
105
114
|
"goal": args.goal,
|
|
106
|
-
"evidence": [],
|
|
107
115
|
"acceptance": state["phases"][args.phase]["acceptance"],
|
|
108
|
-
"
|
|
109
|
-
"verdict": "briefed",
|
|
110
|
-
"hashes": [],
|
|
116
|
+
"status": "briefed",
|
|
111
117
|
},
|
|
112
118
|
indent=2,
|
|
113
119
|
),
|
|
114
120
|
encoding="utf-8",
|
|
115
121
|
)
|
|
116
|
-
print(f"✅ Phase '{args.phase}' briefed → {phase_dir}")
|
|
122
|
+
print(f"✅ Phase '{args.phase}' briefed → {phase_dir / 'phase.json'}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cmd_gate(args):
|
|
126
|
+
"""First-class gate bookkeeping (open/settle/approve/waive/escalate/count)."""
|
|
127
|
+
state = load_state(args.run)
|
|
128
|
+
gates = state.setdefault("gates", {})
|
|
129
|
+
gate = gates.setdefault(
|
|
130
|
+
args.phase, {"status": "closed", "cycles": 0, "waivers": [], "history": []}
|
|
131
|
+
)
|
|
132
|
+
action = (args.action or "").lower()
|
|
133
|
+
if action == "open":
|
|
134
|
+
gate["status"] = "open"
|
|
135
|
+
gate["cycles"] = gate.get("cycles", 0) + 1
|
|
136
|
+
state["gate_cycles"] = state.get("gate_cycles", 0) + 1
|
|
137
|
+
elif action == "settle":
|
|
138
|
+
gate["status"] = "settled"
|
|
139
|
+
elif action == "approve":
|
|
140
|
+
gate["status"] = "approved"
|
|
141
|
+
elif action == "waive":
|
|
142
|
+
gate["status"] = "waived"
|
|
143
|
+
gate["waivers"].append(args.reason or "")
|
|
144
|
+
elif action == "escalate":
|
|
145
|
+
gate["status"] = "escalated"
|
|
146
|
+
gate["cycles"] = gate.get("cycles", 0) + 1
|
|
147
|
+
state["gate_cycles"] = state.get("gate_cycles", 0) + 1
|
|
148
|
+
elif action != "count":
|
|
149
|
+
raise SystemExit(
|
|
150
|
+
"gate --action must be one of: open, settle, approve, waive, escalate, count"
|
|
151
|
+
)
|
|
152
|
+
gate["history"].append({"action": action, "at": now(), "reason": args.reason or ""})
|
|
153
|
+
save_state(args.run, state)
|
|
154
|
+
print(json.dumps({"phase": args.phase, "gate": gate}, indent=2))
|
|
117
155
|
|
|
118
156
|
|
|
119
157
|
def cmd_qa_record(args):
|
|
@@ -211,6 +249,11 @@ def main():
|
|
|
211
249
|
p.add_argument("--phase", required=True)
|
|
212
250
|
p.add_argument("--goal", required=True)
|
|
213
251
|
p.add_argument("--accept", default="")
|
|
252
|
+
p.add_argument(
|
|
253
|
+
"--force",
|
|
254
|
+
action="store_true",
|
|
255
|
+
help="Overwrite author-written GOAL.md/dossier.json if present",
|
|
256
|
+
)
|
|
214
257
|
_add_common(p)
|
|
215
258
|
p = sub.add_parser("qa-record")
|
|
216
259
|
p.add_argument("--run", required=True)
|
|
@@ -227,6 +270,16 @@ def main():
|
|
|
227
270
|
p = sub.add_parser("stop")
|
|
228
271
|
p.add_argument("--run", required=True)
|
|
229
272
|
_add_common(p)
|
|
273
|
+
p = sub.add_parser("gate")
|
|
274
|
+
p.add_argument("--run", required=True)
|
|
275
|
+
p.add_argument("--phase", required=True)
|
|
276
|
+
p.add_argument(
|
|
277
|
+
"--action",
|
|
278
|
+
required=True,
|
|
279
|
+
choices=["open", "settle", "approve", "waive", "escalate", "count"],
|
|
280
|
+
)
|
|
281
|
+
p.add_argument("--reason", default="")
|
|
282
|
+
_add_common(p)
|
|
230
283
|
|
|
231
284
|
args = ap.parse_args()
|
|
232
285
|
root = resolve_project_root(args.project_dir)
|
|
@@ -238,6 +291,7 @@ def main():
|
|
|
238
291
|
"qa-record": cmd_qa_record,
|
|
239
292
|
"expansion": cmd_expansion,
|
|
240
293
|
"stop": cmd_stop,
|
|
294
|
+
"gate": cmd_gate,
|
|
241
295
|
}[args.command](args)
|
|
242
296
|
sys.exit(code if isinstance(code, int) else 0)
|
|
243
297
|
|
|
@@ -111,6 +111,8 @@ export const IUMBTEMS_TOOL_NAMES = [
|
|
|
111
111
|
'iumbtems_set_domain_pack',
|
|
112
112
|
'iumbtems_export_brief',
|
|
113
113
|
'iumbtems_verify_brief',
|
|
114
|
+
'iumbtems_doctor',
|
|
115
|
+
'iumbtems_test',
|
|
114
116
|
];
|
|
115
117
|
|
|
116
118
|
// Deprecated misspelled alias (pre-A2b export). Remove in a semver-major.
|
|
@@ -444,6 +446,25 @@ const TOOL_CATALOG = [
|
|
|
444
446
|
required: ['brief'],
|
|
445
447
|
},
|
|
446
448
|
},
|
|
449
|
+
{
|
|
450
|
+
name: 'iumbtems_doctor',
|
|
451
|
+
description:
|
|
452
|
+
'Preflight health check for the resolved workspace: plugin version, backend binary, search-engine reachability, and workspace writability. Run this first when anything looks wrong.',
|
|
453
|
+
input: {
|
|
454
|
+
type: 'object',
|
|
455
|
+
properties: {
|
|
456
|
+
base_dir: { type: 'string', description: 'Project root (default: IUMBTEMS_PROJECT_DIR or cwd)' },
|
|
457
|
+
mode: { type: 'string', description: 'Operating mode to report for' },
|
|
458
|
+
probe: { type: 'boolean', description: 'Run a live 1-query search probe (default true)' },
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: 'iumbtems_test',
|
|
464
|
+
description:
|
|
465
|
+
'Run the packaged IUMBTEMS test suite (subprocess, timeout-bounded) and report pass/fail with the tail of the output.',
|
|
466
|
+
input: { type: 'object', properties: {} },
|
|
467
|
+
},
|
|
447
468
|
];
|
|
448
469
|
|
|
449
470
|
/** Legacy arg shims -> MCP handler contract. */
|
|
@@ -42,7 +42,20 @@ Always challenge default premises in Round 1:
|
|
|
42
42
|
|
|
43
43
|
## 5. FRONTIER RESOLUTION & FREEZING
|
|
44
44
|
|
|
45
|
+
The frontier is fully tool-drivable (no hand-written JSON):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# author a node (prerequisites gate when it appears on the frontier)
|
|
49
|
+
python3 skills/grilling/socratic_tree.py --add-node --id N1 \
|
|
50
|
+
--question "L1 or L2 for state verification?" --depends-on ""
|
|
51
|
+
# settle a node
|
|
52
|
+
python3 skills/grilling/socratic_tree.py --settle N1 "L1 for finality"
|
|
53
|
+
# inspect / freeze
|
|
54
|
+
python3 skills/grilling/socratic_tree.py --show-frontier
|
|
55
|
+
python3 skills/grilling/socratic_tree.py --export
|
|
56
|
+
```
|
|
57
|
+
|
|
45
58
|
When every branch of the design tree has been visited and the frontier is empty:
|
|
46
59
|
1. Summarize the settled constraints.
|
|
47
|
-
2.
|
|
60
|
+
2. Freeze the settled state to `.research/frontier.json` (`--export`, default path).
|
|
48
61
|
3. Hand off the settled frontier to the **Swarm Orchestrator** to begin empirical dialectic execution.
|
|
@@ -11,10 +11,17 @@ import argparse
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Dict, List, Optional, Any
|
|
13
13
|
|
|
14
|
+
|
|
14
15
|
class DecisionNode:
|
|
15
|
-
def __init__(
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
node_id: str,
|
|
19
|
+
title: str,
|
|
20
|
+
question: str,
|
|
21
|
+
recommended: str,
|
|
22
|
+
options: Optional[List[str]] = None,
|
|
23
|
+
prerequisites: Optional[List[str]] = None,
|
|
24
|
+
):
|
|
18
25
|
self.node_id = node_id
|
|
19
26
|
self.title = title
|
|
20
27
|
self.question = question
|
|
@@ -34,18 +41,18 @@ class DecisionNode:
|
|
|
34
41
|
"recommended": self.recommended,
|
|
35
42
|
"options": self.options,
|
|
36
43
|
"prerequisites": self.prerequisites,
|
|
37
|
-
"settled_answer": self.settled_answer
|
|
44
|
+
"settled_answer": self.settled_answer,
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
@classmethod
|
|
41
|
-
def from_dict(cls, data: Dict[str, Any]) ->
|
|
48
|
+
def from_dict(cls, data: Dict[str, Any]) -> "DecisionNode":
|
|
42
49
|
node = cls(
|
|
43
50
|
node_id=data["node_id"],
|
|
44
51
|
title=data["title"],
|
|
45
52
|
question=data["question"],
|
|
46
53
|
recommended=data["recommended"],
|
|
47
54
|
options=data.get("options", []),
|
|
48
|
-
prerequisites=data.get("prerequisites", [])
|
|
55
|
+
prerequisites=data.get("prerequisites", []),
|
|
49
56
|
)
|
|
50
57
|
node.settled_answer = data.get("settled_answer")
|
|
51
58
|
return node
|
|
@@ -82,7 +89,9 @@ class DesignTree:
|
|
|
82
89
|
self.nodes[node_id].settled_answer = answer
|
|
83
90
|
|
|
84
91
|
def is_complete(self) -> bool:
|
|
85
|
-
return len(self.compute_frontier()) == 0 and all(
|
|
92
|
+
return len(self.compute_frontier()) == 0 and all(
|
|
93
|
+
n.is_settled() for n in self.nodes.values()
|
|
94
|
+
)
|
|
86
95
|
|
|
87
96
|
def export_frontier_json(self, output_path: Path):
|
|
88
97
|
safe_path = Path(os.path.realpath(str(output_path)))
|
|
@@ -93,13 +102,13 @@ class DesignTree:
|
|
|
93
102
|
"nodes": {k: v.to_dict() for k, v in self.nodes.items()},
|
|
94
103
|
"settled_constraints": {
|
|
95
104
|
k: v.settled_answer for k, v in self.nodes.items() if v.is_settled()
|
|
96
|
-
}
|
|
105
|
+
},
|
|
97
106
|
}
|
|
98
107
|
with open(safe_path, "w", encoding="utf-8") as f:
|
|
99
108
|
json.dump(data, f, indent=2)
|
|
100
109
|
|
|
101
110
|
@classmethod
|
|
102
|
-
def load_from_json(cls, file_path: Path) ->
|
|
111
|
+
def load_from_json(cls, file_path: Path) -> "DesignTree":
|
|
103
112
|
safe_path = Path(os.path.realpath(str(file_path)))
|
|
104
113
|
with open(safe_path, "r", encoding="utf-8") as f:
|
|
105
114
|
data = json.load(f)
|
|
@@ -111,12 +120,16 @@ class DesignTree:
|
|
|
111
120
|
|
|
112
121
|
def _display_frontier(tree: DesignTree, frontier: list) -> None:
|
|
113
122
|
print(f"\n🎯 Objective: {tree.objective}")
|
|
114
|
-
print(
|
|
123
|
+
print(
|
|
124
|
+
f"📊 Total Nodes: {len(tree.nodes)} | Settled: {sum(1 for n in tree.nodes.values() if n.is_settled())}"
|
|
125
|
+
)
|
|
115
126
|
if not frontier:
|
|
116
127
|
if tree.nodes and tree.is_complete():
|
|
117
128
|
print("✅ Frontier is EMPTY. All prerequisite branches are fully settled!")
|
|
118
129
|
else:
|
|
119
|
-
print(
|
|
130
|
+
print(
|
|
131
|
+
"ℹ️ No active frontier nodes. Define new decision nodes to begin grilling."
|
|
132
|
+
)
|
|
120
133
|
else:
|
|
121
134
|
print(f"\n⚡ Current Active Frontier ({len(frontier)} questions ready):")
|
|
122
135
|
for idx, node in enumerate(frontier, 1):
|
|
@@ -126,11 +139,49 @@ def _display_frontier(tree: DesignTree, frontier: list) -> None:
|
|
|
126
139
|
|
|
127
140
|
|
|
128
141
|
def main():
|
|
129
|
-
parser = argparse.ArgumentParser(
|
|
142
|
+
parser = argparse.ArgumentParser(
|
|
143
|
+
description="Epistemic Swarm Socratic Decision Tree"
|
|
144
|
+
)
|
|
130
145
|
parser.add_argument("--objective", type=str, help="Research objective")
|
|
131
|
-
parser.add_argument(
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"--file",
|
|
148
|
+
type=str,
|
|
149
|
+
default=".research/frontier.json",
|
|
150
|
+
help="Path to frontier.json",
|
|
151
|
+
)
|
|
152
|
+
parser.add_argument(
|
|
153
|
+
"--show-frontier",
|
|
154
|
+
action="store_true",
|
|
155
|
+
help="Print the current decision frontier",
|
|
156
|
+
)
|
|
157
|
+
parser.add_argument(
|
|
158
|
+
"--settle",
|
|
159
|
+
nargs=2,
|
|
160
|
+
metavar=("NODE_ID", "ANSWER"),
|
|
161
|
+
help="Settle a decision node",
|
|
162
|
+
)
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
"--add-node",
|
|
165
|
+
action="store_true",
|
|
166
|
+
help="Add a decision node (with --id/--question)",
|
|
167
|
+
)
|
|
168
|
+
parser.add_argument("--id", type=str, help="Node id (with --add-node)")
|
|
169
|
+
parser.add_argument("--question", type=str, help="Node question (with --add-node)")
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--title", type=str, default="", help="Node title (defaults to --id)"
|
|
172
|
+
)
|
|
173
|
+
parser.add_argument(
|
|
174
|
+
"--recommended", type=str, default="", help="Recommended option"
|
|
175
|
+
)
|
|
176
|
+
parser.add_argument(
|
|
177
|
+
"--depends-on",
|
|
178
|
+
type=str,
|
|
179
|
+
default="",
|
|
180
|
+
help="Comma-separated prerequisite node ids",
|
|
181
|
+
)
|
|
182
|
+
parser.add_argument(
|
|
183
|
+
"--export", action="store_true", help="Write the frontier JSON and exit"
|
|
184
|
+
)
|
|
134
185
|
|
|
135
186
|
args = parser.parse_args()
|
|
136
187
|
frontier_path = Path(os.path.realpath(str(args.file)))
|
|
@@ -141,14 +192,35 @@ def main():
|
|
|
141
192
|
objective = args.objective or "Epistemic Swarm Research Objective"
|
|
142
193
|
tree = DesignTree(objective=objective)
|
|
143
194
|
|
|
195
|
+
if args.add_node:
|
|
196
|
+
if not args.id or not args.question:
|
|
197
|
+
parser.error("--add-node requires --id and --question")
|
|
198
|
+
prereqs = [p.strip() for p in (args.depends_on or "").split(",") if p.strip()]
|
|
199
|
+
tree.add_node(
|
|
200
|
+
DecisionNode(
|
|
201
|
+
node_id=args.id,
|
|
202
|
+
title=args.title or args.id,
|
|
203
|
+
question=args.question,
|
|
204
|
+
recommended=args.recommended or "",
|
|
205
|
+
options=[],
|
|
206
|
+
prerequisites=prereqs,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
tree.export_frontier_json(frontier_path)
|
|
210
|
+
print(f"Added node {args.id} -> {frontier_path}")
|
|
211
|
+
|
|
144
212
|
if args.settle:
|
|
145
213
|
node_id, answer = args.settle
|
|
146
214
|
tree.settle_node(node_id, answer)
|
|
147
215
|
tree.export_frontier_json(frontier_path)
|
|
148
216
|
print(f"Settled {node_id} -> {answer}")
|
|
149
217
|
|
|
218
|
+
if args.export:
|
|
219
|
+
tree.export_frontier_json(frontier_path)
|
|
220
|
+
print(f"Exported frontier -> {frontier_path}")
|
|
221
|
+
|
|
150
222
|
frontier = tree.compute_frontier()
|
|
151
|
-
if args.show_frontier or not args.settle:
|
|
223
|
+
if args.show_frontier or not (args.settle or args.add_node or args.export):
|
|
152
224
|
_display_frontier(tree, frontier)
|
|
153
225
|
|
|
154
226
|
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/runner/errors.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Structured, typed errors (Heretek-AI/IUMBTEMS#6 item 3.10).
|
|
3
|
+
|
|
4
|
+
The 0.7.10 diagnostic was the most valuable output in the reporter's session, so
|
|
5
|
+
this generalises it: every actionable failure carries a code, the path that was
|
|
6
|
+
searched, where output actually went, the resolved workspace, the spawn cwd, and
|
|
7
|
+
a suggested fix. An agent can act on that; it cannot act on `FileNotFoundError`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import asdict, dataclass, field
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class SwarmError(Exception):
|
|
16
|
+
"""Base typed error with the diagnostic fields an agent needs."""
|
|
17
|
+
|
|
18
|
+
code: str
|
|
19
|
+
message: str
|
|
20
|
+
path_searched: Optional[str] = None
|
|
21
|
+
path_written: Optional[str] = None
|
|
22
|
+
workspace: Optional[str] = None
|
|
23
|
+
spawn_cwd: Optional[str] = None
|
|
24
|
+
suggested_fix: Optional[str] = None
|
|
25
|
+
details: Dict[str, Any] = field(default_factory=dict)
|
|
26
|
+
|
|
27
|
+
def __post_init__(self) -> None:
|
|
28
|
+
super().__init__(self.message)
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
31
|
+
return {k: v for k, v in asdict(self).items() if v not in (None, {}, "")}
|
|
32
|
+
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
lines = [f"[{self.code}] {self.message}"]
|
|
35
|
+
for label, value in (
|
|
36
|
+
("path_searched", self.path_searched),
|
|
37
|
+
("path_written", self.path_written),
|
|
38
|
+
("workspace", self.workspace),
|
|
39
|
+
("spawn_cwd", self.spawn_cwd),
|
|
40
|
+
("suggested_fix", self.suggested_fix),
|
|
41
|
+
):
|
|
42
|
+
if value:
|
|
43
|
+
lines.append(f" {label}: {value}")
|
|
44
|
+
return "\n".join(lines)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DossierNotFound(SwarmError, FileNotFoundError):
|
|
48
|
+
"""A required role dossier is absent.
|
|
49
|
+
|
|
50
|
+
Subclasses FileNotFoundError so existing callers/tests keep working.
|
|
51
|
+
"""
|