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
@@ -3,23 +3,108 @@
3
3
  module Aidp
4
4
  # Detect basic project tooling to seed work loop test & lint commands.
5
5
  # Lightweight heuristic pass – prefers safety over guessing incorrectly.
6
+ # Provides framework-aware command suggestions with optimal flags for output filtering.
6
7
  class ToolingDetector
7
8
  DETECTORS = [
8
9
  :ruby_bundle,
9
10
  :rspec,
11
+ :minitest,
10
12
  :ruby_standardrb,
11
13
  :node_jest,
12
14
  :node_mocha,
13
15
  :node_eslint,
14
- :python_pytest
16
+ :python_pytest,
17
+ :python_ruff
15
18
  ].freeze
16
19
 
17
- Result = Struct.new(:test_commands, :lint_commands, keyword_init: true)
20
+ # Framework identifiers for output filtering
21
+ FRAMEWORKS = {
22
+ rspec: :rspec,
23
+ minitest: :minitest,
24
+ jest: :jest,
25
+ mocha: :jest, # Mocha uses similar output format
26
+ pytest: :pytest
27
+ }.freeze
28
+
29
+ # Enhanced result with framework information
30
+ Result = Struct.new(:test_commands, :lint_commands, :formatter_commands,
31
+ :frameworks, keyword_init: true) do
32
+ # Get test commands with their detected framework
33
+ def test_command_frameworks
34
+ @test_command_frameworks ||= {}
35
+ end
36
+
37
+ # Get the framework for a specific command
38
+ def framework_for_command(command)
39
+ test_command_frameworks[command] || :unknown
40
+ end
41
+ end
42
+
43
+ # Information about a detected command
44
+ CommandInfo = Struct.new(:command, :framework, :flags, keyword_init: true)
18
45
 
19
46
  def self.detect(root = Dir.pwd)
20
47
  new(root).detect
21
48
  end
22
49
 
50
+ # Detect framework from a command string
51
+ # @param command [String] Command to analyze
52
+ # @return [Symbol] Framework identifier (:rspec, :minitest, :jest, :pytest, :unknown)
53
+ def self.framework_from_command(command)
54
+ return :unknown unless command.is_a?(String)
55
+
56
+ case command.downcase
57
+ when /\brspec\b/
58
+ :rspec
59
+ when /\bminitest\b/, /\bruby.*test/, /\brake test\b/
60
+ :minitest
61
+ when /\bjest\b/, /\bmocha\b/
62
+ :jest
63
+ when /\bpytest\b/
64
+ :pytest
65
+ else
66
+ :unknown
67
+ end
68
+ end
69
+
70
+ # Get recommended command flags for better output filtering
71
+ # @param framework [Symbol] Framework identifier
72
+ # @return [Hash] Recommended flags for different verbosity modes
73
+ def self.recommended_flags(framework)
74
+ case framework
75
+ when :rspec
76
+ {
77
+ standard: "--format progress",
78
+ verbose: "--format documentation",
79
+ failures_only: "--format failures --format progress"
80
+ }
81
+ when :minitest
82
+ {
83
+ standard: "",
84
+ verbose: "-v",
85
+ failures_only: ""
86
+ }
87
+ when :jest
88
+ {
89
+ standard: "",
90
+ verbose: "--verbose",
91
+ failures_only: "--reporters=default --silent=false"
92
+ }
93
+ when :pytest
94
+ {
95
+ standard: "-q",
96
+ verbose: "-v",
97
+ failures_only: "-q --tb=short"
98
+ }
99
+ else
100
+ {
101
+ standard: "",
102
+ verbose: "",
103
+ failures_only: ""
104
+ }
105
+ end
106
+ end
107
+
23
108
  def initialize(root = Dir.pwd)
24
109
  @root = root
25
110
  end
@@ -27,34 +112,143 @@ module Aidp
27
112
  def detect
28
113
  tests = []
29
114
  linters = []
115
+ formatters = []
116
+ frameworks = {}
117
+
118
+ detect_ruby_tools(tests, linters, formatters, frameworks)
119
+ detect_node_tools(tests, linters, formatters, frameworks)
120
+ detect_python_tools(tests, linters, formatters, frameworks)
121
+
122
+ result = Result.new(
123
+ test_commands: tests.uniq,
124
+ lint_commands: linters.uniq,
125
+ formatter_commands: formatters.uniq,
126
+ frameworks: frameworks
127
+ )
128
+
129
+ # Store framework mappings in the result
130
+ frameworks.each do |cmd, framework|
131
+ result.test_command_frameworks[cmd] = framework
132
+ end
133
+
134
+ result
135
+ end
136
+
137
+ # Get detailed command information including framework and suggested flags
138
+ # @return [Array<CommandInfo>] Detailed information about detected commands
139
+ def detect_with_details
140
+ commands = []
30
141
 
