aidp 0.47.1 → 0.48.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: 497afaf23ef14fc5ebced21f0ca90a090bcc66f642002fc289d3757dc9a136f8
4
+ data.tar.gz: f2a8e664cb040b4869a5b2b1ccf15fabe9524baed622bf6b9a04ce57e0d3c864
5
5
  SHA512:
6
- metadata.gz: bd7626bbf6245b7f02748abb9761423a4d7e64fc81983ca35bf7ecd14a15b3c6916ac80085e7df5c1dc6861e007cfb372bacf67bef6fb670bd73de5b206655b9
7
- data.tar.gz: 8ef9d9a2dbd220408a45d3d7f4d3bf96b07445d24a5a05867a4c6094a4e0425f69c7f033e42966d73d35ab207d2f157fc781163196ba2c837f4ddc7ad5149371
6
+ metadata.gz: 69b44c42f6c45c7d7239674c2f06f3326b1437c477bea43f98dfcf9fdfab3ed1a9811c14fd20ec49e7af0af0efe798c0cfe7e74443f43a39de263f30e62cb745
7
+ data.tar.gz: 5be71e4d983121166264108f9711245d7f26129a26e318a6a0ce9f38b3a5aaabba30544e818c41a6606671554e8a6264adba4abdd622b0c49e8d0c0e90051591
@@ -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
@@ -321,11 +321,105 @@ module Aidp
321
321
  CREATE UNIQUE INDEX IF NOT EXISTS idx_template_versions_unique ON template_versions(project_dir, template_id, version_number);
322
322
  SQL
323
323
 
324
+ # Version 4: Add strategy execution experience store
325
+ V4_STRATEGY_EXECUTION = <<~SQL
326
+ -- Strategy definitions for orchestration-as-data
327
+ CREATE TABLE IF NOT EXISTS strategies (
328
+ id TEXT PRIMARY KEY,
329
+ project_dir TEXT NOT NULL,
330
+ name TEXT NOT NULL,
331
+ spec TEXT NOT NULL,
332
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
333
+ );
334
+ CREATE INDEX IF NOT EXISTS idx_strategies_project_name ON strategies(project_dir, name);
335
+
336
+ -- Replayable task inputs for strategy execution
337
+ CREATE TABLE IF NOT EXISTS experience_tasks (
338
+ id TEXT PRIMARY KEY,
339
+ project_dir TEXT NOT NULL,
340
+ title TEXT,
341
+ description TEXT NOT NULL,
342
+ input_payload TEXT,
343
+ context TEXT,
344
+ source_run_id TEXT,
345
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
346
+ );
347
+ CREATE INDEX IF NOT EXISTS idx_experience_tasks_project ON experience_tasks(project_dir);
348
+ CREATE INDEX IF NOT EXISTS idx_experience_tasks_source_run ON experience_tasks(source_run_id);
349
+
350
+ -- Individual strategy execution runs, including speculative branches
351
+ CREATE TABLE IF NOT EXISTS experience_runs (
352
+ id TEXT PRIMARY KEY,
353
+ project_dir TEXT NOT NULL,
354
+ task_id TEXT NOT NULL,
355
+ strategy_id TEXT NOT NULL,
356
+ workflow_id TEXT,
357
+ parent_run_id TEXT,
358
+ branch_key TEXT,
359
+ status TEXT NOT NULL,
360
+ depth INTEGER DEFAULT 0,
361
+ input_payload TEXT,
362
+ output_payload TEXT,
363
+ metadata TEXT,
364
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
365
+ completed_at TEXT
366
+ );
367
+ CREATE INDEX IF NOT EXISTS idx_experience_runs_project ON experience_runs(project_dir);
368
+ CREATE INDEX IF NOT EXISTS idx_experience_runs_task ON experience_runs(task_id);
369
+ CREATE INDEX IF NOT EXISTS idx_experience_runs_parent ON experience_runs(parent_run_id);
370
+ CREATE INDEX IF NOT EXISTS idx_experience_runs_strategy ON experience_runs(strategy_id);
371
+
372
+ -- Evaluation signals captured for each run
373
+ CREATE TABLE IF NOT EXISTS experience_evaluations (
374
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
375
+ project_dir TEXT NOT NULL,
376
+ run_id TEXT NOT NULL,
377
+ evaluator_name TEXT NOT NULL,
378
+ score REAL,
379
+ passed INTEGER DEFAULT 0 CHECK (passed IN (0, 1)),
380
+ summary TEXT,
381
+ metadata TEXT,
382
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
383
+ );
384
+ CREATE INDEX IF NOT EXISTS idx_experience_evaluations_run ON experience_evaluations(run_id);
385
+ CREATE INDEX IF NOT EXISTS idx_experience_evaluations_name ON experience_evaluations(project_dir, evaluator_name);
386
+
387
+ -- Artifact metadata produced by runs
388
+ CREATE TABLE IF NOT EXISTS experience_artifacts (
389
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
390
+ project_dir TEXT NOT NULL,
391
+ run_id TEXT NOT NULL,
392
+ role TEXT NOT NULL,
393
+ path TEXT NOT NULL,
394
+ metadata TEXT,
395
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
396
+ );
397
+ CREATE INDEX IF NOT EXISTS idx_experience_artifacts_run ON experience_artifacts(run_id);
398
+
399
+ -- Embeddings are stored as JSON vectors until a dedicated vector store exists
400
+ CREATE TABLE IF NOT EXISTS experience_embeddings (
401
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
402
+ project_dir TEXT NOT NULL,
403
+ task_id TEXT,
404
+ run_id TEXT,
405
+ embedding_type TEXT NOT NULL,
406
+ vector TEXT NOT NULL,
407
+ metadata TEXT,
408
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
409
+ );
410
+ CREATE INDEX IF NOT EXISTS idx_experience_embeddings_task ON experience_embeddings(task_id);
411
+ CREATE INDEX IF NOT EXISTS idx_experience_embeddings_run ON experience_embeddings(run_id);
412
+ SQL
413
+
324
414
  # All migrations in order
415
+ # V4's strategies index is intentionally non-unique so that immutable
416
+ # versions of a named strategy can coexist for replay and auditability;
417
+ # StrategyRepository keys rows by a SHA256(project_dir:name:spec) digest.
325
418
  MIGRATIONS = {
326
419
  1 => V1_INITIAL,
327
420
  2 => V2_PROMPT_FEEDBACK,
328
- 3 => V3_TEMPLATE_VERSIONS
421
+ 3 => V3_TEMPLATE_VERSIONS,
422
+ 4 => V4_STRATEGY_EXECUTION
329
423
  }.freeze
330
424
 
331
425
  # Get SQL for a specific version