Initial commit
This commit is contained in:
141
hooks/entrypoints/stop.sh
Executable file
141
hooks/entrypoints/stop.sh
Executable file
@@ -0,0 +1,141 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user