# 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 } ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📈 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 ```javascript // 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) } ```