192 lines
6.3 KiB
Python
Executable File
192 lines
6.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
Pipeline Verification Hook - Verify Agent Execution
|
||
|
||
This PreCommit hook verifies that the expected agents ran for feature implementations.
|
||
|
||
Hook Type: PreCommit (OPTIONAL - can be enabled for stricter enforcement)
|
||
Trigger: Before git commit completes
|
||
|
||
Purpose:
|
||
- Detect if full SDLC pipeline was skipped
|
||
- Warn about missing agents (researcher, test-master, etc.)
|
||
- Ensure autonomous workflow was used for feature work
|
||
|
||
Behavior:
|
||
- If today's pipeline file doesn't exist → PASS (not a feature commit)
|
||
- If file exists but agents missing → WARN (shows what's missing)
|
||
- If all expected agents ran → PASS (full pipeline executed)
|
||
|
||
Expected agents for feature implementations:
|
||
- researcher (always)
|
||
- planner (architecture/medium+ features)
|
||
- test-master (TDD required)
|
||
- implementer (always)
|
||
- reviewer (quality gate)
|
||
- security-auditor (security-sensitive features)
|
||
- doc-master (always)
|
||
|
||
Configuration (add to .claude/settings.local.json):
|
||
{
|
||
"hooks": {
|
||
"PreCommit": [{
|
||
"hooks": [{
|
||
"type": "command",
|
||
"command": "python .claude/hooks/verify_agent_pipeline.py || exit 1"
|
||
}]
|
||
}]
|
||
}
|
||
}
|
||
|
||
Exit codes:
|
||
0 - All checks passed
|
||
1 - Missing agents detected (if strict mode enabled)
|
||
|
||
Note: By default, this hook WARNS but doesn't block. Set STRICT_PIPELINE=1
|
||
environment variable to block commits when agents are missing.
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
import sys
|
||
from datetime import date
|
||
from pathlib import Path
|
||
|
||
|
||
def get_today_pipeline():
|
||
"""Get today's pipeline JSON file if it exists"""
|
||
session_dir = Path("docs/sessions")
|
||
if not session_dir.exists():
|
||
return None
|
||
|
||
today = date.today().strftime("%Y%m%d")
|
||
pipeline_files = list(session_dir.glob(f"{today}-*-pipeline.json"))
|
||
|
||
if not pipeline_files:
|
||
return None
|
||
|
||
# Return most recent file
|
||
latest = sorted(pipeline_files)[-1]
|
||
return json.loads(latest.read_text())
|
||
|
||
|
||
def has_feature_commits():
|
||
"""
|
||
Check if current commit includes feature work.
|
||
|
||
Heuristic: If code files (src/, *.py, *.js, *.ts, etc.) changed,
|
||
likely a feature commit.
|
||
"""
|
||
try:
|
||
import subprocess
|
||
result = subprocess.run(
|
||
["git", "diff", "--cached", "--name-only"],
|
||
capture_output=True,
|
||
text=True,
|
||
check=True
|
||
)
|
||
changed_files = result.stdout.strip().split("\n")
|
||
|
||
# Check for code files
|
||
code_extensions = {".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs", ".java", ".c", ".cpp"}
|
||
code_dirs = {"src/", "lib/", "app/"}
|
||
|
||
for file in changed_files:
|
||
# Check extension
|
||
if any(file.endswith(ext) for ext in code_extensions):
|
||
return True
|
||
# Check directory
|
||
if any(file.startswith(dir_path) for dir_path in code_dirs):
|
||
return True
|
||
|
||
return False
|
||
except Exception:
|
||
# If git command fails, assume feature commit (safe default)
|
||
return True
|
||
|
||
|
||
def verify_pipeline():
|
||
"""Verify that expected agents ran"""
|
||
# Get strict mode setting
|
||
strict_mode = os.environ.get("STRICT_PIPELINE", "0") == "1"
|
||
|
||
# Check if this is a feature commit
|
||
if not has_feature_commits():
|
||
print("ℹ️ No feature code changes detected, skipping pipeline verification")
|
||
return 0
|
||
|
||
# Get today's pipeline
|
||
pipeline = get_today_pipeline()
|
||
|
||
if not pipeline:
|
||
print("⚠️ Warning: No agent pipeline file found for today")
|
||
print(" Expected: docs/sessions/{date}-{time}-pipeline.json")
|
||
print(" This usually means agents weren't invoked (manual implementation?)")
|
||
print("\n Recommendation: Use /auto-implement for feature work to ensure")
|
||
print(" full SDLC pipeline (research → plan → test → implement → review → security → docs)")
|
||
|
||
if strict_mode:
|
||
print("\n❌ STRICT_PIPELINE=1: Blocking commit (no pipeline evidence)")
|
||
return 1
|
||
else:
|
||
print("\n✅ Allowing commit (strict mode not enabled)")
|
||
print(" Set STRICT_PIPELINE=1 to enforce pipeline verification")
|
||
return 0
|
||
|
||
# Check which agents ran
|
||
completed_agents = {
|
||
entry["agent"]
|
||
for entry in pipeline["agents"]
|
||
if entry["status"] == "completed"
|
||
}
|
||
|
||
# Expected agents (minimum for feature work)
|
||
expected_minimum = {"researcher", "implementer", "doc-master"}
|
||
expected_full = {"researcher", "planner", "test-master", "implementer",
|
||
"reviewer", "security-auditor", "doc-master"}
|
||
|
||
# Check minimum requirements
|
||
if expected_minimum.issubset(completed_agents):
|
||
print(f"✅ Agent pipeline verification passed")
|
||
print(f" Agents ran: {', '.join(sorted(completed_agents))}")
|
||
|
||
# Warn if full pipeline not run (but don't block)
|
||
missing_from_full = expected_full - completed_agents
|
||
if missing_from_full:
|
||
print(f"\n ℹ️ Note: Full pipeline not run (optional agents: {', '.join(missing_from_full)})")
|
||
print(f" This is OK for simple features, but consider using full pipeline for complex work")
|
||
|
||
return 0
|
||
|
||
# Missing minimum requirements
|
||
missing = expected_minimum - completed_agents
|
||
print(f"⚠️ Warning: Minimum agent pipeline not complete")
|
||
print(f" Missing: {', '.join(missing)}")
|
||
print(f" Ran: {', '.join(sorted(completed_agents)) if completed_agents else 'none'}")
|
||
print("\n Expected minimum agents:")
|
||
print(" - researcher (find patterns & best practices)")
|
||
print(" - implementer (write code)")
|
||
print(" - doc-master (update documentation)")
|
||
print("\n Recommendation: Use /auto-implement to ensure full SDLC pipeline")
|
||
|
||
if strict_mode:
|
||
print("\n❌ STRICT_PIPELINE=1: Blocking commit (agents missing)")
|
||
return 1
|
||
else:
|
||
print("\n✅ Allowing commit (strict mode not enabled)")
|
||
print(" Set STRICT_PIPELINE=1 to enforce pipeline verification")
|
||
return 0
|
||
|
||
|
||
def main():
|
||
try:
|
||
return verify_pipeline()
|
||
except Exception as e:
|
||
print(f"⚠️ Pipeline verification error: {e}", file=sys.stderr)
|
||
print(" Allowing commit to proceed (verification hook failed gracefully)")
|
||
return 0 # Don't block on errors
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|