Files
multi-agent-mux/.agents/reports/planner-reviewer-claude-01/report-4ab12ae2.md
T
GodopuandClaude Fable 5.1 c3e6a7e96d release(v4.7.3): bump framework and eight skills to 4.7.3 in lockstep; archive PR #37 merge-side reviews
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
2026-09-24 18:10:28 +09:00

16 KiB
Raw Blame History

🔍 Review Report — PR #37 병합 전 독립 리뷰 (job 4ab12ae2)

  • Reviewer: planner-reviewer-claude-01 (role: planner,reviewer) · 2026-09-24
  • 대상: PR #37 fix/issue-36 @ 59be1c8, base main 1debc7d (v4.7.2). 6커밋, 11파일 +765/−30, git diff --check 클린.
  • 검증 경로: /var/folders/.../T/mam-review/loop-20260924T080014Z-2b3df0/1. identity 확인: HEAD = 59be1c89…cb49, HEAD^{tree} = a009c906…fab7 — 브리프와 일치.
  • 안전 규칙 준수: 검증 경로·소스 워크트리(multi-agent-mux-wt/pr-37)·본 체크아웃(main 1debc7d) 모두 수정·커밋·checkout 없음. 실험은 scratchpad 임시 디렉터리(uuid4)에서만(래퍼는 --dry-run + --agent human + 도달 불가 브로커, 러너는 in-process 스텁), pytest 는 AGENT_PYTHON_BIN + --basetemp=<검증 경로 밖>. 실제 herdr·세션·.mam·데몬 조작 없음, push·merge·PR 상태 변경 없음.

결론 요약

병합을 막을 결함은 없다. 프롬프트는 러너·래퍼·lib.sh 도우미·옛 셸 러너·create 온보딩 다섯 경로 모두에서 파일로 간다(grep -rn -- '--prompt ' 의 잔존 호출은 전부 셸 함수 인자이거나 문서/기본값이다). 비차단 지적 4건(H1–H4). 그중 H1 은 재현한 실제 결함이다: 파이썬 러너는 프롬프트 파일 쓰기가 실패하면(ENOSPC 등) 부분 파일을 .mam 에 남기고 예외를 그대로 던진다 — PR 이 셸 쪽 두 곳에서 고친 바로 그 부류가 러너에는 빠져 있다. 디스크 고갈 급의 경계 상황이라 차단으로 보지 않았지만, 릴리스 커밋이나 즉시 후속에서 세 줄로 닫을 것을 권한다.

1. 전달 경로의 완전성 (항목 1)

경로 이전 이제 근거
파이썬 러너 _run_cmd --prompt <text> argv .mam/prompt-<loop_id>-XXXX.md 에 쓰고 --prompt-file runner.py:467-502
래퍼 submit → registry.py register --prompt "$PROMPT" 파일로 왔으면 --prompt-file 그대로 전달, 텍스트로 왔으면 종전대로 multi-agent-mux-delegate-job:186-208
래퍼 loop 타입의 회차 update --prompt "$current_prompt" $job_dir/prompt.md 에 쓰고 --prompt-file :405-450
lib.sh delegate_submit_job / _delegate_submit_job_legacy (제출 도우미 11곳의 종착) argv _delegate_prompt_file 로 .mam/prompt-submit-XXXXXX 에 쓰고 --prompt-file, 성공·실패 모두 rm -f lib.sh:2038-2046, 2049-2064, 2160-2174
lib_py/delegation/cli.py submit --prompt 필수 --prompt/--prompt-file 상호 배타, register 로 그대로 전달 cli.py:29-33, 64-66
옛 셸 러너 MAM_LOOP_PY=0 delegate_job_safe … --prompt "<text>" (함수 인자) 함수 안에서 --prompt 를 .mam/prompt-shloop-XXXXXX + --prompt-file 로 바꿔 래퍼 exec run_loop.sh:196-221. run_loop.sh 의 잔존 --prompt "…" 11곳은 이 함수 호출이라 exec 을 거치지 않는다
create_session.sh --submit-job 주입문에 과제 본문 포함 과제는 <jobs>/<id>/brief.md 에 쓰고 pane 에는 경로만 create_session.sh:625-646

