LogoSkills

/cc-dev:bugfix — 버그 하나를 끝까지 책임지는 "수리 자동화"

이 워크플로우는 버그 이슈 수정 사이클 전체를 자동화합니다:

/cc-dev:bugfix — 버그 하나를 끝까지 책임지는 "수리 자동화"#

항목내용
실행 명령/cc-dev:bugfix

버그 신고부터 코드 수정, 테스트, PR 병합까지 한 번에 처리하는 워크플로

한마디로#

버그 하나를 발견했을 때, 접수 → 원인 분석 → 수정 → 검증(테스트) → 병합까지 전 과정을 한 명령으로 처리해 주는 "수리 기사"입니다. 가전제품이 고장 났을 때, 접수만 하면 기사님이 와서 고치고 잘 작동하는지 확인까지 한 뒤 마무리해 주는 것과 같아요.

누가·언제 쓰나요#

  • 이미 등록된 버그 이슈 번호가 있고, 그 버그를 고쳐서 마무리하고 싶을 때
  • 아직 이슈가 없고, 스크린샷이나 설명만 있는 새 버그를 처음부터 접수해서 고치고 싶을 때 (--new)
  • 급한 핫픽스라 빠르게 처리해야 할 때 (--skip-tests 같은 옵션 제공)

👉 단, 버그(Bug) 유형 이슈만 처리합니다. 새 기능 개발은 다른 명령(/cc-dev:run)을 씁니다.

무엇을 해주나요#

  • 수정용 브랜치fix/{번호}-{설명} 형식으로 자동 생성됩니다
  • 실기기가 연결돼 있으면 거기에 앱을 직접 띄워 버그를 재현하고, 임시 디버그 로그를 심어 원인을 눈으로 확인한 뒤 고칩니다 (추측 수정 금지)
  • 버그 원인을 찾아 코드를 고치고, 수정 내역을 커밋으로 차곡차곡 기록합니다
  • 고친 뒤 같은 기기에서 다시 재현해 보고, 진단용으로 심었던 임시 로그는 한 줄도 남기지 않고 거둬들입니다
  • 같은 버그가 다시 안 생기도록 테스트(프론트엔드 + 백엔드) 를 작성하고 모두 통과시킵니다
  • PR(코드 리뷰 요청) 을 만들어 이슈와 자동 연결하고, 리뷰 후 병합까지 진행합니다
  • 진행 상황에 맞춰 ZenHub 이슈 상태가 "진행 중 → 리뷰 → 완료(Closed)"로 자동 이동합니다

어떻게 쓰나요#

# 기존 버그 이슈로 시작
/cc-dev:bugfix 123

# 새 버그 리포트부터 시작 (스크린샷 + 설명)
/cc-dev:bugfix --new /tmp/screenshot.png  " 앱 크래시 " 

 # PR만 만들고 병합은 직접 (자동 병합 끄기)
/cc-dev:bugfix 123 --no-merge

# 테스트 건너뛰기 (급한 핫픽스용)
/cc-dev:bugfix 123 --skip-tests

# 기준 브랜치 변경 (기본값: development)
/cc-dev:bugfix 123 --base main

# 특정 기기에서 재현·진단 (기본은 연결된 실기기 자동 선택)
/cc-dev:bugfix 123 --device 00008130-001A2C3D0E1F001E

# 기기 재현 없이 정적 분석만으로 진행 (원인은  ' 추정 ' 으로 기록됨)
/cc-dev:bugfix 123 --skip-device
  • issue_number(이슈 번호) 또는 --new 중 하나는 반드시 필요합니다
  • --no-merge: PR까지만 만들고 병합은 사람이 직접 합니다
  • --skip-tests: 테스트 작성/실행을 생략합니다 (급할 때만)
  • --base: 어느 브랜치를 기준으로 수정할지 지정합니다 (기본 development)
  • --device: 재현에 쓸 기기를 직접 지정합니다 (안 적으면 실기기 → 시뮬레이터 순으로 자동 선택)
  • --skip-device: 기기 재현·진단을 건너뜁니다. 건너뛴 사실과 "원인 추정"은 PR에 그대로 남습니다

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

