aidp 0.34.1 → 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 (132) 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/usage_limit.rb +179 -0
  61. data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
  62. data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
  63. data/lib/aidp/harness/usage_period.rb +234 -0
  64. data/lib/aidp/init/agent_instructions_generator.rb +317 -0
  65. data/lib/aidp/init/runner.rb +25 -4
  66. data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
  67. data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
  68. data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
  69. data/lib/aidp/interfaces/logger_interface.rb +150 -0
  70. data/lib/aidp/interfaces/ui_interface.rb +254 -0
  71. data/lib/aidp/logger.rb +25 -7
  72. data/lib/aidp/orchestration_adapter.rb +279 -0
  73. data/lib/aidp/pr_worktree_manager.rb +4 -15
  74. data/lib/aidp/prompts/feedback_collector.rb +198 -0
  75. data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
  76. data/lib/aidp/prompts/template_evolver.rb +305 -0
  77. data/lib/aidp/prompts/template_version_manager.rb +325 -0
  78. data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
  79. data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
  80. data/lib/aidp/setup/wizard.rb +616 -176
  81. data/lib/aidp/shell_executor.rb +58 -0
  82. data/lib/aidp/style_guide/selector.rb +56 -1
  83. data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
  84. data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
  85. data/lib/aidp/temporal/activities/base_activity.rb +78 -0
  86. data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
  87. data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
  88. data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
  89. data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
  90. data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
  91. data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
  92. data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
  93. data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
  94. data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
  95. data/lib/aidp/temporal/configuration.rb +169 -0
  96. data/lib/aidp/temporal/connection.rb +93 -0
  97. data/lib/aidp/temporal/worker.rb +119 -0
  98. data/lib/aidp/temporal/workflow_client.rb +171 -0
  99. data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
  100. data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
  101. data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
  102. data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
  103. data/lib/aidp/temporal.rb +120 -0
  104. data/lib/aidp/version.rb +1 -1
  105. data/lib/aidp/watch/base_processor.rb +35 -0
  106. data/lib/aidp/watch/build_processor.rb +132 -0
  107. data/lib/aidp/watch/github_state_extractor.rb +0 -8
  108. data/lib/aidp/watch/rebase_processor.rb +245 -0
  109. data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
  110. data/lib/aidp/watch/runner.rb +366 -285
  111. data/lib/aidp/watch/state_store.rb +80 -0
  112. data/lib/aidp/watch/work_item.rb +96 -0
  113. data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
  114. data/lib/aidp/watch/worktree_reconciler.rb +643 -0
  115. data/lib/aidp/workflows/guided_agent.rb +1 -1
  116. data/lib/aidp/worktree.rb +62 -0
  117. data/templates/aidp.yml.example +70 -0
  118. metadata +117 -16
  119. data/lib/aidp/analyze/json_file_storage.rb +0 -292
  120. data/lib/aidp/execute/future_work_backlog.rb +0 -411
  121. data/lib/aidp/providers/adapter.rb +0 -239
  122. data/lib/aidp/providers/aider.rb +0 -264
  123. data/lib/aidp/providers/anthropic.rb +0 -614
  124. data/lib/aidp/providers/base.rb +0 -617
  125. data/lib/aidp/providers/capability_registry.rb +0 -204
  126. data/lib/aidp/providers/codex.rb +0 -267
  127. data/lib/aidp/providers/cursor.rb +0 -304
  128. data/lib/aidp/providers/error_taxonomy.rb +0 -195
  129. data/lib/aidp/providers/gemini.rb +0 -110
  130. data/lib/aidp/providers/github_copilot.rb +0 -227
  131. data/lib/aidp/providers/kilocode.rb +0 -157
  132. data/lib/aidp/providers/opencode.rb +0 -139
