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
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+
5
+ module Aidp
6
+ # Zeitwerk-based class loader with hot code reloading support
7
+ #
8
+ # This module configures Zeitwerk for autoloading AIDP classes and provides
9
+ # a reload capability similar to Rails development mode. When files change
10
+ # (e.g., after git pull in watch mode), calling Aidp::Loader.reload! will
11
+ # unload all classes and allow them to be reloaded on next reference.
12
+ #
13
+ # @example Enable reloading in development
14
+ # Aidp::Loader.setup(enable_reloading: true)
15
+ # # ... code changes on disk ...
16
+ # Aidp::Loader.reload!
17
+ #
18
+ # @example Production mode (no reloading)
19
+ # Aidp::Loader.setup(enable_reloading: false)
20
+ # Aidp::Loader.eager_load!
21
+ module Loader
22
+ class << self
23
+ # @return [Zeitwerk::Loader, nil] The configured loader instance
24
+ attr_accessor :loader
25
+
26
+ # @return [Boolean] Whether reloading is enabled
27
+ attr_accessor :reloading_enabled
28
+
29
+ # Set up the Zeitwerk loader for AIDP
30
+ #
31
+ # @param enable_reloading [Boolean] Whether to enable hot reloading
32
+ # @param eager_load [Boolean] Whether to eager load all classes
33
+ # @return [Zeitwerk::Loader] The configured loader
34
+ def setup(enable_reloading: false, eager_load: false)
35
+ return @loader if @loader
36
+
37
+ Aidp.log_debug("loader", "setup_started",
38
+ enable_reloading: enable_reloading,
39
+ eager_load: eager_load)
40
+
41
+ @reloading_enabled = enable_reloading
42
+ @loader = create_loader
43
+ configure_inflections(@loader)
44
+ configure_ignores(@loader)
45
+
46
+ if enable_reloading
47
+ @loader.enable_reloading
48
+ Aidp.log_debug("loader", "reloading_enabled")
49
+ end
50
+
51
+ @loader.setup
52
+
53
+ if eager_load && !enable_reloading
54
+ @loader.eager_load
55
+ Aidp.log_debug("loader", "eager_load_complete")
56
+ end
57
+
58
+ Aidp.log_info("loader", "setup_complete",
59
+ reloading: enable_reloading,
60
+ eager_loaded: eager_load && !enable_reloading)
61
+
62
+ @loader
63
+ end
64
+
65
+ # Reload all autoloaded classes
66
+ #
67
+ # This unloads all classes managed by Zeitwerk and allows them to be
68
+ # reloaded on next reference. Only works if enable_reloading was true
69
+ # during setup.
70
+ #
71
+ # @return [Boolean] Whether reload was performed
72
+ def reload!
73
+ unless @loader
74
+ Aidp.log_warn("loader", "reload_skipped", reason: "loader_not_setup")
75
+ return false
76
+ end
77
+
78
+ unless @reloading_enabled
79
+ Aidp.log_warn("loader", "reload_skipped", reason: "reloading_disabled")
80
+ return false
81
+ end
82
+
83
+ Aidp.log_info("loader", "reload_started")
84
+
85
+ begin
86
+ @loader.reload
87
+ Aidp.log_info("loader", "reload_complete")
88
+ true
89
+ rescue => e
90
+ Aidp.log_error("loader", "reload_failed", error: e.message)
91
+ false
92
+ end
93
+ end
94
+
95
+ # Check if loader is set up for reloading
96
+ #
97
+ # @return [Boolean]
98
+ def reloading?
99
+ @reloading_enabled == true
100
+ end
101
+
102
+ # Check if loader is set up
103
+ #
104
+ # @return [Boolean]
105
+ def setup?
106
+ !@loader.nil?
107
+ end
108
+
109
+ # Eager load all classes (production mode)
110
+ #
111
+ # @return [void]
112
+ def eager_load!
113
+ @loader&.eager_load
114
+ end
115
+
116
+ # Reset the loader (mainly for testing)
117
+ #
118
+ # @return [void]
119
+ def reset!
120
+ if @loader
121
+ begin
122
+ @loader.unload if @reloading_enabled
123
+ rescue Zeitwerk::SetupRequired
124
+ # If loader was never set up, skip unload
125
+ end
126
+ @loader.unregister
127
+ end
128
+ @loader = nil
129
+ @reloading_enabled = false
130
+ end
131
+
132
+ private
133
+
134
+ def create_loader
135
+ loader = Zeitwerk::Loader.new
136
+ loader.tag = "aidp"
137
+
138
+ # Set the root directory for autoloading
139
+ lib_path = File.expand_path("..", __dir__)
140
+ loader.push_dir(lib_path, namespace: Object)
141
+
142
+ loader
143
+ end
144
+
145
+ # Configure inflections for non-standard class names
146
+ def configure_inflections(loader)
147
+ loader.inflector.inflect(
148
+ # AI-prefixed classes
149
+ "ai_decision_engine" => "AIDecisionEngine",
150
+ "ai_filter_factory" => "AIFilterFactory",
151
+
152
+ # Acronym-based names
153
+ "kb_inspector" => "KBInspector",
154
+ "ui_state" => "UIState",
155
+ "ui_error" => "UIError",
156
+ "rspec_filter_strategy" => "RSpecFilterStrategy",
157
+
158
+ # TTY-related
159
+ "tui" => "TUI",
160
+ "enhanced_tui" => "EnhancedTUI",
161
+
162
+ # Other acronyms
163
+ "pr_worktree_manager" => "PRWorktreeManager",
164
+ "csv_storage" => "CSVStorage",
165
+ "cli" => "CLI",
166
+ "ruby_llm_registry" => "RubyLLMRegistry",
167
+ "rubygems_api_adapter" => "RubyGemsAPIAdapter",
168
+ "submenu" => "SubMenu",
169
+ "terminal_io" => "TerminalIO",
170
+
171
+ # Module folders
172
+ "ui" => "UI"
173
+ )
174
+ end
175
+
176
+ # Configure files/directories to ignore
177
+ def configure_ignores(loader)
178
+ # Ignore files that are manually required before Zeitwerk
179
+ loader.ignore(File.expand_path("version.rb", __dir__))
180
+ loader.ignore(File.expand_path("core_ext", __dir__))
181
+ loader.ignore(File.expand_path("logger.rb", __dir__))
182
+ loader.ignore(File.expand_path("cli/issue_importer.rb", __dir__))
183
+ loader.ignore(File.expand_path("config/paths.rb", __dir__))
184
+
185
+ # Ignore the loader itself
186
+ loader.ignore(__FILE__)
187
+
188
+ # Ignore files with multiple constants (require manually after setup)
189
+ loader.ignore(File.expand_path("auto_update/errors.rb", __dir__))
190
+ loader.ignore(File.expand_path("errors.rb", __dir__))
191
+ loader.ignore(File.expand_path("harness/state/errors.rb", __dir__))
192
+ end
193
+ end
194
+ end
195
+ end
data/lib/aidp/logger.rb CHANGED
@@ -307,6 +307,9 @@ module Aidp
307
307
 
