aidp 0.25.0 → 0.27.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -6
  3. data/lib/aidp/analyze/error_handler.rb +11 -0
  4. data/lib/aidp/cli/checkpoint_command.rb +198 -0
  5. data/lib/aidp/cli/config_command.rb +71 -0
  6. data/lib/aidp/cli/enhanced_input.rb +2 -0
  7. data/lib/aidp/cli/first_run_wizard.rb +8 -7
  8. data/lib/aidp/cli/harness_command.rb +102 -0
  9. data/lib/aidp/cli/jobs_command.rb +3 -3
  10. data/lib/aidp/cli/mcp_dashboard.rb +4 -3
  11. data/lib/aidp/cli/models_command.rb +662 -0
  12. data/lib/aidp/cli/providers_command.rb +223 -0
  13. data/lib/aidp/cli.rb +35 -456
  14. data/lib/aidp/daemon/runner.rb +2 -2
  15. data/lib/aidp/debug_mixin.rb +2 -9
  16. data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
  17. data/lib/aidp/execute/checkpoint_display.rb +38 -37
  18. data/lib/aidp/execute/interactive_repl.rb +2 -1
  19. data/lib/aidp/execute/prompt_manager.rb +4 -4
  20. data/lib/aidp/execute/work_loop_runner.rb +253 -56
  21. data/lib/aidp/execute/workflow_selector.rb +2 -2
  22. data/lib/aidp/harness/config_loader.rb +20 -11
  23. data/lib/aidp/harness/config_manager.rb +5 -5
  24. data/lib/aidp/harness/config_schema.rb +30 -8
  25. data/lib/aidp/harness/configuration.rb +105 -4
  26. data/lib/aidp/harness/enhanced_runner.rb +24 -15
  27. data/lib/aidp/harness/error_handler.rb +26 -5
  28. data/lib/aidp/harness/filter_strategy.rb +45 -0
  29. data/lib/aidp/harness/generic_filter_strategy.rb +63 -0
  30. data/lib/aidp/harness/model_cache.rb +269 -0
  31. data/lib/aidp/harness/model_discovery_service.rb +259 -0
  32. data/lib/aidp/harness/model_registry.rb +201 -0
  33. data/lib/aidp/harness/output_filter.rb +136 -0
  34. data/lib/aidp/harness/provider_manager.rb +18 -3
  35. data/lib/aidp/harness/rspec_filter_strategy.rb +82 -0
  36. data/lib/aidp/harness/runner.rb +5 -0
  37. data/lib/aidp/harness/test_runner.rb +165 -27
  38. data/lib/aidp/harness/thinking_depth_manager.rb +223 -7
  39. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -1
  40. data/lib/aidp/logger.rb +35 -5
  41. data/lib/aidp/providers/adapter.rb +2 -4
  42. data/lib/aidp/providers/anthropic.rb +141 -128
  43. data/lib/aidp/providers/base.rb +98 -2
  44. data/lib/aidp/providers/capability_registry.rb +0 -1
  45. data/lib/aidp/providers/codex.rb +49 -67
  46. data/lib/aidp/providers/cursor.rb +71 -59
  47. data/lib/aidp/providers/gemini.rb +44 -60
  48. data/lib/aidp/providers/github_copilot.rb +2 -66
  49. data/lib/aidp/providers/kilocode.rb +24 -80
  50. data/lib/aidp/providers/opencode.rb +24 -80
  51. data/lib/aidp/safe_directory.rb +10 -3
  52. data/lib/aidp/setup/wizard.rb +345 -8
  53. data/lib/aidp/storage/csv_storage.rb +9 -3
  54. data/lib/aidp/storage/file_manager.rb +8 -2
  55. data/lib/aidp/storage/json_storage.rb +9 -3
  56. data/lib/aidp/version.rb +1 -1
  57. data/lib/aidp/watch/build_processor.rb +40 -1
  58. data/lib/aidp/watch/change_request_processor.rb +659 -0
  59. data/lib/aidp/watch/plan_generator.rb +93 -14
  60. data/lib/aidp/watch/plan_processor.rb +71 -8
  61. data/lib/aidp/watch/repository_client.rb +85 -20
  62. data/lib/aidp/watch/review_processor.rb +3 -3
  63. data/lib/aidp/watch/runner.rb +37 -0
  64. data/lib/aidp/watch/state_store.rb +46 -1
  65. data/lib/aidp/workflows/guided_agent.rb +3 -3
  66. data/lib/aidp/workstream_executor.rb +5 -2
  67. data/lib/aidp.rb +4 -0
  68. data/templates/aidp-development.yml.example +2 -2
  69. data/templates/aidp-production.yml.example +3 -3
  70. data/templates/aidp.yml.example +53 -0
  71. metadata +14 -1
