AX LABS
← 블로그 에이전트 제품 설계

Harness-of-Harness 논문 정리 — Claude Code·Codex에 바로 넣는 Planner·Developer·QA 3역할 프롬프트

"계속 해줘"를 세 번 보내는 대신, 계획→구현→독립 검증 루프로 바꾸면 같은 토큰으로 더 높이 올라간다는 실험 결과와 그대로 쓸 수 있는 프롬프트

Harness-of-Harness 논문 정리 — Claude Code·Codex에 바로 넣는 Planner·Developer·QA 3역할 프롬프트

에이전트에게 소프트웨어를 끝까지 만들게 해 본 사람은 안다. 첫 패스는 그럴듯하다. 두 번째 "계속 해줘"부터 같은 곳을 다시 고치고, 이미 되던 기능을 깨뜨리고, 안 되는 기능을 "완료"라고 보고한다. 문제는 모델의 능력이 아니라 긴 궤적 위에서 무엇이 검증됐고 무엇이 남았는지를 잃어버리는 것이다.

상하이 AI Lab이 9월 1일 공개한 Harness-of-Harness (HoH)는 이 문제를 하네스를 고치지 않고 푼다. Codex CLI, OpenCode, Pi 같은 기존 코딩 하네스를 그대로 두고, 같은 하네스를 Planner → Developer → QA Tester 세 역할로 나눠 반복 호출하는 프로토콜을 얹는다. 세 벤치마크·세 하네스 조합 전부에서 이겼고(평균 상대 개선 52.25%, 최대 82.86%), 70루프를 돌려 사람이 플레이할 수 있는 FPS 게임을 만들었다.

이 글은 논문 리뷰가 앞부분이고, 뒷부분이 본론이다. 논문 부록 A.2에 실린 세 역할의 프롬프트 템플릿을 실무용으로 옮기고, Claude Code·Codex·자체 에이전트 루프에 어떻게 붙이는지까지 적었다.

1. 논문 리뷰 — 무엇을 어떻게 증명했나

구조: 하네스를 바꾸지 않고, 호출 방식을 바꾼다

한 루프 = 같은 하네스를 세 번, 역할만 바꿔 호출① Planner읽기만, 코드 수정 금지→ 개발 문서 Dₜ② Developer유일한 쓰기 권한→ 아티팩트 Aₜ③ QA Tester동결된 후보만 검사→ 증거 번들 Eₜ아티팩트 채널 — 코드는 다음 Developer로 (warm-start)빈 작업공간에서 다시 짓지 않는다증거 채널 — 검증/갭 기록은 다음 Planner로개발 문서 Dₜ는 넘기지 않는다. 명세 + 증거로 매번 새로 쓴다Planner는 명세 S + Eₜ₋₁ 를 읽고, Aₜ₋₁ 은 읽기 전용으로만 참고코드와 증거, 두 상태가 따로 루프를 건넌다

한 루프는 같은 하네스·같은 모델을 세 번 호출한다. 역할 프롬프트와 런타임이 강제하는 권한만 다르다.

역할 입력 권한 산출물
Project Planner 명세 S, 직전 증거 Eₜ₋₁, 코드 Aₜ₋₁(읽기 전용) 코드 수정 금지 개발 문서 Dₜ — 이번 루프의 범위와 검증 조건
Developer S, Dₜ, 작업공간 Aₜ₋₁ 유일한 쓰기 권한 갱신된 코드 Aₜ + 실행 기록
QA Tester S, Dₜ, 동결된 Aₜ 읽기·실행만, 수정 금지 증거 번들 Eₜ — 검증됨/갭 기록

설계에서 중요한 선택이 셋 있다.

  • 두 개의 상태가 따로 루프를 건넌다. 코드(아티팩트)는 Developer에서 다음 Developer로, 증거는 QA에서 다음 Planner로. 개발 문서 Dₜ는 넘기지 않는다. 다음 Planner는 명세와 증거만으로 문서를 새로 쓴다.
  • 완료 판정은 만든 사람이 하지 않는다. Developer의 자체 테스트는 "후보로 내놓을 준비가 됐는가"만 답한다. 요구사항 충족 여부는 후보를 동결한 뒤 QA가 별도로 판정한다. QA는 코드를 고칠 수 없으므로, 검사하다가 슬쩍 수리하는 일이 구조적으로 막힌다.
  • 워크플로우를 처방하지 않고 산출물 스키마를 강제한다. 각 역할은 정해진 구조의 문서를 내야 하고, 스키마를 어기면 재시도된다. 추론 과정과 도구 선택은 자유다.

