feat(box): wire tmux tallies, auto-approvals, and onboard connects into CLI and API
- bin/box-ctl.py: wire tmux-tally, tmux-auto-status, tmux-auto-toggle, tmux-auto-once, and onboard-connects actions with idempotent allowlists - bin/exec-constrained.py: register tmux.tally, tmux.auto_status, and onboard.connects ops for HTTPS execution - bin/super-cli.py: wire box tmux dispatch to approver, add cmd_run/cmd_watch, and json unread formatting - .agents/skills/box/SKILL.md: document tmux worker tally and auto-approval capabilities
This commit is contained in:
+280
-1
@@ -70,6 +70,7 @@ DEFAULT_AGENT_SIDECHATS = {
|
||||
"pip": "646-pip-coord",
|
||||
"muse": "muse tasks",
|
||||
}
|
||||
MUSE_TMUX_LOG_DIR = NETVM_ROOT / "logs" / "tmux"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ANSI Color & Formatting Engine
|
||||
@@ -1082,6 +1083,96 @@ def cmd_runtime(args):
|
||||
print(" %s watcher %s" % (badge_ok("STARTED"), c_cyan(s)))
|
||||
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"]
|
||||
r = mcw._tmux(t["socket"], "break-pane", "-s", t["pane"],
|
||||
"-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()
|
||||
|
||||
else:
|
||||
if as_json:
|
||||
print(json.dumps({"ok": False,
|
||||
@@ -5100,10 +5191,35 @@ def cmd_passkey_info(args):
|
||||
|
||||
|
||||
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 = []
|
||||
data = collect_fleet_data()
|
||||
for item in data:
|
||||
n = item["node"]
|
||||
title = item.get("title", "")
|
||||
@@ -5157,6 +5273,8 @@ def cmd_lookup(args):
|
||||
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"):
|
||||
@@ -5169,6 +5287,159 @@ def cmd_lookup(args):
|
||||
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 []
|
||||
@@ -5384,6 +5655,14 @@ def build_parser():
|
||||
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")
|
||||
|
||||
# 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)")
|
||||
|
||||
Reference in New Issue
Block a user