117 lines
3.5 KiB
Ruby
Executable File
117 lines
3.5 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
require 'claude_hooks'
|
|
require 'json'
|
|
require_relative 'reflexive_agreement_detector'
|
|
|
|
# Stop Hook for Real-Time Reflexive Agreement Correction
|
|
#
|
|
# PURPOSE: Detect reflexive agreement in Claude's just-completed response
|
|
# TRIGGERS: When Claude Code finishes generating a response
|
|
# ACTION: Force continuation with corrective instructions if reflexive agreement detected
|
|
#
|
|
# DETECTION STRATEGY:
|
|
# Reflexive agreement = agreement phrase in first sentence WITHOUT research/substantive work
|
|
# - Checks first sentence for agreement patterns
|
|
# - Verifies no tool use in response (no Read, Grep, WebFetch, etc.)
|
|
# - Checks for substantive pivots or follow-up analysis
|
|
|
|
class StopYouAreNotRight < ClaudeHooks::Stop
|
|
include ReflexiveAgreementDetector
|
|
|
|
def call
|
|
log 'Checking if Claude just used reflexive agreement in response'
|
|
|
|
if just_used_reflexive_agreement?
|
|
log 'Detected reflexive agreement - forcing corrective continuation', level: :warn
|
|
force_substantive_response!
|
|
else
|
|
log 'No reflexive agreement detected - allowing normal stop'
|
|
allow_normal_stop!
|
|
end
|
|
|
|
output
|
|
end
|
|
|
|
private
|
|
|
|
def just_used_reflexive_agreement?
|
|
return false unless transcript_path
|
|
|
|
# Get Claude's most recent message (the one that just completed)
|
|
last_message = get_last_assistant_message
|
|
return false unless last_message
|
|
|
|
text = extract_text(last_message)
|
|
return false unless text.is_a?(String)
|
|
|
|
log "Analyzing response: '#{text[0, 100]}...'"
|
|
|
|
# Use shared detection logic
|
|
is_reflexive = reflexive_agreement?(last_message)
|
|
|
|
if is_reflexive
|
|
first_sentence = extract_first_sentence(text)
|
|
log "Reflexive agreement detected: '#{first_sentence}'"
|
|
else
|
|
log "Not reflexive agreement"
|
|
end
|
|
|
|
is_reflexive
|
|
end
|
|
|
|
def get_last_assistant_message
|
|
return nil unless File.exist?(transcript_path)
|
|
|
|
# Read transcript in reverse to find the most recent assistant message
|
|
assistant_message = nil
|
|
|
|
File.readlines(transcript_path).reverse_each do |line|
|
|
next unless line.include?('"role":"assistant"')
|
|
|
|
begin
|
|
item = JSON.parse(line.strip)
|
|
next unless item['type'] == 'assistant'
|
|
|
|
# Get full message including all content blocks
|
|
assistant_message = item
|
|
break # Found the most recent one
|
|
rescue JSON::ParserError
|
|
next
|
|
end
|
|
end
|
|
|
|
assistant_message
|
|
end
|
|
|
|
def force_substantive_response!
|
|
correction_instructions = <<~INSTRUCTIONS.strip
|
|
I notice I just used a reflexive agreement phrase. Let me provide a more substantive response:
|
|
|
|
Instead of simply agreeing, let me analyze your point with specific technical reasoning, consider potential edge cases or alternative approaches, and offer constructive insights that build collaboratively on your observation.
|
|
INSTRUCTIONS
|
|
|
|
# Force Claude to continue with corrective instructions
|
|
continue_with_instructions!(correction_instructions)
|
|
|
|
log 'Forced continuation with corrective instructions'
|
|
end
|
|
|
|
def allow_normal_stop!
|
|
# Allow Claude to stop normally (default behavior)
|
|
ensure_stopping!
|
|
|
|
# Don't add any output to the transcript for normal stops
|
|
suppress_output!
|
|
end
|
|
end
|
|
|
|
# Testing support - run this file directly to test with sample data
|
|
if __FILE__ == $PROGRAM_NAME
|
|
ClaudeHooks::CLI.test_runner(StopYouAreNotRight) do |input_data|
|
|
input_data['session_id'] = 'stop-correction-test'
|
|
input_data['stop_hook_active'] = false
|
|
end
|
|
end
|