aidp 0.9.6 → 0.11.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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +194 -25
  3. data/lib/aidp/analyze/error_handler.rb +4 -2
  4. data/lib/aidp/{analysis → analyze}/kb_inspector.rb +93 -89
  5. data/lib/aidp/analyze/prioritizer.rb +3 -2
  6. data/lib/aidp/analyze/progress.rb +2 -1
  7. data/lib/aidp/analyze/ruby_maat_integration.rb +7 -3
  8. data/lib/aidp/analyze/runner.rb +73 -11
  9. data/lib/aidp/{analysis → analyze}/seams.rb +1 -1
  10. data/lib/aidp/analyze/steps.rb +10 -8
  11. data/lib/aidp/{analysis → analyze}/tree_sitter_grammar_loader.rb +11 -5
  12. data/lib/aidp/{analysis → analyze}/tree_sitter_scan.rb +21 -15
  13. data/lib/aidp/cli/checkpoint_command.rb +98 -0
  14. data/lib/aidp/cli/first_run_wizard.rb +83 -103
  15. data/lib/aidp/cli/jobs_command.rb +270 -36
  16. data/lib/aidp/cli/terminal_io.rb +3 -3
  17. data/lib/aidp/cli.rb +411 -69
  18. data/lib/aidp/config.rb +5 -8
  19. data/lib/aidp/debug_logger.rb +4 -4
  20. data/lib/aidp/debug_mixin.rb +11 -4
  21. data/lib/aidp/execute/checkpoint.rb +282 -0
  22. data/lib/aidp/execute/checkpoint_display.rb +221 -0
  23. data/lib/aidp/execute/progress.rb +2 -1
  24. data/lib/aidp/execute/prompt_manager.rb +62 -0
  25. data/lib/aidp/execute/runner.rb +67 -20
  26. data/lib/aidp/execute/steps.rb +36 -27
  27. data/lib/aidp/execute/work_loop_runner.rb +308 -0
  28. data/lib/aidp/execute/workflow_selector.rb +50 -26
  29. data/lib/aidp/harness/condition_detector.rb +4 -4
  30. data/lib/aidp/harness/config_schema.rb +40 -0
  31. data/lib/aidp/harness/config_validator.rb +3 -6
  32. data/lib/aidp/harness/configuration.rb +35 -1
  33. data/lib/aidp/harness/enhanced_runner.rb +25 -4
  34. data/lib/aidp/harness/error_handler.rb +103 -28
  35. data/lib/aidp/harness/provider_factory.rb +6 -1
  36. data/lib/aidp/harness/provider_manager.rb +273 -19
  37. data/lib/aidp/harness/runner.rb +14 -6
  38. data/lib/aidp/harness/simple_user_interface.rb +6 -4
  39. data/lib/aidp/harness/status_display.rb +118 -106
  40. data/lib/aidp/harness/test_runner.rb +83 -0
  41. data/lib/aidp/harness/ui/enhanced_tui.rb +7 -5
  42. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +22 -4
  43. data/lib/aidp/harness/ui/error_handler.rb +7 -2
  44. data/lib/aidp/harness/ui/frame_manager.rb +61 -39
  45. data/lib/aidp/harness/ui/job_monitor.rb +2 -0
  46. data/lib/aidp/harness/ui/navigation/main_menu.rb +27 -16
  47. data/lib/aidp/harness/ui/navigation/menu_item.rb +1 -0
  48. data/lib/aidp/harness/ui/navigation/menu_state.rb +1 -0
  49. data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
  50. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +2 -0
  51. data/lib/aidp/harness/ui/progress_display.rb +26 -7
  52. data/lib/aidp/harness/ui/question_collector.rb +2 -0
  53. data/lib/aidp/harness/ui/spinner_group.rb +2 -0
  54. data/lib/aidp/harness/ui/spinner_helper.rb +1 -1
  55. data/lib/aidp/harness/ui/status_manager.rb +4 -2
  56. data/lib/aidp/harness/ui/status_widget.rb +20 -9
  57. data/lib/aidp/harness/ui/workflow_controller.rb +27 -9
  58. data/lib/aidp/harness/user_interface.rb +338 -330
  59. data/lib/aidp/jobs/background_runner.rb +278 -0
  60. data/lib/aidp/message_display.rb +48 -0
  61. data/lib/aidp/provider_manager.rb +13 -7
  62. data/lib/aidp/providers/anthropic.rb +101 -18
  63. data/lib/aidp/providers/base.rb +51 -1
  64. data/lib/aidp/providers/codex.rb +248 -0
  65. data/lib/aidp/providers/cursor.rb +39 -48
  66. data/lib/aidp/providers/gemini.rb +26 -16
  67. data/lib/aidp/providers/github_copilot.rb +263 -0
  68. data/lib/aidp/providers/opencode.rb +38 -47
  69. data/lib/aidp/version.rb +1 -1
  70. data/lib/aidp/workflows/definitions.rb +357 -0
  71. data/lib/aidp/workflows/selector.rb +171 -0
  72. data/lib/aidp.rb +16 -4
  73. data/templates/planning/generate_llm_style_guide.md +119 -0
  74. metadata +43 -31
  75. data/lib/aidp/analyze/progress_visualizer.rb +0 -314
  76. /data/templates/{ANALYZE/02_ARCHITECTURE_ANALYSIS.md → analysis/analyze_architecture.md} +0 -0
  77. /data/templates/{ANALYZE/05_DOCUMENTATION_ANALYSIS.md → analysis/analyze_documentation.md} +0 -0
  78. /data/templates/{ANALYZE/04_FUNCTIONALITY_ANALYSIS.md → analysis/analyze_functionality.md} +0 -0
  79. /data/templates/{ANALYZE/01_REPOSITORY_ANALYSIS.md → analysis/analyze_repository.md} +0 -0
  80. /data/templates/{ANALYZE/06_STATIC_ANALYSIS.md → analysis/analyze_static_code.md} +0 -0
  81. /data/templates/{ANALYZE/03_TEST_ANALYSIS.md → analysis/analyze_tests.md} +0 -0
  82. /data/templates/{ANALYZE/07_REFACTORING_RECOMMENDATIONS.md → analysis/recommend_refactoring.md} +0 -0
  83. /data/templates/{ANALYZE/06a_tree_sitter_scan.md → analysis/scan_with_tree_sitter.md} +0 -0
  84. /data/templates/{EXECUTE/11_STATIC_ANALYSIS.md → implementation/configure_static_analysis.md} +0 -0
  85. /data/templates/{EXECUTE/14_DOCS_PORTAL.md → implementation/create_documentation_portal.md} +0 -0
  86. /data/templates/{EXECUTE/10_IMPLEMENTATION_AGENT.md → implementation/implement_features.md} +0 -0
  87. /data/templates/{EXECUTE/13_DELIVERY_ROLLOUT.md → implementation/plan_delivery.md} +0 -0
  88. /data/templates/{EXECUTE/15_POST_RELEASE.md → implementation/review_post_release.md} +0 -0
  89. /data/templates/{EXECUTE/09_SCAFFOLDING_DEVEX.md → implementation/setup_scaffolding.md} +0 -0
  90. /data/templates/{EXECUTE/02A_ARCH_GATE_QUESTIONS.md → planning/ask_architecture_questions.md} +0 -0
  91. /data/templates/{EXECUTE/00_PRD.md → planning/create_prd.md} +0 -0
  92. /data/templates/{EXECUTE/08_TASKS.md → planning/create_tasks.md} +0 -0
  93. /data/templates/{EXECUTE/04_DOMAIN_DECOMPOSITION.md → planning/decompose_domain.md} +0 -0
  94. /data/templates/{EXECUTE/01_NFRS.md → planning/define_nfrs.md} +0 -0
  95. /data/templates/{EXECUTE/05_CONTRACTS.md → planning/design_apis.md} +0 -0
  96. /data/templates/{EXECUTE/02_ARCHITECTURE.md → planning/design_architecture.md} +0 -0
  97. /data/templates/{EXECUTE/06_THREAT_MODEL.md → planning/design_data_model.md} +0 -0
  98. /data/templates/{EXECUTE/03_ADR_FACTORY.md → planning/generate_adrs.md} +0 -0
  99. /data/templates/{EXECUTE/12_OBSERVABILITY_SLOS.md → planning/plan_observability.md} +0 -0
  100. /data/templates/{EXECUTE/07_TEST_PLAN.md → planning/plan_testing.md} +0 -0
