Files
multi-agent-mux/.agents/skills/lib_py/delegation/cli.py
T
GodopuandClaude Opus 5.5 5edf913d22 fix(followups): F2 - Python entry points report an unreadable env file in one line
A .mam.env that exists but cannot be read or decoded stopped the Python
entry points with an EnvFileError traceback, while the wrapper printed one
`ERROR:` line and exited 1 (PR #43 review claude S1). The mains that load the
env file - publish_event, registry, job_subscriber, cli, bench, heartbeat and
remote - now call mqtt_common.load_env_or_report(), which prints
`ERROR: <path>: cannot read (<exception type>)` and lets the main return 1.
No value or file content is printed. ensure_env_loaded() still raises: no
library function exits the process.

Test: every entry point, for a 000 file and an undecodable one, ends with
exactly that line and rc 1 and creates nothing (14 cases; all 14 fail at
0051fd6 with a traceback).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
2026-10-02 08:09:46 +09:00

111 lines
4.5 KiB
Python

"""`python -m lib_py.delegation.cli` — the router the shell helpers call.
Deliberately thin: every subcommand delegates to the existing `main()` of the
module that already owned that behaviour, so exit codes, stdout shape and side
effects cannot drift between the shell path and the script path. The one piece
of real logic here is `ensure-daemon`, which reproduces the shell helper's
stderr wording and exit status for a missing monitor script.
"""
from __future__ import annotations
import argparse
import logging
import sys
from typing import List, Optional
from lib_py.delegation import daemon, mqtt_common, publish_event, registry
logger = logging.getLogger("delegate_job.cli")
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="lib_py.delegation.cli",
description="delegation helpers invoked by lib.sh",
)
sub = parser.add_subparsers(dest="command", required=True)
p_submit = sub.add_parser("submit", help="register a job; prints the job id")
# Issue #36: lib.sh passes the prompt by file (one argv string is capped
# at 131,072 bytes); --prompt keeps working.
submit_prompt = p_submit.add_mutually_exclusive_group(required=True)
submit_prompt.add_argument("--prompt")
submit_prompt.add_argument("--prompt-file")
p_submit.add_argument("--agent", default="claude-code")
p_submit.add_argument("--agent-session", default="herdr:claude")
p_submit.add_argument("--registry-dir", default=None)
p_pub = sub.add_parser("publish", help="publish a job lifecycle event")
p_pub.add_argument("--job", required=True)
p_pub.add_argument("--event", default="progress", choices=publish_event.VALID_EVENTS)
p_pub.add_argument("--detail", default="")
p_pub.add_argument("--heartbeat", type=int, default=None, metavar="N",
help="start the automatic heartbeat timer (Issue #18)")
p_pub.add_argument("--data", default=None)
p_pub.add_argument("--retained", action="store_true")
p_pub.add_argument("--registry-dir", default=None)
p_pub.add_argument("--attempts", type=int, default=3)
p_pub.add_argument("--expect-dispatch-token", default=None)
p_pub.add_argument("--attempt-id", type=int, default=None)
p_pub.add_argument("-v", "--verbose", action="store_true")
p_daemon = sub.add_parser("ensure-daemon", help="start the subscriber daemon if absent")
p_daemon.add_argument("--job", required=True)
p_daemon.add_argument("--workdir", default=None)
return parser
def main(argv: Optional[List[str]] = None) -> int:
if not mqtt_common.load_env_or_report():
return 1
args = _build_parser().parse_args(argv)
if args.command == "submit":
prompt_arg = (["--prompt-file", args.prompt_file] if args.prompt_file is not None
else ["--prompt", args.prompt])
forward = ["register", *prompt_arg,
"--agent", args.agent, "--agent-session", args.agent_session]
if args.registry_dir:
forward = ["--registry-dir", args.registry_dir] + forward
return registry.main(forward)
if args.command == "publish":
forward = ["--job", args.job, "--event", args.event,
"--detail", args.detail, "--attempts", str(args.attempts)]
if args.data is not None:
forward += ["--data", args.data]
if args.retained:
forward += ["--retained"]
if args.registry_dir:
forward += ["--registry-dir", args.registry_dir]
if args.expect_dispatch_token:
forward += ["--expect-dispatch-token", args.expect_dispatch_token]
if args.attempt_id is not None:
forward += ["--attempt-id", str(args.attempt_id)]
if getattr(args, "heartbeat", None) is not None:
forward += ["--heartbeat", str(args.heartbeat)]
if args.verbose:
forward += ["--verbose"]
return publish_event.main(forward)
if args.command == "ensure-daemon":
try:
pids = daemon.ensure_subscriber_daemon(args.job, args.workdir)
except FileNotFoundError as exc:
# Byte-identical to the legacy shell helper's failure path.
print("ERROR: monitor script not found: {}".format(exc.args[0]), file=sys.stderr)
return 1
if not pids and daemon.daemon_disabled():
print("subscriber daemon not started ({}=1)".format(daemon.NO_DAEMON_ENV),
file=sys.stderr)
for pid in pids:
print(pid)
return 0
return 1
if __name__ == "__main__":
sys.exit(main())