aidp 0.23.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.
- checksums.yaml +4 -4
- data/lib/aidp/cli.rb +3 -0
- data/lib/aidp/execute/work_loop_runner.rb +252 -45
- data/lib/aidp/execute/work_loop_unit_scheduler.rb +27 -2
- data/lib/aidp/harness/condition_detector.rb +42 -8
- data/lib/aidp/harness/config_manager.rb +7 -0
- data/lib/aidp/harness/config_schema.rb +25 -0
- data/lib/aidp/harness/configuration.rb +69 -6
- data/lib/aidp/harness/error_handler.rb +117 -44
- data/lib/aidp/harness/provider_manager.rb +64 -0
- data/lib/aidp/harness/provider_metrics.rb +138 -0
- data/lib/aidp/harness/runner.rb +90 -29
- data/lib/aidp/harness/simple_user_interface.rb +4 -0
- data/lib/aidp/harness/state/ui_state.rb +0 -10
- data/lib/aidp/harness/state_manager.rb +1 -15
- data/lib/aidp/harness/test_runner.rb +39 -2
- data/lib/aidp/logger.rb +34 -4
- data/lib/aidp/providers/adapter.rb +241 -0
- data/lib/aidp/providers/anthropic.rb +75 -7
- data/lib/aidp/providers/base.rb +29 -1
- data/lib/aidp/providers/capability_registry.rb +205 -0
- data/lib/aidp/providers/codex.rb +14 -0
- data/lib/aidp/providers/error_taxonomy.rb +195 -0
- data/lib/aidp/providers/gemini.rb +3 -2
- data/lib/aidp/setup/provider_registry.rb +107 -0
- data/lib/aidp/setup/wizard.rb +115 -31
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +263 -23
- data/lib/aidp/watch/repository_client.rb +4 -4
- data/lib/aidp/watch/runner.rb +37 -5
- data/lib/aidp/workflows/guided_agent.rb +53 -0
- data/lib/aidp/worktree.rb +67 -10
- data/templates/work_loop/decide_whats_next.md +21 -0
- data/templates/work_loop/diagnose_failures.md +21 -0
- metadata +10 -3
- /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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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,
|
|
163
|
-
|
|
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,20 +294,116 @@ 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) != 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}"
|
|
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)
|
|
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
|
|
269
382
|
|
|
270
383
|
# Check if PR should be created based on VCS preferences
|
|
384
|
+
# For watch mode, default to creating PRs (set to false to disable)
|
|
271
385
|
vcs_config = config.dig(:work_loop, :version_control) || {}
|
|
272
|
-
auto_create_pr = vcs_config.fetch(:auto_create_pr,
|
|
273
|
-
|
|
274
|
-
pr_url = if
|
|
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
|
+
)
|
|
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)
|
|
@@ -355,37 +487,131 @@ module Aidp
|
|
|
355
487
|
|
|
356
488
|
def handle_failure(issue:, slug:, result:)
|
|
357
489
|
message = result[:message] || "Unknown failure"
|
|
490
|
+
error_info = result[:error] || result[:error_details]
|
|
358
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
|
+
|
|
359
500
|
comment = <<~COMMENT
|
|
360
501
|
❌ Implementation attempt for ##{issue[:number]} failed.
|
|
361
502
|
|
|
362
503
|
Status: #{result[:status]}
|
|
363
|
-
Details: #{message}
|
|
504
|
+
Details: #{message}#{error_details_section}
|
|
364
505
|
|
|
365
506
|
Please review the repository for partial changes.#{workstream_note}
|
|
366
507
|
COMMENT
|
|
367
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
|
+
|
|
368
520
|
@state_store.record_build_status(
|
|
369
521
|
issue[:number],
|
|
370
522
|
status: "failed",
|
|
371
|
-
details: {message: message, workstream: slug}
|
|
523
|
+
details: {message: message, error: error_info&.to_s, workstream: slug}
|
|
372
524
|
)
|
|
373
525
|
display_message("⚠️ Build failure recorded for issue ##{issue[:number]}", type: :warn)
|
|
374
526
|
end
|
|
375
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
|
+
|
|
376
573
|
def stage_and_commit(issue, working_dir: @project_dir)
|
|
574
|
+
commit_created = false
|
|
575
|
+
|
|
377
576
|
Dir.chdir(working_dir) do
|
|
378
577
|
status_output = run_git(%w[status --porcelain])
|
|
379
578
|
if status_output.strip.empty?
|
|
380
579
|
display_message("ℹ️ No file changes detected after work loop.", type: :muted)
|
|
381
|
-
|
|
580
|
+
Aidp.log_info("build_processor", "no_changes_after_work_loop", issue: issue[:number], working_dir: working_dir)
|
|
581
|
+
return commit_created
|
|
382
582
|
end
|
|
383
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
|
+
|
|
384
594
|
run_git(%w[add -A])
|
|
385
595
|
commit_message = build_commit_message(issue)
|
|
386
596
|
run_git(["commit", "-m", commit_message])
|
|
387
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
|
|
388
612
|
end
|
|
613
|
+
|
|
614
|
+
commit_created
|
|
389
615
|
end
|
|
390
616
|
|
|
391
617
|
def build_commit_message(issue)
|
|
@@ -443,6 +669,7 @@ module Aidp
|
|
|
443
669
|
body = <<~BODY
|
|
444
670
|
## Summary
|
|
445
671
|
- Automated resolution for ##{issue[:number]}
|
|
672
|
+
- Fixes ##{issue[:number]}
|
|
446
673
|
|
|
447
674
|
## Testing
|
|
448
675
|
#{test_summary}
|
|
@@ -453,16 +680,29 @@ module Aidp
|
|
|
453
680
|
pr_strategy = vcs_config[:pr_strategy] || "draft"
|
|
454
681
|
draft = (pr_strategy == "draft")
|
|
455
682
|
|
|
683
|
+
# Assign PR to the issue author
|
|
684
|
+
assignee = issue[:author]
|
|
685
|
+
|
|
456
686
|
output = @repository_client.create_pull_request(
|
|
457
687
|
title: title,
|
|
458
688
|
body: body,
|
|
459
689
|
head: branch_name,
|
|
460
690
|
base: base_branch,
|
|
461
691
|
issue_number: issue[:number],
|
|
462
|
-
draft: draft
|
|
692
|
+
draft: draft,
|
|
693
|
+
assignee: assignee
|
|
463
694
|
)
|
|
464
695
|
|
|
465
|
-
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
|
|
466
706
|
end
|
|
467
707
|
|
|
468
708
|
def gather_test_summary(working_dir: @project_dir)
|
|
@@ -61,8 +61,8 @@ module Aidp
|
|
|
61
61
|
gh_available? ? post_comment_via_gh(number, body) : post_comment_via_api(number, body)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
def create_pull_request(title:, body:, head:, base:, issue_number:)
|
|
65
|
-
gh_available? ? create_pull_request_via_gh(title: title, body: body, head: head, base: base, issue_number: issue_number) : raise("GitHub CLI not available - cannot create PR")
|
|
64
|
+
def create_pull_request(title:, body:, head:, base:, issue_number:, draft: false, assignee: nil)
|
|
65
|
+
gh_available? ? create_pull_request_via_gh(title: title, body: body, head: head, base: base, issue_number: issue_number, draft: draft, assignee: assignee) : raise("GitHub CLI not available - cannot create PR")
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def add_labels(number, *labels)
|
|
@@ -176,7 +176,7 @@ module Aidp
|
|
|
176
176
|
response.body
|
|
177
177
|
end
|
|
178
178
|
|
|
179
|
-
def create_pull_request_via_gh(title:, body:, head:, base:, issue_number:, draft: false)
|
|
179
|
+
def create_pull_request_via_gh(title:, body:, head:, base:, issue_number:, draft: false, assignee: nil)
|
|
180
180
|
cmd = [
|
|
181
181
|
"gh", "pr", "create",
|
|
182
182
|
"--repo", full_repo,
|
|
@@ -185,8 +185,8 @@ module Aidp
|
|
|
185
185
|
"--head", head,
|
|
186
186
|
"--base", base
|
|
187
187
|
]
|
|
188
|
-
cmd += ["--issue", issue_number.to_s] if issue_number
|
|
189
188
|
cmd += ["--draft"] if draft
|
|
189
|
+
cmd += ["--assignee", assignee] if assignee
|
|
190
190
|
|
|
191
191
|
stdout, stderr, status = Open3.capture3(*cmd)
|
|
192
192
|
raise "Failed to create PR via gh: #{stderr.strip}" unless status.success?
|
data/lib/aidp/watch/runner.rb
CHANGED
|
@@ -58,12 +58,25 @@ module Aidp
|
|
|
58
58
|
# Validate safety requirements before starting
|
|
59
59
|
@safety_checker.validate_watch_mode_safety!(force: @force)
|
|
60
60
|
|
|
61
|
+
Aidp.log_info(
|
|
62
|
+
"watch_runner",
|
|
63
|
+
"watch_mode_started",
|
|
64
|
+
repo: @repository_client.full_repo,
|
|
65
|
+
interval: @interval,
|
|
66
|
+
once: @once,
|
|
67
|
+
use_workstreams: @use_workstreams,
|
|
68
|
+
verbose: @verbose
|
|
69
|
+
)
|
|
70
|
+
|
|
61
71
|
display_message("👀 Watch mode enabled for #{@repository_client.full_repo}", type: :highlight)
|
|
62
72
|
display_message("Polling every #{@interval} seconds. Press Ctrl+C to stop.", type: :muted)
|
|
63
73
|
|
|
64
74
|
loop do
|
|
75
|
+
Aidp.log_debug("watch_runner", "poll_cycle.begin", repo: @repository_client.full_repo, interval: @interval)
|
|
65
76
|
process_cycle
|
|
77
|
+
Aidp.log_debug("watch_runner", "poll_cycle.complete", once: @once, next_poll_in: @once ? nil : @interval)
|
|
66
78
|
break if @once
|
|
79
|
+
Aidp.log_debug("watch_runner", "poll_cycle.sleep", seconds: @interval)
|
|
67
80
|
sleep @interval
|
|
68
81
|
end
|
|
69
82
|
rescue Interrupt
|
|
@@ -83,14 +96,22 @@ module Aidp
|
|
|
83
96
|
def process_plan_triggers
|
|
84
97
|
plan_label = @plan_processor.plan_label
|
|
85
98
|
issues = @repository_client.list_issues(labels: [plan_label], state: "open")
|
|
99
|
+
Aidp.log_debug("watch_runner", "plan_poll", label: plan_label, total: issues.size)
|
|
86
100
|
issues.each do |issue|
|
|
87
|
-
|
|
101
|
+
unless issue_has_label?(issue, plan_label)
|
|
102
|
+
Aidp.log_debug("watch_runner", "plan_skip_label_mismatch", issue: issue[:number], labels: issue[:labels])
|
|
103
|
+
next
|
|
104
|
+
end
|
|
88
105
|
|
|
89
106
|
detailed = @repository_client.fetch_issue(issue[:number])
|
|
90
107
|
|
|
91
108
|
# Check author authorization before processing
|
|
92
|
-
|
|
109
|
+
unless @safety_checker.should_process_issue?(detailed, enforce: false)
|
|
110
|
+
Aidp.log_debug("watch_runner", "plan_skip_unauthorized_author", issue: detailed[:number], author: detailed[:author])
|
|
111
|
+
next
|
|
112
|
+
end
|
|
93
113
|
|
|
114
|
+
Aidp.log_debug("watch_runner", "plan_process", issue: detailed[:number])
|
|
94
115
|
@plan_processor.process(detailed)
|
|
95
116
|
rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
|
|
96
117
|
Aidp.log_warn("watch_runner", "unauthorized issue author", issue: issue[:number], error: e.message)
|
|
@@ -100,17 +121,28 @@ module Aidp
|
|
|
100
121
|
def process_build_triggers
|
|
101
122
|
build_label = @build_processor.build_label
|
|
102
123
|
issues = @repository_client.list_issues(labels: [build_label], state: "open")
|
|
124
|
+
Aidp.log_debug("watch_runner", "build_poll", label: build_label, total: issues.size)
|
|
103
125
|
issues.each do |issue|
|
|
104
|
-
|
|
126
|
+
unless issue_has_label?(issue, build_label)
|
|
127
|
+
Aidp.log_debug("watch_runner", "build_skip_label_mismatch", issue: issue[:number], labels: issue[:labels])
|
|
128
|
+
next
|
|
129
|
+
end
|
|
105
130
|
|
|
106
131
|
status = @state_store.build_status(issue[:number])
|
|
107
|
-
|
|
132
|
+
if status["status"] == "completed"
|
|
133
|
+
Aidp.log_debug("watch_runner", "build_skip_completed", issue: issue[:number])
|
|
134
|
+
next
|
|
135
|
+
end
|
|
108
136
|
|
|
109
137
|
detailed = @repository_client.fetch_issue(issue[:number])
|
|
110
138
|
|
|
111
139
|
# Check author authorization before processing
|
|
112
|
-
|
|
140
|
+
unless @safety_checker.should_process_issue?(detailed, enforce: false)
|
|
141
|
+
Aidp.log_debug("watch_runner", "build_skip_unauthorized_author", issue: detailed[:number], author: detailed[:author])
|
|
142
|
+
next
|
|
143
|
+
end
|
|
113
144
|
|
|
145
|
+
Aidp.log_debug("watch_runner", "build_process", issue: detailed[:number])
|
|
114
146
|
@build_processor.process(detailed)
|
|
115
147
|
rescue RepositorySafetyChecker::UnauthorizedAuthorError => e
|
|
116
148
|
Aidp.log_warn("watch_runner", "unauthorized issue author", issue: issue[:number], error: e.message)
|
|
@@ -229,16 +229,34 @@ module Aidp
|
|
|
229
229
|
end
|
|
230
230
|
|
|
231
231
|
combined_prompt = "#{system_prompt}\n\n#{user_prompt}"
|
|
232
|
+
|
|
233
|
+
# Record timing for metrics
|
|
234
|
+
start_time = Time.now
|
|
232
235
|
result = provider.send_message(prompt: combined_prompt)
|
|
236
|
+
duration = Time.now - start_time
|
|
233
237
|
|
|
234
238
|
if result.nil? || result.empty?
|
|
239
|
+
# Record failed metric
|
|
240
|
+
@provider_manager.record_metrics(provider_name, success: false, duration: duration, error: StandardError.new("Empty response")) if @provider_manager.respond_to?(:record_metrics)
|
|
235
241
|
raise ConversationError, "Provider request failed: empty response"
|
|
236
242
|
end
|
|
237
243
|
|
|
244
|
+
# Record successful metric
|
|
245
|
+
@provider_manager.record_metrics(provider_name, success: true, duration: duration) if @provider_manager.respond_to?(:record_metrics)
|
|
246
|
+
|
|
238
247
|
result
|
|
239
248
|
rescue => e
|
|
240
249
|
message = e.message.to_s
|
|
241
250
|
|
|
251
|
+
# Record failed metric (if we haven't already from empty response check)
|
|
252
|
+
if defined?(start_time) && defined?(provider_name) && defined?(duration).nil?
|
|
253
|
+
duration = Time.now - start_time
|
|
254
|
+
@provider_manager.record_metrics(provider_name, success: false, duration: duration, error: e) if @provider_manager.respond_to?(:record_metrics)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Extract rate limit reset time from error message (especially Claude)
|
|
258
|
+
extract_and_save_rate_limit_info(provider_name, message) if defined?(provider_name)
|
|
259
|
+
|
|
242
260
|
# Classify error type for better handling
|
|
243
261
|
classified = if message =~ /resource[_ ]exhausted/i || message =~ /\[resource_exhausted\]/i
|
|
244
262
|
"resource_exhausted"
|
|
@@ -319,6 +337,41 @@ module Aidp
|
|
|
319
337
|
Aidp.logger.warn("guided_agent", "Failed verbose iteration emit", error: e.message)
|
|
320
338
|
end
|
|
321
339
|
|
|
340
|
+
# Extract rate limit reset time from error message and save to provider manager
|
|
341
|
+
def extract_and_save_rate_limit_info(provider_name, error_message)
|
|
342
|
+
return unless @provider_manager.respond_to?(:mark_rate_limited)
|
|
343
|
+
return unless error_message
|
|
344
|
+
|
|
345
|
+
# Claude error format: "please try again in 1m23s" or "retry after X seconds/minutes"
|
|
346
|
+
reset_time = nil
|
|
347
|
+
|
|
348
|
+
if error_message =~ /try again in (\d+)m(\d+)s/i
|
|
349
|
+
minutes = $1.to_i
|
|
350
|
+
seconds = $2.to_i
|
|
351
|
+
reset_time = Time.now + (minutes * 60) + seconds
|
|
352
|
+
elsif error_message =~ /try again in (\d+)s/i
|
|
353
|
+
seconds = $1.to_i
|
|
354
|
+
reset_time = Time.now + seconds
|
|
355
|
+
elsif error_message =~ /try again in (\d+)m/i
|
|
356
|
+
minutes = $1.to_i
|
|
357
|
+
reset_time = Time.now + (minutes * 60)
|
|
358
|
+
elsif error_message =~ /retry after (\d+) seconds?/i
|
|
359
|
+
seconds = $1.to_i
|
|
360
|
+
reset_time = Time.now + seconds
|
|
361
|
+
elsif error_message =~ /retry after (\d+) minutes?/i
|
|
362
|
+
minutes = $1.to_i
|
|
363
|
+
reset_time = Time.now + (minutes * 60)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Mark provider as rate limited with extracted reset time
|
|
367
|
+
if reset_time
|
|
368
|
+
@provider_manager.mark_rate_limited(provider_name, reset_time)
|
|
369
|
+
Aidp.logger.info("guided_agent", "Extracted rate limit reset time", provider: provider_name, reset_at: reset_time.iso8601)
|
|
370
|
+
end
|
|
371
|
+
rescue => e
|
|
372
|
+
Aidp.logger.warn("guided_agent", "Failed to extract rate limit info", error: e.message)
|
|
373
|
+
end
|
|
374
|
+
|
|
322
375
|
def validate_provider_configuration!
|
|
323
376
|
configured = @provider_manager.configured_providers
|
|
324
377
|
if configured.nil? || configured.empty?
|