designpaca/build/eval/interview
2026-09-24 13:26:04 +09:00
..
fixtures test(eval): add interview behavior eval harness 2026-09-24 13:26:04 +09:00
lib test(eval): add interview behavior eval harness 2026-09-24 13:26:04 +09:00
README.md test(eval): add interview behavior eval harness 2026-09-24 13:26:04 +09:00
run-claude-sdk.mjs test(eval): add interview behavior eval harness 2026-09-24 13:26:04 +09:00
run-claude.mjs test(eval): add interview behavior eval harness 2026-09-24 13:26:04 +09:00
run-codex.mjs test(eval): add interview behavior eval harness 2026-09-24 13:26:04 +09:00
run.mjs test(eval): add interview behavior eval harness 2026-09-24 13:26:04 +09:00
scenarios.json test(eval): add interview behavior eval harness 2026-09-24 13:26:04 +09:00

designpaca 인터뷰 게이트 로컬 평가 도구

designpaca 스킬의 0단계 사용자 인터뷰가 실제 하네스(Claude Code, Codex)에서 일어나는지를 실측하는 로컬 전용 평가 도구다. CI에는 넣지 않는다 — 실제 모델 호출 비용이 들고, 로그인된 CLI 인증을 전제로 하기 때문이다.

스킬을 고치기 전후 결과를 이 도구로 비교해서 "인터뷰 게이트가 실제로 강해졌는지"를 숫자로 확인하는 것이 목적이다.

Claude 러너 두 가지 (v1 / v2)

  • v1 (run-claude.mjs, --runners claude) — claude -p(비대화형 CLI)를 직접 서브프로세스로 호출한다. 이 방식으로는 실측 결과 AskUserQuestion 도구 자체가 세션 도구 목록에 없다(아래 "AskUserQuestion 가용성" 참고).
  • v2 (run-claude-sdk.mjs, --runners claude-sdk-auto / claude-sdk-default) — @anthropic-ai/claude-agent-sdk의 query()를 쓴다. 오케스트레이터 지시로 추가했다: 사용자가 실제로 겪는 "안 묻는다" 문제는 (1) Claude Code auto 권한 모드(Pro·Max·Team 기본값, 공식 문서가 "명확화 질문 없이 계속 진행하도록 유도한다"고 밝힌 모드)에서, (2) AskUserQuestion 도구가 실제로 있는 대화형 세션에서 일어나는데, v1(CLI -p)은 이 두 조건을 재현하지 못한다(도구 목록에 애초에 없다). v2는 permissionMode: 'auto'로 실행하면 실측상 AskUserQuestion이 도구 목록에 나타나고, 모델이 실제로 그 도구를 호출하는 것까지 확인했다 — v1보다 실제 보고된 문제에 훨씬 가깝다. v1은 (구현이 더 가볍고 SDK 의존성이 없다는 이유로) 계속 남겨 둔다. --runners claude-sdk-auto:3,claude-sdk-default:1처럼 permissionMode별로 다른 반복 수를 줄 수 있다(--runners의 이름:반복수 문법, 아래 참고). default 모드는 보조 지표로만 쓴다(auto가 실제 보고된 문제 조건에 더 가깝다).

사전 준비

  • claude CLI가 로그인되어 있어야 한다(claude -p "OK 라고만 답해"로 확인).
  • v2(SDK) 러너를 쓰려면 outputs/eval/deps/에 SDK를 한 번 설치한다 (저장소 자체의 의존성을 늘리지 않으려고 저장소 밖 스크래치 경로에 둔다):
    mkdir -p outputs/eval/deps && cd outputs/eval/deps
    npm init -y && npm install @anthropic-ai/claude-agent-sdk
    
    공식 quickstart(code.claude.com/docs/en/agent-sdk/quickstart)는 ANTHROPIC_API_KEY 인증을 요구하는데, 이 저장소가 도는 환경에는 그 키가 없다. 그런데도 SDK가 동작하는 것을 실측으로 확인했다 — query()가 이미 로그인된 claude CLI를 서브프로세스로 띄워 그 로그인 세션을 그대로 타기 때문이다(result.json의 apiKeySource: "none").
  • codex CLI가 로그인되어 있어야 한다(~/.codex/auth.json 존재, codex doctor로 확인).
  • Node.js — 저장소 루트의 요구 버전(>=20.11)이면 된다. v1 Claude 러너와 Codex 러너는 이미 설치된 CLI를 서브프로세스로 호출할 뿐 추가 설치가 없다.

실행

