Issue #42 stages 1-2: the SKILL.md code-block inventory as test data and the removal of blocks the entry scripts already do. Merge-workspace review: round 1 (@8d26106) claude agy grok PASS, codex NOT PASS (R1-R3); fixed in17eba09(docs, inventory data and the guard test only); round 2 4/4 PASS. Full suite at8d261061408 passed / 0 failed. Reports and the round-2 identity sidecars go to .agents/reports/<session>/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
122 lines
15 KiB
Markdown
122 lines
15 KiB
Markdown
# PR #46 단계 1·2 리뷰 — fbd52b0b
|
||
|
||
**NOT PASS**. 제품 코드 변경은 없고 선택 테스트는 모두 통과했다. 다만 새 문서대로 실행할 때 격리 서버의 attach 대상이 사라지는 문제, dry-run의 부작용을 부정하는 설명, 삭제 블록 복귀 가드의 누락이 남아 있다. 문서와 테스트의 국소 수정으로 해결할 수 있으므로 재계획 승격은 필요하지 않다.
|
||
|
||
## 검증 대상과 결과
|
||
|
||
- loop: `loop-20261003T222707Z-a0de27`
|
||
- base: `45d1621c93504e6cfa347b31995f70da868f4172`
|
||
- head: `8d26106195a0a24621f140e76389d206332f84c0`
|
||
- tree: `b579380959e08f721492386da904972092bf8d7f`
|
||
- 검증 cwd: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261003T222707Z-a0de27/1`
|
||
- HEAD/tree 일치, 시작·종료 시 git status clean. `git diff --check 45d1621 HEAD` 통과.
|
||
- 변경 파일은 SKILL.md 4개, JSON 목록, 새 테스트 파일뿐이다. 제품 스크립트/Python 및 버전 변경 없음.
|
||
|
||
검증 cwd에서 다음을 실행했다. **152 passed, 1 skipped in 314.72s**, 종료 코드 0. 로그: `/tmp/fbd52b0b-selected.log`.
|
||
|
||
```sh
|
||
AGENT_PYTHON_BIN=/Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-46/.venv/bin/python PYTHONDONTWRITEBYTECODE=1 /Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux-wt/pr-46/.venv/bin/python -m pytest -p no:cacheprovider -q --basetemp=/tmp/fbd52b0b-selected tests/test_issue42_skill_blocks.py tests/test_tier2_component.py tests/test_orc_onboard.py tests/test_loop_cli.py tests/test_o1_rebuttal.py tests/test_version_consistency.py tests/test_issue10_13_purge.py
|
||
```
|
||
|
||
이 중 purge 파일은 수집 결과 **23건**이며 전부 통과했다. 나머지 지정 6개 모듈은 129 passed, 1 skipped다. skip은 브리프에 명시된 untracked `.claude/skills` 링크 부재다. 전체 스위트는 실행하지 않았다.
|
||
|
||
## 수정 필요 사항
|
||
|
||
### R1 — P2: 분리한 attach 단계에 격리 서버 선택을 보존해야 한다
|
||
|
||
- 변경 근거: `.agents/skills/multi-agent-mux-resume/SKILL.md:30`, `:60`, `:63`.
|
||
- 동작 근거: `resume_session.sh:133`–`:144`는 자식 bash 안에서 서버/워크스페이스를 export하고, `:147`–`:158`은 attach 없이 반환한다. `.agents/skills/lib.sh:255`–`:269`의 shim은 서버 환경값을 실제 CLI의 `--session`으로 바꾼다. native herdr가 환경변수 자체로 라우팅하지 않는다는 사실은 create SKILL.md:147에도 명시돼 있다.
|
||
- 재현: lib.sh를 source하지 않은 새 셸에서 격리 서버의 세션에 대해 새 Workflow의 `bash .../resume_session.sh ... --herdr-session probe-isolated`를 실행한 뒤, 새 안내대로 `herdr agent attach probe-codex`를 실행한다. resume의 export는 부모 셸에 전달되지 않는다. 이미 caller가 다른 서버의 shim을 쓰고 있어도 자식의 설정은 caller의 서버를 바꾸지 않는다.
|
||
- 기대: 방금 resume한 `probe-isolated` 서버의 agent에 attach한다.
|
||
- 실제: 안내된 native 호출 argv에는 `--session probe-isolated`가 없으므로 기본/호출자 서버로 간다. 대상이 없으면 실패하고 동명 대상이 있으면 다른 대상으로 연결될 수 있다. 실제 서버에 접속하지 않고 임시 herdr argv 기록 대역으로 확인했다: `agent attach probe-codex`. 수정 호출은 `--session probe-isolated agent attach probe-codex`였다.
|
||
- 수정 방향: 별도 attach는 유지하되 `herdr --session <row.herdr_session 또는 명시한 서버> agent attach <name>`처럼 서버를 명시한다. default와 격리 서버의 방법을 구분하거나, 현재 셸에서 lib.sh와 정확한 서버 값을 설정하는 방법을 안내한다. child script가 parent 환경을 설정한다고 가정하지 않는다. 새 Workflow에서 source/export를 지웠으므로 verification의 bare `herdr agent get/read`도 같은 관점에서 확인한다.
|
||
|
||
### R2 — P2: dry-run을 무조건 무쓰기라고 설명하면 안 된다
|
||
|
||
- 변경 근거: `.agents/skills/multi-agent-mux-resume/SKILL.md:73`의 “write nothing”, `.agents/skills/multi-agent-mux-create/SKILL.md:164`의 “spawn and write nothing”.
|
||
- 동작 근거: resume `resume_session.sh:168`–`:169`는 macOS에서 agent binary의 quarantine 속성을 제거한다. dry-run 반환은 그보다 뒤인 `:225`–`:227`이다. create도 `create_session.sh:195`–`:196`에서 같은 동작을 하고 `:278`–`:281`에서 dry-run으로 반환한다.
|
||
- 재현: 임시 HOME/WORKSPACE_ROOT/AGENT_SESSIONS_YAML, 빈 합성 레지스트리, mock codex/herdr 및 호출을 기록하는 mock xattr를 준비한다. 실제 resume 진입점에 `--workspace <임시 root> --agent codex --session probe-codex --herdr-session probe-isolated --dry-run`을 전달한다.
|
||
- 기대: 새 옵션 표를 그대로 해석하면 파일/메타데이터 변경 작업도 호출하지 않는다.
|
||
- 실제: rc 0 및 `[dry-run] would spawn: ...`을 반환하면서 **xattr -d가 호출됐다**. 실제 사용자 binary의 속성은 수정하지 않았다. create의 동일 경로는 정적 대조로 확인했다.
|
||
- 수정 방향: 제품 코드를 바꾸지 말고 보장을 “agent spawn 및 session YAML/DB 갱신 생략”처럼 실제 범위로 한정하고, preflight가 binary 메타데이터 등 부수효과를 가질 수 있음을 적는다. 절대적인 무쓰기 보장을 원하면 별도 제품 변경이 필요하며 이 PR의 범위는 아니다.
|
||
|
||
### R3 — P2: 삭제된 (a) 블록의 복귀를 검사하는 가드가 없다
|
||
|
||
- 변경 근거: `tests/test_issue42_skill_blocks.py:121`–`:127`. `test_no_duplicate_block_left()`는 JSON의 class 문자열만 확인한다. `:108`–`:113`은 현행 문서와 현행 목록의 일치만 검사한다. 저장한 `removed[].base_sha`를 현재 블록과 비교하는 코드가 없다.
|
||
- 재현: 테스트 모듈을 가져오고, base resume SKILL.md:59–118의 삭제된 Workflow 본문(sha `63abe1bb3542`)을 현행 문서 끝에 다시 추가한 문자열을 만든다. 목록에도 해당 블록의 start/end/lang/sha를 정상 갱신하고 class=c, kind=example으로 기재한다. 파일을 수정하지 않고 모듈의 `skill_text`와 DATA에 메모리상으로 적용한 뒤 inventory, no-duplicate, replacement, exit-table, evidence 가드를 실행한다.
|
||
- 기대: 알려진 삭제 (a) 본문이 그대로 돌아왔으므로 inventory 갱신 여부와 무관하게 거부한다.
|
||
- 실제: 관련 가드가 전부 통과했다. 새 파일 설명의 “when an (a) block comes back” 보장과 다르다. inventory를 갱신하지 않은 일반 문서 변경은 잘 거부하지만, 그것은 별도의 보장이다.
|
||
- 수정 방향: 알려진 삭제 본문의 hash가 현재 파싱된 본문에 없는지 명시적으로 검사한다. 필요하면 공백 정규화 fingerprint도 추가하되 의미가 다른 예시까지 막지 않는다. 위 재삽입 + inventory 갱신을 회귀 사례로 둔다. 제품 코드 수정은 필요 없다.
|
||
|
||
## 추가 개선 및 가드의 한계
|
||
|
||
### C1 — baseline 37개에 대한 추적 하나가 빠졌다
|
||
|
||
base의 8개 SKILL.md를 실제로 파싱하면 **37개**, 현재는 **33개**다(현재 b=5, c=28). `removed`의 다섯 항목은 base 위치·종료 줄·hash가 모두 맞았다. 그러나 base create SKILL.md:263–266의 `create_session.sh --workspace "$WORKSPACE" ... --session "$SESSION_NAME"` 호출 블록도 삭제됐는데 historical 항목이 없다. 현재 JSON:87의 새 Workflow 항목은 base 168–220만 대체한다고 적고 있고, `prose_mismatches`의 C4는 없는 스크립트 이름을 고쳤다는 설명뿐이다.
|
||
|
||
이 호출은 (c)에 해당하며 삭제 자체가 기능 유실은 아니다. 다만 “37개를 모두 분류했다”는 단계 1의 감사 근거를 정확히 하려면 해당 base 블록을 (c)로 기록하고 새 호출과 통합됐다는 매핑을 추가해야 한다. 전체 baseline 목록과 현행 목록/삭제 이력을 구분하면 `removed`를 무조건 class=a로 강제하는 현재 스키마와도 충돌하지 않는다. 현재 가드는 DATA의 base/base_sha 자체를 검증하지 않는다.
|
||
|
||
### C2 — 종료 코드 스캐너는 현재 세 스크립트의 표에는 맞지만 일반 규칙으로는 부정확하다
|
||
|
||
`tests/test_issue42_skill_blocks.py:68`–`:78`은 heredoc 본문에서 주석을 거르기 전에 SystemExit를 수집하고, heredoc 명령의 `|| true` 여부를 보지 않는다. 반대로 unguarded Python의 `sys.exit(N)`은 수집하지 않는다. 임시 입력에 대한 `_scan_exits()` 탐침 결과:
|
||
|
||
| 입력 | 실제 shell 종료 의미 | 스캐너 결과 |
|
||
|---|---|---|
|
||
| heredoc 안 `# raise SystemExit(9)` | 주석: 9 아님 | passthrough 9 포함 |
|
||
| unguarded heredoc 안 `sys.exit(9)` + set -e | 9 전달 | passthrough 비어 있음 |
|
||
| heredoc 안 `raise SystemExit(9)` + 명령 뒤 `|| true` | 9 흡수, 다음 exit 0 | passthrough 9 포함 |
|
||
|
||
현재 파일의 `SystemExit(4)`/`SystemExit(1)`은 실제 unguarded atomic write라 표가 맞고, stop의 `sys.exit(7)`은 `|| { exit 1; }`로 감싸져 있으므로 7을 제외한 것도 맞다. 따라서 이 부분에서 **현재 종료 코드 표가 틀렸다고 판정하지는 않는다**. 다만 정상적인 주석 추가가 가드를 깨거나 동등한 `sys.exit` 변경을 놓칠 수 있다. Python 본문은 주석/문자열을 제외하고 두 exit 형태를 처리하며, passthrough 여부는 해당 호출의 처리 방식으로 제한하는 작은 fixture 테스트를 권장한다. 범용 shell 파서를 만들 필요는 없다.
|
||
|
||
숫자 행 가드는 설명 열의 의미까지 검증하지 않으므로 아래 종료 경로 검토는 별도로 수행했다. 줄 번호/hash를 고정하는 inventory는 prose 편집으로 블록 위치만 바뀌어도 함께 갱신해야 한다. 이는 설계상 비용이며 판정 사유로 삼지 않았다.
|
||
|
||
## 요청 항목별 대조
|
||
|
||
### 1. 삭제 분류와 resume의 단계별 대응
|
||
|
||
짧은 제품 경로는 `.agents/skills/` 아래다.
|
||
|
||
| 삭제된 resume 단계 | 실제 근거 / 판단 |
|
||
|---|---|
|
||
| session ID 해석 | resume_session.sh:111–112 → resolve_session_id.sh |
|
||
| ID 없는 Class A fresh-spawn | :122–130, :172–184; Class B는 :127–128에서 실패 |
|
||
| 서버/label 선택 | :133–144 |
|
||
| herdr alive면 attach | :147–158은 row 갱신 + 종료이며 attach 없음. 불일치 R1/R2가 JSON에 기록됐고 별도 attach로 정정한 방향은 맞지만 서버 선택 보완 필요(위 R1) |
|
||
| agent command / 기동 | :172–197의 adapter/fallback, :200–211 binary 검사, :237 spawn |
|
||
| handle_startup_dialogs | :241, 이어서 :244 sleep |
|
||
| update_yaml_resumed.sh | :247–250; agent/workspace/server/cmd-full 등 옵션 전달. 이미 살아 있는 경로는 :154–157 |
|
||
| 마지막 attach | 스크립트에 없음. 사람이 별도 실행해야 하는 동작을 prose로 남겼다. 순수한 “모든 줄이 그대로 중복”은 아니며 JSON mismatch가 그 차이를 명시한다 |
|
||
|
||
나머지 삭제 항목: create preflight는 create_session.sh:103–158, Workflow는 :160–168 / :207 / :249–274 / :361–412, YAML append는 :491–610에 구현돼 있다. stop preflight는 stop_session.sh:99 / :125–143 / :184–188에 있다. 기본 stop의 no-op은 purge가 아닐 때만 적용된다. 기존의 잘못된 양 agent 동시 체크, 고정 sleep, 없는 yaml-edit 안내 등을 제거한 것은 타당하다.
|
||
|
||
남겨진 (b) 다섯 개도 타당하다: create/resume/stop의 사후 검증 블록은 운영자가 결과를 확인하는 절차이고, monitor runbook에는 새 drift만 사람에게 보고하기·중지 판단·hard rules가 있다. status preflight에는 status.sh가 하지 않는 `has_real_herdr` 체크가 섞여 있다. 이런 사람/agent 측 절차를 이번 단계에서 지우지 않은 판단은 맞다. 단, 새 진입점 호출은 caller 셸의 변수나 서버 선택을 설정하지 않는다는 R1을 반영해야 한다.
|
||
|
||
### 2. 옵션, 종료 코드, 다섯 정정
|
||
|
||
create parser :74–93 및 resume parser :35–47와 새 옵션을 대조했다. stop의 세 호출 예제는 `--agent`를 유지하고 purge에 `--yes`가 추가됐다. 임시 HOME/WORKSPACE_ROOT/AGENT_SESSIONS_YAML로 create/resume/stop 실제 `--help`를 실행해 모두 rc 0을 확인했다. stop help의 짧은 agent 목록 등 기존 제품 help 불일치는 이 PR에서 새로 만든 문제가 아니다.
|
||
|
||
- resume Case 3: no-attach/permission flag 미검사 정정은 스크립트와 일치. 별도 attach의 라우팅만 R1.
|
||
- purge: stop_session.sh:191–198에서 --yes 없으면 rc 3. 문서 :57은 사용자 확인 후에만 --yes를 허용하므로 판단 단계도 보존됐다.
|
||
- status JSON: status.sh:209–239의 sessions_detail 필드 및 reconcile.sh:1083–1091의 상위 필드와 일치. resume_state 값도 일치한다.
|
||
- create cmd_full: spawn-spec 경로와 일치. 없는 yaml-edit 스크립트 참조 제거는 올바르다.
|
||
- create exit 4: 단순 header 숫자가 아니라 :515의 SystemExit(4)가 전달된다. trap :320–352가 rollback한다. exit 43은 kill 실패 후 살아 있는 경우다. exit 42는 ready timeout의 abort 경로이며 `MAM_KILL_ON_READY_TIMEOUT=1`로 살아 있는 agent도 rollback 대상으로 삼을 수 있다는 추가 설명이 있으면 더 정확하다.
|
||
- resume의 post-spawn update rc 1/2, stop의 post-stop SystemExit(1) 구분을 표에 남긴 점은 맞다. `other`는 set -e를 통해 전달되는 실패를 포함한다.
|
||
|
||
### 3–5. 문서 사용성·가드·범위
|
||
|
||
사용자 purge 확인은 유지됐고 호출 예제는 실행 진입점으로 통일됐다. 기존 `test_comp_docs_stop_examples_pass_agent`도 선택 스위트에서 통과했다(최소 3개, 모두 --agent). 일반 inventory drift를 잡는 효과는 확인했다: 메모리상으로 create/resume/stop/status 각각을 base 문서로 되돌리면 현행 inventory 가드는 네 경우 모두 실패한다. 다만 알려진 삭제 본문 복귀는 R3처럼 누락된다.
|
||
|
||
제품 코드 변경·버전 bump·push/merge/이슈 상태 변경 없음. 실제 세션·herdr 서버·실제 .mam.env 값은 접근하지 않았고, 탐침은 합성 env/임시 HOME/임시 registry/mock binary만 사용했다. 검증 트리와 source/main 코드는 수정하지 않았다. 실제 .mam 쓰기는 이 잡의 지시된 lifecycle 및 최종 리포트에만 한정했다.
|
||
|
||
### 6. “purge 12개 기준 실패” 주장
|
||
|
||
이번 macOS 실행에서 해당 파일은 23/23 통과했다. base..head에서 그 테스트와 purge 구현은 변경되지 않았다. 따라서 이 PR이 purge 회귀를 만들었다는 증거는 없지만, 이 호스트에서도 12개 실패가 당연하다는 주장은 뒷받침되지 않는다. PR의 전체 실행 환경·실패 로그·동일 환경 base 비교가 있어야 Linux의 알려진 실패와 동일하다고 결론 낼 수 있다. 이번 리뷰에서는 외부 PR을 수정하거나 전체 스위트를 재실행하지 않았다.
|
||
|
||
## 독립 탐침 재현 자료
|
||
|
||
- `/tmp/fbd52b0b-probes.py`: safe --help/--dry-run, attach argv, 삭제 Workflow 복귀 가드 탐침. 결과 `/tmp/fbd52b0b-probes.log`.
|
||
- `/tmp/fbd52b0b-guard-probes.py`: base 문서 거부 확인, 종료 코드 스캐너의 합성 입력. 결과 `/tmp/fbd52b0b-guard-probes.log`.
|
||
- 실행: 위 검증 cwd에서 `PYTHONDONTWRITEBYTECODE=1 <위 Python> /tmp/fbd52b0b-probes.py "$PWD"`, 이어서 같은 인터프리터로 guard-probes.py 실행. 모두 종료 코드 0이며, 이는 탐침 실행 성공을 뜻한다. 발견한 문서/가드 문제의 해결을 뜻하지 않는다.
|
||
|
||
[VERDICT: NOT PASS]
|