LogoSkills

issue-state-agent

This agent manages ZenHub issue states.

ํ•ญ๋ชฉ๋‚ด์šฉ
Modelhaiku

/dev:issue-state โ€” ZenHub ์ด์Šˆ ์ƒํƒœ ๊ด€๋ฆฌ ๋„์šฐ๋ฏธ#

ZenHub issue state management agent

ํ•œ๋งˆ๋””๋กœ#

์ž‘์—… ์นด๋“œ(์ด์Šˆ)๋ฅผ ๋ณด๋“œ ์œ„์—์„œ ์•Œ๋งž์€ ์นธ์œผ๋กœ ์˜ฎ๊ธฐ๊ณ , ์ผ์ด ๋๋‚˜๋ฉด ํ™•์‹คํ•˜๊ฒŒ "์™„๋ฃŒ" ์ฒ˜๋ฆฌ๊นŒ์ง€ ๋งˆ๋ฌด๋ฆฌํ•ด ์ฃผ๋Š” ๋„์šฐ๋ฏธ์ž…๋‹ˆ๋‹ค. ์น ํŒ์— ๋ถ™์ธ ํฌ์ŠคํŠธ์ž‡์„ "์ง„ํ–‰ ์ค‘ โ†’ ๊ฒ€ํ†  โ†’ ์™„๋ฃŒ" ์นธ์œผ๋กœ ์˜ฎ๊ธฐ๊ณ , ๋‹ค ๋๋‚œ ํฌ์ŠคํŠธ์ž‡์€ ๋–ผ์–ด๋‚ด ๋ณด๊ด€ํ•จ์— ๋„ฃ๋Š” ์ผ์„ ๋Œ€์‹ ํ•ด ์ค€๋‹ค๊ณ  ๋ณด๋ฉด ๋ฉ๋‹ˆ๋‹ค.

๋ˆ„๊ฐ€ยท์–ธ์ œ ์“ฐ๋‚˜์š”#

  • ๊ฐœ๋ฐœ ์ž๋™ํ™” ํ๋ฆ„(๋ธŒ๋žœ์น˜ ์ƒ์„ฑ โ†’ PR ์ƒ์„ฑ โ†’ ๋ณ‘ํ•ฉ) ์•ˆ์—์„œ ์ด์Šˆ์˜ ํ˜„์žฌ ์œ„์น˜๋ฅผ ์ž๋™์œผ๋กœ ์˜ฎ๊ฒจ์•ผ ํ•  ๋•Œ
  • ๋ธŒ๋žœ์น˜๋ฅผ ๋งŒ๋“  ์งํ›„(โ†’ "์ง„ํ–‰ ์ค‘"), PR์„ ์˜ฌ๋ฆฐ ์งํ›„(โ†’ "๊ฒ€ํ† "), PR์„ ๋ณ‘ํ•ฉํ•œ ์งํ›„(โ†’ "์™„๋ฃŒ/๋‹ซํž˜") ๊ฐ™์€ ๋ถ„๊ธฐ๋งˆ๋‹ค ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค
  • ์ด์Šˆ๊ฐ€ ์ง€๊ธˆ ์–ด๋А ๋‹จ๊ณ„์— ์žˆ๋Š”์ง€, ๋ˆ„๊ฐ€ ๋ถ€๋ชจ-์ž์‹์œผ๋กœ ๋ฌถ์—ฌ ์žˆ๋Š”์ง€ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๊ณ  ์‹ถ์„ ๋•Œ