308
308
  # Module-level logger accessor
309
309
  class << self
310
+ # Expose for testability
311
+ attr_writer :logger
312
+
310
313
  # Set up global logger instance
311
314
  def setup_logger(project_dir = Dir.pwd, config = {})
312
315
  @logger = Logger.new(project_dir, config)
@@ -8,6 +8,11 @@ module Aidp
8
8
  # include Aidp::MessageDisplay
9
9
  # display_message("Hello", type: :success)
10
10
  # Supports color types: :error, :success, :warning, :info, :highlight, :muted
11
+ #
12
+ # Quiet mode:
13
+ # When quiet mode is enabled (via CLI --quiet flag or setting quiet=true on instance),
14
+ # only :error, :warning, and :success messages are displayed. Info, highlight, and muted
15
+ # messages are suppressed to reduce output noise.
11
16
  module MessageDisplay
12
17
  COLOR_MAP = {
13
18
  error: :red,
@@ -19,12 +24,28 @@ module Aidp
19
24
  muted: :bright_black
20
25
  }.freeze
21
26
 
27
+ # Message types that are always shown even in quiet mode
28
+ CRITICAL_TYPES = %i[error warning warn success].freeze
29
+
22
30
  def self.included(base)
23
31
  base.extend(ClassMethods)
24
32
  end
25
33
 
34
+ # Check if quiet mode is enabled
35
+ # Priority: instance @quiet variable > CLI.last_options[:quiet]
36
+ def quiet_mode?
37
+ return @quiet if instance_variable_defined?(:@quiet) && !@quiet.nil?
38
+
39
+ Aidp::CLI.last_options&.dig(:quiet) || false
40
+ rescue
41
+ false
42
+ end
43
+
26
44
  # Instance helper for displaying a colored message via TTY::Prompt
27
45
  def display_message(message, type: :info)
