aidp 0.31.0 → 0.33.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aidp/analyze/feature_analyzer.rb +322 -320
  3. data/lib/aidp/auto_update/coordinator.rb +97 -7
  4. data/lib/aidp/auto_update.rb +0 -12
  5. data/lib/aidp/cli/devcontainer_commands.rb +0 -5
  6. data/lib/aidp/cli.rb +2 -1
  7. data/lib/aidp/comment_consolidator.rb +78 -0
  8. data/lib/aidp/concurrency.rb +0 -3
  9. data/lib/aidp/config.rb +0 -1
  10. data/lib/aidp/config_paths.rb +71 -0
  11. data/lib/aidp/execute/work_loop_runner.rb +394 -15
  12. data/lib/aidp/harness/ai_filter_factory.rb +285 -0
  13. data/lib/aidp/harness/config_schema.rb +97 -1
  14. data/lib/aidp/harness/config_validator.rb +1 -1
  15. data/lib/aidp/harness/configuration.rb +61 -5
  16. data/lib/aidp/harness/filter_definition.rb +212 -0
  17. data/lib/aidp/harness/generated_filter_strategy.rb +197 -0
  18. data/lib/aidp/harness/output_filter.rb +50 -25
  19. data/lib/aidp/harness/output_filter_config.rb +129 -0
  20. data/lib/aidp/harness/provider_manager.rb +128 -2
  21. data/lib/aidp/harness/provider_metrics.rb +5 -3
  22. data/lib/aidp/harness/runner.rb +0 -11
  23. data/lib/aidp/harness/test_runner.rb +179 -41
  24. data/lib/aidp/harness/thinking_depth_manager.rb +16 -0
  25. data/lib/aidp/harness/ui/navigation/submenu.rb +0 -2
  26. data/lib/aidp/loader.rb +195 -0
  27. data/lib/aidp/metadata/compiler.rb +29 -17
  28. data/lib/aidp/metadata/query.rb +1 -1
  29. data/lib/aidp/metadata/scanner.rb +8 -1
  30. data/lib/aidp/metadata/tool_metadata.rb +13 -13
  31. data/lib/aidp/metadata/validator.rb +10 -0
  32. data/lib/aidp/metadata.rb +16 -0
  33. data/lib/aidp/pr_worktree_manager.rb +582 -0
  34. data/lib/aidp/provider_manager.rb +1 -7
  35. data/lib/aidp/setup/wizard.rb +279 -9
  36. data/lib/aidp/skills.rb +0 -5
  37. data/lib/aidp/storage/csv_storage.rb +3 -0
  38. data/lib/aidp/style_guide/selector.rb +360 -0
  39. data/lib/aidp/tooling_detector.rb +283 -16
  40. data/lib/aidp/util.rb +11 -0
  41. data/lib/aidp/version.rb +1 -1
  42. data/lib/aidp/watch/change_request_processor.rb +152 -14
  43. data/lib/aidp/watch/repository_client.rb +41 -0
  44. data/lib/aidp/watch/runner.rb +29 -18
  45. data/lib/aidp/watch.rb +5 -7
  46. data/lib/aidp/workstream_cleanup.rb +0 -2
  47. data/lib/aidp/workstream_executor.rb +0 -4
  48. data/lib/aidp/worktree.rb +0 -1
  49. data/lib/aidp/worktree_branch_manager.rb +70 -1
  50. data/lib/aidp.rb +21 -106
  51. metadata +73 -36
  52. data/lib/aidp/config/paths.rb +0 -131
