aidp 0.23.0 → 0.25.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -1
  3. data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
  4. data/lib/aidp/auto_update/checkpoint.rb +178 -0
  5. data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
  6. data/lib/aidp/auto_update/coordinator.rb +204 -0
  7. data/lib/aidp/auto_update/errors.rb +17 -0
  8. data/lib/aidp/auto_update/failure_tracker.rb +162 -0
  9. data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
  10. data/lib/aidp/auto_update/update_check.rb +106 -0
  11. data/lib/aidp/auto_update/update_logger.rb +143 -0
  12. data/lib/aidp/auto_update/update_policy.rb +109 -0
  13. data/lib/aidp/auto_update/version_detector.rb +144 -0
  14. data/lib/aidp/auto_update.rb +52 -0
  15. data/lib/aidp/cli.rb +168 -1
  16. data/lib/aidp/execute/work_loop_runner.rb +252 -45
  17. data/lib/aidp/execute/work_loop_unit_scheduler.rb +27 -2
  18. data/lib/aidp/harness/condition_detector.rb +42 -8
  19. data/lib/aidp/harness/config_manager.rb +7 -0
  20. data/lib/aidp/harness/config_schema.rb +75 -0
  21. data/lib/aidp/harness/configuration.rb +69 -6
  22. data/lib/aidp/harness/error_handler.rb +117 -44
  23. data/lib/aidp/harness/provider_factory.rb +2 -0
  24. data/lib/aidp/harness/provider_manager.rb +64 -0
  25. data/lib/aidp/harness/provider_metrics.rb +138 -0
  26. data/lib/aidp/harness/runner.rb +90 -29
  27. data/lib/aidp/harness/simple_user_interface.rb +4 -0
  28. data/lib/aidp/harness/state/ui_state.rb +0 -10
  29. data/lib/aidp/harness/state_manager.rb +1 -15
  30. data/lib/aidp/harness/test_runner.rb +39 -2
  31. data/lib/aidp/logger.rb +34 -4
  32. data/lib/aidp/message_display.rb +10 -2
  33. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
  34. data/lib/aidp/provider_manager.rb +2 -0
  35. data/lib/aidp/providers/adapter.rb +241 -0
  36. data/lib/aidp/providers/anthropic.rb +75 -7
  37. data/lib/aidp/providers/base.rb +29 -1
  38. data/lib/aidp/providers/capability_registry.rb +205 -0
  39. data/lib/aidp/providers/codex.rb +14 -0
  40. data/lib/aidp/providers/error_taxonomy.rb +195 -0
  41. data/lib/aidp/providers/gemini.rb +3 -2
  42. data/lib/aidp/providers/kilocode.rb +202 -0
  43. data/lib/aidp/setup/provider_registry.rb +122 -0
  44. data/lib/aidp/setup/wizard.rb +125 -33
  45. data/lib/aidp/skills/composer.rb +4 -0
  46. data/lib/aidp/skills/loader.rb +3 -1
  47. data/lib/aidp/version.rb +1 -1
  48. data/lib/aidp/watch/build_processor.rb +323 -33
  49. data/lib/aidp/watch/ci_fix_processor.rb +448 -0
  50. data/lib/aidp/watch/plan_processor.rb +12 -2
  51. data/lib/aidp/watch/repository_client.rb +384 -4
  52. data/lib/aidp/watch/review_processor.rb +266 -0
  53. data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
  54. data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
  55. data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
  56. data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
  57. data/lib/aidp/watch/runner.rb +222 -5
  58. data/lib/aidp/watch/state_store.rb +53 -0
  59. data/lib/aidp/workflows/guided_agent.rb +53 -0
  60. data/lib/aidp/worktree.rb +67 -10
  61. data/lib/aidp.rb +1 -0
  62. data/templates/work_loop/decide_whats_next.md +21 -0
  63. data/templates/work_loop/diagnose_failures.md +21 -0
  64. metadata +29 -3
  65. /data/{bin → exe}/aidp +0 -0