๋ฌด์—‡์„ ํ•ด์ฃผ๋‚˜์š”#

  • ๋ณด๋“œ ์นธ ์ด๋™(Pipeline) โ€” ์ด์Šˆ๋ฅผ In Progress, Review, Closed ๋“ฑ ์•Œ๋งž์€ ์นธ์œผ๋กœ ์˜ฎ๊น๋‹ˆ๋‹ค
  • ์ด์Šˆ ๋‹ซ๊ธฐ โ€” PR์ด ๋ณ‘ํ•ฉ๋˜๋ฉด ์ด์Šˆ๋ฅผ ์ž๋™์œผ๋กœ ๋‹ซ์Šต๋‹ˆ๋‹ค
  • ํ™•์‹คํ•œ ๋งˆ๋ฌด๋ฆฌ ๊ฒ€์ฆ โ€” ๋ณ‘ํ•ฉ ํ›„ GitHub๊ณผ ZenHub ์–‘์ชฝ ๋ชจ๋‘์—์„œ ์ •๋ง Closed๊ฐ€ ๋๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ์•ˆ ๋์œผ๋ฉด ์ง์ ‘ ๋‹ซ์•„ ๋ณด์ •ํ•ฉ๋‹ˆ๋‹ค (์ด์Šˆ๊ฐ€ "๋‹ซํžŒ ์ค„ ์•Œ์•˜๋Š”๋ฐ ์—ด๋ ค ์žˆ๋Š”" ์‚ฌ๊ณ ๋ฅผ ๋ง‰์•„ ์ค๋‹ˆ๋‹ค)
  • ์ƒํƒœ ์กฐํšŒ โ€” ์ด์Šˆ์˜ ํ˜„์žฌ ์ƒํƒœ์™€ ๋ณด๋“œ ์œ„์น˜๋ฅผ ์•Œ๋ ค ์ค๋‹ˆ๋‹ค
  • ๋ถ€๋ชจ ์ด์Šˆ ์ž๋™ ์ •๋ฆฌ โ€” ๋ฌถ์—ฌ ์žˆ๋Š” ์ž์‹ ์ด์Šˆ๊ฐ€ ์ „๋ถ€ ๋๋‚˜๋ฉด ๋ถ€๋ชจ ์ด์Šˆ๋„ ์ž๋™์œผ๋กœ ๋‹ซ์Šต๋‹ˆ๋‹ค

ํ•ต์‹ฌ ์ฃผ์˜์ : ZenHub๋Š” "๋ณด๋“œ ์นธ ์œ„์น˜"(Pipeline) ์™€ "์‹ค์ œ ์—ด๋ฆผ/๋‹ซํž˜"(GitHub state) ์ด๋ผ๋Š” ๋‘ ๊ฐœ์˜ ๋ณ„๋„ ์ƒํƒœ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค. Done ์นธ์— ์žˆ์–ด๋„ GitHub์—์„œ๋Š” ์—ฌ์ „ํžˆ "์—ด๋ฆผ"์ผ ์ˆ˜ ์žˆ์–ด์„œ, ์ด ๋„์šฐ๋ฏธ๋Š” ์ด ๋‘˜์„ ํ—ท๊ฐˆ๋ฆฌ์ง€ ์•Š๋„๋ก ํ•ญ์ƒ ์‹ค์ œ ์ƒํƒœ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.

์–ด๋–ป๊ฒŒ ์“ฐ๋‚˜์š”#

# ๋ธŒ๋žœ์น˜ ์ƒ์„ฑ ํ›„ โ†’  " ์ง„ํ–‰ ์ค‘ " ์œผ๋กœ ์ด๋™
/dev:issue-state 25 --action move_pipeline --pipeline  " In Progress " 

 # PR ์ƒ์„ฑ ํ›„ โ†’  " ๊ฒ€ํ†  " ๋กœ ์ด๋™
/dev:issue-state 25 --action move_pipeline --pipeline  " Review/QA " 

 # PR ๋ณ‘ํ•ฉ ํ›„ โ†’ ์ •๋ง ๋‹ซํ˜”๋Š”์ง€ ๊ฒ€์ฆ + ์•ˆ ๋‹ซํ˜”์œผ๋ฉด ๋ณด์ •
