aidp 0.12.1 → 0.14.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -0
  3. data/lib/aidp/analyze/json_file_storage.rb +21 -21
  4. data/lib/aidp/cli/enhanced_input.rb +114 -0
  5. data/lib/aidp/cli/first_run_wizard.rb +28 -309
  6. data/lib/aidp/cli/issue_importer.rb +359 -0
  7. data/lib/aidp/cli/mcp_dashboard.rb +3 -3
  8. data/lib/aidp/cli/terminal_io.rb +26 -0
  9. data/lib/aidp/cli.rb +155 -7
  10. data/lib/aidp/daemon/process_manager.rb +146 -0
  11. data/lib/aidp/daemon/runner.rb +232 -0
  12. data/lib/aidp/execute/async_work_loop_runner.rb +216 -0
  13. data/lib/aidp/execute/future_work_backlog.rb +411 -0
  14. data/lib/aidp/execute/guard_policy.rb +246 -0
  15. data/lib/aidp/execute/instruction_queue.rb +131 -0
  16. data/lib/aidp/execute/interactive_repl.rb +335 -0
  17. data/lib/aidp/execute/repl_macros.rb +651 -0
  18. data/lib/aidp/execute/steps.rb +8 -0
  19. data/lib/aidp/execute/work_loop_runner.rb +322 -36
  20. data/lib/aidp/execute/work_loop_state.rb +162 -0
  21. data/lib/aidp/harness/condition_detector.rb +6 -6
  22. data/lib/aidp/harness/config_loader.rb +23 -23
  23. data/lib/aidp/harness/config_manager.rb +61 -61
  24. data/lib/aidp/harness/config_schema.rb +88 -0
  25. data/lib/aidp/harness/config_validator.rb +9 -9
  26. data/lib/aidp/harness/configuration.rb +76 -29
  27. data/lib/aidp/harness/error_handler.rb +13 -13
  28. data/lib/aidp/harness/provider_config.rb +79 -79
  29. data/lib/aidp/harness/provider_factory.rb +40 -40
  30. data/lib/aidp/harness/provider_info.rb +37 -20
  31. data/lib/aidp/harness/provider_manager.rb +58 -53
  32. data/lib/aidp/harness/provider_type_checker.rb +6 -6
  33. data/lib/aidp/harness/runner.rb +7 -7
  34. data/lib/aidp/harness/status_display.rb +33 -46
  35. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -1
  36. data/lib/aidp/harness/ui/job_monitor.rb +7 -7
  37. data/lib/aidp/harness/user_interface.rb +43 -43
  38. data/lib/aidp/init/doc_generator.rb +256 -0
  39. data/lib/aidp/init/project_analyzer.rb +343 -0
  40. data/lib/aidp/init/runner.rb +83 -0
  41. data/lib/aidp/init.rb +5 -0
  42. data/lib/aidp/logger.rb +279 -0
  43. data/lib/aidp/providers/anthropic.rb +100 -26
  44. data/lib/aidp/providers/base.rb +13 -0
  45. data/lib/aidp/providers/codex.rb +28 -27
  46. data/lib/aidp/providers/cursor.rb +141 -34
  47. data/lib/aidp/providers/github_copilot.rb +26 -26
  48. data/lib/aidp/providers/macos_ui.rb +2 -18
  49. data/lib/aidp/providers/opencode.rb +26 -26
  50. data/lib/aidp/setup/wizard.rb +777 -0
  51. data/lib/aidp/tooling_detector.rb +115 -0
  52. data/lib/aidp/version.rb +1 -1
  53. data/lib/aidp/watch/build_processor.rb +282 -0
  54. data/lib/aidp/watch/plan_generator.rb +166 -0
  55. data/lib/aidp/watch/plan_processor.rb +83 -0
  56. data/lib/aidp/watch/repository_client.rb +243 -0
  57. data/lib/aidp/watch/runner.rb +93 -0
  58. data/lib/aidp/watch/state_store.rb +105 -0
  59. data/lib/aidp/watch.rb +9 -0
  60. data/lib/aidp/workflows/guided_agent.rb +344 -23
  61. data/lib/aidp.rb +14 -0
  62. data/templates/implementation/simple_task.md +36 -0
  63. metadata +27 -1
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "json"
5
+ require "fileutils"
6
+
7
+ module Aidp
8
+ # Unified structured logger for all AIDP operations
9
+ # Supports:
10
+ # - Multiple log levels (info, error, debug)
11
+ # - Text and JSONL formats
12
+ # - Automatic rotation
13
+ # - Redaction of secrets
14
+ # - Consistent file layout in .aidp/logs/
15
+ #
16
+ # Usage:
17
+ # Aidp.setup_logger(project_dir, config)
18
+ # Aidp.logger.info("component", "message", key: "value")
19
+ class AidpLogger
20
+ LEVELS = {
21
+ debug: ::Logger::DEBUG,
22
+ info: ::Logger::INFO,
23
+ warn: ::Logger::WARN,
24
+ error: ::Logger::ERROR
25
+ }.freeze
26
+
27
+ LOG_DIR = ".aidp/logs"
28
+ INFO_LOG = "#{LOG_DIR}/aidp.log"
29
+ DEBUG_LOG = "#{LOG_DIR}/aidp_debug.log"
30
+
31
+ DEFAULT_MAX_SIZE = 10 * 1024 * 1024 # 10MB
32
+ DEFAULT_MAX_FILES = 5
33
+
34
+ attr_reader :level, :json_format
35
+
36
+ def initialize(project_dir = Dir.pwd, config = {})
37
+ @project_dir = project_dir
38
+ @config = config
39
+ @level = determine_log_level
40
+ @json_format = config[:json] || false
41
+ @max_size = config[:max_size_mb] ? config[:max_size_mb] * 1024 * 1024 : DEFAULT_MAX_SIZE
42
+ @max_files = config[:max_backups] || DEFAULT_MAX_FILES
43
+
44
+ ensure_log_directory
45
+ migrate_old_logs if should_migrate?
46
+ setup_loggers
47
+ end
48
+
49
+ # Log info level message
50
+ def info(component, message, **metadata)
51
+ log(:info, component, message, **metadata)
52
+ end
53
+
54
+ # Log error level message
55
+ def error(component, message, **metadata)
56
+ log(:error, component, message, **metadata)
57
+ end
58
+
59
+ # Log warn level message
60
+ def warn(component, message, **metadata)
61
+ log(:warn, component, message, **metadata)
62
+ end
63
+
64
+ # Log debug level message
65
+ def debug(component, message, **metadata)
66
+ log(:debug, component, message, **metadata)
67
+ end
68
+
69
+ # Log at specified level
70
+ def log(level, component, message, **metadata)
71
+ return unless should_log?(level)
72
+
73
+ # Redact sensitive data
74
+ safe_message = redact(message)
75
+ safe_metadata = redact_hash(metadata)
76
+
77
+ # Log to appropriate file(s)
78
+ if level == :debug
79
+ write_to_debug(level, component, safe_message, safe_metadata)
80
+ else
81
+ write_to_info(level, component, safe_message, safe_metadata)
82
+ end
83
+
84
+ # Always log errors to both files
85
+ if level == :error
86
+ write_to_debug(level, component, safe_message, safe_metadata)
87
+ end
88
+ end
89
+
90
+ # Close all loggers
91
+ def close
92
+ @info_logger&.close
93
+ @debug_logger&.close
94
+ end
95
+
96
+ private
97
+
98
+ def determine_log_level
99
+ # Priority: ENV > config > default
100
+ level_str = ENV["AIDP_LOG_LEVEL"] || @config[:level] || "info"
101
+ level_sym = level_str.to_sym
102
+ LEVELS.key?(level_sym) ? level_sym : :info
103
+ end
104
+
105
+ def should_log?(level)
106
+ LEVELS[level] >= LEVELS[@level]
107
+ end
108
+
109
+ def ensure_log_directory
110
+ log_dir = File.join(@project_dir, LOG_DIR)
111
+ FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir)
112
+ end
113
+
114
+ def setup_loggers
115
+ info_path = File.join(@project_dir, INFO_LOG)
116
+ debug_path = File.join(@project_dir, DEBUG_LOG)
117
+
118
+ @info_logger = create_logger(info_path)
119
+ @debug_logger = create_logger(debug_path)
120
+ end
121
+
122
+ def create_logger(path)
123
+ logger = ::Logger.new(path, @max_files, @max_size)
124
+ logger.level = ::Logger::DEBUG # Control at write level instead
125
+ logger.formatter = proc { |severity, datetime, progname, msg| "#{msg}\n" }
126
+ logger
127
+ end
128
+
129
+ def write_to_info(level, component, message, metadata)
130
+ entry = format_entry(level, component, message, metadata)
131
+ @info_logger.send(logger_method(level), entry)
132
+ end
133
+
134
+ def write_to_debug(level, component, message, metadata)
135
+ entry = format_entry(level, component, message, metadata)
136
+ @debug_logger.send(logger_method(level), entry)
137
+ end
138
+
139
+ def logger_method(level)
140
+ case level
141
+ when :debug then :debug
142
+ when :info then :info
143
+ when :warn then :warn
144
+ when :error then :error
145
+ else :info
146
+ end
147
+ end
148
+
149
+ def format_entry(level, component, message, metadata)
150
+ if @json_format
151
+ format_json(level, component, message, metadata)
152
+ else
153
+ format_text(level, component, message, metadata)
154
+ end
155
+ end
156
+
157
+ def format_text(level, component, message, metadata)
158
+ timestamp = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
159
+ level_str = level.to_s.upcase
160
+ parts = ["#{timestamp} #{level_str} #{component} #{message}"]
161
+
162
+ unless metadata.empty?
163
+ metadata_str = metadata.map { |k, v| "#{k}=#{redact(v.to_s)}" }.join(" ")
164
+ parts << "(#{metadata_str})"
165
+ end
166
+
167
+ parts.join(" ")
168
+ end
169
+
170
+ def format_json(level, component, message, metadata)
171
+ entry = {
172
+ ts: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ"),
173
+ level: level.to_s,
174
+ component: component,
175
+ msg: message
176
+ }.merge(metadata)
177
+
178
+ JSON.generate(entry)
179
+ end
180
+
181
+ # Redaction patterns for common secrets
182
+ REDACTION_PATTERNS = [
183
+ # API keys and tokens (with capture groups)
184
+ [/\b(api[_-]?key|token|secret|password|passwd|pwd)[=:]\s*['"]?([^\s'")]+)['"]?/i, '\1=<REDACTED>'],
185
+ # Bearer tokens
186
+ [/Bearer\s+[A-Za-z0-9\-._~+\/]+=*/, "<REDACTED>"],
187
+ # GitHub tokens
188
+ [/\bgh[ps]_[A-Za-z0-9_]{36,}/, "<REDACTED>"],
189
+ # AWS keys
190
+ [/\bAKIA[0-9A-Z]{16}/, "<REDACTED>"],
191
+ # Generic secrets in key=value format
192
+ [/\b(secret|credentials?|auth)[=:]\s*['"]?([^\s'")]{8,})['"]?/i, '\1=<REDACTED>']
193
+ ].freeze
194
+
195
+ def redact(text)
196
+ return text unless text.is_a?(String)
197
+
198
+ redacted = text.dup
199
+ REDACTION_PATTERNS.each do |pattern, replacement|
200
+ redacted.gsub!(pattern, replacement)
201
+ end
202
+ redacted
203
+ end
204
+
205
+ def redact_hash(hash)
206
+ hash.transform_values { |v| v.is_a?(String) ? redact(v) : v }
207
+ end
208
+
209
+ # Migration from old debug_logs location
210
+ OLD_DEBUG_DIR = ".aidp/debug_logs"
211
+ OLD_DEBUG_LOG = "#{OLD_DEBUG_DIR}/aidp_debug.log"
212
+
213
+ def should_migrate?
214
+ old_path = File.join(@project_dir, OLD_DEBUG_LOG)
215
+ new_path = File.join(@project_dir, DEBUG_LOG)
216
+
217
+ # Migrate if old exists and new doesn't
218
+ File.exist?(old_path) && !File.exist?(new_path)
219
+ end
220
+
221
+ def migrate_old_logs
222
+ old_path = File.join(@project_dir, OLD_DEBUG_LOG)
223
+ new_path = File.join(@project_dir, DEBUG_LOG)
224
+
225
+ begin
226
+ FileUtils.mv(old_path, new_path)
227
+ log_migration_notice
228
+ rescue => e
229
+ # If migration fails, just continue (new logs will be created)
230
+ warn "Failed to migrate old logs: #{e.message}"
231
+ end
232
+ end
233
+
234
+ def log_migration_notice
235
+ notice = format_text(
236
+ :info,
237
+ "migration",
238
+ "Logs migrated from .aidp/debug_logs/ to .aidp/logs/",
239
+ timestamp: Time.now.utc.iso8601
240
+ )
241
+
242
+ # Write directly to avoid recursion
243
+ info_path = File.join(@project_dir, INFO_LOG)
244
+ File.open(info_path, "a") do |f|
245
+ f.puts notice
246
+ end
247
+ end
248
+ end
249
+
250
+ # Module-level logger accessor
251
+ class << self
252
+ # Set up global logger instance
253
+ def setup_logger(project_dir = Dir.pwd, config = {})
254
+ @logger = AidpLogger.new(project_dir, config)
255
+ end
256
+
257
+ # Get current logger instance (creates default if not set up)
258
+ def logger
259
+ @logger ||= AidpLogger.new
260
+ end
261
+
262
+ # Convenience logging methods
263
+ def log_info(component, message, **metadata)
264
+ logger.info(component, message, **metadata)
265
+ end
266
+
267
+ def log_error(component, message, **metadata)
268
+ logger.error(component, message, **metadata)
269
+ end
270
+
271
+ def log_warn(component, message, **metadata)
272
+ logger.warn(component, message, **metadata)
273
+ end
274
+
275
+ def log_debug(component, message, **metadata)
276
+ logger.debug(component, message, **metadata)
277
+ end
278
+ end
279
+ end
@@ -21,6 +21,25 @@ module Aidp
21
21
  "Anthropic Claude CLI"
