- In stop_session.sh and update_yaml_resumed.sh: standardize explicit --agent option and use resolve_agent_type_from_registry to read agent type from YAML/DB state rather than brittle suffix-only regex inference. - Update multi-agent-mux-stop/SKILL.md, multi-agent-mux-resume/SKILL.md, multi-agent-mux-create/SKILL.md, and deploy/INSTALL.md to standardize passing --agent explicitly. - Fix J-1 in layout.py: refactor _env_int(*names, default=None) to take an explicit default parameter, eliminating the falsy-zero trap so MAM_MIN_PANE_COLS=0 is respected. - Add regression and contract tests: test_j1_env_zero_min_cols_matches_flag_zero, test_j1_env_zero_min_rows_matches_flag_zero, test_comp_stop_agent_fallback_*, test_comp_docs_stop_examples_pass_agent. - Verified 100% UNANIMOUS PASS from Planner claude and Reviewers claude and cline.
46 KiB
📐 구현 계획서 Rev.2 — Job d6f54b7f (원안: 79ff98ed)
- 역할: Planner (
MULTI_AGENT_RULES.md§1 — 저장소 코드/문서 무수정, 산출물은 본 보고서) - 기준 커밋:
14e306b(branchrefactor, working tree clean) - 베이스라인:
pytest tests/ --collect-only→ 333 collected - 입력: Job
9f85218e리뷰[VERDICT: PASS WITH CHALLENGE](Challenge C-1, Observation C-2)
0. Rev.1 → Rev.2 변경 요약
| 항목 | 판정 | 조치 |
|---|---|---|
| Challenge C-1 — T5 문서 가드의 블록 카운팅 오류 + 부분 문자열 허점 | 전면 수용. 두 갈래 모두 실측 확인 | §5 T5 재설계 (§1.10에 실측 근거) |
Observation C-2 — _env_int 의 ValueError 시 continue |
수용. 챌린저가 제시한 것보다 근거가 더 강함 | §4.5 S5 변경 + 전용 테스트 T1b 신설 (§1.11) |
| (자체 정정) Rev.1 §5 의 "신설 8건" | 오산 — 실제 7건 | Rev.2 는 8건(C-2 테스트 1건 추가). 기대 collected 341 은 동일하나 근거가 달라짐 |
| (신규) K-5 | — | 문서화되지 않은 MAM_MIN_COLS 가 문서화된 MAM_MIN_PANE_COLS 보다 우선순위가 높다 (§9) |
C-1 은 계획대로 구현하면 테스트가 100% 실패하는 결함이었습니다. 챌린저의 지적이 정확했고, 실측으로 재현했습니다(§1.10). 다만 챌린저가 제시한 수정안은 다른 실패 모드를 새로 만듭니다 — 문서 전체를 스캔하므로 산문 속 파일명 언급을 명령으로 오인합니다. 그래서 커맨드 단위 검증(챌린저의 핵심 교정) 에 펜스 스코프(추가 보강) 를 합성했습니다. §1.10.3 에 두 실패 모드를 각각 실측했습니다.
C-2 는 챌린저가 "다중 fallback 취지에 부합" 정도로 완곡하게 제기했지만, 실측해 보니 .mam.env.example 이 문서화한 유일한 이름이 조용히 무시되는 경로였습니다. 근거를 강화해 수용합니다(§1.11).
1. 실측 (Measurements)
§1.1 ~ §1.9 는 Rev.1 에서 확정된 실측이며 재검증 없이 유지합니다. §1.10 · §1.11 이 Rev.2 신규입니다.
1.1 에이전트 해석기가 저장소에 4개 존재한다
| # | 위치 | 우선순위 | 실패 시 |
|---|---|---|---|
| 1 | lib_py/agents/registry.py:26 agent_of_row() |
agent 필드 → 이름 접미사 → pane.cmd |
None |
| 2 | stop_session.sh:101-109 |
이름 접미사만 (역할 한정) | exit 2 |
| 3 | update_yaml_resumed.sh:44-51 |
#2 와 완전 동일한 복사본 | exit 2 |
| 4 | run_loop.sh:278 |
agent 필드 → pane.cmd → 하이픈 세그먼트 → claude 기본값 |
실패 없음 |
SESSION_NAME stop/upd run_loop registry
---------------------------------- ---------- ---------- ----------
x-creator-claude claude claude claude
agy-creator-01 EXIT2 agy None ← 라이브 세션
my-project-dev-claude EXIT2 claude claude ← INSTALL.md 예제 이름
worker-1-agy EXIT2 agy agy
foo-cline EXIT2 cline cline
bad-session-name EXIT2 claude None ← run_loop 은 조용히 claude
orc-hermes-main EXIT2 hermes None
agy-creator-01은 지금 이 워크스페이스에 running 으로 등록된 실제 세션입니다.pane.cmd = 'agy'가 기록돼 있는데도--agent없이는exit 2로 거부됩니다. 브리프가 지목한 결함의 재현 가능한 구체 사례입니다.my-project-dev-claude는deploy/INSTALL.md:95가 스스로 문서화한 세션 이름입니다. 접미사가-dev-claude라 #2 의 역할 한정 케이스에 걸리지 않습니다. INSTALL.md 가--agent claude를 명시해 사고가 안 났을 뿐입니다.run_loop.sh는 해석 실패를claude로 흡수합니다. 호출 12곳이라 이번 범위 밖(§9 K-1).
라이브 3개 행에 agent_of_row 직접 적용:
canary-projects-multi-agent-mux-creator-claude agent_of_row='claude' match_cmd=False → 'claude'
canary-projects-multi-agent-mux-creator-cline agent_of_row='cline' match_cmd=False → 'cline'
agy-creator-01 agent_of_row='agy' match_cmd=False → None
match_cmd=True 는 docstring 상 비-입양(non-adoption) 조회용이고 stop/update_yaml_resumed 가 정확히 그 경우입니다. (reconcile.sh 입양 루프 금지라는 3aee63cf §1.2 반증은 유효하며, 이 계획은 reconcile.sh 를 건드리지 않습니다.)
1.2 agent 필드는 존재하지 않는다
row keys 합집합:
['agy_conversation_id_own', 'attach_command', 'child_pid', 'claude_session_id_own',
'cline_conversation_id_own', 'delegate_job_id', 'herdr_server', 'herdr_session',
'herdr_session_created_at', 'herdr_session_epoch', 'kill_command',
'last_visible_status', 'last_visible_status_at_termination', 'mcp_attachments',
'name', 'pane', 'role', 'start_command', 'status', 'tui']
3개 행 전부 agent=None, pane.cmd 는 3개 전부 채워짐. → agent 필드를 쓰는 코드는 추가하지 않고, 우선순위 ①은 테스트로만 고정합니다(T4b).
1.3 load_state_json 은 YAML 이 아니라 SQLite 를 읽는다
lib.sh:938-978 — .db 우선, 없을 때만 .yaml. 라이브에 .mam/agent-sessions.db(40 KiB) 존재. 문서·커밋 메시지에서 "레지스트리" 로 표현합니다.
1.4 비용
| 항목 | 실측 |
|---|---|
load_state_json 1회 |
~34 ms |
python3 기동 + import lib_py.agents.registry |
~27 ms |
stop_session.sh 가 이미 수행하는 load_state_json |
2회 (:97, :113) |
PYTHONPATH 는 lib.sh:25 가 export 하므로 맨 python3 로 임포트 가능. venv 없는 시스템 파이썬(3.9.6)에서 env -i 검증 완료. registry 는 서드파티 의존 없음(yaml 불필요 — 상태는 JSON 으로 env 전달).
1.5 J-1 재현
페이로드: 1패널 width=50, height=30
| 경로 | 결과 |
|---|---|
--min-cols 0 |
right / single_pane_height_constrained |
MAM_MIN_PANE_COLS=0 |
overflow |
MAM_MIN_COLS=0 |
overflow |
--min-rows 0 |
down |
MAM_MIN_PANE_ROWS=0 |
overflow |
대조군 --min-cols 25 vs MAM_MIN_PANE_COLS=25 |
양쪽 동일 (right) |
대조군이 결함을 or 관용구의 falsy-zero 하나로 국소화합니다.
1.6 J-2 임계값
n=3 n//2=1 -> down ← 현행 d3 단언. 상한 검사 도달 불가
n=4 n//2=2 -> overflow
n=5 n//2=2 -> down ← 판별 가능한 최소 홀수
n=6 n//2=3 -> overflow
n=7 n//2=3 -> down
1.7 문서 실태
| 파일 | 현상 |
|---|---|
multi-agent-mux-stop/SKILL.md |
--agent 0회. 워크플로 예제 3개(:68, :72, :77) 전부 생략 |
deploy/INSTALL.md:94, :98 |
--agent claude 이미 명시 — 유일한 모범 사례 |
multi-agent-mux-create/SKILL.md:146 |
AGENT=claude # or agy |
multi-agent-mux-create/SKILL.md:171 |
must be claude or agy — 실물 create_session.sh:86 은 4종을 받음 |
multi-agent-mux-resume/SKILL.md:61 |
# or agy or hermes (cline 누락) |
create_session.sh:4, resolve_session_id.sh:4 |
헤더 주석 <claude|agy> |
update_yaml_resumed.sh:7, :14 |
[--agent claude|agy] |
create_session.sh 는 이미 --agent 필수 + 4종 검증(:83, :85-86). create 쪽은 문서 동기화뿐입니다.
1.8 기존 테스트 계약
| 테스트 | 세션명 | 현행 |
|---|---|---|
tests/test_tier1_unit.py:142 |
bad-session-name |
rc=2, cannot infer agent |
tests/test_tier3_integration.py:398 |
bad-name |
rc=2, cannot infer agent |
두 이름 모두 샌드박스 레지스트리(herdr_sessions: [])에 없습니다. §3 설계 결정을 지배합니다.
1.9 (부수) cd … 2>/dev/null || pwd 결함
line35 result: [/lib.sh] → 존재하지 않음, 항상 :36 폴백
correct form : [/Users/.../.agents/skills/lib.sh]
잔존: stop_session.sh:35, create_session.sh:23, resume_session.sh:6. (update_yaml_resumed.sh:10 은 이미 정상.) 31b2d70 의 R-2 와 동일 결함. 1차 소싱 경로가 100% 죽어 ${WORKSPACE_ROOT:-$PWD} 폴백에만 의존합니다.
1.10 [Rev.2 신규] Challenge C-1 검증
1.10.1 갈래 ① — checked == 2 로 단언이 실패한다 → 확인
Rev.1 T5 의 블록 단위 정규식을 현재 문서에 그대로 적용:
SKILL.md: total fenced bash/sh blocks=3, containing stop_session.sh=1
-> one block holds 3 stop_session.sh invocations; '--agent' present in block: False
INSTALL.md: total fenced bash/sh blocks=7, containing stop_session.sh=1
-> one block holds 2 stop_session.sh invocations; '--agent' present in block: True
CHECKED = 2 (planner asserted >= 4)
assert checked >= 4 는 결정론적으로 실패합니다. 챌린저의 지적이 정확합니다. 제가 §1.7 에서 "예제 3개(:68, :72, :77)" 를 세면서도 그것이 하나의 펜스 안에 들어 있다는 사실을 확인하지 않은 것이 원인입니다 — 개수는 셌지만 경계를 세지 않았습니다.
1.10.2 갈래 ② — 블록 단위 단언의 위양성(False Positive) → 확인
INSTALL.md 사본에서 두 호출 중 하나에서만 --agent 를 제거하는 뮤테이션:
mutation applied (agent count 2 -> 1)
설계 A (블록 단위, Rev.1 원안): blocks=1 all pass? True ← 뮤테이션 미검출
설계 C (펜스+커맨드, Rev.2 정제안): checked=2 missing=1 ← 뮤테이션 검출
블록에 --agent 가 한 번이라도 나오면 통과합니다. 회귀를 못 잡는 가드는 가드가 아니라 주석입니다. 챌린저의 지적이 정확합니다.
1.10.3 챌린저 수정안의 잔여 실패 모드 → 문서 전체 스캔이 산문을 명령으로 오인한다
챌린저 수정안은 doc.read_text() 전체에 커맨드 정규식을 돌립니다. 산문 속 파일명 언급이 있는 문서로 실측:
=== 챌린저 수정안 (문서 전체 스캔) ===
[1] --agent=NO | '`stop_session.sh` does not delete report trees.' ← 위양성
[2] --agent=NO | '`stop_session.sh --purge-conversation` note below.' ← 위양성
[3] --agent=YES | 'bash .../stop_session.sh --session "$S" --agent "$A"'
[4] --agent=NO | 'bash .../stop_session.sh --session "$S"'
=== 펜스 스코프 + 커맨드 단위 (Rev.2) ===
[1] --agent=YES | 'bash .../stop_session.sh --session "$S" --agent "$A"'
[2] --agent=NO | 'bash .../stop_session.sh --session "$S"'
checked=2
산문 두 줄이 각각 checked += 1 되고 --agent 가 없으므로 테스트가 실패합니다. 이것이 가설이 아니라 임박한 문제인 이유:
- 이 계획 §4.4 자체가 stop/SKILL.md 에 산문 문단을 추가합니다.
stop/SKILL.md의## Pitfalls·## When NOT to use절은 성격상 스크립트를 산문으로 언급하게 되는 자리입니다.- 문장을 하나 썼다고 실패하는 가드는 다음 사람이 지웁니다.
현재 두 문서에는 펜스 밖 언급이 0건이라(SKILL.md 3회·INSTALL.md 2회 모두 펜스 안) 챌린저 수정안도 지금은 통과합니다. 하지만 가드의 존재 이유는 미래의 편집을 견디는 것이므로, 지금 통과하는 것만으로는 부족합니다.
1.10.4 정제안 검증 — 계획 §4.4 적용 후
§4.4 대로 편집한 사본(3개 예제에 --agent "$AGENT" 추가 + stop_session.sh 문자열을 포함하지 않는 산문 문단 추가)에 정제안 적용:
SKILL.md checked=3 missing_agent=0
INSTALL.md checked=2 missing_agent=0
총 5건, 전건 통과. 펜스 스코프 덕분에 "산문에 파일명을 쓰지 말라"는 제약이 계획에서 사라집니다 — 이것이 챌린저 수정안 대비 실질 이득입니다.
1.11 [Rev.2 신규] Observation C-2 검증 — 근거는 챌린저가 제시한 것보다 강하다
1.11.1 어느 이름이 정본인가
.mam.env.example:133 # MAM_MIN_PANE_COLS=60
.mam.env.example:137 # MAM_MIN_PANE_ROWS=20
.mam.env.example:143 # MAM_MAX_PANE_COLS=3
lib.sh:432 --min-cols "${MAM_MIN_PANE_COLS:-60}" --min-rows "${MAM_MIN_PANE_ROWS:-20}"
test_herdr_shim_contract.py:100-101 export MAM_MIN_PANE_COLS=60 / MAM_MIN_PANE_ROWS=20
MAM_MIN_COLS / MAM_MIN_ROWS / MAM_MAX_COLS 단축형은 layout.py:191-193 안에서만 등장합니다. 생산 코드·문서·템플릿·테스트 어디에도 없습니다. report-8f0cb35f.md:72 는 정리 작업 당시 "no legacy MAM_MIN_COLS=/MAM_MIN_ROWS= env-prefix style" 을 확인 사항으로 적고 있습니다 — 단축형은 레거시 별칭입니다.
그런데 _env_int("MAM_MIN_COLS", "MAM_MIN_PANE_COLS") 는 레거시 단축형을 먼저 봅니다.
1.11.2 결과: 문서화된 유일한 이름이 조용히 무시된다
env 현행 or 60 return default continue
{} 60 60 60
{'MAM_MIN_PANE_COLS': '0'} 60 0 0
{'MAM_MIN_PANE_COLS': '25'} 25 25 25
{'MAM_MIN_COLS': 'foo'} 60 60 60
{'MAM_MIN_COLS': '', 'MAM_MIN_PANE_COLS': '25'} 25 25 25
{'MAM_MIN_COLS': 'foo', 'MAM_MIN_PANE_COLS': '25'} 60 60 25 ← 차이
{'MAM_MIN_COLS': 'foo', 'MAM_MIN_PANE_COLS': 'bar'} 60 60 60
{'MAM_MIN_COLS': 'foo', 'MAM_MIN_PANE_COLS': '0'} 60 60 0 ← 차이
읽어야 할 두 가지:
-
""와"foo"가 다르게 취급됩니다. 빈 문자열은 다음 후보로 넘어가고(if raw:가 걸러냄), 무효 문자열은 즉시 탈출합니다. 둘 다 "쓸 수 없는 값"인데 처리가 정반대입니다.continue는 이 비대칭을 없앱니다. -
차이가 나는 두 행에서 무시되는 값은
.mam.env.example이 문서화한 바로 그 변수입니다. 운영자가 템플릿대로MAM_MIN_PANE_COLS=25를 설정했는데, 셸 어딘가에 남은MAM_MIN_COLS=foo하나 때문에 60 이 적용됩니다. -
차이는 정확히 2행뿐입니다. 나머지 7행은 세 구현이 완전히 일치합니다. 즉
continue는 J-1 수정과 직교하고, 행동 변경 표면이 "첫 후보 무효 + 후속 후보 유효" 라는 한 조건으로 좁혀집니다. 테스트 1건으로 완전히 고정할 수 있습니다(T1b).
결론: C-2 수용. 챌린저는 "다중 fallback 취지에 부합" 이라는 설계 논거로 제기했는데, 실측하면 문서화된 설정이 무시되는 실동작 결함이라 근거가 더 강합니다. Rev.1 이 return default 를 고른 이유는 "오타 입력에 대한 행동 동등성 보존" 이었고 그 목표 자체는 유효하지만, 위 표의 5·7행이 보여주듯 continue 도 그 목표를 똑같이 만족합니다(모든 후보가 무효면 default). Rev.1 은 더 좁은 불변식을 지키느라 더 나은 것을 놓쳤습니다.
2. 범위
포함
| # | 항목 |
|---|---|
| S1 | lib.sh 에 resolve_agent_type_from_registry() 공용 헬퍼 신설 |
| S2 | stop_session.sh 폴백을 S1 로 교체 + 헤더/usage() 갱신 |
| S3 | update_yaml_resumed.sh 의 동일 복사본을 S1 로 교체 + 헤더/usage() 갱신 |
| S4 | stop/resume/create SKILL.md 및 3개 스크립트 헤더 주석 문서 동기화 |
| S5 | J-1: _env_int(*names, default=None) 리팩터(+ C-2 continue) 및 main() 배선 |
| S6 | 회귀 테스트 8건 신설 + 기존 J-2 가드 1건 보강 |
| S7 | IMPROVEMENTS.md 백로그 등록 및 완료 카운트 갱신 |
| S8 | (분리 커밋) §1.9 cd … && pwd 3곳 |
제외
| 항목 | 제외 사유 |
|---|---|
run_loop.sh:278 통합 |
호출 12곳 + claude 기본값 제거는 행동 변경 → K-1 |
reconcile.sh 해석 경로 |
3aee63cf §1.2 실측 반증 유효 |
agent_of_row 세그먼트 매칭 |
reconcile.sh 입양 판정에 영향 → K-4 |
max_columns falsy-zero |
브리프가 min-cols/min-rows 만 지목 → K-2 |
_env_int 후보 순서 뒤집기 |
문서화된 MAM_MIN_PANE_COLS 를 앞으로 옮기는 것은 우선순위 변경이라 C-2 (무효값 건너뛰기)와 별개 사안 → K-5 |
레지스트리에 agent 필드 쓰기 |
쓰는 코드가 0건이고 요구되지 않음 |
3. 설계 결정 — 폴백을 어디에 넣는가 (Rev.1 유지)
stop_session.sh 현재 순서:
:88 --session 검사 → exit 2
:89 YAML 파일 존재 검사 → exit 1
:97 resolve_herdr_workspace (load_state_json #1)
:101 AGENT 접미사 추론 → exit 2 ← 교체 대상
:113 MAPPED_DATA: row 조회 (load_state_json #2)
:124 row 없음 → exit 1
:152 AGENT 최초 사용
안 A (기각) — :113 블록에 병합. 프로세스 1개 절약, 코드도 가장 깔끔. 기각 사유: 해석이 row 조회 뒤로 밀려 "미등록 + 이름 해석 실패" 세션의 종료 코드가 2 → 1 로 바뀝니다. §1.8 의 두 테스트가 깨지고 헤더 :28-30 의 계약도 바뀝니다. 얻는 것은 34 ms 뿐입니다.
안 B (채택) — :101 자리를 그대로 두고 해석기만 교체.
| 성질 | 결과 |
|---|---|
| 종료 코드 계약 | 불변 (exit 2, 동일 메시지) |
| §1.8 기존 테스트 2건 | 수정 불필요 |
agy-creator-01 |
EXIT2 → agy ✅ |
my-project-dev-claude, foo-cline, worker-1-agy |
EXIT2 → 정상 해석 ✅ |
미래의 agent 명시 필드 |
자동 지원 ✅ |
| 비용 | --agent 생략 시에만 load_state_json 1회 (~34 ms) |
set -euo pipefail 주의: 실패 가능한 명령 치환을 대입에 쓰므로 반드시 || AGENT="" 로 감쌉니다(test_lib_sh_layout_split_in_set_e_subshell 선례). stderr 는 억제하지 않습니다 — 정상 해석 실패는 sys.exit(1) 이라 무출력이고, PYTHONPATH 파손 같은 진짜 오류의 traceback 은 보여야 합니다. 기존 테스트는 부분 문자열 단언이라 traceback 이 섞여도 무영향입니다.
4. 구현
4.1 S1 — lib.sh 공용 헬퍼
resolve_herdr_session()(:989) 바로 앞에 추가.
# resolve_agent_type_from_registry <session_name>
#
# 레지스트리(YAML/DB)에 기록된 사실로 에이전트 종류를 해석한다. 우선순위는
# lib_py.agents.registry.agent_of_row 의 계약을 그대로 따른다:
# ① row['agent'] 명시 필드
# ② 세션명 접미사 (*-{creator,planner,reviewer}-<agent> 및 *-<agent>)
# ③ pane.cmd (정확히 일치하거나 .../<agent> 바이너리 경로)
# 성공하면 에이전트명을 stdout 에 출력하고 0 을, 셋 다 실패하면 아무것도
# 출력하지 않고 1 을 반환한다. 오류 메시지는 호출자가 소유한다 — 각 스크립트가
# 문서화한 종료 코드를 그대로 유지하기 위해서다.
#
# NOTE: agent_of_row 의 match_cmd=True 는 "비-입양 조회" 계약이다. reconcile.sh
# 입양 루프는 이 헬퍼를 쓰면 안 된다 (3aee63cf §1.2 실측 반증).
resolve_agent_type_from_registry() {
local name="$1"
MAM_STATE_JSON="$(load_state_json)" SESSION_NAME="$name" python3 -c "
import os, json, sys
from lib_py.agents.registry import agent_of_row
name = os.environ['SESSION_NAME']
d = json.loads(os.environ.get('MAM_STATE_JSON', '{}'))
row = next((s for s in d.get('herdr_sessions', []) if s.get('name') == name), {})
resolved = agent_of_row(row, session_name=name)
if not resolved:
sys.exit(1)
print(resolved)
"
}
이름을 resolve_agent_type 로 하지 않는 이유: run_loop.sh:278 이 동명 함수를 정의하며 lib.sh 를 source 합니다. 동명이면 run_loop 의 나중 정의가 조용히 덮어써서 12개 호출 지점이 어느 구현을 쓰는지 읽어서는 알 수 없게 됩니다.
4.2 S2 — stop_session.sh
:100-109 교체:
# --agent 미지정 시 레지스트리 기록으로 해석 (B-21).
# ① row['agent'] → ② 세션명 접미사 → ③ pane.cmd 순. 셋 다 실패하면
# 종전과 동일하게 exit 2 (헤더 :27-30 의 종료 코드 계약 유지).
if [ -z "$AGENT" ]; then
AGENT="$(resolve_agent_type_from_registry "$SESSION_NAME")" || AGENT=""
[ -n "$AGENT" ] || {
echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2
exit 2
}
fi
헤더 :15-16:
# --agent <type> — claude | agy | hermes | cline
# (권장: 항상 명시. 미지정 시 레지스트리 기록으로
# 해석 — agent 필드 → 세션명 접미사 → pane.cmd;
# 셋 다 실패하면 exit 2)
usage() :46-47:
--agent <type> — claude | agy | hermes | cline (recommended: always pass it)
(falls back to the registry record: agent field ->
session-name suffix -> pane.cmd)
usage() 에 4개 에이전트명이 모두 남아야 합니다 — test_comp_stop_usage_matches_parser(test_tier2_component.py:711-712)가 단언합니다.
4.3 S3 — update_yaml_resumed.sh
:43-52 를 S2 와 동일한 블록으로 교체(메시지·종료 코드 동일). 헤더 :7 / usage() :14 의 [--agent claude|agy] → [--agent claude|agy|hermes|cline]. :10 은 이미 올바른 소싱 형태이므로 손대지 않습니다.
4.4 S4 — 문서 동기화
multi-agent-mux-stop/SKILL.md
Pre-flight(:38-40):
SESSION_NAME=<workspace>-creator-<agent> # convention
AGENT=claude # claude | agy | hermes | cline — always pass it
AGENT_SESSIONS_YAML=.mam/agent-sessions.yaml
워크플로 예제 3개(:68, :72, :77)에 --agent "$AGENT" 추가:
# 1. Stop gracefully (default — captures ID, shuts down safely, status=stopped)
bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
--session "$SESSION_NAME" --agent "$AGENT"
"Idempotency" 문단(:81) 아래에 추가:
**`--agent` is the standard.** Pass it on every invocation. If omitted, the script
resolves the agent from the registry record — the row's `agent` field, then the
session-name suffix, then `pane.cmd` — and exits 2 if none of the three resolve.
The fallback exists for recovery, not as the normal calling convention: a session
whose name carries no agent suffix (e.g. `agy-creator-01`) is only resolvable
while its registry row survives.
Rev.1 에 있던 제약 삭제. Rev.1 은 이 산문에
stop_session.sh문자열을 쓰지 말라는 제약을 걸어야 했습니다. Rev.2 의 T5 가 펜스 스코프이므로 그 제약이 필요 없습니다(§1.10.3~4). 산문을 자유롭게 쓰십시오.
multi-agent-mux-resume/SKILL.md:61 — AGENT=claude # or agy or hermes → AGENT=claude # claude | agy | hermes | cline — pass it explicitly
multi-agent-mux-create/SKILL.md
:146동일 수정:171— 실물create_session.sh:86과 동일한claude, agy, hermes or cline문구로. 같은case(:158-172)에hermes/clinearm 이 없으므로, 스니펫을 축약하고 실물 스크립트를 가리키게 하는 쪽을 권장합니다. SKILL.md 스니펫이 실물과 갈라지는 것 자체가 이번에 고치는 결함군입니다.
스크립트 헤더 주석 — create_session.sh:4, resolve_session_id.sh:4 의 --agent <claude|agy> → <claude|agy|hermes|cline>
4.5 S5 — J-1 (+ C-2)
layout.py:175-186:
def _env_int(*names: str, default: Optional[int] = None) -> Optional[int]:
"""First *valid* int among the env vars in *names*, else `default`.
`default` is an explicit parameter rather than an `or` at the call site so a
legitimate 0 survives (MAM_MIN_PANE_COLS=0 means 0, not the 60 default).
An unparsable value is skipped rather than raised or treated as terminal: a
typo in an operator's shell must not take the whole layout call down (lib.sh
would silently fall back to 'right'), and must not shadow a later candidate
that IS set correctly -- MAM_MIN_COLS is a legacy alias while
MAM_MIN_PANE_COLS is the name .mam.env.example documents, so aborting on the
first bad value would discard the documented setting. Empty values already
fell through; this makes invalid values behave the same way.
"""
for n in names:
raw = os.environ.get(n, "").strip()
if raw:
try:
return int(raw)
except ValueError:
continue
return default
main() :191-193:
parser.add_argument("--min-cols", type=int, default=_env_int("MAM_MIN_COLS", "MAM_MIN_PANE_COLS", default=60))
parser.add_argument("--min-rows", type=int, default=_env_int("MAM_MIN_ROWS", "MAM_MIN_PANE_ROWS", default=20))
parser.add_argument("--max-cols", type=int, default=_env_int("MAM_MAX_COLS", "MAM_MAX_PANE_COLS"))
--max-cols 는 default=None 이 의도된 의미(미지정 = 상한 없음)이므로 그대로 둡니다.
return default 가 아니라 continue 여야 하는 이유(불변식 확인): 모든 후보가 없거나 무효이면 루프가 끝나 return default 에 도달합니다. 즉 Rev.1 이 지키려던 "오타 입력은 문서화된 기본값으로 흡수된다"는 성질은 그대로 유지되며(§1.11.2 표 4·7행), 달라지는 것은 "첫 후보 무효 + 후속 후보 유효" 한 조건뿐입니다. min_cols=None 으로 compute_2xk_layout 에 들어가 TypeError 가 나는 경로는 두 안 모두에서 발생하지 않습니다.
*names 뒤의 키워드 전용 default 는 Python 3.9 에서 유효합니다(시스템 인터프리터 3.9.6 실측). _env_int 호출자는 main() 3곳뿐입니다.
5. 테스트 계획
신설 8건, 기존 가드 보강 1건. 예상 collected: 333 → 341.
Rev.1 자체 정정: Rev.1 은 "신설 8건 → 341" 이라고 적었으나 실제 열거는 7건이었습니다(T1 3 + T3 1 + T4 2 + T5 1). Rev.2 는 C-2 전용 테스트 T1b 를 더해 실제로 8건이 되며, 341 이라는 수치가 비로소 맞아떨어집니다.
T1 — J-1 env/flag 등가성 (tests/test_layout.py, 3건)
_LAYOUT_ENV_VARS = ("MAM_MIN_COLS", "MAM_MIN_PANE_COLS", "MAM_MIN_ROWS",
"MAM_MIN_PANE_ROWS", "MAM_MAX_COLS", "MAM_MAX_PANE_COLS")
def _run_layout(payload, args=(), env_extra=None):
env = {**os.environ, "PYTHONPATH": os.path.abspath(".agents/skills")}
for k in _LAYOUT_ENV_VARS:
env.pop(k, None) # 호출자 셸의 오염 차단
env.update(env_extra or {})
res = subprocess.run([sys.executable, "-m", "lib_py.layout", "--json", *args],
input=json.dumps(payload), capture_output=True, text=True, env=env)
assert res.returncode == 0, res.stderr
return json.loads(res.stdout)
# height//2 = 15 < min_rows(20) 로 제약 분기 진입, width//2 = 25 가 min_cols 와 비교됨.
_ZERO_TRAP = {"result": {"panes": [
{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 50, "height": 30}}]}}
def test_j1_env_zero_min_cols_matches_flag_zero():
"""J-1: MAM_MIN_PANE_COLS=0 must mean 0, not fall through to the 60 default."""
flag = _run_layout(_ZERO_TRAP, ("--min-cols", "0"))
assert flag["direction"] == "right" and flag["reason"] == "single_pane_height_constrained"
for var in ("MAM_MIN_COLS", "MAM_MIN_PANE_COLS"):
assert _run_layout(_ZERO_TRAP, (), {var: "0"}) == flag, var
def test_j1_env_zero_min_rows_matches_flag_zero():
flag = _run_layout(_ZERO_TRAP, ("--min-rows", "0"))
assert flag["direction"] == "down" and flag["reason"] == "single_pane_split_down"
for var in ("MAM_MIN_ROWS", "MAM_MIN_PANE_ROWS"):
assert _run_layout(_ZERO_TRAP, (), {var: "0"}) == flag, var
def test_j1_nonzero_and_malformed_env_behaviour_unchanged():
"""Behaviour neutrality: non-zero env still applies, and a lone typo still
lands on the documented default instead of crashing on a None comparison."""
assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_PANE_COLS": "25"}) == \
_run_layout(_ZERO_TRAP, ("--min-cols", "25"))
assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_PANE_COLS": "abc"}) == _run_layout(_ZERO_TRAP)
T1b — [Rev.2 신규] C-2: 무효값이 뒤 후보를 가리지 않는다 (1건)
def test_j1b_invalid_alias_does_not_shadow_the_documented_var():
"""C-2: MAM_MIN_COLS is a legacy alias checked first; MAM_MIN_PANE_COLS is the
name .mam.env.example documents. An unparsable value in the alias must be
skipped, not abort the search and discard the documented setting.
Empty values already fell through (`if raw:`); this makes invalid values
behave the same way. When every candidate is unusable, `default` still wins.
"""
good = _run_layout(_ZERO_TRAP, (), {"MAM_MIN_PANE_COLS": "25"})
assert good["direction"] == "right"
# 별칭이 깨져 있어도 문서화된 변수가 적용된다
assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_COLS": "foo",
"MAM_MIN_PANE_COLS": "25"}) == good
# 0 도 마찬가지 (J-1 과의 상호작용)
assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_COLS": "foo",
"MAM_MIN_PANE_COLS": "0"}) == \
_run_layout(_ZERO_TRAP, ("--min-cols", "0"))
# 모든 후보가 무효면 문서화된 기본값으로 흡수 (Rev.1 불변식 보존)
assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_COLS": "foo",
"MAM_MIN_PANE_COLS": "bar"}) == _run_layout(_ZERO_TRAP)
마지막 단언이 중요합니다 — continue 로 바꾸면서 Rev.1 이 지키려던 성질이 깨지지 않았음을 같은 테스트 안에서 못 박습니다.
T2 — J-2 임계값 보강 (기존 test_headless_max_columns_growth_guard 확장, 신설 0건)
# n=5 is the first odd n that can discriminate: n//2 == 2 == max_columns, so an
# over-correction that also checked the cap on the odd branch would return
# overflow here. n=3 has n//2 == 1 and cannot reach the check at all.
d5 = compute_2xk_layout(headless(5), max_columns=2)
assert d5.direction == "down" and not d5.is_overflow
assert d5.reason == "headless_odd_down"
계획 5e4ef463 의 뮤테이션 M6 사양 오류(제가 n=3 을 골랐고 그 값으로는 판별 불가)를 닫습니다.
T3 — agent_of_row 단위 보강 (tests/test_a4_adapter_contract.py, 1건)
def test_agent_of_row_pane_cmd_binary_path_and_failure():
# pane.cmd 가 절대 경로 형태여도 해석된다
assert agent_of_row({'pane': {'cmd': '/usr/local/bin/agy'}}) == 'agy'
# 세 경로 모두 실패하면 None — 호출자가 오류를 소유한다
assert agent_of_row({}, session_name='bad-session-name') is None
# 입양 조회용 match_cmd=False 에서는 pane.cmd 를 보지 않는다
assert agent_of_row({'name': 'agy-creator-01', 'pane': {'cmd': 'agy'}},
match_cmd=False) is None
T4 — stop_session.sh 폴백 (tests/test_tier2_component.py, 2건)
기존 test_comp_stop_sqlite_state_update 의 run_mutation 패턴 사용(herdr 부재 → "herdr already dead, just updating YAML" 경로로 rc=0 완주, 실제 세션 미영향).
def test_comp_stop_agent_fallback_reads_pane_cmd(mam_sandbox):
"""B-21: --agent 생략 시 세션명에 에이전트 접미사가 없어도 레지스트리 행의
pane.cmd 로 해석된다 (라이브 `agy-creator-01` 형태)."""
mutation = """
d['herdr_sessions'] = [{
'name': 'agy-creator-01',
'status': 'running',
'pane': {'cwd': 'WS_PLACEHOLDER', 'cmd': 'agy'}
}]
""".replace("WS_PLACEHOLDER", str(mam_sandbox))
run_mutation(mam_sandbox, mutation)
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
res = subprocess.run(["bash", str(script), "--session", "agy-creator-01"],
capture_output=True, text=True)
assert res.returncode == 0, res.stderr
assert re.search(r"^\s*agent:\s+agy\s*$", res.stdout, re.M), res.stdout
def test_comp_stop_agent_fallback_prefers_explicit_agent_field(mam_sandbox):
"""우선순위 계약: 명시 `agent` 필드가 세션명 접미사와 pane.cmd 를 모두 이긴다."""
mutation = """
d['herdr_sessions'] = [{
'name': 'x-creator-claude',
'status': 'running',
'agent': 'hermes',
'pane': {'cwd': 'WS_PLACEHOLDER', 'cmd': 'claude'}
}]
""".replace("WS_PLACEHOLDER", str(mam_sandbox))
run_mutation(mam_sandbox, mutation)
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
res = subprocess.run(["bash", str(script), "--session", "x-creator-claude"],
capture_output=True, text=True)
assert res.returncode == 0, res.stderr
assert re.search(r"^\s*agent:\s+hermes\s*$", res.stdout, re.M), res.stdout
두 번째가 T4 를 "pane.cmd 를 읽는다" 가 아니라 "agent_of_row 계약을 호출한다" 로 고정합니다. 첫 번째만 있으면 pane.cmd 만 직접 읽는 얕은 구현도 통과합니다.
미해결 계약(exit 2) 은 이미 test_tier1_unit.py:142 와 test_tier3_integration.py:398 이 지킵니다. 두 파일을 수정하지 않은 채 통과하는 것이 안 B 의 증거이므로 중복 테스트를 추가하지 않습니다.
T5 — [Rev.2 재설계] 문서 가드 (tests/test_tier2_component.py, 1건)
Rev.1 원안은 §1.10.1 의 실측대로 checked == 2 로 확정 실패하고, §1.10.2 대로 단일 호출 회귀를 놓칩니다. 챌린저의 커맨드 단위 교정을 채택하되, §1.10.3 의 산문 위양성을 막기 위해 펜스 스코프를 합성합니다.
# 코드 펜스 안의 stop_session.sh 호출을 '명령 단위'로 잘라낸다.
# - 펜스 스코프: 산문 속 `stop_session.sh` 언급을 명령으로 오인하지 않는다
# (Pitfalls / When-NOT-to-use 절은 성격상 스크립트를 산문으로 언급한다).
# - 명령 단위: 한 펜스에 여러 호출이 들어 있어도 각각을 따로 검증한다
# (블록 단위로 보면 그중 하나만 --agent 를 가져도 통과해 버린다).
_FENCE_RE = re.compile(r"```(?:bash|sh)\n(.*?)```", re.S)
_STOP_CALL_RE = re.compile(r"(?:bash\s+)?\S*stop_session\.sh[^\n\\]*(?:\\\n[^\n\\]*)*")
def test_comp_docs_stop_examples_pass_agent():
"""B-21 문서 계약: 문서의 모든 stop_session.sh 예제는 --agent 를 넘긴다.
문서 변경은 뮤테이션 감도가 없으므로 이 가드가 표준의 유일한 집행 장치다."""
repo = Path(__file__).resolve().parent.parent
expected = { # 문서별 최소 예제 수 — 예제를 지워 가드를 무력화하는 것을 막는다
repo / ".agents/skills/multi-agent-mux-stop/SKILL.md": 3,
repo / "deploy/INSTALL.md": 2,
}
for doc, floor in expected.items():
seen = 0
for block in _FENCE_RE.findall(doc.read_text()):
for m in _STOP_CALL_RE.finditer(block):
snippet = m.group(0)
seen += 1
assert "--agent" in snippet, \
f"{doc.name}: stop_session.sh example without --agent:\n{snippet}"
assert seen >= floor, f"{doc.name}: expected >= {floor} examples, saw {seen}"
Rev.1/챌린저안 대비 세 가지가 다릅니다.
| Rev.1 원안 | 챌린저 수정안 | Rev.2 | |
|---|---|---|---|
| 검증 단위 | 코드 블록 | 명령 | 명령 |
| 스캔 범위 | 펜스 | 문서 전체 | 펜스 |
| 개수 하한 | 전역 >= 4 (실패) |
전역 >= 5 |
문서별 (3 / 2) |
전역 카운트를 문서별로 쪼갠 이유: 전역이면 SKILL.md 예제 1개가 사라져도 INSTALL.md 가 6개면 통과합니다. 문서별 하한은 실패를 발생 지점에 국소화합니다.
실측 확인 (§1.10.4): §4.4 적용 후 사본에서 SKILL.md checked=3 missing=0, INSTALL.md checked=2 missing=0.
T6 — 회귀 무영향 확인
bash -n: lib.sh, stop_session.sh, update_yaml_resumed.sh, create_session.sh, resume_session.sh, resolve_session_id.sh.
py_compile: lib_py/layout.py. 시스템 파이썬 3.9.6 임포트 확인.
테스트 파일 사전 조건 2건
test_tier2_component.py:296의FEATURE 3: Stop Session (4 Test Cases)주석 개수 갱신(→ 7). 같은 종류의 드리프트를 새로 만들지 않도록.test_tier2_component.py는Path는 임포트하지만re는 임포트하지 않습니다(:1-10). T4/T5 가re를 쓰므로import re추가 필요.test_layout.py는 T1/T1b 가 쓰는os/json/subprocess/sys를 모두 이미 임포트하고 있어 추가 불필요합니다.
6. 뮤테이션 매트릭스
격리 사본(rsync)에 적용해 지정 테스트가 FAIL 하는지 확인.
| # | 뮤테이션 | FAIL 해야 하는 테스트 |
|---|---|---|
| M1 | stop_session.sh 폴백을 옛 case 블록으로 복원 |
test_comp_stop_agent_fallback_reads_pane_cmd |
| M2 | 헬퍼에서 agent_of_row(row, …) → agent_of_row({}, session_name=name) |
위 + …prefers_explicit_agent_field |
| M3 | 헬퍼에 match_cmd=False 추가 |
…reads_pane_cmd 만 (두 테스트가 서로 다른 성질을 잡음을 증명) |
| M4 | _env_int(…, default=60) → _env_int(…) or 60 |
test_j1_env_zero_min_cols_matches_flag_zero |
| M5 | _env_int 의 except ValueError: continue → return None |
test_j1_nonzero_and_malformed_env_behaviour_unchanged (rc≠0) |
| M5b | [Rev.2] except ValueError: continue → return default |
test_j1b_invalid_alias_does_not_shadow_the_documented_var |
| M6 | 헤드리스 홀수 분기에도 max_columns 검사 추가 (과잉 교정) |
test_headless_max_columns_growth_guard (신설 d5 단언) |
| M7 | SKILL.md 예제 한 곳에서 --agent 삭제 |
test_comp_docs_stop_examples_pass_agent |
| M7b | [Rev.2] INSTALL.md 의 두 호출 중 하나에서만 --agent 삭제 |
동일 (§1.10.2 에서 이미 선실측: Rev.1 설계는 미검출, Rev.2 설계는 missing=1 검출) |
| M7c | [Rev.2] SKILL.md 워크플로 예제 1개를 통째로 삭제 |
동일 (seen >= 3 하한) |
| M8 | update_yaml_resumed.sh 폴백을 옛 case 블록으로 복원 |
— 가드 없음 |
M8 을 정직하게 남깁니다. update_yaml_resumed.sh 의 폴백은 유일한 생산 호출자인 resume_session.sh:66, :129 가 항상 --agent "$AGENT" 를 명시하므로 그 경로에서 도달 불가합니다. 직접 호출 시에만 살아납니다. 도달 불가 경로를 위해 별도 픽스처를 세우는 대신 S3 는 "중복 제거"로 정당화하고 가드 없음을 명시합니다. 리뷰어가 이 판단에 이의가 있으면 T4 와 동형의 테스트 추가가 옳은 처방입니다.
M5 와 M5b 가 서로 다른 테스트를 깨는 것이 C-2 반영의 검증 조건입니다. M5(=None 복귀)는 크래시 경로를, M5b(=Rev.1 안으로 복귀)는 별칭 섀도잉을 각각 잡습니다. 둘 다 잡히지 않으면 T1b 가 의미 없는 테스트라는 뜻입니다.
7. 커밋 분할
| # | 커밋 | 내용 |
|---|---|---|
| 1 | feat(lib,stop,resume): resolve --agent from the registry via agent_of_row (B-21) |
S1 + S2 + S3 + T3 + T4 |
| 2 | docs(skills): standardize explicit --agent across stop/resume/create guides (B-21) |
S4 + T5 |
| 3 | fix(layout): make _env_int take an explicit default and skip invalid values (J-1) |
S5 + T1 + T1b |
| 4 | test(layout): cover the headless growth-guard threshold at n=5 (J-2) |
T2 |
| 5 | docs(improvements): register J-1/J-2/B-21 and refresh the completed count |
S7 |
| 6 | fix(scripts): repair the dead lib.sh sourcing path in stop/create/resume |
S8 (§1.9) |
커밋 1~4 는 각각 독립 revert 가능합니다. 커밋 6 은 §1.9 가 브리프 범위 밖의 별개 사안이므로 분리합니다 — 리뷰어가 범위 이탈로 판단하면 이 커밋만 드롭하면 됩니다.
커밋 3 의 제목이 Rev.1 에서 바뀌었습니다(… and skip invalid values 추가). C-2 가 J-1 과 다른 성질의 변경이므로 제목이 그 사실을 담아야 합니다.
8. IMPROVEMENTS.md 갱신 (S7)
J-1 / J-2 는 현재 IMPROVEMENTS.md 에 등록돼 있지 않습니다(55d1a1d9 리뷰 보고서에만 존재).
ID 충돌 경고: IMPROVEMENTS.md:301 의 C-1("Kanban 문서 29회 언급 vs 실제 구현 0건")과 :37 이 참조하는 C-1(레이아웃 헤드리스 max_columns)은 서로 다른 두 과제가 같은 ID 를 씁니다. 신규는 J-1/J-2/B-21 을 씁니다. 기존 충돌은 K-3.
갱신 항목:
:3최종 갱신일:6총 추적 미해결 과제 카운트:7완료 과제 29 → 30 및 목록에B-21추가:37B-20 후속 정리 줄에 J-1/J-2 해소 한 줄- §2 에
B-21절 신설 — 현상(라이브agy-creator-01이--agent없이exit 2), 원인(해석기 4중화), 조치, 회귀 가드 - §6.2 로드맵 표에 완료 행
- §6.3 파일 소유권 슬롯 표 갱신
9. 후속 백로그 (이번 범위 밖, 등록만)
| ID | 내용 | 근거 |
|---|---|---|
| K-1 | run_loop.sh:278 resolve_agent_type 통합 |
§1.1 — 해석 실패를 claude 로 흡수. cline 세션에 claude 종료키를 보내는 오분류가 구조적으로 가능. 호출 12곳이라 별도 계획 필요 |
| K-2 | compute_2xk_layout 의 if max_columns and … falsy-zero |
--max-cols 0("열 0개")이 "상한 없음"으로 흡수됨. J-1 과 동일 부류 |
| K-3 | IMPROVEMENTS.md 의 C-1 ID 충돌 정리 |
§8 |
| K-4 | agent_of_row 에 하이픈 세그먼트 매칭 추가 여부 |
orc-hermes-main 류 미해결. reconcile.sh 입양 판정 영향 → 실측 선행 |
| K-5 | [Rev.2 신규] _env_int 후보 순서 재검토 |
§1.11.1 — 문서화되지 않은 레거시 MAM_MIN_COLS 가 .mam.env.example 이 문서화한 MAM_MIN_PANE_COLS 보다 우선합니다. C-2(무효값 건너뛰기)는 이 순서 문제를 완화할 뿐 해소하지 않습니다. 둘 다 유효한 값이면 여전히 레거시가 이깁니다. 순서 변경은 행동 변경이므로 별도 항목 |
10. 검증 절차 (Creator 실행)
# 1) 구문
for f in .agents/skills/lib.sh \
.agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \
.agents/skills/multi-agent-mux-resume/scripts/update_yaml_resumed.sh \
.agents/skills/multi-agent-mux-resume/scripts/resume_session.sh \
.agents/skills/multi-agent-mux-resume/scripts/resolve_session_id.sh \
.agents/skills/multi-agent-mux-create/scripts/create_session.sh; do
bash -n "$f" || echo "FAIL $f"
done
python3 -m py_compile .agents/skills/lib_py/layout.py
# 2) J-1 직접 확인 (0 이 살아남는가)
P='{"result":{"panes":[{"pane_id":"p1","rect":{"x":0,"y":0,"width":50,"height":30}}]}}'
printf '%s' "$P" | PYTHONPATH=.agents/skills MAM_MIN_PANE_COLS=0 python3 -m lib_py.layout --json
printf '%s' "$P" | PYTHONPATH=.agents/skills python3 -m lib_py.layout --json --min-cols 0
# → 두 출력이 완전히 동일하고 direction=right
# 3) C-2 직접 확인 (무효 별칭이 문서화된 변수를 가리지 않는가)
printf '%s' "$P" | PYTHONPATH=.agents/skills \
env MAM_MIN_COLS=foo MAM_MIN_PANE_COLS=25 python3 -m lib_py.layout --json
# → direction=right (수정 전에는 overflow)
# 4) 전체 스위트 (베이스라인 333 → 기대 341)
.venv/bin/python -m pytest tests/ -q
# 5) 배포 신선도 (기존 31건 유지)
.venv/bin/python -m pytest tests/test_deploy_freshness.py -q
# 6) 뮤테이션 M1~M7c (격리 사본에서)
금지 사항: tests/test_tier1_unit.py:142 와 tests/test_tier3_integration.py:398 은 수정하지 않습니다. 두 건이 무수정으로 PASS 하는 것이 안 B 의 종료 코드 계약 보존을 입증하는 증거입니다. 고쳐야 통과한다면 구현이 안 A 로 흘러간 것이므로 되돌려야 합니다.
라이브 세션 보호: T4 는 mam_sandbox 안에서만 동작하며 실제 .mam/agent-sessions.yaml 을 건드리지 않습니다. 개발 중 stop_session.sh 를 실 워크스페이스에서 수동 실행하지 마십시오 — canary-projects-multi-agent-mux-creator-claude 가 이 세션입니다.
11. 규모 추정
| 파일 | 변경 |
|---|---|
.agents/skills/lib.sh |
+22 (헬퍼 1개) |
stop_session.sh |
+8 / −10, 헤더·usage +6 |
update_yaml_resumed.sh |
+8 / −9, 헤더·usage +2 |
lib_py/layout.py |
+10 / −6 (docstring 확장 포함) |
multi-agent-mux-stop/SKILL.md |
+12 |
multi-agent-mux-resume/SKILL.md |
+1 / −1 |
multi-agent-mux-create/SKILL.md |
+2 / −2 |
create_session.sh / resolve_session_id.sh |
헤더 각 +1 / −1 |
tests/test_layout.py |
+62 (T1 45 + T1b 17) |
tests/test_a4_adapter_contract.py |
+9 |
tests/test_tier2_component.py |
+58 |
IMPROVEMENTS.md |
+20 |
| (커밋 6) 3개 스크립트 소싱 줄 | +3 / −3 |
총 약 +215 / −35 줄, 파일 12개. 규모 소~중.
12. 챌린저에게
C-1 은 계획대로 짜면 확정 실패하는 결함이었고, 두 갈래 모두 정확했습니다. 특히 갈래 ②(부분 문자열 위양성)는 테스트가 통과하기 때문에 아무도 눈치채지 못하는 종류라 더 값어치가 있습니다. §1.10.2 에서 뮤테이션으로 재현했습니다.
수정안을 그대로 채택하지 않은 부분은 한 곳입니다 — 문서 전체 스캔이 산문 속 파일명 언급을 명령으로 오인합니다(§1.10.3, 위양성 2건 실측). 이 계획 §4.4 자체가 SKILL.md 에 산문을 추가하므로 임박한 문제였습니다. 커맨드 단위라는 핵심 교정은 그대로 채택하고 펜스 스코프를 얹었습니다.
C-2 는 제기하신 근거(다중 fallback 취지)보다 강한 근거가 실측에서 나왔습니다. MAM_MIN_COLS 는 layout.py 밖 어디에도 없는 레거시 별칭이고, 가려지는 MAM_MIN_PANE_COLS 는 .mam.env.example 이 문서화한 유일한 이름입니다. 게다가 현행 코드는 "" 는 건너뛰고 "foo" 는 탈출하는 비대칭을 갖고 있습니다. 수용하고 전용 테스트 T1b 를 신설했습니다.