@@ -3,51 +3,53 @@
3
3
  module Aidp
4
4
  module Analyze
5
5
  module Steps
6
+ # Analysis step specifications
7
+ # Templates are organized by purpose and named with action verbs
6
8
  SPEC = {
7
9
  "01_REPOSITORY_ANALYSIS" => {
8
- "templates" => ["01_repository_analysis.md"],
10
+ "templates" => ["analysis/analyze_repository.md"],
9
11
  "description" => "Initial code-maat based repository mining",
10
12
  "outs" => ["docs/analysis/repository_analysis.md"],
11
13
  "gate" => false
12
14
  },
13
15
  "02_ARCHITECTURE_ANALYSIS" => {
14
- "templates" => ["02_architecture_analysis.md"],
16
+ "templates" => ["analysis/analyze_architecture.md"],
15
17
  "description" => "Identify architectural patterns, dependencies, and violations",
16
18
  "outs" => ["docs/analysis/architecture_analysis.md"],
17
19
  "gate" => true
18
20
  },
19
21
  "03_TEST_ANALYSIS" => {
20
- "templates" => ["03_test_analysis.md"],
22
+ "templates" => ["analysis/analyze_tests.md"],
21
23
  "description" => "Analyze existing test coverage and identify gaps",
22
24
  "outs" => ["docs/analysis/test_analysis.md"],
23
25
  "gate" => false
24
26
  },
25
27
  "04_FUNCTIONALITY_ANALYSIS" => {
26
- "templates" => ["04_functionality_analysis.md"],
28
+ "templates" => ["analysis/analyze_functionality.md"],
27
29
  "description" => "Map features, identify dead code, analyze complexity",
28
30
  "outs" => ["docs/analysis/functionality_analysis.md"],
29
31
  "gate" => false
30
32
  },
31
33
  "05_DOCUMENTATION_ANALYSIS" => {
32
- "templates" => ["05_documentation_analysis.md"],
34
+ "templates" => ["analysis/analyze_documentation.md"],
33
35
  "description" => "Identify missing documentation and generate what's needed",
34
36
  "outs" => ["docs/analysis/documentation_analysis.md"],
35
37
  "gate" => false
36
38
  },
37
39
  "06_STATIC_ANALYSIS" => {
38
- "templates" => ["06_static_analysis.md"],
40
+ "templates" => ["analysis/analyze_static_code.md"],
39
41
  "description" => "Check for existing tools and recommend improvements",
40
42
  "outs" => ["docs/analysis/static_analysis.md"],
41
43
  "gate" => false
42
44
  },
43
45
  "06A_TREE_SITTER_SCAN" => {
44
- "templates" => ["06a_tree_sitter_scan.md"],
46
+ "templates" => ["analysis/scan_with_tree_sitter.md"],
45
47
  "description" => "Tree-sitter powered static analysis to build knowledge base",
46
48
  "outs" => [".aidp/kb/symbols.json", ".aidp/kb/seams.json", ".aidp/kb/hotspots.json"],
47
49
  "gate" => false
48
50
  },
49
51
  "07_REFACTORING_RECOMMENDATIONS" => {
50
- "templates" => ["07_refactoring_recommendations.md"],
52
+ "templates" => ["analysis/recommend_refactoring.md"],
51
53
  "description" => "Provide actionable refactoring guidance",
52
54
  "outs" => ["docs/analysis/refactoring_recommendations.md"],
53
55
  "gate" => true
@@ -2,11 +2,14 @@
2
2
 
3
3
  require "pathname"
4
4
  require "tree_sitter"
5
+ require "tty-prompt"
5
6
  require "fileutils"
6
7
 
7
8
  module Aidp
8
- module Analysis
9
+ module Analyze
9
10
  class TreeSitterGrammarLoader
11
+ include Aidp::MessageDisplay
12
+
10
13
  # Default grammar configurations
11
14
  GRAMMAR_CONFIGS = {
12
15
  "ruby" => {
@@ -61,10 +64,11 @@ module Aidp
61
64
  }
62
65
  }.freeze
63
66
 
64
- def initialize(project_dir = Dir.pwd)
67
+ def initialize(project_dir = Dir.pwd, prompt: TTY::Prompt.new)
65
68
  @project_dir = project_dir
66
69
  @grammars_dir = File.join(project_dir, ".aidp", "grammars")
67
70
  @loaded_grammars = {}
71
+ @prompt = prompt
68
72
  end
69
73
 
70
74
  # Load grammar for a specific language
@@ -92,7 +96,7 @@ module Aidp
92
96
  grammar_path = File.join(@grammars_dir, language)
93
97
 
94
98
  unless File.exist?(grammar_path)
95
- puts "Installing Tree-sitter grammar for #{language}..."
99
+ display_message("Installing Tree-sitter grammar for #{language}...", type: :info)
96
100
  install_grammar(language, config)
97
101
  end
98
102
  end
@@ -110,7 +114,7 @@ module Aidp
110
114
  require "json"
111
115
  File.write(File.join(grammar_path, "grammar.json"), JSON.generate(config))
112
116
 
113
- puts "Grammar for #{language} marked as available"
117
+ display_message("Grammar for #{language} marked as available", type: :success)
114
118
  end
115
119
 
116
120
  def create_parser(language, config)
@@ -140,7 +144,7 @@ module Aidp
140
144
  real: true
141
145
  }
142
146
  rescue TreeSitter::ParserNotFoundError => e
143
- puts "Warning: Tree-sitter parser not found for #{language}: #{e.message}"
147
+ display_message("Warning: Tree-sitter parser not found for #{language}: #{e.message}", type: :warn)
144
148
  create_mock_parser(language)
145
149
  end
146
150
 
@@ -476,6 +480,8 @@ module Aidp
476
480
 
477
481
  nodes
478
482
  end
483
+
484
+ private
479
485
  end
480
486
  end
481
487
  end
@@ -4,19 +4,23 @@ require "json"
4
4
  require "fileutils"
5
5
  require "digest"
6
6
  require "etc"
7
+ require "tty-prompt"
7
8
 
8
9
  require_relative "tree_sitter_grammar_loader"
9
10
  require_relative "seams"
10
11
 
11
12
  module Aidp
12
- module Analysis
13
+ module Analyze
13
14
  class TreeSitterScan
14
- def initialize(root: Dir.pwd, kb_dir: ".aidp/kb", langs: %w[ruby], threads: Etc.nprocessors)
15
+ include Aidp::MessageDisplay
16
+
17
+ def initialize(root: Dir.pwd, kb_dir: ".aidp/kb", langs: %w[ruby], threads: Etc.nprocessors, prompt: TTY::Prompt.new)
15
18
  @root = File.expand_path(root)
16
19
  @kb_dir = File.expand_path(kb_dir, @root)
17
20
  @langs = Array(langs)
18
21
  @threads = threads
19
- @grammar_loader = TreeSitterGrammarLoader.new(@root)
22
+ @prompt = prompt
23
+ @grammar_loader = TreeSitterGrammarLoader.new(@root, prompt: @prompt)
20
24
 
21
25
  # Data structures to accumulate analysis results
22
26
  @symbols = []
@@ -34,14 +38,14 @@ module Aidp
34
38
  end
35
39
 
36
40
  def run
37
- puts "🔍 Starting Tree-sitter static analysis..."
38
- puts "📁 Root: #{@root}"
39
- puts "🗂️ KB Directory: #{@kb_dir}"
40
- puts "🌐 Languages: #{@langs.join(", ")}"
41
- puts "🧵 Threads: #{@threads}"
41
+ display_message("🔍 Starting Tree-sitter static analysis...", type: :highlight)
42
+ display_message("📁 Root: #{@root}", type: :info)
43
+ display_message("🗂️ KB Directory: #{@kb_dir}", type: :info)
44
+ display_message("🌐 Languages: #{@langs.join(", ")}", type: :info)
45
+ display_message("🧵 Threads: #{@threads}", type: :info)
42
46
 
43
47
  files = discover_files
44
- puts "📄 Found #{files.length} files to analyze"
48
+ display_message("📄 Found #{files.length} files to analyze", type: :info)
45
49
 
46
50
  prepare_kb_dir
47
51
  load_cache
@@ -49,8 +53,8 @@ module Aidp
49
53
  parallel_parse(files)
50
54
  write_kb_files
51
55
 
52
- puts "✅ Tree-sitter analysis complete!"
53
- puts "📊 Generated KB files in #{@kb_dir}"
56
+ display_message("✅ Tree-sitter analysis complete!", type: :success)
57
+ display_message("📊 Generated KB files in #{@kb_dir}", type: :success)
54
58
  end
55
59
 
56
60
  private
@@ -144,14 +148,14 @@ module Aidp
144
148
  end
145
149
 
146
150
  def parallel_parse(files)
147
- puts "🔄 Parsing files in parallel..."
151
+ display_message("🔄 Parsing files in parallel...", type: :info)
148
152
 
149
153
  # Group files by language for efficient processing
150
154
  files_by_lang = files.group_by { |file| detect_language(file) }
151
155
 
152
156
  # Process each language group
153
157
  files_by_lang.each do |lang, lang_files|
154
- puts "📝 Processing #{lang_files.length} #{lang} files..."
158
+ display_message("📝 Processing #{lang_files.length} #{lang} files...", type: :info)
155
159
 
156
160
  # Load grammar for this language
157
161
  grammar = @grammar_loader.load_grammar(lang)
@@ -614,7 +618,7 @@ module Aidp
614
618
  end
615
619
 
616
620
  def write_kb_files
617
- puts "💾 Writing knowledge base files..."
621
+ display_message("💾 Writing knowledge base files...", type: :info)
618
622
 
619
623
  prepare_kb_dir
620
624
 
@@ -637,7 +641,7 @@ module Aidp
637
641
  def write_json_file(filename, data)
638
642
  file_path = File.join(@kb_dir, filename)
639
643
  File.write(file_path, JSON.pretty_generate(data))
640
- puts "📄 Written #{filename} (#{data.length} entries)"
644
+ display_message("📄 Written #{filename} (#{data.length} entries)", type: :success)
641
645
  end
642
646
 
643
647
  def generate_hotspots
@@ -681,6 +685,8 @@ module Aidp
681
685
  # This would analyze test file naming and content
682
686
  []
683
687
  end
688
+
689
+ private
684
690
  end
685
691
  end
686
692
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "../execute/checkpoint"
5
+ require_relative "../execute/checkpoint_display"
6
+
7
+ module Aidp
8
+ module CLI
9
+ # CLI command for viewing checkpoint data and progress reports
10
+ class CheckpointCommand < Thor
11
+ desc "show", "Show the latest checkpoint data"
12
+ def show
13
+ checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
14
+ display = Aidp::Execute::CheckpointDisplay.new
15
+
16
+ latest = checkpoint.latest_checkpoint
17
+ if latest
18
+ display.display_checkpoint(latest, show_details: true)
19
+ else
20
+ puts "No checkpoint data found."
21
+ end
22
+ end
23
+
24
+ desc "summary", "Show progress summary with trends"
25
+ def summary
26
+ checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
27
+ display = Aidp::Execute::CheckpointDisplay.new
28
+
29
+ summary = checkpoint.progress_summary
30
+ if summary
31
+ display.display_progress_summary(summary)
32
+ else
33
+ puts "No checkpoint data found."
34
+ end
35
+ end
36
+
37
+ desc "history [LIMIT]", "Show checkpoint history (default: last 10)"
38
+ def history(limit = "10")
39
+ checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
40
+ display = Aidp::Execute::CheckpointDisplay.new
41
+
42
+ history = checkpoint.checkpoint_history(limit: limit.to_i)
43
+ if history.any?
44
+ display.display_checkpoint_history(history, limit: limit.to_i)
45
+ else
46
+ puts "No checkpoint history found."
47
+ end
48
+ end
49
+
50
+ desc "clear", "Clear all checkpoint data"
51
+ option :force, type: :boolean, default: false, desc: "Skip confirmation"
52
+ def clear
53
+ unless options[:force]
54
+ prompt = TTY::Prompt.new
55
+ confirm = prompt.yes?("Are you sure you want to clear all checkpoint data?")
56
+ return unless confirm
57
+ end
58
+
59
+ checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
60
+ checkpoint.clear
61
+ puts "✓ Checkpoint data cleared."
62
+ end
63
+
64
+ desc "metrics", "Show detailed metrics for the latest checkpoint"
65
+ def metrics
66
+ checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
67
+ latest = checkpoint.latest_checkpoint
68
+
69
+ unless latest
70
+ puts "No checkpoint data found."
71
+ return
72
+ end
73
+
74
+ puts
75
+ puts "📊 Detailed Metrics"
76
+ puts "=" * 60
77
+
78
+ metrics = latest[:metrics]
79
+ puts "Lines of Code: #{metrics[:lines_of_code]}"
80
+ puts "File Count: #{metrics[:file_count]}"
81
+ puts "Test Coverage: #{metrics[:test_coverage]}%"
82
+ puts "Code Quality: #{metrics[:code_quality]}%"
83
+ puts "PRD Task Progress: #{metrics[:prd_task_progress]}%"
84
+
85
+ if metrics[:tests_passing]
86
+ puts "Tests: #{metrics[:tests_passing] ? "✓ Passing" : "✗ Failing"}"
87
+ end
88
+
89
+ if metrics[:linters_passing]
90
+ puts "Linters: #{metrics[:linters_passing] ? "✓ Passing" : "✗ Failing"}"
91
+ end
92
+
93
+ puts "=" * 60
94
+ puts
95
+ end
96
+ end
97
+ end
98
+ end
@@ -3,60 +3,52 @@
3
3
 
4
4
  require "yaml"
5
5
  require "tty-prompt"
6
+ require_relative "../harness/provider_factory"
6
7
 
7
8
  module Aidp
8
9
  class CLI
9
10
  # Handles interactive first-time project setup when no aidp.yml exists
10
11
  class FirstRunWizard
12
+ include Aidp::MessageDisplay
13
+
11
14
  TEMPLATES_DIR = File.expand_path(File.join(__dir__, "..", "..", "..", "templates"))
12
15
 
13
- def self.ensure_config(project_dir, input: $stdin, output: $stdout, non_interactive: false, prompt: TTY::Prompt.new)
16
+ def self.ensure_config(project_dir, non_interactive: false, prompt: TTY::Prompt.new)
14
17
  return true if Aidp::Config.config_exists?(project_dir)
15
18
 
16
- wizard = new(project_dir, input: input, output: output, prompt: prompt)
19
+ wizard = new(project_dir, prompt: prompt)
17
20
 
18
- if non_interactive || !input.tty? || !output.tty?
21
+ if non_interactive
19
22
  # Non-interactive environment - create minimal config silently
20
23
  path = wizard.send(:write_minimal_config, project_dir)
21
- output.puts "Created minimal configuration at #{wizard.send(:relative, path)} (non-interactive default)"
24
+ wizard.send(:display_message, "Created minimal configuration at #{wizard.send(:relative, path)} (non-interactive default)", type: :success)
22
25
  return true
23
26
  end
24
27
 
25
28
  wizard.run
26
29
  end
27
30
 
28
- def self.setup_config(project_dir, input: $stdin, output: $stdout, non_interactive: false, prompt: TTY::Prompt.new)
29
- wizard = new(project_dir, input: input, output: output, prompt: prompt)
31
+ def self.setup_config(project_dir, non_interactive: false, prompt: TTY::Prompt.new)
32
+ wizard = new(project_dir, prompt: prompt)
30
33
 
31
- if non_interactive || !input.tty? || !output.tty?
34
+ if non_interactive
32
35
  # Non-interactive environment - skip setup
33
- output.puts "Configuration setup skipped in non-interactive environment"
36
+ wizard.send(:display_message, "Configuration setup skipped in non-interactive environment", type: :info)
34
37
  return true
35
38
  end
36
39
 
37
40
  wizard.run_setup_config
38
41
  end
39
42
 
40
- def initialize(project_dir, input: $stdin, output: $stdout, prompt: TTY::Prompt.new)
43
+ def initialize(project_dir, prompt: TTY::Prompt.new)
41
44
  @project_dir = project_dir
42
- @input = input
43
- @output = output
44
45
  @prompt = prompt
45
46
  end
46
47
 
47
48
  def run
48
49
  banner
49
- loop do
50
- choice = ask_choice
51
- case choice
52
- when "1" then return finish(write_quick_config(@project_dir))
53
- when "2" then return finish(run_custom)
54
- when "q", "Q" then @output.puts("Exiting without creating configuration.")
55
- return false
56
- else
57
- @output.puts "Invalid selection. Please choose one of the listed options."
58
- end
59
- end
50
+ # Always run the full interactive custom configuration flow.
51
+ finish(run_custom)
60
52
  end
61
53
 
62
54
  def run_setup_config
@@ -81,31 +73,19 @@ module Aidp
81
73
  private
82
74
 
83
75
  def banner
84
- @output.puts "\n🚀 First-time setup detected"
85
- @output.puts "No 'aidp.yml' configuration file found in #{relative(@project_dir)}."
86
- @output.puts "Let's create one so you can start using AI Dev Pipeline."
87
- @output.puts
88
- end
89
-
90
- def ask_choice
91
- @output.puts "Choose a configuration style:" unless @asking
92
-
93
- options = {
94
- "Quick setup (cursor + macos, no API keys needed)" => "1",
95
- "Custom setup (choose your own providers and settings)" => "2",
96
- "Quit" => "q"
97
- }
98
-
99
- @prompt.select("Select an option:", options, default: "Quick setup (cursor + macos, no API keys needed)")
76
+ display_message("\n🚀 First-time setup detected", type: :highlight)
77
+ display_message("No 'aidp.yml' configuration file found in #{relative(@project_dir)}.")
78
+ display_message("Creating a configuration so you can start using AI Dev Pipeline.")
79
+ display_message("")
100
80
  end
101
81
 
102
82
  def finish(path)
103
83
  if path
104
- @output.puts "\n✅ Configuration created at #{relative(path)}"
105
- @output.puts "You can edit this file anytime. Continuing startup...\n"
84
+ display_message("\n✅ Configuration created at #{relative(path)}", type: :success)
85
+ display_message("You can edit this file anytime. Continuing startup...\n")
106
86
  true
107
87
  else
108
- @output.puts "❌ Failed to create configuration file."
88
+ display_message("❌ Failed to create configuration file.", type: :error)
109
89
  false
110
90
  end
111
91
  end
@@ -113,7 +93,7 @@ module Aidp
113
93
  def copy_template(filename)
114
94
  src = File.join(TEMPLATES_DIR, filename)
115
95
  unless File.exist?(src)
116
- @output.puts "Template not found: #{filename}"
96
+ display_message("Template not found: #{filename}", type: :error)
117
97
  return nil
118
98
  end
119
99
  dest = File.join(@project_dir, "aidp.yml")
@@ -122,7 +102,7 @@ module Aidp
122
102
  end
123
103
 
124
104
  def write_minimal_config(project_dir)
125
- dest = File.join(project_dir, "aidp.yml")
105
+ dest = File.join(project_dir, ".aidp", "aidp.yml")
126
106
  return dest if File.exist?(dest)
127
107
  data = {
128
108
  "harness" => {
@@ -138,31 +118,7 @@ module Aidp
138
118
  }
139
119
  }
140
120
  }
