LogoSkills

issue-branch-agent

이 에이전트는 ZenHub/GitHub 이슈를 기반으로 Git 브랜치를 생성하고 체크아웃합니다.

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 에서 출발합니다.

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

  1. 지금 상태 확인 — 저장하지 않은 변경이 있는지 살펴봅니다.
  2. 하던 일 임시 보관 — 변경이 있으면 안전하게 따로 챙겨 둡니다(stash). 작업이 사라지지 않습니다.
  3. 기준 브랜치 최신화 — 출발점이 될 브랜치로 옮겨 최신 상태로 맞춥니다. 상위 이슈가 있으면 그 부모 브랜치에서, 없으면 development 에서 출발합니다.
  4. 새 작업 가지 생성·이동 — 정해진 규칙의 이름으로 브랜치를 만들고 그 위로 옮깁니다. 같은 이름이 이미 있으면 새로 만들지 않고 그 브랜치로 이동만 합니다.
  5. 결과 확인 — 지금 올바른 브랜치에 있는지 점검하고, 어느 기준에서 출발했는지 정보를 돌려줍니다.

스택 모드에서의 브랜치 생성 (선택형)#

기본값은 위의 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 initgit rerere 를 자동 활성화합니다. 같은 충돌이 되풀이될 때 이전 해결을 자동으로 재사용하므로, cascade rebase 가 잦은 스택에서 유리합니다(기존 저장소 설정이 바뀌는 부수 효과이니 인지하고 켜세요).
  • 로컬 스택 메타데이터는 .git/gh-stack (JSON)에 있고 커밋되지 않습니다. 이 파일이 워크트리 사이에서 공유되는지는 문서에 명시가 없습니다 — 다만 같은 브랜치가 여러 스택에 속하면 exit 6, 다른 프로세스가 스택을 잡고 있으면 exit 8 이 납니다. 그러니 스택 조작은 한 곳(한 세션·한 워크트리)으로 직렬화하세요. 다른 워크트리에서 다뤄야 하면 gh stack checkout 으로 원격 스택을 내려받아 로컬 추적을 세운 뒤에 하세요.
  • gh stack init 이 exit code 9(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.

  1. Branch Name Generation: Auto-generate branch name from issue number and title
  2. Safe Checkout: Check current working state before safely switching branches
  3. Base Branch Sync: Sync and branch from the latest development branch

Input Parameters#

ParameterRequiredTypeDescription
issue_numbernumberGitHub issue number
issue_titlestringIssue title
issue_typestringfeature | bugfix | hotfix (default: feature)
base_branchstringBase 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 TypeBranch TypeExample
Epicepicepic/10-author-management
Feature/Storyfeaturefeature/25-community-list
Bugbugfixbugfix/30-fix-login-error
Hotfixhotfixhotfix/35-critical-security-fix
Taskfeaturefeature/28-entity-definition
Sub-taskfeaturefeature/29-usecase-implementation

Slug Generation Rules#

  1. 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 like feature/25-.
  2. Lowercase: Convert all characters to lowercase
  3. Remove Special Characters: Keep only letters/numbers (Unicode-aware), drop punctuation/symbols
  4. Spaces → Hyphens: Separate words with hyphens
  5. Max Length: 50 character limit
  6. 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 exist

Step 2: Stash Handling#

# Temporarily save changes
git stash push -m  " WIP: before issue #{issue_number} " 

 # Check stash list
git stash list

Step 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} --rebase

Step 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, 실패는 비차단 경고)
fi

Step 5: Result Verification#

# Check current branch
git branch --show-current

# Verify it matches the expected branch

Error Handling#

Common Errors#

ErrorCauseResolution
uncommitted changesUncommitted changesStash handling
branch already existsSame branch existsCheckout existing branch
merge conflictConflict on pullNotify that conflict resolution is needed
not a git repositoryNot a git repoReturn 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 --prunegit 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-list

Bug Fix#

# Bug issue
/dev:issue-branch 30  " Fix login error "   --type bugfix

# Result
branch_name: bugfix/30-login-error

Korean 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-moklog

Integration 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-agent

Key Rules#

  1. Safety First: Always check current state before starting
  2. Use Stash: Preserve changes via stash when they exist
  3. Hierarchical Base: Branch from base_branch parameter (parent branch or development)
  4. Consistent Naming: epic/ prefix for Epics, feature/ for others
  5. Idempotency: Safe to call again with the same issue
  6. 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-).
  7. Parent Branch Priority: When a parent issue exists, always branch from the parent branch