LogoSkills

/cc-dev:batch — 계층 전체를 한 방에 자동 처리

ZenHub 이슈 계층(Initiative/Project/Epic/Story/Sub-task) 5레벨을 재귀적으로 자동 처리합니다 — 어느 레벨에서 시작해도 그 서브트리 전체를 Orca 오케스트레이션(병렬 디스패치)으로 진행하고, 더 이상 머지할 부모가 없으면 development로 최종 PR을 올립니다.

/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}
# 예: 1234Epic을 통째로 처리 (기존 방식과 100% 동일하게 동작)
/cc-dev:batch 1234

# 예: 20Project를 통째로 처리 — 그 아래 여러 EpicOrca로 병렬 디스패치
/cc-dev:batch 20
  • {issue_number} 자리에는 Initiative/Project/Epic/Story/Sub-task 어느 레벨의 이슈 번호든 넣을 수 있습니다. 레벨은 자동으로 판별합니다.
  • 그 뒤로는 명령이 알아서 하위 이슈들을 찾아 처리하므로, 추가로 입력할 것은 없습니다.
  • 형제 작업을 동시에 진행해도 되는지는 시작 전에 한 번 확인받습니다(Orca 워크트리를 여러 개 띄우는 결정이므로).

안에서 무슨 일이 벌어지나요#

전체 흐름은 이 이슈 하나에 대해 재귀적으로 반복됩니다.

  1. (선택) 기획 명세 정렬 점검 — 확정된 기획 명세가 있으면, 시작 전과 최종 병합 직후에 방향이 맞는지 한 번씩 대조합니다. 없으면 조용히 건너뜁니다.
  2. 하위 이슈가 있는지 확인 — 하위 이슈가 없으면(가장 작은 단위) 곧바로 /cc-dev:run에 넘겨 구현·테스트·PR·병합까지 한 번에 처리하고 끝냅니다. 하위 이슈가 있으면 2번으로 이어집니다.
  3. 이 이슈의 작업 공간 만들기 + 하위 이슈 처리하기 — 이슈 레벨(Initiative~Sub-task)에 맞게, 부모가 있으면 그 부모 브랜치 위에서 없으면 development에서 이 이슈 전용 브랜치를 만듭니다. 그 안에서, 서로 독립적인 하위 이슈는 Orca로 동시에 착수하고(사람 확인 후), 각자 이 흐름을 재귀적으로 반복합니다. 병합만 순서대로 하나씩 합칩니다.
  4. 이 레벨 마무리하기 — 모든 하위 이슈가 닫혔는지 확인하고, 이 레벨 전체를 묶은 최종 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부모 없을 때 basechildren 없을 때
