Files
multi-agent-mux/.agents/skills/lib_py/delegation/cli.py
T
GodopuandClaude Opus 5.5 f26952ccc3 fix(issue-36): the legacy shell loop and lib.sh's submit helpers pass prompts by file
The two remaining exec points that carried a prompt as one argv string
(capped at 131,072 bytes):

- run_loop.sh (MAM_LOOP_PY=0): delegate_job_safe, which all eleven shell
  delegations go through, turns `--prompt <text>` into `--prompt-file` under
  the control root's .mam/ and removes the file once the wrapper returns. Its
  failure lines go to stderr: callers capture stdout, and under set -e a
  failed submit ends the loop at that line with the wrapper's code, so the
  cause was being swallowed.
- lib.sh delegate_submit_job and its legacy twin (create_session's onboarding
  and --submit-job): the prompt is written under .mam/ and handed on as
  --prompt-file; lib_py.delegation.cli submit accepts --prompt-file and
  forwards it to the registry. --prompt keeps working everywhere.

S6/S6b push a 140,000-byte prompt through the CLI and both helpers; S7 runs
the real run_loop.sh with a 140,000-byte plan and a 150,000-byte diff; S7b
fixes a nonzero exit when the implementation or compliance job cannot be
registered. All six fail on 14d11bf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 06:08:25 +00:00

110 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:
mqtt_common.ensure_env_loaded()
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())