aidp 0.32.0 → 0.34.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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/lib/aidp/analyze/feature_analyzer.rb +322 -320
  4. data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
  5. data/lib/aidp/auto_update/coordinator.rb +97 -7
  6. data/lib/aidp/auto_update.rb +0 -12
  7. data/lib/aidp/cli/devcontainer_commands.rb +0 -5
  8. data/lib/aidp/cli/eval_command.rb +399 -0
  9. data/lib/aidp/cli/harness_command.rb +1 -1
  10. data/lib/aidp/cli/security_command.rb +416 -0
  11. data/lib/aidp/cli/tools_command.rb +6 -4
  12. data/lib/aidp/cli.rb +172 -4
  13. data/lib/aidp/comment_consolidator.rb +78 -0
  14. data/lib/aidp/concurrency/exec.rb +3 -0
  15. data/lib/aidp/concurrency.rb +0 -3
  16. data/lib/aidp/config.rb +113 -1
  17. data/lib/aidp/config_paths.rb +91 -0
  18. data/lib/aidp/daemon/runner.rb +8 -4
  19. data/lib/aidp/errors.rb +134 -0
  20. data/lib/aidp/evaluations/context_capture.rb +205 -0
  21. data/lib/aidp/evaluations/evaluation_record.rb +114 -0
  22. data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
  23. data/lib/aidp/evaluations.rb +23 -0
  24. data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
  25. data/lib/aidp/execute/interactive_repl.rb +6 -2
  26. data/lib/aidp/execute/prompt_evaluator.rb +359 -0
  27. data/lib/aidp/execute/repl_macros.rb +100 -1
  28. data/lib/aidp/execute/work_loop_runner.rb +719 -58
  29. data/lib/aidp/execute/work_loop_state.rb +4 -1
  30. data/lib/aidp/execute/workflow_selector.rb +3 -0
  31. data/lib/aidp/harness/ai_decision_engine.rb +79 -0
  32. data/lib/aidp/harness/ai_filter_factory.rb +285 -0
  33. data/lib/aidp/harness/capability_registry.rb +2 -0
  34. data/lib/aidp/harness/condition_detector.rb +3 -0
  35. data/lib/aidp/harness/config_loader.rb +3 -0
  36. data/lib/aidp/harness/config_schema.rb +97 -1
  37. data/lib/aidp/harness/config_validator.rb +1 -1
  38. data/lib/aidp/harness/configuration.rb +61 -5
  39. data/lib/aidp/harness/enhanced_runner.rb +14 -11
  40. data/lib/aidp/harness/error_handler.rb +3 -0
  41. data/lib/aidp/harness/filter_definition.rb +212 -0
  42. data/lib/aidp/harness/generated_filter_strategy.rb +197 -0
  43. data/lib/aidp/harness/output_filter.rb +50 -25
  44. data/lib/aidp/harness/output_filter_config.rb +129 -0
  45. data/lib/aidp/harness/provider_factory.rb +3 -0
  46. data/lib/aidp/harness/provider_manager.rb +96 -2
  47. data/lib/aidp/harness/runner.rb +5 -12
  48. data/lib/aidp/harness/state/persistence.rb +3 -0
  49. data/lib/aidp/harness/state_manager.rb +3 -0
  50. data/lib/aidp/harness/status_display.rb +28 -20
  51. data/lib/aidp/harness/test_runner.rb +179 -41
  52. data/lib/aidp/harness/thinking_depth_manager.rb +44 -28
  53. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
  54. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
  55. data/lib/aidp/harness/ui/error_handler.rb +3 -0
  56. data/lib/aidp/harness/ui/job_monitor.rb +4 -0
  57. data/lib/aidp/harness/ui/navigation/submenu.rb +2 -2
  58. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
  59. data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
  60. data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
  61. data/lib/aidp/harness/user_interface.rb +3 -0
  62. data/lib/aidp/loader.rb +195 -0
  63. data/lib/aidp/logger.rb +3 -0
  64. data/lib/aidp/message_display.rb +31 -0
  65. data/lib/aidp/metadata/compiler.rb +29 -17
  66. data/lib/aidp/metadata/query.rb +1 -1
  67. data/lib/aidp/metadata/scanner.rb +8 -1
  68. data/lib/aidp/metadata/tool_metadata.rb +13 -13
  69. data/lib/aidp/metadata/validator.rb +10 -0
  70. data/lib/aidp/metadata.rb +16 -0
  71. data/lib/aidp/pr_worktree_manager.rb +20 -8
  72. data/lib/aidp/provider_manager.rb +4 -7
  73. data/lib/aidp/providers/base.rb +2 -0
  74. data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
  75. data/lib/aidp/security/secrets_proxy.rb +328 -0
  76. data/lib/aidp/security/secrets_registry.rb +227 -0
  77. data/lib/aidp/security/trifecta_state.rb +220 -0
  78. data/lib/aidp/security/watch_mode_handler.rb +306 -0
  79. data/lib/aidp/security/work_loop_adapter.rb +277 -0
  80. data/lib/aidp/security.rb +56 -0
  81. data/lib/aidp/setup/wizard.rb +283 -11
  82. data/lib/aidp/skills.rb +0 -5
  83. data/lib/aidp/storage/csv_storage.rb +3 -0
  84. data/lib/aidp/style_guide/selector.rb +360 -0
  85. data/lib/aidp/tooling_detector.rb +283 -16
  86. data/lib/aidp/version.rb +1 -1
  87. data/lib/aidp/watch/auto_merger.rb +274 -0
  88. data/lib/aidp/watch/auto_pr_processor.rb +125 -7
  89. data/lib/aidp/watch/build_processor.rb +16 -1
  90. data/lib/aidp/watch/change_request_processor.rb +682 -150
  91. data/lib/aidp/watch/ci_fix_processor.rb +262 -4
  92. data/lib/aidp/watch/feedback_collector.rb +191 -0
  93. data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
  94. data/lib/aidp/watch/implementation_verifier.rb +142 -1
  95. data/lib/aidp/watch/plan_generator.rb +70 -13
  96. data/lib/aidp/watch/plan_processor.rb +12 -5
  97. data/lib/aidp/watch/projects_processor.rb +286 -0
  98. data/lib/aidp/watch/repository_client.rb +871 -22
  99. data/lib/aidp/watch/review_processor.rb +33 -6
  100. data/lib/aidp/watch/runner.rb +80 -29
  101. data/lib/aidp/watch/state_store.rb +233 -0
  102. data/lib/aidp/watch/sub_issue_creator.rb +221 -0
  103. data/lib/aidp/watch.rb +5 -7
  104. data/lib/aidp/workflows/guided_agent.rb +4 -0
  105. data/lib/aidp/workstream_cleanup.rb +0 -2
  106. data/lib/aidp/workstream_executor.rb +3 -4
  107. data/lib/aidp/worktree.rb +61 -12
  108. data/lib/aidp/worktree_branch_manager.rb +347 -101
  109. data/lib/aidp.rb +21 -106
  110. data/templates/implementation/iterative_implementation.md +46 -3
  111. metadata +91 -36
  112. data/lib/aidp/config/paths.rb +0 -131
