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.
@@ -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,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
@@ -22,14 +22,21 @@ module Aidp
22
22
  path
23
23
  rescue SystemCallError => e
24
24
  fallback = determine_fallback_path(path)
25
- Kernel.warn "[#{component_name}] Cannot create directory #{path}: #{e.class}: #{e.message}"
26
- Kernel.warn "[#{component_name}] Using fallback directory: #{fallback}"
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
- Kernel.warn "[#{component_name}] Fallback directory creation also failed: #{e2.class}: #{e2.message}"
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
- Kernel.warn "[AIDP Storage] Cannot create base directory #{@base_dir}: #{e.class}: #{e.message}. Using fallback #{fallback}"
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
- Kernel.warn "[AIDP Storage] Fallback directory creation also failed: #{e2.class}: #{e2.message}. Continuing without persistent CSV storage."
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
- Kernel.warn "[AIDP Storage] Root base_dir detected - using fallback #{fallback} instead of '#{str}'"
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
- Kernel.warn "[AIDP Storage] Base directory normalized to #{@base_dir} after fallback."
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
- Kernel.warn "[AIDP Storage] Root base_dir detected - using fallback #{fallback} instead of '#{str}'"
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
- Kernel.warn "[AIDP Storage] Cannot create base directory #{@base_dir}: #{e.class}: #{e.message}. Using fallback #{fallback}"
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
- Kernel.warn "[AIDP Storage] Fallback directory creation also failed: #{e2.class}: #{e2.message}. Continuing without persistent JSON storage."
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
- Kernel.warn "[AIDP Storage] Root base_dir detected - using fallback #{fallback} instead of '#{str}'"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.25.0"
4
+ VERSION = "0.26.0"
5
5
  end
@@ -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
- "### #{author} (#{created})\n#{comment["body"]}"
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)