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.
@@ -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
data/lib/aidp/database.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "sqlite3"
4
4
  require "json"
5
5
  require "fileutils"
6
+ Kernel.require("set") unless defined?(Set)
6
7
 
7
8
  module Aidp
8
9
  # Database module for SQLite-based storage
@@ -15,6 +16,8 @@ module Aidp
15
16
  # Thread-safe connection cache
16
17
  @connections = {}
17
18
  @mutex = Mutex.new
19
+ @migrated_projects = Set.new
20
+ @migration_mutex = Mutex.new
18
21
 
19
22
  class << self
20
23
  # Get or create a database connection for the given project directory
@@ -26,6 +29,8 @@ module Aidp
26
29
  db_path = ConfigPaths.database_file(project_dir)
27
30
 
28
31
  @mutex.synchronize do
32
+ invalidate_missing_connection!(db_path)
33
+
29
34
  # Return cached connection if valid
30
35
  if @connections[db_path]&.closed? == false
31
36
  return @connections[db_path]
@@ -53,6 +58,23 @@ module Aidp
53
58
  Migrations.run!(project_dir)
54
59
  end
55
60
 
61
+ # Run pending migrations at most once per process for each project directory.
62
+ #
63
+ # @param project_dir [String] Project directory path
64
+ # @return [Array<Integer>] List of applied migration versions
65
+ def migrate_once!(project_dir = Dir.pwd)
66
+ expanded_project_dir = File.expand_path(project_dir)
67
+ require_relative "database/migrations"
68
+
69
+ @migration_mutex.synchronize do
70
+ return [] if migration_current?(expanded_project_dir)
71
+
72
+ migrate!(expanded_project_dir).tap do
73
+ @migrated_projects << expanded_project_dir
74
+ end
75
+ end
76
+ end
77
+
56
78
  # Check if database exists and is initialized
57
79
  #
58
80
  # @param project_dir [String] Project directory path
@@ -134,6 +156,22 @@ module Aidp
134
156
  # Set busy timeout to 5 seconds
135
157
  db.busy_timeout = 5000
136
158
  end
159
+
160
+ def invalidate_missing_connection!(db_path)
161
+ db = @connections[db_path]
162
+ return unless db
163
+ return if db.closed?
164
+ return if File.exist?(db_path)
165
+
166
+ db.close
167
+ @connections.delete(db_path)
168
+ end
169
+
170
+ def migration_current?(project_dir)
171
+ @migrated_projects.include?(project_dir) &&
172
+ exists?(project_dir) &&
173
+ !Migrations.pending?(project_dir)
174
+ end
137
175
  end
138
176
  end
139
177
  end
@@ -909,23 +909,28 @@ module Aidp
909
909
  # Load commands from configuration, supporting both new generic format
910
910
  # and legacy category-specific format for backwards compatibility
911
911
  def load_commands
912
- commands = []
913
-
914
912
  # Load from new generic commands array if present
915
913
  raw_commands = work_loop_config[:commands] || []
916
- commands.concat(normalize_generic_commands(raw_commands))
917
-
918
- # Load from legacy category-specific arrays for backwards compatibility
919
- commands.concat(load_legacy_commands)
914
+ generic_commands = normalize_generic_commands(raw_commands)
915
+ legacy_commands = load_legacy_commands
916
+ commands = deduplicated_commands(generic_commands, legacy_commands)
920
917
 
921
918
  Aidp.log_debug("configuration", "loaded_commands",
922
919
  total: commands.size,
923
920
  from_generic: raw_commands.size,
924
- from_legacy: commands.size - raw_commands.size)
921
+ from_legacy: legacy_commands.size)
925
922
 
926
923
  commands
927
924
  end
928
925
 
926
+ def deduplicated_commands(*command_sets)
927
+ command_sets.flatten.uniq { |command| deduplication_key(command) }
928
+ end
929
+
930
+ def deduplication_key(command)
931
+ command.slice(:command, :required, :run_after, :category, :timeout_seconds)
932
+ end
933
+
929
934
  # Normalize generic command format
930
935
  # @param commands [Array] Raw command configurations
931
936
  # @return [Array<Hash>] Normalized command configs