aidp 0.12.1 → 0.14.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -0
  3. data/lib/aidp/analyze/json_file_storage.rb +21 -21
  4. data/lib/aidp/cli/enhanced_input.rb +114 -0
  5. data/lib/aidp/cli/first_run_wizard.rb +28 -309
  6. data/lib/aidp/cli/issue_importer.rb +359 -0
  7. data/lib/aidp/cli/mcp_dashboard.rb +3 -3
  8. data/lib/aidp/cli/terminal_io.rb +26 -0
  9. data/lib/aidp/cli.rb +155 -7
  10. data/lib/aidp/daemon/process_manager.rb +146 -0
  11. data/lib/aidp/daemon/runner.rb +232 -0
  12. data/lib/aidp/execute/async_work_loop_runner.rb +216 -0
  13. data/lib/aidp/execute/future_work_backlog.rb +411 -0
  14. data/lib/aidp/execute/guard_policy.rb +246 -0
  15. data/lib/aidp/execute/instruction_queue.rb +131 -0
  16. data/lib/aidp/execute/interactive_repl.rb +335 -0
  17. data/lib/aidp/execute/repl_macros.rb +651 -0
  18. data/lib/aidp/execute/steps.rb +8 -0
  19. data/lib/aidp/execute/work_loop_runner.rb +322 -36
  20. data/lib/aidp/execute/work_loop_state.rb +162 -0
  21. data/lib/aidp/harness/condition_detector.rb +6 -6
  22. data/lib/aidp/harness/config_loader.rb +23 -23
  23. data/lib/aidp/harness/config_manager.rb +61 -61
  24. data/lib/aidp/harness/config_schema.rb +88 -0
  25. data/lib/aidp/harness/config_validator.rb +9 -9
  26. data/lib/aidp/harness/configuration.rb +76 -29
  27. data/lib/aidp/harness/error_handler.rb +13 -13
  28. data/lib/aidp/harness/provider_config.rb +79 -79
  29. data/lib/aidp/harness/provider_factory.rb +40 -40
  30. data/lib/aidp/harness/provider_info.rb +37 -20
  31. data/lib/aidp/harness/provider_manager.rb +58 -53
  32. data/lib/aidp/harness/provider_type_checker.rb +6 -6
  33. data/lib/aidp/harness/runner.rb +7 -7
  34. data/lib/aidp/harness/status_display.rb +33 -46
  35. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -1
  36. data/lib/aidp/harness/ui/job_monitor.rb +7 -7
  37. data/lib/aidp/harness/user_interface.rb +43 -43
  38. data/lib/aidp/init/doc_generator.rb +256 -0
  39. data/lib/aidp/init/project_analyzer.rb +343 -0
  40. data/lib/aidp/init/runner.rb +83 -0
  41. data/lib/aidp/init.rb +5 -0
  42. data/lib/aidp/logger.rb +279 -0
  43. data/lib/aidp/providers/anthropic.rb +100 -26
  44. data/lib/aidp/providers/base.rb +13 -0
  45. data/lib/aidp/providers/codex.rb +28 -27
  46. data/lib/aidp/providers/cursor.rb +141 -34
  47. data/lib/aidp/providers/github_copilot.rb +26 -26
  48. data/lib/aidp/providers/macos_ui.rb +2 -18
  49. data/lib/aidp/providers/opencode.rb +26 -26
  50. data/lib/aidp/setup/wizard.rb +777 -0
  51. data/lib/aidp/tooling_detector.rb +115 -0
  52. data/lib/aidp/version.rb +1 -1
  53. data/lib/aidp/watch/build_processor.rb +282 -0
  54. data/lib/aidp/watch/plan_generator.rb +166 -0
  55. data/lib/aidp/watch/plan_processor.rb +83 -0
  56. data/lib/aidp/watch/repository_client.rb +243 -0
  57. data/lib/aidp/watch/runner.rb +93 -0
  58. data/lib/aidp/watch/state_store.rb +105 -0
  59. data/lib/aidp/watch.rb +9 -0
  60. data/lib/aidp/workflows/guided_agent.rb +344 -23
  61. data/lib/aidp.rb +14 -0
  62. data/templates/implementation/simple_task.md +36 -0
  63. metadata +27 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e672ad3f8f54f1b3ad0cc4a804a63d1fa048709d47112497d5a4871d046dfb8b