@@ -9,6 +9,7 @@ require "json"
9
9
 
10
10
  require_relative "../util"
11
11
  require_relative "../config/paths"
12
+ require_relative "../harness/capability_registry"
12
13
  require_relative "provider_registry"
13
14
  require_relative "devcontainer/parser"
14
15
  require_relative "devcontainer/generator"
@@ -24,6 +25,11 @@ module Aidp
24
25
  SCHEMA_VERSION = 1
25
26
  DEVCONTAINER_COMPONENT = "setup_wizard.devcontainer"
26
27
 
28
+ DEFAULT_AUTOCONFIG_TIERS = %w[mini standard pro].freeze
29
+ LEGACY_TIER_ALIASES = {
30
+ advanced: :pro
31
+ }.freeze
32
+
27
33
  attr_reader :project_dir, :prompt, :dry_run
28
34
 
29
35
  def initialize(project_dir = Dir.pwd, prompt: nil, dry_run: false)
@@ -38,11 +44,13 @@ module Aidp
38
44
 
39
45
  def run
40
46
  display_welcome
41
- # Normalize any legacy or label-based model_family entries before prompting
47
+ # Normalize any legacy tier/model_family entries before prompting
42
48
  normalize_existing_model_families!
49
+ normalize_existing_thinking_tiers!
43
50
  return @saved if skip_wizard?
44
51
 
45
52
  configure_providers
53
+ configure_thinking_tiers
46
54
  configure_work_loop
47
55
  configure_branching
48
56
  configure_artifacts
@@ -51,6 +59,9 @@ module Aidp
51
59
  configure_modes
52
60
  configure_devcontainer
53
61
 
62
+ # Finalize any background model discovery
63
+ finalize_background_discovery
64
+
54
65
  yaml_content = generate_yaml
55
66
  display_preview(yaml_content)
56
67
  display_diff(yaml_content) if @existing_config.any?
@@ -279,6 +290,249 @@ module Aidp
279
290
 
280
291
  # Removed MCP configuration step (MCP now expected to be provider-specific if used)
281
292
 
