fix(issue-36): registry and wrapper accept --prompt-file; the wrapper hands the same path on

One argv string is capped at MAX_ARG_STRLEN (131,072 bytes on Linux), so a
prompt passed as --prompt could not reach the registry once it carried a
large diff. submit now takes --prompt-file and passes the same path to
`registry.py register --prompt-file`; the loop type's per-iteration update
goes by file too. --prompt keeps working unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-09-24 02:30:13 +00:00
1 parent 1debc7d924
commit 68a5d31381
3 files changed
+46 -11

No files matched your search

+24 -6
View File
@@ -323,7 +323,11 @@ def _build_parser() -> argparse.ArgumentParser:
sub = parser.add_subparsers(dest="command", required=True) sub = parser.add_subparsers(dest="command", required=True)
p_reg = sub.add_parser("register", help="create a pending job; prints the job id") p_reg = sub.add_parser("register", help="create a pending job; prints the job id")
p_reg.add_argument("--prompt", required=True) # Issue #36: a long prompt goes by file - one argv string is capped at
# MAX_ARG_STRLEN (131,072 bytes on Linux).
reg_prompt = p_reg.add_mutually_exclusive_group(required=True)
reg_prompt.add_argument("--prompt")
reg_prompt.add_argument("--prompt-file", help="read the prompt from this file (utf-8)")
p_reg.add_argument("--agent", default="claude-code") p_reg.add_argument("--agent", default="claude-code")
p_reg.add_argument("--agent-session", default="herdr:claude") p_reg.add_argument("--agent-session", default="herdr:claude")
p_reg.add_argument("--role", default="Worker", help="logical role for the delegated agent (e.g. Worker, Planner, Reviewer)") p_reg.add_argument("--role", default="Worker", help="logical role for the delegated agent (e.g. Worker, Planner, Reviewer)")
@@ -365,7 +369,9 @@ def _build_parser() -> argparse.ArgumentParser:
p_update.add_argument("--job", required=True) p_update.add_argument("--job", required=True)
p_update.add_argument("--status", default=None) p_update.add_argument("--status", default=None)
p_update.add_argument("--agent-session", default=None) p_update.add_argument("--agent-session", default=None)
p_update.add_argument("--prompt", default=None) upd_prompt = p_update.add_mutually_exclusive_group()
upd_prompt.add_argument("--prompt", default=None)
upd_prompt.add_argument("--prompt-file", default=None, help="read the prompt from this file (utf-8)")
p_update.add_argument("--iteration", type=int, default=None) p_update.add_argument("--iteration", type=int, default=None)
p_update.add_argument("--attempt-id", type=int, default=None) p_update.add_argument("--attempt-id", type=int, default=None)
p_update.add_argument("--role", default=None) p_update.add_argument("--role", default=None)
@@ -473,15 +479,26 @@ def _operator_write(job_id: str, registry_dir: str, **fields) -> int:
return 4 return 4
def _prompt_from_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> Optional[str]:
"""The prompt text: from --prompt-file (read verbatim) or from --prompt."""
if getattr(args, "prompt_file", None) is None:
return args.prompt
try:
return Path(args.prompt_file).read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
parser.error(f"cannot read --prompt-file {args.prompt_file}: {exc}")
def main(argv: Optional[List[str]] = None) -> int: def main(argv: Optional[List[str]] = None) -> int:
mqtt_common.ensure_env_loaded() mqtt_common.ensure_env_loaded()
mqtt_common.setup_logging(logging.INFO) mqtt_common.setup_logging(logging.INFO)
args = _build_parser().parse_args(argv) parser = _build_parser()
args = parser.parse_args(argv)
rd = args.registry_dir rd = args.registry_dir
if args.command == "register": if args.command == "register":
job_id = register_job( job_id = register_job(
prompt=args.prompt, prompt=_prompt_from_args(parser, args),
agent=args.agent, agent=args.agent,
agent_session=args.agent_session, agent_session=args.agent_session,
role=args.role, role=args.role,
@@ -540,8 +557,9 @@ def main(argv: Optional[List[str]] = None) -> int:
fields["status"] = args.status fields["status"] = args.status
if args.agent_session is not None: if args.agent_session is not None:
fields["agent_session"] = args.agent_session fields["agent_session"] = args.agent_session
if args.prompt is not None: prompt = _prompt_from_args(parser, args)
fields["prompt"] = args.prompt if prompt is not None:
fields["prompt"] = prompt
if args.iteration is not None: if args.iteration is not None:
fields["iteration"] = args.iteration fields["iteration"] = args.iteration
if args.attempt_id is not None: if args.attempt_id is not None:
@@ -44,6 +44,8 @@ multi-agent-mux-delegate-job submit \
# Liveness contract (Issue #18): the agent heartbeats every N s (automatic), # Liveness contract (Issue #18): the agent heartbeats every N s (automatic),
# the job is unresponsive after K x N s (--idle-timeout overrides), `started` # the job is unresponsive after K x N s (--idle-timeout overrides), `started`
# must arrive within T s of the injection or the inject fails closed. # must arrive within T s of the injection or the inject fails closed.
# --prompt-file <path> replaces --prompt for long prompts (Issue #36): one
# argv string is capped at 131,072 bytes on Linux. The loop always uses it.
# 1b) Same, for an agent on another host (no herdr pane, no shared disk): # 1b) Same, for an agent on another host (no herdr pane, no shared disk):
# the brief travels over the broker as a signed retained dispatch and a # the brief travels over the broker as a signed retained dispatch and a
@@ -109,13 +109,15 @@ usage() {
cat <<'EOF' cat <<'EOF'
multi-agent-mux-delegate-job <command> [options] multi-agent-mux-delegate-job <command> [options]
submit --agent <name> --prompt <text> [--workdir <dir>] [--agent-session <label>] submit --agent <name> (--prompt <text> | --prompt-file <path>) [--workdir <dir>] [--agent-session <label>]
[--timeout <sec>] [--idle-timeout <sec>] [--validate <script>] [--timeout <sec>] [--idle-timeout <sec>] [--validate <script>]
[--heartbeat <sec>] [--heartbeat-misses <count>] [--ack-timeout <sec>] [--heartbeat <sec>] [--heartbeat-misses <count>] [--ack-timeout <sec>]
[--registry-dir <dir>] [--dry-run] [--role <role_name>] [--registry-dir <dir>] [--dry-run] [--role <role_name>]
[--type <direct|loop|discuss>] [--reviewer <reviewer_agent>] [--type <direct|loop|discuss>] [--reviewer <reviewer_agent>]
[--reviewer-session <reviewer_session>] [--max-iterations <count>] [--reviewer-session <reviewer_session>] [--max-iterations <count>]
[--counterpart-role <role_name>] [--strict-role-check] [--counterpart-role <role_name>] [--strict-role-check]
# --prompt-file passes the prompt by file: one argv string is capped
# at 131,072 bytes on Linux (Issue #36). The file is read, not kept.
# --agent-session herdr:<name> injects into a local herdr pane; # --agent-session herdr:<name> injects into a local herdr pane;
# --agent-session remote:<name> dispatches over the broker (Issue #18). # --agent-session remote:<name> dispatches over the broker (Issue #18).
# Liveness contract: the agent heartbeats every --heartbeat N s # Liveness contract: the agent heartbeats every --heartbeat N s
@@ -131,7 +133,7 @@ EOF
} }
# ---- arg parsing helpers -------------------------------------------------- # ---- arg parsing helpers --------------------------------------------------
AGENT="claude-code"; PROMPT=""; WORKDIR="$(pwd)"; AGENT_SESSION="herdr:claude" AGENT="claude-code"; PROMPT=""; PROMPT_FILE=""; WORKDIR="$(pwd)"; AGENT_SESSION="herdr:claude"
TIMEOUT=3600; IDLE_TIMEOUT=""; VALIDATE=""; DRY_RUN=0 TIMEOUT=3600; IDLE_TIMEOUT=""; VALIDATE=""; DRY_RUN=0
# Issue #18 liveness contract (N / K / T). An unset --idle-timeout derives K x N. # Issue #18 liveness contract (N / K / T). An unset --idle-timeout derives K x N.
HEARTBEAT_SEC="${MAM_HEARTBEAT_SEC:-30}"; HEARTBEAT_MISSES="${MAM_HEARTBEAT_MISSES:-4}"; ACK_TIMEOUT="${MAM_ACK_TIMEOUT_S:-180}" HEARTBEAT_SEC="${MAM_HEARTBEAT_SEC:-30}"; HEARTBEAT_MISSES="${MAM_HEARTBEAT_MISSES:-4}"; ACK_TIMEOUT="${MAM_ACK_TIMEOUT_S:-180}"
@@ -148,6 +150,7 @@ parse_opts() {
case "$1" in case "$1" in
--agent) AGENT="$2"; shift 2;; --agent) AGENT="$2"; shift 2;;
--prompt) PROMPT="$2"; shift 2;; --prompt) PROMPT="$2"; shift 2;;
--prompt-file) PROMPT_FILE="$2"; shift 2;;
--workdir) WORKDIR="$2"; shift 2;; --workdir) WORKDIR="$2"; shift 2;;
--agent-session) AGENT_SESSION="$2"; shift 2;; --agent-session) AGENT_SESSION="$2"; shift 2;;
--timeout) TIMEOUT="$2"; shift 2;; --timeout) TIMEOUT="$2"; shift 2;;
@@ -183,7 +186,17 @@ _to_lower() {
cmd_submit() { cmd_submit() {
parse_opts "$@" parse_opts "$@"
[[ -n "$PROMPT" ]] || { echo "submit requires --prompt" >&2; exit 1; } if [[ -n "$PROMPT_FILE" ]]; then
[[ -z "$PROMPT" ]] || { echo "submit takes --prompt or --prompt-file, not both" >&2; exit 1; }
[[ "$PROMPT_FILE" == /* ]] || PROMPT_FILE="$(pwd)/$PROMPT_FILE"
[[ -f "$PROMPT_FILE" && -r "$PROMPT_FILE" ]] || { echo "submit: cannot read --prompt-file $PROMPT_FILE" >&2; exit 1; }
PROMPT="$(<"$PROMPT_FILE")"
fi
[[ -n "$PROMPT" || -n "$PROMPT_FILE" ]] || { echo "submit requires --prompt or --prompt-file" >&2; exit 1; }
# Issue #36: the prompt reaches the registry by file when it came by file,
# never as one argv string (capped at 131,072 bytes).
local reg_prompt=(--prompt "$PROMPT")
[[ -n "$PROMPT_FILE" ]] && reg_prompt=(--prompt-file "$PROMPT_FILE")
PY="$(pick_python)" PY="$(pick_python)"
[[ "$PY" == */* && "$PY" != /* ]] && PY="$(pwd)/$PY" [[ "$PY" == */* && "$PY" != /* ]] && PY="$(pwd)/$PY"
cd "$WORKDIR" cd "$WORKDIR"
@@ -192,7 +205,7 @@ cmd_submit() {
# 1) register job (prints the new job id) # 1) register job (prints the new job id)
JOB_ID="$("$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" register \ JOB_ID="$("$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" register \
--prompt "$PROMPT" --agent "$AGENT" --agent-session "$AGENT_SESSION" --role "$DELEGATE_ROLE" \ "${reg_prompt[@]}" --agent "$AGENT" --agent-session "$AGENT_SESSION" --role "$DELEGATE_ROLE" \
--timeout "$TIMEOUT" --idle-timeout "$IDLE_TIMEOUT" \ --timeout "$TIMEOUT" --idle-timeout "$IDLE_TIMEOUT" \
--heartbeat-sec "$HEARTBEAT_SEC" --heartbeat-misses "$HEARTBEAT_MISSES" --ack-timeout "$ACK_TIMEOUT" \ --heartbeat-sec "$HEARTBEAT_SEC" --heartbeat-misses "$HEARTBEAT_MISSES" --ack-timeout "$ACK_TIMEOUT" \
--transport "$(_is_remote "$AGENT_SESSION" && echo remote || echo herdr)" \ --transport "$(_is_remote "$AGENT_SESSION" && echo remote || echo herdr)" \
@@ -392,6 +405,8 @@ EOF
## 🔎 Task Description ## 🔎 Task Description
$current_prompt $current_prompt
EOF EOF
# Issue #36: the registry gets the prompt by file, not as one argv string
printf '%s' "$current_prompt" > "$job_dir/prompt.md"
echo "provisioned iteration brief: $job_dir/brief.md" echo "provisioned iteration brief: $job_dir/brief.md"
attempt=$((attempt + 1)) attempt=$((attempt + 1))
@@ -432,7 +447,7 @@ EOF
"$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" update \ "$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" update \
--job "$JOB_ID" \ --job "$JOB_ID" \
--agent-session "$current_session" \ --agent-session "$current_session" \
--prompt "$current_prompt" \ --prompt-file "$job_dir/prompt.md" \
--iteration "$iteration" \ --iteration "$iteration" \
--attempt-id "$attempt" \ --attempt-id "$attempt" \
--role "$display_role" \ --role "$display_role" \