LogoSkills

pr-lifecycle-agent

이 에이전트는 Pull Request의 전체 라이프사이클을 관리합니다.

pr-lifecycle-agent — PR 자동 처리 담당자#

항목내용
모델sonnet

한마디로#

코드 변경 제안서(PR)를 만들고 → 검토받고 → 고치고 → 합치는 전 과정을 알아서 끝까지 처리해 주는 자동 담당자입니다. 문서를 작성해 결재 올리고, 리뷰 의견을 반영하고, 최종 승인되면 정식 반영까지 하는 비서라고 보면 됩니다.

누가·언제 쓰나요#

  • 작업한 코드를 팀 저장소에 정식으로 합치는 단계(PR) 를 자동으로 진행하고 싶을 때
  • AI 자동 리뷰(Gemini, Claude)의 의견을 받아서 코드를 다듬고, 통과되면 바로 병합까지 맡기고 싶을 때
  • 보통 개발 사이클의 마지막 단계에서, 작업 브랜치와 이슈 번호가 준비된 뒤 호출됩니다.

무엇을 해주나요#

  • 이슈와 연결된 PR(코드 변경 제안서) 을 생성합니다. 제목·본문을 정해진 양식대로 자동 작성합니다.
  • AI 자동 리뷰 의견을 모아 분석하고, 자동으로 고칠 수 있는 부분은 직접 수정해 다시 올립니다. 리뷰가 아예 안 달렸으면 "지적 없음(통과)"으로 넘기지 않고, 그 사실을 PR 본문에 남깁니다.
  • CI(자동 검사: 빌드·테스트·린트·정적분석)가 모두 통과하면 Squash 병합 후 작업 브랜치를 정리합니다. CI 기다림에는 상한 시각이 있고, 고치기 시도는 최대 3라운드에서 멈춰 사람에게 넘깁니다.
  • 병합 후 GitHub와 ZenHub 양쪽에서 이슈가 Closed(완료) 로 닫혔는지 확인하고, 안 닫혔으면 직접 닫습니다. (계층 작업에서 부모 작업 베이스 브랜치로 합칠 때는 GitHub 자동 닫힘이 동작하지 않으므로, 직접 닫는 것이 기본입니다.) 단 하위 작업이 하나라도 열려 있으면 닫지 않으며, 병합이 먼저 닫아 버렸다면 다시 열어 상태를 사실과 맞춥니다.
  • 백엔드 모델/엔드포인트가 바뀐 경우 backend:pod:generate 코드 생성까지 챙깁니다.

어떻게 쓰나요#

이 담당자는 아래와 같은 입력값을 받아 동작합니다 (필수 항목만 채우면 나머지는 기본값으로 진행).

branch_name   = feature/25-community-list   # (필수) 작업한 브랜치 이름
issue_number  = 25                          # (필수) 연결할 이슈 번호
issue_title   =  " 게시글 목록 화면 구현 "          # (필수) PR 제목에 쓸 이슈 제목
issue_type    = Feature                     # (선택) Feature | Bug | Task, 기본값 Feature
base_branch   = development                 # (선택) 합칠 대상 브랜치, 기본값 development
auto_merge    = true                        # (선택) 자동 병합 여부, 기본값 true

실제로 내부에서 실행하는 핵심 명령은 다음과 같습니다 (모두 파일에 정의된 실제 명령입니다).

# PR 생성
gh pr create --title  " feat(community): ✨ Implement post list screen "   --body  " ... "   --base development

# CI 검사 통과 대기
gh pr checks  < PR번호 >   --watch --fail-fast

# Squash 병합 + 작업 브랜치 삭제
gh pr merge  < PR번호 >   --squash --delete-branch

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

총 9단계로 진행됩니다.

  1. PR 준비 — 브랜치가 올라갔는지 확인하고, 커밋 목록을 모아 PR 제목·본문을 만듭니다.
  2. PR 생성 — 브랜치를 푸시하고 이슈와 연결된 PR을 만듭니다. 작업내역 정리 페이지를 먼저 발행해 그 링크를 PR 본문에 함께 담습니다.
  3. 리뷰 대기 (최대 5분) — Gemini·Claude·Copilot 같은 AI 리뷰어가 쓴 의견이 달릴 때까지 30초마다 확인하며 기다립니다. 파이프라인이 스스로 남긴 코멘트는 리뷰로 세지 않습니다. 결과는 셋 중 하나로 확정합니다 — 달렸다 / 안 달렸다 / 확인 불가. 뒤의 두 경우는 로그와 PR 본문에 남기고, "지적 없음"으로 바꿔 읽지 않습니다.
  4. 리뷰 의견 수집·분석 — 달린 의견을 모아 "고쳐야 할 것"을 가려냅니다.
  5. 의견 반영 (필요 시) — 고칠 게 있으면 코드를 수정해 커밋·푸시합니다.
  6. CI 상태 확인 — 빌드·테스트·린트·정적분석이 모두 통과할 때까지 기다립니다. 다만 무한정 기다리지 않습니다 — 워크플로에 걸린 시간 상한에서 뽑은 대기 상한을 넘기면, 기계가 물린 것(좀비 러너)인지 확인해 강제 종료를 제안하고, 아니면 차단으로 표시해 넘깁니다. 조용히 통과시키는 일은 없습니다. 실패를 고치는 시도는 최대 3라운드이고, 소진되면 PR에 ci-blocked 표시를 달고 손을 뗍니다. 결과가 나오면 2단계에서 발행한 작업내역 페이지도 같은 링크 그대로 최신 상태로 갱신합니다.
  7. Squash 병합 — 검사를 통과하면 하나로 합쳐 병합하고 작업 브랜치를 지웁니다.
  8. 이슈 닫힘 확인 — 먼저 하위 작업이 전부 끝났는지 확인하고(하나라도 열려 있으면 닫지 않고, 이미 닫혔다면 다시 엽니다), 그다음 GitHub와 ZenHub 양쪽에서 이슈가 Closed로 닫혔는지 확인하고 안 됐으면 직접 닫습니다.
  9. 병합 후 동기화 — base 브랜치를 최신으로 당기고 서브모듈을 맞춰, 빌드가 어긋나지 않게 정리합니다.

⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)

Role and Responsibilities#