293
+ # -------------------------------------------
294
+ # Thinking tier configuration (automated model discovery)
295
+ # -------------------------------------------
296
+ def configure_thinking_tiers
297
+ prompt.say("\n🧠 Thinking Tier Configuration")
298
+ prompt.say("-" * 40)
299
+
300
+ # Get configured providers
301
+ primary_provider = get([:harness, :default_provider])
302
+ fallback_providers = Array(get([:harness, :fallback_providers]))
303
+ all_providers = ([primary_provider] + fallback_providers).compact.uniq
304
+
305
+ if all_providers.empty?
306
+ prompt.warn("⚠️ No providers configured. Skipping tier configuration.")
307
+ return
308
+ end
309
+
310
+ # Check if user wants to use automated discovery
311
+ existing_tiers = get([:thinking, :tiers])
312
+ if existing_tiers && !existing_tiers.empty?
313
+ prompt.say("📝 Found existing tier configuration")
314
+ unless prompt.yes?("Would you like to update it with discovered models?", default: false)
315
+ return
316
+ end
317
+ elsif !prompt.yes?("Auto-configure thinking tiers with discovered models?", default: true)
318
+ prompt.say("💡 You can run 'aidp models discover' later to see available models")
319
+ return
320
+ end
321
+
322
+ # Run model discovery
323
+ prompt.say("\n🔍 Discovering available models...")
324
+ discovered_models = discover_models_for_providers(all_providers)
325
+
326
+ if discovered_models.empty?
327
+ prompt.warn("⚠️ No models discovered. Ensure provider CLIs are installed.")
328
+ prompt.say("💡 You can configure tiers manually or run 'aidp models discover' later")
329
+ return
330
+ end
331
+
332
+ # Display discovered models
333
+ display_discovered_models(discovered_models)
334
+
335
+ # Generate tier configuration
336
+ tier_config = generate_tier_configuration(discovered_models, primary_provider)
337
+
338
+ # Show preview
339
+ prompt.say("\n📋 Proposed tier configuration:")
340
+ display_tier_preview(tier_config)
341
+
342
+ # Confirm and save
343
+ if prompt.yes?("\nSave this tier configuration?", default: true)
344
+ set([:thinking, :tiers], tier_config)
345
+ prompt.ok("✅ Thinking tiers configured successfully")
346
+ else
347
+ prompt.say("💡 Skipped tier configuration. You can run 'aidp models discover' later")
348
+ end
349
+ end
350
+
351
+ # Trigger background model discovery for a provider
352
+ # Runs asynchronously and caches results without blocking the wizard
353
+ def trigger_background_discovery(provider_name)
354
+ return unless provider_available_for_discovery?(provider_name)
355
+
356
+ # Store reference to display notification later
357
+ @discovery_threads ||= []
358
+
359
+ thread = Thread.new do
360
+ discover_and_cache_models(provider_name)
361
+ rescue => e
362
+ Aidp.log_debug("setup_wizard", "background discovery failed",
363
+ provider: provider_name, error: e.message)
364
+ end
365
+
366
+ @discovery_threads << {thread: thread, provider: provider_name}
367
+ end
368
+
369
+ # Check if provider CLI is available for discovery
370
+ def provider_available_for_discovery?(provider_name)
371
+ provider_class = get_provider_class(provider_name)
372
+ return false unless provider_class
373
+
374
+ provider_class.respond_to?(:available?) && provider_class.available?
375
+ rescue => e
376
+ Aidp.log_debug("setup_wizard", "provider availability check failed",
377
+ provider: provider_name, error: e.message)
378
+ false
379
+ end
380
+
381
+ # Perform model discovery and cache results
382
+ def discover_and_cache_models(provider_name)
383
+ require_relative "../harness/model_discovery_service"
384
+
385
+ service = Aidp::Harness::ModelDiscoveryService.new
386
+ models = service.discover_models(provider_name, use_cache: false)
387
+
388
+ if models.any?
389
+ Aidp.log_info("setup_wizard", "discovered models in background",
390
+ provider: provider_name, count: models.size)
391
+ end
392
+
393
+ models
394
+ rescue => e
395
+ Aidp.log_debug("setup_wizard", "background discovery failed",
396
+ provider: provider_name, error: e.message)
397
+ []
398
+ end
399
+
400
+ # Get provider class for discovery
401
+ def get_provider_class(provider_name)
402
+ class_name = "Aidp::Providers::#{provider_name.capitalize}"
403
+ Object.const_get(class_name)
404
+ rescue NameError
405
+ nil
406
+ end
407
+
408
+ # Wait for background discovery to complete and show notifications
409
+ #
410
+ # @param timeout [Numeric] Maximum seconds to wait per thread (default: 5)
411
+ def finalize_background_discovery(timeout: 5)
412
+ return unless @discovery_threads&.any?
413
+
414
+ @discovery_threads.each do |entry|
415
+ thread = entry[:thread]
416
+ provider = entry[:provider]
417
+
418
+ # Wait up to timeout seconds for discovery to complete
419
+ thread.join(timeout)
420
+
421
+ if thread.alive?
422
+ Aidp.log_debug("setup_wizard", "discovery timeout, killing thread",
423
+ provider: provider)
424
+ # Kill thread to prevent hanging
425
+ thread.kill
426
+ thread.join(0.1) # Brief wait for cleanup
427
+ else
428
+ # Discovery completed - show notification
429
+ begin
430
+ require_relative "../harness/model_cache"
431
+ cache = Aidp::Harness::ModelCache.new
432
+ cached_models = cache.get_cached_models(provider)
433
+
434
+ if cached_models&.any?
435
+ prompt.say(" 💾 Discovered #{cached_models.size} model#{"s" unless cached_models.size == 1} for #{provider}")
436
+ end
437
+ rescue => e
438
+ Aidp.log_debug("setup_wizard", "failed to check cached models",
439
+ provider: provider, error: e.message)
440
+ end
441
+ end
442
+ end
443
+
444
+ @discovery_threads = []
445
+ end
446
+
447
+ def discover_models_for_providers(providers)
448
+ require_relative "../harness/model_discovery_service"
449
+
450
+ service = Aidp::Harness::ModelDiscoveryService.new
451
+ all_models = {}
452
+
453
+ providers.each do |provider|
454
+ models = service.discover_models(provider, use_cache: true)
455
+ all_models[provider] = models if models.any?
456
+ rescue => e
457
+ Aidp.log_debug("setup_wizard", "discovery failed", provider: provider, error: e.message)
458
+ # Continue with other providers
459
+ end
460
+
461
+ all_models
462
+ end
463
+
464
+ def display_discovered_models(discovered_models)
465
+ discovered_models.each do |provider, models|
466
+ prompt.say("\n✓ Found #{models.size} models for #{provider}:")
467
+ by_tier = models.group_by { |m| m[:tier] }
468
+ valid_thinking_tiers.each do |tier|
469
+ tier_models = by_tier[tier] || []
470
+ next if tier_models.empty?
471
+
472
+ prompt.say(" #{tier.capitalize} tier: #{tier_models.size} model#{"s" unless tier_models.size == 1}")
473
+ end
474
+ end
475
+ end
476
+
477
+ def generate_tier_configuration(discovered_models, primary_provider)
478
+ tier_config = {}
479
+
480
+ # Configure the three most common tiers: mini, standard, and pro
481
+ DEFAULT_AUTOCONFIG_TIERS.each do |tier|
482
+ tier_models = []
483
+
484
+ # Collect primary provider models first (if available)
485
+ primary_models = find_models_for_tier(discovered_models[primary_provider], tier)
486
+ tier_models.concat(primary_models) if primary_models&.any?
487
+
488
+ # Add models from other providers
489
+ discovered_models.each do |provider, models|
490
+ next if provider == primary_provider
491
+ provider_models = find_models_for_tier(models, tier)
492
+ tier_models.concat(provider_models) if provider_models&.any?
493
+ end
494
+
495
+ # Add to config if we found any models for this tier
496
+ if tier_models.any?
497
+ tier_config[tier.to_sym] = {
498
+ models: tier_models.map { |m|
499
+ {
500
+ provider: m[:provider],
501
+ model: m[:name]
502
+ }
503
+ }
504
+ }
505
+ end
506
+ end
507
+
508
+ tier_config
509
+ end
510
+
511
+ def find_model_for_tier(models, target_tier)
512
+ return nil unless models
513
+
514
+ models.find { |m| m[:tier] == target_tier }
515
+ end
516
+
517
+ def find_models_for_tier(models, target_tier)
518
+ return [] unless models
519
+
520
+ models.select { |m| m[:tier] == target_tier }
521
+ end
522
+
523
+ def display_tier_preview(tier_config)
524
+ return if tier_config.empty?
525
+
526
+ tier_config.each do |tier, config|
527
+ models = config[:models] || []
528
+ prompt.say(" #{tier}:")
529
+ models.each do |model_entry|
530
+ prompt.say(" - provider: #{model_entry[:provider]}")
531
+ prompt.say(" model: #{model_entry[:model]}")
532
+ end
533
+ end
534
+ end
535
+
282
536
  # -------------------------------------------
