Initial commit
This commit is contained in:
238
hooks/utils/workflow/plan_parser.py
Executable file
238
hooks/utils/workflow/plan_parser.py
Executable file
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "python-dotenv",
|
||||
# "anthropic",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
"""
|
||||
Plan Parser Utility
|
||||
|
||||
Uses Claude Haiku 4.5 to break down requirements into structured implementation plans.
|
||||
Creates .titanium/plan.json with epics, stories, tasks, and agent assignments.
|
||||
|
||||
Usage:
|
||||
uv run plan_parser.py <requirements_file> <project_path>
|
||||
|
||||
Example:
|
||||
uv run plan_parser.py .titanium/requirements.md "$(pwd)"
|
||||
|
||||
Output:
|
||||
- Creates .titanium/plan.json with structured plan
|
||||
- Prints JSON to stdout
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
||||
def get_claude_model(task_type: str = "default") -> str:
|
||||
"""
|
||||
Get Claude model based on task complexity.
|
||||
|
||||
Args:
|
||||
task_type: "complex" for large model, "default" for small model
|
||||
|
||||
Returns:
|
||||
Model name string
|
||||
"""
|
||||
load_dotenv()
|
||||
|
||||
if task_type == "complex":
|
||||
# Use large model (Sonnet) for complex tasks
|
||||
return os.getenv("ANTHROPIC_LARGE_MODEL", "claude-sonnet-4-5-20250929")
|
||||
else:
|
||||
# Use small model (Haiku) for faster tasks
|
||||
return os.getenv("ANTHROPIC_SMALL_MODEL", "claude-haiku-4-5-20251001")
|
||||
|
||||
|
||||
def parse_requirements_to_plan(requirements_text: str, project_path: str) -> dict:
|
||||
"""
|
||||
Use Claude Haiku 4.5 to break down requirements into structured plan.
|
||||
|
||||
Args:
|
||||
requirements_text: Requirements document text
|
||||
project_path: Absolute path to project directory
|
||||
|
||||
Returns:
|
||||
Structured plan dictionary with epics, stories, tasks
|
||||
"""
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
api_key = os.getenv("ANTHROPIC_API_KEY")
|
||||
if not api_key:
|
||||
print("Error: ANTHROPIC_API_KEY not found in environment variables", file=sys.stderr)
|
||||
print("Please add your Anthropic API key to ~/.env file:", file=sys.stderr)
|
||||
print("ANTHROPIC_API_KEY=sk-ant-your-key-here", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
from anthropic import Anthropic
|
||||
client = Anthropic(api_key=api_key)
|
||||
except ImportError:
|
||||
print("Error: anthropic package not installed", file=sys.stderr)
|
||||
print("This should be handled by uv automatically.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Build Claude prompt
|
||||
prompt = f"""Analyze these requirements and create a structured implementation plan.
|
||||
|
||||
Requirements:
|
||||
{requirements_text}
|
||||
|
||||
Create a JSON plan with this exact structure:
|
||||
{{
|
||||
"epics": [
|
||||
{{
|
||||
"name": "Epic name",
|
||||
"description": "Epic description",
|
||||
"stories": [
|
||||
{{
|
||||
"name": "Story name",
|
||||
"description": "User story or technical description",
|
||||
"tasks": [
|
||||
{{
|
||||
"name": "Task name",
|
||||
"agent": "@agent-name",
|
||||
"estimated_time": "30m",
|
||||
"dependencies": []
|
||||
}}
|
||||
]
|
||||
}}
|
||||
]
|
||||
}}
|
||||
],
|
||||
"agents_needed": ["@api-developer", "@frontend-developer"],
|
||||
"estimated_total_time": "4h"
|
||||
}}
|
||||
|
||||
Available agents to use:
|
||||
- @product-manager: Requirements validation, clarification, acceptance criteria
|
||||
- @api-developer: Backend APIs (REST/GraphQL), database, authentication
|
||||
- @frontend-developer: UI/UX, React/Vue/etc, responsive design
|
||||
- @devops-engineer: CI/CD, deployment, infrastructure, Docker/K8s
|
||||
- @test-runner: Running tests, test execution, test reporting
|
||||
- @tdd-specialist: Writing tests, test-driven development, test design
|
||||
- @code-reviewer: Code review, best practices, code quality
|
||||
- @security-scanner: Security vulnerabilities, security best practices
|
||||
- @doc-writer: Technical documentation, API docs, README files
|
||||
- @api-documenter: OpenAPI/Swagger specs, API documentation
|
||||
- @debugger: Debugging, error analysis, troubleshooting
|
||||
- @refactor: Code refactoring, code improvement, tech debt
|
||||
- @project-planner: Project breakdown, task planning, estimation
|
||||
- @shadcn-ui-builder: UI components using shadcn/ui library
|
||||
- @meta-agent: Creating new custom agents
|
||||
|
||||
Guidelines:
|
||||
1. Break down into logical epics (major features)
|
||||
2. Each epic should have 1-5 stories
|
||||
3. Each story should have 2-10 tasks
|
||||
4. Assign the most appropriate agent to each task
|
||||
5. Estimate time realistically (15m, 30m, 1h, 2h, etc.)
|
||||
6. List dependencies between tasks (use task names)
|
||||
7. Start with @product-manager for requirements validation
|
||||
8. Always include @test-runner or @tdd-specialist for testing
|
||||
9. Consider @security-scanner for auth/payment/sensitive features
|
||||
10. End with @doc-writer for documentation
|
||||
|
||||
Return ONLY valid JSON, no markdown code blocks, no explanations."""
|
||||
|
||||
try:
|
||||
# Get model (configurable via env var, defaults to Sonnet for complex epics)
|
||||
model = get_claude_model("complex") # Use large model for complex epics
|
||||
|
||||
# Call Claude
|
||||
response = client.messages.create(
|
||||
model=model,
|
||||
max_tokens=8192, # Increased for large epics with many stories
|
||||
temperature=0.3, # Lower temperature for deterministic planning
|
||||
messages=[{"role": "user", "content": prompt}]
|
||||
)
|
||||
|
||||
plan_json = response.content[0].text.strip()
|
||||
|
||||
# Clean up markdown code blocks if present
|
||||
if plan_json.startswith("```json"):
|
||||
plan_json = plan_json[7:]
|
||||
if plan_json.startswith("```"):
|
||||
plan_json = plan_json[3:]
|
||||
if plan_json.endswith("```"):
|
||||
plan_json = plan_json[:-3]
|
||||
plan_json = plan_json.strip()
|
||||
|
||||
# Parse and validate JSON
|
||||
plan = json.loads(plan_json)
|
||||
|
||||
# Validate structure
|
||||
if "epics" not in plan:
|
||||
raise ValueError("Plan missing 'epics' field")
|
||||
if "agents_needed" not in plan:
|
||||
raise ValueError("Plan missing 'agents_needed' field")
|
||||
if "estimated_total_time" not in plan:
|
||||
raise ValueError("Plan missing 'estimated_total_time' field")
|
||||
|
||||
# Save plan to file
|
||||
plan_path = Path(project_path) / ".titanium" / "plan.json"
|
||||
plan_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Atomic write
|
||||
temp_path = plan_path.with_suffix('.tmp')
|
||||
with open(temp_path, 'w') as f:
|
||||
json.dump(plan, f, indent=2)
|
||||
temp_path.replace(plan_path)
|
||||
|
||||
return plan
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Claude returned invalid JSON: {e}", file=sys.stderr)
|
||||
print(f"Response was: {plan_json[:200]}...", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error calling Claude API: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI interface for plan parsing."""
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: plan_parser.py <requirements_file> <project_path>", file=sys.stderr)
|
||||
print("\nExample:", file=sys.stderr)
|
||||
print(" uv run plan_parser.py .titanium/requirements.md \"$(pwd)\"", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
requirements_file = sys.argv[1]
|
||||
project_path = sys.argv[2]
|
||||
|
||||
# Validate requirements file exists
|
||||
if not Path(requirements_file).exists():
|
||||
print(f"Error: Requirements file not found: {requirements_file}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Read requirements
|
||||
try:
|
||||
with open(requirements_file, 'r') as f:
|
||||
requirements_text = f.read()
|
||||
except Exception as e:
|
||||
print(f"Error reading requirements file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not requirements_text.strip():
|
||||
print("Error: Requirements file is empty", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Parse requirements to plan
|
||||
plan = parse_requirements_to_plan(requirements_text, project_path)
|
||||
|
||||
# Output plan to stdout
|
||||
print(json.dumps(plan, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
253
hooks/utils/workflow/workflow_state.py
Executable file
253
hooks/utils/workflow/workflow_state.py
Executable file
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "python-dotenv",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
"""
|
||||
Workflow State Management Utility
|
||||
|
||||
Manages workflow state via file-based JSON storage in .titanium/workflow-state.json
|
||||
|
||||
Commands:
|
||||
init <project_path> <workflow_type> <goal> Initialize new workflow
|
||||
update_phase <project_path> <phase> <status> Update current phase
|
||||
get <project_path> Get current state
|
||||
complete <project_path> Mark workflow complete
|
||||
|
||||
Examples:
|
||||
uv run workflow_state.py init "$(pwd)" "development" "Implement user auth"
|
||||
uv run workflow_state.py update_phase "$(pwd)" "implementation" "in_progress"
|
||||
uv run workflow_state.py get "$(pwd)"
|
||||
uv run workflow_state.py complete "$(pwd)"
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# Constants
|
||||
STATE_FILE = ".titanium/workflow-state.json"
|
||||
|
||||
|
||||
def init_workflow(project_path: str, workflow_type: str, goal: str) -> dict:
|
||||
"""
|
||||
Initialize a new workflow state file.
|
||||
|
||||
Args:
|
||||
project_path: Absolute path to project directory
|
||||
workflow_type: Type of workflow (development, bug-fix, refactor, review)
|
||||
goal: User's stated goal for this workflow
|
||||
|
||||
Returns:
|
||||
Initial state dictionary
|
||||
"""
|
||||
state_path = Path(project_path) / STATE_FILE
|
||||
state_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
state = {
|
||||
"workflow_type": workflow_type,
|
||||
"goal": goal,
|
||||
"status": "planning",
|
||||
"started_at": datetime.now().isoformat(),
|
||||
"current_phase": "planning",
|
||||
"phases": [],
|
||||
"completed_tasks": [],
|
||||
"pending_tasks": []
|
||||
}
|
||||
|
||||
# Atomic write
|
||||
temp_path = state_path.with_suffix('.tmp')
|
||||
with open(temp_path, 'w') as f:
|
||||
json.dump(state, f, indent=2)
|
||||
temp_path.replace(state_path)
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def update_phase(project_path: str, phase_name: str, status: str = "in_progress") -> dict:
|
||||
"""
|
||||
Update current workflow phase.
|
||||
|
||||
Args:
|
||||
project_path: Absolute path to project directory
|
||||
phase_name: Name of phase (planning, implementation, review, completed)
|
||||
status: Status of phase (in_progress, completed, failed)
|
||||
|
||||
Returns:
|
||||
Updated state dictionary or None if state doesn't exist
|
||||
"""
|
||||
state_path = Path(project_path) / STATE_FILE
|
||||
|
||||
if not state_path.exists():
|
||||
print(f"Error: No workflow state found at {state_path}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
# Read current state
|
||||
with open(state_path, 'r') as f:
|
||||
state = json.load(f)
|
||||
|
||||
# Update current phase and status
|
||||
state["current_phase"] = phase_name
|
||||
state["status"] = status
|
||||
|
||||
# Update or add phase
|
||||
phase_exists = False
|
||||
for i, p in enumerate(state["phases"]):
|
||||
if p["name"] == phase_name:
|
||||
# Preserve original started_at when updating existing phase
|
||||
state["phases"][i]["status"] = status
|
||||
# Only add completed_at if completing and doesn't already exist
|
||||
if status == "completed" and "completed_at" not in state["phases"][i]:
|
||||
state["phases"][i]["completed_at"] = datetime.now().isoformat()
|
||||
phase_exists = True
|
||||
break
|
||||
|
||||
if not phase_exists:
|
||||
# Create new phase entry with current timestamp
|
||||
phase_entry = {
|
||||
"name": phase_name,
|
||||
"status": status,
|
||||
"started_at": datetime.now().isoformat()
|
||||
}
|
||||
if status == "completed":
|
||||
phase_entry["completed_at"] = datetime.now().isoformat()
|
||||
state["phases"].append(phase_entry)
|
||||
|
||||
# Atomic write
|
||||
temp_path = state_path.with_suffix('.tmp')
|
||||
with open(temp_path, 'w') as f:
|
||||
json.dump(state, f, indent=2)
|
||||
temp_path.replace(state_path)
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def get_state(project_path: str) -> dict:
|
||||
"""
|
||||
Get current workflow state.
|
||||
|
||||
Args:
|
||||
project_path: Absolute path to project directory
|
||||
|
||||
Returns:
|
||||
State dictionary or None if state doesn't exist
|
||||
"""
|
||||
state_path = Path(project_path) / STATE_FILE
|
||||
|
||||
if not state_path.exists():
|
||||
return None
|
||||
|
||||
with open(state_path, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def complete_workflow(project_path: str) -> dict:
|
||||
"""
|
||||
Mark workflow as complete.
|
||||
|
||||
Args:
|
||||
project_path: Absolute path to project directory
|
||||
|
||||
Returns:
|
||||
Updated state dictionary or None if state doesn't exist
|
||||
"""
|
||||
state_path = Path(project_path) / STATE_FILE
|
||||
|
||||
if not state_path.exists():
|
||||
print(f"Error: No workflow state found at {state_path}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
# Read current state
|
||||
with open(state_path, 'r') as f:
|
||||
state = json.load(f)
|
||||
|
||||
# Update to completed
|
||||
state["status"] = "completed"
|
||||
state["current_phase"] = "completed"
|
||||
state["completed_at"] = datetime.now().isoformat()
|
||||
|
||||
# Mark current phase as completed if it exists
|
||||
if state["phases"]:
|
||||
for phase in state["phases"]:
|
||||
if phase["status"] == "in_progress":
|
||||
phase["status"] = "completed"
|
||||
phase["completed_at"] = datetime.now().isoformat()
|
||||
|
||||
# Atomic write
|
||||
temp_path = state_path.with_suffix('.tmp')
|
||||
with open(temp_path, 'w') as f:
|
||||
json.dump(state, f, indent=2)
|
||||
temp_path.replace(state_path)
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI interface for workflow state management."""
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: workflow_state.py <command> <project_path> [args...]", file=sys.stderr)
|
||||
print("\nCommands:", file=sys.stderr)
|
||||
print(" init <project_path> <workflow_type> <goal>", file=sys.stderr)
|
||||
print(" update_phase <project_path> <phase> [status]", file=sys.stderr)
|
||||
print(" get <project_path>", file=sys.stderr)
|
||||
print(" complete <project_path>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1]
|
||||
project_path = sys.argv[2]
|
||||
|
||||
try:
|
||||
if command == "init":
|
||||
if len(sys.argv) < 5:
|
||||
print("Error: init requires workflow_type and goal", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
workflow_type = sys.argv[3]
|
||||
goal = sys.argv[4]
|
||||
state = init_workflow(project_path, workflow_type, goal)
|
||||
print(json.dumps(state, indent=2))
|
||||
|
||||
elif command == "update_phase":
|
||||
if len(sys.argv) < 4:
|
||||
print("Error: update_phase requires phase_name", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
phase_name = sys.argv[3]
|
||||
status = sys.argv[4] if len(sys.argv) > 4 else "in_progress"
|
||||
state = update_phase(project_path, phase_name, status)
|
||||
if state:
|
||||
print(json.dumps(state, indent=2))
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
elif command == "get":
|
||||
state = get_state(project_path)
|
||||
if state:
|
||||
print(json.dumps(state, indent=2))
|
||||
else:
|
||||
print("No workflow found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
elif command == "complete":
|
||||
state = complete_workflow(project_path)
|
||||
if state:
|
||||
print(json.dumps(state, indent=2))
|
||||
else:
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
print(f"Error: Unknown command: {command}", file=sys.stderr)
|
||||
print("\nValid commands: init, update_phase, get, complete", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user