LogoSkills

zenhub-rerank

Epic 전체를 아우르는 전역 백로그 리랭크: 하나의 우선순위 랭킹을 만들어 보드를 재정렬합니다.

/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|allbacklog(기본): Product/Sprint Backlog + Icebox 3종. all: 미배치 이슈까지 포함
  • --dry-run — 보드를 옮기지 않고 순위 결과만 출력
  • --by impact|tier — 정렬 기준. 기본 tier(p0>p1>p2) 후 impact로 동점 처리, impact면 impact 우선

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

  1. 수집 — 워크스페이스에서 저장소·파이프라인을 조회하고, 백로그 3개 칸의 모든 이슈를 가져옵니다.
  2. 신호 읽기 — 각 이슈의 우선순위(p-라벨), impact 라벨, 추정치(estimate), 부모(Epic)를 모읍니다.
  3. 전역 정렬 — tier → impact → 의존성 순으로 한 줄 순위를 만듭니다.
  4. 보드 반영 — P0부터 차례로 카드를 다시 이동시켜 보드 위→아래 순서를 순위에 맞춥니다. (라벨은 안 바꿉니다.)
  5. 팀 뷰 — 팀별로 묶어 보여 주고, P0가 너무 많은 팀에 경고 표시를 답니다.
  6. 기록.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#

OptionValuesDefaultDescription
--scopebacklog | allbacklogbacklog: Product Backlog + Sprint Backlog + Icebox. all: also pulls unassigned issues via searchLatestIssues
--dry-runflagoffCompute + print ranking only; no moveIssueToPipeline, no file write
--byimpact | tiertierSort key. tier = p0>p1>p2 then impact tiebreak; impact = impact first
--include-epicsflagoffAlso 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-epics to 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#

  1. Labels are immutableupdateIssue cannot change priority/impact labels. Reranking is expressed ONLY via pipeline position + .pipeline/ranking.md, never by relabeling.
  2. moveIssueToPipeline has no position arg — approximate top-to-bottom by re-moving in P0-first (descending priority) order.
  3. Every pipelines.find() is fail-closed — throw with the live pipeline list; never dereference an unguarded .id.
  4. getIssuesInPipeline requires repositoryIds — always pass workspace.githubRepositories.map(r => r.id).
  5. 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" in rules/zenhub-conventions.md.
  6. Reference, don't redefine — effort/impact signals follow "Matrix Signals" (setIssueEstimate + immutable impact-* label) and any sprint scoping follows "Sprint Selector Resolution" in rules/zenhub-conventions.md.
  7. --dry-run — compute and print the ranking only; perform no moveIssueToPipeline calls and no file writes.
  8. 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.