LogoSkills

BMAD v6 Helper Utilities

This document contains reusable utilities for BMAD workflows. Skills and commands can reference specific sections to avoid repetition.

This document contains reusable utilities for BMAD workflows. Skills and commands can reference specific sections to avoid repetition.

Config Loading#

Load Global Config#

Path: ~/.claude/config/bmad/config.yaml
Purpose: Get user settings, enabled modules, defaults

Using Read tool:
1. Read ~/.claude/config/bmad/config.yaml
2. Parse YAML to extract:
   - user_name
   - communication_language
   - default_output_folder
   - modules_enabled
3. Store in memory for workflow

Load Project Config#

Path: {project-root}/bmad/config.yaml
Purpose: Get project-specific settings

Using Read tool:
1. Read bmad/config.yaml
2. Parse YAML to extract:
   - project_name
   - project_type
   - project_level
   - output_folder
3. Merge with global config (project overrides global)

Combined Config Load#

Execute in order:
1. Load global config (defaults)
2. Load project config (overrides)
3. Return merged config object

Status File Operations#

Load Workflow Status#

Path: {output_folder}/bmm-workflow-status.yaml (from project config)
Purpose: Check completed workflows, current phase

Using Read tool:
1. Read .bmad/bmm-workflow-status.yaml (or path from config)
2. Parse YAML to extract:
   - project metadata
   - workflow_status array
3. Determine current phase:
   - Find last completed workflow (status = file path)
   - Identify next required/recommended workflow

Update Workflow Status#

Purpose: Mark workflow as complete

Using Edit tool:
1. Load current status file
2. Find workflow by name
3. Update status field:  " {file-path} " 
 4. Update last_updated: current timestamp
5. Save changes

Load Sprint Status#

Path: {output_folder}/sprint-status.yaml
Purpose: Check epic/story progress

Using Read tool:
1. Read .bmad/sprint-status.yaml
2. Parse YAML to extract:
   - sprint_number
   - epics array
   - stories within epics
   - metrics

Update Sprint Status#

Purpose: Add/update epics and stories

Using Edit tool:
1. Load current sprint status
2. Modify epics/stories array
3. Recalculate metrics
4. Update last_updated timestamp
5. Save changes

Git Branch Strategy#

Resolve Branch Names#

Purpose: Compute epic/story/task branch names from sprint-status story entry

Input: story entry (story_id, epic info, title)
Output: { epic_branch, story_branch, task_prefix }

Steps:
1. Extract epic identifier:
   - epic_number = epic ' s ZenHub issue number (e.g., 025)
   - epic_slug = epic name โ†’ lowercase, spaces/special โ†’ hyphens, max 40 chars
   - Example:  " CoUI Flutter Maintenance "   โ†’  " coui-flutter-maintenance " 

 2. Build epic branch name:
   - Format: epic/EPIC-{number}-{epic_slug}
   - Example: epic/EPIC-025-coui-flutter-maintenance

3. Extract story identifier:
   - story_id from sprint-status (e.g.,  " STORY-008 " )
   - story_slug = story title โ†’ lowercase, spaces/special โ†’ hyphens, max 40 chars
   - Example:  " DCM Warning Zero "   โ†’  " dcm-warning-zero " 

 4. Build story branch name:
   - Format: story/STORY-{id}-{story_slug}
   - Example: story/STORY-008-dcm-warning-zero

5. Build task prefix:
   - Format: task/STORY-{id}-
   - Example: task/STORY-008-

6. Return:
   epic_branch:  " epic/EPIC-{number}-{epic_slug} " 
    story_branch:  " story/STORY-{id}-{story_slug} " 
    task_prefix:  " task/STORY-{id}- "

Create Branch Hierarchy#

Purpose: Create epic and story branches in correct hierarchy

Input: epic_branch, story_branch (from Resolve Branch Names)
Requires: git repository

Steps:
1. Check if epic branch exists:
   git branch -a | grep  " {epic_branch} " 

 2. If epic branch does NOT exist:
   git checkout main
   git pull origin main
   git checkout -b {epic_branch}
   git push -u origin {epic_branch}
   Log:  " โœ“ Created epic branch: {epic_branch} " 

 3. Check if story branch exists:
   git branch -a | grep  " {story_branch} " 

 4. If story branch does NOT exist:
   git checkout {epic_branch}
   git pull origin {epic_branch}
   git checkout -b {story_branch}
   git push -u origin {story_branch}
   Log:  " โœ“ Created story branch: {story_branch} " 

 5. If story branch already exists:
   git checkout {story_branch}
   git pull origin {story_branch}
   Log:  " โœ“ Switched to existing story branch: {story_branch} " 

 Fallback:
  If any git operation fails:
  - Log warning:  " โš  Failed to create branch hierarchy. Falling back to flat branch. " 
   - git checkout main
  - git checkout -b feature/STORY-{ID}
  - Return fallback branch name

Create Task Branch#

Purpose: Create a task branch from the story branch for large tasks

Input: story_branch, task_slug (short description)
Output: task branch name

Steps:
1. Ensure on story branch:
   git checkout {story_branch}
   git pull origin {story_branch}

2. Build task branch name:
   - Format: task/STORY-{id}-{task_slug}
   - Example: task/STORY-008-fix-lint-rules

3. Create and push task branch:
   git checkout -b {task_branch}
   git push -u origin {task_branch}
   Log:  " โœ“ Created task branch: {task_branch} " 

 4. Return task branch name

Create PR and Merge#

Purpose: Create a pull request and optionally merge it

Input: source_branch, target_branch, pr_title, pr_body, merge_strategy
  merge_strategy:  " squash "   (taskโ†’story) |  " merge "   (storyโ†’epic, epicโ†’main)

Steps:
1. Push source branch:
   git push origin {source_branch}

2. Check if gh CLI is available:
   which gh

3. If gh CLI available:
   a. Create PR:
      gh pr create --base {target_branch} --head {source_branch} \
        --title  " {pr_title} "   --body  " {pr_body} " 
    b. Extract PR URL from output
   c. Log:  " โœ“ PR created: {pr_url} " 
    d. If auto-merge requested (taskโ†’story only):
      gh pr merge {pr_url} --squash --delete-branch
      Log:  " โœ“ PR merged (squash): {source_branch} โ†’ {target_branch} " 

 4. If gh CLI NOT available:
   Log:  " โš  gh CLI not installed. Please create PR manually: " 
    Log:  "    Source: {source_branch} " 
    Log:  "    Target: {target_branch} " 
    Log:  "    Title: {pr_title} " 
    Log:  "    Merge strategy: {merge_strategy} " 
    Return manual_pr_needed = true