4
- data.tar.gz: 665d0b12e5dff272a0da11fd20d80299eaa4657ab7a87fe4e8b0f80e039e437d
3
+ metadata.gz: 75047af72249152053d5dfe5b5e7800c0c8c844c7a513cf227601b8adb006af2
4
+ data.tar.gz: 574d521b5065f17afa618cfd6b672ed834badca459631011dddde7888796d7c6
5
5
  SHA512:
6
- metadata.gz: 384734aed97d697dce2c06ac6bf6f52b161b6436081ff4feb3b8e99e8038bef2b62a9aa3613b21dc92705569741598b98dcbf72805868f6cbdf82a1a19c3c6dd
7
- data.tar.gz: 3bd5a990c925f10bb1ad863097b1f0a1e84b682e2489b0793242c37cc4034eb6ac8777559cecb6acaef915391dfeb60337074267e52a1cc4c35aee87c793d672
6
+ metadata.gz: 323d8e0cfe1f37934d703488c00cbb4d1769bc0dea9d6f2d826a1a995861e6a8c84d697963a5e9880e5555e1d3e3f32f551f5db8c7ca273de8d79514dbad0e70
7
+ data.tar.gz: 98c37900f0672cade4a7fa838ae94282355e8645fc0ea9a0e2a2e6b2c3d78d265f91e4f66c0f592cf1b8d3297984fdc6fdc26d8efd584df8a0b0c2b543c1f3b8
data/README.md CHANGED
@@ -11,6 +11,13 @@ gem install aidp
11
11
  # Navigate to your project
12
12
  cd /your/project
13
13
 
14
+ # Launch the interactive configuration wizard
15
+ aidp config --interactive
16
+
17
+ # Analyze and bootstrap project docs
18
+ aidp init
19
+ # Creates LLM_STYLE_GUIDE.md, PROJECT_ANALYSIS.md, CODE_QUALITY_PLAN.md
20
+
14
21
  # Start an interactive workflow
15
22
  aidp execute
16
23
 
@@ -14,7 +14,7 @@ module Aidp
14
14
 
15
15
  # Store data in a JSON file
16
16
  def store_data(filename, data)
17
- file_path = get_file_path(filename)
17
+ file_path = file_path(filename)
18
18
 
19
19
  # Ensure directory exists
20
20
  FileUtils.mkdir_p(File.dirname(file_path))
@@ -31,8 +31,8 @@ module Aidp
31
31
  end
32
32
 
33
33
  # Retrieve data from a JSON file
34
- def get_data(filename)
35
- file_path = get_file_path(filename)
34
+ def data(filename)
35
+ file_path = file_path(filename)
36
36
 
37
37
  return nil unless File.exist?(file_path)
38
38
 
@@ -45,12 +45,12 @@ module Aidp
45
45
 
46
46
  # Check if a JSON file exists
47
47
  def data_exists?(filename)
48
- File.exist?(get_file_path(filename))
48
+ File.exist?(file_path(filename))
49
49
  end
50
50
 
51
51
  # Delete a JSON file
52
52
  def delete_data(filename)
53
- file_path = get_file_path(filename)
53
+ file_path = file_path(filename)
54
54
 
55
55
  if File.exist?(file_path)
56
56
  File.delete(file_path)
@@ -89,8 +89,8 @@ module Aidp
89
89
  end
90
90
 
91
91
  # Get project configuration
92
- def get_project_config
93
- get_data("project_config.json")
92
+ def project_config
93
+ data("project_config.json")
94
94
  end
95
95
 
96
96
  # Store runtime status
@@ -99,8 +99,8 @@ module Aidp
99
99
  end
100
100
 
101
101
  # Get runtime status
102
- def get_runtime_status
103
- get_data("runtime_status.json")
102
+ def runtime_status
103
+ data("runtime_status.json")
104
104
  end
105
105
 
106
106
  # Store simple metrics
@@ -109,8 +109,8 @@ module Aidp
109
109
  end
110
110
 
111
111
  # Get simple metrics