283
537
  # Work loop configuration
284
538
  # -------------------------------------------
@@ -670,8 +924,18 @@ module Aidp
670
924
  prompt.say("\n📋 Non-functional requirements & preferred libraries")
671
925
  prompt.say("-" * 40)
672
926
 
673
- return delete_path([:nfrs]) unless prompt.yes?("Configure NFRs?", default: true)
927
+ # Check existing configuration for previous choice
928
+ existing_configure = @config.dig(:nfrs, :configure)
929
+ default_configure = existing_configure.nil? || existing_configure
930
+
931
+ configure = prompt.yes?("Configure NFRs?", default: default_configure)
932
+
933
+ unless configure
934
+ Aidp.log_debug("setup_wizard.nfrs", "opt_out")
935
+ return set([:nfrs, :configure], false)
936
+ end
674
937
 
938
+ set([:nfrs, :configure], true)
675
939
  categories = %i[performance security reliability accessibility internationalization]
676
940
  categories.each do |category|
677
941
  existing = get([:nfrs, category])
@@ -835,7 +1099,7 @@ module Aidp
835
1099
 
836
1100
  def configure_watch_labels
837
1101
  prompt.say("\n🏷️ Watch mode label configuration")
838
- prompt.say(" Configure GitHub issue labels that trigger watch mode actions")
1102
+ prompt.say(" Configure GitHub issue and PR labels that trigger watch mode actions")
839
1103
  existing = get([:watch, :labels]) || {}
