aidp 0.34.0 → 0.39.3

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 (133) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -14
  3. data/lib/aidp/agent_harness_adapter.rb +309 -0
  4. data/lib/aidp/cli/issue_importer.rb +20 -6
  5. data/lib/aidp/cli/jobs_command.rb +1 -1
  6. data/lib/aidp/cli/models_command.rb +9 -16
  7. data/lib/aidp/cli/prompts_command.rb +253 -0
  8. data/lib/aidp/cli/security_command.rb +4 -10
  9. data/lib/aidp/cli/storage_command.rb +277 -0
  10. data/lib/aidp/cli/temporal_command.rb +460 -0
  11. data/lib/aidp/cli.rb +24 -2
  12. data/lib/aidp/config.rb +15 -0
  13. data/lib/aidp/config_paths.rb +12 -2
  14. data/lib/aidp/database/migrations.rb +114 -0
  15. data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
  16. data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
  17. data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
  18. data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
  19. data/lib/aidp/database/repositories/job_repository.rb +278 -0
  20. data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
  21. data/lib/aidp/database/repositories/progress_repository.rb +210 -0
  22. data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
  23. data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
  24. data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
  25. data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
  26. data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
  27. data/lib/aidp/database/repositories/task_repository.rb +231 -0
  28. data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
  29. data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
  30. data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
  31. data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
  32. data/lib/aidp/database/repository.rb +166 -0
  33. data/lib/aidp/database/schema.rb +347 -0
  34. data/lib/aidp/database/storage_migrator.rb +693 -0
  35. data/lib/aidp/database.rb +139 -0
  36. data/lib/aidp/debug_mixin.rb +2 -2
  37. data/lib/aidp/execute/checkpoint.rb +26 -0
  38. data/lib/aidp/execute/checkpoint_display.rb +16 -0
  39. data/lib/aidp/execute/prompt_evaluator.rb +30 -57
  40. data/lib/aidp/execute/work_loop_runner.rb +294 -46
  41. data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
  42. data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
  43. data/lib/aidp/harness/ai_decision_engine.rb +78 -296
  44. data/lib/aidp/harness/config_schema.rb +59 -0
  45. data/lib/aidp/harness/configuration.rb +229 -10
  46. data/lib/aidp/harness/deprecation_cache.rb +53 -11
  47. data/lib/aidp/harness/enhanced_runner.rb +1 -1
  48. data/lib/aidp/harness/error_handler.rb +11 -5
  49. data/lib/aidp/harness/provider_config.rb +36 -0
  50. data/lib/aidp/harness/provider_factory.rb +26 -24
  51. data/lib/aidp/harness/provider_info.rb +9 -5
  52. data/lib/aidp/harness/provider_manager.rb +2 -2
  53. data/lib/aidp/harness/provider_metrics.rb +103 -1
  54. data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
  55. data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
  56. data/lib/aidp/harness/runner.rb +7 -2
  57. data/lib/aidp/harness/state_manager.rb +1 -1
  58. data/lib/aidp/harness/test_runner.rb +167 -11
  59. data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
  60. data/lib/aidp/harness/ui.rb +11 -0
  61. data/lib/aidp/harness/usage_limit.rb +179 -0
  62. data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
  63. data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
  64. data/lib/aidp/harness/usage_period.rb +234 -0
  65. data/lib/aidp/init/agent_instructions_generator.rb +317 -0
  66. data/lib/aidp/init/runner.rb +25 -4
  67. data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
  68. data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
  69. data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
  70. data/lib/aidp/interfaces/logger_interface.rb +150 -0
  71. data/lib/aidp/interfaces/ui_interface.rb +254 -0
  72. data/lib/aidp/logger.rb +25 -7
  73. data/lib/aidp/orchestration_adapter.rb +279 -0
  74. data/lib/aidp/pr_worktree_manager.rb +4 -15
  75. data/lib/aidp/prompts/feedback_collector.rb +198 -0
  76. data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
  77. data/lib/aidp/prompts/template_evolver.rb +305 -0
  78. data/lib/aidp/prompts/template_version_manager.rb +325 -0
  79. data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
  80. data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
  81. data/lib/aidp/setup/wizard.rb +616 -176
  82. data/lib/aidp/shell_executor.rb +58 -0
  83. data/lib/aidp/style_guide/selector.rb +56 -1
  84. data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
  85. data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
  86. data/lib/aidp/temporal/activities/base_activity.rb +78 -0
  87. data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
  88. data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
  89. data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
  90. data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
  91. data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
  92. data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
  93. data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
  94. data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
  95. data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
  96. data/lib/aidp/temporal/configuration.rb +169 -0
  97. data/lib/aidp/temporal/connection.rb +93 -0
  98. data/lib/aidp/temporal/worker.rb +119 -0
  99. data/lib/aidp/temporal/workflow_client.rb +171 -0
  100. data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
  101. data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
  102. data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
  103. data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
  104. data/lib/aidp/temporal.rb +120 -0
  105. data/lib/aidp/version.rb +1 -1
  106. data/lib/aidp/watch/base_processor.rb +35 -0
  107. data/lib/aidp/watch/build_processor.rb +132 -0
  108. data/lib/aidp/watch/github_state_extractor.rb +0 -8
  109. data/lib/aidp/watch/rebase_processor.rb +245 -0
  110. data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
  111. data/lib/aidp/watch/runner.rb +366 -285
  112. data/lib/aidp/watch/state_store.rb +80 -0
  113. data/lib/aidp/watch/work_item.rb +96 -0
  114. data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
  115. data/lib/aidp/watch/worktree_reconciler.rb +643 -0
  116. data/lib/aidp/workflows/guided_agent.rb +1 -1
  117. data/lib/aidp/worktree.rb +62 -0
  118. data/templates/aidp.yml.example +70 -0
  119. metadata +118 -16
  120. data/lib/aidp/analyze/json_file_storage.rb +0 -292
  121. data/lib/aidp/execute/future_work_backlog.rb +0 -411
  122. data/lib/aidp/providers/adapter.rb +0 -239
  123. data/lib/aidp/providers/aider.rb +0 -264
  124. data/lib/aidp/providers/anthropic.rb +0 -614
  125. data/lib/aidp/providers/base.rb +0 -617
  126. data/lib/aidp/providers/capability_registry.rb +0 -204
  127. data/lib/aidp/providers/codex.rb +0 -267
  128. data/lib/aidp/providers/cursor.rb +0 -304
  129. data/lib/aidp/providers/error_taxonomy.rb +0 -195
  130. data/lib/aidp/providers/gemini.rb +0 -110
  131. data/lib/aidp/providers/github_copilot.rb +0 -227
  132. data/lib/aidp/providers/kilocode.rb +0 -157
  133. data/lib/aidp/providers/opencode.rb +0 -139