@@ -2,11 +2,14 @@
2
2
 
3
3
  require "open3"
4
4
  require "time"
5
+ require "fileutils"
5
6
 
6
7
  require_relative "../message_display"
7
8
  require_relative "../execute/prompt_manager"
8
9
  require_relative "../harness/runner"
10
+ require_relative "../harness/state_manager"
9
11
  require_relative "../worktree"
12
+ require_relative "../execute/progress"
10
13
 
11
14
  module Aidp
12
15
  module Watch
@@ -55,6 +58,8 @@ module Aidp
55
58
  working_dir = @project_dir
56
59
  end
57
60
 
61
+ sync_local_aidp_config(working_dir)
62
+
58
63
  prompt_content = build_prompt(issue: issue, plan_data: plan_data)
59
64
  write_prompt(prompt_content, working_dir: working_dir)
60
65
 
@@ -65,14 +70,36 @@ module Aidp
65
70
  handle_success(issue: issue, slug: slug, branch_name: branch_name, base_branch: base_branch, plan_data: plan_data, working_dir: working_dir)
66
71
  elsif result[:status] == "needs_clarification"
67
72
  handle_clarification_request(issue: issue, slug: slug, result: result)
73
+ elsif result[:reason] == :completion_criteria
74
+ handle_incomplete_criteria(issue: issue, slug: slug, branch_name: branch_name, working_dir: working_dir, metadata: result[:failure_metadata])
68
75
  else
69
76
  handle_failure(issue: issue, slug: slug, result: result)
70
77
  end
71
78
  rescue => e
72
- display_message("❌ Implementation failed: #{e.message}", type: :error)
73
- @state_store.record_build_status(issue[:number], status: "failed", details: {error: e.message})
74
- cleanup_workstream(slug) if @use_workstreams && slug
75
- raise
79
+ # Don't re-raise - handle gracefully for fix-forward pattern
80
+ display_message("❌ Implementation failed with exception: #{e.message}", type: :error)
81
+ Aidp.log_error(
82
+ "build_processor",
83
+ "Implementation failed with exception",
84
+ issue: issue[:number],
85
+ error: e.message,
86
+ error_class: e.class.name,
87
+ backtrace: e.backtrace&.first(10)
88
+ )
89
+
90
+ # Create error result to pass to handle_failure
91
+ error_result = {
92
+ status: "error",
93
+ error: e.message,
94
+ error_class: e.class.name,
95
+ message: "Exception during harness execution: #{e.message}"
96
+ }
97
+
98
+ # Handle as failure (posts comment, updates state) but DON'T re-raise
99
+ handle_failure(issue: issue, slug: slug, result: error_result)
100
+
101
+ # Note: We intentionally DON'T re-raise here to allow watch mode to continue
102
+ # The error has been logged, recorded, and reported to GitHub
76
103
  end
77
104
 
78
105
  private
@@ -147,20 +174,18 @@ module Aidp
147
174
  base_branch: base_branch
148
175
  )
149
176
 
150
- if result[:success]
151
- display_message("✅ Workstream created at #{result[:path]}", type: :success)
152
- result[:path]
153
- else
154
- raise "Failed to create workstream: #{result[:message]}"
155
- end
177
+ worktree_path = worktree_path_from_result(result)
178
+ display_message("✅ Workstream created at #{worktree_path}", type: :success)
179
+ worktree_path
156
180
  end
157
181
 
158
182
  def cleanup_workstream(slug)
159
183
  return unless slug
160
184
 
161
185
  display_message("🧹 Cleaning up workstream: #{slug}", type: :info)
162
- result = Aidp::Worktree.remove(slug: slug, project_dir: @project_dir, force: true)
163
- if result[:success]
186
+ result = Aidp::Worktree.remove(slug: slug, project_dir: @project_dir, delete_branch: true)
187
+ removed = (result == true) || (result.respond_to?(:[]) && result[:success])
188
+ if removed
164
189
  display_message("✅ Workstream removed", type: :success)
