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,310 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../repository"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Database
|
|
7
|
+
module Repositories
|
|
8
|
+
# Repository for workstreams and workstream_events tables
|
|
9
|
+
# Replaces workstreams/*/state.json and history.jsonl
|
|
10
|
+
class WorkstreamRepository < Repository
|
|
11
|
+
def initialize(project_dir: Dir.pwd)
|
|
12
|
+
super(project_dir: project_dir, table_name: "workstreams")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Initialize a new workstream
|
|
16
|
+
#
|
|
17
|
+
# @param slug [String] Workstream slug
|
|
18
|
+
# @param task [String, nil] Task description
|
|
19
|
+
# @return [Hash] Created workstream state
|
|
20
|
+
def init(slug:, task: nil)
|
|
21
|
+
now = current_timestamp
|
|
22
|
+
|
|
23
|
+
execute(
|
|
24
|
+
insert_sql([
|
|
25
|
+
:project_dir, :slug, :status, :iteration, :metadata,
|
|
26
|
+
:created_at, :updated_at
|
|
27
|
+
]),
|
|
28
|
+
[
|
|
29
|
+
project_dir,
|
|
30
|
+
slug,
|
|
31
|
+
"active",
|
|
32
|
+
0,
|
|
33
|
+
serialize_json({task: task, started_at: now}),
|
|
34
|
+
now,
|
|
35
|
+
now
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
append_event(slug: slug, type: "created", data: {task: task})
|
|
40
|
+
|
|
41
|
+
Aidp.log_debug("workstream_repository", "initialized", slug: slug)
|
|
42
|
+
|
|
43
|
+
read(slug: slug)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Read workstream state
|
|
47
|
+
#
|
|
48
|
+
# @param slug [String] Workstream slug
|
|
49
|
+
# @return [Hash, nil] Workstream state or nil
|
|
50
|
+
def read(slug:)
|
|
51
|
+
row = query_one(
|
|
52
|
+
"SELECT * FROM workstreams WHERE project_dir = ? AND slug = ?",
|
|
53
|
+
[project_dir, slug]
|
|
54
|
+
)
|
|
55
|
+
return nil unless row
|
|
56
|
+
|
|
57
|
+
deserialize_workstream(row)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Update workstream attributes
|
|
61
|
+
#
|
|
62
|
+
# @param slug [String] Workstream slug
|
|
63
|
+
# @param attrs [Hash] Attributes to update
|
|
64
|
+
# @return [Hash] Updated workstream state
|
|
65
|
+
def update(slug:, **attrs)
|
|
66
|
+
existing = read(slug: slug)
|
|
67
|
+
existing ||= init(slug: slug)
|
|
68
|
+
|
|
69
|
+
now = current_timestamp
|
|
70
|
+
metadata = existing[:metadata] || {}
|
|
71
|
+
metadata = metadata.merge(attrs.except(:status, :iteration, :branch, :worktree_path))
|
|
72
|
+
|
|
73
|
+
execute(
|
|
74
|
+
<<~SQL,
|
|
75
|
+
UPDATE workstreams SET
|
|
76
|
+
status = ?,
|
|
77
|
+
iteration = ?,
|
|
78
|
+
branch = ?,
|
|
79
|
+
worktree_path = ?,
|
|
80
|
+
metadata = ?,
|
|
81
|
+
updated_at = ?
|
|
82
|
+
WHERE project_dir = ? AND slug = ?
|
|
83
|
+
SQL
|
|
84
|
+
[
|
|
85
|
+
attrs[:status] || existing[:status],
|
|
86
|
+
attrs[:iteration] || existing[:iteration],
|
|
87
|
+
attrs[:branch] || existing[:branch],
|
|
88
|
+
attrs[:worktree_path] || existing[:worktree_path],
|
|
89
|
+
serialize_json(metadata),
|
|
90
|
+
now,
|
|
91
|
+
project_dir,
|
|
92
|
+
slug
|
|
93
|
+
]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
Aidp.log_debug("workstream_repository", "updated", slug: slug)
|
|
97
|
+
|
|
98
|
+
read(slug: slug)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Increment iteration counter
|
|
102
|
+
#
|
|
103
|
+
# @param slug [String] Workstream slug
|
|
104
|
+
# @return [Hash] Updated workstream state
|
|
105
|
+
def increment_iteration(slug:)
|
|
106
|
+
existing = read(slug: slug)
|
|
107
|
+
existing ||= init(slug: slug)
|
|
108
|
+
|
|
109
|
+
new_iteration = (existing[:iteration] || 0) + 1
|
|
110
|
+
new_status = (existing[:status] == "paused") ? "active" : existing[:status]
|
|
111
|
+
|
|
112
|
+
state = update(slug: slug, iteration: new_iteration, status: new_status)
|
|
113
|
+
|
|
114
|
+
append_event(slug: slug, type: "iteration", data: {count: new_iteration})
|
|
115
|
+
|
|
116
|
+
state
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Append event to workstream history
|
|
120
|
+
#
|
|
121
|
+
# @param slug [String] Workstream slug
|
|
122
|
+
# @param type [String] Event type
|
|
123
|
+
# @param data [Hash] Event data
|
|
124
|
+
def append_event(slug:, type:, data: {})
|
|
125
|
+
now = current_timestamp
|
|
126
|
+
|
|
127
|
+
execute(
|
|
128
|
+
<<~SQL,
|
|
129
|
+
INSERT INTO workstream_events
|
|
130
|
+
(project_dir, workstream_slug, event_type, event_data, timestamp)
|
|
131
|
+
VALUES (?, ?, ?, ?, ?)
|
|
132
|
+
SQL
|
|
133
|
+
[project_dir, slug, type, serialize_json(data), now]
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
Aidp.log_debug("workstream_repository", "event_appended",
|
|
137
|
+
slug: slug, type: type)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Get recent events for workstream
|
|
141
|
+
#
|
|
142
|
+
# @param slug [String] Workstream slug
|
|
143
|
+
# @param limit [Integer] Maximum events to return
|
|
144
|
+
# @return [Array<Hash>] Recent events
|
|
145
|
+
def recent_events(slug:, limit: 5)
|
|
146
|
+
rows = query(
|
|
147
|
+
<<~SQL,
|
|
148
|
+
SELECT * FROM workstream_events
|
|
149
|
+
WHERE project_dir = ? AND workstream_slug = ?
|
|
150
|
+
ORDER BY timestamp DESC
|
|
151
|
+
LIMIT ?
|
|
152
|
+
SQL
|
|
153
|
+
[project_dir, slug, limit]
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
rows.map { |row| deserialize_event(row) }.reverse
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Pause workstream
|
|
160
|
+
#
|
|
161
|
+
# @param slug [String] Workstream slug
|
|
162
|
+
# @return [Hash] Result with status or error
|
|
163
|
+
def pause(slug:)
|
|
164
|
+
state = read(slug: slug)
|
|
165
|
+
return {error: "Workstream not found"} unless state
|
|
166
|
+
return {error: "Already paused"} if state[:status] == "paused"
|
|
167
|
+
|
|
168
|
+
now = current_timestamp
|
|
169
|
+
update(slug: slug, status: "paused", paused_at: now)
|
|
170
|
+
append_event(slug: slug, type: "paused", data: {})
|
|
171
|
+
|
|
172
|
+
{status: "paused"}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Resume workstream
|
|
176
|
+
#
|
|
177
|
+
# @param slug [String] Workstream slug
|
|
178
|
+
# @return [Hash] Result with status or error
|
|
179
|
+
def resume(slug:)
|
|
180
|
+
state = read(slug: slug)
|
|
181
|
+
return {error: "Workstream not found"} unless state
|
|
182
|
+
return {error: "Not paused"} unless state[:status] == "paused"
|
|
183
|
+
|
|
184
|
+
now = current_timestamp
|
|
185
|
+
update(slug: slug, status: "active", resumed_at: now)
|
|
186
|
+
append_event(slug: slug, type: "resumed", data: {})
|
|
187
|
+
|
|
188
|
+
{status: "active"}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Complete workstream
|
|
192
|
+
#
|
|
193
|
+
# @param slug [String] Workstream slug
|
|
194
|
+
# @return [Hash] Result with status or error
|
|
195
|
+
def complete(slug:)
|
|
196
|
+
state = read(slug: slug)
|
|
197
|
+
return {error: "Workstream not found"} unless state
|
|
198
|
+
return {error: "Already completed"} if state[:status] == "completed"
|
|
199
|
+
|
|
200
|
+
now = current_timestamp
|
|
201
|
+
update(slug: slug, status: "completed", completed_at: now)
|
|
202
|
+
append_event(slug: slug, type: "completed", data: {iterations: state[:iteration]})
|
|
203
|
+
|
|
204
|
+
{status: "completed"}
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Mark workstream as removed
|
|
208
|
+
#
|
|
209
|
+
# @param slug [String] Workstream slug
|
|
210
|
+
def mark_removed(slug:)
|
|
211
|
+
state = read(slug: slug)
|
|
212
|
+
|
|
213
|
+
# Auto-complete if active
|
|
214
|
+
complete(slug: slug) if state && state[:status] == "active"
|
|
215
|
+
|
|
216
|
+
update(slug: slug, status: "removed")
|
|
217
|
+
append_event(slug: slug, type: "removed", data: {})
|
|
218
|
+
|
|
219
|
+
Aidp.log_debug("workstream_repository", "removed", slug: slug)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Check if workstream is stalled
|
|
223
|
+
#
|
|
224
|
+
# @param slug [String] Workstream slug
|
|
225
|
+
# @param threshold_seconds [Integer] Stall threshold in seconds
|
|
226
|
+
# @return [Boolean]
|
|
227
|
+
def stalled?(slug:, threshold_seconds: 3600)
|
|
228
|
+
state = read(slug: slug)
|
|
229
|
+
return false unless state && state[:updated_at]
|
|
230
|
+
return false if state[:status] != "active"
|
|
231
|
+
|
|
232
|
+
updated = Time.parse(state[:updated_at])
|
|
233
|
+
(Time.now.utc - updated).to_i > threshold_seconds
|
|
234
|
+
rescue ArgumentError
|
|
235
|
+
false
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Get elapsed time for workstream
|
|
239
|
+
#
|
|
240
|
+
# @param slug [String] Workstream slug
|
|
241
|
+
# @return [Integer] Elapsed seconds
|
|
242
|
+
def elapsed_seconds(slug:)
|
|
243
|
+
state = read(slug: slug)
|
|
244
|
+
return 0 unless state
|
|
245
|
+
|
|
246
|
+
metadata = state[:metadata] || {}
|
|
247
|
+
return 0 unless metadata[:started_at]
|
|
248
|
+
|
|
249
|
+
started = Time.parse(metadata[:started_at])
|
|
250
|
+
(Time.now.utc - started).to_i
|
|
251
|
+
rescue ArgumentError
|
|
252
|
+
0
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# List all workstreams for project
|
|
256
|
+
#
|
|
257
|
+
# @param status [String, nil] Filter by status
|
|
258
|
+
# @return [Array<Hash>] Workstreams
|
|
259
|
+
def list(status: nil)
|
|
260
|
+
rows = if status
|
|
261
|
+
query(
|
|
262
|
+
"SELECT * FROM workstreams WHERE project_dir = ? AND status = ? ORDER BY updated_at DESC",
|
|
263
|
+
[project_dir, status]
|
|
264
|
+
)
|
|
265
|
+
else
|
|
266
|
+
query(
|
|
267
|
+
"SELECT * FROM workstreams WHERE project_dir = ? ORDER BY updated_at DESC",
|
|
268
|
+
[project_dir]
|
|
269
|
+
)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
rows.map { |row| deserialize_workstream(row) }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
private
|
|
276
|
+
|
|
277
|
+
def deserialize_workstream(row)
|
|
278
|
+
metadata = deserialize_json(row["metadata"]) || {}
|
|
279
|
+
|
|
280
|
+
{
|
|
281
|
+
id: row["id"],
|
|
282
|
+
slug: row["slug"],
|
|
283
|
+
status: row["status"],
|
|
284
|
+
iteration: row["iteration"] || 0,
|
|
285
|
+
branch: row["branch"],
|
|
286
|
+
worktree_path: row["worktree_path"],
|
|
287
|
+
metadata: metadata,
|
|
288
|
+
task: metadata[:task],
|
|
289
|
+
started_at: metadata[:started_at],
|
|
290
|
+
paused_at: metadata[:paused_at],
|
|
291
|
+
resumed_at: metadata[:resumed_at],
|
|
292
|
+
completed_at: metadata[:completed_at],
|
|
293
|
+
created_at: row["created_at"],
|
|
294
|
+
updated_at: row["updated_at"]
|
|
295
|
+
}
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def deserialize_event(row)
|
|
299
|
+
{
|
|
300
|
+
id: row["id"],
|
|
301
|
+
workstream_slug: row["workstream_slug"],
|
|
302
|
+
type: row["event_type"],
|
|
303
|
+
data: deserialize_json(row["event_data"]) || {},
|
|
304
|
+
timestamp: row["timestamp"]
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../repository"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Database
|
|
7
|
+
module Repositories
|
|
8
|
+
# Repository for worktrees table
|
|
9
|
+
# Replaces worktrees.json and pr_worktrees.json
|
|
10
|
+
# Handles both standard and PR worktrees
|
|
11
|
+
class WorktreeRepository < Repository
|
|
12
|
+
TYPES = %w[standard pr].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(project_dir: Dir.pwd)
|
|
15
|
+
super(project_dir: project_dir, table_name: "worktrees")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Register a standard worktree
|
|
19
|
+
#
|
|
20
|
+
# @param slug [String] Worktree slug
|
|
21
|
+
# @param path [String] Filesystem path
|
|
22
|
+
# @param branch [String] Branch name
|
|
23
|
+
# @return [Hash] Worktree info
|
|
24
|
+
def register(slug:, path:, branch:)
|
|
25
|
+
now = current_timestamp
|
|
26
|
+
|
|
27
|
+
execute(
|
|
28
|
+
insert_sql([
|
|
29
|
+
:project_dir, :worktree_type, :path, :branch, :slug,
|
|
30
|
+
:status, :created_at, :updated_at
|
|
31
|
+
]),
|
|
32
|
+
[project_dir, "standard", path, branch, slug, "active", now, now]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
Aidp.log_debug("worktree_repository", "registered",
|
|
36
|
+
slug: slug, type: "standard")
|
|
37
|
+
|
|
38
|
+
find_by_slug(slug)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Register a PR worktree
|
|
42
|
+
#
|
|
43
|
+
# @param pr_number [Integer] PR number
|
|
44
|
+
# @param path [String] Filesystem path
|
|
45
|
+
# @param base_branch [String] Base branch
|
|
46
|
+
# @param head_branch [String] Head branch
|
|
47
|
+
# @param metadata [Hash] Additional metadata
|
|
48
|
+
# @return [Hash] Worktree info
|
|
49
|
+
def register_pr(pr_number:, path:, base_branch:, head_branch:, metadata: {})
|
|
50
|
+
now = current_timestamp
|
|
51
|
+
slug = "pr-#{pr_number}-#{Time.now.to_i}"
|
|
52
|
+
|
|
53
|
+
full_metadata = metadata.merge(
|
|
54
|
+
base_branch: base_branch,
|
|
55
|
+
head_branch: head_branch,
|
|
56
|
+
created_at: Time.now.to_i
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
execute(
|
|
60
|
+
insert_sql([
|
|
61
|
+
:project_dir, :worktree_type, :path, :branch, :slug, :pr_number,
|
|
62
|
+
:status, :metadata, :created_at, :updated_at
|
|
63
|
+
]),
|
|
64
|
+
[
|
|
65
|
+
project_dir, "pr", path, head_branch, slug, pr_number,
|
|
66
|
+
"active", serialize_json(full_metadata), now, now
|
|
67
|
+
]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
Aidp.log_debug("worktree_repository", "registered_pr",
|
|
71
|
+
pr_number: pr_number, slug: slug)
|
|
72
|
+
|
|
73
|
+
find_by_pr(pr_number)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Find worktree by slug
|
|
77
|
+
#
|
|
78
|
+
# @param slug [String] Worktree slug
|
|
79
|
+
# @return [Hash, nil] Worktree info or nil
|
|
80
|
+
def find_by_slug(slug)
|
|
81
|
+
row = query_one(
|
|
82
|
+
"SELECT * FROM worktrees WHERE project_dir = ? AND slug = ?",
|
|
83
|
+
[project_dir, slug]
|
|
84
|
+
)
|
|
85
|
+
deserialize_worktree(row)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Find worktree by PR number
|
|
89
|
+
#
|
|
90
|
+
# @param pr_number [Integer] PR number
|
|
91
|
+
# @return [Hash, nil] Worktree info or nil
|
|
92
|
+
def find_by_pr(pr_number)
|
|
93
|
+
row = query_one(
|
|
94
|
+
"SELECT * FROM worktrees WHERE project_dir = ? AND pr_number = ?",
|
|
95
|
+
[project_dir, pr_number]
|
|
96
|
+
)
|
|
97
|
+
deserialize_worktree(row)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Find worktree by branch
|
|
101
|
+
#
|
|
102
|
+
# @param branch [String] Branch name
|
|
103
|
+
# @return [Hash, nil] Worktree info or nil
|
|
104
|
+
def find_by_branch(branch)
|
|
105
|
+
row = query_one(
|
|
106
|
+
"SELECT * FROM worktrees WHERE project_dir = ? AND branch = ?",
|
|
107
|
+
[project_dir, branch]
|
|
108
|
+
)
|
|
109
|
+
deserialize_worktree(row)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Check if worktree exists
|
|
113
|
+
#
|
|
114
|
+
# @param slug [String] Worktree slug
|
|
115
|
+
# @return [Boolean]
|
|
116
|
+
def exists?(slug)
|
|
117
|
+
!find_by_slug(slug).nil?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# List all worktrees
|
|
121
|
+
#
|
|
122
|
+
# @param type [String, nil] Filter by type (standard, pr)
|
|
123
|
+
# @return [Array<Hash>] Worktrees
|
|
124
|
+
def list(type: nil)
|
|
125
|
+
rows = if type
|
|
126
|
+
query(
|
|
127
|
+
"SELECT * FROM worktrees WHERE project_dir = ? AND worktree_type = ? ORDER BY created_at DESC",
|
|
128
|
+
[project_dir, type]
|
|
129
|
+
)
|
|
130
|
+
else
|
|
131
|
+
query(
|
|
132
|
+
"SELECT * FROM worktrees WHERE project_dir = ? ORDER BY created_at DESC",
|
|
133
|
+
[project_dir]
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
rows.map { |row| deserialize_worktree(row) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# List standard worktrees (for Worktree module)
|
|
141
|
+
#
|
|
142
|
+
# @return [Array<Hash>]
|
|
143
|
+
def list_standard
|
|
144
|
+
list(type: "standard")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# List PR worktrees (for PRWorktreeManager)
|
|
148
|
+
#
|
|
149
|
+
# @return [Hash] Map of pr_number => worktree info
|
|
150
|
+
def list_pr_worktrees
|
|
151
|
+
rows = query(
|
|
152
|
+
"SELECT * FROM worktrees WHERE project_dir = ? AND worktree_type = 'pr'",
|
|
153
|
+
[project_dir]
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
rows.each_with_object({}) do |row, hash|
|
|
157
|
+
wt = deserialize_worktree(row)
|
|
158
|
+
hash[wt[:pr_number].to_s] = wt if wt[:pr_number]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Unregister a worktree by slug
|
|
163
|
+
#
|
|
164
|
+
# @param slug [String] Worktree slug
|
|
165
|
+
def unregister(slug)
|
|
166
|
+
execute(
|
|
167
|
+
"DELETE FROM worktrees WHERE project_dir = ? AND slug = ?",
|
|
168
|
+
[project_dir, slug]
|
|
169
|
+
)
|
|
170
|
+
Aidp.log_debug("worktree_repository", "unregistered", slug: slug)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Unregister a PR worktree
|
|
174
|
+
#
|
|
175
|
+
# @param pr_number [Integer] PR number
|
|
176
|
+
def unregister_pr(pr_number)
|
|
177
|
+
execute(
|
|
178
|
+
"DELETE FROM worktrees WHERE project_dir = ? AND pr_number = ?",
|
|
179
|
+
[project_dir, pr_number]
|
|
180
|
+
)
|
|
181
|
+
Aidp.log_debug("worktree_repository", "unregistered_pr", pr_number: pr_number)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Update worktree status
|
|
185
|
+
#
|
|
186
|
+
# @param slug [String] Worktree slug
|
|
187
|
+
# @param status [String] New status
|
|
188
|
+
def update_status(slug, status)
|
|
189
|
+
execute(
|
|
190
|
+
"UPDATE worktrees SET status = ?, updated_at = ? WHERE project_dir = ? AND slug = ?",
|
|
191
|
+
[status, current_timestamp, project_dir, slug]
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Cleanup stale PR worktrees
|
|
196
|
+
#
|
|
197
|
+
# @param days_threshold [Integer] Days after which to consider stale
|
|
198
|
+
# @return [Array<Hash>] Removed worktrees
|
|
199
|
+
def cleanup_stale_pr(days_threshold: 30)
|
|
200
|
+
threshold_time = (Time.now - (days_threshold * 24 * 60 * 60)).strftime("%Y-%m-%d %H:%M:%S")
|
|
201
|
+
|
|
202
|
+
stale = query(
|
|
203
|
+
"SELECT * FROM worktrees WHERE project_dir = ? AND worktree_type = 'pr' AND created_at < ?",
|
|
204
|
+
[project_dir, threshold_time]
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
stale.each do |row|
|
|
208
|
+
execute("DELETE FROM worktrees WHERE id = ?", [row["id"]])
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
stale.map { |row| deserialize_worktree(row) }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
def deserialize_worktree(row)
|
|
217
|
+
return nil unless row
|
|
218
|
+
|
|
219
|
+
metadata = deserialize_json(row["metadata"]) || {}
|
|
220
|
+
|
|
221
|
+
{
|
|
222
|
+
id: row["id"],
|
|
223
|
+
slug: row["slug"],
|
|
224
|
+
path: row["path"],
|
|
225
|
+
branch: row["branch"],
|
|
226
|
+
worktree_type: row["worktree_type"],
|
|
227
|
+
pr_number: row["pr_number"],
|
|
228
|
+
status: row["status"],
|
|
229
|
+
metadata: metadata,
|
|
230
|
+
base_branch: metadata[:base_branch],
|
|
231
|
+
head_branch: metadata[:head_branch] || row["branch"],
|
|
232
|
+
created_at: row["created_at"],
|
|
233
|
+
updated_at: row["updated_at"],
|
|
234
|
+
active: row["path"] && Dir.exist?(row["path"])
|
|
235
|
+
}
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Database
|
|
7
|
+
# Base repository class for database access
|
|
8
|
+
# Provides common patterns for CRUD operations
|
|
9
|
+
class Repository
|
|
10
|
+
attr_reader :project_dir, :table_name
|
|
11
|
+
|
|
12
|
+
# @param table_name [String] Database table name
|
|
13
|
+
# @param project_dir [String] Project directory path
|
|
14
|
+
def initialize(table_name:, project_dir: Dir.pwd)
|
|
15
|
+
@project_dir = project_dir
|
|
16
|
+
@table_name = table_name
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
protected
|
|
20
|
+
|
|
21
|
+
# Get database connection
|
|
22
|
+
def db
|
|
23
|
+
Database.connection(project_dir)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Execute a query and return all results
|
|
27
|
+
#
|
|
28
|
+
# @param sql [String] SQL query
|
|
29
|
+
# @param params [Array] Query parameters
|
|
30
|
+
# @return [Array<Hash>] Query results
|
|
31
|
+
def query(sql, params = [])
|
|
32
|
+
db.execute(sql, params)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Execute a query and return first result
|
|
36
|
+
#
|
|
37
|
+
# @param sql [String] SQL query
|
|
38
|
+
# @param params [Array] Query parameters
|
|
39
|
+
# @return [Hash, nil] First result or nil
|
|
40
|
+
def query_one(sql, params = [])
|
|
41
|
+
db.execute(sql, params).first
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Execute a query and return single value
|
|
45
|
+
#
|
|
46
|
+
# @param sql [String] SQL query
|
|
47
|
+
# @param params [Array] Query parameters
|
|
48
|
+
# @return [Object, nil] Single value or nil
|
|
49
|
+
def query_value(sql, params = [])
|
|
50
|
+
db.get_first_value(sql, params)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Execute a statement (INSERT, UPDATE, DELETE)
|
|
54
|
+
#
|
|
55
|
+
# @param sql [String] SQL statement
|
|
56
|
+
# @param params [Array] Statement parameters
|
|
57
|
+
def execute(sql, params = [])
|
|
58
|
+
db.execute(sql, params)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Execute within a transaction
|
|
62
|
+
#
|
|
63
|
+
# @yield Block to execute in transaction
|
|
64
|
+
# @return [Object] Block result
|
|
65
|
+
def transaction(&block)
|
|
66
|
+
db.transaction(&block)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get the last inserted row ID
|
|
70
|
+
#
|
|
71
|
+
# @return [Integer] Last row ID
|
|
72
|
+
def last_insert_row_id
|
|
73
|
+
db.last_insert_row_id
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Serialize a value to JSON for storage
|
|
77
|
+
#
|
|
78
|
+
# @param value [Object] Value to serialize
|
|
79
|
+
# @return [String, nil] JSON string or nil
|
|
80
|
+
def serialize_json(value)
|
|
81
|
+
return nil if value.nil?
|
|
82
|
+
|
|
83
|
+
JSON.generate(value)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Deserialize a JSON string from storage
|
|
87
|
+
#
|
|
88
|
+
# @param json_string [String, nil] JSON string
|
|
89
|
+
# @return [Object, nil] Deserialized value or nil
|
|
90
|
+
def deserialize_json(json_string)
|
|
91
|
+
return nil if json_string.nil? || json_string.empty?
|
|
92
|
+
|
|
93
|
+
JSON.parse(json_string, symbolize_names: true)
|
|
94
|
+
rescue JSON::ParserError => e
|
|
95
|
+
Aidp.log_debug("repository", "json_parse_error",
|
|
96
|
+
table: table_name,
|
|
97
|
+
error: e.message)
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Generate current timestamp in SQLite format
|
|
102
|
+
#
|
|
103
|
+
# @return [String] ISO 8601 timestamp
|
|
104
|
+
def current_timestamp
|
|
105
|
+
Time.now.utc.strftime("%Y-%m-%d %H:%M:%S")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Build a simple INSERT statement
|
|
109
|
+
#
|
|
110
|
+
# @param columns [Array<Symbol>] Column names
|
|
111
|
+
# @return [String] INSERT SQL
|
|
112
|
+
def insert_sql(columns)
|
|
113
|
+
placeholders = columns.map { "?" }.join(", ")
|
|
114
|
+
cols = columns.join(", ")
|
|
115
|
+
"INSERT INTO #{table_name} (#{cols}) VALUES (#{placeholders})"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Build a simple UPDATE statement
|
|
119
|
+
#
|
|
120
|
+
# @param columns [Array<Symbol>] Column names to update
|
|
121
|
+
# @param where_column [Symbol] WHERE clause column
|
|
122
|
+
# @return [String] UPDATE SQL
|
|
123
|
+
def update_sql(columns, where_column: :id)
|
|
124
|
+
set_clause = columns.map { |c| "#{c} = ?" }.join(", ")
|
|
125
|
+
"UPDATE #{table_name} SET #{set_clause} WHERE #{where_column} = ?"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Find a record by ID
|
|
129
|
+
#
|
|
130
|
+
# @param id [Object] Record ID
|
|
131
|
+
# @return [Hash, nil] Record or nil
|
|
132
|
+
def find_by_id(id)
|
|
133
|
+
query_one("SELECT * FROM #{table_name} WHERE id = ?", [id])
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Find all records matching project_dir
|
|
137
|
+
#
|
|
138
|
+
# @return [Array<Hash>] Records
|
|
139
|
+
def find_by_project
|
|
140
|
+
query("SELECT * FROM #{table_name} WHERE project_dir = ?", [project_dir])
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Delete a record by ID
|
|
144
|
+
#
|
|
145
|
+
# @param id [Object] Record ID
|
|
146
|
+
def delete_by_id(id)
|
|
147
|
+
execute("DELETE FROM #{table_name} WHERE id = ?", [id])
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Delete all records matching project_dir
|
|
151
|
+
def delete_by_project
|
|
152
|
+
execute("DELETE FROM #{table_name} WHERE project_dir = ?", [project_dir])
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Count records matching project_dir
|
|
156
|
+
#
|
|
157
|
+
# @return [Integer] Record count
|
|
158
|
+
def count_by_project
|
|
159
|
+
query_value(
|
|
160
|
+
"SELECT COUNT(*) FROM #{table_name} WHERE project_dir = ?",
|
|
161
|
+
[project_dir]
|
|
162
|
+
) || 0
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|