@cleocode/skills 2026.4.161 → 2026.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/skills/ct-council/SKILL.md +377 -0
  3. package/skills/ct-council/optimization/HARDENING-PLAYBOOK.md +107 -0
  4. package/skills/ct-council/optimization/README.md +74 -0
  5. package/skills/ct-council/optimization/scenarios.yaml +121 -0
  6. package/skills/ct-council/optimization/scripts/campaign.py +543 -0
  7. package/skills/ct-council/optimization/scripts/test_campaign.py +143 -0
  8. package/skills/ct-council/references/chairman.md +119 -0
  9. package/skills/ct-council/references/contrarian.md +70 -0
  10. package/skills/ct-council/references/evidence-pack.md +145 -0
  11. package/skills/ct-council/references/examples.md +235 -0
  12. package/skills/ct-council/references/executor.md +83 -0
  13. package/skills/ct-council/references/expansionist.md +68 -0
  14. package/skills/ct-council/references/first-principles.md +73 -0
  15. package/skills/ct-council/references/outsider.md +73 -0
  16. package/skills/ct-council/references/peer-review.md +125 -0
  17. package/skills/ct-council/scripts/analyze_runs.py +293 -0
  18. package/skills/ct-council/scripts/fixtures/executor_multi.md +198 -0
  19. package/skills/ct-council/scripts/fixtures/missing_advisor.md +117 -0
  20. package/skills/ct-council/scripts/fixtures/missing_convergence.md +190 -0
  21. package/skills/ct-council/scripts/fixtures/thin_evidence.md +193 -0
  22. package/skills/ct-council/scripts/fixtures/valid.md +226 -0
  23. package/skills/ct-council/scripts/fixtures/valid_with_llmtxt.md +226 -0
  24. package/skills/ct-council/scripts/llmtxt_ref.py +223 -0
  25. package/skills/ct-council/scripts/run_council.py +578 -0
  26. package/skills/ct-council/scripts/telemetry.py +624 -0
  27. package/skills/ct-council/scripts/test_telemetry.py +509 -0
  28. package/skills/ct-council/scripts/test_validate.py +452 -0
  29. package/skills/ct-council/scripts/validate.py +396 -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()