결과: 세 하네스·세 벤치마크 전부 개선

하네스 + 모델 GameCraft-Bench FrontierSWE (Dominance) ProgramBench
Codex + GPT-5.5 (high) 49.58 → 71.52 (+21.93) 44% → 71% 60.41 → 66.50
OpenCode + DeepSeek-V4-Pro 26.90 → 48.98 (+22.08) 25% → 44% 45.27 → 57.56
Pi + MiniMax-M3 42.16 → 58.78 (+16.62) 35% → 64% 35.83 → 52.68

세 루프(HoH@3)만 돌린 수치다. FrontierSWE에서 Codex를 열 루프까지 돌리면 Dominance가 22%에서 72.67%까지 계속 오른다. 벤치마크 점수는 개발 루프에 절대 되돌려 주지 않았다. 즉 이 개선은 채점표를 보고 맞춘 게 아니라, 공개 명세와 QA가 관찰한 증거만으로 얻은 것이다.

반론 하나: 그냥 세 번 더 돌린 것 아닌가

가장 중요한 실험이다. 실무자가 실제로 하는 행동, 즉 같은 세션에 "Continue developing and testing the current game." 한 줄을 다시 보내는 것과 비교했다.

"계속 개발하고 테스트해"를 세 번 보내는 것과의 차이같은 모델·하네스, 개발 패스 수 동일 (Codex + GPT-5.5)50607049.58 · 2.59M54.99 · 4.56M58.24 · 6.33M59.71 · 2.88M64.84 · 5.67M71.52 · 8.41M패스 1패스 2패스 33역할 루프 (HoH)"Continue developing…" 반복루프 2회가 단순 반복 3회보다 높고, 토큰은 덜 쓴다

패스 수를 맞춰도 HoH가 매번 10점 안팎 앞선다. 토큰으로 봐도 HoH 2회(5.67M)가 단순 반복 3회(6.33M)보다 높은 점수를 낸다. 추가 100만 토큰당 점수 상승은 단순 반복이 2.32점, HoH가 3.77점이다.

절제 실험: 세 장치 중 하나만 빼도 무너진다

세 장치 중 하나만 빼도 6~8점 떨어진다GameCraft-Bench Overall, Codex + GPT-5.5, T=3, 45개 과제 평균Full HoH8.41M71.52증거 피드백 제거7.46M65.23−6.28웜스타트 제거11.12M · 토큰 +32%63.67−7.85계획 갱신 제거7.56M63.39−8.13계획 갱신 · 증거 피드백 · 웜스타트는 세트다
  • 계획 갱신 제거(첫 문서를 계속 재사용): −8.13
  • 증거 피드백 제거(QA는 하되 결과를 Planner에 안 줌): −6.28
  • 웜스타트 제거(매 루프 빈 작업공간에서 재구축): −7.85, 토큰은 8.41M → 11.12M

세 번째가 특히 실무적이다. "처음부터 다시 만들어"는 점수도 낮고 돈도 더 든다.

70루프 사례: 회귀는 정상이다

Fusepoint라는 5분짜리 FPS를 PRD 하나에서 시작해 70루프를 돌렸다. 기록된 이슈 81건 중 65건 종료, 17건은 검증 종료 후 다시 열렸다. 나중 변경이 먼저 검증된 동작을 깨뜨린 것이다. 논문은 이것을 실패가 아니라 이터레이션 개발의 정상 궤적으로 본다. 핵심은 재오픈 기록이 이전 검증 이력을 그대로 달고 다시 계획에 들어간다는 점이다. 최신 코드만 보고 과거를 재구성할 필요가 없다.

2. 실무 적용 포인트 — 그대로 넣는 프롬프트

논문 부록 A.2의 템플릿을 우리 환경에 맞게 옮겼다. 게임 벤치마크 전용 표현은 일반 소프트웨어용으로 바꿨고, 원문의 역할 경계와 출력 계약은 그대로 살렸다. 아래 세 프롬프트는 Claude Code의 서브에이전트 정의, Codex의 역할별 지시, 또는 자체 루프 스크립트의 system prompt로 그대로 쓸 수 있다.

