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, base59be1c8-> head708262a, 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
7.6 KiB
🔍 Review Report — PR #37 델타 리뷰 2회차 (job 1a9fbbc4)
- Reviewer:
planner-reviewer-claude-01(role: planner,reviewer) · 2026-09-24 - 대상 델타:
59be1c8..708262a(1커밋, 3파일+63/−7,git diff --check클린), basemain 1debc7d(v4.7.2). - 검증 경로:
/var/folders/.../T/mam-review/loop-20260924T085626Z-b8987e/1. identity 확인:HEAD = 708262a5…1e30,HEAD^{tree} = d601c2d0…5b96— 브리프와 일치. - 1회차 내 판정: PASS + H1~H5 (job
4ab12ae2). 이번에는 H1(러너 쓰기 실패)·H3(빈 파일)·S9 이식성의 정정을 본다. - 안전 규칙 준수: 검증 경로·소스 워크트리·본 체크아웃(
main 1debc7d) 모두 수정·커밋·checkout 없음. 러너 실험은 in-process 스텁, 래퍼는--dry-run+--agent human+도달 불가 브로커, 되돌린 사본은 scratchpad. 실제 herdr·세션·.mam·데몬 조작 없음, push·merge·PR 상태 변경 없음.
결론 요약
세 정정 모두 지적을 닫고, 1회차 판정을 뒤집을 요소는 없다. 내가 1회차에서 H1 을 재현했던 바로 그 스크립트를 정정된 러너에 다시 돌려 "실패 제출·부분 파일 없음·래퍼 미실행"을 확인했고, 정상·exec 실패·쓰기 실패 세 경로에서 파일이 정확히 한 번만 지워지는 것도 확인했다. 비차단 관찰 1건(I1).
1. R1/H1 — 러너의 쓰기 실패 (항목 1)
runner.py:472-491: mkdir·NamedTemporaryFile·write 가 하나의 try 로 들어가고, prompt_file 은 "" 로 초기화된 뒤 파일이 생기는 즉시(write 이전) 이름을 받는다. except OSError 는 이름이 있으면 지우고 CompletedProcess(…, 126, "", "cannot write the prompt file under <mam_dir>: <err> (prompt N bytes)") 를 돌려준다. exec 용 try/finally(:504-516)는 그 뒤에 따로 있어 두 블록이 겹치지 않는다.
실측 (검증 경로 러너 in-process)
| 경로 | 결과 |
|---|---|
쓰기 실패(1회차 H1 재현 스크립트 그대로: write 가 OSError(28)) |
delegate_job 이 None 을 반환(예외 없음), 로그 cannot write the prompt file under …: [Errno 28] No space left on device (prompt 5 bytes), .mam 잔존 0, 래퍼 호출 0 |
| 정상 | 래퍼 호출 시점에 파일 존재·내용 'hello'·권한 0600, os.unlink 호출 1회, 잔존 0 |
exec 실패(subprocess.run 이 OSError(2)) |
rc 126 경로, os.unlink 호출 1회, 잔존 0, 반환 None |
- 반환하는
CompletedProcess는 exec 실패와 같은 rc 126 이라 기존 처리(submit failed (exit 126)로그, busy 재시도 없음, id 없음 →failed_submit_jobs미등록·None 반환 → 호출자return 1)에 그대로 합류한다. S4c 가submit failed (exit 126)·cannot write the prompt file·Traceback부재·래퍼 미실행·잔존 없음을 한꺼번에 단언한다. NamedTemporaryFile생성 자체가 실패하면prompt_file이 빈 채로except에 들어가unlink를 건너뛴다 — 지울 것이 없으니 맞다.mkdir실패도 같은 블록이다.
2. H3 — 빈 --prompt-file 거부 (항목 2)
multi-agent-mux-delegate-job:193 의 [[ -s "$PROMPT_FILE" ]] 는 래퍼 submit 의 입력에만 걸린다. 정당한 빈 프롬프트 사용처를 막는지 실측:
| 경로 | 결과 |
|---|---|
래퍼 submit --prompt-file empty.md |
submit: --prompt-file <절대경로> is empty, rc 1 (S1e) |
래퍼 submit --prompt-file newline.md(개행 1바이트) |
등록됨 — -s 는 바이트 크기 기준이라 공백만 있는 파일은 통과한다(I1) |
래퍼 submit --prompt "" |
종전대로 submit requires --prompt or --prompt-file 거부 — 텍스트 형식과 대칭 |
registry.py register --prompt-file empty.md(직접) |
등록됨(rc 0) — 등록부는 그대로 |
loop 형 회차의 update --prompt-file "$job_dir/prompt.md" |
cmd_submit 을 거치지 않으므로 검사 밖(변경 없음) |
즉 새 검사는 "빈 텍스트를 거부하던" 기존 규칙을 파일 형식으로 확장한 것이고, 등록부·loop 회차 경로는 건드리지 않는다. 막히는 정당한 사용처는 없다.
3. R2/N1 — S9 의 macOS 이식성
sys.platform == "linux" 일 때만 File too large 를 단언하고, rc≠0·prompt-shloop-* 잔존 없음·delegate_job_safe: cannot write the prompt file 은 모든 플랫폼에서 단언한다. 1회차에서 내가 하네스를 직접 돌려 확인한 macOS 동작(rc 1·잔존 없음·원인 문구 출력)과 정확히 일치하는 계약이다.
4. 테스트 (항목 1·3)
4-1. 수정 없이는 실패하는가 — 확인함
검증 경로 사본에서 runner.py 와 래퍼만 59be1c8 판으로 되돌리고 -k "s4c or s1e or s9 or s4 or s1" 을 돌렸다:
2 failed, 6 passed, 1 skipped in 3.11s
FAILED test_s4c_a_prompt_file_that_cannot_be_written_is_a_failed_submit ← H1
FAILED test_s1e_an_empty_prompt_file_is_refused ← H3
S9 는 되돌린 사본에서도 통과한다 — run_loop.sh 쪽 수정은 이미 59be1c8 에 있었고 이번 델타는 테스트의 단언만 이식 가능하게 바꿨기 때문이다(macOS 에서 통과 = R2 정정 확인). S1/S1c/S1d/S4/S4b 는 양쪽 모두 통과(불변 조건).
4-2. 검증 경로에서의 실행
tests/test_issue36_prompt_transport.py tests/test_issue5_phase1_delegation.py tests/test_loop_cli.py
tests/test_issue14_delivery_ack.py tests/test_issue25_plan_reuse.py
→ 127 passed, 1 skipped in 215.94s failed 0 (skip = linux_only S1b)
1회차의 유일한 실패였던 S9 가 이 호스트에서 통과하고, 새 파일 24건(23 통과 + Linux 전용 skip 1)과 인접 계약 스위트가 모두 통과한다.
4-3. 격리
| 항목 | 결과 |
|---|---|
실제 .mam/jobs 전후 |
차이 0 |
prompt-* 잔존(실제 .mam / 검증 경로 .mam) |
0 / 0 |
| 검증 경로 tracked 파일 | 변화 없음(HEAD 708262a), 본 체크아웃 main 1debc7d |
5. 병합 차단 여부 (항목 3)
1회차 PASS 를 뒤집을 요소는 없다. 델타는 러너의 try 범위 확장(runner.py:472-491), 래퍼의 -s 한 줄(:193), 테스트 3건(S4c·S1e·S9 조건부 단언)뿐이고, 정정 대상만 정확히 실패→통과로 바뀌었다. 버전 락스텝 bump 부재(4.7.2, 오케스트레이터가 4.7.3 으로 처리 예정) 외에 병합을 막을 결함은 없다. 1회차 H2(loop SKILL.md 의 MAM_DIFF_MAX_* 의미)·H4($PROMPT 후행 개행)는 이 델타 범위 밖이라 그대로 열려 있다.
| # | 비차단 관찰 | 근거 |
|---|---|---|
| I1 | -s 는 바이트 크기 검사라 개행·공백만 있는 프롬프트 파일은 통과한다. 텍스트 형식(--prompt "")의 거부와 완전히 대칭이려면 [[ -n "$(tr -d '[:space:]' < "$PROMPT_FILE")" ]] 류가 필요하지만, 실무에서 그런 입력은 없다 |
multi-agent-mux-delegate-job:193, 실측 |
6. 교차검증 가이드
RP=/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260924T085626Z-b8987e/1
PY=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python
git -C "$RP" rev-parse HEAD 'HEAD^{tree}' # 708262a… / d601c2d…
# H1 재현 스크립트(1회차 §1 의 것)를 정정된 러너에 → returned None, leftover [], wrapper called 0
# 되돌린 사본: runner.py·래퍼를 59be1c8 판으로 → -k "s4c or s1e or s9 or s4 or s1" → 2 failed / 6 passed / 1 skipped
(cd "$RP" && AGENT_PYTHON_BIN="$PY" PYTHONDONTWRITEBYTECODE=1 "$PY" -m pytest -p no:cacheprovider -q --basetemp=/tmp/bt tests/test_issue36_prompt_transport.py) # 23 passed, 1 skipped
[VERDICT: PASS]