22
22
  end
23
23
 
24
+ def supports_mcp?
25
+ true
26
+ end
27
+
28
+ def fetch_mcp_servers
29
+ return [] unless self.class.available?
30
+
31
+ begin
32
+ # Use claude mcp list command
33
+ result = debug_execute_command("claude", args: ["mcp", "list"], timeout: 5)
34
+ return [] unless result.exit_status == 0
35
+
36
+ parse_claude_mcp_output(result.out)
37
+ rescue => e
38
+ debug_log("Failed to fetch MCP servers via Claude CLI: #{e.message}", level: :debug)
39
+ []
40
+ end
41
+ end
42
+
24
43
  def available?
25
44
  self.class.available?
26
45
  end
@@ -101,11 +120,9 @@ module Aidp
101
120
  return ENV["AIDP_ANTHROPIC_TIMEOUT"].to_i
102
121
  end
103
122
 
104
- # Adaptive timeout based on step type
105
- step_timeout = get_adaptive_timeout
106
- if step_timeout
107
- display_message("🧠 Using adaptive timeout: #{step_timeout} seconds", type: :info)
108
- return step_timeout
123
+ if adaptive_timeout
124
+ display_message("🧠 Using adaptive timeout: #{adaptive_timeout} seconds", type: :info)
125
+ return adaptive_timeout
109
126
  end
