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

23 KiB
Raw Blame History

📐 구현 계획서 Rev.2 — deploy/* 배포 스크립트 개선 (Job 0d43714b)

  • 작성자: Planner (canary-projects-multi-agent-mux-creator-claude)
  • 개정 사유: Creator agy의 이의제기(Job 029f61b1) 반영
  • 선행 문서: Job 101c90a2 계획서 Rev.1 (본 문서가 이를 대체함 — 구현 시 본 문서만 참조)
  • 기준 커밋: 2ff8b2c (branch main)

0. 이의제기 판정 (Challenge Adjudication)

agy의 지적은 실재하는 데이터 소실 위험을 정확히 짚었고, 채택합니다. 다만 원인 귀속과 처방 두 가지에는 실측으로 반박되는 부분이 있어 수정 채택합니다.

# agy의 주장 / 처방 판정 근거
로컬 커스텀 스킬 코드가 무단 덮어쓰기로 소실될 수 있다 채택 E-6/E-7에서 실제 소실 재현. 잔존 사본 0건
"기존 install.sh는 파일이 있으면 건너뛰어 안전하게 보호되었다" 반박 프레임워크 소유 파일은 이미 cp -f무조건 덮어쓰기(install.sh:166). 보호는 정책이 아니라 fetch 블록이 통째로 스킵된 부작용이었음
"R-1(MAM_REFRESH=1)이 이 위험을 발생시킨다" 반박(부분 채택) 위험은 오늘 이미 존재함 — install.sh -f(문서화된 플래그)와 update.sh 두 경로에서 재현됨. R-1은 원인이 아니라 노출 빈도를 넓히는 요인. 따라서 가드는 R-1의 전제조건으로 필수이되, R-1만 고쳐서는 부족
처방 A: 덮어쓰기 전 .agents/skills/.../*.user-bak 자동 백업 반려 → 대체 .agents/는 §5.1에서 의도적으로 gitignore하지 않기로 결정한 경로. 백업이 사용자 저장소에 추적 파일로 쌓이고, manifest에 없어 remove.sh가 절대 청소하지 못함 → agy 본인이 직전 라운드(c6c43df9)에서 지적한 백업 무한 증식을 재현. .mam/skill-backups/<ts>/로 이전
처방 B: 로컬 변경 감지를 diff/hash/mtime 반려 → 대체 치명적 오설계. 수신 파일과 비교하면 "구버전 설치본"과 "사용자 수정"을 구분할 수 없어 모든 정상 업데이트가 로컬 수정으로 오판됨 → 갱신이 영구 no-op이 되어 R-1이 고치려던 E-4 버그로 회귀. mtime은 cp -f가 매 설치마다 갱신하므로 출처 정보가 아예 없음. 설치기가 마지막에 기록한 해시와 비교해야 함(§2.2)
처방 C: 기본 보존 + --overwrite-custom 플래그 + 안내 문구 채택 안전한 기본값. 다만 "조용히 건너뛰기"는 버전 불일치를 유발하므로 항목별 경고 + 상태 기록을 의무화(§2.4)
(미지적) 처방이 install.sh 복사 루프에만 적용됨 보완 추가 update.shremove.sh --force스킬을 먼저 전부 삭제한 뒤 재설치한다. install.sh에 가드를 넣어도 이 경로에서는 이미 파일이 없어 아무 효과가 없음(E-7). 3개 경로 전부를 덮어야 함(§3)

추가 자기수정(Planner 귀책): Rev.1 §5.2가 gitignore_created.mam/install_state에 기록하도록 했으나, update.sh.mam에서 명시적 allowlist 4종만 스테이징하므로 업데이트 시 이 상태가 소실됩니다(§4). agy가 지적하지 않았지만 같은 계열의 결함이므로 함께 수정합니다.


1. 신규 실측 근거 (Evidence, Rev.2)

임시 워크스페이스에 정상 설치 후 프레임워크 소유 파일 2개(scripts/mqtt_common.py, delegate-job 래퍼)에 로컬 커스터마이즈를 가하고 각 경로를 실행했습니다.

E-6. 오늘의 install.sh -f가 이미 무경고로 파괴함

$ bash install.sh -f "$WORK"
exit=0
  mqtt_common.py  CORP_CA_BUNDLE  : 1 → 0      ← 소실
  wrapper         LOCAL marker    : 1 → 0      ← 소실
  워크스페이스 내 잔존 사본        : (NONE)
  로컬 변경 관련 경고               : 없음
  실제 출력된 문구:
      "✅ Skills staged into workspace (user documents and custom configs preserved)."

마지막 줄이 핵심입니다. 설치기는 커스텀 설정을 보존했다고 명시적으로 안심시키는 문구를 출력하면서 같은 실행에서 커스텀 코드를 파괴합니다. 문구의 원래 의도는 .mam.env·사용자 문서를 가리키지만, 스킬 수정본을 잃은 사용자에게는 경고가 아니라 역방향의 오신호입니다. 즉 agy가 지적한 위험은 "R-1이 도입할 미래의 위험"이 아니라 이미 출시되어 문서화된 플래그에 존재하는 현재의 버그입니다.

E-7. 업데이트 경로는 install.sh 가드로 막을 수 없음

update.sh:151이 실행하는 명령을 그대로 재현했습니다.

$ bash remove.sh --force
exit=0
  .agents/skills/.../mqtt_common.py 존재 : NO — deleted
  잔존 사본                              : (NONE)

remove.sh가 manifest에 따라 .agents/skills/** 28개 파일을 삭제한 뒤에install.sh가 실행됩니다. 그 시점에 로컬 수정본은 이미 존재하지 않으므로, 복사 루프에 어떤 감지 로직을 넣어도 감지할 대상이 없습니다. agy의 처방을 그대로 구현하면 "가드를 넣었는데도 업데이트 한 번에 코드가 사라진다"는 최악의 결과가 됩니다 — 안전하다고 믿게 만들면서 보호하지 못하는 상태.

E-8. 감지 기준의 반증

agy가 제안한 "기존 파일이 원본 템플릿과 다른지" 검사를 그대로 적용하면:

상황 로컬 파일 vs 수신 템플릿 올바른 처리 agy 기준의 판정
사용자가 수정함 다름 보존 보존
구버전이 설치돼 있음(정상 갱신 대상) 다름 덮어쓰기 보존 → 갱신 영구 실패
최신본이 이미 설치됨 같음 no-op no-op

2행이 R-1의 유일한 존재 이유입니다. 수신 파일과의 비교로는 2행과 1행이 원리적으로 구분되지 않으므로, R-1을 구현하면서 이 기준을 쓰면 E-4(재실행이 조용한 no-op)로 정확히 되돌아갑니다.


2. 설계 R-1 — 안전 갱신 (Safe Refresh)

2.1 원칙

갱신은 설치기가 스스로 쓴 것만 덮어쓴다. 그 외 모든 것은 사용자 것으로 간주한다.

.env 마이그레이션에서 확립한 증거 기반 소유 판정 원칙과 동일합니다. 소유 증거는 manifest(경로)만으로 부족하며 내용 지문이 필요합니다.

2.2 소유 지문 대장 — .mam/asset_hashes.txt

설치기가 파일을 쓸 때마다 그 시점의 내용 해시를 기록합니다.

<sha256>  .agents/skills/lib.sh
<sha256>  .agents/skills/multi-agent-mux-delegate-job/scripts/mqtt_common.py
...

갱신 시 3-way 판정:

조건 의미 처리
hash(현재) == 대장의 해시 설치기가 쓴 그대로 (미변경) 덮어쓰기 + 대장 갱신
hash(현재) != 대장의 해시 사용자가 수정함 보존 + 경고 + 상태 기록
대장에 항목 없음 (구 설치본) 출처 불명 §2.5 부트스트랩 규칙
hash(현재) == hash(수신) 이미 최신 no-op (백업·경고 불필요)

대상은 .agents/skills/** 28개 파일(432 KB)뿐이므로 해시 비용은 무시할 수준입니다.

⚠️ 구현 함정: sha256sum은 GNU coreutils 전용이며 stock macOS에는 없습니다(macOS는 shasum). 본 개발 머신에는 coreutils가 설치돼 있어 로컬 테스트는 통과하고 실사용자만 깨지는 전형적 분기가 발생합니다. §2.3의 python3 hashlib 인라인을 사용하십시오 — install.sh가 이미 python3를 하드 의존성으로 검증합니다(Rev.1 §2.3과 동일한 근거).

2.3 백업 위치 — .mam/skill-backups/<UTC타임스탬프>/<원경로>

.user-bak 인플레이스 방식을 쓰지 않는 이유:

  1. .agents/의도적으로 gitignore 대상이 아님(Rev.1 §5.1) → 백업이 사용자 저장소에 추적 파일로 유입.
  2. manifest에 등재되지 않으므로 remove.sh영구히 청소하지 못함 → 언인스톨 후에도 잔재.
  3. 갱신할 때마다 누적 → agy가 직전 라운드에서 정확히 지적한 백업 증식 재현.

.mam/skill-backups/는 ① gitignore 관리 블록의 /.mam/으로 이미 커버 ② remove.shdelete_asset ".mam"으로 자동 정리 ③ 사용자 트리 무오염을 모두 만족합니다.

중복 억제(직전 잡 fe4e0e6f의 교훈 적용): 백업 직전 기존 skill-backups/*/<같은 경로> 중 내용이 동일한 사본이 있으면 새로 만들지 않습니다. cmp 실패 시에는 보존 쪽으로 실패(백업 생성)합니다.

