aidp 0.16.0 → 0.17.1

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aidp/analyze/error_handler.rb +32 -13
  3. data/lib/aidp/analyze/kb_inspector.rb +2 -3
  4. data/lib/aidp/analyze/progress.rb +6 -11
  5. data/lib/aidp/cli/mcp_dashboard.rb +1 -1
  6. data/lib/aidp/cli.rb +300 -33
  7. data/lib/aidp/config.rb +1 -1
  8. data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
  9. data/lib/aidp/execute/checkpoint.rb +1 -1
  10. data/lib/aidp/execute/future_work_backlog.rb +1 -1
  11. data/lib/aidp/execute/progress.rb +6 -9
  12. data/lib/aidp/execute/repl_macros.rb +79 -10
  13. data/lib/aidp/harness/config_loader.rb +2 -2
  14. data/lib/aidp/harness/config_validator.rb +1 -1
  15. data/lib/aidp/harness/enhanced_runner.rb +16 -7
  16. data/lib/aidp/harness/error_handler.rb +12 -5
  17. data/lib/aidp/harness/provider_manager.rb +4 -19
  18. data/lib/aidp/harness/runner.rb +2 -2
  19. data/lib/aidp/harness/state/persistence.rb +9 -10
  20. data/lib/aidp/harness/state/workflow_state.rb +3 -2
  21. data/lib/aidp/harness/state_manager.rb +33 -97
  22. data/lib/aidp/harness/status_display.rb +22 -12
  23. data/lib/aidp/harness/ui/enhanced_tui.rb +3 -4
  24. data/lib/aidp/harness/user_interface.rb +11 -6
  25. data/lib/aidp/jobs/background_runner.rb +8 -2
  26. data/lib/aidp/logger.rb +1 -1
  27. data/lib/aidp/message_display.rb +9 -2
  28. data/lib/aidp/providers/anthropic.rb +1 -1
  29. data/lib/aidp/providers/base.rb +4 -4
  30. data/lib/aidp/providers/codex.rb +1 -1
  31. data/lib/aidp/providers/cursor.rb +1 -1
  32. data/lib/aidp/providers/gemini.rb +1 -1
  33. data/lib/aidp/providers/github_copilot.rb +1 -1
  34. data/lib/aidp/providers/macos_ui.rb +1 -1
  35. data/lib/aidp/providers/opencode.rb +1 -1
  36. data/lib/aidp/skills/registry.rb +31 -29
  37. data/lib/aidp/skills/router.rb +178 -0
  38. data/lib/aidp/skills/wizard/builder.rb +141 -0
  39. data/lib/aidp/skills/wizard/controller.rb +145 -0
  40. data/lib/aidp/skills/wizard/differ.rb +232 -0
  41. data/lib/aidp/skills/wizard/prompter.rb +317 -0
  42. data/lib/aidp/skills/wizard/template_library.rb +164 -0
  43. data/lib/aidp/skills/wizard/writer.rb +105 -0
  44. data/lib/aidp/version.rb +1 -1
  45. data/lib/aidp/watch/plan_generator.rb +1 -1
  46. data/lib/aidp/watch/repository_client.rb +13 -9
  47. data/lib/aidp/workflows/guided_agent.rb +2 -312
  48. data/lib/aidp/workstream_executor.rb +8 -2
  49. data/templates/skills/README.md +334 -0
  50. data/templates/skills/architecture_analyst/SKILL.md +173 -0
  51. data/templates/skills/product_strategist/SKILL.md +141 -0
  52. data/templates/skills/repository_analyst/SKILL.md +117 -0
  53. data/templates/skills/test_analyzer/SKILL.md +213 -0
  54. metadata +13 -1
@@ -40,7 +40,7 @@ module Aidp
40
40
  # Get the latest checkpoint data
41
41
  def latest_checkpoint
42
42
  return nil unless File.exist?(@checkpoint_file)
43
- YAML.load_file(@checkpoint_file)
43
+ YAML.safe_load_file(@checkpoint_file, permitted_classes: [Date, Time, Symbol], aliases: true)
44
44
  end
45
45
 
46
46
  # Get checkpoint history for analysis
@@ -218,7 +218,7 @@ module Aidp
218
218
  def load_existing_backlog
