94 lines
3.4 KiB
Python
94 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Enforce Story Context Hook
|
|
Purpose: Block workflow commands that require a story if no story is active
|
|
Trigger: PreToolUse on Bash commands (skill invocations)
|
|
Part of: PRISM Core Development Lifecycle
|
|
"""
|
|
|
|
import sys
|
|
import io
|
|
import json
|
|
import os
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
# Fix Windows console encoding for emoji support
|
|
if sys.stdout.encoding != 'utf-8':
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
|
|
|
def main():
|
|
# Claude Code passes parameters via environment variables
|
|
# Not via stdin JSON
|
|
|
|
# Extract command from environment variables
|
|
command = os.environ.get('TOOL_PARAMS_command', '')
|
|
|
|
# Check if command is a PRISM skill command that requires a story context
|
|
requires_story = False
|
|
command_name = None
|
|
|
|
if '*develop-story' in command:
|
|
requires_story = True
|
|
command_name = 'develop-story'
|
|
elif '*review ' in command:
|
|
requires_story = True
|
|
command_name = 'review'
|
|
elif '*risk ' in command:
|
|
requires_story = True
|
|
command_name = 'risk-profile'
|
|
elif '*design ' in command:
|
|
requires_story = True
|
|
command_name = 'test-design'
|
|
elif '*validate-story-draft ' in command:
|
|
requires_story = True
|
|
command_name = 'validate-story-draft'
|
|
elif '*gate ' in command:
|
|
requires_story = True
|
|
command_name = 'gate'
|
|
elif '*review-qa' in command:
|
|
requires_story = True
|
|
command_name = 'review-qa'
|
|
|
|
if requires_story:
|
|
# Check if there's an active story
|
|
story_file_path = Path('.prism-current-story.txt')
|
|
|
|
if not story_file_path.exists():
|
|
print(f"❌ ERROR: Command '{command_name}' requires an active story", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
print(" No current story found in workflow context", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
print(" REQUIRED: Draft a story first using the core-development-cycle workflow:", file=sys.stderr)
|
|
print(" 1. Run: *planning-review (optional)", file=sys.stderr)
|
|
print(" 2. Run: *draft", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
print(" The draft command will create a story file and establish story context.", file=sys.stderr)
|
|
sys.exit(2) # Block the command
|
|
|
|
story_file = story_file_path.read_text().strip()
|
|
|
|
# Verify story file exists
|
|
if not Path(story_file).exists():
|
|
print(f"❌ ERROR: Current story file not found: {story_file}", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
print(" The story reference is stale or the file was deleted", file=sys.stderr)
|
|
print("", file=sys.stderr)
|
|
print(" REQUIRED: Create a new story:", file=sys.stderr)
|
|
print(" Run: *draft", file=sys.stderr)
|
|
sys.exit(2) # Block the command
|
|
|
|
# Log command with story context
|
|
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
with open('.prism-workflow.log', 'a') as log:
|
|
log.write(f"{timestamp} | COMMAND | {command_name} | {story_file}\n")
|
|
|
|
# Hooks should be silent on success
|
|
# Success is indicated by exit code 0
|
|
|
|
sys.exit(0)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|