2.4 기본 동작과 플래그

--overwrite-custom          # 로컬 수정본까지 덮어쓴다 (백업은 여전히 남김)
MAM_OVERWRITE_CUSTOM=1      # curl | bash 파이프용 환경변수 동치
  • 기본값 = 보존. agy의 처방 C를 채택합니다.
  • 보존 시 파일 목록을 항목별로 출력해야 합니다. 총계만 찍으면 사용자는 무엇이 낡았는지 알 수 없습니다.
️  Local modifications detected — these files were NOT updated:
      .agents/skills/multi-agent-mux-delegate-job/scripts/mqtt_common.py
      .agents/skills/multi-agent-mux-delegate-job/multi-agent-mux-delegate-job
    Your copies are preserved as-is. Backups: .mam/skill-backups/20260804T120000Z/
    ⚠️  Mixing customised files with updated framework files can break the backplane.
    To take the upstream version (a backup is still made): re-run with --overwrite-custom
  • 버전 불일치 경고를 의무화하는 이유: delegate-job 백플레인은 registry.py·mqtt_common.py·publish_event.py·래퍼가 한 벌로 동작합니다. 한 파일만 구버전으로 남으면 "업데이트 성공"이라 표시된 채 이벤트가 실패하는, 진단이 어려운 상태가 됩니다. 조용한 skip은 금지합니다.
  • .mam/version.txtpreserved_local=<n>을 기록해 사후 진단 가능하게 합니다.

