Files
multi-agent-mux/.agents/reports/planner-reviewer-codex-01/report-035792f2.md
T

164 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PR #82 / Issue #78 — b49314c 리뷰: PASS
잡 `035792f2` 리뷰 결과다. 이전 리뷰의 **공유 live fence 덮어쓰기(R1)**와 **미생성 control_plane registry 누락(R2)**는 모두 해소됐다. 원래 실패를 일으켰던 공개 등록 CLI 탐침으로 확인했고, 이번 검토에서 차단 결함을 찾지 못했다.
## Identity·검토 범위
- Loop `loop-20261029T184637Z-6f4228`, round 3.
- Base `a3364e82e1f9e9c6ac504eef377e8dbefba02b8d`.
- Head `b49314c3d039027d3f9478cacecfda587fdc8abc`.
- Tree `d49ff60bbf89437e997f7da21e2fae8df570456b`.
- 검증 경로 `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261029T184637Z-6f4228/3`.
- 기준은 읽기 전용 `tea issues 78 --comments`, `tea pulls 82`, `INSTRUCTION.md`이다. 누적 22파일과 직전 `568385c..HEAD`의 6파일 변경을 실제 snapshot에서 확인했다. 이전에 대조한 누적 reset/adapter/배포 코드와 이번 변경을 함께 검토했으며, truncated summary만으로 판단하지 않았다.
- 파일:줄은 검증 경로 기준이다. creator 작업 트리에서 실행·수정하거나 커밋/브랜치 변경을 하지 않았다.
## 이전 차단 사항의 판정
### R1 — 공유 live fence 덮어쓰기: 해소
- `.agents/skills/lib_py/delegation/registry.py:97`의 reset_marker가 absent/stale/live/unknown 판정을 공유한다.
- `.agents/skills/lib_py/reset.py:507`–`:518`은 각 registry_lock 안에서 기존 marker를 확인한다. 다른 PID의 live 또는 unknown owner이면 FenceBusy를 발생시키고 기존 marker를 교체하지 않는다.
- `reset.sh:154`–`:163`은 fence가 성공한 경로와 거부 사유를 구분한다. EXIT cleanup(:117–124)은 이미 얻은 자신의 marker만 해제한다.
- 체크인 테스트 `test_other_reset_cannot_take_a_live_fence[False/True]`는 B에 대상이 없는 경우/있는 경우를 모두 검증한다. A의 fence bytes 유지, B의 행·대화 보존, B marker cleanup, B 종료 후 외부 등록 거부 및 A 정상 완료를 확인한다.
**별도 재현:** [probe_shared_reset.py](evidence/probe_shared_reset.py), [shared.log](evidence/shared.log).
임시 A의 stop을 마지막 job 검사 뒤 gate에서 대기시킨 채, 같은 MAM_JOBS_DIR를 쓰는 독립된 임시 B에서 원본 reset.sh --yes를 실행했다. B rc=1, `fenced by a reset of A` 사유, 공유 marker bytes 동일, A는 계속 살아 있다. 이어 외부 public registry.py register도 rc=1로 거부돼 잡이 생기지 않았고 A는 rc=0으로 정상 완료했다. 이전 탐침에서 B가 rc0으로 marker를 제거하던 동작이 사라졌다.
추가로 ps 판정 불가를 주입한 탐침에서 신규 register/API와 새 fence 획득 모두 거부됐고 기존 foreign marker bytes가 보존됐다([unknown.log](evidence/unknown.log)). stale marker 교체 및 registry lock barrier는 체크인 테스트로 확인했다.
### R2 — 아직 없는 행 지정 registry가 fence에서 빠짐: 해소
- `.agents/skills/lib_py/update.py:150`–`:163`의 jobs_dirs는 존재 여부와 무관하게 canonical·MAM_JOBS_DIR·행의 control_plane.mam_jobs_dir를 realpath 기준으로 중복 제거하고 addressed_only 범위를 반환한다.
- `reset.py:507`–`:509`가 같은 jobs_dirs를 사용해 없는 디렉터리도 만들고 lock/fence를 획득한다.
- `update.py:166`–`:184`의 open_jobs도 같은 열거 함수를 사용한다. 읽기 검사는 없는 디렉터리를 계속 건너뛰고, 기존 unreadable-directory/record 거부 및 타 제어 평면의 addressed-only 의미를 보존한다.
**별도 재현:** [probe_absent_row_registry.py](evidence/probe_absent_row_registry.py), [absent.log](evidence/absent.log).
reset 시작 전에 target 행에 아직 없는 `<임시 WS>/elsewhere/jobs`를 control_plane.mam_jobs_dir로 기록하고, MAM_JOBS_DIR는 canonical 그대로 뒀다. 마지막 target 검사 뒤 같은 public CLI로 그 경로에 등록을 시도했다. 이번에는 rc=1, `a reset of ... is in progress`로 거부돼 새 잡이 없고 reset은 rc=0으로 끝났다. 중간 행 변경이나 raw 잡 JSON 쓰기를 사용하지 않았다.
체크인 `test_registry_cli_is_refused_after_the_target_recheck[row-absent]`와 fence의 absent 경로 생성·stale 교체·realpath dedupe 테스트도 통과했다. update의 open/terminal/dry-run, unreadable jobs directory, 타 control-plane 대상 필터 회귀 3건도 통과했다.
위 두 탐침의 `old_transcript_exists=false`는 외부 등록이 거부된 뒤 사용자가 승인한 정상 reset이 완료된 결과다. pending 잡을 허용한 상태로 대화를 지우던 이전 실패와 구분된다.
### 이전 라운드 보호의 유지
- canonical/alias/custom 공개 등록 거부는 체크인 매개변수 테스트로 재검증했다.
- `reset.py:522`의 target_id_owned는 post-discovered ID의 cwd 소유권을 기존 verify_session_uuid(revalidate)로 확인한다. `reset.sh:283`에서 false이면 purge 대신 skip한다.
- 이전 foreign Codex 탐침 재실행 결과는 **A 검증 false / B 검증 true / 외부 파일 보존 / reset rc4 / purge 명령 없음**이다([foreign.log](evidence/foreign.log)). own/foreign 체크인 대조도 통과했다.
- strict spec 필드·타입·agent·workspace·중복 이름 검사, row agent/server 비교, pre-assigned ID 없는 행 거부, captured old ID 저장, created/done 미재purge, 시그널·중간 실패·rc4·rc5 재실행 보호가 유지됐다.
## 제안 1–6 및 SKILL.md 주장 대조
| 기준/문서 줄 | 실행 코드·테스트 근거 | 판정 |
|---|---|---|
| 제안1, SKILL:3·23 — root running, worktree/non-running/orchestrator 제외, metadata 유지 | reset.py:105–178 select_targets; reset.sh:306–317 create 인자; 선택/realpath/제외 행/2-target 정상 E2E | 충족 |
| 제안2, SKILL:25 — 열린 잡·loop·update·자신·invalid agent/role·ID/spec 사전 거부 | reset.py:424–473; reset.sh:38–40·98–174; 각 거부·동시 admission 테스트 | 충족 |
| 제안3, SKILL:17–18·24 — parser·확인·dry-run 무변경 | reset.sh:27–79·177–203; 비TTY rc3, dry-run .mam 파일무변경 | 충족. TTY 해석은 아래 사용자 확인 사항 |
| 제안4, SKILL:27 — 첫 stop 전에 원자 spec, 재개 | reset.py:183–272 mkstemp/fsync/replace·strict schema; :275–353 record_old_id/classify_entry; reset.sh:203·228–230·337 | 충족. 5-state, 같은 ID resume, 중단 후 재실행 |
| 제안4, SKILL:28 — 순차 stop → recorded/owned ID → purge → create/onboard | reset.sh:217–323, reset.py:522–537; 기존 stop/create 재사용 | 충족, 병렬 create 없음 |
| 제안5, SKILL:29 — changed/open job/no ID/unverified/in-use skip, 외부 holder 미종료 | reset.sh:233–302, reset.py SKIP_RESULTS; skip·파일/행 보존·rerun 테스트 | 충족 |
| 제안6, SKILL:33–39 — 결과 표와 exit0/1/2/3/4/5, INT/TERM/HUP | reset.py:599–683 build_table, reset.sh trap:126–134·결과:325–349 | 충족. 원본 Mac signal 테스트 사용 |
| SKILL:26·47 — update admission → loop/reset marker → release; run_loop/update/다른 reset 거부; configured registry/alias·owner onboarding·stale 표식 | reset.sh:98–174·317; registry.py:97–135·232 이후; reset.py:492–519; canonical/alias/custom/absent·shared live fence·barrier·owner/stale | 충족. 이번 R1/R2 해소 |
| 함께 고칠 것, SKILL:44 — lsof PID/명령줄·기존 ps 안내·서비스 제외·외부 미종료 | agents/base.py:89–115·385–403, agy in_use_paths, opencode _is_shared_service/_holder_counts | 충족. lsof 부재·timeout·holder·서비스 제외, 실제 Mac lsof 탐침 |
| 함께 고칠 것 — Codex capability UUID leaf만 삭제 | adapters/codex.py:134–179, plausible ID/leaf/symlink 처리, delete 성공·실패 및 타 UUID 보존 | 충족 |
| SKILL:43 — 비가역·먼저 dry-run | 도움말/동의 및 dry-run 분리 | 운영 권고. 과거 dry-run 실행 이력을 강제하는 규칙은 아님 |
| SKILL:45 — Grok shared memory 보존 | 기존 Grok의 세션별 제거 유지, purge/adapter 회귀 | 충족. 메모리 삭제 옵션 추가 없음 |
| SKILL:46 — AGY edit hook | hooks/loop_delegation_guard.sh:51과 reset.sh:110이 같은 loop marker 사용 | 코드 일치 |
| SKILL:48 — 같은 종류 post-discovered 순차 ID 대기 | create의 기존 MAM_ID_ORDER_WAIT_S/adapter ordering, #63 회귀 | 코드 일치 |
명령 예시, 옵션/동작 표, exit 표, safety notes를 코드와 대조했다. 순서·사전 거부·원자 저장·잠금·정리는 스크립트에 있고 테스트로 고정된다. 이번 검토에서 SKILL에만 있고 실행 코드에 빠진 기계적 규칙을 찾지 못했다. 수기 잡 파일 쓰기는 등록 프로토콜 밖이므로 문서대로 target 재검사 때 이미 보이는 기록만 검출한다. 이 문구를 임의 파일 쓰기의 원자적 admission 보장으로 확대하지 않았다.
## 일반 registration·remote·update 영향과 단순화
- marker 없는 registry에서 register_job 10회 + remote mirror/replay 2회 동안 subprocess.run을 금지한 탐침이 통과했다. **일반 경로의 ps/subprocess 호출은 0회**다([fast.log](evidence/fast.log)).
- live fence에서 remote.mirror_job은 ResetInProgressError를 처리해 None을 반환하고 잡 JSON/brief 디렉터리를 만들지 않는다. 기존 registry stdout/CLI/import/환경 계약과 remote finished/newer attempt 재전송도 통과했다.
- 기존과 달리 live marker의 owner 예외·타 session 범위 판단 전에 reset_marker가 liveness를 조회한다. 따라서 marker가 존재하는 registry에서는 최대 두 ps 호출이 있을 수 있다. reset이 없는 보통 등록의 비용 증가와 구분해야 하며 차단 사유는 아니다.
- addressed-only 표식이 있는 타 control-plane registry에서 covered session은 거부하고 unrelated session은 등록되는 별도 탐침도 통과했다([unknown.log](evidence/unknown.log)).
- 이전 위험한 중복이었던 registry 경로 열거가 jobs_dirs 한 함수로 합쳐졌다. reset은 없는 경로에 보호를 설치하고 update는 읽기만 하는 차이는 각 호출부에 남아 적절하다. 순서/필터를 바꾸는 불필요한 재설계는 아니다.
- reset.py 934줄/reset.sh 349줄이라는 양 자체를 결함으로 세지 않았다. 기존 stop/create·verify_session_uuid·open_jobs 재사용이 유지된다. reset.py:54–100의 read-only state 로더는 실제 SQLite -shm까지 바꾸지 않는 dry-run 요구 때문에 복사본을 읽는다. 환경 JSON만 읽는 update._state(:375)로 단순 치환할 수 없으며 추가 추상화는 승인 조건이 아니다.
## 패키징·버전
install.sh의 일반 .agents 복사(:367)와 reset 도움말(:673), remove.sh:96 fallback, gitea-ci.yml:45 ShellCheck 대상, 두 README:60, code-block inventory 및 10-skill layout을 확인했다. detached snapshot에 실제 .claude/skills가 없어 관련 1건은 skipped이며 합성 layout 검사는 통과했다. SKILL version과 lib.sh MAM_VERSION은 4.13.0으로 일치한다. test_version_consistency 목록과 VERSIONS 매트릭스는 브리프대로 릴리스 때 오케스트레이터가 넣을 사항으로 확인했고 누락 결함으로 세지 않았다.
## Mac 실행·정적 검사
Darwin arm64, /bin/bash 3.2.57, 지정 제어 루트 venv Python 3.9.6. 검증 tree에는 venv가 없어 브리프 상단의 명시적 인터프리터를 썼다. 모든 pytest cwd는 검증 경로다. 전체 스위트는 실행하지 않았다.
공통 명령:
```bash
env -u HERDR_SOCKET_PATH -u HERDR_PANE_ID -u HERDR_TAB_ID \
-u HERDR_WORKSPACE_ID -u HERDR_SESSION_NAME -u HERDR_SESSION \
-u WORKSPACE_ROOT -u MAM_ENV_FILE -u MAM_JOBS_DIR \
-u DELEGATE_JOB_LOGS_DIR -u AGENT_SESSIONS_YAML \
PYTHONDONTWRITEBYTECODE=1 \
/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python \
-m pytest -p no:cacheprovider --basetemp=<TMPDIR/아래 이름> -q <선택>
```
| basetemp 이름 (TMPDIR 아래) | 선택 파일/옵션 | 결과 |
|---|---|---|
| mam-eval-035792f2-main | tests/test_issue78_reset.py tests/test_issue78_hints_codex.py tests/test_issue42_skill_blocks.py | **84 passed, 1 skipped, 1 session-teardown error / 700.03s**. [main.log](evidence/main.log) |
| mam-eval-035792f2-purge | tests/test_issue10_13_purge.py tests/test_issue31_purge_residual.py tests/test_a4_adapter_contract.py tests/test_issue63_same_cwd_ids.py | **150 passed / 282.73s**. [purge.log](evidence/purge.log) |
| mam-eval-035792f2-contracts | 아래 9 node, 등록 node 양 mode 포함 | **10 passed / 16.38s**. [contracts.log](evidence/contracts.log) |
| mam-eval-035792f2-shared | -s -p probe_shared_reset tests/test_issue78_reset.py | **1 passed / 33.96s**. [shared.log](evidence/shared.log) |
| mam-eval-035792f2-absent | -s -p probe_absent_row_registry tests/test_issue78_reset.py | **1 passed / 32.64s**. [absent.log](evidence/absent.log) |
| mam-eval-035792f2-foreign | -s -p probe_foreign_id tests/test_issue78_reset.py | **1 passed / 20.60s**. [foreign.log](evidence/foreign.log) |
| mam-eval-035792f2-fast | -s -p probe_registry_paths tests/test_issue78_reset.py | **2 passed / 0.19s**. [fast.log](evidence/fast.log) |
| mam-eval-035792f2-native | -s -p probe_native_holders tests/test_issue78_reset.py | **2 passed / 0.28s**. [native.log](evidence/native.log) |
| mam-eval-035792f2-unknown | -s -p probe_unknown_fence tests/test_issue78_reset.py | **2 passed / 0.16s**. [unknown.log](evidence/unknown.log) |
일반 선택 회귀는 **244 passed, 1 skipped, 1 session-teardown error**, 별도 외부 탐침은 **9 passed**다. 같은 사례를 반복 실행해 합산하지 않았다.
종료 오류 분석용 재실행(중복 합산 안 함):
- `mam-eval-035792f2-layout-rerun`: `tests/test_issue42_skill_blocks.py::test_claude_skills_layouts[broken_entry-False]` — **1 passed / 0.04s**.
- `mam-eval-035792f2-agy-rerun`: `tests/test_issue78_hints_codex.py::test_in_use_hints_list_lsof_holders[agy]`, `tests/test_issue78_reset.py::test_post_discovered_without_recorded_id_8b_and_exit_5` — **2 passed / 64.39s**.
contracts의 node 목록:
```text
tests/test_issue5_phase1_delegation.py::test_c05_submit_job_stdout_is_exactly_the_job_id
tests/test_issue5_phase1_delegation.py::test_registry_cli_contract_snapshot
tests/test_issue5_phase1_delegation.py::test_delegation_package_imports_no_sibling_lib_py_modules
tests/test_issue5_phase1_delegation.py::test_importing_delegation_does_not_mutate_environment
tests/test_issue18_liveness_contract.py::test_remote_runner_ignores_a_resent_dispatch_of_a_finished_attempt
tests/test_issue18_liveness_contract.py::test_remote_runner_never_rolls_a_newer_attempt_back
tests/test_issue72_update.py::test_open_jobs_refuse_and_terminal_or_dry_run_do_not
tests/test_issue72_update.py::test_a_jobs_directory_that_cannot_be_listed_refuses
tests/test_issue72_update.py::test_other_control_planes_count_only_jobs_addressed_to_this_registry
```
외부 플러그인은 명령 앞에 `PYTHONPATH=<evidence 절대 디렉터리>`를 더하고 `-s -p <probe 모듈> tests/test_issue78_reset.py`로 선택한다. probe 파일은 collection을 해당 탐침으로 바꾸며 snapshot의 fixtures/helpers를 그대로 쓴다. shared 탐침은 이전 실패 때의 B rc0 기대만 이번 보호 정책인 rc1/동일 marker bytes 기대로 바꿨고, 외부 register·실제 reset 동작은 유지했다. 제품 코드/체크인 테스트는 수정하지 않았다.
변경 Python 11파일 ast.parse(feature_version=(3,9)), lib.sh/reset.sh/delegate wrapper 각각 /bin/bash -n, git diff --check가 통과했다([static.json](evidence/static.json)). 새 reset 경로에 mapfile/declare -A/flock(1)/setsid(1)/proc, GNU 전용 sed -i/stat -c/readlink -f 의존을 추가하지 않았다. 빈 배열·trap은 native 동작 테스트로 확인했다. 실제 Mac lsof가 자신이 만든 holder PID를 찾았고, timeout 주입 시 10초 상한과 빈 추가 힌트를 확인했다. ShellCheck는 설치되어 있지 않아 실행하지 않았다.
## 세션 종료 guard 오류와 판정 한계
main 실행은 기능 단언 **84 passed, 1 skipped**였지만 세션 종료의 기존 agent_home_leak_guard가 **1 error**를 냈다. 따라서 이 명령의 exit code는 **1**이며, 오류 없는 전체 스위트 통과라고 보고하지 않는다([main.log](evidence/main.log)).
감시기는 실제 AGY 홈에서 `637fea1e-9288-4a72-857e-7d23e04bb12d.db-wal`과 `.db-shm`의 새 디렉터리 항목을 발견했다. 기존 conftest.py:1582–1598도 병행 실제 agent가 새 파일을 만들면 false positive가 가능하다고 설명한다. 오류는 마지막 레이아웃 사례에 붙었지만 그 사례는 tmp_path의 symlink layout만 만든다.
- DB를 SQLite로 열거나 수정하지 않고 lsof의 PID와 ps의 실행 파일·경과 시간만 확인했다. 보유자는 실제 `agy` PID **66608**(약 11시간 실행), **91891**(약 34분 실행)으로, 11분 40초의 main 실행보다 먼저 시작한 외부 프로세스다([ambient-holder.json](evidence/ambient-holder.json)).
- main basetemp 안에는 해당 UUID의 artifact가 없었다([ambient-basetemp.json](evidence/ambient-basetemp.json)). AGY reset 시험은 UA 상수와 임시 HOME을 쓰며, hints 시험은 임시 home/HOME_DIR/ctx와 mock lsof를 쓴다.
- 마지막 레이아웃 node 단독 재실행은 **1 passed / 0.04s**, 종료 guard도 통과했다([layout-rerun.log](evidence/layout-rerun.log)).
- AGY hints와 post-discovered stop/purge/create/재실행 경로의 선택 재실행: **2 passed / 64.39s, 종료 guard 통과**([agy-rerun.log](evidence/agy-rerun.log)). 재실행 수는 일반 회귀 합계에 중복 합산하지 않았다.
위 프로세스·fixture·재실행 증거로 이 종료 오류는 **외부 AGY 동시 활동에 따른 환경 감시 false positive로 판단한다(추론)**. 테스트 누수라고 단정하거나 실제 홈 파일을 제거하는 방식으로 감시기를 통과시키지 않았다. 제품의 이번 변경에 의한 기능 실패는 없어 PASS지만, main 명령 자체의 종료 오류는 그대로 증거와 함께 남긴다.
## 발견 사항·사용자 확인 필요
- **차단 사항 없음.** 이전 R1/R2는 해소됐으며 재계획이 필요하지 않다.
- 비차단: marker가 남아 있는 경로의 liveness 조회 비용은 위 설명과 같다. 추가 작업을 승인 조건으로 삼지 않는다.
- 순차 create, Grok 메모리 옵션 없음, stopped 초기 대상 제외는 작성자의 선택대로 검토했다.
- 이슈의 “--yes 없이는 실행하지 않는다”와 TTY 명시적 y/yes 동의 방식의 차이는 이전 판정처럼 사용자 확인 사항이다. 동의를 받는다는 의미로 검토했으며 flag 자체 강제를 요구한 것으로 보아 차단하지 않았다.
## 격리·종료 확인
실제 작업 공간에 reset/dry-run/purge를 실행하지 않았다. 파괴적 동작은 임시 HOME·자체 .mam·mock herdr 안에서만 수행했다. 실제 .mam.env 값 출력/export, 실제 herdr 서버/설정 변경, SQLite로 실제 에이전트 홈 DB 열기, 실제 opencode 서비스 제어를 하지 않았다. guard 오류 분석의 실제 홈 확인은 위 PID/실행 파일·나이 메타데이터뿐이며 DB 내용은 읽지 않았다. pkill/killall/패턴 kill을 쓰거나 실제 모니터에 신호를 보내지 않았다. native holder 탐침은 자신이 만든 Popen만 회수했다.
이번 basetemp에 속한 테스트 잔류 프로세스는 0개, 실제 모니터는 전후 PID 3714로 같았다([process-audit.json](evidence/process-audit.json)). 시작·종료 HEAD/tree는 위 identity와 일치했고 git status는 빈 출력이다. 소스/브랜치/이슈·PR 상태 변경, push/merge는 없었다. 제어 평면 쓰기는 승인된 lifecycle 이벤트와 리포트·증거뿐이다.
[VERDICT: PASS]