merge: fix/issue-42-skills-link (.claude/skills check takes both local layouts; v4.8.4)
Reviewed in two rounds by claude, agy, grok and codex (4/4 PASS on 150b493).
This commit was merged in pull request #47.
This commit is contained in:
commit
c5c7eff6f7
29 files changed
+602
-28
No files matched your search
@@ -0,0 +1,45 @@
|
|||||||
|
# 리뷰 리포트 — Job 40f4077c (PR #47, fix/issue-42-skills-link)
|
||||||
|
|
||||||
|
- 리뷰어: planner-reviewer-claude-01 (claude)
|
||||||
|
- 리뷰 identity: loop-20261004T043243Z-baf133 / base 6af4a79 / head e1ed270 / tree 2f781e0 — 검증 경로에서 `git rev-parse HEAD 'HEAD^{tree}'` 일치, 미커밋 변경 없음
|
||||||
|
|
||||||
|
## 결론 요약
|
||||||
|
수정은 두 배치(단일 링크 / 스킬별 링크 디렉터리)를 모두 올바르게 받고, 잘못된 배치는 실패시킨다. 차단 결함 없음. 비차단 관찰 2건.
|
||||||
|
|
||||||
|
## 1. 범위 (항목 3)
|
||||||
|
`git diff --stat 6af4a79 e1ed270`: tests/test_issue42_skill_blocks.py 한 파일, +10/−1 (test_claude_skills_link_resolves_to_agents_skills, :240-253). 제품 코드·버전 변경 없음.
|
||||||
|
|
||||||
|
## 2. 배치별 동작 (항목 1)
|
||||||
|
검증 경로 안에 임시 .claude/skills(.gitignore:40 의 `.claude/` 로 무시됨)를 만들어 이 테스트만 돌리고 매번 지웠다. 시작 전 .claude 가 없음을 확인했고, 끝난 뒤 `.claude` 잔여물 없음·`git status --short --ignored` 비어 있음·HEAD 불변을 확인했다. 본 체크아웃의 .claude/skills 는 `ls -la` 로 읽기만 했다(스킬 8개 링크 + lib_py·lib.sh 링크 = 10개 항목, 모두 ../../.agents/skills/<이름>).
|
||||||
|
|
||||||
|
| 배치 | 결과 | 판단 |
|
||||||
|
|---|---|---|
|
||||||
|
| A. 없음 | skip | 의도대로 |
|
||||||
|
| B. 단일 링크 → .agents/skills | pass | 의도대로 |
|
||||||
|
| C. 스킬별 링크 8개 + lib.sh·lib_py 링크(본 체크아웃과 같은 배치) | pass | 의도대로 |
|
||||||
|
| C-old. 같은 배치에서 base(6af4a79) 의 테스트 | **fail** | 수정 대상 결함 재현 |
|
||||||
|
| C2. C + 여분 디렉터리(foreign-skill) | pass | 여분 허용 |
|
||||||
|
| D. stop 링크가 loop 를 가리킴 | fail (`.claude/skills/multi-agent-mux-stop -> …/multi-agent-mux-loop`) | 의도대로 |
|
||||||
|
| E. 스킬 링크 하나 없음 | fail (`… is missing`) | 의도대로 |
|
||||||
|
| F. 스킬 링크 하나가 깨짐 | fail (`… is missing`) | 의도대로 |
|
||||||
|
| G. 링크 대신 복사본 디렉터리 | fail (resolve 가 .claude 쪽) | 의도대로 — 복사본은 드리프트하므로 막는 것이 맞다 |
|
||||||
|
| H. .claude/skills 가 일반 파일 | fail (`… is missing`) | 의도대로 |
|
||||||
|
| J. 단일 링크가 다른 곳(스킬 하나)을 가리킴 | fail | 의도대로 |
|
||||||
|
| K. 빈 디렉터리 | fail | 의도대로 |
|
||||||
|
| L. 스킬별 링크가 중간 링크를 거쳐 해석됨 | pass | resolve() 가 최종 대상을 비교하므로 맞다 |
|
||||||
|
| I. **단일 링크가 깨짐** (`.claude/skills -> ../nowhere`) | **skip** | 관찰 N1 |
|
||||||
|
|
||||||
|
- SKILL_NAMES 가 비면 스킬별 루프가 공집합이라 통과하겠지만, 같은 파일의 test_inventory_covers_eight_skills 가 `len(SKILL_NAMES) == 8` 을 단언하므로(파일 상단 SKILL_NAMES 정의와 함께) 놓치지 않는다.
|
||||||
|
|
||||||
|
## 3. 엄격성 (항목 2)
|
||||||
|
- 여분 항목(lib_py, lib.sh, 다른 스킬 디렉터리)은 허용된다(C, C2). Claude Code 가 스킬로 읽는 것은 SKILL.md 를 가진 하위 디렉터리이므로 여덟 MAM 스킬이 .agents/skills 의 것인지만 보는 현재 기준이 적절하다. 다른 스킬을 같이 두는 정상 사용을 막지 않는다.
|
||||||
|
- lib.sh·lib_py 링크는 검사하지 않는데, 진입 스크립트는 `$SCRIPT_DIR/../..` 에서 lib.sh 를 찾고 없으면 `${WORKSPACE_ROOT:-$PWD}/.agents/skills/lib.sh` 로 폴백하므로(예: reconcile.sh:20-21) 테스트의 필수 조건으로 둘 이유는 없다. 문서의 호출도 모두 `bash .agents/skills/...` 경로를 쓴다.
|
||||||
|
|
||||||
|
## 4. 비차단 관찰
|
||||||
|
- **N1 깨진 단일 링크가 skip 으로 숨는다** (tests/test_issue42_skill_blocks.py:245-246): `link.exists()` 는 링크를 따라가므로 대상이 없는 단일 링크는 "없음"으로 보고 skip 한다. 이 경우 Claude Code 는 스킬을 하나도 못 읽는데 테스트는 통과(skip)한다. base 테스트도 같은 동작이었으므로 이 PR 의 회귀는 아니다. `if not link.exists() and not link.is_symlink(): skip` 으로 바꾸면 깨진 링크는 아래 resolve 비교에서 실패한다.
|
||||||
|
- **N2 실패 메시지**: G(복사본)의 메시지가 `-> <.claude 안 경로>` 로 나와 "링크가 아니라 복사본"이라는 원인이 바로 드러나지 않는다. 사소함.
|
||||||
|
|
||||||
|
## 5. 테스트
|
||||||
|
검증 경로에서 `tests/test_issue42_skill_blocks.py` 전체: **31 passed / 1 skipped** (skip = 이 테스트, 워크트리에 .claude 없음 — 정상). 배치 탐침은 §2.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"loop_id": "loop-20261004T044643Z-1b1440",
|
||||||
|
"base_sha": "e1ed2708d835d712f5f3a9dfd51b8f66ca1ac447",
|
||||||
|
"head_sha": "150b4937f816c6407be16fc4c37ab88e478927e7",
|
||||||
|
"tree_sha": "afce44d2e0f91720d81e8d7d484f0e1358dee680",
|
||||||
|
"review_commit": "150b4937f816c6407be16fc4c37ab88e478927e7",
|
||||||
|
"reviewer": "planner-reviewer-claude-01",
|
||||||
|
"job_id": "dcece792",
|
||||||
|
"verdict": "PASS",
|
||||||
|
"transport": "herdr",
|
||||||
|
"evidence": "review-tree",
|
||||||
|
"isolation": "worktree",
|
||||||
|
"verified_at": "2026-10-04T04:54:16Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 리뷰 리포트 — Job dcece792 (PR #47 2회차 델타, e1ed270..150b493)
|
||||||
|
|
||||||
|
- 리뷰어: planner-reviewer-claude-01 (claude)
|
||||||
|
- 리뷰 identity: loop-20261004T044643Z-1b1440 / base e1ed270 / head 150b493 / tree afce44d — 검증 경로에서 `git rev-parse HEAD 'HEAD^{tree}'` 일치, 미커밋 변경 없음
|
||||||
|
|
||||||
|
## 결론 요약
|
||||||
|
codex R1(그리고 같은 내용인 본인 1회차 관찰 N1)이 닫혔다. 차단 결함 없음.
|
||||||
|
|
||||||
|
## 1. 범위
|
||||||
|
`git diff --stat e1ed270 HEAD` 와 `git diff --stat 6af4a79 HEAD` 모두 tests/test_issue42_skill_blocks.py 한 파일(누적 +59/−3). 제품 코드·버전 변경 없음.
|
||||||
|
|
||||||
|
## 2. R1 종결
|
||||||
|
| 지적 | 수정 (tests/test_issue42_skill_blocks.py) | 확인 |
|
||||||
|
|---|---|---|
|
||||||
|
| codex R1 / claude N1: 깨진 최상위 링크가 `Path.exists()` 때문에 skip | claude_skills_problems(:240-261): 부재 = `not exists() and not is_symlink()` 일 때만 None(skip). symlink 는 `exists() and resolve() == skills_dir.resolve()` 여야 함 | 검증 경로에 `.claude/skills -> ../nowhere` 를 만들어 실제 테스트 실행 → **fail** (1회차 head 에서는 skip 이었음) |
|
||||||
|
|
||||||
|
## 3. 실제 배치 탐침 (검증 경로 안 임시 .claude/skills, 매번 삭제)
|
||||||
|
| 배치 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| 스킬별 링크 8개 + lib.sh·lib_py 링크(본 체크아웃과 동일) | pass |
|
||||||
|
| 단일 링크 → ../.agents/skills | pass |
|
||||||
|
| 깨진 최상위 링크(codex 탐침) | fail |
|
||||||
|
| 스킬 링크 하나가 깨짐 | fail ("… missing or dangling") |
|
||||||
|
| .claude/skills 가 일반 파일 | fail (각 스킬 "missing or dangling") |
|
||||||
|
| 최상위 자기 참조 링크(`skills -> skills`) | fail (RuntimeError: Symlink loop, Python 3.9.6) — 관찰 참고 |
|
||||||
|
시작 전 .claude 부재, 끝난 뒤 잔여물 없음·`git status --short --ignored` 비어 있음·HEAD 불변 확인. 본 체크아웃의 .claude/skills 는 건드리지 않았다.
|
||||||
|
|
||||||
|
## 4. 합성 배치 테스트
|
||||||
|
- test_claude_skills_layouts(:271-300)는 실제 배치 검사(test_claude_skills_link_resolves_to_agents_skills, :264-268)와 **같은 함수** claude_skills_problems 를 부른다.
|
||||||
|
- tmp_path 아래 상대 링크만 쓰고 외부 상태·시간·네트워크에 의존하지 않으므로 결정적이다. 6개 배치(absent/single/per_skill/wrong_skill/broken_top/broken_entry)가 모두 워크트리에서도 돈다.
|
||||||
|
- 실행: 검증 경로에서 `tests/test_issue42_skill_blocks.py` **37 passed / 1 skipped** (1회차 31 + 합성 6; skip 은 실제 배치 테스트, 워크트리라 정상).
|
||||||
|
|
||||||
|
## 5. 비차단 관찰
|
||||||
|
- 최상위 링크가 순환하면 :252 의 메시지용 `link.resolve()` 가 Python 3.9 에서 RuntimeError 를 던져 assert 실패 대신 예외로 끝난다. 어느 쪽이든 테스트는 실패하므로 잘못된 배치를 놓치지 않는다. 메시지를 `os.readlink(link)` 로 만들면 깔끔해진다(선택).
|
||||||
|
- 복사본 디렉터리일 때 메시지가 원인을 직접 말하지 않는 점(1회차 N2)은 그대로이며 사소하다.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"loop_id": "loop-20261004T044643Z-1b1440",
|
||||||
|
"base_sha": "e1ed2708d835d712f5f3a9dfd51b8f66ca1ac447",
|
||||||
|
"head_sha": "150b4937f816c6407be16fc4c37ab88e478927e7",
|
||||||
|
"tree_sha": "afce44d2e0f91720d81e8d7d484f0e1358dee680",
|
||||||
|
"review_commit": "150b4937f816c6407be16fc4c37ab88e478927e7",
|
||||||
|
"reviewer": "planner-reviewer-codex-01",
|
||||||
|
"job_id": "5f1a5a29",
|
||||||
|
"verdict": "PASS",
|
||||||
|
"transport": "herdr",
|
||||||
|
"evidence": "review-tree",
|
||||||
|
"isolation": "worktree",
|
||||||
|
"verified_at": "2026-10-04T04:54:16Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# PR #47 2회차 델타 리뷰 — 5f1a5a29
|
||||||
|
|
||||||
|
**PASS**. 이전 R1(깨진 최상위 skills 링크의 skip)이 해결됐고, 실제 검사와 같은 함수를 사용하는 합성 배치 테스트가 회귀를 잡는다. 새 반려 사유는 발견하지 못했다.
|
||||||
|
|
||||||
|
## 검증 identity와 범위
|
||||||
|
|
||||||
|
- loop: `loop-20261004T044643Z-1b1440`
|
||||||
|
- base: `e1ed2708d835d712f5f3a9dfd51b8f66ca1ac447`
|
||||||
|
- head: `150b4937f816c6407be16fc4c37ab88e478927e7`
|
||||||
|
- tree: `afce44d2e0f91720d81e8d7d484f0e1358dee680`
|
||||||
|
- 검증 cwd: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T044643Z-1b1440/1`
|
||||||
|
|
||||||
|
HEAD/tree 일치, 시작·종료 시 git status clean. 이번 한 커밋의 변경은 `tests/test_issue42_skill_blocks.py` 하나(59 insertions, 12 deletions)이며 제품 코드·버전 변경은 없다. `git diff --check e1ed270 HEAD` 통과.
|
||||||
|
|
||||||
|
## 이전 지적 판정
|
||||||
|
|
||||||
|
| 지적 | 판정 | 근거 |
|
||||||
|
|---|---|---|
|
||||||
|
| R1: `.claude/skills` 자체가 dangling symlink일 때 skip | **종결** | `tests/test_issue42_skill_blocks.py:248`이 exists와 is_symlink가 모두 False인 경우에만 None을 반환한다. 깨진 링크는 :250–252의 symlink 검사에서 문제 목록을 반환하고, 실제 테스트 :264–267이 이를 실패로 처리한다. |
|
||||||
|
|
||||||
|
## 검증 결과
|
||||||
|
|
||||||
|
검증 cwd에서 실행:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 /Users/godopu16/PuKi/laa/canary_projects/multi-agent-mux/.venv/bin/python -m pytest -p no:cacheprovider -q --basetemp=/tmp/5f1a5a29-selected tests/test_issue42_skill_blocks.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**37 passed, 1 skipped in 0.06s**, rc 0. skip은 검증 트리에 원래 로컬 `.claude/skills`가 없기 때문이며, 새 합성 배치 6건은 모두 실행됐다.
|
||||||
|
|
||||||
|
- **같은 실제 경로로 전후 비교:** 검증 트리 안에 임시 `.claude/skills`를 존재하지 않는 대상의 symlink로 만들었다. base e1ed270에서 추출한 기존 함수는 같은 경로에서 SKIP했다. 현재 실제 pytest node는 **1 failed in 0.04s, rc 1**을 반환했다. 이 실패는 의도한 부정 탐침 결과이며 지정 모듈의 실패가 아니다.
|
||||||
|
- **회귀 테스트의 감도:** 현재 helper의 부재 조건만 메모리상 이전 `if not link.exists():`로 되돌린 뒤, 새 `test_claude_skills_layouts(..., "broken_top", False)`를 호출하면 AssertionError가 발생했다. 즉 새 회귀 테스트가 이전 결함을 검출한다.
|
||||||
|
- **정상 배치 유지:** 합성 `single`과 `per_skill`은 통과한다. 본 체크아웃의 스킬별 링크 배치에도 현재 helper를 읽기 전용으로 적용해 문제 목록 `[]`를 확인했다.
|
||||||
|
- **정리:** 임시 링크와 이번 탐침이 만든 부모 디렉터리를 finally에서 제거했다. 검증 트리의 로컬 배치 부재와 git clean 상태를 다시 확인했다.
|
||||||
|
|
||||||
|
독립 탐침은 `/tmp/5f1a5a29-probes.py`에 있다. 위 검증 cwd에서 `PYTHONDONTWRITEBYTECODE=1 <위 Python> /tmp/5f1a5a29-probes.py`로 재현한다. 전체 탐침 실행은 rc 0이며 base SKIP/current FAIL/조건 복원 시 합성 테스트 FAIL을 각각 확인한다. 체크아웃 소스는 변경하지 않았다.
|
||||||
|
|
||||||
|
## 합성 테스트와 범위 판단
|
||||||
|
|
||||||
|
`claude_skills_problems()`(:240–260)가 실제 로컬 배치 테스트(:264)와 합성 테스트(:294)의 공통 검사 함수다. 합성 테스트(:270–300)는 각각의 tmp_path 안에서 두 디렉터리 및 상대 symlink를 구성하고, 없음/단일 링크/개별 링크/잘못된 대상/깨진 최상위 링크/깨진 개별 링크를 직접 검사한다. 프로세스·시간·실제 사용자 배치에 의존하지 않아 결정적이다. 테스트 대상이 파일·디렉터리의 경로 해석이므로 합성 대상에 SKILL.md 본문을 만들지 않은 것도 적절하다.
|
||||||
|
|
||||||
|
기존 모듈의 8개 스킬 inventory 보장과 링크별 대상 비교는 유지됐다. 새로운 helper는 테스트 전용이며 제품 동작을 바꾸지 않는다. 기본 검사가 워크트리에서 skip되어도 합성 6건이 실행되므로 이번 수정의 핵심 조건은 더 이상 로컬 배치 유무에 가려지지 않는다.
|
||||||
|
|
||||||
|
본 체크아웃은 배치 읽기에만 사용했다. 실제 .mam.env·agent 세션·herdr 서버는 건드리지 않았고, 실제 .mam 쓰기는 지시된 lifecycle과 이 리포트에 한정했다. 재계획 승격은 필요 없다.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# 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]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"loop_id": "loop-20261004T044643Z-1b1440",
|
||||||
|
"base_sha": "e1ed2708d835d712f5f3a9dfd51b8f66ca1ac447",
|
||||||
|
"head_sha": "150b4937f816c6407be16fc4c37ab88e478927e7",
|
||||||
|
"tree_sha": "afce44d2e0f91720d81e8d7d484f0e1358dee680",
|
||||||
|
"review_commit": "150b4937f816c6407be16fc4c37ab88e478927e7",
|
||||||
|
"reviewer": "reviewer-agy-01",
|
||||||
|
"job_id": "1c2989b2",
|
||||||
|
"verdict": "PASS",
|
||||||
|
"transport": "herdr",
|
||||||
|
"evidence": "review-tree",
|
||||||
|
"isolation": "worktree",
|
||||||
|
"verified_at": "2026-10-04T04:54:16Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Final Review Report: Job 1c2989b2 (PR #47 / Issue #42 Skills Link Round 2 Delta)
|
||||||
|
|
||||||
|
## 1. Review Identity & Metadata
|
||||||
|
- **Loop ID**: `loop-20261004T044643Z-1b1440`
|
||||||
|
- **Job ID**: `1c2989b2`
|
||||||
|
- **Reviewer**: agy (session: `herdr:reviewer-agy-01`)
|
||||||
|
- **Base Commit**: `e1ed2708d835d712f5f3a9dfd51b8f66ca1ac447` (Round 1 Head)
|
||||||
|
- **Head Commit**: `150b4937f816c6407be16fc4c37ab88e478927e7` (1 commit)
|
||||||
|
- **Tree Hash**: `afce44d2e0f91720d81e8d7d484f0e1358dee680`
|
||||||
|
- **Verification Path**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T044643Z-1b1440/1`
|
||||||
|
- **Git Status**: Clean (working tree clean, no uncommitted changes)
|
||||||
|
- **Whitespace Check (`git diff base...HEAD --check`)**: Clean (0 errors)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Commit Audit (base..head)
|
||||||
|
|
||||||
|
Commit `150b493`: `test(issue-42): review R1 - a dangling .claude/skills link fails instead of skipping`
|
||||||
|
|
||||||
|
Diff summary:
|
||||||
|
- [`tests/test_issue42_skill_blocks.py`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T044643Z-1b1440/1/tests/test_issue42_skill_blocks.py#L237-L300):
|
||||||
|
- Refactored layout validation into helper function [`claude_skills_problems(link, skills_dir, names)`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T044643Z-1b1440/1/tests/test_issue42_skill_blocks.py#L240-L260).
|
||||||
|
- Defined absent status strictly as `not link.exists() and not link.is_symlink()`, ensuring a dangling broken symlink is never falsely treated as absent.
|
||||||
|
- Added deterministic parametrized test [`test_claude_skills_layouts(tmp_path, layout, ok)`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T044643Z-1b1440/1/tests/test_issue42_skill_blocks.py#L270-L300) covering 6 synthetic layouts.
|
||||||
|
|
||||||
|
**Product Code Modifications**: None (0 changes to `.sh` or `.py` product code).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Evaluation of Review Items
|
||||||
|
|
||||||
|
### Item 1: Resolution of Codex R1 (Dangling Top-Level Symlink)
|
||||||
|
- **Issue**: In the previous iteration (`e1ed270`), `if not link.exists(): pytest.skip(...)` followed the symlink. If `.claude/skills` was a dangling broken symlink, `Path.exists()` returned `False`, causing the test to skip rather than fail.
|
||||||
|
- **Fix**: In [`claude_skills_problems`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T044643Z-1b1440/1/tests/test_issue42_skill_blocks.py#L248):
|
||||||
|
```python
|
||||||
|
if not link.exists() and not link.is_symlink():
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
When `.claude/skills` is a broken symlink, `link.is_symlink()` is `True`, so it proceeds to validation. It tests `link.exists() and link.resolve() == skills_dir.resolve()`, which evaluates to `False`, returning a problem entry and causing the test to fail.
|
||||||
|
- **Empirical Validation**:
|
||||||
|
- Injected temporary broken top-level symlink `.claude/skills -> nonexistent`: test immediately failed with `AssertionError: assert ['... -> ... is not ...'] == []`.
|
||||||
|
- Confirmed Codex R1 is fully resolved and closed.
|
||||||
|
|
||||||
|
### Item 2: Deterministic Synthetic Tests & Shared Validation Logic
|
||||||
|
- Both the real filesystem check [`test_claude_skills_link_resolves_to_agents_skills`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T044643Z-1b1440/1/tests/test_issue42_skill_blocks.py#L263-L267) and the synthetic test [`test_claude_skills_layouts`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T044643Z-1b1440/1/tests/test_issue42_skill_blocks.py#L270-L300) use the identical validation function `claude_skills_problems`.
|
||||||
|
- Synthetic test runs 6 isolated cases in `tmp_path`:
|
||||||
|
1. `absent`: correctly returns `None` (triggers skip).
|
||||||
|
2. `single`: valid top-level symlink returns `[]` (passes).
|
||||||
|
3. `per_skill`: valid directory with per-skill symlinks returns `[]` (passes).
|
||||||
|
4. `wrong_skill`: symlink pointing to another skill returns problem (fails).
|
||||||
|
5. `broken_top`: dangling top-level symlink returns problem (fails).
|
||||||
|
6. `broken_entry`: dangling entry symlink inside directory returns problem (fails).
|
||||||
|
- These tests run deterministically in all environments, including isolated worktrees without `.claude/skills`.
|
||||||
|
|
||||||
|
### Item 3: Scope & Boundaries
|
||||||
|
- Only [`tests/test_issue42_skill_blocks.py`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T044643Z-1b1440/1/tests/test_issue42_skill_blocks.py) was modified (+59, -12 lines).
|
||||||
|
- Zero changes to product scripts or Python libraries.
|
||||||
|
- Zero version bumps.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Test Suite Execution Results
|
||||||
|
|
||||||
|
Executed in verification worktree (`/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T044643Z-1b1440/1`) with `--basetemp=/tmp/pytest-pr47-r2`:
|
||||||
|
|
||||||
|
| Test Suite | Result | Duration |
|
||||||
|
|---|---|---|
|
||||||
|
| `tests/test_issue42_skill_blocks.py` | 37 passed, 1 skipped* | 0.07 s |
|
||||||
|
| `tests/test_version_consistency.py` | 3 passed | 0.22 s |
|
||||||
|
| `tests/test_tier1_unit.py` | 67 passed | 62.01 s |
|
||||||
|
|
||||||
|
*\* Note: `test_claude_skills_link_resolves_to_agents_skills` skipped as expected in worktrees lacking untracked `.claude/skills`; synthetic test suite thoroughly exercises all 6 layout conditions.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Conclusion
|
||||||
|
|
||||||
|
PR #47 Round 2 delta cleanly fixes the dangling symlink false-skip defect identified by Codex R1, centralizes logic into a shared helper function, and adds comprehensive synthetic layout test coverage.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Final Review Report: Job 26b5444e (PR #47 / Issue #42 Skills Link)
|
||||||
|
|
||||||
|
## 1. Review Identity & Metadata
|
||||||
|
- **Loop ID**: `loop-20261004T043243Z-baf133`
|
||||||
|
- **Job ID**: `26b5444e`
|
||||||
|
- **Reviewer**: agy (session: `herdr:reviewer-agy-01`)
|
||||||
|
- **Base Commit**: `6af4a796b1bdce3b2e929b7a45a8a22d66b36855` (main, v4.8.3)
|
||||||
|
- **Head Commit**: `e1ed2708d835d712f5f3a9dfd51b8f66ca1ac447` (1 commit)
|
||||||
|
- **Tree Hash**: `2f781e0db4df9a3082b4f55e69c42118868b18a1`
|
||||||
|
- **Verification Path**: `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T043243Z-baf133/1`
|
||||||
|
- **Git Status**: Clean (working tree clean, no uncommitted changes)
|
||||||
|
- **Whitespace Check (`git diff base...HEAD --check`)**: Clean (0 errors)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Commit Audit (base..head)
|
||||||
|
|
||||||
|
Commit `e1ed270`: `test(issue-42): the .claude/skills check accepts both local layouts`
|
||||||
|
|
||||||
|
Diff summary:
|
||||||
|
- [`tests/test_issue42_skill_blocks.py`](file:///var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T043243Z-baf133/1/tests/test_issue42_skill_blocks.py#L238-L253):
|
||||||
|
Modified `test_claude_skills_link_resolves_to_agents_skills()` to support both local `.claude/skills` layouts:
|
||||||
|
1. A single symlink pointing directly to `.agents/skills` (`link.is_symlink()`).
|
||||||
|
2. A local directory containing individual symlinks for each skill in `SKILL_NAMES`.
|
||||||
|
|
||||||
|
**Product Code Modifications**: None (0 changes to `.sh` or `.py` product code).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Review Item Verification & Empirical Testing
|
||||||
|
|
||||||
|
### Item 1: Behavior Across Both Layouts & Edge Cases
|
||||||
|
Empirical tests were executed inside the verification path across all layouts and boundary cases (with temporary `.claude` structures removed after verification):
|
||||||
|
|
||||||
|
1. **Absent `.claude/skills` (Worktree default)**:
|
||||||
|
- Behavior: Skipped with `pytest.skip(".claude/skills is untracked; absent in a worktree")`.
|
||||||
|
- Result: PASS (skipped cleanly).
|
||||||
|
2. **Single Symlink Layout (`.claude/skills -> .agents/skills`)**:
|
||||||
|
- Tested: `ln -s ../.agents/skills .claude/skills`
|
||||||
|
- Behavior: `link.is_symlink()` evaluates to `True`, verifies `link.resolve() == SKILLS.resolve()`.
|
||||||
|
- Result: PASS.
|
||||||
|
3. **Per-Skill Directory Layout (Control plane / Main checkout)**:
|
||||||
|
- Tested: `.claude/skills/` containing symlinks for all 8 `multi-agent-mux-*` skills plus helper links (`lib.sh`, `lib_py`).
|
||||||
|
- Behavior: Verifies `entry.exists()` and `entry.resolve() == (SKILLS / skill).resolve()` for every skill in `SKILL_NAMES`.
|
||||||
|
- Result: PASS.
|
||||||
|
4. **Missing Skill in Directory Layout (Negative test)**:
|
||||||
|
- Tested: Omitted `multi-agent-mux-stop` symlink.
|
||||||
|
- Behavior: Fails immediately with `AssertionError: .claude/skills/multi-agent-mux-stop is missing`.
|
||||||
|
- Result: PASS (correctly caught).
|
||||||
|
5. **Misdirected Symlink Target (Negative test)**:
|
||||||
|
- Tested: Pointed `multi-agent-mux-stop` to `multi-agent-mux-create`.
|
||||||
|
- Behavior: Fails with `AssertionError: .claude/skills/multi-agent-mux-stop -> .../multi-agent-mux-create`.
|
||||||
|
- Result: PASS (correctly caught).
|
||||||
|
6. **Broken Symlink (Negative test)**:
|
||||||
|
- Tested: Pointed `multi-agent-mux-create` to nonexistent target.
|
||||||
|
- Behavior: `entry.exists()` returns `False`, failing with `AssertionError: .claude/skills/multi-agent-mux-create is missing`.
|
||||||
|
- Result: PASS (correctly caught).
|
||||||
|
7. **Regular File at `.claude/skills` (Negative test)**:
|
||||||
|
- Tested: Created plain file `.claude/skills`.
|
||||||
|
- Behavior: Child path lookup on regular file evaluates `entry.exists() == False`, failing cleanly with `AssertionError: .claude/skills/multi-agent-mux-create is missing`.
|
||||||
|
- Result: PASS (correctly caught).
|
||||||
|
8. **Static Completeness of `SKILL_NAMES`**:
|
||||||
|
- `SKILL_NAMES` is defined from `.agents/skills/multi-agent-mux-*/SKILL.md` and enforced by `test_inventory_covers_eight_skills` (`len == 8`). Cannot be empty.
|
||||||
|
|
||||||
|
### Item 2: Strictness & Flexibility Assessment
|
||||||
|
- **Appropriate Flexibility**: Allows additional local symlinks or files within `.claude/skills` (such as `lib.sh` or `lib_py`) without triggering false alarms, reflecting that Claude Code only registers subdirectories containing `SKILL.md`.
|
||||||
|
- **Appropriate Strictness**: Unconditionally requires that all 8 canonical skills are present and resolve to their exact corresponding `.agents/skills/<skill>` directories.
|
||||||
|
|
||||||
|
### Item 3: Scope & Diff Inspection
|
||||||
|
- Exactly 1 file modified: `tests/test_issue42_skill_blocks.py` (+10, -1).
|
||||||
|
- No changes to product scripts, library code, or documentation.
|
||||||
|
- No version bump.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Test Suite Execution Results
|
||||||
|
|
||||||
|
Executed in verification worktree (`/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T043243Z-baf133/1`) with `--basetemp=/tmp/pytest-pr47`:
|
||||||
|
|
||||||
|
| Test Suite | Result | Duration |
|
||||||
|
|---|---|---|
|
||||||
|
| `tests/test_issue42_skill_blocks.py` | 31 passed, 1 skipped* | 0.07 s |
|
||||||
|
| `tests/test_version_consistency.py` | 3 passed | 0.22 s |
|
||||||
|
|
||||||
|
*\* Note: `test_claude_skills_link_resolves_to_agents_skills` skipped as expected in worktrees lacking untracked `.claude/skills`; empirical tests confirmed passing under both layouts.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Conclusion
|
||||||
|
|
||||||
|
PR #47 cleanly and robustly supports both local layouts of `.claude/skills`, preventing false failures on the main checkout while maintaining strict integrity verification across all canonical skills.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"loop_id": "loop-20261004T044643Z-1b1440",
|
||||||
|
"base_sha": "e1ed2708d835d712f5f3a9dfd51b8f66ca1ac447",
|
||||||
|
"head_sha": "150b4937f816c6407be16fc4c37ab88e478927e7",
|
||||||
|
"tree_sha": "afce44d2e0f91720d81e8d7d484f0e1358dee680",
|
||||||
|
"review_commit": "150b4937f816c6407be16fc4c37ab88e478927e7",
|
||||||
|
"reviewer": "reviewer-grok-01",
|
||||||
|
"job_id": "0b77d5de",
|
||||||
|
"verdict": "PASS",
|
||||||
|
"transport": "herdr",
|
||||||
|
"evidence": "review-tree",
|
||||||
|
"isolation": "worktree",
|
||||||
|
"verified_at": "2026-10-04T04:54:16Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Review: PR #47 round 2 @ 150b493
|
||||||
|
|
||||||
|
- Role: Reviewer (grok)
|
||||||
|
- Identity: base `e1ed2708d835d712f5f3a9dfd51b8f66ca1ac447`, head `150b4937f816c6407be16fc4c37ab88e478927e7`, tree `afce44d2e0f91720d81e8d7d484f0e1358dee680`, 1 commit, clean worktree
|
||||||
|
- Verified read-only at `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T044643Z-1b1440/1`, except temporary `.claude/skills` layouts created there and removed afterward. Main stayed at `6af4a79`. The control-plane `.claude/skills` was only read.
|
||||||
|
- `git diff --stat e1ed270..HEAD`: `tests/test_issue42_skill_blocks.py`, +59/−12. `git diff --check` is clean. No product file and no version line.
|
||||||
|
|
||||||
|
Round 1 on `e1ed270` was PASS from this reviewer. The report noted that a dangling `.claude/skills` symlink skipped, because `Path.exists()` follows the link and is false before `is_symlink()` was considered. Codex R1 is that note. This commit closes it.
|
||||||
|
|
||||||
|
| Round 1 | `150b493` |
|
||||||
|
|---|---|
|
||||||
|
| `exists()` false meant skip, including a dangling top-level symlink | Absence is `not exists() and not is_symlink()` (`tests/test_issue42_skill_blocks.py:248-249`). A symlink is then checked (`250-252`): it must exist and resolve to `.agents/skills`, otherwise the function returns a problem. |
|
||||||
|
| The live test skipped in a worktree, so the dangling case never ran | `test_claude_skills_layouts` builds six layouts under `tmp_path` and calls the same function (`294`). `broken_top` expects a non-empty problem list (`282-283`, `299-300`). `None` (the old skip) and `[]` both fail that assertion. |
|
||||||
|
|
||||||
|
## Same function, deterministic layouts
|
||||||
|
|
||||||
|
`claude_skills_problems` (`240-260`) is the only checker. The live test calls it with `REPO / ".claude" / "skills"`, `SKILLS`, and `SKILL_NAMES` (`263-267`). The synthetic test calls it with the temp link, the temp skills directory, and two fixed names (`274-275`, `294`). Success layouts assert `problems == []`. The absent layout asserts `None`. The three failure layouts assert the list is non-empty. They do not pin the message text. The live test does: a present layout must be exactly `[]`.
|
||||||
|
|
||||||
|
The six layouts are built with relative links inside `tmp_path` (`../.agents/skills`, `../../.agents/skills/<name>`). Nothing in the repo tree is required, so the cases run in a worktree. `broken_top` points at `../.agents/no-such-dir`. `broken_entry` replaces the second skill with a link to `gone`. `wrong_skill` points `multi-agent-mux-b` at `multi-agent-mux-a`.
|
||||||
|
|
||||||
|
## Probes
|
||||||
|
|
||||||
|
Temporary layouts in the snapshot, then removed (`git status` clean, `.claude` gone):
|
||||||
|
|
||||||
|
- No `.claude/skills`: `37 passed, 1 skipped in 0.07s`. The skip is the live test. The six synthetic layouts are among the 37.
|
||||||
|
- Dangling `ln -s ../.agents/skills-missing .claude/skills`: the live test failed at `:267` with a non-empty problem list. It did not skip. `Path.resolve()` on that dangling link returned the missing path and did not raise.
|
||||||
|
- Directory of the eight skill links plus `lib.sh` and `lib_py`: the live test passed.
|
||||||
|
|
||||||
|
The control-plane directory is not a symlink. `claude_skills_problems` on that path, the control-plane `.agents/skills`, and this tree's eight `SKILL_NAMES` returned `[]`.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Review: PR #47 @ e1ed270
|
||||||
|
|
||||||
|
- Role: Reviewer (grok)
|
||||||
|
- Identity: base `6af4a796b1bdce3b2e929b7a45a8a22d66b36855`, head `e1ed2708d835d712f5f3a9dfd51b8f66ca1ac447`, tree `2f781e0db4df9a3082b4f55e69c42118868b18a1`, 1 commit, clean worktree
|
||||||
|
- Verified read-only at `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261004T043243Z-baf133/1`, except temporary `.claude/skills` layouts created there and removed afterward. Main stayed at `6af4a79`. The control-plane `.claude/skills` was only read.
|
||||||
|
- `git diff --stat 6af4a79..HEAD`: `tests/test_issue42_skill_blocks.py`, +10/−1. `git diff --check` is clean. No product file and no version line.
|
||||||
|
|
||||||
|
`.claude/` is gitignored (`.gitignore:40`). This checkout's `.claude/skills` is a real directory, not one symlink. It holds the eight `multi-agent-mux-*` links plus `lib.sh` and `lib_py`, each aimed at `../../.agents/skills/<name>`. The old assertion `link.resolve() == SKILLS.resolve()` fails that directory. A worktree has no `.claude/skills`, so the same test skipped during the PR #46 runs.
|
||||||
|
|
||||||
|
## The two layouts
|
||||||
|
|
||||||
|
`test_claude_skills_link_resolves_to_agents_skills` (`tests/test_issue42_skill_blocks.py:240-253`):
|
||||||
|
|
||||||
|
- Missing path: skip (`245-246`). A worktree stays green.
|
||||||
|
- The path is a symlink: it must resolve to `.agents/skills` (`247-249`). Children are not checked, because they are the repo skills.
|
||||||
|
- Otherwise each name from `SKILL_NAMES` (`:22`, the eight `multi-agent-mux-*/SKILL.md` directories) must exist under `.claude/skills` and resolve to `.agents/skills/<skill>` (`250-253`).
|
||||||
|
|
||||||
|
Temporary layouts in the snapshot, deleted before the end (`git status` clean, `.claude` gone):
|
||||||
|
|
||||||
|
| Layout | Result |
|
||||||
|
|---|---|
|
||||||
|
| absent | skipped |
|
||||||
|
| symlink `../.agents/skills` | passed |
|
||||||
|
| symlink to `multi-agent-mux-create` only | failed at `:248` (create's path `!=` `.agents/skills`) |
|
||||||
|
| directory of the eight links, plus `lib.sh` and `lib_py` | full file **32 passed** |
|
||||||
|
| `multi-agent-mux-stop` linked at `multi-agent-mux-create` | failed at `:253`, and the message shows the create path |
|
||||||
|
| `multi-agent-mux-resume` linked at a missing target | failed at `:252` (`is missing`) |
|
||||||
|
| `.claude/skills` a regular file | failed at `:252` (`multi-agent-mux-create is missing`) |
|
||||||
|
|
||||||
|
The control-plane links resolve to the same comparison: `multi-agent-mux-create` there equals `.agents/skills/multi-agent-mux-create`. Pytest was not run in that checkout.
|
||||||
|
|
||||||
|
## Edges
|
||||||
|
|
||||||
|
- A regular file is not accepted. `is_symlink()` is false, then `link / skill` does not exist, so the test fails.
|
||||||
|
- `lib.sh` and `lib_py` beside the eight links do not fail the test. Neither has a `SKILL.md` (`lib.sh` is a file; `lib_py` is the package). Claude Code loads a skill from a directory that contains `SKILL.md`. Those two entries are not skills, and this checkout has them, so rejecting every extra name would be too strict. The test's job is the eight repo skills. An extra untracked directory that did contain its own `SKILL.md` would not be looked at; that is a local skill outside this check.
|
||||||
|
- A broken per-skill link fails `entry.exists()` (`:252`). `exists()` follows the link, so a dangling symlink is "missing" and the test fails.
|
||||||
|
- A broken symlink at `.claude/skills` itself is skipped. `Path.exists()` is false for a dangling symlink, and that check runs before `is_symlink()` (`245-247`). The previous test skipped the same way. Claude Code would not load skills through that link either. A present-but-wrong root symlink, one whose target exists, still fails the resolve check.
|
||||||
|
- An empty `SKILL_NAMES` would make the directory branch pass without checking any entry. The name list is the eight `SKILL.md` parents, and `test_inventory_covers_eight_skills` in the same file requires eight skills. The directory branch is not empty on this tree.
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
Snapshot, control-plane venv, `--basetemp` under `/tmp/adb45follow.*` (removed after each run):
|
||||||
|
|
||||||
|
- No `.claude/skills`: `31 passed, 1 skipped in 0.06s`. The skip is this test.
|
||||||
|
- The per-skill directory with `lib.sh` and `lib_py`: `32 passed in 0.06s`.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -29,7 +29,7 @@ export WORKSPACE_ROOT
|
|||||||
# Framework semantic version. Single runtime source of truth; kept in lockstep
|
# Framework semantic version. Single runtime source of truth; kept in lockstep
|
||||||
# with VERSIONS.md and the 8 SKILL.md frontmatters by tests/test_version_consistency.py.
|
# with VERSIONS.md and the 8 SKILL.md frontmatters by tests/test_version_consistency.py.
|
||||||
# NOTE: unlike other MAM_* variables this one is intentionally NOT env-overridable.
|
# NOTE: unlike other MAM_* variables this one is intentionally NOT env-overridable.
|
||||||
MAM_VERSION="4.8.3"
|
MAM_VERSION="4.8.4"
|
||||||
export MAM_VERSION
|
export MAM_VERSION
|
||||||
AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}"
|
AGENT_SESSIONS_YAML="${AGENT_SESSIONS_YAML:-$WORKSPACE_ROOT/.mam/agent-sessions.yaml}"
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import sys
|
|||||||
from typing import List, Sequence
|
from typing import List, Sequence
|
||||||
|
|
||||||
# Framework version locked with framework constants
|
# Framework version locked with framework constants
|
||||||
FRAMEWORK_VERSION = "4.8.3"
|
FRAMEWORK_VERSION = "4.8.4"
|
||||||
|
|
||||||
CORE_MODULES: Sequence[str] = (
|
CORE_MODULES: Sequence[str] = (
|
||||||
# Standard library modules
|
# Standard library modules
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: multi-agent-mux-create
|
name: multi-agent-mux-create
|
||||||
description: "Create a new agent session (claude, antigravity/agy) in a dedicated herdr session for context-preserving long-running work. Always creates a herdr session — never backgrounds with nohup/disown. Writes the new session to .mam/agent-sessions.yaml. Use when you want to start a fresh agent (no prior UUID) for a new project workspace."
|
description: "Create a new agent session (claude, antigravity/agy) in a dedicated herdr session for context-preserving long-running work. Always creates a herdr session — never backgrounds with nohup/disown. Writes the new session to .mam/agent-sessions.yaml. Use when you want to start a fresh agent (no prior UUID) for a new project workspace."
|
||||||
version: 4.8.3
|
version: 4.8.4
|
||||||
author: godopu
|
author: godopu
|
||||||
license: MIT
|
license: MIT
|
||||||
platforms: [linux, macos]
|
platforms: [linux, macos]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: multi-agent-mux-delegate-job
|
name: multi-agent-mux-delegate-job
|
||||||
description: "Delegate a unit of work to any autonomous agent (claude-code, hermes, agy, grok-build, codex, or a human) and observe it asynchronously over an MQTT event channel. Supported roles include orchestrator, worker, and reviewer."
|
description: "Delegate a unit of work to any autonomous agent (claude-code, hermes, agy, grok-build, codex, or a human) and observe it asynchronously over an MQTT event channel. Supported roles include orchestrator, worker, and reviewer."
|
||||||
version: 4.8.3
|
version: 4.8.4
|
||||||
author: godopu
|
author: godopu
|
||||||
license: MIT
|
license: MIT
|
||||||
platforms: [linux, macos, windows]
|
platforms: [linux, macos, windows]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: multi-agent-mux-loop
|
name: multi-agent-mux-loop
|
||||||
description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached."
|
description: "Run an autonomous planning-execution-review loop using multiple agents (Planner, Creator, Reviewers) in the workspace. Automatically orchestrates plan discussion, code changes, and peer reviews until a unanimous PASS is achieved or the maximum iteration limit is reached."
|
||||||
version: 4.8.3
|
version: 4.8.4
|
||||||
author: godopu
|
author: godopu
|
||||||
license: MIT
|
license: MIT
|
||||||
platforms: [linux, macos]
|
platforms: [linux, macos]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: multi-agent-mux-monitor
|
name: multi-agent-mux-monitor
|
||||||
description: "Run a long-lived reconciler that watches .mam/agent-sessions.yaml against the actual herdr/agent runtime state and reconciles them. Use when you want live visibility into which agent sessions are running, which are dead, which have stale YAML entries, and which have new session ids that haven't been recorded yet. Runs as a persistent loop (`reconcile.sh --subscribe`) that keeps going until it times out, idles out, or is interrupted."
|
description: "Run a long-lived reconciler that watches .mam/agent-sessions.yaml against the actual herdr/agent runtime state and reconciles them. Use when you want live visibility into which agent sessions are running, which are dead, which have stale YAML entries, and which have new session ids that haven't been recorded yet. Runs as a persistent loop (`reconcile.sh --subscribe`) that keeps going until it times out, idles out, or is interrupted."
|
||||||
version: 4.8.3
|
version: 4.8.4
|
||||||
author: godopu
|
author: godopu
|
||||||
license: MIT
|
license: MIT
|
||||||
platforms: [linux, macos]
|
platforms: [linux, macos]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: multi-agent-mux-orc-onboard
|
name: multi-agent-mux-orc-onboard
|
||||||
description: "Register current or specified orchestrator session UUID into agent-sessions.yaml orchestrator_uuids list to prevent sub-agent discovery capture."
|
description: "Register current or specified orchestrator session UUID into agent-sessions.yaml orchestrator_uuids list to prevent sub-agent discovery capture."
|
||||||
version: 4.8.3
|
version: 4.8.4
|
||||||
author: godopu
|
author: godopu
|
||||||
license: MIT
|
license: MIT
|
||||||
platforms: [linux, macos]
|
platforms: [linux, macos]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: multi-agent-mux-resume
|
name: multi-agent-mux-resume
|
||||||
description: "Resume an existing agent (claude, antigravity/agy) conversation by UUID into a herdr session. Reads .mam/agent-sessions.yaml for the saved session/conversation id, spawns (or reuses) a herdr session of the matching name, and runs `claude -r <id>` or `agy --conversation <id>` inside. Use when you want to reattach to a previous session's context, or revive a session whose herdr died but the agent's conversation is still on disk."
|
description: "Resume an existing agent (claude, antigravity/agy) conversation by UUID into a herdr session. Reads .mam/agent-sessions.yaml for the saved session/conversation id, spawns (or reuses) a herdr session of the matching name, and runs `claude -r <id>` or `agy --conversation <id>` inside. Use when you want to reattach to a previous session's context, or revive a session whose herdr died but the agent's conversation is still on disk."
|
||||||
version: 4.8.3
|
version: 4.8.4
|
||||||
author: godopu
|
author: godopu
|
||||||
license: MIT
|
license: MIT
|
||||||
platforms: [linux, macos]
|
platforms: [linux, macos]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: multi-agent-mux-status
|
name: multi-agent-mux-status
|
||||||
description: "Read-only instant snapshot of all agent herdr sessions — name, YAML status, herdr alive, pane cmd/cwd, resume UUID on disk, and any drift. No mutation. Reuses reconcile.sh --dry-run for the diff logic. Use when you want to know 'what's running RIGHT NOW' without spinning up the monitor loop."
|
description: "Read-only instant snapshot of all agent herdr sessions — name, YAML status, herdr alive, pane cmd/cwd, resume UUID on disk, and any drift. No mutation. Reuses reconcile.sh --dry-run for the diff logic. Use when you want to know 'what's running RIGHT NOW' without spinning up the monitor loop."
|
||||||
version: 4.8.3
|
version: 4.8.4
|
||||||
author: godopu
|
author: godopu
|
||||||
license: MIT
|
license: MIT
|
||||||
platforms: [linux, macos]
|
platforms: [linux, macos]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: multi-agent-mux-stop
|
name: multi-agent-mux-stop
|
||||||
description: "Stop an agent herdr session (claude, antigravity/agy) and update .mam/agent-sessions.yaml. Default stops gracefully and marks status=stopped with conversation preserved for resume. Does NOT delete on-disk conversation artifacts (jsonl/db) — those are preserved unless --purge-conversation is passed. Use when ending a work session, switching to a different one, or cleaning up before a fresh start."
|
description: "Stop an agent herdr session (claude, antigravity/agy) and update .mam/agent-sessions.yaml. Default stops gracefully and marks status=stopped with conversation preserved for resume. Does NOT delete on-disk conversation artifacts (jsonl/db) — those are preserved unless --purge-conversation is passed. Use when ending a work session, switching to a different one, or cleaning up before a fresh start."
|
||||||
version: 4.8.3
|
version: 4.8.4
|
||||||
author: godopu
|
author: godopu
|
||||||
license: MIT
|
license: MIT
|
||||||
platforms: [linux, macos]
|
platforms: [linux, macos]
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
본 문서는 `herdr_agent_orchestration` 오케스트레이션 및 메시징 백플레인 워크플로우를 새로운 프로젝트에 도입하여 이식하고, 새로운 개발자/에이전트가 초기 가동을 시작할 때 수행해야 하는 환경 설정 및 구축 절차를 안내합니다.
|
본 문서는 `herdr_agent_orchestration` 오케스트레이션 및 메시징 백플레인 워크플로우를 새로운 프로젝트에 도입하여 이식하고, 새로운 개발자/에이전트가 초기 가동을 시작할 때 수행해야 하는 환경 설정 및 구축 절차를 안내합니다.
|
||||||
|
|
||||||
[](VERSIONS.md)
|
[](VERSIONS.md)
|
||||||
|
|
||||||
새로운 에이전트는 이 안내서에 기술된 절차를 순차적으로 실행하여 초기 환경을 안정적으로 설정할 수 있습니다.
|
새로운 에이전트는 이 안내서에 기술된 절차를 순차적으로 실행하여 초기 환경을 안정적으로 설정할 수 있습니다.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
This document guides you through the setup and initialization procedures required to adopt the `herdr_agent_orchestration` orchestration and messaging backplane workflow in a new project, enabling a new developer or agent to get up and running quickly.
|
This document guides you through the setup and initialization procedures required to adopt the `herdr_agent_orchestration` orchestration and messaging backplane workflow in a new project, enabling a new developer or agent to get up and running quickly.
|
||||||
|
|
||||||
[](VERSIONS.md)
|
[](VERSIONS.md)
|
||||||
|
|
||||||
A new agent can follow the steps in this guide sequentially to establish a stable and reliable initial environment.
|
A new agent can follow the steps in this guide sequentially to establish a stable and reliable initial environment.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Messaging System Technical Analysis & Architecture Report
|
# Messaging System Technical Analysis & Architecture Report
|
||||||
|
|
||||||
Applies to: MAM v4.8.3
|
Applies to: MAM v4.8.4
|
||||||
|
|
||||||
This report provides a comprehensive, deep-dive analysis of the messaging system implemented in the `multi-agent-mux-delegate-job` skill. It covers the MQTT broker architecture, event protocols, job lifecycles, codebase internals, cross-system integration, and a list of known limitations along with production recommendations.
|
This report provides a comprehensive, deep-dive analysis of the messaging system implemented in the `multi-agent-mux-delegate-job` skill. It covers the MQTT broker architecture, event protocols, job lifecycles, codebase internals, cross-system integration, and a list of known limitations along with production recommendations.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Herdr와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이전트 오케스트레이션 및 메시징 백플레인** 프레임워크입니다. `claude`, `agy`, `hermes`, `grok`, `opencode`, `codex` 등 다중 LLM 백엔드 에이전트 전반에 걸쳐 장시간 수행되는 작업(코드 생성, 리팩토링, 보안 검토 등)을 조정, 격리 및 감사할 수 있도록 설계되었습니다.
|
Herdr와 MQTT 브로커를 기반으로 구축된 고신뢰성 **다중 에이전트 오케스트레이션 및 메시징 백플레인** 프레임워크입니다. `claude`, `agy`, `hermes`, `grok`, `opencode`, `codex` 등 다중 LLM 백엔드 에이전트 전반에 걸쳐 장시간 수행되는 작업(코드 생성, 리팩토링, 보안 검토 등)을 조정, 격리 및 감사할 수 있도록 설계되었습니다.
|
||||||
|
|
||||||
[](VERSIONS.md)
|
[](VERSIONS.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
An advanced, high-reliability **Multi-Agent Orchestration & Messaging Backplane** framework built on Herdr and MQTT. It is designed to coordinate, isolate, and audit long-running agent tasks (such as code generation, refactoring, and security reviews) across multiple LLM backend clients (`claude`, `agy`, `hermes`, `grok`, `opencode`, `codex`).
|
An advanced, high-reliability **Multi-Agent Orchestration & Messaging Backplane** framework built on Herdr and MQTT. It is designed to coordinate, isolate, and audit long-running agent tasks (such as code generation, refactoring, and security reviews) across multiple LLM backend clients (`claude`, `agy`, `hermes`, `grok`, `opencode`, `codex`).
|
||||||
|
|
||||||
[](VERSIONS.md)
|
[](VERSIONS.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+23
-10
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## 📌 현재 버전 개요 (Current Release)
|
## 📌 현재 버전 개요 (Current Release)
|
||||||
|
|
||||||
- **프레임워크 버전**: `v4.8.3`
|
- **프레임워크 버전**: `v4.8.4`
|
||||||
- **최신 릴리스 일시**: 2026-10-04 (KST)
|
- **최신 릴리스 일시**: 2026-10-04 (KST)
|
||||||
- **기준 브랜치**: `main`
|
- **기준 브랜치**: `main`
|
||||||
- **핵심 아키텍처**:
|
- **핵심 아키텍처**:
|
||||||
@@ -24,23 +24,36 @@
|
|||||||
|
|
||||||
## 🧭 스킬 패키지 버전 매트릭스 (Skills Version Matrix)
|
## 🧭 스킬 패키지 버전 매트릭스 (Skills Version Matrix)
|
||||||
|
|
||||||
모든 8개 스킬은 YAML frontmatter 메타데이터(`author`, `version`, `platforms`, `environments`) 표준화를 통해 `v4.8.3`로 동기화되어 배포됩니다.
|
모든 8개 스킬은 YAML frontmatter 메타데이터(`author`, `version`, `platforms`, `environments`) 표준화를 통해 `v4.8.4`로 동기화되어 배포됩니다.
|
||||||
|
|
||||||
| 스킬명 | 버전 | 역할 및 주요 책임 | 상태 |
|
| 스킬명 | 버전 | 역할 및 주요 책임 | 상태 |
|
||||||
| :--- | :---: | :--- | :---: |
|
| :--- | :---: | :--- | :---: |
|
||||||
| **`multi-agent-mux-create`** | `4.8.3` | 에이전트 세션 신규 생성 및 Herdr 컨테이너 격리 스폰 | ✅ 배포 |
|
| **`multi-agent-mux-create`** | `4.8.4` | 에이전트 세션 신규 생성 및 Herdr 컨테이너 격리 스폰 | ✅ 배포 |
|
||||||
| **`multi-agent-mux-stop`** | `4.8.3` | 대화 UUID 원자적 캡처 및 세션 안전 종료 (Graceful Stop) | ✅ 배포 |
|
| **`multi-agent-mux-stop`** | `4.8.4` | 대화 UUID 원자적 캡처 및 세션 안전 종료 (Graceful Stop) | ✅ 배포 |
|
||||||
| **`multi-agent-mux-resume`** | `4.8.3` | 온디스크 대화 컨텍스트 기반 Tier-1 초고속 세션 복원 | ✅ 배포 |
|
| **`multi-agent-mux-resume`** | `4.8.4` | 온디스크 대화 컨텍스트 기반 Tier-1 초고속 세션 복원 | ✅ 배포 |
|
||||||
| **`multi-agent-mux-status`** | `4.8.3` | 실시간 Herdr 세션 및 레지스트리 드리프트 스냅샷 조회 | ✅ 배포 |
|
| **`multi-agent-mux-status`** | `4.8.4` | 실시간 Herdr 세션 및 레지스트리 드리프트 스냅샷 조회 | ✅ 배포 |
|
||||||
| **`multi-agent-mux-monitor`** | `4.8.3` | YAML ↔ 런타임 상태 간 자율 조정자 (Reconciler Loop) | ✅ 배포 |
|
| **`multi-agent-mux-monitor`** | `4.8.4` | YAML ↔ 런타임 상태 간 자율 조정자 (Reconciler Loop) | ✅ 배포 |
|
||||||
| **`multi-agent-mux-delegate-job`** | `4.8.3` | MQTT 이벤트 채널 기반 비동기 단위 작업 위임 | ✅ 배포 |
|
| **`multi-agent-mux-delegate-job`** | `4.8.4` | MQTT 이벤트 채널 기반 비동기 단위 작업 위임 | ✅ 배포 |
|
||||||
| **`multi-agent-mux-loop`** | `4.8.3` | Planner-Creator-Reviewer 3자 자율 계획·실행·피어리뷰 루프 | ✅ 배포 |
|
| **`multi-agent-mux-loop`** | `4.8.4` | Planner-Creator-Reviewer 3자 자율 계획·실행·피어리뷰 루프 | ✅ 배포 |
|
||||||
| **`multi-agent-mux-orc-onboard`** | `4.8.3` | 오케스트레이터 UUID 격리 등록 및 서브 세션 오염 방지 | ✅ 배포 |
|
| **`multi-agent-mux-orc-onboard`** | `4.8.4` | 오케스트레이터 UUID 격리 등록 및 서브 세션 오염 방지 | ✅ 배포 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 버전별 상세 변경 내역 (Changelog)
|
## 📋 버전별 상세 변경 내역 (Changelog)
|
||||||
|
|
||||||
|
### 🚀 `v4.8.4` — `.claude/skills` 링크 검사가 두 로컬 배치를 받고 깨진 링크를 잡는다 (2026-10-04, PR #47)
|
||||||
|
|
||||||
|
> **패치 (PATCH Release)**: v4.8.3(PR #46)이 넣은 `test_claude_skills_link_resolves_to_agents_skills` 가 병합 후 본 체크아웃에서 실패했다. 테스트는 `.claude/skills` 를 `.agents/skills` 로 가는 링크 하나로 가정했지만, 이 로컬(git-ignored) 디렉터리는 스킬별 링크를 담은 디렉터리일 수도 있다. PR #46 의 리뷰·스위트는 워크트리에서 돌아 이 테스트가 skip 이었다. 테스트만 고친다.
|
||||||
|
|
||||||
|
#### 🌟 주요 개선 사항
|
||||||
|
1. **두 배치 (`tests/test_issue42_skill_blocks.py`)**: 단일 링크(→ `.agents/skills`) 또는 여덟 스킬 각각이 `.agents/skills/<skill>` 로 해석되는 디렉터리를 모두 받는다.
|
||||||
|
2. **깨진 링크는 실패 (리뷰 codex R1)**: `Path.exists()` 는 링크를 따라가므로 깨진 최상위 링크가 "없음"으로 skip 됐다. 부재는 "존재하지도 않고 링크도 아님"일 때만이다.
|
||||||
|
3. **워크트리에서도 검사**: 검사를 `claude_skills_problems()` 로 분리하고 합성 배치 6 개(없음, 단일 링크, 스킬별 링크, 다른 스킬을 가리키는 링크, 깨진 최상위 링크, 깨진 스킬 링크)로 시험한다.
|
||||||
|
|
||||||
|
#### ✅ 품질 검증
|
||||||
|
- **리뷰 (네 리뷰어)**: 1회차(@e1ed270) claude·agy·grok PASS, codex NOT PASS(R1 깨진 최상위 링크 skip). 150b493 에서 수정. 2회차 **4/4 PASS**. 리포트와 2회차 identity 사이드카는 `.agents/reports/<session>/`.
|
||||||
|
- **테스트**: `tests/test_issue42_skill_blocks.py` 37 passed / 1 skipped(워크트리). codex 의 탐침(트리에 깨진 `.claude/skills`)은 이제 실패, 본 체크아웃의 스킬별 배치는 문제 없음. 테스트 함수 하나만 바뀌어 전체 스위트는 돌리지 않았다(직전 전체 스위트: v4.8.3 후보 1408 passed / 0 failed).
|
||||||
|
|
||||||
### 🚀 `v4.8.3` — Issue #42 단계 1·2: SKILL.md 코드 블록 목록을 테스트 데이터로, 스크립트가 이미 하는 블록 삭제 (2026-10-04, PR #46)
|
### 🚀 `v4.8.3` — Issue #42 단계 1·2: SKILL.md 코드 블록 목록을 테스트 데이터로, 스크립트가 이미 하는 블록 삭제 (2026-10-04, PR #46)
|
||||||
|
|
||||||
> **패치 (PATCH Release)**: 스킬 절차를 SKILL.md 의 코드 블록으로 다시 적어 에이전트가 손으로 따라 하게 하던 곳 가운데, 진입 스크립트가 이미 그대로 하는 블록을 지우고 호출 한 줄과 옵션·종료 코드 표로 바꿨다(Issue #42 의 단계 1·2). 동작 변경 없음. 단계 3~5(스크립트에 없는 절차의 코드화, bash 진입점 정리, 문서 가드 확장)는 #17 항목 4 다음에 진행한다.
|
> **패치 (PATCH Release)**: 스킬 절차를 SKILL.md 의 코드 블록으로 다시 적어 에이전트가 손으로 따라 하게 하던 곳 가운데, 진입 스크립트가 이미 그대로 하는 블록을 지우고 호출 한 줄과 옵션·종료 코드 표로 바꿨다(Issue #42 의 단계 1·2). 동작 변경 없음. 단계 3~5(스크립트에 없는 절차의 코드화, bash 진입점 정리, 문서 가드 확장)는 #17 항목 4 다음에 진행한다.
|
||||||
|
|||||||
@@ -237,8 +237,64 @@ def test_frontmatter_name_matches_directory(skill):
|
|||||||
assert m and m.group(1) == skill
|
assert m and m.group(1) == skill
|
||||||
|
|
||||||
|
|
||||||
|
def claude_skills_problems(link, skills_dir, names):
|
||||||
|
"""None when ``link`` (.claude/skills) is absent; else the list of problems.
|
||||||
|
|
||||||
|
.claude/skills is local (git-ignored) and comes in two layouts: one link to
|
||||||
|
.agents/skills, or a directory with one link per skill. Either way each
|
||||||
|
skill Claude Code loads must be the .agents/skills one. A dangling
|
||||||
|
top-level link is a broken layout, not an absent one: Path.exists()
|
||||||
|
follows the link, so absence is "neither exists nor is a symlink"."""
|
||||||
|
if not link.exists() and not link.is_symlink():
|
||||||
|
return None
|
||||||
|
if link.is_symlink():
|
||||||
|
ok = link.exists() and link.resolve() == skills_dir.resolve()
|
||||||
|
return [] if ok else [f"{link} -> {link.resolve()} is not {skills_dir}"]
|
||||||
|
problems = []
|
||||||
|
for skill in names:
|
||||||
|
entry = link / skill
|
||||||
|
if not entry.exists():
|
||||||
|
problems.append(f"{skill} is missing or dangling")
|
||||||
|
elif entry.resolve() != (skills_dir / skill).resolve():
|
||||||
|
problems.append(f"{skill} -> {entry.resolve()}")
|
||||||
|
return problems
|
||||||
|
|
||||||
|
|
||||||
def test_claude_skills_link_resolves_to_agents_skills():
|
def test_claude_skills_link_resolves_to_agents_skills():
|
||||||
link = REPO / ".claude" / "skills"
|
problems = claude_skills_problems(REPO / ".claude" / "skills", SKILLS, SKILL_NAMES)
|
||||||
if not link.exists():
|
if problems is None:
|
||||||
pytest.skip(".claude/skills is untracked; absent in a worktree")
|
pytest.skip(".claude/skills is untracked; absent in a worktree")
|
||||||
assert link.resolve() == SKILLS.resolve()
|
assert problems == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("layout,ok", [("absent", None), ("single", True), ("per_skill", True),
|
||||||
|
("wrong_skill", False), ("broken_top", False), ("broken_entry", False)])
|
||||||
|
def test_claude_skills_layouts(tmp_path, layout, ok):
|
||||||
|
"""The check on synthetic layouts, so it is exercised in a worktree too."""
|
||||||
|
skills = tmp_path / ".agents" / "skills"
|
||||||
|
names = ["multi-agent-mux-a", "multi-agent-mux-b"]
|
||||||
|
for n in names:
|
||||||
|
(skills / n).mkdir(parents=True)
|
||||||
|
link = tmp_path / ".claude" / "skills"
|
||||||
|
link.parent.mkdir()
|
||||||
|
if layout == "single":
|
||||||
|
link.symlink_to("../.agents/skills")
|
||||||
|
elif layout == "broken_top":
|
||||||
|
link.symlink_to("../.agents/no-such-dir")
|
||||||
|
elif layout in ("per_skill", "wrong_skill", "broken_entry"):
|
||||||
|
link.mkdir()
|
||||||
|
for n in names:
|
||||||
|
(link / n).symlink_to(f"../../.agents/skills/{n}")
|
||||||
|
if layout == "wrong_skill":
|
||||||
|
(link / names[1]).unlink()
|
||||||
|
(link / names[1]).symlink_to(f"../../.agents/skills/{names[0]}")
|
||||||
|
if layout == "broken_entry":
|
||||||
|
(link / names[1]).unlink()
|
||||||
|
(link / names[1]).symlink_to("../../.agents/skills/gone")
|
||||||
|
problems = claude_skills_problems(link, skills, names)
|
||||||
|
if ok is None:
|
||||||
|
assert problems is None
|
||||||
|
elif ok:
|
||||||
|
assert problems == []
|
||||||
|
else:
|
||||||
|
assert problems, layout
|
||||||
Reference in new issue
Block a user