2.5 부트스트랩(대장이 없는 기존 설치본)

기존 설치본에는 asset_hashes.txt가 없습니다. 여기서 "출처 불명 = 보존"을 택하면 설치 기반 전체가 첫 갱신에서 no-op이 되어 R-1이 무력화됩니다. 따라서:

대장 없음 + 내용이 수신본과 다름 → 백업 후 덮어쓰기, 그리고 그 사실을 출력.

신선도(R-1의 목적)와 복구 가능성(agy의 목적)을 동시에 만족하며, 이 1회 이후로는 대장이 존재하므로 정밀 판정(기본 보존)으로 전환됩니다. 내용이 수신본과 같으면 백업도 경고도 만들지 않습니다.

️  No asset fingerprints found (installed by an older version).
    Backing up current skills to .mam/skill-backups/<ts>/ before refresh.

3. 3개 경로 전수 적용 (agy 처방의 결정적 확장)

# 경로 현재 위험 Rev.2 처리
P-A install.sh 갱신(-f 및 R-1 기본 fetch) E-6: 무경고 파괴 §2 안전 갱신 적용
P-B update.sh (문서화된 주 업데이트 수단) E-7: remove.sh가 선삭제 → 가드 무효 remove.sh 호출 전 스냅샷(§3.1)
P-C remove.sh 단독 실행(언인스톨) 수정본이 조용히 삭제됨 항목별 경고 + 조건부 보존(§3.2)

3.1 P-B — update.sh 선스냅샷

update.sh는 이미 .mam.update-tmp 스테이징 구조를 갖고 있으므로 여기에 얹습니다.

