#!/usr/bin/env bash # multi-agent-mux-delegate-job β€” user-facing orchestrator for the multi-agent-mux-delegate-job skill. # # Subcommands: # submit register a job, start the subscriber FIRST, then run the agent, # then (optionally) run a validation script. # status show one job record. # list list all jobs. # verify run a user-supplied --validate script against a job's artifacts. # wait block until all running/pending jobs reach a terminal state. # # This is a reference wrapper: it shells out to the python scripts that live # next to it. Copy it into your project and customise as needed. It never hard # fails if `claude`/`codex`/`herdr` are missing β€” it prints what it would run. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Load local env file (.mam.env preferred, .env fallback) from workspace root or explicit MAM_ENV_FILE REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" TARGET_ENV="${MAM_ENV_FILE:-}" if [[ -z "$TARGET_ENV" ]]; then if [[ -f "$REPO_ROOT/.mam.env" ]]; then TARGET_ENV="$REPO_ROOT/.mam.env" elif [[ -f "$REPO_ROOT/.env" ]]; then TARGET_ENV="$REPO_ROOT/.env" echo "WARNING: Loading deprecated config file '$TARGET_ENV'. Please migrate to '.mam.env'." >&2 elif [[ -f .mam.env ]]; then TARGET_ENV=".mam.env" echo "WARNING: Loading config from cwd relative path '$TARGET_ENV'." >&2 elif [[ -f .env ]]; then TARGET_ENV=".env" echo "WARNING: Loading deprecated config from cwd relative path '$TARGET_ENV'. Please migrate to '.mam.env'." >&2 fi fi # Issue #26: parsed by envfile.py (the Python loader's rules; environment wins), # never sourced. Values come back as shlex-quoted exports over the pipe. The # variables this script has already set are handed over as environment, so # the file cannot replace them either. if [[ -n "$TARGET_ENV" && -f "$TARGET_ENV" ]]; then _env_exports="$(SCRIPT_DIR="$SCRIPT_DIR" REPO_ROOT="$REPO_ROOT" TARGET_ENV="$TARGET_ENV" \ "${DELEGATE_JOB_PYTHON:-python3}" -I "$SCRIPT_DIR/../lib_py/delegation/envfile.py" --shell "$TARGET_ENV")" \ || { echo "ERROR: cannot load $TARGET_ENV (see the line above)" >&2; exit 1; } eval "$_env_exports"; unset _env_exports fi # Source EARLY (before any herdr usage in run_agent) β€” this is what turns # plain `herdr` into the tmux-compat shim (herdr() function) and provides # resolve_herdr_session/send_keys_safe. Sourcing it late meant the # has-session pre-flight check below used to hit the real herdr binary with # a nonexistent subcommand and always fail. source "$SCRIPT_DIR/../lib.sh" # Pick an interpreter: prefer a project .venv, else python3. pick_python() { local py_bin if [[ -n "${DELEGATE_JOB_PYTHON:-}" ]]; then py_bin="$DELEGATE_JOB_PYTHON" elif [[ -n "${AGENT_PYTHON_BIN:-}" ]] && [[ -x "$AGENT_PYTHON_BIN" ]]; then py_bin="$AGENT_PYTHON_BIN" elif [[ -x "${WORKDIR:-.}/.venv/bin/python" ]]; then py_bin="${WORKDIR}/.venv/bin/python" elif [[ -x ".venv/bin/python" ]]; then py_bin="$(pwd)/.venv/bin/python" else py_bin="python3" fi if ! "$py_bin" -c "import paho.mqtt" 2>/dev/null; then echo "ERROR: paho-mqtt package is missing for $py_bin." >&2 echo " Please create a virtual environment and install it:" >&2 echo " python3 -m venv .venv && .venv/bin/pip install -r \"$SCRIPT_DIR/requirements.txt\"" >&2 exit 1 fi echo "$py_bin" } REGISTRY_DIR_DEFAULT="${MAM_JOBS_DIR:-.mam/jobs}" # Issue #21: the injected brief path, the start/success/error commands and the # Output Report Path must work from any cwd - a creator in a git worktree runs # them from there. Called right after `cd "$WORKDIR"`, so a relative registry # still resolves against WORKDIR, as before. _abs_registry_dir() { mkdir -p "$REGISTRY_DIR" REGISTRY_DIR="$(cd -P "$REGISTRY_DIR" && pwd -P)" export SKS_REGISTRY_DIR="$REGISTRY_DIR" } # The audit-log root this wrapper's own subscriber resolves (same resolver: # next to $REGISTRY_DIR unless DELEGATE_JOB_LOGS_DIR says otherwise, Issue # #57), pinned into the injected commands so an agent publishing from another # cwd does not fall back to its own default. _logs_dir_abs() { "$PY" -c 'import os, sys; sys.path.insert(0, sys.argv[1]); from lib_py.delegation.logroot import get_logs_dir; print(os.path.abspath(get_logs_dir(sys.argv[2])))' "$SCRIPT_DIR/.." "$REGISTRY_DIR" } # Issue #17 item 4: the wrapper's decisions are lib_py/delegation/wrapper.py; # bash only branches on what a subcommand prints or returns. _decide() { PYTHONPATH="$SCRIPT_DIR/..${PYTHONPATH:+:$PYTHONPATH}" "$PY" -m lib_py.delegation.wrapper "$@" } # A decision wrapper.py could not make (its module or $PY broken) ends the # wrapper with 1 (fail-closed): never with its own code, 0, or the empty answer # (`exit ""` is 255 on bash 3.2). _decision_failed() { echo "ERROR: the wrapper could not decide '$1' (lib_py.delegation.wrapper: $2); exiting 1 (fail-closed)" >&2 exit 1 } # _decided : a printed decision into $var. _decided() { local __var="$1" __out __rc=0 shift __out="$(_decide "$@")" || __rc=$? [ "$__rc" -eq 0 ] || _decision_failed "$1" "rc $__rc" [ -n "$__out" ] || _decision_failed "$1" "no answer" printf -v "$__var" '%s' "$__out" } # _decided_yes : a yes/no decision (asked with --print, # so "no" is an answer and a failed call is not): returns 0 for yes, 1 for no. _decided_yes() { local yn _decided yn "$@" --print case "$yn" in yes) return 0 ;; no) return 1 ;; esac _decision_failed "$1" "answer '$yn'" } # _exit_for : exit with the wrapper exit code for a job status. _exit_for() { local code _decided code exit-code "$1" [[ "$code" =~ ^[0-9]+$ ]] || _decision_failed exit-code "answer '$code'" exit "$code" } # _pub_cmd : the publish command as one shell-quoted string. _pub_cmd() { local s s="$( if [ -n "${LOGS_DIR_ABS:-}" ]; then printf 'DELEGATE_JOB_LOGS_DIR=%q ' "$LOGS_DIR_ABS"; fi printf '%q ' "$PY" "$SCRIPT_DIR/scripts/publish_event.py" --registry-dir "$REGISTRY_DIR" --job "$JOB_ID" "$@" )" printf '%s' "${s% }" } # _instructions [iteration]: the prompt injected into the agent (direct # and loop jobs share it; test18 pins parts of this wording, so Issue #34's # sub-delegation sentence goes at the end). _instructions() { local pub="$1" iter="${2:-}" brief="$REGISTRY_DIR/$JOB_ID/brief.md" printf 'Your job_id is "%s". Detailed task requirements, instructions, and target output paths%s are documented in the task brief file at: %s. Please READ and follow %s to complete your work. Commands: start='"'"'%s --event started --heartbeat %s'"'"' (run it FIRST, within %s s; it also starts an automatic heartbeat timer), success='"'"'%s --event completed --detail '"'"', error='"'"'%s --event error --detail '"'"'. If you delegate any part of this work with multi-agent-mux-delegate-job submit, add --parent-job %s so the orchestrator waits for it.' \ "$JOB_ID" "${iter:+ for iteration $iter}" "$brief" "$brief" "$pub" "$HEARTBEAT_SEC" "$ACK_TIMEOUT" "$pub" "$pub" "$JOB_ID" } usage() { cat <<'EOF' multi-agent-mux-delegate-job [options] submit --agent (--prompt | --prompt-file ) [--workdir ] [--agent-session