165
190
  else
166
191
  display_message("⚠️ Failed to remove workstream: #{result[:message]}", type: :warn)
@@ -211,7 +236,7 @@ module Aidp
211
236
 
212
237
  def write_prompt(content, working_dir: @project_dir)
213
238
  prompt_manager = Aidp::Execute::PromptManager.new(working_dir)
214
- prompt_manager.write(content)
239
+ prompt_manager.write(content, step_name: IMPLEMENTATION_STEP)
215
240
  display_message("📝 Wrote PROMPT.md with implementation contract", type: :info)
216
241
 
217
242
  if @verbose
@@ -243,10 +268,21 @@ module Aidp
243
268
  end
244
269
 
245
270
  def run_harness(user_input:, working_dir: @project_dir)
271
+ reset_work_loop_state(working_dir)
272
+
273
+ Aidp.log_info(
274
+ "build_processor",
275
+ "starting_harness",
276
+ issue_dir: working_dir,
277
+ workflow_type: :watch_mode,
278
+ selected_steps: [IMPLEMENTATION_STEP]
279
+ )
280
+
246
281
  options = {
247
282
  selected_steps: [IMPLEMENTATION_STEP],
248
283
  workflow_type: :watch_mode,
249
- user_input: user_input
284
+ user_input: user_input,
285
+ non_interactive: true
250
286
  }
251
287
 
252
288
  display_message("🚀 Running harness in execute mode...", type: :info) if @verbose
@@ -258,32 +294,133 @@ module Aidp
258
294
  display_message("\n--- Harness Result ---", type: :muted)
259
295
  display_message("Status: #{result[:status]}", type: :muted)
260
296
  display_message("Message: #{result[:message]}", type: :muted) if result[:message]
297
+ if result[:error]
298
+ display_message("Error: #{result[:error]}", type: :muted)
299
+ display_message("Error Details: #{result[:error_details]}", type: :muted) if result[:error_details]
300
+ end
261
301
  display_message("--- End Result ---\n", type: :muted)
262
302
  end
263
303
 
304
+ Aidp.log_info(
305
+ "build_processor",
306
+ "harness_result",
307
+ status: result[:status],
308
+ message: result[:message],
309
+ error: result[:error],
310
+ error_class: result[:error_class]
311
+ )
312
+
313
+ # Log errors to aidp.log
314
+ if result[:status] == "error"
315
+ error_msg = result[:message] || "Unknown error"
316
+ error_details = {
317
+ status: result[:status],
318
+ message: error_msg,
319
+ error: result[:error]&.to_s,
320
+ error_class: result[:error]&.class&.name,
321
+ backtrace: result[:backtrace]&.first(5)
322
+ }.compact
323
+ Aidp.log_error("build_processor", "Harness execution failed", **error_details)
324
+ end
325
+
264
326
  result
265
327
  end
266
328
 