46
+ # In quiet mode, suppress non-critical messages
47
+ return if quiet_mode? && !CRITICAL_TYPES.include?(type)
48
+
28
49
  # Ensure message is UTF-8 encoded to handle emoji and special characters
29
50
  message_str = message.to_s
30
51
  message_str = message_str.force_encoding("UTF-8") if message_str.encoding.name == "ASCII-8BIT"
@@ -43,8 +64,18 @@ module Aidp
43
64
  end
44
65
 
45
66
  module ClassMethods
67
+ # Check if quiet mode is enabled at class level
68
+ def quiet_mode?
69
+ Aidp::CLI.last_options&.dig(:quiet) || false
70
+ rescue
71
+ false
72
+ end
73
+
46
74
  # Class-level display helper (uses fresh prompt to respect $stdout changes)
47
75
  def display_message(message, type: :info)
76
+ # In quiet mode, suppress non-critical messages
77
+ return if quiet_mode? && !CRITICAL_TYPES.include?(type)
78
+
48
79
  # Ensure message is UTF-8 encoded to handle emoji and special characters
49
80
  message_str = message.to_s
50
81
  message_str = message_str.force_encoding("UTF-8") if message_str.encoding.name == "ASCII-8BIT"
@@ -39,7 +39,7 @@ module Aidp
39
39
  Aidp.log_info("metadata", "Compiling tool directory", directories: @directories, output: output_path)
40
40
 
41
41
  # Scan all directories
42
- scanner = Scanner.new(@directories)
42
+ scanner = Scanner.new(@directories, strict: @strict)
43
43
  @tools = scanner.scan_all
44
44
 
45
45
  # Validate tools
@@ -47,14 +47,24 @@ module Aidp
47
47
  validation_results = validator.validate_all
48
48
 
49
49
  # Handle validation failures
50
- handle_validation_results(validation_results)
50
+ parse_error_results = scanner.parse_errors.map do |err|
51
+ Validator::ValidationResult.new(
52
+ tool_id: "(unknown)",
53
+ file_path: err[:file],
54
+ valid: false,
55
+ errors: [err[:error]],
56
+ warnings: []
57
+ )
58
+ end
59
+
60
+ invalid_results = handle_validation_results(validation_results + parse_error_results)
51
61
 
52
62
  # Build indexes and graphs
53
63
  build_indexes
54
64
  build_dependency_graph
55
65
 
56
66
  # Create directory structure
57
- directory = create_directory_structure
67
+ directory = create_directory_structure(invalid_results: invalid_results)
58
68
 
59
69
  # Write to file
60
70
  write_directory(directory, output_path)
@@ -142,22 +152,23 @@ module Aidp
142
152
  # Create directory structure for serialization
143
153
  #
144
154
  # @return [Hash] Directory structure
