Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:36:18 +08:00
commit 5c5bb04daa
16 changed files with 913 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
{
"name": "claude-bumper-lanes",
"description": "Enforces git diff thresholds to promote disciplined code review during AI agent sessions",
"version": "1.2.0",
"author": {
"name": "Kyle Snow Schwartz",
"email": "kyle.snowschwartz@gmail.com"
},
"commands": [
"./commands"
],
"hooks": [
"./hooks"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# claude-bumper-lanes
Enforces git diff thresholds to promote disciplined code review during AI agent sessions

9
commands/bumper-pause.md Normal file
View File

@@ -0,0 +1,9 @@
---
description: Temporarily suspend threshold enforcement while continuing to track changes
---
/claude-bumper-lanes:bumper-pause
Pausing enforcement... (This command is handled by the UserPromptSubmit hook)
Additional user arguments: $ARGUMENTS

9
commands/bumper-reset.md Normal file
View File

@@ -0,0 +1,9 @@
---
description: Reset the diff baseline and restore threshold budget
---
/claude-bumper-lanes:bumper-reset
Resetting baseline... (This command is handled by the UserPromptSubmit hook)
Additional user arguments: $ARGUMENTS

View File

@@ -0,0 +1,9 @@
---
description: Re-enable threshold enforcement after a pause
---
/claude-bumper-lanes:bumper-resume
Resuming enforcement... (This command is handled by the UserPromptSubmit hook)
Additional user arguments: $ARGUMENTS

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
# pause-baseline.sh - Pause threshold enforcement (NOT a hook)
# Purpose: Temporarily suspend Write/Edit blocking while continuing to track changes
# 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"
# Read command-line argument (sessionId passed from command)
session_id=${1:-}
if [[ -z "$session_id" ]]; then
echo "Warning: No session ID provided"
exit 1
fi
# Load session state
if ! session_state=$(read_session_state "$session_id" 2>/dev/null); then
echo "Warning: No active session found. Cannot pause."
exit 0
fi
# Check if already paused
paused=$(echo "$session_state" | jq -r '.paused // false')
if [[ "$paused" == "true" ]]; then
echo "Bumper lanes already paused. Use /bumper-resume to re-enable enforcement."
exit 0
fi
# Get current score for status message
accumulated_score=$(echo "$session_state" | jq -r '.accumulated_score // 0')
threshold_limit=$(echo "$session_state" | jq -r '.threshold_limit')
# Set paused flag
set_paused "$session_id" true
cat <<EOF
Bumper lanes: Enforcement paused ($accumulated_score/$threshold_limit pts)
Edit/Write operations will proceed without threshold checks.
Score tracking continues in the background.
Use /bumper-resume to re-enable enforcement.
EOF
exit 0

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
# post-tool-use.sh - PostToolUse hook for auto-reset after git commit
# Purpose: Reset bumper-lanes baseline after successful git commits
# Hook: PostToolUse with Bash matcher
# Trigger: After Bash tool completes successfully
# 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"
# Read hook input from stdin
input=$(cat)
session_id=$(echo "$input" | jq -r '.session_id')
tool_name=$(echo "$input" | jq -r '.tool_name')
hook_event_name=$(echo "$input" | jq -r '.hook_event_name')
command=$(echo "$input" | jq -r '.tool_input.command // empty')
# Validate hook event (defensive check)
if [[ "$hook_event_name" != "PostToolUse" ]]; then
exit 0
fi
# Only process Bash tool calls
if [[ "$tool_name" != "Bash" ]]; then
exit 0
fi
# Detect git commit commands (various formats)
# Pattern: git, optional flags (like -C /path), then commit subcommand
# Matches: git commit, git -C /path commit, git --git-dir=/x commit
# Rejects: prose like "use git to commit" (non-flag words between git and commit)
if ! echo "$command" | grep -qE 'git\s+(-{1,2}[A-Za-z-]+([ =]("[^"]*"|\S+))?\s+)*commit\b'; then
exit 0
fi
# Check if session state exists (session-only enforcement)
if ! session_state=$(read_session_state "$session_id" 2>/dev/null); then
# No session state - not enforcing, no reset needed (fail-open)
exit 0
fi
# Capture tree SHA from the commit that just happened
# Use HEAD^{tree} to get the tree from the commit, not current index state
# This ensures we capture exactly what was committed, not what's staged
if ! current_tree=$(git rev-parse HEAD^{tree} 2>/dev/null); then
# Failed to get commit tree - fail open (don't break git workflow)
exit 0
fi
# Reset baseline to current tree (committed state)
if ! reset_baseline_after_commit "$session_id" "$current_tree" 2>/dev/null; then
# Reset failed - fail open
exit 0
fi
# Output structured feedback for Claude Code
# PostToolUse hooks return JSON with systemMessage to inform the agent
jq -n '{
systemMessage: "✓ Bumper lanes: Auto-reset after commit. Fresh budget: 400 pts."
}'
exit 0

168
hooks/entrypoints/pre-tool-use.sh Executable file
View File

@@ -0,0 +1,168 @@
#!/usr/bin/env bash
set -euo pipefail
# pre-tool-use.sh - PreToolUse hook for threshold enforcement
# Purpose: Block file modification tools when diff threshold is 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')
tool_name=$(echo "$input" | jq -r '.tool_name')
hook_event_name=$(echo "$input" | jq -r '.hook_event_name')
# Validate hook event (defensive check)
if [[ "$hook_event_name" != "PreToolUse" ]]; then
exit 0
fi
# Check if tool is a modification tool (early return optimization)
case "$tool_name" in
Write | Edit)
# This is a file modification tool - proceed with threshold check
;;
*)
# Not a file modification tool - allow it immediately
exit 0
;;
esac
# Load session state
if ! session_state=$(read_session_state "$session_id" 2>/dev/null); then
# No session state - fail open (allow tool)
exit 0
fi
# Extract state
baseline_tree=$(echo "$session_state" | jq -r '.baseline_tree')
threshold_limit=$(echo "$session_state" | jq -r '.threshold_limit')
stop_triggered=$(echo "$session_state" | jq -r '.stop_triggered // false')
previous_tree=$(echo "$session_state" | jq -r '.previous_tree // .baseline_tree')
accumulated_score=$(echo "$session_state" | jq -r '.accumulated_score // 0')
paused=$(echo "$session_state" | jq -r '.paused // false')
# Capture current working tree (need this for both paths)
if ! current_tree=$(capture_tree 2>/dev/null); then
# Failed to capture tree - fail open
exit 0
fi
# Check for branch switch BEFORE threshold calculation
# This catches branch changes that Stop hook missed (e.g., switch and immediately edit)
baseline_branch=$(echo "$session_state" | jq -r '.baseline_branch // ""')
current_branch=$(get_current_branch)
if [[ -n "$baseline_branch" ]] && [[ -n "$current_branch" ]] && [[ "$baseline_branch" != "$current_branch" ]]; then
# Branch switched - reset baseline and allow this tool (fresh start)
reset_baseline_stale "$session_id" "$current_tree" "$current_branch"
# Output notification and allow tool
jq -n \
--arg baseline_branch "$baseline_branch" \
--arg current_branch "$current_branch" \
'{
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" },
systemMessage: "↪ Bumper lanes: Branch changed (\($baseline_branch) → \($current_branch)) — baseline auto-reset."
}'
exit 0
fi
# Helper: Output fuel gauge JSON if approaching threshold
output_fuel_gauge() {
local score="$1"
local limit="$2"
local message
message=$(get_fuel_gauge_message "$score" "$limit")
if [[ -n "$message" ]]; then
jq -n --arg msg "$message" '{
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" },
systemMessage: $msg
}'
fi
}
# Check if enforcement is paused
# When paused, we still track changes but don't enforce thresholds
if [[ "$paused" == "true" ]]; then
threshold_data=$(calculate_incremental_threshold "$previous_tree" "$current_tree" "$accumulated_score")
new_accumulated_score=$(echo "$threshold_data" | jq -r '.accumulated_score')
update_incremental_state "$session_id" "$current_tree" "$new_accumulated_score"
jq -n \
--argjson score "$new_accumulated_score" \
--argjson limit "$threshold_limit" \
'{
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" },
systemMessage: "Bumper lanes paused (\($score)/\($limit) pts)"
}'
exit 0
fi
# Check if Stop hook has been triggered
# PreToolUse only blocks AFTER Stop hook has fired once
if [[ "$stop_triggered" != "true" ]]; then
# Stop hasn't triggered yet - allow tool to proceed
# BUT update incremental state for next check
threshold_data=$(calculate_incremental_threshold "$previous_tree" "$current_tree" "$accumulated_score")
new_accumulated_score=$(echo "$threshold_data" | jq -r '.accumulated_score')
update_incremental_state "$session_id" "$current_tree" "$new_accumulated_score"
# Output fuel gauge if approaching threshold
output_fuel_gauge "$new_accumulated_score" "$threshold_limit"
exit 0
fi
# Stop has triggered - now enforce blocking on Write/Edit tools
# Use incremental calculation
threshold_data=$(calculate_incremental_threshold "$previous_tree" "$current_tree" "$accumulated_score")
weighted_score=$(echo "$threshold_data" | jq -r '.accumulated_score')
# Check threshold
if [[ $weighted_score -le $threshold_limit ]]; then
# Under threshold - allow tool and update state
update_incremental_state "$session_id" "$current_tree" "$weighted_score"
# Output fuel gauge if approaching threshold
output_fuel_gauge "$weighted_score" "$threshold_limit"
exit 0
fi
# Over threshold - deny tool call
# 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 denial reason
reason="
🚫 Bumper lanes: Diff threshold exceeded
$breakdown
Cannot modify files while over threshold.
Review your changes and run /bumper-reset to continue.
"
# Output denial decision using modern JSON format
jq -n \
--arg reason "$reason" \
'{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: $reason
}
}'
exit 0

