188 lines
6.3 KiB
Python
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))
|