feat: add force-refresh capability and allow framework-owned skill updates during installation

This commit is contained in:
2026-07-27 13:16:14 +09:00
parent 520168ca55
commit 002d9b268d
2 changed files with 228 additions and 18 deletions
@@ -0,0 +1,162 @@
# 교차 코드 리뷰 리포트 — Job 3c062f3a (재리뷰)
- **대상**: `deploy/install.sh``.agents/` 자산 소유권 분리 + `--refresh-skills` 게이트 우회 도입
- **작업 목표**: "skill files in `.agents/` are updated with latest metadata frontmatter"
- **이전 리뷰**: Job `1b787c36` (`[VERDICT: NOT PASS]` + `[ESCALATE: PLANNER]`)
- **리뷰어 세션**: `canary-projects-multi-agent-mux-creator-claude`
> ⚠️ **역할 불일치 고지 (이전 리뷰에서 이어짐)**: `.mam/agent-sessions.yaml` 상 본 세션의 `role`은 여전히 `planner`이나 브리프는 `Reviewer`를 지정합니다. MULTI_AGENT_RULES.md §1에 따라 명시하되 연속성을 위해 직접 수행했습니다. GM 측에서 레지스트리 role을 정정하거나 리뷰 전담 세션으로 재배정할 것을 권고합니다.
## 검증 방법
정적 판독에 그치지 않고 **실제 코드를 추출해 샌드박스에서 실행**했습니다.
| 검증 | 방법 | 결과 |
|---|---|---|
| 문법 | `bash -n deploy/install.sh` | ✅ 통과 |
| 소유권 분리 동작 | `install.sh:113-118`(헬퍼) + `160-176`(복사 루프)를 `sed`로 **원본에서 추출**해 사전 시딩된 target에 실행 | ✅ 아래 표 |
| manifest 멱등성 | 동일 루프 3회 반복 실행 | ✅ 2줄 유지 |
| 인자 파싱 | `install.sh:10-46` 추출 후 7가지 호출 형태 매트릭스 | ✅ 전부 정상 |
| `update.sh` 호환 | `cat script \| bash -s -- <dir>` (실제 호출 형태) 재현 | ✅ 회귀 없음 |
| `FORCE_REFRESH` 견고성 | `1/0/true/yes/""/2` 6개 값 주입 | ⚠️ N1 |
| shellcheck | 로컬 미설치, `pip install shellcheck-py` 시도 실패(오프라인) | ❌ **미검증** — N7 |
---
## 1. 이전 차단 사유 해소 확인
### ✅ B1 해소 — 사용자 소유 파일 보존 (실측 검증)
`is_framework_owned()``.agents/skills/*`만 덮어쓰기 대상으로 한정했습니다. 사용자 커스터마이즈본을 미리 심어둔 target에 **실제 복사 루프를 실행**한 결과:
| 경로 | 분류 | 실행 후 내용 | 판정 |
|---|---|---|---|
| `.agents/skills/lib.sh` | framework | `UPSTREAM lib` | ✅ 갱신됨 |
| `.agents/skills/multi-agent-mux-resume/SKILL.md` | framework | `UPSTREAM resume SKILL` | ✅ **frontmatter 갱신 — 목표 달성** |
| `.agents/MULTI_AGENT_RULES.md` | user | `USER charter` | ✅ 보존 |
| `.agents/INSTALL.md` | user | `USER install manual` | ✅ 보존 |
| `.agents/references/herdr_docs.md` | user | `USER refs` | ✅ 보존 |
| `.agents/reports/sess-a/report-x.md` | user | `USER report` | ✅ 보존 |
이전 리뷰에서 지적한 헌장 파기 시나리오가 실제로 차단됨을 확인했습니다.
### ✅ B2 해소 — manifest 오염 제거 (가장 중요한 회귀 수정)
manifest append가 각 분기 **내부**로 이동해, 사용자 소유 선존재 파일은 복사도 등재도 되지 않습니다. 위 실행 후 manifest 실측:
```
.agents/skills/lib.sh
.agents/skills/multi-agent-mux-resume/SKILL.md
```
사용자 문서 4종이 **전부 미등재**입니다. 따라서 `remove.sh`의 manifest 기반 `delete_asset` 루프가 이들을 건드리지 않으며, `remove.sh`가 사용자에게 출력하는
```
" (Your own custom files inside .agents/ will NOT be touched)."
```
라는 고지가 다시 참이 됩니다. `remove.sh`의 fallback 경로(manifest 부재 시)도 `.agents/skills/*` 디렉터리만 삭제하고, 3단계 `find .agents -depth -type d -exec rmdir {} +`는 빈 디렉터리만 제거하므로 사용자 문서는 양쪽 경로 모두에서 안전합니다.
부수 확인: 신규 설치 시 `.agents/MULTI_AGENT_RULES.md`는 설치 스크립트가 **생성**했으므로 manifest에 등재되고 언인스톨 시 삭제됩니다 — 이는 올바른 대칭입니다.
### ✅ B3 해소 — 주 업그레이드 경로 동작
`if [ "$FORCE_REFRESH" -eq 1 ] || ! check_assets_present "."` 로 게이트를 우회할 수단이 생겼습니다. 인자 파싱 매트릭스 실측:
| 호출 | `TARGET_DIR` | `FORCE_REFRESH` |
|---|---|---|
| `install.sh` | `$(pwd)` | 0 |
| `install.sh /tmp/x` | `/tmp/x` | 0 |
| `install.sh --refresh-skills` | `$(pwd)` | **1** |
| `install.sh --refresh-skills /tmp/x` | `/tmp/x` | **1** |
| `install.sh /tmp/x --refresh-skills` | `/tmp/x` | **1** |
| `install.sh -f` / `--force` | `$(pwd)` | **1** |
| `install.sh a b` | — | `❌ Unknown argument: b` (exit 1) |
**`update.sh` 회귀 없음**: `update.sh:141``curl … | bash -s -- "$TARGET_DIR"` 에서 `--`는 bash 자신이 소비하므로 스크립트는 위치 인자 1개만 받습니다. 실제 파이프 형태로 재현해 `TARGET_DIR=[/tmp/x] FORCE_REFRESH=[0]` 을 확인했습니다. README 원라이너(무인자)도 정상입니다.
네트워크 실패 시 안전성도 유지됩니다 — `check_assets_present "$STAGE_DIR"` 검증이 복사 **이전**에 있고, `git clone` 실패는 `set -e`로 중단되며 `trap`이 STAGE_DIR을 정리하므로 기존 설치는 무손상입니다.
### ✅ B4 해소 — 주석·출력 정합성
- `install.sh:120-127` 헤더: FW-D1 안전 모델이 새 소유권 정책으로 정확히 재서술됨.
- `install.sh:155-159` 인라인 주석: 적용 범위(`.agents/skills/*` 덮어쓰기 / 그 외 no-clobber·unmanifested)를 실제 동작과 일치하게 기술.
- `install.sh:215`: `"✅ Skills staged into workspace (user documents and custom configs preserved)."` — 이제 참.
### 🔓 에스컬레이션 철회
이전 리뷰의 `[ESCALATE: PLANNER]` 근거였던 두 설계 결정이 모두 일관되게 해소되었습니다.
1. **`.agents/` 소유권 경계** → `.agents/skills/*` = 프레임워크 소유, 그 외 = 사용자 소유. 명시적 헬퍼 함수로 코드에 표현되어 검증·확장 가능합니다.
2. **갱신 책임 주체**`install.sh``--refresh-skills`로 in-place 갱신을 담당하고, `update.sh`는 기존의 remove-후-재설치 방식을 유지합니다. 두 경로가 경합하지 않음을 실측으로 확인했습니다.
**추가 재계획은 불필요합니다.**
---
## 2. 잔여 관찰 (전부 비차단)
### N1. `MAM_FORCE_REFRESH` 비숫자 값 — 조용한 무시 + 원시 셸 에러 (실측)
`[ "$FORCE_REFRESH" -eq 1 ]`은 산술 비교라 비숫자 입력에서 깨집니다. 6개 값 주입 결과:
| 입력 | 동작 | stderr |
|---|---|---|
| `1` | REFRESH | — |
| `0`, `""` | no-refresh | — |
| `true` | **no-refresh** | `[: true: integer expression expected` |
| `yes` | **no-refresh** | `[: yes: integer expression expected` |
| `2` | **no-refresh** | — (완전 무음) |
`if` 문맥이라 `set -e`로 중단되지는 않음을 별도 확인했습니다(`not-taken (survived)`). 즉 **크래시는 없으나**, `MAM_FORCE_REFRESH=true`를 지정한 사용자는 정체불명의 셸 에러를 보고, 갱신은 일어나지 않은 채 `"🎉 Installation complete!"` 를 받습니다. 리터럴 `1` 이외에는 전부 무효라는 사실이 어디에도 드러나지 않습니다.
```bash
# 권장: 문자열 비교로 전환 (0/미설정만 비활성)
if [ "$FORCE_REFRESH" != "0" ] || ! check_assets_present "."; then
```
### N2. 신규 플래그/환경변수가 사용자 문서에 전무
`README.md`, `BOOTSTRAP.md`, `BOOTSTRAP.ko.md`, `deploy/README.md`, `deploy/INSTALL.md`, `.agents/INSTALL.md` 전수 검색 결과 `--refresh-skills` / `MAM_FORCE_REFRESH` 언급이 **0건**입니다. 반면 형제 환경변수 `MAM_REPO_URL` / `MAM_ARCHIVE_URL` / `MAM_INSTALLER_URL``deploy/README.md:48-53`에 문서화되어 있어 일관성도 어긋납니다.
B3의 메커니즘은 갖춰졌지만 **발견 가능성이 없습니다** — README가 안내하는 원라이너를 정상 설치 위에서 재실행하면 여전히 조용히 no-op입니다. 기능이 실사용되려면 최소한 `README.md` Quick Start와 `deploy/README.md` 환경변수 표에 추가가 필요합니다. (동작 자체는 정상이므로 비차단으로 분류하나, **머지 전 처리를 권장**합니다.)
### N3. `--force` 의미 충돌 (suite 내 일관성)
| 스크립트 | `--force` 의미 |
|---|---|
| `remove.sh:18`, `update.sh:16` | 확인 프롬프트 생략 (비대화형) |
| `install.sh:18` (신규) | **네트워크 fetch + skill 덮어쓰기 강제** |
같은 배포 suite에서 정반대 성격입니다. `install.sh`에는 프롬프트가 없어 즉각적 피해는 없으나, `bash remove.sh --force`에 익숙한 사용자가 `bash install.sh --force`를 "무확인 실행"으로 오해하면 의도치 않은 네트워크 fetch와 skill 덮어쓰기가 발생합니다. `--force` 별칭을 떼고 `-f | --refresh-skills`만 남기는 것을 권장합니다.
### N4. 갱신 시 prune 부재
merge-only 복사라 업스트림에서 삭제·개명된 skill 파일이 target에 잔류하고 manifest에도 남습니다. 기존부터 있던 한계지만, `--refresh-skills`가 **공식 갱신 경로로 승격**되면서 체감 중요도가 올라갑니다("갱신했는데 왜 옛 파일이 남지"). manifest에 기록된 `.agents/skills/*` 중 이번 stage에 없는 항목을 정리하는 후속 작업을 권장합니다.
### N5. `cp` 모드 비전파 (기존 이슈)
`cp`는 기존 dest를 덮어쓸 때 dest 퍼미션을 유지하므로, 업스트림의 실행 비트 추가가 갱신 설치에 전파되지 않습니다. 현재 모든 스크립트가 `bash <script>` 형태로 호출되어 실질 영향은 없습니다.
### N6. `.agents/INSTALL.md` 가드 사문화 (기존 이슈, 이번 diff와 무관)
`.agents/INSTALL.md`는 git 추적 대상이며 `deploy/INSTALL.md`와 내용이 다릅니다. 복사 루프가 이를 먼저 생성하므로 `install.sh:205-211``[ ! -e ".agents/INSTALL.md" ]` 가드는 신규 설치에서 항상 거짓 — 도달 불가 코드입니다. 결과적으로 의도한 `deploy/INSTALL.md`가 아닌 `.agents/INSTALL.md`가 배포됩니다. 이번 변경이 만든 문제는 아니나 별도 티켓으로 추적할 가치가 있습니다.
### N7. shellcheck 미검증 — CI가 실제로 강제함
`deploy/gitea-ci.yml:36``shellcheck deploy/install.sh`를 옵션 없이(= 전 severity) 실행하며, 발견 시 non-zero로 스텝이 실패합니다. 본 환경에는 shellcheck가 없고 `shellcheck-py` 설치도 오프라인으로 실패해 **정적 린트를 수행하지 못했습니다**. `bash -n`은 통과했고 신규 코드(`while`/`case` 파싱, 헤어독, `case` 기반 헬퍼)에서 명백한 SC 위반은 육안상 보이지 않으나, **머지 전 CI 린트 통과를 별도 확인하십시오.** 이 항목은 제 검증 범위 밖입니다.
### N8. 워크스페이스 위생 (경과)
- 이전 리뷰에서 지적한 루트 `SKILL.md`(손실된 frontmatter 사본)는 **제거되었습니다** ✅. 다만 그것을 생성한 writer의 경로 해석·키 보존 동작을 점검했는지는 이 diff로 확인되지 않으므로, 원인 규명은 별도로 남겨두시길 권합니다.
- `.agents/skills/multi-agent-mux-delegate-job/*.tmp` 잔여물이 PID를 바꿔가며 재출현했다 자체 소멸합니다(`.29278_90739``.14424_90739`). 비원자적 쓰기의 일시적 흔적으로 보이며 리뷰 대상 변경과 무관합니다.
---
## 3. 결론
이전 리뷰의 차단 사유 4건이 **모두 해소되었고, 실제 코드를 추출해 실행한 기능 테스트로 확인**했습니다. 특히 사용자 소유 파일 보존(B1)과 manifest 미등재를 통한 `remove.sh` 약속 회복(B2)은 실측 결과가 명확합니다. 작업 목표인 skill frontmatter 갱신도 `.agents/skills/**` 범위에서 정확히 동작하며, `update.sh` 호환성 회귀는 없습니다. 소유권 경계를 `is_framework_owned()`라는 단일 함수로 표현한 설계는 향후 정책 변경 시 수정 지점이 하나로 모여 있어 유지보수성도 좋습니다.
잔여 8건은 전부 비차단입니다. 다만 **N2(문서화 부재)** 는 기능의 발견 가능성을 좌우하므로 머지 전 함께 처리하고, **N1(숫자 비교)** 은 한 줄 수정이므로 같이 반영할 것을 권합니다. **N7(shellcheck)** 은 제가 검증하지 못한 항목이니 CI 결과로 갈음해 주십시오.
[VERDICT: PASS]
+65 -17
View File
@@ -8,10 +8,43 @@
set -euo pipefail
# --- Configuration & Defaults ---
TARGET_DIR="${1:-$(pwd)}"
TARGET_DIR=""
FORCE_REFRESH="${MAM_FORCE_REFRESH:-0}"
VENV_NAME=".venv"
MIN_PYTHON_VERSION="3.9"
while [[ $# -gt 0 ]]; do
case "$1" in
-f|--force|--refresh-skills)
FORCE_REFRESH=1
shift
;;
-h|--help)
cat <<EOF
Usage: $0 [options] [target_dir]
Options:
-f, --force, --refresh-skills Force fetch and refresh framework skills under .agents/skills/
-h, --help Show this help message
EOF
exit 0
;;
*)
if [ -z "$TARGET_DIR" ]; then
TARGET_DIR="$1"
else
echo "❌ Error: Unknown argument: $1" >&2
exit 1
fi
shift
;;
esac
done
if [ -z "$TARGET_DIR" ]; then
TARGET_DIR="$(pwd)"
fi
echo "===================================================================="
echo "⚡ Starting Multi-Agent Mux (MAM) Installation"
echo "📂 Target Workspace: $TARGET_DIR"
@@ -76,17 +109,24 @@ check_assets_present() {
return 0
}
# Fetch the orchestration assets if missing or incomplete (for curl one-liner installs).
# Helper to classify framework-owned skill definitions vs user-owned project assets
is_framework_owned() {
case "$1" in
.agents/skills/*) return 0 ;;
*) return 1 ;;
esac
}
# Fetch the orchestration assets if missing or if skill refresh is requested.
#
# Safety model (FW-D1): we NEVER extract the repo archive directly into the
# target. Running inside an existing project must not overwrite the target's
# own files (README.md, FUTURE_WORKS.md, …) or litter it with this repo's
# development docs. Instead we stage the download into a throwaway temp dir,
# verify it, then copy ONLY the runtime assets (.agents/, documents, .env.example)
# into the target with per-file no-clobber guards so a pre-existing target file
# always wins.
if ! check_assets_present "."; then
echo "📥 Orchestration skills not found or incomplete. Fetching from Gitea repository..."
# own files (README.md, FUTURE_WORKS.md, AGENTS.md, MULTI_AGENT_RULES.md) or litter
# it with development docs. Instead we stage the download into a throwaway temp dir,
# verify it, then copy runtime assets: framework skills (.agents/skills/*) are updated,
# while user-owned documents use per-file no-clobber guards so pre-existing target files win.
if [ "$FORCE_REFRESH" -eq 1 ] || ! check_assets_present "."; then
echo "📥 Staging orchestration assets from Gitea repository..."
STAGE_DIR="$(mktemp -d)"
trap 'rm -rf "$STAGE_DIR"' EXIT
@@ -112,19 +152,27 @@ if ! check_assets_present "."; then
MANIFEST_FILE=".mam/install_manifest.txt"
touch "$MANIFEST_FILE"
# Copy ONLY runtime assets into the target, never overwriting an existing
# target file. We merge per-file (POSIX find + an explicit "[ ! -e ]" guard)
# instead of `cp -n`: `cp -n` is non-portable and now prints a deprecation
# warning on GNU coreutils 9.x, whereas the explicit guard is portable to
# BSD/macOS and makes the no-clobber intent obvious.
# Copy runtime assets (.agents/) into the target workspace.
# Framework-owned skill files (.agents/skills/*) are updated/overwritten so that
# latest skill definitions and metadata frontmatter take effect.
# User-owned documents (.agents/MULTI_AGENT_RULES*.md, .agents/INSTALL.md, etc.) use
# explicit no-clobber guards so pre-existing user files are untouched and unmanifested.
mkdir -p .agents
( cd "$STAGE_DIR/.agents" && find . -type f -print ) | while IFS= read -r rel; do
dest=".agents/${rel#./}"
if [ ! -e "$dest" ]; then
mkdir -p "$(dirname "$dest")"
cp "$STAGE_DIR/.agents/$rel" "$dest" || { echo "❌ Error: Failed to copy $rel" >&2; exit 1; }
if is_framework_owned "$dest"; then
cp -f "$STAGE_DIR/.agents/$rel" "$dest" || { echo "❌ Error: Failed to copy $rel" >&2; exit 1; }
if ! grep -Fqx "$dest" "$MANIFEST_FILE" 2>/dev/null; then
echo "$dest" >> "$MANIFEST_FILE"
fi
elif [ ! -e "$dest" ]; then
cp "$STAGE_DIR/.agents/$rel" "$dest" || { echo "❌ Error: Failed to copy $rel" >&2; exit 1; }
if ! grep -Fqx "$dest" "$MANIFEST_FILE" 2>/dev/null; then
echo "$dest" >> "$MANIFEST_FILE"
fi
fi
done
# Copy non-dev documents if they don't already exist.
@@ -164,7 +212,7 @@ if ! check_assets_present "."; then
rm -rf "$STAGE_DIR"
trap - EXIT
echo "✅ Skills staged into workspace (existing files preserved)."
echo "✅ Skills staged into workspace (user documents and custom configs preserved)."
fi
# Sanity check: verify all core files, not just a single one — an empty or