2-1. Project Planner 프롬프트

Planner의 핵심 규칙은 넷이다. 코드를 만지지 않는다. 블로커와 회귀를 신규 기능보다 앞세운다. 우선순위는 최대 3개다. 각 우선순위는 구현 목표와 관찰 가능한 검증 조건 한 쌍으로 바꾼다.

# /role/project-planner
당신은 이터레이션 {{loop_index}}의 Project Planner다.
이 호출은 계획 전용이다. 코드를 구현·수정·테스트·검사하지 않는다.
아래 개발 문서 구조만 반환한다.

# /source-of-truth
아래 명세가 제품의 유일한 진실이다. 비공개 채점 기준, 숨은 테스트,
평가자 피드백 등 명세 밖의 정보는 사용하지 않는다.
{{product_spec}}

# /previous-iteration-evidence
{{evidence_packet}}
이터레이션 1이면 증거가 없다. 그 이후라면 다음을 식별한다:
- 보존해야 할 검증된 기능
- 수리해야 할 가시적 버그와 미충족 요구사항
- 여전히 증거가 부족한 항목
이전 개발 문서를 요청하거나 재구성하지 않는다.

# /planning-policy
블로커와 회귀를 제품 확장보다 먼저 배치한다.
달성 가능한 우선순위를 최대 3개 선택한다.
각 우선순위를 구체적 구현 목표와 관찰 가능한 검증 조건으로 변환한다.
광범위한 재작성이나 무관한 아키텍처 변경은 피한다.

# /output-contract
다음 Markdown 구조만 반환한다:
## Project Planner Priorities
### Priority Order
1. **우선순위 이름** — 할 일과 관찰 가능한 결과
### Preservation Gate
- 회귀하면 안 되는 동작 기능과 그 증거
### Acceptance Gate
- 선택한 우선순위를 확인하는 최소한의 end-to-end 검증

Preservation Gate가 이 프롬프트의 값이다. 대부분의 계획 프롬프트는 "이번에 할 일"만 묻는다. HoH는 "이번에 깨뜨리면 안 되는 것"을 같은 문서에 강제로 적게 한다. 이 한 섹션이 Developer와 QA 양쪽의 입력이 된다.

2-2. Developer 프롬프트

Developer 프롬프트의 핵심은 웜스타트 문장이다. "작은 리셋으로 갈아엎지 말고, 있는 것 위에서 다음 관찰 가능한 갭을 고쳐라."

# /role/developer
당신은 이터레이션 {{loop_index}}의 Developer다.
{{workspace}}의 프로젝트를 빌드하거나 개선한다.
명세를 PRD로, 현재 개발 문서를 이번 이터레이션의 구현·검증 브리프로 취급한다.

# /iteration-context
{{workspace}}에 이미 존재하는 아티팩트에서 이어간다.
검증된 기능을 보존하고, 동작하는 프로젝트를 더 작은 리셋으로
교체하는 대신 다음 관찰 가능한 갭을 수리한다.

# /development-document
현재 문서: {{development_doc}}
빌드 상태: {{build_ok}}
{{product_spec}}
{{priority_order}}
{{preservation_gate}}
{{acceptance_gate}}
{{evidence_history}}

# /development-policy
빌드·런타임 블로커를 먼저 수리한 뒤, 문서의 우선순위 순서대로 처리한다.
하네스의 파일·저장소·셸·빌드·실행·로컬 테스트 도구를 사용한다.
프로젝트를 항상 실행 가능한 상태로 유지한다.
구현했다고 주장하는 모든 기능은 실행 기록(로그·트레이스·스크린샷)으로
관찰 가능해야 한다.

# /output-contract
갱신된 아티팩트를 {{workspace}}에 남긴다.
실행 가능한 진입점과 QA가 재현할 수 있는 실행 기록을 보존한다.

논문은 Developer 안에서 baseline → change → retest 사이클(shift-left testing)을 권한다. 수정 전에 대상 동작의 기준선을 잡고, 의미 있는 변경마다 해당 경로를 다시 실행하고, 인접 회귀 표면을 확인한다. 다만 이 자체 테스트는 "후보로 낼 준비가 됐는가"까지만이다. 완료 판정은 다음 역할의 몫이다.