@@ -3,16 +3,19 @@
3
3
  require "open3"
4
4
  require_relative "../tooling_detector"
5
5
  require_relative "output_filter"
6
+ require_relative "output_filter_config"
6
7
 
7
8
  module Aidp
8
9
  module Harness
9
10
  # Executes test and linter commands configured in aidp.yml
10
11
  # Returns results with exit status and output
12
+ # Applies intelligent output filtering to reduce token consumption
11
13
  class TestRunner
12
14
  def initialize(project_dir, config)
13
15
  @project_dir = project_dir
14
16
  @config = config
15
17
  @iteration_count = 0
18
+ @filter_stats = {total_input_bytes: 0, total_output_bytes: 0}
16
19
  end
17
20
 
18
21
  # Run all configured tests
@@ -47,6 +50,29 @@ module Aidp
47
50
  run_command_category(:documentation, "Documentation")
48
51
  end
49
52
 
53
+ # Preview the commands that will run for each category so callers can log intent
54
+ # Returns a hash of category => array of normalized command entries
55
+ def planned_commands
56
+ {
57
+ tests: resolved_commands(:test),
58
+ lints: resolved_commands(:lint),
59
+ formatters: resolved_commands(:formatter),
60
+ builds: resolved_commands(:build),
61
+ docs: resolved_commands(:documentation)
62
+ }
63
+ end
64
+
65
+ # Get current iteration count
66
+ attr_reader :iteration_count
67
+
68
+ # Get filtering statistics
69
+ attr_reader :filter_stats
70
+
71
+ # Reset iteration counter (useful for testing)
72
+ def reset_iteration_count
73
+ @iteration_count = 0
74
+ end
75
+
50
76
  private