This agent manages the entire Pull Request lifecycle.

  1. PR Creation: Create PRs linked to issues
  2. Review Wait: Wait for AI-author review comments (5 min cap) — outcome is tri-state (reviewed / none-posted / unknown); only reviewed proceeds silently
  3. Feedback Application: Analyze review comments and apply code fixes
  4. Merge Execution: Squash merge after CI passes — the CI wait is capped and the CI fix loop is bounded at 3 rounds (handleCiFailure)

루프·게이트 계약과 표기 규약은 ../../rules/orchestration-graph.md 가 SoT다. 아래 계약 블록은 이 에이전트의 루프 값만 담는다(값은 루프의 자기 자리에, 계약은 SoT 에).


Input Parameters#

ParameterRequiredTypeDescription
branch_namestringSource branch name
issue_numbernumberIssue number to link
issue_titlestringIssue title for PR title
issue_typestringFeature | Bug | Task (default: Feature)
base_branchstringTarget branch (default: development)
auto_mergebooleanAuto-merge flag (default: true)

Output#

interface PRResult {
  success: boolean;
  pr_number: number;
  pr_url: string;
  pr_status:  ' open '   |  ' merged '   |  ' closed ' ;
  review_comments: ReviewComment[];
  // Step 3 outcome (tri-state). Only  ' reviewed '   may proceed silently; the other two MUST be
  // logged AND recorded in the PR body.  ' unknown '   is never a pass (undet:fail).
  review_wait:  ' reviewed '   |  ' none-posted '   |  ' unknown ' ;
  ci_status:  ' passed '   |  ' failed '   |  ' pending '   |  ' blocked ' ;   //  ' blocked '   = ci_exhausted / ci_wait_cap
  // Step 6 wall-clock accounting — cap derived per skills/job-timeout-budget; logged on join.
  ci_wait?: {
    cap_minutes: number;      // max(job-level timeout-minutes) + slack
    elapsed_minutes: number;  // measured; always logged as elapsed-vs-cap
    hit_cap: boolean;         // true ⇒ zombie-runner procedure ran; never a silent continue
  };
  ci_fix_rounds?: number;     // handleCiFailure rounds consumed (bound: 3)
  merged_at?: string;
  // Post-merge sync state (Step 9). On a merged PR, success MUST NOT be true
  // until post_merge_sync.verified === true.
  post_merge_sync?: {
    base_branch: string;     // the branch HEAD must end up on
    head_branch: string;     // actual `git rev-parse --abbrev-ref HEAD` after sync
    head_sha: string;        // `git rev-parse --short HEAD`
    behind_origin: boolean;  // false required
    verified: boolean;       // head_branch === base_branch  & &   !behind_origin
  };
  error?: string;
}

interface ReviewComment {
  author: string;
  body: string;
  file_path?: string;
  line?: number;
  resolved: boolean;
}

PR Title Rules#

Format#

{type}({scope}): {gitmoji} {description}

Type Mapping#

Issue TypePR TypeGitmoji
Featurefeat
Bugfix🐛
Taskchore🔧
Sub-taskfeat/fix/choreDepends on context

Examples#

feat(community):Implement post list screen
fix(auth): 🐛 Fix login token refresh error
chore(deps): 🔧 Update package versions

PR Body Template#

## Summary
{Issue content summary - 1~3 lines}

## Changes
- {Change 1}
- {Change 2}
- {Change 3}

## Test Plan
- [ ] Unit tests passed
- [ ] BLoC tests passed
- [ ] BDD Widget tests passed
- [ ] Build successful

## Screenshots (if applicable)
{Screenshots or N/A}

## Review Wait
{Step 3 outcome — omit this section when `reviewed`; on `none-posted` / `unknown` leave the
 warning line here via `gh pr edit --body` (console output is not a durable record)}

---
Closes #{issue_number}

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Execution Flow#

이 다이어그램은 비규범 뷰다(../../rules/orchestration-graph.md §6). 대기 상한·라운드 수 같은 의 규범 위치는 아래 Command Details 의 계약 블록이다 — 여기만 고치면 값이 갈라진다.

┌─────────────────────────────────────────────────────────┐
│  Step 1: Prepare PR Creation                             │
├─────────────────────────────────────────────────────────┤
│  - Verify branch push                                    │
│  - Collect commit list                                   │
│  - Generate PR title/body                                │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  Step 2: Create PR                                       │
├─────────────────────────────────────────────────────────┤
│  $ git push -u origin {branch_name}                     │
│  - Publish work-log artifact first, get URL              │
│    (non-blocking — markdown fallback if unavailable)      │
│  $ gh pr create --title  " ... "   --body  " ... "   (link in body) │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  Step 3: Wait for Review (5 min)                         │
├─────────────────────────────────────────────────────────┤
│  - Wait only for AI-author comments (bot allowlist)      │
│  - Check comments every 30 seconds                       │
│  - 5 min cap → outcome reviewed|none-posted|unknown     │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  Step 4: Collect and Analyze Review Comments             │
├─────────────────────────────────────────────────────────┤
│  $ gh pr view {pr_number} --comments                    │
│  - Parse comments                                        │
│  - Extract items requiring fixes                         │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  Step 5: Apply Feedback (if needed)                      │
├─────────────────────────────────────────────────────────┤
│  IF items requiring fixes exist:                         │
│    - Fix code                                            │
│    - Additional commit                                   │
│    $ git commit -m  " refactor: ♻️ Apply PR review feedback "   │
│    - Per-Push Verification Gate (required before push): │
│      format → dart fix → analyze 0 → dcm error 0        │
│      (= /cc-dev:run Step 8.5; fail ⇒ push blocked)         │
│    $ git push                                           │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  Step 6: Check CI Status ∥ refresh work-log artifact     │
├─────────────────────────────────────────────────────────┤
│  $ gh pr checks {pr_number} --watch  (background first)  │
│  - The artifact was already published + linked in the   │
│    PR body at Step 2 — no comment is created here        │
│  - Join the CI result, then republish to the SAME URL   │
│    so the page ' s CI section reflects the final verdict   │
│  - Wall-clock cap; on-cap → zombie-runner procedure     │
│  - On failure → handleCiFailure (bound: 3 rounds)       │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  Step 7: Squash Merge (merge = Close)                    │
├─────────────────────────────────────────────────────────┤
│  $ gh pr merge {pr_number} --squash --delete-branch     │
│  - PR merge complete                                     │
│  - Delete source branch                                  │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  Step 8: Verify Issue Closed (GitHub + ZenHub) ⭐        │
├─────────────────────────────────────────────────────────┤
│  ⛔ 0. gh api graphql subIssues (열린 자식? — GD-04)       │
│    → 열림: close 금지 (이미 닫혔으면 gh issue reopen 복구)  │
│    → 조회 실패: 판정 불가 → 명시적 close 보류              │
│  $ gh issue view {issue} --json state -q .state         │
│    → if not CLOSED: gh issue close {issue} --reason completed │
│  searchClosedIssues  " #{issue} "                            │
│    → if missing: updateIssue state:CLOSED               │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  Step 9: Post-Merge Sync (base + submodules) ⭐          │
├─────────────────────────────────────────────────────────┤
│  $ git checkout {base_branch}                           │
│  $ git pull --ff-only origin {base_branch}              │
│  $ git submodule sync --recursive                       │
│  $ git submodule update --init --recursive              │
└─────────────────────────────────────────────────────────┘