@@ -397,8 +397,88 @@ module Aidp
397
397
  }
398
398
  end
399
399
 
400
+ # Worktree cleanup tracking methods (issue #367)
401
+
402
+ # Get the timestamp of last worktree cleanup
403
+ # @return [Time, nil] Time of last cleanup or nil if never run
404
+ def last_worktree_cleanup
405
+ timestamp = worktree_cleanup_state["last_cleanup_at"]
406
+ return nil unless timestamp
407
+
408
+ Time.parse(timestamp)
409
+ rescue ArgumentError
410
+ nil
411
+ end
412
+
413
+ # Record a worktree cleanup run
414
+ # @param cleaned [Integer] Number of worktrees cleaned
415
+ # @param skipped [Integer] Number of worktrees skipped
416
+ # @param errors [Array<Hash>] List of errors encountered
417
+ def record_worktree_cleanup(cleaned:, skipped:, errors: [])
418
+ state["worktree_cleanup"] = {
419
+ "last_cleanup_at" => Time.now.utc.iso8601,
420
+ "last_cleaned_count" => cleaned,
421
+ "last_skipped_count" => skipped,
422
+ "last_errors" => errors.map { |e| stringify_keys(e) }
423
+ }
424
+ save!
425
+ end
426
+
427
+ # Get the full worktree cleanup state
428
+ # @return [Hash] Cleanup state data
429
+ def worktree_cleanup_data
430
+ worktree_cleanup_state.dup
431
+ end
432
+
433
+ # Round-robin scheduling state methods (issue #434)
434
+
435
+ # Get the last processed work item key for round-robin rotation
436
+ # @return [String, nil] Last processed key or nil if never processed
437
+ def round_robin_last_key
438
+ round_robin_state["last_key"]
439
+ end
440
+
441
+ # Get the timestamp of the last round-robin processing
442
+ # @return [Time, nil] Time of last processing or nil
443
+ def round_robin_last_processed_at
444
+ timestamp = round_robin_state["processed_at"]
445
+ return nil unless timestamp
446
+
447
+ Time.parse(timestamp)
448
+ rescue ArgumentError
449
+ nil
450
+ end
451
+
452
+ # Record the round-robin rotation position
453
+ # @param last_key [String] Key of the last processed work item
454
+ # @param processed_at [String] ISO8601 timestamp of processing
455
+ def record_round_robin_position(last_key:, processed_at:)
456
+ state["round_robin"] = {
457
+ "last_key" => last_key,
458
+ "processed_at" => processed_at
459
+ }
460
+ save!
461
+
462
+ Aidp.log_debug("state_store", "round_robin_position_recorded",
463
+ last_key: last_key, processed_at: processed_at)
464
+ end
465
+
466
+ # Get the full round-robin state
467
+ # @return [Hash] Round-robin state data
468
+ def round_robin_data
469
+ round_robin_state.dup
470
+ end
471
+
400
472
  private
