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,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+ require "securerandom"
5
+
6
+ module Aidp
7
+ module Database
8
+ module Repositories
9
+ # Repository for tasks table
10
+ # Replaces tasklist.jsonl
11
+ class TaskRepository < Repository
12
+ VALID_STATUSES = %w[pending in_progress done abandoned].freeze
13
+ VALID_PRIORITIES = %w[high medium low].freeze
14
+
15
+ def initialize(project_dir: Dir.pwd)
16
+ super(project_dir: project_dir, table_name: "tasks")
17
+ end
18
+
19
+ # Create a new task
20
+ #
21
+ # @param description [String] Task description
22
+ # @param priority [String, Symbol] Task priority (high, medium, low)
23
+ # @param session [String, nil] Session identifier
24
+ # @param discovered_during [String, nil] When task was discovered
25
+ # @param tags [Array<String>] Task tags
26
+ # @return [Hash] Created task
27
+ def create(description:, priority: "medium", session: nil, discovered_during: nil, tags: [])
28
+ now = current_timestamp
29
+ id = generate_id
30
+
31
+ execute(
32
+ insert_sql([
33
+ :id, :project_dir, :description, :priority, :status,
34
+ :tags, :source, :created_at, :updated_at
35
+ ]),
36
+ [
37
+ id,
38
+ project_dir,
39
+ description.strip,
40
+ priority.to_s,
41
+ "pending",
42
+ serialize_json(Array(tags)),
43
+ serialize_json({session: session, discovered_during: discovered_during}),
44
+ now,
45
+ now
46
+ ]
47
+ )
48
+
49
+ Aidp.log_debug("task_repository", "created", id: id, description: description)
50
+
51
+ {
52
+ id: id,
53
+ description: description.strip,
54
+ status: :pending,
55
+ priority: priority.to_sym,
56
+ tags: Array(tags),
57
+ session: session,
58
+ discovered_during: discovered_during,
59
+ created_at: now,
60
+ updated_at: now
61
+ }
62
+ end
63
+
64
+ # Update task status
65
+ #
66
+ # @param task_id [String] Task ID
67
+ # @param new_status [String, Symbol] New status
68
+ # @param reason [String, nil] Reason for status change (for abandoned)
69
+ # @return [Hash, nil] Updated task or nil if not found
70
+ def update_status(task_id, new_status, reason: nil)
71
+ task = find(task_id)
72
+ return nil unless task
73
+
74
+ now = current_timestamp
75
+ status_str = new_status.to_s
76
+
77
+ updates = {status: status_str, updated_at: now}
78
+
79
+ case status_str
80
+ when "in_progress"
81
+ updates[:started_at] = now unless task[:started_at]
82
+ when "done"
83
+ updates[:completed_at] = now
84
+ when "abandoned"
85
+ # Store reason in source JSON
86
+ source = task[:source] || {}
87
+ source[:abandoned_reason] = reason
88
+ updates[:source] = serialize_json(source)
89
+ end
90
+
91
+ set_clauses = updates.keys.map { |k| "#{k} = ?" }.join(", ")
92
+ values = updates.values + [task_id, project_dir]
93
+
94
+ execute(
95
+ "UPDATE tasks SET #{set_clauses} WHERE id = ? AND project_dir = ?",
96
+ values
97
+ )
98
+
99
+ Aidp.log_debug("task_repository", "status_updated", id: task_id, status: status_str)
100
+
101
+ find(task_id)
102
+ end
103
+
104
+ # Find task by ID
105
+ #
106
+ # @param task_id [String] Task ID
107
+ # @return [Hash, nil] Task or nil
108
+ def find(task_id)
109
+ row = query_one(
110
+ "SELECT * FROM tasks WHERE id = ? AND project_dir = ?",
111
+ [task_id, project_dir]
112
+ )
113
+ return nil unless row
114
+
115
+ deserialize_task(row)
116
+ end
117
+
118
+ # Get all tasks with optional filters
119
+ #
120
+ # @param status [Symbol, String, nil] Filter by status
121
+ # @param priority [Symbol, String, nil] Filter by priority
122
+ # @param since [Time, nil] Filter by created_at
123
+ # @param tags [Array<String>, nil] Filter by tags (any match)
124
+ # @return [Array<Hash>] Matching tasks
125
+ def all(status: nil, priority: nil, since: nil, tags: nil)
126
+ conditions = ["project_dir = ?"]
127
+ params = [project_dir]
128
+
129
+ if status
130
+ conditions << "status = ?"
131
+ params << status.to_s
132
+ end
133
+
134
+ if priority
135
+ conditions << "priority = ?"
136
+ params << priority.to_s
137
+ end
138
+
139
+ if since
140
+ conditions << "created_at >= ?"
141
+ params << since.strftime("%Y-%m-%d %H:%M:%S")
142
+ end
143
+
144
+ sql = <<~SQL
145
+ SELECT * FROM tasks
146
+ WHERE #{conditions.join(" AND ")}
147
+ ORDER BY created_at DESC
148
+ SQL
149
+
150
+ rows = query(sql, params)
151
+ tasks = rows.map { |row| deserialize_task(row) }
152
+
153
+ # Filter by tags in Ruby (JSON array matching is complex in SQLite)
154
+ if tags && !tags.empty?
155
+ tag_set = tags.map(&:to_s)
156
+ tasks = tasks.select do |task|
157
+ (Array(task[:tags]) & tag_set).any?
158
+ end
159
+ end
160
+
161
+ tasks
162
+ end
163
+
164
+ # Get pending tasks
165
+ #
166
+ # @return [Array<Hash>] Pending tasks
167
+ def pending
168
+ all(status: :pending)
169
+ end
170
+
171
+ # Get in-progress tasks
172
+ #
173
+ # @return [Array<Hash>] In-progress tasks
174
+ def in_progress
175
+ all(status: :in_progress)
176
+ end
177
+
178
+ # Get task counts by status
179
+ #
180
+ # @return [Hash] Counts by status
181
+ def counts
182
+ sql = <<~SQL
183
+ SELECT status, COUNT(*) as count
184
+ FROM tasks
185
+ WHERE project_dir = ?
186
+ GROUP BY status
187
+ SQL
188
+
189
+ rows = query(sql, [project_dir])
190
+ counts_by_status = rows.each_with_object({}) do |row, h|
191
+ h[row["status"].to_sym] = row["count"]
192
+ end
193
+
194
+ {
195
+ total: counts_by_status.values.sum,
196
+ pending: counts_by_status[:pending] || 0,
197
+ in_progress: counts_by_status[:in_progress] || 0,
198
+ done: counts_by_status[:done] || 0,
199
+ abandoned: counts_by_status[:abandoned] || 0
200
+ }
201
+ end
202
+
203
+ private
204
+
205
+ def generate_id
206
+ "task_#{Time.now.to_i}_#{SecureRandom.hex(4)}"
207
+ end
208
+
209
+ def deserialize_task(row)
210
+ source = deserialize_json(row["source"]) || {}
211
+
212
+ {
213
+ id: row["id"],
214
+ description: row["description"],
215
+ status: row["status"]&.to_sym,
216
+ priority: row["priority"]&.to_sym,
217
+ tags: deserialize_json(row["tags"]) || [],
218
+ session: source[:session],
219
+ discovered_during: source[:discovered_during],
220
+ abandoned_reason: source[:abandoned_reason],
221
+ created_at: row["created_at"],
222
+ updated_at: row["updated_at"],
223
+ started_at: row["started_at"],
224
+ completed_at: row["completed_at"],
225
+ source: source
226
+ }
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,383 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+
5
+ module Aidp
6
+ module Database
7
+ module Repositories
8
+ # Repository for template_versions table
9
+ # Manages versioned prompt templates with feedback-based selection
10
+ #
11
+ # Per issue #402:
12
+ # - Store template versions in SQLite
13
+ # - Track positive/negative votes per version
14
+ # - Retain last 5 versions, ensuring at least 2 positive-feedback versions
15
+ # - Select best version based on positive vote count
16
+ #
17
+ class TemplateVersionRepository < Repository
18
+ # Minimum versions to retain per template
19
+ MIN_VERSIONS = 5
20
+
21
+ # Minimum positive-feedback versions to retain
22
+ MIN_POSITIVE_VERSIONS = 2
23
+
24
+ def initialize(project_dir: Dir.pwd)
25
+ super(project_dir: project_dir, table_name: "template_versions")
26
+ end
27
+
28
+ # Create a new template version
29
+ #
30
+ # @param template_id [String] Template identifier (e.g., "work_loop/decide_whats_next")
31
+ # @param content [String] Full YAML content of the template
32
+ # @param parent_version_id [Integer, nil] ID of parent version (for AGD-generated variants)
33
+ # @param metadata [Hash, nil] Additional metadata
34
+ # @return [Hash] Result with :success, :id, :version_number
35
+ def create(template_id:, content:, parent_version_id: nil, metadata: nil)
36
+ # Validate content is present
37
+ if content.nil? || content.to_s.strip.empty?
38
+ Aidp.log_error("template_version_repository", "invalid_content",
39
+ template_id: template_id,
40
+ error: "Content cannot be nil or empty")
41
+ return {success: false, error: "Content cannot be nil or empty"}
42
+ end
43
+
44
+ Aidp.log_debug("template_version_repository", "creating_version",
45
+ template_id: template_id,
46
+ parent_version_id: parent_version_id)
47
+
48
+ transaction do
49
+ # Get next version number for this template
50
+ next_version = next_version_number(template_id)
51
+
52
+ # Deactivate all existing versions for this template
53
+ execute(
54
+ "UPDATE template_versions SET is_active = 0 WHERE project_dir = ? AND template_id = ?",
55
+ [project_dir, template_id]
56
+ )
57
+
58
+ # Insert new version as active
59
+ execute(
60
+ insert_sql([
61
+ :project_dir, :template_id, :version_number, :content,
62
+ :parent_version_id, :is_active, :positive_votes, :negative_votes, :metadata
63
+ ]),
64
+ [
65
+ project_dir,
66
+ template_id,
67
+ next_version,
68
+ content,
69
+ parent_version_id,
70
+ 1, # is_active
71
+ 0, # positive_votes
72
+ 0, # negative_votes
73
+ serialize_json(metadata)
74
+ ]
75
+ )
76
+
77
+ id = last_insert_row_id
78
+
79
+ Aidp.log_info("template_version_repository", "version_created",
80
+ template_id: template_id,
81
+ version_number: next_version,
82
+ id: id)
83
+
84
+ {success: true, id: id, version_number: next_version}
85
+ end
86
+ rescue => e
87
+ Aidp.log_error("template_version_repository", "create_failed",
88
+ template_id: template_id,
89
+ error: e.message)
90
+ {success: false, error: e.message}
91
+ end
92
+
93
+ # Get the active version for a template
94
+ #
95
+ # @param template_id [String] Template identifier
96
+ # @return [Hash, nil] Active version or nil
97
+ def active_version(template_id:)
98
+ row = query_one(
99
+ <<~SQL,
100
+ SELECT * FROM template_versions
101
+ WHERE project_dir = ? AND template_id = ? AND is_active = 1
102
+ LIMIT 1
103
+ SQL
104
+ [project_dir, template_id]
105
+ )
106
+
107
+ deserialize_version(row)
108
+ end
109
+
110
+ # Get a specific version by ID
111
+ #
112
+ # @param id [Integer] Version ID
113
+ # @return [Hash, nil] Version or nil
114
+ def find(id:)
115
+ row = query_one(
116
+ "SELECT * FROM template_versions WHERE id = ? AND project_dir = ?",
117
+ [id, project_dir]
118
+ )
119
+
120
+ deserialize_version(row)
121
+ end
122
+
123
+ # Get all versions for a template
124
+ #
125
+ # @param template_id [String] Template identifier
126
+ # @param limit [Integer] Maximum versions to return
127
+ # @return [Array<Hash>] Versions sorted by version_number descending
128
+ def list(template_id:, limit: 20)
129
+ rows = query(
130
+ <<~SQL,
131
+ SELECT * FROM template_versions
132
+ WHERE project_dir = ? AND template_id = ?
133
+ ORDER BY version_number DESC
134
+ LIMIT ?
135
+ SQL
136
+ [project_dir, template_id, limit]
137
+ )
138
+
139
+ rows.map { |row| deserialize_version(row) }.compact
140
+ end
141
+
142
+ # Record a positive vote for a version
143
+ #
144
+ # @param id [Integer] Version ID
145
+ # @return [Hash] Result with :success
146
+ def record_positive_vote(id:)
147
+ Aidp.log_debug("template_version_repository", "recording_positive_vote", id: id)
148
+
149
+ execute(
150
+ "UPDATE template_versions SET positive_votes = positive_votes + 1 WHERE id = ? AND project_dir = ?",
151
+ [id, project_dir]
152
+ )
153
+
154
+ {success: true}
155
+ rescue => e
156
+ Aidp.log_error("template_version_repository", "positive_vote_failed",
157
+ id: id, error: e.message)
158
+ {success: false, error: e.message}
159
+ end
160
+
161
+ # Record a negative vote for a version
162
+ #
163
+ # @param id [Integer] Version ID
164
+ # @return [Hash] Result with :success
165
+ def record_negative_vote(id:)
166
+ Aidp.log_debug("template_version_repository", "recording_negative_vote", id: id)
167
+
168
+ execute(
169
+ "UPDATE template_versions SET negative_votes = negative_votes + 1 WHERE id = ? AND project_dir = ?",
170
+ [id, project_dir]
171
+ )
172
+
173
+ {success: true}
174
+ rescue => e
175
+ Aidp.log_error("template_version_repository", "negative_vote_failed",
176
+ id: id, error: e.message)
177
+ {success: false, error: e.message}
178
+ end
179
+
180
+ # Activate a specific version
181
+ #
182
+ # @param id [Integer] Version ID to activate
183
+ # @return [Hash] Result with :success
184
+ def activate(id:)
185
+ version = find(id: id)
186
+ return {success: false, error: "Version not found"} unless version
187
+
188
+ Aidp.log_debug("template_version_repository", "activating_version",
189
+ id: id,
190
+ template_id: version[:template_id])
191
+
192
+ transaction do
193
+ # Deactivate all versions for this template
194
+ execute(
195
+ "UPDATE template_versions SET is_active = 0 WHERE project_dir = ? AND template_id = ?",
196
+ [project_dir, version[:template_id]]
197
+ )
198
+
199
+ # Activate the specified version
200
+ execute(
201
+ "UPDATE template_versions SET is_active = 1 WHERE id = ? AND project_dir = ?",
202
+ [id, project_dir]
203
+ )
204
+ end
205
+
206
+ Aidp.log_info("template_version_repository", "version_activated",
207
+ id: id,
208
+ template_id: version[:template_id])
209
+
210
+ {success: true}
211
+ rescue => e
212
+ Aidp.log_error("template_version_repository", "activation_failed",
213
+ id: id, error: e.message)
214
+ {success: false, error: e.message}
215
+ end
216
+
217
+ # Get the best version for a template based on positive votes
218
+ # Prioritizes higher positive vote counts
219
+ #
220
+ # @param template_id [String] Template identifier
221
+ # @return [Hash, nil] Best version or nil
222
+ def best_version(template_id:)
223
+ row = query_one(
224
+ <<~SQL,
225
+ SELECT * FROM template_versions
226
+ WHERE project_dir = ? AND template_id = ?
227
+ ORDER BY positive_votes DESC, version_number DESC
228
+ LIMIT 1
229
+ SQL
230
+ [project_dir, template_id]
231
+ )
232
+
233
+ deserialize_version(row)
234
+ end
235
+
236
+ # Get versions needing evolution (have negative votes)
237
+ #
238
+ # @param template_id [String, nil] Filter by template (nil for all)
239
+ # @return [Array<Hash>] Versions with negative votes
240
+ def versions_needing_evolution(template_id: nil)
241
+ rows = if template_id
242
+ query(
243
+ <<~SQL,
244
+ SELECT * FROM template_versions
245
+ WHERE project_dir = ? AND template_id = ? AND negative_votes > 0
246
+ ORDER BY negative_votes DESC
247
+ SQL
248
+ [project_dir, template_id]
249
+ )
250
+ else
251
+ query(
252
+ <<~SQL,
253
+ SELECT * FROM template_versions
254
+ WHERE project_dir = ? AND negative_votes > 0
255
+ ORDER BY negative_votes DESC
256
+ SQL
257
+ [project_dir]
258
+ )
259
+ end
260
+
261
+ rows.map { |row| deserialize_version(row) }.compact
262
+ end
263
+
264
+ # Prune old versions, keeping:
265
+ # - At least MIN_VERSIONS (5) versions
266
+ # - At least MIN_POSITIVE_VERSIONS (2) positive-feedback versions
267
+ #
268
+ # @param template_id [String] Template identifier
269
+ # @return [Hash] Result with :success, :pruned_count
270
+ def prune_old_versions(template_id:)
271
+ Aidp.log_debug("template_version_repository", "pruning_versions",
272
+ template_id: template_id)
273
+
274
+ # Capture active version reference before list/delete operations.
275
+ # This ensures we never prune the currently active version, even if
276
+ # activation changes during pruning (defensive programming).
277
+ active = active_version(template_id: template_id)
278
+
279
+ versions = list(template_id: template_id, limit: 100)
280
+ return {success: true, pruned_count: 0} if versions.size <= MIN_VERSIONS
281
+
282
+ # Identify versions to keep
283
+ versions_to_keep = identify_versions_to_keep(versions)
284
+ versions_to_prune = versions.reject { |v| versions_to_keep.include?(v[:id]) }
285
+
286
+ # Never prune the active version
287
+ versions_to_prune.reject! { |v| v[:id] == active&.dig(:id) }
288
+
289
+ pruned_count = 0
290
+ versions_to_prune.each do |version|
291
+ execute("DELETE FROM template_versions WHERE id = ?", [version[:id]])
292
+ pruned_count += 1
293
+ end
294
+
295
+ Aidp.log_info("template_version_repository", "versions_pruned",
296
+ template_id: template_id,
297
+ pruned_count: pruned_count,
298
+ remaining_count: versions.size - pruned_count)
299
+
300
+ {success: true, pruned_count: pruned_count}
301
+ rescue => e
302
+ Aidp.log_error("template_version_repository", "prune_failed",
303
+ template_id: template_id, error: e.message)
304
+ {success: false, error: e.message}
305
+ end
306
+
307
+ # Count versions for a template
308
+ #
309
+ # @param template_id [String] Template identifier
310
+ # @return [Integer] Version count
311
+ def count(template_id:)
312
+ query_value(
313
+ "SELECT COUNT(*) FROM template_versions WHERE project_dir = ? AND template_id = ?",
314
+ [project_dir, template_id]
315
+ ) || 0
316
+ end
317
+
318
+ # Check if any versions exist for a template
319
+ #
320
+ # @param template_id [String] Template identifier
321
+ # @return [Boolean]
322
+ def any?(template_id:)
323
+ count(template_id: template_id).positive?
324
+ end
325
+
326
+ # Get all unique template IDs with versions
327
+ #
328
+ # @return [Array<String>] Template IDs
329
+ def template_ids
330
+ rows = query(
331
+ "SELECT DISTINCT template_id FROM template_versions WHERE project_dir = ? ORDER BY template_id",
332
+ [project_dir]
333
+ )
334
+
335
+ rows.map { |r| r["template_id"] }
336
+ end
337
+
338
+ private
339
+
340
+ def next_version_number(template_id)
341
+ current_max = query_value(
342
+ "SELECT MAX(version_number) FROM template_versions WHERE project_dir = ? AND template_id = ?",
343
+ [project_dir, template_id]
344
+ )
345
+
346
+ (current_max || 0) + 1
347
+ end
348
+
349
+ def identify_versions_to_keep(versions)
350
+ keep_ids = Set.new
351
+
352
+ # Always keep the most recent MIN_VERSIONS
353
+ versions.take(MIN_VERSIONS).each { |v| keep_ids << v[:id] }
354
+
355
+ # Ensure we keep at least MIN_POSITIVE_VERSIONS with positive votes
356
+ positive_versions = versions.select { |v| v[:positive_votes].positive? }
357
+ .sort_by { |v| -v[:positive_votes] }
358
+
359
+ positive_versions.take(MIN_POSITIVE_VERSIONS).each { |v| keep_ids << v[:id] }
360
+
361
+ keep_ids
362
+ end
363
+
364
+ def deserialize_version(row)
365
+ return nil unless row
366
+
367
+ {
368
+ id: row["id"],
369
+ template_id: row["template_id"],
370
+ version_number: row["version_number"],
371
+ content: row["content"],
372
+ parent_version_id: row["parent_version_id"],
373
+ is_active: row["is_active"] == 1,
374
+ positive_votes: row["positive_votes"] || 0,
375
+ negative_votes: row["negative_votes"] || 0,
376
+ created_at: row["created_at"],
377
+ metadata: deserialize_json(row["metadata"])
378
+ }
379
+ end
380
+ end
381
+ end
382
+ end
383
+ end