141
- File.write(dest, YAML.dump(data))
142
- dest
143
- end
144
-
145
- def write_quick_config(project_dir)
146
- dest = File.join(project_dir, "aidp.yml")
147
- return dest if File.exist?(dest)
148
- data = {
149
- "harness" => {
150
- "max_retries" => 2,
151
- "default_provider" => "cursor",
152
- "fallback_providers" => ["macos"],
153
- "no_api_keys_required" => true
154
- },
155
- "providers" => {
156
- "cursor" => {
157
- "type" => "subscription",
158
- "default_flags" => []
159
- },
160
- "macos" => {
161
- "type" => "usage_based",
162
- "default_flags" => []
163
- }
164
- }
165
- }
121
+ FileUtils.mkdir_p(File.dirname(dest))
166
122
  File.write(dest, YAML.dump(data))
167
123
  dest
168
124
  end
@@ -191,15 +147,11 @@ module Aidp
191
147
  provider_name = default_provider.split(" - ").first
192
148
 
193
149
  # Validate fallback providers
194
- fallback_input = @prompt.ask("Fallback providers (comma-separated)?", default: provider_name) do |q|
195
- q.validate(/^[a-zA-Z0-9_,\s]+$/, "Invalid characters. Use only letters, numbers, commas, and spaces.")
196
- q.validate(->(input) { validate_provider_list(input, available_providers) }, "One or more providers are not supported.")
197
- end
150
+ fallback_providers = select_fallback_providers(available_providers, provider_name)
198
151
 
