aidp 0.25.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 +45 -6
- data/lib/aidp/analyze/error_handler.rb +11 -0
- 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 +30 -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_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 +46 -0
- data/lib/aidp/safe_directory.rb +10 -3
- 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 +40 -1
- data/lib/aidp/watch/change_request_processor.rb +659 -0
- data/lib/aidp/watch/plan_processor.rb +71 -8
- data/lib/aidp/watch/repository_client.rb +85 -20
- data/lib/aidp/watch/runner.rb +37 -0
- data/lib/aidp/watch/state_store.rb +46 -1
- data/lib/aidp/workstream_executor.rb +5 -2
- data/lib/aidp.rb +4 -0
- data/templates/aidp.yml.example +53 -0
- metadata +6 -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,6 +25,7 @@ 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)
|
|
28
29
|
# Ensure message is UTF-8 encoded to handle emoji and special characters
|
|
29
30
|
message_str = message.to_s
|
|
30
31
|
message_str = message_str.force_encoding("UTF-8") if message_str.encoding.name == "ASCII-8BIT"
|
|
@@ -42,9 +43,32 @@ module Aidp
|
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
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
|
+
|
|
45
68
|
module ClassMethods
|
|
46
69
|
# Class-level display helper (uses fresh prompt to respect $stdout changes)
|
|
47
70
|
def display_message(message, type: :info)
|
|
71
|
+
return if suppress_display_message?(message)
|
|
48
72
|
# Ensure message is UTF-8 encoded to handle emoji and special characters
|
|
49
73
|
message_str = message.to_s
|
|
50
74
|
message_str = message_str.force_encoding("UTF-8") if message_str.encoding.name == "ASCII-8BIT"
|
|
@@ -54,6 +78,28 @@ module Aidp
|
|
|
54
78
|
|
|
55
79
|
private
|
|
56
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
|
+
|
|
57
103
|
# Don't memoize - create fresh prompt each time to respect $stdout redirection in tests
|
|
58
104
|
def class_message_display_prompt
|
|
59
105
|
TTY::Prompt.new
|
data/lib/aidp/safe_directory.rb
CHANGED
|
@@ -22,14 +22,21 @@ module Aidp
|
|
|
22
22
|
path
|
|
23
23
|
rescue SystemCallError => e
|
|
24
24
|
fallback = determine_fallback_path(path)
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
|
|
26
|
+
# Suppress permission warnings during tests to reduce noise
|
|
27
|
+
unless ENV["RSPEC_RUNNING"] == "true"
|
|
28
|
+
Kernel.warn "[#{component_name}] Cannot create directory #{path}: #{e.class}: #{e.message}"
|
|
29
|
+
Kernel.warn "[#{component_name}] Using fallback directory: #{fallback}"
|
|
30
|
+
end
|
|
27
31
|
|
|
28
32
|
# Try to create fallback directory
|
|
29
33
|
begin
|
|
30
34
|
FileUtils.mkdir_p(fallback) unless Dir.exist?(fallback)
|
|
31
35
|
rescue SystemCallError => e2
|
|
32
|
-
|
|
36
|
+
# Suppress fallback errors during tests too
|
|
37
|
+
unless ENV["RSPEC_RUNNING"] == "true"
|
|
38
|
+
Kernel.warn "[#{component_name}] Fallback directory creation also failed: #{e2.class}: #{e2.message}"
|
|
39
|
+
end
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
fallback
|
|
@@ -205,12 +205,12 @@ module Aidp
|
|
|
205
205
|
rescue
|
|
206
206
|
File.join(Dir.tmpdir, "aidp_storage")
|
|
207
207
|
end
|
|
208
|
-
|
|
208
|
+
warn_storage("[AIDP Storage] Cannot create base directory #{@base_dir}: #{e.class}: #{e.message}. Using fallback #{fallback}")
|
|
209
209
|
@base_dir = fallback
|
|
210
210
|
begin
|
|
211
211
|
FileUtils.mkdir_p(@base_dir) unless Dir.exist?(@base_dir)
|
|
212
212
|
rescue SystemCallError => e2
|
|
213
|
-
|
|
213
|
+
warn_storage("[AIDP Storage] Fallback directory creation also failed: #{e2.class}: #{e2.message}. Continuing without persistent CSV storage.")
|
|
214
214
|
end
|
|
215
215
|
end
|
|
216
216
|
end
|
|
@@ -225,11 +225,17 @@ module Aidp
|
|
|
225
225
|
rescue
|
|
226
226
|
File.join(Dir.tmpdir, "aidp_storage")
|
|
227
227
|
end
|
|
228
|
-
|
|
228
|
+
warn_storage("[AIDP Storage] Root base_dir detected - using fallback #{fallback} instead of '#{str}'")
|
|
229
229
|
return fallback
|
|
230
230
|
end
|
|
231
231
|
str
|
|
232
232
|
end
|
|
233
|
+
|
|
234
|
+
# Suppress storage warnings in test/CI environments
|
|
235
|
+
def warn_storage(message)
|
|
236
|
+
return if ENV["RSPEC_RUNNING"] || ENV["CI"] || ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
|
|
237
|
+
Kernel.warn(message)
|
|
238
|
+
end
|
|
233
239
|
end
|
|
234
240
|
end
|
|
235
241
|
end
|
|
@@ -16,7 +16,7 @@ module Aidp
|
|
|
16
16
|
csv_dir = @csv_storage.instance_variable_get(:@base_dir)
|
|
17
17
|
if json_dir != @base_dir || csv_dir != @base_dir
|
|
18
18
|
@base_dir = json_dir # Prefer JSON storage directory
|
|
19
|
-
|
|
19
|
+
warn_storage("[AIDP Storage] Base directory normalized to #{@base_dir} after fallback.")
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -229,11 +229,17 @@ module Aidp
|
|
|
229
229
|
rescue
|
|
230
230
|
File.join(Dir.tmpdir, "aidp_storage")
|
|
231
231
|
end
|
|
232
|
-
|
|
232
|
+
warn_storage("[AIDP Storage] Root base_dir detected - using fallback #{fallback} instead of '#{str}'")
|
|
233
233
|
return fallback
|
|
234
234
|
end
|
|
235
235
|
str
|
|
236
236
|
end
|
|
237
|
+
|
|
238
|
+
# Suppress storage warnings in test/CI environments
|
|
239
|
+
def warn_storage(message)
|
|
240
|
+
return if ENV["RSPEC_RUNNING"] || ENV["CI"] || ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
|
|
241
|
+
Kernel.warn(message)
|
|
242
|
+
end
|
|
237
243
|
end
|
|
238
244
|
end
|
|
239
245
|
end
|
|
@@ -171,12 +171,12 @@ module Aidp
|
|
|
171
171
|
rescue
|
|
172
172
|
File.join(Dir.tmpdir, "aidp_storage")
|
|
173
173
|
end
|
|
174
|
-
|
|
174
|
+
warn_storage("[AIDP Storage] Cannot create base directory #{@base_dir}: #{e.class}: #{e.message}. Using fallback #{fallback}")
|
|
175
175
|
@base_dir = fallback
|
|
176
176
|
begin
|
|
177
177
|
FileUtils.mkdir_p(@base_dir) unless Dir.exist?(@base_dir)
|
|
178
178
|
rescue SystemCallError => e2
|
|
179
|
-
|
|
179
|
+
warn_storage("[AIDP Storage] Fallback directory creation also failed: #{e2.class}: #{e2.message}. Continuing without persistent JSON storage.")
|
|
180
180
|
end
|
|
181
181
|
end
|
|
182
182
|
end
|
|
@@ -192,11 +192,17 @@ module Aidp
|
|
|
192
192
|
rescue
|
|
193
193
|
File.join(Dir.tmpdir, "aidp_storage")
|
|
194
194
|
end
|
|
195
|
-
|
|
195
|
+
warn_storage("[AIDP Storage] Root base_dir detected - using fallback #{fallback} instead of '#{str}'")
|
|
196
196
|
return fallback
|
|
197
197
|
end
|
|
198
198
|
str
|
|
199
199
|
end
|
|
200
|
+
|
|
201
|
+
# Suppress storage warnings in test/CI environments
|
|
202
|
+
def warn_storage(message)
|
|
203
|
+
return if ENV["RSPEC_RUNNING"] || ENV["CI"] || ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
|
|
204
|
+
Kernel.warn(message)
|
|
205
|
+
end
|
|
200
206
|
end
|
|
201
207
|
end
|
|
202
208
|
end
|
data/lib/aidp/version.rb
CHANGED
|
@@ -228,12 +228,51 @@ module Aidp
|
|
|
228
228
|
relevant.map do |comment|
|
|
229
229
|
author = comment["author"] || "unknown"
|
|
230
230
|
created = comment["createdAt"] ? Time.parse(comment["createdAt"]).utc.iso8601 : "unknown"
|
|
231
|
-
|
|
231
|
+
body = strip_archived_plans(comment["body"])
|
|
232
|
+
"### #{author} (#{created})\n#{body}"
|
|
232
233
|
end.join("\n\n")
|
|
233
234
|
rescue
|
|
234
235
|
"_Unable to parse comment thread._"
|
|
235
236
|
end
|
|
236
237
|
|
|
238
|
+
def strip_archived_plans(content)
|
|
239
|
+
return content unless content
|
|
240
|
+
|
|
241
|
+
# Remove all archived plan sections (wrapped in HTML comments)
|
|
242
|
+
result = content.dup
|
|
243
|
+
|
|
244
|
+
# Remove archived plan blocks
|
|
245
|
+
# Safe string-based approach to avoid ReDoS vulnerabilities
|
|
246
|
+
start_prefix = "<!-- ARCHIVED_PLAN_START"
|
|
247
|
+
end_marker = "<!-- ARCHIVED_PLAN_END -->"
|
|
248
|
+
|
|
249
|
+
loop do
|
|
250
|
+
# Find the start of an archived plan block (may have attributes after ARCHIVED_PLAN_START)
|
|
251
|
+
start_idx = result.index(start_prefix)
|
|
252
|
+
break unless start_idx
|
|
253
|
+
|
|
254
|
+
# Find the closing --> of the start marker
|
|
255
|
+
start_marker_end = result.index("-->", start_idx)
|
|
256
|
+
break unless start_marker_end
|
|
257
|
+
|
|
258
|
+
# Find the corresponding end marker
|
|
259
|
+
end_idx = result.index(end_marker, start_marker_end)
|
|
260
|
+
break unless end_idx
|
|
261
|
+
|
|
262
|
+
# Remove the entire block including markers
|
|
263
|
+
result = result[0...start_idx] + result[(end_idx + end_marker.length)..]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Remove HTML-commented sections from active plan
|
|
267
|
+
# Keep the content between START and END markers, but strip the markers themselves
|
|
268
|
+
# This preserves the current plan while removing archived content
|
|
269
|
+
result = result.gsub(/<!-- (PLAN_SUMMARY_START|PLAN_TASKS_START|CLARIFYING_QUESTIONS_START) -->/, "")
|
|
270
|
+
result = result.gsub(/<!-- (PLAN_SUMMARY_END|PLAN_TASKS_END|CLARIFYING_QUESTIONS_END) -->/, "")
|
|
271
|
+
|
|
272
|
+
# Clean up any extra blank lines
|
|
273
|
+
result.gsub(/\n{3,}/, "\n\n").strip
|
|
274
|
+
end
|
|
275
|
+
|
|
237
276
|
def write_prompt(content, working_dir: @project_dir)
|
|
238
277
|
prompt_manager = Aidp::Execute::PromptManager.new(working_dir)
|
|
239
278
|
prompt_manager.write(content, step_name: IMPLEMENTATION_STEP)
|