22 KiB
P2-2 (C-3a + C-4) 레거시 코드 정리 — 구현 계획서 Rev.2
- Job:
84300b64· Role: Planner (MULTI_AGENT_RULES.md§1 — 저장소 코드 미수정) - 선행 계획:
07740cc7(Rev.1) · 반영 챌린지:e63843ca(agy,[VERDICT: PASS WITH CHALLENGE]) - 기준 커밋:
5e519e2(Rev.1 과 동일 — HEAD 이동 없음, §3.3 참조) · 작성 시각: 2026-08-16 - 산출물: 본 계획서 +
proposed/p2-2_c3a_c4.rev2.patch+proposed/test_legacy_isolate_flags_noop.py+proposed/pytest_rev2.log - 검증 방식:
git clone --local --no-hardlinks로 만든 스크래치패드 사본에 패치를 적용해 전체 스위트 + 변이 검사(mutation check)를 실행했습니다. 본 저장소 워킹 트리는 계획 수립 전후 모두 clean 입니다.
0. 챌린지 판정 요약
| # | 챌린지 | 판정 | 근거 |
|---|---|---|---|
| 1 | --isolate/--no-isolate 자동화 회귀 테스트 부재 |
✅ 수용 + 강화 | 제시된 테스트를 그대로 실행 → 통과(0.09s). 변이 4종 중 3종 검출. 나머지 1종(usage 문서 줄 삭제)을 잡도록 assert 1줄 추가 |
| 2 | test_tier1_unit.py:31 섹션 헤더 (7 Test Cases) 동기화 |
✅ 수용 | 현재 5개 헤더 전부 정확(7/6/5/5/6 = 29 = 실측)함을 확인. 방치하면 이 파일 최초의 불일치가 됨. (5 Test Cases) 로 갱신 |
| 3 | IMPROVEMENTS.md 라인 번호를 최신 HEAD 로 동기화 |
⚖️ 사실관계는 반박, 우려는 수용 | HEAD 는 5e519e2 로 이동하지 않았고 Rev.1 의 20개 인용 라인은 전부 현행 일치. 챌린지의 "문두 완료 15건" 은 실측 16건. 다만 §6.1 편집들이 서로의 오프셋을 밀어내는 문제는 실재하므로 편집 순서 명세를 신설(§4.3) |
Rev.1 대비 순증분: 테스트 1건 추가(순감 4 → 순감 3), 섹션 헤더 1줄, 편집 순서 명세 1개 절. 수집 개수 259 → 256.
1. Challenge 1 검증 — 수용, 그리고 한 줄 강화
1.1 제안된 테스트를 그대로 실행
챌린저가 제시한 코드를 한 글자도 고치지 않고 패치된 사본에 넣어 실행했습니다.
1 passed in 0.13s
0.09s call test_create_session_legacy_isolate_flags_noop
0.02s setup
동작합니다. 다만 실측 0.09s 로, 챌린지가 적은 <0.05s 보다 약 2배입니다. 원인은 create_session.sh:25 가 인자 파싱 이전에 source "$_lib_sh" 를 하기 때문이며(플래그 2개 × 서브프로세스 2회), 절대값이 미미하므로 채택에는 영향이 없습니다. 계획에는 실측값으로 적습니다.
1.2 변이 검사 — 이 테스트가 실제로 무엇을 잡는가
"통과한다" 는 것만으로는 가드가 되지 못하므로, 이 테스트가 막으려는 회귀를 직접 주입해 실패하는지 확인했습니다.
| 변이 | 내용 | 챌린지 원안 | 강화안 |
|---|---|---|---|
| A | --isolate · --no-isolate 분기 둘 다 삭제 |
✅ FAIL (rc=2, ERROR: unknown arg: --isolate) |
✅ FAIL |
| B | --no-isolate 한쪽만 삭제 |
✅ FAIL (ERROR: unknown arg: --no-isolate) |
✅ FAIL |
| C | 분기는 두되 echo 를 지워 조용한 no-op 으로 |
✅ FAIL (stderr assert) | ✅ FAIL |
| D | 분기는 두되 usage() 의 문서 줄(:42-43) 삭제 |
❌ PASS (놓침) | ✅ FAIL |
| E | 무변이 대조군 | ✅ PASS | ✅ PASS |
변이 A/B/C 를 잡는다는 점에서 챌린지의 지적은 정확하고 실효적입니다. 특히 B(한쪽만 삭제)를 잡는 것은 for flag in [...] 루프 덕분이며, 원안 설계가 이미 이 경우를 고려했음을 보여줍니다.
D 만 빠져나갑니다. --isolate/--no-isolate 는 create_session.sh:42-43 에서 usage 에 정식 문서화되어 있는 옵션입니다. 챌린지가 지목한 "누군가 미사용으로 오판하여 삭제" 시나리오에서, 가장 먼저 지워질 후보는 실행 분기가 아니라 도움말 줄입니다(C-6 이 정확히 "도움말과 실제 파서의 불일치" 과제인 점을 상기하십시오). 그리고 -h 를 이미 실행하고 있으므로 그 출력은 이미 res.stdout 에 잡혀 있습니다 — 서브프로세스 추가 없이 assert 한 줄이면 닫힙니다.
1.3 채택 최종본
def test_create_session_legacy_isolate_flags_noop(mam_sandbox):
"""Legacy --isolate/--no-isolate must stay a documented no-op, not an arg-parser error."""
create_script = mam_sandbox / "skills" / "multi-agent-mux-create" / "scripts" / "create_session.sh"
for flag in ["--isolate", "--no-isolate"]:
res = subprocess.run(["bash", str(create_script), flag, "-h"], capture_output=True, text=True)
assert res.returncode == 0, f"{flag} rejected by arg parser: {res.stderr}"
assert "NOTE: --isolate/--no-isolate is a no-op" in res.stderr
assert flag in res.stdout, f"{flag} missing from usage() help text"
원안 대비 변경은 3줄입니다.
assert flag in res.stdout신설 — 변이 D 를 닫습니다. 부분 문자열 오탐 우려가 있어 확인했으나"--isolate" in "--no-isolate"는False입니다(--no-isolate는--no다음에 하이픈이 하나뿐이므로--isolate를 부분 문자열로 포함하지 않음). 따라서 단순in으로 두 플래그가 모호함 없이 구분됩니다.assert res.returncode == 0에 실패 메시지 추가 — 실패 시assert 2 == 0대신 어느 플래그가 왜 거부됐는지 즉시 보이게 합니다(루프라서 어느 회차인지 모호해집니다).- docstring 을 계약 문장으로 교체 — "documented no-op" 이 assert 3개의 의도를 그대로 서술합니다.
1.4 배치 결정 — test_tier1_unit.py FEATURE 1
챌린지의 제안대로 tier1 에 둡니다. 스크립트를 실행하는 테스트라 tier2 도 후보였으나, 동일 파일에 정확한 선례가 있습니다:
def test_resume_script_invalid_args(mam_sandbox): # tier1:114 (현행)
script_path = mam_sandbox / "skills" / "multi-agent-mux-resume" / "scripts" / "resolve_session_id.sh"
res = subprocess.run(["bash", str(script_path), ...], capture_output=True, text=True)
assert res.returncode == 2
assert "ERROR: --agent required" in res.stderr
mam_sandbox / "skills" / ... 경로 관례, subprocess.run, rc + stderr assert — 신규 테스트가 이 관용구를 그대로 따릅니다. tier1 은 이미 인자 파서 단위 테스트의 자리입니다. subprocess 는 tests/test_tier1_unit.py:2 에서 이미 임포트되어 있어 추가 임포트도 없습니다.
삭제되는 3건이 있던 바로 그 자리(test_create_derive_session_name_weird_characters 와 test_create_validate_env_key 사이)에 넣습니다.
1.5 격리 검증 — 신규 테스트는 저장소를 오염시키지 않는가
이 테스트는 create_session.sh 를 실행하고, 그 스크립트는 :25 에서 lib.sh 를 source 하며, lib.sh 는 _init_herdr_isolation 으로 $WORKSPACE_ROOT/.mam/shim/herdr 를 씁니다. 실제로 쓰기가 일어나는 테스트이므로 확인했습니다.
rm -rf <clone>/.mam
pytest ...::test_create_session_legacy_isolate_flags_noop → 1 passed
after run, .mam exists? NO
conftest.py:44 의 monkeypatch.setenv("WORKSPACE_ROOT", str(tmp_path)) 가 서브프로세스까지 상속되어 쓰기가 tmp_path 안에 갇힙니다. 저장소 트리에 흔적 0건.
(참고: 전체 스위트를 돌리면 사본에 .mam/shim/ 이 생깁니다. 이는 다른 기존 테스트들이 만드는 것으로 P2-2 이전부터의 성질이며 .gitignore:14 대상입니다. 신규 테스트가 원인이 아님을 위 실험이 분리해 보여 줍니다.)
1.6 이 테스트가 여전히 잡지 못하는 것 (명시)
create_session.sh본문의 동작(세션 생성 자체)은 검증하지 않습니다.-h로 조기 종료하므로 파서 진입 지점까지만 봅니다. 이는 의도된 범위입니다 — 챌린지가 요구한 것은 "인자 파서 게이트" 입니다.- 다른 레거시 no-op 플래그가 생기면 이 테스트는 자동으로 커버하지 않습니다.
for flag in [...]목록에 추가해야 합니다.
2. Challenge 2 검증 — 수용, 범위 명확화
tests/test_tier1_unit.py:31 의 # FEATURE 1: Create Session (7 Test Cases) 를 갱신하라는 지적입니다. 파일 전체의 헤더 정합성을 실측했습니다.
| 헤더 라인 | 섹션 | 선언 | 실측 |
|---|---|---|---|
| 31 | FEATURE 1: Create Session | 7 | 7 ✅ |
| 106 | FEATURE 2: Resume Session | 6 | 6 ✅ |
| 152 | FEATURE 3: Stop Session | 5 | 5 ✅ |
| 197 | FEATURE 4: Status Query | 5 | 5 ✅ |
| 283 | FEATURE 5: Monitor/Reconcile | 6 | 6 ✅ |
| 합계 | 29 | 29 (grep -c "^def test_" = 29) ✅ |
5개 헤더 전부 현재 정확합니다. 이 파일은 메타데이터를 성실하게 유지해 온 파일이고, 따라서 (7 Test Cases) 를 방치하면 그것이 이 파일 최초의 불일치가 됩니다. 챌린지 판단이 옳습니다.
갱신값은 (5 Test Cases) 입니다 — 7 − 3(삭제) + 1(신규) = 5. 다른 4개 헤더는 손대지 않습니다(변동 없음).
패치 적용 후 재실측:
31 FEATURE 1: Create Session claimed=5 actual=5 OK
77 FEATURE 2: Resume Session claimed=6 actual=6 OK
123 FEATURE 3: Stop Session claimed=5 actual=5 OK
168 FEATURE 4: Status Query claimed=5 actual=5 OK
254 FEATURE 5: Monitor/Reconcile claimed=6 actual=6 OK
file total: 27
tests/test_tier2_component.py 에는 이런 개수 선언 헤더가 없으므로 해당 파일은 추가 조치 불필요합니다.
3. Challenge 3 판정 — 사실관계 반박, 우려는 §4.3 으로 수용
3.1 HEAD 는 이동하지 않았습니다
$ git rev-parse --short HEAD
5e519e2
$ git log --oneline -1
5e519e2 docs(improvements): synchronize header counts and roadmap table with completed P2-1 task
Rev.1 의 기준 커밋이 5e519e2 이고 현재 HEAD 도 5e519e2 입니다. 챌린지가 지목한 b490713(P2-1 수정)은 4 커밋 이전이며, 그 이후의 af3dc16 → a875b13 → 5e519e2 가 전부 문서 커밋입니다. 그중 5e519e2 는 커밋 제목 그대로 "헤더 개수와 로드맵 표를 P2-1 완료와 동기화" 한 커밋 — 즉 챌린지가 요구하는 동기화는 Rev.1 작성 시점에 이미 반영된 상태였습니다.
3.2 Rev.1 의 인용 라인 20개 전수 재검증
챌린지를 계기로 §6.1·§6.3 이 인용한 모든 라인을 다시 대조했습니다.
| 인용 | 현행 내용 | 판정 |
|---|---|---|
:5 |
총 추적 미해결 과제: 9건 (아키텍처 2, 엣지케이스 4, 오케스트레이션 0, 레거시 잔재 3) |
✅ |
:6 |
완료된 과제: **16건** (A-1 … P2-1-DelegateJobSafe-TrapFix) |
✅ |
:70 |
## 2. 엣지 케이스 및 런타임 버그 (Edge-case Bugs — 5건) |
✅ |
:107 |
## 4. 레거시 잔재 및 죽은 코드 (Legacy Remnants — 3건) |
✅ |
:109-111 |
C-3 제목 / C-3a / C-3b | ✅ |
:113-116 |
C-4 제목 / 실제 대상 3종 / 목록 제외 / provision_isolation 중복 | ✅ |
:123 |
## 5. 완료된 과제 (Completed Tasks — 13건) |
✅ |
:249 |
로드맵 P2-2 행 ("공허한 테스트 5건") | ✅ |
:260 |
"정리(C 계열)를 P2 에 두는 이유" | ✅ |
:317 :319-322 |
§6.5-1 / §6.5-2 | ✅ |
:328 |
§6.6 결론 ("총 12건") | ✅ |
20/20 일치. 오프셋 충돌은 발생하지 않습니다.
3.3 챌린지의 수치 주장은 사실과 다릅니다
챌린지 §Challenge 3 은 "완료 과제 개수도 13건(문두 완료 15건)으로 갱신되었습니다" 라고 적었습니다. 실측:
:6 - **완료된 과제**: **16건** (A-1, A-3, A-5, B-1, B-3, B-4, B-7, B-8, C-1, C-2,
O-1, O-2, O-3, O-4-OrcOnboard,
Herdr-0.8.0-Compat-SanitizeHash, P2-1-DelegateJobSafe-TrapFix)
쉼표 구분 항목 수 = 16개, 선언값 = 16건. 문두는 15가 아니라 16이며 목록과 자체 정합합니다. Rev.1 §6.1 의 "16건 → 17건" 이 맞습니다.
한편 챌린지가 같은 문장에서 언급한 "C-3/C-4 섹션의 시작 위치가 IMPROVEMENTS.md:107" 은 Rev.1 §6.1 이 이미 :107 로 적고 있는 값과 동일합니다 — 이 대목은 정정이 아니라 Rev.1 의 확인입니다.
3.4 그럼에도 수용하는 부분 — 편집 상호 간섭
챌린지가 우려한 "오프셋 충돌" 은 HEAD 대비로는 존재하지 않지만, 편집 도중에는 실재합니다. §6.1 의 지시 11개가 전부 같은 파일을 대상으로 하고, 그중 3개가 줄 수를 바꿉니다:
:113-116C-4 블록 삭제 (−4줄) → 이후 모든 라인 상향 이동:123직후 P2-2 완료 항목 삽입 (+16줄) → 이후 모든 라인 하향 이동:109-111C-3 축소 (줄 수 변동 가능)
따라서 구현자가 :5 → :328 순으로 위에서 아래로 편집하면 :249 이후의 라인 번호가 전부 어긋납니다. 이것이 챌린지가 감지한 실제 위험이며, 해법은 "HEAD 동기화" 가 아니라 편집 순서 규정입니다. §4.3 에 신설했습니다.
4. Rev.1 대비 변경 명세
Rev.1(
07740cc7)의 §1~§4(실측·경계·위험), §7.1 게이트, §8 비용·효과 정정, §9 예상 지적은 전부 유효하며 변경 없습니다. 아래는 델타만 기술합니다.
4.1 S5 개정 — 테스트 4건 제거 → 4건 제거 + 1건 추가 + 헤더 1줄
tests/test_tier1_unit.py
:31 "(7 Test Cases)" → "(5 Test Cases)" [Challenge 2]
:52-88 test_create_isolation_lever
test_create_isolation_env_prefix 삭제
test_create_isolation_cmd_args
같은 자리 test_create_session_legacy_isolate_flags_noop 신설 [Challenge 1]
tests/test_tier2_component.py
:99-107 test_comp_create_isolation_folder_setup 삭제
패치 전체(proposed/p2-2_c3a_c4.rev2.patch): 5 files, +14 / −72. Rev.1 은 +5/−72 였습니다.
4.2 §7.2 개정 — 수동 스모크 항목 정리
Rev.1 §7.2 의 3개 요구 중 3번(--isolate/--no-isolate 각 1회 수동 실행)은 자동화되었으므로 삭제합니다. 이것이 Challenge 1 의 핵심 성과입니다 — 수동 절차가 CI 게이트로 승격되었습니다.
구현자가 여전히 직접 해야 할 것:
pytest tests/ -q재실행 — 사본에는.mam/(gitignore)이 없습니다. 256 passed 재현 확인.create_session.sh실경로 스모크 1회 (--dry-run가능) —ISOLATE제거가 파서 본류에 영향 없음을 실행으로 확인. (신규 테스트는-h조기 종료 경로까지만 봅니다 — §1.6)
4.3 §6.1 신설 — 편집 순서 (Challenge 3 수용)
IMPROVEMENTS.md 의 11개 지시는 반드시 아래 순서(= 라인 번호 내림차순)로 적용하십시오. 그러면 앞선 편집이 뒤이을 편집의 라인 번호를 바꾸지 않습니다.
| 순 | 대상 | 작업 | 줄 수 변화 |
|---|---|---|---|
| 1 | :319-322 §6.5-2 |
C-4 완료 표기. :320 의 lib.sh:57→:79 를 lib.sh:83→:105 로 정정 |
±0 |
| 2 | :317 §6.5-1 |
C-3a 완료 표기. 총계 표현 있으면 "4건" | ±0 |
| 3 | :260 |
근거 문장 교체 (Rev.1 §8) | ±0 |
| 4 | :249 로드맵 행 |
"5건"→"4건", (✅ 완료 — 256/256 PASS) |
±0 |
| 5 | :123 직후 |
§5 최상단에 P2-2 완료 항목 삽입 (§4.4) | +16 |
| 6 | :123 §5 제목 |
항목 수 갱신 | ±0 |
| 7 | :113-116 C-4 블록 |
§4 에서 삭제 (내용은 5번에서 이미 §5 로 이관) | −4 |
| 8 | :109-111 C-3 |
제목을 C-3b: isolation.root 소비자 처분 (보류 — A-4 M2) 으로 축소, C-3a 줄 제거 |
−1 내외 |
| 9 | :107 §4 제목 |
Legacy Remnants — 3건 → 2건 |
±0 |
| 10 | :6 |
완료 16건 → 17건, 목록에 P2-2-C3a-C4-LegacyCleanup 추가 |
±0 |
| 11 | :5 |
미해결 9건 → 8건, 레거시 잔재 3건 → 2건 |
±0 |
대안 (권장): 라인 번호 대신 고유 문자열 앵커로 편집하면 순서 제약이 사라집니다. 위 11개 지시는 모두 유일 문자열을 갖고 있습니다(예: Legacy Remnants — 3건, 공허한 테스트 5건, Completed Tasks — 13건). 도구가 문자열 치환을 지원한다면 그쪽이 안전합니다.
⚠️ Rev.1 §6.3 은 "
:115/:320의 라인 번호를 정정" 하라고 했으나,:115는 7번에서 삭제되는 C-4 블록 안에 있습니다. 따라서 정정 대상은:320하나이며,:115의 내용은 §5 로 이관될 때(§4.4 마지막 항목) 이미 올바른lib.sh:83-84 → :105로 적혀 나갑니다. Rev.2 에서 정정합니다.
4.4 §6.2 개정 — §5 완료 항목 (테스트 문구 수정)
Rev.1 초안에서 두 번째 불릿만 교체합니다.
- 위 스텁의 빈 출력만 재확인하던 공허한 테스트 4건(`tests/test_tier1_unit.py` 3,
`tests/test_tier2_component.py` 1)을 제거하고, 그 자리에 `--isolate`/`--no-isolate`
레거시 no-op 플래그의 인자 파서 계약을 고정하는
`test_create_session_legacy_isolate_flags_noop` 1건을 신설했습니다. 신규 테스트는
분기 삭제·한쪽만 삭제·조용한 no-op 화·usage 문서 줄 삭제 4종 변이를 모두 검출함을
변이 검사로 입증했습니다. `test_tier1_unit.py:31` 섹션 헤더도 `(5 Test Cases)` 로
동기화했습니다.
마지막 불릿의 수치도 갱신합니다: 전체 회귀 256/256 PASS (100%) (259 → 256, 순감 3 = 제거 4 − 신설 1).
4.5 §6.4 개정 — LOG.md
주요 구현 목록의 테스트 줄을 교체하고 검증 수치를 갱신합니다.
- `tests/test_tier1_unit.py` / `tests/test_tier2_component.py`: 공허한 테스트 4건 제거 및
`--isolate`/`--no-isolate` no-op 회귀 가드 1건 신설(변이 4종 검출 입증), 섹션 헤더 동기화.
- **검증**: `pytest tests/ -q` **256 passed (100%)**.
4.6 §3 미접촉 경계 — 한 줄 보강
Rev.1 §3 표의 --isolate/--no-isolate 행 사유를 다음으로 대체합니다.
레거시 호환 경고이자
create_session.sh:42-43에 정식 문서화된 옵션. 제거하면 기존 호출자가unknown arg로exit 2. P2-2 이후로는test_create_session_legacy_isolate_flags_noop이 CI 게이트로 이를 고정한다.
5. Rev.2 검증 결과
| # | 검증 | 기대 | 실측 |
|---|---|---|---|
| V1 | bash -n lib.sh / create_session.sh |
rc=0 | ✅ (Rev.1 에서 확인, 해당 hunk 무변경) |
| V2 | ast.parse(registry.py) |
rc=0 | ✅ (동상) |
| V3 | 신규 테스트 단독 실행 | pass | ✅ 1 passed, 0.09s call |
| V4 | 변이 A (분기 2개 삭제) | FAIL | ✅ FAIL |
| V5 | 변이 B (한쪽만 삭제) | FAIL | ✅ FAIL |
| V6 | 변이 C (조용한 no-op) | FAIL | ✅ FAIL |
| V7 | 변이 D (usage 문서 줄 삭제) | FAIL | ✅ FAIL (강화 후. 원안은 PASS) |
| V8 | 변이 E (무변이 대조군) | PASS | ✅ PASS |
| V9 | 신규 테스트의 저장소 오염 | 0건 | ✅ .mam 미생성 |
| V10 | tier1 섹션 헤더 5개 정합 | 전부 일치 | ✅ 5/5 |
| V11 | 미사용화되는 헬퍼·임포트 | 없음 | ✅ run_lib_func 15회, get_mqtt_common 7회, subprocess/shlex/hmac/hashlib 전부 잔존 사용 |
| V12 | 수집 개수 | 259 → 256 | ✅ 256 collected |
| V13 | pytest tests/ -q 전체 |
256 passed | ✅ 256 passed in 392.29s |
5.1 전체 회귀 (Rev.2 사본)
256 passed in 392.29s (0:06:32)
원본 로그는 proposed/pytest_rev2.log 입니다. 참고로 Rev.1(255건) 은 376.08s 였습니다 — 차이 16s 는 신규 테스트 1건(0.09s)으로 설명되지 않는 실행 간 편차이며, Rev.1 §8 에서 이미 밝혔듯 이 스위트의 총 실행 시간은 P2-2 의 판단 근거가 아닙니다.
6. 검증 한계 (Rev.1 §10 갱신)
- 실측은
5e519e2로컬 클론에서 수행. 실제 트리에서의 256 passed 는 미확인 — §4.2-1 이 요구합니다. create_session.sh본류 실행 스모크 미수행. 신규 테스트는-h조기 종료 경로까지만 검증합니다(§1.6). §4.2-2 가 요구합니다.- 변이 검사는
create_session.sh4종에 한정.lib.sh스텁 제거·registry.py·_REAL_HERDR_PATH에는 변이 검사를 적용하지 않았습니다(제거 대상이라 고정할 계약이 없음 — Rev.1 §4.3). _REAL_HERDR_PATH의 저장소 외부 소비자 미검색. 확인 범위는 저장소 트리, 생성된.mam/shim/herdr,.agents/hooks/,~/.claude/settings.json(Rev.1 §10-4 유지).shellcheck미설치 — 정적 분석은bash -n까지.- macOS · 직렬 실행. Linux ·
pytest-xdist병렬 미검증(xdist 미설치). 신규 테스트는mam_sandbox(tmp_path) 안에서만 쓰기하므로 병렬 안전할 것으로 판단하나 실측은 아닙니다. - 챌린지 §Challenge 3 의 "15건" 반박은
IMPROVEMENTS.md현행 파일 대조에 근거합니다. 챌린저가 다른 시점의 파일을 봤을 가능성은 배제하지 못하나, HEAD 가5e519e2로 고정되어 있고 워킹 트리가 clean 이므로 두 에이전트가 본 파일은 동일해야 합니다. - 본 계획은 Planner 산출물이므로
IMPROVEMENTS.md/LOG.md/ 소스를 직접 수정하지 않았습니다. §4 는 구현자가 적용할 명세입니다.