51
77
 
52
78
  # Run commands for a specific category (test, lint, formatter, build, documentation)
@@ -59,6 +85,12 @@ module Aidp
59
85
  # Determine output mode based on category
60
86
  mode = determine_output_mode(category)
61
87
 
88
+ Aidp.log_debug("test_runner", "running_category",
89
+ category: category,
90
+ command_count: commands.length,
91
+ iteration: @iteration_count,
92
+ output_mode: mode)
93
+
62
94
  # Execute all commands
63
95
  results = commands.map do |cmd_config|
64
96
  # Handle both string commands (legacy) and hash format (new)
@@ -71,6 +103,23 @@ module Aidp
71
103
  end
72
104
  end
73
105
 
106
+ aggregate_results(results, display_name, mode: mode)
107
+ rescue NameError
108
+ # Logging not available
109
+ commands = resolved_commands(category)
110
+ return {success: true, output: "", failures: [], required_failures: []} if commands.empty?
111
+
112
+ mode = determine_output_mode(category)
113
+ results = commands.map do |cmd_config|
114
+ if cmd_config.is_a?(String)
115
+ result = execute_command(cmd_config, category.to_s)
116
+ result.merge(required: true)
117
+ else
118
+ result = execute_command(cmd_config[:command], category.to_s)
119
+ result.merge(required: cmd_config[:required])
120
+ end
121
+ end
122
+
74
123
  aggregate_results(results, display_name, mode: mode)
75
124
  end
76
125
 
@@ -107,6 +156,11 @@ module Aidp
107
156
  (optional_failures.any? ? "\n" + format_failures(optional_failures, "#{category} - Optional", mode: mode) : "")
108
157
  end
109
158
 
