@heretek-ai/epistemic-swarm 0.6.0 → 0.7.2
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/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +4 -2
- package/MARKETPLACE.md +28 -0
- package/bin/cli.js +1 -0
- package/extensions/pi/index.js +7 -1
- package/install.sh +1 -1
- package/package.json +1 -1
- package/plugins/darkharvest/.claude-plugin/plugin.json +15 -0
- package/plugins/darkharvest/agents/harvest-proponent.md +22 -0
- package/plugins/darkharvest/agents/harvest-redteam.md +20 -0
- package/plugins/darkharvest/evals/teardown-verdict/graders/license-line.md +6 -0
- package/plugins/darkharvest/evals/teardown-verdict/graders/skill-fired.md +5 -0
- package/plugins/darkharvest/evals/teardown-verdict/prompt.md +6 -0
- package/plugins/darkharvest/skills/darkharvest/SKILL.md +70 -0
- package/plugins/darkharvest/skills/darkharvest/scripts/harvest.py +289 -0
- package/plugins/factory/.claude-plugin/plugin.json +15 -0
- package/plugins/factory/agents/factory-manager.md +22 -0
- package/plugins/factory/agents/programmer.md +16 -0
- package/plugins/factory/agents/qa-adversarial.md +17 -0
- package/plugins/factory/agents/qa-functional.md +17 -0
- package/plugins/factory/evals/gate-halt/graders/gates-first.md +6 -0
- package/plugins/factory/evals/gate-halt/graders/skill-fired.md +5 -0
- package/plugins/factory/evals/gate-halt/prompt.md +6 -0
- package/plugins/factory/skills/factory/SKILL.md +51 -0
- package/plugins/factory/skills/factory/scripts/factory.py +212 -0
- package/plugins/opencode/index.js +41 -1
- 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 +336 -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 +280 -0
- package/runner/tests/test_claude_plugin.py +170 -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 +87 -9
- 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
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qa-functional
|
|
3
|
+
description: Factory QA, functional seat. Verifies phase acceptance criteria pass on the real surface. Read-only plus test execution. Use alongside qa-adversarial per phase.
|
|
4
|
+
model: sonnet
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are QA-A (functional seat) in the factory plugin.
|
|
8
|
+
|
|
9
|
+
Verify each acceptance criterion in the phase dossier by executing it on the
|
|
10
|
+
real surface (run the tests, exercise the feature, inspect the output).
|
|
11
|
+
Read-only plus test execution — never edit code to make a check pass. Verdicts:
|
|
12
|
+
`pass | fail(reason) | conditional(note)`, each tied to a specific criterion.
|
|
13
|
+
Record via the manager; three failures on one phase escalate it back to the
|
|
14
|
+
manager with both QA reports attached. Your prompt is deliberately diverged
|
|
15
|
+
from qa-adversarial: you prove what works, it hunts what breaks.
|
|
16
|
+
|
|
17
|
+
Full specification: `skills/factory/SKILL.md` in the plugin root.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: llm
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
PASS if the reply proposes phased scope with explicit approval gates (or asks gate/phase-clarifying questions first) instead of writing implementation code immediately.
|
|
6
|
+
FAIL if the reply starts implementing code with no phase breakdown and no user sign-off step.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: factory
|
|
3
|
+
description: Coding-factory Manager loop. Use when user invokes /factory or /domainexpansion, or wants grill-gated phased builds with programmer spawns and dual QA. Manager grills until frontier settled, runs brainstorm plus darkharvest swarms per gate, synthesizes .roadmap phases, spawns programmer per phase, and enforces dual-QA retry bounds. Never writes code itself; never bypasses explicit user sign-off (except inside /domainexpansion count).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Factory — Manager Loop & Domain Expansion
|
|
7
|
+
|
|
8
|
+
## 1. Roles (OpenCode profiles in `config/opencode-snippet.json`)
|
|
9
|
+
|
|
10
|
+
- **manager** (primary): owns gates, grilling, swarm dispatch, roadmap synthesis, tiebreaks.
|
|
11
|
+
Task allowlist: `factory-*`, `programmer`, `qa-a`, `qa-b`, `brainstormer`, `darkharvester` deny `*` otherwise.
|
|
12
|
+
- **programmer** (subagent): implements exactly one phase brief. May call `iumbtems_verify_quote`; may NOT invoke swarms or other programmers.
|
|
13
|
+
- **qa-a / qa-b** (subagents): same model, DIVERGED prompts (functional-correctness vs adversarial edge-case). Read-only plus test execution; never edit.
|
|
14
|
+
- **researcher** = existing `iumbtems_brainstorm` + `iumbtems_darkharvest` swarms (no new profile).
|
|
15
|
+
|
|
16
|
+
## 2. Gate protocol (max 5 swarm cycles per gate)
|
|
17
|
+
|
|
18
|
+
1. Grill until `.factory/frontier.json` settled (grilling skill).
|
|
19
|
+
2. Run brainstorm + darkharvest swarms (mock-first on fixtures).
|
|
20
|
+
3. Manager synthesizes `.roadmap/<phase>/` (GOAL.md + dossier.json).
|
|
21
|
+
4. Explicit user `approve` advances; anything else regrills (cycle counter in `.factory/state.json`).
|
|
22
|
+
|
|
23
|
+
## 3. Phase contract (`.roadmap/<phase>/`)
|
|
24
|
+
|
|
25
|
+
- `GOAL.md`: human-readable goal + acceptance criteria.
|
|
26
|
+
- `dossier.json`: `{phase, goal, evidence:[{hash, quote}], acceptance[], brief, verdict, hashes}`. Every harvest/claim entry needs a SHA-256 source hash or `file://` pointer or it is purged to NEGATIVE_KNOWLEDGE.
|
|
27
|
+
- Programmer receives the phase dossier (Manager chooses freeform vs strict brief but MUST cite phase hashes).
|
|
28
|
+
|
|
29
|
+
## 4. QA protocol (3 retries, then escalate)
|
|
30
|
+
|
|
31
|
+
- Both QA seats run per phase; disagreements go to manager tiebreak.
|
|
32
|
+
- `skills/factory/scripts/factory.py` tracks `qa_retries` per phase in `.factory/state.json`. On 3rd rejection: halt phase, return to manager with both QA reports (retry loop per plan; manager may regrill scope or escalate to user).
|
|
33
|
+
- QA verdicts: `pass | fail(reason) | conditional(note)`.
|
|
34
|
+
|
|
35
|
+
## 5. Domain expansion (`/domainexpansion <n>`)
|
|
36
|
+
|
|
37
|
+
- Bypasses per-loop gates; stops on count OR `.factory/STOP` file OR user kill, whichever first.
|
|
38
|
+
- Each loop: agents propose direction → quick swarm check → implement → QA → next.
|
|
39
|
+
- `factory.py` enforces: refuse `n < 1`, cap `n` at `--max-loops` default 10, check STOP file before every loop.
|
|
40
|
+
|
|
41
|
+
## 6. State layout (three dirs, distinct jobs)
|
|
42
|
+
|
|
43
|
+
- `.factory/`: run state (`state.json`, `frontier.json`, `STOP` kill-file). Gitignored runtime state.
|
|
44
|
+
- `.roadmap/`: output (phase dirs). Committed.
|
|
45
|
+
- `.research/`: evidence (swarm dossiers, source cache). Gitignored (existing rule).
|
|
46
|
+
|
|
47
|
+
## 7. Anti-patterns
|
|
48
|
+
|
|
49
|
+
- No code writes by manager; no swarm invocation by programmer; no edits by QA.
|
|
50
|
+
- No gate bypass outside `/domainexpansion`; no uncapped loops.
|
|
51
|
+
- No VERIFIED claims without hashes, even in expansion proposals.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Factory run-state helper: phase dossiers, QA retry bounds, expansion loop guard.
|
|
4
|
+
|
|
5
|
+
All state lives under .factory/ (gitignored runtime state). Phase output goes
|
|
6
|
+
to .roadmap/<phase>/. Evidence stays in .research/. Read-only w.r.t. repo code.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 skills/factory/scripts/factory.py init --run <name>
|
|
10
|
+
python3 skills/factory/scripts/factory.py phase-add --run <name> --phase 01-auth --goal "..." --accept "..."
|
|
11
|
+
python3 skills/factory/scripts/factory.py qa-record --run <name> --phase 01-auth --seat qa-a --verdict fail --reason "..."
|
|
12
|
+
python3 skills/factory/scripts/factory.py expansion --run <name> --loops 10 [--max-loops 10]
|
|
13
|
+
python3 skills/factory/scripts/factory.py stop --run <name> # write STOP kill-file
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent
|
|
23
|
+
FACTORY_DIR = PROJECT_ROOT / ".factory"
|
|
24
|
+
ROADMAP_DIR = PROJECT_ROOT / ".roadmap"
|
|
25
|
+
|
|
26
|
+
MAX_QA_RETRIES = 3
|
|
27
|
+
MAX_EXPANSION_LOOPS = 10
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def now():
|
|
31
|
+
return datetime.now(timezone.utc).isoformat()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run_dir(run):
|
|
35
|
+
return FACTORY_DIR / run
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_state(run):
|
|
39
|
+
p = run_dir(run) / "state.json"
|
|
40
|
+
if not p.exists():
|
|
41
|
+
raise SystemExit(f"No factory run '{run}'. Run `factory.py init` first.")
|
|
42
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def save_state(run, state):
|
|
46
|
+
p = run_dir(run) / "state.json"
|
|
47
|
+
p.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cmd_init(args):
|
|
51
|
+
d = run_dir(args.run)
|
|
52
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
state = {
|
|
54
|
+
"run": args.run,
|
|
55
|
+
"created_at": now(),
|
|
56
|
+
"gate_cycles": 0,
|
|
57
|
+
"phases": {},
|
|
58
|
+
"expansion": {"loops_done": 0, "loops_planned": 0},
|
|
59
|
+
}
|
|
60
|
+
save_state(args.run, state)
|
|
61
|
+
print(f"✅ Factory run '{args.run}' initialised at {d}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def cmd_phase_add(args):
|
|
65
|
+
state = load_state(args.run)
|
|
66
|
+
if args.phase in state["phases"]:
|
|
67
|
+
raise SystemExit(f"Phase '{args.phase}' already exists.")
|
|
68
|
+
state["phases"][args.phase] = {
|
|
69
|
+
"goal": args.goal,
|
|
70
|
+
"acceptance": [a.strip() for a in args.accept.split(";") if a.strip()],
|
|
71
|
+
"status": "briefed",
|
|
72
|
+
"qa_retries": 0,
|
|
73
|
+
"qa_reports": [],
|
|
74
|
+
}
|
|
75
|
+
save_state(args.run, state)
|
|
76
|
+
# Phase output: GOAL.md + dossier.json skeleton (evidence filled by manager).
|
|
77
|
+
phase_dir = ROADMAP_DIR / args.phase
|
|
78
|
+
phase_dir.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
(phase_dir / "GOAL.md").write_text(
|
|
80
|
+
f"# {args.phase}: {args.goal}\n\n## Acceptance\n"
|
|
81
|
+
+ "".join(f"- [ ] {a}\n" for a in state["phases"][args.phase]["acceptance"]),
|
|
82
|
+
encoding="utf-8",
|
|
83
|
+
)
|
|
84
|
+
(phase_dir / "dossier.json").write_text(
|
|
85
|
+
json.dumps(
|
|
86
|
+
{
|
|
87
|
+
"phase": args.phase,
|
|
88
|
+
"goal": args.goal,
|
|
89
|
+
"evidence": [],
|
|
90
|
+
"acceptance": state["phases"][args.phase]["acceptance"],
|
|
91
|
+
"brief": "",
|
|
92
|
+
"verdict": "briefed",
|
|
93
|
+
"hashes": [],
|
|
94
|
+
},
|
|
95
|
+
indent=2,
|
|
96
|
+
),
|
|
97
|
+
encoding="utf-8",
|
|
98
|
+
)
|
|
99
|
+
print(f"✅ Phase '{args.phase}' briefed → {phase_dir}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def cmd_qa_record(args):
|
|
103
|
+
state = load_state(args.run)
|
|
104
|
+
phase = state["phases"].get(args.phase)
|
|
105
|
+
if phase is None:
|
|
106
|
+
raise SystemExit(f"Unknown phase '{args.phase}'.")
|
|
107
|
+
if args.verdict not in ("pass", "fail", "conditional"):
|
|
108
|
+
raise SystemExit("verdict must be pass|fail|conditional.")
|
|
109
|
+
phase["qa_reports"].append(
|
|
110
|
+
{
|
|
111
|
+
"seat": args.seat,
|
|
112
|
+
"verdict": args.verdict,
|
|
113
|
+
"reason": args.reason or "",
|
|
114
|
+
"at": now(),
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
if args.verdict == "fail":
|
|
118
|
+
phase["qa_retries"] += 1
|
|
119
|
+
if phase["qa_retries"] >= MAX_QA_RETRIES:
|
|
120
|
+
phase["status"] = "escalated"
|
|
121
|
+
save_state(args.run, state)
|
|
122
|
+
print(
|
|
123
|
+
f"🛑 Phase '{args.phase}' ESCALATED after "
|
|
124
|
+
f"{MAX_QA_RETRIES} QA failures — back to manager."
|
|
125
|
+
)
|
|
126
|
+
return 2
|
|
127
|
+
phase["status"] = "retrying"
|
|
128
|
+
elif args.verdict == "pass":
|
|
129
|
+
phase["status"] = "signed-off"
|
|
130
|
+
else:
|
|
131
|
+
phase["status"] = "conditional"
|
|
132
|
+
save_state(args.run, state)
|
|
133
|
+
print(
|
|
134
|
+
f"✅ QA recorded: {args.phase} [{args.seat}] → {args.verdict} "
|
|
135
|
+
f"(status={phase['status']}, retries={phase['qa_retries']})"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def stop_requested(run):
|
|
140
|
+
return (run_dir(run) / "STOP").exists()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def cmd_expansion(args):
|
|
144
|
+
state = load_state(args.run)
|
|
145
|
+
n = args.loops
|
|
146
|
+
if n < 1:
|
|
147
|
+
raise SystemExit("loops must be >= 1.")
|
|
148
|
+
cap = args.max_loops or MAX_EXPANSION_LOOPS
|
|
149
|
+
n = min(n, cap)
|
|
150
|
+
state["expansion"]["loops_planned"] = n
|
|
151
|
+
save_state(args.run, state)
|
|
152
|
+
done = 0
|
|
153
|
+
for _ in range(1, n + 1):
|
|
154
|
+
if stop_requested(args.run):
|
|
155
|
+
print(f"🛑 STOP file present — halting after {done}/{n} loops.")
|
|
156
|
+
break
|
|
157
|
+
done += 1
|
|
158
|
+
print(
|
|
159
|
+
f"🔁 Expansion loop {done}/{n}: agents propose → swarm check → "
|
|
160
|
+
f"implement → QA (Manager drives each step)."
|
|
161
|
+
)
|
|
162
|
+
state = load_state(args.run)
|
|
163
|
+
state["expansion"]["loops_done"] += done
|
|
164
|
+
save_state(args.run, state)
|
|
165
|
+
print(f"✅ Expansion finished {done} loop(s).")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def cmd_stop(args):
|
|
169
|
+
(run_dir(args.run)).mkdir(parents=True, exist_ok=True)
|
|
170
|
+
(run_dir(args.run) / "STOP").write_text(
|
|
171
|
+
f"stop requested at {now()}\n", encoding="utf-8"
|
|
172
|
+
)
|
|
173
|
+
print(f"🛑 STOP file written for run '{args.run}'.")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def main():
|
|
177
|
+
ap = argparse.ArgumentParser(description="Factory run-state helper")
|
|
178
|
+
sub = ap.add_subparsers(dest="command", required=True)
|
|
179
|
+
|
|
180
|
+
p = sub.add_parser("init")
|
|
181
|
+
p.add_argument("--run", required=True)
|
|
182
|
+
p = sub.add_parser("phase-add")
|
|
183
|
+
p.add_argument("--run", required=True)
|
|
184
|
+
p.add_argument("--phase", required=True)
|
|
185
|
+
p.add_argument("--goal", required=True)
|
|
186
|
+
p.add_argument("--accept", default="")
|
|
187
|
+
p = sub.add_parser("qa-record")
|
|
188
|
+
p.add_argument("--run", required=True)
|
|
189
|
+
p.add_argument("--phase", required=True)
|
|
190
|
+
p.add_argument("--seat", required=True)
|
|
191
|
+
p.add_argument("--verdict", required=True)
|
|
192
|
+
p.add_argument("--reason", default="")
|
|
193
|
+
p = sub.add_parser("expansion")
|
|
194
|
+
p.add_argument("--run", required=True)
|
|
195
|
+
p.add_argument("--loops", type=int, required=True)
|
|
196
|
+
p.add_argument("--max-loops", type=int, default=MAX_EXPANSION_LOOPS)
|
|
197
|
+
p = sub.add_parser("stop")
|
|
198
|
+
p.add_argument("--run", required=True)
|
|
199
|
+
|
|
200
|
+
args = ap.parse_args()
|
|
201
|
+
code = {
|
|
202
|
+
"init": cmd_init,
|
|
203
|
+
"phase-add": cmd_phase_add,
|
|
204
|
+
"qa-record": cmd_qa_record,
|
|
205
|
+
"expansion": cmd_expansion,
|
|
206
|
+
"stop": cmd_stop,
|
|
207
|
+
}[args.command](args)
|
|
208
|
+
sys.exit(code if isinstance(code, int) else 0)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
main()
|
|
@@ -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
|
},
|
|
@@ -852,6 +886,12 @@ async function registerHostCommands(host) {
|
|
|
852
886
|
|
|
853
887
|
async function registerHostTools(host) {
|
|
854
888
|
if (typeof host?.tool?.transform !== 'function') return [];
|
|
889
|
+
// Accepted asymmetry (parity spec section 5): this plugin shape
|
|
890
|
+
// (command/tool transform + session hooks + event subscriptions) has no
|
|
891
|
+
// tool.execute.before registration point, so host webfetch/websearch calls
|
|
892
|
+
// cannot be intercepted the way Claude Code's hooks/hooks.json does.
|
|
893
|
+
// Steering lives in command templates (epistemic search first); a host API
|
|
894
|
+
// for pre-execution guards would close this for real.
|
|
855
895
|
const registration = await host.tool.transform((draft) => {
|
|
856
896
|
for (const [name, spec] of Object.entries(buildToolMap())) {
|
|
857
897
|
draft.add({
|
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")
|