release(v4.11.0): attach uses the herdr-registered name; sessions start and stop only through the skills (Issue #71) - archive the review reports, bump the lockstep version

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-27 07:28:47 +09:00
1 parent 5f6065344f
commit 342a9a4edf
24 files changed
+682 -26

No files matched your search

@@ -0,0 +1,311 @@
# PR #73 (Issue #71) 리뷰 — head 5f60653
- 리뷰어: planner-reviewer-claude-01 (macOS Darwin 25.6, `/bin/bash` 3.2.57, `/usr/bin/python3` 3.9.6, BSD sed/tr)
- 검증 경로: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261026T133440Z-c8961b/2`
- identity 확인 (시작·종료 시 모두):
- HEAD 5f6065344f800c2c5ad6f802e95e9e8ff5cc7b26, tree 98d57bdc60920238c115ece90c90cc694f2744bc
- `git status --porcelain --ignored` 비어 있음
- `.mam` 없음. 추적 파일 `.mam.env.example` 만 있다.
- main HEAD 29cc4fd 그대로.
- 범위: 29cc4fd..5f60653, 커밋 14개, 파일 28개 (+953/−176).
- 읽은 것: 이슈 #71 본문과 댓글("범위 정리" 1–10항, #72 분리), PR #73 본문.
## 0. 이번 잡(f39ae4ba)에 대해
- 이 잡은 같은 루프의 검증 경로 `/2` 다. head 5f60653, tree 98d57bdc 는 앞 잡 0698ffc5(검증 경로 `/1`) 와 같다. 브리프의 확인 항목도 잡 ID와 경로를 빼면 글자까지 같다.
- 그래서 아래 1장의 분석(파일:줄 근거, 리뷰어 시험 p1–p12, 변이 m1–m12, base 비교)은 같은 tree 의 `git archive` 사본에서 얻은 결과를 그대로 쓴다.
- 이번 경로 `/2` 에서 새로 확인한 것:
- 브리프에 붙은 diff(1920줄)가 `git diff 29cc4fd 5f60653` 와 바이트까지 같다. 커밋은 14개다.
- `/2` 의 `lib.sh` 에서 `_sanitize_herdr_agent_name` 을 다시 뽑았다. 정의는 하나이고, 앞서 비교한 정의와 같다.
- `/2` 의 `lib_py` 를 기준으로 bash↔Python 비교를 다시 돌렸다. 408건에 불일치는 0이다. python3 와 해시 도구가 모두 없는 5건도 기대대로 동작했다.
- 테스트 세 묶음을 `/2` 에서 다시 돌렸다. 결과는 아래 2장 표와 같다.
- issue71+issue66: 77 passed
- A: 384 passed, 1 skipped
- B: 543 passed, 3 skipped
- 실제 herdr 서버 두 개(pid 11655, 69309)는 실행 전후로 같다.
- 시작과 끝에 `/2` 의 HEAD·tree 를 확인했다. `status --ignored` 는 비어 있고 `.mam` 은 없다. main HEAD 는 29cc4fd 그대로다.
## 1. 확인한 것과 근거
### 1) 완료 조건과 범위
| 항목 | 결과 | 근거 |
|---|---|---|
| 1 attach 형태 | 충족 | 아래 3) |
| 2 `default` 도 적음 | 충족 | 아래 3) |
| 3 래퍼 attach | 충족 | 아래 3) |
| 4 두 필드 삭제 | 충족 | 아래 4) |
| 5 원칙(스킬로 시작·종료), 행 없는 stop | 충족 | 아래 4), 5) |
| 6 attach 는 예외 | 충족 | attach 만 herdr 명령을 직접 쓴다 |
| 7 변환 규칙 하나 | 충족 | 아래 6) |
| 8 해시 불가 시 오류 | 충족 | 아래 6) |
| 9 마이그레이션 | 충족 | 아래 7) |
| 10 stall_watch 한 곳 | 충족 | 아래 8) |
- 보강 완료 조건도 모두 충족한다.
- 새 행에 두 필드가 없다.
- 해시 도구가 없으면 python3 로 같은 결과를 내고, python3 도 없으면 오류다.
- 옛 행의 마이그레이션은 멱등이고, YAML 과 DB 가 같다.
- 행 없는 stop 은 레지스트리를 바꾸지 않는다.
- 정리 안내는 stop 스킬 명령이다.
- #72(update 스킬)는 끌어들이지 않았다.
- 진입점 `python3 -m lib_py.registry_format` 만 두었다.
- 그 진입점을 부르는 코드는 없다.
- 본문 완료 조건 6(실제 에이전트를 32자 초과 이름으로 띄워 attach)은 수동 확인 항목이다. 브리프 제약(실제 에이전트·실제 herdr 금지)에 따라 하지 않았다.
- 대신 형태가 맞는지는 확인했다. 실제 herdr 0.9.5 의 `agent attach --help` 를 격리 HOME 에서 보면 사용법이 `herdr agent attach <TARGET> [OPTIONS]` 이고, 옵션은 `--takeover` 다.
### 2) 회귀 테스트 tests/test_issue71_herdr_names.py (70개)
- head (검증 경로): issue71 70개와 issue66 7개를 함께 돌려 77 passed.
- base: 29cc4fd 사본에 head 의 테스트 파일을 넣었다. conftest 는 이 PR 에서 바뀌지 않았다.
- 결과: **68 failed, 2 passed**. PR 이 말한 숫자와 같다.
- 통과하는 2개:
- `test_the_names_need_converting_and_the_plain_one_does_not`: 변환 전제를 확인한다.
- `test_wrapper_attach_calls_herdr_with_the_herdr_name[PLAIN]`: 변환이 필요 없는 이름이라 base 에서도 맞다.
- reconcile 경로 (`test_reconcile_registers_with_the_attach_command`, test :126-139) 는 행 등록 경로에 실제로 닿는다.
- mock herdr 에 `cwd=mam_sandbox`, `pane_id` 가 있는 agent 를 넣는다.
- 실행 전에 행이 없음을 확인한다 (:132).
- 실행 뒤 `last_visible_note == "auto-registered by monitor"` 인 행이 하나인지 확인한다 (:137-138).
- 변이 m10 (reconcile 을 옛 형태로) 이 이 테스트 3개를 실패시킨다.
### 3) attach_command (`lib_py/agents/sanitize.py:34-43`)
- 형태: `herdr --session <shlex.quote(server or "default")> agent attach <shlex.quote(sanitize(name))>`.
- 서버가 None 이나 빈 값이어도 `--session default` 가 된다.
- 변환된 이름은 `[a-z0-9_-]` 이므로 quote 를 해도 그대로다.
- 서버 `srv x` 는 `'srv x'` 로 감싸진다. 내 시험과 test :51 에서 확인했다.
- 쓰는 곳은 네 곳이고, 모두 이 함수를 쓴다:
- create: `create_session.sh:566`
- resume 의 새 행: `update_yaml_resumed.sh:151`
- resume 의 서버 변경: `update_yaml_resumed.sh:161`
- reconcile: `reconcile.sh:676`
- 마이그레이션도 같은 함수를 쓴다 (`registry_format.py:62`).
- resume 의 else 분기는 서버가 바뀔 때만 attach_command 를 다시 쓴다 (:159-161).
- 서버가 그대로인 기존 행은 첫 쓰기의 마이그레이션이 이미 새 형태로 바꿔 둔다. 그래서 낡은 값이 남지 않는다.
- lib.sh 래퍼 (`lib.sh:605-613`):
- `agent attach <t> ...` 의 첫 인자를 `_resolve_herdr_target` 으로 변환한다. get/read/prompt 와 같은 함수다.
- 나머지 `"$@"` 는 그대로 넘긴다.
- 테스트 :151-159 가 strict mock 에서 `["agent","attach",<herdr 이름>,"--takeover"]` 를 확인한다.
- 변이 m4 (원래 이름을 넘김) 는 LONG·UPPER 에서 잡힌다.
### 4) kill_command·start_command 삭제
- 코드·문서에는 이 두 키가 남아 있지 않다. 리뷰 트리 전체를 grep 했다.
- 남은 곳: 마이그레이션 상수 (`registry_format.py:37`), 테스트의 "옛 행" 픽스처와 부재 단언, 과거 리뷰 보관본 `.agents/reports/*`.
- `status --json` 의 `start_command` 키는 빠졌다 (`status.sh:224-228`).
- 이 키를 읽는 코드나 테스트는 없다.
- status SKILL.md 의 예시도 같이 고쳤다.
- 래퍼의 `new-session`/`kill-session` 가짜 명령은 남겨 두는 것이 맞다. 아직 쓰는 곳:
- create (`create_session.sh:266-276`, :346)
- stop (`stop_session.sh:157`, :307, :321)
- reconcile
- 사용자에게 보이던 kill-session 안내 두 곳은 stop 스킬 명령으로 바뀌었다:
- create exit 43 (`create_session.sh:364`)
- id_order (`id_order.py:343-345`)
- 바뀐 명령 `stop_session.sh --session X --herdr-session S` 는 행이 없어도 동작한다 (아래 5).
- exit 43 은 `_mark_row_rolled_back "running" "rollback-failed"` 로 행을 남기므로, 그 경우는 행 있는 stop 경로를 탄다.
### 5) 행이 없는 stop (`stop_session.sh:117-191`)
- **행이 없을 때만 그 경로로 간다.**
- 행을 찾는 Python 은 행이 없을 때만 `sys.exit(7)` 을 낸다.
- `[ "${MAPPED_RC:-0}" -ne 7 ] || stop_rowless` (:189) 로 7 일 때만 row-less 로 간다.
- 다른 실패는 `cannot read the row` 로 exit 1 이다 (:190-193).
- 변이 m6 (0 이 아니면 모두 row-less) 은 `test_stop_does_not_take_an_unreadable_row_for_no_row` 에 잡힌다.
- **레지스트리를 읽지 못할 때**는 `load_state_json strict` (:170, lib.sh:1598-1631) 가 exit 1 로 끝난다.
- 해당 경우: YAML 깨짐(DB 없음), DB 손상, sessions 테이블 없음, 테이블 전부 없음.
- 테스트 :514-541 의 4개 매개변수가 이를 확인한다.
- 인자 없는 기존 호출은 동작이 같다. strict 일 때만 raise 하거나 exit 한다.
- **작업 공간 판정** (:142-151) 은 `realpath` 두 개를 비교한다: `cwd == root or cwd.startswith(root + os.sep)`. 내 추가 시험 (head 사본, `tests/test_zz_review71.py`) 결과:
| 경우 | 결과 |
|---|---|
| p1 접두어 혼동 (`<ws>-other`) | exit 5, 닫지 않음 |
| p2 밖에서 작업 공간 안으로 가는 심볼릭 링크 | 0, 닫음 |
| p3 작업 공간 안에서 밖으로 가는 심볼릭 링크 | exit 5 |
| p4 후행 슬래시 | 0 |
| p5 WORKSPACE_ROOT 를 심볼릭 링크로 지정 | 0 |
| p7 32자 초과 이름 (strict mock) | 0 |
- **서버 결정 순서** (:107-113, :128-130) 는 `--herdr-session` → 환경의 `HERDR_SESSION_NAME` → 작업 공간 서버다.
- 환경 값이 비었거나 `default` 이면 작업 공간 서버로 간다. create 의 :185, :253 과 같은 규칙이다.
- p6 (환경 값 `default`) 에서 모든 `agent get` 이 작업 공간 서버로 간 것을 확인했다.
- 테스트 :487-499 는 환경 서버를 확인한다.
- **레지스트리를 바꾸지 않는다.** 모든 row-less 테스트가 YAML/DB 해시를 비교한다 (`_registry_bytes`).
- **`--purge-conversation`** 은 행이 없으면 exit 1 이다 (:124-126, test :544-550).
- **종료 코드:**
- 0 / 1 / 5 / 6 은 SKILL.md 표 (:58-67) 와 헤더 주석 (:30-36), 코드 (:134, :154, :161, :163) 가 같다.
- `test_literal_exit_rule_on_base_scripts` 는 `{0..6}` 이다.
- skill_code_blocks 의 `entry_points` 도 5·6 을 더했다.
### 6) 이름 변환 (`lib.sh:44-82`)
- 정의는 하나다 (`lib.sh:44`). test :171 이 확인한다.
- shim 은 `{ printf shebang; declare -f _sanitize_herdr_agent_name; cat <<'EOF' … } > "$tmp_file"` (:179-1393) 으로 같은 정의를 받는다.
- bash 3.2 의 `declare -f` 출력을 다시 source 한 사본도 아래 비교에 넣었다.
- 이 호스트의 `bash` 는 `/bin/bash` 3.2 하나뿐이라, 테스트의 shim 도 3.2 로 돈다.
- has-session 의 `lambda s: s.lower()[:32]` 대체와 `os.path.abspath(".agents/skills")` import 는 없어졌다.
- 대신 lib.sh 가 계산한 `TARGET_SAN` 을 넘긴다 (:633-641).
- **macOS 실측 비교:** 이름 34개를 Python `sanitize_herdr_agent_name` 과 비교했다.
- 이름: ASCII, 대문자, 32/33/40/200자, 숫자·`_`·`-n`·`-e`·`--help` 로 시작, `\n`(가운데·앞·뒤·연속), `\t`, `\r`, `*?[a]`, `\`, `%s`, `Ü`, `İstanbul…`, `日本語`, 이모지, `K`(U+212A), `ß`, `Dž`, `Ω…` 긴 이름, NBSP 등.
- 원본 정의와 `declare -f` 사본 2 벌 × 환경 6 개 (`LANG=en_US.UTF-8`, `LC_ALL=C`, 로캘 없음, `LC_ALL=UTF-8`+`LC_CTYPE=C`, 해시 도구 없이 python3 만 + UTF-8 또는 C) = **408 건, 불일치 0**.
- ASCII 가 아닌 이름은 바이트 단위 치환을 쓰지 않고 python3 의 문자 단위 `str.lower()`+치환을 쓰므로 Python 과 같다 (:51-56).
- **해시 도구별:** `/sbin/sha1sum` 만, openssl(LibreSSL) 만, 깨진 shasum(빈 출력)에서 openssl 로 넘어가는 경우 모두 Python 과 같았다 (`mam-wt-issue-71-very-lo-54e536a7`).
- **python3 도 해시 도구도 없을 때:**
- 짧은 ASCII 이름(대문자, 끝의 `\n` 포함)은 정상이다.
- 32자 초과는 `no shasum, sha1sum, openssl or python3` 오류로 rc 1 이다.
- ASCII 가 아닌 이름은 `not ASCII and there is no python3` 오류로 rc 1 이다.
- `00000000` 은 어디에도 없다.
- 호출하는 곳은 실패를 넘긴다: `|| return 1`, `|| exit 1`, `wait_for_agent_idle` 은 `|| return 3`, delegate-job 은 진단만 건너뛴다.
- spawn 의 `agent_name` 계산은 루프 앞 (:688) 으로 옮겨졌다. 그래서 이름을 못 구하면 spawn 하지 않는다. test :267 이 확인한다.
### 7) 레지스트리 형식 2 (`registry_format.py`, `atomic_yaml.py:118-137, :192`)
- **순서:** 잠금 → `BEGIN IMMEDIATE` (:79-87) → DB 또는 YAML 읽기 → 마이그레이션 (:121-126) → 호출자 변경 (:144-149) → 검증 → DB 쓰기 → commit → YAML 쓰기.
- 마이그레이션은 잠금 안에서, 호출자 변경 전에 돈다.
- **모든 행을 바꾼다.** `migrate` 는 이름이 있는 모든 행에서 두 키를 지우고 attach_command 를 다시 만든다.
- stopped·terminated 도 포함한다. 변이 m8 (running 만) 은 6 failed 로 잡힌다.
- **YAML 도 반드시 다시 쓴다.** `force_yaml = True` (:126) 이고, 변이 m2 는 6 failed 로 잡힌다.
- **멱등이다.** test :327-338 이 확인하고, 진입점을 두 번 부르면 `2 -> 2, rows changed: 0` 에 바이트도 같다 (test :342-362).
- **YAML 교체가 실패한 뒤의 복구:** DB 가 2 이고 YAML 이 2 미만이면 다음 쓰기에서 `force_yaml` 이 된다 (:127-137).
- 진입점으로 하든 아무 쓰기로 하든 복구된다 (test :379-411).
- 변이 m3 은 2 failed 로 잡힌다.
- **다른 쓰기 경로:** 레지스트리 YAML/DB 를 쓰는 코드는 `atomic_yaml` 하나뿐이다.
- grep 결과 `safe_dump`, `INTO state/sessions` 는 이 파일에만 있다. hermes.py 의 DELETE 는 에이전트 자신의 DB 다.
- orc_onboard 와 autopin 도 `atomic_dump_yaml(_main)` 을 거친다.
- 호출자 변경 가운데 `d` 를 통째로 새로 만드는 것은 없다. 만들어도 다음 쓰기가 다시 마이그레이션하므로 형식 키를 잃지 않는다.
- **읽기 경로:** `load_state_json` 과 `lib_py.state` 는 바꾸지 않았다. strict 인자만 더했다.
- **미래 형식 (p8):** `registry_format: 3` 이면 진입점은 `3 -> 3, rows changed: 0` 이다. 쓰기 뒤에도 키는 3 으로 남고, 행(옛 키 포함)도 그대로다. 내려 쓰지 않는다.
- **비정상 값 (p9):** `"2"`, `True`, `None` 은 1 로 보고 2 로 바꾼다. `0` 은 `0 -> 2` 로 출력된다.
- **진입점 계약:**
- 성공은 stdout 한 줄 `registry-format: <from> -> <to>, rows changed: <n>` 에 exit 0 이다.
- 레지스트리가 없으면 exit 1 이다 (test :423).
- DB 손상 (p10) 은 stderr `registry-format: error: file is not a database` 에 exit 1 이고 stdout 은 비어 있다.
- 검증 거부 (p11) 는 `registry-format: error: VALIDATE: … bad status` 에 exit 1 이다.
- 모두 docstring 과 같다.
- **macOS persistent WAL:** YAML/DB 바이트 비교 테스트(진입점 재실행, row-less stop)가 이 호스트에서 모두 통과했다.
### 8) stall_watch
- `StallWatch.__init__` 이 `self.herdr_name` 을 한 번 구한다 (:229).
- 그 값을 `HerdrSource(..., name=self.herdr_name)` (:230) 와 `wait_once` (:332) 에 같이 쓴다.
- `HerdrSource` 는 keyword-only `name` 이 필수다. 원래 이름으로 대신 쓰는 경로는 없다.
- 다른 호출자는 `test_issue60_linux_fixes.py:99` 하나이고, `name="t"` 를 넘기도록 바뀌었다.
- `tests/test_issue66_stall_watch_sanitized_name.py` 는 diff 가 없고, 7 passed 다.
### 9) 바뀐 기존 테스트
- `test_tier2_component`: `HERDR_SESSION_NAME=` 포함 여부 대신 새 attach 형태 전체가 같은지와 두 키가 없는지를 본다.
- 서버 전파와 라벨이 새지 않는지를 보는 원래 의도는 유지되고, 단언은 더 엄격해졌다.
- `test_issue50_pane_lock:173`: create 가 쓴 행이라는 증거를 `start_command` 대신 `session_id_source` 로 본다.
- reconcile 의 행 (`reconcile.sh:666-680`) 에는 이 키가 없다. 그래서 증거로서 같은 구분을 한다.
- `test_issue21_worktree_prereqs:584`: 제어 루트 경로가 새지 않는지를 attach_command 에서 본다. 의도가 같다.
- `test_tier1_unit` / `test_tier3_integration`:
- "agent 를 추론할 수 없으면 exit 2" 를 이제 **행이 있는** 세션으로 확인한다. 행이 없으면 row-less 경로로 가기 때문이다.
- 의도가 유지된다.
- `test_issue63`, `test_issue23_24` S9: 안내 문구를 새 명령으로 바꿨다.
- `test_layout`, `test_issue42`: heredoc 시작 위치와 종료 코드 집합을 따라 고쳤다.
- `tests/data/skill_code_blocks.json`:
- 블록 33개 가운데 sha 가 바뀐 것은 문서 내용이 바뀐 두 블록이다: create :134 (YAML 행 예시), status :74 (JSON 예시).
- 나머지는 줄 이동과 evidence 줄 번호다.
- `entry_points` 의 stop 종료 코드에 5·6 을 더했다.
- 문서 diff 와 맞다.
- **S11** (`test_issue23_24_startup_path.py:294-316`):
- 비교 기준을 `0fc162e` 로 두었다. `0fc162e..HEAD` 사이 lib.sh 변경은 5f60653 의 `load_state_json` 하나뿐이고, shim heredoc 과 sanitize 함수는 바뀌지 않았다.
- merge commit 으로 병합하면 0fc162e 가 main 이력에 남으므로 계속 맞다.
- 기준을 찾지 못하면 `pytest.skip` 한다. 이는 base(525df2b) 때부터의 동작이다. git 이 없는 설치본이나 squash 병합에서 건너뛰는 것은 기존 설계 그대로라고 본다.
- 검증 경로(git worktree)에서는 **skip 이 아니라 통과**했다 (`-k s11`: 1 passed).
- squash 하면 이 확인이 사라지므로 PR 본문의 "merge commit 필수" 를 지켜야 한다.
### 10) macOS 호환
- 바뀐 Python 6개 파일이 `/usr/bin/python3` 3.9.6 에서 compile 된다.
- `attach_command` 와 `migrate` 가 3.9 에서 실행된다.
- 바뀐 셸 줄에 `declare -A`, `mapfile`, `${x,,}`, `sed -i`, `stat -c`, `readlink -f`, `local -n` 이 없다.
- 이름 변환은 BSD tr/sed 와 bash 3.2 에서 위 6) 처럼 확인했다.
## 2. 실행한 테스트
- 실행 조건:
- 모두 `env -i PATH=/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin`, 임시 HOME/XDG_DATA_HOME 에서 돌렸다.
- `PYTHONDONTWRITEBYTECODE=1 -p no:cacheprovider --basetemp=<스크래치>` 를 썼다.
- `.mam.env` 는 export 하지 않았다.
- 실제 herdr 서버 두 개는 테스트 전후로 같다: pid 11655 `herdr server`, 69309 `herdr --session multi-agent-mux server`, 시작 시각도 같다.
| 대상 | 결과 |
|---|---|
| 검증 경로: test_issue71_herdr_names + test_issue66_stall_watch_sanitized_name | **77 passed** (`/1`, `/2` 모두) |
| base 29cc4fd 사본 + head 의 issue71 테스트 | **68 failed, 2 passed** |
| 검증 경로 A: tier1_unit, tier2_component, tier3_integration, issue23_24_startup_path, issue50_pane_lock, issue21_worktree_prereqs, issue63_same_cwd_ids, issue60_linux_fixes, layout, issue42_skill_blocks | **384 passed, 1 skipped** (`/1`, `/2` 모두; skip: issue42 의 `.claude/skills is untracked; absent in a worktree`, 환경 요인) |
| 검증 경로 B (stop·reconcile·create·resume 인접): b4_session_created, b19_headless_reconcile_fixes, c1_tui_readiness, issue15_registry_isolation, issue17_workspace_root_isolation, issue31_purge_residual, issue24_resume_dialogs, issue40_auto_pin, issue45_restore, issue6_fail_closed, issue9_server_env, issue64_ro_connect, uuid_target, workspace_scope, issue57_logs_follow_registry, issue30_stall_watch, issue30_stall_watch_stop, issue50_dispatch_herdr, issue60_opencode_v2, issue36_prompt_transport, a4_adapter_contract, issue26_ints | **543 passed, 3 skipped** (`/1`, `/2` 모두) |
| S11 단독 (검증 경로) | 1 passed (skip 아님) |
| 리뷰어 추가 시험 (head 사본, p1–p12) | 14 passed. 1개는 내 기대값 오류다 (형식 0 → "0 -> 2" 출력, 동작은 정상) |
| bash↔Python 이름 변환 비교 | 408 건 불일치 0, 도구 없음 5 건 모두 기대대로 (`/2` 의 lib.sh·lib_py 로 다시 실행) |
변이 (head 사본, issue71 70개):
| # | 변이 | 결과 |
|---|---|---|
| m1 | 작업 공간 판정을 `startswith(root)` 로 (접두어 혼동) | **살아남음** (리뷰어 p1 은 잡음) |
| m2 | 마이그레이션 뒤 YAML 강제 쓰기 끔 | 6 failed |
| m3 | YAML 복구 분기 끔 | 2 failed |
| m4 | 래퍼 attach 에 원래 이름 | 2 failed |
| m5 | 줄바꿈 → `-` 제거 | 6 failed |
| m6 | rc≠0 이면 모두 row-less | 1 failed |
| m7 | strict 에서 state 테이블 오류만 무시 | **살아남음** (아래 N3) |
| m8 | running 행만 마이그레이션 | 6 failed |
| m9 | 서버 없으면 `--session ''` | 3 failed |
| m10 | reconcile 을 옛 attach 형태로 | 3 failed |
| m11 | 행 없는 stop 이 환경 서버를 무시 | 1 failed |
| m12 | 해시 실패 시 `00000000` | 4 failed |
## 3. 차단 결함
없음.
## 4. 비차단
### N1: 행 없는 stop 이 제어 루트 밖 워크트리의 세션은 닫지 못한다
- 상황:
- `worktree_prepare.sh:229-236` 은 creator 를 `WORKSPACE_ROOT=$MAIN` 과 `--workspace $WT` 로 만든다.
- `$WT` 는 `--path` 로 받은 임의의 경로이고, 보통 제어 루트 밖이다. 루프의 `mam-wt-*` 나 `$TMPDIR/mam-review/…` 가 그 예다.
- 결과:
- 그런 create 가 행을 등록하기 전에 죽으면, id_order 의 안내 (`id_order.py:343-345`) 는 stop 명령을 준다.
- 그 stop 은 `realpath(cwd)` 가 `WORKSPACE_ROOT` 밖이므로 exit 5 로 거부한다 (`stop_session.sh:151-154`). 다른 작업 공간 테스트 (test :467) 와 같은 경로다.
- 거부는 안전하다(아무것도 바꾸지 않음). 하지만 이 경우에는 안내된 스킬 명령으로 정리할 수 없다.
- 권고: 둘 중 하나.
- (a) row-less 판정에 "이 제어 루트에 묶인 워크트리(`git worktree list`)" 도 넣는다.
- (b) exit 5 메시지에 그 경우의 정리 방법을 적는다.
### N2: 접두어 혼동을 고정하는 테스트가 없다
- m1 이 PR 테스트에서 살아남는다. `<ws>-other` 를 cwd 로 하는 한 줄짜리 시험을 더하기를 권한다. 내 p1 이 그 형태다.
### N3: strict 읽기에서 state 테이블만 없는 DB 는 걸리지 않는다
- m7 이 살아남은 이유는 테스트의 `no-tables` 가 sessions 테이블 오류로도 잡히기 때문이다.
- state 만 없는 DB 에서도 stop 은 sessions 테이블로 행을 정확히 찾으므로 영향은 없다.
### N4: 이 PR 이 건드리지 않은 attach 안내가 남아 있다 (모두 base 부터 있던 것)
- `create_session.sh:770`: `Attach: herdr session attach $SESSION_NAME`. 서버를 적지 않고, 이름도 변환하지 않는다. 행의 `attach_command` 를 출력하기를 권한다.
- `multi-agent-mux-delegate-job:1021`: `HERDR_SESSION_NAME=… herdr agent attach $sess (lib.sh를 source한 셸에서)`. 래퍼가 이제 이름을 변환하므로 동작은 하지만, 새 형태와 다르다.
- `deploy/install_mam.sh:330`: `HERDR_SERVER_NAME=… herdr agent attach`. 없는 환경 변수 이름이다.
### N5: 래퍼 attach 는 대상이 첫 인자라고 가정한다
- `herdr agent attach --takeover <name>` 순서로 쓰면 래퍼는 `--takeover` 를 대상으로 찾다가 exit 1 을 낸다.
- herdr 의 사용법(`<TARGET> [OPTIONS]`)과 문서의 순서대로라면 문제가 없다.
### N6: YAML 이 아직 없는 작업 공간에서는 행 없는 stop 이 동작하지 않는다
- 처음 만든 작업 공간에서 첫 create 가 행 등록 전에 죽어 YAML 이 아직 없으면, stop 은 행 없는 경로 전에 `YAML not found` 로 exit 1 한다 (:101).
- 드문 경우다.
### N7: 정리할 만한 작은 것들
- `create_session.sh:519` 의 `server_opt` 는 쓰는 곳이 없다. base 에서도 그랬다.
- shim 내용이 바뀌었으므로, 같은 제어 루트를 쓰는 옛 버전 체크아웃과 새 버전 체크아웃이 섞이면 `.mam/shim/herdr` 를 서로 덮어쓴다. #45 때와 같은 전환기 비용이다.
[VERDICT: PASS]