159
+ # Add filtering summary if filtering was applied
160
+ if mode != :full && all_failures.any?
161
+ output += "\n[Output filtered: mode=#{mode}]"
162
+ end
163
+
110
164
  {
111
165
  success: success,
112
166
  output: output,
@@ -124,9 +178,12 @@ module Aidp
124
178
  output << "Exit Code: #{failure[:exit_code]}"
125
179
  output << "--- Output ---"
126
180
 
181
+ # Detect framework for filtering
182
+ framework = detect_framework_from_command(failure[:command])
183
+
127
184
  # Apply filtering based on mode and framework
128
- filtered_stdout = filter_output(failure[:stdout], mode, detect_framework_from_command(failure[:command]))
129
- filtered_stderr = filter_output(failure[:stderr], mode, :unknown)
185
+ filtered_stdout = filter_output(failure[:stdout], mode, framework, :test)
186
+ filtered_stderr = filter_output(failure[:stderr], mode, :unknown, :lint)
130
187
 
131
188
  output << filtered_stdout unless filtered_stdout.strip.empty?
132
189
  output << filtered_stderr unless filtered_stderr.strip.empty?
@@ -136,67 +193,139 @@ module Aidp
136
193
  output.join("\n")
137
194
  end
138
195
 
139
- def filter_output(raw_output, mode, framework)
196
+ def filter_output(raw_output, mode, framework, category)
140
197
  return raw_output if mode == :full || raw_output.nil? || raw_output.empty?
141
198
 
142
- filter_config = {
199
+ # Build filter config from configuration or defaults
200
+ filter_config = build_filter_config(mode, category)
201
+
202
+ # Load AI-generated filter definition if available (AGD pattern)
203
+ filter_definition = load_filter_definition_for(category)
204
+
205
+ # Track input size for stats
206
+ @filter_stats[:total_input_bytes] += raw_output.bytesize
207
+
208
+ filter = OutputFilter.new(filter_config, filter_definition: filter_definition)
209
+ filtered = filter.filter(raw_output, framework: framework)
210
+
211
+ # Track output size for stats
212
+ @filter_stats[:total_output_bytes] += filtered.bytesize
213
+
214
+ Aidp.log_debug("test_runner", "output_filtered",
215
+ framework: framework,
143
216
  mode: mode,
144
- include_context: true,
145
- context_lines: 3,
146
- max_lines: 500
147
- }
217
+ input_size: raw_output.bytesize,
218
+ output_size: filtered.bytesize,
219
+ has_custom_definition: !filter_definition.nil?)
148
220
 
149
- filter = OutputFilter.new(filter_config)
150
- filter.filter(raw_output, framework: framework)
221
+ filtered
151
222
  rescue NameError
152
223
  # Logging infrastructure not available
153
224
  raw_output
154
225
  rescue => e
155
- Aidp.log_warn("test_runner", "filter_failed",
156
- error: e.message,
157
- framework: framework)
226
+ begin
227
+ Aidp.log_warn("test_runner", "filter_failed",
228
+ error: e.message,
229
+ framework: framework)
230
+ rescue NameError
231
+ # Logging not available
232
+ end
158
233
  raw_output # Fallback to unfiltered on error
159
234
  end
160
235
 
161
- def detect_framework_from_command(command)
162
- case command
163
- when /rspec/
164
- :rspec
165
- when /minitest/
166
- :minitest
167
- when /jest/
168
- :jest
169
- when /pytest/
170
- :pytest
171
- else
172
- :unknown
236
+ # Load AI-generated filter definition for a category
237
+ # @param category [Symbol] :test or :lint
238
+ # @return [FilterDefinition, nil]
239
+ def load_filter_definition_for(category)
240
+ return nil unless @config.respond_to?(:filter_definition_for)
241
+
242
+ # Map category to definition key (matches setup wizard keys)
243
+ definition_key = case category
244
+ when :test then "unit_test"
245
+ when :lint then "lint"
246
+ else category.to_s
173
247
  end
248
+
249
+ @config.filter_definition_for(definition_key)
250
+ end
251
+
252
+ def build_filter_config(mode, category)
253
+ config_hash = {
254
+ mode: mode,
255
+ include_context: output_filtering_include_context,
256
+ context_lines: output_filtering_context_lines,
257
+ max_lines: max_output_lines_for_category(category)
258
+ }
259
+
260
+ OutputFilterConfig.from_hash(config_hash)
261
+ end
262
+
263
+ def detect_framework_from_command(command)
264
+ # Use ToolingDetector's enhanced framework detection
265
+ Aidp::ToolingDetector.framework_from_command(command)
174
266
  end
175
267
 
176
268
  def determine_output_mode(category)
177
269
  # Check config for category-specific mode
178
270
  case category
179
271
  when :test
180
- if @config.respond_to?(:test_output_mode)
181
- @config.test_output_mode
182
- elsif @iteration_count > 1
183
- :failures_only
184
- else
185
- :full
186
- end
272
+ configured_mode = test_output_mode_from_config
273
+ return configured_mode if configured_mode
274
+
275
+ # Default: full on first iteration, failures_only after
276
+ (@iteration_count > 1) ? :failures_only : :full
187
277
  when :lint
188
- if @config.respond_to?(:lint_output_mode)
189
- @config.lint_output_mode
190
- elsif @iteration_count > 1
191
- :failures_only
192
- else
193
- :full
194
- end
278
+ configured_mode = lint_output_mode_from_config
279
+ return configured_mode if configured_mode
280
+
281
+ # Default: full on first iteration, failures_only after
282
+ (@iteration_count > 1) ? :failures_only : :full
195
283
  else
196
284
  :full
197
285
  end
198
286
  end
199
287
 
288
+ def test_output_mode_from_config
289
+ return nil unless @config.respond_to?(:test_output_mode)
290
+ mode = @config.test_output_mode
291
+ return nil if mode.nil?
292
+ mode.to_sym
293
+ rescue
294
+ nil
295
+ end
296
+
297
+ def lint_output_mode_from_config
298
+ return nil unless @config.respond_to?(:lint_output_mode)
299
+ mode = @config.lint_output_mode
300
+ return nil if mode.nil?
301
+ mode.to_sym
302
+ rescue
303
+ nil
304
+ end
305
+
306
+ def max_output_lines_for_category(category)
307
+ case category
308
+ when :test
309
+ return @config.test_max_output_lines if @config.respond_to?(:test_max_output_lines) && @config.test_max_output_lines
310
+ OutputFilterConfig::DEFAULT_MAX_LINES
311
+ when :lint
312
+ return @config.lint_max_output_lines if @config.respond_to?(:lint_max_output_lines) && @config.lint_max_output_lines
313
+ 300 # Default smaller for lint output
314
+ else
315
+ OutputFilterConfig::DEFAULT_MAX_LINES
316
+ end
317
+ end
318
+
319
+ def output_filtering_include_context
320
+ return @config.output_filtering_include_context if @config.respond_to?(:output_filtering_include_context)
321
+ OutputFilterConfig::DEFAULT_INCLUDE_CONTEXT
322
+ end
323
+
324
+ def output_filtering_context_lines
325
+ return @config.output_filtering_context_lines if @config.respond_to?(:output_filtering_context_lines)
326
+ OutputFilterConfig::DEFAULT_CONTEXT_LINES
327
+ end
328
+
200
329
  # Resolve commands for a specific category
201
330
  # Returns normalized command configs (array of {command:, required:} hashes)
202
331
  def resolved_commands(category)
@@ -239,8 +368,17 @@ module Aidp
239
368
  def detected_tooling
240
369
  @detected_tooling ||= Aidp::ToolingDetector.detect(@project_dir)
241
370
  rescue => e
242
- Aidp.log_warn("test_runner", "tooling_detection_failed", error: e.message)
243
- Aidp::ToolingDetector::Result.new(test_commands: [], lint_commands: [])
371
+ begin
372
+ Aidp.log_warn("test_runner", "tooling_detection_failed", error: e.message)
373
+ rescue NameError
374
+ # Logging not available
375
+ end
376
+ Aidp::ToolingDetector::Result.new(
377
+ test_commands: [],
378
+ lint_commands: [],
379
+ formatter_commands: [],
380
+ frameworks: {}
381
+ )
244
382
  end
245
383
 
246
384
  def log_fallback(type, commands)
@@ -15,7 +15,7 @@ module Aidp
15
15
 
16
16
  def initialize(configuration, registry: nil, root_dir: nil)
17
17
  @configuration = configuration
18
- @registry = registry || CapabilityRegistry.new(root_dir: root_dir || configuration.instance_variable_get(:@project_dir))
18
+ @registry = registry || CapabilityRegistry.new(root_dir: root_dir || configuration.project_dir)
19
19
  @current_tier = nil
20
20
  @session_max_tier = nil
21
21
  @tier_history = []
@@ -152,6 +152,15 @@ module Aidp
152
152
  def select_model_for_tier(tier = nil, provider: nil)
153
153
  tier ||= current_tier
154
154
  validate_tier!(tier)
155
+ provider_has_no_tiers = provider && configuration.configured_tiers(provider).empty?
156
+ provider_has_catalog_models = provider && !@registry.models_for_provider(provider).empty?
157
+
158
+ if provider_has_no_tiers && !provider_has_catalog_models
159
+ Aidp.log_info("thinking_depth_manager", "No configured tiers for provider, deferring to provider auto model selection",
160
+ requested_tier: tier,
161
+ provider: provider)
162
+ return [provider, nil, {auto_model: true, reason: "provider_has_no_tiers"}]
163
+ end
155
164
 
156
165
  # First, try to get models from user's configuration for this tier and provider
157
166
  if provider
@@ -234,43 +243,47 @@ module Aidp
234
243
  model: model_name)
