aidp 0.22.0 → 0.24.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +145 -31
  3. data/lib/aidp/cli.rb +19 -2
  4. data/lib/aidp/execute/work_loop_runner.rb +252 -45
  5. data/lib/aidp/execute/work_loop_unit_scheduler.rb +27 -2
  6. data/lib/aidp/harness/condition_detector.rb +42 -8
  7. data/lib/aidp/harness/config_manager.rb +7 -0
  8. data/lib/aidp/harness/config_schema.rb +25 -0
  9. data/lib/aidp/harness/configuration.rb +69 -6
  10. data/lib/aidp/harness/error_handler.rb +117 -44
  11. data/lib/aidp/harness/provider_manager.rb +64 -0
  12. data/lib/aidp/harness/provider_metrics.rb +138 -0
  13. data/lib/aidp/harness/runner.rb +110 -35
  14. data/lib/aidp/harness/simple_user_interface.rb +4 -0
  15. data/lib/aidp/harness/state/ui_state.rb +0 -10
  16. data/lib/aidp/harness/state_manager.rb +1 -15
  17. data/lib/aidp/harness/test_runner.rb +39 -2
  18. data/lib/aidp/logger.rb +34 -4
  19. data/lib/aidp/providers/adapter.rb +241 -0
  20. data/lib/aidp/providers/anthropic.rb +75 -7
  21. data/lib/aidp/providers/base.rb +29 -1
  22. data/lib/aidp/providers/capability_registry.rb +205 -0
  23. data/lib/aidp/providers/codex.rb +14 -0
  24. data/lib/aidp/providers/error_taxonomy.rb +195 -0
  25. data/lib/aidp/providers/gemini.rb +3 -2
  26. data/lib/aidp/setup/devcontainer/backup_manager.rb +11 -4
  27. data/lib/aidp/setup/provider_registry.rb +107 -0
  28. data/lib/aidp/setup/wizard.rb +189 -31
  29. data/lib/aidp/version.rb +1 -1
  30. data/lib/aidp/watch/build_processor.rb +357 -27
  31. data/lib/aidp/watch/plan_generator.rb +16 -1
  32. data/lib/aidp/watch/plan_processor.rb +54 -3
  33. data/lib/aidp/watch/repository_client.rb +78 -4
  34. data/lib/aidp/watch/repository_safety_checker.rb +12 -3
  35. data/lib/aidp/watch/runner.rb +52 -10
  36. data/lib/aidp/workflows/guided_agent.rb +53 -0
  37. data/lib/aidp/worktree.rb +67 -10
  38. data/templates/work_loop/decide_whats_next.md +21 -0
  39. data/templates/work_loop/diagnose_failures.md +21 -0
  40. metadata +10 -3
  41. /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
@@ -15,14 +18,22 @@ module Aidp
15
18
  class BuildProcessor
16
19
  include Aidp::MessageDisplay
17
20
 
18
- BUILD_LABEL = "aidp-build"
21
+ DEFAULT_BUILD_LABEL = "aidp-build"
22
+ DEFAULT_NEEDS_INPUT_LABEL = "aidp-needs-input"
19
23
  IMPLEMENTATION_STEP = "16_IMPLEMENTATION"
20
24
 
21
- def initialize(repository_client:, state_store:, project_dir: Dir.pwd, use_workstreams: true)
25
+ attr_reader :build_label, :needs_input_label
26
+
27
+ def initialize(repository_client:, state_store:, project_dir: Dir.pwd, use_workstreams: true, verbose: false, label_config: {})
22
28
  @repository_client = repository_client
23
29
  @state_store = state_store
24
30
  @project_dir = project_dir
25
31
  @use_workstreams = use_workstreams
32
+ @verbose = verbose
33
+
34
+ # Load label configuration
35
+ @build_label = label_config[:build_trigger] || label_config["build_trigger"] || DEFAULT_BUILD_LABEL
36
+ @needs_input_label = label_config[:needs_input] || label_config["needs_input"] || DEFAULT_NEEDS_INPUT_LABEL
26
37
  end
27
38
 
28
39
  def process(issue)
@@ -47,6 +58,8 @@ module Aidp
47
58
  working_dir = @project_dir
48
59
  end
