feat: unified fleet CLI, Main Chat preservation policy, sidechat routing, and file transfers
- Added CHAT_POLICY.md and README.md banner enforcing sidechat-first and file-transfer-first rules. - Added strict Main Chat block to super dm send and super dm wo with --allow-main-chat override. - Implemented file transfer staging and metadata registry in super dm send-file and super dm files (with clean subcommand). - Added full job lifecycle management (show, create, enable, disable, delete, run --follow) to super-cli.py and box-ctl.py. - Audited all jobs in jobs/*.json and redirected automated dispatches away from Main Chat. - Hardened chromebox-watchdog.sh with systemd user session environment exports and stale singleton cleanup. - Added compose_check command choice to muse-chat-api.py.
This commit is contained in:
+279
-13
@@ -55,6 +55,8 @@ 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"]
|
||||
@@ -688,9 +690,14 @@ def cmd_dm_send(args):
|
||||
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":
|
||||
print(c_yellow(" ⚠ [CHAT POLICY NOTE] Targeting Main Chat. Per CHAT_POLICY.md, avoid using Main Chat for routine tasks."))
|
||||
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."))
|
||||
|
||||
should_sign = (sender == "super") and not getattr(args, "no_sign", False)
|
||||
mid = None
|
||||
@@ -738,13 +745,22 @@ def cmd_dm_send(args):
|
||||
def cmd_dm_wo(args):
|
||||
sender = resolve_sender(args)
|
||||
recipient = args.to
|
||||
target = getattr(args, "target", "main")
|
||||
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":
|
||||
print(c_yellow(" ⚠ [CHAT POLICY NOTE] Targeting Main Chat. Per CHAT_POLICY.md, prefer sidechats for work orders."))
|
||||
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 ""
|
||||
@@ -762,9 +778,13 @@ def cmd_dm_wo(args):
|
||||
"--raw",
|
||||
signed_wire
|
||||
]
|
||||
print(f"Issuing Cryptographically Signed Work Order {c_green('[WO:' + wo_id + ']')} [{c_green('verified from:' + sender)}] -> [{c_bold(recipient)}/{target}]...")
|
||||
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)
|
||||
sys.exit(res.returncode)
|
||||
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)
|
||||
|
||||
@@ -777,9 +797,13 @@ def cmd_dm_wo(args):
|
||||
wo_content
|
||||
]
|
||||
|
||||
print(f"Issuing Work Order {c_cyan('[WO:' + wo_id + ']')} [{c_cyan('from:' + sender)}] -> [{c_bold(recipient)}/{target}]...")
|
||||
print(f"Issuing Work Order {c_cyan('[WO:' + wo_id + ']')} [{c_cyan('from:' + sender)}] -> [{c_bold(recipient)}/{c_cyan(target)}]...")
|
||||
res = subprocess.run(cmd)
|
||||
sys.exit(res.returncode)
|
||||
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)
|
||||
@@ -941,6 +965,22 @@ def cmd_dm_tail(args):
|
||||
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():
|
||||
@@ -983,6 +1023,24 @@ def cmd_dm_send_file(args):
|
||||
# 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}",
|
||||
@@ -1016,21 +1074,53 @@ def cmd_dm_send_file(args):
|
||||
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():
|
||||
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():
|
||||
if not fp.is_file() or fp.name.startswith("."):
|
||||
continue
|
||||
st = fp.stat()
|
||||
size_kb = f"{st.st_size / 1024:.1f} KB"
|
||||
@@ -1042,14 +1132,21 @@ def cmd_dm_files(args):
|
||||
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
|
||||
"time_rel": t_rel,
|
||||
"note": note
|
||||
})
|
||||
|
||||
if args.json:
|
||||
@@ -1057,18 +1154,20 @@ def cmd_dm_files(args):
|
||||
return
|
||||
|
||||
print("\n" + c_bold(f"=== TRANSFERRED PAYLOADS & STAGED FILES ({len(records)}) ===") + "\n")
|
||||
headers = ["AGENT", "FILENAME", "SIZE", "TRANSFERRED", "STAGED PATH"]
|
||||
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 --agent <agent>") + "\n")
|
||||
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)
|
||||
@@ -2033,6 +2132,145 @@ def cmd_followup_cancel(args):
|
||||
os.replace(tmp_path, FOLLOWUPS_FILE)
|
||||
print(c_green(f"● Cancelled follow-up {match_key}."))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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"))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Domain: LOOP (Intrinsic Loop Strategy, Health, Break Taxonomy, and Control)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -2532,6 +2770,7 @@ def build_parser():
|
||||
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")
|
||||
@@ -2556,7 +2795,9 @@ def build_parser():
|
||||
|
||||
# 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:...])")
|
||||
@@ -2564,7 +2805,10 @@ def build_parser():
|
||||
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="main", help="Target chat (default: main)")
|
||||
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")
|
||||
|
||||
@@ -2763,6 +3007,18 @@ def build_parser():
|
||||
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")
|
||||
|
||||
# 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")
|
||||
@@ -2922,6 +3178,16 @@ def main():
|
||||
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)
|
||||
else:
|
||||
parser.print_help()
|
||||
elif args.domain == "loop":
|
||||
act = getattr(args, "action", None)
|
||||
if not act or act == "status":
|
||||
|
||||
Reference in New Issue
Block a user