| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| Invoke | /zenhub:triage |
| Category | kobic-workflow |
| Complexity | moderate |
| MCP Servers | zenhub |
/zenhub:triage โ ์ธ๋ถ ํผ๋๋ฐฑ โ ์ ์ ๋ ์ด์ ๋ง๋ค๊ธฐ#
ํ๋ง๋๋ก#
์ง์ ๋ฌธ์ยทSlack ์ ๋ณดยท์ฑ์คํ ์ด ๋ฆฌ๋ทฐยท๋ฒ๊ทธ ์ ๊ณ ์ฒ๋ผ ์ฌ๊ธฐ์ ๊ธฐ์ ๋ค์ด์จ ์๊ฒฌ์, ๋ณด๋์ ๋ฐ๋ก ์ฌ๋ฆด ์ ์๊ฒ ๊น๋ํ ์ ๋ฆฌํ "์ ์ ์นด๋"๋ก ๋ง๋ค์ด ์ฃผ๋ ๋๊ตฌ์ ๋๋ค. ๋ฏผ์ ์ฐฝ๊ตฌ ์ง์์ด ๋ค์ด์จ ๋ฏผ์์ "์ด๊ฑฐ ์ ์ ๋ค์ด์จ ๊ทธ ๊ฑด์ด๋ค์" ํ๊ณ ๊ฐ์ ๊ฑด๋ผ๋ฆฌ ๋ฌถ๊ณ , ์ฒ์ ๋ณด๋ ๊ฑด๋ง ์ ์ ์์ฆ์ผ๋ก ์ฌ๋ ค ์ฃผ๋ ์ผ์ ๋์ ํด ์ค๋ค๊ณ ๋ณด๋ฉด ๋ฉ๋๋ค.
๋๊ฐยท์ธ์ ์ฐ๋์#
- ๊ณ ๊ฐ ์ง์/Slack/์ฑ์คํ ์ด ๋ฆฌ๋ทฐ์์ ๋ฐ์ ์๊ฒฌ์ ์ด์๋ก ๋จ๊ฒจ์ผ ํ ๋
- ๊ฐ์ ์ ๊ณ ๊ฐ ์ฌ๋ฌ ๋ฒ ๋ค์ด์ ์ค๋ณต ์ด์๊ฐ ์์ด๋ ๊ฑธ ๋ง๊ณ ์ถ์ ๋
- ์ด๋ ์ฑ๋์์ ๋ค์ด์จ ๊ฑด์ธ์ง(
source:) ์ถ์ ํ๋ฉด์ ๋ชจ์ ๋๊ณ ์ถ์ ๋
๋ฌด์์ ํด์ฃผ๋์#
- ์ค๋ณต ๋จผ์ ํ์ธ โ ์ด๋ฏธ ๊ฐ์ ๋ด์ฉ์ ์ด์(์ด๋ฆฐ ๊ฒยท๋ซํ ๊ฒ ๋ชจ๋)๊ฐ ์์ผ๋ฉด ์๋ก ๋ง๋ค์ง ์๊ณ ๊ทธ์ชฝ์ ์ฝ๋ฉํธ๋ก ํฉ์นฉ๋๋ค
- ๋ถ๋ฅ โ ๋ฒ๊ทธ/์์ฒญ/์์ ๋ฑ ์ด์ ์ข ๋ฅ๋ฅผ ๋ด์ฉ์ผ๋ก ์์์ ๊ณจ๋ผ ์ค๋๋ค
- ์ ์ ๋ ์ด์ ์์ฑ โ ์ฑ๋(
source:)ยท์ํฅ๋(impact-) ๋ผ๋ฒจ๊น์ง ๋ถ์ฌ ์ ์ ์นด๋๋ก ๋ง๋ญ๋๋ค -
์ ์ํจ์ ๋๊ธฐ โ ๋ง๋ ์ด์๋ฅผ
New Issues(์ ์/๋ถ๋ฅ ๋๊ธฐ ์นธ)์ ์ฌ๋ ค ๋ก๋๋ค. ๊ณง์ฅ ๋ฐฑ๋ก๊ทธ๋ก ๋ณด๋ด์ง ์์ต๋๋ค โ ์ฌ๋์ด ํ ๋ฒ ๋ณด๊ณ ๋ถ๋ฅํ๊ฒ์
์ด๋ป๊ฒ ์ฐ๋์#
# Slack์์ ๋ค์ด์จ ๋ฒ๊ทธ ์ ๋ณด๋ฅผ ์ด์๋ก
/zenhub:triage --source slack --type Bug --impact high " ๊ฒฐ์ ํ ์์์ฆ ํ๋ฉด์ด ์ ๋ ์ "
# ์ฑ์คํ ์ด ๋ฆฌ๋ทฐ๋ฅผ ์ ์ (์ข
๋ฅ๋ ๋ด์ฉ์ผ๋ก ์ถ๋ก )
/zenhub:triage --source store --impact med " ๋คํฌ๋ชจ๋์์ ๊ธ์จ๊ฐ ์ ๋ณด์ "
# ์ค์ ๋ก ๋ง๋ค์ง ์๊ณ ์ค๋ณต ๊ฒ์ฌ + ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ง
/zenhub:triage --source support --dry-run " ๋ก๊ทธ์ธ ํ ํฐ ๋ง๋ฃ ์ค๋ฅ "
-
--source <์ฑ๋>โ ์ด๋์ ๋ค์ด์จ ๊ฑด์ง (slack/support/review/store๋ฑ).source:<์ฑ๋>๋ผ๋ฒจ๋ก ์๊ตฌ ๊ธฐ๋ก๋ฉ๋๋ค --type <์ข ๋ฅ>โ ์ด์ ์ข ๋ฅ ์ง์ ์ง์ (์๋ตํ๋ฉด ๋ด์ฉ์ผ๋ก ์ถ๋ก )--impact high|med|lowโ ์ํฅ๋. ์์ฑ ์impact-<level>๋ผ๋ฒจ๋ก ํ์ (๋์ค์ ๋ณ๊ฒฝ ๋ถ๊ฐ)--dry-runโ ์ค๋ณต ๊ฒ์ฌ์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ง ํ๊ณ ์ค์ ์์ฑ/์ด๋์ ํ์ง ์์
์์์ ๋ฌด์จ ์ผ์ด ๋ฒ์ด์ง๋์#
- ์ค๋ณต ๊ฒ์ฌ โ ์ด๋ฆฐ ์ด์์ ๋ซํ ์ด์๋ฅผ ๋ชจ๋ ๋ค์ ธ "๊ฐ์ ์๋ฏธ"์ ๊ธฐ์กด ์ด์๊ฐ ์๋์ง ๋ด ๋๋ค. ๋จ์ ๋จ์ด ๊ฒน์นจ์ด ์๋๋ผ ๋ด์ฉ ์๋ฏธ๋ก ํ๋จํฉ๋๋ค.
-
์ด๋ฏธ ์์ผ๋ฉด โ ์๋ก ๋ง๋ค์ง ์๊ณ ๊ทธ ์ด์์ ์ฝ๋ฉํธ(๋๋ +1)๋ฅผ ๋จ๊ธฐ๊ณ ,
source:<์ฑ๋>๋ผ๋ฒจ ์ถ๊ฐ๋ฅผ ์ ์ํ ๋ค ์ข ๋ฃํฉ๋๋ค. -
์ฒ์ ๋ณด๋ ๊ฑด์ด๋ฉด โ ์ด์ ์ข
๋ฅ๋ฅผ ์ ํ๊ณ , ๋ผ๋ฒจ(
needs-triage,source:,impact-)์ ๋ถ์ฌ ์ ์ด์๋ฅผ ๋ง๋ญ๋๋ค. - ์ ์ํจ ๋๊ธฐ โ ๋ง๋ ์ด์๋ฅผ
New Issues์นธ์ ์ฌ๋ ค ์ฌ๋์ด ๋ถ๋ฅํ ์ ์๊ฒ ๋ก๋๋ค.
์ฐธ๊ณ : ๋ผ๋ฒจ์ ํ ๋ฒ ๋ง๋ค๋ฉด ๋ฐ๊ฟ ์ ์์ผ๋ฏ๋ก ์ฑ๋ยท์ํฅ๋ ๋ผ๋ฒจ์ ์์ฑ ์์ ์ ํ์ ํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋ถ๋ฅ ์ ์ด๋ฏ๋ก ์ ๋
Product Backlog๋ก ๋ฐ๋ก ๋ณด๋ด์ง ์์ต๋๋ค.
โ๏ธ ์์ธ ์ต์ ยท์คํ ๋ช ์ธ (๊ฐ๋ฐ์ / AI ์์ด์ ํธ์ฉ)
Triggers#
- External feedback (support ticket, Slack thread, app-store/store review, bug report) needs to become a tracked issue
- Duplicate inbound reports are piling up and need semantic dedup before creation
- Per-channel triage intake with persistent
source:attribution
Context Trigger Pattern#
/zenhub:triage [--source < channel > ] [--type < issueType > ] [--impact high|med|low] [--dry-run] " < feedback text > "Options / Actions#
| Option | Description | Example |
|---|---|---|
--source <channel> | Origin channel; persisted as source:<channel> label | --source slack |
--type <issueType> | Force ZenHub issue type (else inferred from content) | --type Bug |
--impact high|med|low | Matrix impact axis; persisted as impact-<level> label at creation | --impact high |
--dry-run | Run dedup + preview only; no create / no move | --dry-run |
Execution Flow#
1. DEDUP (semantic, LLM โ not token overlap)#
// Search BOTH open and closed (a fix may already have shipped & closed).
const open = await mcp__zenhub__searchLatestIssues({ query: feedbackText });
const closed = await mcp__zenhub__searchClosedIssues({ query: feedbackText });
// LLM-judge title+body MEANING against the inbound feedback โ do NOT match on
// shared keywords. A " receipt screen blank after payment " report duplicates an
// existing " post-checkout receipt not rendering " issue even with zero word overlap.
const match = semanticDuplicate(feedbackText, [...open, ...closed]); // returns issue | null
if (match) {
// Do NOT create. Comment / +1 on the existing issue and propose the source label.
await mcp__zenhub__updateIssue({ issueId: match.id, body: appendDedupNote(match.body, source) });
// Labels are IMMUTABLE post-creation โ cannot add source: < channel > via updateIssue.
// So: report the match to the user and SUGGEST adding `source: < channel > ` manually.
return report(`์ค๋ณต โ #${match.number} ์ ํฉ์นจ. ๋ผ๋ฒจ ' source:${source} ' ์๋ ์ถ๊ฐ ์ ์ (๋ผ๋ฒจ ๋ถ๋ณ).`);
}2. CLASSIFY (resolve issue type)#
const workspace = await mcp__zenhub__getWorkspacePipelinesAndRepositories();
const repoId = workspace.githubRepositories[0].id; // or find by name
const issueTypes = await mcp__zenhub__getIssueTypes({ repositoryId: repoId });
// --type wins; else infer from content (bug symptom โ " Bug " , request โ " Feature " , chore โ " Task " ).
const typeName = options.type ?? inferType(feedbackText);
const issueType = issueTypes.find(t = > t.name === typeName);
if (!issueType) throw new Error(`์ด์ ํ์
' ${typeName} ' ์์. ๋ผ์ด๋ธ: ${issueTypes.map(t = > t.name).join( " , " )}`);3. CREATE (labels locked at creation โ immutable afterward)#
const typeLabel = typeName.toLowerCase(); // bug / feature / task
const impactLabel = `impact-${options.impact ?? " med " }`; // Matrix impact axis (see Matrix Signals)
if (options.dryRun) return preview({ title, typeName, labels });
const issue = await mcp__zenhub__createGitHubIssue({
title,
body,
repositoryId: repoId,
issueTypeId: issueType.id,
// โ ๏ธ labels are immutable post-creation (updateIssue can ' t change them) โ set ALL here.
labels: [ " needs-triage " , `source:${options.source}`, impactLabel, typeLabel],
});4. STAGE (New Issues โ triage holding column, NOT Product Backlog)#
const newIssues = workspace.pipelines.find(p = > p.name === " New Issues " );
// fail-closed: never leave an unguarded .id
if (!newIssues) throw new Error(` ' New Issues ' ํ์ดํ๋ผ์ธ ์์. ๋ผ์ด๋ธ: ${workspace.pipelines.map(p = > p.name).join( " , " )}`);
await mcp__zenhub__moveIssueToPipeline({ issueId: issue.id, pipelineId: newIssues.id });
// Do NOT route to Product Backlog โ triage is a human gate; staging waits in New Issues.MCP Tool Usage#
// Workspace / repo / type IDs (per zenhub-conventions " Workspace Info Query " )
const workspace = await mcp__zenhub__getWorkspacePipelinesAndRepositories();
const repoIds = workspace.githubRepositories.map(r = > r.id);
const issueTypes = await mcp__zenhub__getIssueTypes({ repositoryId: repoIds[0] });
// Dedup corpus (no getIssue/getChildrenOfParent exist โ search only)
await mcp__zenhub__searchLatestIssues({ query: " #1413 " }); // open; also " parent: < graphqlId > "
await mcp__zenhub__searchClosedIssues({ query: feedbackText }); // already-shipped fixes
// Optional: scan the existing New Issues column to spot near-dupes already staged
const newIssues = workspace.pipelines.find(p = > p.name === " New Issues " );
if (!newIssues) throw new Error(` ' New Issues ' ํ์ดํ๋ผ์ธ ์์. ๋ผ์ด๋ธ: ${workspace.pipelines.map(p = > p.name).join( " , " )}`);
await mcp__zenhub__getIssuesInPipeline({ pipelineId: newIssues.id, repositoryIds: repoIds });Key Rules#
- Dedup is semantic (LLM), not token overlap โ judge title+body MEANING; search BOTH
searchLatestIssues(open) andsearchClosedIssues(closed/shipped). On a match: comment / +1, never create a second issue. /bug-reportmust pre-check this dedup before creating โ any bug-intake path runs the step-1 dedup gate first so support/Slack/store reports never bypass it (cross-link:cc-code-quality:bug-report).- Labels are immutable (
updateIssuecannot change labels) โ setsource:<channel>,impact-<level>,needs-triage, and the type label atcreateGitHubIssuetime. For a dedup match, you can only suggest adding thesource:label manually. source:<channel>enables per-channel tracking โ always persist it so triage volume per origin is queryable.impact-<level>is the Matrix impact axis, not effort โ see rules/zenhub-conventions.md "Matrix Signals" (impact = immutable label; effort =setIssueEstimate, set later during refinement, not at intake).- Stage in
New Issuesonly โ everypipelines.findis fail-closed (throwwith live pipeline list). Never auto-route toProduct Backlog; triage is a human gate. - 2-state model โ closing a duplicate-resolved-elsewhere issue is
gh issue close+updateIssue({ state: "CLOSED" }), never a "Done"/"Closed" column move (there is no such column; "Closed" is GitHub state). See "Issue Closure Policy". --dry-runshort-circuits after dedup + preview โ nocreateGitHubIssue, nomoveIssueToPipeline.