145
- def create_directory_structure
155
+ def create_directory_structure(invalid_results: [])
146
156
  {
147
- version: "1.0.0",
148
- compiled_at: Time.now.iso8601,
149
- tools: @tools.map(&:to_h),
150
- indexes: {
151
- by_type: @indexes[:by_type].transform_values { |tools| tools.map(&:id) },
152
- by_tag: @indexes[:by_tag].transform_values { |tools| tools.map(&:id) },
153
- by_work_unit_type: @indexes[:by_work_unit_type].transform_values { |tools| tools.map(&:id) }
157
+ "version" => "1.0.0",
158
+ "compiled_at" => Time.now.iso8601,
159
+ "tools" => @tools.map(&:to_h),
160
+ "indexes" => {
161
+ "by_type" => @indexes[:by_type].transform_values { |tools| tools.map(&:id) },
162
+ "by_tags" => @indexes[:by_tag].transform_values { |tools| tools.map(&:id) },
163
+ "by_work_unit_type" => @indexes[:by_work_unit_type].transform_values { |tools| tools.map(&:id) }
154
164
  },
155
- dependency_graph: @dependency_graph,
156
- statistics: {
157
- total_tools: @tools.size,
158
- by_type: @tools.group_by(&:type).transform_values(&:size),
159
- total_tags: @indexes[:by_tag].size,
160
- total_work_unit_types: @indexes[:by_work_unit_type].size
165
+ "dependency_graph" => @dependency_graph,
166
+ "errors" => invalid_results.map { |res| {"tool_id" => res.tool_id, "errors" => res.errors, "warnings" => res.warnings} },
167
+ "statistics" => {
168
+ "total_tools" => @tools.size,
169
+ "by_type" => @tools.group_by(&:type).transform_values(&:size),
170
+ "total_tags" => @indexes[:by_tag].size,
171
+ "total_work_unit_types" => @indexes[:by_work_unit_type].size
161
172
  }
162
173
  }
163
174
  end
@@ -223,6 +234,7 @@ module Aidp
223
234
  warnings: result.warnings
224
235
  )
225
236
  end
237
+ invalid_results
226
238
  end
227
239
  end
228
240
  end
@@ -64,7 +64,7 @@ module Aidp
64
64
  Aidp.log_debug("metadata", "Finding by tags", tags: tags, match_all: match_all)
65
65
 
66
66
  tags = Array(tags).map(&:downcase)
67
- indexes = directory["indexes"]["by_tag"]
67
+ indexes = directory["indexes"]["by_tags"] || {}
68
68
 
69
69
  if match_all
70
70
  # Find tools that have ALL specified tags
@@ -17,16 +17,21 @@ module Aidp
17
17
  # Initialize scanner with directory paths
18
18
  #
19
19
  # @param directories [Array<String>] Directories to scan
20
- def initialize(directories = [])
20
+ def initialize(directories = [], strict: false)
21
21
  @directories = Array(directories)
22
+ @strict = strict
23
+ @parse_errors = []
22
24
  end
23
25
 
26
+ attr_reader :parse_errors
27
+
24
28
  # Scan all configured directories
25
29
  #
26
30
  # @return [Array<ToolMetadata>] All discovered tool metadata
27
31
  def scan_all
28
32
  Aidp.log_debug("metadata", "Scanning directories", directories: @directories)
29
33
 
34
+ @parse_errors = []
30
35
  all_tools = []
31
36
  @directories.each do |dir|
32
37
  tools = scan_directory(dir)
@@ -69,6 +74,8 @@ module Aidp
69
74
  file: file_path,
70
75
  error: e.message
71
76
  )
77
+ @parse_errors << {file: file_path, error: e.message}
78
+ raise if @strict
72
79
  end
73
80
 
