aidp 0.32.0 → 0.33.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 +4 -4
  2. data/lib/aidp/analyze/feature_analyzer.rb +322 -320
  3. data/lib/aidp/auto_update/coordinator.rb +97 -7
  4. data/lib/aidp/auto_update.rb +0 -12
  5. data/lib/aidp/cli/devcontainer_commands.rb +0 -5
  6. data/lib/aidp/cli.rb +2 -1
  7. data/lib/aidp/comment_consolidator.rb +78 -0
  8. data/lib/aidp/concurrency.rb +0 -3
  9. data/lib/aidp/config.rb +0 -1
  10. data/lib/aidp/config_paths.rb +71 -0
  11. data/lib/aidp/execute/work_loop_runner.rb +324 -15
  12. data/lib/aidp/harness/ai_filter_factory.rb +285 -0
  13. data/lib/aidp/harness/config_schema.rb +97 -1
  14. data/lib/aidp/harness/config_validator.rb +1 -1
  15. data/lib/aidp/harness/configuration.rb +61 -5
  16. data/lib/aidp/harness/filter_definition.rb +212 -0
  17. data/lib/aidp/harness/generated_filter_strategy.rb +197 -0
  18. data/lib/aidp/harness/output_filter.rb +50 -25
  19. data/lib/aidp/harness/output_filter_config.rb +129 -0
  20. data/lib/aidp/harness/provider_manager.rb +90 -2
  21. data/lib/aidp/harness/runner.rb +0 -11
  22. data/lib/aidp/harness/test_runner.rb +179 -41
  23. data/lib/aidp/harness/thinking_depth_manager.rb +16 -0
  24. data/lib/aidp/harness/ui/navigation/submenu.rb +0 -2
  25. data/lib/aidp/loader.rb +195 -0
  26. data/lib/aidp/metadata/compiler.rb +29 -17
  27. data/lib/aidp/metadata/query.rb +1 -1
  28. data/lib/aidp/metadata/scanner.rb +8 -1
  29. data/lib/aidp/metadata/tool_metadata.rb +13 -13
  30. data/lib/aidp/metadata/validator.rb +10 -0
  31. data/lib/aidp/metadata.rb +16 -0
  32. data/lib/aidp/pr_worktree_manager.rb +2 -2
  33. data/lib/aidp/provider_manager.rb +1 -7
  34. data/lib/aidp/setup/wizard.rb +279 -9
  35. data/lib/aidp/skills.rb +0 -5
  36. data/lib/aidp/storage/csv_storage.rb +3 -0
  37. data/lib/aidp/style_guide/selector.rb +360 -0
  38. data/lib/aidp/tooling_detector.rb +283 -16
  39. data/lib/aidp/version.rb +1 -1
  40. data/lib/aidp/watch/change_request_processor.rb +152 -14
  41. data/lib/aidp/watch/repository_client.rb +41 -0
  42. data/lib/aidp/watch/runner.rb +29 -18
  43. data/lib/aidp/watch.rb +5 -7
  44. data/lib/aidp/workstream_cleanup.rb +0 -2
  45. data/lib/aidp/workstream_executor.rb +0 -4
  46. data/lib/aidp/worktree.rb +0 -1
  47. data/lib/aidp.rb +21 -106
  48. metadata +72 -36
  49. data/lib/aidp/config/paths.rb +0 -131
@@ -10,6 +10,7 @@ require_relative "agent_signal_parser"
10
10
  require_relative "steps"
11
11
  require_relative "../harness/test_runner"
12
12
  require_relative "../errors"
13
+ require_relative "../style_guide/selector"
13
14
 
14
15
  module Aidp
15
16
  module Execute
@@ -54,6 +55,7 @@ module Aidp
54
55
  @checkpoint = Checkpoint.new(project_dir)
55
56
  @checkpoint_display = CheckpointDisplay.new(prompt: @prompt)
56
57
  @guard_policy = GuardPolicy.new(project_dir, config.guards_config)
58
+ @work_context = {}
57
59
  @persistent_tasklist = PersistentTasklist.new(project_dir)
58
60
  @iteration_count = 0
59
61
  @step_name = nil