@@ -0,0 +1,643 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "time"
5
+
6
+ module Aidp
7
+ module Watch
8
+ # Handles reconciliation of dirty worktrees (those with uncommitted changes).
9
+ # Determines appropriate action based on linked issue/PR state:
10
+ # - Open issue/PR: Resume work by triggering build processor
11
+ # - Merged PR: Reconcile changes and create follow-up PR if meaningful diff remains
12
+ # - Closed without merge: Log and optionally clean up
13
+ #
14
+ # This runs as part of the watch mode poll cycle, after cleanup but before sleep.
15
+ class WorktreeReconciler
16
+ # Pattern to extract issue number from worktree slug
17
+ # e.g., "issue-375-thinking-tiers-escalate" -> 375
18
+ ISSUE_SLUG_PATTERN = /^issue-(\d+)-/
19
+
20
+ # Pattern to extract PR number from worktree slug
21
+ # e.g., "pr-456-ci-fix" -> 456
22
+ PR_SLUG_PATTERN = /^pr-(\d+)-/
23
+
24
+ # @param project_dir [String] Project root directory
25
+ # @param repository_client [RepositoryClient] GitHub API client
26
+ # @param build_processor [BuildProcessor] Processor for resuming builds
27
+ # @param state_store [StateStore] For tracking build state
28
+ # @param config [Hash] Configuration options
29
+ def initialize(project_dir:, repository_client:, build_processor:, state_store:, config: {})
30
+ @project_dir = project_dir
31
+ @repository_client = repository_client
32
+ @build_processor = build_processor
33
+ @state_store = state_store
34
+ @config = normalize_config(config)
35
+
36
+ Aidp.log_debug("worktree_reconciler", "initialized",
37
+ project_dir: project_dir,
38
+ config: @config)
39
+ end
40
+
41
+ # Check if reconciliation is due based on last run time
42
+ #
43
+ # @param last_reconcile_at [Time, nil] Time of last reconciliation run
44
+ # @return [Boolean] True if reconciliation should run
45
+ def reconciliation_due?(last_reconcile_at)
46
+ return false unless enabled?
47
+ return true if last_reconcile_at.nil?
48
+
49
+ elapsed = Time.now - last_reconcile_at
50
+ elapsed >= reconciliation_interval_seconds
51
+ end
52
+
53
+ # Execute reconciliation of dirty worktrees
54
+ #
55
+ # @return [Hash] Result containing resumed, reconciled, skipped counts and errors
56
+ def execute
57
+ Aidp.log_info("worktree_reconciler", "reconciliation_started")
58
+
59
+ return {resumed: 0, reconciled: 0, cleaned: 0, skipped: 0, errors: []} unless enabled?
60
+
61
+ worktrees = list_dirty_worktrees
62
+ Aidp.log_debug("worktree_reconciler", "dirty_worktrees_found", count: worktrees.size)
63
+
64
+ results = {resumed: 0, reconciled: 0, cleaned: 0, skipped: 0, errors: []}
65
+
66
+ worktrees.each do |worktree|
67
+ result = process_dirty_worktree(worktree)
68
+ case result[:action]
69
+ when :resumed
70
+ results[:resumed] += 1
71
+ when :reconciled
72
+ results[:reconciled] += 1
73
+ when :cleaned
74
+ results[:cleaned] += 1
75
+ when :skipped
76
+ results[:skipped] += 1
77
+ when :error
78
+ results[:errors] << {slug: worktree[:slug], error: result[:error]}
79
+ end
80
+ end
81
+
82
+ Aidp.log_info("worktree_reconciler", "reconciliation_completed",
83
+ resumed: results[:resumed],
84
+ reconciled: results[:reconciled],
85
+ cleaned: results[:cleaned],
86
+ skipped: results[:skipped],
87
+ errors_count: results[:errors].size)
88
+
89
+ results
90
+ end
91
+
92
+ # Check if reconciliation is enabled
93
+ #
94
+ # @return [Boolean]
95
+ def enabled?
96
+ @config[:enabled]
97
+ end
98
+
99
+ # Get the configured reconciliation interval in seconds
100
+ #
101
+ # @return [Integer]
102
+ def reconciliation_interval_seconds
103
+ @config[:interval_seconds]
104
+ end
105
+
106
+ private
107
+
108
+ def normalize_config(config)
109
+ {
110
+ enabled: config.fetch(:enabled, config.fetch("enabled", true)),
111
+ # Default to checking every 5 minutes
112
+ interval_seconds: config.fetch(:interval_seconds, config.fetch("interval_seconds", 300)),
113
+ base_branch: config.fetch(:base_branch, config.fetch("base_branch", "main")),
114
+ # Auto-resume open issues by default
115
+ auto_resume: config.fetch(:auto_resume, config.fetch("auto_resume", true)),
116
+ # Auto-reconcile merged PRs by default
117
+ auto_reconcile: config.fetch(:auto_reconcile, config.fetch("auto_reconcile", true))
118
+ }
119
+ end
120
+
121
+ def base_branch
122
+ @config[:base_branch]
123
+ end
124
+
125
+ # List all worktrees that have uncommitted changes
126
+ #
127
+ # @return [Array<Hash>] Worktrees with uncommitted changes
128
+ def list_dirty_worktrees
129
+ all_worktrees = Aidp::Worktree.list(project_dir: @project_dir)
130
+
131
+ all_worktrees.select do |worktree|
132
+ next false unless worktree[:active]
133
+ next false unless Dir.exist?(worktree[:path])
134
+
135
+ !worktree_clean?(worktree[:path])
136
+ end
137
+ rescue => e
138
+ Aidp.log_error("worktree_reconciler", "list_worktrees_failed", error: e.message)
139
+ []
140
+ end
141
+
142
+ def worktree_clean?(path)
143
+ stdout, _stderr, status = Open3.capture3(
144
+ "git", "status", "--porcelain",
145
+ chdir: path
146
+ )
147
+
148
+ status.success? && stdout.strip.empty?
149
+ rescue => e
150
+ Aidp.log_error("worktree_reconciler", "check_clean_failed",
151
+ path: path,
152
+ error: e.message)
153
+ true # Assume clean on error to avoid false positives
154
+ end
155
+
156
+ # Process a single dirty worktree
157
+ #
158
+ # @param worktree [Hash] Worktree info from registry
159
+ # @return [Hash] Result with :action and optional :error
160
+ def process_dirty_worktree(worktree)
161
+ slug = worktree[:slug]
162
+ branch = worktree[:branch]
163
+
164
+ Aidp.log_debug("worktree_reconciler", "processing_dirty_worktree",
165
+ slug: slug,
166
+ branch: branch)
167
+
168
+ # Determine what this worktree is for
169
+ issue_number = extract_issue_number(slug)
170
+ pr_number = extract_pr_number(slug) || find_pr_for_branch(branch)
171
+
172
+ if pr_number
173
+ process_pr_worktree(worktree, pr_number, issue_number)
174
+ elsif issue_number
175
+ process_issue_worktree(worktree, issue_number)
176
+ else
177
+ Aidp.log_debug("worktree_reconciler", "orphan_worktree",
178
+ slug: slug,
179
+ reason: "no_linked_issue_or_pr")
180
+ {action: :skipped, reason: "orphan_worktree"}
181
+ end
182
+ rescue => e
183
+ Aidp.log_error("worktree_reconciler", "process_worktree_error",
184
+ slug: slug,
185
+ error: e.message)
186
+ {action: :error, error: e.message}
187
+ end
188
+
189
+ # Process a worktree linked to a PR
190
+ #
191
+ # @param worktree [Hash] Worktree info
192
+ # @param pr_number [Integer] PR number
193
+ # @param issue_number [Integer, nil] Linked issue number if known
194
+ # @return [Hash] Result
195
+ def process_pr_worktree(worktree, pr_number, issue_number)
196
+ pr = fetch_pr_state(pr_number)
197
+
198
+ unless pr
199
+ Aidp.log_debug("worktree_reconciler", "pr_not_found",
200
+ pr_number: pr_number,
201
+ slug: worktree[:slug])
202
+ return {action: :skipped, reason: "pr_not_found"}
203
+ end
204
+
205
+ case pr[:state]
206
+ when "MERGED"
207
+ reconcile_merged_pr(worktree, pr, issue_number)
208
+ when "OPEN"
209
+ resume_open_pr(worktree, pr, issue_number)
210
+ when "CLOSED"
211
+ handle_closed_pr(worktree, pr)
212
+ else
213
+ {action: :skipped, reason: "unknown_pr_state"}
214
+ end
215
+ end
216
+
217
+ # Process a worktree linked to an issue (no PR yet)
218
+ #
219
+ # @param worktree [Hash] Worktree info
220
+ # @param issue_number [Integer] Issue number
221
+ # @return [Hash] Result
222
+ def process_issue_worktree(worktree, issue_number)
223
+ issue = fetch_issue_state(issue_number)
224
+
225
+ unless issue
226
+ Aidp.log_debug("worktree_reconciler", "issue_not_found",
227
+ issue_number: issue_number,
228
+ slug: worktree[:slug])
229
+ return {action: :skipped, reason: "issue_not_found"}
230
+ end
231
+
232
+ if issue[:state] == "OPEN"
233
+ resume_open_issue(worktree, issue)
234
+ else
235
+ Aidp.log_debug("worktree_reconciler", "issue_closed",
236
+ issue_number: issue_number,
237
+ slug: worktree[:slug])
238
+ # Issue is closed but we have uncommitted changes
239
+ # This could be work that was done but never pushed
240
+ {action: :skipped, reason: "issue_closed_with_uncommitted_changes"}
241
+ end
242
+ end
243
+
244
+ # Reconcile a worktree whose PR has been merged
245
+ #
246
+ # @param worktree [Hash] Worktree info
247
+ # @param pr [Hash] PR data
248
+ # @param issue_number [Integer, nil] Linked issue number
249
+ # @return [Hash] Result
250
+ def reconcile_merged_pr(worktree, pr, issue_number)
251
+ return {action: :skipped, reason: "auto_reconcile_disabled"} unless @config[:auto_reconcile]
252
+
253
+ slug = worktree[:slug]
254
+ path = worktree[:path]
255
+
256
+ Aidp.log_info("worktree_reconciler", "reconciling_merged_pr",
257
+ slug: slug,
258
+ pr_number: pr[:number])
259
+
260
+ # Fetch latest base branch to compare against
261
+ fetch_base_branch(path)
262
+
263
+ # Get diff between worktree and merged base branch
264
+ remaining_changes = get_remaining_diff(path, pr[:base_branch] || base_branch)
265
+
266
+ if remaining_changes.empty?
267
+ # No meaningful changes remain - clean up
268
+ Aidp.log_info("worktree_reconciler", "no_remaining_changes",
269
+ slug: slug,
270
+ pr_number: pr[:number])
271
+ cleanup_worktree(slug)
272
+ {action: :cleaned, reason: "no_remaining_changes_after_merge"}
273
+ else
274
+ # There are changes that weren't in the merged PR
275
+ Aidp.log_info("worktree_reconciler", "remaining_changes_found",
276
+ slug: slug,
277
+ pr_number: pr[:number],
278
+ files_changed: remaining_changes.size)
279
+ create_followup_pr(worktree, pr, remaining_changes, issue_number)
280
+ {action: :reconciled, reason: "followup_pr_created"}
281
+ end
282
+ rescue => e
283
+ Aidp.log_error("worktree_reconciler", "reconcile_merged_pr_failed",
284
+ slug: slug,
285
+ error: e.message)
286
+ {action: :error, error: e.message}
287
+ end
288
+
289
+ # Resume work on an open PR
290
+ #
291
+ # @param worktree [Hash] Worktree info
292
+ # @param pr [Hash] PR data
293
+ # @param issue_number [Integer, nil] Linked issue number
294
+ # @return [Hash] Result
295
+ def resume_open_pr(worktree, pr, issue_number)
296
+ return {action: :skipped, reason: "auto_resume_disabled"} unless @config[:auto_resume]
297
+
298
+ # For open PRs with uncommitted changes, we just log it
299
+ # The change-request processor should handle this if there are pending changes
300
+ Aidp.log_info("worktree_reconciler", "open_pr_with_uncommitted_changes",
301
+ slug: worktree[:slug],
302
+ pr_number: pr[:number])
303
+
304
+ # If we have a linked issue, try to resume via build processor
305
+ if issue_number
306
+ resume_via_build_processor(worktree, issue_number)
307
+ else
308
+ {action: :skipped, reason: "open_pr_needs_manual_attention"}
309
+ end
310
+ end
311
+
312
+ # Resume work on an open issue
313
+ #
314
+ # @param worktree [Hash] Worktree info
315
+ # @param issue [Hash] Issue data
316
+ # @return [Hash] Result
317
+ def resume_open_issue(worktree, issue)
318
+ return {action: :skipped, reason: "auto_resume_disabled"} unless @config[:auto_resume]
319
+
320
+ # Check if issue has the build label
321
+ has_build_label = issue[:labels]&.any? { |l| l["name"] == @build_processor.build_label }
322
+
323
+ if has_build_label
324
+ resume_via_build_processor(worktree, issue[:number])
325
+ else
326
+ Aidp.log_debug("worktree_reconciler", "issue_missing_build_label",
327
+ issue_number: issue[:number],
328
+ slug: worktree[:slug])
329
+ {action: :skipped, reason: "issue_missing_build_label"}
330
+ end
331
+ end
332
+
333
+ # Handle a closed (not merged) PR
334
+ #
335
+ # @param worktree [Hash] Worktree info
336
+ # @param pr [Hash] PR data
337
+ # @return [Hash] Result
338
+ def handle_closed_pr(worktree, pr)
339
+ Aidp.log_info("worktree_reconciler", "closed_pr_with_uncommitted_changes",
340
+ slug: worktree[:slug],
341
+ pr_number: pr[:number])
342
+
343
+ # Don't automatically delete - user might want to review the work
344
+ {action: :skipped, reason: "pr_closed_without_merge"}
345
+ end
346
+
347
+ # Resume work via the build processor
348
+ #
349
+ # @param worktree [Hash] Worktree info
350
+ # @param issue_number [Integer] Issue number
351
+ # @return [Hash] Result
352
+ def resume_via_build_processor(worktree, issue_number)
353
+ Aidp.log_info("worktree_reconciler", "resuming_work",
354
+ slug: worktree[:slug],
355
+ issue_number: issue_number)
356
+
357
+ # Fetch the full issue details
358
+ issue = @repository_client.fetch_issue(issue_number)
359
+
360
+ unless issue
361
+ return {action: :error, error: "failed_to_fetch_issue"}
362
+ end
363
+
364
+ # Trigger the build processor
365
+ @build_processor.process(issue)
366
+
367
+ {action: :resumed, reason: "build_processor_triggered"}
368
+ rescue => e
369
+ Aidp.log_error("worktree_reconciler", "resume_failed",
370
+ slug: worktree[:slug],
371
+ issue_number: issue_number,
372
+ error: e.message)
373
+ {action: :error, error: e.message}
374
+ end
375
+
376
+ # Extract issue number from worktree slug
377
+ #
378
+ # @param slug [String] Worktree slug
379
+ # @return [Integer, nil] Issue number or nil
380
+ def extract_issue_number(slug)
381
+ match = slug.match(ISSUE_SLUG_PATTERN)
382
+ match ? match[1].to_i : nil
383
+ end
384
+
385
+ # Extract PR number from worktree slug
386
+ #
387
+ # @param slug [String] Worktree slug
388
+ # @return [Integer, nil] PR number or nil
389
+ def extract_pr_number(slug)
390
+ match = slug.match(PR_SLUG_PATTERN)
391
+ match ? match[1].to_i : nil
392
+ end
393
+
394
+ # Find PR number for a given branch
395
+ #
396
+ # @param branch [String] Branch name
397
+ # @return [Integer, nil] PR number or nil
398
+ def find_pr_for_branch(branch)
399
+ # Use gh CLI to find PR for this branch
400
+ stdout, _stderr, status = Open3.capture3(
401
+ "gh", "pr", "list",
402
+ "--repo", @repository_client.full_repo,
403
+ "--head", branch,
404
+ "--state", "all",
405
+ "--json", "number",
406
+ "--limit", "1"
407
+ )
408
+
409
+ return nil unless status.success?
410
+
411
+ prs = JSON.parse(stdout)
412
+ prs.first&.dig("number")
413
+ rescue => e
414
+ Aidp.log_debug("worktree_reconciler", "find_pr_for_branch_failed",
415
+ branch: branch,
416
+ error: e.message)
417
+ nil
418
+ end
419
+
420
+ # Fetch PR state from GitHub
421
+ #
422
+ # @param pr_number [Integer] PR number
423
+ # @return [Hash, nil] PR data or nil
424
+ def fetch_pr_state(pr_number)
425
+ stdout, _stderr, status = Open3.capture3(
426
+ "gh", "pr", "view", pr_number.to_s,
427
+ "--repo", @repository_client.full_repo,
428
+ "--json", "number,state,mergedAt,baseRefName,headRefName,title"
429
+ )
430
+
431
+ return nil unless status.success?
432
+
433
+ data = JSON.parse(stdout)
434
+ {
435
+ number: data["number"],
436
+ state: data["state"],
437
+ merged_at: data["mergedAt"],
438
+ base_branch: data["baseRefName"],
439
+ head_branch: data["headRefName"],
440
+ title: data["title"]
441
+ }
442
+ rescue => e
443
+ Aidp.log_debug("worktree_reconciler", "fetch_pr_state_failed",
444
+ pr_number: pr_number,
445
+ error: e.message)
446
+ nil
447
+ end
448
+
449
+ # Fetch issue state from GitHub
450
+ #
451
+ # @param issue_number [Integer] Issue number
452
+ # @return [Hash, nil] Issue data or nil
453
+ def fetch_issue_state(issue_number)
454
+ stdout, _stderr, status = Open3.capture3(
455
+ "gh", "issue", "view", issue_number.to_s,
456
+ "--repo", @repository_client.full_repo,
457
+ "--json", "number,state,title,labels"
458
+ )
459
+
460
+ return nil unless status.success?
461
+
462
+ data = JSON.parse(stdout)
463
+ {
464
+ number: data["number"],
465
+ state: data["state"],
466
+ title: data["title"],
467
+ labels: data["labels"]
468
+ }
469
+ rescue => e
470
+ Aidp.log_debug("worktree_reconciler", "fetch_issue_state_failed",
471
+ issue_number: issue_number,
472
+ error: e.message)
473
+ nil
474
+ end
475
+
476
+ # Fetch the latest base branch in the worktree
477
+ #
478
+ # @param path [String] Worktree path
479
+ def fetch_base_branch(path)
480
+ Open3.capture3("git", "fetch", "origin", base_branch, chdir: path)
481
+ end
482
+
483
+ # Get list of files with remaining changes after merge
484
+ #
485
+ # @param path [String] Worktree path
486
+ # @param target_branch [String] Branch to compare against
487
+ # @return [Array<String>] List of changed files
488
+ def get_remaining_diff(path, target_branch)
489
+ # First, get list of modified/untracked files in worktree
490
+ stdout, _stderr, status = Open3.capture3(
491
+ "git", "status", "--porcelain",
492
+ chdir: path
493
+ )
494
+
495
+ return [] unless status.success?
496
+
497
+ changed_files = stdout.lines.map { |line| line[3..].strip }.reject(&:empty?)
498
+ return [] if changed_files.empty?
499
+
500
+ # For each changed file, check if it differs from the target branch
501
+ remaining = []
502
+ changed_files.each do |file|
503
+ # Check if file content differs from target branch
504
+ diff_out, _diff_err, diff_status = Open3.capture3(
505
+ "git", "diff", "origin/#{target_branch}", "--", file,
506
+ chdir: path
507
+ )
508
+
509
+ # If diff is non-empty, this file has changes not in target branch
510
+ remaining << file if diff_status.success? && !diff_out.strip.empty?
511
+ end
512
+
513
+ remaining
514
+ rescue => e
515
+ Aidp.log_error("worktree_reconciler", "get_remaining_diff_failed",
516
+ path: path,
517
+ error: e.message)
518
+ []
519
+ end
520
+
521
+ # Create a follow-up PR for remaining changes
522
+ #
523
+ # @param worktree [Hash] Worktree info
524
+ # @param original_pr [Hash] Original merged PR data
525
+ # @param changed_files [Array<String>] Files with changes
526
+ # @param issue_number [Integer, nil] Linked issue number
527
+ def create_followup_pr(worktree, original_pr, changed_files, issue_number)
528
+ path = worktree[:path]
529
+ slug = worktree[:slug]
530
+
531
+ Aidp.log_info("worktree_reconciler", "creating_followup_pr",
532
+ slug: slug,
533
+ original_pr: original_pr[:number],
534
+ changed_files: changed_files.size)
535
+
536
+ # Create a new branch for the follow-up
537
+ followup_branch = "#{worktree[:branch]}-followup-#{Time.now.to_i}"
538
+
539
+ Dir.chdir(path) do
540
+ # Checkout a new branch from current state
541
+ Open3.capture3("git", "checkout", "-b", followup_branch)
542
+
543
+ # Stage all changes
544
+ Open3.capture3("git", "add", "-A")
545
+
546
+ # Build commit message parts
547
+ issue_ref = issue_number ? "Related issue: ##{issue_number}\n" : ""
548
+ files_list = changed_files.map { |f| "- #{f}" }.join("\n")
549
+
550
+ commit_message = <<~MSG
551
+ Follow-up changes from PR ##{original_pr[:number]}
552
+
553
+ These changes were found in a local worktree after PR ##{original_pr[:number]}
554
+ was merged from another machine. This follow-up PR captures the remaining
555
+ uncommitted work.
556
+
557
+ Original PR: ##{original_pr[:number]} - #{original_pr[:title]}
558
+ #{issue_ref}
559
+ Changed files:
560
+ #{files_list}
561
+
562
+ 🤖 Generated with [Claude Code](https://claude.com/claude-code)
563
+
564
+ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
565
+ MSG
566
+
567
+ Open3.capture3("git", "commit", "-m", commit_message)
568
+
569
+ # Push the branch
570
+ Open3.capture3("git", "push", "-u", "origin", followup_branch)
571
+
572
+ # Build PR body parts
573
+ issue_context = issue_number ? "- Related issue: ##{issue_number}\n" : ""
574
+ files_md = changed_files.map { |f| "- `#{f}`" }.join("\n")
575
+
576
+ pr_body = <<~BODY
577
+ ## Summary
578
+
579
+ This PR captures follow-up changes that were found in a local worktree after
580
+ PR ##{original_pr[:number]} was merged from another machine.
581
+
582
+ ## Context
583
+
584
+ - Original PR: ##{original_pr[:number]}
585
+ #{issue_context}
586
+ ## Changed Files
587
+
588
+ #{files_md}
589
+
590
+ ## Review Notes
591
+
592
+ Please review these changes carefully - they represent uncommitted work that
593
+ was not included in the original PR merge.
594
+
595
+ 🤖 Generated with [Claude Code](https://claude.com/claude-code)
596
+ BODY
597
+
598
+ pr_title = "Follow-up: Additional changes from PR ##{original_pr[:number]}"
599
+
600
+ stdout, stderr, status = Open3.capture3(
601
+ "gh", "pr", "create",
602
+ "--repo", @repository_client.full_repo,
603
+ "--title", pr_title,
604
+ "--body", pr_body,
605
+ "--base", original_pr[:base_branch] || base_branch,
606
+ "--head", followup_branch
607
+ )
608
+
609
+ if status.success?
610
+ Aidp.log_info("worktree_reconciler", "followup_pr_created",
611
+ slug: slug,
612
+ pr_url: stdout.strip)
613
+ else
614
+ Aidp.log_error("worktree_reconciler", "followup_pr_creation_failed",
615
+ slug: slug,
616
+ error: stderr)
617
+ end
618
+ end
619
+ rescue => e
620
+ Aidp.log_error("worktree_reconciler", "create_followup_pr_failed",
621
+ slug: slug,
622
+ error: e.message)
623
+ end
624
+
625
+ # Clean up a worktree
626
+ #
627
+ # @param slug [String] Worktree slug
628
+ def cleanup_worktree(slug)
629
+ Aidp.log_info("worktree_reconciler", "cleaning_up_worktree", slug: slug)
630
+
631
+ Aidp::Worktree.remove(
632
+ slug: slug,
633
+ project_dir: @project_dir,
634
+ delete_branch: true
635
+ )
636
+ rescue => e
637
+ Aidp.log_error("worktree_reconciler", "cleanup_failed",
638
+ slug: slug,
639
+ error: e.message)
640
+ end
641
+ end
642
+ end
643
+ end
@@ -38,7 +38,7 @@ module Aidp
38
38
  @conversation_history = []
39
39
  @user_input = {}
40
40
  @verbose = verbose
41
- @debug_env = ENV["DEBUG"] == "1" || ENV["DEBUG"] == "2"
41
+ @debug_env = debug_level >= Aidp::DebugMixin::DEBUG_BASIC
42
42
  end
43
43
 
44
44
  # Main entry point for guided workflow selection