#!/usr/bin/env ruby # frozen_string_literal: true # Shared detection logic for reflexive agreement patterns # Used by both the stop hook and test harness to ensure consistency module ReflexiveAgreementDetector # Agreement patterns - match at start of first sentence AGREEMENT_PATTERNS = [ /\A\s*[Yy]ou'?re?\s+(absolutely\s+)?(right|correct)/i, /\A\s*[Aa]bsolutely\.?\s*\Z/i, /\A\s*[Yy]es,?\s+you'?re?\s+(totally\s+)?correct/i, /\A\s*[Dd]efinitely\.?\s*\Z/i ].freeze # Pivot words that indicate substantive continuation PIVOT_INDICATORS = /\b(but|however|though|although|that said|except|yet)\b/i # Substantive content markers - require CONCRETE technical evidence only # Removed: "here's why", "the reason", "for example", "because" (setup words, not substance) # Removed: "found|discovered|detected" (can be vacuous: "I found the issue" without details) SUBSTANTIVE_MARKERS = [ /\d+\s*(slots|tokens|ms|bytes|chars|seconds|lines|files|functions|tests|errors?|warnings?)/i, # Specific measurements /```[\s\S]{20,}```/, # Code blocks (min 20 chars) /\b(benchmark|profiled|traced|debugged|stack trace|error message)\b/i # Technical investigation ].freeze # Main detection method # Returns true if message is reflexive agreement (should trigger) def reflexive_agreement?(message) text = extract_text(message) return false unless text.is_a?(String) # STEP 1: Check if first sentence contains agreement first_sentence = extract_first_sentence(text) return false unless first_sentence_has_agreement?(first_sentence) # STEP 2: Check if response contains tool use return false if message_contains_tool_use?(message) # STEP 3: Check for pivot and substantive content has_pivot = first_sentence_has_pivot?(first_sentence) has_substantive = followed_by_substantive_content?(text) # Pivot words only matter if backed by substantive follow-up return false if has_pivot && has_substantive # Substantive content without pivot = still not reflexive return false if has_substantive # At this point: agreement detected, no tool use, no substantive follow-up true end private def extract_text(message) message.dig('message', 'content', 0, 'text') end def extract_first_sentence(text) sentences = text.split(/[.!?]+/) first = sentences[0] || '' first.strip end def first_sentence_has_agreement?(sentence) AGREEMENT_PATTERNS.any? { |pattern| sentence.match?(pattern) } end def message_contains_tool_use?(message) content = message.dig('message', 'content') || [] content.any? { |block| block['type'] == 'tool_use' } end def first_sentence_has_pivot?(sentence) # Only count actual pivots and colons/dashes as setup for substantive content # Removed: "need to|will|should|must|going to|have to" (action promises, not pivots) return true if sentence.match?(PIVOT_INDICATORS) return true if sentence.match?(/[:\u2014\u2013]\s*$/) false end def followed_by_substantive_content?(text) sentences = text.split(/[.!?]+/).map(&:strip).reject(&:empty?) return false if sentences.length <= 1 rest_of_text = sentences[1..].join(' ') # Require SIGNIFICANT follow-up with concrete technical content # Increased from 50 → 100 chars minimum (rules out "Let me check what you have") return false if rest_of_text.length < 100 SUBSTANTIVE_MARKERS.any? { |marker| rest_of_text.match?(marker) } end end