199
152
  restrict = @prompt.yes?("Only use providers that don't require API keys?", default: false)
200
153
 
201
- # Process the inputs
202
- fallback_providers = fallback_input.split(/\s*,\s*/).map(&:strip).reject(&:empty?)
154
+ # Process providers preserving order
203
155
  providers = [provider_name] + fallback_providers
204
156
  providers.uniq!
205
157
 
@@ -248,15 +200,11 @@ module Aidp
248
200
  provider_name = default_provider.split(" - ").first
249
201
 
250
202
  # Validate fallback providers
251
- fallback_input = @prompt.ask("Fallback providers (comma-separated)?", default: current_fallbacks.join(", ")) do |q|
252
- q.validate(/^[a-zA-Z0-9_,\s]+$/, "Invalid characters. Use only letters, numbers, commas, and spaces.")
253
- q.validate(->(input) { validate_provider_list(input, available_providers) }, "One or more providers are not supported.")
254
- end
203
+ fallback_providers = select_fallback_providers(available_providers, provider_name, preselected: current_fallbacks - [provider_name])
255
204
 
256
205
  restrict_input = @prompt.yes?("Only use providers that don't require API keys?", default: current_restrict)
257
206
 
258
- # Process the inputs
259
- fallback_providers = fallback_input.split(/\s*,\s*/).map(&:strip).reject(&:empty?)
207
+ # Process providers preserving order
260
208
  providers = [provider_name] + fallback_providers