@@ -68,6 +70,9 @@ module Aidp
68
70
  @thinking_depth_manager = options[:thinking_depth_manager] || Aidp::Harness::ThinkingDepthManager.new(config)
69
71
  @consecutive_failures = 0
70
72
  @last_tier = nil
73
+
74
+ # Initialize style guide selector for intelligent section selection
75
+ @style_guide_selector = options[:style_guide_selector] || Aidp::StyleGuide::Selector.new(project_dir: project_dir)
71
76
  end
72
77
 
73
78
  # Execute a step using fix-forward work loop pattern
@@ -75,6 +80,7 @@ module Aidp
75
80
  # Never rolls back - only moves forward through fixes
76
81
  def execute_step(step_name, step_spec, context = {})
77
82
  @step_name = step_name
83
+ @work_context = context
78
84
  @iteration_count = 0
79
85
  transition_to(:ready)
80
86
 
@@ -161,9 +167,20 @@ module Aidp
161
167
 
162
168
  transition_to(:apply_patch)
163
169
 
170
+ # Preview provider/model selection and queued checks for this iteration
171
+ preview_provider, preview_model, _model_data = select_model_for_current_tier
172
+ prompt_length = @prompt_manager.read&.length || 0
173
+ checks_summary = planned_checks_summary
174
+ display_iteration_overview(preview_provider, preview_model, prompt_length, checks_summary)
175
+ log_iteration_status("running",
176
+ provider: preview_provider,
177
+ model: preview_model,
178
+ prompt_length: prompt_length,
179
+ checks: checks_summary)
180
+
164
181
  # Wrap agent call in exception handling for true fix-forward
165
182
  begin
166
- agent_result = apply_patch
183
+ agent_result = apply_patch(preview_provider, preview_model)
167
184
  rescue Aidp::Errors::ConfigurationError
168
185
  # Configuration errors should crash immediately (crash-early principle)
169
186
  # Re-raise without catching
@@ -258,6 +275,12 @@ module Aidp
258
275
  display_task_summary
259
276
  display_message("✅ Step #{@step_name} completed after #{@iteration_count} iterations", type: :success)
260
277
  display_state_summary
278
+ log_iteration_status("completed",
279
+ provider: preview_provider,
280
+ model: preview_model,
281
+ prompt_length: prompt_length,
282
+ checks: checks_summary,
283
+ task_status: "complete")
261
284
  archive_and_cleanup
262
285
 
