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, 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
This commit is contained in:
1 parent
e80186f560
commit
22586d2a01
28 files changed
+825
-26
No files matched your search
@@ -0,0 +1,128 @@
|
||||
# 🔍 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]
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"loop_id": "loop-20260925T072328Z-2f5853",
|
||||
"base_sha": "ed293dcef4c346e0050a31cb46812bcdcf1d67cf",
|
||||
"head_sha": "e80186f56064eaf93a2f32c48d31d84816894774",
|
||||
"tree_sha": "3501868872d828894f031911b8ca31b01ce77a72",
|
||||
"review_commit": "e80186f56064eaf93a2f32c48d31d84816894774",
|
||||
"reviewer": "planner-reviewer-claude-01",
|
||||
"job_id": "895b0370",
|
||||
"verdict": "PASS",
|
||||
"transport": "herdr",
|
||||
"evidence": "review-tree",
|
||||
"isolation": "worktree",
|
||||
"verified_at": "2026-09-25T07:43:24Z"
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# 🔍 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` 클린), base `main 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-feedback` CLI 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. 교차검증 가이드
|
||||
|
||||
```bash
|
||||
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]
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"loop_id": "loop-20260925T072328Z-2f5853",
|
||||
"base_sha": "ed293dcef4c346e0050a31cb46812bcdcf1d67cf",
|
||||
"head_sha": "e80186f56064eaf93a2f32c48d31d84816894774",
|
||||
"tree_sha": "3501868872d828894f031911b8ca31b01ce77a72",
|
||||
"review_commit": "e80186f56064eaf93a2f32c48d31d84816894774",
|
||||
"reviewer": "planner-reviewer-codex-01",
|
||||
"job_id": "bc07e3f0",
|
||||
"verdict": "PASS",
|
||||
"transport": "herdr",
|
||||
"evidence": "review-tree",
|
||||
"isolation": "worktree",
|
||||
"verified_at": "2026-09-25T07:43:24Z"
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
# PR #38 델타 리뷰 2회차 — bc07e3f0
|
||||
|
||||
**PASS.** 이전 리뷰의 차단 지적 R1/N1 및 R2가 해소됐다. K1 안내문 보완도 확인했으며, 이번 델타에서 새로운 병합 차단 결함을 발견하지 못했다.
|
||||
|
||||
## 대상 및 범위
|
||||
|
||||
- base: `ed293dcef4c346e0050a31cb46812bcdcf1d67cf`
|
||||
- head: `e80186f56064eaf93a2f32c48d31d84816894774`
|
||||
- tree: `3501868872d828894f031911b8ca31b01ce77a72`
|
||||
- 검증 경로: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260925T072328Z-2f5853/1`
|
||||
- `git diff ed293dc...HEAD`: registry.py, runner.py, test_issue34_job_tree.py 세 파일, +73/-10.
|
||||
- 검증 전후 HEAD/tree 일치 및 빈 git status 확인. 소스/제품 파일 수정, 커밋, 실제 herdr 세션 조작은 하지 않았다. 임시 사본과 격리된 registry/audit 자료만 사용했다.
|
||||
- 이번에는 델타 및 직접 영향 범위를 검증했다. 이전 회차의 광범위한 287개 항목을 현재 HEAD에서 전부 재실행했다고 주장하지 않는다. 예정된 4.7.4 버전 bump 부재는 판정에서 제외했다.
|
||||
|
||||
## 지적별 재판정
|
||||
|
||||
### R1/N1 — 취소 사유 누락: 해소
|
||||
|
||||
`.agents/skills/lib_py/delegation/registry.py:316`의 `get_feedback(..., events=...)` 기본값은 기존 completed/error로 유지된다. audit의 source_event/event와 로컬 events.log 모두 지정한 이벤트 집합을 사용한다. `.agents/skills/lib_py/loop/runner.py:690`의 하위 잡 조회만 cancelled를 추가한다.
|
||||
|
||||
실제 래퍼·등록부·발행기를 사용하는 T13 두 사례가 통과했다. 별도 탐침에서도 다음을 확인했다.
|
||||
|
||||
- cancelled + session_busy → NOT-DISPATCHED / session_busy
|
||||
- cancelled + target_not_found → NOT-DISPATCHED / target_not_found
|
||||
- cancelled + rolled back by create_session → NOT-DISPATCHED / 원래 사유
|
||||
|
||||
감사 로그 event 형식, 감사 로그 source_event 형식, 감사 로그가 없는 .events.log 폴백을 각각 분리해 검사했다. 이전 error 다음에 cancelled가 있는 자료에서 기본 API와 get-feedback CLI는 이전 error detail을 유지하고, 명시적으로 cancelled를 포함한 호출만 취소 detail을 읽었다. 래퍼의 기존 get-feedback CLI 호출에도 기본 동작 변경이 없다.
|
||||
|
||||
### R2 — 원격 deadline 사유 오분류: 해소
|
||||
|
||||
`.agents/skills/lib_py/loop/runner.py:695`의 첫 단어 판정이 실제 `deadline_timeout (remote runner)`를 CLOSED-UNCONFIRMED로 분류한다. T14가 통과했으며, 별도로 실제 `remote.handle_dispatch(..., exec_cmd=None)`에 timeout_sec=1을 주어 마감을 발생시켰다.
|
||||
|
||||
이 탐침은 임시 registry/audit와 127.0.0.1:9 브로커를 사용했다. heartbeat 타이머 생성만 생략하고 실제 started/error 발행 및 로컬 기록 경로를 사용했다. 결과는 다음과 같다.
|
||||
|
||||
```text
|
||||
remote runner rc: 2
|
||||
record: error
|
||||
reason: deadline_timeout (remote runner)
|
||||
classification: CLOSED-UNCONFIRMED
|
||||
review table: 미확인 종결 1
|
||||
warning: its agent may still be writing
|
||||
```
|
||||
|
||||
브로커 연결 거절은 의도한 격리 조건이며, 로컬 기록과 분류는 정상 검증됐다.
|
||||
|
||||
deadline_timeout, agent_blocked 및 설명이 붙은 사유는 미확인 종결로 분류된다. session_busy, target_not_found, simulated, deadline_timeout_recovered 같은 다른 error 사유는 일반 error로 남는다. completed는 사유가 deadline_timeout이어도 completed이고, running은 OPEN이다. 따라서 부분 문자열 포함만으로 무관한 사유를 마감으로 분류하는 변경은 아니다.
|
||||
|
||||
### K1 — 귀속 불명 잡 판정 안내: 해소
|
||||
|
||||
`.agents/skills/lib_py/loop/runner.py:41`의 STRAY_RULE 및 `:726`의 조건부 추가를 확인했다. T6와 별도 프롬프트 탐침에서 다음이 성립했다.
|
||||
|
||||
- 자손과 귀속 불명 열린 잡이 모두 없으면 추가 블록은 빈 문자열이다.
|
||||
- 완료된 연결 자손만 있을 때 STRAY_RULE은 붙지 않는다.
|
||||
- 귀속 불명 열린 잡이 있으면 STRAY_RULE이 정확히 한 번 붙는다.
|
||||
- 그 잡이 종결되면 STRAY_RULE은 사라진다.
|
||||
|
||||
안내는 해당 잡의 산출물이 작업 목표에 필요한 경우 NOT PASS를 요구한다. 모든 무관한 잡을 자동으로 실패로 간주하지 않으며, 연결된 자손에 대한 SUBJOB_RULE과 모순되지 않는다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
브리프의 Python 및 검증 경로를 사용했다. 모든 pytest 실행은 `AGENT_PYTHON_BIN=<main venv Python> PYTHONDONTWRITEBYTECODE=1 python -m pytest -q -p no:cacheprovider --basetemp=<검증 경로 밖 임시 경로>` 형태였다.
|
||||
|
||||
| 검증 | 결과 |
|
||||
|---|---|
|
||||
| test_issue34_job_tree.py 전체 20개 + registry CLI 계약·shim help·패키지 import 경계 3개 | **23 passed / 84.42s** |
|
||||
| audit event / audit source_event / .events.log 폴백별 상태 분류 14개 | **42개 분류 단언 통과** |
|
||||
| 위 세 저장 형식별 기본 API·확장 API·기본 CLI | **9개 호환성 단언 통과** |
|
||||
| 실제 원격 마감 producer → feedback → 분류/표/경고 | 통과 |
|
||||
| 빈 프롬프트 및 STRAY_RULE 조건부 노출 | 통과 |
|
||||
| 변경 Python AST 구문 검사, git diff --check | 통과 |
|
||||
|
||||
선택한 기존 계약 테스트:
|
||||
|
||||
- `test_registry_cli_contract_snapshot`
|
||||
- `test_shim_help_output_matches_pre_move_capture`
|
||||
- `test_delegation_package_imports_no_sibling_lib_py_modules`
|
||||
|
||||
### 역검증
|
||||
|
||||
임시 skills 사본에서 registry.py와 runner.py만 ed293dc 버전으로 되돌렸다. 현행 테스트를 그 사본에 연결하면 다음 동작 단언들이 각각 실패했다.
|
||||
|
||||
| 사례 | 실패 근거 |
|
||||
|---|---|
|
||||
| T13[target_not_found] | test_issue34_job_tree.py:251 — cancelled / - 이므로 NOT-DISPATCHED + 사유 단언 실패 |
|
||||
| T13[session_busy] | 같은 줄 — 동일한 취소 사유 누락 |
|
||||
| T14 | :270 — 원격 마감이 error로 남아 CLOSED-UNCONFIRMED 단언 실패 |
|
||||
| T6 | :509 — STRAY_RULE 문장 부재 |
|
||||
|
||||
최초 역검증 pytest 결과는 4 failed, 16 deselected, 1 teardown error / 9.82s였다. 추가 teardown 오류는 임시 사본의 non-delegation 모듈을 불러온 것을 기존 provenance guard가 검출한 것으로, 위 네 동작 실패와 별개다. 이를 구분하기 위해 동일한 네 테스트 함수를 독립 Python 프로세스에서 각 임시 경로와 MonkeyPatch context로 직접 실행했다. 네 경우 모두 정확히 위 동작 단언 줄에서 실패했고 다른 오류 없이 역검증을 마쳤다. 현행 HEAD의 대응 네 테스트는 앞선 정상 pytest 실행에서 모두 통과했다.
|
||||
|
||||
보조 자료:
|
||||
|
||||
- 저장 형식/분류 탐침: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/bc07e3f0-probes-1m01abus/`
|
||||
- 원격 producer/프롬프트 탐침: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/bc07e3f0-integration-siocsre6/`
|
||||
- 정정 전 skills 사본: `/tmp/bc07e3f0-reverse.0e3SXO/skills/`
|
||||
|
||||
이전 R1/R2에 따른 NOT PASS를 현재 리뷰 대상 e80186f에서는 철회한다. 추가 재계획 요청은 없다.
|
||||
|
||||
[VERDICT: PASS]
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# PR #38 독립 리뷰 — c1dec920
|
||||
|
||||
판정: **NOT PASS**. 새 하위 잡 분류 코드가 실제 종결 이벤트 생산자/조회자의 계약과 맞지 않는 P2 결함 두 건이 재현됐다. 기존 테스트의 실패와 구분하여 아래에 기록한다. 설계 재계획이 필요한 사안은 아니며, 종결 사유 조회·정규화와 회귀 테스트 추가로 수정할 수 있다.
|
||||
|
||||
## 검증 대상
|
||||
|
||||
- 역할: planner,reviewer / 이번 잡은 Reviewer.
|
||||
- base: `7a4a0c209195c20719a50e28398416ccd19004e2`
|
||||
- head: `ed293dcef4c346e0050a31cb46812bcdcf1d67cf`
|
||||
- tree: `671b0e59eef915f89d1dce63e9c0796db132899c`
|
||||
- 검증 경로: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260925T055502Z-a37cf1/1`
|
||||
- 아래 파일:라인은 이 HEAD 기준이다. 검증 전후 HEAD/tree가 일치하고 `git status --short`는 비어 있다. 제품 코드 수정, 커밋, 실제 herdr 세션 조작은 하지 않았다. 탐침·수정 방향 검증·역검증은 임시 자료 또는 일회성 Python 프로세스에서 수행했다.
|
||||
|
||||
## R1 — P2: 취소 이벤트의 사유를 읽지 못해 NOT-DISPATCHED 분기가 작동하지 않음
|
||||
|
||||
**근거**
|
||||
|
||||
- `.agents/skills/lib_py/loop/runner.py:687`: `registry.get_feedback()`에서 종결 사유를 가져온다.
|
||||
- 같은 파일 `:693`: cancelled + session_busy/target_not_found/rolled back 사유일 때만 NOT-DISPATCHED로 분류한다.
|
||||
- `.agents/skills/lib_py/delegation/registry.py:325`, `:327`, `:342`: audit 및 로컬 events.log 경로 모두 completed/error만 읽고 cancelled는 제외한다.
|
||||
- 실제 생산자는 `.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job:926`에서 cancelled를 발행한다. create rollback도 `.agents/skills/multi-agent-mux-create/scripts/create_session.sh:340`에서 cancelled + `rolled back by create_session`을 발행한다.
|
||||
|
||||
**재현 절차**
|
||||
|
||||
1. 임시 HOME/WORKSPACE_ROOT/registry/audit 경로를 만들고 브로커를 127.0.0.1:9로 설정한다.
|
||||
2. 실제 delegate-job 래퍼의 `submit --agent human --agent-session herdr:img --dry-run --registry-dir <tmp-reg> --prompt probe`로 부모를 등록한다. 같은 명령에 `--parent-job <parent>`를 추가해 자식을 각각 등록한다.
|
||||
3. 자식 레코드의 dispatch token과 attempt를 사용해 실제 `publish_event.py --registry-dir <tmp-reg> --job <child> --expect-dispatch-token <token> --attempt-id 1 --event cancelled --detail <reason>`를 실행한다.
|
||||
4. 같은 registry/audit 환경에서 `LoopRunner._subjob_state(registry.load_job(child, registry_dir))` 및 `_subjobs_block([parent])`를 읽는다.
|
||||
|
||||
| 발행한 detail | 기대 상태·사유 | 실제 상태·사유 |
|
||||
|---|---|---|
|
||||
| session_busy | NOT-DISPATCHED / session_busy | cancelled / - |
|
||||
| target_not_found | NOT-DISPATCHED / target_not_found | cancelled / - |
|
||||
| rolled back by create_session | NOT-DISPATCHED / rolled back by create_session | cancelled / - |
|
||||
|
||||
세 경우 모두 레코드는 cancelled이고 events.log에 정확한 detail이 남아 있다. 브로커 연결 거절로 publisher rc=2지만 로컬 종결·로그 기록은 완료됐다. `get_feedback()`만 빈 문자열을 반환한다. 리뷰 표에서는 세 사유가 모두 사라진다.
|
||||
|
||||
**영향 및 수정 방향**
|
||||
|
||||
리뷰어가 실제 실행 후 실패와 애초에 전달되지 않은 잡을 구분할 수 없다. 현재는 둘 다 실패 건수에 포함되므로 이 문제만으로 자동 PASS가 되는 것은 아니다. 그러나 이번 PR이 명시한 상태/사유 근거 제공 계약을 충족하지 못한다.
|
||||
|
||||
종결 사유 조회가 cancelled도 처리하도록 하거나, 기존 completed/error feedback 계약을 유지해야 한다면 세 terminal 이벤트를 다루는 별도 조회 함수를 새 소비자에서 사용해야 한다. audit와 로컬 events.log fallback을 모두 검사하는 회귀 테스트를 추가한다. 새 T12는 cancelled 이벤트의 존재만 검사하고 그 이벤트가 리뷰 표에서 어떻게 분류되는지는 검사하지 않는다.
|
||||
|
||||
임시 Python 프로세스에서 `get_feedback`의 세 이벤트 필터에 cancelled를 포함시킨 뒤 같은 저장 레코드 세 건이 모두 기대한 NOT-DISPATCHED + 원래 사유로 분류됨을 확인했다. 소스 파일은 변경하지 않았다.
|
||||
|
||||
재현 자료: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/c1dec920-wrapper-probe-_555g31h/review-block.md` 및 인접 `reg/`, `.mam-logs/`.
|
||||
|
||||
## R2 — P2: 원격 러너의 실제 deadline 사유를 CLOSED-UNCONFIRMED로 인식하지 않음
|
||||
|
||||
**근거**
|
||||
|
||||
- `.agents/skills/lib_py/loop/runner.py:36`: 허용 사유는 정확히 deadline_timeout/agent_blocked 두 문자열이다.
|
||||
- 같은 파일 `:690`: 문자열 완전 일치로 미확인 종결을 판단한다.
|
||||
- `.agents/skills/lib_py/delegation/remote.py:352`: 실행 명령 없이 외부 에이전트의 종료를 기다리는 원격 러너는 제한 시간이 지나면 `deadline_timeout (remote runner)`라는 detail로 error를 발행한다. 외부 에이전트를 중단시키는 경로는 아니다.
|
||||
|
||||
**재현 절차**
|
||||
|
||||
1. 임시 registry/audit와 127.0.0.1:9 브로커 환경에서 UUID4 job_id/dispatch_token, attempt_id=1, timeout_sec=1인 dispatch를 만든다.
|
||||
2. 실제 `remote.handle_dispatch(dispatch, registry_dir, "probe", None)`를 실행한다. 테스트에서는 heartbeat 타이머 생성만 생략하도록 `remote._publish`를 감싸고, started/error 발행·로컬 기록은 실제 producer를 사용한다. 실제 에이전트는 실행하지 않는다.
|
||||
3. 종료 레코드를 `LoopRunner._subjob_state()`에 전달한다.
|
||||
|
||||
실측:
|
||||
|
||||
```text
|
||||
runner rc: 2
|
||||
record status: error
|
||||
get_feedback: deadline_timeout (remote runner)
|
||||
expected: CLOSED-UNCONFIRMED / deadline_timeout (remote runner)
|
||||
actual: error / deadline_timeout (remote runner)
|
||||
```
|
||||
|
||||
**영향 및 수정 방향**
|
||||
|
||||
이 원격 마감은 에이전트가 계속 작업할 수 있는 미확인 종결인데, 일반 error로 집계되어 미확인 종결 수와 해당 경고가 누락된다. error도 실패 규칙의 대상이고 하위 잡이 있는 경우 지문 가드는 켜지므로, 이 사례만으로 거짓 PASS가 발생했다고 주장하지는 않는다.
|
||||
|
||||
실제 생산자의 사유를 canonical reason으로 정규화하거나, 알려진 원격 마감 문자열을 분류기에 포함시킨다. 임의의 모든 error를 마감으로 취급해서는 안 된다. 실제 `handle_dispatch(..., exec_cmd=None)`의 마감 결과를 소비하는 회귀 테스트가 필요하다. 일회성 Python 프로세스에서 이 정확한 문자열을 인식하게 했을 때 기대한 CLOSED-UNCONFIRMED가 됨을 확인했다.
|
||||
|
||||
실제 producer 탐침 자료: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/c1dec920-extra-uj1j3nhp/`.
|
||||
|
||||
## 검증 결과 및 범위
|
||||
|
||||
실행 인터프리터는 브리프의 main venv Python이며, 검증 경로에서 `AGENT_PYTHON_BIN=<해당 Python> PYTHONDONTWRITEBYTECODE=1 python -m pytest -q -p no:cacheprovider --basetemp=<검증 경로 밖 임시 경로>`를 사용했다.
|
||||
|
||||
선택 파일:
|
||||
|
||||
- `test_issue34_job_tree.py` — 신규 17개 사례 포함
|
||||
- `test_issue21_worktree_prereqs.py`, `test_issue21_phase2_loop_a.py`, `test_issue21_loop_b.py`, `test_issue21_acceptance.py`
|
||||
- `test_issue5_phase1_delegation.py`
|
||||
- `test_tier4_e2e.py`
|
||||
- `test_issue23_24_startup_path.py`
|
||||
- `test_issue7_workspace_packing.py`, `test_herdr_shim_contract.py`
|
||||
|
||||
총 **287개 서로 다른 선택 항목**의 통과를 확인했다. 한 번의 무조건적인 전체 green 실행이라는 의미는 아니며, 실행 조건과 원시 결과는 다음과 같다.
|
||||
|
||||
| 실행 | 결과 |
|
||||
|---|---|
|
||||
| 선택 묶음 최초 집계 | 284 passed, 3 failed / 894.44s |
|
||||
| Issue21 L11, A4 재실행 | 2 passed / 2.26s |
|
||||
| Tier4 scenario5 별도 재실행 | 1 passed / 338.00s, MAM_INJECT_ACK_GRACE_S=1 |
|
||||
| 변경 셸 bash -n, 변경 Python AST 구문 검사 | 통과 |
|
||||
| git diff --check base..HEAD | 통과 |
|
||||
|
||||
L11/A4의 실패는 테스트가 `mam-review` 문자열이 없어야 한다고 검사하는데 basetemp 이름을 `mam-review-c1dec920.*`로 정했기 때문이다. 코드의 실제 경로 누출/정리 실패가 아니며, 이 문자열이 없는 `/tmp/c1dec920-rerun.*`에서 두 항목이 통과했다.
|
||||
|
||||
최초 Tier4 scenario5는 모의 에이전트가 started를 발행하지 않아 잡마다 180초 ACK 대기를 하는 것을 확인하고 해당 샌드박스 loop 프로세스 트리만 중단했다. 따라서 최초 실패의 rc=143은 제품 결함 판정에 사용하지 않았다. 같은 테스트를 ACK grace=1로 재실행해 실제 두 리뷰 회차와 최종 PASS까지 통과했다. 기본 180초 설정으로 끝까지 완료한 결과는 주장하지 않는다.
|
||||
|
||||
역검증은 임시 skills 사본에서 **runner.py만 base 버전으로 교체**하여 신규 T3와 T5b[True]를 실행했다. 2 failed / 10.45s로, 자손 대기와 CLOSED-UNCONFIRMED 리뷰 근거가 없다는 이유로 실패했다. 제품 파일에는 역변경을 적용하지 않았다.
|
||||
|
||||
추가 확인:
|
||||
|
||||
- 실제 래퍼/등록부로 부모 flag, 존재하지 않는 부모 거절, MAM_JOB_ID fallback, 직접 자식 목록을 검사하는 신규 테스트가 통과했다. 깊이는 조회 시 계산된다. 순환/자기 참조는 조회의 visited 집합으로 무한 탐색을 방지하며, Python register_job API 자체가 모든 잘못된 parent 입력을 거부하는 것은 아니다.
|
||||
- 503개 임시 레코드(자손 502개) 탐침에서 깊이 1/2/3과 자기 참조 배제가 맞았고, 조회는 약 0.0098초였다. 매 폴링 전체 스캔은 남아 있다. MAM_JOB_TREE_MAX는 스캔 자체의 상한이 아니라 대기를 중단하는 상한이며, 이후 리뷰 표에서 열린 잡을 숨기지 않는다.
|
||||
- 실행·수정·순응 경로의 wait_for_job_tree 연결과 이전 creator 잡 자손의 후속 회차 노출을 확인했다. 자손 대기 시간/개수 초과는 루프 즉시 실패가 아니라 리뷰 근거로 처리하는 설계다. 실제 signal handler는 checkpoint 후 exit한다.
|
||||
- 자손과 귀속 불명 열린 잡이 모두 없으면 prompt 추가 블록은 빈 문자열이다. 자신의 created_jobs는 제외한다. 다른 루프의 부모 없는 새 열린 잡은 시간 기반 휴리스틱으로 나타날 수 있다. 문서의 기본 wait-open-jobs=off 및 무관한 잡에 대한 경고와 일치하지만, 이 목록을 해당 creator 소유임이 입증된 목록으로 해석해서는 안 된다.
|
||||
- 임시 Git 저장소에서 .mam, jobs, 상대경로 audit 파일 변경은 fingerprint를 바꾸지 않고 소스 변경은 바꾸는 것을 확인했다. 소스 밖 제어 경로 제외와 identity 모드 분리는 코드로 확인했다.
|
||||
- 같은 서버 다른 workspace 조회, 명시적 불일치/외부 pane scope 보존, 기존 교차 서버 규칙, workspace packing, shim 계약 테스트가 통과했다.
|
||||
- parent 안내문 및 loop 문서는 주요 대기/리뷰/지문 동작을 설명한다. 위 R1/R2는 그 설명과 실제 terminal reason 소비 계약의 차이다.
|
||||
- 예정된 4.7.4 버전 bump 부재는 거절 사유로 삼지 않았다.
|
||||
|
||||
두 종결 분류 결함을 수정하고 실제 producer → reason reader → 리뷰 표까지 잇는 회귀 테스트를 추가한 뒤 재리뷰해야 한다.
|
||||
|
||||
[VERDICT: NOT PASS]
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# Review Report: PR #38 (fix(#34): 루프가 creator의 하위 위임 잡까지 기다리고, 리뷰어에게 하위 잡 상태를 근거로 제공)
|
||||
|
||||
- **Job ID**: `af4c8897`
|
||||
- **Reviewer**: Antigravity (`agy`, session: `herdr:reviewer-agy-01`, Role: `Reviewer`)
|
||||
- **PR**: #38 (`fix/issue-34` @ `ed293dc`, base `main` `7a4a0c2` / v4.7.3)
|
||||
- **Review Identity & Tree**:
|
||||
- `loop_id`: `loop-20260925T055502Z-a37cf1`
|
||||
- `base`: `7a4a0c209195c20719a50e28398416ccd19004e2`
|
||||
- `head`: `ed293dcef4c346e0050a31cb46812bcdcf1d67cf`
|
||||
- `tree`: `671b0e59eef915f89d1dce63e9c0796db132899c`
|
||||
- **Verification Worktree**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260925T055502Z-a37cf1/1`
|
||||
- **Verdict**: **PASS**
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary & Verification Identity
|
||||
|
||||
PR #38 addresses Issue #34, resolving the fundamental orchestration flaw where a Creator agent delegating sub-tasks via `multi-agent-mux-delegate-job` could terminate its root job while child jobs were still running or pending. Previously, the loop immediately proceeded to the review phase, resulting in reviewers passing half-finished work or reviewing snapshots that were subsequently overwritten by asynchronous sub-jobs.
|
||||
|
||||
PR #38 introduces:
|
||||
1. **Hierarchical Job Tracking**: Records `parent_job` in `.mam/jobs/<id>/job.json`, with strict registration-time validation, cycle detection, and limit bounding (`MAM_JOB_TREE_MAX=50`).
|
||||
2. **Context Propagation & Safety Nets**: Automatic default propagation of `MAM_JOB_ID` through runner environment injection and shell wrappers, with fallback detection for unattributed open jobs created during the turn.
|
||||
3. **Deterministic Job-Tree Waiting**: The orchestrator runner waits for all descendants across implementation, correction, and compliance rounds up to `MAM_CHILD_WAIT_S` (default 3900s), classifying child states into definitive categories (`OPEN`, `CLOSED-UNCONFIRMED`, `NOT-DISPATCHED`, `completed`, `error`).
|
||||
4. **Structured Review Evidence & Clean Prompt Hygiene**: Reviewers receive structured markdown tables of child job states and strict review rules (`SUBJOB_RULE`). Zero sub-jobs and zero unattributed jobs produce an empty string, guaranteeing byte-for-byte prompt hygiene for un-delegated loops.
|
||||
5. **Stale Review Guard**: Cryptographic fingerprinting (`_fingerprint()`) invalidates reviews if source files or worktree change during review rounds when sub-jobs exist, guarding against late writes from sub-agents closed by timeout.
|
||||
6. **Cross-Workspace Herdr Scoping Fix**: Resolves pane-inherited `HERDR_WORKSPACE_ID` issues in `lib.sh` (`mam_herdr`), ensuring pane callers can locate agents in other workspaces on the same server while strictly adhering to Issue #23/#24 isolation boundaries and preflight target checks (`target_not_found`, exit 5).
|
||||
|
||||
### Identity & Tree Verification
|
||||
The verification was executed in an isolated detached worktree at commit `ed293dc`:
|
||||
- `git rev-parse HEAD` = `ed293dcef4c346e0050a31cb46812bcdcf1d67cf`
|
||||
- `git rev-parse 'HEAD^{tree}'` = `671b0e59eef915f89d1dce63e9c0796db132899c`
|
||||
- `git diff --check 7a4a0c2...HEAD` = Clean (0 formatting / whitespace issues)
|
||||
- Worktree Status: Clean
|
||||
|
||||
---
|
||||
|
||||
## 2. Detailed Technical Audit (Items 1 – 9)
|
||||
|
||||
### Item 1: Parent Job Tracking & Registry Schema
|
||||
- **File**: `lib_py/delegation/registry.py`
|
||||
- Added optional `parent_job: str | None = None` to `Job` dataclass and serialization schema (`to_dict` / `from_dict`).
|
||||
- `register()` validates that if `--parent-job` is supplied, the referenced parent job directory and `job.json` must exist in the registry (`jobs_dir / parent_job / "job.json"`). If nonexistent, registration halts with exit code 1 (`ValueError: parent job <id> does not exist in registry`).
|
||||
- Added query helpers:
|
||||
- `list_jobs(parent=...)`: filters jobs by parent identifier.
|
||||
- `children_of(parent_id)`: returns immediate direct child jobs.
|
||||
- `descendants_of(parent_id, max_jobs=50)`: performs a breadth-first search (BFS) traversing the entire subtree.
|
||||
- **Loop/Cycle Safety**: Maintains a `seen = {parent_id}` set. Any circular references or repeated visits are ignored. If total discovered descendants reach `max_jobs` (`MAM_JOB_TREE_MAX`, default 50), traversal halts and raises a bounded exception / warning, preventing unbounded recursion and denial-of-service.
|
||||
|
||||
### Item 2: Parent ID Propagation & Safety Net
|
||||
- **Files**: `multi-agent-mux-delegate-job`, `create_session.sh`, `lib_py/loop/runner.py`
|
||||
- `multi-agent-mux-delegate-job submit` adds `--parent-job <id>`. If omitted, it automatically falls back to `os.environ.get("MAM_JOB_ID")`.
|
||||
- When falling back to `MAM_JOB_ID`, if the ID is not found in the registry, it logs a warning and falls back to root (allowing standalone executions outside the loop).
|
||||
- `create_session.sh` injects guidance into the agent prompt reminding creators to propagate `--parent-job "$DELEGATE_JOB_ID"` when delegating sub-tasks.
|
||||
- **Unattributed Open Jobs Safety Net**: If a sub-agent executes a delegation without passing `--parent-job`, `lib_py/loop/runner.py` detects any jobs registered during the creator's turn that remain open and unparented (`unattributed_open_jobs()`). These are surfaced in reviewer prompts under `[귀속되지 않은 열린 잡]`. If `--wait-open-jobs` is enabled, the runner automatically waits for them before review.
|
||||
|
||||
### Item 3: Descendant Waiting & Classification
|
||||
- **File**: `lib_py/loop/runner.py`
|
||||
- Integrated `wait_for_job_tree(root_job_id)` into:
|
||||
1. Initial implementation turn (`_run_creator_round`)
|
||||
2. Correction rounds (`_run_correction_round`)
|
||||
3. Compliance audit turns (`_run_compliance_round`)
|
||||
- The runner waits for the root job, then enters `wait_for_descendants()`, polling all active descendants until all reach terminal state or the timeout (`MAM_CHILD_WAIT_S`, default 3900s) expires.
|
||||
- Sub-job classification logic (`_subjob_state()`):
|
||||
- `OPEN`: status not in `("completed", "error", "cancelled")`.
|
||||
- `CLOSED-UNCONFIRMED`: closed administratively by the orchestrator via `deadline_timeout` or `agent_blocked`, or status is `error` without a recorded error reason. Explicitly warns reviewers that the sub-agent process may still be active and writing.
|
||||
- `NOT-DISPATCHED`: cancelled prior to dispatch due to `session_busy`, `target_not_found`, or `rolled back`.
|
||||
- `completed` / `error`: standard terminal states, including error detail messages.
|
||||
|
||||
### Item 4: Reviewer Evidence & Prompt Hygiene
|
||||
- **File**: `lib_py/loop/runner.py`
|
||||
- Generates structured markdown blocks:
|
||||
- `[하위 위임 잡]`: Markdown table containing `Job ID`, `Role`, `Target Agent`, `Status`, `Reason / Note`.
|
||||
- `[귀속되지 않은 열린 잡]`: Markdown table for unparented jobs initiated during the creator turn.
|
||||
- **Prompt Hygiene**: When `len(subjobs) == 0` and `len(unattributed) == 0`, `_subjobs_block()` returns `""`. In runs where sub-delegation is not utilized, prompt formatting is 100% byte-for-byte identical to baseline.
|
||||
- **SUBJOB_RULE**: The injected reviewer instruction mandates that reviewers must evaluate the outcome of sub-jobs and issue `[VERDICT: NOT PASS]` if any sub-job remains `OPEN`, `CLOSED-UNCONFIRMED`, or failed without a documented replacement.
|
||||
|
||||
### Item 5: Stale Review Guard in Non-Identity Mode
|
||||
- **File**: `lib_py/loop/runner.py`
|
||||
- `_fingerprint()` computes an SHA-256 hash of `(source_head, worktree_tree(exclude=...))`.
|
||||
- Exclusions strictly ignore dynamic ephemeral paths: `.mam`, `.mam/jobs`, and delegate job log directories.
|
||||
- **Armed Condition**: The guard is armed exclusively when sub-jobs exist (`subjobs != ""`). This prevents false triggers on unpinned files or unrelated workspace churn when no child delegations took place.
|
||||
- If late writes from a timeout-closed sub-job alter the source repository during the review phase:
|
||||
- Any intermediate approvals are discarded.
|
||||
- Re-review is triggered.
|
||||
- If the change occurs on the final iteration or post-approval, the loop terminates with exit code 1 to prevent committing stale/corrupted state.
|
||||
|
||||
### Item 6: Same-Server Cross-Workspace Herdr Scoping
|
||||
- **File**: `lib.sh` (`mam_herdr`)
|
||||
- Added helper `_herdr_scope_is_pane_inherited()`.
|
||||
- Strips `HERDR_WORKSPACE_ID` from the environment only during session lookups (`agent`, `has-session`, `kill-session`) when the workspace ID in the environment matches the caller pane's workspace prefix.
|
||||
- Preserves Issue #23/#24 cross-server boundaries and explicit caller overrides (`test_t11`).
|
||||
- Added preflight target check: if an agent target is not visible on the server, it reports `target_not_found` (exit code 5) rather than falsely classifying it as `session_busy`.
|
||||
|
||||
### Item 7: Test Suite & Regression Coverage
|
||||
All test suites were executed inside the verification worktree:
|
||||
1. `tests/test_issue34_job_tree.py`: **17/17 PASSED** (0:01:16)
|
||||
- T1: `submit` records `--parent-job` and validates parent presence.
|
||||
- T2 & T2b: Runner waits for children and grandchildren; timeout and max tree size bounded.
|
||||
- T3: Loop waits for sub-jobs before initiating review.
|
||||
- T4 & T5: Open or failed sub-jobs surface as evidence and block reviews.
|
||||
- T5b: Timeout-closed sub-job unconfirmed state and late write voids round.
|
||||
- T6: Unattributed open jobs surfaced and waited on request.
|
||||
- T7: Single job without children maintains baseline execution path.
|
||||
- T8: Stale review guard in non-worktree mode.
|
||||
- T9: Correction rounds preserve and aggregate earlier creator sub-jobs across iterations.
|
||||
- T10, T11, T12: Herdr cross-workspace lookups, caller-chosen workspace scoping, and `target_not_found` preflight handling.
|
||||
2. Regression Suites:
|
||||
- `tests/test_version_consistency.py` & `tests/test_issue5_phase1_delegation.py`: **69/69 PASSED**
|
||||
- `tests/test_issue23_24_startup_path.py` & `tests/test_loop_cli.py`: **79/79 PASSED**
|
||||
- **Total Verified Tests**: **165 tests passed, 0 failures, 0 regressions**.
|
||||
|
||||
### Item 8 & 9: Documentation & Versioning
|
||||
- `MULTI_AGENT_RULES.md`, `MULTI_AGENT_RULES.ko.md`, and `SKILL.md` have been updated to document parent job delegation requirements, reviewer evaluation rules for child jobs, and stale guard semantics.
|
||||
- Version consistency is verified; v4.7.4 version bump is reserved for the orchestrator release commit in accordance with project rules.
|
||||
|
||||
---
|
||||
|
||||
## 3. Conclusion
|
||||
|
||||
PR #38 provides a robust, safe, and backwards-compatible implementation of hierarchical job tracking and sub-job waiting. All edge cases (cycles, timeouts, late-writing agents, cross-workspace panes, unparented delegations) are defensively handled and thoroughly covered by automated tests.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"loop_id": "loop-20260925T072328Z-2f5853",
|
||||
"base_sha": "ed293dcef4c346e0050a31cb46812bcdcf1d67cf",
|
||||
"head_sha": "e80186f56064eaf93a2f32c48d31d84816894774",
|
||||
"tree_sha": "3501868872d828894f031911b8ca31b01ce77a72",
|
||||
"review_commit": "e80186f56064eaf93a2f32c48d31d84816894774",
|
||||
"reviewer": "reviewer-agy-01",
|
||||
"job_id": "f17a85df",
|
||||
"verdict": "PASS",
|
||||
"transport": "herdr",
|
||||
"evidence": "review-tree",
|
||||
"isolation": "worktree",
|
||||
"verified_at": "2026-09-25T07:43:24Z"
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
# Review Report: PR #38 Delta Review Round 2 (fix(#34) @ e80186f)
|
||||
|
||||
- **Job ID**: `f17a85df`
|
||||
- **Reviewer**: Antigravity (`agy`, session: `herdr:reviewer-agy-01`, Role: `Reviewer`)
|
||||
- **PR Target**: #38 (`fix/issue-34` @ `e80186f`, previous review target `ed293dc`, base `main` `7a4a0c2` / v4.7.3)
|
||||
- **Review Identity & Tree**:
|
||||
- `loop_id`: `loop-20260925T072328Z-2f5853`
|
||||
- `base`: `ed293dcef4c346e0050a31cb46812bcdcf1d67cf`
|
||||
- `head`: `e80186f56064eaf93a2f32c48d31d84816894774`
|
||||
- `tree`: `3501868872d828894f031911b8ca31b01ce77a72`
|
||||
- **Verification Worktree**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260925T072328Z-2f5853/1`
|
||||
- **Verdict**: **PASS**
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary & Verification Context
|
||||
|
||||
This is the Round 2 Delta Review of PR #38, evaluating the two commits (`8a9101e` and `e80186f`) added on top of `ed293dc` to resolve the review findings from Round 1:
|
||||
1. **N1/R1 (grok / codex, blocking)**: `registry.get_feedback` previously only parsed `completed` and `error` events, discarding `cancelled` event details. Sub-jobs cancelled prior to injection (e.g., `target_not_found` or `session_busy`) surfaced in the review table as bare uninformative `cancelled` rather than `NOT-DISPATCHED`.
|
||||
2. **R2 (codex, blocking)**: Remote runner deadlines publish `error --detail "deadline_timeout (remote runner)"`. Exact-string matching in `_subjob_state` failed to identify this as an orchestrator timeout, missing `CLOSED-UNCONFIRMED` categorization and late-write warnings.
|
||||
3. **K1 (claude, non-blocking)**: The `[귀속되지 않은 열린 잡]` section displayed orphan jobs without explicit adjudication instructions.
|
||||
|
||||
### Verification Identity & Git Cleanliness
|
||||
- `git rev-parse HEAD` = `e80186f56064eaf93a2f32c48d31d84816894774`
|
||||
- `git rev-parse 'HEAD^{tree}'` = `3501868872d828894f031911b8ca31b01ce77a72`
|
||||
- `git diff ed293dc...HEAD --check` = Clean (0 issues)
|
||||
- `git diff 7a4a0c2...HEAD --check` = Clean (0 issues)
|
||||
- Worktree Status: Clean
|
||||
|
||||
---
|
||||
|
||||
## 2. Detailed Verification of Round 1 Corrective Changes
|
||||
|
||||
### (1) N1/R1 Fix: `get_feedback` Event Filtering & `cancelled` Detail Capture
|
||||
- **Files Modified**:
|
||||
- `lib_py/delegation/registry.py`: `get_feedback(job_id, registry_dir=..., events=("completed", "error"))`
|
||||
- `lib_py/loop/runner.py`: `_subjob_state()` passes `events=("completed", "error", "cancelled")`
|
||||
- **Audit Findings**:
|
||||
- **Preserved Default Semantics**: `events` parameter defaults to `("completed", "error")`. Existing callers (such as `registry.py` CLI `feedback` / line 669) that pass only `job_id` and `registry_dir` remain 100% backward-compatible.
|
||||
- **Dual-Path Support**: Both the primary unified ndjson audit log path (`e.get("source_event") in events` / `e.get("event") in events`) and the fallback local `.events.log` path (`payload.get("event") in events`) correctly check against `events`.
|
||||
- **Regression Test T13**: Added `test_t13_a_sub_job_cancelled_before_injection_is_not_dispatched_in_the_table` parametrized over `["target_not_found", "session_busy"]`. Both cases pass and confirm classification into `| NOT-DISPATCHED | <reason> |`. Without the fix, `reason` is empty and the table displays `cancelled | -`, failing the assertion.
|
||||
|
||||
### (2) R2 Fix: Prefix Matching for Orchestrator Timeout Strings
|
||||
- **File Modified**:
|
||||
- `lib_py/loop/runner.py`:
|
||||
```python
|
||||
closed_kind = reason.split(" ", 1)[0] if reason else ""
|
||||
if closed_kind in ORCHESTRATOR_CLOSED or (status == "error" and not reason):
|
||||
return "CLOSED-UNCONFIRMED", reason or "-"
|
||||
```
|
||||
- **Audit Findings**:
|
||||
- **Accurate Prefix Extraction**: `reason.split(" ", 1)[0]` extracts the initial keyword (e.g., `"deadline_timeout"` from `"deadline_timeout (remote runner)"`).
|
||||
- **No Misclassification**: The candidate set `ORCHESTRATOR_CLOSED` strictly equals `{"deadline_timeout", "agent_blocked"}`. Other failure or cancellation reasons (such as `session_busy`, `target_not_found`, or `rolled back...`) have prefixes not in this set and therefore are never misclassified as `CLOSED-UNCONFIRMED`.
|
||||
- **Regression Test T14**: Added `test_t14_the_remote_runners_deadline_wording_is_closed_unconfirmed` using the real registry and event publisher. Verifies `CLOSED-UNCONFIRMED` state and `미확인 종결 1` warning in the prompt. Without `closed_kind`, this test fails.
|
||||
|
||||
### (3) K1 Fix: `STRAY_RULE` Guidance for Unparented Open Jobs
|
||||
- **File Modified**:
|
||||
- `lib_py/loop/runner.py`: Added `STRAY_RULE` constant and conditional injection into `stray_line` if and only if `stray` is non-empty.
|
||||
- **Audit Findings**:
|
||||
- **Rule Coherence**: `SUBJOB_RULE` instructs reviewers to fail the round if any known sub-job is open, unconfirmed, or failed. `STRAY_RULE` instructs reviewers to fail the round if any unparented open job's deliverables were necessary for the task objectives. The two rules operate harmoniously and without contradiction.
|
||||
- **Prompt Hygiene**: When neither registered sub-jobs nor stray jobs exist, `_subjobs_block()` continues to return `""`, ensuring byte-for-byte prompt parity for non-delegating tasks.
|
||||
- **Regression Test T6 Assertion**: `test_t6_an_unattributed_open_job_is_shown_and_waited_for_only_on_request` now asserts that `STRAY_RULE` is included in the reviewer prompt.
|
||||
|
||||
---
|
||||
|
||||
## 3. Test Suite & Regression Verification
|
||||
|
||||
All tests were executed against the verification worktree (`loop-20260925T072328Z-2f5853/1`):
|
||||
1. **`tests/test_issue34_job_tree.py`**:
|
||||
- **20 / 20 PASSED** (including T13 parametrized × 2, T14, and updated T6) in 84.48s.
|
||||
2. **`tests/test_version_consistency.py` & `tests/test_issue5_phase1_delegation.py`**:
|
||||
- **69 / 69 PASSED** in 12.07s.
|
||||
3. **`tests/test_issue23_24_startup_path.py` & `tests/test_loop_cli.py`**:
|
||||
- **79 / 79 PASSED** in 195.92s.
|
||||
4. **Overall Total**: **168 tests passed, 0 failures, 0 regressions**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Conclusion
|
||||
|
||||
All Round 1 reviewer findings (N1, R1, R2, K1) have been accurately addressed with clean, minimal code changes and rigorous regression test coverage. No edge-case misclassifications, prompt pollution, or regressions were detected. Version lockstep bump to v4.7.4 is properly reserved for the orchestrator release commit.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,50 @@
|
||||
# 🔍 Review Report — Job addfcf70 (PR #38 @ `ed293dc`)
|
||||
|
||||
- **Reviewer**: `reviewer-grok-01` · 2026-09-25
|
||||
- **검증 경로**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260925T055502Z-a37cf1/1`
|
||||
- **identity**: base `7a4a0c2` head `ed293dc` tree `671b0e5` (일치)
|
||||
- **본 체크아웃**: `main` `7a4a0c2` 유지
|
||||
|
||||
`git diff --check 7a4a0c2...HEAD` 클린. 6커밋.
|
||||
|
||||
## NOT PASS
|
||||
|
||||
### N1. `cancelled` 사유는 `get_feedback` 이 읽지 않아 `NOT-DISPATCHED` 가 되지 않는다
|
||||
|
||||
`registry.py:316-349` `get_feedback` 은 마지막 `completed`/`error` 의 detail 만 돌려준다. `cancelled` 는 보지 않는다.
|
||||
|
||||
래퍼는 미투입 종료를 `publish_event --event cancelled --detail target_not_found|session_busy` 로만 남긴다 (`multi-agent-mux-delegate-job` finalize). `runner.py:687-694` `_subjob_state` 는 그 reason 이 `session_busy` / `target_not_found` / `rolled back…` 일 때만 `NOT-DISPATCHED` 다. reason 이 항상 `""` 이므로 표는 `cancelled` / `-` 가 된다.
|
||||
|
||||
`deadline_timeout`·`agent_blocked` 는 `error` 이벤트 detail 이라 `CLOSED-UNCONFIRMED` 는 맞다. 사유 없는 `error` 도 맞다. 빠지는 것은 취소 쪽뿐이다. `test_issue34_job_tree.py` 는 `NOT-DISPATCHED` 를 단언하지 않는다.
|
||||
|
||||
**대안**: `get_feedback` 이 마지막 `cancelled` detail 도 반환하게 하거나, `_subjob_state` 가 `events.log` 의 마지막 `cancelled` detail 을 직접 읽게 하라. 그 뒤에 `target_not_found` 잡이 표에서 `NOT-DISPATCHED | target_not_found` 인 테스트를 둘 것.
|
||||
|
||||
리뷰어 규칙이 `cancelled` 자체도 실패로 보게 하므로 판정이 뒤집히지는 않는다. 사유(투입 전 취소 vs 에이전트 실패)는 사라진다.
|
||||
|
||||
## 그 외 (N1 만 막음)
|
||||
|
||||
**(1)** 없는 `--parent-job` 은 래퍼와 `registry.py register` 둘 다 exit 1. `MAM_JOB_ID` 가 이 등록부에 없으면 경고 후 부모 없음. `descendants_of` 는 한 번 스캔, `seen` 으로 순환 중단, `depth` 는 조회 때 1부터. 폴링마다 전체 스캔은 수백 건에서 부담이 작다. `MAM_JOB_TREE_MAX` 초과·`MAM_CHILD_WAIT_S` 만료는 대기를 멈추고 루프는 리뷰로 간다. 표는 상한 없이 자손을 그리므로 `OPEN` 이 남아 NOT PASS 규칙이 적용된다. 기다리는 쪽이 더 위험하다.
|
||||
|
||||
**(2)** 주입문·brief·create `--submit-job` 이 `--parent-job` 을 적는다. 에이전트가 빠뜨리면 `--wait-open-jobs`(기본 off)와 `[귀속되지 않은 열린 잡]` 이 안전망이다. 원격은 `MAM_JOB_ID` 가 이 등록부에 있을 때만 기본 부모.
|
||||
|
||||
**(3)** `wait_for_job_tree` 는 실행(`round_roots = [exec_jid]`), 수정·순응(`append` 후)에 걸린다. `MAM_CHILD_WAIT_S` 기본 3900 은 그 호출 시각부터. SIGINT/SIGTERM 핸들러는 `sys.exit` 이라 `interrupted` 폴링은 보조다. `wait_for_descendants` 가 False 여도 `wait_for_job_tree` 는 True 를 돌려 리뷰로 넘긴다(OPEN 은 표에 남음).
|
||||
|
||||
**(4)** 자손도 stray 도 없으면 `_subjobs_block` 은 `""`. stray 는 `created_jobs` 를 빼므로 이 루프의 리뷰어 잡은 제외된다. 다른 루프의 부모 없는 열린 잡은 표시된다(기본 대기는 안 함). `SUBJOB_RULE` 은 OPEN·CLOSED-UNCONFIRMED·실패 시 NOT PASS, 대체 완료는 예외라고 적는다.
|
||||
|
||||
**(5)** `_fingerprint` 는 `.mam`·jobs·감사 로그가 소스 안일 때만 exclude. 절대 `DELEGATE_JOB_LOGS_DIR` 은 소스 밖이라 비교에 안 들어간다. 가드는 `subjobs` 가 비지 않을 때만(1d4f338). identity 모드(`--worktree`/`--base`)는 `light` 를 안 켠다. 승격 직전 `light` 재확인이 있다. `test_e2e_scenario5_multi_agent_review_loop` 통과.
|
||||
|
||||
**(6)** 같은 서버에서 `agent|has-session|kill-session` 이고 `HERDR_PANE_ID` 의 워크스페이스가 `HERDR_WORKSPACE_ID` 와 같을 때만 그 ID 를 뺀다. 교차 서버는 기존처럼 세 ID 를 전부 뺀다. pane 밖·불일치 ID·`new-session`/pane 호출은 유지(#7 패킹). `target_not_found` 는 preflight detail·cancelled 이벤트에 들어간다(표 라벨은 N1).
|
||||
|
||||
**(7)** `test_issue34_job_tree.py` 는 실제 래퍼 `--dry-run`·registry·publish. tmp 만 씀.
|
||||
|
||||
**(8)–(9)** SKILL·MULTI_AGENT_RULES(ko) 가 `--parent-job`, `MAM_CHILD_WAIT_S`, `--wait-open-jobs`, 판정 규칙, stale 가드를 적는다. 4.7.4 bump 는 오케스트레이터 예정.
|
||||
|
||||
## 테스트
|
||||
|
||||
검증 경로, `AGENT_PYTHON_BIN` = 컨트롤 `.venv`, basetemp `/tmp`:
|
||||
|
||||
`test_issue34_job_tree.py` + `test_issue23_24_startup_path.py` + `test_tier4_e2e.py` + `test_issue5_phase1_delegation.py` + `test_issue21_worktree_prereqs.py`
|
||||
|
||||
**193 passed in 1429.09s.** N1 은 이 스위트에 단언이 없다.
|
||||
|
||||
[VERDICT: NOT PASS]
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"loop_id": "loop-20260925T072328Z-2f5853",
|
||||
"base_sha": "ed293dcef4c346e0050a31cb46812bcdcf1d67cf",
|
||||
"head_sha": "e80186f56064eaf93a2f32c48d31d84816894774",
|
||||
"tree_sha": "3501868872d828894f031911b8ca31b01ce77a72",
|
||||
"review_commit": "e80186f56064eaf93a2f32c48d31d84816894774",
|
||||
"reviewer": "reviewer-grok-01",
|
||||
"job_id": "df6a4aa1",
|
||||
"verdict": "PASS",
|
||||
"transport": "herdr",
|
||||
"evidence": "review-tree",
|
||||
"isolation": "worktree",
|
||||
"verified_at": "2026-09-25T07:43:24Z"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# 🔍 Review Report — Job df6a4aa1 (PR #38 델타 `ed293dc...e80186f`)
|
||||
|
||||
- **Reviewer**: `reviewer-grok-01` · 2026-09-25
|
||||
- **검증 경로**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260925T072328Z-2f5853/1`
|
||||
- **identity**: head `e80186f` tree `3501868` (일치). 1회차 grok N1 을 이 델타가 닫음.
|
||||
- **본 체크아웃**: `main` `7a4a0c2` 유지.
|
||||
|
||||
`git diff --check ed293dc...HEAD` 클린. 커밋 `8a9101e`, `e80186f`.
|
||||
|
||||
## (1) 세 정정 — 닫힘
|
||||
|
||||
**N1.** `get_feedback` (`registry.py:316-317`) 기본 `events` 는 `("completed", "error")` 그대로다. 감사 ndjson 은 `source_event`/`event`, `.events.log` 폴백은 `event` 를 같은 튜플로 본다. `_subjob_state` (`runner.py:690-691`) 만 `cancelled` 를 더 넣는다. T13: `target_not_found`·`session_busy` 가 `| NOT-DISPATCHED | <reason> |`. 임시 사본에서 그 인자를 빼면 T13 두 건이 실패했다.
|
||||
|
||||
**R2.** 종류는 `reason.split(" ", 1)[0]` (`runner.py:696-697`). `deadline_timeout (remote runner)` → `CLOSED-UNCONFIRMED`, 미확인 종결 1. `session_busy`·`target_not_found` 는 `ORCHESTRATOR_CLOSED` 에 없어 미확인으로 안 간다. 취소 쪽은 예전처럼 완전 일치/`rolled back` 접두다. 임시 사본에서 완전 일치로 되돌리면 T14 가 실패했다.
|
||||
|
||||
**K1.** stray 가 있을 때만 `STRAY_RULE` (`runner.py:726-729`). 없으면 그 문장이 없고, 자손도 stray 도 없으면 `""`. SUBJOB_RULE 은 표에 있는 자손용이고, STRAY_RULE 은 “필요할 때만 NOT PASS” 라 모순이 아니다. T6 통과.
|
||||
|
||||
## (2) 1회차·락스텝
|
||||
|
||||
부모 검증, 자손 대기, 같은 서버 이름 조회, stale 가드는 이 델타가 되돌리지 않는다. `feedback` CLI 와 다른 `get_feedback` 호출은 기본 인자라 취소 이벤트를 새로 집지 않는다. 4.7.4 bump 는 오케스트레이터 예정. 병합 차단 아님.
|
||||
|
||||
## 테스트
|
||||
|
||||
검증 경로, `AGENT_PYTHON_BIN` = 컨트롤 `.venv`, basetemp `/tmp`:
|
||||
|
||||
T13×2, T14, T6 → **4 passed in 13.93s**.
|
||||
|
||||
## 종합
|
||||
|
||||
1회차 N1·R2·K1 이 코드와 macOS 실행과 맞다. 설계 재작업 불필요.
|
||||
|
||||
[VERDICT: PASS]
|
||||
Reference in new issue
Block a user