aidp 0.24.0 → 0.26.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +72 -7
  3. data/lib/aidp/analyze/error_handler.rb +11 -0
  4. data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
  5. data/lib/aidp/auto_update/checkpoint.rb +178 -0
  6. data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
  7. data/lib/aidp/auto_update/coordinator.rb +204 -0
  8. data/lib/aidp/auto_update/errors.rb +17 -0
  9. data/lib/aidp/auto_update/failure_tracker.rb +162 -0
  10. data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
  11. data/lib/aidp/auto_update/update_check.rb +106 -0
  12. data/lib/aidp/auto_update/update_logger.rb +143 -0
  13. data/lib/aidp/auto_update/update_policy.rb +109 -0
  14. data/lib/aidp/auto_update/version_detector.rb +144 -0
  15. data/lib/aidp/auto_update.rb +52 -0
  16. data/lib/aidp/cli.rb +165 -1
  17. data/lib/aidp/execute/work_loop_runner.rb +225 -55
  18. data/lib/aidp/harness/config_loader.rb +20 -11
  19. data/lib/aidp/harness/config_schema.rb +80 -8
  20. data/lib/aidp/harness/configuration.rb +73 -2
  21. data/lib/aidp/harness/filter_strategy.rb +45 -0
  22. data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
  23. data/lib/aidp/harness/output_filter.rb +136 -0
  24. data/lib/aidp/harness/provider_factory.rb +2 -0
  25. data/lib/aidp/harness/provider_manager.rb +18 -3
  26. data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
  27. data/lib/aidp/harness/test_runner.rb +165 -27
  28. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
  29. data/lib/aidp/logger.rb +35 -5
  30. data/lib/aidp/message_display.rb +56 -2
  31. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
  32. data/lib/aidp/provider_manager.rb +2 -0
  33. data/lib/aidp/providers/kilocode.rb +202 -0
  34. data/lib/aidp/safe_directory.rb +10 -3
  35. data/lib/aidp/setup/provider_registry.rb +15 -0
  36. data/lib/aidp/setup/wizard.rb +12 -4
  37. data/lib/aidp/skills/composer.rb +4 -0
  38. data/lib/aidp/skills/loader.rb +3 -1
  39. data/lib/aidp/storage/csv_storage.rb +9 -3
  40. data/lib/aidp/storage/file_manager.rb +8 -2
  41. data/lib/aidp/storage/json_storage.rb +9 -3
  42. data/lib/aidp/version.rb +1 -1
  43. data/lib/aidp/watch/build_processor.rb +106 -17
  44. data/lib/aidp/watch/change_request_processor.rb +659 -0
  45. data/lib/aidp/watch/ci_fix_processor.rb +448 -0
  46. data/lib/aidp/watch/plan_processor.rb +81 -8
  47. data/lib/aidp/watch/repository_client.rb +465 -20
  48. data/lib/aidp/watch/review_processor.rb +266 -0
  49. data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
  50. data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
  51. data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
  52. data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
  53. data/lib/aidp/watch/runner.rb +222 -0
  54. data/lib/aidp/watch/state_store.rb +99 -1
  55. data/lib/aidp/workstream_executor.rb +5 -2
  56. data/lib/aidp.rb +5 -0
  57. data/templates/aidp.yml.example +53 -0
  58. metadata +25 -1
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "open3"
4
4
  require_relative "../tooling_detector"
5
+ require_relative "output_filter"
5
6
 
6
7
  module Aidp
7
8
  module Harness
@@ -11,30 +12,68 @@ module Aidp
11
12
  def initialize(project_dir, config)
12
13
  @project_dir = project_dir
13
14
  @config = config
15
+ @iteration_count = 0
14
16
  end
15
17
 
16
18
  # Run all configured tests
17
- # Returns: { success: boolean, output: string, failures: array }
19
+ # Returns: { success: boolean, output: string, failures: array, required_failures: array }
18
20
  def run_tests