/dev:issue-state 25 --action verify_closed
  • ์ˆซ์ž(25)๋Š” GitHub ์ด์Šˆ ๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค.
  • --action์—๋Š” move_pipeline(์นธ ์ด๋™), close(๋‹ซ๊ธฐ), verify_closed(๋‹ซํž˜ ๊ฒ€์ฆ), update(๋‚ด์šฉ ๊ฐฑ์‹ ), get_status(์ƒํƒœ ์กฐํšŒ)๋ฅผ ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • --pipeline์€ ์˜ฎ๊ธธ ๋ณด๋“œ ์นธ ์ด๋ฆ„์œผ๋กœ, move_pipeline์ผ ๋•Œ๋งŒ ํ•จ๊ป˜ ์”๋‹ˆ๋‹ค.
  • ๋ณ‘ํ•ฉ ํ›„์—๋Š” verify_closed๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. GitHub์˜ Closes # ์ž๋™ ๋‹ซ๊ธฐ๊ฐ€ ์กฐ์šฉํžˆ ์‹คํŒจํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, ์ด ๋™์ž‘์ด ์–‘์ชฝ์„ ํ™•์ธํ•˜๊ณ  ๋น ์ง„ ๋ถ€๋ถ„์„ ์ง์ ‘ ๋ณด์ •ํ•ด ์ค๋‹ˆ๋‹ค.

์•ˆ์—์„œ ๋ฌด์Šจ ์ผ์ด ๋ฒŒ์–ด์ง€๋‚˜์š”#

์š”์ฒญํ•œ --action์— ๋”ฐ๋ผ ๋‹ค์Œ ํ๋ฆ„์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

  1. ์นธ ์ด๋™(move_pipeline) โ€” ์ด์Šˆ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ด ํ˜„์žฌ ์นธ์„ ํ™•์ธํ•˜๊ณ , ๋ชฉํ‘œ ์นธ์œผ๋กœ ์˜ฎ๊ธด ๋’ค, ์ž˜ ์˜ฎ๊ฒจ์กŒ๋Š”์ง€ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  2. ๋‹ซ๊ธฐ(close) โ€” ์ด์Šˆ๋ฅผ CLOSED๋กœ ๋ฐ”๊พธ๊ณ , ๋ถ€๋ชจ ์ด์Šˆ๊ฐ€ ์žˆ์œผ๋ฉด ์ž์‹์ด ์ „๋ถ€ ๋๋‚ฌ๋Š”์ง€ ํ™•์ธํ•ด ๊ทธ๋ ‡๋‹ค๋ฉด ๋ถ€๋ชจ๋„ ๋‹ซ์Šต๋‹ˆ๋‹ค.
  3. ๋‹ซํž˜ ๊ฒ€์ฆ(verify_closed) โ€” ์ง„์งœ ๊ธฐ์ค€์ธ GitHub ์ƒํƒœ๋ฅผ ๋จผ์ € ํ™•์ธํ•˜๊ณ , ๋‹ซํ˜€ ์žˆ์ง€ ์•Š์œผ๋ฉด ์ง์ ‘ ๋‹ซ์Šต๋‹ˆ๋‹ค. ๊ทธ๋‹ค์Œ ZenHub๊ฐ€ Closed ์นธ์œผ๋กœ ๋™๊ธฐํ™”๋๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ๋ˆ„๋ฝ๋์œผ๋ฉด ์ง์ ‘ ๋ณด์ •ํ•ฉ๋‹ˆ๋‹ค.
  4. ์ƒํƒœ ์กฐํšŒ(get_status) โ€” ์ด์Šˆ์˜ ์ƒํƒœ, ๋ณด๋“œ ์œ„์น˜, ๋ถ€๋ชจ-์ž์‹ ๊ด€๊ณ„๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

โš™๏ธ ์ƒ์„ธ ์˜ต์…˜ยท์‹คํ–‰ ๋ช…์„ธ (๊ฐœ๋ฐœ์ž / AI ์—์ด์ „ํŠธ์šฉ)

Role and Responsibilities#

