Files
multi-agent-mux/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-79ff98ed.md
T
Godopu f7e1513585 refactor: standardize --agent option usage, improve YAML registry fallback, and fix layout falsy-zero trap (J-1)
- 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.
2026-08-24 10:08:27 +09:00

46 KiB
Raw Blame History

📐 구현 계획서 Rev.2 — Job d6f54b7f (원안: 79ff98ed)

  • 역할: Planner (MULTI_AGENT_RULES.md §1 — 저장소 코드/문서 무수정, 산출물은 본 보고서)
  • 기준 커밋: 14e306b (branch refactor, working tree clean)
  • 베이스라인: pytest tests/ --collect-only333 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_intValueErrorcontinue 수용. 챌린저가 제시한 것보다 근거가 더 강함 §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
  1. agy-creator-01 은 지금 이 워크스페이스에 running 으로 등록된 실제 세션입니다. pane.cmd = 'agy' 가 기록돼 있는데도 --agent 없이는 exit 2 로 거부됩니다. 브리프가 지목한 결함의 재현 가능한 구체 사례입니다.
  2. my-project-dev-claudedeploy/INSTALL.md:95 가 스스로 문서화한 세션 이름입니다. 접미사가 -dev-claude 라 #2 의 역할 한정 케이스에 걸리지 않습니다. INSTALL.md 가 --agent claude 를 명시해 사고가 안 났을 뿐입니다.
  3. 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)

PYTHONPATHlib.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  ← 차이

읽어야 할 두 가지:

  1. """foo" 가 다르게 취급됩니다. 빈 문자열은 다음 후보로 넘어가고(if raw: 가 걸러냄), 무효 문자열은 즉시 탈출합니다. 둘 다 "쓸 수 없는 값"인데 처리가 정반대입니다. continue 는 이 비대칭을 없앱니다.

  2. 차이가 나는 두 행에서 무시되는 값은 .mam.env.example 이 문서화한 바로 그 변수입니다. 운영자가 템플릿대로 MAM_MIN_PANE_COLS=25 를 설정했는데, 셸 어딘가에 남은 MAM_MIN_COLS=foo 하나 때문에 60 이 적용됩니다.

  3. 차이는 정확히 2행뿐입니다. 나머지 7행은 세 구현이 완전히 일치합니다. 즉 continue 는 J-1 수정과 직교하고, 행동 변경 표면이 "첫 후보 무효 + 후속 후보 유효" 라는 한 조건으로 좁혀집니다. 테스트 1건으로 완전히 고정할 수 있습니다(T1b).

결론: C-2 수용. 챌린저는 "다중 fallback 취지에 부합" 이라는 설계 논거로 제기했는데, 실측하면 문서화된 설정이 무시되는 실동작 결함이라 근거가 더 강합니다. Rev.1 이 return default 를 고른 이유는 "오타 입력에 대한 행동 동등성 보존" 이었고 그 목표 자체는 유효하지만, 위 표의 5·7행이 보여주듯 continue 도 그 목표를 똑같이 만족합니다(모든 후보가 무효면 default). Rev.1 은 더 좁은 불변식을 지키느라 더 나은 것을 놓쳤습니다.


2. 범위

포함

# 항목
S1 lib.shresolve_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 EXIT2agy
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:61AGENT=claude # or agy or hermesAGENT=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/cline arm 이 없으므로, 스니펫을 축약하고 실물 스크립트를 가리키게 하는 쪽을 권장합니다. 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-colsdefault=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_updaterun_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:142test_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건

  1. test_tier2_component.py:296FEATURE 3: Stop Session (4 Test Cases) 주석 개수 갱신(→ 7). 같은 종류의 드리프트를 새로 만들지 않도록.
  2. test_tier2_component.pyPath 는 임포트하지만 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_intexcept ValueError: continuereturn None test_j1_nonzero_and_malformed_env_behaviour_unchanged (rc≠0)
M5b [Rev.2] except ValueError: continuereturn 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:301C-1("Kanban 문서 29회 언급 vs 실제 구현 0건")과 :37 이 참조하는 C-1(레이아웃 헤드리스 max_columns)은 서로 다른 두 과제가 같은 ID 를 씁니다. 신규는 J-1/J-2/B-21 을 씁니다. 기존 충돌은 K-3.

갱신 항목:

  1. :3 최종 갱신일
  2. :6 총 추적 미해결 과제 카운트
  3. :7 완료 과제 29 → 30 및 목록에 B-21 추가
  4. :37 B-20 후속 정리 줄에 J-1/J-2 해소 한 줄
  5. §2 에 B-21 절 신설 — 현상(라이브 agy-creator-01--agent 없이 exit 2), 원인(해석기 4중화), 조치, 회귀 가드
  6. §6.2 로드맵 표에 완료 행
  7. §6.3 파일 소유권 슬롯 표 갱신

9. 후속 백로그 (이번 범위 밖, 등록만)

ID 내용 근거
K-1 run_loop.sh:278 resolve_agent_type 통합 §1.1 — 해석 실패를 claude 로 흡수. cline 세션에 claude 종료키를 보내는 오분류가 구조적으로 가능. 호출 12곳이라 별도 계획 필요
K-2 compute_2xk_layoutif max_columns and … falsy-zero --max-cols 0("열 0개")이 "상한 없음"으로 흡수됨. J-1 과 동일 부류
K-3 IMPROVEMENTS.mdC-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:142tests/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_COLSlayout.py 밖 어디에도 없는 레거시 별칭이고, 가려지는 MAM_MIN_PANE_COLS.mam.env.example 이 문서화한 유일한 이름입니다. 게다가 현행 코드는 "" 는 건너뛰고 "foo" 는 탈출하는 비대칭을 갖고 있습니다. 수용하고 전용 테스트 T1b 를 신설했습니다.