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
@@ -0,0 +1,416 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+ require "tty-table"
5
+ require_relative "../config"
6
+ require_relative "../security"
7
+
8
+ module Aidp
9
+ class CLI
10
+ # CLI commands for security management
11
+ #
12
+ # Provides commands for:
13
+ # - aidp security status Show current security posture
14
+ # - aidp security register <name> Register a secret with the proxy
15
+ # - aidp security unregister <name> Remove a registered secret
16
+ # - aidp security list List registered secrets (names only)
17
+ # - aidp security audit Run security audit (RSpec tests)
18
+ class SecurityCommand
19
+ def initialize(project_dir: Dir.pwd, prompt: TTY::Prompt.new)
20
+ @project_dir = project_dir
21
+ @prompt = prompt
22
+ end
23
+
24
+ # Run security command
25
+ #
26
+ # @param args [Array<String>] Command arguments
27
+ # @return [Integer] Exit code
28
+ def run(args)
29
+ subcommand = args.shift
30
+
31
+ case subcommand
32
+ when "status"
33
+ run_status
34
+ when "register", "register-secret"
35
+ secret_name = args.shift
36
+ unless secret_name
37
+ @prompt.error("Error: secret name required")
38
+ @prompt.say("Usage: aidp security register <name> [--env-var VAR_NAME]")
39
+ return 1
40
+ end
41
+ # Parse optional --env-var flag
42
+ env_var = parse_env_var_option(args) || secret_name
43
+ run_register(secret_name, env_var)
44
+ when "unregister"
45
+ secret_name = args.shift
46
+ unless secret_name
47
+ @prompt.error("Error: secret name required")
48
+ @prompt.say("Usage: aidp security unregister <name>")
49
+ return 1
50
+ end
51
+ run_unregister(secret_name)
52
+ when "list", "secrets"
53
+ run_list
54
+ when "audit"
55
+ run_audit(args)
56
+ when "proxy-status"
57
+ run_proxy_status
58
+ when nil, "help", "--help", "-h"
59
+ show_help
60
+ 0
61
+ else
62
+ @prompt.error("Unknown subcommand: #{subcommand}")
63
+ show_help
64
+ 1
65
+ end
66
+ end
67
+
68
+ # Show help message
69
+ def show_help
70
+ @prompt.say("\nAIDP Security Management")
71
+ @prompt.say("\n" + "=" * 40)
72
+ @prompt.say("\nUsage:")
73
+ @prompt.say(" aidp security status Show current security posture")
74
+ @prompt.say(" aidp security register <name> Register a secret with the proxy")
75
+ @prompt.say(" aidp security unregister <name> Remove a registered secret")
76
+ @prompt.say(" aidp security list List registered secrets (names only)")
77
+ @prompt.say(" aidp security proxy-status Show secrets proxy status")
78
+ @prompt.say(" aidp security audit Run security audit tests")
79
+ @prompt.say("\nOptions for register:")
80
+ @prompt.say(" --env-var VAR_NAME Environment variable containing the secret")
81
+ @prompt.say(" (defaults to the secret name if not provided)")
82
+ @prompt.say("\nExamples:")
83
+ @prompt.say(" aidp security status")
84
+ @prompt.say(" aidp security register GITHUB_TOKEN")
85
+ @prompt.say(" aidp security register github_token --env-var GITHUB_TOKEN")
86
+ @prompt.say(" aidp security list")
87
+ @prompt.say("\n" + "=" * 40)
88
+ end
89
+
90
+ # Run status command - show current security posture
91
+ def run_status
92
+ Aidp.log_debug("security_cli", "showing_status")
93
+
94
+ config = Aidp::Config.security_config(@project_dir)
95
+ rule_of_two = config[:rule_of_two] || {}
96
+ proxy_config = config[:secrets_proxy] || {}
97
+
98
+ @prompt.say("\n" + "=" * 50)
99
+ @prompt.say("AIDP Security Status")
100
+ @prompt.say("=" * 50)
101
+
102
+ # Rule of Two status
103
+ @prompt.say("\n Rule of Two Enforcement")
104
+ @prompt.say("-" * 30)
105
+ enabled = rule_of_two.fetch(:enabled, true)
106
+ policy = rule_of_two[:policy] || "strict"
107
+ status_icon = enabled ? "\u2713" : "\u2717"
108
+ @prompt.say(" Status: #{status_icon} #{enabled ? "Enabled" : "Disabled"}")
109
+ @prompt.say(" Policy: #{policy}")
110
+
111
+ # Enforcer status
112
+ enforcer = Aidp::Security.enforcer
113
+ summary = enforcer.status_summary
114
+ @prompt.say(" Active work units: #{summary[:active_work_units]}")
115
+ @prompt.say(" Completed work units: #{summary[:completed_work_units]}")
116
+
117
+ # Secrets Proxy status
118
+ @prompt.say("\n Secrets Proxy")
119
+ @prompt.say("-" * 30)
120
+ proxy_enabled = proxy_config.fetch(:enabled, true)
121
+ token_ttl = proxy_config[:token_ttl] || 300
122
+ status_icon = proxy_enabled ? "\u2713" : "\u2717"
123
+ @prompt.say(" Status: #{status_icon} #{proxy_enabled ? "Enabled" : "Disabled"}")
124
+ @prompt.say(" Token TTL: #{token_ttl} seconds")
125
+
126
+ # Registered secrets count
127
+ registry = Aidp::Security.secrets_registry
128
+ secrets = registry.list
129
+ @prompt.say(" Registered secrets: #{secrets.count}")
130
+
131
+ # Active tokens
132
+ proxy = Aidp::Security.secrets_proxy
133
+ active_tokens = proxy.active_tokens_summary
134
+ @prompt.say(" Active tokens: #{active_tokens.count}")
135
+
136
+ @prompt.say("\n" + "=" * 50)
137
+ @prompt.say("Use 'aidp security list' to see registered secrets")
138
+ @prompt.say("Use 'aidp security register <name>' to add secrets")
139
+ @prompt.say("")
140
+
141
+ 0
142
+ end
143
+
144
+ # Run register command - register a secret with the proxy
145
+ def run_register(secret_name, env_var)
146
+ Aidp.log_info("security_cli", "registering_secret",
147
+ name: secret_name,
148
+ env_var: env_var)
149
+
150
+ registry = Aidp::Security.secrets_registry
151
+
152
+ # Check if already registered
153
+ if registry.registered?(secret_name)
154
+ @prompt.warn("Secret '#{secret_name}' is already registered")
155
+ existing = registry.get(secret_name)
156
+ @prompt.say(" Env var: #{existing[:env_var] || existing["env_var"]}")
157
+ return 1
158
+ end
159
+
160
+ # Check if env var exists
161
+ unless ENV.key?(env_var)
162
+ @prompt.warn("Warning: Environment variable '#{env_var}' is not currently set")
163
+ unless @prompt.yes?("Continue anyway?")
164
+ return 1
165
+ end
166
+ end
167
+
168
+ # Ask for optional description
169
+ description = @prompt.ask("Description (optional):") do |q|
170
+ q.required false
171
+ end
172
+
173
+ # Ask for optional scopes
174
+ scopes = @prompt.ask("Allowed scopes (comma-separated, optional):") do |q|
175
+ q.required false
176
+ end
177
+ scope_list = scopes&.split(",")&.map(&:strip)&.reject(&:empty?) || []
178
+
179
+ begin
180
+ result = registry.register(
181
+ name: secret_name,
182
+ env_var: env_var,
183
+ description: description,
184
+ scopes: scope_list
185
+ )
186
+
187
+ @prompt.ok("Secret '#{secret_name}' registered successfully")
188
+ @prompt.say(" ID: #{result[:id]}")
189
+ @prompt.say(" Env var: #{env_var}")
190
+ @prompt.say(" Registered at: #{result[:registered_at]}")
191
+
192
+ if scope_list.any?
193
+ @prompt.say(" Scopes: #{scope_list.join(", ")}")
194
+ end
195
+
196
+ @prompt.say("\nThe secret value will be proxied through short-lived tokens.")
197
+ @prompt.say("Agent processes will not have direct access to '#{env_var}'.")
198
+
199
+ 0
200
+ rescue => e
201
+ @prompt.error("Failed to register secret: #{e.message}")
202
+ Aidp.log_error("security_cli", "registration_failed",
203
+ name: secret_name,
204
+ error: e.message)
205
+ 1
206
+ end
207
+ end
208
+
209
+ # Run unregister command - remove a registered secret
210
+ def run_unregister(secret_name)
211
+ Aidp.log_info("security_cli", "unregistering_secret", name: secret_name)
212
+
213
+ registry = Aidp::Security.secrets_registry
214
+
215
+ unless registry.registered?(secret_name)
216
+ @prompt.error("Secret '#{secret_name}' is not registered")
217
+ return 1
218
+ end
219
+
220
+ # Confirm unregistration
221
+ unless @prompt.yes?("Are you sure you want to unregister '#{secret_name}'?")
222
+ @prompt.say("Cancelled")
223
+ return 0
224
+ end
225
+
226
+ # Revoke any active tokens for this secret
227
+ proxy = Aidp::Security.secrets_proxy
228
+ revoked_count = proxy.revoke_all_for_secret(secret_name)
229
+
230
+ if registry.unregister(name: secret_name)
231
+ @prompt.ok("Secret '#{secret_name}' unregistered")
232
+ if revoked_count > 0
233
+ @prompt.say(" Revoked #{revoked_count} active token(s)")
234
+ end
235
+ 0
236
+ else
237
+ @prompt.error("Failed to unregister secret")
238
+ 1
239
+ end
240
+ end
241
+
242
+ # Run list command - list registered secrets
243
+ def run_list
244
+ Aidp.log_debug("security_cli", "listing_secrets")
245
+
246
+ registry = Aidp::Security.secrets_registry
247
+ secrets = registry.list
248
+
249
+ if secrets.empty?
250
+ @prompt.say("\nNo secrets registered")
251
+ @prompt.say("Use 'aidp security register <name>' to register a secret")
252
+ return 0
253
+ end
254
+
255
+ @prompt.say("\n" + "=" * 60)
256
+ @prompt.say("Registered Secrets")
257
+ @prompt.say("=" * 60)
258
+
259
+ headers = ["Name", "Env Var", "Has Value", "Scopes", "Registered"]
260
+ rows = secrets.map do |secret|
261
+ scopes = secret[:scopes] || []
262
+ scope_str = scopes.any? ? scopes.join(", ") : "(any)"
263
+ has_value = secret[:has_value] ? "\u2713" : "\u2717"
264
+ registered = secret[:registered_at]&.split("T")&.first || "unknown"
265
+
266
+ [secret[:name], secret[:env_var], has_value, scope_str, registered]
267
+ end
268
+
269
+ table = TTY::Table.new(headers, rows)
270
+ @prompt.say(table.render(:unicode, padding: [0, 1]))
271
+
272
+ @prompt.say("\n#{secrets.count} secret(s) registered")
273
+ @prompt.say("")
274
+
275
+ 0
276
+ end
277
+
278
+ # Run proxy-status command - show secrets proxy status
279
+ def run_proxy_status
280
+ Aidp.log_debug("security_cli", "showing_proxy_status")
281
+
282
+ proxy = Aidp::Security.secrets_proxy
283
+ active_tokens = proxy.active_tokens_summary
284
+
285
+ @prompt.say("\n" + "=" * 60)
286
+ @prompt.say("Secrets Proxy Status")
287
+ @prompt.say("=" * 60)
288
+
289
+ @prompt.say("\nActive Tokens: #{active_tokens.count}")
290
+
291
+ if active_tokens.any?
292
+ headers = ["Secret", "Scope", "Expires In", "Used"]
293
+ rows = active_tokens.map do |token|
294
+ ttl = token[:remaining_ttl]
295
+ expires = (ttl > 60) ? "#{ttl / 60}m #{ttl % 60}s" : "#{ttl}s"
296
+ used = token[:used] ? "\u2713" : "\u2717"
297
+ [token[:secret_name], token[:scope] || "(any)", expires, used]
298
+ end
299
+
300
+ table = TTY::Table.new(headers, rows)
301
+ @prompt.say(table.render(:unicode, padding: [0, 1]))
302
+ end
303
+
304
+ # Show usage log
305
+ usage_log = proxy.usage_log(limit: 10)
306
+ if usage_log.any?
307
+ @prompt.say("\nRecent Token Usage (last 10):")
308
+ usage_log.each do |entry|
309
+ @prompt.say(" - #{entry[:secret_name]} (#{entry[:scope] || "any"}) at #{entry[:used_at]}")
310
+ end
311
+ end
312
+
313
+ @prompt.say("")
314
+ 0
315
+ end
316
+
317
+ # Default timeout for audit command (5 minutes)
318
+ AUDIT_TIMEOUT_SECONDS = 300
319
+
320
+ # Run audit command - run security audit tests
321
+ def run_audit(args)
322
+ Aidp.log_info("security_cli", "running_audit")
323
+
324
+ @prompt.say("\nRunning Security Audit...")
325
+ @prompt.say("=" * 40)
326
+
327
+ # Check for RSpec
328
+ rspec_path = File.join(@project_dir, "spec", "aidp", "security")
329
+
330
+ unless Dir.exist?(rspec_path)
331
+ @prompt.warn("Security spec directory not found: #{rspec_path}")
332
+ @prompt.say("Creating security audit scenarios...")
333
+
334
+ # Create the directory
335
+ FileUtils.mkdir_p(rspec_path)
336
+ @prompt.ok("Created #{rspec_path}")
337
+ end
338
+
339
+ # Run RSpec for security specs
340
+ @prompt.say("\nRunning security RSpec tests...")
341
+
342
+ # Check if there are any spec files
343
+ spec_files = Dir.glob(File.join(rspec_path, "**/*_spec.rb"))
344
+
345
+ if spec_files.empty?
346
+ @prompt.warn("No security spec files found")
347
+ @prompt.say("Add security tests to: #{rspec_path}")
348
+ return 0
349
+ end
350
+
351
+ # Run RSpec with timeout protection
352
+ cmd = "bundle exec rspec #{rspec_path} --format documentation"
353
+ @prompt.say("$ #{cmd}\n")
354
+ @prompt.say("(timeout: #{AUDIT_TIMEOUT_SECONDS / 60} minutes)\n")
355
+
356
+ exit_status = run_with_timeout(cmd, AUDIT_TIMEOUT_SECONDS)
357
+
358
+ case exit_status
359
+ when 0
360
+ @prompt.ok("\nSecurity audit passed")
361
+ when :timeout
362
+ @prompt.error("\nSecurity audit timed out after #{AUDIT_TIMEOUT_SECONDS / 60} minutes")
363
+ return 1
364
+ else
365
+ @prompt.error("\nSecurity audit failed")
366
+ end
367
+
368
+ exit_status.is_a?(Integer) ? exit_status : 1
369
+ end
370
+
371
+ private
372
+
373
+ # Run a command with timeout protection
374
+ # @param cmd [String] The command to execute
375
+ # @param timeout [Integer] Timeout in seconds
376
+ # @return [Integer, Symbol] Exit status or :timeout
377
+ def run_with_timeout(cmd, timeout)
378
+ pid = spawn(cmd)
379
+ start_time = Time.now
380
+
381
+ loop do
382
+ # Check if process has exited
383
+ result = Process.waitpid(pid, Process::WNOHANG)
384
+ return $?.exitstatus if result
385
+
386
+ # Check timeout
387
+ if Time.now - start_time > timeout
388
+ Process.kill("TERM", pid)
389
+ sleep 0.5
390
+ begin
391
+ Process.kill("KILL", pid)
392
+ rescue Errno::ESRCH
393
+ # Process already exited
394
+ end
395
+ Process.waitpid(pid)
396
+ return :timeout
397
+ end
398
+
399
+ sleep 0.5
400
+ end
401
+ rescue => e
402
+ Aidp.log_error("security_cli", "audit_execution_error", error: e.message)
403
+ 1
404
+ end
405
+
406
+ # Parse --env-var option from args
407
+ def parse_env_var_option(args)
408
+ idx = args.index("--env-var")
409
+ return nil unless idx
410
+
411
+ args.delete_at(idx) # Remove --env-var
412
+ args.delete_at(idx) # Remove the value and return it
413
+ end
414
+ end
415
+ end
416
+ end
@@ -21,9 +21,11 @@ module Aidp
21
21
  #
