- CLAUDE.md 를 .claude/rules/ 4파일로 분할 (project/stack/principles/workflow) - Plans.md 루트 인덱스 (스프린트 목차 + 참조만) - hooks/ pre/post-tool.sh 스켈레톤 (no-op, Sprint 002에서 구현) - .plans/OVERVIEW.md — 목표/범위/성공기준 8개 - .plans/failure-audit.md — F1~F6 실패 감사 (증거 기반) - .plans/design/ 6개 문서: * state-machine.md (XState v5 FSM 설계) * sprint-contract.md (Zod schema + validator) * skill-enforcement.md (4계층 방어) * handoff.md (상태 전이 기반 자매 통신) * retry-policy.md (backoff + escalation) * qa-template.md (체크리스트 runtime) - .plans/sprints/ 8개 스프린트 명세 (SPRINT-000~007) - .plans/migration/from-hanarang-harness.md (자산 매트릭스 + 단계별 가이드) 총 17개 문서, 약 2146 lines. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
155 lines
5.0 KiB
Markdown
155 lines
5.0 KiB
Markdown
# Design — Skill Enforcement
|
|
|
|
> **처방 대상**: F1 (하네스 skill bypass)
|
|
|
|
## 문제
|
|
|
|
기존 hanarang-harness 에서 나랑이가 "worker 스폰해서 바로 시작한다" 라며 skill 진입을 건너뛰었다. skill 이 권고 수준이라 강제력이 없었다. 결과적으로 파이프라인 일관성이 무너졌다.
|
|
|
|
## 해결 전략 — 다층 방어
|
|
|
|
1. **Layer 1 — 시스템 프롬프트 강제문** (약한 방어)
|
|
- 자매 시스템 프롬프트에 "반드시 `/rails` 스킬을 거쳐야 한다" 명시
|
|
- LLM 지시 준수 기대치에 의존 — 약함
|
|
|
|
2. **Layer 2 — Pre-Tool Hook (중간 방어)**
|
|
- 자매가 Write / Edit / Bash 를 호출하기 직전
|
|
- 현재 세션이 `rails-skill-context` 를 세팅했는지 확인
|
|
- 세팅 안 됐으면 exit 2 로 차단하고 "먼저 /rails 를 호출하세요" 메시지
|
|
|
|
3. **Layer 3 — Post-Tool Hook (강한 방어)**
|
|
- 자매가 작업 결과를 커밋한 후
|
|
- `.rails/skill-trace.jsonl` 을 확인해 파이프라인이 skill 경로를 탔는지 감사
|
|
- 우회 감지 시 작업을 자동 revert + escalation
|
|
|
|
4. **Layer 4 — Contract Validator (최종 방어)**
|
|
- Sprint Contract 의 DoD 에 `skill-path-taken` 체크 포함
|
|
- contract validator 가 `.rails/skill-trace.jsonl` 을 읽어 검증
|
|
- 우회한 경우 `ABORT_PRECHECK` 반환
|
|
|
|
## 구현 — Layer 2/3 상세
|
|
|
|
### Pre-Tool Hook 로직
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
# hooks/pre-tool.sh
|
|
set -euo pipefail
|
|
|
|
# stdin 에서 event JSON 읽기
|
|
EVENT=$(cat)
|
|
|
|
# 이 이벤트가 Write / Edit / Bash 인지 확인
|
|
TOOL=$(echo "$EVENT" | jq -r '.tool_name // empty')
|
|
case "$TOOL" in
|
|
Write|Edit|Bash) ;;
|
|
*) exit 0 ;;
|
|
esac
|
|
|
|
# 작업 디렉토리 추출
|
|
CWD="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
|
|
# skill context 파일 확인
|
|
SKILL_CTX="$CWD/.rails/skill-context.json"
|
|
if [ ! -f "$SKILL_CTX" ]; then
|
|
echo "[rails-enforce] Skill context missing. /rails 스킬을 먼저 호출하세요." >&2
|
|
exit 2 # 차단
|
|
fi
|
|
|
|
# skill context 가 유효한지 (최근 N초 이내) 확인
|
|
AGE=$(jq -r '.ageSeconds // 999' "$SKILL_CTX")
|
|
if [ "$AGE" -gt 300 ]; then
|
|
echo "[rails-enforce] Skill context stale (>${AGE}s). 세션을 재시작하세요." >&2
|
|
exit 2
|
|
fi
|
|
|
|
exit 0
|
|
```
|
|
|
|
### Post-Tool Hook 로직
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
# hooks/post-tool.sh
|
|
set -euo pipefail
|
|
|
|
EVENT=$(cat)
|
|
CWD="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
TRACE="$CWD/.rails/skill-trace.jsonl"
|
|
|
|
# 이 도구 사용을 trace 에 append
|
|
mkdir -p "$(dirname "$TRACE")"
|
|
echo "$EVENT" | jq -c \
|
|
'{ ts: now, tool: .tool_name, cwd: env.PWD, session_id: env.CLAUDE_SESSION_ID }' \
|
|
>> "$TRACE"
|
|
|
|
exit 0
|
|
```
|
|
|
|
### Skill Context 구조
|
|
|
|
`/rails <subcommand>` 슬래시 커맨드 진입 시 skill 이 생성:
|
|
|
|
```json
|
|
{
|
|
"skillName": "rails",
|
|
"subcommand": "plan",
|
|
"sprintId": "SPRINT-001",
|
|
"contractId": "01HW0XYZ...",
|
|
"sessionId": "<session-id>",
|
|
"createdAt": "2026-04-10T12:00:00Z",
|
|
"ageSeconds": 5,
|
|
"allowedTools": ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
|
|
}
|
|
```
|
|
|
|
skill 의 entry 스크립트가 이 파일을 기록한다. 이 파일이 있으면 "skill 진입 완료" 증거.
|
|
|
|
### Skill Trace 파일
|
|
|
|
`.rails/skill-trace.jsonl` — 자매가 파이프라인 진행 중 호출한 도구 로그:
|
|
|
|
```jsonl
|
|
{"ts":1765789200,"tool":"Read","cwd":"/home/narang/projects/arang","session_id":"ab12"}
|
|
{"ts":1765789201,"tool":"Bash","cwd":"/home/narang/projects/arang","session_id":"ab12"}
|
|
{"ts":1765789202,"tool":"Write","cwd":"/home/narang/projects/arang","session_id":"ab12"}
|
|
```
|
|
|
|
Contract validator 는 이 파일에서 다음을 확인:
|
|
- trace 가 존재하는가?
|
|
- 첫 이벤트 이전에 `skill-context.json` 이 세팅됐는가?
|
|
- 세션 ID 가 일관된가? (중간에 다른 세션이 개입 안 했는가)
|
|
|
|
## 우회 사례 & 대응
|
|
|
|
| 우회 시도 | 감지 | 대응 |
|
|
|---|---|---|
|
|
| 자매가 skill 무시하고 직접 Write | Pre-hook (skill-context 없음) | exit 2 로 차단 + 에러 메시지 |
|
|
| 자매가 skill 호출 후 **딴 데서** 작업 | Post-hook (cwd mismatch) | trace 에 기록, validator 에서 FAIL |
|
|
| 자매가 skill 호출 후 stale 세션 재사용 | Pre-hook (ageSeconds > 300) | exit 2 + 재시작 안내 |
|
|
| 자매가 trace 파일 삭제 | Pre-hook (trace 파일 없음) | exit 2 |
|
|
| 자매가 trace 파일 변조 | Post-hook signature 불일치 | (향후) hash chain 으로 감지 |
|
|
|
|
## 예외 — Escape Hatch
|
|
|
|
긴급 상황에서 enforcement 를 일시 해제할 필요가 있을 수 있다:
|
|
|
|
- 환경 변수 `RAILS_ENFORCE=off` 세팅 시 hook 이 warn 만 남기고 통과
|
|
- 단, **반드시 로그에 기록**되고 다음 스프린트 시작 시 경고 표시
|
|
- production 에서는 기본 on
|
|
|
|
## 실패 시 복구
|
|
|
|
skill 우회가 감지된 경우:
|
|
|
|
1. 자매의 최근 commit 을 `git revert` 로 되돌림 (hanarang-rails orchestrator 권한)
|
|
2. 파이프라인 FSM 을 이전 상태로 rollback
|
|
3. 디스코드에 에스컬레이션 메시지 전송
|
|
4. 로그에 구조화 기록
|
|
|
|
## 참고
|
|
|
|
- `principles.md` 원칙 4
|
|
- `failure-audit.md` F1
|
|
- Claude Code hooks: `.claude/settings.json` → `PreToolUse` / `PostToolUse` matcher
|