aidp 0.47.1 → 0.49.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 687e2dc3dc54edf4fcdabb7ca75462595d51f0d8a1f3ad09d1954d035f576b05
4
- data.tar.gz: '089d63153f9a059188ce59385882d3920b32fbb40d4be644bbc877eade2263b6'
3
+ metadata.gz: 0c5a91840d333d7b33122434b8cc2efcad0149becf640e3932d5d1ea6fd799f1
4
+ data.tar.gz: 0604c7d3798c6dc90aa1ccf543916e73e63fabdd4bcc58ae1a0864cbb0277a0a
5
5
  SHA512:
6
- metadata.gz: bd7626bbf6245b7f02748abb9761423a4d7e64fc81983ca35bf7ecd14a15b3c6916ac80085e7df5c1dc6861e007cfb372bacf67bef6fb670bd73de5b206655b9
7
- data.tar.gz: 8ef9d9a2dbd220408a45d3d7f4d3bf96b07445d24a5a05867a4c6094a4e0425f69c7f033e42966d73d35ab207d2f157fc781163196ba2c837f4ddc7ad5149371
6
+ metadata.gz: 8dbaefb32e45cd9687524dea4cd72dc4c38eb13bb766f7e5b476e67481338ad76e6b6c72c0a80cd1e479487701266036c8d1b8cbab74ae288ff44ccaa5d891ac
7
+ data.tar.gz: b2de14911ec9b019235a4966d74208a61d5e2970a9fd710b31d0b58d2ec6f741bbdd6eb0a8314a6fbc7916bbd3c24c6352efa577080d356001dbd583b4aed6f3
@@ -280,13 +280,45 @@ module Aidp
280
280
  # Extract wizard-compatible config including custom_ports
281
281
  {
282
282
  providers: aidp_config.dig("providers")&.keys,
283
- test_framework: aidp_config.dig("work_loop", "test_commands", 0, "framework"),
283
+ test_framework: extract_test_framework_for_generation(aidp_config),
284
284
  linters: aidp_config.dig("work_loop", "linting", "tools"),
285
285
  watch_mode: aidp_config.dig("work_loop", "watch", "enabled"),
286
286
  custom_ports: aidp_config.dig("devcontainer", "custom_ports")
287
287
  }.compact
288
288
  end
289
289
 
290
+ def extract_test_framework_for_generation(aidp_config)
291
+ framework_from_work_loop_commands(aidp_config) ||
292
+ aidp_config.dig("work_loop", "test_commands", 0, "framework")
293
+ end
294
+
295
+ def framework_from_work_loop_commands(aidp_config)
296
+ candidates = Array(aidp_config.dig("work_loop", "commands"))
297
+ command_text = tagged_test_command(candidates) || first_recognized_string_command(candidates)
298
+ framework = Aidp::ToolingDetector.framework_from_command(command_text)
299
+ return if framework == :unknown
300
+
301
+ framework.to_s
302
+ end
303
+
304
+ # `work_loop.commands` entries may be hashes with a category, or bare
305
+ # command strings (no category available). Prefer an explicitly tagged
306
+ # test command; when only bare strings are present, fall back to the
307
+ # first one whose command text matches a known test framework.
308
+ def tagged_test_command(candidates)
309
+ candidates.find do |candidate|
310
+ candidate.is_a?(Hash) &&
311
+ # `generate_yaml` writes category as a symbol (e.g. `:test`); YAML
312
+ # preserves symbols on load, so coerce to string before comparing.
313
+ candidate["category"].to_s == "test"
314
+ end&.dig("command")
315
+ end
316
+
317
+ def first_recognized_string_command(candidates)
318
+ candidates.select { |candidate| candidate.is_a?(String) }
319
+ .find { |command| Aidp::ToolingDetector.framework_from_command(command) != :unknown }
320
+ end
321
+
290
322
  def build_config_from_aidp_yml(devcontainer_config)
291
323
  config = {}
292
324
 
@@ -3,6 +3,8 @@
3
3
  require "tty-prompt"
