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))