1-1. 래퍼의 파일 처리 (실측, --dry-run --agent human)

  • 상대 --prompt-file "my prompts/p 1.md" 는 호출자 cwd 기준으로 절대화된다(:191, cd "$WORKDIR" 이전) — --workdir 가 다른 곳이어도 찾는다. 상대 --registry-dir "reg dir" 는 종전대로 --workdir 기준이다(기존 의미 유지). 공백 경로 모두 안전.
  • 등록된 prompt 는 바이트 단위로 동일('line1\nline2\n\n', 후행 개행 보존) — registry 가 파일을 직접 읽기 때문. 래퍼의 $PROMPT 변수($(<file))는 후행 개행을 잃지만 그것은 브리프 본문에만 쓰인다(H4).
  • 래퍼는 호출자의 파일을 지우지 않는다(소유권은 호출자). --prompt 와 --prompt-file 동시 → 거부, 없는 파일 → cannot read --prompt-file <절대경로> 거부.
  • 읽는 시점: register 는 래퍼의 첫 단계(:207)이고 래퍼는 잡이 끝날 때까지 블록하므로, 러너·lib.sh·셸 러너가 래퍼 반환 후 지우는 순서는 안전하다.

1-2. 임시 파일의 위치·권한·정리

생성자 위치 권한 정리
러너 제어 루트 .mam/ (self.mam_dir) NamedTemporaryFile → 0600 finally: os.unlink — 성공·실패·OSError(exec 실패) 모두 (:498-502)
lib.sh ${WORKSPACE_ROOT}/.mam/ mktemp → 0600 제출 뒤 rm -f; 쓰기 실패 시 rm -f 후 return 1 (lib.sh:2042)
셸 러너 $MAM_REAL_ROOT/.mam/ mktemp → 0600 래퍼 반환 뒤 rm -f; 쓰기 실패 시 제거 + 원인 로그 + return 1 (:201-205)
create <jobs>/<id>/brief.md (잡 디렉터리, 보존 대상) umask 실패 시 잡 error + exit 1 (:632-637)

H1 (비차단이지만 실제 결함, 재현) — 러너의 쓰기 실패는 부분 파일을 남기고 예외를 던진다

runner.py:472-477 의 with NamedTemporaryFile(...) as fh: fh.write(p_text) 는 try 바깥에 있다. 파일이 만들어진 뒤 write 가 실패하면(ENOSPC·EIO) finally 의 unlink 에 닿지 않는다. 검증 경로의 러너를 in-process 로 띄우고 write 가 OSError(28) 을 내도록 감싼 결과:

RAISED: OSError [Errno 28] No space left on device      ← delegate_job 밖으로 그대로 전파(트레이스백, 126 처리 아님)
leftover prompt files in .mam: ['prompt-loop-…-2ussg8e6.md']  ← 부분 파일 잔존
wrapper called: 0

같은 부류를 PR 은 lib.sh:2042(|| { rm -f "$f"; return 1; })과 run_loop.sh:201-205 에서 고쳤고 S9 로 고정했지만, 러너에는 대응이 없고 테스트(S4/S4b)도 subprocess.run 의 OSError 만 다룬다. 브리프 항목 1 의 "쓰기 실패 시 부분 파일이 남지 않는지"에 대해 러너는 아니오다.

→ 권고: with 블록을 try 안으로 옮기고 except OSError 에서 unlink 후 CompletedProcess(cmd, 126, "", "cannot write the prompt file: …") 를 돌려주면 exec 실패와 같은 "실패 제출" 경로로 합쳐진다(3–4줄). self.mam_dir.mkdir 실패도 같은 블록에서 잡힌다.

