@agentikos/omega-os 0.19.38 → 0.19.39

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 (31) hide show
  1. package/omega/Agentik_Engine/omega_engine/__init__.py +1 -1
  2. package/omega/Agentik_Engine/omega_engine/__pycache__/cli.cpython-313.pyc +0 -0
  3. package/omega/Agentik_Engine/omega_engine/__pycache__/paperclip_bridge.cpython-313.pyc +0 -0
  4. package/omega/Agentik_Engine/omega_engine/__pycache__/prompt_audit.cpython-313.pyc +0 -0
  5. package/omega/Agentik_Engine/omega_engine/__pycache__/tmux.cpython-313.pyc +0 -0
  6. package/omega/Agentik_Engine/omega_engine/__pycache__/tui.cpython-313.pyc +0 -0
  7. package/omega/Agentik_Engine/omega_engine/cli.py +39 -0
  8. package/omega/Agentik_Engine/omega_engine/paperclip_bridge.py +110 -0
  9. package/omega/Agentik_Engine/omega_engine/prompt_audit.py +395 -0
  10. package/omega/Agentik_Engine/omega_engine/tmux.py +16 -0
  11. package/omega/Agentik_Engine/omega_engine/tui.py +269 -67
  12. package/omega/Agentik_Engine/pyproject.toml +1 -1
  13. package/omega/Agentik_Engine/tests/__pycache__/test_paperclip_status.cpython-313-pytest-8.4.2.pyc +0 -0
  14. package/omega/Agentik_Engine/tests/__pycache__/test_paperclip_status.cpython-313.pyc +0 -0
  15. package/omega/Agentik_Engine/tests/__pycache__/test_prompt_audit.cpython-313-pytest-8.4.2.pyc +0 -0
  16. package/omega/Agentik_Engine/tests/__pycache__/test_prompt_audit.cpython-313.pyc +0 -0
  17. package/omega/Agentik_Engine/tests/__pycache__/test_tui_runtime.cpython-313-pytest-8.4.2.pyc +0 -0
  18. package/omega/Agentik_Engine/tests/__pycache__/test_tui_runtime.cpython-313.pyc +0 -0
  19. package/omega/Agentik_Engine/tests/test_paperclip_status.py +142 -0
  20. package/omega/Agentik_Engine/tests/test_prompt_audit.py +199 -0
  21. package/omega/Agentik_Engine/tests/test_tui_runtime.py +106 -0
  22. package/omega/Agentik_SSOT/VERSION +1 -1
  23. package/omega/Agentik_SSOT/docs/AUDIT-V0.19.39.md +161 -0
  24. package/omega/Agentik_SSOT/rules/audit-gates.md +189 -0
  25. package/omega/Agentik_SSOT/rules/constitution.md +7 -0
  26. package/omega/Agentik_SSOT/rules/orchestration.md +215 -0
  27. package/omega/Agentik_SSOT/rules/prompt-protocols.md +219 -0
  28. package/omega/Agentik_SSOT/rules/scope-safety.md +197 -0
  29. package/omega/Agentik_SSOT/rules/three-laws.md +214 -0
  30. package/omega/Agentik_SSOT/rules/verified-completion.md +216 -0
  31. package/package.json +1 -1
@@ -188,7 +188,7 @@ from omega_engine.genesis import (
188
188
  )
189
189
  from omega_engine import plan as plan_v7
190
190
 
191
- __version__ = "0.19.38"
191
+ __version__ = "0.19.39"
192
192
 
