pr-lifecycle-agent
이 에이전트는 Pull Request의 전체 라이프사이클을 관리합니다.
| 항목 | 내용 |
|---|---|
| 모델 | 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 --draft --title "feat(community): ✨ Implement post list screen" --body "..." --base development
# 준비 완료로 올려 무거운 검사를 한 번 띄운 뒤 통과 대기
gh pr ready <PR번호>
gh pr checks <PR번호> --watch --fail-fast
# Squash 병합 + 작업 브랜치 삭제
gh pr merge <PR번호> --squash --delete-branch
안에서 무슨 일이 벌어지나요#
총 9단계로 진행됩니다.
- PR 준비 — 브랜치가 올라갔는지 확인하고, 커밋 목록을 모아 PR 제목·본문을 만듭니다.
- PR 생성 — 작업내역을 정리한 HTML 문서를 먼저 커밋해 브랜치와 함께 올리고, 그 위치와 링크를 PR 본문에 담아 이슈와 연결된 PR을 만듭니다. PR 은 초안으로 만들어 가벼운 검사만 돌게 합니다.
- 리뷰 대기 (최대 5분) — 먼저 이 저장소에 AI 리뷰어가 있는지 봅니다. PR 마다 리뷰하는 워크플로도 없고 최근 머지된 PR 들에도 AI 리뷰가 없었다고 확인되면, 기다리지 않고 그 사실만 PR 본문에 남깁니다. 확인이 안 되면 평소처럼 기다립니다. 기다릴 때는 Gemini·Claude·Copilot 같은 AI 리뷰어가 쓴 의견이 달릴 때까지 30초마다 확인합니다. 파이프라인이 스스로 남긴 코멘트는 리뷰로 세지 않습니다. 결과는 달렸다 / 안 달렸다 / 확인 불가 / 리뷰어 없음 중 하나로 확정합니다. "달렸다"가 아니면 로그와 PR 본문에 남기고, "지적 없음"으로 바꿔 읽지 않습니다.
- 리뷰 의견 수집·분석 — 달린 의견을 모아 "고쳐야 할 것"을 가려냅니다.
- 의견 반영 (필요 시) — 고칠 게 있으면 코드를 수정해 커밋·푸시합니다.
-
CI 상태 확인 — 기다리기 전에 초안을 준비 완료로 올려 무거운 검사(통합·골든·빌드 등)를 한 번만 띄웁니다(초안의 초록은 무거운 검사를 건너뛴 결과라 통과가 아닙니다). 그다음 빌드·테스트·린트·정적분석이 모두 통과할 때까지 기다립니다. 실패는 로컬에서 재현해 고친 뒤 올리고, 재현되지 않는 일시적 실패(플레이크·러너 문제)는 실패한 검사만 다시 돌립니다. 다만
무한정 기다리지 않습니다 — 워크플로에 걸린 시간 상한에서 뽑은 대기 상한을 넘기면, 기계가 물린 것(좀비 러너)인지 확인해 강제 종료를 제안하고, 아니면 차단으로 표시해 넘깁니다. 조용히 통과시키는 일은 없습니다. 실패를 고치는 시도는
최대 3라운드이고, 소진되면 PR에
ci-blocked표시를 달고 손을 뗍니다. 검사 결과는 PR 화면의 체크가 정본이라, 2단계에서 커밋한 작업내역 문서는 고치지 않습니다. - Squash 병합 — 검사를 통과하면 하나로 합쳐 병합하고 작업 브랜치를 지웁니다.
- 이슈 닫힘 확인 — 먼저 하위 작업이 전부 끝났는지 확인하고(하나라도 열려 있으면 닫지 않고, 이미 닫혔다면 다시 엽니다), 그다음 GitHub와 ZenHub 양쪽에서 이슈가 Closed로 닫혔는지 확인하고 안 됐으면 직접 닫습니다.
- 병합 후 동기화 — base 브랜치를 최신으로 당기고 서브모듈을 맞춰, 빌드가 어긋나지 않게 정리합니다.
⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)
Role and Responsibilities#
This agent manages the entire Pull Request lifecycle.
- PR Creation: Create PRs linked to issues
- Review Wait: Wait for AI-author review comments (5 min cap) — outcome is
reviewed/none-posted/unknown, ornot-configuredwhen a pre-check proves the repo has no AI reviewer (the wait is skipped and recorded); onlyreviewedproceeds silently - Feedback Application: Analyze review comments and apply code fixes
- 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#
| Parameter | Required | Type | Description |
|---|---|---|---|
branch_name | ✅ | string | Source branch name |
issue_number | ✅ | number | Issue number to link |
issue_title | ✅ | string | Issue title for PR title |
issue_type | ❌ | string | Feature | Bug | Task (default: Feature) |
base_branch | ❌ | string | Target branch (default: development) |
auto_merge | ❌ | boolean | Auto-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. Only 'reviewed' may proceed silently; the others MUST be logged AND recorded
// in the PR body. 'unknown' is never a pass (undet:fail). 'not-configured' means the pre-check
// proved the repo has no AI reviewer, so the wait was skipped — not applicable, not a pass.
review_wait: 'reviewed' | 'none-posted' | 'unknown' | 'not-configured';
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 Type | PR Type | Gitmoji |
|---|---|---|
| Feature | feat | ✨ |
| Bug | fix | 🐛 |
| Task | chore | 🔧 |
| Sub-task | feat/fix/chore | Depends on context |
Examples#
feat(community): ✨ Implement post list screen
fix(auth): 🐛 Fix login token refresh error
chore(deps): 🔧 Update package versionsPR 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
## 📄 작업내역
- 경로: `.claude/docs/{scope}/pr-work-{issue_number}.html`
- 링크: https://github.com/{owner}/{repo}/blob/{head_branch}/.claude/docs/{scope}/pr-work-{issue_number}.html
{커밋하지 못했으면 `## 📄 작업내역 (마크다운 폴백)` + 요약으로 바꾼다 — skills/pr-work-artifact}
## Screenshots (if applicable)
{Screenshots or N/A}
## Review Wait
{Step 3 outcome — omit this section when `reviewed`; on `none-posted` / `unknown` / `not-configured`
leave the 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 │
├─────────────────────────────────────────────────────────┤
│ - Write work-log HTML, commit it (docs-only commit) │
│ (non-blocking — markdown fallback if it can't commit) │
│ $ git push -u origin {branch_name} │
│ $ gh pr create --draft --title ... --body ... (link) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 3: Wait for Review (5 min) │
├─────────────────────────────────────────────────────────┤
│ - Pre-check: no AI reviewer -> skip (not-configured) │
│ - 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 │
│ (= /cc-dev:run Step 8.5; fail ⇒ push blocked) │
│ $ git push │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 6: Check CI Status (hard gate) │
├─────────────────────────────────────────────────────────┤
│ $ gh pr ready {pr_number} (if draft) → wait new run │
│ $ gh pr checks {pr_number} --watch (background first) │
│ - The work-log HTML was committed + linked at Step 2 — │
│ this step never edits it (PR checks are the record) │
│ - Join the CI result and gate on it; no comment here │
│ - 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#
Commit the work-log HTML before creating the PR, push it with the branch, then create the PR with
its repo path and GitHub link already 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). The doc is
non-blocking — ignored path, oversized file or failed commit ⇒ inline a markdown summary in the body instead.
# Write the work-log HTML and commit it — a docs-only commit, so the Per-Push Gate does not re-run for it
git add .claude/docs/community/pr-work-25.html
git commit -m "docs(community): 📝 작업내역 (#25)"
# Push branch (code commits + the doc commit)
git push -u origin feature/25-community-list
# Create PR — path + link already in the body, not a follow-up comment
# 초안으로 연다 — 2층 CI 저장소에서 초안은 가벼운 층만 돈다. 무거운 층은 Step 6 이 ready 로 올려 한 번 띄운다
# (skills/ci-receipts A-1·A-2). 단층 CI 저장소에서도 해가 없다.
gh pr create --draft \
--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
## 📄 작업내역
- 경로: `.claude/docs/community/pr-work-25.html`
- 링크: https://github.com/{owner}/{repo}/blob/feature/25-community-list/.claude/docs/community/pr-work-25.html
↳ review guide · design rationale · local verification results · out of scope
> GitHub 은 HTML 을 소스로 보여 준다 — 렌더된 화면은 체크아웃에서 `open .claude/docs/community/pr-work-25.html` 로 연다.
---
Closes #25
🤖 Generated with [Claude Code](https://claude.ai/claude-code)
EOF
)" \
--base developmentStep 3: Review Wait#
종료 조건은 AI 리뷰어가 남긴 코멘트의 존재이며, 코멘트 총량은 조건이 아니다. PR 에는 리뷰가 아닌
코멘트(사람의 메모, 파이프라인의 경고·차단 코멘트, 예전 방식이 남긴 ``
마커 코멘트 등)도 달리므로, .comments | length > 0 은 그것을 리뷰로 오인해 첫 tick 에 빠져나온다 —
아무것도 기다리지 않은 것이다. 작성자 allowlist 는 ../../commands/pr/review-processor.md → "Reviewer Identification"
을 그대로 쓴다(여기서 복제·확장하지 않는다).
3-0. 사전 판정 — 이 저장소에 AI 리뷰어가 있는가. AI 리뷰어가 아예 없는 저장소에서 5분을 기다리면
결과는 매번 none-posted 이고 시간만 쓴다. 그래서 폴링 전에 두 신호를 보고, 둘 다 0건으로 확인됐을
때만 대기를 생략한다.
| 신호 | 보는 곳 | 왜 |
|---|---|---|
| A. PR 트리거 AI 리뷰 워크플로 | PR base 브랜치의 .github/workflows/* 중 anthropics/claude-code-action 을 쓰고 pull_request/pull_request_target 로 트리거되는 파일 | 이 PR 에도 리뷰가 달릴 수 있다 |
| B. 최근 AI 리뷰 흔적 | 최근 머지된 PR 5건의 코멘트·리뷰 중 아래 allowlist 작성자 | 워크플로 없이 붙는 앱형 리뷰어(gemini-code-assist · copilot)를 잡는다 |
- A·B 모두 0 →
not-configured. 폴링하지 않고 PR body 에 기록한 뒤 Step 4 로 간다. - 하나라도 1 이상 → 아래 폴링을 그대로 돈다.
- 조회가 하나라도 실패 → 판정 불가이므로 생략하지 않는다. 폴링을 그대로 돈다 — 5분은 비용이고, 잘못 생략하면 리뷰를 놓친다.
@claude멘션에만 반응하는 워크플로(issue_comment등 PR 이벤트가 아닌 트리거)는 A 로 세지 않는다 — PR 이 열려도 리뷰가 달리지 않는다.- 한계: 앱형 리뷰어를 막 설치해 머지된 PR 에 아직 흔적이 없으면 B 가 0 이라, 그사이 몇 PR 은 대기를 생략할 수 있다.
# AI 리뷰어 base login — allowlist SoT: commands/pr/review-processor.md "Reviewer Identification"
# gemini-code-assist[bot] · claude[bot] · github-copilot[bot]
# ⚠️ gh 가 `[bot]` 접미사를 붙여 줄 때와 빼고 줄 때가 있으므로 양쪽을 정규화해 비교한다
# 3-0. 사전 판정 — A(PR 트리거 AI 리뷰 워크플로) · B(최근 머지 PR 5건의 AI 리뷰 흔적)
PRECHECK=unknown # none | present | unknown — none 일 때만 대기를 생략한다
BASE_REF=$(gh pr view "$PR_NUMBER" --json baseRefName -q .baseRefName 2>/dev/null) || BASE_REF=''
if [ -n "$BASE_REF" ] && git fetch origin "$BASE_REF" --quiet 2>/dev/null; then
A=0
for f in $(git ls-tree --name-only "origin/$BASE_REF" .github/workflows/ 2>/dev/null); do
BODY=$(git show "origin/$BASE_REF:$f" 2>/dev/null) || continue
printf '%s' "$BODY" | grep -q 'anthropics/claude-code-action' || continue
# 블록형 `pull_request:` 와 한 줄형 `on: [push, pull_request]` 를 센다.
# `pull_request_review_comment:` 같은 멘션 트리거는 뒤에 `_` 가 붙어 걸리지 않는다
printf '%s' "$BODY" | grep -qE '^[[:space:]]*pull_request(_target)?:|^on:.*pull_request(_target)?([^_a-z]|$)' \
&& A=$((A + 1))
done
if RECENT=$(gh pr list --state merged --limit 5 --json comments,reviews 2>/dev/null); then
B=$(printf '%s' "$RECENT" | jq '
["gemini-code-assist", "claude", "github-copilot"] as $bots
| [ .[] | (.comments[]?, .reviews[]?)
| ((.author.login // "") | sub("\\[bot\\]$"; ""))
| select(. as $a | $bots | index($a)) ] | length')
# B 가 숫자가 아니면(jq 실패) 비교가 거짓이 되어 present 로 떨어진다 — 생략하지 않는 쪽이다
if [ "$A" -eq 0 ] && [ "$B" -eq 0 ]; then PRECHECK=none; else PRECHECK=present; fi
fi
fi
echo "#3: precheck=$PRECHECK workflows=${A:-?} recentAi=${B:-?}"
REVIEW_WAIT=unknown # reviewed | none-posted | unknown | not-configured — 기본값은 통과가 아니다
FAILS=0
if [ "$PRECHECK" = none ]; then
REVIEW_WAIT=not-configured # 폴링 0회 — 이 사실을 PR body 에 남긴다(아래)
else
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
fi
echo "#3: outcome=$REVIEW_WAIT"Outcome (삼분 판정 규약: ../../rules/orchestration-graph.md §3 · not-configured 는 §3.1 의 「구조적으로 해당 없음」이다)
| outcome | 뜻 | 처리 |
|---|---|---|
reviewed | AI 작성자 코멘트/리뷰 ≥ 1건 | 유일하게 무음 통과 가능 → Step 4 |
none-posted | 조회는 성공했고 5분 예산 소진, AI 코멘트 0건 | 로그 + PR body 내구 기록 후 진행. "AI 리뷰가 지적 없음" 으로 읽지 않는다 |
unknown | 조회 3연속 실패 — 리뷰 유무 자체를 모른다 | 통과 아님(undet:fail). PR body 기록 + review_wait:'unknown' 반환 → Step 12 머지 승인 시 사람에게 노출 |
not-configured | 사전 판정에서 A·B 모두 0건으로 확인 — 이 저장소엔 AI 리뷰어가 없다 | 폴링 생략. 로그 + PR body 기록 후 진행. 해당 없음이지 통과가 아니다 — "지적 없음" 으로 읽지 않는다 |
none-posted / unknown / not-configured 는 PR body 의 ## Review Wait 섹션에 남는다(콘솔은 내구 기록이 아니다).
## Review Wait
- ⚠️ AI review: none-posted (5m/5m, 0 comments from gemini|claude|copilot)
→ 리뷰가 도달하지 않은 것이며, 지적이 없다는 뜻이 아니다.
- ℹ️ AI review: not-configured (PR 트리거 AI 리뷰 워크플로 0 · 최근 머지 PR 5건의 AI 코멘트 0) — 대기 생략
→ 이 저장소엔 AI 리뷰어가 없다. 지적이 없다는 뜻이 아니다.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: precheck == none (not-configured — 폴링 0회) | aiComments >= 1 (reviewed) |
tick == 10 (none-posted) | FAILS == 3 (unknown)
budget: 사전 판정 1회(PR 조회 2번 + base 워크플로 읽기) + 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: 사전 판정 한 줄 "#3: precheck=present workflows=1 recentAi=0" + 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
# ⚠️ 수집물에는 리뷰가 아닌 코멘트(사람 메모·파이프라인 경고·예전 방식의 작업내역 마커 코멘트)가 섞여 있다.
# 분석 대상은 Step 3 과 **같은 작성자 allowlist** 로 좁힌다 (allowlist SoT:
# commands/pr/review-processor.md "Reviewer Identification"). review_wait != 'reviewed' 이면
# "지적 0건" 이 아니라 **리뷰가 없었음** 이므로, 그 상태로 무음 통과시키지 않는다.Step 6: Check CI Status (hard gate)#
The work-log HTML was already committed and linked in the PR body at Step 2, and this step never
edits it — an edit means a commit, a push and a fresh CI run that makes the doc stale again; the PR
checks are the record of CI (plugins/cc-dev/rules/artifact-publishing.md §9). This step only waits
for CI and gates on the result. No PR comment is created here.
# 0-a) 초안이면 준비 완료로 올린다 — 초안의 초록은 무거운 잡이 skipped 된 결과라 통과가 아니다(skills/ci-receipts).
# ready 직후 곧바로 watch 하면 ready 전에 끝난 초안 run 의 체크만 보고 끝날 수 있다 —
# ready 전 run id 와 비교해 새 run 이 보일 때까지(15초 × 12) 기다린다. 없으면 판정 불가로 차단한다(시간 경과로 단층 CI 라고 추정하지 않는다).
# 조회 실패를 "초안 아님"으로 접지 않는다 — 판정 불가는 통과가 아니다.
DRAFT=$(gh pr view "$PR_NUMBER" --json isDraft -q .isDraft) || { echo "#6: 초안 여부 조회 실패 — 판정 불가"; exit 1; }
if [ "$DRAFT" = "true" ]; then
HEAD_SHA=$(gh pr view "$PR_NUMBER" --json headRefOid -q .headRefOid) || exit 1
run_ids() {
local ids
ids=$(gh run list --commit "$HEAD_SHA" --event pull_request --limit 100 --json databaseId -q '.[].databaseId') || return 1
printf '%s\n' "$ids" | sort
}
BEFORE=$(run_ids) || exit 1
gh pr ready "$PR_NUMBER" || exit 1
NEW=""
for _ in $(seq 12); do
sleep 15
AFTER=$(run_ids) || exit 1
NEW=$(comm -13 <(echo "$BEFORE") <(echo "$AFTER"))
[ -n "$NEW" ] && break
done
if [ -z "$NEW" ]; then
echo "#6: 새 CI run 확인 불가 — 트리거·대기열 확인 필요. 기존 초안 체크로 머지하지 않는다"
exit 1
fi
echo "#6: draft→ready · 새 run $NEW"
elif [ "$DRAFT" != "false" ]; then
echo "#6: 초안 여부 판정 불가"
exit 1
fi
# 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) The work-log HTML is not touched here — PR checks are the record of CI
# 3) Join the CI result **within the cap**.
# ⚠️ `--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| Concern | Rule |
|---|---|
| Draft | 기다리기 전에 gh pr ready — 초안의 초록(무거운 잡 skipped)은 통과가 아니다. ready 전 run 과 비교해 새 run 을 확인한 뒤 watch, 3분 안에 없으면 판정 불가로 차단한다. 단층 CI 는 워크플로에 초안 분기가 없고 기존 실행이 필요한 검사를 전부 수행했음을 확인한 경우에만 기존 체크를 쓴다 |
| Work-log doc | Committed + linked in the PR body at Step 2 — this step never edits it and never comments |
| CI verdict | Hard gate — read from the PR checks, never from the doc |
| Wall-clock cap | cap = 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 log | elapsed=Xm/cap=Ym 를 항상 함께 남긴다 (cap 을 안 적으면 다음 사람이 상한을 다시 추측한다) |
| Re-push | Re-runs this step. If the work-log must change, it rides in the fix commit — never a docs-only commit/push, no new link, no comment |
Loop contract — pr-lifecycle.step6-ci-wait (필드 정의: ../../rules/orchestration-graph.md §2)
inv: watch 전에 PR 이 초안이 아니다(0-a) — 초안이면 ready 후 ready 뒤 새 run 을 대상으로 기다린다
대기 중에는 push 하지 않는다(새 push 는 이 대기를 종료시키고 새 run 을 대상으로 재진입)
대기 중에는 작업내역 문서를 고치지 않는다 — CI 판정은 PR 체크만 본다(하드 게이트)
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 SKILL 이 SoT
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;Closedpipeline = GitHubclosed(1:1).Closes #Nauto-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/, andepic/→developmentwhendevelopmentis 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 api1회로 끝나는 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-branchchecks out the base branch but does not pull it, andgit pullupdates only the recorded submodule pointers — not the submodule working trees. Always pull the base branch and rungit submodule updateso 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_BRANCHand not behind origin. Returnpost_merge_sync.verified = trueonly after the two checks above pass; otherwise setsuccess = falseand 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 --draft --title ... --base ... | gh stack submit [--auto] — 브랜치 push + PR 생성/갱신 + 스택 생성/갱신을 한 번에. --open 은 주지 않는다(초안 유지 → Step 6 이 층마다 ready) |
| 상태 조회 | gh pr view / gh pr checks | gh stack view --json — 스택 전체 층·PR·상태를 한 번에. 층별 CI 판정은 그 층 PR 에 gh pr checks 로 확인 |
| Step 7 머지 | gh pr merge $PR_NUMBER --squash --delete-branch | gh stack merge <pr-number> --squash -y — 지정 PR 까지 all-or-nothing |
| Step 9 머지 후 동기화 | git checkout base → git pull --ff-only → submodule | gh stack sync --prune + 기존 Step 9 절차 전부 그대로 |
gh stack submit 의 --auto 는 에디터를 생략하고 제목을 자동 생성한다는 뜻이며(기본은 draft
생성), --open 을 함께 주면 ready for review 로 만든다. 입력 파라미터 auto_merge 나
gh 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→ ZenHubClosed동기화 확인)를 스택 모드에서도 그대로 수행한다.
관련 문서#
- stacked-prs — 스택 모드 SSOT. 채택 판단, CI 비용 맞교환, 설치, 명령 레퍼런스, exit code 별 조치, 비대화형 실행 주의가 모두 여기에 있다. 이 에이전트에서 실패 처리 분기가 필요하면 그 문서의 "실패 모드 — exit code 별 조치" 절을 본다.
Review Comment Analysis#
Comment Type Classification#
| Type | Keywords | Handling |
|---|---|---|
| Required fix | must, required, fix | Fix immediately |
| Recommended fix | should, consider, suggest | Fix if possible |
| Question | ?, why, how | Answer or add code comment |
| Approval | LGTM, approved, good | No 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#
| Check | Description | On Failure |
|---|---|---|
build | Build success | Fix build errors |
test | Tests pass | Fix tests |
lint | Lint passes | Fix formatting/lint |
analyze | Static analysis | Fix analysis errors |
Failure Handling#
1. Analyze failure cause — 실패한 검사 · 테스트를 **로컬에서 재현**한다(좁혀 실행.
같은 기계에 셀프호스트 러너가 있으면 전체 실행 금지 — 러너를 굶긴다)
├─ 재현됨 → 2
└─ 재현 안 됨 + 플레이크 · 러너 문제로 진단(근거: 같은 잡의 간헐 실패 이력 · 러너 디스크/네트워크 오류 등)
→ gh run rerun <run-id> --failed (실패 잡만 — 전체 재실행 · 빈 커밋 push 금지) → 6 (이것도 라운드 하나)
↓
2. Determine if auto-fix is possible
↓
3. Apply fix
↓
4. Per-Push Verification Gate (format → analyze 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.5 를
bound:3 invalidates:S8.5,S10.5 로 확정한다.
| 항목 | 값 |
|---|---|
| 시그니처 | handleCiFailure(pr, ciResult) → { rounds, failingChecks, outcome } |
| outcome | fixed | ci_exhausted — 머지로 진행 가능한 값은 fixed 뿐 |
| 라운드 1회 | 진단(로컬 재현) → 수정 → Step 8.5 per-push gate → push → CI 재대기(Step 6 계약, cap 포함). 플레이크 · 러너로 진단되면 수정 대신 gh run rerun <run-id> --failed → CI 재대기 |
| 절대 금지 | 소진 후 재푸시 · // ignore: 삽입이나 진단 없는 재시도로 초록 만들기 · 전체 재실행 · 빈 커밋 push · 미확정을 passed 로 반환 |
Loop contract — run.step10_5-ci-fix-repush (필드 정의: ../../rules/orchestration-graph.md §2)
inv: push 되는 커밋은 예외 없이 Step 8.5 per-push gate 를 통과한 것 (analyze 0)
`// ignore:` 삽입 · `LEFTHOOK=0` · **진단 없는** 재실행으로 초록을 만드는 것 금지 — 플레이크 · infra/runner 로
진단된 실패만 `gh run rerun <run-id> --failed`(실패 잡만, 라운드로 센다). 전체 재실행 · 빈 커밋 push 금지
수정 커밋은 그 실패를 로컬에서 재현 · 수정 · 검증한 것이다 — CI 를 시험 루프로 쓰지 않는다
같은 PR 을 고친다 — 작업내역을 고쳐야 하면 그 수정 커밋에 함께(문서만의 커밋·push 금지, 새 링크·새 코멘트 금지)
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/QA → In Progress (Step 8.5 B)
# ⛔ 여기서 제어를 반환한다. 재푸시 금지 · 머지 금지 · ci_status = 'blocked' 로 보고한다.Error Handling#
Common Errors#
| Error | Cause | Resolution |
|---|---|---|
PR already exists | PR exists for same branch | Update existing PR |
merge conflict | Conflict with base branch | Conflict resolution needed |
checks failed | CI failure | Fix and retry within 3 rounds (handleCiFailure); move issue Review/QA → In Progress (Step 8.5 B) |
ci exhausted | 3 rounds consumed without shrinking the failing set | Label ci-blocked + BLOCKED('ci_exhausted') + return control — never push again, never merge |
ci wait cap | Wall-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 review | Review wait ended none-posted / unknown / not-configured | Log + record in the PR body ## Review Wait; never read as "review found nothing" |
review required | Review approval needed | Wait for review |
changes requested | Review bounced back for rework | Move issue Review/QA → In Progress; re-push returns it to Review/QA (Step 8.5 B) |
PR closed (unmerged) | Abandoned / superseded PR | Move 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 전 재검증
dart format . && dart fix --apply
melos run analyze # 0건 필수 (실패 시 push 금지)
git push --force-with-leasePost-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 Pattern | Meaning |
|---|---|
backend/kobic_server/lib/src/protocol/**/*.spy.yaml | Model file changes |
backend/kobic_server/lib/src/feature/**/*.dart | Endpoint/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:generateSkipping this step causes:
- Type mismatch between kobic_client and kobic_server
- Unable to call new APIs from frontend
- Build errors
Key Rules#
- Follow Title Rules: Conventional Commits + Gitmoji + Korean
- Issue Link Required: Always include
Closes #issue_number - Wait for Review: Wait up to 5 minutes for an AI-author comment (allowlist:
commands/pr/review-processor.md). 코멘트 총량으로 판정하지 않는다 — 리뷰가 아닌 코멘트(사람 메모· 파이프라인 경고)가 섞인다. 결과는reviewed/none-posted/unknown삼분이고,reviewed만 무음 통과다. AI 리뷰어가 없다고 확인된 저장소(not-configured)에서는 기다리지 않고 그 사실만 PR body 에 남긴다 - Apply Feedback: Analyze and auto-apply review comments
- Squash Merge: Always squash merge to clean up history
- Merge = Close:
Closes #Nauto-closes only on default-branch merges. For a hierarchical merge into a parent work-base branch (story/,epic/) it never fires, so rungh issue closeexplicitly — the primary action, not a fallback. After every merge confirm the issue isclosedon GitHub andClosedon ZenHub (updateIssue state:CLOSEDif not synced). Never leave it inDone. - Branch Cleanup: Auto-delete source branch after merge
- Backend Code Generation: Run
backend:pod:generateon backend changes after merge - 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