This agent manages ZenHub issue states.

  1. Pipeline Move: Move issues to the appropriate Pipeline
  2. Issue Close: Auto-close issues after PR merge
  3. Closure Verification: After merge, verify the issue reached Closed on both GitHub and ZenHub, and fall back to an explicit close if not
  4. Status Query: Check current issue state
  5. Parent Issue Management: Update parent issue when all children are completed

โš ๏ธ ZenHub Two-State Model (read first)#

ZenHub tracks two independent states; conflating them causes "issue stuck open":

StateOwnerMeaning
PipelineZenHubBoard column (In Progress, Review/QA, Done, โ€ฆ). Just a position.
GitHub stateGitHubThe real open / closed flag.
  • Done pipeline โ‰  closed โ€” an issue in Done is still open on GitHub.
  • Closed pipeline = GitHub closed, 1:1 โ€” judge open/closed by state, not pipeline.
  • This repo's policy is "merge = Close" (B): AI agents run full-stack E2E/review before merge, so Done is not used. A PR merging with Closes #N auto-closes the GitHub issue โ†’ ZenHub syncs to Closed โ€” but Closes #N fires only when the PR merges into the repo's default branch.
  • Hierarchical merges into a parent work-base branch (story/, epic/) never auto-close (base โ‰  default branch); Closes #N can also silently miss / lag on default-branch merges. So after every merge the agent runs action: verify_closed, which closes explicitly (gh issue close + ZenHub state:CLOSED). For hierarchical merges this explicit close is the primary mechanism, not a fallback.

Input Parameters#

ParameterRequiredTypeDescription
issue_numberโœ…numberGitHub issue number
actionโœ…stringmove_pipeline | close | verify_closed | update | get_status
target_pipelineโŒstringTarget Pipeline (when action=move_pipeline)
update_bodyโŒstringContent to update (when action=update)

Output#

interface IssueStateResult {
  success: boolean;
  issue_number: number;
  current_pipeline: string;
  previous_pipeline?: string;
  state:  ' OPEN '   |  ' CLOSED ' ;
  parent_issue?: {
    number: number;
    all_children_done: boolean;
  };
  error?: string;
}

Pipeline Structure#

Petmedi Workspace Pipeline#

