#!/usr/bin/env bash # Safety settings: exit on errors, treat unset variables as errors, and catch pipeline failures set -euo pipefail # Find and navigate to the git repo root (or current dir if not in a repo) so file paths work correctly regardless of where the commit command is run ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")" cd "$ROOT" resolve_template() { local tmpl="$1" rel_path="$2" local today dirpath basename name ext feature_id stage today="$(date +%F)" dirpath="$(dirname "$rel_path")" basename="$(basename "$rel_path")" name="${basename%.*}" ext="${basename##*.}" feature_id="" stage="" feature_id="$(echo "$rel_path" | sed -n 's|.*Docs/features/\(FR_[^/]*\).*|\1|p')" stage="$(echo "$basename" | sed -n 's/^\([A-Za-z0-9_-]\+\)\.discussion\.md$/\1|p')" echo "$tmpl" \ | sed -e "s_{date}_$today_g" \ -e "s_{rel}_$rel_path_g" \ -e "s_{dir}_$dirpath_g" \ -e "s_{basename}_$basename_g" \ -e "s_{name}_$name_g" \ -e "s_{ext}_$ext_g" \ -e "s_{feature_id}_$feature_id_g" \ -e "s_{stage}_$stage_g" } # Helper function to apply a patch with 3-way merge fallback apply_patch_with_3way() { local patch_file="$1" local target_file="$2" if [ ! -f "$patch_file" ]; then echo >&2 "[pre-commit] Error: Patch file not found: $patch_file" return 1 fi # Attempt 3-way apply if git apply --index --3way --recount --whitespace=nowarn "$patch_file"; then echo >&2 "[pre-commit] Applied patch to $target_file with 3-way merge." elif git apply --index "$patch_file"; then echo >&2 "[pre-commit] Applied patch to $target_file with strict apply (3-way failed)." else echo >&2 "[pre-commit] Error: Failed to apply patch to $target_file." echo >&2 " Manual intervention may be required." return 1 fi return 0 } # Helper function to check if changes to a discussion file are append-only check_append_only_discussion() { local disc_file="$1" local diff_output # Get the cached diff for the discussion file diff_output=$(git diff --cached "$disc_file") # Check if there are any deletions or modifications to existing lines # This is a simplified check; a more robust solution would parse hunks 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 header fields (status, timestamps, feature_id, stage_id) # This is a basic check and might need refinement based on actual header structure # For now, we'll allow changes to lines that look like header fields. # A more robust solution would parse YAML front matter. 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, ensure they are at the end of the file, or are allowed header modifications # This is a very basic check. A more advanced check would compare line numbers. # For now, if there are additions and no deletions/modifications to body, we assume append-only. if echo "$diff_output" | grep -E "^\+[^+]" | grep -Ev "^\+\+\+ b/" > /dev/null && [ "$header_modified" -eq 0 ]; then : # Placeholder for more robust append-only check fi return 0 } # -------- collect staged files ---------- # Get list of staged added/modified files into STAGED array, exit early if none found mapfile -t STAGED < <(git diff --cached --name-only --diff-filter=AM || true) [ "${#STAGED[@]}" -eq 0 ] && exit 0 # -------- ensure discussion summaries exist (companion files) ---------- # Create and auto-stage a summary template file for any discussion file that doesn't already have one 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" if [ ! -f "$sum" ]; then # Copy the template content directly cat "$template_path" > "$sum" git add "$sum" fi } # Process each staged discussion file and ensure it has a summary for f in "${STAGED[@]}"; do case "$f" in Docs/features/*/discussions/*.discussion.md) ensure_summary "$f" if ! check_append_only_discussion "$f"; then exit 1 # Exit with error if append-only check fails fi ;; esac done # -------- orchestration (non-blocking status) ---------- # -------- automation runner (AI outputs) ---------- if [ -f "automation/runner.py" ]; then if ! python3 -m automation.runner; then echo "[pre-commit] automation.runner failed" >&2 exit 1 fi fi # -------- orchestration (non-blocking status) ---------- # NOTE: automation/workflow.py provides non-blocking vote status reporting. # It parses VOTE: lines from staged discussion files and prints a summary. # Run workflow status check if available, but don't block commit if it fails. if [ -x "automation/workflow.py" ]; then python3 automation/workflow.py --status || true fi exit 0