Entropyk/generate_status.py

188 lines
6.3 KiB
Python

import os
import re
from datetime import datetime
import json
epics_file = '_bmad-output/planning-artifacts/epics.md'
status_file = '_bmad-output/implementation-artifacts/sprint-status.yaml'
story_location = '_bmad-output/implementation-artifacts'
def to_kebab_case(s):
# keep alphanumeric, spaces and hyphens
s = re.sub(r'[^a-zA-Z0-9\s-]', '', s)
s = re.sub(r'[\s]+', '-', s.strip()).lower()
return re.sub(r'-+', '-', s)
epics = []
current_epic = None
with open(epics_file, 'r', encoding='utf-8') as f:
for line in f:
epic_match = re.match(r'^## Epic (\d+): (.*)', line)
if epic_match:
epics.append({
'num': epic_match.group(1),
'title': epic_match.group(2).strip(),
'stories': []
})
current_epic = epics[-1]
continue
story_match = re.match(r'^### Story (\d+)\.(\d+):\s*(.*)', line)
if story_match and current_epic:
title = story_match.group(3).strip().replace('*', '')
kebab_title = to_kebab_case(title)
story_key = f"{story_match.group(1)}-{story_match.group(2)}-{kebab_title}"
current_epic['stories'].append(story_key)
existing_status = {}
if os.path.exists(status_file):
with open(status_file, 'r', encoding='utf-8') as f:
in_dev_status = False
for line in f:
line = line.strip('\n')
if line.startswith('development_status:'):
in_dev_status = True
continue
if in_dev_status and ':' in line and not line.strip().startswith('#'):
parts = line.split(':', 1)
existing_status[parts[0].strip()] = parts[1].strip()
def upgrade_status(current, new_status):
order = ['backlog', 'ready-for-dev', 'in-progress', 'review', 'done', 'completed']
try:
curr_idx = order.index(current)
except ValueError:
curr_idx = -1
try:
new_idx = order.index(new_status)
except ValueError:
new_idx = -1
return order[max(curr_idx, new_idx)] if max(curr_idx, new_idx) >= 0 else new_status
# Compute statuses
computed_statuses = {}
epic_count = 0
story_count = 0
done_count = 0
epics_in_progress_count = 0
for epic in epics:
epic_num = epic['num']
epic_key = f"epic-{epic_num}"
epic_count += 1
epic_stat = existing_status.get(epic_key, 'backlog')
if epic_stat == 'completed': epic_stat = 'done'
story_stats = {}
any_story_started = False
all_stories_done = True
if len(epic['stories']) == 0:
all_stories_done = False
for story_key in epic['stories']:
story_count += 1
stat = existing_status.get(story_key, 'backlog')
if stat == 'completed': stat = 'done'
story_md_path = os.path.join(story_location, f"{story_key}.md")
if os.path.exists(story_md_path):
stat = upgrade_status(stat, 'ready-for-dev')
any_story_started = True
if stat in ['in-progress', 'review', 'done']:
any_story_started = True
if stat != 'done':
all_stories_done = False
else:
done_count += 1
story_stats[story_key] = stat
if any_story_started and epic_stat == 'backlog':
epic_stat = 'in-progress'
if all_stories_done and epic_stat in ['backlog', 'in-progress']:
epic_stat = 'done'
if epic_stat == 'in-progress':
epics_in_progress_count += 1
computed_statuses[epic_key] = epic_stat
for k, v in story_stats.items():
computed_statuses[k] = v
retro_key = f"epic-{epic_num}-retrospective"
computed_statuses[retro_key] = existing_status.get(retro_key, 'optional')
lines = [
"# Sprint Status - Entropyk",
f"# Last Updated: {datetime.now().strftime('%Y-%m-%d')}",
"# Project: Entropyk",
"# Project Key: NOKEY",
"# Tracking System: file-system",
f"# Story Location: {story_location}",
"",
"# STATUS DEFINITIONS:",
"# ==================",
"# Epic Status:",
"# - backlog: Epic not yet started",
"# - in-progress: Epic actively being worked on",
"# - done: All stories in epic completed",
"#",
"# Epic Status Transitions:",
"# - backlog → in-progress: Automatically when first story is created (via create-story)",
"# - in-progress → done: Manually when all stories reach 'done' status",
"#",
"# Story Status:",
"# - backlog: Story only exists in epic file",
"# - ready-for-dev: Story file created in stories folder",
"# - in-progress: Developer actively working on implementation",
"# - review: Ready for code review (via Dev's code-review workflow)",
"# - done: Story completed",
"#",
"# Retrospective Status:",
"# - optional: Can be completed but not required",
"# - done: Retrospective has been completed",
"#",
"# WORKFLOW NOTES:",
"# ===============",
"# - Epic transitions to 'in-progress' automatically when first story is created",
"# - Stories can be worked in parallel if team capacity allows",
"# - SM typically creates next story after previous one is 'done' to incorporate learnings",
"# - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended)",
"",
f"generated: {datetime.now().strftime('%Y-%m-%d')}",
"project: Entropyk",
"project_key: NOKEY",
"tracking_system: file-system",
f"story_location: {story_location}",
"",
"development_status:"
]
for epic in epics:
epic_num = epic['num']
epic_key = f"epic-{epic_num}"
lines.append(f" # Epic {epic_num}: {epic['title']}")
lines.append(f" {epic_key}: {computed_statuses[epic_key]}")
for story_key in epic['stories']:
lines.append(f" {story_key}: {computed_statuses[story_key]}")
retro_key = f"epic-{epic_num}-retrospective"
lines.append(f" {retro_key}: {computed_statuses[retro_key]}")
lines.append("")
with open(status_file, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
print(json.dumps({
"file": status_file,
"epic_count": epic_count,
"story_count": story_count,
"epics_in_progress": epics_in_progress_count,
"done_count": done_count
}, indent=2))