LogoSkills

zenhub:triage

Turn external feedback (support/Slack/review/store) into deduped, well-formed ZenHub issues staged in New Issues

ํ•ญ๋ชฉ๋‚ด์šฉ
Invoke/zenhub:triage
Categorykobic-workflow
Complexitymoderate
MCP Serverszenhub

/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. ์ค‘๋ณต ๊ฒ€์‚ฌ โ€” ์—ด๋ฆฐ ์ด์Šˆ์™€ ๋‹ซํžŒ ์ด์Šˆ๋ฅผ ๋ชจ๋‘ ๋’ค์ ธ "๊ฐ™์€ ์˜๋ฏธ"์˜ ๊ธฐ์กด ์ด์Šˆ๊ฐ€ ์žˆ๋Š”์ง€ ๋ด…๋‹ˆ๋‹ค. ๋‹จ์ˆœ ๋‹จ์–ด ๊ฒน์นจ์ด ์•„๋‹ˆ๋ผ ๋‚ด์šฉ ์˜๋ฏธ๋กœ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.
  2. ์ด๋ฏธ ์žˆ์œผ๋ฉด โ€” ์ƒˆ๋กœ ๋งŒ๋“ค์ง€ ์•Š๊ณ  ๊ทธ ์ด์Šˆ์— ์ฝ”๋ฉ˜ํŠธ(๋˜๋Š” +1)๋ฅผ ๋‚จ๊ธฐ๊ณ , source:<์ฑ„๋„> ๋ผ๋ฒจ ์ถ”๊ฐ€๋ฅผ ์ œ์•ˆํ•œ ๋’ค ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค.
  3. ์ฒ˜์Œ ๋ณด๋Š” ๊ฑด์ด๋ฉด โ€” ์ด์Šˆ ์ข…๋ฅ˜๋ฅผ ์ •ํ•˜๊ณ , ๋ผ๋ฒจ(needs-triage, source:, impact-)์„ ๋ถ™์—ฌ ์ƒˆ ์ด์Šˆ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  4. ์ ‘์ˆ˜ํ•จ ๋Œ€๊ธฐ โ€” ๋งŒ๋“  ์ด์Šˆ๋ฅผ 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#

OptionDescriptionExample
--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|lowMatrix impact axis; persisted as impact-<level> label at creation--impact high
--dry-runRun 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#

  1. Dedup is semantic (LLM), not token overlap โ€” judge title+body MEANING; search BOTH searchLatestIssues (open) and searchClosedIssues (closed/shipped). On a match: comment / +1, never create a second issue.
  2. /bug-report must 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).
  3. Labels are immutable (updateIssue cannot change labels) โ†’ set source:<channel>, impact-<level>, needs-triage, and the type label at createGitHubIssue time. For a dedup match, you can only suggest adding the source: label manually.
  4. source:<channel> enables per-channel tracking โ€” always persist it so triage volume per origin is queryable.
  5. 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).
  6. Stage in New Issues only โ€” every pipelines.find is fail-closed (throw with live pipeline list). Never auto-route to Product Backlog; triage is a human gate.
  7. 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".
  8. --dry-run short-circuits after dedup + preview โ€” no createGitHubIssue, no moveIssueToPipeline.