261
209
  providers.uniq!
262
210
 
@@ -310,14 +258,10 @@ module Aidp
310
258
 
311
259
  def ask(prompt, default: nil)
312
260
  if default
313
- @output.print "#{prompt} [#{default}]: "
261
+ @prompt.ask("#{prompt}:", default: default)
314
262
  else
315
- @output.print "#{prompt}: "
263
+ @prompt.ask("#{prompt}:")
316
264
  end
317
- @output.flush
318
- ans = @input.gets&.strip
319
- return default if (ans.nil? || ans.empty?) && default
320
- ans
321
265
  end
322
266
 
323
267
  def relative(path)
@@ -331,24 +275,25 @@ module Aidp
331
275
 
332
276
  # Get available providers for validation
333
277
  def get_available_providers
334
- # Define the available providers based on the system
335
- available = ["cursor", "anthropic", "gemini", "macos", "opencode"]
336
-
337
- # Add descriptions for better UX
338
- available.map do |provider|
339
- case provider
340
- when "cursor"
341
- "cursor - Cursor AI (no API key required)"
342
- when "anthropic"
343
- "anthropic - Anthropic Claude (requires API key)"
344
- when "gemini"
345
- "gemini - Google Gemini (requires API key)"
346
- when "macos"
347
- "macos - macOS UI Automation (no API key required)"
348
- when "opencode"
349
- "opencode - OpenCode (no API key required)"
278
+ # Get all supported providers from the factory (single source of truth)
279
+ all_providers = Aidp::Harness::ProviderFactory::PROVIDER_CLASSES.keys
280
+
281
+ # Filter out providers we don't want to show in the wizard
282
+ # - "anthropic" is an internal name, we show "claude" instead
283
+ # - "macos" is disabled (as per issue #73)
284
+ excluded = ["anthropic", "macos"]
285
+ available = all_providers - excluded
286
+
287
+ # Get display names from the providers themselves
288
+ available.map do |provider_name|
289
+ provider_class = Aidp::Harness::ProviderFactory::PROVIDER_CLASSES[provider_name]
290
+ if provider_class
291
+ # Instantiate to get display name
292
+ instance = provider_class.new
293
+ display_name = instance.display_name
294
+ "#{provider_name} - #{display_name}"
350
295
  else