19
- test_commands = resolved_test_commands
20
- return {success: true, output: "", failures: []} if test_commands.empty?
21
-
22
- results = test_commands.map { |cmd| execute_command(cmd, "test") }
23
- aggregate_results(results, "Tests")
21
+ @iteration_count += 1
22
+ run_command_category(:test, "Tests")
24
23
  end
25
24
 
26
25
  # Run all configured linters
27
- # Returns: { success: boolean, output: string, failures: array }
26
+ # Returns: { success: boolean, output: string, failures: array, required_failures: array }
28
27
  def run_linters
29
- lint_commands = resolved_lint_commands
30
- return {success: true, output: "", failures: []} if lint_commands.empty?
28
+ @iteration_count += 1
29
+ run_command_category(:lint, "Linters")
30
+ end
31
31
 
32
- results = lint_commands.map { |cmd| execute_command(cmd, "linter") }
33
- aggregate_results(results, "Linters")
32
+ # Run all configured formatters
33
+ # Returns: { success: boolean, output: string, failures: array, required_failures: array }
34
+ def run_formatters
35
+ run_command_category(:formatter, "Formatters")
36
+ end
37
+
38
+ # Run all configured build commands
39
+ # Returns: { success: boolean, output: string, failures: array, required_failures: array }
40
+ def run_builds
41
+ run_command_category(:build, "Build")
42
+ end
43
+
44
+ # Run all configured documentation commands
45
+ # Returns: { success: boolean, output: string, failures: array, required_failures: array }
46
+ def run_documentation
47
+ run_command_category(:documentation, "Documentation")
34
48
  end
35
49
 
36
50
  private
37
51
 
52
+ # Run commands for a specific category (test, lint, formatter, build, documentation)
53
+ def run_command_category(category, display_name)
54
+ commands = resolved_commands(category)
55
+
56
+ # If no commands configured, return success (empty check passes)
57
+ return {success: true, output: "", failures: [], required_failures: []} if commands.empty?
58
+
59
+ # Determine output mode based on category
60
+ mode = determine_output_mode(category)
61
+
62
+ # Execute all commands
63
+ results = commands.map do |cmd_config|
64
+ # Handle both string commands (legacy) and hash format (new)
65
+ if cmd_config.is_a?(String)
66
+ result = execute_command(cmd_config, category.to_s)
67
+ result.merge(required: true)
68
+ else
69
+ result = execute_command(cmd_config[:command], category.to_s)
70
+ result.merge(required: cmd_config[:required])
71
+ end
72
+ end
73
+
74
+ aggregate_results(results, display_name, mode: mode)
75
+ end
76
+
38
77
  def execute_command(command, type)
39
78
  stdout, stderr, status = Open3.capture3(command, chdir: @project_dir)
40
79
 
@@ -48,53 +87,152 @@ module Aidp
48
87
  }
49
88
  end
50
89
 
51
- def aggregate_results(results, category)
52
- failures = results.reject { |r| r[:success] }
53
- success = failures.empty?
90
+ def aggregate_results(results, category, mode: :full)
91
+ # Separate required and optional command failures
92
+ all_failures = results.reject { |r| r[:success] }
93
+ required_failures = all_failures.select { |r| r[:required] }
94
+ optional_failures = all_failures.reject { |r| r[:required] }
95
+
96
+ # Success only if all REQUIRED commands pass
97
+ # Optional command failures don't block completion
98
+ success = required_failures.empty?
54
99
 
55
- output = if success
56
- "#{category}: All passed"
100
+ output = if all_failures.empty?
101
+ "#{category}: All passed (#{results.length} commands)"
102
+ elsif required_failures.empty?
103
+ "#{category}: Required checks passed (#{optional_failures.length} optional warnings)\n" +
104
+ format_failures(optional_failures, "#{category} - Optional", mode: mode)
57
105
  else
58
- format_failures(failures, category)
106
+ format_failures(required_failures, "#{category} - Required", mode: mode) +
107
+ (optional_failures.any? ? "\n" + format_failures(optional_failures, "#{category} - Optional", mode: mode) : "")
59
108
  end