5. Return { pr_url, merged, manual_pr_needed }

Merge strategy reference:
  - task โ†’ story: squash merge (clean commit history)
  - story โ†’ epic: merge commit (preserve story history)
  - epic โ†’ main: merge commit (preserve epic history)

Publish Pipeline Docs PR#

Purpose: Land the pipeline ' s *document* artifacts on the default branch through a PR, so a
         document the pipeline produced never ends the pipeline living only in one working tree.

Single definition. Callers cite this block; they do not restate it:
  - /cc-product:breakdown  Phase 5  โ€” stage_set = discovery..breakdown, scope = planning
  - /cc-product:launch     Phase 4  โ€” stage_set = launch,               scope = launch
  The development stage is deliberately NOT a caller: its documents ride along with the code PR
  that /cc-dev:run already opens. Do not add a second publication path for them.

Reuses: `Create PR and Merge` (above) for the push/create/merge mechanics.

Input:  slug, stage_set (list of config/pipeline.json stage keys), scope (planning | launch)
Output: { pr_number, pr_url, merged, degraded_reason }

Steps:
1. Collect the document set:
   a. For each stage in stage_set, read .pipeline/{slug}.yaml โ†’ stages.{stage}.artifacts
   b. Add the state file itself: .pipeline/{slug}.yaml
   c. Keep only paths under `docs/`, `.claude/docs/` or `.pipeline/` โ€” this helper publishes
      *documents*. A stage whose artifacts are source paths (scaffold: `lib/**`, `test/**`,
      `*.feature`) therefore contributes nothing here, and the  " docs-only diff "   property below is
      enforced by this filter rather than assumed
   d. Keep the paths that exist and are non-empty
   e. Every declared path that is missing is carried into the report **by name** โ€” a document
      cannot be dropped by being silently absent from the collection

2. Refuse to publish a document that did not pass its gate:
   a. Gated stage โ€” read stages.{stage}.gate.verdict
      - pass | skipped | BYPASSED โ†’ publish (BYPASSED is a recorded decision; the PR body says so)
      - fail | undetermined | missing gate block โ†’ do NOT publish that stage ' s documents; name the
        stage and its verdict in the report. Unknown is not pass (orchestration-graph.md ยง3)
   b. Gateless stage (`discovery`, `launch` โ€” gate: null in config/pipeline.json) โ€” there is no
      verdict to read, so judge by stages.{stage}.status
      - completed | skipped โ†’ publish whatever it wrote (a skipped stage wrote nothing)
      - in-progress | failed | pending โ†’ do NOT publish; the stage has not finished producing the
        document being published

3. Idempotency โ€” check before creating anything. The head branch name is deterministic, so match on
   it rather than on title text:
   gh pr list --head docs/{slug}-{scope} --state all --json number,state
   - A merged PR exists and no collected file differs from the default branch
     โ†’ skip, return { merged: true, pr_number:  < existing >   }
   - An open PR exists โ†’ push onto that same branch; never open a second PR
   - Also consult stages.{stage}.docs_pr (ยง state file) โ€” it is the record that survives `/clear`

4. Branch from the **default branch**, never from a stage/story/epic branch:
   git fetch origin
   git checkout -B docs/{slug}-{scope} origin/{default_branch}

5. Stage exactly the collected paths โ€” never `git add -A`:
   git add -- {collected paths}
   Abort if `git diff --cached --name-only` contains a path outside the collected set. This PR
   carries documents; picking up a stray source edit turns a reviewable docs PR into a code PR.

6. Commit:
   docs({slug}): ๐Ÿ“ {scope} ์‚ฐ์ถœ๋ฌผ โ€” {stage_set summary}

7. Create PR and merge โ€” `Create PR and Merge` with merge_strategy:  " squash " .
   The PR body carries, per stage: gate name, verdict, attempts, and any skipped_checks /
   dropped / BYPASSED entries from .pipeline/{slug}.yaml. A reviewer must be able to tell which
   document passed its gate and which was waved through, without opening the state file.

8. Record on every stage that contributed at least one file to this PR (not on the whole
   stage_set โ€” a stage that contributed nothing was not published, and saying it was is a lie the
   next re-run would act on):
   .pipeline/{slug}.yaml โ†’ stages.{stage}.docs_pr: { number, url, merged_at, status }
   Write this **before** yielding to a context-clear handoff โ€” an unrecorded publication is
   indistinguishable from no publication on re-entry.