351
- provider
296
+ provider_name
352
297
  end
353
298
  end
354
299
  end
@@ -364,6 +309,41 @@ module Aidp
364
309
  valid_providers = available_providers.map { |p| p.split(" - ").first }
365
310
  providers.all? { |provider| valid_providers.include?(provider) }
366
311
  end
312
+
313
+ # Interactive ordered multi-select for fallback providers
314
+ def select_fallback_providers(available_with_labels, default_provider, preselected: [])
315
+ # Extract provider names and exclude the already chosen default
316
+ options = available_with_labels.map { |o| o.split(" - ").first }
317
+ candidates = options.reject { |p| p == default_provider }
318
+
319
+ return [] if candidates.empty?
320
+
321
+ selected = preselected.select { |p| candidates.include?(p) }
322
+
323
+ loop do
324
+ display_message("\nSelect fallback providers in order of preference (first = highest priority).", type: :info)
325
+ display_message("Current order: #{selected.empty? ? "(none)" : selected.join(" > ")}", type: :muted)
326
+ choice = @prompt.select("Add provider, or choose an action:", cycle: true) do |menu|
327
+ (candidates - selected).each { |prov| menu.choice("Add #{prov}", prov) }
328
+ menu.choice("Done", :done)
329
+ menu.choice("Clear", :clear) unless selected.empty?
330
+ menu.choice("Remove last (#{selected.last})", :remove) unless selected.empty?
331
+ end
332
+
333
+ case choice
334
+ when :done
335
+ break
336
+ when :clear
337
+ selected.clear
338
+ when :remove
339
+ selected.pop
340
+ else
341
+ selected << choice unless selected.include?(choice)
342
+ end
343
+ end
344
+
345
+ selected
346
+ end
367
347
  end
368
348
  end
369
349
  end