@agentikos/omega-os 0.19.36 → 0.19.38

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/bin/omega-os.js CHANGED
@@ -36,7 +36,12 @@ console.log(" Omega OS — npx bootstrap");
36
36
  console.log(" -> handing off to install.sh");
37
37
  console.log("");
38
38
 
39
- const args = process.argv.slice(2);
39
+ // Drop the literal `--` separator if npx/npm forwarded it. Without this,
40
+ // `npx -y @agentikos/omega-os@latest -- --full` lands in install.sh as
41
+ // `install.sh -- --full`, and the case-block rejects the unknown `--`.
42
+ const rawArgs = process.argv.slice(2);
43
+ const args = rawArgs.filter((a) => a !== "--");
44
+
40
45
  const result = spawnSync("bash", [installer, ...args], {
41
46
  stdio: "inherit",
42
47
  cwd: pkgRoot,
@@ -461,6 +461,49 @@ PY
461
461
  return $?
462
462
  }
463
463
 
464
+ # --- 38 -----------------------------------------------------------------------
465
+ #
466
+ # step_personas — seed the canonical OmegaOS context + every LLM's
467
+ # expected filename for both chat sessions.
468
+ #
469
+ # Without this step, the canonical OMEGAOS-CONTEXT.md and the per-LLM
470
+ # persona files (CLAUDE.md / GEMINI.md / AGENTS.md / QWEN.md /
471
+ # .opencode/CONTEXT.md / .continue/CONTEXT.md / CONVENTIONS.md /
472
+ # HERMES.md) only land the FIRST time the user spawns AISB-chat or
473
+ # Hermès-chat. Eager seeding lets the operator inspect + edit the
474
+ # canonical at install time and guarantees `omega doctor` sees every
475
+ # persona file before the first session.
476
+ #
477
+ # Idempotent — re-runs only write files whose content actually differs
478
+ # from the canonical.
479
+ step_personas() {
480
+ PYTHONPATH="$OMEGA_HOME/Agentik_Engine" python3 - <<PY 2>>"$LOG_FILE"
481
+ import os
482
+ from pathlib import Path
483
+ os.environ["OMEGA_HOME"] = "$OMEGA_HOME"
484
+ home = Path("$OMEGA_HOME")
485
+ from omega_engine.personas import (
486
+ ensure_canonical, write_all_personas, canonical_path, supported_llm_ids,
487
+ )
488
+
489
+ src = ensure_canonical(home)
490
+ print(f" canonical: {src.relative_to(home)} ({src.stat().st_size} bytes)")
491
+
492
+ # Mirror to BOTH chat-context dirs so every LLM has its persona ready
493
+ # from day one — no first-spawn lazy materialization.
494
+ for label in ("aisb-master", "hermes"):
495
+ ctx = home / "Agentik_Coding" / "chat-contexts" / label
496
+ written = write_all_personas(home, ctx)
497
+ # Count EVERY persona file (root + subdir like .opencode/CONTEXT.md).
498
+ files = sorted(str(p.relative_to(ctx))
499
+ for p in ctx.rglob("*.md"))
500
+ print(f" {label}/: {len(files)} persona files ({', '.join(files)})")
501
+
502
+ print(f" supported LLMs: {len(supported_llm_ids())}")
503
+ PY
504
+ return $?
505
+ }
506
+
464
507
  # --- 40 -----------------------------------------------------------------------
465
508
  #
466
509
  # step_clis — install system CLIs + Printing Press CLI library.
package/install.sh CHANGED
@@ -62,6 +62,10 @@ while [ $# -gt 0 ]; do
62
62
  fi
63
63
  ;;
64
64
  -h|--help) awk 'NR==1 && /^#!/ {next} /^#/ {sub(/^#[ ]?/, ""); print; next} {exit}' "$0"; exit 0 ;;
65
+ # Accept the literal `--` separator as a no-op so recipes like
66
+ # `npx -y @agentikos/omega-os@latest -- --full` survive even if the
67
+ # JS launcher's filter is bypassed.
68
+ --) shift ;;
65
69
  *) die "unknown argument: $1" ;;
66
70
  esac
67
71
  done
