release(v4.8.7): Issue #17 follow-ups - archive the review reports and bump the lockstep version

Merge-workspace review loop (--all-reviewer): round 1 (@8241bd5) claude
grok PASS, agy NOT PASS (two static wrapper-literal tests did not follow
_decided/_exit_for), codex NOT PASS (R1: a yes/no decision that could not
run read as "no"); fixed in 541c3fe and 65b78b6. Round 2 (@65b78b6) 4/4
PASS. Full suite at 65b78b6 on macOS / Python 3.9 / herdr 0.9.3: 1730
passed / 0 failed / 56 skipped; 18 of the skips were the uninitialized
nats-docker submodule in the fresh worktree, and with it
tests/test_deploy_freshness.py is 31 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-08 11:32:39 +09:00
1 parent 65b78b6ec4
commit cd82ab6e72
28 files changed
+594 -25

No files matched your search

@@ -0,0 +1,43 @@
# 리뷰 리포트 — Job 4566e690 (Issue #17 후속, fix/issue17-followups 56d0bfd..8241bd5)
- 리뷰어: planner-reviewer-claude-01 (claude)
- 리뷰 identity: loop-20261007T224638Z-cf1efb / base 56d0bfd / head 8241bd5 / tree efef640 — 검증 경로에서 `git rev-parse HEAD 'HEAD^{tree}'` 일치, 미커밋 변경 없음
- 커밋 6개: 25c9970·f19f940(항목 1), a670ef5(항목 2), d6cb019(항목 3), c90a0af(항목 4), 8241bd5(테스트 환경 고정). 버전·VERSIONS.md 변경 없음(브리프대로).
## 결론 요약
항목 1~4 가 요구대로 반영됐고, 정상 경로의 종료 코드·출력·레지스트리·이벤트는 변하지 않았다. 추가 테스트 13건은 수정 전 코드에서 실제로 실패한다(255·127·2 등 예측된 코드로). 항목 1 에는 브리프 문구("테스트 전제 수정")를 넘는 **제품 코드 변경**(herdr_restore.py 의 placeholder 닫기)이 포함되는데, 실제 herdr 0.9.3 에서 그 변경 없이는 테스트도 resume 의 닫기도 성립하지 않음을 확인했으므로 정당한 범위 확장으로 판단한다(§1). 차단 결함 없음. 비차단 관찰 3건(§6).
## 1. 항목 1 — herdr 0.9.3 과 test_b
- 브리프의 전제 "닫기 동작은 cwd 를 보지 않는다 → :275 만 두 경로 허용" 은 불완전했다. herdr 0.9.3 은 디렉터리가 사라진 pane 을 **프로세스 없는 placeholder**(`restore_error` 설정)로 복원하고 `pane process-info` 는 `pane_not_found` 로 rc 1 을 낸다. 실제 서버 대조: 스크래치 사본에 **이전 herdr_restore.py** 만 넣고 test_b 를 돌리면 `close-restored-shells: Command […'pane','process-info','--pane','w2:p1'] returned non-zero exit status 1` 로 실패(1 failed, 11.8 s) — 즉 옛 코드는 Y 의 placeholder 에서 예외를 내 resume 의 닫기 단계 전체가 rc 1 로 끝나고(래퍼는 경고만), 아무 pane 도 닫지 못한다.
- 수정(herdr_restore.py:58-66 `_nothing_running`): `_shell_in_foreground` 가 CalledProcessError 를 내면 **pane 에 `restore_error` 가 있고 출력에 `pane_not_found` 가 있을 때만** "실행 중인 것 없음"으로 보고, 그 외 실패는 그대로 올린다. 두 조건이 모두 herdr 자신이 붙인 표지라 사용자 pane·다른 세션 pane 이 이 경로로 닫힐 수 없다(agent_session 일치 조건 :83 은 그 앞에서 여전히 적용). 단위 테스트 2건이 "restore_error 없음 + pane_not_found", "restore_error 있음 + 다른 오류" 를 모두 예외 유지로 고정한다. fail-safe 방향(닫는 쪽이 아니라 두는 쪽) 유지.
- `_herdr` 는 `subprocess.run(..., capture_output=True, check=True)` 이므로 CalledProcessError.stderr/.output 이 채워진다(:46-49) — 판정에 쓰는 문자열이 실제로 들어온다.
- test_issue45_restore.py:267·:275-277 주석·단언이 0.9.1($HOME)/0.9.3(원래 경로) 둘 다 허용. 다른 문서의 "$HOME 으로 복원" 주장: SKILL.md 들에는 없음(create:120, resume:63 은 "empty shells" 만 말함), VERSIONS.md 과거 항목은 브리프대로 제외.
- 실행: 이 Mac(herdr 0.9.3)에서 tests/test_issue45_restore.py **5개 모두 통과, skip 0**.
## 2. 항목 2 — N2 fail-closed
- 래퍼(:103-127): `_decision_failed` 는 stderr 에 사유를 적고 `exit 1`; `_decided <var> <sub> …` 는 rc≠0 또는 빈 출력이면 실패; `_exit_for` 는 답이 `^[0-9]+$` 가 아니면 실패. 다섯 `exit "$(_decide exit-code …)"` 가 `_exit_for` 로, 네 `sub-status` 대입과 `finalize-begin` 이 `_decided` 로, `finalize-end` 는 rc 검사(:856-857), `cmd_wait` 의 wait-rc 는 `_decided` + 숫자 검사(:1070-1071). 모든 호출이 서브셸 밖(직접 호출)이라 `exit 1` 이 래퍼를 끝낸다; `{ …; } 4>&1` 그룹도 서브셸이 아니다.
- 정상 경로 불변: `_decided` 는 같은 명령의 같은 한 줄 출력을 변수에 넣고, `finalize-end` 의 "소유자 사라짐 → rc 0·빈 답" 은 전과 같이 FINAL_JOB_STATUS="" 로 남는다(의도된 기존 동작). test_f_the_normal_path_is_unchanged(실제 래퍼, human 잡, 닿지 않는 브로커 → rc 3, 마지막 줄 = 로그 경로, 오류 문구 없음) 통과. 옛·새 비교 테스트(test_c_*) 전부 통과.
- rc 로만 읽는 호출: `subscriber-up` 실패(모듈 깨짐) → "subscriber died early" 경로 → `_decided sub-status` 가 실패 → exit 1(fail-closed, OK). `stall-watch` 실패 → `|| return 0` 으로 **감시만 꺼진 채 진행**(:718). 브리프가 "범위에 넣지 않은 것은 이유를 적으라" 했는데 리뷰어에게 그 이유는 보이지 않는다 — 관찰 N1. #30 감시자는 notify-only 라 잡 종결에는 영향이 없어 비차단.
- 추가 테스트가 수정 전 코드에서 실패하는가: 이전 래퍼(56d0bfd)를 넣은 사본에서 test_f_* 11건 + test_s_no_printed_decision_is_read_raw 모두 **실패** — exit-code 는 `exit: : numeric argument required` 로 **255**, 인터프리터 부재는 **127**, finalize-begin 빈 답은 finalize-end argparse `--evidence-until: invalid int value: ''` 로 **2**. 세 가지 모두 브리프가 금지한 종료 방식이며 새 코드는 전부 1 로 끝난다.
- 이식성: `/bin/bash -n` 통과(3.2), `printf -v`·`[[ =~ ]]` 는 3.2 에서 지원. 테스트는 `/bin/sh` 셸 스크립트 대역과 `sys.executable` 만 쓴다.
## 3. 항목 3 — N1 문구
wrapper.py:24-29 가 "이 모듈은 stdlib 만 import 하지만 `python -m lib_py.delegation.wrapper` 는 패키지 `__init__` 을 먼저 실어 paho 가 로드되며, 래퍼의 $PY 는 원래 paho 가 필요하다"로 정정. 실행 경로 변경 없음(diff 상 docstring 만).
## 4. 항목 4 — N3 주석
lib_int.sh:9-10 에 "PATH 의 python3 를 쓴다(단독 source 라 $PY 없음); 없으면 mam_int/mam_int_knob 이 아무것도 출력하지 않는다(rc 127)" 추가. 코드 불변.
## 5. 테스트 실행 (검증 경로, 워크트리 .venv Python 3.9.6, paho 있음, `env -i` 로 HERDR_*·MAM_*·MQTT_*·WORKSPACE_ROOT 제거)
| 실행 | 결과 |
|---|---|
| tests/test_issue45_restore.py, test_issue45_restore_unit.py, test_issue17_item4_decisions.py, test_issue33_budget.py | **310 passed / 0 failed / 0 skipped** (425 s) |
| 새 테스트 16건, 이전 래퍼·이전 herdr_restore.py 사본 | **13 failed** / 3 passed (실패 = F 11 + 정적 가드 + placeholder 단위) |
| test_b (실서버), 이전 herdr_restore.py 사본 | 1 failed (pane_not_found 예외) |
격리: 실행 전후 herdr 프로세스는 기존 `multi-agent-mux` 서버·attach·오케스트레이터 `agent wait`(pid 만 교체)뿐, `/private/tmp/m45*` 없음, `~/.config/herdr` sessions·config.toml md5 불변. 실제 .mam/jobs 항목 수 불변. 루프백 bind 제약 없음(브로커 테스트 정상 실행).
## 6. 비차단 관찰
- **N1 stall-watch 판정 실패 시 조용히 감시 생략**(래퍼 :718 `|| return 0`): 모듈이 깨지면 stderr 에 아무 말 없이 stall watcher 가 꺼진다. 종결·레지스트리·이벤트에는 영향이 없어 fail-closed 요구 밖이지만, `_decide` 의 rc 가 1(판정 "감시 안 함")인지 그 밖(실패)인지 구분해 후자는 경고 한 줄을 남기면 진단이 쉬워진다. 브리프가 요구한 "범위 제외 이유" 가 리포트에 있는지 오케스트레이터가 확인하길 권한다.
- **N2 `_env` 가 상속 WORKSPACE_ROOT 를 비우지 않음**(tests/test_issue17_item4_decisions.py:224-230): HERDR_/MAM_/SKS_ 만 걷어낸다. 새 테스트(:659-660, :692)는 WORKSPACE_ROOT 를 명시하지만, 래퍼를 source 하는 기존 `_run_both` 4곳(:246, :372, :561, :586, PR #51 유래)은 `_env(work, …)` 만 써서 에이전트 pane 에서 돌리면 그 pane 의 WORKSPACE_ROOT 로 lib.sh 가 초기화된다 — 브리프가 적은 사고(상속된 WORKSPACE_ROOT 로 다른 워크스페이스의 shim 재작성)와 같은 경로다. `_env` 에서 WORKSPACE_ROOT 를 제거하고 tmp_path 로 고정하면 한 줄로 막힌다. 이 델타가 만든 문제가 아니라 비차단.
- **N3 docstring 줄바꿈**(herdr_restore.py:7-8): "A pane keeps the\n`agent_session` its agent\nreported" 로 어색하게 끊겨 있다. 내용 문제 없음.
[VERDICT: PASS]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261007T224638Z-cf1efb",
"base_sha": "56d0bfda54e4f4fe1ccc138cc262edfb14cfb4a3",
"head_sha": "65b78b6ec44e44904afe34915563522d4631767f",
"tree_sha": "b805d9f9e6639c7359115ea9336eac95d7ab85bc",
"review_commit": "65b78b6ec44e44904afe34915563522d4631767f",
"reviewer": "planner-reviewer-claude-01",
"job_id": "f0f131cd",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-08T01:16:15Z"
}
@@ -0,0 +1,40 @@
# 리뷰 리포트 — Job f0f131cd (Issue #17 후속 2회차 델타, 8241bd5..65b78b6)
- 리뷰어: planner-reviewer-claude-01 (claude)
- 리뷰 identity: loop-20261007T224638Z-cf1efb / base 56d0bfd / head 65b78b6 / tree b805d9f — 검증 경로(…/2)에서 `git rev-parse HEAD 'HEAD^{tree}'` 일치, 미커밋 변경 없음
- 범위: 커밋 2개 — 541c3fe(agy 지적: 동결 리터럴을 `_decided`/`_exit_for` 로), 65b78b6(codex R1: 실패한 yes/no 판정은 "no" 가 아니다). wrapper.py +24/−4, 래퍼 +18/−4, 테스트 3개. 버전·VERSIONS·문서 변경 없음(브리프대로).
## 결론 요약
codex R1 이 닫혔고, 그 수정이 1회차에 내가 남긴 N1(stall-watch 판정 실패 시 조용히 감시 생략)도 함께 닫는다. 정상 경로(yes/no 두 분기 모두)의 동작은 불변. 새 테스트 15건이 1회차 head 코드에서 실제로 실패하며, 8건은 codex 가 지적한 바로 그 증상(rc 0)으로 실패한다. 차단 결함 없음.
## 1. 지적 종결
| 지적 | 수정 | 확인 |
|---|---|---|
| codex R1: `_decide subscriber-up`/`stall-watch` 를 rc 로 읽어, 판정 **실패**(rc 1·127, 빈 답)가 "no" 와 구분되지 않음. subscriber-up 에서는 rc 0 으로 끝난 구독자가 "not up" → "died early" 경로 → sub-status 0 = completed → **submit 이 0 으로 종료** | wrapper.py:8-17, :335-340, :344-353 — 두 yes/no 서브커맨드에 `--print`(yes/no 출력 + rc 0). 래퍼 :121-131 `_decided_yes` 가 `_decided` 로 답을 받아 `yes`/`no` 외에는 `_decision_failed` → exit 1. 호출 3곳(:396, :568, :730) 교체 | 1회차 head(8241bd5) 래퍼+wrapper.py 를 넣은 사본에서 test_f_a_failed_yes_no_decision_ends_the_submit_with_1 9건 **모두 실패** — subscriber-up 6건은 `assert 0 == 1`(codex 의 증상 그대로), stall-watch 3건은 오류 문구 없이 정상 종료. 현재 코드에서는 9건 모두 rc 1 + "could not decide" |
| claude N1(1회차): stall-watch 판정 실패가 `\|\| return 0` 으로 조용히 감시만 끔 | 위와 같음 — "no" 는 여전히 `return 0`(감시 안 함), 실패는 exit 1 | stall-watch-direct-{error,gone,empty} 통과 |
| agy: test_issue5_phase1_delegation·test_tier1_unit 의 동결 리터럴이 옛 `_decide sub-status`/`exit "$(_decide exit-code` 문자열을 가리킴 | `_decided job_status sub-status "$sub_rc"`, `_exit_for "$job_status"` 로 갱신 | 두 파일 통과(아래) |
## 2. 정상 경로 불변
- rc 형식은 그대로 남아 있다(`--print` 없이 subscriber-up 1 → rc 1 확인; test_u_yes_no_print 가 두 형식을 함께 고정).
- test_f_yes_and_no_keep_their_branches(direct·loop × up/not-up): "yes" 는 진행, "no" 는 "subscriber died early" 경로로 가고 direct 의 종료 코드는 0/1 — 옛 코드에서도 통과(4 passed)하고 새 코드에서도 통과한다 = 분기 결과 불변. 옛·새 비교 테스트(test_c_*) 전부 통과.
- `_decided_yes` 는 `if …; then`(:396, :568)과 `… || return 0`(:730) 안에서 직접 호출되며 서브셸이 아니므로 `exit 1` 이 래퍼를 끝낸다. `--print` 를 위치 인자 뒤에 붙여도 argparse 가 받는다(3.9 에서 직접 확인: `stall-watch remote:r claude --print` → stderr 진단 + stdout `no`, rc 0).
- `stall_watch` 의 진단 메시지는 전과 같이 stderr 로만 나가 stdout 의 한 줄 답을 더럽히지 않는다.
## 3. 정적 가드
test_s_no_printed_decision_is_read_raw 가 `_decide (subscriber-up|stall-watch)` 의 rc 읽기 재유입을 막고 `_decided_yes` 호출 수(2+1)를 고정한다. 옛 코드에서 실패, 새 코드에서 통과.
## 4. 이식성
`/bin/bash -n`(3.2.57) 통과. 추가된 bash 는 `case`·`local`·함수 호출뿐. wrapper.py 변경은 argparse `store_true` 와 print.
## 5. 테스트 실행 (검증 경로 …/2, 워크트리 .venv Python 3.9.6, `env -i` 로 HERDR_*·MAM_*·MQTT_*·WORKSPACE_ROOT 제거)
| 실행 | 결과 |
|---|---|
| test_issue45_restore, test_issue45_restore_unit, test_issue17_item4_decisions, test_issue33_budget, test_issue5_phase1_delegation, test_tier1_unit | **461 passed / 0 failed / 0 skipped** (537 s; 실서버 restore 5건 포함, herdr 0.9.3) |
| 새 테스트 19건, 1회차 head 코드 사본 | **15 failed** / 4 passed (실패 = yes/no 실패 9 + `--print` 단위 5 + 정적 가드; 통과 4 = 분기 불변 테스트, 의도대로) |
격리: 실행 전후 herdr 프로세스 차이는 오케스트레이터의 `agent wait` pid 교체뿐, `/private/tmp/m45*` 없음, `~/.config/herdr` sessions·config.toml 불변, 실제 .mam/jobs 항목 수 불변.
## 6. 비차단
- 1회차 N2(`_env` 가 상속 WORKSPACE_ROOT 를 비우지 않아 `_run_both` 4곳이 에이전트 pane 의 워크스페이스로 lib.sh 를 초기화할 수 있음)와 N3(herdr_restore.py docstring 줄바꿈)는 이 델타에서 다루지 않았다. 둘 다 이 PR 이 만든 문제가 아니며 그대로 비차단.
- 새 관찰 없음.
[VERDICT: PASS]
@@ -0,0 +1,59 @@
# Issue #17 follow-ups — Codex review
Job: 70d302c2
Loop: loop-20261007T224638Z-cf1efb
Base: 56d0bfda54e4f4fe1ccc138cc262edfb14cfb4a3
Head: 8241bd53b179cf8332541264c3bf12b8e3eca44a
Tree: efef640d36f6666f107e188d554c39f05400d4ac
Validation path: /var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261007T224638Z-cf1efb/1
## Finding R1 — subscriber-up decision failures can still report successful submission (P2)
Evidence: .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job:384-395 (direct), :556-567 (loop/discuss); .agents/skills/lib_py/delegation/wrapper.py:332-333.
The new failure handling only protects printed answers. An execution failure in the subscriber-up predicate is consumed as a normal false answer. If the subscriber has already exited 0, the error branch maps that code to completed, finalize-begin keeps it, and _exit_for exits 0. This violates N2's requirement that a failed decision call ends the wrapper with 1, never 0.
I read the exclusion rationale in commit a670ef5: it states that subscriber-up failure treats the subscriber as dead and reaches a fail-closed sub-status/exit-code path. Those later calls can succeed and report completed, so the rationale does not cover a failure confined to the earlier process invocation. This is the same per-decision failure-injection model used by the added regression tests, not an assumption that every later Python invocation must also fail.
Reproduction (no broker or agent session):
1. Use tests/test_issue17_item4_decisions.py::_failing_py with decision="subscriber-up", mode="error" (or "gone").
2. Set SUB_STANDIN=1 and SUB_RC=0. The shim makes only job_subscriber.py return 0 and only the targeted predicate fail; other Python calls remain real.
3. Call _human_submit with a temporary HOME/workspace/registry and that shim as $PY.
4. Expected: submit exits 1 with a decision-failure diagnostic. Actual: module traceback rc 1 -> submit rc 0; missing-interpreter rc 127 -> submit rc 0. The wrapper prints "subscriber died early (... exit=0 ...)" and a log path, then returns success.
Scripts: /tmp/70d302c2-probes.py and /tmp/70d302c2-predicate-errors.py. All created job records are in temporary probe directories, not the real registry.
Fix direction: distinguish a successfully computed boolean from inability to execute the decision. A printed boolean through the existing _decided machinery (possibly a new flag/subcommand retaining the old CLI contract) can return execution status 0 for both valid answers while preserving the normal true/false branches in Bash. Treat nonzero execution status or missing/invalid output as _decision_failed. Do not simply fail on every current rc 1, since false legitimately uses that code. At minimum, rc values outside 0/1 must never be silently treated as false, but that alone cannot fix the reproduced import-error rc 1 case. Add tests for both true and false predicates, plus selectively failing subscriber-up with subscriber rc 0, in both direct and loop/discuss call sites.
The same ambiguity exists at :718 for stall-watch: a probe returning 127 yielded caller rc 0 and continued execution ("survived"), silently disabling the watcher. The creator explicitly describes the watcher as optional/notify-only; this observation is related evidence, while the submission-success counterexample above is sufficient for NOT PASS.
## Other requirements
- Restore premise: tests/test_issue45_restore.py now accepts HOME or the original removed directory. The source docstring distinguishes herdr 0.9.1/0.9.3. The additional production handling for restore_error + pane_not_found is explained by commit f19f940: merely relaxing cwd exposed a placeholder with no process, so closing previously raised before reaching other candidates. The matching session, no-live-agent, non-default-server and normal foreground-shell filters remain.
- Restore regression negative control: executed the new placeholder unit-test function against the base herdr_restore implementation using a stubbed _herdr; it raises CalledProcessError at base and passes at head.
- Printed decisions: the introduced _decided/_exit_for checks cover exit-code, sub-status, finalize-begin, wait-rc and finalize-end process failures. Allowing empty finalize-end output is intentional for OwnerGone, whose CLI handler returns normally without a printed decision. Bash 3.2 dynamic scoping makes printf -v assign the intended caller variables.
- Exit-code negative control: the exact base wrapper placed in a temporary dependency layout with the same failure shim exits 255, while head exits 1. This confirms the added failure test actually detects the original bug.
- N1 accurately documents the package import path to paho; execution code did not change.
- N3 accurately documents PATH python3 and missing-interpreter behavior; executable code did not change.
- Six item-scoped commits, seven changed files, no version bump or VERSIONS entry. No further code defect was identified in the reviewed normal-path handling.
## Verification
- Python 3.9 from the designated issue17-followups/.venv; native /bin/bash 3.2.57.
- With --noconftest, cache disabled, external basetemp and per-test temporary WORKSPACE_ROOT: tests/test_issue17_item4_decisions.py + tests/test_issue45_restore_unit.py -> **281 passed, 4 failed in 292.91s**. All newly added failure-handling and restore unit tests passed. Log: /tmp/70d302c2-selected.log.
- The four failures are existing zombie-owner tests: test_u_an_exited_unreaped_owner_is_not_alive, both test_c_finalize_ends_with_an_unreaped_wrapper parameters, and test_c_an_unreaped_wrapper_leaves_the_real_record_alone. A separate owned-child probe confirmed ps execution raises PermissionError(1, Operation not permitted), causing the existing _alive fallback to return True for an unreaped exited child. The wrapper.py executable AST is identical to base (only its module docstring changed). These sandbox-caused failures are not a new finding against this PR.
- Budget tests with --noconftest and selection "not test_b3 and not test_r3 and not test_b7": **13 passed, 7 deselected in 28.88s**. Log: /tmp/70d302c2-budget.log. This exercises budget propagation, child wait caps, remote deadline labels and closed-unconfirmed classification without needing a reachable broker.
- Explicit loopback bind probe failed with PermissionError(1, Operation not permitted). Consequently no full broker suite result is claimed. Real-herdr restore integration was not run in this sandbox; the orchestrator retains that validation responsibility. Static inspection of its rig confirms disposable HOME/HOME_DIR, mamt45-* session names, scrubbed inherited server selectors and fake agent commands, rather than the user's server/config.
- Both shell files pass /bin/bash -n. All five changed Python files pass AST parsing. git diff --check BASE HEAD passes.
- HEAD and tree match the brief; final git status --porcelain is empty.
- Independent probes reproduced subscriber-up failures returning submit 0 for both module-error rc 1 and interpreter-error rc 127, plus swallowed stall-watch rc 127.
- Negative controls confirmed exit-code failure base 255 -> head 1 and placeholder regression base exception -> head pass.
- The environment restrictions alone do not determine the verdict. R1 is an independently reproduced N2 requirement violation.
## Isolation and lifecycle
All verification ran from the specified review tree. Inherited HERDR_*, MAM_*, MQTT_*, SKS_* and control-plane bindings were cleared; temporary HOME/HOME_DIR/WORKSPACE_ROOT, XDG_CONFIG_HOME and an absent MAM_ENV_FILE were supplied. Tests sourcing lib.sh received a per-test temporary WORKSPACE_ROOT via an environment-only pytest plugin. Direct probes used synthetic jobs and temporary registries. No .mam.env was exported, no live herdr or agent session was used, and no creator/source files, commits or branches were changed. Base comparison artifacts and logs were outside the review tree.
The requested started publication was attempted first; MQTT exhausted three attempts with Operation not permitted and exited 2. The requested completed command was also executed after saving the report; all three MQTT attempts failed with Operation not permitted and it exited 2. The report exists, but broker delivery of started/completed is not confirmed.
[VERDICT: NOT PASS]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261007T224638Z-cf1efb",
"base_sha": "56d0bfda54e4f4fe1ccc138cc262edfb14cfb4a3",
"head_sha": "65b78b6ec44e44904afe34915563522d4631767f",
"tree_sha": "b805d9f9e6639c7359115ea9336eac95d7ab85bc",
"review_commit": "65b78b6ec44e44904afe34915563522d4631767f",
"reviewer": "planner-reviewer-codex-01",
"job_id": "fc553fcd",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-08T01:16:15Z"
}
@@ -0,0 +1,61 @@
# Issue #17 follow-ups, round 2 — Codex review
Job: fc553fcd
Loop: loop-20261007T224638Z-cf1efb
Base: 56d0bfda54e4f4fe1ccc138cc262edfb14cfb4a3
Previous round: 8241bd53b179cf8332541264c3bf12b8e3eca44a
Head: 65b78b6ec44e44904afe34915563522d4631767f
Tree: b805d9f9e6639c7359115ea9336eac95d7ab85bc
Validation path: /var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261007T224638Z-cf1efb/2
## Conclusion
PASS. The prior R1 failure-to-success path is closed. No new blocking defect was found in the cumulative change or the two follow-up commits. This review uses the brief's permitted network-free validation; it does not claim full broker or real-herdr integration execution in this sandbox.
## Previous finding closure
| Case | Previous round | Current head | Result |
| --- | --- | --- | --- |
| subscriber-up execution error, direct submit, subscriber rc 0 | submit exits 0 | submit exits 1 with decision-failure diagnostic | R1 closed |
| Same error in loop submit | submit exits 0 | submit exits 1 | R1 closed |
| stall-watch execution error | submit exits 0 | submit exits 1 | Related ambiguity closed |
| Successful yes/no answers | Truth encoded in process status | --print emits yes/no with process status 0; Bash restores the same branches | Normal semantics preserved |
| Invalid successful predicate stdout in if / OR context | No printed-answer validation | Exit 1, no continuation | Guard verified |
Independent negative controls executed the exact previous-round shell wrapper in a temporary dependency layout and the current wrapper against the same selective module-error shim. All three old/new pairs above were reproduced. The added tests also cover rc 127 and empty successful output. Probe source: /tmp/fc553fcd-probes.py.
Code evidence:
- .agents/skills/lib_py/delegation/wrapper.py:301-309 adds optional --print; :335-353 separates yes/no output from execution status. Without --print, the existing predicate CLI still returns 0/1.
- .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job:112-130 checks execution status, nonempty output and the exact yes/no vocabulary. _decision_failed exits the calling shell explicitly, so the failure is not suppressed by Bash's conditional-context errexit rules.
- Both subscriber branches now use _decided_yes (:396, :568), as does start_stall_watch (:730). The prior unchecked predicate paths are gone.
## Cumulative requirements and scope
The herdr cwd expectation accepts both documented versions; the placeholder handling retains session identity, live-agent and isolated-server filtering. Its unit cases include the valid placeholder, missing restore marker and another process-info error. The implementation and restore tests are unchanged since the prior review, where the new placeholder test was demonstrated to fail on base and pass on head.
Printed decision failures remain normalized to exit 1. The intentional empty finalize-end result for OwnerGone remains allowed. The prior base-vs-head exit-code negative control (255 -> 1) remains applicable because that helper is unchanged in this round.
N1's package-import/paho explanation and N3's PATH python3 explanation remain accurate, with no additional runtime changes. The two stale static test literals now match _decided/_exit_for; both corrected contract tests passed.
The round adds two commits and modifies five files relative to the prior head. AST comparison confirms wrapper.py's existing executable changes since that head are confined to _parser and main, plus the new _yes_no function; timing, owner checks, disk/event handling and finalization are unchanged. The cumulative diff contains nine files, no version bump or VERSIONS entry.
## Validation
- tests/test_issue17_item4_decisions.py and tests/test_issue45_restore_unit.py, --noconftest -k "not unreaped": **299 passed, 4 deselected in 262.30s**. Log: /tmp/fc553fcd-selected.log.
- tests/test_issue33_budget.py, --noconftest -k "not test_b3 and not test_r3 and not test_b7": **13 passed, 7 deselected in 28.77s**. Log: /tmp/fc553fcd-budget.log.
- The two modified static contract tests in test_issue5_phase1_delegation.py and test_tier1_unit.py: **2 passed in 0.16s**, using --noconftest and a minimal temporary mam_sandbox fixture.
- Total executed pytest cases: **314 passed**; 11 explicitly deselected for the documented sandbox/integration limitations.
- Independent previous/head predicate-failure comparisons and invalid-output probes: all assertions passed.
- No product-test failures occurred among the executed cases.
All tests used the designated issue17-followups/.venv Python 3.9, disabled bytecode/cache writes and external basetemp directories. /bin/bash is the native 3.2 implementation. Both changed shell files pass bash -n; all changed Python files pass AST parsing; git diff --check passes. The review HEAD/tree matched the brief, and the final working tree was clean.
## Environment limits and isolation
The inherited sandbox prevents loopback socket binding and ps execution. As diagnosed in the prior round, the four unreaped-owner cases depend on ps; they were explicitly deselected here rather than reported as product failures. Broker-dependent budget cases and real-herdr restore integration remain for the orchestrator, as the brief permits. No claim is made that these omitted integration cases passed in this session.
Inherited HERDR_*, MAM_*, MQTT_*, SKS_* and control-plane binding variables were removed before test/probe execution. HOME, HOME_DIR, WORKSPACE_ROOT, XDG_CONFIG_HOME and absent MAM_ENV_FILE were redirected to temporary paths. The decisions suite used an environment-only pytest plugin to give every test its own WORKSPACE_ROOT. The two static contract tests used a minimal temporary fixture with a read-only link to the review skills and temporary registry data. No live herdr server, real agent session or .mam.env export was used. No source files, commits, branches, PRs or issue states were modified.
The requested started command ran first, but MQTT exhausted three attempts with Operation not permitted and exited 2. Completion publication is attempted after saving the report.
[VERDICT: PASS]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261007T224638Z-cf1efb",
"base_sha": "56d0bfda54e4f4fe1ccc138cc262edfb14cfb4a3",
"head_sha": "65b78b6ec44e44904afe34915563522d4631767f",
"tree_sha": "b805d9f9e6639c7359115ea9336eac95d7ab85bc",
"review_commit": "65b78b6ec44e44904afe34915563522d4631767f",
"reviewer": "reviewer-agy-01",
"job_id": "5a0c16dd",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-08T01:16:15Z"
}
@@ -0,0 +1,93 @@
# PR Review Report: Issue #17 Followups (Round 2 Review)
- **Reviewer Agent**: `agy` (session: `herdr:reviewer-agy-01`)
- **Job ID**: `5a0c16dd`
- **Verification Directory**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261007T224638Z-cf1efb/2`
- **Base Commit**: `56d0bfda54e4f4fe1ccc138cc262edfb14cfb4a3` (main)
- **Head Commit**: `65b78b6ec44e44904afe34915563522d4631767f` (fix/issue17-followups)
- **Cumulative Commits**: 8 commits (`25c9970` ~ `65b78b6`)
- **Delta Under Inspection**: `8241bd5`..`65b78b6` (commits `541c3fe` and `65b78b6`)
---
## 1. Executive Summary
In Round 1 (`67a528ff`), this reviewer returned `[VERDICT: NOT PASS]` due to test regressions in `tests/test_tier1_unit.py:847` and `tests/test_issue5_phase1_delegation.py:390` where frozen literal assertions still expected the pre-refactor `_decide sub-status` and `_decide exit-code` call signatures. Additionally, `codex` noted that boolean decisions (`subscriber-up` and `stall-watch`) that failed at runtime would mistakenly be treated as a negative decision (`"no"`) rather than triggering a fail-closed exit.
In this round, the Creator has completely resolved all issues across commits `541c3fe` and `65b78b6`:
1. Updated `test_tier1_unit.py` and `test_issue5_phase1_delegation.py` to freeze the new helper signatures (`_decided job_status sub-status` and `_exit_for`).
2. Implemented `--print` flag for boolean subcommands in `wrapper.py` and introduced `_decided_yes` in `multi-agent-mux-delegate-job`, ensuring failed yes/no decisions fail-closed with exit code 1.
3. Added comprehensive test coverage in `tests/test_issue17_item4_decisions.py` verifying that failed boolean decisions exit with 1.
All tests across affected and regression suites executed cleanly (**656 passed, 1 skipped in 1147.89s**). The changes are surgical, robust, and fully conform to all behavioral invariance contracts.
---
## 2. Resolution of Round 1 Findings Matrix
| Finding | Origin | Issue & Location | Resolution in Round 2 (`541c3fe`, `65b78b6`) | Status |
|---|---|---|---|---|
| **1. Frozen Literal Assertion in `test_tier1_unit.py`** | `agy` (Round 1) | `tests/test_tier1_unit.py:847` asserted `_decide sub-status "$sub_rc"`, which was replaced by `_decided` in `multi-agent-mux-delegate-job`. | Updated assertion to `_decided job_status sub-status "$sub_rc"` in commit `541c3fe`. | **CLOSED** (Verified) |
| **2. Frozen Literal Assertion in `test_issue5_phase1_delegation.py`** | `agy` (Round 1) | `tests/test_issue5_phase1_delegation.py:390` asserted literals `_decide sub-status "$sub_rc"` and `_decide exit-code "$job_status"`. | Updated assertions to `_decided job_status sub-status "$sub_rc"` and `_exit_for "$job_status"` in commit `541c3fe`. | **CLOSED** (Verified) |
| **3. Failed Boolean Decisions Passing as "No"** | `codex` (Round 1) | In bash, `_decide subscriber-up` or `_decide stall-watch` failing with rc > 0 evaluated as false (`"no"`), bypassing fail-closed error handling and potentially masking failures. | In commit `65b78b6`, added `--print` flag to `wrapper.py` (prints `"yes"`/`"no"` and exits 0) and added `_decided_yes` helper in bash wrapper to enforce fail-closed exit 1 on execution failure. | **CLOSED** (Verified) |
---
## 3. Evaluation of Core Task Requirements
### 3.1. herdr 0.9.3 Compatibility (Item 1)
- **`tests/test_issue45_restore.py:275`**: Accommodates both `herdr 0.9.1` (`$HOME` fallback) and `herdr 0.9.3` (retaining removed directory path with placeholder) via `assert _pane_with(rig, ids["Y"])["cwd"] in (str(rig.root), str(rig.root / "wsY"))`.
- **`lib_py/herdr_restore.py:_nothing_running`**: Handles placeholders restored by `herdr 0.9.3` where `pane process-info` errors with `pane_not_found` and `restore_error` is set, allowing resume to cleanly close dead placeholder panes.
- **`tests/test_issue45_restore_unit.py`**: Stubbed tests `test_a_placeholder_for_a_missing_directory_is_closed` and `test_any_other_process_info_failure_still_stops_the_run` verify that only authentic placeholders are closed, while unexpected errors continue to halt execution.
### 3.2. Fail-Closed on Decision Failure (Item 2 & N2)
- **`_decision_failed()`**: Emits explicit error diagnostic to stderr and terminates with `exit 1`.
- **`_decided()`**: Executes `_decide`, verifies non-zero exit and non-empty output, routing failures to `_decision_failed`.
- **`_decided_yes()`**: Executes `_decide ... --print`, strictly validating that stdout contains `"yes"` (returns 0) or `"no"` (returns 1). Any execution error or unexpected output triggers `_decision_failed`.
- **`_exit_for()`**: Validates that numeric exit code was returned, terminating with `exit "$code"`.
- **`finalize_unfinished` & `cmd_wait`**: Checked return codes on `finalize-end` and validated numeric formatting on `wait-rc`.
- **Coverage**: Verified via extensive unit and integration tests in `tests/test_issue17_item4_decisions.py` covering failure modes (`error`, `gone`, `empty`) across `exit-code`, `sub-status`, `finalize-begin`, `finalize-end`, `subscriber-up`, and `stall-watch`.
### 3.3. Documentation Updates (Items 3 & 4)
- **`lib_py/delegation/wrapper.py:21-29`**: Accurately clarifies that while `wrapper.py` itself imports standard library modules only, executing as a module (`-m lib_py.delegation.wrapper`) triggers package initialization loading `paho-mqtt`.
- **`lib_int.sh:9-11`**: Clearly documents that standalone sourcing executes `python3` from `PATH`.
---
## 4. Comprehensive Test Suite Execution Results
All tests were executed directly in the review verification environment (`/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261007T224638Z-cf1efb/2`):
```bash
AGENT_PYTHON_BIN=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/issue17-followups/.venv/bin/python \
PYTHONDONTWRITEBYTECODE=1 \
/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/issue17-followups/.venv/bin/python \
-m pytest -p no:cacheprovider -q --basetemp=/tmp/pytest-issue17-r2 \
tests/test_tier1_unit.py \
tests/test_issue5_phase1_delegation.py \
tests/test_issue17_item4_decisions.py \
tests/test_issue45_restore.py \
tests/test_issue45_restore_unit.py \
tests/test_issue33_budget.py \
tests/test_issue26_ints.py \
tests/test_issue30_stall_watch.py \
tests/test_issue30_stall_watch_stop.py \
tests/test_issue18_liveness_contract.py \
tests/test_issue14_delivery_ack.py
```
### Results Summary
- **Total Tests**: 657
- **Passed**: 656
- **Skipped**: 1 (environment-specific non-root test)
- **Failed**: 0
- **Duration**: 1147.89s (0:19:07)
- **Exit Code**: `0`
---
## 5. Conclusion & Final Verdict
All blockers from Round 1 have been completely resolved. The codebase is clean, robust, thoroughly covered by deterministic tests, and preserves 100% behavioral invariance.
[VERDICT: PASS]
@@ -0,0 +1,118 @@
# PR Review Report: Issue #17 Followups (PR #51 Post-Merge Fixes)
- **Reviewer Agent**: `agy` (session: `herdr:reviewer-agy-01`)
- **Job ID**: `67a528ff`
- **Verification Directory**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261007T224638Z-cf1efb/1`
- **Base Commit**: `56d0bfda54e4f4fe1ccc138cc262edfb14cfb4a3` (main)
- **Head Commit**: `8241bd53b179cf8332541264c3bf12b8e3eca44a` (fix/issue17-followups)
- **Cumulative Commits**: 6 commits (`25c9970` ~ `8241bd5`)
---
## 1. Executive Summary
This review covers the Issue #17 follow-up work bundle addressing:
1. `herdr 0.9.3` compatibility for removed pane directories (`test_issue45_restore.py`, `lib_py/herdr_restore.py`, `test_issue45_restore_unit.py`).
2. N2: Fail-closed error handling for failed `_decide` calls (preventing `exit ""` -> rc 255 on bash 3.2).
3. N1: Docstring clarification in `wrapper.py` regarding package-level paho import.
4. N3: Comment update in `lib_int.sh` documenting `python3` PATH dependency.
While items 1, 3, and 4 are well-implemented and the new unit tests pass cleanly, **item 2 (commit `a670ef5`) introduces regressions in existing test suites** (`test_tier1_unit.py` and `test_issue5_phase1_delegation.py`). The commit replaced raw `_decide` subshell invocations with helper functions `_decided` and `_exit_for` in `multi-agent-mux-delegate-job`, but failed to update the frozen literal assertions in the existing test suite, causing 2 test failures.
Therefore, the verdict for this round is **NOT PASS**.
---
## 2. Evaluation of Task Requirements
| Item | Description | Implementation Status | Assessment |
|---|---|---|---|
| **1. herdr 0.9.3 Compatibility** | `test_b_only_the_resumed_sessions_restored_shell_is_closed` failed on herdr 0.9.3 because removed pane directories retain their old path rather than resetting to `$HOME`. `lib_py/herdr_restore.py` must close placeholders with `restore_error` and `pane_not_found`. | `tests/test_issue45_restore.py:275`, `.agents/skills/lib_py/herdr_restore.py:130-138`, `tests/test_issue45_restore_unit.py:424-463` | **PASS**: Correctly handles both herdr 0.9.1 and 0.9.3 behavior; unit tests and full restore test pass without touching live sessions. |
| **2. N2 Fail-Closed on `_decide` Failure** | When `_decide` fails, `exit "$(_decide exit-code ...)"` evaluated to `exit ""` (rc 255 in bash 3.2). The wrapper must exit 1 (fail-closed) on failure, never 255, 0, or empty string. | `multi-agent-mux-delegate-job:100-128`, `tests/test_issue17_item4_decisions.py:309-382` | **NOT PASS (Test Regression)**: Logic in `multi-agent-mux-delegate-job` is sound, but broke existing freeze tests in `test_tier1_unit.py` and `test_issue5_phase1_delegation.py`. |
| **3. N1 Docstring Clarification** | `wrapper.py` docstring claimed "Stdlib only", but running via `-m lib_py.delegation.wrapper` loads `lib_py/delegation/__init__.py` and transitively requires `paho`. | `lib_py/delegation/wrapper.py:21-29` | **PASS**: Accurate docstring explanation added. |
| **4. N3 `lib_int.sh` PATH Comment** | Document that `lib_int.sh` uses `python3` from PATH. | `lib_int.sh:9-11` | **PASS**: Comment added cleanly without code churn. |
---
## 3. Test Failures & Detailed Findings
Running the regression suite in the review verification directory (`/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261007T224638Z-cf1efb/1`):
```bash
AGENT_PYTHON_BIN=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/issue17-followups/.venv/bin/python \
PYTHONDONTWRITEBYTECODE=1 \
/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/issue17-followups/.venv/bin/python \
-m pytest -p no:cacheprovider -q --basetemp=/tmp/pytest-failed-inspect \
tests/test_tier1_unit.py::test_g10_delegate_job_rc3_not_mistaken_for_error \
tests/test_issue5_phase1_delegation.py::test_wrapper_exit_code_mapping_literals_are_frozen
```
### Failure 1: `tests/test_tier1_unit.py:847`
- **Test**: `test_g10_delegate_job_rc3_not_mistaken_for_error`
- **Failure**:
```python
> assert '_decide sub-status "$sub_rc"' in delegate_script.read_text()
E AssertionError: assert '_decide sub-status "$sub_rc"' in ...
```
- **Cause**: Commit `a670ef5` replaced line 405 (and line 574) of `multi-agent-mux-delegate-job`:
```bash
# Before (base 56d0bfd):
job_status="$(_decide sub-status "$sub_rc" --job "$JOB_ID" --registry-dir "$REGISTRY_DIR")"
# After (commit a670ef5):
_decided job_status sub-status "$sub_rc" --job "$JOB_ID" --registry-dir "$REGISTRY_DIR"
```
Because `_decide sub-status "$sub_rc"` no longer appears literally in `multi-agent-mux-delegate-job`, the assertion in `test_tier1_unit.py:847` fails.
### Failure 2: `tests/test_issue5_phase1_delegation.py:390`
- **Test**: `test_wrapper_exit_code_mapping_literals_are_frozen`
- **Failure**:
```python
> for literal in ('_decide sub-status "$sub_rc"', '_decide exit-code "$job_status"', "--sub-rc \"$sub_rc\""):
> assert literal in text, literal
E AssertionError: _decide sub-status "$sub_rc"
```
- **Cause**: Commit `a670ef5` replaced all occurrences of:
- `_decide sub-status "$sub_rc"` with `_decided job_status sub-status "$sub_rc"`
- `exit "$(_decide exit-code "$job_status")"` with `_exit_for "$job_status"`
Neither literal is present in the wrapper script anymore, causing the freeze verification in `test_issue5_phase1_delegation.py:390` to fail.
---
## 4. Required Fixes for Next Round
In worktree `/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/issue17-followups`:
1. **Update `tests/test_tier1_unit.py:847`**:
Change:
```python
assert '_decide sub-status "$sub_rc"' in delegate_script.read_text()
```
To match the new helper signature:
```python
assert '_decided job_status sub-status "$sub_rc"' in delegate_script.read_text()
```
2. **Update `tests/test_issue5_phase1_delegation.py:390`**:
Change:
```python
for literal in ('_decide sub-status "$sub_rc"', '_decide exit-code "$job_status"', "--sub-rc \"$sub_rc\""):
assert literal in text, literal
```
To match the new helper signatures:
```python
for literal in ('_decided job_status sub-status "$sub_rc"', '_exit_for "$job_status"', "--sub-rc \"$sub_rc\""):
assert literal in text, literal
```
3. **Verify Full Test Suite**:
Run:
```bash
AGENT_PYTHON_BIN=.venv/bin/python PYTHONDONTWRITEBYTECODE=1 .venv/bin/python -m pytest -p no:cacheprovider -q tests/test_tier1_unit.py tests/test_issue5_phase1_delegation.py tests/test_issue17_item4_decisions.py tests/test_issue45_restore.py tests/test_issue45_restore_unit.py tests/test_issue33_budget.py
```
Confirm all tests pass without errors.
---
## 5. Final Verdict
[VERDICT: NOT PASS]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261007T224638Z-cf1efb",
"base_sha": "56d0bfda54e4f4fe1ccc138cc262edfb14cfb4a3",
"head_sha": "65b78b6ec44e44904afe34915563522d4631767f",
"tree_sha": "b805d9f9e6639c7359115ea9336eac95d7ab85bc",
"review_commit": "65b78b6ec44e44904afe34915563522d4631767f",
"reviewer": "reviewer-grok-01",
"job_id": "518e45d3",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-08T01:16:15Z"
}
@@ -0,0 +1,36 @@
# Review: Issue #17 follow-ups @ 65b78b6
- Role: Reviewer (grok)
- Identity: loop `loop-20261007T224638Z-cf1efb`, snapshot `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261007T224638Z-cf1efb/2`, base `56d0bfda54e4f4fe1ccc138cc262edfb14cfb4a3` (main still `56d0bfd`), head `65b78b6ec44e44904afe34915563522d4631767f`, tree `b805d9f9e6639c7359115ea9336eac95d7ab85bc`. `8241bd5` is an ancestor. Snapshot porcelain empty. `git diff --check 56d0bfd..HEAD` clean.
- Delta since the PASS at `8241bd5`: `541c3fe` and `65b78b6`, five files, +80/−16. Cumulative base..head: nine files, +267/−33. No version bump and no `VERSIONS.md` edit.
- Verified read-only at the snapshot. Python 3.9.6 from the follow-up worktree `.venv`, bash 3.2.57, herdr 0.9.3. Inherited `HERDR_*`, `MAM_*`, `MQTT_*`, and `WORKSPACE_ROOT` were unset. `HOME` and `WORKSPACE_ROOT` were the basetemp. `.mam.env` was not exported. `started` for this dispatch is `dispatch_verified` with attempt-id 1.
## A failed yes/no decision exits 1
`wrapper.py` `_yes_no` (`:335-339`) prints `yes` or `no` and returns 0 when `--print` is set. Without `--print` it returns 0 for yes and 1 for no. `subscriber-up` and `stall-watch` both take `--print` (`:301-309`) and return through `_yes_no` (`:346-353`). The module head (`:15-17`) says the wrapper uses `--print`, so a failure to decide cannot pass for no.
`_decided_yes` (`multi-agent-mux-delegate-job:123-131`) calls `_decided` with `--print`. `yes` returns 0, `no` returns 1, and any other text calls `_decision_failed` (`:106-109`), which `exit 1`. `_decided` (`:112-118`) already exits 1 when the command status is non-zero or stdout is empty, before that case.
The handshake uses `if _decided_yes subscriber-up "$sub_exit"` at `:396` and `:568`. `start_stall_watch` (`:730`) is `_decided_yes stall-watch "$session" "$agent" || return 0`. A real `no` still returns 1, so the watcher is skipped and the subscriber "died early" branch still runs. A failed decision executes `exit 1` inside `_decision_failed`, so that `|| return 0` and the `if` leave the wrapper at 1.
`test_f_a_failed_yes_no_decision_ends_the_submit_with_1` (`tests/test_issue17_item4_decisions.py:693-699`) covers modes error, gone, and empty for subscriber-up on direct and loop submits, and for stall-watch on a direct submit. The stand-in subscriber exits 0 before `SUBSCRIBED`. Each run expects rc 1 and `could not decide '<decision>'`.
`test_f_yes_and_no_keep_their_branches` (`:702-710`) keeps the live branches. `SUB_RC` 0 stays on the yes path: "subscriber died early" is absent, and a direct submit exits 0. `SUB_RC` 1 takes the died-early path, and a direct submit exits 1. The direct rc 0 also shows that the later `wait "$sub_pid"` (`:437-438`), after the handshake already waited that pid, still finishes the submit on this bash 3.2.57.
`test_u_yes_no_print` (`:713-721`) checks `--print` stdout `yes`/`no` with rc 0, the rc form of subscriber-up still 0 or 1, stall-watch `herdr:c`/`claude` printing yes, and `remote:r`/`claude` printing no.
`test_s_no_printed_decision_is_read_raw` (`:734-743`) drops comment lines, then requires `_decided_yes subscriber-up` twice and `_decided_yes stall-watch` once, and rejects a raw `_decide subscriber-up` or `_decide stall-watch`. The comment at `:725` still names `_decide stall-watch`. The call that runs is `:730`.
## Frozen literals
`541c3fe` points `tests/test_issue5_phase1_delegation.py:390` and `tests/test_tier1_unit.py:847` at `_decided job_status sub-status "$sub_rc"`. The issue5 pin also requires `_exit_for "$job_status"`. `EXIT_CODES` and `sub_status()` stay asserted in Python (`test_issue5_phase1_delegation.py:393-395`).
## Items already passed at 8241bd5
The herdr 0.9.3 placeholder close, the N1 docstring, the N3 PATH comment, and the printed-decision `_decided` / `_exit_for` path are absent from `git diff 8241bd5 HEAD`. That review recorded 305 passed (`test_issue17_item4_decisions.py`, `test_issue33_budget.py`, `test_issue45_restore_unit.py`) and 5 passed (`tests/test_issue45_restore.py`). After this review, `~/.config/herdr/config.toml` mtime is still `1784456394` and the directory mtime is still `1791382452`.
## Tests this round
`--basetemp=/tmp/518efollow.hbS1ua`. `tests/test_issue17_item4_decisions.py`, `tests/test_issue33_budget.py`, `tests/test_issue5_phase1_delegation.py`, `tests/test_tier1_unit.py`: **442 passed** in 472.25 s. Snapshot porcelain stayed empty. Main stayed `56d0bfd`.
[VERDICT: PASS]
@@ -0,0 +1,44 @@
# Review: Issue #17 follow-ups @ 8241bd5
- Role: Reviewer (grok)
- Identity: loop `loop-20261007T224638Z-cf1efb`, base `56d0bfda54e4f4fe1ccc138cc262edfb14cfb4a3` (main), head `8241bd53b179cf8332541264c3bf12b8e3eca44a`, tree `efef640d36f6666f107e188d554c39f05400d4ac`, six commits, clean snapshot
- Scope: `git diff 56d0bfd HEAD`. Seven files, +192/−22, `git diff --check` clean. No version bump and no `VERSIONS.md` edit. Main stayed `56d0bfd`.
- Verified read-only at `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261007T224638Z-cf1efb/1`. Python 3.9.6 from the follow-up worktree venv, bash 3.2.57, herdr 0.9.3. Inherited `HERDR_*`, `MAM_*`, and `MQTT_*` were unset. `HOME` and `WORKSPACE_ROOT` were the basetemp. `.mam.env` was not exported.
## 1. herdr 0.9.3 restored cwd
`test_b_only_the_resumed_sessions_restored_shell_is_closed` accepts Y's cwd as either `rig.root` or `rig.root/wsY` (`tests/test_issue45_restore.py:275-277`). The comment names 0.9.1 (`$HOME`) and 0.9.3 (the old path). The close still does not read `cwd`.
`herdr_restore.py:4-20` says the same thing. `SKILL.md` describes the close as "same session id, no agent, nothing running" and does not claim the missing directory becomes `$HOME`. The historical `VERSIONS.md` line is outside this change, as the task asked.
On 0.9.3 a removed directory comes back as a placeholder: `restore_error` is set and `pane process-info` fails with `pane_not_found`. `_nothing_running` (`herdr_restore.py:58-65`) closes that pane. Any other `CalledProcessError` still propagates, so the run stops and the pane is not closed. A process-info that succeeds still uses `_shell_in_foreground` (`:50-55`): a real shell pid in the foreground, and nothing else. The unit tests cover the placeholder, `pane_not_found` without `restore_error`, and a different error (`tests/test_issue45_restore_unit.py:447-462`).
On this Mac the whole `tests/test_issue45_restore.py` file passed, including Y being closed and X's normal empty shell still being the one that closes, while the user's shell, the live agent, and the busy shell stay. The fixture's HOME is the throwaway rig (`test_issue45_restore.py:5-6`, `:179`). `~/.config/herdr/config.toml` mtime stayed `1784456394` and the directory mtime stayed `1791382452`.
## 2. A failed decision exits 1
`_decided` (`multi-agent-mux-delegate-job:112-118`) keeps the printed answer only when the command exits 0 and the answer is non-empty. Otherwise `_decision_failed` (`:106-109`) prints the subcommand and `exit 1`. `_exit_for` (`:122-126`) also requires a decimal answer before `exit`. `cmd_wait` uses the same two checks (`:1070-1071`). `finalize-end` keeps its fd-3 swap and treats a non-zero status as the same failure (`:852-857`). An empty answer with status 0 is still accepted there, which is the path that leaves the job unclosed when the wrapper is already gone.
`printf -v` and `[[ =~ ^[0-9]+$ ]]` are valid on this bash 3.2.57. The assignment is not inside `local`, so a failing command substitution is visible to the `||` branch.
Sourced from base `56d0bfd`, the same human submit with a broken `exit-code` ended at the old line 414: `exit: : numeric argument required`, rc **255**, after a normal wall-clock close. The new `_exit_for` on that failure, on an empty answer, and on `wait-rc` rc 127 each exits **1** with `could not decide`.
`subscriber-up` stays in `if _decide subscriber-up` (`:358`, `:530`). A non-zero result is "not up", and the following `_decided sub-status` exits 1 when the module itself is broken. `stall-watch` stays `_decide stall-watch … || return 0` (`:692` area, unchanged). A failed watch decision skips the watcher and the job continues; the watcher never closes a job, and turning that skip into `exit 1` would fail a job that 7209750 and 56d0bfd would still run. The static guard `test_s_no_printed_decision_is_read_raw` allows only the `_decided` capture and the `finalize-end` capture, and forbids `exit "$(`.
Successful decisions are unchanged: the old-bash comparison in `tests/test_issue17_item4_decisions.py` passed, and `test_f_the_normal_path_is_unchanged` gets rc 3, the audit-log path on the last stdout line, and no `could not decide`.
## 3. "Stdlib only"
`wrapper.py:24-31` now says this module imports the standard library, and `python -m lib_py.delegation.wrapper` imports the package first. `lib_py/delegation/__init__.py:19-20` imports `service`, and `service.py` imports `mqtt_common`, which imports `paho.mqtt.client`. The wrapper's `$PY` already needed paho for the subscriber and `publish_event.py`. The command line is still `_decide` at `multi-agent-mux-delegate-job:99-101`.
## 4. `python3` on PATH
`lib_int.sh:9-10` records that `mam_int` and `mam_int_knob` run `python3` from PATH and, without one, print nothing and return 127. The function bodies are still the two `python3 -I` calls (`:17-18`). No caller was switched to `$PY`.
## Tests
`--basetemp=/tmp/a652follow.8mWeKg`. `tests/test_issue17_item4_decisions.py`, `tests/test_issue33_budget.py`, `tests/test_issue45_restore_unit.py`: **305 passed** in 360.13 s.
`--basetemp=/tmp/a652herdr.CltSYm`. `tests/test_issue45_restore.py`: **5 passed** in 65.96 s. Snapshot `git status` was empty after both.
[VERDICT: PASS]
+1 -1
View File
@@ -29,7 +29,7 @@ export WORKSPACE_ROOT
# Framework semantic version. Single runtime source of truth; kept in lockstep # Framework semantic version. Single runtime source of truth; kept in lockstep
# with VERSIONS.md and the 8 SKILL.md frontmatters by tests/test_version_consistency.py. # with VERSIONS.md and the 8 SKILL.md frontmatters by tests/test_version_consistency.py.
# NOTE: unlike other MAM_* variables this one is intentionally NOT env-overridable. # NOTE: unlike other MAM_* variables this one is intentionally NOT env-overridable.
MAM_VERSION="4.8.6" MAM_VERSION="4.8.7"
export MAM_VERSION export MAM_VERSION
AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}" AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}"
+1 -1
View File
@@ -13,7 +13,7 @@ import sys
from typing import List, Sequence from typing import List, Sequence
# Framework version locked with framework constants # Framework version locked with framework constants
FRAMEWORK_VERSION = "4.8.6" FRAMEWORK_VERSION = "4.8.7"
CORE_MODULES: Sequence[str] = ( CORE_MODULES: Sequence[str] = (
# Standard library modules # Standard library modules
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-create name: multi-agent-mux-create
description: "Create a new agent session (claude, antigravity/agy) in a dedicated herdr session for context-preserving long-running work. Always creates a herdr session — never backgrounds with nohup/disown. Writes the new session to .mam/agent-sessions.yaml. Use when you want to start a fresh agent (no prior UUID) for a new project workspace." description: "Create a new agent session (claude, antigravity/agy) in a dedicated herdr session for context-preserving long-running work. Always creates a herdr session — never backgrounds with nohup/disown. Writes the new session to .mam/agent-sessions.yaml. Use when you want to start a fresh agent (no prior UUID) for a new project workspace."
version: 4.8.6 version: 4.8.7
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-delegate-job name: multi-agent-mux-delegate-job
description: "Delegate a unit of work to any autonomous agent (claude-code, hermes, agy, grok-build, codex, or a human) and observe it asynchronously over an MQTT event channel. Supported roles include orchestrator, worker, and reviewer." description: "Delegate a unit of work to any autonomous agent (claude-code, hermes, agy, grok-build, codex, or a human) and observe it asynchronously over an MQTT event channel. Supported roles include orchestrator, worker, and reviewer."
version: 4.8.6 version: 4.8.7
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos, windows] platforms: [linux, macos, windows]
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-loop name: multi-agent-mux-loop
description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached." description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached."
version: 4.8.6 version: 4.8.7
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-monitor name: multi-agent-mux-monitor
description: "Run a long-lived reconciler that watches .mam/agent-sessions.yaml against the actual herdr/agent runtime state and reconciles them. Use when you want live visibility into which agent sessions are running, which are dead, which have stale YAML entries, and which have new session ids that haven't been recorded yet. Runs as a persistent loop (`reconcile.sh --subscribe`) that keeps going until it times out, idles out, or is interrupted." description: "Run a long-lived reconciler that watches .mam/agent-sessions.yaml against the actual herdr/agent runtime state and reconciles them. Use when you want live visibility into which agent sessions are running, which are dead, which have stale YAML entries, and which have new session ids that haven't been recorded yet. Runs as a persistent loop (`reconcile.sh --subscribe`) that keeps going until it times out, idles out, or is interrupted."
version: 4.8.6 version: 4.8.7
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-orc-onboard name: multi-agent-mux-orc-onboard
description: "Register current or specified orchestrator session UUID into agent-sessions.yaml orchestrator_uuids list to prevent sub-agent discovery capture." description: "Register current or specified orchestrator session UUID into agent-sessions.yaml orchestrator_uuids list to prevent sub-agent discovery capture."
version: 4.8.6 version: 4.8.7
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-resume name: multi-agent-mux-resume
description: "Resume an existing agent (claude, antigravity/agy) conversation by UUID into a herdr session. Reads .mam/agent-sessions.yaml for the saved session/conversation id, spawns (or reuses) a herdr session of the matching name, and runs `claude -r <id>` or `agy --conversation <id>` inside. Use when you want to reattach to a previous session's context, or revive a session whose herdr died but the agent's conversation is still on disk." description: "Resume an existing agent (claude, antigravity/agy) conversation by UUID into a herdr session. Reads .mam/agent-sessions.yaml for the saved session/conversation id, spawns (or reuses) a herdr session of the matching name, and runs `claude -r <id>` or `agy --conversation <id>` inside. Use when you want to reattach to a previous session's context, or revive a session whose herdr died but the agent's conversation is still on disk."
version: 4.8.6 version: 4.8.7
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-status name: multi-agent-mux-status
description: "Read-only instant snapshot of all agent herdr sessions — name, YAML status, herdr alive, pane cmd/cwd, resume UUID on disk, and any drift. No mutation. Reuses reconcile.sh --dry-run for the diff logic. Use when you want to know 'what's running RIGHT NOW' without spinning up the monitor loop." description: "Read-only instant snapshot of all agent herdr sessions — name, YAML status, herdr alive, pane cmd/cwd, resume UUID on disk, and any drift. No mutation. Reuses reconcile.sh --dry-run for the diff logic. Use when you want to know 'what's running RIGHT NOW' without spinning up the monitor loop."
version: 4.8.6 version: 4.8.7
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
name: multi-agent-mux-stop name: multi-agent-mux-stop
description: "Stop an agent herdr session (claude, antigravity/agy) and update .mam/agent-sessions.yaml. Default stops gracefully and marks status=stopped with conversation preserved for resume. Does NOT delete on-disk conversation artifacts (jsonl/db) — those are preserved unless --purge-conversation is passed. Use when ending a work session, switching to a different one, or cleaning up before a fresh start." description: "Stop an agent herdr session (claude, antigravity/agy) and update .mam/agent-sessions.yaml. Default stops gracefully and marks status=stopped with conversation preserved for resume. Does NOT delete on-disk conversation artifacts (jsonl/db) — those are preserved unless --purge-conversation is passed. Use when ending a work session, switching to a different one, or cleaning up before a fresh start."
version: 4.8.6 version: 4.8.7
author: godopu author: godopu
license: MIT license: MIT
platforms: [linux, macos] platforms: [linux, macos]
+1 -1
View File
@@ -2,7 +2,7 @@
본 문서는 `herdr_agent_orchestration` 오케스트레이션 및 메시징 백플레인 워크플로우를 새로운 프로젝트에 도입하여 이식하고, 새로운 개발자/에이전트가 초기 가동을 시작할 때 수행해야 하는 환경 설정 및 구축 절차를 안내합니다. 본 문서는 `herdr_agent_orchestration` 오케스트레이션 및 메시징 백플레인 워크플로우를 새로운 프로젝트에 도입하여 이식하고, 새로운 개발자/에이전트가 초기 가동을 시작할 때 수행해야 하는 환경 설정 및 구축 절차를 안내합니다.
[![Version: v4.8.6](https://img.shields.io/badge/version-v4.8.6-blue.svg)](VERSIONS.md) [![Version: v4.8.7](https://img.shields.io/badge/version-v4.8.7-blue.svg)](VERSIONS.md)
새로운 에이전트는 이 안내서에 기술된 절차를 순차적으로 실행하여 초기 환경을 안정적으로 설정할 수 있습니다. 새로운 에이전트는 이 안내서에 기술된 절차를 순차적으로 실행하여 초기 환경을 안정적으로 설정할 수 있습니다.
+1 -1
View File
@@ -2,7 +2,7 @@
This document guides you through the setup and initialization procedures required to adopt the `herdr_agent_orchestration` orchestration and messaging backplane workflow in a new project, enabling a new developer or agent to get up and running quickly. This document guides you through the setup and initialization procedures required to adopt the `herdr_agent_orchestration` orchestration and messaging backplane workflow in a new project, enabling a new developer or agent to get up and running quickly.
[![Version: v4.8.6](https://img.shields.io/badge/version-v4.8.6-blue.svg)](VERSIONS.md) [![Version: v4.8.7](https://img.shields.io/badge/version-v4.8.7-blue.svg)](VERSIONS.md)
A new agent can follow the steps in this guide sequentially to establish a stable and reliable initial environment. A new agent can follow the steps in this guide sequentially to establish a stable and reliable initial environment.
+1 -1
View File
@@ -1,6 +1,6 @@
# Messaging System Technical Analysis & Architecture Report # Messaging System Technical Analysis & Architecture Report
Applies to: MAM v4.8.6 Applies to: MAM v4.8.7
This report provides a comprehensive, deep-dive analysis of the messaging system implemented in the `multi-agent-mux-delegate-job` skill. It covers the MQTT broker architecture, event protocols, job lifecycles, codebase internals, cross-system integration, and a list of known limitations along with production recommendations. This report provides a comprehensive, deep-dive analysis of the messaging system implemented in the `multi-agent-mux-delegate-job` skill. It covers the MQTT broker architecture, event protocols, job lifecycles, codebase internals, cross-system integration, and a list of known limitations along with production recommendations.
+1 -1
View File
@@ -2,7 +2,7 @@
Herdr와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이전트 오케스트레이션 및 메시징 백플레인** 프레임워크입니다. `claude`, `agy`, `hermes`, `grok`, `opencode`, `codex` 등 다중 LLM 백엔드 에이전트 전반에 걸쳐 장시간 수행되는 작업(코드 생성, 리팩토링, 보안 검토 등)을 조정, 격리 및 감사할 수 있도록 설계되었습니다. Herdr와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이전트 오케스트레이션 및 메시징 백플레인** 프레임워크입니다. `claude`, `agy`, `hermes`, `grok`, `opencode`, `codex` 등 다중 LLM 백엔드 에이전트 전반에 걸쳐 장시간 수행되는 작업(코드 생성, 리팩토링, 보안 검토 등)을 조정, 격리 및 감사할 수 있도록 설계되었습니다.
[![Version: v4.8.6](https://img.shields.io/badge/version-v4.8.6-blue.svg)](VERSIONS.md) [![Version: v4.8.7](https://img.shields.io/badge/version-v4.8.7-blue.svg)](VERSIONS.md)
--- ---
+1 -1
View File
@@ -2,7 +2,7 @@
An advanced, high-reliability **Multi-Agent Orchestration & Messaging Backplane** framework built on Herdr and MQTT. It is designed to coordinate, isolate, and audit long-running agent tasks (such as code generation, refactoring, and security reviews) across multiple LLM backend clients (`claude`, `agy`, `hermes`, `grok`, `opencode`, `codex`). An advanced, high-reliability **Multi-Agent Orchestration & Messaging Backplane** framework built on Herdr and MQTT. It is designed to coordinate, isolate, and audit long-running agent tasks (such as code generation, refactoring, and security reviews) across multiple LLM backend clients (`claude`, `agy`, `hermes`, `grok`, `opencode`, `codex`).
[![Version: v4.8.6](https://img.shields.io/badge/version-v4.8.6-blue.svg)](VERSIONS.md) [![Version: v4.8.7](https://img.shields.io/badge/version-v4.8.7-blue.svg)](VERSIONS.md)
--- ---
+29 -10
View File
@@ -6,7 +6,7 @@
## 📌 현재 버전 개요 (Current Release) ## 📌 현재 버전 개요 (Current Release)
- **프레임워크 버전**: `v4.8.6` - **프레임워크 버전**: `v4.8.7`
- **최신 릴리스 일시**: 2026-10-08 (KST) - **최신 릴리스 일시**: 2026-10-08 (KST)
- **기준 브랜치**: `main` - **기준 브랜치**: `main`
- **핵심 아키텍처**: - **핵심 아키텍처**:
@@ -24,23 +24,42 @@
## 🧭 스킬 패키지 버전 매트릭스 (Skills Version Matrix) ## 🧭 스킬 패키지 버전 매트릭스 (Skills Version Matrix)
모든 8개 스킬은 YAML frontmatter 메타데이터(`author`, `version`, `platforms`, `environments`) 표준화를 통해 `v4.8.6`로 동기화되어 배포됩니다. 모든 8개 스킬은 YAML frontmatter 메타데이터(`author`, `version`, `platforms`, `environments`) 표준화를 통해 `v4.8.7`로 동기화되어 배포됩니다.
| 스킬명 | 버전 | 역할 및 주요 책임 | 상태 | | 스킬명 | 버전 | 역할 및 주요 책임 | 상태 |
| :--- | :---: | :--- | :---: | | :--- | :---: | :--- | :---: |
| **`multi-agent-mux-create`** | `4.8.6` | 에이전트 세션 신규 생성 및 Herdr 컨테이너 격리 스폰 | ✅ 배포 | | **`multi-agent-mux-create`** | `4.8.7` | 에이전트 세션 신규 생성 및 Herdr 컨테이너 격리 스폰 | ✅ 배포 |
| **`multi-agent-mux-stop`** | `4.8.6` | 대화 UUID 원자적 캡처 및 세션 안전 종료 (Graceful Stop) | ✅ 배포 | | **`multi-agent-mux-stop`** | `4.8.7` | 대화 UUID 원자적 캡처 및 세션 안전 종료 (Graceful Stop) | ✅ 배포 |
| **`multi-agent-mux-resume`** | `4.8.6` | 온디스크 대화 컨텍스트 기반 Tier-1 초고속 세션 복원 | ✅ 배포 | | **`multi-agent-mux-resume`** | `4.8.7` | 온디스크 대화 컨텍스트 기반 Tier-1 초고속 세션 복원 | ✅ 배포 |
| **`multi-agent-mux-status`** | `4.8.6` | 실시간 Herdr 세션 및 레지스트리 드리프트 스냅샷 조회 | ✅ 배포 | | **`multi-agent-mux-status`** | `4.8.7` | 실시간 Herdr 세션 및 레지스트리 드리프트 스냅샷 조회 | ✅ 배포 |
| **`multi-agent-mux-monitor`** | `4.8.6` | YAML ↔ 런타임 상태 간 자율 조정자 (Reconciler Loop) | ✅ 배포 | | **`multi-agent-mux-monitor`** | `4.8.7` | YAML ↔ 런타임 상태 간 자율 조정자 (Reconciler Loop) | ✅ 배포 |
| **`multi-agent-mux-delegate-job`** | `4.8.6` | MQTT 이벤트 채널 기반 비동기 단위 작업 위임 | ✅ 배포 | | **`multi-agent-mux-delegate-job`** | `4.8.7` | MQTT 이벤트 채널 기반 비동기 단위 작업 위임 | ✅ 배포 |
| **`multi-agent-mux-loop`** | `4.8.6` | Planner-Creator-Reviewer 3자 자율 계획·실행·피어리뷰 루프 | ✅ 배포 | | **`multi-agent-mux-loop`** | `4.8.7` | Planner-Creator-Reviewer 3자 자율 계획·실행·피어리뷰 루프 | ✅ 배포 |
| **`multi-agent-mux-orc-onboard`** | `4.8.6` | 오케스트레이터 UUID 격리 등록 및 서브 세션 오염 방지 | ✅ 배포 | | **`multi-agent-mux-orc-onboard`** | `4.8.7` | 오케스트레이터 UUID 격리 등록 및 서브 세션 오염 방지 | ✅ 배포 |
--- ---
## 📋 버전별 상세 변경 내역 (Changelog) ## 📋 버전별 상세 변경 내역 (Changelog)
### 🚀 `v4.8.7` — Issue #17 후속: 래퍼가 판정을 못 내리면 1 로 끝나고, resume 이 herdr 0.9.3 의 자리표시 pane 을 닫는다 (2026-10-08)
> **패치 (PATCH Release)**: v4.8.6(PR #51) 리뷰가 남긴 후속. 판정 모듈이 실패할 때의 래퍼 종료 코드, herdr 0.9.3 에서 드러난 #45 B 의 빈틈, 문서 두 곳. 정상 경로의 종료 코드·출력·레지스트리·이벤트는 같다.
#### 🌟 주요 개선 사항
1. **판정 실패는 1 (`multi-agent-mux-delegate-job`, `lib_py/delegation/wrapper.py`)**: `exit "$(_decide exit-code …)"` 는 판정 모듈이나 `$PY` 가 깨지면 `exit ""` → bash 3.2 에서 255 였고, 다른 판정도 저마다 다르게 실패했다(wait-rc 255, finalize-begin 실패 시 finalize-end 의 argparse 2, set -e 아래 인터프리터 자신의 코드 1·127). 이제 값을 내는 판정은 `_decided`(0 이 아닌 rc·빈 답이면 실패), 종료는 `_exit_for`(숫자가 아니면 실패)를 거치고, 실패는 `_decision_failed` 가 `ERROR:` 한 줄과 함께 1 로 끝낸다(fail-closed).
2. **예/아니오 판정 (리뷰 codex R1)**: `subscriber-up`·`stall-watch` 는 rc 로만 답해, 실행조차 못 한 호출(import 오류 rc 1, 인터프리터 없음 127)이 "아니오"로 읽혔다 — 0 으로 끝난 구독자에서 제출이 0(completed)으로 끝났고, stall-watch 실패는 감시를 조용히 껐다. 두 서브커맨드는 `--print` 로 `yes`/`no` 를 출력하고(rc 0), 래퍼는 `_decided_yes` 로 묻는다. 그 밖의 답·실패는 1 로 끝난다. `--print` 없는 rc 계약(예 0, 아니오 1)은 그대로.
3. **herdr 0.9.3 의 자리표시 pane (`lib_py/herdr_restore.py`)**: herdr 0.9.3 은 저장된 디렉터리가 사라진 pane 을 `$HOME` 의 셸이 아니라 원래 cwd 의 자리표시(`restore_error` "Saved directory is unavailable…", 프로세스 없음)로 복원하고, 그 pane 의 `pane process-info` 는 `pane_not_found` 로 답한다. `close_restored_shells` 는 여기서 예외로 exit 1 하며 다른 pane 을 보지 않아, #45 B 가 닫아야 할 지워진 워크트리의 pane 이 남았다. `restore_error` 가 있고 **그리고** `pane_not_found` 인 경우만 "실행 중인 것 없음"으로 보고 닫는다. 다른 process-info 실패는 전처럼 exit 1. docstring 도 버전별 복원 위치로 정정.
4. **테스트 전제 (`tests/test_issue45_restore.py`)**: 지워진 cwd 의 복원 위치를 herdr 0.9.1(`$HOME`)·0.9.3(원래 경로) 둘 다 받는다.
5. **문서**: `wrapper.py` 머리말 — 모듈은 stdlib 만 import 하지만 `python -m lib_py.delegation.wrapper` 는 패키지 `__init__` 을 거쳐 paho 를 싣는다(N1). `lib_int.sh` — 단독 source 될 때 PATH 의 `python3` 를 쓴다(N3).
#### ✅ 품질 검증
- **새 테스트**: 판정 실패(traceback·rc 127·빈 답) × 판정(exit-code, sub-status, finalize-begin·-end, wait-rc, subscriber-up 직접·loop/discuss, stall-watch)이 1 과 `ERROR:` 줄로 끝남 — 수정 전 코드에서 255 또는 0 으로 실패. 예/아니오 두 분기 보존, `--print` 단위표, rc 로 읽는 예/아니오 판정을 금지하는 정적 가드. herdr stub 으로 자리표시 닫기(56d0bfd 에서 실패)와, `restore_error` 없는 `pane_not_found`·다른 오류의 `restore_error` 는 여전히 예외.
- **리뷰 (병합 워크스페이스, 루프 `--all-reviewer`)**: 1회차(@8241bd5) claude·grok PASS, agy NOT PASS(래퍼 문자열을 고정한 기존 정적 테스트 2 건이 `_decided`/`_exit_for` 로 바뀐 호출을 못 따라감), codex NOT PASS(R1 위 2). 541c3fe·65b78b6 에서 수정. 2회차(@65b78b6) **4/4 PASS**. 리포트와 2회차 identity 사이드카는 `.agents/reports/<session>/`.
- **전체 스위트 (병합 워크스페이스, macOS·Python 3.9·herdr 0.9.3, MQTT 환경변수 없이)**: 65b78b6 **1730 passed / 0 failed / 56 skipped**(72 분). skip 중 18 건은 새 워크트리에 nats-docker 서브모듈이 없어서였고, 서브모듈을 받은 뒤 `tests/test_deploy_freshness.py` **31 passed**. 나머지 38 skip 은 v4.8.6 과 같다(tomllib 전용 34 등). v4.8.6 의 herdr 0.9.3 실패(`test_issue45_restore`)는 이번에 통과.
#### 📝 후속
- `tests/test_issue17_item4_decisions.py` 의 `_env` 가 상속된 `WORKSPACE_ROOT` 를 비우지 않아, 옛/새 bash 비교 헬퍼가 에이전트 pane 의 워크스페이스로 lib.sh 를 초기화할 수 있다(claude 비차단).
### 🚀 `v4.8.6` — Issue #17 항목 4: delegate-job 래퍼의 판정이 Python 으로 옮겨진다 (2026-10-08) ### 🚀 `v4.8.6` — Issue #17 항목 4: delegate-job 래퍼의 판정이 Python 으로 옮겨진다 (2026-10-08)
> **패치 (PATCH Release)**: 동작 변화 없는 리팩터링. 래퍼(bash)의 판정 로직을 `lib_py/delegation/wrapper.py` 로 옮기고, #26 정수 규칙의 두 구현을 `lib_py/ints.py` 하나로 합쳤다. 종료 코드·라벨·레지스트리 기록·이벤트·멈춤 감시 동작은 같다. #17 의 나머지 항목은 열린 채 남는다. > **패치 (PATCH Release)**: 동작 변화 없는 리팩터링. 래퍼(bash)의 판정 로직을 `lib_py/delegation/wrapper.py` 로 옮기고, #26 정수 규칙의 두 구현을 `lib_py/ints.py` 하나로 합쳤다. 종료 코드·라벨·레지스트리 기록·이벤트·멈춤 감시 동작은 같다. #17 의 나머지 항목은 열린 채 남는다.