31
142
  if ruby_project?
32
- tests << bundle_prefix("rspec") if rspec?
33
- linters << bundle_prefix("standardrb") if standard_rb?
143
+ if rspec?
144
+ commands << CommandInfo.new(
145
+ command: bundle_prefix("rspec"),
146
+ framework: :rspec,
147
+ flags: self.class.recommended_flags(:rspec)
148
+ )
149
+ end
150
+
151
+ if minitest?
152
+ commands << CommandInfo.new(
153
+ command: bundle_prefix("ruby -Itest test"),
154
+ framework: :minitest,
155
+ flags: self.class.recommended_flags(:minitest)
156
+ )
157
+ end
34
158
  end
35
159
 
36
160
  if node_project?
37
- tests << npm_or_yarn("test") if package_script?("test")
38
- %w[lint eslint].each do |script|
39
- if package_script?(script)
40
- linters << npm_or_yarn(script)
41
- break
42
- end
161
+ if jest?
162
+ commands << CommandInfo.new(
163
+ command: npm_or_yarn("test"),
164
+ framework: :jest,
165
+ flags: self.class.recommended_flags(:jest)
166
+ )
43
167
  end
44
168
  end
45
169
 
46
170
  if python_pytest?
47
- tests << "pytest -q"
171
+ commands << CommandInfo.new(
172
+ command: "pytest",
173
+ framework: :pytest,
174
+ flags: self.class.recommended_flags(:pytest)
175
+ )
48
176
  end
49
177
 
50
- Result.new(
51
- test_commands: tests.uniq,
52
- lint_commands: linters.uniq
53
- )
178
+ commands
54
179
  end
55
180
 
56
181
  private
57
182
 
183
+ def detect_ruby_tools(tests, linters, formatters, frameworks)
184
+ return unless ruby_project?
185
+
186
+ if rspec?
187
+ cmd = bundle_prefix("rspec")
188
+ tests << cmd
189
+ frameworks[cmd] = :rspec
190
+ end
191
+
192
+ if minitest?
193
+ cmd = bundle_prefix("ruby -Itest test")
194
+ tests << cmd
195
+ frameworks[cmd] = :minitest
196
+ end
197
+
198
+ if standard_rb?
199
+ linters << bundle_prefix("standardrb")
200
+ formatters << bundle_prefix("standardrb --fix")
201
+ end
202
+
203
+ if rubocop?
204
+ linters << bundle_prefix("rubocop") unless standard_rb?
205
+ formatters << bundle_prefix("rubocop -A") unless standard_rb?
206
+ end
207
+ end
208
+
209
+ def detect_node_tools(tests, linters, formatters, frameworks)
210
+ return unless node_project?
211
+
212
+ if jest?
213
+ cmd = npm_or_yarn("test")
214
+ tests << cmd
215
+ frameworks[cmd] = :jest
216
+ elsif package_script?("test")
217
+ tests << npm_or_yarn("test")
218
+ end
219
+
220
+ %w[lint eslint].each do |script|
221
+ if package_script?(script)
222
+ linters << npm_or_yarn(script)
223
+ break
224
+ end
225
+ end
226
+
227
+ %w[format prettier].each do |script|
228
+ if package_script?(script)
229
+ formatters << npm_or_yarn(script)
230
+ break
231
+ end
232
+ end
233
+ end
234
+
235
+ def detect_python_tools(tests, linters, formatters, frameworks)
236
+ if python_pytest?
237
+ cmd = "pytest -q"
238
+ tests << cmd
239
+ frameworks[cmd] = :pytest
240
+ end
241
+
242
+ if python_ruff?
243
+ linters << "ruff check ."
244
+ formatters << "ruff format ."
245
+ elsif python_flake8?
246
+ linters << "flake8"
247
+ end
248
+
249
+ formatters << "black ." if python_black?
250
+ end
251
+
58
252
  def bundle_prefix(cmd)
59
253
  File.exist?(File.join(@root, "Gemfile")) ? "bundle exec #{cmd}" : cmd
60
254
  end
@@ -72,6 +266,24 @@ module Aidp
72
266
  end
73
267
  end
74
268
 