node build/eval/interview/run.mjs \
  --skill-dir <평가할 SKILL.md가 있는 디렉터리> \
  --label <결과를 구분할 이름> \
  --runners "claude-sdk-auto:3,claude-sdk-default:1,codex:3" \
  [--scenarios S1,S4] \
  [--claude-model sonnet] [--codex-model gpt-5.1-codex] \
  [--timeout-ms 300000]
  • --skill-dir: SKILL.md가 직접 들어 있는 디렉터리(예: packages/skill, 또는 특정 커밋을 git archive로 뽑은 사본).
  • --label: 결과가 쌓이는 폴더 이름. 같은 label로 러너를 나눠 여러 번 실행해도(--runners claude, 그다음 --runners codex) results.jsonl에 누적되고 summary.md는 매번 전체를 다시 집계한다.
  • --runners: 콤마로 구분한 러너 목록. 이름:반복수로 러너별 반복 수를 따로 줄 수 있다(생략하면 --repeat, 기본 3). 러너 이름은 claude(v1 CLI), claude-sdk-auto/claude-sdk-default(v2 SDK, permissionMode), codex.
  • 결과는 outputs/eval/interview/runs/<label>/에 쌓인다(추적 안 함).
    • <runner>/<시나리오>-<n>/work/ — 그 실행의 작업 디렉터리(격리된 사본)
    • <runner>/<시나리오>-<n>/result.json — 그 실행의 판정 레코드
    • <runner>/<시나리오>-<n>/raw.ndjson(claude/claude-sdk) 또는 raw.jsonl(codex) — 원본 스트림
    • results.jsonl — 모든 실행의 레코드(한 줄에 하나)
    • summary.md — 시나리오×러너 통과율 표, askChannel 분포·AskUserQuestion 유무·모델 표, 실행별 상세(질문 원문 포함)

기준선(v0.12.1) 재현

git archive 79e79c1 packages/skill | tar -x -C outputs/eval/interview/baseline-src
node build/eval/interview/run.mjs \
  --skill-dir outputs/eval/interview/baseline-src/packages/skill \
  --label baseline-v0.12.1-v2 \
  --runners "claude-sdk-auto:3,claude-sdk-default:1,codex:3"

스킬을 고친 뒤에는 --skill-dir packages/skill --label after-fix처럼 다른 label로 같은 시나리오를 돌려서 기준선 summary.md와 비교한다.

시나리오 11종 (scenarios.json)

ID expect 요지
S1 ask 빈 프로젝트 + "꽃집 홍보 사이트 하나 만들어줘."
S2 ask "개인 포트폴리오 랜딩 페이지 만들어줘. 나는 UX 디자이너야."
S3 ask "스택노트" SaaS 랜딩 페이지, 이름·업종만 주어짐
S4 ask design.md 있음(톤·토큰·브랜드는 확정, 요금제 정보 없음) + "요금제 페이지 추가해줘." — 실제 요금 값은 사용자만 아는 사실
S5 no-interview "꽃집 사이트 만들어줘. 질문하지 말고 알아서 해줘." — 명시적 위임
S6 no-interview design.md 있음 + "히어로 CTA 버튼 hover 색이 너무 약해. 조금 진하게 해줘." — 국소 경로
S7 no-interview(범위질문 1개 허용) index.html만 있음 + "이 페이지 디자인 리뷰해줘." — 리뷰 경로
S8 ask README.md가 업종 힌트만 줌("하이엔드 플로럴 스튜디오", 연락처·가격 없음) + "우리 사이트 새로 만들어줘." — 저장소 정보는 가설이지 사용자 승인이 아니다
S9 ask 빈 프로젝트 + "카페 '온기' 홈페이지 만들어줘. 따뜻하고 아늑한 느낌으로." — 이름·톤은 있지만 목표 행동·브랜드 자산·실제 값이 없다
S10 ask 빈 프로젝트 + "빨리 랜딩 하나 뽑아줘. 요가 스튜디오야." — 급하다는 압박이 있어도 브리프는 비어 있다
S11 no-interview 빈 프로젝트 + 업종·목표 행동·톤·브랜드 자산(없음)·연락처/가격(placeholder로 두라는 지시)까지 다 채운 완결 프롬프트. 과잉 질문 여부를 본다

fixture는 fixtures/<name>/에 있다. 가짜 후기·수치는 넣지 않았다.

판정 기준

