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,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+
5
+ module Aidp
6
+ module Database
7
+ module Repositories
8
+ # Repository for workstreams and workstream_events tables
9
+ # Replaces workstreams/*/state.json and history.jsonl
10
+ class WorkstreamRepository < Repository
11
+ def initialize(project_dir: Dir.pwd)
12
+ super(project_dir: project_dir, table_name: "workstreams")
13
+ end
14
+
15
+ # Initialize a new workstream
16
+ #
17
+ # @param slug [String] Workstream slug
18
+ # @param task [String, nil] Task description
19
+ # @return [Hash] Created workstream state
20
+ def init(slug:, task: nil)
21
+ now = current_timestamp
22
+
23
+ execute(
24
+ insert_sql([
25
+ :project_dir, :slug, :status, :iteration, :metadata,
26
+ :created_at, :updated_at
27
+ ]),
28
+ [
29
+ project_dir,
30
+ slug,
31
+ "active",
32
+ 0,
33
+ serialize_json({task: task, started_at: now}),
34
+ now,
35
+ now
36
+ ]
37
+ )
38
+
39
+ append_event(slug: slug, type: "created", data: {task: task})
40
+
41
+ Aidp.log_debug("workstream_repository", "initialized", slug: slug)
42
+
43
+ read(slug: slug)
44
+ end
45
+
46
+ # Read workstream state
47
+ #
48
+ # @param slug [String] Workstream slug
49
+ # @return [Hash, nil] Workstream state or nil
50
+ def read(slug:)
51
+ row = query_one(
52
+ "SELECT * FROM workstreams WHERE project_dir = ? AND slug = ?",
53
+ [project_dir, slug]
54
+ )
55
+ return nil unless row
56
+
57
+ deserialize_workstream(row)
58
+ end
59
+
60
+ # Update workstream attributes
61
+ #
62
+ # @param slug [String] Workstream slug
63
+ # @param attrs [Hash] Attributes to update
64
+ # @return [Hash] Updated workstream state
65
+ def update(slug:, **attrs)
66
+ existing = read(slug: slug)
67
+ existing ||= init(slug: slug)
68
+
69
+ now = current_timestamp
70
+ metadata = existing[:metadata] || {}
71
+ metadata = metadata.merge(attrs.except(:status, :iteration, :branch, :worktree_path))
72
+
73
+ execute(
74
+ <<~SQL,
75
+ UPDATE workstreams SET
76
+ status = ?,
77
+ iteration = ?,
78
+ branch = ?,
79
+ worktree_path = ?,
80
+ metadata = ?,
81
+ updated_at = ?
82
+ WHERE project_dir = ? AND slug = ?
83
+ SQL
84
+ [
85
+ attrs[:status] || existing[:status],
86
+ attrs[:iteration] || existing[:iteration],
87
+ attrs[:branch] || existing[:branch],
88
+ attrs[:worktree_path] || existing[:worktree_path],
89
+ serialize_json(metadata),
90
+ now,
91
+ project_dir,
92
+ slug
93
+ ]
94
+ )
95
+
96
+ Aidp.log_debug("workstream_repository", "updated", slug: slug)
97
+
98
+ read(slug: slug)
99
+ end
100
+
101
+ # Increment iteration counter
102
+ #
103
+ # @param slug [String] Workstream slug
104
+ # @return [Hash] Updated workstream state
105
+ def increment_iteration(slug:)
106
+ existing = read(slug: slug)
107
+ existing ||= init(slug: slug)
108
+
109
+ new_iteration = (existing[:iteration] || 0) + 1
110
+ new_status = (existing[:status] == "paused") ? "active" : existing[:status]
111
+
112
+ state = update(slug: slug, iteration: new_iteration, status: new_status)
113
+
114
+ append_event(slug: slug, type: "iteration", data: {count: new_iteration})
115
+
116
+ state
117
+ end
118
+
119
+ # Append event to workstream history
120
+ #
121
+ # @param slug [String] Workstream slug
122
+ # @param type [String] Event type
123
+ # @param data [Hash] Event data
124
+ def append_event(slug:, type:, data: {})
125
+ now = current_timestamp
126
+
127
+ execute(
128
+ <<~SQL,
129
+ INSERT INTO workstream_events
130
+ (project_dir, workstream_slug, event_type, event_data, timestamp)
131
+ VALUES (?, ?, ?, ?, ?)
132
+ SQL
133
+ [project_dir, slug, type, serialize_json(data), now]
134
+ )
135
+
136
+ Aidp.log_debug("workstream_repository", "event_appended",
137
+ slug: slug, type: type)
138
+ end
139
+
140
+ # Get recent events for workstream
141
+ #
142
+ # @param slug [String] Workstream slug
143
+ # @param limit [Integer] Maximum events to return
144
+ # @return [Array<Hash>] Recent events
145
+ def recent_events(slug:, limit: 5)
146
+ rows = query(
147
+ <<~SQL,
148
+ SELECT * FROM workstream_events
149
+ WHERE project_dir = ? AND workstream_slug = ?
150
+ ORDER BY timestamp DESC
151
+ LIMIT ?
152
+ SQL
153
+ [project_dir, slug, limit]
154
+ )
155
+
156
+ rows.map { |row| deserialize_event(row) }.reverse
157
+ end
158
+
159
+ # Pause workstream
160
+ #
161
+ # @param slug [String] Workstream slug
162
+ # @return [Hash] Result with status or error
163
+ def pause(slug:)
164
+ state = read(slug: slug)
165
+ return {error: "Workstream not found"} unless state
166
+ return {error: "Already paused"} if state[:status] == "paused"
167
+
168
+ now = current_timestamp
169
+ update(slug: slug, status: "paused", paused_at: now)
170
+ append_event(slug: slug, type: "paused", data: {})
171
+
172
+ {status: "paused"}
173
+ end
174
+
175
+ # Resume workstream
176
+ #
177
+ # @param slug [String] Workstream slug
178
+ # @return [Hash] Result with status or error
179
+ def resume(slug:)
180
+ state = read(slug: slug)
181
+ return {error: "Workstream not found"} unless state
182
+ return {error: "Not paused"} unless state[:status] == "paused"
183
+
184
+ now = current_timestamp
185
+ update(slug: slug, status: "active", resumed_at: now)
186
+ append_event(slug: slug, type: "resumed", data: {})
187
+
188
+ {status: "active"}
189
+ end
190
+
191
+ # Complete workstream
192
+ #
193
+ # @param slug [String] Workstream slug
194
+ # @return [Hash] Result with status or error
195
+ def complete(slug:)
196
+ state = read(slug: slug)
197
+ return {error: "Workstream not found"} unless state
198
+ return {error: "Already completed"} if state[:status] == "completed"
199
+
200
+ now = current_timestamp
201
+ update(slug: slug, status: "completed", completed_at: now)
202
+ append_event(slug: slug, type: "completed", data: {iterations: state[:iteration]})
203
+
204
+ {status: "completed"}
205
+ end
206
+
207
+ # Mark workstream as removed
208
+ #
209
+ # @param slug [String] Workstream slug
210
+ def mark_removed(slug:)
211
+ state = read(slug: slug)
212
+
213
+ # Auto-complete if active
214
+ complete(slug: slug) if state && state[:status] == "active"
215
+
216
+ update(slug: slug, status: "removed")
217
+ append_event(slug: slug, type: "removed", data: {})
218
+
219
+ Aidp.log_debug("workstream_repository", "removed", slug: slug)
220
+ end
221
+
222
+ # Check if workstream is stalled
223
+ #
224
+ # @param slug [String] Workstream slug
225
+ # @param threshold_seconds [Integer] Stall threshold in seconds
226
+ # @return [Boolean]
227
+ def stalled?(slug:, threshold_seconds: 3600)
228
+ state = read(slug: slug)
229
+ return false unless state && state[:updated_at]
230
+ return false if state[:status] != "active"
231
+
232
+ updated = Time.parse(state[:updated_at])
233
+ (Time.now.utc - updated).to_i > threshold_seconds
234
+ rescue ArgumentError
235
+ false
236
+ end
237
+
238
+ # Get elapsed time for workstream
239
+ #
240
+ # @param slug [String] Workstream slug
241
+ # @return [Integer] Elapsed seconds
242
+ def elapsed_seconds(slug:)
243
+ state = read(slug: slug)
244
+ return 0 unless state
245
+
246
+ metadata = state[:metadata] || {}
247
+ return 0 unless metadata[:started_at]
248
+
249
+ started = Time.parse(metadata[:started_at])
250
+ (Time.now.utc - started).to_i
251
+ rescue ArgumentError
252
+ 0
253
+ end
254
+
255
+ # List all workstreams for project
256
+ #
257
+ # @param status [String, nil] Filter by status
258
+ # @return [Array<Hash>] Workstreams
259
+ def list(status: nil)
260
+ rows = if status
261
+ query(
262
+ "SELECT * FROM workstreams WHERE project_dir = ? AND status = ? ORDER BY updated_at DESC",
263
+ [project_dir, status]
264
+ )
265
+ else
266
+ query(
267
+ "SELECT * FROM workstreams WHERE project_dir = ? ORDER BY updated_at DESC",
268
+ [project_dir]
269
+ )
270
+ end
271
+
272
+ rows.map { |row| deserialize_workstream(row) }
273
+ end
274
+
275
+ private
276
+
277
+ def deserialize_workstream(row)
278
+ metadata = deserialize_json(row["metadata"]) || {}
279
+
280
+ {
281
+ id: row["id"],
282
+ slug: row["slug"],
283
+ status: row["status"],
284
+ iteration: row["iteration"] || 0,
285
+ branch: row["branch"],
286
+ worktree_path: row["worktree_path"],
287
+ metadata: metadata,
288
+ task: metadata[:task],
289
+ started_at: metadata[:started_at],
290
+ paused_at: metadata[:paused_at],
291
+ resumed_at: metadata[:resumed_at],
292
+ completed_at: metadata[:completed_at],
293
+ created_at: row["created_at"],
294
+ updated_at: row["updated_at"]
295
+ }
296
+ end
297
+
298
+ def deserialize_event(row)
299
+ {
300
+ id: row["id"],
301
+ workstream_slug: row["workstream_slug"],
302
+ type: row["event_type"],
303
+ data: deserialize_json(row["event_data"]) || {},
304
+ timestamp: row["timestamp"]
305
+ }
306
+ end
307
+ end
308
+ end
309
+ end
310
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+
5
+ module Aidp
6
+ module Database
7
+ module Repositories
8
+ # Repository for worktrees table
9
+ # Replaces worktrees.json and pr_worktrees.json
10
+ # Handles both standard and PR worktrees
11
+ class WorktreeRepository < Repository
12
+ TYPES = %w[standard pr].freeze
13
+
14
+ def initialize(project_dir: Dir.pwd)
15
+ super(project_dir: project_dir, table_name: "worktrees")
16
+ end
17
+
18
+ # Register a standard worktree
19
+ #
20
+ # @param slug [String] Worktree slug
21
+ # @param path [String] Filesystem path
22
+ # @param branch [String] Branch name
23
+ # @return [Hash] Worktree info
24
+ def register(slug:, path:, branch:)
25
+ now = current_timestamp
26
+
27
+ execute(
28
+ insert_sql([
29
+ :project_dir, :worktree_type, :path, :branch, :slug,
30
+ :status, :created_at, :updated_at
31
+ ]),
32
+ [project_dir, "standard", path, branch, slug, "active", now, now]
33
+ )
34
+
35
+ Aidp.log_debug("worktree_repository", "registered",
36
+ slug: slug, type: "standard")
37
+
38
+ find_by_slug(slug)
39
+ end
40
+
41
+ # Register a PR worktree
42
+ #
43
+ # @param pr_number [Integer] PR number
44
+ # @param path [String] Filesystem path
45
+ # @param base_branch [String] Base branch
46
+ # @param head_branch [String] Head branch
47
+ # @param metadata [Hash] Additional metadata
48
+ # @return [Hash] Worktree info
49
+ def register_pr(pr_number:, path:, base_branch:, head_branch:, metadata: {})
50
+ now = current_timestamp
51
+ slug = "pr-#{pr_number}-#{Time.now.to_i}"
52
+
53
+ full_metadata = metadata.merge(
54
+ base_branch: base_branch,
55
+ head_branch: head_branch,
56
+ created_at: Time.now.to_i
57
+ )
58
+
59
+ execute(
60
+ insert_sql([
61
+ :project_dir, :worktree_type, :path, :branch, :slug, :pr_number,
62
+ :status, :metadata, :created_at, :updated_at
63
+ ]),
64
+ [
65
+ project_dir, "pr", path, head_branch, slug, pr_number,
66
+ "active", serialize_json(full_metadata), now, now
67
+ ]
68
+ )
69
+
70
+ Aidp.log_debug("worktree_repository", "registered_pr",
71
+ pr_number: pr_number, slug: slug)
72
+
73
+ find_by_pr(pr_number)
74
+ end
75
+
76
+ # Find worktree by slug
77
+ #
78
+ # @param slug [String] Worktree slug
79
+ # @return [Hash, nil] Worktree info or nil
80
+ def find_by_slug(slug)
81
+ row = query_one(
82
+ "SELECT * FROM worktrees WHERE project_dir = ? AND slug = ?",
83
+ [project_dir, slug]
84
+ )
85
+ deserialize_worktree(row)
86
+ end
87
+
88
+ # Find worktree by PR number
89
+ #
90
+ # @param pr_number [Integer] PR number
91
+ # @return [Hash, nil] Worktree info or nil
92
+ def find_by_pr(pr_number)
93
+ row = query_one(
94
+ "SELECT * FROM worktrees WHERE project_dir = ? AND pr_number = ?",
95
+ [project_dir, pr_number]
96
+ )
97
+ deserialize_worktree(row)
98
+ end
99
+
100
+ # Find worktree by branch
101
+ #
102
+ # @param branch [String] Branch name
103
+ # @return [Hash, nil] Worktree info or nil
104
+ def find_by_branch(branch)
105
+ row = query_one(
106
+ "SELECT * FROM worktrees WHERE project_dir = ? AND branch = ?",
107
+ [project_dir, branch]
108
+ )
109
+ deserialize_worktree(row)
110
+ end
111
+
112
+ # Check if worktree exists
113
+ #
114
+ # @param slug [String] Worktree slug
115
+ # @return [Boolean]
116
+ def exists?(slug)
117
+ !find_by_slug(slug).nil?
118
+ end
119
+
120
+ # List all worktrees
121
+ #
122
+ # @param type [String, nil] Filter by type (standard, pr)
123
+ # @return [Array<Hash>] Worktrees
124
+ def list(type: nil)
125
+ rows = if type
126
+ query(
127
+ "SELECT * FROM worktrees WHERE project_dir = ? AND worktree_type = ? ORDER BY created_at DESC",
128
+ [project_dir, type]
129
+ )
130
+ else
131
+ query(
132
+ "SELECT * FROM worktrees WHERE project_dir = ? ORDER BY created_at DESC",
133
+ [project_dir]
134
+ )
135
+ end
136
+
137
+ rows.map { |row| deserialize_worktree(row) }
138
+ end
139
+
140
+ # List standard worktrees (for Worktree module)
141
+ #
142
+ # @return [Array<Hash>]
143
+ def list_standard
144
+ list(type: "standard")
145
+ end
146
+
147
+ # List PR worktrees (for PRWorktreeManager)
148
+ #
149
+ # @return [Hash] Map of pr_number => worktree info
150
+ def list_pr_worktrees
151
+ rows = query(
152
+ "SELECT * FROM worktrees WHERE project_dir = ? AND worktree_type = 'pr'",
153
+ [project_dir]
154
+ )
155
+
156
+ rows.each_with_object({}) do |row, hash|
157
+ wt = deserialize_worktree(row)
158
+ hash[wt[:pr_number].to_s] = wt if wt[:pr_number]
159
+ end
160
+ end
161
+
162
+ # Unregister a worktree by slug
163
+ #
164
+ # @param slug [String] Worktree slug
165
+ def unregister(slug)
166
+ execute(
167
+ "DELETE FROM worktrees WHERE project_dir = ? AND slug = ?",
168
+ [project_dir, slug]
169
+ )
170
+ Aidp.log_debug("worktree_repository", "unregistered", slug: slug)
171
+ end
172
+
173
+ # Unregister a PR worktree
174
+ #
175
+ # @param pr_number [Integer] PR number
176
+ def unregister_pr(pr_number)
177
+ execute(
178
+ "DELETE FROM worktrees WHERE project_dir = ? AND pr_number = ?",
179
+ [project_dir, pr_number]
180
+ )
181
+ Aidp.log_debug("worktree_repository", "unregistered_pr", pr_number: pr_number)
182
+ end
183
+
184
+ # Update worktree status
185
+ #
186
+ # @param slug [String] Worktree slug
187
+ # @param status [String] New status
188
+ def update_status(slug, status)
189
+ execute(
190
+ "UPDATE worktrees SET status = ?, updated_at = ? WHERE project_dir = ? AND slug = ?",
191
+ [status, current_timestamp, project_dir, slug]
192
+ )
193
+ end
194
+
195
+ # Cleanup stale PR worktrees
196
+ #
197
+ # @param days_threshold [Integer] Days after which to consider stale
198
+ # @return [Array<Hash>] Removed worktrees
199
+ def cleanup_stale_pr(days_threshold: 30)
200
+ threshold_time = (Time.now - (days_threshold * 24 * 60 * 60)).strftime("%Y-%m-%d %H:%M:%S")
201
+
202
+ stale = query(
203
+ "SELECT * FROM worktrees WHERE project_dir = ? AND worktree_type = 'pr' AND created_at < ?",
204
+ [project_dir, threshold_time]
205
+ )
206
+
207
+ stale.each do |row|
208
+ execute("DELETE FROM worktrees WHERE id = ?", [row["id"]])
209
+ end
210
+
211
+ stale.map { |row| deserialize_worktree(row) }
212
+ end
213
+
214
+ private
215
+
216
+ def deserialize_worktree(row)
217
+ return nil unless row
218
+
219
+ metadata = deserialize_json(row["metadata"]) || {}
220
+
221
+ {
222
+ id: row["id"],
223
+ slug: row["slug"],
224
+ path: row["path"],
225
+ branch: row["branch"],
226
+ worktree_type: row["worktree_type"],
227
+ pr_number: row["pr_number"],
228
+ status: row["status"],
229
+ metadata: metadata,
230
+ base_branch: metadata[:base_branch],
231
+ head_branch: metadata[:head_branch] || row["branch"],
232
+ created_at: row["created_at"],
233
+ updated_at: row["updated_at"],
234
+ active: row["path"] && Dir.exist?(row["path"])
235
+ }
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Aidp
6
+ module Database
7
+ # Base repository class for database access
8
+ # Provides common patterns for CRUD operations
9
+ class Repository
10
+ attr_reader :project_dir, :table_name
11
+
12
+ # @param table_name [String] Database table name
13
+ # @param project_dir [String] Project directory path
14
+ def initialize(table_name:, project_dir: Dir.pwd)
15
+ @project_dir = project_dir
16
+ @table_name = table_name
17
+ end
18
+
19
+ protected
20
+
21
+ # Get database connection
22
+ def db
23
+ Database.connection(project_dir)
24
+ end
25
+
26
+ # Execute a query and return all results
27
+ #
28
+ # @param sql [String] SQL query
29
+ # @param params [Array] Query parameters
30
+ # @return [Array<Hash>] Query results
31
+ def query(sql, params = [])
32
+ db.execute(sql, params)
33
+ end
34
+
35
+ # Execute a query and return first result
36
+ #
37
+ # @param sql [String] SQL query
38
+ # @param params [Array] Query parameters
39
+ # @return [Hash, nil] First result or nil
40
+ def query_one(sql, params = [])
41
+ db.execute(sql, params).first
42
+ end
43
+
44
+ # Execute a query and return single value
45
+ #
46
+ # @param sql [String] SQL query
47
+ # @param params [Array] Query parameters
48
+ # @return [Object, nil] Single value or nil
49
+ def query_value(sql, params = [])
50
+ db.get_first_value(sql, params)
51
+ end
52
+
53
+ # Execute a statement (INSERT, UPDATE, DELETE)
54
+ #
55
+ # @param sql [String] SQL statement
56
+ # @param params [Array] Statement parameters
57
+ def execute(sql, params = [])
58
+ db.execute(sql, params)
59
+ end
60
+
61
+ # Execute within a transaction
62
+ #
63
+ # @yield Block to execute in transaction
64
+ # @return [Object] Block result
65
+ def transaction(&block)
66
+ db.transaction(&block)
67
+ end
68
+
69
+ # Get the last inserted row ID
70
+ #
71
+ # @return [Integer] Last row ID
72
+ def last_insert_row_id
73
+ db.last_insert_row_id
74
+ end
75
+
76
+ # Serialize a value to JSON for storage
77
+ #
78
+ # @param value [Object] Value to serialize
79
+ # @return [String, nil] JSON string or nil
80
+ def serialize_json(value)
81
+ return nil if value.nil?
82
+
83
+ JSON.generate(value)
84
+ end
85
+
86
+ # Deserialize a JSON string from storage
87
+ #
88
+ # @param json_string [String, nil] JSON string
89
+ # @return [Object, nil] Deserialized value or nil
90
+ def deserialize_json(json_string)
91
+ return nil if json_string.nil? || json_string.empty?
92
+
93
+ JSON.parse(json_string, symbolize_names: true)
94
+ rescue JSON::ParserError => e
95
+ Aidp.log_debug("repository", "json_parse_error",
96
+ table: table_name,
97
+ error: e.message)
98
+ nil
99
+ end
100
+
101
+ # Generate current timestamp in SQLite format
102
+ #
103
+ # @return [String] ISO 8601 timestamp
104
+ def current_timestamp
105
+ Time.now.utc.strftime("%Y-%m-%d %H:%M:%S")
106
+ end
107
+
108
+ # Build a simple INSERT statement
109
+ #
110
+ # @param columns [Array<Symbol>] Column names
111
+ # @return [String] INSERT SQL
112
+ def insert_sql(columns)
113
+ placeholders = columns.map { "?" }.join(", ")
114
+ cols = columns.join(", ")
115
+ "INSERT INTO #{table_name} (#{cols}) VALUES (#{placeholders})"
116
+ end
117
+
118
+ # Build a simple UPDATE statement
119
+ #
120
+ # @param columns [Array<Symbol>] Column names to update
121
+ # @param where_column [Symbol] WHERE clause column
122
+ # @return [String] UPDATE SQL
123
+ def update_sql(columns, where_column: :id)
124
+ set_clause = columns.map { |c| "#{c} = ?" }.join(", ")
125
+ "UPDATE #{table_name} SET #{set_clause} WHERE #{where_column} = ?"
126
+ end
127
+
128
+ # Find a record by ID
129
+ #
130
+ # @param id [Object] Record ID
131
+ # @return [Hash, nil] Record or nil
132
+ def find_by_id(id)
133
+ query_one("SELECT * FROM #{table_name} WHERE id = ?", [id])
134
+ end
135
+
136
+ # Find all records matching project_dir
137
+ #
138
+ # @return [Array<Hash>] Records
139
+ def find_by_project
140
+ query("SELECT * FROM #{table_name} WHERE project_dir = ?", [project_dir])
141
+ end
142
+
143
+ # Delete a record by ID
144
+ #
145
+ # @param id [Object] Record ID
146
+ def delete_by_id(id)
147
+ execute("DELETE FROM #{table_name} WHERE id = ?", [id])
148
+ end
149
+
150
+ # Delete all records matching project_dir
151
+ def delete_by_project
152
+ execute("DELETE FROM #{table_name} WHERE project_dir = ?", [project_dir])
153
+ end
154
+
155
+ # Count records matching project_dir
156
+ #
157
+ # @return [Integer] Record count
158
+ def count_by_project
159
+ query_value(
160
+ "SELECT COUNT(*) FROM #{table_name} WHERE project_dir = ?",
161
+ [project_dir]
162
+ ) || 0
163
+ end
164
+ end
165
+ end
166
+ end