Issue #34: the loop waits for the creator job's sub-jobs, reviewers get the sub-job table as evidence, a run with sub-jobs voids a round whose source changed during review, and a pane caller reaches an agent of another workspace on its own server. VERSIONS.md carries the entry; the merge-side reports (round 1 @ed293dc and round 2 @e80186f with identity sidecars, baseed293dc-> heade80186f, 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.2 KiB
🔍 Review Report — PR #38 델타 리뷰 2회차 (job 895b0370)
- Reviewer:
planner-reviewer-claude-01(role: planner,reviewer) · 2026-09-25 - 대상 델타:
ed293dc..e80186f(2커밋, 3파일+73/−10,git diff --check클린), basemain 7a4a0c2(v4.7.3). - 검증 경로:
/var/folders/.../T/mam-review/loop-20260925T072328Z-2f5853/1. identity 확인:HEAD = e80186f5…4774,HEAD^{tree} = 35018688…7a72— 브리프와 일치. - 1회차 내 판정: PASS + K1~K4 (job
107d435d). 이번에는 N1/R1(cancelled 사유)·R2(원격 마감 문구)·K1(STRAY_RULE)의 정정을 본다. - 안전 규칙 준수: 검증 경로·소스 워크트리·본 체크아웃(
main 7a4a0c2) 모두 수정·커밋·checkout 없음. 실험은 in-process 러너(env -u MAM_VERSION, 임시 등록부·감사 로그)와 되돌린 사본에서만. 실제 herdr·세션·.mam·데몬 조작 없음, push·merge·PR 상태 변경 없음.
결론 요약
세 정정 모두 지적을 닫고, 1회차 판정을 뒤집을 요소는 없다. 종결 사유 12가지를 _subjob_state 에 넣어 분류가 설계대로임을 확인했고(감사 ndjson 경로·.events.log 폴백 양쪽), get_feedback 의 기본 동작은 그대로다. 되돌린 사본에서 T13×2·T14·T6 만 실패한다. 비차단 관찰 1건(L1).
1. N1/R1 — cancelled 사유 (항목 1)
registry.get_feedback(job_id, registry_dir, events=("completed","error"))(registry.py:316-317): 기본값이 종전과 같아 호출자 — 래퍼의get-feedbackCLI 3곳(:556,:565,:593)과registry.py get-feedback서브커맨드(:668) — 는 인자 없이 부르므로 동작 불변. 실측:error "first"뒤cancelled "session_busy"가 온 잡에서 기본 호출은'first',events=(…,"cancelled")는'session_busy'._subjob_state(runner.py:684-702)만cancelled를 포함해 읽는다(:691).- 두 경로 모두 확인:
.events.log에만cancelled가 있는 잡 →NOT-DISPATCHED/target_not_found; 감사 ndjson 에만(.events.log없음,source_event: cancelled) 있는 잡 →NOT-DISPATCHED/session_busy.
2. R2 — 첫 단어 판정 (항목 1)
closed_kind = reason.split(" ", 1)[0](:696) 을 ORCHESTRATOR_CLOSED 와 비교한다. 원격 러너의 실제 문구는 remote.py:352 의 "deadline_timeout (remote runner)" 다. 오분류 여부를 사유 12가지로 실측:
| 상태·사유 | 분류 |
|---|---|
error deadline_timeout (래퍼) |
CLOSED-UNCONFIRMED |
error deadline_timeout (remote runner) |
CLOSED-UNCONFIRMED (R2 정정) |
error agent_blocked |
CLOSED-UNCONFIRMED |
error 에이전트 자신의 사유(tests failed on 3 cases) |
error(실패) — 첫 단어 tests 는 튜플 밖 |
error 사유 없음 |
CLOSED-UNCONFIRMED (기존 규칙) |
cancelled target_not_found / session_busy / rolled back by create_session |
NOT-DISPATCHED (N1 정정; session_busy 는 첫 단어 판정에 걸리지 않고 cancelled 분기로 간다) |
cancelled 사유 없음 / operator abort |
cancelled(실패 집계) |
completed all done |
completed |
첫 단어 판정이 session_busy·target_not_found·rolled 등을 미확인 종결로 오분류하지 않는다. 남는 이론적 여지는 에이전트가 자기 사유를 deadline_timeout/agent_blocked 로 시작하는 문장으로 쓰는 경우뿐인데, 이 두 단어는 오케스트레이터 예약어라 실무에서 겹치지 않는다(L1, 정보).
3. K1 — STRAY_RULE (항목 1)
runner.py:41-42 의 문장은 "귀속되지 않은 열린 잡은 --parent-job 없이 등록된 이 작업의 하위 위임일 수 있다; 그 산출물이 작업 목표에 필요하면 NOT PASS" 이고, _subjobs_block 은 stray 가 있을 때만 stray 줄 뒤에 붙인다(:727-729). SUBJOB_RULE(하위 잡이 있으면 대체 완료 입증이 없는 한 NOT PASS)과 모순되지 않는다 — stray 는 무관한 잡일 수 있으므로 "필요하면" 이라는 조건부가 맞고, 둘 다 같은 방향(미완 → NOT PASS)이다. stray 가 없으면 프롬프트는 1회차와 바이트 동일(if not rows and not stray: return "" 유지).
4. 테스트 (항목 1·2)
4-1. 수정 없이는 실패하는가 — 확인함
검증 경로 사본에서 registry.py·runner.py 만 ed293dc 판으로 되돌리고 -k "t13 or t14 or t6 or t5" 를 돌렸다:
4 failed, 3 passed, 13 deselected in 22.39s
FAILED test_t13_…[target_not_found] ← N1
FAILED test_t13_…[session_busy] ← N1
FAILED test_t14_the_remote_runners_deadline_wording_is_closed_unconfirmed ← R2
FAILED test_t6_an_unattributed_open_job_is_shown_and_waited_for_only_on_request ← K1 (새 단언)
T5/T5b(deadline_timeout 정확 일치·늦은 쓰기 무효화)는 양쪽에서 통과 — 기존 분류가 정정으로 바뀌지 않았다. T13·T14 는 실제 registry.py close + publish_event 로 종결을 만드는 시험이라(mock 아님) 원격 러너의 실제 문구 그대로를 검증한다.
4-2. 검증 경로에서의 실행
tests/test_issue34_job_tree.py tests/test_issue5_phase1_delegation.py tests/test_issue14_delivery_ack.py
tests/test_loop_cli.py tests/test_issue25_plan_reuse.py tests/test_issue21_loop_b.py
→ 158 passed in 271.04s failed 0 · skipped 0
새 파일 20건 전부 통과, registry.py CLI 스냅샷·help 캡처(issue5 phase1) 통과 → get_feedback 시그니처 확장이 CLI 계약을 건드리지 않는다. 실제 .mam/jobs 전후 차이 0, 검증 경로 tracked 파일 변화 없음(HEAD e80186f), 본 체크아웃 main 7a4a0c2.
5. 병합 차단 여부 (항목 2)
1회차 PASS 를 뒤집을 요소는 없다. 델타는 get_feedback 의 기본값 보존 인자 하나, _subjob_state 의 두 줄, STRAY_RULE 한 문장, 회귀 3건이며, 정정 대상만 정확히 실패→통과로 바뀌었다. 버전 락스텝 bump 부재(lib.sh:32 4.7.3, 오케스트레이터가 4.7.4 로 처리 예정) 외에 병합을 막을 결함은 없다. 1회차 K2(MAM_JOB_TREE_MAX 초과 시 대기 생략)·K3(target_not_found 의 rc 5)·K4(폴링마다 전체 스캔)는 이 델타 범위 밖이라 그대로 열려 있다.
| # | 비차단 관찰 | 근거 |
|---|---|---|
| L1 | 첫 단어 판정은 에이전트가 자기 사유를 deadline_timeout/agent_blocked 로 시작해 쓰면 미확인 종결로 본다 — 두 단어는 오케스트레이터 예약어라 실무 충돌은 없다 |
runner.py:696, §2 표 |
6. 교차검증 가이드
RP=/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260925T072328Z-2f5853/1
PY=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python
git -C "$RP" rev-parse HEAD 'HEAD^{tree}' # e80186f… / 3501868…
# in-process (env -u MAM_VERSION): 임시 등록부에 12가지 종결 사유를 .events.log / 감사 ndjson 으로 심고 _subjob_state 호출 (§2 표)
# 되돌린 사본: registry.py·runner.py 를 ed293dc 판으로 → -k "t13 or t14 or t6 or t5" → 4 failed / 3 passed
(cd "$RP" && AGENT_PYTHON_BIN="$PY" PYTHONDONTWRITEBYTECODE=1 "$PY" -m pytest -p no:cacheprovider -q --basetemp=/tmp/bt tests/test_issue34_job_tree.py) # 20 passed
[VERDICT: PASS]