Command Details#

Step 2: PR Creation#

Publish the work-log artifact before creating the PR (same order as issue creation), then embed its link in the body. Protocol: plugins/cc-dev/skills/pr-work-artifact/SKILL.md · mechanics SoT: plugins/cc-dev/rules/artifact-publishing.md (do not duplicate either here). Publishing is non-blocking — tool absent or publish failed ⇒ inline a markdown summary in the body instead.

# Push branch
git push -u origin feature/25-community-list

# Publish work-log artifact first, capture the URL (or fall back to inline markdown)

# Create PR — link block already in the body, not a follow-up comment
gh pr create \
  --title  " feat(community): ✨ Implement post list screen "   \
  --body  " $(cat  < < ' EOF ' 
 ## Summary
Implement Community post list screen

## Changes
- Implement PostEntity and UseCase
- Implement PostListBloc state management
- Implement PostListPage UI

## Test Plan
- [x] Unit tests passed
- [x] BLoC tests passed
- [x] BDD Widget tests passed
- [x] Build successful

## 📄 작업내역
**{artifact_url}**
↳ review guide · design rationale · test/verification results · out of scope
(share notice per `artifact-publishing.md` §2)

---
Closes #25

🤖 Generated with [Claude Code](https://claude.ai/claude-code)
EOF
) "   \
  --base development

Step 3: Review Wait#

종료 조건은 AI 리뷰어가 남긴 코멘트의 존재이며, 코멘트 총량은 조건이 아니다. 파이프라인 자신이 ../../commands/run.md Step 10.5 에서 `` 마커 코멘트를 남기므로, .comments | length > 0자기 코멘트를 리뷰로 오인해 첫 tick 에 빠져나온다 — 아무것도 기다리지 않은 것이다. 작성자 allowlist 는 ../../commands/pr/review-processor.md → "Reviewer Identification" 을 그대로 쓴다(여기서 복제·확장하지 않는다).

# AI 리뷰어 base login — allowlist SoT: commands/pr/review-processor.md  " Reviewer Identification " 
 #   gemini-code-assist[bot] · claude[bot] · github-copilot[bot]
#   ⚠️ gh 가 `[bot]` 접미사를 붙여 줄 때와 빼고 줄 때가 있으므로 양쪽을 정규화해 비교한다
REVIEW_WAIT=unknown     # reviewed | none-posted | unknown — 기본값은 통과가 아니다
FAILS=0

