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,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "template_version_manager"
5
+ require_relative "../harness/ai_decision_engine"
6
+
7
+ module Aidp
8
+ module Prompts
9
+ # Evolves prompt templates using AGD (AI-Generated Determinism) pattern
10
+ #
11
+ # Per issue #402:
12
+ # - When negative feedback is recorded, AI generates improved template variants
13
+ # - Uses "fix forward" methodology - no rollbacks, only new improved versions
14
+ # - Evolution is triggered automatically on negative feedback
15
+ #
16
+ # AGD Pattern:
17
+ # - AI runs ONCE at configuration/evolution time to generate improved template
18
+ # - Improved template is stored in database
19
+ # - Runtime uses stored template deterministically (no AI calls)
20
+ #
21
+ # @example Evolve a template based on negative feedback
22
+ # evolver = TemplateEvolver.new(config)
23
+ # result = evolver.evolve(
24
+ # template_id: "work_loop/decide_whats_next",
25
+ # suggestions: ["Be more specific about next unit selection"],
26
+ # context: { iterations: 15, task_type: "refactoring" }
27
+ # )
28
+ #
29
+ class TemplateEvolver
30
+ # Template path for evolution prompts
31
+ EVOLUTION_TEMPLATE_PATH = "decision_engine/template_evolution"
32
+
33
+ attr_reader :config, :version_manager, :ai_decision_engine
34
+
35
+ # Special marker to indicate "use default" vs explicit nil
36
+ NOT_PROVIDED = Object.new.freeze
37
+ private_constant :NOT_PROVIDED
38
+
39
+ def initialize(
40
+ config,
41
+ version_manager: nil,
42
+ ai_decision_engine: NOT_PROVIDED,
43
+ project_dir: Dir.pwd
44
+ )
45
+ @config = config
46
+ @project_dir = project_dir
47
+ @version_manager = version_manager ||
48
+ TemplateVersionManager.new(project_dir: project_dir)
49
+ @ai_decision_engine = if ai_decision_engine == NOT_PROVIDED
50
+ build_ai_decision_engine
51
+ else
52
+ ai_decision_engine
53
+ end
54
+ end
55
+
56
+ # Evolve a template based on feedback
57
+ #
58
+ # @param template_id [String] Template identifier
59
+ # @param suggestions [Array<String>] User-provided improvement suggestions
60
+ # @param context [Hash] Additional context (iterations, task_type, etc.)
61
+ # @return [Hash] Result with :success, :new_version_id, :changes
62
+ def evolve(template_id:, suggestions: [], context: {})
63
+ Aidp.log_debug("template_evolver", "starting_evolution",
64
+ template_id: template_id,
65
+ suggestion_count: suggestions.size)
66
+
67
+ # Get current active version
68
+ active_version = @version_manager.active_version(template_id: template_id)
69
+ unless active_version
70
+ Aidp.log_warn("template_evolver", "no_active_version",
71
+ template_id: template_id)
72
+ return {success: false, error: "No active version to evolve"}
73
+ end
74
+
75
+ # Skip if AI engine not available
76
+ unless @ai_decision_engine
77
+ Aidp.log_warn("template_evolver", "ai_engine_unavailable")
78
+ return {success: false, error: "AI decision engine not available"}
79
+ end
80
+
81
+ # Generate improved template using AI (AGD pattern - runs once)
82
+ evolution_result = generate_improved_template(
83
+ current_content: active_version[:content],
84
+ suggestions: suggestions,
85
+ context: context
86
+ )
87
+
88
+ unless evolution_result[:success]
89
+ Aidp.log_error("template_evolver", "evolution_failed",
90
+ template_id: template_id,
91
+ error: evolution_result[:error])
92
+ return evolution_result
93
+ end
94
+
95
+ # Create new version with improved content
96
+ create_result = @version_manager.create_evolved_version(
97
+ template_id: template_id,
98
+ new_content: evolution_result[:improved_content],
99
+ parent_version_id: active_version[:id],
100
+ metadata: {
101
+ suggestions: suggestions,
102
+ context: context,
103
+ changes: evolution_result[:changes]
104
+ }
105
+ )
106
+
107
+ if create_result[:success]
108
+ Aidp.log_info("template_evolver", "evolution_complete",
109
+ template_id: template_id,
110
+ parent_version_id: active_version[:id],
111
+ new_version_id: create_result[:id],
112
+ change_count: evolution_result[:changes]&.size || 0)
113
+
114
+ {
115
+ success: true,
116
+ new_version_id: create_result[:id],
117
+ new_version_number: create_result[:version_number],
118
+ parent_version_id: active_version[:id],
119
+ changes: evolution_result[:changes]
120
+ }
121
+ else
122
+ create_result
123
+ end
124
+ end
125
+
126
+ # Batch evolve all templates needing improvement
127
+ #
128
+ # @return [Array<Hash>] Results for each evolved template
129
+ def evolve_all_pending
130
+ versions_needing = @version_manager.versions_needing_evolution
131
+
132
+ Aidp.log_info("template_evolver", "evolving_pending_versions",
133
+ count: versions_needing.size)
134
+
135
+ results = []
136
+ versions_needing.each do |version|
137
+ # Load metadata with suggestions from negative feedback
138
+ metadata = version[:metadata] || {}
139
+ suggestions = metadata[:suggestions] || []
140
+ context = metadata[:context] || {}
141
+
142
+ result = evolve(
143
+ template_id: version[:template_id],
144
+ suggestions: suggestions,
145
+ context: context
146
+ )
147
+
148
+ results << result.merge(template_id: version[:template_id])
149
+ end
150
+
151
+ results
152
+ end
153
+
154
+ private
155
+
156
+ def build_ai_decision_engine
157
+ return nil unless @config.respond_to?(:default_provider)
158
+
159
+ Harness::AIDecisionEngine.new(@config, project_dir: @project_dir)
160
+ rescue => e
161
+ Aidp.log_debug("template_evolver", "ai_engine_creation_failed",
162
+ error: e.message)
163
+ nil
164
+ end
165
+
166
+ def generate_improved_template(current_content:, suggestions:, context:)
167
+ Aidp.log_debug("template_evolver", "generating_improved_template",
168
+ content_size: current_content.size,
169
+ suggestion_count: suggestions.size)
170
+
171
+ # Parse current template
172
+ current_template = YAML.safe_load(current_content, permitted_classes: [Symbol], aliases: true)
173
+
174
+ # Build evolution prompt
175
+ prompt = build_evolution_prompt(
176
+ current_template: current_template,
177
+ suggestions: suggestions,
178
+ context: context
179
+ )
180
+
181
+ schema = evolution_schema
182
+
183
+ begin
184
+ result = @ai_decision_engine.decide(
185
+ :template_evolution,
186
+ context: {prompt: prompt},
187
+ schema: schema,
188
+ tier: :standard, # Use standard tier for thoughtful improvements
189
+ cache_ttl: nil # Each evolution is unique
190
+ )
191
+
192
+ # Build improved template YAML
193
+ improved_template = apply_improvements(current_template, result)
194
+ improved_content = YAML.dump(improved_template)
195
+
196
+ {
197
+ success: true,
198
+ improved_content: improved_content,
199
+ changes: result[:changes] || []
200
+ }
201
+ rescue => e
202
+ Aidp.log_error("template_evolver", "ai_evolution_failed",
203
+ error: e.message,
204
+ error_class: e.class.name)
205
+
206
+ {success: false, error: "AI evolution failed: #{e.message}"}
207
+ end
208
+ end
209
+
210
+ def build_evolution_prompt(current_template:, suggestions:, context:)
211
+ prompt_text = current_template["prompt"] || current_template[:prompt] || ""
212
+ variables = current_template["variables"] || current_template[:variables] || []
213
+
214
+ <<~PROMPT
215
+ # Template Evolution Task
216
+
217
+ You are improving a prompt template based on user feedback.
218
+
219
+ ## Current Template
220
+
221
+ Name: #{current_template["name"] || current_template[:name]}
222
+ Description: #{current_template["description"] || current_template[:description]}
223
+ Version: #{current_template["version"] || current_template[:version]}
224
+
225
+ ### Current Prompt Text
226
+
227
+ ```
228
+ #{prompt_text}
229
+ ```
230
+
231
+ ### Variables Used
232
+
233
+ #{variables.join(", ")}
234
+
235
+ ## User Feedback (Suggestions for Improvement)
236
+
237
+ #{suggestions.map { |s| "- #{s}" }.join("\n")}
238
+
239
+ ## Context
240
+
241
+ #{context.map { |k, v| "- #{k}: #{v}" }.join("\n")}
242
+
243
+ ## Your Task
244
+
245
+ Improve this template based on the feedback. Focus on:
246
+ 1. Addressing specific user suggestions
247
+ 2. Making instructions clearer and more specific
248
+ 3. Adding missing context or guidance
249
+ 4. Improving completion criteria definitions
250
+ 5. Maintaining existing variables (do not add new ones unless essential)
251
+
252
+ Provide the improved prompt text and describe the changes made.
253
+ PROMPT
254
+ end
255
+
256
+ def evolution_schema
257
+ {
258
+ type: "object",
259
+ properties: {
260
+ improved_prompt: {
261
+ type: "string",
262
+ description: "The improved prompt text"
263
+ },
264
+ changes: {
265
+ type: "array",
266
+ items: {type: "string"},
267
+ description: "List of changes made to the template"
268
+ },
269
+ reasoning: {
270
+ type: "string",
271
+ description: "Reasoning for the improvements"
272
+ }
273
+ },
274
+ required: ["improved_prompt", "changes"]
275
+ }
276
+ end
277
+
278
+ def apply_improvements(current_template, ai_result)
279
+ # Use merge to create a new hash rather than deep copying. This is cleaner
280
+ # than Marshal.load/dump which has performance overhead, can fail with complex
281
+ # objects, and has security implications with untrusted data.
282
+ current_version = current_template["version"] || "1.0.0"
283
+
284
+ # Create new hash by merging original with updated fields.
285
+ # Evolution history is tracked in metadata (passed to create_evolved_version)
286
+ # rather than appending to description, which would accumulate redundant
287
+ # suffixes like "(AI-evolved)" on each evolution cycle.
288
+ current_template.merge(
289
+ "prompt" => ai_result[:improved_prompt],
290
+ "version" => increment_version(current_version)
291
+ )
292
+ end
293
+
294
+ def increment_version(version_string)
295
+ parts = version_string.to_s.split(".")
296
+ parts = ["1", "0", "0"] if parts.empty?
297
+
298
+ # Increment patch version
299
+ parts[2] = (parts[2].to_i + 1).to_s
300
+
301
+ parts.join(".")
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,325 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../database/repositories/template_version_repository"
4
+ require_relative "prompt_template_manager"
5
+
6
+ module Aidp
7
+ module Prompts
8
+ # Manages versioned prompt templates with feedback-based selection
9
+ #
10
+ # Per issue #402:
11
+ # - Positive feedback: Count votes, prioritize for future use
12
+ # - Negative feedback: Trigger AGD to create new variant
13
+ # - Retain last 5 versions, ensuring at least 2 positive-feedback versions
14
+ # - Per-project feedback tracking
15
+ # - Focus on work-loop templates initially
16
+ #
17
+ # @example Record positive feedback
18
+ # manager = TemplateVersionManager.new(project_dir: Dir.pwd)
19
+ # manager.record_positive_feedback(template_id: "work_loop/decide_whats_next")
20
+ #
21
+ # @example Record negative feedback (triggers evolution)
22
+ # manager.record_negative_feedback(
23
+ # template_id: "work_loop/decide_whats_next",
24
+ # suggestions: ["Be more specific about next unit selection"]
25
+ # )
26
+ #
27
+ class TemplateVersionManager
28
+ # Categories eligible for versioning (focus on work-loop initially)
29
+ VERSIONED_CATEGORIES = %w[work_loop].freeze
30
+
31
+ attr_reader :project_dir, :repository, :template_manager
32
+
33
+ def initialize(
34
+ project_dir: Dir.pwd,
35
+ repository: nil,
36
+ template_manager: nil
37
+ )
38
+ @project_dir = project_dir
39
+ @repository = repository ||
40
+ Database::Repositories::TemplateVersionRepository.new(project_dir: project_dir)
41
+ # Pass self as version_manager to avoid circular dependency where
42
+ # PromptTemplateManager creates its own TemplateVersionManager instance
43
+ @template_manager = template_manager ||
44
+ PromptTemplateManager.new(project_dir: project_dir, version_manager: self)
45
+ end
46
+
47
+ # Check if a template is eligible for versioning
48
+ #
49
+ # @param template_id [String] Template identifier
50
+ # @return [Boolean]
51
+ def versionable?(template_id)
52
+ return false unless valid_template_id?(template_id)
53
+
54
+ category = template_id.split("/").first
55
+ VERSIONED_CATEGORIES.include?(category)
56
+ end
57
+
58
+ # Validate template_id format (must be "category/name")
59
+ #
60
+ # @param template_id [String] Template identifier
61
+ # @return [Boolean]
62
+ def valid_template_id?(template_id)
63
+ return false if template_id.nil? || template_id.empty?
64
+
65
+ parts = template_id.split("/")
66
+ parts.length == 2 && parts.all? { |p| !p.empty? }
67
+ end
68
+
69
+ # Initialize versioning for a template by importing current content
70
+ #
71
+ # @param template_id [String] Template identifier
72
+ # @return [Hash] Result with :success, :version_id
73
+ def initialize_versioning(template_id:)
74
+ return {success: false, error: "Template not versionable"} unless versionable?(template_id)
75
+ return {success: true, already_versioned: true} if @repository.any?(template_id: template_id)
76
+
77
+ Aidp.log_debug("template_version_manager", "initializing_versioning",
78
+ template_id: template_id)
79
+
80
+ # Load current template content from file (not versioned, since we're initializing)
81
+ template_data = @template_manager.load_template(template_id, use_versioned: false)
82
+ return {success: false, error: "Template not found"} unless template_data
83
+
84
+ # Store as version 1
85
+ content = YAML.dump(template_data)
86
+ result = @repository.create(
87
+ template_id: template_id,
88
+ content: content,
89
+ metadata: {source: "initial_import"}
90
+ )
91
+
92
+ if result[:success]
93
+ Aidp.log_info("template_version_manager", "versioning_initialized",
94
+ template_id: template_id,
95
+ version_number: result[:version_number])
96
+ end
97
+
98
+ result
99
+ end
100
+
101
+ # Record positive feedback for active template version
102
+ #
103
+ # @param template_id [String] Template identifier
104
+ # @return [Hash] Result with :success
105
+ def record_positive_feedback(template_id:)
106
+ return {success: false, error: "Template not versionable"} unless versionable?(template_id)
107
+
108
+ # Ensure versioning is initialized
109
+ init_result = initialize_versioning(template_id: template_id)
110
+ return init_result unless init_result[:success] || init_result[:already_versioned]
111
+
112
+ active = @repository.active_version(template_id: template_id)
113
+ return {success: false, error: "No active version"} unless active
114
+
115
+ Aidp.log_debug("template_version_manager", "recording_positive_feedback",
116
+ template_id: template_id,
117
+ version_id: active[:id])
118
+
119
+ result = @repository.record_positive_vote(id: active[:id])
120
+
121
+ if result[:success]
122
+ Aidp.log_info("template_version_manager", "positive_feedback_recorded",
123
+ template_id: template_id,
124
+ version_id: active[:id])
125
+ end
126
+
127
+ result
128
+ end
129
+
130
+ # Record negative feedback for active template version
131
+ # Triggers AGD-based evolution when evolve_on_negative is true
132
+ #
133
+ # @param template_id [String] Template identifier
134
+ # @param suggestions [Array<String>] User-provided improvement suggestions
135
+ # @param context [Hash] Additional context for evolution
136
+ # @param evolve_on_negative [Boolean] Whether to trigger evolution (default: true)
137
+ # @return [Hash] Result with :success, :evolution_triggered, :new_version_id
138
+ def record_negative_feedback(template_id:, suggestions: [], context: {}, evolve_on_negative: true)
139
+ return {success: false, error: "Template not versionable"} unless versionable?(template_id)
140
+
141
+ # Ensure versioning is initialized
142
+ init_result = initialize_versioning(template_id: template_id)
143
+ return init_result unless init_result[:success] || init_result[:already_versioned]
144
+
145
+ active = @repository.active_version(template_id: template_id)
146
+ return {success: false, error: "No active version"} unless active
147
+
148
+ Aidp.log_debug("template_version_manager", "recording_negative_feedback",
149
+ template_id: template_id,
150
+ version_id: active[:id],
151
+ suggestion_count: suggestions.size)
152
+
153
+ # Record the negative vote
154
+ result = @repository.record_negative_vote(id: active[:id])
155
+ return result unless result[:success]
156
+
157
+ Aidp.log_info("template_version_manager", "negative_feedback_recorded",
158
+ template_id: template_id,
159
+ version_id: active[:id],
160
+ total_negative: active[:negative_votes] + 1)
161
+
162
+ # Store suggestions and context for later evolution
163
+ evolution_result = {
164
+ success: true,
165
+ evolution_triggered: false,
166
+ version_id: active[:id]
167
+ }
168
+
169
+ # Per issue #402: "Negative feedback (even a single vote) should trigger AGD"
170
+ if evolve_on_negative
171
+ evolution_result[:evolution_pending] = true
172
+ evolution_result[:suggestions] = suggestions
173
+ evolution_result[:context] = context
174
+
175
+ Aidp.log_info("template_version_manager", "evolution_pending",
176
+ template_id: template_id,
177
+ version_id: active[:id])
178
+ end
179
+
180
+ evolution_result
181
+ end
182
+
183
+ # Evolve a template by creating a new AGD-generated variant
184
+ # Called by TemplateEvolver after AI generates improved content
185
+ #
186
+ # @param template_id [String] Template identifier
187
+ # @param new_content [String] New template YAML content
188
+ # @param parent_version_id [Integer] ID of the version being evolved
189
+ # @param metadata [Hash] Evolution metadata
190
+ # @return [Hash] Result with :success, :new_version_id
191
+ def create_evolved_version(template_id:, new_content:, parent_version_id:, metadata: {})
192
+ return {success: false, error: "Template not versionable"} unless versionable?(template_id)
193
+
194
+ Aidp.log_debug("template_version_manager", "creating_evolved_version",
195
+ template_id: template_id,
196
+ parent_version_id: parent_version_id)
197
+
198
+ result = @repository.create(
199
+ template_id: template_id,
200
+ content: new_content,
201
+ parent_version_id: parent_version_id,
202
+ metadata: metadata.merge(source: "agd_evolution")
203
+ )
204
+
205
+ if result[:success]
206
+ # Prune old versions
207
+ @repository.prune_old_versions(template_id: template_id)
208
+
209
+ Aidp.log_info("template_version_manager", "evolved_version_created",
210
+ template_id: template_id,
211
+ new_version_number: result[:version_number],
212
+ parent_version_id: parent_version_id)
213
+ end
214
+
215
+ result
216
+ end
217
+
218
+ # Get the best template version to use
219
+ # Prioritizes versions with higher positive votes
220
+ #
221
+ # @param template_id [String] Template identifier
222
+ # @return [Hash, nil] Best version or nil
223
+ def best_version(template_id:)
224
+ return nil unless versionable?(template_id)
225
+
226
+ @repository.best_version(template_id: template_id)
227
+ end
228
+
229
+ # Get the currently active version
230
+ #
231
+ # @param template_id [String] Template identifier
232
+ # @return [Hash, nil] Active version or nil
233
+ def active_version(template_id:)
234
+ return nil unless versionable?(template_id)
235
+
236
+ @repository.active_version(template_id: template_id)
237
+ end
238
+
239
+ # Activate a specific version
240
+ #
241
+ # @param version_id [Integer] Version ID to activate
242
+ # @return [Hash] Result with :success
243
+ def activate_version(version_id:)
244
+ @repository.activate(id: version_id)
245
+ end
246
+
247
+ # List all versions for a template
248
+ #
249
+ # @param template_id [String] Template identifier
250
+ # @param limit [Integer] Maximum versions to return
251
+ # @return [Array<Hash>] Versions
252
+ def list_versions(template_id:, limit: 20)
253
+ return [] unless versionable?(template_id)
254
+
255
+ @repository.list(template_id: template_id, limit: limit)
256
+ end
257
+
258
+ # Get versions that need evolution (have negative feedback)
259
+ #
260
+ # @param template_id [String, nil] Filter by template
261
+ # @return [Array<Hash>] Versions needing evolution
262
+ def versions_needing_evolution(template_id: nil)
263
+ @repository.versions_needing_evolution(template_id: template_id)
264
+ end
265
+
266
+ # Get version statistics for a template
267
+ #
268
+ # @param template_id [String] Template identifier
269
+ # @return [Hash] Statistics
270
+ def version_stats(template_id:)
271
+ return {error: "Template not versionable"} unless versionable?(template_id)
272
+
273
+ versions = @repository.list(template_id: template_id, limit: 100)
274
+
275
+ {
276
+ template_id: template_id,
277
+ total_versions: versions.size,
278
+ active_version: versions.find { |v| v[:is_active] }&.dig(:version_number),
279
+ total_positive_votes: versions.sum { |v| v[:positive_votes] },
280
+ total_negative_votes: versions.sum { |v| v[:negative_votes] },
281
+ best_version: @repository.best_version(template_id: template_id)&.dig(:version_number),
282
+ oldest_version: versions.last&.dig(:created_at),
283
+ newest_version: versions.first&.dig(:created_at)
284
+ }
285
+ end
286
+
287
+ # Get all template IDs that have versioning enabled
288
+ #
289
+ # @return [Array<String>] Template IDs
290
+ def versioned_template_ids
291
+ @repository.template_ids
292
+ end
293
+
294
+ # Render a versioned template, using the active version if available
295
+ #
296
+ # @param template_id [String] Template identifier
297
+ # @param variables [Hash] Variables to substitute
298
+ # @return [String, nil] Rendered prompt or nil
299
+ def render_versioned(template_id, **variables)
300
+ return nil unless versionable?(template_id)
301
+
302
+ active = @repository.active_version(template_id: template_id)
303
+ return nil unless active
304
+
305
+ Aidp.log_debug("template_version_manager", "rendering_versioned_template",
306
+ template_id: template_id,
307
+ version_id: active[:id],
308
+ version_number: active[:version_number])
309
+
310
+ # Parse the stored YAML content
311
+ template_data = YAML.safe_load(active[:content], permitted_classes: [Symbol], aliases: true)
312
+ prompt_text = template_data["prompt"] || template_data[:prompt]
313
+ return nil unless prompt_text
314
+
315
+ # Substitute variables
316
+ result = prompt_text.dup
317
+ variables.each do |key, value|
318
+ result.gsub!("{{#{key}}}", value.to_s)
319
+ end
320
+
321
+ result
322
+ end
323
+ end
324
+ end
325
+ end