1) (remove.sh 호출 전) asset_hashes.txt 기준으로 수정된 프레임워크 파일 산출
2) .mam.update-tmp/skill-backups/<ts>/ 로 복사   ← remove.sh가 지우지 못하는 위치
3) bash "$REMOVER" --force "$TARGET_DIR"
4) 새 install.sh 실행
5) .mam.update-tmp/skill-backups → .mam/skill-backups 로 복원 + 목록 출력

주의: 이 스냅샷은 "수정본을 자동으로 되살리지 않습니다." 업데이트 후 트리에는 최신 프레임워크가 들어가고, 사용자 수정본은 백업으로만 남습니다. 자동 병합은 3-way merge가 필요해 셸 설치기의 책임 범위를 넘습니다. 출력에서 이 점을 명확히 말해야 합니다 — "백업했다"가 "복원했다"로 오해되면 안 됩니다.

💾 3 locally-modified skill file(s) backed up to .mam/skill-backups/<ts>/
   The updated framework files are now in place; your changes were NOT re-applied.
   Diff and re-apply manually if you still need them.

3.2 P-C — remove.sh 언인스톨

  • 삭제 대상 중 로컬 수정 파일이 있을 때만 동작합니다(없으면 출력·디렉토리 생성 모두 없음 → 일반 사용자에게 잔재 0).
  • 대화형: 확인 프롬프트에 수정 파일 목록을 포함해 사용자가 중단할 수 있게 합니다.
  • -y/--force: 목록을 출력하고 <workspace>/.mam-skill-backup.<ts>/로 보존합니다. .mam 내부는 언인스톨 시 삭제되므로 쓸 수 없습니다.
  • --purge-skills 지정 시에만 보존 없이 삭제합니다.

이는 직전 잡(P-1)에서 확립한 "-y(비대화 의도)는 삭제 권한이 아니다" 원칙의 직접 적용이며, 루트에 백업을 남기는 것은 .mam.env.mam-backup이 이미 따르는 기존 전례와 일관됩니다. 관리 블록에 /.mam-skill-backup.*/를 추가합니다(§5).


4. 자기수정 — .mam 잔존 allowlist 결함

update.sh:94-120.mam에서 다음 4종만 스테이징합니다: agent-sessions.*, jobs/, delegate_job_logs/, install_manifest.txt. remove.sh.mam을 통째로 지우므로 여기 없는 것은 업데이트 때마다 소멸합니다.

영향:

파일 도입 위치 소실 시 결과
.mam/install_state (gitignore_created) Rev.1 §5.2 언인스톨 시 .gitignore 소유 판정 불가 → 빈 파일 잔존(안전측 실패, 경미)
.mam/asset_hashes.txt Rev.2 §2.2 업데이트마다 대장 소실 → 매번 부트스트랩 경로 → 정밀 판정이 영구히 동작하지 않음
.mam/version.txt Rev.1 §6.2 버전 이력 단절
.mam/skill-backups/ Rev.2 §2.3 백업이 업데이트로 삭제

조치: update.sh의 스테이징/복원 목록에 위 4개를 추가하고, 나아가 개별 열거 대신 .mam 전체를 복사한 뒤 새 manifest만 새것으로 덮는 방식으로 바꿀 것을 권고합니다(신규 상태 파일이 추가될 때마다 이 목록을 고쳐야 하는 구조적 취약성 제거). 후자를 택할 경우 .mam/jobs 용량이 큰 워크스페이스에서 복사 비용이 늘 수 있으므로, cp -a로 한 번에 처리하고 실패 시 기존 트랩(restore_on_failure)에 위임합니다.


5. Rev.1에서 변경되는 항목 요약

변경
§5.1 관리 블록 /.mam-skill-backup.*/ 1행 추가
§5.2 소유권 기록 .mam/install_state 유지, 단 §4에 따라 update.sh 잔존 목록에 반드시 포함
§6.1 fetch 기본화 §2 안전 갱신 가드 구현이 선행 조건. 가드 없이 기본값만 바꾸는 커밋은 금지
§6.2 버전 스탬프 preserved_local=<n> 필드 추가
§7 커밋 C3 분할 및 C12~C14 추가(§6)
§8 테스트 T-D21~T-D28 추가(§7)
§11 리스크 RK-8 신설, RK-5 완화책 보강

