spec_scout 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.idea/.gitignore +10 -0
  3. data/.idea/Projects.iml +41 -0
  4. data/.idea/copilot.data.migration.ask2agent.xml +6 -0
  5. data/.idea/modules.xml +8 -0
  6. data/.idea/vcs.xml +6 -0
  7. data/.rspec_status +236 -0
  8. data/Gemfile +11 -0
  9. data/Gemfile.lock +72 -0
  10. data/LICENSE +21 -0
  11. data/README.md +433 -0
  12. data/Rakefile +12 -0
  13. data/examples/README.md +321 -0
  14. data/examples/best_practices.md +401 -0
  15. data/examples/configurations/basic_config.rb +24 -0
  16. data/examples/configurations/ci_config.rb +35 -0
  17. data/examples/configurations/conservative_config.rb +32 -0
  18. data/examples/configurations/development_config.rb +37 -0
  19. data/examples/configurations/performance_focused_config.rb +38 -0
  20. data/examples/output_formatter_demo.rb +67 -0
  21. data/examples/sample_outputs/console_output_high_confidence.txt +27 -0
  22. data/examples/sample_outputs/console_output_medium_confidence.txt +27 -0
  23. data/examples/sample_outputs/console_output_no_action.txt +27 -0
  24. data/examples/sample_outputs/console_output_risk_detected.txt +27 -0
  25. data/examples/sample_outputs/json_output_high_confidence.json +108 -0
  26. data/examples/sample_outputs/json_output_no_action.json +108 -0
  27. data/examples/workflows/basic_workflow.md +159 -0
  28. data/examples/workflows/ci_integration.md +372 -0
  29. data/exe/spec_scout +7 -0
  30. data/lib/spec_scout/agent_result.rb +44 -0
  31. data/lib/spec_scout/agents/database_agent.rb +113 -0
  32. data/lib/spec_scout/agents/factory_agent.rb +179 -0
  33. data/lib/spec_scout/agents/intent_agent.rb +223 -0
  34. data/lib/spec_scout/agents/risk_agent.rb +290 -0
  35. data/lib/spec_scout/base_agent.rb +72 -0
  36. data/lib/spec_scout/cli.rb +158 -0
  37. data/lib/spec_scout/configuration.rb +162 -0
  38. data/lib/spec_scout/consensus_engine.rb +535 -0
  39. data/lib/spec_scout/enforcement_handler.rb +182 -0
  40. data/lib/spec_scout/output_formatter.rb +307 -0
  41. data/lib/spec_scout/profile_data.rb +37 -0
  42. data/lib/spec_scout/profile_normalizer.rb +238 -0
  43. data/lib/spec_scout/recommendation.rb +62 -0
  44. data/lib/spec_scout/safety_validator.rb +127 -0
  45. data/lib/spec_scout/spec_scout.rb +519 -0
  46. data/lib/spec_scout/testprof_integration.rb +206 -0
  47. data/lib/spec_scout/version.rb +5 -0
  48. data/lib/spec_scout.rb +43 -0
  49. metadata +166 -0
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module SpecScout
6
+ # Safety validation module to ensure no spec file mutations during analysis
7
+ # and prevent auto-application of code changes by default
8
+ class SafetyValidator
9
+ class SafetyViolationError < StandardError; end
10
+
11
+ def initialize(config)
12
+ @config = config
13
+ @monitored_files = Set.new
14
+ @original_file_states = {}
15
+ end
16
+
17
+ # Monitor spec files to ensure they are not modified during analysis
18
+ def monitor_spec_files(spec_paths)
19
+ return unless @config.enabled?
20
+
21
+ spec_paths = Array(spec_paths).compact
22
+ spec_paths.each do |path|
23
+ next unless File.exist?(path)
24
+
25
+ @monitored_files.add(path)
26
+ @original_file_states[path] = {
27
+ mtime: File.mtime(path),
28
+ size: File.size(path),
29
+ checksum: file_checksum(path)
30
+ }
31
+ end
32
+ end
33
+
34
+ # Validate that no monitored files have been modified
35
+ def validate_no_mutations!
36
+ return unless @config.enabled?
37
+
38
+ violations = []
39
+
40
+ @monitored_files.each do |path|
41
+ next unless File.exist?(path)
42
+
43
+ original_state = @original_file_states[path]
44
+ current_state = {
45
+ mtime: File.mtime(path),
46
+ size: File.size(path),
47
+ checksum: file_checksum(path)
48
+ }
49
+
50
+ violations << "File modified during analysis: #{path}" if file_modified?(original_state, current_state)
51
+ end
52
+
53
+ return if violations.empty?
54
+
55
+ raise SafetyViolationError, "Safety violation detected:\n#{violations.join("\n")}"
56
+ end
57
+
58
+ # Ensure no auto-application of code changes
59
+ def prevent_auto_application!
60
+ return unless @config.enabled?
61
+
62
+ return unless @config.auto_apply_enabled?
63
+
64
+ raise SafetyViolationError,
65
+ 'Auto-application of code changes is not allowed by default. Use explicit configuration to enable.'
66
+ end
67
+
68
+ # Validate non-blocking operation mode
69
+ def validate_non_blocking_mode!
70
+ return unless @config.enabled?
71
+
72
+ # In non-blocking mode, we should never exit with failure unless explicitly configured
73
+ return unless @config.blocking_mode_enabled?
74
+
75
+ raise SafetyViolationError,
76
+ 'Blocking mode is not allowed by default. Use enforcement mode configuration to enable.'
77
+ end
78
+
79
+ # Check if the system is operating in safe mode
80
+ def safe_mode?
81
+ @config.enabled? &&
82
+ !@config.auto_apply_enabled? &&
83
+ !@config.blocking_mode_enabled?
84
+ end
85
+
86
+ # Get safety status report
87
+ def safety_status
88
+ {
89
+ safe_mode: safe_mode?,
90
+ monitored_files: @monitored_files.size,
91
+ auto_apply_disabled: !@config.auto_apply_enabled?,
92
+ non_blocking_mode: !@config.blocking_mode_enabled?,
93
+ mutations_detected: mutations_detected?
94
+ }
95
+ end
96
+
97
+ private
98
+
99
+ def file_checksum(path)
100
+ require 'digest'
101
+ Digest::SHA256.file(path).hexdigest
102
+ rescue StandardError
103
+ nil
104
+ end
105
+
106
+ def file_modified?(original, current)
107
+ original[:mtime] != current[:mtime] ||
108
+ original[:size] != current[:size] ||
109
+ (original[:checksum] && current[:checksum] && original[:checksum] != current[:checksum])
110
+ end
111
+
112
+ def mutations_detected?
113
+ @monitored_files.any? do |path|
114
+ next false unless File.exist?(path)
115
+
116
+ original_state = @original_file_states[path]
117
+ current_state = {
118
+ mtime: File.mtime(path),
119
+ size: File.size(path),
120
+ checksum: file_checksum(path)
121
+ }
122
+
123
+ file_modified?(original_state, current_state)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,519 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecScout
4
+ # Main orchestration class that coordinates TestProf integration, agents, and consensus
5
+ class SpecScout
6
+ attr_reader :config, :safety_validator, :enforcement_handler
7
+
8
+ def initialize(config = nil)
9
+ @config = config || ::SpecScout.configuration
10
+ @config.validate!
11
+ @safety_validator = SafetyValidator.new(@config)
12
+ @enforcement_handler = EnforcementHandler.new(@config)
13
+ end
14
+
15
+ # Main entry point for both CLI and programmatic execution
16
+ def analyze(spec_location = nil)
17
+ log_debug("Starting SpecScout analysis for: #{spec_location || 'all specs'}")
18
+
19
+ return disabled_result unless @config.enabled?
20
+
21
+ begin
22
+ # Validate enforcement configuration
23
+ log_debug('Validating enforcement configuration')
24
+ @enforcement_handler.validate_enforcement_config!
25
+
26
+ # Perform safety validations before analysis
27
+ log_debug('Performing safety validations')
28
+ perform_safety_validations(spec_location)
29
+
30
+ # Execute TestProf profiling if enabled
31
+ log_debug('Executing TestProf profiling')
32
+ profile_data = execute_profiling(spec_location)
33
+ return no_profile_data_result unless profile_data
34
+
35
+ # Run agent analysis
36
+ log_debug('Running agent analysis')
37
+ agent_results = run_agents(profile_data)
38
+
39
+ # Check if we have any successful agent results
40
+ successful_results = agent_results.reject { |result| result.verdict == :agent_failed }
41
+ return no_agents_result if successful_results.empty?
42
+
43
+ # Generate consensus recommendation
44
+ log_debug('Generating consensus recommendation')
45
+ recommendation = generate_recommendation(agent_results, profile_data)
46
+
47
+ # Validate no mutations occurred during analysis
48
+ log_debug('Validating no mutations occurred')
49
+ @safety_validator.validate_no_mutations!
50
+
51
+ # Handle enforcement mode
52
+ log_debug('Handling enforcement mode')
53
+ enforcement_result = @enforcement_handler.handle_enforcement(
54
+ recommendation[:recommendation],
55
+ profile_data
56
+ )
57
+
58
+ final_result = recommendation.merge(enforcement_result)
59
+ log_debug('SpecScout analysis completed successfully')
60
+ final_result
61
+ rescue SafetyValidator::SafetyViolationError => e
62
+ log_error("Safety violation detected: #{e.message}")
63
+ handle_safety_violation(e)
64
+ rescue EnforcementHandler::EnforcementFailureError => e
65
+ log_error("Enforcement failure: #{e.message}")
66
+ handle_enforcement_error(e)
67
+ rescue StandardError => e
68
+ log_error("Unexpected error during analysis: #{e.message}")
69
+ log_debug("Full backtrace: #{e.backtrace.join("\n")}") if debug_enabled?
70
+ handle_error(e)
71
+ end
72
+ end
73
+
74
+ # CLI execution mode
75
+ def self.run_cli(args = ARGV)
76
+ begin
77
+ config = parse_cli_args(args)
78
+ scout = new(config)
79
+
80
+ puts '🔍 SpecScout starting analysis...' if config.console_output? && config.debug_mode?
81
+
82
+ result = scout.analyze
83
+
84
+ # Output results
85
+ if result[:recommendation] && result[:profile_data]
86
+ formatter = OutputFormatter.new(result[:recommendation], result[:profile_data])
87
+ output = config.json_output? ? formatter.format_json : formatter.format_recommendation
88
+ puts output
89
+ elsif result[:disabled]
90
+ puts 'SpecScout is disabled' if config.console_output?
91
+ elsif result[:no_profile_data]
92
+ puts 'No profile data available - ensure TestProf is properly configured' if config.console_output?
93
+ elsif result[:no_agents]
94
+ puts 'No agents produced results - check agent configuration' if config.console_output?
95
+ elsif result[:error]
96
+ puts "Analysis failed: #{result[:error].message}" if config.console_output?
97
+ end
98
+
99
+ # Handle enforcement mode output
100
+ puts result[:enforcement_message] if result[:enforcement_message] && !config.json_output?
101
+
102
+ # Handle safety violations
103
+ puts "🚨 Safety violation: #{result[:safety_violation]}" if result[:safety_violation] && !config.json_output?
104
+
105
+ # Handle enforcement errors
106
+ puts "⚠️ Enforcement error: #{result[:enforcement_error]}" if result[:enforcement_error] && !config.json_output?
107
+
108
+ # Exit with appropriate code for CI
109
+ exit_code = result[:exit_code] || (result[:should_fail] ? 1 : 0)
110
+
111
+ puts "🏁 SpecScout completed with exit code: #{exit_code}" if config.console_output? && config.debug_mode?
112
+ exit(exit_code)
113
+ rescue StandardError => e
114
+ warn "🚨 SpecScout CLI failed: #{e.message}"
115
+ warn "Backtrace: #{e.backtrace.join("\n")}" if ENV['SPEC_SCOUT_DEBUG']
116
+ exit(1)
117
+ end
118
+
119
+ result
120
+ end
121
+
122
+ # Programmatic execution mode
123
+ def self.analyze_spec(spec_location = nil, config = nil)
124
+ scout = new(config)
125
+ scout.analyze(spec_location)
126
+ end
127
+
128
+ private
129
+
130
+ def perform_safety_validations(spec_location)
131
+ # Prevent auto-application of code changes by default
132
+ @safety_validator.prevent_auto_application!
133
+
134
+ # Validate non-blocking operation mode
135
+ @safety_validator.validate_non_blocking_mode!
136
+
137
+ # Monitor spec files to detect mutations
138
+ spec_paths = collect_spec_paths(spec_location)
139
+ @safety_validator.monitor_spec_files(spec_paths)
140
+ end
141
+
142
+ def collect_spec_paths(spec_location)
143
+ return [] unless spec_location
144
+
145
+ if File.directory?(spec_location)
146
+ Dir.glob(File.join(spec_location, '**', '*_spec.rb'))
147
+ elsif File.file?(spec_location)
148
+ [spec_location]
149
+ else
150
+ # Try to find spec files in common locations
151
+ spec_dirs = %w[spec test]
152
+ spec_files = []
153
+
154
+ spec_dirs.each do |dir|
155
+ next unless Dir.exist?(dir)
156
+
157
+ spec_files.concat(Dir.glob(File.join(dir, '**', '*_spec.rb')))
158
+ spec_files.concat(Dir.glob(File.join(dir, '**', '*_test.rb')))
159
+ end
160
+
161
+ spec_files
162
+ end
163
+ end
164
+
165
+ def handle_safety_violation(error)
166
+ warn "🚨 Safety Violation: #{error.message}" if @config.console_output?
167
+
168
+ {
169
+ recommendation: nil,
170
+ profile_data: nil,
171
+ agent_results: [],
172
+ safety_violation: error.message,
173
+ should_fail: true, # Safety violations should always fail
174
+ exit_code: 1
175
+ }
176
+ end
177
+
178
+ def handle_enforcement_error(error)
179
+ warn "⚠️ Enforcement Error: #{error.message}" if @config.console_output?
180
+
181
+ {
182
+ recommendation: error.recommendation,
183
+ profile_data: nil,
184
+ agent_results: [],
185
+ enforcement_error: error.message,
186
+ should_fail: true,
187
+ exit_code: 1
188
+ }
189
+ end
190
+
191
+ def execute_profiling(spec_location)
192
+ return nil unless @config.test_prof_enabled?
193
+
194
+ log_debug("Starting TestProf integration for: #{spec_location || 'all specs'}")
195
+
196
+ integration = TestProfIntegration.new(@config)
197
+ profile_data = integration.execute_profiling(spec_location)
198
+
199
+ if profile_data.nil? || profile_data.empty?
200
+ log_debug('No profile data returned from TestProf integration')
201
+ return nil
202
+ end
203
+
204
+ log_debug("TestProf data extracted successfully: #{profile_data.keys}")
205
+
206
+ normalizer = ProfileNormalizer.new
207
+ normalized_data = normalizer.normalize(profile_data, build_example_context(spec_location))
208
+
209
+ log_debug('Profile data normalized successfully')
210
+ normalized_data
211
+ rescue TestProfIntegration::TestProfError => e
212
+ log_error("TestProf integration failed: #{e.message}")
213
+ nil
214
+ rescue ProfileNormalizer::NormalizationError => e
215
+ log_error("Profile data normalization failed: #{e.message}")
216
+ nil
217
+ rescue StandardError => e
218
+ log_error("Unexpected error during profiling: #{e.message}")
219
+ log_debug("Backtrace: #{e.backtrace.join("\n")}") if debug_enabled?
220
+ nil
221
+ end
222
+
223
+ def run_agents(profile_data)
224
+ log_debug("Starting agent analysis with #{@config.enabled_agents.size} enabled agents")
225
+ agent_results = []
226
+
227
+ # Run each enabled agent
228
+ @config.enabled_agents.each do |agent_name|
229
+ next unless @config.agent_enabled?(agent_name)
230
+
231
+ log_debug("Running #{agent_name} agent")
232
+
233
+ begin
234
+ agent = create_agent(agent_name, profile_data)
235
+ result = agent.evaluate
236
+
237
+ # Validate agent result structure
238
+ validate_agent_result(result, agent_name)
239
+
240
+ # Handle both Hash and AgentResult objects
241
+ if result.is_a?(AgentResult)
242
+ agent_results << result
243
+ else
244
+ # Convert Hash to AgentResult for consistency
245
+ agent_result = AgentResult.new(
246
+ agent_name: agent_name,
247
+ verdict: result[:verdict],
248
+ confidence: result[:confidence],
249
+ reasoning: result[:reasoning],
250
+ metadata: result[:metadata] || {}
251
+ )
252
+ agent_results << agent_result
253
+ end
254
+ log_debug("#{agent_name} agent completed: #{result[:verdict]} (#{result[:confidence]})")
255
+ rescue StandardError => e
256
+ log_error("Agent #{agent_name} failed: #{e.message}")
257
+ log_debug("Agent #{agent_name} backtrace: #{e.backtrace.join("\n")}") if debug_enabled?
258
+
259
+ # Create a failed agent result for debugging
260
+ failed_result = AgentResult.new(
261
+ agent_name: agent_name,
262
+ verdict: :agent_failed,
263
+ confidence: :none,
264
+ reasoning: "Agent execution failed: #{e.message}",
265
+ metadata: { error: e.message, failed_at: Time.now }
266
+ )
267
+
268
+ # Only include failed results in debug mode
269
+ agent_results << failed_result if debug_enabled?
270
+ end
271
+ end
272
+
273
+ log_debug("Agent analysis completed: #{agent_results.size} successful results")
274
+ agent_results
275
+ end
276
+
277
+ def create_agent(agent_name, profile_data)
278
+ case agent_name
279
+ when :database
280
+ Agents::DatabaseAgent.new(profile_data)
281
+ when :factory
282
+ Agents::FactoryAgent.new(profile_data)
283
+ when :intent
284
+ Agents::IntentAgent.new(profile_data)
285
+ when :risk
286
+ Agents::RiskAgent.new(profile_data)
287
+ else
288
+ raise ArgumentError, "Unknown agent: #{agent_name}"
289
+ end
290
+ end
291
+
292
+ def generate_recommendation(agent_results, profile_data)
293
+ log_debug("Generating consensus from #{agent_results.size} agent results")
294
+
295
+ begin
296
+ consensus = ConsensusEngine.new(agent_results, profile_data)
297
+ recommendation = consensus.generate_recommendation
298
+
299
+ log_debug("Consensus generated: #{recommendation[:action]} (#{recommendation[:confidence]})")
300
+
301
+ {
302
+ recommendation: recommendation,
303
+ profile_data: profile_data,
304
+ agent_results: agent_results
305
+ }
306
+ rescue StandardError => e
307
+ log_error("Consensus generation failed: #{e.message}")
308
+ log_debug("Consensus backtrace: #{e.backtrace.join("\n")}") if debug_enabled?
309
+
310
+ # Return a fallback result
311
+ {
312
+ recommendation: create_fallback_recommendation(e),
313
+ profile_data: profile_data,
314
+ agent_results: agent_results
315
+ }
316
+ end
317
+ end
318
+
319
+ def handle_error(error)
320
+ error_message = "SpecScout analysis failed: #{error.message}"
321
+
322
+ if @config.console_output?
323
+ warn error_message
324
+ warn "Error type: #{error.class.name}"
325
+
326
+ if debug_enabled?
327
+ warn 'Full backtrace:'
328
+ warn error.backtrace.join("\n")
329
+ else
330
+ warn 'Run with SPEC_SCOUT_DEBUG=true for full backtrace'
331
+ end
332
+ end
333
+
334
+ {
335
+ recommendation: nil,
336
+ profile_data: nil,
337
+ agent_results: [],
338
+ error: error,
339
+ error_message: error_message,
340
+ should_fail: false, # Don't fail by default on unexpected errors
341
+ exit_code: 0
342
+ }
343
+ end
344
+
345
+ def disabled_result
346
+ {
347
+ recommendation: nil,
348
+ profile_data: nil,
349
+ agent_results: [],
350
+ disabled: true,
351
+ should_fail: false,
352
+ exit_code: 0
353
+ }
354
+ end
355
+
356
+ def no_profile_data_result
357
+ {
358
+ recommendation: nil,
359
+ profile_data: nil,
360
+ agent_results: [],
361
+ no_profile_data: true,
362
+ should_fail: false,
363
+ exit_code: 0
364
+ }
365
+ end
366
+
367
+ def no_agents_result
368
+ {
369
+ recommendation: nil,
370
+ profile_data: nil,
371
+ agent_results: [],
372
+ no_agents: true,
373
+ should_fail: false,
374
+ exit_code: 0
375
+ }
376
+ end
377
+
378
+ # Parse CLI arguments into configuration
379
+ def self.parse_cli_args(args)
380
+ config = ::SpecScout.configuration.dup
381
+
382
+ i = 0
383
+ while i < args.length
384
+ case args[i]
385
+ when '--disable'
386
+ config.enable = false
387
+ when '--no-testprof'
388
+ config.use_test_prof = false
389
+ when '--enforce'
390
+ config.enforcement_mode = true
391
+ when '--fail-on-high-confidence'
392
+ config.fail_on_high_confidence = true
393
+ when '--auto-apply'
394
+ config.auto_apply_enabled = true
395
+ when '--blocking-mode'
396
+ config.blocking_mode_enabled = true
397
+ when '--output'
398
+ i += 1
399
+ config.output_format = args[i] if i < args.length
400
+ when '--enable-agent'
401
+ i += 1
402
+ config.enable_agent(args[i]) if i < args.length
403
+ when '--disable-agent'
404
+ i += 1
405
+ config.disable_agent(args[i]) if i < args.length
406
+ when '--help', '-h'
407
+ print_help
408
+ exit(0)
409
+ end
410
+ i += 1
411
+ end
412
+
413
+ config.validate!
414
+ config
415
+ end
416
+
417
+ def self.print_help
418
+ puts <<~HELP
419
+ SpecScout - Intelligent test optimization advisor
420
+
421
+ Usage: spec_scout [options]
422
+
423
+ Options:
424
+ --disable Disable SpecScout analysis
425
+ --no-testprof Disable TestProf integration
426
+ --enforce Enable enforcement mode (fail on high confidence)
427
+ --fail-on-high-confidence Fail on high confidence recommendations
428
+ --auto-apply Enable auto-application of code changes (UNSAFE)
429
+ --blocking-mode Enable blocking operation mode
430
+ --output FORMAT Output format (console, json)
431
+ --enable-agent AGENT Enable specific agent (database, factory, intent, risk)
432
+ --disable-agent AGENT Disable specific agent
433
+ --help, -h Show this help message
434
+
435
+ Safety Options:
436
+ By default, SpecScout operates in safe mode:
437
+ - No spec files are modified during analysis
438
+ - No code changes are auto-applied
439
+ - Non-blocking operation (recommendations only)
440
+
441
+ Examples:
442
+ spec_scout # Run with default settings (safe mode)
443
+ spec_scout --enforce # Enable enforcement mode
444
+ spec_scout --output json # JSON output
445
+ spec_scout --disable-agent risk # Disable risk agent
446
+ spec_scout --auto-apply --enforce # UNSAFE: Enable auto-application
447
+ HELP
448
+ end
449
+
450
+ # Build example context for profile normalization
451
+ def build_example_context(spec_location)
452
+ context = {}
453
+
454
+ if spec_location
455
+ context[:location] = spec_location
456
+ context[:file_path] = spec_location
457
+ end
458
+
459
+ context
460
+ end
461
+
462
+ # Validate agent result structure
463
+ def validate_agent_result(result, agent_name)
464
+ # Handle both Hash and AgentResult objects
465
+ if result.is_a?(AgentResult)
466
+ # AgentResult objects are already validated
467
+ true
468
+ elsif result.is_a?(Hash)
469
+ # Validate Hash format (for backward compatibility)
470
+ required_keys = %i[verdict confidence reasoning]
471
+ missing_keys = required_keys - result.keys
472
+
473
+ unless missing_keys.empty?
474
+ raise ArgumentError, "Agent #{agent_name} result missing required keys: #{missing_keys}"
475
+ end
476
+
477
+ # Validate confidence levels
478
+ valid_confidence_levels = %i[high medium low none]
479
+ unless valid_confidence_levels.include?(result[:confidence])
480
+ raise ArgumentError, "Agent #{agent_name} returned invalid confidence level: #{result[:confidence]}"
481
+ end
482
+ else
483
+ raise ArgumentError, "Agent #{agent_name} must return a Hash or AgentResult, got #{result.class}"
484
+ end
485
+ end
486
+
487
+ # Create fallback recommendation when consensus fails
488
+ def create_fallback_recommendation(error)
489
+ Recommendation.new(
490
+ spec_location: 'unknown',
491
+ action: :no_action,
492
+ from_value: nil,
493
+ to_value: nil,
494
+ confidence: :none,
495
+ explanation: "Unable to generate recommendation due to consensus engine failure: #{error.message}",
496
+ agent_results: []
497
+ )
498
+ end
499
+
500
+ # Logging helpers
501
+ def log_debug(message)
502
+ return unless debug_enabled?
503
+
504
+ return unless @config.console_output?
505
+
506
+ puts "[DEBUG] SpecScout: #{message}"
507
+ end
508
+
509
+ def log_error(message)
510
+ return unless @config.console_output?
511
+
512
+ warn "[ERROR] SpecScout: #{message}"
513
+ end
514
+
515
+ def debug_enabled?
516
+ ENV['SPEC_SCOUT_DEBUG'] == 'true' || @config.debug_mode?
517
+ end
518
+ end
519
+ end