840
1104
 
841
1105
  plan_trigger = ask_with_default(
@@ -858,11 +1122,29 @@ module Aidp
858
1122
  existing[:build_trigger] || "aidp-build"
859
1123
  )
860
1124
 
1125
+ review_trigger = ask_with_default(
1126
+ "Label to trigger code review",
1127
+ existing[:review_trigger] || "aidp-review"
1128
+ )
1129
+
1130
+ ci_fix_trigger = ask_with_default(
1131
+ "Label to trigger CI remediation",
1132
+ existing[:ci_fix_trigger] || "aidp-fix-ci"
1133
+ )
1134
+
1135
+ change_request_trigger = ask_with_default(
1136
+ "Label to trigger PR change implementation",
1137
+ existing[:change_request_trigger] || "aidp-request-changes"
1138
+ )
1139
+
861
1140
  set([:watch, :labels], {
862
1141
  plan_trigger: plan_trigger,
863
1142
  needs_input: needs_input,
864
1143
  ready_to_build: ready_to_build,
865
- build_trigger: build_trigger
1144
+ build_trigger: build_trigger,
1145
+ review_trigger: review_trigger,
1146
+ ci_fix_trigger: ci_fix_trigger,
1147
+ change_request_trigger: change_request_trigger
866
1148
  })
867
1149
  end
868
1150
 
@@ -953,9 +1235,9 @@ module Aidp
953
1235
  def show_next_steps
954
1236
  prompt.say("\n🎉 Setup complete!")
955
1237
  prompt.say("\nNext steps:")
956
- prompt.say(" 1. Export provider API keys as environment variables.")
957
- prompt.say(" 2. Run 'aidp init' to analyze the project.")
958
- prompt.say(" 3. Run 'aidp execute' to start a work loop.")
1238
+ prompt.say(" 1. Configure provider tools (set required API keys or connections).")
1239
+ prompt.say(" 2. Run 'aidp' to start a work loop.")
1240
+ prompt.say(" 3. Run 'aidp watch <owner/repo>' to enable watch mode automation.")
959
1241
  prompt.say("")
960
1242
  end
961
1243
 
@@ -1225,6 +1507,9 @@ module Aidp
1225
1507
  # Enhance messaging with display name when available
1226
1508
  display_name = discover_available_providers.invert.fetch(provider_name, provider_name)