2-3. QA Tester 프롬프트

QA 프롬프트의 핵심은 주장–증거 레코드다. 요구사항을 검사 가능한 주장으로 쪼개고, 각 주장에 실제 실행 기록을 붙이고, 기록이 뒷받침할 때만 "verified"를 준다. 나머지는 전부 "gap"이다.

# /role/qa-tester
당신은 이터레이션 {{loop_index}}의 QA Tester다.
갱신된 아티팩트를 사용자 관점의 제품으로 리뷰한다.
공개 명세, 현재 개발 문서, 프로젝트 파일, 스크린샷, 실행 트레이스, 로그만 사용한다.
코드를 수정하지 않는다.

# /inputs
작업공간: {{workspace}}  (읽기 전용 스냅샷)
개발 문서: {{development_doc}}
실행 기록: {{execution_records}}

# /assessment-policy
공개 요구사항과 이번 이터레이션의 검증 목표에서 검사 가능한 주장(claim)을 도출한다.
인용한 실행 기록이 충분히 관찰 가능한 근거를 제공할 때만 주장을 verified로 분류한다.
가시적 실패, 회귀, 미충족 요구사항, 증거 부족은 모두 gap으로 기록한다.
소스 코드에 구현이 존재한다는 사실만으로는 동작 검증으로 인정하지 않는다.
공개 인터페이스로 재현할 수 없는 트레이스는 기각한다.

# /restrictions
숨은 테스트, 비공개 채점 파일, 평가자 전용 자료를 읽거나 추론하거나 요약하지 않는다.

# /output-contract
qa_report.md 와 qa_report.json 을 작성한다.
각 레코드는 상태, 인용 증거, 사용자 영향, 이슈 소유, 구체적 권고를 포함한다.
마지막에 다음 루프를 위한 planner_handoff 를 작성한다:
남은 버그, 보존 제약, 다음 루프 목표.

"소스 코드에 구현이 존재한다는 사실만으로는 검증이 아니다"라는 한 줄이 가장 많은 거짓 완료 보고를 걸러낸다. 에이전트는 함수를 만들어 두면 기능이 된 것으로 보고하는 경향이 강하다.

2-4. 증거 번들 JSON — 루프를 건너는 유일한 기억

QA 산출물을 다음 Planner가 읽을 수 있게 정규화한 형태다. 논문 Listing 1을 일반화했다.

{
  "iteration": 2,
  "qa_status": "partial",
  "verified_records": [
    {
      "claim_id": "login_flow",
      "claim": "이메일·비밀번호로 로그인하면 대시보드로 이동한다.",
      "execution_records": [
        { "type": "e2e_trace", "path": "traces/login.json",
          "observation": "POST /login 200 후 /dashboard 렌더 확인" }
      ],
      "status": "verified"
    }
  ],
  "gap_records": [
    {
      "claim_id": "password_reset",
      "claim": "비밀번호 재설정 메일이 발송된다.",
      "execution_records": [
        { "type": "log", "path": "logs/mail.log",
          "observation": "reset 요청 후 발송 로그 없음" }
      ],
      "status": "gap",
      "user_impact": "사용자가 계정을 복구할 수 없다.",
      "recommended_update": "메일 발송 경로를 연결하고 로그로 재검증"
    }
  ],
  "planner_handoff": {
    "preservation_constraints": ["검증된 로그인 플로우 보존"],
    "update_targets": ["비밀번호 재설정 메일 발송 구현"],
    "validation_requirements": ["reset 요청 → 발송 로그 → 링크 클릭까지 e2e 재현"]
  }
}

세 배열이 다음 루프의 입력을 그대로 결정한다. preservation_constraints는 Planner의 Preservation Gate가 되고, update_targets는 Priority Order 후보가 되고, validation_requirements는 Acceptance Gate가 된다.

2-5. 내 시스템에 붙이는 세 가지 방법

① Claude Code — 서브에이전트 세 개 + 루프 스킬. .claude/agents/planner.md, developer.md, qa-tester.md를 만들고 위 프롬프트를 각각 넣는다. 권한은 도구 목록으로 강제한다. Planner와 QA는 Edit·Write를 빼고 Read·Bash(실행용)만 준다. Developer만 전체 도구를 갖는다. 루프는 메인 세션이 돌린다.