4
4
  require "tty-table"
5
5
  require "pastel"
6
+ require_relative "../strategy_execution/strategy_loader"
7
+ require_relative "../strategy_execution/experience_store"
6
8
  require_relative "terminal_io"
7
9
 
8
10
  module Aidp
@@ -30,6 +32,10 @@ module Aidp
30
32
  case subcommand
31
33
  when "start", nil
32
34
  start_workflow(args)
35
+ when "replay"
36
+ replay_workflow(args)
37
+ when "benchmark"
38
+ benchmark_workflows(args)
33
39
  when "list"
34
40
  list_workflows(args)
35
41
  when "status"
@@ -61,6 +67,9 @@ module Aidp
61
67
  display_message("")
62
68
  display_message("Temporal Workflow Commands:", type: :info)
63
69
  display_message(" aidp temporal start [issue_number] - Start issue-to-PR workflow", type: :info)
70
+ display_message(" aidp temporal start strategy <strategy.yml> [task.json] - Start strategy workflow", type: :info)
71
+ display_message(" aidp temporal replay <run_id> <strategy.yml> - Replay a task with a new strategy", type: :info)
72
+ display_message(" aidp temporal benchmark <strategy.yml> <run_id> [run_id...] - Replay many runs", type: :info)
64
73
  display_message(" aidp temporal list - List active workflows", type: :info)
65
74
  display_message(" aidp temporal status <workflow_id> - Show workflow status", type: :info)
66
75
  display_message(" aidp temporal signal <workflow_id> <signal> - Send signal to workflow", type: :info)
@@ -80,9 +89,11 @@ module Aidp
80
89
  start_issue_to_pr_workflow(issue_number)
81
90
  when "workloop", "work_loop"
82
91
  start_work_loop_workflow(args)
92
+ when "strategy"
93
+ start_strategy_workflow(args)
83
94
  else
84
95
  display_message("Unknown workflow type: #{workflow_type}", type: :error)
85
- display_message("Available: issue, workloop", type: :info)
96
+ display_message("Available: issue, workloop, strategy", type: :info)
86
97
  end
87
98
  end
88
99
 
@@ -90,6 +101,7 @@ module Aidp
90
101
  @prompt.select("Select workflow type:") do |menu|
91
102
  menu.choice "Issue to PR (full pipeline)", "issue"
92
103
  menu.choice "Work Loop (fix-forward iteration)", "workloop"
104
+ menu.choice "Strategy Execution", "strategy"
93
105
  end
94
106
  end
95
107
 
@@ -144,6 +156,92 @@ module Aidp
144
156
  display_message("Monitor with: aidp temporal status #{handle.id}", type: :info)
145
157
  end
146
158
 
