plan_setup/(PLAN.md·phase·task·STATE.md·LOG.md) 과 harness_setup/(HARNESS.md·quality_gates/) 을 읽고 이 프로젝트의 개발 루프를 loop_setup/LOOP.md 한 문서로 고정한다. 다음 태스크를 어떻게 고르고, 태스크의 verification 값으로 어느 게이트를 어떤 순서로 돌리며, 실패를 진짜 결함·낡은 스펙·환경 문제로 어떻게 분류하고, 재수정 사이클을 최대 3회로 세다가 언제 멈춰 사용자에게 보고하며, task_validation 을 구현 세션과 어떻게 분리하고, STATE.md·LOG.md 를 언제 갱신하고 언제 커밋하며, 차단이 풀린 태스크를 어떻게 여는지, 태스크마다 새 세션을 누가 어떻게 여는지(스크립트 구동·오르카·서브에이전트), 병렬 운용(기본)을 위해 태스크 단위 DAG 를 머메이드로 그리고 자원 충돌을 사전 검사하며 코디네이터·슬롯 워크트리를 어떻게 운용하는지, 오르카 구동이 진행할...
Installs into .claude/skills of the current project.
Are you the author of Loop Setup?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/gangjaeyu-loop-setup)
---
name: loop-setup
description: plan_setup/(PLAN.md·phase·task·STATE.md·LOG.md) 과 harness_setup/(HARNESS.md·quality_gates/) 을 읽고 이 프로젝트의 개발 루프를 loop_setup/LOOP.md 한 문서로 고정한다. 다음 태스크를 어떻게 고르고, 태스크의 verification 값으로 어느 게이트를 어떤 순서로 돌리며, 실패를 진짜 결함·낡은 스펙·환경 문제로 어떻게 분류하고, 재수정 사이클을 최대 3회로 세다가 언제 멈춰 사용자에게 보고하며, task_validation 을 구현 세션과 어떻게 분리하고, STATE.md·LOG.md 를 언제 갱신하고 언제 커밋하며, 차단이 풀린 태스크를 어떻게 여는지, 태스크마다 새 세션을 누가 어떻게 여는지(스크립트 구동·오르카·서브에이전트), 병렬 운용(기본)을 위해 태스크 단위 DAG 를 머메이드로 그리고 자원 충돌을 사전 검사하며 코디네이터·슬롯 워크트리를 어떻게 운용하는지, 오르카 구동이 진행할 수 있는데 멈춘 상태(잠든 워커·죽은 스크립트·사람 프롬프트)를 어떻게 감시하는지를 정한다. 사용자가 "개발 루프 세팅해줘", "이제 구현 시작하자", "루프 만들어줘", "LOOP.md 만들어줘", "태스크 어떤 순서로 진행할지 정해줘", "실패하면 몇 번까지 재시도할지 정해줘"라고 하거나, 하네스 검토를 마치고 다음 단계로 넘어갈 때 반드시 이 스킬을 사용한다. 멀티 에이전트·병렬 운용을 실제로 켜는 요청은 LOOP.md 가 이미 있으면 loop-update 의 몫이다. 플랜과 하네스는 있는데 "그래서 지금 무엇부터 하면 되는가"가 문서로 없는 상태에서 첫 태스크 구현을 시작하려 할 때도 이 스킬로 먼저 루프를 고정한다. loop_setup/LOOP.md 가 이미 있으면 이 스킬이 아니라 loop-update 를 쓴다.
---
# 개발 루프 세팅 (Layer 5)
## 이 스킬의 목적
하네스가 **"태스크 하나가 끝났다고 말해도 되는 조건"**을 정했다면,
루프는 **누가 언제 그것을 확인하고 무엇을 기록하는가**를 정한다.
원본 사양은 `LOOP.md` 를 **개발 프로세스의 헌법**이라고 불렀다. 그 표현이 정확하다.
이 문서의 독자는 사람이 아니라 **다음 세션의 에이전트**이고, 세션은 계속 끊긴다.
끊긴 자리에서 다시 읽히는 것이 `STATE.md` 와 이 문서 둘뿐이다.
```
구현 세션이 열린다
→ LOOP.md 를 읽는다 (무엇을 어떤 순서로 하는가)
→ STATE.md 를 읽는다 (지금 어디인가)
→ 태스크 하나를 구현한다
→ lint·unit·e2e 를 순서대로 돌린다 (하네스가 정의한 명령)
→ "task_validation 대기" 를 적고 닫는다
검증 세션이 열린다 → task_validation 만 돌려 판정 블록을 내고 닫는다 ← 루프가 실제로 만들어 내는 분리
반영 세션이 열린다 → 판정을 STATE.md·LOG.md 에 옮기고, 완료·커밋 또는 수정·재실행을 하고 닫는다
```
루프의 품질은 "순서가 촘촘한가"가 아니라 셋으로 판정된다.
1. **세션이 임의의 지점에서 죽어도 다음 세션이 같은 자리에서 이어지는가** — 원본이 STATE.md·LOG.md 를 둔 이유가 이것이다
2. **통과하지 않은 것이 통과로 기록되지 않는가** — 미활성 게이트, 분리되지 않은 검증, 소진된 재시도
3. **막혔을 때 사람에게 오는가** — 스스로 기준을 낮추는 쪽으로 풀지 않는다
### 여기서 하는 일과 하지 않는 일
| 하지 않는 것 | 어디서 하는가 |
|---|---|
| 요구사항·기술 결정·설계를 읽고 해석하는 것 | 이미 태스크 문서로 내려왔다. 루프는 PRD·TRD·아키텍처를 읽지 않는다 |
| 태스크·phase 를 나누거나 수용 기준·`verification` 을 고치는 것 | Layer 3 (`plan-generator` 10단계 부분 갱신) — 루프는 `Next Action` 에 무엇을 고쳐야 하는지 적고 멈춘다 |
| 게이트가 무엇을 검사하고 어떤 명령으로 도는지 **정하는 것** | Layer 4 (`harness-setup`). 루프는 그 명령을 **꺼내 돌릴 뿐**이다 |
| 게이트를 고치는 것 (흐름 정의, 활성 시점, `local-dev`) | `harness-update` — 루프는 **부르는 조건**만 안다 |
| 태스크 코드를 실제로 구현하는 것 | 루프를 **도는** 세션. 이 스킬은 루프를 **세운다** |
| 루프를 고쳐 나가는 것 | `loop-update` — 규칙은 phase 가 진행되며 바뀐다 |
**루프가 게이트 내용을 다시 정하지 않는 이유가 있다.** 게이트를 돌리는 쪽과 게이트를 고치는 쪽이 같으면,
실패를 없애는 가장 빠른 길이 언제나 기준을 낮추는 것이 된다. 루프는 **돌리고 분류하고 보고하는 쪽**이고,
고치는 것은 `harness-update` 로 넘긴다. 이 분업이 무너지면 하네스는 조용히 죽는다.
### 이 스킬은 루프를 **세우지** 돌리지 않는다
가장 흔한 오해가 이것이다. 이 스킬을 실행한 결과물은 **`loop_setup/LOOP.md` 문서 하나**이고,
태스크 코드는 한 줄도 만들어지지 않는다. 사용자가 "이제 구현 시작하자"고 말해 이 스킬이 트리거됐더라도,
여기서 하는 일은 **첫 태스크를 구현하는 것이 아니라 앞으로 모든 태스크가 따를 순서를 고정하는 것**이다.
문서를 다 쓴 뒤에 "이 순서로 phase-01-task-01 부터 시작할까요?"라고 묻는다.
그 대답을 받고 시작하는 것이 첫 사이클이고, 그때는 이미 이 스킬이 아니라 `LOOP.md` 가 안내한다.
---
## 1단계. 입력 확보와 사전 점검
**두 곳만 읽는다.** 루프는 요구사항을 해석하지 않는다 — 그건 이미 태스크 문서의 수용 기준으로 내려왔고,
루프가 PRD를 다시 읽으면 태스크에 적히지 않은 요구를 구현하러 가게 된다.
| 입력 | 여기서 무엇을 꺼내는가 |
|---|---|
| `plan_setup/PLAN.md` | phase 구성과 순서, 차단 목록 |
| `plan_setup/phase-*/phase-*.md` | phase 수용 기준 → **phase 완료 판정의 근거** |
| `plan_setup/phase-*/phase-*-tasks/*.md` | frontmatter 전부 — `id`·`depends_on`·`files`·`status`·`verification`·`blocked_by`. **DAG 의 재료다** |
| `plan_setup/STATE.md` | 지금 어느 phase·태스크인가. **어떤 절이 이미 있는가** (절을 더할 자리를 정하려면 필요하다) |
| `plan_setup/LOG.md` | 기록 형식. 이미 진행된 이력이 있으면 그것 |
| `harness_setup/HARNESS.md` | 게이트별 **명령·활성 시점·현재 상태**, `local-dev` 명령과 옵션, '하네스 밖' |
| `harness_setup/quality_gates/*.md` | 게이트 정의서의 **파일 이름**과 `## 명령` 절의 위치. 실패 분류 기준 |
| `harness_setup/scripts/` | `local-dev` 의 실제 옵션 이름 (`-Status`·`-Reset`·`-Logs`·`-Down`) |
### 하네스가 없으면 루프를 세우지 않는다
`harness_setup/quality_gates/` 가 없으면 **멈추고 `harness-setup` 을 먼저 하자고 말한다.**
루프의 절반은 "게이트를 순서대로 돌린다"인데, 돌릴 명령이 없으면 그 절반이 빈 문장이 된다.
빈 문장으로 세운 루프는 첫 태스크에서 각 세션이 자기 나름의 검증을 만들어 내고, 그러면 무엇이 통과한 것인지 알 수 없다.
`plan_setup/` 이 없으면 마찬가지로 `plan-generator` 가 먼저다.
### 실제 값을 꺼내 넣는다
`LOOP.md` 는 일반론이 아니라 **이 프로젝트의 문서**다. 아래는 하네스와 플랜에서 실제 값을 꺼내 채운다.
- `local-dev` 의 **실제 경로와 옵션 이름** — `.ps1` 인지 `.sh` 인지, `-Status` 인지 `--status` 인지
- **첫 태스크 ID**와 phase 목록
- **차단 태스크의 개수와 ID 범위** — 루프가 매번 걸러야 할 대상이다
- 게이트별 **활성 시점 태스크 ID**
경로와 옵션을 일반형으로 남겨 두면 첫 세션이 그것을 추측하고, 추측이 틀리면 환경 준비가 실패한 채로 게이트가 돈다.
---
## 2단계. 한 사이클의 뼈대
원본 사양은 17단계 목록을 예시로 보여 준다. **그 목록을 그대로 옮기는 것이 목적이 아니다** —
앞 레이어가 확정한 것과 맞추면 순서가 달라지고, 억지로 예시를 지키면 아무도 못 지키는 항목이 남는다.
아래 넷을 반영해 다시 쓴다.
**(1) 게이트 이름을 사이클에 하드코딩하지 않는다.**
원본 6~9단계는 `lint`·`unit`·`E2E`·`task validation` 을 네 줄로 적었지만,
Layer 4가 **게이트 문서 이름 = 태스크의 `verification` 값**으로 확정했다.
사이클은 "`verification` 을 읽어 그 게이트들을 정해진 순서로 돈다" 한 줄이고, 네 이름은 순서 규칙에만 남는다.
게이트가 줄어든 태스크가 있어도 사이클을 고칠 필요가 없어진다.
**(2) `task_validation` 앞에 세션 경계를 그린다.**
원본 목록에서 9단계는 앞의 셋과 같은 줄에 있지만, 실제로는 **세션이 바뀌는 지점**이다 — 그 앞에서 한 번, 그 뒤에서 한 번(7단계의 세 토막).
줄 하나로 두면 같은 세션이 이어서 판정하게 되고, 그 순간 이 게이트는 이름만 남는다.
**여기가 Layer 5의 핵심 구현 지점**이므로 사이클에서 눈에 보이게 끊는다.
**(3) 실패와 완료 앞에 판정을 하나씩 끼운다.**
- 원본 10단계 "실패하면 원인 분석 및 수정" **앞에 분류**를 넣는다 — 분류 없이 고치면 거의 항상 기준을 낮추는 쪽으로 끝난다
- 원본 12단계 "성공하면 Task completed" **앞에 미활성 확인**을 넣는다 — 미활성은 통과가 아니다
**(4) 기록을 완료 시점에만 하지 않는다.**
원본은 13·14단계(STATE 갱신·LOG 기록)를 완료 뒤에 두었다. 그런데 세션은 **구현 중에 가장 많이 끊긴다.**
시작할 때 `in_progress` 를 남기지 않으면 다음 세션이 그 태스크를 `pending` 으로 보고 처음부터 다시 한다.
원본이 STATE.md·LOG.md 를 둔 목적("세션이 갑작스럽게 종료되더라도 이어서")에 맞추면 기록은 **시작·전이·완료 세 번**이다.
여기에 원본 본문이 목록 밖에서 말한 것 하나를 되살린다 — **"성공하면 커밋 → task done"**.
17단계 목록에는 커밋이 없지만 본문과 Agent/Worktree 절에는 있다. **본문 쪽이 맞다.**
---
## 3단계. 태스크 선택 규칙
원본의 다섯 규칙을 그대로 살리되, 판정 근거를 **frontmatter 필드**로 못 박는다.
"blocked 를 실행하지 않는다"가 규칙으로만 있으면 매 세션이 blocked 를 다르게 판정한다.
| 규칙 | 무엇을 읽어 판정하는가 |
|---|---|
| `blocked` 는 실행하지 않는다 | `status: blocked` 와 `blocked_by` 의 OQ·CF ID. **차단은 사용자가 미결을 풀어야 열린다** |
| dependency 가 완료되지 않은 태스크는 실행하지 않는다 | `depends_on` 의 모든 ID가 `completed` 인가 |
| 한 번에 하나만 완료 상태로 전환한다 | 진행 중인 태스크가 이미 있으면 그것을 끝낸다. `failed` 인 태스크는 `Next Action` 에 사람의 `답:` 이 있을 때만 이어받는다 |
| 태스크 범위를 임의로 확장하지 않는다 | `files` 와 `architecture` 밖을 건드리지 않는다 |
| 요구사항이 모호하면 추측하지 않고 멈춘다 | 수용 기준이 관찰 가능하지 않으면 Layer 3으로 |
**범위 확장 금지가 실제로 판정되게 하는 것이 `files` 와 `architecture` 다.** Layer 3이 이 두 필드를
기계로 읽으라고 만들어 두었다. "관련된 김에 여기도 고쳤다"는 그 자체로 나쁘지 않지만,
그 파일은 다른 태스크의 `files` 이고 그 태스크의 검증을 받지 않은 채 커밋에 섞여 들어간다.
경계 밖을 고쳐야 한다고 판단되면 **고치지 말고 보고한다** — 태스크를 나누는 것은 Layer 3의 일이다.
**막다른 곳도 규칙으로 다룬다.** 실행 가능한 태스크가 0개인데 미완료 태스크가 남아 있으면
그건 차단이 풀리기를 기다리는 상태이거나 `depends_on` 에 순환이 있는 것이다.
전자는 사용자에게 **어느 OQ·CF 를 풀면 무엇이 열리는지**를 보고하고, 후자는 Layer 3으로 돌려보낸다.
`blocked_by: [폐기]` 인 태스크는 차단이 아니라 Layer 3이 치운 것이므로 이 보고에 넣지 않는다.
이걸 안 적어 두면 세션이 "할 게 없다"에서 멈춰 아무 말 없이 끝난다.
### 차단 해소 절차 — `blocked` 를 `pending` 으로 바꾸는 주인은 루프다
`blocked` 는 사용자가 미결(OQ)이나 충돌(CF)을 풀어야 열리는데, **푼 뒤에 태스크 문서를 누가 고치는가**가 정해져 있지 않으면
`status: blocked` 와 `blocked_by` 가 그대로 남아 루프가 그 태스크를 영원히 건너뛴다.
Layer 3은 초기값만 만들고, `harness-update` 는 태스크 문서를 고치지 않는다. 그래서 **사이클을 도는 세션이 한다.**
사용자가 "OQ-007 풀렸다"고 알리면 그 세션은 다음 순서로 처리한다.
1. **앞 문서에 해소가 반영됐는지 확인한다** — PRD 8절·TRD 12절의 미결 표에서 그 OQ가 사라졌거나 결정으로 옮겨졌는지,
CF면 overview.md 7절에서 `지금 취한 조치` 가 해소로 바뀌었는지. **반영되지 않았으면 태스크를 열지 않는다** —
대화에서만 풀린 미결 위에 코드를 얹으면 다음 세션은 여전히 막힌 문서를 읽는다.
그때는 `Next Action` 에 `질문: OQ-007 해소가 PRD 8절에 반영되지 않았다. 문서를 고친 뒤 다시 알려 달라` 를 적고 종료한다 —
받은 `답:` 을 지우고 아무 말 없이 끝나면 사람은 왜 안 먹혔는지 모른다.
(루프가 PRD·TRD·아키텍처를 읽지 않는다는 1단계 원칙의 **유일한 예외**다. 그 표 한 줄을 확인하는 것이지 요구사항을 해석하는 것이 아니다)
2. **그 ID를 `blocked_by` 에 가진 태스크 문서를 전부 찾는다** — `PLAN.md` 5절 차단 표가 색인이다
3. 각 태스크에서 `blocked_by` 의 그 ID를 지우고, **비어 있게 되면** `status` 를 `pending` 으로 바꾼다.
다른 ID가 남아 있으면 `blocked` 그대로다
4. **미결의 답이 수용 기준을 바꾸는 경우는 여기서 고치지 않는다** — 태스크 문서의 `acceptance_criteria` 를 다시 쓰는 것은
Layer 3의 일이라, 그 태스크만 `blocked` 로 두고 "수용 기준 재작성 필요"를 `Next Action` 에 적어 보고한다
5. `STATE.md` 의 `Task Status`·`Next Action` 을 고치고 `LOG.md` 에 한 줄 남긴다 —
`### <태스크 ID> Unblocked (OQ-007 해소).`
6. **`harness-update` 를 부른다** — 그 태스크에 걸려 `대기` 로 빼 두었던 흐름·커버 영역이 하네스 문서에 있다(10단계 "시점 무관").
무인 자식이면 ①(대기 → 활성 후보)만 하고, ②③이 필요하면 `질문:` 으로 남긴다(7단계의 규칙)
이 절차를 `LOOP.md` 에 적는다. 미결이 풀리는 것은 태스크 완료보다 드물어서, 적어 두지 않으면 매번 다르게 처리되거나 잊힌다.
**어느 토막의 일인가.** 구동기 ①에서는 사람이 `STATE.md` 의 `Next Action` 에 **`답: 해소 OQ-007`** 로 적어 두고(대화형 세션이면 말로),
다음 **구현 토막**이 0절 5번에서 그것을 읽어 이 절차를 먼저 끝낸 뒤 태스크 선택으로 간다. 병렬(①-병렬·②)에서는 코디네이터 자식이 한다.
구현 지시문에 "0절대로 시작하라"가 들어 있는 이유가 이것이다 — 안 그러면 자식은 `blocked` 를 풀지 않고 다른 태스크를 집는다.
---
## 4단계. 게이트 실행
### 이름으로 열고 명령을 꺼낸다
태스크의 `verification` 값 하나가 `quality_gates/` 아래 **같은 이름의 문서 하나**다.
`verification: [lint, unit, e2e, task_validation]` → `quality_gates/lint.md`·`unit.md`·`e2e.md`·`task_validation.md`.
**넷 중 셋만 명령으로 돈다.** 게이트 정의서를 열어 보면 형식이 두 가지다.
| 게이트 | 어디를 읽는가 | 어떻게 판정하는가 |
|---|---|---|
| `lint`·`unit`·`e2e` | `## 명령` 절의 명령 한 줄 | 돌려서 **종료 코드**로 |
| `task_validation` | `## 절차` 와 `## 판정 형식` | **세션을 열어** 판정 블록(`PASS`/`FAIL`/`판정 불가`)을 받아서 |
**`task_validation.md` 에는 `## 명령` 절이 없다.** 넷을 같은 모양으로 다루려다 없는 절을 찾게 되면
거기서 멈추거나, 더 나쁘게는 앞 셋의 통과를 넷째의 통과로 대신 읽는다.
넷째는 명령이 아니라 **세션을 여는 것**이고, 그것이 이 게이트의 전부다 (6단계).
**활성 시점도 마찬가지다.** 앞 셋은 정의서에 `## 활성 시점` 절이 있지만 `task_validation.md` 에는 없다.
**정의서에 없으면 `HARNESS.md` 의 게이트 개요 표에서 읽는다.** 두 곳이 다르면 정의서가 이긴다 — 표는 요약이다.
명령을 `LOOP.md` 에 베껴 적지 않는다 — 하네스가 갱신되면 베낀 쪽이 낡고,
낡은 명령은 "돌긴 도는데 다른 것을 검사하는" 상태를 만든다. **LOOP.md 는 명령이 아니라 명령을 찾는 방법을 적는다.**
예외는 `local-dev` 하나다. 그건 게이트가 아니라 환경 준비 수단이고, 경로와 옵션이 일반형이면 첫 세션이 추측한다.
### 순서는 고정이고 앞이 실패하면 뒤를 안 돌린다
`lint → unit → e2e → task_validation`. `verification` 배열의 순서가 아니라 이 순서다
(배열에 없는 게이트는 건너뛴다). 비용이 싼 순서이기도 하지만 더 중요한 이유가 있다 —
**타입이 안 맞는 코드로 돌린 e2e 실패는 그 흐름이 깨졌다는 뜻이 아니다.** 원인을 찾는 데 시간만 쓴다.
### 게이트를 돌리기 전에 환경을 준비한다
`e2e` 와 `task_validation` 은 실행 중인 애플리케이션을 전제한다. `local-dev` 를 먼저 돌리고,
그게 실패하면 그것은 **게이트 실패가 아니라 환경 문제**다 (5단계의 분류로 간다).
### 활성 시점 전에는 결과가 무엇이든 `미활성` 이다
게이트 정의서(없으면 `HARNESS.md` 표)의 활성 시점이 가리키는 태스크가 아직 `completed` 가 아니면,
**그 게이트의 결과는 돌리든 안 돌리든 `미활성` 이다.** 명령을 돌려도 되고 비용이 아까우면 건너뛰어도 되지만,
**나온 값을 통과로도 실패로도 읽지 않는다.**
이 규칙이 필요한 이유는 두 방향이다. 하네스의 게이트 정의서들이 각각 적어 둔 것을 보면 —
"`package.json` 이 없어 명령이 실패하는 것", "테스트 0건으로 명령이 실패하는 것",
"활성 흐름이 0개여서 `no tests found` 로 끝난 것" — **실패도 통과도 아닌 것이 양쪽으로 다 생긴다.**
0건 실행은 대부분의 러너에서 초록불이라 통과로 새고, 도구가 아직 없어서 나는 오류는 실패로 새어
멀쩡한 태스크를 재시도 한도까지 몰고 간다. 둘 다 막는 값이 `미활성` 이다.
**활성 시점에 도달한 뒤에는 반드시 돌린다.** 그때 "검사 대상 없음"으로 끝나면 그건 미활성이 아니라
활성 시점이 틀렸거나 게이트 명령이 실제 파일 배치와 어긋난 것이고, `harness-update` 로 간다.
### `부분 활성` 이 실제로 쓰이는 자리
상태 어휘는 하네스가 정한 넷을 그대로 쓴다 — `활성` / `부분 활성` / `미활성` / `대기`.
**루프가 새 이름을 만들지 않는다.** 만들면 하네스 문서와 STATE.md 가 다른 말을 하게 된다.
넷 중 `부분 활성` 은 **활성 시점이 하나가 아닐 때** 쓴다. 하네스는 게이트 안을 더 쪼개어
"전체는 이 태스크부터, 그중 어떤 부분은 저 태스크부터"로 적어 두는 일이 있다
(예: unit 게이트가 통과하기 시작하는 시점과 그 안의 통합 테스트가 도는 시점이 다르다).
그 사이 구간이 `부분 활성` 이고, **활성으로 올리면 아직 못 도는 부분이 통과로 새고
미활성으로 두면 이미 도는 부분이 판정에서 빠진다.** 흐름 목록에도 같은 표기가 붙는다 — 붙이는 것은 `harness-update` 이고 루프는 읽기만 한다.
`대기` 는 `blocked` 태스크에 걸린 것이고, 사람이 미결을 풀어야 풀린다. 루프가 올리지 않는다.
---
## 5단계. 실패 처리 — 분류가 먼저, 재시도는 3회
### 고치기 전에 무엇이 잘못됐는지부터 정한다
Layer 4가 셋을 확정했고, 게이트를 실제로 돌려 보면 **코드가 잘못된 것이 아닌 갈래가 둘 더 나온다.**
**분류를 건너뛰고 바로 코드를 고치는 것이 이 루프에서 가장 비싼 실수**다 —
원인이 코드가 아닌데 코드를 고치면 고칠수록 멀쩡한 코드가 망가지고, 재시도 예산은 그대로 소모된다.
| 분류 | 알아보는 법 | 무엇을 고치는가 | 게이트 재실행 |
|---|---|---|---|
| **진짜 결함** | 같은 순서를 수동으로 밟아도 같은 결과 | **코드** (루프) | **최대 3회, 처음부터 전부** |
| **낡은 스펙** | 화면·규약이 의도적으로 바뀌었다. 게이트가 옛 정의를 본다 | 흐름 정의 (`harness-update`) | 1회, 실패한 게이트만 |
| **환경 문제** | `local-dev` 가 0이 아닌 코드로 끝났다. 안 뜬 구성 요소, 남은 데이터, 네트워크 | 환경 (재준비 → `harness-update`) | 1회, 실패한 게이트만 |
| **흔들림** | 같은 코드로 돌릴 때마다 결과가 다르다. 재시도로 살아난 통과(`flaky`) | **그 테스트** (`harness-update` — 안정화는 ①이라 무인 자식도 한다. 단언을 빼는 것은 ③) | 1회, 실패한 게이트만 |
| **판정 불가** | 수용 기준이 관찰 가능하게 쓰이지 않았다 | 태스크 문서 (Layer 3) | 0회 — 즉시 중단 |
**"재시도를 세지 않는다"가 "봐주고 넘어간다"는 뜻이 아니다.** 다섯 갈래 전부 그 태스크는 통과가 아니다.
세지 않는 이유는 하나뿐이다 — **3회는 코드를 고치는 예산인데 뒤 넷은 고칠 곳이 코드가 아니고**,
루프가 고칠 것이 없으니 다시 돌려도 같은 결과다. 그래서 **같은 분류가 그 태스크에서 또 나오면 즉시 멈춘다.**
남은 1회는 예산이 아니라 **고친 것이 고쳐졌는지 보는 확인 실행**이고, 0으로 만들면 그것을 모른다.
**재실행 범위도 갈린다.** 흔들림·환경 문제·낡은 스펙을 고칠 때는 **제품 코드가 한 줄도 안 바뀐다** —
선택자를 고치거나 컨테이너를 다시 띄운 것뿐이라 lint·unit 결과가 바뀔 수 없다. 그래서 **실패한 게이트부터** 돈다.
`진짜 결함` 만 처음부터 전부 다시 돈다 — 코드를 고치면 다른 파일이 깨질 수 있고,
하네스의 lint 게이트도 수정한 파일이 아니라 프로젝트 전체를 도는 것으로 정의돼 있다.
### 코드가 아닌 갈래를 알아보는 법
**환경 문제는 `local-dev` 의 종료 코드로 확정된다.** 하네스가 실패한 단계를 숫자로 구별해 두었으므로
**0이 아니면 그 자체가 환경 문제**이고 추측할 것이 없다. 다만 **누가 고치는지가 갈린다** —
런타임·엔진이 없거나 환경변수가 비어 있는 것은 사람이 설치·설정할 일이라 사용자에게 가고,
의존 서비스·스키마·앱이 안 뜨는 것은 재준비 후 안 되면 `harness-update` 다.
`-Status` 와 `-Logs` 가 이 판정의 도구이고, 하네스가 그것을 둔 이유가 이것이다.
**흔들림은 하네스가 통과 조건에서 이미 걸러 준다.** 그런데 이건 앞 셋 어디에도 안 맞는다 —
코드는 멀쩡하고(실제로 통과했다), 화면이 바뀐 것도 아니고, 서비스도 잘 떠 있다.
**갈래를 안 만들면 거의 항상 `진짜 결함` 으로 찍히고, 코드를 고쳐도 흔들림은 안 사라져 3회가 통째로 날아간다.**
고칠 곳은 테스트 설계이고 그건 `harness-update` 의 일이라 루프의 예산이 나올 자리가 없다.
넘길 곳은 하네스 e2e 게이트의 흔들림 규칙(역할 기반 선택자, 실행마다 고유한 데이터, 인증 상태 재사용,
외부 값은 실제 경로에서 읽기)이다. **흔들림을 남겨 둔 채 진행하는 선택지는 없다** — 흔들리는 게이트는 꺼진 게이트다.
**검증자가 "환경이 이상해서 판정을 멈췄다"고 하면 그건 `FAIL` 이 아니다.** 그 출력을 실패로 받으면
**안 뜬 화면을 보고 "FAIL" 을 적은 것과 같아져** 멀쩡한 코드를 세 번 고치게 된다. `환경 문제` 로 받는다.
하네스의 판정 형식이 그것을 위해 `RESULT: 환경 문제` 를 갖고 있다(`local-dev` 상태 확인이 비0이거나 앱이 응답하지 않을 때).
반영 토막은 그 값을 보면 코드가 아니라 환경을 재준비하고 `대기` 로 되돌린다.
`판정 불가` 는 구현이 아니라 **태스크 문서의 문제**다. 고치지 말고 즉시 멈춰 Layer 3으로 보고한다.
### 3회는 루프가 정한 값이 아니다
**원본 사양이 문장으로 못 박은 규칙이고, 루프가 조정할 수 있는 값이 아니다.** `loop-setup` 이 하는 일은
그것을 **어떻게 셀지**를 이 프로젝트에 맞게 적는 것이지 몇 회로 할지 고르는 것이 아니다.
사용자가 "3번으로 자르지 말고 통과할 때까지 해 달라"고 요청하는 일이 실제로 있다. **그때 상한을 지우지 않는다.**
- **한도가 사라지면 남는 선택지가 하나뿐이 된다.** 네 번, 다섯 번 고쳐도 안 통과할 때 다음 수는
수용 기준을 자기 구현에 맞게 읽는 것이고, 사람은 그것을 눈치채지 못한다 — 기록에는 통과만 남는다.
**3회 상한은 그 지점에 사람을 부르려고 있는 것**이지 세 번이 충분해서가 아니다
- **불편의 진짜 원인은 대개 상한이 아니다.** 세 번을 다 쓰고 멈추는 일이 반복됐다면 그중 몇 번이
환경 문제나 흔들림에 소모된 것이다. 답은 상한 제거가 아니라 **분류를 먼저 하는 것**이고,
분류가 붙으면 코드가 아닌 실패는 예산을 쓰지 않으므로 세 번은 실제로 세 번이 된다
- **정말 늘려야 한다면 그건 원본 사양을 고치는 결정**이다. 루프 문서에서 조용히 바꿀 일이 아니다
같은 이유로 **게이트를 건너뛰게 하는 것, 분리 세션을 포기하는 것**도 요청받았다고 반영할 값이 아니다.
셋 다 "사용자가 요청한 검증을 사용자 모르게 줄이는" 쪽으로만 움직인다. **요청을 무시하라는 뜻이 아니다** —
왜 그 값인지, 원하는 결과를 다른 방법으로 얻을 수 있는지를 답하고, 그래도 바꾸겠다면 무엇이 사라지는지를 밝힌다.
### 어떻게 세고, 언제 멈추는가
```
게이트 실행 1회차 → 실패 (진짜 결함)
수정 1 → 재실행 2회차 → 실패 ← 사이클 1
수정 2 → 재실행 3회차 → 실패 ← 사이클 2
수정 3 → 재실행 4회차 → 실패 ← 사이클 3 → 여기서 멈춘다
```
**세는 것은 '수정한 횟수'이지 게이트를 돌린 횟수가 아니고, `진짜 결함` 만 센다.**
카운터는 태스크마다이고 태스크가 완료되거나 바뀌면 0으로 돌아간다.
**태스크를 넘어 반복되는 실패는 따로 센다.** 카운터가 태스크마다 리셋되므로, 같은 원인이 여러 태스크에 걸쳐
반복되면 영원히 안 잡힌다 — 흐름 하나가 계속 흔들리는데 태스크마다 "이번이 처음"이 되어 매번 낭비하고 넘어간다.
그래서 **실패한 흐름·검사 항목의 ID를 분류와 함께 `LOG.md` 에 남기고**, 같은 것이 **두 태스크에 걸쳐** 나오면
태스크 카운터와 무관하게 `harness-update` 로 올린다. 여기가 `harness-update` 0단계의 **"두 태스크에 걸친 같은 실패"**
트리거가 실제로 걸리는 자리이고, `LOG.md` 를 append-only 로 둔 이유이기도 하다 —
지금 상태만 보는 `STATE.md` 로는 이 패턴이 보이지 않는다.
**규칙만 적어 두면 아무도 세지 않는다.** 실전(ERP, 2026-09)에서 이 규칙은 `LOOP.md` 에 있었는데, 같은 unit 테스트가 phase-06 부터 phase-11 까지
여러 태스크에 걸쳐 흔들리는 동안 한 번도 실행되지 않았다 — 코디네이터는 그것을 `알림:` 한 줄로 쌓고 다음 배정으로 넘어갔다.
원인은 셋이었고, 셋 다 막는다.
1. **세는 단계가 지시문에 없었다.** 규칙은 설명 절에 있었고 코디네이터가 실제로 따르는 지시문에는 없었다. 그래서 **세는 일을 지시문의 한 단계로 넣는다** —
병렬은 코디네이터 지시문(반영 뒤, 배정 전), ①-단일은 반영 지시문(완료 후 점검). 판단 없이 셀 수 있도록 **실패 줄의 형식을 고정한다**:
`LOG.md` 에 `- 실패: <태스크 ID> / <게이트> / <검사 ID> / <분류>` (검사 ID = `FLOW-xx`·테스트 파일 경로·lint 규칙 이름).
같은 검사 ID 가 **서로 다른 태스크 ID 둘 이상**에서 나오면 걸린 것이다. 처리한 뒤에는 `- 반복 처리: <검사 ID> → <무엇을 했나>` 를 남기고,
다음 판정은 그 줄 **뒤의** 실패만 센다 — 안 그러면 이미 고친 것을 매 라운드 다시 올린다
2. **걸려도 할 수 있는 일이 없었다.** 흔들림의 원인이 공유 DB 처럼 격리 부족이면 고칠 것은 `loop-update`(격리 상향)와 `harness-update`(`local-dev` 에 슬롯 변수)이고,
둘 다 ②(범위 확대)라 무인 코디네이터가 할 수 없다. 할 수 없으면 **`질문:` 으로 올리고 새 배정을 멈춘다**(돌던 행은 끝까지 돈다).
`알림:` 으로 남기는 선택지는 없다 — `알림:` 은 구동기를 멈추지 않으므로 사람이 볼 때까지 같은 낭비가 태스크마다 반복된다.
고칠 방법이 ①(테스트 안정화·흐름 정의 보정)이면 그 자리에서 `harness-update` 를 부른다
3. **흔들림이 격리 부족에서 오는지 가르는 기준이 없었다.** 격리 3에서 흔들리는 검사가 **DB·앱을 쓰는 테스트**(통합 unit·e2e)이면 격리 부족으로 본다 —
테스트를 아무리 안정화해도 다른 슬롯이 같은 DB 를 동시에 고친다. 그때 가는 곳은 `harness-update` 가 아니라 `loop-update`(격리 1·2)다
**한 태스크 안에서는 분류를 합쳐 센다.** 코드가 아닌 갈래는 각각 1회이지만, 그것들이 같은 태스크에서 **합쳐 3회** 쌓이면
`진짜 결함` 이 아니어도 멈춘다. 분류만 바꿔 가며 재실행을 이어 가면 예산이 사실상 없어지기 때문이다.
### 소진하면 무엇을 하는가
**작업을 일시 중단하고 사용자에게 보고한다. 스스로 풀지 않는다.** 원본이 정한 것이고,
계속 진행하게 두면 다음 선택지가 하나뿐이다 — 수용 기준을 자기 구현에 맞게 읽는 것.
멈출 때 **다음 세션이 이어받을 수 있게 남긴다.** 사용자의 답이 며칠 뒤에 올 수도 있다.
- 태스크 `status: failed`, `STATE.md` 의 `Task Status` 도 같은 값
- `Last Verification` — 어느 게이트에서 무엇이 실패했는가 한 줄
- 재시도 이력 — 각 시도가 무엇으로 분류됐고 무엇을 고쳤는가 (**6단계에서 정한 자리에**)
- `Next Action` — **사용자에게 무엇을 묻고 있는가**. 선택지가 있으면 선택지
- `LOG.md` — 관찰한 것. 원인 추정이 아니라 "무엇을 했더니 무엇이 보였는가"
- **작업 트리의 미커밋 변경은 그대로 둔다.** 커밋하지 않고(검증받지 않은 상태를 기본선으로 만들지 않는다), 되돌리지도 않는다.
그 태스크는 `Current Task` 로 남아 있으므로 다음 세션은 다른 태스크를 집지 않는다
**사람의 답은 둘 중 하나이고, 각각 종착 상태가 정해져 있다.** 정해 두지 않으면 `failed` 가 영원한 상태가 되어 후행 태스크가 전부 막힌다.
| 답 | 루프가 하는 것 |
|---|---|
| **"이어서"** (필요하면 힌트와 함께) | `status` 를 `in_progress` 로 되돌리고, `Retry` 의 `Cycle` 을 0으로 — **`History` 는 지우지 않는다**(같은 시도를 반복하지 않으려면 남아야 한다). `LOG.md` 에 "사용자 승인으로 재시도 예산 갱신" 한 줄. 원본이 "사용자의 응답에 따라 후속 작업을 진행"이라고 정한 자리라 3회 규칙의 예외가 아니다 |
| **"건너뛰라"** | `files` 에 적힌 파일을 되돌리고(`files` 밖의 변경은 범위 확장이었으므로 보고), `status` 는 `failed` 그대로, `Current Task` 를 비우고 **`Retry` 를 초기화**하고 `LOG.md` 에 남긴다. 다음 세션은 이 태스크에 의존하지 않는 다른 실행 가능 태스크를 집는다. **후행 태스크는 `depends_on` 규칙으로 자동으로 열리지 않는다** — 그래서 건너뛴 태스크는 폐기하거나 다시 자를 때까지 후행이 서 있고, 그 결정은 `plan-generator` 10단계의 일이다. 무인 자식은 보고할 통로가 `STATE.md` 뿐이므로 `Next Action` 에 **`알림(plan-generator, ~다음 phase 전): <ID> 건너뜀 — 폐기 또는 재작성 필요, 막힌 후행 n개`** 를 남긴다. `알림:` 은 `질문:` 과 달리 구동기를 멈추지 않고, 처리 주체가 처리한 뒤 지운다(아래 "`알림:` 에는 주인과 만료가 있다") |
보고에는 **어느 분류였는지와 그 근거**를 반드시 넣는다. 사용자가 판단할 재료가 그것이다.
### `알림:` 에는 주인과 만료가 있다
`질문:` 은 구동기를 멈추고 `알림:` 은 멈추지 않는다. 멈추지 않는 것은 장점이지만, **주인도 기한도 없는 `알림:` 은 아무도 읽지 않는 목록이 된다** —
실전에서 phase-03 부터 60줄 넘게 쌓였고 그중 여럿이 이미 조건이 충족된 트리거(격리 재검토, `harness-update` 후보)였다.
그래서 형식을 고정한다: **`알림(<처리 주체>, ~<만료>): <내용>`**. 처리 주체는 `사람`·`plan-generator`·`harness-update`·`loop-update` 중 하나,
만료는 기본이 **다음 phase 를 열기 전**이다.
- **루프가 스스로 할 수 있는 것은 `알림:` 으로 남기지 않는다** — `harness-update` ①로 되는 것이면 지금 한다. 남기는 것은 ②③이거나 다른 레이어의 일뿐이다
- **phase 를 열기 전에 만료된 `알림:` 을 확인한다** — 남아 있으면 한 번에 모아 `질문: 처리되지 않은 알림 n개 — 처리 / 다음 phase 까지 연장 / 폐기` 로 올리고
다음 phase 를 열지 않는다. phase 안에서는 묶어 두고 경계에서 한 번 묻는 것이라, 사람을 태스크마다 부르지 않으면서 쌓이지도 않는다
- **처리한 쪽이 지운다.** 연장은 만료만 바꾸고 `LOG.md` 에 한 줄 남긴다
---
## 6단계. 세션이 끊겨도 남아야 하는 값 — STATE.md 에 절을 더한다
재시도 횟수는 **세션이 죽으면 사라지는 값인데 사라지면 안 되는 값**이다.
지금 STATE.md 에는 둘 자리가 없다. 원본의 다섯 절은 예시이지 상한이 아니므로 **절을 더한다.**
`Last Verification` 에 끼워 넣지 않는다 — 그 절은 Layer 4가 "지금 상태 한 줄"로 정했고 게이트를 돌릴 때마다 덮어써진다.
덮어써지는 자리에 누적값을 두면 첫 재실행에서 카운터가 사라진다.
### 더하는 절 둘
**`# Retry`** — 현재 태스크의 재시도 예산. 태스크가 완료되거나 바뀌면 초기화한다.
```markdown
# Retry
* **Task:** phase-02-task-03
* **Cycle:** 2 / 3 (진짜 결함만 센다)
* **Gate:** e2e
* **Classification:** 진짜 결함
* **History:**
* 1) e2e FLOW-03 / 흔들림 / harness-update 로 선택자 수정 (Cycle 미증가)
* 2) e2e FLOW-03 / 진짜 결함 / 세션 만료 처리 수정
* 3) e2e FLOW-03 / 진짜 결함 / 쿠키 SameSite 수정
```
`History` 가 있어야 다음 세션이 **같은 것을 또 고치지 않는다.** 숫자만 남기면 "1회 남았다"는 알지만
앞의 시도들이 무엇을 했는지 몰라 첫 시도를 반복한다.
**코드가 아닌 갈래도 여기 쌓되 `Cycle` 은 올리지 않는다** — 세지 않기로 한 것이 이력에서는 보여야
"같은 분류가 두 번째"와 "같은 흐름이 두 태스크에 걸쳐"를 판정할 수 있다.
그래서 각 줄에 **게이트·흐름 ID·분류·무엇을 했는지**를 함께 적는다. 셋 중 하나라도 빠지면 판정이 안 된다.
**`# Standing Decisions`** — 사람이 내렸고 **한 번 이상 유효한** 판단.
```markdown
# Standing Decisions
* **미활성 e2e 로 완료 허용:** phase-02-task-07 활성 시점까지 (2026-09-09 승인)
```
이 절이 필요한 이유가 있다. **미활성 게이트를 달고 태스크를 완료시킬지는 루프가 아니라 사람이 정한다**고
Layer 4가 확정했는데, 초기에는 거의 모든 게이트가 미활성이라 그대로 두면 **모든 태스크마다 멈춰서 묻게 된다.**
한 번 물어 받은 답을 여기 적어 두면 그 게이트가 활성될 때까지 다시 묻지 않는다.
대화가 아니라 문서에 남기는 이유는, 그 승인이 다음 세션에도 유효해야 하기 때문이다.
**둘 다 STATE.md 안이고 새 파일을 만들지 않는다.** Layer 4가 "검증 결과는 STATE.md 와 LOG.md 로만 간다"고
정한 것과 같은 이유다 — 기록처가 셋이 되면 셋이 서로 어긋난다.
### 승인은 만료된다 — 루프가 매 사이클 확인해 지운다
승인은 **"그 게이트가 활성될 때까지"** 로 발급된다. 그러니 **게이트가 활성되는 순간 죽은 종이가 된다.**
남겨 두면 **루프가 실제로 도는 게이트를 "승인받았으니 건너뛴다"로 읽는다** — 통과율은 오히려 좋아 보이는,
가장 조용한 종류의 구멍이다. 그리고 이것은 하네스가 고장 나서가 아니라 **잘 굴러가서** 생긴다.
게이트 활성은 정상 진행이고, 하네스가 좋을수록 더 일찍 온다.
**이 확인은 루프가 매 사이클 스스로 한다.** 별도 갱신을 부를 일이 아니다 —
사이클을 열 때 `Standing Decisions` 를 이미 읽고(다시 묻지 않기 위해) 게이트 활성 여부도 이미 판정하므로
(3단계·4단계), **대조할 재료가 이미 손에 있다.** 만료된 항목은 그 자리에서 지우고 `LOG.md` 에 한 줄 남긴다.
**루프가 자기 승인을 건드려도 안전한 이유는 방향 때문이다.**
| 승인에 대해 | 누가 | 왜 |
|---|---|---|
| 사람에게 묻고 기록한다 | **루프** | 사람이 답한 것이다 |
| **만료된 것을 지운다** | **루프** | 봐주는 것을 **없애는** 방향이라 자기를 봐줄 수 없다 |
| 범위를 넓힌다 (게이트 단위로 등) | `loop-update` | 느슨해지는 방향이라 등급 판정과 보고가 필요하다 |
| 절의 형식을 바꾼다 (병렬 등) | `loop-update` | Layer 3이 만든 절까지 건드리게 된다 |
**무기한 승인을 만들지 않는다.** 범위나 기한이 없는 승인은 만료 확인이 불가능해지고,
그러면 이 절이 "한 번 봐주면 영원히 봐주는 자리"가 된다.
---
## 7단계. 세션 경계 — 한 태스크에 한 세션, 검증은 다른 세션
원본이 못 박았다 — **"한 task에 한 세션(클로드·코덱스 동일)을 전제로 한다."**
그 "한 세션"은 **구현하는 세션**을 말한다. 검증은 거기서 떼어 내야 하므로 한 태스크에 세션이 하나 더 붙고,
판정을 받아 마무리하는 자리까지 세면 **셋**이다(아래 "구동기"의 세 토막).
```
구현 세션 : 태스크 하나를 구현하고 lint·unit·e2e 를 돌린다. 검증 대기를 적고 닫는다
검증 세션 : task_validation 만 돈다. 구현 세션이 무엇을 했는지 모른다
반영 세션 : 판정을 기록하고 완료·커밋 또는 수정·재실행을 한다 (병렬에서는 코디네이터 자식이, ③에서는 상위 세션이 겸한다)
```
### 검증 세션에 무엇을 주고 무엇을 안 주는가
하네스의 `task_validation.md` 가 목록을 갖고 있다. **루프는 그 목록을 실제로 지키는 쪽**이다.
| 준다 | 주지 않는다 |
|---|---|
| 태스크 문서 경로 | 구현 세션의 대화·요약·"무엇을 했는지" 설명 |
| 실행 중인 앱의 접속 주소 | 구현자의 통과 보고나 자체 점검 결과 |
| `quality_gates/task_validation.md` 경로 | 앞 게이트가 통과했다는 사실 이상의 실행 로그 |
| 판정 블록을 그대로 출력하라는 지시 | 원인 추정, 확인 요청, "이 부분은 이렇게 만들었다" |
| (필요 시) 태스크의 `architecture` 가 가리키는 설계 절 — 하네스가 허용한 유일한 추가 입력 | 그 밖의 문서, 커밋 메시지, 최근 변경 파일 |
**주지 않는 목록이 이 게이트의 실질이다.** 설계 절을 여는 것을 허용하는 이유는 수용 기준이 `database.md §3.2` 처럼
설계를 인용하는 일이 흔하고, 그 절은 구현 세션이 아니라 Layer 2가 쓴 것이라 구현 판단이 새지 않기 때문이다. 가장 흔한 누수는 "참고하라고 붙이는 한 줄 요약"인데,
그 한 줄이 정확히 구현 중에 형성된 판단이다.
### 분리가 안 되는 환경이면 통과로 기록하지 않는다
환경마다 분리 수단이 다르다(서브에이전트, 새 세션, 다른 워크트리의 에이전트).
`LOOP.md` 에는 **이 프로젝트에서 실제로 쓸 수단**을 적는다.
수단이 없으면 **`task_validation` 을 `미활성` 로 두고 사용자에게 알린다.**
같은 세션이 구현하고 판정한 것을 통과로 적으면, 통과 기록만 남고 실제로는 구현자가 자기 코드를 승인한 것이다.
그건 안 하느니만 못하다 — 아무도 그 태스크를 다시 보지 않게 되기 때문이다.
### 태스크가 끝나면 세션을 닫는다
이어서 다음 태스크를 하지 않는다. 컨텍스트가 넘어가면 다음 태스크가 앞 태스크의 판단 위에서 구현되고,
무엇보다 **다음 태스크의 검증 분리가 이미 오염된 상태로 시작한다.**
세션을 닫아도 잃는 것이 없도록 만든 것이 STATE.md·LOG.md 이므로, 여기서 그것을 실제로 쓴다.
### 세션을 누가 여는가 — 구동기
"한 태스크에 한 세션"과 "검증은 다른 세션"은 **누군가 세션을 계속 열어 줘야** 성립한다.
에이전트는 자기 세션을 닫고 새 세션으로 바뀔 수 없지만, **새 세션을 자식 프로세스로 띄우는 것은 된다.**
그래서 `LOOP.md` 에 구동기를 적는다 — 적지 않으면 태스크마다 사람이 세션을 열게 되고, 그건 자동화가 아니다.
구동기는 셋 중 하나다. **오르카가 있으면 ②, 없으면 ①이 기본**이고, 둘 다 병렬(11단계)을 운용한다. 사용자가 다른 방식을 지정하면 그것을 그대로 따른다.
| 모드 | 무엇으로 세션을 여는가 | 언제 |
|---|---|---|
| **① 스크립트 구동** | `claude -p "<지시문>"` / `codex exec "<지시문>"` 을 반복하는 스크립트. **단일**(세 토막을 차례로)과 **병렬**(코디네이터 자식 하나 + 슬롯마다 구현·검증 자식) 두 형태가 있다 | **오르카가 없을 때의 기본.** 두 환경에서 같다 |
| **② 오르카 스크립트 구동** | 오르카 터미널 하나에서 도는 **구동 스크립트**가 워커를 `orca orchestration task-create --spec "<지시문>" --display-name "worker<N>_<태스크 ID>"` → `worker-start --task <그 ID> --worktree <슬롯> --agent claude\|codex` 로 띄우고 `check --wait` 로 받는다. 판단은 ①-병렬과 같은 **코디네이터 자식**(`claude -p`)이 라운드마다 새로 한다 | **오르카가 있을 때의 기본.** 워커 카드·질문 응답·생존 확인을 오르카가 맡는다. 명령 표면은 `orca skills get orchestration` 으로 그 버전 것을 읽는다 |
| **③ 서브에이전트** | 상위 세션이 Agent 도구로 구현·검증 서브에이전트를 띄운다. 검증 서브에이전트의 출력을 상위 세션이 **판정 파일로 저장한 뒤** 같은 반영 절차를 밟는다 | 클로드 코드 전용 대안. 사용자가 지정할 때만 |
오르카 유무는 세팅 때 확인한다(`orca --version` 같은 CLI 응답). 있으면 ②, 없으면 ①-병렬로 적고, 사용자가 병렬을 끄면 ①-단일이다.
**①-병렬의 모양.** 셸 스크립트는 판단하지 않는다는 원칙은 그대로다 — 판단은 **코디네이터 자식**이 하고, 스크립트는 **띄우고 기다리기**만 한다.
```
반복 {
질문: 이 있고 답: 이 없으면 → 멈추고 사람에게 / Phase Overview 전부 completed 면 → 끝
claude -p "<코디네이터 지시문>" ← 결과 파일 반영 → 병합 → 기록 → 완료·커밋 → 다음 배정 (STATE.md 의 배정표를 갱신)
배정표의 행마다 워커 트리에서 병렬로 띄우고 전부 끝날 때까지 기다린다:
토막 = 구현·수정 → claude -p "<구현 지시문> 태스크: <ID>" > <임시>/done-<ID>.txt
토막 = 검증 → claude -p "<검증 지시문> 태스크: <ID>" > <임시>/verdict-<ID>.txt
배정표가 비어 있고 코디네이터가 Next Action 에 "배정 없음" 을 남겼으면 → 멈춤 (차단 대기 또는 막다른 곳)
}
```
코디네이터는 **구현하지 않는다.** FAIL 로 돌아온 태스크의 수정도 코디네이터가 하지 않고 배정표의 그 행을 `수정` 으로 되돌려 같은 슬롯의 새 구현 자식이 고친다
(`Retry` 가 1 오른다). ②의 코디네이터도 이 자식과 같은 것이고, 매 라운드 새 세션이라 컨텍스트가 쌓이지 않는다.
구현 자식의 마지막 출력(`task_validation 대기` / `task_validation 미활성` / `failed: <사유>`)은 `done-<ID>.txt` 로 받고, 검증 자식의 판정 블록은 `verdict-<ID>.txt` 로 받는다.
**병렬 워커는 `STATE.md`·`LOG.md`·태스크 문서·하네스 문서를 쓰지 않는다** — 워커 브랜치는 코드만 바꾸고, 기록할 것은 출력 블록으로 넘긴다(11단계 필자 규칙).
문서는 코디네이터 트리 경로에서 읽는다 — 워커 트리의 문서는 낡았을 수 있다.
**②의 모양 — 코디네이터를 상주시키지 않는다.** ②는 ①-병렬과 **판단하는 쪽이 같고 워커를 띄우는 수단만 다르다.**
처음 설계는 대화형 코디네이터 하나가 phase 내내 살아서 워커를 띄우고 기다렸는데, 실전(ERP, 2026-09)에서 그 세션 10개가 전체 토큰의 30%를 썼다 —
기다리는 동안의 폴링(`check`·`worker-list`·대기)이 한 phase 에 수백 번이고, 그때마다 수십만 토큰으로 자란 컨텍스트를 통째로 다시 읽었다.
기다리기는 판단이 아니므로 **스크립트가 한다.** 판단(반영·분류·배정)은 라운드마다 **새로 여는 코디네이터 자식**이 하므로 컨텍스트가 쌓이지 않는다.
```
구동 스크립트 (오르카 터미널에서 띄운다. 오르카 CLI 는 호출한 터미널을 코디네이터로 묶는다 — 밖에서 돌리면 no_active_sender_terminal)
띄우기: 이미 돌면(PID 파일) 알리고 끝. 아니면 환경을 파일로 적고 터미널 프로세스 밖에서 자신을 다시 띄운다 — 그 뒤 터미널을 닫아도 돈다
(스크립트 자신의 환경에는 ORCA_* 를 두지 않고 orca 호출에만 붙인다. 밖의 감시도 같은 방식으로 띄운다)
시작: 저장해 둔 Run 이 있으면 run-use --id <Run> (재시작해도 돌던 워커를 잃지 않는다), 없으면 run-create 후 Run ID 를 (임시)/loop-run.txt 에.
쌓인 배달을 먼저 처리하고, 남은 dispatch 가 exited 면 멈춘다("정리 필요")
반복 {
질문: 이 있고 답: 이 없으면 → 새로 띄우지 않는다. 돌고 있는 워커가 다 끝나면 멈추고 사람에게
Phase Overview 가 전부 completed 면 → 끝
격리 3 이고 검증 워커가 돌고 있으면 → 코디네이터 자식·e2e·앱 전환을 미루고 곧장 아래 대기로 (worker_done 처리·ack 는 미루지 않는다)
claude -p "<코디네이터 지시문>" --add-dir <임시> ← ①-병렬과 같은 지시문·허용 목록. 결과 파일 반영 → 병합 → 기록 → 배정(STATE.md 의 Assignments)
Assignments 행 중 dispatch 파일도 결과 파일(done-·verdict-<ID>.txt)도 없는 행마다 (결과 파일이 남은 행은 끝난 워커다 — 다음 자식이 반영한다):
구현·수정·검증 → task-create --spec "worker-N: <지시문> 태스크: <ID>" --display-name "worker<N>_<ID>"(검증이면 끝에 _검증)
→ worker-start --task <받은 task id> --worktree path:<슬롯 트리> --agent <8절> --model <8절> (슬롯 1 도 path: — current 는 터미널의 워크트리로 풀린다)
(워커는 출력 블록·판정 블록을 (임시)/done-<ID>.txt·verdict-<ID>.txt 에 쓰고 worker_done --report-path 로 끝낸다)
e2e 대기·검증(격리 3) → 슬롯 순서대로 하나씩: 앱 전환 → e2e 대기면 e2e 명령을 직접 돌리고(통과면 행을 검증으로) → 검증 워커를 띄우면 거기서 멈춘다(한 번에 하나)
도는 dispatch 가 0개면 → 기다리지 않고 반복의 첫 줄로 (깨울 worker_done 이 없다). 그 한 바퀴에서도 새 dispatch·행 변화가 없으면 멈추고 사람에게
check --wait --types "worker_done,escalation,question" 을 Run 에 건다 (한 번 60초. timeout 은 체크포인트이지 실패가 아니다):
question·escalation → claude -p "<질문 답변 지시문>" → 첫 줄(코드 울타리 ``` 줄과 빈 줄은 건너뛰고, 앞 공백과 ** 같은 강조 표시는 벗긴다)이 "답:" 이면 reply. "질문:" 이면(자식이 Next Action 에 적었다)
reply "사람 판단 대기 — 지금 상태 그대로 STATUS: failed: 사람 판단 대기 블록을 쓰고 끝내라"
(reply 가 연결 끊김으로 실패하면 그 배달을 ack 하지 않는다 — 다시 와서 다시 답한다. answer_conflict 는 이미 답한 것이다)
worker_done → 결과 파일이 있는지 본다(없으면 본문을 파일로 남긴다 — END 가 없으니 코디네이터가 '중단'으로 처리), worker-release,
그리고 다음 라운드로 (끝난 것이 하나라도 있으면 코디네이터가 반영한다. 나머지 워커는 계속 돈다)
빈 대기가 15분 이어지면 → worker-list 의 생존 판정을 본다. exited 인데 worker_done 이 없으면 결과 파일이 없을 때만 END 없는 "exited, worker_done 없음" 을 적고 다음 라운드로
매 대기 뒤 → 턴이 끝난 채 조용한 워커를 send 로 한 번 깨우고, 답 없는 질문·시작 뒤 전사 없음을 알린다
}
자식이 실패하면 → 한도면 재설정까지 쉬고, 아니면 1·5·15분 뒤 다시(3회). 그래도 실패하면 멈춘다
멈출 때 → 처리한 배달을 check --ack <배달 ID> --peek 로 ack 만 하고, 종료 사유("질문 대기"·"배정 없음"·"전체 완료"·그 밖의 원인)를 남기고 알린다
밖의 감시 (따로 1분마다, 읽기만): 스크립트가 죽었거나 대기에서 멈춤 · 한 단계가 너무 김 · 사람이 답할 프롬프트 · 새 질문:·종료 사유·답: 이 적힘 → 알림
```
스크립트는 **판단하지 않는다** — 띄우고, 기다리고, 받은 질문을 판단 자식에게 넘기고, 결과 파일을 확인할 뿐이다. `STATE.md` 의 필자는 코디네이터 자식이다(질문 답변 자식은 `Next Action` 에 `질문:` 한 줄만 쓴다).
**스크립트가 `STATE.md` 를 건드리는 예외는 하나뿐이다** — 자식과 자식 사이에, 결과에 따라 `Assignments` 행의 **토막 값만** 기계적으로 바꾼다(격리 3 의 `e2e 대기` → e2e 종료 코드가 0 이면 `검증`, 아니면 `수정`).
종료 코드만 보는 일이라 판단이 없고, 필자가 겹치지 않는다 — 스크립트는 차례로 돌아 코디네이터 자식·질문 답변 자식이 도는 동안에는 쓰지 않고, 그때 도는 워커는 `STATE.md` 를 쓰지 않는다. 그 밖의 칸(`Retry`·`Last Verification`·`Next Action`)은 쓰지 않는다 — e2e 출력은 `(임시)/e2e-<ID>.txt` 로 넘긴다.
**phase 경계를 그냥 지난다** — ①-병렬과 같다. 다음 phase 코디네이터를 새 터미널로 띄울 필요가 없고(실전에서 그 터미널 생성이 수십 번 시간 초과로 실패했다),
그래도 "phase 마다 새 컨텍스트"는 더 강하게 지켜진다 — 판단 자식이 라운드마다 새로 열리기 때문이다.
사람이 스크립트를 여는 것은 처음 시작할 때와 `질문:` 에 답한 뒤 재개할 때뿐이다.
이 모양은 세팅 때 실제로 돌려 확인한다(12단계의 스모크 테스트) — 오르카 CLI 는 버전마다 표면이 바뀐다.
**②의 정지 감시 — 일하는 워커는 건드리지 않고, 진행할 수 있는데 멈춘 상태만 잡는다.** 구동 스크립트가 늘 `check --wait` 로 기다리므로 "코디네이터가 턴을 끝내 못 봄" 은 구조적으로 없다.
그래도 남는 정지가 있다(ERP 실측·실험, 2026-09~10) — 백그라운드 완료 알림이나 끊긴 `ask` 뒤 잠든 워커, 사람만 풀 수 있는 프롬프트, 시작하다 만 워커, 한도·과부하로 실패한 자식,
PC·오르카 재시작 뒤 끊긴 워커, 죽거나 한 단계에 묶인 스크립트. 감시는 이것을 **두 자리**에서 본다 — 스크립트 안(대기 뒤·자식 뒤·시작 때 판정하고, 정해진 일곱 가지 조치만 한다:
깨우기 1회·시작 재시도 1회·한도 쉼·자식 재시도·사유를 남기고 멈춤·도는 dispatch 0 이면 첫 줄로·이중 기동 거부)과 **스크립트 밖의 작은 감시**(파일과 읽기 전용 명령만 보고 알림만 —
스크립트 자신이 죽은 것은 스크립트가 알릴 수 없다). 나머지는 모두 알림이고, 같은 상태는 한 번만 알린다. 감시는 자식을 띄우지 않고 `STATE.md` 를 쓰지 않는다 — 필자는 그대로다.
워커를 멈추거나 다시 띄우는 근거는 오르카의 양성 증거(`worker-start` 실패 영수증, `exited`)뿐이고, 무언가가 없다는 것(전사 없음·응답 없음)은 알림의 근거일 뿐이다.
깨우기는 `send --to dispatch:<ID>` 다 — 쉬는 워커에 오르카가 "You have 1 orchestration message" 를 입력으로 넣어 깨운다(2026-10-01 실험). 답은 늘 `reply` 로 보내고 `send` 는 깨우기에만 쓴다.
세부(항목 10개·기준값)는 4절 ② 블록·"밖의 감시" 와 8절에 있다.
**한 태스크의 사이클은 세 토막이고, 토막마다 세션이 다르다.** 검증이 별도 세션이라는 것은 곧 **검증 앞과 뒤도 같은 세션일 수 없다**는 뜻이다 —
구현 세션은 검증 결과를 기다릴 수 없고(끝나야 검증 세션이 열린다), 검증 세션은 `STATE.md` 를 쓰지 않는다. 그래서 판정을 받아 완료·커밋·실패 처리를 하는
**반영** 토막이 따로 있다.
| 토막 | 사이클의 단계 | 누가 하나 |
|---|---|---|
| **구현** | 태스크 선택 → 시작 전 점검 → `in_progress` 기록 → 구현 → `local-dev` → `lint`·`unit`·`e2e` → `Last Verification` 에 **`task_validation 대기`** 를 적고 종료. **`task_validation` 이 아직 미활성**(활성 시점 태스크 미완료)이면 대기 대신 **`task_validation 미활성`** 을 적는다 — 검증 세션을 열 것이 없다 | ① 구현 자식 / ② 워커 / ③ 서브에이전트 |
| **검증** | `task_validation` 만. 판정 블록을 출력하고 종료 | ① 검증 자식 / ② 검증 워커 / ③ 검증 서브에이전트 |
| **반영** | 판정 블록을 `STATE.md`·`LOG.md` 로 옮김 → PASS 면 완료 판정·커밋·완료 후 점검·phase 판정 / FAIL 이면 분류 → 수정 → 앞 게이트 재실행 → 다시 `task_validation 대기` / 판정 불가·환경 문제·3회 소진이면 `Next Action` | ①-단일 **반영 자식** / ①-병렬·② **코디네이터 자식**(수정은 슬롯의 구현 워커에게 되돌린다) / ③ 상위 세션 |
①에서 반영이 세 번째 자식인 이유는 셸 스크립트가 판정을 해석하지 못하기 때문이다. **구동기는 판단하지 않고 `STATE.md` 만 읽어 어느 토막을 열지 정한다** —
판정 파일이 있으면 반영, `Last Verification` 이 `task_validation 대기` 면 검증, `task_validation 미활성` 이면 **판정 파일 없이 바로 반영**
(반영 토막은 `Standing Decisions` 의 승인이 있으면 완료로, 없으면 `질문:` 으로 처리한다), 그 외는 구현. 진짜 결함으로 수정한 뒤에는 앞 게이트를 다시 돌리고 다시 `대기` 를 적으므로
검증 토막이 다시 열린다. 재시도 카운터는 `Retry` 절에 있어 토막을 넘어 살아남는다.
초기 phase 에서는 거의 모든 태스크가 `미활성` 경로로 가고, 그것이 정상이다 — 세팅 때 받아 둔 승인이 `Standing Decisions` 에 있으면 사람이 붙지 않아도 돈다.
**사람의 답이 돌아오는 길도 정한다.** 자식이 `Next Action` 에 질문을 적고 종료하면 구동기는 멈춘다. 사람은 둘 중 하나로 답한다 —
`STATE.md` 의 `Next Action` 아래에 `답:` 줄을 직접 적거나, 대화형 세션을 열어 루프에게 말한다(그 세션이 `답:` 을 적는다).
다음 자식은 0절에서 `답:` 을 읽어 반영하고 지운다. 이 길이 없으면 자식이 던진 질문을 받을 곳이 없어 구동기가 영원히 멈춘다.
세 모드가 공통으로 지켜야 할 것 넷.
- **지시문은 `LOOP.md` 에 고정된 것을 그대로 쓴다.** 구현·검증·반영 셋. 매번 손으로 쓰면 매번 다른 것이 새어 나간다.
검증 지시문은 4·7단계의 목록 그대로다
- **자식 세션은 사람에게 물을 수 없다.** 미활성 승인·모호한 기준·3회 소진처럼 물어야 할 일이 생기면
`Next Action` 에 적고 **종료**한다. 구동기는 `STATE.md` 를 읽어 `failed`·`Next Action` 에 질문이 있으면 멈추고 사람에게 넘긴다.
②는 워커의 `ask` 를 구동 스크립트가 받아 **질문 답변 자식**에게 넘기므로, 문서로 답할 수 있는 것은 사람 없이 답하고 나머지만 `질문:` 으로 올린다
- **권한은 허용 목록으로 주고, 토막마다 다르다.** 자식은 승인을 물을 사람이 없다(클로드 코드 `--allowedTools`, 코덱스 승인 모드).
**구현·반영**: 파일 읽기·쓰기, 하네스 명령(`local-dev`, 게이트 명령), `git add`·`git commit`·`git restore`(건너뛰기의 되돌리기), 판정 파일 삭제.
**검증**: 파일 읽기, `local-dev`, `HARNESS.md` 의 관찰 수단 도구 — **쓰기와 git 은 주지 않는다**(검증 세션은 `STATE.md` 를 쓰지 않고 코드를 고치지 않는다).
되돌리기와 삭제가 빠지면 건너뛰기와 판정 파일 정리가 거부로 끝나고, 검증에 관찰 수단이 빠지면 검증자가 코드를 읽는다.
**판정 파일·결과 파일은 작업 폴더 밖(OS 임시 폴더)이라 허용 목록만으로는 지울 수 없다** — 그것을 다루는 반영·코디네이터·질문 답변 자식에는 `--add-dir <임시>` 를 함께 준다
(2026-10-01 실험: `Bash(rm:*)` 가 있어도 코디네이터 자식이 결과 파일을 지우지 못해 루프가 진전 없음으로 섰다).
- **자식이 `harness-update` 를 부를 때는 ①(상태 반영)만 무인으로 한다.** ②(범위 확대)·③(약화)이 필요하다고 판단되면 고치지 않고
`Next Action` 에 `질문:` 으로 적고 종료한다. `harness-update` 가 ③에 승인을 요구하는데 자식은 승인을 받을 수 없기 때문이고,
이것이 "루프는 하네스를 고치지 않는다"와 "필요하면 적극 갱신한다"가 무인 실행에서 함께 성립하는 방법이다 목록 밖의 쓰기는 거부로 끝나고 멈추지 않는다는 것을 확인했고,
읽기 전용 셸 명령은 목록에 없어도 통과한다. **권한 전체 건너뛰기는 사용자가 명시적으로 켠 경우에만** 쓴다
- **판정 블록은 파일로 받는다.** 검증 세션의 표준 출력을 파일로 받아 반영 토막이 `STATE.md`·`LOG.md` 로 옮긴다.
검증 세션 자신이 `STATE.md` 를 쓰게 하지 않는다 — 병렬에서 필자를 하나로 두는 규칙과 같다.
**판정 파일은 기록이 아니라 전달 수단이다.** `plan_setup/`·`loop_setup/` 밖에 두고 옮긴 뒤 지운다.
"기록처는 STATE.md·LOG.md 둘뿐"이라는 규칙의 예외가 아니다 — 옮기고 나면 사라지기 때문이다.
경로는 **태스크 ID 로 파생**한다(`<임시 폴더>/verdict-<태스크 ID>.txt`) — 병렬에서 워커 셋이 한 경로에 덮어쓰지 않게.
**잘린 파일은 판정이 아니다.** 하네스의 판정 형식은 마지막 줄이 `END` 다. `END` 가 없으면 검증 세션이 도중에 죽은 것이므로
파일을 지우고 `대기` 를 유지한다(검증이 다시 열린다). 같은 태스크에서 두 번 연속 잘리면 `질문:` — 검증 세션 자체가 죽는 원인은 환경이다
①-병렬과 ② 모두 워크트리는 **태스크 수만큼이 아니라 슬롯만큼**이다(11단계의 슬롯 규칙). **`main` 에서는 아무도 작업하지 않는다** —
코디네이터와 **첫 워커(`worker-1`)는 코디네이터 트리(`coordinator`)에서**, 동시에 코드를 쓰는 둘째부터는 `worker-N` 워크트리에서 돈다. 검증 자식은 구현이 돈 자리에서 이어 돈다.
완료된 태스크는 코디네이터 트리에 병합된 뒤 **`main` 에 fast-forward** 된다 — `main` 은 언제나 "검증된 최신"이고 사용자의 자리다.
폭이 1인 구간은 코디네이터 트리의 `worker-1` 하나로 워크트리 0개, 단일과 같다. 폭이 2 이상인 구간의 DB·포트 격리는 11단계에서 세팅 때 정해 둔 것을 따른다.
---
## 8단계. 완료 판정과 기록
### 언제 완료인가
원본의 문장이 그대로 답이다 — **"Task는 코드 작성이 끝났을 때 완료가 아니다."**
완료 조건은 하나다 — `verification` 의 게이트마다 **활성 상태에서 통과했거나, 미활성이면 `Standing Decisions` 에 유효한 승인이 있다.**
둘 중 어느 쪽도 아닌 게이트가 하나라도 있으면 완료가 아니다.
미활성 게이트에 승인이 없으면 완료가 아니라 **판단이 필요한 상태**다. 그때 **묻고**, 받은 답을 범위·기한과 함께 `Standing Decisions` 에 적는다.
커밋 조건도 같다 — 아래 "커밋"의 "네 게이트 통과"는 이 완료 조건을 뜻한다.
### 기록 순서가 중요하다
세션은 이 세 번의 쓰기 사이에서도 죽는다. 그래서 **순서를 정해 두면 중간에 죽어도 진실이 남는다.**
```
1) 태스크 문서의 status ← 원본. depends_on 판정이 이것을 읽는다
2) STATE.md ← 현재 위치 요약
3) LOG.md ← append-only 이력
4) 커밋 ← 네 게이트 통과 후에만
```
**어긋나면 태스크 문서가 이긴다.** 그래서 세션을 열 때 한 가지를 더 한다 —
`STATE.md` 의 `Task Status` 와 태스크 frontmatter 의 `status` 가 다르면 **태스크 문서 기준으로 STATE.md 를 맞추고
그 사실을 LOG.md 에 남긴다.** 앞 세션이 1)과 2) 사이에서 죽은 흔적이고, 이 한 줄이 없으면
다음 세션이 이미 끝난 태스크를 다시 구현하거나 끝나지 않은 태스크를 완료로 읽는다.
### 무엇을 기록하는가
| 시점 | STATE.md | LOG.md |
|---|---|---|
| 태스크 시작 | `Current Task`, `status: in_progress`, `Retry` 초기화 | `### <태스크 ID> Started.` |
| 게이트 결과 | `Last Verification` (태스크/결과/사유) | 통과는 한 줄. **실패는 관찰과 분류를 함께**, 그리고 `- 실패: <ID> / <게이트> / <검사 ID> / <분류>` 한 줄(반복 판정의 재료) |
| 검증 대기 | `Last Verification` = `<ID> / task_validation 대기 / —`. 구현 토막의 마지막 쓰기이고 구동기가 검증 토막을 여는 신호다 | `### <ID> lint·unit·e2e PASS. task_validation 대기.` |
| 재시도 | `Retry` 의 `Cycle`·`History` | 무엇을 고쳤는가 |
| 완료 | `status: completed`, 다음 태스크, `Retry` 초기화, `Last Verification` 을 `<ID> / PASS / —` 로 덮어쓴다(미활성 승인 완료면 `PASS (미활성 승인)`). **`대기`·`미활성` 이 남아 있으면 구동기가 반영 토막을 다시 열어 구현하지 않은 다음 태스크를 완료시킨다** | `### <태스크 ID> Verification PASS.` |
| 잘린 판정 | `Retry` 의 `History` 에 `검증 중단` 한 줄(`Cycle` 은 안 올린다). 같은 태스크에서 두 줄째면 `Next Action` 에 `질문:` — 반영 세션은 매번 새로 열리므로 여기 안 남기면 두 번째를 알 수 없다 | `### <ID> task_validation 판정 파일 잘림.` |
| 중단 | `status: failed`, `Next Action` 에 물을 것 | 세 번의 시도와 관찰 |
| phase 완료 | `Phase Overview`(그 phase `completed`, 다음 `in_progress`), `Current Phase`, phase 문서 `Status`, `Task Status` 교체 | `### phase-NN Complete.` |
| phase 완료 묶음 실패 | `Phase Overview` 는 `in_progress` 그대로, `Next Action` 에 `질문:` | `### phase-NN 완료 묶음 FAIL — FLOW-xx (분류).` |
**실패한 관찰을 LOG.md 에 남기는 것이 특히 중요하다.** 그게 없으면 다음 세션이 같은 것을 처음부터 다시 확인하고,
재시도 예산은 이미 줄어 있다. 관찰은 재현 가능한 문장으로 쓴다 — "정상 동작 안 함"이 아니라
"계정 B로 A의 인보이스 URL을 열었더니 200과 본문이 보였다".
### 커밋
**네 게이트를 통과한 뒤에만 커밋한다.** 커밋 메시지에 태스크 ID를 넣어 `LOG.md` 와 이력이 이어지게 한다.
통과 전에 커밋하면 검증받지 않은 상태가 기본선이 되고, 다음 태스크가 그 위에서 시작한다.
**merge·rebase 는 루프가 하지 않는다** — 원본이 orchestration layer 의 책임으로 정했다.
---
## 9단계. phase 완료 판정
원본이 "phase 완료 여부는 자동 판정되어야 한다"고 적었다. 판정 근거는 셋이다.
1. 그 phase 의 모든 태스크가 `completed` 인가 — `blocked` 가 남아 있으면 **완료가 아니다.** 사용자에게 보고한다.
예외는 하나, `blocked_by` 가 `[폐기]` 뿐인 태스크(Layer 3이 폐기한 것)는 없는 것으로 친다. 건너뛰어 `failed` 로 남은 태스크는 예외가 아니다 —
그건 폐기하거나 다시 자를 때까지 phase 를 막는 것이 맞고, 그 결정은 `plan-generator` 10단계다
2. `phase-NN-<이름>.md` 의 **Acceptance Criteria** 가 충족되는가 — 태스크 하나로는 안 보이는 것이 여기 있다
3. 하네스의 **phase 완료 묶음**이 통과하는가 — 아래
### phase 완료 묶음
하네스는 e2e 흐름 중 **시간이 오래 걸리는 몇 개를 매 태스크 묶음에서 빼서 'phase 완료 묶음'으로 둔다**
(브라우저 여러 종, 전 구간 시간 측정 등). 매번 돌리면 느려지고, 느려지면 사람이 게이트를 끄기 때문이다.
그래서 **phase 완료 시점이 그것을 돌리는 유일한 자리**이고, 여기서 안 돌리면 그 흐름들은 영원히 안 돈다.
**이 묶음이 실패하면 phase 를 완료로 찍지 않는다.** 하네스가 이것을 "게이트 명령이 아니다"라고 적어 둔 것은
**태스크마다 돌지 않는다는 뜻**이지 통과하지 않아도 된다는 뜻이 아니다.
여기서 넘어가면 그 흐름은 다음 phase 완료까지 아무도 다시 보지 않고, 그사이 태스크가 그 위에 쌓인다.
**다만 재시도 3회를 적용하지 않는다.** 그 예산은 태스크 하나를 대상으로 하는데 이 실패는
phase 전체가 대상이라 "무엇을 세 번 고칠지"가 정해지지 않는다. 대신 **깨진 흐름과 그것을 만든 태스크를
찾아 `Next Action` 에 `질문:` 으로 적고 사용자에게 보고한다** — 그 태스크를 다시 열지, 흐름 정의를 고칠지는 사람이 정한다.
(실패가 `흔들림`·`환경 문제` 이면 5단계의 분류가 그대로 적용된다. 여기서도 분류가 먼저다.)
**실패한 phase 위에 다음 phase 를 쌓지 못하게 막는 것은 태스크 선택 규칙이다.** `Phase Overview` 에서 현재 phase 가 `completed` 가 아니면
다음 phase 의 태스크는 `depends_on` 이 다 차 있어도 열지 않는다. 이 한 줄이 없으면 phase-02 의 태스크가 전부 `completed` 라는 이유로
다음 구현 세션이 phase-03 을 집고, 완료 묶음 실패는 `Next Action` 한 줄로만 남는다.
기록은 `Phase Overview` 를 `in_progress` 그대로 두고 `LOG.md` 에 `### phase-NN 완료 묶음 FAIL — FLOW-xx (분류).` 를 남기는 것이다.
`Last Verification` 은 태스크 단위라 여기 쓰지 않는다.
**사람의 답이 온 뒤의 진입점도 정해 둔다.** 이 상태는 "실행 가능 태스크 0개 + 미완료 태스크 0개"라 막다른 곳 규칙에도 안 걸린다.
답은 둘 중 하나다 — **`답: 재판정`**(흐름 정의를 `harness-update` 로 고쳤거나 환경을 손봤을 때)이면 다음 세션이 0절에서 그것을 읽어
**반영 토막으로 phase 판정을 다시 돈다**(태스크 완료 없이 phase 판정만 하는 유일한 경우). **`답: <태스크 ID> 다시`** 면 그 태스크를
`in_progress`·`Cycle 0` 으로 되돌려 구현 토막이 집는다. 둘 다 아니면 `질문:` 을 지우지 않고 종료한다 — 답이 안 맞으면 구동기가 다시 멈추는 편이 낫다.
**차단 때문에 `대기` 인 흐름은 실행 대상이 아니다.** 끝까지 측정할 수 없는 흐름의 부분 구간 값을
판정으로 쓰지 않는다 — 하네스가 그렇게 표시해 둔 흐름이 있으면 그 표시를 따른다.
phase 완료를 STATE.md 와 LOG.md 에 남기고, `Task Status` 를 **다음 phase 의 태스크로 교체한다**
(Layer 3이 "현재 phase 의 태스크만 나열한다"로 정했다). 이때 고치는 값이 셋이다 — 빠뜨리면 다음 세션이 끝난 phase 를 다시 돈다.
- `Phase Overview` 의 그 phase 를 `completed` 로, 다음 phase 를 `in_progress` 로. **이 절의 값은 태스크 status 와 같은 어휘 중 `pending`·`in_progress`·`completed` 셋만 쓴다**
(게이트 상태 어휘 `활성`·`미활성`… 과는 다른 공간이다). 첫 태스크를 시작할 때 그 phase 를 `in_progress` 로 올리는 것도 루프다
- `Current Phase` 를 다음 번호로
- 그 phase 문서(`phase-NN-<이름>.md`)의 `Status` 를 `completed` 로. Layer 3이 `pending` 으로 초기화만 해 둔 값이고 **여기 말고는 아무도 안 고친다**
---
## 10단계. `harness-update` 를 부르는 자리
원본도 "필요한 경우 적극적으로 업데이트되어야 한다"고 적었고, `harness-update` 0단계에 조건이 정리돼 있다.
**루프는 그 조건을 확인하는 자리를 갖는다.** 확인 자체는 비용이 거의 없고, 안 하면 비싼 쪽으로 걸린다.
| 언제 | 무엇을 확인하는가 |
|---|---|
| **태스크 시작 전** | 이 태스크의 수용 기준을 판정할 수단이 있는가 / 덮는 흐름·커버 영역이 목록에 있는가 / 쓰는 의존 서비스가 `local-dev` 에 있는가 / 게이트 명령이 이 태스크 유형에 실제로 걸리는가 |
| **태스크 완료 후** | 이 태스크가 어느 게이트의 활성 시점인가 / 어떤 흐름의 마지막 조각을 채웠나 / 필수 커버 영역을 덮었나 / 새 의존 서비스·환경변수가 생겼나 |
| **시점 무관** | 차단(OQ·CF)이 해소됐을 때(3단계의 차단 해소 절차 마지막 단계) / 같은 흐름·검사 ID의 실패가 두 태스크에 걸쳐 나왔을 때 |
**시작 전 점검을 빼먹으면 가장 비싸다.** 구현이 끝난 뒤에 "검증할 수단이 없다"를 알면 그 세션을 버리게 되고,
구현 토막이 세션 하나인 구조에서 그건 세션 하나를 통째로 잃는 것이다.
**무인 자식이 `harness-update` 를 부를 때는 ①(상태 반영)만 한다.** ②③은 `질문:` 으로 남긴다(7단계).
**병렬에서는 코디네이터만 자기 트리에서 부른다.** 워커는 시작 전 점검에서 부족을 보면 `STATUS: 하네스 부족` 으로 종료하고(11단계), 완료 후 점검은 코디네이터의 반영 토막이다.
**"게이트가 실패했다"는 그 자체로 트리거가 아니다.** 5단계에서 `낡은 스펙`·`환경 문제`로 분류됐을 때만이다.
분류 없이 실패마다 하네스를 부르면, 실패를 없애는 가장 빠른 길이 기준을 낮추는 것이 된다.
---
## 11단계. 병렬 운용 — DAG·자원 충돌·슬롯·코디네이터
기본은 **병렬**이다(사용자 결정, 2026-09-13). 태스크가 이미 파일 단위로 잘려 있고 `depends_on`·`files` 가 다 적혀 있으면
동시에 열 수 있는 태스크를 하나씩 도는 것이 낭비이고, 병렬의 위험(파일 충돌·공유 런타임·기록 충돌)은 이 절이 전부 규칙으로 막는다.
"한 task에 한 세션"은 그대로다 — 병렬은 그 세션이 **동시에 여럿** 열리는 것이지 한 세션이 여러 태스크를 하는 것이 아니다.
**세팅 때 네 가지를 묻는다**(한 번씩). ①병렬로 갈 것인가 — 기본은 예. 아니라고 하면 ①-단일 구동기로 적고 아래 격리는 미결로 남긴다.
②동시 에이전트 수 상한 — 구간별 폭(아래)을 보여 주고 고르게 한다. 폭보다 큰 수는 논다.
③워크트리 간 DB·포트 격리 방식 — 아래 "환경 격리" 절의 셋 중 하나. 병렬이면 지금 정한다. **테스트가 DB 를 쓰면 1·2를 추천으로 제시한다**(아래).
④역할별 모델 — 아래 "역할별 모델" 절. 동시 수와 같은 자리에서 묻는다 — **폭 × 모델 = 사용량**이라 둘을 따로 정하면 한도를 넘긴 뒤에 안다.
조건부로 둘 더 — 저장소가 동기화 폴더 아래면 **슬롯 경로**("슬롯과 워크트리" 절), 오르카가 있으면 **에이전트 배치**(구현·검증·코디네이터 각각 claude/codex).
그리고 **오르카 유무를 확인해** 구동기를 ②(있음) 또는 ①-병렬(없음)로 적는다. 묻지 않고 단일로 적으면 이 절 전체가 "알아서 하라"가 된다.
### 역할별 모델 — 묻고, 모르면 추천한다
자식 세션은 플래그로 모델과 추론 강도를 받는다(`claude -p --model --effort`, `codex exec -m -c model_reasoning_effort=`, Agent 도구 `model`).
적지 않으면 세 역할이 **전부 사용자의 기본 모델**로 돌고 그게 무엇인지 어디에도 안 남는다. 코디네이터가 가벼운 모델이면 배정·병합 판단이 흔들려 슬롯 전부가 헛돌고,
구현 워커가 최상위 모델이면 호출 총량이 가장 큰 자리라 한도를 먼저 친다. 역할마다 다르게 두는 이유가 그것이다.
**정하지 않고 묻는다.** 모델 이름은 몇 달 단위로 바뀌고 사용자의 요금제·한도는 스킬이 모른다. 세 역할을 한 번에 보여 주고 넷 중 하나를 고르게 한다.
```
역할별 모델을 정합니다. 모르시면 "추천대로"를 고르세요.
코디네이터 — 배정·실패 분류·병합·질문 판단만. 라운드당 1회, 코드는 안 씀
구현 워커 — 가장 많이·길게 도는 세션
검증 워커 — 관찰·판정만. 구현과 다른 모델(또는 다른 CLI)이면 분리가 한 겹 더 강해집니다
A) 추천대로 (코디네이터 최상위 / 구현 보통 / 검증 보통)
B) 전부 최상위 — 품질 우선. 사용량이 폭 × 최상위 단가
C) 전부 보통 — 비용 우선. 코디네이터의 병합·분류 판단이 흔들릴 수 있음
D) 역할마다 직접 지정
```
**"추천대로"의 내용과 이유** — 사용자가 판단할 수 있게 이유를 같이 보여 준다.
| 역할 | 급 | 추론 강도 | 왜 |
|---|---|---|---|
| 코디네이터 | **최상위** | 높음 | 판단만 하고 호출이 적다. 여기서 틀리면 슬롯 전부가 헛돈다 |
| 구현 워커 | **보통** | 보통 | 호출 총량이 가장 크다. 태스크가 `priority: P0` 이고 `files` 3개 이상이거나, 3회 소진 뒤 사용자가 "이어서"로 답한 재배정이면 **한 급 올린다** |
| 검증 워커 | **보통** | 보통 | 관찰·판정. 가벼운 급은 브라우저 관찰 단계가 많아 `판정 불가` 가 잦을 것으로 본다 (가정) |
| 반영 토막(①-단일) | 보통 | 보통 | STATE.md·LOG.md 기록과 규칙 기반 분류 |
**급을 실제 이름으로 바꾸는 것은 세팅 시점에 한다.** 클로드 코드는 `claude --help` 의 `--model` 별칭(`opus`·`sonnet`·`haiku` 등), 코덱스는 설치 버전의 모델 문서를 보고
`최상위`·`보통`·`가벼움` 에 해당하는 이름을 8절 표에 적는다. **스킬 본문과 지시문에는 이름을 박지 않는다** — 은퇴한 모델 이름이 스크립트에 남으면 자식이 뜨지 않는다.
모델은 **8절 표 한 곳**에 있고 구동기 스크립트가 거기서 읽는다. 바꿀 때 한 곳만 고친다(`loop-update`).
`--fallback-model` 같은 자동 대체는 쓰지 않는다 — 급이 소리 없이 바뀌면 판정 기록이 어느 모델의 것인지 알 수 없다.
### DAG 는 반드시 만들고, `loop_setup/DAG.md` 에 스크립트로 그린다
원본이 못 박았다 — **"전체 phase(s)/task(s) 구조에서 DAG를 구현하여 모든 dependency를 확인"**.
재료는 이미 있다. 노드는 태스크, 간선은 `depends_on`. **손으로 그리지 않고 스크립트로 만든다.**
**그림은 `LOOP.md` 가 아니라 `loop_setup/DAG.md` 에 들어간다**(사용자 결정 2026-09-14 — 원본 구조에 생성물 하나를 더한다).
`LOOP.md` 는 모든 세션이 시작할 때 읽는 문서인데 태스크 50개짜리 머메이드는 10KB 를 넘어 그 문서에서 가장 큰 덩어리가 되고,
태스크가 바뀔 때마다 LLM 이 50개 노드를 손으로 다시 그리게 된다. 그래서 둘로 나눈다.
| 파일 | 무엇 | 누가 만드나 |
|---|---|---|
| `harness_setup/scripts/dag-render.*` | 태스크 frontmatter 만 읽어 아래 넷을 뽑는 **생성 스크립트**. 언어·확장자는 하네스 스크립트와 같은 기준(사용자 OS·프로젝트 런타임). **`--ready` 옵션**은 `DAG.md` 를 쓰지 않고, 지금의 태스크 `status` 로 진입 차수를 다시 세어 **지금 열 수 있는 후보**(`status: pending`, `depends_on` 이 전부 `completed`, `blocked` 와 그 후행 아님)를 phase·`files` 와 함께 출력한다 — 코디네이터가 배정마다 부른다. **모르는 옵션이면 아무것도 쓰지 않고 0 이 아닌 코드로 끝난다**(코디네이터가 `--help` 를 시험하다 `DAG.md` 를 다시 쓰지 않게) | 이 스킬이 세팅 때 쓰고 **실제로 한 번 돌려 본다**(`--ready` 도) |
| `loop_setup/DAG.md` | 스크립트의 출력. ①`mermaid` 그래프(phase 는 `subgraph`, 노드는 태스크 ID, 실선 `depends_on`, **점선 = `files` 겹침**, `blocked` 는 회색 점선 테두리) ②구간별 폭 표 ③순환·미존재 참조·`blocked` 후행 확인 결과 ④**의존 경로 없는 파일 겹침 쌍 목록** | 스크립트. **손으로 고치지 않는다** — 고쳐도 다음 실행에서 사라진다 |
| `LOOP.md` 8절 | 규칙, 자원 충돌 **처리** 표(스크립트가 뽑은 쌍에 LLM 이 처리를 붙인 것), 슬롯·격리 표, 그리고 "그림은 `DAG.md`" 한 줄 | 이 스킬 |
사용자가 `DAG.md` 로 실행 순서를 검토한다. **코디네이터는 `DAG.md` 의 폭 표로 배정하지 않는다** — 폭 표는 세팅 때의 추정(같은 phase 안의 깊이로 묶은 단계)이고,
실제로는 선행이 끝나는 대로 태스크가 열린다. 배정은 **지금의 `completed` 집합으로 진입 차수를 다시 세서** 한다 — `dag-render --ready` 의 후보에 `LOOP.md` 8절 처리 표와 `DAG.md` 의 점선을 대어 고른다.
실전(ERP, 2026-09)에서 배정 지시문이 폭 표를 판단 입력으로 적어 두었더니, 선행이 다 끝난 태스크가 슬롯이 빈 채 3시간 가까이 배정되지 않았다(그중 절반 남짓은 1.10.0 이전의 상주 코디네이터가 잠든 시간이고, 나머지는 깨어 있는 코디네이터가 그 태스크를 열지 않은 시간이다).
`PLAN.md` 3절의 phase 단위 그래프와 겹치지 않는다 — 그쪽은 phase 사이, 이쪽은 태스크 사이다.
태스크가 바뀌면 `loop-update` 가 **스크립트를 다시 돌린다.** 스크립트가 `harness_setup/scripts/` 에 있는 이유는 OS·런타임 기준이 거기 있고 `local-dev` 와 나란히 관리되기 때문이며,
`harness-update` 는 이 파일을 구성 요소 표와 무관한 것으로 알고 지우지 않는다.
그리고 **DAG 를 만들 때 확인할 것**을 적는다. 스크립트가 확인 결과를 `DAG.md` 에 내고, 걸린 것이 있으면 세팅 때 사람이 본다.
- **순환이 없는가** — 있으면 아무것도 실행 가능하지 않다. Layer 3으로 돌려보낸다
- **없는 태스크 ID를 가리키는 `depends_on` 이 있는가** — 영원히 열리지 않는 노드가 된다
- **`blocked` 노드와 그 후행은 제외한다** — 후행까지 빼지 않으면 열리지 않을 태스크를 실행 가능으로 센다
- **동시 실행 후보 = 진입 차수 0 이고 `blocked` 가 아닌 노드** — 진입 차수는 `completed` 인 선행을 뺀 **지금의** 그래프로 센다(`--ready`)
- **`files` 가 겹치는 두 태스크는 간선이 없어도 동시에 스케줄하지 않는다** — 의존 관계는 없지만 같은 파일을 고친다.
Layer 3이 `files` 를 태스크마다 적어 둔 덕에 이건 기계로 판정된다
**마지막 규칙이 DAG 를 충돌 예방 수단으로 만드는 부분이다.** 순수한 의존 간선만으로는 "동시에 실행해도 된다"까지
말할 수 없다 — 의존이 없다는 것은 순서가 자유롭다는 뜻이지 자원이 안 겹친다는 뜻이 아니다.
### 간선을 세웠으면 폭도 잰다
DAG 를 만들었으면 **동시에 열리는 노드가 최대 몇 개인지, 구간마다 몇 개인지**를 함께 낸다.
간선만 확인하고 넘어가면 "병렬로 돌리자"가 근거 없는 결정이 된다.
**폭이 실제 병렬도의 상한이기 때문이다.** 에이전트를 넷 띄워도 그 구간의 폭이 2면 둘은 논다.
그리고 폭은 대개 고르지 않다 — 앞쪽 phase 에서 넓어졌다가 뒤로 갈수록 좁아지고,
마지막 구간은 사실상 직렬인 경우가 흔하다. 첫 태스크는 거의 언제나 혼자 도는 병목이다.
그래서 `LOOP.md` 8절에 **구간별 폭과 권장 에이전트 수**를 표로 적는다(폭은 `dag-render` 가 `DAG.md` 에 낸 것을 옮겨 적고, 권장 수는 사용자 상한과 폭의 작은 쪽). 이 숫자가 없으면
"넷으로 돌리자"는 결정이 실제로 무엇을 얻는지 아무도 모르는 채로 내려지고,
그 결정에 딸려 오는 비용(환경 격리, 워크트리 관리, `STATE.md` 갱신 충돌)만 확실하게 발생한다.
세팅 때 묻는 "동시 에이전트 수 상한"은 이 표를 보여 주고 받는다. **이 표는 세팅 때의 추정이라 상한을 고르는 근거이지 배정의 근거가 아니다** — 배정은 `--ready` 다.
### 자원 충돌은 스케줄 전에 표로 확인한다
DAG 의 간선은 순서만 말한다. **간선이 없는 두 태스크가 동시에 돌아도 되는가**는 별도 검사이고, 그 결과가 없으면 코디네이터가 매번 다르게 판단한다.
그래서 배정 규칙과 별개로 **세팅 때 한 번 검사해 결과를 `LOOP.md` 8절에 표로 남긴다.** 검사는 셋이고 전부 frontmatter 로 기계 판정된다
(파일 겹침 쌍은 `dag-render` 가 뽑고, 나머지 둘은 같은 frontmatter 를 패턴으로 훑는다).
| 검사 | 무엇을 보나 | 걸리면 |
|---|---|---|
| **파일 겹침** | 의존 경로가 없는 두 태스크의 `files` 에 같은 경로가 있는가 | DAG 에 점선을 긋고 **동시 금지**. 겹침이 우연이 아니라 같은 모듈을 나눠 만든 것이면 `알림(plan-generator, …):` 으로 의존 추가를 요청한다 |
| **공유 런타임 자원** | `files` 에 마이그레이션·시드·스키마·환경 설정·포트 설정·CI 설정처럼 **여러 트리가 한 인스턴스를 나눠 쓰는 자원**이 있는가. 패턴은 `HARNESS.md` 2절 구성 요소 표에서 뽑는다(DB → 마이그레이션 폴더, 앱 → 포트·환경 파일) | 그 자원 이름으로 **자원 그룹**을 만들고 기본은 **그룹 직렬** — 같은 그룹끼리만 동시 금지, 다른 태스크와는 동시 가능. 격리 방식이 그 자원을 덮으면(슬롯별 DB 등) **격리가 덮음**으로 낮춘다 |
| **`files` 가 비었는데 코드를 쓰는 태스크** | `files: []` 인데 수용 기준이 코드·화면을 말한다 | 무엇을 건드릴지 모르므로 **단독**(그 라운드에 혼자). `알림(plan-generator, …):` 으로 `files` 보완을 요청한다 |
**처리 값은 셋이다** — `단독` / `그룹 직렬: <자원 그룹>` / `격리가 덮음`. 공유 자원을 전부 `단독` 으로 두면 마이그레이션 파일이 있는 태스크가 열 개인 phase 에서 폭이 1로 떨어진다.
마이그레이션 저널처럼 **파일 자체가 겹치는 것**은 그 그룹 안에서만 직렬이면 되고, 슬롯별 DB 가 있으면 런타임 충돌은 격리가 이미 덮는다.
`단독` 은 무엇을 건드릴지 모르는 태스크에만 남긴다.
표의 열은 `태스크(쌍) / 자원 / 종류 / 자원 그룹 / 처리` 다섯이다. 결과가 0건이면 "0건"이라고 적는다 — 비어 있는 것과 안 한 것은 다르다.
코디네이터는 배정할 때 `--ready` 후보에 이 표와 `DAG.md` 의 점선만 대어 본다 — "돌고 있는 행과 같은 그룹이면 대기". **검사를 배정 때마다 다시 하지 않는다** — 태스크가 바뀌면 `loop-update` 가 다시 검사한다.
### 병렬에서 태스크는 코디네이터가 나눠 준다
단일 에이전트의 구현 지시문은 "실행 가능한 태스크 하나"를 세션이 스스로 고르게 한다. **병렬에서는 그러면 안 된다** —
워커 셋이 같은 DAG 를 읽으면 같은 진입 차수 0 노드를 집고, 워크트리가 갈린 순간 A가 쓴 `status: in_progress` 는 B의 트리에서 보이지 않아
선점 정보로 쓸 수 없다. 그래서 **스케줄러는 코디네이터**이고, 구현 지시문에 `태스크: <ID>` 한 줄을 넣어 워커는 2절의 선택 규칙을 건너뛴다.
코디네이터가 `dag-render --ready` 의 후보(지금의 `completed` 집합으로 다시 센 진입 차수 0·`blocked` 아님) 중 현재 phase·자원 충돌 표에 안 걸림을 판정해 배정하고, 그 배정표(`STATE.md` 의 `Assignments` — ①-병렬과 ② 같다)가 병합 전까지 "무엇이 돌고 있는가"의 원본이다.
**태스크 문서 `status` 도 병렬에서는 코디네이터가 자기 트리에서만 쓴다** — 배정할 때 `in_progress`, 병합 뒤 `completed`, 정지 때 `failed`.
워커가 자기 트리의 태스크 문서를 고치면 `failed` 뒤 "이어서"나 건너뛰기 경로에서 코디네이터 트리의 값과 워커 브랜치의 값이 병합 충돌을 낸다.
워커 브랜치는 **코드만** 바꾼다. "어긋나면 태스크 문서가 이긴다"는 코디네이터 트리에서 그대로 성립한다.
병렬에서 달라지는 것을 적는다.
- **워커는 문서를 코디네이터 트리에서 읽는다** — `plan_setup/`(`STATE.md` 의 `Retry`·`Standing Decisions`·`Next Action`, 태스크 문서)과 `harness_setup/` 의 문서는
워커 트리의 것이 낡았을 수 있으므로 **코디네이터 트리 경로**(8절에 적는다)에서 읽는다. 코드·게이트 명령·`local-dev` 는 자기 트리에서 돌린다
- **워커는 0절의 쓰기 항목을 건너뛴다** — 불일치 맞추기(2번), 승인 만료 지우기(4번), `답:` 반영(5번), phase 를 열 때의 확인(7번)은 코디네이터의 일이다. 워커는 읽기만 한다.
차단 해소 절차도 코디네이터가 한다(3단계 "병렬에서는 코디네이터 자식이"와 같다)
- **구현 워커의 출력은 블록이다** — 판정 블록처럼 형식을 고정하고 마지막 줄이 `END` 다.
```
TASK: <ID>
STATUS: task_validation 대기 | task_validation 미활성 | e2e 대기 (격리 3) | failed: <사유> | 하네스 부족: <무엇이 없나>
RETRY: <게이트 / 흐름·검사 ID / 분류 / 무엇을 했나> (구현 세션 안의 사이클마다 한 줄. 없으면 "없음")
LOG: <LOG.md 에 남길 문장> (여러 줄 가능)
END
```
코디네이터가 `RETRY` 줄을 그 태스크의 `Retry History` 로, `LOG` 줄을 `LOG.md` 로 옮긴다. 이것이 없으면 구현 세션 안에서 쓴 재시도가 기록에 남지 않아
"두 태스크에 걸친 같은 실패"를 잡을 수 없다. **`END` 가 없으면 워커가 도중에 죽은 것** — 판정 파일과 같이 다룬다(`History` 에 `구현 중단`, 행 유지, 두 번째면 `질문:`)
- **`harness-update` 는 코디네이터만 자기 트리에서 부른다** — 두 슬롯이 각자 `HARNESS.md` 를 고치면 병합에서 충돌한다. 워커는 시작 전 점검(10단계)에서
부족을 발견하면 구현하지 않고 `STATUS: 하네스 부족` 으로 종료하고, 코디네이터가 ①이면 갱신 뒤 행을 `구현` 으로 되돌리고 ②③이면 `질문:` 이다.
완료 후 점검은 원래 반영 토막의 일이라 코디네이터가 한다
- **정지 행은 슬롯을 놓지 않는다** — 3회 소진·판정 불가로 `정지` 된 태스크의 미커밋 변경은 그 워커 트리에 있다. 사람의 답이 "이어서"면 같은 슬롯에서 `수정` 으로,
"건너뛰라"면 그 워커 트리에서 `git reset --hard coordinator && git clean -fd` 로 비운다(`files` 되돌리기의 병렬판)
- **완료는 한 번에 하나씩** — 원본의 "한 번에 하나의 Task만 완료 상태로 전환"은 병렬에서도 지킨다. 코디네이터는 도착한 판정을 순서대로 하나씩 병합·완료한다
- **그림에 없는 태스크는 배정하지 않는다** — `DAG.md` 에 없는 태스크 문서가 있으면(`plan-generator` 가 더한 뒤 `dag-render` 를 아직 안 돌린 것) `알림(loop-update, ~다음 phase 전): DAG 에 없는 태스크 <ID>` 를 남기고
그 태스크는 건너뛴다. 충돌 검사를 안 거친 태스크를 동시에 돌리지 않기 위해서다
- **②의 코디네이터는 상주하지 않는다** — 판단은 라운드마다 새로 여는 코디네이터 자식(①-병렬과 같은 지시문)이 하고, 기다리기는 구동 스크립트가 한다(7단계 "②의 모양").
그래서 phase 경계에서 코디네이터를 교체할 일이 없다 — 스크립트는 `Phase Overview` 전부 `completed` 까지 반복하며 phase 경계를 그냥 지나고,
판단 자식은 매 라운드 빈 컨텍스트로 시작하므로 앞 phase 의 대화가 새지 않는다(2026-09-23 변경. 그전에는 대화형 코디네이터가 phase 내내 살아 있다가 종료 직전에
다음 phase 코디네이터를 새 터미널로 띄웠는데, 상주 세션이 전체 토큰의 30%를 쓰고 터미널 생성이 반복해 시간 초과로 실패했다).
다음 라운드에 남길 말은 `STATE.md` `Next Action` 의 `알림:` 과 `LOG.md` 뿐이다. 루프가 멈추는 조건은 ①과 같이 둘 — `질문:` 이 있고 `답:` 이 없을 때(돌던 워커가 끝나기를 기다린 뒤), `Phase Overview` 가 전부 `completed` 일 때
- **구동 스크립트는 오르카 터미널에서 띄우고, 터미널 프로세스 밖으로 분리해 돈다** — 오르카 CLI 는 호출한 터미널을 Run 의 코디네이터로 묶는다(밖에서 띄우면 `no_active_sender_terminal`).
띄운 뒤에는 자신을 터미널 밖에서 다시 띄우고 `ORCA_*` 는 orca 호출에만 붙여, 터미널을 닫아도 그 바인딩으로 계속 돈다(4절 ②, 8절 "분리 실행 방식").
Run ID 는 `(임시)/loop-run.txt` 에 두고, 스크립트가 죽었다 다시 뜨면 `run-use --id <Run>` 으로 같은 Run 을 이어받는다 — 돌던 워커와 그 우편함을 잃지 않는다
- **②에서 워커의 에이전트는 워커마다 고른다** — `worker-start --agent claude|codex` 가 워커 단위라 구현은 클로드, 검증은 코덱스처럼 섞을 수 있다.
검증을 구현과 다른 에이전트로 두면 분리가 한 겹 더 강해진다. 8절 "에이전트·모델 배치"에 적는다. 두 CLI 와 플러그인이 둘 다 설치되어 있어야 한다.
모델도 같은 표에서 — 워커가 뜰 때 8절 표의 모델·추론 강도를 플래그로 받는다(①③도 같다. 11단계 "역할별 모델")
- **넓힌 `Last Verification`·`Retry` 에서 어느 블록을 읽는가** — 지시문의 `태스크: <ID>` 가 그 답이다. 코디네이터가 그 ID의 블록만 쓰고,
워커는 읽기만 한다. 워커가 단일 모드 지시문대로 `STATE.md`·`LOG.md` 를 쓰면 필자 규칙이 깨지므로 병렬 지시문에 "쓰지 말라"를 덧붙인다.
②의 워커는 표준 출력을 넘길 수 없으므로(대화형 에이전트다) **출력 블록·판정 블록을 `(임시)/done-<ID>.txt`·`verdict-<ID>.txt` 에 직접 쓰고**
`worker_done --report-path <그 파일>` 로 끝낸다. `worker_done` 본문은 세 문장 요약이라 블록을 담을 자리가 아니다. 그다음은 ①-병렬과 똑같은 반영 절차다
- **②의 워커는 `ask` 를 포그라운드로 돌린다** — 백그라운드로 걸고 턴을 끝내면 답이 와도 완료 알림을 놓친 채 잠들 수 있다
(실전 ERP: reply 1초 뒤 도착한 백그라운드 완료 알림이 20분 가까이 처리되지 않았고, 코디네이터가 `send` 로 건드린 뒤에야 풀렸다). Claude Code 는 600초를 넘긴 명령을 백그라운드로 옮기므로
Bash 도구 timeout 은 상한 600000, `ask --timeout-ms` 는 그보다 짧은 540000 으로 둔다. **시간 초과는 실패가 아니다** — `ask --resume <같은 메시지 ID>` 로 이어 간다
(오르카: "Timeout leaves the question pending; resume with the original message ID"). 안 적으면 워커는 대기 창 몇 번 만에 `failed` 로 보고하려 한다.
**연결이 끊겨 끝난 ask 도 실패가 아니다** — 오르카가 재시작되면 포그라운드 `ask` 가 "runtime closed the connection"·`runtime_unavailable` 로 끝나는데, 질문은 남아 있고 답도 그대로 받을 수 있다.
안 적으면 워커는 `--resume` 없이 턴을 끝내 잠들고(살아 있음·`activity: done`), 답이 와도 깨어나지 않는다(2026-10-01 실험). 30초 뒤 같은 ID 로 `ask --resume`, 세 번 연속 연결이 안 되면 `failed`
프리앰블은 `check --wait`·`ask` 안에서 막혀 있는 동안 heartbeat 를 건너뛰는 것을 허용하므로 포그라운드 대기가 heartbeat 규칙과 부딪히지 않는다. 지시문은 ② 워커 덧붙임(4절)에 있다
- **대기는 Run 단위다** — 구동 스크립트가 워커마다 기다리지 않고 `check --wait` 를 Run 에 걸고, `worker_done` 이 하나라도 오면 다음 라운드(판단 자식)로 넘어간다.
①-병렬처럼 전부 끝나기를 기다리지 않는 이유는 오르카가 워커마다 끝을 알려 주기 때문이다 — 빨리 끝난 슬롯이 놀지 않는다.
판정 파일은 태스크 ID 로 파생된 경로라 셋이 동시에 검증해도 겹치지 않는다.
**도는 dispatch 가 0개면 기다리지 않는다** — `check --wait` 를 깨우는 것은 `worker_done` 뿐인데 도는 워커가 없으면 그것이 오지 않는다. 실전(ERP phase-12)에서
격리 3 의 e2e 가 실패해 행이 `수정` 이 된 순간 도는 워커가 없었고, 다음 라운드는 영원히 열리지 않았다. 반복의 첫 줄로 돌아가 코디네이터 자식이 그 행을 다시 배정하게 하되,
그 한 바퀴에서도 새 dispatch·행 변화가 없으면 멈춘다 — 안 그러면 진전 없는 코디네이터 자식이 끝없이 불린다.
대기 한 번은 60초, 생존 확인은 빈 대기가 **15분** 이어질 때다(오르카 가이드의 "빈 대기 세 번"은 15분 대기 기준이다 — 60초 대기에 횟수로 세면 3분 만에 생존을 의심한다).
생존 판정이 `exited` 인데 `worker_done` 이 없으면 결과 파일이 없을 때만 END 없는 줄을 적는다 — 코디네이터가 중단으로 세어 두 번째에 `질문:` 을 올리게 하는 것이다.
결과 파일 없이 dispatch 파일만 지우면 같은 행이 새 Task 로 다시 떠 오르카의 연속 실패 차단도 우회하며 끝없이 돈다
### 슬롯과 워크트리 — `main` 은 비워 두고, 워크트리는 필요한 자리에만
원본의 다섯 규칙을 그대로 살린다. 동시에 일하는 에이전트는 독립 워크트리에서 일하고, 남의 워크트리를 고치지 않고,
공통 파일을 건드릴 때는 충돌 가능성을 확인하고, 태스크 완료 후 커밋하고, merge·rebase 는 orchestration 의 책임이다.
**이 문서에서 `main` 은 저장소의 기본 브랜치를 뜻한다.** 이름이 `master`·`develop`·`trunk` 면 그 이름이다. 세팅 때 한 번 알아내 8절에 실제 이름으로 적고,
**명령은 전부 그 값을 쓴다** — `git symbolic-ref --short refs/remotes/origin/HEAD`(원격 기본), 없으면 `git branch --show-current`, 둘이 다르거나 애매하면 사용자에게 묻는다.
이름을 박아 두면 `master` 저장소에서 워크트리 생성·병합·fast-forward 가 전부 "브랜치 없음"으로 실패한다.
**`main` 에서는 아무도 작업하지 않는다.** `main` 은 사용자의 자리다 — 완료·검증된 상태만 있고, 사용자가 진행 중에 따로 살펴보거나 손대는 곳이다.
루프의 자리는 코디네이터 트리 하나이고, 워크트리는 **동시에 코드를 쓰는 워커가 둘 이상일 때 둘째부터**만 더 만든다. 처음 설계는 첫 워커를 `main` 에 두었는데,
그 대가로 `main` 이 작업 중 파일로 더러워지고 사용자가 중간에 쓸 자리가 없었다(사용자 결정 2026-09-14). 오르카 화면에서 카드는 워크트리 단위라 코디네이터와 `worker-1` 은 한 카드에 보인다 — 그래도 된다.
**슬롯은 워커의 자리이고, 워커 이름은 워크트리 유무와 무관하게 `worker-N` 이다.** 태스크마다 워크트리를 만들지 않는다 —
슬롯 2 이상만 워크트리이고, 슬롯 수(= 동시 에이전트 수 상한)만큼 만들어 재사용한다.
| 트리 | 브랜치 | 누가 쓰나 |
|---|---|---|
| `main` | `main` | **사용자.** 루프는 완료된 태스크를 fast-forward 로 넣기만 한다 |
| **코디네이터 트리** | `coordinator` | 코디네이터(문서의 유일한 필자 — 7단계의 두 예외 밖, 배정, 병합, `harness-update`, phase 완료 묶음) **그리고 슬롯 1 의 워커 `worker-1`.** 폭이 1인 구간은 이것뿐이라 워크트리 0개. `files` 가 빈 비코드 태스크도 여기 |
| **`worker-N`** (N = 2 … 상한) | `worker-N` | 같은 라운드에 **동시에 코드를 쓰는** 둘째 이후 워커. 코디네이터 트리에서 분기 |
| (새로 없음) | 구현이 돈 자리 | **검증 워커** — 구현이 돈 트리의 앱을 봐야 하므로 같은 슬롯에서 **이어서**(동시가 아니라 순서대로) 돈다 |
코디네이터 트리는 세팅 때 `main` 에서 만든다. 워커 트리는 **폭이 2 이상인 구간에 들어갈 때** 코디네이터 트리에서 분기해 만들고, 태스크가 끝나면 지우지 않고 다음 배정에 재사용한다.
**재배정 직전에 코디네이터가 청소한다** — 워커 트리는 `git reset --hard coordinator && git clean -fd`(자기 `worker-N` 브랜치를 코디네이터 브랜치 끝으로 맞추고 untracked 제거.
`git checkout coordinator` 는 쓰지 않는다 — `coordinator` 브랜치는 코디네이터 트리에 체크아웃돼 있어 다른 워크트리에서는 git 이 거부한다. 병합이 끝난 뒤라 버려지는 커밋은 없다. `-x` 는 쓰지 않는다 — `node_modules`·빌드 캐시가 재사용의 이유다),
슬롯 1(코디네이터 트리)은 코디네이터가 자기 문서를 커밋한 뒤 `git clean -fd`. 코디네이터와 `worker-1` 이 같은 트리를 쓰므로 **코디네이터는 `git add -A` 를 쓰지 않고 문서 경로만 add 한다** — 워커의 작업 중 파일이 문서 커밋에 딸려 들어가면 안 된다.
브랜치만 되돌리면 앞 태스크가 만들다 만 파일이 남아 다음 워커의 lint 가 그것을 잡거나 자기 것으로 오인한다.
폭이 줄어 안 쓰는 워커 트리는 지워도 되고 두어도 된다 — 두면 환경 한 벌이 남아 있으므로 `LOOP.md` 에 어느 쪽인지 적는다.
②에서는 슬롯 1 의 워커가 `--worktree path:<코디네이터 트리>`, 슬롯 2 이상이 `--worktree path:<worker-N 워크트리>` 다 — `current` 는 쓰지 않는다. 스크립트의 위치가 아니라 **그 오르카 터미널이 속한 워크트리**로 풀려, 메인 트리의 터미널에서 띄우면 슬롯 1 워커가 `main` 에 뜬다(2026-10-01 실험). `main` 에서는 띄우지 않는다. 카드 이름은 `coordinator`·`worker-N` 으로 둔다(`orca worktree set --display-name`). **에이전트 목록에 보이는 워커 이름은 태스크를 만들 때 정한다** — `task-create --display-name "worker<N>_<태스크 ID>"`(검증 워커는 끝에 `_검증`) 뒤 `worker-start --task` 로 띄운다. `worker-start --spec` 한 번으로 띄우면 오르카가 `worker-task_<내부 ID>` 로 이름을 자동으로 붙여 어느 슬롯의 어느 태스크인지 알아볼 수 없다. (`worker-start --display-name` 은 새 워크트리를 만들 때의 워크트리 이름이라 기존 워크트리에서는 거부된다. 터미널 탭 제목은 에이전트가 덮어쓴다.)
**`main` 반영은 태스크 완료마다다.** 워커 트리에서 커밋 → 코디네이터 트리에 병합 → `main` 을 코디네이터 브랜치로 fast-forward. `main` 은 항상 "검증된 최신"이라
사용자가 중간에 봐도 반쯤 된 것이 없다. **사용자가 `main` 에 직접 커밋했으면** 코디네이터가 라운드 시작 때 `git merge <기본 브랜치>` 을 자기 트리에 먼저 한다 — 충돌이면 `질문:`.
fast-forward 가 안 되는 상태(사용자 커밋을 아직 안 받았다)면 병합 뒤 다시 시도하고, 그래도 안 되면 `질문:`.
**단일 모드(병렬 끔, ①-단일)도 `main` 에서 돌지 않는다** — 코디네이터 트리(`coordinator`) 하나에서 세 토막이 돌고, 반영 토막이 완료마다 `main` 을 fast-forward 한다.
**슬롯 경로는 세팅 때 정하고 8절 슬롯 표에 적는다.** 기본은 형제 폴더 `../<저장소>-coordinator`·`../<저장소>-worker-N` 이지만, **저장소가 동기화 폴더 아래이면 그 기본은 틀린다** —
OneDrive·Dropbox·iCloud·Google Drive 같은 경로에서는 형제 폴더도 동기화 대상이라 슬롯마다 `node_modules` 한 벌이 올라가고 파일 잠금이 난다.
저장소 경로에 그런 흔적이 있으면 **저장소 안 `.wt/coordinator`·`.wt/worker-N`**(`.gitignore` 에 `.wt/` 를 더한다. 저장소 안의 워크트리는 git 이 허용한다) 또는 **동기화 밖 경로**를 제안하고,
어느 쪽이든 사용자가 고른 값을 적는다. 이건 플러그인이 아니라 환경의 문제라 세팅 때 한 번 묻고 끝낸다.
슬롯마다 의존성이 한 벌씩 생기는 것은 감수한다 — 패키지 저장소 공유(pnpm store 등)는 하네스 쪽 결정이라 보고에만 올린다.
**병합은 코디네이터(orchestration)의 일이다.** PASS 뒤 워커 트리에서 커밋하고 코디네이터 트리에 병합한 뒤 `main` 을 fast-forward 한다. 충돌이 나면 코디네이터가 풀지 않고 `질문:` 이다 —
충돌은 자원 충돌 검사가 놓친 겹침이므로 사람이 보고 표를 고쳐야 한다.
여기에 앞에서 정한 것을 잇는다 — **각 슬롯은 자기 세션이므로 `task_validation` 분리는 여전히 필요하다.**
같은 슬롯의 같은 에이전트가 구현하고 판정하면 병렬로 돌든 아니든 이 게이트는 이름만 남는다.
### 병렬에서 `STATE.md` 가 꼬이는 두 가지와 그 해법
**`STATE.md` 는 하나인데 에이전트는 넷이다.** 여기서 서로 다른 문제가 둘 나오고, 해법도 다르다.
**(가) 칸이 모자란다.** `Current Task` 는 값 하나, `Last Verification` 은 "지금 상태 한 줄",
`Retry` 는 태스크 하나를 담게 되어 있다. 넷이 동시에 돌면 **넣을 자리가 없다.**
→ 그 절들을 **태스크별 블록이나 목록으로 넓힌다.** 넓힐 때 **기존 값을 새 형식으로 옮긴다** —
지우면 진행 중인 재시도 이력이 사라지고 그 태스크는 앞의 시도를 처음부터 반복한다.
**(나) 동시에 쓰다가 서로 덮어쓴다.** 칸을 나눠 줘도 **파일 쓰기는 통째로** 일어나므로,
"각자 자기 줄만 고친다"로는 여전히 잃는다. **잠금으로 풀지 않는다** — 잠근 채로 죽은 에이전트가 전체를 세운다.
→ **필자를 하나로 줄인다.**
| | 단일 에이전트 | 병렬 |
|---|---|---|
| 태스크 문서의 `status` | 에이전트가 쓴다 | **코디네이터만 자기 트리에서** — 배정 시 `in_progress`, 병합 뒤 `completed`, 정지 시 `failed`. 워커 브랜치는 코드만 바꾼다 |
| `STATE.md` | 에이전트가 쓴다 | **orchestration(코디네이터)만 쓴다** — 예외: 질문 답변 자식(②)은 `질문:` 한 줄, 구동 스크립트는 자식 사이에 `Assignments` 토막 값만(격리 3 의 e2e 결과, 7단계) |
| `LOG.md` | 에이전트가 쓴다 | **orchestration 만 쓴다** — 워커는 출력 블록(`LOG:` 줄·판정 블록)을 넘기고, 병합 순서대로 append 된다 |
| 하네스 문서 | 에이전트가 `harness-update` 로 | **코디네이터만 자기 트리에서** — 워커는 `하네스 부족` 으로 돌려보낸다 |
**이 분업이 맞는 이유가 있다.** 워크트리가 갈리면 같은 파일을 두 트리가 고치는 순간 병합 충돌이 생기고, 문서 파일은 코드와 달리 "어느 쪽이 맞는가"를
병합이 판단할 수 없다. 그래서 **문서는 전부 코디네이터 트리의 한 필자**, 슬롯은 코드만이다. 워커가 중간에 죽어도 배정표와 워커 트리의 미커밋 변경이 남아 있어
코디네이터가 같은 슬롯에서 이어 가게 할 수 있다 — **세션이 끊겨도 이어지게 만든 장치가 여기서 그대로 쓰인다.**
루프의 "어긋나면 태스크 문서가 이긴다"(8단계)는 코디네이터 트리 안에서 그대로 성립한다.
`STATE.md` 가 실시간이 아니게 되는 것은 감수한다. 실시간인데 틀린 요약보다 조금 늦고 맞는 요약이 낫고,
지금 무엇이 도는지가 급하면 **태스크 문서를 보면 된다** — 거기가 원본이다.
**`Current Task`·`Last Verification` 은 Layer 3이 만든 절이다.** 넓히는 것은 정당하지만
**Layer 3이 초기화한 형식을 바꾸는 일이므로 보고에 올린다** — 조용히 바꾸면 `plan-generator` 의 형식 규칙과
실제 파일이 갈라지고, 다음에 누가 맞는지 아무도 모른다.
### 환경 격리는 병렬이면 세팅 때 정한다
**DAG 와 자원 충돌 표가 덮는 것과 덮지 못하는 것이 다르다.**
- **덮는다** — 실행 순서(`depends_on`), 파일 충돌(`files` 겹침), 공유 자원 파일을 가진 태스크의 그룹 직렬
- **덮지 못한다** — 실행 중인 앱과 DB 같은 **공유 런타임 인스턴스.** 서로 간선도 파일 겹침도 없는 두 태스크가
각자의 e2e 로 같은 DB 에 데이터를 만들고 지운다. `local-dev` 의 초기화는 전역이다
포트는 겹치면 뜨지 않아 즉시 드러나지만 **DB는 조용히 오염된다.** 그래서 폭이 2 이상인 구간에 들어가기 전에 반드시 닫혀 있어야 한다.
Layer 4가 이 문제를 여기로 넘겼고, 하네스는 **포트·DB 이름을 환경변수에서 읽게 해 두는 것**까지 했다.
병렬이 기본이므로 **세팅 때 묻고 정한다.** 선택지 셋과 되돌리기 비용은 이렇다.
| 선택지 | 무엇 | 되돌리기 비용 |
|---|---|---|
| **1. 슬롯별 환경 통째 분리** | 슬롯 번호로 DB·포트·컨테이너 이름을 파생(`SLOT=2` → `PORT=3002`, `DB=app_slot2`). 완전 격리, 자원 N벌 | `local-dev` 가 슬롯 환경변수를 받아야 한다 — 안 받으면 `harness-update` |
| **2. DB 서버 하나 + 슬롯별 데이터베이스·전용 계정** | 가볍다. `local-dev` 에 권한 발급 단계가 붙는다 | 같은 `harness-update`. 앱 포트는 여전히 슬롯별 |
| **3. 격리 없이 앱을 쓰는 게이트만 직렬화** | 새로 만들 것이 거의 없다. 병렬 이득이 **구현·lint·unit 까지**이고, e2e 와 검증은 앱이 하나라 **슬롯 순서대로** 돈다(아래 "격리 3의 앱 전환") | 없음. 나중에 1·2로 올릴 수 있다 |
**추천을 붙여 묻는다 — 테스트가 DB 를 쓰면 1·2, 안 쓰면 3.** `HARNESS.md` 2절의 "공유 DB 를 쓰는 게이트" 줄을 본다(없으면 unit 정의서에 DB 에 붙는 통합 테스트가 있는지, e2e 가 DB 를 쓰는지 직접 확인하고 그 줄이 없다는 것을 보고한다).
그렇다면 격리 3의 "되돌리기 비용 없음"은 착시다 — 격리 3에서도 **lint·unit 은 병렬로 돈다**(앱 전환은 e2e 부터다). 그래서 두 슬롯의 통합 unit 이
한 DB 에 동시에 데이터를 만들고 지우고, 제약을 떨어뜨렸다 붙이는 테스트가 있으면 서로의 스키마까지 바꾼다.
실전(ERP, 2026-09)에서 격리 3을 "나중에 재검토"로 시작했다가 phase-11 까지 같은 흔들림이 반복됐고, 그 비용은 `local-dev` 에 슬롯 번호로 DB 이름을 파생하는 분기 하나보다 훨씬 컸다.
3을 고르는 것이 맞는 경우는 테스트가 공유 DB 를 쓰지 않을 때(순수 unit, e2e 만 DB)이고, 그때도 `option.description` 에 "통합 테스트가 생기면 1·2로 올린다"를 적는다.
사용자가 DB 를 쓰는데도 3을 고르면 받아들이되, **9절에 판단 시점을 확인 가능한 형태로 적는다**(아래 "미결의 `언제`").
정한 것을 `LOOP.md` 8절에 **슬롯별 환경변수 값 표**로 적는다(슬롯 / 포트 / DB 이름 / 환경 파일). 워커는 그 표의 값을 `local-dev` 에 넘긴다.
**`harness_setup/scripts/` 가 그 변수를 읽는지 확인한다** — 읽지 않으면 격리는 정했지만 적용되지 않은 상태이므로,
9절 미결에 **`격리 적용 대기 — harness-update 필요`** 로 적고 보고에 올린다. 첫 태스크는 거의 언제나 폭 1이라 그때까지는 문제가 없지만,
폭 2 구간에 들어가기 전에 닫혀 있지 않으면 코디네이터는 그 구간을 폭 1로 낮춰 돌고 `알림(harness-update, ~다음 phase 전): 격리 적용 대기` 를 남긴다.
**격리 3의 앱 전환과 e2e 토막.** 격리 3은 포트가 하나라 실행 중인 앱이 **어느 워커 트리의 코드인지**가 문제가 된다. 두 군데서 부딪힌다 —
검증 워커는 자기 슬롯의 앱을 봐야 하는데 다른 슬롯의 앱이 떠 있고, **구현 워커 안의 e2e 게이트**도 앱을 쓰므로 구현 워커 둘이 동시에 e2e 를 돌리면 같은 포트에서 충돌한다.
"e2e·검증은 차례로"라고만 적으면 e2e 가 구현 토막 안에 있다는 사실과 어긋난다. 그래서 격리 3에서는 구현 토막을 e2e 앞에서 자른다.
- **구현 워커는 `lint`·`unit` 까지만 돌리고 `STATUS: e2e 대기` 로 끝낸다.** e2e 를 돌리지 않는다
- 구동 스크립트(①·② 같다)가 `e2e 대기` 행을 **슬롯 순서대로 하나씩** 처리한다 — ①실행 중인 앱을 `local-dev -Down` ②그 워커 트리에서 `local-dev` ③**e2e 게이트 명령을 직접 돌린다**
(종료 코드라 판단이 없다 — 스크립트가 해도 원칙에 맞는다) ④통과면 행을 `검증` 으로 바꾸고 바로 검증 자식을 띄운다, 실패면 행을 `수정` 으로 되돌리고 e2e 출력을 `(임시)/e2e-<ID>.txt` 에 남긴다(분류는 수정 자식이 그 파일을 읽고 하고, `Retry History` 에는 수정 자식의 `RETRY:` 줄을 코디네이터가 옮긴다. 스크립트는 `Retry` 를 쓰지 않는다)
- 검증 행도 같은 전환을 거친다 — 앱이 그 슬롯의 것이 아니면 `-Down` 후 다시 띄운다. 전환은 라운드 안에서 슬롯 순서를 지키므로 e2e 뒤에 바로 검증이 오면 전환이 없다
- **②에서는 검증 워커가 비동기로 돈다** — ①-병렬처럼 자식이 끝나기를 기다리며 차례로 도는 것이 아니라 띄우고 나면 스크립트가 다음 줄로 넘어가므로, 같은 규칙을 명시해야 한다.
검증 워커는 **띄우기 직전에 그 슬롯으로 앱을 전환하고, 한 번에 하나만** 띄운다. 그 워커가 도는 동안 스크립트는 e2e·앱 전환을 하지 않고 **코디네이터 자식도 미룬다**(코디네이터의 `local-dev`·phase 완료 묶음이 앱을 바꾼다).
**`worker_done` 처리(결과 파일 확인·release·dispatch 파일 삭제)와 ack 는 미루지 않는다** — 오르카는 ack 안 된 배달 뒤의 메시지를 주지 않으므로, ack 를 미루면 그 검증 워커의 질문까지 막혀 교착한다.
자식을 미루는 동안 끝난 구현 워커의 결과 파일은 남아 있고, 행 띄우기는 결과 파일이 남은 행을 건너뛰므로 다시 뜨지 않는다
- 격리 1·2에서는 이 절이 없다. 슬롯마다 앱이 따로 떠 있어 구현 토막이 e2e 까지 한 번에 돈다
즉 격리 3의 병렬은 **정직하게 구현·lint·unit 까지**다. e2e 가 긴 프로젝트면 이득이 작으므로 세팅 때 그 사실을 `option.description` 에 적는다.
**사용자가 병렬을 끄면** 이 절은 미결로 남긴다. 종류는 `유예`, 기본값은 단일, 판단 시점은 병렬을 켜기 직전(`loop-update`). 선택지는 위 셋 그대로다.
**미결 번호는 앞 문서에서 이어진다.** PRD 8절·TRD 12절·아키텍처 overview.md 9절 중 마지막 OQ 다음 번호를 쓴다
(플랜과 하네스에는 미결 절이 없다). 이것이 이 스킬이 앞 문서를 여는 유일한 순간이고, 보는 것은 **그 세 표의 마지막 행 번호**뿐이다 —
1단계의 "두 곳만 읽는다"는 요구사항을 해석하지 않는다는 뜻이지 번호 하나를 확인하는 것까지 막지 않는다.
레이어마다 `OQ-001` 로 다시 시작하면 어느 문서의 미결인지 구별되지 않는다.
**미결의 `언제` 는 루프가 확인할 수 있는 시점으로 적는다** — `phase-03 진입 전`·`phase-02-task-05 완료 후`·`폭 2 구간 전` 처럼.
루프는 phase 를 열 때 이 열을 보고(0절 7번), 시점이 왔으면 `질문:` 으로 올린다. "e2e 가 길어지면" 같은 조건은 그 조건을 볼 시점을 함께 적는다.
실전에서 "phase-03 전에 재검토"로 적힌 격리 미결이 아무도 확인하지 않아 기본값으로 굳었다 — 적어 두기만 한 판단 시점은 없는 것과 같다.
---
## 12단계. 파일 쓰기
```
project_name/loop_setup/
├── LOOP.md
└── DAG.md ← dag-render 의 출력. 손으로 고치지 않는다
project_name/harness_setup/scripts/
├── dag-render.* ← 이 스킬이 만드는 생성 스크립트 (OS·런타임 기준은 local-dev 와 같다)
├── loop-drive.* ← 구동 스크립트. 4절의 ①/② 알고리즘을 옮긴 것. 판단하지 않는다
└── loop-watch.* ← ②의 밖의 감시. 4절 "밖의 감시" 를 옮긴 것. 읽기만 하고 알림만 한다
```
**원본이 그린 구조는 `LOOP.md` 하나이고, 규칙은 여전히 그 한 파일에만 있다.** `DAG.md` 는 규칙이 아니라 **생성물**이라 예외다(11단계, 사용자 결정 2026-09-14) —
매 세션이 읽는 문서에 10KB 그림을 넣지 않기 위해서다. 루프 규칙을 다른 파일로 쪼개지 않는다 — 흩어져 있으면 일부만 읽고 시작하게 된다.
`dag-render` 는 쓰고 나서 **실제로 돌려 `DAG.md` 를 만들어 본다.** 돌지 않는 스크립트는 그림이 없는 것과 같다. `--ready` 도 한 번 돌려 첫 태스크가 후보로 나오는지 본다.
**`loop-drive` 도 이 스킬이 쓰고 돌려 본다**(2026-09-23 추가). 구동기를 `LOOP.md` 의 의사 코드로만 두면 첫 실행 때 누군가 그 자리에서 스크립트를 지어내고,
②는 오르카 CLI 의 JSON(배달 ID·디스패치 ID·`--ack`)을 다뤄야 해서 지어낸 스크립트가 가장 잘 틀리는 곳이다. `dag-render` 와 같은 자리·같은 런타임 기준으로 둔다.
오르카 `--json` 응답은 **들여쓴 여러 줄 JSON 봉투** `{"id", "ok", "result"}` 다(거부는 `"ok": false` + `error.code` + 종료 코드 1, 연결이 끊긴 호출 실패는 JSON 이 없다). 출력 전체를 한 번에 읽는다 —
한 줄만 읽던 스크립트가 실전 첫 호출(`run-create` 성공)을 실패로 읽고 멈췄다(2026-10-01). 결과 모양도 지어내지 않는다 — `run-create` 는 `result.run.id`, `task-create` 는 `result.task.id`,
`worker-start` 는 `result.dispatchId`, `check` 는 `result.deliveryId`·`messages[]`·`replayed`(메시지의 `payload` 는 JSON **문자열**이고 `dispatchId` 는 그 안에 있다), `worker-list` 는 `result.workers[]`(각 `dispatchId`·`projection.liveness.verdict`).
Node 로 쓰면 **파일 지우기는 `fs.unlinkSync`, 폴더 복사·삭제는 직접 돈다** — Node 24·Windows 에서 경로에 한글 같은 비ASCII 가 있으면 `fs.rmSync` 는 오류 없이 아무것도 지우지 않고(단일 파일도), 재귀 `fs.cpSync` 는 프로세스를 메시지 없이 죽인다(2026-10-01 확인).
dispatch·결과 파일이 지워지지 않으면 끝난 워커를 도는 것으로 보고 루프가 영원히 기다린다.
- **①은 드라이런** — `STATE.md` 를 읽어 이번 라운드에 띄울 명령을 출력만 하고 끝나는 옵션을 두고 한 번 돌린다
- **②는 스모크 테스트** — 오르카 터미널에서 Run 을 하나 만들고 **가벼운 모델의 시험 워커 하나**에 "파일을 고치지 말고, ask 로 한 번 묻고, 받은 답을 결과 파일에 END 까지 쓰고 worker_done --report-path 로 끝내라"를 준다.
스크립트가 질문을 받아 `claude -p` 로 답하고, `worker_done` 과 결과 파일을 받고, `worker-release` 까지 돌면 통과다(실측 1분 안쪽).
스모크도 실제와 같이 **분리해서** 띄우고, 도는 동안 그 터미널을 닫아 본다 — 스크립트가 살아서 끝까지 가고(생존 파일이 계속 갱신된다), 밖의 감시가 떠서 알림 수단으로 시험 알림 하나를 보내면 통과다.
macOS·Linux 의 분리 방식(`setsid nohup`)은 아직 실측이 없다 — 여기서 확인한 결과를 8절에 적는다. 닫기를 살아남지 못하면 "터미널을 닫지 마라" 를 8절에 적는다
이것이 도는지 보지 않고 루프를 넘기면 첫 phase 에서 사람이 스크립트를 고치게 된다. 결과를 8절 "스모크 테스트"에 적는다
그리고 `plan_setup/STATE.md` 에 **6단계에서 정한 두 절(`# Retry`, `# Standing Decisions`)을 더한다.** 병렬(①-병렬)이면 **`# Assignments`** 도 더하고
`Current Task`·`Last Verification`·`Retry` 를 태스크별 블록으로 넓힌다(11단계 (가) — Layer 3 절을 바꾸므로 보고에 올린다).
기존 다섯 절은 건드리지 않고 `Last Verification` 뒤에 끼운다. `Retry` 는 비운 초기 상태로 두고,
**`Standing Decisions` 에는 이 스킬이 8단계에서 사용자에게 물어 받은 미활성 승인을 게이트별로 범위·기한과 함께 적어 넣는다.**
비워 두면 첫 태스크에서 모든 게이트가 미활성이라 구동기가 곧바로 멈춘다 — 세팅 때 한 번 묻는 이유가 그것이다.
**파일을 쓰고 `main` 에 커밋한 뒤 코디네이터 트리를 만든다** — `git worktree add <8절 경로> -B coordinator <8절 기본 브랜치>`. 워커 트리(둘째부터)는 코디네이터가 폭 2 구간에 들어갈 때 만든다.
커밋 전에 만들면 코디네이터 브랜치에 `LOOP.md` 가 없다 — 워크트리는 커밋된 것만 본다. 오르카가 있으면 카드 이름을 `coordinator` 로 둔다.
**`loop_setup/LOOP.md` 가 이미 있으면 덮어쓰기 전에 읽는다.** phase 가 진행되며 `loop-update` 가
규칙을 조정해 두었다면 그게 사라진다. 갱신이 목적이면 이 스킬이 아니라 `loop-update` 다.
### `CLAUDE.md` · `AGENTS.md` 갱신
`loop_setup/LOOP.md` 를 쓴 뒤 프로젝트 루트의 `CLAUDE.md` 와 `AGENTS.md` 를 **같은 내용으로** 갱신한다. 사용자가 직접 연 세션과 루프의 자식 세션은 이 스킬을 읽지 않고 시작하는데,
그때 자동으로 읽히는 파일은 Claude Code 는 `CLAUDE.md`, Codex 는 `AGENTS.md` 뿐이다. 그래서 **지금 어느 호스트에서 돌든 둘 다 쓴다** —
한쪽만 쓰면 다른 호스트의 세션이 규칙을 모른 채 시작한다. 내용은 **어느 세션에서든 반드시 지킬 것**만 두고 세부 규칙은 문서를 가리킨다 —
규칙을 옮겨 적으면 두 곳이 곧 어긋나고, 어긋나면 세션은 짧은 쪽을 믿는다.
- 둘 다 없으면 아래 뼈대로 만든다. 있으면 **`## 루프 (loop_setup/LOOP.md)` 절과 `현재 단계` 줄만** 고친다. 다른 절과 사용자가 손으로 적은 내용은 건드리지 않는다
- 두 파일이 서로 다르면 지금 호스트의 파일을 기준으로 다른 쪽을 맞춘다. 손으로 적은 줄은 양쪽에 남긴다
- 파일은 있는데 `## 문서 지도`·`## 공통 규칙` 이 없으면(예: `/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`
- 뒤 문서는 앞 문서를 근거로 한다. 충돌하면 앞 문서가 이기고, 앞 문서를 고치려면 그 레이어의 스킬을 다시 부른다
- 현재 단계: <마지막으로 끝낸 스킬> → 다음: <다음 스킬>
## 공통 규칙
- 앞 레이어의 **파일**만 입력이다. 대화 기억이나 추측으로 요구사항·기술 결정을 만들지 않는다
- ID(`FR-`·`NFR-`·`US-`·`OQ-`·`TD-`·`phase-NN-task-MM`)는 참조만 한다. 새 ID 는 그 레이어의 스킬만 만든다
- 사용자가 말하지 않은 값은 `(가정)` 으로 표시한다. 모호하면 추측하지 말고 묻는다
- 요청된 범위를 조용히 넓히거나 줄이지 않는다. 범위 판단은 보고한다
```
`## 루프 (loop_setup/LOOP.md)` 절에는 이것만 적는다. 자식 세션(`claude -p`·`codex exec`)은 스킬 없이 이 파일과 지시문만 들고 시작하므로,
"LOOP.md 0절부터"가 여기 없으면 자식은 규칙을 모른 채 태스크를 고른다. 규칙 본문은 옮기지 않는다 — LOOP.md 가 규칙이고 이 절은 문지기다.
```markdown
## 루프 (loop_setup/LOOP.md)
- 세션을 열면 `loop_setup/LOOP.md` 0절부터 따른다. 이 절은 요약이 아니다 — LOOP.md 가 규칙이다
- 완료는 네 게이트를 통과한 뒤다. 코드가 끝난 것은 완료가 아니다
- 실패→수정→재검증은 3회까지. 소진하면 STATE.md `Next Action` 에 `질문:` 을 적고 멈춘다
- 커밋은 네 게이트 통과 뒤. 시점과 횟수는 LOOP.md 5절. 검증 세션은 STATE.md·LOG.md 를 쓰지 않는다
- 병렬이면 자기 슬롯 워크트리 밖을 고치지 않고, 워커는 STATE.md·LOG.md·태스크 문서·하네스 문서를 쓰지 않는다(출력 블록으로 넘긴다). merge 는 코디네이터 몫
- 구동: <스크립트 / 오르카 / 서브에이전트> · 동시 <N> · 격리 <방식> · 모델 코디 <급> / 구현 <급> / 검증 <급> ← loop-update 가 갱신
```
---
## 13단계. 자체 점검
| 확인할 것 | 통과 조건 | 걸렸을 때 |
|---|---|---|
| 구조 | `loop_setup/LOOP.md` 가 원본 이름 그대로이고 규칙은 그 한 파일에만. `loop_setup/DAG.md` 는 생성물, `harness_setup/scripts/dag-render.*` 가 있고 **실제로 돌려 봤음**(`--ready` 포함) | 맞춘다. 규칙이 다른 파일에 있으면 옮긴다 |
| 게이트 참조 | 게이트를 **`verification` 값으로 여는 방법**이 적혀 있고, 명령을 베껴 적은 곳이 0건 | 고친다. 베낀 명령은 반드시 낡는다 |
| 게이트 형식 | 앞 셋은 `## 명령`+종료 코드, `task_validation` 은 **명령이 아니라 세션**이라고 구별됨 | 고친다. 넷째에 `## 명령` 절은 없다 |
| 활성 시점 출처 | 정의서에 없으면 `HARNESS.md` 표에서 읽는다고 적혀 있음 | 채운다. 넷째 게이트가 여기 걸린다 |
| 게이트 순서 | `lint → unit → e2e → task_validation` 고정이고 앞 실패 시 중단이 명시됨 | 채운다 |
| 미활성 | 활성 시점 전에는 **결과가 무엇이든 미활성**이고, 그 상태로 완료시킬지는 사람이 정한다 | 채운다. 0건 실행은 초록불, 도구 부재는 빨간불로 샌다 |
| 부분 활성 | **언제 그 값이 되는지**(활성 시점이 하나가 아닐 때)가 적혀 있음 | 채운다. 어휘만 나열하면 아무도 안 쓴다 |
| 상태 어휘 | 게이트·흐름 상태에 `활성`·`부분 활성`·`미활성`·`대기` 넷 밖의 이름이 0건. (태스크·phase 상태의 `pending`…`completed` 와 `Last Verification` 의 `task_validation 대기` 는 다른 어휘 공간이다) | 맞춘다 |
| 분류 우선 | 실패 시 **고치기 전에** 분류하는 단계가 수정 앞에 있음 | 순서를 바꾼다 |
| 분류 개수 | `흔들림` 과 `판정 불가` 가 갈래로 있음 | 채운다. 없으면 둘 다 `진짜 결함` 으로 찍혀 코드를 고치게 된다 |
| 환경 판정 | `local-dev` **종료 코드**로 환경 문제를 확정하고, 사람이 고칠 것과 재준비할 것이 갈림 | 채운다. 추측할 필요가 없는 자리다 |
| 판정 중단 | 검증자가 "환경이 이상해 판정을 멈췄다"를 `FAIL` 이 아니라 환경 문제로 받음 | 고친다 |
| 재시도 정의 | 3회가 **무엇을 세는지** 예시로 명확하고, `진짜 결함` 만 센다고 적혀 있음 | 채운다. 애매하면 안 지켜진다 |
| 재시도 불변 | 3회가 **원본 사양이 못 박은 값**이고 루프가 조정하지 않는다고 적혀 있음 | 고친다. 요청받으면 분류를 먼저 하라고 답한다 |
| 헛도는 재실행 | 코드가 아닌 갈래의 재실행이 태스크당 **분류마다 1회**로 묶여 있음 | 줄인다. 3회를 주면 아무것도 검증 않는 실행이 다섯 번 는다 |
| 재실행 범위 | **실패한 게이트부터** 돌고, 앞 게이트는 제품 코드가 바뀌었을 때만 다시 돈다 | 채운다. 흔들림 하나가 lint·unit 까지 끌고 가면 1회로 묶은 의미가 없다 |
| 태스크 간 반복 | 실패한 흐름·검사 ID가 `LOG.md` 에 남고, 두 태스크에 걸치면 `harness-update` 로 감 | 채운다. 없으면 같은 낭비가 태스크마다 반복된다 |
| 중단 시 인계 | 3회 소진 시 남길 것(상태·이력·물을 것)이 항목으로 있음 | 채운다 |
| 재시도 보존 | 재시도 횟수와 이력이 **세션을 넘겨 남는 자리**에 있음 | 정한다. 대화에만 있으면 세션이 죽을 때 사라진다 |
| 승인 만료 | **사이클 0단계에 만료 확인이 있고**, 승인마다 범위·기한이 있음(무기한 0건) | 채운다. 죽은 승인은 도는 게이트를 건너뛰게 한다 |
| 새 파일 | STATE.md·LOG.md 외에 새 상태 파일이 0건 (판정 파일·병렬의 `done-<ID>.txt`·격리 3의 `e2e-<ID>.txt` 는 `plan_setup/`·`loop_setup/` 밖의 일시 파일이고 반영 뒤 지운다. `loop_setup/DAG.md` 는 상태가 아니라 생성물이라 예외) | 지운다. 기록처가 셋이면 셋이 어긋난다 |
| 병렬 필자 | 병렬이면 태스크 문서·`STATE.md`·`LOG.md`·하네스 문서의 필자가 **코디네이터(자기 트리) 하나**이고(예외: ②의 질문 답변 자식은 `Next Action` 의 `질문:` 한 줄, 구동 스크립트는 자식 사이의 `Assignments` 토막 값 — 7단계), 워커는 문서를 코디네이터 트리에서 읽고 출력 블록(`END` 종결)으로 넘긴다고 적혀 있음 | 채운다. 슬롯이 문서를 고치면 병합 충돌이 되고, 출력 형식이 없으면 구현 세션 안의 재시도가 기록에서 사라진다 |
| 모델 배치 | 8절 표에 역할마다 실제 모델 이름·추론 강도가 있고 사용자가 고른 선택지가 적혀 있음. 지시문·스크립트 본문에 모델 이름이 0건 | 묻지 않았으면 지금 묻는다. 이름이 지시문에 박혀 있으면 표 참조로 바꾼다 |
| 분리 세션 | `task_validation` 앞에 세션 경계가 있고, **주지 않는 것** 목록이 있음 | 채운다. 없으면 분리가 말뿐이다 |
| 분리 불가 | 분리 수단이 없을 때 **통과로 기록하지 않는다**가 적혀 있음 | 채운다 |
| 구동기 | 세션을 여는 모드(①스크립트 단일·병렬 / ②오르카 스크립트 구동 / ③서브에이전트)가 **오르카 유무를 확인해** 정해져 있고, **구현·검증·반영 세 토막**의 고정 지시문·허용 목록·판정 파일 경로·구동기가 `STATE.md` 로 토막을 고르는 규칙이 있음. 병렬이면 **코디네이터 지시문**과 `done-<ID>.txt` 경로도, ②면 **질문 답변 지시문**과 워커가 결과 파일을 직접 쓰는 덧붙임도 | 채운다. 없으면 태스크마다 사람이 세션을 열거나 판정을 아무도 반영하지 않는다 |
| 응답 경로 | 사람의 답이 `Next Action` 의 `답:` 줄로 돌아오고 0절이 그것을 읽는다고 적혀 있음 | 채운다. 없으면 자식이 던진 질문을 받을 곳이 없어 구동기가 영원히 멈춘다 |
| phase 순서 | 현재 phase 가 `completed` 가 아니면 다음 phase 태스크를 열지 않는다는 선택 규칙과, 완료 묶음 실패 뒤 `답: 재판정`/`답: <ID> 다시` 진입점이 있고 **구동기가 `답: 재판정` 을 반영 토막으로 보낸다** | 채운다. 없으면 실패한 phase 위에 다음 phase 가 쌓이거나 정지도 진행도 아닌 루프가 된다 |
| 완료 후 소거 | 완료 시 `Last Verification` 을 `PASS` 로 덮어써 `대기`·`미활성` 이 남지 않는다고 적혀 있음 | 채운다. 남으면 구동기가 반영 토막을 다시 열어 구현하지 않은 태스크를 완료시킨다 |
| 잘린 판정 | 잘린 판정 파일이 `Retry History` 에 `검증 중단` 으로 남고 두 번째에 `질문:` 이 된다고 적혀 있음 | 채운다. 기록처가 없으면 "두 번 연속"을 셀 수 없어 검증↔반영이 무한 반복된다 |
| phase 갱신 값 | phase 완료 시 `Phase Overview`(값 셋)·`Current Phase`·phase 문서 `Status` 를 루프가 고친다고 적혀 있음 | 채운다. 초기화만 있고 갱신 주인이 없는 값이 된다 |
| 병렬 배정 | 병렬이면 코디네이터가 `태스크: <ID>` 로 배정하고 워커는 선택 규칙을 건너뛰며, 배정표가 `STATE.md` 의 `Assignments` 절에 있음. 코디네이터 지시문의 배정 단계가 `dag-render --ready` 후보에서 고르고 **폭 표를 배정 근거로 쓰지 않음** | 채운다. 워커 셋이 같은 태스크를 집는다. 폭 표로 배정하면 선행이 끝난 태스크가 "다음 단계"로 읽혀 슬롯이 논다 |
| 병렬 여부 | 세팅 때 병렬·에이전트 수 상한·격리 방식을 **물어 받은 답**이 문서에 있음(기본 병렬) | 묻는다. 묻지 않고 단일로 적은 것은 기본값 위반이다 |
| 실패 작업 트리 | 3회 소진 시 미커밋 변경을 그대로 두고, 사용자가 건너뛰라고 할 때만 `files` 를 되돌린다고 적혀 있음 | 채운다. 없으면 다음 세션이 더러운 트리 위에서 다른 태스크를 시작한다 |
| 기록 시점 | 시작·전이·완료 세 시점이 있고 쓰기 순서가 정해져 있음 | 채운다. 완료 시점만 있으면 중간에 죽은 세션이 복구되지 않는다 |
| 불일치 복구 | STATE.md 와 태스크 `status` 가 어긋났을 때의 처리가 있음 | 채운다 |
| 커밋 시점 | 네 게이트 통과 후이고, merge·rebase 는 루프 밖이라고 적혀 있음 | 맞춘다 |
| 선택 규칙 | `blocked`·`depends_on`·범위 확장의 판정 근거가 **frontmatter 필드**로 적혀 있음 | 채운다. 규칙만 있으면 매번 다르게 판정한다 |
| 막다른 곳 | 실행 가능한 태스크가 0개일 때의 처리가 있음 | 채운다 |
| 차단 해소 | `blocked` → `pending` 을 **루프가** 어떤 순서로 바꾸는지(앞 문서 확인 → `blocked_by` → `status` → 기록 → `harness-update`)가 있음 | 채운다. 주인이 없으면 풀린 미결 위의 태스크가 영원히 건너뛰어진다 |
| phase 판정 | 태스크 전부 완료 **와** phase 수용 기준 **와** phase 완료 묶음 통과 셋 다 | 채운다 |
| phase 완료 묶음 | 실패 시 phase 를 완료로 찍지 않되 재시도 3회는 쓰지 않는다는 것이 적혀 있음 | 채운다. "게이트 명령이 아니다"는 통과 불필요가 아니다 |
| 병렬 폭 | DAG 를 세웠으면 **구간별 폭과 권장 에이전트 수**가 함께 나옴 | 잰다. 폭이 실제 병렬도의 상한이다 |
| 하네스 갱신 | `harness-update` 를 부르는 조건이 시점별로 있음 | 채운다 |
| DAG | 순환·미존재 참조·`blocked` 후행·`files` 겹침 확인이 있음 | 채운다 |
| DAG 그림 | `loop_setup/DAG.md` 에 태스크 단위 `mermaid` 그래프·폭 표·확인 결과·겹침 쌍이 있고, **노드 수 = 태스크 문서 수**, `LOOP.md` 8절이 그것을 가리킴. 8절 안에 머메이드가 없음 | 스크립트를 돌린다. 8절에 그림이 있으면 뺀다 |
| 자원 충돌 표 | 파일 겹침·공유 런타임 자원·빈 `files` 세 검사의 결과 표가 있고(0건이면 "0건"), 처리 값이 `단독`/`그룹 직렬`/`격리가 덮음` 셋 중 하나이며 공유 자원의 기본이 `그룹 직렬` | 검사한다. 전부 `단독` 이면 폭이 1로 떨어진다 |
| 슬롯 경로 | 슬롯 워크트리 경로가 8절에 실제 값으로 있고, 저장소가 동기화 폴더 아래면 형제 폴더가 아님 | 정한다. 동기화 폴더의 형제 워크트리는 `node_modules` 를 동기화한다 |
| 격리 3 | 격리 3이면 구현 토막이 `unit` 에서 끊기고(`STATUS: e2e 대기`) 앱 전환·e2e 직접 실행·검증이 슬롯 순서로 있음. ②면 검증 워커가 **띄우기 직전 앱 전환·한 번에 하나**이고, 도는 동안 스크립트가 코디네이터 자식·e2e·앱 전환을 미루되 `worker_done` 처리·ack 는 미루지 않으며, 코디네이터 지시문에도 같은 금지가 있음 | 채운다. 없으면 구현 워커 둘의 e2e 가 한 포트에서 충돌하고, ②의 검증 워커가 다른 슬롯의 앱을 보고 판정한다 |
| ② 대기 | ② 의사코드에 "도는 dispatch 0 이면 기다리지 않고 첫 줄로(그 한 바퀴에 진전 없으면 멈춤)", 대기 60초·생존 확인 "빈 대기 15분", exited·`worker_done` 없음이면 결과 파일이 없을 때만 END 없는 줄을 적는 규칙이 있음 | 채운다. 없으면 e2e 실패·마지막 태스크 직후 스크립트가 영원히 기다리고, 매번 죽는 워커가 끝없이 다시 뜬다 |
| ② 답 읽기 | 질문 답변 자식의 출력을 읽을 때 코드 울타리·빈 줄을 건너뛰고 앞 공백·강조를 벗긴 첫 줄로 `답:`/`질문:` 을 가른다고 적혀 있음 | 채운다. 울타리로 감싼 답을 못 알아보면 reply 가 안 가고 워커는 답을 기다리며 선다 |
| ② 오르카 응답 | `loop-drive` 가 오르카 출력 전체(여러 줄 JSON)를 읽고 `payload` 문자열에서 `dispatchId` 를 꺼내며, reply 가 응답 JSON 없이 실패한 배달만 ack 하지 않고(`answer_conflict` 는 답함), 멈출 때 처리한 배달을 `check --ack <id> --peek` 로 ack 함. 구동 터미널은 구동 전용이고 자식의 표준 입력은 끊는다고 적혀 있음 | 고친다. 한 줄만 읽으면 첫 호출의 성공을 실패로 읽고, 호출 실패에도 ack 하면 질문이 답 없이 닫힌다 |
| ② 워커의 ask | ② 워커 덧붙임에 `ask` 포그라운드(Bash timeout 600000, `--timeout-ms 540000`)와 "시간 초과는 실패가 아니다 — `ask --resume <같은 ID>`", "연결이 끊겨 끝나도 30초 뒤 `ask --resume`, 세 번 연속이면 `failed`" 가 있음 | 채운다. 백그라운드 ask 의 완료 알림을 놓친 워커는 잠들고, 시간 초과를 실패로 읽은 워커는 `failed` 로 끝내며, 오르카 재시작으로 끊긴 ask 뒤 턴을 끝낸 워커는 답이 와도 깨지 않는다 |
| ② 슬롯·임시 폴더 | 슬롯 1 워커도 `--worktree path:<코디네이터 트리>` 로 띄우고(`current` 0건), 결과 파일을 다루는 자식(①-단일 반영·코디네이터·질문 답변)에 `--add-dir <임시>` 가 있음 | 고친다. `current` 는 구동 터미널의 워크트리(`main` 일 수 있다)로 풀리고, `--add-dir` 이 없으면 자식이 결과 파일을 못 지워 루프가 진전 없음으로 선다 |
| ② 정지 감시 | 4절 ② 블록에 띄우기(터미널 프로세스 밖으로 분리, 자기 환경에 `ORCA_*` 없음)·시작 확인(PID 파일, `--force`)·단계·생존 파일·종료 사유(앞머리 순서 "질문 대기" → "배정 없음" → "전체 완료" → 원인)·쌓인 배달 처리와 `exited` 정리 필요·자식 실패(한도 쉼·1/5/15분 3회)·시작 실패·깨우기(`send` 1회, `agentWait` 값이면 깨우지 않음)·답 없는 질문 15분·이미 답한 질문 건너뛰기(`answered.txt`·`inbox` 의 `thread_id`)가 있고, "밖의 감시" 블록(읽기 전용·알림만 — 죽음·정체·단계 상한·사람 프롬프트·질문:·종료 사유·답:)과 8절 분리 실행 방식·알림 수단·감시 기준값이 있으며, `loop-watch.*` 를 썼음 | 채운다. 없으면 잠든 워커·사람 프롬프트·죽은 스크립트를 사람이 우연히 볼 때까지 루프가 서 있고(실측: 활동 시간의 22%), 재생된 이미 답한 질문이 `질문:` 을 남겨 루프가 선다 |
| 끝난 행 재기동 | 행 띄우기(①-병렬·② 모두)가 dispatch 파일뿐 아니라 **결과 파일이 남은 행도 건너뜀** | 채운다. ②는 `worker_done` 때 dispatch 파일을 지우므로, 자식이 결과를 반영하기 전(미룸·반영 실패)에 끝난 워커가 다시 뜬다 |
| 구동 스크립트 | `harness_setup/scripts/loop-drive.*`(②면 `loop-watch.*` 도)가 있고 4절 알고리즘과 같으며, ①은 드라이런·②는 스모크 테스트(분리해서 띄운 스크립트로 시험 워커의 ask/reply·결과 파일·worker_done·release, 터미널을 닫아도 계속 돎, 시험 알림)를 **실제로 돌렸음**. ②에 코디네이터가 상주하는 자리·phase 마다 코디네이터를 교체하는 명령이 0건이고, ② 워커를 `task-create --display-name "worker<N>_<태스크 ID>"` → `worker-start --task` 로 띄우며, 스크립트를 오르카 터미널에서 띄워 터미널 프로세스 밖으로 분리해 돌리며(4절 ② 띄우기) 재시작 시 `run-use` 로 Run 을 이어받는다고 적혀 있음 | 쓰고 돌린다. 상주 코디네이터는 폴링마다 전체 컨텍스트를 다시 읽는다 |
| 반복 판정 | 실패 줄 형식(`- 실패: <ID> / <게이트> / <검사 ID> / <분류>`)이 고정돼 있고, 반복 판정이 **코디네이터 지시문(병렬)·반영 지시문(단일)의 한 단계**로 있으며, ②③·격리 부족이면 `질문:` 과 배정 중지, 처리 뒤 `- 반복 처리:` 줄이 있음 | 채운다. 설명 절에만 있는 트리거는 아무도 실행하지 않는다 |
| 알림 | `알림(<주체>, ~<만료>):` 형식이고, 루프가 스스로 할 수 있는 것은 알림으로 남기지 않으며, phase 를 열 때 만료된 알림을 `질문:` 으로 모은다(0절 7번) | 채운다. 주인·기한 없는 알림은 쌓이기만 한다 |
| 미결 시점 | 9절 미결의 `언제` 가 루프가 확인할 수 있는 시점이고, phase 를 열 때 확인한다(0절 7번) | 고친다. 확인하지 않는 판단 시점은 기본값이 굳는 것과 같다 |
| 격리 추천 | 테스트가 DB 를 쓰는지 확인했고, 쓰면 1·2를 추천으로 제시했음. 3을 골랐으면 그 판단 시점이 9절에 확인 가능한 형태로 있음 | 확인한다. 격리 3에서도 unit 은 병렬로 돈다 |
| 슬롯 규칙 | `main` 에서 작업하는 자리가 0건이고, 코디네이터 트리(`coordinator`)에 코디네이터와 `worker-1` 이 있고 둘째부터 `worker-N` 트리이며, 검증은 구현이 돈 자리에서 이어 돌고, 재배정 청소(`git clean -fd`)·기본 브랜치 fast-forward·재사용·정리 규칙이 있으며, **기본 브랜치 이름이 8절에 실제 값으로** 적혀 있음 | 채운다. 태스크마다 만들면 환경 N벌이 낭비되고, `main` 에서 돌리면 사용자 자리가 더러워진다 |
| 격리 | 병렬이면 격리 방식이 **확정**되고 슬롯별 환경변수 표가 있으며 `local-dev` 가 그 변수를 읽는지 확인했음. 단일이면 **기본값·선택지·판단 시점**과 함께 미결 | 정한다. 미결인 채로 폭 2에 들어가면 DB가 조용히 오염된다 |
| 병렬 기록 | 병렬에서 `STATE.md` 가 꼬이는 둘(**칸 부족 / 동시 쓰기**)이 구별되고 각각의 해법이 있음 | 채운다. 칸만 나누면 통째 쓰기에서 서로 덮어쓴다 |
| 필자 규칙 | 병렬의 `STATE.md`·`LOG.md`·태스크 문서·하네스 문서 필자가 **코디네이터(자기 트리) 하나**이고(예외: ②의 질문 답변 자식은 `Next Action` 의 `질문:` 한 줄, 구동 스크립트는 자식 사이의 `Assignments` 토막 값 — 7단계) 워커 브랜치는 코드만 바꾼다 | 채운다. 잠금으로 풀면 잠근 채 죽은 에이전트가 전체를 세우고, 슬롯이 문서를 고치면 병합 충돌이 된다 |
| 실제 값 | `local-dev` 경로·옵션, 첫 태스크 ID, 활성 시점이 **이 프로젝트의 실제 값** | 채운다. 일반형은 첫 세션이 추측하게 만든다 |
| 하네스 침범 | 게이트 내용·활성 시점·흐름 목록을 **고친** 곳이 0건 | 되돌린다. `harness-update` 의 몫이다 |
| 플랜 침범 | 태스크·수용 기준·`verification` 을 고친 곳이 0건 | 되돌리고 Layer 3으로 보고한다 |
| `CLAUDE.md` · `AGENTS.md` | 두 파일이 같고 `## 루프` 절이 갱신됐고 각 200줄 이하 | 자기 절만 고친다. 넘치면 규칙을 문서로 돌려보내고 가리키는 줄만 남긴다 |
**자체 점검 결과를 `LOOP.md` 안에 남기지 않는다.** 점검은 작업 기록이고 문서는 산출물이다.
문서에 남기면 규칙이 바뀌어도 점검표가 남아, 지킨 적 없는 것을 지켰다고 말하게 된다. **결과는 14단계 보고로 간다.**
**고칠 것과 보고할 것이 다르다.** 형식 문제(빠진 단계, 잘못된 경로, 일반형으로 남은 옵션)는 고친다.
**범위 판단은 보고한다** — 재시도 횟수를 바꾸는 것, 게이트를 건너뛰게 하는 것, 분리 세션을 포기하는 것은
사용자가 요청한 검증을 사용자 모르게 줄이는 일이다.
---
## 14단계. 작성 후 보고
이 순서로 보고한다.
1. **한 사이클이 어떻게 도는가** — 단계 목록을 짧게. **원본 17단계에서 바꾼 것과 그 이유**를 함께
2. **게이트를 어떻게 여는가** — `verification` → 문서 이름 → `## 명령`. 명령을 베껴 적지 않았다는 것
3. **실패 처리** — 다섯 분류와 각각의 재실행 횟수, 3회가 무엇을 세는지, 소진 시 무엇을 남기고 무엇을 묻는지.
**사용자가 3회를 늘려 달라고 했다면 유지한 사실과 그 이유, 그리고 대신 제시한 답(분류가 먼저)을 여기 적는다**
4. **재시도 값을 어디에 두었는가** — STATE.md 에 더한 절과 그 이유. **STATE.md 를 고쳤다는 사실은 반드시 알린다**
5. **`task_validation` 분리를 이 환경에서 어떻게 만드는가** — 분리가 보장되지 않으면 **그 사실을 명시한다**
그리고 **구동기 모드** — 어느 모드를 기본으로 잡았고, 허용 목록에 무엇을 넣었으며, 사람에게 넘어오는 지점이 어디인지
6. **`harness-update` 를 부르는 자리** — 시점별로 몇 개인지
7. **병렬 운용** — 병렬 여부·에이전트 수 상한·격리 방식·슬롯 경로·**역할별 모델**(고른 선택지와 실제 이름, 사용량 = 폭 × 모델이라는 것)로 받은 답, **`loop_setup/DAG.md` 를 검토해 달라는 요청**(실행 순서가 의도와 맞는지)과
`dag-render` 를 실제로 돌려 봤다는 사실, 자원 충돌 표(몇 건이고 무엇이 `단독`·`그룹 직렬` 인지), **구간별 폭과 권장 에이전트 수**, 코디네이터 트리·워커 트리가 몇 개 어디에 생기고 `main` 이 언제 반영되는지,
`local-dev` 가 슬롯 변수를 읽는지(안 읽으면 `harness-update` 필요), 격리 3이면 병렬 이득이 unit 까지라는 것과 테스트가 DB 를 쓰는지 확인한 결과. 단일로 갔다면 격리를 미결로 남긴 이유와 선택지.
**구동 스크립트** — `loop-drive` 를 어디에 썼고 드라이런·스모크 테스트가 어떻게 돌았는지, 사람이 여는 명령이 무엇인지(②는 오르카 터미널 안에서)
8. **플랜·하네스와 어긋난 것** — 없는 태스크를 가리키는 활성 시점, 순환 `depends_on`, 실행 불가능한 게이트 명령
9. 다음 단계 안내: "검토하시고, 괜찮으면 `phase-01-task-01` 부터 첫 사이클을 시작할까요?"
**원본 예시에서 바꾼 것을 반드시 보고한다.** 원본의 17단계·Task Selection·Completion·Agent/Worktree 절은
형식을 보여주는 예시이고 조정해도 되지만, **조정 자체가 문제가 아니라 조정을 알리지 않는 것이 문제다.**
사용자는 자기가 설계한 것이 어디서 어떻게 달라졌는지 알아야 다음 판단을 한다.
---
## 산출물 템플릿
### `loop_setup/LOOP.md`
````markdown
# 개발 루프: (제품 이름)
- **입력 문서**: plan_setup/(PLAN.md·phase·task·STATE.md·LOG.md), harness_setup/(HARNESS.md·quality_gates/)
- **전제**: 한 태스크에 구현 세션 하나. 검증과 반영은 별도 세션(4절의 세 토막). **기본은 병렬**(8절) — 폭이 1인 구간은 단일과 같다
- **읽는 순서**: 세션을 열면 이 문서 → `plan_setup/STATE.md`. **구동기가 준 지시문이 토막을 지정하면 그 지시문이 0절 6번보다 앞선다**
## 0. 세션을 열 때
1. 이 문서와 `STATE.md` 를 읽는다
2. `STATE.md` 의 `Task Status` 와 태스크 문서의 `status` 가 어긋나면 **태스크 문서 기준으로 맞추고** LOG.md 에 남긴다
(앞 세션이 기록 도중 끊긴 흔적이다)
3. `Retry` 절에 진행 중인 이력이 있으면 **거기서 이어받는다.** 앞의 시도를 반복하지 않는다
4. **`Standing Decisions` 의 만료를 확인한다** — 각 항목이 걸린 게이트의 **활성 시점 태스크**(정의서, 없으면 `HARNESS.md` 표)가
이미 `completed` 면 그 승인은 죽은 것이다. `HARNESS.md` 표의 `활성` 표시를 기다리지 않는다 — 표는 `harness-update` 가 올리는 요약이라
한 박자 늦을 수 있고, 활성 시점 태스크의 완료는 루프 자신이 판정한 사실이다. **지우고 `LOG.md` 에 한 줄 남긴다.**
남겨 두면 실제로 도는 게이트를 "승인받았으니 건너뛴다"로 읽게 된다 — 통과율은 오히려 좋아 보인다.
지우는 것은 봐주는 것을 **없애는** 방향이라 루프가 스스로 해도 된다.
**범위를 넓히는 것은 `loop-update` 다**
5. `Next Action` 에 **`답:`** 줄이 있으면 그것이 사람의 답이다. 반영하고 `질문:`·`답:` 두 줄을 지운 뒤 `LOG.md` 에 남긴다.
"반영"은 답의 종류대로다 — 미활성 승인이면 `Standing Decisions` 에 **범위·기한과 함께** 적고, 3회 소진의 답이면 3절의 "이어서/건너뛰라" 처리,
`답: 해소 OQ-0xx` 면 2절의 차단 해소 절차, phase 완료 묶음 실패의 답이면 `답: 재판정`(반영 토막으로 phase 판정 재실행) 또는 `답: <ID> 다시`(그 태스크 `in_progress`·`Cycle 0`),
수용 기준 재작성이면 `plan-generator` 10단계로 넘긴 뒤 종료. 답이 어느 종류에도 안 맞거나 **처리 도중 막히면**(예: 해소가 앞 문서에 없다)
`답:` 을 지우지 않고 `질문:` 을 새로 적어 종료한다 — 흔적 없이 끝나면 구동기가 다음 태스크로 넘어가 버린다.
`알림(<주체>, ~<만료>):` 줄은 답이 아니다 — 그 주체가 처리한 뒤 지운다(만료 확인은 7번)
6. 어느 토막인지 정한다(지시문이 지정했으면 그것이 우선) — **판정 파일(4절)이 있으면 반영 토막**. 판정 파일이 없고 `Last Verification` 이
`task_validation 미활성` 이면 **판정 없이 반영 토막**(승인 있으면 완료, 없으면 `질문:`). `task_validation 대기` 면 이 세션의 일은 없다(검증 세션이 열려야 한다).
셋 다 아니면 구현 토막이다.
반영 토막은 판정 파일을 열어 **마지막 줄이 `END` 인지** 먼저 본다 — 없으면 검증 세션이 도중에 죽은 것이니 파일을 지우고 `대기` 를 유지한 채 종료한다
(같은 태스크에서 두 번 연속이면 `질문:`). 그다음 `LOG.md` 의 마지막 줄이 **이미 그 판정을 담고 있는지** 본다 — 있으면 앞 반영 세션이 파일을 지우기 전에 죽은 것이므로
파일만 지우고 그다음 단계(완료·커밋 또는 수정)를 이어 간다. 판정을 두 번 기록하지 않는다
7. **phase 를 열 때**(앞 phase 가 `completed` 가 되고 다음 phase 의 첫 태스크를 집기 직전) 두 가지를 확인한다. 걸리면 그 phase 를 열지 않고 `질문:` 을 남긴다
- **만료된 `알림(…, ~<만료>):`** — 남아 있으면 모아서 `질문: 처리되지 않은 알림 n개 — 처리 / 다음 phase 까지 연장 / 폐기`
- **9절 미결 중 `언제` 가 지금인 것**(`phase-NN 진입 전`·`<태스크 ID> 완료 후` 처럼 적혀 있다) — `질문: <OQ-ID> 판단 시점 도달 — <선택지>`.
기본값을 그대로 이어 쓰려면 사람이 그렇게 답하면 된다. 아무도 확인하지 않는 판단 시점은 기본값이 영원히 굳는 것과 같다
(병렬이면 코디네이터 자식의 일이고 워커는 건너뛴다)
## 1. 한 사이클
한 태스크는 **구현 → 검증 → 반영** 세 토막이고 토막마다 세션이 다르다(4절). 표의 `토막` 열이 어느 세션의 일인지다.
| # | 토막 | 단계 | 근거·기록 |
|---|---|---|---|
| 1 | 구현 | `STATE.md` 로 현재 phase·태스크 확인 | |
| 2 | 구현 | 다음 실행 가능한 태스크 선택 | 2절 |
| 3 | 구현 | **시작 전 하네스 점검** | 7절 |
| 4 | 구현 | `status: in_progress`, `STATE.md`(그 phase 의 첫 태스크면 `Phase Overview` 도 `in_progress`), `LOG.md` 기록 | 5절 |
| 5 | 구현 | 태스크 구현 — `files` 와 `architecture` 안에서만 | 2절 |
| 6 | 구현 | `local-dev` 로 환경 준비 | 6절 |
| 7 | 구현 | `verification` 의 `lint`·`unit`·`e2e` 를 **순서대로** 실행. 실패하면 3절대로 분류 → 수정 → 재실행을 **이 세션 안에서** 한다(최대 3회). **격리 3의 병렬 워커는 `unit` 까지만** 돌리고 `e2e 대기` 로 끝낸다(8절) | 3절 |
| 8 | 구현 | `Last Verification` 에 `task_validation 대기` 를 적고 **세션을 닫는다**. `task_validation` 이 미활성이면 `task_validation 미활성` 을 적는다(검증 세션 없이 반영 토막이 12번부터). 3회 소진·환경 문제·판정 불가는 여기서도 3절대로 `failed`/`Retry`/`질문:` 을 남기고 닫는다 | 4·5절 |
| 9 | 검증 | **새 세션**이 `task_validation` 만 돌려 판정 블록을 출력하고 닫는다 | 4절 |
| 10 | 반영 | 판정 블록을 `STATE.md`·`LOG.md` 로 옮긴다 | 5절 |
| 11 | 반영 | 실패하면 **분류 → 수정 → 앞 게이트 재실행 (최대 3회)** → 다시 8번 | 3절 |
| 12 | 반영 | 전부 통과하면 완료 판정 (미활성 게이트 확인) | 5절 |
| 13 | 반영 | `status: completed` → `STATE.md` → `LOG.md` → 커밋 | 5절 |
| 14 | 반영 | **완료 후 하네스 점검** | 7절 |
| 15 | 반영 | phase 의 모든 태스크가 끝났으면 phase 판정 | 5절 |
| 16 | 반영 | 세션을 닫는다. 다음 태스크는 새 구현 세션 | 4절 |
## 2. 태스크 선택
| 규칙 | 판정 근거 |
|---|---|
| `blocked` 는 실행하지 않는다 | `status`·`blocked_by`. 사용자가 미결을 풀어야 열린다 |
| dependency 미완료는 실행하지 않는다 | `depends_on` 이 전부 `completed` |
| 한 번에 하나만 완료로 전환한다 | 진행 중인 것이 있으면 그것부터. `failed` 인 `Current Task` 는 `Next Action` 에 `답:` 이 있을 때만 이어받고, 없으면 새 태스크를 집지 않고 종료한다 |
| 범위를 확장하지 않는다 | `files`·`architecture` 밖은 건드리지 않는다 |
| 모호하면 추측하지 않고 멈춘다 | 관찰 불가능한 수용 기준은 Layer 3으로 |
| 현재 phase 가 끝나기 전에는 다음 phase 를 열지 않는다 | `Phase Overview` 에서 현재 phase 가 `completed` 가 아니면 다음 phase 태스크는 `depends_on` 이 차 있어도 집지 않는다 (완료 묶음 실패가 여기서 막힌다) |
**실행 가능한 태스크가 0개인데 미완료가 남았으면** 차단 대기이거나 `depends_on` 순환이다.
전자는 어느 미결을 풀면 무엇이 열리는지 보고하고, 후자는 Layer 3으로 돌려보낸다. `blocked_by: [폐기]` 는 차단이 아니다 — 보고에 넣지 않는다.
### 차단 해소
사용자는 `Next Action` 에 **`답: 해소 OQ-0xx`** 로 알린다(대화형이면 말로). 다음 **구현 토막**이 0절 5번에서 이것을 읽어
태스크 선택보다 먼저 아래를 한다(병렬이면 코디네이터 자식). 순서:
①앞 문서(PRD 8절·TRD 12절·overview 7절)에 해소가 반영됐는지 확인 — 안 됐으면 열지 않고, `답:` 을 지우지 말고
`질문: OQ-0xx 해소가 <문서 절>에 반영되지 않았다. 문서를 고친 뒤 다시 알려 달라` 를 적고 종료 →
②`PLAN.md` 5절에서 그 ID를 `blocked_by` 에 가진 태스크를 찾는다 →
③`blocked_by` 에서 그 ID를 지우고, 비면 `status: pending` →
④답이 수용 기준을 바꾸면 그 태스크는 `blocked` 그대로 두고 "수용 기준 재작성 필요(Layer 3)"를 `Next Action` 에 →
⑤`STATE.md`·`LOG.md`(`### <ID> Unblocked (OQ-0xx 해소).`) → ⑥`harness-update` (대기 해제, 7절).
## 3. 게이트
**태스크의 `verification` 값 = `quality_gates/` 아래 문서 이름.** 형식이 둘이다.
| 게이트 | 어디를 읽나 | 판정 |
|---|---|---|
| `lint`·`unit`·`e2e` | `## 명령` 의 명령 한 줄 | 돌려서 종료 코드로 |
| `task_validation` | `## 절차`·`## 판정 형식` | **세션을 열어** 판정 블록을 받아서 (4절) |
**`task_validation.md` 에는 `## 명령` 절이 없다.** 넷째는 명령이 아니라 세션이다.
**명령을 이 문서에 베껴 적지 않는다** — 하네스가 갱신되면 베낀 쪽이 낡는다.
순서는 `lint → unit → e2e → task_validation` 고정. **앞이 실패하면 뒤를 돌리지 않는다.**
### 활성 시점과 미활성
활성 시점은 게이트 정의서에서 읽고, **없으면 `HARNESS.md` 표에서 읽는다**(`task_validation` 이 그렇다).
그 태스크가 아직 `completed` 가 아니면 **결과가 무엇이든 `미활성` 이다** — 돌려도 되고 건너뛰어도 되지만
나온 값을 통과로도 실패로도 읽지 않는다. 0건 실행은 초록불이 되고, 도구가 없어서 나는 오류는 빨간불이 된다.
**활성 시점 이후에는 반드시 돌린다.** 그때 "검사 대상 없음"이면 미활성이 아니라 `harness-update` 다.
`부분 활성` 은 **활성 시점이 하나가 아닐 때**다(게이트 안의 일부가 더 늦게 열리는 경우).
`대기` 는 `blocked` 에 걸린 것이고 사람이 미결을 풀어야 풀린다. 어휘 넷 밖의 이름을 만들지 않는다.
미활성 게이트를 달고 완료시킬지는 **사람이 정한다** (`STATE.md` 의 `Standing Decisions` 에 승인이 있으면 그것을 따른다).
**승인은 그 게이트가 활성되면 만료된다** — 0절에서 지우고 넘어온 상태여야 한다.
승인이 없으면 묻고, 받은 답을 범위·기한과 함께 기록한다. **무기한 승인을 만들지 않는다.**
### 실패
**고치기 전에 분류한다. "재시도를 안 센다"가 "봐준다"는 뜻이 아니다** — 전부 통과가 아니고,
세지 않는 이유는 **고칠 곳이 코드가 아니기 때문**이다(재시도 3회는 코드 수정 예산이다).
| 분류 | 알아보는 법 | 무엇을 고치나 | 게이트 재실행 |
|---|---|---|---|
| 진짜 결함 | 수동으로 같은 순서를 밟아도 같은 결과 | **코드** | **최대 3회** |
| 낡은 스펙 | 화면·규약이 의도적으로 바뀌었다 | 흐름 정의 (`harness-update`) | 고쳐진 뒤 1회 |
| 환경 문제 | `local-dev` 가 **0이 아닌 코드로 끝났다** | 환경 (재준비 → `harness-update`) | 재준비 뒤 1회 |
| 흔들림 | 재시도로 살아난 통과(`flaky`)가 1건이라도 있다 | **그 테스트** (`harness-update`) | 고쳐진 뒤 1회 |
| 판정 불가 | 수용 기준이 관찰 가능하게 쓰이지 않았다 | 태스크 문서 (Layer 3) | 0회 — 즉시 중단 |
**뒤 넷에 3회를 주지 않는 이유**: 루프가 고칠 것이 없어서 다시 돌려도 같은 결과다.
흔들림은 도구가 이미 재시도해서 낸 판정이라 더욱 그렇다. **같은 분류가 그 태스크에서 두 번째로 나오면 멈춘다.**
그 1회는 예산이 아니라 **고친 것이 고쳐졌는지 보는 확인 실행**이다.
**재실행은 실패한 게이트부터 한다.** 앞 게이트는 **제품 코드가 바뀌었을 때만** 다시 돈다 —
흔들림·환경 문제·낡은 스펙을 고칠 때는 제품 코드가 안 바뀌므로 lint·unit 결과가 바뀔 수 없다.
`진짜 결함` 만 처음부터 전부 다시 돈다(코드를 고치면 다른 파일이 깨질 수 있다).
그리고 **실패마다 `LOG.md` 에 이 형식의 줄을 남긴다** — `- 실패: <태스크 ID> / <게이트> / <검사 ID> / <분류>`
(검사 ID = `FLOW-xx`·테스트 파일 경로·lint 규칙 이름). 형식이 고정돼야 판단 없이 셀 수 있다.
**반복 판정**(병렬은 코디네이터 지시문 2.5), ①-단일은 반영 지시문의 완료 후 점검): 같은 검사 ID 가 **서로 다른 태스크 ID 둘 이상**에서 나왔으면
(그 ID 의 마지막 `- 반복 처리:` 줄 뒤만 센다) 태스크 카운터와 무관하게 처리한다 — 그러지 않으면 태스크 61개에서 61번 낭비한다.
- 고칠 것이 **테스트 안정화·흐름 정의 보정(`harness-update` ①)** 이면 지금 한다
- **격리 3에서 DB·앱을 쓰는 테스트가 흔들리면** 격리 부족이다 — `loop-update`(격리 1·2)와 `harness-update`(슬롯 변수)가 필요하고 둘 다 ②라 무인으로 못 한다
- ②③이 필요하면 **`질문:` 을 남기고 새 배정을 멈춘다**(돌던 행은 끝까지). `알림:` 으로 미루지 않는다
- 처리했으면 `- 반복 처리: <검사 ID> → <무엇을 했나>` 를 남긴다
- **환경 문제는 `local-dev` 종료 코드로 확정된다** — 추측하지 않는다. (실제 코드 표: 아래 6절)
전제·환경변수 계열은 사람이 설치·설정할 일이라 **사용자에게**, 의존 서비스·스키마·앱 계열은 재준비 후 `harness-update`.
- **검증자가 "환경이 이상해 판정을 멈췄다"고 하면 `FAIL` 이 아니라 환경 문제다.** 안 뜬 화면을 보고
FAIL 을 적으면 멀쩡한 코드를 세 번 고치게 된다.
- **흔들림을 남겨 둔 채 진행하지 않는다.** 흔들리는 게이트는 꺼진 게이트다.
### 재시도 3회
```
실행 1회차 실패 → 수정1 → 재실행 → 실패 (사이클 1)
→ 수정2 → 재실행 → 실패 (사이클 2)
→ 수정3 → 재실행 → 실패 (사이클 3) → 중단·보고
```
**세는 것은 수정 횟수이고 `진짜 결함` 만 센다.** 태스크가 바뀌면 0으로 돌아간다.
코드가 아닌 갈래도 같은 태스크에서 합쳐 3회 쌓이면 멈춘다(분류만 바꿔 예산을 우회하는 길을 막는다).
태스크를 넘어 같은 흐름·검사 ID가 두 태스크에 걸쳐 실패하면 `harness-update` 다(7절).
**3회는 원본 사양이 못 박은 값이고 루프가 조정하지 않는다.** 늘려 달라는 요청을 받으면
상한을 지우는 대신 **분류가 먼저라는 점**을 답한다 — 세 번이 부족했던 이유는 대개 그중 몇 번이
환경 문제나 흔들림에 소모됐기 때문이고, 분류가 붙으면 세 번은 실제로 세 번이 된다.
**소진하면 일시 중단하고 사용자에게 보고한다.** 스스로 기준을 낮춰 풀지 않는다. 남길 것:
`status: failed` / `Last Verification` / `Retry` 이력 / `Next Action` 의 물을 것 / `LOG.md` 의 관찰.
**미커밋 변경은 그대로 둔다** — 커밋도 되돌리기도 하지 않는다.
사람의 답이 **"이어서"** 면 `status: in_progress`, `Cycle` 0, `History` 유지, LOG 한 줄.
**"건너뛰라"** 면 `files` 의 파일을 되돌리고 `failed` 그대로 둔 채 `Current Task` 를 비우고 `Retry` 를 초기화한다.
후행 태스크는 `depends_on` 규칙 때문에 자동으로 열리지 않는다 — 폐기·재작성은 `plan-generator` 10단계의 일이므로
`Next Action` 에 `알림(plan-generator, ~다음 phase 전): <ID> 건너뜀 — 폐기 또는 재작성 필요, 막힌 후행 n개` 를 남긴다(`알림:` 은 구동기를 멈추지 않는다. 만료는 0절 7번).
## 4. 세션 경계
한 태스크에 **구현 세션 + 검증 세션 + 반영 세션**(구동기 ①-단일). ①-병렬·②에서는 코디네이터 자식이, ③에서는 상위 세션이 반영을 겸한다.
병렬이면 **지시문의 `태스크: <ID>` 와 `Assignments` 의 그 행이 토막을 정한다** — 0절 6번의 `Last Verification` 판독은 단일에서만 쓴다.
**검증 세션에 주는 것**: 태스크 문서 경로 / 실행 중인 앱 주소 / `quality_gates/task_validation.md` / 판정 블록 출력 지시
/ (필요 시) 태스크 `architecture` 가 가리키는 설계 절
**주지 않는 것**: 구현 세션의 대화·요약·"무엇을 했는지" / 통과 보고 / 원인 추정 / 커밋 메시지·최근 변경 파일
— **주지 않는 목록이 이 게이트의 실질이다.**
이 프로젝트의 분리 수단: (실제 수단). **수단이 없으면 `미활성` 로 두고 사용자에게 알린다.**
같은 세션이 구현하고 판정한 것을 통과로 적으면 통과 기록만 남고 아무도 그 태스크를 다시 보지 않는다.
태스크가 끝나면 **세션을 닫는다.** 이어서 다음 태스크를 하지 않는다.
### 구동기 — 세션을 여는 방법
**모드**: (① 스크립트 구동 단일·병렬 / ② 오르카 스크립트 구동 / ③ 서브에이전트 중 이 프로젝트가 쓰는 것. 오르카 있음 → ②, 없음 → ①-병렬. 병렬을 껐으면 ①-단일)
**오르카 확인 결과**: (있음/없음 — 확인한 명령과 날짜)
한 태스크는 **구현 → 검증 → 반영** 세 토막이고 토막마다 세션이 다르다(1절 표). 지시문 셋은 고정이다.
```
구현 지시문 (토막: 구현):
loop_setup/LOOP.md 와 plan_setup/STATE.md 를 읽고 0절대로 시작하라. 실행 가능한 태스크 하나를 구현하고
lint·unit·e2e 까지 돌린 뒤, Last Verification 에 "task_validation 대기" 를 적고 종료하라.
task_validation 의 활성 시점 태스크가 아직 completed 가 아니면 "task_validation 미활성" 을 적어라.
task_validation 은 돌리지 마라. 앞 게이트가 실패하면 3절대로 분류하고 진짜 결함만 고쳐 재실행하라(최대 3회).
3회 소진·환경 문제·판정 불가면 3절대로 status·Retry·LOG 를 남기고 Next Action 에 "질문:" 으로 적고 종료하라.
미커밋 변경은 그대로 두라. harness-update 는 ①(상태 반영)만 하고 ②③이 필요하면 "질문:" 으로 남겨라.
검증 지시문 (토막: 검증):
다음 태스크의 수용 기준을 실행 중인 애플리케이션에서 관찰해 판정하라.
- 태스크 문서: (경로) - 절차와 판정 형식: harness_setup/quality_gates/task_validation.md
- 애플리케이션: (주소). 안 떠 있으면 (local-dev 명령) 으로 준비하고, 그래도 안 뜨면 RESULT 를 "환경 문제" 로 적어라
- 이 두 문서, 태스크의 architecture 가 가리키는 설계 절, 실행 중인 앱 외의 정보는 찾지 마라.
STATE.md·LOG.md 를 쓰지 말고 코드를 고치지 말고, 판정 블록만 마지막 END 줄까지 출력하라.
반영 지시문 (토막: 반영):
loop_setup/LOOP.md 와 plan_setup/STATE.md 를 읽고 0절대로 시작하라. (판정 파일 경로) 가 있으면 마지막 줄이 END 인지 보고,
아니면 잘린 파일이니 지우고 "task_validation 대기" 를 유지한 채 종료하라(두 번 연속이면 "질문:").
잘린 파일이면 Retry History 에 "검증 중단" 한 줄을 남겨라(두 줄째면 "질문:").
END 가 있으면 판정 블록을 STATE.md·LOG.md 로 옮기고 판정 파일을 지워라.
Last Verification 이 "task_validation 미활성" 이면 Standing Decisions 의 승인을 따라 완료 판정으로 가라.
PASS(또는 승인된 미활성)면 완료 판정·커밋·완료 후 점검·phase 판정을 하고, Last Verification 을 "<ID> / PASS / —" 로 덮어쓴 뒤 종료하라.
Next Action 에 "답: 재판정" 이 있으면 태스크 완료 없이 phase 판정만 다시 하고 두 줄을 지워라.
FAIL 이면 분류하고, 진짜 결함이면 고친 뒤 lint·unit·e2e 를 다시 돌리고 Last Verification 에 "task_validation 대기" 를 적고 종료하라.
RESULT 가 "환경 문제" 면 코드를 건드리지 말고 local-dev 로 재준비한 뒤 "task_validation 대기" 로 되돌려 종료하라(두 번째면 "질문:").
판정 불가·3회 소진·승인 없는 미활성·phase 완료 묶음 실패면 Next Action 에 "질문:" 으로 적고 종료하라.
실패는 LOG.md 에 "- 실패: <ID> / <게이트> / <검사 ID> / <분류>" 줄로 남기고, 완료 후 점검에서 3절의 반복 판정을 하라.
harness-update 는 ①(상태 반영)만 하고 ②③이 필요하면 "질문:" 으로 남겨라.
① 스크립트 구동 — 구동기는 판단하지 않는다. STATE.md 만 읽어 어느 토막을 열지 정한다.
아래 모든 `claude -p` 에 8절 "에이전트·모델 배치" 표의 값을 `--model <모델> --effort <강도>` 로 붙인다(코덱스는 `-m <모델> -c model_reasoning_effort="<강도>"`).
토막마다 다르다 — 코디네이터·반영은 그 행, 구현·수정은 구현 행, 검증은 검증 행. 이름은 여기 박지 않고 표에서 읽는다.
판정 파일·결과 파일을 지우는 반영·코디네이터 자식에는 --add-dir <임시> 도 붙인다(자식은 작업 폴더 밖을 쓰지 못한다 — 허용 목록에 삭제가 있어도. 코덱스는 그 버전의 같은 옵션):
반복 {
Next Action 에 "질문:" 이 있고 "답:" 이 없으면 → 멈추고 사람에게
Phase Overview 가 전부 completed 면 → 끝
Next Action 에 "답: 재판정" 이 있으면 → claude -p "<반영 지시문>" --allowedTools "<허용 목록>"
판정 파일이 있으면 → claude -p "<반영 지시문>" --allowedTools "<허용 목록>"
Last Verification 이 "task_validation 대기" 면 → claude -p "<검증 지시문>" --allowedTools "<검증 허용 목록>" > (판정 파일 경로)
Last Verification 이 "task_validation 미활성" 이면 → claude -p "<반영 지시문>" --allowedTools "<허용 목록>"
그 외 → claude -p "<구현 지시문>" --allowedTools "<허용 목록>"
}
② 오르카 스크립트 구동 (오르카 있음, 기본) — 구동 스크립트는 판단하지 않는다. 사람이 오르카 터미널에서 띄우고, 스크립트는 그 터미널의 프로세스 밖에서 돈다:
띄우기 (사람이 오르카 터미널에서 loop-drive 를 실행할 때 — 8절 "구동 시작 명령". 오르카 터미널 밖이면 ORCA_TERMINAL_HANDLE 이 없어 거부한다) {
아래 "시작 확인" 을 한다 — 이미 돌면 알리고 끝난다
지금 환경 전체를 (임시)/loop-env.json 에 적고, 터미널 프로세스 트리 밖에서 자신을 분리 모드로 다시 띄운다
(Windows: WMI Win32_Process.Create, 창 숨김 / macOS·Linux: setsid nohup). 띄운 PID 를 알리고 (임시)/loop-drive.log 를 따라 읽는다
(터미널을 닫거나 Ctrl+C 해도 스크립트는 계속 돈다. Start-Process·& 처럼 터미널 셸의 자손이면 터미널을 닫을 때 함께 죽는다)
}
분리 모드 시작 {
loop-env.json 에서 ORCA_* 를 뺀 값을 자기 환경으로 쓰고, ORCA_* 는 orca 호출에만 붙인다
(오르카는 닫힌 터미널의 ORCA_* 를 가진 프로세스를 정리한다. 붙여 넘긴 ORCA_* 로 닫힌 터미널의 바인딩을 계속 쓴다)
시작 확인 [감시 원칙 5 — 한 벌만 돈다]: (임시)/loop-drive.pid(8절 모양 — PID·그 프로세스의 시작 시각·실행 ID)가 있고 그 PID 가 살아 있고 시작 시각이 같으면 → 돈다
(그 PID 가 살아 있는데 시작 시각을 읽지 못하면 도는 것으로 본다)
PID 파일이 없는데 앞 실행의 단계·생존 파일이 있고 종료 사유(loop-exit.txt)가 없으면 → 도는지 알 수 없으니 돈다고 본다
돈다고 보면 어떤 파일도 쓰지 않고(기록 줄·래치도 쓰지 않는다 — 터미널 출력과 OS 알림만) "이미 실행 중이거나 비정상 종료 — 단계 <단계>, 마지막 갱신 <시각>. PC 재시작·강제 종료 뒤라면 확인하고 --force 로 시작하라" 를 알리고 끝난다
(흔적이 하나도 없거나 종료 사유가 있으면 시작한다. --force 는 이 확인만 건너뛴다)
앞 실행의 loop-exit.txt·loop-stage.txt·loop-alive.txt 를 보관 이름(뒤에 앞 실행 ID)으로 옮기고, 새 실행 ID·PID·시작 시각을 loop-drive.pid 에 쓴다
남은 감시(loop-watch.pid 의 PID)가 살아 있으면 끝내고, 아래 "밖의 감시" 를 같은 방식(트리 밖, ORCA_* 없이)으로 새로 띄워 그 PID·시작 시각을 loop-watch.pid 에 쓴다
여기부터 끝까지 최상위 try/finally — 어떻게 끝나든 종료 사유를 loop-exit.txt 에 남긴다(아래 "멈출 때")
단계 파일 loop-stage.txt(단계 이름·시작 시각, 쉬는 단계면 끝날 예정 시각 — 임시 이름에 쓰고 바꿔 단다)는 단계가 바뀔 때마다, 생존 파일 loop-alive.txt 는 1분마다(대기 계열 단계 동안에도) 쓴다
(이 둘의 쓰기가 실패해도 루프를 멈추지 않는다 — Windows 는 기존 파일 위로 바꿔 달기가 EPERM 으로 가끔 거부된다. 몇 번 다시 하고 안 되면 바로 쓴다)
(임시)/loop-run.txt 에 Run ID 가 있으면 orca orchestration run-use --id <Run>, 없으면 run-create --objective "<제품> 루프" 후 그 파일에 적는다
[감시 9] 쌓인 배달부터: 대기 없는 check 를 빈 결과가 나올 때까지 되풀이하며 아래 대기의 처리와 같이 처리하고 다음 check 에 --ack 한다
그다음 남은 dispatch-<ID>.txt 마다 worker-list --json 의 projection.liveness.verdict 가 exited(또는 worker-show 의 observation.status 가 exited)면
→ 멈춤 "정리 필요: <ID>" (알림: "확인한 뒤 dispatch 파일을 지우고 다시 시작하면 그 행이 다시 뜬다 — 결과 파일이 남아 있으면 먼저 반영된다")
unverifiable·missing 은 증거가 아니다 — 알림만 하고 아직 돈다고 본다(오르카 재시작 뒤 옛 dispatch 가 그렇게 읽힌다)
}
반복 {
Next Action 에 "질문:" 이 있고 "답:" 이 없으면 → 새로 띄우지 않는다(코디네이터 자식도 부르지 않고 곧장 아래 check 로). 돌고 있는 워커가 다 끝나면 멈춘다
Phase Overview 가 전부 completed 면 → 멈춘다
격리 3 이고 검증 행에 dispatch-<ID>.txt 가 있으면(검증 워커가 돈다) → 코디네이터 자식·e2e·앱 전환을 하지 않고 곧장 아래 check 로
(앱이 그 검증 워커의 것이어야 한다. worker_done 처리와 ack 는 미루지 않는다 — ack 를 미루면 뒤 메시지가 막힌다)
claude -p "<코디네이터 지시문>" --allowedTools "<코디네이터 허용 목록>" --add-dir <임시> ← ①-병렬과 같다
(--add-dir 없이는 자식이 작업 폴더 밖의 결과 파일을 지우지 못한다 — 허용 목록에 삭제가 있어도. 그러면 결과 파일이 남은 행을 끝난 워커로 건너뛰어 진전 없음으로 선다)
[감시 6] 0 이 아닌 코드로 끝나면 → 아래 "자식 실패"
[감시 7 가] 0 으로 끝났는데 새 질문:·배정 없음:·새 행이 없고, (dag-render 실행 명령) --ready 의 후보가 있고 빈 슬롯이 있으면 → 알림 "배정 확인 요청: <후보>"
Assignments 가 비어 있고 Next Action 에 "배정 없음:" 이 있고 돌고 있는 워커가 없으면 → 멈춘다
Assignments 행 중 dispatch-<ID>.txt 도 결과 파일(done-<ID>.txt / verdict-<ID>.txt)도 없는 행마다
(결과 파일이 남은 행은 끝난 워커다 — worker_done 때 dispatch 파일을 지웠으니 이것까지 안 보면 다시 띄운다. 다음 코디네이터 자식이 반영한다):
토막 = 구현 | 수정 | 검증(격리 1·2) → orca orchestration task-create --spec "worker-N: <지시문> 태스크: <ID> <② 워커 덧붙임>"
--display-name "worker<N>_<ID>"(검증이면 "worker<N>_<ID>_검증") --json → 받은 task id 로
orca orchestration worker-start --task <task id> --worktree <슬롯>
--agent <8절 CLI> --model <8절 모델> [--effort <8절 강도>] --json
(슬롯 1 은 path:<코디네이터 트리>, 2 이상은 path:<worker-N 워크트리>. current 는 쓰지 않는다 — 스크립트 위치가 아니라
그 오르카 터미널이 속한 워크트리로 풀려, 메인 트리 터미널에서 띄우면 워커가 main 에 뜬다. main 에서는 띄우지 않는다) → 받은 dispatchId 를 dispatch-<ID>.txt 에
[감시 5] worker-start 가 dispatch 없이 거부되면(ok:false — selector_not_found·task_not_startable 등) → 멈춤 "시작 실패: <ID> — <코드>"
dispatch 는 생겼는데 state 가 ready 가 아니면: failed → 영수증의 recovery 대로 --retry-of <그 dispatch> 로 한 번 더(worktree·agent·model 을 다시 적는다)
outcome_unknown → worker-show 의 worker.state 가 ready·active 이거나 projection.stage.detail 이 input_accepted 면 그대로 둔다. 아니면 1~2분 뒤 한 번 더 보고,
그래도 같으면 recovery·nextSteps(없으면 worker-stop → 멈춤 확인)를 따라 --retry-of 한 번. 재시도도 실패하면 → 멈춤 "시작 실패: <ID>"
(실패한 시작을 그냥 두면 그 행이 띄울 행으로 남거나 dispatch 파일만 남아 헛바퀴·영원한 대기가 된다)
토막 = e2e 대기 | 검증(격리 3) → 아래 차례 처리로 미룬다
격리 3 이면 미룬 행을 슬롯 순서대로 하나씩:
앱 전환 (local-dev -Down → 그 워커 트리에서 local-dev)
e2e 대기 행 → e2e 게이트 명령을 직접 실행. 종료 코드 0 이면 행을 검증으로 바꾸고 그 검증 워커를 띄운다,
아니면 행을 수정으로 되돌리고 출력을 (임시)/e2e-<ID>.txt 에 남기고 다음 행으로
검증 행 → 검증 워커를 띄운다 (task-create·worker-start 는 위와 같다)
검증 워커를 띄웠으면 여기서 멈춘다 — 검증 워커는 한 번에 하나다. 남은 행은 그 워커가 끝난 뒤의 라운드에서
도는 dispatch 가 0개면(dispatch-<ID>.txt 가 하나도 없으면) → 기다리지 않고 반복의 첫 줄로 간다 [감시 7 나]
(기다리면 깨울 worker_done 이 없어 영원히 선다 — 격리 3 의 e2e 실패로 행이 수정이 된 직후, 마지막 태스크가 끝난 직후)
단 첫 줄로 간 그 한 바퀴에서도 새 dispatch 도 Assignments 행 변화도 없으면 → 멈춘다 "진전 없음: <그 바퀴의 마지막 단계>"
(코디네이터 자식을 진전 없이 거듭 부르지 않는다. 한도로 쉰 바퀴는 세지 않는다)
orca orchestration check [--ack <앞 배달 ID>] --wait --types "worker_done,escalation,question" --timeout-ms <8절 값: 60000> --json
question · escalation → [감시 결함 3] 먼저 이미 답이 있는지 본다 — 그 메시지 ID 가 (임시)/answered.txt 에 있거나, orca orchestration inbox --limit 200 --json 을
ORCA_* 없이 불러 thread_id 가 그 ID 인 다른 메시지(답 — subject "Re: Question")가 있으면 → 자식도 reply 도 없이 답한 것으로 보고 넘긴다
(재생된 배달의 이미 답한 질문이다 — 사람이 직접 답했을 수도 있다. 자식을 부르면 dispatch 를 모르는 자식이 Next Action 에 질문: 을 적어 루프가 선다)
claude -p "<질문 답변 지시문>" --allowedTools "<질문 답변 허용 목록>" --add-dir <임시>
첫 줄(코드 울타리 ``` 줄과 빈 줄은 건너뛰고, 앞 공백과 ** 같은 강조 표시는 벗긴다)이 "답: …" 이면 orca orchestration reply --id <메시지> --body "<답>"
(claude -p 는 출력을 코드 울타리로 감싸거나 **답:** 처럼 강조하기도 한다 — 글자 그대로의 첫 줄만 보면 답을 놓쳐 reply 를 보내지 않는다)
"질문:" 이면 reply --body "사람 판단 대기 — 지금 상태 그대로 STATUS: failed: 사람 판단 대기 블록을 결과 파일에 쓰고 끝내라"
[감시 6] 첫 줄이 둘 다 아니거나 자식이 0 이 아닌 코드로 끝나면 → 아래 "자식 실패". 답할 때까지 이 배달은 ack 하지 않는다
(같은 배달의 worker_done 은 아래대로 바로 처리한다 — 미루면 G12 로 뒤 메시지가 막힌다)
reply 결과: 성공 또는 answer_conflict(이미 답한 질문 — 재생된 배달) = 답함 → 메시지 ID 를 answered.txt 에 적는다.
그 밖의 오류 코드(대상 dispatch 가 끝남 등) = 답할 수 없는 질문 — 넘기고 알린다.
응답 JSON 이 없는 호출 실패(연결 끊김)는 [감시 6] 처럼 1분·5분·15분 뒤 같은 답으로 reply 만 다시 한다(자식은 다시 부르지 않는다 — 단계 "재시도 대기").
그래도 실패하면 그 배달을 ack 하지 않은 채 멈춘다 "오르카 연결 없음: reply <메시지>"(다음 시작 때 [감시 9] 가 다시 처리한다)
(호출 실패에도 ack 하면 그 질문은 답 없이 닫히고 워커는 ask 에서 기다리다 끝난다. 간격 없이 다음 check 에 맡기면 재생된 배달이 곧바로 다시 와 답변 자식을 쉬지 않고 거듭 부른다)
worker_done → 결과 파일(done-<ID>.txt / verdict-<ID>.txt)이 없으면 메시지 본문을 그 파일에 적는다(END 가 없으니 코디네이터가 중단으로 처리)
→ worker-release --dispatch <ID> → dispatch-<ID>.txt 삭제 → 다음 라운드로 (나머지 워커는 계속 돈다)
(dispatch 는 메시지 payload — JSON 문자열 — 의 dispatchId 로 찾는다. 오르카 응답은 들여쓴 여러 줄 JSON 이다 — 한 줄만 읽으면 성공을 실패로 읽는다)
매 대기 뒤 → [감시 3] 아래 "깨우기". [감시 10] 받은 질문에 reply 도 질문: 기록도 없이 15분이면 → 알림(재시도 대기·한도 대기 중에도 1분마다 본다).
[감시 5] 띄운 지 5분 안에 전사(worker-read --source transcript)에 첫 메시지가 없는 워커 → 알림. 감시의 생존 파일이 없거나 3분 넘게 정체면 감시를 다시 띄운다
빈 대기가 15분 이어지면 → orca orchestration worker-list --json 의 생존 판정. exited 인데 worker_done 이 없는 dispatch 마다:
그 행의 결과 파일이 없을 때만 "exited, worker_done 없음" 을 END 없이 결과 파일에 적고(코디네이터가 중단으로 센다 — 두 번째면 질문:.
워커가 END 까지 쓰고 죽었으면 그 결과를 덮어쓰지 않는다) → dispatch 파일을 지우고 다음 라운드로.
결과 파일 없이 지우기만 하면 매번 같은 자리에서 죽는 워커가 새 Task 로 끝없이 다시 뜬다
배달은 그 안의 메시지를 다 처리한 뒤 다음 check 의 --ack 로 ack 한다(ack 전까지는 받아 간 배달도 같은 배달 ID 로 다시 온다)
worker_done 이 없으면(빈 대기·질문만 받은 대기) 라운드가 아니다 — 반복의 첫 줄로 가지 않고 check 를 이어 간다. check 가 응답 JSON 없이 실패하면(연결 끊김) 빈 대기로 본다
두 확인 사이가 예정의 두 배를 넘으면(절전 복귀) 그 회차는 판정하지 않고 타이머만 다시 잡는다
}
자식 실패 [감시 6] (코디네이터 자식·질문 답변 자식) {
출력에 사용량 한도 문구("usage limit"·"limit reached"·"resets <시각>" 등)가 있으면 한도다 → 재설정 시각까지(모르면 15분) 새 자식·워커를 띄우지 않고 check 도 부르지 않으며
깨우기도 멈추고 알림 1회(단계 "한도 대기"). 쉰 뒤 실패한 그 자식부터 다시 돌린다
그 밖이면(한도로 가를 수 없으면 이쪽 — 끝없이 쉬지 않는다) 1분·5분·15분 뒤 최대 3회 다시 돌린다(단계 "재시도 대기" — 과부하 529 는 한 번으로 풀리지 않을 수 있다)
그래도 실패하면 → 멈춤 "자식 실패: <종료 코드·출력 첫 줄>". 답변 자식이었으면 그 배달은 ack 하지 않은 채 멈춘다(다음 시작 때 [감시 9] 가 다시 처리한다)
}
깨우기 [감시 3] (매 대기 뒤. 코디네이터 자식이 도는 동안·한도 쉼 동안은 하지 않는다. codex 워커는 깨우지 않고 아래 조건이면 알림만) {
도는 dispatch 마다 orca orchestration worker-read --dispatch <ID> --source transcript --json(50개씩 cursor 로 이어 읽는다)의 마지막 메시지가 도구 호출 없는 assistant 텍스트(턴이 끝남)이고,
그 워커의 worker_done 을 받지 않았고 전사에도 성공한 worker_done 전송(Bash 결과 "Sent msg_…")이 없는데, 다음 중 하나가 기준 시간을 넘으면:
① 워커 전사 파일(~/.claude/projects/<워커 트리 경로의 구분자·비ASCII 를 - 로>/*.jsonl 가운데 첫 사용자 메시지에 그 dispatch ID 가 정확히 든 것 — 추측하지 않는다)에
queue-operation enqueue 뒤 dequeue·remove 가 없음 — 3분 (백그라운드 완료 알림을 놓쳤다)
② 스크립트가 그 워커의 질문에 reply 한 뒤 워커 쪽 새 메시지가 없음 — 3분 (오르카 재시작으로 끊긴 ask 뒤 잠들었다)
③′ 이번 턴에 건 백그라운드(Bash run_in_background·Monitor·Agent)도 끝나지 않은 백그라운드도 없음 — 5분
③ 그 밖에 턴이 끝난 뒤 새 메시지가 없음 — 20분
가드: worker-show 의 observation.agentWait 에 값이 있으면(사람이 답할 프롬프트 — ask 로 기다리는 동안은 null 이다) 깨우지 않고 알림.
terminal.preview 에 한도 화면 문구("usage limit"·"continuing automatically at")가 있으면 깨우지 않고 알림
통과하면 orca orchestration send --to dispatch:<ID> --subject "깨우기" --body "이것은 답이나 승인이 아니다. 기다리던 reply·백그라운드 결과를 확인하고 계속하라" --json 을 한 번
(쉬는 워커는 이것으로 깬다 — 오르카가 워커 입력에 "You have 1 orchestration message" 를 넣는다). 5분 뒤에도 새 메시지가 없거나 같은 워커에 두 번째로 성립하면 → 알림
worker-read 가 오류(판정 불가)면 깨우지 않고, worker-show 의 terminal.lastOutputAt 이 20분 정체면 알림. 전사 파일 찾기(①)는 깨우기 판단에만 쓴다
}
알림 (구동 스크립트·밖의 감시 공통) → (임시)/loop-alerts.log 에 "<시각> <종류> <대상> <요약>" 한 줄을 남기고 OS 알림을 띄운다(8절 "알림 수단").
종류는 이 이름만 쓴다(공백 없는 한 낱말 — 사람이 기록을 찾아 읽는다): 스크립트죽음 · 스크립트정체 · 단계상한 · 사람프롬프트 · 새질문 · 정지 · 재시작 · 한도 · 전사없음 · 깨워도조용 · 다시잠듦 ·
워커한도화면 · 출력정체 · 배정확인 · 생존확인불가 · 답할수없음 · 답없는질문. 대상은 태스크 ID·단계 이름·"-" 중 하나(공백은 _ 로)
같은 (종류, 대상, 요약) 은 실행 ID 마다 한 번만 알린다(래치 — (임시)/loop-latch-<실행 ID>.txt). OS 알림이 실패해도 기록 줄은 남는다.
요약에는 경과 분·현재 시각처럼 매번 바뀌는 값을 넣지 않는다 — 넣으면 래치가 깨져 1분마다 같은 알림이 나간다(상태가 시작된 시각처럼 그 상태에 고정된 값은 된다)
멈출 때(어느 정지든) → 처리하고 아직 ack 하지 않은 배달이 있으면 orca orchestration check --ack <그 배달 ID> --peek --json 으로 ack 만 하고(답 못 한 질문이 든 배달은 ack 하지 않는다),
종료 사유를 loop-exit.txt 에 쓰고 알린다. 종료 사유 = 앞머리 + " — " + 실제 원인. 앞머리는 이 순서로 정한다:
Next Action 에 답 없는 질문: 이 있으면 "질문 대기" → "배정 없음:" 이 있으면 "배정 없음" → Phase Overview 가 전부 completed 면 "전체 완료"
→ 그 밖은 그 정지의 사유("진전 없음"·"시작 실패"·"자식 실패"·"정리 필요"·오류 문구). 밖의 감시는 앞머리로만 자기 수명을 정한다
(ack 만 하는 명령은 없고 --peek 조합이 그 자리다. --peek 없이 부르면 다음 배치를 새 배달로 받아 온다 — 그 배달도 다음 시작 때
다시 오므로 잃지는 않지만 처리하지 않을 배달을 받을 이유가 없다. 아무것도 안 하고 끝나면 처리한 배치가 다음 시작 때 다시 온다)
② 워커 덧붙임(안의 (임시)·<ID>·<그 파일> 은 실제 값으로 바꿔 넣는다): "출력 블록(검증이면 판정 블록)을 (임시)/done-<ID>.txt(검증이면 (임시)/verdict-<ID>.txt)에 END 까지 쓰고,
프리앰블의 worker_done 명령에 --report-path <그 파일> 을 붙여 끝내라. 코디네이터에게 물을 것은 프리앰블의 ask 로 묻는다.
ask 는 포그라운드로 돌려라(Bash 도구 timeout 600000, ask 의 --timeout-ms 540000 — Claude Code 는 600초를 넘긴 명령을 백그라운드로 옮기므로 여유가 필요하다).
시간 초과는 실패가 아니다 — 같은 메시지 ID 로 ask --resume 을 이어 가라.
ask 가 연결이 끊겨 끝나도("runtime closed the connection" · runtime_unavailable — 오르카가 재시작될 때) 실패가 아니다. 턴을 끝내지 말고 30초쯤 기다렸다가
같은 메시지 ID 로 ask --resume 을 다시 돌려라(그동안 온 답은 거기서 받는다). 세 번 연속 연결이 안 되면 그때 STATUS: failed: 오르카 연결 없음 으로 끝내라."
구동 스크립트가 STATE.md 에 쓰는 것은 자식 사이의 Assignments 토막 값(격리 3 의 e2e 대기 → 검증/수정) 하나뿐이다. Retry·Next Action 은 쓰지 않는다(e2e 출력은 e2e-<ID>.txt).
코디네이터 교체는 없다 — 스크립트가 phase 경계를 그냥 지나고, 판단 자식은 라운드마다 새로 열린다.
사람이 스크립트를 여는 것은 처음과 질문: 에 답한 뒤뿐이다(8절 "구동 시작 명령").
밖의 감시 (② — harness_setup/scripts/loop-watch.*. 구동 스크립트가 시작할 때 트리 밖에서 ORCA_* 없이 띄운다. 1분마다 파일과 읽기 전용 명령만 보고 알림만 한다):
run-use·check·reply·send 와 STATE.md 쓰기는 하지 않는다(Run 바인딩을 빼앗아 스크립트의 check 가 consumer_fenced 로 막히거나 필자가 둘이 된다).
오르카는 worker-show·worker-list --run <Run>·inbox 만 부른다(ORCA_* 없이도 된다). 1분마다 loop-watch.alive 를 쓰고:
[감시 1] loop-exit.txt 가 없는데 loop-drive.pid 의 PID 가 없어졌으면 → 즉시 알림 "구동 스크립트가 죽었다 — 확인하고 다시 시작하라(재부팅 뒤면 --force)"
loop-alive.txt 가 3분 넘게 정체이고 loop-stage.txt 가 대기 계열(대기·한도 대기·재시도 대기)이거나 없으면 → 알림 "구동 스크립트가 대기에서 멈췄다"
[감시 2] loop-stage.txt 의 단계가 8절 상한을 넘으면 → 알림(끄지 않는다)
[감시 4] 도는 dispatch 마다 worker-show 의 observation.agentWait 에 값이 있으면 → 즉시 알림 "사람이 답할 프롬프트: <워커>"
(보조: 전사 마지막이 결과 없는 도구 호출로 5분이고 terminal.preview 에 권한 확인 문구가 있으면 알림)
[감시 8] Next Action 절에 답: 없는 새 질문: 이 생기면 → 알림("스크립트가 도는 중이면 답: 만 적고 다시 띄우지 마라")
loop-exit.txt 가 생기면 → 사유 그대로 알림. 앞머리가 "질문 대기" 가 아니면 알리고 끝난다
"질문 대기" 면 Next Action 절에 답: 이 생길 때까지 1분마다 파일과 PID 만 보며 남고, 답: 이 생겼고 스크립트가 없으면 → 알림 "답이 적혔다 — 구동 스크립트를 다시 시작하라" 후 끝난다
질문:·답: 은 Next Action 절 안에서만 본다. 래치는 구동 스크립트와 같은 파일을 쓴다(같은 실행 ID 에 같은 알림은 한 번).
③ 서브에이전트 — 상위 세션이 구현·검증 서브에이전트를 차례로 띄우고 반영 토막을 직접 한다
①-병렬 (오르카 없음, 기본) — 스크립트는 띄우고 기다리기만, 판단은 코디네이터 자식이:
반복 {
Next Action 에 "질문:" 이 있고 "답:" 이 없으면 → 멈추고 사람에게
Phase Overview 가 전부 completed 면 → 끝
claude -p "<코디네이터 지시문>" --allowedTools "<코디네이터 허용 목록>" --add-dir <임시>
STATE.md 의 Assignments 행마다 — 결과 파일(done-·verdict-<ID>.txt)이 남은 행은 건너뛴다(끝난 자식이다. 다시 띄우면 결과를 덮어쓴다)
(워커 트리에서, 병렬로 띄우고 전부 끝날 때까지 기다린다):
토막 = 구현 | 수정 → claude -p "<구현 지시문> 태스크: <ID>" --allowedTools "<허용 목록>" > (임시)/done-<ID>.txt
토막 = 검증 → claude -p "<검증 지시문> 태스크: <ID>" --allowedTools "<검증 허용 목록>" > (임시)/verdict-<ID>.txt
격리 3이면 위 구현·수정 행이 끝난 뒤, e2e 대기 행과 검증 행을 슬롯 순서대로 하나씩:
앱 전환 (local-dev -Down → 그 워커 트리에서 local-dev) → e2e 대기 행이면 e2e 게이트 명령을 직접 실행
→ 종료 코드 0 이면 행을 검증으로 바꾸고 이어서 검증 자식, 아니면 행을 수정으로 되돌리고 출력을 (임시)/e2e-<ID>.txt 에 남긴다
(판단이 없는 전환·실행이라 스크립트가 한다. 분류는 다음 라운드의 수정 자식이 e2e-<ID>.txt 를 읽고 한다)
Assignments 가 비어 있고 Next Action 에 "배정 없음:" 이 있으면 → 멈춤 (차단 대기 또는 막다른 곳)
}
코디네이터 지시문 (토막: 반영+배정. ①-병렬과 ② 공통):
loop_setup/LOOP.md 와 plan_setup/STATE.md 를 읽고 0절대로 시작하라(0절의 쓰기 항목은 네 일이다). 너는 구현하지 않는다.
0) 사용자가 기본 브랜치(8절)에 커밋한 것이 있으면 git merge <기본 브랜치> 를 먼저 하라(충돌이면 질문: 과 종료). 그다음 Next Action 의 답: 을 0절 5번대로 반영하라 — 차단 해소, 이어서(같은 슬롯 행을 수정으로, Cycle 0), 건너뛰라(그 워커 트리에서 git reset --hard coordinator && git clean -fd, 행·슬롯을 비우고
failed 유지·알림:), 재판정, <ID> 다시(행을 구현으로).
1) (임시)/done-<ID>.txt 가 있는 행마다: END 가 없으면 워커 중단 — Retry History 에 "구현 중단", 파일을 지우고 행을 유지하라(두 번째면 질문:).
STATUS 가 "task_validation 대기" 면 행을 검증으로, "e2e 대기"(격리 3) 면 행을 e2e 대기로, "미활성" 이면 승인을 따라 완료 판정으로, "failed:" 면 3절대로 Retry·질문: 을 남기고 행을 정지로,
"하네스 부족" 이면 harness-update 를 ①만 하고 행을 구현으로 되돌려라(②③이면 질문: 과 정지). RETRY 줄은 그 태스크의 Retry History 로(분류가 진짜 결함인 줄마다 Cycle +1 — 격리 3 의 e2e 실패로 시작한 수정도 여기서 센다), LOG 줄은 LOG.md 로 옮기고 파일을 지워라.
2) (임시)/verdict-<ID>.txt 가 있는 행마다: END 를 확인하고(없으면 잘린 것 — 지우고 행을 검증으로 유지, 두 번째면 질문:) 판정을 STATE·LOG 로 옮겨라.
PASS 면 한 번에 하나씩: 그 워커 트리에서 커밋하고 코디네이터 트리에 병합한 뒤 태스크 문서를 completed 로, 코디네이터 트리에서 커밋, 기본 브랜치(8절)를 fast-forward, 완료 후 점검(harness-update ①)·phase 판정(완료 묶음은 그 워커 트리의 앱으로 돌린다),
행을 지우고 슬롯을 비워라(워커 트리는 git reset --hard coordinator && git clean -fd, 슬롯 1 은 문서 커밋 뒤 git clean -fd). 병합 충돌은 풀지 말고 질문: 이다.
FAIL 진짜 결함이면 Retry 를 올리고 관찰을 Retry History 에 적은 뒤 행을 수정으로 되돌려라(같은 슬롯). 환경 문제면 local-dev 재준비 후 행을 검증으로.
판정 불가·3회 소진이면 질문: 과 행 정지 — 슬롯은 비우지 않는다.
실패는 LOG.md 에 "- 실패: <ID> / <게이트> / <검사 ID> / <분류>" 줄로 남겨라.
격리 3 이면 검증 행에 dispatch 파일이 있는 동안(검증 워커가 도는 중) local-dev·e2e·phase 완료 묶음처럼 앱을 바꾸는 일을 하지 말고 다음 라운드로 미뤄라 — 앱 전환은 구동 스크립트가 슬롯 순서로 한다.
2.5) 반복 판정(3절): LOG.md 에서 같은 검사 ID 가 서로 다른 태스크 둘 이상에 나왔는지 세라(그 ID 의 마지막 "- 반복 처리:" 뒤만).
걸렸고 harness-update ①로 고칠 수 있으면 지금 하고, ②③이거나 격리 부족(격리 3에서 DB·앱을 쓰는 테스트의 흔들림)이면 질문: 을 남기고 3) 배정을 건너뛰어라.
처리했으면 "- 반복 처리: <검사 ID> → <무엇을 했나>" 를 남겨라. 네가 할 수 있는 것을 알림: 으로 미루지 마라.
phase 를 새로 열 차례면 0절 7번(만료된 알림·판단 시점이 온 미결)을 먼저 하라 — 걸리면 질문: 과 함께 다음 phase 를 열지 마라.
3) 배정: (dag-render 실행 명령) --ready 를 돌려 지금 열 수 있는 후보(지금의 completed 집합으로 다시 센 진입 차수 0, blocked 아님)를 받고,
그중 현재 phase·DAG.md 에 있음·돌고 있는 행과 파일 겹침(DAG.md 점선)·같은 자원 그룹·단독 규칙(8절 자원 충돌 표)에 안 걸리는 태스크를
빈 슬롯 수만큼 골라 행을 추가하고 태스크 문서를 in_progress 로 써라. DAG.md 의 폭 표는 세팅 때의 추정이다 — 배정 근거로 쓰지 마라(선행이 끝났으면 "다음 단계" 여도 지금 연다). 슬롯 1 은 이 트리(coordinator), 슬롯 2 이상은 worker-N 워크트리(없으면 여기서 git worktree add 로 만든다).
Assignments 머리줄(동시 n / 상한 N · 워크트리 목록)을 갱신하라. DAG 에 없는 태스크 문서가 있으면 "알림(loop-update, ~다음 phase 전): DAG 에 없는 태스크 <ID>".
배정할 것이 없고 미완료가 남았으면 Next Action 에 "배정 없음: <이유>" 를 남겨라(구동 스크립트의 정지 신호다. 다음 라운드에 배정하면 지운다).
4) STATE.md 의 Task Status·Current Phase 를 태스크 문서 기준으로 맞추고 종료하라. 병렬에서 문서를 쓰는 것은 너뿐이다(구동 스크립트가 자식 사이에 바꾸는 Assignments 토막 값과 질문 답변 자식의 질문: 한 줄만 예외).
질문 답변 지시문 (②, 워커의 ask·escalation 을 받을 때):
loop_setup/LOOP.md 와 plan_setup/STATE.md, 그리고 태스크 <ID> 의 문서를 읽어라. worker-N 이 이렇게 물었다: "<질문>" (선택지: <있으면>).
이 문서들과 harness_setup/ 문서로 답할 수 있으면 첫 줄에 "답: <답>" 만 출력하라.
요구사항 해석·수용 기준 변경·게이트 약화·범위 확장·미결 판단이 필요한 질문이면 답하지 말고,
STATE.md Next Action 에 "질문: <ID> — <질문>" 을 적고 첫 줄에 "질문:" 만 출력하라. 그 밖의 파일은 쓰지 마라.
병렬 구현·검증 지시문에 덧붙이는 줄: "너는 worker-N 이다. plan_setup/·harness_setup/ 문서는 코디네이터 트리 (경로) 에서 읽고, 0절의 쓰기 항목은 건너뛰어라.
STATE.md·LOG.md·태스크 문서·하네스 문서를 쓰지 마라. local-dev 는 8절 슬롯 환경변수 표의 값으로 자기 트리에서 돌려라.
(구현) 시작 전 하네스 점검에서 부족하면 구현하지 말고 STATUS: 하네스 부족 으로 끝내라. 격리 3이면 e2e 를 돌리지 말고 unit 까지 통과한 뒤 STATUS: e2e 대기 로 끝내라.
(수정) (임시)/e2e-<ID>.txt 가 있으면 그것이 실패한 e2e 출력이다 — 분류하고 진짜 결함만 고친 뒤 지워라. 마지막에 8절 워커 규칙의 출력 블록을 END 까지 출력하라."
```
**코디네이터 허용 목록**: 파일 읽기·쓰기(`STATE.md`·`LOG.md`·태스크 문서·`harness_setup/` 문서), `local-dev`, `dag-render --ready`(배정 단계 — 옵션 없는 실행은 `DAG.md` 를 다시 쓰므로 `--ready` 한 가지만), phase 완료 묶음 명령,
`git add`·`git commit`·`git merge`·`git worktree`·`git checkout`·`git branch`, 결과 파일 삭제. 제품 코드 수정은 주지 않는다.
**질문 답변 허용 목록**(②): 파일 읽기, `STATE.md` 쓰기. 답을 만드는 자리라 코드·git·하네스 명령은 주지 않는다.
**②의 워커에는 허용 목록을 줄 수 없다** — `worker-start` 에 그 플래그가 없고, 워커는 사용자의 에이전트 권한 설정으로 돈다. 그래서 검증 워커의 "쓰기·git 금지"는 지시문으로만 지켜진다. 이 사실을 보고에 적고, 사용자의 권한 설정에 무엇을 허용해야 하는지(게이트 명령·`local-dev`·파일 쓰기)를 함께 안내한다.
**격리 3의 e2e 게이트 명령은 구동 스크립트가 직접 돌린다**(①-병렬·② 같다) — 종료 코드만 보는 일이라 판단이 없다.
**허용 목록**: 구현·반영은 파일 읽기·쓰기, `local-dev`, 게이트 명령, `git add`·`git commit`·`git restore`, 판정 파일 삭제.
검증은 파일 읽기, `local-dev`, 그리고 `HARNESS.md` 에 적힌 **관찰 수단**(브라우저 조작 도구 또는 e2e 도구 실행)만.
권한 전체 건너뛰기는 사용자가 명시적으로 켠 경우에만.
**판정 파일**: `<OS 임시 폴더>/verdict-<태스크 ID>.txt` (저장소 밖. 태스크 ID 로 파생해 병렬에서 겹치지 않는다). 검증 세션의 출력을 받는 **일시 파일**이고
반영 토막이 옮긴 뒤 지운다. 기록이 아니라 전달 수단이라 커밋하지 않고, 남아 있으면 반영이 안 된 것이다. 마지막 줄 `END` 가 없으면 잘린 것이다.
**사람에게 넘어오는 지점**: `Next Action` 의 `질문:` (3회 소진·판정 불가·미활성 승인·차단 대기·phase 완료 묶음 실패·반복 판정의 ②③·만료된 알림·판단 시점이 온 미결·②에서 문서로 답할 수 없는 워커 질문).
**사람의 답이 돌아오는 길**: `Next Action` 아래 `답:` 줄을 직접 적거나 대화형 세션에서 루프에게 말한다. 다음 자식이 0절에서 읽어 반영하고 지운다.
**판정 블록**은 검증 세션이 출력하고 반영 토막이 `STATE.md`·`LOG.md` 로 옮긴다 — 검증 세션은 그 두 파일을 쓰지 않는다.
**앱 서버는 세션이 끝나도 살아 있어야 한다** — `local-dev` 가 분리된 프로세스로 띄운다(하네스의 규칙). 검증 세션은 `-Status` 로 확인하고 없으면 다시 띄운다.
## 5. 완료와 기록
**코드 작성이 끝난 것은 완료가 아니다.** `verification` 의 게이트마다 **활성 상태에서 통과했거나,
미활성이면 `Standing Decisions` 에 유효한 승인이 있어야** 완료다. 커밋 조건도 이것과 같다.
기록 순서: **태스크 `status` → `STATE.md` → `LOG.md` → 커밋.** 어긋나면 태스크 문서가 이긴다.
| 시점 | STATE.md | LOG.md |
|---|---|---|
| 시작 | `Current Task`, `in_progress`, `Retry` 초기화. phase 의 첫 태스크면 `Phase Overview` 를 `in_progress` 로 | `### <ID> Started.` |
| 게이트 | `Last Verification` | 통과는 한 줄, **실패는 관찰과 분류** + `- 실패: <ID> / <게이트> / <검사 ID> / <분류>` |
| 재시도 | `Retry` 의 `Cycle`(진짜 결함만 +1)·`History`(모든 갈래) | 무엇을 고쳤는가 |
| 검증 대기 | `Last Verification` = `<ID> / task_validation 대기 / —` (구현 토막의 마지막 쓰기). 미활성이면 `task_validation 미활성` | `### <ID> lint·unit·e2e PASS. task_validation 대기.` |
| 완료 | `completed`, 다음 태스크, `Retry` 초기화, **`Last Verification` 을 `<ID> / PASS / —`(미활성 승인이면 `PASS (미활성 승인)`)로 덮어쓴다** — `대기`·`미활성` 을 남기면 구동기가 반영 토막을 또 연다 | `### <ID> Verification PASS.` |
| 잘린 판정 | `Retry` 의 `History` 에 `검증 중단` 한 줄(`Cycle` 미증가). 두 줄째면 `Next Action` 에 `질문:` | `### <ID> task_validation 판정 파일 잘림.` |
| 중단 | `failed`, `Next Action` | 세 번의 시도와 관찰 |
| phase 완료 | `Phase Overview`·`Current Phase`·phase 문서 `Status`·`Task Status` 교체 | `### phase-NN Complete.` |
| phase 완료 묶음 실패 | `Phase Overview` 그대로, `Next Action` 에 `질문:` | `### phase-NN 완료 묶음 FAIL — FLOW-xx (분류).` |
관찰은 재현 가능한 문장으로 — "정상 동작 안 함"이 아니라 "무엇을 했더니 무엇이 보였다".
**커밋은 위 완료 조건을 만족한 뒤에만.** 메시지에 태스크 ID. **merge·rebase 는 루프 밖(orchestration)이다.**
**phase 완료**: 모든 태스크가 `completed`(`blocked` 나 `failed` 가 남았으면 완료가 아니다. `blocked_by: [폐기]` 만 예외) **그리고** phase 문서의
Acceptance Criteria 충족 **그리고** 'phase 완료 묶음'이 통과.
`Phase Overview`(그 phase `completed`, 다음 phase `in_progress`)·`Current Phase`·phase 문서의 `Status` 를 갱신하고
`Task Status` 를 다음 phase 의 태스크로 교체한다. `Phase Overview` 의 값은 `pending`·`in_progress`·`completed` 셋뿐이다.
**phase 완료 묶음**(느려서 매 태스크에서 뺀 흐름들)은 **여기가 유일하게 도는 자리**다. 안 돌리면 영원히 안 돈다.
**실패하면 phase 를 완료로 찍지 않는다** — "게이트 명령이 아니다"는 태스크마다 안 돈다는 뜻이지
통과하지 않아도 된다는 뜻이 아니다. 다만 **재시도 3회는 적용하지 않는다**(예산은 태스크 단위다).
깨진 흐름과 그것을 만든 태스크를 `Next Action` 에 적고 사용자에게 보고한다.
차단으로 `대기` 인 흐름은 실행 대상이 아니고, 부분 구간 값을 판정으로 쓰지 않는다.
## 6. 환경 준비와 실패 분류의 도구
`local-dev` 명령: (실제 경로와 옵션). **종료 코드가 실패한 단계를 구별한다.**
| 코드 | 무엇이 실패했나 | 누가 고치나 |
|---|---|---|
| 0 | 성공 | — |
| (전제) | 런타임·컨테이너 엔진이 없다 | **사용자** — 설치가 필요하다 |
| (환경변수) | 필수 키가 비어 있다 | **사용자** — 값을 채워야 한다 |
| (의존 서비스 / 스키마·시드 / 앱) | 컨테이너·마이그레이션·앱이 안 떴다 | 재준비 → 안 되면 `harness-update` |
> 실제 코드 번호는 `HARNESS.md` 의 로컬 개발 환경 절에 있다. **0이 아니면 그 자체가 환경 문제 확정**이고
> 여기서 추측할 것이 없다. `-Status` 는 아무것도 띄우지 않고 판정만 출력하므로 분류에 먼저 쓴다.
## 7. 하네스 갱신을 부르는 자리
| 언제 | 확인 |
|---|---|
| 태스크 시작 전 | 수용 기준 판정 수단 / 덮는 흐름·커버 영역 / 의존 서비스 / 게이트 명령이 이 유형에 걸리는가 |
| 태스크 완료 후 | 활성 시점인가 / 흐름의 마지막 조각인가 / 커버 영역을 덮었나 / 새 의존 서비스·환경변수 |
| 시점 무관 | 차단(OQ·CF) 해소(2절 절차의 마지막 단계) / 같은 흐름·검사 ID의 실패가 두 태스크에 걸쳐 나옴 |
**전부 예이면 부르지 않는다.** "게이트 실패"는 그 자체로 트리거가 아니다 — 분류가 먼저다.
**병렬에서는 코디네이터만 자기 트리에서 부른다.** 워커는 시작 전 점검에서 부족을 보면 `STATUS: 하네스 부족` 으로 끝낸다(8절 워커 규칙).
## 8. 병렬 운용
**기본은 병렬.** 병렬 여부: (예/아니요 — 세팅 때 받은 답). 동시 에이전트 수 상한: (N). 구동기: (②/①-병렬/①-단일).
폭이 1인 구간은 단일과 같다. 아니요면 이 절의 DAG·충돌 표만 유효하고 슬롯·격리는 9절 미결이다.
### DAG (태스크 단위)
**그림은 `loop_setup/DAG.md`** — `harness_setup/scripts/dag-render.*` 의 출력이고 손으로 고치지 않는다. 태스크가 바뀌면 `loop-update` 가 스크립트를 다시 돌린다.
`DAG.md` 에 있는 것: `mermaid` 그래프(실선 = `depends_on`, **점선 = `files` 겹침(동시 금지)**, 회색 점선 테두리 = `blocked`) / 구간별 폭 표 / 확인 결과(순환·없는 ID·`blocked` 후행) / 파일 겹침 쌍 목록.
규칙: 순환 없음 / 없는 ID 참조 없음 / `blocked` 와 그 후행 제외 / **동시 실행 후보 = 진입 차수 0** /
**`files` 가 겹치면 간선이 없어도 동시에 스케줄하지 않는다.**
재생성: `(dag-render 실행 명령)` → `loop_setup/DAG.md`. 마지막 생성: (날짜, 태스크 n개).
**배정 후보**: `(dag-render 실행 명령) --ready` — 지금의 태스크 `status` 로 다시 센 진입 차수 0(`pending`·선행 전부 `completed`·`blocked` 아님). 코디네이터가 배정마다 부른다.
### 구간별 폭 (DAG.md 의 표를 세팅 때 옮겨 적은 요약. 권장 에이전트 수는 사용자가 정한 상한과 폭의 작은 쪽. **세팅 때의 추정이다 — 상한을 고른 근거이지 배정의 근거가 아니다. 배정은 `--ready`**)
| 구간 | 폭 | 권장 에이전트 |
|---|---|---|
| phase-01 시작 | 1 | 1 |
| phase-01-task-01 완료 후 | 1 (01-02·01-03 겹침이라 직렬) | 1 |
### 자원 충돌 검사 결과
(파일 겹침 / 공유 런타임 자원 / 빈 `files`. 0건이면 "0건". 처리 값은 `단독` / `그룹 직렬: <그룹>` / `격리가 덮음`. 코디네이터는 이 표와 `DAG.md` 점선만 본다)
| 태스크(쌍) | 자원 | 종류 | 자원 그룹 | 처리 |
|---|---|---|---|---|
| 01-02 ↔ 01-03 | `src/app/layout.tsx` | 파일 겹침 | — | 동시 금지(점선). 같은 모듈이면 `알림(plan-generator, …):` 으로 의존 추가 요청 |
| 01-01, 02-03, 03-01 | `db/migrations/*`, `drizzle/meta/_journal.json` | 공유 런타임(DB 스키마·저널) | `migration` | 그룹 직렬 — 같은 그룹끼리만 동시 금지, 다른 태스크와는 동시 가능 |
| 04-02 | `files: []` | 빈 `files` | — | 단독. `알림(plan-generator, …):` 으로 `files` 보완 요청 |
### 배정
**코디네이터가 태스크를 나눠 준다.** 구현 지시문에 `태스크: <ID>` 를 넣고 워커는 2절 선택을 건너뛴다.
코디네이터가 `--ready` 후보(지금의 `completed` 집합으로 다시 센 진입 차수 0·`blocked` 아님) 중 현재 phase·`DAG.md` 에 있음·돌고 있는 행과 충돌 없음(파일 겹침 점선, 같은 자원 그룹, `단독`)을 판정한다.
폭 표로 배정하지 않는다 — 선행이 끝난 태스크는 표의 "다음 단계"여도 지금 열린다.
배정표는 `STATE.md` 의 `Assignments`(①-병렬·② 같다)이고 병합 전까지 "무엇이 도는가"의 원본이다.
### 슬롯과 워크트리
**기본 브랜치**: `(실제 이름 — main / master / …)`. 아래의 `main` 은 이것을 뜻한다. **여기서는 아무도 작업하지 않는다** — 사용자의 자리. 완료된 태스크만 fast-forward 로 들어온다.
**코디네이터 트리**: `(절대 경로)`, 브랜치 `coordinator`. 코디네이터(문서 필자 — 4절의 두 예외 밖·배정·병합) + 슬롯 1 의 `worker-1`. 코디네이터는 문서 경로만 add 한다(`git add -A` 금지).
**워커 트리** `worker-N`(N = 2 … 상한): `(경로 규칙 — 기본 ../<저장소>-worker-N. 저장소가 OneDrive 같은 동기화 폴더 아래면 저장소 안 .wt/worker-N 또는 동기화 밖 경로)`. `.wt/` 를 쓰면 `.gitignore` 에 있어야 한다.
워커 이름은 워크트리 유무와 무관하게 `worker-N`. 검증 워커는 구현이 돈 자리에서 이어서 돈다. 폭 1 이면 코디네이터 트리의 `worker-1` 하나, 워크트리 0개.
워커 트리는 폭 2 구간에 들어갈 때 만들고 재사용한다 — 재배정 직전 `git reset --hard coordinator && git clean -fd`(`git checkout coordinator` 는 다른 워크트리에 체크아웃된 브랜치라 거부된다). 폭이 줄어 안 쓰면: (지운다 / 둔다).
태스크 완료 = 워커 트리 커밋 → 코디네이터 트리 병합 → 기본 브랜치 fast-forward. 사용자가 기본 브랜치에 커밋했으면 라운드 시작 때 `git merge <기본 브랜치>` 를 코디네이터 트리에 먼저.
**에이전트·모델 배치** — 구동기가 자식을 띄울 때 이 표에서 읽는다. 이름은 여기에만 있다(바꾸면 `loop-update`).
| 역할 | CLI | 모델 | 추론 강도 | 한 급 올리는 조건 |
|---|---|---|---|---|
| 코디네이터 | (claude / codex) | (최상위 급의 실제 이름) | high | — |
| 구현 워커 | (claude / codex) | (보통 급의 실제 이름) | medium | `priority: P0` 이고 `files` 3개 이상 / 3회 소진 뒤 "이어서" 재배정 |
| 검증 워커 | (claude / codex — 구현과 다르게 두면 분리가 한 겹 더 강해진다) | (보통 급의 실제 이름) | medium | — |
| 반영 토막(①-단일) | (claude / codex) | (보통 급의 실제 이름) | medium | — |
사용자 선택: (추천대로 / 전부 최상위 / 전부 보통 / 직접 지정 — 날짜). 자동 대체(`--fallback-model`)는 쓰지 않는다. ②의 질문 답변 자식도 코디네이터 행으로 띄운다.
**구동 스크립트**: `harness_setup/scripts/loop-drive.(실제 확장자)` — 4절의 ①/② 알고리즘을 그대로 옮긴 것. 모드: (①-단일 / ①-병렬 / ②). 모델은 이 표에서 읽는다. ②면 **밖의 감시** `harness_setup/scripts/loop-watch.(같은 확장자)` 도.
**구동 시작 명령**: (실제 명령. ②면 오르카 터미널 안에서 — 예: 코디네이터 트리의 오르카 터미널에서 `node harness_setup/scripts/loop-drive.mjs`. 스크립트가 터미널 프로세스 밖으로 자신을 분리해 띄우고 로그를 따라 읽는다 — 그 뒤 터미널을 닫아도 루프는 돈다.
이미 돌고 있으면 띄우지 않고 알린다. 멈추려면 `loop-drive.pid` 의 PID 를 끝낸다 — 종료 사유는 남지 않고(밖의 감시가 "죽었다" 를 알린다) 다음 시작은 PID 가 없으니 그대로 된다. PID 파일 없이 흔적만 남았거나 그 PID 의 시작 시각을 읽지 못해 "비정상 종료" 로 거부되면 확인하고 `--force` 를 붙인다)
사람이 여는 것은 처음과 `질문:` 에 답한 뒤 재개할 때뿐이다. 스크립트가 죽었다 다시 뜨면 ②는 `(임시)/loop-run.txt` 의 Run 을 `run-use` 로 이어받는다.
**분리 실행 방식**(②): (실제 값 — Windows: WMI `Win32_Process.Create`, 창 숨김 / macOS·Linux: `setsid nohup`). `Start-Process`·`&` 처럼 터미널 셸의 자손이면 터미널을 닫을 때 함께 죽고,
자기 환경에 `ORCA_*` 를 가진 프로세스는 트리 밖이어도 오르카가 정리한다 — 스크립트는 띄우기 때 적은 `(임시)/loop-env.json` 에서 `ORCA_*` 를 뺀 값을 자기 환경으로 쓰고 `ORCA_*` 는 orca 호출에만 붙인다(2026-10-01 탐침. macOS·Linux 는 스모크에서 확인).
**알림 수단**(②): `(임시)/loop-alerts.log` 한 줄 + OS 알림 — (실제 값. Windows: 모듈 없이 Windows PowerShell 5.1 의 토스트 `[Windows.UI.Notifications.ToastNotificationManager]` / macOS: `osascript -e 'display notification …'` / Linux: `notify-send`). OS 알림이 실패해도 기록 줄은 남는다.
**감시 기준값**(②, 잠정 — 실측으로 조정): 단계 상한 — 앱 전환 5분 · 태스크 e2e 15분 · 오르카 명령 5분 · 코디네이터 자식 60분(phase 완료 묶음 포함) · 답변 자식 10분 · 재시도 대기 그 회차 간격+2분 · 한도 대기 재설정 시각+15분(모르면 17분).
생존 파일 정체 3분 · 시작 뒤 첫 전사 5분 · 깨우기 ①② 3분·③′ 5분·③ 20분, 깨운 뒤 확인 5분 · 답 없는 질문 15분 · 자식 재시도 1·5·15분(3회) · 한도 재설정을 모르면 15분.
**임시 폴더의 구동 파일**(②): `loop-run.txt`(Run) · `loop-drive.pid`·`loop-env.json`·`loop-stage.txt`·`loop-alive.txt`·`loop-exit.txt`(종료 사유) · `loop-watch.pid`·`loop-watch.alive` · `loop-alerts.log`·`loop-latch-<실행 ID>.txt` · `answered.txt` · `dispatch-<ID>.txt`·`done-`·`verdict-`·`e2e-<ID>.txt` · `loop-drive.log`.
이것은 기록이 아니라 구동 상태다(기록처는 `STATE.md`·`LOG.md` 둘뿐) — 커밋하지 않고, 지울 때는 스크립트가 멈춘 뒤에 지운다.
구동 스크립트와 밖의 감시가 서로 읽는 파일의 모양은 고정이다 — `loop-drive.pid`·`loop-watch.pid` = JSON `{"pid": <숫자>, "start": "<시작 시각 읽기 명령의 출력>", "run": "<실행 ID>"}` · `loop-stage.txt` = JSON `{"stage": "<단계>", "at": "<ISO 시각>", "until": "<ISO — 쉬는 단계에서 끝날 시각을 알 때만. 한도 재설정 시각을 모르면 쓰지 않는다 — 감시는 17분으로 잰다>", "run": "<실행 ID>"}`
(단계 이름: 시작 · 대기 · 코디네이터 자식 · 답변 자식 · 오르카 명령 · 앱 전환 · 태스크 e2e · 재시도 대기 · 한도 대기) · `loop-alive.txt` = ISO 시각 한 줄 · `loop-exit.txt` = 종료 사유 한 줄 · `loop-latch-<실행 ID>.txt` = 줄마다 `<종류>|<대상>|<요약>`.
**프로세스 시작 시각 읽기**: (실제 명령 — Windows: `powershell.exe -NoProfile -Command "(Get-Process -Id <pid>).StartTime.ToUniversalTime().ToString('o')"` / macOS·Linux: `ps -o lstart= -p <pid>`). 구동 스크립트의 종료 코드는 "전체 완료"·"질문 대기"·"배정 없음" 이면 0, 그 밖의 정지는 1.
**구동 터미널은 구동 전용이다**(②) — 오르카는 Run 에 읽지 않은 메시지가 남아 있으면 `You have N orchestration message. Run \`orca orchestration check …\`` 를 그 터미널의 **입력으로** 넣는다.
셸이면 명령으로 실행하려다 오류 한 줄을 내고 끝난다(무시한다 — 메시지는 스크립트가 받는다). 그 터미널에서 대화형 에이전트가 돌면 이 문구가 사람 말로 섞여, 그 세션이 지시대로 `check` 를 불러 스크립트가 처리할 배달을 가져갈 수 있다 —
그 터미널에서 대화형 에이전트를 열지 않는다. 스크립트는 자식(`claude -p`·`orca`)에게 터미널 입력을 물려주지 않는다(표준 입력 `ignore`).
**대기 한 번의 길이**(②): `60000`ms(60초). 기다리는 것은 스크립트라 짧아도 토큰이 들지 않고, 짧아야 대기 뒤의 확인을 제때 한다. 빈 대기는 체크포인트다 — **빈 대기가 15분 이어지면** `worker-list` 로 생존만 본다(횟수가 아니라 시간으로 센다).
**스모크 테스트**: (날짜 — ②면 가벼운 모델의 시험 워커 하나로 run-create → worker-start → ask/reply → 결과 파일 + worker_done → release 까지 돈 결과. ①이면 드라이런 결과)
독립 워크트리에서 작업 / 남의 워크트리를 고치지 않는다 / 공통 파일은 충돌 가능성을 확인한다 /
태스크 완료 후 커밋 / **merge·rebase 는 코디네이터(orchestration)의 책임.** 병합 충돌은 풀지 않고 `질문:`.
### 슬롯별 환경 (격리 방식: 1 / 2 / 3)
| 슬롯 | 포트 | DB 이름 | 환경 파일 |
|---|---|---|---|
| 1 | 3001 | app_slot1 | `.env.slot1` |
| 2 | 3002 | app_slot2 | `.env.slot2` |
`local-dev` 가 이 변수를 읽는지: (확인함 / **안 읽음 — harness-update 필요, 그때까지 폭 1**).
**격리 3이면**: 구현 워커는 `unit` 까지만 돌리고 `STATUS: e2e 대기` 로 끝낸다. 구동기가 `e2e 대기`·`검증` 행을 슬롯 순서대로 하나씩 — 앱 전환(`local-dev -Down` → 그 워커 트리에서 `local-dev`)
→ e2e 명령 직접 실행(0이면 검증 자식, 아니면 `수정` 행 + `e2e-<ID>.txt`) → 검증. 병렬 이득은 구현·lint·unit 까지다.
슬롯이 여럿이어도 **`task_validation` 분리는 여전히 필요하다.**
### 병렬에서 기록이 꼬이지 않게 하는 두 규칙
**(가) 칸을 넓힌다** — `Current Task`·`Last Verification`·`Retry` 를 태스크별 블록이나 목록으로.
넓힐 때 **기존 값을 새 형식으로 옮긴다.** 지우면 진행 중인 재시도 이력이 사라진다.
`Current Task`·`Last Verification` 은 Layer 3이 만든 절이므로 **바꾼 사실을 보고에 올린다.**
**(나) 필자를 하나로 줄인다** — 칸을 나눠도 파일 쓰기는 통째로 일어나 서로 덮어쓴다.
잠금으로 풀지 않는다(잠근 채 죽은 에이전트가 전체를 세운다).
| 무엇 | 병렬에서 누가 쓰나 |
|---|---|
| 태스크 문서의 `status` | **코디네이터만 자기 트리에서** — 배정 시 `in_progress`, 병합 뒤 `completed`, 정지 시 `failed`. 워커 브랜치는 코드만 바꾼다 |
| `STATE.md` | **orchestration 만.** 넓힌 `Last Verification`·`Retry` 는 지시문의 `태스크: <ID>` 블록만 읽고 쓴다. 예외: 질문 답변 자식(②)은 `Next Action` 에 `질문:` 한 줄, 구동 스크립트는 자식 사이에 `Assignments` 토막 값만(격리 3 의 `e2e 대기` → `검증`/`수정`) |
| `LOG.md` | **orchestration 만.** 워커는 출력 블록(`LOG:` 줄·판정 블록)을 넘기고 병합 순서대로 append 된다 |
| 하네스 문서 (`harness-update`) | **코디네이터만 자기 트리에서.** 워커는 `STATUS: 하네스 부족` 으로 돌려보낸다 |
**문서는 전부 코디네이터 트리의 한 필자, 슬롯은 코드만.** 같은 문서를 두 트리가 고치면 병합이 어느 쪽이 맞는지 판단할 수 없다.
워커가 죽어도 배정표와 워커 트리의 미커밋 변경이 남아 같은 슬롯에서 이어 간다.
`STATE.md` 가 실시간이 아니게 되는 것은 감수한다 — 지금 무엇이 도는지가 급하면 태스크 문서를 본다.
### 워커 규칙 (병렬)
- 코디네이터 트리 경로: `(절대 경로)`. `plan_setup/`·`harness_setup/` 문서는 **여기서 읽는다**. 코드·게이트 명령·`local-dev` 는 자기 트리(슬롯 1 은 코디네이터 트리, 2 이상은 `worker-N`)에서
- 0절의 쓰기 항목(2·4·5·7)을 건너뛴다. 차단 해소·승인 만료·`답:` 반영·phase 열 때의 확인은 코디네이터
- 구현 워커의 출력 블록: `TASK:` / `STATUS:`(`task_validation 대기` | `task_validation 미활성` | `e2e 대기`(격리 3) | `failed: <사유>` | `하네스 부족: <무엇>`) / `RETRY:`(사이클마다 한 줄) / `LOG:`(남길 문장) / `END`
- 수정 워커는 `(임시)/e2e-<ID>.txt` 가 있으면 그것이 실패한 e2e 출력이다 — 3절대로 분류하고 진짜 결함만 고친 뒤 파일을 지운다
- 시작 전 하네스 점검에서 부족을 보면 구현하지 않고 `STATUS: 하네스 부족` 으로 종료. `harness-update` 는 부르지 않는다
- 정지 행은 슬롯을 점유한 채 남는다. "건너뛰라"면 코디네이터가 그 워커 트리를 `git reset --hard coordinator && git clean -fd` 로 비운다
- `DAG.md` 에 없는 태스크는 배정 대상이 아니다(`알림(loop-update, …)`). 완료는 한 번에 하나씩 병합·완료한다
## 9. 미결
(병렬이면 격리는 8절에서 정했으므로 여기 없다. `local-dev` 가 슬롯 변수를 안 읽으면 아래 첫 행. 병렬을 껐으면 둘째 행.
**`언제` 는 루프가 확인할 수 있는 시점으로** — `phase-NN 진입 전`·`<태스크 ID> 완료 후`·`폭 2 구간 전`. 루프가 phase 를 열 때 이 열을 보고 시점이 왔으면 `질문:` 으로 올린다(0절 7번).)
| ID | 미결 | 종류 | 기본값 | 무엇을 보면 정하나 | 언제 |
|---|---|---|---|---|---|
| — | 격리 적용 대기 — `local-dev` 가 슬롯 환경변수를 읽지 않음 | 차단 | 폭 1로 운용 | `harness-update` 가 스크립트를 고쳤는가 | 폭 2 구간에 들어가기 전 |
| OQ-0xx | (병렬을 껐을 때만) 슬롯 간 DB·포트 격리 방식 | 유예 | 단일 | 실제 동시 에이전트 수, DB를 공유해야 하는 태스크의 실재 여부 | 병렬을 켜기 직전 (`loop-update`) |
선택지와 되돌리기 비용(8절 격리 표와 같다):
1. **슬롯별 환경 통째 분리** — 슬롯 번호로 DB·포트·컨테이너 이름을 파생. 완전 격리, 자원 N벌
2. **DB 서버 하나 + 슬롯별 데이터베이스와 전용 계정** — 가볍다. `local-dev` 에 권한 발급 단계가 추가된다
3. **격리 없이 공유 자원을 쓰는 게이트만 직렬화** — 새로 만들 것이 거의 없다. 병렬 이득이 구현·lint·unit 까지
**DAG 와 충돌 표는 순서와 파일 충돌을 덮지만 공유 런타임 인스턴스(DB·앱)는 덮지 못한다.** 포트는 겹치면 안 떠서 즉시 드러나고,
**DB는 조용히 오염된다.** 그래서 폭 2 구간에 들어가기 전에 반드시 닫혀 있어야 한다.
````
### `plan_setup/STATE.md` 에 더하는 절
````markdown
# Retry
* **Task:** —
* **Cycle:** 0 / 3
* **Gate:** —
* **Classification:** —
* **History:** —
---
# Standing Decisions
* (없음)
---
# Assignments
동시: 0 / 상한 N · 워크트리: coordinator
| 태스크 | 워커 | 토막 | 시작 |
|---|---|---|---|
| (없음) | | | |
````
> `Last Verification` 뒤, `Next Action` 앞에 끼운다. 기존 다섯 절은 건드리지 않는다.
> `Retry` 는 태스크가 바뀌면 초기화하고, `Standing Decisions` 는 사람이 승인한 것만 쌓인다.
> `Assignments` 는 **병렬일 때만** 넣는다(①-병렬·②의 배정표. 코디네이터 자식이 쓰고 구동 스크립트가 읽는다 — 스크립트는 격리 3 의 e2e 결과로 토막 값만 바꾼다). 토막 값은 `구현`·`수정`·`e2e 대기`(격리 3만)·`검증`·`정지` 다섯.
> 병렬이면 `Current Task`·`Last Verification`·`Retry` 도 **태스크별 블록**으로 넓힌다(11단계 (가)) — Layer 3 절을 바꾸는 것이므로 보고에 올린다.
---
## 에이전트별 참고
- Claude Code에서 실행 중이면: `references/claude-code.md`
- Codex에서 실행 중이면: `references/codex.md`
**둘 다 반드시 확인할 것은 하나다** — 이 환경에서 `task_validation` 을 **어떻게 분리된 세션으로 돌리는가**.
루프가 그 분리를 실제로 만들어 내는 쪽이라, 여기가 Layer 5의 핵심 구현 지점이다.
분리 수단이 없는 환경이면 `LOOP.md` 에 그 사실을 적고 사용자에게 알린다.
분리된 척하는 검증은 통과 기록만 남기고 아무것도 막지 않는다.