49
60
 
61
+ sync_local_aidp_config(working_dir)
62
+
50
63
  prompt_content = build_prompt(issue: issue, plan_data: plan_data)
51
64
  write_prompt(prompt_content, working_dir: working_dir)
52
65
 
@@ -55,14 +68,38 @@ module Aidp
55
68
 
56
69
  if result[:status] == "completed"
57
70
  handle_success(issue: issue, slug: slug, branch_name: branch_name, base_branch: base_branch, plan_data: plan_data, working_dir: working_dir)
71
+ elsif result[:status] == "needs_clarification"
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])
58
75
  else
59
76
  handle_failure(issue: issue, slug: slug, result: result)
60
77
  end
61
78
  rescue => e
62
- display_message("❌ Implementation failed: #{e.message}", type: :error)
63
- @state_store.record_build_status(issue[:number], status: "failed", details: {error: e.message})
64
- cleanup_workstream(slug) if @use_workstreams && slug
65
- 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
66
103
  end
67
104
 
68
105
  private
@@ -137,20 +174,18 @@ module Aidp
137
174
  base_branch: base_branch
138
175
  )
139
176
 
140
- if result[:success]
141
- display_message("✅ Workstream created at #{result[:path]}", type: :success)
142
- result[:path]
143
- else
144
- raise "Failed to create workstream: #{result[:message]}"
145
- end
177
+ worktree_path = worktree_path_from_result(result)
178
+ display_message("✅ Workstream created at #{worktree_path}", type: :success)
179
+ worktree_path
146
180
  end
147
181
 
148
182
  def cleanup_workstream(slug)
149
183
  return unless slug
150
184
 
151
185
  display_message("🧹 Cleaning up workstream: #{slug}", type: :info)
152
- result = Aidp::Worktree.remove(slug: slug, project_dir: @project_dir, force: true)
153
- 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
154
189
  display_message("✅ Workstream removed", type: :success)
155
190
  else
156
191
  display_message("⚠️ Failed to remove workstream: #{result[:message]}", type: :warn)
@@ -201,37 +236,174 @@ module Aidp
201
236
 
202
237
  def write_prompt(content, working_dir: @project_dir)
203
238
  prompt_manager = Aidp::Execute::PromptManager.new(working_dir)
204
- prompt_manager.write(content)
239
+ prompt_manager.write(content, step_name: IMPLEMENTATION_STEP)
205
240
  display_message("📝 Wrote PROMPT.md with implementation contract", type: :info)
241
+
242
+ if @verbose
243
+ display_message("\n--- Implementation Prompt ---", type: :muted)
244
+ display_message(content.strip, type: :muted)
245
+ display_message("--- End Prompt ---\n", type: :muted)
246
+ end
206
247
  end
207
248
 
208
249
  def build_user_input(issue:, plan_data:)
209
250
  tasks = Array(plan_value(plan_data, "tasks"))
210
- {
251
+ user_input = {
211
252
  "Implementation Contract" => plan_value(plan_data, "summary").to_s,
212
253
  "Tasks" => tasks.map { |task| "- #{task}" }.join("\n"),
213
254
  "Issue URL" => issue[:url]
214
255
  }.delete_if { |_k, v| v.nil? || v.empty? }
256
+
257
+ if @verbose
258
+ display_message("\n--- User Input for Harness ---", type: :muted)
259
+ user_input.each do |key, value|
260
+ display_message("#{key}:", type: :muted)
261
+ display_message(value, type: :muted)
262
+ display_message("", type: :muted)
263
+ end
264
+ display_message("--- End User Input ---\n", type: :muted)
265
+ end
266
+
267
+ user_input
215
268
  end
216
269
 
217
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
+
218
281
  options = {
219
282
  selected_steps: [IMPLEMENTATION_STEP],
220
283
  workflow_type: :watch_mode,
221
- user_input: user_input
284
+ user_input: user_input,
285
+ non_interactive: true
222
286
  }
287
+
288
+ display_message("🚀 Running harness in execute mode...", type: :info) if @verbose
289
+
223
290
  runner = Aidp::Harness::Runner.new(working_dir, :execute, options)
224
- runner.run
291
+ result = runner.run
292
+
293
+ if @verbose
294
+ display_message("\n--- Harness Result ---", type: :muted)
295
+ display_message("Status: #{result[:status]}", type: :muted)
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
301
+ display_message("--- End Result ---\n", type: :muted)
302
+ end
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
+
326
+ result
327
+ end
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) != File.read(target_config)
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}"
225
373
  end