329
+ def reset_work_loop_state(working_dir)
330
+ state_manager = Aidp::Harness::StateManager.new(working_dir, :execute)
331
+ state_manager.clear_state
332
+ Aidp::Execute::Progress.new(working_dir).reset
333
+ rescue => e
334
+ display_message("⚠️ Failed to reset work loop state before execution: #{e.message}", type: :warn)
335
+ Aidp.log_warn("build_processor", "failed_to_reset_work_loop_state", error: e.message, working_dir: working_dir)
336
+ end
337
+
338
+ def enqueue_decider_followup(target_dir)
339
+ work_loop_dir = File.join(target_dir, ".aidp", "work_loop")
340
+ FileUtils.mkdir_p(work_loop_dir)
341
+ request_path = File.join(work_loop_dir, "initial_units.txt")
342
+ File.open(request_path, "a") { |file| file.puts("decide_whats_next") }
343
+ Aidp.log_info("build_processor", "scheduled_decider_followup", request_path: request_path)
344
+ rescue => e
345
+ Aidp.log_warn("build_processor", "failed_to_schedule_decider", error: e.message)
346
+ end
347
+
348
+ def sync_local_aidp_config(target_dir)
349
+ return if target_dir.nil? || target_dir == @project_dir
350
+
351
+ source_config = File.join(@project_dir, ".aidp", "aidp.yml")
352
+ return unless File.exist?(source_config)
353
+
354
+ target_config = File.join(target_dir, ".aidp", "aidp.yml")
355
+ FileUtils.mkdir_p(File.dirname(target_config))
356
+
357
+ # Only copy when target missing or differs
358
+ if !File.exist?(target_config) || File.read(source_config, encoding: "UTF-8") != File.read(target_config, encoding: "UTF-8")
359
+ FileUtils.cp(source_config, target_config)
360
+ end
361
+ rescue => e
362
+ display_message("⚠️ Failed to sync AIDP config to workstream: #{e.message}", type: :warn)
363
+ end
364
+
365
+ def worktree_path_from_result(result)
366
+ return result if result.is_a?(String)
367
+
368
+ path = result[:path] || result["path"]
369
+ return path if path
370
+
371
+ message = result[:message] || "unknown error"
372
+ raise "Failed to create workstream: #{message}"
373
+ end
374
+
267
375
  def handle_success(issue:, slug:, branch_name:, base_branch:, plan_data:, working_dir:)
268
- stage_and_commit(issue, working_dir: working_dir)
376
+ changes_committed = stage_and_commit(issue, working_dir: working_dir)
269
377
 
270
- # Check if PR should be created based on VCS preferences
271
- vcs_config = config.dig(:work_loop, :version_control) || {}
272
- auto_create_pr = vcs_config.fetch(:auto_create_pr, false)
378
+ unless changes_committed
379
+ handle_no_changes(issue: issue, slug: slug, branch_name: branch_name, working_dir: working_dir)
380
+ return
381
+ end
273
382
 
274
- pr_url = if auto_create_pr
383
+ # Check if PR should be created based on VCS preferences
384
+ # For watch mode, default to creating PRs (set to false to disable)
385
+ vcs_config = config_dig(:work_loop, :version_control) || {}
386
+ auto_create_pr = config_value(vcs_config, :auto_create_pr, true)
387
+
388
+ pr_url = if !changes_committed
389
+ Aidp.log_info(
390
+ "build_processor",
391
+ "skipping_pr_no_commits",
392
+ issue: issue[:number],
393
+ branch: branch_name,
394
+ working_dir: working_dir
395
+ )
396
+ display_message("ℹ️ Skipping PR creation because there are no commits on #{branch_name}.", type: :muted)
397
+ nil
398
+ elsif auto_create_pr
399
+ Aidp.log_info(
400
+ "build_processor",
401
+ "creating_pull_request",
402
+ issue: issue[:number],
403
+ branch: branch_name,
404
+ base_branch: base_branch,
405
+ working_dir: working_dir
406
+ )
275
407
  create_pull_request(issue: issue, branch_name: branch_name, base_branch: base_branch, working_dir: working_dir)
276
408
  else
277
409
  display_message("ℹ️ Skipping PR creation (disabled in VCS preferences)", type: :muted)
278
410
  nil
279
411
  end
280
412
 
413
+ # Fetch the user who added the most recent label
414
+ label_actor = @repository_client.most_recent_label_actor(issue[:number])
415
+
281
416
  workstream_note = @use_workstreams ? "\n- Workstream: `#{slug}`" : ""
282
417
  pr_line = pr_url ? "\n- Pull Request: #{pr_url}" : ""
418
+ actor_tag = label_actor ? "cc @#{label_actor}\n\n" : ""
283
419
 
284
420
  comment = <<~COMMENT
285
421
  ✅ Implementation complete for ##{issue[:number]}.
286
- - Branch: `#{branch_name}`#{workstream_note}#{pr_line}
422
+
423
+ #{actor_tag}- Branch: `#{branch_name}`#{workstream_note}#{pr_line}
287
424
 
288
425
  Summary:
289
426
  #{plan_value(plan_data, "summary")}
@@ -316,10 +453,20 @@ module Aidp
316
453
  questions = result[:clarification_questions] || []
317
454
  workstream_note = @use_workstreams ? " The workstream `#{slug}` has been preserved." : " The branch has been preserved."
318
455
 
456
+ # Fetch the user who added the most recent label
457
+ label_actor = @repository_client.most_recent_label_actor(issue[:number])
458
+
319
459
  # Build comment with questions
320
460
  comment_parts = []
321
461
  comment_parts << "❓ Implementation needs clarification for ##{issue[:number]}."
322
462
  comment_parts << ""
463
+
464
+ # Tag the label actor if available
465
+ if label_actor
466
+ comment_parts << "cc @#{label_actor}"
467
+ comment_parts << ""
468
+ end
469
+
323
470
  comment_parts << "The AI agent needs additional information to proceed with implementation:"
324
471
  comment_parts << ""
325
472
  questions.each_with_index do |question, index|
@@ -355,49 +502,143 @@ module Aidp
355
502
 
356
503
  def handle_failure(issue:, slug:, result:)
357
504
  message = result[:message] || "Unknown failure"
505
+ error_info = result[:error] || result[:error_details]
358
506
  workstream_note = @use_workstreams ? " The workstream `#{slug}` has been left intact for debugging." : " The branch has been left intact for debugging."
507
+
508
+ # Build detailed error message for the comment
509
+ error_details_section = if error_info
510
+ "\nError: #{error_info}"
511
+ else
512
+ ""
513
+ end
514
+
359
515
  comment = <<~COMMENT
360
516
  ❌ Implementation attempt for ##{issue[:number]} failed.
361
517
 
362
518
  Status: #{result[:status]}
363
- Details: #{message}
519
+ Details: #{message}#{error_details_section}
364
520
 
365
521
  Please review the repository for partial changes.#{workstream_note}
366
522
  COMMENT
367
523
  @repository_client.post_comment(issue[:number], comment)
524
+
525
+ # Log the failure with full details
526
+ Aidp.log_error(
527
+ "build_processor",
528
+ "Build failed for issue ##{issue[:number]}",
529
+ status: result[:status],
530
+ message: message,
531
+ error: error_info&.to_s,
532
+ workstream: slug
533
+ )
534
+
368
535
  @state_store.record_build_status(
369
536
  issue[:number],
370
537
  status: "failed",
371
- details: {message: message, workstream: slug}
538
+ details: {message: message, error: error_info&.to_s, workstream: slug}
372
539
  )
373
540
  display_message("⚠️ Build failure recorded for issue ##{issue[:number]}", type: :warn)
374
541
  end
375
542
 
543
+ def handle_no_changes(issue:, slug:, branch_name:, working_dir:)
544
+ location_note = if @use_workstreams
545
+ "The workstream `#{slug}` has been preserved for review."
546
+ else
547
+ "Branch `#{branch_name}` remains checked out for inspection."
548
+ end
549
+
550
+ @state_store.record_build_status(
551
+ issue[:number],
552
+ status: "no_changes",
553
+ details: {branch: branch_name, workstream: slug}
554
+ )
555
+
556
+ Aidp.log_warn(
557
+ "build_processor",
558
+ "noop_build_result",
559
+ issue: issue[:number],
560
+ branch: branch_name,
561
+ workstream: slug
562
+ )
563
+
564
+ display_message("⚠️ Implementation produced no changes; labels remain untouched. #{location_note}", type: :warn)
565
+ enqueue_decider_followup(working_dir)
566
+ end
567
+
568
+ def handle_incomplete_criteria(issue:, slug:, branch_name:, working_dir:, metadata:)
569
+ display_message("⚠️ Completion criteria unmet; scheduling additional fix-forward iteration.", type: :warn)
570
+ enqueue_decider_followup(working_dir)
571
+
572
+ @state_store.record_build_status(
573
+ issue[:number],
574
+ status: "pending_fix_forward",
575
+ details: {branch: branch_name, workstream: slug, criteria: metadata}
576
+ )
577
+
578
+ Aidp.log_info(
579
+ "build_processor",
580
+ "pending_fix_forward",
581
+ issue: issue[:number],
582
+ branch: branch_name,
583
+ workstream: slug,
584
+ criteria: metadata
585
+ )
586
+ end
587
+
376
588
  def stage_and_commit(issue, working_dir: @project_dir)
