aidp 0.33.0 → 0.34.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
  4. data/lib/aidp/cli/eval_command.rb +399 -0
  5. data/lib/aidp/cli/harness_command.rb +1 -1
  6. data/lib/aidp/cli/security_command.rb +416 -0
  7. data/lib/aidp/cli/tools_command.rb +6 -4
  8. data/lib/aidp/cli.rb +170 -3
  9. data/lib/aidp/concurrency/exec.rb +3 -0
  10. data/lib/aidp/config.rb +113 -0
  11. data/lib/aidp/config_paths.rb +20 -0
  12. data/lib/aidp/daemon/runner.rb +8 -4
  13. data/lib/aidp/errors.rb +134 -0
  14. data/lib/aidp/evaluations/context_capture.rb +205 -0
  15. data/lib/aidp/evaluations/evaluation_record.rb +114 -0
  16. data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
  17. data/lib/aidp/evaluations.rb +23 -0
  18. data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
  19. data/lib/aidp/execute/interactive_repl.rb +6 -2
  20. data/lib/aidp/execute/prompt_evaluator.rb +359 -0
  21. data/lib/aidp/execute/repl_macros.rb +100 -1
  22. data/lib/aidp/execute/work_loop_runner.rb +399 -47
  23. data/lib/aidp/execute/work_loop_state.rb +4 -1
  24. data/lib/aidp/execute/workflow_selector.rb +3 -0
  25. data/lib/aidp/harness/ai_decision_engine.rb +79 -0
  26. data/lib/aidp/harness/capability_registry.rb +2 -0
  27. data/lib/aidp/harness/condition_detector.rb +3 -0
  28. data/lib/aidp/harness/config_loader.rb +3 -0
  29. data/lib/aidp/harness/enhanced_runner.rb +14 -11
  30. data/lib/aidp/harness/error_handler.rb +3 -0
  31. data/lib/aidp/harness/provider_factory.rb +3 -0
  32. data/lib/aidp/harness/provider_manager.rb +6 -0
  33. data/lib/aidp/harness/runner.rb +5 -1
  34. data/lib/aidp/harness/state/persistence.rb +3 -0
  35. data/lib/aidp/harness/state_manager.rb +3 -0
  36. data/lib/aidp/harness/status_display.rb +28 -20
  37. data/lib/aidp/harness/thinking_depth_manager.rb +32 -32
  38. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
  39. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
  40. data/lib/aidp/harness/ui/error_handler.rb +3 -0
  41. data/lib/aidp/harness/ui/job_monitor.rb +4 -0
  42. data/lib/aidp/harness/ui/navigation/submenu.rb +2 -0
  43. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
  44. data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
  45. data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
  46. data/lib/aidp/harness/ui.rb +11 -0
  47. data/lib/aidp/harness/user_interface.rb +3 -0
  48. data/lib/aidp/loader.rb +2 -2
  49. data/lib/aidp/logger.rb +3 -0
  50. data/lib/aidp/message_display.rb +31 -0
  51. data/lib/aidp/pr_worktree_manager.rb +18 -6
  52. data/lib/aidp/provider_manager.rb +3 -0
  53. data/lib/aidp/providers/base.rb +2 -0
  54. data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
  55. data/lib/aidp/security/secrets_proxy.rb +328 -0
  56. data/lib/aidp/security/secrets_registry.rb +227 -0
  57. data/lib/aidp/security/trifecta_state.rb +220 -0
  58. data/lib/aidp/security/watch_mode_handler.rb +306 -0
  59. data/lib/aidp/security/work_loop_adapter.rb +277 -0
  60. data/lib/aidp/security.rb +56 -0
  61. data/lib/aidp/setup/wizard.rb +4 -2
  62. data/lib/aidp/version.rb +1 -1
  63. data/lib/aidp/watch/auto_merger.rb +274 -0
  64. data/lib/aidp/watch/auto_pr_processor.rb +125 -7
  65. data/lib/aidp/watch/build_processor.rb +16 -1
  66. data/lib/aidp/watch/change_request_processor.rb +680 -286
  67. data/lib/aidp/watch/ci_fix_processor.rb +262 -4
  68. data/lib/aidp/watch/feedback_collector.rb +191 -0
  69. data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
  70. data/lib/aidp/watch/implementation_verifier.rb +142 -1
  71. data/lib/aidp/watch/plan_generator.rb +70 -13
  72. data/lib/aidp/watch/plan_processor.rb +12 -5
  73. data/lib/aidp/watch/projects_processor.rb +286 -0
  74. data/lib/aidp/watch/repository_client.rb +861 -53
  75. data/lib/aidp/watch/review_processor.rb +33 -6
  76. data/lib/aidp/watch/runner.rb +51 -11
  77. data/lib/aidp/watch/state_store.rb +233 -0
  78. data/lib/aidp/watch/sub_issue_creator.rb +221 -0
  79. data/lib/aidp/workflows/guided_agent.rb +4 -0
  80. data/lib/aidp/workstream_executor.rb +3 -0
  81. data/lib/aidp/worktree.rb +61 -11
  82. data/lib/aidp/worktree_branch_manager.rb +347 -101
  83. data/templates/implementation/iterative_implementation.md +46 -3
  84. metadata +21 -1