159
+ def start_strategy_workflow(args)
160
+ strategy_path = args.shift
161
+ task_path = args.shift
162
+
163
+ unless strategy_path
164
+ display_message("Usage: aidp temporal start strategy <strategy.yml> [task.json]", type: :error)
165
+ return
166
+ end
167
+
168
+ strategy = load_strategy(strategy_path)
169
+ task = task_path ? load_task(task_path) : prompt_for_task
170
+
171
+ handle = Aidp::Temporal.start_workflow(
172
+ Aidp::Temporal::Workflows::StrategyExecutionWorkflow,
173
+ {
174
+ project_dir: @project_dir,
175
+ strategy: strategy.to_h,
176
+ task: task
177
+ },
178
+ project_dir: @project_dir
179
+ )
180
+
181
+ display_message("Started strategy workflow #{strategy.name}", type: :success)
182
+ display_message(" Workflow ID: #{handle.id}", type: :info)
183
+ end
184
+
185
+ def replay_workflow(args)
186
+ original_run_id = args.shift
187
+ strategy_path = args.shift
188
+
189
+ unless original_run_id && strategy_path
190
+ display_message("Usage: aidp temporal replay <run_id> <strategy.yml>", type: :error)
191
+ return
192
+ end
193
+
194
+ bundle = experience_store.replay_bundle(original_run_id)
195
+ unless bundle
196
+ display_message("Replay source run not found: #{original_run_id}", type: :error)
197
+ return
198
+ end
199
+
200
+ strategy = load_strategy(strategy_path)
201
+ replay_task = build_replay_task(bundle[:task], original_run_id)
202
+
203
+ handle = Aidp::Temporal.start_workflow(
204
+ Aidp::Temporal::Workflows::StrategyExecutionWorkflow,
205
+ {
206
+ project_dir: @project_dir,
207
+ strategy: strategy.to_h,
208
+ task: replay_task
209
+ },
210
+ project_dir: @project_dir
211
+ )
212
+
213
+ display_message("Replay started from run #{original_run_id}", type: :success)
214
+ display_message(" Workflow ID: #{handle.id}", type: :info)
215
+ end
216
+
217
+ def benchmark_workflows(args)
218
+ strategy_path = args.shift
219
+ run_ids = args
220
+
221
+ unless strategy_path && run_ids.any?
222
+ display_message("Usage: aidp temporal benchmark <strategy.yml> <run_id> [run_id...]", type: :error)
223
+ return
224
+ end
225
+
226
+ strategy = load_strategy(strategy_path)
227
+ started = run_ids.filter_map do |run_id|
228
+ bundle = experience_store.replay_bundle(run_id)
229
+ next unless bundle
230
+
231
+ Aidp::Temporal.start_workflow(
232
+ Aidp::Temporal::Workflows::StrategyExecutionWorkflow,
233
+ {
234
+ project_dir: @project_dir,
235
+ strategy: strategy.to_h,
236
+ task: build_replay_task(bundle[:task], run_id)
237
+ },
238
+ project_dir: @project_dir
239
+ ).id
240
+ end
241
+
242
+ display_message("Started #{started.length} benchmark replay workflow(s)", type: :success)
243
+ end
244
+
147
245
  def list_workflows(args)
148
246
  require_relative "../temporal"
149
247
 
@@ -327,7 +425,9 @@ module Aidp
327
425
  worker.register_workflows(
328
426
  Aidp::Temporal::Workflows::IssueToPrWorkflow,
329
427
  Aidp::Temporal::Workflows::WorkLoopWorkflow,
330
- Aidp::Temporal::Workflows::SubIssueWorkflow
428
+ Aidp::Temporal::Workflows::SubIssueWorkflow,
429
+ Aidp::Temporal::Workflows::StrategyBranchWorkflow,
430
+ Aidp::Temporal::Workflows::StrategyExecutionWorkflow
331
431
  )
332
432
 
333
433
  # Register all activities
@@ -342,7 +442,9 @@ module Aidp
342
442
  Aidp::Temporal::Activities::RecordCheckpointActivity.new,
343
443
  Aidp::Temporal::Activities::CreatePrActivity.new,
344
444
  Aidp::Temporal::Activities::RunWorkLoopIterationActivity.new,
345
- Aidp::Temporal::Activities::AnalyzeSubTaskActivity.new
445
+ Aidp::Temporal::Activities::AnalyzeSubTaskActivity.new,
446
+ Aidp::Temporal::Activities::ExecuteCliCommandActivity.new,
447
+ Aidp::Temporal::Activities::ManageExperienceStoreActivity.new
346
448
  )
347
449
 
348
450
  display_message(@pastel.green("Worker started"), type: :success)
@@ -420,6 +522,33 @@ module Aidp
420
522
  display_message(" 3. Start workflow: aidp temporal start issue <number>", type: :info)
421
523
  end
422
524
 
