254 lines
6.6 KiB
Python
254 lines
6.6 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Phase Management Utilities.
|
||
|
|
|
||
|
|
Centralized phase tracking for multi-agent pipeline.
|
||
|
|
|
||
|
|
Provides:
|
||
|
|
get_current_phase - Returns current phase number
|
||
|
|
get_total_phases - Returns total phase count
|
||
|
|
get_phase_action - Returns action name for phase
|
||
|
|
get_phase_info - Returns "N/M (action)" format
|
||
|
|
set_phase - Sets current_phase
|
||
|
|
advance_phase - Advances to next phase
|
||
|
|
get_phase_for_action - Returns phase number for action
|
||
|
|
map_subagent_to_action - Map subagent type to action name
|
||
|
|
is_phase_completed - Check if phase is completed
|
||
|
|
is_current_action - Check if at specific action
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
|
||
|
|
def _read_json_file(path: Path) -> dict | None:
|
||
|
|
"""Read and parse a JSON file."""
|
||
|
|
try:
|
||
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
||
|
|
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def _write_json_file(path: Path, data: dict) -> bool:
|
||
|
|
"""Write dict to JSON file."""
|
||
|
|
try:
|
||
|
|
path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
||
|
|
return True
|
||
|
|
except (OSError, IOError):
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Phase Functions
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
def get_current_phase(task_json: Path) -> int:
|
||
|
|
"""Get current phase number.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
task_json: Path to task.json file.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Current phase number, or 0 if not found.
|
||
|
|
"""
|
||
|
|
data = _read_json_file(task_json)
|
||
|
|
if not data:
|
||
|
|
return 0
|
||
|
|
return data.get("current_phase", 0) or 0
|
||
|
|
|
||
|
|
|
||
|
|
def get_total_phases(task_json: Path) -> int:
|
||
|
|
"""Get total number of phases.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
task_json: Path to task.json file.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Total phase count, or 0 if not found.
|
||
|
|
"""
|
||
|
|
data = _read_json_file(task_json)
|
||
|
|
if not data:
|
||
|
|
return 0
|
||
|
|
|
||
|
|
next_action = data.get("next_action", [])
|
||
|
|
if isinstance(next_action, list):
|
||
|
|
return len(next_action)
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
def get_phase_action(task_json: Path, phase: int) -> str:
|
||
|
|
"""Get action name for a specific phase.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
task_json: Path to task.json file.
|
||
|
|
phase: Phase number.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Action name, or "unknown" if not found.
|
||
|
|
"""
|
||
|
|
data = _read_json_file(task_json)
|
||
|
|
if not data:
|
||
|
|
return "unknown"
|
||
|
|
|
||
|
|
next_action = data.get("next_action", [])
|
||
|
|
if isinstance(next_action, list):
|
||
|
|
for item in next_action:
|
||
|
|
if isinstance(item, dict) and item.get("phase") == phase:
|
||
|
|
return item.get("action", "unknown")
|
||
|
|
return "unknown"
|
||
|
|
|
||
|
|
|
||
|
|
def get_phase_info(task_json: Path) -> str:
|
||
|
|
"""Get formatted phase info: "N/M (action)".
|
||
|
|
|
||
|
|
Args:
|
||
|
|
task_json: Path to task.json file.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Formatted string like "1/4 (implement)".
|
||
|
|
"""
|
||
|
|
data = _read_json_file(task_json)
|
||
|
|
if not data:
|
||
|
|
return "N/A"
|
||
|
|
|
||
|
|
current_phase = data.get("current_phase", 0) or 0
|
||
|
|
total_phases = get_total_phases(task_json)
|
||
|
|
action_name = get_phase_action(task_json, current_phase)
|
||
|
|
|
||
|
|
if current_phase == 0 or current_phase is None:
|
||
|
|
return f"0/{total_phases} (pending)"
|
||
|
|
else:
|
||
|
|
return f"{current_phase}/{total_phases} ({action_name})"
|
||
|
|
|
||
|
|
|
||
|
|
def set_phase(task_json: Path, phase: int) -> bool:
|
||
|
|
"""Set current phase to a specific value.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
task_json: Path to task.json file.
|
||
|
|
phase: Phase number to set.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True on success, False on error.
|
||
|
|
"""
|
||
|
|
data = _read_json_file(task_json)
|
||
|
|
if not data:
|
||
|
|
return False
|
||
|
|
|
||
|
|
data["current_phase"] = phase
|
||
|
|
return _write_json_file(task_json, data)
|
||
|
|
|
||
|
|
|
||
|
|
def advance_phase(task_json: Path) -> bool:
|
||
|
|
"""Advance to next phase.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
task_json: Path to task.json file.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True on success, False on error or at final phase.
|
||
|
|
"""
|
||
|
|
data = _read_json_file(task_json)
|
||
|
|
if not data:
|
||
|
|
return False
|
||
|
|
|
||
|
|
current = data.get("current_phase", 0) or 0
|
||
|
|
total = get_total_phases(task_json)
|
||
|
|
next_phase = current + 1
|
||
|
|
|
||
|
|
if next_phase > total:
|
||
|
|
return False # Already at final phase
|
||
|
|
|
||
|
|
data["current_phase"] = next_phase
|
||
|
|
return _write_json_file(task_json, data)
|
||
|
|
|
||
|
|
|
||
|
|
def get_phase_for_action(task_json: Path, action: str) -> int:
|
||
|
|
"""Get phase number for a specific action name.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
task_json: Path to task.json file.
|
||
|
|
action: Action name.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Phase number, or 0 if not found.
|
||
|
|
"""
|
||
|
|
data = _read_json_file(task_json)
|
||
|
|
if not data:
|
||
|
|
return 0
|
||
|
|
|
||
|
|
next_action = data.get("next_action", [])
|
||
|
|
if isinstance(next_action, list):
|
||
|
|
for item in next_action:
|
||
|
|
if isinstance(item, dict) and item.get("action") == action:
|
||
|
|
return item.get("phase", 0)
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
def map_subagent_to_action(subagent_type: str) -> str:
|
||
|
|
"""Map subagent type to action name.
|
||
|
|
|
||
|
|
Used by hooks to determine which action a subagent corresponds to.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
subagent_type: Subagent type string.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Corresponding action name.
|
||
|
|
"""
|
||
|
|
mapping = {
|
||
|
|
"implement": "implement",
|
||
|
|
"check": "check",
|
||
|
|
"debug": "debug",
|
||
|
|
"research": "research",
|
||
|
|
}
|
||
|
|
return mapping.get(subagent_type, subagent_type)
|
||
|
|
|
||
|
|
|
||
|
|
def is_phase_completed(task_json: Path, phase: int) -> bool:
|
||
|
|
"""Check if a phase is completed (current_phase > phase).
|
||
|
|
|
||
|
|
Args:
|
||
|
|
task_json: Path to task.json file.
|
||
|
|
phase: Phase number to check.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if phase is completed.
|
||
|
|
"""
|
||
|
|
current = get_current_phase(task_json)
|
||
|
|
return current > phase
|
||
|
|
|
||
|
|
|
||
|
|
def is_current_action(task_json: Path, action: str) -> bool:
|
||
|
|
"""Check if we're at a specific action.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
task_json: Path to task.json file.
|
||
|
|
action: Action name to check.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if current phase matches the action.
|
||
|
|
"""
|
||
|
|
current = get_current_phase(task_json)
|
||
|
|
action_phase = get_phase_for_action(task_json, action)
|
||
|
|
return current == action_phase
|
||
|
|
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# Main Entry (for testing)
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
import sys
|
||
|
|
|
||
|
|
if len(sys.argv) > 1:
|
||
|
|
path = Path(sys.argv[1])
|
||
|
|
print(f"Task JSON: {path}")
|
||
|
|
print(f"Phase info: {get_phase_info(path)}")
|
||
|
|
print(f"Current phase: {get_current_phase(path)}")
|
||
|
|
print(f"Total phases: {get_total_phases(path)}")
|
||
|
|
else:
|
||
|
|
print("Usage: python3 phase.py <task.json>")
|