data/lib/aidp/cli.rb CHANGED
@@ -2,6 +2,7 @@
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"
@@ -46,6 +47,18 @@ module Aidp
46
47
  options = parse_options(args)
47
48
  self.last_options = options
48
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
+
49
62
  if options[:help]
50
63
  display_message(options[:parser].to_s, type: :info)
51
64
  return 0
@@ -56,6 +69,12 @@ module Aidp
56
69
  return 0
57
70
  end
58
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
+
59
78
  # Initialize logger from aidp.yml config
60
79
  # Priority: ENV variable > aidp.yml > default (info)
61
80
  setup_logging(Dir.pwd)
@@ -147,6 +166,117 @@ module Aidp
147
166
  Aidp.logger.warn("cli", "Failed to load logging config, using defaults", error: e.message)
148
167
  end
149
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
+
150
280
  def parse_options(args)
151
281
  options = {}
152
282
 
@@ -217,6 +347,9 @@ module Aidp
217
347
  opts.on("-v", "--version", "Show version information") { options[:version] = true }
218
348
  opts.on("--setup-config", "Setup or reconfigure config file") { options[:setup_config] = true }
219
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 }
220
353
 
221
354
  opts.separator ""
222
355
  opts.separator "Examples:"
@@ -257,7 +390,7 @@ module Aidp
257
390
  # Determine if the invocation is a subcommand style call
258
391
  def subcommand?(args)
259
392
  return false if args.nil? || args.empty?
260
- %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)
261
394
  end
262
395
 
263
396
  def run_subcommand(args)
@@ -269,6 +402,7 @@ module Aidp
269
402
  when "harness" then run_harness_command(args)
270
403
  when "providers" then run_providers_command(args)
271
404
  when "checkpoint" then run_checkpoint_command(args)
405
+ when "eval" then run_eval_command(args)
272
406
  when "mcp" then run_mcp_command(args)
273
407
  when "issue" then run_issue_command(args)
274
408
  when "config" then run_config_command(args)
@@ -281,6 +415,7 @@ module Aidp
281
415
  when "settings" then run_settings_command(args)
282
416
  when "models" then run_models_command(args)
283
417
  when "tools" then run_tools_command(args)
418
+ when "security" then run_security_command(args)
284
419
  else
285
420
  display_message("Unknown command: #{cmd}", type: :info)
286
421
  return 1
@@ -423,6 +558,13 @@ module Aidp
423
558
  command.run(args)
424
559
  end
425
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
+
426
568
  def format_time_ago_simple(seconds)
427
569
  if seconds < 60
428
570
  "#{seconds.to_i}s ago"
@@ -633,6 +775,12 @@ module Aidp
633
775
  tools_cmd.run(args)
634
776
  end
635
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
+
636
784
  def run_issue_command(args)
637
785
  require_relative "cli/issue_importer"