60
109
 
61
110
  {
62
111
  success: success,
63
112
  output: output,
64
- failures: failures
113
+ failures: all_failures,
114
+ required_failures: required_failures,
115
+ optional_failures: optional_failures
65
116
  }
66
117
  end
67
118
 
68
- def format_failures(failures, category)
119
+ def format_failures(failures, category, mode: :full)
69
120
  output = ["#{category} Failures:", ""]
70
121
 
71
122
  failures.each do |failure|
72
123
  output << "Command: #{failure[:command]}"
73
124
  output << "Exit Code: #{failure[:exit_code]}"
74
125
  output << "--- Output ---"
75
- output << failure[:stdout] unless failure[:stdout].strip.empty?
76
- output << failure[:stderr] unless failure[:stderr].strip.empty?
126
+
127
+ # Apply filtering based on mode and framework
128
+ filtered_stdout = filter_output(failure[:stdout], mode, detect_framework_from_command(failure[:command]))
129
+ filtered_stderr = filter_output(failure[:stderr], mode, :unknown)
130
+
131
+ output << filtered_stdout unless filtered_stdout.strip.empty?
132
+ output << filtered_stderr unless filtered_stderr.strip.empty?
77
133
  output << ""
78
134
  end
79
135
 
80
136
  output.join("\n")
81
137
  end
82
138
 
139
+ def filter_output(raw_output, mode, framework)
140
+ return raw_output if mode == :full || raw_output.nil? || raw_output.empty?
141
+
142
+ filter_config = {
143
+ mode: mode,
144
+ include_context: true,
145
+ context_lines: 3,
146
+ max_lines: 500
147
+ }
148
+
149
+ filter = OutputFilter.new(filter_config)
150
+ filter.filter(raw_output, framework: framework)
151
+ rescue NameError
152
+ # Logging infrastructure not available
153
+ raw_output
154
+ rescue => e
155
+ Aidp.log_warn("test_runner", "filter_failed",
156
+ error: e.message,
157
+ framework: framework)
158
+ raw_output # Fallback to unfiltered on error
159
+ end
160
+
161
+ def detect_framework_from_command(command)
162
+ case command
163
+ when /rspec/
164
+ :rspec
165
+ when /minitest/
166
+ :minitest
167
+ when /jest/
168
+ :jest
169
+ when /pytest/
170
+ :pytest
171
+ else
172
+ :unknown
173
+ end
174
+ end
175
+
176
+ def determine_output_mode(category)
177
+ # Check config for category-specific mode
178
+ case category
179
+ when :test
180
+ if @config.respond_to?(:test_output_mode)
181
+ @config.test_output_mode
182
+ elsif @iteration_count > 1
183
+ :failures_only
184
+ else
185
+ :full
186
+ end
187
+ when :lint
188
+ if @config.respond_to?(:lint_output_mode)
189
+ @config.lint_output_mode
190
+ elsif @iteration_count > 1
191
+ :failures_only
192
+ else
193
+ :full
194
+ end
195
+ else
196
+ :full
197
+ end
198
+ end
199
+
200
+ # Resolve commands for a specific category
201
+ # Returns normalized command configs (array of {command:, required:} hashes)
202
+ def resolved_commands(category)
203
+ case category
204
+ when :test
205
+ resolved_test_commands
206
+ when :lint
207
+ resolved_lint_commands
208
+ when :formatter
209
+ @config.formatter_commands
210
+ when :build
211
+ @config.build_commands
212
+ when :documentation
213
+ @config.documentation_commands
214
+ else
215
+ []
216
+ end
217
+ end
218
+
83
219
  def resolved_test_commands
84
- explicit = Array(@config.test_commands).compact.map(&:strip).reject(&:empty?)
220
+ explicit = @config.test_commands
85
221
  return explicit unless explicit.empty?
86
222
 