219
219
  return unless File.exist?(@backlog_file)
220
220
 
221
- data = YAML.load_file(@backlog_file)
221
+ data = YAML.safe_load_file(@backlog_file, permitted_classes: [Date, Time, Symbol], aliases: true)
222
222
  @entries = data["entries"] || [] if data.is_a?(Hash)
223
223
  @entries = symbolize_keys_deep(@entries)
224
224
  rescue => e
@@ -9,9 +9,10 @@ module Aidp
9
9
  class Progress
10
10
  attr_reader :project_dir, :progress_file
11
11
 
12
- def initialize(project_dir)
12
+ def initialize(project_dir, skip_persistence: false)
13
13
  @project_dir = project_dir
14
14
  @progress_file = File.join(project_dir, ".aidp", "progress", "execute.yml")
15
+ @skip_persistence = skip_persistence
15
16
  load_progress
16
17
  end
17
18
 
@@ -61,23 +62,19 @@ module Aidp
61
62
  private
62
63
 
63
64
  def load_progress
64
- # In test mode, only skip file operations if no progress file exists
65
- if (ENV["RACK_ENV"] == "test" || defined?(RSpec)) && !File.exist?(@progress_file)
65
+ if @skip_persistence && !File.exist?(@progress_file)
66
66
  @progress = {}
67
67
  return
68
68
  end
69
-
70
- @progress = if File.exist?(@progress_file)
71
- YAML.load_file(@progress_file) || {}
69
+ @progress = if !@skip_persistence && File.exist?(@progress_file)
70
+ YAML.safe_load_file(@progress_file, permitted_classes: [Date, Time, Symbol], aliases: true) || {}
72
71
  else
73
72
  {}
74
73
  end
75
74
  end
76
75
 
77
76
  def save_progress
78
- # In test mode, skip file operations to avoid hanging
79
- return if ENV["RACK_ENV"] == "test" || defined?(RSpec)
80
-
77
+ return if @skip_persistence
81
78
  FileUtils.mkdir_p(File.dirname(@progress_file))
82
79
  File.write(@progress_file, @progress.to_yaml)
83
80
  end
@@ -9,7 +9,7 @@ module Aidp
9
9
  # - /split - Divide work into smaller contracts
10
10
  # - /halt-on <pattern> - Pause on specific test failures
11
11
  class ReplMacros
12
- attr_reader :pinned_files, :focus_patterns, :halt_patterns, :split_mode, :current_workstream
12
+ attr_reader :pinned_files, :focus_patterns, :halt_patterns, :split_mode, :current_workstream, :current_skill
13
13
 
14
14
  def initialize(project_dir: Dir.pwd)
15
15
  @pinned_files = Set.new
@@ -18,6 +18,7 @@ module Aidp
18
18
  @split_mode = false
19
19
  @project_dir = project_dir
20
20
  @current_workstream = nil
21
+ @current_skill = nil
21
22
  @commands = register_commands
22
23
  end
23
24
 
@@ -65,6 +66,7 @@ module Aidp
65
66
  halt_patterns: @halt_patterns,
66
67
  split_mode: @split_mode,
67
68
  current_workstream: @current_workstream,
69
+ current_skill: @current_skill,
68
70
  active_constraints: active_constraints_count
69
71
  }
70
72
  end
@@ -113,6 +115,30 @@ module Aidp
113
115
  true
114
116
  end
115
117
 
118
+ # Retrieve the current skill object, or nil if none is selected
119
+ #
120
+ # This method provides access to the full skill object (with content, providers, etc.)
121
+ # for the currently selected skill via `/skill use <id>`.
122
+ #
123
+ # @return [Aidp::Skills::Skill, nil] The current skill object or nil
124
+ #
125
+ # @example
126
+ # repl = ReplMacros.new(project_dir: Dir.pwd)
127
+ # repl.execute("/skill use repository_analyst")
128
+ # skill = repl.current_skill_object
129
+ # puts skill.content if skill # => skill's markdown content
130
+ def current_skill_object
131
+ return nil unless @current_skill
132
+
133
+ require_relative "../skills"
134
+ registry = Aidp::Skills::Registry.new(project_dir: @project_dir)
135
+ registry.load_skills
136
+ registry.find(@current_skill)
137
+ rescue => e
138
+ Aidp.log_error("repl_macros", "Failed to load current skill object", error: e.message)
139
+ nil
140
+ end
141
+
116
142
  private