269
+ def minitest?
270
+ test_dir = File.join(@root, "test")
271
+ return false unless File.exist?(test_dir)
272
+
273
+ # Check for minitest in Gemfile or test files
274
+ gemfile_has_minitest = begin
275
+ File.readlines(File.join(@root, "Gemfile")).grep(/minitest/).any?
276
+ rescue
277
+ false
278
+ end
279
+
280
+ return true if gemfile_has_minitest
281
+
282
+ # Check for test files that use minitest
283
+ test_files = Dir.glob(File.join(test_dir, "**", "*_test.rb"))
284
+ test_files.any?
285
+ end
286
+
75
287
  def standard_rb?
76
288
  File.exist?(File.join(@root, "Gemfile")) &&
77
289
  begin
@@ -81,6 +293,16 @@ module Aidp
81
293
  end
82
294
  end
83
295
 
296
+ def rubocop?
297
+ File.exist?(File.join(@root, ".rubocop.yml")) ||
298
+ (File.exist?(File.join(@root, "Gemfile")) &&
299
+ begin
300
+ File.readlines(File.join(@root, "Gemfile")).grep(/rubocop/).any?
301
+ rescue
302
+ false
303
+ end)
304
+ end
305
+
84
306
  def package_json
85
307
  @package_json ||= begin
86
308
  path = File.join(@root, "package.json")
@@ -107,9 +329,54 @@ module Aidp
107
329
  end
108
330
  end
109
331
 
332
+ def jest?
333
+ return false unless node_project?
334
+
335
+ # Check for jest in dependencies or devDependencies
336
+ deps = package_json&.dig("dependencies") || {}
337
+ dev_deps = package_json&.dig("devDependencies") || {}
338
+
339
+ deps.key?("jest") || dev_deps.key?("jest") ||
340
+ package_json&.dig("scripts", "test")&.include?("jest")
341
+ end
342
+
110
343
  def python_pytest?
111
344
  Dir.glob(File.join(@root, "**", "pytest.ini")).any? ||
112
- Dir.glob(File.join(@root, "**", "conftest.py")).any?
345
+ Dir.glob(File.join(@root, "**", "conftest.py")).any? ||
346
+ (File.exist?(File.join(@root, "pyproject.toml")) &&
347
+ begin
348
+ File.read(File.join(@root, "pyproject.toml")).include?("pytest")
349
+ rescue
350
+ false
351
+ end)
352
+ end
353
+
354
+ def python_ruff?
355
+ File.exist?(File.join(@root, "pyproject.toml")) &&
356
+ begin
357
+ File.read(File.join(@root, "pyproject.toml")).include?("[tool.ruff]")
358
+ rescue
359
+ false
360
+ end
361
+ end
362
+
363
+ def python_flake8?
364
+ File.exist?(File.join(@root, ".flake8")) ||
365
+ File.exist?(File.join(@root, "setup.cfg")) &&
366
+ begin
367
+ File.read(File.join(@root, "setup.cfg")).include?("[flake8]")
368
+ rescue
369
+ false
370
+ end
371
+ end
372
+
373
+ def python_black?
374
+ File.exist?(File.join(@root, "pyproject.toml")) &&
375
+ begin
376
+ File.read(File.join(@root, "pyproject.toml")).include?("[tool.black]")
377
+ rescue
378
+ false
379
+ end
113
380
  end
114
381
  end
115
382
  end
data/lib/aidp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.32.0"
4
+ VERSION = "0.33.0"
5
5
  end
@@ -40,6 +40,12 @@ module Aidp
40
40
  @project_dir = project_dir
41
41
  @verbose = verbose
42
42
 
43
+ # Log initialization details
44
+ Aidp.log_debug("change_request_processor", "initializing",
45
+ provider_name: provider_name,
46
+ project_dir: project_dir,
47
+ verbose: verbose)
48
+
43
49
  # Initialize verifier
