aidp 0.32.0 → 0.34.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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/lib/aidp/analyze/feature_analyzer.rb +322 -320
  4. data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
  5. data/lib/aidp/auto_update/coordinator.rb +97 -7
  6. data/lib/aidp/auto_update.rb +0 -12
  7. data/lib/aidp/cli/devcontainer_commands.rb +0 -5
  8. data/lib/aidp/cli/eval_command.rb +399 -0
  9. data/lib/aidp/cli/harness_command.rb +1 -1
  10. data/lib/aidp/cli/security_command.rb +416 -0
  11. data/lib/aidp/cli/tools_command.rb +6 -4
  12. data/lib/aidp/cli.rb +172 -4
  13. data/lib/aidp/comment_consolidator.rb +78 -0
  14. data/lib/aidp/concurrency/exec.rb +3 -0
  15. data/lib/aidp/concurrency.rb +0 -3
  16. data/lib/aidp/config.rb +113 -1
  17. data/lib/aidp/config_paths.rb +91 -0
  18. data/lib/aidp/daemon/runner.rb +8 -4
  19. data/lib/aidp/errors.rb +134 -0
  20. data/lib/aidp/evaluations/context_capture.rb +205 -0
  21. data/lib/aidp/evaluations/evaluation_record.rb +114 -0
  22. data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
  23. data/lib/aidp/evaluations.rb +23 -0
  24. data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
  25. data/lib/aidp/execute/interactive_repl.rb +6 -2
  26. data/lib/aidp/execute/prompt_evaluator.rb +359 -0
  27. data/lib/aidp/execute/repl_macros.rb +100 -1
  28. data/lib/aidp/execute/work_loop_runner.rb +719 -58
  29. data/lib/aidp/execute/work_loop_state.rb +4 -1
  30. data/lib/aidp/execute/workflow_selector.rb +3 -0
  31. data/lib/aidp/harness/ai_decision_engine.rb +79 -0
  32. data/lib/aidp/harness/ai_filter_factory.rb +285 -0
  33. data/lib/aidp/harness/capability_registry.rb +2 -0
  34. data/lib/aidp/harness/condition_detector.rb +3 -0
  35. data/lib/aidp/harness/config_loader.rb +3 -0
  36. data/lib/aidp/harness/config_schema.rb +97 -1
  37. data/lib/aidp/harness/config_validator.rb +1 -1
  38. data/lib/aidp/harness/configuration.rb +61 -5
  39. data/lib/aidp/harness/enhanced_runner.rb +14 -11
  40. data/lib/aidp/harness/error_handler.rb +3 -0
  41. data/lib/aidp/harness/filter_definition.rb +212 -0
  42. data/lib/aidp/harness/generated_filter_strategy.rb +197 -0
  43. data/lib/aidp/harness/output_filter.rb +50 -25
  44. data/lib/aidp/harness/output_filter_config.rb +129 -0
  45. data/lib/aidp/harness/provider_factory.rb +3 -0
  46. data/lib/aidp/harness/provider_manager.rb +96 -2
  47. data/lib/aidp/harness/runner.rb +5 -12
  48. data/lib/aidp/harness/state/persistence.rb +3 -0
  49. data/lib/aidp/harness/state_manager.rb +3 -0
  50. data/lib/aidp/harness/status_display.rb +28 -20
  51. data/lib/aidp/harness/test_runner.rb +179 -41
  52. data/lib/aidp/harness/thinking_depth_manager.rb +44 -28
  53. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
  54. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
  55. data/lib/aidp/harness/ui/error_handler.rb +3 -0
  56. data/lib/aidp/harness/ui/job_monitor.rb +4 -0
  57. data/lib/aidp/harness/ui/navigation/submenu.rb +2 -2
  58. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
  59. data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
  60. data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
  61. data/lib/aidp/harness/user_interface.rb +3 -0
  62. data/lib/aidp/loader.rb +195 -0
  63. data/lib/aidp/logger.rb +3 -0
  64. data/lib/aidp/message_display.rb +31 -0
  65. data/lib/aidp/metadata/compiler.rb +29 -17
  66. data/lib/aidp/metadata/query.rb +1 -1
  67. data/lib/aidp/metadata/scanner.rb +8 -1
  68. data/lib/aidp/metadata/tool_metadata.rb +13 -13
  69. data/lib/aidp/metadata/validator.rb +10 -0
  70. data/lib/aidp/metadata.rb +16 -0
  71. data/lib/aidp/pr_worktree_manager.rb +20 -8
  72. data/lib/aidp/provider_manager.rb +4 -7
  73. data/lib/aidp/providers/base.rb +2 -0
  74. data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
  75. data/lib/aidp/security/secrets_proxy.rb +328 -0
  76. data/lib/aidp/security/secrets_registry.rb +227 -0
  77. data/lib/aidp/security/trifecta_state.rb +220 -0
  78. data/lib/aidp/security/watch_mode_handler.rb +306 -0
  79. data/lib/aidp/security/work_loop_adapter.rb +277 -0
  80. data/lib/aidp/security.rb +56 -0
  81. data/lib/aidp/setup/wizard.rb +283 -11
  82. data/lib/aidp/skills.rb +0 -5
  83. data/lib/aidp/storage/csv_storage.rb +3 -0
  84. data/lib/aidp/style_guide/selector.rb +360 -0
  85. data/lib/aidp/tooling_detector.rb +283 -16
  86. data/lib/aidp/version.rb +1 -1
  87. data/lib/aidp/watch/auto_merger.rb +274 -0
  88. data/lib/aidp/watch/auto_pr_processor.rb +125 -7
  89. data/lib/aidp/watch/build_processor.rb +16 -1
  90. data/lib/aidp/watch/change_request_processor.rb +682 -150
  91. data/lib/aidp/watch/ci_fix_processor.rb +262 -4
  92. data/lib/aidp/watch/feedback_collector.rb +191 -0
  93. data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
  94. data/lib/aidp/watch/implementation_verifier.rb +142 -1
  95. data/lib/aidp/watch/plan_generator.rb +70 -13
  96. data/lib/aidp/watch/plan_processor.rb +12 -5
  97. data/lib/aidp/watch/projects_processor.rb +286 -0
  98. data/lib/aidp/watch/repository_client.rb +871 -22
  99. data/lib/aidp/watch/review_processor.rb +33 -6
  100. data/lib/aidp/watch/runner.rb +80 -29
  101. data/lib/aidp/watch/state_store.rb +233 -0
  102. data/lib/aidp/watch/sub_issue_creator.rb +221 -0
  103. data/lib/aidp/watch.rb +5 -7
  104. data/lib/aidp/workflows/guided_agent.rb +4 -0
  105. data/lib/aidp/workstream_cleanup.rb +0 -2
  106. data/lib/aidp/workstream_executor.rb +3 -4
  107. data/lib/aidp/worktree.rb +61 -12
  108. data/lib/aidp/worktree_branch_manager.rb +347 -101
  109. data/lib/aidp.rb +21 -106
  110. data/templates/implementation/iterative_implementation.md +46 -3
  111. metadata +91 -36
  112. data/lib/aidp/config/paths.rb +0 -131