87
- detected = detected_tooling.test_commands
88
- log_fallback(:tests, detected) unless detected.empty?
223
+ # Auto-detect test commands if none explicitly configured
224
+ detected = detected_tooling.test_commands.map { |cmd| {command: cmd, required: true} }
225
+ log_fallback(:tests, detected.map { |c| c[:command] }) unless detected.empty?
89
226
  detected
90
227
  end
91
228
 
92
229
  def resolved_lint_commands
93
- explicit = Array(@config.lint_commands).compact.map(&:strip).reject(&:empty?)
230
+ explicit = @config.lint_commands
94
231
  return explicit unless explicit.empty?
95
232
 
96
- detected = detected_tooling.lint_commands
97
- log_fallback(:linters, detected) unless detected.empty?
233
+ # Auto-detect lint commands if none explicitly configured
234
+ detected = detected_tooling.lint_commands.map { |cmd| {command: cmd, required: true} }
235
+ log_fallback(:linters, detected.map { |c| c[:command] }) unless detected.empty?
98
236
  detected
99
237
  end
100
238
 
@@ -23,13 +23,16 @@ module Aidp
23
23
  def initialize(prompt: TTY::Prompt.new, tty: $stdin)
24
24
  @cursor = TTY::Cursor
25
25
  @screen = TTY::Screen
26
- @pastel = Pastel.new
27
26
  @prompt = prompt
28
27
 
29
28
  # Headless (non-interactive) detection for test/CI environments:
30
29
  # - STDIN not a TTY (captured by PTY/tmux harness or test environment)
31
30
  @headless = !!(tty.nil? || !tty.tty?)
32
31
 
32
+ # Initialize Pastel with disabled colors in headless mode to avoid
33
+ # "closed stream" errors when checking TTY capabilities
34
+ @pastel = Pastel.new(enabled: !@headless)
35
+
33
36
  @current_mode = nil
34
37
  @workflow_active = false
35
38
  @current_step = nil
data/lib/aidp/logger.rb CHANGED
@@ -18,6 +18,27 @@ module Aidp
18
18
  # Aidp.setup_logger(project_dir, config)
19
19
  # Aidp.logger.info("component", "message", key: "value")
20
20
  class Logger
