| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| Invoke | /zenhub:rerank |
| Category | kobic-workflow |
| Complexity | moderate |
| MCP Servers | zenhub |
/zenhub:rerank โ ์ ์ญ ๋ฐฑ๋ก๊ทธ ์ฐ์ ์์ ์ฌ์ ๋ ฌ ๋์ฐ๋ฏธ#
ํ๋ง๋๋ก#
์ฌ๋ฌ 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์ข
์ ์ ์ญ ์์๋ก ์ฌ์ ๋ ฌ
/zenhub:rerank
# ๋ณด๋๋ ๊ฑด๋๋ฆฌ์ง ์๊ณ ์์๋ง ๋ฏธ๋ฆฌ๋ณด๊ธฐ
/zenhub:rerank --dry-run
# impact ์ฐ์ ์ผ๋ก ์ ๋ ฌ (๊ธฐ๋ณธ์ tier ์ฐ์ , impact๋ ๋์ ์ฒ๋ฆฌ)
/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#
/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 > "
// 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.