그 외 R-2(필수 마크다운), R-3(.mam_deploy), R-4(.gitignore) 설계와 B-1/B-2/B-3 차단 항목은 Rev.1 그대로 유효합니다. agy도 해당 부분에는 이의를 제기하지 않았습니다.


6. 개정 커밋 분해

# 커밋 대상 비고
C1 feat(deploy): ship only runtime-essential markdown to targets install.sh Rev.1 §3.2
C2 fix(deploy): align install_mam.sh asset excludes with install.sh install_mam.sh Rev.1 §3.3
C3a feat(deploy): record asset fingerprints on install install.sh §2.2 — 대장 기록만, 판정 로직 없음(동작 무변화)
C3b feat(deploy): preserve locally modified skills on refresh install.sh §2.3-2.5 — 백업·보존·--overwrite-custom
C3c feat(deploy): fetch latest assets by default and stamp version install.sh Rev.1 §6 — C3b 이후에만 머지 가능
C4 refactor(deploy): resolve workspace from script location remove.sh, update.sh Rev.1 §4.2
C5 feat(deploy): support .mam_deploy layout in uninstaller/updater remove.sh, update.sh Rev.1 §4.3/4.4
C6 feat(deploy): install remove.sh/update.sh under .mam_deploy/ install.sh C5 이후
C7 feat(deploy): manage a .gitignore block for installed artifacts install.sh Rev.1 §5.1-5.3 + §5
C8 feat(deploy): strip the managed .gitignore block on uninstall remove.sh C7 이후
C9 feat(deploy): unify install_mam.sh gitignore and deploy scripts install_mam.sh Rev.1 §5.4
C12 fix(deploy): preserve .mam state files across the update cycle update.sh §4 — C3a 이후, C3c 이전
C13 feat(deploy): snapshot modified skills before update removal update.sh §3.1
C14 feat(deploy): warn and preserve modified skills on uninstall remove.sh §3.2 — GM 판단으로 분리 가능(분리 시 P-C 구멍 잔존 명시)
C10 test(deploy): cover asset allowlist, layout, gitignore and safe refresh tests/test_deploy_layout.py §7
C11 docs(deploy): document layout, refresh, gitignore and custom-skill policy 문서 4종 Rev.1 §9 + --overwrite-custom·백업 정책

신규 순서 제약 (위반 시 데이터 소실 커밋이 트리에 남음):

  • C3a → C3b → C3c — 대장 없이 판정 로직을 넣으면 전량 부트스트랩 경로로 빠지고, 가드 없이 fetch만 기본화하면 E-6의 파괴를 전 사용자에게 기본값으로 배포하게 됩니다. C3c를 먼저 머지하는 것은 금지입니다.
  • C3a → C12 — 대장을 만들자마자 업데이트가 그것을 지우면 §2.2가 영구히 부트스트랩 모드로 동작합니다.
  • C13은 C3a 이후 — 수정 파일 산출에 대장이 필요합니다.

7. 추가 테스트 (Rev.1 T-D1~T-D20에 이어서)

ID 검증 내용 판정
T-D21 커스터마이즈된 스킬 파일이 있는 상태로 갱신 → 파일 내용 보존, exit 0, 목록이 stdout에 출력됨 B-4
T-D22 미변경 스킬 파일은 정상 갱신됨(E-4 회귀 방지가 T-D21에 의해 무력화되지 않았는지) B-4
T-D23 구버전이 설치된 상태(대장 존재, 사용자 미수정)에서 갱신 시 덮어써짐 — §E-8 2행 오판 방지 B-4
T-D24 --overwrite-custom 시 덮어쓰되 .mam/skill-backups/<ts>/에 사본 존재
T-D25 백업이 .agents/ 하위에 생성되지 않음(find .agents -name '*.user-bak' → 0건)
T-D26 동일 내용 2회 갱신 시 백업 디렉토리 증식하지 않음(중복 억제)
T-D27 update.sh 1회 실행 후 .mam/asset_hashes.txt·install_state·version.txt·skill-backups/모두 잔존 B-5
T-D28 수정본이 있는 상태로 update.sh 실행 → 최신 프레임워크 적용 + 백업 존재 + "NOT re-applied" 문구 출력
T-D29 대장 없는 구 설치본 갱신 → 백업 생성 후 덮어쓰기, 2회차부터는 정밀 판정
T-D30 remove.sh -y (수정본 존재) → .mam-skill-backup.<ts>/ 생성; 수정본 없으면 디렉토리 미생성 C14 채택 시
T-D31 해시 계산이 sha256sum/shasum 존재 여부에 의존하지 않음(PATH에서 둘 다 제거해도 통과)