112
- def get_simple_metrics
113
- get_data("simple_metrics.json")
112
+ def simple_metrics
113
+ data("simple_metrics.json")
114
114
  end
115
115
 
116
116
  # Store analysis session data
@@ -119,8 +119,8 @@ module Aidp
119
119
  end
120
120
 
121
121
  # Get analysis session data
122
- def get_analysis_session(session_id)
123
- get_data("sessions/#{session_id}.json")
122
+ def analysis_session(session_id)
123
+ data("sessions/#{session_id}.json")
124
124
  end
125
125
 
126
126
  # List analysis sessions
@@ -145,8 +145,8 @@ module Aidp
145
145
  end
146
146
 
147
147
  # Get user preferences
148
- def get_user_preferences
149
- get_data("user_preferences.json")
148
+ def user_preferences
149
+ data("user_preferences.json")
150
150
  end
151
151
 
152
152
  # Store cache data
@@ -161,8 +161,8 @@ module Aidp
161
161
  end
162
162
 
163
163
  # Get cache data (respects TTL)
164
- def get_cache(cache_key)
165
- cache_file_data = get_data("cache/#{cache_key}.json")
164
+ def cache(cache_key)
165
+ cache_file_data = data("cache/#{cache_key}.json")
166
166
  return nil unless cache_file_data
167
167
 
168
168
  # Check TTL if specified
@@ -203,7 +203,7 @@ module Aidp
203
203
  end
204
204
 
205
205
  # Get storage statistics
206
- def get_storage_statistics
206
+ def storage_statistics
207
207
  files = list_files
208
208
 
