issue-branch-agent — 이슈용 작업 가지 만들기#
| 항목 | 내용 |
|---|---|
| 모델 | haiku |
한마디로#
이슈(할 일 카드) 하나를 받아, 그 작업만 따로 진행할 전용 작업 공간(브랜치) 을 자동으로 만들어 주는 도구입니다. 공동 문서에서 바로 고치지 않고, "내 사본"을 떠서 작업하다가 나중에 합치는 것과 같아요.
누가·언제 쓰나요#
- 개발자(또는 자동화 에이전트)가 ZenHub/GitHub 이슈 하나를 본격적으로 작업하기 시작할 때
-
보통
/cc-dev:run자동 흐름 안에서 첫 단계로 호출됩니다. 이슈 정보를 조회한 뒤, 코드를 짜기(implementation) 직전에 이 도구가 작업 가지를 먼저 만들어 둡니다.
무엇을 해주나요#
-
이슈 번호와 제목으로 일관된 규칙의 브랜치 이름을 자동 생성합니다.
- 예:
feature/25-community-list,bugfix/30-login-error,epic/10-author-management
- 예:
-
작업 종류에 따라 앞머리(prefix)를 붙여 줍니다. 기능은
feature/, 버그는bugfix/, 긴급 수정은hotfix/, 큰 묶음은epic/. -
한글 제목은 자동으로 로마자(영문)로 옮겨 깔끔한 이름으로 변환합니다. 한글만 있는 제목도 빈 이름이 되지 않습니다. (예:
[Story] Community 엔티티 정의→feature/28-story-community-entity,커뮤니티 목록→feature/25-keomyuniti-moklog) - 새 가지를 만들기 전에 하던 작업이 날아가지 않도록 안전하게 임시 보관(stash)하고, 최신 기준 브랜치에서 출발합니다.
어떻게 쓰나요#
# 기본: 기능 이슈
/dev:issue-branch 25 " Community list screen "
# 결과 → feature/25-community-list
# 버그 수정: --type bugfix 로 종류 지정
/dev:issue-branch 30 " Fix login error " --type bugfix
# 결과 → bugfix/30-login-error
# 한글 제목: 자동으로 로마자(영문)로 변환 — 한글만 있어도 빈 이름이 되지 않습니다
/dev:issue-branch 28 " [Story] Community 엔티티 정의 "
# 결과 → feature/28-story-community-entity
/dev:issue-branch 25 " 커뮤니티 목록 "
# 결과 → feature/25-keomyuniti-moklog (예전엔 feature/25- 로 깨졌음)
-
--type으로 작업 종류(feature/bugfix/hotfix)를 정할 수 있습니다. 생략하면feature입니다. --base로 어느 브랜치에서 가지를 칠지 정할 수 있습니다. 생략하면development에서 출발합니다.
안에서 무슨 일이 벌어지나요#
- 지금 상태 확인 — 저장하지 않은 변경이 있는지 살펴봅니다.
- 하던 일 임시 보관 — 변경이 있으면 안전하게 따로 챙겨 둡니다(stash). 작업이 사라지지 않습니다.
-
기준 브랜치 최신화 — 출발점이 될 브랜치로 옮겨 최신 상태로 맞춥니다. 상위 이슈가 있으면 그 부모 브랜치에서, 없으면
development에서 출발합니다. - 새 작업 가지 생성·이동 — 정해진 규칙의 이름으로 브랜치를 만들고 그 위로 옮깁니다. 같은 이름이 이미 있으면 새로 만들지 않고 그 브랜치로 이동만 합니다.
- 결과 확인 — 지금 올바른 브랜치에 있는지 점검하고, 어느 기준에서 출발했는지 정보를 돌려줍니다.
스택 모드에서의 브랜치 생성 (선택형)#
기본값은 위의 5단계 그대로입니다. GitHub 네이티브 Stacked PR 은 기존 브랜치 계층을 대체하지 않는 선택형 실행 모드이고, 켜지 않으면 이 에이전트의 동작은 하나도 달라지지 않습니다.
켤 자리는 딱 하나입니다 — 순차 의존이 있는 형제 이슈들(앞 이슈가 끝나야 뒤 이슈를 시작할 수 있는 사슬)을 한 줄로 늘어놓을 때. 서로 독립인 형제는 지금처럼 Orca 병렬 워크트리로 두는 게 맞습니다. 채택 판단·CI 비용 맞교환·머지 의미론은 stacked-prs 를 보세요.
층 쌓는 순서#
⚠️
/cc-dev:batch로 디스패치된 워크트리 안에서는 이 절의 명령을 실행하지 않습니다. 스택 조작(init/add/link/submit/sync/merge)의 소유권은 오케스트레이터 한 곳에 있습니다(batch.md 의 "3. 디스패치" 절). 워크트리에서 중복 실행하면 같은 브랜치가 여러 스택에 속해 exit 6(disambiguation 필요)·exit 8(스택 잠금)이 상시 발생합니다. 디스패치된 세션은 기본 5단계로만 동작하고, 스택 구성은 오케스트레이터에 맡기세요.
| 위치 | 명령 | 비고 |
|---|---|---|
| 첫 층 (사슬의 맨 아래) | gh stack init -b <trunk> [branch] |
-b
로 trunk 를 지정합니다. 생략하면
저장소 기본 브랜치
가 trunk 가 되므로, 이 흐름의 기준이
development
라면
-b development
를 반드시 명시하세요
|
| 이후 층 | gh stack add [-Am "메시지"] [branch] |
현재 HEAD 위에 새 브랜치를 만들어 스택
최상단
에 얹습니다.
-A
(전체 스테이지)·
-u
(추적 파일만)는
-m
과 함께 써야 합니다
|
⚠️
gh stack add는 스택 최상단 브랜치에서만 실행할 수 있습니다. 이미 쌓인 층 사이에 새 층을 끼워 넣으려면gh stack modify의 삽입(i아래 /I위)을 써야 하는데, 이건 대화형 TUI 라 무인(headless) 실행에서는 쓸 수 없습니다. 무인 파이프라인이라면 층을 항상 위로만 쌓도록 이슈 순서를 미리 정해 두세요 — 중간 삽입이 필요해진 시점에서 그 배치는 사람 손이 필요합니다.
브랜치 이름 규칙은 바뀌지 않는다#
스택 모드라고 이름 규칙이 달라지지 않습니다. 아래 "⚙️ 상세 옵션·실행 명세" 의 Branch Naming Rules
({type}/{issue_number}-{slug}, epic/ · feature/
· bugfix/ · hotfix/ prefix)를 그대로 씁니다.
스택은 브랜치들의 순서를 기록할 뿐, 이름을 정하지 않습니다.
⚠️
gh stack add에-m만 주고 브랜치 이름을 생략하면 날짜+slug 이름이 자동 생성됩니다. 그러면 이름에서 이슈 번호가 사라져 위 규칙이 깨집니다. 이 에이전트가 만든 브랜치 이름을 항상 인자로 명시하세요.
부수 효과·주의#
-
gh stack init은git rerere를 자동 활성화합니다. 같은 충돌이 되풀이될 때 이전 해결을 자동으로 재사용하므로, cascade rebase 가 잦은 스택에서 유리합니다(기존 저장소 설정이 바뀌는 부수 효과이니 인지하고 켜세요). -
로컬 스택 메타데이터는
.git/gh-stack(JSON)에 있고 커밋되지 않습니다. 이 파일이 워크트리 사이에서 공유되는지는 문서에 명시가 없습니다 — 다만 같은 브랜치가 여러 스택에 속하면 exit 6, 다른 프로세스가 스택을 잡고 있으면 exit 8 이 납니다. 그러니 스택 조작은 한 곳(한 세션·한 워크트리)으로 직렬화하세요. 다른 워크트리에서 다뤄야 하면gh stack checkout으로 원격 스택을 내려받아 로컬 추적을 세운 뒤에 하세요. -
gh stack init이 exit code9(Stacked PR 미활성)로 실패하면 이 저장소에 아직 기능이 켜지지 않은 것입니다(2026-07-30 public preview, 저장소 순차 롤아웃 중). 재시도하지 말고 기본 5단계로 폴백하세요. - 그 밖의 exit code 별 조치는 stacked-prs 의 "실패 모드 — exit code 별 조치" 절을 따릅니다.
⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)
Role and Responsibilities#
This agent creates and checks out Git branches based on ZenHub/GitHub issues.
- Branch Name Generation: Auto-generate branch name from issue number and title
- Safe Checkout: Check current working state before safely switching branches
- Base Branch Sync: Sync and branch from the latest development branch
Input Parameters#
| Parameter | Required | Type | Description |
|---|---|---|---|
issue_number | ✅ | number | GitHub issue number |
issue_title | ✅ | string | Issue title |
issue_type | ❌ | string | feature | bugfix | hotfix (default: feature) |
base_branch | ❌ | string | Base branch (default: development) |
Output#
interface BranchResult {
success: boolean;
branch_name: string;
base_branch: string;
error?: string;
}Branch Naming Rules#
Format#
{type}/{issue_number}-{slug}Prefix by Type#
| Issue Type | Branch Type | Example |
|---|---|---|
| Epic | epic | epic/10-author-management |
| Feature/Story | feature | feature/25-community-list |
| Bug | bugfix | bugfix/30-fix-login-error |
| Hotfix | hotfix | hotfix/35-critical-security-fix |
| Task | feature | feature/28-entity-definition |
| Sub-task | feature | feature/29-usecase-implementation |
Slug Generation Rules#
- Romanize non-ASCII (do NOT delete): Transliterate Korean/CJK/other scripts to ASCII before slugging — e.g.
커뮤니티 목록→keomyuniti-moklog. Never strip non-ASCII to nothing: a Korean-only title that is deleted yields an empty slug and a degenerate branch likefeature/25-. - Lowercase: Convert all characters to lowercase
- Remove Special Characters: Keep only letters/numbers (Unicode-aware), drop punctuation/symbols
- Spaces → Hyphens: Separate words with hyphens
- Max Length: 50 character limit
- Never empty: If the slug ends up empty, fall back to the issue-type word (e.g.
feature/25-story) — never emit a number-only branch (feature/25-).
// IMPORTANT: if `title` contains non-ASCII (e.g. Korean), romanize it to ASCII FIRST (Rule 1),
// then pass the romanized string here. createSlug NEVER returns an empty string.
function createSlug(title: string, issueTypeWord: string = ' work ' ): string {
const slug = title
.toLowerCase()
.replace(/[^\p{L}\p{N}\s-]/gu, ' ' ) // strip punctuation/symbols; keep Unicode letters/numbers
.replace(/[\s_]+/g, ' - ' ) // spaces/underscores → hyphens
.replace(/-+/g, ' - ' ) // collapse consecutive hyphens
.substring(0, 50) // length limit
.replace(/^-|-$/g, ' ' ); // trim leading/trailing hyphens
// Empty-slug guard (LANG-04): a non-romanized Korean-only title must NOT yield ' ' .
return slug || issueTypeWord;
}Execution Flow#
┌─────────────────────────────────────────────────────────┐
│ Step 1: Check Current State │
├─────────────────────────────────────────────────────────┤
│ $ git status │
│ - Check for uncommitted changes │
│ - Determine if stash is needed │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 2: Handle Changes │
├─────────────────────────────────────────────────────────┤
│ IF changes exist: │
│ $ git stash push -m " WIP: before #{issue_number} " │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 3: Sync Base Branch (hierarchical branch support) │
├─────────────────────────────────────────────────────────┤
│ $ git checkout {base_branch} │
│ $ git pull origin {base_branch} │
│ │
│ base_branch determination: │
│ ├── --base option specified → use that branch │
│ ├── Sub-task → parent Story branch │
│ ├── Story → parent Epic branch │
│ ├── Epic → development │
│ └── Independent issue → development (default) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 4: Create and Checkout Branch │
├─────────────────────────────────────────────────────────┤
│ $ git checkout -b {branch_name} │
│ - If branch exists → checkout only │
│ - Epic issue → epic/{number}-{slug} │
│ - Others → feature/{number}-{slug} │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Step 5: Verify Result │
├─────────────────────────────────────────────────────────┤
│ $ git branch --show-current │
│ - Verify branch creation success │
│ - Return base_branch info │
└─────────────────────────────────────────────────────────┘Command Details#
Step 1: Status Check#
# Check current state
git status --porcelain
# If output is not empty, changes existStep 2: Stash Handling#
# Temporarily save changes
git stash push -m " WIP: before issue #{issue_number} "
# Check stash list
git stash listStep 3: Base Branch Sync (hierarchical branch support)#
# ⭐ 원격 동기화 먼저 — 다른 머신/세션이 만든 계층 브랜치는 fetch 전엔 보이지 않는다
# (SoT: skills/branch-hierarchy/SKILL.md " 계층 브랜치 해석 계약 " R1)
git fetch origin --prune --quiet
# Move to base_branch (default: development, hierarchical: parent branch)
git checkout ${base_branch}
# Update to latest
git pull origin ${base_branch} --rebaseStep 4: Branch Creation (adopt-before-create)#
계층 브랜치(initiative|project|epic|story|task/)는 만들기 전에 반드시 찾는다. 이름의
{slug} 는 제목을 로마자로 옮긴 것이라 머신·세션마다 갈리므로, 조회는 이슈 번호 글롭으로 한다
(R2). 단독 이슈(feature|fix|chore/)도 같은 조회로 중복 착수를 한 번 더 거른다.
# 1) 이미 있는가 — 원격에 직접 묻는다 (로컬 remote-tracking ref 를 믿지 않는다)
CANDIDATES=$(git ls-remote --heads origin " refs/heads/{prefix}/{issue_number}-* " | sed ' s|.*refs/heads/|| ' )
COUNT=$(printf ' %s\n ' " $CANDIDATES " | grep -c .)
# 2) 있으면 그 이름을 그대로 채택한다 — 재계산한 이름으로 새로 만들지 않는다
# ⛔ ls-remote 결과는 **여러 줄일 수 있다.** 그대로 브랜치명으로 쓰면 checkout 이 실패해,
# " 복구 표가 실제로 일어난다고 말하는 상태 " (같은 이슈에 계층 브랜치 2개)에서 에이전트가 죽는다.
if [ " $COUNT " -gt 1 ]; then
# branch-hierarchy R4 tie-break 를 **여기서 수행한다**: 대장 → 열린 PR base/head → 커밋 수 → 사전순.
# 채택 후 ⚠️ 경고 로그 + 그 이슈에 후보 목록 코멘트 + 대장 기록. 탈락 후보는 삭제하지 않는다.
EXISTING={R4 로 고른 단 하나의 후보}
elif [ " $COUNT " -eq 1 ]; then
EXISTING= " $CANDIDATES "
else
EXISTING= " "
fi
if [ -n " $EXISTING " ]; then
git checkout " $EXISTING " 2 > /dev/null || git checkout -b " $EXISTING " " origin/$EXISTING "
else
# 3) 없을 때만 생성 — 대장( < !-- cc-dev:branch-registry -- > )에 이름이 있으면 그 이름을 쓴다
git checkout -b {branch_name}
# 4) ⭐ 커밋이 0개여도 즉시 push — push 안 된 계층 브랜치는 다른 머신에서 " 없는 것 " 이고
# `gh pr create --base` 도 찾지 못한다 (R6)
git push -u origin {branch_name} || {
# R5 — 레이스: 그 사이 다른 머신이 만들었다. 새 이름을 만들지 말고 원격을 채택한다.
# ⚠️ 자기 커밋이 0개면 rebase 하지 않는다 — 그 상태의 `origin/{branch}..HEAD` 는 **base
# 브랜치의 커밋들**이라, 이미 upstream 에 있는 커밋을 새 해시로 복제해 공유 브랜치에 push 한다.
git fetch origin {branch_name} || exit 1
if [ " $(git rev-list --count origin/${base_branch}..HEAD) " -eq 0 ]; then
git checkout -B {branch_name} origin/{branch_name} # 자기 커밋 없음 → 원격 그대로 채택
else
git rebase origin/{branch_name} || { echo " ⛔ rebase 충돌 — 수동 해결 후 재실행 " ; exit 1; }
fi
git push -u origin {branch_name}
}
# 5) 대장에 기록 — 다음 머신·세션이 이 이름을 그대로 쓴다 (R3, 실패는 비차단 경고)
fiStep 5: Result Verification#
# Check current branch
git branch --show-current
# Verify it matches the expected branchError Handling#
Common Errors#
| Error | Cause | Resolution |
|---|---|---|
uncommitted changes | Uncommitted changes | Stash handling |
branch already exists | Same branch exists | Checkout existing branch |
merge conflict | Conflict on pull | Notify that conflict resolution is needed |
not a git repository | Not a git repo | Return error |
push rejected (non-fast-forward) | 다른 머신·세션이 그 사이 같은 브랜치를 만들었다 | ⛔ 새 이름을 만들지 않는다 — git fetch origin {branch} → 자기 커밋 0개면 checkout -B, 있으면 rebase → 재push (R5) |
| 계층 브랜치 후보가 2개 이상 | 다른 머신이 다른 slug 로 만들었다 | R4 tie-break 로 하나 채택 + 대장 기록 + 경고/코멘트. 탈락본 삭제 금지(다른 머신의 미푸시 작업이 있을 수 있다) |
| 부모 브랜치가 "없다"고 나옴 | git fetch 전이라 원격 브랜치가 로컬에 안 보임 | git fetch origin --prune 후 git ls-remote --heads origin "refs/heads/{prefix}/{n}-*" 로 재확인 (R1·R2) |
Recovery Strategy#
# Recover stash (after work is done)
git stash pop
# Recover specific item from stash list
git stash apply stash@{0}Usage Examples#
Basic Usage#
# Feature issue
/dev:issue-branch 25 " Community list screen "
# Result
branch_name: feature/25-community-listBug Fix#
# Bug issue
/dev:issue-branch 30 " Fix login error " --type bugfix
# Result
branch_name: bugfix/30-login-errorKorean Title Handling#
# Mixed Korean/English title — Korean romanized, special chars removed
/dev:issue-branch 28 " [Story] Community 엔티티 정의 "
# Result
branch_name: feature/28-story-community-entity
# Korean-ONLY title — romanized to ASCII, NEVER empty (was the LANG-04 bug: " feature/25- " )
/dev:issue-branch 25 " 커뮤니티 목록 "
# Result
branch_name: feature/25-keomyuniti-moklogIntegration Scenario#
Called from /cc-dev:run (existing-issue path)#
1. issue-cycle queries issue info
↓
2. issue-branch-agent called
- issue_number: 25
- issue_title: " Community list screen "
- issue_type: feature
↓
3. Branch creation complete
- branch_name: feature/25-community-list
↓
4. Passed to implementation-agentKey Rules#
- Safety First: Always check current state before starting
- Use Stash: Preserve changes via stash when they exist
- Hierarchical Base: Branch from base_branch parameter (parent branch or development)
- Consistent Naming:
epic/prefix for Epics,feature/for others - Idempotency: Safe to call again with the same issue
- ASCII-safe, never empty: Romanize non-ASCII (Korean/CJK) to ASCII rather than deleting it; if the slug would be empty, fall back to the issue-type word (
feature/25-story). Never emit a number-only branch (feature/25-). - Parent Branch Priority: When a parent issue exists, always branch from the parent branch