project-standalo-sonic-cloud/skills/guardrail-orchestrator/scripts/workflow_manager.py

1042 lines
37 KiB
Python
Executable File

#!/usr/bin/env python3
"""Workflow state management for automated orchestration with approval gates."""
import argparse
import json
import os
import shutil
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional
# Try to import yaml, fall back to basic parsing if not available
try:
import yaml
HAS_YAML = True
except ImportError:
HAS_YAML = False
# ============================================================================
# YAML Helpers
# ============================================================================
def load_yaml(filepath: str) -> dict:
"""Load YAML file."""
if not os.path.exists(filepath):
return {}
with open(filepath, 'r') as f:
content = f.read()
if not content.strip():
return {}
if HAS_YAML:
return yaml.safe_load(content) or {}
# Enhanced fallback parser with nested dict support
def parse_value(val: str):
"""Parse a YAML value string into Python type."""
val = val.strip()
if val in ('null', '~', ''):
return None
if val == 'true':
return True
if val == 'false':
return False
if val == '[]':
return []
if val == '{}':
return {}
# Check for integer
if val.lstrip('-').isdigit():
return int(val)
# Check for float
try:
if '.' in val:
return float(val)
except ValueError:
pass
# Handle quoted strings
if len(val) >= 2 and val[0] in ('"', "'") and val[-1] == val[0]:
return val[1:-1]
return val
result = {}
# Stack: list of (dict_ref, indent_level) to track nesting
stack = [(result, -1)]
current_list = None
list_indent = -1
for line in content.split('\n'):
# Skip empty lines and comments
stripped = line.strip()
if not stripped or stripped.startswith('#'):
continue
# Calculate indentation (number of leading spaces)
indent = len(line) - len(line.lstrip())
# Handle list items
if stripped.startswith('- '):
item_value = stripped[2:].strip()
# Find the appropriate container
while len(stack) > 1 and stack[-1][1] >= indent:
stack.pop()
current_dict = stack[-1][0]
# If last key was set to empty dict, convert to list
if current_list is not None and list_indent < indent:
current_list.append(parse_value(item_value))
continue
# Handle key: value pairs
if ':' in stripped:
# Pop stack to correct nesting level
while len(stack) > 1 and stack[-1][1] >= indent:
stack.pop()
key, _, value = stripped.partition(':')
key = key.strip()
value = value.strip()
current_dict = stack[-1][0]
if value:
# Key with immediate value
parsed = parse_value(value)
current_dict[key] = parsed
if isinstance(parsed, list):
current_list = parsed
list_indent = indent
else:
current_list = None
list_indent = -1
else:
# Key without value - could be nested dict or list
# Default to dict, will be converted if we see list items
current_dict[key] = {}
stack.append((current_dict[key], indent))
current_list = None
list_indent = -1
return result
def save_yaml(filepath: str, data: dict):
"""Save data to YAML file."""
os.makedirs(os.path.dirname(filepath), exist_ok=True)
if HAS_YAML:
with open(filepath, 'w') as f:
yaml.dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
else:
# Simple YAML writer
def write_value(value, indent=0):
prefix = ' ' * indent
lines = []
if isinstance(value, dict):
for k, v in value.items():
if isinstance(v, (dict, list)) and v:
lines.append(f"{prefix}{k}:")
lines.extend(write_value(v, indent + 1))
elif isinstance(v, list):
lines.append(f"{prefix}{k}: []")
else:
lines.append(f"{prefix}{k}: {v}")
elif isinstance(value, list):
for item in value:
if isinstance(item, dict):
lines.append(f"{prefix}-")
for k, v in item.items():
lines.append(f"{prefix} {k}: {v}")
else:
lines.append(f"{prefix}- {item}")
return lines
lines = write_value(data)
with open(filepath, 'w') as f:
f.write('\n'.join(lines))
# ============================================================================
# Workflow State Management
# ============================================================================
PHASES = [
'INITIALIZING',
'DESIGNING',
'AWAITING_DESIGN_APPROVAL',
'DESIGN_APPROVED',
'DESIGN_REJECTED',
'IMPLEMENTING',
'REVIEWING',
'SECURITY_REVIEW', # New phase for security audit
'AWAITING_IMPL_APPROVAL',
'IMPL_APPROVED',
'IMPL_REJECTED',
'COMPLETING',
'COMPLETED',
'PAUSED',
'FAILED'
]
VALID_TRANSITIONS = {
'INITIALIZING': ['DESIGNING', 'FAILED'],
'DESIGNING': ['AWAITING_DESIGN_APPROVAL', 'FAILED'],
'AWAITING_DESIGN_APPROVAL': ['DESIGN_APPROVED', 'DESIGN_REJECTED', 'PAUSED'],
'DESIGN_APPROVED': ['IMPLEMENTING', 'FAILED'],
'DESIGN_REJECTED': ['DESIGNING'],
'IMPLEMENTING': ['REVIEWING', 'FAILED', 'PAUSED'],
'REVIEWING': ['SECURITY_REVIEW', 'IMPLEMENTING', 'FAILED'], # Must pass through security
'SECURITY_REVIEW': ['AWAITING_IMPL_APPROVAL', 'IMPLEMENTING', 'FAILED'], # Can go back to fix
'AWAITING_IMPL_APPROVAL': ['IMPL_APPROVED', 'IMPL_REJECTED', 'PAUSED'],
'IMPL_APPROVED': ['COMPLETING', 'FAILED'],
'IMPL_REJECTED': ['IMPLEMENTING'],
'COMPLETING': ['COMPLETED', 'FAILED'],
'COMPLETED': [],
'PAUSED': PHASES, # Can resume to any phase
'FAILED': ['INITIALIZING', 'DESIGNING', 'IMPLEMENTING'] # Can retry
}
def get_workflow_dir() -> Path:
"""Get the .workflow directory path."""
return Path('.workflow')
def get_current_state_path() -> Path:
"""Get the current workflow state file path."""
return get_workflow_dir() / 'current.yml'
def get_history_dir() -> Path:
"""Get the workflow history directory."""
return get_workflow_dir() / 'history'
def create_workflow(feature: str) -> dict:
"""Create a new workflow state."""
now = datetime.now()
workflow_id = f"workflow_{now.strftime('%Y%m%d_%H%M%S')}"
state = {
'id': workflow_id,
'feature': feature,
'current_phase': 'INITIALIZING',
'gates': {
'design_approval': {
'status': 'pending',
'approved_at': None,
'approved_by': None,
'rejection_reason': None,
'revision_count': 0
},
'implementation_approval': {
'status': 'pending',
'approved_at': None,
'approved_by': None,
'rejection_reason': None,
'revision_count': 0
}
},
'progress': {
'entities_designed': 0,
'tasks_created': 0,
'tasks_implemented': 0,
'tasks_reviewed': 0,
'tasks_approved': 0,
'tasks_completed': 0
},
'tasks': {
'pending': [],
'in_progress': [],
'review': [],
'approved': [],
'completed': [],
'blocked': []
},
'started_at': now.isoformat(),
'updated_at': now.isoformat(),
'completed_at': None,
'last_error': None,
'resume_point': {
'phase': 'INITIALIZING',
'task_id': None,
'action': 'start_workflow'
},
'checkpoints': [] # List of checkpoint snapshots for recovery
}
# Ensure directory exists
get_workflow_dir().mkdir(exist_ok=True)
get_history_dir().mkdir(exist_ok=True)
# Save state
save_yaml(str(get_current_state_path()), state)
return state
def load_current_workflow() -> Optional[dict]:
"""Load the current workflow state from the active version."""
state_path = get_current_state_path()
if not state_path.exists():
return None
# Read current.yml to get active version
current = load_yaml(str(state_path))
active_version = current.get('active_version')
if not active_version:
return None
# Load the version's session.yml
version_session_path = get_workflow_dir() / 'versions' / active_version / 'session.yml'
if not version_session_path.exists():
return None
session = load_yaml(str(version_session_path))
current_phase = session.get('current_phase', 'INITIALIZING')
# Convert session format to state format expected by show_status
return {
'id': session.get('session_id', active_version),
'feature': session.get('feature', 'Unknown'),
'current_phase': current_phase,
'gates': {
'design_approval': session.get('approvals', {}).get('design', {'status': 'pending'}),
'implementation_approval': session.get('approvals', {}).get('implementation', {'status': 'pending'})
},
'progress': {
'entities_designed': session.get('summary', {}).get('entities_created', 0),
'tasks_created': session.get('summary', {}).get('total_tasks', 0),
'tasks_implemented': session.get('summary', {}).get('tasks_completed', 0),
'tasks_reviewed': 0,
'tasks_completed': session.get('summary', {}).get('tasks_completed', 0)
},
'tasks': {
'pending': [],
'in_progress': [],
'review': [],
'approved': [],
'completed': session.get('task_sessions', []),
'blocked': []
},
'version': active_version,
'status': session.get('status', 'unknown'),
'last_error': None,
'started_at': session.get('started_at', ''),
'updated_at': session.get('updated_at', ''),
'completed_at': session.get('completed_at'),
'resume_point': {
'phase': current_phase,
'task_id': None,
'action': 'continue_workflow'
}
}
def save_workflow(state: dict):
"""Save workflow state to the version's session.yml file."""
# Get active version
current_path = get_current_state_path()
if not current_path.exists():
print("Error: No current.yml found")
return
current = load_yaml(str(current_path))
active_version = current.get('active_version')
if not active_version:
print("Error: No active version set")
return
# Get the version's session.yml path
version_session_path = get_workflow_dir() / 'versions' / active_version / 'session.yml'
if not version_session_path.exists():
print(f"Error: Session file not found: {version_session_path}")
return
# Load existing session data
session = load_yaml(str(version_session_path))
# Create backup
backup_path = version_session_path.with_suffix('.yml.bak')
shutil.copy(version_session_path, backup_path)
# Update session with state changes
session['current_phase'] = state['current_phase']
session['updated_at'] = datetime.now().isoformat()
if state.get('completed_at'):
session['completed_at'] = state['completed_at']
session['status'] = 'completed'
# Update approvals
if 'gates' in state:
if 'approvals' not in session:
session['approvals'] = {}
if state['gates'].get('design_approval', {}).get('status') == 'approved':
session['approvals']['design'] = state['gates']['design_approval']
if state['gates'].get('implementation_approval', {}).get('status') == 'approved':
session['approvals']['implementation'] = state['gates']['implementation_approval']
save_yaml(str(version_session_path), session)
def transition_phase(state: dict, new_phase: str, error: str = None) -> bool:
"""Transition workflow to a new phase."""
current = state['current_phase']
if new_phase not in PHASES:
print(f"Error: Invalid phase '{new_phase}'")
return False
if new_phase not in VALID_TRANSITIONS.get(current, []):
print(f"Error: Cannot transition from '{current}' to '{new_phase}'")
print(f"Valid transitions: {VALID_TRANSITIONS.get(current, [])}")
return False
state['current_phase'] = new_phase
state['resume_point']['phase'] = new_phase
if new_phase == 'FAILED' and error:
state['last_error'] = error
if new_phase == 'COMPLETED':
state['completed_at'] = datetime.now().isoformat()
# Set appropriate resume action
resume_actions = {
'INITIALIZING': 'start_workflow',
'DESIGNING': 'continue_design',
'AWAITING_DESIGN_APPROVAL': 'await_user_approval',
'DESIGN_APPROVED': 'start_implementation',
'DESIGN_REJECTED': 'revise_design',
'IMPLEMENTING': 'continue_implementation',
'REVIEWING': 'continue_review',
'SECURITY_REVIEW': 'run_security_audit',
'AWAITING_IMPL_APPROVAL': 'await_user_approval',
'IMPL_APPROVED': 'start_completion',
'IMPL_REJECTED': 'fix_implementation',
'COMPLETING': 'continue_completion',
'COMPLETED': 'workflow_done',
'PAUSED': 'resume_workflow',
'FAILED': 'retry_or_abort'
}
state['resume_point']['action'] = resume_actions.get(new_phase, 'unknown')
save_workflow(state)
return True
def approve_gate(state: dict, gate: str, approver: str = 'user') -> bool:
"""Approve a gate."""
if gate not in ['design_approval', 'implementation_approval']:
print(f"Error: Invalid gate '{gate}'")
return False
state['gates'][gate]['status'] = 'approved'
state['gates'][gate]['approved_at'] = datetime.now().isoformat()
state['gates'][gate]['approved_by'] = approver
# Transition to next phase
if gate == 'design_approval':
transition_phase(state, 'DESIGN_APPROVED')
else:
transition_phase(state, 'IMPL_APPROVED')
return True
def reject_gate(state: dict, gate: str, reason: str) -> bool:
"""Reject a gate."""
if gate not in ['design_approval', 'implementation_approval']:
print(f"Error: Invalid gate '{gate}'")
return False
state['gates'][gate]['status'] = 'rejected'
state['gates'][gate]['rejection_reason'] = reason
state['gates'][gate]['revision_count'] += 1
# Transition to rejection phase
if gate == 'design_approval':
transition_phase(state, 'DESIGN_REJECTED')
else:
transition_phase(state, 'IMPL_REJECTED')
return True
def update_progress(state: dict, **kwargs):
"""Update progress counters."""
for key, value in kwargs.items():
if key in state['progress']:
state['progress'][key] = value
save_workflow(state)
def update_task_status(state: dict, task_id: str, new_status: str):
"""Update task status in workflow state."""
# Remove from all status lists
for status in state['tasks']:
if task_id in state['tasks'][status]:
state['tasks'][status].remove(task_id)
# Add to new status list
if new_status in state['tasks']:
state['tasks'][new_status].append(task_id)
# Update resume point if task is in progress
if new_status == 'in_progress':
state['resume_point']['task_id'] = task_id
save_workflow(state)
def save_checkpoint(state: dict, description: str, data: dict = None) -> dict:
"""Save a checkpoint for recovery during long operations.
Args:
state: Current workflow state
description: Human-readable description of checkpoint
data: Optional additional data to store
Returns:
The checkpoint object that was created
"""
checkpoint = {
'id': f"checkpoint_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
'timestamp': datetime.now().isoformat(),
'phase': state['current_phase'],
'description': description,
'resume_point': state['resume_point'].copy(),
'progress': state['progress'].copy(),
'data': data or {}
}
# Keep only last 10 checkpoints to avoid bloat
if 'checkpoints' not in state:
state['checkpoints'] = []
state['checkpoints'].append(checkpoint)
if len(state['checkpoints']) > 10:
state['checkpoints'] = state['checkpoints'][-10:]
save_workflow(state)
return checkpoint
def get_latest_checkpoint(state: dict) -> Optional[dict]:
"""Get the most recent checkpoint.
Returns:
Latest checkpoint or None if no checkpoints exist
"""
checkpoints = state.get('checkpoints', [])
return checkpoints[-1] if checkpoints else None
def restore_from_checkpoint(state: dict, checkpoint_id: str = None) -> bool:
"""Restore workflow state from a checkpoint.
Args:
state: Current workflow state
checkpoint_id: Optional specific checkpoint ID, defaults to latest
Returns:
True if restoration was successful
"""
checkpoints = state.get('checkpoints', [])
if not checkpoints:
print("Error: No checkpoints available")
return False
# Find checkpoint
if checkpoint_id:
checkpoint = next((c for c in checkpoints if c['id'] == checkpoint_id), None)
if not checkpoint:
print(f"Error: Checkpoint '{checkpoint_id}' not found")
return False
else:
checkpoint = checkpoints[-1]
# Restore state from checkpoint
state['resume_point'] = checkpoint['resume_point'].copy()
state['progress'] = checkpoint['progress'].copy()
state['current_phase'] = checkpoint['phase']
state['last_error'] = None # Clear any error since we're recovering
save_workflow(state)
print(f"Restored from checkpoint: {checkpoint['description']}")
return True
def list_checkpoints(state: dict) -> list:
"""List all available checkpoints.
Returns:
List of checkpoint summaries
"""
return [
{
'id': c['id'],
'timestamp': c['timestamp'],
'phase': c['phase'],
'description': c['description']
}
for c in state.get('checkpoints', [])
]
def clear_checkpoints(state: dict):
"""Clear all checkpoints (typically after successful completion)."""
state['checkpoints'] = []
save_workflow(state)
def archive_workflow(state: dict, suffix: str = ''):
"""Archive completed/aborted workflow."""
history_dir = get_history_dir()
history_dir.mkdir(exist_ok=True)
filename = f"{state['id']}{suffix}.yml"
archive_path = history_dir / filename
save_yaml(str(archive_path), state)
# Remove current state
current_path = get_current_state_path()
if current_path.exists():
current_path.unlink()
def show_status(state: dict):
"""Display workflow status."""
print()
print("" + "" * 58 + "")
print("" + "WORKFLOW STATUS".center(58) + "")
print("" + "" * 58 + "")
print("" + f" ID: {state['id']}".ljust(58) + "")
print("" + f" Feature: {state['feature'][:45]}".ljust(58) + "")
print("" + f" Phase: {state['current_phase']}".ljust(58) + "")
print("" + "" * 58 + "")
print("" + " APPROVAL GATES".ljust(58) + "")
design_gate = state['gates']['design_approval']
impl_gate = state['gates']['implementation_approval']
design_icon = "" if design_gate['status'] == 'approved' else "" if design_gate['status'] == 'rejected' else ""
impl_icon = "" if impl_gate['status'] == 'approved' else "" if impl_gate['status'] == 'rejected' else ""
print("" + f" {design_icon} Design: {design_gate['status']}".ljust(58) + "")
print("" + f" {impl_icon} Implementation: {impl_gate['status']}".ljust(58) + "")
print("" + "" * 58 + "")
print("" + " PROGRESS".ljust(58) + "")
p = state['progress']
print("" + f" Entities Designed: {p['entities_designed']}".ljust(58) + "")
print("" + f" Tasks Created: {p['tasks_created']}".ljust(58) + "")
print("" + f" Tasks Implemented: {p['tasks_implemented']}".ljust(58) + "")
print("" + f" Tasks Reviewed: {p['tasks_reviewed']}".ljust(58) + "")
print("" + f" Tasks Completed: {p['tasks_completed']}".ljust(58) + "")
print("" + "" * 58 + "")
print("" + " TASK BREAKDOWN".ljust(58) + "")
t = state['tasks']
print("" + f" ⏳ Pending: {len(t['pending'])}".ljust(58) + "")
print("" + f" 🔄 In Progress: {len(t['in_progress'])}".ljust(58) + "")
print("" + f" 🔍 Review: {len(t['review'])}".ljust(58) + "")
print("" + f" ✅ Approved: {len(t['approved'])}".ljust(58) + "")
print("" + f" ✓ Completed: {len(t['completed'])}".ljust(58) + "")
print("" + f" 🚫 Blocked: {len(t['blocked'])}".ljust(58) + "")
if state['last_error']:
print("" + "" * 58 + "")
print("" + " ⚠️ LAST ERROR".ljust(58) + "")
print("" + f" {state['last_error'][:52]}".ljust(58) + "")
print("" + "" * 58 + "")
print("" + " TIMESTAMPS".ljust(58) + "")
started = str(state.get('started_at', ''))[:19]
updated = str(state.get('updated_at', ''))[:19]
print("" + f" Started: {started}".ljust(58) + "")
print("" + f" Updated: {updated}".ljust(58) + "")
if state.get('completed_at'):
completed = str(state['completed_at'])[:19]
print("" + f" Completed: {completed}".ljust(58) + "")
print("" + "" * 58 + "")
# ============================================================================
# CLI Interface
# ============================================================================
def main():
parser = argparse.ArgumentParser(description="Workflow state management")
subparsers = parser.add_subparsers(dest='command', help='Commands')
# create command
create_parser = subparsers.add_parser('create', help='Create new workflow')
create_parser.add_argument('feature', help='Feature to implement')
# status command
subparsers.add_parser('status', help='Show workflow status')
# transition command
trans_parser = subparsers.add_parser('transition', help='Transition to new phase')
trans_parser.add_argument('phase', choices=PHASES, help='Target phase')
trans_parser.add_argument('--error', help='Error message (for FAILED phase)')
# approve command
approve_parser = subparsers.add_parser('approve', help='Approve a gate')
approve_parser.add_argument('gate', choices=['design', 'implementation'], help='Gate to approve')
approve_parser.add_argument('--approver', default='user', help='Approver name')
# reject command
reject_parser = subparsers.add_parser('reject', help='Reject a gate')
reject_parser.add_argument('gate', choices=['design', 'implementation'], help='Gate to reject')
reject_parser.add_argument('reason', help='Rejection reason')
# progress command
progress_parser = subparsers.add_parser('progress', help='Update progress')
progress_parser.add_argument('--entities', type=int, help='Entities designed')
progress_parser.add_argument('--tasks-created', type=int, help='Tasks created')
progress_parser.add_argument('--tasks-impl', type=int, help='Tasks implemented')
progress_parser.add_argument('--tasks-reviewed', type=int, help='Tasks reviewed')
progress_parser.add_argument('--tasks-completed', type=int, help='Tasks completed')
# task command
task_parser = subparsers.add_parser('task', help='Update task status')
task_parser.add_argument('task_id', help='Task ID')
task_parser.add_argument('status', choices=['pending', 'in_progress', 'review', 'approved', 'completed', 'blocked'])
# archive command
archive_parser = subparsers.add_parser('archive', help='Archive workflow')
archive_parser.add_argument('--suffix', default='', help='Filename suffix (e.g., _aborted)')
# exists command
subparsers.add_parser('exists', help='Check if workflow exists')
# checkpoint command
checkpoint_parser = subparsers.add_parser('checkpoint', help='Manage checkpoints')
checkpoint_parser.add_argument('action', choices=['save', 'list', 'restore', 'clear'],
help='Checkpoint action')
checkpoint_parser.add_argument('--description', '-d', help='Checkpoint description (for save)')
checkpoint_parser.add_argument('--id', help='Checkpoint ID (for restore)')
checkpoint_parser.add_argument('--data', help='JSON data to store (for save)')
# validate command - run implementation validation
validate_parser = subparsers.add_parser('validate', help='Validate implementation against design')
validate_parser.add_argument('--checklist', action='store_true', help='Generate markdown checklist')
validate_parser.add_argument('--fix-suggestions', action='store_true', help='Show fix suggestions')
# generate-types command - generate TypeScript types from design
gentypes_parser = subparsers.add_parser('generate-types', help='Generate TypeScript types from design')
gentypes_parser.add_argument('--output-dir', default='types', help='Output directory')
# checklist command - view/update implementation checklist
checklist_parser = subparsers.add_parser('checklist', help='View/update implementation checklist')
checklist_parser.add_argument('action', choices=['show', 'check', 'uncheck', 'summary'],
help='Checklist action')
checklist_parser.add_argument('--item', help='Item ID to check/uncheck')
checklist_parser.add_argument('--reference', help='Reference for checked item (file, commit, etc)')
args = parser.parse_args()
if args.command == 'create':
state = create_workflow(args.feature)
print(f"Created workflow: {state['id']}")
print(f"Feature: {args.feature}")
print(f"State saved to: {get_current_state_path()}")
elif args.command == 'status':
state = load_current_workflow()
if state:
show_status(state)
else:
print("No active workflow found.")
print("Start a new workflow with: /workflow:spawn <feature>")
elif args.command == 'transition':
state = load_current_workflow()
if not state:
print("Error: No active workflow")
sys.exit(1)
if transition_phase(state, args.phase, args.error):
print(f"Transitioned to: {args.phase}")
else:
sys.exit(1)
elif args.command == 'approve':
state = load_current_workflow()
if not state:
print("Error: No active workflow")
sys.exit(1)
gate = f"{args.gate}_approval"
if approve_gate(state, gate, args.approver):
print(f"Approved: {args.gate}")
elif args.command == 'reject':
state = load_current_workflow()
if not state:
print("Error: No active workflow")
sys.exit(1)
gate = f"{args.gate}_approval"
if reject_gate(state, gate, args.reason):
print(f"Rejected: {args.gate}")
print(f"Reason: {args.reason}")
elif args.command == 'progress':
state = load_current_workflow()
if not state:
print("Error: No active workflow")
sys.exit(1)
updates = {}
if args.entities is not None:
updates['entities_designed'] = args.entities
if args.tasks_created is not None:
updates['tasks_created'] = args.tasks_created
if args.tasks_impl is not None:
updates['tasks_implemented'] = args.tasks_impl
if args.tasks_reviewed is not None:
updates['tasks_reviewed'] = args.tasks_reviewed
if args.tasks_completed is not None:
updates['tasks_completed'] = args.tasks_completed
if updates:
update_progress(state, **updates)
print("Progress updated")
elif args.command == 'task':
state = load_current_workflow()
if not state:
print("Error: No active workflow")
sys.exit(1)
update_task_status(state, args.task_id, args.status)
print(f"Task {args.task_id}{args.status}")
elif args.command == 'archive':
state = load_current_workflow()
if not state:
print("Error: No active workflow")
sys.exit(1)
archive_workflow(state, args.suffix)
print(f"Workflow archived to: {get_history_dir()}/{state['id']}{args.suffix}.yml")
elif args.command == 'exists':
state = load_current_workflow()
if state:
print("true")
sys.exit(0)
else:
print("false")
sys.exit(1)
elif args.command == 'checkpoint':
state = load_current_workflow()
if not state:
print("Error: No active workflow")
sys.exit(1)
if args.action == 'save':
if not args.description:
print("Error: --description required for save")
sys.exit(1)
data = None
if args.data:
try:
data = json.loads(args.data)
except json.JSONDecodeError:
print("Error: --data must be valid JSON")
sys.exit(1)
checkpoint = save_checkpoint(state, args.description, data)
print(f"Checkpoint saved: {checkpoint['id']}")
print(f"Description: {args.description}")
elif args.action == 'list':
checkpoints = list_checkpoints(state)
if not checkpoints:
print("No checkpoints available")
else:
print("\n" + "=" * 60)
print("CHECKPOINTS".center(60))
print("=" * 60)
for cp in checkpoints:
print(f"\n ID: {cp['id']}")
print(f" Time: {cp['timestamp'][:19]}")
print(f" Phase: {cp['phase']}")
print(f" Description: {cp['description']}")
print("\n" + "=" * 60)
elif args.action == 'restore':
if restore_from_checkpoint(state, args.id):
print("Workflow state restored successfully")
else:
sys.exit(1)
elif args.action == 'clear':
clear_checkpoints(state)
print("All checkpoints cleared")
elif args.command == 'validate':
state = load_current_workflow()
if not state:
print("Error: No active workflow")
sys.exit(1)
# Find design document
version = state.get('version', 'v001')
design_path = get_workflow_dir() / 'versions' / version / 'design' / 'design_document.yml'
if not design_path.exists():
print(f"Error: Design document not found at {design_path}")
sys.exit(1)
# Run validation script
import subprocess
scripts_dir = Path(__file__).parent
validate_script = scripts_dir / 'validate_implementation.py'
cmd = ['python3', str(validate_script), str(design_path)]
if args.checklist:
checklist_path = get_workflow_dir() / 'versions' / version / 'implementation_checklist.md'
cmd.extend(['--checklist', str(checklist_path)])
result = subprocess.run(cmd, capture_output=False)
# Update workflow state with validation status
validation_status = 'passed' if result.returncode == 0 else 'failed'
session_path = get_workflow_dir() / 'versions' / version / 'session.yml'
session = load_yaml(str(session_path))
session['last_validation'] = {
'status': validation_status,
'timestamp': datetime.now().isoformat(),
'checklist_path': str(get_workflow_dir() / 'versions' / version / 'implementation_checklist.md')
}
save_yaml(str(session_path), session)
sys.exit(result.returncode)
elif args.command == 'generate-types':
state = load_current_workflow()
if not state:
print("Error: No active workflow")
sys.exit(1)
version = state.get('version', 'v001')
design_path = get_workflow_dir() / 'versions' / version / 'design' / 'design_document.yml'
if not design_path.exists():
print(f"Error: Design document not found at {design_path}")
sys.exit(1)
# Run type generation script
import subprocess
scripts_dir = Path(__file__).parent
gentypes_script = scripts_dir / 'generate_types.py'
cmd = ['python3', str(gentypes_script), str(design_path), '--output-dir', args.output_dir]
result = subprocess.run(cmd, capture_output=False)
sys.exit(result.returncode)
elif args.command == 'checklist':
state = load_current_workflow()
if not state:
print("Error: No active workflow")
sys.exit(1)
version = state.get('version', 'v001')
checklist_path = get_workflow_dir() / 'versions' / version / 'checklist.yml'
if args.action == 'show':
if checklist_path.exists():
checklist = load_yaml(str(checklist_path))
print("\n" + "=" * 60)
print("IMPLEMENTATION CHECKLIST".center(60))
print("=" * 60)
for category, items in checklist.get('items', {}).items():
print(f"\n{category.upper()}")
print("-" * 40)
for item_id, item in items.items():
status = "" if item.get('checked') else ""
ref = f"{item.get('reference')}" if item.get('reference') else ""
print(f" {status} {item_id}{ref}")
# Summary
total = sum(len(items) for items in checklist.get('items', {}).values())
checked = sum(1 for items in checklist.get('items', {}).values()
for item in items.values() if item.get('checked'))
print(f"\n{'=' * 60}")
print(f" Progress: {checked}/{total} ({100*checked//total if total else 0}%)")
print(f"{'=' * 60}\n")
else:
print("No checklist found. Run 'validate --checklist' first.")
elif args.action == 'check':
if not args.item:
print("Error: --item required")
sys.exit(1)
checklist = load_yaml(str(checklist_path)) if checklist_path.exists() else {'items': {}}
# Find and check the item
found = False
for category, items in checklist.get('items', {}).items():
if args.item in items:
items[args.item]['checked'] = True
items[args.item]['checked_at'] = datetime.now().isoformat()
if args.reference:
items[args.item]['reference'] = args.reference
found = True
break
if found:
save_yaml(str(checklist_path), checklist)
print(f"✅ Checked: {args.item}")
if args.reference:
print(f" Reference: {args.reference}")
else:
print(f"Error: Item '{args.item}' not found in checklist")
sys.exit(1)
elif args.action == 'uncheck':
if not args.item:
print("Error: --item required")
sys.exit(1)
checklist = load_yaml(str(checklist_path)) if checklist_path.exists() else {'items': {}}
for category, items in checklist.get('items', {}).items():
if args.item in items:
items[args.item]['checked'] = False
items[args.item].pop('checked_at', None)
items[args.item].pop('reference', None)
save_yaml(str(checklist_path), checklist)
print(f"⬜ Unchecked: {args.item}")
break
else:
print(f"Error: Item '{args.item}' not found")
sys.exit(1)
elif args.action == 'summary':
if checklist_path.exists():
checklist = load_yaml(str(checklist_path))
total = sum(len(items) for items in checklist.get('items', {}).values())
checked = sum(1 for items in checklist.get('items', {}).values()
for item in items.values() if item.get('checked'))
print(f"\n📋 Checklist Summary")
print(f" Total items: {total}")
print(f" Checked: {checked}")
print(f" Remaining: {total - checked}")
print(f" Progress: {100*checked//total if total else 0}%\n")
else:
print("No checklist found.")
else:
parser.print_help()
if __name__ == "__main__":
main()