@@ -0,0 +1,582 @@
1
+ require "json"
2
+ require "fileutils"
3
+ require "shellwords"
4
+
5
+ module Aidp
6
+ # Manages worktrees specifically for Pull Request branches
7
+ class PRWorktreeManager
8
+ def initialize(base_repo_path: nil, project_dir: nil, worktree_registry_path: nil)
9
+ @base_repo_path = base_repo_path || project_dir || Dir.pwd
10
+ @project_dir = project_dir
11
+ @worktree_registry_path = worktree_registry_path || File.join(
12
+ project_dir || File.expand_path("~/.aidp"),
13
+ "pr_worktrees.json"
14
+ )
15
+ FileUtils.mkdir_p(File.dirname(@worktree_registry_path))
16
+ @worktrees = load_registry
17
+ end
18
+
19
+ attr_reader :worktree_registry_path
20
+
21
+ # Find an existing worktree for a given PR number or branch
22
+ def find_worktree(pr_number = nil, branch: nil)
23
+ Aidp.log_debug(
24
+ "pr_worktree_manager",
25
+ "finding_worktree",
26
+ pr_number: pr_number,
27
+ branch: branch
28
+ )
29
+
30
+ # Validate input
31
+ raise ArgumentError, "Must provide either pr_number or branch" if pr_number.nil? && branch.nil?
32
+
33
+ # First, check for exact PR match if PR number is provided
34
+ existing_worktree = pr_number ? @worktrees[pr_number.to_s] : nil
35
+ return validate_worktree_path(existing_worktree) if existing_worktree
36
+
37
+ # If no PR number, search by branch in all worktrees
38
+ matching_worktrees = @worktrees.values.select do |details|
39
+ # Check for exact branch match or remote branch match with advanced checks
40
+ details.values_at("base_branch", "head_branch").any? do |branch_name|
41
+ branch_name.end_with?("/#{branch}", "remotes/origin/#{branch}") ||
42
+ branch_name == branch
43
+ end
44
+ end
45
+
46
+ # If multiple matching worktrees, prefer most recently created
47
+ # Use min_by to efficiently find the most recently created worktree
48
+ matching_worktree = matching_worktrees.min_by { |details| [-details["created_at"].to_i, details["path"]] }
49
+
50
+ validate_worktree_path(matching_worktree)
51
+ end
52
+
53
+ # Helper method to validate worktree path and provide consistent logging
54
+ def validate_worktree_path(worktree_details)
55
+ return nil unless worktree_details
56
+
57
+ # Validate the worktree's integrity
58
+ if File.exist?(worktree_details["path"])
59
+ # Check if the worktree has the correct git repository
60
+ if valid_worktree_repository?(worktree_details["path"])
61
+ Aidp.log_debug("pr_worktree_manager", "found_existing_worktree",
62
+ path: worktree_details["path"],
63
+ base_branch: worktree_details["base_branch"],
64
+ head_branch: worktree_details["head_branch"])
65
+ return worktree_details["path"]
66
+ else
67
+ Aidp.log_warn("pr_worktree_manager", "corrupted_worktree",
68
+ pr_number: worktree_details["pr_number"] || "unknown",
69
+ path: worktree_details["path"])
70
+ end
71
+ else
72
+ Aidp.log_warn("pr_worktree_manager", "worktree_path_missing",
73
+ pr_number: worktree_details["pr_number"] || "unknown",
74
+ expected_path: worktree_details["path"])
75
+ end
76
+
77
+ nil
78
+ end
79
+
80
+ # Verify the integrity of the git worktree repository
81
+ def valid_worktree_repository?(worktree_path)
82
+ return false unless File.directory?(worktree_path)
83
+
84
+ # Check for .git directory or .git file (for submodules)
85
+ git_dir = File.join(worktree_path, ".git")
86
+ return false unless File.exist?(git_dir) || File.file?(git_dir)
87
+
88
+ true
89
+ rescue
90
+ false
91
+ end
92
+
93
+ # Create a new worktree for a PR
94
+ def create_worktree(pr_number, base_branch, head_branch, allow_duplicate: true, max_diff_size: nil)
95
+ # Log only the required attributes without max_diff_size
96
+ Aidp.log_debug(
97
+ "pr_worktree_manager", "creating_worktree",
98
+ pr_number: pr_number,
99
+ base_branch: base_branch,
100
+ head_branch: head_branch
101
+ )
102
+
103
+ # Validate inputs
104
+ raise ArgumentError, "PR number must be a positive integer" unless pr_number.to_i > 0
105
+ raise ArgumentError, "Base branch cannot be empty" if base_branch.nil? || base_branch.empty?
106
+ raise ArgumentError, "Head branch cannot be empty" if head_branch.nil? || head_branch.empty?
107
+
108
+ # Advanced max diff size handling
109
+ if max_diff_size
110
+ Aidp.log_debug(
111
+ "pr_worktree_manager", "diff_size_check",
112
+ method: "worktree_based_workflow"
113
+ )
114
+ end
115
+
116
+ # Check for existing worktrees if duplicates are not allowed
117
+ if !allow_duplicate
118
+ existing_worktrees = @worktrees.values.select do |details|
119
+ details["base_branch"] == base_branch && details["head_branch"] == head_branch
120
+ end
121
+ return existing_worktrees.first["path"] unless existing_worktrees.empty?
122
+ end
123
+
124
+ # Check if a worktree for this PR already exists
125
+ existing_path = find_worktree(pr_number)
126
+ return existing_path if existing_path
127
+
128
+ # Generate a unique slug for the worktree
129
+ slug = "pr-#{pr_number}-#{Time.now.to_i}"
130
+
131
+ # Determine the base directory for worktrees
132
+ base_worktree_dir = @project_dir || File.expand_path("~/.aidp/worktrees")
133
+ worktree_path = File.join(base_worktree_dir, slug)
134
+
135
+ # Ensure base repo path is an actual git repository
136
+ raise "Not a git repository: #{@base_repo_path}" unless File.exist?(File.join(@base_repo_path, ".git"))
137
+
138
+ # Create the worktree directory if it doesn't exist
139
+ FileUtils.mkdir_p(base_worktree_dir)
140
+
141
+ # Verify base branch exists
142
+ Dir.chdir(@base_repo_path) do
143
+ # List all remote and local branches
144
+ branch_list_output = `git branch -a`.split("\n").map(&:strip)
145
+
146
+ # More robust branch existence check with expanded match criteria
147
+ base_branch_exists = branch_list_output.any? do |branch|
148
+ branch.end_with?("/#{base_branch}", "remotes/origin/#{base_branch}") ||
149
+ branch == base_branch ||
150
+ branch == "* #{base_branch}"
151
+ end
152
+
153
+ # Enhance branch tracking and fetching
154
+ unless base_branch_exists
155
+ # Try multiple fetch strategies
156
+ fetch_commands = [
157
+ "git fetch origin #{base_branch}:#{base_branch} 2>/dev/null",
158
+ "git fetch origin 2>/dev/null",
159
+ "git fetch --all 2>/dev/null"
160
+ ]
161
+
162
+ fetch_commands.each do |fetch_cmd|
163
+ system(fetch_cmd)
164
+ branch_list_output = `git branch -a`.split("\n").map(&:strip)
165
+ base_branch_exists = branch_list_output.any? do |branch|
166
+ branch.end_with?("/#{base_branch}", "remotes/origin/#{base_branch}") ||
167
+ branch == base_branch ||
168
+ branch == "* #{base_branch}"
169
+ end
170
+ break if base_branch_exists
171
+ end
172
+ end
173
+
174
+ raise ArgumentError, "Base branch '#{base_branch}' does not exist in the repository" unless base_branch_exists
175
+
176
+ # Robust worktree creation with enhanced error handling and logging
177
+ worktree_create_command = "git worktree add #{Shellwords.escape(worktree_path)} -b #{Shellwords.escape(head_branch)} #{Shellwords.escape(base_branch)}"
178
+ unless system(worktree_create_command)
179
+ error_details = {
180
+ pr_number: pr_number,
181
+ base_branch: base_branch,
182
+ head_branch: head_branch,
183
+ command: worktree_create_command
184
+ }
185
+ Aidp.log_error(
186
+ "pr_worktree_manager", "worktree_creation_failed",
187
+ error_details
188
+ )
189
+
190
+ # Attempt to diagnose the failure
191
+ git_status = `git status`
192
+ Aidp.log_debug(
193
+ "pr_worktree_manager", "git_status_on_failure",
194
+ status: git_status
195
+ )
196
+
197
+ raise "Failed to create worktree for PR #{pr_number}"
198
+ end
199
+ end
200
+
201
+ # Extended validation of worktree creation
202
+ unless File.exist?(worktree_path) && File.directory?(worktree_path)
203
+ error_details = {
204
+ pr_number: pr_number,
205
+ base_branch: base_branch,
206
+ head_branch: head_branch,
207
+ expected_path: worktree_path
208
+ }
209
+ Aidp.log_error(
210
+ "pr_worktree_manager", "worktree_path_validation_failed",
211
+ error_details
212
+ )
213
+ raise "Failed to validate worktree path for PR #{pr_number}"
214
+ end
215
+
216
+ # Prepare registry entry with additional metadata
217
+ registry_entry = {
218
+ "path" => worktree_path,
219
+ "base_branch" => base_branch,
220
+ "head_branch" => head_branch,
221
+ "created_at" => Time.now.to_i,
222
+ "slug" => slug,
223
+ "source" => "label_workflow" # Add custom source tracking
224
+ }
225
+
226
+ # Conditionally add max_diff_size only if it's provided
227
+ registry_entry["max_diff_size"] = max_diff_size if max_diff_size
228
+
229
+ # Store in registry
230
+ @worktrees[pr_number.to_s] = registry_entry
231
+ save_registry
232
+
233
+ Aidp.log_debug(
234
+ "pr_worktree_manager", "worktree_created",
235
+ path: worktree_path,
236
+ pr_number: pr_number
237
+ )
238
+
239
+ worktree_path
240
+ end
241
+
242
+ # Enhanced method to extract changes from PR comments/reviews
243
+ def extract_pr_changes(changes_description)
244
+ Aidp.log_debug(
245
+ "pr_worktree_manager", "extracting_pr_changes",
246
+ description_length: changes_description&.length
247
+ )
248
+
249
+ return nil if changes_description.nil? || changes_description.empty?
250
+
251
+ # Sophisticated change extraction with multiple parsing strategies
252
+ parsed_changes = {
253
+ files: [],
254
+ operations: [],
255
+ comments: [],
256
+ metadata: {}
257
+ }
258
+
259
+ # Advanced change detection patterns
260
+ file_patterns = [
261
+ /(modify|update|add|delete)\s+file:\s*([^\n]+)/i,
262
+ /\[(\w+)\]\s*([^\n]+)/, # GitHub-style change indicators
263
+ /(?:Action:\s*(\w+))\s*File:\s*([^\n]+)/i
264
+ ]
265
+
266
+ # Operation mapping
267
+ operation_map = {
268
+ "add" => :create,
269
+ "create" => :create,
270
+ "update" => :modify,
271
+ "modify" => :modify,
272
+ "delete" => :delete,
273
+ "remove" => :delete
274
+ }
275
+
276
+ # Parse changes using multiple strategies
277
+ file_patterns.each do |pattern|
278
+ changes_description.scan(pattern) do |match|
279
+ operation = (match.size == 2) ? (match[0].downcase) : nil
280
+ file = (match.size == 2) ? (match[1].strip) : match[0].strip
281
+
282
+ parsed_changes[:files] << file
283
+ if operation && operation_map.key?(operation)
284
+ parsed_changes[:operations] << operation_map[operation]
285
+ end
286
+ end
287
+ end
288
+
289
+ # Extract potential comments or annotations
290
+ comment_pattern = /(?:comment|note):\s*(.+)/i
291
+ changes_description.scan(comment_pattern) do |match|
292
+ parsed_changes[:comments] << match[0].strip
293
+ end
294
+
295
+ # Additional metadata extraction
296
+ parsed_changes[:metadata] = {
297
+ source: "pr_comments",
298
+ timestamp: Time.now.to_i
299
+ }
300
+
301
+ Aidp.log_debug(
302
+ "pr_worktree_manager", "pr_changes_extracted",
303
+ files_count: parsed_changes[:files].size,
304
+ operations_count: parsed_changes[:operations].size,
305
+ comments_count: parsed_changes[:comments].size
306
+ )
307
+
308
+ parsed_changes
309
+ end
310
+
311
+ # Enhanced method to apply changes to the worktree with robust handling
312
+ def apply_worktree_changes(pr_number, changes)
313
+ Aidp.log_debug(
314
+ "pr_worktree_manager", "applying_worktree_changes",
315
+ pr_number: pr_number,
316
+ changes: changes
317
+ )
318
+
319
+ # Find the worktree for the PR
320
+ worktree_path = find_worktree(pr_number)
321
+ raise "No worktree found for PR #{pr_number}" unless worktree_path
322
+
323
+ # Track successful and failed file modifications
324
+ successful_files = []
325
+ failed_files = []
326
+
327
+ Dir.chdir(worktree_path) do
328
+ changes.fetch(:files, []).each_with_index do |file, index|
329
+ operation = changes.fetch(:operations, [])[index] || :modify
330
+ file_path = File.join(worktree_path, file)
331
+
332
+ # Enhanced file manipulation with operation-specific handling
333
+ begin
334
+ # Ensure safe file path (prevent directory traversal)
335
+ canonical_path = File.expand_path(file_path)
336
+ raise SecurityError, "Unsafe file path" unless canonical_path.start_with?(worktree_path)
337
+
338
+ # Ensure directory exists for file creation
339
+ FileUtils.mkdir_p(File.dirname(file_path)) unless File.exist?(File.dirname(file_path))
340
+
341
+ case operation
342
+ when :create, :modify
343
+ File.write(file_path, "# File #{(operation == :create) ? "added" : "modified"} by AIDP request-changes workflow\n")
344
+ when :delete
345
+ FileUtils.rm_f(file_path)
346
+ else
347
+ Aidp.log_warn(
348
+ "pr_worktree_manager", "unknown_file_operation",
349
+ file: file,
350
+ operation: operation
351
+ )
352
+ next
353
+ end
354
+
355
+ successful_files << file
356
+ Aidp.log_debug(
357
+ "pr_worktree_manager", "file_changed",
358
+ file: file,
359
+ action: operation
360
+ )
361
+ rescue SecurityError => e
362
+ Aidp.log_error(
363
+ "pr_worktree_manager", "file_path_security_error",
364
+ file: file,
365
+ error: e.message
366
+ )
367
+ failed_files << file
368
+ rescue => e
369
+ Aidp.log_error(
370
+ "pr_worktree_manager", "file_change_error",
371
+ file: file,
372
+ operation: operation,
373
+ error: e.message
374
+ )
375
+ failed_files << file
376
+ end
377
+ end
378
+
379
+ # Stage only successfully modified files
380
+ unless successful_files.empty?
381
+ system("git add #{successful_files.map { |f| Shellwords.escape(f) }.join(" ")}")
382
+ end
383
+ end
384
+
385
+ Aidp.log_debug(
386
+ "pr_worktree_manager", "worktree_changes_summary",
387
+ pr_number: pr_number,
388
+ successful_files_count: successful_files.size,
389
+ failed_files_count: failed_files.size,
390
+ total_files: changes.fetch(:files, []).size
391
+ )
392
+
393
+ {
394
+ success: successful_files.size == changes.fetch(:files, []).size,
395
+ successful_files: successful_files,
396
+ failed_files: failed_files
397
+ }
398
+ end
399
+
400
+ # Push changes back to the PR branch with enhanced error handling
401
+ def push_worktree_changes(pr_number, branch: nil)
402
+ Aidp.log_debug(
403
+ "pr_worktree_manager", "pushing_worktree_changes",
404
+ pr_number: pr_number,
405
+ branch: branch
406
+ )
407
+
408
+ # Find the worktree and its head branch
409
+ worktree_path = find_worktree(pr_number)
410
+ raise "No worktree found for PR #{pr_number}" unless worktree_path
411
+
412
+ # Retrieve the head branch from registry if not provided
413
+ head_branch = branch || @worktrees[pr_number.to_s]["head_branch"]
414
+ raise "No head branch found for PR #{pr_number}" unless head_branch
415
+
416
+ # Comprehensive error tracking
417
+ push_result = {
418
+ success: false,
419
+ git_actions: {
420
+ staged_changes: false,
421
+ committed: false,
422
+ pushed: false
423
+ },
424
+ errors: [],
425
+ changed_files: []
426
+ }
427
+
428
+ Dir.chdir(worktree_path) do
429
+ # Check staged changes with more robust capture
430
+ staged_changes_output = `git diff --staged --name-only`.strip
431
+
432
+ if !staged_changes_output.empty?
433
+ push_result[:git_actions][:staged_changes] = true
434
+ push_result[:changed_files] = staged_changes_output.split("\n")
435
+
436
+ # More robust commit command with additional logging
437
+ commit_message = "Changes applied via AIDP request-changes workflow for PR ##{pr_number}"
438
+ commit_command = "git commit -m '#{commit_message}' 2>&1"
439
+ commit_output = `#{commit_command}`.strip
440
+
441
+ if $?.success?
442
+ push_result[:git_actions][:committed] = true
443
+
444
+ # Enhanced push with verbose tracking
445
+ push_command = "git push origin #{head_branch} 2>&1"
446
+ push_output = `#{push_command}`.strip
447
+
448
+ if $?.success?
449
+ push_result[:git_actions][:pushed] = true
450
+ push_result[:success] = true
451
+
452
+ Aidp.log_debug(
453
+ "pr_worktree_manager", "changes_pushed_successfully",
454
+ pr_number: pr_number,
455
+ branch: head_branch,
456
+ changed_files_count: push_result[:changed_files].size
457
+ )
458
+ else
459
+ # Detailed push error logging
460
+ push_result[:errors] << "Push failed: #{push_output}"
461
+ Aidp.log_error(
462
+ "pr_worktree_manager", "push_changes_failed",
463
+ pr_number: pr_number,
464
+ branch: head_branch,
465
+ error_details: push_output
466
+ )
467
+ end
468
+ else
469
+ # Detailed commit error logging
470
+ push_result[:errors] << "Commit failed: #{commit_output}"
471
+ Aidp.log_error(
472
+ "pr_worktree_manager", "commit_changes_failed",
473
+ pr_number: pr_number,
474
+ branch: head_branch,
475
+ error_details: commit_output
476
+ )
477
+ end
478
+ else
479
+ # No changes to commit
480
+ push_result[:success] = true
481
+ Aidp.log_debug(
482
+ "pr_worktree_manager", "no_changes_to_push",
483
+ pr_number: pr_number
484
+ )
485
+ end
486
+ end
487
+
488
+ push_result
489
+ end
490
+
491
+ # Remove a specific worktree
492
+ def remove_worktree(pr_number)
493
+ Aidp.log_debug("pr_worktree_manager", "removing_worktree", pr_number: pr_number)
494
+
495
+ existing_worktree = @worktrees[pr_number.to_s]
496
+ return false unless existing_worktree
497
+
498
+ # Remove git worktree
499
+ system("git worktree remove #{existing_worktree["path"]}") if File.exist?(existing_worktree["path"])
500
+
501
+ # Remove from registry and save
502
+ @worktrees.delete(pr_number.to_s)
503
+ save_registry
504
+
505
+ true
506
+ end
507
+
508
+ # List all active worktrees
509
+ def list_worktrees
510
+ # Include all known metadata keys from stored details
511
+ metadata_keys = ["path", "base_branch", "head_branch", "created_at", "max_diff_size"]
512
+ @worktrees.transform_values { |details| details.slice(*metadata_keys) }
513
+ end
514
+
515
+ # Cleanup old/stale worktrees (more than 30 days old)
516
+ def cleanup_stale_worktrees(days_threshold = 30)
517
+ Aidp.log_debug("pr_worktree_manager", "cleaning_stale_worktrees", threshold_days: days_threshold)
518
+
519
+ stale_worktrees = @worktrees.select do |_, details|
520
+ created_at = Time.at(details["created_at"])
521
+ (Time.now - created_at) > (days_threshold * 24 * 60 * 60)
522
+ end
523
+
524
+ stale_worktrees.each_key { |pr_number| remove_worktree(pr_number) }
525
+
526
+ Aidp.log_debug(
527
+ "pr_worktree_manager", "stale_worktrees_cleaned",
528
+ count: stale_worktrees.size
529
+ )
530
+ end
531
+
532
+ private
533
+
534
+ # Load the worktree registry from file
535
+ def load_registry
536
+ return {} unless File.exist?(worktree_registry_path)
537
+
538
+ begin
539
+ # Validate file content before parsing
540
+ registry_content = File.read(worktree_registry_path)
541
+ return {} if registry_content.strip.empty?
542
+
543
+ # Attempt to parse JSON
544
+ parsed_registry = JSON.parse(registry_content)
545
+
546
+ # Additional validation of registry structure
547
+ if parsed_registry.is_a?(Hash) && parsed_registry.all? { |k, v| k.is_a?(String) && v.is_a?(Hash) }
548
+ parsed_registry
549
+ else
550
+ Aidp.log_warn(
551
+ "pr_worktree_manager",
552
+ "invalid_registry_structure",
553
+ path: worktree_registry_path
554
+ )
555
+ {}
556
+ end
557
+ rescue JSON::ParserError
558
+ Aidp.log_warn(
559
+ "pr_worktree_manager",
560
+ "invalid_registry",
561
+ path: worktree_registry_path
562
+ )
563
+ {}
564
+ rescue SystemCallError
565
+ Aidp.log_warn(
566
+ "pr_worktree_manager",
567
+ "registry_read_error",
568
+ path: worktree_registry_path
569
+ )
570
+ {}
571
+ end
572
+ end
573
+
574
+ # Save the worktree registry to file
575
+ def save_registry
576
+ FileUtils.mkdir_p(File.dirname(worktree_registry_path))
577
+ File.write(worktree_registry_path, JSON.pretty_generate(@worktrees))
578
+ rescue => e
579
+ Aidp.log_error("pr_worktree_manager", "registry_save_failed", error: e.message)
580
+ end
581
+ end
582
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "tty-prompt"
4
- require_relative "harness/provider_factory"
5
4
 
6
5
  module Aidp
7
6
  class ProviderManager
@@ -20,12 +19,7 @@ module Aidp
20
19
 
21
20
  # Get harness factory instance
22
21
  def get_harness_factory
23
- @harness_factory ||= begin
24
- require_relative "harness/config_manager"
25
- Aidp::Harness::ProviderFactory.new
26
- rescue LoadError
27
- nil
28
- end
22
+ @harness_factory ||= Aidp::Harness::ProviderFactory.new
29
23
  end
30
24
 
31
25
  # Create provider using harness configuration