235
244
  return [provider, model_name, model_data]
236
245
  end
237
-
238
- # If provider doesn't support tier and switching allowed, try others
239
- unless configuration.allow_provider_switch_for_tier?
240
- Aidp.log_warn("thinking_depth_manager", "Provider lacks tier in catalog, switching disabled",
241
- tier: tier,
242
- provider: provider)
243
- return nil
244
- end
245
- end
246
-
247
- # Try all providers in catalog
248
- if provider && !configuration.allow_provider_switch_for_tier?
249
- return nil
246
+ # Per issue #323: Don't return nil here - let fallback logic handle missing tiers
250
247
  end
251
248
 
252
- providers_to_try = provider ? [@registry.provider_names - [provider]].flatten : @registry.provider_names
249
+ # Try all providers in catalog if provider switching is allowed
250
+ if configuration.allow_provider_switch_for_tier?
251
+ providers_to_try = provider ? (@registry.provider_names - [provider]) : @registry.provider_names
253
252
 
254
- providers_to_try.each do |prov_name|
255
- model_name, model_data = @registry.best_model_for_tier(tier, prov_name)
256
- if model_name
257
- Aidp.log_info("thinking_depth_manager", "Selected model from catalog (alternate provider)",
258
- tier: tier,
259
- original_provider: provider,
260
- selected_provider: prov_name,
261
- model: model_name)
262
- return [prov_name, model_name, model_data]
253
+ providers_to_try.each do |prov_name|
254
+ model_name, model_data = @registry.best_model_for_tier(tier, prov_name)
255
+ if model_name
256
+ Aidp.log_info("thinking_depth_manager", "Selected model from catalog (alternate provider)",
257
+ tier: tier,
258
+ original_provider: provider,
259
+ selected_provider: prov_name,
260
+ model: model_name)
261
+ return [prov_name, model_name, model_data]
262
+ end
263
263
  end
