From 1ddf2afceae3b2677a461ffcb36618ffd4566219 Mon Sep 17 00:00:00 2001 From: Jonah Schulte Date: Tue, 6 Jan 2026 08:21:44 -0500 Subject: [PATCH] feat: enhance sprint scripts with smart path resolution and conditional batching - Add auto-detection of project root and story directories - Enable scripts to work from any working directory - Add explicit path override options via CLI arguments - Improve error messages with suggested paths when files not found - Update smart batching to show options only when time_saved > 0 - Reduce decision fatigue by skipping batching menu when no benefit --- scripts/lib/add-status-fields.py | 229 +++++++++++--- scripts/lib/sprint-status-updater.py | 296 +++++++++++++----- .../steps/step-02-pre-gap-analysis.md | 79 ++++- 3 files changed, 465 insertions(+), 139 deletions(-) diff --git a/scripts/lib/add-status-fields.py b/scripts/lib/add-status-fields.py index 73dc67d2..e54b5311 100755 --- a/scripts/lib/add-status-fields.py +++ b/scripts/lib/add-status-fields.py @@ -2,16 +2,102 @@ """ Add Status field to story files that are missing it. Uses sprint-status.yaml as source of truth. +Smart path resolution - finds files regardless of working directory. + +Features: + - Auto-detects project root and file locations + - Works from any working directory + - Explicit path overrides via CLI arguments + - Clear error messages if files not found """ import re +import sys from pathlib import Path -from typing import Dict +from typing import Dict, Optional -def load_sprint_status(path: str = "docs/sprint-artifacts/sprint-status.yaml") -> Dict[str, str]: + +def find_project_root() -> Path: + """ + Find project root by looking for .git directory or other markers. + Works from any subdirectory. + """ + current = Path.cwd() + + # Try up to 10 levels up + for _ in range(10): + if (current / '.git').exists(): + return current + if (current / '.claude').exists(): + return current + current = current.parent + if current == current.parent: # Reached filesystem root + break + + # Fallback to current working directory + return Path.cwd() + + +def find_story_dir(project_root: Optional[Path] = None) -> Path: + """ + Auto-detect story directory location. + Tries multiple possible paths and returns the first one that exists. + """ + if project_root is None: + project_root = find_project_root() + + # Try paths in order of preference + candidates = [ + project_root / "_bmad-output" / "implementation-artifacts" / "sprint-artifacts", + project_root / "docs" / "sprint-artifacts", + project_root / "sprint-artifacts", + Path.cwd() / "sprint-artifacts", + ] + + for candidate in candidates: + if candidate.exists(): + return candidate + + # If none found, suggest the most likely paths + print(f"ERROR: Could not find story directory.", file=sys.stderr) + print(f"Tried:", file=sys.stderr) + for candidate in candidates: + print(f" - {candidate}", file=sys.stderr) + raise FileNotFoundError("Story directory not found") + + +def find_sprint_status(project_root: Optional[Path] = None, story_dir: Optional[Path] = None) -> Path: + """ + Auto-detect sprint-status.yaml location. + Looks in the story directory or its parent. + """ + if story_dir is None: + story_dir = find_story_dir(project_root) + + # Try paths in order of preference + candidates = [ + story_dir / "sprint-status.yaml", + story_dir.parent / "sprint-status.yaml", + ] + + for candidate in candidates: + if candidate.exists(): + return candidate + + # If none found, suggest the most likely paths + print(f"ERROR: Could not find sprint-status.yaml", file=sys.stderr) + print(f"Tried:", file=sys.stderr) + for candidate in candidates: + print(f" - {candidate}", file=sys.stderr) + raise FileNotFoundError("sprint-status.yaml not found") + + +def load_sprint_status(path: Path) -> Dict[str, str]: """Load story statuses from sprint-status.yaml""" - with open(path) as f: - lines = f.readlines() + if not path.exists(): + raise FileNotFoundError(f"sprint-status.yaml not found at: {path}") + + lines = path.read_text().split('\n') statuses = {} in_dev_status = False @@ -34,6 +120,7 @@ def load_sprint_status(path: str = "docs/sprint-artifacts/sprint-status.yaml") - return statuses + def add_status_to_story(story_file: Path, status: str) -> bool: """Add Status field to story file if missing""" content = story_file.read_text() @@ -41,11 +128,11 @@ def add_status_to_story(story_file: Path, status: str) -> bool: # Check if Status field already exists (handles both "Status:" and "**Status:**") if re.search(r'^\*?\*?Status:', content, re.MULTILINE | re.IGNORECASE): return False # Already has Status field - + # Find the first section after the title (usually ## Story or ## Description) # Insert Status field before that lines = content.split('\n') - + # Find insertion point (after title, before first ## section) insert_idx = None for idx, line in enumerate(lines): @@ -56,57 +143,111 @@ def add_status_to_story(story_file: Path, status: str) -> bool: # Found first section - insert before it insert_idx = idx break - + if insert_idx is None: # No ## sections found, insert after title insert_idx = 1 - + # Insert blank line, Status field, blank line lines.insert(insert_idx, '') lines.insert(insert_idx + 1, f'**Status:** {status}') lines.insert(insert_idx + 2, '') - + # Write back story_file.write_text('\n'.join(lines)) return True + def main(): - story_dir = Path("docs/sprint-artifacts") - statuses = load_sprint_status() - - added = 0 - skipped = 0 - missing = 0 - - for story_file in sorted(story_dir.glob("*.md")): - story_id = story_file.stem - - # Skip special files - if (story_id.startswith('.') or - story_id.startswith('EPIC-') or - 'COMPLETION' in story_id.upper() or - 'SUMMARY' in story_id.upper() or - 'REPORT' in story_id.upper() or - 'README' in story_id.upper()): - continue - - if story_id not in statuses: - print(f"âš ī¸ {story_id}: Not in sprint-status.yaml") - missing += 1 - continue - - status = statuses[story_id] - - if add_status_to_story(story_file, status): - print(f"✓ {story_id}: Added Status: {status}") - added += 1 + """Main entry point for CLI usage""" + import argparse + + parser = argparse.ArgumentParser( + description='Add Status field to story files that are missing it', + epilog='Path arguments are optional - script auto-detects locations. Use --story-dir and --sprint-status to override.' + ) + parser.add_argument('--story-dir', default=None, + help='Path to story files directory (auto-detected if omitted)') + parser.add_argument('--sprint-status', default=None, + help='Path to sprint-status.yaml (auto-detected if omitted)') + args = parser.parse_args() + + try: + # Auto-detect paths if not provided + project_root = find_project_root() + print(f"📁 Project root: {project_root}", file=sys.stderr) + + if args.story_dir: + story_dir = Path(args.story_dir).resolve() else: - skipped += 1 - - print() - print(f"✅ Added Status field to {added} stories") - print(f"â„šī¸ Skipped {skipped} stories (already have Status)") - print(f"âš ī¸ {missing} stories not in sprint-status.yaml") + story_dir = find_story_dir(project_root) + + if args.sprint_status: + sprint_status_path = Path(args.sprint_status).resolve() + else: + sprint_status_path = find_sprint_status(project_root, story_dir) + + print(f"📖 Story directory: {story_dir}", file=sys.stderr) + print(f"📋 Sprint status file: {sprint_status_path}", file=sys.stderr) + print("", file=sys.stderr) + + # Load statuses from sprint-status.yaml + print("Loading sprint-status.yaml...", file=sys.stderr) + statuses = load_sprint_status(sprint_status_path) + print(f"✓ Found {len(statuses)} entries in sprint-status.yaml", file=sys.stderr) + print("", file=sys.stderr) + + # Scan story files and add Status fields + print("Scanning story files...", file=sys.stderr) + added = 0 + skipped = 0 + missing = 0 + + for story_file in sorted(story_dir.glob("*.md")): + story_id = story_file.stem + + # Skip special files + if (story_id.startswith('.') or + story_id.startswith('EPIC-') or + 'COMPLETION' in story_id.upper() or + 'SUMMARY' in story_id.upper() or + 'REPORT' in story_id.upper() or + 'README' in story_id.upper() or + 'INDEX' in story_id.upper() or + 'REVIEW' in story_id.upper() or + 'AUDIT' in story_id.upper()): + continue + + if story_id not in statuses: + print(f"âš ī¸ {story_id}: Not in sprint-status.yaml", file=sys.stderr) + missing += 1 + continue + + status = statuses[story_id] + + if add_status_to_story(story_file, status): + print(f"✓ {story_id}: Added Status: {status}", file=sys.stderr) + added += 1 + else: + skipped += 1 + + print("", file=sys.stderr) + print(f"✅ Added Status field to {added} stories", file=sys.stderr) + print(f"â„šī¸ Skipped {skipped} stories (already have Status)", file=sys.stderr) + if missing > 0: + print(f"âš ī¸ {missing} stories not in sprint-status.yaml", file=sys.stderr) + + sys.exit(0) + + except FileNotFoundError as e: + print(f"✗ {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"✗ Error: {e}", file=sys.stderr) + import traceback + traceback.print_exc() + sys.exit(1) + if __name__ == '__main__': main() diff --git a/scripts/lib/sprint-status-updater.py b/scripts/lib/sprint-status-updater.py index a1ebb73c..2ceb3a03 100755 --- a/scripts/lib/sprint-status-updater.py +++ b/scripts/lib/sprint-status-updater.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """ Sprint Status Updater - Robust YAML updater for sprint-status.yaml +Smart path resolution - finds files regardless of working directory Purpose: Update sprint-status.yaml entries while preserving: - Comments @@ -8,14 +9,21 @@ Purpose: Update sprint-status.yaml entries while preserving: - Section structure - Manual annotations +Features: + - Auto-detects project root and story directory + - Works from any working directory + - Explicit path overrides via CLI arguments + - Clear error messages if files not found + Created: 2026-01-02 Part of: Full Workflow Fix (Option C) """ import re import sys +import os from pathlib import Path -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Optional from datetime import datetime @@ -23,7 +31,11 @@ class SprintStatusUpdater: """Updates sprint-status.yaml while preserving structure and comments""" def __init__(self, sprint_status_path: str): - self.path = Path(sprint_status_path) + self.path = Path(sprint_status_path).resolve() + + if not self.path.exists(): + raise FileNotFoundError(f"sprint-status.yaml not found at: {self.path}") + self.content = self.path.read_text() self.lines = self.content.split('\n') self.updates_applied = 0 @@ -211,7 +223,82 @@ class SprintStatusUpdater: return self.path -def scan_story_statuses(story_dir: str = "docs/sprint-artifacts") -> Dict[str, str]: +def find_project_root() -> Path: + """ + Find project root by looking for .git directory or other markers. + Works from any subdirectory. + """ + current = Path.cwd() + + # Try up to 10 levels up + for _ in range(10): + if (current / '.git').exists(): + return current + if (current / '.claude').exists(): + return current + current = current.parent + if current == current.parent: # Reached filesystem root + break + + # Fallback to current working directory + return Path.cwd() + + +def find_story_dir(project_root: Optional[Path] = None) -> Path: + """ + Auto-detect story directory location. + Tries multiple possible paths and returns the first one that exists. + """ + if project_root is None: + project_root = find_project_root() + + # Try paths in order of preference + candidates = [ + project_root / "_bmad-output" / "implementation-artifacts" / "sprint-artifacts", + project_root / "docs" / "sprint-artifacts", + project_root / "sprint-artifacts", + Path.cwd() / "sprint-artifacts", + ] + + for candidate in candidates: + if candidate.exists(): + return candidate + + # If none found, suggest the most likely paths + print(f"ERROR: Could not find story directory.", file=sys.stderr) + print(f"Tried:", file=sys.stderr) + for candidate in candidates: + print(f" - {candidate}", file=sys.stderr) + raise FileNotFoundError("Story directory not found") + + +def find_sprint_status(project_root: Optional[Path] = None, story_dir: Optional[Path] = None) -> Path: + """ + Auto-detect sprint-status.yaml location. + Looks in the story directory or its parent. + """ + if story_dir is None: + story_dir = find_story_dir(project_root) + + # Try paths in order of preference + candidates = [ + story_dir / "sprint-status.yaml", + story_dir.parent / "sprint-status.yaml", + ] + + for candidate in candidates: + if candidate.exists(): + return candidate + + # If none found, suggest the most likely paths + print(f"ERROR: Could not find sprint-status.yaml", file=sys.stderr) + print(f"Tried:", file=sys.stderr) + for candidate in candidates: + print(f" - {candidate}", file=sys.stderr) + raise FileNotFoundError("sprint-status.yaml not found") + + +def scan_story_statuses(story_dir: Path) -> Dict[str, str]: """ Scan all story files and extract EXPLICIT Status: fields @@ -222,7 +309,11 @@ def scan_story_statuses(story_dir: str = "docs/sprint-artifacts") -> Dict[str, s Returns: Dict mapping story_id -> normalized_status (ONLY for stories with explicit Status: field) """ - story_dir_path = Path(story_dir) + story_dir_path = Path(story_dir).resolve() + + if not story_dir_path.exists(): + raise FileNotFoundError(f"Story directory not found: {story_dir_path}") + story_files = list(story_dir_path.glob("*.md")) STATUS_MAPPINGS = { @@ -312,109 +403,140 @@ def main(): """Main entry point for CLI usage""" import argparse - parser = argparse.ArgumentParser(description='Update sprint-status.yaml from story files') + parser = argparse.ArgumentParser( + description='Update sprint-status.yaml from story files', + epilog='Path arguments are optional - script auto-detects locations. Use --sprint-status and --story-dir to override.' + ) parser.add_argument('--dry-run', action='store_true', help='Show changes without applying') parser.add_argument('--validate', action='store_true', help='Validate only (exit 1 if discrepancies)') - parser.add_argument('--sprint-status', default='docs/sprint-artifacts/sprint-status.yaml', - help='Path to sprint-status.yaml') - parser.add_argument('--story-dir', default='docs/sprint-artifacts', - help='Path to story files directory') + parser.add_argument('--sprint-status', default=None, + help='Path to sprint-status.yaml (auto-detected if omitted)') + parser.add_argument('--story-dir', default=None, + help='Path to story files directory (auto-detected if omitted)') parser.add_argument('--epic', type=str, help='Validate specific epic only (e.g., epic-1)') parser.add_argument('--mode', choices=['validate', 'fix'], default='validate', help='Mode: validate (report only) or fix (apply updates)') args = parser.parse_args() - # Scan story files - print("Scanning story files...", file=sys.stderr) - story_statuses = scan_story_statuses(args.story_dir) + try: + # Auto-detect paths if not provided + project_root = find_project_root() + print(f"📁 Project root: {project_root}", file=sys.stderr) - # Filter by epic if specified - if args.epic: - # Extract epic number from epic key (e.g., "epic-1" -> "1") - epic_match = re.match(r'epic-([0-9a-z-]+)', args.epic) - if epic_match: - epic_num = epic_match.group(1) - # Filter stories that start with this epic number - story_statuses = {k: v for k, v in story_statuses.items() - if k.startswith(f"{epic_num}-")} - print(f"✓ Filtered to {len(story_statuses)} stories for {args.epic}", file=sys.stderr) + if args.story_dir: + story_dir = Path(args.story_dir).resolve() else: - print(f"WARNING: Invalid epic format: {args.epic}", file=sys.stderr) + story_dir = find_story_dir(project_root) - print(f"✓ Scanned {len(story_statuses)} story files", file=sys.stderr) - print("", file=sys.stderr) + if args.sprint_status: + sprint_status_path = Path(args.sprint_status).resolve() + else: + sprint_status_path = find_sprint_status(project_root, story_dir) - # Load sprint-status.yaml - updater = SprintStatusUpdater(args.sprint_status) + print(f"📖 Story directory: {story_dir}", file=sys.stderr) + print(f"📋 Sprint status file: {sprint_status_path}", file=sys.stderr) + print("", file=sys.stderr) - # Find discrepancies - discrepancies = [] + # Scan story files + print("Scanning story files...", file=sys.stderr) + story_statuses = scan_story_statuses(story_dir) - for story_id, new_status in story_statuses.items(): - # Check current status in sprint-status.yaml - current_status = None - in_dev_status = False + # Filter by epic if specified + if args.epic: + # Extract epic number from epic key (e.g., "epic-1" -> "1") + epic_match = re.match(r'epic-([0-9a-z-]+)', args.epic) + if epic_match: + epic_num = epic_match.group(1) + # Filter stories that start with this epic number + story_statuses = {k: v for k, v in story_statuses.items() + if k.startswith(f"{epic_num}-")} + print(f"✓ Filtered to {len(story_statuses)} stories for {args.epic}", file=sys.stderr) + else: + print(f"WARNING: Invalid epic format: {args.epic}", file=sys.stderr) - for line in updater.lines: - if line.strip() == 'development_status:': - in_dev_status = True - continue + print(f"✓ Scanned {len(story_statuses)} story files", file=sys.stderr) + print("", file=sys.stderr) - if in_dev_status and story_id in line: - match = re.match(r'\s+[a-z0-9-]+:\s*(\S+)', line) - if match: - current_status = match.group(1) - break + # Load sprint-status.yaml + updater = SprintStatusUpdater(str(sprint_status_path)) - if current_status is None: - discrepancies.append((story_id, 'NOT-IN-FILE', new_status)) - elif current_status != new_status: - discrepancies.append((story_id, current_status, new_status)) + # Find discrepancies + discrepancies = [] - # Report - if not discrepancies: - print("✓ sprint-status.yaml is up to date!", file=sys.stderr) + for story_id, new_status in story_statuses.items(): + # Check current status in sprint-status.yaml + current_status = None + in_dev_status = False + + for line in updater.lines: + if line.strip() == 'development_status:': + in_dev_status = True + continue + + if in_dev_status and story_id in line: + match = re.match(r'\s+[a-z0-9-]+:\s*(\S+)', line) + if match: + current_status = match.group(1) + break + + if current_status is None: + discrepancies.append((story_id, 'NOT-IN-FILE', new_status)) + elif current_status != new_status: + discrepancies.append((story_id, current_status, new_status)) + + # Report + if not discrepancies: + print("✓ sprint-status.yaml is up to date!", file=sys.stderr) + sys.exit(0) + + print(f"⚠ Found {len(discrepancies)} discrepancies:", file=sys.stderr) + print("", file=sys.stderr) + + for story_id, old_status, new_status in discrepancies[:20]: + if old_status == 'NOT-IN-FILE': + print(f" [ADD] {story_id}: (not in file) → {new_status}", file=sys.stderr) + else: + print(f" [UPDATE] {story_id}: {old_status} → {new_status}", file=sys.stderr) + + if len(discrepancies) > 20: + print(f" ... and {len(discrepancies) - 20} more", file=sys.stderr) + + print("", file=sys.stderr) + + # Handle mode parameter + if args.mode == 'validate' or args.validate: + print("✗ Validation failed - discrepancies found", file=sys.stderr) + sys.exit(1) + + if args.dry_run: + print("DRY RUN: Would update sprint-status.yaml", file=sys.stderr) + sys.exit(0) + + # Apply updates (--mode fix or default behavior) + print("Applying updates...", file=sys.stderr) + + for story_id, old_status, new_status in discrepancies: + comment = f"Updated {datetime.now().strftime('%Y-%m-%d')}" + updater.update_story_status(story_id, new_status, comment) + + # Add verification timestamp + updater.add_verification_note() + + # Save + updater.save(backup=True) + + print(f"✓ Applied {updater.updates_applied} updates", file=sys.stderr) + print(f"✓ Updated: {updater.path}", file=sys.stderr) sys.exit(0) - print(f"⚠ Found {len(discrepancies)} discrepancies:", file=sys.stderr) - print("", file=sys.stderr) - - for story_id, old_status, new_status in discrepancies[:20]: - if old_status == 'NOT-IN-FILE': - print(f" [ADD] {story_id}: (not in file) → {new_status}", file=sys.stderr) - else: - print(f" [UPDATE] {story_id}: {old_status} → {new_status}", file=sys.stderr) - - if len(discrepancies) > 20: - print(f" ... and {len(discrepancies) - 20} more", file=sys.stderr) - - print("", file=sys.stderr) - - # Handle mode parameter - if args.mode == 'validate' or args.validate: - print("✗ Validation failed - discrepancies found", file=sys.stderr) + except FileNotFoundError as e: + print(f"✗ {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"✗ Error: {e}", file=sys.stderr) + import traceback + traceback.print_exc() sys.exit(1) - - if args.dry_run: - print("DRY RUN: Would update sprint-status.yaml", file=sys.stderr) - sys.exit(0) - - # Apply updates (--mode fix or default behavior) - print("Applying updates...", file=sys.stderr) - - for story_id, old_status, new_status in discrepancies: - comment = f"Updated {datetime.now().strftime('%Y-%m-%d')}" - updater.update_story_status(story_id, new_status, comment) - - # Add verification timestamp - updater.add_verification_note() - - # Save - updater.save(backup=True) - - print(f"✓ Applied {updater.updates_applied} updates", file=sys.stderr) - print(f"✓ Updated: {updater.path}", file=sys.stderr) - sys.exit(0) if __name__ == '__main__': diff --git a/src/modules/bmm/workflows/4-implementation/super-dev-pipeline/steps/step-02-pre-gap-analysis.md b/src/modules/bmm/workflows/4-implementation/super-dev-pipeline/steps/step-02-pre-gap-analysis.md index 7e3925d1..65e1cc03 100644 --- a/src/modules/bmm/workflows/4-implementation/super-dev-pipeline/steps/step-02-pre-gap-analysis.md +++ b/src/modules/bmm/workflows/4-implementation/super-dev-pipeline/steps/step-02-pre-gap-analysis.md @@ -401,7 +401,14 @@ done **Interactive Mode:** -Display gap analysis report and batching plan: +Display gap analysis report with conditional batching menu. + +**CRITICAL DECISION LOGIC:** +- If `batchable_count > 0 AND time_saved > 0`: Show batching options +- If `batchable_count = 0 OR time_saved = 0`: Skip batching options (no benefit) + +**When Batching Has Benefit (time_saved > 0):** + ``` Gap Analysis Complete + Smart Batching Plan @@ -425,11 +432,42 @@ Estimated time: {estimated_hours} hours (with batching) [H] Halt pipeline ``` -**Batch Mode:** Auto-accept changes and batching plan +**When Batching Has NO Benefit (time_saved = 0):** -### 7. Update Story File with Batching Plan +``` +Gap Analysis Complete -Add batching plan to story file: +Task Analysis: +- {done_count} tasks already complete (will check) +- {unclear_count} tasks refined to {refined_count} specific tasks +- {missing_count} new tasks added +- {needed_count} tasks ready for implementation + +Smart Batching Analysis: +- Batchable patterns detected: 0 +- Tasks requiring individual execution: {work_count} +- Estimated time savings: none (tasks require individual attention) + +Total work: {work_count} tasks +Estimated time: {estimated_hours} hours + +[A] Accept changes +[E] Edit tasks manually +[H] Halt pipeline +``` + +**Why Skip Batching Option When Benefit = 0:** +- Reduces decision fatigue +- Prevents pointless "batch vs no-batch" choice when outcome is identical +- Cleaner UX when batching isn't applicable + +**Batch Mode:** Auto-accept changes (batching plan applied only if benefit > 0) + +### 8. Update Story File with Batching Plan (Conditional) + +**ONLY add batching plan if `time_saved > 0`.** + +If batching has benefit (time_saved > 0), add batching plan to story file: ```markdown ## Smart Batching Plan @@ -457,7 +495,9 @@ Add batching plan to story file: - Savings: {savings} hours ``` -### 8. Update Pipeline State +If batching has NO benefit (time_saved = 0), **skip this section entirely** and just add gap analysis results. + +### 9. Update Pipeline State Update state file: - Add `2` to `stepsCompleted` @@ -474,7 +514,7 @@ Update state file: tasks_added: {count} smart_batching: - enabled: true + enabled: {true if time_saved > 0, false otherwise} patterns_detected: {count} batchable_tasks: {count} individual_tasks: {count} @@ -483,9 +523,12 @@ Update state file: estimated_savings: {hours} ``` -### 9. Present Summary with Batching Plan +**Note:** `smart_batching.enabled` is set to `false` when batching has no benefit, preventing unnecessary batching plan generation. + +### 10. Present Summary (Conditional Format) + +**When Batching Has Benefit (time_saved > 0):** -Display: ``` Pre-Gap Analysis Complete + Smart Batching Plan @@ -509,6 +552,26 @@ Time Estimate: Ready for Implementation ``` +**When Batching Has NO Benefit (time_saved = 0):** + +``` +Pre-Gap Analysis Complete + +Development Type: {greenfield|brownfield|hybrid} +Work Remaining: {work_count} tasks + +Codebase Status: +- Existing implementations reviewed: {existing_count} +- New implementations needed: {new_count} + +Smart Batching Analysis: +- Batchable patterns detected: 0 +- Tasks requiring individual execution: {work_count} +- Estimated time: {estimated_hours} hours + +Ready for Implementation +``` + **Interactive Mode Menu:** ``` [C] Continue to Implementation