data/lib/aidp/cli.rb CHANGED
@@ -2,12 +2,14 @@
2
2
 
3
3
  require "optparse"
4
4
  require "tty-prompt"
5
+ require "stringio"
5
6
  require_relative "harness/runner"
6
7
  require_relative "execute/workflow_selector"
7
8
  require_relative "harness/ui/enhanced_tui"
8
9
  require_relative "harness/ui/enhanced_workflow_selector"
9
10
  require_relative "harness/enhanced_runner"
10
11
  require_relative "cli/first_run_wizard"
12
+ require_relative "cli/issue_importer"
11
13
  require_relative "rescue_logging"
12
14
  require_relative "concurrency"
13
15
 
@@ -45,6 +47,18 @@ module Aidp
45
47
  options = parse_options(args)
46
48
  self.last_options = options
47
49
 
50
+ # Validate incompatible options
51
+ if options[:quiet] && options[:verbose]
52
+ display_message("❌ --quiet and --verbose are mutually exclusive", type: :error)
53
+ return 1
54
+ end
55
+
56
+ # --quiet is incompatible with default interactive mode (no subcommand)
57
+ if options[:quiet] && !options[:help] && !options[:version]
58
+ display_message("❌ --quiet is not compatible with interactive mode. Use with 'watch' command instead.", type: :error)
59
+ return 1
60
+ end
61
+
48
62
  if options[:help]
49
63
  display_message(options[:parser].to_s, type: :info)
50
64
  return 0
@@ -55,6 +69,12 @@ module Aidp
55
69
  return 0
56
70
  end
57
71
 
