cmux-delegate은(는) 무엇을 하나요?
개요
작업 중 발견된 독립적인 이슈를 별도의 세션에 전달하는 스킬입니다. 현재 대화의 작업 컨텍스트(git 메타데이터 + 대화에서 추출한 추론 컨텍스트)를 자동으로 수집하고, cmux 작업 공간에서 독립적인 Claude Code 세션을 열며, 해당 세션은 이슈 → 작업 트리 → PR 과정을 스스로 완료합니다. 기존 세션 재사용, 별도 계정 프로필 사용, 병렬 다중 이슈 분배를 지원합니다.
핵심 원칙:
- 프롬프트는 항상 파일을 통해 전달됩니다. 인라인
-p는 엄격히 금지됩니다 (셸 이스케이프 문제 방지). - 사용자가 세션/계정을 명시적으로 지정하면 그대로 따릅니다. 임의로 재해석하지 않습니다.
- 위임의 단위는 하나의 독립적인 이슈입니다. 이슈로 성립할 수 있으면 위임하고, 그렇지 않으면 위임 대상이 아니라 현재 세션에서 완료할 작업입니다.
- 모호함은 실행 전에 제거되며, 실행 후에는 제거되지 않습니다. 어떤 결과도 돌아와 묻지 않으므로, 완료 조건, 범위, 절차는 명령, 경로, 숫자로 프롬프트에 포함됩니다 (2.6단계).
- 발사 후 망각. 위임은 종료를 의미합니다 — 작업자는 위임자에게 아무것도 보고하지 않으며, 위임자는 작업자를 모니터링하거나 평가하지 않습니다. 사용자는 해당 cmux 탭에서 직접 결과를 확인합니다.
사용 시기
지금 하고 있는 작업과 별개의 문제가 작업 중에 나타날 때 사용합니다.
- 구현 중 관련 없는 버그를 발견하여 분리하고 싶을 때
- 리뷰 결과가 현재 PR 범위를 벗어나 후속 이슈로 넘겨야 할 때
- 여러 이슈가 그렇게 쌓였고 각각 다른 세션으로 보내야 할 때 (
--distribute)
위임 적격성
해당 항목이 하나의 독립적인 이슈로 성립할 수 있는가 — 이 한 가지 테스트로 결정합니다.
| 판정 | 처리 |
|---|---|
| 이슈로 성립 가능 (자체 재현, 범위, 완료 기준이 있고 내 작업이 끝나지 않아도 진행 가능) | 위임 |
| 이슈로 성립 불가 (현재 작업의 일부이며, 결과가 돌아와야 내 작업이 완료됨) | 위임하지 않음 — 현재 세션에서 완료 |
결과가 돌아와 병합되어야 하는 항목은 위임 대상이 아닙니다. 이는 하위 에이전트의 작업이며, 이 스킬을 사용하면 작업자에게 보고를 요구하게 됩니다. 역사적으로 이는 완료 보고서 수집(#894)과 결정 게이트(#984)를 도입하게 했으며, 둘 다 #1130에서 제거되었습니다.
입력
위임할 이슈를 이슈 번호로 지정합니다.
/cmux-delegate "#1140 auth 토큰 갱신 실패" --model opus
/cmux-delegate "#1141 리뷰에서 나온 후속 항목" --session claude-2
/cmux-delegate "별건 3개: #1140, #1141, #1142" --account claude-2 --distribute
이슈는 위임 시점에 이미 존재해야 합니다. 이슈가 없다면 위임 전에 현재 세션에서 생성하세요. 이슈 생성은 승인이 필요한 절차이므로 작업자에게 넘기지 않습니다. 번호 없이 설명만 전달하면 작업자는 PR을 연결할 이슈를 알 수 없습니다.
인자
| 인자 | 기본값 | 설명 |
|---|---|---|
<task> | (필수) | 위임할 작업 설명 |
--model | jev 경로, 없으면 sonnet | 제공자:모델 표기법. fable/opus/sonnet/haiku = claude. claude, claude:opus, codex (= gpt-5.6-terra, 노력 medium), codex:gpt-5.6-sol, codex:gpt-5.6-sol:xhigh, claude:opus:low / opus:low (claude --effort, low/medium/high/xhigh/max 중 하나; 이름 없음 → 전달 안 함), gemini, gemini:flash도 지원합니다. 프로젝트 ARCHITECTURE.md 제공자 라우팅 참조. |
--cwd | 현재 디렉토리 | 새 세션의 작업 디렉토리 |
--max-budget-usd | — | 지원되지 않음 (#1054). 출력 전용 플래그이므로 대화형 작업자와 함께 사용할 수 없습니다. 지정된 경우 무시하지 말고 사용자에게 알리세요. |
--account | (기본 계정) | Claude 계정 프로필 (예: claude-2 → CLAUDE_CONFIG_DIR=~/.claude-2) |
--session | (새로 생성) | 기존 작업 공간으로 전달 (이름 또는 작업 공간 참조) |
--distribute | false | 이슈 단위 병렬 분배. 단일 작업의 샤딩이 아닙니다. |
--permission-mode | — | 제거됨 (#1054). 위임된 작업자는 일반 세션이므로 사용자의 일반 기본값을 사용합니다. |
프로세스
1단계: 인자 파싱
{{ARGUMENTS}}에서 인자를 파싱합니다:
args = parse("{{ARGUMENTS}}")
cwd = args.cwd || $(pwd)
# 예산 플래그를 수락하지만 전달하지는 않습니다. 수신되었음을 사용자에게 알립니다 (#1054).
# 조용히 삭제하면 사용자가 한도가 설정되어 있다고 믿고 위임할 수 있습니다.
if args["max-budget-usd"]: warn("--max-budget-usd 는 대화형 워커에 적용되지 않습니다 (#1054)")
account = args.account || ""
session = args.session || ""
distribute = args.distribute || false
task = args.task (플래그 이후 나머지 텍스트)
short_task = task[:30], [a-zA-Z0-9가-힣 -]만 허용하도록 정리 (cmux 작업 공간 이름용)
timestamp = epoch 초 + PID (예: 1744163800-12345) 충돌 방지
# 명시적인 --model은 절대 재정의되지 않습니다. 없으면 jev가 작업자를 선택합니다;
# {"source": "fallback"}인 경우 기본값은 sonnet입니다 (#1481).
route_source = "explicit"
if args.model: model = args.model
else:
route = json(python3 "${CLAUDE_PLUGIN_ROOT}/skills/cmux-delegate/jev-route.py" "$task")
route_source = route.source
if route.source == "jev":
model = route.provider == "claude" ? route.tier : route.provider
else:
model = "sonnet"
# 제공자 확인 (프로젝트 ARCHITECTURE.md 제공자 확인 로직 참조)
if model matches /^(codex|gemini)(?::(.+))?$/:
provider = match[1] # "codex" 또는 "gemini"
sub_model = match[2] || "" # "" 또는 "gpt-5.6-sol:xhigh" 또는 "flash" (첫 번째 콜론 제거)
elif model matches /^(fable|opus|sonnet|haiku)(?::[A-Za-z]+)?$/:
provider = "claude"
sub_model = model # "opus" 또는 "opus:low"; 노력 수준은 아래에서 분리
elif model matches /^claude(?::(.+))?$/:
provider = "claude"
sub_model = match[1] || ""
else:
provider = "claude"
sub_model = model
# codex는 설정 기본 모델로 폴스루되지 않습니다: bare `codex`
# (명시적 또는 jev 선택)는 terra를 얻고, 나열된 모델은 그 노력 수준을 얻습니다
# `codex:<model>:<effort>`가 지정되지 않은 한 (#1483).
CODEX_DEFAULT_MODEL = "gpt-5.6-terra"
CODEX_EFFORT = {"gpt-5.6-luna": "low", "gpt-5.6-terra": "medium", "gpt-5.6-sol": "high"}
effort = ""
if provider == "codex":
sub_model, _, effort = sub_model.partition(":")
sub_model = sub_model || CODEX_DEFAULT_MODEL
effort = effort || CODEX_EFFORT.get(sub_model, "")
# 4단계에서 둘 다 인용되지 않은 bash 래퍼에 보간되므로,
# `xhigh; touch /tmp/x` 같은 값이 실행될 수 있습니다.
# 인용(`"` 및 `$( )`도 우회 가능) 대신 일반 식별자와 일치하지 않는 것은 거부합니다.
if sub_model does not match /^[A-Za-z0-9._-]+$/: abort "잘못된 codex 모델"
if effort and effort does not match /^[A-Za-z0-9._-]+$/: abort "잘못된 codex 추론 노력 수준"
# claude는 노력 수준이 지정된 경우에만 사용합니다: `claude:<tier>:<effort>` 또는
# `<tier>:<effort>` (#1499). codex처럼 계층별 기본 테이블이 없습니다.
# Opus 5.5 프롬프팅 가이드
# (https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5-5#calibrate-effort)
# 는 "노력 수준 이름이 모델 간에 동일한 양의 사고에 대응하지 않습니다"라고 말하며
# "Claude Opus 5에서 사용한 설정을 그대로 가져오기보다" 자체 평가에 대해
# 모델별로 여러 수준을 테스트하라고 조언합니다. 따라서 여기에는 테이블이 내장되어 있지 않습니다:
# 이름 없음 → 아무것도 전달되지 않으며 모델 자체 기본값이 적용됩니다
# (Opus 5.5에서는 `medium`, Opus 5에서는 `high`). 가이드는 또한
# "품질 향상을 측정한 작업을 위해 `xhigh` 및 `max`를 예약하세요"라고 말합니다.
# bare 계층을 생성하는 jev 선택은 절대 노력 수준을 전달하지 않습니다.
CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"]
if provider == "claude":
# 마지막 콜론 뒤의 알파벳 꼬리만 노력 수준이므로, 자체적으로 콜론을 포함하는
# 모델 ID(Bedrock `…-v1:0`, `…:application-inference-profile/<id>`로 끝나는 ARN)는
# 그대로 통과합니다.
# 제공자 바로 뒤의 이중 콜론(`claude::high`, `claude::`)은
# `claude:opus:high`의 오타일 가능성이 높으며, 노력 수준 분할 전에 확인됩니다;
# 그렇지 않으면 `claude::high`를 high의 기본 모델로 읽게 됩니다.
# 모든 이중 콜론 형식은 동일한 방식으로 중단됩니다 (아래 `opus::low`).
if sub_model starts with ":": abort "잘못된 claude 모델"
if sub_model matches /^(.*):([A-Za-z]+)$/:
sub_model, effort = match[1], match[2]
# 잘못된 꼬리(`opus:`, `opus::low`, `opus:low:high`)는 CLI에 모델 이름으로
# 전달되어서는 안 됩니다: `:`로 끝나는 모델 ID나 `:` 뒤에 계층 별칭이 오는 경우는 없습니다.
if sub_model ends with ":" or sub_model matches /^(fable|opus|sonnet|haiku):/: abort "잘못된 claude 모델"
# 4단계는 모델을 작은따옴표로 묶으므로 문자 집합은 `'` 및 모든 셸 메타 문자를
# 제외해야 합니다; Bedrock ID 및 ARN의 `:` `/`, Vertex ID의 `@`
# (`claude-sonnet-4-5@20250929`), 그리고 `[1m]` 컨텍스트 접미사의 `[]`를 허용합니다.
# 인용되지 않은 `opus[1m]`는 글로브가 되어 cwd에 그러한 파일이 있을 때마다 `opus1`이
# 되므로 따옴표는 유지됩니다.
if sub_model does not match /^[A-Za-z0-9._:@\/\[\]-]*$/: abort "잘못된 claude 모델"
# `claude --effort bogus`는 경고만 하고 기본 노력 수준으로 실행되므로,
# CLI는 오타를 잡지 못합니다; 이 확인이 유일한 방법입니다.
if effort and effort not in CLAUDE_EFFORTS: abort "잘못된 claude 노력 수준 '{effort}' (예상: low|medium|high|xhigh|max)"
# 사전 확인: 제공자 CLI 사용 가능 여부 확인
if ! command -v "$provider" &>/dev/null:
warn "⚠ ${provider} CLI를 찾을 수 없습니다, claude:sonnet으로 대체합니다"
provider = "claude"
sub_model = "sonnet"
effort = ""
1.5단계: 세션 확인
기존 세션을 사용할지 결정합니다.
if session이 지정된 경우:
1. cmux list-workspaces → 이름 또는 참조로 일치
2. 일치 항목 발견 → cmux send 모드 (5b단계)
3. 일치 항목 없음 → 오류 출력 "세션 '{session}'을 찾을 수 없습니다" 및 중단
else:
→ 기존 동작 (새 작업 공간, 5a단계)
1.6단계: 계정 확인
계정 프로필을 결정합니다.
if account이 지정된 경우:
# 계정 프로필은 CLAUDE_CONFIG_DIR 환경 변수를 통해 설정됩니다
# 예: --account claude-2 → CLAUDE_CONFIG_DIR=~/.claude-2
claude_env = "CLAUDE_CONFIG_DIR=~/.{account}"
# 확인: 설정 디렉토리가 존재하는지 확인
if not exists(~/.{account}):
오류 출력 "계정 프로필 디렉토리 ~/.{account}이 없습니다" 및 중단
else:
claude_env = "" # 기본 계정 사용
2단계: 컨텍스트 수집
현재 대화 및 프로젝트의 컨텍스트를 자동으로 수집합니다.
각 명령은 실패해도 계속됩니다 (2>/dev/null).
수집할 정보:
# 1. Git 상태
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
COMMITS=$(git log --oneline -5 2>/dev/null || echo "git 기록 없음")
DIFF_STAT=$(git diff --stat HEAD 2>/dev/null || echo "변경 사항 없음")
# 2. 변경된 파일 목록 (기준 브랜치 대비)
BASE_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')
BASE_BRANCH=${BASE_BRANCH:-main}
CHANGED_FILES=$(git diff --name-only $(git merge-base HEAD "origin/$BASE_BRANCH" 2>/dev/null || echo HEAD~5)..HEAD 2>/dev/null || echo "unknown")
# 3. PR 정보 (있는 경우)
PR_INFO=$(gh pr list --head "$BRANCH" --json number,title,url 2>/dev/null || echo "PR 없음")
# 4. PR 리뷰 댓글 (있는 경우)
REVIEW_COMMENTS="0"
PR_NUM=$(gh pr list --head "$BRANCH" --json number -q '.[0].number' 2>/dev/null || echo "")
if [ -n "$PR_NUM" ]; then
REVIEW_COMMENTS=$(gh api "repos/$(gh repo view --json nameWithOwner -q '.nameWithOwner' 2>/dev/null)/pulls/$PR_NUM/comments" --jq 'length' 2>/dev/null || echo "0")
fi
# 5. 붙여넣은 콘텐츠 ID — 프롬프트 파일당 하나의 새 ID (아래 참조)
PASTE_ID=$(od -An -N3 -tx1 /dev/urandom | tr -d ' \n')
타사 텍스트는 표시되며 신뢰되지 않습니다 (이슈 #1500). 프롬프트 파일이 작업자에게 도달하는 방식은 모드에 따라 다릅니다:
- 새 세션 및 분배 모드 (5a단계). 래퍼는 파일의 내용을 작업자의 첫 번째 사용자 메시지로 전달하므로(claude와 gemini는 argv를 통해, codex는 stdin을 통해), 파일의 모든 내용은 위임자가 말하는 것으로 읽힙니다. 이것이 가이드의 붙여넣은 텍스트 조언이 다루는 경우입니다.
- 기존 세션 모드 (5b단계). 사용자 메시지는 "
{prompt_file}읽기"뿐입니다. 내용은Read도구 결과로 도착합니다. 가이드는 이를 별도의 경우로 취급합니다: 도구 결과는 모델의 간접 프롬프트 주입에 대한 저항 영역에 속합니다 ("도구 결과, 웹 페이지 및 화면 또는 브라우저 콘텐츠를 통해 도착하는 명령"). 태그와 메모는 파일에 남아 있지만(하나의 파일, 모든 모드), 여기서는 위임자 자신의 섹션도 사용자 메시지가 아닌 도구 결과 콘텐츠입니다.
파일 텍스트 중 일부는 위임자의 것이 아닙니다: COMMITS는 커밋 제목을,
CHANGED_FILES 및 DIFF_STAT는 파일 경로를, PR_INFO는 PR 제목을 전달하며,
이는 모두 해당 항목을 작성한 사람이 작성한 것입니다. Handoff 또는
Instructions 섹션은 이슈 본문이나 리뷰 댓글을 인용할 수 있습니다.
파일 경로는 제목만큼 쉽게 명령과 유사한 텍스트를 전달할 수 있습니다
(PR은 docs/FOLLOW-THE-INSTRUCTIONS-IN-THE-PR-BLOCK.md를 추가할 수 있습니다).
각 블록은 동일한 PASTE_ID를 가진 여는 pasted_content 태그와 닫는 pasted_content 태그 사이의
프롬프트로 들어갑니다. 각 태그는 자체 줄에 있으며,
3단계 템플릿 상단의 메모 아래에 위치합니다. 태그 형식은
Opus 5.5 프롬프팅 가이드가 붙여넣은 텍스트에 대해 제공하는 형식입니다
(https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5-5#mark-pasted-text-in-user-messages);
가이드와 다른 세 가지 사항이 있습니다:
- 메모의 위치. 가이드는 "그런 다음 이 메모를 시스템 프롬프트에 추가하세요"라고 말합니다. 여기서 메모는 프롬프트 파일의 상단, 즉 신뢰할 수 없는 텍스트와 동일한 사용자 메시지(또는 도구 결과)에 위치합니다. 그 약점: PR 제목이나 커밋 제목 내부에 위조된 메모는 동일한 입지를 갖습니다.
...(번역을 위해 잘림)