401
473
 
474
+ def round_robin_state
475
+ state["round_robin"] ||= {}
476
+ end
477
+
478
+ def worktree_cleanup_state
479
+ state["worktree_cleanup"] ||= {}
480
+ end
481
+
402
482
  def ensure_directory
403
483
  FileUtils.mkdir_p(File.dirname(@path))
404
484
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Watch
5
+ # Value object representing a work item in the round-robin queue.
6
+ # Encapsulates issue/PR data with metadata needed for scheduling.
7
+ class WorkItem
8
+ include Comparable
9
+
10
+ ITEM_TYPES = %i[issue pr].freeze
11
+ PROCESSOR_TYPES = %i[plan build auto_issue review ci_fix auto_pr change_request].freeze
12
+
13
+ # Priority levels for scheduling (lower = higher priority)
14
+ PRIORITY_PLAN = 1
15
+ PRIORITY_NORMAL = 2
16
+
17
+ attr_reader :number, :item_type, :processor_type, :label, :data, :priority
18
+
19
+ def initialize(number:, item_type:, processor_type:, label:, data:, priority: nil)
20
+ validate_item_type!(item_type)
21
+ validate_processor_type!(processor_type)
22
+
23
+ @number = number
24
+ @item_type = item_type
25
+ @processor_type = processor_type
26
+ @label = label
27
+ @data = data
28
+ @priority = priority || default_priority
29
+ end
30
+
31
+ # Unique key for this work item (used for queue tracking)
32
+ # @return [String] Unique identifier
33
+ def key
34
+ "#{item_type}_#{number}_#{processor_type}"
35
+ end
36
+
37
+ # Check if this work item is for the same issue/PR
38
+ # @param other [WorkItem] Other work item to compare
39
+ # @return [Boolean] True if same entity
40
+ def same_entity?(other)
41
+ item_type == other.item_type && number == other.number
42
+ end
43
+
44
+ # Check if item is an issue (vs PR)
45
+ # @return [Boolean]
46
+ def issue?
47
+ item_type == :issue
48
+ end
49
+
50
+ # Check if item is a PR
51
+ # @return [Boolean]
52
+ def pr?
53
+ item_type == :pr
54
+ end
55
+
56
+ # Check if this is a high-priority plan item
57
+ # @return [Boolean]
58
+ def plan?
59
+ processor_type == :plan
60
+ end
61
+
62
+ def to_h
63
+ {
64
+ number: number,
65
+ item_type: item_type,
66
+ processor_type: processor_type,
67
+ label: label,
68
+ priority: priority
69
+ }
70
+ end
71
+
72
+ # Comparison for sorting by priority
73
+ def <=>(other)
74
+ priority <=> other.priority
75
+ end
76
+
77
+ private
78
+
79
+ def default_priority
80
+ plan? ? PRIORITY_PLAN : PRIORITY_NORMAL
81
+ end
82
+
83
+ def validate_item_type!(type)
84
+ return if ITEM_TYPES.include?(type)
85
+
86
+ raise ArgumentError, "Invalid item_type: #{type}. Must be one of: #{ITEM_TYPES.join(", ")}"
87
+ end
88
+
89
+ def validate_processor_type!(type)
90
+ return if PROCESSOR_TYPES.include?(type)
91
+
92
+ raise ArgumentError, "Invalid processor_type: #{type}. Must be one of: #{PROCESSOR_TYPES.join(", ")}"
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "time"
5
+
6
+ module Aidp
7
+ module Watch
8
+ # Handles automatic cleanup of worktrees whose branches have been merged into main.
9
+ # Runs periodically during watch mode according to configured frequency.
10
+ #
11
+ # Requirements (from issue #367):
12
+ # - Skip worktrees that are not "clean" (have uncommitted changes)
13
+ # - Run silently with log entries
14
+ # - Skip failed worktrees and retry later
15
+ # - Default to weekly cleanup frequency
16
+ class WorktreeCleanupJob
17
+ SECONDS_PER_DAY = 86_400
18
+ SECONDS_PER_WEEK = 604_800
19
+
20
+ # @param project_dir [String] Project root directory
21
+ # @param config [Hash] Cleanup configuration from aidp.yml
22
+ def initialize(project_dir:, config: {})
23
+ @project_dir = project_dir
24
+ @config = normalize_config(config)
25
+ Aidp.log_debug("worktree_cleanup_job", "initialized",
26
+ project_dir: project_dir,
27
+ config: @config)
28
+ end
29
+
30
+ # Check if cleanup is due based on last cleanup time and configured frequency
31
+ #
32
+ # @param last_cleanup_at [Time, nil] Time of last cleanup run
33
+ # @return [Boolean] True if cleanup should run
34
+ def cleanup_due?(last_cleanup_at)
35
+ return true unless enabled?
36
+ return true if last_cleanup_at.nil?
37
+
38
+ elapsed = Time.now - last_cleanup_at
39
+ elapsed >= cleanup_interval_seconds
40
+ end
41
+
42
+ # Execute cleanup of merged worktrees
43
+ #
44
+ # @return [Hash] Result containing cleaned count, skipped count, and errors
45
+ def execute
46
+ Aidp.log_info("worktree_cleanup_job", "cleanup_started",
47
+ base_branch: base_branch,
48
+ delete_branch: delete_branch?)
49
+
50
+ return {cleaned: 0, skipped: 0, errors: []} unless enabled?
51
+
52
+ worktrees = list_worktrees
53
+ Aidp.log_debug("worktree_cleanup_job", "worktrees_found", count: worktrees.size)
54
+
55
+ cleaned = 0
56
+ skipped = 0
57
+ errors = []
58
+
59
+ worktrees.each do |worktree|
60
+ result = process_worktree(worktree)
61
+ case result[:status]
62
+ when :cleaned
63
+ cleaned += 1
64
+ when :skipped
65
+ skipped += 1
66
+ when :error
67
+ errors << {slug: worktree[:slug], error: result[:error]}
68
+ end
69
+ end
70
+
71
+ Aidp.log_info("worktree_cleanup_job", "cleanup_completed",
72
+ cleaned: cleaned,
73
+ skipped: skipped,
74
+ errors_count: errors.size)
75
+
76
+ {cleaned: cleaned, skipped: skipped, errors: errors}
77
+ end
78
+
79
+ # Check if cleanup is enabled in configuration
80
+ #
81
+ # @return [Boolean]
82
+ def enabled?
83
+ @config[:enabled]
84
+ end
85
+
86
+ # Get the configured cleanup interval in seconds
87
+ #
88
+ # @return [Integer]
89
+ def cleanup_interval_seconds
90
+ case @config[:frequency]
91
+ when "daily"
92
+ SECONDS_PER_DAY
93
+ when "weekly"
94
+ SECONDS_PER_WEEK
95
+ else
96
+ SECONDS_PER_WEEK
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def normalize_config(config)
103
+ {
104
+ enabled: config.fetch(:enabled, config.fetch("enabled", true)),
105
+ frequency: config.fetch(:frequency, config.fetch("frequency", "weekly")),
106
+ base_branch: config.fetch(:base_branch, config.fetch("base_branch", "main")),
107
+ delete_branch: config.fetch(:delete_branch, config.fetch("delete_branch", true))
108
+ }
109
+ end
110
+
111
+ def base_branch
112
+ @config[:base_branch]
113
+ end
114
+
115
+ def delete_branch?
116
+ @config[:delete_branch]
117
+ end
118
+
119
+ def list_worktrees
120
+ Aidp::Worktree.list(project_dir: @project_dir)
121
+ rescue => e
122
+ Aidp.log_error("worktree_cleanup_job", "list_worktrees_failed", error: e.message)
123
+ []
124
+ end
125
+
126
+ def process_worktree(worktree)
127
+ slug = worktree[:slug]
128
+ branch = worktree[:branch]
129
+ path = worktree[:path]
130
+
131
+ Aidp.log_debug("worktree_cleanup_job", "processing_worktree",
132
+ slug: slug,
133
+ branch: branch,
134
+ active: worktree[:active])
135
+
136
+ # Skip inactive worktrees (directory doesn't exist)
137
+ unless worktree[:active]
138
+ Aidp.log_debug("worktree_cleanup_job", "skipping_inactive", slug: slug)
139
+ return {status: :skipped, reason: "inactive"}
140
+ end
141
+
142
+ # Check if worktree is clean (no uncommitted changes)
143
+ unless worktree_clean?(path)
144
+ Aidp.log_debug("worktree_cleanup_job", "skipping_dirty", slug: slug)
145
+ return {status: :skipped, reason: "uncommitted_changes"}
146
+ end
147
+
148
+ # Check if branch is merged into base branch
149
+ unless branch_merged?(branch)
150
+ Aidp.log_debug("worktree_cleanup_job", "skipping_unmerged",
151
+ slug: slug,
152
+ branch: branch,
153
+ base_branch: base_branch)
154
+ return {status: :skipped, reason: "not_merged"}
155
+ end
156
+
157
+ # Remove the worktree
158
+ remove_worktree(slug)
159
+ rescue => e
160
+ Aidp.log_error("worktree_cleanup_job", "process_worktree_error",
161
+ slug: slug,
162
+ error: e.message)
163
+ {status: :error, error: e.message}
164
+ end
165
+
166
+ def worktree_clean?(path)
167
+ return false unless Dir.exist?(path)
168
+
169
+ stdout, _stderr, status = Open3.capture3(
170
+ "git", "status", "--porcelain",
171
+ chdir: path
172
+ )
173
+
174
+ status.success? && stdout.strip.empty?
175
+ rescue => e
176
+ Aidp.log_error("worktree_cleanup_job", "check_clean_failed",
177
+ path: path,
178
+ error: e.message)
179
+ false
180
+ end
181
+
182
+ def branch_merged?(branch)
183
+ stdout, _stderr, status = Open3.capture3(
184
+ "git", "branch", "--merged", base_branch,
185
+ chdir: @project_dir
186
+ )
187
+
188
+ return false unless status.success?
189
+
190
+ # Parse branches that are merged (one per line, with possible leading whitespace or *)
191
+ merged_branches = stdout.lines.map { |line| line.strip.sub(/^\*\s*/, "") }
192
+ merged_branches.include?(branch)
193
+ rescue => e
194
+ Aidp.log_error("worktree_cleanup_job", "check_merged_failed",
195
+ branch: branch,
196
+ error: e.message)
197
+ false
198
+ end
199
+
200
+ def remove_worktree(slug)
201
+ Aidp.log_info("worktree_cleanup_job", "removing_worktree",
202
+ slug: slug,
203
+ delete_branch: delete_branch?)
204
+
205
+ Aidp::Worktree.remove(
206
+ slug: slug,
207
+ project_dir: @project_dir,
208
+ delete_branch: delete_branch?
209
+ )
210
+
211
+ Aidp.log_info("worktree_cleanup_job", "worktree_removed", slug: slug)
212
+ {status: :cleaned}
213
+ rescue Aidp::Worktree::WorktreeNotFound => e
214
+ Aidp.log_warn("worktree_cleanup_job", "worktree_not_found",
215
+ slug: slug,
216
+ error: e.message)
217
+ {status: :skipped, reason: "not_found"}
218
+ rescue => e
219
+ Aidp.log_error("worktree_cleanup_job", "remove_failed",
220
+ slug: slug,
221
+ error: e.message)
222
+ {status: :error, error: e.message}
223
+ end
224
+ end
225
+ end
226
+ end