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
@@ -1,292 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "fileutils"
5
-
6
- module Aidp
7
- module Analyze
8
- class JsonFileStorage
9
- def initialize(project_dir = Dir.pwd, storage_dir = ".aidp/json")
10
- @project_dir = project_dir
11
- @storage_dir = File.join(project_dir, storage_dir)
12
- ensure_storage_directory
13
- end
14
-
15
- # Store data in a JSON file
16
- def store_data(filename, data)
17
- file_path = file_path(filename)
18
-
19
- # Ensure directory exists
20
- FileUtils.mkdir_p(File.dirname(file_path))
21
-
22
- # Write data as pretty JSON
23
- File.write(file_path, JSON.pretty_generate(data))
24
-
25
- {
26
- filename: filename,
27
- file_path: file_path,
28
- stored_at: Time.now,
29
- success: true
30
- }
31
- end
32
-
33
- # Retrieve data from a JSON file
34
- def data(filename)
35
- file_path = file_path(filename)
36
-
37
- return nil unless File.exist?(file_path)
38
-
39
- begin
40
- JSON.parse(File.read(file_path))
41
- rescue JSON::ParserError => e
42
- raise "Invalid JSON in file #{filename}: #{e.message}"
43
- end
44
- end
45
-
46
- # Check if a JSON file exists
47
- def data_exists?(filename)
48
- File.exist?(file_path(filename))
49
- end
50
-
51
- # Delete a JSON file
52
- def delete_data(filename)
53
- file_path = file_path(filename)
54
-
55
- if File.exist?(file_path)
56
- File.delete(file_path)
57
- {
58
- filename: filename,
59
- deleted: true,
60
- deleted_at: Time.now
61
- }
62
- else
63
- {
64
- filename: filename,
65
- deleted: false,
66
- message: "File does not exist"
67
- }
68
- end
69
- end
70
-
71
- # List all JSON files in the storage directory
72
- def list_files
73
- return [] unless Dir.exist?(@storage_dir)
74
-
75
- Dir.glob(File.join(@storage_dir, "**", "*.json")).map do |file_path|
76
- relative_path = file_path.sub("#{@storage_dir}/", "")
77
- {
78
- filename: relative_path,
79
- file_path: file_path,
80
- size: File.size(file_path),
81
- modified_at: File.mtime(file_path)
82
- }
83
- end
84
- end
85
-
86
- # Store project configuration
87
- def store_project_config(config_data)
88
- store_data("project_config.json", config_data)
89
- end
90
-
91
- # Get project configuration
92
- def project_config
93
- data("project_config.json")
94
- end
95
-
96
- # Store runtime status
97
- def store_runtime_status(status_data)
98
- store_data("runtime_status.json", status_data)
99
- end
100
-
101
- # Get runtime status
102
- def runtime_status
103
- data("runtime_status.json")
104
- end
105
-
106
- # Store simple metrics
107
- def store_simple_metrics(metrics_data)
108
- store_data("simple_metrics.json", metrics_data)
109
- end
110
-
111
- # Get simple metrics
112
- def simple_metrics
113
- data("simple_metrics.json")
114
- end
115
-
116
- # Store analysis session data
117
- def store_analysis_session(session_id, session_data)
118
- store_data("sessions/#{session_id}.json", session_data)
119
- end
120
-
121
- # Get analysis session data
122
- def analysis_session(session_id)
123
- data("sessions/#{session_id}.json")
124
- end
125
-
126
- # List analysis sessions
127
- def list_analysis_sessions
128
- sessions_dir = File.join(@storage_dir, "sessions")
129
- return [] unless Dir.exist?(sessions_dir)
130
-
131
- Dir.glob(File.join(sessions_dir, "*.json")).map do |file_path|
132
- session_id = File.basename(file_path, ".json")
133
- {
134
- session_id: session_id,
135
- file_path: file_path,
136
- size: File.size(file_path),
137
- modified_at: File.mtime(file_path)
138
- }
139
- end
140
- end
141
-
142
- # Store user preferences
143
- def store_user_preferences(preferences_data)
144
- store_data("user_preferences.json", preferences_data)
145
- end
146
-
147
- # Get user preferences
148
- def user_preferences
149
- data("user_preferences.json")
150
- end
151
-
152
- # Store cache data
153
- def store_cache(cache_key, cache_data, ttl_seconds = nil)
154
- cache_data_with_ttl = {
155
- data: cache_data,
156
- cached_at: Time.now.iso8601,
157
- ttl_seconds: ttl_seconds
158
- }
159
-
160
- store_data("cache/#{cache_key}.json", cache_data_with_ttl)
161
- end
162
-
163
- # Get cache data (respects TTL)
164
- def cache(cache_key)
165
- cache_file_data = data("cache/#{cache_key}.json")
166
- return nil unless cache_file_data
167
-
168
- # Check TTL if specified
169
- if cache_file_data["ttl_seconds"]
170
- cached_at = Time.parse(cache_file_data["cached_at"])
171
- if Time.now - cached_at > cache_file_data["ttl_seconds"]
172
- # Cache expired, delete it
173
- delete_data("cache/#{cache_key}.json")
174
- return nil
175
- end
176
- end
177
-
178
- cache_file_data["data"]
179
- end
180
-
181
- # Clear expired cache entries
182
- def clear_expired_cache
183
- cache_dir = File.join(@storage_dir, "cache")
184
- return 0 unless Dir.exist?(cache_dir)
185
-
186
- cleared_count = 0
187
- Dir.glob(File.join(cache_dir, "*.json")).each do |file_path|
188
- cache_data = JSON.parse(File.read(file_path))
189
- if cache_data["ttl_seconds"] && cache_data["cached_at"]
190
- cached_at = Time.parse(cache_data["cached_at"])
191
- if Time.now - cached_at > cache_data["ttl_seconds"]
192
- File.delete(file_path)
193
- cleared_count += 1
194
- end
195
- end
196
- rescue JSON::ParserError
197
- # Invalid JSON, delete the file
198
- File.delete(file_path)
199
- cleared_count += 1
200
- end
201
-
202
- cleared_count
203
- end
204
-
205
- # Get storage statistics
206
- def storage_statistics
207
- files = list_files
208
-
209
- {
210
- total_files: files.length,
211
- total_size: files.sum { |f| f[:size] },
212
- storage_directory: @storage_dir,
213
- oldest_file: files.min_by { |f| f[:modified_at] }&.dig(:modified_at),
214
- newest_file: files.max_by { |f| f[:modified_at] }&.dig(:modified_at),
215
- file_types: files.group_by { |f| File.extname(f[:filename]) }.transform_values(&:count)
216
- }
217
- end
218
-
219
- # Export all data to a single JSON file
220
- def export_all_data(export_filename = "aidp_data_export.json")
221
- export_data = {
222
- "exported_at" => Time.now.iso8601,
223
- "storage_directory" => @storage_dir,
224
- "files" => {}
225
- }
226
-
227
- files = list_files
228
- files.each do |file_info|
229
- data = data(file_info[:filename])
230
- export_data["files"][file_info[:filename]] = {
231
- "data" => data,
232
- "metadata" => {
233
- "size" => file_info[:size],
234
- "modified_at" => file_info[:modified_at]
235
- }
236
- }
237
- end
238
-
239
- export_path = File.join(@storage_dir, export_filename)
240
- File.write(export_path, JSON.pretty_generate(export_data))
241
-
242
- {
243
- export_filename: export_filename,
244
- export_path: export_path,
245
- files_exported: files.length,
246
- exported_at: Time.now
247
- }
248
- end
249
-
250
- # Import data from an exported JSON file
251
- def import_data(import_filename)
252
- import_path = file_path(import_filename)
253
-
254
- unless File.exist?(import_path)
255
- raise "Import file does not exist: #{import_filename}"
256
- end
257
-
258
- begin
259
- import_data = JSON.parse(File.read(import_path))
260
- rescue JSON::ParserError => e
261
- raise "Invalid JSON in import file: #{e.message}"
262
- end
263
-
264
- unless import_data["files"]
265
- raise "Invalid import file format: missing 'files' key"
266
- end
267
-
268
- imported_count = 0
269
- import_data["files"].each do |filename, file_data|
270
- store_data(filename, file_data["data"])
271
- imported_count += 1
272
- end
273
-
274
- {
275
- imported_count: imported_count,
276
- imported_at: Time.now,
277
- success: true
278
- }
279
- end
280
-
281
- private
282
-
283
- def file_path(filename)
284
- File.join(@storage_dir, filename)
285
- end
286
-
287
- def ensure_storage_directory
288
- FileUtils.mkdir_p(@storage_dir)
289
- end
290
- end
291
- end
292
- end
@@ -1,411 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "yaml"
4
- require "fileutils"
5
-
6
- module Aidp
7
- module Execute
8
- # Manages a backlog of future work items discovered during work loops
9
- # Captures style violations, technical debt, and refactoring opportunities
10
- # that are not directly related to the current feature being implemented
11
- class FutureWorkBacklog
12
- attr_reader :project_dir, :entries, :current_context
13
-
14
- # Entry types
15
- ENTRY_TYPES = {
16
- style_violation: "Style Violation",
17
- refactor_opportunity: "Refactor Opportunity",
18
- technical_debt: "Technical Debt",
19
- todo: "TODO",
20
- performance: "Performance Issue",
21
- security: "Security Concern",
22
- documentation: "Documentation Needed"
23
- }.freeze
24
-
25
- # Priority levels
26
- PRIORITIES = {
27
- low: 1,
28
- medium: 2,
29
- high: 3,
30
- critical: 4
31
- }.freeze
32
-
33
- def initialize(project_dir, options = {})
34
- @project_dir = project_dir
35
- @backlog_dir = File.join(project_dir, ".aidp")
36
- @backlog_file = options[:backlog_file] || File.join(@backlog_dir, "future_work.yml")
37
- @markdown_file = File.join(@backlog_dir, "future_work.md")
38
- @entries = []
39
- @current_context = {}
40
- @options = options
41
-
42
- ensure_backlog_directory
43
- load_existing_backlog
44
- end
45
-
46
- # Add a new future work entry
47
- # @param entry_hash [Hash] Entry details
48
- # @option entry_hash [Symbol] :type Entry type (see ENTRY_TYPES)
49
- # @option entry_hash [String] :file File path
50
- # @option entry_hash [Integer,Range,String] :lines Line number(s)
51
- # @option entry_hash [String] :reason Description of the issue
52
- # @option entry_hash [String] :recommendation Recommended fix
53
- # @option entry_hash [Symbol] :priority Priority level (see PRIORITIES)
54
- # @option entry_hash [Hash] :metadata Additional metadata
55
- def add_entry(entry_hash)
56
- entry = normalize_entry(entry_hash)
57
-
58
- # Avoid duplicates
59
- return if duplicate?(entry)
60
-
61
- entry[:id] = generate_entry_id
62
- entry[:created_at] = Time.now.utc.iso8601
63
- entry[:context] = @current_context.dup
64
-
65
- @entries << entry
66
- entry
67
- end
68
-
69
- # Set context for subsequent entries (e.g., current work loop, step)
70
- def set_context(context_hash)
71
- @current_context.merge!(context_hash)
72
- end
73
-
74
- # Clear context
75
- def clear_context
76
- @current_context.clear
77
- end
78
-
79
- # Save backlog to disk (both YAML and Markdown)
80
- def save
81
- save_yaml
82
- save_markdown
83
- end
84
-
85
- # Get entries filtered by criteria
86
- def filter(criteria = {})
87
- filtered = @entries
88
-
89
- filtered = filtered.select { |e| e[:type] == criteria[:type] } if criteria[:type]
90
- filtered = filtered.select { |e| e[:file] == criteria[:file] } if criteria[:file]
91
- filtered = filtered.select { |e| e[:priority] == criteria[:priority] } if criteria[:priority]
92
- filtered = filtered.select { |e| e[:context][:work_loop] == criteria[:work_loop] } if criteria[:work_loop]
93
-
94
- filtered
95
- end
96
-
97
- # Get entries grouped by type
98
- def by_type
99
- @entries.group_by { |e| e[:type] }
100
- end
101
-
102
- # Get entries grouped by file
103
- def by_file
104
- @entries.group_by { |e| e[:file] }
105
- end
106
-
107
- # Get entries grouped by priority
108
- def by_priority
109
- @entries.group_by { |e| e[:priority] }.sort_by { |priority, _| -PRIORITIES[priority] }.to_h
110
- end
111
-
112
- # Get summary statistics
113
- def summary
114
- {
115
- total: @entries.size,
116
- by_type: by_type.transform_values(&:size),
117
- by_priority: by_priority.transform_values(&:size),
118
- files_affected: @entries.map { |e| e[:file] }.uniq.size
119
- }
120
- end
121
-
122
- # Mark entry as resolved
123
- def resolve_entry(entry_id, resolution_note = nil)
124
- entry = @entries.find { |e| e[:id] == entry_id }
125
- return unless entry
126
-
127
- entry[:resolved] = true
128
- entry[:resolved_at] = Time.now.utc.iso8601
129
- entry[:resolution_note] = resolution_note if resolution_note
130
- end
131
-
132
- # Remove resolved entries
133
- def clear_resolved
134
- @entries.reject! { |e| e[:resolved] }
135
- end
136
-
137
- # Convert entry to work loop PROMPT.md content
138
- def entry_to_prompt(entry_id)
139
- entry = @entries.find { |e| e[:id] == entry_id }
140
- return unless entry
141
-
142
- <<~PROMPT
143
- # Work Loop: #{entry_type_display(entry[:type])}
144
-
145
- ## Task Description
146
-
147
- **File**: #{entry[:file]}
148
- **Lines**: #{entry[:lines]}
149
- **Priority**: #{entry[:priority].to_s.upcase}
150
-
151
- ## Issue
152
-
153
- #{entry[:reason]}
154
-
155
- ## Recommended Fix
156
-
157
- #{entry[:recommendation]}
158
-
159
- ## Acceptance Criteria
160
-
161
- - [ ] #{entry[:reason]} is resolved
162
- - [ ] Code follows LLM_STYLE_GUIDE
163
- - [ ] Tests pass
164
- - [ ] No new style violations introduced
165
-
166
- ## Original Context
167
-
168
- - Work Loop: #{entry[:context][:work_loop] || "N/A"}
169
- - Step: #{entry[:context][:step] || "N/A"}
170
- - Created: #{entry[:created_at]}
171
-
172
- ## Completion
173
-
174
- Mark this complete by adding: STATUS: COMPLETE
175
- PROMPT
176
- end
177
-
178
- # Display summary of backlog
179
- def display_summary(output = $stdout)
180
- return if @entries.empty?
181
-
182
- output.puts "\n" + "=" * 80
183
- output.puts "📝 Future Work Backlog Summary"
184
- output.puts "=" * 80
185
-
186
- sum = summary
187
- output.puts "\nTotal Items: #{sum[:total]}"
188
- output.puts "Files Affected: #{sum[:files_affected]}"
189
-
190
- if sum[:by_type].any?
191
- output.puts "\nBy Type:"
192
- sum[:by_type].each do |type, count|
193
- output.puts " #{entry_type_display(type)}: #{count}"
194
- end
195
- end
196
-
197
- if sum[:by_priority].any?
198
- output.puts "\nBy Priority:"
199
- sum[:by_priority].each do |priority, count|
200
- output.puts " #{priority.to_s.upcase}: #{count}"
201
- end
202
- end
203
-
204
- output.puts "\n" + "-" * 80
205
- output.puts "Review backlog: .aidp/future_work.md"
206
- output.puts "Convert to work loop: aidp backlog convert <entry-id>"
207
- output.puts "=" * 80 + "\n"
208
- end
209
-
210
- private
211
-
212
- # Ensure backlog directory exists
213
- def ensure_backlog_directory
214
- FileUtils.mkdir_p(@backlog_dir) unless Dir.exist?(@backlog_dir)
215
- end
216
-
217
- # Load existing backlog from disk
218
- def load_existing_backlog
219
- return unless File.exist?(@backlog_file)
220
-
221
- data = YAML.safe_load_file(@backlog_file, permitted_classes: [Date, Time, Symbol], aliases: true)
222
- @entries = data["entries"] || [] if data.is_a?(Hash)
223
- @entries = symbolize_keys_deep(@entries)
224
- rescue => e
225
- warn "Warning: Could not load existing backlog: #{e.message}"
226
- @entries = []
227
- end
228
-
229
- # Save backlog to YAML
230
- def save_yaml
231
- data = {
232
- "version" => "1.0",
233
- "generated_at" => Time.now.utc.iso8601,
234
- "project" => @project_dir,
235
- "entries" => @entries.map { |e| stringify_keys_deep(e) }
236
- }
237
-
238
- File.write(@backlog_file, YAML.dump(data))
239
- end
240
-
241
- # Save backlog to Markdown (human-readable)
242
- def save_markdown
243
- content = generate_markdown
244
-
245
- File.write(@markdown_file, content)
246
- end
247
-
248
- # Generate Markdown representation
249
- def generate_markdown
250
- lines = []
251
- lines << "# Future Work Backlog"
252
- lines << ""
253
- lines << "Generated: #{Time.now.utc.iso8601}"
254
- lines << "Project: #{@project_dir}"
255
- lines << ""
256
-
257
- sum = summary
258
- lines << "## Summary"
259
- lines << ""
260
- lines << "- **Total Items**: #{sum[:total]}"
261
- lines << "- **Files Affected**: #{sum[:files_affected]}"
262
- lines << ""
263
-
264
- # Group by priority
265
- by_priority.each do |priority, entries|
266
- lines << "## #{priority.to_s.upcase} Priority (#{entries.size})"
267
- lines << ""
268
-
269
- # Group by type within priority
270
- entries.group_by { |e| e[:type] }.each do |type, type_entries|
271
- lines << "### #{entry_type_display(type)}"
272
- lines << ""
273
-
274
- type_entries.each do |entry|
275
- lines << format_entry_markdown(entry)
276
- lines << ""
277
- end
278
- end
279
- end
280
-
281
- lines << "---"
282
- lines << ""
283
- lines << "## Usage"
284
- lines << ""
285
- lines << "Convert an entry to a work loop:"
286
- lines << "```bash"
287
- lines << "aidp backlog convert <entry-id>"
288
- lines << "```"
289
- lines << ""
290
-
291
- lines.join("\n")
292
- end
293
-
294
- # Format single entry as Markdown
295
- def format_entry_markdown(entry)
296
- lines = []
297
- lines << "#### #{entry[:id]} - #{entry[:file]}"
298
- lines << ""
299
- lines << "**Lines**: #{entry[:lines]}"
300
- lines << ""
301
- lines << "**Issue**: #{entry[:reason]}"
302
- lines << ""
303
- lines << "**Recommendation**: #{entry[:recommendation]}"
304
- lines << ""
305
-
306
- if entry[:context].any?
307
- lines << "**Context**: Work Loop: #{entry[:context][:work_loop] || "N/A"}, Step: #{entry[:context][:step] || "N/A"}"
308
- lines << ""
309
- end
310
-
311
- lines << "*Created: #{entry[:created_at]}*"
312
-
313
- lines.join("\n")
314
- end
315
-
316
- # Normalize entry hash
317
- def normalize_entry(entry_hash)
318
- {
319
- type: entry_hash[:type] || :technical_debt,
320
- file: normalize_path(entry_hash[:file]),
321
- lines: normalize_lines(entry_hash[:lines]),
322
- reason: entry_hash[:reason] || "No reason provided",
323
- recommendation: entry_hash[:recommendation] || "No recommendation provided",
324
- priority: entry_hash[:priority] || :medium,
325
- metadata: entry_hash[:metadata] || {},
326
- resolved: false
327
- }
328
- end
329
-
330
- # Check if entry is duplicate
331
- def duplicate?(entry)
332
- @entries.any? do |existing|
333
- existing[:file] == entry[:file] &&
334
- existing[:lines] == entry[:lines] &&
335
- existing[:reason] == entry[:reason] &&
336
- !existing[:resolved]
337
- end
338
- end
339
-
340
- # Generate unique entry ID
341
- def generate_entry_id
342
- timestamp = Time.now.to_i
343
- random = SecureRandom.hex(4)
344
- "fw-#{timestamp}-#{random}"
345
- end
346
-
347
- # Normalize file path (relative to project)
348
- def normalize_path(file_path)
349
- return file_path unless file_path
350
-
351
- path = Pathname.new(file_path)
352
- project = Pathname.new(@project_dir)
353
-
354
- if path.absolute?
355
- path.relative_path_from(project).to_s
356
- else
357
- path.to_s
358
- end
359
- rescue ArgumentError
360
- file_path
361
- end
362
-
363
- # Normalize line numbers
364
- def normalize_lines(lines)
365
- case lines
366
- when Integer
367
- lines.to_s
368
- when Range
369
- "#{lines.begin}-#{lines.end}"
370
- when String
371
- lines
372
- else
373
- "unknown"
374
- end
375
- end
376
-
377
- # Get display name for entry type
378
- def entry_type_display(type)
379
- ENTRY_TYPES[type] || type.to_s.split("_").map(&:capitalize).join(" ")
380
- end
381
-
382
- # Recursively symbolize keys
383
- def symbolize_keys_deep(obj)
384
- case obj
385
- when Hash
386
- obj.each_with_object({}) do |(key, value), result|
387
- result[key.to_sym] = symbolize_keys_deep(value)
388
- end
389
- when Array
390
- obj.map { |item| symbolize_keys_deep(item) }
391
- else
392
- obj
393
- end
394
- end
395
-
396
- # Recursively stringify keys
397
- def stringify_keys_deep(obj)
398
- case obj
399
- when Hash
400
- obj.each_with_object({}) do |(key, value), result|
401
- result[key.to_s] = stringify_keys_deep(value)
402
- end
403
- when Array
404
- obj.map { |item| stringify_keys_deep(item) }
405
- else
406
- obj
407
- end
408
- end
409
- end
410
- end
411
- end