Files
multi-agent-mux/.agents/reports/canary-projects-multi-agent-mux-creator-claude/plan-73b18819.md
T

23 KiB

📐 구현 계획서 Rev.2 — C-6: stop_session.sh 레거시 주석 및 구버전 사용법 정리

  • Job ID: 32167a9d (Rev.1 = 73b18819)
  • Planner: claude (session: herdr:canary-projects-multi-agent-mux-creator-claude)
  • Role: Planner (MULTI_AGENT_RULES.md §1 — 본 작업에서 저장소 코드 0건 수정)
  • 반영 대상 Challenge: 8b6b574f (agy, Worker / Plan Reviewer) — [VERDICT: PASS WITH CHALLENGE]
  • 기준 커밋: 5ed39f8 (refactor, 작업 트리에 미추적 VERSIONS.md 1건)
  • 백로그 항목: C-6 / 로드맵 P2-3

0. 요약

Challenge 는 타당합니다. 전면 수용합니다. 격리 클론에서 실제 mam_sandbox 픽스처로 실행해 재현했습니다 — Rev.1 §4.2-(2) 는 4개 하위 케이스 중 3개가 rc=2 로 실패했을 것입니다.

다만 Rev.2 는 챌린저의 권고안을 그대로 채택하지 않고 두 가지를 더합니다.

  1. 챌린저 권고(valid_session 사용)는 증상을 해소하지만, 가드를 C-6 과 무관한 불변식(:91-100 에이전트 접미사 명명 규칙)에 결합시킵니다. rc=25가지 서로 다른 원인에 공유되고 있다는 것이 이 오탐의 근본 원인이므로, Rev.2 는 종료 코드 대신 stderr 메시지를 단언해 원인 결합 자체를 제거합니다.
  2. 확정 가드를 뮤테이션으로 검증하는 과정에서, 챌린저도 저도 놓쳤던 구멍 1건을 찾았습니다 — Rev.1 이 §1.1 에 결함으로 등재한 usage():41--agent claude|agy 과소 표기를, Rev.1·챌린저 양쪽 가드 모두 탐지하지 못합니다(M3). Rev.2 에서 닫았습니다.
항목 Rev.1 Rev.2
§4.2-(2) 세션명 nosuch (오탐 — 3/4 rc=2) test-project-creator-claude
§4.2-(2) 단언 rc != 2 단독 stderr 메시지 단언 + rc != 2 보조
usage() 에이전트 목록 검증 없음 (M3 구멍) 추가
가드 뮤테이션 검증 계획만 제시 3종 실측 완료
나머지(§1~§3, §5, §7) 변경 없음

1. Challenge 판정 — 수용 (실측 재현)

1.1 챌린저 지적의 사실 확인

챌린저가 인용한 블록은 실재합니다. 정확한 위치는 :91-100(챌린저 표기 :92-100), exit 2:98 입니다.

# stop_session.sh:91-100
# --agent 미지정 시 이름 suffix 로 fallback (P1-F)
if [ -z "$AGENT" ]; then
  case "$SESSION_NAME" in
    *-creator-claude|*-planner-claude|*-reviewer-claude) AGENT=claude ;;
    ...
    *) echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2; exit 2 ;;   # :98
  esac
fi

챌린저가 지적한 실행 순서도 정확합니다. YAML 존재 검사는 :80, 에이전트 추론은 :91 이므로 추론이 뒤에 옵니다. 그리고 tests/conftest.py:15-52mam_sandbox 픽스처는 agent-sessions.yaml실제로 생성합니다(herdr_sessions: []). 따라서 :80 은 통과하고 :98 에 도달합니다 — "샌드박스 상태에 따라 결과가 뒤바뀐다"는 챌린저의 우려가 아니라, 결정론적으로 항상 실패합니다.

1.2 실측 — 격리 클론 + 실제 mam_sandbox 픽스처

git clone --local --no-hardlinks 로 만든 클론에 프로브 테스트를 넣어 측정했습니다.

