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/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
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../config_paths"
4
+
5
+ module Aidp
6
+ module Evaluations
7
+ # Captures rich context for evaluation records
8
+ #
9
+ # Gathers information about:
10
+ # - Prompt metadata (template, persona, skills, provider, model, tokens, settings)
11
+ # - Work-loop data (unit count, checkpoints, retries, file modifications)
12
+ # - Environment details (devcontainer status, Ruby version, branch info)
13
+ #
14
+ # @example Capturing context
15
+ # context = ContextCapture.new(project_dir: Dir.pwd)
16
+ # data = context.capture(step_name: "01_INIT", iteration: 3)
17
+ class ContextCapture
18
+ def initialize(project_dir: Dir.pwd, config: nil)
19
+ @project_dir = project_dir
20
+ @config = config
21
+
22
+ Aidp.log_debug("context_capture", "initialize", project_dir: project_dir)
23
+ end
24
+
25
+ # Capture full context for an evaluation
26
+ #
27
+ # @param step_name [String, nil] Current work loop step
28
+ # @param iteration [Integer, nil] Current iteration number
29
+ # @param provider [String, nil] AI provider being used
30
+ # @param model [String, nil] AI model being used
31
+ # @param additional [Hash] Additional context to include
32
+ # @return [Hash] Captured context
33
+ def capture(step_name: nil, iteration: nil, provider: nil, model: nil, additional: {})
34
+ Aidp.log_debug("context_capture", "capture",
35
+ step_name: step_name, iteration: iteration, provider: provider)
36
+
37
+ {
38
+ prompt: capture_prompt_context(step_name),
39
+ work_loop: capture_work_loop_context(step_name, iteration),
40
+ environment: capture_environment_context,
41
+ provider: {
42
+ name: provider,
43
+ model: model
44
+ },
45
+ timestamp: Time.now.iso8601
46
+ }.merge(additional)
47
+ end
48
+
49
+ # Capture minimal context (for quick evaluations)
50
+ #
51
+ # @return [Hash] Minimal context with timestamp and environment basics
52
+ def capture_minimal
53
+ Aidp.log_debug("context_capture", "capture_minimal")
54
+
55
+ {
56
+ environment: {
57
+ ruby_version: RUBY_VERSION,
58
+ branch: current_git_branch,
59
+ aidp_version: aidp_version
60
+ },
61
+ timestamp: Time.now.iso8601
62
+ }
63
+ end
64
+
65
+ # Capture watch mode context for evaluating watch outputs
66
+ #
67
+ # @param repo [String] Repository in owner/repo format
68
+ # @param number [Integer] Issue or PR number
69
+ # @param processor_type [String] Type of processor (plan, review, build, ci_fix, change_request)
70
+ # @return [Hash] Watch mode context
71
+ def capture_watch(repo:, number:, processor_type:)
72
+ Aidp.log_debug("context_capture", "capture_watch",
73
+ repo: repo, number: number, processor_type: processor_type)
74
+
75
+ {
76
+ watch: {
77
+ repo: repo,
78
+ number: number,
79
+ processor_type: processor_type,
80
+ state: load_watch_state(repo, number, processor_type)
81
+ },
82
+ environment: capture_environment_context,
83
+ timestamp: Time.now.iso8601
84
+ }
85
+ end
86
+
87
+ private
88
+
89
+ def load_watch_state(repo, number, processor_type)
90
+ # Try to load state from the watch state store
91
+ state_file = find_watch_state_file(repo)
92
+ return nil unless state_file && File.exist?(state_file)
93
+
94
+ require "yaml"
95
+ state = YAML.safe_load_file(state_file, permitted_classes: [Time, Date, Symbol])
96
+ return nil unless state
97
+
98
+ # Extract relevant state based on processor type
99
+ case processor_type
100
+ when "plan"
101
+ state.dig("issues", number.to_s, "plan") ||
102
+ state.dig(:issues, number, :plan)
103
+ when "review"
104
+ state.dig("pull_requests", number.to_s, "review") ||
105
+ state.dig(:pull_requests, number, :review)
106
+ when "build"
107
+ state.dig("issues", number.to_s, "build") ||
108
+ state.dig(:issues, number, :build)
109
+ when "ci_fix"
110
+ state.dig("pull_requests", number.to_s, "ci_fix") ||
111
+ state.dig(:pull_requests, number, :ci_fix)
112
+ when "change_request"
113
+ state.dig("pull_requests", number.to_s, "change_request") ||
114
+ state.dig(:pull_requests, number, :change_request)
115
+ end
116
+ rescue => e
117
+ Aidp.log_error("context_capture", "load_watch_state failed", error: e.message)
118
+ nil
119
+ end
120
+
121
+ def find_watch_state_file(repo)
122
+ watch_dir = File.join(@project_dir, ".aidp", "watch")
123
+ return nil unless Dir.exist?(watch_dir)
124
+
125
+ # Sanitize repo name the same way StateStore does
126
+ sanitized = repo.tr("/", "_").gsub(/[^a-zA-Z0-9_-]/, "")
127
+ state_file = File.join(watch_dir, "#{sanitized}.yml")
128
+
129
+ File.exist?(state_file) ? state_file : nil
130
+ end
131
+
132
+ def capture_prompt_context(step_name)
133
+ prompt_file = File.join(@project_dir, ".aidp", "PROMPT.md")
134
+ return {} unless File.exist?(prompt_file)
135
+
136
+ content = File.read(prompt_file)
137
+ {
138
+ step_name: step_name,
139
+ prompt_length: content.length,
140
+ has_prompt: true
141
+ }
142
+ rescue => e
143
+ Aidp.log_error("context_capture", "capture_prompt_context failed", error: e.message)
144
+ {}
145
+ end
146
+
147
+ def capture_work_loop_context(step_name, iteration)
148
+ checkpoint_file = ConfigPaths.checkpoint_file(@project_dir)
149
+ checkpoint_data = if File.exist?(checkpoint_file)
150
+ require "yaml"
151
+ YAML.safe_load_file(checkpoint_file, permitted_classes: [Time, Date, Symbol])
152
+ end
153
+
154
+ {
155
+ step_name: step_name,
156
+ iteration: iteration,
157
+ checkpoint: checkpoint_data ? {
158
+ status: checkpoint_data["status"] || checkpoint_data[:status],
159
+ metrics: checkpoint_data["metrics"] || checkpoint_data[:metrics]
160
+ } : nil
161
+ }
162
+ rescue => e
163
+ Aidp.log_error("context_capture", "capture_work_loop_context failed", error: e.message)
164
+ {step_name: step_name, iteration: iteration}
165
+ end
166
+
167
+ def capture_environment_context
168
+ {
169
+ ruby_version: RUBY_VERSION,
170
+ platform: RUBY_PLATFORM,
171
+ branch: current_git_branch,
172
+ commit: current_git_commit,
173
+ devcontainer: in_devcontainer?,
174
+ aidp_version: aidp_version
175
+ }
176
+ end
177
+
178
+ def current_git_branch
179
+ Dir.chdir(@project_dir) do
180
+ `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
181
+ end
182
+ rescue
183
+ nil
184
+ end
185
+
186
+ def current_git_commit
187
+ Dir.chdir(@project_dir) do
188
+ `git rev-parse --short HEAD 2>/dev/null`.strip
189
+ end
190
+ rescue
191
+ nil
192
+ end
193
+
194
+ def in_devcontainer?
195
+ File.exist?("/.dockerenv") || ENV["REMOTE_CONTAINERS"] == "true"
196
+ end
197
+
198
+ def aidp_version
199
+ Aidp::VERSION if defined?(Aidp::VERSION)
200
+ rescue
201
+ nil
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Aidp
6
+ module Evaluations
7
+ # Represents a single evaluation record
8
+ #
9
+ # An evaluation captures user feedback (good/neutral/bad) for AIDP outputs
10
+ # such as prompts, work units, or full work loops, along with rich context.
11
+ #
12
+ # @example Creating an evaluation
13
+ # record = EvaluationRecord.new(
14
+ # rating: "good",
15
+ # comment: "Generated code was clean and well-structured",
16
+ # target_type: "work_unit",
17
+ # target_id: "01_INIT"
18
+ # )
19
+ class EvaluationRecord
20
+ VALID_RATINGS = %w[good neutral bad].freeze
21
+ VALID_TARGET_TYPES = %w[prompt work_unit work_loop step plan review build ci_fix change_request].freeze
22
+
23
+ attr_reader :id, :rating, :comment, :target_type, :target_id,
24
+ :context, :created_at
25
+
26
+ def initialize(rating:, comment: nil, target_type: nil, target_id: nil, context: {}, id: nil, created_at: nil)
27
+ @id = id || generate_id
28
+ @rating = validate_rating(rating)
29
+ @comment = comment
30
+ @target_type = validate_target_type(target_type)
31
+ @target_id = target_id
32
+ @context = context || {}
33
+ @created_at = created_at || Time.now.iso8601
34
+
35
+ Aidp.log_debug("evaluation_record", "create",
36
+ id: @id, rating: @rating, target_type: @target_type, target_id: @target_id)
37
+ end
38
+
39
+ # Convert to hash for storage
40
+ def to_h
41
+ {
42
+ id: @id,
43
+ rating: @rating,
44
+ comment: @comment,
45
+ target_type: @target_type,
46
+ target_id: @target_id,
47
+ context: @context,
48
+ created_at: @created_at
49
+ }
50
+ end
51
+
52
+ # Create record from stored hash
53
+ def self.from_h(hash)
54
+ hash = symbolize_keys(hash)
55
+ new(
56
+ id: hash[:id],
57
+ rating: hash[:rating],
58
+ comment: hash[:comment],
59
+ target_type: hash[:target_type],
60
+ target_id: hash[:target_id],
61
+ context: hash[:context] || {},
62
+ created_at: hash[:created_at]
63
+ )
64
+ end
65
+
66
+ # Check if rating is positive
67
+ def good?
68
+ @rating == "good"
69
+ end
70
+
71
+ # Check if rating is negative
72
+ def bad?
73
+ @rating == "bad"
74
+ end
75
+
76
+ # Check if rating is neutral
77
+ def neutral?
78
+ @rating == "neutral"
79
+ end
80
+
81
+ private
82
+
83
+ def generate_id
84
+ "eval_#{Time.now.strftime("%Y%m%d_%H%M%S")}_#{SecureRandom.hex(4)}"
85
+ end
86
+
87
+ def validate_rating(rating)
88
+ rating_str = rating.to_s.downcase
89
+ unless VALID_RATINGS.include?(rating_str)
90
+ raise ArgumentError, "Invalid rating '#{rating}'. Must be one of: #{VALID_RATINGS.join(", ")}"
91
+ end
92
+ rating_str
93
+ end
94
+
95
+ def validate_target_type(target_type)
96
+ return nil if target_type.nil?
97
+ type_str = target_type.to_s.downcase
98
+ unless VALID_TARGET_TYPES.include?(type_str)
99
+ raise ArgumentError, "Invalid target_type '#{target_type}'. Must be one of: #{VALID_TARGET_TYPES.join(", ")}"
100
+ end
101
+ type_str
102
+ end
103
+
104
+ class << self
105
+ private
106
+
107
+ def symbolize_keys(hash)
108
+ return hash unless hash.is_a?(Hash)
109
+ hash.transform_keys { |k| k.is_a?(String) ? k.to_sym : k }
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end