376 lines
11 KiB
Python
Executable File
376 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Unified Session Tracker Hook - Dispatcher for SubagentStop Session Tracking
|
|
|
|
Consolidates SubagentStop session tracking hooks:
|
|
- session_tracker.py (basic session logging)
|
|
- log_agent_completion.py (structured pipeline tracking)
|
|
- auto_update_project_progress.py (PROJECT.md progress updates)
|
|
|
|
Hook: SubagentStop (runs when a subagent completes)
|
|
|
|
Environment Variables (opt-in/opt-out):
|
|
TRACK_SESSIONS=true/false (default: true)
|
|
TRACK_PIPELINE=true/false (default: true)
|
|
AUTO_UPDATE_PROGRESS=true/false (default: false)
|
|
|
|
Environment Variables (provided by Claude Code):
|
|
CLAUDE_AGENT_NAME - Name of the subagent that completed
|
|
CLAUDE_AGENT_OUTPUT - Output from the subagent
|
|
CLAUDE_AGENT_STATUS - Status: "success" or "error"
|
|
|
|
Exit codes:
|
|
0: Always (non-blocking hook)
|
|
|
|
Usage:
|
|
# As SubagentStop hook (automatic)
|
|
CLAUDE_AGENT_NAME=researcher CLAUDE_AGENT_STATUS=success python unified_session_tracker.py
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
|
|
|
|
# ============================================================================
|
|
# Dynamic Library Discovery
|
|
# ============================================================================
|
|
|
|
def find_lib_dir() -> Optional[Path]:
|
|
"""
|
|
Find the lib directory dynamically.
|
|
|
|
Searches:
|
|
1. Relative to this file: ../lib
|
|
2. In project root: plugins/autonomous-dev/lib
|
|
3. In global install: ~/.autonomous-dev/lib
|
|
|
|
Returns:
|
|
Path to lib directory or None if not found
|
|
"""
|
|
candidates = [
|
|
Path(__file__).parent.parent / "lib", # Relative to hooks/
|
|
Path.cwd() / "plugins" / "autonomous-dev" / "lib", # Project root
|
|
Path.home() / ".autonomous-dev" / "lib", # Global install
|
|
]
|
|
|
|
for candidate in candidates:
|
|
if candidate.exists():
|
|
return candidate
|
|
|
|
return None
|
|
|
|
|
|
# Add lib to path
|
|
LIB_DIR = find_lib_dir()
|
|
if LIB_DIR:
|
|
sys.path.insert(0, str(LIB_DIR))
|
|
|
|
# Optional imports with graceful fallback
|
|
try:
|
|
from agent_tracker import AgentTracker
|
|
HAS_AGENT_TRACKER = True
|
|
except ImportError:
|
|
HAS_AGENT_TRACKER = False
|
|
|
|
try:
|
|
from project_md_updater import ProjectMdUpdater
|
|
HAS_PROJECT_UPDATER = True
|
|
except ImportError:
|
|
HAS_PROJECT_UPDATER = False
|
|
|
|
|
|
# ============================================================================
|
|
# Configuration
|
|
# ============================================================================
|
|
|
|
# Check configuration from environment
|
|
TRACK_SESSIONS = os.environ.get("TRACK_SESSIONS", "true").lower() == "true"
|
|
TRACK_PIPELINE = os.environ.get("TRACK_PIPELINE", "true").lower() == "true"
|
|
AUTO_UPDATE_PROGRESS = os.environ.get("AUTO_UPDATE_PROGRESS", "false").lower() == "true"
|
|
|
|
|
|
# ============================================================================
|
|
# Session Logging (Basic)
|
|
# ============================================================================
|
|
|
|
class SessionTracker:
|
|
"""Basic session logging to docs/sessions/."""
|
|
|
|
def __init__(self):
|
|
"""Initialize session tracker."""
|
|
self.session_dir = Path("docs/sessions")
|
|
self.session_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Find or create session file for today
|
|
today = datetime.now().strftime("%Y%m%d")
|
|
session_files = list(self.session_dir.glob(f"{today}-*.md"))
|
|
|
|
if session_files:
|
|
# Use most recent session file from today
|
|
self.session_file = sorted(session_files)[-1]
|
|
else:
|
|
# Create new session file
|
|
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
self.session_file = self.session_dir / f"{timestamp}-session.md"
|
|
|
|
# Initialize with header
|
|
self.session_file.write_text(
|
|
f"# Session {timestamp}\n\n"
|
|
f"**Started**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
|
f"---\n\n"
|
|
)
|
|
|
|
def log(self, agent_name: str, message: str) -> None:
|
|
"""
|
|
Log agent action to session file.
|
|
|
|
Args:
|
|
agent_name: Name of agent
|
|
message: Message to log
|
|
"""
|
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
entry = f"**{timestamp} - {agent_name}**: {message}\n\n"
|
|
|
|
# Append to session file
|
|
with open(self.session_file, "a") as f:
|
|
f.write(entry)
|
|
|
|
|
|
def track_basic_session(agent_name: str, message: str) -> bool:
|
|
"""
|
|
Track agent completion in basic session log.
|
|
|
|
Args:
|
|
agent_name: Name of agent
|
|
message: Completion message
|
|
|
|
Returns:
|
|
True if logged successfully, False otherwise
|
|
"""
|
|
if not TRACK_SESSIONS:
|
|
return False
|
|
|
|
try:
|
|
tracker = SessionTracker()
|
|
tracker.log(agent_name, message)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
# ============================================================================
|
|
# Pipeline Tracking (Structured)
|
|
# ============================================================================
|
|
|
|
def extract_tools_from_output(output: str) -> Optional[List[str]]:
|
|
"""
|
|
Best-effort extraction of tools used from agent output.
|
|
|
|
Args:
|
|
output: Agent output text
|
|
|
|
Returns:
|
|
List of tool names or None if no tools detected
|
|
"""
|
|
tools = []
|
|
|
|
# Common tool mentions in output
|
|
if "Read tool" in output or "reading file" in output.lower():
|
|
tools.append("Read")
|
|
if "Write tool" in output or "writing file" in output.lower():
|
|
tools.append("Write")
|
|
if "Edit tool" in output or "editing file" in output.lower():
|
|
tools.append("Edit")
|
|
if "Bash tool" in output or "running command" in output.lower():
|
|
tools.append("Bash")
|
|
if "Grep tool" in output or "searching" in output.lower():
|
|
tools.append("Grep")
|
|
if "WebSearch" in output or "web search" in output.lower():
|
|
tools.append("WebSearch")
|
|
if "WebFetch" in output or "fetching URL" in output.lower():
|
|
tools.append("WebFetch")
|
|
if "Task tool" in output or "invoking agent" in output.lower():
|
|
tools.append("Task")
|
|
|
|
return tools if tools else None
|
|
|
|
|
|
def track_pipeline_completion(agent_name: str, agent_output: str, agent_status: str) -> bool:
|
|
"""
|
|
Track agent completion in structured pipeline.
|
|
|
|
Args:
|
|
agent_name: Name of agent
|
|
agent_output: Agent output text
|
|
agent_status: "success" or "error"
|
|
|
|
Returns:
|
|
True if tracked successfully, False otherwise
|
|
"""
|
|
if not TRACK_PIPELINE or not HAS_AGENT_TRACKER:
|
|
return False
|
|
|
|
try:
|
|
tracker = AgentTracker()
|
|
|
|
if agent_status == "success":
|
|
# Extract tools used
|
|
tools = extract_tools_from_output(agent_output)
|
|
|
|
# Create summary (first 100 chars)
|
|
summary = agent_output[:100].replace("\n", " ") if agent_output else "Completed"
|
|
|
|
# Auto-track agent first (idempotent)
|
|
tracker.auto_track_from_environment(message=summary)
|
|
|
|
# Complete the agent
|
|
tracker.complete_agent(agent_name, summary, tools)
|
|
else:
|
|
# Extract error message
|
|
error_msg = agent_output[:100].replace("\n", " ") if agent_output else "Failed"
|
|
|
|
# Auto-track even for failures
|
|
tracker.auto_track_from_environment(message=error_msg)
|
|
|
|
# Fail the agent
|
|
tracker.fail_agent(agent_name, error_msg)
|
|
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
# ============================================================================
|
|
# PROJECT.md Progress Updates
|
|
# ============================================================================
|
|
|
|
def should_trigger_progress_update(agent_name: str) -> bool:
|
|
"""
|
|
Check if PROJECT.md progress update should trigger.
|
|
|
|
Only triggers for doc-master (last agent in pipeline).
|
|
|
|
Args:
|
|
agent_name: Name of agent that completed
|
|
|
|
Returns:
|
|
True if should trigger, False otherwise
|
|
"""
|
|
return agent_name == "doc-master"
|
|
|
|
|
|
def check_pipeline_complete() -> bool:
|
|
"""
|
|
Check if all 7 agents in pipeline completed.
|
|
|
|
Returns:
|
|
True if pipeline complete, False otherwise
|
|
"""
|
|
if not HAS_AGENT_TRACKER:
|
|
return False
|
|
|
|
try:
|
|
# Check latest session file
|
|
session_dir = Path("docs/sessions")
|
|
session_files = list(session_dir.glob("*-pipeline.json"))
|
|
|
|
if not session_files:
|
|
return False
|
|
|
|
# Read latest session
|
|
latest_session = sorted(session_files)[-1]
|
|
session_data = json.loads(latest_session.read_text())
|
|
|
|
# Check if all expected agents completed
|
|
# Issue #147: Consolidated to only active agents in /auto-implement pipeline
|
|
expected_agents = [
|
|
"researcher-local",
|
|
"planner",
|
|
"test-master",
|
|
"implementer",
|
|
"reviewer",
|
|
"security-auditor",
|
|
"doc-master"
|
|
]
|
|
|
|
completed_agents = {
|
|
entry["agent"] for entry in session_data.get("agents", [])
|
|
if entry.get("status") == "completed"
|
|
}
|
|
|
|
return set(expected_agents).issubset(completed_agents)
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def update_project_progress() -> bool:
|
|
"""
|
|
Update PROJECT.md with goal progress.
|
|
|
|
Returns:
|
|
True if updated successfully, False otherwise
|
|
"""
|
|
if not AUTO_UPDATE_PROGRESS or not HAS_PROJECT_UPDATER:
|
|
return False
|
|
|
|
try:
|
|
# Note: Progress tracking feature deprioritized (Issue #147: Agent consolidation)
|
|
# Would update PROJECT.md via ProjectMdUpdater if implemented.
|
|
return False
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
# ============================================================================
|
|
# Main Hook Entry Point
|
|
# ============================================================================
|
|
|
|
def main() -> int:
|
|
"""
|
|
Main hook entry point.
|
|
|
|
Reads agent info from environment, dispatches tracking.
|
|
|
|
Returns:
|
|
Always 0 (non-blocking hook)
|
|
"""
|
|
# Get agent info from environment (provided by Claude Code)
|
|
agent_name = os.environ.get("CLAUDE_AGENT_NAME", "unknown")
|
|
agent_output = os.environ.get("CLAUDE_AGENT_OUTPUT", "")
|
|
agent_status = os.environ.get("CLAUDE_AGENT_STATUS", "success")
|
|
|
|
# Create summary message
|
|
summary = agent_output[:100].replace("\n", " ") if agent_output else "Completed"
|
|
|
|
# Dispatch tracking (all are non-blocking)
|
|
try:
|
|
# Basic session logging
|
|
track_basic_session(agent_name, summary)
|
|
|
|
# Structured pipeline tracking
|
|
track_pipeline_completion(agent_name, agent_output, agent_status)
|
|
|
|
# PROJECT.md progress updates (only for doc-master)
|
|
if should_trigger_progress_update(agent_name) and check_pipeline_complete():
|
|
update_project_progress()
|
|
except Exception:
|
|
# Graceful degradation - never block workflow
|
|
pass
|
|
|
|
# Always succeed (non-blocking hook)
|
|
output = {
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "SubagentStop"
|
|
}
|
|
}
|
|
print(json.dumps(output))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|