From 9972c07c610dc6ae5e78923a2c32ad0867d210b8 Mon Sep 17 00:00:00 2001 From: operator Date: Fri, 9 Oct 2026 19:21:12 -0400 Subject: [PATCH] feat(cli): add shorthand error helpers, usage polling on bare box, and comprehensive help manuals --- bin/box-work.py | 107 +- bin/super-cli.py | 8055 ++++++++++++++++++++++++++++++++++++++++ tests/test_box_work.py | 58 + 3 files changed, 8219 insertions(+), 1 deletion(-) create mode 100755 bin/super-cli.py diff --git a/bin/box-work.py b/bin/box-work.py index 6a696ec..d1aa728 100755 --- a/bin/box-work.py +++ b/bin/box-work.py @@ -783,8 +783,113 @@ def cmd_chats(args): print(f"[{c_cyan(ag)} : {c_dim(tname)}] {c_dim(ts)} {c_bold(author)}:\n{text}\n" + c_dim("-" * 60)) print() +WORK_COMMAND_EXAMPLES = { + "box work": [ + "box work # View fleet workspace dashboard & signals", + "box work check [agent] # Audit pre-flight health gates", + "box work heal # Automated remediation & chat nudge", + "box work start \"\" --to <agent> # Start & dispatch new build ticket", + "box work assign <issue#> --to <agent> # Assign existing ticket", + "box work merge <pr#> # Verify tests and merge PR to master", + "box work chats --agent <name> # View live multi-agent chat feed", + ], + "box work start": [ + "box work start \"Fix SSH perms\" --to 646", + "box work start \"Build integration tests\" --to pip --goal \"Run pytest on endpoints\"", + "box work start \"Emergency rebuild\" --to dev --force", + ], + "box work check": [ + "box work check # Check all agents", + "box work check 646 # Check specific agent", + ], + "box work heal": [ + "box work heal dev # Heal dev agent (token, perms, tunnel nudge)", + "box work heal 646", + ], + "box work assign": [ + "box work assign 218 --to 646", + ], + "box work merge": [ + "box work merge 217 # Test and merge PR 217 into master", + ], + "box work chats": [ + "box work chats # Last 10 chat messages across fleet", + "box work chats --agent opm --limit 5", + ], +} + +def format_work_error_shorthand(parser, message): + lines = [] + lines.append(f"\n{c_bold(c_red('āŒ CLI ERROR:'))} {c_bold(message)}\n") + lines.append(c_bold(c_yellow("šŸ’” SHORTHAND USAGE HELPER:"))) + lines.append(f" Command: {c_bold(parser.prog)}") + + sub_action = next((a for a in parser._actions if isinstance(a, argparse._SubParsersAction)), None) + if sub_action: + lines.append(f"\n{c_bold(' Available Subcommands:')}") + for name, subp in sub_action.choices.items(): + h = subp.description or getattr(subp, "help", "") or "" + if not h and getattr(sub_action, "_choices_actions", None): + for ca in sub_action._choices_actions: + if ca.dest == name: + h = ca.help or "" + break + lines.append(f" • {c_bold(f'{name:<12}')} {c_dim(h)}") + + positionals = [a for a in parser._actions if not a.option_strings and a.dest != 'help' and not isinstance(a, argparse._SubParsersAction)] + required_options = [a for a in parser._actions if a.option_strings and a.required and a.dest != 'help'] + optional_options = [a for a in parser._actions if a.option_strings and not a.required and a.dest != 'help'] + + if positionals or required_options: + lines.append(f"\n{c_bold(' Required Parameters / Arguments:')}") + for a in positionals: + lines.append(f" • {c_bold(f'{a.dest:<14}')} {a.help or '(positional)'}") + for a in required_options: + opts = "/".join(a.option_strings) + lines.append(f" • {c_bold(f'{opts:<14}')} {a.help or '(required flag)'}") + + if optional_options: + lines.append(f"\n{c_bold(' Optional Flags:')}") + for a in optional_options: + opts = "/".join(a.option_strings) + lines.append(f" • {c_cyan(f'{opts:<14}')} {c_dim(a.help or '')}") + + prog_key = parser.prog.strip() + examples = WORK_COMMAND_EXAMPLES.get(prog_key) or WORK_COMMAND_EXAMPLES.get("box work") + if examples: + lines.append(f"\n{c_bold(' Quick Examples:')}") + for ex in examples: + lines.append(f" {c_green(ex)}") + + lines.append(f"\n šŸ“– {c_dim('For complete manual:')} {c_bold(f'{parser.prog} --help')} {c_dim('(or')} {c_bold(f'box help {parser.prog.split()[-1]}')}{c_dim(')')}\n") + return "\n".join(lines) + +class WorkArgumentParser(argparse.ArgumentParser): + def error(self, message): + print(format_work_error_shorthand(self, message), file=sys.stderr) + sys.exit(2) + + def format_help(self): + base_help = super().format_help() + prog_key = self.prog.strip() + examples = WORK_COMMAND_EXAMPLES.get(prog_key) or WORK_COMMAND_EXAMPLES.get("box work") + extra = [] + if examples: + extra.append(c_bold("\nSHORTHAND EXAMPLES:")) + for ex in examples: + extra.append(f" {c_green(ex)}") + extra.append(c_bold("\nOPERATIONAL GUIDELINES:")) + extra.append(f" • {c_cyan('Shorthand parameter reference:')} run {c_bold('box')} alone") + extra.append(f" • {c_cyan('Comprehensive manual:')} run {c_bold('box help work')}") + extra.append(f" • {c_cyan('JSON output:')} append {c_bold('--json')} to any query command\n") + return base_help + "\n".join(extra) + def main(): - parser = argparse.ArgumentParser( + if len(sys.argv) > 1 and "help" in sys.argv[1:]: + idx = sys.argv.index("help") + sys.argv[idx] = "--help" + + parser = WorkArgumentParser( prog="box work", description="Fleet Workspace, Work Scope, and Task Orchestration Engine." ) diff --git a/bin/super-cli.py b/bin/super-cli.py new file mode 100755 index 0000000..bbb827b --- /dev/null +++ b/bin/super-cli.py @@ -0,0 +1,8055 @@ +#!/usr/bin/env python3 +"""super-cli.py — Unified Off-Board Orchestrator CLI for NetVM & Box. + +Empowers 'off-board super' to govern, task, and observe the fleet of +autonomous browser agents (muse, pip, 646, opm) on bl and interface +with the Google Cloud VM web surfaces. + +Domains: + super muse-choices — Muse TUI A/B/C auto-answer daemon (on/off/status/logs) + super fleet — Node health, CDP status, active tabs, watch, restart, heal + super watchdog — Watchdog timers: status, trigger runs + super dm — Inter-agent DMs, work orders ([WO]), acks, live log tail + super thread — Inspect agent main chats, sidechats, and scrollbacks + super job — Manage scheduled jobs, systemd timers, triggers, logs + super web — Probe VM web surfaces (box.muse-dev.online), auth, sync + super loop — Intrinsic loop strategy, fleet loop health, break taxonomy, vars + super strat — Shortcut to inspect & configure loop modulation strategies + super vars — Shortcut to inspect & adjust runtime control variables + +Usage: + super fleet [status|watch|restart|cdp|heal] + super watchdog [status|run <node|relay>] + super dm [send|wo|ack|log|tail] + super thread [list|view] + super job [list|status|run|log|create|enable|disable|delete] + super web [health|test-auth|sync] + super loop [status|health|breaks|strat|vars|close|nudge|sweep|harvest|remediate] + super strat [show|set|reset|eval] + super vars [list|get|set|reset|history|rollback] +""" + +import argparse +import hashlib +import json +import os +import re +import shutil +import subprocess +import sys +import time +import urllib.error +import urllib.request +from datetime import datetime, timezone +from pathlib import Path + +# Paths +NETVM_ROOT = Path("/home/super/Projects/NetVM") +BIN_DIR = NETVM_ROOT / "bin" +JOBS_DIR = NETVM_ROOT / "jobs" +TRANSFERS_DIR = NETVM_ROOT / "transfers" +DM_LOG = NETVM_ROOT / "dm-log.jsonl" +JOB_LOG = NETVM_ROOT / "job-log.jsonl" +CTL_LOG = NETVM_ROOT / "box-ctl.jsonl" +REGISTRY_FILE = NETVM_ROOT / "NODES.md" +CHAT_HISTORY_LOG = NETVM_ROOT / "logs" / "chat-history.jsonl" +WATERMARKS_FILE = NETVM_ROOT / "siphon-watermarks.json" +FOLLOWUPS_FILE = NETVM_ROOT / "followups.json" +JOB_SIDECHATS_FILE = NETVM_ROOT / "job-sidechats.json" +RESPONSE_HARVESTER_PY = BIN_DIR / "response-harvester.py" +FOLLOWUP_SWEEPER_PY = BIN_DIR / "followup-sweeper.py" +PIPELINES_FILE = NETVM_ROOT / "pipelines.json" +JOB_DISPATCH_PY = BIN_DIR / "job-dispatch.py" + +# Agent Constants +VALID_NODES = ["muse", "pip", "646", "opm", "def", "dev"] +DEFAULT_SENDER = "super" +DEFAULT_AGENT_SIDECHATS = { + "646": "646 tasks", + "opm": "heartbeat", + "pip": "646-pip-coord", + "muse": "muse tasks", +} +MUSE_TMUX_LOG_DIR = NETVM_ROOT / "logs" / "tmux" + +# --------------------------------------------------------------------------- +# ANSI Color & Formatting Engine +# --------------------------------------------------------------------------- +USE_COLOR = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None + +def _c(code: str, text: str) -> str: + return f"\033[{code}m{text}\033[0m" if USE_COLOR else str(text) + +def c_bold(s: str) -> str: return _c("1", s) +def c_dim(s: str) -> str: return _c("2", s) +def c_green(s: str) -> str: return _c("32", s) +def c_red(s: str) -> str: return _c("31", s) +def c_yellow(s: str) -> str: return _c("33", s) +def c_blue(s: str) -> str: return _c("34", s) +def c_magenta(s: str) -> str: return _c("35", s) +def c_cyan(s: str) -> str: return _c("36", s) + +def badge_ok(s: str = "UP") -> str: return c_green(f"ā— {s}") +def badge_warn(s: str = "WARN") -> str: return c_yellow(f"ā— {s}") +def badge_err(s: str = "DOWN") -> str: return c_red(f"ā— {s}") +def badge_dim(s: str) -> str: return c_dim(f"ā—‹ {s}") + +def print_table(headers: list, rows: list, alignments: list = None): + """Print an aligned tabular display with clean ANSI formatting.""" + if not rows: + print(c_dim(" (no records found)")) + return + + num_cols = len(headers) + alignments = alignments or ["left"] * num_cols + + # Calculate max visual length per column (stripping ANSI escapes) + strip_ansi = re.compile(r"\033\[[0-9;]*m") + + def vlen(s): + return len(strip_ansi.sub("", str(s))) + + col_widths = [vlen(h) for h in headers] + for row in rows: + for i in range(num_cols): + val = str(row[i]) if i < len(row) else "" + col_widths[i] = max(col_widths[i], vlen(val)) + + # Header + hdr_parts = [] + sep_parts = [] + for i, h in enumerate(headers): + w = col_widths[i] + align = alignments[i] + pad = w - vlen(h) + if align == "right": + hdr_parts.append(" " * pad + c_bold(h)) + else: + hdr_parts.append(c_bold(h) + " " * pad) + sep_parts.append("─" * w) + + print(" " + " ".join(hdr_parts)) + print(" " + c_dim(" ".join(sep_parts))) + + # Rows + for row in rows: + row_parts = [] + for i in range(num_cols): + val = str(row[i]) if i < len(row) else "" + w = col_widths[i] + align = alignments[i] + pad = max(0, w - vlen(val)) + if align == "right": + row_parts.append(" " * pad + val) + else: + row_parts.append(val + " " * pad) + print(" " + " ".join(row_parts)) + +def parse_relative_time(ts_str: str) -> str: + """Convert ISO timestamp to short relative time (e.g. 5m ago, 2h ago).""" + if not ts_str: + return "-" + try: + dt = datetime.fromisoformat(ts_str.replace("Z", "+00:00")) + now = datetime.now(timezone.utc) + diff = int((now - dt).total_seconds()) + if diff < 0: + return "just now" + if diff < 60: + return f"{diff}s ago" + if diff < 3600: + return f"{diff // 60}m ago" + if diff < 86400: + return f"{diff // 3600}h ago" + return f"{diff // 86400}d ago" + except Exception: + return ts_str[:16].replace("T", " ") + +# --------------------------------------------------------------------------- +# Registry & Network Helpers +# --------------------------------------------------------------------------- +def get_node_network_info(node: str) -> dict: + """Compute deterministic veth/peer IP and load pinned CDP port.""" + tag = hashlib.sha256(node.encode()).hexdigest()[:8] + idx = int(tag[:3], 16) % 200 + 10 + gw_ip = f"10.201.{idx}.1" + peer_ip = f"10.201.{idx}.2" + + # Pinned ports from NODES.md registry + pinned_ports = { + "muse": 9410, + "pip": 9420, + "646": 9430, + "opm": 9440, + "def": 9450, + "dev": 9455, + } + cdp_port = pinned_ports.get(node, 9222 + int(tag[4:7], 16) % 2000) + + return { + "node": node, + "netns": f"warp-{node}", + "tag": tag, + "gw_ip": gw_ip, + "peer_ip": peer_ip, + "cdp_port": cdp_port, + } + +def probe_cdp_status(peer_ip: str, cdp_port: int, timeout: float = 1.5) -> dict: + """Probe Chromium DevTools protocol on peer IP.""" + url = f"http://{peer_ip}:{cdp_port}/json/version" + t0 = time.time() + try: + req = urllib.request.Request(url, headers={"User-Agent": "super-cli/1.0"}) + with urllib.request.urlopen(req, timeout=timeout) as resp: + elapsed_ms = int((time.time() - t0) * 1000) + if resp.status == 200: + data = json.load(resp) + return { + "ok": True, + "latency_ms": elapsed_ms, + "browser": data.get("Browser", "Chromium"), + } + except Exception as e: + return {"ok": False, "error": str(e), "latency_ms": None} + return {"ok": False, "error": "unknown error", "latency_ms": None} + +def fetch_active_tab(peer_ip: str, cdp_port: int, timeout: float = 1.5) -> dict: + """Retrieve title and URL of the active browser page.""" + url = f"http://{peer_ip}:{cdp_port}/json/list" + try: + req = urllib.request.Request(url, headers={"User-Agent": "super-cli/1.0"}) + with urllib.request.urlopen(req, timeout=timeout) as resp: + tabs = json.load(resp) + pages = [t for t in tabs if t.get("type") == "page"] + if pages: + return { + "title": pages[0].get("title", ""), + "url": pages[0].get("url", ""), + } + except Exception: + pass + return {"title": "-", "url": "-"} + +def check_process_alive(node: str) -> bool: + """Check if Chromium process exists for the node's profile.""" + try: + res = subprocess.run( + ["pgrep", "-f", f"chrome-box/profiles/{node}"], + capture_output=True, text=True, timeout=2 + ) + return res.returncode == 0 + except Exception: + return False + +def get_queue_depth(node: str) -> int: + """Check active flock tickets in /tmp/cdp-queue/<node>/.""" + q_dir = Path(f"/tmp/cdp-queue/{node}") + if not q_dir.exists(): + return 0 + try: + tickets = [f for f in q_dir.iterdir() if f.name.startswith("ticket-")] + return len(tickets) + except Exception: + return 0 + +# --------------------------------------------------------------------------- +# Domain: FLEET +# --------------------------------------------------------------------------- +def _host_evidence_for(results): + """Host watchdog evidence, or None when unneeded/unavailable. + + Queried only when at least one node is fully blind (both local + probes negative). Never raises: evidence must not break status. + """ + if not any(not r["proc_alive"] and not r["cdp_ok"] for r in results): + return None + try: + import host_evidence + return host_evidence.collect([r["node"] for r in results]) + except Exception: + return None + + +def _resolve_statuses(results) -> None: + """Attach effective status/source/evidence to each collected node. + + proc_alive/cdp_ok keep their local-probe meaning; status is the + display verdict. Host evidence only decides the fully-blind pattern + (both local probes negative); live local signals always win. + """ + try: + import host_evidence + have_mod = True + except ImportError: + have_mod = False + ev_map = _host_evidence_for(results) if have_mod else None + for r in results: + ev = (ev_map or {}).get(r["node"]) if ev_map else None + if not r["proc_alive"] and r["cdp_ok"]: + # CDP answers: the browser is definitionally alive even when + # pgrep cannot see it (PID-blind shell, renamed profile path). + r["status"], r["source"] = "ACTIVE", "local" + elif r["proc_alive"] and not r["cdp_ok"]: + r["status"], r["source"] = "CDP_DOWN", "local" + elif r["proc_alive"] and r["cdp_ok"]: + r["status"], r["source"] = "ACTIVE", "local" + elif ev is None: + r["status"], r["source"] = "STOPPED", "local" + else: + r["status"], r["source"] = host_evidence.effective_status( + False, False, ev.get("browser", "unknown"), + ev.get("cdp", "unknown")) + r["evidence"] = ev + + +def collect_fleet_data() -> list: + results = [] + for node in VALID_NODES: + info = get_node_network_info(node) + proc_alive = check_process_alive(node) + cdp = probe_cdp_status(info["peer_ip"], info["cdp_port"]) + tab = fetch_active_tab(info["peer_ip"], info["cdp_port"]) if cdp["ok"] else {"title": "-", "url": "-"} + q_depth = get_queue_depth(node) + + approval_pending = False + approval_detail = None + if cdp["ok"] and ("(1)" in tab.get("title", "") or "approval" in tab.get("title", "").lower()): + try: + import approvals + app_info = approvals.inspect_node_approvals(node) + if app_info.get("has_pending"): + approval_pending = True + approval_detail = app_info + except Exception: + pass + + results.append({ + "node": node, + "netns": info["netns"], + "peer_ip": info["peer_ip"], + "cdp_port": info["cdp_port"], + "proc_alive": proc_alive, + "cdp_ok": cdp["ok"], + "latency_ms": cdp["latency_ms"], + "title": tab["title"], + "url": tab["url"], + "queue_depth": q_depth, + "approval_pending": approval_pending, + "approval_detail": approval_detail, + }) + _resolve_statuses(results) + return results + +def cmd_fleet_status(args): + data = collect_fleet_data() + if args.json: + print(json.dumps({"ok": True, "fleet": data}, indent=2)) + return + + print("\n" + c_bold("=== NETVM FLEET STATUS ===") + c_dim(f" ({datetime.now().strftime('%H:%M:%S')} local)\n")) + # Top-line host stability badge + try: + sys.path.insert(0, str(NETVM_ROOT / "watchers")) + import importlib.util + spec = importlib.util.spec_from_file_location("box_stability_watcher", str(NETVM_ROOT / "watchers" / "box-stability-watcher.py")) + bsw = importlib.util.module_from_spec(spec) + spec.loader.exec_module(bsw) + m = bsw.get_system_metrics() + load_str = f"Load: {m['load_1m']} (1m) | {m['load_5m']} (5m) [Cores: {m['cpu_count']}]" + ram_str = f"RAM: {m['ram_used_pct']}%" + if m['load_1m'] < 20 and m['ram_used_pct'] < 80: + stab_badge = badge_ok("STABLE") + elif m['load_1m'] >= 60 or m['ram_used_pct'] >= 95: + stab_badge = badge_err("EMERGENCY") + else: + stab_badge = badge_warn("ELEVATED") + print(f" HOST {c_bold('bl')}: {stab_badge} {c_dim(load_str + ' ' + ram_str)}\n") + except Exception: + pass + + headers = ["NODE", "STATUS", "PEER IP:PORT", "LATENCY", "QUEUE", "ACTIVE PAGE / THREAD"] + rows = [] + has_any_approval = False + for item in data: + # Status calculation (effective status; see _resolve_statuses) + if item.get("approval_pending"): + status = badge_warn("APPROVAL_REQ") + has_any_approval = True + elif item.get("status") == "ACTIVE": + status = badge_ok("ACTIVE") + elif item.get("status") == "CDP_DOWN": + status = badge_warn("CDP_DOWN") + elif item.get("status") == "UNKNOWN": + status = badge_dim("UNKNOWN") + else: + status = badge_err("STOPPED") + + lat = f"{item['latency_ms']}ms" if item["latency_ms"] is not None else "-" + q = str(item["queue_depth"]) if item["queue_depth"] > 0 else c_dim("idle") + + # Format title/URL nicely + title = item["title"] + if item.get("approval_pending"): + target = item.get("approval_detail", {}).get("ip") or "request" + title = f"{c_yellow('[APPROVAL: ' + target + ']')} {title}" + elif "thread/" in item["url"]: + m = re.search(r"thread/([0-9a-fA-F-]+)", item["url"]) + if m: + uuid_short = m.group(1)[:8] + title = f"{title} [{c_cyan(uuid_short)}]" + elif item["url"] == "https://muse.ai/": + title = f"{title} [{c_dim('home')}]" + + rows.append([ + c_bold(item["node"]), + status, + f"{item['peer_ip']}:{item['cdp_port']}", + lat, + q, + title[:50] + ]) + + print_table(headers, rows) + if any(r.get("source") == "host-evidence" for r in data): + print(c_dim(" [*] status via host watchdog evidence (local probes blind in this shell)")) + if has_any_approval: + print("\n" + c_yellow(" ⚠ Agent(s) held up on browser approval. Run 'box approvals' to inspect/allow.")) + print("\n" + c_dim(" Commands: super fleet watch | super fleet heal <node> | box approvals [check|allow|auto]") + "\n") + +def cmd_fleet_watch(args): + interval = getattr(args, "interval", 2) + try: + while True: + # Clear terminal + sys.stdout.write("\033[2J\033[H") + sys.stdout.flush() + cmd_fleet_status(args) + print(c_dim(f" [Auto-refreshing every {interval}s. Press Ctrl+C to exit]")) + time.sleep(interval) + except KeyboardInterrupt: + print("\n" + c_dim("Exited watch mode.")) + +def cmd_fleet_restart(args): + node = args.node + if node not in VALID_NODES: + print(f"Error: Unknown node '{node}'. Valid: {', '.join(VALID_NODES)}", file=sys.stderr) + sys.exit(1) + + print(f"Restarting node '{c_bold(node)}' via netvm-node-up.sh...") + cmd = ["sudo", str(BIN_DIR / "netvm-node-up.sh"), node] + res = subprocess.run(cmd, text=True) + if res.returncode == 0: + print(c_green(f"Node '{node}' brought up successfully.")) + else: + print(c_red(f"Node restart exited with code {res.returncode}.")) + sys.exit(res.returncode) + +def cmd_fleet_cdp(args): + node = args.node + if node not in VALID_NODES: + print(f"Error: Unknown node '{node}'", file=sys.stderr) + sys.exit(1) + + info = get_node_network_info(node) + peer = info["peer_ip"] + port = info["cdp_port"] + + if args.json: + print(json.dumps({ + "node": node, + "peer_ip": peer, + "port": port, + "url": f"http://{peer}:{port}", + "ssh_forward": f"ssh -L {port}:{peer}:{port} super@100.123.153.75" + }, indent=2)) + return + + print(f"\n{c_bold('CDP Access for ' + node)}:") + print(f" Host Endpoint : {c_cyan(f'http://{peer}:{port}/json/version')}") + print(f" SSH Forward : {c_yellow(f'ssh -L {port}:{peer}:{port} super@100.123.153.75')}") + print(f" Local Connect : {c_dim(f'http://127.0.0.1:{port}')} (after forwarding)\n") + +# --------------------------------------------------------------------------- +# Domain: FLEET HEAL + WATCHDOG +# --------------------------------------------------------------------------- +SYSTEMD_SYSTEM_DIR = Path("/etc/systemd/system") +WATCHDOG_LOCK_TMPL = "/tmp/chromebox-watchdog-{node}.lock" +WARP_PROBE_URL = "https://1.1.1.1/cdn-cgi/trace" +CHROMEBOX_TIMER_TMPL = """[Unit] +Description=Run chromebox watchdog for {node} every 2 minutes + +[Timer] +RandomizedDelaySec=30s +OnBootSec=2min +OnUnitActiveSec=2min +Unit=chromebox-watchdog@{node}.service + +[Install] +WantedBy=timers.target +""" + + +def _sh(cmd, timeout=120, input_text=None): + """Run cmd, capture output. Returns (returncode, combined_output).""" + try: + r = subprocess.run(cmd, capture_output=True, text=True, + timeout=timeout, input=input_text) + return r.returncode, ((r.stdout or "") + (r.stderr or "")).strip() + except subprocess.TimeoutExpired: + return 124, "timed out after %ds: %s" % (timeout, " ".join(cmd)) + except OSError as e: + return 127, str(e) + + +def _heal_lock_step(node): + """Clear a stale unwritable watchdog lock (blocks watchdog startup).""" + lock = Path(WATCHDOG_LOCK_TMPL.format(node=node)) + if not lock.exists() or os.access(lock, os.W_OK): + return True, "lock writable or absent" + rc, out = _sh(["sudo", "rm", "-f", str(lock)], timeout=30) + if rc == 0: + return True, "removed stale lock %s" % lock + return False, "cannot remove %s: %s" % (lock, out) + + +def _heal_timer_step(node): + """Install (if missing) and enable the node's watchdog timer.""" + timer = "chromebox-watchdog-%s.timer" % node + path = SYSTEMD_SYSTEM_DIR / timer + if not path.exists(): + rc, out = _sh(["sudo", "tee", str(path)], timeout=30, + input_text=CHROMEBOX_TIMER_TMPL.format(node=node)) + if rc != 0: + return False, "cannot install %s: %s" % (timer, out) + rc, out = _sh(["sudo", "systemctl", "daemon-reload"], timeout=60) + if rc != 0: + return False, "daemon-reload failed: %s" % out + rc, out = _sh(["sudo", "systemctl", "enable", "--now", timer], timeout=60) + if rc != 0: + return False, "enable --now failed: %s" % out + return True, "timer installed+active" + + +def _heal_tunnel_step(node): + """Restart the node's Warp tunnel + CDP relay via netvm-node-up.sh.""" + rc, out = _sh(["sudo", str(BIN_DIR / "netvm-node-up.sh"), node], timeout=240) + last = out.strip().splitlines()[-1] if out.strip() else "(no output)" + if rc != 0: + return False, "netvm-node-up.sh rc=%d: %s" % (rc, last) + if "egress=unknown" in out or "no handshake yet" in out: + return False, last + return True, last + + +def _warp_egress_ok(node): + """True when the node's netns reaches the warp probe URL.""" + rc, out = _sh(["sudo", "ip", "netns", "exec", "warp-%s" % node, + "curl", "-sk", "--max-time", "10", WARP_PROBE_URL], + timeout=30) + if rc != 0: + tail = out.splitlines()[-1] if out else "curl failed" + return False, tail + for line in out.splitlines(): + if line.startswith("ip="): + return True, line + return False, "no ip= line in probe response" + + +def cmd_fleet_heal(args): + node = args.node + if node not in VALID_NODES: + print(f"Error: Unknown node '{node}'. Valid: {', '.join(VALID_NODES)}", file=sys.stderr) + sys.exit(1) + as_json = getattr(args, "json", False) + info = get_node_network_info(node) + steps = [] + + def record(step, ok, detail): + steps.append({"step": step, "ok": ok, "detail": detail}) + if not as_json: + mark = c_green("ok") if ok else c_red("FAIL") + print(f" [{mark}] {step}: {detail}") + + if not as_json: + print(f"\n{c_bold('Healing node ' + node)} ({info['netns']}, {info['peer_ip']}:{info['cdp_port']})") + + record("lock", *_heal_lock_step(node)) + record("timer", *_heal_timer_step(node)) + record("tunnel", *_heal_tunnel_step(node)) + egress_ok, egress_detail = _warp_egress_ok(node) + record("egress", egress_ok, egress_detail) + relay = probe_cdp_status(info["peer_ip"], info["cdp_port"]) + if relay["ok"]: + record("relay", True, "CDP answers in %sms" % relay["latency_ms"]) + else: + record("relay", False, relay.get("error", "unreachable")) + + verdict = "RECOVERED" if egress_ok else "DOWN" + if as_json: + print(json.dumps({"ok": egress_ok, "node": node, + "verdict": verdict, "steps": steps}, indent=2)) + else: + print(f"\n Verdict: {c_green(verdict) if egress_ok else c_red(verdict)}") + if not egress_ok: + print(c_dim(" Tunnel still down after restart — if it persists, the Warp identity " + f"likely needs regenerating (human: netvm-new-identity.sh {node})")) + print() + sys.exit(0 if egress_ok else 1) + + +def _unit_state(unit): + """(enabled, active) booleans for a systemd unit.""" + rc_e, _ = _sh(["systemctl", "is-enabled", unit], timeout=15) + rc_a, _ = _sh(["systemctl", "is-active", unit], timeout=15) + return rc_e == 0, rc_a == 0 + + +def cmd_watchdog_status(args): + as_json = getattr(args, "json", False) + try: + import host_evidence + ev = host_evidence.collect() + except Exception: + ev = {} + data = [] + for node in VALID_NODES: + timer = "chromebox-watchdog-%s.timer" % node + enabled, active = _unit_state(timer) + node_ev = ev.get(node, {}) + data.append({ + "node": node, + "timer": timer, + "enabled": enabled, + "active": active, + "browser": node_ev.get("browser", "unknown"), + "browser_detail": node_ev.get("browser_detail", ""), + "cdp": node_ev.get("cdp", "unknown"), + "cdp_detail": node_ev.get("cdp_detail", ""), + }) + relay_enabled, relay_active = _unit_state("cdp-relay-watchdog.timer") + relay = {"timer": "cdp-relay-watchdog.timer", + "enabled": relay_enabled, "active": relay_active} + if as_json: + print(json.dumps({"ok": True, "nodes": data, "relay": relay}, indent=2)) + return + + print("\n" + c_bold("=== WATCHDOG STATUS ===") + c_dim(f" ({datetime.now().strftime('%H:%M:%S')} local)\n")) + headers = ["NODE", "TIMER", "BROWSER", "CDP"] + rows = [] + for item in data: + timer = badge_ok("active") if item["active"] else ( + badge_warn("ENABLED") if item["enabled"] else badge_dim("missing")) + rows.append([c_bold(item["node"]), timer, + item["browser"], item["cdp"]]) + print_table(headers, rows) + rmark = badge_ok("active") if relay["active"] else badge_dim("missing") + print(f" relay: {rmark} {c_dim('cdp-relay-watchdog.timer')}\n") + + +def cmd_watchdog_run(args): + target = args.target + as_json = getattr(args, "json", False) + if target == "relay": + unit = "cdp-relay-watchdog.service" + elif target in VALID_NODES: + unit = "chromebox-watchdog@%s.service" % target + else: + print("Error: Specify node or 'relay' (e.g. box watchdog run dev)", file=sys.stderr) + sys.exit(1) + rc, out = _sh(["sudo", "systemctl", "start", unit], timeout=180) + tail = out[-500:] if out else "" + if as_json: + print(json.dumps({"ok": rc == 0, "unit": unit, "detail": tail}, indent=2)) + else: + if rc == 0: + print(c_green(f"Watchdog run triggered: {unit}")) + else: + print(c_red(f"Failed to start {unit}: {tail}")) + sys.exit(0 if rc == 0 else 1) + +# --------------------------------------------------------------------------- +# Domain: APPROVALS +# --------------------------------------------------------------------------- +def cmd_approvals(args): + import approvals + action = getattr(args, "app_action", None) or "check" + node = getattr(args, "node", None) + + if action in ("check", "list", "inspect"): + nodes = [node] if node else VALID_NODES + fleet = approvals.check_fleet_approvals(nodes) + if getattr(args, "json", False): + print(json.dumps({"ok": True, "approvals": fleet}, indent=2)) + return + + print("\n" + c_bold("=== FLEET APPROVALS STATUS ===") + c_dim(f" ({datetime.now().strftime('%H:%M:%S')} local)\n")) + headers = ["NODE", "STATUS", "TARGET / IP", "PURPOSE / DETAILS", "TRUST", "ACTIONS"] + rows = [] + pending_count = 0 + untrusted_count = 0 + input_wait_count = 0 + + for it in fleet: + n = it["node"] + st = it["status"] + if st == "PENDING": + pending_count += 1 + badge = badge_err("PENDING") if not it.get("is_trusted") else badge_warn("PENDING") + target = it.get("target") or it.get("ip") or "-" + purp = (it.get("purpose") or it.get("title") or "-")[:50] + trust = c_green("TRUSTED") if it.get("is_trusted") else c_red("UNTRUSTED") + btns = " ".join([f"[{b}]" for b in it.get("buttons", [])]) + if not it.get("is_trusted"): + untrusted_count += 1 + elif st == "KEY_APPROVAL": + pending_count += 1 + badge = badge_warn("KEY_REQ") + target = it.get("target") or "VM passkey" + purp = (it.get("purpose") or it.get("title") or "-")[:50] + trust = c_cyan("OPERATOR") + btns = " ".join([f"[{b}]" for b in it.get("buttons", ["Allow", "Deny"])]) + untrusted_count += 1 + elif st == "INPUT_WAIT": + waits = it.get("input_waits") or [] + badge = badge_warn("INPUT") + target = "-" + purp = "; ".join(f"{w.get('task')}: {w.get('status')}" for w in waits)[:60] + trust = "-" + btns = c_dim("answer in task") + input_wait_count += 1 + elif st == "UNREACHABLE": + if it.get("host_cdp_ok") is True: + badge = badge_dim("BLIND") + purp = c_dim("CDP ok on host; blind here") + elif it.get("host_cdp_ok") is False: + badge = badge_err("OFFLINE") + purp = c_dim("CDP down (host agrees)") + else: + badge = badge_dim("OFFLINE") + purp = c_dim("CDP unreachable") + target = "-" + trust = "-" + btns = "-" + elif st == "ERROR": + badge = badge_err("ERROR") + target = "-" + purp = it.get("error", "-")[:40] + trust = "-" + btns = "-" + else: + badge = badge_ok("CLEAR") + target = "-" + purp = c_dim("No pending approvals") + trust = "-" + btns = "-" + + rows.append([c_bold(n), badge, target, purp, trust, btns]) + + print_table(headers, rows) + + if input_wait_count > 0: + print("\n" + c_yellow(f" ⚠ {input_wait_count} node(s) have tasks waiting for human input (not auto-resolvable).")) + if pending_count > 0: + print("\n" + c_yellow(f" ⚠ {pending_count} pending approval(s) detected across fleet.")) + if untrusted_count > 0: + print(c_red(f" ⚠ {untrusted_count} approval(s) require manual review / key grant: 'box approvals allow <node>' or 'box approvals deny <node>'.")) + else: + print(c_cyan(" All pending approvals are for trusted infrastructure. Run 'box approvals auto' to resolve.")) + print() + elif input_wait_count == 0: + unreach = [it for it in fleet if it.get("status") == "UNREACHABLE"] + blind_ok = [it["node"] for it in unreach + if it.get("host_cdp_ok") is True] + blind_unknown = [it["node"] for it in unreach + if it.get("host_cdp_ok") is None] + if blind_ok: + print("\n" + c_dim(" ? %d node(s) blind from this shell " + "(CDP ok on host); queues unverified: %s." + % (len(blind_ok), ", ".join(blind_ok))) + "\n") + if blind_unknown: + print("\n" + c_dim(" ? %d node(s) unreachable, host evidence " + "inconclusive: %s." + % (len(blind_unknown), + ", ".join(blind_unknown))) + "\n") + if not blind_ok and not blind_unknown: + print("\n" + c_green(" āœ” All agent approval queues clear. No agents blocked.") + "\n") + + # Verbose or inspect breakdown + is_verbose = getattr(args, "verbose", False) or action == "inspect" + if is_verbose: + print(c_bold("\n=== DETAILED NODE INSPECTION ===")) + for it in fleet: + n = it["node"] + if it.get("has_pending") or it.get("input_waits") or action == "inspect": + print(f"\n [{c_bold(n)}] Status: {it.get('status')} | Page: {c_cyan(it.get('page_url') or '-')}") + if it.get("has_pending"): + print(f" Egress Target: {it.get('target') or it.get('ip')} ({'TRUSTED' if it.get('is_trusted') else 'UNTRUSTED'})") + print(f" Prompt Title : {it.get('title')}") + if it.get("purpose"): + print(f" Purpose : {it.get('purpose')}") + print(f" Buttons : {', '.join(it.get('buttons') or [])}") + if it.get("input_waits"): + print(f" Tasks Awaiting Human Input ({len(it['input_waits'])}):") + for w in it["input_waits"]: + print(f" • {c_bold(w.get('task'))}") + print(f" Status: {w.get('status')} ({w.get('when')})") + # Correlate with active subagents on this node + try: + import subagent_tracker + active_subs = subagent_tracker.get_active_sessions(n) + if active_subs: + print(f" Active Subagents on Node ({len(active_subs)}):") + for sub in active_subs[-3:]: + s_id = sub.get("session_id", "")[:8] + s_title = sub.get("title") or "subagent" + print(f" • [{s_id}] {c_bold(s_title)} (spawned: {sub.get('spawned_at', '-')})") + except Exception: + pass + print() + + elif action in ("allow", "approve"): + if not node: + print(c_red("Error: Must specify node for allow. e.g. 'box approvals allow 646'"), file=sys.stderr) + sys.exit(1) + always = getattr(args, "always", False) + force = getattr(args, "force", False) + message = getattr(args, "message", None) + allow_main_chat = getattr(args, "allow_main_chat", False) + res = approvals.allow_node_approval(node, always=always, force=force, message=message, + allow_main_chat=allow_main_chat) + if getattr(args, "json", False): + print(json.dumps(res, indent=2)) + return + if res.get("ok"): + if res.get("type") == "key_approval": + notified = "agent notified" if res.get("notified") else "⚠ agent NOT notified (follow up manually)" + print(c_green(f"āœ” Approved operator key request for node '{node}'. Logged to audit trail. {notified}.")) + else: + decision_str = "Always allow this site" if always else "Allow once" + print(c_green(f"āœ” Approved request on node '{node}' ({decision_str}). Dialog dismissed: {res.get('dismissed')}.")) + else: + print(c_red(f"āœ– Failed to approve on node '{node}': {res.get('error')}")) + sys.exit(2 if "Untrusted" in res.get("error", "") else 1) + + elif action == "deny": + if not node: + print(c_red("Error: Must specify node for deny. e.g. 'box approvals deny 646'"), file=sys.stderr) + sys.exit(1) + message = getattr(args, "message", None) + allow_main_chat = getattr(args, "allow_main_chat", False) + res = approvals.deny_node_approval(node, message=message, allow_main_chat=allow_main_chat) + if getattr(args, "json", False): + print(json.dumps(res, indent=2)) + return + if res.get("ok"): + if res.get("type") == "key_approval": + notified = "agent notified" if res.get("notified") else "⚠ agent NOT notified (follow up manually)" + print(c_green(f"āœ” Denied operator key request for node '{node}'. Logged to audit trail. {notified}.")) + else: + print(c_green(f"āœ” Denied request on node '{node}'. Dialog dismissed: {res.get('dismissed')}.")) + else: + print(c_red(f"āœ– Failed to deny on node '{node}': {res.get('error')}")) + sys.exit(1) + + elif action == "auto": + nodes = [node] if node else VALID_NODES + res = approvals.auto_approve_fleet(nodes) + if getattr(args, "json", False): + print(json.dumps(res, indent=2)) + return + approved = res.get("auto_approved", []) + untrusted = res.get("untrusted_pending", []) + if approved: + print("\n" + c_green(f"āœ” Auto-approved {len(approved)} trusted request(s):")) + for a in approved: + print(f" • {c_bold(a['node'])}: {a.get('target_ip')} — {a.get('title')}") + if untrusted: + print("\n" + c_yellow(f"⚠ {len(untrusted)} untrusted request(s) require manual decision:")) + for u in untrusted: + print(f" • {c_bold(u['node'])}: {u.get('ip')} — {u.get('title')} (run: box approvals allow {u['node']} --force)") + if not approved and not untrusted: + print(c_green("āœ” All nodes clear. No approvals pending.")) + print() + if untrusted: + sys.exit(2) + + elif action == "watch": + interval = getattr(args, "interval", 2) + auto_mode = getattr(args, "auto", False) + try: + while True: + sys.stdout.write("\033[2J\033[H") + sys.stdout.flush() + if auto_mode: + auto_res = approvals.auto_approve_fleet(nodes=[node] if node else VALID_NODES) + if auto_res.get("auto_approved"): + for a in auto_res["auto_approved"]: + print(c_green(f"[AUTO-APPROVED] {a['node']}: {a.get('target_ip')}")) + # Print status + setattr(args, "app_action", "check") + cmd_approvals(args) + auto_label = c_cyan(" [AUTO-APPROVE ENABLED]") if auto_mode else "" + print(c_dim(f" [Watching every {interval}s{auto_label}. Press Ctrl+C to exit]")) + time.sleep(interval) + except KeyboardInterrupt: + print("\n" + c_dim("Exited watch mode.")) + + elif action == "reply": + if not node: + print(c_red("Error: Must specify node for reply. e.g. 'box approvals reply pip \"proceed\"'"), file=sys.stderr) + sys.exit(1) + message = getattr(args, "message", "") + allow_main = getattr(args, "allow_main_chat", False) + res = approvals.reply_node_task(node, message, allow_main_chat=allow_main) + if getattr(args, "json", False): + print(json.dumps(res, indent=2)) + return + if res.get("ok"): + print(c_green(f"āœ” Dispatched reply to node '{node}': \"{message}\" (thread: {res.get('page_url')})")) + else: + print(c_red(f"āœ– Failed to reply to node '{node}': {res.get('error')}")) + sys.exit(2 if "Main Chat" in res.get("error", "") else 1) + + elif action == "dismiss": + if not node: + print(c_red("Error: Must specify node for dismiss. e.g. 'box approvals dismiss pip'"), file=sys.stderr) + sys.exit(1) + res = approvals.dismiss_node_task(node) + if getattr(args, "json", False): + print(json.dumps(res, indent=2)) + return + if res.get("ok"): + print(c_green(f"āœ” Dismissed task popup on node '{node}' ({res.get('result')}).")) + else: + print(c_red(f"āœ– Failed to dismiss task popup on node '{node}': {res.get('error')}")) + sys.exit(1) + elif action in ("clear", "clear-all", "clear_all"): + target_node = node + if action in ("clear-all", "clear_all"): + target_node = None + res = approvals.clear_node_waits(target_node) + if getattr(args, "json", False): + print(json.dumps(res, indent=2)) + return + cleared = res.get("total_cleared", 0) + target_desc = f"node '{target_node}'" if target_node else "all fleet nodes" + print(c_green(f"āœ” Cleared {cleared} input wait(s) on {target_desc}.")) + + elif action in ("request-key", "request_key"): + if not node: + print(c_red("Error: Must specify node for request-key. e.g. 'box approvals request-key 646'"), file=sys.stderr) + sys.exit(1) + reason = getattr(args, "reason", "Operator passkey access requested") + caller = os.environ.get("BOX_CALLER") or getattr(args, "from_agent", "super") + res = approvals.request_key_approval(node, reason=reason, caller=caller) + if getattr(args, "json", False): + print(json.dumps(res, indent=2)) + return + print(c_green(f"āœ” Registered passkey approval request for node '{node}'.")) + print(f" Reason: {c_cyan(reason)}") + print(c_dim(f" Operator can approve with: box approvals allow {node}")) + + elif action in ("gates", "gate"): + scope = getattr(args, "scope", None) + if scope: + res = approvals.verify_coordinator_signoff(scope) + if getattr(args, "json", False): + print(json.dumps(res, indent=2)) + return + if res.get("ok"): + print(c_green(f"\nāœ” Coordinator gate for scope '{scope}' is SIGNED-OFF.")) + print(f" Coordinator: {c_cyan(res.get('coordinator'))}") + print(f" Accepted At: {c_dim(res.get('accepted_at'))}") + print(f" Record: {res.get('doc_name')}\n") + else: + print(c_red(f"\nāœ– Coordinator gate verification FAILED for scope '{scope}':")) + print(f" Error: {res.get('error')}\n") + sys.exit(1) + return + + gates = approvals.scan_coordinator_gates() + if getattr(args, "json", False): + print(json.dumps({"ok": True, "gates": gates}, indent=2)) + return + print("\n" + c_bold("=== COORDINATOR GATES & DECISION RECORDS ===\n")) + if not gates: + print(c_dim(" (no coordinator decision records found in docs/)")) + print() + return + headers = ["RECORD", "SCOPE", "STATUS", "COORDINATOR", "ACCEPTED AT", "TARGETS"] + rows = [] + for g in gates: + st = c_green("SIGNED-OFF") if g.get("is_signed_off") else c_yellow(str(g.get("status", "DRAFT")).upper()) + targets = ", ".join(g.get("signoff_targets") or []) if isinstance(g.get("signoff_targets"), list) else str(g.get("signoff_targets") or "-") + rows.append([ + g.get("doc_name", "-"), + c_cyan(g.get("scope", "-")), + st, + g.get("coordinator", "-"), + str(g.get("accepted_at", "-"))[:19], + targets or "-", + ]) + print_table(headers, rows) + print() + +# --------------------------------------------------------------------------- +# Domain: RUNTIME (agentic management of Muse CLI tmux runtimes) +# --------------------------------------------------------------------------- +def cmd_runtime(args): + import shlex + import muse_choice_watcher as mcw + action = getattr(args, "rt_action", None) or "list" + as_json = getattr(args, "json", False) + if getattr(args, "socket", None): + args.socket = mcw.resolve_socket(args.socket) + + if action == "list": + sock = getattr(args, "socket", None) + errors = {} + rows = mcw.all_runtime_rows([sock] if sock else None, errors=errors) + if getattr(args, "muse_only", False): + rows = [r for r in rows if r["is_muse"]] + if as_json: + print(json.dumps({"ok": True, "runtimes": rows, + "errors": errors}, indent=2)) + return + print(c_bold("\n=== MUSE RUNTIMES ===\n")) + if not rows: + print(c_dim(" No panes found.")) + for s, e in errors.items(): + print(c_dim(" %s: %s" % (s, e))) + print() + return + headers = ["SOCKET", "SESSION", "NODE", "PANE", "CMD", + "STATE", "APPROVE", "MODE", "WATCHER"] + table = [] + for r in rows: + state = r["state"] + if r["state"] == "approval-pending" and r["prompt_kind"]: + state = "%s(%s/%s)" % (state, r["prompt_kind"], + r["prompt_key"] or "…") + if r["is_muse"]: + eff_bypass = r.get("effective_bypass") + if eff_bypass is None: + eff_bypass = bool(r["auto_approve"]) + approve = (badge_ok("YES") if eff_bypass + else badge_err("NO")) + mode = (r.get("effective_mode") + or r.get("permission_mode") or "default") + if eff_bypass and mode != "yolo": + mode += "!" + mode = mode[:14] + else: + approve = badge_dim("-") + mode = badge_dim("-") + watcher = (badge_ok("ALIVE %s" % r["watcher_pid"]) + if r["watcher_alive"] else badge_dim("-")) + table.append([os.path.basename(r["socket"]), + "%s:%s" % (r["session"], r["window"]), + r["node"] or badge_dim("-"), + r["pane"], (r["cmd"] or "")[:26], state, + approve, mode, watcher]) + print_table(headers, table) + for s, e in errors.items(): + print(c_dim(" %s: %s" % (s, e))) + print() + + elif action == "send": + sock = getattr(args, "socket", None) or mcw.KNOWN_SOCKETS[0] + pane = args.pane + keys = args.keys + enter = not getattr(args, "no_enter", False) + pre = mcw.pane_state(sock, pane) + if pre.get("error"): + if as_json: + print(json.dumps({"ok": False, "error": pre["error"], + "socket": sock, "pane": pane, + "detail": pre.get("detail")})) + return + if pre["error"] == "socket_unreachable": + print(c_red("Error: cannot reach socket %s: %s" + % (sock, pre.get("detail") or "tmux error")), + file=sys.stderr) + else: + print(c_red("Error: no such pane %s on %s" % (pane, sock)), + file=sys.stderr) + sys.exit(1) + # Verified send: keys + Enter in one tmux call arrive as a paste + # burst, which the muse composer takes as a newline instead of a + # submit. send_answer paces literal text and Enter apart (with a + # render check for composer-bound prompts) so the submit lands. + ok, detail = mcw.send_answer(sock, pane, keys, enter=enter, + kind=pre.get("prompt_kind"), sig=None) + if as_json: + print(json.dumps({ + "ok": ok, "socket": sock, "pane": pane, + "pre_state": pre["state"], + "prompt_kind": pre["prompt_kind"], + "sent": keys, "enter": enter, + "verified": detail["verified"], + "retried": detail["retried"], + "error": None if ok else "tmux error"}, indent=2)) + return + print("\n Pane %s on %s [%s]" % ( + c_bold(pane), sock, c_cyan(pre["state"]))) + if ok: + how = " (verified)" if detail["verified"] else "" + print(" %s sent %r%s%s" % (c_green("āœ”"), keys, + " + Enter" if enter else "", how)) + else: + print(" %s send failed: %s" % (c_red("✘"), "tmux error")) + sys.exit(1) + print() + + elif action == "open": + sock = getattr(args, "socket", None) or mcw.KNOWN_SOCKETS[0] + session = getattr(args, "session", None) + dry_run = getattr(args, "dry_run", False) + + def _fail_open(msg, candidates=None): + if as_json: + print(json.dumps({"ok": False, "error": msg, + "socket": sock, "session": session, + "sessions": candidates or []})) + return + print(c_red("Error: %s" % msg), file=sys.stderr) + if candidates: + print(" Sessions on %s: %s" % (sock, ", ".join(candidates)), + file=sys.stderr) + sys.exit(1) + + def _sessions(): + r = mcw._tmux(sock, "list-sessions", "-F", "#{session_name}", + timeout=10) + if r.returncode != 0: + return [] + return [ln.strip() for ln in (r.stdout or "").split("\n") + if ln.strip()] + + if not os.path.exists(sock): + _fail_open("no such socket %s" % sock) + return + if not session: + names = _sessions() + if len(names) == 1: + session = names[0] + else: + _fail_open("no session given and %s on %s" + % ("no sessions found" if not names + else "multiple sessions", sock), names) + return + else: + probe = mcw._tmux(sock, "has-session", "-t", session, + timeout=10) + if probe.returncode != 0: + _fail_open("no such session %s on %s" % (session, sock), + _sessions()) + return + argv = ["tmux", "-S", sock, "attach-session", "-t", session] + if dry_run or as_json: + if as_json: + print(json.dumps({"ok": True, "dry_run": bool(dry_run), + "socket": sock, "session": session, + "argv": argv}, indent=2)) + return + print(c_bold("\n=== RUNTIME OPEN (dry-run) ===\n")) + print(" Socket: %s" % sock) + print(" Session: %s" % c_cyan(session)) + print(" Command: %s" % shlex.join(argv)) + print() + return + try: + os.execvp("tmux", argv) + except OSError as e: + print(c_red("Error: cannot exec tmux: %s" % e), + file=sys.stderr) + sys.exit(1) + + elif action == "launch": + sock = getattr(args, "socket", None) or mcw.FLEET_SOCKETS[0] + # Track box-launched session + try: + box_state_file = NETVM_ROOT / ".state" / "box-launched-sessions.json" + box_state_file.parent.mkdir(parents=True, exist_ok=True) + box_data = {} + if box_state_file.exists(): + box_data = json.loads(box_state_file.read_text()) + box_data[args.session] = {"socket": sock, "launched_at": datetime.now(timezone.utc).isoformat(), "origin": "box-cli"} + box_state_file.write_text(json.dumps(box_data, indent=2)) + except Exception: + pass + session = args.session + window = getattr(args, "window", None) + dry_run = getattr(args, "dry_run", False) + muse_args = list(getattr(args, "muse_args", None) or []) + cmdline, injected = mcw.muse_launch_cmdline(muse_args) + if dry_run: + if as_json: + print(json.dumps({ + "ok": True, "dry_run": True, "socket": sock, + "session": session, "window": window, + "cmdline": cmdline, "injected": injected}, indent=2)) + return + print(c_bold("\n=== RUNTIME LAUNCH (dry-run) ===\n")) + print(" Socket: %s" % sock) + print(" Session: %s" % c_cyan(session)) + print(" Command: %s" % cmdline) + if injected: + print(" %s approval trail injected: %s" % ( + c_green("āœ”"), " ".join(injected))) + else: + print(" %s caller already sets approval flags; " + "nothing injected" % c_dim("•")) + print() + return + exists = mcw._tmux(sock, "has-session", "-t", session, timeout=10) + if exists.returncode == 0: + if as_json: + print(json.dumps({"ok": False, "error": "session_exists", + "socket": sock, "session": session})) + return + print(c_red("Error: session '%s' already exists on %s" + % (session, sock)), file=sys.stderr) + sys.exit(1) + t_args = ["new-session", "-d", "-s", session] + if window: + t_args.extend(["-n", window]) + t_args.append(cmdline) + r = mcw._tmux(sock, *t_args, timeout=15) + if r.returncode != 0: + err = (r.stderr or r.stdout or "").strip() or "tmux error" + if as_json: + print(json.dumps({"ok": False, "error": err, + "socket": sock, "session": session})) + return + print(c_red("Error: launch failed: %s" % err), + file=sys.stderr) + sys.exit(1) + mcw.wait_for_session_pane(sock, session, timeout=10) + rec = mcw.reconcile(sockets=[sock]) + if as_json: + print(json.dumps({"ok": True, "socket": sock, + "session": session, "window": window, + "cmdline": cmdline, "injected": injected, + "reconcile": rec}, indent=2)) + return + print(c_green("\nāœ” Launched '%s' on %s") % (session, sock)) + print(" Command: %s" % c_dim(cmdline)) + for s in rec.get("started") or []: + print(" %s watcher %s" % (badge_ok("STARTED"), c_cyan(s))) + for f in rec.get("failed") or []: + print(" %s watcher %s (retry: box muse-choices reconcile)" + % (badge_err("FAILED"), c_cyan(f))) + print() + + elif action == "layout": + sock = getattr(args, "socket", None) + rows = mcw.all_runtime_rows([sock] if sock else None) + minimum = {"width": mcw.MIN_APPROVAL_WIDTH, + "height": mcw.MIN_APPROVAL_HEIGHT} + if as_json: + print(json.dumps({"ok": True, "runtimes": rows, + "minimum": minimum}, indent=2)) + return + print(c_bold("\n=== RUNTIME LAYOUT ===\n")) + print(c_dim(" Minimum for reliable approvals: %dx%d\n" % ( + minimum["width"], minimum["height"]))) + if not rows: + print(c_dim(" No panes found.")) + print() + return + headers = ["SOCKET", "SESSION", "PANE", "DIMS", "GEO", "CMD"] + table = [] + for r in rows: + dims = ("%dx%d" % (r["width"], r["height"]) + if r["width"] is not None else "-") + if not r["is_muse"]: + geo = badge_dim("-") + elif r["squeezed"]: + geo = badge_err("SQUEEZED") + else: + geo = badge_ok("OK") + table.append([os.path.basename(r["socket"]), + "%s:%s" % (r["session"], r["window"]), + r["pane"], dims, geo, (r["cmd"] or "")[:26]]) + print_table(headers, table) + targets = mcw.spread_targets(rows) + if targets: + print(c_yellow(" %d squeezed runtime(s): %s" % ( + len(targets), ", ".join( + "%s:%s" % (t["session"], t["pane"]) + for t in targets)))) + print(c_dim(" Run `box runtime spread` to break them into " + "own windows.")) + print() + + elif action == "spread": + sock = getattr(args, "socket", None) + session = getattr(args, "session", None) + dry_run = getattr(args, "dry_run", False) + rows = mcw.all_runtime_rows([sock] if sock else None) + if session: + rows = [r for r in rows if r["session"] == session] + targets = mcw.spread_targets(rows) + if dry_run: + if as_json: + print(json.dumps({"ok": True, "dry_run": True, + "targets": targets}, indent=2)) + return + print(c_bold("\n=== RUNTIME SPREAD (dry-run) ===\n")) + if not targets: + print(c_dim(" No squeezed runtimes; nothing to spread.")) + for t in targets: + print(" Would break %s %s:%s (%dx%d) into own " + "window" % (t["socket"], t["session"], t["pane"], + t["width"], t["height"])) + print() + return + spread, failed = [], [] + for t in targets: + name = "spread-%s" % t["pane"] + # -t session: pins the new window to the SOURCE session. + # Without it break-pane follows the ATTACHED session and + # flings panes across sessions (observed live on scratch). + r = mcw._tmux(t["socket"], "break-pane", "-s", t["pane"], + "-t", "%s:" % t["session"], "-n", name, + timeout=15) + if r.returncode == 0: + spread.append("%s:%s" % (t["session"], t["pane"])) + else: + failed.append({ + "pane": "%s:%s" % (t["session"], t["pane"]), + "error": (r.stderr or r.stdout or "").strip() + or "tmux error"}) + if as_json: + print(json.dumps({"ok": not failed, "spread": spread, + "failed": failed}, indent=2)) + return + print(c_bold("\n=== RUNTIME SPREAD ===\n")) + if not targets: + print(c_dim(" No squeezed runtimes; nothing to spread.")) + for s in spread: + print(" %s spread %s (watchers follow pane ids)" % ( + badge_ok("MOVED"), c_cyan(s))) + for f in failed: + print(" %s %s: %s" % ( + badge_err("FAILED"), f["pane"], f["error"])) + print() + + elif action == "reconcile": + import runtime_reconcile as rec + manifest = (getattr(args, "manifest", None) + or str(NETVM_ROOT / "fleet" / "agents.json")) + tasks = getattr(args, "tasks", None) + dry_run = getattr(args, "dry_run", False) + adopt = getattr(args, "adopt", False) + report = rec.run_reconcile(manifest, tasks_dir=tasks, + dry_run=dry_run, adopt=adopt) + if as_json: + print(json.dumps(report, indent=2)) + return + print(c_bold("\n=== RUNTIME RECONCILE%s ===\n" % ( + " (dry-run)" if dry_run else ""))) + if not report["agents"] and not report["errors"]: + print(c_dim(" Manifest declares no agents.")) + for a in report["agents"]: + if a["action"] in ("launched", "briefed", "adopted"): + mark = badge_ok(a["action"].upper()) + elif a["action"] in ("failed",): + mark = badge_err("FAILED") + elif a["action"] in ("launch", "brief"): + mark = c_cyan("WOULD " + a["action"].upper()) + else: + mark = c_dim("• " + a["action"]) + print(" %s %s [%s]: %s" % ( + mark, c_cyan(a["session"]), a["hat"], a["detail"])) + for c in report["claims"]["requeued"]: + print(" %s task %s (%s)" % ( + badge_ok("REQUEUED"), c_cyan(c["task"]), c["reason"])) + for n in report.get("nudges") or []: + print(" %s %s nudge to %s: %s" % ( + badge_ok("NUDGED"), n["kind"], + c_cyan(n["session"]), n["detail"])) + for e in report["claims"]["errors"] + report["errors"]: + print(" %s %s" % (badge_err("ERROR"), e)) + w = report.get("watchers") or {} + if w.get("started"): + print(" %s watchers: %s" % ( + badge_ok("STARTED"), ", ".join(w["started"]))) + print() + if not report["ok"]: + sys.exit(1) + + elif action == "kill": + import runtime_reconcile as rec + sock = getattr(args, "socket", None) or mcw.KNOWN_SOCKETS[0] + session = args.session + err = rec.kill_session(sock, session) + if as_json: + print(json.dumps({"ok": err is None, "socket": sock, + "session": session, + "error": err}, indent=2)) + return + if err is None: + print(c_green("\nāœ” Killed '%s' on %s\n") % (session, sock)) + return + print(c_red("Error: cannot kill '%s' on %s: %s" + % (session, sock, err)), file=sys.stderr) + sys.exit(1) + + elif action == "restart": + import runtime_reconcile as rec + sock = getattr(args, "socket", None) or mcw.KNOWN_SOCKETS[0] + session = args.session + manifest = (getattr(args, "manifest", None) + or str(NETVM_ROOT / "fleet" / "agents.json")) + dry_run = getattr(args, "dry_run", False) + res = rec.restart_agent(manifest, sock, session, dry_run=dry_run) + ok = res["action"] in ("restarted", "restart") + if as_json: + print(json.dumps({"ok": ok, "socket": sock, + "session": session, + "action": res["action"], + "detail": res["detail"]}, indent=2)) + return + if ok: + print(c_green("\nāœ” %s '%s': %s\n") + % ("Restarted" if res["action"] == "restarted" + else "Would restart", session, res["detail"])) + return + print(c_red("Error: cannot restart '%s': %s" + % (session, res["detail"])), file=sys.stderr) + sys.exit(1) + + elif action == "brief": + import runtime_reconcile as rec + sock = getattr(args, "socket", None) or mcw.KNOWN_SOCKETS[0] + session = args.session + manifest = (getattr(args, "manifest", None) + or str(NETVM_ROOT / "fleet" / "agents.json")) + dry_run = getattr(args, "dry_run", False) + res = rec.brief_agent(manifest, sock, session, dry_run=dry_run) + ok = res["action"] in ("briefed", "brief") + if as_json: + print(json.dumps({"ok": ok, "socket": sock, + "session": session, + "action": res["action"], + "detail": res["detail"]}, indent=2)) + return + if ok: + print(c_green("\nāœ” %s '%s': %s\n") + % ("Briefed" if res["action"] == "briefed" + else "Would brief", session, res["detail"])) + return + print(c_red("Error: cannot brief '%s': %s" + % (session, res["detail"])), file=sys.stderr) + sys.exit(1) + + else: + if as_json: + print(json.dumps({"ok": False, + "error": "unknown_action", + "action": action})) + return + print(c_red("Error: unknown runtime action '%s'" % action), + file=sys.stderr) + sys.exit(1) + + +# --------------------------------------------------------------------------- +# Domain: TASKS (agent work queue: pending/claimed/done) +# --------------------------------------------------------------------------- +def _tasks_age(age_s): + if age_s is None: + return "-" + if age_s < 90: + return "%ds" % int(age_s) + if age_s < 5400: + return "%dm" % int(age_s // 60) + if age_s < 172800: + return "%dh" % int(age_s // 3600) + return "%dd" % int(age_s // 86400) + + +def cmd_work(args): + import box_work + action = getattr(args, "work_action", None) + if not action or action == "status": + box_work.cmd_status(args) + elif action == "start": + box_work.cmd_start(args) + elif action == "assign": + box_work.cmd_assign(args) + elif action == "merge": + box_work.cmd_merge(args) + elif action == "check": + box_work.cmd_check(args) + elif action == "heal": + box_work.cmd_heal(args) + elif action == "chats": + box_work.cmd_chats(args) + else: + box_work.cmd_status(args) + + +def cmd_tasks(args): + import runtime_reconcile as rec + action = getattr(args, "tasks_action", None) or "list" + as_json = getattr(args, "json", False) + tasks_dir = (getattr(args, "dir", None) + or str(NETVM_ROOT / "fleet" / "tasks")) + + if action == "list": + queue = getattr(args, "queue", None) or "all" + rows = rec.list_tasks(tasks_dir, queue=queue) + if as_json: + print(json.dumps({"ok": True, "tasks": rows, + "dir": tasks_dir}, indent=2)) + return + print(c_bold("\n=== TASK QUEUE ===\n")) + if not rows: + print(c_dim(" No tasks in %s." % queue)) + print() + return + headers = ["QUEUE", "NAME", "OWNER", "AGE"] + table = [[r["queue"], r["name"][:44], + r["owner"] or badge_dim("-"), + _tasks_age(r["age_s"])] for r in rows] + print_table(headers, table) + print() + + elif action == "show": + name = args.name + res = rec.read_task(tasks_dir, name) + if as_json: + print(json.dumps({"ok": "error" not in res, + "dir": tasks_dir, **res}, indent=2)) + return + if "error" in res: + print(c_red("Error: %s" % res["error"]), file=sys.stderr) + sys.exit(1) + print(c_bold("\n=== TASK %s [%s] ===\n" % ( + res["name"], res["queue"]))) + print(res["text"].rstrip("\n")) + print() + + elif action == "create": + res = rec.create_task( + tasks_dir, args.name, getattr(args, "title", ""), + getattr(args, "goal", ""), getattr(args, "steps", "") or "", + dry_run=getattr(args, "dry_run", False)) + if as_json: + print(json.dumps({"ok": res["ok"], "dir": tasks_dir, + **{k: v for k, v in res.items() + if k != "ok"}}, indent=2)) + return + if not res["ok"]: + print(c_red("Error: %s" % res["error"]), file=sys.stderr) + sys.exit(1) + print(c_green("\nāœ” %s %s\n" % ( + "Would create" if res.get("dry_run") else "Created", + res["path"]))) + + elif action == "claim": + res = rec.claim_task(tasks_dir, args.name, args.owner, + dry_run=getattr(args, "dry_run", False)) + if as_json: + print(json.dumps({"ok": res["ok"], "dir": tasks_dir, + **{k: v for k, v in res.items() + if k != "ok"}}, indent=2)) + return + if not res["ok"]: + print(c_red("Error: %s" % res["error"]), file=sys.stderr) + sys.exit(1) + print(c_green("\nāœ” %s %s\n" % ( + "Would claim" if res.get("dry_run") else "Claimed", + res["path"]))) + + elif action == "done": + res = rec.complete_task(tasks_dir, args.name, + getattr(args, "result", "") or "", + dry_run=getattr(args, "dry_run", False)) + if as_json: + print(json.dumps({"ok": res["ok"], "dir": tasks_dir, + **{k: v for k, v in res.items() + if k != "ok"}}, indent=2)) + return + if not res["ok"]: + print(c_red("Error: %s" % res["error"]), file=sys.stderr) + sys.exit(1) + print(c_green("\nāœ” %s %s\n" % ( + "Would complete" if res.get("dry_run") else "Completed", + res["path"]))) + + elif action == "requeue": + res = rec.requeue_task(tasks_dir, args.name, + dry_run=getattr(args, "dry_run", False)) + if as_json: + print(json.dumps({"ok": res["ok"], "dir": tasks_dir, + **{k: v for k, v in res.items() + if k != "ok"}}, indent=2)) + return + if not res["ok"]: + print(c_red("Error: %s" % res["error"]), file=sys.stderr) + sys.exit(1) + print(c_green("\nāœ” %s %s\n" % ( + "Would requeue" if res.get("dry_run") else "Requeued", + res["path"]))) + + elif action == "sweep": + manifest = (getattr(args, "manifest", None) + or str(NETVM_ROOT / "fleet" / "agents.json")) + dry_run = getattr(args, "dry_run", False) + res = rec.sweep_now(manifest, tasks_dir=tasks_dir, + dry_run=dry_run) + if as_json: + print(json.dumps({"ok": res["ok"], "dir": tasks_dir, + "dry_run": dry_run, + "live_sessions": res["live_sessions"], + "requeued": res["requeued"], + "errors": res["errors"]}, indent=2)) + return + print(c_bold("\n=== TASK SWEEP%s ===\n" % ( + " (dry-run)" if dry_run else ""))) + if not res["requeued"] and not res["errors"]: + print(c_dim(" No stale claims.")) + for c in res["requeued"]: + print(" %s task %s (%s)" % ( + badge_ok("REQUEUED"), c_cyan(c["task"]), c["reason"])) + for e in res["errors"]: + print(" %s %s" % (badge_err("ERROR"), e)) + print() + if not res["ok"]: + sys.exit(1) + + else: + if as_json: + print(json.dumps({"ok": False, + "error": "unknown_action", + "action": action})) + return + print(c_red("Error: unknown tasks action '%s'" % action), + file=sys.stderr) + sys.exit(1) + + +# --------------------------------------------------------------------------- +# Domain: MUSE-CHOICES (Muse TUI A/B/C auto-answer daemon) +# --------------------------------------------------------------------------- +def cmd_muse_choices(args): + import muse_choice_watcher as mcw + action = getattr(args, "mc_action", None) or "status" + as_json = getattr(args, "json", False) + if getattr(args, "socket", None): + args.socket = mcw.resolve_socket(args.socket) + caller = os.environ.get("BOX_CALLER") or getattr(args, "from_agent", None) or "super" + + if action == "on": + dry_run = getattr(args, "dry_run", False) + state = mcw.set_enabled(True, dry_run=dry_run, by=caller) + res = mcw.reconcile() + if as_json: + print(json.dumps({"ok": True, "desired": state, "reconcile": res}, indent=2)) + return + mode = "DRY-RUN (logging only)" if dry_run else c_yellow("LIVE (typing A into Muse panes)") + print(c_green("\nāœ” Muse choice auto-answers ON.") + f" [{mode}]\n") + for s in res["started"]: + print(f" {badge_ok('STARTED')} {c_cyan(s)}") + for s in res["already"]: + print(f" {badge_dim('RUNNING')} {s}") + for s in res.get("failed") or []: + print(f" {badge_err('FAILED')} {s}") + if not res["started"] and not res["already"] and not res.get("failed"): + print(c_dim(" No Muse panes found on known tmux sockets.")) + print() + + elif action == "off": + state = mcw.set_enabled(False, by=caller) + stopped = mcw.stop_all() + if as_json: + print(json.dumps({"ok": True, "desired": state, "stopped": stopped}, indent=2)) + return + print(c_green("\nāœ” Muse choice auto-answers OFF.")) + for r in stopped: + if r.get("status") == "stopped-orphan": + label = "(orphan %s:%s)" % (r.get("socket"), r.get("pane")) + else: + label = r.get("pidfile") + print(f" {badge_dim('STOPPED')} {label} (pid {r.get('pid')})") + if not stopped: + print(c_dim(" No watchers were running.")) + print() + + elif action == "reconcile": + res = mcw.reconcile() + if as_json: + print(json.dumps({"ok": True, "reconcile": res}, indent=2)) + return + print(f"\n Enabled: {c_bold(str(res['enabled']))} Dry-run: {res['dry_run']}") + for s in res["started"]: + print(f" {badge_ok('STARTED')} {c_cyan(s)}") + for s in res["stopped"]: + print(f" {badge_dim('STOPPED')} {s}") + for s in res["pruned"]: + print(f" {badge_dim('PRUNED')} {s}") + for s in res.get("failed") or []: + print(f" {badge_err('FAILED')} {s}") + if not res["started"] and not res["stopped"] and not res["pruned"] and not res.get("failed"): + print(c_dim(" Already in desired state.")) + print() + + elif action == "logs": + sock = getattr(args, "socket", None) + pane = getattr(args, "pane", None) + n = getattr(args, "n", 20) + path = mcw.logfile_for(sock, pane) + try: + with open(path) as f: + lines = f.readlines() + except OSError: + print(c_red(f"Error: no log at {path}"), file=sys.stderr) + sys.exit(1) + if as_json: + recs = [] + for line in lines[-n:]: + try: + recs.append(json.loads(line)) + except Exception: + recs.append({"raw": line.rstrip("\n")}) + print(json.dumps({"ok": True, "log": path, "records": recs}, indent=2)) + return + print(c_bold(f"\n=== {path} (last {n}) ===\n")) + for line in lines[-n:]: + print(line.rstrip("\n")) + print() + + elif action == "resolve": + sock = getattr(args, "socket", None) + pane = getattr(args, "pane", None) + decision = getattr(args, "decision", None) + hf = mcw.read_hold(sock, pane) + if not hf: + if as_json: + print(json.dumps({"ok": True, "held": None, + "note": "nothing held"}, indent=2)) + return + print(c_dim("\n Nothing held on %s:%s.\n" % (sock, pane))) + return + if decision == "deny" and hf.get("kind") not in mcw.NEGATIVE_KEYS: + msg = ("refusing deny on %s prompt (D2: questions always " + "resolve top-choice); use approve or wait for expiry" + % (hf.get("kind") or "?")) + if as_json: + print(json.dumps({"ok": False, "reason": msg}, indent=2)) + return + print(c_red("\n Error: " + msg + "\n"), file=sys.stderr) + sys.exit(1) + hf["directive"] = decision + mcw.write_hold(sock, pane, hf) + mcw.audit("muse-choice-resolved", + name="%s:%s" % (os.path.basename(sock), pane), + caller=caller, + extra={"socket": sock, "pane": pane, + "decision": decision, "sig": hf.get("sig")}) + if as_json: + print(json.dumps({"ok": True, "resolved": decision, + "pane": pane}, indent=2)) + return + print(c_green("\nāœ” Hold on %s:%s will %s within a poll." + % (sock, pane, + "approve" if decision == "approve" else "deny"))) + print() + + else: # status + desired = mcw.get_desired() + watchers = mcw.status_all() + answers = mcw.recent_answers(5) + held = mcw.list_holds() + if as_json: + print(json.dumps({"ok": True, "desired": desired, + "watchers": watchers, "held": held, + "recent_answers": answers}, indent=2)) + return + state_badge = badge_ok("ON") if desired["enabled"] else badge_dim("OFF") + dry = " (dry-run)" if desired.get("dry_run") else "" + print("\n" + c_bold("=== MUSE CHOICE AUTO-ANSWERS ===") + f" {state_badge}{dry}") + upd = desired.get("updated_at") or "-" + by = desired.get("updated_by") or "-" + print(c_dim(f" Desired state: enabled={desired['enabled']} dry_run={desired.get('dry_run')} updated={upd} by={by}\n")) + if watchers: + headers = ["WATCHER", "STATUS", "PID", "LOG BYTES"] + rows = [] + for w in watchers: + if w.get("orphan"): + badge = badge_warn("ORPHAN") + name = "(orphan %s:%s)" % (w.get("socket"), w.get("pane")) + else: + badge = badge_ok("ALIVE") if w["alive"] else badge_err("DEAD") + name = w["pidfile"] + rows.append([name, badge, str(w["pid"] or "-"), + str(w["log_bytes"] if w["log_bytes"] is not None else "-")]) + print_table(headers, rows) + else: + print(c_dim(" No watcher state files.")) + if held: + print(c_bold("\n Held prompts (resolve via: box muse-choices resolve --socket S --pane P approve|deny):")) + for h in held: + try: + left = max(0, int(float(h.get("held_until", 0)) - time.time())) + except (TypeError, ValueError): + left = -1 + if mcw.hold_renews_forever(h, h.get("kind")): + when = "gate (renews, never auto-approves)" + elif left >= 0: + when = f"expires in {left}s" + else: + when = "expiry unknown" + print(f" • {badge_warn('HELD')} {c_bold(h.get('pane', '?'))} " + f"{h.get('kind')}/{h.get('key')} rule={h.get('rule')} " + f"{when}") + print(f" {c_dim((h.get('reason') or '')[:100])}") + print(f" {c_dim((h.get('text') or '')[:100])}") + if answers: + print(c_bold("\n Recent answers (box audit feed):")) + for a in answers: + opts = "; ".join(a.get("options") or [])[:80] + kind = a.get("kind") or "-" + key = a.get("key") or "-" + print(f" • {c_dim(a.get('ts', ''))} {c_bold(a.get('name', ''))} {kind}/{key} sig={a.get('sig')} ok={a.get('ok')}") + print(f" {c_dim(opts)}") + else: + print(c_dim("\n No answers recorded in box audit feed.")) + print() + +def resolve_sender(args) -> str: + explicit = getattr(args, "from_agent", None) + if explicit and explicit != DEFAULT_SENDER: + return explicit + caller_env = os.environ.get("BOX_CALLER") + if caller_env and caller_env in ("muse", "pip", "646", "opm", "super"): + return caller_env + return explicit or DEFAULT_SENDER + +def sign_message(sender: str, message: str, key_path: Path = None, msg_id: str = None) -> tuple: + """Signs message with SSH key (namespace 'dm') using ssh-keygen -Y sign. + Returns (signed_raw_wire_format, msg_id).""" + import tempfile + key_path = key_path or Path.home() / ".ssh" / "id_ed25519" + if not key_path.exists(): + raise FileNotFoundError(f"Signing key not found: {key_path}") + + mid = msg_id or hashlib.sha256(f"{sender}-{time.time()}-{os.urandom(8).hex()}".encode()).hexdigest()[:8] + payload = f"[from:{sender}] [id:{mid}]\n\n{message}" + + with tempfile.TemporaryDirectory() as td: + payf = Path(td) / "payload" + with open(payf, "w") as f: + f.write(payload) + + res = subprocess.run( + ["ssh-keygen", "-Y", "sign", "-f", str(key_path), "-n", "dm", str(payf)], + capture_output=True, text=True, timeout=10 + ) + if res.returncode != 0: + raise RuntimeError(f"ssh-keygen sign failed: {res.stderr}") + + sigf = Path(td) / "payload.sig" + with open(sigf, "r") as f: + sigblock = f.read().strip() + + signed_wire = f"[from:{sender}] [id:{mid}]\n\n{message}\n\n{sigblock}" + return signed_wire, mid + +# --------------------------------------------------------------------------- +# Domain: INVITE +# --------------------------------------------------------------------------- +def cmd_invite(args): + import invite + from invite_handler import salvage_blocked_node + action = getattr(args, "invite_action", None) or "status" + node = getattr(args, "node", None) + as_json = getattr(args, "json", False) + + if action in ("status", "list"): + nodes = [node] if node else VALID_NODES + res = invite.fleet_invite_status(nodes) + if as_json: + print(json.dumps({"ok": True, "agents": res}, indent=2)) + return + print("\n" + c_bold("=== FLEET INVITE STATUS ===") + "\n") + headers = ["NODE", "CODE", "REDEEMED", "USES LEFT", "USED", "REWARD"] + rows = [] + for n in nodes: + it = res.get(n, {}) + if not it.get("ok"): + rows.append([c_bold(n), c_red("ERROR"), "-", "-", "-", + (it.get("error") or "")[:40]]) + continue + rows.append([c_bold(n), c_cyan(it.get("code") or "-"), + "yes" if it.get("has_redeemed") else c_green("no"), + str(it.get("uses_remaining") + if it.get("uses_remaining") is not None else "-"), + str(it.get("use_count") + if it.get("use_count") is not None else "-"), + ((it.get("reward") or {}).get("title") or "-")[:40]]) + print_table(headers, rows) + print() + elif action in ("code", "find"): + try: + res = invite.get_invite(node) + except invite.InviteError as e: + print(c_red(f" Error: {e}"), file=sys.stderr) + sys.exit(2) + if as_json: + print(json.dumps(res, indent=2)) + elif res.get("ok"): + print(f" {c_bold(node)} invite code: {c_cyan(res['code'])}") + if res.get("uses_remaining") is not None: + print(f" Uses remaining: {res['uses_remaining']}") + print(f" Source: {res.get('source', 'api')}") + else: + print(c_red(f" Error: {res.get('error')}"), file=sys.stderr) + sys.exit(1) + elif action == "redeem": + code = getattr(args, "code", None) + method = getattr(args, "method", "auto") + notify_target = getattr(args, "notify", None) or getattr(args, "notify_target", None) + notify_agent = getattr(args, "notify_agent", None) + try: + if method != "api" or notify_target or notify_agent: + from invite_handler import InviteHandler + with InviteHandler(node) as h: + res_obj = h.redeem_code(code, method=method, notify_target=notify_target, notify_agent=notify_agent) + res = res_obj.to_dict() + res["ok"] = res_obj.success + else: + res = invite.redeem_invite(node, code) + except invite.InviteError as e: + print(c_red(f" Error: {e}"), file=sys.stderr) + sys.exit(2) + except Exception as e: + print(c_red(f" Error during redemption: {e}"), file=sys.stderr) + sys.exit(1) + if as_json: + print(json.dumps(res, indent=2)) + elif res.get("ok"): + msg = f" āœ” Redeemed {res.get('code') or code} on {node}." + if res.get("detail"): + msg += f" {res['detail']}" + print(c_green(msg)) + else: + reason_str = res.get("reason") or res.get("error") or "failed" + print(c_red(f" ✘ Redeem failed on {node}: {reason_str}"), file=sys.stderr) + if res.get("detail"): + print(c_dim(f" Detail: {res['detail']}"), file=sys.stderr) + if res.get("field_missing"): + print(c_yellow(f" Note: Expected redemption input field was missing or hidden on @{node}."), file=sys.stderr) + if res.get("loopback_notified"): + print(c_cyan(f" Loopback: Dispatched alert to {notify_agent or 'coordinator'}/{notify_target}."), file=sys.stderr) + sys.exit(1) + elif action == "salvage": + target = getattr(args, "node", "646") or "646" + notify_target = getattr(args, "notify", None) or getattr(args, "notify_target", None) + notify_agent = getattr(args, "notify_agent", None) + res = salvage_blocked_node( + target, + helper_node=getattr(args, "helper", None), + notify_target=notify_target, + notify_agent=notify_agent, + ) + if as_json: + print(json.dumps(res, indent=2)) + return + print(f"\n{c_bold('=== SALVAGE REPORT FOR ' + target.upper() + ' ===')}") + code = res.get("invite_code") or res.get("code_redeemed") + print(f" Invite Code to Credit: {c_cyan(str(code))}") + if res.get("success"): + print(c_green(f" Status: SUCCESS! Redeemed on {res.get('helper_node')}. 1 billion tokens credited to {target}!")) + else: + diag = res.get("error") or (res.get("redemption_result") or {}).get("detail") or "Redemption failed on helper peer" + print(c_yellow(f" Diagnosis: {diag}")) + action_hint = res.get("share_instruction") or f"Redeem code '{code}' via onboarding pipeline to grant 1B tokens to {target}." + print(f" Action: {action_hint}") + print(f" Command: {c_cyan('box onboard start <new_node> --email <client_email> --for ' + target)}") + if res.get("loopback_notified"): + print(c_cyan(f" Loopback: Notice dispatched to {notify_agent or 'coordinator'}/{notify_target}.")) + print() + + +# --------------------------------------------------------------------------- +# Domain: USAGE +# --------------------------------------------------------------------------- +def cmd_usage(args): + import invite + node = getattr(args, "node", None) or getattr(args, "node_pos", None) + as_json = getattr(args, "json", False) + nodes = [node] if node else VALID_NODES + res = invite.fleet_usage(nodes) + if as_json: + print(json.dumps({"ok": True, "agents": res}, indent=2)) + return + print("\n" + c_bold("=== FLEET USAGE LIMITS ===") + "\n") + headers = ["NODE", "WEEKLY RESET", "WEEKLY USED", "ADDITIONAL", + "ADD LEFT", "REDEEMED"] + rows = [] + has_blocked = False + for n in nodes: + it = res.get(n, {}) + if not it.get("ok"): + rows.append([c_bold(n), c_red("ERROR"), "-", "-", + (it.get("error") or "")[:30], "-"]) + continue + if not it.get("stats_loaded", True): + rows.append([c_bold(n), c_yellow("UNLOADED"), "-", "-", + "Stats did not render", "-"]) + continue + wu_val = it.get("weekly_used_pct") + wu = ("%d%%" % wu_val) if wu_val is not None else "-" + au_val = it.get("additional_used_pct") + au = ("%d%% used" % au_val) if au_val is not None else "-" + add_left = it.get("additional_left") or "-" + redeemed_val = it.get("has_redeemed") + redeemed_str = "yes" if redeemed_val else (c_green("no") if redeemed_val is False else "-") + if (wu_val is not None and wu_val >= 100 and au_val is not None and au_val >= 100) or ("0 tokens left" in add_left): + has_blocked = True + n_disp = c_red(f"ā— {n}") + else: + n_disp = c_bold(n) + rows.append([n_disp, it.get("weekly_reset") or "-", + wu, "%s / %s" % (it.get("additional_expires") or "-", + au), + add_left, redeemed_str]) + print_table(headers, rows) + if has_blocked: + print("\n" + c_yellow(" ⚠ One or more agents have reached usage limits. Run 'box invite salvage <node>' to resolve.") + "\n") + else: + + print() + + +# --------------------------------------------------------------------------- +# Domain: SETTINGS +# --------------------------------------------------------------------------- +def cmd_settings(args): + from settings_rpa import SettingsRPA + action = getattr(args, "settings_action", "usage") or "usage" + node = getattr(args, "node", "646") or "646" + as_json = getattr(args, "json", False) + + with SettingsRPA(node) as rpa: + if action == "usage": + usage = rpa.read_usage() + if as_json: + print(json.dumps(usage.to_dict(), indent=2)) + return + status_badge = badge_err("BLOCKED") if usage.is_blocked else badge_ok("ACTIVE") + redeemed_badge = c_green("No (Eligible to redeem)") if not usage.has_redeemed else "Yes (Already redeemed)" + print(f"\n{c_bold('=== Settings RPA Usage: ' + node + ' ===')}") + print(f" Plan: {usage.plan}") + print(f" Weekly Limit: {usage.weekly_reset_text} ({c_bold(str(usage.weekly_percent_used) + '%')} used)") + print(f" Extra Tokens: {usage.extra_tokens_remaining} ({c_bold(str(usage.extra_percent_used) + '%')} used)") + print(f" Has Redeemed: {redeemed_badge}") + print(f" Status: {status_badge}\n") + elif action == "check-redeem": + info = rpa.check_redeem_entrypoint() + print(json.dumps(info, indent=2)) + + +# --------------------------------------------------------------------------- +# Domain: KPI (Spend Monitoring, Performance & Runtime Preservation) +# --------------------------------------------------------------------------- +def cmd_kpi(args): + import kpi + act = getattr(args, "kpi_action", "status") or "status" + as_json = getattr(args, "json", False) + + if act == "status": + nodes = [args.node] if getattr(args, "node", None) else kpi.VALID_NODES + kpis = kpi.fleet_kpi(nodes) + if as_json: + print(json.dumps({k: v.to_dict() for k, v in kpis.items()}, indent=2)) + return + print("\n" + c_bold("=== NETVM FLEET KPI & RUNTIME PRESERVATION DASHBOARD ===") + "\n") + header = f"{'NODE':<6} {'QUOTA':<10} {'CALLS':<12} {'JOBS':<10} {'SUBAGENTS':<11} {'TMUX':<6} {'UPTIME':<8} {'ROUTE':<9} {'EFFICIENCY':<15}" + sep = f"{'────':<6} {'─────────':<10} {'───────────':<12} {'─────────':<10} {'──────────':<11} {'────':<6} {'──────':<8} {'───────':<9} {'──────────────':<15}" + print(c_bold(header)) + print(sep) + for n in nodes: + k = kpis.get(n) + if not k: + continue + q_str = f"{k.weekly_used_pct}%" if k.weekly_used_pct is not None else "Active" + c_str = f"{k.calls_sent} ({k.calls_verified}v)" + j_str = f"{k.jobs_completed}/{k.jobs_assigned}" + sub_str = str(k.subagents_active) + tmux_str = str(k.tmux_workers_active) + up_str = f"{k.uptime_hours}h" + print(f"{c_bold(k.node):<15} {q_str:<10} {c_str:<12} {j_str:<10} {sub_str:<11} {tmux_str:<6} {up_str:<8} {k.route_status:<9} {k.efficiency_rating:<15}") + print("\n" + c_dim("Run 'box kpi report <node>' for prescriptive runtime preservation advisories.\n")) + elif act == "report": + node = getattr(args, "node", "646") or "646" + res = kpi.get_agent_kpi(node) + if as_json: + print(json.dumps(res.to_dict(), indent=2)) + return + print(f"\n{c_bold('=== KPI & RUNTIME REPORT: @' + res.node.upper() + ' ===')}") + print(f" Weekly Quota: {res.weekly_used_pct}% used") + print(f" Extra Tokens: {res.extra_tokens_remaining}") + print(f" Blocked Status: {'YES (LIMIT REACHED)' if res.is_blocked else 'NO (HEALTHY)'}") + print(f" Messages / Calls: {res.calls_sent} sent ({res.calls_verified} verified delivered)") + print(f" Jobs Dispatched: {res.jobs_completed} completed / {res.jobs_assigned} assigned") + print(f" Active Subagents: {res.subagents_active}") + print(f" Active Tmux Workers:{res.tmux_workers_active}") + print(f" Process Uptime: {res.uptime_hours} hours") + print(f" Route Health: {res.route_status}") + print(f" Efficiency Index: {res.efficiency_index} ({res.efficiency_rating})") + print(f"\n {c_yellow('[RUNTIME PRESERVATION ADVISORY]')}\n {res.preservation_advisory}\n") + elif act == "routes": + routes = {n: kpi.check_node_routes(n) for n in kpi.VALID_NODES} + if as_json: + print(json.dumps(routes, indent=2)) + else: + print("\n" + c_bold("=== NETVM ROUTE HEALTH ===")) + for n, st in routes.items(): + print(f" @{c_bold(n):<15} : {st}") + print() + elif act == "spawn-worker": + res = kpi.spawn_tmux_worker(args.node, args.session, args.worker_command) + if as_json: + print(json.dumps(res, indent=2)) + else: + if res.get("ok"): + print(c_green(f"āœ” {res.get('message')}")) + else: + print(c_red(f"✘ Failed to spawn worker: {res.get('error')}"), file=sys.stderr) + sys.exit(1) + elif act == "auto-spawn": + nodes = [args.node] if getattr(args, "node", None) else None + results = kpi.auto_spawn_workers(nodes=nodes, dry_run=getattr(args, "dry_run", False)) + if as_json: + print(json.dumps(results, indent=2)) + return + dry_label = c_yellow(" (DRY-RUN)") if getattr(args, "dry_run", False) else "" + print("\n" + c_bold(f"=== AUTO-SPAWN WORKER RECONCILIATION{dry_label} ===")) + for r in results: + n = r.get("node") + action = r.get("action") + if action == "spawned": + print(c_green(f" āœ” @{n:<5} : SPAWNED session '{r.get('session')}' ({r.get('work_type')}: {r.get('work_name')})")) + elif action == "would_spawn": + print(c_cyan(f" ? @{n:<5} : WOULD SPAWN session '{r.get('session')}' ({r.get('work_type')}: {r.get('work_name')})")) + elif action == "skip": + print(c_dim(f" - @{n:<5} : SKIP ({r.get('reason')})")) + elif action == "idle": + print(c_dim(f" - @{n:<5} : IDLE ({r.get('reason')})")) + elif action == "error": + print(c_red(f" ✘ @{n:<5} : ERROR ({r.get('error')})")) + print() + + +# --------------------------------------------------------------------------- +# Domain: ONBOARD (Agent-driven Onboarding & Invite Salvage Pipeline) +# --------------------------------------------------------------------------- + +def cmd_onboard(args): + import onboard_pipeline + action = getattr(args, "onboard_action", "start") or "start" + as_json = getattr(args, "json", False) + + if action == "start": + node = args.node + email = args.email + for_agent = getattr(args, "for_agent", None) + code = getattr(args, "code", None) + acct_name = getattr(args, "account_name", None) + + res = onboard_pipeline.start_onboarding(node, email, beneficiary_node=for_agent, invite_code=code, account_name=acct_name) + if as_json: + print(json.dumps(res, indent=2)) + return + if res.get("ok"): + print(f"\n{c_bold('=== ONBOARDING PIPELINE STARTED: ' + node.upper() + ' ===')}") + print(f" Target Node: {c_bold(node)}") + print(f" Client Email: {email}") + print(f" Beneficiary: @{res.get('beneficiary_node')} ({c_cyan(str(res.get('invite_code_queued')))})") + print(f" Status: {c_yellow(res.get('status', 'in_progress').upper())}") + print(f" Next Step: {res.get('message')}\n") + else: + print(c_red(f"\n✘ Onboarding failed: {res.get('error')}\n"), file=sys.stderr) + sys.exit(1) + + elif action == "submit-otp": + node = args.node + otp = args.otp + email = getattr(args, "email", None) + + res = onboard_pipeline.submit_onboarding_otp(node, otp, email=email) + if as_json: + print(json.dumps(res, indent=2)) + return + if res.get("ok"): + print(f"\n{c_green('=== ONBOARDING & REDEMPTION COMPLETE ===')}") + print(f" Node: {c_bold(node)}") + print(f" Beneficiary: @{res.get('beneficiary_node')}") + print(f" Token Grant: +1,000,000,000 Muse tokens each") + print(f" Message: {c_green(res.get('message'))}\n") + else: + print(c_red(f"\n✘ OTP submission failed: {res.get('error')}\n"), file=sys.stderr) + sys.exit(1) + + elif action == "status": + node = args.node + state = onboard_pipeline.OnboardState.load(node) + if not state: + print(c_red(f"No onboarding record found for node '{node}'"), file=sys.stderr) + sys.exit(1) + if as_json: + print(json.dumps(onboard_pipeline.asdict(state), indent=2)) + return + print(f"\n{c_bold('=== ONBOARDING STATUS: @' + state.node.upper() + ' ===')}") + print(f" Email: {state.email}") + print(f" Current Stage: {c_cyan(state.stage)}") + print(f" Beneficiary Agent:@{state.beneficiary_node or '-'} (Code: {c_cyan(str(state.invite_code or '-'))})") + if state.detail: + print(f" Detail: {state.detail}") + if state.redemption_result: + rr = state.redemption_result + r_st = c_green("SUCCESS") if rr.get("ok") else c_red("FAILED") + print(f" Redemption: {r_st}") + print() + + elif action == "connects": + conn_list = onboard_pipeline.get_all_connects() + if as_json: + print(json.dumps(conn_list, indent=2)) + return + print(f"\n{c_bold('=== ACTIVE FLEET & CLIENT ONBOARD CONNECTS ===')}") + print(f" {'NODE':<8} {'TYPE':<16} {'STAGE / STATUS':<18} {'CDP':<8} {'INVITE':<10} {'ROLE / DETAIL'}") + print(" " + "─" * 78) + for c in conn_list: + node = c.get("node", "") + t_str = c.get("type", "") + st_str = c.get("stage", c.get("status", "")) + cdp = str(c.get("cdp_port") or "-") + code = c.get("invite_code") or "-" + role = c.get("role") or c.get("detail") or c.get("email") or "" + print(f" {node:<8} {t_str:<16} {st_str:<18} {cdp:<8} {code:<10} {role}") + print() + + elif action == "salvage-wo": + node = getattr(args, "node", "646") or "646" + target_chat = getattr(args, "target", "646 tasks") or "646 tasks" + res = onboard_pipeline.issue_salvage_work_order(node, to_sidechat=target_chat) + if as_json: + print(json.dumps(res, indent=2)) + return + if res.get("ok"): + print(c_green(f"\nāœ” Cryptographically signed salvage work order dispatched for @{node} -> {target_chat}\n")) + else: + print(c_red(f"\n✘ Failed to dispatch work order: {res}\n"), file=sys.stderr) + sys.exit(1) + + elif action == "feed-matrix": + matrix = onboard_pipeline.calculate_feeding_weights() + if as_json: + print(json.dumps({"ok": True, "feed_matrix": matrix}, indent=2)) + return + print("\n" + c_bold("=== FLEET FEEDING & TOKEN ALLOCATION MATRIX ===")) + print(c_dim(" (Ranked by: Urgency + Work Done Over Time + Job Amount + Role Criticality)\n")) + headers = ["RANK", "AGENT", "ROLE", "FEED SCORE", "STATUS", "JOBS", "WORK DONE", "CODE"] + rows = [] + for idx, r in enumerate(matrix, 1): + st = c_red("BLOCKED") if r["is_blocked"] else (c_yellow("LIMIT") if r["status"] == "LIMIT_REACHED" else c_green("ACTIVE")) + rows.append([ + f"#{idx}", + c_bold(r["node"]), + r["role"], + c_cyan(str(r["feeding_weight"]) + " pts"), + st, + str(r["jobs_count"]), + f"{r['work_done_msgs']} msgs", + c_bold(r["code"]), + ]) + print_table(headers, rows) + print() + + +# --------------------------------------------------------------------------- +# Domain: CHROMEBOX (agent browser settings-menu toggles) +# --------------------------------------------------------------------------- +def cmd_chromebox(args): + from hatch_menu import toggles as menu_toggles + action = getattr(args, "chrome_action", None) + if action != "permissions": + print(c_red(" Error: expected 'permissions' (box chromebox " + "permissions <node> list|get|set|describe)"), + file=sys.stderr) + sys.exit(2) + node = getattr(args, "node", None) + as_json = getattr(args, "json", False) + perm_action = getattr(args, "perm_action", None) or "list" + + if perm_action == "list": + tab = getattr(args, "tab", None) + try: + res = menu_toggles.list_toggles(node, tab=tab) + except menu_toggles.MenuError as e: + print(c_red(f" Error: {e}"), file=sys.stderr) + sys.exit(2) + if as_json: + print(json.dumps(res, indent=2)) + return + if not res.get("ok"): + print(c_red(f" Error: {res.get('error')}"), file=sys.stderr) + sys.exit(1) + print("\n" + c_bold(f"=== CHROMEBOX TOGGLES: {node} ===") + "\n") + rows = [] + for name in sorted(res.get("toggles", {})): + val = res["toggles"][name] + rows.append([name, c_red("-") if val is None else str(val)]) + print_table(["TOGGLE", "VALUE"], rows) + print() + elif perm_action == "get": + name = getattr(args, "toggle", None) + try: + res = menu_toggles.get_toggle(node, name) + except menu_toggles.MenuError as e: + print(c_red(f" Error: {e}"), file=sys.stderr) + sys.exit(2) + if as_json: + print(json.dumps(res, indent=2)) + elif res.get("ok"): + print(f" {c_bold(name)} = {c_cyan(res['value'])}") + else: + print(c_red(f" Error: {res.get('error')}"), file=sys.stderr) + sys.exit(1) + elif perm_action == "set": + name = getattr(args, "toggle", None) + value = getattr(args, "value", None) + try: + res = menu_toggles.set_toggle(node, name, value) + except menu_toggles.MenuError as e: + print(c_red(f" Error: {e}"), file=sys.stderr) + sys.exit(2) + if as_json: + print(json.dumps(res, indent=2)) + elif res.get("ok"): + print(c_green(f" āœ” {name} = {res['value']} on {node} " + f"(readback verified).")) + else: + print(c_red(f" ✘ Set failed on {node}: {res.get('error')}"), + file=sys.stderr) + sys.exit(1) + elif perm_action == "describe": + tab = getattr(args, "tab", None) + try: + res = menu_toggles.describe_tab(node, tab) + except menu_toggles.MenuError as e: + print(c_red(f" Error: {e}"), file=sys.stderr) + sys.exit(2) + print(json.dumps(res, indent=2)) + else: + print(c_red(f" Error: unknown action {perm_action!r}"), + file=sys.stderr) + sys.exit(2) + + +# --------------------------------------------------------------------------- +# Domain: DM (Direct Messaging & Work Orders) +# --------------------------------------------------------------------------- +def wait_for_reply(recipient: str, target: str, msg_id: str = None, timeout: int = 60) -> str: + """Wait for recipient agent to reply, displaying an animated spinner.""" + spinner_chars = ["ā ‹", "ā ™", "ā ¹", "ā ø", "ā ¼", "ā “", "ā ¦", "ā §", "ā ‡", "ā "] + t0 = time.time() + idx = 0 + dm_pos = DM_LOG.stat().st_size if DM_LOG.exists() else 0 + found_reply = None + chat_hist_file = NETVM_ROOT / "logs" / "chat-history.jsonl" + hist_pos = chat_hist_file.stat().st_size if chat_hist_file.exists() else 0 + + sys.stdout.write("\n") + try: + while time.time() - t0 < timeout: + spin = spinner_chars[idx % len(spinner_chars)] + elapsed = int(time.time() - t0) + sys.stdout.write(f"\r {c_cyan(spin)} Awaiting response from {c_bold(recipient)}/{target}... ({elapsed}s/{timeout}s) [Ctrl+C to stop waiting]") + sys.stdout.flush() + idx += 1 + + # 1. Check DM_LOG for return message from recipient + if DM_LOG.exists() and DM_LOG.stat().st_size > dm_pos: + with open(DM_LOG, "r") as f: + f.seek(dm_pos) + for line in f: + line = line.strip() + if not line: + continue + try: + d = json.loads(line) + ev_type = d.get("type", "") + sender = d.get("agent", "") + if sender == recipient and ev_type in ("sent", "send_done", "verified"): + m = d.get("msg") or d.get("body") + if m and (not msg_id or msg_id not in m): + found_reply = m + break + except Exception: + pass + dm_pos = f.tell() + + if found_reply: + break + + # 2. Check chat-history.jsonl (from harvester if active) + if chat_hist_file.exists() and chat_hist_file.stat().st_size > hist_pos: + with open(chat_hist_file, "r") as f: + f.seek(hist_pos) + for line in f: + line = line.strip() + if not line: + continue + try: + d = json.loads(line) + if d.get("agent") == recipient: + m = d.get("text") or d.get("msg") + if m and (not msg_id or msg_id not in m) and not m.startswith("[from:super]"): + found_reply = m + break + except Exception: + pass + hist_pos = f.tell() + + if found_reply: + break + + # 3. Periodically poll thread messages via dm.py read every ~2.5s + if idx % 12 == 0: + try: + read_cmd = ["python3", str(BIN_DIR / "dm.py"), "read", "--agent", recipient, "--target", target, "--n", "3"] + r_read = subprocess.run(read_cmd, capture_output=True, text=True, timeout=8) + if r_read.returncode == 0 and r_read.stdout: + for block in r_read.stdout.split("\n---\n"): + block_s = block.strip() + if block_s and (not msg_id or f"[id:{msg_id}]" not in block_s) and not block_s.startswith("[from:super]"): + found_reply = block_s + break + except Exception: + pass + + if found_reply: + break + + time.sleep(0.2) + + sys.stdout.write("\r" + " " * 85 + "\r") + sys.stdout.flush() + + if found_reply: + print(f"\n{c_bold(c_cyan('[' + recipient + ']:'))}") + for l in found_reply.splitlines(): + print(f" {l}") + print() + return found_reply + else: + print(c_yellow(f"\n (No response received from {recipient} within {timeout}s)\n")) + return None + + except KeyboardInterrupt: + sys.stdout.write("\r" + " " * 85 + "\r") + sys.stdout.flush() + print(c_dim("\n (Stopped waiting for response)\n")) + return None + +def get_available_targets(agent: str) -> list: + """Find known sidechats and targets for an agent.""" + targets = [] + # 1. From job-sidechats.json + if (NETVM_ROOT / "job-sidechats.json").exists(): + try: + with open(NETVM_ROOT / "job-sidechats.json") as f: + sc_data = json.load(f) + for k, v in sc_data.items(): + if isinstance(v, dict): + if v.get("agent") == agent: + targets.append({"name": k, "type": "sidechat", "desc": f"registered coordination ({k})"}) + elif agent in k: + targets.append({"name": k, "type": "sidechat", "desc": f"registered sidechat ({k})"}) + except Exception: + pass + + # 2. Add well-known agent sidechats + if agent == "646": + targets.append({"name": "646 tasks", "type": "sidechat", "desc": "primary task & check-in sidechat"}) + targets.append({"name": "646-opm-work", "type": "sidechat", "desc": "opm work sidechat"}) + elif agent == "opm": + targets.append({"name": "heartbeat", "type": "sidechat", "desc": "heartbeat & health sidechat"}) + elif agent == "pip": + targets.append({"name": "646-pip-coord", "type": "sidechat", "desc": "pip & 646 coordination sidechat"}) + + # Deduplicate by name + seen = set() + unique_targets = [] + for t in targets: + if t["name"] not in seen: + seen.add(t["name"]) + unique_targets.append(t) + + # Add Main Chat last with caution label + unique_targets.append({ + "name": "main", + "type": "main", + "desc": "Main Chat (CAUTION: avoid per CHAT_POLICY.md)" + }) + return unique_targets + +def _render_chat_history(agent: str, target: str, limit: int = 5): + """Fetch and display recent chat messages from thread.""" + try: + read_cmd = ["python3", str(BIN_DIR / "dm.py"), "read", "--agent", agent, "--target", target, "--n", str(limit)] + r = subprocess.run(read_cmd, capture_output=True, text=True, timeout=12) + if r.returncode == 0 and r.stdout: + blocks = [b.strip() for b in r.stdout.split("\n---\n") if b.strip()] + if blocks: + print(c_dim("--- Recent Messages ---")) + for b in blocks[-limit:]: + if b.startswith("[from:super]"): + print(f"{c_green('[super]:')}") + elif f"[from:{agent}]" in b or f"from {agent}" in b: + print(f"{c_cyan('[' + agent + ']:')}") + else: + print(f"{c_dim('[message]:')}") + for line in b.splitlines()[:5]: + print(f" {line}") + print(c_dim("-----------------------\n")) + except Exception: + pass + +def cmd_dm_chat(args): + agent = args.agent + target = getattr(args, "target", None) + timeout = getattr(args, "timeout", 60) or 60 + + print("\n" + c_bold(f"=== INTERACTIVE CONVERSATION SESSION: {c_cyan(agent.upper())} ===") + "\n") + + # Target selection if not provided + if not target: + available = get_available_targets(agent) + print(c_bold("Select chat target:") + c_dim(" (Prefer sidechats to keep Main Chat clean)\n")) + for idx, t in enumerate(available, 1): + tag = c_green("[Sidechat - RECOMMENDED]") if t["type"] == "sidechat" else c_yellow("[Main Chat - USE WITH CAUTION]") + print(f" [{idx}] {c_bold(t['name']):<18} {tag} {c_dim(t['desc'])}") + print() + + try: + choice = input(f"Choose target [1-{len(available)}] (default: 1): ").strip() + if not choice: + target = available[0]["name"] + else: + choice_idx = int(choice) - 1 + if 0 <= choice_idx < len(available): + target = available[choice_idx]["name"] + else: + target = available[0]["name"] + except (ValueError, KeyboardInterrupt): + target = available[0]["name"] + + print(f"\nEntering chat session with {c_bold(agent)} on {c_cyan(target)}...") + if target == "main": + print(c_yellow(" ⚠ WARNING: Main Chat targeted. Per CHAT_POLICY.md, keep interactions minimal.")) + print(c_dim(" Commands: /exit (or /quit), /refresh (reload history), /switch (change target), /wo <title> <body>\n")) + + # Initial history render + _render_chat_history(agent, target, limit=5) + + # REPL loop + while True: + try: + prompt_str = f"{c_green('super')} ({c_cyan(target)}) > " + line = input(prompt_str).strip() + if not line: + continue + + # Handle slash commands + if line in ("/exit", "/quit", ":q"): + print(c_dim("\nExited chat session.\n")) + break + elif line in ("/refresh", "/history"): + _render_chat_history(agent, target, limit=10) + continue + elif line == "/switch": + available = get_available_targets(agent) + print(c_bold("\nSelect new target:")) + for idx, t in enumerate(available, 1): + print(f" [{idx}] {t['name']} ({t['desc']})") + try: + c = input(f"Choose target [1-{len(available)}]: ").strip() + if c and int(c) - 1 in range(len(available)): + target = available[int(c) - 1]["name"] + print(f"Switched target to {c_cyan(target)}.\n") + _render_chat_history(agent, target, limit=5) + except Exception: + pass + continue + elif line.startswith("/wo "): + parts = line[4:].split(" ", 1) + wo_title = parts[0] + wo_body = parts[1] if len(parts) > 1 else wo_title + wo_id = hashlib.sha256(f"super-{agent}-{wo_title}-{time.time()}".encode()).hexdigest()[:8] + wo_content = f"[WO:{wo_id}] {wo_title} — {wo_body}" + signed_wire, mid = sign_message("super", wo_content, msg_id=wo_id) + print(c_dim(f"Issuing Work Order [WO:{wo_id}]...")) + r = subprocess.run([ + "python3", str(BIN_DIR / "dm.py"), "send", + "--agent", "super", "--to", agent, "--target", target, + "--raw", signed_wire + ], capture_output=True, text=True) + if r.returncode == 0: + print(c_green(f"āœ” Work Order [WO:{wo_id}] delivered.")) + wait_for_reply(agent, target, msg_id=wo_id, timeout=timeout) + else: + print(c_red(f"Delivery failed: {r.stderr or r.stdout}")) + continue + + # Standard message + signed_wire, mid = sign_message("super", line) + r = subprocess.run([ + "python3", str(BIN_DIR / "dm.py"), "send", + "--agent", "super", "--to", agent, "--target", target, + "--raw", signed_wire + ], capture_output=True, text=True) + + if r.returncode == 0: + print(c_green(f"āœ” Delivered & verified [{mid}].")) + wait_for_reply(agent, target, msg_id=mid, timeout=timeout) + else: + print(c_red(f"Delivery failed: {r.stderr or r.stdout}")) + + except (KeyboardInterrupt, EOFError): + print(c_dim("\nExited chat session.\n")) + break + +def cmd_dm_send(args): + sender = resolve_sender(args) + recipient = args.to + target = getattr(args, "target", "main") + message = args.message + wait = getattr(args, "wait", False) + timeout = getattr(args, "timeout", 60) or 60 + allow_main = getattr(args, "allow_main_chat", False) + + if target == "main": + if not allow_main: + print(c_red("ERROR: Targeting Main Chat is blocked by CHAT_POLICY.md to prevent chat degradation."), file=sys.stderr) + print(c_dim(" To send routine task/payloads, use a sidechat: --target '<sidechat>'.\n To override intentionally, pass --allow-main-chat."), file=sys.stderr) + sys.exit(1) + print(c_yellow(" ⚠ [CHAT POLICY OVERRIDE] Main Chat targeted with --allow-main-chat. Keep interaction minimal.")) + + # Soft pre-flight grammar check against lookup_internal + try: + import lookup_engine + is_val, kind, hint = lookup_engine.validate_outbound_sentence(message) + if not is_val and hint: + print(c_yellow(f"\n ⚠ [GRAMMAR NOTICE]\n {hint}\n"), file=sys.stderr) + except Exception: + pass + + should_sign = (sender == "super") and not getattr(args, "no_sign", False) + mid = None + if should_sign: + try: + signed_wire, mid = sign_message(sender, message) + cmd = [ + "python3", str(BIN_DIR / "dm.py"), "send", + "--agent", sender, + "--to", recipient, + "--target", target, + "--raw", + ] + if getattr(args, "expect_reply", False): + cmd.append("--expect-reply") + if getattr(args, "reply_timeout", None): + cmd.extend(["--reply-timeout", str(args.reply_timeout)]) + cmd.append(signed_wire) + print(f"Dispatching Cryptographically Signed DM [{c_green('verified from:' + sender)}] -> [{c_bold(recipient)}/{target}]...") + res = subprocess.run(cmd) + if res.returncode != 0: + sys.exit(res.returncode) + if wait: + wait_for_reply(recipient, target, msg_id=mid, timeout=timeout) + sys.exit(0) + except Exception as e: + print(f"Signing notice: {e}, sending standard DM...", file=sys.stderr) + + cmd = [ + "python3", str(BIN_DIR / "dm.py"), "send", + "--agent", sender, + "--to", recipient, + "--target", target, + ] + if getattr(args, "expect_reply", False): + cmd.append("--expect-reply") + if getattr(args, "reply_timeout", None): + cmd.extend(["--reply-timeout", str(args.reply_timeout)]) + cmd.append(message) + + print(f"Dispatching DM [{c_cyan('from:' + sender)}] -> [{c_bold(recipient)}/{target}]...") + res = subprocess.run(cmd) + if res.returncode != 0: + sys.exit(res.returncode) + if wait: + wait_for_reply(recipient, target, msg_id=mid, timeout=timeout) + sys.exit(0) + +def cmd_dm_wo(args): + sender = resolve_sender(args) + recipient = args.to + target = getattr(args, "target", None) + if not target: + target = DEFAULT_AGENT_SIDECHATS.get(recipient, "main") + title = args.title + body = args.body + priority = getattr(args, "priority", "routine") + wait = getattr(args, "wait", True) + timeout = getattr(args, "timeout", 60) or 60 + allow_main = getattr(args, "allow_main_chat", False) + + if target == "main": + if not allow_main: + print(c_red("ERROR: Work Orders must target sidechats per CHAT_POLICY.md."), file=sys.stderr) + print(c_dim(" Defaulting to sidechat recommended. To override, pass --allow-main-chat."), file=sys.stderr) + sys.exit(1) + print(c_yellow(" ⚠ [CHAT POLICY OVERRIDE] Work Order targeted to Main Chat with --allow-main-chat.")) + + wo_id = hashlib.sha256(f"{sender}-{recipient}-{title}-{time.time()}".encode()).hexdigest()[:8] + prefix = "[URGENT] " if priority == "urgent" else "" + wo_content = f"{prefix}[WO:{wo_id}] [from {sender}] {title} — {body}" + + + should_sign = (sender == "super") and not getattr(args, "no_sign", False) + if should_sign: + try: + signed_wire, mid = sign_message(sender, wo_content, msg_id=wo_id) + cmd = [ + "python3", str(BIN_DIR / "dm.py"), "send", + "--agent", sender, + "--to", recipient, + "--target", target, + "--raw", + signed_wire + ] + print(f"Issuing Cryptographically Signed Work Order {c_green('[WO:' + wo_id + ']')} [{c_green('verified from:' + sender)}] -> [{c_bold(recipient)}/{c_cyan(target)}]...") + res = subprocess.run(cmd) + if res.returncode != 0: + sys.exit(res.returncode) + if wait: + wait_for_reply(recipient, target, msg_id=wo_id, timeout=timeout) + sys.exit(0) + except Exception as e: + print(f"Signing notice: {e}, sending standard work order...", file=sys.stderr) + + cmd = [ + "python3", str(BIN_DIR / "dm.py"), "send", + "--agent", sender, + "--to", recipient, + "--target", target, + "--expect-reply", + wo_content + ] + + print(f"Issuing Work Order {c_cyan('[WO:' + wo_id + ']')} [{c_cyan('from:' + sender)}] -> [{c_bold(recipient)}/{c_cyan(target)}]...") + res = subprocess.run(cmd) + if res.returncode != 0: + sys.exit(res.returncode) + if wait: + wait_for_reply(recipient, target, msg_id=wo_id, timeout=timeout) + sys.exit(0) + +def cmd_dm_ack(args): + sender = resolve_sender(args) + recipient = args.to + target = getattr(args, "target", "main") + ref_id = args.id + + ack_message = f"[ACK:{ref_id}]" + cmd = [ + "python3", str(BIN_DIR / "dm.py"), "send", + "--agent", sender, + "--to", recipient, + "--target", target, + ack_message + ] + + print(f"Sending Acknowledgment {c_green('[ACK:' + ref_id + ']')} -> [{c_bold(recipient)}/{target}]...") + res = subprocess.run(cmd) + sys.exit(res.returncode) + +def cmd_dm_verify(args): + cmd = ["python3", str(BIN_DIR / "dm.py"), "verify-sig"] + if getattr(args, "agent", None): + cmd.extend(["--agent", args.agent]) + if getattr(args, "target", None): + cmd.extend(["--target", args.target]) + if getattr(args, "message", None): + cmd.append(args.message) + res = subprocess.run(cmd) + sys.exit(res.returncode) + +def cmd_dm_log(args): + n = getattr(args, "n", 25) + filter_agent = getattr(args, "agent", None) + filter_text = getattr(args, "filter", None) + + if not DM_LOG.exists(): + print(c_dim("dm-log.jsonl not found.")) + return + + def _match(data): + if filter_agent and (data.get("agent") != filter_agent and data.get("to") != filter_agent): + return False + if filter_text: + if filter_text.lower() not in json.dumps(data).lower(): + return False + return True + + with open(DM_LOG, "r") as f: + lines = f.readlines() + + entries = [] + if isinstance(n, int) and n >= 1: + # Walk newest-first, parsing only until n matches: identical + # result to a full parse + [-n:] at O(n) instead of O(file). + for line in reversed(lines): + line = line.strip() + if not line: + continue + try: + data = json.loads(line) + except Exception: + continue + if not _match(data): + continue + entries.append(data) + if len(entries) >= n: + break + entries.reverse() + else: + # Legacy path: preserve entries[-n:] quirks for n <= 0. + for line in lines: + line = line.strip() + if not line: + continue + try: + data = json.loads(line) + except Exception: + continue + if not _match(data): + continue + entries.append(data) + entries = entries[-n:] + + if args.json: + print(json.dumps({"ok": True, "entries": entries}, indent=2)) + return + + print("\n" + c_bold(f"=== RECENT INTER-AGENT DMs (last {len(entries)}) ===") + "\n") + + headers = ["TIME", "TYPE", "FROM -> TO", "TARGET", "STATUS / DETAIL"] + rows = [] + for item in entries: + t_rel = parse_relative_time(item.get("ts", "")) + ev_type = item.get("type", "unknown") + + # Colorize type badges + if ev_type == "sent" or ev_type == "send_done": + badge = c_green("SENT") + elif ev_type == "verified": + badge = c_green("āœ” VERIFIED") + elif "fail" in ev_type: + badge = c_red("FAIL") + elif ev_type == "retry": + badge = c_yellow("RETRY") + elif ev_type == "send_start": + badge = c_blue("START") + else: + badge = c_dim(ev_type[:8].upper()) + + from_to = f"{item.get('agent', '-')} -> {item.get('to', '-')}" + target = item.get("target", "-") + + msg = item.get("msg") or item.get("reason") or item.get("error") or "" + if not msg: + if ev_type == "retry": + msg = f"attempt {item.get('attempt', '?')} (id: {item.get('id', '-')})" + elif ev_type == "failed": + msg = f"delivery failed (id: {item.get('id', '-')}, verified: {item.get('verified', False)})" + elif ev_type == "sent": + msg = f"delivered (id: {item.get('id', '-')}, verified: {item.get('verified', True)})" + elif ev_type == "verified": + msg = f"confirmed in DOM (id: {item.get('id', '-')})" + elif ev_type == "send_done": + msg = f"completed send (id: {item.get('id', '-')})" + elif ev_type == "alias_resolved": + msg = f"resolved {item.get('target')} -> {item.get('thread_uuid', '')[:8]}" + elif ev_type == "nav_ok": + msg = f"navigated to thread {item.get('thread_uuid', '')[:8]}" + elif item.get("id"): + msg = f"id: {item.get('id')}" + msg_clean = " ".join(msg.split()) + + # Highlight protocol tokens + if "[WO:" in msg_clean: + msg_clean = re.sub(r"\[WO:([a-f0-9]+)\]", lambda m: c_magenta(m.group(0)), msg_clean) + if "[ACK:" in msg_clean: + msg_clean = re.sub(r"\[ACK:([a-f0-9]+)\]", lambda m: c_green(m.group(0)), msg_clean) + if "[JOB" in msg_clean: + msg_clean = re.sub(r"\[JOB ([^\]]+)\]", lambda m: c_cyan(m.group(0)), msg_clean) + if "[RESULT" in msg_clean: + msg_clean = re.sub(r"\[RESULT ([^\]]+)\]", lambda m: c_yellow(m.group(0)), msg_clean) + + rows.append([ + t_rel, + badge, + from_to, + target[:16], + msg_clean[:60] if msg_clean else c_dim("-") + ]) + + print_table(headers, rows) + print("\n" + c_dim(" Commands: super dm tail | super dm send --to <agent> \"msg\" | super dm wo --to <agent> --title \"T\" \"body\"") + "\n") + +def cmd_dm_tail(args): + filter_text = getattr(args, "filter", None) + print(c_bold("=== STREAMING DM LOG (Ctrl+C to stop) ===") + "\n") + + if not DM_LOG.exists(): + print(c_red("Error: dm-log.jsonl does not exist.")) + return + + with open(DM_LOG, "r") as f: + # Seek to end + f.seek(0, os.SEEK_END) + try: + while True: + line = f.readline() + if not line: + time.sleep(0.5) + continue + try: + data = json.loads(line.strip()) + if filter_text and filter_text.lower() not in line.lower(): + continue + ts = parse_relative_time(data.get("ts", "")) + ev_type = data.get("type", "") + sender = data.get("agent", "") + to = data.get("to", "") + msg = data.get("msg") or data.get("reason") or data.get("error") or "" + print(f"[{ts}] {c_cyan(ev_type.upper()):<12} {sender} -> {to}: {msg[:90]}") + except Exception: + pass + except KeyboardInterrupt: + print("\n" + c_dim("Tail stopped.")) + +TRANSFERS_REGISTRY_FILE = TRANSFERS_DIR / ".transfers-registry.json" + +def _load_transfers_registry() -> dict: + if TRANSFERS_REGISTRY_FILE.exists(): + try: + with open(TRANSFERS_REGISTRY_FILE, "r") as f: + return json.load(f) + except Exception: + return {} + return {} + +def _save_transfers_registry(reg: dict): + TRANSFERS_DIR.mkdir(parents=True, exist_ok=True) + with open(TRANSFERS_REGISTRY_FILE, "w") as f: + json.dump(reg, f, indent=2) + +def cmd_dm_send_file(args): + file_path = Path(args.file).resolve() + if not file_path.exists() or not file_path.is_file(): + print(c_red(f"Error: File not found: {file_path}"), file=sys.stderr) + sys.exit(1) + + recipient = args.to + target = getattr(args, "target", None) + if not target: + target = DEFAULT_AGENT_SIDECHATS.get(recipient, "main") + + if target == "main": + print(c_yellow(" ⚠ [CHAT POLICY NOTE] Targeting Main Chat. Per CHAT_POLICY.md, avoid using Main Chat for routine payloads.")) + + note = getattr(args, "note", None) + wait = getattr(args, "wait", False) + timeout = getattr(args, "timeout", 60) or 60 + + # 1. Compute checksum and file stats + file_stat = file_path.stat() + size_bytes = file_stat.st_size + size_kb = size_bytes / 1024.0 + + hasher = hashlib.sha256() + with open(file_path, "rb") as f: + while chunk := f.read(65536): + hasher.update(chunk) + sha256_full = hasher.hexdigest() + sha256_short = sha256_full[:12] + + # 2. Stage file in transfers directory + agent_transfers_dir = TRANSFERS_DIR / recipient + agent_transfers_dir.mkdir(parents=True, exist_ok=True) + + ts_prefix = datetime.now().strftime("%Y%m%d_%H%M%S") + dest_filename = f"{ts_prefix}_{file_path.name}" + dest_path = agent_transfers_dir / dest_filename + shutil.copy2(file_path, dest_path) + + # 3. Construct protocol payload pointer + file_id = hashlib.sha256(f"{recipient}-{dest_filename}-{time.time()}".encode()).hexdigest()[:8] + + # Save to transfers registry + reg = _load_transfers_registry() + reg[file_id] = { + "file_id": file_id, + "recipient": recipient, + "original_name": file_path.name, + "original_path": str(file_path), + "staged_path": str(dest_path), + "sha256": sha256_full, + "size_bytes": size_bytes, + "size_kb": f"{size_kb:.1f} KB", + "staged_at": datetime.now(timezone.utc).isoformat(), + "note": note, + "sender": "super", + "target": target + } + _save_transfers_registry(reg) + + lines = [ + f"[FILE:{file_id}] {file_path.name} ({size_kb:.1f} KB, sha256:{sha256_short})", + f"Path: {dest_path}", + ] + if note: + lines.append(f"Note: {note}") + + payload_message = "\n".join(lines) + + # 4. Dispatch cryptographically signed DM to recipient sidechat + signed_wire, mid = sign_message("super", payload_message, msg_id=file_id) + print(f"Staged file {c_bold(file_path.name)} -> {c_dim(str(dest_path))}") + print(f"Dispatching File Pointer {c_green('[FILE:' + file_id + ']')} [{c_green('verified from:super')}] -> [{c_bold(recipient)}/{c_cyan(target)}]...") + + cmd = [ + "python3", str(BIN_DIR / "dm.py"), "send", + "--agent", "super", + "--to", recipient, + "--target", target, + "--raw", + signed_wire + ] + res = subprocess.run(cmd) + if res.returncode != 0: + print(c_red(f"File notification delivery failed with code {res.returncode}"), file=sys.stderr) + sys.exit(res.returncode) + + print(c_green(f"āœ” File notification delivered to {recipient}/{target}.")) + + if wait: + wait_for_reply(recipient, target, msg_id=file_id, timeout=timeout) + +def cmd_dm_files(args): + subaction = getattr(args, "files_action", "list") + filter_agent = getattr(args, "agent", None) + + if subaction == "clean": + older_than_days = getattr(args, "older_than", 7) + cutoff_sec = time.time() - (older_than_days * 86400) + reg = _load_transfers_registry() + removed_count = 0 + retained_reg = {} + + # Scan files on disk + for ad in (TRANSFERS_DIR.iterdir() if TRANSFERS_DIR.exists() else []): + if not ad.is_dir() or ad.name.startswith("."): + continue + if filter_agent and ad.name != filter_agent: + continue + for fp in list(ad.iterdir()): + if fp.is_file() and fp.stat().st_mtime < cutoff_sec: + try: + fp.unlink() + removed_count += 1 + except Exception as e: + print(c_red(f"Error removing {fp}: {e}")) + + for fid, rec in reg.items(): + staged = Path(rec.get("staged_path", "")) + if staged.exists(): + retained_reg[fid] = rec + _save_transfers_registry(retained_reg) + + print(c_green(f"āœ” Cleaned {removed_count} payload file(s) older than {older_than_days} day(s).")) + return + + if not TRANSFERS_DIR.exists(): + print(c_dim("No transferred files found (transfers/ directory is empty).")) + return + + reg = _load_transfers_registry() + records = [] + agent_dirs = [TRANSFERS_DIR / filter_agent] if filter_agent else sorted(TRANSFERS_DIR.iterdir()) + + for ad in agent_dirs: + if not ad.is_dir() or ad.name.startswith("."): + continue + agent_name = ad.name + for fp in sorted(ad.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True): + if not fp.is_file() or fp.name.startswith("."): + continue + st = fp.stat() + size_kb = f"{st.st_size / 1024:.1f} KB" + mtime = datetime.fromtimestamp(st.st_mtime, tz=timezone.utc).isoformat() + t_rel = parse_relative_time(mtime) + + clean_name = fp.name + m = re.match(r"^\d{8}_\d{6}_(.+)$", fp.name) + if m: + clean_name = m.group(1) + + # Match in registry if available + reg_entry = next((r for r in reg.values() if r.get("staged_path") == str(fp)), {}) + file_id = reg_entry.get("file_id", "-") + note = reg_entry.get("note", "") + + records.append({ + "agent": agent_name, + "file_id": file_id, + "filename": clean_name, + "staged_name": fp.name, + "path": str(fp), + "size_kb": size_kb, + "mtime": mtime, + "time_rel": t_rel, + "note": note + }) + + if args.json: + print(json.dumps({"ok": True, "files": records}, indent=2)) + return + + print("\n" + c_bold(f"=== TRANSFERRED PAYLOADS & STAGED FILES ({len(records)}) ===") + "\n") + headers = ["AGENT", "FILE ID", "FILENAME", "SIZE", "TRANSFERRED", "NOTE", "STAGED PATH"] + rows = [] + for r in records: + rows.append([ + c_cyan(r["agent"]), + c_green(r["file_id"]) if r["file_id"] != "-" else c_dim("-"), + c_bold(r["filename"]), + r["size_kb"], + r["time_rel"], + r["note"][:20] if r["note"] else c_dim("-"), + c_dim(r["path"]) + ]) + print_table(headers, rows) + print("\n" + c_dim(" Commands: super dm send-file --to <agent> <path> | super dm files clean [--older-than N]") + "\n") + +# --------------------------------------------------------------------------- +# Domain: THREAD (Chat Oversight via box-chat.py) +# --------------------------------------------------------------------------- +def cmd_thread_list(args): + agent = getattr(args, "agent", None) + if not agent: + print("\n" + c_bold("=== FLEET REGISTERED SIDECHATS & THREAD MAPPINGS ===") + "\n") + sc_file = NETVM_ROOT / "job-sidechats.json" + rows = [] + headers = ["TARGET / ALIAS", "AGENT", "THREAD UUID", "PURPOSE / NOTES"] + if sc_file.exists(): + try: + with open(sc_file, "r") as f: + data = json.load(f) + for k, v in sorted(data.items()): + if isinstance(v, dict): + ag = v.get("agent", "-") + uuid = v.get("thread_uuid") or v.get("uuid") or "-" + desc = v.get("description") or v.get("purpose") or "-" + rows.append([c_cyan(k), c_bold(ag), uuid, desc[:45]]) + elif isinstance(v, str): + rows.append([c_cyan(k), "-", v, "-"]) + except Exception as e: + print(c_red(f"Error reading job-sidechats.json: {e}")) + print_table(headers, rows) + print("\n" + c_dim(" To view specific thread: box thread view <agent> <uuid|alias>") + "\n") + print(c_dim(" To list agent active sessions: box thread list <agent>") + "\n") + return + + threads = [] + + def is_noise(title): + t = (title or "").lower().strip() + return bool(re.search(r"generate.*(chat|session)?.*title", t)) + + # Fast path: try fast headless gateway via muse_hybrid (isolated per-node WARP egress) + try: + import muse_hybrid + gw_threads, gw_err = muse_hybrid.get_threads(agent) + if gw_threads and not gw_err: + for t in gw_threads: + if is_noise(t.get("title")): + continue + threads.append({ + "id": t.get("session_id", ""), + "kind": "thread" if t.get("thread") else "chat", + "title": t.get("title") or "(no title)", + "participants": [agent], + "last_message_at": t.get("updated", "") + }) + except Exception: + pass + + # Fallback to box-chat.py / CDP if gateway returned no threads + if not threads: + cmd = ["python3", str(BIN_DIR / "box-chat.py"), "thread-list", agent] + res = subprocess.run(cmd, capture_output=True, text=True) + try: + data = json.loads(res.stdout) + raw_threads = data.get("threads", []) if data.get("ok") else [] + threads = [t for t in raw_threads if not is_noise(t.get("title"))] + except Exception: + threads = [] + + if args.json: + print(json.dumps({"ok": True, "threads": threads}, indent=2)) + return + + print("\n" + c_bold(f"=== ACTIVE THREADS FOR {agent.upper()} ({len(threads)}) ===") + "\n") + + headers = ["ID / UUID", "KIND", "TITLE", "PARTICIPANTS", "LAST ACTIVE"] + rows = [] + for t in threads: + tid = t.get("id", "") + tid_disp = c_cyan(tid[:12]) if tid != "main" else c_bold("main") + kind = t.get("kind", "") + title = t.get("title", "") + parts = ", ".join(t.get("participants", [])) + last_act = parse_relative_time(t.get("last_message_at", "")) + + rows.append([tid_disp, kind, title[:35], parts, last_act]) + + print_table(headers, rows) + print("\n" + c_dim(f" View thread: super thread view {agent} <id>") + "\n") + +def cmd_thread_view(args): + agent = args.agent + thread_id = args.thread_id + limit = getattr(args, "limit", 15) + messages = [] + + # 1. Resolve alias or name if known + try: + import dm + resolved = dm.resolve_sidechat_target(thread_id, agent) + if resolved and resolved != thread_id: + thread_id = resolved + except Exception: + pass + + # 2. If short UUID prefix (6-12 hex chars), resolve against agent's thread list + if re.fullmatch(r"[0-9a-fA-F]{6,12}", str(thread_id).strip()): + try: + import muse_hybrid + gw_threads, _ = muse_hybrid.get_threads(agent) + if gw_threads: + for t in gw_threads: + sid = t.get("session_id", "") + if sid.lower().startswith(str(thread_id).strip().lower()): + thread_id = sid + break + except Exception: + pass + + # Fast path: try fast headless gateway via muse_hybrid (isolated per-node WARP egress) + try: + import muse_hybrid + gw_msgs, gw_err = muse_hybrid.get_history(agent, thread_id=thread_id, limit=limit) + if gw_msgs and not gw_err: + for m in gw_msgs: + role = m.get("role", "unknown") + messages.append({ + "from": {"name": role, "role": role}, + "text": m.get("text", "") + }) + except Exception: + pass + + # Fallback to box-chat.py / CDP if gateway returned no messages + if not messages: + cmd = ["python3", str(BIN_DIR / "box-chat.py"), "thread-messages", agent, thread_id, "--limit", str(limit)] + try: + res = subprocess.run(cmd, capture_output=True, text=True, timeout=15) + data = json.loads(res.stdout) + messages = data.get("messages", []) if data.get("ok") else [] + except Exception: + messages = [] + if not messages: + print(c_red(f"Error viewing thread {thread_id}: no messages returned")) + return + + if args.json: + print(json.dumps({"ok": True, "messages": messages}, indent=2)) + return + + print("\n" + c_bold(f"=== THREAD {thread_id} ({agent}) — {len(messages)} messages ===") + "\n") + + for msg in messages: + sender_info = msg.get("from", {}) + sender_name = sender_info.get("name") or sender_info.get("role") or "unknown" + text = msg.get("text", "") + + # Colorize sender + if sender_name in ("human", "super", "operator-main"): + sender_tag = c_yellow(f"[{sender_name}]") + else: + sender_tag = c_cyan(f"[{sender_name}]") + + print(f"{sender_tag}:") + for line in text.split("\n"): + print(f" {line}") + print() + +# --------------------------------------------------------------------------- +# Domain: JOB (Systemd Timers & Job Definitions) +# --------------------------------------------------------------------------- +def cmd_job_list(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "job-list"] + if getattr(args, "archived", False): + cmd.append("--archived") + res = subprocess.run(cmd, capture_output=True, text=True) + + try: + data = json.loads(res.stdout) + except Exception: + print(c_red("Failed to load jobs:") + f"\n{res.stdout}") + return + + if args.json: + print(json.dumps(data, indent=2)) + return + + jobs = data.get("jobs", []) + title_extra = " (INCLUDING ARCHIVED)" if getattr(args, "archived", False) else "" + print("\n" + c_bold(f"=== DEFINED JOBS ({len(jobs)}){title_extra} ===") + "\n") + + headers = ["NAME", "AGENT", "SCHEDULE", "TIMEOUT", "STATUS", "DESCRIPTION"] + rows = [] + for j in jobs: + sched = j.get("schedule", "-") + sched_disp = c_green(sched) if sched != "manual" else c_dim("manual") + is_arch = j.get("archived", False) + status_disp = c_yellow("ARCHIVED") if is_arch else c_green("ACTIVE") + rows.append([ + c_bold(j.get("name", "")), + j.get("agent", "-"), + sched_disp, + f"{j.get('timeout', '-')}s", + status_disp, + (j.get("description") or "-")[:45] + ]) + + print_table(headers, rows) + print("\n" + c_dim(" Commands: box job show <name> | box job archive <name> | box job unarchive <name> | box job run <name>") + "\n") + + +def cmd_job_archive(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "job-archive", args.name] + if getattr(args, "force", False): + cmd.append("--force") + res = subprocess.run(cmd, capture_output=True, text=True) + try: + data = json.loads(res.stdout) + except Exception: + print(c_red("Failed to archive job:") + f"\n{res.stderr or res.stdout}") + return + if args.json: + print(json.dumps(data, indent=2)) + return + if data.get("ok"): + print(c_green(f"āœ” Archived job '{args.name}' to jobs/archive/{args.name}.json")) + else: + print(c_red(f"Error archiving job '{args.name}': {data.get('error', 'unknown error')}")) + + +def cmd_job_unarchive(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "job-unarchive", args.name] + res = subprocess.run(cmd, capture_output=True, text=True) + try: + data = json.loads(res.stdout) + except Exception: + print(c_red("Failed to unarchive job:") + f"\n{res.stderr or res.stdout}") + return + if args.json: + print(json.dumps(data, indent=2)) + return + if data.get("ok"): + print(c_green(f"āœ” Unarchived job '{args.name}' to jobs/{args.name}.json")) + else: + print(c_red(f"Error unarchiving job '{args.name}': {data.get('error', 'unknown error')}")) + +def cmd_job_show(args): + name = args.name + # 1. Fetch job definition via box-ctl + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "job-get", name] + res = subprocess.run(cmd, capture_output=True, text=True) + try: + jdata = json.loads(res.stdout) + except Exception: + print(c_red(f"Failed to query job '{name}': {res.stdout or res.stderr}")) + return + + if not jdata.get("ok"): + print(c_red(f"Error: {jdata.get('error', 'Job not found')}")) + return + + job = jdata.get("job", {}) + + # 2. Fetch timer status + tcmd = ["python3", str(BIN_DIR / "box-ctl.py"), "timer-status", name] + tres = subprocess.run(tcmd, capture_output=True, text=True) + tdata = None + try: + parsed_t = json.loads(tres.stdout) + if parsed_t.get("ok"): + tdata = parsed_t + except Exception: + pass + + if args.json: + print(json.dumps({"ok": True, "job": job, "timer": tdata}, indent=2)) + return + + print("\n" + c_bold(f"=== JOB DEFINITION: {name} ===") + "\n") + print(f" Description : {job.get('description', '-')}") + print(f" Target Agent: {c_cyan(job.get('agent', '-'))}") + sched = job.get('schedule', '-') + print(f" Schedule : {c_green(sched) if sched != 'manual' else c_dim('manual')}") + print(f" Timeout : {job.get('timeout', 300)}s") + print(f" On Failure : {job.get('on_failure', 'alert')}") + if job.get("chain_next"): + print(f" Chain Next : {job.get('chain_next')}") + if job.get("dm_target"): + print(f" DM Target : {job.get('dm_target')}") + + sidechat = job.get("sidechat") + if sidechat and any(sidechat.values()): + print(f"\n {c_bold('Sidechat Config:')}") + print(f" Create : {sidechat.get('create', False)}") + if sidechat.get("name_template"): + print(f" Name Template : {sidechat.get('name_template')}") + if sidechat.get("reuse_key"): + print(f" Reuse Key : {sidechat.get('reuse_key')}") + + followup = job.get("followup") + if followup: + print(f"\n {c_bold('Follow-up Policy:')}") + print(f" Expect Reply : {followup.get('expect_reply', False)}") + print(f" Timeout : {followup.get('timeout', '-')}") + print(f" Nudges : {followup.get('nudges', 0)}") + if followup.get("escalate"): + print(f" Escalate To : {followup.get('escalate')}") + if followup.get("route"): + print(f" Route : {followup.get('route')}") + + print(f"\n {c_bold('Prompt Template:')}") + prompt = job.get("prompt_template", "") + for line in prompt.splitlines(): + print(f" {c_dim(line)}") + + print(f"\n {c_bold('Systemd Timer Status:')}") + if tdata: + unit = tdata.get("unit", f"job-{name}.timer") + act = badge_ok() if tdata.get("active") else badge_err("INACTIVE") + en = str(tdata.get("enabled")) + cal = tdata.get("oncalendar") or "-" + last_run = tdata.get("last_run") or "-" + next_run = tdata.get("next_run") or "-" + res_str = tdata.get("last_result") or "-" + print(f" Unit File : {unit}") + print(f" Active : {act}") + print(f" Enabled : {en}") + print(f" Schedule : {c_cyan(cal)}") + print(f" Last Run : {last_run}") + print(f" Next Run : {next_run}") + print(f" Result : {res_str}") + else: + print(f" {badge_dim('NOT CONFIGURED')} (Run 'super job enable {name}' to activate)") + print() + +def cmd_job_status(args): + name = getattr(args, "name", None) + if name: + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "timer-status", name] + else: + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "timer-list"] + + res = subprocess.run(cmd, capture_output=True, text=True) + try: + data = json.loads(res.stdout) + except Exception: + print(c_red("Failed to query timers:") + f"\n{res.stdout}") + return + + if args.json: + print(json.dumps(data, indent=2)) + return + + if name: + if not data.get("ok"): + print(c_red(f"Error querying timer '{name}': {data.get('error', 'Unknown error')}")) + return + print(f"\n{c_bold('Timer Status: ' + name)}") + print(f" Unit File : {data.get('unit')}") + print(f" Active : {badge_ok() if data.get('active') else badge_err('INACTIVE')}") + print(f" Enabled : {data.get('enabled')}") + print(f" Schedule : {c_cyan(data.get('oncalendar', '-'))}") + print(f" Last Run : {data.get('last_run') or '-'}") + print(f" Next Run : {data.get('next_run') or '-'}") + print(f" Result : {data.get('last_result') or '-'}\n") + else: + timers = data.get("timers", []) + print("\n" + c_bold(f"=== ACTIVE TIMERS ({len(timers)}) ===") + "\n") + headers = ["NAME", "UNIT", "ACTIVE", "ENABLED"] + rows = [] + for t in timers: + act = badge_ok() if t.get("active") else badge_err("INACTIVE") + rows.append([c_bold(t.get("name", "")), t.get("unit", ""), act, str(t.get("enabled"))]) + print_table(headers, rows) + print() + +def cmd_job_create(args): + name = args.name + if not re.match(r"^[a-z0-9-]{1,64}$", name): + print(c_red("Error: Job name must match ^[a-z0-9-]{1,64}$"), file=sys.stderr) + sys.exit(1) + + job_file = JOBS_DIR / f"{name}.json" + existed = job_file.exists() + update_mode = getattr(args, "update", False) + if existed and not update_mode: + print(c_red(f"Error: Job '{name}' already exists. Use --update to modify existing job."), file=sys.stderr) + sys.exit(1) + + file_arg = getattr(args, "file", None) + if file_arg: + try: + if file_arg == "-": + raw_data = json.load(sys.stdin) + else: + with open(file_arg, "r") as f: + raw_data = json.load(f) + job_dict = raw_data + job_dict["name"] = name + except Exception as e: + print(c_red(f"Error loading JSON from '{file_arg}': {e}"), file=sys.stderr) + sys.exit(1) + else: + # Check required fields + if not args.agent: + print(c_red("Error: --agent is required when not using --file."), file=sys.stderr) + sys.exit(1) + if not args.prompt: + print(c_red("Error: --prompt is required when not using --file."), file=sys.stderr) + sys.exit(1) + + job_dict = { + "name": name, + "agent": args.agent, + "schedule": args.schedule or "manual", + "description": args.description or f"Job {name}", + "prompt_template": args.prompt, + "timeout": args.timeout or 300, + "on_failure": getattr(args, "on_failure", "alert") or "alert", + "chain_next": getattr(args, "chain_next", None), + } + if getattr(args, "sidechat_create", False) or getattr(args, "sidechat_name", None) or getattr(args, "reuse_key", None): + job_dict["sidechat"] = { + "create": bool(getattr(args, "sidechat_create", False) or getattr(args, "sidechat_name", None) or getattr(args, "reuse_key", None)), + } + if getattr(args, "sidechat_name", None): + job_dict["sidechat"]["name_template"] = args.sidechat_name + if getattr(args, "reuse_key", None): + job_dict["sidechat"]["reuse_key"] = args.reuse_key + + if getattr(args, "dm_target", None): + job_dict["dm_target"] = args.dm_target + + # Call box-ctl.py job-put + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "job-put", name] + res = subprocess.run(cmd, input=json.dumps(job_dict), capture_output=True, text=True) + try: + resp = json.loads(res.stdout) + except Exception: + print(c_red(f"Failed to save job via box-ctl: {res.stdout or res.stderr}"), file=sys.stderr) + sys.exit(1) + + if not resp.get("ok"): + print(c_red(f"Error saving job: {resp.get('error', 'unknown error')}"), file=sys.stderr) + if "detail" in resp: + print(c_dim(f"Detail: {json.dumps(resp['detail'])}"), file=sys.stderr) + sys.exit(1) + + action_label = "Updated" if existed else "Created" + print(c_green(f"āœ” {action_label} job '{name}' (saved to jobs/{name}.json and committed to git)")) + + # Manage timer + schedule = job_dict.get("schedule", "manual") + no_enable = getattr(args, "no_enable", False) + + timer_unit = Path.home() / ".config" / "systemd" / "user" / f"job-{name}.timer" + timer_existed = timer_unit.exists() + + if schedule != "manual" and not no_enable: + if timer_existed: + subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-delete", name, "--keep-job"], + capture_output=True, text=True) + r_timer = subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-create", name], + capture_output=True, text=True) + try: + tresp = json.loads(r_timer.stdout) + if tresp.get("ok"): + print(c_green(f"āœ” Systemd timer active: job-{name}.timer ({tresp.get('oncalendar')})")) + else: + print(c_yellow(f"⚠ Timer could not be activated: {tresp.get('error')}")) + except Exception: + print(c_yellow(f"⚠ Timer creation output: {r_timer.stdout or r_timer.stderr}")) + elif timer_existed and schedule == "manual": + subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-delete", name, "--keep-job"], + capture_output=True, text=True) + print(c_dim("Systemd timer removed because schedule is set to 'manual'.")) + +def cmd_job_enable(args): + name = args.name + job_file = JOBS_DIR / f"{name}.json" + if not job_file.exists(): + print(c_red(f"Error: Job definition jobs/{name}.json not found."), file=sys.stderr) + sys.exit(1) + + try: + jdata = json.loads(job_file.read_text()) + if jdata.get("schedule") == "manual": + print(c_red(f"Error: Cannot enable timer for job '{name}' with schedule 'manual'."), file=sys.stderr) + print(c_dim("Update schedule to a valid cron pattern first using 'super job create --update'."), file=sys.stderr) + sys.exit(1) + except Exception as e: + print(c_red(f"Error reading job definition: {e}"), file=sys.stderr) + sys.exit(1) + + timer_unit = Path.home() / ".config" / "systemd" / "user" / f"job-{name}.timer" + if not timer_unit.exists(): + print(f"Timer unit not found for '{name}'. Creating systemd timer...") + res = subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-create", name], + capture_output=True, text=True) + else: + print(f"Activating existing timer for '{name}'...") + res = subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-enable", name], + capture_output=True, text=True) + + try: + data = json.loads(res.stdout) + if data.get("ok"): + print(c_green(f"āœ” Timer job-{name}.timer enabled and active.")) + else: + print(c_red(f"Error enabling timer: {data.get('error', 'unknown error')}")) + sys.exit(1) + except Exception: + print(res.stdout or res.stderr) + +def cmd_job_disable(args): + name = args.name + timer_unit = Path.home() / ".config" / "systemd" / "user" / f"job-{name}.timer" + if not timer_unit.exists(): + print(c_yellow(f"Timer job-{name}.timer is not currently installed.")) + return + + print(f"Disabling timer for '{name}'...") + res = subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-disable", name], + capture_output=True, text=True) + subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-stop", name], + capture_output=True, text=True) + try: + data = json.loads(res.stdout) + if data.get("ok"): + print(c_green(f"āœ” Timer job-{name}.timer disabled and stopped.")) + else: + print(c_red(f"Error disabling timer: {data.get('error', 'unknown error')}")) + sys.exit(1) + except Exception: + print(res.stdout or res.stderr) + +def cmd_job_delete(args): + name = args.name + job_file = JOBS_DIR / f"{name}.json" + timer_unit = Path.home() / ".config" / "systemd" / "user" / f"job-{name}.timer" + + if not job_file.exists() and not timer_unit.exists(): + print(c_red(f"Error: Job '{name}' not found."), file=sys.stderr) + sys.exit(1) + + force = getattr(args, "force", False) + yes = getattr(args, "yes", False) + + if not (yes or force): + prompt_msg = f"Are you sure you want to delete job '{c_bold(name)}' and its timer? [y/N]: " + try: + choice = input(prompt_msg).strip().lower() + if choice not in ("y", "yes"): + print(c_dim("Deletion aborted.")) + return + except KeyboardInterrupt: + print("\n" + c_dim("Deletion aborted.")) + return + + # 1. Delete timer if exists + if timer_unit.exists(): + print(f"Removing systemd timer job-{name}.timer...") + r_timer = subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "timer-delete", name, "--keep-job"], + capture_output=True, text=True) + try: + tdata = json.loads(r_timer.stdout) + if not tdata.get("ok") and not force: + print(c_red(f"Error deleting timer: {tdata.get('error')}"), file=sys.stderr) + sys.exit(1) + except Exception: + pass + + # 2. Delete job file + if job_file.exists(): + print(f"Removing job definition jobs/{name}.json...") + r_job = subprocess.run(["python3", str(BIN_DIR / "box-ctl.py"), "job-delete", name, "--force"], + capture_output=True, text=True) + try: + jdata = json.loads(r_job.stdout) + if jdata.get("ok"): + print(c_green(f"āœ” Job '{name}' and its systemd units deleted successfully.")) + else: + print(c_red(f"Error deleting job: {jdata.get('error')}"), file=sys.stderr) + sys.exit(1) + except Exception: + print(r_job.stdout or r_job.stderr) + +def cmd_job_run(args): + name = args.name + follow = getattr(args, "follow", False) + + job_file = JOBS_DIR / f"{name}.json" + if not job_file.exists(): + print(c_red(f"Error: Job definition jobs/{name}.json not found."), file=sys.stderr) + sys.exit(1) + + print(f"Triggering job dispatch for '{c_bold(name)}'...") + + if not follow: + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "job-trigger", name] + res = subprocess.run(cmd) + sys.exit(res.returncode) + + # Follow mode: open log files and track from current end position + dm_pos = DM_LOG.stat().st_size if DM_LOG.exists() else 0 + job_pos = JOB_LOG.stat().st_size if JOB_LOG.exists() else 0 + + # Trigger job in background + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "job-trigger", name] + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + print(c_dim(f"Following execution logs for '{name}' (Press Ctrl+C to exit)...")) + + start_time = time.time() + dispatched = False + completed = False + + try: + while time.time() - start_time < 90: # 90s max follow + if proc.poll() is not None and not dispatched: + out_txt, err_txt = proc.communicate() + try: + pdata = json.loads(out_txt) + if not pdata.get("ok"): + print(c_red(f"Trigger failed: {pdata.get('error')}")) + return + else: + dispatched = True + except Exception: + pass + + if JOB_LOG.exists() and JOB_LOG.stat().st_size > job_pos: + with open(JOB_LOG, "r") as f: + f.seek(job_pos) + for line in f: + line = line.strip() + if not line: + continue + try: + d = json.loads(line) + if d.get("job_name") == name or name in str(d.get("job_id", "")): + ev = d.get("type") or d.get("event") or "event" + badge = c_green("DISPATCH") if "dispatched" in ev else ( + c_red("FAILED") if "fail" in ev else c_cyan(ev.upper()) + ) + print(f" {badge} [{c_bold(name)}] {d.get('target', '')} {d.get('error', '')}") + if "failed" in ev or "timeout" in ev: + completed = True + except Exception: + pass + job_pos = f.tell() + + if DM_LOG.exists() and DM_LOG.stat().st_size > dm_pos: + with open(DM_LOG, "r") as f: + f.seek(dm_pos) + for line in f: + line = line.strip() + if not line: + continue + try: + d = json.loads(line) + msg = d.get("msg") or d.get("error") or "" + if name in msg or "[RESULT" in msg or "[WO" in msg: + ev_type = d.get("type", "") + sender = d.get("agent", "") + to = d.get("to", "") + badge = c_green("VERIFIED") if ev_type == "verified" else c_cyan(ev_type.upper()) + print(f" {badge} DM {sender} -> {to}: {msg[:100]}") + if "[RESULT" in msg: + completed = True + except Exception: + pass + dm_pos = f.tell() + + if completed: + print(c_green(f"āœ” Job execution completed.")) + break + + time.sleep(0.5) + + except KeyboardInterrupt: + print("\n" + c_dim("Stopped following.")) + +def cmd_job_log(args): + n = getattr(args, "n", 20) + filter_name = getattr(args, "name", None) + + if not JOB_LOG.exists(): + print(c_dim("job-log.jsonl not found.")) + return + + entries = [] + with open(JOB_LOG, "r") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + d = json.loads(line) + if filter_name and d.get("job_name") != filter_name and filter_name not in d.get("job_id", ""): + continue + entries.append(d) + except Exception: + continue + + entries = entries[-n:] + if args.json: + print(json.dumps({"ok": True, "entries": entries}, indent=2)) + return + + print("\n" + c_bold(f"=== RECENT JOB ACTIVITY (last {len(entries)}) ===") + "\n") + headers = ["TIME", "EVENT", "JOB ID", "AGENT", "DETAIL"] + rows = [] + for item in entries: + t_rel = parse_relative_time(item.get("ts", "")) + ev = item.get("event", "-") + badge = c_green("DISPATCH") if "dispatched" in ev else c_dim(ev[:12]) + job_id = item.get("job_id", "-") + agent = item.get("agent", "-") + detail = item.get("target") or item.get("error") or "" + + rows.append([t_rel, badge, job_id[:25], agent, detail]) + + print_table(headers, rows) + print() + +# --------------------------------------------------------------------------- +# Domain: WEB (Google Cloud VM Web Surfaces Probe) +# --------------------------------------------------------------------------- +def cmd_web_health(args): + endpoints = [ + {"name": "Box Console (Front-Door)", "url": "https://box.muse-dev.online/"}, + {"name": "Board Service", "url": "https://board.muse-dev.online/"}, + {"name": "VM Reverse Proxy (sslip)", "url": "https://34-139-37-135.sslip.io/"}, + {"name": "Box Timers API", "url": "https://box.muse-dev.online/api/box/timers"}, + ] + + print("\n" + c_bold("=== GOOGLE CLOUD VM WEB SURFACES HEALTH (34.139.37.135) ===") + "\n") + + results = [] + headers = ["SURFACE", "ENDPOINT", "STATUS", "LATENCY", "SERVER / VIA"] + rows = [] + + for ep in endpoints: + t0 = time.time() + try: + req = urllib.request.Request( + ep["url"], + headers={"User-Agent": "super-cli/1.0 (bl; health-probe)"} + ) + with urllib.request.urlopen(req, timeout=4) as resp: + elapsed_ms = int((time.time() - t0) * 1000) + code = resp.status + via = resp.headers.get("Via") or resp.headers.get("Server") or "ok" + badge = badge_ok(f"{code}") + except urllib.error.HTTPError as e: + elapsed_ms = int((time.time() - t0) * 1000) + code = e.code + via = e.headers.get("Via") or e.headers.get("Server") or "-" + # 401/403 means auth works as intended + if code in (401, 403): + badge = badge_ok(f"{code} AUTH_GATED") + else: + badge = badge_warn(f"{code}") + except Exception as e: + elapsed_ms = None + code = "ERR" + via = str(e)[:25] + badge = badge_err("UNREACHABLE") + + lat_disp = f"{elapsed_ms}ms" if elapsed_ms is not None else "-" + rows.append([c_bold(ep["name"]), ep["url"], badge, lat_disp, via[:25]]) + results.append({"name": ep["name"], "url": ep["url"], "code": code, "latency_ms": elapsed_ms}) + + print_table(headers, rows) + print("\n" + c_dim(" Operator note: box.muse-dev.online requires PIN or Bearer session cookie.") + "\n") + + if args.json: + print(json.dumps({"ok": True, "results": results}, indent=2)) + +def cmd_web_test_auth(args): + print("\n" + c_bold("Testing Box API Authentication Gate:") + "\n") + url = "https://box.muse-dev.online/api/box/timers" + try: + req = urllib.request.Request(url, headers={"User-Agent": "super-cli/1.0"}) + with urllib.request.urlopen(req, timeout=4) as resp: + print(c_yellow(f"Unexpected: Endpoint responded without auth: HTTP {resp.status}")) + except urllib.error.HTTPError as e: + body = e.read().decode("utf-8", errors="ignore") + if e.code in (401, 403): + print(f" Endpoint: {url}") + print(f" Response: {badge_ok(f'HTTP {e.code}')} (Protection active)") + print(f" Body : {c_dim(body.strip()[:100])}\n") + else: + print(c_yellow(f"HTTP {e.code}: {body}")) + except Exception as e: + print(c_red(f"Error testing auth: {e}")) + +def cmd_web_sync(args): + print("\n" + c_bold("=== DATA SYNC INTEGRITY (bl -> VM) ===") + "\n") + files = [ + ("DM Log", DM_LOG), + ("Job Log", JOB_LOG), + ("Control Audit", CTL_LOG), + ] + headers = ["DATA STORE", "PATH", "SIZE", "RECORDS", "LAST MODIFIED"] + rows = [] + for label, path in files: + if path.exists(): + stat = path.stat() + size_kb = f"{stat.st_size / 1024:.1f} KB" + # Count lines + try: + with open(path, "r") as f: + lines = sum(1 for _ in f) + except Exception: + lines = 0 + mtime = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat() + mtime_rel = parse_relative_time(mtime) + rows.append([c_bold(label), str(path), size_kb, str(lines), mtime_rel]) + else: + rows.append([c_bold(label), str(path), "-", "0", c_dim("missing")]) + + print_table(headers, rows) + print() + +# --------------------------------------------------------------------------- +# Domain: CRED (Credentials & Onboarding Client) +# --------------------------------------------------------------------------- +def cmd_cred_initiate(args): + import cred_client + client = cred_client.CredClient() + res = client.initiate(args.node, args.email, service=getattr(args, "service", "muse"), account_name=getattr(args, "account_name", None)) + if getattr(args, "json", False): + print(json.dumps(res, indent=2)) + else: + st = res.get("status") + if st == "awaiting_otp": + print("\n" + c_yellow(f"[APPROVAL_NEEDED] Code sent to {args.email} for node '{args.node}'.") + "\n") + print(f" Submit OTP with: super cred submit-otp --node {args.node} --otp <code>\n") + sys.exit(2) + elif st == "active": + print("\n" + c_green(f"[SUCCESS] Node '{args.node}' is already authenticated and active.") + "\n") + else: + print(f"[{st.upper()}] {res.get('message') or res.get('detail')}") + sys.exit(res.get("code", 1)) + +def cmd_cred_submit_otp(args): + import cred_client + client = cred_client.CredClient() + res = client.submit_otp(args.node, args.otp, email=getattr(args, "email", None)) + if getattr(args, "json", False): + print(json.dumps(res, indent=2)) + else: + st = res.get("status") + if st == "active": + print("\n" + c_green(f"[SUCCESS] Node '{args.node}' successfully signed in!") + "\n") + else: + print(f"[{st.upper()}] {res.get('message') or res.get('detail')}") + sys.exit(res.get("code", 1)) + +def cmd_cred_status(args): + import cred_client + client = cred_client.CredClient() + res = client.status(args.node) + if getattr(args, "json", False): + print(json.dumps(res, indent=2)) + else: + print("\n" + c_bold(f"=== CRED STATUS: {args.node} ===") + "\n") + print(f" Node : {c_bold(res['node'])}") + print(f" Email : {res['email']}") + print(f" Status : {res['status']}") + print(f" CDP Port : {res['cdp_port'] or '-'}") + alive_b = badge_ok("YES") if res['session_alive'] else badge_warn("NO") + print(f" Session Alive: {alive_b}") + if res["detail"]: + print(f" Detail : {c_dim(res['detail'])}") + print() + +def cmd_cred_link_instagram(args): + import cred_client + client = cred_client.CredClient() + res = client.link_instagram(args.node, notify=getattr(args, "notify", False)) + if getattr(args, "json", False): + print(json.dumps(res, indent=2)) + else: + print("\n" + c_bold(f"=== INSTAGRAM LINKING: {args.node} ===") + "\n") + if res.get("error"): + print(c_red(f" Error: {res['error']}")) + sys.exit(1) + print(f" Tailscale Portal: {c_cyan(res['portal_url'])}") + print(f" Direct IP Portal: {c_cyan(res['portal_ip_url'])}") + oauth_preview = res['direct_oauth_url'][:75] + "..." if res.get('direct_oauth_url') else "-" + print(f" OAuth URL : {c_dim(oauth_preview)}") + if getattr(args, "notify", False): + n_badge = badge_ok("SENT") if res['email_notified'] else badge_err("FAILED") + print(f" Email Alert : {n_badge}") + print("\n" + c_yellow(" → Tap either Tailscale link from your phone/browser to complete Meta age verification.") + "\n") + +def cmd_cred_meta_audit(args): + import cred_client + client = cred_client.CredClient() + res = client.audit_meta(args.node) + if getattr(args, "json", False): + print(json.dumps(res, indent=2)) + else: + print("\n" + c_bold(f"=== META ACCOUNTS CENTER AUDIT: {args.node} ===") + "\n") + if res.get("error"): + print(c_red(f" Error: {res['error']}")) + sys.exit(1) + email_str = res.get('email') or c_dim('(none / phone-only)') + print(f" Meta Account Email: {c_cyan(email_str)}") + profiles = res.get("profiles", []) + print(f" Linked Profiles ({len(profiles)}):") + for p_info in profiles: + p_type = p_info.get("type", "unknown") + p_name = p_info.get("name", "unknown") + badge = c_green(f"[{p_type}]") if p_type == "instagram" else c_magenta(f"[{p_type}]") + print(f" - {badge} {c_bold(p_name)}") + print() + +def cmd_cred_list(args): + import cred_client + client = cred_client.CredClient() + items = client.list_all() + if getattr(args, "json", False): + print(json.dumps(items, indent=2)) + else: + print("\n" + c_bold("=== FLEET CREDENTIAL & ONBOARDING STATUS ===") + "\n") + headers = ["NODE", "STATUS", "CDP", "ALIVE", "EMAIL"] + rows = [] + for it in items: + alive_b = badge_ok("YES") if it["session_alive"] else badge_warn("NO") + rows.append([c_bold(it['node']), it['status'], str(it['cdp_port'] or '-'), alive_b, it['email']]) + print_table(headers, rows) + print() + + +# --------------------------------------------------------------------------- +# Domain: HARVEST (Response & Readback Harvester) +# --------------------------------------------------------------------------- +def cmd_harvest_run(args): + cmd = [sys.executable, str(RESPONSE_HARVESTER_PY), "--once"] + if getattr(args, "agent", None): + cmd.extend(["--agent", args.agent]) + if getattr(args, "dry_run", False): + cmd.append("--dry-run") + if getattr(args, "json", False): + cmd.append("--json") + try: + res = subprocess.run(cmd, capture_output=True, text=True) + if getattr(args, "json", False): + print(res.stdout.strip()) + else: + if res.stdout.strip(): + print(res.stdout.strip()) + if res.stderr.strip(): + print(c_yellow(res.stderr.strip()), file=sys.stderr) + except Exception as e: + print(c_red(f"Error running harvest: {e}"), file=sys.stderr) + sys.exit(1) + +def cmd_harvest_status(args): + watermarks = {} + if WATERMARKS_FILE.exists(): + try: + with open(WATERMARKS_FILE, "r") as f: + watermarks = json.load(f) + except Exception: + pass + + sidechats = {} + if JOB_SIDECHATS_FILE.exists(): + try: + with open(JOB_SIDECHATS_FILE, "r") as f: + sidechats = json.load(f) + except Exception: + pass + + latest_seen = {} + if CHAT_HISTORY_LOG.exists(): + try: + with open(CHAT_HISTORY_LOG, "r") as f: + for line in f: + if not line.strip(): continue + try: + rec = json.loads(line) + k = f"{rec.get('agent')}:{rec.get('thread_id')}" + latest_seen[k] = rec.get("ts") + except Exception: + continue + except Exception: + pass + + if getattr(args, "json", False): + print(json.dumps({ + "watermarks": watermarks, + "sidechats": sidechats, + "latest_events": latest_seen + }, indent=2)) + return + + now_str = datetime.now().strftime("%H:%M:%S") + print(f"\n=== RESPONSE HARVESTER STATUS === ({now_str} local)\n") + + headers = ["AGENT", "THREAD / ALIAS", "ID", "WATERMARK", "LAST HARVESTED", "STATUS"] + rows = [] + + for agent in VALID_NODES: + wm_main = watermarks.get(f"{agent}:main", "-") + last_ts = latest_seen.get(f"{agent}:main") + status_b = badge_ok("ACTIVE") if wm_main != "-" else badge_dim("IDLE") + rows.append([ + c_bold(agent), + "Main Chat", + c_dim("main"), + wm_main[:16] if wm_main != "-" else c_dim("-"), + parse_relative_time(last_ts) if last_ts else c_dim("none"), + status_b + ]) + + for key, val in sidechats.items(): + if key.startswith("_"): continue + if isinstance(val, dict): + tid = val.get("thread_uuid") or val.get("uuid") + a = val.get("agent", "opm") + elif isinstance(val, str): + tid = val + a = "opm" + else: + continue + + if a == agent and tid: + wm_sc = watermarks.get(f"{agent}:{tid}", "-") + last_sc_ts = latest_seen.get(f"{agent}:{tid}") + status_sc = badge_ok("ACTIVE") if wm_sc != "-" else badge_dim("IDLE") + rows.append([ + c_dim(f" └─ {agent}"), + key, + c_dim(tid[:8] + "…"), + wm_sc[:16] if wm_sc != "-" else c_dim("-"), + parse_relative_time(last_sc_ts) if last_sc_ts else c_dim("none"), + status_sc + ]) + + print_table(headers, rows) + print(c_dim("\n Commands: super harvest run | super harvest tail\n")) + +def _print_harvest_line(line: str, filter_text: str = None): + try: + e = json.loads(line) + text = e.get("text", "") + if filter_text and filter_text.lower() not in text.lower(): + return + ts = e.get("ts", "")[11:19] + agent = e.get("agent", "unknown") + author = e.get("author", "unknown") + thread = e.get("thread_name") or e.get("thread_id", "main") + + auth_color = c_green if author == "assistant" else c_cyan + snippet = text.replace("\n", " ")[:120] + print(f" {c_dim(ts)} {c_bold(f'[{agent}:{thread}]')} {auth_color(author)}: {snippet}") + except Exception: + pass + +def cmd_harvest_tail(args): + if not CHAT_HISTORY_LOG.exists(): + print(c_dim(f"Chat history log empty ({CHAT_HISTORY_LOG})")) + return + print(c_bold(f"=== TAILING CHAT HARVEST STREAM ({CHAT_HISTORY_LOG.name}) === (Ctrl+C to stop)") + "\n") + try: + with open(CHAT_HISTORY_LOG, "r") as f: + lines = f.readlines() + for line in lines[-15:]: + _print_harvest_line(line, getattr(args, "filter", None)) + + f.seek(0, os.SEEK_END) + while True: + line = f.readline() + if line: + _print_harvest_line(line, getattr(args, "filter", None)) + else: + time.sleep(0.5) + except KeyboardInterrupt: + print() + +# --------------------------------------------------------------------------- +# Domain: FOLLOWUP (Deadline Tracking & Nudges) +# --------------------------------------------------------------------------- +def cmd_followup_list(args): + followups = {} + if FOLLOWUPS_FILE.exists(): + try: + with open(FOLLOWUPS_FILE, "r") as f: + followups = json.load(f) + except Exception: + pass + + if getattr(args, "json", False): + print(json.dumps(followups, indent=2)) + return + + now_str = datetime.now().strftime("%H:%M:%S") + print(f"\n=== FOLLOW-UP TRACKING === ({now_str} local)\n") + + headers = ["DM ID", "ROUTE", "TARGET", "NUDGES", "DEADLINE", "STATUS", "SNIPPET"] + rows = [] + now = datetime.now(timezone.utc) + + for dm_id, rec in sorted(followups.items(), key=lambda x: x[1].get("sent_at", ""), reverse=True): + st = rec.get("status", "pending") + if not getattr(args, "all", False) and st not in ("pending", "escalated"): + continue + + route = f"{rec.get('sender', '?')} → {c_bold(rec.get('recipient', '?'))}" + target = rec.get("target", "main") + if len(target) > 18: + target = target[:17] + "…" + nudges = f"{rec.get('nudges_sent', 0)}/{rec.get('nudges_allowed', 2)}" + + deadline_str = rec.get("deadline") + deadline_display = c_dim("-") + if deadline_str and st == "pending": + try: + dl_dt = datetime.fromisoformat(deadline_str.replace("Z", "+00:00")) + diff = int((dl_dt - now).total_seconds()) + if diff > 0: + deadline_display = c_green(f"in {diff // 60}m") + else: + deadline_display = c_red(f"{abs(diff) // 60}m overdue") + except Exception: + deadline_display = deadline_str[:16] + + if st == "pending": + status_badge = badge_warn("PENDING") + elif st == "escalated": + status_badge = badge_err("ESCALATED") + elif st == "resolved": + status_badge = badge_ok("RESOLVED") + else: + status_badge = badge_dim(st.upper()) + + snippet = rec.get("prompt_snippet") or rec.get("resolved_snippet") or "" + snippet = snippet.replace("\n", " ")[:35] + + rows.append([ + c_bold(dm_id[:8]), + route, + target, + nudges, + deadline_display, + status_badge, + c_dim(snippet) + ]) + + print_table(headers, rows) + if not getattr(args, "all", False): + print(c_dim("\n (Showing pending & escalated only; use --all to view resolved records)")) + print(c_dim(" Commands: super followup sweep | super followup cancel <dm_id>\n")) + +def cmd_followup_sweep(args): + cmd = [sys.executable, str(FOLLOWUP_SWEEPER_PY), "--once"] + if getattr(args, "dry_run", False): + cmd.append("--dry-run") + try: + res = subprocess.run(cmd, capture_output=True, text=True) + if res.stdout.strip(): + print(res.stdout.strip()) + if res.stderr.strip(): + print(c_yellow(res.stderr.strip()), file=sys.stderr) + except Exception as e: + print(c_red(f"Error running sweeper: {e}"), file=sys.stderr) + sys.exit(1) + +def cmd_followup_cancel(args): + if not FOLLOWUPS_FILE.exists(): + print(c_red("No follow-ups found."), file=sys.stderr) + sys.exit(1) + try: + with open(FOLLOWUPS_FILE, "r") as f: + data = json.load(f) + except Exception as e: + print(c_red(f"Error reading follow-ups: {e}"), file=sys.stderr) + sys.exit(1) + + target_id = args.dm_id.strip() + match_key = None + for k in data.keys(): + if k == target_id or k.startswith(target_id): + match_key = k + break + + if not match_key: + print(c_red(f"Follow-up ID '{target_id}' not found."), file=sys.stderr) + sys.exit(1) + + data[match_key]["status"] = "canceled" + data[match_key]["canceled_at"] = datetime.now(timezone.utc).isoformat() + tmp_path = f"{FOLLOWUPS_FILE}.tmp.{os.getpid()}" + with open(tmp_path, "w") as f: + json.dump(data, f, indent=2) + os.replace(tmp_path, FOLLOWUPS_FILE) + print(c_green(f"ā— Cancelled follow-up {match_key}.")) + + # Also stand down the VM-side nudge controller (request-sweeper.py), + # which tracks dm_followup records independently of bl's local file. + # Best-effort: the local cancel already succeeded, so a VM failure + # (unreachable, no record, bad response) must not fail the command. + _vm_followup_cancel(match_key) + + +def _vm_followup_cancel(dm_id): + """POST /api/box/followups/cancel so the VM-side sweeper stops nudging. + + Best-effort: never raises. Prints a one-line outcome. + """ + import json as _json + import os as _os + import subprocess as _sp + import time as _time + import urllib.parse as _up + import urllib.request as _ur + import urllib.error as _ue + + box_api = _os.environ.get("BOX_API_BASE", "https://box.muse-dev.online") + key = _os.environ.get("BOX_SIGN_KEY", + _os.path.expanduser("~/.ssh/id_ed25519")) + endpoint = "followups/cancel" + ts_now = str(int(_time.time())) + payload = ("%s\n%s" % (ts_now, endpoint)).encode() + try: + pr = _sp.run(["ssh-keygen", "-Y", "sign", "-f", key, "-n", "box"], + input=payload, capture_output=True, timeout=15) + sig = pr.stdout.decode().strip() + if not sig: + raise RuntimeError("empty signature") + except Exception as e: + print(c_yellow(" VM follow-up cancel skipped (signing failed: %s)" % e)) + return + query = _up.urlencode({"identity": "bl", "ts": ts_now, "sig": sig}) + url = "%s/api/box/followups/cancel?%s" % (box_api, query) + try: + req = _ur.Request(url, + data=_json.dumps({"dm_id": dm_id}).encode(), + headers={"Content-Type": "application/json", + "User-Agent": "super-cli/1.0 (bl)"}, + method="POST") + with _ur.urlopen(req, timeout=30) as r: + resp = _json.load(r) + except _ue.HTTPError as e: + if e.code == 404: + print(c_dim(" VM: no dm_followup record (bl-only follow-up).")) + else: + try: + detail = e.read().decode("utf-8", errors="ignore")[:120] + except Exception: + detail = "" + print(c_yellow(" VM cancel failed: HTTP %s %s" % (e.code, detail))) + return + except Exception as e: + print(c_yellow(" VM cancel failed: %s" % e)) + return + if resp.get("canceled"): + print(c_green(" VM: follow-up canceled (%s)." + % resp.get("request_id"))) + else: + print(c_dim(" VM: %s." % resp.get("reason", "not canceled"))) + + +# --------------------------------------------------------------------------- +# Domain: PIPELINE (Multi-Agent Workflow Pipelines) +# --------------------------------------------------------------------------- + +def cmd_pipeline_run(args): + name = args.name.strip() + job_file = JOBS_DIR / f"{name}.json" + if not job_file.exists(): + print(c_red(f"Error: Pipeline root job '{name}.json' not found in jobs/"), file=sys.stderr) + sys.exit(1) + + import pipeline_engine + run_id = pipeline_engine.generate_pipeline_run_id(name) + entry = pipeline_engine.create_pipeline(name, run_id=run_id) + target = entry.get("target") + + print(c_bold(f"\n=== LAUNCHING MULTI-AGENT PIPELINE [{name}] ===")) + print(f" Run ID: {c_cyan(run_id)}") + print(f" Target: {c_yellow(target)}") + print(f" Step 1: {c_bold(name)}\n") + + cmd = [sys.executable, str(JOB_DISPATCH_PY), name, "--pipeline-run", run_id, "--step-n", "1"] + if getattr(args, "dry_run", False): + cmd.append("--dry-run") + + res = subprocess.run(cmd) + if res.returncode == 0: + print(c_green(f"\nāœ” Pipeline '{run_id}' dispatched step 1 successfully.")) + print(c_dim(" Track with: super pipeline status | super pipeline history\n")) + else: + print(c_red(f"\nāœ– Step 1 dispatch failed with code {res.returncode}"), file=sys.stderr) + sys.exit(res.returncode) + + +def cmd_pipeline_status(args): + import pipeline_engine + active = pipeline_engine.list_active_pipelines() + + if getattr(args, "json", False): + print(json.dumps(active, indent=2)) + return + + now_str = datetime.now().strftime("%H:%M:%S") + print(f"\n=== ACTIVE MULTI-AGENT PIPELINES === ({now_str} local)\n") + + if not active: + print(c_dim(" (no active pipeline runs currently executing)")) + print(c_dim(" Launch one with: super pipeline run <job_name>\n")) + return + + headers = ["RUN ID", "PIPELINE", "TARGET", "STEP", "CURRENT AGENT", "ACTIVE JOB ID", "STARTED", "STATUS"] + rows = [] + + for p in active: + run_id = p.get("run_id", "-") + p_name = p.get("pipeline_name", "-") + target = p.get("target", "-") + cur_step_n = p.get("current_step", 1) + steps = p.get("steps", []) + last_step = steps[-1] if steps else {} + agent = last_step.get("agent", "-") + job_id = last_step.get("job_id", "-") + created_at = p.get("created_at") + + rows.append([ + c_bold(run_id), + p_name, + c_yellow(target), + f"Step {cur_step_n}", + c_cyan(agent), + c_dim(job_id[:16] + "…") if len(job_id) > 16 else job_id, + parse_relative_time(created_at) if created_at else c_dim("-"), + badge_ok("RUNNING"), + ]) + + print_table(headers, rows) + + # Details on steps for active runs + for p in active: + print(c_bold(f"\n Active Pipeline Details [{p.get('run_id')}]:")) + for s in p.get("steps", []): + st = s.get("status", "unknown") + st_badge = badge_ok("COMPLETED") if st == "completed" else badge_warn("RUNNING") if st == "dispatched" else badge_err(st.upper()) + res_snippet = s.get("result", "") + res_disp = f" → {c_dim(res_snippet[:70])}" if res_snippet else "" + print(f" • Step {s.get('step_n')}: {c_bold(s.get('job_name'))} ({s.get('agent')}) [{st_badge}]{res_disp}") + + print(c_dim("\n Commands: super pipeline run <name> | super pipeline history\n")) + + +def cmd_pipeline_history(args): + import pipeline_engine + limit = getattr(args, "limit", 20) or 20 + history = pipeline_engine.list_pipeline_history(limit=limit) + + if getattr(args, "json", False): + print(json.dumps(history, indent=2)) + return + + now_str = datetime.now().strftime("%H:%M:%S") + print(f"\n=== PIPELINE RUN HISTORY (Recent {len(history)}) === ({now_str} local)\n") + + if not history: + print(c_dim(" (no pipeline history found)")) + return + + headers = ["RUN ID", "PIPELINE", "TARGET", "STEPS", "STARTED", "COMPLETED", "STATUS"] + rows = [] + + for p in history: + run_id = p.get("run_id", "-") + p_name = p.get("pipeline_name", "-") + target = p.get("target", "-") + steps = p.get("steps", []) + step_summary = f"{len(steps)} step{'s' if len(steps) != 1 else ''}" + created_at = p.get("created_at") + completed_at = p.get("completed_at") + st = p.get("status", "unknown") + + if st == "completed": + badge = badge_ok("COMPLETED") + elif st == "running": + badge = badge_warn("RUNNING") + else: + badge = badge_err("FAILED") + + rows.append([ + c_bold(run_id), + p_name, + c_yellow(target), + step_summary, + parse_relative_time(created_at) if created_at else c_dim("-"), + parse_relative_time(completed_at) if completed_at else c_dim("active"), + badge, + ]) + + print_table(headers, rows) + print(c_dim("\n Commands: super pipeline status | super pipeline run <name>\n")) + + +def cmd_pipeline_stop(args): + import pipeline_engine + run_id = args.run_id.strip() + reason = getattr(args, "reason", "cancelled_by_operator") or "cancelled_by_operator" + entry = pipeline_engine.stop_pipeline(run_id, reason=reason) + if not entry: + print(c_red(f"Error: Pipeline run '{run_id}' not found."), file=sys.stderr) + sys.exit(1) + print(c_green(f"āœ” Pipeline run '{entry.get('run_id')}' stopped/canceled.")) + if getattr(args, "json", False): + print(json.dumps(entry, indent=2)) + + +def cmd_pipeline_prune(args): + import pipeline_engine + max_age = getattr(args, "max_age", 1) or 1 + pruned = pipeline_engine.prune_pipelines(max_age_hours=max_age) + if pruned: + print(c_green(f"āœ” Pruned {len(pruned)} stale pipeline runs (> {max_age}h): {', '.join(pruned)}")) + else: + print(c_dim(f"āœ” No stale running pipelines found (> {max_age}h).")) + if getattr(args, "json", False): + print(json.dumps({"pruned": pruned, "count": len(pruned)}, indent=2)) + + +# --------------------------------------------------------------------------- +# Domain: DEPLOY (Multi-Agent Pipelines, Subagent Spawning, and Oversight) +# --------------------------------------------------------------------------- + +def cmd_deploy(args): + action = getattr(args, "action", None) + + if action == "subagent": + agent = args.agent + title = args.title or "subagent-task" + prompt = args.prompt + wait_val = getattr(args, "wait", 30) + wait = 30 if wait_val is None else int(wait_val) + + print(c_bold(f"\n=== DEPLOYING SUBAGENT ON [{agent}] ===")) + print(f" Agent: {c_cyan(agent)}") + print(f" Title: {c_yellow(title)}") + print(f" Prompt: {c_dim(prompt[:90])}...\n") + + import muse_hybrid + res, err = muse_hybrid.start_session(agent, title=title) + if err or not res: + print(c_red(f"āœ– Failed to spawn subagent session: {err}"), file=sys.stderr) + sys.exit(1) + + session_id = res.get("session_id") + print(c_green(f"āœ” Subagent session spawned: {session_id}")) + + try: + import subagent_tracker + subagent_tracker.register_session(agent, session_id, title=title, prompt=prompt) + except Exception: + pass + + if prompt: + print(f" Dispatching task prompt to subagent (waiting up to {wait}s)...") + send_res, send_err = muse_hybrid.send_message(agent, prompt, thread_id=session_id, wait=wait) + if send_err: + print(c_yellow(f"Notice: {send_err}")) + else: + print(c_green("āœ” Task prompt delivered.")) + + # Fetch fresh history to show assistant's initial work + msgs, _ = muse_hybrid.get_history(agent, thread_id=session_id, limit=5) + if msgs: + for m in msgs: + role = m.get("role") + if role == "assistant": + print(f"\n{c_bold('--- Subagent Response ---')}\n{m.get('text')}\n") + + print(f" Oversee anytime with: {c_cyan(f'box thread view {agent} {session_id}')}\n") + return + + # Pipeline deployment + pipe_name = getattr(args, "name", None) + if not pipe_name: + print(c_red("Error: Specify pipeline name (e.g. box deploy pipeline pipe-demo-step1)"), file=sys.stderr) + sys.exit(1) + + args.name = pipe_name + cmd_pipeline_run(args) + + +# --------------------------------------------------------------------------- +# Domain: LOOP (Intrinsic Loop Strategy, Health, Break Taxonomy, and Control) +# --------------------------------------------------------------------------- + +def cmd_loop_status(args): + agent = getattr(args, "agent", None) + status_filter = getattr(args, "status", None) + limit = getattr(args, "n", 30) + + from gravity import reconstruct_loops + loops = reconstruct_loops(limit=limit, agent=agent, status_filter=status_filter) + + if getattr(args, "json", False): + print(json.dumps({"ok": True, "loops": loops, "count": len(loops)}, indent=2)) + return + + prog = Path(sys.argv[0]).name if sys.argv and sys.argv[0] else "super" + if prog.endswith(".py"): + prog = "super" + + print("\n" + c_bold(f"=== INTRINSIC LOOPS ({len(loops)} tracked) ===") + "\n") + headers = ["LOOP ID", "AGENT", "TARGET", "PURPOSE", "STATE", "AGE", "DEADLINE", "NUDGES", "DETAIL"] + rows = [] + now = datetime.now(timezone.utc) + + for l in loops: + lid = l.get("loop_id", "-")[:8] + ag = l.get("agent", "-") + tgt = l.get("target", "main") + if len(tgt) > 16: + tgt = tgt[:14] + ".." + purp = l.get("purpose", "-") + st = l.get("state", "LANDED") + if st == "FIRING": + st_badge = c_cyan("ā—‹ FIRING") + elif st == "LANDED": + st_badge = c_blue("ā— LANDED") + elif st == "SEEN": + st_badge = c_yellow("ā— SEEN") + elif st in ("ANSWERED", "CLOSED"): + st_badge = badge_ok(st) + elif st == "NUDGED": + st_badge = badge_warn("NUDGED") + elif st == "ESCALATED": + st_badge = badge_err("ESCALATED") + elif st == "BROKEN": + st_badge = badge_err("BROKEN") + else: + st_badge = badge_dim(st) + + sent_rel = parse_relative_time(l.get("sent_at", "")) + dl_str = l.get("deadline", "") + if dl_str: + try: + dl_dt = datetime.fromisoformat(dl_str.replace("Z", "+00:00")) + diff_s = int((dl_dt - now).total_seconds()) + if diff_s > 0: + dl_disp = c_green(f"in {diff_s // 60}m") + else: + dl_disp = c_red(f"{abs(diff_s) // 60}m ago") + except Exception: + dl_disp = dl_str[:10] + else: + dl_disp = c_dim("-") + + nudges = f"{l.get('nudges_sent', 0)}/{l.get('nudges_allowed', 2)}" + detail = l.get("summary", "")[:30] + + rows.append([c_bold(lid), ag, tgt, purp, st_badge, sent_rel, dl_disp, nudges, c_dim(detail)]) + + print_table(headers, rows) + print(c_dim(f"\n Commands: {prog} loop health | {prog} loop breaks | {prog} loop strat | {prog} loop vars | {prog} loop close <id>\n")) + + +def cmd_loop_health(args): + threshold = getattr(args, "threshold", None) + from gravity import get_fleet_loop_health + data = get_fleet_loop_health(threshold=threshold) + + if getattr(args, "json", False): + print(json.dumps({"ok": True, **data}, indent=2)) + return + + summary = data.get("summary", {}) + t_val = summary.get("threshold", 0.5) + + print("\n" + c_bold(f"=== INTRINSIC FLEET LOOP HEALTH (Threshold: {t_val:.2f}) ===") + "\n") + headers = ["AGENT", "STATUS", "HEALTH RATIO", "LANDED", "ANSWERED", "VERDICT"] + rows = [] + + for ag, info in sorted(data.get("agents", {}).items()): + st = info.get("status", "IDLE") + if st == "HEALTHY": + badge = badge_ok("HEALTHY") + verd = c_green("PASS") + elif st == "DEGRADED": + badge = badge_err("DEGRADED") + verd = c_red("FAIL") + else: + badge = badge_dim("IDLE") + verd = c_dim("NO TRAFFIC") + + hpct = info.get("health_pct", "-") + landed = str(info.get("landed", 0)) + answered = str(info.get("answered", 0)) + rows.append([c_bold(ag), badge, hpct, landed, answered, verd]) + + print_table(headers, rows) + ov = summary.get("overall_health_pct", "-") + tot_l = summary.get("total_landed", 0) + tot_a = summary.get("total_answered", 0) + fleet_st = c_green("HEALTHY") if summary.get("healthy") else c_red("DEGRADED") + print(f"\n {c_bold('Overall Fleet Health')}: {ov} ({tot_a}/{tot_l} loops closed/answered) — {fleet_st}\n") + + +def cmd_loop_breaks(args): + from gravity import diagnose_breaks + breaks = diagnose_breaks() + + if getattr(args, "json", False): + print(json.dumps({"ok": True, "breaks": breaks, "count": len(breaks)}, indent=2)) + return + + if not breaks: + print("\n" + c_green("āœ” All intrinsic loops and tracking mechanisms healthy. No breaks detected.") + "\n") + return + + print("\n" + c_bold(f"=== INTRINSIC LOOP BREAK DIAGNOSTICS ({len(breaks)} issues) ===") + "\n") + headers = ["SEVERITY", "BREAK TYPE", "COMPONENT / AGENT", "DIAGNOSIS", "REMEDIATION"] + rows = [] + for b in breaks: + sev = badge_err(b.get("severity", "CRITICAL")) if b.get("severity") == "CRITICAL" else badge_warn("WARNING") + btype = b.get("type", "-") + target = b.get("component") or b.get("agent") or "-" + diag = b.get("detail", "") + rem = b.get("remedy", "") + rows.append([sev, c_bold(btype), target, diag, c_dim(rem)]) + print_table(headers, rows) + print() + + +def cmd_loop_strat(args): + subact = getattr(args, "strat_action", None) or "show" + from modulate import get_all_strategies, set_strategy_override, reset_strategy_override, modulate, render_tags, InputType + + prog = Path(sys.argv[0]).name if sys.argv and sys.argv[0] else "super" + if prog.endswith(".py"): + prog = "super" + + if subact == "show": + strats = get_all_strategies() + filter_type = getattr(args, "type", None) + if filter_type: + strats = [s for s in strats if s.get("input_type") == filter_type.lower()] + + if getattr(args, "json", False): + print(json.dumps({"ok": True, "strategies": strats}, indent=2)) + return + + print("\n" + c_bold("=== INTRINSIC LOOP STRATEGY MATRIX (MODULATION) ===") + "\n") + headers = ["INPUT TYPE", "SUBTYPE", "AGENT", "TRACK", "PRIORITY", "TIMEOUT", "NUDGES", "ESCALATE", "SOURCE"] + rows = [] + for s in strats: + itype = s.get("input_type", "") + sub = s.get("subtype") or "-" + ag = s.get("agent") or "-" + tr = str(s.get("track", "-")) + prio = s.get("priority", "normal") + if prio == "critical": + prio_disp = c_red("CRITICAL") + elif prio == "important": + prio_disp = c_yellow("IMPORTANT") + elif prio == "routine": + prio_disp = c_dim("ROUTINE") + else: + prio_disp = c_cyan(prio.upper()) + + timeout = f"{s.get('timeout_s', 0)}s" + nudges = str(s.get("nudges", 0)) + esc = s.get("escalate") or "-" + src = c_yellow("OVERRIDE") if s.get("is_override") else c_dim("BUILTIN") + + rows.append([c_bold(itype), sub, ag, tr, prio_disp, timeout, nudges, esc, src]) + + print_table(headers, rows) + print(c_dim(f"\n Modify: {prog} loop strat set <type> [--subtype S] [--agent A] [--priority P] [--timeout S] [--nudges N] [--escalate AGENT]\n")) + + elif subact == "set": + itype = args.type + sub = getattr(args, "subtype", None) + agent = getattr(args, "agent", None) + res = set_strategy_override( + itype, sub, agent, + track=getattr(args, "track", None), + priority=getattr(args, "priority", None), + timeout_s=getattr(args, "timeout", None), + nudges=getattr(args, "nudges", None), + escalate=getattr(args, "escalate", None), + by="super" + ) + target_desc = f"{itype}:{sub or '*'}:{agent or '*'}" if agent else f"{itype}:{sub or '*'}" + print(c_green(f"āœ” Updated loop strategy override for '{target_desc}'.")) + if getattr(args, "json", False): + print(json.dumps({"ok": True, "strategy": res}, indent=2)) + + elif subact == "reset": + itype = args.type + sub = getattr(args, "subtype", None) + agent = getattr(args, "agent", None) + ok = reset_strategy_override(itype, sub, agent, by="super") + target_desc = f"{itype}:{sub or '*'}:{agent or '*'}" if agent else f"{itype}:{sub or '*'}" + if ok: + print(c_green(f"āœ” Reset loop strategy override for '{target_desc}' to default.")) + else: + print(c_yellow(f"No override was active for '{target_desc}'.")) + + elif subact == "eval": + itype_str = args.type + sub = getattr(args, "subtype", None) + agent = getattr(args, "agent", None) + actionable = getattr(args, "actionable", False) + try: + itype = InputType(itype_str.lower()) + except ValueError: + itype = InputType.MANUAL + pol = modulate(itype, sub.upper() if sub else None, agent=agent, actionable=actionable) + if getattr(args, "json", False): + print(json.dumps({"ok": True, "policy": pol.__dict__ if pol else None}, indent=2)) + return + if pol: + print(f"\n{c_bold('Resolved Policy:')}") + print(f" Input Type : {pol.input_type.value}") + print(f" Subtype : {pol.subtype or '-'}") + print(f" Agent Scope: {agent or '-'}") + print(f" Priority : {pol.priority.value}") + print(f" Timeout : {pol.timeout_s}s") + print(f" Max Nudges : {pol.nudges}") + print(f" Escalate To: {pol.escalate or '-'}") + print(f" Rendered Tags: {c_cyan(render_tags(pol))}\n") + else: + print(f"\n{c_dim('Policy: Non-tracked input (no follow-up record created).')}\n") + + +def cmd_loop_vars(args): + subact = getattr(args, "vars_action", None) or "list" + from variables import Variables + v = Variables() + + prog = Path(sys.argv[0]).name if sys.argv and sys.argv[0] else "super" + if prog.endswith(".py"): + prog = "super" + + if subact == "list": + if getattr(args, "json", False): + print(json.dumps({"ok": True, "variables": v.all(), "schemas": {n: v.schema(n) for n in v.names()}}, indent=2)) + return + + names = v.names() + print("\n" + c_bold(f"=== RUNTIME CONTROL VARIABLES ({len(names)} registered) ===") + "\n") + headers = ["VARIABLE NAME", "VALUE", "DEFAULT", "UNIT", "RANGE", "DESCRIPTION"] + rows = [] + + for name in names: + val = v.get(name) + spec = v.schema(name) + d_val = spec.get("default") + unit = spec.get("unit", "-") + min_v = spec.get("min") + max_v = spec.get("max") + rng = f"{min_v}..{max_v}" if min_v is not None and max_v is not None else "-" + desc = spec.get("description", "")[:40] + + val_str = str(val) + if val != d_val: + val_disp = c_cyan(f"{val_str} *") + else: + val_disp = val_str + + rows.append([c_bold(name), val_disp, str(d_val), unit, rng, c_dim(desc)]) + + print_table(headers, rows) + print(c_dim(f"\n Adjust: {prog} loop vars set <name> <value> | Reset: {prog} loop vars reset <name> (* = modified)\n")) + + elif subact == "get": + name = args.name + try: + val = v.get(name) + spec = v.schema(name) + if getattr(args, "json", False): + print(json.dumps({"ok": True, "name": name, "value": val, "schema": spec}, indent=2)) + return + print(f"\n{c_bold('Variable: ' + name)}") + print(f" Current Value : {c_cyan(str(val))}") + print(f" Default Value : {spec.get('default')}") + print(f" Type : {spec.get('type')}") + print(f" Unit : {spec.get('unit', '-')}") + if "min" in spec and "max" in spec: + print(f" Allowed Range : {spec.get('min')} .. {spec.get('max')}") + print(f" Description : {spec.get('description')}\n") + except Exception as e: + print(c_red(f"Error: {e}"), file=sys.stderr) + sys.exit(1) + + elif subact == "set": + name = args.name + val_raw = args.value + try: + spec = v.schema(name) + vtype = spec.get("type", "str") + if vtype == "int": + val = int(val_raw) + elif vtype == "float": + val = float(val_raw) + elif vtype == "bool": + val = val_raw.lower() in ("true", "1", "yes") + else: + val = val_raw + new_val = v.set(name, val, by="super") + print(c_green(f"āœ” Variable '{name}' updated to {new_val}.")) + if getattr(args, "json", False): + print(json.dumps({"ok": True, "name": name, "value": new_val}, indent=2)) + except Exception as e: + print(c_red(f"Error setting variable: {e}"), file=sys.stderr) + sys.exit(1) + + elif subact == "reset": + name = args.name + try: + def_val = v.reset(name, by="super") + print(c_green(f"āœ” Variable '{name}' reset to default ({def_val}).")) + if getattr(args, "json", False): + print(json.dumps({"ok": True, "name": name, "value": def_val}, indent=2)) + except Exception as e: + print(c_red(f"Error resetting variable: {e}"), file=sys.stderr) + sys.exit(1) + + elif subact == "history": + name = getattr(args, "name", None) + limit = getattr(args, "limit", 20) + entries = v.history(name=name, limit=limit) + if getattr(args, "json", False): + print(json.dumps({"ok": True, "history": entries, "count": len(entries)}, indent=2)) + return + if not entries: + print("\n" + c_dim(" (no variable change history found)") + "\n") + return + print("\n" + c_bold(f"=== VARIABLE MODIFICATION AUDIT HISTORY ({len(entries)} entries) ===") + "\n") + headers = ["TIMESTAMP", "VARIABLE", "ACTION", "OLD VALUE", "NEW VALUE", "CHANGED BY"] + rows = [] + for e in entries: + act = e.get("action", "") + if act == "set": + act_badge = c_cyan("SET") + elif act == "reset": + act_badge = c_yellow("RESET") + elif act == "rollback": + act_badge = c_magenta("ROLLBACK") + else: + act_badge = act + rows.append([ + e.get("ts", "")[:19].replace("T", " "), + c_bold(e.get("name", "")), + act_badge, + str(e.get("old_value", "-")), + c_bold(str(e.get("new_value", "-"))), + e.get("by", "-") + ]) + print_table(headers, rows) + print(c_dim(f"\n Rollback: {prog} loop vars rollback <name> [--revision REV]\n")) + + elif subact == "rollback": + name = args.name + rev = getattr(args, "revision", None) + try: + new_val = v.rollback(name, revision=rev, by="super") + print(c_green(f"āœ” Variable '{name}' rolled back to {new_val}.")) + if getattr(args, "json", False): + print(json.dumps({"ok": True, "name": name, "value": new_val}, indent=2)) + except Exception as e: + print(c_red(f"Error rolling back variable: {e}"), file=sys.stderr) + sys.exit(1) + + +def cmd_loop_close(args): + dm_id = args.dm_id.strip() + cmd = [sys.executable, str(BIN_DIR / "box-ctl.py"), "loop-resolve", dm_id] + if getattr(args, "note", None): + cmd.append(args.note) + res = subprocess.run(cmd, capture_output=True, text=True) + try: + data = json.loads(res.stdout) + if data.get("ok"): + print(c_green(f"āœ” Loop {dm_id} marked resolved/closed.")) + else: + print(c_red(f"Error closing loop: {data.get('error')}"), file=sys.stderr) + except Exception: + print(res.stdout or res.stderr) + + +def cmd_loop_nudge(args): + dm_id = args.dm_id.strip() + if not FOLLOWUPS_FILE.exists(): + print(c_red("followups.json not found."), file=sys.stderr) + return + try: + with open(FOLLOWUPS_FILE, "r") as f: + fdata = json.load(f) + rec = fdata.get(dm_id) + if not rec: + print(c_red(f"No pending follow-up found for ID '{dm_id}'."), file=sys.stderr) + return + rec["deadline"] = datetime.now(timezone.utc).isoformat() + with open(FOLLOWUPS_FILE, "w") as f: + json.dump(fdata, f, indent=2) + print(c_green(f"āœ” Armed immediate nudge for loop {dm_id}. Running sweeper...")) + subprocess.run([sys.executable, str(FOLLOWUP_SWEEPER_PY), "--once"]) + except Exception as e: + print(c_red(f"Error nudging loop: {e}"), file=sys.stderr) + + +def cmd_loop_sweep(args): + cmd_followup_sweep(args) + + +def cmd_loop_harvest(args): + cmd_harvest_run(args) + + +def cmd_loop_remediate(args): + dry_run = getattr(args, "dry_run", False) + from gravity import remediate_breaks + res = remediate_breaks(dry_run=dry_run) + + if getattr(args, "json", False): + print(json.dumps({"ok": True, **res}, indent=2)) + return + + mode_str = c_yellow("DRY RUN (Simulated)") if dry_run else c_green("ACTIVE RECOVERY") + print("\n" + c_bold(f"=== PROGRESSIVE INTRINSIC LOOP REMEDIATION [{mode_str}] ===") + "\n") + + remediated = res.get("remediated", []) + escalated = res.get("escalated", []) + + if remediated: + print(c_bold(f"Auto-Remediated Soft Breakages ({len(remediated)}):")) + headers = ["ACTION", "LOOP ID", "AGENT", "REMEDIATION DETAIL"] + rows = [] + for r in remediated: + act_badge = c_green("āœ” " + r.get("action", "")) + rows.append([act_badge, c_bold(r.get("loop_id", "-")[:8]), r.get("agent", "-"), r.get("detail", "")]) + print_table(headers, rows) + print() + else: + print(c_dim(" āœ” No soft breakages require remediation at this time.\n")) + + if escalated: + print(c_red(c_bold(f"⚠ Escalated Hard Failures ({len(escalated)} - Manual Intervention Required):"))) + headers = ["SEVERITY", "BREAK TYPE", "TARGET", "ISSUE DETAIL", "REMEDY"] + rows = [] + for esc in escalated: + sev = badge_err("CRITICAL") if esc.get("severity") == "CRITICAL" else badge_warn("WARNING") + rows.append([sev, c_bold(esc.get("type", "-")), esc.get("agent") or esc.get("component") or "-", esc.get("detail", ""), c_dim(esc.get("remedy", ""))]) + print_table(headers, rows) + print() + +# --------------------------------------------------------------------------- +# CLI Argument Parser Setup +# --------------------------------------------------------------------------- + +def cmd_ssh_mint(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "ssh-mint", args.name] + if args.force: + cmd.append("--force") + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + print(c_green(f"āœ“ Minted ed25519 SSH keypair for '{args.name}'")) + print(f" Private key: {d.get('private_key')}") + print(f" Public key: {d.get('public_key')}") + print(f" Public key text: {d.get('public_key_content')}") + print(c_dim(" Registered into: ") + ", ".join(d.get("registered_in", []))) + else: + print(c_red(f"āœ— Failed: {d.get('error')} ({d.get('code')})")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_ssh_list(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "ssh-list"] + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + keys = d.get("keys", []) + print("\n" + c_bold(f"=== MANAGED SSH KEYS ({len(keys)}) ===") + "\n") + headers = ["NAME", "PRIVATE KEY", "PUB STATUS", "PUBLIC KEY PREVIEW"] + rows = [] + for k in keys: + pub_txt = k.get("public_key") or "" + preview = (pub_txt[:32] + "..." + pub_txt[-16:]) if len(pub_txt) > 48 else pub_txt + rows.append([c_cyan(k.get("name")), k.get("private_key"), c_green("āœ“ present") if k.get("has_public") else c_red("missing"), preview]) + print_table(headers, rows) + print("\n" + c_dim(" Mint new key: box ssh mint <name> [--force]") + "\n") + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_ssh_show(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "ssh-show", args.name] + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + print(c_bold(f"=== SSH KEY DETAILS: {args.name} ===")) + print(f" Private key: {d.get('private_key')}") + print(f" Public key: {d.get('public_key')}") + print(f" Fingerprint: {d.get('fingerprint')}") + print(f"\n Public Key:\n {c_cyan(d.get('public_key_content'))}\n") + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_ssh_ports(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "ssh-ports"] + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + tunnels = d.get("tunnels", {}) + jump = d.get("jump_host", "34.139.37.135") + print("\n" + c_bold(f"=== CONTAINER SSH & REVERSE TUNNEL REGISTRY (JUMP: {jump}) ===") + "\n") + headers = ["AGENT", "SSH PORT", "TERM PORT", "USER", "DIAL-IN COMMAND"] + rows = [] + for name, info in tunnels.items(): + port = info.get("port") + tport = info.get("terminal") + u = info.get("user", "hatch") + dial = f"ssh -J super@{jump} -p {port} {u}@localhost" + rows.append([c_cyan(name), str(port), str(tport), u, c_yellow(dial)]) + print_table(headers, rows) + print("\n" + c_dim(" To execute remote cmd: ssh -J super@<jump> -p <port> hatch@localhost '<cmd>'") + "\n") + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_ssh_info(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "ssh-info", args.name] + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + name = d.get("account", args.name) + port = d.get("port") + u = d.get("container_user", "hatch") + jump = d.get("jump_host", "34.139.37.135") + print("\n" + c_bold(f"=== SSH DIAL-IN FOR AGENT: {name} ===") + "\n") + print(f" Container User: {c_cyan(u)}") + print(f" Reverse SSH Port:{c_yellow(str(port))}") + print(f" GCP Jump Host: {jump}") + print(f" From VM directly: {c_green(d.get('direct_from_vm', ''))}") + print(f" Via Jump Host: {c_green(d.get('jump_command', ''))}") + print(f"\n Modify .md via SSH:") + print(f" {c_dim(d.get('cat_example', ''))}\n") + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_ssh_check(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "ssh-check"] + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + except Exception: + sys.stdout.write(res.stdout) + return + if not d.get("ok"): + print(c_red(f"āœ— Failed: {d.get('error')}")) + return + jump = d.get("jump_host", "?") + reachable = d.get("jump_reachable") + wall = d.get("latency_ms") + print("\n" + c_bold(f"=== CONTAINER SSH TUNNEL HEALTH (JUMP: {jump}) ===") + "\n") + if not reachable: + print(c_red(f" āœ— Jump host unreachable: {d.get('error', 'unknown')}")) + print(c_dim(" Tunnel states below are UNKNOWN (last sweep failed).") + "\n") + elif wall is not None: + print(c_dim(f" Sweep completed in {wall}ms at {d.get('checked_at', '')}") + "\n") + headers = ["AGENT", "SSH", "LAT", "BANNER", "TERM", "STATE", "DIAL"] + rows = [] + accounts = d.get("accounts", {}) + + def _state(v): + if v is True: + return c_green("UP") + if v is False: + return c_red("DOWN") + return c_dim("?") + + for name, info in accounts.items(): + sport = info.get("ssh_port", "?") + tport = info.get("term_port", "?") + lat = info.get("ssh_latency_ms") + lat_s = f"{lat}ms" if lat is not None else "-" + banner = (info.get("ssh_banner") or "-")[:28] + therm = info.get("term_http") or "" + tstate = _state(info.get("term_up")) + if info.get("term_up") and therm: + tstate += c_dim(f" {therm[:18]}") + u = info.get("container_user", "hatch") + dial = f"ssh -J super@{jump} -p {sport} {u}@localhost" + rows.append([c_cyan(name), f":{sport} {_state(info.get('ssh_up'))}", + lat_s, banner, f":{tport}", tstate, c_yellow(dial)]) + print_table(headers, rows) + print() + + +def cmd_md_audit(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md-audit"] + (args.accounts or []) + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + agents = d.get("agents", {}) + print("\n" + c_bold(f"=== MUSE AGENT .MD & OPERATIONAL DRIVE AUDIT ({len(agents)} AGENTS) ===") + "\n") + for acct, info in agents.items(): + if not info.get("connected"): + print(c_red(f"[{acct.upper()}] āœ— Connection failed: {info.get('error')}")) + continue + score = info.get("drive_score", 0) + score_str = c_green(f"{score}/100 [HIGH DRIVE]") if score >= 75 else (c_yellow(f"{score}/100 [PARTIAL]") if score >= 50 else c_red(f"{score}/100 [LOW/STALE]")) + vm = info.get("vm_id", "unknown") + print(f"[{c_bold(acct.upper())}] Drive: {score_str} VM: {c_dim(vm)}") + headers = ["FILE", "STATUS", "SIZE", "MODIFIED"] + rows = [] + for fname, finfo in info.get("files", {}).items(): + exists = finfo.get("exists") + st = c_green("āœ“ present") if exists else c_red("āœ— missing") + sz = f"{finfo.get('size', 0)} B" + mod = (finfo.get("modified") or "-")[:19] + rows.append([fname, st, sz, mod]) + print_table(headers, rows) + issues = info.get("issues", []) + if issues: + print(c_dim(" Issues / Gaps:")) + for iss in issues: + print(c_yellow(f" • {iss}")) + print() + print(c_dim(" Fix agent drive: box md inject-drive <agent> | box md sync-all") + "\n") + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_md_list(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md-list", args.account, args.path] + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + entries = d.get("entries", []) + print("\n" + c_bold(f"=== FILES IN {args.account}:{args.path or '/'} ({len(entries)}) ===") + "\n") + headers = ["NAME", "TYPE", "SIZE", "MODIFIED"] + rows = [] + for e in entries: + t = e.get("type", "file") + name_colored = c_cyan(e.get("name")) if t == "directory" else e.get("name") + rows.append([name_colored, t, str(e.get("size", "-")), (e.get("modifiedAt") or "-")[:19]]) + print_table(headers, rows) + print() + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_md_read(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md-read", args.account, args.filename] + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + text = d.get("text", "") + print(f"\n{c_bold('--- ' + args.account + ':' + args.filename + ' (' + str(len(text)) + ' chars) ---')}\n") + print(text) + print() + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_md_write(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md-write", args.account, args.filename] + if args.file: + cmd.extend(["--file", args.file]) + elif args.content: + cmd.extend(["--content", args.content]) + else: + print(c_red("Error: specify --content or --file")) + return + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + print(c_green(f"āœ“ Wrote {d.get('bytes_written')} bytes to {args.account}:{args.filename} via Hatch")) + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_md_diff(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md-diff", args.account, args.filename] + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + if d.get("identical"): + print(c_green(f"āœ“ {args.account}:{args.filename} matches local shared/operators/{args.filename} exactly.")) + else: + print(f"\n{c_bold('=== DIFF: ' + args.account + ':' + args.filename + ' vs shared/operators/' + args.filename + ' ===')}\n") + diff = d.get("diff", "") + for line in diff.splitlines(): + if line.startswith("+"): + print(c_green(line)) + elif line.startswith("-"): + print(c_red(line)) + elif line.startswith("@@"): + print(c_cyan(line)) + else: + print(line) + print() + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_md_inject_drive(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md-inject-drive", args.account] + if args.force: + cmd.append("--force") + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + print("\n" + c_bold(f"=== INJECTED OPERATIONAL DRIVE INTO AGENT: {args.account.upper()} ===") + "\n") + for u in d.get("updates", []): + print(c_green(f" āœ“ Injected {u.get('file'):26} ({u.get('bytes')} bytes)")) + print(f"\n{c_green('āœ“ Successfully activated SOUL.md, PROACTIVE_PREFERENCES.md, and HEARTBEAT.md!')}\n") + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_md_sync_all(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md-sync-all"] + if args.force: + cmd.append("--force") + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + print("\n" + c_bold("=== FLEET-WIDE OPERATIONAL DRIVE INJECTION ===") + "\n") + results = d.get("results", {}) + for acct, r in results.items(): + if r.get("ok"): + files_str = ", ".join(u.get("file") for u in r.get("updates", [])) + print(c_green(f" āœ“ {acct.upper():6} drive injected ({files_str})")) + else: + print(c_red(f" āœ— {acct.upper():6} failed: {r.get('error')}")) + print(f"\n{c_green('āœ“ Fleet synchronization complete.')}\n") + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_md_amend(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md", "amend", args.filename] + if args.file: + cmd.extend(["--file", args.file]) + elif args.content: + cmd.extend(["--content", args.content]) + if args.author: + cmd.extend(["--author", args.author]) + if args.reason: + cmd.extend(["--reason", args.reason]) + + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + commit_info = f" (git commit: {d.get('commit')})" if d.get("commit") else "" + print(c_green(f"\nāœ“ Successfully amended shared/operators/{args.filename}{commit_info}")) + print(f" Author: {c_cyan(d.get('author'))}") + print(f" Bytes: {d.get('bytes_written')}\n") + print(c_dim(" Broadcast to fleet with: box md sync-all\n")) + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_md_append(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md", "append", args.filename, args.text] + if args.author: + cmd.extend(["--author", args.author]) + if args.section: + cmd.extend(["--section", args.section]) + + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + commit_info = f" (git commit: {d.get('commit')})" if d.get("commit") else "" + print(c_green(f"\nāœ“ Successfully appended note to shared/operators/{args.filename}{commit_info}")) + print(f" Author: {c_cyan(d.get('author'))}\n") + print(c_dim(" Broadcast to fleet with: box md sync-all\n")) + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_md_pull(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "md", "pull", args.account, args.filename] + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + print(c_green(f"āœ“ Pulled canonical shared/operators/{args.filename} into {args.account} container ({d.get('bytes_written')} B)")) + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_md_watchdog(args): + watchdog_py = BIN_DIR / "agent-drive-watchdog.py" + if args.sub_action == "status": + cmd = ["python3", str(watchdog_py), "--status"] + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + print("\n" + c_bold("=== NETVM FLEET AGENT DRIVE WATCHDOG STATUS ===") + "\n") + print(f" Last Run: {c_cyan(d.get('last_run', 'never'))}") + st = d.get("agent_status", {}) + headers = ["AGENT", "DRIVE SCORE", "STATUS", "LAST CHECKED", "ISSUES"] + rows = [] + for acct, info in st.items(): + score = info.get("score", 0) + score_str = c_green(f"{score}/100") if score == 100 else c_red(f"{score}/100") + stat = c_green("HIGH DRIVE") if info.get("status") == "HIGH_DRIVE" else c_red(info.get("status", "DEGRADED")) + dt = (info.get("last_checked") or "-")[:19] + iss = ", ".join(info.get("issues", [])) or c_dim("none") + rows.append([c_bold(acct.upper()), score_str, stat, dt, iss]) + print_table(headers, rows) + + # Show timer status + tr = subprocess.run(["systemctl", "--user", "is-active", "agent-drive-watchdog.timer"], capture_output=True, text=True) + t_stat = tr.stdout.strip() + t_badge = c_green("ACTIVE (every 10m)") if t_stat == "active" else c_yellow(t_stat) + print(f"\n Systemd Timer: {t_badge}\n") + except Exception: + sys.stdout.write(res.stdout) + elif args.sub_action == "run": + cmd = ["python3", str(watchdog_py), "--once"] + if getattr(args, "no_heal", False): + cmd.append("--no-heal") + print("\n" + c_bold("=== RUNNING FLEET AGENT DRIVE WATCHDOG CYCLE ===") + "\n") + subprocess.run(cmd) + print() + else: + print("Usage: box md watchdog {status|run}") + + +def cmd_swarm_list(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "swarm-list"] + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + swarms = d.get("swarms", []) + print("\n" + c_bold(f"=== SWARMS ({len(swarms)}) ===") + "\n") + headers = ["ID", "STATUS", "LABEL", "TOTAL", "DONE", "RUNNING", "FAILED", "CREATED"] + rows = [] + for sw in swarms: + st = sw.get("status") + st_color = c_green(st) if st == "completed" else (c_yellow(st) if st in ("running", "partial") else c_dim(st)) + rows.append([ + c_cyan(sw.get("swarm_id", "")[:18]), + st_color, + (sw.get("label") or "-")[:20], + str(sw.get("count", 0)), + str(sw.get("done", 0)), + str(sw.get("running", 0)), + str(sw.get("failed", 0)), + (sw.get("created_ts") or "")[:16] + ]) + print_table(headers, rows) + print("\n" + c_dim(" Spawn swarm: box swarm spawn <count> <task> [--label L]") + "\n") + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_swarm_status(args): + sid = args.swarm_id + if not sid.startswith("sw-"): + sid = "sw-" + sid + # Support prefix matching if exact ID is truncated (e.g. 18 chars) + sw_file = Path(__file__).resolve().parent.parent / "swarms.json" + if sw_file.exists(): + try: + import json + data = json.loads(sw_file.read_text(encoding="utf-8")) + if sid not in data: + matches = [k for k in data.keys() if k.startswith(sid)] + if len(matches) == 1: + sid = matches[0] + except Exception: + pass + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "swarm-status", sid] + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + print(c_bold(f"\n=== SWARM STATUS: {args.swarm_id} ===")) + sw = d.get("swarm", {}) + print(f" Status: {sw.get('status')}") + print(f" Counts: {d.get('counts')}") + slots = sw.get("slots", []) + print(f"\n Slots ({len(slots)}):") + for s in slots: + res_txt = s.get("result") + res_prev = (str(res_txt)[:50] + "...") if res_txt and len(str(res_txt)) > 50 else str(res_txt) + print(f" Slot {s.get('slot')}: [{s.get('status')}] agent={s.get('agent_id')} subagent={s.get('subagent_session_id')} res={res_prev}") + print() + else: + print(c_red(f"āœ— Failed: {d.get('error')}")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_swarm_spawn(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "swarm-spawn", str(args.count), args.task] + if args.label: + cmd.extend(["--label", args.label]) + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + print(c_green(f"āœ“ Spawned swarm: {d.get('swarm_id')} ({d.get('count')} slots)")) + else: + print(c_red(f"āœ— Failed: {d.get('error')} ({d.get('code')})")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_swarm_prune(args): + cmd = ["python3", str(BIN_DIR / "box-ctl.py"), "swarm-prune", "--stale-hours", str(args.stale_hours)] + if args.confirm: + cmd.append("--confirm") + res = subprocess.run(cmd, capture_output=True, text=True) + if args.json: + sys.stdout.write(res.stdout) + return + try: + d = json.loads(res.stdout) + if d.get("ok"): + print(c_green(f"āœ“ Pruned {d.get('archived_count')} swarms older than {d.get('stale_hours')}h to 'archived'")) + else: + print(c_yellow(f"! {d.get('error')} (use --confirm)")) + except Exception: + sys.stdout.write(res.stdout) + + +def cmd_passkey_info(args): + """Display operator passkey reference and agent approval architecture, or fetch from VM.""" + action = getattr(args, "action", "show") or "show" + is_json = getattr(args, "json", False) + + if action in ("fetch", "get"): + vm_host = "34.139.37.135" + vm_paths = ["/srv/box/passkey.txt", "/etc/netvm/passkey.txt"] + cmd = [ + "ssh", + "-o", "BatchMode=yes", + "-o", "ConnectTimeout=3", + f"super@{vm_host}", + f"cat {vm_paths[0]} 2>/dev/null || cat {vm_paths[1]} 2>/dev/null" + ] + key_content = "" + fetch_err = None + try: + res = subprocess.run(cmd, capture_output=True, text=True, timeout=5) + if res.returncode == 0 and res.stdout.strip(): + key_content = res.stdout.strip() + else: + fetch_err = res.stderr.strip() or "Empty output or authentication required" + except subprocess.TimeoutExpired: + fetch_err = "SSH connection timed out" + except Exception as e: + fetch_err = str(e) + + if key_content: + if is_json: + print(json.dumps({ + "ok": True, + "fetched": True, + "passkey": key_content, + "vm_host": vm_host, + "path": vm_paths[0], + "operator_pin": "3128", + "surface": "https://box.muse-dev.online/" + }, indent=2)) + return + print("\n" + c_bold("=== OPERATOR PASSKEY (FETCHED FROM VM) ===") + "\n") + print(f" {c_bold('Passkey Content')}: {c_green(key_content)}") + print(f" {c_bold('Source Host')} : {c_cyan(vm_host)} ({vm_paths[0]})") + print(f" {c_bold('Web Surface')} : https://box.muse-dev.online/ (PIN: 3128)\n") + return + else: + if is_json: + print(json.dumps({ + "ok": False, + "fetched": False, + "vm_host": vm_host, + "path": vm_paths[0], + "fallback_path": vm_paths[1], + "operator_pin": "3128", + "surface": "https://box.muse-dev.online/", + "error": f"Could not fetch passkey directly: {fetch_err}", + "note": "Passkey is stored in a single .txt file on the VM (34.139.37.135) only, NOT on BL (100.123.153.75).", + "operator_command": f"ssh super@{vm_host} 'cat {vm_paths[0]}'", + "agent_approval_command": "box approvals request-key <node> --reason '<reason>'" + }, indent=2)) + return + print("\n" + c_bold("=== VM PASSKEY FETCH PROBE ===") + "\n") + print(f" {c_bold('Target VM Host')} : {c_cyan(vm_host)}") + print(f" {c_bold('Target Paths')} : {vm_paths[0]} (fallback: {vm_paths[1]})") + print(f" {c_bold('Probe Result')} : {c_yellow('Direct SSH access unavailable (' + (fetch_err or 'auth required') + ')')}\n") + print(c_bold(" Location Reality (VM vs BL):")) + print(f" • {c_yellow('VM (' + vm_host + ')')} : Key material lives in a {c_bold('single text file (.txt)')} on the VM.") + print(f" • {c_red('BL (100.123.153.75)')} : {c_bold('NO passkey / secret material exists on the dedicated BL.')}\n") + print(f" {c_bold('Console PIN')} : {c_green('3128')} (sets session cookie: {c_dim('ops_session')} at {c_cyan('https://box.muse-dev.online/')})\n") + print(c_bold(" Operator Access:")) + print(f" ssh super@{vm_host} 'cat {vm_paths[0]}'\n") + print(c_bold(" Agent UX / Approval Request:")) + print(f" Agents must not hunt BL. If passkey authorization is needed, request it via:") + print(f" {c_cyan('box approvals request-key <node> --reason \"<reason>\"')}\n") + return + + if is_json: + print(json.dumps({ + "ok": True, + "surface": "https://box.muse-dev.online/", + "operator_pin": "3128", + "session_cookie": "ops_session", + "key_location": { + "host": "Google Cloud VM (34.139.37.135)", + "canonical_path": "/srv/box/passkey.txt", + "fallback_path": "/etc/netvm/passkey.txt", + "note": "Stored in a single text file (.txt) on the VM — NOT on the dedicated BL compute node (100.123.153.75)", + "credstore_path": "/etc/netvm/meta-credentials/store.age", + "credstore_key": "/etc/netvm/meta-credentials/.age-key" + }, + "operator_fetch_cmd": "ssh super@34.139.37.135 'cat /srv/box/passkey.txt'", + "agent_approval_protocol": { + "rule": "Agents do not hold administrative passkeys or credentials directly. Secrets never reside on bl.", + "request_flow": "1. Agent signals APPROVAL_REQUEST: key=<reason> or runs 'box approvals request-key <node>'.\n2. Operator verifies request.\n3. Operator retrieves key/PIN from VM single text file (.txt) or submits OTP.\n4. Operator approves via 'box approvals allow <node>'.", + "request_command": "box approvals request-key <node> --reason '<reason>'", + "sidechats": ["646 tasks", "pip tasks", "#jobs", "heartbeat"] + } + }, indent=2)) + return + + print("\n" + c_bold("=== OPERATOR PASSKEY & KEY MATERIAL ARCHITECTURE ===") + "\n") + print(f" {c_bold('Box Front-Door Console')}: {c_cyan('https://box.muse-dev.online/')}") + print(f" {c_bold('Operator PIN / Passkey')}: {c_green('3128')} (sets session cookie: {c_dim('ops_session')})\n") + + print(c_bold(" Location Reality (VM vs BL):")) + print(f" • {c_yellow('VM (34.139.37.135)')} : Key material lives in a {c_bold('single text file (.txt)')} on the VM.") + print(f" - Canonical path: {c_cyan('/srv/box/passkey.txt')}") + print(f" - Fallback path : {c_cyan('/etc/netvm/passkey.txt')}") + print(f" - Fetch command : {c_dim('box passkey fetch')}") + print(f" • {c_red('BL (100.123.153.75)')} : {c_bold('NO passkey / secret material exists on the dedicated BL.')}") + print(f" • Credstore on VM : /etc/netvm/meta-credentials/store.age (age-encrypted, root 600)") + print(f" • Age Key on VM : /etc/netvm/meta-credentials/.age-key\n") + + print(c_bold(" Agent UX & Operator Approval Flow:")) + print(f" 1. {c_cyan('Never hunt on bl')} : Agents must never attempt to grep or locate passkeys on bl.") + print(f" 2. {c_cyan('Signal Approval')} : If an agent requires key access or operator privilege:") + print(f" - Run: {c_yellow('box approvals request-key <node> --reason \"<reason>\"')}") + print(f" - Or emit {c_yellow('APPROVAL_NEEDED: <details>')} in task sidechat") + print(f" - Or exit with code {c_yellow('2')} (per INFRA.md convention)") + print(f" 3. {c_cyan('Operator Action')} : Operator consults the single .txt file on the VM to authenticate") + print(f" and approves via {c_dim('box approvals allow <node>')}.") + print(f" 4. {c_cyan('Target Sidechats')} : Use dedicated sidechats ({c_dim('646 tasks, pip tasks, #jobs, heartbeat')}).\n") + + + +def _lookup_unreads(args): + data = collect_fleet_data() + as_json = getattr(args, "json", False) + if as_json: + nodes_out = [] + for item in data: + n = item.get("node") + title = item.get("title", "") + unread_cnt = 0 + if "(1)" in title or "(2)" in title or "(3)" in title: + m = re.search(r"\((\d+)\)", title) + unread_cnt = int(m.group(1)) if m else 1 + thread_info = "-" + if "thread/" in item.get("url", ""): + thread_info = item["url"].split("thread/")[-1][:12] + elif item.get("url") == "https://muse.ai/": + thread_info = "home" + nodes_out.append({ + "node": n, + "unread": unread_cnt, + "title": title, + "thread": thread_info, + "approval_pending": bool(item.get("approval_pending")), + }) + print(json.dumps({"ok": True, "nodes": nodes_out})) + return + + print("\n" + c_bold("=== FLEET UNREAD / ACTIVITY STATUS ===") + "\n") + headers = ["NODE", "UNREAD COUNT", "ACTIVE PAGE / TITLE", "LAST SEEN / THREAD"] + rows = [] + for item in data: + n = item["node"] + title = item.get("title", "") + unread = "0" + if "(1)" in title or "(2)" in title or "(3)" in title: + m = re.search(r"\((\d+)\)", title) + unread = c_yellow(m.group(1)) if m else c_yellow("1+") + elif item.get("approval_pending"): + unread = c_yellow("APPROVAL") + + thread_info = "-" + if "thread/" in item.get("url", ""): + thread_info = item["url"].split("thread/")[-1][:12] + elif item.get("url") == "https://muse.ai/": + thread_info = "home" + + rows.append([c_bold(n), unread, title[:45], thread_info]) + print_table(headers, rows) + print("\n" + c_dim(" To view unread messages: box thread view <node> <thread>") + "\n") + + +def cmd_lookup(args): + """Seamless unified lookup across nodes, threads, unreads, approvals, and keys.""" + lookup_args = getattr(args, "lookup_args", []) or [] + if "--json" in lookup_args: + setattr(args, "json", True) + sub = getattr(args, "target", None) + if not sub or sub in ("all", "summary"): + print("\n" + c_bold("=== SEAMLESS FLEET LOOKUP SUMMARY ===") + c_dim(f" ({datetime.now().strftime('%H:%M:%S')} local)\n")) + # 1. Fleet status + cmd_fleet_status(args) + # 2. Approvals summary + try: + import approvals + app_list = approvals.check_fleet_approvals(VALID_NODES) + pending = [a for a in app_list if a.get("status") == "PENDING"] + if pending: + print(c_yellow(f" ⚠ {len(pending)} pending approval(s): {', '.join(a['node'] for a in pending)} (run: box approvals)")) + else: + print(c_green(" āœ” Approval Queues: All clean")) + except Exception: + pass + # 3. Key note + print(c_dim(" ℹ Passkey: Stored in single .txt on VM (34.139.37.135), not bl. (run: box passkey)")) + print() + return + + if sub in ("key", "passkey", "auth"): + cmd_passkey_info(args) + elif sub in ("fleet", "nodes"): + cmd_fleet_status(args) + elif sub in ("threads", "sidechats"): + cmd_thread_list(args) + elif sub in ("unread", "unreads"): + _lookup_unreads(args) + elif sub in ("approvals", "approval"): + cmd_approvals(args) + elif sub in ("docs", "doc", "surfaces", "sentence", "regex", "parse"): + extra = getattr(args, "lookup_args", []) or [] + sub_args = [sub] if sub not in ("docs", "doc") else [] + cmd = [sys.executable, str(BIN_DIR / "docs-lookup.py")] + sub_args + extra + res = subprocess.run(cmd) + sys.exit(res.returncode) + else: + print(f"Unknown lookup target '{sub}'. Choose from: fleet, threads, unread, approvals, key, docs", file=sys.stderr) + + +def _sanitize_tmux_name(name: str) -> str: + cleaned = re.sub(r"[^a-zA-Z0-9_-]", "-", name).strip("-") + cleaned = re.sub(r"-+", "-", cleaned) + return cleaned or "run" + + +def _write_watch_script(session: str) -> tuple[str, str]: + watch_session = f"{session}-watch" + script_path = f"/tmp/{watch_session}.sh" + content = f"""#!/bin/bash +target="{session}" +exit_file="/tmp/{session}.exit" +max=200 +count=0 + +while [ $count -lt $max ]; do + if ! tmux -S /tmp/tmux-muse.sock has-session -t "$target" 2>/dev/null; then + break + fi + pane_content=$(tmux -S /tmp/tmux-muse.sock capture-pane -p -t "$target" 2>/dev/null) + if echo "$pane_content" | grep -q "› 1. Yes, proceed"; then + tmux -S /tmp/tmux-muse.sock send-keys -t "$target" "1" Enter + fi + sleep 1 + count=$((count+1)) +done +touch "$exit_file" +""" + p = Path(script_path) + p.write_text(content, encoding="utf-8") + os.chmod(script_path, 0o755) + return watch_session, script_path + + +def cmd_run(args): + """Run a headless muse-code session in tmux on /tmp/tmux-muse.sock.""" + if not shutil.which("tmux"): + print("tmux not found", file=sys.stderr) + sys.exit(2) + if not shutil.which("muse-code"): + print("muse-code not found", file=sys.stderr) + sys.exit(2) + + prompt = getattr(args, "prompt", None) + prompt_file = getattr(args, "prompt_file", None) + if not prompt and not prompt_file: + if sys.stdin.isatty(): + print("Error: prompt or --prompt-file required", file=sys.stderr) + sys.exit(2) + prompt = sys.stdin.read() + if not prompt.strip(): + print("Error: empty prompt", file=sys.stderr) + sys.exit(2) + + raw_session = getattr(args, "session", None) or f"run-{int(time.time())}" + session = _sanitize_tmux_name(raw_session) + auto_approve = getattr(args, "auto_approve", False) + + prompt_path = f"/tmp/{session}.prompt" + if prompt: + Path(prompt_path).write_text(prompt if prompt.endswith("\n") else prompt + "\n", encoding="utf-8") + elif prompt_file: + prompt_path = prompt_file + + wrapper_path = f"/tmp/{session}.sh" + cmd_parts = [ + "cd /home/super/Projects/NetVM", + f"muse-code --prompt-file {prompt_path}", + ] + if getattr(args, "provider", None): + cmd_parts.append(f"--provider {args.provider}") + if getattr(args, "model", None): + cmd_parts.append(f"--model {args.model}") + if getattr(args, "effort", None): + cmd_parts.append(f"--reasoning-effort {args.effort}") + if getattr(args, "permission_profile", None): + cmd_parts.append(f"--permission-profile {args.permission_profile}") + if getattr(args, "trust_workspace", False): + cmd_parts.append("--trust-workspace") + if getattr(args, "approval_mode", None): + cmd_parts.append(f"--approval-mode {args.approval_mode}") + + full_cmd = " && ".join(cmd_parts) + wrapper_content = f"#!/bin/bash\n{full_cmd}\ntouch /tmp/{session}.exit\n" + Path(wrapper_path).write_text(wrapper_content, encoding="utf-8") + os.chmod(wrapper_path, 0o755) + + MUSE_TMUX_LOG_DIR.mkdir(parents=True, exist_ok=True) + log_path = MUSE_TMUX_LOG_DIR / f"{session}.log" + + tmux_cmd = [ + "tmux", "-S", "/tmp/tmux-muse.sock", + "new-session", "-d", "-s", session, + f"{wrapper_path} 2>&1 | tee {log_path}" + ] + res = subprocess.run(tmux_cmd) + if res.returncode != 0: + print(f"Error launching tmux session: {res.stderr or 'failed'}", file=sys.stderr) + sys.exit(res.returncode or 1) + + print(f"session: {session}") + print(f"attach: tmux -S /tmp/tmux-muse.sock attach -t {session}") + + if auto_approve: + watch_session, watch_script = _write_watch_script(session) + watch_log = MUSE_TMUX_LOG_DIR / f"{watch_session}.log" + w_cmd = [ + "tmux", "-S", "/tmp/tmux-muse.sock", + "new-session", "-d", "-s", watch_session, + f"{watch_script} 2>&1 | tee {watch_log}" + ] + w_res = subprocess.run(w_cmd) + if w_res.returncode == 0: + print(f"watcher: {watch_session}") + print(f"watcher-log: {watch_log}") + + +def cmd_watch(args): + """Spawn an auto-approve watcher on an existing tmux session.""" + if not shutil.which("tmux"): + print("tmux not found", file=sys.stderr) + sys.exit(2) + + raw_session = getattr(args, "session", None) + if not raw_session: + print("Error: session name required", file=sys.stderr) + sys.exit(2) + session = _sanitize_tmux_name(raw_session) + + has_res = subprocess.run(["tmux", "-S", "/tmp/tmux-muse.sock", "has-session", "-t", session]) + if has_res.returncode != 0: + print(f"Error: session '{session}' does not exist", file=sys.stderr) + sys.exit(2) + + watch_session, watch_script = _write_watch_script(session) + MUSE_TMUX_LOG_DIR.mkdir(parents=True, exist_ok=True) + watch_log = MUSE_TMUX_LOG_DIR / f"{watch_session}.log" + + w_cmd = [ + "tmux", "-S", "/tmp/tmux-muse.sock", + "new-session", "-d", "-s", watch_session, + f"{watch_script} 2>&1 | tee {watch_log}" + ] + w_res = subprocess.run(w_cmd) + if w_res.returncode != 0: + print(f"Error starting watcher: {w_res.stderr or 'failed'}", file=sys.stderr) + sys.exit(w_res.returncode or 1) + + print(f"watching: {session}") + print(f"watcher: {watch_session}") + print(f"watcher-log: {watch_log}") + + +def cmd_docs_dispatch(args): + """Bridge 'box docs' and 'super docs' directly to bin/docs-lookup.py.""" + d_args = getattr(args, "docs_args", []) or [] + cmd = [sys.executable, str(BIN_DIR / "docs-lookup.py")] + d_args + res = subprocess.run(cmd) + sys.exit(res.returncode) + + +def cmd_tmux_dispatch(args): + """Bridge 'box tmux' commands directly to bin/muse-tmux.py or tmux_auto_approver.py.""" + t_args = getattr(args, "tmux_args", []) or [] + if t_args and t_args[0] in ("tally", "auto", "watch", "once", "match", "rules", "status"): + approver_bin = str(BIN_DIR / "tmux_auto_approver.py") + if t_args[0] == "auto": + sub = t_args[1:] or ["status"] + else: + sub = t_args + cmd = [sys.executable, approver_bin] + sub + res = subprocess.run(cmd) + sys.exit(res.returncode) + tmux_bin = str(BIN_DIR / "muse-tmux.py") + if not t_args: + t_args = ["list"] + cmd = [sys.executable, tmux_bin] + t_args + res = subprocess.run(cmd) + sys.exit(res.returncode) + + +def cmd_flow_dispatch(args): + """Bridge 'box flow' commands directly to bin/flow_engine.py.""" + flow_bin = str(BIN_DIR / "flow_engine.py") + f_args = getattr(args, "flow_args", []) or [] + cmd = [sys.executable, flow_bin] + f_args + res = subprocess.run(cmd) + sys.exit(res.returncode) + + +def cmd_muse_dispatch(args): + """Bridge 'box muse' commands to muse-cli-node or interactive REPL / multi-node lookups.""" + m_args = getattr(args, "muse_args", []) or [] + if not m_args: + cmd = [str(BIN_DIR / "muse"), "--help"] + res = subprocess.run(cmd) + sys.exit(res.returncode) + + first = m_args[0] + + # If first is 'tmux', dispatch to tmux + if first == "tmux": + cmd = [sys.executable, str(BIN_DIR / "muse-tmux.py")] + m_args[1:] + res = subprocess.run(cmd) + sys.exit(res.returncode) + + # If first is a cross-fleet query + if first in ("status", "fleet"): + cmd_fleet_status(args) + return + + if first in ("passkey", "key"): + cmd_passkey_info(args) + return + + if first in ("lookup", "lookups"): + setattr(args, "target", m_args[1] if len(m_args) > 1 else None) + cmd_lookup(args) + return + + if first in ("threads", "sidechats"): + cmd_thread_list(args) + return + + if first in ("unread", "unreads"): + _lookup_unreads(args) + return + + # If first is a valid node + if first in VALID_NODES: + node = first + subargs = m_args[1:] + if not subargs: + subargs = ["status"] + if subargs[0] == "chat": + cmd = [sys.executable, str(BIN_DIR / "muse-chat-repl.py"), node] + subargs[1:] + res = subprocess.run(cmd) + sys.exit(res.returncode) + cmd = [str(BIN_DIR / "muse-cli-node"), node] + subargs + res = subprocess.run(cmd) + sys.exit(res.returncode) + + # Fallback to bin/muse wrapper + cmd = [str(BIN_DIR / "muse")] + m_args + res = subprocess.run(cmd) + sys.exit(res.returncode) + + +# --------------------------------------------------------------------------- +# Domain: SYSOP (one-shot fleet installer) +# --------------------------------------------------------------------------- +SYSOP_SYSTEMD_DIR = NETVM_ROOT / "systemd" + +# `systemctl show -p NextElapseUSecRealtime --value` reports this when a +# timer has no computed next elapse (UINT64_MAX = USEC_INFINITY). +SYSOP_NO_NEXT = {"", "0", "n/a", "18446744073709551615"} + + +def sysop_user_units_dir(): + return Path.home() / ".config" / "systemd" / "user" + + +def sysop_unit_files(systemd_dir=None): + """Sorted unit filenames (*.timer + *.service) shipped in systemd/.""" + d = Path(systemd_dir) if systemd_dir else SYSOP_SYSTEMD_DIR + if not d.is_dir(): + return [] + return sorted(p.name for p in d.iterdir() + if p.is_file() and p.suffix in (".timer", ".service")) + + +def sysop_timer_needs_anchor(timer_path): + """True when a timer uses relative triggers (OnBootSec/OnActiveSec/ + OnUnitActiveSec) whose NEXT stays empty until the service runs once.""" + try: + text = Path(timer_path).read_text() + except OSError: + return False + return any(k in text for k in ("OnUnitActiveSec=", "OnBootSec=", + "OnActiveSec=")) + + +def sysop_is_daemon(service_path): + """True for long-lived daemons that must not be one-shot started.""" + try: + text = Path(service_path).read_text() + except OSError: + return False + return "Restart=always" in text + + +def _sysop_next(run, timer): + """NEXT elapse for a timer, or None when absent/unparseable.""" + rc, out = run(["systemctl", "--user", "show", timer, + "-p", "NextElapseUSecRealtime", "--value"]) + nxt = (out or "").strip() + if rc == 0 and nxt not in SYSOP_NO_NEXT: + return nxt + return None + + +def _sysop_service_state(run, service): + """ActiveState for a service ('' when unknown).""" + rc, out = run(["systemctl", "--user", "show", service, + "-p", "ActiveState", "--value"]) + return (out or "").strip() if rc == 0 else "" + + +def sysop_install_units(systemd_dir=None, user_dir=None, dry_run=False, + run=None, progress=None, anchor_timeout=90, + poll_interval=3): + """Link fleet units, reload, enable timers, anchor + verify them. + + Returns a result dict with per-unit reports and a failures list. + Idempotent: correct symlinks are kept, systemctl calls are re-runnable. + With dry_run=True nothing is changed and no command is executed. + + progress, when given, is called with short status lines as work + happens (the run is otherwise silent for minutes behind slow + service starts). Anchors never block indefinitely: services are + started --no-block and NEXT is polled up to anchor_timeout; a + still-activating service is reported in-progress (it self-anchors + on completion) rather than failed. + """ + run = run or _sh + say = progress or (lambda line: None) + src_dir = Path(systemd_dir) if systemd_dir else SYSOP_SYSTEMD_DIR + dst_dir = Path(user_dir) if user_dir else sysop_user_units_dir() + units = sysop_unit_files(src_dir) + timers = [u for u in units if u.endswith(".timer")] + services = {u for u in units if u.endswith(".service")} + + result = {"ok": True, "dry_run": dry_run, "units": [], + "daemon_reload": {"ok": True, "detail": ""}, + "timers": [], "failures": []} + + def fail(msg): + result["failures"].append(msg) + result["ok"] = False + + if not timers: + fail("no *.timer units discovered in %s" % src_dir) + return result + + # (1) Symlink every shipped unit into the user systemd dir. + if not dry_run: + try: + dst_dir.mkdir(parents=True, exist_ok=True) + except OSError as e: + fail("cannot create %s: %s" % (dst_dir, e)) + return result + for name in units: + src = src_dir / name + dst = dst_dir / name + if dry_run: + result["units"].append( + {"unit": name, "status": "would-link", + "src": str(src), "dst": str(dst)}) + continue + try: + if dst.is_symlink() and dst.resolve() == src.resolve(): + result["units"].append( + {"unit": name, "status": "already-linked", + "dst": str(dst)}) + continue + if dst.is_symlink() or dst.exists(): + dst.unlink() + dst.symlink_to(src) + result["units"].append( + {"unit": name, "status": "linked", "dst": str(dst)}) + except OSError as e: + result["units"].append( + {"unit": name, "status": "failed", "error": str(e)}) + fail("link %s: %s" % (name, e)) + + if dry_run: + for timer in timers: + result["timers"].append( + {"timer": timer, "enabled": None, "anchored": None, + "anchor_skipped": None, "next": None, + "note": "would enable --now, anchor, and verify"}) + return result + + # (2) Reload the user manager. + say("daemon-reload ...") + rc, out = run(["systemctl", "--user", "daemon-reload"]) + result["daemon_reload"] = {"ok": rc == 0, "detail": out} + if rc != 0: + fail("daemon-reload: %s" % (out or ("exit %d" % rc))) + + # (3)-(5) Enable, anchor, and verify each timer. + for timer in timers: + entry = {"timer": timer, "enabled": False, "anchored": False, + "anchor_skipped": None, "next": None, "note": None} + say("enable --now %s ..." % timer) + rc, out = run(["systemctl", "--user", "enable", "--now", timer]) + entry["enabled"] = rc == 0 + if rc != 0: + fail("enable --now %s: %s" % (timer, out or ("exit %d" % rc))) + # (4) Anchor relative timers: start the matching oneshot + # service once so OnUnitActiveSec gains a reference timestamp + # (fresh-install-mid-boot otherwise leaves NEXT empty). The + # start is --no-block: slow first runs (backlog scrapes) + # would otherwise stall the whole install with no output. + service = timer[:-len(".timer")] + ".service" + if service not in services: + entry["anchor_skipped"] = "no matching service shipped" + elif not sysop_timer_needs_anchor(src_dir / timer): + entry["anchor_skipped"] = "calendar timer needs no anchor" + elif sysop_is_daemon(src_dir / service): + entry["anchor_skipped"] = "long-lived daemon, not started" + else: + state = _sysop_service_state(run, service) + if state == "activating": + say("anchor %s: already running, waiting for NEXT ..." + % service) + else: + say("anchor %s: starting ..." % service) + rc, out = run(["systemctl", "--user", "start", + "--no-block", service]) + if rc != 0: + fail("start %s: %s" + % (service, out or ("exit %d" % rc))) + state = "start-failed" + # (5) Poll for a real NEXT elapse (bounded). + if state != "start-failed": + polled = _sysop_poll_next( + run, say, timer, service, anchor_timeout, + poll_interval) + msg = polled.pop("_fail", None) + if msg: + fail(msg) + entry.update(polled) + # Timers whose anchor was skipped still get one NEXT check. + if entry["anchor_skipped"] and entry["next"] is None: + nxt = _sysop_next(run, timer) + if nxt is not None: + entry["next"] = nxt + else: + fail("verify %s: no NEXT elapse" % timer) + result["timers"].append(entry) + + return result + + +def _sysop_poll_next(run, say, timer, service, timeout, interval): + """Poll until the timer shows NEXT, the service fails, or timeout. + + Returns a partial timer entry (anchored/next/note). A service + still activating at timeout is reported in-progress rather than + failed: its running start job anchors the timer on completion. + """ + deadline = time.monotonic() + max(0, timeout) + while True: + nxt = _sysop_next(run, timer) + if nxt is not None: + return {"anchored": True, "next": nxt, "note": None} + state = _sysop_service_state(run, service) + if state == "failed": + return {"anchored": False, "next": None, + "note": None, "_fail": + "anchor %s: service failed " + "(journalctl --user -u %s)" % (service, service)} + if time.monotonic() >= deadline: + if state == "activating": + say("anchor %s: still running, timer self-anchors " + "on completion" % service) + return {"anchored": "in-progress", "next": None, + "note": "anchor running; verify NEXT shortly"} + return {"anchored": False, "next": None, + "note": None, "_fail": + "verify %s: no NEXT elapse " + "(service state: %s)" % (timer, state or "?")} + say("anchor %s: waiting for NEXT ..." % service) + time.sleep(max(0, interval)) + + +def cmd_sysop_install(args): + dry_run = getattr(args, "dry_run", False) + as_json = getattr(args, "json", False) + # Live progress: stdout in text mode, stderr in --json mode so + # stdout stays pure machine-readable JSON. + progress = (lambda line: print(" ... %s" % line, flush=True, + file=sys.stderr if as_json else sys.stdout)) + result = sysop_install_units(dry_run=dry_run, progress=progress) + if as_json: + print(json.dumps(result, indent=2)) + sys.exit(0 if result["ok"] else 1) + print(c_bold("\n=== SYSOP INSTALL%s ===\n" % ( + " (dry-run)" if dry_run else ""))) + for u in result["units"]: + st = u["status"] + mark = (badge_ok("LINKED") if st == "linked" + else badge_dim("KEPT") if st == "already-linked" + else c_cyan("ā—‹ WOULD-LINK") if st == "would-link" + else badge_err("FAILED")) + print(" %s %s" % (mark, u["unit"])) + if not dry_run: + dr = result["daemon_reload"] + print(" %s daemon-reload" % ( + badge_ok("OK") if dr["ok"] else badge_err("FAILED"))) + print() + for t in result["timers"]: + en = t["enabled"] + emark = (badge_dim("?") if en is None + else badge_ok("ON") if en else badge_err("OFF")) + if t.get("anchor_skipped"): + amark = c_dim("- %s" % t["anchor_skipped"]) + elif t.get("anchored") == "in-progress": + amark = c_cyan("ā—‹ ANCHORING") + elif t["anchored"]: + amark = badge_ok("ANCHORED") + elif t["anchored"] is None: + amark = badge_dim("?") + else: + amark = badge_err("ANCHOR-FAILED") + nxt = (t["next"] or c_dim(t["note"]) if t.get("note") + else t["next"] or (c_dim("pending (dry-run)") + if dry_run else badge_err("NO NEXT"))) + print(" %s %-32s %s NEXT=%s" % (emark, t["timer"], amark, + nxt)) + print() + if result["failures"]: + for f in result["failures"]: + print(" %s %s" % (c_red("✘"), f)) + print() + sys.exit(1) + print(" %s fleet units installed and verified.\n" % c_green("āœ”")) + sys.exit(0) + + +# --------------------------------------------------------------------------- +# CLI Usage Helpers, Error Formatting, and Deep Manuals +# --------------------------------------------------------------------------- + +COMMAND_EXAMPLES = { + "box work": [ + "box work # View fleet workspace dashboard & signals", + "box work check [agent] # Audit pre-flight health gates", + "box work heal <agent> # Automated remediation & chat nudge", + "box work start \"<title>\" --to <agent> # Start & dispatch new build ticket", + "box work assign <issue#> --to <agent> # Assign existing ticket", + "box work merge <pr#> # Verify tests and merge PR to master", + "box work chats --agent <name> # View live multi-agent chat feed", + ], + "box work start": [ + "box work start \"Fix SSH perms\" --to 646", + "box work start \"Build integration tests\" --to pip --goal \"Run pytest on endpoints\"", + "box work start \"Emergency rebuild\" --to dev --force", + ], + "box work check": [ + "box work check # Check all agents", + "box work check 646 # Check specific agent", + ], + "box work heal": [ + "box work heal dev # Heal dev agent (token, perms, tunnel nudge)", + "box work heal 646", + ], + "box work assign": [ + "box work assign 218 --to 646", + ], + "box work merge": [ + "box work merge 217 # Test and merge PR 217 into master", + ], + "box work chats": [ + "box work chats # Last 10 chat messages across fleet", + "box work chats --agent opm --limit 5", + ], + "box tasks": [ + "box tasks list # List all tasks across queues", + "box tasks list --queue pending", + "box tasks show 218-restore-keys.md", + "box tasks create 219-my-task.md --title \"Task title\"", + ], + "box fleet": [ + "box fleet status # Node health & CDP table", + "box fleet watch # Stream status updates", + "box fleet restart muse", + "box fleet heal dev", + ], + "box approvals": [ + "box approvals check # Check pending browser approvals", + "box approvals allow 646 # Approve pending browser request", + "box approvals allow muse --always # Whitelist site permanently", + ], + "box dm": [ + "box dm log --limit 10 # View recent direct messages", + "box dm send dev \"Tunnel is down\"", + "box dm wo 646 \"Restore root authorized_keys\"", + ], + "box job": [ + "box job list # List scheduled & autonomous jobs", + "box job show <job_id>", + "box job run <job_id> # Trigger execution immediately", + ], + "box tmux": [ + "box tmux list # List tmux worker sessions", + "box tmux auto status # Status of tmux auto-approver", + ], +} + +PRIMARY_DOMAINS = [ + ("work", "Fleet workspace, task orchestration, worker scope, signals"), + ("tasks", "Agent task file queue (pending/claimed/done)"), + ("fleet", "Node health, CDP status, active tabs, watch, restart, heal"), + ("approvals", "Inspect and handle agent browser & gateway approvals"), + ("dm", "Direct messaging pipeline between operators and agents"), + ("job", "Scheduled & autonomous job management"), + ("tmux", "Tmux runtime & worker session manager"), + ("sysop", "Fleet operations installer (systemd units & timers)"), + ("help", "Comprehensive manual and documentation for any command"), +] + +def format_error_shorthand(parser, message): + lines = [] + lines.append(f"\n{c_bold(c_red('āŒ CLI ERROR:'))} {c_bold(message)}\n") + lines.append(c_bold(c_yellow("šŸ’” SHORTHAND USAGE HELPER:"))) + lines.append(f" Command: {c_bold(parser.prog)}") + + sub_action = next((a for a in parser._actions if isinstance(a, argparse._SubParsersAction)), None) + if sub_action: + if parser.prog in ("box", "super"): + lines.append(f"\n{c_bold(' Primary Domains & Commands:')}") + for d, desc in PRIMARY_DOMAINS: + lines.append(f" • {c_bold(f'{d:<12}')} {c_dim(desc)}") + else: + lines.append(f"\n{c_bold(' Available Subcommands:')}") + for name, subp in sub_action.choices.items(): + h = subp.description or getattr(subp, "help", "") or "" + if not h and getattr(sub_action, "_choices_actions", None): + for ca in sub_action._choices_actions: + if ca.dest == name: + h = ca.help or "" + break + lines.append(f" • {c_bold(f'{name:<12}')} {c_dim(h)}") + + positionals = [a for a in parser._actions if not a.option_strings and a.dest != 'help' and not isinstance(a, argparse._SubParsersAction)] + required_options = [a for a in parser._actions if a.option_strings and a.required and a.dest != 'help'] + optional_options = [a for a in parser._actions if a.option_strings and not a.required and a.dest != 'help'] + + if positionals or required_options: + lines.append(f"\n{c_bold(' Required Parameters / Arguments:')}") + for a in positionals: + lines.append(f" • {c_bold(f'{a.dest:<14}')} {a.help or '(positional)'}") + for a in required_options: + opts = "/".join(a.option_strings) + lines.append(f" • {c_bold(f'{opts:<14}')} {a.help or '(required flag)'}") + + if optional_options: + lines.append(f"\n{c_bold(' Optional Flags:')}") + for a in optional_options: + opts = "/".join(a.option_strings) + lines.append(f" • {c_cyan(f'{opts:<14}')} {c_dim(a.help or '')}") + + prog_key = parser.prog.strip() + if prog_key.startswith("super "): + prog_key = "box " + prog_key[6:] + examples = COMMAND_EXAMPLES.get(prog_key) + if not examples: + parts = prog_key.split() + if len(parts) > 2: + parent_key = " ".join(parts[:2]) + examples = COMMAND_EXAMPLES.get(parent_key) + + if examples: + lines.append(f"\n{c_bold(' Quick Examples:')}") + for ex in examples: + lines.append(f" {c_green(ex)}") + + lines.append(f"\n šŸ“– {c_dim('For complete manual:')} {c_bold(f'{parser.prog} --help')} {c_dim('(or')} {c_bold(f'box help {parser.prog.split()[-1]}')}{c_dim(')')}\n") + return "\n".join(lines) + +class BoxArgumentParser(argparse.ArgumentParser): + def error(self, message): + print(format_error_shorthand(self, message), file=sys.stderr) + sys.exit(2) + + def format_help(self): + base_help = super().format_help() + prog_key = self.prog.strip() + if prog_key.startswith("super "): + prog_key = "box " + prog_key[6:] + examples = COMMAND_EXAMPLES.get(prog_key) + if not examples: + parts = prog_key.split() + if len(parts) > 2: + parent_key = " ".join(parts[:2]) + examples = COMMAND_EXAMPLES.get(parent_key) + + extra = [] + if examples: + extra.append(c_bold("\nSHORTHAND EXAMPLES:")) + for ex in examples: + extra.append(f" {c_green(ex)}") + + extra.append(c_bold("\nOPERATIONAL GUIDELINES:")) + extra.append(f" • {c_cyan('Shorthand parameter reference:')} run {c_bold('box')} alone") + extra.append(f" • {c_cyan('Master comprehensive manual:')} run {c_bold('box help')} or {c_bold('box help <domain>')}") + extra.append(f" • {c_cyan('JSON output:')} append {c_bold('--json')} to any query command\n") + + return base_help + "\n".join(extra) + +def print_box_usage_reference(): + """Prints categorized primary domains and input parameters when box is run alone.""" + print(c_bold("\n=== BOX ORCHESTRATOR: INPUT PARAMETERS & USAGE REFERENCE ===\n")) + print(f"Usage: {c_bold('box <domain> [action] [arguments...] [options...]')}") + print(f" {c_bold('box help [domain]')} | {c_bold('box <domain> --help')}\n") + print(c_bold("PRIMARY DOMAINS & INPUT PARAMETERS:")) + + domains_spec = [ + ("work", "Fleet workspace, task orchestration, worker scope, and active signals", [ + ("box work [status]", "Show full operational work dashboard & worker signals"), + ("box work check [agent]", "Pre-flight health gates (Hatch, Restore, Git Config)"), + ("box work heal <agent>", "Automated remediation (tokens, collaborator, dial-in, chat)"), + ("box work start \"<title>\" --to <agent> [--goal \"<goal>\"] [--force]", "Instantly start & assign new ticket to agent"), + ("box work assign <issue#> --to <agent> [--force]", "Assign existing Gitea ticket to an agent"), + ("box work merge <pr#>", "Verify test suite and merge PR to master"), + ("box work chats [--agent <name>] [--limit <n>]", "Inspect live agent chat feeds with stream filtering"), + ]), + ("tasks", "Agent task file queue (fleet/tasks/{pending,claimed,done})", [ + ("box tasks list [--queue pending|claimed|done|all]", "List task queue files across queues"), + ("box tasks show <task-name>", "Print contents of a task file"), + ("box tasks create <name> --title \"<title>\"", "Write new pending task from template"), + ]), + ("fleet", "Node health, CDP status, active tabs, watch, restart, heal", [ + ("box fleet [status]", "Show NetVM node status table (muse, pip, 646, opm, dev, def)"), + ("box fleet watch [--interval <sec>]", "Live streaming status monitor"), + ("box fleet restart <node>", "Restart node browser & services"), + ("box fleet cdp <node>", "Print DevTools Protocol endpoint URL"), + ("box fleet heal <node>", "Run node remediation"), + ]), + ("approvals", "Inspect and handle agent browser & gateway approvals", [ + ("box approvals check [--node <name>] [-v]", "List pending modal browser approval prompts"), + ("box approvals allow <node> [--always]", "Approve pending browser prompt"), + ("box approvals inspect <node>", "Inspect active DOM modal elements"), + ]), + ("dm", "Direct messaging pipeline between operators and agents", [ + ("box dm log [--node <name>] [--limit <n>]", "Read signed message log"), + ("box dm send <target> \"<message>\"", "Send message to node/agent"), + ("box dm wo <agent> \"<instruction>\"", "Send formal work order to agent"), + ("box dm ack <msg_id>", "Acknowledge received work order"), + ]), + ("job", "Scheduled & autonomous job management", [ + ("box job list [--all]", "List configured jobs and timers"), + ("box job show <job_id>", "Display job configuration"), + ("box job run <job_id>", "Trigger immediate execution"), + ("box job status <job_id>", "Check execution status"), + ]), + ("tmux", "Tmux runtime & worker session manager", [ + ("box tmux [list]", "List active sessions on socket"), + ("box tmux auto [status|watch]", "Monitor automated approval daemon"), + ]), + ("sysop", "Fleet operations installer", [ + ("box sysop install [--dry-run]", "Install & verify systemd units and timers"), + ]), + ("help", "Comprehensive manual and documentation for any command", [ + ("box help [domain]", "Deep documentation & manual"), + ]), + ] + + for name, desc, cmds in domains_spec: + print(f" {c_bold(c_cyan(f'{name:<11}'))} {c_dim(desc)}") + for cmd_syntax, cmd_desc in cmds: + print(f" • {c_bold(cmd_syntax):<64} {c_dim(cmd_desc)}") + print() + + print(c_bold("QUICK DISPATCH SHORTCUTS:")) + print(f" Start Task: {c_green('box work start \"<title>\" --to <agent>')}") + print(f" Merge PR: {c_green('box work merge <pr#>')}") + print(f" Heal Agent: {c_green('box work heal <agent>')}") + print(f" Check Health: {c_green('box work check [agent]')}") + print(f"\n{c_dim('Run')} {c_bold('box <domain> --help')} {c_dim('or')} {c_bold('box help <domain>')} {c_dim('for full manuals and argument details.')}\n") + +def print_master_help(): + """Prints comprehensive, deep master manual for box help / box --help.""" + banner = """ +================================================================================ + BOX ORCHESTRATOR COMPREHENSIVE CLI & RUNTIME MANUAL +================================================================================ +""" + print(c_bold(banner)) + print(f"""{c_bold("SYNOPSIS:")} + box <domain> [action] [arguments...] [options...] + box help [domain] + box <domain> --help | box <domain> <action> --help + +{c_bold("OVERVIEW:")} + The 'box' CLI is the unified orchestration tool for NetVM nodes, cloud muse + agents (opm, 646, dev, pip, def, muse, muse-main), Gitea CI/CD build tasks, + approval workflows, DM message routing, scheduled jobs, and persistent tmux runtimes. + +{c_bold("CORE ARCHITECTURE & WORKER ROLES:")} + • {c_bold("opm")} (port 2228) : Fleet orchestrator & lead coordinator + • {c_bold("646")} (port 2226) : System & core runtime operator + • {c_bold("dev")} (port 2230) : Feature development & dark-node builder + • {c_bold("pip")} (port 2227) : Integration & Python builder + • {c_bold("def")} (port 2229) : Defense & telemetry monitor + • {c_bold("muse")} (port 2225) : Cloud workspace agent + • {c_bold("muse-main")} (port 2224) : GCP host node & tunnel anchor + +{c_bold("DOMAINS & ACTION SPECIFICATIONS:")} + +1. {c_bold("WORK & BUILD PIPELINE (box work ...)")} + Orchestrates autonomous cloud agents, Gitea issue-to-branch pipelines, PR merges, + and pre-flight node health verification. + • {c_bold("box work [status]")} + Parameters: None (optional --json) + Description: Full operational dashboard (worker scope, signals, tickets, PRs, chats). + • {c_bold("box work check [agent]")} + Parameters: agent (optional positional: opm, 646, dev, pip, def, muse) + Description: Pre-flight health gates (Hatch reverse tunnels, Restore persistence, Git credentials). + • {c_bold("box work heal <agent>")} + Parameters: agent (required positional) + Description: Automated self-healing engine (Gitea collaborator rights, partition tokens, + SSH container credential injection, chat recovery nudge). + • {c_bold("box work start \"<title>\" --to <agent> [--goal \"<goal>\"] [--force]")} + Parameters: + title (required positional): Short ticket title + --to (required flag): Target worker agent + --goal (optional flag): Detailed instructions / task goal + --force (optional flag): Bypass failed pre-flight health gate + Description: Runs pre-flight health gate, auto-heals if blocked, creates Gitea Issue #N, + and dispatches briefing directly into agent live chat. + • {c_bold("box work assign <issue#> --to <agent> [--force]")} + Parameters: + issue# (required positional integer): Existing Gitea issue number + --to (required flag): Target agent + Description: Reassigns issue, verifies pre-flight health, notifies agent. + • {c_bold("box work merge <pr#>")} + Parameters: pr# (required positional integer): Pull Request number + Description: Runs test suite verification, merges PR into master, and triggers + post-receive loop terminus hook. + • {c_bold("box work chats [--agent <name>] [--limit <n>]")} + Parameters: + --agent (optional flag): Filter events for specific agent + --limit (optional flag, default 10): Number of events to show + Description: Multi-agent live chat log viewer with agent-scoped stream filtering. + +2. {c_bold("TASK FILE QUEUE (box tasks ...)")} + File-backed agent task queues in fleet/tasks/{{pending,claimed,done}}. + • {c_bold("box tasks list [--queue pending|claimed|done|all] [--dir <path>]")} + • {c_bold("box tasks show <name>")} + • {c_bold("box tasks create <name> --title \"<title>\"")} + +3. {c_bold("FLEET & NODE MANAGEMENT (box fleet ...)")} + Controls Chromium NetVM nodes, D-Bus network namespaces, and CDP endpoints. + • {c_bold("box fleet [status]")} Show active nodes, latencies, threads + • {c_bold("box fleet watch [--interval <sec>]")} Real-time continuous monitoring + • {c_bold("box fleet restart <node>")} Restart node browser/profile + • {c_bold("box fleet cdp <node>")} Show DevTools protocol endpoint + • {c_bold("box fleet heal <node>")} Remediate crashed or stuck node + +4. {c_bold("BROWSER APPROVALS & GATEWAYS (box approvals ...)")} + Inspects and resolves browser modal prompts, ethical-captcha gates, and domain permissions. + • {c_bold("box approvals check [--node <name>] [-v]")} List pending approvals + • {c_bold("box approvals allow <node> [--always]")} Approve pending request + • {c_bold("box approvals inspect <node>")} Inspect active DOM modal elements + +5. {c_bold("DIRECT MESSAGING & WORK ORDERS (box dm ...)")} + Encrypted and signed inter-agent communication pipeline. + • {c_bold("box dm log [--node <name>] [--limit <n>]")} Read signed message log + • {c_bold("box dm send <target> \"<message>\"")} Send message to peer node + • {c_bold("box dm wo <agent> \"<instruction>\"")} Issue formal agent work order + • {c_bold("box dm ack <msg_id>")} Acknowledge received work order + +6. {c_bold("SCHEDULED JOBS (box job ...)")} + Background automation and recurrent job scheduling. + • {c_bold("box job list [--all]")} List all jobs and timers + • {c_bold("box job show <job_id>")} Inspect job JSON configuration + • {c_bold("box job run <job_id>")} Trigger one-shot immediate run + +7. {c_bold("TMUX PERSISTENCE RUNTIME (box tmux ...)")} + Headless terminal session management and auto-approval agents. + • {c_bold("box tmux [list]")} List active sessions on socket + • {c_bold("box tmux auto [status|watch]")} Monitor automated approval daemon + +{c_bold("ENVIRONMENT & CONFIGURATION:")} + NETVM_ROOT Path to NetVM workspace root (default: /home/super/Projects/NetVM) + CLICOLOR_FORCE Set to 1 to force ANSI color output in non-tty pipes + NO_COLOR Set to disable ANSI color formatting + GITEA_URL Base URL for Gitea API (auto-detected: loopback on bl, public domain on PC) + GITEA_TOKEN API token for Gitea automation + +{c_bold("EXIT CODES:")} + 0 Success + 1 Operational or pre-flight failure + 2 CLI syntax or missing argument error + +Run 'box <domain> --help' or 'box help <domain>' for in-depth flags on any command. +""") + +def build_parser(): + common = BoxArgumentParser(add_help=False) + common.add_argument("--json", action="store_true", help="Output machine-readable JSON") + + prog_name = Path(sys.argv[0]).name if sys.argv and sys.argv[0] else "box" + if prog_name.endswith(".py"): + prog_name = "box" + + parser = BoxArgumentParser( + prog=prog_name, + description=f"{prog_name} — Unified Orchestrator CLI for NetVM & Box", + formatter_class=argparse.RawDescriptionHelpFormatter, + parents=[common] + ) + subparsers = parser.add_subparsers(dest="domain", help="Orchestration Domain") + + # Domain: FLEET + p_fleet = subparsers.add_parser("fleet", parents=[common], help="Node health, CDP status, active tabs, watch, restart, heal") + p_fleet.add_argument("action", nargs="?", default="status", choices=["status", "watch", "restart", "cdp", "heal"]) + p_fleet.add_argument("node", nargs="?", default=None, help="Target node (for restart / cdp / heal)") + p_fleet.add_argument("--interval", type=int, default=2, help="Watch refresh interval in seconds") + + # Domain: WATCHDOG + p_watchdog = subparsers.add_parser("watchdog", parents=[common], help="Watchdog timers: status, trigger runs") + p_watchdog.add_argument("action", nargs="?", default="status", choices=["status", "run"]) + p_watchdog.add_argument("target", nargs="?", default=None, help="Target node (or 'relay') for run") + + # Domain: APPROVALS + p_approvals = subparsers.add_parser("approvals", parents=[common], help="Inspect and handle agent browser & gateway approvals") + p_approval = subparsers.add_parser("approval", parents=[common], help="Alias for 'approvals'") + + for p_app in (p_approvals, p_approval): + p_app.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node") + app_sub = p_app.add_subparsers(dest="app_action") + + p_app_check = app_sub.add_parser("check", parents=[common], help="Check fleet approval states") + p_app_check.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node") + p_app_check.add_argument("-v", "--verbose", action="store_true", help="Show detailed task prompts, thread URLs, and card text") + + p_app_list = app_sub.add_parser("list", parents=[common], help="Alias for 'check'") + p_app_list.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node") + p_app_list.add_argument("-v", "--verbose", action="store_true", help="Show detailed task prompts, thread URLs, and card text") + + p_app_inspect = app_sub.add_parser("inspect", parents=[common], help="Inspect detailed approval and input-wait status on a node") + p_app_inspect.add_argument("node", choices=VALID_NODES, help="Target node to inspect") + + p_app_allow = app_sub.add_parser("allow", parents=[common], help="Approve pending browser request") + p_app_allow.add_argument("node", choices=VALID_NODES, help="Target node to approve") + p_app_allow.add_argument("--always", action="store_true", help="Click 'Always allow this site' instead of 'Allow once'") + p_app_allow.add_argument("--force", action="store_true", help="Force approval even if target is untrusted") + p_app_allow.add_argument("--message", default=None, help="Optional message/credential to deliver to the waiting agent") + p_app_allow.add_argument("--allow-main-chat", action="store_true", help="Allow reply into Main Chat if agent is waiting there") + + p_app_approve = app_sub.add_parser("approve", parents=[common], help="Alias for 'allow'") + p_app_approve.add_argument("node", choices=VALID_NODES, help="Target node to approve") + p_app_approve.add_argument("--always", action="store_true", help="Click 'Always allow this site' instead of 'Allow once'") + p_app_approve.add_argument("--force", action="store_true", help="Force approval even if target is untrusted") + p_app_approve.add_argument("--message", default=None, help="Optional message/credential to deliver to the waiting agent") + p_app_approve.add_argument("--allow-main-chat", action="store_true", help="Allow reply into Main Chat if agent is waiting there") + + p_app_deny = app_sub.add_parser("deny", parents=[common], help="Deny pending browser request") + p_app_deny.add_argument("node", choices=VALID_NODES, help="Target node to deny") + p_app_deny.add_argument("--message", default=None, help="Optional message to deliver to the waiting agent") + p_app_deny.add_argument("--allow-main-chat", action="store_true", help="Allow reply into Main Chat if agent is waiting there") + + p_app_auto = app_sub.add_parser("auto", parents=[common], help="Auto-approve all trusted requests across fleet") + p_app_auto.add_argument("--node", choices=VALID_NODES, default=None, help="Target node (or all nodes)") + + p_app_watch = app_sub.add_parser("watch", parents=[common], help="Live watch pending approvals") + p_app_watch.add_argument("--interval", type=int, default=2, help="Watch refresh interval in seconds") + p_app_watch.add_argument("--auto", action="store_true", help="Automatically approve trusted requests as they appear") + p_app_watch.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node") + + p_app_reply = app_sub.add_parser("reply", parents=[common], help="Reply to a waiting agent task/prompt in its active thread") + p_app_reply.add_argument("node", choices=VALID_NODES, help="Target node to reply to") + p_app_reply.add_argument("message", help="Message or answer to dispatch") + p_app_reply.add_argument("--allow-main-chat", action="store_true", help="Explicit override if the active thread is Main Chat") + + p_app_dismiss = app_sub.add_parser("dismiss", parents=[common], help="Dismiss any open task dialog/popup on a node") + p_app_dismiss.add_argument("node", choices=VALID_NODES, help="Target node to dismiss dialog on") + + p_app_clear = app_sub.add_parser("clear", parents=[common], help="Clear and dismiss pending input waits on a node or all nodes") + p_app_clear.add_argument("node", nargs="?", choices=VALID_NODES, default=None, help="Target node (or omit for all nodes)") + + p_app_clear_all = app_sub.add_parser("clear-all", parents=[common], help="Clear and dismiss all pending input waits across fleet") + + p_app_req_key = app_sub.add_parser("request-key", parents=[common], help="Request operator passkey/key approval for an agent") + p_app_req_key.add_argument("node", choices=VALID_NODES, help="Target node requesting key") + p_app_req_key.add_argument("--reason", default="Passkey authentication required", help="Reason for key request") + + p_app_gates = app_sub.add_parser("gates", aliases=["gate"], parents=[common], help="Inspect coordinator decision record gates") + p_app_gates.add_argument("--scope", default=None, help="Check specific gate scope (e.g. role-layer, merges-to-main)") + + # Domain: MUSE-CHOICES + p_mc = subparsers.add_parser("muse-choices", parents=[common], help="Muse TUI A/B/C choice auto-answer daemon (on/off/status/logs)") + mc_sub = p_mc.add_subparsers(dest="mc_action") + + p_mc_on = mc_sub.add_parser("on", parents=[common], help="Enable auto-answers on all Muse panes") + p_mc_on.add_argument("--dry-run", action="store_true", help="Log answers instead of sending keys") + + p_mc_off = mc_sub.add_parser("off", parents=[common], help="Disable auto-answers and stop all watchers") + + p_mc_status = mc_sub.add_parser("status", parents=[common], help="Show desired state, watchers, recent answers") + + p_mc_logs = mc_sub.add_parser("logs", parents=[common], help="Tail one pane watcher log") + p_mc_logs.add_argument("--socket", required=True, help="Tmux socket path (e.g. /tmp/tmux-1000/default)") + p_mc_logs.add_argument("--pane", required=True, help="Pane id (e.g. %%37)") + p_mc_logs.add_argument("-n", type=int, default=20, help="Lines to show (default: 20)") + + p_mc_rec = mc_sub.add_parser("reconcile", parents=[common], help="Enforce desired state now (start missing / stop excess)") + + p_mc_res = mc_sub.add_parser("resolve", parents=[common], help="Resolve a held prompt (approve now or deny it)") + p_mc_res.add_argument("--socket", required=True, help="Tmux socket path (e.g. /tmp/tmux-1000/default)") + p_mc_res.add_argument("--pane", required=True, help="Pane id (e.g. %%37)") + p_mc_res.add_argument("decision", choices=["approve", "deny"], help="Release the hold to approve, or deny it (permission kinds only)") + + # Domain: RUNTIME + p_rt = subparsers.add_parser("runtime", parents=[common], help="Muse CLI tmux runtimes: list/send/launch/reconcile/kill/restart/brief") + rt_sub = p_rt.add_subparsers(dest="rt_action") + + p_rt_list = rt_sub.add_parser("list", parents=[common], help="List panes with runtime state + approval posture (default)") + p_rt_list.add_argument("--socket", default=None, help="Only this tmux socket") + p_rt_list.add_argument("--muse-only", action="store_true", help="Only Muse CLI panes") + + p_rt_send = rt_sub.add_parser("send", parents=[common], help="Send keys to a pane (reports pre-send state)") + p_rt_send.add_argument("--socket", default=None, help="Tmux socket (default: /tmp/tmux-1000/default)") + p_rt_send.add_argument("pane", help="Pane id (e.g. %%37)") + p_rt_send.add_argument("keys", help="Keys / text to send") + p_rt_send.add_argument("--no-enter", action="store_true", help="Do not send Enter after keys") + + p_rt_open = rt_sub.add_parser("open", parents=[common], help="Attach to a session on a socket (execs tmux attach)") + p_rt_open.add_argument("--socket", default=None, help="Tmux socket (default: /tmp/tmux-1000/default)") + p_rt_open.add_argument("--session", default=None, help="Session name (default: the only session)") + p_rt_open.add_argument("--dry-run", action="store_true", help="Print the attach plan without attaching") + + p_rt_launch = rt_sub.add_parser("launch", parents=[common], help="Launch a Muse session with approval trail injected (on-request)") + p_rt_launch.add_argument("--socket", default=None, help="Tmux socket (default: /tmp/tmux-muse.sock for Box fleet)") + p_rt_launch.add_argument("--session", required=True, help="New tmux session name") + p_rt_launch.add_argument("--window", "-w", default=None, help="Initial window name") + p_rt_launch.add_argument("--dry-run", action="store_true", help="Print the launch plan without creating") + p_rt_launch.add_argument("muse_args", nargs=argparse.REMAINDER, default=[], help="Extra muse args after --") + + p_rt_layout = rt_sub.add_parser("layout", parents=[common], help="Show pane geometry vs approval minimums") + p_rt_layout.add_argument("--socket", default=None, help="Only this tmux socket") + + p_rt_spread = rt_sub.add_parser("spread", parents=[common], help="Break squeezed runtimes into own windows") + p_rt_spread.add_argument("--socket", default=None, help="Only this tmux socket") + p_rt_spread.add_argument("--session", default=None, help="Only this tmux session") + p_rt_spread.add_argument("--dry-run", action="store_true", help="Print the spread plan without moving panes") + + p_rt_rec = rt_sub.add_parser("reconcile", parents=[common], help="Enforce fleet/agents.json: relaunch missing, brief fresh panes, requeue stale claims") + p_rt_rec.add_argument("--manifest", default=None, help="Manifest path (default: fleet/agents.json)") + p_rt_rec.add_argument("--tasks", default=None, help="Task queue dir (default: alongside manifest)") + p_rt_rec.add_argument("--dry-run", action="store_true", help="Print the plan without changing anything") + p_rt_rec.add_argument("--adopt", action="store_true", help="Record live sessions as briefed without sending") + + p_rt_kill = rt_sub.add_parser("kill", parents=[common], help="Kill a session on a socket") + p_rt_kill.add_argument("--socket", default=None, help="Tmux socket (default: /tmp/tmux-1000/default)") + p_rt_kill.add_argument("--session", required=True, help="Session name to kill") + + p_rt_restart = rt_sub.add_parser("restart", parents=[common], help="Kill + relaunch + brief one manifest agent") + p_rt_restart.add_argument("--socket", default=None, help="Tmux socket (default: /tmp/tmux-1000/default)") + p_rt_restart.add_argument("--session", required=True, help="Manifest session name") + p_rt_restart.add_argument("--manifest", default=None, help="Manifest path (default: fleet/agents.json)") + p_rt_restart.add_argument("--dry-run", action="store_true", help="Print the plan without changing anything") + + p_rt_brief = rt_sub.add_parser("brief", parents=[common], help="Send the manifest brief to a live idle pane") + p_rt_brief.add_argument("--socket", default=None, help="Tmux socket (default: /tmp/tmux-1000/default)") + p_rt_brief.add_argument("--session", required=True, help="Manifest session name") + p_rt_brief.add_argument("--manifest", default=None, help="Manifest path (default: fleet/agents.json)") + p_rt_brief.add_argument("--dry-run", action="store_true", help="Print the plan without changing anything") + + p_work = subparsers.add_parser("work", parents=[common], help="Fleet workspace, task orchestration, worker scope, and active signals") + work_sub = p_work.add_subparsers(dest="work_action") + p_w_status = work_sub.add_parser("status", parents=[common], help="Show full operational work dashboard (default)") + p_w_check = work_sub.add_parser("check", parents=[common], help="Run pre-flight health checks (Hatch, Restore, Git Config)") + p_w_check.add_argument("agent", nargs="?", help="Optional specific agent name to check") + p_w_heal = work_sub.add_parser("heal", parents=[common], help="Run automated healing on an agent") + p_w_heal.add_argument("agent", help="Agent username to heal") + p_w_start = work_sub.add_parser("start", parents=[common], help="Instantly start and assign new build ticket to an agent") + p_w_start.add_argument("title", help="Ticket title / summary") + p_w_start.add_argument("--to", dest="agent", required=True, help="Agent username (opm, 646, dev, pip, def, muse)") + p_w_start.add_argument("--goal", help="Optional detailed goal description") + p_w_start.add_argument("--force", action="store_true", help="Bypass pre-flight health gate") + p_w_assign = work_sub.add_parser("assign", parents=[common], help="Assign existing ticket to an agent") + p_w_assign.add_argument("issue", type=int, help="Issue number (e.g. 215)") + p_w_assign.add_argument("--to", dest="agent", required=True, help="Agent username") + p_w_assign.add_argument("--force", action="store_true", help="Bypass pre-flight health gate") + p_w_merge = work_sub.add_parser("merge", parents=[common], help="Merge an open PR into master") + p_w_merge.add_argument("pr", type=int, help="Pull request number (e.g. 214)") + p_w_chats = work_sub.add_parser("chats", parents=[common], help="View recent live chat activity") + p_w_chats.add_argument("--agent", help="Filter by agent name") + p_w_chats.add_argument("--limit", type=int, default=10, help="Number of messages to show") + + p_tasks = subparsers.add_parser("tasks", parents=[common], help="Agent work queue: pending/claimed/done files (distinct from scheduled jobs)") + p_tasks.add_argument("--dir", default=None, help="Task queue dir (default: fleet/tasks)") + tasks_sub = p_tasks.add_subparsers(dest="tasks_action") + + p_t_list = tasks_sub.add_parser("list", parents=[common], help="List tasks across queues (default)") + p_t_list.add_argument("--dir", default=argparse.SUPPRESS, help="Task queue dir (default: fleet/tasks)") + p_t_list.add_argument("--queue", choices=["pending", "claimed", "done", "all"], default="all", help="Only this queue") + + p_t_show = tasks_sub.add_parser("show", parents=[common], help="Print one task file") + p_t_show.add_argument("--dir", default=argparse.SUPPRESS, help="Task queue dir (default: fleet/tasks)") + p_t_show.add_argument("name", help="Task name (base or owner-suffixed)") + + p_t_create = tasks_sub.add_parser("create", parents=[common], help="Write a new pending task from template") + p_t_create.add_argument("--dir", default=argparse.SUPPRESS, help="Task queue dir (default: fleet/tasks)") + p_t_create.add_argument("name", help="Task name like 012-slug.md") + p_t_create.add_argument("--title", required=True, help="Short title") + p_t_create.add_argument("--goal", required=True, help="Goal text") + p_t_create.add_argument("--steps", default="", help="Steps text") + p_t_create.add_argument("--dry-run", action="store_true", help="Print the plan without writing") + + p_t_claim = tasks_sub.add_parser("claim", parents=[common], help="Atomically claim a pending task") + p_t_claim.add_argument("--dir", default=argparse.SUPPRESS, help="Task queue dir (default: fleet/tasks)") + p_t_claim.add_argument("name", help="Pending task name") + p_t_claim.add_argument("--as", dest="owner", required=True, help="Owner tmux session name") + p_t_claim.add_argument("--dry-run", action="store_true", help="Print the plan without moving") + + p_t_done = tasks_sub.add_parser("done", parents=[common], help="Append notes and move a claim to done/") + p_t_done.add_argument("--dir", default=argparse.SUPPRESS, help="Task queue dir (default: fleet/tasks)") + p_t_done.add_argument("name", help="Claimed task name (base or suffixed)") + p_t_done.add_argument("--result", default="", help="Result notes to append") + p_t_done.add_argument("--dry-run", action="store_true", help="Print the plan without moving") + + p_t_req = tasks_sub.add_parser("requeue", parents=[common], help="Move a claim back to pending/") + p_t_req.add_argument("--dir", default=argparse.SUPPRESS, help="Task queue dir (default: fleet/tasks)") + p_t_req.add_argument("name", help="Claimed task name (base or suffixed)") + p_t_req.add_argument("--dry-run", action="store_true", help="Print the plan without moving") + + p_t_sweep = tasks_sub.add_parser("sweep", parents=[common], help="Requeue stale/dead-owner claims now") + p_t_sweep.add_argument("--dir", default=argparse.SUPPRESS, help="Task queue dir (default: fleet/tasks)") + p_t_sweep.add_argument("--manifest", default=None, help="Manifest path (default: fleet/agents.json)") + p_t_sweep.add_argument("--dry-run", action="store_true", help="Print the plan without moving") + + # Domain: INVITE + p_invite = subparsers.add_parser("invite", parents=[common], help="Muse.ai invite codes: find per-agent codes and redeem") + p_invite.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node (status)") + inv_sub = p_invite.add_subparsers(dest="invite_action") + + p_inv_status = inv_sub.add_parser("status", parents=[common], help="Invite code matrix across fleet (default)") + p_inv_status.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node") + + p_inv_list = inv_sub.add_parser("list", parents=[common], help="Alias for 'status'") + p_inv_list.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node") + + p_inv_code = inv_sub.add_parser("code", parents=[common], help="Show one agent's invite code") + p_inv_code.add_argument("node", choices=VALID_NODES, help="Target node") + + p_inv_find = inv_sub.add_parser("find", parents=[common], help="Alias for 'code'") + p_inv_find.add_argument("node", choices=VALID_NODES, help="Target node") + + p_inv_redeem = inv_sub.add_parser("redeem", parents=[common], help="Redeem an invite code on a node") + p_inv_redeem.add_argument("node", choices=VALID_NODES, help="Target node") + p_inv_redeem.add_argument("code", help="6-char invite code") + p_inv_redeem.add_argument("--method", choices=["auto", "dom", "api"], default="auto", help="Redemption method (default: auto)") + p_inv_redeem.add_argument("--notify", default=None, help="Sidechat to notify on loopback") + p_inv_redeem.add_argument("--notify-agent", default=None, help="Agent to notify on loopback") + + p_inv_salvage = inv_sub.add_parser("salvage", parents=[common], help="Salvage an out-of-tokens agent (defaults to 646)") + p_inv_salvage.add_argument("node", nargs="?", default="646", choices=VALID_NODES, help="Blocked agent node (default: 646)") + p_inv_salvage.add_argument("--helper", choices=VALID_NODES, help="Specific helper agent to redeem code") + p_inv_salvage.add_argument("--notify", default=None, help="Sidechat to notify on loopback") + p_inv_salvage.add_argument("--notify-agent", default=None, help="Agent to notify on loopback") + + # Domain: USAGE + p_usage = subparsers.add_parser("usage", parents=[common], help="Muse.ai usage limits per agent") + p_usage.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node") + p_usage.add_argument("node_pos", nargs="?", default=None, choices=VALID_NODES, help="Optional node positional argument") + + # Domain: SETTINGS + p_settings = subparsers.add_parser("settings", parents=[common], help="Settings menu RPA automation") + settings_sub = p_settings.add_subparsers(dest="settings_action") + p_set_usage = settings_sub.add_parser("usage", parents=[common], help="Extract usage via Settings RPA") + p_set_usage.add_argument("node", choices=VALID_NODES, default="646", nargs="?", help="Target node (default: 646)") + p_set_check = settings_sub.add_parser("check-redeem", parents=[common], help="Check if Redeem Invite Code is in General Settings") + p_set_check.add_argument("node", choices=VALID_NODES, default="646", nargs="?", help="Target node (default: 646)") + + # Domain: ONBOARD + p_onboard = subparsers.add_parser("onboard", parents=[common], help="Agent onboarding & automated invite code salvage pipeline") + onboard_sub = p_onboard.add_subparsers(dest="onboard_action") + + p_onb_start = onboard_sub.add_parser("start", parents=[common], help="Start onboarding pipeline (provisions infra + initiates auth)") + p_onb_start.add_argument("node", help="Target new node label (e.g. dev2, client1)") + p_onb_start.add_argument("--email", required=True, help="Client login email") + p_onb_start.add_argument("--for", dest="for_agent", choices=VALID_NODES, help="Beneficiary agent to unblock (defaults to most urgent)") + p_onb_start.add_argument("--code", help="Explicit 6-character invite code to redeem") + p_onb_start.add_argument("--account-name", help="Display name hint for multi-account selection") + + p_onb_otp = onboard_sub.add_parser("submit-otp", parents=[common], help="Submit transient OTP and complete auto-redemption") + p_onb_otp.add_argument("node", help="Target node label") + p_onb_otp.add_argument("otp", help="6-digit verification code") + p_onb_otp.add_argument("--email", help="Client email (optional)") + + p_onb_stat = onboard_sub.add_parser("status", parents=[common], help="Query onboarding pipeline state for a node") + p_onb_stat.add_argument("node", help="Target node label") + + p_onb_wo = onboard_sub.add_parser("salvage-wo", parents=[common], help="Dispatch cryptographically signed salvage work order to sidechat") + p_onb_wo.add_argument("node", nargs="?", default="646", choices=VALID_NODES, help="Blocked agent node (default: 646)") + p_onb_wo.add_argument("--target", default="646 tasks", help="Target sidechat (default: 646 tasks)") + + onboard_sub.add_parser("feed-matrix", parents=[common], help="Display all agents ranked by feeding weight, job volume, role, and work done over time") + onboard_sub.add_parser("connects", parents=[common], help="Inventory of all active fleet nodes & client onboard connects") + + # Domain: KPI + p_kpi = subparsers.add_parser("kpi", parents=[common], help="Fleet KPI, spend monitoring & runtime preservation") + p_kpi.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node") + kpi_sub = p_kpi.add_subparsers(dest="kpi_action") + + p_kpi_status = kpi_sub.add_parser("status", parents=[common], help="Show fleet KPI dashboard (default)") + p_kpi_status.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node") + + p_kpi_report = kpi_sub.add_parser("report", parents=[common], help="Detailed KPI & preservation report for an agent") + p_kpi_report.add_argument("node", choices=VALID_NODES, help="Target node") + + p_kpi_routes = kpi_sub.add_parser("routes", parents=[common], help="Check route and CDP health across agents") + + p_kpi_spawn = kpi_sub.add_parser("spawn-worker", parents=[common], help="Spawn background tmux worker to preserve runtime") + p_kpi_spawn.add_argument("node", choices=VALID_NODES, help="Target agent") + p_kpi_spawn.add_argument("session", help="Session label") + p_kpi_spawn.add_argument("worker_command", help="Command to run in background") + + p_kpi_autospawn = kpi_sub.add_parser("auto-spawn", parents=[common], help="Reconcile idle agents and auto-spawn background tmux workers") + p_kpi_autospawn.add_argument("--node", choices=VALID_NODES, default=None, help="Filter by node") + p_kpi_autospawn.add_argument("--dry-run", action="store_true", help="Observe and report what would be spawned without executing") + + # Domain: CHROMEBOX + + p_chromebox = subparsers.add_parser("chromebox", parents=[common], help="Agent browser settings-menu toggles (chromebox RPA)") + chrome_sub = p_chromebox.add_subparsers(dest="chrome_action") + p_chrome_perm = chrome_sub.add_parser("permissions", parents=[common], help="Settings menu toggles (permissions + related tabs)") + p_chrome_perm.add_argument("node", choices=VALID_NODES, help="Target node") + perm_sub = p_chrome_perm.add_subparsers(dest="perm_action") + + p_perm_list = perm_sub.add_parser("list", parents=[common], help="List toggle states (default)") + p_perm_list.add_argument("--tab", default=None, help="Only this settings tab") + + p_perm_get = perm_sub.add_parser("get", parents=[common], help="Read one toggle") + p_perm_get.add_argument("toggle", help="Toggle address (e.g. permissions.web_access, permissions.websites:example.com)") + + p_perm_set = perm_sub.add_parser("set", parents=[common], help="Set one toggle (verified with readback)") + p_perm_set.add_argument("toggle", help="Toggle address") + p_perm_set.add_argument("value", help="Value (on/off, Allow/Ask/Deny, auto_allow/always_ask, theme name)") + + p_perm_desc = perm_sub.add_parser("describe", parents=[common], help="Full inventory of one settings tab (JSON)") + p_perm_desc.add_argument("tab", help="Settings tab name (e.g. Permissions)") + + # Domain: DM + p_dm = subparsers.add_parser("dm", parents=[common], help="Inter-agent DMs, work orders ([WO]), acks, live log tail") + dm_sub = p_dm.add_subparsers(dest="action") + + # super dm send + p_dm_send = dm_sub.add_parser("send", parents=[common], help="Send a direct message") + p_dm_send.add_argument("--to", required=True, choices=VALID_NODES, help="Recipient node") + p_dm_send.add_argument("--from", dest="from_agent", default=DEFAULT_SENDER, help="Sender identity (default: super)") + p_dm_send.add_argument("--target", default="main", help="Target chat/sidechat (default: main)") + p_dm_send.add_argument("--allow-main-chat", action="store_true", help="Explicit override allowing send directly to Main Chat") + p_dm_send.add_argument("-w", "--wait", action="store_true", help="Wait for recipient agent to reply") + p_dm_send.add_argument("-t", "--timeout", type=int, default=60, help="Wait timeout in seconds (default: 60)") + p_dm_send.add_argument("--expect-reply", action="store_true", help="Arm follow-up tracking") + p_dm_send.add_argument("--reply-timeout", type=int, default=None, help="Timeout in seconds") + p_dm_send.add_argument("--no-sign", action="store_true", help="Send without cryptographic SSH signature") + p_dm_send.add_argument("message", help="Message body") + + # super dm chat + p_dm_chat = dm_sub.add_parser("chat", parents=[common], help="Interactive conversational chat session with an agent") + p_dm_chat.add_argument("agent", choices=VALID_NODES, help="Target agent to converse with") + p_dm_chat.add_argument("--target", default=None, help="Target sidechat or 'main'") + p_dm_chat.add_argument("-t", "--timeout", type=int, default=60, help="Reply timeout in seconds (default: 60)") + + # super dm send-file + p_dm_sf = dm_sub.add_parser("send-file", parents=[common], help="Transfer a file to an agent and send a pointer in chat") + p_dm_sf.add_argument("--to", required=True, choices=VALID_NODES, help="Recipient node") + p_dm_sf.add_argument("--target", default=None, help="Target sidechat (default: agent primary task sidechat)") + p_dm_sf.add_argument("--note", default=None, help="Optional operator note or instructions") + p_dm_sf.add_argument("-w", "--wait", action="store_true", help="Wait for recipient agent to reply/acknowledge") + p_dm_sf.add_argument("-t", "--timeout", type=int, default=60, help="Reply timeout in seconds (default: 60)") + p_dm_sf.add_argument("file", help="Path to file to transfer") + + # super dm files + p_dm_files = dm_sub.add_parser("files", parents=[common], help="List transferred files and staged payloads") + p_dm_files.add_argument("files_action", nargs="?", default="list", choices=["list", "clean"], help="Action: list (default) or clean") + p_dm_files.add_argument("--agent", choices=VALID_NODES, default=None, help="Filter by recipient agent") + p_dm_files.add_argument("--older-than", type=int, default=7, help="Retention cutoff in days for clean (default: 7)") + + # super dm wo + p_dm_wo = dm_sub.add_parser("wo", parents=[common], help="Dispatch a structured Work Order ([WO:...])") + p_dm_wo.add_argument("--to", required=True, choices=VALID_NODES, help="Recipient node") + p_dm_wo.add_argument("--title", required=True, help="Work Order title") + p_dm_wo.add_argument("--priority", choices=["routine", "urgent"], default="routine", help="Priority level") + p_dm_wo.add_argument("--from", dest="from_agent", default=DEFAULT_SENDER, help="Sender identity (default: super)") + p_dm_wo.add_argument("--target", default=None, help="Target sidechat (default: agent primary task sidechat)") + p_dm_wo.add_argument("--allow-main-chat", action="store_true", help="Explicit override allowing work order in Main Chat") + p_dm_wo.add_argument("--no-wait", dest="wait", action="store_false", help="Do not wait for reply") + p_dm_wo.add_argument("-t", "--timeout", type=int, default=60, help="Wait timeout in seconds (default: 60)") + p_dm_wo.add_argument("--no-sign", action="store_true", help="Issue without cryptographic SSH signature") + p_dm_wo.add_argument("body", help="Work Order detailed description") + + # super dm ack + p_dm_ack = dm_sub.add_parser("ack", parents=[common], help="Acknowledge a DM or Work Order") + p_dm_ack.add_argument("id", help="DM ID or Work Order ID") + p_dm_ack.add_argument("--to", required=True, choices=VALID_NODES, help="Recipient node") + p_dm_ack.add_argument("--from", dest="from_agent", default=DEFAULT_SENDER, help="Sender identity (default: super)") + p_dm_ack.add_argument("--target", default="main", help="Target chat (default: main)") + + # super dm log + p_dm_log = dm_sub.add_parser("log", parents=[common], help="Display recent DM activity") + p_dm_log.add_argument("-n", type=int, default=25, help="Number of records to show") + p_dm_log.add_argument("--agent", choices=VALID_NODES, default=None, help="Filter by agent") + p_dm_log.add_argument("--filter", default=None, help="Search filter in text") + + # super dm tail + p_dm_tail = dm_sub.add_parser("tail", parents=[common], help="Live stream incoming DMs") + p_dm_tail.add_argument("--filter", default=None, help="Search filter in text") + + # super dm verify + p_dm_verify = dm_sub.add_parser("verify", parents=[common], help="Verify cryptographic signature on a DM") + p_dm_verify.add_argument("message", nargs="?", default=None, help="Message text containing signature block") + p_dm_verify.add_argument("--agent", choices=VALID_NODES, default=None, help="Scan agent's recent reads") + p_dm_verify.add_argument("--target", default=None, help="Scan target chat's recent reads") + + # Domain: THREAD + p_thread = subparsers.add_parser("thread", parents=[common], help="Inspect agent main chats, sidechats, and scrollbacks") + thread_sub = p_thread.add_subparsers(dest="action") + + p_thread_list = thread_sub.add_parser("list", parents=[common], help="List active threads for an agent or all fleet sidechats") + p_thread_list.add_argument("agent", nargs="?", default=None, choices=VALID_NODES + [None], help="Agent node to inspect (default: all registered fleet sidechats)") + + p_thread_view = thread_sub.add_parser("view", parents=[common], help="View recent messages from an agent's thread") + p_thread_view.add_argument("agent", choices=VALID_NODES, help="Agent node to inspect") + p_thread_view.add_argument("thread_id", help="Thread UUID or 'main'") + p_thread_view.add_argument("--limit", type=int, default=15, help="Number of messages to retrieve") + + # Domain: JOB + p_job = subparsers.add_parser("job", parents=[common], help="Manage scheduled jobs, systemd timers, triggers, logs") + job_sub = p_job.add_subparsers(dest="action") + + p_job_list = job_sub.add_parser("list", parents=[common], help="List defined jobs") + p_job_list.add_argument("--archived", "-a", action="store_true", help="Include archived jobs from jobs/archive/") + + p_job_show = job_sub.add_parser("show", parents=[common], help="Inspect full job definition and timer details") + p_job_show.add_argument("name", help="Job name") + + p_job_status = job_sub.add_parser("status", parents=[common], help="Check timer status") + p_job_status.add_argument("name", nargs="?", default=None, help="Job/timer name") + + p_job_create = job_sub.add_parser("create", parents=[common], help="Create or update a scheduled job") + p_job_create.add_argument("name", help="Job name (^[a-z0-9-]{1,64}$)") + p_job_create.add_argument("--agent", choices=VALID_NODES, default=None, help="Target agent") + p_job_create.add_argument("--schedule", default=None, help="Cron schedule (e.g. '*/15 * * * *' or 'manual')") + p_job_create.add_argument("--prompt", default=None, help="Prompt template") + p_job_create.add_argument("--description", default=None, help="Job description") + p_job_create.add_argument("--timeout", type=int, default=300, help="Timeout in seconds (60-3600, default: 300)") + p_job_create.add_argument("--on-failure", choices=["alert", "retry", "ignore"], default="alert", help="Failure policy") + p_job_create.add_argument("--chain-next", default=None, help="Next job name to chain on success") + p_job_create.add_argument("--sidechat-create", action="store_true", help="Dispatch in a sidechat") + p_job_create.add_argument("--sidechat-name", default=None, help="Sidechat name template") + p_job_create.add_argument("--reuse-key", default=None, help="Sidechat reuse key") + p_job_create.add_argument("--dm-target", default=None, help="Target chat or thread name") + p_job_create.add_argument("--file", default=None, help="Path to JSON file with job definition (or '-' for stdin)") + p_job_create.add_argument("--no-enable", action="store_true", help="Do not enable systemd timer immediately") + p_job_create.add_argument("--update", action="store_true", help="Allow updating an existing job") + + p_job_enable = job_sub.add_parser("enable", parents=[common], help="Enable and activate a job's systemd timer") + p_job_enable.add_argument("name", help="Job name") + + p_job_disable = job_sub.add_parser("disable", parents=[common], help="Stop and disable a job's systemd timer") + p_job_disable.add_argument("name", help="Job name") + + p_job_delete = job_sub.add_parser("delete", parents=[common], help="Delete a job and its timer") + p_job_delete.add_argument("name", help="Job name") + p_job_delete.add_argument("-y", "--yes", action="store_true", help="Bypass confirmation prompt") + p_job_delete.add_argument("--force", action="store_true", help="Force deletion even if timer error") + + p_job_run = job_sub.add_parser("run", parents=[common], help="Trigger a job immediately") + p_job_run.add_argument("name", help="Job name to trigger") + p_job_run.add_argument("-f", "--follow", action="store_true", help="Stream live dispatch and execution events") + + p_job_log = job_sub.add_parser("log", parents=[common], help="View recent job execution events") + p_job_log.add_argument("-n", type=int, default=20, help="Number of events") + p_job_log.add_argument("name", nargs="?", default=None, help="Filter by job name") + + p_job_archive = job_sub.add_parser("archive", parents=[common], help="Archive a retired manual job to jobs/archive/") + p_job_archive.add_argument("name", help="Job name to archive") + p_job_archive.add_argument("--force", action="store_true", help="Force archive even if marked protected") + + p_job_unarchive = job_sub.add_parser("unarchive", parents=[common], help="Restore an archived job from jobs/archive/") + p_job_unarchive.add_argument("name", help="Job name to restore") + + + # Domain: WEB + p_web = subparsers.add_parser("web", parents=[common], help="Probe VM web surfaces (box.muse-dev.online), auth, sync") + web_sub = p_web.add_subparsers(dest="action") + + web_sub.add_parser("health", parents=[common], help="Check Google Cloud VM web surface health") + web_sub.add_parser("test-auth", parents=[common], help="Verify operator auth gating on the VM") + web_sub.add_parser("sync", parents=[common], help="Check local log size and sync integrity") + + # Domain: CRED + p_cred = subparsers.add_parser("cred", parents=[common], help="Credential console API & client onboarding") + cred_sub = p_cred.add_subparsers(dest="action") + + p_c_init = cred_sub.add_parser("initiate", parents=[common], help="Initiate onboarding flow for a node") + p_c_init.add_argument("--node", required=True, help="Node label (e.g. opm, pip, 646)") + p_c_init.add_argument("--email", required=True, help="Client login email") + p_c_init.add_argument("--service", default="muse", help="Service name (default: muse)") + p_c_init.add_argument("--account-name", default=None, help="Display name hint for multi-account selector") + + p_c_otp = cred_sub.add_parser("submit-otp", parents=[common], help="Submit transient OTP verification code") + p_c_otp.add_argument("--node", required=True, help="Node label") + p_c_otp.add_argument("--otp", required=True, help="6-digit verification code") + p_c_otp.add_argument("--email", default=None, help="Client email (optional)") + + p_c_stat = cred_sub.add_parser("status", parents=[common], help="Query onboarding & vitality status for a node") + p_c_stat.add_argument("--node", required=True, help="Node label") + + p_c_link = cred_sub.add_parser("link-instagram", parents=[common], help="Generate Tailscale one-tap portal & OAuth link for age verification") + p_c_link.add_argument("--node", required=True, help="Node label") + p_c_link.add_argument("--notify", action="store_true", help="Send email alert to operator via local MTA") + + p_c_audit = cred_sub.add_parser("meta-audit", parents=[common], help="Audit Meta Accounts Center for linked profiles and email") + p_c_audit.add_argument("--node", required=True, help="Node label") + + cred_sub.add_parser("list", parents=[common], help="List all registered nodes and vitality statuses") + + + # Domain: SSH + p_ssh = subparsers.add_parser("ssh", parents=[common], help="Mint and manage fleet SSH signing/access keys") + ssh_sub = p_ssh.add_subparsers(dest="action") + + p_s_mint = ssh_sub.add_parser("mint", parents=[common], help="Mint a new ed25519 SSH keypair and register in allowed_signers") + p_s_mint.add_argument("name", help="Identity/agent name (e.g. 646, pip, dev, canary)") + p_s_mint.add_argument("--force", action="store_true", help="Overwrite existing keypair") + + p_s_list = ssh_sub.add_parser("list", parents=[common], help="List managed SSH keypairs and registration status") + p_s_show = ssh_sub.add_parser("show", parents=[common], help="Show public key content and fingerprint") + p_s_show.add_argument("name", help="Identity/agent name") + + p_s_ports = ssh_sub.add_parser("ports", parents=[common], help="List container reverse tunnel port mappings") + p_s_info = ssh_sub.add_parser("info", parents=[common], help="Show SSH dial-in commands and reverse tunnel coordinates") + p_s_info.add_argument("name", help="Agent identity (e.g. 646, muse, pip)") + p_s_check = ssh_sub.add_parser("check", parents=[common], help="Live SSH/terminal tunnel health sweep via jump host") + + # Domain: MD (Hatch Agent Markdown & Operational Drive) + p_md = subparsers.add_parser("md", parents=[common], help="Manage agent .md system files & inject operational DRIVE via Hatch") + md_sub = p_md.add_subparsers(dest="action") + + p_md_audit = md_sub.add_parser("audit", parents=[common], help="Audit .md files and DRIVE scores across fleet agents") + p_md_audit.add_argument("accounts", nargs="*", help="Optional account filter") + + p_md_list = md_sub.add_parser("list", parents=[common], help="List files in agent container via Hatch") + p_md_list.add_argument("account", help="Agent account (e.g. muse, pip, 646, opm, def, dev)") + p_md_list.add_argument("path", nargs="?", default="", help="Subdirectory path") + + p_md_read = md_sub.add_parser("read", parents=[common], help="Read an agent .md file via Hatch") + p_md_read.add_argument("account", help="Agent account") + p_md_read.add_argument("filename", help="Filename (e.g. SOUL.md, HEARTBEAT.md)") + + p_md_write = md_sub.add_parser("write", parents=[common], help="Write an agent .md file via Hatch") + p_md_write.add_argument("account", help="Agent account") + p_md_write.add_argument("filename", help="Filename (e.g. SOUL.md)") + p_md_write.add_argument("--content", help="Text content to write") + p_md_write.add_argument("--file", help="Local file to copy from") + + p_md_diff = md_sub.add_parser("diff", parents=[common], help="Diff container .md file against shared operator template") + p_md_diff.add_argument("account", help="Agent account") + p_md_diff.add_argument("filename", help="Filename (e.g. SOUL.md)") + + p_md_drive = md_sub.add_parser("inject-drive", parents=[common], help="Inject high-drive operator files into an agent container") + p_md_drive.add_argument("account", help="Agent account") + p_md_drive.add_argument("--force", action="store_true", help="Force overwrite") + + p_md_sync = md_sub.add_parser("sync-all", parents=[common], help="Inject high-drive operator files across all active fleet agents") + p_md_sync.add_argument("--force", action="store_true", help="Force overwrite") + + p_md_amend = md_sub.add_parser("amend", parents=[common], help="Amend centralized shared operator file with validation and git commit") + p_md_amend.add_argument("filename", help="Filename (e.g. AGENTS.md, TOOLS.md)") + p_md_amend.add_argument("--content", help="New content") + p_md_amend.add_argument("--file", help="File with new content") + p_md_amend.add_argument("--author", default="operator", help="Author of amendment") + p_md_amend.add_argument("--reason", default="", help="Reason for amendment") + + p_md_append = md_sub.add_parser("append", parents=[common], help="Safely append a lesson or note to a shared operator file") + p_md_append.add_argument("filename", help="Filename (e.g. AGENTS.md)") + p_md_append.add_argument("text", help="Text to append") + p_md_append.add_argument("--author", default="operator", help="Author of amendment") + p_md_append.add_argument("--section", default=None, help="Optional section header") + + p_md_pull = md_sub.add_parser("pull", parents=[common], help="Pull canonical shared template into an agent container") + p_md_pull.add_argument("account", help="Agent account") + p_md_pull.add_argument("filename", help="Filename (e.g. SOUL.md)") + + p_md_wd = md_sub.add_parser("watchdog", parents=[common], help="Fleet agent drive watchdog daemon & status") + p_md_wd.add_argument("sub_action", choices=["status", "run"], nargs="?", default="status", help="Watchdog action: status (default) or run (execute cycle)") + p_md_wd.add_argument("--no-heal", action="store_true", help="Audit only; do not auto-heal degraded agents") + + # Domain: HARVEST + p_harvest = subparsers.add_parser("harvest", parents=[common], help="Readback & response harvesting engine") + harvest_sub = p_harvest.add_subparsers(dest="action") + p_h_run = harvest_sub.add_parser("run", parents=[common], help="Run a harvest cycle immediately") + p_h_run.add_argument("--agent", choices=VALID_NODES, default=None, help="Target specific agent") + p_h_run.add_argument("--dry-run", action="store_true", help="Scrape without persisting watermarks") + + p_h_status = harvest_sub.add_parser("status", parents=[common], help="Display active watermarks and harvest metrics") + + p_h_tail = harvest_sub.add_parser("tail", parents=[common], help="Live stream ingested chat messages") + p_h_tail.add_argument("--filter", default=None, help="Search filter in text") + + # Domain: FOLLOWUP + p_followup = subparsers.add_parser("followup", parents=[common], help="Scheduled follow-up deadline tracking and nudges") + followup_sub = p_followup.add_subparsers(dest="action") + p_f_list = followup_sub.add_parser("list", parents=[common], help="List tracked follow-ups and deadlines") + p_f_list.add_argument("--all", action="store_true", help="Include resolved and historical records") + + p_f_sweep = followup_sub.add_parser("sweep", parents=[common], help="Trigger deadline check and nudge dispatch immediately") + p_f_sweep.add_argument("--dry-run", action="store_true", help="Simulate sweep without sending DMs") + + p_f_cancel = followup_sub.add_parser("cancel", parents=[common], help="Cancel a pending follow-up") + p_f_cancel.add_argument("dm_id", help="DM ID to cancel") + + # Domain: LOOP (Intrinsic Loop Management) + p_loop = subparsers.add_parser("loop", parents=[common], help="Intrinsic loop strategy, health, break taxonomy, and variables") + loop_sub = p_loop.add_subparsers(dest="action") + + p_l_status = loop_sub.add_parser("status", parents=[common], help="Show active and recent intrinsic loops") + p_l_status.add_argument("--agent", choices=VALID_NODES, default=None, help="Filter by agent") + p_l_status.add_argument("--status", choices=["pending", "active", "nudged", "escalated", "closed", "resolved", "all"], default=None, help="Filter by state") + p_l_status.add_argument("-n", type=int, default=30, help="Number of loops to display") + + p_l_health = loop_sub.add_parser("health", parents=[common], help="Evaluate per-agent and fleet loop health metrics") + p_l_health.add_argument("--threshold", type=float, default=None, help="Health ratio threshold (default: 0.5)") + + p_l_breaks = loop_sub.add_parser("breaks", parents=[common], help="Run break detection diagnostics across loops") + + # loop strat + p_l_strat = loop_sub.add_parser("strat", parents=[common], help="Inspect and configure loop modulation strategies") + strat_sub = p_l_strat.add_subparsers(dest="strat_action") + p_ls_show = strat_sub.add_parser("show", parents=[common], help="Display active modulation strategy matrix") + p_ls_show.add_argument("type", nargs="?", default=None, help="Filter by input type (wake, job, siphon, manual, health, heartbeat)") + + p_ls_set = strat_sub.add_parser("set", parents=[common], help="Override loop modulation strategy") + p_ls_set.add_argument("type", help="Input type (wake, job, siphon, manual, health, heartbeat)") + p_ls_set.add_argument("--subtype", default=None, help="Subtype (e.g. overdue, canary, alert, blocker)") + p_ls_set.add_argument("--agent", choices=VALID_NODES, default=None, help="Target agent scope") + p_ls_set.add_argument("--track", choices=["true", "false", "actionable", "always", "never"], default=None, help="Track mode") + p_ls_set.add_argument("--priority", choices=["routine", "normal", "important", "critical"], default=None, help="Priority rank") + p_ls_set.add_argument("--timeout", type=int, default=None, help="Timeout in seconds before nudge") + p_ls_set.add_argument("--nudges", type=int, default=None, help="Max automatic nudges") + p_ls_set.add_argument("--escalate", default=None, help="Escalation target identity (e.g. opm, user, none)") + + p_ls_reset = strat_sub.add_parser("reset", parents=[common], help="Reset loop strategy override to builtin default") + p_ls_reset.add_argument("type", help="Input type") + p_ls_reset.add_argument("--subtype", default=None, help="Subtype") + p_ls_reset.add_argument("--agent", choices=VALID_NODES, default=None, help="Target agent scope") + + p_ls_eval = strat_sub.add_parser("eval", parents=[common], help="Evaluate/simulate strategy resolution for input") + p_ls_eval.add_argument("type", help="Input type") + p_ls_eval.add_argument("--subtype", default=None, help="Subtype") + p_ls_eval.add_argument("--agent", choices=VALID_NODES, default=None, help="Target agent scope") + p_ls_eval.add_argument("--actionable", action="store_true", help="Actionable flag (for wake)") + + # loop vars + p_l_vars = loop_sub.add_parser("vars", parents=[common], help="Inspect and adjust runtime control variables") + vars_sub = p_l_vars.add_subparsers(dest="vars_action") + vars_sub.add_parser("list", parents=[common], help="List all registered control variables") + + p_lv_get = vars_sub.add_parser("get", parents=[common], help="Inspect a specific control variable") + p_lv_get.add_argument("name", help="Variable name") + + p_lv_set = vars_sub.add_parser("set", parents=[common], help="Set a control variable value") + p_lv_set.add_argument("name", help="Variable name") + p_lv_set.add_argument("value", help="New value") + + p_lv_reset = vars_sub.add_parser("reset", parents=[common], help="Reset a control variable to default") + p_lv_reset.add_argument("name", help="Variable name") + + p_lv_hist = vars_sub.add_parser("history", parents=[common], help="Show variable modification audit history") + p_lv_hist.add_argument("name", nargs="?", default=None, help="Variable name") + p_lv_hist.add_argument("-n", "--limit", type=int, default=20, help="Number of records") + + p_lv_rb = vars_sub.add_parser("rollback", parents=[common], help="Roll back a variable to a previous value") + p_lv_rb.add_argument("name", help="Variable name") + p_lv_rb.add_argument("--revision", default=None, help="Revision step (int) or timestamp") + + p_l_remed = loop_sub.add_parser("remediate", parents=[common], help="Run progressive auto-remediation for soft breaks") + p_l_remed.add_argument("--dry-run", action="store_true", help="Simulate remediation without applying fixes") + + p_l_close = loop_sub.add_parser("close", parents=[common], help="Manually close/resolve a pending loop") + p_l_close.add_argument("dm_id", help="DM ID or Loop ID") + p_l_close.add_argument("--note", default=None, help="Resolution note") + + p_l_nudge = loop_sub.add_parser("nudge", parents=[common], help="Trigger immediate nudge for a pending loop") + p_l_nudge.add_argument("dm_id", help="DM ID or Loop ID") + + p_l_sweep = loop_sub.add_parser("sweep", parents=[common], help="Run immediate sweep cycle") + p_l_sweep.add_argument("--dry-run", action="store_true", help="Simulate sweep without sending DMs") + + p_l_harvest = loop_sub.add_parser("harvest", parents=[common], help="Run immediate harvest cycle") + p_l_harvest.add_argument("--dry-run", action="store_true", help="Scrape without persisting watermarks") + + # Domain: PIPELINE + p_pipe = subparsers.add_parser("pipeline", parents=[common], help="Multi-agent workflow pipelines and chained handoffs") + pipe_sub = p_pipe.add_subparsers(dest="action") + + p_pipe_run = pipe_sub.add_parser("run", parents=[common], help="Start a new multi-agent pipeline run") + p_pipe_run.add_argument("name", help="Root pipeline job name (e.g. pipe-demo-step1)") + p_pipe_run.add_argument("--dry-run", action="store_true", help="Simulate pipeline without sending DMs") + + p_pipe_status = pipe_sub.add_parser("status", parents=[common], help="Display active pipeline runs and step states") + p_pipe_hist = pipe_sub.add_parser("history", parents=[common], help="Show historical pipeline runs and outcomes") + p_pipe_hist.add_argument("-n", "--limit", type=int, default=20, help="Number of records to display") + + p_pipe_stop = pipe_sub.add_parser("stop", parents=[common], help="Cancel/stop an active pipeline run") + p_pipe_stop.add_argument("run_id", help="Pipeline run ID (prefix match supported)") + p_pipe_stop.add_argument("--reason", default="cancelled_by_operator", help="Cancellation reason") + + p_pipe_prune = pipe_sub.add_parser("prune", parents=[common], help="Prune/timeout stale running pipelines") + p_pipe_prune.add_argument("--max-age", type=float, default=1.0, help="Max age in hours (default 1.0)") + + # Top-level domain aliases: strat and vars + p_strat_alias = subparsers.add_parser("strat", parents=[common], help="Shortcut for 'loop strat'") + strat_alias_sub = p_strat_alias.add_subparsers(dest="strat_action") + p_sa_show = strat_alias_sub.add_parser("show", parents=[common], help="Display active modulation strategy matrix") + p_sa_show.add_argument("type", nargs="?", default=None, help="Filter by input type") + + p_sa_set = strat_alias_sub.add_parser("set", parents=[common], help="Override loop modulation strategy") + p_sa_set.add_argument("type", help="Input type") + p_sa_set.add_argument("--subtype", default=None, help="Subtype") + p_sa_set.add_argument("--agent", choices=VALID_NODES, default=None, help="Target agent scope") + p_sa_set.add_argument("--track", choices=["true", "false", "actionable", "always", "never"], default=None, help="Track mode") + p_sa_set.add_argument("--priority", choices=["routine", "normal", "important", "critical"], default=None, help="Priority rank") + p_sa_set.add_argument("--timeout", type=int, default=None, help="Timeout in seconds before nudge") + p_sa_set.add_argument("--nudges", type=int, default=None, help="Max automatic nudges") + p_sa_set.add_argument("--escalate", default=None, help="Escalation target identity") + + p_sa_reset = strat_alias_sub.add_parser("reset", parents=[common], help="Reset loop strategy override") + p_sa_reset.add_argument("type", help="Input type") + p_sa_reset.add_argument("--subtype", default=None, help="Subtype") + p_sa_reset.add_argument("--agent", choices=VALID_NODES, default=None, help="Target agent scope") + + p_sa_eval = strat_alias_sub.add_parser("eval", parents=[common], help="Evaluate/simulate strategy resolution") + p_sa_eval.add_argument("type", help="Input type") + p_sa_eval.add_argument("--subtype", default=None, help="Subtype") + p_sa_eval.add_argument("--agent", choices=VALID_NODES, default=None, help="Target agent scope") + p_sa_eval.add_argument("--actionable", action="store_true", help="Actionable flag") + + p_vars_alias = subparsers.add_parser("vars", parents=[common], help="Shortcut for 'loop vars'") + vars_alias_sub = p_vars_alias.add_subparsers(dest="vars_action") + vars_alias_sub.add_parser("list", parents=[common], help="List all control variables") + + p_va_get = vars_alias_sub.add_parser("get", parents=[common], help="Inspect a specific control variable") + p_va_get.add_argument("name", help="Variable name") + + p_va_set = vars_alias_sub.add_parser("set", parents=[common], help="Set a control variable value") + p_va_set.add_argument("name", help="Variable name") + p_va_set.add_argument("value", help="New value") + + p_va_reset = vars_alias_sub.add_parser("reset", parents=[common], help="Reset a control variable to default") + p_va_reset.add_argument("name", help="Variable name") + + p_va_hist = vars_alias_sub.add_parser("history", parents=[common], help="Show variable modification audit history") + p_va_hist.add_argument("name", nargs="?", default=None, help="Variable name") + p_va_hist.add_argument("-n", "--limit", type=int, default=20, help="Number of records") + + p_va_rb = vars_alias_sub.add_parser("rollback", parents=[common], help="Roll back a variable to a previous value") + p_va_rb.add_argument("name", help="Variable name") + p_va_rb.add_argument("--revision", default=None, help="Revision step (int) or timestamp") + + # Domain: deploy (Deploy multi-agent pipelines or subagent tasks with oversight) + p_deploy = subparsers.add_parser("deploy", parents=[common], help="Deploy multi-agent task or pipeline across nodes with live oversight") + deploy_sub = p_deploy.add_subparsers(dest="action") + + p_dep_pipe = deploy_sub.add_parser("pipeline", parents=[common], help="Deploy multi-agent pipeline") + p_dep_pipe.add_argument("name", help="Pipeline root job name (e.g. pipe-demo-step1)") + p_dep_pipe.add_argument("--dry-run", action="store_true", help="Simulate pipeline without sending DMs") + + p_dep_sub = deploy_sub.add_parser("subagent", parents=[common], help="Spawn sub-agent session and dispatch task") + p_dep_sub.add_argument("--agent", required=True, choices=VALID_NODES, help="Agent node to spawn subagent on") + p_dep_sub.add_argument("--title", default="subagent-task", help="Title for the subagent session") + p_dep_sub.add_argument("prompt", help="Task prompt for the subagent") + p_dep_sub.add_argument("--wait", type=int, default=30, help="Seconds to wait for subagent response") + + + # Domain: SWARM + p_swarm = subparsers.add_parser("swarm", parents=[common], help="Manage multi-agent swarm tasks and worker allocations") + swarm_sub = p_swarm.add_subparsers(dest="action") + + p_sw_list = swarm_sub.add_parser("list", parents=[common], help="List all swarms and slot rollups") + p_sw_status = swarm_sub.add_parser("status", parents=[common], help="Show status and slot details for a swarm") + p_sw_status.add_argument("swarm_id", help="Swarm identifier (sw-...)") + + p_sw_spawn = swarm_sub.add_parser("spawn", parents=[common], help="Spawn a new multi-slot worker swarm") + p_sw_spawn.add_argument("count", type=int, help="Number of worker slots (1-50)") + p_sw_spawn.add_argument("task", help="Task prompt for the swarm workers") + p_sw_spawn.add_argument("--label", default=None, help="Optional tracking label") + + p_sw_prune = swarm_sub.add_parser("prune", parents=[common], help="Archive stale partial/terminal swarms") + p_sw_prune.add_argument("--stale-hours", type=float, default=6.0, help="Hours of inactivity before pruning (default: 6)") + p_sw_prune.add_argument("--confirm", action="store_true", help="Confirm execution of pruning") + + # Domain: subagent (Direct alias for spawning & managing subagents) + p_subagent = subparsers.add_parser("subagent", parents=[common], help="Spawn sub-agent session and dispatch task") + subagent_sub = p_subagent.add_subparsers(dest="action") + p_sub_spawn = subagent_sub.add_parser("spawn", parents=[common], help="Spawn sub-agent session and dispatch task") + p_sub_spawn.add_argument("--agent", required=True, choices=VALID_NODES, help="Agent node to spawn subagent on") + p_sub_spawn.add_argument("--title", default="subagent-task", help="Title for the subagent session") + p_sub_spawn.add_argument("prompt", help="Task prompt for the subagent") + p_sub_spawn.add_argument("--wait", type=int, default=30, help="Seconds to wait for subagent response") + + + # Domain: FLOW (Agentic workflows in persistent tmux panes with delta read-backs) + p_flow = subparsers.add_parser("flow", parents=[common], help="Manage agentic flows in persistent tmux panes (start, read, send, list, stop)") + p_flow.add_argument("flow_args", nargs=argparse.REMAINDER, help="Arguments passed directly to flow_engine.py") + + # Domain: TMUX (Headless background tmux sessions with automatic logging) + p_tmux = subparsers.add_parser("tmux", parents=[common], help="Manage headless background tmux sessions on /tmp/tmux-muse.sock") + p_tmux.add_argument("tmux_args", nargs=argparse.REMAINDER, help="Arguments passed directly to muse-tmux.py") + + # Domain: muse (fast headless gateway via muse-cli-node with isolated per-node Cloudflare WARP egress) + p_muse = subparsers.add_parser("muse", parents=[common], help="Direct headless gateway client (muse-cli-node)") + p_muse.add_argument("muse_args", nargs=argparse.REMAINDER, help="Arguments passed to muse-cli-node or fleet lookups") + + # Domain: LOOKUP (Unified seamless lookups across nodes, threads, unread, approvals, key, docs) + p_lookup = subparsers.add_parser("lookup", aliases=["lookups"], parents=[common], help="Seamless fleet lookups (summary, fleet, threads, unread, approvals, key, docs)") + p_lookup.add_argument("target", nargs="?", default="summary", choices=["summary", "fleet", "nodes", "threads", "sidechats", "unread", "unreads", "approvals", "key", "passkey", "docs", "doc", "surfaces", "sentence", "regex", "parse"], help="Lookup target") + p_lookup.add_argument("lookup_args", nargs=argparse.REMAINDER, help="Additional arguments passed to lookup engine") + + # Domain: PASSKEY (Operator passkey location & agent approval protocol) + p_passkey = subparsers.add_parser("passkey", aliases=["key"], parents=[common], help="Display operator passkey location (VM-only), PIN, & agent approval protocol") + p_passkey.add_argument("action", nargs="?", default="show", choices=["show", "fetch", "get"], help="Passkey action: 'show' details or 'fetch' from VM") + + # Domain: DOCS (Internal agent lookups, surfaces, sentence grammar, and regex engine) + p_docs = subparsers.add_parser("docs", aliases=["doc"], parents=[common], help="Agent lookups, surfaces for box.muse-dev.online, sentence grammar & regex") + p_docs.add_argument("docs_args", nargs=argparse.REMAINDER, help="Arguments passed directly to docs-lookup.py") + + # Domain: TUI (Interactive full-screen Muse TUI & Box fleet console) + p_tui = subparsers.add_parser("tui", parents=[common], help="Interactive full-screen Muse TUI & Box fleet console") + p_tui.add_argument("tui_args", nargs=argparse.REMAINDER, help="Arguments passed directly to muse-tui.py") + + # Domain: SYSOP (one-shot fleet installer: link units, enable timers, anchor + verify) + p_sysop = subparsers.add_parser("sysop", parents=[common], help="Fleet operations: one-shot systemd unit installer") + sysop_sub = p_sysop.add_subparsers(dest="sysop_action") + p_sysop_install = sysop_sub.add_parser("install", parents=[common], help="Link systemd units, enable timers, anchor and verify them") + p_sysop_install.add_argument("--dry-run", action="store_true", help="Print actions without changing anything") + + return parser + +def main(): + if len(sys.argv) > 1: + if sys.argv[1] == "tui": + tui_args = sys.argv[2:] + if tui_args and tui_args[0] in ("onboard", "tmux", "connects", "approvals"): + cmd = [sys.executable, str(BIN_DIR / "box-onboard-tui.py")] + tui_args[1:] + elif tui_args and tui_args[0] in ("fleet", "oversight"): + cmd = [sys.executable, str(BIN_DIR / "box-fleet-tui.py")] + tui_args[1:] + else: + cmd = [sys.executable, str(BIN_DIR / "muse-tui.py"), "--mode", "box"] + tui_args + res = subprocess.run(cmd) + sys.exit(res.returncode) + elif sys.argv[1] in ("onboard-tui", "dev-tui"): + cmd = [sys.executable, str(BIN_DIR / "box-onboard-tui.py")] + sys.argv[2:] + res = subprocess.run(cmd) + sys.exit(res.returncode) + elif sys.argv[1] in ("fleet-tui",): + cmd = [sys.executable, str(BIN_DIR / "box-fleet-tui.py")] + sys.argv[2:] + res = subprocess.run(cmd) + sys.exit(res.returncode) + elif sys.argv[1] == "tmux": + if len(sys.argv) > 2 and sys.argv[2] in ("tally", "auto", "watch", "once", "match", "rules", "status"): + if sys.argv[2] == "auto": + t_sub = sys.argv[3:] or ["status"] + else: + t_sub = sys.argv[2:] + cmd = [sys.executable, str(BIN_DIR / "tmux_auto_approver.py")] + t_sub + res = subprocess.run(cmd) + sys.exit(res.returncode) + cmd = [sys.executable, str(BIN_DIR / "muse-tmux.py")] + (sys.argv[2:] or ["list"]) + res = subprocess.run(cmd) + sys.exit(res.returncode) + elif sys.argv[1] in ("tmux-auto", "auto-dev"): + cmd = [sys.executable, str(BIN_DIR / "tmux_auto_approver.py")] + sys.argv[2:] + res = subprocess.run(cmd) + sys.exit(res.returncode) + elif sys.argv[1] in ("docs", "doc"): + cmd = [sys.executable, str(BIN_DIR / "docs-lookup.py")] + sys.argv[2:] + res = subprocess.run(cmd) + sys.exit(res.returncode) + elif sys.argv[1] in ("stability", "stable"): + cmd = [sys.executable, str(NETVM_ROOT / "watchers" / "box-stability-watcher.py")] + sub = sys.argv[2:] + if not sub or sub[0] == "status": + cmd.append("--status") + elif sub[0] == "check": + cmd.append("--check") + elif sub[0] == "json": + cmd.extend(["--status", "--json"]) + elif sub[0] == "resume" and len(sub) > 1: + cmd.extend(["--resume", sub[1]]) + else: + cmd.extend(sub) + res = subprocess.run(cmd) + sys.exit(res.returncode) + elif sys.argv[1] == "muse": + m_args = sys.argv[2:] + if not m_args: + cmd = [str(BIN_DIR / "muse"), "--help"] + res = subprocess.run(cmd) + sys.exit(res.returncode) + if m_args[0] == "tmux": + cmd = [sys.executable, str(BIN_DIR / "muse-tmux.py")] + (m_args[1:] or ["list"]) + res = subprocess.run(cmd) + sys.exit(res.returncode) + if m_args[0] in ("status", "fleet"): + sys.argv = [sys.argv[0], "fleet", "status"] + elif m_args[0] in ("passkey", "key"): + sys.argv = [sys.argv[0], "passkey"] + m_args[1:] + elif m_args[0] in ("lookup", "lookups"): + sys.argv = [sys.argv[0], "lookup"] + m_args[1:] + elif m_args[0] in ("threads", "sidechats"): + sys.argv = [sys.argv[0], "thread", "list"] + m_args[1:] + elif m_args[0] in ("unread", "unreads"): + sys.argv = [sys.argv[0], "lookup", "unread"] + elif m_args[0] in VALID_NODES: + node = m_args[0] + sub = m_args[1:] + if not sub: + sub = ["status"] + if sub[0] == "chat": + cmd = [sys.executable, str(BIN_DIR / "muse-chat-repl.py"), node] + sub[1:] + res = subprocess.run(cmd) + sys.exit(res.returncode) + cmd = [str(BIN_DIR / "muse-cli-node"), node] + sub + res = subprocess.run(cmd) + sys.exit(res.returncode) + else: + cmd = [str(BIN_DIR / "muse")] + m_args + res = subprocess.run(cmd) + sys.exit(res.returncode) + + # Handle empty arguments (box alone) + if len(sys.argv) == 1: + print_box_usage_reference() + cmd_fleet_status(argparse.Namespace(json=False)) + sys.exit(0) + + # Handle help variations + if len(sys.argv) > 1: + if sys.argv[1] == "help": + if len(sys.argv) == 2: + print_master_help() + sys.exit(0) + else: + target_domain = sys.argv[2] + rest = sys.argv[3:] + sys.argv = [sys.argv[0], target_domain] + rest + ["--help"] + elif sys.argv[1] in ("--help", "-h") and len(sys.argv) == 2: + print_master_help() + sys.exit(0) + elif "help" in sys.argv[2:]: + h_idx = sys.argv.index("help") + sys.argv[h_idx] = "--help" + + parser = build_parser() + args = parser.parse_args() + + # Route commands + if args.domain == "fleet": + act = getattr(args, "action", "status") or "status" + if act == "status": + cmd_fleet_status(args) + elif act == "watch": + cmd_fleet_watch(args) + elif act == "restart": + if not getattr(args, "node", None): + print("Error: Specify node to restart (e.g. super fleet restart muse)", file=sys.stderr) + sys.exit(1) + cmd_fleet_restart(args) + elif act == "cdp": + if not getattr(args, "node", None): + print("Error: Specify node (e.g. super fleet cdp muse)", file=sys.stderr) + sys.exit(1) + cmd_fleet_cdp(args) + elif act == "heal": + if not getattr(args, "node", None): + print("Error: Specify node to heal (e.g. super fleet heal dev)", file=sys.stderr) + sys.exit(1) + cmd_fleet_heal(args) + elif args.domain == "watchdog": + act = getattr(args, "action", "status") or "status" + if act == "status": + cmd_watchdog_status(args) + elif act == "run": + if not getattr(args, "target", None): + print("Error: Specify node or 'relay' (e.g. box watchdog run dev)", file=sys.stderr) + sys.exit(1) + cmd_watchdog_run(args) + elif args.domain == "dm": + act = getattr(args, "action", None) + if not act or act == "log": + cmd_dm_log(args) + elif act == "send": + cmd_dm_send(args) + elif act == "wo": + cmd_dm_wo(args) + elif act == "ack": + cmd_dm_ack(args) + elif act == "tail": + cmd_dm_tail(args) + elif act == "chat": + cmd_dm_chat(args) + elif act == "send-file": + cmd_dm_send_file(args) + elif act == "files": + cmd_dm_files(args) + elif act == "verify": + cmd_dm_verify(args) + else: + parser.print_help() + elif args.domain == "thread": + act = getattr(args, "action", None) + if act == "list": + cmd_thread_list(args) + elif act == "view": + cmd_thread_view(args) + else: + parser.print_help() + elif args.domain == "job": + act = getattr(args, "action", None) + if not act or act == "list": + cmd_job_list(args) + elif act == "show": + cmd_job_show(args) + elif act == "create": + cmd_job_create(args) + elif act == "enable": + cmd_job_enable(args) + elif act == "disable": + cmd_job_disable(args) + elif act == "delete": + cmd_job_delete(args) + elif act == "status": + cmd_job_status(args) + elif act == "run": + cmd_job_run(args) + elif act == "log": + cmd_job_log(args) + elif act == "archive": + cmd_job_archive(args) + elif act == "unarchive": + cmd_job_unarchive(args) + else: + parser.print_help() + elif args.domain == "web": + act = getattr(args, "action", None) + if not act or act == "health": + cmd_web_health(args) + elif act == "test-auth": + cmd_web_test_auth(args) + elif act == "sync": + cmd_web_sync(args) + else: + parser.print_help() + elif args.domain == "cred": + act = getattr(args, "action", None) + if not act or act == "list": + cmd_cred_list(args) + elif act == "initiate": + cmd_cred_initiate(args) + elif act == "submit-otp": + cmd_cred_submit_otp(args) + elif act == "status": + cmd_cred_status(args) + elif act == "link-instagram": + cmd_cred_link_instagram(args) + elif act == "meta-audit": + cmd_cred_meta_audit(args) + else: + parser.print_help() + elif args.domain == "ssh": + act = getattr(args, "action", None) + if not act or act == "list": + cmd_ssh_list(args) + elif act == "mint": + cmd_ssh_mint(args) + elif act == "show": + cmd_ssh_show(args) + elif act == "ports": + cmd_ssh_ports(args) + elif act == "info": + cmd_ssh_info(args) + elif act == "check": + cmd_ssh_check(args) + else: + p_ssh.print_help() + elif args.domain == "md": + act = getattr(args, "action", None) + if not act or act == "audit": + cmd_md_audit(args) + elif act == "list": + cmd_md_list(args) + elif act == "read": + cmd_md_read(args) + elif act == "write": + cmd_md_write(args) + elif act == "diff": + cmd_md_diff(args) + elif act == "inject-drive": + cmd_md_inject_drive(args) + elif act == "sync-all": + cmd_md_sync_all(args) + elif act == "amend": + cmd_md_amend(args) + elif act == "append": + cmd_md_append(args) + elif act == "pull": + cmd_md_pull(args) + elif act == "watchdog": + cmd_md_watchdog(args) + else: + p_md.print_help() + elif args.domain == "harvest": + act = getattr(args, "action", None) + if not act or act == "status": + cmd_harvest_status(args) + elif act == "run": + cmd_harvest_run(args) + elif act == "tail": + cmd_harvest_tail(args) + else: + parser.print_help() + elif args.domain == "followup": + act = getattr(args, "action", None) + if not act or act == "list": + cmd_followup_list(args) + elif act == "sweep": + cmd_followup_sweep(args) + elif act == "cancel": + cmd_followup_cancel(args) + else: + parser.print_help() + elif args.domain == "pipeline": + act = getattr(args, "action", None) + if not act or act == "status": + cmd_pipeline_status(args) + elif act == "run": + cmd_pipeline_run(args) + elif act == "history": + cmd_pipeline_history(args) + elif act == "stop": + cmd_pipeline_stop(args) + elif act == "prune": + cmd_pipeline_prune(args) + else: + parser.print_help() + elif args.domain == "loop": + act = getattr(args, "action", None) + if not act or act == "status": + cmd_loop_status(args) + elif act == "health": + cmd_loop_health(args) + elif act == "breaks": + cmd_loop_breaks(args) + elif act == "strat": + cmd_loop_strat(args) + elif act == "vars": + cmd_loop_vars(args) + elif act == "close": + cmd_loop_close(args) + elif act == "nudge": + cmd_loop_nudge(args) + elif act == "sweep": + cmd_loop_sweep(args) + elif act == "harvest": + cmd_loop_harvest(args) + elif act == "remediate": + cmd_loop_remediate(args) + else: + parser.print_help() + elif args.domain == "strat": + cmd_loop_strat(args) + elif args.domain == "vars": + cmd_loop_vars(args) + elif args.domain == "swarm": + act = getattr(args, "action", None) + if not act or act == "list": + cmd_swarm_list(args) + elif act == "status": + cmd_swarm_status(args) + elif act == "spawn": + cmd_swarm_spawn(args) + elif act == "prune": + cmd_swarm_prune(args) + else: + p_swarm.print_help() + elif args.domain in ("approvals", "approval", "blocked"): + cmd_approvals(args) + elif args.domain == "muse-choices": + cmd_muse_choices(args) + elif args.domain == "runtime": + cmd_runtime(args) + elif args.domain == "work": + cmd_work(args) + elif args.domain == "tasks": + cmd_tasks(args) + elif args.domain == "invite": + cmd_invite(args) + elif args.domain == "usage": + cmd_usage(args) + elif args.domain == "settings": + cmd_settings(args) + elif args.domain == "onboard": + cmd_onboard(args) + elif args.domain == "kpi": + cmd_kpi(args) + elif args.domain == "chromebox": + cmd_chromebox(args) + elif args.domain in ("deploy", "subagent"): + if args.domain == "subagent": + args.action = "subagent" + cmd_deploy(args) + elif args.domain in ("lookup", "lookups"): + cmd_lookup(args) + elif args.domain in ("passkey", "key"): + cmd_passkey_info(args) + elif args.domain == "flow": + cmd_flow_dispatch(args) + elif args.domain == "tmux": + cmd_tmux_dispatch(args) + elif args.domain == "muse": + cmd_muse_dispatch(args) + elif args.domain in ("docs", "doc"): + cmd_docs_dispatch(args) + elif args.domain == "sysop": + act = getattr(args, "sysop_action", None) + if act == "install": + cmd_sysop_install(args) + else: + parser.print_help() + else: + parser.print_help() + +if __name__ == "__main__": + main() diff --git a/tests/test_box_work.py b/tests/test_box_work.py index c39b688..2db8ad2 100644 --- a/tests/test_box_work.py +++ b/tests/test_box_work.py @@ -58,5 +58,63 @@ class TestBoxWork(unittest.TestCase): self.assertFalse(res["ready"]) self.assertEqual(res["overall"], "FAIL") + def test_cli_alone_prints_usage_reference(self): + import subprocess + proc = subprocess.run([sys.executable, str(REPO_ROOT / "bin" / "super-cli.py")], capture_output=True, text=True) + self.assertEqual(proc.returncode, 0) + self.assertIn("BOX ORCHESTRATOR: INPUT PARAMETERS & USAGE REFERENCE", proc.stdout) + self.assertIn("PRIMARY DOMAINS & INPUT PARAMETERS:", proc.stdout) + self.assertIn("box work", proc.stdout) + self.assertIn("box tasks", proc.stdout) + self.assertIn("box fleet", proc.stdout) + + def test_cli_help_prints_master_manual(self): + import subprocess + proc = subprocess.run([sys.executable, str(REPO_ROOT / "bin" / "super-cli.py"), "help"], capture_output=True, text=True) + self.assertEqual(proc.returncode, 0) + self.assertIn("BOX ORCHESTRATOR COMPREHENSIVE CLI & RUNTIME MANUAL", proc.stdout) + self.assertIn("DOMAINS & ACTION SPECIFICATIONS:", proc.stdout) + + def test_cli_domain_help_prints_subcommands_and_examples(self): + import subprocess + proc = subprocess.run([sys.executable, str(REPO_ROOT / "bin" / "super-cli.py"), "work", "help"], capture_output=True, text=True) + self.assertEqual(proc.returncode, 0) + self.assertIn("SHORTHAND EXAMPLES:", proc.stdout) + self.assertIn("OPERATIONAL GUIDELINES:", proc.stdout) + self.assertIn("box work start", proc.stdout) + + def test_cli_missing_args_prints_error_and_shorthand_helper(self): + import subprocess + proc = subprocess.run([sys.executable, str(REPO_ROOT / "bin" / "super-cli.py"), "work", "start"], capture_output=True, text=True) + self.assertEqual(proc.returncode, 2) + err = proc.stderr + self.assertIn("CLI ERROR:", err) + self.assertIn("SHORTHAND USAGE HELPER:", err) + self.assertIn("title", err) + self.assertIn("--to", err) + self.assertIn("Quick Examples:", err) + + def test_cli_invalid_subcommand_prints_shorthand_helper(self): + import subprocess + proc = subprocess.run([sys.executable, str(REPO_ROOT / "bin" / "super-cli.py"), "work", "invalid_action_xyz"], capture_output=True, text=True) + self.assertEqual(proc.returncode, 2) + err = proc.stderr + self.assertIn("CLI ERROR:", err) + self.assertIn("SHORTHAND USAGE HELPER:", err) + self.assertIn("Available Subcommands:", err) + self.assertIn("status", err) + self.assertIn("start", err) + + def test_cli_invalid_domain_prints_shorthand_helper(self): + import subprocess + proc = subprocess.run([sys.executable, str(REPO_ROOT / "bin" / "super-cli.py"), "invalid_domain_xyz"], capture_output=True, text=True) + self.assertEqual(proc.returncode, 2) + err = proc.stderr + self.assertIn("CLI ERROR:", err) + self.assertIn("SHORTHAND USAGE HELPER:", err) + self.assertIn("Primary Domains & Commands:", err) + self.assertIn("work", err) + self.assertIn("fleet", err) + if __name__ == "__main__": unittest.main()