117
143
 
118
144
  # Register all available REPL commands
@@ -1257,25 +1283,25 @@ module Aidp
1257
1283
  lines = ["Available Skills:", ""]
1258
1284
  by_source = registry.by_source
1259
1285
 
1260
- if by_source[:builtin].any?
1261
- lines << "Built-in Skills:"
1262
- by_source[:builtin].each do |skill_id|
1286
+ if by_source[:template].any?
1287
+ lines << "Template Skills:"
1288
+ by_source[:template].each do |skill_id|
1263
1289
  skill = registry.find(skill_id)
1264
1290
  lines << " • #{skill_id} - #{skill.description}"
1265
1291
  end
1266
1292
  lines << ""
1267
1293
  end
1268
1294
 
1269
- if by_source[:custom].any?
1270
- lines << "Custom Skills:"
1271
- by_source[:custom].each do |skill_id|
1295
+ if by_source[:project].any?
1296
+ lines << "Project Skills:"
1297
+ by_source[:project].each do |skill_id|
1272
1298
  skill = registry.find(skill_id)
1273
- lines << " • #{skill_id} - #{skill.description} [CUSTOM]"
1299
+ lines << " • #{skill_id} - #{skill.description} [PROJECT]"
1274
1300
  end
1275
1301
  lines << ""
1276
1302
  end
1277
1303
 
1278
- lines << "Use '/skill show <id>' for details"
1304
+ lines << "Use '/skill show <id>' for details or '/skill use <id>' to activate"
1279
1305
 
1280
1306
  {
1281
1307
  success: true,
@@ -1412,10 +1438,53 @@ module Aidp
1412
1438
  }
1413
1439
  end
1414
1440
 
1441
+ when "use"
1442
+ # Switch to a specific skill
1443
+ skill_id = args.shift
1444
+
1445
+ unless skill_id
1446
+ return {
1447
+ success: false,
1448
+ message: "Usage: /skill use <skill-id>",
1449
+ action: :none
1450
+ }
1451
+ end
1452
+
1453
+ begin
1454
+ registry = Aidp::Skills::Registry.new(project_dir: @project_dir)
1455
+ registry.load_skills
1456
+
1457
+ skill = registry.find(skill_id)
1458
+
1459
+ unless skill
1460
+ return {
1461
+ success: false,
1462
+ message: "Skill not found: #{skill_id}\nUse '/skill list' to see available skills",
1463
+ action: :none
1464
+ }
1465
+ end
1466
+
1467
+ # Store the current skill for the session
1468
+ @current_skill = skill_id
1469
+
1470
+ {
1471
+ success: true,
1472
+ message: "✓ Now using skill: #{skill.name} (#{skill_id})\n\n#{skill.description}",
1473
+ action: :switch_skill,
1474
+ data: {skill_id: skill_id, skill: skill}
1475
+ }
1476
+ rescue => e
1477
+ {
1478
+ success: false,
1479
+ message: "Failed to switch skill: #{e.message}",
1480
+ action: :none
1481
+ }
1482
+ end
1483
+
1415
1484
  else
1416
1485
  {
1417
1486
  success: false,
1418
- message: "Usage: /skill <command> [args]\n\nCommands:\n list - List all available skills\n show <id> - Show detailed skill information\n search <query> - Search skills by keyword\n\nExamples:\n /skill list\n /skill show repository_analyst\n /skill search git",
1487
+ message: "Usage: /skill <command> [args]\n\nCommands:\n list - List all available skills\n show <id> - Show detailed skill information\n search <query> - Search skills by keyword\n use <id> - Switch to a specific skill\n\nExamples:\n /skill list\n /skill show repository_analyst\n /skill search git\n /skill use repository_analyst",
1419
1488
  action: :none
1420
1489
  }
1421
1490
  end
@@ -9,9 +9,9 @@ module Aidp
9
9
  module Harness
10
10
  # Enhanced configuration loader for harness
11
11
  class ConfigLoader
12
- def initialize(project_dir = Dir.pwd)
12
+ def initialize(project_dir = Dir.pwd, validator: nil)
13
13
  @project_dir = project_dir