--session 추가 인자 rc stderr 첫 줄
nosuch --reason x 2 cannot infer agent from 'nosuch'
nosuch --purge-conversation 2 cannot infer agent from 'nosuch'
nosuch --yes 2 cannot infer agent from 'nosuch'
nosuch --agent hermes 1 session 'nosuch' not in …yaml
test-project-creator-claude --reason x 1 session … not in …yaml
test-project-creator-claude --purge-conversation 1 session … not in …yaml
test-project-creator-claude --yes 1 session … not in …yaml
test-project-creator-claude --agent hermes 1 session … not in …yaml
test-project-creator-claude --purge-conversation --yes 1 session … not in …yaml

Rev.1 의 assert r.returncode != 2 는 4개 중 3개에서 실패합니다(--agent 를 준 케이스만 추론을 건너뛰어 통과). Challenge 확정.

부수 확인: Rev.1 §9 한계에서 "--purge-conversation--yes 없이 호출 시 rc=1 인지 rc=3 인지 구현 시 실측 필요"라고 남겼던 항목도 해소되었습니다 — rc=1(레지스트리 조회가 확인 프롬프트보다 먼저)입니다.


2. 챌린저 권고안 평가 — 채택하되 보강

2.1 권고안은 작동합니다

valid_session = "test-project-creator-claude"*-creator-claude 에 접미사 매칭되어 AGENT=claude 로 추론되고, 4개 케이스 전부 rc=1 로 끝납니다(위 표 하단 5행). 측정으로 확인했습니다.

2.2 그러나 근본 원인은 세션명이 아니라 rc=2 의 과부하입니다

stop_session.sh 에서 exit 25곳에서 발생합니다.

원인
:67 폐지 플래그(--mode/--capture-id/--graceful)
:70 unknown arg
:76 invalid agent type
:79 --session 누락
:98 cannot infer agent ← 이번 오탐의 원인

가드가 검증하려는 것은 오직 :70 하나("도움말이 광고하는 플래그를 파서가 unknown 으로 튕기지 않는다")인데, rc != 2 는 나머지 4개와 구별하지 못합니다. 챌린저의 valid_session:98 만 회피할 뿐 :76·:79 는 여전히 구별하지 못하며, 더 나쁘게는 가드를 :91-100에이전트 접미사 명명 규칙에 결합시킵니다. 훗날 역할명이 추가되거나 creator 가 개명되면, C-6 가드가 C-6 과 무관한 이유로 깨지고 실패 메시지도 C-6 을 가리키지 않습니다.

2.3 Rev.2 의 보강 — stderr 메시지 단언

assert "unknown arg" not in r.stderr      # 파서가 이 플래그를 모른다고 하지 않았다
assert "deprecated" not in r.stderr       # 폐지 플래그로 취급하지도 않았다
assert r.returncode != 2                  # (보조) 위 둘을 빠져나간 rc=2 도 없다

이 단언은 5개 원인 중 정확히 검증 대상인 것만 지목합니다. 실측 표에서 확인되듯 nosuch 케이스의 stderr 는 cannot infer agent 이므로 메시지 단언만으로는 세션명이 무엇이든 통과합니다 — 즉 챌린저 권고보다 엄밀히 더 견고합니다.

두 가지를 모두 채택합니다: 챌린저의 valid_session(원인 제거) + 메시지 단언(결합 제거). 어느 한쪽이 미래에 무력화돼도 다른 쪽이 남습니다.


3. 🆕 Rev.2 신규 발견 — 가드가 usage():41 결함을 놓침 (M3)

확정 가드를 뮤테이션 검증하던 중 발견했습니다. 챌린저도 Rev.1 도 지적하지 못한 구멍입니다.

Rev.1 §1.1 은 usage():41[--agent claude|agy] 가 검증기(:74-77)의 4종 수용과 어긋난다고 결함으로 등재했습니다. 그런데 Rev.1·챌린저 양쪽 가드 모두 이 결함을 탐지하지 못합니다.

뮤테이션 M3: 수정된 클론에서 usage() 의 에이전트 목록만 claude|agy 로 되돌림

결과: 1 passed     ← 가드가 통과시킴 ❌

C-6 이 고치기로 한 결함 중 하나가 가드 밖에 있었던 셈입니다. Rev.2 에서 다음 3줄로 닫았습니다.