110
127
 
111
128
  # Default timeout
@@ -113,27 +130,29 @@ module Aidp
113
130
  TIMEOUT_DEFAULT
114
131
  end
115
132
 
116
- def get_adaptive_timeout
117
- # Timeout recommendations based on step type patterns
118
- step_name = ENV["AIDP_CURRENT_STEP"] || ""
119
-
120
- case step_name
121
- when /REPOSITORY_ANALYSIS/
122
- TIMEOUT_REPOSITORY_ANALYSIS
123
- when /ARCHITECTURE_ANALYSIS/
124
- TIMEOUT_ARCHITECTURE_ANALYSIS
125
- when /TEST_ANALYSIS/
126
- TIMEOUT_TEST_ANALYSIS
127
- when /FUNCTIONALITY_ANALYSIS/
128
- TIMEOUT_FUNCTIONALITY_ANALYSIS
129
- when /DOCUMENTATION_ANALYSIS/
130
- TIMEOUT_DOCUMENTATION_ANALYSIS
131
- when /STATIC_ANALYSIS/
132
- TIMEOUT_STATIC_ANALYSIS
133
- when /REFACTORING_RECOMMENDATIONS/
134
- TIMEOUT_REFACTORING_RECOMMENDATIONS
135
- else
136
- nil # Use default
133
+ def adaptive_timeout
134
+ @adaptive_timeout ||= begin
135
+ # Timeout recommendations based on step type patterns
136
+ step_name = ENV["AIDP_CURRENT_STEP"] || ""
137
+
138
+ case step_name
139
+ when /REPOSITORY_ANALYSIS/
140
+ TIMEOUT_REPOSITORY_ANALYSIS
141
+ when /ARCHITECTURE_ANALYSIS/
142
+ TIMEOUT_ARCHITECTURE_ANALYSIS
143
+ when /TEST_ANALYSIS/
144
+ TIMEOUT_TEST_ANALYSIS
145
+ when /FUNCTIONALITY_ANALYSIS/
146
+ TIMEOUT_FUNCTIONALITY_ANALYSIS
147
+ when /DOCUMENTATION_ANALYSIS/
148
+ TIMEOUT_DOCUMENTATION_ANALYSIS
149
+ when /STATIC_ANALYSIS/
150
+ TIMEOUT_STATIC_ANALYSIS
151
+ when /REFACTORING_RECOMMENDATIONS/
152
+ TIMEOUT_REFACTORING_RECOMMENDATIONS
153
+ else
154
+ nil # Use default
155
+ end
137
156
  end
