Files
multi-agent-mux/.agents/skills/lib_py/delegation/cli.py
T
Godopu ec61511699 fix(issue-6): fail-closed registry timeout and TUI turn handshake
- Introduce finalize_unfinished in multi-agent-mux-delegate-job for fail-closed atomic transitions
- Enforce CAS dispatch_token and attempt_id pinning across engine, CLI, and lifecycle scripts
- Integrate Herdr wait_for_agent_idle turn handshake to prevent prompt loss and race conditions
- Resolve bash 3.2 set -u empty array expansion compatibility in lib.sh
- Preserve Issue #5 R2 contract in CAS transition engine (engine.py, publish_event.py, service.py)
- Add comprehensive regression suites (test_issue6_fail_closed.py, test_issue6_turn_handshake.py)
- Bump lockstep framework and skills to v4.4.1 across lib.sh, eager.py, VERSIONS.md, 8 SKILL.md
- Synchronize documentation (IMPROVEMENTS.md, MESSAGING.md, state-transitions.md, task.md, phase5-e2e.md)
- Unanimous PASS achieved across 4 reviewer team leaders (Claude, Codex, OpenCode, Grok)

Closes #6
2026-09-09 14:02:47 +09:00

97 lines
3.7 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")
p_submit.add_argument("--prompt", required=True)
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("--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":
forward = ["register", "--prompt", args.prompt,
"--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 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
for pid in pids:
print(pid)
return 0
return 1
if __name__ == "__main__":
sys.exit(main())