for agent in ("claude", "agy", "hermes", "cline"):
    assert agent in res.stdout, f"usage() omits supported agent {agent}"

재검증: 강화 후 baseline 1 passed, M3 재적용 시 1 failed. 구멍이 닫혔음을 실측했습니다.


4. 확정 회귀 가드

4.1 설계 원칙 (Rev.1 §4.1 유지)

직전 리뷰 31730364 에서 뮤테이션으로 드러난 실패 사례 — test_delegate_agent_resolution_and_fallback 이 테스트 파일 안에 case 문을 복사해 실행한 탓에 생산 코드 결함을 완전히 되돌려도 통과 — 를 반복하지 않도록, 가드는 stop_session.sh직접 실행하고 그 파일을 직접 읽습니다.

4.2 확정 코드 — tests/test_tier2_component.py 에 추가

def test_comp_stop_usage_matches_parser(mam_sandbox):
    """C-6: help text and parser must not drift apart."""
    script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"

    # 에이전트 접미사 추론(:91-100)이 성립하는 이름 — rc=2 의 다섯 원인 중
    # 'cannot infer agent'(:98)를 배제하기 위함 (Challenge 8b6b574f)
    VALID = "test-project-creator-claude"

    # 1) --help 는 성공하고, 폐지된 플래그를 광고하지 않는다
    res = subprocess.run(["bash", str(script), "--help"], capture_output=True, text=True)
    assert res.returncode == 0
    for dead in ("--mode", "--capture-id", "--graceful"):
        assert dead not in res.stdout, f"usage() still advertises {dead}"

    # 1b) 검증기가 받는 에이전트는 전부 도움말에 나온다 (Rev.2 M3)
    for agent in ("claude", "agy", "hermes", "cline"):
        assert agent in res.stdout, f"usage() omits supported agent {agent}"

    # 2) 도움말이 광고하는 플래그는 전부 파서가 받는다
    #    rc=2 는 5가지 원인을 공유하므로 stderr 메시지로 직접 지목한다
    for flag, args in (("--reason", ["--reason", "x"]),
                       ("--purge-conversation", ["--purge-conversation"]),
                       ("--yes", ["--yes"]),
                       ("--agent", ["--agent", "hermes"])):
        r = subprocess.run(["bash", str(script), "--session", VALID] + args,
                           capture_output=True, text=True)
        assert "unknown arg" not in r.stderr, f"usage() advertises {flag} but parser rejects it: {r.stderr}"
        assert "deprecated" not in r.stderr, f"usage() advertises deprecated {flag}: {r.stderr}"
        assert r.returncode != 2, f"{flag} -> rc=2: {r.stderr}"

    # 3) 폐지된 플래그는 전용 메시지와 함께 rc=2 로 거부된다 (특별 취급 유지)
    for dead in ("--mode", "--capture-id", "--graceful"):
        r = subprocess.run(["bash", str(script), "--session", VALID, dead, "hard"],
                           capture_output=True, text=True)
        assert r.returncode == 2
        assert "deprecated" in r.stderr

    # 4) 헤더 주석도 폐지 플래그를 사용법으로 광고하지 않는다
    head = "".join(script.read_text().splitlines(keepends=True)[:35])
    assert "--mode soft|hard" not in head

4.3 뮤테이션 검증 — Rev.2 에서 실측 완료

Rev.1 은 뮤테이션을 "구현자 필수 수행"으로 지시만 했으나, Rev.2 는 계획 단계에서 직접 수행했습니다. 격리 클론에 §3 단계 1~2 의 문서 수정을 적용한 뒤:

# 뮤테이션 기대 실측
(baseline, 수정 적용 상태) PASS 1 passed
M1 파서에서 --reason) 분기 삭제 (도움말은 계속 광고) FAIL 1 failed:19 unknown arg 단언
M2 헤더에 [--mode soft|hard] 행 복원 FAIL 1 failed:30 헤더 단언
M3 usage() 에이전트 목록을 claude|agy 로 축소 FAIL 강화 전 1 passed → 강화 후 1 failed

M1 이 가드의 핵심 가치를 증명합니다 — 도움말과 파서 중 한쪽만 바뀌면 즉시 실패하며, 이것이 C-6 을 애초에 만든 드리프트입니다.

