commit 5c5bb04daa2c08b70429732a7e1b05c26b20c343 Author: Zhongwei Li Date: Sun Nov 30 08:36:18 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..400c345 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -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" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a165b3 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# claude-bumper-lanes + +Enforces git diff thresholds to promote disciplined code review during AI agent sessions diff --git a/commands/bumper-pause.md b/commands/bumper-pause.md new file mode 100644 index 0000000..a35a4fc --- /dev/null +++ b/commands/bumper-pause.md @@ -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 diff --git a/commands/bumper-reset.md b/commands/bumper-reset.md new file mode 100644 index 0000000..797779c --- /dev/null +++ b/commands/bumper-reset.md @@ -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 diff --git a/commands/bumper-resume.md b/commands/bumper-resume.md new file mode 100644 index 0000000..39e8fa3 --- /dev/null +++ b/commands/bumper-resume.md @@ -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 diff --git a/hooks/entrypoints/pause-baseline.sh b/hooks/entrypoints/pause-baseline.sh new file mode 100755 index 0000000..1d75caa --- /dev/null +++ b/hooks/entrypoints/pause-baseline.sh @@ -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 </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 diff --git a/hooks/entrypoints/pre-tool-use.sh b/hooks/entrypoints/pre-tool-use.sh new file mode 100755 index 0000000..7e4dc20 --- /dev/null +++ b/hooks/entrypoints/pre-tool-use.sh @@ -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 diff --git a/hooks/entrypoints/reset-baseline.sh b/hooks/entrypoints/reset-baseline.sh new file mode 100755 index 0000000..6066b26 --- /dev/null +++ b/hooks/entrypoints/reset-baseline.sh @@ -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 </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 <&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 diff --git a/hooks/entrypoints/session-start.sh b/hooks/entrypoints/session-start.sh new file mode 100755 index 0000000..7fe394d --- /dev/null +++ b/hooks/entrypoints/session-start.sh @@ -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 diff --git a/hooks/entrypoints/stop.sh b/hooks/entrypoints/stop.sh new file mode 100755 index 0000000..8c884d9 --- /dev/null +++ b/hooks/entrypoints/stop.sh @@ -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 diff --git a/hooks/entrypoints/user-prompt-submit.sh b/hooks/entrypoints/user-prompt-submit.sh new file mode 100755 index 0000000..85dfe0a --- /dev/null +++ b/hooks/entrypoints/user-prompt-submit.sh @@ -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 diff --git a/hooks/hooks.json b/hooks/hooks.json new file mode 100644 index 0000000..614e8f9 --- /dev/null +++ b/hooks/hooks.json @@ -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" + } + ] + } + ] + } +} diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..5ca9828 --- /dev/null +++ b/plugin.lock.json @@ -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": [] + } +} \ No newline at end of file