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,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+
5
+ module Aidp
6
+ module Database
7
+ module Repositories
8
+ # Repository for progress table
9
+ # Replaces progress/execute.yml and progress/analyze.yml
10
+ class ProgressRepository < Repository
11
+ VALID_MODES = %w[execute analyze].freeze
12
+
13
+ def initialize(project_dir: Dir.pwd)
14
+ super(project_dir: project_dir, table_name: "progress")
15
+ end
16
+
17
+ # Get progress for a mode
18
+ #
19
+ # @param mode [String, Symbol] Mode (execute or analyze)
20
+ # @return [Hash] Progress data
21
+ def get(mode)
22
+ mode_str = mode.to_s
23
+ row = query_one(
24
+ "SELECT * FROM progress WHERE project_dir = ? AND mode = ?",
25
+ [project_dir, mode_str]
26
+ )
27
+
28
+ return empty_progress(mode_str) unless row
29
+
30
+ deserialize_progress(row)
31
+ end
32
+
33
+ # Get completed steps
34
+ #
35
+ # @param mode [String, Symbol] Mode
36
+ # @return [Array<String>] Completed step names
37
+ def completed_steps(mode)
38
+ progress = get(mode)
39
+ progress[:steps_completed] || []
40
+ end
41
+
42
+ # Get current step
43
+ #
44
+ # @param mode [String, Symbol] Mode
45
+ # @return [String, nil] Current step name
46
+ def current_step(mode)
47
+ progress = get(mode)
48
+ progress[:current_step]
49
+ end
50
+
51
+ # Check if step is completed
52
+ #
53
+ # @param mode [String, Symbol] Mode
54
+ # @param step_name [String] Step name
55
+ # @return [Boolean]
56
+ def step_completed?(mode, step_name)
57
+ completed_steps(mode).include?(step_name)
58
+ end
59
+
60
+ # Mark step as completed
61
+ #
62
+ # @param mode [String, Symbol] Mode
63
+ # @param step_name [String] Step name
64
+ def mark_step_completed(mode, step_name)
65
+ mode_str = mode.to_s
66
+ progress = get(mode)
67
+
68
+ steps = progress[:steps_completed] || []
69
+ steps << step_name unless steps.include?(step_name)
70
+
71
+ now = current_timestamp
72
+ started_at = progress[:started_at] || now
73
+
74
+ upsert_progress(
75
+ mode: mode_str,
76
+ current_step: nil,
77
+ steps_completed: steps,
78
+ started_at: started_at,
79
+ updated_at: now
80
+ )
81
+
82
+ Aidp.log_debug("progress_repository", "step_completed",
83
+ mode: mode_str, step: step_name)
84
+ end
85
+
86
+ # Mark step as in progress
87
+ #
88
+ # @param mode [String, Symbol] Mode
89
+ # @param step_name [String] Step name
90
+ def mark_step_in_progress(mode, step_name)
91
+ mode_str = mode.to_s
92
+ progress = get(mode)
93
+
94
+ now = current_timestamp
95
+ started_at = progress[:started_at] || now
96
+
97
+ upsert_progress(
98
+ mode: mode_str,
99
+ current_step: step_name,
100
+ steps_completed: progress[:steps_completed] || [],
101
+ started_at: started_at,
102
+ updated_at: now
103
+ )
104
+
105
+ Aidp.log_debug("progress_repository", "step_in_progress",
106
+ mode: mode_str, step: step_name)
107
+ end
108
+
109
+ # Reset progress for a mode
110
+ #
111
+ # @param mode [String, Symbol] Mode
112
+ def reset(mode)
113
+ mode_str = mode.to_s
114
+ execute(
115
+ "DELETE FROM progress WHERE project_dir = ? AND mode = ?",
116
+ [project_dir, mode_str]
117
+ )
118
+
119
+ Aidp.log_debug("progress_repository", "reset", mode: mode_str)
120
+ end
121
+
122
+ # Get started_at timestamp
123
+ #
124
+ # @param mode [String, Symbol] Mode
125
+ # @return [Time, nil] Started at time
126
+ def started_at(mode)
127
+ progress = get(mode)
128
+ return nil unless progress[:started_at]
129
+
130
+ Time.parse(progress[:started_at])
131
+ rescue ArgumentError
132
+ nil
133
+ end
134
+
135
+ # Upsert progress data (for migrations and direct updates)
136
+ #
137
+ # @param mode [String] Mode (execute or analyze)
138
+ # @param current_step [String, nil] Current step name
139
+ # @param steps_completed [Array<String>] Completed step names
140
+ # @param started_at [String] Started at timestamp
141
+ # @param updated_at [String] Updated at timestamp
142
+ def upsert_progress(mode:, current_step:, steps_completed:, started_at:, updated_at:)
143
+ existing = query_one(
144
+ "SELECT id FROM progress WHERE project_dir = ? AND mode = ?",
145
+ [project_dir, mode]
146
+ )
147
+
148
+ if existing
149
+ execute(
150
+ <<~SQL,
151
+ UPDATE progress SET
152
+ current_step = ?,
153
+ steps_completed = ?,
154
+ started_at = ?,
155
+ updated_at = ?
156
+ WHERE project_dir = ? AND mode = ?
157
+ SQL
158
+ [
159
+ current_step,
160
+ serialize_json(steps_completed),
161
+ started_at,
162
+ updated_at,
163
+ project_dir,
164
+ mode
165
+ ]
166
+ )
167
+ else
168
+ execute(
169
+ insert_sql([
170
+ :project_dir, :mode, :current_step, :steps_completed,
171
+ :started_at, :updated_at
172
+ ]),
173
+ [
174
+ project_dir,
175
+ mode,
176
+ current_step,
177
+ serialize_json(steps_completed),
178
+ started_at,
179
+ updated_at
180
+ ]
181
+ )
182
+ end
183
+ end
184
+
185
+ private
186
+
187
+ def empty_progress(mode)
188
+ {
189
+ mode: mode,
190
+ current_step: nil,
191
+ steps_completed: [],
192
+ started_at: nil,
193
+ updated_at: nil
194
+ }
195
+ end
196
+
197
+ def deserialize_progress(row)
198
+ {
199
+ id: row["id"],
200
+ mode: row["mode"],
201
+ current_step: row["current_step"],
202
+ steps_completed: deserialize_json(row["steps_completed"]) || [],
203
+ started_at: row["started_at"],
204
+ updated_at: row["updated_at"]
205
+ }
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+
5
+ module Aidp
6
+ module Database
7
+ module Repositories
8
+ # Repository for prompt_archive table
9
+ # Replaces prompt_archive/*.md files
10
+ # Write-only audit trail of prompts
11
+ class PromptArchiveRepository < Repository
12
+ def initialize(project_dir: Dir.pwd)
13
+ super(project_dir: project_dir, table_name: "prompt_archive")
14
+ end
15
+
16
+ # Archive a prompt
17
+ #
18
+ # @param step_name [String, nil] Step name
19
+ # @param content [String] Prompt content
20
+ # @return [Integer] Archive entry ID
21
+ def archive(step_name:, content:)
22
+ now = current_timestamp
23
+
24
+ execute(
25
+ insert_sql([:project_dir, :step_name, :content, :archived_at]),
26
+ [project_dir, step_name, content, now]
27
+ )
28
+
29
+ id = last_insert_row_id
30
+
31
+ Aidp.log_debug("prompt_archive_repository", "archived",
32
+ id: id, step: step_name, size: content.length)
33
+
34
+ id
35
+ end
36
+
37
+ # Get recent archived prompts
38
+ #
39
+ # @param limit [Integer] Maximum entries
40
+ # @param step_name [String, nil] Filter by step
41
+ # @return [Array<Hash>] Archive entries
42
+ def recent(limit: 50, step_name: nil)
43
+ rows = if step_name
44
+ query(
45
+ <<~SQL,
46
+ SELECT * FROM prompt_archive
47
+ WHERE project_dir = ? AND step_name = ?
48
+ ORDER BY archived_at DESC
49
+ LIMIT ?
50
+ SQL
51
+ [project_dir, step_name, limit]
52
+ )
53
+ else
54
+ query(
55
+ <<~SQL,
56
+ SELECT * FROM prompt_archive
57
+ WHERE project_dir = ?
58
+ ORDER BY archived_at DESC
59
+ LIMIT ?
60
+ SQL
61
+ [project_dir, limit]
62
+ )
63
+ end
64
+
65
+ rows.map { |row| deserialize_entry(row) }
66
+ end
67
+
68
+ # Get archive entry by ID
69
+ #
70
+ # @param id [Integer] Entry ID
71
+ # @return [Hash, nil] Entry or nil
72
+ def find(id)
73
+ row = query_one(
74
+ "SELECT * FROM prompt_archive WHERE id = ? AND project_dir = ?",
75
+ [id, project_dir]
76
+ )
77
+ deserialize_entry(row)
78
+ end
79
+
80
+ # Get latest archived prompt for a step
81
+ #
82
+ # @param step_name [String] Step name
83
+ # @return [Hash, nil] Latest entry or nil
84
+ def latest_for_step(step_name)
85
+ row = query_one(
86
+ <<~SQL,
87
+ SELECT * FROM prompt_archive
88
+ WHERE project_dir = ? AND step_name = ?
89
+ ORDER BY archived_at DESC, id DESC
90
+ LIMIT 1
91
+ SQL
92
+ [project_dir, step_name]
93
+ )
94
+ deserialize_entry(row)
95
+ end
96
+
97
+ # Get archive stats
98
+ #
99
+ # @return [Hash] Statistics
100
+ def stats
101
+ total = query_value(
102
+ "SELECT COUNT(*) FROM prompt_archive WHERE project_dir = ?",
103
+ [project_dir]
104
+ ) || 0
105
+
106
+ by_step_rows = query(
107
+ <<~SQL,
108
+ SELECT step_name, COUNT(*) as count
109
+ FROM prompt_archive
110
+ WHERE project_dir = ?
111
+ GROUP BY step_name
112
+ SQL
113
+ [project_dir]
114
+ )
115
+
116
+ by_step = by_step_rows.each_with_object({}) do |row, h|
117
+ h[row["step_name"] || "unknown"] = row["count"]
118
+ end
119
+
120
+ first = query_one(
121
+ "SELECT archived_at FROM prompt_archive WHERE project_dir = ? ORDER BY archived_at ASC LIMIT 1",
122
+ [project_dir]
123
+ )
124
+
125
+ last = query_one(
126
+ "SELECT archived_at FROM prompt_archive WHERE project_dir = ? ORDER BY archived_at DESC LIMIT 1",
127
+ [project_dir]
128
+ )
129
+
130
+ {
131
+ total: total,
132
+ by_step: by_step,
133
+ first_archived_at: first&.dig("archived_at"),
134
+ last_archived_at: last&.dig("archived_at")
135
+ }
136
+ end
137
+
138
+ # Search archived prompts
139
+ #
140
+ # @param query_text [String] Search text
141
+ # @param limit [Integer] Maximum results
142
+ # @return [Array<Hash>] Matching entries
143
+ def search(query_text, limit: 20)
144
+ rows = query(
145
+ <<~SQL,
146
+ SELECT * FROM prompt_archive
147
+ WHERE project_dir = ? AND content LIKE ?
148
+ ORDER BY archived_at DESC
149
+ LIMIT ?
150
+ SQL
151
+ [project_dir, "%#{query_text}%", limit]
152
+ )
153
+
154
+ rows.map { |row| deserialize_entry(row) }
155
+ end
156
+
157
+ # Clear old archives (keep recent N days)
158
+ #
159
+ # @param days_to_keep [Integer] Days of history to retain
160
+ # @return [Integer] Number of entries deleted
161
+ def cleanup(days_to_keep: 30)
162
+ threshold = (Time.now - (days_to_keep * 24 * 60 * 60)).strftime("%Y-%m-%d %H:%M:%S")
163
+
164
+ count = query_value(
165
+ "SELECT COUNT(*) FROM prompt_archive WHERE project_dir = ? AND archived_at < ?",
166
+ [project_dir, threshold]
167
+ ) || 0
168
+
169
+ execute(
170
+ "DELETE FROM prompt_archive WHERE project_dir = ? AND archived_at < ?",
171
+ [project_dir, threshold]
172
+ )
173
+
174
+ Aidp.log_debug("prompt_archive_repository", "cleanup",
175
+ deleted: count, threshold: threshold)
176
+
177
+ count
178
+ end
179
+
180
+ private
181
+
182
+ def deserialize_entry(row)
183
+ return nil unless row
184
+
185
+ {
186
+ id: row["id"],
187
+ step_name: row["step_name"],
188
+ content: row["content"],
189
+ archived_at: row["archived_at"]
190
+ }
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+
5
+ module Aidp
6
+ module Database
7
+ module Repositories
8
+ # Repository for prompt_feedback table
9
+ # Tracks prompt template effectiveness for AGD evolution
10
+ class PromptFeedbackRepository < Repository
11
+ VALID_OUTCOMES = %w[success failure abandoned timeout].freeze
12
+ VALID_REACTIONS = %w[positive negative neutral].freeze
13
+
14
+ def initialize(project_dir: Dir.pwd)
15
+ super(project_dir: project_dir, table_name: "prompt_feedback")
16
+ end
17
+
18
+ # Record feedback for a prompt template
19
+ #
20
+ # @param record [Hash] Feedback data with keys:
21
+ # - template_id [String] Template identifier
22
+ # - outcome [String] success/failure/abandoned/timeout
23
+ # - iterations [Integer, nil] Number of iterations to completion
24
+ # - user_reaction [String, nil] positive/negative/neutral
25
+ # - suggestions [Array<String>, nil] Improvement suggestions
26
+ # - context [Hash, nil] Additional context
27
+ # @return [Hash] Result with :success and :id
28
+ def record(record)
29
+ execute(
30
+ insert_sql([
31
+ :project_dir, :template_id, :outcome, :iterations,
32
+ :user_reaction, :suggestions, :context, :aidp_version
33
+ ]),
34
+ [
35
+ project_dir,
36
+ record[:template_id],
37
+ record[:outcome].to_s,
38
+ record[:iterations],
39
+ record[:user_reaction]&.to_s,
40
+ serialize_json(record[:suggestions]),
41
+ serialize_json(record[:context] || {}),
42
+ Aidp::VERSION
43
+ ]
44
+ )
45
+
46
+ Aidp.log_debug("prompt_feedback_repository", "recorded",
47
+ template_id: record[:template_id], outcome: record[:outcome])
48
+
49
+ {success: true, id: last_insert_row_id}
50
+ rescue => e
51
+ Aidp.log_debug("prompt_feedback_repository", "record_failed",
52
+ template_id: record[:template_id], error: e.message)
53
+ {success: false, error: e.message}
54
+ end
55
+
56
+ # Get summary statistics for a template
57
+ #
58
+ # @param template_id [String] Template identifier
59
+ # @return [Hash] Summary statistics
60
+ def summary(template_id:)
61
+ total = query_value(
62
+ "SELECT COUNT(*) FROM prompt_feedback WHERE project_dir = ? AND template_id = ?",
63
+ [project_dir, template_id]
64
+ ) || 0
65
+
66
+ return empty_summary(template_id) if total.zero?
67
+
68
+ success_count = query_value(
69
+ "SELECT COUNT(*) FROM prompt_feedback WHERE project_dir = ? AND template_id = ? AND outcome = 'success'",
70
+ [project_dir, template_id]
71
+ ) || 0
72
+
73
+ failure_count = query_value(
74
+ "SELECT COUNT(*) FROM prompt_feedback WHERE project_dir = ? AND template_id = ? AND outcome = 'failure'",
75
+ [project_dir, template_id]
76
+ ) || 0
77
+
78
+ avg_iterations = query_value(
79
+ "SELECT AVG(iterations) FROM prompt_feedback WHERE project_dir = ? AND template_id = ? AND iterations IS NOT NULL",
80
+ [project_dir, template_id]
81
+ )
82
+
83
+ positive_reactions = query_value(
84
+ "SELECT COUNT(*) FROM prompt_feedback WHERE project_dir = ? AND template_id = ? AND user_reaction = 'positive'",
85
+ [project_dir, template_id]
86
+ ) || 0
87
+
88
+ negative_reactions = query_value(
89
+ "SELECT COUNT(*) FROM prompt_feedback WHERE project_dir = ? AND template_id = ? AND user_reaction = 'negative'",
90
+ [project_dir, template_id]
91
+ ) || 0
92
+
93
+ # Get suggestions
94
+ suggestion_rows = query(
95
+ "SELECT suggestions FROM prompt_feedback WHERE project_dir = ? AND template_id = ? AND suggestions IS NOT NULL",
96
+ [project_dir, template_id]
97
+ )
98
+ all_suggestions = suggestion_rows.flat_map { |r| deserialize_json(r["suggestions"]) || [] }.compact.uniq
99
+
100
+ first_row = query_one(
101
+ "SELECT created_at FROM prompt_feedback WHERE project_dir = ? AND template_id = ? ORDER BY created_at ASC LIMIT 1",
102
+ [project_dir, template_id]
103
+ )
104
+ last_row = query_one(
105
+ "SELECT created_at FROM prompt_feedback WHERE project_dir = ? AND template_id = ? ORDER BY created_at DESC LIMIT 1",
106
+ [project_dir, template_id]
107
+ )
108
+
109
+ {
110
+ template_id: template_id,
111
+ total_uses: total,
112
+ success_rate: (total > 0) ? (success_count.to_f / total * 100).round(1) : 0,
113
+ success_count: success_count,
114
+ failure_count: failure_count,
115
+ avg_iterations: avg_iterations&.round(1),
116
+ positive_reactions: positive_reactions,
117
+ negative_reactions: negative_reactions,
118
+ common_suggestions: all_suggestions.take(5),
119
+ first_use: first_row&.dig("created_at"),
120
+ last_use: last_row&.dig("created_at")
121
+ }
122
+ end
123
+
124
+ # List feedback entries with filtering
125
+ #
126
+ # @param template_id [String, nil] Filter by template
127
+ # @param outcome [String, nil] Filter by outcome
128
+ # @param limit [Integer] Maximum entries
129
+ # @return [Array<Hash>] Feedback entries
130
+ def list(template_id: nil, outcome: nil, limit: 100)
131
+ conditions = ["project_dir = ?"]
132
+ params = [project_dir]
133
+
134
+ if template_id
135
+ conditions << "template_id = ?"
136
+ params << template_id
137
+ end
138
+
139
+ if outcome
140
+ conditions << "outcome = ?"
141
+ params << outcome.to_s
142
+ end
143
+
144
+ params << limit
145
+
146
+ rows = query(
147
+ <<~SQL,
148
+ SELECT * FROM prompt_feedback
149
+ WHERE #{conditions.join(" AND ")}
150
+ ORDER BY created_at DESC, id DESC
151
+ LIMIT ?
152
+ SQL
153
+ params
154
+ )
155
+
156
+ rows.map { |row| deserialize_feedback(row) }
157
+ end
158
+
159
+ # Find templates that need improvement
160
+ #
161
+ # @param min_uses [Integer] Minimum uses to consider
162
+ # @param max_success_rate [Float] Success rate threshold
163
+ # @return [Array<Hash>] Templates needing improvement
164
+ def templates_needing_improvement(min_uses: 5, max_success_rate: 70.0)
165
+ # Use a single aggregated query instead of N+1 queries
166
+ rows = query(
167
+ <<~SQL,
168
+ SELECT
169
+ template_id,
170
+ COUNT(*) as total_uses,
171
+ SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as success_count,
172
+ SUM(CASE WHEN outcome = 'failure' THEN 1 ELSE 0 END) as failure_count,
173
+ AVG(CASE WHEN iterations IS NOT NULL THEN iterations END) as avg_iterations,
174
+ SUM(CASE WHEN user_reaction = 'positive' THEN 1 ELSE 0 END) as positive_reactions,
175
+ SUM(CASE WHEN user_reaction = 'negative' THEN 1 ELSE 0 END) as negative_reactions
176
+ FROM prompt_feedback
177
+ WHERE project_dir = ?
178
+ GROUP BY template_id
179
+ HAVING COUNT(*) >= ?
180
+ SQL
181
+ [project_dir, min_uses]
182
+ )
183
+
184
+ rows.filter_map do |row|
185
+ total = row["total_uses"]
186
+ success_count = row["success_count"]
187
+ success_rate = (total > 0) ? (success_count.to_f / total * 100).round(1) : 0
188
+
189
+ next if success_rate > max_success_rate
190
+
191
+ {
192
+ template_id: row["template_id"],
193
+ total_uses: total,
194
+ success_rate: success_rate,
195
+ success_count: success_count,
196
+ failure_count: row["failure_count"],
197
+ avg_iterations: row["avg_iterations"]&.round(1),
198
+ positive_reactions: row["positive_reactions"],
199
+ negative_reactions: row["negative_reactions"]
200
+ }
201
+ end.sort_by { |s| s[:success_rate] }
202
+ end
203
+
204
+ # Clear all feedback
205
+ #
206
+ # @return [Hash] Result with count
207
+ def clear
208
+ count = query_value(
209
+ "SELECT COUNT(*) FROM prompt_feedback WHERE project_dir = ?",
210
+ [project_dir]
211
+ ) || 0
212
+
213
+ execute("DELETE FROM prompt_feedback WHERE project_dir = ?", [project_dir])
214
+
215
+ Aidp.log_debug("prompt_feedback_repository", "cleared", count: count)
216
+ {success: true, count: count}
217
+ end
218
+
219
+ # Check if any feedback exists
220
+ #
221
+ # @return [Boolean]
222
+ def any?
223
+ count = query_value(
224
+ "SELECT COUNT(*) FROM prompt_feedback WHERE project_dir = ?",
225
+ [project_dir]
226
+ ) || 0
227
+ count.positive?
228
+ end
229
+
230
+ private
231
+
232
+ def deserialize_feedback(row)
233
+ return nil unless row
234
+
235
+ {
236
+ id: row["id"],
237
+ template_id: row["template_id"],
238
+ outcome: row["outcome"],
239
+ iterations: row["iterations"],
240
+ user_reaction: row["user_reaction"],
241
+ suggestions: deserialize_json(row["suggestions"]),
242
+ context: deserialize_json(row["context"]) || {},
243
+ aidp_version: row["aidp_version"],
244
+ created_at: row["created_at"]
245
+ }
246
+ end
247
+
248
+ def empty_summary(template_id)
249
+ {
250
+ template_id: template_id,
251
+ total_uses: 0,
252
+ success_rate: 0,
253
+ success_count: 0,
254
+ failure_count: 0,
255
+ avg_iterations: nil,
256
+ positive_reactions: 0,
257
+ negative_reactions: 0,
258
+ common_suggestions: [],
259
+ first_use: nil,
260
+ last_use: nil
261
+ }
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end