Files
multi-agent-mux/.agents/reports/planner-reviewer-claude-01/report-107d435d.md
T
GodopuandClaude Fable 5.1 22586d2a01 release(v4.7.4): bump framework and eight skills to 4.7.4 in lockstep; archive PR #38 merge-side reviews
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, base ed293dc -> head e80186f, 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-25 16:44:20 +09:00

129 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🔍 Review Report — PR #38 병합 전 독립 리뷰 (job `107d435d`)
- **Reviewer**: `planner-reviewer-claude-01` (role: planner,reviewer) · 2026-09-25
- **대상**: PR #38 `fix/issue-34` **@ `ed293dc`**, base `main 7a4a0c2` (v4.7.3). 6커밋, 11파일 `+924/−16`, `git diff --check` 클린.
- **검증 경로**: `/var/folders/.../T/mam-review/loop-20260925T055502Z-a37cf1/1`. **identity 확인**: `HEAD = ed293dce…67cf`, `HEAD^{tree} = 671b0e59…899c` — 브리프와 일치.
- **안전 규칙 준수**: 검증 경로·소스 워크트리(`multi-agent-mux-wt/pr-38`)·본 체크아웃(`main 7a4a0c2`) 모두 수정·커밋·checkout 없음. 등록부 실험은 scratchpad 임시 등록부에서 `registry.py` 를 직접 호출(래퍼 미사용, 실제 herdr 에 잡 생성 없음), 러너 실험은 in-process(`env -u MAM_VERSION`, 임시 git 저장소), lib.sh 는 함수 추출 + `env -i`. 실제 `.mam`·데몬·세션·herdr 서버 조작 없음, push·merge·PR 상태 변경 없음. 이 pane 의 `HERDR_PANE_ID`/`HERDR_WORKSPACE_ID` 는 **읽기만** 했다.
## 결론 요약
**병합을 막을 결함은 없다.** 부모 기록·자손 대기·리뷰 근거 표·stale 가드·같은 서버 다른 워크스페이스 조회 규칙을 각각 독립 실험으로 확인했고, 새 테스트 17건 중 **16건이 수정 전 소스에서 실패**한다. 비차단 관찰 4건(K1–K4). 그중 K1(귀속되지 않은 열린 잡만 있을 때 판정 규칙 문구가 없음)은 브리프 항목 2·4 의 "충분한 근거" 질문에 직접 닿는다.
## 1. 부모 기록 (항목 1)
| 확인 | 결과 |
|---|---|
| `--parent-job` 검증 | `registry.py register`(`:540-544`) 가 등록부에 없는 부모를 `unknown parent job <id>` + rc 1 로 거부. 래퍼(`:217-226`)는 명시 `--parent-job` 을 그대로 넘겨 **같은 검증**에 걸리게 하고, 기본 `MAM_JOB_ID` 는 이 등록부에 그 레코드가 있을 때만 쓰며 없으면 `WARNING: … registering without a parent job` 후 부모 없이 등록 — 두 층의 규칙이 일치한다 |
| 자기 참조 | `--job-id X --parent-job X` 는 등록 시점에 X 가 없어 거부됨(실측). `update` 에는 `--parent-job` 이 없어 **부모는 등록 후 불변** → 정상 경로로는 순환이 만들어질 수 없다 |
| 순환 방어 | 레코드 파일을 직접 고쳐 `root.parent = grandchild` 순환을 만든 뒤 `descendants_of` 를 돌려도 종료한다(`seen` 집합, 방문 1회): root 기준 `[(child,1),(grandchild,2)]`, grandchild 기준 `[(root,1),(child,2)]` |
| `depth` | 저장하지 않고 BFS 에서 계산(자식 1, 손자 2 — 실측) |
| 비용 | `descendants_of` 는 폴링(3 s)마다 `list_jobs` **전체 스캔** 1회(레코드 수 N 의 JSON 읽기). `_subjobs_block` 은 회차마다 root 수만큼 스캔 + `unattributed_open_jobs` 1회. 수백 건이면 문제없고 수천 건이면 3 s 폴링당 수천 파일 읽기 — 등록부 정리 정책이 없는 오래된 제어 평면에서 체감될 수 있다(K4) |
| `MAM_JOB_TREE_MAX`(50) | 넘으면 `log_error` 후 **기다리지 않고** 리뷰로 간다. 안전한 쪽인가: 열린 자손은 `[하위 위임 잡]` 표에 OPEN 으로 나오고 SUBJOB_RULE 이 NOT PASS 를 지시하므로 "조용히 통과"는 아니다. 다만 51개 이상의 행이 프롬프트에 붙는다 |
## 2. 부모 ID 전달 (항목 2)
- 로컬 에이전트: 래퍼가 주입문 끝(`_instructions`, test18 문구 보존)과 `brief.md` 의 `Parent for sub-delegations` 줄(`:251`, `:427`), create 의 온보딩 주입문(`create_session.sh:646`)으로 알린다. 에이전트가 붙이지 않으면 추적이 빠지는 것은 사실이고, 안전망은 두 가지다: (a) `[귀속되지 않은 열린 잡]` 줄 — `unattributed_open_jobs` 가 "부모 없음 + pending/running + 이 루프가 만든 잡 제외 + creator 잡 등록 이후" 로 고른다(실측: 루프 자신의 뒤이은 잡은 제외, 남의 root 잡은 표시); (b) `--wait-open-jobs` 로 그 잡들까지 대기(기본 off — 같은 제어 평면의 무관한 잡이 루프를 잡을 수 있으므로; 문서와 일치).
- **K1 (비차단, 근거 충분성)**: 하위 잡이 하나도 없고 귀속되지 않은 열린 잡만 있을 때 블록은 `[하위 위임 잡] 없음 … [귀속되지 않은 열린 잡]: <id>(…)` 만 담고 **SUBJOB_RULE 을 붙이지 않는다**(`_subjobs_block` 의 `if not rows:` 분기 — 실측: `has rule: False`). SUBJOB_RULE 자체도 "하위 잡"만 말하고 귀속되지 않은 잡을 어떻게 볼지는 말하지 않는다. 즉 "creator 가 `--parent-job` 을 빠뜨린" 바로 그 경우에 리뷰어는 사실은 받지만 지시는 받지 못한다. 한 문장("귀속되지 않은 열린 잡이 이 작업의 하위 위임으로 보이면 OPEN 하위 잡과 같이 취급")을 stray 줄 뒤에 붙이면 닫힌다.
- 원격 러너: `MAM_JOB_ID` 가 자동 부모가 되지만 원격 호스트의 등록부에는 그 잡이 없을 수 있어 "있을 때만" 규칙이 맞다(래퍼 주석과 일치).
## 3. 자손 대기 (항목 3)
- `wait_for_job_tree` 는 실행(`:1291`)·수정(`:1520`)·순응(`:1591`) 세 creator 잡 모두에 걸리고, `round_roots` 에 누적된다.
- `MAM_CHILD_WAIT_S`(3900) 의 기준점은 `wait_for_descendants` 진입 시각 = **creator 잡 종결 시점**(문서와 일치). 초과 시 `log_warn` 후 False 를 돌려주지만 `wait_for_job_tree` 는 root 의 결과만 반환하므로 루프는 리뷰로 진행하고 열린 잡은 표에 OPEN 으로 남는다(설계대로).
- `interrupted`: 시그널 핸들러가 체크포인트 저장 후 `sys.exit(128+signum)` 하므로(`_handle_signal`) 폴링 루프의 `if self.interrupted: return False` 는 실제로는 닿지 않는 방어 코드다 — 무해.
- 분류(`_subjob_state`)와 `get_feedback` 의 실제 값: 래퍼는 마감 시 `close --request deadline_timeout` 뒤 `publish_event --event error --detail deadline_timeout`(inject 단계면 `agent_blocked`)을 남기고(`:928-930`), preflight/idle 실패는 `cancelled --detail session_busy` 또는 새 `target_not_found`(`:896`, `:952`), create 롤백은 `cancelled "rolled back by create_session"` 이다. `get_feedback` 은 마지막 `completed/error`(감사 로그) 또는 events.log 의 detail 을 돌려주므로 `ORCHESTRATOR_CLOSED` 튜플·`startswith("rolled back")`·`session_busy|target_not_found` 가 정확히 그 문자열들과 맞는다. 사유 없는 `error` 를 CLOSED-UNCONFIRMED 로 보는 규칙은 "에이전트 자신의 보고가 아니다" 는 뜻에서 보수적이며 옳다.
## 4. 리뷰어 근거 (항목 4)
in-process 실측(`_subjobs_block`):
| 상황 | 결과 |
|---|---|
| 하위 잡 없음·stray 없음 | **`''`** — 프롬프트 바이트 동일 |
| 이 루프가 뒤에 만든 잡(`created_jobs`)만 존재 | `''` — 리뷰어 자신의 잡·앞 회차 리뷰 잡은 stray 로 오인되지 않는다(회차의 블록은 리뷰 잡 위임 **전에** 계산된다) |
| 남의 root 잡(부모 없음, creator 잡 이후 등록) | 블록 생성, stray 줄에 id 표시, **규칙 문구 없음**(K1) |
| pending 자식 1개 | 표(`\| OPEN \| - \| 1 \|`) + 집계 + SUBJOB_RULE |
`descendants_of` 가 `depth` 를 주고 `_subjob_state` 가 상태·사유를 주므로 표의 열은 정확하다. 표는 앞 회차 creator 잡의 자손까지 매 회차 다시 읽는다(`round_roots` 누적, T9).
## 5. stale 가드 — 옵션 없는 루프 (항목 5)
`_fingerprint`(`runner.py:954-972`)는 `head` 와, `.mam`(`mam_dir`)·`jobs_dir`·감사 로그 디렉터리(`DELEGATE_JOB_LOGS_DIR`, 상대면 `workspace_root` 기준으로 결합)를 `git add -A -- . :(exclude)<rel>` 로 뺀 tree 다. in-process 실측(임시 git 저장소, **`.gitignore` 없음** — 즉 `.mam` 이 무시되지 않는 최악 조건):
| 상황 | 결과 |
|---|---|
| `DELEGATE_JOB_LOGS_DIR=.mam/delegate_job_logs`(상대) 로 감사 로그·`jobs/*.json` 을 새로 쓴 뒤 | 지문 **불변** — 제어 평면 쓰기는 제외된다 |
| 소스에 untracked 파일 1개 추가 | 지문 **변경** |
| 감사 로그 디렉터리가 소스 **밖**(절대경로) | `relative_to` 가 `ValueError` → 제외 목록에서 빠지고 지문 계산은 정상 |
- **켜지는 조건**: `light = self._fingerprint()` 는 `not identity and subjobs`(`:1350-1352`) — 하위 잡 표나 stray 줄이 있을 때만이다(1d4f338). 하위 위임 없는 루프는 옛 동작 그대로라 `test_tier4_e2e.py::test_e2e_scenario5` 류(소스에 매 잡 바뀌는 untracked 가 있는 경우)가 다시 살아난다 — 이번 실행에서 tier4 포함 통과(§7).
- **적용 지점**: 셀프 리뷰 뒤(`:1368`), 리뷰 판정 수집 뒤(`:1463`, identity 모드의 B7 과 `elif` 로 배타), 승격 직전(`:1595`, identity 모드의 B10 은 `:1613` 로 별도). identity 모드에서는 `light` 가 `None` 으로 남아 **이중 적용이 없다**.
- 지문을 만들 수 없으면(예: dirty 서브모듈) 경고 1회 후 가드 off — 옵션 없는 모드의 기존 동작으로 되돌아가는 것이라 수용 가능하다.
## 6. 같은 서버 다른 워크스페이스 위임 (항목 6)
`_herdr_scope_is_pane_inherited`(`lib.sh:1464-1473`)와 `mam_herdr` 의 `elif`(`:1485-1486`)를 함수 추출 + `env -i` 로 10가지 조합에 돌렸다(전부 설계와 일치):
| 호출 | 환경 | 결과 |
|---|---|---|
| `agent`/`has-session`/`kill-session`(`-L` 접두 포함) | `HERDR_WORKSPACE_ID == HERDR_PANE_ID` 의 워크스페이스, 같은 서버 | **WS id 만 제거**(pane·tab id 유지) |
| `new-session`, `pane split` | 동일 | **유지** — pane 배치·워크스페이스 패킹(#7) 불변 |
| `agent` | WS id ≠ pane 의 워크스페이스(어긋난 스코프) | 유지(의도된 스코프) |
| `agent` | pane id 없음(pane 밖 명시 스코프) | 유지 |
| `agent` | 기본 서버 pane → 이름 있는 서버 | **CROSS-all3** — #23 규칙이 `if` 로 먼저 잡아 세 id 모두 제거, 이 규칙은 `elif` 라 닿지 않는다(충돌 없음) |
| `agent` | 대상 미지정(앰비언트) | WS id 만 제거(같은 서버이므로 같은 취급) |
- 이 pane 의 실제 값도 규칙의 전제(`HERDR_PANE_ID = "<ws>:<pane>"`, `HERDR_WORKSPACE_ID = <ws>`)와 맞았다(값은 읽기만).
- 이름 유일성 근거(herdr `--skill` 문구)는 코드 주석에 인용돼 있고, T10/T11 이 두 방향을 고정한다.
- `target_not_found`: preflight 의 `has-session` 실패가 `PREFLIGHT_REASON` 을 세우고(`:952`) `finalize_unfinished` 가 `cancelled --detail target_not_found` 로 닫는다(`:896`). **rc 는 종전과 같은 5** 다(T12 가 `returncode == 5` 를 고정) — 즉 이벤트 detail 은 정확해졌지만 종료 코드는 "busy" 와 같아서, 루프의 busy 1회 재시도(`MAM_SESSION_BUSY_RETRY_S`)가 없는 대상에도 한 번 더 돌아간다(K3).
## 7. 새 테스트 (항목 7)
- **실제 경로**: T1·T12 는 실제 래퍼(`--agent human`, 도달 불가 브로커)와 `registry.py` 로 등록·종결한다. T3~T9 는 `python -m lib_py.loop` 를 하위 프로세스로 띄우고 가짜 래퍼가 **실제 래퍼**(`FAKE_REAL_WRAPPER`)로 하위 잡을 등록해 자손 대기·표·가드를 통과시킨다. T10~T12 는 mock herdr 위의 샌드박스. `HOME`·`WORKSPACE_ROOT`·`DELEGATE_JOB_LOGS_DIR` 는 `tmp_path`.
- **수정 없이는 실패하는가 — 확인함**: 검증 경로 사본에서 PR 이 바꾼 소스 6파일을 `7a4a0c2` 판으로 되돌리고 새 파일을 돌린 결과 **16 failed / 1 passed**(16.7 s). 통과 1건은 T11("호출자가 고른 워크스페이스 id 는 여전히 스코프") — 정정 전에도 성립해야 하는 불변 조건이다.
- **검증 경로에서의 실행** (9파일: 새 파일 + issue5 phase1·tier4 e2e·issue21 loop B/acceptance·issue23/24·loop_cli·issue25·herdr shim contract):
```
→ 1 failed, 243 passed in 1460.19s (0:24:20)
FAILED tests/test_issue23_24_startup_path.py::test_s7c_a_started_that_was_followed_by_a_terminal_status_still_counts
```
- 새 파일 17건 **전부 통과**, `test_tier4_e2e.py::test_e2e_scenario5`(브리프 항목 5 의 회귀) **통과**(873 s — 이 호스트에서 다른 스위트가 동시에 돌아 load avg 2.5 였다), registry CLI 스냅샷·help 캡처(issue5 phase1) 통과 → `--parent-job` 추가가 기존 CLI 계약을 깨지 않는다.
- **실패 1건은 부하 flake** 로 판정한다: s7c 는 create/registry 의 `started` 증거 시험이며 PR 은 그 경로를 건드리지 않았다(create 는 주입문에 문장 하나 추가뿐). 합동 실행에서 40 s 걸려 `_seed_job` 단계(`--ack-timeout 2`)에서 실패했고, **단독 재실행에서는 s7b 와 함께 2 passed in 10.8 s** 로 통과했다.
- **격리**: 실제 `.mam/jobs`·`.mam/delegate_job_logs` 전후 **차이 0 / 차이 0**. 검증 경로 tracked 파일 변화 없음(HEAD `ed293dc`), 본 체크아웃 `main 7a4a0c2`.
- **실행 시간**: 새 파일 자체는 되돌린 사본 기준 17 s 안팎이고, 합동 실행의 24 분은 tier4 e2e(약 17 분)와 호스트 부하 몫이다.
## 8. 문서 (항목 8)
- loop SKILL.md `:214`(`--wait-open-jobs` 표), `:221`(하위 위임 잡: 대기 범위·`MAM_CHILD_WAIT_S` 기준점·`MAM_JOB_TREE_MAX`·표/규칙·회차 누적), `:223`(stale 가드) — 코드와 일치한다.
- delegate-job SKILL.md `:49-53`·Best Practices, MULTI_AGENT_RULES(ko/en) `:154` — `--parent-job` 의무와 기본값·거부 규칙이 서술돼 있다.
- 래퍼 usage 와 `brief.md` 의 `Parent for sub-delegations` 줄, create 온보딩 주입문 — 모두 같은 문장이다.
## 9. 병합 차단 여부 (항목 9)
버전 락스텝 bump 부재(`lib.sh:32` `4.7.3`, 오케스트레이터가 4.7.4 로 처리 예정) 외에 **병합을 막을 결함은 없다**.
| # | 비차단 관찰 | 근거 / 권고 |
|---|---|---|
| K1 | 귀속되지 않은 열린 잡**만** 있을 때 블록에 SUBJOB_RULE 이 없고, 규칙 문구도 그 잡을 언급하지 않는다 — `--parent-job` 을 빠뜨린 바로 그 경우에 리뷰어가 지시 없이 사실만 받는다 | `runner.py:697-724` 의 `if not rows:` 분기, 실측 `has rule: False`. stray 줄 뒤에 한 문장 권고 |
| K2 | `MAM_JOB_TREE_MAX` 초과 시 대기를 건너뛰고 리뷰로 간다 — 표의 OPEN 행과 규칙이 NOT PASS 를 유도하므로 조용한 통과는 아니지만, 50+ 행이 프롬프트에 붙는다 | `:646-654`. 정보 |
| K3 | `target_not_found` 는 이벤트 detail 만 바뀌고 래퍼 rc 는 `session_busy` 와 같은 **5** 라 루프의 busy 1회 재시도가 없는 대상에도 돈다 | `:896`, `:952`, T12 `returncode == 5`. rc 분리 권고(후속) |
| K4 | `descendants_of`/`unattributed_open_jobs` 는 폴링(3 s)마다 등록부 전체 JSON 을 읽는다 — 수천 건 누적된 제어 평면에서 체감 가능 | `registry.py:259-282`. 등록부 정리 정책 또는 인덱스가 생기기 전까지는 정보 |
## 10. 교차검증 가이드
```bash
RP=/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260925T055502Z-a37cf1/1
PY=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python
git -C "$RP" rev-parse HEAD 'HEAD^{tree}' # ed293dc… / 671b0e5…
# 등록부: 부모 기록·list --parent·미지 부모/자기 참조 거부 (임시 등록부, registry.py 직접)
# descendants_of: 손자 depth 2, 레코드 파일로 순환을 만들어도 종료
# 러너 in-process (env -u MAM_VERSION): _subjobs_block '' / stray / child 표, _fingerprint 의 .mam 제외
# lib.sh: _herdr_scope_is_pane_inherited 10케이스 (§6)
# 되돌린 사본: 소스 6파일을 7a4a0c2 판으로 → tests/test_issue34_job_tree.py → 16 failed / 1 passed
(cd "$RP" && AGENT_PYTHON_BIN="$PY" PYTHONDONTWRITEBYTECODE=1 "$PY" -m pytest -p no:cacheprovider -q --basetemp=/tmp/bt \
tests/test_issue23_24_startup_path.py::test_s7c_a_started_that_was_followed_by_a_terminal_status_still_counts) # 단독 통과
```
[VERDICT: PASS]