14
- @validator = ConfigValidator.new(project_dir)
14
+ @validator = validator || ConfigValidator.new(project_dir)
15
15
  @config_cache = nil
16
16
  @last_loaded = nil
17
17
  @last_signature = nil # stores {mtime:, size:, hash:}
@@ -256,7 +256,7 @@ module Aidp
256
256
  return unless @config_file
257
257
 
258
258
  begin
259
- @config = YAML.load_file(@config_file) || {}
259
+ @config = YAML.safe_load_file(@config_file, permitted_classes: [Date, Time, Symbol], aliases: true) || {}
260
260
  rescue => e
261
261
  @config = {}
262
262
  @validation_result = {
@@ -22,10 +22,19 @@ module Aidp
22
22
  error: "error"
23
23
  }.freeze
24
24
 
25
- def initialize(project_dir, mode = :analyze, options = {})
25
+ # Simple sleeper abstraction for test control
26
+ class Sleeper
27
+ def sleep(duration)
28
+ Kernel.sleep(duration)
29
+ end
30
+ end
31
+
32
+ def initialize(project_dir, mode = :analyze, options = {}, prompt: TTY::Prompt.new, sleeper: Sleeper.new)
26
33
  @project_dir = project_dir
27
34
  @mode = mode.to_sym
28
35
  @options = options
36
+ @prompt = prompt
37
+ @sleeper = sleeper
29
38
  @state = STATES[:idle]
30
39
  @start_time = nil
31
40
  @current_step = nil
@@ -50,7 +59,7 @@ module Aidp
50
59
  @configuration = Configuration.new(project_dir)
51
60
  @state_manager = StateManager.new(project_dir, @mode)
52
61
  @condition_detector = ConditionDetector.new
53
- @provider_manager = ProviderManager.new(@configuration, prompt: TTY::Prompt.new)
62
+ @provider_manager = ProviderManager.new(@configuration, prompt: @prompt)
54
63
  @error_handler = ErrorHandler.new(@provider_manager, @configuration)
55
64
  @completion_checker = CompletionChecker.new(@project_dir, @workflow_type)
56
65
  end
@@ -240,7 +249,7 @@ module Aidp
240
249
  # Remove job after a delay to show completion
241
250
  # UI delay to let user see completion status before removal
242
251
  Thread.new do
243
- sleep 2 # Acceptable for UI timing
252
+ @sleeper.sleep(2) # UI timing delay
244
253
  @tui.remove_job(step_job_id)
245
254
  end
246
255
 
@@ -349,9 +358,9 @@ module Aidp
349
358
  def get_mode_runner
350
359
  case @mode
351
360
  when :analyze
352
- Aidp::Analyze::Runner.new(@project_dir, self, prompt: TTY::Prompt.new)
361
+ Aidp::Analyze::Runner.new(@project_dir, self, prompt: @prompt)
353
362
  when :execute
354
- Aidp::Execute::Runner.new(@project_dir, self, prompt: TTY::Prompt.new)
363
+ Aidp::Execute::Runner.new(@project_dir, self, prompt: @prompt)
355
364
  else
356
365
  raise ArgumentError, "Unsupported mode: #{@mode}"
357
366
  end
@@ -376,7 +385,7 @@ module Aidp
376
385
  def handle_pause_condition
377
386
  case @state
378
387
  when STATES[:paused]
379
- sleep(1)
388
+ @sleeper.sleep(1)
380
389
  when STATES[:waiting_for_user]
381
390
  # User interface handles this
382
391
  nil
@@ -503,7 +512,7 @@ module Aidp
503
512
  while Time.now < reset_time && @state == STATES[:waiting_for_rate_limit]
504
513
  remaining = reset_time - Time.now
505
514
  @tui.show_message("⏳ Rate limit reset in #{remaining.to_i} seconds", :info)
506
- sleep(1)
515
+ @sleeper.sleep(1)
507
516
  end
508
517
  end
509
518
 
@@ -10,10 +10,19 @@ module Aidp
10
10
  class ErrorHandler
11
11
  include Aidp::DebugMixin
12
12
 
