693 lines
37 KiB
Markdown
693 lines
37 KiB
Markdown
# 📐 구현 계획서 **Rev.2** — Job `5801cbe2` (원안: `55a872a8`)
|
||
|
||
- **역할**: Planner (`MULTI_AGENT_RULES.md` §1 — 저장소 코드/문서 무수정, 산출물은 본 보고서)
|
||
- **기준 커밋**: `320f036` (working tree clean)
|
||
- **베이스라인**: `pytest tests/ --collect-only` → **346 collected**
|
||
- **입력**: Job `01d929b8` 리뷰 `[VERDICT: PASS WITH CHALLENGE]` (Challenge C-1, Observation C-2·C-3)
|
||
|
||
---
|
||
|
||
## 0. Rev.1 → Rev.2 변경 요약
|
||
|
||
| 항목 | 판정 | 조치 |
|
||
|---|---|---|
|
||
| **Challenge C-1** — `resolve_herdr_workspace()` 폴백 우선순위 역전 | **수용. 실측으로 확인, 지적보다 결함이 한 단계 더 확정적** | §4.3 순서 교체 (§1.9) |
|
||
| **Observation C-2** — 입양 행에 `herdr_workspace` 누락 | **수용.** 같은 dict 의 `herdr_server` 누락(K-2)까지 함께 닫음 | 신설 **S10** (§1.11) |
|
||
| **Observation C-3** — `HERDR_WORKSPACE` 환경변수 비대칭 | **수용.** `set -u` 하 자기참조 확장이 안전함을 실측 | §4.4 (§1.12) |
|
||
| **(자체 재감사) 신규** | Rev.1 의 공백 | 재정의된 `resolve_herdr_workspace` 의 **호출자 집합이 Rev.1 에 없었음**. C-1 을 반영하면 **create 는 이 함수를 써서는 안 됨**이 드러남 (§1.10, §3 D5) |
|
||
|
||
C-1 은 정확합니다. 그리고 챌린저가 제시한 것보다 **한 단계 더 확정적인 결함**입니다 — 챌린저는 *"호출자가 대부분 `ws` 를 넘긴다"* 고 썼는데, 실측하면 `stop_session.sh` 에는 **`--workspace` 파서 자체가 없어서** `${WORKSPACE:-$WORKSPACE_ROOT}` 가 **구조적으로 항상** 호출자의 루트로 고정됩니다(§1.9.1). "다를 수도 있다"가 아니라 "세션의 cwd 가 될 수 없다"입니다.
|
||
|
||
다만 C-1 을 반영하면 Rev.1 이 덮지 않은 문제가 새로 드러납니다. **행을 먼저 보는 해석기를 `create_session.sh` 가 쓰면 재생성 시 낡은 라벨을 물려받습니다** — create 는 `terminated`/`archived` 동명 행 위에 재생성할 수 있기 때문입니다(§1.10 실측). Rev.2 는 이 함정을 §3 D5 로 명시적으로 닫습니다.
|
||
|
||
---
|
||
|
||
## 1. 실측 (Measurements)
|
||
|
||
> §1.1 ~ §1.8 은 Rev.1 에서 확정된 실측이며 재검증 없이 유지합니다. §1.9 ~ §1.12 가 Rev.2 신규입니다.
|
||
|
||
### 1.1 `herdr_workspace` — 읽기 6곳, 쓰기 0곳
|
||
|
||
| # | 위치 | 용도 | 오염 시 결과 |
|
||
|---|---|---|---|
|
||
| 1 | `lib.sh:1027` `resolve_herdr_session()` | 소켓 이름 해석 | **모든 하위 소비자로 전파** |
|
||
| 2 | `reconcile.sh:135` `_srv` | `herdr -L <_srv> kill-session` | 🔴 **파괴적** — 잘못된 소켓에 kill |
|
||
| 3 | `reconcile.sh:389` `unique_servers` | 살아있는 세션 열거 | 🔴 세션을 못 찾음 → `terminated` 오판 |
|
||
| 4 | `reconcile.sh:486` drift 판정 | `(name, srv) not in alive_set` | 🔴 라이브 세션을 `terminated` 로 덮어씀 |
|
||
| 5 | `status.sh:132` | JSON 출력 | 🟡 표시 오류 |
|
||
| 6 | `status.sh:241` | 테이블 출력 | 🟡 표시 오류 |
|
||
|
||
```
|
||
'herdr_session': create_session.sh:314, update_yaml_resumed.sh:121/135, reconcile.sh:566
|
||
'herdr_server': create_session.sh:315, update_yaml_resumed.sh:122/136
|
||
'herdr_workspace': (0건)
|
||
```
|
||
|
||
라이브 레지스트리 3개 행 모두 `herdr_workspace=None`.
|
||
|
||
### 1.2 오인 재현
|
||
|
||
```
|
||
resolve_herdr_session (소켓 이름을 돌려줘야 함)
|
||
legacy(herdr_workspace만 있음) -> my-workspace-label ← 라벨이 소켓 이름으로
|
||
both(herdr_session+workspace) -> real-socket
|
||
|
||
resolve_herdr_workspace (별칭 — 동일한가?)
|
||
legacy -> my-workspace-label
|
||
both -> real-socket ← 라벨을 물었는데 소켓이 나옴
|
||
```
|
||
|
||
### 1.3 `resolve_herdr_workspace()` 는 순수 별칭이고 호출자 4곳 전부 소켓을 원한다
|
||
|
||
| 호출자 | 대입 대상 | 원하는 것 |
|
||
|---|---|---|
|
||
| `create_session.sh:217` | `HERDR_SESSION_NAME` | 소켓 |
|
||
| `stop_session.sh:107` | `HERDR_SESSION_NAME` | 소켓 |
|
||
| `multi-agent-mux-delegate-job:466` | `HERDR_SESSION_NAME` | 소켓 |
|
||
| `multi-agent-mux-resume/SKILL.md:76` (문서) | `HERDR_SESSION_NAME` | 소켓 |
|
||
|
||
### 1.4 `status.sh` 는 이미 라벨과 값이 어긋나 있다
|
||
|
||
```python
|
||
:232 print(f"{'NAME':<44} {'WORKSPACE':<12} ...") ← 헤더는 WORKSPACE
|
||
:241 server = s.get('herdr_session') or s.get('herdr_server') ... ← 값은 소켓
|
||
```
|
||
|
||
### 1.5 기존 테스트 2건이 이름과 반대로 동작한다
|
||
|
||
`tests/test_tier1_unit.py:79/85` 는 함수명이 `..._resolve_herdr_session_...` 인데 `resolve_herdr_workspace` 를 호출합니다. 호출만 바꾸면 이름과 내용이 처음으로 일치합니다.
|
||
|
||
### 1.6 목표 ① 행동 중립성
|
||
|
||
6개 지점에서 폴백 항 제거 → **346건 중 추가 실패 0건**. (`test_d23`/`test_d29` 2건 실패는 무뮤테이션 대조군에서도 동일 — `.git`·`nats-docker` 누락 사본 아티팩트.)
|
||
|
||
동시에 **커버리지 공백**의 증거이기도 합니다: 폴백을 타는 테스트가 0건.
|
||
|
||
### 1.7 `--herdr-workspace` 기본값의 판별 가능성
|
||
|
||
`derive_workspace_slug(<repo>)` → `mam-canary-projects-multi-agent-mux`. `herdr_session` 기본값과 **글자 그대로 동일**해질 위험 → §3 D3.
|
||
|
||
### 1.8 (Rev.1 §1.1 부수) `reconcile.sh:566` 입양 행은 `herdr_server` 를 쓰지 않는다
|
||
|
||
---
|
||
|
||
### 1.9 **[Rev.2] Challenge C-1 검증**
|
||
|
||
#### 1.9.1 전제 확인 — `stop_session.sh` 에는 `--workspace` 파서가 **없다**
|
||
|
||
```
|
||
$ grep -n -- "--workspace\|^WORKSPACE=\|WORKSPACE:-" stop_session.sh
|
||
107: HERDR_SESSION_NAME="$(resolve_herdr_workspace "$SESSION_NAME" "${WORKSPACE:-$WORKSPACE_ROOT}")"
|
||
```
|
||
|
||
`--workspace` case arm 도, `WORKSPACE=` 대입도 없습니다. 즉 `$WORKSPACE` 는 **항상 미설정**이고 `${WORKSPACE:-$WORKSPACE_ROOT}` 는 **항상 `$WORKSPACE_ROOT`** — 운영자가 서 있는 디렉터리입니다. 세션의 실제 cwd 는 `TARGET_CWD` 로 `:113-130` 에서 따로 뽑습니다.
|
||
|
||
챌린저는 *"대부분의 호출자는 `ws` 를 항상 넘긴다"* 고 썼는데, stop 의 경우는 그보다 강합니다 — 넘기는 값이 **세션의 워크스페이스일 수가 없습니다.**
|
||
|
||
#### 1.9.2 두 순서의 차이 — 실측
|
||
|
||
```
|
||
session ws 인자 Rev.1 챌린지안
|
||
------------------------------------------------------------------------------------
|
||
registered-with-label /path/to/project_b explicit-label explicit-label
|
||
registered-no-label /path/to/project_b to-project-b to-project-a <-- 차이
|
||
registered-no-label (없음) to-project-a to-project-a
|
||
registered-no-cwd /path/to/project_b to-project-b to-project-b
|
||
unregistered-session /path/to/project_b to-project-b to-project-b
|
||
unregistered-session (없음) (빈값) (빈값)
|
||
```
|
||
|
||
**차이는 정확히 한 행뿐**입니다 — *등록된 행 + 라벨 없음 + 호출자의 `ws` 가 행의 `pane.cwd` 와 다름*. 이 경우 Rev.1 은 **호출자의 워크스페이스**를, 챌린지안은 **세션 자신의 워크스페이스**를 돌려줍니다.
|
||
|
||
그리고 데드 코드 주장도 성립합니다: Rev.1 의 3순위(`if row: pane.cwd`)는 `ws` 가 빈 경우에만 도달하는데, 현재 호출자 3곳 전부 값을 넘기므로 **어느 생산 경로에서도 도달 불가**합니다. 새로 쓰는 함수에 도달 불가 분기를 넣는 것은 그 자체로 설계 오류입니다.
|
||
|
||
#### 1.9.3 왜 챌린지안이 옳은가 — 저장소의 기존 계약과 일치
|
||
|
||
| 해석기 | 우선순위 | 호출자 인자의 위치 |
|
||
|---|---|---|
|
||
| `resolve_herdr_session` (`lib.sh:1025-1044`) | 행 → 폴백 | 행이 없을 때만 |
|
||
| `agent_of_row` (`registry.py:26`) | `agent` 필드 → 이름 → `pane.cmd` | **없음** (전부 행 유래) |
|
||
| **Rev.1 §4.3** | 라벨 → **호출자 `ws`** → `pane.cwd` | 행 유래 사실보다 위 ❌ |
|
||
|
||
Rev.1 은 자기 §D4 가 세운 원칙("엉뚱한 출처가 새어 들어오면 안 된다")을 자기 구현에서 어겼습니다. **등록된 행이 있으면 행에 적힌 사실이 호출자 인자를 이깁니다.** 챌린지 수용.
|
||
|
||
### 1.10 **[Rev.2 자체 재감사] C-1 을 반영하면 create 는 이 함수를 쓰면 안 된다**
|
||
|
||
C-1 을 반영하면 해석기가 **행을 먼저** 봅니다. 그런데 `create_session.sh:296-307` 은 동명 행 위에 **재생성이 가능**합니다:
|
||
|
||
```python
|
||
running_same = [s for s in sessions if s.get('name') == name and s.get('status') == 'running']
|
||
if running_same:
|
||
raise SystemExit(4) # running 이면 거부
|
||
sessions[:] = [s for s in sessions if s.get('name') != name] # terminated/archived 는 제거 후 재등록
|
||
```
|
||
|
||
따라서 `--session <기존 이름>` 으로 **다른 디렉터리에서** 재생성할 때, 행-우선 해석기를 쓰면 **낡은 `pane.cwd` 에서 파생된 라벨을 물려받습니다**. create 는 새 사실을 *세우는* 쪽이지 *조회하는* 쪽이 아닙니다.
|
||
|
||
→ **create 의 기본값은 `$WORKSPACE` 에서 직접 계산합니다**(§3 D5). 이것이 안전한 이유는 두 슬러그 구현의 패리티가 성립하기 때문입니다:
|
||
|
||
```
|
||
경로 bash derive_workspace_slug(-mam) python slug()
|
||
/Users/.../canary_projects/multi-agent-mux canary-projects-multi-agent-mux canary-projects-multi-agent-mux 일치
|
||
/tmp workspace-tmp workspace-tmp 일치
|
||
/private/var/folders/q_/x q--x q--x 일치
|
||
/Users/godopu16/My_Proj.v2 godopu16-my-projv2 godopu16-my-projv2 일치
|
||
/ workspace-root workspace-root 일치
|
||
```
|
||
|
||
5/5 일치(`_`→`-` 치환, `.` 제거, 루트 처리 포함). 다만 **두 구현이 존재한다는 사실 자체가 리스크**이므로 §5 T10 으로 패리티를 계약화합니다.
|
||
|
||
### 1.11 **[Rev.2] Observation C-2 검증**
|
||
|
||
`reconcile.sh:560-573` 입양 dict:
|
||
|
||
```python
|
||
entry = {
|
||
'name': name, 'status': 'running', 'role': role,
|
||
'herdr_session_created_at': ..., 'herdr_session_epoch': created_epoch,
|
||
'herdr_session': srv, ← herdr_server 없음 (K-2)
|
||
'pane': {..., 'cwd': pm['cwd']}, ← cwd 는 여기 이미 있음
|
||
'start_command': f'... -c "{pm["cwd"]}" ...',
|
||
...
|
||
}
|
||
```
|
||
|
||
`herdr_workspace` 도 없고 `herdr_server` 도 없습니다. 그리고 파생에 필요한 `pm['cwd']` 는 **같은 dict 안에 이미 있습니다**. 두 줄 추가로 C-2 와 K-2 를 동시에 닫을 수 있어, Rev.1 이 범위 밖(K-2)으로 뒀던 판단을 뒤집습니다 — 비용이 사실상 0 이고 §4.7 이 이 필드를 표시하기 시작하는 이상 입양 행만 `-` 로 뜨는 것은 새 드리프트입니다.
|
||
|
||
### 1.12 **[Rev.2] Observation C-3 검증 — `set -u` 안전**
|
||
|
||
```
|
||
[env 미설정] [env 설정]
|
||
OPT=(없음) env=(미설정) -> proj-x OPT=(없음) env=from-env -> from-env
|
||
OPT=from-flag env=(미설정) -> from-flag OPT=from-flag env=from-env -> from-flag
|
||
```
|
||
|
||
`set -euo pipefail` 하에서 `${HERDR_WORKSPACE_OPT:-${HERDR_WORKSPACE:-${ws_slug#mam-}}}` 는 **unbound 오류 없이** 플래그 > env > 슬러그 순으로 동작합니다. `HERDR_SESSION_NAME` 과 대칭이 맞습니다. 수용.
|
||
|
||
---
|
||
|
||
## 2. 범위
|
||
|
||
**포함**
|
||
|
||
| # | 항목 |
|
||
|---|---|
|
||
| **S1** | 6개 읽기 지점에서 `herdr_workspace` 폴백 항 제거 → `herdr_session or herdr_server` 고정 |
|
||
| **S2** | 호출자 4곳 → `resolve_herdr_session` 이관 + `test_tier1_unit.py` 2건 정정 (**게이트**) |
|
||
| **S3** | `resolve_herdr_workspace()` 재정의 — **C-1 순서** 적용 |
|
||
| **S4** | `create_session.sh`: `--herdr-workspace` 파싱·usage·**env 폴백(C-3)**·기본값·YAML |
|
||
| **S5** | `resume_session.sh` / `update_yaml_resumed.sh`: `--herdr-workspace` 지원·영속화 |
|
||
| **S6** | `stop_session.sh`: `--herdr-workspace` usage/parser |
|
||
| **S7** | `status.sh` 컬럼 분리, `reconcile.sh` 라벨 표시 |
|
||
| **S8** | SKILL.md 3종 + `resume/SKILL.md:76` |
|
||
| **S9** | 테스트 tier1 + tier2 신설 |
|
||
| **S10** | **[Rev.2 신설]** `reconcile.sh:566` 입양 행에 `herdr_workspace` + `herdr_server` 기입 (C-2 + K-2) |
|
||
|
||
**제외**
|
||
|
||
| 항목 | 사유 |
|
||
|---|---|
|
||
| `multi-agent-mux-delegate-job` 소켓 lookup 재설계 | `:466` 한 줄이 전부이고 S2 로 해소 (§1.3 전수 확인) |
|
||
| `reconcile.sh` 의 `herdr -L <srv>` vs 심의 `--session` 불일치 | 선재 이슈, 브리프와 무관 → K-3 |
|
||
| `herdr_server` 필드 **제거** | 하위 호환 별칭으로 유지 (S10 은 *추가*이지 제거가 아님) |
|
||
|
||
---
|
||
|
||
## 3. 설계 결정
|
||
|
||
### D1 — 순서: ①이 ②보다 반드시 먼저 (Rev.1 유지)
|
||
|
||
`herdr_workspace` writer 가 0 이라 결함이 잠복 상태이고, 목표 ②가 바로 그 writer 를 만듭니다. S1 없이 S4 만 넣으면 그 커밋이 결함을 활성화합니다. S1 은 §1.6 대로 오늘 무해합니다.
|
||
|
||
### D2 — 이름 되찾기: 호출자 이관 → 재정의 2단계 (Rev.1 유지)
|
||
|
||
1단계 후 `grep -rn 'resolve_herdr_workspace' --include='*.sh' --include='*.py' .` 이 **정의 1줄 외 0건**임을 게이트로 확인하고 2단계 진입.
|
||
|
||
### D3 — `--herdr-workspace` 기본값: `mam-` 접두사 없는 슬러그 (Rev.1 유지)
|
||
|
||
접두사를 유지하면 두 필드가 기본 상태에서 동일 문자열이 되어 **테스트가 두 필드를 구분하지 못합니다**(J-2 의 `n=3` 함정과 동형). `derive_session_name()` 이 이미 쓰는 `${base_slug#mam-}` 관용구를 재사용합니다.
|
||
|
||
### D4 — 폴백 체인의 최종 형태 (Rev.1 유지)
|
||
|
||
```python
|
||
srv = s.get('herdr_session') or s.get('herdr_server') or 'default' # 라벨은 절대 들어오지 않음
|
||
ws = s.get('herdr_workspace') or <pane.cwd 파생> # 소켓으로 폴백하지 않음
|
||
```
|
||
|
||
### D5 — **[Rev.2 신설]** 재정의된 해석기의 **호출자 집합**
|
||
|
||
Rev.1 은 함수를 재정의하면서 **누가 부를지 적지 않았습니다.** C-1 을 반영하면 이 공백이 실제 함정이 됩니다(§1.10).
|
||
|
||
| 소비자 | 해석 방법 | 이유 |
|
||
|---|---|---|
|
||
| `update_yaml_resumed.sh` | **`resolve_herdr_workspace` 호출** | 등록된 행의 사실이 우선이어야 함 — C-1 이 겨냥한 정확한 경우 |
|
||
| `create_session.sh` | **`${ws_slug#mam-}` 직접 계산** (함수 미사용) | 재생성 시 낡은 행의 `pane.cwd` 를 물려받지 않기 위해 (§1.10 실측) |
|
||
| `status.sh` / `reconcile.sh` | 행의 `herdr_workspace` 를 읽고, 없으면 `pane.cwd` 에서 인라인 파생 | 표시 전용, 인라인 Python 이라 `lib.sh` 를 거치지 않음 |
|
||
| `stop_session.sh` | 사용하지 않음 | 소켓만 필요 (§4.6) |
|
||
|
||
**create 가 함수를 쓰지 않는다는 결정이 D5 의 핵심**입니다. 두 슬러그 구현이 갈릴 위험은 §5 T10 패리티 테스트로 막습니다.
|
||
|
||
### D6 — **[Rev.2 신설]** `--workspace` 는 라벨링 수단이 아니다
|
||
|
||
C-1 의 이면입니다. 운영자가 라벨을 바꾸고 싶으면 `--herdr-workspace` 를 씁니다. `--workspace` 는 "이 명령이 실행되는 맥락"이지 "세션이 속한 워크스페이스"가 아닙니다. 이 구분을 §4.6 usage 와 SKILL.md 에 한 줄씩 명시합니다.
|
||
|
||
---
|
||
|
||
## 4. 구현
|
||
|
||
### 4.1 S1 — 폴백 항 제거 (6곳)
|
||
|
||
```diff
|
||
- val = s.get('herdr_session') or s.get('herdr_server') or s.get('herdr_workspace')
|
||
+ val = s.get('herdr_session') or s.get('herdr_server')
|
||
```
|
||
`lib.sh:1027`. 동형으로 `reconcile.sh:135/389/486`, `status.sh:132/241` (뒤 넷은 `... or 'default'` 유지).
|
||
|
||
각 지점 주석:
|
||
|
||
```python
|
||
# herdr_workspace 는 워크스페이스 *라벨* 이지 소켓 이름이 아니다. 폴백에 넣으면
|
||
# 라벨이 `herdr -L <name>` 의 소켓 인자로 흘러들어간다 (reconcile.sh:135 는 kill).
|
||
```
|
||
|
||
### 4.2 S2 — 호출자 이관 (게이트)
|
||
|
||
| 파일:줄 | 변경 |
|
||
|---|---|
|
||
| `create_session.sh:217`, `stop_session.sh:107`, `multi-agent-mux-delegate-job:466` | `resolve_herdr_workspace` → `resolve_herdr_session` |
|
||
| `multi-agent-mux-resume/SKILL.md:76`, `multi-agent-mux-delegate-job:43`(주석), `lib.sh:1011`(주석) | 〃 |
|
||
| `tests/test_tier1_unit.py:82, :88, :92` | 〃 (§1.5) |
|
||
|
||
### 4.3 S3 — `resolve_herdr_workspace()` 재정의 (**C-1 반영**)
|
||
|
||
```bash
|
||
# resolve_herdr_workspace <session_name> [workspace]
|
||
#
|
||
# 이 MAM 세션 행의 워크스페이스 *라벨* 을 돌려준다. herdr 소켓/데몬 이름이
|
||
# 아니다 — 그쪽은 resolve_herdr_session() 이다. 라벨이 소켓 인자로 흘러가면
|
||
# reconcile.sh 가 엉뚱한 소켓에 kill-session 을 날린다.
|
||
#
|
||
# 우선순위 (C-1: 등록된 행의 사실이 호출자 인자를 이긴다):
|
||
# ① row['herdr_workspace'] — 명시 기록
|
||
# ② row['pane']['cwd'] 의 슬러그 — 등록된 세션의 실제 작업 디렉터리
|
||
# ③ 인자 workspace 의 슬러그 — 미등록 세션 전용 폴백
|
||
# ④ 빈 문자열
|
||
# 주의 1: herdr_session / herdr_server 로는 절대 폴백하지 않는다 (D4).
|
||
# 주의 2: create_session.sh 는 이 함수를 쓰지 않는다 — 재생성 시 낡은 행의
|
||
# pane.cwd 를 물려받기 때문 (D5).
|
||
resolve_herdr_workspace() {
|
||
local session_name="$1"
|
||
local workspace="${2:-}"
|
||
MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$session_name" TARGET_WS="$workspace" python3 -c "
|
||
import sys, os, json, re
|
||
name = os.environ['SESSION_NAME']
|
||
ws = os.environ.get('TARGET_WS', '').strip()
|
||
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
|
||
|
||
def slug(path):
|
||
if not path:
|
||
return ''
|
||
a = os.path.abspath(path)
|
||
parent = os.path.basename(os.path.dirname(a)) or 'workspace'
|
||
work = os.path.basename(a) or 'root'
|
||
if parent in ('/', '.'): parent = 'workspace'
|
||
if work in ('/', '.'): work = 'root'
|
||
s = f'{parent}-{work}'.lower().replace('_', '-')
|
||
return re.sub(r'[^a-zA-Z0-9-]', '', s).lstrip('-')
|
||
|
||
row = next((s for s in d.get('herdr_sessions', []) if s.get('name') == name), None)
|
||
|
||
# ① 명시 기록
|
||
if row and row.get('herdr_workspace'):
|
||
print(row['herdr_workspace']); sys.exit(0)
|
||
|
||
# ② 등록된 행의 실제 cwd — 호출자 인자보다 우선 (C-1)
|
||
if row:
|
||
derived = slug((row.get('pane') or {}).get('cwd', ''))
|
||
if derived:
|
||
print(derived); sys.exit(0)
|
||
|
||
# ③ 미등록(또는 cwd 부재) 세션 폴백
|
||
if ws:
|
||
derived = slug(ws)
|
||
if derived:
|
||
print(derived); sys.exit(0)
|
||
|
||
print('')
|
||
"
|
||
}
|
||
```
|
||
|
||
Rev.1 대비 바뀐 것은 ②와 ③의 순서, 그리고 ②가 빈 값을 낼 때 ③으로 흘러가도록 `if derived:` 가드를 둔 점입니다(챌린저 처방 그대로).
|
||
|
||
### 4.4 S4 — `create_session.sh` (**C-3 + D5 반영**)
|
||
|
||
```bash
|
||
HERDR_WORKSPACE_OPT="" # :56 부근, set -u 안전
|
||
...
|
||
--herdr-workspace) HERDR_WORKSPACE_OPT="$2"; shift 2 ;; # :68 부근
|
||
```
|
||
|
||
usage:
|
||
|
||
```
|
||
--herdr-workspace NAME workspace label recorded in the registry
|
||
(flag > $HERDR_WORKSPACE > workspace slug without mam-).
|
||
A label only — it never selects a herdr socket;
|
||
use --herdr-session for that.
|
||
```
|
||
|
||
기본값 — `ws_slug` 계산 직후 **한 곳에서만** 계산합니다:
|
||
|
||
```bash
|
||
# 플래그 > 환경변수 > 워크스페이스 슬러그 (C-3: HERDR_SESSION_NAME 과 대칭).
|
||
# D5: resolve_herdr_workspace 를 쓰지 않는다 — 동명 terminated 행 위에 재생성할 때
|
||
# 낡은 pane.cwd 에서 파생된 라벨을 물려받기 때문 (create 는 사실을 세우는 쪽).
|
||
MAM_WS_LABEL="${HERDR_WORKSPACE_OPT:-${HERDR_WORKSPACE:-${ws_slug#mam-}}}"
|
||
```
|
||
|
||
> 내부 변수를 `HERDR_WORKSPACE` 가 아니라 `MAM_WS_LABEL` 로 둡니다. 같은 이름을 쓰면 이후 `atomic_dump_yaml ... HERDR_WORKSPACE="$HERDR_WORKSPACE"` 에서 **입력 채널과 출력 채널이 한 이름을 공유**해 읽는 사람이 어느 쪽인지 판단할 수 없게 됩니다. `create_session.sh` 는 `HERDR_SESSION_NAME` 블록을 `:140` 과 `spawn():176` 두 곳에 중복시킨 전력이 있으므로, 이 계산은 **단일 지점**임을 주석으로 못박습니다.
|
||
|
||
dry-run 출력에 실어 파싱 감도를 확보합니다(`1b18eb9a` §4.1 교훈):
|
||
|
||
```bash
|
||
echo "[dry-run] would spawn: herdr session '$SESSION_NAME' in $WORKSPACE (agent=$AGENT, herdr_session=${HERDR_SESSION_NAME:-default}, herdr_workspace=${MAM_WS_LABEL})"
|
||
```
|
||
|
||
YAML 직렬화 (`:314-315` 옆, env 는 `MAM_WS_LABEL="$MAM_WS_LABEL"` 로 전달):
|
||
|
||
```python
|
||
'herdr_session': server_name,
|
||
'herdr_server': server_name,
|
||
'herdr_workspace': os.environ.get('MAM_WS_LABEL', ''),
|
||
```
|
||
|
||
### 4.5 S5 — resume 계열
|
||
|
||
`resume_session.sh` / `update_yaml_resumed.sh` 에 `--herdr-workspace` 파싱을 추가하고, `resume_session.sh` 는 **두 호출 지점 모두**(`:72-74`, `:136-138`)에 전달합니다. `2d3fef82` 에서 `--herdr-session` 이 정확히 이 대칭 누락으로 반려됐습니다.
|
||
|
||
`update_yaml_resumed.sh` 는 **D5 대로 `resolve_herdr_workspace` 를 사용**합니다:
|
||
|
||
```bash
|
||
if [ -n "$HERDR_WORKSPACE_OPT" ]; then
|
||
MAM_WS_LABEL="$HERDR_WORKSPACE_OPT"
|
||
export MAM_WS_LABEL_EXPLICIT="1"
|
||
else
|
||
MAM_WS_LABEL="$(resolve_herdr_workspace "$SESSION_NAME" "${WORKSPACE:-}")"
|
||
export MAM_WS_LABEL_EXPLICIT="0"
|
||
fi
|
||
export MAM_WS_LABEL
|
||
```
|
||
|
||
영속화는 `--herdr-session` 이 확립한 명시/백필 패턴을 그대로 따릅니다:
|
||
|
||
```python
|
||
else:
|
||
wsl = os.environ.get('MAM_WS_LABEL', '')
|
||
ws_explicit = os.environ.get('MAM_WS_LABEL_EXPLICIT') == '1'
|
||
if wsl and (ws_explicit or not target.get('herdr_workspace')):
|
||
target['herdr_workspace'] = wsl
|
||
```
|
||
|
||
신규 행(`target is None`) 분기에도 `'herdr_workspace': wsl` 을 추가합니다 — `1b18eb9a` §O-1 이 지적한 커버리지 공백을 §5 T7 로 함께 닫습니다.
|
||
|
||
### 4.6 S6 — `stop_session.sh`
|
||
|
||
usage/parser 에 추가하되 라우팅에는 쓰지 않습니다(D6):
|
||
|
||
```
|
||
--herdr-workspace <name> — recorded label only; never selects a socket
|
||
(use --herdr-session for that). Note: stop has no
|
||
--workspace flag — the session's own workspace is
|
||
read from its registry row, not from where you stand.
|
||
```
|
||
|
||
### 4.7 S7 — 표시
|
||
|
||
```python
|
||
print(f"{'NAME':<44} {'SOCKET':<12} {'WORKSPACE':<14} {'YAML':<10} {'HERDR':<6} ...")
|
||
...
|
||
socket = s.get('herdr_session') or s.get('herdr_server') or 'default'
|
||
wslabel = s.get('herdr_workspace') or _slug((s.get('pane') or {}).get('cwd','')) or '-'
|
||
```
|
||
|
||
§1.4 의 라벨/값 불일치가 여기서 해소됩니다. `status.sh:132` JSON 에도 `herdr_workspace` 키 추가(기존 `server` 키는 계약이므로 유지).
|
||
|
||
### 4.8 S10 — **[Rev.2 신설]** 입양 행 (C-2 + K-2)
|
||
|
||
`reconcile.sh:566` 부근, 같은 dict 안에 이미 있는 `pm['cwd']` 를 재사용:
|
||
|
||
```python
|
||
'herdr_session': srv,
|
||
'herdr_server': srv, # K-2: 다른 두 writer 와 필드 세트 정합
|
||
'herdr_workspace': _slug(pm['cwd']), # C-2: 입양 행만 WORKSPACE 가 '-' 로 뜨지 않도록
|
||
```
|
||
|
||
`_slug()` 는 `reconcile.sh` 인라인 Python 안의 헬퍼로 두되, §5 T10 이 `lib.sh` 구현과의 패리티를 계약화합니다.
|
||
|
||
---
|
||
|
||
## 5. 테스트 계획
|
||
|
||
신설 **13건** (Rev.1 8건 + Rev.2 5건). 예상 collected **346 → 359**.
|
||
|
||
### T1 (tier1) — 두 해석기가 다른 것을 돌려준다
|
||
|
||
```python
|
||
seed_row(name="d-creator-claude", herdr_session="socket-A", herdr_workspace="label-B")
|
||
assert resolve_herdr_session(...) == "socket-A"
|
||
assert resolve_herdr_workspace(...) == "label-B"
|
||
```
|
||
|
||
### T2 (tier1) — 라벨이 소켓으로 새지 않는다 (**핵심 가드**)
|
||
|
||
```python
|
||
seed_row(name="legacy-creator-claude", herdr_workspace="my-label") # herdr_session 없음
|
||
assert resolve_herdr_session("legacy-creator-claude") != "my-label"
|
||
```
|
||
|
||
§1.6 대로 현재 스위트에 이 성질을 잡는 테스트가 0건입니다. 제거 확인이 아니라 **재도입 검출**이 목적입니다.
|
||
|
||
### T3 (tier1) — 소켓 해석기 폴백 항이 정확히 둘
|
||
|
||
`herdr_server` 만 있는 행 → 그 값. 둘 다 없는 행 → 기존 계약 유지.
|
||
|
||
### T3b (tier1) — **[Rev.2 신설]** C-1 우선순위 계약
|
||
|
||
```python
|
||
def test_workspace_resolver_prefers_the_row_over_the_caller_argument(mam_sandbox):
|
||
"""C-1: 등록된 행에는 herdr_workspace 가 없지만 pane.cwd 가 있다.
|
||
호출자가 '다른' 워크스페이스를 넘겨도 행의 cwd 가 이긴다.
|
||
(stop_session.sh 는 --workspace 파서가 없어 항상 호출자의 루트를 넘긴다.)"""
|
||
seed_row(name="pa-creator-claude", pane_cwd="/path/to/project_a") # 라벨 없음
|
||
r = run_lib_func(mam_sandbox, "resolve_herdr_workspace",
|
||
"pa-creator-claude", "/path/to/project_b")
|
||
assert r.stdout.strip() == "to-project-a" # ← project_b 가 아님
|
||
|
||
def test_workspace_resolver_uses_the_argument_only_when_unregistered(mam_sandbox):
|
||
"""③ 분기가 살아 있음을 확인 — 미등록 세션에서는 인자가 쓰인다."""
|
||
r = run_lib_func(mam_sandbox, "resolve_herdr_workspace",
|
||
"not-registered", "/path/to/project_b")
|
||
assert r.stdout.strip() == "to-project-b"
|
||
```
|
||
|
||
두 번째 단언이 중요합니다 — C-1 을 반영하면서 ③ 분기를 통째로 죽이지 않았음을 고정합니다.
|
||
|
||
### T4 (tier2) — `--herdr-workspace` 파싱 + 기본값 + **env 폴백(C-3)**
|
||
|
||
```python
|
||
assert "herdr_workspace=my-label" in dry_run(flag="my-label")
|
||
# 생략 + env 설정 → env 가 이긴다 (C-3)
|
||
assert "herdr_workspace=from-env" in dry_run(env={"HERDR_WORKSPACE": "from-env"})
|
||
# 플래그와 env 동시 → 플래그가 이긴다
|
||
assert "herdr_workspace=my-label" in dry_run(flag="my-label", env={"HERDR_WORKSPACE": "from-env"})
|
||
# 둘 다 없음 → 접두사 없는 슬러그, 그리고 herdr_session 기본값과 다르다 (D3)
|
||
out = dry_run()
|
||
assert f"herdr_workspace={bare}" in out and f"herdr_session=mam-{bare}" in out
|
||
```
|
||
|
||
마지막 줄이 **한 테스트 안에서 두 필드가 서로 다름**을 고정합니다.
|
||
|
||
### T5 (tier2) — create YAML 전파
|
||
|
||
`herdr_session` / `herdr_server` / `herdr_workspace` 3개를 각각 단언하고, `herdr_workspace` 값이 `start_command`/`attach_command`/`kill_command` 에 **들어가지 않음**을 함께 단언(라벨이 라우팅에 새지 않음).
|
||
|
||
### T6 (tier2) — resume 전파 (양쪽 호출 지점)
|
||
|
||
`--herdr-workspace NEW-LABEL` → 행의 `herdr_workspace` 갱신, `herdr_session` **불변**.
|
||
|
||
### T7 (tier2) — resume 신규 행 분기
|
||
|
||
`herdr_sessions: []` 로 시작 → `herdr_session`·`herdr_server`·`herdr_workspace` 3개 모두 기록. (`1b18eb9a` §O-1)
|
||
|
||
### T8 (tier2) — stop 인자 수용
|
||
|
||
`test_comp_stop_usage_matches_parser` 플래그 목록에 `--herdr-workspace` 추가.
|
||
|
||
### T9 (tier2) — **[Rev.2 신설]** create 재생성 함정 (D5)
|
||
|
||
```python
|
||
def test_create_does_not_inherit_a_stale_workspace_label(mam_sandbox, mock_herdr, mock_agents):
|
||
"""D5: 동명 terminated 행이 다른 cwd 를 갖고 있어도, 재생성은 --workspace 에서
|
||
라벨을 파생한다. (행-우선 해석기를 쓰면 낡은 라벨을 물려받는다.)"""
|
||
seed_row(name="reuse-creator-claude", status="terminated",
|
||
pane_cwd="/old/place", herdr_workspace="old-label")
|
||
run_create(workspace=mam_sandbox, session="reuse-creator-claude") # --herdr-workspace 없음
|
||
row = read_row("reuse-creator-claude")
|
||
assert row["herdr_workspace"] != "old-label"
|
||
assert row["herdr_workspace"] == expected_bare_slug(mam_sandbox)
|
||
```
|
||
|
||
### T10 (tier1) — **[Rev.2 신설]** 슬러그 구현 패리티
|
||
|
||
```python
|
||
@pytest.mark.parametrize("path", ["/tmp", "/", "/a/My_Proj.v2", "/private/var/folders/q_/x"])
|
||
def test_slug_parity_between_bash_and_python(mam_sandbox, path):
|
||
"""D5 는 두 슬러그 구현의 일치에 의존한다 (lib.sh derive_workspace_slug 와
|
||
resolve_herdr_workspace / reconcile.sh 의 인라인 slug())."""
|
||
b = run_lib_func(mam_sandbox, "derive_workspace_slug", path).stdout.strip()
|
||
p = run_lib_func(mam_sandbox, "resolve_herdr_workspace", "not-registered", path).stdout.strip()
|
||
assert b.removeprefix("mam-") == p
|
||
```
|
||
|
||
§1.10 에서 5/5 일치를 실측했으므로 이 테스트는 현재 통과합니다. 값어치는 **미래의 분기 방지**입니다.
|
||
|
||
### T11 (tier2) — **[Rev.2 신설]** 입양 행 (S10)
|
||
|
||
reconcile drift-B 입양을 태우고 새로 등록된 행에 `herdr_session`·`herdr_server`·`herdr_workspace` 3개가 모두 있고, `herdr_workspace` 가 `pane.cwd` 파생값과 일치함을 단언.
|
||
|
||
### T12 (tier2) — **[Rev.2 신설]** 표시 컬럼 분리 (S7)
|
||
|
||
소켓과 라벨이 다른 행을 심고 `status.sh` 출력에서 **두 값이 각자 컬럼에 나타남**을 단언. §1.4 의 헤더/값 불일치 회귀 방지.
|
||
|
||
---
|
||
|
||
## 6. 뮤테이션 매트릭스
|
||
|
||
| # | 뮤테이션 | FAIL 해야 하는 테스트 |
|
||
|---|---|---|
|
||
| M1 | `lib.sh:1027` 에 `or s.get('herdr_workspace')` 재도입 | **T2** |
|
||
| M2 | `resolve_herdr_workspace` 를 다시 별칭으로 | **T1** |
|
||
| M3 | 새 해석기에 `or row.get('herdr_session')` 폴백 추가 (D4 위반) | **T1** |
|
||
| **M3b** | **[Rev.2]** ②③ 순서를 Rev.1 로 되돌림 (`ws` 를 `pane.cwd` 앞으로) | **T3b 첫 단언** |
|
||
| **M3c** | **[Rev.2]** ③ 분기 삭제 (과잉 교정) | **T3b 둘째 단언** |
|
||
| M4 | `reconcile.sh:486` 에 폴백 항 재도입 | **미검출** — 아래 정적 가드로 대응 |
|
||
| M5 | create 파서가 `--herdr-workspace` 값을 버림 | **T4, T5** |
|
||
| M6 | 기본값을 `${ws_slug}` (접두사 유지)로 | **T4** |
|
||
| **M6b** | **[Rev.2]** env 폴백 제거 (`${HERDR_WORKSPACE:-}` 항 삭제) | **T4 둘째 단언** |
|
||
| M7 | `herdr_workspace` 를 `start_command` 에 주입 | **T5** |
|
||
| M8 | resume 주 경로에서 `--herdr-workspace` 미전달 | **T6** |
|
||
| M9 | 신규 행 dict 에서 `herdr_workspace` 제거 | **T7** |
|
||
| **M10** | **[Rev.2]** create 가 `resolve_herdr_workspace` 를 쓰도록 변경 (D5 위반) | **T9** |
|
||
| **M11** | **[Rev.2]** 입양 dict 에서 `herdr_workspace` 제거 | **T11** |
|
||
| **M12** | **[Rev.2]** `status.sh` 가 두 컬럼에 같은 값을 출력 | **T12** |
|
||
|
||
**M3b 와 M3c 가 서로 다른 단언을 깨야 합니다.** 하나는 순서 역전을, 다른 하나는 과잉 교정(`ws` 분기 제거)을 잡습니다. 둘 중 하나라도 잡히지 않으면 T3b 가 한쪽만 보는 테스트라는 뜻입니다 — J-2 에서 `n=3` 을 골라 M6 을 판별하지 못했던 실수를 반복하지 않기 위한 조건입니다.
|
||
|
||
**M4 를 정직하게 남깁니다.** `reconcile.sh`/`status.sh` 의 4개 지점은 각자 인라인 Python 이라 `lib.sh` 해석기를 거치지 않습니다. T2 는 `lib.sh` 만 지킵니다. 픽스처 4개 대신 **소스 수준 정적 가드 1건**으로 묶습니다.
|
||
|
||
```python
|
||
def test_no_socket_lookup_falls_back_to_workspace_label():
|
||
"""B-22 구조 가드: 소켓 lookup 표현식에 herdr_workspace 가 다시 끼어들지 못한다.
|
||
reconcile.sh:135 는 이 값을 `herdr -L <name> kill-session` 에 넘긴다."""
|
||
pat = re.compile(r"herdr_session'\)\s*or\s*.*herdr_workspace")
|
||
for f in (LIB_SH, RECONCILE_SH, STATUS_SH):
|
||
for i, line in enumerate(f.read_text().splitlines(), 1):
|
||
assert not pat.search(line), f"{f.name}:{i} — socket lookup falls back to the workspace label:\n{line}"
|
||
```
|
||
|
||
문자열 가드는 원래 감도가 약하지만, 이 결함은 **형태 자체가 한 줄 관용구**라 정확히 겨냥할 수 있습니다. **M4 를 실제로 검출하는지 뮤테이션으로 확인하는 것**을 수용 조건에 넣습니다.
|
||
|
||
---
|
||
|
||
## 7. 커밋 분할
|
||
|
||
| # | 커밋 | 내용 | 선행 |
|
||
|---|---|---|---|
|
||
| **1** | `fix(lib,monitor,status): stop resolving the workspace label as a herdr socket name (B-22)` | S1 + T2 + M4 정적 가드 | — |
|
||
| **2** | `refactor(lib,skills): point every caller at resolve_herdr_session (B-22)` | S2 (게이트 포함) | 1 |
|
||
| **3** | `feat(lib): make resolve_herdr_workspace return the workspace label (B-22)` | S3 + T1 + T3 + **T3b** + **T10** | 2 |
|
||
| **4** | `feat(create): add --herdr-workspace and serialize it as a distinct field` | S4 + T4 + T5 + **T9** | 3 |
|
||
| **5** | `feat(resume,stop): support --herdr-workspace end to end` | S5 + S6 + T6 + T7 + T8 | 4 |
|
||
| **6** | `feat(status,monitor): record and show the workspace label` | S7 + **S10** + **T11** + **T12** | 4 |
|
||
| **7** | `docs(skills): document --herdr-workspace and the socket/label split` | S8 | 5, 6 |
|
||
|
||
커밋 1 이 반드시 첫 번째여야 합니다(D1). 커밋 1~3 은 §1.6 대로 전부 행동 중립이며 실제 기능은 커밋 4 부터 시작합니다. 커밋 2/3 분리는 D2 게이트 때문입니다.
|
||
|
||
Rev.1 대비 변경: 커밋 3 에 T3b·T10, 커밋 4 에 T9, 커밋 6 에 S10·T11·T12 가 추가됐습니다. 커밋 개수는 그대로입니다.
|
||
|
||
---
|
||
|
||
## 8. 검증 절차 (Creator 실행)
|
||
|
||
```bash
|
||
# 1) 구문 — 변경 7개 스크립트 bash -n
|
||
|
||
# 2) D2 게이트 (커밋 2 직후) — 정의 1줄만 남아야 함
|
||
grep -rn 'resolve_herdr_workspace' --include='*.sh' --include='*.py' . | grep -v '^./.agents/reports/'
|
||
|
||
# 3) 폴백 항 소멸 (커밋 1 직후)
|
||
grep -rn "or s.get('herdr_workspace')" --include='*.sh' . | grep -v '^./.agents/reports/'
|
||
# → 0건
|
||
|
||
# 4) C-1 순서 직접 확인 (커밋 3 직후)
|
||
# herdr_workspace 없고 pane.cwd=/path/to/project_a 인 행에
|
||
# resolve_herdr_workspace <name> /path/to/project_b
|
||
# → to-project-a 여야 함 (to-project-b 면 순서가 역전된 것)
|
||
|
||
# 5) 전체 스위트 (베이스라인 346 → 기대 359)
|
||
.venv/bin/python -m pytest tests/ -q
|
||
|
||
# 6) 뮤테이션 M1~M12 + M4 정적 가드 확인
|
||
```
|
||
|
||
> **측정 주의**: 격리 사본에서 스위트를 돌릴 때는 `.git` 과 `nats-docker/` 를 함께 복사하십시오. 빠뜨리면 `test_d23_compose_image_matches_doc_and_is_alpine` 와 `test_d29_env_secrets_never_tracked` 가 **사본 아티팩트로** 실패해 뮤테이션 결과를 오독합니다(§1.6 에서 실제로 발생).
|
||
|
||
---
|
||
|
||
## 9. 후속 백로그 (범위 밖, 등록만)
|
||
|
||
| ID | 내용 |
|
||
|---|---|
|
||
| **K-1** | `test_o2_18_orphan_steal_lock_recovered` 부하 민감 플레이크 — `acquire_bg()` 의 고정 `time.sleep(0.3)` |
|
||
| ~~K-2~~ | ~~입양 행 `herdr_server` 누락~~ → **S10 으로 범위 내 흡수** |
|
||
| **K-3** | `reconcile.sh:392-396` 이 `herdr -L <srv>` 를 `subprocess.run` 으로 직접 호출 — `lib.sh` 심의 `--session` 경로 우회. 소켓 스코핑이 실제로 걸리는지 미검증 |
|
||
| **K-4** | `README.md:98,100` / `README.ko.md:80,82` 의 구 `herdr -L <server>` 서술 (선재 드리프트) |
|
||
| **K-5** | `create_session.sh:216` 의 `HERDR_SERVER_OPT` 가드 무동작 (`1b18eb9a` §O-2) |
|
||
| **K-6** | **[Rev.2 신설]** `stop_session.sh` 에 `--workspace` 파서 부재 — `${WORKSPACE:-$WORKSPACE_ROOT}` 가 항상 후자로 고정(§1.9.1). D6 대로 stop 은 행에서 읽으면 되므로 이번 범위에서는 결함이 아니지만, `resolve_herdr_session` 의 미등록 폴백 품질에는 영향 |
|
||
|
||
---
|
||
|
||
## 10. 규모 추정
|
||
|
||
| 파일 | 변경 |
|
||
|---|---|
|
||
| `lib.sh` | +36 / −3 |
|
||
| `reconcile.sh` | +9 / −3 (S10 포함) |
|
||
| `status.sh` | +10 / −2 |
|
||
| `create_session.sh` | +15 |
|
||
| `resume_session.sh` | +8 |
|
||
| `update_yaml_resumed.sh` | +18 |
|
||
| `stop_session.sh` | +6 |
|
||
| `multi-agent-mux-delegate-job` | +1 / −1 |
|
||
| SKILL.md 3종 + `resume/SKILL.md` | +20 |
|
||
| `tests/test_tier1_unit.py` | +60 (T1~T3b, T10, 기존 2건 정정) |
|
||
| `tests/test_tier2_component.py` | +140 (T4~T9, T11, T12) |
|
||
| 정적 가드 | +12 |
|
||
|
||
총 **약 +335 / −9 줄**, 파일 12개, 커밋 7개. 규모 **중** (Rev.1 대비 테스트 +87줄).
|
||
|
||
---
|
||
|
||
## 11. 챌린저에게
|
||
|
||
C-1 은 정확하고, 실측해 보니 지적보다 **한 단계 더 확정적**이었습니다. `stop_session.sh` 에는 `--workspace` 파서가 아예 없어서(§1.9.1) 넘어가는 값이 세션의 워크스페이스일 **가능성 자체가 없습니다**. "다를 수 있다"가 아니라 "구조적으로 다르다"입니다. 그리고 Rev.1 의 3순위가 어느 생산 경로에서도 도달 불가라는 데드 코드 지적도 그대로 성립합니다.
|
||
|
||
무엇보다, Rev.1 은 자기 §D4 가 세운 원칙("엉뚱한 출처가 새어 들어오면 안 된다")을 자기 §4.3 구현에서 어겼습니다. 같은 저장소의 `resolve_herdr_session` 과 `agent_of_row` 는 둘 다 행 유래 사실을 호출자 인자보다 앞에 둡니다. 제 구현만 예외였습니다.
|
||
|
||
C-1 을 반영하면서 Rev.1 이 덮지 않은 문제가 하나 새로 드러났습니다 — **재정의된 함수를 누가 부를지 Rev.1 에 없었고**, 행-우선 해석기를 `create_session.sh` 가 쓰면 동명 `terminated` 행 위에 재생성할 때 낡은 라벨을 물려받습니다(§1.10). D5 와 T9/M10 으로 닫았습니다. 지적 하나가 계획의 다른 구멍을 드러낸 셈입니다.
|
||
|
||
C-2 는 수용하면서 Rev.1 이 범위 밖(K-2)으로 뒀던 `herdr_server` 누락도 함께 끌어왔습니다. 같은 dict 두 줄이고, §4.7 이 이 필드를 표시하기 시작하는 이상 입양 행만 `-` 로 뜨는 것은 새 드리프트이기 때문입니다.
|
||
|
||
C-3 도 수용했습니다. 다만 내부 변수명을 `HERDR_WORKSPACE` 대신 `MAM_WS_LABEL` 로 둡니다 — 같은 이름이면 입력 채널(사용자 env)과 출력 채널(`atomic_dump_yaml` 전달)이 한 이름을 공유해 읽는 사람이 구분할 수 없게 되고, 이 파일은 `HERDR_SESSION_NAME` 블록을 두 곳에 중복시킨 전력이 있습니다.
|