BMAD-METHOD/src/modules/bmm/workflows/po/epic-dashboard/instructions.md

8.9 KiB

Epic Dashboard - Enterprise Progress Visibility

The workflow execution engine is governed by: {project-root}/_bmad/core/tasks/workflow.xml You MUST have already loaded and processed: {installed_path}/workflow.yaml

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📊 EPIC DASHBOARD ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Call: mcp__github__get_me()

GitHub MCP not accessible - cannot fetch epic data HALT Query all epics: Call: mcp__github__search_issues({ query: "repo:{{github_owner}}/{{github_repo}} label:type:epic is:open" }) Query specific epic: Call: mcp__github__search_issues({ query: "repo:{{github_owner}}/{{github_repo}} label:epic:{{epic_key}}" })

epics = response.items

No epics found{{#if epic_key}} for epic:{{epic_key}}{{/if}}.

Tip: Create epics as GitHub Issues with label type:epic HALT

For each epic, fetch stories: for epic in epics: epic_label = extract_epic_key(epic) # e.g., "epic:2"

Fetch all stories for this epic

stories_response = await mcp__github__search_issues({ query: "repo:{{github_owner}}/{{github_repo}} label:type:story label:{{epic_label}}" })

epic.stories = stories_response.items

Calculate metrics

epic.metrics = { total: epic.stories.length, done: count_by_label(epic.stories, "status:done"), in_review: count_by_label(epic.stories, "status:in-review"), in_progress: count_by_label(epic.stories, "status:in-progress"), backlog: count_by_label(epic.stories, "status:backlog"), blocked: count_by_label(epic.stories, "priority:blocked") }

epic.metrics.progress = (epic.metrics.done / epic.metrics.total * 100).toFixed(0) + "%" epic.metrics.active_work = epic.metrics.in_progress + epic.metrics.in_review

for epic in epics: epic.risks = []

Check for stale in-progress stories (no update in 24h)

for story in epic.stories: if has_label(story, "status:in-progress"): hours_since_update = calculate_hours_since(story.updated_at) if hours_since_update > 24: epic.risks.push({ story: story, risk: "stale", message: "No activity for " + hours_since_update + "h" })

Check for blocked stories

for story in epic.stories: if has_label(story, "priority:blocked"): epic.risks.push({ story: story, risk: "blocked", message: "Story blocked - needs attention" })

Check for stories in review too long (>48h)

for story in epic.stories: if has_label(story, "status:in-review"): hours_since_update = calculate_hours_since(story.updated_at) if hours_since_update > 48: epic.risks.push({ story: story, risk: "review-delayed", message: "In review for " + hours_since_update + "h" })

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📊 EPIC OVERVIEW ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

{{#each epics}} ┌─────────────────────────────────────────┐ │ EPIC {{epic_key}}: {{title}} ├─────────────────────────────────────────┤ │ Progress: [{{progress_bar}}] {{metrics.progress}} │ │ Stories: {{metrics.total}} total │ Done: {{metrics.done}} │ 👀 In Review: {{metrics.in_review}} │ 🔨 In Progress: {{metrics.in_progress}} │ 📋 Backlog: {{metrics.backlog}} │ 🚫 Blocked: {{metrics.blocked}} │ {{#if risks.length}} │ ⚠️ RISKS: {{risks.length}} {{#each risks}} │ • {{story.story_key}}: {{message}} {{/each}} {{/if}} └─────────────────────────────────────────┘

{{/each}}

for epic in epics: # Get closed stories with timestamps closed_stories = filter(epic.stories, has_label("status:done"))

Group by completion date

completion_by_date = {} for story in closed_stories: date = format_date(story.closed_at) completion_by_date[date] = (completion_by_date[date] || 0) + 1

epic.burndown = { total_scope: epic.metrics.total, completed: epic.metrics.done, remaining: epic.metrics.total - epic.metrics.done, completion_history: completion_by_date }

<output>

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📈 BURNDOWN ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

{{#each epics}} Epic {{epic_key}}: {{burndown.completed}}/{{burndown.total_scope}} stories completed ({{burndown.remaining}} remaining)

Recent Completions: {{#each burndown.completion_history as |count date|}} {{date}}: {{count}} {{#if (gt count 1)}}stories{{else}}story{{/if}} completed {{/each}}

{{/each}}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📋 STORY DETAILS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

{{#each epics}}

Epic {{epic_key}}: {{title}}

{{#each stories}} | {{story_key}} | {{title}} | {{status}} | @{{assignee.login or "-"}} | {{/each}}

{{/each}}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Actions: [E] View specific Epic (enter epic key) [D] Toggle story Details [B] Toggle Burndown [R] Refresh data [Q] Quit

Choice:

Enter epic key (e.g., 2): Set epic_key = input Goto step 1 (refetch with filter) Toggle show_details Goto step 3 Toggle show_burndown Goto step 3 Goto step 1 (refresh) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Epic Dashboard closed. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Exit

Helper Functions

// Extract epic key from issue labels
function extract_epic_key(epic) {
  for (label of epic.labels) {
    if (label.name.startsWith("epic:")) {
      return label.name.replace("epic:", "")
    }
  }
  return epic.number.toString()
}

// Count stories with specific label
function count_by_label(stories, label_name) {
  return stories.filter(s =>
    s.labels.some(l => l.name === label_name)
  ).length
}

// Check if story has label
function has_label(story, label_name) {
  return story.labels.some(l => l.name === label_name)
}

// Calculate hours since timestamp
function calculate_hours_since(timestamp) {
  const diff = Date.now() - new Date(timestamp).getTime()
  return Math.floor(diff / (1000 * 60 * 60))
}

// Generate ASCII progress bar
function generate_progress_bar(percent, width = 20) {
  const filled = Math.floor(percent * width / 100)
  const empty = width - filled
  return '█'.repeat(filled) + '░'.repeat(empty)
}