72
+ # Undocumented: Launch test mode for CI/CD validation
73
+ # Initializes app components and exits cleanly without running full workflows
74
+ if options[:launch_test]
75
+ return run_launch_test(:interactive)
76
+ end
77
+
58
78
  # Initialize logger from aidp.yml config
59
79
  # Priority: ENV variable > aidp.yml > default (info)
60
80
  setup_logging(Dir.pwd)
@@ -146,6 +166,117 @@ module Aidp
146
166
  Aidp.logger.warn("cli", "Failed to load logging config, using defaults", error: e.message)
147
167
  end
148
168
 
169
+ # Quick exit launch test for CI/CD validation
170
+ # Initializes app components and exits cleanly without running full workflows
171
+ def run_launch_test(mode)
172
+ Aidp.log_debug("cli", "launch_test_started", mode: mode)
173
+ display_message("Aidp version #{Aidp::VERSION}", type: :info)
174
+
175
+ # Initialize logging
176
+ setup_logging(Dir.pwd)
177
+
178
+ case mode
179
+ when :interactive
180
+ run_interactive_launch_test
181
+ when :watch
182
+ run_watch_launch_test
183
+ else
184
+ display_message("Unknown launch test mode: #{mode}", type: :error)
185
+ return 1
186
+ end
187
+
188
+ Aidp.log_info("cli", "launch_test_completed", mode: mode)
189
+ display_message("Launch test completed successfully", type: :success)
190
+ 0
191
+ rescue => e
192
+ log_rescue(e, component: "cli", action: "launch_test", fallback: 1, mode: mode)
193
+ display_message("Launch test failed: #{e.message}", type: :error)
194
+ 1
195
+ end
196
+
197
+ def run_interactive_launch_test
198
+ Aidp.log_debug("cli", "interactive_launch_test", step: "init_tui")
199
+
200
+ # Initialize TUI components (validates they can be created)
201
+ tui = Aidp::Harness::UI::EnhancedTUI.new
202
+ Aidp.log_debug("cli", "interactive_launch_test", step: "tui_created")
203
+
204
+ # Initialize workflow selector (validates harness loading)
205
+ _selector = Aidp::Harness::UI::EnhancedWorkflowSelector.new(tui, project_dir: Dir.pwd)
206
+ Aidp.log_debug("cli", "interactive_launch_test", step: "workflow_selector_created")
207
+
208
+ # Initialize config manager (validates config loading)
209
+ _config_manager = Aidp::Harness::ConfigManager.new(Dir.pwd)
210
+ Aidp.log_debug("cli", "interactive_launch_test", step: "config_manager_created")
211
+
212
+ # Validate EnhancedRunner can be instantiated (orchestrates workflows)
213
+ Aidp.log_debug("cli", "interactive_launch_test", step: "validate_enhanced_runner")
214
+ require_relative "harness/enhanced_runner"
215
+ _runner = Aidp::Harness::EnhancedRunner.new(Dir.pwd, :execute, {mode: :execute})
216
+ Aidp.log_debug("cli", "interactive_launch_test", step: "enhanced_runner_created")
217
+ display_message("Enhanced Runner instantiation verified", type: :info)
218
+
219
+ # Validate FirstRunWizard can be loaded (critical for setup)
220
+ Aidp.log_debug("cli", "interactive_launch_test", step: "validate_first_run_wizard")
221
+ require_relative "cli/first_run_wizard"
222
+ # Don't instantiate to avoid triggering actual wizard
223
+ Aidp.log_debug("cli", "interactive_launch_test", step: "first_run_wizard_loaded")
224
+ display_message("First Run Wizard loaded", type: :info)
225
+
226
+ # Validate Init::Runner can be instantiated (init command)
227
+ Aidp.log_debug("cli", "interactive_launch_test", step: "validate_init_runner")
228
+ require_relative "init/runner"
229
+ mock_prompt = TTY::Prompt.new(input: StringIO.new, output: StringIO.new)
230
+ _init_runner = Aidp::Init::Runner.new(Dir.pwd, prompt: mock_prompt, options: {dry_run: true})
231
+ Aidp.log_debug("cli", "interactive_launch_test", step: "init_runner_created")
232
+ display_message("Init Runner instantiation verified", type: :info)
233
+
234
+ display_message("Interactive mode initialization verified", type: :info)
235
+ ensure
236
+ tui&.restore_screen
237
+ end
238
+
239
+ def run_watch_launch_test
240
+ Aidp.log_debug("cli", "watch_launch_test", step: "init_config")
241
+
242
+ # Load config to validate configuration parsing
243
+ config_manager = Aidp::Harness::ConfigManager.new(Dir.pwd)
244
+ config = config_manager.config || {}
245
+ watch_config = config[:watch] || config["watch"] || {}
246
+
247
+ Aidp.log_debug("cli", "watch_launch_test", step: "config_loaded", has_watch_config: !watch_config.empty?)
248
+
249
+ display_message("Watch mode configuration verified", type: :info)
250
+
251
+ # Instantiate Runner to validate all dependencies are loadable
252
+ # Use mock GitHub client to avoid external API calls
253
+ Aidp.log_debug("cli", "watch_launch_test", step: "validate_runner")
254
+ mock_gh_client = Class.new do
255
+ def available?
256
+ false
257
+ end
258
+ end.new
259
+
260
+ Aidp::Watch::Runner.new(
261
+ issues_url: "https://github.com/test/test/issues",
262
+ interval: 30,
263
+ once: true,
264
+ gh_available: mock_gh_client,
265
+ prompt: TTY::Prompt.new(input: StringIO.new, output: StringIO.new)
266
+ )
267
+
268
+ Aidp.log_debug("cli", "watch_launch_test", step: "runner_instantiated")
269
+ display_message("Watch mode Runner instantiation verified", type: :info)
270
+
271
+ # Validate key Watch mode dependencies are loadable
272
+ Aidp.log_debug("cli", "watch_launch_test", step: "validate_watch_dependencies")
273
+ require_relative "watch/plan_generator"
274
+ require_relative "auto_update"
275
+ require_relative "worktree"
276
+ Aidp.log_debug("cli", "watch_launch_test", step: "watch_dependencies_loaded")
277
+ display_message("Watch mode dependencies loaded", type: :info)
278
+ end
279
+
149
280
  def parse_options(args)