193
193
  __all__ = [
194
194
  "__version__",
@@ -474,6 +474,45 @@ def cmd_doctor(args: argparse.Namespace) -> int:
474
474
  except Exception as exc: # noqa: BLE001
475
475
  line("FAIL", f"personas check failed: {exc}")
476
476
 
477
+ # 7c. Prompt audit — each AISB role's prompt must reference the
478
+ # Three Laws + LMC protocol + done.json contract. Otherwise the
479
+ # agent runs blind and orchestration breaks at the first dispatch.
480
+ section("prompts")
481
+ try:
482
+ from omega_engine.prompt_audit import audit_aisb_suite
483
+ rep = audit_aisb_suite(home)
484
+ if not rep.per_agent:
485
+ line("warn", "no AISB prompts found — step 25-aisb-suite may have been skipped")
486
+ else:
487
+ for r in rep.per_agent:
488
+ status = "ok" if r.score >= 80 else ("warn" if r.score >= 60 else "FAIL")
489
+ line(status, f"{r.agent_id}: {r.score}/100"
490
+ + (f" — missing: {', '.join(r.violations[:2])}" if r.violations else ""))
491
+ avg_status = "ok" if rep.average_score >= 80 else "warn"
492
+ line(avg_status, f"average suite score: {rep.average_score:.1f}/100")
493
+ if rep.missing_critical:
494
+ line("warn", f"weak prompts (<60): {', '.join(rep.missing_critical)}")
495
+ except Exception as exc: # noqa: BLE001
496
+ line("FAIL", f"prompt audit failed: {exc}")
497
+
498
+ # 7d. Orchestration chain — AISB → Oracle → Worker → Checker must
499
+ # all be wired and share the LMC protocol vocabulary.
500
+ section("orchestration")
501
+ try:
502
+ from omega_engine.prompt_audit import orchestration_health
503
+ oh = orchestration_health(home)
504
+ for k, v in (("aisb_master_present", "AISB master prompt"),
505
+ ("oracle_present", "Oracle role prompt"),
506
+ ("workers_role_present", "Worker-class prompts"),
507
+ ("checker_present", "Checker prompts (Seraph/Smith)"),
508
+ ("lmc_protocol_present", "LMC protocol document")):
509
+ line("ok" if oh.get(k) else "warn", v)
510
+ overlap = oh.get("shared_vocab_overlap", 0.0)
511
+ line("ok" if overlap >= 0.6 else "warn",
512
+ f"shared `.done.json` vocab: {overlap*100:.0f}% of agents")
513
+ except Exception as exc: # noqa: BLE001
514
+ line("FAIL", f"orchestration check failed: {exc}")
515
+
477
516
  # 7b. Per-project vaults
478
517
  section("project vaults")
479
518
  try:
@@ -26,6 +26,7 @@ from __future__ import annotations
26
26
  import json
27
27
  import os
28
28
  import shutil
29
+ import socket
29
30
  import subprocess
30
31
  import time
31
32
  from dataclasses import asdict, dataclass, field
@@ -584,3 +585,112 @@ def start_server(bind: str = "loopback", *, detach: bool = True) -> int:
584
585
  )
585
586
  return proc.pid
586
587
  return subprocess.call(args)