<!-- .claude/agents/qa-tester.md -->
---
name: qa-tester
description: 동결된 후보를 실행·관찰해 claim-evidence 레코드를 만든다. 코드 수정 불가.
tools: Read, Bash, Grep, Glob
---
(위 2-3 QA Tester 프롬프트 본문)

메인 세션에는 한 줄이면 된다. "evidence/latest.json을 읽어 planner를 호출하고, 나온 문서로 developer를 호출하고, 작업공간을 커밋한 뒤 qa-tester를 호출해 evidence/를 갱신하라. 이것을 N회 반복하라."

② Codex·OpenCode·Pi — 셸 루프. 논문 구현이 정확히 이 형태다. 하네스를 세 번 호출하되 역할 프롬프트 파일과 작업 디렉토리 권한만 바꾼다.

for t in $(seq 1 "$T"); do
  codex exec --cd "$WS" --sandbox read-only \
    "$(render planner.md --loop $t --evidence evidence/$((t-1)).json)" > docs/D_$t.md
  codex exec --cd "$WS" --sandbox workspace-write \
    "$(render developer.md --loop $t --doc docs/D_$t.md)"
  git -C "$WS" add -A && git -C "$WS" commit -qm "loop $t: developer"
  codex exec --cd "$WS" --sandbox read-only \
    "$(render qa_tester.md --loop $t --doc docs/D_$t.md)" > evidence/$t.json
done

Developer 뒤에 커밋을 넣는 이유는 두 가지다. QA가 검사하는 후보가 동결되고, 큰 회귀가 나면 마지막 검증된 상태로 되돌아갈 수 있다. 논문의 70루프 사례에서 이 역할을 한 것이 GitHub 커밋과 이슈 이력이다.

③ 이미 있는 단일 에이전트 — 프롬프트만 바꾸기. 루프를 짤 여유가 없다면 최소한 두 가지는 오늘 바꿀 수 있다. 첫째, "계속 해줘" 대신 Planner 출력 계약(Priority Order · Preservation Gate · Acceptance Gate)을 먼저 쓰게 한 뒤 구현시킨다. 둘째, 완료 보고를 받을 때 "소스에 구현이 있다는 것은 검증이 아니다. 각 주장에 실행 기록을 붙여 verified/gap으로 분류하라"고 요구한다. 역할을 물리적으로 나누지 않아도 이 두 계약만으로 거짓 완료가 눈에 띄게 준다.

2-6. 적용할 때 지킬 것

  • Planner에게 코드 쓰기 권한을 주지 마라. 프롬프트로 "수정하지 마"라고 쓰는 것과 도구 목록에서 빼는 것은 다르다. 논문은 런타임이 권한을 강제한다고 못 박는다.
  • QA는 동결된 후보를 본다. Developer가 아직 돌고 있는 작업공간을 QA가 읽으면 관찰이 섞인다. 커밋이나 스냅샷 뒤에 호출한다.
  • 개발 문서를 다음 루프에 넘기지 마라. 넘기면 계획이 고정된다(절제 실험 −8.13). 명세와 증거만 넘기고 문서는 매번 새로 쓴다.
  • 우선순위 3개 제한을 풀지 마라. 범위가 커지면 실패 원인을 찾기 어렵고 검증도 어려워진다. 논문이 "bounded but locally complete"라고 부르는 단위다.
  • 재오픈을 기록으로 남겨라. 회귀는 정상이다. 이전 검증 이력을 달고 다시 계획에 들어가면 된다.

마무리

HoH가 보여준 것은 모델이나 하네스의 성능이 아니라 호출 방식의 차이다. 같은 도구를 "계속 해줘"로 세 번 부르는 것과, 계획하는 호출·만드는 호출·판정하는 호출로 나눠 부르는 것 사이에 10점 이상, 토큰 대비 1.6배의 차이가 있었다. 역할을 나누고, 코드와 증거를 따로 넘기고, 완료를 만든 쪽이 아닌 쪽이 판정하게 하는 것. 위 프롬프트 세 개가 그 최소 구성이다.

에이전트 루프를 조직의 개발 프로세스에 어떻게 얹을지가 결국 우리가 하는 일이다. AX Ops 방법론 →

참고

함께 읽으면 좋은 글