2. 실패 처리 (항목 2)

  • exec 실패(OSError → rc 126): _run_cmd 가 CompletedProcess(cmd, 126, "", "cannot start the wrapper: … (prompt N bytes, wrapper …)") 를 돌려주므로 첫 시도·busy 재시도 모두 같은 "실패 제출" 경로로 들어간다. 재시도 실패 시 job id 가 없는 경우에도 이제 log_error 를 남긴다(runner.py:525, 이전에는 무언). failed_submit_jobs 처리는 바뀌지 않았다(id 가 있으면 등록, 없으면 None 반환 → 호출자가 return 1). S4/S4b 가 E2BIG·EAGAIN 으로 고정.
  • 재심·중재 미등록 → rc 1: :1354-1357, :1376-1378. 판정 잡이 등록조차 안 됐는데 "반론 기각"이나 "중재 없음"으로 흘러가던 것을 실패로 바꾼 것이라 맞다.
  • 순응(compliance) 잡 wait_for_job 실패 → rc 1 (:1392-1395): 의도된 강화로 판단한다. 근거는 정규 수정 잡과의 대칭이다 — 일반 correction 잡도 if not correct_jid or not self.wait_for_job(correct_jid): return 1 (:1323-1326) 로 끝난다. 순응 잡은 "반론이 기각됐으니 그대로 고쳐라"는 수정 잡의 변형이므로, 시간 초과·에러로 닫힌 잡을 두고 다음 리뷰 라운드로 가면 바뀌지 않은 트리를 다시 리뷰하게 된다. 루프가 wait_for_job 실패를 통과시키는 자리는 이제 self-review(:1187)뿐이고 그것은 판정 리포트로 따로 걸러진다. S3c 가 세 경로(comply/adjudication/arbitration)의 미등록을 고정한다.

3. MAM_DIFF_MAX_BYTES/MAM_DIFF_MAX_LINES (항목 3)

  • diff_collect.sh:9-11 주석이 "증거 크기의 상한이지 전달 상한이 아니다"를 명시하고, 상한값·잘림 마커·runner.py 의 l15 규칙(잘린 diff 면 diff-only 원격 리뷰어 거부, 로컬 진행)은 변경되지 않았다(runner 의 해당 블록은 diff 밖). S5 가 "옛 사각지대"(131,072 < diff < 200,000 바이트)의 diff 가 통째로 전달됨을 고정한다.
  • H2 (비차단, 문서): loop SKILL.md 에는 이 두 변수도, 잘림 마커의 의미도 적혀 있지 않다(grep 0건). delegate-job SKILL.md 는 argv 상한만 말한다. "전달은 무제한, 증거는 200 KB/4,000 줄에서 잘리며 잘린 diff 는 diff-only 원격 리뷰어에게 증거가 못 된다"는 한 문단이 loop SKILL.md 에 있어야 항목 3 의 "일관 반영"이 완성된다.

4. create_session.sh (항목 4)

  • 잡 등록은 종전대로 lib.sh 의 delegate_submit_job(파일 경유)이고, 그 뒤 <jobs>/<id>/brief.md 에 과제를 쓰고 pane 에는 Task: read the complete task in <절대경로> and follow it. 만 주입한다(:625-646). $pub(delegate_publish_cmd 의 --expect-dispatch-token·--attempt-id)와 started 기반 판정은 손대지 않았다 — #23 계약 유지. S8 이 주입문에 brief 경로는 있고 과제 본문은 없음, --expect-dispatch-token·--event started 가 남아 있음을 단언한다.
  • 경로: ${MAM_JOBS_DIR:-${WORKSPACE_ROOT:-$PWD}/.mam/jobs} 를 절대화하므로 워크트리 creator(제어 루트 바인딩)도 자기 cwd 와 무관하게 읽을 수 있다. 래퍼가 만드는 brief.md 와 같은 자리다.
  • 실패 시 잡 error + JOB_TERMINALIZED=1 + exit 1 → 기존 트랩이 세션을 되돌린다(초기화 실패와 같은 취급).