Docs-only diff: the collected set is confined to docs/**, .claude/docs/** and .pipeline/**, so
  there is no changed package for the /cc-dev:pr:preflight test gate to run and it is not
  invoked. Repository-level document checks (CI, link/lint) are NOT waived by that.

Degradation โ€” never silent. A document that did not reach the default branch is named:
  - gh CLI missing / no git remote:
      commit locally on docs/{slug}-{scope}, return manual_pr_needed with that branch name,
      record docs_pr.status: degraded + reason
  - Collected set is empty:
      do NOT open an empty PR. Report which declared paths were missing and leave docs_pr: null.
      Empty is a finding, not a success
  - A stage was withheld by step 2:
      publish the rest; report the withheld stage and its verdict
  - PR opens but CI fails:
      leave the PR open and report its number. Never force-merge a docs PR to keep the pipeline moving
Degradation never halts the pipeline โ€” it is recorded and reported, and the next stage continues.

ZenHub Integration#

Load ZenHub Conventions#

Purpose: Load cached ZenHub IDs from conventions file; merge with live MCP data if available

Path: {project-root}/bmad/zenhub-conventions.yaml

Steps:
1. Try to read bmad/zenhub-conventions.yaml
   - If exists: Parse YAML, populate zh_conventions cache
   - If not exists: zh_conventions = empty (will be populated by Load ZenHub Context)

2. Extract cached values:
   - zh_github_repo_id = conventions.repository.id
   - zh_org_id = conventions.organization.id
   - zh_issue_types = conventions.issue_types (epic, feature, task, bug)
   - zh_pipelines = conventions.pipelines (product_backlog, sprint_backlog, etc.)
   - zh_sub_tasks_enabled = conventions.sub_tasks.enabled
   - zh_issue_creation = conventions.issue_creation

3. If all required IDs present (repository.id, at least epic + feature issue types, pipelines):
   - Set zh_conventions_loaded = true
   - Log:  " โœ“ ZenHub conventions loaded from cache " 
    Else:
   - Set zh_conventions_loaded = false
   - Log:  " โš  ZenHub conventions incomplete โ€” MCP discovery required " 

 4. Return zh_conventions object

Load ZenHub Context#

Purpose: Initialize ZenHub MCP connection and load workspace metadata

Steps:
0. Call helpers.md#Load-ZenHub-Conventions (load cached IDs first)

1. Call getWorkspacePipelinesAndRepositories()
   - Extract GitHub repository ID (graphql ID for createGitHubIssue)
   - Extract ZenHub organization ID (for setDatesForIssue)
   - Extract pipeline IDs by name:
     -  " Product Backlog "   โ†’ zh_pipelines[ " Product Backlog " ]
     -  " Sprint Backlog "   โ†’ zh_pipelines[ " Sprint Backlog " ]
     -  " In Progress "   โ†’ zh_pipelines[ " In Progress " ]
     -  " Review/QA "   โ†’ zh_pipelines[ " Review/QA " ]
     // Done pipeline is not used โ€” issues auto-close via GitHub  " Closes # "   keyword on PR merge

2. Call getIssueTypes(repositoryId: zh_github_repo_id)
   - Map issue type names to IDs:
     -  " Epic "   โ†’ zh_issue_types[ " Epic " ]
     -  " Feature "   โ†’ zh_issue_types[ " Feature " ]
     -  " Sub-task "   โ†’ zh_issue_types[ " Sub-task " ] (Level 5 โ€” the correct type for sub-tasks)
     -  " Task "   โ†’ zh_issue_types[ " Task " ] (Level 4 โ€” FALLBACK only, see below)
     -  " Project "   โ†’ zh_issue_types[ " Project " ] (Level 2, optional โ€” wraps multiple Epics)
     -  " Initiative "   โ†’ zh_issue_types[ " Initiative " ] (Level 1, optional โ€” wraps multiple Projects)
     -  " Bug "   โ†’ zh_issue_types[ " Bug " ] (optional)
   - Determine the sub-task type to use:
     - If  " Sub-task "   type exists: zh_sub_tasks_supported = true, zh_sub_task_type_name =  " Sub-task " 
      - Else if  " Task "   type exists: zh_sub_tasks_supported = true, zh_sub_task_type_name =  " Task " , log  " โš   ' Sub-task '   issue type not found in this workspace โ€” falling back to  ' Task '   as a substitute. Note: Task is Level 4, the same level as the parent Story (Feature), so this creates a same-level parent-child link rather than a proper Level 5 sub-task. " 
      - Else: zh_sub_tasks_supported = false

3. Call getSprint() โ†’ zh_active_sprint (id, name, dates)
   Call getUpcomingSprint() โ†’ zh_next_sprint (id, name, dates)

4. Set zh_available = true

5. Auto-save conventions file:
   - Merge MCP results into zh_conventions object
   - Write updated bmad/zenhub-conventions.yaml with all discovered IDs
   - Update last_updated timestamp
   - Log:  " โœ“ ZenHub conventions saved to bmad/zenhub-conventions.yaml " 

 On any failure:
  - If zh_conventions_loaded = true:
    - Set zh_available = true (use cached values)
    - Output warning:  " โš  ZenHub MCP unavailable. Using cached conventions. " 
   - If zh_conventions_loaded = false:
    - Set zh_available = false
    - Output warning:  " โš  ZenHub MCP unavailable. Continuing with local-only workflow. " 
   - Continue with existing workflow (no abort)

Sync Epic to ZenHub#

Purpose: Create a GitHub issue for an epic and set its ZenHub type

Input: epic_name, epic_description, sprint_start_date, sprint_end_date
Requires: zh_available = true, zh_github_repo_id, zh_issue_types[ " Epic " ]

Steps:
1. Call createGitHubIssue:
   - repositoryId: zh_github_repo_id
   - title:  " {epic_name} " 
    - body: epic_description โ€” ๊ณ„์•ฝ ๋ธ”๋ก + `## ๐Ÿ“„ ์ƒ์„ธ ๊ธฐํš` ๋งํฌ ๋ธ”๋ก
     (Epic ์€ ํ•ญ์ƒ ์•„ํ‹ฐํŒฉํŠธ ๋ฐœํ–‰ ๋Œ€์ƒ. ์ „๋žต ์„œ์ˆ ์€ ์•„ํ‹ฐํŒฉํŠธ๋กœ, ์šฐ์„ ์ˆœ์œ„ ํ‘œ๋Š” ๋ณธ๋ฌธ์—.
      ๋ฐœํ–‰์€ createGitHubIssue ๋ณด๋‹ค ๋จผ์ €. `Artifact` ๋ถ€์žฌ ์‹œ ์ „๋Ÿ‰ ๋งˆํฌ๋‹ค์šด ํด๋ฐฑ.
      SoT: cc-dev/rules/zenhub-conventions.md โ†’ Issue Body Artifact Contract)

2. Extract zh_epic_id from response

3. Call setIssueType:
   - issueIds: [zh_epic_id]
   - issueTypeId: zh_issue_types[ " Epic " ]

4. If sprint dates available, call setDatesForIssue:
   - issueId: zh_epic_id
   - zenhubOrganizationId: zh_org_id
   - startDate: sprint_start_date (YYYY-MM-DD)
   - endDate: sprint_end_date (YYYY-MM-DD)

5. Return zh_epic_id and GitHub issue URL/number

Sync Story to ZenHub#

Purpose: Create a GitHub issue for a story (work item), link to epic, set estimate and sprint

Input: story_title, story_body, story_points, zh_epic_id (optional),
       sprint_id (optional), pipeline_id
Requires: zh_available = true, zh_github_repo_id, zh_issue_types[ " Feature " ]

Resolve work item type (never hardcode  " Feature "   โ€” same classification as
cc-dev ' s `resolveWorkItemType()` in zenhub-integration-agent.md / commands/dev/run.md ' s
`issueTypeMap`, so both creation paths agree on the same story ' s type):
  - If story_title/story_body matches fix|bug|error|์ˆ˜์ •|๋ฒ„๊ทธ|์˜ค๋ฅ˜ โ†’ zh_issue_types[ " Bug " ] (if available, else fall back to  " Feature " )
  - Else if matches refactor|improve|optimize|config|build|๊ฐœ์„ |๋ฆฌํŒฉํ† ๋ง|์ตœ์ ํ™”|์„ค์ •|๋นŒ๋“œ โ†’ zh_issue_types[ " Task " ]
  - Else โ†’ zh_issue_types[ " Feature " ] (default: add/implement/create/screen)

Steps:
1. Call createGitHubIssue:
   - repositoryId: zh_github_repo_id
   - title:  " {story_title} " 
    - body: story_body โ€” ๊ณ„์•ฝ ๋ธ”๋ก(acceptance criteria ยท ๋ฒ”์œ„ ยท DoD)์€ ๋ณธ๋ฌธ์— ์œ ์ง€,
     user storyยทtechnical notes ์„œ์ˆ ์ด 20ํ–‰์„ ๋„˜์œผ๋ฉด ์•„ํ‹ฐํŒฉํŠธ๋กœ ๋ฐœํ–‰ํ•ด ๋งํฌ ๋ธ”๋ก์œผ๋กœ ๋Œ€์ฒด
     (SoT: cc-dev/rules/zenhub-conventions.md โ†’ Issue Body Artifact Contract)
   - parentIssueId: zh_epic_id (if available)

2. Extract zh_story_id and issue number/URL from response

3. Call setIssueType:
   - issueIds: [zh_story_id]
   - issueTypeId: resolved type from above (Feature/Bug/Task) โ€” never hardcoded

4. Call setIssueEstimate:
   - issueId: zh_story_id
   - estimate: story_points

5. If sprint_id available, call addIssuesToSprints:
   - issueIds: [zh_story_id]
   - sprintIds: [sprint_id]

6. Call moveIssueToPipeline:
   - issueId: zh_story_id
   - pipelineId: pipeline_id (Sprint Backlog or Product Backlog)

7. Return zh_story_id, issue number, GitHub issue URL

Sync Sub-task to ZenHub#

Purpose: Create a GitHub issue for a sub-task, link to parent story

Input: sub_task_title, sub_task_body, zh_story_id (parent story),
       pipeline_id (optional, default: Sprint Backlog)
Requires: zh_available = true, zh_github_repo_id, zh_sub_tasks_supported = true
          (zh_sub_task_type_name set by Load-ZenHub-Context โ€”  " Sub-task "   preferred,  " Task "   fallback)

Pre-check:
  If zh_sub_tasks_supported = false (neither  " Sub-task "   nor  " Task "   type exists in this workspace):
    Log:  " โš  Sub-task issue type not available in this workspace. Skipping. " 
     Return null

Steps:
1. Call createGitHubIssue:
   - repositoryId: zh_github_repo_id
   - title:  " {sub_task_title} " 
    - body: sub_task_body (markdown โ€” Sub-task ๋Š” ์•„ํ‹ฐํŒฉํŠธ ๋ฐœํ–‰ ๋Œ€์ƒ์ด ์•„๋‹ˆ๋‹ค)
   - parentIssueId: zh_story_id

2. Extract zh_sub_task_id and issue number/URL from response

3. Call setIssueType:
   - issueIds: [zh_sub_task_id]
   - issueTypeId: zh_issue_types[zh_sub_task_type_name]
     //  " Sub-task "   (Level 5) when available;  " Task "   (Level 4) only as a same-level fallback โ€” see Load-ZenHub-Context

4. If pipeline_id provided, call moveIssueToPipeline:
   - issueId: zh_sub_task_id
   - pipelineId: pipeline_id

5. Log:  " โœ“ Sub-task synced: {title} โ†’ #{issue_number} (parent: #{story_number}, type: {zh_sub_task_type_name}) " 

 6. Return zh_sub_task_id, issue number, GitHub issue URL

Auto-Generate Sub-tasks#

Purpose: Automatically generate sub-tasks from a story ' s acceptance criteria and technical tasks

Input: story_document (parsed story content), zh_story_id (parent story ZenHub ID)
Requires: zh_available = true, zh_sub_tasks_enabled = true

Steps:
1. Parse story document to extract:
   a. Acceptance criteria (each becomes a validation sub-task)
   b. Technical notes โ†’ components/endpoints (each becomes an implementation sub-task)
   c. Testing items (each becomes a test sub-task)

2. Generate sub-task list:
   - Implementation tasks:  " Implement {component/endpoint/feature} " 
    - Validation tasks:  " Validate: {acceptance criterion} " 
    - Test tasks:  " Test: {test scenario} " 

 3. Present preview to user:
   

Auto-Generated Sub-tasks for STORY-{ID}:

Implementation: 1. Implement {component_1} 2. Implement {component_2} 3. Create {API endpoint}

Validation: 4. Validate: {AC-1 summary} 5. Validate: {AC-2 summary}

Testing: 6. Test: Unit tests for {component} 7. Test: Integration test for {flow}

Total: {count} sub-tasks

Create these sub-tasks? (y/n/edit)


 4. If confirmed:
   For each sub-task:
     a. Call helpers.md#Generate-Sub-task-Body with sub-task details
     b. Call helpers.md#Sync-Sub-task-to-ZenHub:
        - sub_task_title, sub_task_body, zh_story_id
     c. Collect zh_sub_task_id, issue number

5. Return array of created sub-tasks with ZenHub IDs

On failure: Log warning for each failed sub-task, continue with remaining

Generate Epic Body#

Purpose: Generate structured markdown body for Epic GitHub issues

Input: epic_name, epic_description, stories[] (list of story summaries),
       sprint_info (optional), architecture_context (optional)

Output: Formatted markdown string

Template:
  ## Epic: {epic_name}

  ### Description
  {epic_description}

  ### Stories
  | # | Story | Points | Priority |
  |---|-------|--------|----------|
  | STORY-{id} | {title} | {points} | {priority} |
  ...

  ### Sprint
  - Sprint: {sprint_name}
  - Start: {start_date}
  - End: {end_date}

  ### Architecture Context
  {architecture_summary โ€” relevant components/layers}

  ### Acceptance Criteria
  - [ ] All stories completed and merged
  - [ ] Integration tested across story boundaries
  - [ ] Epic branch merged to main

  ---
  *Generated by BMAD Method v6*

Generate Story Body#

Purpose: Generate structured markdown body for Story GitHub issues

Input: story (parsed story document or sprint plan entry)

Output: Formatted markdown string

Template:
  ## {user_story_statement}

  ### Description
  {description_background_scope}

  ### Acceptance Criteria
  - [ ] {criterion_1}
  - [ ] {criterion_2}
  ...

  ### Technical Notes
  {technical_notes_summary}

  ### Dependencies
  {dependency_list_or_none}

  ### Story Points: {points}

  ### Definition of Done
  - [ ] Code implemented on story branch
  - [ ] Unit tests passing ( > =80% coverage)
  - [ ] Code review approved
  - [ ] Acceptance criteria validated
  - [ ] PR merged to epic branch

  ---
  *Generated by BMAD Method v6*

Generate Sub-task Body#

Purpose: Generate structured markdown body for Sub-task GitHub issues

Input: sub_task_title, sub_task_type (implementation|validation|test),
       parent_story_id, context (relevant technical details)

Output: Formatted markdown string

Template:
  ## Sub-task: {sub_task_title}

  **Parent Story:** STORY-{parent_story_id}
  **Type:** {sub_task_type}

  ### Description
  {context_description}

  ### Acceptance Criteria
  - [ ] {specific_criterion_for_this_sub_task}

  ### Notes
  {implementation_hints_or_test_scenarios}

  ---
  *Generated by BMAD Method v6*

Sync Story Dependencies to ZenHub#

Purpose: Create blocking relationships between stories in ZenHub

Input: dependency_map (array of {blocking_story_id, blocked_story_id})
Requires: zh_available = true

Steps:
1. For each dependency in dependency_map:
   - Resolve local story IDs to zh_issue_ids (from cross-reference)
   - Call createBlockage:
     - blockingIssueId: zh_id of blocking story
     - blockedIssueId: zh_id of blocked story

2. Log each dependency created
3. Skip if either story has no zh_issue_id (warn and continue)

Store ZenHub Cross-Reference#

Purpose: Add ZenHub metadata to local documents for traceability

Input: local_doc_path, zh_issue_id, zh_issue_number, zh_issue_url

Steps:
1. If local story document exists (.bmad/stories/STORY-{ID}.md):
   - Add ZenHub reference section or update existing:
     **ZenHub:** #{zh_issue_number} ({zh_issue_url})

2. If sprint-status.yaml exists:
   - Find story entry by story_id
   - Add/update fields:
     zh_issue_id:  " {zh_issue_id} " 
      zh_issue_number: {zh_issue_number}
     zh_issue_url:  " {zh_issue_url} "

Move Pipeline with Context#

Purpose: Move a ZenHub issue to a pipeline by name with error handling

Input: zh_issue_id, pipeline_name (e.g.,  " In Progress " ,  " Review/QA " )
Note: The Done pipeline is not used. Completion is handled via GitHub Close.
Requires: zh_available = true, zh_pipelines map
โš ๏ธ zh_pipelines ๋Š” ๋ผ์ด๋ธŒ 6์ข… ์ „๋ถ€๋กœ ์‹œ๋“œํ•ด์•ผ ํ•œ๋‹ค(New Issues, Icebox, Product Backlog,
   Sprint Backlog, In Progress, Review/QA). ๋กœ๋“œ ์‹œ 6๊ฐœ ๋ชจ๋‘ ์กด์žฌํ•˜๋Š”์ง€ assert.

Steps:
1. Resolve pipeline_name to pipeline_id (fail-closed):
   pipeline_id = zh_pipelines[pipeline_name]
   If not found: RAISE `Pipeline  ' {pipeline_name} '   ๋ฏธํ•ด์„. ๋ผ์ด๋ธŒ: {zh_pipelines keys}`.
   (warn+return ์œผ๋กœ ๋ฌด์Œ skip ํ•˜์ง€ ๋ง ๊ฒƒ โ€” ์ด์Šˆ๊ฐ€ ์—‰๋šฑํ•œ ์ปฌ๋Ÿผ์— ์•ˆ์ฐฉํ•ด๋„ ๋กœ๊ทธ๋งŒ ๋‚จ๋Š”๋‹ค.)

2. Call moveIssueToPipeline:
   - issueId: zh_issue_id
   - pipelineId: pipeline_id

3. Log:  " โœ“ ZenHub: #{issue_number} โ†’ {pipeline_name} " 

 On failure (MCP error):
  - Retry with backoff (small bounded count).
  - ์ง€์† ์‹คํŒจ ์‹œ: ํ•ด๋‹น ์ด์Šˆ๋ฅผ unsynced ๋กœ ํ‘œ์‹œํ•ด ํ›„์† sweep ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๊ณ  ์‚ฌ์šฉ์ž์—๊ฒŒ surface.
    ๋ฌด์Œ best-effort ๊ธˆ์ง€ โ€” date-setting ๋“ฑ ๋‹ค๋ฅธ ๊ฒฝ๋กœ๊ฐ€ fail-closed ์ธ๋ฐ move ๋งŒ fail-open ์ด๋ฉด
    ๊ฐ€๋“œ ์ฒ ํ•™์ด ๋ถˆ์ผ์น˜ํ•œ๋‹ค.

Template Operations#

Load Template#

Purpose: Load document template for workflow

Using Read tool:
1. Read template from: ~/.claude/config/bmad/templates/{workflow-name}.md (user config โ€” ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์„ค์น˜ํ•œ ์ปค์Šคํ…€ ํ…œํ”Œ๋ฆฟ, `cc-product:builder` ์Šคํ‚ฌ ์ฐธ๊ณ )
2. Store template content
3. Extract variable placeholders: {{variable_name}}

Apply Variables to Template#

Purpose: Substitute {{variables}} with actual values

Process:
1. For each variable in template:
   - {{project_name}} โ†’ from config
   - {{date}} โ†’ current date (YYYY-MM-DD)
   - {{timestamp}} โ†’ current ISO timestamp
   - {{user_name}} โ†’ from global config
   - {{custom_var}} โ†’ from user input
2. Replace all {{variable}} with values
3. Return completed document

Save Output Document#

Purpose: Write completed document to output folder

Using Write tool:
1. Determine output path:
   - {output_folder}/{workflow-name}-{project-name}-{date}.md
   - Example: .bmad/prd-myapp-2025-01-11.md
2. Write content to path
3. Return file path for status update

Variable Substitution#

Standard Variables#

{{project_name}}           โ†’ config: project_name
{{project_type}}           โ†’ config: project_type
{{project_level}}          โ†’ config: project_level
{{user_name}}              โ†’ config: user_name
{{date}}                   โ†’ current date (YYYY-MM-DD)
{{timestamp}}              โ†’ current timestamp (ISO 8601)
{{output_folder}}          โ†’ config: output_folder

Conditional Variables#

{{PRD_STATUS}}             โ†’  " required "   if level  > = 2, else  " recommended " 
 {{TECH_SPEC_STATUS}}       โ†’  " required "   if level  < = 1, else  " optional " 
 {{ARCHITECTURE_STATUS}}    โ†’  " required "   if level  > = 2, else  " optional "

Level-Based Logic#

Level 0 (1 story):         PRD optional, tech-spec required, no architecture
Level 1 (1-10 stories):    PRD recommended, tech-spec required, no architecture
Level 2 (5-15 stories):    PRD required, tech-spec optional, architecture required
Level 3 (12-40 stories):   PRD required, tech-spec optional, architecture required
Level 4 (40+ stories):     PRD required, tech-spec optional, architecture required

Workflow Recommendations#

Determine Next Workflow#

Input: workflow_status array
Output: recommended next workflow

Logic:
1. If no product-brief and project new โ†’ Recommend: /product-brief
2. If product-brief complete, no PRD/tech-spec โ†’ Recommend based on level:
   - Level 0-1: /tech-spec
   - Level 2+: /prd
3. If PRD/tech-spec complete, no architecture, level 2+ โ†’ Recommend: /architecture
4. If architecture complete (or not required) โ†’ Recommend: /sprint-planning
5. If sprint active โ†’ Recommend: /create-story or /dev-story

Status Display Format#

โœ“ = Completed (green)
โš  = Required but not started (yellow)
โ†’ = Current phase indicator
- = Optional/not required

Example:
โœ“ Phase 1: Analysis
  โœ“ product-brief (.bmad/product-brief-myapp-2025-01-11.md)
  - research (optional)

โ†’ Phase 2: Planning [CURRENT]
  โš  prd (required - NOT STARTED)
  - tech-spec (optional)

Phase 3: Solutioning
  - architecture (required)

Path Resolution#

Resolve Project Root#

Method: Use environment or detect
- Claude Code provides working directory
- Use `{project-root}` as placeholder
- Replace at runtime with actual path

Resolve Config Paths#

~/.claude/config/bmad/config.yaml           โ†’ Global config
{project-root}/bmad/config.yaml             โ†’ Project config
{project-root}/{output_folder}              โ†’ Output directory (usually .bmad/)

Resolve Template Paths#

~/.claude/config/bmad/templates/{name}.md   โ†’ Template files (user config)

Error Handling#

File Not Found#

If config file missing:
  - Use defaults
  - Prompt user to run /workflow-init

If status file missing:
  - Inform user project not initialized
  - Offer to run /workflow-init

If template missing:
  - Use inline template
  - Log warning

Invalid YAML#

If YAML parse error:
  - Show error message
  - Provide file path
  - Suggest manual fix or reinit

Token Optimization Tips#

Reference vs. Embed#

โœ“ Good:  " Follow helper instructions in utils/helpers.md#Load-Global-Config " 
 โœ— Bad: Embed full instructions in every command

โœ“ Good:  " Use standard variables from helpers.md#Standard-Variables " 
 โœ— Bad: List all variables in every template

Lazy Loading#

โœ“ Good: Load config only when needed
โœ— Bad: Load all files upfront

โœ“ Good: Read status file when checking progress
โœ— Bad: Keep status in memory throughout chat

Reuse Patterns#

โœ“ Good:  " Execute Step 1-3 from helpers.md#Combined-Config-Load " 
 โœ— Bad: Repeat config loading steps in every workflow

Quick Reference Commands#

For Skills/Commands#

To load config: See helpers.md#Combined-Config-Load
To check status: See helpers.md#Load-Workflow-Status
To update status: See helpers.md#Update-Workflow-Status
To use template: See helpers.md#Load-Template + helpers.md#Apply-Variables-to-Template
To save output: See helpers.md#Save-Output-Document
To recommend next: See helpers.md#Determine-Next-Workflow
To load ZenHub conventions: See helpers.md#Load-ZenHub-Conventions
To init ZenHub: See helpers.md#Load-ZenHub-Context
To sync epic: See helpers.md#Sync-Epic-to-ZenHub
To sync story: See helpers.md#Sync-Story-to-ZenHub
To sync sub-task: See helpers.md#Sync-Sub-task-to-ZenHub
To auto-generate sub-tasks: See helpers.md#Auto-Generate-Sub-tasks
To generate epic body: See helpers.md#Generate-Epic-Body
To generate story body: See helpers.md#Generate-Story-Body
To generate sub-task body: See helpers.md#Generate-Sub-task-Body
To sync deps: See helpers.md#Sync-Story-Dependencies-to-ZenHub
To store xref: See helpers.md#Store-ZenHub-Cross-Reference
To resolve branches: See helpers.md#Resolve-Branch-Names
To create branch hierarchy: See helpers.md#Create-Branch-Hierarchy
To create task branch: See helpers.md#Create-Task-Branch
To create PR: See helpers.md#Create-PR-and-Merge
To move pipeline: See helpers.md#Move-Pipeline-with-Context
To check Agent Teams: See helpers.md#Check-Agent-Teams-Available
To spawn teammate: See helpers.md#Spawn-BMAD-Teammate
To create team tasks: See helpers.md#Create-Team-Task-List
To collect results: See helpers.md#Collect-Team-Results
To run quality gate: See helpers.md#Team-Quality-Gate

Agent Teams Integration#

Check Agent Teams Available#

Purpose: Detect if Claude Code Agent Teams feature is available

Steps:
1. Check environment variable:
   CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS

2. If set and truthy:
   - Set teams_available = true
   - Log:  " โœ“ Agent Teams available (experimental) " 

 3. If NOT set or falsy:
   - Set teams_available = false
   - Log:  " โš  Agent Teams not available. Use sequential workflows instead. " 
    - Provide guidance:
      " To enable Agent Teams, set the environment variable:
      CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
      Then restart Claude Code. " 

 4. Return teams_available flag

Spawn BMAD Teammate#

Purpose: Launch a teammate with role-specific context and constraints

Input: role (developer|reviewer|story-creator), context_payload,
       effort_tier (frugal|standard|frontier) โ€” REQUIRED, never left unset
Requires: teams_available = true

effort_tier = the `tier:` field every agent-spawning node must declare
(SoT: cc-dev/rules/orchestration-graph.md โ†’ ยง5 Fan-out Obligations;
 tier โ†’ model mapping: cc-dev/skills/agent-teams/SKILL.md โ†’ Effort Routing Convention):

  Per-role defaults โ€” applied AND logged when the caller omits effort_tier:
    developer      โ†’ standard   (implementation/test level reasoning)
    reviewer       โ†’ standard   (teammate returns findings; the Lead composes the verdict)
    story-creator  โ†’ standard   (template-driven authoring)
    mechanical assignments (status transitions, file moves, readiness checks) โ†’ frugal
    frontier       โ†’ only when the teammate ' s OWN verdict gates the pipeline
                     (e.g. a Solutioning Gate reviewer). State the reason at the call site.

  - unset = silent inherit. This helper is the largest fan-out surface in the repo
    (3~5 teammates per spawn), so an unset tier is not  " a default "   โ€” it is an Nร—
    multiplier on the caller ' s tier.
  - If effort_tier is absent AND the role has no default above, or the value is not one of
    frugal|standard|frontier โ†’ ABORT the spawn. Do NOT inherit.
  - Record the resolved tier in the task description so Collect-Team-Results can report it.

Role-specific prompt construction:

--- developer role ---
Prompt:
   " You are a BMAD Developer teammate. Your assignment:

   ## Story
   {story_document_content}

   ## Architecture Summary
   {architecture_key_sections}

   ## File Ownership
   You MUST only modify files in these paths:
   {owned_file_paths}
   Do NOT modify any files outside your ownership boundary.

   ## Branch (already checked out โ€” do NOT switch)
   Work on branch: {story_branch}
   Epic branch: {epic_branch}

   ## Quality Standards
   - All acceptance criteria must pass
   - Write tests alongside implementation
   - Run lint/typecheck before completing
   - Follow existing project conventions

   ## Constraints
   - Do NOT modify sprint-status.yaml (Lead only)
   - Report completion via task update
   - If blocked, update task with blocker description " 

 --- reviewer role ---
Prompt:
   " You are a BMAD Reviewer teammate. Your assignment:

   ## Review Target
   Document: {document_path}
   Type: {document_type}

   ## Review Perspective
   Role: {review_perspective} (PM|Architect|Developer|Scrum Master)
   Focus: {review_focus_description}

   ## Checklist
   {review_checklist_items}

   ## Output
   Write your review to: {review_output_path}
   Format:
   - Overall Assessment: (Pass/Conditional Pass/Fail)
   - Strengths: (bullet list)
   - Issues: (numbered, with severity: Critical/Major/Minor)
   - Recommendations: (bullet list)

   ## Constraints
   - Do NOT modify the reviewed document
   - Do NOT modify sprint-status.yaml (Lead only)
   - Report completion via task update " 

 --- story-creator role ---
Prompt:
   " You are a BMAD Story Creator teammate. Your assignment:

   ## Epic
   {epic_name}: {epic_description}

   ## Stories to Create
   {story_id_list_with_titles}

   ## Story Template
   Follow the standard BMAD story template:
   - User Story (As a... I want... So that...)
   - Description (Background, Scope, User Flow)
   - Acceptance Criteria (testable, specific)
   - Technical Notes (components, APIs, DB changes)
   - Dependencies
   - Definition of Done
   - Story Points (Fibonacci: 1,2,3,5,8,13)

   ## Architecture Context
   {architecture_key_sections}

   ## Output
   Write each story to: .bmad/stories/{story_id}.md
   Each teammate creates DIFFERENT story files โ€” no conflicts.

   ## Constraints
   - Do NOT modify sprint-status.yaml (Lead only)
   - Do NOT modify other teammates '   story files
   - Report completion via task update " 

 Common footer appended to ALL roles:
   " IMPORTANT CONSTRAINTS:
   - Never modify sprint-status.yaml โ€” only the Lead updates status files
   - Never change HEAD: no git checkout / git switch / git reset --hard / git stash in this
     worktree. You share ONE worktree with your siblings โ€” substrate row  ' Agent Teams '   of
     cc-dev/rules/orchestration-graph.md โ†’ ยง4 Substrate Selection: same worktree, no
     per-teammate branches โ€” and changing HEAD destroys their uncommitted work.
     This binds EVERY agent, the Lead included (its quality gate too), for as long as any
     teammate is live. Need an isolated tree? `git worktree add --detach` a throwaway one
     and remove it afterwards.
   - Your effort tier for this assignment: {effort_tier}
   - Complete your work and mark your task as completed
   - If you encounter issues, describe them in your task update "

Create Team Task List#

Purpose: Create shared task list for teammate coordination

Input: assignments (array of {teammate_role, description, context})
Output: task_ids array

Steps:
1. For each assignment:
   a. Call TaskCreate:
      - subject:  " {role}: {short_description} " 
       - description: Full assignment details including file ownership
      - activeForm:  " {role_verb} {short_description} " 
    b. Store returned task_id

2. Set up dependencies (if any):
   For each dependency pair (blocking_task_id, blocked_task_id):
     Call TaskUpdate:
       - taskId: blocked_task_id
       - addBlockedBy: [blocking_task_id]

3. Return array of task_ids for monitoring

Example:
  TaskCreate:  " developer: Implement STORY-001 user registration " 
   TaskCreate:  " developer: Implement STORY-002 user login " 
   TaskUpdate: task_2 addBlockedBy [task_1]  (if STORY-002 depends on STORY-001)

Collect Team Results#

Purpose: BARRIER โ€” block until every team task is terminal, then gather and summarize outputs

Input: task_ids (from Create-Team-Task-List), poll_interval, wall_clock_cap
Output: results summary โ€” only when every task_id is terminal (completed|failed)

This is a loop, not a single poll. A one-shot poll that logs  " still in progress "   and returns
hands the caller a MOVING tree: Team-Quality-Gate then runs against work that siblings are
still writing.

Loop contract โ€” contract:L-CTR (agent-teams.collect-results-barrier).
Field definitions: cc-dev/rules/orchestration-graph.md โ†’ ยง2 Loop Contract.
The values below live here, at the loop ' s own site:

  inv:      the task_ids set is fixed for the whole barrier (no task added mid-barrier);
            no agent changes HEAD while the barrier is open
  prog:     pending = count(task_ids not terminal), monotonically non-increasing
            no-prog: pending unchanged for 5 consecutive iterations โ†’ do NOT keep spending
            budget on the same poll; escalate per the 3-rung stall ladder
            (cc-dev/agents/sequential-workflow.md) and surface the stalled task_ids
  term:     pending == 0
  budget:   poll_interval 60s, wall_clock_cap 45min โ†’ max 45 iterations.
            Single entry per team run; no re-entry (a second call starts a new barrier).
            Number selection SoT: cc-dev/skills/job-timeout-budget/SKILL.md
  exhaust:  on cap โ†’ list the still-running task_ids and ABORT the barrier. The caller MUST
            NOT run Team-Quality-Gate on partial work; gating a moving tree is worse than
            not gating. Never  " warn and continue " .
  resume:   TaskList is the position โ€” task status is the durable state, not an in-session
            counter (a counter does not survive /clear). Re-polling is idempotent.
  log:      one line per iteration:  " barrier #3: pending 2/4 (task_7, task_9), 12m/45m " 
             plus a durable record of the abort and of every task dropped as failed

Steps:
1. Barrier loop (repeat until term: or exhaust:):
   a. Call TaskList, filter to team task_ids
   b. Classify each task: completed | failed | in_progress
   c. If pending  >   0 AND elapsed  <   wall_clock_cap:
      - Emit the log: line above
      - Wait poll_interval, then repeat from (a)
   d. If pending  >   0 AND elapsed  > = wall_clock_cap:
      - Log:  " โ›” Barrier timed out at {elapsed}. Still running: {pending_task_ids} " 
       - Return { barrier:  " timeout " , pending_task_ids, summary: null } and ABORT
        (caller: do NOT run Team-Quality-Gate, do NOT report partial work as complete)
   e. If pending == 0: fall through to step 2

2. For each completed task:
   a. Call TaskGet(task_id) to retrieve final details
   b. Identify output artifacts:
      - developer: Modified files, test results
      - reviewer: Review document at {review_output_path}
      - story-creator: Story documents in .bmad/stories/
   c. Record the effort_tier the teammate ran at (from Spawn-BMAD-Teammate)

3. For each failed task:
   - Record it durably as dropped: { task_id, task_subject, reported_blocker }
   - It counts toward pending == 0, but it is NOT an artifact โ€” the summary must show it

4. Build integrated summary:
   Barrier: closed ({total}/{total} terminal)
   Completed: {count}/{total}   Failed: {count}/{total}
   Artifacts:
     - {file_path_1}: {description}
     - {file_path_2}: {description}
   Issues Reported:
     - {task_id}: {issue_description} (if any)

5. Return summary object (barrier:  " closed " )

Team Quality Gate#

Purpose: Run automated quality checks on teammate work โ€” in place, without touching HEAD

Input: story_branch (branch to validate), epic_branch (diff base)
Output: { verdict:  " pass " | " fail " | " undetermined " , passed: boolean, results: object }

Run at most ONE gate at a time. commands/team-dev.md Part 6 calls this on each teammate completion,
so serialize the calls โ€” do not gate several teammates in parallel.

Steps:
1. Detect project type:
   - Check for package.json โ†’ Node.js/TypeScript
   - Check for pubspec.yaml โ†’ Flutter/Dart
   - Check for requirements.txt/pyproject.toml โ†’ Python
   - Check for Cargo.toml โ†’ Rust
   - Check for go.mod โ†’ Go

2. Validate IN PLACE โ€” never change HEAD:
   โ›” Do NOT run `git checkout {story_branch}` here. commands/team-dev.md Part 6 calls this gate on
      EACH teammate completion while the siblings are still editing the SAME worktree
      (substrate row  ' Agent Teams '   of cc-dev/rules/orchestration-graph.md โ†’ ยง4 Substrate
      Selection: one worktree, no per-teammate branches). A global checkout yanks the tree
      out from under live teammates and their uncommitted work is gone.
   a. Assert, do not switch:
      git rev-parse --abbrev-ref HEAD == {story_branch}
      If it differs โ†’ the gate is undetermined (step 5c). Report it; do NOT switch.
   b. Scope the checks to what the teammate actually changed:
      CHANGED_FILES = git diff --name-only {epic_branch}...HEAD (exclude generated files)
      Scoped analysis is also what Step 8.5 does โ€” see phase-gates.md โ†’ Step 8.5 Gate.
   c. Only if a check genuinely needs an isolated tree (e.g. a full test run that rewrites
      build output), use a throwaway detached worktree and remove it afterwards:
        git worktree add --detach {tmp_dir} {story_branch}
        run the checks inside {tmp_dir}
        git worktree remove {tmp_dir}
      Give {tmp_dir} a unique path so its runtime slot does not collide with the shared
      worktree (cc-dev/skills/parallel-test-env/SKILL.md; slots derive deterministically
      from worktree path/branch โ€” orchestration-graph.md โ†’ ยง4.3).

3. Run quality checks based on project type (scoped to CHANGED_FILES where the tool allows):

   Node.js/TypeScript:
     lint: npm run lint (or npx eslint .)
     typecheck: npx tsc --noEmit (if tsconfig.json exists)
     test: npm test

   Flutter/Dart:
     lint: dart analyze (or melos run analyze)
     dcm: dcm analyze --no-fatal-style (or melos run dcm:analyze)
     format: dart format --set-exit-if-changed .
     test: flutter test (or melos run test)
     typecheck: NOT DEFINED for Dart โ€” the analyzer covers it. Record it as skipped,
                never as passed.

   Python:
     lint: pylint src/ (or ruff check .)
     typecheck: mypy src/ (if mypy installed)
     test: pytest

4. Collect results โ€” one state per check, not a boolean:
   state โˆˆ passed | failed | skipped | unknown
     skipped = the check is not defined for this project type (record the reason)
     unknown = defined but could not be judged: tool missing, command errored, HEAD
               mismatch, or a piped exit code that hid the real one
   lint: { state, output_summary }
   dcm: { state, output_summary }            (Flutter/Dart)
   typecheck: { state, output_summary }
   tests: { state, count, coverage }

5. Determine gate verdict โ€” tri-state, per cc-dev/rules/orchestration-graph.md โ†’ ยง3 Gate
   Contract:
   a. verdict =  " pass "   iff EVERY check DEFINED for this project type is `passed`
      and no check is `unknown`. Also log which checks were skipped (step 5d).
   b. verdict =  " fail "   if any defined check is `failed`.
   c. verdict =  " undetermined "   if any defined check is `unknown` (incl. the HEAD mismatch
      of step 2a). undetermined does NOT pass โ€” ยง3 rule 2: ํŒ์ • ๋ถˆ๊ฐ€ โ‰  ํ†ต๊ณผ. Report it to
      the Lead exactly like a failure. Only genuine tool absence may be relaxed to warn
      (ยง3.1 case 3), and only with the skip recorded in the returned object and in the
      durable gate log โ€” console-only is not durable.
   d. ALWAYS log the skipped set with reasons, e.g.
       " Skipped: typecheck (not defined for Flutter/Dart โ€” the analyzer covers it) " 
       A silent skip is the fail-open type of the same name (ยง3.2, GD-01): the check
      disappears and nobody learns the gate got smaller.
   โ›” Never `passed = lint_passed AND typecheck_passed AND tests_passed`. `typecheck` is
      undefined for Dart, so that formula either ANDs an undefined value into a phantom
      failure or reads  " not applicable "   as  " not failing "   โ€” fail-open type null-as-pass
      (ยง3.2, FO-04). It also never covered dcm at all.

6. Return:
   {
     verdict:  " pass "   |  " fail "   |  " undetermined " ,
     passed: (verdict ===  " pass " ),   // kept for existing callers; only  " pass "   is true
     lint: { state, output_summary },
     dcm: { state, output_summary },
     typecheck: { state, output_summary },
     tests: { state, count, coverage },
     skipped: [ { check, reason } ],
     branch: story_branch,           // asserted, never switched to
     head_unchanged: true
   }

On fail or undetermined:
  Log:  " โŒ Quality gate {verdict} for {story_branch} " 
   Log details of each failed/unknown check, and the skipped set
  Return passed = false (do NOT auto-fix โ€” report to Lead for decision)
  The fix โ†’ re-gate cycle the Lead may start is a loop owned by the CALLER: it declares
  its own bound: + invalidates: at its call site (orchestration-graph.md โ†’ ยง2, ยง7 rule 4).
  This helper does not retry.