aidp 0.31.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 (52) 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 +394 -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 +128 -2
  21. data/lib/aidp/harness/provider_metrics.rb +5 -3
  22. data/lib/aidp/harness/runner.rb +0 -11
  23. data/lib/aidp/harness/test_runner.rb +179 -41
  24. data/lib/aidp/harness/thinking_depth_manager.rb +16 -0
  25. data/lib/aidp/harness/ui/navigation/submenu.rb +0 -2
  26. data/lib/aidp/loader.rb +195 -0
  27. data/lib/aidp/metadata/compiler.rb +29 -17
  28. data/lib/aidp/metadata/query.rb +1 -1
  29. data/lib/aidp/metadata/scanner.rb +8 -1
  30. data/lib/aidp/metadata/tool_metadata.rb +13 -13
  31. data/lib/aidp/metadata/validator.rb +10 -0
  32. data/lib/aidp/metadata.rb +16 -0
  33. data/lib/aidp/pr_worktree_manager.rb +582 -0
  34. data/lib/aidp/provider_manager.rb +1 -7
  35. data/lib/aidp/setup/wizard.rb +279 -9
  36. data/lib/aidp/skills.rb +0 -5
  37. data/lib/aidp/storage/csv_storage.rb +3 -0
  38. data/lib/aidp/style_guide/selector.rb +360 -0
  39. data/lib/aidp/tooling_detector.rb +283 -16
  40. data/lib/aidp/util.rb +11 -0
  41. data/lib/aidp/version.rb +1 -1
  42. data/lib/aidp/watch/change_request_processor.rb +152 -14
  43. data/lib/aidp/watch/repository_client.rb +41 -0
  44. data/lib/aidp/watch/runner.rb +29 -18
  45. data/lib/aidp/watch.rb +5 -7
  46. data/lib/aidp/workstream_cleanup.rb +0 -2
  47. data/lib/aidp/workstream_executor.rb +0 -4
  48. data/lib/aidp/worktree.rb +0 -1
  49. data/lib/aidp/worktree_branch_manager.rb +70 -1
  50. data/lib/aidp.rb +21 -106
  51. metadata +73 -36
  52. data/lib/aidp/config/paths.rb +0 -131
@@ -7,8 +7,10 @@ require_relative "guard_policy"
7
7
  require_relative "work_loop_unit_scheduler"
8
8
  require_relative "deterministic_unit"
9
9
  require_relative "agent_signal_parser"
10
+ require_relative "steps"
10
11
  require_relative "../harness/test_runner"
11
12
  require_relative "../errors"
13
+ require_relative "../style_guide/selector"
12
14
 
13
15
  module Aidp
14
16
  module Execute
@@ -53,6 +55,7 @@ module Aidp
53
55
  @checkpoint = Checkpoint.new(project_dir)
54
56
  @checkpoint_display = CheckpointDisplay.new(prompt: @prompt)
55
57
  @guard_policy = GuardPolicy.new(project_dir, config.guards_config)
58
+ @work_context = {}
56
59
  @persistent_tasklist = PersistentTasklist.new(project_dir)
57
60
  @iteration_count = 0
58
61
  @step_name = nil
@@ -67,6 +70,9 @@ module Aidp
67
70
  @thinking_depth_manager = options[:thinking_depth_manager] || Aidp::Harness::ThinkingDepthManager.new(config)
68
71
  @consecutive_failures = 0
69
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)
70
76
  end
71
77
 
72
78
  # Execute a step using fix-forward work loop pattern
@@ -74,6 +80,7 @@ module Aidp
74
80
  # Never rolls back - only moves forward through fixes
75
81
  def execute_step(step_name, step_spec, context = {})
76
82
  @step_name = step_name
83
+ @work_context = context
77
84
  @iteration_count = 0
78
85
  transition_to(:ready)
79
86
 
@@ -81,6 +88,7 @@ module Aidp
81
88
 
82
89
  display_message("🔄 Starting hybrid work loop for step: #{step_name}", type: :info)
83
90
  display_message(" Flow: Deterministic ↔ Agentic with fix-forward core", type: :info)
91
+ display_work_context(step_name, context)
84
92
 
85
93
  display_guard_policy_status
86
94
  display_pending_tasks
@@ -159,9 +167,20 @@ module Aidp
159
167
 
160
168
  transition_to(:apply_patch)