구현자는 위 표를 재현만 하면 됩니다(신규 설계 불필요).


5. 구현 계획 (Rev.1 대비 변경 없음)

단계 1 — 헤더 주석 교체 (:2-30, 29줄)

# stop_session.sh — multi-agent-mux-stop 의 부속 스크립트
# Usage:
#   bash stop_session.sh --session <name> [--agent claude|agy|hermes|cline] \
#       [--reason <reason>] [--purge-conversation] [--yes]
#
# 동작: 항상 graceful stop 입니다. send-keys 로 정상 종료를 유도하고
#       (미종료 시 SIGTERM → SIGKILL 폴백), kill 직전에 이 워크스페이스의
#       conversation id 를 row 에 확정 기록해 다음 resume 이 tier-1(race-free)
#       으로 복원되게 합니다. status 는 running -> stopped 로 전이합니다.
#       멱등: 이미 stopped 면 no-op + exit 0.
#
# 옵션:
#   --session <name>        — 대상 세션 (필수)
#   --agent <type>          — claude | agy | hermes | cline
#                             (미지정 시 세션명 접미사로 추론; 추론 실패 시 exit 2)
#   --reason <reason>       — 상태 전이 사유 (stop_reason). 기본값 manual_stop
#   --purge-conversation    — 디스크의 conversation artifact 까지 삭제.
#                             status=terminated, resumable=false 로 전이하며
#                             resume 불가. --yes 없이는 확인 프롬프트(exit 3)
#   --yes                   — --purge-conversation 의 확인 프롬프트 생략
#
# 폐지된 옵션: --mode / --capture-id / --graceful 는 각각 exit 2 로 거부됩니다.
#              graceful 종료와 id 캡처는 이제 무조건 수행되며, soft/hard 모드
#              구분은 --purge-conversation 유무로 대체되었습니다.
#
# Exit codes:
#   0 = success (or already-stopped no-op) | 1 = YAML not found / not registered
#   2 = invalid args | 3 = interactive confirmation required (--yes 누락)
#   4 = purge aborted (herdr session survived the kill chain)

Rev.2 추가: --agent 항목에 접미사 추론 동작(:91-100)을 한 줄 명기합니다. Challenge 가 드러냈듯 이 동작은 문서화되어 있지 않아 계획자·리뷰어 양쪽이 놓쳤던 부분입니다. C-6 의 취지("문서가 실제 동작과 일치할 것")에 정확히 부합합니다.

단계 2 — usage() 보강 (:39-47)

usage() {
  cat <<EOF
Usage: $0 --session <name> [--agent claude|agy|hermes|cline] [--reason <reason>]
          [--purge-conversation] [--yes]

Arguments:
  --session <name>     — target session name (required)
  --agent <type>       — claude | agy | hermes | cline
                         (inferred from the session-name suffix when omitted)
  --reason <reason>    — stop_reason field (default: manual_stop)
  --purge-conversation — also delete on-disk conversation artifacts;
                         status becomes terminated and resume is impossible
  --yes                — skip the --purge-conversation confirmation prompt

Stop is always graceful and always captures the conversation id.
(idempotent: stopping an already-stopped session is a no-op with exit 0)
EOF
}

단계 3 — 내부 주석 3곳 + 경고 문자열 1곳

위치 조치
:157 # --capture-id: kill 직전에 …# 캡처: kill 직전에 …
:166 WARN: --capture-id requested but no conversation id resolvedWARN: no conversation id resolved before stop (nothing on disk yet)
:172 # --graceful: send-keys 로 …# graceful 종료: send-keys 로 …
:257 # --capture-id: 항상 captured UUID 기록# 항상 captured UUID 기록 (purge 가 아닐 때만)

단계 4 — MESSAGING.md:346-348

| `stopped`    | stopped via `multi-agent-mux-stop` (default); conversation preserved for resume | `stop` |
| `terminated` | stopped with `--purge-conversation`, or herdr-dead detected; conversation deleted / session gone | `stop --purge-conversation`, `monitor` reconcile |
| `archived`   | legacy value — no producer since `--mode soft` was removed; kept in the validation whitelist for rows written by older versions | (none) |

