# 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
Call: mcp__github__get_me()
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
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"
})
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
}
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)
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)
}
```