View File

@@ -0,0 +1,86 @@
#!/usr/bin/env bash
set -euo pipefail
# reset-baseline.sh - Reset baseline script (NOT a hook)
# Purpose: Reset baseline tree to current working tree state, update session state
# 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"
# Read command-line argument (sessionId passed from command)
session_id=${1:-}
if [[ -z "$session_id" ]]; then
echo "⚠ Bumper Lanes: Error - No session ID provided"
exit 1
fi
# Load session state
if ! session_state=$(read_session_state "$session_id" 2>/dev/null); then
# No active session - print error message
echo "⚠ Bumper Lanes: No active session found. Baseline reset skipped."
exit 0
fi
old_baseline=$(echo "$session_state" | jq -r '.baseline_tree')
threshold_limit=$(echo "$session_state" | jq -r '.threshold_limit')
created_at=$(echo "$session_state" | jq -r '.created_at')
# Compute final diff stats (for reporting accepted changes)
current_tree=$(capture_tree)
if [[ -z "$current_tree" ]]; then
echo "⚠ Bumper Lanes: Failed to reset baseline. Please try again."
exit 1
fi
diff_output=$(compute_diff "$old_baseline" "$current_tree")
# Parse diff stats inline (git diff-tree --shortstat format)
# Format: "N files changed, X insertions(+), Y deletions(-)"
files_changed=0
lines_added=0
lines_deleted=0
if [[ "$diff_output" =~ ([0-9]+)\ file ]]; then
files_changed=${BASH_REMATCH[1]}
fi
if [[ "$diff_output" =~ ([0-9]+)\ insertion ]]; then
lines_added=${BASH_REMATCH[1]}
fi
if [[ "$diff_output" =~ ([0-9]+)\ deletion ]]; then
lines_deleted=${BASH_REMATCH[1]}
fi
total_lines=$((lines_added + lines_deleted))
# Update session state with new baseline and clear incremental tracking
new_baseline="$current_tree"
write_session_state "$session_id" "$new_baseline"
set_stop_triggered "$session_id" false
# Reset incremental tracking: previous_tree = baseline, accumulated_score = 0
update_incremental_state "$session_id" "$new_baseline" 0
# Build confirmation message
# Format timestamps for display
old_timestamp=$(date -r "$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$created_at" +%s)" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || echo "$created_at")
new_timestamp=$(date "+%Y-%m-%d %H:%M:%S")
# Truncate SHAs for display
old_baseline_short="${old_baseline:0:7}"
new_baseline_short="${new_baseline:0:7}"
# Build multi-line confirmation message
cat <<EOF
✓ Baseline reset complete.
Previous baseline: $old_baseline_short (captured $old_timestamp)
New baseline: $new_baseline_short (captured $new_timestamp)
Changes accepted: $files_changed files, $lines_added insertions(+), $lines_deleted deletions(-) [$total_lines lines total]
You now have a fresh diff budget of $threshold_limit points. Pick up where we left off?
EOF
exit 0

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
# resume-baseline.sh - Resume threshold enforcement (NOT a hook)
# Purpose: Re-enable Write/Edit blocking after a pause
# 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"
# Read command-line argument (sessionId passed from command)
session_id=${1:-}
if [[ -z "$session_id" ]]; then
echo "Warning: No session ID provided"
exit 1
fi
# Load session state
if ! session_state=$(read_session_state "$session_id" 2>/dev/null); then
echo "Warning: No active session found. Cannot resume."
exit 0
fi
# Check if already unpaused
paused=$(echo "$session_state" | jq -r '.paused // false')
if [[ "$paused" != "true" ]]; then
echo "Bumper lanes not paused. Enforcement is already active."
exit 0
fi
# Get current score for status message
accumulated_score=$(echo "$session_state" | jq -r '.accumulated_score // 0')
threshold_limit=$(echo "$session_state" | jq -r '.threshold_limit')
stop_triggered=$(echo "$session_state" | jq -r '.stop_triggered // false')
# Clear paused flag
set_paused "$session_id" false
# Calculate percentage for status
pct=$((accumulated_score * 100 / threshold_limit))
# Check if over threshold and warn
if [[ $accumulated_score -gt $threshold_limit ]]; then
cat <<EOF
Warning: Bumper lanes resumed — OVER THRESHOLD ($accumulated_score/$threshold_limit pts, ${pct}%)
Write/Edit operations will be blocked until /bumper-reset.
Review your changes before resetting.
EOF
elif [[ $pct -ge 75 ]]; then
cat <<EOF
Bumper lanes: Enforcement resumed ($accumulated_score/$threshold_limit pts, ${pct}%)
Approaching threshold. Consider committing working state soon.
EOF
else
cat <<EOF
Bumper lanes: Enforcement resumed ($accumulated_score/$threshold_limit pts)
Threshold checks active.
EOF
fi
exit 0

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
# session-end.sh - SessionEnd hook for checkpoint cleanup
# Purpose: Remove session-specific checkpoint file when Claude session terminates
# Read hook input from stdin
input=$(cat)
session_id=$(echo "$input" | jq -r '.session_id // ""')
# Validate session_id
if [[ -z "$session_id" ]]; then
echo "ERROR: No session_id provided to SessionEnd hook" >&2
exit 0 # Fail open - SessionEnd can't block anyway
fi
# Check if git repo (checkpoint dir won't exist otherwise)
if ! git rev-parse --git-dir &>/dev/null; then
exit 0
fi
checkpoint_dir=".git/bumper-checkpoints"
state_file="$checkpoint_dir/session-$session_id"
# Remove this session's checkpoint file
if [[ -f "$state_file" ]]; then
rm -f "$state_file" 2>/dev/null || true
fi
# Optional: Clean stale checkpoints (files older than 30 days)
# Uncomment the line below to enable automatic stale file cleanup:
# find "$checkpoint_dir" -type f -name "session-*" -mtime +30 -delete 2>/dev/null || true
exit 0

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
# session-start.sh - SessionStart hook for baseline capture
# Purpose: Capture working tree state as baseline when Claude session starts
# 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"
# Read hook input from stdin
input=$(cat)
session_id=$(echo "$input" | jq -r '.session_id')
# Hook is already executed in project directory (cwd field in JSON)
# Check if this is a git repository
if ! git rev-parse --git-dir &>/dev/null; then
# Not a git repo - disable plugin gracefully
exit 0
fi
# Capture baseline tree
baseline_tree=$(capture_tree)
if [[ -z "$baseline_tree" ]]; then
echo "ERROR: Failed to capture baseline tree" >&2
exit 0 # Fail open
fi
# Capture current branch name for staleness detection
baseline_branch=$(get_current_branch)
# Write session state (with branch tracking)
write_session_state "$session_id" "$baseline_tree" "$baseline_branch"
# Allow session start
exit 0