@@ -182,6 +186,7 @@ STEPS=(
182
186
  "35-providers:step_providers"
183
187
  "36-tmux-config:step_tmux_config"
184
188
  "37-hermes-brief:step_hermes_brief"
189
+ "38-personas:step_personas"
185
190
  # v0.19.21 — MCP install dropped from the default sequence. MCPs are
186
191
  # token-expensive (each call pays the protocol round-trip + the
187
192
  # server's system prompt overhead). We replace with system CLIs +
@@ -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.36"
191
+ __version__ = "0.19.38"
192
192
 
193
193
  __all__ = [
194
194
  "__version__",
@@ -440,6 +440,40 @@ def cmd_doctor(args: argparse.Namespace) -> int:
440
440
  except Exception as exc: # noqa: BLE001
441
441
  line("FAIL", f"agent suite check failed: {exc}")
442
442
 
443
+ # 7a-bis. Multi-LLM personas — canonical + per-LLM mirrors for the
444
+ # two chat contexts (AISB-master + Hermès). Step 38-personas should
445
+ # have materialized these at install time; if they're missing the
446
+ # user can re-run `omega menu-tui` → "Re-seed personas".
447
+ section("personas")
448
+ try:
449
+ from omega_engine.personas import canonical_path, supported_llm_ids
450
+ canon = canonical_path(home)
451
+ if canon.exists():
452
+ line("ok", f"canonical: {canon.relative_to(home)} ({canon.stat().st_size}B)")
453
+ else:
454
+ line("warn",
455
+ f"canonical missing at {canon.relative_to(home)} — "
456
+ "spawn AISB-chat once or re-run install step 38-personas")
457
+ # Each chat context should have at least one persona file per LLM
458
+ # subgroup. We count both root *.md and subdir CONTEXT.md.
459
+ for label in ("aisb-master", "hermes"):
460
+ ctx = home / "Agentik_Coding" / "chat-contexts" / label
461
+ if not ctx.is_dir():
462
+ line("warn", f"chat-contexts/{label}/ not seeded yet")
463
+ continue
464
+ files = sorted(p.relative_to(ctx).as_posix()
465
+ for p in ctx.rglob("*.md") if p.is_file())
466
+ if files:
467
+ line("ok",
468
+ f"chat-contexts/{label}/: {len(files)} persona file(s) "
469
+ f"({', '.join(files[:4])}{'...' if len(files) > 4 else ''})")
470
+ else:
471
+ line("warn",
472
+ f"chat-contexts/{label}/ exists but has no persona files")
473
+ line("ok", f"supported LLM persona ids: {len(supported_llm_ids())}")
474
+ except Exception as exc: # noqa: BLE001
475
+ line("FAIL", f"personas check failed: {exc}")
476
+
443
477
  # 7b. Per-project vaults
444
478
  section("project vaults")
445
479
  try:
@@ -3208,6 +3242,17 @@ def cmd_paperclip(args: argparse.Namespace) -> int:
3208
3242
  print(f" start failed: {exc}")
3209
3243
  return 2
3210
3244
 
3245
+ if sub == "url":
3246
+ bind = getattr(args, "bind", None) or "loopback"
3247
+ host = getattr(args, "host", None)
3248
+ url = PB.dashboard_url(bind=bind, host=host)
3249
+ print(f" paperclip dashboard: {url}")
3250
+ return 0
3251
+
3252
+ if sub == "remote-setup":
3253
+ print(PB.remote_setup_help())
3254
+ return 0
3255
+
3211
3256
  if sub == "heartbeat":
3212
3257
  # Drop a heartbeat from the current shell — used by `dispatch-to-
3213
3258
  # session.sh` style scripts so Paperclip sees workers come up + report.
@@ -4527,6 +4572,15 @@ def _build_parser() -> argparse.ArgumentParser:
4527
4572
  help="launch the Paperclip web dashboard (npx onboard)")
4528
4573
  p_pcs.add_argument("--bind", choices=["loopback", "lan", "tailnet"],
4529
4574
  default="loopback")
4575
+ p_pcu = pc_sub.add_parser("url",
4576
+ help="print the URL where the dashboard is reachable")
4577
+ p_pcu.add_argument("--bind", choices=["loopback", "lan", "tailnet"],
4578
+ default="loopback")
4579
+ p_pcu.add_argument("--host", default=None,
4580
+ help="override host (e.g. for a Cloudflare Tunnel URL)")
4581
+ pc_sub.add_parser("remote-setup",
4582
+ help="how to reach a VPS-hosted Paperclip from your Mac/PC "
4583
+ "(SSH port-forward / Tailscale / Cloudflare Tunnel)")
4530
4584
  p_pch = pc_sub.add_parser("heartbeat",
4531
4585
  help="send a heartbeat from a tmux session (script callable)")
4532
4586
  p_pch.add_argument("--agent-id", required=True,
@@ -488,6 +488,82 @@ def status() -> BridgeStatus:
488
488
  # ---------------------------------------------------------------------------
489
489
 
490
490
 
491
+ def dashboard_url(bind: str = "loopback", port: int = 8080,
492
+ host: str | None = None) -> str:
493
+ """Return the URL where the Paperclip dashboard is reachable.
494
+
495
+ Resolution:
496
+ - loopback → http://localhost:<port>
497
+ - lan → http://<lan-ip>:<port> (best-effort interface IP)
498
+ - tailnet → http://<tailscale-ip>:<port> (reads `tailscale ip -4`)
499
+ - host → http://<host>:<port> (explicit override)
500
+ """
501
+ if host:
502
+ return f"http://{host}:{port}"
503
+ if bind == "tailnet":
504
+ try:
505
+ r = subprocess.run(["tailscale", "ip", "-4"],
506
+ capture_output=True, text=True, timeout=5)
507
+ ip = (r.stdout or "").strip().splitlines()
508
+ if ip:
509
+ return f"http://{ip[0]}:{port}"
510
+ except (FileNotFoundError, subprocess.SubprocessError):
511
+ pass
512
+ if bind == "lan":
513
+ # Best-effort: use `hostname -I` (Linux) or `ipconfig getifaddr en0` (Mac).
514
+ for cmd in [["hostname", "-I"],
515
+ ["ipconfig", "getifaddr", "en0"]]:
516
+ try:
517
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
518
+ ip = (r.stdout or "").strip().split()
519
+ if ip:
520
+ return f"http://{ip[0]}:{port}"
521
+ except (FileNotFoundError, subprocess.SubprocessError):
522
+ continue
523
+ return f"http://localhost:{port}"
524
+
525
+
526
+ def remote_setup_help() -> str:
527
+ """Return setup instructions for accessing a VPS-hosted Paperclip
528
+ dashboard from the operator's Mac/PC.
529
+
530
+ Three paths, from simplest to most secure:
531
+
532
+ 1. SSH port-forward (no extra software needed)
533
+ 2. Tailscale (zero-config VPN, recommended)
534
+ 3. Cloudflare Tunnel (public URL with auth)
535
+ """
536
+ return (
537
+ " Three ways to reach a VPS-hosted Paperclip dashboard from\n"
538
+ " your Mac/PC. Pick what fits your setup:\n"
539
+ "\n"
540
+ " ─── (1) SSH PORT-FORWARD (no extra install) ───\n"
541
+ " From your Mac/PC, run ONCE per session:\n"
542
+ " ssh -p 42820 -N -L 8080:localhost:8080 hacker@<vps-ip>\n"
543
+ " Then open http://localhost:8080 on your Mac browser.\n"
544
+ " Leave the SSH window open while you use the dashboard.\n"
545
+ "\n"
546
+ " ─── (2) TAILSCALE (recommended) ───\n"
547
+ " On the VPS:\n"
548
+ " curl -fsSL https://tailscale.com/install.sh | sh\n"
549
+ " sudo tailscale up\n"
550
+ " omega paperclip start --bind tailnet\n"
551
+ " On your Mac/PC: install Tailscale, log into the same account,\n"
552
+ " open http://<vps-tailscale-name>:8080.\n"
553
+ " Auth handled by Tailscale ACLs; no public exposure.\n"
554
+ "\n"
555
+ " ─── (3) CLOUDFLARE TUNNEL (public URL) ───\n"
556
+ " On the VPS:\n"
557
+ " brew install cloudflared (or apt-get install cloudflared)\n"
558
+ " cloudflared tunnel --url http://localhost:8080\n"
559
+ " cloudflared prints a public *.trycloudflare.com URL.\n"
560
+ " For a stable subdomain, use a named tunnel + DNS.\n"
561
+ " Combine with Cloudflare Access for auth.\n"
562
+ "\n"
563
+ " Currently bound: see `omega paperclip status`\n"
564
+ )
565
+
566
+
491
567
  def start_server(bind: str = "loopback", *, detach: bool = True) -> int:
492
568
  """Launch the Paperclip onboard server.
493
569
 
@@ -493,6 +493,11 @@ def _arrow_menu() -> int:
493
493
  (_label("Stealth scrape", "CloakBrowser"), "scrape:cloak"),
494
494
  (_label("Fast scrape", "Scrapling"), "scrape:scrapling"),
495
495
  ("", "__sep__"),
496
+ (_section("GOVERNANCE (Paperclip L0)"), "__sep__"),
497
+ (_label("Paperclip dashboard","web UI for 14 agents"), "paperclip:dashboard"),
498
+ (_label("Paperclip status", "bridge health"), "paperclip:status"),
499
+ (_label("Paperclip register","(re)sync OmegaOS company"), "paperclip:register"),
500
+ ("", "__sep__"),
496
501
  (_section("EXIT"), "__sep__"),
497
502
  (_label("Detach", "session keeps running"), "detach"),
498
503
  (_label("Quit Omega", "kills the tmux session"), "quit:kill"),
@@ -522,20 +527,27 @@ def _arrow_menu() -> int:
522
527
 
523
528
  while True:
524
529
  items = _build_items()
525
- lines = [disp for disp, _ in items]
530
+ # v0.19.37 bulletproof matching via tab-delimited index column.
531
+ # fzf with --ansi STRIPS escape codes from stdout, which broke
532
+ # the old `display == pick` exact-string match (every pick fell
533
+ # through, menu just reloaded). Now we prefix each line with a
534
+ # stable index (`<i>\t<display>`) and use --with-nth=2.. so fzf
535
+ # only DISPLAYS the second column but RETURNS the full line.
536
+ # We split off the index and look up the action — survives any
537
+ # ANSI/whitespace transformation fzf might apply.
538
+ lines = []
539
+ for i, (disp, key) in enumerate(items):
540
+ lines.append(f"{i}\t{disp}")
526
541
  header = (
527
542
  f"{ORANGE}{BOLD}Ω Omega OS v{__version__}{RST} "
528
543
  f"{MUTED}• ↑↓ navigate • ↵ pick • / search • Esc refresh{RST}"
529
544
  )
530
545
  try:
531
- # Claude Code LIGHT theme (white-paper) — bg cream #FAFAF7,
532
- # fg dark slate #3D3929, accent orange #D97757, muted warm
533
- # gray #88837A, soft border #A8A29E. Reads cleanly under
534
- # both light and dark terminal backgrounds (we override
535
- # fzf's bg explicitly).
536
546
  proc = subprocess.run(
537
547
  ["fzf",
538
548
  "--ansi",
549
+ "--delimiter=\t",
550
+ "--with-nth=2..",
539
551
  "--prompt=Ω › ",
540
552
  f"--header={header}",
541
553
  "--header-first",
@@ -570,12 +582,21 @@ def _arrow_menu() -> int:
570
582
  # Esc → refresh.
571
583
  continue
572
584
  pick = proc.stdout.rstrip("\n")
573
- action = None
574
- for disp, key in items:
575
- if disp == pick and key != "__sep__":
576
- action = key
577
- break
578
- if action is None:
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
+ if "\t" not in pick:
589
+ continue
590
+ idx_str, _disp_part = pick.split("\t", 1)
591
+ try:
592
+ idx = int(idx_str)
593
+ except ValueError:
594
+ continue
595
+ if not (0 <= idx < len(items)):
596
+ continue
597
+ action = items[idx][1]
598
+ if action == "__sep__":
599
+ # User picked a section header — silent reroll.
579
600
  continue
580
601
 
581
602
  # === Dispatch ===
@@ -665,6 +686,41 @@ def _arrow_menu() -> int:
665
686
  _run_inline([OMEGA_BIN, "scrape", url, "--engine", "scrapling"])
666
687
  continue
667
688
 
689
+ if action == "paperclip:dashboard":
690
+ # Show status, then offer to start if not running and open
691
+ # the URL. Reuses `omega paperclip start` which handles npx.
692
+ _run_inline([OMEGA_BIN, "paperclip", "status"])
693
+ try:
694
+ ans = input(f" {ORANGE}start the Paperclip dashboard now? "
695
+ f"[y/N]:{RST} ").strip().lower()
696
+ except (EOFError, KeyboardInterrupt):
697
+ ans = ""
698
+ if ans in ("y", "yes"):
699
+ bind = "loopback"
700
+ try:
701
+ bind_ans = input(
702
+ f" {ORANGE}bind mode [loopback/lan/tailnet] "
703
+ f"(default loopback):{RST} "
704
+ ).strip().lower() or "loopback"
705
+ if bind_ans in ("loopback", "lan", "tailnet"):
706
+ bind = bind_ans
707
+ except (EOFError, KeyboardInterrupt):
708
+ pass
709
+ _run_inline([OMEGA_BIN, "paperclip", "start", "--bind", bind])
710
+ # Try to open the browser at the default URL.
711
+ import shutil as _sh
712
+ opener = _sh.which("open") or _sh.which("xdg-open")
713
+ if opener:
714
+ subprocess.run([opener, "http://localhost:8080"],
715
+ capture_output=True)
716
+ continue
717
+ if action == "paperclip:status":
718
+ _run_inline([OMEGA_BIN, "paperclip", "status"])
719
+ continue
720
+ if action == "paperclip:register":
721
+ _run_inline([OMEGA_BIN, "paperclip", "register"])
722
+ continue
723
+
668
724
 
669
725
  def run_tui(prefer_textual: bool = False,
670
726
  force_repl: bool = False) -> int:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "omega-engine"
3
- version = "0.19.36"
3
+ version = "0.19.38"
4
4
  description = "The Omega OS orchestration engine — event-sourced, verified-completion agent graphs."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -48,6 +48,7 @@ class TestInstallerStepList(unittest.TestCase):
48
48
  # name (the human label) → step function name
49
49
  ("36-tmux-config", "step_tmux_config"),
50
50
  ("37-hermes-brief", "step_hermes_brief"),
51
+ ("38-personas", "step_personas"),
51
52
  ("59-hermes-session", "step_hermes_session"),
52
53
  ]
53
54
 
@@ -497,5 +498,134 @@ class TestPostInstallCardSurfacesEntryPoints(unittest.TestCase):
497
498
  self.assertIn("tmux", text)
498
499
 
499
500
 
501
+ # ---------------------------------------------------------------------------
502
+ # step_personas (v0.19.38) — eagerly seeds canonical OMEGAOS-CONTEXT.md +
503
+ # every LLM's expected persona filename for AISB-master + Hermès chats,
504
+ # so the user does NOT have to spawn a chat once before personas exist.
505
+ # ---------------------------------------------------------------------------
506
+
507
+
508
+ class TestStepPersonasBehavior(unittest.TestCase):
509
+ """Without this step, personas materialize lazily on first chat spawn —
510
+ `omega doctor` shows them missing and the operator can't pre-edit the
511
+ canonical. This locks in eager seeding."""
512
+
513
+ def test_personas_land_in_both_chat_contexts(self):
514
+ with tempfile.TemporaryDirectory() as tmp:
515
+ home = Path(tmp) / "Omega"
516
+ home.mkdir()
517
+ # Mirror the engine into OMEGA_HOME the way step_structure does.
518
+ engine_src = REPO_ROOT / "omega" / "Agentik_Engine"
519
+ engine_dst = home / "Agentik_Engine"
520
+ engine_dst.mkdir()
521
+ for entry in engine_src.iterdir():
522
+ if entry.name in {"tests", "__pycache__"}:
523
+ continue
524
+ if entry.is_dir():
525
+ import shutil
526
+ shutil.copytree(entry, engine_dst / entry.name)
527
+ else:
528
+ (engine_dst / entry.name).write_bytes(entry.read_bytes())
529
+
530
+ old_home = os.environ.get("OMEGA_HOME")
531
+ os.environ["OMEGA_HOME"] = str(home)
532
+ sys.path.insert(0, str(engine_dst))
533
+ try:
534
+ # Reload the engine modules under the new OMEGA_HOME.
535
+ import importlib
536
+ import omega_engine.personas as P
537
+ importlib.reload(P)
538
+
539
+ # Mirror what step_personas does (the heredoc body).
540
+ P.ensure_canonical(home)
541
+ for label in ("aisb-master", "hermes"):
542
+ ctx = home / "Agentik_Coding" / "chat-contexts" / label
543
+ P.write_all_personas(home, ctx)
544
+ finally:
545
+ if old_home is None:
546
+ os.environ.pop("OMEGA_HOME", None)
547
+ else:
548
+ os.environ["OMEGA_HOME"] = old_home
549
+ sys.path.remove(str(engine_dst))
550
+
551
+ # Canonical lands at the SSOT path.
552
+ canon = home / "Agentik_SSOT" / "personas" / "OMEGAOS-CONTEXT.md"
553
+ self.assertTrue(canon.is_file(),
554
+ f"canonical must exist at {canon} — operator must be able "
555
+ "to edit it BEFORE the first chat spawn")
556
+ self.assertGreater(canon.stat().st_size, 500,
557
+ "canonical context looks suspiciously small")
558
+
559
+ # Per-LLM mirrors land in BOTH chat contexts.
560
+ for label in ("aisb-master", "hermes"):
561
+ ctx = home / "Agentik_Coding" / "chat-contexts" / label
562
+ for fname in ("CLAUDE.md", "GEMINI.md", "AGENTS.md",
563
+ "QWEN.md", "HERMES.md", "CONVENTIONS.md"):
564
+ self.assertTrue((ctx / fname).is_file(),
565
+ f"missing {ctx}/{fname} — chat context not fully seeded")
566
+ # Subdir personas (.opencode shared by opencode/openrouter/deepseek).
567
+ self.assertTrue((ctx / ".opencode" / "CONTEXT.md").is_file(),
568
+ f"missing .opencode/CONTEXT.md in {ctx}")
569
+ self.assertTrue((ctx / ".continue" / "CONTEXT.md").is_file(),
570
+ f"missing .continue/CONTEXT.md in {ctx}")
571
+
572
+ def test_step_personas_heredoc_calls_required_helpers(self):
573
+ """Lock in the heredoc body — if someone renames or removes
574
+ ensure_canonical / write_all_personas the step still has to call
575
+ them or the install silently stops seeding."""
576
+ body = STEPS_SH.read_text()
577
+ m = re.search(r"^step_personas\(\) \{(.+?)^\}", body,
578
+ re.DOTALL | re.MULTILINE)
579
+ self.assertIsNotNone(m, "step_personas function not found in steps.sh")
580
+ fn_body = m.group(1)
581
+ for name in ("ensure_canonical", "write_all_personas"):
582
+ self.assertIn(name, fn_body,
583
+ f"step_personas must call {name}() — without it the install "
584
+ "stops seeding personas")
585
+ for label in ("aisb-master", "hermes"):
586
+ self.assertIn(label, fn_body,
587
+ f"step_personas must seed chat-contexts/{label}/")
588
+
589
+
590
+ # ---------------------------------------------------------------------------
591
+ # v0.19.38 — `--` separator must NOT raise "unknown argument".
592
+ # Repro: `npx -y @agentikos/omega-os@latest -- --full` lands in install.sh
593
+ # as `install.sh -- --full`, and the case-block used to reject `--`.
594
+ # ---------------------------------------------------------------------------
595
+
596
+
597
+ class TestDashDashSeparatorAccepted(unittest.TestCase):
598
+ """Two layers must accept `--`:
599
+ 1. bin/omega-os.js strips it before exec'ing bash.
600
+ 2. install.sh accepts it as a no-op as defense-in-depth."""
601
+
602
+ def test_install_sh_has_dash_dash_no_op_case(self):
603
+ if not INSTALL_SH.is_file():
604
+ self.skipTest("install.sh not in repo")
605
+ text = INSTALL_SH.read_text()
606
+ # The case-block must explicitly match `--`. Use re.search with
607
+ # MULTILINE so `^` matches each line start.
608
+ self.assertTrue(
609
+ re.search(r"^\s*--\)\s+shift\s*;;", text, re.MULTILINE),
610
+ "install.sh must accept `--` as a no-op — without it "
611
+ "`bash install.sh -- --full` dies with `unknown argument: --`")
612
+
613
+ def test_bin_launcher_filters_dash_dash(self):
614
+ bin_js = REPO_ROOT / "bin" / "omega-os.js"
615
+ if not bin_js.is_file():
616
+ self.skipTest("bin/omega-os.js not in repo")
617
+ text = bin_js.read_text()
618
+ # The filter must drop the literal `--` so the bash installer's
619
+ # case-block isn't even reached for the npx-injected separator.
620
+ # Match `.filter(...)` containing the literal "--" on the same line.
621
+ # Use DOTALL-free single-line scan, just look for the two anchors
622
+ # together: `.filter(` and `"--"` within ~80 chars.
623
+ self.assertTrue(
624
+ re.search(r'\.filter\(.{0,80}"--"', text),
625
+ "bin/omega-os.js must filter the `--` separator out of argv "
626
+ "before passing to install.sh — pattern `.filter(... \"--\")` "
627
+ "not found")
628
+
629
+
500
630
  if __name__ == "__main__":
501
631
  unittest.main()
@@ -100,5 +100,81 @@ class TestProviderStateSharedModule(unittest.TestCase):
100
100
  provider_state.set_active_provider)
101
101
 
102
102
 
103
+ class TestFzfMenuMatching(unittest.TestCase):
104
+ """v0.19.37 — the menu was broken because fzf with --ansi STRIPS
105
+ ANSI codes from its stdout, so the old `display == pick` exact
106
+ match was always False. Every pick fell through and the menu just
107
+ re-rendered. These tests lock in the index-based matching that
108
+ survives any ANSI/whitespace transformation."""
109
+
110
+ def test_fzf_ansi_strips_codes_from_stdout(self):
111
+ """Documents the upstream behaviour we're working around."""
112
+ import shutil
113
+ import subprocess
114
+ if not shutil.which("fzf"):
115
+ self.skipTest("fzf not installed")
116
+ ORANGE = "\033[38;2;217;119;87m"
117
+ RST = "\033[0m"
118
+ input_line = f" {ORANGE}AISB master chat{RST} hint"
119
+ proc = subprocess.run(
120
+ ["fzf", "--ansi", "--filter=AISB"],
121
+ input=input_line, capture_output=True, text=True,
122
+ )
123
+ pick = proc.stdout.rstrip("\n")
124
+ # The stdout has the SAME visible characters but NO escape codes.
125
+ self.assertEqual(pick, " AISB master chat hint")
126
+ self.assertNotEqual(pick, input_line,
127
+ "if these match, fzf preserves ANSI — our workaround was unnecessary "
128
+ "but harmless; if not (current behaviour), the workaround is required")
129
+
130
+ def test_arrow_menu_uses_tab_delimited_index(self):
131
+ """Inspect the source of `_arrow_menu` to confirm it builds
132
+ tab-delimited input + parses the index column. If someone reverts
133
+ to display-equality matching, this test fails LOUDLY."""
134
+ import inspect
135
+ from omega_engine.tui import _arrow_menu
136
+ src = inspect.getsource(_arrow_menu)
137
+ # The fix must use --delimiter=\t + --with-nth=2.. + split("\t", 1).
138
+ self.assertIn("--delimiter=", src,
139
+ "menu must use tab-delimited input to survive fzf's --ansi strip")
140
+ self.assertIn("--with-nth", src,
141
+ "menu must use --with-nth to display only the visual column")
142
+ self.assertIn('split("\\t", 1)', src,
143
+ "menu must parse the index column from the tab-delimited pick")
144
+ self.assertNotIn("if disp == pick", src,
145
+ "REGRESSION: don't bring back display-equality matching — "
146
+ "fzf strips ANSI from stdout, the equality always fails")
147
+
148
+
149
+ class TestPaperclipMenuIntegration(unittest.TestCase):
150
+ """The menu must surface the Paperclip governance actions
151
+ (dashboard / status / register) the user explicitly asked for in
152
+ v0.19.37. Also lock in the URL helper + remote-setup help string."""
153
+
154
+ def test_paperclip_dashboard_url_returns_string(self):
155
+ from omega_engine.paperclip_bridge import dashboard_url
156
+ self.assertTrue(dashboard_url("loopback").startswith("http://"))
157
+ # Explicit host override.
158
+ self.assertEqual(dashboard_url("loopback", host="vps.example.com"),
159
+ "http://vps.example.com:8080")
160
+
161
+ def test_remote_setup_help_lists_three_paths(self):
162
+ from omega_engine.paperclip_bridge import remote_setup_help
163
+ text = remote_setup_help()
164
+ # The three documented paths must all appear.
165
+ for kw in ("SSH PORT-FORWARD", "TAILSCALE", "CLOUDFLARE TUNNEL"):
166
+ self.assertIn(kw, text,
167
+ f"remote_setup_help must document the {kw} path")
168
+
169
+ def test_arrow_menu_contains_paperclip_actions(self):
170
+ import inspect
171
+ from omega_engine.tui import _arrow_menu
172
+ src = inspect.getsource(_arrow_menu)
173
+ for action in ("paperclip:dashboard", "paperclip:status",
174
+ "paperclip:register"):
175
+ self.assertIn(action, src,
176
+ f"arrow menu must wire {action} (user asked for it in v0.19.37)")
177
+
178
+
103
179
  if __name__ == "__main__":
104
180
  unittest.main()
@@ -1 +1 @@
1
- 0.19.36
1
+ 0.19.38
@@ -0,0 +1,90 @@
1
+ # OmegaOS v0.19.38 — multi-LLM persona seeding + `--` argument fix
2
+
3
+ > Run 2026-05-25, post-v0.19.37 (fzf indexed matching + Paperclip menu).
4
+ > Two surgical fixes, both fully tested.
5
+
6
+ ## 1. The two issues fixed
7
+
8
+ | # | Bug | Trigger | Root cause | Fix |
9
+ |---|---|---|---|---|
10
+ | 1 | `!! unknown argument: --` | `npx -y @agentikos/omega-os@latest -- --full` | `bin/omega-os.js` forwarded `--` raw → `install.sh` case-block had no `--)` arm | (a) launcher filters `--` from argv, (b) install.sh accepts `--` as a no-op (defense-in-depth) |
11
+ | 2 | Personas lazy on first chat spawn | Operator can't inspect/edit `OMEGAOS-CONTEXT.md` before first AISB-chat; `omega doctor` showed nothing | No install step called `personas.ensure_canonical()` / `write_all_personas()` — engine was wired but never invoked at install | New `step_personas` (position 38) eagerly seeds canonical + both chat-context dirs with all 8 LLM persona files |
12
+
13
+ ## 2. What `step_personas` does (full materialization at install time)
14
+
15
+ ```
16
+ $OMEGA_HOME/
17
+ ├── Agentik_SSOT/personas/OMEGAOS-CONTEXT.md (1) canonical
18
+ ├── Agentik_Coding/chat-contexts/aisb-master/ (8) AISB chat
19
+ │ ├── CLAUDE.md ← claude / claude-code
20
+ │ ├── GEMINI.md ← gemini-cli
21
+ │ ├── AGENTS.md ← openai codex
22
+ │ ├── QWEN.md ← qwen-code
23
+ │ ├── CONVENTIONS.md ← aider
24
+ │ ├── HERMES.md ← hermès
25
+ │ ├── .opencode/CONTEXT.md ← opencode / openrouter / deepseek
26
+ │ └── .continue/CONTEXT.md ← continue.dev
27
+ └── Agentik_Coding/chat-contexts/hermes/ (8) Hermès chat
28
+ └── … same 8 files …
29
+ ```
30
+
31
+ **Result: 17 persona files on disk at install time** (1 canonical + 16 mirrors).
32
+ Operator can `cat / vim` any of them before the first chat spawn. Each LLM CLI
33
+ that lands in either dir reads its native filename and gets the SAME OmegaOS
34
+ context. Hot-swap (`omega switch <provider>`) just re-renders the active
35
+ context's pointer; nothing else moves.
36
+
37
+ ## 3. The Three Laws are in the canonical (mandatory baseline for every LLM)
38
+
39
+ Every persona file is a mirror of `Agentik_SSOT/personas/OMEGAOS-CONTEXT.md`,
40
+ which embeds:
41
+
42
+ 1. **Code lies. Comments lie. Only runtime tells the truth.**
43
+ 2. **Researcher, not sycophant.** Challenge → think → iterate → root cause.
44
+ 3. **Autonomous execution.** When dispatched, never wait — decide, execute,
45
+ report via `.done.json`.
46
+
47
+ Plus the L1-L5 layer map, the 17 Quality Arsenal audits, the OMEGA_HOME
48
+ folder map, and the `omega` command surface.
49
+
50
+ ## 4. Multi-agent setup — verification (from runtime)
51
+
52
+ | Question | Answer | Evidence |
53
+ |---|---|---|
54
+ | Is the canonical persona file linked for every LLM? | **Yes.** 10 LLM ids → 8 distinct filenames (.opencode/CONTEXT.md is shared by opencode/openrouter/deepseek). | `omega_engine/personas.py::_LLM_PERSONA_PATHS` |
55
+ | Is everything setup at install time (not lazy)? | **Yes (new in v0.19.38).** `step_personas` runs at install step 38 and seeds both chat contexts before the user opens any session. | `bootstrap/lib/steps.sh::step_personas`, `install.sh STEPS[]` |
56
+ | Are rules respected per LLM? | **Yes.** Same canonical written to every LLM filename — same Three Laws, same architecture, same audits surface. | `personas.write_all_personas()` content equality check |
57
+ | Are AISB + Oracle + Workers using Claude Max OAuth (no API keys)? | **Yes.** `spawn_aisb_chat` shells out to `claude` which reads `~/.claude/.credentials.json`; no `ANTHROPIC_API_KEY` injection. | `omega_engine/tmux.py::spawn_aisb_chat` |
58
+ | Is Hermès budget-isolated? | **Yes.** `spawn_hermes_chat` reads `ANTHROPIC_API_KEY_HERMES` from the age-encrypted vault and inlines it on the `claude` command for that session only — never touches Max OAuth. | `omega_engine/tmux.py::spawn_hermes_chat` lines 433-468 |
59
+ | Does `omega doctor` verify all this? | **Yes (new in v0.19.38).** New `personas` section reports canonical + per-LLM file counts for both contexts. | `cli.py::cmd_doctor` `section("personas")` |
60
+ | Will the user have to do anything post-install? | **No.** One `npx -y @agentikos/omega-os@latest --full` and every LLM persona is on disk, Three Laws encoded, AISB/Hermès tmux sessions ready. | end-to-end install pipeline |
61
+
62
+ ## 5. Tests (regression locks)
63
+
64
+ `tests/test_installer_wiring.py` — 4 new tests:
65
+
66
+ | Test | Locks in |
67
+ |---|---|
68
+ | `TestInstallerStepList` (extended) | `38-personas:step_personas` is registered in `install.sh STEPS[]` AND defined in `steps.sh` |
69
+ | `TestStepPersonasBehavior::test_personas_land_in_both_chat_contexts` | Running step_personas against a temp `OMEGA_HOME` materializes canonical + 8 persona files in BOTH aisb-master/ and hermes/ |
70
+ | `TestStepPersonasBehavior::test_step_personas_heredoc_calls_required_helpers` | The heredoc body explicitly calls `ensure_canonical` AND `write_all_personas` AND seeds both labels — rename-safe |
71
+ | `TestDashDashSeparatorAccepted::test_install_sh_has_dash_dash_no_op_case` | `install.sh` case-block contains `--) shift ;;` — without it `bash install.sh -- --full` dies |
72
+ | `TestDashDashSeparatorAccepted::test_bin_launcher_filters_dash_dash` | `bin/omega-os.js` filters `--` from argv before passing to bash |
73
+
74
+ **Suite total: 627 passed** (was 623; +4 new, +0 regressions).
75
+
76
+ ## 6. Recipe (after v0.19.38 ships)
77
+
78
+ ```bash
79
+ # Either of these works now (the `--` is no-op):
80
+ npx -y @agentikos/omega-os@latest --full
81
+ npx -y @agentikos/omega-os@latest -- --full
82
+ ```
83
+
84
+ ## 7. Verdict
85
+
86
+ ✅ Multi-LLM persona setup is **complete and verified at install time**.
87
+ ✅ `--` argument bug **eliminated at both layers** (launcher filter + install.sh no-op).
88
+ ✅ `omega doctor` **now reports persona health** alongside AISB suite, providers, vault, etc.
89
+ ✅ No regression in existing 623 tests.
90
+ ✅ User's invariant satisfied: **"l'utilisateur, une fois qu'il a setup tout l'outil OmegaOS, doit être 100% fonctionnel. Il n'a rien à faire à part l'utiliser."**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentikos/omega-os",
3
- "version": "0.19.36",
3
+ "version": "0.19.38",
4
4
  "description": "Omega OS — installable agentic operating system with verified-completion orchestration. Event-sourced engine, 8-block rack, autonomous agents, MCP.",
5
5
  "bin": {
6
6
  "omega-os": "bin/omega-os.js"