aidp 0.21.1 → 0.23.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/devcontainer_commands.rb +501 -0
- data/lib/aidp/cli/issue_importer.rb +15 -3
- data/lib/aidp/cli.rb +107 -2
- data/lib/aidp/execute/prompt_manager.rb +16 -2
- data/lib/aidp/execute/runner.rb +10 -5
- data/lib/aidp/execute/work_loop_runner.rb +3 -3
- data/lib/aidp/harness/runner.rb +20 -6
- data/lib/aidp/harness/state/persistence.rb +12 -1
- data/lib/aidp/harness/state_manager.rb +13 -1
- data/lib/aidp/jobs/background_runner.rb +3 -1
- data/lib/aidp/logger.rb +41 -5
- data/lib/aidp/safe_directory.rb +87 -0
- data/lib/aidp/setup/devcontainer/backup_manager.rb +182 -0
- data/lib/aidp/setup/devcontainer/generator.rb +409 -0
- data/lib/aidp/setup/devcontainer/parser.rb +249 -0
- data/lib/aidp/setup/devcontainer/port_manager.rb +286 -0
- data/lib/aidp/setup/wizard.rb +219 -0
- data/lib/aidp/storage/csv_storage.rb +39 -2
- data/lib/aidp/storage/file_manager.rb +28 -3
- data/lib/aidp/storage/json_storage.rb +41 -2
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +94 -4
- 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 +74 -0
- data/lib/aidp/watch/repository_safety_checker.rb +12 -3
- data/lib/aidp/watch/runner.rb +17 -7
- data/lib/aidp/workflows/guided_agent.rb +8 -42
- metadata +7 -1
|
@@ -11,7 +11,7 @@ module Aidp
|
|
|
11
11
|
include Aidp::RescueLogging
|
|
12
12
|
|
|
13
13
|
def initialize(base_dir = ".aidp")
|
|
14
|
-
@base_dir = base_dir
|
|
14
|
+
@base_dir = sanitize_base_dir(base_dir)
|
|
15
15
|
ensure_directory_exists
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -156,7 +156,46 @@ module Aidp
|
|
|
156
156
|
end
|
|
157
157
|
|
|
158
158
|
def ensure_directory_exists
|
|
159
|
-
|
|
159
|
+
return if Dir.exist?(@base_dir)
|
|
160
|
+
begin
|
|
161
|
+
FileUtils.mkdir_p(@base_dir)
|
|
162
|
+
rescue SystemCallError => e
|
|
163
|
+
# Fallback when directory creation fails (e.g., attempting to write to '/.aidp')
|
|
164
|
+
fallback = begin
|
|
165
|
+
home = Dir.respond_to?(:home) ? Dir.home : nil
|
|
166
|
+
if home && !home.empty? && File.writable?(home)
|
|
167
|
+
File.join(home, ".aidp")
|
|
168
|
+
else
|
|
169
|
+
File.join(Dir.tmpdir, "aidp_storage")
|
|
170
|
+
end
|
|
171
|
+
rescue
|
|
172
|
+
File.join(Dir.tmpdir, "aidp_storage")
|
|
173
|
+
end
|
|
174
|
+
Kernel.warn "[AIDP Storage] Cannot create base directory #{@base_dir}: #{e.class}: #{e.message}. Using fallback #{fallback}"
|
|
175
|
+
@base_dir = fallback
|
|
176
|
+
begin
|
|
177
|
+
FileUtils.mkdir_p(@base_dir) unless Dir.exist?(@base_dir)
|
|
178
|
+
rescue SystemCallError => e2
|
|
179
|
+
Kernel.warn "[AIDP Storage] Fallback directory creation also failed: #{e2.class}: #{e2.message}. Continuing without persistent JSON storage."
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def sanitize_base_dir(dir)
|
|
185
|
+
return Dir.pwd if dir.nil? || dir.to_s.strip.empty?
|
|
186
|
+
str = dir.to_s
|
|
187
|
+
# If given root '/', redirect to a writable location to avoid EACCES on CI
|
|
188
|
+
if str == File::SEPARATOR
|
|
189
|
+
fallback = begin
|
|
190
|
+
home = Dir.home
|
|
191
|
+
(home && !home.empty? && File.writable?(home)) ? File.join(home, ".aidp") : File.join(Dir.tmpdir, "aidp_storage")
|
|
192
|
+
rescue
|
|
193
|
+
File.join(Dir.tmpdir, "aidp_storage")
|
|
194
|
+
end
|
|
195
|
+
Kernel.warn "[AIDP Storage] Root base_dir detected - using fallback #{fallback} instead of '#{str}'"
|
|
196
|
+
return fallback
|
|
197
|
+
end
|
|
198
|
+
str
|
|
160
199
|
end
|
|
161
200
|
end
|
|
162
201
|
end
|
data/lib/aidp/version.rb
CHANGED
|
@@ -15,14 +15,22 @@ module Aidp
|
|
|
15
15
|
class BuildProcessor
|
|
16
16
|
include Aidp::MessageDisplay
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
DEFAULT_BUILD_LABEL = "aidp-build"
|
|
19
|
+
DEFAULT_NEEDS_INPUT_LABEL = "aidp-needs-input"
|
|
19
20
|
IMPLEMENTATION_STEP = "16_IMPLEMENTATION"
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
attr_reader :build_label, :needs_input_label
|
|
23
|
+
|
|
24
|
+
def initialize(repository_client:, state_store:, project_dir: Dir.pwd, use_workstreams: true, verbose: false, label_config: {})
|
|
22
25
|
@repository_client = repository_client
|
|
23
26
|
@state_store = state_store
|
|
24
27
|
@project_dir = project_dir
|
|
25
28
|
@use_workstreams = use_workstreams
|
|
29
|
+
@verbose = verbose
|
|
30
|
+
|
|
31
|
+
# Load label configuration
|
|
32
|
+
@build_label = label_config[:build_trigger] || label_config["build_trigger"] || DEFAULT_BUILD_LABEL
|
|
33
|
+
@needs_input_label = label_config[:needs_input] || label_config["needs_input"] || DEFAULT_NEEDS_INPUT_LABEL
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
def process(issue)
|
|
@@ -55,6 +63,8 @@ module Aidp
|
|
|
55
63
|
|
|
56
64
|
if result[:status] == "completed"
|
|
57
65
|
handle_success(issue: issue, slug: slug, branch_name: branch_name, base_branch: base_branch, plan_data: plan_data, working_dir: working_dir)
|
|
66
|
+
elsif result[:status] == "needs_clarification"
|
|
67
|
+
handle_clarification_request(issue: issue, slug: slug, result: result)
|
|
58
68
|
else
|
|
59
69
|
handle_failure(issue: issue, slug: slug, result: result)
|
|
60
70
|
end
|
|
@@ -203,15 +213,33 @@ module Aidp
|
|
|
203
213
|
prompt_manager = Aidp::Execute::PromptManager.new(working_dir)
|
|
204
214
|
prompt_manager.write(content)
|
|
205
215
|
display_message("📝 Wrote PROMPT.md with implementation contract", type: :info)
|
|
216
|
+
|
|
217
|
+
if @verbose
|
|
218
|
+
display_message("\n--- Implementation Prompt ---", type: :muted)
|
|
219
|
+
display_message(content.strip, type: :muted)
|
|
220
|
+
display_message("--- End Prompt ---\n", type: :muted)
|
|
221
|
+
end
|
|
206
222
|
end
|
|
207
223
|
|
|
208
224
|
def build_user_input(issue:, plan_data:)
|
|
209
225
|
tasks = Array(plan_value(plan_data, "tasks"))
|
|
210
|
-
{
|
|
226
|
+
user_input = {
|
|
211
227
|
"Implementation Contract" => plan_value(plan_data, "summary").to_s,
|
|
212
228
|
"Tasks" => tasks.map { |task| "- #{task}" }.join("\n"),
|
|
213
229
|
"Issue URL" => issue[:url]
|
|
214
230
|
}.delete_if { |_k, v| v.nil? || v.empty? }
|
|
231
|
+
|
|
232
|
+
if @verbose
|
|
233
|
+
display_message("\n--- User Input for Harness ---", type: :muted)
|
|
234
|
+
user_input.each do |key, value|
|
|
235
|
+
display_message("#{key}:", type: :muted)
|
|
236
|
+
display_message(value, type: :muted)
|
|
237
|
+
display_message("", type: :muted)
|
|
238
|
+
end
|
|
239
|
+
display_message("--- End User Input ---\n", type: :muted)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
user_input
|
|
215
243
|
end
|
|
216
244
|
|
|
217
245
|
def run_harness(user_input:, working_dir: @project_dir)
|
|
@@ -220,8 +248,20 @@ module Aidp
|
|
|
220
248
|
workflow_type: :watch_mode,
|
|
221
249
|
user_input: user_input
|
|
222
250
|
}
|
|
251
|
+
|
|
252
|
+
display_message("🚀 Running harness in execute mode...", type: :info) if @verbose
|
|
253
|
+
|
|
223
254
|
runner = Aidp::Harness::Runner.new(working_dir, :execute, options)
|
|
224
|
-
runner.run
|
|
255
|
+
result = runner.run
|
|
256
|
+
|
|
257
|
+
if @verbose
|
|
258
|
+
display_message("\n--- Harness Result ---", type: :muted)
|
|
259
|
+
display_message("Status: #{result[:status]}", type: :muted)
|
|
260
|
+
display_message("Message: #{result[:message]}", type: :muted) if result[:message]
|
|
261
|
+
display_message("--- End Result ---\n", type: :muted)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
result
|
|
225
265
|
end
|
|
226
266
|
|
|
227
267
|
def handle_success(issue:, slug:, branch_name:, base_branch:, plan_data:, working_dir:)
|
|
@@ -257,12 +297,62 @@ module Aidp
|
|
|
257
297
|
)
|
|
258
298
|
display_message("🎉 Posted completion comment for issue ##{issue[:number]}", type: :success)
|
|
259
299
|
|
|
300
|
+
# Remove build label after successful completion
|
|
301
|
+
begin
|
|
302
|
+
@repository_client.remove_labels(issue[:number], @build_label)
|
|
303
|
+
display_message("🏷️ Removed '#{@build_label}' label after completion", type: :info)
|
|
304
|
+
rescue => e
|
|
305
|
+
display_message("⚠️ Failed to remove build label: #{e.message}", type: :warn)
|
|
306
|
+
# Don't fail the process if label removal fails
|
|
307
|
+
end
|
|
308
|
+
|
|
260
309
|
# Keep workstream for review - don't auto-cleanup on success
|
|
261
310
|
if @use_workstreams
|
|
262
311
|
display_message("ℹ️ Workstream #{slug} preserved for review. Remove with: aidp ws rm #{slug}", type: :muted)
|
|
263
312
|
end
|
|
264
313
|
end
|
|
265
314
|
|
|
315
|
+
def handle_clarification_request(issue:, slug:, result:)
|
|
316
|
+
questions = result[:clarification_questions] || []
|
|
317
|
+
workstream_note = @use_workstreams ? " The workstream `#{slug}` has been preserved." : " The branch has been preserved."
|
|
318
|
+
|
|
319
|
+
# Build comment with questions
|
|
320
|
+
comment_parts = []
|
|
321
|
+
comment_parts << "❓ Implementation needs clarification for ##{issue[:number]}."
|
|
322
|
+
comment_parts << ""
|
|
323
|
+
comment_parts << "The AI agent needs additional information to proceed with implementation:"
|
|
324
|
+
comment_parts << ""
|
|
325
|
+
questions.each_with_index do |question, index|
|
|
326
|
+
comment_parts << "#{index + 1}. #{question}"
|
|
327
|
+
end
|
|
328
|
+
comment_parts << ""
|
|
329
|
+
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."
|
|
330
|
+
comment_parts << ""
|
|
331
|
+
comment_parts << workstream_note.to_s
|
|
332
|
+
|
|
333
|
+
comment = comment_parts.join("\n")
|
|
334
|
+
@repository_client.post_comment(issue[:number], comment)
|
|
335
|
+
|
|
336
|
+
# Update labels: remove build trigger, add needs input
|
|
337
|
+
begin
|
|
338
|
+
@repository_client.replace_labels(
|
|
339
|
+
issue[:number],
|
|
340
|
+
old_labels: [@build_label],
|
|
341
|
+
new_labels: [@needs_input_label]
|
|
342
|
+
)
|
|
343
|
+
display_message("🏷️ Updated labels: removed '#{@build_label}', added '#{@needs_input_label}' (needs clarification)", type: :info)
|
|
344
|
+
rescue => e
|
|
345
|
+
display_message("⚠️ Failed to update labels for issue ##{issue[:number]}: #{e.message}", type: :warn)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
@state_store.record_build_status(
|
|
349
|
+
issue[:number],
|
|
350
|
+
status: "needs_clarification",
|
|
351
|
+
details: {questions: questions, workstream: slug}
|
|
352
|
+
)
|
|
353
|
+
display_message("💬 Posted clarification request for issue ##{issue[:number]}", type: :success)
|
|
354
|
+
end
|
|
355
|
+
|
|
266
356
|
def handle_failure(issue:, slug:, result:)
|
|
267
357
|
message = result[:message] || "Unknown failure"
|
|
268
358
|
workstream_note = @use_workstreams ? " The workstream `#{slug}` has been left intact for debugging." : " The branch has been left intact for debugging."
|
|
@@ -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
|
|
|
@@ -65,6 +65,20 @@ module Aidp
|
|
|
65
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")
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
def add_labels(number, *labels)
|
|
69
|
+
gh_available? ? add_labels_via_gh(number, labels.flatten) : add_labels_via_api(number, labels.flatten)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def remove_labels(number, *labels)
|
|
73
|
+
gh_available? ? remove_labels_via_gh(number, labels.flatten) : remove_labels_via_api(number, labels.flatten)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def replace_labels(number, old_labels:, new_labels:)
|
|
77
|
+
# Remove old labels and add new ones atomically where possible
|
|
78
|
+
remove_labels(number, *old_labels) unless old_labels.empty?
|
|
79
|
+
add_labels(number, *new_labels) unless new_labels.empty?
|
|
80
|
+
end
|
|
81
|
+
|
|
68
82
|
private
|
|
69
83
|
|
|
70
84
|
def list_issues_via_gh(labels:, state:)
|
|
@@ -180,6 +194,66 @@ module Aidp
|
|
|
180
194
|
stdout.strip
|
|
181
195
|
end
|
|
182
196
|
|
|
197
|
+
def add_labels_via_gh(number, labels)
|
|
198
|
+
return if labels.empty?
|
|
199
|
+
|
|
200
|
+
cmd = ["gh", "issue", "edit", number.to_s, "--repo", full_repo]
|
|
201
|
+
labels.each { |label| cmd += ["--add-label", label] }
|
|
202
|
+
|
|
203
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
204
|
+
raise "Failed to add labels via gh: #{stderr.strip}" unless status.success?
|
|
205
|
+
|
|
206
|
+
stdout.strip
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def add_labels_via_api(number, labels)
|
|
210
|
+
return if labels.empty?
|
|
211
|
+
|
|
212
|
+
uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}/labels")
|
|
213
|
+
request = Net::HTTP::Post.new(uri)
|
|
214
|
+
request["Content-Type"] = "application/json"
|
|
215
|
+
request.body = JSON.dump({labels: labels})
|
|
216
|
+
|
|
217
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
218
|
+
http.request(request)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
raise "Failed to add labels via API (#{response.code})" unless response.code.start_with?("2")
|
|
222
|
+
response.body
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def remove_labels_via_gh(number, labels)
|
|
226
|
+
return if labels.empty?
|
|
227
|
+
|
|
228
|
+
cmd = ["gh", "issue", "edit", number.to_s, "--repo", full_repo]
|
|
229
|
+
labels.each { |label| cmd += ["--remove-label", label] }
|
|
230
|
+
|
|
231
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
232
|
+
raise "Failed to remove labels via gh: #{stderr.strip}" unless status.success?
|
|
233
|
+
|
|
234
|
+
stdout.strip
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def remove_labels_via_api(number, labels)
|
|
238
|
+
return if labels.empty?
|
|
239
|
+
|
|
240
|
+
labels.each do |label|
|
|
241
|
+
# URL encode the label name
|
|
242
|
+
encoded_label = URI.encode_www_form_component(label)
|
|
243
|
+
uri = URI("https://api.github.com/repos/#{full_repo}/issues/#{number}/labels/#{encoded_label}")
|
|
244
|
+
request = Net::HTTP::Delete.new(uri)
|
|
245
|
+
|
|
246
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
|
247
|
+
http.request(request)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# 404 is OK - label didn't exist
|
|
251
|
+
unless response.code.start_with?("2") || response.code == "404"
|
|
252
|
+
raise "Failed to remove label '#{label}' via API (#{response.code})"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
183
257
|
def normalize_issue(raw)
|
|
184
258
|
{
|
|
185
259
|
number: raw["number"],
|
|
@@ -163,16 +163,25 @@ module Aidp
|
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
def public_repos_allowed?
|
|
166
|
-
|
|
166
|
+
# Support both string and symbol keys
|
|
167
|
+
safety_config = @config[:safety] || @config["safety"] || {}
|
|
168
|
+
(safety_config[:allow_public_repos] || safety_config["allow_public_repos"]) == true
|
|
167
169
|
end
|
|
168
170
|
|
|
169
171
|
def author_allowlist
|
|
170
|
-
|
|
172
|
+
# Support both string and symbol keys
|
|
173
|
+
safety_config = @config[:safety] || @config["safety"] || {}
|
|
174
|
+
@author_allowlist ||= Array(
|
|
175
|
+
safety_config[:author_allowlist] || safety_config["author_allowlist"]
|
|
176
|
+
).compact.map(&:to_s)
|
|
171
177
|
end
|
|
172
178
|
|
|
173
179
|
def safe_environment?
|
|
174
180
|
# Check if running in a container
|
|
175
|
-
|
|
181
|
+
# Support both string and symbol keys
|
|
182
|
+
safety_config = @config[:safety] || @config["safety"] || {}
|
|
183
|
+
require_container = safety_config[:require_container] || safety_config["require_container"]
|
|
184
|
+
in_container? || require_container == false
|
|
176
185
|
end
|
|
177
186
|
|
|
178
187
|
def in_container?
|
data/lib/aidp/watch/runner.rb
CHANGED
|
@@ -19,12 +19,13 @@ module Aidp
|
|
|
19
19
|
|
|
20
20
|
DEFAULT_INTERVAL = 30
|
|
21
21
|
|
|
22
|
-
def initialize(issues_url:, interval: DEFAULT_INTERVAL, provider_name: nil, gh_available: nil, project_dir: Dir.pwd, once: false, use_workstreams: true, prompt: TTY::Prompt.new, safety_config: {}, force: false)
|
|
22
|
+
def initialize(issues_url:, interval: DEFAULT_INTERVAL, provider_name: nil, gh_available: nil, project_dir: Dir.pwd, once: false, use_workstreams: true, prompt: TTY::Prompt.new, safety_config: {}, force: false, verbose: false)
|
|
23
23
|
@prompt = prompt
|
|
24
24
|
@interval = interval
|
|
25
25
|
@once = once
|
|
26
26
|
@project_dir = project_dir
|
|
27
27
|
@force = force
|
|
28
|
+
@verbose = verbose
|
|
28
29
|
|
|
29
30
|
owner, repo = RepositoryClient.parse_issues_url(issues_url)
|
|
30
31
|
@repository_client = RepositoryClient.new(owner: owner, repo: repo, gh_available: gh_available)
|
|
@@ -33,16 +34,23 @@ module Aidp
|
|
|
33
34
|
config: safety_config
|
|
34
35
|
)
|
|
35
36
|
@state_store = StateStore.new(project_dir: project_dir, repository: "#{owner}/#{repo}")
|
|
37
|
+
|
|
38
|
+
# Extract label configuration from safety_config (it's actually the full watch config)
|
|
39
|
+
label_config = safety_config[:labels] || safety_config["labels"] || {}
|
|
40
|
+
|
|
36
41
|
@plan_processor = PlanProcessor.new(
|
|
37
42
|
repository_client: @repository_client,
|
|
38
43
|
state_store: @state_store,
|
|
39
|
-
plan_generator: PlanGenerator.new(provider_name: provider_name)
|
|
44
|
+
plan_generator: PlanGenerator.new(provider_name: provider_name, verbose: verbose),
|
|
45
|
+
label_config: label_config
|
|
40
46
|
)
|
|
41
47
|
@build_processor = BuildProcessor.new(
|
|
42
48
|
repository_client: @repository_client,
|
|
43
49
|
state_store: @state_store,
|
|
44
50
|
project_dir: project_dir,
|
|
45
|
-
use_workstreams: use_workstreams
|
|
51
|
+
use_workstreams: use_workstreams,
|
|
52
|
+
verbose: verbose,
|
|
53
|
+
label_config: label_config
|
|
46
54
|
)
|
|
47
55
|
end
|
|
48
56
|
|
|
@@ -73,9 +81,10 @@ module Aidp
|
|
|
73
81
|
end
|
|
74
82
|
|
|
75
83
|
def process_plan_triggers
|
|
76
|
-
|
|
84
|
+
plan_label = @plan_processor.plan_label
|
|
85
|
+
issues = @repository_client.list_issues(labels: [plan_label], state: "open")
|
|
77
86
|
issues.each do |issue|
|
|
78
|
-
next unless issue_has_label?(issue,
|
|
87
|
+
next unless issue_has_label?(issue, plan_label)
|
|
79
88
|
|
|
80
89
|
detailed = @repository_client.fetch_issue(issue[:number])
|
|
81
90
|
|
|
@@ -89,9 +98,10 @@ module Aidp
|
|
|
89
98
|
end
|
|
90
99
|
|
|
91
100
|
def process_build_triggers
|
|
92
|
-
|
|
101
|
+
build_label = @build_processor.build_label
|
|
102
|
+
issues = @repository_client.list_issues(labels: [build_label], state: "open")
|
|
93
103
|
issues.each do |issue|
|
|
94
|
-
next unless issue_has_label?(issue,
|
|
104
|
+
next unless issue_has_label?(issue, build_label)
|
|
95
105
|
|
|
96
106
|
status = @state_store.build_status(issue[:number])
|
|
97
107
|
next if status["status"] == "completed"
|
|
@@ -33,7 +33,6 @@ module Aidp
|
|
|
33
33
|
@provider_manager = Aidp::Harness::ProviderManager.new(@config_manager, prompt: @prompt)
|
|
34
34
|
@conversation_history = []
|
|
35
35
|
@user_input = {}
|
|
36
|
-
@invalid_planning_responses = 0
|
|
37
36
|
@verbose = verbose
|
|
38
37
|
@debug_env = ENV["DEBUG"] == "1" || ENV["DEBUG"] == "2"
|
|
39
38
|
end
|
|
@@ -138,7 +137,7 @@ module Aidp
|
|
|
138
137
|
end
|
|
139
138
|
|
|
140
139
|
response = call_provider_for_analysis(system_prompt, user_prompt)
|
|
141
|
-
parsed =
|
|
140
|
+
parsed = parse_planning_response(response)
|
|
142
141
|
# Attach raw response for debug
|
|
143
142
|
parsed[:raw_response] = response
|
|
144
143
|
emit_verbose_raw_prompt(system_prompt, user_prompt, response)
|
|
@@ -378,53 +377,20 @@ module Aidp
|
|
|
378
377
|
json_match = response_text.match(/```json\s*(\{.*?\})\s*```/m) ||
|
|
379
378
|
response_text.match(/(\{.*\})/m)
|
|
380
379
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
rescue JSON::ParserError
|
|
384
|
-
{error: :invalid_format}
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
# Provides structured fallback sequence when provider keeps returning invalid planning JSON.
|
|
388
|
-
# After exceeding sequence length, switches to manual entry question.
|
|
389
|
-
def safe_parse_planning_response(response_text)
|
|
390
|
-
parsed = parse_planning_response(response_text)
|
|
391
|
-
return parsed unless parsed.is_a?(Hash) && parsed[:error] == :invalid_format
|
|
392
|
-
|
|
393
|
-
@invalid_planning_responses += 1
|
|
394
|
-
fallback_sequence = [
|
|
395
|
-
"Provide scope (key features) and primary users.",
|
|
396
|
-
"List 3-5 key functional requirements and any technical constraints.",
|
|
397
|
-
"Supply any non-functional requirements (performance/security) or type 'skip'."
|
|
398
|
-
]
|
|
399
|
-
|
|
400
|
-
if @invalid_planning_responses <= fallback_sequence.size
|
|
401
|
-
{complete: false, questions: [fallback_sequence[@invalid_planning_responses - 1]], reasoning: "Fallback due to invalid provider response (format)", error: :fallback}
|
|
402
|
-
else
|
|
403
|
-
display_message("[ERROR] Provider returned invalid planning JSON #{@invalid_planning_responses} times. Enter combined plan details manually.", type: :error)
|
|
404
|
-
{complete: false, questions: ["Enter plan details manually (features; users; requirements; constraints) or type 'skip'"], reasoning: "Manual recovery mode", error: :manual_recovery}
|
|
380
|
+
unless json_match
|
|
381
|
+
raise ConversationError, "Provider returned invalid format: no JSON found in response"
|
|
405
382
|
end
|
|
383
|
+
|
|
384
|
+
JSON.parse(json_match[1], symbolize_names: true)
|
|
385
|
+
rescue JSON::ParserError => e
|
|
386
|
+
raise ConversationError, "Provider returned invalid JSON: #{e.message}"
|
|
406
387
|
end
|
|
407
388
|
|
|
408
389
|
def update_plan_from_answer(plan, question, answer)
|
|
409
390
|
# Simple heuristic-based plan updates
|
|
410
391
|
# In a more sophisticated implementation, use AI to categorize answers
|
|
411
392
|
|
|
412
|
-
|
|
413
|
-
# by broader keyword heuristics (e.g., it contains the word 'users').
|
|
414
|
-
if question.start_with?("Enter plan details manually")
|
|
415
|
-
unless answer.strip.downcase == "skip"
|
|
416
|
-
parts = answer.split(/;|\|/).map(&:strip).reject(&:empty?)
|
|
417
|
-
features, users, requirements, constraints = parts
|
|
418
|
-
plan[:scope][:included] ||= []
|
|
419
|
-
plan[:scope][:included] << features if features
|
|
420
|
-
plan[:users][:personas] ||= []
|
|
421
|
-
plan[:users][:personas] << users if users
|
|
422
|
-
plan[:requirements][:functional] ||= []
|
|
423
|
-
plan[:requirements][:functional] << requirements if requirements
|
|
424
|
-
plan[:constraints][:technical] ||= []
|
|
425
|
-
plan[:constraints][:technical] << constraints if constraints
|
|
426
|
-
end
|
|
427
|
-
elsif question.downcase.include?("scope") || question.downcase.include?("include")
|
|
393
|
+
if question.downcase.include?("scope") || question.downcase.include?("include")
|
|
428
394
|
plan[:scope][:included] ||= []
|
|
429
395
|
plan[:scope][:included] << answer
|
|
430
396
|
elsif question.downcase.include?("user") || question.downcase.include?("who")
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aidp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.23.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -245,6 +245,7 @@ files:
|
|
|
245
245
|
- lib/aidp/analyze/tree_sitter_grammar_loader.rb
|
|
246
246
|
- lib/aidp/analyze/tree_sitter_scan.rb
|
|
247
247
|
- lib/aidp/cli.rb
|
|
248
|
+
- lib/aidp/cli/devcontainer_commands.rb
|
|
248
249
|
- lib/aidp/cli/enhanced_input.rb
|
|
249
250
|
- lib/aidp/cli/first_run_wizard.rb
|
|
250
251
|
- lib/aidp/cli/issue_importer.rb
|
|
@@ -354,6 +355,11 @@ files:
|
|
|
354
355
|
- lib/aidp/providers/github_copilot.rb
|
|
355
356
|
- lib/aidp/providers/opencode.rb
|
|
356
357
|
- lib/aidp/rescue_logging.rb
|
|
358
|
+
- lib/aidp/safe_directory.rb
|
|
359
|
+
- lib/aidp/setup/devcontainer/backup_manager.rb
|
|
360
|
+
- lib/aidp/setup/devcontainer/generator.rb
|
|
361
|
+
- lib/aidp/setup/devcontainer/parser.rb
|
|
362
|
+
- lib/aidp/setup/devcontainer/port_manager.rb
|
|
357
363
|
- lib/aidp/setup/wizard.rb
|
|
358
364
|
- lib/aidp/skills.rb
|
|
359
365
|
- lib/aidp/skills/composer.rb
|