150
281
  options = {}
151
282
 
@@ -216,6 +347,9 @@ module Aidp
216
347
  opts.on("-v", "--version", "Show version information") { options[:version] = true }
217
348
  opts.on("--setup-config", "Setup or reconfigure config file") { options[:setup_config] = true }
218
349
  opts.on("--verbose", "Show detailed prompts and raw provider responses during guided workflow") { options[:verbose] = true }
350
+ opts.on("--quiet", "Suppress non-critical output (incompatible with --verbose and --interactive)") { options[:quiet] = true }
351
+ # Undocumented: Quick exit launch test for CI/CD validation
352
+ opts.on("--launch-test", nil) { options[:launch_test] = true }
219
353
 
220
354
  opts.separator ""
221
355
  opts.separator "Examples:"
@@ -256,7 +390,7 @@ module Aidp
256
390
  # Determine if the invocation is a subcommand style call
257
391
  def subcommand?(args)
258
392
  return false if args.nil? || args.empty?
259
- %w[status jobs kb harness providers checkpoint mcp issue config init watch ws work skill settings models tools].include?(args.first)
393
+ %w[status jobs kb harness providers checkpoint eval mcp issue config init watch ws work skill settings models tools security].include?(args.first)
260
394
  end
261
395
 
262
396
  def run_subcommand(args)
@@ -268,6 +402,7 @@ module Aidp
268
402
  when "harness" then run_harness_command(args)
269
403
  when "providers" then run_providers_command(args)
270
404
  when "checkpoint" then run_checkpoint_command(args)
405
+ when "eval" then run_eval_command(args)
271
406
  when "mcp" then run_mcp_command(args)
272
407
  when "issue" then run_issue_command(args)
273
408
  when "config" then run_config_command(args)
@@ -280,6 +415,7 @@ module Aidp
280
415
  when "settings" then run_settings_command(args)
281
416
  when "models" then run_models_command(args)
282
417
  when "tools" then run_tools_command(args)
418
+ when "security" then run_security_command(args)
283
419
  else
284
420
  display_message("Unknown command: #{cmd}", type: :info)
285
421
  return 1
@@ -422,6 +558,13 @@ module Aidp
422
558
  command.run(args)
423
559
  end
424
560
 
561
+ def run_eval_command(args)
562
+ # Delegate to EvalCommand
563
+ require_relative "cli/eval_command"
564
+ command = EvalCommand.new(prompt: create_prompt)
565
+ command.run(args)
566
+ end
567
+
425
568
  def format_time_ago_simple(seconds)