263
286
  return build_agentic_payload(
@@ -272,6 +295,12 @@ module Aidp
272
295
  display_message(" All checks passed but tasks not complete", type: :warning)
273
296
  display_message(" #{task_completion_result[:message]}", type: :warning)
274
297
  display_task_summary
298
+ log_iteration_status("checks_passed_tasks_incomplete",
299
+ provider: preview_provider,
300
+ model: preview_model,
301
+ prompt_length: prompt_length,
302
+ checks: checks_summary,
303
+ task_status: "incomplete")
275
304
  transition_to(:next_patch)
276
305
 
277
306
  # Append task completion requirement to PROMPT.md
@@ -279,6 +308,11 @@ module Aidp
279
308
  end
280
309
  else
281
310
  display_message(" All checks passed but work not marked complete", type: :info)
311
+ log_iteration_status("checks_passed_waiting_agent_completion",
312
+ provider: preview_provider,
313
+ model: preview_model,
314
+ prompt_length: prompt_length,
315
+ checks: checks_summary)
282
316
  transition_to(:next_patch)
283
317
  end
284
318
  else
@@ -289,6 +323,12 @@ module Aidp
289
323
  diagnostic = diagnose_failures(all_results)
290
324
 
291
325
  transition_to(:next_patch)
326
+ log_iteration_status("checks_failed",
327
+ provider: preview_provider,
328
+ model: preview_model,
329
+ prompt_length: prompt_length,
330
+ checks: checks_summary,
331
+ failures: failure_summary_for_log(all_results))
292
332
  prepare_next_iteration(all_results, diagnostic)
293
333
  end
294
334
  end
@@ -499,8 +539,8 @@ module Aidp
499
539
  end
500
540
 
501
541
  # Apply patch - send PROMPT.md to agent
502
- def apply_patch
503
- send_to_agent
542
+ def apply_patch(selected_provider = nil, selected_model = nil)
543
+ send_to_agent(selected_provider: selected_provider, selected_model: selected_model)
504
544
  end
505
545
 
506
546
  # Check if agent marked work complete
@@ -549,7 +589,9 @@ module Aidp
549
589
  # Traditional prompt building (fallback or when optimization disabled)
550
590
  template_content = load_template(step_spec["templates"]&.first)
551
591
  prd_content = load_prd
552
- style_guide = load_style_guide
592
+ # Use provider-aware style guide loading - skips for Claude/Copilot,
593
+ # selects relevant STYLE_GUIDE sections for other providers
594
+ style_guide = load_style_guide_for_provider(context)
553
595
  user_input = format_user_input(context[:user_input])
554
596
  deterministic_outputs = Array(context[:deterministic_outputs])
555
597
  previous_summary = context[:previous_agent_summary]
@@ -729,7 +771,7 @@ module Aidp
729
771
  parts.join("\n")
730
772
  end
731
773
 
732
- def send_to_agent
774
+ def send_to_agent(selected_provider: nil, selected_model: nil)
733
775
  prompt_content = @prompt_manager.read
734
776
  return {status: "error", message: "PROMPT.md not found"} unless prompt_content
735
777
 
@@ -737,9 +779,11 @@ module Aidp
737
779
  full_prompt = build_work_loop_header(@step_name, @iteration_count) + "\n\n" + prompt_content
738
780
 
739
781
  # Select model based on thinking depth tier
740
- provider_name, model_name, _model_data = select_model_for_current_tier
782
+ provider_name = selected_provider
783
+ model_name = selected_model
784
+ provider_name, model_name, _model_data = select_model_for_current_tier if provider_name.nil? || model_name.nil?
741
785
 
742
- if provider_name.nil? || model_name.nil?
786
+ if provider_name.nil?
743
787
  Aidp.logger.error("work_loop", "Failed to select model for tier",
744
788
  tier: @thinking_depth_manager.current_tier,
745
789
  step: @step_name,
@@ -750,7 +794,8 @@ module Aidp
750
794
  # Log model selection
751
795
  tier = @thinking_depth_manager.current_tier
752
796
  if @last_tier != tier
753
- display_message(" 💡 Using tier: #{tier} (#{provider_name}/#{model_name})", type: :info)
797
+ model_label = model_name || "auto"
798
+ display_message(" 💡 Using tier: #{tier} (#{provider_name}/#{model_label})", type: :info)
754
799
  @last_tier = tier
755
800
  end
756
801
 
@@ -772,6 +817,164 @@ module Aidp
772
817
  end
773
818
  end
774
819
 
820
+ def display_iteration_overview(provider_name, model_name, prompt_length, checks_summary = nil)
821
+ tier = @thinking_depth_manager.current_tier
822
+ checks = checks_summary
823
+ checks ||= summarize_checks(@test_runner.planned_commands) if @test_runner.respond_to?(:planned_commands)
824
+ model_label = model_name || "auto"
825
+ context_labels = iteration_context_labels
826
+
827
+ display_message(" • Step: #{@step_name} | Tier: #{tier} | Model: #{provider_name}/#{model_label}", type: :info)
828
+ display_message(" • Prompt size: #{prompt_length} chars | State: #{STATES[@current_state]}", type: :info)
829
+ display_message(" • Upcoming checks: #{checks}", type: :info) if checks && !checks.empty?
830
+ display_message(" • Context: #{context_labels.join(" | ")}", type: :info) if context_labels.any?
831
+
832
+ # Display output filtering configuration if enabled
833
+ filtering_info = summarize_output_filtering
834
+ display_message(" • Output filtering: #{filtering_info}", type: :info) if filtering_info
835
+ end
836
+
837
+ # Summarize output filtering configuration
838
+ def summarize_output_filtering
839
+ return nil unless @config.respond_to?(:output_filtering_enabled?) && @config.output_filtering_enabled?
840
+
841
+ iteration = @test_runner.respond_to?(:iteration_count) ? @test_runner.iteration_count : 0
842
+
843
+ test_mode = if @config.respond_to?(:test_output_mode)
844
+ @config.test_output_mode
845
+ elsif iteration > 1
846
+ :failures_only
847
+ else
848
+ :full
849
+ end
850
+
851
+ lint_mode = if @config.respond_to?(:lint_output_mode)
852
+ @config.lint_output_mode
853
+ elsif iteration > 1
854
+ :failures_only
855
+ else
856
+ :full
857
+ end
858
+
859
+ if test_mode == :full && lint_mode == :full
860
+ nil # Don't show message when no filtering is active
861
+ else
862
+ "test=#{test_mode}, lint=#{lint_mode}"
863
+ end
864
+ rescue
865
+ nil
866
+ end
867
+
868
+ # Display output filtering statistics after test/lint runs
869
+ def display_filtering_stats
870
+ return unless @test_runner.respond_to?(:filter_stats)
871
+
872
+ stats = @test_runner.filter_stats
873
+ return if stats[:total_input_bytes].zero?
874
+
875
+ reduction = ((stats[:total_input_bytes] - stats[:total_output_bytes]).to_f / stats[:total_input_bytes] * 100).round(1)
876
+ return if reduction <= 0
877
+
878
+ display_message(" 📉 Token optimization: #{reduction}% reduction " \
879
+ "(#{format_bytes(stats[:total_input_bytes])} → #{format_bytes(stats[:total_output_bytes])})", type: :info)
880
+ rescue
881
+ # Silently ignore errors in stats display
882
+ end
883
+
884
+ def format_bytes(bytes)
885
+ if bytes >= 1024 * 1024
886
+ "#{(bytes / 1024.0 / 1024.0).round(1)}MB"
887
+ elsif bytes >= 1024
888
+ "#{(bytes / 1024.0).round(1)}KB"
889
+ else
890
+ "#{bytes}B"
891
+ end
892
+ end
893
+
894
+ def summarize_checks(planned)
895
+ labels = {
896
+ tests: "tests",
897
+ lints: "linters",
898
+ formatters: "formatters",
899
+ builds: "builds",
900
+ docs: "docs"
901
+ }
902
+
903
+ summaries = planned.map do |category, commands|
904
+ count = Array(commands).size
905
+ next if count.zero?
906
+
907
+ label = labels[category] || category.to_s
908
+ cmd_names = Array(commands).map do |cmd|
909
+ cmd.is_a?(Hash) ? cmd[:command] : cmd
910
+ end
911
+
912
+ if cmd_names.size <= 2
913
+ "#{label} (#{cmd_names.join(", ")})"
914
+ else
915
+ "#{label} (#{cmd_names.first(2).join(", ")} +#{cmd_names.size - 2} more)"
916
+ end
917
+ end.compact
918
+
919
+ summaries.join(" | ")
920
+ rescue => e
921
+ Aidp.log_warn("work_loop", "summarize_checks_failed", error: e.message)
922
+ nil
923
+ end
924
+
925
+ def planned_checks_summary
926
+ return nil unless @test_runner.respond_to?(:planned_commands)
927
+
928
+ summarize_checks(@test_runner.planned_commands)
929
+ end
930
+
931
+ def failure_summary_for_log(all_results)
932
+ Array(all_results).each_with_object([]) do |(category, results), summary|
933
+ next if results[:success]
934
+
935
+ failures = results[:required_failures] || results[:failures] || []
936
+ count = failures.size
937
+ commands = Array(failures).map { |f| f[:command] }.compact
938
+
939
+ summary << if commands.any?
940
+ "#{category}: #{count} (#{commands.first(2).join(", ")})"
941
+ else
942
+ "#{category}: #{count}"
943
+ end
944
+ end
945
+ rescue => e
946
+ Aidp.log_warn("work_loop", "failure_summary_for_log_failed", error: e.message)
947
+ []
948
+ end
949
+
950
+ def log_iteration_status(status, provider:, model:, prompt_length:, checks: nil, failures: nil, task_status: nil)
951
+ context_labels = iteration_context_labels
952
+ metadata = {
953
+ step: @step_name,
954
+ iteration: @iteration_count,
955
+ state: STATES[@current_state],
956
+ tier: @thinking_depth_manager.current_tier,
957
+ provider: provider,
958
+ model: model,
959
+ prompt_length: prompt_length,
960
+ checks: checks,
961
+ failures: failures,
962
+ task_status: task_status
963
+ }
964
+
965
+ metadata.merge!(iteration_context_metadata)
966
+ metadata.delete_if { |_, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
967
+
968
+ message = "Iteration #{@iteration_count} for #{@step_name}: #{status}"
969
+ message += " | #{context_labels.join(" | ")}" if context_labels.any?
970
+
971
+ Aidp.log_info("work_loop_iteration",
972
+ message,
973
+ **metadata)
974
+ rescue => e
975
+ Aidp.log_warn("work_loop", "failed_to_log_iteration_status", error: e.message)
976
+ end
977
+
775
978
  def build_work_loop_header(step_name, iteration)
776
979
  parts = []
777
980
  parts << "# Work Loop: #{step_name} (Iteration #{iteration})"
@@ -832,6 +1035,24 @@ module Aidp
832
1035
  parts.join("\n")
833
1036
  end
834
1037
 
1038
+ def iteration_context_metadata
1039
+ ctx = (@options || {}).merge(@work_context || {})
1040
+ {
1041
+ issue: issue_context_label(ctx),
1042
+ pr: pr_context_label(ctx),
1043
+ step_position: step_position_label(@step_name, ctx)
1044
+ }.compact
1045
+ end
1046
+
1047
+ def iteration_context_labels
1048
+ meta = iteration_context_metadata
1049
+ labels = []
1050
+ labels << meta[:issue] if meta[:issue]
1051
+ labels << meta[:pr] if meta[:pr]
1052
+ labels << meta[:step_position] if meta[:step_position]
1053
+ labels
1054
+ end
1055
+
835
1056
  def prompt_marked_complete?
836
1057
  prompt_content = @prompt_manager.read
837
1058
  return false unless prompt_content
@@ -938,30 +1159,50 @@ module Aidp
938
1159
 
939
1160
  # Check if we should reinject the style guide at this iteration
940
1161
  def should_reinject_style_guide?
1162
+ # Skip reinjection for providers with instruction files (Claude, GitHub Copilot)
1163
+ current_provider = @provider_manager&.current_provider
1164
+ return false unless @style_guide_selector.provider_needs_style_guide?(current_provider)
1165
+
941
1166
  # Reinject on intervals (5, 10, 15, etc.) but not on iteration 1
942
1167
  @iteration_count > 1 && (@iteration_count % STYLE_GUIDE_REMINDER_INTERVAL == 0)
943
1168
  end
944
1169
 
945
1170
  # Create style guide reminder text
946
1171
  def reinject_style_guide_reminder
947
- style_guide = load_style_guide
1172
+ current_provider = @provider_manager&.current_provider
1173
+
1174
+ # Skip for providers with instruction files
1175
+ unless @style_guide_selector.provider_needs_style_guide?(current_provider)
1176
+ Aidp.log_debug("work_loop", "skipping_style_guide_reminder",
1177
+ provider: current_provider,
1178
+ reason: "provider has instruction file")
1179
+ return ""
1180
+ end
1181
+
948
1182
  template_content = load_current_template
949
1183
 
1184
+ # Use provider-aware style guide loading with context-based section selection
1185
+ style_guide = load_style_guide_for_provider(@work_context)
1186
+
950
1187
  reminder = []
951
1188
  reminder << "### 🔄 Style Guide & Template Reminder (Iteration #{@iteration_count})"
952
1189
  reminder << ""
953
1190
  reminder << "**IMPORTANT**: To prevent drift from project conventions, please review:"
954
1191
  reminder << ""
955
1192
 
956
- if style_guide
957
- reminder << "#### LLM Style Guide"
958
- reminder << "```"
959
- # Include first 1000 chars of style guide to keep context manageable
960
- style_guide_preview = (style_guide.length > 1000) ? style_guide[0...1000] + "\n...(truncated)" : style_guide
1193
+ if style_guide && !style_guide.empty?
1194
+ reminder << "#### Relevant Style Guide Sections"
1195
+ reminder << "```markdown"
1196
+ # Include selected sections (already limited by selector)
1197
+ style_guide_preview = if style_guide.length > 2000
1198
+ style_guide[0...2000] + "\n...(truncated)"
1199
+ else
1200
+ style_guide
1201
+ end
961
1202
  reminder << style_guide_preview
962
1203
  reminder << "```"
963
1204
  reminder << ""
964
- display_message(" [STYLE_GUIDE] Re-injecting LLM_STYLE_GUIDE at iteration #{@iteration_count}", type: :info)
1205
+ display_message(" [STYLE_GUIDE] Re-injecting selected STYLE_GUIDE sections at iteration #{@iteration_count}", type: :info)
965
1206
  end
966
1207
 
967
1208
  if template_content
@@ -1051,6 +1292,74 @@ module Aidp
1051
1292
  File.exist?(style_guide_path) ? File.read(style_guide_path) : nil
1052
1293
  end
1053
1294
 
1295
+ # Load style guide content appropriate for the current provider and context
1296
+ # Returns nil for providers with instruction files (Claude, GitHub Copilot)
1297
+ # Returns selected STYLE_GUIDE sections for other providers
1298
+ #
1299
+ # @param context [Hash] Task context for keyword extraction
1300
+ # @return [String, nil] Style guide content or nil if not needed
1301
+ def load_style_guide_for_provider(context = {})
1302
+ current_provider = @provider_manager&.current_provider
1303
+
1304
+ # Skip style guide for providers with their own instruction files
1305
+ unless @style_guide_selector.provider_needs_style_guide?(current_provider)
1306
+ Aidp.log_debug("work_loop", "skipping_style_guide",
1307
+ provider: current_provider,
1308
+ reason: "provider has instruction file")
1309
+ return nil
1310
+ end
1311
+
1312
+ # Extract keywords from context for intelligent section selection
1313
+ keywords = extract_style_guide_keywords(context)
1314
+
1315
+ # Select relevant sections from STYLE_GUIDE.md
1316
+ content = @style_guide_selector.select_sections(
1317
+ keywords: keywords,
1318
+ include_core: true,
1319
+ max_lines: 500 # Limit to keep prompt size manageable
1320
+ )
1321
+
1322
+ return nil if content.nil? || content.empty?
1323
+
1324
+ Aidp.log_debug("work_loop", "style_guide_selected",
1325
+ provider: current_provider,
1326
+ keywords: keywords,
1327
+ content_lines: content.lines.count)
1328
+
1329
+ content
1330
+ end
1331
+
1332
+ # Extract keywords from task context for style guide section selection
1333
+ #
1334
+ # @param context [Hash] Task context
1335
+ # @return [Array<String>] Keywords for section selection
1336
+ def extract_style_guide_keywords(context)
1337
+ keywords = []
1338
+
1339
+ # Extract from step name
1340
+ step_lower = @step_name.to_s.downcase
1341
+ keywords << "testing" if step_lower.include?("test")
1342
+ keywords << "implementation" if step_lower.include?("implement")
1343
+ keywords << "refactor" if step_lower.include?("refactor")
1344
+
1345
+ # Extract from user input
1346
+ user_input = context[:user_input]
1347
+ if user_input.is_a?(Hash)
1348
+ keywords.concat(@style_guide_selector.extract_keywords(user_input.values.join(" ")))
1349
+ elsif user_input.is_a?(String)
1350
+ keywords.concat(@style_guide_selector.extract_keywords(user_input))
1351
+ end
1352
+
1353
+ # Extract from affected files
1354
+ affected_files = context[:affected_files] || []
1355
+ affected_files.each do |file|
1356
+ keywords << "testing" if file.include?("spec") || file.include?("test")
1357
+ keywords << "tty" if file.include?("cli") || file.include?("tui")
1358
+ end
1359
+
1360
+ keywords.uniq
1361
+ end
1362
+
1054
1363
  def format_user_input(user_input)
1055
1364
  return nil if user_input.nil? || user_input.empty?
1056
1365