대략 다음 순서로 진행됩니다.

  1. 영향 범위 점검 — 고치기 전에 "이 버그를 건드리면 어디까지 영향이 가는지"를 먼저 살핍니다. 위험도가 낮으면(LOW) 바로 진행, 중간(MEDIUM)이면 사용자에게 확인, 높으면(HIGH) 더 신중한 다른 워크플로(/plan → /build)를 권합니다. 훑어야 할 대상이 상한(40건)을 넘으면 넘긴 항목을 목록으로 남기고, 다 못 본 상태를 "LOW"로 내리지 않습니다.
  2. 버그 접수 (--new일 때) — 스크린샷·설명을 분석해 ZenHub 이슈를 새로 만듭니다.
  3. 이슈 정보 조회 — 이슈의 제목·내용·재현 방법을 읽어 원인을 분석합니다.
  4. 브랜치 생성 — 수정용 브랜치를 만들고 이슈를 "진행 중"으로 옮깁니다.
  5. 실기기 재현·진단 — 컴퓨터에 기기가 연결돼 있는지 먼저 확인하고, 연결돼 있으면 그 기기에 앱을 직접 띄워 이슈에 적힌 재현 절차를 그대로 밟아 봅니다. 그런 다음 의심 지점에 표시가 붙은 임시 로그를 심고 화면을 다시 불러오며, 문제가 실제로 어디서 터지는지를 파일·줄 번호까지 짚어냅니다. 실기기가 없으면 시뮬레이터를 쓰고, 그것도 없으면 사용자에게 확인한 뒤 "원인은 추정"이라고 PR에 남깁니다 — 조용히 건너뛰지 않습니다.
  6. 버그 수정 — 5번에서 실제로 확인한 근거를 바탕으로 코드를 고치고, 변경 사항을 커밋합니다. 근거 없이 추측만으로 고치지 않습니다.
  7. 기기에서 재확인 + 임시 로그 회수 — 앱을 다시 띄워 같은 절차를 밟아 버그가 더는 재현되지 않는지 확인하고, 진단하려고 심어 둔 임시 로그를 한 줄도 남기지 않고 거둬들입니다. 아직 재현되면 진단 단계로 한 번 되돌아가고, 그래도 재현되면 거기서 멈춥니다(안 고쳐진 것을 고쳤다고 하지 않습니다).
  8. 기존 테스트 확인 — 손대기 전 기존 테스트가 잘 도는지 기준선(통과 개수) 을 기록합니다. 이때 기존 테스트는 고칠 수는 있어도 약화시킬 수는 없습니다 — 건너뛰기(skip) 추가·테스트 삭제 금지, 통과 개수는 줄어들 수 없습니다.
  9. 테스트 작성 — 같은 버그가 재발하지 않도록 프론트·백엔드 테스트를 추가합니다.
  10. 테스트 실행·검증 — 모든 테스트를 돌리고, 실패하면 최대 3번까지 자동 수정을 시도합니다. 매 시도마다 실패 개수가 실제로 줄어야 하며, 줄지 않거나 3번을 다 쓰면 거기서 멈춥니다. 8번에서 잡은 기준선보다 통과 개수가 적으면 통과로 보지 않습니다.
  11. PR 생성 — 코드 리뷰 요청을 만들고 이슈와 자동으로 연결합니다(Closes #번호). 이 직전에 /cc-dev:run의 푸시 전 검사(분석/린트 0건)와 중복 착수 재확인을 그 문서의 절차 그대로 빌려서 한 번 돌리고, 임시 진단 로그가 한 줄도 남지 않았는지 다시 확인합니다.
  12. 병합·마무리 — 리뷰가 끝나면 먼저 CI(자동 검사) 결과가 "통과"인지 확인합니다. 실패든 "검사가 아예 없음"이든 통과가 확인되지 않으면 병합하지 않습니다. 통과 후 병합하면 이슈가 자동으로 "완료(Closed)" 처리되고, 기준 브랜치를 최신 상태로 정리합니다. 단 이 버그 이슈에 하위 작업이 열려 있으면 병합하지 않고 멈춥니다(하위 작업을 먼저 끝내야 합니다).

👉 이 경로는 기능 개발용 /cc-dev:run보다 검사가 적은 지름길입니다. 어떤 검사를 일부러 건너뛰고 그 대신 무엇을 PR 본문에 남기는지는 아래 상세 명세의 "Waived Gates" 표에 다 적혀 있습니다. CI 통과와 하위 작업 확인 두 가지는 어떤 옵션으로도 건너뛸 수 없습니다 (--skip-tests 포함).


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

Overview#

This workflow automates the entire bug issue fix cycle:

  1. Bug report creation (optional)
  2. Branch creation
  3. Runtime repro & diagnosis on a real device (Step 3.5) — device-first, instrumentation-driven root cause
  4. Bug fix work (driven by that evidence, not by guesswork)
  5. Runtime fix verification + instrumentation removal (Step 4.2)
  6. Existing-test baseline capture (Step 4.5) + unit test writing (frontend + backend)
  7. Test execution and regression verification against that baseline
  8. PR creation and merge (CI verdict = hard gate)

Usage#

# 기존 버그 이슈로 시작
/cc-dev:bugfix {issue_number}

# 새 버그 리포트부터 시작
/cc-dev:bugfix --new [이미지_경로]  " 버그 설명 "

Parameters#

ParameterRequiredDescription
issue_number✅*Existing bug issue number
--new✅*New bug report creation mode
--no-mergePR creation only (no merge)
--skip-testsSkip tests (for urgent hotfixes)
--baseBase branch (default: development)
--deviceRepro target device id/name (default: auto — physical → virtual)
--skip-deviceSkip Step 3.5/4.7 runtime repro (recorded as runtime-repro waiver)

*Either issue_number or --new is required

Option Examples#

# PR only (no merge)
/cc-dev:bugfix 123 --no-merge

# Skip tests (urgent)
/cc-dev:bugfix 123 --skip-tests

# Change base branch
/cc-dev:bugfix 123 --base main

# New bug + image analysis
/cc-dev:bugfix --new /tmp/screenshot.png  " App crash " 

 # Pin the repro device (id from `flutter devices --machine`)
/cc-dev:bugfix 123 --device 00008130-001A2C3D0E1F001E

# Static-only analysis (no device repro) — cause is recorded as  " presumed " 
 /cc-dev:bugfix 123 --skip-device

Execution Flow#

아래 박스 다이어그램은 파생 뷰(non-normative) 다. 예산·게이트 판정 같은 은 각 Step 절의 실행 블록이 규범이며, 둘이 어긋나면 실행 블록이 이긴다. 표기법·루프 계약 7필드·게이트 tri-state· 팬아웃 의무는 ../rules/orchestration-graph.md 가 SoT다 — 여기서 다시 정의하지 않는다. 이 문서의 루프 id: L-bf3.5→Step 3.5 · L-bf4.5→Step 4.5 · L-bf6→Step 6.

┌─────────────────────────────────────────────────────────────────┐
│                    /cc-dev:bugfix                                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Step 0: Blast Radius Analysis (before fix)                     │
│  ├── Identify all callers/dependents of affected code (Grep)    │
│  ├── Symbol grep budget 40 · over-cap → deferred log (no cut)   │
│  ├── Map test coverage of affected areas                        │
│  ├── Flag untested areas that could regress                     │
│  └── Risk assessment:                                           │
│       LOW  (5 files)  → proceed                                │
│       MED  (5-15 files or multi-layer) → confirm with user      │
│       HIGH ( > 15 files or core module)  → suggest /plan → /build │
│       deferred  >   0LOW 금지 (판정 불가는 통과가 아니다)       │
│                                                                 │
│  Step 1: Bug Report Creation (with --new option)                │
│  ├── Call /cc-quality:bug-report (image analysis)                          │
│  ├── Create ZenHub issue                                        │
│  └── Result: obtain issue_number                                │
│                                                                 │
│  Step 2: Query Issue Info                                        │
│  ├── mcp__zenhub__searchLatestIssues                           │
│  ├── Extract issue type, title, body                            │
│  └── Analyze bug cause                                          │
│                                                                 │
│  Step 3: Branch Creation                                         │
│  ├── fix/{number}-{slug} format                                 │
│  ├── issue-state-agent →  " In Progress "   (+ 부모 cascade)         │
│  └── Based on development branch                                │
│                                                                 │
│  Step 3.5: Runtime Repro  &   Diagnosis (device-first) ⚠️           │
│  ├── flutter devices --machine → tier: physical  >   virtual       │
│  ├── none/undetermined → ASK → waived: runtime-repro (기록)     │
│  ├── flutter run -d {id} (background) → 이슈 재현 절차 실행     │
│  ├── 태그 계측 BUGFIX-DEBUG(#N) + hot reload → 로그 수집        │
│  └── Loop L-bf3.5 (budget 3) → rootCause evidence(file:line)    │
│                                                                 │
│  Step 4: Bug Fix Work                                            │
│  ├── Step 3.5 근거(file:line)를 고친다 — 추측 수정 금지         │
│  ├── Search related code                                        │
│  ├── Fix code                                                    │
│  └── Create incremental commits (계측 라인은 스테이징 제외)     │
│                                                                 │
│  Step 4.2: Runtime Fix Verification + 계측 회수 ⚠️               │
│  ├── hot restart → 재현 절차 재실행 → reproAfterFix             │
│  ├── 재현 잔존 → Step 3.5 복귀(bound 1)BLOCKED 2회차         │
│  ├── null(판정 불가) ≠ 고쳐짐 — unverified 로 기록              │
│  ├── BUGFIX-DEBUG 전량 회수 → git grep 잔여 0건 게이트          │
│  └── 회수 후 1회 재확인 → app process kill                      │
│                                                                 │
│  Step 4.5: Existing Test Verification (Baseline) ⚠️              │
│  ├── Run existing tests for related packages                    │
│  ├── On failure → repair loop L-bf4.5 (budget 2 iterations)     │
│  ├── 약화 금지 — no new skip:, no test deletion                 │
│  └── Record baseline pass count (Step 6 이 대조하는 값)         │
│                                                                 │
│  Step 5: Test Writing (required, delegated to cc-flutter)    │
│  ├── Frontend tests                                              │
│  │   ├── unit-test-agent → UseCase parameter passing tests      │
│  │   ├── bloc-test-agent → BLoC state transition verification   │
│  │   └── widget-test-agent → State copyWith rendering tests     │
│  ├── Backend tests                                               │
│  │   ├── Endpoint auth/permission tests                         │
│  │   ├── Filter parameter passing integration tests             │
│  │   └── Service logic unit tests (if applicable)               │
│  └── Create test commits                                        │
│                                                                 │
│  Step 6: Test Execution and Regression Verification              │
│  ├── Run all tests (existing + new)                             │
│  ├── Auto-fix loop L-bf6 — budget 3, failed count must drop     │
│  └── Gate: failed==0  & &   passed  > = baseline (skipped may not up) │
│                                                                 │
│  Step 7: PR Creation                                             │
│  ├── ⛔ runPrePushGate() + dup recheck (run.md 8.5/9.0 위임)    │
│  ├── ⛔ 진단 계측 잔여 0(BUGFIX-DEBUG)Step 4.2 재확인     │
│  ├── gh pr create                                               │
│  ├── issue-state-agent →  " Review "                                │
│  └── Auto-link issue (Closes #{number})                         │
│                                                                 │
│  Step 8: Merge (merge = Close, unless --no-merge)                │
│  ├── Wait for review                                            │
│  ├── ⛔ Gate 0: openChildrenStatus none (unknown = block)       │
│  ├── ⛔ Gate 1: CI verdict pass (fail·pending·0= block)      │
│  ├── Squash merge                                               │
│  ├── Issue auto-Close via GitHub  " Closes # "   keyword             │
│  └── ⭐ Verify closed (GitHub + ZenHub) + fallback              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Detailed Implementation#

Step 0: Blast Radius Analysis#

Run before creating a branch to assess fix complexity and risk:

// 0. 이 사이클의 **내구 기록 누산기** — 여기 담긴 것만 Step 7 에서 PR body 로 실린다
//    (run.md 의 prBodyExtras 와 같은 역할. 콘솔 경고는 기록이 아니다)
const prBodyExtras = {
  blastRadiusDeferred: null, runtimeDiagnosis: null,
  testBaseline: null, testResult: null, waivedGates: [],
};

// 1. Identify affected code area from issue description
const affectedFiles = await Grep({ pattern: bugPattern, output_mode:  " files_with_matches "   });

// 2. Find all callers/dependents
//    기질(substrate)은 **순차 유지** — 이 중첩 grep 은 inline 이며 병렬화하지 않는다
//    (기질 선택 근거: ../rules/orchestration-graph.md §4 —  " 순차/변경 없음 " 이 정당한 답).
//    재시도 루프가 아니라 **경계 있는 열거**이므로 7필드 루프 계약 대상이 아니다.
//    필요한 것은 §5 의 두 가지뿐: 예산(cap)과 **deferred 로그**(조용한 절단 금지).
const SYMBOL_CAP = 40;              // grep 호출 상한 (affectedFiles 전체 합산)
const dependents = [];
const deferredSymbols = [];         // cap 때문에 훑지 못한 심볼 — 조용히 버리지 않는다
let symbolBudget = SYMBOL_CAP;
for (const file of affectedFiles) {
  const exports = extractExports(file); // classes, functions, mixins
  for (const symbol of exports) {
    if (symbolBudget  < = 0) { deferredSymbols.push(`${file}:${symbol}`); continue; }
    symbolBudget--;
    const callers = await Grep({ pattern: symbol, output_mode:  " files_with_matches "   });
    dependents.push(...callers);
  }
}
const uniqueDependents = [...new Set(dependents)];
// deferred 로그 — 콘솔이 아니라 판정 출력과 PR body 양쪽에 남긴다(SoT §5: 조용한 절단 금지)
if (deferredSymbols.length  >   0) {
  prBodyExtras.blastRadiusDeferred = deferredSymbols;   // Step 7 PR body 에 실린다
  console.warn(`⚠️ 심볼 예산 ${SYMBOL_CAP} 초과 — 미조회 ${deferredSymbols.length}: ${deferredSymbols.join( " ,  " )}`);
}

// 3. Map test coverage
const testFiles = await Glob({ pattern:  " test/**/*_test.dart "   });
const coveredFiles = matchSourceToTest(uniqueDependents, testFiles);
const uncoveredFiles = uniqueDependents.filter(f = >   !coveredFiles.includes(f));

// 4. Risk assessment
const fileCount = uniqueDependents.length;
const layerCount = countLayers(uniqueDependents); // presentation, domain, data, backend

let riskLevel;
if (fileCount  < = 5  & &   layerCount  < = 1) {
  riskLevel =  " LOW " ;  // → proceed directly
} else if (fileCount  < = 15 || layerCount  < = 2) {
  riskLevel =  " MEDIUM " ;  // → confirm with user
} else {
  riskLevel =  " HIGH " ;  // → suggest /plan → /build workflow
}

// ⚠️ 절단된 판정은 LOW 가 될 수 없다 — fileCount 는 **하한**일 뿐이다.
//    판정 불가를 통과로 읽지 않는다 (SoT §3: undetermined 기본값은 fail).
if (deferredSymbols.length  >   0  & &   riskLevel ===  " LOW " ) riskLevel =  " MEDIUM " ;

Output:

╔════════════════════════════════════════════════════════════════╗
║  Blast Radius Analysis                                         ║
╠════════════════════════════════════════════════════════════════╣
║  Affected files: {fileCount}                                   ║
║  Layers touched: {layers}                                      ║
║  Test coverage: {coveredCount}/{totalCount} files covered       ║
║  Deferred symbols (over cap 40): {deferredCount}               ║
║  Uncovered areas:                                              ║
║    - {uncoveredFile1}                                          ║
║    - {uncoveredFile2}                                          ║
║  Risk Level: {LOW|MEDIUM|HIGH}                                 ║
║                                                                ║
║  {recommendation}                                              ║
╚════════════════════════════════════════════════════════════════╝
  • LOW: 자동 진행
  • MEDIUM: 사용자에게 확인 요청 ("Proceed with bugfix?" / "Switch to /plan → /build?")
  • HIGH: /plan → /build 워크플로우 권장 (구조적 변경이 필요할 가능성 높음)
  • deferred > 0: LOW 로 내려가지 않는다(최소 MEDIUM). 훑지 못한 심볼이 있으면 fileCount 는 하한값이며, 미조회 목록은 판정 출력과 PR body(blastRadiusDeferred)에 남는다 — ../rules/orchestration-graph.md §5(deferred 로그) · §3(판정 불가 ≠ 통과).

Step 1: Bug Report Creation (Optional)#

⚠️ 이슈(버그 리포트) 생성은 ZenHub(GitHub)에만 합니다. Jira에는 이슈를 만들지 않으며, Jira는 생성된 이슈를 조회·확인하는 읽기 전용 용도로만 씁니다. 정책: SKILL.md → "Issue Tracker Policy".

When --new option is provided:

// /cc-quality:bug-report 스킬 호출
const bugReport = await Skill({
  skill:  " bug-report " ,
  args: `${imagePath}  " ${description} " `
});

// Extract created issue number
const issueNumber = bugReport.issue_number;

Step 2: Query Issue Info#

// Query issue info
const issueResult = await mcp__zenhub__searchLatestIssues({
  query: issue_number.toString()
});

const issue = issueResult.find(i = >   i.number === issue_number);

// Extract bug info
const bugInfo = {
  id: issue.id,
  number: issue.number,
  title: issue.title,
  body: issue.body,           // Includes reproduction steps, expected/actual results
  severity: extractSeverity(issue.labels),
  area: extractArea(issue.labels),
};

Step 3: Branch Creation#

// Generate branch name: fix/{number}-{slug}
// createSlug romanizes non-ASCII and never returns  ' '   (see issue-branch-agent.md)
const slug = createSlug(bugInfo.title,  " fix " );
const branchName = `fix/${bugInfo.number}-${slug}`;
// Defense-in-depth (LANG-05): a Korean-only title must not yield  " fix/30- " .
if (!slug || branchName.endsWith( " - " )) throw new Error(`빈 slug — 브랜치명 비정상(${branchName})`);

// Create branch
await Bash(`git checkout -b ${branchName} origin/development`);

// ⛔ 착수 전 점유 확인 — 이 버그를 다른 세션이 잡고 있으면 브랜치도 만들지 않고 물러난다
//    (판정 SoT: ../rules/zenhub-conventions.md → Work Claim Contract, `run.md` Step 0.4 와 같은 규칙)
if (await claimStatus(bugInfo.number, me) ===  " other-live " ) {
  throw new Error(`⛔ #${bugInfo.number} 는 다른 세션이 진행 중 — [INCOMPLETE: issue_occupied]. 상대 종료 후 재실행`);
}

// Pipeline move: In Progress (fail-closed id 해석 — undefined silent no-op 방지)
const workspace = await mcp__zenhub__getWorkspacePipelinesAndRepositories();
const inProgress = workspace.pipelines.find(p = >   p.name ===  " In Progress " );
if (!inProgress) throw new Error(` ' In Progress '   파이프라인 없음. 라이브: ${workspace.pipelines.map(p = >   p.name).join( " ,  " )}`);
await mcp__zenhub__moveIssueToPipeline({ issueId: bugInfo.id, pipelineId: inProgress.id });
await acquireClaim(bugInfo, { branch: branchName });   // 이동과 같은 자리 — 대장 없이는 cascade 와 구분되지 않는다

// 부모(Epic 등)가 아직 착수 전 칸이면 조부모까지 함께  " 진행 중 " 으로 cascade —
// agents/dev/issue-state-agent.md → cascadeStartToParents
await cascadeStartToParents(bugInfo.number);

// 스프린트 배정(타임라인 가시성) — 버그 이슈를 활성 스프린트에 올려 로드맵/번다운에 노출.
// 기본 on, --no-sprint 로 opt-out. 셀렉터: rules/zenhub-conventions.md →  " Sprint Selector Resolution " .
if (!options.noSprint) {
  const sprint = options.sprint ===  " next " 
     ? await mcp__zenhub__getUpcomingSprint()
    : await mcp__zenhub__getSprint();            // id 없음 = 활성
  if (sprint?.id) await mcp__zenhub__addIssuesToSprints({ issueIds: [bugInfo.id], sprintIds: [sprint.id] });
}

Step 3.5: Runtime Repro & Diagnosis — 실기기 우선 ⚠️#

추측으로 고치지 않는다. 이 단계는 코드를 고치지 않고 관측만 한다 — 연결된 기기에 앱을 띄워 이슈의 재현 절차를 실제로 밟고, 태그 붙은 임시 로그를 심어 원인을 파일:라인 + 로그 실측으로 특정한다. 여기서 나온 근거가 Step 4 가 고칠 대상이고, Step 4.2 가 "정말 고쳐졌는지" 대조할 기준이다.

왜 별도 단계인가: 종전 Step 4 는 이슈 본문 + Grep 만으로 원인을 추론했다. 런타임에서만 드러나는 원인(비동기 순서, 상태 초기화, 플랫폼 채널, 권한, 실기기 전용 하드웨어)은 정적 분석에 보이지 않는다. 기기가 연결돼 있는데 띄워 보지 않는 것은 가진 증거를 버리는 것이다.

기기 등급(tier)physical(실기기) > virtual(시뮬레이터·에뮬레이터) > none. --device 로 고정할 수 있고, 지정한 기기가 없으면 조용히 다른 기기로 갈아타지 않는다. 조작 도구(marionette / flutter-skill / flutter-inspector) 선택 기준은 cc-flutter:mcp-tool-selection 이 SoT다 — 여기서 재정의하지 않는다.

// ── 1. 기기 실측 — tier 판정의 SoT 는 `flutter devices --machine` 이다.
//    run.md Step 0 의 `flutter devices | grep •` 는 **존재 여부**만 답한다(tier 를 답하지 않는다).
const devicesRaw = (await Bash(`command -v flutter  > /dev/null 2 > & 1  & &   flutter devices --machine 2 > /dev/null || true`)).trim();
let devices = null;
try { devices = devicesRaw ? JSON.parse(devicesRaw) : null; } catch { devices = null; }

// 조회 실패를  " 기기 없음(빈 배열) " 으로 읽지 않는다 — empty-set pass 방지(SoT §3.2).
// 둘 다 결과는 waive 지만 **PR 에 남는 이유가 다르다**(없음 vs 판정 불가).
const deviceProbe = devices === null ?  " undetermined "   : devices.length === 0 ?  " none "   :  " ok " ;

// 실행 후보만 남긴다. `emulator === false` = 실기기(데스크톱 타깃 포함), `true` = 시뮬레이터/에뮬레이터.
// 웹 타깃은 VM Service 기반 계측·캡처가 안 되므로 이 경로의 후보가 아니다(run.md Step 7.3 과 같은 제한).
const WEB_TARGETS = [ " chrome " ,  " edge " ,  " web-server " ];
const runnable = (devices ?? []).filter(d = >   d.isSupported !== false  & &   !WEB_TARGETS.includes(d.id));

// 이슈가 플랫폼을 말하면(라벨·본문의 ios/android/macos 등) 그쪽을 먼저 고른다.
// ⚠️ 이 필터는 후보를 0으로 만들 수 없다 — 플랫폼 단서가 없거나 일치가 없으면 **전체를 그대로 반환**한다.
//    (필터가 후보를 지워  " 기기 없음 " 으로 떨어지는 것이 이 자리의 fail-open 이다)
const matchesIssuePlatform = (list) = >   {
  const hint = [ " ios " ,  " android " ,  " macos " ,  " windows " ,  " linux " ]
    .find(p = >   `${bugInfo.area ??  " " } ${bugInfo.title} ${bugInfo.body}`.toLowerCase().includes(p));
  const matched = hint ? list.filter(d = >   (d.targetPlatform ??  " " ).toLowerCase().includes(hint)) : [];
  return matched.length  >   0 ? matched : list;
};
const pick = (isEmu) = >   matchesIssuePlatform(runnable.filter(d = >   d.emulator === isEmu));

if (options.device  & &   !runnable.find(d = >   d.id === options.device || d.name === options.device)) {
  throw new Error(
    `⛔ --device ${options.device} 를 찾지 못함 — 다른 기기로 대체하지 않는다. ` +
    `연결된 기기: ${runnable.map(d = >   `${d.name}(${d.id})`).join( " ,  " ) ||  " 없음 " }`
  );
}
const target = options.device
  ? runnable.find(d = >   d.id === options.device || d.name === options.device)
  : (pick(false)[0] ?? pick(true)[0] ?? null);
const deviceTier = !target ?  " none "   : target.emulator === false ?  " physical "   :  " virtual " ;

// ── 2. 기기 없음 / 명시적 스킵 — **무음 스킵 금지**(GD-01 계열).
//    run.md Step 0 의 device 예외(§ " ⚠️ 예외 " )와 같은 취급이다: 확인받고, PR body 에 영구 기록한다.
if (!target || options.skipDevice) {
  const confirm = options.unattended
    ?  " 스킵 확인 " 
     : await AskUserQuestion({
        questions: [{
          header:  " 런타임 진단 스킵 확인 " ,
          question: !target
            ? `재현에 쓸 기기가 없습니다(probe: ${deviceProbe}). 실행 없이 정적 분석만으로 원인을  ' 추정 ' 할까요?`
            :  " --skip-device — 기기 재현 없이 진행하면 원인은 추정으로만 남습니다. 계속할까요? " ,
          options: [
            { label:  " 스킵 확인 " , description:  " 원인은  ' 추정(런타임 미검증) ' 으로 PR body 에 기록됩니다 "   },
            { label:  " 기기 연결 후 재시도 " , description:  " 기기를 연결·부팅한 뒤 Step 3.5 부터 재개합니다 "   },
          ],
        }],
      });
  if (confirm ===  " 기기 연결 후 재시도 " ) {
    // 재개는 멱등하다 — 브랜치(Step 3)는 이미 있으므로 같은 명령을 다시 실행하면 이 단계로 들어온다
    throw new Error(`⛔ 기기 연결 대기 — 연결 후 \`/cc-dev:bugfix ${bugInfo.number}\` 재실행 (Step 3 은 멱등)`);
  }
  prBodyExtras.runtimeDiagnosis = {
    status:  " waived " , deviceTier:  " none " , probe: deviceProbe,
    reason: options.skipDevice ?  " --skip-device (user confirmed) "   : `no device (${deviceProbe})`,
  };
  prBodyExtras.waivedGates.push( " runtime-repro " );
}

// ── 3. 앱 기동 — 백그라운드 별도 프로세스. Step 4.2 가 **같은 프로세스**에 hot restart 를 건다
if (!prBodyExtras.runtimeDiagnosis) {
  const boot = await Bash(`flutter run -d ${target.id} --debug --print-dtd 2 > & 1  & `, { run_in_background: true });
  // 추출 규칙은 run.md Step 7.3 과 동일 — 재구현하지 않는다(DTD 와 VM Service 는 서로 다른 URI다)
  const vmServiceUri = extractVmServiceUri(boot.stdout);
  if (!vmServiceUri) {
    throw new Error(`⛔ VM Service URI 파싱 실패 — 부팅 실패를  ' 재현 안 됨 ' 으로 읽지 않는다:\n${boot.stdout.slice(-800)}`);
  }
  const artifactDir = `.claude/docs/bugfix/${bugInfo.number}/runtime`;
  await Bash(`mkdir -p ${artifactDir}`);

  // ── 4. 진단 루프 L-bf3.5 — (재현 → 관측 → 계측 → hot reload) 1바퀴가 1 iteration ──
  const DIAG_BUDGET = 3;
  const unknowns = new Set([ " repro " ,  " failing-point " ,  " root-cause " ]);  // prog: 이 집합이 매 iteration 줄어야 한다
  const instrumentation = [];   // 심은 태그 위치 — Step 4.2 가 **이 목록을 회수한다**
  let repro = null, rootCause = null, round = 0;

  while (unknowns.size  >   0  & &   round  <   DIAG_BUDGET) {
    round++;
    const before = unknowns.size;

    // (a) 재현 — 이슈 본문의 재현 절차를 그대로 밟고 증빙을 남긴다(스크린샷·로그).
    //     reproduced 는 tri-state: true | false | null(판정 불가). null 을 false 로 읽지 않는다.
    repro = await reproduceFromIssue(bugInfo.body, { vmServiceUri, artifactDir, round });
    if (repro.reproduced !== null) unknowns.delete( " repro " );

    // (b) 관측 — 에러/스택트레이스/상태. 이 단계에서 앱 동작을 바꾸는 수정은 금지다
    const errors = await Skill({ skill:  " cc-inspector:log " , args:  " errors "   });
    await Write({ file_path: `${artifactDir}/logs-round${round}.txt`, content: errors.text ??  " "   });
    if (errors.stackTrace) unknowns.delete( " failing-point " );

    // (c) 계측 — 아직 지점/원인이 안 잡히면 의심 경로에 **태그 붙은 임시 로그**를 심는다.
    //     불변식 3가지: ① 태그 필수 ② 관측 전용(조건·분기·상태 변경 금지) ③ 소스에만(테스트 파일 계측 금지)
    //
    //       // BUGFIX-DEBUG(#123) — Step 4.2 에서 회수
    //       debugPrint( ' [bugfix#123] loadBooks in: filter=$filter · state=${state.status} ' );
    //
    //     반영은 hot reload. main()/전역/상태 shape 변경이 필요하면 hot restart 로 올린다.
    for (const site of chooseInstrumentationSites({ repro, errors, dependents: uniqueDependents })) {
      await addTaggedLog(site, bugInfo.number);      // 같은 지점에 같은 로그를 다시 심지 않는다
      instrumentation.push(site);                   //  " file:line "   형식
    }
    await hotReload();                              // dart MCP 또는 marionette hot_reload

    // (d) 원인 특정 — 로그 실측으로 파일:라인을 짚었을 때만 확정이다(추론만으로는 아니다)
    rootCause = await inferRootCause({ logs: `${artifactDir}/logs-round${round}.txt`, instrumentation });
    if (rootCause?.file  & &   rootCause?.evidence) unknowns.delete( " root-cause " );

    console.log(`bf3.5 #${round}/${DIAG_BUDGET}: repro=${repro.reproduced} · 계측 ${instrumentation.length}곳 · unknown ${before}→${unknowns.size}`);
    if (unknowns.size  > = before) break;   // no-prog: 남은 예산을 같은 계측에 쓰지 않고 Rung 2 로 올린다
  }

  // ── 5. 판정 —  " 재현 안 됨 " 을  " 버그 없음 " 으로 읽지 않는다 ──
  //    가상 기기는 하드웨어 의존 영역(카메라·푸시·결제·생체·권한·딥링크·백그라운드)을 재현하지 못한다.
  //    그 조합에서의 재현 실패는 **근거가 아니라 한계**이므로 limits 로 남긴다.
  const HW_DEPENDENT = [ " camera " ,  " push " ,  " iap " ,  " 결제 " ,  " biometric " ,  " permission " ,  " deeplink " ,  " background " ,  " bluetooth " ,  " location " ];
  const hwDependent = HW_DEPENDENT.some(a = > 
     `${bugInfo.area ??  " " } ${bugInfo.title} ${bugInfo.body}`.toLowerCase().includes(a));

  prBodyExtras.runtimeDiagnosis = {
    status: unknowns.size === 0 ?  " confirmed "   :  " inconclusive " ,
    deviceTier, device: `${target.name}(${target.id})`, vmServiceUri,
    reproduced: repro?.reproduced ?? null,
    rootCause,                       // { file, line, evidence } — Step 4 가 고칠 대상
    artifacts: artifactDir,
    instrumentation,                 // Step 4.2 회수 대상 (여기 없는 임시 로그는 애초에 만들지 않는다)
    limits: deviceTier ===  " virtual "   & &   hwDependent
      ?  " virtual device — 하드웨어 의존 영역 미검증(재현 실패를  ' 버그 없음 ' 으로 읽지 말 것) " 
       : null,
    rounds: round,
  };
}

루프 계약 L-bf3.5 (bugfix.step3_5-runtime-diagnosis) — 필드 규약은 ../rules/orchestration-graph.md §2, 값은 여기가 자기 자리다.

inv:      이 루프는 **관측만 한다** — 앱 동작을 바꾸는 수정 금지(그건 Step 4).
          임시 로그는 `BUGFIX-DEBUG(#N)` 태그 필수 · 조건/분기/상태 변경 금지 · 테스트 파일 계측 금지
prog:     residual = unknowns{repro, failing-point, root-cause}.size, 매 iteration 강한 감소
          no-prog: 같은 지점에 같은 계측을 반복하지 않는다 → 한 층 위/아래(호출자·피호출자)로 옮기거나 Rung 2
term:     unknowns.size === 0 — 재현 여부 확정 + 실패 지점 특정 + rootCause.evidence(파일:라인 + 로그 실측)
budget:   3 iterations (1 iteration = 재현 → 관측 → 계측 → hot reload)
exhaust:  BLOCKED 아님 — `runtimeDiagnosis.status =  " inconclusive " ` 로 내려 적고 Step 4 로 진행한다.** " 원인 확정 " 을 주장하지 않는다**(PR body 에 inconclusive 로 실린다).
          기기 부재(`waived`)와 진단 실패(`inconclusive`)는 서로 다른 값으로 구분해 기록한다
resume:   `.claude/docs/bugfix/{N}/runtime/` 산출물 + `RESIDUE_CMD`(git grep BUGFIX-DEBUG)로 계측 위치 재조회
          (앱은 재기동해도 멱등 — 계측은 워킹트리에 있고 커밋되지 않는다)
log:       " bf3.5 #2/3: repro=yes · 계측 3곳 · unknown 3→1 "   + 심은 태그 위치와 수집한 로그 경로
  • --skip-tests 는 이 단계를 면제하지 않는다. 런타임 진단을 빼는 스위치는 --skip-device 하나뿐이고, 그 스킵조차 무음이 아니다(waivedGates: runtime-repro + PR body 기록).
  • 진단 산출물(.claude/docs/bugfix/{N}/runtime/)은 커밋하지 않는다 — PR body 에는 경로와 요약만 싣는다.

Step 4: Bug Fix Work#

// ⛔ 근거 우선 — Step 3.5 가 `confirmed` 면 그 근거(file:line)를 고친다.
//    `inconclusive`·`waived` 여도 정적 분석으로 진행할 수 있지만, 그때 원인은 **추정**이며
//    PR body 에 그렇게 실린다.  " 추정 " 을  " 확인 " 으로 승격시키는 것은 이 문서에서 금지한다.
const rc = prBodyExtras.runtimeDiagnosis;
if (rc?.status ===  " confirmed "   & &   rc.rootCause?.file) {
  console.log(`▶ 수정 대상(런타임 확인): ${rc.rootCause.file}:${rc.rootCause.line} — ${rc.rootCause.evidence}`);
} else {
  console.warn(`⚠️ 런타임 근거 없음(${rc?.status ??  " none " }) — 정적 분석 기반 **추정** 수정으로 진행`);
}

// Bug cause analysis
// 1. Identify reproduction steps from issue body
// 2. Search related code (Grep, Glob)
// 3. Infer cause  ← Step 3.5 의 rootCause 가 있으면 그것이 이 추론을 대체한다

// Code fix
// - Modify related files
// - Create incremental commits

// ⚠️ 계측 라인은 커밋하지 않는다 — 수정 파일 경로를 **명시해** 스테이징한다.
//    `git commit -am` 은 워킹트리의 BUGFIX-DEBUG 계측까지 함께 싣는다(금지).
await Bash(`git add ${fixedPaths.join( "   " )}`);
await Bash(`git commit -m  " fix(${scope}): 🐛 ${bugInfo.title} " `);

Step 4.2: Runtime Fix Verification & Instrumentation Removal ⚠️#

수정이 기기에서 실제로 통했는지 같은 절차로 되밟아 확인하고, 진단용 계측을 전량 회수한다. Step 3.5 를 waive 한 경우(기기 없음/--skip-device)에는 이 단계도 함께 waive 되며, 그 사실은 이미 waivedGates 에 남아 있다 — 여기서 다시 면제 결정을 하지 않는다.

const diag = prBodyExtras.runtimeDiagnosis;
if (diag  & &   diag.status !==  " waived " ) {
  // ── 1. 재현 재실행 — 코드 수정은 hot reload 로 안 잡히는 경우가 많다(상태 초기화 필요) ──
  await hotRestart();   // dart MCP `hot_restart` 또는 marionette `hot_restart` — 도구 선택은 mcp-tool-selection
  const after = await reproduceFromIssue(bugInfo.body, { vmServiceUri: diag.vmServiceUri, artifactDir: diag.artifacts, round:  " after-fix "   });

  if (after.reproduced === null) {
    // 판정 불가를  " 고쳐짐 " 으로 읽지 않는다 (null-as-pass 방지, SoT §3.2)
    diag.fixVerified =  " unverified " ;
    console.warn( " ⚠️ 수정 후 재현 판정 불가 —  ' 고쳐짐 ' 으로 기록하지 않는다 " );
  } else if (after.reproduced === true) {
    // 아직 재현된다 = 안 고쳐졌다. Step 3.5 로 **1회만** 복귀한다(bound:1).
    if (diag.reworked) {
      await blockIssue(bugInfo,  " runtime_fix_unverified " ,
        { detail:  " 수정 후에도 기기에서 재현됨(2회차) — bound:1 소진 "   });
      throw new Error(`⛔ BLOCKED( ' runtime_fix_unverified ' ) — 수정 후에도 기기에서 재현됨(2회차). ` +
        `보드 반영(holding) + 점유 해제 완료, 중단`);
    }
    diag.reworked = true;               // invalidates: rootCause · Step 4 의 수정 커밋 재검토
    return await rerunFromStep3_5();    // 이 문서의 유일한 되돌림 엣지(bound:1).
                                        // 계측은 살아 있으므로 다음 라운드는 그 위에서 이어간다
  } else {
    diag.fixVerified =  " verified " ;
  }

  // ── 2. 계측 회수 — 남길 로그는 **태그를 떼고** 프로젝트 로깅 규약으로 다시 쓴다(그 결정도 기록한다) ──
  //     removeInstrumentation(site): 태그 줄을 지우거나, 승격 대상이면 규약 로깅으로 교체하고
  //     교체 사실을 반환한다. 어느 쪽이든 `BUGFIX-DEBUG` 문자열은 남지 않는다.
  diag.promotedLogs = [];
  for (const site of diag.instrumentation ?? []) {
    const r = await removeInstrumentation(site);
    if (r?.promoted) diag.promotedLogs.push(r.promoted);   // 영구 로깅 승격분 — PR body 에 실린다
  }

  // ⛔ 잔여 게이트 — 사람 눈이 아니라 grep 이 판정한다. 커밋/워킹트리 양쪽을 한 번에 본다.
  //    `--untracked` 로 아직 add 하지 않은 파일까지 보고, 진단 산출물 디렉터리만 제외한다.
  const RESIDUE_CMD = `git grep -n --untracked  " BUGFIX-DEBUG "   -- .  ' :!.claude/docs/bugfix '   || true`;
  const residue = (await Bash(RESIDUE_CMD)).trim();
  if (residue) throw new Error(`⛔ 진단 계측 잔여 — 회수 후 재시도:\n${residue}`);
  // 일반 디버그 산출물(print/debugPrint/하드코딩 debug 플래그)의 정본 체크리스트는
  // ../agents/pr-readiness-agent.md 다 — 여기서 재정의하지 않는다.

  // ── 3. 회수 후 1회 재확인 — 계측에 부작용이 섞여 있었다면 여기서 드러난다 ──
  await hotRestart();
  const sanity = await reproduceFromIssue(bugInfo.body, { vmServiceUri: diag.vmServiceUri, artifactDir: diag.artifacts, round:  " after-cleanup "   });
  if (sanity.reproduced === true) {
    throw new Error( " ⛔ 계측 회수 후 재현 — 계측이 동작을 바꾸고 있었다(관측 전용 불변식 위반). Step 3.5 로 복귀 " );
  }

  // ── 4. 앱 프로세스 종료 (run.md Step 7.3 과 같은 종료 규칙) ──
  await Bash(`kill %1 2 > /dev/null || true`);

  // 계측을 커밋하지 않았다면 회수 커밋도 없다. 실수로 커밋됐다면 회수 커밋을 따로 남긴다:
  //   git commit -m  " chore(${scope}): 🔥 진단 계측 회수 " 
 }

게이트 요약 (tri-state)fixVerifiedverified 하나만 통과다.

fixVerified의미처리
verified수정 후 같은 절차에서 재현되지 않음통과 — PR body 에 기록
unverified재현 판정 불가(앱 부팅 실패·절차 불명 등)통과 아님 — PR body 에 미검증으로 명기, 리뷰어 확인 필요
(재현 잔존)아직 재현됨Step 3.5 복귀 1회 → 2회차면 BLOCKED('runtime_fix_unverified')

Step 4.5: Existing Test Verification (Baseline) ⚠️#

Step 6 이 대조할 기준선을 여기서 한 번만 만든다. 기준선 없이 "회귀 없음"을 주장할 수 없다.

불변식 — 기존 테스트는 고칠 수는 있어도 약화시킬 수 없다. skip:/@Skip 신규 추가 금지, test/testWidgets/blocTest/group 삭제 금지, 통과 개수는 줄어들 수 없다. 이 셋 중 하나라도 위반하면 게이트가 아니라 위장이다.

--skip-tests 는 Step 4.5–6 을 함께 건너뛴다. 그때는 prBodyExtras.waivedGates.push("tests") 로 스킵 사실을 PR body 에 남긴다(콘솔만 남는 스킵은 없다 — SoT §7 규칙 6). 기준선이 없으면 Step 6 의 회귀 대조는 "미검증"이며, null 을 "회귀 없음"으로 읽지 않는다(§3.2 null-as-pass).

// 1) 수정 직후 상태로 기존 테스트 전량 1회 실측 — 이 값이 Step 6 의 비교 대상이다
//    parseTestSummary: `flutter test` 요약(+N passed / M failed / K skipped)을 숫자로 읽고,
//    요약 줄을 찾지 못하면 null 을 반환한다. exit code 를 파이프로 읽어 판정하지 않는다
//    (SoT §3.2 pipe-masked exit code — `|| true` 로 죽인 exit code 를 신뢰하지 않는다).
let baseline = parseTestSummary(
  await Bash(`melos exec --scope=${package} -- flutter test --reporter expanded 2 > & 1 || true`)
);   // { passed, failed, skipped, total }

// 2) 판정 불가를  " 기준선 0 "   으로 읽지 않는다 (SoT §3 — undetermined ≠ pass,
//    §3.2 nothing-to-check pass 형). 수집 0건은 컴파일/의존성 문제이므로 먼저 복구한다.
if (!baseline || baseline.total === 0) {
  throw new Error( " ⛔ 기준선 판정 불가 — 테스트 수집 0건. 코드젠/의존성 복구 후 재실행 " );
}

// 3) 실패가 있으면 L-bf4.5 보수 루프 (예산 2 iterations, 계약은 아래)
let bfRound = 0;
while (baseline.failed  >   0  & &   bfRound  <   2) {
  bfRound++;
  const before = baseline.failed;
  //  우선순위: (a) 내가 만든 회귀인지 확인 → 수정 코드 교정, (b) 그다음에만 기존 테스트 보수
  await repairBaseline(baseline);
  baseline = parseTestSummary(
    await Bash(`melos exec --scope=${package} -- flutter test --reporter expanded 2 > & 1 || true`)
  );
  console.log(`bf4.5 #${bfRound}/2: failed ${before}→${baseline.failed} · passed ${baseline.passed}`);
  if (baseline.failed  > = before) break;   // no-prog → 같은 수정 반복 금지
}

// 4) 약화 금지 기계 검사 — 사람의 선의에 맡기지 않는다
const testDiff = await Bash(`git diff ${options.base ??  " origin/development " }...HEAD --  " **/test/** "   || true`);
const addedSkips   = (testDiff.match(/^\+.*(\bskip:\s*(true|[ ' " ])|@Skip\b)/gm) ?? []).length;
const deletedTests = (testDiff.match(/^-\s*(test|testWidgets|blocTest|group)\s*\(/gm) ?? []).length;
if (addedSkips  >   0 || deletedTests  >   0) {
  throw new Error(`⛔ 기존 테스트 약화 감지 — skip 추가 ${addedSkips}건 · 테스트 삭제 ${deletedTests}건. 되돌린 뒤 원인을 고친다`);
}

if (baseline.failed  >   0) {
  //  exhaust: 여기서 멈춘다. 기준선 없이 Step 5–6 으로 진행하면 Step 6 의 비교가 성립하지 않는다
  throw new Error(`⛔ BLOCKED( ' baseline_repair_exhausted ' ) — 기존 테스트 실패 ${baseline.failed}건 잔존`);
}

// 5) 내구 기록 — 기준선은 콘솔이 아니라 PR body 로 간다 (Step 7 이 싣는다)
prBodyExtras.testBaseline = { passed: baseline.passed, skipped: baseline.skipped, total: baseline.total };

루프 계약 L-bf4.5 (bugfix.step4_5-baseline-repair) — 필드 규약은 ../rules/orchestration-graph.md §2, 값은 여기가 자기 자리다.

inv:      기존 테스트는 고칠 수는 있어도 **약화시킬 수 없다** — `skip:`/`@Skip` 신규 추가 금지,
          test/testWidgets/blocTest/group 삭제 금지, passed 는 줄어들 수 없다
prog:     residual = baseline.failed, 매 iteration 강한 감소
          no-prog: 동일 실패 집합에 같은 수정 반복 금지 → Rung 2(`/cc-dev:unstuck`)로 종류를 바꾼다
term:     baseline.failed === 0  & &   baseline.total  >   0 (수집 0건은 term 이 아니라 판정 불가)
budget:   2 iterations (1회는 수정 코드 교정, 1회는 기존 테스트 보수)
exhaust:  BLOCKED( ' baseline_repair_exhausted ' )../rules/zenhub-conventions.md → Blocked Issue
          Contract 대로 보드 반영 후 중단. Step 5 로 진행 금지, `--skip-tests` 로 우회 금지
resume:   `melos exec --scope={package} -- flutter test` 재실측으로 위치 판정
          (대화 카운터는 /clear 를 못 넘으므로 예산이 아니다)
log:       " bf4.5 #1/2: failed 7→2 · passed 51 "   + 손댄 기존 테스트 파일과  " 약화 아님 "   근거
  • exhaust: 의 3단 사다리 자체는 ../agents/sequential-workflow.md 가 SoT다(여기서 새 사다리를 발명하지 않는다).
  • --skip-testsStep 5–6 진입 전에만 유효하다. 기준선을 잡은 뒤 실패를 만나 사후에 켜는 것은 게이트 삭제이므로 금지한다.

Step 5: Unit Test Writing (Required)#

Frontend + backend unit tests must be written before PR creation.

5-1. Frontend Tests

Write the following tests depending on the changed code area:

// State tests: copyWith, nullable field clearing, Equatable, etc.
// File: test/presentation/bloc/{feature}_state_test.dart
// - Verify initial state default values
// - Verify copyWith updates
// - Verify nullable field clearing (clearXxx flags)
// - Verify value preservation (existing values maintained on copyWith() call)

// BLoC tests: event handlers → state transitions
// File: test/presentation/bloc/{feature}_bloc_test.dart
// - Verify per-event state transitions with blocTest()
// - Mock Repository setUp
// - Set initial state with seed()
// - Verify sequential state changes with expect()

// UseCase tests: parameter passing and error handling
// File: test/domain/usecase/{usecase}_test.dart
// - Verify correct parameter passing on Repository calls (verify)
// - Separate success/failure cases
// - Verify Params Equatable

5-2. Backend Tests

// Integration tests: using withServerpod()
// File: backend/kobic_server/test/integration/{feature}_test.dart
// - Auth test: reject unauthenticated access to requireLogin endpoints
// - Filter parameter passing test: verify correct calls per filter
// - Combined filter test: apply all filters simultaneously
// - Pagination test: pass limit/offset parameters

// Service unit tests (on business logic changes)
// File: backend/kobic_server/test/unit/{feature}/{service}_test.dart

5-3. Test Fixture Writing Rules

// Serverpod generated DTO classes have many required fields
// → Create test fixtures via helper functions
// e.g.: _createTestBook(), _createCategory()

// Mock generation: @GenerateNiceMocks + build_runner
// → Must regenerate mocks after writing test files
await Bash(`melos exec --scope=${package} -- dart run build_runner build -d`);

5-4. Test Commit

await Bash(`git commit -m  " test(${scope}): ✅ ${bugInfo.title} 테스트 추가 " `);

Step 6: Test Execution and Verification#

예산 3 은 여기(실행 경로)에 있다. ASCII 다이어그램의 "up to 3 times" 는 이 값의 파생 뷰다.

// Run all tests (existing + new)
let run = parseTestSummary(
  await Bash(`melos exec --scope=${package} -- flutter test --reporter expanded 2 > & 1 || true`)
);
if (!run || run.total === 0) throw new Error( " ⛔ 테스트 수집 0건 — 판정 불가는 통과가 아니다 " );

// 대조 기준선 — Step 4.5 가 기록만 하고 아무도 비교하지 않던 값을 여기서 쓴다
const base = prBodyExtras.testBaseline;
if (!base) throw new Error( " ⛔ 기준선 없음 — Step 4.5 를 건너뛰고 Step 6 만 실행할 수 없다 (null 은  ' 회귀 없음 ' 이 아니다) " );

// L-bf6 auto-fix 루프 — 예산·진행량·탈락 기록이 전부 이 블록 안에 있다
const AUTOFIX_BUDGET = 3;      // iterations
let attempt = 0;
while (run.failed  >   0  & &   attempt  <   AUTOFIX_BUDGET) {
  attempt++;
  const before = run.failed;
  await autoFixTestFailures(run);
  //   - Fix fixtures (generated class signature mismatches, etc.)
  //   - Build transitive dependencies (missing .g.dart, .module.dart)
  //   - Regenerate mocks (@GenerateNiceMocks + build_runner)
  run = parseTestSummary(
    await Bash(`melos exec --scope=${package} -- flutter test --reporter expanded 2 > & 1 || true`)
  );
  console.log(`bf6 #${attempt}/${AUTOFIX_BUDGET}: failed ${before}→${run.failed} · passed ${run.passed} (baseline ${base.passed})`);
  if (run.failed  > = before) break;   // no-prog: 남은 예산을 같은 수정에 쓰지 않고 Rung 2 로 올린다
}

// ⛔ 기준선 대조 — 세 조건 중 하나라도 어기면 통과가 아니다
const regressed = [];
if (run.failed  >   0)               regressed.push(`실패 ${run.failed}건 잔존 (auto-fix ${attempt}/${AUTOFIX_BUDGET} 소진)`);
if (run.passed  <   base.passed)     regressed.push(`통과 감소 ${base.passed}→${run.passed}`);
if (run.skipped  >   base.skipped)   regressed.push(`skip 증가 ${base.skipped}→${run.skipped} (약화 금지 위반)`);
if (regressed.length  >   0) {
  //  exhaust: PR 생성으로 넘어가지 않는다. `--skip-tests` 로 사후 우회 금지
  throw new Error(`⛔ BLOCKED( ' test_autofix_exhausted ' ) — ${regressed.join( "   ·  " )}`);
}

prBodyExtras.testResult = { passed: run.passed, skipped: run.skipped, total: run.total, autoFixRounds: attempt };

루프 계약 L-bf6 (bugfix.step6-test-autofix) — 필드 규약은 ../rules/orchestration-graph.md §2, 값은 여기가 자기 자리다.

inv:      auto-fix 는 테스트를 약화시키지 않는다(L-bf4.5 불변식 그대로) · 커밋은 원자적
prog:     residual = run.failed, 매 iteration 강한 감소. 로그에 before→after 를 반드시 적는다
          no-prog: residual 이 줄지 않으면 즉시 중단(break)Rung 2(`/cc-dev:unstuck`)
term:     run.failed === 0  & &   run.passed  > = baseline.passed  & &   run.skipped  < = baseline.skipped
budget:   3 iterations (ASCII 다이어그램의  " up to 3 times "**유일한** 실행 위치)
exhaust:  blockIssue(bugInfo,  ' test_autofix_exhausted ' ) (보드 + 점유 해제)PR 생성 차단.
          `--skip-tests` 로 사후 우회 금지 (그 옵션은 Step 5 진입 전에만 유효하다)
resume:   테스트 재실행 실측 + `prBodyExtras.testBaseline`(Step 4.5 기록) 재조회
log:       " bf6 #2/3: failed 4→1 · passed 60 (baseline 56) "   + 탈락시킨 실패 테스트 목록

Step 7: PR Creation#

// ⛔ PR 생성 전 위임 게이트 2종 — 절차를 복제하지 않고 run.md 를 그대로 호출한다
//    (무엇을 위임하고 무엇을 면제하는지는 Step 8 의  " Waived Gates "   표가 정본)
// 1) commands/run.md → Step 8.5 `runPrePushGate()` — dart analyze 0건 · DCM error 0건.
//    `LEFTHOOK=0`·`// ignore:` 로 0건을 만드는 우회 금지. 실패 시 PR 생성 차단.
await runPrePushGate();

// 2) commands/run.md → Step 9.0 중복 착수 재확인 — `--state all` 이 핵심(머지된 경쟁 PR 은
//    `--state open` 으로 보이지 않는다). 조회 실패는 통과가 아니다 (SoT §3).
const rivalsRaw = (await Bash(
  `gh pr list --state all --search  " ${bugInfo.number} "   --json number,state,mergedAt || true`
)).trim();
if (!rivalsRaw) throw new Error(`⛔ #${bugInfo.number} 경쟁 PR 조회 실패 — 판정 불가는 통과가 아니다`);
const alreadyMerged = JSON.parse(rivalsRaw).filter(p = >   p.mergedAt);
if (alreadyMerged.length  >   0) {
  throw new Error(`⛔ 이미 머지된 PR 존재(#${alreadyMerged.map(p = >   p.number).join( " , # " )}) — 중복 작업 확인 후 재개`);
}

// 3) ⛔ 진단 계측 잔여 게이트 — Step 4.2 가 이미 봤지만, 그 뒤 커밋에서 다시 들어올 수 있다.
//    이 게이트는 `--skip-tests`·`--skip-device` 로도 면제되지 않는다(스킵했으면 계측 자체가 없어야 한다).
//    명령은 Step 4.2 의 `RESIDUE_CMD` 와 같은 것을 쓴다(두 곳에서 다른 범위를 보지 않는다).
const residueAtPr = (await Bash(RESIDUE_CMD)).trim();
if (residueAtPr) throw new Error(`⛔ 진단 계측이 PR 에 실릴 뻔했다 — 회수 후 재시도:\n${residueAtPr}`);

// Create PR
const prTitle = `fix(${scope}): 🐛 ${bugInfo.title}`;
const prBody = `
## Summary
- ${bugInfo.title} bug fix

## Changes
- ${changesSummary}

## Test Plan
- [ ] Reproduction steps verified
- [ ] Tests passed

## Runtime Diagnosis (Step 3.5 / 4.2)
${(() = >   {
  const d = prBodyExtras.runtimeDiagnosis;
  if (!d || d.status ===  " waived " )
    return `- ⚠️ 런타임 재현 **미실행** (${d?.reason ??  " no device " }) — 아래 원인은 정적 분석 기반 **추정**이며 기기에서 확인되지 않았습니다`;
  return [
    `- Device: ${d.device} (tier: ${d.deviceTier})`,
    `- Repro before fix: ${d.reproduced === null ?  " 판정 불가 "   : d.reproduced ?  " 재현됨 "   :  " 재현 안 됨 " } · 진단 라운드 ${d.rounds}/3`,
    `- Root cause: ${d.status ===  " confirmed "   ? `\`${d.rootCause.file}:${d.rootCause.line}\` — ${d.rootCause.evidence}` :  " **미확정(inconclusive)** — 추정 원인으로 수정함 " }`,
    `- Fix verified on device: ${d.fixVerified ===  " verified "   ?  " ✅ 재현되지 않음 "   :  " ⚠️ 미검증 — 리뷰어 확인 필요 " }`,
    `- Instrumentation: ${d.instrumentation.length}곳 심고 전량 회수 (잔여 0건 게이트 통과)${(d.promotedLogs ?? []).length ? ` · 영구 로깅 승격 ${d.promotedLogs.length}건` :  " " }`,
    d.limits ? `- ⚠️ Limits: ${d.limits}` : null,
    `- Evidence: \`${d.artifacts}\` (로그·스크린샷, 커밋하지 않음)`,
  ].filter(Boolean).join( " \n " );
})()}

## Verification (hotfix 경로 보상 기록)
- Baseline (Step 4.5): ${prBodyExtras.testBaseline ? `${prBodyExtras.testBaseline.passed}/${prBodyExtras.testBaseline.total} passed, skipped ${prBodyExtras.testBaseline.skipped}` :  " 미검증 (tests waived — 회귀 없음을 주장하지 않는다) " }
- After fix (Step 6): ${prBodyExtras.testResult ? `${prBodyExtras.testResult.passed}/${prBodyExtras.testResult.total} passed, auto-fix ${prBodyExtras.testResult.autoFixRounds}/3 rounds` :  " 미검증 (tests waived) " }
- Blast radius deferred: ${(prBodyExtras.blastRadiusDeferred ?? []).join( " ,  " ) ||  " none " }
- Waived gates: ${(prBodyExtras.waivedGates ?? []).join( " ,  " ) ||  " none " }

Closes #${bugInfo.number}

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

// PR 번호를 붙잡아 둔다 — Step 8 의 CI 게이트가 이 번호로 checks 를 조회한다
const prUrl = (await Bash(`gh pr create --title  " ${prTitle} "   --body  " ${prBody} " `)).trim();
const prNumber = Number(prUrl.match(/\/pull\/(\d+)/)?.[1]);
if (!prNumber) throw new Error(`⛔ PR 번호 파싱 실패 — CI 게이트를 조회할 대상이 없다: ${prUrl}`);

// Pipeline move: Review
await mcp__zenhub__moveIssueToPipeline({
  issueId: bugInfo.id,
  pipelineId: reviewPipelineId
});

Step 8: Merge (merge = Close)#

// 0. ⛔ 열린 자식 게이트 (Parent Closure Invariant — rules/zenhub-conventions.md)
//    Bug/Task 도 Sub-task 를 가질 수 있는 컨테이너 타입이므로(BRANCH_BEARING_PARENT_TYPES),
//    이 경로에도 같은 불변식이 적용된다. 자식 없는 일반 버그에서는 gh api 1회 no-op.
const bugKids = await openChildrenStatus(bugInfo.number);
if (!mayClose(bugKids, bugInfo.issueType)) {
  throw new Error(
    `⛔ #${bugInfo.number} 머지 차단 — ${bugKids.status ===  " unknown " 
       ?  " 자식 이슈 조회 실패(판정 불가) " 
       : `열린 자식 ${bugKids.open.length}: ${bugKids.open.map(c = >   " # "   + c.number).join( " ,  " )}`}`
  );
}

// 1. ⛔ CI 판정 하드 게이트 (tri-state — 통과는 `pass` 하나뿐)
//    ../rules/orchestration-graph.md §3: undetermined 의 기본값은 fail.  " 검사 0건 " 과  " pending "   은
//    통과가 아니다(§3.2 nothing-to-check pass). exit code 를 grep 파이프로 읽지 않고 `--json` 을
//    파싱한다(§3.2 pipe-masked exit code). 이 게이트는 `--skip-tests` 로도 면제되지 않으며,
//    판정 규칙은 commands/run.md → Step 10.5 의 CI 하드 게이트와 동일하다.
const checksRaw = (await Bash(`gh pr checks ${prNumber} --json name,state,bucket || true`)).trim();
const checks = checksRaw ? JSON.parse(checksRaw) : null;
const ciVerdict =
  checks === null || checks.length === 0                          ?  " undetermined "    // 조회 실패·검사 0건
  : checks.some(c = >   c.bucket ===  " fail "   || c.bucket ===  " cancel " ) ?  " fail " 
   : checks.some(c = >   c.bucket ===  " pending " )                       ?  " undetermined "    // 아직 미확정
  :  " pass " ;
if (ciVerdict !==  " pass " ) {
  throw new Error(
    `⛔ #${bugInfo.number} 머지 차단 — CI 판정 ${ciVerdict}` +
    (checks?.length ? ` (실패/미확정: ${checks.filter(c = >   c.bucket !==  " pass " ).map(c = >   c.name).join( " ,  " )})` :  "   (검사 0건) " )
  );
}

// Squash merge → issue auto-Close via GitHub  " Closes # "   keyword → ZenHub syncs to Closed
// Never move to Done (Done ≠ closed)
await Bash(`gh pr merge --squash --delete-branch`);

// ⭐ Closure verification + fallback (ZenHub two-state model)
const ghState = (await Bash(`gh issue view ${bugInfo.number} --json state -q .state`)).trim();
if (ghState !==  " CLOSED " ) {
  await Bash(`gh issue close ${bugInfo.number} --reason completed`);
}
const closed = await mcp__zenhub__searchClosedIssues({ query: `#${bugInfo.number}` });
if (!closed.find(i = >   i.number === bugInfo.number)) {
  await mcp__zenhub__updateIssue({ issueId: bugInfo.id, state:  " CLOSED "   });
}
await releaseClaim(bugInfo,  " closed:merged " );   // 닫힌 이슈에 점유가 남으면 후속 작업이 자기 자신에 막힌다

// ⭐ Post-merge: base 브랜치로 이동 + 최신화 + 서브모듈 동기화 (필수)
//   gh pr merge --delete-branch 는 base를 pull 하지 않고, git pull 은 서브모듈 포인터만
//   갱신할 뿐 워킹트리는 체크아웃하지 않는다. git submodule update 로 base가 가리키는
//   커밋에 정렬한다 (Serverpod/Melos 모노레포 빌드/코드젠 drift 방지). 서브모듈 없으면 no-op.
await Bash(`
  git checkout development
  git pull --ff-only origin development
  git submodule sync --recursive
  git submodule update --init --recursive
`);

Waived Gates — /cc-dev:run 대비 이 경로가 무엇을 빼는지 ⚠️

이 경로는 /cc-dev:run 의 13단계보다 게이트가 적다. /cc-dev:go 가 버그를 여기로 보낼 수 있으므로 차이를 문서 밖에 두지 않는다. 아래 표가 그 차이의 정본이며, 세 가지 규칙이 붙는다.

  1. 표에 없는 게이트를 즉석에서 면제할 수 없다. 새로 빼려면 이 표에 줄을 먼저 추가한다.
  2. 면제는 PR body 에 남는 것만 유효하다 — 콘솔 경고는 내구 기록이 아니다 (../rules/orchestration-graph.md §7 규칙 6). 면제한 항목은 prBodyExtras.waivedGates 에 넣어 Step 7 의 ## Verification 로 실린다.
  3. 면제 불가 3종: CI 판정(ciVerdict === "pass") · mayClose(openChildrenStatus) · 진단 계측 잔여 0건(BUGFIX-DEBUG). --skip-tests·--skip-device·--no-merge 를 포함한 어떤 옵션도 이 셋을 덮지 못한다.
  4. 이 경로 고유 게이트의 면제는 그 절이 정본이다. run.md 에 없는 게이트(Step 3.5 런타임 진단)는 아래 표가 아니라 Step 3.5 절이 면제 조건을 정한다 — --skip-device 또는 기기 부재일 때만이고, 그때도 waivedGates: runtime-repro + PR body ## Runtime Diagnosis 에 "추정"으로 남는다.
run.md 게이트이 경로의 처리보상 기록 (PR body)
Step 8 pr:preflight (unit/widget/integ)면제 가능--skip-tests 는 Step 4.5–6 을 함께 스킵tests + 기준선 "미검증" 명기 (null 을 통과로 읽지 않는다)
Step 8.3 BDD coverage 100% + assertions면제 — 버그 수정은 .feature 를 요구하지 않는다bdd-coverage + Step 5 회귀 테스트 파일 목록
Step 8.5 pre-push (analyze/DCM 0건)위임 — Step 7 에서 runPrePushGate() 호출면제 아님. 실패 시 PR 생성 차단
Step 8.7 code review gate (8범주 자동 리뷰)면제 — Step 8 "Wait for review" 의 사람 리뷰로 대체auto-code-review + 리뷰어
Step 9.0 중복 착수 재확인 (--state all)위임 — Step 7 에서 실행면제 아님. 기머지 PR 발견 시 차단
Step 9 pre-PR 열린 자식 0건동일 — Step 8 gate 0 mayClose(openChildrenStatus)면제 불가. unknown 도 차단
Step 10.5 CI 대기 ∥ 작업내역 아티팩트분리 — CI 판정은 Step 8 gate 1 의 하드 게이트, 아티팩트 발행만 면제work-artifact + PR body Changes 요약
Step 7.3 design verification (Figma↔runtime)해당 없음 — 화면 기능이 아닌 버그 수정. 단 기기 재현은 Step 3.5 가 대신 수행한다(디자인 대조가 아니라 동작 재현)화면 버그면 /cc-dev:run 으로 보낸다
Step 12 머지 승인 ASK면제 — 리뷰 승인이 그 자리를 대신한다merge-approval (--no-merge 로 사람 머지 선택 가능)

위임(delegate)한 두 줄은 절차를 복제하지 않는다commands/run.md 의 해당 Step 을 호출할 뿐이다. 판정 규칙이 바뀌면 run.md 한 곳만 바뀐다.


Output Format#

On Success#

╔════════════════════════════════════════════════════════════════╗
║  Bug Cycle Complete: #123                                      ║
╠════════════════════════════════════════════════════════════════╣
║                                                                ║
║  🐛 Bug: fix: 로그인 소셜 로그인 버튼 미동작                    ║
║  🌿 Branch: fix/123-social-login-button                        ║
║                                                                ║
║  📝 Commits:                                                   ║
║    1. fix(auth): 🐛 소셜 로그인 버튼 클릭 핸들러 수정           ║
║    2. test(auth): ✅ 소셜 로그인 버튼 테스트 추가               ║
║                                                                ║
║  🔬 Runtime (Step 3.5/4.2):                                    ║
║    - Device: iPhone 15 Pro (physical) · repro=yes              ║
║    - Root cause: book_list_bloc.dart:142 (filter 미전달)       ║
║    - Fix verified on device: ✅ 재현되지 않음                  ║
║    - Instrumentation: 3곳 심고 전량 회수 (잔여 0)            ║
║                                                                ║
║  ✅ Tests:                                                     ║
║    - Baseline (4.5): 30/30 passed, skipped 0                   ║
║    - FE State: 16/16 passed                                    ║
║    - FE BLoC: 10/10 passed                                     ║
║    - FE UseCase: 10/10 passed                                  ║
║    - BE Integration: 10/10 passed                              ║
║    - Auto-fix rounds: 1/3 · passed 3076 (no skip added)     ║
║                                                                ║
║  🔒 Gates: children=none · CI=pass · waived: work-artifact     ║
║                                                                ║
║  🔀 PR: #456                                                   ║
║    - URL: https://github.com/coco-de/kobic/pull/456            ║- Status: Merged ✅                                         ║
║                                                                ║
║  📊 Duration: 8m 15s                                           ║
║  🏁 Final State: CLOSED (by merge)                             ║
║                                                                ║
╚════════════════════════════════════════════════════════════════╝

On Failure#

╔════════════════════════════════════════════════════════════════╗
║  Bug Cycle Failed: #123                                        ║
╠════════════════════════════════════════════════════════════════╣
║                                                                ║
║  ❌ Failed at: Step 4 (Bug fix work)                             ║
║                                                                ║
║  Error Details:                                                ║
║    - Cause identification failed: additional info needed       ║
║    - Related code not found                                    ║
║                                                                ║
║  Current State:                                                ║
║    - Branch: fix/123-social-login (exists)                     ║
║    - Commits: 0                                                ║
║    - Pipeline: In Progress                                     ║
║    - PR: Not created                                           ║
║                                                                ║
║  Recovery Options:                                             ║
║    1. Request additional info on the issue                     ║
║    2. Manually analyze cause then retry                        ║
║    3. /cc-dev:bugfix 123 --skip-tests                     ║
║                                                                ║
╚════════════════════════════════════════════════════════════════╝

TodoWrite Integration#

Progress Tracking#

TodoWrite([
  { content:  " Bug report creation " , status:  " completed " , activeForm:  " Bug report creation complete "   },
  { content:  " Issue info query " , status:  " in_progress " , activeForm:  " Querying issue info "   },
  { content:  " Branch creation " , status:  " pending " , activeForm:  " Branch creation pending "   },
  { content:  " Runtime repro  &   diagnosis on device (Step 3.5) " , status:  " pending " , activeForm:  " Runtime diagnosis pending "   },
  { content:  " Bug fix work " , status:  " pending " , activeForm:  " Bug fix pending "   },
  { content:  " Runtime fix verification + 계측 회수 (Step 4.2) " , status:  " pending " , activeForm:  " Runtime verification pending "   },
  { content:  " Existing test baseline (Step 4.5) " , status:  " pending " , activeForm:  " Baseline pending "   },
  { content:  " Unit test writing (FE+BE) " , status:  " pending " , activeForm:  " Test writing pending "   },
  { content:  " Test execution and verification " , status:  " pending " , activeForm:  " Test execution pending "   },
  { content:  " PR creation (+ runPrePushGate, dup recheck) " , status:  " pending " , activeForm:  " PR creation pending "   },
  { content:  " Merge and close (CI verdict gate) " , status:  " pending " , activeForm:  " Merge pending "   },
]);

Error Handling#

Step-by-Step Recovery Strategy#

Failed StepStateRecovery Method
Blast radiusNo changesRetry or switch to /plan/build
Bug reportNo changesRetry /cc-quality:bug-report
Branch creationIssue existsRetry
Runtime diagnosis (Step 3.5)Branch exists, 계측이 워킹트리에 있음기기 연결 후 같은 명령 재실행(멱등). 예산(3) 소진은 차단이 아니라 inconclusive 기록 — 단 "원인 확정"으로 승격 금지. --device 지정 기기가 없으면 다른 기기로 대체하지 말고 목록을 확인한다
Bug fixBranch existsManual fix then retry
Runtime verification (Step 4.2)수정 커밋 있음, 계측 잔존 가능재현 잔존이면 Step 3.5 복귀 1회 → 2회차는 BLOCKED('runtime_fix_unverified'). 계측 잔여는 git grep BUGFIX-DEBUG 로 위치를 찾아 전량 회수 후 재시도. 중간에 throw 하면 앱 프로세스가 남는다 — 재개 전 kill %1 또는 pkill -f "flutter run" 로 정리한다(재개는 멱등)
Baseline (Step 4.5)Code modified, 기준선 미확정수집 0건이면 코드젠/의존성 복구 후 재실행. 예산(2) 소진은 BLOCKED('baseline_repair_exhausted') — 기존 테스트를 약화시켜 통과시키지 않는다
Test writingCode modifiedFix fixtures, regenerate mocks, build transitive deps
Test executionTests written수동 테스트 수정 후 재실행 (예산 3 소진 = BLOCKED('test_autofix_exhausted')). 사후 --skip-tests 금지 — 그 옵션은 Step 5 진입 전에만 유효하다
PR creationTests passedrunPrePushGate() 재실행 후 gh pr create 수동 실행
MergePR existsCI 판정이 pass 가 될 때까지 대기 후 재시도. pending·검사 0건은 통과가 아니다

Key Rules#

  1. Verify Bug Issue Type: Only process Bug type issues
  2. Branch Naming: Follow fix/{number}-{slug} format
  3. Commit Message: fix({scope}): 🐛 {description} format
  4. Tests Required (before PR): Frontend (State/BLoC/UseCase) + Backend (integration/unit) tests must be written and passed before PR creation
  5. Separate Test Commits: Separate fix commits from test commits for easier review
  6. Issue Link: Include Closes #{number} in PR
  7. State Tracking: Update Pipeline state at every step
  8. Baseline Invariant (Step 4.5 → Step 6): 기존 테스트는 고칠 수는 있어도 약화시킬 수 없다skip:/@Skip 신규 추가 금지, 테스트 삭제 금지, passed 감소 금지. Step 4.5 가 기록한 기준선은 Step 6 이 반드시 대조한다
  9. CI Verdict is a Hard Gate (Step 8): 머지 전 ciVerdict === "pass" 만 통과. fail·pending·검사 0건·조회 실패는 전부 차단이며 --skip-tests 로도 면제되지 않는다
  10. Waived Gates are Declared: /cc-dev:run 대비 면제하는 게이트는 "Waived Gates" 표에 적힌 것만이고, 면제 사실은 PR body 에 남는다 (콘솔 경고는 기록이 아니다)
  11. Device-First Diagnosis (Step 3.5): 기기가 연결돼 있으면 반드시 띄워서 재현한 뒤 고친다 — 우선순위는 실기기 > 시뮬레이터 > 없음. 기기 없음/--skip-device 는 무음 스킵이 아니라 확인 + 기록(waivedGates: runtime-repro)이며, 그때 원인은 추정으로만 적는다. --skip-tests 는 이 단계를 면제하지 않는다
  12. Instrumentation is Tagged and Returned (Step 3.5 → 4.2): 진단용 임시 로그는 BUGFIX-DEBUG(#N) 태그를 달고 관측만 한다(조건·분기·상태 변경 금지). 수정 후 전량 회수하며, 잔여 0건은 git grep 이 판정하는 면제 불가 게이트다 (일반 디버그 산출물 체크리스트의 정본은 agents/pr-readiness-agent.md)
  13. A Fix is Not a Fix Until It Stops Reproducing (Step 4.2): 같은 기기·같은 절차에서 재현되지 않아야 verified 다. 판정 불가(unverified)는 통과가 아니라 PR body 에 남는 미검증이고, 재현 잔존은 Step 3.5 복귀 1회 후 BLOCKED('runtime_fix_unverified')

  • /cc-quality:bug-report: Bug report creation
  • /cc-dev:run {number}: General issue cycle
  • /cc-dev:zenhub:manage: ZenHub pipeline management
  • /cc-inspector:log: Step 3.5 런타임 로그·에러 수집 (errors / search {keyword})
  • /cc-inspector:bloc, /cc-inspector:network, /cc-inspector:nav: 상태·통신·라우트 원인 좁히기
  • /cc-marionette:smoke: 재현 절차 진입 전 화면이 뜨는지 빠른 확인 (선택·비차단)
  • cc-flutter:mcp-tool-selection: Step 3.5 조작 도구(marionette / flutter-skill / inspector) 선택 기준 SoT
  • cc-inspector:flutter-inspector: 9종 하위 인스펙터 개요
  • issue-state-agent: Issue state management
  • implementation-agent: Code implementation
  • test-runner-agent: Test execution
  • pr-lifecycle-agent: PR management
  • pr-readiness-agent: 디버그 산출물·계측 잔여 체크리스트 정본 (Step 4.2 / Step 7 가 참조)