141
hooks/entrypoints/stop.sh Executable file
View 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

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
# user-prompt-submit.sh - UserPromptSubmit hook for /bumper-reset command
# Purpose: Watch for /bumper-reset in user prompt and execute reset-baseline.sh
# Source library functions (for potential future use)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Read hook input from stdin
input=$(cat)
prompt=$(echo "$input" | jq -r '.prompt // ""')
session_id=$(echo "$input" | jq -r '.session_id')
# Helper function to output command result as JSON
output_command_result() {
local output="$1"
jq -n \
--arg output "$output" \
'{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: $output
}
}'
}
# Check if user typed /claude-bumper-lanes:bumper-reset
if [[ "$prompt" == *"/claude-bumper-lanes:bumper-reset"* ]]; then
reset_output=$("$SCRIPT_DIR/reset-baseline.sh" "$session_id" 2>&1)
output_command_result "$reset_output"
exit 0
fi
# Check if user typed /claude-bumper-lanes:bumper-pause
if [[ "$prompt" == *"/claude-bumper-lanes:bumper-pause"* ]]; then
pause_output=$("$SCRIPT_DIR/pause-baseline.sh" "$session_id" 2>&1)
output_command_result "$pause_output"
exit 0
fi
# Check if user typed /claude-bumper-lanes:bumper-resume
if [[ "$prompt" == *"/claude-bumper-lanes:bumper-resume"* ]]; then
resume_output=$("$SCRIPT_DIR/resume-baseline.sh" "$session_id" 2>&1)
output_command_result "$resume_output"
exit 0
fi
exit 0