226
374
 
227
375
  def handle_success(issue:, slug:, branch_name:, base_branch:, plan_data:, working_dir:)
228
- stage_and_commit(issue, working_dir: working_dir)
376
+ changes_committed = stage_and_commit(issue, working_dir: working_dir)
377
+
378
+ unless changes_committed
379
+ handle_no_changes(issue: issue, slug: slug, branch_name: branch_name, working_dir: working_dir)
380
+ return
381
+ end
229
382
 
230
383
  # Check if PR should be created based on VCS preferences
384
+ # For watch mode, default to creating PRs (set to false to disable)
231
385
  vcs_config = config.dig(:work_loop, :version_control) || {}
232
- auto_create_pr = vcs_config.fetch(:auto_create_pr, false)
233
-
234
- pr_url = if auto_create_pr
386
+ auto_create_pr = vcs_config.fetch(: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
+ )
235
407
  create_pull_request(issue: issue, branch_name: branch_name, base_branch: base_branch, working_dir: working_dir)
236
408
  else
237
409
  display_message("ℹ️ Skipping PR creation (disabled in VCS preferences)", type: :muted)
@@ -257,45 +429,189 @@ module Aidp
257
429
  )
258
430
  display_message("🎉 Posted completion comment for issue ##{issue[:number]}", type: :success)
259
431
 
432
+ # Remove build label after successful completion
433
+ begin
434
+ @repository_client.remove_labels(issue[:number], @build_label)
435
+ display_message("🏷️ Removed '#{@build_label}' label after completion", type: :info)
436
+ rescue => e
437
+ display_message("⚠️ Failed to remove build label: #{e.message}", type: :warn)
438
+ # Don't fail the process if label removal fails
439
+ end
440
+
260
441
  # Keep workstream for review - don't auto-cleanup on success
261
442
  if @use_workstreams
262
443
  display_message("ℹ️ Workstream #{slug} preserved for review. Remove with: aidp ws rm #{slug}", type: :muted)
263
444
  end
264
445
  end
265
446
 
447
+ def handle_clarification_request(issue:, slug:, result:)
448
+ questions = result[:clarification_questions] || []
449
+ workstream_note = @use_workstreams ? " The workstream `#{slug}` has been preserved." : " The branch has been preserved."
450
+
451
+ # Build comment with questions
452
+ comment_parts = []
453
+ comment_parts << "❓ Implementation needs clarification for ##{issue[:number]}."
454
+ comment_parts << ""
455
+ comment_parts << "The AI agent needs additional information to proceed with implementation:"
456
+ comment_parts << ""
457
+ questions.each_with_index do |question, index|
458
+ comment_parts << "#{index + 1}. #{question}"
459
+ end
460
+ comment_parts << ""
461
+ comment_parts << "**Next Steps**: Please reply with answers to the questions above. Once resolved, remove the `#{@needs_input_label}` label and add the `#{@build_label}` label to resume implementation."
462
+ comment_parts << ""
463
+ comment_parts << workstream_note.to_s
464
+
465
+ comment = comment_parts.join("\n")
466
+ @repository_client.post_comment(issue[:number], comment)
467
+
468
+ # Update labels: remove build trigger, add needs input
469
+ begin
470
+ @repository_client.replace_labels(
471
+ issue[:number],
472
+ old_labels: [@build_label],
473
+ new_labels: [@needs_input_label]
474
+ )
475
+ display_message("🏷️ Updated labels: removed '#{@build_label}', added '#{@needs_input_label}' (needs clarification)", type: :info)
476
+ rescue => e
477
+ display_message("⚠️ Failed to update labels for issue ##{issue[:number]}: #{e.message}", type: :warn)
478
+ end
479
+
480
+ @state_store.record_build_status(
481
+ issue[:number],
482
+ status: "needs_clarification",
483
+ details: {questions: questions, workstream: slug}
484
+ )
485
+ display_message("💬 Posted clarification request for issue ##{issue[:number]}", type: :success)
486
+ end
487
+
266
488
  def handle_failure(issue:, slug:, result:)