6. 문서 동기화 (Rev.1 대비 변경 없음)

6.1 IMPROVEMENTS.md — 7곳

현재 변경 후
:3 최종 갱신일 2026-08-16 (P3-1/A-4 …) 날짜·사유에 C-6 완료 반영
:5 미해결 6건 (… 레거시 1) 미해결 5건 (… 레거시 0)
:6 완료 19건 완료 20건, 목록에 C-6 추가
:107 ## 4. … (Legacy Remnants — 1건) … (Legacy Remnants — 0건 — 전원 완료) (:103 §3 표기법과 동일)
:109-110 C-6 항목 삭제 (§5 로 이동)
:114 ## 5. … (Completed Tasks — 19건) … (Completed Tasks — 20건)
:253 | **P2-3** | **C-6** | 도움말 3줄 정정 | 극소 | — | … **(✅ 완료 — 가드 신설, 전체 263/263 PASS)** |

§5 신규 항목:

### **C-6 (P2-3): `stop_session.sh` 레거시 주석 및 구버전 사용법 정리** — ✅ 완료
- 헤더 주석이 광고하던 `--mode soft|hard` / `--capture-id` / `--graceful` 3종은 파서가 `exit 2` 로
  거부하는 폐지 플래그였습니다. 헤더 29줄을 현재 CLI 에 맞게 교체하고, `usage()` 에 누락돼 있던
  옵션 설명과 `--agent` 접미사 추론 동작을 보강했으며, Option B 이후 무의미해진 "워크스페이스에
  격리된" 표현과 내부 주석 3곳의 플래그 표기를 정리했습니다.
- `MESSAGING.md` 상태 표가 제거된 플래그로 `stopped`/`terminated` 를 정의하던 것을 교정하고,
  생산자가 사라진 `archived` 를 레거시 값으로 명기했습니다.
- 도움말과 파서의 일치를 강제하는 회귀 가드를 신설하고 뮤테이션 3종(M1~M3)으로 방어력을
  검증했습니다 — C-6 은 문서 과제라 기존 테스트가 전혀 잡지 못하던 영역입니다.

주의: :5 의 "레거시 잔재 0건"과 :107 §4 헤더는 반드시 함께 바꿉니다. 직전 3라운드 리뷰에서 이 쌍의 불일치가 매번 지적되었습니다.

6.2 LOG.md

## 📌 1. 금일 작업 내용 요약 아래 기존 ### 1) P3-1 … 앞에 신규 항목을 삽입하고 기존 P3-1 을 ### 2) 로 조정합니다. 머리말 - **최종 기록일시** · - **작업 상태** 도 갱신합니다.

### 1) **C-6 (P2-3): `stop_session.sh` 레거시 주석 및 구버전 사용법 정리** — **완료**
- **배경**: 헤더 주석이 폐지 플래그 3종을 사용법으로 광고했으나 파서는 전용 메시지와 함께
  `exit 2` 로 거부하고 있었음(실측). 백로그에는 "도움말 3줄"로 등재돼 있었으나 실제 대상은
  헤더 29줄 + `usage()` + 내부 주석 3곳 + `MESSAGING.md` 상태 표였음.
- **주요 구현**: (파일별 변경 요약)
- **검증**: `pytest` 263/263 PASS. 신규 가드에 대해 뮤테이션 M1~M3 전부 FAIL 확인.

7. archived 사문 상태값 — Option A 확정

Rev.1 §7 에서 판단을 요청했고 챌린저가 §4-3 에서 Option A 에 전적으로 동의했으므로 확정합니다.

  • A. 현상 유지 + 문서 명기atomic_yaml.py:18 화이트리스트와 reconcile.sh:474 관용 목록은 손대지 않고, MESSAGING.md 에 "레거시 값, 현재 생산자 없음"을 명기 (§5 단계 4 에 반영 완료).
  • B(완전 은퇴)는 기존 데이터에 archived 행이 있으면 검증 실패로 전체 쓰기가 막히므로 마이그레이션이 필요합니다 — C-6("극소") 범위를 벗어납니다.

