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.
- 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/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 +117 -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
|
@@ -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
|