267
489
  message = result[:message] || "Unknown failure"
490
+ error_info = result[:error] || result[:error_details]
268
491
  workstream_note = @use_workstreams ? " The workstream `#{slug}` has been left intact for debugging." : " The branch has been left intact for debugging."
492
+
493
+ # Build detailed error message for the comment
494
+ error_details_section = if error_info
495
+ "\nError: #{error_info}"
496
+ else
497
+ ""
498
+ end
499
+
269
500
  comment = <<~COMMENT
270
501
  ❌ Implementation attempt for ##{issue[:number]} failed.
271
502
 
272
503
  Status: #{result[:status]}
273
- Details: #{message}
504
+ Details: #{message}#{error_details_section}
274
505
 
275
506
  Please review the repository for partial changes.#{workstream_note}
276
507
  COMMENT
277
508
  @repository_client.post_comment(issue[:number], comment)
509
+
510
+ # Log the failure with full details
511
+ Aidp.log_error(
512
+ "build_processor",
513
+ "Build failed for issue ##{issue[:number]}",
514
+ status: result[:status],
515
+ message: message,
516
+ error: error_info&.to_s,
517
+ workstream: slug
518
+ )
519
+
278
520
  @state_store.record_build_status(
279
521
  issue[:number],
280
522
  status: "failed",
281
- details: {message: message, workstream: slug}
523
+ details: {message: message, error: error_info&.to_s, workstream: slug}
282
524
  )
283
525
  display_message("⚠️ Build failure recorded for issue ##{issue[:number]}", type: :warn)
284
526
  end
285
527
 
528
+ def handle_no_changes(issue:, slug:, branch_name:, working_dir:)
529
+ location_note = if @use_workstreams
530
+ "The workstream `#{slug}` has been preserved for review."
531
+ else
532
+ "Branch `#{branch_name}` remains checked out for inspection."
533
+ end
534
+
535
+ @state_store.record_build_status(
536
+ issue[:number],
537
+ status: "no_changes",
538
+ details: {branch: branch_name, workstream: slug}
539
+ )
540
+
541
+ Aidp.log_warn(
542
+ "build_processor",
543
+ "noop_build_result",
544
+ issue: issue[:number],
545
+ branch: branch_name,
546
+ workstream: slug
547
+ )
548
+
549
+ display_message("⚠️ Implementation produced no changes; labels remain untouched. #{location_note}", type: :warn)
550
+ enqueue_decider_followup(working_dir)
551
+ end
552
+
553
+ def handle_incomplete_criteria(issue:, slug:, branch_name:, working_dir:, metadata:)
554
+ display_message("⚠️ Completion criteria unmet; scheduling additional fix-forward iteration.", type: :warn)
555
+ enqueue_decider_followup(working_dir)
556
+
557
+ @state_store.record_build_status(
558
+ issue[:number],
559
+ status: "pending_fix_forward",
560
+ details: {branch: branch_name, workstream: slug, criteria: metadata}
561
+ )
562
+
563
+ Aidp.log_info(
564
+ "build_processor",
565
+ "pending_fix_forward",
566
+ issue: issue[:number],
567
+ branch: branch_name,
568
+ workstream: slug,
569
+ criteria: metadata
570
+ )
571
+ end
572
+
286
573
  def stage_and_commit(issue, working_dir: @project_dir)
574
+ commit_created = false
575
+
287
576
  Dir.chdir(working_dir) do
288
577
  status_output = run_git(%w[status --porcelain])
289
578
  if status_output.strip.empty?
290
579
  display_message("ℹ️ No file changes detected after work loop.", type: :muted)
291
- return
580
+ Aidp.log_info("build_processor", "no_changes_after_work_loop", issue: issue[:number], working_dir: working_dir)
581
+ return commit_created
292
582
  end
293
583
 
584
+ changed_entries = status_output.lines.map(&:strip).reject(&:empty?)
585
+ Aidp.log_info(
586
+ "build_processor",
587
+ "changes_detected_after_work_loop",
588
+ issue: issue[:number],
589
+ working_dir: working_dir,
590
+ changed_file_count: changed_entries.length,
591
+ changed_files_sample: changed_entries.first(10)
592
+ )
593
+
294
594
  run_git(%w[add -A])