MESSAGING.md 를 C-6 범위에 포함하는 것도 챌린저가 §4-2 에서 동의했으므로 확정합니다.


8. 검증 절차

# 명령 / 확인 기대
1 bash -n .../stop_session.sh OK
2 bash stop_session.sh --help; echo $? rc=0, 폐지 플래그 미노출, 4개 에이전트 전부 노출
3 --mode / --capture-id / --graceful rc=2 + deprecated 메시지 유지
4 --agent bogus rc=2 (invalid agent type)
5 grep -rn -- "--mode soft" .agents/ *.md 0건
6 뮤테이션 M1 — 파서에서 --reason) 삭제 가드 FAIL
7 뮤테이션 M2 — 헤더에 --mode soft|hard 복원 가드 FAIL
8 뮤테이션 M3usage() 에이전트 목록 축소 가드 FAIL
9 pytest tests/ -q 263 passed
10 env -u PYTHONPATH pytest tests/test_tier2_component.py -q 전부 통과 (환경 비의존)
11 IMPROVEMENTS.md :5:107 대조 레거시 카운트 일치
12 IMPROVEMENTS.md :6:114 대조 둘 다 20건

9번은 약 6분 30초 소요됩니다(직전 실측 262 passed / 381.58s). 백그라운드 실행 권장.

10번 근거: 직전 라운드에서 신규 테스트가 주변 셸의 PYTHONPATH 에 의존해 CI 를 적색으로 만든 사례(N1)가 있었습니다. 확정 가드는 subprocess.run(["bash", ...]) 만 쓰므로 해당 위험이 없으나 확인 절차는 유지합니다.


9. 변경 규모 및 리스크

파일 변경
stop_session.sh 헤더 29줄 교체, usage() 약 +10줄, 내부 주석 3곳 + 경고 문자열 1곳
MESSAGING.md 3줄
IMPROVEMENTS.md 7곳 + §5 신규 항목
LOG.md 1개 블록 + 머리말
tests/test_tier2_component.py +1 test
테스트 총계 262 → 263
리스크 평가
동작 회귀 없음. 실행 경로 무변경. 유일한 예외 :166 경고 문자열은 단언하는 테스트 0건 확인
가드 오탐 해소. Challenge 원인(:98)을 세션명으로 제거하고, rc=2 과부하를 메시지 단언으로 우회
가드 무력화 해소. M1~M3 실측으로 방어력 증명
카운트 불일치 재발 §8 의 11·12번으로 차단

권장 커밋 분할

  1. docs(stop): rewrite stop_session.sh header and usage to match the current CLI (C-6) — 단계 1~3
  2. test(stop): guard help text against parser drift (C-6) — §4
  3. docs(messaging,improvements,log): sync status table and backlog for C-6 — 단계 4 + §6

2번을 1번 뒤에 두면, 가드가 1번 없이 실패하고 1번과 함께 통과함을 커밋 순서로 증명할 수 있습니다.


10. 한계

  • 확정 가드는 격리 클론에서 실행 검증했으나, 저장소 본체에는 적용하지 않았습니다(Planner 역할). 클론은 검증 후 삭제했고 작업 트리는 계획 수립 전후 동일(?? VERSIONS.md 1건)합니다.
  • 뮤테이션 M1M3 은 §5 단계 12 의 문서 수정을 클론에 부분 적용한 상태에서 수행했습니다(헤더 --mode 행 삭제 + usage() 확장). 단계 3·4 는 가드 대상이 아니므로 적용하지 않았습니다.
  • 전체 회귀(263)는 재실행하지 않았습니다. 262 passed / 381.58s 가 유효 기준이며 HEAD 가 5ed39f8 로 진행되었으므로 구현 시 재측정이 필요합니다.
  • MESSAGING.md 는 폐지 플래그 3종 검색으로 걸린 3줄만 확인했고 나머지는 감사하지 않았습니다.
  • :91-100 접미사 추론의 역할 목록(creator/planner/reviewer)이 실제 사용되는 역할 전부를 덮는지는 확인하지 않았습니다. C-6 범위 밖이며, 가드는 이 목록에 의존하지 않도록(§2.3) 설계했습니다.