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
@@ -0,0 +1,693 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "json"
5
+ require "fileutils"
6
+ require_relative "../config_paths"
7
+ require_relative "../database"
8
+
9
+ module Aidp
10
+ module Database
11
+ # Migrates file-based storage to SQLite database
12
+ #
13
+ # Handles migration of all .aidp directory files to the new SQLite storage.
14
+ # Supports dry-run mode, backup creation, and rollback.
15
+ #
16
+ # Usage:
17
+ # migrator = StorageMigrator.new(project_dir: Dir.pwd)
18
+ # migrator.migrate!
19
+ #
20
+ class StorageMigrator
21
+ class MigrationError < StandardError; end
22
+
23
+ attr_reader :project_dir, :stats, :errors
24
+
25
+ def initialize(project_dir: Dir.pwd, dry_run: false)
26
+ @project_dir = project_dir
27
+ @dry_run = dry_run
28
+ @stats = Hash.new(0)
29
+ @errors = []
30
+ end
31
+
32
+ # Check if migration is needed
33
+ #
34
+ # @return [Boolean] True if file-based storage exists
35
+ def migration_needed?
36
+ file_storage_exists?
37
+ end
38
+
39
+ # Check if database already has data
40
+ #
41
+ # @return [Boolean] True if database has migrated data
42
+ def already_migrated?
43
+ return false unless File.exist?(ConfigPaths.database_file(project_dir))
44
+
45
+ Database::Migrations.run!(project_dir)
46
+ # Check if any tables have data
47
+ db = Database.connection(project_dir)
48
+ tables = %w[checkpoints tasks progress harness_state workstreams watch_state]
49
+ tables.any? do |table|
50
+ count = db.get_first_value("SELECT COUNT(*) FROM #{table}")
51
+ count.to_i > 0
52
+ end
53
+ rescue
54
+ false
55
+ end
56
+
57
+ # Run the migration
58
+ #
59
+ # @param backup [Boolean] Create backup before migration
60
+ # @return [Hash] Migration results
61
+ def migrate!(backup: true)
62
+ Aidp.log_info("storage_migrator", "starting_migration",
63
+ project_dir: project_dir, dry_run: @dry_run)
64
+
65
+ unless migration_needed?
66
+ Aidp.log_info("storage_migrator", "no_migration_needed")
67
+ return {status: :skipped, reason: "No file-based storage found"}
68
+ end
69
+
70
+ create_backup if backup && !@dry_run
71
+ Database::Migrations.run!(project_dir) unless @dry_run
72
+
73
+ migrate_checkpoints
74
+ migrate_tasks
75
+ migrate_progress
76
+ migrate_harness_state
77
+ migrate_workstreams
78
+ migrate_watch_state
79
+ migrate_worktrees
80
+ migrate_evaluations
81
+ migrate_provider_info
82
+ migrate_model_cache
83
+ migrate_deprecated_models
84
+ migrate_secrets
85
+ migrate_prompt_archive
86
+ migrate_jobs
87
+ migrate_provider_metrics
88
+
89
+ result = {
90
+ status: @errors.empty? ? :success : :partial,
91
+ stats: @stats.dup,
92
+ errors: @errors.dup,
93
+ dry_run: @dry_run
94
+ }
95
+
96
+ Aidp.log_info("storage_migrator", "migration_complete",
97
+ status: result[:status], stats: @stats)
98
+
99
+ result
100
+ end
101
+
102
+ # Create backup of .aidp directory
103
+ #
104
+ # @return [String] Backup directory path
105
+ def create_backup
106
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
107
+ backup_dir = File.join(project_dir, ".aidp_backup_#{timestamp}")
108
+
109
+ Aidp.log_info("storage_migrator", "creating_backup", backup_dir: backup_dir)
110
+
111
+ FileUtils.cp_r(ConfigPaths.aidp_dir(project_dir), backup_dir)
112
+ @stats[:backup_created] = 1
113
+ backup_dir
114
+ end
115
+
116
+ # Clean up old file-based storage after successful migration
117
+ #
118
+ # @param keep_config [Boolean] Keep aidp.yml config file
119
+ def cleanup_old_storage!(keep_config: true)
120
+ return if @dry_run
121
+
122
+ Aidp.log_info("storage_migrator", "cleaning_up_old_storage")
123
+
124
+ files_to_remove = migrated_files
125
+ files_to_remove << ConfigPaths.config_file(project_dir) unless keep_config
126
+
127
+ files_to_remove.each do |file|
128
+ next unless File.exist?(file)
129
+
130
+ FileUtils.rm_rf(file)
131
+ @stats[:files_removed] += 1
132
+ end
133
+
134
+ # Remove empty directories
135
+ cleanup_empty_directories
136
+ end
137
+
138
+ private
139
+
140
+ def file_storage_exists?
141
+ files = [
142
+ ConfigPaths.checkpoint_file(project_dir),
143
+ ConfigPaths.checkpoint_history_file(project_dir),
144
+ File.join(ConfigPaths.aidp_dir(project_dir), "tasks.json"),
145
+ ConfigPaths.execute_progress_file(project_dir),
146
+ ConfigPaths.analyze_progress_file(project_dir)
147
+ ]
148
+
149
+ dirs = [
150
+ ConfigPaths.harness_state_dir(project_dir),
151
+ ConfigPaths.providers_dir(project_dir),
152
+ ConfigPaths.evaluations_dir(project_dir),
153
+ ConfigPaths.jobs_dir(project_dir)
154
+ ]
155
+
156
+ files.any? { |f| File.exist?(f) } || dirs.any? { |d| Dir.exist?(d) && !Dir.empty?(d) }
157
+ end
158
+
159
+ def migrated_files
160
+ [
161
+ ConfigPaths.checkpoint_file(project_dir),
162
+ ConfigPaths.checkpoint_history_file(project_dir),
163
+ File.join(ConfigPaths.aidp_dir(project_dir), "tasks.json"),
164
+ ConfigPaths.execute_progress_file(project_dir),
165
+ ConfigPaths.analyze_progress_file(project_dir),
166
+ File.join(ConfigPaths.aidp_dir(project_dir), "worktrees.json"),
167
+ File.join(ConfigPaths.aidp_dir(project_dir), "pr_worktrees.json"),
168
+ File.join(ConfigPaths.aidp_dir(project_dir), "workstreams.json"),
169
+ File.join(ConfigPaths.aidp_dir(project_dir), "watch_state.yml"),
170
+ File.join(ConfigPaths.aidp_dir(project_dir), "watch_results.yml"),
171
+ File.join(ConfigPaths.aidp_dir(project_dir), "deprecated_models.json"),
172
+ File.join(ConfigPaths.aidp_dir(project_dir), "provider_metrics.yml"),
173
+ File.join(ConfigPaths.aidp_dir(project_dir), "provider_rate_limits.yml"),
174
+ ConfigPaths.secrets_registry_file(project_dir),
175
+ ConfigPaths.harness_state_dir(project_dir),
176
+ ConfigPaths.providers_dir(project_dir),
177
+ ConfigPaths.evaluations_dir(project_dir),
178
+ ConfigPaths.jobs_dir(project_dir),
179
+ ConfigPaths.model_cache_dir(project_dir),
180
+ File.join(ConfigPaths.aidp_dir(project_dir), "prompt_archive")
181
+ ]
182
+ end
183
+
184
+ def cleanup_empty_directories
185
+ dirs = [
186
+ ConfigPaths.progress_dir(project_dir),
187
+ ConfigPaths.harness_state_dir(project_dir),
188
+ ConfigPaths.providers_dir(project_dir),
189
+ ConfigPaths.evaluations_dir(project_dir),
190
+ ConfigPaths.jobs_dir(project_dir),
191
+ ConfigPaths.model_cache_dir(project_dir),
192
+ ConfigPaths.security_dir(project_dir),
193
+ File.join(ConfigPaths.aidp_dir(project_dir), "prompt_archive")
194
+ ]
195
+
196
+ dirs.each do |dir|
197
+ next unless Dir.exist?(dir)
198
+ FileUtils.rm_rf(dir) if Dir.empty?(dir)
199
+ end
200
+ end
201
+
202
+ # Migration methods for each data type
203
+
204
+ def migrate_checkpoints
205
+ checkpoint_file = ConfigPaths.checkpoint_file(project_dir)
206
+ history_file = ConfigPaths.checkpoint_history_file(project_dir)
207
+
208
+ return unless File.exist?(checkpoint_file) || File.exist?(history_file)
209
+
210
+ Aidp.log_debug("storage_migrator", "migrating_checkpoints")
211
+
212
+ require_relative "repositories/checkpoint_repository"
213
+ repo = Repositories::CheckpointRepository.new(project_dir: project_dir)
214
+
215
+ # Migrate current checkpoint
216
+ if File.exist?(checkpoint_file)
217
+ data = YAML.safe_load_file(checkpoint_file, permitted_classes: [Time, Symbol])
218
+ if data && !@dry_run
219
+ repo.save_checkpoint(
220
+ step_name: data["step"] || data[:step],
221
+ iteration: data["iteration"] || data[:iteration] || data["step_index"] || data[:step_index] || 0,
222
+ status: data["status"] || data[:status] || "unknown",
223
+ run_loop_started_at: data["run_loop_started_at"] || data[:run_loop_started_at],
224
+ metrics: data
225
+ )
226
+ end
227
+ @stats[:checkpoints_migrated] += 1
228
+ end
229
+
230
+ # Migrate checkpoint history
231
+ if File.exist?(history_file)
232
+ File.foreach(history_file) do |line|
233
+ next if line.strip.empty?
234
+ entry = JSON.parse(line, symbolize_names: true)
235
+ unless @dry_run
236
+ repo.append_history(
237
+ step_name: entry[:step],
238
+ iteration: entry[:iteration] || entry[:step_index] || 0,
239
+ status: entry[:status] || "completed",
240
+ metrics: entry
241
+ )
242
+ end
243
+ @stats[:checkpoint_history_migrated] += 1
244
+ rescue JSON::ParserError => e
245
+ @errors << {type: :checkpoint_history, error: e.message}
246
+ end
247
+ end
248
+ rescue => e
249
+ @errors << {type: :checkpoints, error: e.message}
250
+ Aidp.log_error("storage_migrator", "checkpoint_migration_failed", error: e.message)
251
+ end
252
+
253
+ def migrate_tasks
254
+ tasks_file = File.join(ConfigPaths.aidp_dir(project_dir), "tasks.json")
255
+ return unless File.exist?(tasks_file)
256
+
257
+ Aidp.log_debug("storage_migrator", "migrating_tasks")
258
+
259
+ require_relative "repositories/task_repository"
260
+ repo = Repositories::TaskRepository.new(project_dir: project_dir)
261
+
262
+ data = JSON.parse(File.read(tasks_file), symbolize_names: true)
263
+ tasks = data[:tasks] || data["tasks"] || []
264
+
265
+ tasks.each do |task|
266
+ unless @dry_run
267
+ repo.create(
268
+ description: task[:title] || task["title"] || task[:description] || task["description"],
269
+ priority: task[:priority] || task["priority"] || "medium",
270
+ tags: task[:tags] || task["tags"] || []
271
+ )
272
+ end
273
+ @stats[:tasks_migrated] += 1
274
+ end
275
+ rescue => e
276
+ @errors << {type: :tasks, error: e.message}
277
+ Aidp.log_error("storage_migrator", "tasks_migration_failed", error: e.message)
278
+ end
279
+
280
+ def migrate_progress
281
+ %w[execute analyze].each do |mode|
282
+ progress_file = File.join(ConfigPaths.progress_dir(project_dir), "#{mode}.yml")
283
+ next unless File.exist?(progress_file)
284
+
285
+ Aidp.log_debug("storage_migrator", "migrating_progress", mode: mode)
286
+
287
+ require_relative "repositories/progress_repository"
288
+ repo = Repositories::ProgressRepository.new(project_dir: project_dir)
289
+
290
+ data = YAML.safe_load_file(progress_file, permitted_classes: [Time, Symbol])
291
+ next unless data
292
+
293
+ now = Time.now.utc.strftime("%Y-%m-%d %H:%M:%S")
294
+ unless @dry_run
295
+ repo.upsert_progress(
296
+ mode: mode,
297
+ current_step: data["current_step"] || data[:current_step],
298
+ steps_completed: data["steps_completed"] || data[:steps_completed] || [],
299
+ started_at: data["started_at"] || data[:started_at] || now,
300
+ updated_at: now
301
+ )
302
+ end
303
+ @stats[:progress_migrated] += 1
304
+ end
305
+ rescue => e
306
+ @errors << {type: :progress, error: e.message}
307
+ Aidp.log_error("storage_migrator", "progress_migration_failed", error: e.message)
308
+ end
309
+
310
+ def migrate_harness_state
311
+ state_dir = ConfigPaths.harness_state_dir(project_dir)
312
+ return unless Dir.exist?(state_dir)
313
+
314
+ Aidp.log_debug("storage_migrator", "migrating_harness_state")
315
+
316
+ require_relative "repositories/harness_state_repository"
317
+ repo = Repositories::HarnessStateRepository.new(project_dir: project_dir)
318
+
319
+ Dir.glob(File.join(state_dir, "*_state.json")).each do |state_file|
320
+ mode = File.basename(state_file, "_state.json")
321
+ data = JSON.parse(File.read(state_file), symbolize_names: true)
322
+
323
+ unless @dry_run
324
+ repo.save_state(mode, data)
325
+ end
326
+ @stats[:harness_states_migrated] += 1
327
+ rescue => e
328
+ @errors << {type: :harness_state, file: state_file, error: e.message}
329
+ end
330
+ rescue => e
331
+ @errors << {type: :harness_state, error: e.message}
332
+ Aidp.log_error("storage_migrator", "harness_state_migration_failed", error: e.message)
333
+ end
334
+
335
+ def migrate_workstreams
336
+ workstreams_file = File.join(ConfigPaths.aidp_dir(project_dir), "workstreams.json")
337
+ return unless File.exist?(workstreams_file)
338
+
339
+ Aidp.log_debug("storage_migrator", "migrating_workstreams")
340
+
341
+ require_relative "repositories/workstream_repository"
342
+ repo = Repositories::WorkstreamRepository.new(project_dir: project_dir)
343
+
344
+ data = JSON.parse(File.read(workstreams_file), symbolize_names: true)
345
+ workstreams = data[:workstreams] || data["workstreams"] || data
346
+
347
+ workstreams = [workstreams] if workstreams.is_a?(Hash)
348
+
349
+ workstreams.each do |ws|
350
+ slug = ws[:slug] || ws["slug"] || ws[:name] || ws["name"]
351
+ next unless slug
352
+
353
+ unless @dry_run
354
+ repo.init(
355
+ slug: slug,
356
+ task: ws[:task] || ws["task"] || ws[:name] || ws["name"]
357
+ )
358
+ # Update additional attributes if present
359
+ branch = ws[:branch] || ws["branch"]
360
+ if branch
361
+ repo.update(slug: slug, branch: branch)
362
+ end
363
+ end
364
+ @stats[:workstreams_migrated] += 1
365
+ end
366
+ rescue => e
367
+ @errors << {type: :workstreams, error: e.message}
368
+ Aidp.log_error("storage_migrator", "workstreams_migration_failed", error: e.message)
369
+ end
370
+
371
+ def migrate_watch_state
372
+ watch_state_file = File.join(ConfigPaths.aidp_dir(project_dir), "watch_state.yml")
373
+ watch_results_file = File.join(ConfigPaths.aidp_dir(project_dir), "watch_results.yml")
374
+
375
+ return unless File.exist?(watch_state_file) || File.exist?(watch_results_file)
376
+
377
+ # Watch state has a complex format that requires issue-specific records.
378
+ # Skip automatic migration - the data will be regenerated when watch mode runs.
379
+ Aidp.log_debug("storage_migrator", "skipping_watch_state_migration",
380
+ reason: "Watch state will be regenerated on next watch mode run")
381
+
382
+ @stats[:watch_state_skipped] = 1 if File.exist?(watch_state_file)
383
+ @stats[:watch_results_skipped] = 1 if File.exist?(watch_results_file)
384
+ end
385
+
386
+ def migrate_worktrees
387
+ worktrees_file = File.join(ConfigPaths.aidp_dir(project_dir), "worktrees.json")
388
+ pr_worktrees_file = File.join(ConfigPaths.aidp_dir(project_dir), "pr_worktrees.json")
389
+
390
+ return unless File.exist?(worktrees_file) || File.exist?(pr_worktrees_file)
391
+
392
+ Aidp.log_debug("storage_migrator", "migrating_worktrees")
393
+
394
+ require_relative "repositories/worktree_repository"
395
+ repo = Repositories::WorktreeRepository.new(project_dir: project_dir)
396
+
397
+ # Standard worktrees
398
+ if File.exist?(worktrees_file)
399
+ data = JSON.parse(File.read(worktrees_file), symbolize_names: true)
400
+ worktrees = data[:worktrees] || data["worktrees"] || data
401
+
402
+ worktrees.each do |slug, wt|
403
+ wt = wt.merge(slug: slug) if wt.is_a?(Hash)
404
+ slug_str = (wt[:slug] || slug).to_s
405
+ path = wt[:path] || wt["path"]
406
+ branch = wt[:branch] || wt["branch"]
407
+ next unless slug_str && path && branch
408
+
409
+ unless @dry_run
410
+ repo.register(slug: slug_str, path: path, branch: branch)
411
+ end
412
+ @stats[:worktrees_migrated] += 1
413
+ end
414
+ end
415
+
416
+ # PR worktrees
417
+ if File.exist?(pr_worktrees_file)
418
+ data = JSON.parse(File.read(pr_worktrees_file), symbolize_names: true)
419
+ pr_worktrees = data[:pr_worktrees] || data["pr_worktrees"] || data
420
+
421
+ pr_worktrees.each do |pr_num, wt|
422
+ path = wt[:path] || wt["path"]
423
+ branch = wt[:branch] || wt["branch"] || "main"
424
+ next unless path
425
+
426
+ unless @dry_run
427
+ repo.register_pr(
428
+ pr_number: pr_num.to_s.to_i,
429
+ path: path,
430
+ base_branch: wt[:base_branch] || wt["base_branch"] || "main",
431
+ head_branch: wt[:head_branch] || wt["head_branch"] || branch
432
+ )
433
+ end
434
+ @stats[:pr_worktrees_migrated] += 1
435
+ end
436
+ end
437
+ rescue => e
438
+ @errors << {type: :worktrees, error: e.message}
439
+ Aidp.log_error("storage_migrator", "worktrees_migration_failed", error: e.message)
440
+ end
441
+
442
+ def migrate_evaluations
443
+ eval_dir = ConfigPaths.evaluations_dir(project_dir)
444
+ return unless Dir.exist?(eval_dir)
445
+
446
+ Aidp.log_debug("storage_migrator", "migrating_evaluations")
447
+
448
+ require_relative "repositories/evaluation_repository"
449
+ repo = Repositories::EvaluationRepository.new(project_dir: project_dir)
450
+
451
+ Dir.glob(File.join(eval_dir, "*.json")).each do |eval_file|
452
+ next if File.basename(eval_file) == "index.json"
453
+
454
+ data = JSON.parse(File.read(eval_file), symbolize_names: true)
455
+ unless @dry_run
456
+ repo.store(
457
+ id: data[:id] || File.basename(eval_file, ".json"),
458
+ rating: data[:rating] || data["rating"] || data[:status] || data["status"] || "neutral",
459
+ target_type: data[:type] || data["type"] || data[:target_type] || data["target_type"],
460
+ target_id: data[:target_id] || data["target_id"],
461
+ feedback: data[:comment] || data["comment"] || data[:feedback] || data["feedback"],
462
+ context: data[:context] || data["context"] || data
463
+ )
464
+ end
465
+ @stats[:evaluations_migrated] += 1
466
+ rescue => e
467
+ @errors << {type: :evaluation, file: eval_file, error: e.message}
468
+ end
469
+ rescue => e
470
+ @errors << {type: :evaluations, error: e.message}
471
+ Aidp.log_error("storage_migrator", "evaluations_migration_failed", error: e.message)
472
+ end
473
+
474
+ def migrate_provider_info
475
+ providers_dir = ConfigPaths.providers_dir(project_dir)
476
+ return unless Dir.exist?(providers_dir)
477
+
478
+ Aidp.log_debug("storage_migrator", "migrating_provider_info")
479
+
480
+ require_relative "repositories/provider_info_cache_repository"
481
+ repo = Repositories::ProviderInfoCacheRepository.new(project_dir: project_dir)
482
+
483
+ Dir.glob(File.join(providers_dir, "*_info.yml")).each do |info_file|
484
+ provider_name = File.basename(info_file, "_info.yml")
485
+ data = YAML.safe_load_file(info_file, permitted_classes: [Time, Symbol])
486
+
487
+ unless @dry_run
488
+ repo.cache(provider_name, data)
489
+ end
490
+ @stats[:provider_info_migrated] += 1
491
+ rescue => e
492
+ @errors << {type: :provider_info, file: info_file, error: e.message}
493
+ end
494
+ rescue => e
495
+ @errors << {type: :provider_info, error: e.message}
496
+ Aidp.log_error("storage_migrator", "provider_info_migration_failed", error: e.message)
497
+ end
498
+
499
+ def migrate_model_cache
500
+ cache_file = File.join(ConfigPaths.model_cache_dir(project_dir), "models.json")
501
+ return unless File.exist?(cache_file)
502
+
503
+ Aidp.log_debug("storage_migrator", "migrating_model_cache")
504
+
505
+ require_relative "repositories/model_cache_repository"
506
+ repo = Repositories::ModelCacheRepository.new(project_dir: project_dir)
507
+
508
+ data = JSON.parse(File.read(cache_file), symbolize_names: true)
509
+
510
+ data.each do |provider, cache_entry|
511
+ provider = provider.to_s
512
+ models = cache_entry[:models] || cache_entry["models"] || []
513
+ ttl = cache_entry[:ttl] || cache_entry["ttl"] || 86400
514
+
515
+ unless @dry_run
516
+ repo.cache_models(provider, models, ttl: ttl)
517
+ end
518
+ @stats[:model_cache_migrated] += 1
519
+ end
520
+ rescue => e
521
+ @errors << {type: :model_cache, error: e.message}
522
+ Aidp.log_error("storage_migrator", "model_cache_migration_failed", error: e.message)
523
+ end
524
+
525
+ def migrate_deprecated_models
526
+ deprecated_file = File.join(ConfigPaths.aidp_dir(project_dir), "deprecated_models.json")
527
+ return unless File.exist?(deprecated_file)
528
+
529
+ Aidp.log_debug("storage_migrator", "migrating_deprecated_models")
530
+
531
+ require_relative "repositories/deprecated_models_repository"
532
+ repo = Repositories::DeprecatedModelsRepository.new(project_dir: project_dir)
533
+
534
+ data = JSON.parse(File.read(deprecated_file), symbolize_names: true)
535
+ providers = data[:providers] || data["providers"] || {}
536
+
537
+ providers.each do |provider, models|
538
+ provider = provider.to_s
539
+ models.each do |model_name, info|
540
+ unless @dry_run
541
+ repo.add(
542
+ provider_name: provider,
543
+ model_name: model_name.to_s,
544
+ replacement: info[:replacement] || info["replacement"],
545
+ reason: info[:reason] || info["reason"]
546
+ )
547
+ end
548
+ @stats[:deprecated_models_migrated] += 1
549
+ end
550
+ end
551
+ rescue => e
552
+ @errors << {type: :deprecated_models, error: e.message}
553
+ Aidp.log_error("storage_migrator", "deprecated_models_migration_failed", error: e.message)
554
+ end
555
+
556
+ def migrate_secrets
557
+ secrets_file = ConfigPaths.secrets_registry_file(project_dir)
558
+ return unless File.exist?(secrets_file)
559
+
560
+ Aidp.log_debug("storage_migrator", "migrating_secrets")
561
+
562
+ require_relative "repositories/secrets_repository"
563
+ repo = Repositories::SecretsRepository.new(project_dir: project_dir)
564
+
565
+ data = JSON.parse(File.read(secrets_file), symbolize_names: true)
566
+ secrets = data[:secrets] || data["secrets"] || data
567
+
568
+ secrets.each do |secret|
569
+ name = secret[:name] || secret["name"]
570
+ env_var = secret[:env_var] || secret["env_var"]
571
+ next unless name && env_var
572
+
573
+ unless @dry_run
574
+ repo.register(
575
+ name: name,
576
+ env_var: env_var,
577
+ description: secret[:description] || secret["description"],
578
+ scopes: secret[:scopes] || secret["scopes"] || []
579
+ )
580
+ end
581
+ @stats[:secrets_migrated] += 1
582
+ end
583
+ rescue => e
584
+ @errors << {type: :secrets, error: e.message}
585
+ Aidp.log_error("storage_migrator", "secrets_migration_failed", error: e.message)
586
+ end
587
+
588
+ def migrate_prompt_archive
589
+ archive_dir = File.join(ConfigPaths.aidp_dir(project_dir), "prompt_archive")
590
+ return unless Dir.exist?(archive_dir)
591
+
592
+ Aidp.log_debug("storage_migrator", "migrating_prompt_archive")
593
+
594
+ require_relative "repositories/prompt_archive_repository"
595
+ repo = Repositories::PromptArchiveRepository.new(project_dir: project_dir)
596
+
597
+ Dir.glob(File.join(archive_dir, "*.md")).each do |prompt_file|
598
+ content = File.read(prompt_file)
599
+ filename = File.basename(prompt_file, ".md")
600
+
601
+ # Parse filename for step name (format: timestamp_step_provider.md)
602
+ parts = filename.split("_")
603
+ step = parts[1..-2]&.join("_") || "unknown"
604
+
605
+ unless @dry_run
606
+ repo.archive(step_name: step, content: content)
607
+ end
608
+ @stats[:prompts_migrated] += 1
609
+ rescue => e
610
+ @errors << {type: :prompt_archive, file: prompt_file, error: e.message}
611
+ end
612
+ rescue => e
613
+ @errors << {type: :prompt_archive, error: e.message}
614
+ Aidp.log_error("storage_migrator", "prompt_archive_migration_failed", error: e.message)
615
+ end
616
+
617
+ def migrate_jobs
618
+ jobs_dir = ConfigPaths.jobs_dir(project_dir)
619
+ return unless Dir.exist?(jobs_dir)
620
+
621
+ Aidp.log_debug("storage_migrator", "migrating_jobs")
622
+
623
+ require_relative "repositories/job_repository"
624
+ repo = Repositories::JobRepository.new(project_dir: project_dir)
625
+
626
+ Dir.glob(File.join(jobs_dir, "*.json")).each do |job_file|
627
+ data = JSON.parse(File.read(job_file), symbolize_names: true)
628
+
629
+ unless @dry_run
630
+ # Note: create() generates its own job_id, so old IDs are not preserved
631
+ new_job_id = repo.create(
632
+ job_type: data[:type] || data["type"] || "background",
633
+ input: data[:options] || data["options"] || data[:input] || data["input"] || {}
634
+ )
635
+
636
+ # Update status based on stored state
637
+ status = data[:status] || data["status"]
638
+ case status
639
+ when "running"
640
+ repo.start(new_job_id, pid: data[:pid] || data["pid"])
641
+ when "completed"
642
+ output = data[:result] || data["result"] || data[:output] || data["output"] || {}
643
+ repo.complete(new_job_id, output: output)
644
+ when "failed"
645
+ repo.fail(new_job_id, error: data[:error] || data["error"] || "Unknown error")
646
+ end
647
+ end
648
+ @stats[:jobs_migrated] += 1
649
+ rescue => e
650
+ @errors << {type: :job, file: job_file, error: e.message}
651
+ end
652
+ rescue => e
653
+ @errors << {type: :jobs, error: e.message}
654
+ Aidp.log_error("storage_migrator", "jobs_migration_failed", error: e.message)
655
+ end
656
+
657
+ def migrate_provider_metrics
658
+ metrics_file = File.join(ConfigPaths.aidp_dir(project_dir), "provider_metrics.yml")
659
+ rate_limits_file = File.join(ConfigPaths.aidp_dir(project_dir), "provider_rate_limits.yml")
660
+
661
+ return unless File.exist?(metrics_file) || File.exist?(rate_limits_file)
662
+
663
+ Aidp.log_debug("storage_migrator", "migrating_provider_metrics")
664
+
665
+ require_relative "repositories/provider_metrics_repository"
666
+ repo = Repositories::ProviderMetricsRepository.new(project_dir: project_dir)
667
+
668
+ if File.exist?(metrics_file)
669
+ data = YAML.safe_load_file(metrics_file, permitted_classes: [Time, Symbol])
670
+ data&.each do |provider, metrics|
671
+ unless @dry_run
672
+ repo.save_metrics(provider.to_s, metrics)
673
+ end
674
+ @stats[:provider_metrics_migrated] += 1
675
+ end
676
+ end
677
+
678
+ if File.exist?(rate_limits_file)
679
+ data = YAML.safe_load_file(rate_limits_file, permitted_classes: [Time, Symbol])
680
+ data&.each do |provider, limits|
681
+ unless @dry_run
682
+ repo.save_rate_limits(provider.to_s, limits)
683
+ end
684
+ @stats[:rate_limits_migrated] += 1
685
+ end
686
+ end
687
+ rescue => e
688
+ @errors << {type: :provider_metrics, error: e.message}
689
+ Aidp.log_error("storage_migrator", "provider_metrics_migration_failed", error: e.message)
690
+ end
691
+ end
692
+ end
693
+ end