@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.
- package/omega/Agentik_Engine/omega_engine/__init__.py +1 -1
- package/omega/Agentik_Engine/omega_engine/__pycache__/cli.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/paperclip_bridge.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/prompt_audit.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/tmux.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/tui.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/cli.py +39 -0
- package/omega/Agentik_Engine/omega_engine/paperclip_bridge.py +110 -0
- package/omega/Agentik_Engine/omega_engine/prompt_audit.py +395 -0
- package/omega/Agentik_Engine/omega_engine/tmux.py +16 -0
- package/omega/Agentik_Engine/omega_engine/tui.py +269 -67
- package/omega/Agentik_Engine/pyproject.toml +1 -1
- package/omega/Agentik_Engine/tests/__pycache__/test_paperclip_status.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_paperclip_status.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_prompt_audit.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_prompt_audit.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_tui_runtime.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_tui_runtime.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/test_paperclip_status.py +142 -0
- package/omega/Agentik_Engine/tests/test_prompt_audit.py +199 -0
- package/omega/Agentik_Engine/tests/test_tui_runtime.py +106 -0
- package/omega/Agentik_SSOT/VERSION +1 -1
- package/omega/Agentik_SSOT/docs/AUDIT-V0.19.39.md +161 -0
- package/omega/Agentik_SSOT/rules/audit-gates.md +189 -0
- package/omega/Agentik_SSOT/rules/constitution.md +7 -0
- package/omega/Agentik_SSOT/rules/orchestration.md +215 -0
- package/omega/Agentik_SSOT/rules/prompt-protocols.md +219 -0
- package/omega/Agentik_SSOT/rules/scope-safety.md +197 -0
- package/omega/Agentik_SSOT/rules/three-laws.md +214 -0
- package/omega/Agentik_SSOT/rules/verified-completion.md +216 -0
- package/package.json +1 -1
|
@@ -462,46 +462,180 @@ def _arrow_menu() -> int:
|
|
|
462
462
|
# `omega_engine.provider_state` module both can read.
|
|
463
463
|
from omega_engine.provider_state import active_provider as _active_provider
|
|
464
464
|
|
|
465
|
+
# v0.19.39 — chat-first redesign. The TUI now opens on CONVERSATIONS
|
|
466
|
+
# (live tmux sessions: AISB chat, Hermès chat, active Oracles per
|
|
467
|
+
# project, active Workers per task) with ●/○ status dots. Setup,
|
|
468
|
+
# config, infra, audits, scrape, etc. move into sub-menus opened from
|
|
469
|
+
# a short "MENU" section. Reasoning: the user spends 99% of their
|
|
470
|
+
# time in conversations — the menu should reflect that, not bury the
|
|
471
|
+
# chats below 6 sections of admin options.
|
|
472
|
+
DOT_ON = f"{ORANGE}●{RST}" # alive
|
|
473
|
+
DOT_OFF = f"{MUTED}○{RST}" # not running
|
|
474
|
+
|
|
475
|
+
def _dot(alive: bool) -> str:
|
|
476
|
+
return DOT_ON if alive else DOT_OFF
|
|
477
|
+
|
|
478
|
+
def _conv_label(name: str, hint: str, alive: bool) -> str:
|
|
479
|
+
"""Conversation row: `<dot> <name> <dim hint>`."""
|
|
480
|
+
dot = _dot(alive)
|
|
481
|
+
return f" {dot} {name:<32}{DIM}{hint}{RST}"
|
|
482
|
+
|
|
483
|
+
def _paperclip_status_quick() -> tuple[bool, str]:
|
|
484
|
+
"""Lightweight inline Paperclip probe — short timeout, no raise.
|
|
485
|
+
Returns (alive, hint). Hint is empty when down, ``localhost:8080``
|
|
486
|
+
(or whatever port the bridge reports) when up. Uses the new
|
|
487
|
+
``paperclip_bridge.is_running()`` from chantier 4 when present;
|
|
488
|
+
falls back gracefully to a plain TCP probe.
|
|
489
|
+
"""
|
|
490
|
+
try:
|
|
491
|
+
from omega_engine.paperclip_bridge import is_running as _pcst
|
|
492
|
+
st = _pcst(HOME)
|
|
493
|
+
if getattr(st, "running", False):
|
|
494
|
+
port = getattr(st, "port", None) or 8080
|
|
495
|
+
return True, f"localhost:{port}"
|
|
496
|
+
except Exception: # noqa: BLE001
|
|
497
|
+
# Fallback: a 200ms TCP probe to 127.0.0.1:8080.
|
|
498
|
+
import socket as _s
|
|
499
|
+
try:
|
|
500
|
+
with _s.create_connection(("127.0.0.1", 8080), timeout=0.2):
|
|
501
|
+
return True, "localhost:8080"
|
|
502
|
+
except OSError:
|
|
503
|
+
pass
|
|
504
|
+
return False, ""
|
|
505
|
+
|
|
506
|
+
def _active_oracles_and_workers() -> tuple[list, list]:
|
|
507
|
+
"""Categorize live tmux sessions into (oracles, workers).
|
|
508
|
+
Each entry is the ``TmuxSession`` instance."""
|
|
509
|
+
all_sessions = tmux.list_sessions(HOME)
|
|
510
|
+
oracles = sorted([s for s in all_sessions if s.category == "oracle"],
|
|
511
|
+
key=lambda x: x.name)
|
|
512
|
+
workers = sorted([s for s in all_sessions if s.category == "worker"],
|
|
513
|
+
key=lambda x: x.name)
|
|
514
|
+
return oracles, workers
|
|
515
|
+
|
|
465
516
|
def _build_items() -> list[tuple[str, str]]:
|
|
466
|
-
"""Return (display, action_key) — section headers have key '__sep__'.
|
|
517
|
+
"""Return (display, action_key) — section headers have key '__sep__'.
|
|
518
|
+
|
|
519
|
+
v0.19.39 layout (chat-first):
|
|
520
|
+
1. CONVERSATIONS — AISB / Hermès / live Oracles / live Workers
|
|
521
|
+
2. QUICK ACTIONS — new chat, new project, run mission, Paperclip
|
|
522
|
+
3. MENU — sub-menus (audits, setup, infra, health)
|
|
523
|
+
4. EXIT
|
|
524
|
+
"""
|
|
467
525
|
provider = _active_provider()
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
(
|
|
484
|
-
|
|
485
|
-
(
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
("",
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
526
|
+
aisb_alive = tmux.omega_window_alive("aisb")
|
|
527
|
+
hermes_alive = tmux.omega_window_alive("hermes")
|
|
528
|
+
oracles, workers = _active_oracles_and_workers()
|
|
529
|
+
paperclip_alive, paperclip_hint = _paperclip_status_quick()
|
|
530
|
+
|
|
531
|
+
items: list[tuple[str, str]] = []
|
|
532
|
+
|
|
533
|
+
# ── CONVERSATIONS ───────────────────────────────────────────────
|
|
534
|
+
items.append((_section("CONVERSATIONS"), "__sep__"))
|
|
535
|
+
items.append((_conv_label("AISB master", "claude (Max OAuth)", aisb_alive), "open:aisb"))
|
|
536
|
+
items.append((_conv_label("Hermès", "claude (Anthropic API)", hermes_alive), "open:hermes"))
|
|
537
|
+
|
|
538
|
+
# Per-project live oracles + workers (only when there are any —
|
|
539
|
+
# don't waste a row when the user has nothing running).
|
|
540
|
+
if oracles or workers:
|
|
541
|
+
items.append(("", "__sep__"))
|
|
542
|
+
if oracles:
|
|
543
|
+
items.append((f" {DIM}— Active Oracles ({len(oracles)}) —{RST}", "__sep__"))
|
|
544
|
+
for s in oracles:
|
|
545
|
+
proj = s.project or "?"
|
|
546
|
+
hint = f"project: {proj}"
|
|
547
|
+
items.append((_conv_label(s.name, hint, True), f"attach:{s.name}"))
|
|
548
|
+
if workers:
|
|
549
|
+
items.append((f" {DIM}— Active Workers ({len(workers)}) —{RST}", "__sep__"))
|
|
550
|
+
for s in workers:
|
|
551
|
+
# Extract task from `<proj>-worker-N-<task>`.
|
|
552
|
+
task = s.name.split("worker-", 1)[-1].split("-", 1)[-1] if "worker-" in s.name else "?"
|
|
553
|
+
hint = f"task: {task}"
|
|
554
|
+
items.append((_conv_label(s.name, hint, True), f"attach:{s.name}"))
|
|
555
|
+
|
|
556
|
+
items.append(("", "__sep__"))
|
|
557
|
+
|
|
558
|
+
# ── QUICK ACTIONS ───────────────────────────────────────────────
|
|
559
|
+
items.append((_section("QUICK ACTIONS"), "__sep__"))
|
|
560
|
+
items.append((_label("+ New AISB chat", "fresh session"), "open:aisb:new"))
|
|
561
|
+
items.append((_label("+ New Hermès chat", "fresh session"), "open:hermes:new"))
|
|
562
|
+
items.append((_label("+ New project", "Genesis pipeline"), "genesis:new"))
|
|
563
|
+
items.append((_label("Run a mission", "verified completion"), "run:mission"))
|
|
564
|
+
# Paperclip dashboard with live status indicator.
|
|
565
|
+
pclbl = f"Paperclip dashboard"
|
|
566
|
+
pcdot = _dot(paperclip_alive)
|
|
567
|
+
pchint = paperclip_hint if paperclip_alive else "not running"
|
|
568
|
+
items.append((f" {pcdot} {pclbl:<32}{DIM}{pchint}{RST}", "paperclip:dashboard"))
|
|
569
|
+
|
|
570
|
+
items.append(("", "__sep__"))
|
|
571
|
+
|
|
572
|
+
# ── MENU (sub-menus for everything else) ────────────────────────
|
|
573
|
+
items.append((_section("MENU"), "__sep__"))
|
|
574
|
+
items.append((_label("Quality Arsenal", "17 forensic audits"), "submenu:audits"))
|
|
575
|
+
items.append((_label("Setup & config", f"LLM: {provider}"), "submenu:setup"))
|
|
576
|
+
items.append((_label("Infrastructure", "sessions, scrape"), "submenu:infra"))
|
|
577
|
+
items.append((_label("Health checks", "doctor, status"), "submenu:health"))
|
|
578
|
+
items.append((_label("Paperclip governance", "register, status"), "submenu:paperclip"))
|
|
579
|
+
|
|
580
|
+
items.append(("", "__sep__"))
|
|
581
|
+
|
|
582
|
+
# ── EXIT ────────────────────────────────────────────────────────
|
|
583
|
+
items.append((_section("EXIT"), "__sep__"))
|
|
584
|
+
items.append((_label("Detach", "session keeps running"), "detach"))
|
|
585
|
+
items.append((_label("Quit Omega", "kills the tmux session"), "quit:kill"))
|
|
586
|
+
|
|
587
|
+
return items
|
|
588
|
+
|
|
589
|
+
def _submenu_items(name: str) -> list[tuple[str, str]]:
|
|
590
|
+
"""Return (display, action_key) for one of the named sub-menus.
|
|
591
|
+
Every sub-menu ends with a synthetic '← back' row that aborts the
|
|
592
|
+
cascaded fzf with key 'back'."""
|
|
593
|
+
if name == "audits":
|
|
594
|
+
return [
|
|
595
|
+
(_section("QUALITY ARSENAL"), "__sep__"),
|
|
596
|
+
(_label("List all 17 audits"), "audit:menu"),
|
|
597
|
+
(_label("Run an audit", "interactive"), "audit:run"),
|
|
598
|
+
("", "__sep__"),
|
|
599
|
+
(_label("← back"), "back"),
|
|
600
|
+
]
|
|
601
|
+
if name == "setup":
|
|
602
|
+
return [
|
|
603
|
+
(_section("SETUP & CONFIG"), "__sep__"),
|
|
604
|
+
(_label("Switch LLM provider"), "switch:provider"),
|
|
605
|
+
(_label("Accounts", "Claude Max pool"), "accounts:menu"),
|
|
606
|
+
(_label("Vault", "encrypted secrets"), "vault:menu"),
|
|
607
|
+
("", "__sep__"),
|
|
608
|
+
(_label("← back"), "back"),
|
|
609
|
+
]
|
|
610
|
+
if name == "infra":
|
|
611
|
+
return [
|
|
612
|
+
(_section("INFRASTRUCTURE"), "__sep__"),
|
|
613
|
+
(_label("List tmux sessions"), "sessions:list"),
|
|
614
|
+
(_label("Open project shell"), "project:open"),
|
|
615
|
+
("", "__sep__"),
|
|
616
|
+
(_label("Stealth scrape", "CloakBrowser"), "scrape:cloak"),
|
|
617
|
+
(_label("Fast scrape", "Scrapling"), "scrape:scrapling"),
|
|
618
|
+
("", "__sep__"),
|
|
619
|
+
(_label("← back"), "back"),
|
|
620
|
+
]
|
|
621
|
+
if name == "health":
|
|
622
|
+
return [
|
|
623
|
+
(_section("HEALTH"), "__sep__"),
|
|
624
|
+
(_label("omega doctor", "full health check"), "cmd:doctor"),
|
|
625
|
+
(_label("omega status", "task state"), "cmd:status"),
|
|
626
|
+
("", "__sep__"),
|
|
627
|
+
(_label("← back"), "back"),
|
|
628
|
+
]
|
|
629
|
+
if name == "paperclip":
|
|
630
|
+
return [
|
|
631
|
+
(_section("PAPERCLIP GOVERNANCE (L0)"), "__sep__"),
|
|
632
|
+
(_label("Open dashboard"), "paperclip:dashboard"),
|
|
633
|
+
(_label("Status", "bridge health"), "paperclip:status"),
|
|
634
|
+
(_label("Register", "(re)sync OmegaOS company"), "paperclip:register"),
|
|
635
|
+
("", "__sep__"),
|
|
636
|
+
(_label("← back"), "back"),
|
|
637
|
+
]
|
|
638
|
+
return [(_label("← back"), "back")]
|
|
505
639
|
|
|
506
640
|
def _run_inline(cmd_argv, *, shell: bool = False) -> None:
|
|
507
641
|
os.system("clear")
|
|
@@ -525,23 +659,29 @@ def _arrow_menu() -> int:
|
|
|
525
659
|
except (EOFError, KeyboardInterrupt):
|
|
526
660
|
return ""
|
|
527
661
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
662
|
+
def _pick(items: list[tuple[str, str]],
|
|
663
|
+
header_extra: str = "") -> str | None:
|
|
664
|
+
"""Render one fzf pass over ``items`` and return the chosen
|
|
665
|
+
action key (the second element of the picked tuple), or ``None``
|
|
666
|
+
on Esc / Ctrl-C / error.
|
|
667
|
+
|
|
668
|
+
Skips section headers and blank rows automatically — the caller
|
|
669
|
+
never has to worry about ``__sep__`` picks.
|
|
670
|
+
|
|
671
|
+
v0.19.37 lock-in: input is tab-delimited ``<idx>\\t<display>``,
|
|
672
|
+
fzf --with-nth=2.. shows only the visual column, and we parse
|
|
673
|
+
the index column out of stdout — survives any ANSI/whitespace
|
|
674
|
+
transformation fzf applies.
|
|
675
|
+
"""
|
|
538
676
|
lines = []
|
|
539
|
-
for i, (disp,
|
|
677
|
+
for i, (disp, _key) in enumerate(items):
|
|
540
678
|
lines.append(f"{i}\t{disp}")
|
|
541
679
|
header = (
|
|
542
680
|
f"{ORANGE}{BOLD}Ω Omega OS v{__version__}{RST} "
|
|
543
681
|
f"{MUTED}• ↑↓ navigate • ↵ pick • / search • Esc refresh{RST}"
|
|
544
682
|
)
|
|
683
|
+
if header_extra:
|
|
684
|
+
header = f"{header}\n{MUTED}{header_extra}{RST}"
|
|
545
685
|
try:
|
|
546
686
|
proc = subprocess.run(
|
|
547
687
|
["fzf",
|
|
@@ -577,27 +717,50 @@ def _arrow_menu() -> int:
|
|
|
577
717
|
input="\n".join(lines), capture_output=True, text=True,
|
|
578
718
|
)
|
|
579
719
|
except (KeyboardInterrupt, subprocess.SubprocessError):
|
|
580
|
-
return
|
|
720
|
+
return None
|
|
581
721
|
if proc.returncode != 0:
|
|
582
|
-
|
|
583
|
-
continue
|
|
722
|
+
return None
|
|
584
723
|
pick = proc.stdout.rstrip("\n")
|
|
585
|
-
# Parse the index column. The tab is always the first \t in the
|
|
586
|
-
# picked line (the rest of the display may contain colour codes
|
|
587
|
-
# but no tabs).
|
|
588
724
|
if "\t" not in pick:
|
|
589
|
-
|
|
590
|
-
idx_str,
|
|
725
|
+
return None
|
|
726
|
+
idx_str, _ = pick.split("\t", 1)
|
|
591
727
|
try:
|
|
592
728
|
idx = int(idx_str)
|
|
593
729
|
except ValueError:
|
|
594
|
-
|
|
730
|
+
return None
|
|
595
731
|
if not (0 <= idx < len(items)):
|
|
596
|
-
|
|
732
|
+
return None
|
|
597
733
|
action = items[idx][1]
|
|
598
734
|
if action == "__sep__":
|
|
599
|
-
|
|
735
|
+
return None
|
|
736
|
+
return action
|
|
737
|
+
|
|
738
|
+
def _open_submenu(name: str) -> str | None:
|
|
739
|
+
"""Render a sub-menu in a loop until the user picks an action OR
|
|
740
|
+
chooses 'back'. Returns the action key to dispatch (the caller
|
|
741
|
+
runs it), or None when the user backs out / Esc'd.
|
|
742
|
+
"""
|
|
743
|
+
while True:
|
|
744
|
+
sub_items = _submenu_items(name)
|
|
745
|
+
picked = _pick(sub_items, header_extra=f"sub-menu: {name} · Esc/← back to main")
|
|
746
|
+
if picked is None or picked == "back":
|
|
747
|
+
return None
|
|
748
|
+
return picked
|
|
749
|
+
|
|
750
|
+
while True:
|
|
751
|
+
items = _build_items()
|
|
752
|
+
action = _pick(items)
|
|
753
|
+
if action is None:
|
|
754
|
+
# Esc → just refresh the conversations panel (cheap — re-renders
|
|
755
|
+
# with live tmux state).
|
|
600
756
|
continue
|
|
757
|
+
# Sub-menu indirection: when the user picks a sub-menu, render it
|
|
758
|
+
# in its own loop until they pick a real action or back out.
|
|
759
|
+
if action.startswith("submenu:"):
|
|
760
|
+
sub_name = action.split(":", 1)[1]
|
|
761
|
+
action = _open_submenu(sub_name)
|
|
762
|
+
if action is None:
|
|
763
|
+
continue
|
|
601
764
|
|
|
602
765
|
# === Dispatch ===
|
|
603
766
|
if action == "detach":
|
|
@@ -606,19 +769,50 @@ def _arrow_menu() -> int:
|
|
|
606
769
|
if action == "quit:kill":
|
|
607
770
|
subprocess.run(["tmux", "kill-session", "-t", "Omega"])
|
|
608
771
|
return 0
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
772
|
+
# v0.19.39 — attach to a live Oracle / Worker tmux session. The
|
|
773
|
+
# menu lists them as `attach:<session_name>`. We use
|
|
774
|
+
# `select-window` when the session is already a window inside
|
|
775
|
+
# Omega; otherwise we switch the client to that session.
|
|
776
|
+
if action.startswith("attach:"):
|
|
777
|
+
target = action.split(":", 1)[1]
|
|
778
|
+
# If the target is a window of Omega, just select it. Else
|
|
779
|
+
# switch-client to that session (works under nested tmux).
|
|
780
|
+
wnames = subprocess.run(
|
|
781
|
+
["tmux", "list-windows", "-t", "Omega", "-F", "#W"],
|
|
782
|
+
capture_output=True, text=True,
|
|
783
|
+
)
|
|
784
|
+
if (wnames.returncode == 0
|
|
785
|
+
and target in (wnames.stdout or "").splitlines()):
|
|
786
|
+
subprocess.run(["tmux", "select-window",
|
|
787
|
+
"-t", f"Omega:{target}"])
|
|
788
|
+
else:
|
|
789
|
+
# Switch to the foreign session. Use switch-client (works
|
|
790
|
+
# when we're already inside a tmux client) or fall back to
|
|
791
|
+
# attach for non-tmux contexts.
|
|
792
|
+
rc = subprocess.run(
|
|
793
|
+
["tmux", "switch-client", "-t", target],
|
|
794
|
+
capture_output=True,
|
|
795
|
+
).returncode
|
|
796
|
+
if rc != 0:
|
|
797
|
+
_run_inline(f"echo ' cannot attach to {target} — is it alive?' "
|
|
798
|
+
f"&& tmux has-session -t {target} && "
|
|
799
|
+
f"echo ' tmux says: yes' || echo ' tmux says: no'",
|
|
800
|
+
shell=True)
|
|
801
|
+
continue
|
|
802
|
+
# v0.19.39 — "+ New" actions: kill existing chat window first so
|
|
803
|
+
# the user gets a clean fresh session.
|
|
804
|
+
if action in ("open:aisb", "open:aisb:new"):
|
|
805
|
+
force = (action == "open:aisb:new")
|
|
613
806
|
persona = HOME / "Agentik_SSOT" / "agents" / "aisb" / "CLAUDE.md"
|
|
614
807
|
ctx_dir = tmux._ensure_chat_context_dir(HOME, "aisb-master", persona)
|
|
615
808
|
tmux.spawn_chat_in_omega(
|
|
616
809
|
"aisb", ctx_dir=ctx_dir, run_command="claude",
|
|
617
|
-
force_replace=
|
|
810
|
+
force_replace=force,
|
|
618
811
|
)
|
|
619
812
|
subprocess.run(["tmux", "select-window", "-t", "Omega:aisb"])
|
|
620
813
|
continue
|
|
621
|
-
if action
|
|
814
|
+
if action in ("open:hermes", "open:hermes:new"):
|
|
815
|
+
force = (action == "open:hermes:new")
|
|
622
816
|
persona = HOME / "Agentik_SSOT" / "docs" / "LAYERS.md"
|
|
623
817
|
ctx_dir = tmux._ensure_chat_context_dir(HOME, "hermes", persona)
|
|
624
818
|
try:
|
|
@@ -632,10 +826,18 @@ def _arrow_menu() -> int:
|
|
|
632
826
|
if api_key else "claude")
|
|
633
827
|
tmux.spawn_chat_in_omega(
|
|
634
828
|
"hermes", ctx_dir=ctx_dir, run_command=run_cmd,
|
|
635
|
-
force_replace=
|
|
829
|
+
force_replace=force,
|
|
636
830
|
)
|
|
637
831
|
subprocess.run(["tmux", "select-window", "-t", "Omega:hermes"])
|
|
638
832
|
continue
|
|
833
|
+
if action == "audit:run":
|
|
834
|
+
audit_id = _prompt("audit id (codeaudit, uiuxaudit, flowaudit, ...)")
|
|
835
|
+
if audit_id:
|
|
836
|
+
_run_inline([OMEGA_BIN, "audit", audit_id])
|
|
837
|
+
continue
|
|
838
|
+
# (v0.19.31 open:aisb / open:hermes legacy handlers removed in
|
|
839
|
+
# v0.19.39 — the new unified handlers above (which accept both
|
|
840
|
+
# plain and `:new` variants) cover the same surface.)
|
|
639
841
|
if action == "switch:provider":
|
|
640
842
|
_run_inline([OMEGA_BIN, "switch"]); continue
|
|
641
843
|
if action == "genesis:new":
|
package/omega/Agentik_Engine/tests/__pycache__/test_paperclip_status.cpython-313-pytest-8.4.2.pyc
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/omega/Agentik_Engine/tests/__pycache__/test_tui_runtime.cpython-313-pytest-8.4.2.pyc
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Tests for omega_engine.paperclip_bridge.is_running().
|
|
2
|
+
|
|
3
|
+
Locks in the live status probe contract so the TUI can render ●/○ next to
|
|
4
|
+
the Paperclip menu items. Five detection paths covered:
|
|
5
|
+
|
|
6
|
+
1. Empty PAPERCLIP_HOME → not running (detection="none")
|
|
7
|
+
2. Stale pidfile → not running
|
|
8
|
+
3. Live pidfile (self PID) → running (detection="pidfile")
|
|
9
|
+
4. No pidfile + open socket → running (detection="port-scan")
|
|
10
|
+
5. Running case sets url → http://localhost:<port>
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import socket
|
|
16
|
+
import tempfile
|
|
17
|
+
import unittest
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from unittest import mock
|
|
20
|
+
|
|
21
|
+
from omega_engine import paperclip_bridge as P
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _free_port() -> int:
|
|
25
|
+
"""Allocate (and release) an ephemeral TCP port."""
|
|
26
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
27
|
+
s.bind(("127.0.0.1", 0))
|
|
28
|
+
return s.getsockname()[1]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestIsRunningDetectionPaths(unittest.TestCase):
|
|
32
|
+
"""Each of the three detection paths plus the URL invariant."""
|
|
33
|
+
|
|
34
|
+
def test_no_pidfile_no_port_returns_not_running(self):
|
|
35
|
+
"""Empty PAPERCLIP_HOME + closed port → running=False, detection=none."""
|
|
36
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
37
|
+
with mock.patch.dict(os.environ, {"PAPERCLIP_HOME": tmp}, clear=False):
|
|
38
|
+
# Pick a port nothing is listening on (allocate then release).
|
|
39
|
+
port = _free_port()
|
|
40
|
+
status = P.is_running(port=port)
|
|
41
|
+
self.assertFalse(status.running)
|
|
42
|
+
self.assertEqual(status.detection, "none")
|
|
43
|
+
self.assertIsNone(status.pid)
|
|
44
|
+
self.assertIsNone(status.port)
|
|
45
|
+
self.assertIsNone(status.url)
|
|
46
|
+
|
|
47
|
+
def test_stale_pidfile_returns_not_running(self):
|
|
48
|
+
"""Pidfile points at a dead PID → running=False (falls through to port,
|
|
49
|
+
which is also closed in this test)."""
|
|
50
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
51
|
+
with mock.patch.dict(os.environ, {"PAPERCLIP_HOME": tmp}, clear=False):
|
|
52
|
+
run_dir = Path(tmp) / "run"
|
|
53
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
# 99999999 is virtually guaranteed to be unused on Linux
|
|
55
|
+
# (default PID max is 4194304); we also mock os.kill to be
|
|
56
|
+
# sure across any host's PID-recycling weirdness.
|
|
57
|
+
(run_dir / "dashboard.pid").write_text("99999999\n")
|
|
58
|
+
|
|
59
|
+
def fake_kill(pid: int, sig: int) -> None:
|
|
60
|
+
if pid == 99999999:
|
|
61
|
+
raise ProcessLookupError(pid)
|
|
62
|
+
# delegate to the real os.kill for any other pid
|
|
63
|
+
return os._real_kill(pid, sig) # pragma: no cover
|
|
64
|
+
|
|
65
|
+
# Stash the real kill (in case fake_kill ever delegates).
|
|
66
|
+
os._real_kill = os.kill # type: ignore[attr-defined]
|
|
67
|
+
try:
|
|
68
|
+
port = _free_port()
|
|
69
|
+
with mock.patch("omega_engine.paperclip_bridge.os.kill",
|
|
70
|
+
side_effect=fake_kill):
|
|
71
|
+
status = P.is_running(port=port)
|
|
72
|
+
finally:
|
|
73
|
+
del os._real_kill # type: ignore[attr-defined]
|
|
74
|
+
self.assertFalse(status.running)
|
|
75
|
+
self.assertEqual(status.detection, "none")
|
|
76
|
+
self.assertIsNone(status.pid)
|
|
77
|
+
|
|
78
|
+
def test_live_pidfile_returns_running(self):
|
|
79
|
+
"""Pidfile points at this test process (alive) → running=True,
|
|
80
|
+
detection='pidfile', does NOT touch the network."""
|
|
81
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
82
|
+
with mock.patch.dict(os.environ, {"PAPERCLIP_HOME": tmp}, clear=False):
|
|
83
|
+
run_dir = Path(tmp) / "run"
|
|
84
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
self_pid = os.getpid()
|
|
86
|
+
(run_dir / "dashboard.pid").write_text(f"{self_pid}\n")
|
|
87
|
+
# Patch socket so we'd notice if the code accidentally tried
|
|
88
|
+
# to connect (it shouldn't — pidfile hit returns immediately).
|
|
89
|
+
with mock.patch("omega_engine.paperclip_bridge.socket.socket") \
|
|
90
|
+
as sock_cls:
|
|
91
|
+
status = P.is_running(port=8080)
|
|
92
|
+
sock_cls.assert_not_called()
|
|
93
|
+
self.assertTrue(status.running)
|
|
94
|
+
self.assertEqual(status.detection, "pidfile")
|
|
95
|
+
self.assertEqual(status.pid, self_pid)
|
|
96
|
+
self.assertEqual(status.port, 8080)
|
|
97
|
+
|
|
98
|
+
def test_port_scan_fallback_running(self):
|
|
99
|
+
"""No pidfile, but a socket IS listening on the probe port →
|
|
100
|
+
running=True, detection='port-scan', pid=None."""
|
|
101
|
+
# Bind a real listener on an ephemeral port for the duration of
|
|
102
|
+
# the probe; close it at the end.
|
|
103
|
+
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
104
|
+
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
105
|
+
listener.bind(("127.0.0.1", 0))
|
|
106
|
+
listener.listen(1)
|
|
107
|
+
port = listener.getsockname()[1]
|
|
108
|
+
try:
|
|
109
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
110
|
+
with mock.patch.dict(os.environ,
|
|
111
|
+
{"PAPERCLIP_HOME": tmp}, clear=False):
|
|
112
|
+
# No pidfile written → pidfile branch skipped, falls
|
|
113
|
+
# through to port scan.
|
|
114
|
+
status = P.is_running(port=port)
|
|
115
|
+
finally:
|
|
116
|
+
listener.close()
|
|
117
|
+
self.assertTrue(status.running)
|
|
118
|
+
self.assertEqual(status.detection, "port-scan")
|
|
119
|
+
self.assertIsNone(status.pid)
|
|
120
|
+
self.assertEqual(status.port, port)
|
|
121
|
+
|
|
122
|
+
def test_url_field_returns_localhost_url_when_running(self):
|
|
123
|
+
"""When running=True, status.url is http://localhost:<port>."""
|
|
124
|
+
# Use the pidfile path (cheapest, no socket needed).
|
|
125
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
126
|
+
with mock.patch.dict(os.environ, {"PAPERCLIP_HOME": tmp}, clear=False):
|
|
127
|
+
run_dir = Path(tmp) / "run"
|
|
128
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
(run_dir / "dashboard.pid").write_text(f"{os.getpid()}\n")
|
|
130
|
+
status = P.is_running(port=8080)
|
|
131
|
+
self.assertTrue(status.running)
|
|
132
|
+
self.assertIsNotNone(status.url)
|
|
133
|
+
# Accept either localhost or 127.0.0.1 form so the implementation
|
|
134
|
+
# can pick whichever it prefers — the contract is "loopback URL".
|
|
135
|
+
self.assertIn(status.url, {
|
|
136
|
+
"http://localhost:8080",
|
|
137
|
+
"http://127.0.0.1:8080",
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
unittest.main()
|