161
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
+
162
181
  # Wrap agent call in exception handling for true fix-forward
163
182
  begin
164
- agent_result = apply_patch
183
+ agent_result = apply_patch(preview_provider, preview_model)
165
184
  rescue Aidp::Errors::ConfigurationError
166
185
  # Configuration errors should crash immediately (crash-early principle)
167
186
  # Re-raise without catching
@@ -256,6 +275,12 @@ module Aidp
256
275
  display_task_summary
257
276
  display_message("✅ Step #{@step_name} completed after #{@iteration_count} iterations", type: :success)
258
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")
259
284
  archive_and_cleanup
260
285
 
261
286
  return build_agentic_payload(
@@ -270,6 +295,12 @@ module Aidp
270
295
  display_message(" All checks passed but tasks not complete", type: :warning)
271
296
  display_message(" #{task_completion_result[:message]}", type: :warning)
272
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")
273
304
  transition_to(:next_patch)
274
305
 
275
306
  # Append task completion requirement to PROMPT.md
@@ -277,6 +308,11 @@ module Aidp
277
308
  end
278
309
  else
279
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)
280
316
  transition_to(:next_patch)
281
317
  end
282
318
  else
@@ -287,6 +323,12 @@ module Aidp
287
323
  diagnostic = diagnose_failures(all_results)
288
324
 
289
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))
290
332
  prepare_next_iteration(all_results, diagnostic)
291
333
  end
292
334
  end
@@ -497,8 +539,8 @@ module Aidp
497
539
  end
498
540
 
499
541
  # Apply patch - send PROMPT.md to agent
500
- def apply_patch
501
- 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)
502
544
  end
503
545
 
504
546
  # Check if agent marked work complete
@@ -547,7 +589,9 @@ module Aidp
547
589
  # Traditional prompt building (fallback or when optimization disabled)
548
590
  template_content = load_template(step_spec["templates"]&.first)
549
591
  prd_content = load_prd
550
- 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)
551
595
  user_input = format_user_input(context[:user_input])
552
596
  deterministic_outputs = Array(context[:deterministic_outputs])
553
597
  previous_summary = context[:previous_agent_summary]
@@ -727,7 +771,7 @@ module Aidp
727
771
  parts.join("\n")
728
772
  end
729
773
 
730
- def send_to_agent
774
+ def send_to_agent(selected_provider: nil, selected_model: nil)
731
775
  prompt_content = @prompt_manager.read
732
776
  return {status: "error", message: "PROMPT.md not found"} unless prompt_content
733
777
 
@@ -735,9 +779,11 @@ module Aidp
735
779
  full_prompt = build_work_loop_header(@step_name, @iteration_count) + "\n\n" + prompt_content
736
780
 
737
781
  # Select model based on thinking depth tier
738
- 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?
739
785
 
740
- if provider_name.nil? || model_name.nil?
786
+ if provider_name.nil?
741
787
  Aidp.logger.error("work_loop", "Failed to select model for tier",
742
788
  tier: @thinking_depth_manager.current_tier,
743
789
  step: @step_name,
@@ -748,7 +794,8 @@ module Aidp
748
794
  # Log model selection
749
795
  tier = @thinking_depth_manager.current_tier
750
796
  if @last_tier != tier
751
- 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)
752
799
  @last_tier = tier
753
800
  end
754
801
 
@@ -770,6 +817,164 @@ module Aidp
770
817
  end
771
818
  end
772
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
+
773
978
  def build_work_loop_header(step_name, iteration)
774
979
  parts = []
775
980
  parts << "# Work Loop: #{step_name} (Iteration #{iteration})"
@@ -830,6 +1035,24 @@ module Aidp
830
1035
  parts.join("\n")
831
1036
  end
832
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
+
833
1056
  def prompt_marked_complete?
834
1057
  prompt_content = @prompt_manager.read
835
1058
  return false unless prompt_content
@@ -936,30 +1159,50 @@ module Aidp
936
1159
 
937
1160
  # Check if we should reinject the style guide at this iteration
938
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
+
939
1166
  # Reinject on intervals (5, 10, 15, etc.) but not on iteration 1
940
1167
  @iteration_count > 1 && (@iteration_count % STYLE_GUIDE_REMINDER_INTERVAL == 0)
941
1168
  end
942
1169
 
943
1170
  # Create style guide reminder text
944
1171
  def reinject_style_guide_reminder
