@cleocode/skills 2026.5.16 → 2026.5.17
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/package.json +1 -1
- package/skills/ct-council/SKILL.md +377 -0
- package/skills/ct-council/optimization/HARDENING-PLAYBOOK.md +107 -0
- package/skills/ct-council/optimization/README.md +74 -0
- package/skills/ct-council/optimization/scenarios.yaml +121 -0
- package/skills/ct-council/optimization/scripts/campaign.py +543 -0
- package/skills/ct-council/optimization/scripts/test_campaign.py +143 -0
- package/skills/ct-council/references/chairman.md +119 -0
- package/skills/ct-council/references/contrarian.md +70 -0
- package/skills/ct-council/references/evidence-pack.md +145 -0
- package/skills/ct-council/references/examples.md +235 -0
- package/skills/ct-council/references/executor.md +83 -0
- package/skills/ct-council/references/expansionist.md +68 -0
- package/skills/ct-council/references/first-principles.md +73 -0
- package/skills/ct-council/references/outsider.md +73 -0
- package/skills/ct-council/references/peer-review.md +125 -0
- package/skills/ct-council/scripts/analyze_runs.py +293 -0
- package/skills/ct-council/scripts/fixtures/executor_multi.md +198 -0
- package/skills/ct-council/scripts/fixtures/missing_advisor.md +117 -0
- package/skills/ct-council/scripts/fixtures/missing_convergence.md +190 -0
- package/skills/ct-council/scripts/fixtures/thin_evidence.md +193 -0
- package/skills/ct-council/scripts/fixtures/valid.md +226 -0
- package/skills/ct-council/scripts/fixtures/valid_with_llmtxt.md +226 -0
- package/skills/ct-council/scripts/llmtxt_ref.py +223 -0
- package/skills/ct-council/scripts/run_council.py +578 -0
- package/skills/ct-council/scripts/telemetry.py +624 -0
- package/skills/ct-council/scripts/test_telemetry.py +509 -0
- package/skills/ct-council/scripts/test_validate.py +452 -0
- package/skills/ct-council/scripts/validate.py +396 -0
- package/skills.json +19 -0
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
run_council.py — driver script that turns a Council run from a hand-driven
|
|
4
|
+
ritual into a reproducible artifact.
|
|
5
|
+
|
|
6
|
+
It does NOT spawn agents itself (the orchestrator does that — see SKILL.md
|
|
7
|
+
"Phase ownership" table). What it provides:
|
|
8
|
+
|
|
9
|
+
1. A canonical layout for run artifacts under
|
|
10
|
+
<out-dir>/<timestamp>-<short-id>/{phase0.md, phase1.md, peer.md,
|
|
11
|
+
output.md, run.json}.
|
|
12
|
+
2. Phase-gating: each phase validates before the next is allowed to start.
|
|
13
|
+
3. Validator + telemetry hooks: a validated `output.md` is automatically
|
|
14
|
+
appended to .cleo/council-runs.jsonl with optional --tokens / --wall-clock
|
|
15
|
+
stamps.
|
|
16
|
+
4. A `--scenario` flag that names the shakedown (1..8) for telemetry
|
|
17
|
+
filtering and exit-criteria reporting.
|
|
18
|
+
|
|
19
|
+
Subcommands:
|
|
20
|
+
|
|
21
|
+
init <question> create a new run directory + skeleton phase0 prompt
|
|
22
|
+
validate <run-dir> run validate.py on the assembled output.md
|
|
23
|
+
ingest <run-dir> validate + emit telemetry to the JSONL log
|
|
24
|
+
list show all runs under the configured runs dir
|
|
25
|
+
|
|
26
|
+
Convention: assemble phase outputs into <run-dir>/output.md by hand or by
|
|
27
|
+
your subagent harness; then run `ingest`. The script is the audit trail,
|
|
28
|
+
not the agent runtime.
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
python3 run_council.py init "Should we ship X?" --scenario baseline
|
|
32
|
+
# ... orchestrator runs Phase 0..3 and writes <run-dir>/output.md ...
|
|
33
|
+
python3 run_council.py ingest <run-dir> --tokens 41250 --wall-clock 73.4
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import argparse
|
|
39
|
+
import datetime as _dt
|
|
40
|
+
import hashlib
|
|
41
|
+
import json
|
|
42
|
+
import re
|
|
43
|
+
import subprocess
|
|
44
|
+
import sys
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
48
|
+
DEFAULT_RUNS_DIR = Path(".cleo/council-runs")
|
|
49
|
+
DEFAULT_LOG_PATH = Path(".cleo/council-runs.jsonl")
|
|
50
|
+
INDEX_FILENAME = "INDEX.jsonl" # lives inside DEFAULT_RUNS_DIR
|
|
51
|
+
INDEX_SCHEMA_VERSION = "1.0.0"
|
|
52
|
+
DEFAULT_TITLE_MAX_LEN = 60
|
|
53
|
+
|
|
54
|
+
PHASE0_TEMPLATE = """# The Council — {question}
|
|
55
|
+
|
|
56
|
+
## Evidence pack
|
|
57
|
+
|
|
58
|
+
<!--
|
|
59
|
+
3–7 items. Each item: `path:line | symbol | sha | URL | llmtxt:slug` — one-line rationale.
|
|
60
|
+
The validator (scripts/validate.py) refuses to advance Phase 1 until this section
|
|
61
|
+
is well-formed.
|
|
62
|
+
-->
|
|
63
|
+
|
|
64
|
+
1. ``
|
|
65
|
+
2. ``
|
|
66
|
+
3. ``
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _short_id(question: str) -> str:
|
|
71
|
+
seed = (question + _dt.datetime.now(tz=_dt.timezone.utc).isoformat()).encode()
|
|
72
|
+
return hashlib.sha256(seed).hexdigest()[:8]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ─── INDEX.jsonl helpers (run roster across the project) ────────────────────
|
|
76
|
+
#
|
|
77
|
+
# INDEX.jsonl is a project-scoped human-readable roster of council runs. One
|
|
78
|
+
# entry per run, upsert-by-run_id (re-running ingest does NOT duplicate). Lives
|
|
79
|
+
# at <runs-dir>/INDEX.jsonl. Distinct from the deeper telemetry log
|
|
80
|
+
# (.cleo/council-runs.jsonl) which carries the full per-run gate/disposition
|
|
81
|
+
# structure for analyze_runs.py.
|
|
82
|
+
|
|
83
|
+
def _auto_title(question: str, max_len: int = DEFAULT_TITLE_MAX_LEN) -> str:
|
|
84
|
+
"""Derive a short human-readable title from the restated question.
|
|
85
|
+
|
|
86
|
+
Strips common prefixes, normalizes whitespace, truncates with an ellipsis.
|
|
87
|
+
"""
|
|
88
|
+
q = re.sub(r"\s+", " ", question or "").strip()
|
|
89
|
+
# Drop common interrogative prefixes for terseness.
|
|
90
|
+
q = re.sub(
|
|
91
|
+
r"^(Should\s+(we|the|I)\s+|Is\s+the\s+|Is\s+|Does\s+|Can\s+(we|I)\s+|Which\s+(of\s+)?)",
|
|
92
|
+
"",
|
|
93
|
+
q,
|
|
94
|
+
flags=re.IGNORECASE,
|
|
95
|
+
)
|
|
96
|
+
q = q[:1].upper() + q[1:] if q else q
|
|
97
|
+
if len(q) <= max_len:
|
|
98
|
+
return q.rstrip("?.") or question[:max_len]
|
|
99
|
+
return q[: max_len - 1].rstrip(",.;: ") + "…"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _index_path(runs_dir: Path) -> Path:
|
|
103
|
+
return runs_dir / INDEX_FILENAME
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _read_index(runs_dir: Path) -> list[dict]:
|
|
107
|
+
p = _index_path(runs_dir)
|
|
108
|
+
if not p.exists():
|
|
109
|
+
return []
|
|
110
|
+
out = []
|
|
111
|
+
for line in p.read_text().splitlines():
|
|
112
|
+
line = line.strip()
|
|
113
|
+
if not line:
|
|
114
|
+
continue
|
|
115
|
+
try:
|
|
116
|
+
out.append(json.loads(line))
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
# Preserve unparseable lines as raw so we never silently drop data.
|
|
119
|
+
out.append({"_raw": line})
|
|
120
|
+
return out
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _upsert_index(runs_dir: Path, run_id: str, updates: dict) -> dict:
|
|
124
|
+
"""Upsert a run's INDEX entry by run_id; rewrite the file in place.
|
|
125
|
+
|
|
126
|
+
Returns the final entry. Re-running ingest must not create duplicates.
|
|
127
|
+
"""
|
|
128
|
+
entries = _read_index(runs_dir)
|
|
129
|
+
found = None
|
|
130
|
+
for e in entries:
|
|
131
|
+
if not isinstance(e, dict):
|
|
132
|
+
continue
|
|
133
|
+
if e.get("run_id") == run_id:
|
|
134
|
+
e.update(updates)
|
|
135
|
+
found = e
|
|
136
|
+
break
|
|
137
|
+
if found is None:
|
|
138
|
+
found = {"schema_version": INDEX_SCHEMA_VERSION, "run_id": run_id, **updates}
|
|
139
|
+
entries.append(found)
|
|
140
|
+
|
|
141
|
+
p = _index_path(runs_dir)
|
|
142
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
with p.open("w", encoding="utf-8") as f:
|
|
144
|
+
for e in entries:
|
|
145
|
+
if isinstance(e, dict) and "_raw" in e:
|
|
146
|
+
f.write(e["_raw"] + "\n")
|
|
147
|
+
else:
|
|
148
|
+
f.write(json.dumps(e, ensure_ascii=False) + "\n")
|
|
149
|
+
return found
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _extract_recommendation_snippet(verdict_md: str, max_len: int = 200) -> str | None:
|
|
153
|
+
"""Pull the first sentence of the verdict's recommendation, for INDEX browsing."""
|
|
154
|
+
m = re.search(
|
|
155
|
+
r"###\s+Recommendation\s*\n(.+?)(?=\n###|\Z)",
|
|
156
|
+
verdict_md,
|
|
157
|
+
re.DOTALL,
|
|
158
|
+
)
|
|
159
|
+
if not m:
|
|
160
|
+
return None
|
|
161
|
+
body = re.sub(r"\s+", " ", m.group(1).strip())
|
|
162
|
+
if not body:
|
|
163
|
+
return None
|
|
164
|
+
# First sentence (period followed by space-or-EOL).
|
|
165
|
+
sent_match = re.match(r"(.+?[.!?])(\s|$)", body)
|
|
166
|
+
sent = sent_match.group(1) if sent_match else body
|
|
167
|
+
if len(sent) > max_len:
|
|
168
|
+
sent = sent[: max_len - 1].rstrip() + "…"
|
|
169
|
+
return sent
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def cmd_init(args) -> int:
|
|
173
|
+
runs_dir = Path(args.runs_dir)
|
|
174
|
+
runs_dir.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
|
|
176
|
+
ts = _dt.datetime.now(tz=_dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
177
|
+
rid = _short_id(args.question)
|
|
178
|
+
run_dir = runs_dir / f"{ts}-{rid}"
|
|
179
|
+
run_dir.mkdir(parents=True, exist_ok=False)
|
|
180
|
+
|
|
181
|
+
(run_dir / "phase0.md").write_text(PHASE0_TEMPLATE.format(question=args.question))
|
|
182
|
+
|
|
183
|
+
title = (args.title or _auto_title(args.question)).strip()
|
|
184
|
+
now = _dt.datetime.now(tz=_dt.timezone.utc).isoformat(timespec="seconds")
|
|
185
|
+
|
|
186
|
+
metadata = {
|
|
187
|
+
"schema_version": "1.0.0",
|
|
188
|
+
"run_id": rid,
|
|
189
|
+
"ts": ts,
|
|
190
|
+
"title": title,
|
|
191
|
+
"description": args.description or args.question,
|
|
192
|
+
"created_at": now,
|
|
193
|
+
"question": args.question,
|
|
194
|
+
"scenario": args.scenario,
|
|
195
|
+
"subagent_mode": args.subagent_mode,
|
|
196
|
+
}
|
|
197
|
+
(run_dir / "run.json").write_text(json.dumps(metadata, indent=2))
|
|
198
|
+
|
|
199
|
+
# Auto-write the INDEX.jsonl entry — `status: initialized` until ingest.
|
|
200
|
+
_upsert_index(runs_dir, rid, {
|
|
201
|
+
"ts": ts,
|
|
202
|
+
"title": title,
|
|
203
|
+
"description": args.description or args.question,
|
|
204
|
+
"scenario": args.scenario,
|
|
205
|
+
"run_dir": str(run_dir),
|
|
206
|
+
"created_at": now,
|
|
207
|
+
"status": "initialized",
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
print(f"📝 Run initialized: {run_dir}")
|
|
211
|
+
print(f" Title: {title}")
|
|
212
|
+
print(f" Indexed at: {_index_path(runs_dir)}")
|
|
213
|
+
print(f" Next: write evidence pack into {run_dir / 'phase0.md'}")
|
|
214
|
+
return 0
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _run_validate(output_path: Path, *, json_out: bool = False) -> tuple[int, str]:
|
|
218
|
+
cmd = [sys.executable, str(SCRIPT_DIR / "validate.py")]
|
|
219
|
+
if json_out:
|
|
220
|
+
cmd.append("--json")
|
|
221
|
+
cmd.append(str(output_path))
|
|
222
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
223
|
+
return result.returncode, (result.stdout if json_out else result.stdout + result.stderr)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def cmd_validate(args) -> int:
|
|
227
|
+
run_dir = Path(args.run_dir)
|
|
228
|
+
output_path = run_dir / "output.md"
|
|
229
|
+
if not output_path.exists():
|
|
230
|
+
print(f"❌ {output_path} not found. Assemble phases into output.md first.", file=sys.stderr)
|
|
231
|
+
return 2
|
|
232
|
+
code, out = _run_validate(output_path, json_out=args.json)
|
|
233
|
+
print(out)
|
|
234
|
+
return code
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def cmd_ingest(args) -> int:
|
|
238
|
+
run_dir = Path(args.run_dir)
|
|
239
|
+
output_path = run_dir / "output.md"
|
|
240
|
+
meta_path = run_dir / "run.json"
|
|
241
|
+
|
|
242
|
+
if not output_path.exists():
|
|
243
|
+
print(f"❌ {output_path} not found.", file=sys.stderr)
|
|
244
|
+
return 2
|
|
245
|
+
|
|
246
|
+
code, out = _run_validate(output_path)
|
|
247
|
+
print(out)
|
|
248
|
+
if code != 0:
|
|
249
|
+
print("❌ Validation failed — aborting ingest. Fix structural violations and re-run.", file=sys.stderr)
|
|
250
|
+
return 1
|
|
251
|
+
|
|
252
|
+
# Generate the lean deliverables alongside the audit transcript.
|
|
253
|
+
sys.path.insert(0, str(SCRIPT_DIR))
|
|
254
|
+
import telemetry as _t # noqa: E402
|
|
255
|
+
|
|
256
|
+
md = output_path.read_text()
|
|
257
|
+
try:
|
|
258
|
+
verdict_md = _t.render_verdict(md)
|
|
259
|
+
(run_dir / "verdict.md").write_text(verdict_md)
|
|
260
|
+
tldr_md = _t.render_tldr(md)
|
|
261
|
+
(run_dir / "tldr.md").write_text(tldr_md)
|
|
262
|
+
print(f"📄 Verdict written to {run_dir / 'verdict.md'}", file=sys.stderr)
|
|
263
|
+
print(f"📌 TL;DR written to {run_dir / 'tldr.md'}", file=sys.stderr)
|
|
264
|
+
except Exception as e:
|
|
265
|
+
print(f"⚠️ Could not generate lean deliverables: {e}", file=sys.stderr)
|
|
266
|
+
|
|
267
|
+
meta = {}
|
|
268
|
+
if meta_path.exists():
|
|
269
|
+
try:
|
|
270
|
+
meta = json.loads(meta_path.read_text())
|
|
271
|
+
except json.JSONDecodeError:
|
|
272
|
+
meta = {}
|
|
273
|
+
|
|
274
|
+
cmd = [sys.executable, str(SCRIPT_DIR / "telemetry.py"), "--append", "--log", str(args.log)]
|
|
275
|
+
if args.tokens is not None:
|
|
276
|
+
cmd += ["--tokens", str(args.tokens)]
|
|
277
|
+
if args.wall_clock is not None:
|
|
278
|
+
cmd += ["--wall-clock", str(args.wall_clock)]
|
|
279
|
+
cmd.append(str(output_path))
|
|
280
|
+
|
|
281
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
282
|
+
sys.stdout.write(result.stdout)
|
|
283
|
+
sys.stderr.write(result.stderr)
|
|
284
|
+
if result.returncode != 0:
|
|
285
|
+
return result.returncode
|
|
286
|
+
|
|
287
|
+
# Stamp scenario / metadata into a sidecar so analyze_runs can dimension by it.
|
|
288
|
+
if meta.get("scenario"):
|
|
289
|
+
sidecar_path = Path(args.log).with_suffix(".sidecar.jsonl")
|
|
290
|
+
sidecar_path.parent.mkdir(parents=True, exist_ok=True)
|
|
291
|
+
with sidecar_path.open("a", encoding="utf-8") as f:
|
|
292
|
+
f.write(json.dumps({
|
|
293
|
+
"run_id": meta.get("run_id"),
|
|
294
|
+
"scenario": meta.get("scenario"),
|
|
295
|
+
"subagent_mode": meta.get("subagent_mode"),
|
|
296
|
+
"question": meta.get("question"),
|
|
297
|
+
"ingested_at": _dt.datetime.now(tz=_dt.timezone.utc).isoformat(timespec="seconds"),
|
|
298
|
+
}) + "\n")
|
|
299
|
+
print(f"📎 Sidecar metadata appended to {sidecar_path}", file=sys.stderr)
|
|
300
|
+
|
|
301
|
+
# Upsert the INDEX.jsonl entry: status → ingested + verdict snippet + validation.
|
|
302
|
+
if meta.get("run_id"):
|
|
303
|
+
# Re-read the validator output to pluck the validation summary.
|
|
304
|
+
try:
|
|
305
|
+
json_validation = subprocess.run(
|
|
306
|
+
[sys.executable, str(SCRIPT_DIR / "validate.py"), "--json", str(output_path)],
|
|
307
|
+
capture_output=True, text=True,
|
|
308
|
+
)
|
|
309
|
+
v_payload = json.loads(json_validation.stdout) if json_validation.stdout else {}
|
|
310
|
+
except (json.JSONDecodeError, OSError):
|
|
311
|
+
v_payload = {}
|
|
312
|
+
v = v_payload.get("violations") or []
|
|
313
|
+
validation_summary = {
|
|
314
|
+
"valid": v_payload.get("valid", True),
|
|
315
|
+
"warnings": sum(1 for x in v if x.get("kind") == "warning"),
|
|
316
|
+
"structural_violations": sum(1 for x in v if x.get("kind") == "structural"),
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
rec_snippet = None
|
|
320
|
+
verdict_path = run_dir / "verdict.md"
|
|
321
|
+
if verdict_path.exists():
|
|
322
|
+
rec_snippet = _extract_recommendation_snippet(verdict_path.read_text())
|
|
323
|
+
|
|
324
|
+
runs_dir = run_dir.parent
|
|
325
|
+
_upsert_index(runs_dir, meta["run_id"], {
|
|
326
|
+
"status": "ingested",
|
|
327
|
+
"ingested_at": _dt.datetime.now(tz=_dt.timezone.utc).isoformat(timespec="seconds"),
|
|
328
|
+
"validation": validation_summary,
|
|
329
|
+
"verdict_recommendation": rec_snippet,
|
|
330
|
+
"tokens": args.tokens,
|
|
331
|
+
"wall_clock_seconds": args.wall_clock,
|
|
332
|
+
})
|
|
333
|
+
print(f"📋 INDEX.jsonl updated at {_index_path(runs_dir)}", file=sys.stderr)
|
|
334
|
+
return 0
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
_STATUS_GLYPH = {
|
|
338
|
+
"ingested": "✅",
|
|
339
|
+
"initialized": "▶ ", # in progress
|
|
340
|
+
"failed": "❌",
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _format_index_row(entry: dict) -> tuple[str, str, str, str, str, str]:
|
|
345
|
+
"""Render one INDEX entry as a row tuple for the list table."""
|
|
346
|
+
rid = entry.get("run_id", "?")
|
|
347
|
+
ts = entry.get("ts") or entry.get("created_at", "")
|
|
348
|
+
# Display ts in a friendlier form: 2026-04-25 03:39
|
|
349
|
+
if ts and "T" in ts:
|
|
350
|
+
ts_display = ts.replace("T", " ").rstrip("Z")[:16]
|
|
351
|
+
else:
|
|
352
|
+
ts_display = ts[:16]
|
|
353
|
+
status = entry.get("status", "?")
|
|
354
|
+
glyph = _STATUS_GLYPH.get(status, " ")
|
|
355
|
+
title = entry.get("title", "(untitled)")
|
|
356
|
+
scenario = entry.get("scenario") or "—"
|
|
357
|
+
return (glyph, status, rid, ts_display, scenario, title)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def cmd_list(args) -> int:
|
|
361
|
+
runs_dir = Path(args.runs_dir)
|
|
362
|
+
entries = _read_index(runs_dir)
|
|
363
|
+
|
|
364
|
+
if not entries:
|
|
365
|
+
if not runs_dir.exists():
|
|
366
|
+
print(f"(no runs yet — {runs_dir} does not exist)")
|
|
367
|
+
else:
|
|
368
|
+
print(f"(no runs indexed at {_index_path(runs_dir)})")
|
|
369
|
+
return 0
|
|
370
|
+
|
|
371
|
+
# Filter by status if requested.
|
|
372
|
+
if args.status:
|
|
373
|
+
entries = [e for e in entries if isinstance(e, dict) and e.get("status") == args.status]
|
|
374
|
+
|
|
375
|
+
# Newest first by ts (lexicographic on ISO ts string works).
|
|
376
|
+
entries = sorted(
|
|
377
|
+
[e for e in entries if isinstance(e, dict) and "_raw" not in e],
|
|
378
|
+
key=lambda e: e.get("ts", ""),
|
|
379
|
+
reverse=True,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
if args.json:
|
|
383
|
+
print(json.dumps(entries, indent=2, ensure_ascii=False))
|
|
384
|
+
return 0
|
|
385
|
+
|
|
386
|
+
if args.limit:
|
|
387
|
+
entries = entries[: args.limit]
|
|
388
|
+
|
|
389
|
+
rows = [_format_index_row(e) for e in entries]
|
|
390
|
+
if not rows:
|
|
391
|
+
print(f"(no runs match filter)")
|
|
392
|
+
return 0
|
|
393
|
+
|
|
394
|
+
# Column widths.
|
|
395
|
+
w_status = max(len(r[1]) for r in rows)
|
|
396
|
+
w_id = max(len(r[2]) for r in rows)
|
|
397
|
+
w_ts = max(len(r[3]) for r in rows)
|
|
398
|
+
w_scen = max(len(r[4]) for r in rows)
|
|
399
|
+
|
|
400
|
+
print(f"# Council runs ({len(rows)})")
|
|
401
|
+
print()
|
|
402
|
+
print(f"{'':2} {'STATUS':<{w_status}} {'ID':<{w_id}} {'WHEN':<{w_ts}} {'SCENARIO':<{w_scen}} TITLE")
|
|
403
|
+
print(f"{'':2} {'-' * w_status} {'-' * w_id} {'-' * w_ts} {'-' * w_scen} {'-' * 40}")
|
|
404
|
+
for glyph, status, rid, ts, scen, title in rows:
|
|
405
|
+
print(f"{glyph} {status:<{w_status}} {rid:<{w_id}} {ts:<{w_ts}} {scen:<{w_scen}} {title}")
|
|
406
|
+
return 0
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def cmd_find(args) -> int:
|
|
410
|
+
"""Search INDEX.jsonl for runs whose title/description/scenario match a query."""
|
|
411
|
+
runs_dir = Path(args.runs_dir)
|
|
412
|
+
entries = _read_index(runs_dir)
|
|
413
|
+
if not entries:
|
|
414
|
+
print(f"(no runs at {_index_path(runs_dir)})")
|
|
415
|
+
return 0
|
|
416
|
+
|
|
417
|
+
needle = args.query.lower()
|
|
418
|
+
matches = []
|
|
419
|
+
for e in entries:
|
|
420
|
+
if not isinstance(e, dict) or "_raw" in e:
|
|
421
|
+
continue
|
|
422
|
+
haystack = " ".join(str(e.get(k, "")) for k in ("title", "description", "question", "scenario", "verdict_recommendation"))
|
|
423
|
+
if needle in haystack.lower():
|
|
424
|
+
matches.append(e)
|
|
425
|
+
|
|
426
|
+
if not matches:
|
|
427
|
+
print(f"(no runs match '{args.query}')")
|
|
428
|
+
return 0
|
|
429
|
+
|
|
430
|
+
matches = sorted(matches, key=lambda e: e.get("ts", ""), reverse=True)
|
|
431
|
+
if args.json:
|
|
432
|
+
print(json.dumps(matches, indent=2, ensure_ascii=False))
|
|
433
|
+
return 0
|
|
434
|
+
|
|
435
|
+
for e in matches:
|
|
436
|
+
glyph, status, rid, ts, scen, title = _format_index_row(e)
|
|
437
|
+
print(f"{glyph} {rid} {ts} [{scen}] {title}")
|
|
438
|
+
if e.get("verdict_recommendation"):
|
|
439
|
+
print(f" ↳ {e['verdict_recommendation']}")
|
|
440
|
+
print(f" ↳ {e.get('run_dir', '?')}")
|
|
441
|
+
return 0
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def cmd_show(args) -> int:
|
|
445
|
+
"""Show one INDEX entry in full (run_id can be a prefix)."""
|
|
446
|
+
runs_dir = Path(args.runs_dir)
|
|
447
|
+
entries = _read_index(runs_dir)
|
|
448
|
+
candidates = [e for e in entries if isinstance(e, dict) and e.get("run_id", "").startswith(args.run_id)]
|
|
449
|
+
if not candidates:
|
|
450
|
+
print(f"❌ No run_id matching '{args.run_id}'", file=sys.stderr)
|
|
451
|
+
return 1
|
|
452
|
+
if len(candidates) > 1:
|
|
453
|
+
print(f"❌ Ambiguous prefix '{args.run_id}' — matches {len(candidates)} runs:", file=sys.stderr)
|
|
454
|
+
for c in candidates:
|
|
455
|
+
print(f" - {c.get('run_id')}: {c.get('title')}", file=sys.stderr)
|
|
456
|
+
return 1
|
|
457
|
+
print(json.dumps(candidates[0], indent=2, ensure_ascii=False))
|
|
458
|
+
return 0
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def cmd_reindex(args) -> int:
|
|
462
|
+
"""Rebuild INDEX.jsonl from on-disk run.json files (recovery / migration)."""
|
|
463
|
+
runs_dir = Path(args.runs_dir)
|
|
464
|
+
if not runs_dir.exists():
|
|
465
|
+
print(f"❌ {runs_dir} does not exist", file=sys.stderr)
|
|
466
|
+
return 1
|
|
467
|
+
|
|
468
|
+
# Save existing index to recover ingest-time fields (verdict snippet, validation).
|
|
469
|
+
existing = {e.get("run_id"): e for e in _read_index(runs_dir) if isinstance(e, dict)}
|
|
470
|
+
|
|
471
|
+
rebuilt: list[dict] = []
|
|
472
|
+
for d in sorted(runs_dir.iterdir()):
|
|
473
|
+
if not d.is_dir():
|
|
474
|
+
continue
|
|
475
|
+
meta_path = d / "run.json"
|
|
476
|
+
if not meta_path.exists():
|
|
477
|
+
continue
|
|
478
|
+
try:
|
|
479
|
+
meta = json.loads(meta_path.read_text())
|
|
480
|
+
except json.JSONDecodeError:
|
|
481
|
+
continue
|
|
482
|
+
|
|
483
|
+
rid = meta.get("run_id")
|
|
484
|
+
ts = meta.get("ts") or d.name.split("-", 1)[0]
|
|
485
|
+
question = meta.get("question", "")
|
|
486
|
+
title = meta.get("title") or _auto_title(question)
|
|
487
|
+
description = meta.get("description") or question
|
|
488
|
+
|
|
489
|
+
has_output = (d / "output.md").exists()
|
|
490
|
+
has_verdict = (d / "verdict.md").exists()
|
|
491
|
+
|
|
492
|
+
entry = {
|
|
493
|
+
"schema_version": INDEX_SCHEMA_VERSION,
|
|
494
|
+
"run_id": rid,
|
|
495
|
+
"ts": ts,
|
|
496
|
+
"title": title,
|
|
497
|
+
"description": description,
|
|
498
|
+
"scenario": meta.get("scenario", "—"),
|
|
499
|
+
"run_dir": str(d),
|
|
500
|
+
"created_at": meta.get("created_at"),
|
|
501
|
+
"status": "ingested" if has_verdict else ("initialized" if has_output else "initialized"),
|
|
502
|
+
}
|
|
503
|
+
# Preserve existing ingest-time fields if present.
|
|
504
|
+
prior = existing.get(rid, {})
|
|
505
|
+
for k in ("ingested_at", "validation", "verdict_recommendation", "tokens", "wall_clock_seconds"):
|
|
506
|
+
if k in prior:
|
|
507
|
+
entry[k] = prior[k]
|
|
508
|
+
|
|
509
|
+
# If verdict.md exists but we have no recommendation snippet, derive it.
|
|
510
|
+
if has_verdict and "verdict_recommendation" not in entry:
|
|
511
|
+
try:
|
|
512
|
+
entry["verdict_recommendation"] = _extract_recommendation_snippet(
|
|
513
|
+
(d / "verdict.md").read_text()
|
|
514
|
+
)
|
|
515
|
+
entry["status"] = "ingested"
|
|
516
|
+
except OSError:
|
|
517
|
+
pass
|
|
518
|
+
|
|
519
|
+
rebuilt.append(entry)
|
|
520
|
+
|
|
521
|
+
p = _index_path(runs_dir)
|
|
522
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
523
|
+
with p.open("w", encoding="utf-8") as f:
|
|
524
|
+
for e in rebuilt:
|
|
525
|
+
f.write(json.dumps(e, ensure_ascii=False) + "\n")
|
|
526
|
+
print(f"📋 Rebuilt {p} from {len(rebuilt)} run dir(s).")
|
|
527
|
+
return 0
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def main():
|
|
531
|
+
parser = argparse.ArgumentParser(description="Council run driver.")
|
|
532
|
+
parser.add_argument("--runs-dir", default=str(DEFAULT_RUNS_DIR))
|
|
533
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
534
|
+
|
|
535
|
+
p_init = sub.add_parser("init", help="Create a new run directory.")
|
|
536
|
+
p_init.add_argument("question", help="Restated question (one sentence).")
|
|
537
|
+
p_init.add_argument("--title", default=None, help="Short human-readable title (auto-derived from question if omitted).")
|
|
538
|
+
p_init.add_argument("--description", default=None, help="Longer description (defaults to the full question).")
|
|
539
|
+
p_init.add_argument("--scenario", default="baseline", help="Scenario tag for telemetry (e.g. baseline, external-doc-heavy, three-way, sparse-ops, contradictory, non-cleo, mini, contention).")
|
|
540
|
+
p_init.add_argument("--subagent-mode", action="store_true", help="Mark this run as subagent-mode (default: orchestrator-driven).")
|
|
541
|
+
p_init.set_defaults(func=cmd_init)
|
|
542
|
+
|
|
543
|
+
p_val = sub.add_parser("validate", help="Validate <run-dir>/output.md.")
|
|
544
|
+
p_val.add_argument("run_dir")
|
|
545
|
+
p_val.add_argument("--json", action="store_true")
|
|
546
|
+
p_val.set_defaults(func=cmd_validate)
|
|
547
|
+
|
|
548
|
+
p_ing = sub.add_parser("ingest", help="Validate + telemetry-append a completed run.")
|
|
549
|
+
p_ing.add_argument("run_dir")
|
|
550
|
+
p_ing.add_argument("--log", default=str(DEFAULT_LOG_PATH))
|
|
551
|
+
p_ing.add_argument("--tokens", type=int, default=None)
|
|
552
|
+
p_ing.add_argument("--wall-clock", type=float, default=None)
|
|
553
|
+
p_ing.set_defaults(func=cmd_ingest)
|
|
554
|
+
|
|
555
|
+
p_list = sub.add_parser("list", help="List runs from INDEX.jsonl.")
|
|
556
|
+
p_list.add_argument("--status", default=None, help="Filter by status (initialized | ingested | failed).")
|
|
557
|
+
p_list.add_argument("--limit", type=int, default=None, help="Show only N most recent.")
|
|
558
|
+
p_list.add_argument("--json", action="store_true", help="Emit JSON instead of table.")
|
|
559
|
+
p_list.set_defaults(func=cmd_list)
|
|
560
|
+
|
|
561
|
+
p_find = sub.add_parser("find", help="Search runs by title/description/scenario/verdict text.")
|
|
562
|
+
p_find.add_argument("query", help="Substring to search (case-insensitive).")
|
|
563
|
+
p_find.add_argument("--json", action="store_true")
|
|
564
|
+
p_find.set_defaults(func=cmd_find)
|
|
565
|
+
|
|
566
|
+
p_show = sub.add_parser("show", help="Show one INDEX entry in full.")
|
|
567
|
+
p_show.add_argument("run_id", help="Run id (or prefix — must be unambiguous).")
|
|
568
|
+
p_show.set_defaults(func=cmd_show)
|
|
569
|
+
|
|
570
|
+
p_reidx = sub.add_parser("reindex", help="Rebuild INDEX.jsonl from on-disk run.json files.")
|
|
571
|
+
p_reidx.set_defaults(func=cmd_reindex)
|
|
572
|
+
|
|
573
|
+
args = parser.parse_args()
|
|
574
|
+
sys.exit(args.func(args))
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
if __name__ == "__main__":
|
|
578
|
+
main()
|