5. 새 테스트 22건 (항목 5)

  • 실제 경로를 지나는가: S1 은 실제 래퍼(bash multi-agent-mux-delegate-job submit --dry-run)와 실제 registry.py 로 140,000 바이트를 등록해 바이트 단위 동일을 단언한다. S2/S3/S3b/S3c 는 python -m lib_py.loop 를 하위 프로세스로 띄우고 가짜 래퍼가 받은 인자를 검사한다. S7/S7b/S9 는 run_loop.sh(MAM_LOOP_PY=0)를 실제로 돈다. S8 은 mock herdr 위에서 create_session.sh 를 실제로 돈다. mock 으로 우회하는 것은 S4/S4b(subprocess.run 을 OSError 로 대체 — 커널 대신 코드 경로를 검증하는 것이 목적)뿐이다.
  • macOS: MAX_ARG_STRLEN 이 없어 단일 인자 150 KB 가 통과하므로 S1b(음성 대조군)는 linux_only 로 skip 되고, S8 의 strace 검사는 Linux 에서만 붙는다. 즉 이 호스트에서 스위트는 커널 상한이 아니라 코드 경로(파일로 전달됐는가)를 검증한다 — 브리프의 우려 그대로이며, 설계상 옳다. 실제 상한 재현은 Linux CI 몫.
  • 격리: _env() 가 HOME·WORKSPACE_ROOT·DELEGATE_JOB_LOGS_DIR 를 tmp_path 로, 브로커를 127.0.0.1:9 로 고정하고 MAM_JOBS_DIR/MAM_REAL_ROOT 를 제거한다. 실제 .mam·홈 접촉 없음(§6 실측).
  • 수정 없이는 실패하는가 — 확인함: 검증 경로 사본에서 PR 이 바꾼 소스 8파일을 1debc7d 판으로 되돌리고 새 파일을 돌린 결과 18 failed / 3 passed / 1 skipped(S1b). 통과 3건은 정정 전에도 성립하던 것(래퍼 실패 → rc 1 등)이다.

6. 검증 경로에서의 테스트 실행 (항목 5·6)

tests/test_issue36_prompt_transport.py tests/test_issue5_phase1_delegation.py tests/test_issue14_delivery_ack.py
tests/test_loop_cli.py tests/test_issue21_loop_b.py tests/test_issue21_acceptance.py tests/test_issue23_24_startup_path.py
tests/test_issue25_plan_reuse.py
→ 1 failed, 237 passed, 1 skipped in 422.71s (0:07:02)
  • skip 1: S1b(linux_only) — 설계된 것(§5).
  • failed 1: S9 — 코드 결함이 아니라 테스트의 macOS 이식성 문제(H5). S9 는 ulimit -f 1 로 쓰기를 실제로 실패시킨 뒤 세 가지를 단언한다: rc≠0, stderr 에 File too large, 부분 파일 없음 + 원인 문구. 같은 하네스를 scratchpad 에서 직접 돌린 결과 rc=1, 부분 파일 없음, delegate_job_safe: cannot write the prompt file 출력 — 즉 PR 이 고친 동작(제거 + 원인 표시)은 macOS 에서도 성립한다. 실패한 것은 두 번째 단언뿐이다: macOS 의 /bin/bash 3.2 는 printf 의 쓰기 실패에 대해 printf: write error: File too large 문자열을 내지 않는다(Linux bash 5 의 형식). strerror(EFBIG) 자체는 macOS 에서도 "File too large" 이므로 순수하게 bash 버전 차이다. S1b 처럼 linux_only 로 두거나, 그 단언을 빼고 원인 문구 단언만 남기면 된다.
  • 기존 계약 스위트(issue5 phase1 의 registry CLI 스냅샷·help 캡처, issue14, loop_cli, issue21 loop B·acceptance, issue23/24 S7*, issue25)는 전부 통과 — --prompt 하위 호환과 온보딩 계약이 유지된다(항목 4·6).