44
50
  @verifier = ImplementationVerifier.new(
45
51
  repository_client: repository_client,
@@ -50,6 +56,11 @@ module Aidp
50
56
  @change_request_label = label_config[:change_request_trigger] || label_config["change_request_trigger"] || DEFAULT_CHANGE_REQUEST_LABEL
51
57
  @needs_input_label = label_config[:needs_input] || label_config["needs_input"] || DEFAULT_NEEDS_INPUT_LABEL
52
58
 
59
+ # Log label details
60
+ Aidp.log_debug("change_request_processor", "label_configuration",
61
+ change_request_label: @change_request_label,
62
+ needs_input_label: @needs_input_label)
63
+
53
64
  # Load change request configuration
54
65
  @config = {
55
66
  enabled: true,
@@ -61,9 +72,18 @@ module Aidp
61
72
  allow_large_pr_worktree_bypass: true # Default to always using worktree for large PRs
62
73
  }.merge(symbolize_keys(change_request_config))
63
74
 
75
+ # Log configuration details
76
+ Aidp.log_debug("change_request_processor", "change_request_config",
77
+ config: @config.transform_values { |v| v.is_a?(Proc) ? "Proc" : v })
78
+
64
79
  # Load safety configuration
65
80
  @safety_config = safety_config
66
81
  @author_allowlist = Array(@safety_config[:author_allowlist] || @safety_config["author_allowlist"])
82
+
83
+ # Log safety configuration
84
+ Aidp.log_debug("change_request_processor", "safety_configuration",
85
+ author_allowlist: @author_allowlist,
86
+ allowlist_count: @author_allowlist.length)
67
87
  end
68
88
 
69
89
  def process(pr)
@@ -142,15 +162,29 @@ module Aidp
142
162
  def filter_authorized_comments(comments, pr_data)
143
163
  # If allowlist is empty (for private repos), consider PR author and all commenters
144
164
  # For public repos, enforce allowlist
165
+ Aidp.log_debug("change_request_processor", "filtering_authorized_comments",
166
+ total_comments: comments.length,
167
+ allowlist_count: @author_allowlist.length,
168
+ is_private_repo: @author_allowlist.empty?)
169
+
145
170
  if @author_allowlist.empty?
146
171
  # Private repo: trust all comments from PR participants
172
+ Aidp.log_debug("change_request_processor", "private_repo_comments_allowed",
173
+ comments_allowed: comments.length)
147
174
  comments
148
175
  else
149
176
  # Public repo: only allow comments from allowlisted users
150
- comments.select do |comment|
177
+ authorized_comments = comments.select do |comment|
151
178
  author = comment[:author]
152
179
  @author_allowlist.include?(author)
153
180
  end
181
+
182
+ Aidp.log_debug("change_request_processor", "public_repo_comment_filtering",
183
+ total_comments: comments.length,
184
+ authorized_comments: authorized_comments.length,
185
+ allowed_authors: authorized_comments.map { |c| c[:author] })
186
+
187
+ authorized_comments
154
188
  end
155
189
  end
156
190
 
@@ -395,14 +429,83 @@ module Aidp
395
429
  def create_worktree_for_pr(pr_data)
396
430
  head_ref = pr_data[:head_ref]
397
431
  pr_number = pr_data[:number]
398
- slug = "pr-#{pr_number}-change-requests"
399
432
 
400
- display_message("🌿 Creating worktree for PR ##{pr_number}: #{head_ref}", type: :info)
433
+ # Configure slug and worktree strategy
434
+ slug = pr_data.fetch(:worktree_slug, "pr-#{pr_number}-change-requests")
435
+ strategy = @config.fetch(:worktree_strategy, "auto")
436
+
437
+ display_message("🌿 Preparing worktree for PR ##{pr_number}: #{head_ref} (Strategy: #{strategy})", type: :info)
401
438
 
439
+ # Pre-create setup: fetch latest refs
402
440
  Dir.chdir(@project_dir) do
403
441
  run_git(%w[fetch origin], allow_failure: true)
404
442
  end
405
443
 
444
+ # Worktree creation strategy
445
+ worktree_path =
446
+ case strategy
447
+ when "always_create"
448
+ create_fresh_worktree(pr_data, slug)
449
+ when "reuse_only"
450
+ find_existing_worktree(pr_data, slug)
451
+ else # 'auto' or default
452
+ find_existing_worktree(pr_data, slug) || create_fresh_worktree(pr_data, slug)
453
+ end
454
+
455
+ Aidp.log_debug(
456
+ "change_request_processor",
457
+ "worktree_resolved",
458
+ pr_number: pr_number,
459
+ branch: head_ref,
460
+ path: worktree_path,
461
+ strategy: strategy
462
+ )
463
+
464
+ display_message("✅ Worktree available at #{worktree_path}", type: :success)
465
+ worktree_path
466
+ rescue => e
467
+ Aidp.log_error(
468
+ "change_request_processor",
469
+ "worktree_creation_failed",
470
+ pr_number: pr_number,
471
+ error: e.message,
472
+ backtrace: e.backtrace&.first(5)
473
+ )
474
+ display_message("❌ Failed to create worktree: #{e.message}", type: :error)
475
+ raise
476
+ end
477
+
478
+ private
479
+
480
+ def find_existing_worktree(pr_data, slug)
481
+ head_ref = pr_data[:head_ref]
482
+ pr_number = pr_data[:number]
483
+
484
+ # First check for existing worktree by branch
485
+ existing = Aidp::Worktree.find_by_branch(branch: head_ref, project_dir: @project_dir)
486
+ return existing[:path] if existing && existing[:active]
487
+
488
+ # If no branch-specific worktree, look for PR-specific worktree
489
+ pr_worktrees = Aidp::Worktree.list(project_dir: @project_dir)
490
+ pr_specific_worktree = pr_worktrees.find do |w|
491
+ w[:slug]&.include?("pr-#{pr_number}")
492
+ end
493
+
494
+ pr_specific_worktree ? pr_specific_worktree[:path] : nil
495
+ end
496
+
497
+ def create_fresh_worktree(pr_data, slug)
498
+ head_ref = pr_data[:head_ref]
499
+ pr_number = pr_data[:number]
500
+
501
+ Aidp.log_debug(
502
+ "change_request_processor",
503
+ "creating_new_worktree",
504
+ pr_number: pr_number,
505
+ branch: head_ref,
506
+ slug: slug
507
+ )
508
+
406
509
  result = Aidp::Worktree.create(
407
510
  slug: slug,
408
511
  project_dir: @project_dir,
@@ -410,11 +513,7 @@ module Aidp
410
513
  base_branch: pr_data[:base_ref]
411
514
  )
412
515
 
413
- worktree_path = result[:path]
414
- Aidp.log_debug("change_request_processor", "worktree_created", pr_number: pr_number, branch: head_ref, path: worktree_path)
415
- display_message("✅ Worktree created at #{worktree_path}", type: :success)
416
-
417
- worktree_path
516
+ result[:path]
418
517
  end
419
518
 
420
519
  def apply_changes(changes)
@@ -571,22 +670,42 @@ module Aidp
571
670
  end
572
671
 
573
672
  def handle_incomplete_implementation(pr:, analysis:, verification_result:)
673
+ Aidp.log_debug("change_request_processor", "start_incomplete_implementation_handling",
674
+ pr_number: pr[:number],
675
+ verification_result: {
676
+ missing_items_count: verification_result[:missing_items]&.length || 0,
677
+ additional_work_count: verification_result[:additional_work]&.length || 0
678
+ })
679
+
574
680
  display_message("⚠️ Implementation incomplete; creating follow-up tasks.", type: :warn)
575
681
 
576
682
  # Create tasks for missing requirements
577
683
  if verification_result[:additional_work] && !verification_result[:additional_work].empty?
684
+ Aidp.log_debug("change_request_processor", "preparing_follow_up_tasks",
685
+ pr_number: pr[:number],
686
+ additional_work_tasks_count: verification_result[:additional_work].length)
578
687
  create_follow_up_tasks(@project_dir, verification_result[:additional_work])
579
688
  end
580
689
 
581
690
  # Record state but do not post a separate comment
582
691
  # (verification details will be included in the next summary comment)
583
- @state_store.record_change_request(pr[:number], {
692
+ state_record = {
584
693
  status: "incomplete_implementation",
585
694
  timestamp: Time.now.utc.iso8601,
586
695
  verification_reasons: verification_result[:reasons],
587
696
  missing_items: verification_result[:missing_items],
588
697
  additional_work: verification_result[:additional_work]
589
- })
698
+ }
699
+
700
+ # Log the details of the state record before storing
701
+ Aidp.log_debug("change_request_processor", "recording_incomplete_implementation_state",
702
+ pr_number: pr[:number],
703
+ status: state_record[:status],
704
+ verification_reasons_count: state_record[:verification_reasons]&.length || 0,
705
+ missing_items_count: state_record[:missing_items]&.length || 0,
706
+ additional_work_count: state_record[:additional_work]&.length || 0)
707
+
708
+ @state_store.record_change_request(pr[:number], state_record)
590
709
 
591
710
  display_message("📝 Recorded incomplete implementation status for PR ##{pr[:number]}", type: :info)
592
711
 
@@ -603,36 +722,55 @@ module Aidp
603
722
  def create_follow_up_tasks(working_dir, additional_work)
604
723
  return if additional_work.nil? || additional_work.empty?
605
724
 
725
+ Aidp.log_debug("change_request_processor", "start_creating_follow_up_tasks",
726
+ working_dir: working_dir,
727
+ additional_work_tasks_count: additional_work.length)
728
+
606
729
  tasklist_file = File.join(working_dir, ".aidp", "tasklist.jsonl")
607
730
  FileUtils.mkdir_p(File.dirname(tasklist_file))
608
731
 
609
732
  require_relative "../execute/persistent_tasklist"
610
733
  tasklist = Aidp::Execute::PersistentTasklist.new(working_dir)
611
734
 
735
+ tasks_created = []
612
736
  additional_work.each do |task_description|
613
- tasklist.create(
737
+ task = tasklist.create(
614
738
  description: task_description,
615
739
  priority: :high,
616
740
  source: "verification"
617
741
  )
742
+ tasks_created << task
618
743
  end
619
744
 
620
745
  display_message("📝 Created #{additional_work.length} follow-up task(s) for continued work", type: :info)
621
746
 
747
+ Aidp.log_debug("change_request_processor", "follow_up_tasks_details",
748
+ task_count: tasks_created.length,
749
+ working_dir: working_dir,
750
+ task_descriptions: tasks_created.map(&:description))
751
+
622
752
  Aidp.log_info(
623
753
  "change_request_processor",
624
754
  "created_follow_up_tasks",
625
- task_count: additional_work.length,
755
+ task_count: tasks_created.length,
626
756
  working_dir: working_dir
627
757
  )
758
+
759
+ tasks_created
628
760
  rescue => e
629
- display_message("⚠️ Failed to create follow-up tasks: #{e.message}", type: :warn)
630
761
  Aidp.log_error(
631
762
  "change_request_processor",
632
763
  "failed_to_create_follow_up_tasks",
633
764
  error: e.message,
634
- backtrace: e.backtrace&.first(5)
765
+ error_class: e.class.name,
766
+ backtrace: e.backtrace&.first(5),
767
+ working_dir: working_dir
635
768
  )
769
+
770
+ display_message("⚠️ Failed to create follow-up tasks: #{e.message}", type: :warn)
771
+
772
+ # Return an empty array to indicate failure
773
+ []
636
774
  end
637
775
 
638
776
  def handle_clarification_needed(pr:, analysis:)
@@ -120,6 +120,47 @@ module Aidp
120
120
  gh_available? ? fetch_pr_comments_via_gh(number) : fetch_pr_comments_via_api(number)
121
121
  end
122
122
 
123
+ # Create or update a categorized comment (e.g., under a header) on an issue.
124
+ # If a comment with the category header exists, either append to it or
125
+ # replace it while archiving the previous content inline.
126
+ def consolidate_category_comment(issue_number, category_header, content, append: false)
127
+ existing_comment = find_comment(issue_number, category_header)
128
+
129
+ if existing_comment.nil?
130
+ Aidp.log_debug("repository_client", "creating_category_comment",
131
+ issue: issue_number,
132
+ header: category_header)
133
+ return post_comment(issue_number, "#{category_header}\n\n#{content}")
134
+ end
135
+
136
+ existing_body = existing_comment[:body] || existing_comment["body"] || ""
137
+ content_without_header = existing_body.sub(/\A#{Regexp.escape(category_header)}\s*/, "").strip
138
+
139
+ new_body =
140
+ if append
141
+ Aidp.log_debug("repository_client", "appending_category_comment",
142
+ issue: issue_number,
143
+ header: category_header)
144
+ segments = [category_header, content_without_header, content].reject(&:empty?)
145
+ segments.join("\n\n")
146
+ else
147
+ Aidp.log_debug("repository_client", "replacing_category_comment",
148
+ issue: issue_number,
149
+ header: category_header)
150
+ timestamp = Time.now.utc.iso8601
151
+ archive_marker = "<!-- ARCHIVED_PLAN_START #{timestamp} ARCHIVED_PLAN_END -->"
152
+ [category_header, content, archive_marker, content_without_header].join("\n\n")
153
+ end
154
+
155
+ update_comment(existing_comment[:id] || existing_comment["id"], new_body)
156
+ rescue => e
157
+ Aidp.log_error("repository_client", "consolidate_category_comment_failed",
158
+ issue: issue_number,
159
+ header: category_header,
160
+ error: e.message)
161
+ raise "GitHub error: #{e.message}"
162
+ end
163
+
123
164
  private
124
165
 
125
166
  # Retry a GitHub CLI operation with exponential backoff