426
569
  if seconds < 60
427
570
  "#{seconds.to_i}s ago"
@@ -632,6 +775,12 @@ module Aidp
632
775
  tools_cmd.run(args)
633
776
  end
634
777
 
778
+ def run_security_command(args)
779
+ require_relative "cli/security_command"
780
+ security_cmd = Aidp::CLI::SecurityCommand.new(project_dir: Dir.pwd, prompt: create_prompt)
781
+ security_cmd.run(args)
782
+ end
783
+
635
784
  def run_issue_command(args)
636
785
  require_relative "cli/issue_importer"
637
786
 
@@ -669,7 +818,7 @@ module Aidp
669
818
  return
670
819
  end
671
820
 
672
- importer = IssueImporter.new
821
+ importer = Aidp::IssueImporter.new
673
822
  issue_data = importer.import_issue(identifier)
674
823
 
675
824
  if issue_data
@@ -825,7 +974,7 @@ module Aidp
825
974
 
826
975
  def run_watch_command(args)
827
976
  if args.empty?
828
- display_message("Usage: aidp watch <issues_url> [--interval SECONDS] [--provider NAME] [--once] [--no-workstreams] [--force] [--verbose]", type: :info)
977
+ display_message("Usage: aidp watch <issues_url> [--interval SECONDS] [--provider NAME] [--once] [--no-workstreams] [--force] [--verbose] [--quiet]", type: :info)
829
978
  return
830
979
  end
831
980
 
@@ -836,6 +985,8 @@ module Aidp
836
985
  use_workstreams = true # Default to using workstreams
837
986
  force = false
838
987
  verbose = false
988
+ quiet = false
989
+ launch_test = false
839
990
 
840
991
  until args.empty?
841
992
  token = args.shift
@@ -853,11 +1004,27 @@ module Aidp
853
1004
  force = true
854
1005
  when "--verbose"
855
1006
  verbose = true
1007
+ when "--quiet"
1008
+ quiet = true
1009
+ when "--launch-test"
1010
+ launch_test = true
856
1011
  else
857
1012
  display_message("⚠️ Unknown watch option: #{token}", type: :warn)
858
1013
  end
859
1014
  end
860
1015
 
1016
+ # Validate incompatible options
1017
+ if quiet && verbose
1018
+ display_message("❌ --quiet and --verbose are mutually exclusive", type: :error)
1019
+ return 1
1020
+ end
1021
+
1022
+ # Undocumented: Launch test mode for CI/CD validation
1023
+ # Exits after validating watch mode initialization
1024
+ if launch_test
1025
+ return run_launch_test(:watch)
1026
+ end
1027
+
861
1028
  # Initialize logger for watch mode
862
1029
  setup_logging(Dir.pwd)
863
1030
 
@@ -876,7 +1043,8 @@ module Aidp
876
1043
  prompt: create_prompt,
877
1044
  safety_config: watch_config,
878
1045
  force: force,
879
- verbose: verbose
1046
+ verbose: verbose,
1047
+ quiet: quiet
880
1048
  )
881
1049
  runner.start
