feat(retention): implement Piece 2 job archival, CLI wiring, and rotation driver
This commit is contained in:
+128
-17
@@ -960,7 +960,7 @@ def cmd_runtime(args):
|
||||
state = r["state"]
|
||||
if r["state"] == "approval-pending" and r["prompt_kind"]:
|
||||
state = "%s(%s/%s)" % (state, r["prompt_kind"],
|
||||
r["prompt_key"])
|
||||
r["prompt_key"] or "…")
|
||||
if r["is_muse"]:
|
||||
approve = (badge_ok("YES") if r["auto_approve"]
|
||||
else badge_err("NO"))
|
||||
@@ -995,29 +995,30 @@ def cmd_runtime(args):
|
||||
print(c_red("Error: no such pane %s on %s" % (pane, sock)),
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
t_args = ["send-keys", "-t", pane, keys]
|
||||
if enter:
|
||||
t_args.append("Enter")
|
||||
r = mcw._tmux(sock, *t_args, timeout=10)
|
||||
ok = r.returncode == 0
|
||||
# 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,
|
||||
"error": (r.stderr or r.stdout or "").strip() or None
|
||||
if not ok else None}, indent=2))
|
||||
"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:
|
||||
print(" %s sent %r%s" % (c_green("✔"),
|
||||
keys, " + Enter" if enter else ""))
|
||||
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("✘"),
|
||||
(r.stderr or r.stdout or "").strip() or "tmux error"))
|
||||
print(" %s send failed: %s" % (c_red("✘"), "tmux error"))
|
||||
sys.exit(1)
|
||||
print()
|
||||
|
||||
@@ -2845,6 +2846,8 @@ def cmd_thread_view(args):
|
||||
# ---------------------------------------------------------------------------
|
||||
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:
|
||||
@@ -2858,23 +2861,63 @@ def cmd_job_list(args):
|
||||
return
|
||||
|
||||
jobs = data.get("jobs", [])
|
||||
print("\n" + c_bold(f"=== DEFINED JOBS ({len(jobs)}) ===") + "\n")
|
||||
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", "DESCRIPTION"]
|
||||
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",
|
||||
(j.get("description") or "-")[:50]
|
||||
status_disp,
|
||||
(j.get("description") or "-")[:45]
|
||||
])
|
||||
|
||||
print_table(headers, rows)
|
||||
print("\n" + c_dim(" Commands: super job show <name> | super job status <name> | super job run <name> | super job create <name>") + "\n")
|
||||
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
|
||||
@@ -4718,6 +4761,58 @@ def cmd_ssh_info(args):
|
||||
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)
|
||||
@@ -5931,6 +6026,7 @@ def build_parser():
|
||||
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")
|
||||
@@ -5974,6 +6070,14 @@ def build_parser():
|
||||
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")
|
||||
@@ -6025,6 +6129,7 @@ def build_parser():
|
||||
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")
|
||||
@@ -6480,6 +6585,10 @@ def main():
|
||||
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":
|
||||
@@ -6520,6 +6629,8 @@ def main():
|
||||
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":
|
||||
|
||||
Reference in New Issue
Block a user