Files
2025-11-30 08:36:18 +08:00

142 lines
5.1 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# stop.sh - Stop hook for threshold enforcement
# Purpose: Check diff threshold when agent stops, block if exceeded
# Source library functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../lib/git-state.sh"
source "$SCRIPT_DIR/../lib/state-manager.sh"
source "$SCRIPT_DIR/../lib/threshold-calculator.sh"
# Read hook input from stdin
input=$(cat)
session_id=$(echo "$input" | jq -r '.session_id')
stop_hook_active=$(echo "$input" | jq -r '.stop_hook_active // false')
# Hook is already executed in project directory (cwd field in JSON)
# If already blocked once, allow stop this time to prevent infinite loop
if [[ "$stop_hook_active" == "true" ]]; then
exit 0
fi
# Load session state
if ! session_state=$(read_session_state "$session_id" 2>/dev/null); then
# No baseline - allow stop (fail open)
exit 0
fi
# Exit early if bumper lanes already tripped - allow review collaboration
stop_triggered=$(echo "$session_state" | jq -r '.stop_triggered // false')
if [[ "$stop_triggered" == "true" ]]; then
# PreToolUse is actively blocking Write/Edit - no need to block stop
exit 0
fi
# Exit early if enforcement is paused - allow work to continue
paused=$(echo "$session_state" | jq -r '.paused // false')
if [[ "$paused" == "true" ]]; then
# Still need to track changes while paused
current_tree=$(capture_tree)
if [[ -n "$current_tree" ]]; then
previous_tree=$(echo "$session_state" | jq -r '.previous_tree // .baseline_tree')
accumulated_score=$(echo "$session_state" | jq -r '.accumulated_score // 0')
threshold_data=$(calculate_incremental_threshold "$previous_tree" "$current_tree" "$accumulated_score")
weighted_score=$(echo "$threshold_data" | jq -r '.accumulated_score')
update_incremental_state "$session_id" "$current_tree" "$weighted_score"
fi
exit 0
fi
baseline_tree=$(echo "$session_state" | jq -r '.baseline_tree')
baseline_branch=$(echo "$session_state" | jq -r '.baseline_branch // ""')
threshold_limit=$(echo "$session_state" | jq -r '.threshold_limit')
previous_tree=$(echo "$session_state" | jq -r '.previous_tree // .baseline_tree')
accumulated_score=$(echo "$session_state" | jq -r '.accumulated_score // 0')
# Capture current working tree
current_tree=$(capture_tree)
if [[ -z "$current_tree" ]]; then
echo "ERROR: Failed to capture current tree" >&2
exit 0 # Fail open
fi
# Detect branch switch - auto-reset baseline to keep diffs meaningful
current_branch=$(get_current_branch)
if [[ -n "$baseline_branch" ]] && [[ -n "$current_branch" ]] && [[ "$baseline_branch" != "$current_branch" ]]; then
# Branch switched - reset baseline (score to 0, stop_triggered to false, update branch)
reset_baseline_stale "$session_id" "$current_tree" "$current_branch"
# Allow stop via JSON API (continue: true)
jq -n \
--arg baseline_branch "$baseline_branch" \
--arg current_branch "$current_branch" \
'{
continue: true,
systemMessage: "↪ Bumper lanes: Branch changed (\($baseline_branch) → \($current_branch)) — baseline auto-reset.",
suppressOutput: false
}'
# Exit after JSON response (avoid threshold check)
exit 0
fi
# Compute incremental threshold (previous → current + accumulated)
threshold_data=$(calculate_incremental_threshold "$previous_tree" "$current_tree" "$accumulated_score")
weighted_score=$(echo "$threshold_data" | jq -r '.accumulated_score')
if [[ $weighted_score -le $threshold_limit ]]; then
# Under threshold - allow stop and update incremental state
update_incremental_state "$session_id" "$current_tree" "$weighted_score"
exit 0
fi
# Over threshold - set stop_triggered flag to activate PreToolUse blocking
set_stop_triggered "$session_id" true
# Also update incremental state
update_incremental_state "$session_id" "$current_tree" "$weighted_score"
# Format breakdown for user message
# Note: threshold_data contains both weighted_score (delta) and accumulated_score (total)
# For user display, we need to show the accumulated total, not just this turn's delta
threshold_data_for_display=$(echo "$threshold_data" | jq '.weighted_score = .accumulated_score')
breakdown=$(format_threshold_breakdown "$threshold_data_for_display" "$threshold_limit")
# Build reason message
reason="
⚠️ Bumper lanes: Diff threshold exceeded
$breakdown
Ask the User: Would you like to conduct a structured, manual review?
This workflow ensures incremental code review at predictable checkpoints.
"
threshold_pct=$(awk "BEGIN {printf \"%.0f\", ($weighted_score / $threshold_limit) * 100}")
# Output block decision to STDOUT (JSON API pattern with exit code 0)
jq -n \
--arg decision "block" \
--arg reason "$reason" \
--argjson continue true \
--arg systemMessage "/bumper-reset after code review." \
--argjson threshold_data "$threshold_data" \
--argjson threshold_limit "$threshold_limit" \
--argjson threshold_percentage "$threshold_pct" \
'{
continue: $continue,
systemMessage: $systemMessage,
suppressOutput: true,
decision: $decision,
reason: $reason,
threshold_data: ($threshold_data + {threshold_limit: $threshold_limit, threshold_percentage: $threshold_percentage})
}'
exit 0