CLAUDE.md@.claude · diff
git:20260808.5c6631a to git:20260817.ec35524
2 added, 0 removed. Audit A to A.
# hi-vibe
## 개요
Claude Code용 "바이브 코딩 안전벨트" 플러그인. AI가 자주 생략하는 검색·기록·
검증 습관을 **문서 자동화 + AI 규율 + 기계 강제(훅·선택형 lint/CI)** 3층으로
워크플로에 끼워 넣는다. 주 대상은 **Python 개인·소규모 프로젝트**. 이 저장소는
그 플러그인 자체의 소스다. **문서 체계(CLAUDE.md·CHANGELOG·MODULE.md)는 hi-vibe
방식을 그대로 쓰지만, 훅은 여기서 켜지 않는다** — 만드는 중인 버전이 만드는 곳에
파일을 쓰면 결함이 곧바로 이 저장소에 남는다(2026-08-02에 Bash 명령 원문이
handover로 복사되는 유출이 있었다). 게다가 훅은 **설치된 캐시 버전**에서 돌므로
지금 고치는 소스가 아니라 옛 버전이 이 저장소를 검사하게 된다.
**`doctor`의 "아직 init 안 함"은 정상이고 의도한 상태다** — 켜지 말 것.
## 핵심 요구사항
- **핵심 훅·스캐너는 Python 표준 라이브러리만** 쓴다 (외부 패키지 0). 이걸 깨면 설치 부담 약속이 무너진다.
- **훅은 항상 fail-open(exit 0)** — 어떤 예외도 호스트(Claude Code)를 중단시키면 안 된다.
- **프로젝트별 opt-in** — 훅은 `.hi-vibe/`가 있는 프로젝트에서만 동작한다.
- **PostToolUse는 `Write|Edit|MultiEdit`만 본다(Claude Code 계약).** Bash로 들어온 변경은 훅에 안 잡히므로, 그 구멍은 다른 층에서 메운다 — Stop 훅이 Bash 명령까지 보고(`_common.bash_wrote_files`), 비밀키는 `check`의 저장소 전체 스캔이 유일한 그물이다. **훅에만 의존하는 안전장치를 새로 만들지 말 것.** `bash_wrote_files`는 **대표적인 쓰기 명령을 추정할 뿐 완전하지 않다**(`perl -pi`·`git apply`·프로젝트 전용 CLI·빌드 도구는 빠진다) — 문서에 "Bash 수정도 전부 즉시 검사한다"고 쓰지 말 것. <!-- hi-vibe: allow-overclaim (금지 문구 인용) -->
- **`SessionEnd` 훅은 전부 합쳐 1.5초 예산을 나눠 쓴다(Claude Code 계약).** 나가는 길을 붙잡는 자리이므로 여기에 무거운 일을 넣지 말 것 — 지금은 트랜스크립트 파싱+파일 쓰기로 실측 0.03초다. 그리고 이 훅은 **무엇도 막지 못한다**(exit 2도 stderr만). 막아야 하는 검사를 여기 붙이지 말 것.
- **안전장치를 사람 주의력에 기대지 않는다.** 알림은 쌓이면 신호가 아니다. 할 수 있으면 대신 하고(Stop 훅이 턴을 막고 리뷰를 지시 — 수행은 AI), 못 하면 사용자가 있는 자리로 가져온다(CI 실패·훅 사망을 대화창에서 알림). "알려줬는데 안 봤다"는 설계 실패지 사용자 잘못이 아니다.
- **과장 금지** — README·랜딩은 구현이 실제로 하는 것만 말한다. 기계 강제와 AI 규율을 뭉뚱그리지 않는다.
- **중복·유사 함수 탐지는 Python(AST) 전용.** JS/TS는 심볼·이름 충돌·파일 크기만. 이 경계를 넓게 읽히게 쓰지 말 것.
- **임계값 하드코딩으로 자기 편하게 만들지 말 것** — 스캐너 기준(400줄/60줄 등)은 근본 원인 판단용이지 무르게 조정하는 가드레일이 아니다.
- **규칙을 추가할 땐 먼저 물어라 — "하나를 빼거나 `references/`로 밀 수 있는가?"** 절대 법칙은 아니다(독립적으로 필요한 안전 규칙이면 순증가가 맞을 수 있다). 다만 기본값은 "**순증가에는 근거가 필요하다**"입니다. 특히 `write-gate`는 엄격하게 — Stop 훅이 리뷰를 강제하므로 코드 쓰는 턴마다 통째로 로드된다. 지시가 길어지면 모델이 핵심 원칙과 사소한 예시를 **같은 무게로** 받아들여 정작 중요한 규칙을 덜 지킨다. 지금 필요한 건 기능 추가가 아니라 **성장 억제**다. 줄 수로 자르지 말고 아래 증상으로 판단한다: ①적힌 핵심 규칙을 자주 놓침 ②비슷한 상황에서 판단이 오락가락 ③사소한 것까지 매번 장황 ④새 규칙이 기존 규칙과 충돌 ⑤리뷰 전 로딩·판단 시간이 계속 늘어남. `SKILL.md`에는 **판단에 필요한 원칙·순서·분기만**, 긴 체크리스트·사례·보고 형식은 `references/`로(repo-xray가 이미 그렇게 한다).
- 버전을 올릴 땐 `plugin.json` version + CHANGELOG를 같은 커밋에. showcase(랜딩)·release(GitHub Release)는 거기서 자동 파생된다.
## 실행 방법
```bash
python3 -m unittest discover -s tests # 전체 테스트 (CI: Python 3.8·3.9·3.12)
python3 scripts/doctor.py --root . # 훅·스캐너 실제 실행 자가진단
python3 skills/repo-xray/scripts/audit.py scan --root . # 구조 스캔
```
## 함정
<!-- 폴더 목록은 여기 없다 — `ls`로 1초면 보이고 금방 낡는다.
코드만 봐서는 모르는 것만 적는다. -->
- **두 에이전트의 차이는 "의심 대상"이다.** `fresh-eyes`는 **코드**를 의심하고(의도가 필요), `proof-eyes`는 **스캐너**를 의심한다(증거가 필요). 이름만 보고 둘을 바꿔 쓰면 리뷰가 헛돈다.
- **`test_command_modes.py`의 `COMMAND_MODE`가 자동/직접 분류의 단일 기준이다.** 새 명령을 만들면 거기 먼저 적고 문서를 맞춘다. 반대로 하면 문서 네 곳이 조용히 갈린다(실제로 세 번 겪었다).
- **`write-gate/SKILL.md`의 재발 유형은 "한 파일 안에서 두 문단이 반대를 시키는 것"이다.** 세 번 났다(폐기한 기능을 charter가 계속 요구 · `107`의 "고르라고 묻지 않는다" vs `288`의 안내 문구 · `277`의 "중복이니 한 곳만" vs `325`의 "`👋` 줄을 붙여라"). **문단을 새로 넣을 땐 그 문단이 무엇을 금지하는지 목록으로 적고, 그 목록이 다른 절의 지시와 겹치는지 본다.** 특히 `👋` 줄처럼 **설명이 아닌 것**(세는 표시·형식 계약)을 설명 규칙에 섞지 말 것.
- **`hooks/scripts/_common.py`는 재수출 표면이다(정의는 `_base`·`_ci`·`_transcript`·`_agent_watch`·`_handover`).** 새 함수는 주제 모듈에 정의하고 `_common`에 임포트 한 줄. **테스트에서 함수를 바꿔치기할 땐 정의된 모듈을 patch할 것** — `_common._run_gh_json`을 바꿔도 `_ci` 안의 호출은 원본을 본다(실제로 4개 테스트가 이걸로 깨졌다).
- **`doctor.py`는 두 얼굴이다** — 사용자용 `/hi-vibe:doctor`와, 스킬이 세션당 한 번 부르는 생존 확인. 후자는 사용자가 치는 명령이 아니다.
- **랜딩(`docs/index.html`)의 업데이트 타임라인은 손으로 고치지 마라** — CHANGELOG의 `show:ko`/`show:en`에서 자동 생성된다.
- **README는 설치·첫 실행만 담는다(v0.37.0~).** 명령어 분류표·평가 프롬프트·기능 설명의 **유일본은 랜딩**(`docs/index.html`)이다. README에 다시 넣지 마라 — 검사받지 않는 사본이 되어 조용히 갈린다(`test_command_modes`·`test_eval_prompt_sync`가 "없는 상태"를 지킨다). 그 대가로 **랜딩이 죽으면 README만으로는 알 수 없다**는 것도 안 채로 정한 것이다.
- **테스트가 문서의 모양을 붙잡게 두지 마라.** 예전엔 검사가 README 표를 읽어서, README를 사용자에게 맞게 줄일 수가 없었다. 주장을 지우면 검사도 그 자리를 떠나야 한다. **"검사 범위가 좁아지면 안 된다"는 원칙은 *주장이 남아 있는데 검사만 뺄 때* 적용된다** — 주장 자체를 지운 경우와 헷갈리지 말 것.
- **동작 하나를 설명하는 문장은 여러 곳에 산다.** 랜딩 한/영 · 해당 스킬 · 템플릿 · **훅 안의 문자열 상수** · 명령 `.md` · **`scripts/doctor.py`(문구와 실행 목록 양쪽)**. 훅을 하나 늘렸더니 "훅 4종"이 정확히 열 곳에 있었다. 동작을 바꾸면 그 전부를 봐야 한다. 실제로 `handover`가 무엇을 남기나 하나를 고치는 데 세 릴리스가 걸렸다 — 훅 문자열과 랜딩 "세 겹"을 몰랐기 때문이다.
- **문구가 아니라 주장으로 찾아라.** 같은 주장이 자리마다 다른 말로 적혀 있다("맥락 안 잃게" / "까먹지 않아요" / "맥락이 안 끊긴다" / "never loses the thread") <!-- hi-vibe: allow-overclaim (금지 문구 인용) -->. 방금 고친 문구로 grep하면 나머지가 안 걸린다. 주어(handover·CHANGELOG·review…)로 훑어라.
## 결정 기록
- **`audit.py`(스캐너)가 자기 스캔에서 oversized 후보(400줄 기준 초과)로 잡히지만 유지한다.** 응집도 높은 단일 책임(저장소 구조 스캔) 파일이고, 60줄 초과 함수는 `find_near_duplicates`(알고리즘)·`cmd_scan` 둘뿐이다. 스캐너는 "삭제 판정"이 아니라 "검토 후보"를 줄 뿐이며 판단은 사람이 한다 — 이건 그 철학의 dogfooding 예다. (테스트 파일 2개도 같은 이유로 후보에 잡히지만 유지.) **다음에 또 "쪼개자"고 하지 말 것.**
+ - **`fresh-eyes`의 판단 항목은 5개에서 4개로 줄였고, 1번은 "끝까지 갔나"다** (2026-08-17). 서로 다른 두 프로젝트(정적 사이트 하나·모의투자 서비스 하나)에서 하루씩 실사용한 기록을 대조한 결과다. **적중한 발견은 거의 전부 "절반만 고침 / 파일 사이 어긋남"이었고**(캐시버스팅 `?v=` 미갱신, `.js`/`.mjs` 짝 반쪽, `import` 누락, 값과 반대인 주석), **틀리거나 값이 낮았던 것은 과잉설계·스코프 크립 쪽**이었다(사용자가 같은 턴에 요청한 것을 스코프 크립으로 지적한 오탐 포함). 그런데 정작 잘 잡는 그것을 **지침이 시키지 않고 있었다.** 그래서 ①"끝까지 갔나"를 1번으로 신설 ②`숨은 결합` 삭제(`write-gate` 체크리스트 7번과 **문장이 같았다** — 사용자가 같은 지적을 두 번 읽는다) ③`미래의 발목` 삭제(두 세션 0건, "다음 변경을 어렵게 만드나"는 반박이 불가능해 채워 넣기 좋은 자리) ④나머지 3개는 강등. **과잉설계·스코프 크립을 지우지 않은 이유**: 두 기록 다 *버그 수정* 세션이라 기능을 짓는 상황을 표본에 못 넣었다. 기능 세션에서 다시 재기 전엔 지우지 말 것. **"이도저도 아닌 리뷰"로 되돌리지 말 것** — 항목을 늘리자는 제안이 오면 이 기록을 먼저 볼 것.
+
## 문서 규칙
<!-- 이 파일이 왜 `.claude/` 안에 있나: 플러그인 루트의 `CLAUDE.md`는
`claude plugin validate --strict`가 경고한다("플러그인 컨텍스트로 로드되지
않으니 스킬을 쓰라"). 이 파일은 배포용이 아니라 **이 저장소가 자기를
hi-vibe로 관리하는** 파일이라 스킬로 바꿀 수 없다. 공식 문서상
`./CLAUDE.md`와 `./.claude/CLAUDE.md`는 **둘 다 프로젝트 지침으로 자동
로드**되므로 옮겨서 둘 다 만족시켰다. 루트로 되돌리지 말 것.
(사용자 프로젝트에는 여전히 루트 `CLAUDE.md`를 만든다 — 거긴 플러그인
루트가 아니라 경고 대상이 아니다.) -->
이 프로젝트는 hi-vibe 문서 시스템을 씁니다. 폴더의 **책임**이 바뀌면 해당
`MODULE.md`를 같은 턴에 갱신하세요. 이 파일은 **코드만 봐서는 모를 것**이
바뀌었을 때만 건드립니다(새 제약·새 함정·기록할 결정). 파일을 옮긴 건
해당하지 않습니다. 세션 맥락은 `handover.md`, 실질 변경 이력과 트러블슈팅은
`CHANGELOG.md`에 기록합니다.
CLAUDE.md는 120줄을 넘기지 않습니다 — 상세는 항상 MODULE.md로.