--- name: harness-setup description: docs/PRD.md·docs/TRD.md·docs/ARCHITECTURE/·plan_setup/(PLAN.md·phase·task) 을 읽고 프로젝트의 검증 하네스를 harness_setup/ 아래에 세운다. quality_gates/ 에 lint·unit·e2e·task_validation 네 게이트의 정의서(무엇을 검사하고 어떤 명령으로 돌며 무엇이 통과인가, 언제부터 활성인가)를 만들고, scripts/ 에 사용자 OS에 맞는 local-dev 스크립트(DB·API·앱 서버·Redis 같은 의존 서비스를 한 명령으로 준비)를 만들며, task_validation 이 구현 세션과 분리된 독립 세션에서 돌도록 절차를 고정한다. 사용자가 "하네스 세팅해줘", "품질 게이트 만들어줘", "테스트 파이프라인 구성해줘", "검증 환경 세팅해줘", "로컬 개발 환경 세팅해줘", "lint/테스트 어떻게 돌릴지 정해줘", "E2E 붙여줘"라고 하거나, 마스터 플랜 검토를 마치고 다음 단계로 넘어갈 때 반드시 이 스킬을 사용한다. 태스크 문서의 verification 이 [lint, unit, e2e, task_validation] 인데 그 게이트를 실제로 무엇으로 돌지가 문서로 없는 상태에서 구현을 시작하거나 개발 루프(loop-setup)를 세우려 할 때도 이 스킬로 먼저 하네스를 고정한다. --- # 하네스 세팅 (Layer 4) ## 이 스킬의 목적 하네스는 **"태스크 하나가 끝났다고 말해도 되는 조건"**을 코드가 아니라 문서와 명령으로 고정한 것이다. 마스터 플랜의 모든 태스크가 `verification: [lint, unit, e2e, task_validation]` 을 달고 있는데, **그 네 이름이 실제로 무엇인지는 아직 아무 데도 없다.** Layer 3이 일부러 비워 두고 여기로 넘겼다. 여기서 만든 것이 뒤에서 이렇게 쓰인다. ``` 태스크 하나 (phase-02-task-03.md, verification: [lint, unit, e2e, task_validation]) → 루프가 구현을 끝내고 quality_gates/lint.md 의 명령을 돌린다 (Layer 5) → unit → e2e 순으로 이어 돌린다 → task_validation 은 구현한 세션을 닫고 새 세션이 돌린다 ← 이게 이 레이어의 핵심 → 넷 다 통과해야 completed. 실패하면 수정 후 재실행 (최대 3회, Layer 5) ``` 그래서 이 하네스의 품질은 "도구를 잘 골랐는가"가 아니라 세 가지로 판정된다. 1. **명령 한 줄로 돌고 종료 코드로 성패가 갈리는가** — 사람이 로그를 읽어야 판정되는 게이트는 게이트가 아니다 2. **구현한 쪽이 스스로 통과 판정을 내릴 수 없는가** — task_validation 이 분리된 세션에서 도는 이유가 이것이다 3. **아직 못 도는 게이트가 '통과'로 보이지 않는가** — 테스트 0건 실행은 초록불이 되고, 초록불은 아무도 다시 안 본다 ### 여기서 하는 일과 하지 않는 일 | 하지 않는 것 | 어디서 하는가 | |---|---| | 요구사항·기술 결정·설계를 새로 정하거나 바꾸는 것 | PRD·TRD·아키텍처 — 사용자가 정하면 이 세션이 그 문서부터 고치고 이어 간다(10단계 앞의 "앞 문서 반영") | | 테스트 러너·브라우저 자동화 같은 **도구 선택** | TRD (`trd-generator`) 의 TD. 하네스는 고르지 않고 **구성**한다 | | 태스크·phase 를 나누거나 수용 기준을 고쳐 쓰는 것 | Layer 3 (`plan-generator`) — 판정 불가능한 기준을 만나면 그 10단계로 반영하고 이어 간다 | | 태스크 코드를 실제로 구현하는 것 | Layer 5 개발 루프 | | 루프의 실행 순서·실패 재시도 3회·워크트리 운용 | Layer 5 (`loop-setup`) | | STATE.md·LOG.md 의 갱신 | Layer 5 개발 루프 — 하네스는 **무엇을 기록할지의 형식**만 정한다 | | 하네스를 고쳐 나가는 것 | `harness-update` — 게이트와 스크립트는 phase 가 진행되며 계속 바뀐다 | **도구를 새로 고르지 않는 이유가 있다.** TRD가 TD-0xx 로 고른 도구에는 근거 요구사항과 되돌리기 비용이 붙어 있다. 하네스가 여기서 다른 것을 고르면 그 근거가 끊기고, 되돌리기 비용을 아무도 계산하지 않은 결정이 파이프라인 한복판에 들어간다. TRD가 정한 것은 그대로 쓴다. **TRD가 침묵한 것 중 되돌리기 비용이 낮은 것**(포매터 규칙, 커버리지 보고 형식 등)은 하네스가 정하고 `(하네스 결정)` 이라고 표시한 뒤 보고한다. **비용이 큰 것**(러너 교체, 브라우저 도구 추가)은 사용자에게 묻고, 정해지면 TRD 에 TD 로 먼저 적은 뒤 구성한다("앞 문서 반영"). ### 하네스는 코드가 아직 없는 상태에서 세워진다 플랜 직후에 서기 때문에, 하네스를 세우는 시점에 `package.json` 도 없고 테스트 파일도 0개인 것이 정상이다. 여기서 가장 흔한 사고가 이것이다 — **테스트 0건을 실행하면 대부분의 러너가 성공으로 끝난다.** 그러면 lint 도 unit 도 e2e 도 초록불이고, 하네스는 처음부터 아무것도 막지 않는 장식이 된다. 그래서 게이트마다 **활성 시점**을 적는다. "이 태스크가 완료되면 이 게이트가 실제로 판정을 시작한다"는 태스크 ID다. 활성 전에는 **초록불이 아니라 `미활성`** 으로 구별되어야 한다. 미활성 게이트는 통과가 아니라 "아직 판정하지 않음"이고, 그 상태로 태스크를 완료시킬지는 루프가 아니라 사람이 정할 일이다. **플랜에 이미 도구 설정 태스크가 있으면 하네스가 그것을 대신 구현하지 않는다.** 플랜의 phase-01 은 보통 프로젝트 뼈대와 타입 검사·마이그레이션 실행기를 세우는 태스크로 시작한다. 하네스는 게이트를 **정의**하고 그 태스크를 활성 시점으로 가리킨다. 같은 것을 두 곳에서 만들면 한쪽이 반드시 낡는다. --- ## 1단계. 입력 확보와 사전 점검 다섯 곳을 읽는다. 앞 세 개는 **무엇을 검증해야 하는가**를 주고, 뒤 두 개는 **무엇을 언제 검증할 수 있는가**를 준다. | 입력 | 여기서 무엇을 꺼내는가 | |---|---| | `docs/PRD.md` | 유저 스토리와 P0 흐름 → **E2E 흐름 목록의 재료**. NFR 중 관찰로만 확인되는 것 | | `docs/TRD.md` | 테스트 절의 도구(TD)와 **필수 커버 영역 표**(TRD 8.1절 "반드시 덮어야 할 영역" — 하네스에서는 이 이름으로 부른다) → unit 게이트가 덮어야 할 목록. 로컬 개발 환경 절의 **구성 요소 표** → local-dev 가 준비할 것 | | `docs/ARCHITECTURE/*.md` | 계층 경계와 의존 방향(lint 로 강제할 수 있는 규칙), 각 설계 단위의 검증 기준 | | `plan_setup/PLAN.md` | **`Layer 4로 넘기는 검증` 절 — 이것을 위해 여기 왔다.** 태스크 수용 기준으로 담기지 못한 NFR 목록이 여기 있다. phase 구성과 P0 완료 지점 | | `plan_setup/phase-*/phase-*-tasks/*.md` | 각 태스크의 `verification`·`files`·`architecture`·`status`·`acceptance_criteria`. 게이트가 실제로 무엇에 걸리는지 | **PLAN.md 의 `Layer 4로 넘기는 검증` 절을 요약하지 말고 항목 단위로 처리한다.** 그 표의 각 행은 "태스크 하나로는 판정할 수 없어서 하네스가 받아 가야 하는 요구사항"이고, 여기서 배치하지 않으면 그 요구사항은 플랜에서도 빠지고 하네스에서도 빠져 **아무도 검증하지 않는 상태**가 된다. 배치처는 셋 중 하나다 — ①어느 게이트가 덮는다 ②phase 완료 시에만 도는 측정으로 둔다 ③하네스로도 못 덮는다(운영 관측 등). ③이면 그 사실을 `HARNESS.md` 의 '하네스 밖' 절과 보고에 남긴다. **조용히 빠뜨리는 것과 명시적으로 밖에 두는 것은 다르다.** **PLAN 7절만 보고 끝내지 않는다.** 그 절은 "태스크로 담기지 못한 것"의 목록이라, **태스크 수용 기준 안에 들어간 채로 측정 수단이 필요한 것**은 거기 없다 — "질의가 3개이고 응답 P95가 1.5초 이내다", "PDF 생성이 10초 이내다" 같은 기준이다. 이런 기준은 태스크에 배분되어 있어서 하네스가 안 봐도 되는 것처럼 보이지만, **그것을 재는 수단은 게이트가 갖고 있어야 한다.** 없으면 그 기준은 판정할 수 없고, 판정할 수 없는 기준은 통과로 기록되거나 태스크를 세운다. 그래서 태스크 수용 기준을 훑을 때 **숫자가 붙은 기준**을 따로 모아, 그것을 잴 수단이 어느 게이트에 있는지 정한다. **앞 문서가 없으면 하네스를 세우지 않는다.** 특히 플랜이 없으면 게이트가 무엇에 걸리는지, 활성 시점이 언제인지를 정할 근거가 없어 "일반적으로 좋은 파이프라인"을 만들게 되고, 그건 이 프로젝트의 어떤 요구사항과도 연결되지 않는다. `plan-generator` 를 먼저 제안한다. ### 사용자 OS 를 먼저 확인한다 `scripts/` 의 로컬 개발 스크립트가 OS 에 직접 걸린다. **지금 세션이 도는 OS를 확인하고 그것을 기본으로 삼는다.** TRD가 개발자 OS를 미결(OQ)로 남겨 두었으면 그 사실을 기억해 두었다가, 확인된 값과 다르면 보고에 올린다. ### blocked 태스크는 검증 대상이 아니다 `status: blocked` 인 태스크는 `blocked_by` 에 OQ·CF ID를 갖고 있고, **아직 정해지지 않은 것 위에 있다.** 그 태스크의 수용 기준을 E2E 흐름이나 필수 커버 영역에 넣으면 하네스는 처음부터 실패한 채로 서고, 빨간불이 상시화되면 게이트 전체가 무시된다. 차단된 것은 **`대기`** 로 표시해 목록에 남기되 실행 대상에서 뺀다. 차단이 풀리면 `harness-update` 가 활성으로 올린다. 같은 취급을 받는 것이 둘 더 있다 — Layer 3이 폐기한 태스크(`blocked_by: [폐기]`)와 루프가 3회 소진 뒤 사용자 답으로 건너뛰어 `failed` 로 남긴 태스크. 둘 다 그 태스크에 걸린 흐름을 실행 대상에 두면 상시 빨간불이 된다. `harness-update` 가 `대기` 로 빼되 "해소를 기다리는 것이 아니다"라고 표시한다. ## 2단계. 네 게이트의 공통 규칙 개별 게이트를 설계하기 전에, 넷 모두에 걸리는 것을 먼저 정한다. **순서는 lint → unit → e2e → task_validation 이고, 앞이 실패하면 뒤를 돌리지 않는다.** 비용이 싼 순서이기도 하지만 더 중요한 이유가 있다 — 타입이 안 맞는 코드로 돌린 E2E 실패는 그 흐름이 깨졌다는 뜻이 아니라 그냥 컴파일이 안 된다는 뜻이라, 원인을 찾는 데 시간만 쓴다. **게이트는 읽기 전용 판정이다.** 자동 수정(`--fix`, 포매터 쓰기, 스냅샷 갱신)을 게이트 안에서 돌리지 않는다. 게이트가 코드를 고치면 "무엇이 통과했는가"가 흐려지고, 고친 결과를 다시 자기가 검사해 항상 통과한다. 자동 수정은 구현 단계에서 사람이나 루프가 돌리는 것이고, 게이트는 그 결과를 판정만 한다. **명령 한 줄, 종료 코드로 판정.** 각 게이트 정의서에는 실제로 붙여 넣어 돌릴 수 있는 명령이 정확히 하나 있어야 한다. "테스트를 돌린다" 같은 서술은 루프가 매번 다르게 해석하고, 다르게 해석된 게이트는 비교할 수 없다. **게이트 문서의 이름은 `verification` 값과 같다.** 태스크에 `verification: [lint, unit, e2e, task_validation]` 이 있으면 `quality_gates/lint.md`·`unit.md`·`e2e.md`·`task_validation.md` 다. 대응표를 두지 않기 위해서다 — Layer 3이 태스크 ID를 파일 이름과 같게 만든 것과 같은 이유이고, 표는 게이트가 늘거나 이름이 바뀔 때 낡는다. ### 상태 어휘는 넷이다 게이트·E2E 흐름·필수 커버 영역의 상태를 이 넷으로만 적는다. 갱신할 때마다 새 이름을 만들면 `harness-update` 가 무엇을 무엇으로 올려야 하는지 매번 다시 정하게 된다. | 상태 | 뜻 | |---|---| | **활성** | 지금 판정한다. 실패하면 태스크가 완료되지 않는다 | | **부분 활성** | 걸린 태스크 중 일부만 완료되어 **그만큼만 판정한다.** 남은 태스크가 무엇인지 함께 적는다 | | **미활성** | 활성 시점에 아직 도달하지 않았다. **통과가 아니라 "아직 판정하지 않음"이다** | | **대기** | `blocked` 태스크에 걸려 있다. 차단이 풀릴 때까지 실행 대상이 아니다 | **`부분 활성` 이 필요한 이유가 있다.** 필수 커버 영역 하나가 태스크 여럿에 걸리는 일이 흔하다 (소유권 판정은 상세·목록·검색·PDF 경로에 각각 걸린다). 그중 둘만 끝난 상태를 `활성` 으로 적으면 나머지 경로가 안 덮인 채로 덮였다고 말하게 되고, `미활성` 으로 적으면 이미 도는 검증이 없는 것처럼 보인다. **둘 다 사실과 다르고, 사실과 다른 표는 다음 갱신에서 잘못된 판단의 근거가 된다.** **미활성과 대기를 구별한다.** 미활성은 시간이 지나면 저절로 풀리고, 대기는 사람이 미결을 풀어야 풀린다. 같은 이름으로 적으면 "언젠가 되겠지"로 묶여서 아무도 차단을 해소하지 않는다. **게이트를 늘리지 않는다.** 원본 사양이 정한 4단계가 전부다. 타입 검사·보안 스캔·접근성 검사처럼 추가하고 싶은 것은 네 게이트 중 하나 안으로 넣는다(타입 검사는 lint, 계층 위반 검사는 lint 또는 unit). 다섯 번째 게이트를 만들면 태스크의 `verification` 기본값과 어긋나고, 그 값을 고치는 것은 Layer 3의 일이다. **게이트를 늘리는 편이 낫다고 판단되면 만들지 말고 보고한다.** ## 3단계. lint 게이트 **무엇을 판정하는가** — 코드가 프로젝트의 형식·타입·구조 규칙을 지켰는가. 동작은 보지 않는다. 세 가지를 이 게이트에 넣는다. - **정적 검사와 형식 검사** — 검사만 하고 고치지 않는다 - **타입 검사** — 있으면 반드시 넣는다. 파일 하나를 고쳤을 때 **다른 파일이 깨지는 것**은 여기서만 잡힌다 - **아키텍처가 정한 구조 규칙** 중 기계로 검사되는 것 — 계층 간 의존 방향, 금지된 import 경로 등 - **의존성 취약점 스캔** — 외부 패키지에 알려진 결함이 있는지. 내 코드가 아니라 **남의 코드**를 보는 검사이고, 명령 한 줄에 몇 초라 여기 붙일 수 있다. **임계 수준(어느 심각도부터 막을 것인가)은 근거가 있어야 한다** — TRD·PRD에 보안 요구사항이 있으면 그것을 근거로 적고, 없으면 하네스가 정한 값임을 `(하네스 결정)` 으로 표시한다 **구조 규칙을 lint 로 강제할 때는 오류 메시지에 고치는 방법을 넣는다.** "이 import는 금지됨"만 나오면 루프가 규칙을 우회하는 쪽으로 고치고, 그건 규칙이 막으려던 것을 그대로 통과시킨다. 어느 계층을 거쳐야 하는지까지 메시지에 있으면 한 번에 맞게 고친다. **수정한 파일만이 아니라 프로젝트 전체를 돈다.** 태스크는 파일 하나의 수정이지만 그 영향은 하나가 아니고, lint 는 전체를 돌아도 빠르다. 범위를 좁혀 얻는 몇 초는 "내 파일은 깨끗한데 빌드가 안 되는" 상태를 만든다. ## 4단계. unit 게이트 **무엇을 판정하는가** — 수정된 단위가 의도대로 동작하고, 이미 되던 것이 깨지지 않았는가. **TRD의 필수 커버 영역 표가 이 게이트의 목표다.** TRD는 도구와 "반드시 덮어야 할 영역"까지만 정해 두었고 (금액 계산, 소유권 판정, 상태 전이 같은 것들), **각 영역에 실제로 실행되는 테스트가 최소 1건씩 있는지**가 이 게이트가 성숙했다는 뜻이다. 그 대응을 `unit.md` 에 표로 남긴다 — 영역 / 근거 요구사항 / 그 영역을 덮는 태스크 / 상태. **두 묶음을 구분한다.** 그 태스크의 `files` 에 딸린 테스트 파일과, 전체 스위트(회귀)다. 전체 스위트가 게이트의 판정 대상이고, 딸린 테스트만 돌려서 통과시키면 다른 곳이 깨진 채로 완료된다. **커버리지 숫자를 게이트로 두지 않는다.** 숫자를 맞추라고 하면 숫자를 맞추는 테스트가 생기고, 그건 정작 틀리면 안 되는 것을 덮지 않는다. 판정 기준은 숫자가 아니라 **필수 커버 영역이 실제로 덮였는가**다. 커버리지는 보고 값으로만 남긴다. **통합 테스트가 실제 DB에 붙는다면 그 전제를 명시한다** — `local-dev` 로 의존 서비스가 떠 있어야 이 게이트가 돈다. 전제가 안 적히면 루프는 실패 원인을 코드에서 찾고, 원인은 DB가 안 떠 있는 것이다. **그리고 공유 DB 를 쓰는지를 `HARNESS.md` 2절에 한 줄로 적는다** — 어느 게이트가 DB 에 붙는지, 그중 **스키마를 바꾸는 테스트**(제약을 떼었다 붙이기, 테이블 생성·삭제)가 있는지. 병렬 격리를 정하는 `loop-setup` 이 이 줄을 보고 슬롯별 DB(격리 1·2)를 추천할지 정한다. 실전(ERP, 2026-09)에서 이 사실이 어디에도 안 적혀 격리 3(공유 DB)으로 시작했고, 두 슬롯의 unit 이 한 DB 에서 서로의 데이터와 제약을 건드려 여섯 phase 동안 흔들렸다. ## 5단계. e2e 게이트 — 흐름 목록은 PRD를 읽고 정한다 **무엇을 판정하는가** — 사용자가 브라우저에서 실제로 하는 흐름이 끝까지 동작하는가. 단위 테스트가 전부 통과해도 화면과 서버를 이어 붙이면 안 되는 일이 흔하고, 그것만 잡는 게이트다. **흐름 목록을 정하는 것이 이 단계의 실제 작업이다.** TRD는 도구만 정했고, 어떤 흐름을 덮을지는 여기로 넘어왔다. PRD의 유저 스토리와 P0 요구사항에서 뽑는다. 흐름 하나마다 이렇게 적는다. | 항목 | 왜 필요한가 | |---|---| | 흐름 ID (`FLOW-01`) 와 이름 | 태스크·보고·실패 기록이 같은 문자열로 이 흐름을 가리킨다 | | 단계 (사용자가 하는 행동 순서) | 재현 가능해야 다른 세션이 같은 것을 돌린다 | | 덮는 요구사항 (FR·NFR) | 이 흐름이 없어지면 무엇이 안 덮이는지가 보인다 | | **실패 경로 최소 1개** | 성공 경로만 도는 E2E는 절반만 검증한다. 권한 없는 접근, 잘못된 입력, 중복 요청 | | 활성 phase | 이 흐름을 구성하는 마지막 태스크가 완료되는 phase. 그전에는 `대기` | **흐름을 많이 만들지 않는다.** E2E는 태스크마다 회귀로 도는 가장 느린 게이트다. 느려지면 사람이 게이트를 끄고, 꺼진 게이트는 없는 것과 같다. **P0 흐름을 먼저 채우고 기능 전수 나열은 하지 않는다** — 기능 단위 검증은 unit 의 일이고, E2E 는 "여러 태스크가 이어 붙어야 비로소 성립하는 것"만 본다. **두 묶음으로 나눈다.** - **매 태스크 묶음** — 활성화된 흐름 중 빠른 것. 이게 게이트의 기본 판정 대상이다 - **phase 완료 묶음** — 시간이 걸리는 측정(전 구간 소요 시간, 브라우저 여러 종, 대량 데이터)은 여기 둔다. Layer 3이 phase 완료를 자동 판정 단위로 만들어 두었으므로 걸어 둘 자리가 있다. **돌리는 명령은 `e2e.md` 의 `## phase 완료 묶음 명령` 절 하나에 둔다** — 루프의 구동 스크립트가 그 절을 그때그때 읽어 판단 없이 돌린다(판단 자식은 허용 목록·10분 상한 때문에 못 돌린다). 그래서 그 절에는 지금 활성인 묶음 흐름만 도는 명령이 있어야 하고, 흐름이 활성되거나 `대기` 로 빠지면 `harness-update` 가 그 명령(또는 그것이 읽는 설정)을 함께 고친다 **아직 화면이 없는 phase 에서도 이 게이트를 빼지 않는다** — 그때 e2e 는 회귀로 돈다. 활성 흐름이 0개면 그 사실이 `미활성` 으로 드러나야 하고, 조용한 통과가 되면 안 된다. ### 흔들리는 E2E 는 꺼진 게이트와 같다 같은 코드로 두 번 돌렸는데 한 번은 통과하고 한 번은 실패하면, 사람은 곧 **실패를 무시하기 시작한다.** 느려서 꺼지는 것과 결과가 같고, 오히려 나쁘다 — 꺼진 게이트는 꺼진 줄이라도 안다. 그래서 흔들림을 줄이는 규칙을 `e2e.md` 에 함께 적는다. - **선택자는 역할과 이름으로 잡는다** — 사용자에게 보이는 것(버튼 이름, 라벨)으로 찾고, 그것으로 안 되는 곳에만 최소한의 표식을 붙인다. 화면 구조를 따라가는 선택자는 관계없는 수정에서 깨진다 - **실행마다 고유한 테스트 데이터를 만든다** — 고정된 이메일·번호를 쓰면 두 번째 실행이 첫 번째가 남긴 것과 부딪힌다. 유일 제약이 걸린 프로젝트에서는 특히 그렇다 - **인증 상태는 한 번 만들어 재사용한다** — 흐름마다 로그인을 다시 하면 그만큼 느려지고, 로그인 화면이 바뀔 때마다 모든 흐름이 함께 깨진다. 인증 흐름 자체를 검증하는 흐름 하나만 진짜로 로그인한다 - **외부에서 와야 하는 값은 실제 경로에서 가져온다** — 메일로 오는 재설정 링크 같은 것은 로컬 메일 캡처에서 읽는다. 값을 하드코딩하면 그 흐름은 실제로 무엇도 검증하지 않는다 - **값은 한 번 읽어 비교하지 않고, 조건이 될 때까지 기다리는 단언으로 본다** — 화면은 비동기로 바뀐다. 한 번 읽은 값이 맞는지는 읽은 순간의 운이다 - **크기·위치·색 같은 측정은 전환·로드가 끝난 뒤에 한다** — 애니메이션 중간이나 이미지가 뜨기 전에 잰 값은 매번 다르다 - **목록에서 특정 행을 찾을 때 순서나 첫 페이지를 가정하지 않는다** — 다른 흐름이 만든 데이터가 섞이면 자리가 바뀐다. 검색·필터로 좁혀 찾는다 **이 목록은 세팅 때로 끝나지 않고 자란다.** 흔들림을 안정화할 때마다 `harness-update` 가 그 원인 패턴을 `e2e.md`(unit 이면 `unit.md`)의 흔들림 규칙 표에 한 줄 더한다. 그러지 않으면 같은 실수를 다음 태스크의 테스트가 반복하고, 루프는 그 규칙을 워커 지시문에 매번 손으로 적게 된다. 스펙을 쓰는 세션은 이 표를 먼저 읽는다. ### 단언은 세 층으로 한다 화면만 보면 **"성공 메시지는 떴는데 저장은 안 된"** 상태를 통과시킨다. 흐름마다 세 층을 확인한다. 1. **화면 상태** — 사용자가 보는 것 (메시지, 이동한 화면, 목록에 나타남) 2. **서버 상태** — 실제로 남았는가 (DB 행, 발급된 토큰, 기록된 이력) 3. **사용자에게 보이는 결과** — 다시 조회했을 때·다른 계정에서 볼 때 기대대로인가 ### 실패는 고치기 전에 분류한다 E2E 실패를 없애는 가장 빠른 길은 **단언을 느슨하게 만드는 것**이고, 그렇게 하면 게이트가 조용히 죽는다. 그래서 실패를 만나면 먼저 셋 중 무엇인지 정한다. | 분류 | 어떻게 알아보는가 | 무엇을 고치는가 | |---|---|---| | **진짜 결함** | 수동으로 같은 순서를 밟아도 같은 결과 | 코드. 태스크 수정으로 돌아간다 | | **낡은 스펙** | 화면이나 규약이 의도적으로 바뀌었고 흐름이 옛 모습을 기대한다 | 흐름 정의. `harness-update` 의 일이다 | | **환경 문제** | 의존 서비스가 안 떴거나 데이터가 남아 있다 | `local-dev` 로 환경을 다시 준비한다. 코드도 스펙도 고치지 않는다 | **분류 결과를 기록에 남긴다.** Layer 5가 실패를 3회까지만 재시도하는데, 세 번이 전부 환경 문제였다면 그건 구현이 잘못된 것이 아니라 하네스가 잘못된 것이고, 그 구별이 기록에 없으면 아무도 하네스를 고치지 않는다. ## 6단계. task_validation 게이트 — 분리된 세션에서 돈다 **이 게이트가 하네스의 핵심이고, 나머지 셋과 성격이 다르다.** 앞의 셋은 도구가 판정하지만 이것은 에이전트가 판정한다. 그리고 **그 에이전트는 그 태스크를 구현한 세션이 아니어야 한다.** ### 왜 분리하는가 구현한 세션은 자기가 무엇을 **의도했는지** 알고 있다. 그래서 화면이 실제로 그렇게 동작하지 않아도 그렇게 보이고, 수용 기준의 애매한 문장을 자기 구현에 맞는 쪽으로 읽는다. 나쁜 의도가 아니라 **정보의 문제**다 — 구현 중에 형성된 판단이 관찰을 덮어쓴다. 그래서 코드 작성과 검증을 의도적으로 떼어 놓는다. 검증자에게는 **태스크 문서에 적힌 문장이 정보의 전부**이고, 그건 사용자가 나중에 그 기능을 처음 써 볼 때와 같은 조건이다. ### 검증자에게 주는 것과 주지 않는 것 | 준다 | 주지 않는다 | |---|---| | 태스크 문서 경로 (`plan_setup/phase-NN/phase-NN-tasks/phase-NN-task-MM.md`) | 구현 세션의 대화·요약·"무엇을 했는지" 설명 | | 실행 중인 애플리케이션 (`local-dev` 로 띄운 것) | 구현자가 작성한 통과 보고나 자체 점검 결과 | | `quality_gates/task_validation.md` (절차와 판정 형식) | "이 부분은 이렇게 동작하도록 만들었다" 류의 안내 | | 필요하면 `architecture` 가 가리키는 설계 절 | 앞 게이트가 통과했다는 사실 이상의 실행 로그 | **주지 않는 목록이 이 게이트의 실질이다.** 하나라도 새면 분리한 의미가 없어진다. "참고하라고 요약만 붙이는 것"이 가장 흔한 누수인데, 그 요약이 바로 구현 중에 형성된 판단이다. ### 어떻게 판정하는가 **기본은 실행 관찰이다.** 애플리케이션을 실제로 조작해서 수용 기준 한 줄마다 결과를 본다. **코드를 읽는 것은 수용 기준이 코드를 대상으로 할 때만** 한다 — "이 모듈 밖에 해당 테이블 INSERT가 0건이다" 같은 기준은 관찰로 판정되지 않으므로 검색이 정당한 수단이다. 그 외에는 코드를 근거로 PASS를 주지 않는다. **코드가 그렇게 쓰여 있는 것과 그렇게 동작하는 것은 다르고**, 그 차이를 잡으라고 이 게이트가 있다. ### 무엇으로 관찰하는가 원본 사양이 이 게이트를 **"서비스(브라우저)를 통해 해당 기능을 검증"**하는 것으로 정했다. 그러니 관찰 수단의 기본은 브라우저이고, **검증자가 무엇으로 브라우저를 조작하는지가 정해져 있지 않으면 이 게이트는 코드 읽기로 조용히 내려앉는다** — 그게 이 게이트가 막으려던 것이다. 하네스를 세울 때 이 환경에 어떤 수단이 있는지 확인해 `HARNESS.md` 에 적고, `task_validation.md` 에 그 순서를 둔다. | 순위 | 수단 | 언제 | |---|---|---| | ① | 에이전트가 직접 조작하는 브라우저 도구 (브라우저 자동화 MCP 등) | 화면이 있는 기준 전부. **기본** | | ② | e2e 게이트의 브라우저 자동화 도구로 검증자가 **그 자리에서 일회성 스크립트**를 써서 실행 | ①이 없을 때. 스크립트는 저장소에 남기지 않고, 제품 코드를 건드리지 않는다 | | ③ | HTTP 호출·DB 질의 | 화면이 없는 기준 (API 응답, 저장된 값, 권한 오류) | ②로 내려간 판정은 **판정 블록에 그 사실을 적는다**(`관찰 수단: 2`). 조용히 내려가면 ①로 본 것과 구별되지 않는다. ①도 ②도 없으면 화면 기준은 `판정 불가` 이고 그 사실을 사용자에게 알린다 — 코드를 읽고 PASS 를 주는 것보다 낫다. ③은 강등이 아니다. 기준이 화면을 말하지 않으면 HTTP·DB 가 곧 관찰이다. 판정의 규칙 넷. - **수용 기준 한 줄마다 PASS / FAIL 과 관찰한 증거를 적는다.** 무엇을 했고 무엇이 보였는지. **증거 없는 PASS 는 PASS 가 아니다** — 그건 구현 문서를 읽고 동의한 것에 불과하다 - **검증자는 고치지 않는다.** 고치는 순간 검증자가 구현자가 되고, 다음 기준부터는 자기 코드를 검증한다. 실패는 실패로 돌려보내고 수정은 구현 쪽에서 한다 - **원인 분석이나 수정 제안을 하지 않는다.** 관찰한 것만 적는다. 원인 추정이 붙으면 구현 세션이 그것을 따라가고, 틀린 추정이면 엉뚱한 곳을 고친 뒤 다시 실패한다 - **판정할 수 없으면 PASS 도 FAIL 도 아니다.** 기준이 관찰 가능하게 쓰이지 않았으면 `판정 불가` 로 돌려보낸다. 그건 구현의 문제가 아니라 태스크 문서의 문제이고, Layer 3으로 돌아갈 일이다. **환경 때문에 관찰 자체를 못 했으면 `환경 문제`** 로 적는다 — 넷째 값이 있는 이유는 루프가 그것을 FAIL 로 읽어 코드를 고치지 않게 하기 위해서다 - **판정 블록은 `END` 줄로 끝낸다.** 루프는 이 줄로 잘린 파일을 알아본다 ### 결과를 어디에 남기는가 **새 상태 파일도 증거 디렉터리도 만들지 않는다.** 진행 상태는 `STATE.md`, 이력은 `LOG.md` 가 이미 갖고 있고 (Layer 3이 만들고 Layer 5가 갱신한다), 세 번째 기록처가 생기면 셋이 서로 어긋난다. 검증자는 판정 블록을 출력하고, 루프가 그것을 두 곳으로 옮긴다. - `STATE.md` 의 `Last Verification` — **지금 상태 한 줄.** 태스크 / 결과 / 실패면 사유 - `LOG.md` — **관찰한 증거가 여기 남는다.** 실패한 기준 번호와 "무엇을 했더니 무엇이 보였는가". 통과는 한 줄이면 되지만 **실패는 관찰을 남긴다** — 그게 없으면 다음 세션이 같은 것을 처음부터 다시 확인한다 `task_validation.md` 는 **그 블록의 형식**만 고정한다. 증거가 텍스트로 남는다는 뜻이므로, 관찰을 "정상 동작함" 대신 **재현 가능한 문장**으로 쓰게 한다 — "계정 B로 A의 인보이스 URL을 열었더니 200과 본문이 보였다". ## 7단계. scripts/ — 로컬 개발 환경 **무엇을 하는 스크립트인가** — 명령 하나로 개발에 필요한 것이 전부 준비된 상태를 만든다. DB·메일 캡처·캐시 같은 의존 서비스를 띄우고, 스키마와 시드를 적용하고, 앱 개발 서버를 실행한다. 이게 하네스의 일부인 이유는 **unit 의 통합 테스트와 e2e·task_validation 이 전부 실행 중인 환경을 전제하기 때문**이다. 환경을 띄우는 방법이 사람 머릿속에만 있으면 분리된 검증 세션이 그것을 재현하지 못한다. ### 파일 이름과 OS **원본 사양이 적은 이름을 그대로 쓴다** — `local-dev.sh`, `local-dev.bat` 또는 `local-dev.ps1`. **사용자 OS 를 우선한다**: Windows 면 `local-dev.ps1` 을, macOS·Linux 면 `local-dev.sh` 를 주 스크립트로 만든다. **기본은 한 OS 용 하나다.** 두 벌을 만들면 한쪽만 갱신되어 갈라지고, 갈라진 스크립트는 "내 컴퓨터에서는 되는데"를 그대로 재현한다. 다른 OS 가 실제로 필요해지면 그때 `harness-update` 가 추가한다. ### 환경이 바뀌어도 알아서 준비되게 하는 방법 DB만 있던 환경에 캐시가 붙고 API 서버가 하나 더 생기는 일은 phase 가 진행되며 반드시 일어난다. 그때마다 스크립트를 다시 쓰지 않으려면, **구성 요소 목록이 단일 출처이고 스크립트가 그것을 그대로 반영**해야 한다. - **구성 요소 표를 `HARNESS.md` 에 둔다** — 이름 / 형태(컨테이너·호스트 실행·로컬 설치) / 준비 여부를 판정하는 명령 / 왜 필요한가. TRD 9.3절의 표(구성 요소 / 왜 필요한가 / 근거 결정)를 **그대로 받아** 형태와 준비 판정 열을 더한다. TRD에 없는 구성 요소를 여기서 조용히 넣으면 그건 설계 유발 충돌이다 — 사용자에게 묻고, 넣기로 하면 TRD 9.3절과 근거 TD 부터 고친 뒤 표에 넣는다("앞 문서 반영") - **스크립트는 표의 한 행을 같은 모양의 블록 하나로 구현한다** — 이미 준비됐는지 확인 → 없으면 준비 → **준비될 때까지 대기** → 실패하면 무엇을 해야 하는지 출력. 새 구성 요소는 블록 하나 추가로 끝난다 - **필요한 것의 목록을 스크립트 여기저기에 흩뿌리지 않는다.** 상단에 모아 두면 표와 대조할 수 있고, 대조할 수 있어야 `harness-update` 가 무엇을 고칠지 안다 - **포트·계정·경로를 코드에 박지 않는다.** 환경변수 파일에서 읽는다. 값이 바뀌었을 때 고칠 곳이 한 군데여야 한다 ### 스크립트가 지켜야 할 것 - **아직 없는 단계는 실패가 아니라 건너뜀이다** — 코드가 0줄인 phase 에서는 스키마도 앱 진입점도 없다. 그때 스키마·시드 단계와 앱 실행 단계는 "대상 없음 — 건너뜀"을 출력하고 **0으로** 끝난다(그 단계의 활성 시점 태스크가 완료되기 전에는 판정 대상이 아니다 — 게이트의 `미활성` 과 같은 논리). 안 그러면 첫 태스크부터 `local-dev` 가 비0으로 끝나고, 루프는 "0이 아니면 환경 문제"로 읽어 멀쩡한 태스크를 재준비와 `harness-update` 로 몰고 간다. 어느 태스크부터 각 단계가 판정 대상이 되는지를 `HARNESS.md` 2절 종료 코드 표에 함께 적는다 - **띄운 것은 세션이 끝나도 살아 있다** — 의존 서비스는 물론 앱 개발 서버도 **스크립트를 부른 셸과 분리된 프로세스**로 띄운다. 루프는 구현 세션이 닫힌 뒤 다른 세션이 검증하므로, 서버가 부른 셸에 매여 있으면 검증 세션이 열릴 때마다 앱이 죽어 있고 그 실패는 전부 '환경 문제'로 찍힌다. 포그라운드로 붙잡고 있는 스크립트는 헤드리스 세션을 영원히 안 끝나게도 만든다 - **멱등하다** — 두 번 돌려도 안전하다. 이미 떠 있는 것을 다시 띄우지 않는다. 루프는 세션마다 이 명령을 돌리므로, 두 번째 실행이 환경을 망가뜨리면 두 번째 태스크부터 막힌다 - **준비될 때까지 기다린다** — 포트가 열린 것과 쓸 수 있는 것은 다르다. DB는 실제 질의 한 번, 앱 서버는 응답 한 번으로 판정한다. 여기를 건너뛰면 첫 테스트만 이유 없이 실패하는 증상이 상시화된다 - **전제부터 확인하고 멈춘다** — 필요한 런타임·컨테이너 엔진이 없으면 절반쯤 진행하지 말고 **무엇이 없는지와 어떻게 설치하는지**를 출력하고 종료한다 - **어디서 멈췄는지 말한다** — 실패한 단계 이름과 다음 행동을 출력하고, **종료 코드를 단계마다 다르게** 둔다. 성공 0, 그리고 전제 / 환경변수 / 의존 서비스 / 스키마·시드 / 앱 실행의 다섯 단계에 서로 다른 0이 아닌 값. 이 표를 `HARNESS.md` 2절에 적는다. **루프와 분리된 검증 세션은 이 숫자만 보고 실패를 가른다** — 0이 아니면 그 자체로 환경 문제이고, 전제·환경변수 계열은 사람이 설치·설정할 일이라 사용자에게, 의존 서비스·스키마·앱 계열은 재준비 후 `harness-update` 로 간다. 코드가 하나뿐이면 이 갈림이 불가능해 출력을 읽고 추측하게 되고, 추측은 세션마다 다르다 - **초기화 수단을 둔다** — `-Reset` / `--reset` 으로 데이터를 지우고 처음부터. 검증이 남긴 데이터가 다음 검증의 결과를 바꾸는 것을 막는다 - **띄우는 것 말고도 세 가지를 할 수 있어야 한다** — 지금 무엇이 떠 있는지(`-Status` / `--status`), 안 뜬 것의 로그(`-Logs` / `--logs`), 내리기(`-Down` / `--down`). **이게 없으면 실패 분류가 불가능하다.** 분리된 검증 세션은 구현 과정을 모르는 채로 들어오는데, 화면이 안 뜰 때 코드가 깨진 것인지 의존 서비스가 죽은 것인지 구별할 수단이 없으면 **환경 문제를 구현 실패로 보고**하고, 루프는 멀쩡한 코드를 세 번 고치다 멈춘다. 원본 사양의 재시도 3회가 엉뚱한 이유로 소모되는 것이다. 준비 판정은 이미 스크립트 안에 있으므로 `-Status` 는 그 판정을 **띄우지 않고 출력만** 하면 된다 - **비밀값을 파일에 적지 않는다** — 예시 환경변수 파일에는 키 이름과 형식만 두고, 실제 값은 두지 않는다 - **프로세스는 자기가 띄운 것만, 같은 프로세스인지 확인하고 끈다** — `local-dev -Down`·게이트 실행기가 남은 서버·드라이버를 정리할 때 이 PC 에는 다른 슬롯의 워커 에이전트(claude·codex — codex 는 `node` 로 뜬다)와 사람의 프로그램이 함께 돈다. **이름으로 끄지 않고**(`node`·`cargo` 를 이름으로 끄면 워커가 죽는다), **PID 하나만 믿지도 않는다** — Windows 는 끝난 프로세스의 PID 를 곧바로 다른 프로세스에 다시 준다. 끌 PID 는 기록할 때 그 프로세스의 **시작 시각을 함께 적고**, 끄기 직전에 시작 시각이 같을 때만 끈다. 자손을 부모 PID 로 모을 때는 **자식의 시작 시각이 부모보다 늦은 것만** 자손으로 친다(부모가 끝나도 자식의 부모 PID 칸은 그대로 남아, 그 번호를 다시 받은 프로세스의 "자식" 처럼 보인다). Windows 에서 가장 확실한 길은 잡 오브젝트(작업 개체)로 띄워 그 잡째 끄는 것이다. 실전(2026-10-13, 동시 3): 게이트 실행기가 `cargo test --workspace` 의 자손 PID 를 15분 동안 모아 두었다가 끝날 때 전부 껐는데, 그사이 끝난 자손의 PID 를 다시 받은 다른 슬롯의 codex 검증 워커가 함께 꺼졌다 — 오류 기록 없이 사라졌고 1시간 반 동안 아무도 몰랐다. 병렬 루프에서 워커를 죽이는 가장 흔한 길은 에이전트가 아니라 하네스 스크립트다 **여러 에이전트가 동시에 도는 경우는 여기서 정하지 않는다.** Layer 5가 멀티 에이전트로 갈 때 워크트리 둘이 같은 DB와 같은 포트를 쓰게 되는데, 그것을 격리로 풀지 권한으로 풀지는 루프의 운용 방식이 정해진 뒤에야 답이 나온다. 하네스가 지금 할 일은 **포트·DB 이름을 환경변수에서 읽게 해 두는 것**과 **그 환경변수 이름과 공유 DB 사용 여부를 `HARNESS.md` 2절에 적는 것**이고 (값을 스크립트에 박지 않는 이유가 여기에도 있다), 실제 격리 방식은 `loop-setup` 으로 넘긴다. 격리 1·2로 정해지면 슬롯 번호에서 DB 이름·포트를 파생하는 분기는 `harness-update` 가 더한다. ## 8단계. 파일 쓰기 `harness_setup/` 아래에 만든다. **원본 사양이 그린 구조가 `quality_gates/` 와 `scripts/` 이고, 그 이름을 그대로 쓴다.** ``` project_name/harness_setup/ ├── HARNESS.md ├── quality_gates/ │ ├── lint.md │ ├── unit.md │ ├── e2e.md │ └── task_validation.md └── scripts/ ├── local-dev.ps1 (사용자 OS 가 Windows 인 경우. 그 외에는 local-dev.sh) └── .env.example (필요한 키 이름과 형식만. 값은 두지 않는다) ``` **도구 설정 파일은 도구가 요구하는 자리에 둔다.** 대부분의 린터·테스트 러너·브라우저 자동화 도구는 프로젝트 루트의 정해진 이름을 읽으므로, 그 파일들을 `quality_gates/` 로 옮기면 도구가 찾지 못한다. 게이트 정의서가 **그 설정 파일의 경로를 가리키는 것**으로 연결한다. `quality_gates/` 에 들어가는 것은 **게이트의 정의**(무엇을 검사하고 어떤 명령이며 무엇이 통과인가)이고, 흐름 목록처럼 도구가 위치를 강제하지 않는 것은 `quality_gates/` 아래 하위 폴더에 둘 수 있다. **이미 `harness_setup/` 이 있으면 덮어쓰기 전에 읽는다.** phase 가 진행되며 `harness-update` 가 갱신해 둔 것이라면 활성 시점·커버 영역·흐름 목록의 진행 상태가 거기 쌓여 있고, 초기값으로 덮으면 그게 사라진다. 그럴 때는 이 스킬이 아니라 `harness-update` 를 쓴다. ### `CLAUDE.md` · `AGENTS.md` 갱신 `harness_setup/` 을 쓴 뒤 프로젝트 루트의 `CLAUDE.md` 와 `AGENTS.md` 를 **같은 내용으로** 갱신한다. 사용자가 직접 연 세션과 루프의 자식 세션은 이 스킬을 읽지 않고 시작하는데, 그때 자동으로 읽히는 파일은 Claude Code 는 `CLAUDE.md`, Codex 는 `AGENTS.md` 뿐이다. 그래서 **지금 어느 호스트에서 돌든 둘 다 쓴다** — 한쪽만 쓰면 다른 호스트의 세션이 규칙을 모른 채 시작한다. 내용은 **어느 세션에서든 반드시 지킬 것**만 두고 세부 규칙은 문서를 가리킨다 — 규칙을 옮겨 적으면 두 곳이 곧 어긋나고, 어긋나면 세션은 짧은 쪽을 믿는다. - 둘 다 없으면 아래 뼈대로 만든다. 있으면 **`## 하네스 (harness_setup/)` 절과 `현재 단계` 줄만** 고친다. 다른 절과 사용자가 손으로 적은 내용은 건드리지 않는다 - 두 파일이 서로 다르면 지금 호스트의 파일을 기준으로 다른 쪽을 맞춘다. 손으로 적은 줄은 양쪽에 남긴다 - 파일은 있는데 `## 문서 지도`·`## 공통 규칙` 이 없으면(예: `/init` 이 만든 파일) 기존 내용 **아래에** 뼈대 두 절을 더한다. 기존 내용은 손대지 않는다 - 절이 없으면 `공통 규칙` 뒤에 제품·기술·구조·플랜·하네스·루프 순서의 자리에 더한다 - 전체 **100줄 안팎, 200줄을 넘기지 않는다.** 넘치면 규칙을 문서 쪽으로 돌려보내고 가리키는 줄만 남긴다 ```markdown # <프로젝트 이름> > harness-loop-fullstack 스킬이 만들고 갱신하는 파일이다. 각 절은 그 레이어의 스킬만 고친다. > `CLAUDE.md` 와 `AGENTS.md` 는 같은 내용이다. 한쪽을 손으로 고치면 다른 쪽도 같이 고친다. > 세부 규칙은 각 문서에 있다 — 여기에는 어느 세션에서든 반드시 지킬 것만 둔다. 200줄을 넘기지 않는다. ## 문서 지도 - `docs/IDEA.md` → `docs/PRD.md` → `docs/TRD.md` → `docs/ARCHITECTURE/` → `plan_setup/` → `harness_setup/` → `loop_setup/LOOP.md` - 뒤 문서는 앞 문서를 근거로 한다. 어긋나면 앞 문서가 이긴다 — 바꿔야 하면 사용자 확인 뒤 **가장 앞 문서부터** 고치고(버전·`변경 이력` 한 줄. 프로젝트 완료 뒤에는 `feature-add`·`refactor-plan` 의 후미 절로) 사이 문서를 맞춘 다음 이어 간다(변경 전파). 손으로 고칠 때도 같다. 루프 세션은 앞 문서를 고치지 않고 LOOP.md 대로 멈춘다 - 현재 단계: <마지막으로 끝낸 스킬> → 다음: <다음 스킬> ## 공통 규칙 - 앞 레이어의 **파일**만 입력이다. 대화 기억이나 추측으로 요구사항·기술 결정을 만들지 않는다 - ID(`FR-`·`NFR-`·`US-`·`OQ-`·`TD-`·`phase-NN-task-MM`)는 참조만 한다. 새 ID 는 마지막 번호 다음으로만 만든다 — OQ 는 프로젝트 전체(`HARNESS.md`·`LOOP.md` 의 미결 표까지)에서, 나머지는 그 문서에서 센다(변경 전파로 앞 문서에 더할 때도) - 사용자가 말하지 않은 값은 `(가정)` 으로 표시한다. 모호하면 추측하지 말고 묻는다 - 요청된 범위를 조용히 넓히거나 줄이지 않는다. 범위 판단은 보고한다 ``` `## 하네스 (harness_setup/)` 절에는 이것만 적는다. 명령은 정의서의 것을 그대로 옮긴다 — 여기 적힌 명령이 실제와 다르면 세션은 이쪽을 돌린다. ```markdown ## 하네스 (harness_setup/) - 로컬 환경: `` 한 명령. 게이트를 돌리기 전에 먼저 - lint `<명령>` · unit `<명령>` · e2e `<명령>` · task_validation 은 분리 세션(`harness_setup/HARNESS.md`) - 게이트를 건너뛰거나 테스트를 약하게 고쳐 통과시키지 않는다. 실패 원인이 하네스 쪽이면 `harness-update` 를 부른다 - 구현한 세션은 task_validation 을 하지 않는다. 검증 세션은 실행 중인 서비스를 관찰해 판정한다. 코드를 근거로 PASS 를 주지 않는다 - 활성 게이트: <예: lint·unit 활성, e2e·task_validation 은 phase-02-task-01 부터> ← harness-update 가 갱신 ``` ## 9단계. 자체 점검 | 확인할 것 | 통과 조건 | 걸렸을 때 | |---|---|---| | 구조 | `harness_setup/quality_gates/` 와 `harness_setup/scripts/` 가 원본 이름 그대로 존재 | 맞춘다 | | 게이트 파일 이름 | `verification` 의 네 값과 정확히 같은 이름의 문서 4개 | 맞춘다. 대응표를 만들지 않는다 | | 실행 가능성 | 게이트마다 붙여 넣어 돌릴 수 있는 명령이 정확히 하나 | 채운다. 서술만 있으면 매번 다르게 해석된다 | | 판정 방법 | 게이트마다 종료 코드로 성패가 갈림 | 고친다. 로그를 읽어야 알면 게이트가 아니다 | | 활성 시점 | 게이트마다 활성 시점이 **실제 태스크 ID** 로 적혀 있음 | 채운다. 없는 ID를 지어내지 않는다 | | 미활성 구분 | 활성 전 상태가 통과가 아니라 `미활성` 로 표시됨 | 고친다. 0건 실행은 초록불이 된다 | | 상태 어휘 | 상태가 `활성`·`부분 활성`·`미활성`·`대기` 넷 안에서만 쓰임 | 맞춘다. 새 이름을 만들면 갱신이 매번 재정의한다 | | 자동 수정 | 게이트 명령에 자동 수정이 0건 | 뺀다. 게이트는 판정만 한다 | | 필수 커버 영역 | TRD의 모든 필수 커버 영역이 unit 게이트 표에 있고 담당 태스크가 붙음 | 채운다 | | E2E 흐름 | 흐름마다 실패 경로가 최소 1개, 덮는 요구사항과 활성 phase 가 적혀 있음 | 채운다 | | phase 완료 묶음 명령 | `e2e.md` 에 `## phase 완료 묶음 명령` 절이 있고, 코드 블록이 한 줄 = 명령 하나이며 활성인 묶음 흐름만 돌고(`대기` 흐름 제외), 활성 시점 전이면 비어 있음 | 채운다. 루프의 구동 스크립트가 이 절을 읽어 돌린다 — 없거나 흐름 ID 만 있으면 phase 가 완료로 찍히지 않는다 | | E2E 흔들림 | 선택자·테스트 데이터·인증 재사용·기다리는 단언·안정 뒤 측정·순서 비가정 규칙이 `e2e.md` 에 **출처 열이 있는 표**로 있고, 실패 분류 기준이 있음 | 채운다. 흔들리는 게이트는 꺼진 게이트다. 표여야 `harness-update` 가 한 줄씩 더한다 | | 공유 DB 표시 | `HARNESS.md` 2절에 공유 DB 를 쓰는 게이트(스키마를 바꾸는 테스트 유무)와 슬롯 변수 이름이 있음 | 적는다. `loop-setup` 이 격리 추천의 근거로 읽는다 | | 단언 층 | 흐름의 확인이 화면 상태에서 끝나지 않고 서버 상태까지 감 | 채운다 | | 증거 | 판정 형식이 기준마다 관찰한 증거를 요구하고, 실패 시 그것이 LOG.md 로 감 | 채운다. 증거 디렉터리를 새로 만들지 않는다 | | 차단 취급 | `status: blocked` 태스크의 기준이 실행 대상에 0건 | 뺀다. 상시 빨간불은 게이트를 무력화한다 | | PLAN 7절 | `Layer 4로 넘기는 검증` 의 모든 항목이 게이트·phase 측정·'하네스 밖' 중 하나로 배치됨 | 배치한다. 못 덮으면 밖에 두고 보고한다 | | 측정 수단 | 숫자가 붙은 수용 기준마다 그것을 잴 수단이 어느 게이트에 있는지 정해짐 | 정한다. 못 재면 '하네스 밖'에 두고 보고한다 | | 분리 세션 | `task_validation.md` 에 검증자에게 **주지 않는 것** 목록이 있음 | 채운다. 이게 없으면 분리가 말뿐이다 | | 관찰 수단 | 이 환경의 브라우저 조작 도구를 확인해 `HARNESS.md` 에 적었고, `task_validation.md` 에 수단 순서와 강등 표기 규칙이 있음 | 채운다. 없으면 검증이 코드 읽기로 내려앉는다 | | 근거 없는 기준 | 통과선을 세운 검사마다 근거 요구사항이 있거나 `(하네스 결정)` 표시가 있음 | 표시하거나 뺀다. 합의된 적 없는 관문은 세우지 않는다 | | 도구 침범 | TRD가 정한 도구를 바꾼 곳이 0건 | 되돌린다. 바꾸고 싶으면 TRD로 보고한다 | | 게이트 개수 | 네 게이트 외에 새로 만든 게이트가 0건 | 안으로 넣거나 보고한다 | | local-dev OS | 주 스크립트가 확인된 사용자 OS 용이고 확장자가 원본 이름과 같음 | 맞춘다 | | 멱등성 | 두 번 실행해도 안전하다는 것이 스크립트에 실제로 구현됨 | 고친다 | | 분리 실행 | 앱 서버와 의존 서비스가 스크립트를 부른 셸과 분리된 프로세스로 떠서, 스크립트가 끝나도 `-Status` 가 준비됨을 보고함 | 고친다. 아니면 검증 세션마다 앱이 죽어 있다 | | 프로세스 정리 | `local-dev`·게이트 실행기가 프로세스를 끄는 곳마다 이름이 아니라 자기가 띄운 PID 이고, 끄기 직전에 기록한 시작 시각과 같은지 보며, 자손은 부모보다 늦게 시작한 것만 모음(또는 잡 오브젝트) | 고친다. PID 재사용으로 다른 슬롯의 워커 에이전트를 죽인다(실전 2026-10-13) | | 건너뜀 | 스키마·앱 단계가 대상이 없을 때 비0이 아니라 "건너뜀"과 0으로 끝나고, 판정 시작 태스크가 `HARNESS.md` 2절에 있음 | 고친다. 아니면 첫 태스크부터 환경 문제로 찍힌다 | | 대기 | 각 의존 서비스에 준비 판정이 있음 (포트 확인만으로 끝내지 않음) | 채운다 | | 상태 확인 | 띄우기 말고 상태·로그·내리기 수단이 있음 | 채운다. 없으면 검증 세션이 환경 문제를 구현 실패로 보고한다 | | 종료 코드 | 다섯 단계가 서로 다른 0이 아닌 코드로 끝나고, 그 표가 `HARNESS.md` 2절에 있으며 스크립트와 일치 | 맞춘다. 루프가 이 숫자만 보고 "사람이 고칠 것 / 재준비할 것"을 가른다 | | 비밀값 | 스크립트·예시 파일에 실제 비밀값이 0건 | 지운다 | | 플랜 침범 | 태스크·수용 기준을 이 스킬이 직접 고쳐 쓴 곳이 0건 ("앞 문서 반영"으로 `plan-generator` 10단계를 불러 — 부를 수 없으면 그 규칙대로 — 고친 것은 침범이 아니다) | 되돌리고 Layer 3으로 보고한다 | | 루프 침범 | 실패 재시도 횟수·태스크 선택 규칙·커밋 규칙을 **정한** 곳이 0건 (Layer 5가 정한 것을 근거로 참조하는 것은 괜찮다) | 지운다. Layer 5의 몫이다 | | `CLAUDE.md` · `AGENTS.md` | 두 파일이 같고 `## 하네스` 절이 갱신됐고 각 200줄 이하 | 자기 절만 고친다. 넘치면 규칙을 문서로 돌려보내고 가리키는 줄만 남긴다 | **자체 점검 결과를 하네스 문서 안에 남기지 않는다.** 점검은 작업 기록이고 문서는 산출물이다. 문서에 남기면 게이트가 바뀌어도 점검표가 남아, 통과한 적 없는 상태를 통과했다고 말하게 된다. **결과는 10단계 보고로 간다.** **고칠 것과 보고할 것이 다르다.** 형식 문제(빠진 명령, 잘못된 파일 이름, 없는 활성 시점)는 고친다. **범위 판단은 보고한다** — 도구를 바꾸는 것, 게이트를 늘리는 것, 덮기 어려운 요구사항을 목록에서 빼는 것은 사용자가 요청한 검증을 사용자 모르게 줄이는 일이다. ### 앞 문서 반영 — 보고하고 멈추지 않는다 하네스를 세우다 보면 앞 문서를 바꿔야 풀리는 것이 나온다 — TRD 에 없는 도구·구성 요소(Redis, 브라우저 자동화), 관찰할 수 없게 쓰인 태스크 수용 기준, 없는 태스크를 가리키는 참조, 하네스로 잴 수 없는 NFR 수치. 보고만 하고 넘어가면 루프가 그 자리에서 3회 실패한 뒤에야 같은 문제가 다시 올라온다. 1. **결정이면 사용자에게 묻는다**(선택지 셋 이하, 여러 건이면 묶어서). 이미 합의된 것이 문서에 안 적힌 것이면 묻지 않는다. 답을 받기 전에는 반영하지 않는다 — 대화할 수 없는 세션(무인·평가)이면 결정이든 기록이든 다른 레이어 문서는 고치지 않고(그 스킬도 부르지 않고) 지금처럼 보고·CF·미결로 남긴다. 2. **가장 앞 문서부터 고친다** — 요구사항·NFR 수치는 `prd-generator` 8단계, 도구·구성 요소는 `trd-generator` 9단계, 구조는 `architecture-generator` 9단계, 수용 기준·태스크 참조는 `plan-generator` 10단계. 부를 수 없으면 그 문서의 부분 갱신 규칙대로 직접 고친다(바뀐 항목만, 번호 유지, 새 것은 끝번호, 추적표, 버전과 `변경 이력` 한 줄) 3. **사이 문서를 맞춘다** — 고친 문서와 플랜 사이에서 바뀐 ID 를 찾아 그 자리만. TRD 에 TD 를 더했으면 아키텍처에 그것을 쓰는 설계 단위가 있는지, 플랜에 그것을 준비하는 태스크가 필요한지 4. 하네스 작성으로 돌아가고, 10단계 보고 6번에 적는다. `HARNESS.md` 머리의 `입력 문서` 버전을 실제와 맞춘다 ## 10단계. 작성 후 보고 이 순서로 보고한다. 1. **네 게이트가 각각 무엇으로 도는가** — 명령과 판정 기준을 한 줄씩. **활성 시점(어느 태스크가 끝나야 실제로 도는가)**을 함께 2. **task_validation 의 분리 방식** — 검증자가 무엇을 받고 무엇을 받지 않는지, 이 환경에서 세션을 어떻게 분리하는지. **분리가 보장되지 않는 환경이면 그 사실을 여기서 명시한다.** 분리된 척하는 검증은 안 하느니만 못하다 3. **E2E 흐름 목록** — 몇 개이고 어느 phase 에서 활성화되는지. 차단 때문에 `대기` 인 흐름 4. **`local-dev`** — 어느 OS 용이고 무엇을 준비하며 어떻게 돌리는지 5. **PLAN.md 7절 항목의 배치 결과** — 항목마다 어느 게이트가 받았는지. **하네스로도 못 덮는 것**과 그 이유 6. **앞 문서 반영과 남은 어긋남** — 이번에 고친 TRD·플랜(태스크 수용 기준)의 새 버전과 바뀐 ID, 누가 무엇을 골랐는가. 사용자가 보류해 남은 것. 도구가 비어 있어 하네스가 정한 것 7. **하네스에 더 필요해 보이는 것** — 아래를 보라. **만들지 않고 제안만 한다** 8. 다음 단계 안내: "검토하시고, 괜찮으면 개발 루프 세팅으로 넘어갈까요? (`loop-setup`)" ### 더 필요해 보이는 것은 제안만 한다 원본 사양은 하네스에 무엇이 더 필요한지를 열어 두었다. 네 게이트와 로컬 개발 환경 밖에서 이 프로젝트에 필요해 보이는 것이 생기면 **만들지 말고 목록으로 제안한다.** 흔히 후보가 되는 것은 성능 목표(응답 시간·생성 시간)를 **회귀로** 지키는 측정 묶음, 브라우저 여러 종을 도는 실행 구성, 접근성 검사 같은 것들이다. **검증 증거를 파일로 쌓는 것은 제안하지 않는다.** 증거는 `LOG.md` 에 텍스트로 남기기로 이미 정해져 있고, 기록처를 늘리면 STATE.md·LOG.md 와 어긋나는 세 번째 출처가 생긴다. **게이트를 순서대로 돌리는 실행, 실패 시 재시도, 하네스 갱신 시점 판단은 제안 목록에 넣지 않는다.** 원본 사양이 그것을 Layer 5 루프의 일로 이미 정해 두었다. **근거 요구사항이 없는 검사에 통과선을 만들지 않는다.** 접근성이 대표적이다 — PRD에 접근성 요구사항이 없는데 하네스가 기준을 정하면, 사용자가 합의한 적 없는 관문이 선다. 그때는 "PRD에 요구사항을 넣는 것이 먼저"라고 보고한다. **임의로 넣지 않는 이유가 있다.** 하네스는 모든 태스크가 매번 통과해야 하는 관문이라, 여기 들어간 것 하나가 앞으로의 모든 세션에 비용으로 붙는다. 무엇을 관문에 세울지는 사용자의 결정이다. --- ## 산출물 템플릿 ### `harness_setup/HARNESS.md` ````markdown # 검증 하네스: (제품 이름) - **입력 문서**: docs/PRD.md (0.1), docs/TRD.md (0.1), docs/ARCHITECTURE/ (0.1), plan_setup/PLAN.md (0.1) - **상태**: 사람 검토 대기 ## 1. 게이트 개요 태스크 하나는 아래 네 게이트를 **순서대로** 통과해야 완료된다. 앞이 실패하면 뒤를 돌리지 않는다. | 순서 | 게이트 | 정의서 | 명령 | 활성 시점 | 현재 | |---|---|---|---|---|---| | 1 | lint | `quality_gates/lint.md` | | phase-01-task-01 | 미활성 | | 2 | unit | `quality_gates/unit.md` | | | 미활성 | | 3 | e2e | `quality_gates/e2e.md` | | | 미활성 | | 4 | task_validation | `quality_gates/task_validation.md` | (분리 세션) | | 미활성 | > `미활성` 은 통과가 아니라 "아직 판정하지 않음"이다. > **task_validation 의 관찰 수단**: (이 환경의 브라우저 조작 도구. 없으면 "없음 — e2e 도구로 일회성 스크립트" 라고 적는다) > `status: blocked` 인 태스크는 어느 게이트의 대상도 아니다. ## 2. 로컬 개발 환경 - **명령**: `scripts/local-dev.ps1` (또는 `.sh`) — 초기화 `-Reset`, 상태 `-Status`, 로그 `-Logs`, 내리기 `-Down` - **전제**: (필요한 런타임·컨테이너 엔진) - **종료 코드**: 0 성공 / (n) 전제 / (n) 환경변수 / (n) 의존 서비스 / (n) 스키마·시드 / (n) 앱. 실패하면 실패한 단계 이름과 다음 행동을 출력한다. 전제·환경변수는 사용자가 고치고, 나머지는 재준비 후 `harness-update` - **단계별 판정 시작 시점**: 스키마·시드는 `phase-NN-task-MM` 완료부터, 앱 실행은 `phase-NN-task-MM` 완료부터. 그전에는 "건너뜀"으로 0 - **공유 DB 를 쓰는 게이트**: (예: unit 통합 테스트 — 스키마를 바꾸는 테스트 있음 / e2e / task_validation). 병렬 격리를 정할 때 `loop-setup` 이 이 줄을 읽는다 - **슬롯 변수**: (`local-dev` 가 읽는 포트·DB 이름 환경변수 이름. 격리 1·2면 슬롯 번호에서 파생하는 분기 — 없으면 "없음(격리 3 전제)") > 루프는 이 표의 숫자로 환경 문제를 확정한다. 단계가 늘면 코드도 여기와 스크립트에 함께 더한다. | 구성 요소 | 형태 | 준비 판정 | 왜 필요한가 | |---|---|---|---| | | 컨테이너 / 호스트 실행 / 로컬 설치 | | | > 이 표가 단일 출처다. 구성 요소가 늘면 여기에 행을 더하고 스크립트에 블록 하나를 더한다. ## 3. 하네스 밖 (요구사항 중 이 하네스로 판정할 수 없는 것과, 대신 무엇으로 확인하는가.) | 요구사항 | 왜 게이트로 못 덮는가 | 무엇으로 확인하는가 | |---|---|---| ## 4. 하네스 결정 (TRD가 비워 둔 자리를 하네스가 채운 것. 근거 없이 세운 통과선도 여기 적는다.) | 정한 것 | 왜 하네스가 정했는가 | 되돌리기 비용 | |---|---|---| ```` ### `harness_setup/quality_gates/lint.md` (unit·e2e 도 같은 뼈대) ````markdown # 게이트 1 — lint ## 무엇을 판정하는가 (한 문단. 무엇을 보고 무엇을 보지 않는가.) ## 명령 ``` (붙여 넣어 그대로 돌릴 수 있는 명령 한 줄) ``` ## 통과 조건 - 종료 코드 0 - (그 외 판정 기준) ## 활성 시점 - `phase-NN-task-MM` 완료 시부터. 그전에는 `미활성` ## 무엇을 검사하는가 | 검사 | 근거 | 설정 파일 | |---|---|---| | | `backend.md §3.1` / TD-0xx | `프로젝트 루트/…` | ## 전제 (이 게이트가 돌기 전에 필요한 것. 없으면 '없음') ## 하지 않는 것 - 자동 수정 — 게이트는 판정만 한다 ```` ### `harness_setup/quality_gates/e2e.md` 의 흐름 목록 ````markdown ## 덮는 흐름 | ID | 흐름 | 단계 요약 | 덮는 요구사항 | 실패 경로 | 활성 phase | 상태 | |---|---|---|---|---|---|---| | FLOW-01 | | | FR-00x, NFR-00x | | 02 | 대기 | > **매 태스크 묶음**: (빠른 흐름 ID들) > **phase 완료 묶음**: (시간이 걸리는 측정) > 차단된 태스크에 걸린 흐름은 `대기` 이고 실행 대상이 아니다. ## phase 완료 묶음 명령 > 게이트 명령이 아니다 — 루프의 구동 스크립트가 phase 끝에 **이 절의 코드 블록을 그때 읽어** 한 줄씩 차례로 돌리고 줄마다 종료 코드를 남긴다(판단 없이). > 한 줄 = 붙여 넣어 그대로 돌릴 수 있는 명령 하나. 느린 lint·unit 묶음이 있으면 그것도 한 줄씩. **활성인 묶음 흐름만 돈다** — `대기` 흐름은 명령·설정에서 빠져 있어야 한다(구동 스크립트는 거르지 않는다). > 활성 시점: `phase-NN-task-MM` 완료 시부터. 그전에는 코드 블록을 비워 둔다 — 비어 있으면 루프가 `미활성` 으로 읽는다. ``` (명령 한 줄씩. 아직 없으면 빈 블록) ``` ## 흔들림을 줄이는 규칙 > 스펙을 쓰기 전에 읽는다. 흔들림을 안정화할 때마다 `harness-update` 가 원인 패턴을 한 줄 더한다(①). | 규칙 | 출처 | |---|---| | 선택자는 역할과 이름으로. 표식은 그것으로 안 되는 곳에만 | 세팅 | | 실행마다 고유한 테스트 데이터 | 세팅 | | 인증 상태는 한 번 만들어 재사용 (인증 흐름 자체를 보는 흐름만 진짜로 로그인) | 세팅 | | 외부에서 오는 값(메일 링크 등)은 실제 경로에서 읽는다. 하드코딩하지 않는다 | 세팅 | | 값은 조건이 될 때까지 기다리는 단언으로 본다. 한 번 읽어 비교하지 않는다 | 세팅 | | 크기·위치·색 측정은 전환·로드가 끝난 뒤에 | 세팅 | | 목록의 순서·첫 페이지를 가정하지 않는다. 검색·필터로 좁혀 찾는다 | 세팅 | ## 단언 세 층 1. 화면 상태 / 2. 서버 상태(DB·토큰·이력) / 3. 다시 조회하거나 다른 계정에서 볼 때의 결과 ## 실패 분류 (고치기 전에 정한다) | 분류 | 알아보는 법 | 고칠 곳 | |---|---|---| | 진짜 결함 | 수동으로 같은 순서를 밟아도 같은 결과 | 코드 | | 낡은 스펙 | 화면·규약이 의도적으로 바뀌었다 | 흐름 정의 (`harness-update`) | | 환경 문제 | 의존 서비스 미기동, 남은 데이터 | `local-dev` 재준비 | > 분류를 `LOG.md` 에 남긴다. 세 번의 실패가 전부 환경 문제였다면 고칠 것은 코드가 아니라 하네스다. ```` ### `harness_setup/quality_gates/task_validation.md` ````markdown # 게이트 4 — task_validation ## 무엇을 판정하는가 그 태스크의 수용 기준이 **실행 중인 애플리케이션에서 실제로 관찰되는가**. ## 분리 원칙 이 검증은 **그 태스크를 구현한 세션이 아닌 세션**에서 돈다. 구현 중에 형성된 판단이 관찰을 덮어쓰는 것을 막기 위해서다. 그 세션을 **어떻게 여는지(헤드리스 자식 세션·오르카 워커·서브에이전트)는 `loop_setup/LOOP.md` 의 구동기가 정한다** — 기본은 컨텍스트 없는 새 프로세스(`claude -p` / `codex exec`)다. 이 문서는 그 세션에 무엇을 주고 무엇을 주지 않는지만 고정한다. ### 검증자가 받는 것 1. 태스크 문서 경로 — `plan_setup/phase-NN/phase-NN-tasks/phase-NN-task-MM.md` 2. 실행 중인 애플리케이션 (`scripts/local-dev…` 로 준비) 3. 이 문서 4. (필요 시) 태스크의 `architecture` 가 가리키는 설계 절 ### 검증자가 받지 않는 것 - 구현 세션의 대화·요약·"무엇을 했는지" 설명 - 구현자가 쓴 통과 보고나 자체 점검 결과 - 원인 추정이나 확인 요청 ## 절차 1. `local-dev` 로 환경을 준비한다 (필요하면 초기화 옵션 `-Reset`/`--reset`). 뭔가 이상하면 상태 옵션 `-Status`/`--status` 로 무엇이 안 떴는지 먼저 본다 — **환경 문제를 구현 실패로 보고하지 않기 위해서다** (옵션 표기는 이 프로젝트의 스크립트 것을 쓴다) 2. 태스크 문서의 수용 기준을 번호대로 읽는다 3. 기준마다 애플리케이션을 실제로 조작해 결과를 관찰한다 — 아래 '관찰 수단' 순서대로 4. 수용 기준이 코드를 대상으로 할 때만 코드를 검사한다 5. 아래 형식으로 판정을 출력한다 ## 관찰 수단 1. (이 환경의 브라우저 조작 도구 — `HARNESS.md` 에 적힌 것) — 화면이 있는 기준의 기본 2. 없으면 e2e 도구로 일회성 스크립트를 써서 실행. 저장소에 남기지 않고 제품 코드를 건드리지 않는다. **판정 블록에 `관찰 수단: 2` 를 적는다** 3. 화면이 없는 기준은 HTTP 호출·DB 질의로 관찰한다 1도 2도 없으면 화면 기준은 `판정 불가` 다. 코드를 읽고 PASS 를 주지 않는다. ## 판정 형식 ``` TASK: phase-NN-task-MM GATE: task_validation 관찰 수단: 1 | 2 | 3 RESULT: PASS | FAIL | 판정 불가 | 환경 문제 - [1] PASS — (무엇을 했고 무엇이 보였는가) - [2] FAIL — (관찰한 것. 원인 추정 없이) END ``` `환경 문제` 는 `local-dev` 상태 확인이 비0이거나 앱이 응답하지 않아 **관찰 자체를 못 한 경우**다. FAIL 이 아니다 — 안 뜬 화면을 FAIL 로 적으면 멀쩡한 코드를 고치게 된다. 마지막 줄 `END` 는 루프가 **잘린 판정 파일**을 알아보는 표식이다. 검증 세션이 도중에 죽으면 이 줄이 없다. ## 판정 규칙 - 증거 없는 PASS 는 PASS 가 아니다 - 검증자는 코드를 고치지 않는다 — 고치면 다음 기준부터 자기 코드를 검증하게 된다 - 원인 분석과 수정 제안을 하지 않는다 — 틀린 추정은 엉뚱한 수정을 부른다 - 기준이 관찰 가능하게 쓰이지 않았으면 `판정 불가` 로 돌려보낸다 (Layer 3의 일이다) ## 결과 기록 루프가 이 블록을 두 곳으로 옮긴다. - `plan_setup/STATE.md` 의 `Last Verification` — 지금 상태 한 줄 - `plan_setup/LOG.md` — **관찰한 증거.** 통과는 한 줄, 실패는 기준 번호와 관찰을 함께 관찰은 재현 가능한 문장으로 쓴다 — "정상 동작함"이 아니라 "계정 B로 A의 인보이스 URL을 열었더니 200과 본문이 보였다". **하네스는 별도의 상태 파일이나 증거 디렉터리를 만들지 않는다.** 증거는 LOG.md 에 텍스트로 남는다. ```` ### `harness_setup/scripts/local-dev.*` ````markdown 스크립트는 아래 단계를 이 순서로 갖는다. 각 의존 서비스는 **같은 모양의 블록 하나**다. 1. 전제 확인 — 필요한 런타임·엔진이 없으면 설치 방법을 출력하고 종료(비0) 2. 환경변수 준비 — 예시 파일에서 만들고, 빠진 필수 키를 이름으로 나열 3. 의존 서비스 — 각 구성 요소마다: 이미 준비됐는가 → 없으면 준비 → 준비될 때까지 대기 → 실패 시 안내 4. 스키마·시드 — 마이그레이션 적용, 시드 투입 (대량 시드는 옵션). 마이그레이션이 아직 없으면 "건너뜀" 출력 후 계속 5. 앱 실행 — 개발 서버를 **분리된 프로세스**로 실행(스크립트가 끝나도 살아 있다). 응답 한 번으로 준비 판정. 앱 진입점이 아직 없으면 "건너뜀" 출력 후 0으로 종료 (실패가 아니다) 6. 요약 출력 — 접속 주소, 예시 계정, 다음에 할 것 옵션: `-Reset` / `--reset` (데이터 초기화 후 처음부터), 대량 시드 옵션, `-Status` / `--status` (구성 요소별 준비 여부만 출력, 아무것도 띄우지 않는다), `-Logs` / `--logs` (안 뜬 구성 요소의 로그), `-Down` / `--down` (띄운 것을 내린다). 종료 코드: 성공 0, 실패는 **단계마다 다른** 0이 아닌 값(전제 / 환경변수 / 의존 서비스 / 스키마·시드 / 앱) + 실패한 단계 이름. 그 표를 `HARNESS.md` 2절에 그대로 적는다 — 루프가 읽는 곳이 거기다. `-Status` 는 분리된 검증 세션이 **실패를 '환경 문제'로 분류할 수 있게 하는 수단**이다. 그것이 없으면 환경 문제가 구현 실패로 보고되고, 루프가 멀쩡한 코드를 재시도 한도까지 고친다. ```` --- ## 에이전트별 참고 - Claude Code에서 실행 중이면: `references/claude-code.md` - Codex에서 실행 중이면: `references/codex.md` **둘 다 반드시 확인할 것은 하나다** — 이 환경에서 `task_validation` 을 **어떻게 분리된 세션으로 돌리는가**. 분리 수단이 없는 환경이면 그 사실을 사용자에게 알린다. 분리된 척하는 검증은 안 하느니만 못하다 — 통과했다는 기록만 남고 실제로는 구현자가 자기 코드를 승인한 것이기 때문이다.