/cc-dev:batch — 계층 전체를 한 방에 자동 처리#
| 항목 | 내용 |
|---|---|
| 실행 명령 | /cc-dev:batch |
| 분류 | 워크플로우 |
| MCP 서버 | zenhub, sequential |
한마디로#
Initiative(전략 테마) · Project(제품 단위) · Epic(기능 단위) · Story(작업) · Sub-task(세부 작업) — 5단계 계층 중 어느 레벨의 이슈 번호를 던져도, 그 아래에 매달린 모든 하위 이슈를
자동으로 개발·검토·병합해 주는 자동화 명령입니다. 회사 전체 목표 하나를 던지면 그 아래 여러 프로젝트·기능·작업을 알아서 나눠 동시에 진행하고, 다 끝나면 차곡차곡 위로 합쳐 올라가는 "조립 라인"이라고 보면 됩니다. 이 팀은 전원이 Orca(orchestration
+ orca-cli 스킬)를 쓰므로, 서로 상관없는 형제 작업(같은 부모를 둔 여러 자식)은 별도 워크트리에서 동시에 진행하고, 머지만 순서대로 합쳐 충돌을 막습니다. 다만 동시에 여는 작업 공간은
기본 3개까지입니다 — 개발 머신 한 대가 감당하는 실측 상한이 그 정도라서, 넘치는 형제는 대기줄에 세우고 자리가 나는 대로 이어서 진행합니다(조용히 빠뜨리지 않고 미룬 이슈 번호를 전부 기록합니다).
누가·언제 쓰나요#
- 여러 개의 하위 작업이 딸린 Epic 하나(기존 방식), 또는 그보다 큰 Project·Initiative 하나를 통째로 진행하고 싶을 때
- 작업 하나하나 직접 브랜치를 만들고 PR을 올리고 병합하는 반복 작업을 사람이 일일이 챙기지 않고 자동으로 돌리고 싶을 때
- 서로 독립적인 여러 형제 작업(예: Project 아래 여러 Epic, Epic 아래 여러 Story)을 동시에 진행해 전체 소요 시간을 줄이고 싶을 때
- 마지막에 사람이 한 번만 최종 승인하면 되는, 손이 덜 가는 일괄 처리 방식을 원할 때
무엇을 해주나요#
- 어떤 레벨의 이슈 번호를 넣든, 그 이슈 자신의 작업 공간(브랜치)을 먼저 만들고 그 안에서 하위 이슈들을 처리합니다.
- 다른 컴퓨터·다른 세션이 이미 만들어 둔 작업 공간을 그대로 이어받습니다. 같은 프로젝트의 기능(Epic)을 다른 자리에서 끝냈더라도, 원래 프로젝트 줄기를 이슈 번호로 찾아 거기에 합칩니다 — 줄기 이름의 뒷부분(제목을 영문으로 옮긴 부분)이 자리마다 다르게 지어져도 헷갈리지 않습니다. 이름이 갈려 같은 프로젝트에 줄기가 두 개 생기고 한쪽이 텅 비는 사고를 막는 장치입니다.
- 서로 의존관계가 없는 형제 이슈는 Orca 워크트리로 병렬 착수하고, 의존관계가 있는 형제는 순서를 지켜 처리합니다. 워크트리를 늘리기 전에는 항상 "이렇게 나눠서 동시에 진행할까요?"를 먼저 확인받습니다 — 이 확인은 맨 처음 실행에서 딱 한 번 받고 아래 단계로 전달됩니다(작업 공간 안에는 답할 사람이 없기 때문입니다). 사람이 답할 수 없는 무인 실행이면 승인 없이 작업 공간을 늘리지 않고 순차로 내려갑니다 — 결과는 같고 시간만 더 걸립니다.
- 동시에 여는 작업 공간 수에는 상한이 있고(기본 3), 넘치는 형제는 대기줄에서 순서를 기다립니다. 한 작업 공간이 정해진 시간(기본 4시간) 안에 끝을 알리지 않으면 그 하위 이슈만 "막힘"으로 표시하고 나머지 형제는 계속 진행합니다 — 응답 없는 하나가 전체를 영구히 붙잡지 못하게 합니다.
-
합쳐진(머지된) 작업 공간은 그 자리에서 반납합니다. 자리를 비워야 대기줄의 다음 형제가 들어갈 수 있기 때문입니다. 반납 전에는 "정말 합쳐졌는지 · 안 올라간 변경이 남아 있지 않은지"를 확인하고, 그 작업 공간 전용으로 만들어졌던 것들(전용 데이터베이스·컨테이너·포트·가상 기기)을
폴더를 지우기 전에 먼저 돌려줍니다 — 순서가 뒤바뀌면 주인 없는 찌꺼기가 영구히 남습니다. 막힘으로 표시된 작업 공간은 사람이 들여다볼 현장이므로 지우지 않고 남깁니다.
도중에 세션이 끊겨 정리를 놓쳐도 다음 실행이 밀린 정리를 따라잡습니다(
--keep-worktrees로 끌 수 있습니다). - 각 하위 이슈마다 개발 → 테스트 → 검토(Code Review) → PR 생성 → 병합을 자동으로 진행합니다. 병합 자체는 충돌을 막기 위해 항상 한 번에 하나씩(순서대로) 이뤄지고, 합치기 직전에 그 하위 작업을 최신 상태 위로 올려 검사를 다시 통과시킨 뒤 합칩니다(앞 순서가 이미 합쳐져 기준이 달라졌기 때문입니다).
- 병합이 끝나면 해당 ZenHub 이슈를 자동으로 "완료(Close)" 처리하고, 제대로 닫혔는지까지 다시 확인(검증)합니다.
- 하위 작업이 하나라도 남아 있으면 상위 작업(Epic 등)을 닫지 않습니다. 하위 목록은 GitHub의 하위 이슈 링크를 기준으로 빠짐없이 확인하며, 목록 조회 자체가 실패하면 "하위 작업 없음"으로 넘기지 않고 그 자리에서 멈춥니다. 병합 직후 뒤늦게 남은 하위 작업이 발견되면 상위 작업을 자동으로 다시 열어 상태를 사실과 맞춥니다. 다만 아무리 재시도해도 안 풀리는 하위 작업 하나 때문에 상위 작업이 영원히 마무리 불가가 되지는 않습니다 — 사람이 그 하위 작업을 "이번 범위에서 제외(보류)"로 명시하면, 그 사실을 이슈와 PR에 남기고 상위 작업을 마무리할 수 있습니다. 조용히 넘어가는 길은 없습니다.
- 이미 등록돼 있는 하위 작업이 있으면 그것을 처리합니다 — 같은 내용의 작업 카드를 새로 만들어 처리하지 않습니다(원래 카드가 열린 채 남는 것을 막습니다).
- 다른 세션이 이미 잡고 있는 하위 이슈는 건드리지 않습니다 — 그 하나만 "다른 세션 진행 중"으로 표시하고 빼고, 나머지 형제는 계속 진행합니다. 같은 일감을 두 세션이 각자 끝까지 해버리는(한쪽 작업이 통째로 버려지는) 사고를 막는 장치입니다. 다만 그렇게 건너뛴 하위 이슈가 있으면 상위 작업을 닫지는 않습니다 — 끝났는지 확인된 게 아니기 때문입니다
- 작업을 시작·검토 단계로 넘길 때마다 보드(파이프라인) 칸을 실제로 옮깁니다 — 상위 작업이 아직 "착수 전" 칸에 있으면 그 위 단계까지 함께 "진행 중"으로 올려, 보드만 봐도 지금 무엇이 돌아가는지 보이게 합니다.
-
모든 하위 이슈가 끝나면 이번에 처리한 레벨 전체를 묶은 PR을 부모 브랜치(없으면
development)로 만들어 병합까지 마칩니다. 이 마지막 병합만 사람의 승인을 거칩니다(단, 부모가 남아 있다면 부모 자신의 마무리는 별도로/cc-dev:batch {부모번호}를 다시 실행해야 합니다 — 한 번의 실행이 계층 전체를 끝까지 밀어붙이지 않고, 실행 단위를 명확히 남겨 둡니다).
어떻게 쓰나요#
/cc-dev:batch {issue_number}
# 예: 1234번 Epic을 통째로 처리 (기존 방식과 100% 동일하게 동작)
/cc-dev:batch 1234
# 예: 20번 Project를 통째로 처리 — 그 아래 여러 Epic을 Orca로 병렬 디스패치
/cc-dev:batch 20
-
{issue_number}자리에는 Initiative/Project/Epic/Story/Sub-task 어느 레벨의 이슈 번호든 넣을 수 있습니다. 레벨은 자동으로 판별합니다. - 그 뒤로는 명령이 알아서 하위 이슈들을 찾아 처리하므로, 추가로 입력할 것은 없습니다.
- 형제 작업을 동시에 진행해도 되는지는 시작 전에 한 번 확인받습니다(Orca 워크트리를 여러 개 띄우는 결정이므로).
안에서 무슨 일이 벌어지나요#
전체 흐름은 이 이슈 하나에 대해 재귀적으로 반복됩니다.
- (선택) 기획 명세 정렬 점검 — 확정된 기획 명세가 있으면, 시작 전과 최종 병합 직후에 방향이 맞는지 한 번씩 대조합니다. 없으면 조용히 건너뜁니다.
-
하위 이슈가 있는지 확인 — 하위 이슈가 없으면(가장 작은 단위) 곧바로
/cc-dev:run에 넘겨 구현·테스트·PR·병합까지 한 번에 처리하고 끝냅니다. 하위 이슈가 있으면 2번으로 이어집니다. -
이 이슈의 작업 공간 만들기 + 하위 이슈 처리하기 — 이슈 레벨(Initiative~Sub-task)에 맞게, 부모가 있으면 그 부모 브랜치 위에서 없으면
development에서 이 이슈 전용 브랜치를 만듭니다. 그 안에서, 서로 독립적인 하위 이슈는 Orca로 동시에 착수하고(사람 확인 후), 각자 이 흐름을 재귀적으로 반복합니다. 병합만 순서대로 하나씩 합칩니다. -
이 레벨 마무리하기 — 모든 하위 이슈가 닫혔는지 확인하고, 이 레벨 전체를 묶은 최종 PR을 부모 브랜치(또는
development)로 만듭니다. 사람의 최종 승인을 받은 뒤 병합하면 이 이슈가 자동으로 종료됩니다. 부모가 있었다면, 부모의 형제들도 모두 끝났는지만 확인해 신호를 남기고(자동으로 부모를 병합하지는 않음) 정지합니다 — 부모까지 마저 진행하려면/cc-dev:batch {부모번호}를 다시 실행합니다.
⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)
Usage#
/cc-dev:batch {issue_number} [--merge=pre-authorized] [--no-parallel]
[--max-parallel N] [--worker-timeout {h}] [--unattended]
[--keep-worktrees]{issue_number}: Initiative/Project/Epic/Feature·Bug·Task(=Story)/Sub-task 중 어느 레벨이든. 레벨은getIssueTypes()의level필드로 자동 판별한다 (rules/zenhub-conventions.md→ "Issue Type Hierarchy" 참조).--merge=pre-authorized:/cc-dev:go가 단일 계획 게이트 통과 후 전파하는 값. 재귀적으로 모든 하위 레벨의 최종 PR 승인 질문(이 문서의 "컨테이너 종료" 단계, 그리고 하위/cc-dev:run//cc-dev:batch호출)에 전파되어 즉시 머지한다. 완료성 Hard Gate·품질 게이트·MAJOR-DRIFT 확인·병렬 디스패치 계획 승인(아래 참조)은 그대로 유효 — 생략되는 것은 "머지해도 되는가"에 대한 사람의 승인 클릭뿐이다.--no-parallel: Orca가 가용해도 형제 이슈를 항상 순차 처리한다. 형제끼리 파일이 겹칠 걱정이 있거나 디버깅 중일 때 사용. 결과는 동일하고 소요 시간만 늘어난다.--max-parallel N: 동시 워크트리 상한(기본3). 이 값은 예산이며 cap 때문에 미룬 형제는 deferred 로그에 이름으로 남는다(아래 "3. 디스패치").N=1은--no-parallel과 동등하다.--worker-timeout {h}: 워커 1개당 wall-clock 예산(기본4h). 초과 시 그 child 를BLOCKED('worker_timeout')로 표시하고 형제는 계속한다(아래 "3. 디스패치" — Rung 3 의미 그대로).--keep-worktrees: 머지된 자식의 Orca 워크트리를 회수하지 않는다(기본값은 회수). 디버깅·사후 분석으로 체크아웃을 남겨야 할 때만 쓴다. 지정하면 회수도 고아 스윕도 돌지 않으므로 디스크와 병렬 슬롯이 회복되지 않는다 — 미회수 목록은 그대로 로그·대장에 남는다(조용한 누적 금지). 회수 절차·안전 판정의 SoT 는 orca-worktree-lifecycle.--unattended: 이 실행에 답할 사람이 없다는 선언(/cc-dev:go --auto가 전파하는 값이거나 CI/무인 호출). 스폰 승인이 필요한 자리는 승인 없이 스폰하지 않고 순차로 내려간다(=--no-parallel로 강등). 그 외AskUserQuestion자리의 무인 기본값과 남겨야 하는 내구 기록은 아래 "Unattended & Approval Contract" 표가 유일한 정의다.
이 문서는 /cc-dev:batch가 5레벨 어디서 시작해도 동일하게 따르는 하나의 재귀 알고리즘을 정의한다. {epic_number}만 넣던 기존 사용법은 이 알고리즘의 특수 케이스로 100% 하위 호환된다.
Hierarchical Branch Strategy (5 levels)#
SoT는 skills/branch-hierarchy/SKILL.md — 아래는 이 커맨드가 그 규칙을 어떻게 실행하는지의 요약이다.
development
└── initiative/{n}-{slug} ← 부모 없음(구조상 최상위). base: development
└── project/{n}-{slug} ← Initiative 부모가 있으면 그 브랜치, 없으면 development
└── epic/{n}-{slug} ← Project 부모가 있으면 그 브랜치, 없으면 development
├── story/{n}-{slug} ← base: epic/{n}-{slug}
│ ├── task/{n}-{slug} ← base: story/{n}-{slug}
│ └── task/{n}-{slug}
└── story/{n}-{slug}| 레벨 | prefix | 부모 있을 때 base | 부모 없을 때 base | children 없을 때 |
|---|---|---|---|---|
| Initiative | initiative/ | (구조상 부모 없음) | development | 드묾 — 사용자 확인 후 leaf로 진행 |
| Project | project/ | initiative/{n}-* | development | 드묾 — 사용자 확인 후 leaf로 진행 |
| Epic | epic/ | project/{n}-* | development | 드묾 — 사용자 확인 후 leaf로 진행(기존 "단독 Epic" 케이스) |
| Story | story/ | epic/{n}-* | (부모 없으면 feature|fix|chore/ — 이 알고리즘 밖의 단독 이슈 케이스) | 정상 — 항상 leaf 가능 |
| Sub-task | task/ | story/{n}-* | (Sub-task는 항상 부모 있음) | 항상 leaf(최하위) |
Initiative/Project/Epic이 children 0개인 것은 정상적인 leaf(Story/Sub-task)와 달리 이례적이므로, Hard Gate(아래) 에서 "판정 불가 — 자식이 없는 게 맞는지" 확인을 받은 뒤에만 곧바로 구현으로 진행한다(기존 batch.md의 "단독 Epic" 처리와 동일 원칙을 Project/Initiative까지 확장).
CI 게이팅은 그대로 base-branch 기준이다(pull_request.branches: [development, main] 류) — 레벨이 5개로 늘어나도 development로 향하는 최종 PR에서만 전체 CI가 돈다는 성질은 변하지 않는다. 중간 계층 PR(브랜치→브랜치)은 CI가 돌지 않는다.
⚠️
development는 이 팀의 고정된 통합 브랜치 이름이지, "GitHub 저장소의 default 브랜치"를 그때그때 조회해서 정하는 값이 아니다.gh repo view --json defaultBranchRef는 오직 "Closes #N이 자동으로 발화하는가"(=default 브랜치로 머지됐는가)를 판정할 때만 쓴다(rules/zenhub-conventions.md"Issue Closure Policy" 참조) — 계층의 최종 병합 대상 자체는 항상development다. 이 저장소의 GitHub default가development가 아닐 수 있으므로(예:main), 최상위 레벨(부모 없는 Initiative/Project/Epic)이development로 머지돼도 자동 close가 안 될 수 있다 — 그래서 아래 "Issue Closure & Post-Merge Sync"가 항상 명시적 검증+폴백을 요구한다.
Orca Parallel Dispatch (형제 이슈 병렬 처리)#
컨테이너 레벨(Initiative/Project/Epic, 그리고 Sub-task를 가진 Story)이 children을 2개 이상 가지면, 서로 독립적인 children은 Orca로 병렬 착수한다.
0. 가용성 확인 (Phase 0에서 1회)#
1. Skill(orca-cli) 호출 — discovery stub이므로 항상 최신 가이드를 로드한다. 캐시된
서브커맨드를 추측해서 쓰지 않는다(버전마다 바뀐다).
2. Skill(orchestration) 호출 — 마찬가지로 최신 가이드를 로드한다.
3. `ORCA status --json` 로 앱 실행 확인. 미실행이면 `ORCA open --json` 시도.
4. 그래도 실패하거나 `--no-parallel`이 지정됐으면: 병렬 디스패치를 끄고 이 배치 실행 전체를
순차 처리로 전환한다 — `skills/agent-teams/SKILL.md`의 Fallback Principle과 동일 원칙
(기능 차이 없음, 소요 시간만 달라짐). 1회 경고 로그만 남기고 하드 실패하지 않는다.
5. **고아 스윕 1회**(`B0.r`) — 이전 실행이 죽거나 `/clear` 로 끊겨 **회수하지 못한 워크트리**를
따라잡는다(`--keep-worktrees` 면 생략). 실행 시점은 **대상 이슈가 확정된 Phase 0.3 직후**다 —
대장이 그 컨테이너 이슈의 마커 코멘트에 있으므로 그 전에는 읽을 수 없다. 대상은 시도 대장에
`worktree` 가 적혔고 아직 `reclaimed` 가 아닌 행 ∩ 실재하는 워크트리 목록이며, 후보마다
회수 안전 판정 5종을 그대로 적용한다. 절차·판정·금지의 SoT 는
[orca-worktree-lifecycle](../skills/orca-worktree-lifecycle/SKILL.md) 다 — 여기서 재서술하지
않는다. 스윕 실패는 **경고 후 계속**이며 배치를 막지 않는다.
🧹 sweep: 후보 {n}건 · 회수 {k}건 · 보존 {m}건({사유 요약})
⛔ 대장에 없는 워크트리는 **남의 것**이다 — 보고만 하고 손대지 않는다.1. 의존관계 판단 (병렬 후보 선별)#
rules/zenhub-conventions.md의 우선순위 스코어링 "Dependency(blocker)" 축과 같은 기준을 쓴다:
다른 형제가 이 형제의 산출물(예: 공통 Entity/데이터 계층)에 의존한다고 판단되면 그 형제는
순차 처리 대상, 그렇지 않으면 병렬 후보다. 애매하면 순차 쪽으로 기운다(안전 우선).
2. 병렬 계획 승인 (건너뛰지 않는다)#
Orca 워크트리는 agent-teams.md의 Agent Teams보다 훨씬 무겁다(별도 git worktree + 별도
터미널/세션). 그래서 스폰 전에 항상 AskUserQuestion으로 계획을 확인받는다 —
--merge=pre-authorized가 지정돼 있어도 이 확인은 생략하지 않는다(그건 "최종 머지 승인"이고
이건 "병렬 착수 계획 승인"으로, 서로 다른 대상이다):
확인 내용: 병렬 후보 목록(형제 이슈 번호·제목), 각자 배정될 워크트리/브랜치,
순차로 남긴 형제와 그 이유,
**동시 실행 수 N = min(병렬 후보 수, --max-parallel 기본 3)**,
**1차로 동시에 열리는 형제**와 **대기줄에 남는 형제**(번호·제목) 목록,
워커 1개당 wall-clock 예산(--worker-timeout 기본 4h)과 초과 시 동작이 승인은 최상위 호출에서 정확히 한 번 취하고 하위로 전파한다 — 디스패치된 워크트리는
비대화형이므로 그 안에서는 이 질문에 답할 사람이 없다(rules/orchestration-graph.md §2 마지막
항목: 워커 안에서 도달 가능한 루프의 exhaust: 로 대화형 AskUserQuestion 은 무효다). 재귀
/cc-dev:batch {child} 는 이미 받은 승인을 재사용하고 다시 묻지 않는다.
--unattended(또는 /cc-dev:go --auto) 로 답할 사람이 없으면 승인 없이 스폰하지 않는다 —
그 자리는 --no-parallel 로 강등해 순차로 내려간다(skills/agent-teams/SKILL.md Fallback
Principle: 기능 차이 없음, 소요 시간만 다름). 하드 실패도, 무승인 스폰도 아니다.
3. 디스패치#
폭은 예산이다 (rules/orchestration-graph.md §5): 동시에 열리는 워크트리는
width = min(병렬 후보 수, --max-parallel) 이고 기본 --max-parallel = 3 이다.
근거는 이 저장소의 실측값이다 — plugins/cc-serverpod/skills/serverpod-worktree-parallel/SKILL.md
→ "현실적인 병렬 개수"(M-시리즈 32GB에서 3~4개가 상한, build_runner 동시 실행 시 더 줄어듦),
plugins/cc-dev/skills/parallel-test-env/SKILL.md 의 디바이스/포트 슬롯 상한(시뮬레이터·에뮬레이터
·브라우저 프로필이 워크트리마다 하나씩 필요하다), 그리고 skills/agent-teams/SKILL.md
→ "Team Size 3~5" 의 팀 상한. 이 셋과 어긋나는 값을 여기서 새로 정하지 않는다.
슬롯 루프 (contract:L-B2a — 필드는 아래 " Loop Contracts " 에 있다):
1. 승인된 병렬 후보를 의존성 없는 것부터 순서대로 늘어놓고, 앞에서 width 개만
**1차 착수**한다. 남은 형제는 **대기줄(wait queue)** 에 넣는다.
2. cap 때문에 미룬 형제는 전부 **deferred 로그**에 이름으로 남긴다 — 조용한 절단 금지:
⏸️ deferred(cap=3): #{n} {title} · #{n} {title} … (슬롯 회수 시 착수)
3. 워커 하나가 끝나(worker_done) 슬롯이 비면 대기줄 맨 앞을 착수하고, 그 전이를 로그에 남긴다.
4. 대기줄이 비고 in-flight 가 0 이면 루프 종료.
착수되는 각 child 마다:
- orca-cli 가이드가 제공하는 명령으로 `myBranch`(이 레벨 자신의 브랜치) 기준
워크트리를 만든다 (tier: standard — `skills/agent-teams/SKILL.md` Effort Routing
Convention. 미설정은 기본값이 아니라 width 배 승수다).
- ⭐ **스폰 응답의 워크트리 id 를 즉시 시도 대장에 적는다** — ` < repoId > :: < worktreePath > `
**두 조각이 붙은 한 값**을 통째로 적는다(repo id 만 잘라 적으면 나중에 그 워크트리를
지목하지 못한다). 이 기록이 아래 " 4.5 회수 " 와 Phase 0 스윕의 **유일한 후보 명단**이고,
워크트리 **경로**에서 파생된 자원(워크트리별 DB·포트 블록·컨테이너)의 주인을 되짚는
유일한 근거다 — 체크아웃을 지운 뒤에는 되짚을 방법이 없다.
(대장 위치·칸 정의는 아래 " 5.1 시도 대장 " , 회수 절차의 SoT 는
[orca-worktree-lifecycle](../skills/orca-worktree-lifecycle/SKILL.md))
- 그 워크트리 안에서 child를 처리한다. `{inherited}` = 이 실행이 받은 `--merge=pre-authorized`·
`--unattended` 를 **문자 그대로** 옮겨 붙인 것이다(새로 판단하지 않는다):
children(child)가 있으면 → /cc-dev:batch {child.number} {inherited} (재귀)
없으면(leaf) → /cc-dev:run {child.number} {inherited}
resolveBaseBranch()(commands/run.md)가 이미 만들어진 `myBranch`를 child의 부모
브랜치로 자동 인식하므로, 별도 --base 지정이 필요 없다.
⛔ `{inherited}` 를 빠뜨리면 그 child 워크트리 안에서 `/cc-dev:run` 의 " Unattended & Approval
Contract " (`commands/run.md`)가 답할 사람 없는 `AskUserQuestion` 을 만나 조용히 멈춘다 —
그 워크트리에는 콘솔을 볼 사람이 없다.
- orchestration 가이드의 task 디스패치 / worker_done 대기 패턴으로 완료를 감시한다.
⏱️ **워커당 wall-clock 예산 = --worker-timeout(기본 4h)**. 숫자를 고르는 방식의 SoT 는
`skills/job-timeout-budget/SKILL.md`(실측 최대 × 2, 표본 없으면 형제 잡에 정합) —
표본이 없어 가장 무거운 형제 잡(배포 150분)에 정합시켜 4h 로 둔다.
⏱️ **on-timeout(선언된 행동, 침묵 금지)**: 그 child 를 `BLOCKED( ' worker_timeout ' )` 로
표시하고(`blockIssue(child, ' worker_timeout ' )` — holding 이동 + 사유 코멘트 + **점유 해제**.
⚠️ 해제를 빠뜨리면 죽은 워커의 점유가 TTL(4h)까지 그 child 를 잠근다) 워크트리
세션을 종료한 뒤 **나머지 형제는 계속**한다 — stall ladder Rung 3 와 동일한 의미
(`agents/sequential-workflow.md` → " Rung 3 — BLOCKED + CONTINUE " ). 시도 기록은
아래 " 5. Stall Ladder " 의 시도 대장에 남는다.
⛔ 이 행동이 없으면 worker_done 을 끝내 못 보내는 워커 하나가 Phase 2-d 를 영구히
붙잡는다(완료성 게이트는 in-flight 형제가 있는 동안 평가되지 않는다).
순차로 남긴 child는 기존과 동일하게 하나씩 처리한다.⚠️ 아래 "형제 처리 전략" 의 선택형 스택 모드를 켠 경우 — 스택 조작은 한 번에 한 워크트리에서만 한다. 병렬 워크트리 여럿이 같은 스택을 동시에 건드리면
gh stack이 exit 8(다른 프로세스가 스택 잠금) 으로 떨어질 수 있다.gh stack init/add/link/submit/sync/merge는 오케스트레이터(이 레벨) 한 곳에서만 실행하고, 디스패치된 워크트리 안에서는 실행하지 않는다. 워크트리마다gh stack init을 중복 실행하면 같은 브랜치가 여러 스택에 속해 exit 6(disambiguation 필요) 이 상시 발생한다. exit code 별 조치의 SoT 는 stacked-prs 의 "실패 모드" 절이다.
4. 머지는 항상 직렬화#
구현이 병렬이어도 myBranch로의 병합은 항상 한 번에 하나씩이다 — 완료된 순서대로 PR→
squash merge하고, 매 머지 후 myBranch를 git pull --ff-only로 최신화한 뒤 다음 머지를
진행한다(충돌 방지). 이는 각 child의 /cc-dev:run·/cc-dev:batch 사이클 마지막 단계
(Post-merge sync)가 이미 수행하는 일이므로, 오케스트레이터가 별도 락(lock)을 구현할 필요는
없다 — 다만 동시에 두 child가 머지를 시도하지 않도록, 완료(worker_done) 신호가 도착한
순서대로 머지 단계만 하나씩 순서를 매겨 실행한다.
이 조인의 이름은 mode:stream serialize:1 order:worker_done-arrival 이다
(rules/orchestration-graph.md §5 — 기본은 mode:stream, 꼬리를 줄 세울 때 serialize:k).
barrier 가 아니다: 형제 하나가 늦거나 BLOCKED 여도 큐를 잡아 두지 않는다 — BLOCKED 형제는
큐에서 빠지고 남은 도착 순서대로 계속 머지한다. barrier 는 Phase 2-d 완료성 게이트 한 자리뿐이다.
머지 큐 루프 (contract:L-B2b — 필드는 아래 " Loop Contracts " 에 있다). 큐 항목 하나마다:
1. child 를 **현재 `myBranch` 위로 갱신**한다 — 앞 항목이 이미 squash 머지돼 base 가
달라져 있다. (rebase 또는 `myBranch` 를 child 브랜치로 머지, 팀 관례에 맞게 택1)
2. 갱신된 head 에서 **`runPrePushGate()` 재실행**(`run.md` Step 8.5 — 그 문서가 SoT,
여기서 복제하지 않는다) 또는 **갱신된 head 에 대한 CI green 확인**.
⛔ 이 단계가 없으면 child #2 의 게이트는 **child #1 의 squash 이전 `myBranch`** 를
기준으로 통과한 판정이 되고, 그 상태로 머지된다. `--merge=pre-authorized` 도
이 재검사를 덮지 못한다(사전 승인은 " 승인 클릭 " 하나만 생략한다).
3. 갱신 중 충돌이 나면 conflict rung 을 적용한다:
rung C1 — `skills/merge-conflict-resolution/SKILL.md` 절차로 해소 후 2번으로 복귀
(해소 전략·파일 유형별 규칙은 그 문서가 SoT — 여기서 재서술하지 않는다)
rung C2 — 같은 파일에서 의미상 동일 충돌이 반복되면 그 child 를 큐에서 빼고
`blockIssue(child, ' merge_conflict ' )` 로 표시(보드 + 점유 해제), **나머지 큐는 계속** 진행
4. squash merge → `myBranch` 를 `git pull --ff-only` 로 최신화 → 다음 항목.
5. **머지된 child 의 워크트리를 회수한다** — 아래 " 4.5 머지된 형제의 워크트리 회수 " .
⛔ 이 자리이지 worker_done 자리가 아니다: 1~3번이 그 체크아웃을 아직 쓴다.
회수 실패는 **경고 후 계속**이며 머지를 되돌리거나 큐를 멈추지 않는다.
6. **큐 상태 전이는 전부 로그로 남긴다**(내구 — 조용한 재정렬 금지):
🔀 queue: #{n} enqueued(arrival #k) / rebased onto {myBranch}@{sha} /
re-gate pass|fail / merged({sha}) / dequeued(BLOCKED:{reason})
🧹 reclaim: #{n} worktree={id} (merged {sha}) → removed | kept({사유}) | leak({사유})⚠️ 선택형 스택 모드에서는 이 절의 "완료된 순서대로 머지" 가정이 깨진다. 스택 PR 은 가장 아래 미머지 PR 부터 시작하는 연속 묶음(contiguous group) 으로만 머지할 수 있고, 중간 PR 을 단독으로 머지할 수 없다 — 그 아래 층들이 항상 함께 머지된다. 따라서 위층이 먼저 완료돼도 아래층이 끝날 때까지 머지 대기이며, 이 자리의
order:worker_done-arrival은 스택 순서에 양보한다. 도착 순서를 조용히 재정렬하지 말고 대기 사실을 큐 로그에 남긴다 (🔀 queue: #{n} waiting(stack-bottom #{m} 미머지)). 완료 순서 ≠ 스택 순서가 상시로 발생한다면, 그 형제들은 애초에 스택에 넣지 말았어야 할 독립 형제다(아래 "형제 처리 전략" 의 판별 표).⚠️ 또한 auto-merge 는 스택에서 미지원이다 — 스택 모드에서는
gh pr merge --auto계열을 쓰지 않고gh stack merge <pr> --squash -y로 머지한다. 나머지 머지 의미론(중간 PR 단독 머지 불가, 머지 후 자동 rebase+retarget, 머지 큐에서의 eject 전파)의 SoT 는 stacked-prs 의 "머지 의미론" 절이다.
4.5 머지된 형제의 워크트리 회수 (머지 = 반납)#
머지는 그 워크트리의 수명이 끝나는 지점이다. 산출물이 myBranch 에 들어갔으므로 체크아웃은
언제든 재현 가능하고, 반대로 그 자리를 잡고 있으면 대기줄의 다음 형제가 들어갈 슬롯이 없다
(--max-parallel 은 예산이지 권고가 아니다 — 근거는 위 "3. 디스패치"의 머신 실측값).
절차·안전 판정·순서·금지의 SoT 는 orca-worktree-lifecycle 다 — 여기서 복제하지 않는다. 이 커맨드가 고정하는 것은 접점 세 가지뿐이다:
| 접점 | 이 커맨드에서의 자리 |
|---|---|
| 언제 | 머지 큐 루프 4번 직후(=5번). worker_done 도, 레벨 종료도 아니다 |
| 무엇을 | 시도 대장에 worktree 가 적힌 그 child 하나. 대장에 없으면 대상이 아니다 |
| 결과 기록 | 대장 state → reclaimed · 큐 로그에 🧹 reclaim: 한 줄 |
- ⛔
BLOCKED로 dequeue 된 형제의 워크트리는 회수하지 않는다 — 사람이 들여다볼 유일한 현장이다(worker_timeout은 터미널만 정지하고 워킹트리는 남긴다). 회수는 머지된 항목의 후처리이지 큐에서 빠진 항목의 뒤처리가 아니다. - ⛔ 회수 실패로 머지를 재시도하지 않는다. 머지·close 는 이미 되돌릴 수 없고, 회수는 그
뒤의 살림이다. 실패는 대장에
leak({사유})로 남기고 다음 배치의 Phase 0 스윕이 재시도한다. --keep-worktrees면 이 절 전체가 no-op 이고, 미회수 child 목록을 로그에 남긴다(조용한 누적 금지).- ⚠️ 선택형 스택 모드에서는 한 번의 머지가 여러 층을 한꺼번에 닫는다(연속 묶음 all-or-nothing).
그러면 회수 대상도 그 묶음에 포함된 child 전부다 — 큐 항목 하나만 회수하고 나머지 층의
워크트리를 남기지 않는다.
gh stack sync --prune이 정리하는 것은 머지된 PR 브랜치이지 워크트리가 아니다(둘을 같은 것으로 읽으면 워크트리가 조용히 쌓인다). - 이 레벨 자신의 워크트리는 여기서 다루지 않는다 — 이 배치가 스스로 만든 것이 아니라 부모가 만들어 준 것이므로, 자기 제거는 금지이고 Phase 3 에서 "반납 가능" 표시만 남긴다 (같은 스킬 §1 — 자기 발밑을 지우면 Phase 3-6.6/3-8 이 한 줄도 실행되지 않는다).
5. Stall Ladder (형제 단위, 재사용)#
한 child가 반복 실패해도 나머지 형제를 막지 않는다 — sequential-workflow.md의
3단 stall ladder를 child 단위로 그대로 적용한다(그 문서의 "Story"는 여기서는 "임의
레벨의 child 이슈"로 읽는다): (1) 같은 전략 재시도(소폭) → (2) 의미상 동일 실패 반복 시
/cc-dev:unstuck solo <affinity-persona>로 리프레임 → (3) 그래도 안 풀리면 그 child를
BLOCKED('unstuck_exhausted')로 표시하고 나머지 형제는 계속 진행 —
blockIssue(child, 'unstuck_exhausted') 호출이다(rules/zenhub-conventions.md → Pipeline State
Contract). 문구만 남기면 그 child 는 In Progress + 점유 상태로 잔류해, 다음 batch 가 자기 앞선
세션 때문에 SKIPPED-OCCUPIED 로 건너뛴다.
5.1 시도 대장 (attempt ledger) — 재개 livelock 의 예산
/cc-dev:batch 는 멱등 재개(Phase 0-2)라서, 재실행마다 사다리가 처음부터 다시 돈다 — 대화
카운터는 /clear 를 넘지 못하므로 예산이 아니다. 그래서 child 단위 시도 횟수를 내구 기록에
적고, 재개할 때 그것을 읽어 예산을 이어 쓴다:
- 위치: 이 컨테이너 이슈의 마커 코멘트 ` < !-- cc-dev:batch-attempt-ledger -- > `
(upsert 방식은 `skills/pr-work-artifact/SKILL.md` 의 마커 코멘트 프로토콜 재사용 — 복제 금지)
- 항목: | child | attempts | last_cause(의미 요약) | worktree | state |
- `worktree`: 스폰 응답의 id **전체**(` < repoId > :: < worktreePath > `). 순차 처리면 `-`.
워크트리 회수와 고아 스윕의 **유일한 후보 명단**이다(위 4.5 · Phase 0-5).
- `state`: open / blocked({reason}) / waived / closed / **reclaimed** / **leak({사유})**
- 예산: child 하나당 **누적 3회**(= batch 재실행을 가로질러 합산). 3회 소진 시 그 child 는
더 재시도하지 않고 `blocked` 로 고정되며, 다음 재실행도 그 child 를 다시 돌리지 않는다.
- 로그: 매 시도마다 한 줄 — `🔁 #{child} attempt {k}/3 (누적) cause={요약}`
- ⛔ **회수용 대장을 따로 만들지 않는다** — 두 대장이 어긋나는 날 어느 쪽이 사실인지 아무도
모른다. 워크트리 상태는 이 대장의 같은 행에 두 칸으로 붙인다.5.2 Descope / Waive — 영구 BLOCKED child 의 탈출구
시도 예산이 소진돼 blocked 로 고정된 child 는 그대로 두면 이 레벨을 영구히 마무리 불가로
만든다(Phase 2-d 완료성 게이트가 열린 자식을 보고 계속 차단한다). 탈출구는 두 가지뿐이며 둘 다
명시적 사람 결정 + 내구 기록을 요구한다 — 조용히 통과하는 길은 없다:
| 경로 | 무엇을 하는가 | 결과 |
|---|---|---|
| descope (선호) | 그 child 를 이 부모에서 떼어낸다 — setParentForIssues 로 재부모 지정하거나 gh issue close --reason "not planned" | 부모의 자식 집합에서 사라지므로 openChildrenStatus() 가 자연히 none 이 된다. 게이트에 예외가 생기지 않는다 |
| waive | child 를 부모에 남긴 채 이 레벨의 마무리만 허용 | 시도 대장의 state 를 waived 로 바꾸고, 사유 + 후속 이슈 번호를 child 이슈 코멘트와 이 레벨 PR body(## Waived Children)에 남긴다 |
- waive 는
AskUserQuestion결정이다(무인 기본값은 아래 "Unattended & Approval Contract" 표). 자동으로 waive 되는 경로는 없다 — 자동 waive 는 완료성 게이트를 fail-open 으로 되돌린다. - 완료성 게이트는 열린 자식을
open과waived로 구분한다 — 판정 함수는 그대로openChildrenStatus()tri-state 이고(rules/orchestration-graph.md§3 — 새 판정 함수를 만들지 않는다), batch 가 그 결과의open목록을 대장과 대조해 분류한다. 통과 조건:status === "none"또는 (status === "open"이고 모든 open child 가 대장에서waived).unknown은 여전히 무조건 차단이다.
형제 처리 전략 — 병렬 워크트리 vs 선형 스택#
위 "Orca Parallel Dispatch" 가 기본값이며 이번 절로 대체되지 않는다. 이 절은 그 위에 얹는 선택형 실행 모드 하나를 정의한다 — 형제들이 순차 의존일 때만 켜는 GitHub 네이티브 Stacked PR 경로다. 명령·플래그·exit code·금지 사항 등 실행 계약의 SoT 는 stacked-prs 이고, 여기서는 판별 기준과 이 커맨드와의 접점만 정의한다(복제하지 않는다).
⚠️ 2026-07-30 public preview 다. 전 저장소에 순차 롤아웃 중이고, 머지 큐 지원은 "이후 몇 주에 걸쳐 점진 롤아웃" 이라고 changelog 에 명시돼 있다. 안정 기능으로 가정하지 말고 항상 아래 "폴백" 경로를 준비한다 — 저장소에서 아직 활성화되지 않았으면
gh stack은 exit 9 로 떨어진다.
판별 — 어느 형제를 어느 쪽으로 보내는가#
새 판별 기준을 만들지 않는다. 위 "1. 의존관계 판단 (병렬 후보 선별)" 이 이미 내린 판정을 그대로 재사용해 갈래 하나만 더 나눈다:
| 위 1번의 판정 | 처리 | 근거 |
|---|---|---|
| 독립 형제 | 현행 유지 — Orca 병렬 워크트리(width:3) + 5레벨 수동 계층 | 스택은 선형 사슬이라 같은 base 를 공유하는 형제는 하나의 스택에 들어갈 수 없다. 억지로 묶으면 병렬성만 잃고, 완료된 형제가 남의 진도에 묶인다 |
| 순차 의존 형제 | 선형 스택 후보 — 의존 순서대로 한 줄로 늘어놓고(linearize) gh stack 으로 묶는다 | 어차피 직렬로 처리될 형제다. 층 사이 base 관계를 GitHub 이 직접 관리해 준다 |
| 애매함 | 스택을 켜지 않는다 — 기존대로 순차 디스패치 | 위 1번의 "애매하면 순차 쪽(안전 우선)" 과 같은 방향. 스택은 이득이 확실할 때만 켠다 |
⛔ 이 커맨드의 계층은 트리이고 스택은 선형 사슬이다. 스택이 진짜 이득인 자리는 "순차 의존이 있는 형제들을 한 줄로 늘어놓는(linearize) 경우" 하나뿐이다 — 이 구분을 흐리면 독립 형제까지 직렬화돼 전체 소요 시간만 늘어난다.
채택 이유 — 스택이 대체해 주는 수작업#
위 "4. 머지는 항상 직렬화" 머지 큐 루프의 1번(child 를 현재 myBranch 위로 갱신) 이,
이 커맨드가 머지 직전마다 손으로 도는 rebase 루프다 — 앞 형제가 squash 머지돼 base 가
움직였기 때문이다. 스택 모드에서는 이 루프가 사라진다. 이것이 채택 이유다:
| 현행(기본값) | 스택 모드 |
|---|---|
머지 직전마다 child 를 myBranch 위로 손으로 갱신(rebase 또는 머지) | gh stack sync (fetch → trunk fast-forward → cascade rebase → push → PR 상태 동기화) |
| 앞 형제가 머지되면 뒤 형제 PR 의 base 를 사람이 다시 맞춤 | 머지 직후 다음 미머지 PR 이 자동 rebase + 자동 retarget 되어 맨 아래로 내려온다 |
| 머지된 브랜치 정리를 따로 수행 | gh stack sync --prune 이 머지된 PR 브랜치를 정리 |
⚠️ 대체되는 것은 1번(갱신)뿐이다 — 2번(갱신된 head 재검증)은 그대로 유효하다.
runPrePushGate()재실행(또는 갱신된 head 의 CI green 확인)은 스택 모드에서도 생략하지 않는다. 더구나 스택의 trunk 를 저장소 기본 브랜치가 아닌 브랜치로 잡을 때(이 커맨드는 거의 항상myBranch를 trunk 로 쓴다) 어느 CI 워크플로가 몇 번 도는지는 문서에 명시가 없다 — 재검증 근거를 "층별 CI green" 에 기대지 말고runPrePushGate()재실행을 기본으로 둔다.
⚠️ CI 비용 맞교환 — 기본 브랜치 대상 PR 로 트리거되는 CI 는 스택의 모든 층에서 돈다. 즉 스택을 켜면 branch-hierarchy 의 절감(중간 계층 PR 에서는 CI 를 돌리지 않는다)이 무효화된다. 층별 CI 신호를 얻는 대신 CI 비용 절감을 포기하는 맞교환이며, 비교 표는 stacked-prs 의 "CI 비용 맞교환" 절에 있다.
비대화형 배치 실행에서의 명령 시퀀스#
이 커맨드는 헤드리스로 돌기 때문에 대화형에 빠지는 형태를 쓰지 않는다(전수 표는 stacked-prs 의 "비대화형 / 무인 실행 주의" 절):
# trunk 는 저장소 기본 브랜치가 아니라 **이 레벨의 myBranch** 다 — -b 를 반드시 명시한다.
# ⚠️ 인자 없이 `gh stack init` 을 실행하면 대화형이다. 무인 실행에서는 브랜치를 함께 준다
# (인자 순서에 대한 명시는 문서에 없다 — bottom→top 이 명시된 것은 `gh stack link` 뿐이다).
gh stack init -b " ${myBranch} " task/101-entity task/102-data task/103-presentation
# push + PR 생성/갱신. --auto 는 에디터를 생략하며 **draft** 로 만든다.
# 바로 리뷰를 받아야 하면 --open 을 함께 준다.
gh stack submit --auto
# 상태는 항상 기계 판독으로 읽는다 (pager 를 통과하는 사람용 출력을 파싱하지 않는다).
gh stack view --json
# base 가 움직였을 때(앞 층 머지·trunk 갱신) — 위 " 머지 큐 루프 1번 " 의 대체물.
gh stack sync --prune
# 머지 — auto-merge 미지원이므로 `gh pr merge --auto` 를 쓰지 않는다. -y 로 확인 프롬프트 생략.
gh stack merge " ${bottom_pr} " --squash -y{inherited} 전파 · 시도 대장 · BLOCKED 표시 · 완료성 Hard Gate 3회 · 최종 머지 승인은 스택
모드에서도 그대로다. 스택은 브랜치 base 관리 방식만 바꾸며, 이 커맨드의 게이트를 하나도
면제하지 않는다.
⚠️ 스택 모드에서도 이슈 자동 종료를 가정하지 않는다. 스택 머지 시
Closes #N이 각 층에 대해 어떻게 동작하는지는 문서에 명시가 없다. 아래 "Issue Closure & Post-Merge Sync" 의 명시적 close + 검증 + 폴백을 스택 모드에서도 층마다 그대로 수행한다.
사후 결합 — Orca 워크트리에서 이미 만들어진 브랜치·PR 묶기#
이 커맨드는 형제를 워크트리에서 먼저 착수하므로, 브랜치와 PR 이 스택보다 먼저 생기는 경우가
흔하다. 처음부터 init/add 로 쌓는 경로가 안 맞을 때는 gh stack link 로 사후에 묶는다 —
GitHub API 만 호출하고 로컬 추적을 만들지 않으므로 각 워크트리의 로컬 상태를 건드리지 않는다:
# ⚠️ 전제: 아래 두 Story 는 #105 가 #101 의 산출물(목록 화면·라우트)을 전제하는
# **순차 의존** 관계임이 위 판별 표에서 확인된 경우다. 독립 형제라면 link 하지 않는다.
# 인자는 반드시 bottom → top 순서. 브랜치는 자동 push 되고, 기존 PR 은 재사용되며,
# base 가 잘못 잡혀 있으면 교정된다.
gh stack link --base " ${myBranch} " story/101-author-list story/105-author-detail
gh stack view --jsonlink는 로컬 추적을 만들지 않는다 — 로컬에서 스택 명령을 쓰려면gh stack checkout <pr-number>로 셋업을 따로 받는다.- 독립임이 확인된 형제는
link하지 않는다 — 인위적 직렬화만 생긴다. link도 위 "3. 디스패치" 의 exit 8 주의를 그대로 받는다: 한 곳에서만 실행한다.
폴백 — 스택 모드를 쓸 수 없을 때#
gh stack 이 exit 2(스택 아님/스택 없음) 또는 exit 9(Stacked PR 미활성)로 떨어지면 스택
모드를 포기하고 위 "Orca Parallel Dispatch" 의 기본 경로로 폴백한다. 기본값이 그것이므로
작업은 막히지 않는다 — skills/agent-teams/SKILL.md 의 Fallback Principle 과 같은 성격이다
(기능 차이 없음, 방식만 다름). 나머지 exit code 별 조치는
stacked-prs 의 "실패 모드" 절이 SoT 다.
Unattended & Approval Contract#
AskUserQuestion 은 디스패치된 워크트리 안에서 무효다 — 그 세션에 답할 사람이 없다
(rules/orchestration-graph.md §2). 그래서 이 커맨드의 사람-게이트는 최상위 호출에서 취해
아래로 전파하거나, 취할 수 없으면 선언된 무인 기본값으로 강등된다. 아래가 워커 안에서
도달 가능한 AskUserQuestion 자리의 전수 목록이며 유일한 정의다:
| # | 자리 | 무엇을 묻나 | 무인(--unattended) 기본값 | 남겨야 하는 내구 기록 |
|---|---|---|---|---|
| ① | Phase 0.3-4 | 대상 이슈를 못 찾음 — 번호 확인 | 중단(추측해서 진행하지 않는다) | 호출자에게 [INCOMPLETE: issue_not_found] 보고 |
| ② | Phase 0.5-3 (pre/post) | MAJOR-DRIFT — 그래도 계속? | 경고 남기고 계속 (go.md --auto 정의와 동일) | 이 레벨 PR body ## Seed Alignment + 컨테이너 이슈 코멘트 |
| ③ | Phase 1-2a | Initiative/Project/Epic 이 children 0개 — 맞나? | leaf 로 진행하지 않고 정지 (오판 시 자식 위에서 닫는 #3451 경로) | BLOCKED('childless_container_unconfirmed') + 보드 + 이슈 코멘트 |
| ④ | 위 "2. 병렬 계획 승인" | 워크트리 N개 스폰해도 되나 | 순차로 강등(--no-parallel) — 무승인 스폰 금지 | degrade 로그 + 이 레벨 PR body ## Dispatch 1줄 |
| ⑤ | 위 "5.2 Descope / Waive" | 영구 BLOCKED child 를 제외할까 | waive 하지 않는다 — child 는 blocked 유지, 이 레벨은 완료성 게이트에서 정지 | 시도 대장 + child 이슈 코멘트 + 호출자에 [INCOMPLETE: children_blocked] |
| ⑥ | Phase 3-5 | 최종 머지 승인 | --merge=pre-authorized 가 함께 지정된 경우에만 머지. 아니면 PR 을 열어 둔 채 정지 | PR body + 호출자에 [INCOMPLETE: merge_approval_unavailable] |
| ⑦ | leaf 위임 구간(/cc-dev:run) | 열린 PR 충돌(Step 0.5) · Agent Teams 스폰 승인 · --skip-tests/--skip-bdd · 디자인 에스컬레이션 4종 · Step 7.3 도구 부재·repair 잔존 · Step 12 머지 승인(전수 8종) | commands/run.md 의 "Unattended & Approval Contract" 표(①~⑧) 가 SoT — 여기서 재정의하지 않는다 | 같음(그 문서의 규정) |
- ④는 스폰 승인이고 ⑥은 머지 승인이다 —
--merge=pre-authorized는 ⑥만 면제하며 ④를 절대 대신하지 않는다(rules/orchestration-graph.md§4 매트릭스: Orca 행의 "스폰 전 사람 승인 = 항상,--merge=pre-authorized도 면제 못 함"). - 무인 강등은 하드 실패가 아니다 —
skills/agent-teams/SKILL.mdFallback Principle 그대로 기능 차이 없이 소요 시간만 늘어난다. 단 ①③⑤⑥은 정지이며, 정지도 내구 기록을 남긴다. - ⛔ 점유(
[INCOMPLETE: issue_occupied])는 이 표에 없다 — 질문이 아니기 때문이다. 이 레벨이 이미 다른 세션에 잡혀 있으면(claimStatus === "other-live") 사람에게 묻지 않고 착수하지 않는다. 무인이라고 완화되지도,--merge=pre-authorized로 면제되지도 않는다. child 단위로 발생하면SKIPPED-OCCUPIED로 그 child 만 빼고 형제를 계속한다 (../rules/zenhub-conventions.md→ Work Claim Contract).
Flow (규범 선언 — 이 흐름의 유일한 규범 소스)#
표기법·게이트 tri-state·루프 계약 7필드·기질 매트릭스는 rules/orchestration-graph.md 가 SoT다
(여기서 재서술하지 않는다). 아래 블록이 이 흐름의 규범 선언이고, 이 문서의 Phase 표·
"Flow Diagram" ASCII 박스·"MCP Call Order" 의사코드·Verification Checklist 는 모두 그 파생
뷰(비규범) 다 — 어긋나면 블록이 이긴다(같은 문서 §6). 그 문서 §5.1 의 Phase 2–3 예시는 이
블록의 부분 발췌이며, 어긋나면 여기가 정본이다(노드 id 는 양쪽이 같은 이름을 쓴다).
기질은 변경 없음 — Orca 워크트리 디스패치가 그대로 정답이다: 형제 하나하나가 보드 상태를 가진 장기 대화형 세션이고 머지 전에 사람 승인이 필요한데, Workflow 도구에는 그 게이트가 없다 (같은 문서 §4.1 결정 절차 3·5번). 이번 변경은 폭을 좁히고(width cap) 실패 경로를 채우는 것뿐이며 새 병렬성을 도입하지 않는다.
B0 ACT Phase 0 tooling preflight (GD-02)
B0.g GATE gh 인증 (머지 전에 실패) verdict:gh auth status exit undet:fail fail:HALT
B0.3 ACT Phase 0.3 레벨 판별 (getIssueTypes)
B0.3g GATE 대상 이슈 존재 verdict:searchLatestIssues 히트 undet:fail fail:HALT
B0.r ACT 고아 워크트리 스윕 (미회수 따라잡기) writes:orca:worktree,ledger skip:--keep-worktrees||!orca
B0.5p ASK Phase 0.5 pre-batch seed 정렬 options:계속|중단 unattended:계속+기록
B1 ACT Phase 1 leaf/container 분기 판정
B1.g GATE listChildren(self).known verdict:known undet:fail fail:HALT
B1a ASK children 0개가 맞나 (컨테이너 타입만) options:leaf 진행|중단 unattended:정지
B1b ACT /cc-dev:run {n} 위임 (검증된 leaf) tier:standard # 이 실행은 여기서 끝난다
B1.5 ACT Phase 1.5 자기 브랜치 + In Progress cascade writes:git:branch,zenhub:pipeline,claim:acquire
B1.5b GATE 브랜치 확보가 원격 권위 조회였나 verdict:git fetch --prune & & ls-remote " {prefix}/{n}-* " 로 조회 후 확보 undet:fail fail:HALT
B2ap ASK 병렬 스폰 계획 승인 (최상위 1회, 전파) options:승인|순차로 unattended:~~ > 순차
B2a.c GATE child 점유 확인 (디스패치 직전) verdict:claimStatus(child)!== " other-live " undet:브랜치/PR 활동으로 강등 fail:B2a.q
B2a FORK Phase 2-a 독립 형제 병렬 착수 isolate:worktree width:3 tier:standard
B2a.q LOOP 슬롯 회수 + 대기큐 드레인 (deferred 로그) contract:L-B2a
B2a.g GATE listChildren(child).known verdict:known undet:fail fail:B2c
B2a.1 ACT child 처리 (batch 재귀 | run) own:child-worktree tier:standard
B2w WAIT worker_done 대기 timeout:4h on-timeout:BLOCKED( ' worker_timeout ' )+형제 계속
B2b JOIN 머지 꼬리 mode:stream serialize:1 order:worker_done-arrival because:myBranch 충돌 방지
B2b.q LOOP 머지 큐 (갱신→재게이트→머지) contract:L-B2b
B2b.g GATE 갱신된 head 재검증 verdict:runPrePushGate() exit==0 || ci.bucket=== " pass " undet:fail fail:B2b.q
B2b.r ACT 머지된 child 워크트리 회수 (큐 5번) writes:orca:worktree,ledger skip:--keep-worktrees||!orca because:슬롯·디스크는 머지 시점에 반납된다
B2c LOOP stall ladder + 시도 대장 (child 단위) contract:L-B2c
B2c.w ASK descope / waive 결정 options:descope|waive|차단 유지 unattended:차단 유지
B2d GATE Phase 2-d 완료성 Hard Gate ① mode:barrier because:cross-item completeness, fresh re-query, no cache
verdict:mayClose(openChildrenStatus) || allOpenWaived(ledger) undet:fail fail:HALT
B3 ACT Phase 3 컨테이너 최종 PR + Review/QA writes:github:pr,zenhub:pipeline,claim:release(종료 시) own:lead
B3.5 ACT CI 대기 ∥ 작업내역 (run.md Step 10.5) extern:true
B3.9 GATE 완료성 Hard Gate ② (머지 직전) verdict:mayClose(openChildrenStatus) || allOpenWaived(ledger) undet:fail fail:HALT
B3.9b GATE PR base retarget (branch-hierarchy R7) verdict:baseRefName==4.95에서 재해석한 myBaseBranch(조회만) & & merge-base --is-ancestor origin/{base} origin/{PR head} & & rev-list --count origin/{base}..origin/{PR head} > 0 undet:fail fail:HALT skip:스택모드
B3.12 ASK 최종 머지 승인 options:승인|중단 pre:--merge=pre-authorized unattended:pre 없으면 정지
B3.6 ACT squash merge + close 검증 + 폴백 writes:github:issue,zenhub:state
B3.66 GATE 완료성 ③ (종료 직후 · 복구 전용) verdict:openChildrenStatus!== " open " undet:warn(머지 후 — 막을 대상이 이미 없다) fail:B3.66r
B3.66r LOOP 재오픈 복구 + 남은 자식 재개 contract:L-B3.66r
B3.67 ACT 부모 완료 신호 (자동 병합 아님) writes:github:comment
B3.8 ACT post-merge base 최신화 + submodule sync writes:git:base
B3.r ACT 이 레벨 워크트리 반납 표시 (자기 제거 금지) writes:orca:worktree-card because:회수는 만든 쪽(부모)의 몫이다
B0.5q ASK Phase 0.5 post-batch seed 정렬 options:계속|보류 unattended:계속+기록
B0 -- > B0.g -- > B0.3 -- > B0.3g -- > B0.r -- > B0.5p -- > B1 -- > B1.g
B1.g -- > B1a -- > B1b # children===0 (검증된 leaf) 경로 — 배타적, 여기서 종료
B1.g -- > B1.5 # children > 0 (container) 경로 — 배타적
B1.5 -- > B1.5b -- > B2ap -- > B2a.c -- > B2a -- > B2a.q -- > B2a.g -- > B2a.1 -- > B2w -- > B2b -- > B2b.q -- > B2b.g
B2b.g -- > B2b.r -- > B2d -- > B3 -- > B3.5 -- > B3.9 -- > B3.9b -- > B3.12 -- > B3.6 -- > B3.66 -- > B3.67 -- > B3.8 -- > B3.r -- > B0.5q
B2c -- > B2c.w -- > B2d # 영구 BLOCKED child 가 남았을 때만 지나는 자리
B0.5p ~~ > B1 on:!LOCKED seed record:log( " seed 없음/DRAFT → skip " ) # 구조적 해당 없음
B0.5p ~~ > B1 on:--unattended & & MAJOR-DRIFT record:PR body(## Seed Alignment)+이슈 코멘트
B1a ~~ > HALT on:--unattended record:blockIssue( ' childless_container_unconfirmed ' )(board+claim:release)+이슈 코멘트
B2ap ~~ > B2a.q on:--unattended||--no-parallel record:PR body(## Dispatch)+degrade 로그 # width:1 강등, 형제 처리는 그대로
B2w ~~ > B2b on:worker_timeout record:blockIssue( ' worker_timeout ' )(board+claim:release)+시도 대장
B2a.c ~~ > B2a.q on:claim==other-live record:SKIPPED-OCCUPIED(큐에서 제외, 보드·대장 손대지 않음)+형제 계속
B2c ~~ > B2b on:rung3||attempts > =3 record:blockIssue( ' unstuck_exhausted ' )(board+claim:release)+시도 대장 # 형제는 계속
B2c.w ~~ > B2d on:--unattended||waive 거부 record:시도 대장(state=blocked)+child 코멘트+[INCOMPLETE]
B3.12 ~~ > HALT on:--unattended & & !--merge=pre-authorized record:PR 유지+[INCOMPLETE: merge_approval_unavailable]
B0.r ~~ > B0.5p on:--keep-worktrees||!orca record:log(미회수 후보 목록 — 조용한 누적 금지)
B2b.r ~~ > B2d on:--keep-worktrees||!orca record:log(미회수 child 목록)+대장 worktree 행 유지
B2b.r ~~ > B2d on:reclaim.fail||!safe5 record:대장 state=leak({사유}) → 다음 배치 B0.r 이 재시도 # 머지는 되돌리지 않는다
B3.r ~~ > B0.5q on:!orca||워크트리 밖 실행 record:log(no-op — 부모가 만든 워크트리가 없다)
B2a.1 == > B2c on:child.fail bound:3(누적, 시도 대장) invalidates:B2a.1
B2a.g == > B2c on:known===false bound:1 invalidates:B2a.1
B2b.g == > B2b.q on:reGate.fail||conflict bound:2 invalidates:B2b.g
B3.66 == > B3.66r on:status=== " open " bound:1 invalidates:B2d,B3.9읽는 법 네 가지. 첫째, 구현은 width:3 으로만 벌어지고 머지는 serialize:1 로 좁혀지며,
mode:barrier 는 B2d 한 자리다 — 전수 재조회가 유일한 교차 항목 근거다. 둘째, B2b.g 가
새로 생긴 자리다: 갱신된 head 를 다시 검증하지 않으면 B2a.1 이 통과시킨 게이트는 앞 형제의
squash 이전 myBranch 를 기준으로 한 판정이다. 셋째, B2d·B3.9·B3.66 은 같은 술어를
세 번 평가하는 별개 게이트이며(그 사이에 CI·리뷰로 수 시간이 흐른다 — #3451 은 11시간),
통과는 none 하나뿐이고 unknown 은 차단이다. fail:HALT 는 종단 sink 이므로 대응 ==> 가
없다(표기법 §1 RESERVED). 넷째, width:3 을 실제로 되돌려 놓는 자리는 B2b.r 다 — 워크트리를
여는 B2a 에 짝이 되는 반납 노드가 없으면 --max-parallel 은 한 배치 안에서만 유효한 숫자가
되고, 다음 배치는 이미 찬 디스크·슬롯 위에서 시작한다. B0.r(놓친 회수 따라잡기)과
B3.r(자기 워크트리는 표시만, 제거는 부모 몫)이 그 짝을 세션 경계 너머까지 닫는다.
Loop Contracts (이 흐름의 LOOP 7필드 — 값은 여기가 자기 자리다)#
L-B2a 디스패치 슬롯 + 대기큐 (위 " 3. 디스패치 " )
inv: inFlight ≤ width · 대기큐 ∪ inFlight ∪ 완료 = 승인된 병렬 후보 전체
(합집합이 깨지면 형제가 조용히 사라진 것이다 — 즉시 실패)
prog: pending = 대기큐 길이, 슬롯 회수마다 강한 감소
no-prog: 슬롯이 비었는데 pending 이 안 줄면 그 후보를 순차 경로로 넘기고 Rung 2 로 올린다
term: pending === 0 & & inFlight === 0
budget: width = min(후보 수, --max-parallel 기본 3) 동시 · 후보당 착수 시도 2회
· 워커당 wall-clock 4h(--worker-timeout)
exhaust: 착수 2회 실패 → 순차 경로로 강등 → 그래도 실패면 BLOCKED( ' dispatch_failed ' ) + 형제 계속
resume: `ORCA status --json` 의 실재 워크트리 + 시도 대장 + 각 child 의 GitHub state 로 위치 판정
(이미 Closed 인 child 는 착수하지 않는다 — 멱등)
⚠️ **이미 머지·Closed 인 child 의 잔존 워크트리는 재개 대상이 아니라 회수 대상이다**
(대장 state 가 `reclaimed` 가 아닌 행) — B0.r 스윕이 처리한다. 거기에 워커를 다시
띄우면 닫힌 이슈를 두 번 돌린다
log: " slot 2/3: #123 착수 (pending 4→3) " · ⏸️ deferred(cap=3) 는 **이슈 번호·제목으로** 남긴다
· 스폰 직후 `worktree={id}` 를 대장에 적은 사실을 한 줄로 (회수의 유일한 명단)
L-B2b 머지 큐 (위 " 4. 머지는 항상 직렬화 " )
inv: worker_done 이 도착한 child 만 큐에 있고 항상 1개만 머지 중(serialize:1)
· 머지 직전 head 는 현재 myBranch 를 조상으로 갖는다
· **회수는 머지된 항목에만 일어난다** — dequeue(BLOCKED) 된 항목의 워크트리는 보존
prog: queued = 큐 길이, 항목 처리(머지 또는 dequeue)마다 강한 감소
no-prog: 같은 child 가 같은 이유로 재게이트 2회 실패 → rung C2(dequeue + BLOCKED)
term: queued === 0
budget: 항목당 갱신+재게이트 2회 · 충돌 rung C1 2회 · 큐 길이는 형제 수(증가 없음)
exhaust: BLOCKED( ' merge_conflict ' ) 로 dequeue + 나머지 큐 계속 (이미 머지된 것은 되돌리지 않는다)
resume: `gh pr list --base {myBranch} --state all` + `git log {myBranch}` 재실측으로 어디까지
머지됐는지 판정 (큐 메모리는 /clear 를 못 넘는다). 머지된 child 재머지 금지 — 멱등
· 머지는 됐는데 대장 state 가 `reclaimed` 가 아닌 행 = **회수 미완** → B0.r 스윕이 잇는다
(이미 사라진 워크트리 id 는 " 없음 " 이 성공이다 — 대장만 맞춘다)
log: 🔀 queue 전이 한 줄씩(enqueued/rebased/re-gate/merged/dequeued) + 잔여 큐 길이
🧹 reclaim 전이 한 줄씩(removed/kept({사유})/leak({사유})) — 조용한 정리·조용한 누수 금지
L-B2c child stall ladder + 시도 대장 (위 " 5. Stall Ladder " )
inv: 한 child 의 실패가 형제 처리를 멈추지 않는다 · 보드 상태가 실제 상태와 일치한다
prog: attempts(child) 단조 증가 **+ last_cause 가 의미상 달라져야 한다**
no-prog: 의미상 동일 실패면 같은 예산을 더 쓰지 않고 Rung 2 로 올린다
term: child 가 Closed · 또는 누적 attempts 3 소진 → `blocked` 고정
budget: inner: Rung 1 재시도 2회 / Rung 2 리프레임 1회 · outer: child 당 **누적 3회**
(batch 재실행을 가로질러 시도 대장에서 합산 — 대화 카운터는 예산이 아니다)
exhaust: Rung 3 — blockIssue(child, ' unstuck_exhausted ' ) (보드 + 점유 해제) + 형제 계속. 이 레벨의 마무리는
§5.2 descope/waive 로만 열린다(자동 waive 없음)
resume: 시도 대장 마커 코멘트 + child 의 GitHub state/파이프라인 재조회
log: " 🔁 #123 attempt 2/3(누적) cause={의미 요약} " + 탈락 시 이유와 대장 state 전이
L-B3.66r 종료 후 복구 + 재개 (Phase 3-6.6)
inv: 머지는 되돌리지 않는다 · `gh issue reopen` **이후에만** In Progress 로 옮긴다
prog: 남은 open child 수, 재개 1회당 강한 감소
no-prog: 재개했는데 open 수가 그대로면 그 child 를 §5.2 로 보낸다
term: openChildrenStatus(issue) === " none " (또는 남은 open 전부 `waived`)
budget: reopen 1회 / batch 실행 · 재개는 `/cc-dev:batch {n}` 재실행 1회당 1 사이클
exhaust: BLOCKED + 사람 호출 — 컨테이너를 열린 자식 위에서 닫힌 채로 두지 않는다
resume: openChildrenStatus 재조회 + 시도 대장 (reopen·코멘트·파이프라인 이동 모두 멱등)
log: " ♻️ #20 reopen: 남은 자식 #21,#22 " + 세 완료성 체크포인트의 **타임스탬프**Workflow#
Phase 0: Tooling Preflight & Recoverability (GD-02)#
이 알고리즘을 시작하기 전에 한 번 필수 도구를 점검하고, 중단 시 재개 방법을 명시한다.
1. Tooling preflight (진입 전 1회):
- command -v gh & & gh auth status → 미인증이면 즉시 중단(머지 전에 실패, 계층 중간 아님)
- command -v melos / dart / flutter → 부재 시 /cc-dev:run " Step 0 Degradation Contract " 를 따른다(우회 + 경고)
- Orca 가용성 확인 (위 " Orca Parallel Dispatch — 0. 가용성 확인 " ) — 실패해도 순차 폴백일 뿐 하드 실패 아님
- 중복 착수 확인은 각 leaf 의 /cc-dev:run " Step 0.5 Duplicate Work Preflight " 가 담당한다
(컨테이너 레벨에서 미리 검사하지 않는다 — leaf 착수 직전이 가장 정확하다)
- **대화 가능성 판정 1회**: `--unattended`(또는 `/cc-dev:go --auto` 전파)이면 이 실행 전체를
무인으로 취급한다 → 위 " Unattended & Approval Contract " 표의 기본값이 적용되고, 스폰 승인이
필요한 자리는 `--no-parallel` 로 강등된다. 이 판정은 하위 재귀 호출에 그대로 전파한다.
2. Resume 계약 (멱등):
- 이미 Closed 인 child 는 건너뛴다(Phase 2에서 상태 확인).
- ⭐ **놓친 워크트리 회수도 여기서 이어받는다**(`B0.r` — 대상 이슈가 확정된 Phase 0.3 직후,
대장을 읽을 수 있게 된 시점에 1회). 세션이 죽거나 `/clear` 로 끊기면 머지 시점의 회수가
통째로 날아가므로, 회수는 **이벤트 하나에만 매달지 않는다**. 절차는
[orca-worktree-lifecycle](../skills/orca-worktree-lifecycle/SKILL.md) → " 고아 스윕 " .
- 중간 실패 시 `/cc-dev:batch {issue_number}` 를 다시 실행하면 마지막으로 머지된 child 다음부터 이어서 처리한다.
- 따라서 한 child 실패가 이 레벨 전체를 " 절반-머지 " 로 영구 고착시키지 않는다.
- ⭐ **재개는 같은 머신일 필요가 없다.** 다른 컴퓨터·다른 세션에서 재실행해도 같은 계층 브랜치를
이어받는다 — 단, 그러려면 브랜치 조회가 **원격 권위 조회 + 번호 글롭 + 브랜치 대장**이어야 한다
(Phase 1.5-0/2, branch-hierarchy R1~R3). 로컬 remote-tracking ref 나 재계산한 slug 에 의존하는
순간 재개가 " 새 브랜치 생성 " 으로 갈라진다.배경: 도구 1개 부재나 한 child 실패가 전체를 중간에 좌초시키지 않는다(GD-02). 아래 Phase 2 의 post-merge 동기화 실패는 경고 후 계속한다(중단 아님).
⚠️ children 조회는 항상 GD-04 방식 — 아래 Phase 1 의 children 조회와 Phase 2-d 완료성 Hard Gate 모두
mcp__zenhub__searchLatestIssues({query: "parent:${id}"})단독으로 판정하지 않는다. 이 툴은 "최신 20개 이슈"만 훑고 그 안에서 필터링해, 오래 전 만들어진 뒤 안 건드린 sub-issue 는 창 밖으로 밀려나 거짓으로 0건 처리된다 —commands/run.mdStep 0.6 (GD-04) 의gh api graphqlsub-issues 조회를 completeness source of truth 로 쓰고, ZenHub 검색은 pipeline/issueType 같은 부가 메타데이터 보강 조회에만 병행한다. (실사고: Epic #3451 이 이 결함으로 실제 sub-issue 5개를 못 찾아 중복 이슈 5개를 새로 만들고 원본을 고아로 남겼다 — 상세는run.mdGD-04 참조.)
Phase 0.3: Entry Issue Resolution (레벨 판별)#
1. `getWorkspacePipelinesAndRepositories()` + `getIssueTypes({repositoryId})` 로
워크스페이스의 이슈 타입 레벨 테이블 확보 (Initiative(1)/Project(2)/Epic(3)/
Feature·Bug·Task(4)/Sub-task(5) — 워크스페이스에 없는 레벨은 생략될 수 있다,
`rules/zenhub-conventions.md` " Issue Type Hierarchy " 참조).
2. `searchLatestIssues({query: " #{issue_number} " })` 로 대상 이슈 + `issueType` + `parentIssue` 조회.
3. 레벨에 맞는 브랜치 prefix 결정 — `resolveBaseBranch()`(commands/run.md)와 동일 매핑:
Initiative → initiative/ · Project → project/ · Epic → epic/ ·
parent 있는 Feature/Bug/Task → story/ · Sub-task → task/
4. 못 찾으면(이슈 없음) 사용자에게 정확한 번호를 확인받고 중단.Phase 0.5: Optional Seed-Alignment Checkpoint (D4 — opt-in, no-op on absence)#
이 배치는 길게 도는 자동화라, 작업 묶음이 원래 기획에서 조금씩 표류(drift) 하기 쉽다. 그래서 docs/seed-spec-*.md 가 존재하고 Status: LOCKED 일 때에 한해, 시작 직전(pre-batch) 과 최종 병합 직후(post-batch) 에 정렬 점검을 한 번씩 실행한다. 이 점검은 /cc-spec:status 프로토콜을 그대로 재사용한다 — 읽기 전용(아무 파일도 쓰지 않음), 최신 LOCKED docs/seed-spec-*.md 로드, 선택적 --against {file|dir|PR}.
0.5-1. Seed 탐지 (게이트):
- SEED=$(ls -1 docs/seed-spec-*.md 2 > /dev/null | tail -1)
- SEED 가 없으면 → **no-op (조용히 skip)**. 경고도 차단도 없음.
- SEED 가 있으나 머리글이 `Status: LOCKED` 가 아니면(DRAFT) → **no-op + 1줄 안내**( " 기획 명세 미확정 → 정렬 점검 skip " ). DRAFT 를 강제로 잠그지 않는다.
- → 즉, 시드가 없거나 미확정인 저장소에서 `/cc-dev:batch` 는 **절대 하드페일하지 않는다**(submodule 동기화 no-op 와 동일한 graceful degradation 형태).
0.5-2. 정렬 점검 (LLM-SEMANTIC, /cc-spec:status 재사용):
- 대조 대상:
- pre-batch → 이번 대상 이슈 아래 **계획된** 하위 이슈 집합(제목/본문/AC)
- post-batch → 최종 병합 직후 **완료(Closed)된** 이슈 집합 + 이 레벨 브랜치 누적 변경(diff/PR)
- `/cc-spec:status --against {계획/완료 이슈 집합 또는 최종 PR}` 를 실행해 LLM-SEMANTIC 정렬 리포트를 받는다:
- **AC-ID 상태 echo** — Seed S4(Acceptance Boundaries)의 각 합격 기준을 `AC-01`, `AC-02` … 로 식별하고 PENDING / IN-PROGRESS / MET / FAILED / WAIVED 로 표기.
- **목표 정렬 판정** — 작업 묶음의 의도가 Seed S1(Core Problem)과 같은 방향인지 ALIGNED / MINOR-DRIFT / MAJOR-DRIFT 로 판정.
- **제약 위반 플래그** — 작업이 위반하는 것으로 보이는 Seed S2(Immutable Constraints) 항목을 나열.
- ⚠️ **의미 기반 판단만 사용한다(절대 어휘/토큰 일치 아님)**: Seed 와 이슈/AC 텍스트는 **한국어 우선**이라 공백·토큰 겹침 매칭은 조사·띄어쓰기에서 깨진다. drift/정렬/AC 매칭은 모두 **LLM 의미 판단**으로 수행한다(`/cc-spec:status` 와 동일 원칙).
0.5-3. 판정별 행동:
- ALIGNED / MINOR-DRIFT → 리포트만 출력하고 그대로 진행(차단 아님).
- **MAJOR-DRIFT → 경고 후 일시정지(필수)**: 어긋난 지점을 요약해 보여주고 `AskUserQuestion` 으로 " 기획과 크게 어긋남 — 그래도 계속할까요? " 확인을 받는다.
- pre-batch: 사용자가 " 중단 " 선택 시 배치를 시작하지 않고 종료(이슈/명세 조정 후 재실행). " 계속 " 선택 시 사유를 로그에 남기고 Phase 1 로 진행.
- post-batch: " 중단 " 선택 시 이 레벨의 종료를 보류(이 레벨 PR 은 이미 병합됐으므로 추가 후속 이슈로 표류 항목을 추적). " 계속 " 선택 시 그대로 마무리.
- 이 점검은 **품질 게이트가 아니라 방향 점검**이다 — 테스트/lint/리뷰 Critical 처럼 hard fail 하지 않고, **MAJOR-DRIFT 에서만 사용자 확인으로 멈춘다**.
0.5-4. 멱등/재개: 읽기 전용이라 부작용이 없다. `/cc-dev:batch {issue_number}` 재실행 시 pre-batch 점검도 다시 도는 게 정상이며, 이미 Closed 인 child 는 Phase 2 에서 건너뛴다.배경(D4): 길게 도는 batch 가 child 씩 처리되는 동안 누적되면 "개별 PR 은 통과했지만 전체가 원래 기획 목표에서 벗어난" 상태가 될 수 있다. 이 checkpoint 는 그 표류를 시작 시점과 마무리 시점에 의미 기반으로 한 번씩 비추는 거울이다. 단, 기획(Seed)이 없는 저장소에서도 batch 는 그대로 동작해야 하므로 시드 부재/미확정 시 완전한 no-op 으로 둔다(opt-in). 점검 로직은
/cc-spec:status(D3) 에 위임하며 여기서 어떤 비교 알고리즘도 새로 구현하지 않는다.
Phase 1: Leaf/Container Branch Point#
이 이슈 자신이 leaf(하위 이슈 없음)인지 container(하위 이슈 있음)인지부터 판정한다 —
둘은 서로 배타적인 경로이며, container 전용 단계(Phase 1.5~3)는 leaf 에는 적용하지 않는다
(leaf 인데 batch.md 가 별도로 브랜치를 만들면 /cc-dev:run 이 만드는 브랜치와 중복/충돌한다).
0. (Phase 0.5) Pre-batch seed-alignment checkpoint — LOCKED seed 있으면 LLM-SEMANTIC 정렬 점검(MAJOR-DRIFT 시 확인 후 진행), 없으면 no-op skip
1. `{ children, known } = listChildren(issue_number)` 로 직계 자식을 **전수** 조회한다 —
**GD-04 방식**(`run.md` Step 0.6): GitHub 네이티브 `subIssues`(`totalCount`/`nodes`)가
completeness source of truth 다. ZenHub `searchLatestIssues({query: " parent:{issue.id} " })`
단독 판정 금지( " 최신 20개 " 창 밖의 sub-issue 를 놓친다) — pipeline/issueType 같은 부가
메타데이터가 필요하면 확인된 각 자식 번호로 `searchLatestIssues({query: " #{n} " })` 를 보강
조회한다. 이 조회의 단일 정의는 `rules/zenhub-conventions.md` → " Child Enumeration Contract " .
- `known === false` (조회 실패) → ⛔ **여기서 중단**한다. 조회 실패를 " 자식 없음 " 으로 읽으면
컨테이너를 leaf 로 오판해 `/cc-dev:run` 이 자식을 무시하고 컨테이너를 닫아 버린다
(`#3451` 사고 경로). `gh auth status` 확인 후 재실행하라고 안내하고 정지.
1.5. **기존 자식 재사용 계약** — children 이 1개 이상이면 **그 이슈들을 처리한다.** 같은 작업을
담은 새 이슈를 만들어 그것만 처리하는 것은 금지다(`rules/zenhub-conventions.md` →
" Existing-Children Reuse " , `run.md` Step 0.6 과 같은 규칙). 본문·AC 가 낡았으면
`updateIssue` 로 **그 자식을 갱신**한다.
2. children.length === 0 **이고 `known === true`** (검증된 leaf) 이면:
a. Initiative/Project/Epic 레벨이면 → ⚠️ 판정 불가: " 자식이 없는 게 맞습니까? " 를
AskUserQuestion 으로 확인받은 뒤에만 진행 (Story/Sub-task 는 확인 없이 정상 진행 —
leaf 가 기본값이다).
b. `/cc-dev:run {issue_number}` 를 그대로 호출한다 — batch.md 는 여기서 브랜치를 별도로
만들지 않는다. `/cc-dev:run` 자신의 Step 4 `resolveBaseBranch()` 가 이 이슈의 레벨에
맞는 브랜치(parent 있으면 그 브랜치 기반, 없으면 development 기반)를 스스로 만들고,
Step 9~12.5 가 구현 → 테스트 → lint/DCM → 코드리뷰 게이트 → PR → squash merge →
close+검증까지 전부 수행한다.
c. 반환: 이 이슈는 완료. **이 실행은 여기서 끝난다** — 아래 Phase 1.5/2/3 는 container
케이스 전용이다.
3. children.length > 0 (container) 이면 → Phase 1.5 로 진행.Phase 1.5: Own Branch Creation (container 전용)#
0. ⭐ **원격 동기화 먼저**: `git fetch origin --prune` — 이 배치가 **다른 머신·다른 세션이 만들어 둔
브랜치를 이어받는** 경우가 정상 경로다(브레이크다운은 이슈만 만들고 브랜치는 만들지 않으므로,
project/ 브랜치는 " 그 아래 Epic 을 처음 진행한 세션 " 이 만들어 둔 상태다). fetch 없이 origin/*
를 읽으면 그 브랜치가 " 없음 " 으로 보이고 **가짜 부모 브랜치를 하나 더 만든다**.
1. myBaseBranch 결정: parentIssue 가 있으면 그 브랜치를 최신 pull(없으면 재귀적으로 생성 —
resolveBaseBranch() 와 동일 절차), 없으면 development 를 최신 pull
2. 이 이슈 전용 브랜치 확보: **먼저 찾고, 없을 때만 만든다** —
`git ls-remote --heads origin " refs/heads/{levelPrefix}/{issue_number}-* " ` (번호 글롭)
+ 브랜치 대장(` < !-- cc-dev:branch-registry -- > `) 조회. 찾으면 그대로 체크아웃(멱등 재개),
**2개 이상 나오면 R4 tie-break** 로 하나만 채택(대장 기록 + 경고 + 탈락본 삭제 금지),
없으면 `{levelPrefix}/{issue_number}-{slug}` 로 만들고 **커밋이 0개여도 즉시
`git push -u origin`** 한 뒤 대장에 이름을 기록한다.
⛔ **재계산한 전체 이름으로 완전일치 조회를 하지 않는다** — slug 는 제목을 로마자로 옮긴 것이라
세션마다 갈린다(`저자 관리` → `jeoja-gwanli` / `jeoja-gwanri`). 이 한 줄이 어긋나면 같은
Project 에 계층 브랜치가 둘 생기고, Epic 이 가짜 부모로 머지돼 진짜 Project 브랜치는 빈 채
남는다. 절차 전문(R1~R7)의 SoT 는
[branch-hierarchy](../skills/branch-hierarchy/SKILL.md) → " 계층 브랜치 해석 계약 " .
3. 이 이슈를 In Progress 파이프라인으로 이동 — 이 이슈 자신의 parentIssue(Project/Initiative
등)가 아직 착수 전 칸(New Issues/Icebox/Product Backlog/Sprint Backlog)에 있다면
조부모까지 함께 cascade한다(`cascadeStartToParents`, `agents/dev/issue-state-agent.md`
참조) — `/cc-dev:batch`가 중간 레벨 이슈로 직접 호출된 경우에도 그 위 계층이 " 아직
손 안 댄 일 " 처럼 보드에 방치되지 않도록 한다.⚠️ 3번은 문장이 아니라 실행이다 — 컨테이너 레벨이 보드에서 움직이지 않는 것("Epic 개발 진행 중인데 파이프라인이 Product Backlog 그대로")의 원인이 이 단계의 누락이었다. 반드시 아래를 호출한다(멱등 — 이미 In Progress 이상이면 no-op):
⛔ 그 전에 이 레벨의 점유부터 판정한다.
claimStatus(issue.number, me) === "other-live"면 이 컨테이너는 이미 다른 batch 세션이 돌리고 있다 — 보드도 건드리지 않고[INCOMPLETE: issue_occupied]로 호출자에 보고하고 종료한다.mine(내 재개)·stale(인수)·none은 아래를 그대로 진행한다.
const workspace = await mcp__zenhub__getWorkspacePipelinesAndRepositories();
const pid = (n) = > { const p = workspace.pipelines.find(p = > p.name === n);
if (!p) throw new Error(`파이프라인 ' ${n} ' 없음. 라이브: ${workspace.pipelines.map(p= > p.name).join( " , " )}`); return p.id; };
// 3-a. 이 컨테이너 자신을 In Progress 로
await mcp__zenhub__moveIssueToPipeline({ issueId: issue.id, pipelineId: pid( " In Progress " ) });
// 3-a2. 이 **레벨**의 점유 획득 — 같은 Epic 에 batch 가 두 번 도는 것을 막는 유일한 신호다.
// (SoT: ../rules/zenhub-conventions.md → Work Claim Contract)
// ⛔ 이 점유는 **자식 착수를 막지 않는다** — 자식은 이 batch 자신이 디스패치한다.
await acquireClaim(issue, { branch: myBranch });
// 3-b. 부모 체인이 아직 착수 전 칸이면 조부모까지 함께 (agents/dev/issue-state-agent.md)
// ⛔ cascade 는 점유 대장을 쓰지 않는다 — 부모를 잡으면 형제 Epic 이 전부 막힌다.
await cascadeStartToParents(issue.number);
// 3-c. 읽어서 확인 — 무음 누락 금지
const after = (await mcp__zenhub__searchLatestIssues({ query: `#${issue.number}` }))
.find(i = > i.number === issue.number);
console.log(`📍 #${issue.number} 파이프라인: ${after?.pipelineIssue?.pipeline?.name}`);Phase 2: Recursive Child Processing (container 전용)#
a. 형제 children 중 서로 의존성 없는 것들은 **Orca 오케스트레이션으로 병렬 착수**한다
(위 " Orca Parallel Dispatch " 절차 그대로 — **동시 워크트리 ≤ --max-parallel(기본 3)**, 넘치는
형제는 대기큐 + deferred 로그, 워커당 wall-clock 4h 초과 시 `BLOCKED( ' worker_timeout ' )` 후
형제 계속. 루프 계약은 위 `L-B2a`). 의존관계가 있는 children 은 순서를 지켜 직렬 디스패치.
⛔ **라우팅 판정 앞에 점유부터 본다.** 워커를 띄우는 것은 워크트리·세션을 소비하는 일이라,
이미 남이 잡고 있는 child 를 디스패치하면 **두 세션이 같은 이슈를 각자 완주**한다
(`../rules/zenhub-conventions.md` → Work Claim Contract):
const claim = await claimStatus(child.number, me);
claim === " other-live " → 디스패치하지 않고 `SKIPPED-OCCUPIED` 로 큐에서 뺀다.
**형제는 계속**한다(`BLOCKED` 형제 처리와 같은 원칙 — 큐를 잡아 두지 않는다).
완료성 Hard Gate 에서는 **OPEN 자식과 동일 취급**이다 — 점유됐다고 컨테이너를 닫지 않는다.
claim === " stale " → 인수 기록 후 정상 디스패치 · `mine`/`none` → 정상 디스패치
⚠️ 워크트리 병렬은 **같은 계정·같은 머신**에서 형제 워커를 띄운다 — 점유 대장의 `owner` 가
`host:워크트리경로` 인 이유가 이것이다(호스트만으로는 형제끼리 구분되지 않는다).
각 child 의 라우팅은 **Phase 1 과 동일한 판정**을 쓴다 — `listChildren(child.number)` 로
`{ children, known }` 을 받아:
known === false → ⛔ 이 child 는 라우팅하지 않는다(BLOCKED 표시 후 형제 계속).
조회 실패를 leaf 로 읽으면 컨테이너가 /cc-dev:run 으로 흘러가 자식 위에서 닫힌다.
children.length > 0 (container) → /cc-dev:batch {child.number} {inherited} (이 알고리즘 재귀
적용 — 재귀 호출도 동일하게 Phase 1 의 leaf/container 판정부터 다시 거친다)
children.length === 0 (검증된 leaf) → /cc-dev:run {child.number} {inherited}
`{inherited}` 는 위 " 3. 디스패치 " 와 같은 의미다 — 이 실행이 받은 `--merge=pre-authorized`·
`--unattended` 를 그대로 옮겨 붙인다(새로 판단하지 않는다).
resolveBaseBranch()(commands/run.md)가 이미 만들어진 `myBranch`를 child의 부모 브랜치로
자동 인식하므로, 별도 --base 지정이 필요 없다.
b. **머지는 항상 직렬화**한다(병렬 구현과 별개) — 이 조인의 이름은
**`mode:stream serialize:1 order:worker_done-arrival`** 이다(barrier 아님). 워커가 끝난
순서대로 하나씩 처리하고, **머지 직전 그 child 를 현재 `myBranch` 위로 갱신한 뒤
`runPrePushGate()` 를 재실행(또는 갱신된 head 의 CI green 확인)** 한 다음
PR→squash merge 하며, 매 머지 후 `myBranch` 를 pull 해 다음 머지 전에 최신화(충돌 방지).
충돌은 rung C1/C2(`skills/merge-conflict-resolution/SKILL.md`)로 처리하고 큐 상태 전이는
전부 로그로 남긴다 — 상세·루프 계약은 위 " 4. 머지는 항상 직렬화 " 와 `L-B2b`.
⛔ **`BLOCKED` 형제는 큐를 잡아 두지 않는다** — 큐에서 빠지고 나머지 도착 순서대로 계속한다.
⭐ **머지된 child 의 워크트리는 그 자리에서 회수한다**(위 " 4.5 " ) — 슬롯·디스크가 회복돼야
대기줄의 다음 형제가 착수한다. `BLOCKED` 로 빠진 형제의 워크트리는 **보존**한다(진단 현장).
c. child 하나가 반복 실패하면 위 " Stall Ladder " 를 적용: (1) 재시도 → (2) `/cc-dev:unstuck
solo < persona > ` 리프레임 → (3) `BLOCKED( ' unstuck_exhausted ' )` 표시 후 **나머지 형제는
계속 진행**(이 이슈 전체를 막지 않음). 시도 횟수는 **시도 대장**(위 5.1)에 누적 기록되어
batch 재실행을 가로질러 예산으로 쓰이고, 누적 3회 소진 시 그 child 는 `blocked` 로 고정된다.
d. **완료성 Hard Gate** (= `rules/zenhub-conventions.md` " Parent Closure Invariant " 의 1번째 지점):
이 자리 하나만 **`mode:barrier`** 다 — `because:` 는 *cross-item completeness, fresh re-query,
no cache*(교차 항목 근거. `rules/orchestration-graph.md` §4.2 의 확인된 barrier 목록에 이미
등재). 모든 children 을 **전수** 재조회해 전부 GitHub `CLOSED` 인지 확인한다 —
**GD-04 방식 필수**(GitHub 네이티브 `subIssues` 를 근거로, ZenHub `parent:` 검색 단독 금지):
const kids = await openChildrenStatus(issue.number); // Child Enumeration Contract — tri-state
if (kids.status !== " none " ) → ⛔ 이 레벨의 PR/머지/종료 중단. BLOCKED( ' unstuck_exhausted ' )·
BLOCKED( ' worker_timeout ' )·BLOCKED( ' merge_conflict ' ) 는 **그대로 OPEN 으로 간주**한다.
`SKIPPED-OCCUPIED`(남이 잡고 있어 착수하지 않은 child)도 마찬가지다 — 남이 끝내 줄 수도 있지만
**이 실행이 확인한 것은 " 안 닫혔다 " 뿐**이다. waive 대상도 아니다(사람 결정이 없었다).
throw `#${issue_number} 미완료: ${kids.status === " unknown "
? " 자식 조회 실패(판정 불가) " : `OPEN child ${kids.open.length}개 (${kids.open.map(c= > " # " +c.number)})`}. 해결/재시도 후 batch 재실행`
⛔ **판정을 `open.length > 0` 로 쓰지 않는다.** 조회가 실패하면 결과가 `[]` 가 되고
`open.length > 0` 는 **`[]` 에서 통과**한다 — 즉 조회가 깨진 순간에 정확히 게이트가 열린다
(`#3451` 사고 경로). tri-state 에서 통과는 `none` 하나뿐이고, `unknown` 은 차단이다.
✅ **유일한 예외 — `waived`**: `status === " open " ` 이어도 **열린 자식 전부**가 시도 대장에서
`waived`(위 5.2, 사람 결정 + 내구 기록 완료)이면 통과한다. 판정 함수는 그대로
`openChildrenStatus()` 이고 새 판정 함수를 만들지 않는다 — batch 가 그 `open` 목록을 대장과
대조해 `open` / `waived` 로 분류할 뿐이다. `unknown` 은 예외 없이 차단이며, `blocked`(waive
되지 않은 것)도 차단이다. 이 예외가 없으면 영구 BLOCKED child 하나가 이 레벨을 **영구히
마무리 불가**로 고정한다(재실행할수록 같은 자리에서 같은 이유로 멈추는 livelock).
🕒 **판정마다 타임스탬프를 남긴다** — `⏱️ gate① {ISO8601} status={none|open|unknown} waived={n}`.
세 체크포인트(2-d / 3-4.9 / 3-6.6) 사이 간격이 감사 가능해야 한다(`#3451` 은 11시간이었다).
e. 전부 닫혔으면 `myBranch` 를 리뷰하고 PR 생성 (아래 " Phase 3 " 그대로 이어짐).🔀 a·b 단계의 형제가 순차 의존이면 선택형 스택 모드를 검토한다 — b 단계의 "머지 직전
myBranch위로 갱신" 루프를gh stack sync+ 자동 retarget 이 대신한다(재게이트는 그대로 유효). 판별 표·명령 시퀀스·CI 비용 맞교환·폴백은 위 "형제 처리 전략 — 병렬 워크트리 vs 선형 스택" 절, 실행 계약의 SoT 는 stacked-prs. 독립 형제는 지금처럼 Orca 병렬 워크트리로 둔다.
Phase 3: Container Finalization (이 레벨 자신의 최종 PR)#
모든 children 이 완료된 뒤 (아래에서 pid(name) 은 Phase 1.5-3 의 fail-closed 파이프라인 ID
해석기, openChildrenStatus() 는 rules/zenhub-conventions.md → "Child Enumeration Contract"):
1. ⛔ 완료성 Hard Gate — 위 Phase 2-d 그대로(`waived` 예외와 타임스탬프 로그 포함).
통과 못 하면 여기서 정지(재실행으로 재개).
1.5. ⭐ **myBaseBranch 재해석 (다른 머신이 그 사이 부모 브랜치를 만들었을 수 있다)** —
Phase 1.5 이후 children 처리로 **수 시간~수 일**이 흐른다. 그 사이 다른 머신·세션이 부모
브랜치를 만들었을 수 있고, Phase 1.5 에서 부모를 못 찾아 `development` 를 base 로 잡았다면
지금 그 값은 낡았다. `git fetch origin --prune` 후 부모 브랜치를 **번호 글롭 + 대장**으로 다시
해석한다(branch-hierarchy R1~R4). 바뀌었으면 myBaseBranch 를 갱신하고 로그에 남긴다:
`🎯 base 재해석: {이전} → {지금}`.
2. Review changes against myBaseBranch
2.3. ⭐ **base 드리프트 흡수 — 일반 머지** (myBaseBranch 가 `development` 처럼 계속 움직이는
브랜치일 때): children 처리 동안 base 가 수십 커밋 앞서 있으면 최종 PR 은 BEHIND·충돌로
시작한다. `git fetch origin & & git merge origin/{myBaseBranch}` 로 흡수하고 충돌은 **파일별로**
해소한다(`merge-conflict-resolution` 스킬 — 생성 파일은 재생성, 비즈니스 로직은 양쪽 의도
이식). ⛔ `-s ours`·`-X ours`·일괄 `--ours` 금지 — base 의 변경이 조용히 되돌아가고
되돌림을 잡을 테스트가 함께 삭제돼 CI 는 green 이다. 머지 후 리포에 되돌림 가드
(`check_merge_revert.py` 류)가 있으면 돌리고, 정당한 재작성은 커밋 메시지에 예외 마커로
남긴다. 머지 커밋 메시지도 리포 commit-msg 훅 형식(type·scope)을 따른다 — `merge:` 타입은
흔히 거부된다.
2.5. ⭐ **승격 전 " 전체 CI 전용 " 게이트 로컬 선실행** — 계층 PR(`story→epic→project`)에는
경량 검증(변경 패키지 분석·테스트·골든)만 돌고 **포맷 잡의 가드 스크립트·DCM·백엔드 unit·
웹 빌드는 한 번도 돌지 않는다**. 그것들이 처음 도는 자리가 이 최종 PR 이라, 자식이 많을수록
승격 PR 이 처음 만나는 실패가 누적되고 그 시점엔 수백 파일 diff 라 원인 자식을 되짚기
어렵다. PR 을 열기 **전에** 로컬에서 돌린다:
# ci.yml 의 단일행 가드 호출을 그대로 뽑아 전부 실행 (실측: 96개 · 수 분)
grep -oE '^[[:space:]]*run: python3 \.github/scripts/[a-z_]+\.py[^#]*$' .github/workflows/ci.yml \
| sed -E 's/^[[:space:]]*run: //' | sort -u | while IFS= read -r CMD; do
eval "$CMD" >/dev/null 2>&1 || echo "❌ $CMD"; done
bash scripts/lefthook/dcm_check.sh format --changed # 리포에 있는 경우
<pre><code> 실패한 가드가 **정상 코드를 오탐**하면(자식 Epic 의 리팩토링이 가드의 정규식 전제를 깬
경우 — 실사고: 테마를 형제 파일로 분리하자 한 파일만 보던 버튼 규격 가드가 4개 테마를
오탐) 예외 마커로 덮지 말고 가드의 해석 범위를 넓히고 회귀 테스트에 **실제 오탐 형태**를
첫 케이스로 넣는다. 결과는 3번 PR 본문의 로컬 게이트 실측에 함께 적는다.
- Create PR → targeting myBaseBranch(부모 브랜치, 없으면 development); 작업내역 아티팩트를
먼저 발행해 PR 본문에 링크로 심은 채 생성한다 (
run.mdStep 9 프로토콜 재사용, 복제 금지). PR body 에Closes #{issue_number}+ (parentIssue 가 있으면)Parent: #{parent.number}포함. 이 레벨이 여러 child 를 합친 것이므로 작업내역에 포함된 child 목록과 child 별 아티팩트 링크를 함께 싣는다. 발행은 비차단(마크다운 폴백) — 실패해도 PR 생성은 계속한다. 3.2. 이 컨테이너를 Review/QA 로 이동 — leaf 는/cc-dev:runStep 10 이 하지만, 컨테이너 레벨은 여기서 직접 해야 한다(누락 시 Epic 이 In Progress 에 머문 채 머지된다):reflectBoardState(issue, "review")(=moveIssueToPipeline(pid("Review/QA"))+ read-back,../rules/zenhub-conventions.md→ Pipeline State Contract 전이표 4행) — deployment-gated 트래커는rules/deployment-gated-status.md예외 적용. 점유는 여기서 해제하지 않는다 — 머지까지가 이 세션의 작업이다(해제는 Phase 3 마지막). 3.5. CI 대기 ∥ 작업내역 아티팩트 CI 상태 갱신 —run.mdStep 10.5 프로토콜을 그대로 재사용(복제 금지). 3단계에서 심어진 같은 URL 을 CI 결과로 갱신할 뿐, 새 댓글은 남기지 않는다. 갱신은 비차단, CI 판정은 하드 게이트 —--merge=pre-authorized도 CI 통과 요구는 덮지 못한다. - Conduct code review
4.9. ⛔ 머지 직전 완료성 재검증 (Parent Closure Invariant 2번째 지점 — 1번과 별개 게이트):
openChildrenStatus(issue.number)를 다시 호출해status === "none"을 재확인한다. 1번 게이트 이후 CI+리뷰로 수 시간이 흐르며 그 사이 QA 가 새 자식을 달거나 자식이 재오픈될 수 있다(#3451은 게이트와 머지 사이가 11시간이었다). 위반 시 머지하지 않고 정지 —--merge=pre-authorized도 이 게이트를 덮지 못한다(사전 승인은 "승인 클릭" 하나만 생략한다). 🕒 타임스탬프:⏱️ gate② {ISO8601} status={…} waived={n} (gate① 이후 {경과})— 1번과 이 게이트의 간격을 로그에 남긴다. 판정 술어는 1번과 동일하다(변경 없음). 4.95. ⛔ 머지 직전 PR base retarget (branch-hierarchy R7 —run.mdS12b 와 같은 게이트): ① myBaseBranch 를 여기서 다시 해석한다(조회만 — 머지 직전에 브랜치를 만들지 않는다). 1.5번 값은 PR 생성·CI 대기·리뷰로 수 시간 낡았다:git fetch origin --prune+ 번호 글롭 + 대장(R1~R4). ②gh pr view {pr} --json baseRefName,headRefName의baseRefName이 다르면gh pr edit {pr} --base {myBaseBranch}로 옮긴다. ③git fetch origin {myBaseBranch} {headRefName} --quiet후 둘 다 확인한다 —git merge-base --is-ancestor origin/{myBaseBranch} origin/{headRefName}(head 가 새 base 의 자손인가) 그리고git rev-list --count origin/{myBaseBranch}..origin/{headRefName}> 0. 하나라도 실패하면 머지하지 않고 정지한다(계보 어긋남). ⚠️ 로컬HEAD로 세지 않는다 — children 은 GitHub 에서 squash merge 되고 리드의 로컬myBranch는 뒤처져 있을 수 있다(Phase 2 의 post-merge 동기화 실패는 "경고 후 계속"이고, 재개 경로의 Phase 1.5-2 는 찾은 브랜치를 pull 하지 않는다). 그러면 정상 PR 이 0건으로 읽혀 막힌다. fetch 없이origin/{base}를 읽는 것도 금지다(R1). ⚠️ 스택 모드에서는 이 게이트를 적용하지 않는다 — base 소유자가gh stack이다(R7 주석). ⛔ 이 게이트가 없으면 Epic PR 이development로 머지된다 — Project 브랜치에는 그 커밋이 없으므로 Project→development 최종 PR 이 빈 diff 가 되고, "Epic 은 전부 닫혔는데 Project 에는 아무 변경도 없다"로 착지한다.--merge=pre-authorized도 이 게이트를 덮지 못한다. - Squash merge after user approval (머지 = Close) —
--merge=pre-authorized면 승인 질문 생략 후 즉시 머지(1번·4.9번 완료성 Hard Gate 는 이미 통과한 상태여야 함) - 이슈 종료 —
myBaseBranch가 저장소의 GitHub default 브랜치면Closes #{issue_number}로 자동 닫힘, 아니면(대부분의 경우 — 부모 브랜치이거나, development 가 default 가 아니거나) 자동으로 닫히지 않으므로 명시적 종료 필요. 6.5. ⭐ Close 검증 + 폴백:gh issue view {issue_number} --json state→ 미close 시gh issue close {issue_number} --reason completed; ZenHub 미동기화 시updateIssue state:CLOSED. 6.6. ⛔ 종료 후 자식 불변식 재확인 + 복구 (Parent Closure Invariant 3번째 지점):Closes #N은 머지 순간 GitHub 이 자식을 보지 않고 발화하므로, 4.9 이후 머지까지의 틈에서도 위반이 성립할 수 있다. 닫힌 직후openChildrenStatus(issue.number)를 한 번 더 호출하고,status === "open"이면 재오픈해 복구한다 —gh issue reopen→gh issue comment(남은 자식 목록) → reopen 이후에만moveIssueToPipeline(pid("In Progress")). 그리고 "남은 자식 처리 후/cc-dev:batch {issue_number}재실행" 을 경고로 남긴다. 머지 자체는 되돌리지 않는다. (실행 코드:run.mdStep 12.5-3 과 동일 — 여기서 복제하지 않는다) 🕒 타임스탬프:⏱️ gate③ {ISO8601} status={…} (gate② 이후 {경과}). 복구 루프의 예산·재개는 위L-B3.66r. 이 지점의unknown은 차단이 아니라 경고다(머지가 끝나 막을 대상이 없다 —rules/zenhub-conventions.md"예외 1곳 — 머지 이후 지점").open만 재오픈을 유발한다. 6.7. ⭐ 부모 완료 신호 확인 (재귀 cascade 아님) — 이 이슈에 parentIssue 가 있으면,checkAndCloseParent(issue_number)(agents/dev/issue-state-agent.md)를 호출해 형제들이 모두 Closed 인지 확인한다. 부모가 이제 자기 브랜치를 가진 레벨(Initiative/Project/Epic/ Story)이므로, 조건을 만족해도 자동으로 부모를 닫지 않는다 — "/cc-dev:batch {parent}를 실행해 마무리하세요"라는 신호만 남기고 여기서 정지한다(아래 "정지 조건" 참조). parentIssue 가 없으면 no-op. - ⭐ Post-merge:
git checkout {myBaseBranch} && git pull --ff-only origin {myBaseBranch} && git submodule sync --recursive && git submodule update --init --recursive(base 최신화 + 서브모듈 동기화) — 실패는 경고 후 계속. 서브모듈 없으면 no-op. 7.5. ⭐ 워크트리 정리 마무리 (B3.r+ 남은 회수) — 두 가지를 구분한다:- 이 레벨 자신의 워크트리: 이 배치가 부모에게 디스패치돼 워크트리 안에서 돌고 있다면,
그 워크트리의 카드 상태를 완료로, 코멘트를 "머지 #{pr} — 회수 가능"으로 갱신하는
표시만 한다(
markWorktreeReclaimable()— 정의 자리는 orca-worktree-lifecycle §1,run.mdStep 12.6 과 같은 함수다). ⛔ 자기 제거는 금지다 — 지우면 6.6(자식 불변식 복구)·6.7(부모 신호)· 7(base 최신화)이 한 줄도 실행되지 않고 세션이 사라진다. 실제 제거는 부모의 머지 큐가 자기 자리에서 한다(위 4.5 — 만든 쪽이 회수한다). - 자식 워크트리 잔여분: 머지 시점 회수에 실패해
leak으로 남은 행이 있으면 여기서 1회 재시도하고, 그래도 실패하면 대장에 남긴다(다음 배치의B0.r이 잇는다). 회수 실패는 경고 후 계속 — 이 레벨의 종료를 막지 않는다. 판정·순서·금지의 SoT 는 orca-worktree-lifecycle. 워크트리 밖에서 직접 실행됐거나 Orca 가 없으면 no-op.
- 이 레벨 자신의 워크트리: 이 배치가 부모에게 디스패치돼 워크트리 안에서 돌고 있다면,
그 워크트리의 카드 상태를 완료로, 코멘트를 "머지 #{pr} — 회수 가능"으로 갱신하는
표시만 한다(
- (Phase 0.5) Post-batch seed-alignment checkpoint — LOCKED seed 있으면 완료된 이슈 집합 + 최종 PR 을 LLM-SEMANTIC 으로 재대조, MAJOR-DRIFT 시 경고 후 종료 확인. 없으면 no-op skip.
정지 조건 (재귀는 여기서 멈춘다)#
/cc-dev:batch {issue_number} 한 번의 실행은 {issue_number} 자신의 PR이 머지·close 된
시점에 끝난다:
- {issue_number} 에 부모가 없었다면 →
development로의 최종 PR 이 머지·close 된 시점. - {issue_number} 에 부모가 있었다면 → 그 부모 브랜치로의 PR 이 머지·close 된 시점.
부모 이슈 자체를 마저 진행하려면 이 프롬프트를 부모 이슈 번호로 다시 실행한다
(
/cc-dev:batch {parent_number}) — 별도 실행 단위다.
이렇게 실행 단위를 한 단계씩 명시적으로 나누는 이유: 한 번의 실행이 계층 전체(예: Sub-task
하나에서 시작해 Initiative까지)를 자동으로 밀어붙이면, 상위 레벨(Project/Initiative)의 큰
통합 PR이 사람의 확인 없이 통째로 넘어갈 위험이 있다. 각 레벨의 최종 병합마다 (또는
--merge=pre-authorized가 명시적으로 지정된 경우에만) 승인이 걸리도록, 레벨 경계마다
재실행 지점을 둔다.
Flow Diagram#
📄 비규범(파생 뷰) — 사람이 훑는 그림이다. 규범 선언은 위 "Flow (규범 선언)" 의
```flow블록 하나이며, 어긋나면 블록이 이긴다(rules/orchestration-graph.md§6). 반복 횟수·타임아웃 같은 값을 이 박스에만 적지 않는다.
┌─────────────────────────────────────────────────────────────────┐
│ /cc-dev:batch {issue_number} (어느 레벨이든) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Phase 0.3: 레벨 판별 (Initiative~Sub-task) │
│ Phase 1: Leaf/Container 판정 (children 전수 조회 — GitHub sub-issues)│
│ ├── 조회 실패(known=false) → ⛔ 중단 (leaf 로 오판 금지) │
│ ├── children 없음(검증된 leaf) → /cc-dev:run {n} 위임 → 종료 │
│ └── children 있음(container) → 그 자식들을 처리 (새로 만들지 않음) │
│ │
│ Phase 1.5: 이 이슈의 브랜치 확보 + 보드 반영 (container 전용) │
│ ├── git fetch --prune → ls-remote " {prefix}/{n}-* " + 브랜치 대장 │
│ │ └── 있으면 그대로 채택 (다른 머신이 만든 것도 여기서 잡힌다) │
│ ├── 없을 때만: parent 브랜치(없으면 development) 기반으로 생성 │
│ ├── git checkout -b {prefix}/{n}-{slug} & & git push -u origin │
│ │ └── 커밋 0개여도 즉시 push + 대장에 이름 기록 │
│ └── In Progress 이동 + cascadeStartToParents (부모 체인까지) │
│ │
│ Phase 2: Recursive Child Processing (container 전용) │
│ ├── 독립 형제 → Orca 병렬 디스패치 (승인 후, ≤3 + 대기큐) │
│ │ └── 각 워크트리에서 /cc-dev:batch {child} 재귀 실행 │
│ │ (child 도 Phase 1 leaf/container 판정부터 다시 거침) │
│ ├── 의존 형제 → 순차 디스패치 │
│ ├── 머지는 직렬 (도착 순, 갱신→재게이트→머지) │
│ └── 머지된 child 워크트리 회수 (파생 자원 반납 → 제거 → 대장) │
│ └── BLOCKED 형제의 워크트리는 보존 (진단 현장) │
│ │
│ Phase 3: Container Finalization │
│ ├── 완료성 Hard Gate ① (전 children CLOSED — tri-state) │
│ ├── 작업내역 아티팩트 발행 → PR 본문에 링크 (run.md Step 9) │
│ ├── 이 레벨 브랜치 → 부모 브랜치(또는 development) PR → Review/QA │
│ ├── CI 대기 ∥ 아티팩트 CI 상태 갱신 (run.md Step 10.5) │
│ ├── Review → 완료성 Hard Gate ② (머지 직전 재검증) │
│ ├── PR base 재해석 → 다르면 gh pr edit --base (retarget, R7) │
│ ├── → Squash merge │
│ ├── Close 검증 → 완료성 확인 ③ (위반 시 gh issue reopen 복구) │
│ └── 부모 완료 신호 확인 (자동 병합 아님 — /cc-dev:batch {parent} 안내) │
│ │
└─────────────────────────────────────────────────────────────────┘MCP Call Order#
📄 비규범(파생 뷰) — 호출 순서를 보여주는 의사코드다. 규범 선언은 위 "Flow (규범 선언)" 의
```flow블록이며, 어긋나면 블록이 이긴다(rules/orchestration-graph.md§6).아래에서
pid(name)은 Phase 1.5-3 의 fail-closed 파이프라인 ID 해석기,listChildren()/openChildrenStatus()/mayClose()는rules/zenhub-conventions.md→ "Child Enumeration Contract",cascadeStartToParents()는agents/dev/issue-state-agent.md다.ledgerState(n)은 위 "5.1 시도 대장" 의 마커 코멘트에서 읽은 그 child 의 state (open/blocked/waived/closed)이며, 읽기 실패는"unknown"을 돌려주고 절대waived로 취급하지 않는다(fail-closed — 대장을 못 읽는 순간 게이트가 열리면 안 된다).
// Phase 0.3: 레벨 판별
const workspace = await mcp__zenhub__getWorkspacePipelinesAndRepositories();
const issueTypes = await mcp__zenhub__getIssueTypes({ repositoryId });
const target = (await mcp__zenhub__searchLatestIssues({ query: `#${issueNumber}` }))
.find(i = > i.number === issueNumber);
// Phase 1: leaf/container 판정 — /cc-dev:batch {issue_number} 는 이 함수 하나로 귀결된다
async function processIssue(issueNumber) {
const issue = (await mcp__zenhub__searchLatestIssues({ query: `#${issueNumber}` }))
.find(i = > i.number === issueNumber);
// ⚠️ 자식 조회는 Child Enumeration Contract 를 쓴다 (rules/zenhub-conventions.md).
// GD-04: ZenHub searchLatestIssues({query: " parent:… " }) 는 " 최신 20개 " 창 안에서만
// 필터링하고 실패 시 [] 를 돌려주므로, leaf/container 판정과 완료성 게이트의 근거로
// 쓸 수 없다. completeness 는 GitHub 네이티브 subIssues 로 확인한다(run.md Step 0.6).
const { children, known } = await listChildren(issueNumber); // GD-04 subIssues 기반, tri-state 래퍼
if (!known) {
throw new Error(
`#${issueNumber} 자식 조회 실패 — leaf/container 판정 불가. ` +
`조회 실패를 " 자식 없음 " 으로 처리하면 컨테이너를 leaf 로 오판해 열린 자식 위에서 닫는다. ` +
`gh auth status 확인 후 재실행하라.`
);
}
if (children.length === 0) {
// 검증된 leaf — batch.md 는 브랜치를 만들지 않는다. /cc-dev:run 에 그대로 위임한다.
// resolveBaseBranch() (run.md) 가 이 이슈의 레벨/parent 에 맞는 브랜치를 스스로 찾거나 만든다.
// await Task({ subagent_type: " implementation-agent " , prompt: `/cc-dev:run ${issueNumber}` }) 등,
// run.md Step 4~12.5 전체가 여기서 실행된다.
return;
}
// container — Phase 1.5: 이 이슈 자신의 브랜치를 먼저 **찾고**, 없을 때만 만든다.
// ⚠️ 조회는 `git fetch --prune` 후 `ls-remote " refs/heads/{prefix}/{n}-* " ` + 브랜치 대장이다
// (branch-hierarchy " 계층 브랜치 해석 계약 " R1~R4). 재계산한 전체 이름으로 완전일치 조회를
// 하면 다른 머신이 다른 로마자 표기로 만든 브랜치를 놓치고 가짜 부모를 하나 더 만든다.
const myBranch = ensureOwnBranch(issue); // 찾으면 채택, 없으면 생성 → 즉시 push → 대장 기록
// Phase 1.5-3: 보드 반영 — 컨테이너 자신 + 부모 체인 (누락 시 Epic 이 Product Backlog 에 방치된다)
await mcp__zenhub__moveIssueToPipeline({ issueId: issue.id, pipelineId: pid( " In Progress " ) });
await cascadeStartToParents(issue.number); // agents/dev/issue-state-agent.md
// Phase 2 — 독립 children 은 Orca 병렬, 의존 children 은 순차
// ⚠️ 여기서 처리하는 것은 위에서 조회한 **기존** children 이다. 같은 작업의 새 이슈를 만들어
// 그것만 처리하면 원본 자식이 열린 채 남아 부모가 불변식을 위반한다(Existing-Children Reuse).
// (parallel dispatch 상세는 위 " Orca Parallel Dispatch " 절 참조)
// width = min(후보 수, --max-parallel ?? 3). 초과분은 대기큐 + deferred 로그(조용한 절단 금지).
for (const child of independentBatchesInDependencyOrder(children)) {
// child 마다: processIssue(child.number) 를 재귀 호출(=별도 워크트리에서 /cc-dev:batch {child.number})
// 워커 대기: wall-clock --worker-timeout ?? 4h → 초과 시 BLOCKED( ' worker_timeout ' ) + 형제 계속
// 각 child 완료 후 머지는 직렬 — worker_done 도착 순서대로 하나씩:
// 현재 myBranch 위로 갱신 → runPrePushGate() 재실행(또는 갱신 head CI green) → PR→merge
// (재검증 없이 머지하면 앞 형제의 squash 이전 base 로 통과한 판정을 그대로 쓰는 것이다)
}
// Phase 3-1: Hard Gate — GD-04 방식으로 **재조회** (Phase 2 도중 child 가 새로 만들어졌을
// 수 있으므로 캐시된 children 을 재사용하지 않는다). tri-state — " none " 만 통과하며,
// `open.length > 0` 식 판정은 `[]` 에서 공허하게 통과하므로 쓰지 않는다.
const kids = await openChildrenStatus(issue.number);
// waived 예외: 열린 자식 **전부** 가 시도 대장에서 waived 인 경우에만 통과 (Phase 2-d ✅ 항목).
// unknown 은 예외 없이 차단. 판정 함수는 openChildrenStatus() 하나이며 새로 만들지 않는다.
const allWaived = kids.status === " open " & & kids.open.every(c = > ledgerState(c.number) === " waived " );
console.log(`⏱️ gate① ${new Date().toISOString()} status=${kids.status} waived=${kids.open.filter(c = > ledgerState(c.number) === " waived " ).length}`);
if (kids.status !== " none " & & !allWaived) {
throw new Error(`#${issue.number} 미완료: ${kids.status === " unknown "
? " 자식 조회 실패(판정 불가) " : `OPEN child ${kids.open.length}개`}`);
}
// Phase 3-2: 이 레벨 자신의 PR → 부모 브랜치(또는 development) → Review/QA 이동 → CI/리뷰
// gh pr create --base " ${issue.parentIssue ? myBaseBranch : ' development ' } " ...
// Phase 3-4.9: 머지 **직전** 에 openChildrenStatus 를 다시 호출해 재확인 (CI/리뷰 사이에 자식이 늘 수 있다)
// 머지 후 close+검증 → Phase 3-6.6 에서 한 번 더 확인, 위반이면 gh issue reopen 으로 복구.
// 그리고 부모가 있으면 checkAndCloseParent(issue.number) 로 신호만 남기고 정지.
}Pipeline Query#
Pipeline IDs differ per workspace. Dynamic query at session start:
const workspace = await mcp__zenhub__getWorkspacePipelinesAndRepositories();
const pipelines = workspace.pipelines;
// Find by name in pipelines: " New Issues " , " In Progress " , " Review/QA " , etc.
// Done pipeline is NOT used (Done ≠ closed). Merge = Close: `Closes #` → GitHub closed
// → ZenHub syncs to Closed. Verify with searchClosedIssues + fallback updateIssue.PR Creation Template (레벨 무관 — 공통)#
아래 템플릿은 PR 생성 시
run.mdStep 9 가 작업내역 아티팩트를 먼저 발행해 그 링크를 PR 본문에 심은 채로 만든다 (댓글 아님). 생성 후에는run.mdStep 10.5 가 CI 를 백그라운드로 띄우고 결과가 나오면 같은 URL 로 페이지 내용만 갱신한다. leaf(Story/Sub-task) PR 은/cc-dev:run이 이미 Step 9/10.5 를 수행하므로 batch 가 따로 호출하지 않는다. 프로토콜:skills/pr-work-artifact/SKILL.md· 발행 규약:rules/artifact-publishing.md
gh pr create --base " {parentBranch:-development} " \
--title " {issue.title} " --body " $(cat < < ' EOF '
## Summary
- {Change summary}
## Included Children
- ✅ #{child_1} - {title}
- ✅ #{child_2} - {title}
## Dispatch ← 병렬 승인/강등과 cap 결과의 내구 기록
- width {N}/{--max-parallel} · deferred: #{n}, #{n} (슬롯 회수 후 착수)
- degraded to sequential: {yes(--unattended: 스폰 승인 불가)|no}
## Waived Children ← 있을 때만 포함 (§5.2 waive 결정의 내구 기록)
- ⏸️ #{child_k} - {title} — 사유 {요약} · 후속 이슈 #{follow_up}
## Seed Alignment ← LOCKED seed 가 있을 때만 포함 (Phase 0.5)
- pre-batch {ALIGNED|MINOR-DRIFT|MAJOR-DRIFT} / post-batch {…} · MAJOR-DRIFT 처리 {확인|무인 경고}
## Related Issue
- Closes #{issue_number}
- Parent: #{parent_number} ← parentIssue 가 있을 때만 포함
## Test Plan
- [ ] Full integration tests passed
- [ ] Manual testing complete (해당 레벨에 맞게 조정 — Sub-task/Story 는 unit/BLoC 위주)
🤖 Generated with [Claude Code](https://claude.ai/claude-code)
EOF
) "Sub-task/Story PR은 기존 그대로 story/·task/ base를 쓴다 — 예시는
branch-hierarchy.md → "수동 운영 시 명령어 예시" 참조.
Issue Closure & Post-Merge Sync ⭐#
모든 머지는 해당 이슈를 Close(검증 포함)하고, base 브랜치를 최신으로 동기화한다. 정책 = "머지 = Close" (B).
⚠️ ZenHub 2-상태 모델: Pipeline(보드 칼럼)과 GitHub state(open/closed)는 별개다.
Done칼럼으로 옮겨도 GitHub 이슈는open그대로 —Done≠ closed.Closed파이프라인만 GitHubclosed와 1:1. 따라서Done은 쓰지 않고, 머지 후 GitHub state로 닫힘을 검증하고 폴백한다.
| 단계 | 머지 방향 | 이슈 Close (검증 포함) | Post-merge 체크아웃 |
|---|---|---|---|
| Sub-task | → Story 브랜치 | Sub-task 이슈 Close + verify_closed | git checkout story/{story}-{slug} && git pull + submodule update |
| Story | → Epic 브랜치 | Story 이슈 Close + verify_closed | git checkout epic/{epic}-{slug} && git pull + submodule update |
| Epic | → Project 브랜치(있으면) 또는 development | Epic 이슈 Close + verify_closed | git checkout {epic 의 myBaseBranch} && git pull + submodule update |
| Project | → Initiative 브랜치(있으면) 또는 development | Project 이슈 Close + verify_closed | git checkout {project 의 myBaseBranch} && git pull + submodule update |
| Initiative | → development | Initiative 이슈 종료 + verify_closed | git checkout development && git pull + submodule update |
⛔ Parent Closure Invariant: 어느 레벨이든 열린 자식이 하나라도 있으면 닫지 않는다.
Closes #N은 default 브랜치 머지 순간 자식을 보지 않고 발화하므로, 이 불변식은 GitHub 에 위임할 수 없다 — Phase 3-1(PR 전) · 3-4.9(머지 직전) · 3-6.6(종료 직후 복구) 세 지점에서 워크플로우가 직접 확인한다. 판정은 항상openChildrenStatus()tri-state (rules/zenhub-conventions.md→ "Child Enumeration Contract" / "Parent Closure Invariant"). 단 하나의 예외는waived— 열린 자식이 전부 사람 결정으로 이번 범위에서 제외되고 그 사실이 시도 대장·child 코멘트·PR body## Waived Children에 남았을 때다(위 5.2).blocked(waive 안 된 것)와unknown은 여전히 차단이고, 자동 waive 경로는 없다.
- 각 PR body의
Closes #{number}키워드로 squash 머지 시, base가 저장소의 GitHub default 브랜치일 때만 GitHub 이슈가 자동 Close되고 ZenHub도 동기화된다. 그 외(대부분의 중간 계층 머지)에는 명시적 close가 primary 메커니즘이다. - Close 검증 + 폴백 (매 머지 후 필수):
gh issue view {N} --json state로 GitHubclosed확인 → 아니면gh issue close {N} --reason completed. 이어searchClosedIssues "#{N}"로 ZenHub 동기화 확인 → 누락 시updateIssue state:CLOSED. - 하위 이슈가 모두 Close되고 이 레벨 브랜치 PR이 부모 브랜치(또는 development)에 머지되면 이 레벨 이슈가 종료되며, base 브랜치를 최신으로 체크아웃한다.
- 부모 완료 신호는 자동 병합이 아니다 — 이 레벨이 닫힌 뒤 부모의 형제들도 모두 닫혔는지만 확인하고, 부모 자신의 PR 생성·머지는
/cc-dev:batch {parent_number}를 다시 실행해야 한다(위 "정지 조건" 참조). - 서브모듈 동기화 (매 머지 후 필수):
git pull은 superproject의 서브모듈 포인터만 갱신하고 서브모듈 워킹트리는 체크아웃하지 않는다. base 최신화 직후git submodule sync --recursive && git submodule update --init --recursive로 서브모듈을 base가 가리키는 커밋에 정렬한다. 서브모듈이 없으면 no-op.
Verification Checklist#
📄 비규범(파생 뷰) — 규범 선언은 위 "Flow (규범 선언)" 의
```flow블록이다.
- (seed가 LOCKED일 때만) pre-batch / post-batch seed-alignment 점검 실행, MAJOR-DRIFT 시 사용자 확인 — seed 부재/DRAFT면 no-op skip (체크 불필요)
- 레벨 판별(Phase 0.3) 결과가 실제 이슈 타입과 일치
- 자식 조회를 GitHub sub-issues(Child Enumeration Contract)로 했고, 조회 실패를 "자식 없음"으로 처리하지 않았음
- 기존 자식을 그대로 처리했음 — 같은 작업의 새 이슈를 만들지 않았음(Existing-Children Reuse)
- 이 이슈의 브랜치가 올바른 base(부모 브랜치, 없으면 development)에서 분기됨
- Phase 1.5에서 이 이슈를 실제로 In Progress로 옮겼고(보드에서 확인), 부모 체인도 cascade됐음(아직 착수 전 칸이었다면)
- Phase 3-3.2에서 이 이슈를 Review/QA로 옮겼음(PR 생성 직후)
- Phase 1.5에서 이 레벨의 점유를 획득했고(cascade 로 올린 부모에는 쓰지 않았음), 종료(머지=Close·BLOCKED·중단) 시 해제했음
- 디스패치 전 각 child 의 점유를 확인했고,
other-live인 child 는SKIPPED-OCCUPIED로 빼고 형제를 계속했음 BLOCKED(*)로 뺀 child 마다blockIssue()를 실제로 호출했음(보드가 In Progress 에 남아 있지 않음)- children이 있으면 각 child 브랜치가 이 이슈의 브랜치에서 분기됨
- Initiative/Project/Epic 이 children 0개면 사용자 확인을 거쳤음 (Story/Sub-task는 확인 불필요)
- children 조회·완료성 Hard Gate 모두 GD-04 방식(
gh api graphqlsub-issues)으로 했음 — ZenHubparent:검색 단독 판정 아님 - container 이슈 처리 중 새 sub-issue를 만들기 전 기존 sub-issue 유무를 GD-04 방식으로 먼저 확인했음(
run.mdStep 0.6) - 병렬 디스패치 전 사용자 승인을 받았음(
--merge=pre-authorized와 무관하게 항상) — 승인은 최상위 1회만 취하고 하위로 전파했음(워커 안에서 다시 묻지 않았음) - 답할 사람이 없는 실행(
--unattended)에서 무승인 스폰이 없었음 — 순차로 강등하고 그 사실을 PR body## Dispatch에 남겼음 - 동시 워크트리가
--max-parallel(기본 3)을 넘지 않았고, cap 때문에 미룬 형제를 이슈 번호·제목으로 deferred 로그에 남겼음(조용한 절단 없음) - 각 워커에 wall-clock 예산(기본 4h)이 걸려 있었고, 초과분은
BLOCKED('worker_timeout')+ 형제 계속으로 처리됐음 - 병렬 구현이어도 머지는 완료 순서대로 직렬화됨(
mode:stream serialize:1 order:worker_done-arrival),BLOCKED형제가 큐를 잡아 두지 않았음 - 스폰 직후 워크트리 id 전체(
<repoId>::<path>)를 시도 대장에 적었음 — 회수·스윕의 유일한 명단 - 머지된 child 마다 머지 직후(worker_done 자리가 아니라) 워크트리를 회수했고, 안전 판정 5종을 통과한 것만 지웠음 — 하나라도 미확정이면 보존
- 워크트리 경로에서 파생된 자원(워크트리별 DB·compose 프로젝트·시뮬레이터/브라우저 슬롯)을 체크아웃 제거 전에 반납했음
BLOCKED(unstuck/conflict/timeout) child 의 워크트리는 보존했음 — 진단 현장을 지우지 않았음- 회수/보존/누수를 전부 로그(
🧹)와 대장(reclaimed/leak)에 남겼고, 회수 실패가 머지 재시도나 사이클 중단을 부르지 않았음 - 이 레벨 자신의 워크트리는 표시만 하고 자기 제거하지 않았음(제거는 부모의 머지 큐가 수행)
- (선택형 스택 모드를 켠 경우에만) 스택에 넣은 형제가 순차 의존이었고, 스택 조작을 한 워크트리에서만 했으며(exit 8/6 회피),
gh pr merge --auto대신gh stack merge를 썼고, 머지가 가장 아래 미머지 PR 부터 연속 묶음으로만 이뤄졌음 — 스택 미사용이면 체크 불필요 - 머지 직전 각 child 를 현재
myBranch위로 갱신하고runPrePushGate()재실행(또는 갱신 head CI green)을 통과시켰음 — 앞 형제의 squash 이전 base 로 통과한 판정을 재사용하지 않았음 - 머지 큐의 상태 전이(enqueued/rebased/re-gate/merged/dequeued)를 전부 로그로 남겼음
- 반복 실패 child는 stall ladder(retry →
/cc-dev:unstuck solo→ BLOCKED)를 적용하고, BLOCKED여도 이 레벨은 계속 진행 - child 별 시도 횟수를 시도 대장(마커 코멘트)에 누적 기록해 batch 재실행이 예산을 이어 썼음 — 재실행마다 사다리를 처음부터 다시 돌리지 않았음
- 영구 BLOCKED child 가 있었다면 descope 또는 waive(사람 결정 + 내구 기록)로 처리했고, 자동 waive 는 없었음.
unknown은 예외 없이 차단했음 - 각 child PR이
Closes #{number}로 이슈를 Close - 각 머지 후 GitHub
closed+ ZenHubClosed검증 (verify_closed, Done에 방치 금지) - 완료성 Hard Gate를 3회 통과: PR 생성 전(3-1) · 머지 직전(3-4.9) · 종료 직후(3-6.6) — 3-6.6 위반 시 reopen 복구까지 수행
- 세 완료성 게이트 판정마다 타임스탬프를 남겼음(게이트 간 경과 시간이 감사 가능)
- 이 레벨 PR 머지 시 이 레벨 이슈 자동/명시적 종료 + 검증
- 부모가 있으면
checkAndCloseParent로 형제 완료 여부만 확인(자동 병합 아님) — 신호만 남기고 정지 - 각 머지 후 base 브랜치 최신 체크아웃 (
git pull) + 서브모듈 동기화 - BDD scenarios vs actual implementation comparison (leaf 레벨)
- Code conventions followed
- Lint 0 issues + DCM error 0 issues
Related#
- orchestration-graph.md —
```flow표기법, 루프 계약 7필드, 게이트 tri-state, 기질 매트릭스, fan-out 의무의 SoT - merge-conflict-resolution/SKILL.md — 머지 큐 rung C1 의 충돌 해소 절차 SoT
- job-timeout-budget/SKILL.md —
--worker-timeout같은 예산 숫자를 고르는 방식의 SoT - orca-worktree-lifecycle — 머지 후 워크트리 회수(안전 판정 5종·반납 순서·자기 제거 금지·고아 스윕)의 SoT. 이 문서는 회수 시점(머지 큐 5번 · Phase 0 스윕 · Phase 3-7.5)만 고정한다
- serverpod-worktree-parallel · parallel-test-env —
--max-parallel기본 3 의 실측 근거(머신 상한 3~4, 디바이스/포트 슬롯)이자, 회수 시 반납해야 하는 파생 자원(워크트리별 DB·포트 블록·디바이스 슬롯)의 할당 규칙 SoT - branch-hierarchy.md — 5레벨 브랜치 규칙의 SoT
- stacked-prs — 선택형 선형 스택 실행 모드의 SoT(명령·머지 의미론·exit code·비대화형 주의·CI 비용 맞교환). 순차 의존 형제를 한 줄로 묶을 때만 켠다 — 기본값은 위 "Orca Parallel Dispatch" 다
- zenhub-conventions.md — 이슈 타입 계층, Issue Closure Policy
- issue-state-agent.md —
checkAndCloseParent(부모 완료 신호, 자동 병합 아님) ·cascadeStartToParents(부모 착수 신호, 조부모까지 자동 반영) - sequential-workflow.md — Stall Ladder 상세, per-issue 사이클 detail
- run.md — leaf 이슈 사이클(
resolveBaseBranch()포함), Step 10.5 CI대기/아티팩트 프로토콜 orca-cli/orchestration스킬 — 병렬 디스패치 실행 계층(discovery stub — 실행 직전Skill()로 최신 가이드 로드)- go.md — 이 커맨드를 Epic 단위로 호출하는 원스톱 파이프라인(Project/Initiative 단위 호출로도 확장 가능, 하위 호환 유지)