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.
- checksums.yaml +4 -4
- data/README.md +14 -14
- data/lib/aidp/agent_harness_adapter.rb +309 -0
- data/lib/aidp/cli/issue_importer.rb +20 -6
- data/lib/aidp/cli/jobs_command.rb +1 -1
- data/lib/aidp/cli/models_command.rb +9 -16
- data/lib/aidp/cli/prompts_command.rb +253 -0
- data/lib/aidp/cli/security_command.rb +4 -10
- data/lib/aidp/cli/storage_command.rb +277 -0
- data/lib/aidp/cli/temporal_command.rb +460 -0
- data/lib/aidp/cli.rb +24 -2
- data/lib/aidp/config.rb +15 -0
- data/lib/aidp/config_paths.rb +12 -2
- data/lib/aidp/database/migrations.rb +114 -0
- data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
- data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
- data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
- data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
- data/lib/aidp/database/repositories/job_repository.rb +278 -0
- data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
- data/lib/aidp/database/repositories/progress_repository.rb +210 -0
- data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
- data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
- data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
- data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
- data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
- data/lib/aidp/database/repositories/task_repository.rb +231 -0
- data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
- data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
- data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
- data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
- data/lib/aidp/database/repository.rb +166 -0
- data/lib/aidp/database/schema.rb +347 -0
- data/lib/aidp/database/storage_migrator.rb +693 -0
- data/lib/aidp/database.rb +139 -0
- data/lib/aidp/debug_mixin.rb +2 -2
- data/lib/aidp/execute/checkpoint.rb +26 -0
- data/lib/aidp/execute/checkpoint_display.rb +16 -0
- data/lib/aidp/execute/prompt_evaluator.rb +30 -57
- data/lib/aidp/execute/work_loop_runner.rb +294 -46
- data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
- data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
- data/lib/aidp/harness/ai_decision_engine.rb +78 -296
- data/lib/aidp/harness/config_schema.rb +59 -0
- data/lib/aidp/harness/configuration.rb +229 -10
- data/lib/aidp/harness/deprecation_cache.rb +53 -11
- data/lib/aidp/harness/enhanced_runner.rb +1 -1
- data/lib/aidp/harness/error_handler.rb +11 -5
- data/lib/aidp/harness/provider_config.rb +36 -0
- data/lib/aidp/harness/provider_factory.rb +26 -24
- data/lib/aidp/harness/provider_info.rb +9 -5
- data/lib/aidp/harness/provider_manager.rb +2 -2
- data/lib/aidp/harness/provider_metrics.rb +103 -1
- data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
- data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
- data/lib/aidp/harness/runner.rb +7 -2
- data/lib/aidp/harness/state_manager.rb +1 -1
- data/lib/aidp/harness/test_runner.rb +167 -11
- data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
- data/lib/aidp/harness/ui.rb +11 -0
- data/lib/aidp/harness/usage_limit.rb +179 -0
- data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
- data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
- data/lib/aidp/harness/usage_period.rb +234 -0
- data/lib/aidp/init/agent_instructions_generator.rb +317 -0
- data/lib/aidp/init/runner.rb +25 -4
- data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
- data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
- data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
- data/lib/aidp/interfaces/logger_interface.rb +150 -0
- data/lib/aidp/interfaces/ui_interface.rb +254 -0
- data/lib/aidp/logger.rb +25 -7
- data/lib/aidp/orchestration_adapter.rb +279 -0
- data/lib/aidp/pr_worktree_manager.rb +4 -15
- data/lib/aidp/prompts/feedback_collector.rb +198 -0
- data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
- data/lib/aidp/prompts/template_evolver.rb +305 -0
- data/lib/aidp/prompts/template_version_manager.rb +325 -0
- data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
- data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
- data/lib/aidp/setup/wizard.rb +616 -176
- data/lib/aidp/shell_executor.rb +58 -0
- data/lib/aidp/style_guide/selector.rb +56 -1
- data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
- data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
- data/lib/aidp/temporal/activities/base_activity.rb +78 -0
- data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
- data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
- data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
- data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
- data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
- data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
- data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
- data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
- data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
- data/lib/aidp/temporal/configuration.rb +169 -0
- data/lib/aidp/temporal/connection.rb +93 -0
- data/lib/aidp/temporal/worker.rb +119 -0
- data/lib/aidp/temporal/workflow_client.rb +171 -0
- data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
- data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
- data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
- data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
- data/lib/aidp/temporal.rb +120 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/base_processor.rb +35 -0
- data/lib/aidp/watch/build_processor.rb +132 -0
- data/lib/aidp/watch/github_state_extractor.rb +0 -8
- data/lib/aidp/watch/rebase_processor.rb +245 -0
- data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
- data/lib/aidp/watch/runner.rb +366 -285
- data/lib/aidp/watch/state_store.rb +80 -0
- data/lib/aidp/watch/work_item.rb +96 -0
- data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
- data/lib/aidp/watch/worktree_reconciler.rb +643 -0
- data/lib/aidp/workflows/guided_agent.rb +1 -1
- data/lib/aidp/worktree.rb +62 -0
- data/templates/aidp.yml.example +70 -0
- metadata +118 -16
- data/lib/aidp/analyze/json_file_storage.rb +0 -292
- data/lib/aidp/execute/future_work_backlog.rb +0 -411
- data/lib/aidp/providers/adapter.rb +0 -239
- data/lib/aidp/providers/aider.rb +0 -264
- data/lib/aidp/providers/anthropic.rb +0 -614
- data/lib/aidp/providers/base.rb +0 -617
- data/lib/aidp/providers/capability_registry.rb +0 -204
- data/lib/aidp/providers/codex.rb +0 -267
- data/lib/aidp/providers/cursor.rb +0 -304
- data/lib/aidp/providers/error_taxonomy.rb +0 -195
- data/lib/aidp/providers/gemini.rb +0 -110
- data/lib/aidp/providers/github_copilot.rb +0 -227
- data/lib/aidp/providers/kilocode.rb +0 -157
- 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
|