fix(#36): 위임 프롬프트를 argv 가 아닌 파일로 전달하고, 위임 실패를 종료 코드 1 로 보고 #37

Merged
Godopu merged 8 commits from fix/issue-36 into main 2026-09-24 09:10:39 +00:00
Owner

Closes #36

리뷰 루프가 diff 를 리뷰 프롬프트에 넣어 bash 에 단일 argv 원소로 넘기던 탓에, 프롬프트가 리눅스의 단일 인자 상한(MAX_ARG_STRLEN, 131,072 바이트)을 넘으면 OSError: [Errno 7] 로 죽고 종료 코드 0 으로 끝나던 문제를 고친다.

변경 요지

프롬프트는 파일로 전달된다. 프롬프트가 지나는 경로 전체에서 argv 로 긴 본문을 넘기던 지점을 없앴다.

  • 파이썬 러너(lib_py/loop/runner.py)는 모든 위임 프롬프트를 제어 루트 .mam 아래 임시 파일에 쓰고 래퍼에 --prompt-file 로 넘긴다. 래퍼가 끝나면 지운다.
  • delegate-job 래퍼와 등록부(registry.py)가 --prompt-file 을 받는다. 래퍼는 받은 경로를 등록부에 그대로 넘긴다.
  • 옛 셸 러너(MAM_LOOP_PY=0)는 위임 11곳이 모두 거치는 delegate_job_safe 가 프롬프트를 임시 파일로 바꿔 외부 명령에 넘긴다. 쓰기에 실패하면 파일을 지우고 stderr 로 진단한다.
  • lib.sh 제출 도우미와 lib_py/delegation/cli.py 사슬도 파일로 넘긴다.
  • create_session 은 에이전트에게 주입하는 안내문에 과제 본문 대신 잡 디렉터리의 brief 절대경로만 넣는다. delegate-job 래퍼가 원래 하던 방식과 같다.

기존 --prompt 인자는 그대로 받는다.

위임 실패는 성공처럼 끝나지 않는다. 래퍼를 실행할 수 없으면(OSError) 실패한 제출로 다루고, 첫 시도와 재시도 모두에서 원인을 남기고 루프가 종료 코드 1 로 끝난다. 반론 이후의 재심·중재·순응 잡이 등록되지 않은 경우도 판정이 아니라 실패로 끝난다.

수집기 상한의 의미를 다시 정했다. MAM_DIFF_MAX_BYTES·MAM_DIFF_MAX_LINES 는 이제 리뷰어에게 보여줄 증거의 크기만 제한한다. 전달 자체에는 크기 한계가 없다. 잘린 diff 를 증거로 쓸 때의 기존 규칙은 그대로다.

검증

항목 결과
전체 스위트 (59be1c8) 1111 통과 / 12 실패 / 19 건너뜀, 실패 12건은 모두 이 호스트의 기준 실패(tests/test_issue10_13_purge.py)이며 base 에서도 같다
리뷰어 5명 전원 PASS
tests/test_issue36_prompt_transport.py 22 통과

리뷰는 세 라운드로 진행했다. 전체 구현 루프(1debc7d..14d11bf)는 2회차에서 전원 PASS 였다. 이후 남은 argv 지점을 닫은 두 커밋은 각각 델타 리뷰를 받았고, 마지막 라운드(f26952c..59be1c8)에서 전원 PASS 다.

새 테스트는 131,072 바이트를 넘는 실제 프롬프트로 실제 subprocess·exec 경로를 통과시킨다. 수정 전 코드(1debc7d)에 같은 테스트 파일을 두고 돌리면 13건 중 11건이 실패한다.

리뷰에서 나온 지적

  • codex, 1차. 반론 처리 경로에서 잡 등록이 실패해도 루프가 계속 진행했다. 14d11bf 로 막았다.
  • 오케스트레이터. 1차 PASS 이후에도 옛 셸 러너와 lib.sh 제출 도우미가 프롬프트를 argv 로 넘겼다. f26952c 로 닫았다.
  • codex, 델타. strace 로 실제 경로를 추적해 두 가지를 재현했다. create_session 이 주입 안내문에 본문 전체를 다시 붙여 130,500 바이트 입력에서 execve 가 E2BIG 로 실패했고, 셸 러너가 쓰기 실패 시 부분 파일을 남겼다. 59be1c8 로 고쳤고, codex 가 제출한 재현 자산을 수정 없이 돌려 통과를 확인했다.

참고

전체 스위트에서 test_issue18_liveness_contract.py 두 건과 test_issue6_fail_closed.py::test_t1_15 가 부하 상태에서 간헐적으로 실패했다. 이번 변경과 무관하다. 수정본에서 따로 돌리면 통과하고, 수정 전 코드에서는 따로 돌려도 실패한다.

🤖 Generated with Claude Code

Closes #36 리뷰 루프가 diff 를 리뷰 프롬프트에 넣어 `bash` 에 단일 argv 원소로 넘기던 탓에, 프롬프트가 리눅스의 단일 인자 상한(`MAX_ARG_STRLEN`, 131,072 바이트)을 넘으면 `OSError: [Errno 7]` 로 죽고 종료 코드 0 으로 끝나던 문제를 고친다. ## 변경 요지 **프롬프트는 파일로 전달된다.** 프롬프트가 지나는 경로 전체에서 argv 로 긴 본문을 넘기던 지점을 없앴다. - 파이썬 러너(`lib_py/loop/runner.py`)는 모든 위임 프롬프트를 제어 루트 `.mam` 아래 임시 파일에 쓰고 래퍼에 `--prompt-file` 로 넘긴다. 래퍼가 끝나면 지운다. - delegate-job 래퍼와 등록부(`registry.py`)가 `--prompt-file` 을 받는다. 래퍼는 받은 경로를 등록부에 그대로 넘긴다. - 옛 셸 러너(`MAM_LOOP_PY=0`)는 위임 11곳이 모두 거치는 `delegate_job_safe` 가 프롬프트를 임시 파일로 바꿔 외부 명령에 넘긴다. 쓰기에 실패하면 파일을 지우고 stderr 로 진단한다. - lib.sh 제출 도우미와 `lib_py/delegation/cli.py` 사슬도 파일로 넘긴다. - create_session 은 에이전트에게 주입하는 안내문에 과제 본문 대신 잡 디렉터리의 brief 절대경로만 넣는다. delegate-job 래퍼가 원래 하던 방식과 같다. 기존 `--prompt` 인자는 그대로 받는다. **위임 실패는 성공처럼 끝나지 않는다.** 래퍼를 실행할 수 없으면(`OSError`) 실패한 제출로 다루고, 첫 시도와 재시도 모두에서 원인을 남기고 루프가 종료 코드 1 로 끝난다. 반론 이후의 재심·중재·순응 잡이 등록되지 않은 경우도 판정이 아니라 실패로 끝난다. **수집기 상한의 의미를 다시 정했다.** `MAM_DIFF_MAX_BYTES`·`MAM_DIFF_MAX_LINES` 는 이제 리뷰어에게 보여줄 증거의 크기만 제한한다. 전달 자체에는 크기 한계가 없다. 잘린 diff 를 증거로 쓸 때의 기존 규칙은 그대로다. ## 검증 | 항목 | 결과 | |---|---| | 전체 스위트 (`59be1c8`) | 1111 통과 / 12 실패 / 19 건너뜀, 실패 12건은 모두 이 호스트의 기준 실패(`tests/test_issue10_13_purge.py`)이며 base 에서도 같다 | | 리뷰어 5명 | 전원 PASS | | `tests/test_issue36_prompt_transport.py` | 22 통과 | 리뷰는 세 라운드로 진행했다. 전체 구현 루프(`1debc7d..14d11bf`)는 2회차에서 전원 PASS 였다. 이후 남은 argv 지점을 닫은 두 커밋은 각각 델타 리뷰를 받았고, 마지막 라운드(`f26952c..59be1c8`)에서 전원 PASS 다. 새 테스트는 131,072 바이트를 넘는 실제 프롬프트로 실제 subprocess·exec 경로를 통과시킨다. 수정 전 코드(`1debc7d`)에 같은 테스트 파일을 두고 돌리면 13건 중 11건이 실패한다. ## 리뷰에서 나온 지적 - **codex, 1차.** 반론 처리 경로에서 잡 등록이 실패해도 루프가 계속 진행했다. `14d11bf` 로 막았다. - **오케스트레이터.** 1차 PASS 이후에도 옛 셸 러너와 lib.sh 제출 도우미가 프롬프트를 argv 로 넘겼다. `f26952c` 로 닫았다. - **codex, 델타.** `strace` 로 실제 경로를 추적해 두 가지를 재현했다. create_session 이 주입 안내문에 본문 전체를 다시 붙여 130,500 바이트 입력에서 `execve` 가 `E2BIG` 로 실패했고, 셸 러너가 쓰기 실패 시 부분 파일을 남겼다. `59be1c8` 로 고쳤고, codex 가 제출한 재현 자산을 수정 없이 돌려 통과를 확인했다. ## 참고 전체 스위트에서 `test_issue18_liveness_contract.py` 두 건과 `test_issue6_fail_closed.py::test_t1_15` 가 부하 상태에서 간헐적으로 실패했다. 이번 변경과 무관하다. 수정본에서 따로 돌리면 통과하고, 수정 전 코드에서는 따로 돌려도 실패한다. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Godopu added 6 commits 2026-09-24 07:51:10 +00:00
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>
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 <noreply@anthropic.com>
S1-S1d push a 140,000-byte prompt through the real wrapper and registry by
file, with a negative control that the same prompt as an argument fails with
E2BIG. S2 runs the real runner against a recording wrapper. S3/S3b run a real
`python -m lib_py.loop` whose wrapper fails or cannot start. S4/S4b/S4c cover
the exec-error handler, the busy retry and the compliance log. S5 sends a
collected diff from the former 131,072..200,000-byte blind spot through whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of f5cadd9 (planner-reviewer-codex-01, R1): the compliance job's
registration failure was only logged, so a passing next round ended the
real loop process with rc 0. Compliance now returns 1 when it cannot be
registered or does not finish, as the correction step already does.
Re-adjudication and arbitration also return 1 when their job cannot be
registered; a registered job that yields no verdict keeps the designed
fail-closed fallback.

S4c (stubbed delegate_job, a fixture whose later rounds never pass) is
replaced by S3c: a real `python -m lib_py.loop` process whose wrapper runs
an executable that simulates the agents, refusing the compliance,
re-adjudication or arbitration registration while the next round passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Review of f26952c (codex, job cc849cbf):

- P2: create_session registered the task by file but then pasted the whole
  task into the onboarding text, which send_keys_safe hands to `herdr agent
  prompt` as one argv string. A 130,500-byte --submit-job was registered
  intact and never reached the agent (execve E2BIG). The task now goes into
  <jobs>/<id>/brief.md, as the delegate-job wrapper does, and the injected
  text names that path. The publish command and its dispatch token (#23) are
  unchanged. The other send_keys_safe callers (the wrapper's short pointer,
  stop_session's exit key) carry no task text.
- P3: delegate_job_safe's write-failure branch left the partial prompt file
  behind and logged to stdout, which callers capture. It now removes the file
  and logs to stderr.

S8 (real create_session.sh, 100 and 130,500 bytes, strace when present) and
S9 (the real function, RLIMIT_FSIZE write failure) carry the reviewer's
reproductions over; all three fail on f26952c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Godopu added 1 commit 2026-09-24 08:55:58 +00:00
- runner._run_cmd: the write of the prompt file was outside the cleanup
  and error handling that the exec had (merge review: claude H1, codex R1).
  A failed write now removes the partial file and returns the same 126
  failed submit an unstartable wrapper does, so the loop ends with rc 1
  and no traceback. Test S4c.
- test S9 required the literal "File too large" in stderr; bash 3.2 on
  macOS prints nothing for EFBIG, so the whole suite failed on this host
  (agy, grok, codex R2). The contract asserted everywhere is rc != 0, no
  leftover file and the portable diagnostic line; the libc wording is
  checked on Linux only.
- the wrapper accepted an empty --prompt-file and registered an empty
  prompt (claude H3); it is refused now. Test S1e.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
Godopu added 1 commit 2026-09-24 09:10:30 +00:00
Issue #36: delegated prompts travel by file, a delegation that fails ends
the loop with rc 1. VERSIONS.md carries the entry; the merge-side reports
(round 1 @59be1c8 and round 2 @708262a with identity sidecars, base
59be1c8 -> head 708262a, evidence review-tree) are archived under
.agents/reports/<session>/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
Godopu merged commit 7a4a0c2091 into main 2026-09-24 09:10:39 +00:00
Author
Owner

병합 완료 — main 7a4a0c2 (v4.7.3, Gitea merge 커밋)

병합 워크스페이스 재검토 (PR head 를 자체 venv 워크트리에 받아 --worktree --base --review-only --all-reviewer):

  • 1회차 @59be1c8: claude PASS, agy·grok·codex NOT PASS. 지적은 세 갈래.
    • 새 테스트 S9 가 stderr 의 리터럴 File too large 를 요구해 macOS bash 3.2 에서 실패(agy·grok·codex R2). 전체 스위트도 이 1건만 실패(1139 통과 / 2 skip).
    • Python 러너 _run_cmd 의 프롬프트 파일 쓰기 실패가 정리·오류 처리 범위 밖이라 부분 파일이 .mam 에 남고 예외가 루프까지 올라옴 — PR 이 셸 쪽 두 곳에서 고친 부류가 러너에 빠져 있었다(claude H1·codex R1, 재현).
    • 래퍼가 빈 --prompt-file 을 받아 빈 프롬프트를 등록(claude H3).
  • 정정 708262a: 쓰기 실패는 exec 실패와 같은 rc 126 실패 제출(래퍼 미실행, 부분 파일 제거), libc 문구는 Linux 에서만 단언, 빈 파일 거부. 회귀 S4c·S1e 는 정정을 되돌리면 실패.
  • 2회차 @708262a: claude·agy·grok·codex 4인 전원 PASS(identity 4건 base 59be1c8 → head 708262a, evidence review-tree). 관련 스위트 125 통과 / 1 skip.
  • codex 는 이 호스트의 SC_ARG_MAX 를 넘는 1,114,112 바이트 프롬프트가 argv 로는 OSError(errno=7), 파일로는 공백 경로·다른 --workdir 조합에서도 원문 그대로 등록됨을 실측했다.
  • 릴리스 c3e6a7e: 4.7.2 → 4.7.3 락스텝, 리포트 8건 + identity 4건 아카이브.

비차단 후속: MAM_DIFF_MAX_* 의 "증거 상한, 전달 무제한" 의미를 loop SKILL.md 에도(H2); 래퍼의 $(<file) 이 후행 개행을 잃어 등록부 프롬프트와 브리프 본문이 다를 수 있음(H4); 순응 잡의 실행 실패도 rc 1 로 끝내는 강화는 타당하나 로그 문구가 "Failed to register" 라 등록 실패와 구분되지 않음(grok·codex).

## 병합 완료 — main `7a4a0c2` (v4.7.3, Gitea merge 커밋) **병합 워크스페이스 재검토** (PR head 를 자체 venv 워크트리에 받아 `--worktree --base --review-only --all-reviewer`): - 1회차 @59be1c8: claude PASS, agy·grok·codex NOT PASS. 지적은 세 갈래. - 새 테스트 S9 가 stderr 의 리터럴 `File too large` 를 요구해 macOS bash 3.2 에서 실패(agy·grok·codex R2). 전체 스위트도 이 1건만 실패(1139 통과 / 2 skip). - Python 러너 `_run_cmd` 의 프롬프트 파일 **쓰기** 실패가 정리·오류 처리 범위 밖이라 부분 파일이 `.mam` 에 남고 예외가 루프까지 올라옴 — PR 이 셸 쪽 두 곳에서 고친 부류가 러너에 빠져 있었다(claude H1·codex R1, 재현). - 래퍼가 빈 `--prompt-file` 을 받아 빈 프롬프트를 등록(claude H3). - 정정 `708262a`: 쓰기 실패는 exec 실패와 같은 rc 126 실패 제출(래퍼 미실행, 부분 파일 제거), libc 문구는 Linux 에서만 단언, 빈 파일 거부. 회귀 S4c·S1e 는 정정을 되돌리면 실패. - 2회차 @708262a: claude·agy·grok·codex **4인 전원 PASS**(identity 4건 base 59be1c8 → head 708262a, evidence review-tree). 관련 스위트 125 통과 / 1 skip. - codex 는 이 호스트의 `SC_ARG_MAX` 를 넘는 1,114,112 바이트 프롬프트가 argv 로는 `OSError(errno=7)`, 파일로는 공백 경로·다른 `--workdir` 조합에서도 원문 그대로 등록됨을 실측했다. - 릴리스 `c3e6a7e`: 4.7.2 → 4.7.3 락스텝, 리포트 8건 + identity 4건 아카이브. **비차단 후속**: `MAM_DIFF_MAX_*` 의 "증거 상한, 전달 무제한" 의미를 loop SKILL.md 에도(H2); 래퍼의 `$(<file)` 이 후행 개행을 잃어 등록부 프롬프트와 브리프 본문이 다를 수 있음(H4); 순응 잡의 실행 실패도 rc 1 로 끝내는 강화는 타당하나 로그 문구가 "Failed to register" 라 등록 실패와 구분되지 않음(grok·codex).
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tmpl/multi-agent-mux#37