882
1050
  rescue ArgumentError => e
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ # Consolidates comments for GitHub issues and PRs by category
5
+ class CommentConsolidator
6
+ CATEGORY_HEADERS = {
7
+ progress: "## 🔄 Progress Report",
8
+ exceptions: "## 🚨 Exceptions and Errors",
9
+ completion: "## ✅ Completion Summary"
10
+ }
11
+
12
+ # @param repository_client [Aidp::Watch::RepositoryClient] GitHub repository client
13
+ # @param number [Integer] Issue or PR number
14
+ def initialize(repository_client:, number:)
15
+ @client = repository_client
16
+ @number = number
17
+ end
18
+
19
+ # Search for an existing comment by its category header
20
+ # @param category [Symbol] Comment category (:progress, :exceptions, :completion)
21
+ # @return [Hash, nil] Existing comment or nil if not found
22
+ def find_category_comment(category)
23
+ Aidp.log_debug("comment_consolidator", "searching_category_comment",
24
+ number: @number, category: category)
25
+
26
+ header = CATEGORY_HEADERS[category]
27
+ raise ArgumentError, "Invalid category: #{category}" unless header
28
+
29
+ comment = @client.find_comment(@number, header)
30
+ Aidp.log_debug("comment_consolidator", "find_category_comment_result",
31
+ found: !comment.nil?)
32
+ comment
33
+ end
34
+
35
+ # Update an existing category comment or create a new one
36
+ # @param category [Symbol] Comment category (:progress, :exceptions, :completion)
37
+ # @param new_content [String] New content to add to the comment
38
+ # @param append [Boolean] Whether to append or replace existing content
39
+ # @return [String] Result of comment operation (comment ID or response body)
40
+ def consolidate_comment(category:, new_content:, append: true)
41
+ Aidp.log_debug("comment_consolidator", "consolidating_comment", number: @number, category: category, append: append)
42
+
43
+ header = CATEGORY_HEADERS[category]
44
+ raise ArgumentError, "Invalid category: #{category}" unless header
45
+
46
+ existing_comment = find_category_comment(category)
47
+
48
+ content = if existing_comment && append
49
+ # Append new content with timestamp
50
+ existing_body = existing_comment[:body]
51
+ updated_body = if existing_body.include?(header)
52
+ existing_body.lines.first(1).join +
53
+ "### #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}\n\n" +
54
+ new_content + "\n\n" +
55
+ existing_body.lines[1..]&.join
56
+ else
57
+ # Reconstruct comment if header is missing
58
+ "#{header}\n\n### #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}\n\n" +
59
+ new_content + "\n\n" +
60
+ existing_body
61
+ end
62
+ updated_body
63
+ else
64
+ # Create new or replace content
65
+ "#{header}\n\n### #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}\n\n" + new_content
66
+ end
67
+
68
+ # Update or create comment
69
+ if existing_comment
70
+ Aidp.log_debug("comment_consolidator", "updating_existing_comment", comment_id: existing_comment[:id])
71
+ @client.update_comment(existing_comment[:id], content)
72
+ else
73
+ Aidp.log_debug("comment_consolidator", "creating_new_comment")
74
+ @client.post_comment(@number, content)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -21,6 +21,9 @@ module Aidp
21
21
  # Exec.shutdown_all
22
22
  module Exec
23
23
  class << self
24
+ # Expose for testability - reset pool cache between tests
25
+ attr_writer :pools, :default_pool
26
+
24
27
  # Get or create a named thread pool.
25
28
  #
26
29
  # Pools are cached by name. Calling this method multiple times with the
@@ -1,9 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "concurrent-ruby"
4
- require_relative "concurrency/wait"
5
- require_relative "concurrency/backoff"
6
- require_relative "concurrency/exec"
7
4
 
8
5
  module Aidp
9
6
  # Concurrency utilities for deterministic waiting, retry/backoff, and executor management.
data/lib/aidp/config.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
- require_relative "config/paths"
5
4
 
6
5
  module Aidp
7
6
  # Configuration management for both execute and analyze modes
@@ -171,6 +170,64 @@ module Aidp
171
170
  method: "zfc_automatic",
172
171
  allow_parallel: true
173
172
  }
173
+ },
174
+ evaluations: {
175
+ enabled: true,
176
+ prompt_after_work_loop: false,
177
+ capture_full_context: true,
178
+ directory: ".aidp/evaluations"
179
+ },
180
+ security: {
181
+ rule_of_two: {
182
+ enabled: true,
183
+ policy: "strict" # strict or relaxed
184
+ },
185
+ secrets_proxy: {
186
+ enabled: true,
187
+ token_ttl: 300 # seconds
188
+ },
189
+ watch_mode: {
190
+ max_retry_attempts: 3,
191
+ fail_forward_enabled: true,
192
+ needs_input_label: "aidp-needs-input"
193
+ }
194
+ },
195
+ watch: {
196
+ enabled: false,
197
+ polling_interval: 30,
198
+ labels: {
199
+ plan_trigger: "aidp-plan",
200
+ build_trigger: "aidp-build",
201
+ review_trigger: "aidp-review",
202
+ fix_ci_trigger: "aidp-fix-ci",
203
+ change_request_trigger: "aidp-request-changes",
204
+ auto_trigger: "aidp-auto",
205
+ parent_pr: "aidp-parent-pr",
206
+ sub_pr: "aidp-sub-pr"
207
+ },
208
+ projects: {
209
+ enabled: false,
210
+ default_project_id: nil,
211
+ field_mappings: {
212
+ status: "Status",
213
+ priority: "Priority",
214
+ skills: "Skills",
215
+ personas: "Personas",
216
+ blocking: "Blocking"
217
+ },
218
+ auto_create_fields: true,
219
+ sync_interval: 60,
220
+ default_status_values: ["Backlog", "Todo", "In Progress", "In Review", "Done"],
221
+ default_priority_values: ["Low", "Medium", "High", "Critical"]
222
+ },
223
+ auto_merge: {
224
+ enabled: true,
225
+ sub_issue_prs_only: true,
226
+ require_ci_success: true,
227
+ require_reviews: 0,
228
+ merge_method: "squash",
229
+ delete_branch: true
230
+ }
174
231
  }
