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

364 lines
12 KiB
Python

#!/usr/bin/env python3
"""Task management utilities for the guardrail workflow."""
import argparse
import os
import sys
from datetime import datetime
from pathlib import Path
# Try to import yaml, fall back to basic parsing if not available
try:
import yaml
HAS_YAML = True
except ImportError:
HAS_YAML = False
def parse_yaml_simple(content: str) -> dict:
"""Simple YAML parser for basic task files."""
result = {}
current_key = None
current_list = None
for line in content.split('\n'):
line = line.rstrip()
if not line or line.startswith('#'):
continue
# Handle list items
if line.startswith(' - '):
if current_list is not None:
current_list.append(line[4:].strip())
continue
# Handle key-value pairs
if ':' in line and not line.startswith(' '):
key, _, value = line.partition(':')
key = key.strip()
value = value.strip()
if value:
result[key] = value
current_list = None
else:
result[key] = []
current_list = result[key]
current_key = key
return result
def load_yaml(filepath: str) -> dict:
"""Load YAML file."""
with open(filepath, 'r') as f:
content = f.read()
if HAS_YAML:
return yaml.safe_load(content) or {}
return parse_yaml_simple(content)
def save_yaml(filepath: str, data: dict):
"""Save data to YAML file."""
if HAS_YAML:
with open(filepath, 'w') as f:
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
else:
# Simple YAML writer
lines = []
for key, value in data.items():
if isinstance(value, list):
lines.append(f"{key}:")
for item in value:
lines.append(f" - {item}")
elif isinstance(value, str) and '\n' in value:
lines.append(f"{key}: |")
for line in value.split('\n'):
lines.append(f" {line}")
else:
lines.append(f"{key}: {value}")
with open(filepath, 'w') as f:
f.write('\n'.join(lines))
# ============================================================================
# Version-Aware Task Directory
# ============================================================================
def get_workflow_dir() -> Path:
"""Get the .workflow directory path."""
return Path('.workflow')
def get_current_tasks_dir() -> str:
"""Get the tasks directory for the currently active workflow version.
Returns the version-specific tasks directory if a workflow is active,
otherwise falls back to 'tasks' for backward compatibility.
"""
current_path = get_workflow_dir() / 'current.yml'
if not current_path.exists():
return 'tasks' # Fallback for no active workflow
current = load_yaml(str(current_path))
version = current.get('active_version')
if not version:
return 'tasks' # Fallback
tasks_dir = get_workflow_dir() / 'versions' / version / 'tasks'
tasks_dir.mkdir(parents=True, exist_ok=True)
return str(tasks_dir)
# ============================================================================
# Task Operations
# ============================================================================
def find_tasks(tasks_dir: str, filters: dict = None) -> list:
"""Find all task files matching filters."""
tasks = []
tasks_path = Path(tasks_dir)
if not tasks_path.exists():
return tasks
for filepath in tasks_path.glob('**/*.yml'):
try:
task = load_yaml(str(filepath))
task['_filepath'] = str(filepath)
# Apply filters
if filters:
match = True
for key, value in filters.items():
if task.get(key) != value:
match = False
break
if not match:
continue
tasks.append(task)
except Exception as e:
print(f"Warning: Could not parse {filepath}: {e}", file=sys.stderr)
return tasks
def list_tasks(tasks_dir: str, status: str = None, agent: str = None):
"""List tasks with optional filtering."""
filters = {}
if status:
filters['status'] = status
if agent:
filters['agent'] = agent
tasks = find_tasks(tasks_dir, filters)
if not tasks:
print("No tasks found.")
return
# Group by status
by_status = {}
for task in tasks:
s = task.get('status', 'unknown')
if s not in by_status:
by_status[s] = []
by_status[s].append(task)
print("\n" + "=" * 60)
print("TASK LIST")
print("=" * 60)
status_order = ['pending', 'in_progress', 'review', 'approved', 'completed', 'blocked']
for s in status_order:
if s in by_status:
print(f"\n{s.upper()} ({len(by_status[s])})")
print("-" * 40)
for task in by_status[s]:
agent = task.get('agent', '?')
priority = task.get('priority', 'medium')
print(f" [{agent}] {task.get('id', 'unknown')} ({priority})")
print(f" {task.get('title', 'No title')}")
def get_next_task(tasks_dir: str, agent: str) -> dict:
"""Get next available task for an agent."""
tasks = find_tasks(tasks_dir, {'agent': agent, 'status': 'pending'})
if not tasks:
return None
# Sort by priority (high > medium > low)
priority_order = {'high': 0, 'medium': 1, 'low': 2}
tasks.sort(key=lambda t: priority_order.get(t.get('priority', 'medium'), 1))
# Check dependencies
for task in tasks:
deps = task.get('dependencies', [])
if not deps:
return task
# Check if all dependencies are completed
all_deps_done = True
for dep_id in deps:
dep_tasks = find_tasks(tasks_dir, {'id': dep_id})
if dep_tasks and dep_tasks[0].get('status') != 'completed':
all_deps_done = False
break
if all_deps_done:
return task
return None
def update_task_status(tasks_dir: str, task_id: str, new_status: str, notes: str = None):
"""Update task status."""
tasks = find_tasks(tasks_dir, {'id': task_id})
if not tasks:
print(f"Error: Task {task_id} not found")
return False
task = tasks[0]
filepath = task['_filepath']
# Remove internal field
del task['_filepath']
# Update status
task['status'] = new_status
if new_status == 'completed':
task['completed_at'] = datetime.now().isoformat()
if notes:
task['review_notes'] = notes
save_yaml(filepath, task)
print(f"Updated {task_id} to {new_status}")
return True
def complete_all_tasks(tasks_dir: str):
"""Mark all non-completed tasks as completed."""
tasks = find_tasks(tasks_dir)
completed_count = 0
for task in tasks:
if task.get('status') != 'completed':
filepath = task['_filepath']
del task['_filepath']
task['status'] = 'completed'
task['completed_at'] = datetime.now().isoformat()
save_yaml(filepath, task)
completed_count += 1
print(f" Completed: {task.get('id', 'unknown')}")
print(f"\nMarked {completed_count} task(s) as completed.")
return completed_count
def show_status(tasks_dir: str, manifest_path: str):
"""Show overall workflow status."""
tasks = find_tasks(tasks_dir)
# Count by status
status_counts = {}
agent_counts = {'frontend': {'pending': 0, 'completed': 0},
'backend': {'pending': 0, 'completed': 0},
'reviewer': {'pending': 0}}
for task in tasks:
s = task.get('status', 'unknown')
status_counts[s] = status_counts.get(s, 0) + 1
agent = task.get('agent', 'unknown')
if agent in agent_counts:
if s == 'pending':
agent_counts[agent]['pending'] += 1
elif s == 'completed':
if 'completed' in agent_counts[agent]:
agent_counts[agent]['completed'] += 1
print("\n" + "" + "" * 58 + "")
print("" + "WORKFLOW STATUS".center(58) + "")
print("" + "" * 58 + "")
print("" + " TASKS BY STATUS".ljust(58) + "")
print("" + f" ⏳ Pending: {status_counts.get('pending', 0)}".ljust(58) + "")
print("" + f" 🔄 In Progress: {status_counts.get('in_progress', 0)}".ljust(58) + "")
print("" + f" 🔍 Review: {status_counts.get('review', 0)}".ljust(58) + "")
print("" + f" ✅ Approved: {status_counts.get('approved', 0)}".ljust(58) + "")
print("" + f" ✓ Completed: {status_counts.get('completed', 0)}".ljust(58) + "")
print("" + f" 🚫 Blocked: {status_counts.get('blocked', 0)}".ljust(58) + "")
print("" + "" * 58 + "")
print("" + " TASKS BY AGENT".ljust(58) + "")
print("" + f" 🎨 Frontend: {agent_counts['frontend']['pending']} pending, {agent_counts['frontend']['completed']} completed".ljust(58) + "")
print("" + f" ⚙️ Backend: {agent_counts['backend']['pending']} pending, {agent_counts['backend']['completed']} completed".ljust(58) + "")
print("" + f" 🔍 Reviewer: {agent_counts['reviewer']['pending']} pending".ljust(58) + "")
print("" + "" * 58 + "")
def main():
parser = argparse.ArgumentParser(description="Task management for guardrail workflow")
subparsers = parser.add_subparsers(dest='command', help='Commands')
# list command
list_parser = subparsers.add_parser('list', help='List tasks')
list_parser.add_argument('--status', help='Filter by status')
list_parser.add_argument('--agent', help='Filter by agent')
list_parser.add_argument('--tasks-dir', default=None, help='Tasks directory (defaults to current version)')
# next command
next_parser = subparsers.add_parser('next', help='Get next task for agent')
next_parser.add_argument('agent', choices=['frontend', 'backend', 'reviewer'])
next_parser.add_argument('--tasks-dir', default=None, help='Tasks directory (defaults to current version)')
# update command
update_parser = subparsers.add_parser('update', help='Update task status')
update_parser.add_argument('task_id', help='Task ID')
update_parser.add_argument('status', choices=['pending', 'in_progress', 'review', 'approved', 'completed', 'blocked'])
update_parser.add_argument('--notes', help='Review notes')
update_parser.add_argument('--tasks-dir', default=None, help='Tasks directory (defaults to current version)')
# status command
status_parser = subparsers.add_parser('status', help='Show workflow status')
status_parser.add_argument('--tasks-dir', default=None, help='Tasks directory (defaults to current version)')
status_parser.add_argument('--manifest', default='project_manifest.json', help='Manifest path')
# complete-all command
complete_all_parser = subparsers.add_parser('complete-all', help='Mark all tasks as completed')
complete_all_parser.add_argument('--tasks-dir', default=None, help='Tasks directory (defaults to current version)')
args = parser.parse_args()
# Resolve tasks_dir to version-specific directory if not explicitly provided
if hasattr(args, 'tasks_dir') and args.tasks_dir is None:
args.tasks_dir = get_current_tasks_dir()
if args.command == 'list':
list_tasks(args.tasks_dir, args.status, args.agent)
elif args.command == 'next':
task = get_next_task(args.tasks_dir, args.agent)
if task:
print(f"Next task for {args.agent}: {task.get('id')}")
print(f" Title: {task.get('title')}")
print(f" Files: {task.get('file_paths', [])}")
else:
print(f"No pending tasks for {args.agent}")
elif args.command == 'update':
update_task_status(args.tasks_dir, args.task_id, args.status, args.notes)
elif args.command == 'status':
show_status(args.tasks_dir, args.manifest)
elif args.command == 'complete-all':
complete_all_tasks(args.tasks_dir)
else:
parser.print_help()
if __name__ == "__main__":
main()