264
264
  end
265
265
 
266
266
  # No model found for requested tier - try fallback to other tiers
267
- Aidp.log_warn("thinking_depth_manager", "No model found for requested tier, trying fallback",
267
+ # Per issue #323: fallback events log at debug level
268
+ Aidp.log_debug("thinking_depth_manager", "tier_not_found_trying_fallback",
268
269
  tier: tier,
269
270
  provider: provider)
270
271
 
271
272
  result = try_fallback_tiers(tier, provider)
272
273
 
274
+ # If no model found after fallback, defer to provider auto model selection
275
+ # This allows providers to select their own model when no explicit tier config exists
276
+ # Per issue #323: log at debug level, don't constrain model selection
277
+ if result.nil? && provider
278
+ Aidp.log_debug("thinking_depth_manager", "no_model_for_tier_deferring_to_provider",
279
+ requested_tier: tier,
280
+ provider: provider,
281
+ reason: provider_has_no_tiers ? "provider_has_no_tiers" : "tier_not_configured")
282
+ return [provider, nil, {auto_model: true, reason: provider_has_no_tiers ? "provider_has_no_tiers" : "tier_not_configured"}]
283
+ end
284
+
273
285
  unless result
286
+ # This path should only be reached when no provider is specified
274
287
  # Enhanced error message with discovery hints
275
288
  display_enhanced_tier_error(tier, provider)
276
289
 
@@ -436,7 +449,8 @@ module Aidp
436
449
 
437
450
  if configured_models.any?
438
451
  model_name = configured_models.first
439
- Aidp.log_warn("thinking_depth_manager", "Falling back to different tier (from config)",
452
+ # Per issue #323: fallback events log at debug level
453
+ Aidp.log_debug("thinking_depth_manager", "tier_fallback_from_config",
440
454
  requested_tier: requested_tier,
441
455
  fallback_tier: fallback_tier,
442
456
  provider: provider,
@@ -450,7 +464,8 @@ module Aidp
450
464
  if provider
451
465
  model_name, model_data = @registry.best_model_for_tier(fallback_tier, provider)
452
466
  if model_name
453
- Aidp.log_warn("thinking_depth_manager", "Falling back to different tier (from catalog)",
467
+ # Per issue #323: fallback events log at debug level
468
+ Aidp.log_debug("thinking_depth_manager", "tier_fallback_from_catalog",
454
469
  requested_tier: requested_tier,
455
470
  fallback_tier: fallback_tier,
456
471
  provider: provider,
@@ -466,7 +481,8 @@ module Aidp
466
481
 
467
482
  model_name, model_data = @registry.best_model_for_tier(fallback_tier, prov_name)
468
483
  if model_name
469
- Aidp.log_warn("thinking_depth_manager", "Falling back to different tier and provider (from catalog)",
484
+ # Per issue #323: fallback events log at debug level
485
+ Aidp.log_debug("thinking_depth_manager", "tier_fallback_provider_switch",
470
486
  requested_tier: requested_tier,
471
487
  fallback_tier: fallback_tier,
472
488
  requested_provider: provider,
@@ -20,6 +20,10 @@ module Aidp
20
20
 
21
21
  class DisplayError < TUIError; end
22
22
 
23
+ # Expose state for testability
24
+ attr_accessor :headless, :current_mode, :workflow_active, :current_step
25
+ attr_reader :jobs
26
+
23
27
  def initialize(prompt: TTY::Prompt.new, tty: $stdin)
24
28
  @cursor = TTY::Cursor
25
29
  @screen = TTY::Screen
@@ -11,6 +11,10 @@ module Aidp
11
11
  class EnhancedWorkflowSelector
12
12
  class WorkflowError < StandardError; end
13
13
 
14
+ # Expose state for testability
15
+ attr_reader :tui, :project_dir, :user_input
16
+ attr_accessor :workflow_selector
17
+
14
18
  def initialize(tui = nil, project_dir: Dir.pwd)
15
19
  @tui = tui || EnhancedTUI.new
16
20
  @user_input = {}
@@ -18,6 +18,9 @@ module Aidp
18
18
 
19
19
  class InteractionError < UIError; end
20
20
 
21
+ # Expose for testability
22
+ attr_reader :logger
23
+
21
24
  def initialize(ui_components = {})
22
25
  @logger = ui_components[:logger] || default_logger
23
26
  @formatter = ui_components[:formatter] || ErrorFormatter.new
@@ -17,6 +17,10 @@ module Aidp
17
17
 
18
18
  class MonitorError < JobMonitorError; end
19
19
 
20
+ # Expose for testability
21
+ attr_accessor :monitor_thread
22
+ attr_reader :update_callbacks
23
+
20
24
  JOB_STATUSES = {
21
25
  pending: "Pending",
22
26
  running: "Running",
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "main_menu"
4
-
5
3
  module Aidp
6
4
  module Harness
7
5
  module UI
@@ -24,6 +22,8 @@ module Aidp
24
22
 
25
23
  attr_reader :title, :parent_menu
26
24
  attr_accessor :drill_down_enabled, :max_depth
25
+ # Expose current_level for testability
26
+ attr_accessor :current_level
27
27
 
28
28
  def add_submenu_item(item)
29
29
  validate_submenu_item(item)
@@ -17,6 +17,9 @@ module Aidp
17
17
 
18
18
  class SelectionError < WorkflowError; end
19
19
 
20
+ # Expose for testability
21
+ attr_reader :prompt
22
+
20
23
  WORKFLOW_MODES = {
21
24
  simple: {
22
25
  name: "Simple Mode",
@@ -132,6 +135,9 @@ module Aidp
132
135
 
133
136
  # Formats workflow selection display
134
137
  class WorkflowFormatter
138
+ # Expose for testability
139
+ attr_reader :pastel
140
+
135
141
  def initialize
136
142
  @pastel = Pastel.new
137
143
  end
@@ -11,6 +11,9 @@ module Aidp
11
11
  class SpinnerHelper
12
12
  class SpinnerError < StandardError; end
13
13
 
14
+ # Expose for testability
15
+ attr_accessor :active_spinners
16
+
14
17
  def initialize
15
18
  @pastel = Pastel.new
16
19
  @active_spinners = []
@@ -16,6 +16,9 @@ module Aidp
16
16
 
17
17
  class ControlError < WorkflowError; end
18
18
 
19
+ # Expose for testability
20
+ attr_accessor :pause_time, :control_thread
21
+
19
22
  WORKFLOW_STATES = {
20
23
  running: "Running",
21
24
  paused: "Paused",
@@ -9,6 +9,9 @@ module Aidp
9
9
  class UserInterface
10
10
  include Aidp::MessageDisplay
11
11
 
12
+ # Expose for testability
13
+ attr_reader :auto_confirm_defaults, :show_help_automatically, :verbose_mode, :file_selection_enabled
14
+
12
15
  def initialize(prompt: TTY::Prompt.new)
13
16
  @input_history = []
14
17
  @file_selection_enabled = false