release(v4.7.4): bump framework and eight skills to 4.7.4 in lockstep; archive PR #38 merge-side reviews

Issue #34: the loop waits for the creator job's sub-jobs, reviewers get
the sub-job table as evidence, a run with sub-jobs voids a round whose
source changed during review, and a pane caller reaches an agent of
another workspace on its own server. VERSIONS.md carries the entry; the
merge-side reports (round 1 @ed293dc and round 2 @e80186f with identity
sidecars, base ed293dc -> head e80186f, evidence review-tree) are archived
under .agents/reports/<session>/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
GodopuandClaude Fable 5.1 committed 2026-09-25 16:44:20 +09:00
1 parent e80186f560
commit 22586d2a01
28 files changed
+825 -26

No files matched your search

@@ -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]