21
+ # Custom log device that suppresses IOError exceptions for closed streams.
22
+ # This prevents "log shifting failed. closed stream" errors during test cleanup.
23
+ class SafeLogDevice < ::Logger::LogDevice
24
+ private
25
+
26
+ # Override handle_write_errors to suppress warnings for closed stream errors.
27
+ # Ruby's Logger::LogDevice calls warn() when write operations fail, which
28
+ # produces "log shifting failed. closed stream" messages during test cleanup.
29
+ def handle_write_errors(mesg)
30
+ yield
31
+ rescue *@reraise_write_errors
32
+ raise
33
+ rescue IOError => e
34
+ # Silently ignore closed stream errors - these are expected during
35
+ # test cleanup when loggers are finalized after streams are closed
36
+ return if e.message.include?("closed stream")
37
+ warn("log #{mesg} failed. #{e}")
38
+ rescue => e
39
+ warn("log #{mesg} failed. #{e}")
40
+ end
41
+ end
21
42
  LEVELS = {
22
43
  debug: ::Logger::DEBUG,
23
44
  info: ::Logger::INFO,
@@ -140,14 +161,17 @@ module Aidp
140
161
  dir = File.dirname(path)
141
162
  FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
142
163
 
143
- logger = ::Logger.new(path, @max_files, @max_size)
164
+ # Create logger with custom SafeLogDevice that suppresses closed stream errors
165
+ logdev = SafeLogDevice.new(path, shift_age: @max_files, shift_size: @max_size)
166
+ logger = ::Logger.new(logdev)
144
167
  logger.level = ::Logger::DEBUG # Control at write level instead
145
168
  logger.formatter = proc { |severity, datetime, progname, msg| "#{msg}\n" }
146
169
  logger
147
170
  rescue => e
148
171
  # Fall back to STDERR if file logging fails
149
172
  Kernel.warn "[AIDP Logger] Failed to create log file at #{path}: #{e.message}. Falling back to STDERR."
150
- logger = ::Logger.new($stderr)
173
+ logdev = SafeLogDevice.new($stderr)
174
+ logger = ::Logger.new(logdev)
151
175
  logger.level = ::Logger::DEBUG
152
176
  logger.formatter = proc { |severity, datetime, progname, msg| "#{msg}\n" }
153
177
  logger
@@ -246,7 +270,9 @@ module Aidp
246
270
  raw_input = dir.to_s
247
271
  raw_invalid = raw_input.empty? || raw_input.match?(/[<>|]/) || raw_input.match?(/[\x00-\x1F]/)
248
272
  if raw_invalid
249
- Kernel.warn "[AIDP Logger] Invalid project_dir '#{raw_input}' - falling back to #{Dir.pwd}"
273
+ unless ENV["RSPEC_RUNNING"] == "true"
274
+ Kernel.warn "[AIDP Logger] Invalid project_dir '#{raw_input}' - falling back to #{Dir.pwd}"
275
+ end
250
276
  if Dir.pwd == File::SEPARATOR
251
277
  fallback = begin
252
278
  home = Dir.home
@@ -255,7 +281,9 @@ module Aidp
255
281
  Dir.tmpdir
256
282
  end
257
283
  @root_fallback = fallback
258
- Kernel.warn "[AIDP Logger] Root directory detected - using #{fallback} for logging instead of '#{Dir.pwd}'"
284
+ unless ENV["RSPEC_RUNNING"] == "true"
285
+ Kernel.warn "[AIDP Logger] Root directory detected - using #{fallback} for logging instead of '#{Dir.pwd}'"
286
+ end
259
287
  return fallback
260
288
  end
261
289
  return Dir.pwd
@@ -268,7 +296,9 @@ module Aidp
268
296
  Dir.tmpdir
269
297
  end
270
298
  @root_fallback = fallback
271
- Kernel.warn "[AIDP Logger] Root directory detected - using #{fallback} for logging instead of '#{raw_input}'"
299
+ unless ENV["RSPEC_RUNNING"] == "true"
300
+ Kernel.warn "[AIDP Logger] Root directory detected - using #{fallback} for logging instead of '#{raw_input}'"
301
+ end
272
302
  return fallback
273
303
  end
274
304
  raw_input
@@ -25,8 +25,13 @@ module Aidp
25
25
 
26
26
  # Instance helper for displaying a colored message via TTY::Prompt
27
27
  def display_message(message, type: :info)
28
+ return if suppress_display_message?(message)
29
+ # Ensure message is UTF-8 encoded to handle emoji and special characters
30
+ message_str = message.to_s
31
+ message_str = message_str.force_encoding("UTF-8") if message_str.encoding.name == "ASCII-8BIT"
32
+ message_str = message_str.encode("UTF-8", invalid: :replace, undef: :replace)
28
33
  prompt = message_display_prompt
29
- prompt.say(message, color: COLOR_MAP.fetch(type, :white))
34
+ prompt.say(message_str, color: COLOR_MAP.fetch(type, :white))
30
35
  end
31
36
 
32
37
  # Provide a memoized prompt per including instance (if it defines @prompt)
@@ -38,14 +43,63 @@ module Aidp
38
43
  end
39
44
  end
40
45
 
46
+ # Check if specific display message should be suppressed in test/CI environments
47
+ def suppress_display_message?(message)
48
+ return false unless in_test_environment?
49
+
50
+ message_str = message.to_s
51
+ # Only suppress specific automated status messages, not CLI output
52
+ message_str.include?("🔄 Provider switch:") ||
53
+ message_str.include?("🔄 Model switch:") ||
54
+ message_str.include?("🔴 Circuit breaker opened") ||
55
+ message_str.include?("🟢 Circuit breaker reset") ||
56
+ message_str.include?("❌ No providers available") ||
57
+ message_str.include?("❌ No models available") ||
58
+ message_str.include?("📊 Execution Summary") ||
59
+ message_str.include?("▶️ [") || # Workstream execution messages
60
+ message_str.include?("✅ [") || # Workstream success messages
61
+ message_str.include?("❌ [") # Workstream failure messages
62
+ end
63
+
64
+ def in_test_environment?
65
+ ENV["RSPEC_RUNNING"] || ENV["CI"] || ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
66
+ end
67
+
41
68
  module ClassMethods
42
69
  # Class-level display helper (uses fresh prompt to respect $stdout changes)
43
70
  def display_message(message, type: :info)
44
- class_message_display_prompt.say(message, color: COLOR_MAP.fetch(type, :white))
71
+ return if suppress_display_message?(message)
72
+ # Ensure message is UTF-8 encoded to handle emoji and special characters
73
+ message_str = message.to_s
74
+ message_str = message_str.force_encoding("UTF-8") if message_str.encoding.name == "ASCII-8BIT"
75
+ message_str = message_str.encode("UTF-8", invalid: :replace, undef: :replace)
76
+ class_message_display_prompt.say(message_str, color: COLOR_MAP.fetch(type, :white))
45
77
  end
46
78
 
47
79
  private
48
80
 
81
+ # Check if specific display message should be suppressed in test/CI environments
82
+ def suppress_display_message?(message)
83
+ return false unless in_test_environment?
84
+
85
+ message_str = message.to_s
86
+ # Only suppress specific automated status messages, not CLI output
87
+ message_str.include?("🔄 Provider switch:") ||
88
+ message_str.include?("🔄 Model switch:") ||
89
+ message_str.include?("🔴 Circuit breaker opened") ||
90
+ message_str.include?("🟢 Circuit breaker reset") ||
91
+ message_str.include?("❌ No providers available") ||
92
+ message_str.include?("❌ No models available") ||
93
+ message_str.include?("📊 Execution Summary") ||
94
+ message_str.include?("▶️ [") || # Workstream execution messages
95
+ message_str.include?("✅ [") || # Workstream success messages
96
+ message_str.include?("❌ [") # Workstream failure messages
97
+ end
98
+
99
+ def in_test_environment?
100
+ ENV["RSPEC_RUNNING"] || ENV["CI"] || ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
101
+ end
102
+
49
103
  # Don't memoize - create fresh prompt each time to respect $stdout redirection in tests
50
104
  def class_message_display_prompt
51
105
  TTY::Prompt.new
@@ -82,7 +82,7 @@ module Aidp
82
82
  guide_path = File.join(@project_dir, "docs", "LLM_STYLE_GUIDE.md")
83
83
  return nil unless File.exist?(guide_path)
84
84
 
85
- File.read(guide_path)
85
+ File.read(guide_path, encoding: "UTF-8")
86
86
  end
87
87
 
88
88
  # Parse markdown content into fragments
@@ -90,6 +90,8 @@ module Aidp
90
90
  # @param content [String] Markdown content
91
91
  # @return [Array<Fragment>] Parsed fragments
92
92
  def parse_fragments(content)
93
+ # Ensure content is UTF-8 encoded
94
+ content = content.encode("UTF-8", invalid: :replace, undef: :replace) unless content.encoding == Encoding::UTF_8
93
95
  lines = content.lines
94
96
  current_content = []
95
97
  current_heading = nil
@@ -142,6 +142,8 @@ module Aidp
142
142
  Aidp::Providers::Anthropic.new(prompt: prompt)
143
143
  when "gemini"
144
144
  Aidp::Providers::Gemini.new(prompt: prompt)
145
+ when "kilocode"
146
+ Aidp::Providers::Kilocode.new(prompt: prompt)
145
147
  when "github_copilot"
146
148
  Aidp::Providers::GithubCopilot.new(prompt: prompt)
147
149
  when "codex"
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+ require_relative "base"
5
+ require_relative "../util"
6
+ require_relative "../debug_mixin"
7
+
8
+ module Aidp
9
+ module Providers
10
+ class Kilocode < Base
11
+ include Aidp::DebugMixin
12
+
13
+ def self.available?
14
+ !!Aidp::Util.which("kilocode")
15
+ end
16
+
17
+ def name
18
+ "kilocode"
19
+ end
20
+
21
+ def display_name
22
+ "Kilocode"
23
+ end
24
+
25
+ def send_message(prompt:, session: nil)
26
+ raise "kilocode not available" unless self.class.available?
27
+
28
+ # Smart timeout calculation
29
+ timeout_seconds = calculate_timeout
30
+
31
+ debug_provider("kilocode", "Starting execution", {timeout: timeout_seconds})
32
+ debug_log("📝 Sending prompt to kilocode (length: #{prompt.length})", level: :info)
33
+
34
+ # Check if streaming mode is enabled
35
+ streaming_enabled = ENV["AIDP_STREAMING"] == "1" || ENV["DEBUG"] == "1"
36
+ if streaming_enabled
37
+ display_message("📺 Display streaming enabled - output buffering reduced", type: :info)
38
+ end
39
+
40
+ # Check if prompt is too large and warn
41
+ if prompt.length > 3000
42
+ debug_log("⚠️ Large prompt detected (#{prompt.length} chars) - this may cause rate limiting", level: :warn)
43
+ end
44
+
45
+ # Set up activity monitoring
46
+ setup_activity_monitoring("kilocode", method(:activity_callback))
47
+ record_activity("Starting kilocode execution")
48
+
49
+ # Create a spinner for activity display
50
+ spinner = TTY::Spinner.new("[:spinner] :title", format: :dots, hide_cursor: true)
51
+ spinner.auto_spin
52
+
53
+ activity_display_thread = Thread.new do
54
+ start_time = Time.now
55
+ loop do
56
+ sleep 0.5 # Update every 500ms to reduce spam
57
+ elapsed = Time.now - start_time
58
+
59
+ # Break if we've been running too long or state changed
60
+ break if elapsed > timeout_seconds || @activity_state == :completed || @activity_state == :failed
61
+
62
+ update_spinner_status(spinner, elapsed, "🔄 kilocode")
63
+ end
64
+ end
65
+
66
+ begin
67
+ # Build kilocode command arguments
68
+ args = ["--auto"]
69
+
70
+ # Add model if specified
71
+ model = ENV["KILOCODE_MODEL"]
72
+ if model
73
+ args.concat(["-m", model])
74
+ end
75
+
76
+ # Add workspace detection if needed
77
+ if Dir.exist?(".git") && ENV["KILOCODE_WORKSPACE"]
78
+ args.concat(["--workspace", ENV["KILOCODE_WORKSPACE"]])
79
+ end
80
+
81
+ # Set authentication via environment variable
82
+ env_vars = {}
83
+ if ENV["KILOCODE_TOKEN"]
84
+ env_vars["KILOCODE_TOKEN"] = ENV["KILOCODE_TOKEN"]
85
+ end
86
+
87
+ # Use debug_execute_command for better debugging
88
+ result = debug_execute_command("kilocode", args: args, input: prompt, timeout: timeout_seconds, streaming: streaming_enabled, env: env_vars)
89
+
90
+ # Log the results
91
+ debug_command("kilocode", args: args, input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)
92
+
93
+ if result.exit_status == 0
94
+ spinner.success("✓")
95
+ mark_completed
96
+ result.out
97
+ else
98
+ spinner.error("✗")
99
+ mark_failed("kilocode failed with exit code #{result.exit_status}")
100
+ debug_error(StandardError.new("kilocode failed"), {exit_code: result.exit_status, stderr: result.err})
101
+ raise "kilocode failed with exit code #{result.exit_status}: #{result.err}"
102
+ end
103
+ rescue => e
104
+ spinner&.error("✗")
105
+ mark_failed("kilocode execution failed: #{e.message}")
106
+ debug_error(e, {provider: "kilocode", prompt_length: prompt.length})
107
+ raise
108
+ ensure
109
+ cleanup_activity_display(activity_display_thread, spinner)
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def calculate_timeout
116
+ # Priority order for timeout calculation:
117
+ # 1. Quick mode (for testing)
118
+ # 2. Environment variable override
119
+ # 3. Adaptive timeout based on step type
120
+ # 4. Default timeout
121
+
122
+ if ENV["AIDP_QUICK_MODE"]
123
+ display_message("⚡ Quick mode enabled - #{TIMEOUT_QUICK_MODE / 60} minute timeout", type: :highlight)
124
+ return TIMEOUT_QUICK_MODE
125
+ end
126
+
127
+ if ENV["AIDP_KILOCODE_TIMEOUT"]
128
+ return ENV["AIDP_KILOCODE_TIMEOUT"].to_i
129
+ end
130
+
131
+ if adaptive_timeout
132
+ display_message("🧠 Using adaptive timeout: #{adaptive_timeout} seconds", type: :info)
133
+ return adaptive_timeout
134
+ end
135
+
136
+ # Default timeout
137
+ display_message("📋 Using default timeout: #{TIMEOUT_DEFAULT / 60} minutes", type: :info)
138
+ TIMEOUT_DEFAULT
139
+ end
140
+
141
+ def adaptive_timeout
142
+ @adaptive_timeout ||= begin
143
+ # Timeout recommendations based on step type patterns
144
+ step_name = ENV["AIDP_CURRENT_STEP"] || ""
145
+
146
+ case step_name
147
+ when /REPOSITORY_ANALYSIS/
148
+ TIMEOUT_REPOSITORY_ANALYSIS
149
+ when /ARCHITECTURE_ANALYSIS/
150
+ TIMEOUT_ARCHITECTURE_ANALYSIS
151
+ when /TEST_ANALYSIS/
152
+ TIMEOUT_TEST_ANALYSIS
153
+ when /FUNCTIONALITY_ANALYSIS/
154
+ TIMEOUT_FUNCTIONALITY_ANALYSIS
155
+ when /DOCUMENTATION_ANALYSIS/
156
+ TIMEOUT_DOCUMENTATION_ANALYSIS
157
+ when /STATIC_ANALYSIS/
158
+ TIMEOUT_STATIC_ANALYSIS
159
+ when /REFACTORING_RECOMMENDATIONS/
160
+ TIMEOUT_REFACTORING_RECOMMENDATIONS
161
+ else
162
+ nil # Use default
163
+ end
164
+ end
165
+ end
166
+
167
+ def activity_callback(state, message, provider)
168
+ # This is now handled by the animated display thread
169
+ # Only print static messages for state changes
170
+ case state
171
+ when :starting
172
+ display_message("🚀 Starting kilocode execution...", type: :info)
173
+ when :completed
174
+ display_message("✅ kilocode execution completed", type: :success)
175
+ when :failed
176
+ display_message("❌ kilocode execution failed: #{message}", type: :error)
177
+ end
178
+ end
179
+
180
+ def setup_activity_monitoring(provider_name, callback)
181
+ @activity_callback = callback
182
+ @activity_state = :starting
183
+ @activity_start_time = Time.now
184
+ end
185
+
186
+ def record_activity(message)
187
+ @activity_state = :running
188
+ @activity_callback&.call(:running, message, "kilocode")
189
+ end
190
+
191
+ def mark_completed
192
+ @activity_state = :completed
193
+ @activity_callback&.call(:completed, "Execution completed", "kilocode")
194
+ end
195
+
196
+ def mark_failed(reason)
197
+ @activity_state = :failed
198
+ @activity_callback&.call(:failed, reason, "kilocode")
199
+ end
200
+ end
201
+ end
202
+ end