175
232
  }.freeze
176
233
 
@@ -288,6 +345,38 @@ module Aidp
288
345
  symbolize_keys(tool_metadata_section)
289
346
  end
290
347
 
348
+ # Get evaluations configuration
349
+ def self.evaluations_config(project_dir = Dir.pwd)
350
+ config = load_harness_config(project_dir)
351
+ evaluations_section = config[:evaluations] || config["evaluations"] || {}
352
+
353
+ # Convert string keys to symbols for consistency
354
+ symbolize_keys(evaluations_section)
355
+ end
356
+
357
+ # Get security configuration
358
+ def self.security_config(project_dir = Dir.pwd)
359
+ config = load_harness_config(project_dir)
360
+ security_section = config[:security] || config["security"] || {}
361
+
362
+ # Convert string keys to symbols for consistency
363
+ symbolize_keys(security_section)
364
+ end
365
+
366
+ # Check if Rule of Two enforcement is enabled
367
+ def self.rule_of_two_enabled?(project_dir = Dir.pwd)
368
+ sec_config = security_config(project_dir)
369
+ rule_of_two = sec_config[:rule_of_two] || {}
370
+ rule_of_two.fetch(:enabled, true)
371
+ end
372
+
373
+ # Check if Secrets Proxy is enabled
374
+ def self.secrets_proxy_enabled?(project_dir = Dir.pwd)
375
+ sec_config = security_config(project_dir)
376
+ proxy_config = sec_config[:secrets_proxy] || {}
377
+ proxy_config.fetch(:enabled, true)
378
+ end
379
+
291
380
  # Check if configuration file exists
292
381
  def self.config_exists?(project_dir = Dir.pwd)
293
382
  ConfigPaths.config_exists?(project_dir)
@@ -394,9 +483,32 @@ module Aidp
394
483
  merged[:waterfall] = merged[:waterfall].merge(symbolize_keys(waterfall_section))
395
484
  end
396
485
 
486
+ # Deep merge evaluations config
487
+ if config[:evaluations] || config["evaluations"]
488
+ evaluations_section = config[:evaluations] || config["evaluations"]
489
+ merged[:evaluations] = merged[:evaluations].merge(symbolize_keys(evaluations_section))
490
+ end
491
+
492
+ # Deep merge security config
493
+ if config[:security] || config["security"]
494
+ security_section = config[:security] || config["security"]
495
+ merged[:security] = deep_merge_hash(merged[:security], symbolize_keys(security_section))
496
+ end
497
+
397
498
  merged
398
499
  end
399
500
 
501
+ # Deep merge for nested hashes (preserves nested defaults)
502
+ private_class_method def self.deep_merge_hash(base, override)
503
+ base.merge(override) do |_key, base_val, override_val|
504
+ if base_val.is_a?(Hash) && override_val.is_a?(Hash)
505
+ deep_merge_hash(base_val, override_val)
506
+ else
507
+ override_val
508
+ end
509
+ end
510
+ end
511
+
400
512
  private_class_method def self.symbolize_keys(hash)
401
513
  return hash unless hash.is_a?(Hash)