74
81
  Aidp.log_debug(
@@ -146,19 +146,19 @@ module Aidp
146
146
  # @return [Hash] Metadata as hash (for JSON)
147
147
  def to_h
148
148
  {
149
- type: type,
150
- id: id,
151
- title: title,
152
- summary: summary,
153
- version: version,
154
- applies_to: applies_to,
155
- work_unit_types: work_unit_types,
156
- priority: priority,
157
- capabilities: capabilities,
158
- dependencies: dependencies,
159
- experimental: experimental,
160
- source_path: source_path,
161
- file_hash: file_hash
149
+ "type" => type,
150
+ "id" => id,
151
+ "title" => title,
152
+ "summary" => summary,
153
+ "version" => version,
154
+ "applies_to" => applies_to,
155
+ "work_unit_types" => work_unit_types,
156
+ "priority" => priority,
157
+ "capabilities" => capabilities,
158
+ "dependencies" => dependencies,
159
+ "experimental" => experimental,
160
+ "source_path" => source_path,
161
+ "file_hash" => file_hash
162
162
  }
163
163
  end
164
164
 
@@ -84,6 +84,10 @@ module Aidp
84
84
  warnings << "Tool content is very short (#{tool.content.length} characters)"
85
85
  end
86
86
 
87
+ unless valid_version_format?(tool.version)
88
+ warnings << "Version '#{tool.version}' is not in semver format (MAJOR.MINOR.PATCH)"
89
+ end
90
+
87
91
  ValidationResult.new(
88
92
  tool_id: tool.id,
89
93
  file_path: tool.source_path,
@@ -156,6 +160,12 @@ module Aidp
156
160
  # For now, this is a placeholder for future deprecation warnings
157
161
  end
158
162
 
163
+ def valid_version_format?(version)
164
+ version.to_s.match?(/\A\d+\.\d+\.\d+\z/)
165
+ rescue
166
+ false
167
+ end
168
+
159
169
  # Write validation errors to log file
160
170
  #
161
171
  # @param results [Array<ValidationResult>] Validation results
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ # Namespace marker for metadata components
5
+ module Metadata
6
+ end
7
+ end
8
+
9
+ # Load key metadata components
10
+ require_relative "metadata/parser"
11
+ require_relative "metadata/tool_metadata"
12
+ require_relative "metadata/scanner"
13
+ require_relative "metadata/validator"
14
+ require_relative "metadata/compiler"
15
+ require_relative "metadata/cache"
16
+ require_relative "metadata/query"
@@ -3,15 +3,27 @@ require "fileutils"
3
3
  require "shellwords"
4
4
 
5
5
  module Aidp
6
+ # Simple shell command executor wrapper for testability
7
+ class ShellExecutor
8
+ def run(command)
9
+ `#{command}`
10
+ end
11
+
12
+ def success?
13
+ $?.success?
14
+ end
15
+ end
16
+
6
17
  # Manages worktrees specifically for Pull Request branches
7
- class PrWorktreeManager
8
- def initialize(base_repo_path: nil, project_dir: nil, worktree_registry_path: nil)
18
+ class PRWorktreeManager
19
+ def initialize(base_repo_path: nil, project_dir: nil, worktree_registry_path: nil, shell_executor: nil)
9
20
  @base_repo_path = base_repo_path || project_dir || Dir.pwd
10
21
  @project_dir = project_dir
11
22
  @worktree_registry_path = worktree_registry_path || File.join(
12
23
  project_dir || File.expand_path("~/.aidp"),
13
24
  "pr_worktrees.json"
14
25
  )
26
+ @shell_executor = shell_executor || ShellExecutor.new
15
27
  FileUtils.mkdir_p(File.dirname(@worktree_registry_path))
16
28
  @worktrees = load_registry
17
29
  end
@@ -258,7 +270,7 @@ module Aidp
258
270
 
259
271
  # Advanced change detection patterns
260
272
  file_patterns = [
261
- /(?:modify|update|add|delete)\s+file:\s*([^\n]+)/i,
273
+ /(modify|update|add|delete)\s+file:\s*([^\n]+)/i,
262
274
  /\[(\w+)\]\s*([^\n]+)/, # GitHub-style change indicators
263
275
  /(?:Action:\s*(\w+))\s*File:\s*([^\n]+)/i
264
276
  ]
@@ -427,7 +439,7 @@ module Aidp
427
439
 
428
440
  Dir.chdir(worktree_path) do
429
441
  # Check staged changes with more robust capture
430
- staged_changes_output = `git diff --staged --name-only`.strip
442
+ staged_changes_output = @shell_executor.run("git diff --staged --name-only").strip
431
443
 
432
444
  if !staged_changes_output.empty?
433
445
  push_result[:git_actions][:staged_changes] = true
@@ -436,16 +448,16 @@ module Aidp
436
448
  # More robust commit command with additional logging
437
449
  commit_message = "Changes applied via AIDP request-changes workflow for PR ##{pr_number}"
438
450
  commit_command = "git commit -m '#{commit_message}' 2>&1"
439
- commit_output = `#{commit_command}`.strip
451
+ commit_output = @shell_executor.run(commit_command).strip
440
452
 
441
- if $?.success?
453
+ if @shell_executor.success?
442
454
  push_result[:git_actions][:committed] = true
443
455
 
444
456
  # Enhanced push with verbose tracking
445
457
  push_command = "git push origin #{head_branch} 2>&1"
446
- push_output = `#{push_command}`.strip
458
+ push_output = @shell_executor.run(push_command).strip
447
459
 
448
- if $?.success?
460
+ if @shell_executor.success?
449
461
  push_result[:git_actions][:pushed] = true
450
462
  push_result[:success] = true
451
463
 
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "tty-prompt"
4
- require_relative "harness/provider_factory"
5
4
 
6
5
  module Aidp
7
6
  class ProviderManager
8
7
  class << self
8
+ # Expose for testability
9
+ attr_accessor :harness_factory
10
+
9
11
  def get_provider(provider_type, options = {})
10
12
  factory = get_harness_factory
11
13
  raise "Harness factory not available" unless factory
@@ -20,12 +22,7 @@ module Aidp
20
22
 
21
23
  # Get harness factory instance
22
24
  def get_harness_factory
23
- @harness_factory ||= begin
24
- require_relative "harness/config_manager"
25
- Aidp::Harness::ProviderFactory.new
26
- rescue LoadError
27
- nil
28
- end
25
+ @harness_factory ||= Aidp::Harness::ProviderFactory.new
29
26
  end
30
27
 
31
28
  # Create provider using harness configuration
@@ -48,6 +48,8 @@ module Aidp
48
48
  }.freeze
49
49
 
50
50
  attr_reader :activity_state, :last_activity_time, :start_time, :step_name, :model
51
+ # Expose for testability
52
+ attr_writer :harness_context
51
53
 
52
54
  def initialize(output: nil, prompt: TTY::Prompt.new)
53
55
  @activity_state = :idle