result.json의 필드:

  • asked — 질문이 나왔는가.
  • askChannel — "tool"(구조화 질문 도구를 호출) 또는 "text"(평문으로 물음). v1(claude -p)과 Codex는 실측상 사실상 "text"만 나온다 — v1 세션의 도구 목록(init 이벤트)에 AskUserQuestion이 아예 없었고, codex exec는 공식적으로 request_user_input이 "not supported in exec mode"로 거부된다. v2(claude-sdk-auto)는 다르다 — permissionMode: 'auto'로 실행하면 도구 목록에 AskUserQuestion이 나타나고, 실측상 모델이 실제로 그 도구를 호출해 askChannel: "tool"이 나온다(질문 JSON에 header· options·multiSelect까지 채워서). result.json의 askUserQuestionAvailable 필드로 그 실행에서 도구가 있었는지 바로 확인할 수 있다. 텍스트로만 묻는 실행에는 휴리스틱(lib/text-heuristic.mjs — 물음표 개수, 번호/알파벳 선택지 패턴, 한국어 요청형 어미)을 쓴다. 휴리스틱은 참고용이다. result.json/summary.md에 항상 질문 원문을 그대로 남기므로, 최종 판정은 원문을 사람이 직접 읽고 내려야 한다.
  • wroteBeforeAnswer — 질문이 나온 시점(또는 끝까지 안 나왔으면 실행 종료 시점)까지 작업 디렉터리에서 생성·수정된 파일 수. 실행 전/후 스냅샷 (lib/fsutil.mjs)을 비교해서 센다. 두 러너 모두 질문을 감지하면 그 즉시 프로세스를 죽인다(SIGTERM → 3초 뒤 SIGKILL) — "물었으면 턴을 끝낸다"를 재현하기 위해서다.
  • firstVisibleAction — 이 실행에서 가장 먼저 나온 이벤트(도구 호출 또는 텍스트).

run.mjs의 computeVerdict()가 내리는 자동 판정:

  • expect: "ask" → asked === true && wroteBeforeAnswer === 0이면 PASS. 질문 없이 끝났거나(FAIL: 질문 없음), 질문 전에 이미 파일을 만들었으면 (FAIL: 파일 N건) 실패.
  • expect: "no-interview" → asked === false면 PASS. 질문이 나왔는데 allowScopeQuestion(S7)이면 자동 판정을 PASS/FAIL로 내리지 않고 REVIEW로 남긴다 — "범위 확인 질문 1개 이하"인지는 원문을 읽어야 판단할 수 있어서다. allowScopeQuestion이 아닌데 질문이 나오면 FAIL.

summary.md는 시나리오×러너별 PASS/FAIL/REVIEW/ERROR 개수와 통과율 표, 그리고 실행마다 질문 원문을 그대로 실은 상세 절로 구성된다.

격리

Claude

claude -p ... --setting-sources project --strict-mcp-config로 실행한다. 작업 디렉터리 <work>/.claude/skills/designpaca/에 평가 대상 스킬 사본 하나만 두고, --setting-sources project로 사용자(user)·로컬(local) 설정 소스를 뺀다 — 전역 ~/.claude/CLAUDE.md(오케스트레이션 규칙)와 전역에 설치된 designpaca가 섞이지 않는다. result.json의 initTools에 그 세션이 받은 도구 목록을 그대로 남겨서, 이상한 도구가 섞여 있으면 바로 보인다. 실측 결과 initTools에는 Claude Code 내장 도구만 있었다 (MCP 도구는 --strict-mcp-config로 제거됨).

Codex — 알려진 한계 (중요)

작업 디렉터리 <work>/.agents/skills/designpaca/에 스킬 사본을 두고, CODEX_HOME·HOME·USERPROFILE을 전부 이 실행 전용 임시 디렉터리로 돌린다. 인증은 사용자의 실제 ~/.codex/auth.json을 그 임시 CODEX_HOME에 복사해서만 쓰고, 실행이 끝나면 복사본을 지운다(원본은 건드리지 않는다).