589
+ commit_created = false
590
+
377
591
  Dir.chdir(working_dir) do
378
592
  status_output = run_git(%w[status --porcelain])
379
593
  if status_output.strip.empty?
380
594
  display_message("ℹ️ No file changes detected after work loop.", type: :muted)
381
- return
595
+ Aidp.log_info("build_processor", "no_changes_after_work_loop", issue: issue[:number], working_dir: working_dir)
596
+ return commit_created
382
597
  end
383
598
 
599
+ changed_entries = status_output.lines.map(&:strip).reject(&:empty?)
600
+ Aidp.log_info(
601
+ "build_processor",
602
+ "changes_detected_after_work_loop",
603
+ issue: issue[:number],
604
+ working_dir: working_dir,
605
+ changed_file_count: changed_entries.length,
606
+ changed_files_sample: changed_entries.first(10)
607
+ )
608
+
384
609
  run_git(%w[add -A])
385
610
  commit_message = build_commit_message(issue)
386
611
  run_git(["commit", "-m", commit_message])
387
612
  display_message("💾 Created commit: #{commit_message.lines.first.strip}", type: :info)
613
+ Aidp.log_info(
614
+ "build_processor",
615
+ "commit_created",
616
+ working_dir: working_dir,
617
+ issue: issue[:number],
618
+ commit_summary: commit_message.lines.first.strip
619
+ )
620
+
621
+ # Push the branch to remote
622
+ current_branch = run_git(%w[branch --show-current]).strip
623
+ run_git(["push", "-u", "origin", current_branch])
624
+ display_message("⬆️ Pushed branch '#{current_branch}' to remote", type: :info)
625
+ Aidp.log_info("build_processor", "branch_pushed", branch: current_branch, working_dir: working_dir)
626
+ commit_created = true
388
627
  end
628
+
629
+ commit_created
389
630
  end
390
631
 
391
632
  def build_commit_message(issue)
392
- vcs_config = config.dig(:work_loop, :version_control) || {}
633
+ vcs_config = config_dig(:work_loop, :version_control) || {}
393
634
 
394
635
  # Base message components
395
636
  issue_ref = "##{issue[:number]}"
396
637
  title = issue[:title]
397
638
 
398
639
  # Determine commit prefix based on configuration
399
- prefix = if vcs_config[:conventional_commits]
400
- commit_style = vcs_config[:commit_style] || "default"
640
+ prefix = if config_value(vcs_config, :conventional_commits)
641
+ commit_style = config_value(vcs_config, :commit_style, "default")
401
642
  emoji = (commit_style == "emoji") ? "✨ " : ""
402
643
  scope = (commit_style == "angular") ? "(implementation)" : ""
403
644
  "#{emoji}feat#{scope}: "
@@ -409,7 +650,7 @@ module Aidp
409
650
  main_message = "#{prefix}implement #{issue_ref} #{title}"
410
651
 
411
652
  # Add co-author attribution if configured
412
- if vcs_config.fetch(:co_author_ai, true)
653
+ if config_value(vcs_config, :co_author_ai, true)
413
654
  provider_name = detect_current_provider || "AI Agent"
414
655
  co_author = "\n\nCo-authored-by: #{provider_name} <ai@aidp.dev>"
415
656
  main_message + co_author
@@ -432,15 +673,40 @@ module Aidp
432
673
  @config ||= begin
433
674
  config_manager = Aidp::Harness::ConfigManager.new(@project_dir)
434
675
  config_manager.config || {}