209
209
  {
@@ -226,7 +226,7 @@ module Aidp
226
226
 
227
227
  files = list_files
228
228
  files.each do |file_info|
229
- data = get_data(file_info[:filename])
229
+ data = data(file_info[:filename])
230
230
  export_data["files"][file_info[:filename]] = {
231
231
  "data" => data,
232
232
  "metadata" => {
@@ -249,7 +249,7 @@ module Aidp
249
249
 
250
250
  # Import data from an exported JSON file
251
251
  def import_data(import_filename)
252
- import_path = get_file_path(import_filename)
252
+ import_path = file_path(import_filename)
253
253
 
254
254
  unless File.exist?(import_path)
255
255
  raise "Import file does not exist: #{import_filename}"
@@ -280,7 +280,7 @@ module Aidp
280
280
 
281
281
  private
282
282
 
283
- def get_file_path(filename)
283
+ def file_path(filename)
284
284
  File.join(@storage_dir, filename)
285
285
  end
286
286
 
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+ require "reline"
5
+
6
+ module Aidp
7
+ class CLI
8
+ # Enhanced input handler with full readline-style key bindings using Reline
9
+ class EnhancedInput
10
+ # Standard key bindings supported by Reline:
11
+ # - Ctrl-A: Move to beginning of line
12
+ # - Ctrl-E: Move to end of line
13
+ # - Ctrl-W: Delete word backward
14
+ # - Ctrl-K: Kill to end of line
15
+ # - Ctrl-U: Kill to beginning of line
16
+ # - Ctrl-D: Delete character forward
17
+ # - Ctrl-H/Backspace: Delete character backward
18
+ # - Left/Right arrows: Move cursor
19
+ # - Alt-F/Alt-B: Move forward/backward by word
20
+ # - Home/End: Jump to beginning/end
21
+ # - Ctrl-T: Transpose characters
22
+ # - And many more Emacs-style bindings
23
+
24
+ def initialize(prompt: nil, input: nil, output: nil, use_reline: true)
25
+ @use_reline = use_reline
26
+ @input = input || $stdin
27
+ @output = output || $stdout
28
+ @prompt = prompt || TTY::Prompt.new(
29
+ input: @input,
30
+ output: @output,
31
+ enable_color: true,
32
+ interrupt: :exit
33
+ )
34
+ @show_hints = false
35
+ end
36
+
37
+ # Ask a question with full readline support
38
+ # Uses Reline for readline-style editing when use_reline is true
39
+ def ask(question, **options)
40
+ # If reline is enabled and we're in a TTY, use reline for better editing
41
+ if @use_reline && @input.tty?
42
+ default = options[:default]
43
+ required = options[:required] || false
44
+
45
+ # Display helpful hint on first use
46
+ if @show_hints
47
+ @output.puts "💡 Hint: Use Ctrl-A (start), Ctrl-E (end), Ctrl-W (delete word), Ctrl-K (kill line)"
48
+ @show_hints = false
49
+ end
50
+
51
+ # Use Reline for input with full key binding support
52
+ loop do
53
+ prompt_text = question.to_s
54
+ prompt_text += " (#{default})" if default
55
+ prompt_text += " "
56
+
57
+ # Reline provides full readline editing capabilities
58
+ Reline.output = @output
59
+ Reline.input = @input
60
+ Reline.completion_append_character = " "
61
+
62
+ answer = Reline.readline(prompt_text, false)
63
+
64
+ # Handle Ctrl-D (nil return)
65
+ if answer.nil?
66
+ @output.puts
67
+ raise Interrupt
68
+ end
69
+
70
+ answer = answer.strip
71
+ answer = default if answer.empty? && default
72
+
73
+ if required && (answer.nil? || answer.empty?)
74
+ @output.puts " Value required."
75
+ next
76
+ end
77
+
78
+ return answer
79
+ end
80
+ else
81
+ # Fall back to TTY::Prompt's ask
82
+ @prompt.ask(question, **options)
83
+ end
84
+ rescue Interrupt
85
+ @output.puts
86
+ raise
87
+ end
88
+
89
+ # Enable hints for key bindings
90
+ def enable_hints!
91
+ @show_hints = true
92
+ end
93
+
94
+ # Disable Reline (fall back to TTY::Prompt)
95
+ def disable_reline!
96
+ @use_reline = false
97
+ end
98
+
99
+ # Enable Reline
100
+ def enable_reline!
101
+ @use_reline = true
102
+ end
103
+
104
+ # Delegate other methods to underlying prompt
105
+ def method_missing(method, *args, **kwargs, &block)
106
+ @prompt.send(method, *args, **kwargs, &block)
107
+ end
108
+
109
+ def respond_to_missing?(method, include_private = false)
110
+ @prompt.respond_to?(method, include_private) || super
111
+ end
112
+ end
113
+ end
114
+ end
@@ -1,44 +1,36 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "yaml"
4
+ require "time"
5
5
  require "tty-prompt"
6
- require_relative "../harness/provider_factory"
7
- require_relative "../config/paths"
8
6
 
9
7
  module Aidp
10
8
  class CLI
11
- # Handles interactive first-time project setup when no aidp.yml exists
9
+ # Wrapper around Aidp::Setup::Wizard to preserve existing CLI entry points.
12
10
  class FirstRunWizard
13
11
  include Aidp::MessageDisplay
14
12
 
15
- TEMPLATES_DIR = File.expand_path(File.join(__dir__, "..", "..", "..", "templates"))
16
-
17
13
  def self.ensure_config(project_dir, non_interactive: false, prompt: TTY::Prompt.new)
18
14
  return true if Aidp::Config.config_exists?(project_dir)
19
15
 
20
16
  wizard = new(project_dir, prompt: prompt)
21
17
 
22
18
  if non_interactive
23
- # Non-interactive environment - create minimal config silently
24
- path = wizard.send(:write_minimal_config, project_dir)
25
- wizard.send(:display_message, "Created minimal configuration at #{wizard.send(:relative, path)} (non-interactive default)", type: :success)
26
- return true
19
+ wizard.create_minimal_config
20
+ wizard.send(:display_message, "Created minimal configuration (non-interactive default)", type: :success)
21
+ true
22
+ else
23
+ wizard.run
27
24
  end
28
-
29
- wizard.run
30
25
  end
31
26
 
32
27
  def self.setup_config(project_dir, non_interactive: false, prompt: TTY::Prompt.new)
33
- wizard = new(project_dir, prompt: prompt)
34
-
35
28
  if non_interactive
36
- # Non-interactive environment - skip setup
37
- wizard.send(:display_message, "Configuration setup skipped in non-interactive environment", type: :info)
29
+ new(project_dir, prompt: prompt).send(:display_message, "Configuration setup skipped in non-interactive environment", type: :info)
38
30
  return true
39
31
  end
40
32
 
41
- wizard.run_setup_config
33
+ new(project_dir, prompt: prompt).run
42
34
  end
43
35
 
44
36
  def initialize(project_dir, prompt: TTY::Prompt.new)
@@ -47,306 +39,33 @@ module Aidp
47
39
  end
48
40
 
49
41
  def run
50
- banner
51
- # Always run the full interactive custom configuration flow.
52
- finish(run_custom)
53
- end
54
-
55
- def run_setup_config
56
- @prompt.say("🔧 Configuration Setup", color: :blue)
57
- @prompt.say("Setting up your configuration file with current values as defaults.")
58
- @prompt.say("")
59
-
60
- # Load existing config to use as defaults (if it exists)
61
- existing_config = load_existing_config
62
-
63
- if existing_config
64
- # Run custom configuration with existing values as defaults
65
- finish(run_custom_with_defaults(existing_config))
66
- else
67
- # No existing config, run the normal setup flow
68
- @prompt.say("No existing configuration found. Running first-time setup...")
69
- @prompt.say("")
70
- run
71
- end
72
- end
73
-
74
- private
75
-
76
- def banner
77
- display_message("\n🚀 First-time setup detected", type: :highlight)
78
- display_message("No 'aidp.yml' configuration file found in #{relative(@project_dir)}.")
79
- display_message("Creating a configuration so you can start using AI Dev Pipeline.")
80
- display_message("")
81
- end
82
-
83
- def finish(path)
84
- if path
85
- display_message("\n✅ Configuration created at #{relative(path)}", type: :success)
86
- display_message("You can edit this file anytime. Continuing startup...\n")
87
- true
88
- else
89
- display_message("❌ Failed to create configuration file.", type: :error)
90
- false
91
- end
42
+ wizard = Aidp::Setup::Wizard.new(@project_dir, prompt: @prompt)
43
+ wizard.run
92
44
  end
93
45
 
94
- def copy_template(filename)
95
- src = File.join(TEMPLATES_DIR, filename)
96
- unless File.exist?(src)
97
- display_message("Template not found: #{filename}", type: :error)
98
- return nil
99
- end
100
- dest = Aidp::ConfigPaths.config_file(@project_dir)
46
+ def create_minimal_config
101
47
  Aidp::ConfigPaths.ensure_config_dir(@project_dir)
102
- File.write(dest, File.read(src))
103
- dest
104
- end
105
-
106
- def write_minimal_config(project_dir)
107
- dest = Aidp::ConfigPaths.config_file(project_dir)
108
- return dest if File.exist?(dest)
109
- data = {
110
- "harness" => {
111
- "max_retries" => 2,
112
- "default_provider" => "cursor",
113
- "fallback_providers" => ["cursor"],
114
- "no_api_keys_required" => false
115
- },
48
+ minimal = {
49
+ "schema_version" => Aidp::Setup::Wizard::SCHEMA_VERSION,
50
+ "generated_by" => "aidp setup wizard minimal",
51
+ "generated_at" => Time.now.utc.iso8601,
116
52
  "providers" => {
117
- "cursor" => {
118
- "type" => "subscription",
119
- "default_flags" => []
53
+ "llm" => {
54
+ "name" => "cursor",
55
+ "model" => "cursor-agent",
56
+ "temperature" => 0.2,
57
+ "max_tokens" => 1024
120
58
  }
121
- }
122
- }
123
- Aidp::ConfigPaths.ensure_config_dir(project_dir)
124
- File.write(dest, YAML.dump(data))
125
- dest
126
- end
127
-
128
- def write_example_config(project_dir)
129
- Aidp::Config.create_example_config(project_dir)
130
- Aidp::ConfigPaths.config_file(project_dir)
131
- end
132
-
133
- def run_custom
134
- dest = Aidp::ConfigPaths.config_file(@project_dir)
135
- return dest if File.exist?(dest)
136
-
137
- @prompt.say("Interactive custom configuration: press Enter to accept defaults shown in [brackets].")
138
- @prompt.say("")
139
-
140
- # Get available providers for validation
141
- available_providers = get_available_providers
142
-
143
- # Use TTY::Prompt select for primary provider
144
- # Find the formatted string that matches the default
145
- default_option = available_providers.find { |option| option.start_with?("cursor -") } || available_providers.first
146
- default_provider = @prompt.select("Default provider?", available_providers, default: default_option)
147
-
148
- # Extract just the provider name from the formatted string
149
- provider_name = default_provider.split(" - ").first
150
-
151
- # Validate fallback providers
152
- fallback_providers = select_fallback_providers(available_providers, provider_name)
153
-
154
- restrict = @prompt.yes?("Only use providers that don't require API keys?", default: false)
155
-
156
- # Process providers preserving order
157
- providers = [provider_name] + fallback_providers
158
- providers.uniq!
159
-
160
- provider_section = {}
161
- providers.each do |prov|
162
- provider_section[prov] = {"type" => (prov == "cursor") ? "subscription" : "usage_based", "default_flags" => []}
163
- end
164
-
165
- data = {
166
- "harness" => {
167
- "max_retries" => 2,
168
- "default_provider" => provider_name,
169
- "fallback_providers" => fallback_providers,
170
- "no_api_keys_required" => restrict
171
- },
172
- "providers" => provider_section
173
- }
174
- Aidp::ConfigPaths.ensure_config_dir(@project_dir)
175
- File.write(dest, YAML.dump(data))
176
- dest
177
- end
178
-
179
- def run_custom_with_defaults(existing_config)
180
- dest = Aidp::ConfigPaths.config_file(@project_dir)
181
-
182
- # Extract current values from existing config
183
- harness_config = existing_config[:harness] || existing_config["harness"] || {}
184
- providers_config = existing_config[:providers] || existing_config["providers"] || {}
185
-
186
- current_default = harness_config[:default_provider] || harness_config["default_provider"] || "cursor"
187
- current_fallbacks = harness_config[:fallback_providers] || harness_config["fallback_providers"] || [current_default]
188
- current_restrict = harness_config[:no_api_keys_required] || harness_config["no_api_keys_required"] || false
189
-
190
- # Use TTY::Prompt for interactive configuration
191
- @prompt.say("Interactive configuration update: press Enter to keep current values shown in [brackets].")
192
- @prompt.say("")
193
-
194
- # Get available providers for validation
195
- available_providers = get_available_providers
196
-
197
- # Use TTY::Prompt select for primary provider
198
- # Find the formatted string that matches the current default
199
- default_option = available_providers.find { |option| option.start_with?("#{current_default} -") } || available_providers.first
200
- default_provider = @prompt.select("Default provider?", available_providers, default: default_option)
201
-
202
- # Extract just the provider name from the formatted string
203
- provider_name = default_provider.split(" - ").first
204
-
205
- # Validate fallback providers
206
- fallback_providers = select_fallback_providers(available_providers, provider_name, preselected: current_fallbacks - [provider_name])
207
-
208
- restrict_input = @prompt.yes?("Only use providers that don't require API keys?", default: current_restrict)
209
-
210
- # Process providers preserving order
211
- providers = [provider_name] + fallback_providers
212
- providers.uniq!
213
-
214
- # Build provider section
215
- provider_section = {}
216
- providers.each do |prov|
217
- # Try to preserve existing provider config if it exists
218
- existing_provider = providers_config[prov.to_sym] || providers_config[prov.to_s]
219
- if existing_provider
220
- # Convert existing provider config to string keys
221
- converted_provider = {}
222
- existing_provider.each { |k, v| converted_provider[k.to_s] = v }
223
- # Ensure the type is correct (fix old "package" and "api" types)
224
- if converted_provider["type"] == "package"
225
- converted_provider["type"] = "subscription"
226
- elsif converted_provider["type"] == "api"
227
- converted_provider["type"] = "usage_based"
228
- end
229
- provider_section[prov] = converted_provider
230
- else
231
- provider_section[prov] = {"type" => (prov == "cursor") ? "subscription" : "usage_based", "default_flags" => []}
232
- end
233
- end
234
-
235
- # Build the new config
236
- data = {
237
- "harness" => {
238
- "max_retries" => harness_config[:max_retries] || harness_config["max_retries"] || 2,
239
- "default_provider" => provider_name,
240
- "fallback_providers" => fallback_providers,
241
- "no_api_keys_required" => restrict_input
242
59
  },
243
- "providers" => provider_section
60
+ "work_loop" => {
61
+ "test" => {
62
+ "unit" => "bundle exec rspec",
63
+ "timeout_seconds" => 1800
64
+ }
65
+ }
244
66
  }
245
67
 
246
- Aidp::ConfigPaths.ensure_config_dir(@project_dir)
247
- File.write(dest, YAML.dump(data))
248
- dest
249
- end
250
-
251
- def load_existing_config
252
- config_file = Aidp::ConfigPaths.config_file(@project_dir)
253
- return nil unless File.exist?(config_file)
254
-
255
- begin
256
- YAML.load_file(config_file) || {}
257
- rescue => e
258
- @prompt.say("❌ Failed to load existing configuration: #{e.message}", color: :red)
259
- nil
260
- end
261
- end
262
-
263
- def ask(prompt, default: nil)
264
- if default
265
- @prompt.ask("#{prompt}:", default: default)
266
- else
267
- @prompt.ask("#{prompt}:")
268
- end
269
- end
270
-
271
- def relative(path)
272
- pn = Pathname.new(path)
273
- wd = Pathname.new(@project_dir)
274
- rel = pn.relative_path_from(wd).to_s
275
- rel.start_with?("..") ? path : rel
276
- rescue
277
- path
278
- end
279
-
280
- # Get available providers for validation
281
- def get_available_providers
282
- # Get all supported providers from the factory (single source of truth)
283
- all_providers = Aidp::Harness::ProviderFactory::PROVIDER_CLASSES.keys
284
-
285
- # Filter out providers we don't want to show in the wizard
286
- # - "anthropic" is an internal name, we show "claude" instead
287
- # - "macos" is disabled (as per issue #73)
288
- excluded = ["anthropic", "macos"]
289
- available = all_providers - excluded
290
-
291
- # Get display names from the providers themselves
292
- available.map do |provider_name|
293
- provider_class = Aidp::Harness::ProviderFactory::PROVIDER_CLASSES[provider_name]
294
- if provider_class
295
- # Instantiate to get display name
296
- instance = provider_class.new
297
- display_name = instance.display_name
298
- "#{provider_name} - #{display_name}"
299
- else
300
- provider_name
301
- end
302
- end
303
- end
304
-
305
- # Validate provider list input
306
- def validate_provider_list(input, available_providers)
307
- return true if input.nil? || input.empty?
308
-
309
- # Extract provider names from the input
310
- providers = input.split(/\s*,\s*/).map(&:strip).reject(&:empty?)
311
-
312
- # Check if all providers are valid
313
- valid_providers = available_providers.map { |p| p.split(" - ").first }
314
- providers.all? { |provider| valid_providers.include?(provider) }
315
- end
316
-
317
- # Interactive ordered multi-select for fallback providers
318
- def select_fallback_providers(available_with_labels, default_provider, preselected: [])
319
- # Extract provider names and exclude the already chosen default
320
- options = available_with_labels.map { |o| o.split(" - ").first }
321
- candidates = options.reject { |p| p == default_provider }
322
-
323
- return [] if candidates.empty?
324
-
325
- selected = preselected.select { |p| candidates.include?(p) }
326
-
327
- loop do
328
- display_message("\nSelect fallback providers in order of preference (first = highest priority).", type: :info)
329
- display_message("Current order: #{selected.empty? ? "(none)" : selected.join(" > ")}", type: :muted)
330
- choice = @prompt.select("Add provider, or choose an action:", cycle: true) do |menu|
331
- (candidates - selected).each { |prov| menu.choice("Add #{prov}", prov) }
332
- menu.choice("Done", :done)
333
- menu.choice("Clear", :clear) unless selected.empty?
334
- menu.choice("Remove last (#{selected.last})", :remove) unless selected.empty?
335
- end
336
-
337
- case choice
338
- when :done
339
- break
340
- when :clear
341
- selected.clear
342
- when :remove
343
- selected.pop
344
- else
345
- selected << choice unless selected.include?(choice)
346
- end
347
- end
348
-
349
- selected
68
+ File.write(Aidp::ConfigPaths.config_file(@project_dir), minimal.to_yaml)
350
69
  end
351
70
  end
352
71
  end