CascadingDev/assets/hooks/pre-commit

193 lines
7.7 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# CascadingDev Pre-commit Hook
# =============================
# This hook orchestrates AI-powered automation during git commits.
#
# What it does:
# 1. Creates companion summary files (.sum.md) for discussion files
# 2. Validates that discussion files are append-only (no deletions)
# 3. Runs automation/runner.py to generate AI-powered outputs
# 4. Runs automation/workflow.py to update vote summaries
#
# Safety: Exits on errors to prevent broken commits
set -euo pipefail
# Navigate to git repository root so all file paths work correctly
# regardless of where the user ran the commit command
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
cd "$ROOT"
# ============================================================================
# FUNCTION: check_append_only_discussion
# ============================================================================
# Validates that changes to discussion files only add content (no deletions).
#
# Discussion files are append-only to maintain conversation history.
# YAML header fields (status, timestamps, etc.) are allowed to change.
#
# Args:
# $1 - Path to discussion file (e.g., "Docs/features/FR_*/discussions/*.discussion.md")
#
# Returns:
# 0 if valid (append-only or header changes only)
# 1 if invalid (deletions detected in body content)
#
# Note: This is a basic implementation using regex. A more robust version
# would parse YAML front matter and validate append-only at the line level.
# ============================================================================
check_append_only_discussion() {
local disc_file="$1"
local diff_output
# Get the staged diff for this file
diff_output=$(git diff --cached "$disc_file")
# Look for deletion lines (lines starting with "-" but not "---" file markers)
# If we find any, the user may have deleted existing content
if echo "$diff_output" | grep -E "^-[^-]" | grep -Ev "^--- a/" | grep -Ev "^\+\+\+ b/"; then
echo >&2 "[pre-commit] Error: Deletions or modifications detected in existing lines of $disc_file."
echo >&2 " Discussion files must be append-only, except for allowed header fields."
return 1
fi
# Check for modifications to YAML header fields
# These are allowed (e.g., updating status: OPEN → READY_FOR_IMPLEMENTATION)
# A more robust check would parse YAML front matter instead of using regex
local header_modified=0
if echo "$diff_output" | grep -E "^[-+]" | grep -E "^(status|created|updated|feature_id|stage_id):" > /dev/null; then
header_modified=1
fi
# If there are additions and no body deletions, we're good
# This assumes additions are at the end (append-only)
if echo "$diff_output" | grep -E "^\+[^+]" | grep -Ev "^\+\+\+ b/" > /dev/null && [ "$header_modified" -eq 0 ]; then
: # Valid: additions only (append-only pattern)
fi
return 0
}
# ============================================================================
# FUNCTION: ensure_summary
# ============================================================================
# Creates a companion summary file (.sum.md) for a discussion file if missing.
#
# Summary files provide structured views of discussions:
# - Vote counts and latest votes per participant
# - Action items, questions, decisions
# - Timeline of major updates
#
# The Python automation (workflow.py) updates these files, but it expects
# them to already exist. This function ensures the initial template is created.
#
# Args:
# $1 - Path to discussion file (e.g., "feature.discussion.md")
#
# Creates:
# - {discussion}.sum.md (e.g., "feature.discussion.sum.md")
# - Auto-stages the new file for inclusion in the commit
#
# Template location: assets/templates/feature.discussion.sum.md
# ============================================================================
ensure_summary() {
local disc="$1"
local dir; dir="$(dirname "$disc")"
local sum="$dir/$(basename "$disc" .md).sum.md"
local template_path="assets/templates/feature.discussion.sum.md"
# Only create if it doesn't exist
if [ ! -f "$sum" ]; then
# Copy template content to the summary file
cat "$template_path" > "$sum"
# Auto-stage so it's included in this commit
git add "$sum"
echo >&2 "[pre-commit] Created summary file: $sum"
fi
}
# ============================================================================
# STEP 1: Collect Staged Files
# ============================================================================
# Get all files staged for commit (Added or Modified only)
# Exit early if there's nothing to process
mapfile -t STAGED < <(git diff --cached --name-only --diff-filter=AM || true)
[ "${#STAGED[@]}" -eq 0 ] && exit 0
# ============================================================================
# STEP 2: Process Discussion Files
# ============================================================================
# For each staged discussion file:
# 1. Ensure companion .sum.md file exists (create from template if needed)
# 2. Validate append-only constraint (no deletions to conversation history)
#
# Discussion file pattern: Docs/features/FR_*/discussions/*.discussion.md
# ============================================================================
for f in "${STAGED[@]}"; do
case "$f" in
Docs/features/*/discussions/*.discussion.md)
# Create .sum.md companion file if it doesn't exist
ensure_summary "$f"
# Validate append-only constraint
if ! check_append_only_discussion "$f"; then
exit 1 # Abort commit if deletions detected
fi
;;
esac
done
# ============================================================================
# STEP 3: Run AI Automation (runner.py)
# ============================================================================
# The runner processes staged files according to .ai-rules.yml configuration:
# - Reads staged file diffs
# - Generates AI prompts with context
# - Calls Claude API to generate patches
# - Applies patches to output files
# - Auto-stages generated files
#
# Examples:
# - request.md → generates feature.discussion.md (initial AI comment + vote)
# - feature.discussion.md → may update implementation.discussion.md
#
# Exit codes:
# - 0: Success (or no rules matched, or graceful error handling)
# - 1: Fatal error (malformed YAML, missing dependencies, etc.)
#
# Debug artifacts: .git/ai-rules-debug/*.{raw.out,clean.diff,sanitized.diff,final.diff}
# ============================================================================
if [ -f "automation/runner.py" ]; then
if ! python3 -m automation.runner; then
echo "[pre-commit] automation.runner failed" >&2
exit 1
fi
fi
# ============================================================================
# STEP 4: Run Workflow Automation (workflow.py)
# ============================================================================
# The workflow parses discussion files and updates summary files:
# - Extracts VOTE: lines (READY, CHANGES, REJECT)
# - Counts votes per participant (latest vote wins)
# - Extracts questions, action items, decisions (Q:, TODO:, DECISION:)
# - Updates .sum.md files with structured data
# - Auto-stages updated summary files
#
# This runs AFTER runner.py so it can process any discussion files that
# were just generated by the AI automation.
#
# Exit behavior: Always returns 0 (|| true) so vote tracking never blocks commits.
# This is intentional - vote summaries are informational only.
# ============================================================================
if [ -x "automation/workflow.py" ]; then
python3 automation/workflow.py --status || true
fi
# ============================================================================
# Success - Allow Commit to Proceed
# ============================================================================
exit 0