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.
- checksums.yaml +4 -4
- data/README.md +72 -7
- data/lib/aidp/analyze/error_handler.rb +11 -0
- data/lib/aidp/auto_update/bundler_adapter.rb +66 -0
- data/lib/aidp/auto_update/checkpoint.rb +178 -0
- data/lib/aidp/auto_update/checkpoint_store.rb +182 -0
- data/lib/aidp/auto_update/coordinator.rb +204 -0
- data/lib/aidp/auto_update/errors.rb +17 -0
- data/lib/aidp/auto_update/failure_tracker.rb +162 -0
- data/lib/aidp/auto_update/rubygems_api_adapter.rb +95 -0
- data/lib/aidp/auto_update/update_check.rb +106 -0
- data/lib/aidp/auto_update/update_logger.rb +143 -0
- data/lib/aidp/auto_update/update_policy.rb +109 -0
- data/lib/aidp/auto_update/version_detector.rb +144 -0
- data/lib/aidp/auto_update.rb +52 -0
- data/lib/aidp/cli.rb +165 -1
- data/lib/aidp/execute/work_loop_runner.rb +225 -55
- data/lib/aidp/harness/config_loader.rb +20 -11
- data/lib/aidp/harness/config_schema.rb +80 -8
- data/lib/aidp/harness/configuration.rb +73 -2
- data/lib/aidp/harness/filter_strategy.rb +45 -0
- data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
- data/lib/aidp/harness/output_filter.rb +136 -0
- data/lib/aidp/harness/provider_factory.rb +2 -0
- data/lib/aidp/harness/provider_manager.rb +18 -3
- data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
- data/lib/aidp/harness/test_runner.rb +165 -27
- data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
- data/lib/aidp/logger.rb +35 -5
- data/lib/aidp/message_display.rb +56 -2
- data/lib/aidp/prompt_optimization/style_guide_indexer.rb +3 -1
- data/lib/aidp/provider_manager.rb +2 -0
- data/lib/aidp/providers/kilocode.rb +202 -0
- data/lib/aidp/safe_directory.rb +10 -3
- data/lib/aidp/setup/provider_registry.rb +15 -0
- data/lib/aidp/setup/wizard.rb +12 -4
- data/lib/aidp/skills/composer.rb +4 -0
- data/lib/aidp/skills/loader.rb +3 -1
- data/lib/aidp/storage/csv_storage.rb +9 -3
- data/lib/aidp/storage/file_manager.rb +8 -2
- data/lib/aidp/storage/json_storage.rb +9 -3
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +106 -17
- data/lib/aidp/watch/change_request_processor.rb +659 -0
- data/lib/aidp/watch/ci_fix_processor.rb +448 -0
- data/lib/aidp/watch/plan_processor.rb +81 -8
- data/lib/aidp/watch/repository_client.rb +465 -20
- data/lib/aidp/watch/review_processor.rb +266 -0
- data/lib/aidp/watch/reviewers/base_reviewer.rb +164 -0
- data/lib/aidp/watch/reviewers/performance_reviewer.rb +65 -0
- data/lib/aidp/watch/reviewers/security_reviewer.rb +65 -0
- data/lib/aidp/watch/reviewers/senior_dev_reviewer.rb +33 -0
- data/lib/aidp/watch/runner.rb +222 -0
- data/lib/aidp/watch/state_store.rb +99 -1
- data/lib/aidp/workstream_executor.rb +5 -2
- data/lib/aidp.rb +5 -0
- data/templates/aidp.yml.example +53 -0
- 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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
30
|
-
|
|
28
|
+
@iteration_count += 1
|
|
29
|
+
run_command_category(:lint, "Linters")
|
|
30
|
+
end
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|
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(
|
|
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:
|
|
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
|
-
|
|
76
|
-
|
|
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 =
|
|
220
|
+
explicit = @config.test_commands
|
|
85
221
|
return explicit unless explicit.empty?
|
|
86
222
|
|
|
87
|
-
|
|
88
|
-
|
|
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 =
|
|
230
|
+
explicit = @config.lint_commands
|
|
94
231
|
return explicit unless explicit.empty?
|
|
95
232
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/aidp/message_display.rb
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|