22
22
  # @param project_dir [String] Project directory path
23
23
  # @param prompt [TTY::Prompt] TTY prompt instance
24
- def initialize(project_dir: Dir.pwd, prompt: TTY::Prompt.new)
24
+ # @param query_class [Class] Query class for dependency injection (testing)
25
+ def initialize(project_dir: Dir.pwd, prompt: TTY::Prompt.new, query_class: nil)
25
26
  @project_dir = project_dir
26
27
  @prompt = prompt
28
+ @query_class = query_class || Metadata::Query
27
29
  end
28
30
 
29
31
  # Run tools command
@@ -79,7 +81,7 @@ module Aidp
79
81
  @prompt.say("\nValidating tool metadata...")
80
82
 
81
83
  cache = create_cache
82
- query = Metadata::Query.new(cache: cache)
84
+ query = @query_class.new(cache: cache)
83
85
 
84
86
  begin
85
87
  query.directory
@@ -116,7 +118,7 @@ module Aidp
116
118
  Aidp.log_info("tools", "Showing tool info", tool_id: tool_id)
117
119
 
118
120
  cache = create_cache
119
- query = Metadata::Query.new(cache: cache)
121
+ query = @query_class.new(cache: cache)
120
122
 
121
123
  tool = query.find_by_id(tool_id)
122
124
 
@@ -155,7 +157,7 @@ module Aidp
155
157
  Aidp.log_info("tools", "Listing all tools")
156
158
 
157
159
  cache = create_cache
158
- query = Metadata::Query.new(cache: cache)
160
+ query = @query_class.new(cache: cache)
159
161
 
160
162
  query.directory
161
163
  stats = query.statistics