The .claude/skills check accepts both local layouts and fails on a dangling link. Review: round 1 (@e1ed270) claude agy grok PASS, codex NOT PASS (a dangling top-level link was skipped); fixed in 150b493; round 2 4/4 PASS. Reports and the round-2 identity sidecars go to .agents/reports/<session>/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
71 lines
6.8 KiB
Markdown
71 lines
6.8 KiB
Markdown
# PR #47 리뷰 — f275a259
|
||
|
||
**NOT PASS**. 단일 링크와 스킬별 링크 디렉터리를 모두 받는 수정은 동작한다. 다만 `.claude/skills` 자체가 깨진 심볼릭 링크이면 부재로 오인해 skip하는 경계 사례가 남아 있다. 작은 테스트 수정으로 해결할 수 있으며 제품 코드 변경이나 재계획은 필요 없다.
|
||
|
||
## Identity와 범위
|
||
|
||
- loop: `loop-20261004T043243Z-baf133`
|
||
- base: `6af4a796b1bdce3b2e929b7a45a8a22d66b36855`
|
||
- head: `e1ed2708d835d712f5f3a9dfd51b8f66ca1ac447`
|
||
- tree: `2f781e0db4df9a3082b4f55e69c42118868b18a1`
|
||
- 검증 cwd: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T043243Z-baf133/1`
|
||
|
||
HEAD/tree 일치, 시작·종료 시 git status clean. 변경 파일은 `tests/test_issue42_skill_blocks.py` 하나이며 제품 코드·버전 변경은 없다. `git diff --check 6af4a79 HEAD` 통과.
|
||
|
||
## R1 — P2: 깨진 최상위 링크를 skip하지 말 것
|
||
|
||
**근거:** `tests/test_issue42_skill_blocks.py:245`–`:248`.
|
||
|
||
`Path.exists()`는 링크 자체가 아니라 대상을 따라 검사하므로 dangling symlink에서 False다. 따라서 `.claude/skills`가 존재하는 잘못된 링크여도 :245의 부재 분기가 실행돼 :247의 symlink 검증까지 도달하지 않는다. 정상적인 “이 워크트리에 로컬 배치가 없음”과 “로컬 배치가 있으나 깨짐”을 구별하지 못한다.
|
||
|
||
**재현:** 검증 트리의 원래 `.claude/skills`가 없음을 확인하고, 그 경로에 존재하지 않는 `absent-skills-target`을 가리키는 임시 링크를 만든 뒤 해당 pytest node를 실행했다. 실행 후 링크를 삭제했다.
|
||
|
||
```sh
|
||
PYTHONDONTWRITEBYTECODE=1 /Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python -m pytest -p no:cacheprovider -q --basetemp=/tmp/f275a259-broken_single tests/test_issue42_skill_blocks.py::test_claude_skills_link_resolves_to_agents_skills
|
||
```
|
||
|
||
- 기대: 잘못된 로딩 경로이므로 fail, nonzero.
|
||
- 실제: **1 skipped, rc 0**. 반면 디렉터리 안 깨진 개별 링크는 정상적으로 fail한다.
|
||
- 영향: 모든 프로젝트 스킬의 진입 경로가 끊겨도 “워크트리에 없음”으로 숨겨져 CI/검증이 성공한다.
|
||
- 수정 방향: 부재 skip을 `if not link.exists() and not link.is_symlink():`로 한정하거나 symlink 검증을 부재 검사보다 먼저 수행한다. 완전 부재는 계속 skip하고, dangling root link는 실패해야 한다. 부재/정상 단일 링크/정상 개별 링크/깨진 최상위 링크를 회귀 사례로 남긴다.
|
||
|
||
메모리상으로 위 조건만 바꾼 같은 함수에 대해 **완전 부재는 SKIP, 깨진 링크는 AssertionError**가 되는 것을 확인했다. 체크아웃 코드는 수정하지 않았다.
|
||
|
||
이 `exists()` 선행 분기는 이전 버전에도 있었다. 이번 변경이 새로 만든 회귀라고 주장하지 않는다. 그러나 브리프가 명시적으로 요청한 깨진 링크 검토에서 확인된 허점이고, 이 함수가 두 배치의 경로 무결성을 검증하도록 수정되는 범위 안에서 해결할 수 있으므로 반려 사유로 남긴다.
|
||
|
||
## 실행 결과
|
||
|
||
원래 워크트리 배치(로컬 `.claude/skills` 없음)에서 지정 모듈 실행:
|
||
|
||
```sh
|
||
PYTHONDONTWRITEBYTECODE=1 /Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python -m pytest -p no:cacheprovider -q --basetemp=/tmp/f275a259-selected tests/test_issue42_skill_blocks.py
|
||
```
|
||
|
||
**31 passed, 1 skipped in 0.06s**, rc 0. 이 기본 실행은 로컬 링크 분기를 검증하지 않으므로 아래 임시 배치 탐침을 별도로 실행했다.
|
||
|
||
| 임시 배치 | 실제 pytest 결과 | 판단 |
|
||
|---|---|---|
|
||
| `.claude/skills` 완전 부재 | 1 skipped, rc 0 | 정상 |
|
||
| 8개 스킬별 올바른 링크 | 1 passed, rc 0 | 정상; base 함수는 같은 배치에서 AssertionError |
|
||
| 위 배치 + lib.sh/lib_py 링크 및 notes.txt | 1 passed, rc 0 | 정상 |
|
||
| 개별 링크 하나가 다른 스킬을 가리킴 | 1 failed, rc 1 | 정상 거부 |
|
||
| 개별 링크 하나가 깨짐 | 1 failed, rc 1 | 정상 거부 |
|
||
| 단일 링크 → `.agents/skills` | 1 passed, rc 0 | 정상 |
|
||
| 단일 링크 → 다른 기존 디렉터리 | 1 failed, rc 1 | 정상 거부 |
|
||
| 단일 링크 → 존재하지 않는 대상 | 1 skipped, rc 0 | **R1** |
|
||
| 일반 파일 | 1 failed, rc 1 | 정상 거부 |
|
||
| 빈 디렉터리 | 1 failed, rc 1 | 정상 거부 |
|
||
|
||
탐침 스크립트: `/tmp/f275a259-layout-probes.py`; 결과: `/tmp/f275a259-layout-probes.log`. 위 cwd에서 `PYTHONDONTWRITEBYTECODE=1 <위 Python> /tmp/f275a259-layout-probes.py`로 재현한다. 스크립트는 원래 경로가 없는지 먼저 확인하고 자신이 만든 임시 배치만 finally에서 정리한다. 기대한 부정 사례의 pytest 실패는 제품 스위트 실패 수치에 합산하지 않았다.
|
||
|
||
## 나머지 검토 항목
|
||
|
||
- **실제 본 체크아웃:** 읽기 전용 조사에서 `.claude/skills`는 디렉터리이며 8개 스킬 링크와 lib.sh/lib_py 링크가 있다. 8개 스킬의 resolve 결과는 각각 본 체크아웃의 `.agents/skills/<skill>`과 정확히 일치했다. 수정의 동기가 된 정상 배치를 지원한다.
|
||
- **빈 SKILL_NAMES:** 대상 함수만 메모리상 `SKILL_NAMES=[]`로 실행하면 for-loop가 비어 통과한다. 그러나 같은 모듈의 `test_inventory_covers_eight_skills`가 8개를 강제해 전체 모듈은 실패한다. 현재 모듈 단위 실행에는 침묵하는 성공이 없으므로 별도 반려 사유는 아니다. 대상 node를 단독 운영할 때까지 방어하려면 nonempty 단언을 추가할 수 있다.
|
||
- **여분 항목:** framework 8개를 확인하는 테스트에서 lib.sh/lib_py 등 여분 항목을 금지하지 않는 것은 적절하다. Claude Code 공식 문서는 프로젝트 스킬을 `.claude/skills/<skill-name>/SKILL.md`로 설명하며 스킬 폴더의 symlink를 지원한다. 일반 helper 파일·SKILL.md 없는 helper 디렉터리를 framework 스킬로 취급할 필요는 없다. 다만 이 테스트는 다른 사용자 스킬 전체를 감사하지 않으므로 docstring의 “each skill Claude Code loads”는 “each framework skill” 정도로 좁히면 더 정확하다. [공식 Claude Code 스킬 로딩 문서](https://code.claude.com/docs/en/skills#choose-where-skills-load).
|
||
- **과도한 엄격성:** 스킬 디렉터리의 내용 복사본은 같은 resolve 대상이 아니므로 거부된다. 브리프가 두 링크 배치만 지원하려는 계약에 맞는다. 다른 추가 사용자 스킬의 존재는 허용된다.
|
||
|
||
검증 트리 안의 임시 `.claude/skills`는 모두 삭제했다. 본 체크아웃의 배치를 변경하지 않았으며 제품 코드·실제 .mam.env·실제 agent/herdr 상태를 건드리지 않았다. 실제 `.mam` 쓰기는 지시된 잡 lifecycle과 이 최종 리포트에 한정했다.
|
||
|
||
[VERDICT: NOT PASS]
|