295
595
  commit_message = build_commit_message(issue)
296
596
  run_git(["commit", "-m", commit_message])
297
597
  display_message("💾 Created commit: #{commit_message.lines.first.strip}", type: :info)
598
+ Aidp.log_info(
599
+ "build_processor",
600
+ "commit_created",
601
+ working_dir: working_dir,
602
+ issue: issue[:number],
603
+ commit_summary: commit_message.lines.first.strip
604
+ )
605
+
606
+ # Push the branch to remote
607
+ current_branch = run_git(%w[branch --show-current]).strip
608
+ run_git(["push", "-u", "origin", current_branch])
609
+ display_message("⬆️ Pushed branch '#{current_branch}' to remote", type: :info)
610
+ Aidp.log_info("build_processor", "branch_pushed", branch: current_branch, working_dir: working_dir)
611
+ commit_created = true
298
612
  end
613
+
614
+ commit_created
299
615
  end
300
616
 
301
617
  def build_commit_message(issue)
@@ -353,6 +669,7 @@ module Aidp
353
669
  body = <<~BODY
354
670
  ## Summary
355
671
  - Automated resolution for ##{issue[:number]}
672
+ - Fixes ##{issue[:number]}
356
673
 
357
674
  ## Testing
358
675
  #{test_summary}
@@ -363,16 +680,29 @@ module Aidp
363
680
  pr_strategy = vcs_config[:pr_strategy] || "draft"
364
681
  draft = (pr_strategy == "draft")
365
682
 
683
+ # Assign PR to the issue author
684
+ assignee = issue[:author]
685
+
366
686
  output = @repository_client.create_pull_request(
367
687
  title: title,
368
688
  body: body,
369
689
  head: branch_name,
370
690
  base: base_branch,
371
691
  issue_number: issue[:number],
372
- draft: draft
692
+ draft: draft,
693
+ assignee: assignee
373
694
  )
374
695
 
375
- extract_pr_url(output)
696
+ pr_url = extract_pr_url(output)
697
+ Aidp.log_info(
698
+ "build_processor",
699
+ "pull_request_created",
700
+ issue: issue[:number],
701
+ branch: branch_name,
702
+ base_branch: base_branch,
703
+ pr_url: pr_url
704
+ )
705
+ pr_url
376
706
  end
377
707
 
378
708
  def gather_test_summary(working_dir: @project_dir)
@@ -26,8 +26,9 @@ module Aidp
26
26
  Focus on concrete engineering tasks. Ensure questions are actionable.
27
27
  PROMPT
28
28
 
29
- def initialize(provider_name: nil)
29
+ def initialize(provider_name: nil, verbose: false)
30
30
  @provider_name = provider_name
31
+ @verbose = verbose
31
32
  end
32
33
 
33
34
  def generate(issue)
@@ -67,7 +68,21 @@ module Aidp
67
68
 
68
69
  def generate_with_provider(provider, issue)
69
70
  payload = build_prompt(issue)
71
+
72
+ if @verbose
73
+ display_message("\n--- Plan Generation Prompt ---", type: :muted)
74
+ display_message(payload.strip, type: :muted)
75
+ display_message("--- End Prompt ---\n", type: :muted)
76
+ end
77
+
70
78
  response = provider.send_message(prompt: payload)
79
+
80
+ if @verbose
81
+ display_message("\n--- Provider Response ---", type: :muted)
82
+ display_message(response.strip, type: :muted)
83
+ display_message("--- End Response ---\n", type: :muted)
84
+ end
85
+
71
86
  parsed = parse_structured_response(response)
72
87
 
73
88
  return parsed if parsed
@@ -11,13 +11,32 @@ module Aidp
11
11
  class PlanProcessor
12
12
  include Aidp::MessageDisplay
13
13
 