Initiativeinitiative/(구조상 부모 없음)development드묾 — 사용자 확인 후 leaf로 진행
Projectproject/initiative/{n}-*development드묾 — 사용자 확인 후 leaf로 진행
Epicepic/project/{n}-*development드묾 — 사용자 확인 후 leaf로 진행(기존 "단독 Epic" 케이스)
Storystory/epic/{n}-*(부모 없으면 feature|fix|chore/ — 이 알고리즘 밖의 단독 이슈 케이스)정상 — 항상 leaf 가능
Sub-tasktask/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 stackexit 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하고, 매 머지 후 myBranchgit 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 하나. 대장에 없으면 대상이 아니다
결과 기록대장 statereclaimed · 큐 로그에 🧹 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 이 된다. 게이트에 예외가 생기지 않는다
waivechild 를 부모에 남긴 채 이 레벨의 마무리만 허용시도 대장의 statewaived 로 바꾸고, 사유 + 후속 이슈 번호를 child 이슈 코멘트와 이 레벨 PR body(## Waived Children)에 남긴다
  • waive 는 AskUserQuestion 결정이다(무인 기본값은 아래 "Unattended & Approval Contract" 표). 자동으로 waive 되는 경로는 없다 — 자동 waive 는 완료성 게이트를 fail-open 으로 되돌린다.
  • 완료성 게이트는 열린 자식을 openwaived 로 구분한다 — 판정 함수는 그대로 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 stackexit 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 --json
  • link 는 로컬 추적을 만들지 않는다 — 로컬에서 스택 명령을 쓰려면 gh stack checkout <pr-number> 로 셋업을 따로 받는다.
  • 독립임이 확인된 형제는 link 하지 않는다 — 인위적 직렬화만 생긴다.
  • link 도 위 "3. 디스패치" 의 exit 8 주의를 그대로 받는다: 한 곳에서만 실행한다.

폴백 — 스택 모드를 쓸 수 없을 때#

gh stackexit 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-2aInitiative/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.md Fallback 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:barrierB2d 한 자리다 — 전수 재조회가 유일한 교차 항목 근거다. 둘째, 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 3blockIssue(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.md Step 0.6 (GD-04)gh api graphql sub-issues 조회를 completeness source of truth 로 쓰고, ZenHub 검색은 pipeline/issueType 같은 부가 메타데이터 보강 조회에만 병행한다. (실사고: Epic #3451 이 이 결함으로 실제 sub-issue 5개를 못 찾아 중복 이슈 5개를 새로 만들고 원본을 고아로 남겼다 — 상세는 run.md GD-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 가 수십 커밋 앞서 있으면 최종 PRBEHIND·충돌로
     시작한다. `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 을 열기 **전에** 로컬에서 돌린다:
     
bash
 # 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개 테마를
 오탐) 예외 마커로 덮지 말고 가드의 해석 범위를 넓히고 회귀 테스트에 **실제 오탐 형태**를
 첫 케이스로 넣는다. 결과는 3PR 본문의 로컬 게이트 실측에 함께 적는다.
  1. Create PR → targeting myBaseBranch(부모 브랜치, 없으면 development); 작업내역 아티팩트를 먼저 발행해 PR 본문에 링크로 심은 채 생성한다 (run.md Step 9 프로토콜 재사용, 복제 금지). PR body 에 Closes #{issue_number} + (parentIssue 가 있으면) Parent: #{parent.number} 포함. 이 레벨이 여러 child 를 합친 것이므로 작업내역에 포함된 child 목록과 child 별 아티팩트 링크를 함께 싣는다. 발행은 비차단(마크다운 폴백) — 실패해도 PR 생성은 계속한다. 3.2. 이 컨테이너를 Review/QA 로 이동 — leaf 는 /cc-dev:run Step 10 이 하지만, 컨테이너 레벨은 여기서 직접 해야 한다(누락 시 Epic 이 In Progress 에 머문 채 머지된다): reflectBoardState(issue, &quot;review&quot;) (= moveIssueToPipeline(pid(&quot;Review/QA&quot;)) + read-back, ../rules/zenhub-conventions.md → Pipeline State Contract 전이표 4행) — deployment-gated 트래커는 rules/deployment-gated-status.md 예외 적용. 점유는 여기서 해제하지 않는다 — 머지까지가 이 세션의 작업이다(해제는 Phase 3 마지막). 3.5. CI 대기 ∥ 작업내역 아티팩트 CI 상태 갱신run.md Step 10.5 프로토콜을 그대로 재사용(복제 금지). 3단계에서 심어진 같은 URL 을 CI 결과로 갱신할 뿐, 새 댓글은 남기지 않는다. 갱신은 비차단, CI 판정은 하드 게이트 — --merge=pre-authorized 도 CI 통과 요구는 덮지 못한다.
  2. Conduct code review 4.9. ⛔ 머지 직전 완료성 재검증 (Parent Closure Invariant 2번째 지점 — 1번과 별개 게이트): openChildrenStatus(issue.number)다시 호출해 status === &quot;none&quot; 을 재확인한다. 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.md S12b 와 같은 게이트): ① myBaseBranch 를 여기서 다시 해석한다(조회만 — 머지 직전에 브랜치를 만들지 않는다). 1.5번 값은 PR 생성·CI 대기·리뷰로 수 시간 낡았다: git fetch origin --prune + 번호 글롭 + 대장(R1~R4). ② gh pr view {pr} --json baseRefName,headRefNamebaseRefName 이 다르면 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 도 이 게이트를 덮지 못한다.
  3. Squash merge after user approval (머지 = Close) — --merge=pre-authorized 면 승인 질문 생략 후 즉시 머지(1번·4.9번 완료성 Hard Gate 는 이미 통과한 상태여야 함)
  4. 이슈 종료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 === &quot;open&quot; 이면 재오픈해 복구한다 — gh issue reopengh issue comment(남은 자식 목록) → reopen 이후에만 moveIssueToPipeline(pid(&quot;In Progress&quot;)). 그리고 "남은 자식 처리 후 /cc-dev:batch {issue_number} 재실행" 을 경고로 남긴다. 머지 자체는 되돌리지 않는다. (실행 코드: run.md Step 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.
  5. ⭐ Post-merge: git checkout {myBaseBranch} &amp;&amp; git pull --ff-only origin {myBaseBranch} &amp;&amp; git submodule sync --recursive &amp;&amp; git submodule update --init --recursive (base 최신화 + 서브모듈 동기화) — 실패는 경고 후 계속. 서브모듈 없으면 no-op. 7.5. ⭐ 워크트리 정리 마무리 (B3.r + 남은 회수) — 두 가지를 구분한다:
    • 이 레벨 자신의 워크트리: 이 배치가 부모에게 디스패치돼 워크트리 안에서 돌고 있다면, 그 워크트리의 카드 상태를 완료로, 코멘트를 "머지 #{pr} — 회수 가능"으로 갱신하는 표시만 한다(markWorktreeReclaimable() — 정의 자리는 orca-worktree-lifecycle §1, run.md Step 12.6 과 같은 함수다). ⛔ 자기 제거는 금지다 — 지우면 6.6(자식 불변식 복구)·6.7(부모 신호)· 7(base 최신화)이 한 줄도 실행되지 않고 세션이 사라진다. 실제 제거는 부모의 머지 큐가 자기 자리에서 한다(위 4.5 — 만든 쪽이 회수한다).
    • 자식 워크트리 잔여분: 머지 시점 회수에 실패해 leak 으로 남은 행이 있으면 여기서 1회 재시도하고, 그래도 실패하면 대장에 남긴다(다음 배치의 B0.r 이 잇는다). 회수 실패는 경고 후 계속 — 이 레벨의 종료를 막지 않는다. 판정·순서·금지의 SoT 는 orca-worktree-lifecycle. 워크트리 밖에서 직접 실행됐거나 Orca 가 없으면 no-op.
  6. (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) PRReview/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.md Step 9 가 작업내역 아티팩트를 먼저 발행해 그 링크를 PR 본문에 심은 채로 만든다 (댓글 아님). 생성 후에는 run.md Step 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 파이프라인만 GitHub closed와 1:1. 따라서 Done은 쓰지 않고, 머지 후 GitHub state로 닫힘을 검증하고 폴백한다.

단계머지 방향이슈 Close (검증 포함)Post-merge 체크아웃
Sub-task→ Story 브랜치Sub-task 이슈 Close + verify_closedgit checkout story/{story}-{slug} && git pull + submodule update
Story→ Epic 브랜치Story 이슈 Close + verify_closedgit checkout epic/{epic}-{slug} && git pull + submodule update
Epic→ Project 브랜치(있으면) 또는 developmentEpic 이슈 Close + verify_closedgit checkout {epic 의 myBaseBranch} && git pull + submodule update
Project→ Initiative 브랜치(있으면) 또는 developmentProject 이슈 Close + verify_closedgit checkout {project 의 myBaseBranch} && git pull + submodule update
Initiative→ developmentInitiative 이슈 종료 + verify_closedgit 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로 GitHub closed 확인 → 아니면 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 graphql sub-issues)으로 했음 — ZenHub parent: 검색 단독 판정 아님
  • container 이슈 처리 중 새 sub-issue를 만들기 전 기존 sub-issue 유무를 GD-04 방식으로 먼저 확인했음(run.md Step 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 + ZenHub Closed 검증 (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
  • 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.mdcheckAndCloseParent(부모 완료 신호, 자동 병합 아님) · 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 단위 호출로도 확장 가능, 하위 호환 유지)