1227
1509
  prompt.say(" • #{action_word.capitalize} provider '#{display_name}' (#{provider_name}) with billing type '#{provider_type}' and model family '#{model_family}'")
1510
+
1511
+ # Trigger background model discovery for newly added/updated provider
1512
+ trigger_background_discovery(provider_name) unless @dry_run
1228
1513
  end
1229
1514
 
1230
1515
  def edit_or_remove_provider(provider_name, primary_provider, fallbacks)
@@ -1335,6 +1620,44 @@ module Aidp
1335
1620
  end
1336
1621
  end
1337
1622
 
1623
+ def normalize_existing_thinking_tiers!
1624
+ tiers_cfg = @config.dig(:thinking, :tiers)
1625
+ return unless tiers_cfg.is_a?(Hash)
1626
+
1627
+ LEGACY_TIER_ALIASES.each do |legacy, canonical|
1628
+ next unless tiers_cfg.key?(legacy)
1629
+
1630
+ legacy_cfg = tiers_cfg.delete(legacy) || {}
1631
+ canonical_cfg = tiers_cfg[canonical] || {}
1632
+ merged_models = merge_tier_models(canonical_cfg[:models], legacy_cfg[:models])
1633
+ tiers_cfg[canonical] = canonical_cfg.merge(models: merged_models)
1634
+ @warnings << "Normalized thinking tier '#{legacy}' to '#{canonical}'"
1635
+ end
1636
+
1637
+ valid = valid_thinking_tiers
1638
+ tiers_cfg.keys.each do |tier|
1639
+ next if valid.include?(tier.to_s)
1640
+
1641
+ tiers_cfg.delete(tier)
1642
+ @warnings << "Removed unsupported thinking tier '#{tier}' from configuration"
1643
+ end
1644
+ end
1645
+
1646
+ def merge_tier_models(existing_models, new_models)
1647
+ combined = []
1648
+ (Array(existing_models) + Array(new_models)).each do |entry|
1649
+ next unless entry.is_a?(Hash)
1650
+ provider = entry[:provider]
1651
+ model = entry[:model]
1652
+ next unless provider && model
1653
+
1654
+ unless combined.any? { |m| m[:provider] == provider && m[:model] == model }
1655
+ combined << entry
1656
+ end
1657
+ end
1658
+ combined
1659
+ end
1660
+
1338
1661
  def load_existing_config
1339
1662
  return {} unless File.exist?(config_path)
1340
1663
  YAML.safe_load_file(config_path, permitted_classes: [Time]) || {}
@@ -1468,6 +1791,12 @@ module Aidp
1468
1791
  File.exist?(File.join(project_dir, relative_path))
1469
1792
  end
1470
1793
 
1794
+ def valid_thinking_tiers
1795
+ Aidp::Harness::CapabilityRegistry::VALID_TIERS
1796
+ rescue NameError
1797
+ %w[mini standard thinking pro max]
1798
+ end
1799
+
1471
1800
  def configure_devcontainer
1472
1801
  prompt.say("\n🐳 Devcontainer Configuration")
1473
1802
  Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.start")
@@ -1482,10 +1811,18 @@ module Aidp
1482
1811
  path: parser.detect)
1483
1812
  end
1484
1813
 
1814
+ # Check existing configuration for previous choice
1815
+ existing_manage = @config.dig(:devcontainer, :manage)
1816
+ default_manage = if existing_manage.nil?
1817
+ existing_devcontainer ? true : false
1818
+ else
1819
+ existing_manage
1820
+ end
1821
+
1485
1822
  # Ask if user wants AIDP to manage devcontainer
1486
1823
  manage = prompt.yes?(
1487
1824
  "Would you like AIDP to manage your devcontainer configuration?",
1488
- default: (@config.dig(:devcontainer, :manage) || existing_devcontainer) ? true : false
1825
+ default: default_manage
1489
1826
  )
1490
1827
 
1491
1828
  unless manage
@@ -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.27.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)