14
- PLAN_LABEL = "aidp-plan"
14
+ # Default label names
15
+ DEFAULT_PLAN_LABEL = "aidp-plan"
16
+ DEFAULT_NEEDS_INPUT_LABEL = "aidp-needs-input"
17
+ DEFAULT_READY_LABEL = "aidp-ready"
18
+ DEFAULT_BUILD_LABEL = "aidp-build"
19
+
15
20
  COMMENT_HEADER = "## 🤖 AIDP Plan Proposal"
16
21
 
17
- def initialize(repository_client:, state_store:, plan_generator:)
22
+ attr_reader :plan_label, :needs_input_label, :ready_label, :build_label
23
+
24
+ def initialize(repository_client:, state_store:, plan_generator:, label_config: {})
18
25
  @repository_client = repository_client
19
26
  @state_store = state_store
20
27
  @plan_generator = plan_generator
28
+
29
+ # Load label configuration with defaults
30
+ @plan_label = label_config[:plan_trigger] || label_config["plan_trigger"] || DEFAULT_PLAN_LABEL
31
+ @needs_input_label = label_config[:needs_input] || label_config["needs_input"] || DEFAULT_NEEDS_INPUT_LABEL
32
+ @ready_label = label_config[:ready_to_build] || label_config["ready_to_build"] || DEFAULT_READY_LABEL
33
+ @build_label = label_config[:build_trigger] || label_config["build_trigger"] || DEFAULT_BUILD_LABEL
34
+ end
35
+
36
+ # For backward compatibility
37
+ def self.plan_label_from_config(config)
38
+ labels = config[:labels] || config["labels"] || {}
39
+ labels[:plan_trigger] || labels["plan_trigger"] || DEFAULT_PLAN_LABEL
21
40
  end
22
41
 
23
42
  def process(issue)
@@ -35,14 +54,39 @@ module Aidp
35
54
 
36
55
  display_message("💬 Posted plan comment for issue ##{number}", type: :success)
37
56
  @state_store.record_plan(number, plan_data.merge(comment_body: comment_body, comment_hint: COMMENT_HEADER))
57
+
58
+ # Update labels: remove plan trigger, add appropriate status label
59
+ update_labels_after_plan(number, plan_data)
38
60
  end
39
61
 
40
62
  private
41
63
 
64
+ def update_labels_after_plan(number, plan_data)
65
+ questions = Array(plan_data[:questions])
66
+ has_questions = questions.any? && !questions.all? { |q| q.to_s.strip.empty? }
67
+
68
+ # Determine which label to add based on whether there are questions
69
+ new_label = has_questions ? @needs_input_label : @ready_label
70
+ status_text = has_questions ? "needs input" : "ready to build"
71
+
72
+ begin
73
+ @repository_client.replace_labels(
74
+ number,
75
+ old_labels: [@plan_label],
76
+ new_labels: [new_label]
77
+ )
78
+ display_message("🏷️ Updated labels: removed '#{@plan_label}', added '#{new_label}' (#{status_text})", type: :info)
79
+ rescue => e
80
+ display_message("⚠️ Failed to update labels for issue ##{number}: #{e.message}", type: :warn)
81
+ # Don't fail the whole process if label update fails
82
+ end
83
+ end
84
+
42
85
  def build_comment(issue:, plan:)
43
86
  summary = plan[:summary].to_s.strip
44
87
  tasks = Array(plan[:tasks])
45
88
  questions = Array(plan[:questions])
89
+ has_questions = questions.any? && !questions.all? { |q| q.to_s.strip.empty? }
46
90
 
47
91
  parts = []
48
92
  parts << COMMENT_HEADER
@@ -59,7 +103,14 @@ module Aidp
59
103
  parts << "### Clarifying Questions"
60
104
  parts << format_numbered(questions, placeholder: "_No questions identified_")
61
105
  parts << ""
62
- parts << "Please reply inline with answers to the questions above. Once the discussion is resolved, apply the `aidp-build` label to begin implementation."
106
+
107
+ # Add instructions based on whether there are questions
108
+ parts << if has_questions
109
+ "**Next Steps**: Please reply with answers to the questions above. Once resolved, remove the `#{@needs_input_label}` label and add the `#{@build_label}` label to begin implementation."
110
+ else
111
+ "**Next Steps**: This plan is ready for implementation. Add the `#{@build_label}` label to begin."
112
+ end
113
+
63
114
  parts.join("\n")
64
115
  end
65
116