138
157
  end
139
158
 
@@ -184,6 +203,61 @@ module Aidp
184
203
  # Return original output if parsing fails
185
204
  output
186
205
  end
206
+
207
+ # Parse Claude MCP server list output
208
+ def parse_claude_mcp_output(output)
209
+ servers = []
210
+ return servers unless output
211
+
212
+ lines = output.lines
213
+ lines.reject! { |line| /checking mcp server health/i.match?(line) }
214
+
215
+ lines.each do |line|
216
+ line = line.strip
217
+ next if line.empty?
218
+
219
+ # Try to parse Claude format: "name: command - ✓ Connected"
220
+ if line =~ /^([^:]+):\s*(.+?)\s*-\s*(✓|✗)\s*(.+)$/
221
+ name = Regexp.last_match(1).strip
222
+ command = Regexp.last_match(2).strip
223
+ status_symbol = Regexp.last_match(3)
224
+ status_text = Regexp.last_match(4).strip
225
+
226
+ servers << {
227
+ name: name,
228
+ status: (status_symbol == "✓") ? "connected" : "error",
229
+ description: command,
230
+ enabled: status_symbol == "✓",
231
+ error: (status_symbol == "✗") ? status_text : nil,
232
+ source: "claude_cli"
233
+ }
234
+ next
235
+ end
236
+
237
+ # Try to parse legacy table format
238
+ next if /Name.*Status/i.match?(line)
239
+ next if /^[-=]+$/.match?(line)
240
+
241
+ parts = line.split(/\s{2,}/)
242
+ next if parts.size < 2
243
+
244
+ name = parts[0]&.strip
245
+ status = parts[1]&.strip
246
+ description = parts[2..]&.join(" ")&.strip
247
+
248
+ next unless name && !name.empty?
249
+
250
+ servers << {
251
+ name: name,
252
+ status: status || "unknown",
253
+ description: description,
254
+ enabled: status&.downcase == "enabled" || status&.downcase == "connected",
255
+ source: "claude_cli"
256
+ }
257
+ end
258
+
259
+ servers
260
+ end
187
261
  end
