wizard-central-toolkit 0.2.0__py3-none-any.whl

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 (77) hide show
  1. wiz_central_toolkit/__init__.py +2 -0
  2. wiz_central_toolkit/bannerwiz/__main__.py +6 -0
  3. wiz_central_toolkit/bannerwiz/cli.py +79 -0
  4. wiz_central_toolkit/bannerwiz/lib/__init__.py +1 -0
  5. wiz_central_toolkit/bannerwiz/lib/banner_wizard.py +999 -0
  6. wiz_central_toolkit/bannerwiz/lib/banner_wrapper.py +199 -0
  7. wiz_central_toolkit/bannerwiz/lib/cli.py +79 -0
  8. wiz_central_toolkit/codeguardwiz/__init__.py +1 -0
  9. wiz_central_toolkit/codeguardwiz/__main__.py +19 -0
  10. wiz_central_toolkit/codeguardwiz/cli.py +971 -0
  11. wiz_central_toolkit/codeguardwiz/codeguard_wizard.py +269 -0
  12. wiz_central_toolkit/codeguardwiz/rust_engine.py +348 -0
  13. wiz_central_toolkit/codeguardwiz/static_engine.py +307 -0
  14. wiz_central_toolkit/common/__init__.py +1 -0
  15. wiz_central_toolkit/debugwiz/__init__.py +1 -0
  16. wiz_central_toolkit/debugwiz/__main__.py +360 -0
  17. wiz_central_toolkit/debugwiz/debug_wizard.py +364 -0
  18. wiz_central_toolkit/diffwiz/__init__.py +0 -0
  19. wiz_central_toolkit/diffwiz/__main__.py +21 -0
  20. wiz_central_toolkit/diffwiz/cli.py +134 -0
  21. wiz_central_toolkit/diffwiz/lib/__init__.py +1 -0
  22. wiz_central_toolkit/diffwiz/lib/cli.py +144 -0
  23. wiz_central_toolkit/diffwiz/lib/diff_wizard.py +89 -0
  24. wiz_central_toolkit/godfatherwiz/__init__.py +0 -0
  25. wiz_central_toolkit/godfatherwiz/__main__.py +18 -0
  26. wiz_central_toolkit/godfatherwiz/ai.py +136 -0
  27. wiz_central_toolkit/godfatherwiz/lib/__init__.py +1 -0
  28. wiz_central_toolkit/godfatherwiz/lib/cli.py +925 -0
  29. wiz_central_toolkit/godfatherwiz/lib/godfather_wizard.py +1552 -0
  30. wiz_central_toolkit/jsonwiz/__init__.py +0 -0
  31. wiz_central_toolkit/jsonwiz/__main__.py +21 -0
  32. wiz_central_toolkit/jsonwiz/cli.py +4 -0
  33. wiz_central_toolkit/jsonwiz/lib/__init__.py +1 -0
  34. wiz_central_toolkit/jsonwiz/lib/cli.py +138 -0
  35. wiz_central_toolkit/jsonwiz/lib/json_wizard.py +133 -0
  36. wiz_central_toolkit/pythonwiz/__init__.py +0 -0
  37. wiz_central_toolkit/pythonwiz/__main__.py +21 -0
  38. wiz_central_toolkit/pythonwiz/lib/__init__.py +1 -0
  39. wiz_central_toolkit/pythonwiz/lib/cli.py +247 -0
  40. wiz_central_toolkit/pythonwiz/lib/python_wizard.py +353 -0
  41. wiz_central_toolkit/readwiz/__init__.py +5 -0
  42. wiz_central_toolkit/readwiz/__main__.py +15 -0
  43. wiz_central_toolkit/readwiz/cli.py +46 -0
  44. wiz_central_toolkit/readwiz/lib/__init__.py +1 -0
  45. wiz_central_toolkit/readwiz/lib/cli.py +47 -0
  46. wiz_central_toolkit/readwiz/lib/read_wizard.py +121 -0
  47. wiz_central_toolkit/rustwiz/__init__.py +5 -0
  48. wiz_central_toolkit/rustwiz/__main__.py +7 -0
  49. wiz_central_toolkit/rustwiz/cli.py +4 -0
  50. wiz_central_toolkit/rustwiz/lib/__init__.py +1 -0
  51. wiz_central_toolkit/rustwiz/lib/cli.py +53 -0
  52. wiz_central_toolkit/rustwiz/lib/rust_wizard.py +128 -0
  53. wiz_central_toolkit/tmuxwiz/__init__.py +0 -0
  54. wiz_central_toolkit/tmuxwiz/__main__.py +15 -0
  55. wiz_central_toolkit/tmuxwiz/cli.py +7 -0
  56. wiz_central_toolkit/tmuxwiz/lib/__init__.py +1 -0
  57. wiz_central_toolkit/tmuxwiz/lib/cli.py +10 -0
  58. wiz_central_toolkit/tmuxwiz/lib/tmux_wizard.py +229 -0
  59. wiz_central_toolkit/todowiz/__init__.py +0 -0
  60. wiz_central_toolkit/todowiz/__main__.py +22 -0
  61. wiz_central_toolkit/todowiz/cli.py +4 -0
  62. wiz_central_toolkit/todowiz/lib/__init__.py +1 -0
  63. wiz_central_toolkit/todowiz/lib/cli.py +82 -0
  64. wiz_central_toolkit/todowiz/lib/todo_wizard.py +134 -0
  65. wiz_central_toolkit/tools_registry.py +62 -0
  66. wiz_central_toolkit/wiz/__init__.py +0 -0
  67. wiz_central_toolkit/wiz/__main__.py +141 -0
  68. wiz_central_toolkit/writewiz/__init__.py +1 -0
  69. wiz_central_toolkit/writewiz/__main__.py +9 -0
  70. wiz_central_toolkit/writewiz/cli.py +264 -0
  71. wiz_central_toolkit/writewiz/write_wizard.py +384 -0
  72. wizard_central_toolkit-0.2.0.dist-info/METADATA +203 -0
  73. wizard_central_toolkit-0.2.0.dist-info/RECORD +77 -0
  74. wizard_central_toolkit-0.2.0.dist-info/WHEEL +5 -0
  75. wizard_central_toolkit-0.2.0.dist-info/entry_points.txt +50 -0
  76. wizard_central_toolkit-0.2.0.dist-info/licenses/LICENSE +201 -0
  77. wizard_central_toolkit-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,925 @@