402
514
 
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Aidp
6
+ # Centralized path management for all AIDP internal files
7
+ # Ensures consistent file locations and prevents path-related bugs
8
+ module ConfigPaths
9
+ def self.aidp_dir(project_dir = Dir.pwd) = File.join(project_dir, ".aidp")
10
+ def self.config_file(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "aidp.yml")
11
+ def self.config_dir(project_dir = Dir.pwd) = aidp_dir(project_dir)
12
+ def self.progress_dir(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "progress")
13
+ def self.execute_progress_file(project_dir = Dir.pwd) = File.join(progress_dir(project_dir), "execute.yml")
14
+ def self.analyze_progress_file(project_dir = Dir.pwd) = File.join(progress_dir(project_dir), "analyze.yml")
15
+ def self.harness_state_dir(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "harness")
16
+ def self.harness_state_file(mode, project_dir = Dir.pwd) = File.join(harness_state_dir(project_dir), "#{mode}_state.json")
17
+ def self.providers_dir(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "providers")
18
+ def self.provider_info_file(provider_name, project_dir = Dir.pwd) = File.join(providers_dir(project_dir), "#{provider_name}_info.yml")
19
+ def self.jobs_dir(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "jobs")
20
+ def self.checkpoint_file(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "checkpoint.yml")
21
+ def self.checkpoint_history_file(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "checkpoint_history.jsonl")
22
+ def self.json_storage_dir(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "json")
23
+ def self.model_cache_dir(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "model_cache")
24
+ def self.work_loop_dir(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "work_loop")
25
+ def self.logs_dir(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "logs")
26
+ def self.evaluations_dir(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "evaluations")
27
+ def self.evaluations_index_file(project_dir = Dir.pwd) = File.join(evaluations_dir(project_dir), "index.json")
28
+
29
+ # Security module paths
30
+ def self.security_dir(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "security")
31
+ def self.secrets_registry_file(project_dir = Dir.pwd) = File.join(security_dir(project_dir), "secrets_registry.json")
32
+ def self.security_audit_log_file(project_dir = Dir.pwd) = File.join(security_dir(project_dir), "audit.jsonl")
33
+ def self.mcp_risk_profile_file(project_dir = Dir.pwd) = File.join(security_dir(project_dir), "mcp_risk_profile.yml")
34
+
35
+ def self.config_exists?(project_dir = Dir.pwd)
36
+ File.exist?(config_file(project_dir))
37
+ end
38
+
39
+ def self.ensure_aidp_dir(project_dir = Dir.pwd)
40
+ dir = aidp_dir(project_dir)
41
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
42
+ dir
43
+ end
44
+
45
+ def self.ensure_config_dir(project_dir = Dir.pwd)
46
+ ensure_aidp_dir(project_dir)
47
+ end
48
+
49
+ def self.ensure_progress_dir(project_dir = Dir.pwd)
50
+ dir = progress_dir(project_dir)
51
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
52
+ dir
53
+ end
54
+
55
+ def self.ensure_harness_state_dir(project_dir = Dir.pwd)
56
+ dir = harness_state_dir(project_dir)
57
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
58
+ dir
59
+ end
60
+
61
+ def self.ensure_providers_dir(project_dir = Dir.pwd)
62
+ dir = providers_dir(project_dir)
63
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
64
+ dir
65
+ end
66
+
67
+ def self.ensure_jobs_dir(project_dir = Dir.pwd)
68
+ dir = jobs_dir(project_dir)
69
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
70
+ dir
71
+ end
72
+
73
+ def self.ensure_json_storage_dir(project_dir = Dir.pwd)
74
+ dir = json_storage_dir(project_dir)
75
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
76
+ dir
77
+ end
78
+
79
+ def self.ensure_evaluations_dir(project_dir = Dir.pwd)
80
+ dir = evaluations_dir(project_dir)
81
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
82
+ dir
83
+ end
84
+
85
+ def self.ensure_security_dir(project_dir = Dir.pwd)
86
+ dir = security_dir(project_dir)
87
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
88
+ dir
89
+ end
90
+ end
91
+ end
@@ -10,15 +10,19 @@ module Aidp
10
10
  # Main daemon runner for background mode execution
11
11
  # Manages work loops, watch mode, and IPC communication
12
12
  class Runner
13
- def initialize(project_dir, config, options = {}, process_manager: nil)
13
+ # Allow reading/writing @running for testability
14
+ attr_accessor :running
15
+
16
+ def initialize(project_dir, config, options = {}, process_manager: nil,
17
+ work_loop_runner: nil, watch_runner: nil, ipc_server: nil)
14
18
  @project_dir = project_dir
15
19
  @config = config
16
20
  @options = options
17
21
  @process_manager = process_manager || ProcessManager.new(project_dir)
18
22
  @running = false
19
- @work_loop_runner = nil
20
- @watch_runner = nil
21
- @ipc_server = nil
23
+ @work_loop_runner = work_loop_runner
24
+ @watch_runner = watch_runner
25
+ @ipc_server = ipc_server
22
26
  end
23
27
 
24
28
  # Start daemon in background