638
786
 
@@ -826,7 +974,7 @@ module Aidp
826
974
 
827
975
  def run_watch_command(args)
828
976
  if args.empty?
829
- 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)
830
978
  return
831
979
  end
832
980
 
@@ -837,6 +985,8 @@ module Aidp
837
985
  use_workstreams = true # Default to using workstreams
838
986
  force = false
839
987
  verbose = false
988
+ quiet = false
989
+ launch_test = false
840
990
 
841
991
  until args.empty?
842
992
  token = args.shift
@@ -854,11 +1004,27 @@ module Aidp
854
1004
  force = true
855
1005
  when "--verbose"
856
1006
  verbose = true
1007
+ when "--quiet"
1008
+ quiet = true
1009
+ when "--launch-test"
1010
+ launch_test = true
857
1011
  else
858
1012
  display_message("⚠️ Unknown watch option: #{token}", type: :warn)
859
1013
  end
860
1014
  end
861
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
+
862
1028
  # Initialize logger for watch mode
863
1029
  setup_logging(Dir.pwd)
864
1030
 
@@ -877,7 +1043,8 @@ module Aidp
877
1043
  prompt: create_prompt,
878
1044
  safety_config: watch_config,
879
1045
  force: force,
880
- verbose: verbose
1046
+ verbose: verbose,
1047
+ quiet: quiet
881
1048
  )
882
1049
  runner.start
883
1050
  rescue ArgumentError => e
@@ -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
data/lib/aidp/config.rb CHANGED
@@ -170,6 +170,64 @@ module Aidp
170
170
  method: "zfc_automatic",
171
171
  allow_parallel: true
172
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
+ }
173
231
  }
174
232
  }.freeze
175
233
 
@@ -287,6 +345,38 @@ module Aidp
287
345
  symbolize_keys(tool_metadata_section)
288
346
  end
289
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
+
290
380
  # Check if configuration file exists
291
381
  def self.config_exists?(project_dir = Dir.pwd)
292
382
  ConfigPaths.config_exists?(project_dir)
@@ -393,9 +483,32 @@ module Aidp
393
483
  merged[:waterfall] = merged[:waterfall].merge(symbolize_keys(waterfall_section))
394
484
  end
395
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
+
396
498
  merged
397
499
  end
398
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
+
399
512
  private_class_method def self.symbolize_keys(hash)
400
513
  return hash unless hash.is_a?(Hash)
401
514
 
@@ -23,6 +23,14 @@ module Aidp
23
23
  def self.model_cache_dir(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "model_cache")
24
24
  def self.work_loop_dir(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "work_loop")
25
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")
26
34
 
27
35
  def self.config_exists?(project_dir = Dir.pwd)
28
36
  File.exist?(config_file(project_dir))
@@ -67,5 +75,17 @@ module Aidp
67
75
  FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
68
76
  dir
69
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
70
90
  end
71
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
data/lib/aidp/errors.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "time"
4
+ require "json"
5
+
3
6
  module Aidp
4
7
  # Error classes for AIDP
5
8
  module Errors
@@ -8,5 +11,136 @@ module Aidp
8
11
  class ValidationError < StandardError; end
9
12
  class StateError < StandardError; end
10
13
  class UserError < StandardError; end