525
+ def load_strategy(strategy_path)
526
+ StrategyExecution::StrategyLoader.new(project_dir: @project_dir).load_file(strategy_path)
527
+ end
528
+
529
+ def build_replay_task(bundle_task, source_run_id)
530
+ bundle_task.except(:id, :source_run_id).merge(source_run_id: source_run_id)
531
+ end
532
+
533
+ def load_task(task_path)
534
+ JSON.parse(File.read(expand_path(task_path)), symbolize_names: true)
535
+ end
536
+
537
+ def prompt_for_task
538
+ {
539
+ title: @prompt.ask("Task title:"),
540
+ description: @prompt.ask("Task description:")
541
+ }
542
+ end
543
+
544
+ def experience_store
545
+ @experience_store ||= StrategyExecution::ExperienceStore.new(project_dir: @project_dir)
546
+ end
547
+
548
+ def expand_path(path)
549
+ File.expand_path(path, @project_dir)
550
+ end
551
+
423
552
  def format_status(status)
424
553
  case status.to_s
425
554
  when "running"
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+
5
+ module Aidp
6
+ module Database
7
+ module Repositories
8
+ class ExperienceArtifactRepository < Repository
9
+ def initialize(project_dir: Dir.pwd)
10
+ super(project_dir: project_dir, table_name: "experience_artifacts")
11
+ end
12
+
13
+ def record(run_id:, role:, path:, metadata: {})
14
+ execute(
15
+ insert_sql([:project_dir, :run_id, :role, :path, :metadata]),
16
+ [project_dir, run_id, role, path, serialize_json(metadata || {})]
17
+ )
18
+
19
+ Aidp.log_debug("experience_artifact_repository", "recorded",
20
+ run_id: run_id, role: role, path: path)
21
+ end
22
+
23
+ def list_for_run(run_id)
24
+ rows = query(
25
+ "SELECT * FROM experience_artifacts WHERE run_id = ? AND project_dir = ? ORDER BY id ASC",
26
+ [run_id, project_dir]
27
+ )
28
+ rows.map do |row|
29
+ {
30
+ role: row["role"],
31
+ path: row["path"],
32
+ metadata: deserialize_json(row["metadata"]) || {},
33
+ created_at: row["created_at"]
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+
5
+ module Aidp
6
+ module Database
7
+ module Repositories
8
+ class ExperienceEvaluationRepository < Repository
9
+ def initialize(project_dir: Dir.pwd)
10
+ super(project_dir: project_dir, table_name: "experience_evaluations")
11
+ end
12
+
13
+ def record(run_id:, evaluator_name:, score:, passed:, summary: nil, metadata: {})
14
+ execute(
15
+ insert_sql([
16
+ :project_dir, :run_id, :evaluator_name, :score, :passed, :summary, :metadata
17
+ ]),
18
+ [
19
+ project_dir,
20
+ run_id,
21
+ evaluator_name,
22
+ score,
23
+ passed ? 1 : 0,
24
+ summary,
25
+ serialize_json(metadata || {})
26
+ ]
27
+ )
28
+
29
+ Aidp.log_debug("experience_evaluation_repository", "recorded",
30
+ run_id: run_id,
31
+ evaluator_name: evaluator_name,
32
+ score: score,
33
+ passed: passed)
34
+ end
35
+
36
+ def list_for_run(run_id)
37
+ rows = query(
38
+ "SELECT * FROM experience_evaluations WHERE run_id = ? AND project_dir = ? ORDER BY id ASC",
39
+ [run_id, project_dir]
40
+ )
41
+ rows.map do |row|
42
+ {
43
+ evaluator_name: row["evaluator_name"],
44
+ score: row["score"],
45
+ passed: row["passed"] == 1,
46
+ summary: row["summary"],
47
+ metadata: deserialize_json(row["metadata"]) || {},
48
+ created_at: row["created_at"]
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require_relative "../repository"
5
+
6
+ module Aidp
7
+ module Database
8
+ module Repositories
9
+ class ExperienceRunRepository < Repository
10
+ def initialize(project_dir: Dir.pwd)
11
+ super(project_dir: project_dir, table_name: "experience_runs")
12
+ end
13
+
14
+ def start(task_id:, strategy_id:, workflow_id:, depth:, input_payload:, parent_run_id: nil, branch_key: nil, metadata: {})
15
+ run_id = SecureRandom.uuid
16
+
17
+ execute(
18
+ insert_sql([
19
+ :id, :project_dir, :task_id, :strategy_id, :workflow_id, :parent_run_id,
20
+ :branch_key, :status, :depth, :input_payload, :metadata
21
+ ]),
22
+ [
23
+ run_id,
24
+ project_dir,
25
+ task_id,
26
+ strategy_id,
27
+ workflow_id,
28
+ parent_run_id,
29
+ branch_key,
30
+ "running",
31
+ depth,
32
+ serialize_json(input_payload || {}),
33
+ serialize_json(metadata || {})
34
+ ]
35
+ )
36
+
37
+ Aidp.log_debug("experience_run_repository", "started",
38
+ id: run_id,
39
+ task_id: task_id,
40
+ strategy_id: strategy_id,
41
+ branch_key: branch_key,
42
+ depth: depth)
43
+
44
+ load(run_id)
45
+ end
46
+
47
+ def complete(run_id:, status:, output_payload:, metadata: {})
48
+ merged_metadata = existing_metadata(run_id).merge(metadata || {})
49
+
50
+ execute(
51
+ <<~SQL,
52
+ UPDATE experience_runs
53
+ SET status = ?, output_payload = ?, metadata = ?, completed_at = ?
54
+ WHERE id = ? AND project_dir = ?
55
+ SQL
56
+ [
57
+ status,
58
+ serialize_json(output_payload || {}),
59
+ serialize_json(merged_metadata),
60
+ current_timestamp,
61
+ run_id,
62
+ project_dir
63
+ ]
64
+ )
65
+
66
+ Aidp.log_debug("experience_run_repository", "completed",
67
+ id: run_id, status: status)
68
+
69
+ load(run_id)
70
+ end
71
+
72
+ def load(run_id)
73
+ row = query_one(
74
+ "SELECT * FROM experience_runs WHERE id = ? AND project_dir = ?",
75
+ [run_id, project_dir]
76
+ )
77
+ deserialize_run(row)
78
+ end
79
+
80
+ def bundle_for_replay(run_id)
81
+ row = query_one(
82
+ <<~SQL,
83
+ SELECT runs.*, tasks.title AS task_title, tasks.description AS task_description,
84
+ tasks.input_payload AS task_input_payload, tasks.context AS task_context,
85
+ tasks.source_run_id AS task_source_run_id
86
+ FROM experience_runs runs
87
+ JOIN experience_tasks tasks ON tasks.id = runs.task_id
88
+ WHERE runs.id = ? AND runs.project_dir = ?
89
+ SQL
90
+ [run_id, project_dir]
91
+ )
92
+ return nil unless row
93
+
94
+ run = deserialize_run(row)
95
+ run.merge(
96
+ task: {
97
+ id: row["task_id"],
98
+ title: row["task_title"],
99
+ description: row["task_description"],
100
+ input_payload: deserialize_json(row["task_input_payload"]) || {},
101
+ context: deserialize_json(row["task_context"]) || {},
102
+ source_run_id: row["task_source_run_id"]
103
+ }
104
+ )
105
+ end
106
+
107
+ def children(parent_run_id)
108
+ rows = query(
109
+ "SELECT * FROM experience_runs WHERE parent_run_id = ? AND project_dir = ? ORDER BY started_at ASC",
110
+ [parent_run_id, project_dir]
111
+ )
112
+ rows.map { |row| deserialize_run(row) }
113
+ end
114
+
115
+ private
116
+
117
+ def existing_metadata(run_id)
118
+ load(run_id)&.fetch(:metadata, {}) || {}
119
+ end
120
+
121
+ def deserialize_run(row)
122
+ return nil unless row
123
+
124
+ {
125
+ id: row["id"],
126
+ task_id: row["task_id"],
127
+ strategy_id: row["strategy_id"],
128
+ workflow_id: row["workflow_id"],
129
+ parent_run_id: row["parent_run_id"],
130
+ branch_key: row["branch_key"],
131
+ status: row["status"],
132
+ depth: row["depth"],
133
+ input_payload: deserialize_json(row["input_payload"]) || {},
134
+ output_payload: deserialize_json(row["output_payload"]) || {},
135
+ metadata: deserialize_json(row["metadata"]) || {},
136
+ started_at: row["started_at"],
137
+ completed_at: row["completed_at"]
138
+ }
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require_relative "../repository"
5
+
6
+ module Aidp
7
+ module Database
8
+ module Repositories
9
+ class ExperienceTaskRepository < Repository
10
+ def initialize(project_dir: Dir.pwd)
11
+ super(project_dir: project_dir, table_name: "experience_tasks")
12
+ end
13
+
14
+ def create(description:, title: nil, input_payload: {}, context: {}, source_run_id: nil)
15
+ task_id = SecureRandom.uuid
16
+
17
+ execute(
18
+ insert_sql([:id, :project_dir, :title, :description, :input_payload, :context, :source_run_id]),
19
+ [
20
+ task_id,
21
+ project_dir,
22
+ title,
23
+ description,
24
+ serialize_json(input_payload || {}),
25
+ serialize_json(context || {}),
26
+ source_run_id
27
+ ]
28
+ )
29
+
30
+ Aidp.log_debug("experience_task_repository", "created",
31
+ id: task_id, source_run_id: source_run_id)
32
+
33
+ load(task_id)
34
+ end
35
+
36
+ def load(task_id)
37
+ row = query_one(
38
+ "SELECT * FROM experience_tasks WHERE id = ? AND project_dir = ?",
39
+ [task_id, project_dir]
40
+ )
41
+ deserialize_task(row)
42
+ end
43
+
44
+ private
45
+
46
+ def deserialize_task(row)
47
+ return nil unless row
48
+
49
+ {
50
+ id: row["id"],
51
+ title: row["title"],
52
+ description: row["description"],
53
+ input_payload: deserialize_json(row["input_payload"]) || {},
54
+ context: deserialize_json(row["context"]) || {},
55
+ source_run_id: row["source_run_id"],
56
+ created_at: row["created_at"]
57
+ }
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require_relative "../repository"
5
+
6
+ module Aidp
7
+ module Database
8
+ module Repositories
9
+ class StrategyRepository < Repository
10
+ def initialize(project_dir: Dir.pwd)
11
+ super(project_dir: project_dir, table_name: "strategies")
12
+ end
13
+
14
+ def register(name:, spec:)
15
+ strategy_id = Digest::SHA256.hexdigest("#{project_dir}:#{name}:#{spec}")
16
+
17
+ # Concurrent sibling child workflows can register the identical
18
+ # strategy at the same time (same deterministic strategy_id), so
19
+ # the insert must be idempotent rather than a check-then-insert to
20
+ # avoid a primary-key race between them.
21
+ execute(
22
+ "INSERT OR IGNORE INTO #{table_name} (id, project_dir, name, spec) VALUES (?, ?, ?, ?)",
23
+ [strategy_id, project_dir, name, spec]
24
+ )
25
+
26
+ Aidp.log_debug("strategy_repository", "registered", id: strategy_id, name: name)
27
+
28
+ {id: strategy_id, name: name}
29
+ end
30
+
31
+ def load(id)
32
+ row = query_one("SELECT * FROM strategies WHERE id = ? AND project_dir = ?", [id, project_dir])
33
+ deserialize_strategy(row)
34
+ end
35
+
36
+ def find_by_name(name)
37
+ row = query_one(
38
+ "SELECT * FROM strategies WHERE project_dir = ? AND name = ? ORDER BY created_at DESC, rowid DESC",
39
+ [project_dir, name]
40
+ )
41
+ deserialize_strategy(row)
42
+ end
43
+
44
+ private
45
+
46
+ def deserialize_strategy(row)
47
+ return nil unless row
48
+
49
+ {
50
+ id: row["id"],
51
+ name: row["name"],
52
+ spec: deserialize_json(row["spec"]) || {},
53
+ created_at: row["created_at"]
54
+ }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end