188
262
  end
189
263
  end
@@ -73,6 +73,19 @@ module Aidp
73
73
  raise NotImplementedError, "#{self.class} must implement #send"
74
74
  end
75
75
 
76
+ # Fetch MCP servers configured for this provider
77
+ # Returns an array of server hashes with keys: :name, :status, :description, :enabled
78
+ # Override in subclasses to provide provider-specific MCP server detection
79
+ def fetch_mcp_servers
80
+ []
81
+ end
82
+
83
+ # Check if this provider supports MCP servers
84
+ # Override in subclasses to provide accurate MCP support detection
85
+ def supports_mcp?
86
+ false
87
+ end
88
+
76
89
  # Set job context for background execution
77
90
  def set_job_context(job_id:, execution_id:, job_manager:)
78
91
  @job_context = {
@@ -120,8 +120,9 @@ module Aidp
120
120
  args += ["--model", model]
121
121
  end
122
122
 
123
- # Add approval flag
123
+ # Add approval flag - but warn about interactive behavior
124
124
  if ask_for_approval
125
+ debug_log("⚠️ WARNING: --ask-for-approval flag may cause interactive prompts that could hang AIDP", level: :warn)
125
126
  args += ["--ask-for-approval"]
126
127
  end
127
128
 
@@ -196,11 +197,9 @@ module Aidp
196
197
  return ENV["AIDP_CODEX_TIMEOUT"].to_i
197
198
  end
198
199
 
199
- # Adaptive timeout based on step type
200
- step_timeout = get_adaptive_timeout
201
- if step_timeout
202
- display_message("🧠 Using adaptive timeout: #{step_timeout} seconds", type: :info)
203
- return step_timeout
200
+ if adaptive_timeout
201
+ display_message("🧠 Using adaptive timeout: #{adaptive_timeout} seconds", type: :info)
202
+ return adaptive_timeout
204
203
  end
205
204
 
206
205
  # Default timeout
@@ -208,27 +207,29 @@ module Aidp
208
207
  TIMEOUT_DEFAULT
209
208
  end
210
209
 
211
- def get_adaptive_timeout
212
- # Timeout recommendations based on step type patterns
213
- step_name = ENV["AIDP_CURRENT_STEP"] || ""
214
-
215
- case step_name
216
- when /REPOSITORY_ANALYSIS/
217
- TIMEOUT_REPOSITORY_ANALYSIS
218
- when /ARCHITECTURE_ANALYSIS/
219
- TIMEOUT_ARCHITECTURE_ANALYSIS
220
- when /TEST_ANALYSIS/
221
- TIMEOUT_TEST_ANALYSIS
222
- when /FUNCTIONALITY_ANALYSIS/
223
- TIMEOUT_FUNCTIONALITY_ANALYSIS
224
- when /DOCUMENTATION_ANALYSIS/
225
- TIMEOUT_DOCUMENTATION_ANALYSIS
226
- when /STATIC_ANALYSIS/
227
- TIMEOUT_STATIC_ANALYSIS
228
- when /REFACTORING_RECOMMENDATIONS/
229
- TIMEOUT_REFACTORING_RECOMMENDATIONS
230
- else
231
- nil # Use default
210
+ def adaptive_timeout
211
+ @adaptive_timeout ||= begin
212
+ # Timeout recommendations based on step type patterns
213
+ step_name = ENV["AIDP_CURRENT_STEP"] || ""
214
+
215
+ case step_name
216
+ when /REPOSITORY_ANALYSIS/
217
+ TIMEOUT_REPOSITORY_ANALYSIS
218
+ when /ARCHITECTURE_ANALYSIS/
219
+ TIMEOUT_ARCHITECTURE_ANALYSIS
220
+ when /TEST_ANALYSIS/
221
+ TIMEOUT_TEST_ANALYSIS
222
+ when /FUNCTIONALITY_ANALYSIS/
223
+ TIMEOUT_FUNCTIONALITY_ANALYSIS
224
+ when /DOCUMENTATION_ANALYSIS/
225
+ TIMEOUT_DOCUMENTATION_ANALYSIS
226
+ when /STATIC_ANALYSIS/
227
+ TIMEOUT_STATIC_ANALYSIS
228
+ when /REFACTORING_RECOMMENDATIONS/
229
+ TIMEOUT_REFACTORING_RECOMMENDATIONS
230
+ else
231
+ nil # Use default
232
+ end
232
233
  end
233
234
  end
234
235