14
+
15
+ # Exception raised when a Rule of Two policy violation occurs
16
+ # Contains detailed context about the violation for logging and debugging
17
+ class PolicyViolation < StandardError
18
+ attr_reader :flag, :source, :current_state, :suggested_mitigations
19
+
20
+ def initialize(flag:, source:, current_state:, message: nil)
21
+ @flag = flag
22
+ @source = source
23
+ @current_state = current_state
24
+ @suggested_mitigations = build_suggested_mitigations(flag, current_state)
25
+
26
+ super(message || default_message)
27
+ end
28
+
29
+ # Export violation details for logging
30
+ def to_h
31
+ {
32
+ type: "rule_of_two_violation",
33
+ flag_attempted: @flag,
34
+ source: @source,
35
+ current_state: @current_state,
36
+ suggested_mitigations: @suggested_mitigations,
37
+ timestamp: Time.now.iso8601
38
+ }
39
+ end
40
+
41
+ # JSON representation for structured logging
42
+ def to_json(*args)
43
+ to_h.to_json(*args)
44
+ end
45
+
46
+ private
47
+
48
+ def default_message
49
+ "Rule of Two violation: Cannot enable '#{@flag}' - would create lethal trifecta"
50
+ end
51
+
52
+ # Build contextual mitigation suggestions based on which flags are enabled
53
+ def build_suggested_mitigations(flag, state)
54
+ mitigations = []
55
+
56
+ # Suggest based on which flags would form the trifecta
57
+ if state[:untrusted_input]
58
+ mitigations << {
59
+ action: "sanitize_input",
60
+ description: "Sanitize the untrusted input before processing to remove the untrusted_input flag",
61
+ command: "aidp security sanitize-input --source <source>"
62
+ }
63
+ end
64
+
65
+ if state[:private_data]
66
+ mitigations << {
67
+ action: "use_secrets_proxy",
68
+ description: "Route credential access through the Secrets Proxy to get short-lived tokens",
69
+ command: "aidp security proxy-request --secret <name>"
70
+ }
71
+ end
72
+
73
+ if state[:egress]
74
+ mitigations << {
75
+ action: "disable_egress",
76
+ description: "Disable external communication for this operation",
77
+ command: "Use deterministic unit or sandbox the operation"
78
+ }
79
+ end
80
+
81
+ # Add flag-specific suggestion for the attempted flag
82
+ case flag
83
+ when :untrusted_input
84
+ mitigations << {
85
+ action: "validate_source",
86
+ description: "Validate and trust the input source (e.g., from trusted author allowlist)",
87
+ command: "Add author to watch.safety.author_allowlist in aidp.yml"
88
+ }
89
+ when :private_data
90
+ mitigations << {
91
+ action: "scope_credentials",
92
+ description: "Use capability-scoped tokens instead of full credentials",
93
+ command: "aidp security register-secret --scoped"
94
+ }
95
+ when :egress
96
+ mitigations << {
97
+ action: "queue_for_approval",
98
+ description: "Queue the operation for manual execution outside the agent context",
99
+ command: "Operation will be logged for manual review"
100
+ }
101
+ end
102
+
103
+ mitigations.uniq { |m| m[:action] }
104
+ end
105
+ end
106
+
107
+ # Exception raised when secrets proxy cannot fulfill a request
108
+ class SecretsProxyError < StandardError
109
+ attr_reader :secret_name, :reason
110
+
111
+ def initialize(secret_name:, reason:)
112
+ @secret_name = secret_name
113
+ @reason = reason
114
+ super("Secrets proxy error for '#{secret_name}': #{reason}")
115
+ end
116
+ end
117
+
118
+ # Exception raised when attempting to access an unregistered secret
119
+ class UnregisteredSecretError < SecretsProxyError
120
+ def initialize(secret_name:)
121
+ super(
122
+ secret_name: secret_name,
123
+ reason: "Secret not registered. Use 'aidp security register-secret #{secret_name}' to register."
124
+ )
125
+ end
126
+ end
127
+
128
+ # Exception raised when a token has expired
129
+ class TokenExpiredError < SecretsProxyError
130
+ def initialize(secret_name:, expired_at:)
131
+ super(
132
+ secret_name: secret_name,
133
+ reason: "Token expired at #{expired_at}. Request a new token via the secrets proxy."
134
+ )
135
+ end
136
+ end
137
+ end
138
+
139
+ # Convenience aliases in Security module for backward compatibility
140
+ module Security
141
+ PolicyViolation = Aidp::Errors::PolicyViolation
142
+ SecretsProxyError = Aidp::Errors::SecretsProxyError
143
+ UnregisteredSecretError = Aidp::Errors::UnregisteredSecretError
144
+ TokenExpiredError = Aidp::Errors::TokenExpiredError
11
145
  end
12
146
  end