/cc-dev:zenhub:rerank — 전역 백로그 우선순위 재정렬 도우미#
| 항목 | 내용 |
|---|---|
| 실행 명령 | /cc-dev:zenhub:rerank |
| 분류 | 워크플로우 |
| 난이도 | ●●○ 보통 |
| MCP 서버 | zenhub |
한마디로#
여러 Epic에 흩어진 일감을 한 줄로 세워 "이게 1순위, 저게 2순위" 전체 순서를 매겨 주는 도구입니다. 서랍마다 따로 "내 책상 1순위"를 적어 둔 포스트잇들을, 사무실 전체 기준 하나의 우선순위 줄로 다시 세워 주는 일이라고 보면 됩니다.
누가·언제 쓰나요#
- 서로 다른 세션·다른 Epic에서 만든 이슈들이 각자 "P0"를 달고 있어, 진짜 무엇부터 할지 헷갈릴 때
- 스프린트 계획 전, 백로그 전체를 한 기준으로 줄 세우고 싶을 때
- PM/팀 리드가 팀별로 P0가 너무 많지 않은지(과부하) 한눈에 보고 싶을 때
무엇을 해주나요#
- 백로그 칸(Product Backlog / Sprint Backlog / Icebox)의 모든 이슈를 모아 하나의 전역 순위로 정렬합니다 (tier → impact → 의존성 순)
- 보드의 카드 위치를 그 순서에 가깝게 다시 배치합니다 (위에서 아래로 = 높은 우선순위)
- 팀별로 묶어 보여 주고, P0가 과도하게 몰린 팀에 깃발을 세워 줍니다
- 순위 근거를
.pipeline/ranking.md표(순위·이슈번호·tier·impact·근거)로 남깁니다
중요: ZenHub 라벨은 한번 달면 못 바꿉니다. 그래서 이 도구는 우선순위 라벨을 고치지 않습니다 — 재정렬은 "보드 칸 위치 + ranking.md 표"로만 표현합니다.
어떻게 쓰나요#
# 기본: 백로그 3종을 전역 순위로 재정렬
/cc-dev:zenhub:rerank
# 보드는 건드리지 않고 순위만 미리보기
/cc-dev:zenhub:rerank --dry-run
# impact 우선으로 정렬 (기본은 tier 우선, impact는 동점 처리)
/cc-dev:zenhub:rerank --by impact
-
--scope backlog|all—backlog(기본): Product/Sprint Backlog + Icebox 3종.all: 미배치 이슈까지 포함 --dry-run— 보드를 옮기지 않고 순위 결과만 출력-
--by impact|tier— 정렬 기준. 기본tier(p0>p1>p2) 후 impact로 동점 처리,impact면 impact 우선
안에서 무슨 일이 벌어지나요#
- 수집 — 워크스페이스에서 저장소·파이프라인을 조회하고, 백로그 3개 칸의 모든 이슈를 가져옵니다.
- 신호 읽기 — 각 이슈의 우선순위(p-라벨), impact 라벨, 추정치(estimate), 부모(Epic)를 모읍니다.
- 전역 정렬 — tier → impact → 의존성 순으로 한 줄 순위를 만듭니다.
- 보드 반영 — P0부터 차례로 카드를 다시 이동시켜 보드 위→아래 순서를 순위에 맞춥니다. (라벨은 안 바꿉니다.)
- 팀 뷰 — 팀별로 묶어 보여 주고, P0가 너무 많은 팀에 경고 표시를 답니다.
-
기록 —
.pipeline/ranking.md에 순위 표를 저장합니다. (--dry-run이면 화면 출력만.)
⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)
Triggers#
- When cross-Epic / cross-team issues each carry their own "P0" and no single global order exists
- Before sprint planning, to linearize the whole backlog under one ranking
- When a PM wants a team-grouped view and over-loaded-P0 flags
Context Trigger Pattern#
/cc-dev:zenhub:rerank [--scope backlog|all] [--dry-run] [--by impact|tier]Options / Actions#
| Option | Values | Default | Description |
|---|---|---|---|
--scope | backlog | all | backlog | backlog: Product Backlog + Sprint Backlog + Icebox. all: also pulls unassigned issues via searchLatestIssues |
--dry-run | flag | off | Compute + print ranking only; no moveIssueToPipeline, no file write |
--by | impact | tier | tier | Sort key. tier = p0>p1>p2 then impact tiebreak; impact = impact first |
--include-epics | flag | off | Also rank Epic/Project-level issues (default: ranked separately from Story-level, never mixed into the same list — see "Level Separation" below) |
Level Separation: these backlog pipelines can hold both Epic/Project (L2-3) and Story/Feature/Bug/Task (L4) issues at once (per
rules/zenhub-conventions.md's placement matrix). Ranking them on one shared list would pit a whole Epic against a single Story on the same priority axis, which isn't a meaningful comparison. By default this command ranks Story-level issues only (its stated purpose — "여러 Epic에 흩어진 일감을 한 줄로 세운다" — is about Story-level work); Epic/Project issues are collected but reported in a separate section, not interleaved into the Story ranking. Pass--include-epicsto rank them too, still in their own separate list.
Execution Flow#
1. Resolve repos + pipelines (fail-closed)#
const workspace = await mcp__zenhub__getWorkspacePipelinesAndRepositories();
const repoIds = workspace.githubRepositories.map(r = > r.id);
if (repoIds.length === 0) throw new Error( " 저장소 0개 — 라이브 워크스페이스 확인 필요 " );
// 3 backlog pipelines, every find() must be fail-closed (never an unguarded .id)
const want = [ " Product Backlog " , " Sprint Backlog " , " Icebox " ];
const backlogPipelines = want.map(name = > {
const p = workspace.pipelines.find(p = > p.name === name);
if (!p) throw new Error(`파이프라인 " ${name} " 없음. 라이브: ${workspace.pipelines.map(p = > p.name).join( " , " )}`);
return p;
});
// NOTE: there is NO " Done " / " Closed " column. Reranking never closes issues
// and never moves to a Done column — see " Issue Closure Policy " in rules/zenhub-conventions.md.2. Collect issues + signals#
let issues = [];
for (const p of backlogPipelines) {
const res = await mcp__zenhub__getIssuesInPipeline({ pipelineId: p.id, repositoryIds: repoIds });
issues.push(...res.issues.map(i = > ({ ...i, _pipeline: p.name })));
}
// --scope all: also pull unassigned/loose issues (no getIssue/getChildrenOfParent exist)
if (scope === " all " ) {
const loose = await mcp__zenhub__searchLatestIssues({ query: " no:pipeline " });
issues.push(...loose.issues.map(i = > ({ ...i, _pipeline: null })));
}
// Per-issue signals from labels/estimate/parent (labels are IMMUTABLE — read only)
const tierRank = { " p0 " : 0, " p1 " : 1, " p2 " : 2 }; // priority label → rank
const impactRank = { " impact-high " : 0, " impact-med " : 1, " impact-low " : 2 };
const sig = issues.map(i = > {
const labels = (i.labels || []).map(l = > l.name);
return {
id: i.id, number: i.number, title: i.title, pipeline: i._pipeline,
level: i.issueType?.level ?? null, // hierarchy level (1=Initiative..5=Sub-task) — see Level Separation
tier: labels.find(n = > tierRank[n] !== undefined) ?? " p2 " ,
impact: labels.find(n = > impactRank[n] !== undefined) ?? " impact-low " ,
estimate: i.estimate?.value ?? null, // effort signal (setIssueEstimate)
parentId: i.parent?.id ?? null, // dependency / Epic grouping
};
});
// Level Separation (see Options table): split Story-level (L4) from Epic/Project-level (L2-3)
// BEFORE sorting, so the two are never compared on the same priority axis.
const storyLevelSig = sig.filter(s = > s.level == null || s.level > = 4);
const upperLevelSig = sig.filter(s = > s.level != null & & s.level < 4);3. Global sort (tier → impact → dependency)#
const by = opts.by ?? " tier " ;
const cmp = (a, b) = > {
const t = (x) = > tierRank[x.tier] ?? 9;
const m = (x) = > impactRank[x.impact] ?? 9;
if (by === " impact " ) return (m(a) - m(b)) || (t(a) - t(b)) || depCmp(a, b);
return (t(a) - t(b)) || (m(a) - m(b)) || depCmp(a, b); // default: tier first, impact tiebreak
};
// Level Separation: Story-level (L4) is THE ranking this command reports/moves by default.
// Epic/Project-level (L2-3) is sorted into its own separate list — never merged with storyLevelSig,
// so an Epic never competes against a single Story on the same priority axis.
storyLevelSig.sort(cmp);
if (opts.includeEpics) upperLevelSig.sort(cmp); // only when --include-epics is passed
// depCmp: issues whose parent appears earlier (blockers/Epics) rank ahead; stable otherwise.4. Reflect onto board (moveIssueToPipeline has NO position arg)#
// moveIssueToPipeline cannot set an index. To approximate top-to-bottom order,
// re-move issues in DESCENDING priority (P0 first) so the highest land on top.
// Labels are immutable, so the durable rerank signal = pipeline membership + ranking.md.
// Level Separation: only storyLevelSig moves by default — Epic/Project issues are left
// untouched unless --include-epics is passed, and even then move as their own pass
// (never interleaved with Story issues in the same sort/move sequence).
if (!opts.dryRun) {
const movable = opts.includeEpics ? [...storyLevelSig, ...upperLevelSig] : storyLevelSig;
for (const s of movable) { // each sub-list already sorted P0-first
const target = backlogPipelines.find(p = > p.name === s.pipeline);
if (!target) throw new Error(`타깃 파이프라인 " ${s.pipeline} " 미해석. 라이브: ${workspace.pipelines.map(p = > p.name).join( " , " )}`);
await mcp__zenhub__moveIssueToPipeline({ issueId: s.id, pipelineId: target.id });
}
}5. Team view + over-P0 flag#
const team = await mcp__zenhub__getTeamMembers(); // group ranked issues per assignee/team
// flag any team holding > N p0 issues (default N=3) as overloaded in the report.6. Persist ranking#
// Write .pipeline/ranking.md with TWO sections (never merged into one table):
// ## Story-level ranking — storyLevelSig, columns: rank | #issue | tier | impact | reason
// ## Epic/Project ranking — upperLevelSig, same columns (present only when --include-epics)
// --dry-run → print the same tables to stdout, write nothing.MCP Tool Usage#
// Repos + pipelines (source of repoIds and pipeline.id)
const workspace = await mcp__zenhub__getWorkspacePipelinesAndRepositories();
const repoIds = workspace.githubRepositories.map(r = > r.id);
// Backlog contents (repositoryIds REQUIRED)
await mcp__zenhub__getIssuesInPipeline({ pipelineId, repositoryIds: repoIds });
// Loose issues for --scope all (no getIssue / getChildrenOfParent exist)
await mcp__zenhub__searchLatestIssues({ query: " #1413 " }); // or " parent: < graphqlId > "
// ⚠️ GD-04: " parent: < graphqlId > " only scans the " latest 20 issues " — old/untouched
// children silently drop out. Fine for this loose-issue survey; never use it as a
// completeness check. See commands/run.md Step 0.6 for the reliable alternative.
// Reorder approximation (no position param)
await mcp__zenhub__moveIssueToPipeline({ issueId, pipelineId });
// Team grouping
await mcp__zenhub__getTeamMembers();Key Rules#
- Labels are immutable —
updateIssuecannot change priority/impact labels. Reranking is expressed ONLY via pipeline position +.pipeline/ranking.md, never by relabeling. moveIssueToPipelinehas no position arg — approximate top-to-bottom by re-moving in P0-first (descending priority) order.- Every
pipelines.find()is fail-closed — throw with the live pipeline list; never dereference an unguarded.id. getIssuesInPipelinerequiresrepositoryIds— always passworkspace.githubRepositories.map(r => r.id).- Respect the 2-state model — rerank never closes issues and never moves to a Done/Closed column (there is none). Closing remains
gh issue close+updateIssue(state:CLOSED)per the "Issue Closure Policy" inrules/zenhub-conventions.md. - Reference, don't redefine — effort/impact signals follow "Matrix Signals" (
setIssueEstimate+ immutableimpact-*label) and any sprint scoping follows "Sprint Selector Resolution" inrules/zenhub-conventions.md. --dry-run— compute and print the ranking only; perform nomoveIssueToPipelinecalls and no file writes.- Level Separation — Story-level (L4: Feature/Bug/Task) issues rank and move by default. Epic/Project-level (L2-3) issues are collected but sorted/reported in their own separate list, never merged into the Story comparison — an Epic and a single Story are not commensurable on the same priority axis. Epic/Project issues only move with
--include-epics, and even then as their own separate pass.