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
This commit is contained in:
Jonah Schulte 2026-01-06 08:21:44 -05:00
parent 4ea18e97fb
commit 1ddf2afcea
3 changed files with 465 additions and 139 deletions

View File

@ -2,16 +2,102 @@
""" """
Add Status field to story files that are missing it. Add Status field to story files that are missing it.
Uses sprint-status.yaml as source of truth. 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 re
import sys
from pathlib import Path 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""" """Load story statuses from sprint-status.yaml"""
with open(path) as f: if not path.exists():
lines = f.readlines() raise FileNotFoundError(f"sprint-status.yaml not found at: {path}")
lines = path.read_text().split('\n')
statuses = {} statuses = {}
in_dev_status = False in_dev_status = False
@ -34,6 +120,7 @@ def load_sprint_status(path: str = "docs/sprint-artifacts/sprint-status.yaml") -
return statuses return statuses
def add_status_to_story(story_file: Path, status: str) -> bool: def add_status_to_story(story_file: Path, status: str) -> bool:
"""Add Status field to story file if missing""" """Add Status field to story file if missing"""
content = story_file.read_text() 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:**") # Check if Status field already exists (handles both "Status:" and "**Status:**")
if re.search(r'^\*?\*?Status:', content, re.MULTILINE | re.IGNORECASE): if re.search(r'^\*?\*?Status:', content, re.MULTILINE | re.IGNORECASE):
return False # Already has Status field return False # Already has Status field
# Find the first section after the title (usually ## Story or ## Description) # Find the first section after the title (usually ## Story or ## Description)
# Insert Status field before that # Insert Status field before that
lines = content.split('\n') lines = content.split('\n')
# Find insertion point (after title, before first ## section) # Find insertion point (after title, before first ## section)
insert_idx = None insert_idx = None
for idx, line in enumerate(lines): 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 # Found first section - insert before it
insert_idx = idx insert_idx = idx
break break
if insert_idx is None: if insert_idx is None:
# No ## sections found, insert after title # No ## sections found, insert after title
insert_idx = 1 insert_idx = 1
# Insert blank line, Status field, blank line # Insert blank line, Status field, blank line
lines.insert(insert_idx, '') lines.insert(insert_idx, '')
lines.insert(insert_idx + 1, f'**Status:** {status}') lines.insert(insert_idx + 1, f'**Status:** {status}')
lines.insert(insert_idx + 2, '') lines.insert(insert_idx + 2, '')
# Write back # Write back
story_file.write_text('\n'.join(lines)) story_file.write_text('\n'.join(lines))
return True return True
def main(): def main():
story_dir = Path("docs/sprint-artifacts") """Main entry point for CLI usage"""
statuses = load_sprint_status() import argparse
added = 0 parser = argparse.ArgumentParser(
skipped = 0 description='Add Status field to story files that are missing it',
missing = 0 epilog='Path arguments are optional - script auto-detects locations. Use --story-dir and --sprint-status to override.'
)
for story_file in sorted(story_dir.glob("*.md")): parser.add_argument('--story-dir', default=None,
story_id = story_file.stem help='Path to story files directory (auto-detected if omitted)')
parser.add_argument('--sprint-status', default=None,
# Skip special files help='Path to sprint-status.yaml (auto-detected if omitted)')
if (story_id.startswith('.') or args = parser.parse_args()
story_id.startswith('EPIC-') or
'COMPLETION' in story_id.upper() or try:
'SUMMARY' in story_id.upper() or # Auto-detect paths if not provided
'REPORT' in story_id.upper() or project_root = find_project_root()
'README' in story_id.upper()): print(f"📁 Project root: {project_root}", file=sys.stderr)
continue
if args.story_dir:
if story_id not in statuses: story_dir = Path(args.story_dir).resolve()
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
else: else:
skipped += 1 story_dir = find_story_dir(project_root)
print() if args.sprint_status:
print(f"✅ Added Status field to {added} stories") sprint_status_path = Path(args.sprint_status).resolve()
print(f" Skipped {skipped} stories (already have Status)") else:
print(f"⚠️ {missing} stories not in sprint-status.yaml") 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__': if __name__ == '__main__':
main() main()

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Sprint Status Updater - Robust YAML updater for sprint-status.yaml 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: Purpose: Update sprint-status.yaml entries while preserving:
- Comments - Comments
@ -8,14 +9,21 @@ Purpose: Update sprint-status.yaml entries while preserving:
- Section structure - Section structure
- Manual annotations - 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 Created: 2026-01-02
Part of: Full Workflow Fix (Option C) Part of: Full Workflow Fix (Option C)
""" """
import re import re
import sys import sys
import os
from pathlib import Path from pathlib import Path
from typing import Dict, List, Tuple from typing import Dict, List, Tuple, Optional
from datetime import datetime from datetime import datetime
@ -23,7 +31,11 @@ class SprintStatusUpdater:
"""Updates sprint-status.yaml while preserving structure and comments""" """Updates sprint-status.yaml while preserving structure and comments"""
def __init__(self, sprint_status_path: str): 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.content = self.path.read_text()
self.lines = self.content.split('\n') self.lines = self.content.split('\n')
self.updates_applied = 0 self.updates_applied = 0
@ -211,7 +223,82 @@ class SprintStatusUpdater:
return self.path 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 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: Returns:
Dict mapping story_id -> normalized_status (ONLY for stories with explicit Status: field) 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")) story_files = list(story_dir_path.glob("*.md"))
STATUS_MAPPINGS = { STATUS_MAPPINGS = {
@ -312,109 +403,140 @@ def main():
"""Main entry point for CLI usage""" """Main entry point for CLI usage"""
import argparse 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('--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('--validate', action='store_true', help='Validate only (exit 1 if discrepancies)')
parser.add_argument('--sprint-status', default='docs/sprint-artifacts/sprint-status.yaml', parser.add_argument('--sprint-status', default=None,
help='Path to sprint-status.yaml') help='Path to sprint-status.yaml (auto-detected if omitted)')
parser.add_argument('--story-dir', default='docs/sprint-artifacts', parser.add_argument('--story-dir', default=None,
help='Path to story files directory') 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('--epic', type=str, help='Validate specific epic only (e.g., epic-1)')
parser.add_argument('--mode', choices=['validate', 'fix'], default='validate', parser.add_argument('--mode', choices=['validate', 'fix'], default='validate',
help='Mode: validate (report only) or fix (apply updates)') help='Mode: validate (report only) or fix (apply updates)')
args = parser.parse_args() args = parser.parse_args()
# Scan story files try:
print("Scanning story files...", file=sys.stderr) # Auto-detect paths if not provided
story_statuses = scan_story_statuses(args.story_dir) project_root = find_project_root()
print(f"📁 Project root: {project_root}", file=sys.stderr)
# Filter by epic if specified if args.story_dir:
if args.epic: story_dir = Path(args.story_dir).resolve()
# 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: 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) if args.sprint_status:
print("", file=sys.stderr) sprint_status_path = Path(args.sprint_status).resolve()
else:
sprint_status_path = find_sprint_status(project_root, story_dir)
# Load sprint-status.yaml print(f"📖 Story directory: {story_dir}", file=sys.stderr)
updater = SprintStatusUpdater(args.sprint_status) print(f"📋 Sprint status file: {sprint_status_path}", file=sys.stderr)
print("", file=sys.stderr)
# Find discrepancies # Scan story files
discrepancies = [] print("Scanning story files...", file=sys.stderr)
story_statuses = scan_story_statuses(story_dir)
for story_id, new_status in story_statuses.items(): # Filter by epic if specified
# Check current status in sprint-status.yaml if args.epic:
current_status = None # Extract epic number from epic key (e.g., "epic-1" -> "1")
in_dev_status = False 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: print(f"✓ Scanned {len(story_statuses)} story files", file=sys.stderr)
if line.strip() == 'development_status:': print("", file=sys.stderr)
in_dev_status = True
continue
if in_dev_status and story_id in line: # Load sprint-status.yaml
match = re.match(r'\s+[a-z0-9-]+:\s*(\S+)', line) updater = SprintStatusUpdater(str(sprint_status_path))
if match:
current_status = match.group(1)
break
if current_status is None: # Find discrepancies
discrepancies.append((story_id, 'NOT-IN-FILE', new_status)) discrepancies = []
elif current_status != new_status:
discrepancies.append((story_id, current_status, new_status))
# Report for story_id, new_status in story_statuses.items():
if not discrepancies: # Check current status in sprint-status.yaml
print("✓ sprint-status.yaml is up to date!", file=sys.stderr) 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) sys.exit(0)
print(f"⚠ Found {len(discrepancies)} discrepancies:", file=sys.stderr) except FileNotFoundError as e:
print("", file=sys.stderr) print(f"{e}", file=sys.stderr)
sys.exit(1)
for story_id, old_status, new_status in discrepancies[:20]: except Exception as e:
if old_status == 'NOT-IN-FILE': print(f"✗ Error: {e}", file=sys.stderr)
print(f" [ADD] {story_id}: (not in file) → {new_status}", file=sys.stderr) import traceback
else: traceback.print_exc()
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) 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__': if __name__ == '__main__':

View File

@ -401,7 +401,14 @@ done
**Interactive Mode:** **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 Gap Analysis Complete + Smart Batching Plan
@ -425,11 +432,42 @@ Estimated time: {estimated_hours} hours (with batching)
[H] Halt pipeline [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 ```markdown
## Smart Batching Plan ## Smart Batching Plan
@ -457,7 +495,9 @@ Add batching plan to story file:
- Savings: {savings} hours - 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: Update state file:
- Add `2` to `stepsCompleted` - Add `2` to `stepsCompleted`
@ -474,7 +514,7 @@ Update state file:
tasks_added: {count} tasks_added: {count}
smart_batching: smart_batching:
enabled: true enabled: {true if time_saved > 0, false otherwise}
patterns_detected: {count} patterns_detected: {count}
batchable_tasks: {count} batchable_tasks: {count}
individual_tasks: {count} individual_tasks: {count}
@ -483,9 +523,12 @@ Update state file:
estimated_savings: {hours} 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 Pre-Gap Analysis Complete + Smart Batching Plan
@ -509,6 +552,26 @@ Time Estimate:
Ready for Implementation 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:** **Interactive Mode Menu:**
``` ```
[C] Continue to Implementation [C] Continue to Implementation