From c77806b1b13a49fac719016f1ba376763c8c9968 Mon Sep 17 00:00:00 2001 From: Godopu Date: Thu, 24 Sep 2026 02:30:13 +0000 Subject: [PATCH] fix(issue-36): the loop passes every delegated prompt as a file; a wrapper that cannot start is a failed submit, on the first try and on the retry delegate_job writes the prompt to a temp file under the control root's .mam/ and hands the wrapper --prompt-file; the file is removed once the wrapper returns. An OSError from starting the wrapper becomes a failed submit (rc 126) that the existing error branch logs, so the session-busy retry no longer dereferences a missing result. A retry that yields no job id, and a compliance job that cannot be registered, are now logged instead of silent. The collector caps are documented as the size of the evidence a reviewer is shown; delivery has no cap of its own any more. The loop skill notes that a pipeline hides the loop's exit code. Co-Authored-By: Claude Opus 5.5 --- .agents/skills/lib_py/loop/runner.py | 31 +++++++++++++++++-- .agents/skills/multi-agent-mux-loop/SKILL.md | 1 + .../scripts/diff_collect.sh | 3 ++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.agents/skills/lib_py/loop/runner.py b/.agents/skills/lib_py/loop/runner.py index c5d7726..6d5bdff 100644 --- a/.agents/skills/lib_py/loop/runner.py +++ b/.agents/skills/lib_py/loop/runner.py @@ -464,7 +464,17 @@ class LoopRunner: if not wrapper_script.exists(): wrapper_script = Path(__file__).resolve().parent.parent / "multi-agent-mux-delegate-job" / "multi-agent-mux-delegate-job" - def _run_cmd(p_text: str): + def _run_cmd(p_text: str) -> subprocess.CompletedProcess: + # Issue #36: the prompt goes by file. One argv string is capped at + # MAX_ARG_STRLEN (131,072 bytes on Linux), and a review prompt + # carries the whole diff. The wrapper blocks until the job ends, + # so nothing reads the file after it returns. + self.mam_dir.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=self.mam_dir, + prefix=f"prompt-{self.loop_id}-", suffix=".md", + delete=False) as fh: + fh.write(p_text) + prompt_file = fh.name cmd = [ "bash", str(wrapper_script), "submit", "--agent-session", self.session_label(agent_session), @@ -472,12 +482,24 @@ class LoopRunner: "--type", "direct", "--role", role, "--registry-dir", str(self.jobs_dir), - "--prompt", p_text, + "--prompt-file", prompt_file, ] + self.contract_args() env = dict(os.environ) if "DELEGATE_JOB_PYTHON" not in env: env["DELEGATE_JOB_PYTHON"] = sys.executable - return subprocess.run(cmd, capture_output=True, text=True, cwd=str(self.workspace_root), env=env) + try: + return subprocess.run(cmd, capture_output=True, text=True, cwd=str(self.workspace_root), env=env) + except OSError as e: + # A wrapper that cannot start is a failed submit (126, the shell's + # "cannot execute"), on the first try and on the retry alike. + msg = (f"cannot start the wrapper: {e} " + f"(prompt {len(p_text.encode('utf-8'))} bytes, wrapper {wrapper_script})") + return subprocess.CompletedProcess(cmd, 126, "", msg) + finally: + try: + os.unlink(prompt_file) + except OSError: + pass res = _run_cmd(prompt) match = re.search(r"registered job:\s*([A-Za-z0-9_-]+)", res.stdout) @@ -500,6 +522,7 @@ class LoopRunner: self.failed_submit_jobs.add(job_id_retry) log_error(f"delegate_job retry failed (exit {res_retry.returncode}):\n{res_retry.stderr or res_retry.stdout}") return job_id_retry + log_error(f"delegate_job retry failed (exit {res_retry.returncode}):\n{res_retry.stderr or res_retry.stdout}") if job_id: self.created_jobs.append(job_id) self.failed_submit_jobs.add(job_id) @@ -1362,6 +1385,8 @@ class LoopRunner: comp_jid = self._dispatch(self.creator, creator_type, "Worker", comply_prompt) if comp_jid: self.wait_for_job(comp_jid) + else: + log_error("Failed to register Creator compliance job.") # Promotion if self.plan_mode and self.current_plan and self.plan_job_id: diff --git a/.agents/skills/multi-agent-mux-loop/SKILL.md b/.agents/skills/multi-agent-mux-loop/SKILL.md index 1007730..3a56ddf 100644 --- a/.agents/skills/multi-agent-mux-loop/SKILL.md +++ b/.agents/skills/multi-agent-mux-loop/SKILL.md @@ -264,4 +264,5 @@ bash .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh \ - **Fail-closed on missing verdict**: 최종 Verdict 토큰이 누락되거나 리포트 픽업에 실패하면, 파서는 **경고 후 통과시키는 것이 아니라** 안전을 위해 즉시 `NOT PASS`로 판정(fail-closed)하고 교정 사이클을 수행합니다. 리뷰어에게는 반드시 리포트 끝에 단독 행으로 토큰을 찍도록 지시해야 합니다. - **Session Availability**: `run_loop.sh` 기동 전에 참조되는 Planner, Target Agent, Reviewer 세션들이 모두 herdr 세션으로 기동되어 (`status.sh` 기준 `alive` 및 `running`) 있어야 합니다. - **동시 루프 기동 금지 (NFS Lock Shadowing)**: 동일한 작업 트리 내에서 다수의 `run_loop.sh` 제어기를 동시에 기동하면 SQLite DB 갱신 경합 및 YAML 데이터 오염이 발생합니다. 하나의 루프가 끝날 때까지 다른 루프를 병렬로 기동하지 마십시오. (실제로는 loop lock 이 두 번째 루프를 exit 1 로 거부한다 — run_loop.sh 의 `mam_acquire_loop_lock` 분기) +- **파이프라인의 종료 코드 (Issue #36)**: 위임이 실패하면 루프는 원인을 stderr 에 남기고 0 이 아닌 코드로 끝납니다. 루프를 `… | tee log` 처럼 파이프라인으로 돌리면 `$?` 는 `tee` 의 것이므로 `set -o pipefail` 또는 `${PIPESTATUS[0]}` 로 루프의 코드를 보십시오. stderr 도 함께 남기십시오(`2>&1`). - **원자적 아카이빙 (Promotion)**: 루프 성공 종료 시 최종 계획서와 검증 리포트들은 `.agents/reports//` 디렉토리로 원자적으로 덮어쓰기(`mv -f`)되어 보존됩니다. 해당 경로의 리포트들로 VCS 추적성을 확보해야 합니다. diff --git a/.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh b/.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh index 7c75d76..e95d3c6 100755 --- a/.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh +++ b/.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh @@ -6,6 +6,9 @@ MAM_DIFF_UNAVAILABLE_MARKER="!!! CHANGE SET UNAVAILABLE !!!" MAM_DIFF_TRUNCATED_MARKER="!!! DIFF TRUNCATED !!!" MAM_DIFF_NESTED_MARKER="!!! NOT EXPANDED IN THIS DIFF !!!" +# These caps bound the evidence a reviewer is shown, nothing else. The prompt +# reaches the agent by file (Issue #36), so delivery has no size cap of its own: +# only a diff over these limits is truncated (and marked). MAM_DIFF_MAX_BYTES="${MAM_DIFF_MAX_BYTES:-200000}" MAM_DIFF_MAX_LINES="${MAM_DIFF_MAX_LINES:-4000}"