13
- def initialize(provider_manager, configuration, metrics_manager = nil)
13
+ # Simple wrapper to allow dependency injection of sleep behavior in tests
14
+ class Sleeper
15
+ def sleep(seconds)
16
+ Kernel.sleep(seconds)
17
+ end
18
+ end
19
+
20
+ # @param sleeper [#sleep] object responding to sleep(seconds); injectable for tests
21
+ def initialize(provider_manager, configuration, metrics_manager = nil, sleeper: nil)
14
22
  @provider_manager = provider_manager
15
23
  @configuration = configuration
16
24
  @metrics_manager = metrics_manager
25
+ @sleeper = sleeper || Sleeper.new
17
26
  @retry_strategies = {}
18
27
  @retry_counts = {}
19
28
  @error_history = []
@@ -112,7 +121,7 @@ module Aidp
112
121
  if should_retry?(error_info, strategy)
113
122
  delay = @backoff_calculator.calculate_delay(attempt, strategy[:backoff_strategy] || :exponential, 1, 10)
114
123
  debug_log("🔁 Retry attempt #{attempt} for #{current_provider}", level: :info, data: {delay: delay, error_type: error_info[:error_type]})
115
- sleep(delay) if delay > 0
124
+ @sleeper.sleep(delay) if delay > 0
116
125
  retry
117
126
  end
118
127
  end
@@ -191,9 +200,7 @@ module Aidp
191
200
  )
192
201
 
193
202
  # Wait for backoff delay
194
- if delay > 0
195
- sleep(delay)
196
- end
203
+ @sleeper.sleep(delay) if delay > 0
197
204
 
198
205
  # Execute the retry
199
206
  retry_result = execute_retry_attempt(error_info, strategy, context)
@@ -12,9 +12,10 @@ module Aidp
12
12
  include Aidp::MessageDisplay
13
13
  include Aidp::RescueLogging
14
14
 
15
- def initialize(configuration, prompt: TTY::Prompt.new)
15
+ def initialize(configuration, prompt: TTY::Prompt.new, binary_checker: Aidp::Util)
16
16
  @configuration = configuration
17
17
  @prompt = prompt
18
+ @binary_checker = binary_checker
18
19
  @current_provider = nil
19
20
  @current_model = nil
20
21
  @provider_history = []
@@ -1069,18 +1070,6 @@ module Aidp
1069
1070
  def provider_cli_available?(provider_name)
1070
1071
  normalized = normalize_provider_name(provider_name)
1071
1072
 
1072
- # Handle test environment overrides
1073
- if defined?(RSpec) || ENV["RSPEC_RUNNING"]
1074
- # Force claude to be missing for testing
1075
- if ENV["AIDP_FORCE_CLAUDE_MISSING"] == "1" && normalized == "claude"
1076
- return [false, "binary_missing"]
1077
- end
1078
- # Force claude to be available for testing
1079
- if ENV["AIDP_FORCE_CLAUDE_AVAILABLE"] == "1" && normalized == "claude"
1080
- return [true, "available"]
1081
- end
1082
- end
1083
-
1084
1073
  cache_key = "#{provider_name}:#{normalized}"
1085
1074
  cached = @binary_check_cache[cache_key]
1086
1075
  if cached && (Time.now - cached[:checked_at] < @binary_check_ttl)
@@ -1098,7 +1087,7 @@ module Aidp
1098
1087
  return [true, nil]
1099
1088
  end
1100
1089
  path = begin
1101
- Aidp::Util.which(binary)
1090
+ @binary_checker.which(binary)
1102
1091
  rescue => e
1103
1092
  log_rescue(e, component: "provider_manager", action: "locate_binary", fallback: nil, binary: binary)
1104
1093
  nil
@@ -1164,10 +1153,6 @@ module Aidp
1164
1153
  statuses = provider_health_status
1165
1154
  metrics = all_metrics
1166
1155
  configured = configured_providers
1167
- # Ensure fresh binary probe results in test mode so stubs of Aidp::Util.which take effect
1168
- if defined?(RSpec) || ENV["RSPEC_RUNNING"]
1169
- @binary_check_cache.clear
1170
- end
1171
1156
  rows_by_normalized = {}
1172
1157
  configured.each do |prov|
1173
1158
  # Temporarily hide macos provider until it's user-configurable
