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.
- checksums.yaml +4 -4
- data/README.md +145 -31
- data/lib/aidp/cli.rb +19 -2
- 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 +110 -35
- 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/devcontainer/backup_manager.rb +11 -4
- data/lib/aidp/setup/provider_registry.rb +107 -0
- data/lib/aidp/setup/wizard.rb +189 -31
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +357 -27
- data/lib/aidp/watch/plan_generator.rb +16 -1
- data/lib/aidp/watch/plan_processor.rb +54 -3
- data/lib/aidp/watch/repository_client.rb +78 -4
- data/lib/aidp/watch/repository_safety_checker.rb +12 -3
- data/lib/aidp/watch/runner.rb +52 -10
- 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
|
|
@@ -15,14 +18,22 @@ module Aidp
|
|
|
15
18
|
class BuildProcessor
|
|
16
19
|
include Aidp::MessageDisplay
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
DEFAULT_BUILD_LABEL = "aidp-build"
|
|
22
|
+
DEFAULT_NEEDS_INPUT_LABEL = "aidp-needs-input"
|
|
19
23
|
IMPLEMENTATION_STEP = "16_IMPLEMENTATION"
|
|
20
24
|
|
|
21
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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,
|
|
153
|
-
|
|
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,
|
|
233
|
-
|
|
234
|
-
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
|
+
)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|