1
+ """godfather-wizard face — the Grand Master, fleet dispatcher & REPL."""
2
+
3
+ import json
4
+ import re
5
+ import sys
6
+ import readline
7
+
8
+ from collections import Counter
9
+ from pathlib import Path
10
+ from .godfather_wizard import (
11
+ FATHER_LAW, call_sibling, call_internal, call_ollama, speak_if_enabled,
12
+ truth_anchor, classify_route, seated_model, seated_models, seat_for,
13
+ seat_label, seat_unavailable_line, set_model_assignment,
14
+ unset_model_assignment,
15
+ brainstorm_reply, codegen_current_content, codegen_proposal,
16
+ render_codegen_diff, confirm_codegen_write, stage_codegen_content,
17
+ land_codegen_content, fleet_census, resolve_sibling, load_routes,
18
+ save_routes, journal, ledger_rows, _now, schedule_task,
19
+ queue_tasks, render_banner, list_all_slash_commands,
20
+ fleet_invocable_names,
21
+ session_load, session_save)
22
+
23
+ USAGE = """Godfather Wizard — the Grand Master, fleet dispatcher & REPL
24
+ alias: godfatherwiz
25
+ usage: godfather_wizard <command> [args]
26
+
27
+ CLI Commands:
28
+ godfather_wizard audit <path> truth anchor + codeguard_wizard audit
29
+ godfather_wizard task <intent> todo_wizard intake
30
+ godfather_wizard run <directive> deterministic keyword router
31
+ godfather_wizard routes print routing table
32
+ godfather_wizard fleet bin-lane census
33
+ godfather_wizard law print FATHER LAW
34
+ godfather_wizard truth <path> anchor ground truth (internal senses)
35
+ godfather_wizard diagnose <dir> debug_wizard audit + classify
36
+ godfather_wizard heal <file> debug_wizard propose → write_wizard wall
37
+ godfather_wizard status fleet-wide health check
38
+ godfather_wizard repl launch interactive REPL cockpit
39
+ godfather_wizard siblings list all registered citizens
40
+ godfather_wizard verify <wizard> single citizen health check
41
+ godfather_wizard consent <file> force write_wizard commit prompt
42
+ godfather_wizard schedule <intent> queue task
43
+ godfather_wizard queue show scheduled tasks
44
+ godfather_wizard history run history across delegations
45
+ godfather_wizard stats fleet-wide execution counts
46
+ godfather_wizard versions law drift detection
47
+ godfather_wizard help [command] full usage, or one subcommand's usage
48
+ godfather_wizard census full fleet roster
49
+ godfather_wizard models show providers, live seats + assignments
50
+ godfather_wizard models set <task> <model>
51
+ assign a seat to default, brainstorm,
52
+ build, debug, diff, or audit — a bare
53
+ Ollama model name, or provider:model
54
+ (ollama, meta, anthropic, openai,
55
+ google, deepseek)
56
+ godfather_wizard models unset <task>
57
+ remove a per-task model assignment
58
+
59
+ exit codes: 0 ok | 1 failed | 2 out of scope"""
60
+
61
+ CMDS = ("audit", "task", "run", "routes", "fleet", "law", "truth", "diagnose",
62
+ "heal", "status", "repl", "siblings", "verify", "consent", "schedule",
63
+ "queue", "history", "stats", "versions", "help", "census", "models")
64
+
65
+ def _fail(msg):
66
+ print(f"[FAIL] {msg}")
67
+ return 1
68
+
69
+ def _confirm_write_class(pipeline):
70
+ """The double ask (house law) for a write-class route.
71
+
72
+ First the plain ask; then a typed confirmation naming the
73
+ action being agreed to. Anything but the two exact answers —
74
+ including EOF — is a no, and nothing executes.
75
+ """
76
+ action = " ".join(pipeline)
77
+ try:
78
+ first = input(f"āš ļø write-class route: {action}\n"
79
+ "this changes files. proceed? [y/N] ")
80
+ except (KeyboardInterrupt, EOFError):
81
+ return False
82
+ if first.strip().lower() != "y":
83
+ return False
84
+ try:
85
+ typed = input(f"confirm by typing: yes, {action}\n> ")
86
+ except (KeyboardInterrupt, EOFError):
87
+ return False
88
+ return typed.strip() == f"yes, {action}"
89
+
90
+ def main():
91
+ try:
92
+ return _dispatch()
93
+ except Exception as e:
94
+ print(f"[FAIL] {type(e).__name__}: {e}")
95
+ return 1
96
+
97
+ def _dispatch():
98
+ args = sys.argv[1:]
99
+ if args and args[0] in ("-h", "--help"):
100
+ print(USAGE)
101
+ return 0
102
+ if not args:
103
+ # bare invocation = the throne room: banner, voice, chat, slash commands
104
+ return run_repl()
105
+ if args[0] not in CMDS:
106
+ print(USAGE)
107
+ return 2
108
+
109
+ cmd = args[0]
110
+
111
+ # === CORE DISPATCHERS ===
112
+ if cmd == "law":
113
+ print(FATHER_LAW)
114
+ return 0
115
+
116
+ if cmd == "versions":
117
+ import hashlib
118
+ print("father_law_sha256:", hashlib.sha256(FATHER_LAW.encode()).hexdigest())
119
+ return 0
120
+
121
+ if cmd == "census":
122
+ count, names = fleet_census()
123
+ print(f"[OK] {count} certified citizens on the fleet")
124
+ for i, n in enumerate(names, 1):
125
+ print(f" #{i:02d} {n}")
126
+ return 0
127
+
128
+ if cmd == "models":
129
+ if len(args) == 1:
130
+ rc, out = seated_models()
131
+ print(out)
132
+ return rc
133
+ if args[1] == "set" and len(args) == 4:
134
+ rc, out = set_model_assignment(args[2], args[3])
135
+ print(out)
136
+ return rc
137
+ if args[1] == "unset" and len(args) == 3:
138
+ rc, out = unset_model_assignment(args[2])
139
+ print(out)
140
+ return rc
141
+ print(USAGE)
142
+ return 2
143
+
144
+ if cmd == "help":
145
+ if len(args) < 2:
146
+ print(USAGE)
147
+ return 0
148
+ target = args[1]
149
+ for line in USAGE.splitlines():
150
+ if target in line:
151
+ print(line.strip())
152
+ return 0
153
+
154
+ if cmd == "routes":
155
+ routes = load_routes()
156
+ # The bank also holds scheduled-task records (task_* keys,
157
+ # dict values) — those are the queue's business, not routes.
158
+ # The routing table shows actual routes only.
159
+ shown = {k: v for k, v in routes.items()
160
+ if not k.startswith("task_") and isinstance(v, list)}
161
+ if not shown:
162
+ print("(routing table empty)")
163
+ return 0
164
+ for k, v in shown.items():
165
+ print(f" {k:25s} -> {' '.join(v)}")
166
+ return 0
167
+
168
+ if cmd == "fleet":
169
+ count, names = fleet_census()
170
+ print(f"[OK] {count} sibling wizards installed")
171
+ for n in names:
172
+ print(f" {n}")
173
+ return 0
174
+
175
+ if cmd == "siblings":
176
+ count, names = fleet_census()
177
+ for i, n in enumerate(names, 1):
178
+ print(f"[{i:2d}] {n}")
179
+ return 0
180
+
181
+ if cmd == "history":
182
+ rows = ledger_rows()
183
+ if not rows:
184
+ print("(ledger empty — no dispatches yet)")
185
+ return 0
186
+ for row in rows:
187
+ print(f"{row['at']} {row['event']:16s} {row['target']} {row['detail']}")
188
+ return 0
189
+
190
+ if cmd == "stats":
191
+ counts = Counter(r["event"] for r in ledger_rows())
192
+ print(dict(counts) if counts else "(no events recorded)")
193
+ return 0
194
+
195
+ if cmd == "status":
196
+ # Probe the fleet that is actually installed — the same census
197
+ # fleet/siblings report. Absent citizens are not probed at all.
198
+ _count, installed = fleet_census()
199
+ if not installed:
200
+ print("[FAIL] no sibling wizards installed")
201
+ return 1
202
+ worst = 0
203
+ for w in installed:
204
+ if resolve_sibling(w) is None:
205
+ print(f"[FAIL] {w} unresponsive")
206
+ worst = 1
207
+ continue
208
+ rc, out = call_sibling([w, "--help"])
209
+ # exit 2 = usage fallthrough on --help, NOT death. Death = rc 1.
210
+ alive = rc in (0, 2)
211
+ print(f"[{'OK' if alive else 'FAIL'}] {w} {'reachable' if alive else 'unresponsive'}")
212
+ if not alive:
213
+ worst = 1
214
+ return worst
215
+
216
+ if cmd == "repl":
217
+ return run_repl()
218
+
219
+ # === PIPELINERS ===
220
+ if cmd == "audit":
221
+ if len(args) < 2:
222
+ return _fail("audit requires a path")
223
+ rc_overall = 0
224
+ print("--- PHASE 1: truth anchor ---")
225
+ rc, out = truth_anchor(args[1])
226
+ print(out or "(no output)")
227
+ if rc != 0:
228
+ rc_overall = 1
229
+ print("--- PHASE 2: codeguard_wizard audit ---")
230
+ rc, out = call_sibling(["codeguard_wizard", "audit", args[1]])
231
+ print(out or "(no output)")
232
+ if rc != 0:
233
+ rc_overall = 1
234
+ print(f"[{'OK' if rc_overall == 0 else 'FAIL'}] audit pipeline complete")
235
+ return rc_overall
236
+
237
+ if cmd == "task":
238
+ if len(args) < 2:
239
+ return _fail("task requires an intent")
240
+ intent = " ".join(args[1:])
241
+ # todowiz alone carries the task now — there is no second
242
+ # phase, and the output must not claim one ran.
243
+ print("--- todo_wizard intake ---")
244
+ rc, out = call_sibling(["todo_wizard", "add", intent])
245
+ print(out or "(no output)")
246
+ if rc != 0:
247
+ print("[FAIL] task pipeline complete")
248
+ return 1
249
+ print("[OK] task pipeline complete")
250
+ return 0
251
+
252
+ if cmd == "run":
253
+ if len(args) < 2:
254
+ return _fail("run requires a directive")
255
+ directive = " ".join(args[1:])
256
+ rc, anchor = truth_anchor(".")
257
+ print(f"--- truth anchor (law 2) ---")
258
+ print(anchor or "(no output)")
259
+
260
+ pipeline, reason, wall = classify_route(directive, [])
261
+ if pipeline is None and wall:
262
+ # Unmapped mutation intent — refused at the wall, as
263
+ # it always has been; the lawful road is writewiz.
264
+ print(f"[CONSENT WALL] {reason}")
265
+ print("to mutate: echo 'content' | write_wizard propose <path>")
266
+ print("then answer the wall yourself: write_wizard commit <path>")
267
+ journal("WALL_ROUTED", directive, "mutation detected")
268
+ return 0
269
+ if pipeline is None:
270
+ print("out of scope — no route matches this directive")
271
+ print("run 'godfather_wizard routes' to see the lawful table")
272
+ journal("ROUTED", directive, "out of scope")
273
+ return 2
274
+ if wall:
275
+ # A write-class route: there is no MCP gate in this
276
+ # project, so the double ask is the load-bearing wall.
277
+ print(f"[CONSENT WALL] {reason}")
278
+ if not _confirm_write_class(pipeline):
279
+ print("[FAIL] consent withheld — nothing executed")
280
+ journal("WALL_ROUTED", directive, "consent withheld")
281
+ return 0
282
+ journal("WALL_ROUTED", directive, "consent given — executing")
283
+
284
+ journal("ROUTED", directive, reason)
285
+ print(f"[ROUTE] {reason}")
286
+ print(f"--- executing: {' '.join(pipeline)} ---")
287
+ internal = call_internal(pipeline)
288
+ if internal is not None:
289
+ rc, out = internal
290
+ else:
291
+ rc, out = call_sibling(pipeline)
292
+ print(out or "(no output)")
293
+ if rc == 0:
294
+ print("[OK] pipeline complete")
295
+ else:
296
+ print(f"[FAIL] pipeline ended with exit {rc}")
297
+ return rc
298
+
299
+ if cmd == "truth":
300
+ if len(args) < 2:
301
+ return _fail("truth requires a path")
302
+ rc, out = truth_anchor(args[1])
303
+ print(out or "(no output)")
304
+ return rc
305
+
306
+ if cmd == "diagnose":
307
+ if len(args) < 2:
308
+ return _fail("diagnose requires a directory")
309
+ print("--- PHASE 1: debug_wizard audit ---")
310
+ rc, out = call_sibling(["debug_wizard", "audit", args[1]])
311
+ print(out or "(no output)")
312
+ rc_overall = 1 if rc != 0 else 0
313
+ # Phase 2: classify the worst error file the census can name.
314
+ # debugwiz has no 'triage' — hotspot ranks the error files and
315
+ # classify breaks the worst one down.
316
+ rc, out = call_sibling(["debug_wizard", "hotspot", args[1]])
317
+ worst_file = None
318
+ for line in out.splitlines():
319
+ m = re.match(r"\[HOT\] \d+ errors\s+(.+)$", line.strip())
320
+ if m:
321
+ worst_file = m.group(1).strip()
322
+ break
323
+ if worst_file is None:
324
+ print("--- PHASE 2: no error files — nothing to classify ---")
325
+ return rc_overall
326
+ print(f"--- PHASE 2: debug_wizard classify {worst_file} ---")
327
+ rc, out = call_sibling(["debug_wizard", "classify", worst_file])
328
+ print(out or "(no output)")
329
+ if rc == 2: # usage error — the chain itself misfired
330
+ rc_overall = 1
331
+ return rc_overall
332
+
333
+ if cmd == "heal":
334
+ if len(args) < 2:
335
+ return _fail("heal requires a file path")
336
+ target = Path(args[1]).expanduser()
337
+ audit_root = target if target.is_dir() else target.parent
338
+ print("--- PHASE 1: debug_wizard audit ---")
339
+ rc, out = call_sibling(["debug_wizard", "audit", str(audit_root)])
340
+ print(out or "(no output)")
341
+ if rc == 0:
342
+ print("[OK] nothing sick — nothing to heal")
343
+ return 0
344
+ if rc != 1:
345
+ # Not the "sickness found" verdict — the chain itself broke
346
+ # (usage error or a sibling that would not answer).
347
+ return 1
348
+ # Name the patient: the file asked for, if it is among the
349
+ # sick — otherwise the worst file the hotspot census names.
350
+ rc, out = call_sibling(["debug_wizard", "hotspot", str(audit_root)])
351
+ sick = []
352
+ for line in out.splitlines():
353
+ m = re.match(r"\[HOT\] \d+ errors\s+(.+)$", line.strip())
354
+ if m:
355
+ sick.append(m.group(1).strip())
356
+ if target.is_file():
357
+ patient = None
358
+ for f in sick:
359
+ try:
360
+ if Path(f).resolve() == target.resolve():
361
+ patient = target
362
+ break
363
+ except OSError:
364
+ continue
365
+ if patient is None:
366
+ print(f"[OK] {target} shows no errors — nothing to heal in it")
367
+ return 0
368
+ elif sick:
369
+ patient = Path(sick[0])
370
+ else:
371
+ return _fail("audit reported sickness but no file could be named")
372
+ print(f"--- PHASE 2: debug_wizard propose {patient} ---")
373
+ rc, out = call_sibling(["debug_wizard", "propose", str(patient)])
374
+ print(out or "(no output)")
375
+ if rc != 0:
376
+ return 1
377
+ fix_target = Path.home() / ".debug-wizard" / f"fix_{patient.stem}.txt"
378
+ print(f"handoff: write_wizard commit {fix_target} # your keystroke, Governor")
379
+ return 0
380
+
381
+ if cmd == "verify":
382
+ if len(args) < 2:
383
+ return _fail("verify requires a wizard name")
384
+ name = args[1]
385
+ if resolve_sibling(name) is None:
386
+ # Not installed is out of scope (Law 5: exit 2) — a different
387
+ # fact from installed-but-unresponsive, and reported as such.
388
+ print(f"[FAIL] {name} not installed")
389
+ return 2
390
+ rc, out = call_sibling([name, "--help"])
391
+ # exit 2 = usage fallthrough on --help, NOT death. Death = rc 1.
392
+ alive = rc in (0, 2)
393
+ print(f"[{'OK' if alive else 'FAIL'}] {name} {'healthy' if alive else 'unresponsive'}")
394
+ return 0 if alive else 1
395
+
396
+ if cmd == "consent":
397
+ if len(args) < 2:
398
+ return _fail("consent requires a file path")
399
+ print(f"write_wizard commit {args[1]} # your keystroke, Governor")
400
+ return 0
401
+
402
+ if cmd == "schedule":
403
+ if len(args) < 2:
404
+ return _fail("schedule requires an intent")
405
+ intent = " ".join(args[1:])
406
+ task_id = schedule_task(intent)
407
+ print(f"[OK] task scheduled: id={task_id}, intent={intent}")
408
+ return 0
409
+
410
+ if cmd == "queue":
411
+ tasks = queue_tasks()
412
+ if not tasks:
413
+ print("(queue empty)")
414
+ return 0
415
+ for t in tasks:
416
+ print(f"[{t['status']}] {t['id']} — {t['intent']}")
417
+ return 0
418
+
419
+ print("out of scope")
420
+ return 2
421
+
422
+ # ============================================================================
423
+ # INTERACTIVE REPL MODE — THE COCKPIT
424
+ # ============================================================================
425
+
426
+ def _load_cfg():
427
+ import json
428
+ from pathlib import Path as _P
429
+ cfgf = _P.home() / ".godfather" / "config.json"
430
+ try:
431
+ return json.loads(cfgf.read_text())
432
+ except Exception:
433
+ return {}
434
+
435
+ def _save_cfg(cfg):
436
+ import json
437
+ from pathlib import Path as _P
438
+ cfgf = _P.home() / ".godfather" / "config.json"
439
+ cfgf.parent.mkdir(parents=True, exist_ok=True)
440
+ cfgf.write_text(json.dumps(cfg, indent=2))
441
+
442
+ def _pick_model():
443
+ """Numbered model dropdown — picks from the LIVE Ollama roster, persists choice."""
444
+ import os
445
+ import subprocess
446
+ print("\nšŸ’¾ MODEL SELECTOR (live Ollama roster)")
447
+ try:
448
+ r = subprocess.run(["ollama", "list"], capture_output=True, text=True, timeout=60)
449
+ names = [ln.split()[0] for ln in r.stdout.strip().splitlines()[1:] if ln.strip()]
450
+ except (OSError, subprocess.SubprocessError):
451
+ names = []
452
+ if not names:
453
+ print("[FAIL] no models found — is Ollama running?")
454
+ return
455
+ for i, n in enumerate(names, 1):
456
+ print(f" [{i}] {n}")
457
+ try:
458
+ idx_pick = int(input("šŸ‘‘ model # ").strip()) - 1
459
+ if 0 <= idx_pick < len(names):
460
+ chosen = names[idx_pick]
461
+ os.environ["GODFATHER_MODEL"] = chosen
462
+ cfg = _load_cfg()
463
+ cfg["model"] = chosen
464
+ _save_cfg(cfg)
465
+ print(f"[OK] oracle set to {chosen} (persisted in ~/.godfather/config.json)")
466
+ else:
467
+ print("[FAIL] invalid selection")
468
+ except ValueError:
469
+ print("[FAIL] enter a number")
470
+ except (KeyboardInterrupt, EOFError):
471
+ print("\n[OK] model selection cancelled — Sovereignty preserved.")
472
+
473
+ def _pick_voice():
474
+ """Universal voice picker — direct disk discovery, no device assumptions.
475
+ Scans every plausible Piper root on THIS machine + env overrides.
476
+ Pairing law: a voice is real only with its .onnx.json config beside it."""
477
+ import os
478
+ from pathlib import Path as _P
479
+ print("\nšŸŽ™ļø VOICE SELECTOR (universal disk discovery)")
480
+
481
+ home = _P.home()
482
+ roots = []
483
+ for var in ("VOICE_ROOT", "PAPER_VOICE_ROOT", "PIPER_VOICE_PATH"):
484
+ val = os.environ.get(var)
485
+ if val:
486
+ roots.append(_P(val))
487
+ roots.extend([
488
+ home / "piper" / "voices",
489
+ home / ".local" / "share" / "piper" / "voices",
490
+ home / "piper-voices",
491
+ _P("/usr/share/piper/voices"),
492
+ _P("/usr/local/share/piper/voices"),
493
+ home / ".config" / "piper" / "voices",
494
+ ])
495
+
496
+ found = {} # name -> root it lives in
497
+ for root in roots:
498
+ if not root.is_dir():
499
+ continue
500
+ for onnx in root.glob("*.onnx"):
501
+ if (root / (onnx.name + ".json")).exists() or onnx.with_suffix(".onnx.json").exists():
502
+ found.setdefault(onnx.stem, root)
503
+
504
+ if not found:
505
+ print("[FAIL] no paired Piper voices found on this machine")
506
+ print("roots scanned:")
507
+ for r in roots:
508
+ print(f" {r} {'(exists)' if r.is_dir() else ''}")
509
+ print("hint: export VOICE_ROOT=<your voices dir> and retry")
510
+ return
511
+
512
+ # active voice: first .active found in any root
513
+ active = ""
514
+ for r in roots:
515
+ af = r / ".active"
516
+ if af.is_file():
517
+ active = af.read_text().strip()
518
+ break
519
+
520
+ names = sorted(found)
521
+ print(f"found {len(names)} voice(s):")
522
+ for i, v in enumerate(names, 1):
523
+ mark = " <-- active" if v == active else ""
524
+ print(f" [{i:2d}] {v}{mark}")
525
+
526
+ try:
527
+ pick = input("šŸ‘‘ voice # ").strip()
528
+ idx = int(pick) - 1
529
+ except ValueError:
530
+ print("[FAIL] enter a number")
531
+ return
532
+ except (KeyboardInterrupt, EOFError):
533
+ print("\n[OK] voice selection cancelled — Sovereignty preserved.")
534
+ return
535
+ if not (0 <= idx < len(names)):
536
+ print(f"[FAIL] pick 1-{len(names)}")
537
+ return
538
+
539
+ chosen = names[idx]
540
+ root = found[chosen]
541
+ (root / ".active").write_text(chosen)
542
+ cfg = _load_cfg()
543
+ cfg["voice"] = chosen
544
+ cfg["voice_root"] = str(root)
545
+ _save_cfg(cfg)
546
+ # offer immediate ear test
547
+ try:
548
+ rc, out = call_sibling(["voicewiz", "speak", "voice set to " + chosen])
549
+ if rc != 0:
550
+ print(f"[OK] voice set to {chosen} (no ear test — voicewiz speak unavailable)")
551
+ else:
552
+ print(f"[OK] voice set to {chosen} — did you hear it?")
553
+ except Exception:
554
+ print(f"[OK] voice set to {chosen}")
555
+
556
+ def _classify_repl_input(user_input):
557
+ """Return the action and arguments for one REPL line."""
558
+ text = user_input.strip()
559
+
560
+ if not text:
561
+ return ("empty", [])
562
+
563
+ if text in ("/quit", "/exit"):
564
+ return ("quit", [])
565
+
566
+ if text == "/help":
567
+ return ("help", [])
568
+
569
+ if text == "/voices":
570
+ return ("voices", [])
571
+
572
+ if text == "/model":
573
+ return ("model", [])
574
+
575
+ if text == "/more-commands":
576
+ return ("more-commands", [])
577
+
578
+ if text.startswith("/wiz"):
579
+ tokens = text.split()
580
+ return ("wiz", tokens[1:])
581
+
582
+ if text.startswith("/"):
583
+ parts = text.split()
584
+ return ("slash", parts)
585
+
586
+ return ("chat", [text])
587
+
588
+ def _codegen_path_key(path):
589
+ return str(Path(path).expanduser().resolve())
590
+
591
+
592
+ def _prepare_codegen(task, target_token, instruction, last_proposal,
593
+ effective_instruction=None):
594
+ """Current truth + a proposal for one codegen command.
595
+
596
+ Returns (proposal, current, reused, error_line). A reviewed
597
+ /diff proposal is reused only for the same path and instruction
598
+ and only while the file still matches the reviewed base.
599
+ """
600
+ target = Path(target_token).expanduser()
601
+ current, error = codegen_current_content(target)
602
+ if error:
603
+ return None, None, False, error
604
+ if task == "debug" and not target.is_file():
605
+ return None, None, False, f"[FAIL] debug requires a file: {target}"
606
+ if last_proposal \
607
+ and _codegen_path_key(last_proposal.get("path", "")) \
608
+ == _codegen_path_key(target) \
609
+ and last_proposal.get("instruction") == instruction \
610
+ and last_proposal.get("base_content") == current:
611
+ proposal = dict(last_proposal)
612
+ proposal["task"] = task
613
+ return proposal, current, True, None
614
+ extra = ""
615
+ if task == "debug":
616
+ notes = instruction or "no user notes"
617
+ if target.suffix == ".py":
618
+ rc, out = call_sibling(["python_wizard", "check", str(target)])
619
+ extra = (f"User notes: {notes}\n"
620
+ f"python_wizard check exit {rc}:\n{out or '(no output)'}")
621
+ else:
622
+ extra = f"User notes: {notes}"
623
+ proposal, error = codegen_proposal(
624
+ task, target, effective_instruction or instruction, current,
625
+ extra_context=extra)
626
+ if error:
627
+ return None, current, False, error
628
+ proposal["instruction"] = instruction
629
+ return proposal, current, False, None
630
+
631
+
632
+ def _review_codegen_diff(target, proposal, current):
633
+ if target.exists():
634
+ print(f"[NOTE] {target} exists — this is a full-content revision")
635
+ seat = proposal.get("seat")
636
+ byline = (f"seat: {seat_label(seat)}" if seat
637
+ else proposal["model"])
638
+ print(f"--- {proposal['task']} proposal by {byline} "
639
+ f"for {target} ---")
640
+ print(render_codegen_diff(target, current, proposal["content"]))
641
+
642
+
643
+ def _diff_codegen(args, last_proposal):
644
+ if len(args) < 3:
645
+ print("[FAIL] /diff requires a path and an instruction")
646
+ return last_proposal
647
+ target = Path(args[1]).expanduser()
648
+ instruction = " ".join(args[2:])
649
+ proposal, current, reused, error = _prepare_codegen(
650
+ "diff", args[1], instruction, last_proposal)
651
+ if error:
652
+ print(error)
653
+ return last_proposal
654
+ if reused:
655
+ print("the proposal you reviewed — no new seat call")
656
+ _review_codegen_diff(target, proposal, current)
657
+ proposal["base_content"] = current
658
+ return proposal
659
+
660
+
661
+ def _write_codegen(task, args, last_proposal):
662
+ if task == "build" and len(args) < 3:
663
+ print("[FAIL] /build requires a path and an instruction")
664
+ return
665
+ if task == "debug" and len(args) < 2:
666
+ print("[FAIL] /debug requires a file path")
667
+ return
668
+ target = Path(args[1]).expanduser()
669
+ instruction = " ".join(args[2:])
670
+ effective = instruction or "Repair the errors in this file."
671
+ proposal, current, reused, error = _prepare_codegen(
672
+ task, args[1], instruction, last_proposal,
673
+ effective_instruction=effective)
674
+ if error:
675
+ print(error)
676
+ return
677
+ if reused:
678
+ print("the proposal you reviewed — no new seat call")
679
+ _review_codegen_diff(target, proposal, current)
680
+ if proposal["content"] == current:
681
+ print("[OK] no changes — nothing staged, nothing written")
682
+ return
683
+ ok, detail = stage_codegen_content(target, proposal["content"])
684
+ if not ok:
685
+ print(f"[FAIL] proposal could not stage via write_wizard: {detail}")
686
+ return
687
+ print(f"[STAGED] {task} proposal for {target} — {detail}")
688
+ if not confirm_codegen_write(target):
689
+ print("[CONSENT WALL] consent withheld — nothing written; "
690
+ "the staged proposal stays in write_wizard's quarantine")
691
+ return
692
+ ok, detail = land_codegen_content(target)
693
+ if not ok:
694
+ print(f"[FAIL] write_wizard commit refused: {detail}")
695
+ return
696
+ journal("CODEGEN", target,
697
+ f"task={task} model={proposal['model']} "
698
+ f"instruction={effective}")
699
+ print(f"[OK] {task} proposal written to {target} through write_wizard")
700
+
701
+
702
+ def run_repl():
703
+ """Launch interactive REPL with banner, voice, ollama chat, slash-commands."""
704
+ import os
705
+ render_banner()
706
+ slash_map = list_all_slash_commands()
707
+ session = session_load()
708
+ session["session"] = session.get("session", 0) + 1
709
+ session_save(session)
710
+ cfg = _load_cfg()
711
+ if cfg.get("model"):
712
+ os.environ["GODFATHER_MODEL"] = cfg["model"] # restore persisted choice
713
+ print(f"\nšŸ‘ļø SESSION {session['session']} — Sovereign Fleet Cockpit\n")
714
+ print("Type /help for slash commands, /quit to exit.\n")
715
+ chat_memory = []
716
+ brainstorm_mode = False
717
+ brainstorm_memory = []
718
+ brainstorm_tool_noted = False
719
+ last_proposal = None
720
+
721
+ while True:
722
+ try:
723
+ raw_input = input("šŸ‘‘ godfather> ")
724
+ action, args = _classify_repl_input(raw_input)
725
+ except (EOFError, KeyboardInterrupt):
726
+ print("\n[OK] Godfather bows out. Sovereignty preserved.")
727
+ break
728
+
729
+ if not raw_input:
730
+ continue
731
+
732
+ if raw_input in ("/quit", "/exit"):
733
+ print("[OK] Godfather bows out. Sovereignty preserved.")
734
+ break
735
+
736
+ if brainstorm_mode and raw_input.startswith("/") \
737
+ and raw_input.split()[0] != "/brainstorm":
738
+ # Any other slash leaves brainstorm mode.
739
+ brainstorm_mode = False
740
+ brainstorm_memory = []
741
+ brainstorm_tool_noted = False
742
+
743
+ if raw_input.split()[0:1] == ["/brainstorm"]:
744
+ parts = raw_input.split()
745
+ if parts == ["/brainstorm", "end"]:
746
+ brainstorm_mode = False
747
+ brainstorm_memory = []
748
+ brainstorm_tool_noted = False
749
+ print("[OK] brainstorm mode ended — hands remain off")
750
+ continue
751
+ if not brainstorm_mode:
752
+ brainstorm_mode = True
753
+ brainstorm_memory = []
754
+ brainstorm_tool_noted = False
755
+ print("[OK] brainstorm mode — hands off: the seat can "
756
+ "talk only; no tools run and no files change. "
757
+ "Use /brainstorm end to leave.")
758
+ if len(parts) > 1:
759
+ text = raw_input[len("/brainstorm"):].strip()
760
+ rc, reply, brainstorm_memory = brainstorm_reply(
761
+ text, brainstorm_memory)
762
+ print(f"\nšŸ‘‘ godfather: {reply}\n" if rc == 0 else reply)
763
+ if rc == 0:
764
+ if any(line.strip().upper().startswith("TOOL:")
765
+ for line in reply.splitlines()) \
766
+ and not brainstorm_tool_noted:
767
+ print("[NOTE] brainstorm is hands-off: a TOOL: "
768
+ "line was treated as text; nothing executed.")
769
+ brainstorm_tool_noted = True
770
+ speak_if_enabled(reply)
771
+ continue
772
+
773
+ if raw_input.split()[0:1] == ["/build"]:
774
+ _write_codegen("build", raw_input.split(), last_proposal)
775
+ continue
776
+
777
+ if raw_input.split()[0:1] == ["/debug"]:
778
+ _write_codegen("debug", raw_input.split(), last_proposal)
779
+ continue
780
+
781
+ if raw_input.split()[0:1] == ["/diff"]:
782
+ # /diff already names diffwiz's two-file comparison. The
783
+ # codegen form is /diff <path> <instruction...>. Longer
784
+ # forms are unambiguously codegen; a three-token form is
785
+ # codegen when its last token is not a second file.
786
+ parts = raw_input.split()
787
+ if len(parts) > 3 or (len(parts) == 3 and not Path(
788
+ parts[2]).expanduser().is_file()):
789
+ last_proposal = _diff_codegen(parts, last_proposal)
790
+ continue
791
+
792
+ if raw_input == "/help":
793
+ print("šŸ“œ AVAILABLE SLASH COMMANDS:")
794
+ for cmd, desc in slash_map.items():
795
+ print(f" {cmd:15s} → {desc}")
796
+ print(" /diff <file> <instruction> → godfather_wizard diff review"
797
+ " (proposal form; the two-file form above compares two files)")
798
+ print(" /voices → pick voice (numbered dropdown)")
799
+ print(" /model → pick model (numbered dropdown)")
800
+ print(" /more-commands → full fleet arsenal (all citizens)")
801
+ continue
802
+
803
+ if raw_input == "/voices":
804
+ _pick_voice()
805
+ continue
806
+
807
+ if raw_input == "/model":
808
+ _pick_model()
809
+ continue
810
+
811
+ if raw_input == "/more-commands":
812
+ count, names = fleet_census()
813
+ print(f"\nšŸ“š FLEET ARSENAL — {count} citizens")
814
+ print("=" * 60)
815
+ for w in names:
816
+ rc, out = call_sibling([w, "--help"])
817
+ lines = out.splitlines()
818
+ print(f"\n{w.upper()} ({'ā˜…' if rc == 0 else 'āœ—'}):")
819
+ for line in lines[:10]:
820
+ print(f" {line}")
821
+ if len(lines) > 10:
822
+ print(f" ... ({len(lines) - 10} more, run '{w} --help')")
823
+ print("\n" + "=" * 60)
824
+ continue
825
+
826
+ if raw_input.startswith("/"):
827
+ cmd = raw_input.split()[0]
828
+ if cmd in slash_map:
829
+ # ARG LAW: keep the full pipeline (cmd + subcommand),
830
+ # and split user args into proper argv tokens. Passing the
831
+ # whole arg string as ONE element made citizens receive
832
+ # 'FLEET-STATE.md' as an unknown subcommand.
833
+ pipeline = slash_map[cmd].split()
834
+ args = raw_input[len(cmd):].strip().split()
835
+ internal = call_internal(pipeline + args)
836
+ if internal is not None:
837
+ rc, out = internal
838
+ else:
839
+ rc, out = call_sibling(pipeline + args)
840
+ print(out or "(no output)")
841
+ elif cmd == "/wiz" and len(raw_input.split()) > 1:
842
+ # UNIVERSAL DELEGATION: /wiz <citizen> [args...]
843
+ # Resolves against the installed fleet — a citizen that
844
+ # does not exist on disk cannot be invoked. Zero fabrication.
845
+ parts = raw_input.split()
846
+ target = parts[1]
847
+ wiz_args = parts[2:]
848
+ real = resolve_sibling(target)
849
+ if real is None:
850
+ candidates = [n for n in fleet_invocable_names()
851
+ if n.startswith(target)]
852
+ if not candidates:
853
+ print(f"out of scope — no citizen named '{target}' installed")
854
+ continue
855
+ if len(candidates) == 1:
856
+ real = resolve_sibling(candidates[0])
857
+ else:
858
+ print(f"[FAIL] ambiguous citizen '{target}': {candidates}")
859
+ continue
860
+ print(f"[ROUTE] universal delegation -> {real.name} {' '.join(wiz_args)}".rstrip())
861
+ rc, out = call_sibling([real.name] + wiz_args)
862
+ print(out or "(no output)")
863
+ else:
864
+ print(f"[FAIL] unknown slash command: {cmd}")
865
+ print("run /help to see available commands, or /wiz <citizen> [args]")
866
+ continue
867
+
868
+ if brainstorm_mode:
869
+ rc, reply, brainstorm_memory = brainstorm_reply(
870
+ raw_input, brainstorm_memory)
871
+ print(f"\nšŸ‘‘ godfather: {reply}\n" if rc == 0 else reply)
872
+ if rc == 0:
873
+ if any(line.strip().upper().startswith("TOOL:")
874
+ for line in reply.splitlines()) \
875
+ and not brainstorm_tool_noted:
876
+ print("[NOTE] brainstorm is hands-off: a TOOL: "
877
+ "line was treated as text; nothing executed.")
878
+ brainstorm_tool_noted = True
879
+ speak_if_enabled(reply)
880
+ continue
881
+
882
+ # Plain text: the deterministic router answers FIRST (R4).
883
+ # Only unmapped text reaches the oracle, and only when a
884
+ # model seat is actually serving — seatless unmapped text
885
+ # is plainly out of scope, never an oracle error.
886
+ pipeline, reason, wall = classify_route(raw_input, [])
887
+ if pipeline is None and wall:
888
+ print(f"[CONSENT WALL] {reason}")
889
+ print("to mutate: echo 'content' | write_wizard propose <path>")
890
+ print("then answer the wall yourself: write_wizard commit <path>")
891
+ journal("WALL_ROUTED", raw_input, "mutation detected")
892
+ continue
893
+ if pipeline is not None:
894
+ if wall and not _confirm_write_class(pipeline):
895
+ print("[FAIL] consent withheld — nothing executed")
896
+ journal("WALL_ROUTED", raw_input, "consent withheld")
897
+ continue
898
+ if wall:
899
+ journal("WALL_ROUTED", raw_input, "consent given — executing")
900
+ journal("ROUTED", raw_input, reason)
901
+ print(f"[ROUTE] {reason}")
902
+ internal = call_internal(pipeline)
903
+ if internal is not None:
904
+ rc, out = internal
905
+ else:
906
+ rc, out = call_sibling(pipeline)
907
+ print(out or "(no output)")
908
+ continue
909
+
910
+ seat = seat_for("default")
911
+ if seat is None:
912
+ print("out of scope — no route matches this text, "
913
+ "and no model seat is serving")
914
+ continue
915
+ # Unmapped text + a live seat → chat WITH HANDS: the oracle
916
+ # may request real tools; it only ever suggests.
917
+ print(f"šŸ¤– consulting oracle (seat: {seat_label(seat)})...",
918
+ flush=True)
919
+ from .godfather_wizard import oracle_tool_loop
920
+ reply, chat_memory = oracle_tool_loop(raw_input, seat, chat_memory)
921
+ print(f"\nšŸ‘‘ godfather: {reply}\n")
922
+ speak_if_enabled(reply) # the whole word, uncut
923
+
924
+ if __name__ == "__main__":
925
+ sys.exit(main())