@@ -1384,7 +1369,7 @@ module Aidp
1384
1369
  @current_provider = provider_type
1385
1370
 
1386
1371
  # Execute the prompt with the provider
1387
- result = provider.send(prompt: prompt, session: nil)
1372
+ result = provider.send_message(prompt: prompt, session: nil)
1388
1373
 
1389
1374
  # Return structured result
1390
1375
  {
@@ -188,9 +188,9 @@ module Aidp
188
188
  def get_mode_runner
189
189
  case @mode
190
190
  when :analyze
191
- Aidp::Analyze::Runner.new(@project_dir, self, prompt: TTY::Prompt.new)
191
+ Aidp::Analyze::Runner.new(@project_dir, self, prompt: @prompt)
192
192
  when :execute
193
- Aidp::Execute::Runner.new(@project_dir, self, prompt: TTY::Prompt.new)
193
+ Aidp::Execute::Runner.new(@project_dir, self, prompt: @prompt)
194
194
  else
195
195
  raise ArgumentError, "Unsupported mode: #{@mode}"
196
196
  end
@@ -8,22 +8,25 @@ module Aidp
8
8
  module State
9
9
  # Handles file I/O and persistence for state management
10
10
  class Persistence
11
- def initialize(project_dir, mode)
11
+ def initialize(project_dir, mode, skip_persistence: false)
12
12
  @project_dir = project_dir
13
13
  @mode = mode
14
14
  @state_dir = File.join(project_dir, ".aidp", "harness")
15
15
  @state_file = File.join(@state_dir, "#{mode}_state.json")
16
16
  @lock_file = File.join(@state_dir, "#{mode}_state.lock")
17
+ # Use explicit skip_persistence flag for dependency injection
18
+ # Callers should set skip_persistence: true for test/dry-run scenarios
19
+ @skip_persistence = skip_persistence
17
20
  ensure_state_directory
18
21
  end
19
22
 
20
23
  def has_state?
21
- return false if test_mode?
24
+ return false if @skip_persistence
22
25
  File.exist?(@state_file)
23
26
  end
24
27
 
25
28
  def load_state
26
- return {} if test_mode? || !has_state?
29
+ return {} if @skip_persistence || !has_state?
27
30
 
28
31
  with_lock do
29
32
  content = File.read(@state_file)
@@ -35,7 +38,7 @@ module Aidp
35
38
  end
36
39
 
37
40
  def save_state(state_data)
38
- return if test_mode?
41
+ return if @skip_persistence
39
42
 
40
43
  with_lock do
41
44
  state_with_metadata = add_metadata(state_data)
@@ -44,7 +47,7 @@ module Aidp
44
47
  end
45
48
 
46
49
  def clear_state
47
- return if test_mode?
50
+ return if @skip_persistence
48
51
 
49
52
  with_lock do
50
53
  File.delete(@state_file) if File.exist?(@state_file)
@@ -53,10 +56,6 @@ module Aidp
53
56
 
54
57
  private
55
58
 
56
- def test_mode?
57
- ENV["RACK_ENV"] == "test" || defined?(RSpec)
58
- end
59
-
60
59
  def add_metadata(state_data)
61
60
  state_data.merge(
62
61
  mode: @mode,
@@ -76,7 +75,7 @@ module Aidp
76
75
  end
77
76
 
78
77
  def with_lock(&block)
79
- return yield if test_mode?
78
+ return yield if @skip_persistence
80
79
 
81
80
  acquire_lock_with_timeout(&block)
82
81
  ensure
@@ -10,11 +10,12 @@ module Aidp
10
10
  module State
11
11
  # Manages workflow-specific state and progress tracking
12
12
  class WorkflowState
13
- def initialize(persistence, project_dir, mode)
13
+ def initialize(persistence, project_dir, mode, progress_tracker_factory: nil)
14
14
  @persistence = persistence
15
15
  @project_dir = project_dir
16
16
  @mode = mode
17
- @progress_tracker = create_progress_tracker
17
+ @progress_tracker_factory = progress_tracker_factory
18
+ @progress_tracker = @progress_tracker_factory ? @progress_tracker_factory.call : create_progress_tracker
18
19
  end
19
20
 
20
21
  def completed_steps