release(v4.8.0): bump framework and eight skills to 4.8.0 in lockstep; archive PR #41 reviews (three rounds)

Issue #33 (job budget, wall_clock_timeout vs deadline_timeout) and
Issue #30 (notify-only stall watcher). Reviews of PR #41 in the merge
workspace: round 1 (@1673818) claude N1 / codex R1 R2 NOT PASS, agy grok
PASS; round 2 (@af0a2a2) grok N1 / codex D1 D2 NOT PASS, claude agy PASS;
round 3 (@9200562) 4/4 PASS. Reports and the round-3 identity sidecars go
to .agents/reports/<session>/. The design consensus on items A-D is in the
VERSIONS.md entry.

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-09-26 16:08:37 +09:00
1 parent 9200562c91
commit 53f87e4560
32 files changed
+1263 -26

No files matched your search

@@ -0,0 +1,103 @@
# PR #41 독립 리뷰 — 0f12973f
**NOT PASS.** 알림 전용 원칙과 두 종료 시계의 분리는 대체로 구현됐지만, 감시자의 래퍼 종료 처리와 완료 후 재주입 방지에 재현 가능한 결함이 있다. 아래 R1·R2를 수정한 뒤 재검토해야 한다. 전체 설계를 다시 짤 수준은 아니며, 설계 합의 의견이나 예정된 4.8.0 버전 변경을 판정 근거로 삼지 않았다.
## 검증 identity와 범위
- loop: `loop-20260926T025230Z-a30a6d`
- base: `8ea65bb23830b56c4530897270edaa6e556de392`
- head: `1673818bb5fed14ed6fee87884e70d39e81fe91f`
- tree: `00d5e03e2d521c87c3474c032f2af6f7a1af9e92`
- 검증 cwd: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20260926T025230Z-a30a6d/1`
- 30개 파일의 누적 변경을 검토했다. 검증 전후 HEAD/tree가 일치하고 `git status --porcelain`은 비어 있다. 소스 수정·커밋·실제 에이전트/herdr 세션 조작은 하지 않았다. 테스트·탐침의 레지스트리, HOME, 모의 herdr, 프로세스는 임시 디렉터리에 격리했다.
이하 파일:라인은 이 head 기준이며 `.agents/skills/` 아래 경로는 생략하지 않았다.
## 병합 차단 결함
### R1 · P2 — 래퍼가 SIGTERM으로 끝나면 stall watcher가 고아로 남는다
**근거:** `.agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job:700`에서 감시자를 백그라운드로 실행하고 PID를 저장한다. 정리는 같은 파일 `:385`, `:560`의 정상 `wait` 반환 뒤에만 실행한다. `stop_stall_watch`는 `:708`부터 있지만 EXIT/TERM/INT 정리 trap에 연결돼 있지 않다. 기존 `:1088`의 EXIT trap은 agent bootstrap 전용이며 `:1102`, `:1108`, `:1114`에서 해제된다.
**재현:** 검증 트리의 실제 래퍼를 `MAM_DELEGATE_JOB_NO_MAIN=1`로 source한다. 임시 레지스트리에 UUID4에서 생성한 잡 ID와 `status=running`을 쓰고, PATH 맨 앞에 항상 `working` snapshot을 반환하는 모의 herdr를 둔다. 실제 `start_stall_watch herdr:isolated-probe codex`를 호출하고 래퍼가 해당 PID를 wait하도록 한 뒤 래퍼 PID에만 SIGTERM을 보낸다. 감시자는 실제 `python -m lib_py.stall_watch` 프로세스다.
**기대:** SIGTERM 같은 처리 가능한 래퍼 종료에서 감시자와 소유한 대기 프로세스를 정리하고 회수한다. 잡 상태는 감시자가 임의로 닫지 않는다.
**실제:** 래퍼 rc `-15` 뒤에도 `ps`가 감시자 `30374`, PPID `1`, 상태 `S`를 반환했다. 원래 잡이 열린 상태면 이 감시자는 계속 관측하고 나중에 알림·재주입할 수 있다. 정상 완료만 확인하는 `tests/test_issue30_stall_watch.py:503`의 S9로는 잡히지 않는다. 탐침 종료 후 탐침 전용 프로세스 그룹을 정리했고 해당 PID의 소멸을 확인했다.
**수정 방향:** 감시자 시작 전에 래퍼 소유 자식의 공통 EXIT/TERM/INT 정리를 설치하고 기존 bootstrap trap과 합성한다. kill과 wait는 bash가 맡되, 정상·오류·취소 경로 모두에서 실행되도록 한다. 감시자가 대기 중 생성한 herdr 자식도 유한 시간 안에 정리되는지 포함한 신호 종료 테스트를 추가한다. SIGKILL을 trap으로 처리하라는 요구는 아니다.
### R2 · P2 — herdr 대기 중 완료된 잡에도 idle 재주입을 실행한다
**근거:** `.agents/skills/lib_py/stall_watch.py:270`에서 잡 상태를 읽고, `:284`에서 최대 수십 초 대기한 뒤 `:310`에서 이전 상태 그대로 `_act`에 전달한다. `_act`는 에이전트 상태·자식·입력창만 새로 수집하며 잡 상태와 dispatch identity는 다시 확인하지 않는다. `:352`의 알림 뒤 `:354`는 무조건 `prompt`를 호출한다. `_notify`의 `:243` 부근은 발행 거부를 로그로만 남기므로 발행 CAS가 재주입을 막아주지도 않는다.
**재현:** 실제 `StallWatch.run()`에 모의 `wait_once`와 Source를 주입한다. 첫 대기에서 idle/seq=1을 관측한다. 두 번째 대기가 돌아오기 직전에 임시 잡 JSON을 `running → completed`로 바꾸고 TIMEOUT을 반환한다. 시계는 idle 임계값을 넘게 하고, collect는 같은 seq·살아 있는 PID·자식 없음·빈 입력을 반환한다. 알림/프롬프트만 메모리에 기록해 실제 발행과 세션 조작을 피한다.
**기대:** 완료를 재확인한 감시자는 종료하며 idle 알림·재주입을 하지 않는다.
**실제:** `run()`은 최종 rc 0으로 종료하지만 그 전에 `notifications=['stalled:idle']`, `prompts=[('completed', 'Job … otherwise continue where you left off.')]`를 기록했다. 즉 실제 prompt 호출 시점의 레코드는 이미 completed였다. 래퍼의 정상 stop과도 경합하고, R1로 남은 감시자에서는 더욱 명확히 발생한다. 동일 잡의 다음 dispatch에서도 status만 확인하는 현재 방식은 이전 token의 prompt를 차단하지 못한다.
**수정 방향:** 블로킹 관측 뒤와 재주입 직전에 잡의 비종결 상태 및 token/attempt 일치를 확인하고, 사라짐·불일치에서는 중단한다. 발행 결과를 구조화해 CAS 거부 이후에는 prompt를 보내지 않도록 한다. 완료/redispatch가 관측과 실행 사이에 끼어드는 결정적 테스트를 추가한다. registry 락을 긴 herdr RPC 동안 유지하는 방식은 피한다.
R1·R2의 실행 가능한 격리 탐침은 `/tmp/0f12973f-tools/probes.py`에 남겼다. 검증 cwd에서 브리프의 Python으로 이 파일과 위 검증 경로를 인자로 실행하면 재현된다. 탐침은 실제 서버 대신 자체 모의 herdr를 사용한다.
## 추가 개선 사항 — 비차단
1. **선행 0 예산 처리 불일치.** 래퍼 `:147`의 bash 산술은 `MAM_JOB_TIMEOUT_S=08`을 8진수로 해석하다 오류를 내고 `TIMEOUT=3600`으로 대체한다. 같은 값은 `lib_py/loop/runner.py:600`에서 8로 해석된다. 탐침 결과는 `value too great for base`와 `TIMEOUT=3600`이었다. 공통 Python 양의 정수 파서에서 십진수로 정규화하는 것이 낫다. 정상 정수·CLI 우선순위 테스트는 통과했다.
2. **문서의 오래된 종료 설명.** delegate-job `SKILL.md:99`는 아직 total budget도 `deadline_timeout`이라 설명하고, `:101`의 broker-unavailable deadline 도달도 같은 라벨로 적는다. 구현은 두 경우 모두 wall clock이 소진되면 `wall_clock_timeout`이다. `:100`의 “before v4.7.5”도 이번 4.8.0 릴리스 계획과 맞춰 정리해야 한다. loop `SKILL.md:209`, `:222`의 예산·하위 잡 라벨 반영은 확인했다.
3. **테스트 이식성/격리.** stall watcher 테스트 `:39`의 모듈 전체 Linux skip은 순수 judge·pane parser 테스트까지 숨긴다. /proc·Linux 프로세스 사실을 쓰는 사례만 별도로 표시하는 편이 좋다. 기존 전달 테스트 `test_issue18_liveness_contract.py:497`은 unreachable 환경을 만들기 전에 잡을 등록한다. 잡의 저장된 broker가 환경을 덮어쓰고(`mqtt_common.py:294`), `:511`의 thread join 시간까지 측정하므로 외부 브로커 지연에 따라 시간 assertion이 실패한다. 등록 단계부터 브로커를 격리해야 한다.
## 코드 계약 확인
- `--timeout > MAM_JOB_TIMEOUT_S > 3600`, loop `--job-timeout`, 공통 `contract_args()` 전달 경로와 예산+grace(기본 300초) 대기 상한을 확인했다. 구현·리뷰·수정·재심·중재·순응은 공통 delegate 경로를 사용한다. 명시적 `MAM_CHILD_WAIT_S`는 유지된다. remote dispatch는 `timeout_sec`을 운반하고 두 remote 만료 문구 모두 새 라벨을 쓴다.
- subscriber의 rc 2/4와 bash 상수 일치, `delegate-job wait`의 rc 2 호환, broker-down 재관측 및 disk-heartbeat 유예가 wall deadline에 닿는 라벨을 검토했다. registry close/engine의 새 request 및 하위 잡 `CLOSED-UNCONFIRMED` 분류 테스트가 통과했다. 별도 순수 전이 탐침에서도 `wall_clock_timeout → error` 후 늦은 completed는 `conflicting completed after terminal error; refusing`으로 거부됐다.
- stall watcher의 단일 발행 경로는 `progress`뿐이다. close/error/cancelled 발행이나 자동 승인 경로가 없고, AST 테스트도 이를 확인한다. blocked는 detection 문구를 붙이고 답하지 않는다. idle은 연속 idle 기간, seq 재확인, 어댑터 신뢰도, PID/자손, 빈 입력, 1회 제한을 조합한다. 정상 local watcher 수명, human/remote 제외, `MAM_STALL_WATCH=0`, herdr 장애 경고/종료, loop의 실시간 알림 relay는 모의 환경에서 확인했다. 이 긍정적 결과가 R1·R2의 예외 경로를 보장하지는 않는다.
- 기존 G-8에서 wall=idle=0.5였던 경합을 wall=0.5/idle=30으로 바꾼 것은 타당하다. rc 이름 상수와 CLI help snapshot, issue18 contract_args 기대값 수정도 새 인터페이스에 부합한다. conftest의 process-info shape·detection·seq·wait 지연은 모의 객체에 국한된다.
- 5종 idle pane은 herdr 0.9.1, 2026-09-25 캡처 메타데이터를 가진다. typing 변형은 실측이 아닌 합성이라는 점이 테스트에 명시돼 있다. 실측된 빈 입력과 합성 비어 있지 않은 입력은 모두 통과했다. 실제 Claude 시험 A/B 자체는 이번 브리프의 세션 조작 금지 때문에 재실행하지 않았다. 모의 blocked/idle/자식/입력 조건 검증을 실제 에이전트 실험의 독립 재현으로 부풀리지 않는다.
## bash/Python 경계 표
| 위치 | 새로 수행하는 일 | 경계 판단 / 후속 이전 대상 |
| --- | --- | --- |
| 래퍼 `:146` | env 양의 정수 검증·기본값 선택 | 정책 결정이다. Python 공통 예산 파서로 이전. |
| 래퍼 `:832` | subscriber rc를 job 상태 의미로 매핑 | 프로세스 rc 수집은 bash, 상태/실패 의미 결정은 Python. |
| 래퍼 `:902`, `:950` | 현재 시각과 deadline 비교 후 FINALIZE_CLOCK 선택 | 단순 전달이 아니라 어느 시계가 끝났는지 결정한다. #17 항목 4 대상. |
| 래퍼 `:956` | rc/clock/phase에 따라 close request·사유 선택 | 종결 정책이다. Python이 구조화된 결과를 반환하도록 이전. |
| 래퍼 `:687` | env switch·remote·human 감시 제외 | 현재는 bash에 있는 정책. 장기적으로 Python의 감시 가능 여부 결정과 합칠 수 있다. |
| 래퍼 `:700`, `:711` | 프로세스 시작·kill·wait | bash 책임에 맞는다. R1 수명 정리는 여기서 보강. |
| 래퍼 `:1214` | 외부 wait rc 4를 호환 rc 2로 변환 | 얇은 CLI 경계 변환으로 유지 가능하나 상수/계약은 Python과 일치해야 한다. |
따라서 “bash는 라벨을 나르기만 한다”는 주장은 정확하지 않다. 다만 #17 항목 4 이전이 이미 후속으로 합의됐으므로 이 경계상 부채 자체는 비차단이다.
## 실행 검증 결과
인터프리터는 브리프의 `.venv/bin/python`(macOS Python 3.9.6)을 사용했다. pytest는 `AGENT_PYTHON_BIN` 지정, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, 검증 트리 밖의 `--basetemp=/tmp/0f12973f-*`로 실행했다.
| 실행 | 결과 |
| --- | --- |
| issue33, issue30, issue18, a4 adapter, tier1, issue5 phase1 — 원본 선택 265건 | 214 passed / 3 failed / 48 skipped, 342.12초 |
| 위 실패 3건만 재실행 | 2 passed / 전달 timing 1 failed, 49.73초 |
| 전달 timing: 잡 등록부터 MQTT=127.0.0.1:9로 격리 | 1 passed, 2.36초 |
| issue30: 외부 pytest plugin으로 모듈 Linux skip을 선택적으로 해제 | 43 passed / 2 failed / 3 skipped, 306.06초 |
| 위 R4 2건: 임시 herdr 모의 스크립트의 프로세스 간 계측만 보정 | 2 passed, 125.18초 |
| base parser 임시 사본을 현재 S14에 적용 | Codex/Grok/OpenCode 3건 예상 실패; agy/claude는 기존에도 통과 |
| 같은 S14에 head parser 적용 | 5건 통과 |
| 정적 검사 | `git diff --check`, 두 bash 진입점 `bash -n` 통과 |
첫 표준 실패 중 2건은 브로커 subscribe/heartbeat 타이밍이었고 재실행에서 통과했다. 나머지 전달 테스트는 위에 설명한 저장된 broker와 thread join의 영향을 분리해 통과를 확인했다. 보조 base archive 실행도 통과했으나 브로커 변동이 있으므로 이것만으로 코드 회귀라고 판정하지 않았다.
강제로 활성화한 R4 2건의 최초 실패 값은 연속 idle 시간이 각각 `-0.00066`, `-0.00741`초였다. Python 3.9.6/macOS의 각 모의 herdr 프로세스에서 monotonic 기준점이 달라지는 계측 문제다. 임시 모의 스크립트에서만 cross-process timestamp를 `time.time()`으로 바꾸고 생산 감시자 코드는 그대로 두자 둘 다 통과했다. 원본 Linux suite가 macOS에서 그대로 통과했다고 주장하지 않는다. Linux process-name 2건과 /proc heartbeat 1건은 검증하지 않았다.
종합하면 선택한 265개 사례 중 262개는 위 재실행/이식 조건에서 통과를 확인했고 3개 Linux 전용 사례는 미검증이다. 별도 R1·R2 탐침은 제품 결함을 재현했다. 테스트 수만으로 PASS를 내릴 수 없다. 검사와 진단용 임시 plugin은 `/tmp/0f12973f-tools/`에 있고 검증 트리는 수정하지 않았다.
## 설계 합의
**A — 동의.** #30은 stalled 판단으로 잡을 닫거나 blocked 승인을 자동 수락해서는 안 된다. 이 PR은 `progress` 단일 발행 경로와 close 경로 부재로 그 원칙을 구현한다. idle의 1회 계속 요청도 합의 범위에 맞는다. 다만 알림 전용이어도 prompt는 외부 부작용이므로 R1·R2의 수명/dispatch 경계를 지켜야 한다. 이번 NOT PASS는 그 실행 안전성 결함이며 알림 전용 원칙에 대한 반대가 아니다.
**B — 동의.** 연장을 #39의 명시적인 프로토콜 요청으로 분리하는 것이 타당하다. 동기 래퍼 호출 중인 러너의 관측 정체를 작업 진행 여부나 연장 승인으로 해석하기 어렵고, seq 정체도 정상 장기 작업에서 생긴다. 요청은 승인이 아니어야 한다. 오케스트레이터가 잡별 누적 최대 예산/횟수를 두고 token·attempt로 인증된 요청을 멱등 처리해 새 절대 deadline을 확정·배포하도록 권한다. 이 규칙을 remote에도 동일하게 적용하면 무한 연장과 양쪽 deadline 불일치를 줄일 수 있다.
**C — 동의.** #40의 트리거는 completed 수신 측이어야 한다. 에이전트 publish_event에 YAML/SQLite 제어 평면 쓰기를 넣으면 워크트리 creator와 `MAM_NO_SUBSCRIBER_DAEMON=1`에서 소유권·경로 문제가 다시 생긴다. 공통 Python 완료 수신 처리기를 두고 래퍼 구독/디스크 완료 관측을 기본 진입점으로 삼는 쪽이 standalone 위임까지 포괄한다. loop `wait_for_job` 직후는 누락 복구 경로로 같은 처리기를 호출할 수 있으며 job/token/attempt 단위로 중복 capture/pin을 막아야 한다. 어댑터는 id_lifecycle·capture·pin 사실/동작을 제공하고, 실행 시점과 제어 평면 쓰기의 권한은 수신 측이 갖는다. #17 항목 4에 흡수하거나 바로 다음 순서가 적절하다.
**D — 조건부 동의.** 이번 PR에서 종결 처리 전체 이전까지 요구하지 않는 것은 범위를 제한한다는 점에서 타당하다. 다만 위 표처럼 bash에 새 정책이 들어간 사실을 인정하고 후속 작업으로 추적해야 한다. #17 항목 4에서 예산 정규화, rc→상태 해석, deadline/idle 판정, FINALIZE_CLOCK와 phase별 close request·사유, 디스크 유예/재관측 종료 결정을 Python으로 옮긴다. bash에는 spawn/wait/kill, 신호 정리 및 얇은 외부 CLI 호환 처리를 남긴다. 이 분리와 별개로 R1·R2는 이번 병합 전에 수정해야 한다.
[VERDICT: NOT PASS]