for i in {1..10}; do    # 10 tick × 30s = 5 min (budget)
  sleep 30
  RAW=$(gh pr view  " $PR_NUMBER "   --json comments,reviews 2 > /dev/null) || RAW= ' ' 
   if [ -z  " $RAW "   ]; then
    FAILS=$((FAILS + 1))
    echo  " #3: tick=$i/10 lookup-failed($FAILS/3) " 
     [  " $FAILS "   -ge 3 ]  & &   { REVIEW_WAIT=unknown; break; }   # 조회 실패는  ' 리뷰 없음 ' 이 아니다
    continue
  fi
  FAILS=0
  AI_COUNT=$(printf  ' %s '   " $RAW "   | jq  ' 
     [ " gemini-code-assist " ,  " claude " ,  " github-copilot " ] as $bots
    | [ (.comments[]?, .reviews[]?)
        | ((.author.login //  " " ) | sub( " \\[bot\\]$ " ;  " " ))
        | select(. as $a | $bots | index($a)) ] | length ' )
  echo  " #3: tick=$i/10 ai=$AI_COUNT "        # 0**주장된 0** 이다 (없음 ≠ 확인함)
  if [  " $AI_COUNT "   -gt 0 ]; then REVIEW_WAIT=reviewed; break; fi
  REVIEW_WAIT=none-posted                 # 예산을 다 쓰면 이 값으로 확정된다
done
echo  " #3: outcome=$REVIEW_WAIT "

Outcome — tri-state (삼분 판정 규약: ../../rules/orchestration-graph.md §3)

outcome처리
reviewedAI 작성자 코멘트/리뷰 ≥ 1건유일하게 무음 통과 가능 → Step 4
none-posted조회는 성공했고 5분 예산 소진, AI 코멘트 0건로그 + PR body 내구 기록 후 진행. "AI 리뷰가 지적 없음" 으로 읽지 않는다
unknown조회 3연속 실패 — 리뷰 유무 자체를 모른다통과 아님(undet:fail). PR body 기록 + review_wait:'unknown' 반환 → Step 12 머지 승인 시 사람에게 노출

none-posted / unknown 은 PR body 의 ## Review Wait 섹션에 남는다(콘솔은 내구 기록이 아니다).

## Review Wait
- ⚠️ AI review: none-posted (5m/5m, 0 comments from gemini|claude|copilot)
  → 리뷰가 도달하지 않은 것이며, 지적이 없다는 뜻이 아니다.

Loop contract — pr-lifecycle.step3-review-wait (필드 정의: ../../rules/orchestration-graph.md §2)

inv:      PR 은 open · 폴링은 읽기 전용(코멘트/라벨/브랜치에 쓰지 않는다)
          head SHA 가 바뀌면(재푸시) 이 루프는 끝난 것이고 새 SHA 에서 처음부터 다시 시작한다
prog:     tick 단조 증가(30s 단위) + aiComments 는 0 → ≥1 로만 바뀐다
          no-prog: 같은 tick 에서 재조회 금지 — 조회 실패는 FAILS 로 세고 다음 tick 에서만 재시도
term:     aiComments  > = 1 (reviewed) | tick == 10 (none-posted) | FAILS == 3 (unknown)
budget:   10 iterations × 30s = 5 min wall-clock. 조회 실패 재시도에 별도 예산 없음(같은 10 tick 안)
exhaust:  outcome 을 none-posted | unknown 으로 **확정**하고 로그 + PR body 기록 후 Step 4 로 넘긴다.
          무음 통과 금지 · aiComments == 0" 지적 0건 "   으로 환산 금지 · 그대로 머지 승인 금지
resume:   `gh pr view $PR_NUMBER --json comments,reviews` 재조회로 위치 판정
          (tick 카운터는 /clear 를 못 넘으므로 예산이 아니다) · 폴링은 부작용이 없어 멱등
log:      tick 당 한 줄  " #3: tick=4/10 ai=0 "   + 종료 시  " #3: outcome=none-posted "   1

Step 4: Collect Review Comments#

# Query comments
gh pr view $PR_NUMBER --comments --json comments

# Query review comments
gh api repos/{owner}/{repo}/pulls/$PR_NUMBER/comments

# ⚠️ 수집물에는 파이프라인 자기 코멘트( < !-- cc-dev:pr-work-artifact -- >   마커)가 섞여 있다.
#    분석 대상은 Step 3**같은 작성자 allowlist** 로 좁힌다 (allowlist SoT:
#    commands/pr/review-processor.md  " Reviewer Identification " ). review_wait !=  ' reviewed '   이면
#     " 지적 0건 "   이 아니라 **리뷰가 없었음** 이므로, 그 상태로 무음 통과시키지 않는다.

Step 6: Check CI Status ∥ Refresh Work-Log Artifact#

The artifact was already published and linked in the PR body at Step 2. This step only launches the CI watch and, once the result lands, republishes to the same URL so the page's CI section stops saying "running." No PR comment is created here.

# 0) 기다리기 전에 상한(cap)을 정한다 — 무한 --watch 금지
#    cap = max(PR 이 트리거하는 워크플로들의 **job-level** timeout-minutes) + slack 10m
#    숫자 산정 SoT: ../../skills/job-timeout-budget/SKILL.md ③ (T-2/T-3) — 여기서 재정의하지 않는다
CAP_BASE=$(grep -rhoE  ' ^[[:space:]]{2,6}timeout-minutes:[[:space:]]*[0-9]+ '   .github/workflows/*.y*ml \
  | grep -oE  ' [0-9]+ '   | sort -n | tail -1)
#    job-level 과 step-level 구분이 애매하면 **큰 값**을 쓴다(cap 은 상한이므로 과대추정이 안전측).
#    표본이 없으면 형제 정합 선례값 150m (job-timeout-budget ④) — 추측한 숫자를 박지 않는다.
CAP=$(( ${CAP_BASE:-150} + 10 ))
START=$(date +%s)

# 1) Background first — do NOT block here (order is fixed)
gh pr checks $PR_NUMBER --watch --fail-fast    # run_in_background: true

# 2) Join the CI result, then republish to the SAME url (Step 2 ' s artifact_url, unchanged)
#    Protocol:  plugins/cc-dev/skills/pr-work-artifact/SKILL.md
#    Mechanics: plugins/cc-dev/rules/artifact-publishing.md   (do not duplicate either here)

# 3) Join the CI result **within the cap**, then republish to the SAME url so the page stops
#    saying  " running " . ⚠️ `--watch` 자체는 상한이 없다 → join 은 백그라운드 핸들을 30s 간격으로
#    확인하며 ELAPSED 를 재계산하고, CAP 을 넘으면 기다림을 끊고 4) 로 간다(무한 join 금지).
gh pr checks $PR_NUMBER                        # snapshot after the watch returns (or on cap)
ELAPSED=$(( ( $(date +%s) - START ) / 60 ))
echo  " #6: elapsed=${ELAPSED}m/cap=${CAP}m verdict=${CI_VERDICT:-pending} "     # 항상 elapsed-vs-cap 로 남긴다

# 4) on-cap (ELAPSED  > = CAP) — 무음 continue 금지, pass 승격 금지.
#    좀비 러너 절차: ../../skills/job-timeout-budget/SKILL.md ⑥ (여기서 절차를 복제하지 않는다)
if [  " $ELAPSED "   -ge  " $CAP "   ]  & &   [  " ${CI_VERDICT:-pending} "   =  " pending "   ]; then
  gh api orgs/{owner}/actions/runners --jq  ' .runners[] | select(.busy==true) | {name,status,busy} ' 
   #  status=offline  & &   busy=true  → 좀비 확정 → force-cancel 을 **제안**(대화형 세션에서만 ASK):
  #    gh api -X POST repos/{owner}/{repo}/actions/runs/{run_id}/force-cancel
  #  좀비 아님 / 디스패치된 워커(물어볼 사람이 없다, orchestration-graph.md §2)
  #    → blockIssue(issue,  ' ci_wait_cap ' ) (holding + 사유 코멘트 + 점유 해제), ci_status 는  ' blocked '   로 반환하고 머지하지 않는다
fi
ConcernRule
Link locationPR body (already embedded at Step 2) — this step never comments
RefreshNon-blocking — tool absent or republish failed ⇒ keep the existing page/URL as-is
CI verdictHard gate — unaffected by whether the refresh succeeded
Wall-clock capcap = max(job-level timeout-minutes) + 10m slack. 산정 SoT: ../../skills/job-timeout-budget/SKILL.md — cap 없는 --watch 는 대기가 아니라 정지다
On-cap좀비 러너 절차(busy==true && status: offline → force-cancel 제안) → 좀비 아니면 BLOCKED('ci_wait_cap'). 무음 continue 금지 · pending→passed 승격 금지
Join logelapsed=Xm/cap=Ym항상 함께 남긴다 (cap 을 안 적으면 다음 사람이 상한을 다시 추측한다)
Re-pushRe-runs this step; refreshes the same URL (no new link, no comment ever created)

Loop contract — pr-lifecycle.step6-ci-wait (필드 정의: ../../rules/orchestration-graph.md §2)

inv:      대기 중에는 push 하지 않는다(새 push 는 이 대기를 종료시키고 새 run 을 대상으로 재진입)
          아티팩트 발행 성공/실패는 CI 판정에 영향을 주지 않는다(발행은 비차단, 판정은 하드 게이트)
prog:     elapsed(min) 단조 증가 + pendingChecks 집합은 단조 비증가
          no-prog: elapsed 가 cap 의 절반을 넘었는데 pendingChecks 가 그대로면 좀비 의심 →
                   ⑥ 의 러너 상태 조회를 1회 선행하고 결과를 로그에 남긴다
term:     모든 체크가 확정(pass|fail) | elapsed  > = cap
budget:   cap = max(job-level timeout-minutes) + 10 min slack. 표본 조회 불가 시 160 min
          (= 선례 최대 job-level 150m + slack 10m). 숫자 근거는 job-timeout-budget SKILLSoT
exhaust:  on-cap 은 좀비 러너 절차 → force-cancel 제안(대화형) 또는 blockIssue(issue,  ' ci_wait_cap ' ).
          무음 continue 금지 · pass 금지 · 미확정을  ' passed '   로 반환 금지
resume:   `gh pr checks $PR_NUMBER` 1회 스냅샷으로 위치 판정(백그라운드 watch 핸들은 세션을 못 넘는다)
          재조회는 읽기 전용이라 멱등 · 재푸시 후에는 새 run 을 대상으로 cap 을 다시 계산한다
log:      join 시 한 줄  " #6: elapsed=12m/cap=160m verdict=pass "   + on-cap 시 러너 조회 결과

Step 7: Squash Merge#

# Squash merge + delete branch
gh pr merge $PR_NUMBER --squash --delete-branch

# Merge commit message (uses PR title)
#  " feat(community): ✨ Implement post list screen (#25) "

이 PR 이 스택에 속해 있으면 이 명령 대신 gh stack merge 를 쓴다 — 아래 "스택 모드 (GitHub Stacked PR)" 절.

Step 8: Verify Issue Closed (merge = Close) ⭐#

ZenHub tracks Pipeline and GitHub state separately. Done ≠ closed; Closed pipeline = GitHub closed (1:1). Closes #N auto-closes the issue only when the PR merges into the repo's default branch. For a hierarchical merge into a parent work-base branch (task/story/, story/epic/, and epic/development when development is not the default), it never fires — so the explicit close below is the primary action here, not a fallback. It also covers silent miss / sync lag on default-branch merges.

먼저 Parent Closure Invariant 를 지킨다 — 이 이슈에 열린 자식이 있으면 닫지 않는다. 자식 조회는 Child Enumeration Contract(GitHub sub-issues, tri-state, fail-closed)를 쓴다. Closes #N 이 이미 닫아 버렸다면 아래 0번이 재오픈해 복구한다. leaf 이슈에서는 gh api 1회로 끝나는 no-op 이다.

# 0. ⛔ 열린 자식 확인 — 열려 있으면 close 하지 않고, 이미 닫혔다면 재오픈해 복구한다.
#    openChildrenStatus(): rules/zenhub-conventions.md →  " Child Enumeration Contract " 
 #    ( " open " =열린 자식 있음 ·  " none " =통과 ·  " unknown " =조회 실패 → 통과 아님)
OWNER=$(gh repo view --json owner -q .owner.login); NAME=$(gh repo view --json name -q .name)
# GD-04: GitHub 네이티브 subIssues (recency 창 없음). state 는 대문자(OPEN/CLOSED).
# ⚠️ 실패 시 gh 는 에러 JSON**stdout** 으로 쏟는다 → 마커는 부분일치로 판정한다
#    (`[  " $X "   =  " __LOOKUP_FAILED__ "   ]` 완전일치는 에러 JSON 이 앞에 붙어 매칭에 실패한다)
OPEN_KIDS=$(gh api graphql -f query= " { repository(owner:\ " $OWNER\ " , name:\ " $NAME\ " ) \
  { issue(number:$ISSUE_NUMBER) { subIssues(first:100) { nodes { number state } } } } } "   \
  -q  ' [.data.repository.issue.subIssues.nodes[] | select(.state== " OPEN " ) | .number] | join( " , " ) '   \
  2 > /dev/null || echo  " __LOOKUP_FAILED__ " )
ISSUE_TYPE= " ${ISSUE_TYPE:-} "     # ZenHub issueType (Sub-task 는 구조적 leaf → unknown 시 통과 허용)

if [[  " $OPEN_KIDS "   == *__LOOKUP_FAILED__*  & &   " $ISSUE_TYPE "   !=  " Sub-task "   ]]; then
  # unknown — 컨테이너일 수 있으므로 차단 (mayClose(): rules/zenhub-conventions.md)
  echo  " ⚠️ #$ISSUE_NUMBER 자식 조회 실패 — 열린 자식 여부 미확인, 명시적 close 보류 " 
   echo  "     → gh auth status 확인 후 재실행하거나, 하위 이슈를 수동 확인하십시오 " 
   exit 1
elif [[  " $OPEN_KIDS "   != *__LOOKUP_FAILED__*  & &   -n  " $OPEN_KIDS "   ]]; then
  echo  " ⛔ #$ISSUE_NUMBER 열린 자식 있음($OPEN_KIDS) — close 하지 않는다 " 
   # `Closes #N` 이 이미 닫아 버린 경우: 재오픈 → (reopen 이후에만) In Progress 로 복구
  [  " $(gh issue view $ISSUE_NUMBER --json state -q .state) "   =  " CLOSED "   ]  & &   {
    gh issue reopen $ISSUE_NUMBER
    gh issue comment $ISSUE_NUMBER --body  " ⚠️ 열린 하위 이슈가 남아 재오픈했습니다: $OPEN_KIDS " 
     # + reopen 이후 In Progress 로 이동 (moveIssueToPipeline — 아래 typescript 블록 참조)
  }
  exit 1   # ⛔ Step 8 종료 — 아래 1·2(명시적 close)는 실행하지 않는다
fi
# ↑ 여기까지 왔으면 status = none (또는 Sub-task 의 unknown 경고 통과) → 정상 close 경로

# `Closes #N` fires only on default-branch merges; hierarchical (non-default base) merges never auto-close.
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name)

# 1. GitHub is the source of truth for open/closed — close explicitly whenever still open
STATE=$(gh issue view $ISSUE_NUMBER --json state -q .state)
if [  " $STATE "   !=  " CLOSED "   ]; then
  # base != default (hierarchical merge), missing keyword, cross-repo link, etc.
  gh issue close $ISSUE_NUMBER --reason completed
fi
// 2. Confirm ZenHub synced GitHub closed → Closed pipeline (fallback on sync lag)
const closed = await mcp__zenhub__searchClosedIssues({ query: `#${issueNumber}` });
if (!closed.find(i = >   i.number === issueNumber)) {
  await mcp__zenhub__updateIssue({ issueId, state:  " CLOSED "   });
}

Step 8.5: Non-Merge Outcomes — abandoned PR / rework ⭐#

Step 8 covers the merge=Close path. But a PR can also be closed without merging (abandoned/superseded) or bounced back for rework (changes requested / CI failed). Both must move the issue off Review/QA so the board reflects reality — otherwise the issue is stranded in Review/QA forever (PR closed-unmerged) or shows "in review" while actually being reworked.

// 파이프라인 id (fail-closed)
const ws = await mcp__zenhub__getWorkspacePipelinesAndRepositories();
const pipe = (name) = >   {
  const p = ws.pipelines.find(p = >   p.name === name);
  if (!p) throw new Error(`파이프라인  ' ${name} '   없음. 라이브: ${ws.pipelines.map(p = >   p.name).join( " ,  " )}`);
  return p;
};

// A) Abandoned: PR이 머지 없이 닫힘(mergedAt == null) → 이슈를 In Progress 로 되돌리고 surface.
//    verify_closed(Step 8)는 GitHub state 기반이라 OPEN 이슈엔 안 걸린다 — 명시 분기가 필요.
const pr = JSON.parse(await Bash(`gh pr view ${prNumber} --json state,mergedAt`));
if (pr.state ===  " CLOSED "   & &   !pr.mergedAt) {
  await mcp__zenhub__moveIssueToPipeline({ issueId, pipelineId: pipe( " In Progress " ).id });
  console.warn(`⚠️ PR #${prNumber} 머지 없이 닫힘 — 이슈 #${issueNumber}In Progress 로 되돌림(재작업/대체 필요).`);
  // 작업 자체를 포기하면 사용자 판단으로 Sprint Backlog/Icebox 재분류 또는 명시적 close(gh issue close).
}

// B) Rework: 리뷰가 변경 요청(CHANGES_REQUESTED) 또는 CI 실패 → Review/QA 에서 In Progress 로 되돌림.
if (reviewDecision ===  " CHANGES_REQUESTED "   || ciStatus ===  " failed " ) {
  await mcp__zenhub__moveIssueToPipeline({ issueId, pipelineId: pipe( " In Progress " ).id });
  // 재작업 후 re-push 하면 PR 갱신 단계에서 다시 Review/QA 로 이동한다(작업-상태 가시성 유지).
}

Step 9: Post-Merge Sync (base branch + submodules) ⭐#

gh pr merge --delete-branch checks out the base branch but does not pull it, and git pull updates only the recorded submodule pointers — not the submodule working trees. Always pull the base branch and run git submodule update so submodules align to the commits the base now points to (prevents build/codegen drift in a Serverpod/Melos monorepo). No-op when the repo has no submodules.

# base_branch = PR base (hierarchy: parent branch, independent: development)
git checkout $BASE_BRANCH
git pull --ff-only origin $BASE_BRANCH
git submodule sync --recursive
git submodule update --init --recursive

# Verify before returning — required to set post_merge_sync.verified = true
HEAD_BRANCH=$(git rev-parse --abbrev-ref HEAD)
test  " $HEAD_BRANCH "   =  " $BASE_BRANCH "   || echo  " ❌ not on base branch: $HEAD_BRANCH " 
 git status -sb | head -1 | grep -q  ' behind '   & &   echo  " ❌ base is behind origin — re-pull "

This step is part of the merge, not an optional epilogue. A merged PR is only "done" once HEAD is on $BASE_BRANCH and not behind origin. Return post_merge_sync.verified = true only after the two checks above pass; otherwise set success = false and report the merge as incomplete. Never report a merged PR as finished while still on the deleted feature branch or a stale base.


스택 모드 (GitHub Stacked PR)#

이 절은 선택형 실행 모드다. 기본값은 위의 Step 1~9 그대로이며, GitHub 네이티브 Stacked PR 은 기존 5레벨 브랜치 계층을 대체하지 않는다. 순차 의존이 있는 형제 작업들을 한 줄로 늘어놓은 (linearize) 경우에만 켠다 — 서로 독립인 형제는 지금처럼 병렬 워크트리로 둔다. 채택 판단 기준·CI 비용 맞교환·설치·exit code 표는 stacked-prs 가 SSOT 이므로 여기서 복제하지 않는다.

⚠️ Stacked PR 은 2026-07-30 기준 public preview 이고, 머지 큐 지원은 점진 롤아웃 중이다. 저장소에 아직 도착하지 않았을 수 있으므로, 스택 모드는 켜기 전에 사용 가능 여부를 확인한다.

단계 대응표#

단계기본(비스택)스택 모드
Step 2 PR 생성gh pr create --title ... --base ...gh stack submit [--auto] [--open] — 브랜치 push + PR 생성/갱신 + 스택 생성/갱신을 한 번에
상태 조회gh pr view / gh pr checksgh stack view --json — 스택 전체 층·PR·상태를 한 번에. 층별 CI 판정은 그 층 PR 에 gh pr checks 로 확인
Step 7 머지gh pr merge $PR_NUMBER --squash --delete-branchgh stack merge <pr-number> --squash -y — 지정 PR 까지 all-or-nothing
Step 9 머지 후 동기화git checkout basegit pull --ff-only → submodulegh stack sync --prune + 기존 Step 9 절차 전부 그대로

gh stack submit--auto에디터를 생략하고 제목을 자동 생성한다는 뜻이며(기본은 draft 생성), --open 을 함께 주면 ready for review 로 만든다. 입력 파라미터 auto_mergegh pr merge --auto 와는 아무 관계가 없다 — 이름만 닮았을 뿐이다.

머지 실행과 그 이후#

# 머지 — 지정 PR 까지의 PR 들이 단일 all-or-nothing 연산으로 머지된다
gh stack merge $PR_NUMBER --squash -y

# 머지 후: 다음 미머지 PR 은 서버가 자동으로 rebase + 스택 base 로 retarget 해 맨 아래로 내려온다.
# 로컬 스택은 아래 명령으로 맞춘다 (--prune 이 머지된 PR 의 브랜치를 정리한다)
gh stack sync --prune

⚠️ gh stack sync --prune 은 Step 9 를 대체하지 않는다. 스택 브랜치를 맞출 뿐이며, base 브랜치 pull 과 git submodule sync --recursive / git submodule update --init --recursive 까지 수행하는지는 문서에 명시가 없다. Step 9 의 base pull + 서브모듈 갱신 절차와 post_merge_sync.verified 검증은 스택 모드에서도 그대로 유효하다 — 생략하면 Serverpod/Melos 모노레포에서 빌드·코드생성 드리프트가 그대로 재발한다.

⚠️ 스택 모드 3대 주의#

⚠️ 1. auto-merge 미지원. "Auto-merge is not supported for stacked pull requests." 스택에 속한 PR 에 gh pr merge --auto 계열을 쓰지 마라. 입력 파라미터 auto_merge = true 라도 스택 모드에서는 auto-merge 를 켜지 않고, 조건이 갖춰진 시점에 gh stack merge명시적으로 머지한다.

⚠️ 2. 중간 단독 머지 불가. 중간 층 PR 만 따로 머지할 수 없고, 그 아래 PR 들이 항상 함께 머지된다. 머지 가능한 단위는 가장 아래 미머지 PR 부터 연속된 묶음뿐이며 순서를 건너뛸 수 없다. 따라서 리뷰 승인·CI 통과도 아래층부터 확보해야 한다 — 위층만 초록이면 아무것도 머지되지 않는다.

⚠️ 3. API 로 머지할 때. REST/GraphQL 로 머지를 실행한다면 stacked PR 은 비동기 머지 API 를 써야 한다. 일반 PR 머지 API 를 그대로 재사용하지 마라.

머지 큐 · 브랜치 보호 — SSOT 위임#

머지 큐 동작(순서 투입, 이젝트 시 위층 동반 제거)과 브랜치 보호 규칙의 강제 범위는 stacked-prs 의 "머지 의미론"·"CI 비용 맞교환" 절이 SSOT 다 — 여기서 재서술하지 않는다. 이 에이전트가 실행 시 지켜야 할 것만 남긴다:

  • 큐에서 PR 이 빠지면 개별 PR 을 다시 넣지 말고, 원인을 고친 뒤 스택 단위로 다시 머지한다.
  • 큐 대기 중에는 gh stack modify 로 스택을 재구성하지 않는다(전제조건 위반).
  • 게이트를 건너뛰는 경로는 없다 — 머지 요구사항 우회는 stacked PR 에서 미지원이다.

이슈 종료#

⚠️ 스택 머지 시 각 층의 Closes #N 이 어떻게 동작하는지는 문서에 명시가 없다. 스택이라고 자동 종료를 가정하지 마라. Step 8 의 명시적 close 경로(열린 자식 확인 → gh issue close → ZenHub Closed 동기화 확인)를 스택 모드에서도 그대로 수행한다.

관련 문서#

  • stacked-prs — 스택 모드 SSOT. 채택 판단, CI 비용 맞교환, 설치, 명령 레퍼런스, exit code 별 조치, 비대화형 실행 주의가 모두 여기에 있다. 이 에이전트에서 실패 처리 분기가 필요하면 그 문서의 "실패 모드 — exit code 별 조치" 절을 본다.

Review Comment Analysis#

Comment Type Classification#

TypeKeywordsHandling
Required fixmust, required, fixFix immediately
Recommended fixshould, consider, suggestFix if possible
Question?, why, howAnswer or add code comment
ApprovalLGTM, approved, goodNo fix needed

Auto-applicable Items#

  • Code style fixes (formatting)
  • Naming changes
  • Import cleanup
  • Comment additions
  • Simple logic fixes

Items Requiring Manual Review#

  • Architecture change suggestions
  • Business logic changes
  • Performance optimization suggestions
  • Security-related issues

CI Check Handling#

Checks That Must Pass#

CheckDescriptionOn Failure
buildBuild successFix build errors
testTests passFix tests
lintLint passesFix formatting/lint
analyzeStatic analysisFix analysis errors

Failure Handling#

1. Analyze failure cause
   ↓
2. Determine if auto-fix is possible
   ↓
3. Apply fix
   ↓
4. Per-Push Verification Gate (format → analyze 0 → dcm error 0)5. Commit  &   push
   ↓
6. Wait for CI re-run       (= Step 6 계약으로 재진입, 그 cap 을 그대로 쓴다)7. Verify success/failure   (실패면 다음 라운드 — 단, 아래 계약대로 **최대 3 라운드**)

handleCiFailure Contract (bounded CI fix ∥ re-push loop) ⭐#

../../commands/run.md Step 10.5 는 await handleCiFailure(pr, ciResult) 를 호출하지만 그 계약이 어디에도 선언돼 있지 않았다 — 호출 지점이 위임했을 뿐 루프는 사라지지 않으므로 (../../rules/orchestration-graph.md §2 "위임된 루프도 루프다"), 계약의 자리는 여기다. 이 선언이 그 문서 §1.1 의 S10.5 ==> S8.5 bound:? invalidates:S8.5bound:3 invalidates:S8.5,S10.5 로 확정한다.

항목
시그니처handleCiFailure(pr, ciResult) → { rounds, failingChecks, outcome }
outcomefixed | ci_exhausted머지로 진행 가능한 값은 fixed
라운드 1회진단 → 수정 → Step 8.5 per-push gate → push → CI 재대기(Step 6 계약, cap 포함)
절대 금지소진 후 재푸시 · // ignore: 삽입이나 재시도만으로 초록 만들기 · 미확정을 passed 로 반환

Loop contract — run.step10_5-ci-fix-repush (필드 정의: ../../rules/orchestration-graph.md §2)

inv:      push 되는 커밋은 예외 없이 Step 8.5 per-push gate 를 통과한 것 (analyze 0 · dcm error 0)
          `// ignore:` 삽입 · `LEFTHOOK=0` · 재실행만으로 초록을 만드는 것 금지
          같은 PR·같은 아티팩트 URL·같은 마커 코멘트를 갱신한다(새 링크/새 코멘트 금지)
prog:     failingChecks 가 **진부분집합으로 축소**되거나, 진단된 원인의 KIND 가 직전 라운드와 다르다
          (kind ∈ {build, test, lint, analyze, infra/runner, flake}) — 둘 다 아니면 진전 0
          no-prog: 같은 실패 집합 + 같은 KIND 로 두 번째 수정 금지 →
                   ../sequential-workflow.md 의 Rung 2(`/cc-dev:unstuck`)로 종류를 바꾼다
term:     failingChecks ==(outcome=fixed) | rounds == 3 (outcome=ci_exhausted)
budget:   3 rounds (라운드 = 수정 1 + push 1 + CI 재대기 1). 라운드마다 Step 6 이 자기 cap 으로
          재진입하며, 중첩 대기는 3 × cap 을 넘지 않는다
exhaust:  라벨 `ci-blocked` + BLOCKED( ' ci_exhausted ' ) + 제어 반환. **다시 push 하지 않고 머지하지 않는다**
resume:   `gh pr checks` 실측 + PR 라벨(`ci-blocked`) + 라운드 기록 코멘트로 위치 판정
          (대화 카운터는 /clear 를 못 넘으므로 예산이 아니다) · 라운드 기록은 마커 코멘트 1개에 upsert
log:      라운드당 한 줄  " ci-fix #2: failing 3→1 kind=test→analyze " 
           + 소진 시 남은 실패 체크 이름 전부와 마지막 진단 KIND

소진(ci_exhausted) 시 실행하는 것:

gh pr edit  " $PR_NUMBER "   --add-label ci-blocked
gh pr comment  " $PR_NUMBER "   --body  " ⛔ CI 수정 3라운드 소진 — 남은 실패: $FAILING. 사람 개입 필요. " 
 # + 보드 반영: blockIssue(issue,  ' ci_exhausted ' )[Pipeline State Contract](../../rules/zenhub-conventions.md#pipeline-state-contract)
#   (holding 이동 + createBlockage(차단자가 이슈일 때만) + 사유 코멘트 + 점유 해제를 한 묶음으로)
#   (createBlockage + holding 컬럼 이동) · 이슈는 Review/QAIn Progress (Step 8.5 B)
# ⛔ 여기서 제어를 반환한다. 재푸시 금지 · 머지 금지 · ci_status =  ' blocked '   로 보고한다.

Error Handling#

Common Errors#

ErrorCauseResolution
PR already existsPR exists for same branchUpdate existing PR
merge conflictConflict with base branchConflict resolution needed
checks failedCI failureFix and retry within 3 rounds (handleCiFailure); move issue Review/QA → In Progress (Step 8.5 B)
ci exhausted3 rounds consumed without shrinking the failing setLabel ci-blocked + BLOCKED('ci_exhausted') + return control — never push again, never merge
ci wait capWall-clock cap hit with checks still pending (zombie runner suspected)Zombie procedure (busy==true & status: offline → force-cancel) → else BLOCKED('ci_wait_cap'); never a silent continue
no AI reviewReview wait ended none-posted / unknownLog + record in the PR body ## Review Wait; never read as "review found nothing"
review requiredReview approval neededWait for review
changes requestedReview bounced back for reworkMove issue Review/QA → In Progress; re-push returns it to Review/QA (Step 8.5 B)
PR closed (unmerged)Abandoned / superseded PRMove issue → In Progress + surface for reclassification — never leave it in Review/QA (Step 8.5 A)

Recovery Strategy#

# Resolve conflicts
git fetch origin development
git rebase origin/development
# Manually resolve conflicts
git add .
git rebase --continue
# Per-Push Verification Gate — rebase 결과물도 예외 없이 push 전 재검증
melos run format  & &   dart fix --apply
melos run analyze        # 0건 필수 (실패 시 push 금지)
melos run dcm:analyze    # error 0건 필수 (실패 시 push 금지)
git push --force-with-lease

Post-Merge Check ⚠️#

After PR merge, verify the following on the development branch.

Backend Change Detection#

When merged commits include the following files, Backend code generation is required:

File PatternMeaning
backend/kobic_server/lib/src/protocol/**/*.spy.yamlModel file changes
backend/kobic_server/lib/src/feature/**/*.dartEndpoint/service changes

Auto-Execution#

# 1. Update development branch
git checkout development
git pull origin development

# 2. Code generation on Backend changes [required]
melos run backend:pod:generate

# 3. Commit generated code (if changes exist)
git add .
git commit -m  " chore(backend): 🔧 code generation " 
 git push origin development

⚠️ Important#

The same applies when merging development into other branches:

# After merging development in a feature branch
git merge development

# If there were Backend changes, run code generation
melos run backend:pod:generate

Skipping this step causes:

  • Type mismatch between kobic_client and kobic_server
  • Unable to call new APIs from frontend
  • Build errors

Key Rules#

  1. Follow Title Rules: Conventional Commits + Gitmoji + Korean
  2. Issue Link Required: Always include Closes #issue_number
  3. Wait for Review: Wait up to 5 minutes for an AI-author comment (allowlist: commands/pr/review-processor.md). 코멘트 총량으로 판정하지 않는다 — 파이프라인 자기 코멘트가 섞인다. 결과는 reviewed / none-posted / unknown 삼분이고, reviewed 만 무음 통과다
  4. Apply Feedback: Analyze and auto-apply review comments
  5. Squash Merge: Always squash merge to clean up history
  6. Merge = Close: Closes #N auto-closes only on default-branch merges. For a hierarchical merge into a parent work-base branch (story/, epic/) it never fires, so run gh issue close explicitly — the primary action, not a fallback. After every merge confirm the issue is closed on GitHub and Closed on ZenHub (updateIssue state:CLOSED if not synced). Never leave it in Done.
  7. Branch Cleanup: Auto-delete source branch after merge
  8. Backend Code Generation: Run backend:pod:generate on backend changes after merge
  9. Bounded Waits, Bounded Retries: CI 대기는 cap = max(job-level timeout-minutes) + 10m 에서 멈추고 좀비 러너 절차로 넘어간다(skills/job-timeout-budget), CI 수정은 3 라운드에서 ci-blocked + BLOCKED('ci_exhausted') 로 멈춘다. 무음 continue·무한 대기·미확정 pass 는 전부 금지 — 루프/게이트 계약 규약은 ../../rules/orchestration-graph.md