격리·위생 실측

항목 결과
실제 .mam/jobs / .mam/delegate_job_logs 전후 차이 0 / 차이 0
실제 .mam 의 prompt-* 잔존 0
검증 경로 .mam 의 prompt-* 잔존 0 (러너·셸 러너·lib.sh 의 정리가 실제 실행에서도 성립)
검증 경로 tracked 파일 변화 없음(HEAD 59be1c8), 본 체크아웃 main 1debc7d
실행 시간 새 파일 22건은 34 s 안팎(되돌린 사본 기준 34.5 s); 전체 7 분은 issue14/18 계열의 벽시계 시험 몫

7. 문서 (항목 7)

  • delegate-job SKILL.md :44-48: --prompt-file 의 의미와 argv 상한, "루프는 항상 파일을 쓴다" 명시 ✓. 래퍼 usage(:112-121)도 갱신 ✓.
  • loop SKILL.md :267: 파이프라인에서의 종료 코드(pipefail/PIPESTATUS) 안내 ✓. 그러나 MAM_DIFF_MAX_* 의 새 의미는 없다(H2).
  • .claude/skills: 본 체크아웃의 것은 git 에 추적되지 않는 로컬 심링크(../../.agents/skills/*)라 자동 동기화된다 — 배포 사본 정합 문제 없음.

8. 병합 차단 여부 (항목 8)

버전 락스텝 bump 부재(lib.sh:32 4.7.2, 오케스트레이터가 4.7.3 으로 처리 예정) 외에 병합을 막을 결함은 없다. 다만 H1 은 릴리스 커밋에 함께 넣거나 즉시 후속으로 닫기를 권한다 — 셸 쪽과 같은 부류이고 수정이 세 줄이다.

# 지적 성격 근거
H1 러너의 프롬프트 파일 쓰기 실패 시 부분 파일 잔존 + 예외 전파(126 실패 제출로 합류하지 않음) — 재현 비차단(경계 상황), 후속 권고 1순위 runner.py:472-477 vs :490-502; §1 H1
H2 MAM_DIFF_MAX_* 의 "증거 상한, 전달 무제한" 의미가 loop SKILL.md 에 없음 문서 diff_collect.sh:9-11 에만 있음
H3 래퍼는 빈 --prompt-file 을 받아들여 빈 프롬프트를 등록한다(실측) 사소 multi-agent-mux-delegate-job:195 의 -n "$PROMPT" || -n "$PROMPT_FILE"
H4 래퍼의 $PROMPT="$(<file)" 는 후행 개행을 잃는다 — registry 의 프롬프트(파일 그대로)와 브리프 본문이 그 점에서 다를 수 있음 사소 :193
H5 S9 의 File too large 단언이 macOS bash 3.2 에서 실패(문자열 형식 차이); 실질 단언 2개는 통과 테스트 이식성 §6

9. 교차검증 가이드

RP=/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260924T080014Z-2b3df0/1
PY=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python
git -C "$RP" rev-parse HEAD 'HEAD^{tree}'                          # 59be1c8… / a009c90…
grep -rn -- '--prompt ' "$RP/.agents/skills" | grep -v '\.md:'      # 래퍼 usage/기본값, cli 주석, run_loop.sh 의 함수 호출뿐
# H1 재현: 러너 in-process, NamedTemporaryFile.write 가 OSError(28) → .mam/prompt-* 잔존 + 예외 전파 (§1 H1 의 스크립트)
# S9 하네스 직접 실행(macOS): rc=1, 부분 파일 없음, "cannot write the prompt file" 출력, 단 "File too large" 문자열 없음
# 되돌린 사본: 소스 8파일을 1debc7d 판으로 → tests/test_issue36_prompt_transport.py → 18 failed / 3 passed / 1 skipped

[VERDICT: PASS]