435
- rescue
676
+ rescue => e
677
+ Aidp.log_error("build_processor", "config_load_exception", project_dir: @project_dir, error: e.message, backtrace: e.backtrace&.first(5))
436
678
  {}
437
679
  end
438
680
  end
439
681
 
682
+ # Helper to safely dig into config with both string and symbol keys
683
+ def config_dig(*keys)
684
+ value = config
685
+ keys.each do |key|
686
+ return nil unless value.is_a?(Hash)
687
+ # Try both symbol and string versions of the key
688
+ value = value[key] || value[key.to_s] || value[key.to_sym]
689
+ return nil if value.nil?
690
+ end
691
+ value
692
+ end
693
+
694
+ # Helper to get config value with both string and symbol key support
695
+ def config_value(hash, key, default = nil)
696
+ return default unless hash.is_a?(Hash)
697
+ # Check each key variation explicitly to handle false/nil values correctly
698
+ return hash[key] if hash.key?(key)
699
+ return hash[key.to_s] if hash.key?(key.to_s)
700
+ return hash[key.to_sym] if hash.key?(key.to_sym)
701
+ default
702
+ end
703
+
440
704
  def create_pull_request(issue:, branch_name:, base_branch:, working_dir: @project_dir)
441
705
  title = "aidp: Resolve ##{issue[:number]} - #{issue[:title]}"
442
706
  test_summary = gather_test_summary(working_dir: working_dir)
443
707
  body = <<~BODY
708
+ Fixes ##{issue[:number]}
709
+
444
710
  ## Summary
445
711
  - Automated resolution for ##{issue[:number]}
446
712
 
@@ -449,20 +715,44 @@ module Aidp
449
715
  BODY
450
716
 
451
717
  # Determine if PR should be draft based on VCS preferences
452
- vcs_config = config.dig(:work_loop, :version_control) || {}
453
- pr_strategy = vcs_config[:pr_strategy] || "draft"
718
+ vcs_config = config_dig(:work_loop, :version_control) || {}
719
+ pr_strategy = config_value(vcs_config, :pr_strategy, "draft")
454
720
  draft = (pr_strategy == "draft")
455
721
 
722
+ # Fetch the user who added the most recent label to assign the PR
723
+ label_actor = @repository_client.most_recent_label_actor(issue[:number])
724
+ assignee = label_actor || issue[:author]
725
+
726
+ Aidp.log_info(
727
+ "build_processor",
728
+ "assigning_pr",
729
+ issue: issue[:number],
730
+ assignee: assignee,
731
+ label_actor: label_actor,
732
+ fallback_to_author: label_actor.nil?
733
+ )
734
+
456
735
  output = @repository_client.create_pull_request(
457
736
  title: title,
458
737
  body: body,
459
738
  head: branch_name,
460
739
  base: base_branch,
461
740
  issue_number: issue[:number],
462
- draft: draft
741
+ draft: draft,
742
+ assignee: assignee
463
743
  )
464
744
 
465
- extract_pr_url(output)
745
+ pr_url = extract_pr_url(output)
746
+ Aidp.log_info(
747
+ "build_processor",
748
+ "pull_request_created",
749
+ issue: issue[:number],
750
+ branch: branch_name,
751
+ base_branch: base_branch,
752
+ pr_url: pr_url,
753
+ assignee: assignee
754
+ )
755
+ pr_url
466
756
  end
467
757
 
468
758
  def gather_test_summary(working_dir: @project_dir)
@@ -470,7 +760,7 @@ module Aidp
470
760
  log_path = File.join(".aidp", "logs", "test_runner.log")
471
761
  return "- Fix-forward harness executed; refer to #{log_path}" unless File.exist?(log_path)
472
762
 
473
- recent = File.readlines(log_path).last(20).map(&:strip).reject(&:empty?)
763
+ recent = File.readlines(log_path, encoding: "UTF-8").last(20).map(&:strip).reject(&:empty?)
474
764
  if recent.empty?
475
765
  "- Fix-forward harness executed successfully."
476
766
  else