67
hooks/hooks.json Normal file
View File

@@ -0,0 +1,67 @@
{
"description": "Bumper Lanes hook configuration for threshold enforcement",
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/entrypoints/session-start.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/entrypoints/stop.sh"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/entrypoints/user-prompt-submit.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/entrypoints/pre-tool-use.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/entrypoints/post-tool-use.sh"
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/entrypoints/session-end.sh"
}
]
}
]
}
}

105
plugin.lock.json Normal file
View File

@@ -0,0 +1,105 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:kylesnowschwartz/claude-bumper-lanes:bumper-lanes-plugin",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "c1966d425ec5aac617ac0e3e2f5c2ece72e19835",
"treeHash": "6463824b80fa53ae189e55e5a8ebfee42d4242b94a593cb543596f63f06ab1de",
"generatedAt": "2025-11-28T10:20:02.022187Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "claude-bumper-lanes",
"description": "Enforces git diff thresholds to promote disciplined code review during AI agent sessions",
"version": "1.2.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "8ecf5ff6d08f9a18306f97eb7fbab4824b548e6379fe6a6192b336f6cda32552"
},
{
"path": "hooks/hooks.json",
"sha256": "73687af1261fd167c5f694e2cb364e4d5e8747aa1902eb3111adcbb8474648f8"
},
{
"path": "hooks/lib/git-state.sh",
"sha256": "faedd43659d24a6cb3df1c1039bef011ee5b9272c8acd2a9d45c319aa9a41c14"
},
{
"path": "hooks/lib/state-manager.sh",
"sha256": "a8b286251b84080fced34b54c6febd846a652b4beae20eebe55e84217704f7d5"
},
{
"path": "hooks/lib/threshold-calculator.sh",
"sha256": "6d486b76a15e5c836bda9ecc730da7e209c059605d0c94f431dac5a5734e96e2"
},
{
"path": "hooks/entrypoints/user-prompt-submit.sh",
"sha256": "0b406abc15b70b9ff251c0cef998909b8361ffb18930019f18aad0ef7f460951"
},
{
"path": "hooks/entrypoints/pause-baseline.sh",
"sha256": "cd6a160ddf4fee8654be0d3cdf169551a4742a384b1411065b85a92cec1b36c4"
},
{
"path": "hooks/entrypoints/reset-baseline.sh",
"sha256": "3f43dfa791e54952d180b9d2a10a19b8dc43f409af978d695f198f2398952131"
},
{
"path": "hooks/entrypoints/pre-tool-use.sh",
"sha256": "58bca0058f4f1012fd22ddede7f7de45ce04431f296a9ab6c0b7b9d83912fd1c"
},
{
"path": "hooks/entrypoints/session-start.sh",
"sha256": "44622c20335effe57b8d79ffbd4a0c497a0151983992be428d2293411d91a2c3"
},
{
"path": "hooks/entrypoints/session-end.sh",
"sha256": "29d8ee18e4d45d66f9b97940dc9fc677bebd9ed47d0e61193953621aee016493"
},
{
"path": "hooks/entrypoints/post-tool-use.sh",
"sha256": "801d417179369d3e18741c5721f6fdfa28e52d9b3aad28fea491ced294772c76"
},
{
"path": "hooks/entrypoints/stop.sh",
"sha256": "8f875bfb76a88d6386a1860055598881858b4bce21a951adc10321718d234550"
},
{
"path": "hooks/entrypoints/resume-baseline.sh",
"sha256": "0536ec1561672b716f9bef0d1e68ff3eb067775b7af32f3b76d31e4cf862cbaa"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "9216794e8daea5816d25aab492a9c82c8b7318d840096d32a1b74587cf185724"
},
{
"path": "commands/bumper-reset.md",
"sha256": "cbaf768a331cb313ce600d77e166185b855b9d2070f447c16bbc0615250b37bc"
},
{
"path": "commands/bumper-resume.md",
"sha256": "e55729fa98e9f1342840ef500269163465fdecce95433102c9ee1fab3508a70d"
},
{
"path": "commands/bumper-pause.md",
"sha256": "a0c810c68d5ea2e743f333fcfd5de59b03125b8ecf8140e5d3f008a72f89ff32"
}
],
"dirSha256": "6463824b80fa53ae189e55e5a8ebfee42d4242b94a593cb543596f63f06ab1de"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}