New Issues โ†’ Product Backlog โ†’ Sprint Backlog โ†’ In Progress โ†’ Review/QA โ†’ [Merge = Close โ†’ Closed]
PipelineDescriptionEntry Condition
New IssuesNewly created issuesOn issue creation
Product BacklogAwaiting prioritizationAfter triage
Sprint BacklogAssigned to sprintDuring sprint planning
In ProgressWork in progressOn branch creation
Review/QAAwaiting reviewOn PR creation โ€” deployment-gated trackers (Jira/Unibook): on staging deploy instead (stay In Progress through PR + merge). See deployment-gated-status.
DoneCompletedNot used (Done โ‰  closed; would leave issue open)
ClosedCompleted & closedPR merge (Closes #) โ†’ GitHub closed โ†’ ZenHub sync. Verify + fallback. (Not applicable to deployment-gated Jira โ€” no Closes # auto-close.)

Pipeline Moves by Workflow#

Moves During Issue Cycle#

1. Branch creation complete
   โ†’ move_pipeline:  " In Progress " 

 2. PR creation complete
   โ†’ move_pipeline:  " Review/QA " 
    โ†’ โš ๏ธ deployment-gated trackers (Jira/Unibook): SKIP โ€” keep  " In Progress " ;
     move to  " In Review "   only when the merge is reflected in the staging deploy
     (see rules/deployment-gated-status.md)

3. PR merge complete (merge = Close)
   โ†’ Issue auto-closes via GitHub  " Closes # "   keyword โ†’ ZenHub syncs to Closed
   โ†’ Never move to Done (Done โ‰  closed)
   โ†’ action: verify_closed  (verify GitHub closed + ZenHub Closed, fallback if not)

4. All child issues completed (parent issue)
   โ†’ Close parent issue (directly Close, never via Done)

Execution Flow#

action: move_pipeline#

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Step 1: Query Issue Info                                โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  mcp__zenhub__searchLatestIssues                        โ”‚
โ”‚  - Query issue ID                                        โ”‚
โ”‚  - Check current Pipeline                                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                          โ”‚
                          โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Step 2: Move Pipeline                                   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  mcp__zenhub__moveIssueToPipeline                       โ”‚
โ”‚  - issueId: {issue_graphql_id}                          โ”‚
โ”‚  - pipelineId: {target_pipeline_id}                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                          โ”‚
                          โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Step 3: Verify Result                                   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  - Verify move success                                   โ”‚
โ”‚  - Return current Pipeline                               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

action: close#

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Step 1: Close Issue                                     โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  mcp__zenhub__updateIssue                               โ”‚
โ”‚  - issueId: {issue_graphql_id}                          โ”‚
โ”‚  - state:  " CLOSED "                                        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                          โ”‚
                          โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Step 2: Check Parent Issue                              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  IF parent issue exists:                                 โ”‚
โ”‚    - Check status of all child issues                    โ”‚
โ”‚    - If all completed โ†’ close parent issue               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

action: verify_closed (after every merge) โญ#

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Step 1: Verify GitHub state (source of truth)          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  $ gh issue view {N} --json state -q .state             โ”‚
โ”‚  IF state !=  " CLOSED " :                                   โ”‚
โ”‚    # `Closes #N` did not fire โ†’ close explicitly         โ”‚
โ”‚    $ gh issue close {N} --reason completed               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                          โ”‚
                          โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Step 2: Verify ZenHub synced to Closed pipeline        โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  mcp__zenhub__searchClosedIssues({ query:  " #{N} "   })     โ”‚
โ”‚  IF issue not in closed results (sync lag):              โ”‚
โ”‚    mcp__zenhub__updateIssue({ issueId, state:  " CLOSED "   })โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
// Closure verification + fallback
async function verifyClosed(issueNumber: number, issueId: string) {
  // 1. GitHub is the source of truth for open/closed
  const ghState = (await Bash(`gh issue view ${issueNumber} --json state -q .state`)).trim();
  if (ghState !==  " CLOSED " ) {
    await Bash(`gh issue close ${issueNumber} --reason completed`);
  }
  // 2. Confirm ZenHub synced GitHub closed โ†’ Closed pipeline
  const closed = await mcp__zenhub__searchClosedIssues({ query: `#${issueNumber}` });
  if (!closed.find(i = >   i.number === issueNumber)) {
    await mcp__zenhub__updateIssue({ issueId, state:  " CLOSED "   });
  }
}

action: get_status#

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Query Issue Status                                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  mcp__zenhub__searchLatestIssues                        โ”‚
โ”‚  - Return issue state                                    โ”‚
โ”‚  - Return Pipeline info                                  โ”‚
โ”‚  - Return parent/child relationships                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

MCP Tool Usage#

Issue Query#

// Search by issue number
const result = await mcp__zenhub__searchLatestIssues({
  query:  " 25 " 
 });

// Extract issue info from result
const issue = result.find(i = >   i.number === 25);
const issueId = issue.id;  // GraphQL ID
const pipelineId = issue.pipelineIssue.pipeline.id;

Pipeline Move#

// Query pipeline list
const pipelines = await mcp__zenhub__getWorkspacePipelinesAndRepositories();

// Find Pipeline ID
const targetPipeline = pipelines.pipelines.find(
  p = >   p.name ===  " In Progress " 
 );

// Move issue
await mcp__zenhub__moveIssueToPipeline({
  issueId: issueId,
  pipelineId: targetPipeline.id
});

Issue Close#

// Change issue state
await mcp__zenhub__updateIssue({
  issueId: issueId,
  state:  " CLOSED " 
 });

Parent-Child Relationship Check#

// Check parentIssue field when querying issues
const issue = result[0];
if (issue.parentIssue) {
  const parentNumber = issue.parentIssue.number;
  // Check all children of the parent issue
}

Pipeline ID Mapping#

Query Pipeline ID per Workspace#

// Query workspace info
const workspace = await mcp__zenhub__getWorkspacePipelinesAndRepositories();

// Pipeline list
workspace.pipelines.forEach(p = >   {
  console.log(`${p.name}: ${p.id}`);
});

Pipeline Name โ†’ ID Conversion#

// โš ๏ธ ๊ฐ’์€ ๋ผ์ด๋ธŒ ํŒŒ์ดํ”„๋ผ์ธ ์ด๋ฆ„๊ณผ ์ •ํ™•ํžˆ ์ผ์น˜ํ•ด์•ผ ํ•œ๋‹ค.
//    (๊ณผ๊ฑฐ  ' Backlog ' / ' Review '   ๋Š” ์‹ค์žฌ ์ปฌ๋Ÿผ์ด ์•„๋‹ˆ๋ผ find() ๊ฐ€ undefined โ†’ silent no-op ์˜€๋‹ค.)
const PIPELINE_NAMES = {
   ' new ' :  ' New Issues ' ,
   ' icebox ' :  ' Icebox ' ,
   ' backlog ' :  ' Product Backlog ' ,
   ' sprint ' :  ' Sprint Backlog ' ,
   ' progress ' :  ' In Progress ' ,
   ' review ' :  ' Review/QA ' ,
  //  ' done ' / ' closed '   ์—†์Œ โ€” Done ์ปฌ๋Ÿผ ๋ฏธ์‚ฌ์šฉ, Closed ๋Š” GitHub state(์ปฌ๋Ÿผ ์•„๋‹˜)
};

function getPipelineId(shortName: string, pipelines: Pipeline[]) {
  const fullName = PIPELINE_NAMES[shortName];
  const pipeline = pipelines.find(p = >   p.name === fullName);
  // fail-closed: ๋ชป ์ฐพ์œผ๋ฉด throw (undefined ๋ฅผ ๊ทธ๋Œ€๋กœ move ์— ๋„˜๊ฒจ ๋ฌด์Œ ๋ˆ„๋ฝํ•˜๋Š” ๊ฒƒ์„ ๊ธˆ์ง€)
  if (!pipeline) {
    throw new Error(
      `Pipeline  ' ${fullName ?? shortName} '   not found. Live: ${pipelines.map(p = >   p.name).join( " ,  " )}`
    );
  }
  return pipeline.id;
}

Automatic Parent Issue Management#

Handle Parent When Child Is Completed#

1. Close child issue
   โ†“
2. Query parent issue
   โ†“
3. Check status of all child issues
   โ†“
4. If all are CLOSED:
   - Close parent issue (without moving to Done pipeline)

Called from: commands/dev/run.md Step 12.5 (when the just-closed issue's type is Epic โ€” recurses to Project/Initiative) and commands/dev/batch.md Phase 3 Step 6.6 (after the Epic PR merges into development). Story-level closes do not trigger this โ€” only an Epic close recurses upward.

Epic Auto-Completion Condition#

async function checkAndCloseParent(childIssueNumber: number) {
  // โš ๏ธ ZenHub MCP ์—๋Š” getIssue / getChildrenOfParent ๊ฐ€ ์—†๋‹ค โ€” searchLatestIssues ๋กœ๋งŒ ์กฐํšŒํ•œ๋‹ค.
  const childRes = await mcp__zenhub__searchLatestIssues({ query: `#${childIssueNumber}` });
  const child = childRes.find(i = >   i.number === childIssueNumber);
  const parent = child?.parentIssue;                 // { id, number }
  if (!parent) return;

  // ๋ถ€๋ชจ์˜ ๋ชจ๋“  ์ž์‹ ์กฐํšŒ (๊ฒ€์ƒ‰ DSL: parent: < graphqlId > )
  const siblings = await mcp__zenhub__searchLatestIssues({ query: `parent:${parent.id}` });

  // โš ๏ธ ๋นˆ/์กฐํšŒ์‹คํŒจ ๊ฒฐ๊ณผ๋กœ ์ธํ•œ ์กฐ๊ธฐ close ๋ฐฉ์ง€:
  //    ์ž์‹์ด 1๊ฐœ ์ด์ƒ์ด๊ณ  ์ „๋ถ€ CLOSED ์ผ ๋•Œ๋งŒ ์ง„ํ–‰ (๋นˆ ๋ฐฐ์—ด์˜ .every ๋Š” true ๊ฐ€ ๋˜์–ด ์œ„ํ—˜).
  if (!siblings || siblings.length === 0) return;    // ํŒ์ • ๋ถˆ๊ฐ€ โ†’ close ์•ˆ ํ•จ
  const allDone = siblings.every(s = >   (s.state ||  " " ).toUpperCase() ===  " CLOSED " );
  if (!allDone) return;

  // ๋ถ€๋ชจ๋ฅผ ๋‹ซ๋Š”๋‹ค (merge=Close, Done ๋ฏธ์‚ฌ์šฉ). GitHub ๊ฐ€ source of truth โ†’ ๋ช…์‹œ์  close ํ›„ ์–‘์ชฝ ๊ฒ€์ฆ.
  await Bash(`gh issue close ${parent.number} --reason completed`);
  await mcp__zenhub__updateIssue({ issueId: parent.id, state:  " CLOSED "   });
  const closed = await mcp__zenhub__searchClosedIssues({ query: `#${parent.number}` });
  if (!closed.find(i = >   i.number === parent.number)) {
    await mcp__zenhub__updateIssue({ issueId: parent.id, state:  " CLOSED "   });  // ๋™๊ธฐํ™” ์ง€์—ฐ ์‹œ ์žฌ์‹œ๋„
  }

  // ์กฐ๋ถ€๋ชจ๋„ ์žฌ๊ท€์ ์œผ๋กœ ํ™•์ธ (Story ์™„๋ฃŒ โ†’ Epic ์™„๋ฃŒ โ†’ Project ์™„๋ฃŒ ์—ฐ์‡„)
  await checkAndCloseParent(parent.number);
}

Usage Examples#

After Branch Creation#

# Move to In Progress
/dev:issue-state 25 --action move_pipeline --pipeline  " In Progress " 

 # Result:
# issue_number: 25
# current_pipeline:  " In Progress " 
 # previous_pipeline:  " Sprint Backlog "

After PR Creation#

# Move to Review
/dev:issue-state 25 --action move_pipeline --pipeline  " Review/QA "

After PR Merge (merge = Close)#

# Issue auto-closes via GitHub  " Closes # "   keyword โ†’ ZenHub syncs to Closed.
# Never move to Done (Done โ‰  closed). Then VERIFY + fallback:
/dev:issue-state 25 --action verify_closed
#   1. gh issue view 25 โ†’ if not CLOSED โ†’ gh issue close 25 --reason completed
#   2. searchClosedIssues  " #25 "   โ†’ if missing โ†’ updateIssue state:CLOSED
# Parent issue is auto-checked when all children are closed

Error Handling#

Common Errors#

ErrorCauseResolution
Issue not foundWrong issue numberVerify number
Pipeline not foundWrong Pipeline nameVerify name
Already in pipelineAlready in that PipelineSkip
Cannot closeNo permissionManual handling needed

Recovery Strategy#

1. Log the error
2. Return current state
3. Report failure (success: false)

Key Rules#

  1. State Tracking: Record state before and after every move
  2. Two-State Awareness: Pipeline โ‰  GitHub state; judge closed by GitHub state, never by a Done/Closed column alone
  3. Merge = Close: Never park merged issues in Done; after merge, run verify_closed so GitHub closed and ZenHub Closed are both confirmed
  4. Automatic Parent Management: Auto-check parent when child is completed
  5. Idempotency: Safely skip when moving to the same Pipeline / when already closed
  6. Failure Tolerance: Continue workflow even on failure
  7. Detailed Logging: Detailed logs for all state changes