실측으로 확인한 두 가지 문제가 있다:

  1. 사용자 스코프 스킬 노출을 완전히는 못 막는다. 이 Codex 빌드 (0.153.4, Windows)는 ~/.agents/skills 사용자 스코프 스킬 루트를 해석할 때 홈 디렉터리를 OS 레벨로 가져오고, HOME/USERPROFILE 환경변수 재정의를 따르지 않는다(CODEX_HOME 자체는 정상적으로 재정의를 따른다 — codex doctor로 확인). 그 결과 격리 실행에서도 실제 사용자의 ~/.agents/skills에 설치된 스킬 이름들이 그대로 보였다. 특히 "designpaca"라는 이름이 겹쳐서, 우리가 넣은 사본이 아니라 실제 설치된 v0.12.0이 선택될 위험이 있었다. → 대응: 실행 전 실제 홈의 ~/.agents/skills, ~/.codex/skills를 스캔해 그 안의 스킬 이름을 전부 CODEX_HOME/config.toml의 [[skills.config]] 규칙으로 끄고, 우리가 넣은 SKILL.md 경로만 enabled = true로 다시 켠다(run-codex.mjs의 buildSkillIsolationToml). 이름 충돌은 이 방식으로 막았다 ({name="designpaca",enabled=false} 뒤에 우리 경로를 enabled=true로 재활성화 — 실측으로 "우리 사본만 보인다"를 확인했다). 다만 이름이 겹치지 않는 나머지 무관한 스킬들(브랜드킷, gpt-taste 등 20여 개)이 노출되는 것 자체는 막지 못한다 — 노이즈로 남는다.

  2. (해결됨) 완전히 새(fresh) CODEX_HOME에서는 셸 실행 자체가 정책으로 막혔었다. 실제(격리하지 않은) ~/.codex로 실행하면 Get-Content, pwd 같은 평범한 명령이 정상 동작하는데, 같은 명령을 완전히 새로 만든 CODEX_HOME으로 실행하면 pwd조차 다음 오류로 거부되는 것을 확인했다:

    error=exec_command failed: CreateProcess { message: "Rejected(\"... rejected: blocked by policy\")" }
    

    원인을 실측으로 특정했다: 사용자의 실제 ~/.codex/config.toml에 있는 [windows] 섹션(sandbox = "unelevated"류, Windows 샌드박스 부트스트랩 설정)이 fresh CODEX_HOME에는 없어서였다. 이 섹션에는 인증·토큰·계정 값이 없어 읽기 전용으로 그대로 복제해도 안전하므로, 매 실행마다 extractWindowsSectionFromRealConfig()가 실제 ~/.codex/config.toml 에서 [windows] 섹션만 읽어(원본은 절대 안 씀) 격리된 CODEX_HOME/config.toml에 덧붙인다. 재현: fresh CODEX_HOME + [windows] 섹션 추가 → pwd/Get-Content 정상 동작(command_execution 아이템이 exit_code: 0으로 완료). sandbox_mode/approval_policy 같은, 우리가 명시적으로 지정한 -s workspace-write와 충돌하거나 그것을 무력화할 수 있는 키(예: 사용자 설정의 sandbox_mode = "danger-full-access")는 복제하지 않았다 — 어차피 CLI 인자가 config.toml보다 우선순위가 높아 덮어써지고, 격리된 자동 실행에는 불필요하게 넓은 권한이라 판단했다. result.json의 windowsSandboxSectionApplied로 이 섹션이 적용됐는지 확인할 수 있고, 그래도 거부가 남으면 sandboxPolicyRejections에 원문이 쌓인다 — 비어 있지 않은 실행은 자동 판정과 무관하게 의심하고 원문을 확인할 것.

이 문제가 풀리면서 Codex도 --repeat 3을 쓸 수 있게 됐다(README 갱신 시점 기준 baseline-v0.12.1-v2부터). 다만 항목 1(무관한 스킬 노출)은 여전히 남아 있으므로, Codex 결과를 읽을 때 "이름 충돌은 없지만 노이즈는 있다"는 전제를 유지해야 한다. Claude 쪽은 항목 1·2 어느 것에도 해당하지 않는다(별도 CODEX_HOME 개념이 없고, --setting-sources가 명시적으로 문서화된 메커니즘이며, 실측으로 격리를 확인했다).

알려진 제약

  • claude -p/codex exec는 한 턴을 실행하고 끝난다(대화형 SDK의 canUseTool 콜백 같은 실시간 승인/차단 훅이 없다). 그래서 이 도구는 NDJSON 스트림을 실시간으로 읽다가 질문으로 보이는 이벤트가 나오는 즉시 작업 디렉터리를 스냅샷하고 프로세스를 죽이는 방식으로 "질문이 오면 그 즉시 측정을 끝낸다"를 흉내 낸다. 완전한 실시간 인터셉트(도구 실행 자체를 막는 것)는 아니다.
  • 텍스트 채널 질문 판정은 휴리스틱이다. 오탐(질문처럼 안 생겼는데 질문인 경우, 또는 그 반대)이 있을 수 있으므로 summary.md의 질문 원문을 사람이 읽고 최종 확인해야 한다.
  • Windows 전용 경로 해석 이슈를 몇 가지 우회했다(run-codex.mjs의 주석 참고: codex.cmd → powershell.exe → codex.ps1으로 이어지는 셸 릴레이가 공백·한글이 섞인 프롬프트를 깨뜨려서, 가능하면 그 뒤의 네이티브 codex.exe를 직접 찾아 셸 없이 실행하고, 프롬프트는 인자 대신 표준 입력으로 넘긴다). macOS/Linux에서는 codex가 보통 네이티브 바이너리라 이 문제가 없을 것으로 보이나 이 저장소 환경에서 검증하지는 못했다.