588
+
589
+
590
+ # ---------------------------------------------------------------------------
591
+ # Live status probe — is the Paperclip dashboard actually running?
592
+ # ---------------------------------------------------------------------------
593
+
594
+
595
+ @dataclass
596
+ class PaperclipStatus:
597
+ """Snapshot of whether the Paperclip dashboard daemon is alive.
598
+
599
+ Consumed by the TUI to render ●/○ next to Paperclip menu items so the
600
+ operator can tell at a glance whether the dashboard is reachable.
601
+ """
602
+
603
+ running: bool
604
+ pid: int | None # process PID if alive (None for port-scan hits)
605
+ port: int | None # port the dashboard listens on (8080 default)
606
+ url: str | None # http://host:port if running, else None
607
+ detection: str # "pidfile" | "port-scan" | "none"
608
+
609
+
610
+ def is_running(omega_home: Path | str | None = None,
611
+ *,
612
+ port: int = 8080,
613
+ host: str = "127.0.0.1") -> PaperclipStatus:
614
+ """Probe the Paperclip daemon — running or not?
615
+
616
+ Detection strategy (cheap → expensive, returns on first hit):
617
+ 1. Read ``$PAPERCLIP_HOME/run/dashboard.pid`` (default ``~/.paperclip``).
618
+ If file exists AND ``os.kill(pid, 0)`` succeeds → running.
619
+ 2. Else try TCP connect to ``<host>:<port>`` with a 0.2s timeout.
620
+ If accept → running but no pidfile (caller may warn).
621
+ 3. Else → not running.
622
+
623
+ Never raises. Never blocks more than ~0.3s total.
624
+
625
+ Args:
626
+ omega_home: Accepted for API symmetry with the rest of the bridge;
627
+ the actual pidfile lives under ``$PAPERCLIP_HOME`` (resolved
628
+ via :func:`paperclip_home`), not under ``$OMEGA_HOME``.
629
+ port: TCP port to probe in the fallback path. Default 8080.
630
+ host: Loopback host to probe. Default 127.0.0.1.
631
+
632
+ Returns:
633
+ :class:`PaperclipStatus` — never None.
634
+ """
635
+ del omega_home # signature consistency only; pidfile lives under PAPERCLIP_HOME
636
+
637
+ # ---- 1. Pidfile check (cheapest) -----------------------------------
638
+ pidfile = paperclip_home() / "run" / "dashboard.pid"
639
+ if pidfile.exists():
640
+ try:
641
+ pid_text = pidfile.read_text().strip()
642
+ pid = int(pid_text)
643
+ except (OSError, ValueError):
644
+ pid = None
645
+ if pid is not None and pid > 0:
646
+ alive = False
647
+ try:
648
+ os.kill(pid, 0)
649
+ alive = True
650
+ except ProcessLookupError:
651
+ alive = False
652
+ except PermissionError:
653
+ # PID exists but is owned by another user — still proves
654
+ # SOMETHING is alive at that PID, so treat as running.
655
+ alive = True
656
+ except OSError:
657
+ alive = False
658
+ if alive:
659
+ return PaperclipStatus(
660
+ running=True,
661
+ pid=pid,
662
+ port=port,
663
+ url=f"http://localhost:{port}",
664
+ detection="pidfile",
665
+ )
666
+ # Stale pidfile — fall through to port scan.
667
+
668
+ # ---- 2. Port scan fallback (~0.2s) ---------------------------------
669
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
670
+ try:
671
+ sock.settimeout(0.2)
672
+ try:
673
+ sock.connect((host, port))
674
+ return PaperclipStatus(
675
+ running=True,
676
+ pid=None,
677
+ port=port,
678
+ url=f"http://localhost:{port}",
679
+ detection="port-scan",
680
+ )
681
+ except (OSError, socket.timeout):
682
+ pass
683
+ finally:
684
+ try:
685
+ sock.close()
686
+ except OSError:
687
+ pass
688
+
689
+ # ---- 3. Not running -------------------------------------------------
690
+ return PaperclipStatus(
691
+ running=False,
692
+ pid=None,
693
+ port=None,
694
+ url=None,
695
+ detection="none",
696
+ )
@@ -0,0 +1,395 @@
1
+ """Prompt audit — verify the AISB agent prompts are well-formed AND
2
+ reference the contracts they MUST reference.
3
+
4
+ Without this, the engine ships agents that read like generic system prompts
5
+ and the Three Laws / LMC protocol / verified-completion (`.done.json`)
6
+ contracts silently drift out of the role files. Orchestration breaks at
7
+ the first dispatch because a worker doesn't know its done-signal contract
8
+ or an oracle doesn't know it must enforce the laws.
9
+
10
+ The audit applies per-file (each role's prompt is scored in isolation —
11
+ not after the engine concatenates LMC + shared protocols at spawn time)
12
+ because the file on disk is what an operator reads and edits. If a role's
13
+ isolated file is silent on the Three Laws, the operator can't tell the
14
+ contract is present, and any edit that breaks the loader will silently
15
+ strip the laws too.
16
+
17
+ Public API:
18
+
19
+ - ``audit_agent_prompt(path)`` -> AgentPromptReport (score 0..100, checks dict)
20
+ - ``audit_aisb_suite(omega_home)`` -> SuiteReport (per-agent + averages)
21
+ - ``orchestration_health(omega_home)`` -> dict of presence + overlap
22
+
23
+ Only stdlib (``pathlib``, ``re``, ``dataclasses``). No new deps.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ from dataclasses import dataclass, field
29
+ from pathlib import Path
30
+ from typing import Optional
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Data classes — what each audit returns
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ @dataclass
39
+ class CheckResult:
40
+ """One contract check on one prompt file."""
41
+
42
+ passed: bool
43
+ evidence: Optional[str] = None # short matched snippet, None if not found
44
+ points: int = 0 # points awarded for this check
45
+
46
+
47
+ @dataclass
48
+ class AgentPromptReport:
49
+ """Audit result for a single agent prompt .md file."""
50
+
51
+ agent_id: str
52
+ file_path: str
53
+ score: int = 0 # 0..100
54
+ checks: dict[str, CheckResult] = field(default_factory=dict)
55
+ violations: list[str] = field(default_factory=list) # human-readable
56
+
57
+
58
+ @dataclass
59
+ class SuiteReport:
60
+ """Audit result for the whole AISB suite."""
61
+
62
+ per_agent: list[AgentPromptReport] = field(default_factory=list)
63
+ average_score: float = 0.0
64
+ missing_critical: list[str] = field(default_factory=list) # score < 60
65
+ orchestration_chain_intact: bool = False
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Contract patterns — what every well-formed agent prompt should reference
70
+ # ---------------------------------------------------------------------------
71
+
72
+
73
+ # Three Laws — case-insensitive search for any of these markers
74
+ _RE_THREE_LAWS = re.compile(
75
+ r"(three\s+laws|first\s+law|second\s+law|third\s+law|"
76
+ r"law\s+1|law\s+2|law\s+3)",
77
+ re.IGNORECASE,
78
+ )
79
+
80
+ # LMC protocol — concept or filename
81
+ _RE_LMC = re.compile(
82
+ r"(lmc[\s\-_]*(protocol|gate)?|lead[\s\-]+manager[\s\-]+checker|"
83
+ r"lmc-protocol\.md)",
84
+ re.IGNORECASE,
85
+ )
86
+
87
+ # Verified-completion contract — `.done.json`
88
+ _RE_DONE_JSON = re.compile(r"\.done\.json|done\.json", re.IGNORECASE)
89
+
90
+ # Done-marker tooling — the script that writes the .done.json signal
91
+ _RE_DONE_MARKER = re.compile(
92
+ r"(worker[\s\-]?mark[\s\-]?done|oracle[\s\-]?mark[\s\-]?done|"
93
+ r"mark[\s\-]?done\.sh|write_done\b|done_signal|"
94
+ r"done[\s\-]?marker)",
95
+ re.IGNORECASE,
96
+ )
97
+
98
+ # Scope / role-specific responsibilities — at least one of these tokens
99
+ # tells us the file declares what the agent owns / is allowed to touch.
100
+ _RE_SCOPE = re.compile(
101
+ r"(scope|files?_owned|owns?\s+R[\s\-]?\d+|responsibilit(y|ies)|"
102
+ r"^\s*role\b|owned\s+files|files\s+owned|what\s+you\s+own|"
103
+ r"\bowns\b)",
104
+ re.IGNORECASE | re.MULTILINE,
105
+ )
106
+
107
+ # Fresh context handoff vocabulary
108
+ _RE_FRESH_CONTEXT = re.compile(
109
+ r"fresh[\s\-]+context|self[\s\-]+contained\s+brief|"
110
+ r"self[\s\-]+contained\s+context",
111
+ re.IGNORECASE,
112
+ )
113
+
114
+ # Banned phrases — the no-time-panic global rule (rule 46). These are the
115
+ # action-oriented forms that authorize cheating, NOT the descriptive forms
116
+ # (e.g. "lightweight utility agent" describes a haiku tier — fine).
117
+ # We explicitly DO NOT include bare "lightweight" because templates
118
+ # legitimately describe haiku-tier agents as lightweight (architecture
119
+ # descriptor, not a shortcut authorization).
120
+ _BANNED_PHRASES = [
121
+ "streamlined approach",
122
+ "streamlined version",
123
+ "skip audit",
124
+ "skip the audit",
125
+ "quick version",
126
+ "to save time",
127
+ "custom scoring",
128
+ "too heavyweight",
129
+ "lightweight audit",
130
+ "simplified protocol",
131
+ ]
132
+
133
+ # Per-check point weights (sum = 100)
134
+ _WEIGHTS = {
135
+ "three_laws": 25,
136
+ "lmc_protocol": 15,
137
+ "done_json": 20,
138
+ "done_marker": 10,
139
+ "scope": 15,
140
+ "fresh_context": 10,
141
+ "no_banned": 5,
142
+ }
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Helpers
147
+ # ---------------------------------------------------------------------------
148
+
149
+
150
+ def _short_evidence(text: str, match: re.Match) -> str:
151
+ """Return a short snippet (~60 chars) around the match for the report."""
152
+ start = max(0, match.start() - 10)
153
+ end = min(len(text), match.end() + 20)
154
+ snippet = text[start:end].replace("\n", " ").strip()
155
+ if len(snippet) > 80:
156
+ snippet = snippet[:77] + "..."
157
+ return snippet
158
+
159
+
160
+ def _check_pattern(
161
+ text: str,
162
+ pattern: re.Pattern,
163
+ name: str,
164
+ points: int,
165
+ ) -> CheckResult:
166
+ """Run a regex check and return a CheckResult."""
167
+ m = pattern.search(text)
168
+ if m:
169
+ return CheckResult(passed=True, evidence=_short_evidence(text, m),
170
+ points=points)
171
+ return CheckResult(passed=False, evidence=None, points=0)
172
+
173
+
174
+ def _check_no_banned(text: str, points: int) -> CheckResult:
175
+ """Pass iff no banned phrase appears. The phrase list is short and
176
+ targets action-oriented patterns (e.g. 'streamlined approach') rather
177
+ than descriptive ones (e.g. bare 'lightweight') to avoid false
178
+ positives in legitimate architecture descriptions."""
179
+ lowered = text.lower()
180
+ for phrase in _BANNED_PHRASES:
181
+ if phrase in lowered:
182
+ return CheckResult(passed=False, evidence=phrase, points=0)
183
+ return CheckResult(passed=True, evidence=None, points=points)
184
+
185
+
186
+ def _agent_id_from_path(path: Path) -> str:
187
+ """Derive a short agent id from the file path (the .stem)."""
188
+ return path.stem
189
+
190
+
191
+ # ---------------------------------------------------------------------------
192
+ # Public API — single file
193
+ # ---------------------------------------------------------------------------
194
+
195
+
196
+ def audit_agent_prompt(path: Path) -> AgentPromptReport:
197
+ """Score one agent prompt .md file on the seven contract checks.
198
+
199
+ Scoring (sum = 100):
200
+ - Three Laws reference 25 pts
201
+ - LMC protocol reference 15 pts
202
+ - `.done.json` reference 20 pts
203
+ - Done-marker tooling reference 10 pts
204
+ - Scope / files_owned / responsibilities 15 pts
205
+ - Fresh context / self-contained brief 10 pts
206
+ - No banned phrases (rule 46-no-time-panic) 5 pts
207
+
208
+ Returns a fully-populated :class:`AgentPromptReport`. If the file does
209
+ not exist or can't be read, returns a report with score=0 and a single
210
+ violation explaining why.
211
+ """
212
+ path = Path(path)
213
+ agent_id = _agent_id_from_path(path)
214
+ report = AgentPromptReport(agent_id=agent_id, file_path=str(path))
215
+
216
+ try:
217
+ text = path.read_text(encoding="utf-8")
218
+ except (OSError, UnicodeDecodeError) as exc:
219
+ report.violations.append(f"unreadable file: {exc}")
220
+ return report
221
+
222
+ checks: dict[str, CheckResult] = {
223
+ "three_laws": _check_pattern(text, _RE_THREE_LAWS,
224
+ "three_laws",
225
+ _WEIGHTS["three_laws"]),
226
+ "lmc_protocol": _check_pattern(text, _RE_LMC,
227
+ "lmc_protocol",
228
+ _WEIGHTS["lmc_protocol"]),
229
+ "done_json": _check_pattern(text, _RE_DONE_JSON,
230
+ "done_json",
231
+ _WEIGHTS["done_json"]),
232
+ "done_marker": _check_pattern(text, _RE_DONE_MARKER,
233
+ "done_marker",
234
+ _WEIGHTS["done_marker"]),
235
+ "scope": _check_pattern(text, _RE_SCOPE,
236
+ "scope",
237
+ _WEIGHTS["scope"]),
238
+ "fresh_context": _check_pattern(text, _RE_FRESH_CONTEXT,
239
+ "fresh_context",
240
+ _WEIGHTS["fresh_context"]),
241
+ "no_banned": _check_no_banned(text, _WEIGHTS["no_banned"]),
242
+ }
243
+ report.checks = checks
244
+
245
+ # Compute score and violations.
246
+ score = sum(c.points for c in checks.values())
247
+ report.score = min(100, score)
248
+
249
+ # Human-readable violations for the doctor line summary.
250
+ human = {
251
+ "three_laws": "Three Laws",
252
+ "lmc_protocol": "LMC protocol",
253
+ "done_json": "`.done.json` contract",
254
+ "done_marker": "done-marker tooling",
255
+ "scope": "scope/responsibilities",
256
+ "fresh_context": "fresh context handoff",
257
+ "no_banned": "banned phrase present",
258
+ }
259
+ for name, res in checks.items():
260
+ if not res.passed:
261
+ label = human[name]
262
+ if name == "no_banned" and res.evidence:
263
+ label = f"{label} ({res.evidence})"
264
+ report.violations.append(label)
265
+
266
+ return report
267
+
268
+
269
+ # ---------------------------------------------------------------------------
270
+ # Public API — full suite
271
+ # ---------------------------------------------------------------------------
272
+
273
+
274
+ def _aisb_dir(omega_home: Path) -> Path:
275
+ return Path(omega_home) / "Agentik_SSOT" / "agents" / "aisb"
276
+
277
+
278
+ def _iter_aisb_prompts(omega_home: Path) -> list[Path]:
279
+ """Return every .md directly under aisb/ (subdirs excluded)."""
280
+ aisb = _aisb_dir(omega_home)
281
+ if not aisb.is_dir():
282
+ return []
283
+ return sorted(p for p in aisb.iterdir()
284
+ if p.is_file() and p.suffix == ".md")
285
+
286
+
287
+ def audit_aisb_suite(omega_home: Path) -> SuiteReport:
288
+ """Audit every .md directly under ``aisb/`` (subdirs excluded).
289
+
290
+ Returns a :class:`SuiteReport`. If no prompts are found, ``per_agent``
291
+ is an empty list and downstream callers should warn (the install
292
+ step 25-aisb-suite probably was skipped).
293
+ """
294
+ omega_home = Path(omega_home)
295
+ paths = _iter_aisb_prompts(omega_home)
296
+ per_agent = [audit_agent_prompt(p) for p in paths]
297
+
298
+ if per_agent:
299
+ avg = sum(r.score for r in per_agent) / len(per_agent)
300
+ else:
301
+ avg = 0.0
302
+
303
+ missing_critical = sorted(r.agent_id for r in per_agent if r.score < 60)
304
+
305
+ return SuiteReport(
306
+ per_agent=per_agent,
307
+ average_score=avg,
308
+ missing_critical=missing_critical,
309
+ orchestration_chain_intact=_chain_intact(per_agent),
310
+ )
311
+
312
+
313
+ def _chain_intact(reports: list[AgentPromptReport]) -> bool:
314
+ """The AISB → Oracle → Worker chain is intact when:
315
+ - each of the three roles exists in the suite, AND
316
+ - all three reference `.done.json` (the shared completion vocabulary).
317
+
318
+ 'Worker' is taken loosely: morpheus (the canonical executor) or
319
+ construct (alias target for the generic 'worker' role) satisfies the
320
+ worker slot. This matches the alias map in ``prompts.py``.
321
+ """
322
+ by_id = {r.agent_id: r for r in reports}
323
+ aisb_master = by_id.get("CLAUDE") # the master prompt of the AISB suite
324
+ oracle = by_id.get("oracle")
325
+ worker = by_id.get("morpheus") or by_id.get("construct")
326
+
327
+ if not (aisb_master and oracle and worker):
328
+ return False
329
+ return all(r.checks.get("done_json", CheckResult(passed=False)).passed
330
+ for r in (aisb_master, oracle, worker))
331
+
332
+
333
+ # ---------------------------------------------------------------------------
334
+ # Public API — orchestration health
335
+ # ---------------------------------------------------------------------------
336
+
337
+
338
+ def orchestration_health(omega_home: Path) -> dict:
339
+ """Higher-level check: is the AISB → Oracle → Worker → Checker chain
340
+ wired and does the suite share the verified-completion vocabulary?
341
+
342
+ Returns a dict with:
343
+ - ``aisb_master_present`` — CLAUDE.md exists in aisb/
344
+ - ``oracle_present`` — oracle.md exists
345
+ - ``workers_role_present`` — morpheus.md OR construct.md exists
346
+ - ``checker_present`` — seraph.md OR smith.md exists
347
+ - ``lmc_protocol_present`` — lmc-protocol.md exists
348
+ - ``shared_vocab_overlap`` — fraction (0.0..1.0) of agents
349
+ mentioning `.done.json` in their file
350
+ """
351
+ omega_home = Path(omega_home)
352
+ aisb = _aisb_dir(omega_home)
353
+
354
+ def has(name: str) -> bool:
355
+ return (aisb / name).is_file()
356
+
357
+ out: dict = {
358
+ "aisb_master_present": has("CLAUDE.md"),
359
+ "oracle_present": has("oracle.md"),
360
+ "workers_role_present": has("morpheus.md") or has("construct.md"),
361
+ "checker_present": has("seraph.md") or has("smith.md"),
362
+ "lmc_protocol_present": has("lmc-protocol.md"),
363
+ }
364
+
365
+ # Shared vocabulary overlap — fraction of agents that reference
366
+ # `.done.json` in their file. A high overlap means the completion
367
+ # contract is part of the muscle memory of the suite; a low overlap
368
+ # means some agents are silent on the done signal and may complete
369
+ # tasks without writing the structured result downstream consumers
370
+ # expect.
371
+ paths = _iter_aisb_prompts(omega_home)
372
+ if not paths:
373
+ out["shared_vocab_overlap"] = 0.0
374
+ return out
375
+
376
+ matches = 0
377
+ for p in paths:
378
+ try:
379
+ text = p.read_text(encoding="utf-8")
380
+ except (OSError, UnicodeDecodeError):
381
+ continue
382
+ if _RE_DONE_JSON.search(text):
383
+ matches += 1
384
+ out["shared_vocab_overlap"] = matches / len(paths)
385
+ return out
386
+
387
+
388
+ __all__ = [
389
+ "CheckResult",
390
+ "AgentPromptReport",
391
+ "SuiteReport",
392
+ "audit_agent_prompt",
393
+ "audit_aisb_suite",
394
+ "orchestration_health",
395
+ ]
@@ -325,6 +325,22 @@ def _spawn_with_shell_then_run(
325
325
  return name
326
326
 
327
327
 
328
+ def omega_window_alive(window_name: str) -> bool:
329
+ """True if a window named ``window_name`` exists inside the Omega
330
+ master tmux session.
331
+
332
+ Used by the TUI chat-list panel to render ● (alive) vs ○ (off) next
333
+ to AISB-chat / Hermès-chat. Cheap — one ``tmux list-windows`` call;
334
+ returns False on any error including 'no Omega session'.
335
+ """
336
+ if not is_alive("Omega"):
337
+ return False
338
+ rc, out = _tmux("list-windows", "-t", "Omega", "-F", "#W")
339
+ if rc != 0:
340
+ return False
341
+ return window_name in (out or "").splitlines()
342
+
343
+
328
344
  def spawn_chat_in_omega(
329
345
  window_name: str,
330
346
  *,