945
- 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
+
946
1182
  template_content = load_current_template
947
1183
 
1184
+ # Use provider-aware style guide loading with context-based section selection
1185
+ style_guide = load_style_guide_for_provider(@work_context)
1186
+
948
1187
  reminder = []
949
1188
  reminder << "### 🔄 Style Guide & Template Reminder (Iteration #{@iteration_count})"
950
1189
  reminder << ""
951
1190
  reminder << "**IMPORTANT**: To prevent drift from project conventions, please review:"
952
1191
  reminder << ""
953
1192
 
954
- if style_guide
955
- reminder << "#### LLM Style Guide"
956
- reminder << "```"
957
- # Include first 1000 chars of style guide to keep context manageable
958
- 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
959
1202
  reminder << style_guide_preview
960
1203
  reminder << "```"
961
1204
  reminder << ""
962
- 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)
963
1206
  end
964
1207
 
965
1208
  if template_content
@@ -1049,6 +1292,74 @@ module Aidp
1049
1292
  File.exist?(style_guide_path) ? File.read(style_guide_path) : nil
1050
1293
  end
1051
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
+
1052
1363
  def format_user_input(user_input)
1053
1364
  return nil if user_input.nil? || user_input.empty?
1054
1365
 
@@ -1286,6 +1597,74 @@ module Aidp
1286
1597
  display_message("")
1287
1598
  end
1288
1599
 
1600
+ # Show watch-mode context (issue/PR, step position) to improve situational awareness
1601
+ def display_work_context(step_name, context)
1602
+ parts = work_context_parts(step_name, context)
1603
+ return if parts.empty?
1604
+
1605
+ Aidp.log_debug("work_loop", "work_context", step: step_name, parts: parts)
1606
+ display_message(" 📡 Context: #{parts.join(" | ")}", type: :info)
1607
+ end
1608
+
1609
+ def work_context_parts(step_name, context)
1610
+ ctx = (@options || {}).merge(context || {})
1611
+ parts = []
1612
+
1613
+ if (step_label = step_position_label(step_name, ctx))
1614
+ parts << step_label
1615
+ end
1616
+
1617
+ if (issue_label = issue_context_label(ctx))
1618
+ parts << issue_label
1619
+ end
1620
+
1621
+ if (pr_label = pr_context_label(ctx))
1622
+ parts << pr_label
1623
+ end
1624
+
1625
+ parts << "Watch mode" if ctx[:workflow_type].to_s == "watch_mode"
1626
+
1627
+ parts.compact
1628
+ end
1629
+
1630
+ def step_position_label(step_name, context)
1631
+ steps = Array(context[:selected_steps]).map(&:to_s)
1632
+ steps = Aidp::Execute::Steps::SPEC.keys if steps.empty?
1633
+ steps = [step_name] if steps.empty?
1634
+ steps << step_name unless steps.include?(step_name)
1635
+
1636
+ index = steps.index(step_name)
1637
+ return nil unless index
1638
+
1639
+ "Step #{index + 1}/#{steps.size} (#{step_name})"
1640
+ end
1641
+
1642
+ def issue_context_label(context)
1643
+ issue_number = context[:issue_number] ||
1644
+ context.dig(:issue, :number) ||
1645
+ extract_number_from_url(context[:issue_url] || context.dig(:issue, :url) || context.dig(:user_input, "Issue URL"), /issues\/(\d+)/)
1646
+
1647
+ return nil unless issue_number
1648
+
1649
+ "Issue ##{issue_number}"
1650
+ end
1651
+
1652
+ def pr_context_label(context)
1653
+ pr_number = context[:pr_number] ||
1654
+ context.dig(:pull_request, :number) ||
1655
+ extract_number_from_url(context[:pr_url] || context.dig(:pull_request, :url) || context.dig(:user_input, "PR URL") || context.dig(:user_input, "Pull Request URL"), /pull\/(\d+)/)
1656
+
1657
+ return nil unless pr_number
1658
+
1659
+ "PR ##{pr_number}"
1660
+ end
1661
+
1662
+ def extract_number_from_url(url, pattern)
1663
+ return nil unless url
1664
+ match = url.to_s.match(pattern)
1665
+ match && match[1]
1666
+ end
1667
+
1289
1668
  # Append task completion requirement to PROMPT.md
1290
1669
  def append_task_requirement_to_prompt(message)
1291
1670
  task_requirement = []