차단 항목 추가: B-4(안전 갱신 3-way 판정 — 실패 시 사용자 코드 소실 또는 갱신 영구 실패), B-5(.mam 상태 파일 잔존 — 실패 시 B-4가 구조적으로 동작 불능).

차단 항목은 총 5건: B-1, B-2, B-3, B-4, B-5.


8. DoD 게이트 (추가분)

게이트 조건
I. 커스텀 보존 수정된 스킬 파일이 install.sh 갱신·install.sh -f·update.sh 3경로 모두에서 소실되지 않음(원본 보존 또는 백업 존재)
J. 갱신 유효성 미수정 파일은 3경로 모두에서 최신본으로 갱신됨 — 보존 로직이 R-1을 무력화하지 않았음을 증명
K. 무오염 갱신·업데이트·언인스톨 후 .agents/ 하위에 백업/잔재 파일 0건
L. 문구 정합 "custom configs preserved" 류 문구가 실제로 보존된 대상만 지칭하도록 수정됨(E-6의 오신호 제거)

게이트 L은 문구 한 줄이지만 별도 항목으로 둡니다. E-6에서 확인했듯 틀린 안심 문구는 경고가 없는 것보다 나쁩니다.


9. 리스크 (개정)

ID 리스크 영향 완화
RK-1~RK-7 Rev.1과 동일 Rev.1 §11
RK-8 보존 로직이 과도하게 동작해 갱신이 사실상 no-op화(E-4 회귀) 높음 수신본이 아닌 대장과 비교(§2.2), T-D22/T-D23이 차단
RK-9 백업했다는 문구를 사용자가 "복원됐다"로 오해 §3.1 문구 규정, 게이트 L
RK-10 C3c(fetch 기본화)를 C3b보다 먼저 머지 치명 §6 순서 제약, B-4
RK-11 버전 스큐(구 파일 1개 + 신 파일 다수)로 백플레인 오작동 항목별 경고 의무화(§2.4), --overwrite-custom 안내
RK-5(개정) fetch 기본화로 오프라인/CI 실패 --no-refresh + 자산 부재 시 fetch 유지

10. 결론

agy의 이의제기는 채택합니다. 다만 세 가지를 수정합니다.

  1. 이 위험은 R-1이 만드는 것이 아니라 install.sh -fupdate.sh에 이미 존재하는 버그입니다(E-6/E-7). 따라서 가드는 R-1의 부속이 아니라 선행 조건이며, R-1을 채택하지 않더라도 독립적으로 고쳐야 합니다.
  2. 감지 기준을 수신 템플릿과의 비교에서 설치 시점 해시 대장과의 비교로 바꿉니다. 원안대로면 정상 갱신과 사용자 수정이 구분되지 않아 R-1이 무력화됩니다(E-8).
  3. 백업 위치를 .agents/**/*.user-bak에서 **.mam/skill-backups/<ts>/**로 옮기고, 보호 범위를 install.sh 복사 루프에서 install/update/remove 3경로 전체로 확장합니다. 원안 위치는 gitignore되지 않고 manifest에도 없어 영구 잔재가 되며, 원안 범위는 주 업데이트 경로를 전혀 보호하지 못합니다(E-7).

부수적으로, agy가 지적하지 않았으나 같은 계열인 Planner 귀책 결함 1건(.mam 상태 파일이 업데이트 시 소멸, §4)을 함께 수정합니다.


11. 역할 경계

본 문서는 설계 산출물이며 Planner는 저장소 코드를 일절 수정하지 않았습니다(MULTI_AGENT_RULES.md §4). E-6~E-8 실험은 임시 디렉토리(/tmp/mam_cust_*)에서 수행 후 정리했으며, 저장소 워킹트리는 클린 상태입니다. 구현은 Creator, 커밋은 GM 소관입니다.

차단 항목은 B-1, B-2, B-3, B-4, B-5 5건입니다.

[AGREEMENT: REACHED]