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
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. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ExitHelper 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)
}