364 lines
12 KiB
Python
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()
|