aidp 0.34.1 ā 0.39.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +14 -14
- data/lib/aidp/agent_harness_adapter.rb +309 -0
- data/lib/aidp/cli/issue_importer.rb +20 -6
- data/lib/aidp/cli/jobs_command.rb +1 -1
- data/lib/aidp/cli/models_command.rb +9 -16
- data/lib/aidp/cli/prompts_command.rb +253 -0
- data/lib/aidp/cli/security_command.rb +4 -10
- data/lib/aidp/cli/storage_command.rb +277 -0
- data/lib/aidp/cli/temporal_command.rb +460 -0
- data/lib/aidp/cli.rb +24 -2
- data/lib/aidp/config.rb +15 -0
- data/lib/aidp/config_paths.rb +12 -2
- data/lib/aidp/database/migrations.rb +114 -0
- data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
- data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
- data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
- data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
- data/lib/aidp/database/repositories/job_repository.rb +278 -0
- data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
- data/lib/aidp/database/repositories/progress_repository.rb +210 -0
- data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
- data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
- data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
- data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
- data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
- data/lib/aidp/database/repositories/task_repository.rb +231 -0
- data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
- data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
- data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
- data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
- data/lib/aidp/database/repository.rb +166 -0
- data/lib/aidp/database/schema.rb +347 -0
- data/lib/aidp/database/storage_migrator.rb +693 -0
- data/lib/aidp/database.rb +139 -0
- data/lib/aidp/debug_mixin.rb +2 -2
- data/lib/aidp/execute/checkpoint.rb +26 -0
- data/lib/aidp/execute/checkpoint_display.rb +16 -0
- data/lib/aidp/execute/prompt_evaluator.rb +30 -57
- data/lib/aidp/execute/work_loop_runner.rb +294 -46
- data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
- data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
- data/lib/aidp/harness/ai_decision_engine.rb +78 -296
- data/lib/aidp/harness/config_schema.rb +59 -0
- data/lib/aidp/harness/configuration.rb +229 -10
- data/lib/aidp/harness/deprecation_cache.rb +53 -11
- data/lib/aidp/harness/enhanced_runner.rb +1 -1
- data/lib/aidp/harness/error_handler.rb +11 -5
- data/lib/aidp/harness/provider_config.rb +36 -0
- data/lib/aidp/harness/provider_factory.rb +26 -24
- data/lib/aidp/harness/provider_info.rb +9 -5
- data/lib/aidp/harness/provider_manager.rb +2 -2
- data/lib/aidp/harness/provider_metrics.rb +103 -1
- data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
- data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
- data/lib/aidp/harness/runner.rb +7 -2
- data/lib/aidp/harness/state_manager.rb +1 -1
- data/lib/aidp/harness/test_runner.rb +167 -11
- data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
- data/lib/aidp/harness/usage_limit.rb +179 -0
- data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
- data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
- data/lib/aidp/harness/usage_period.rb +234 -0
- data/lib/aidp/init/agent_instructions_generator.rb +317 -0
- data/lib/aidp/init/runner.rb +25 -4
- data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
- data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
- data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
- data/lib/aidp/interfaces/logger_interface.rb +150 -0
- data/lib/aidp/interfaces/ui_interface.rb +254 -0
- data/lib/aidp/logger.rb +25 -7
- data/lib/aidp/orchestration_adapter.rb +279 -0
- data/lib/aidp/pr_worktree_manager.rb +4 -15
- data/lib/aidp/prompts/feedback_collector.rb +198 -0
- data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
- data/lib/aidp/prompts/template_evolver.rb +305 -0
- data/lib/aidp/prompts/template_version_manager.rb +325 -0
- data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
- data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
- data/lib/aidp/setup/wizard.rb +616 -176
- data/lib/aidp/shell_executor.rb +58 -0
- data/lib/aidp/style_guide/selector.rb +56 -1
- data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
- data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
- data/lib/aidp/temporal/activities/base_activity.rb +78 -0
- data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
- data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
- data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
- data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
- data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
- data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
- data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
- data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
- data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
- data/lib/aidp/temporal/configuration.rb +169 -0
- data/lib/aidp/temporal/connection.rb +93 -0
- data/lib/aidp/temporal/worker.rb +119 -0
- data/lib/aidp/temporal/workflow_client.rb +171 -0
- data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
- data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
- data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
- data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
- data/lib/aidp/temporal.rb +120 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/base_processor.rb +35 -0
- data/lib/aidp/watch/build_processor.rb +132 -0
- data/lib/aidp/watch/github_state_extractor.rb +0 -8
- data/lib/aidp/watch/rebase_processor.rb +245 -0
- data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
- data/lib/aidp/watch/runner.rb +366 -285
- data/lib/aidp/watch/state_store.rb +80 -0
- data/lib/aidp/watch/work_item.rb +96 -0
- data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
- data/lib/aidp/watch/worktree_reconciler.rb +643 -0
- data/lib/aidp/workflows/guided_agent.rb +1 -1
- data/lib/aidp/worktree.rb +62 -0
- data/templates/aidp.yml.example +70 -0
- metadata +117 -16
- data/lib/aidp/analyze/json_file_storage.rb +0 -292
- data/lib/aidp/execute/future_work_backlog.rb +0 -411
- data/lib/aidp/providers/adapter.rb +0 -239
- data/lib/aidp/providers/aider.rb +0 -264
- data/lib/aidp/providers/anthropic.rb +0 -614
- data/lib/aidp/providers/base.rb +0 -617
- data/lib/aidp/providers/capability_registry.rb +0 -204
- data/lib/aidp/providers/codex.rb +0 -267
- data/lib/aidp/providers/cursor.rb +0 -304
- data/lib/aidp/providers/error_taxonomy.rb +0 -195
- data/lib/aidp/providers/gemini.rb +0 -110
- data/lib/aidp/providers/github_copilot.rb +0 -227
- data/lib/aidp/providers/kilocode.rb +0 -157
- data/lib/aidp/providers/opencode.rb +0 -139
data/lib/aidp/setup/wizard.rb
CHANGED
|
@@ -7,6 +7,8 @@ require "time"
|
|
|
7
7
|
require "fileutils"
|
|
8
8
|
require "json"
|
|
9
9
|
require "ostruct"
|
|
10
|
+
require_relative "in_memory_config_adapter"
|
|
11
|
+
require_relative "in_memory_config_manager"
|
|
10
12
|
|
|
11
13
|
module Aidp
|
|
12
14
|
module Setup
|
|
@@ -104,35 +106,23 @@ module Aidp
|
|
|
104
106
|
# Provider configuration
|
|
105
107
|
# -------------------------------------------
|
|
106
108
|
def discover_available_providers
|
|
107
|
-
|
|
108
|
-
provider_files = Dir.glob("*.rb", base: providers_dir)
|
|
109
|
-
|
|
110
|
-
# Exclude base classes and non-provider utility files
|
|
111
|
-
excluded_files = ["base.rb", "adapter.rb", "capability_registry.rb", "error_taxonomy.rb"]
|
|
112
|
-
provider_files -= excluded_files
|
|
109
|
+
require "agent_harness"
|
|
113
110
|
|
|
114
111
|
providers = {}
|
|
112
|
+
registry = AgentHarness::Providers::Registry.instance
|
|
115
113
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
provider_class = Aidp::Providers.const_get(class_name)
|
|
125
|
-
|
|
126
|
-
# Create a temporary instance to get the display name
|
|
127
|
-
if provider_class.respond_to?(:new)
|
|
128
|
-
instance = provider_class.new
|
|
129
|
-
display_name = instance.respond_to?(:display_name) ? instance.display_name : provider_name.capitalize
|
|
130
|
-
providers[display_name] = provider_name
|
|
131
|
-
end
|
|
132
|
-
rescue => e
|
|
133
|
-
# Skip providers that can't be loaded, but don't fail the entire discovery
|
|
134
|
-
warn "Warning: Could not load provider #{provider_name}: #{e.message}" if ENV["DEBUG"]
|
|
114
|
+
registry.all.each do |provider_name|
|
|
115
|
+
provider_class = registry.get(provider_name)
|
|
116
|
+
|
|
117
|
+
# Create a temporary instance to get the display name
|
|
118
|
+
if provider_class.respond_to?(:new)
|
|
119
|
+
instance = provider_class.new
|
|
120
|
+
display_name = instance.respond_to?(:display_name) ? instance.display_name : provider_name.to_s.capitalize
|
|
121
|
+
providers[display_name] = provider_name.to_s
|
|
135
122
|
end
|
|
123
|
+
rescue => e
|
|
124
|
+
# Skip providers that can't be loaded, but don't fail the entire discovery
|
|
125
|
+
warn "Warning: Could not load provider #{provider_name}: #{e.message}" if Aidp.debug_env_enabled?
|
|
136
126
|
end
|
|
137
127
|
|
|
138
128
|
providers
|
|
@@ -156,17 +146,20 @@ module Aidp
|
|
|
156
146
|
end
|
|
157
147
|
|
|
158
148
|
# Save primary provider
|
|
159
|
-
set([
|
|
149
|
+
set(%i[harness default_provider], provider_choice) unless provider_choice == "custom"
|
|
160
150
|
|
|
161
|
-
|
|
151
|
+
# Always ask for billing config when running interactive wizard (force: true)
|
|
152
|
+
# This ensures users can update billing type when reconfiguring
|
|
153
|
+
ensure_provider_billing_config(provider_choice, force: true) unless provider_choice == "custom"
|
|
162
154
|
|
|
163
155
|
# Prompt for fallback providers (excluding the primary), pre-select existing
|
|
164
|
-
existing_fallbacks = Array(get([
|
|
156
|
+
existing_fallbacks = Array(get(%i[harness fallback_providers])).map(&:to_s) - [provider_choice]
|
|
165
157
|
fallback_choices = available_providers.reject { |_, name| name == provider_choice }
|
|
166
158
|
fallback_default_names = existing_fallbacks.filter_map { |provider_name| fallback_choices.key(provider_name) }
|
|
167
159
|
|
|
168
160
|
prompt.say("\nš” Use ā/ā arrows to navigate, SPACE to select/deselect, ENTER to confirm")
|
|
169
|
-
fallback_selected = prompt.multi_select("Select fallback providers (used if primary fails):",
|
|
161
|
+
fallback_selected = prompt.multi_select("Select fallback providers (used if primary fails):",
|
|
162
|
+
default: fallback_default_names) do |menu|
|
|
170
163
|
fallback_choices.each do |display_name, provider_name|
|
|
171
164
|
menu.choice display_name, provider_name
|
|
172
165
|
end
|
|
@@ -176,9 +169,7 @@ module Aidp
|
|
|
176
169
|
end
|
|
177
170
|
# Recovery: if multi_select unexpectedly returns empty and there were no existing fallbacks, offer a single-select
|
|
178
171
|
if fallback_selected.empty? && existing_fallbacks.empty? && !fallback_choices.empty?
|
|
179
|
-
if ENV["AIDP_FALLBACK_DEBUG"] == "1"
|
|
180
|
-
prompt.say("[debug] invoking recovery single-select for first fallback")
|
|
181
|
-
end
|
|
172
|
+
prompt.say("[debug] invoking recovery single-select for first fallback") if ENV["AIDP_FALLBACK_DEBUG"] == "1"
|
|
182
173
|
if prompt.yes?("No fallback selected. Add one?", default: true)
|
|
183
174
|
recovery_choice = prompt.select("Select a fallback provider:") do |menu|
|
|
184
175
|
fallback_choices.each do |display_name, provider_name|
|
|
@@ -192,13 +183,14 @@ module Aidp
|
|
|
192
183
|
|
|
193
184
|
# If user selected none but we had existing fallbacks, confirm removal
|
|
194
185
|
if fallback_selected.empty? && existing_fallbacks.any?
|
|
195
|
-
keep = prompt.no?("No fallbacks selected. Remove existing fallbacks (#{existing_fallbacks.join(", ")})?",
|
|
186
|
+
keep = prompt.no?("No fallbacks selected. Remove existing fallbacks (#{existing_fallbacks.join(", ")})?",
|
|
187
|
+
default: false)
|
|
196
188
|
fallback_selected = existing_fallbacks if keep
|
|
197
189
|
end
|
|
198
190
|
|
|
199
191
|
# Remove any accidental duplication of primary provider & save (preserve order)
|
|
200
192
|
cleaned_fallbacks = fallback_selected.reject { |name| name == provider_choice }
|
|
201
|
-
set([
|
|
193
|
+
set(%i[harness fallback_providers], cleaned_fallbacks)
|
|
202
194
|
|
|
203
195
|
# Auto-create minimal provider configs for fallbacks if missing
|
|
204
196
|
cleaned_fallbacks.each do |fp|
|
|
@@ -215,16 +207,18 @@ module Aidp
|
|
|
215
207
|
loop do
|
|
216
208
|
remaining = available_providers.reject { |_, name| ([provider_choice] + cleaned_fallbacks).include?(name) }
|
|
217
209
|
break if remaining.empty?
|
|
210
|
+
|
|
218
211
|
add_choice = prompt.select("Select additional fallback provider:") do |menu|
|
|
219
212
|
remaining.each { |display, name| menu.choice display, name }
|
|
220
213
|
menu.choice "Done", :done
|
|
221
214
|
end
|
|
222
215
|
break if add_choice == :done
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
216
|
+
|
|
217
|
+
next if cleaned_fallbacks.include?(add_choice)
|
|
218
|
+
|
|
219
|
+
cleaned_fallbacks << add_choice
|
|
220
|
+
set(%i[harness fallback_providers], cleaned_fallbacks)
|
|
221
|
+
ensure_provider_billing_config(add_choice, force: true)
|
|
228
222
|
end
|
|
229
223
|
end
|
|
230
224
|
# Recompute editable after additions
|
|
@@ -241,9 +235,7 @@ module Aidp
|
|
|
241
235
|
end
|
|
242
236
|
# Sentinel option: add a new fallback provider that isn't yet in editable list
|
|
243
237
|
remaining = available_map.values - editable
|
|
244
|
-
if remaining.any?
|
|
245
|
-
menu.choice "ā Add fallback providerā¦", :add_fallback
|
|
246
|
-
end
|
|
238
|
+
menu.choice "ā Add fallback providerā¦", :add_fallback if remaining.any?
|
|
247
239
|
menu.choice "Done", :done
|
|
248
240
|
end
|
|
249
241
|
|
|
@@ -252,16 +244,21 @@ module Aidp
|
|
|
252
244
|
break
|
|
253
245
|
when :add_fallback
|
|
254
246
|
# Allow user to pick from remaining providers by display name
|
|
255
|
-
remaining_map = available_map.select
|
|
247
|
+
remaining_map = available_map.select do |disp, internal|
|
|
248
|
+
!editable.include?(internal) && internal != provider_choice
|
|
249
|
+
end
|
|
256
250
|
add_choice = prompt.select("Select provider to add as fallback:") do |menu|
|
|
257
251
|
remaining_map.each { |disp, internal| menu.choice disp, internal }
|
|
258
252
|
menu.choice "Cancel", :cancel
|
|
259
253
|
end
|
|
260
254
|
next if add_choice == :cancel
|
|
255
|
+
|
|
261
256
|
unless cleaned_fallbacks.include?(add_choice)
|
|
262
257
|
cleaned_fallbacks << add_choice
|
|
263
|
-
set([
|
|
264
|
-
|
|
258
|
+
set(%i[harness fallback_providers], cleaned_fallbacks)
|
|
259
|
+
if ENV["AIDP_FALLBACK_DEBUG"] == "1"
|
|
260
|
+
prompt.say("[debug] ensuring billing config for newly added fallback '#{add_choice}'")
|
|
261
|
+
end
|
|
265
262
|
ensure_provider_billing_config(add_choice, force: true)
|
|
266
263
|
editable = ([provider_choice] + cleaned_fallbacks).uniq.reject { |p| p == "custom" }
|
|
267
264
|
end
|
|
@@ -296,7 +293,7 @@ module Aidp
|
|
|
296
293
|
(existing[:max_retries] || 2).to_s
|
|
297
294
|
) { |value| value.to_i }
|
|
298
295
|
|
|
299
|
-
set([
|
|
296
|
+
set(%i[harness max_retries], max_retries)
|
|
300
297
|
end
|
|
301
298
|
|
|
302
299
|
# Removed MCP configuration step (MCP now expected to be provider-specific if used)
|
|
@@ -309,8 +306,8 @@ module Aidp
|
|
|
309
306
|
prompt.say("-" * 40)
|
|
310
307
|
|
|
311
308
|
# Get configured providers
|
|
312
|
-
primary_provider = get([
|
|
313
|
-
fallback_providers = Array(get([
|
|
309
|
+
primary_provider = get(%i[harness default_provider])
|
|
310
|
+
fallback_providers = Array(get(%i[harness fallback_providers]))
|
|
314
311
|
all_providers = ([primary_provider] + fallback_providers).compact.uniq
|
|
315
312
|
|
|
316
313
|
if all_providers.empty?
|
|
@@ -326,9 +323,7 @@ module Aidp
|
|
|
326
323
|
|
|
327
324
|
if has_existing_tiers
|
|
328
325
|
prompt.say("š Found existing tier configuration")
|
|
329
|
-
unless prompt.yes?("Would you like to update it with discovered models?", default: false)
|
|
330
|
-
return
|
|
331
|
-
end
|
|
326
|
+
return unless prompt.yes?("Would you like to update it with discovered models?", default: false)
|
|
332
327
|
elsif !prompt.yes?("Auto-configure thinking tiers with discovered models?", default: true)
|
|
333
328
|
prompt.say("š” You can run 'aidp models discover' later to see available models")
|
|
334
329
|
return
|
|
@@ -432,11 +427,11 @@ module Aidp
|
|
|
432
427
|
tier_models = find_models_for_tier(models, tier)
|
|
433
428
|
|
|
434
429
|
# Add to config if we found any models for this tier
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
|
|
430
|
+
next unless tier_models.any?
|
|
431
|
+
|
|
432
|
+
provider_tiers[tier.to_sym] = {
|
|
433
|
+
models: tier_models.map { |m| m[:name] }
|
|
434
|
+
}
|
|
440
435
|
end
|
|
441
436
|
|
|
442
437
|
# Only add provider if it has at least one tier configured
|
|
@@ -489,8 +484,7 @@ module Aidp
|
|
|
489
484
|
|
|
490
485
|
configure_work_loop_limits
|
|
491
486
|
configure_output_filtering
|
|
492
|
-
|
|
493
|
-
configure_linting
|
|
487
|
+
configure_commands
|
|
494
488
|
configure_watch_patterns
|
|
495
489
|
configure_guards
|
|
496
490
|
configure_coverage
|
|
@@ -498,6 +492,208 @@ module Aidp
|
|
|
498
492
|
configure_vcs_behavior
|
|
499
493
|
end
|
|
500
494
|
|
|
495
|
+
# Configure generic deterministic commands for work loop
|
|
496
|
+
# Replaces the old category-specific configure_test_commands and configure_linting
|
|
497
|
+
def configure_commands
|
|
498
|
+
prompt.say("\nš Deterministic Commands Configuration")
|
|
499
|
+
prompt.say(" Commands run automatically during work loops to validate changes")
|
|
500
|
+
prompt.say(" Commands can run: after each unit, at full loop end, or on completion")
|
|
501
|
+
|
|
502
|
+
existing_commands = get(%i[work_loop commands]) || []
|
|
503
|
+
|
|
504
|
+
# If user has existing commands, offer to edit or start fresh
|
|
505
|
+
if existing_commands.any?
|
|
506
|
+
prompt.say("\n Found #{existing_commands.size} existing command(s)")
|
|
507
|
+
action = prompt.select("What would you like to do?") do |menu|
|
|
508
|
+
menu.choice "Keep existing commands", :keep
|
|
509
|
+
menu.choice "Add more commands", :add
|
|
510
|
+
menu.choice "Replace all commands", :replace
|
|
511
|
+
menu.choice "Skip command configuration", :skip
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
case action
|
|
515
|
+
when :skip, :keep, nil
|
|
516
|
+
return
|
|
517
|
+
when :replace
|
|
518
|
+
existing_commands = []
|
|
519
|
+
when :add
|
|
520
|
+
# Continue to the loop below with existing commands
|
|
521
|
+
else
|
|
522
|
+
# Unexpected value - treat as skip for safety
|
|
523
|
+
return
|
|
524
|
+
end
|
|
525
|
+
else
|
|
526
|
+
return unless prompt.yes?("Configure deterministic commands?", default: true)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
commands = existing_commands.dup
|
|
530
|
+
loop do
|
|
531
|
+
prompt.say("\n Current commands: #{commands.size}")
|
|
532
|
+
commands.each_with_index do |cmd, i|
|
|
533
|
+
prompt.say(" #{i + 1}. [#{cmd[:category]}] #{cmd[:name]}: #{cmd[:command]}")
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
action = prompt.select("\nWhat would you like to do?") do |menu|
|
|
537
|
+
# Done is first so tests default to exiting the loop
|
|
538
|
+
menu.choice "Done configuring commands", :done
|
|
539
|
+
menu.choice "Add a command", :add
|
|
540
|
+
menu.choice "Add from auto-detected tooling", :detect
|
|
541
|
+
menu.choice "Remove a command", :remove if commands.any?
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
case action
|
|
545
|
+
when :add
|
|
546
|
+
cmd = collect_command_details
|
|
547
|
+
commands << cmd if cmd
|
|
548
|
+
when :detect
|
|
549
|
+
detected = add_detected_commands(commands)
|
|
550
|
+
commands = detected
|
|
551
|
+
when :remove
|
|
552
|
+
commands = remove_command_interactive(commands)
|
|
553
|
+
when :done, nil
|
|
554
|
+
# Exit on explicit done or unexpected nil (safety for tests)
|
|
555
|
+
break
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
set(%i[work_loop commands], commands)
|
|
560
|
+
prompt.say("ā
Configured #{commands.size} command(s)")
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Collect details for a single command
|
|
564
|
+
def collect_command_details
|
|
565
|
+
prompt.say("\n Adding new command:")
|
|
566
|
+
|
|
567
|
+
name = prompt.ask("Command name (identifier):", required: true) do |q|
|
|
568
|
+
q.validate(/\A[a-z0-9_]+\z/i, "Name must be alphanumeric with underscores")
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
command = prompt.ask("Shell command to run:", required: true)
|
|
572
|
+
|
|
573
|
+
category_choices = [
|
|
574
|
+
["Test (unit, integration, e2e)", :test],
|
|
575
|
+
["Lint (code style)", :lint],
|
|
576
|
+
["Formatter (auto-fix)", :formatter],
|
|
577
|
+
["Build (compilation)", :build],
|
|
578
|
+
["Documentation", :documentation],
|
|
579
|
+
["Custom", :custom]
|
|
580
|
+
]
|
|
581
|
+
category = prompt.select("Category:") do |menu|
|
|
582
|
+
category_choices.each { |label, value| menu.choice label, value }
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
run_after_choices = [
|
|
586
|
+
["After each work unit iteration (recommended for tests/linters)", :each_unit],
|
|
587
|
+
["Only at end of full work loop", :full_loop],
|
|
588
|
+
["Only when agent marks work complete (recommended for formatters)", :on_completion]
|
|
589
|
+
]
|
|
590
|
+
run_after = prompt.select("When should this command run?") do |menu|
|
|
591
|
+
run_after_choices.each { |label, value| menu.choice label, value }
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
required = prompt.yes?("Is this command required to pass? (failures block completion)", default: true)
|
|
595
|
+
|
|
596
|
+
{
|
|
597
|
+
name: name,
|
|
598
|
+
command: command,
|
|
599
|
+
category: category,
|
|
600
|
+
run_after: run_after,
|
|
601
|
+
required: required,
|
|
602
|
+
timeout_seconds: nil
|
|
603
|
+
}
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
# Add auto-detected commands
|
|
607
|
+
def add_detected_commands(existing_commands)
|
|
608
|
+
detected = detect_all_tooling
|
|
609
|
+
return existing_commands if detected.empty?
|
|
610
|
+
|
|
611
|
+
prompt.say("\n Auto-detected tooling:")
|
|
612
|
+
detected.each_with_index do |cmd, i|
|
|
613
|
+
prompt.say(" #{i + 1}. [#{cmd[:category]}] #{cmd[:command]}")
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
selected = prompt.multi_select("Select commands to add:") do |menu|
|
|
617
|
+
detected.each_with_index do |cmd, i|
|
|
618
|
+
# Skip if already exists
|
|
619
|
+
existing = existing_commands.any? { |e| e[:command] == cmd[:command] }
|
|
620
|
+
label = "[#{cmd[:category]}] #{cmd[:command]}"
|
|
621
|
+
label += " (already added)" if existing
|
|
622
|
+
menu.choice label, i, disabled: existing ? "(already added)" : nil
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
new_commands = existing_commands.dup
|
|
627
|
+
selected.each do |idx|
|
|
628
|
+
new_commands << detected[idx]
|
|
629
|
+
end
|
|
630
|
+
new_commands
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# Detect all tooling and return as generic command configs
|
|
634
|
+
def detect_all_tooling
|
|
635
|
+
commands = []
|
|
636
|
+
|
|
637
|
+
# Detect test command
|
|
638
|
+
test_cmd = detect_unit_test_command
|
|
639
|
+
if test_cmd && !test_cmd.empty?
|
|
640
|
+
commands << {
|
|
641
|
+
name: "unit_tests",
|
|
642
|
+
command: test_cmd,
|
|
643
|
+
category: :test,
|
|
644
|
+
run_after: :each_unit,
|
|
645
|
+
required: true,
|
|
646
|
+
timeout_seconds: 1800
|
|
647
|
+
}
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# Detect lint command
|
|
651
|
+
lint_cmd = detect_lint_command
|
|
652
|
+
if lint_cmd && !lint_cmd.empty?
|
|
653
|
+
commands << {
|
|
654
|
+
name: "lint",
|
|
655
|
+
command: lint_cmd,
|
|
656
|
+
category: :lint,
|
|
657
|
+
run_after: :each_unit,
|
|
658
|
+
required: true,
|
|
659
|
+
timeout_seconds: 300
|
|
660
|
+
}
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Detect format command
|
|
664
|
+
format_cmd = detect_format_command
|
|
665
|
+
if format_cmd && !format_cmd.empty?
|
|
666
|
+
commands << {
|
|
667
|
+
name: "format",
|
|
668
|
+
command: format_cmd,
|
|
669
|
+
category: :formatter,
|
|
670
|
+
run_after: :on_completion,
|
|
671
|
+
required: false,
|
|
672
|
+
timeout_seconds: 120
|
|
673
|
+
}
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
commands
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
# Remove a command interactively
|
|
680
|
+
def remove_command_interactive(commands)
|
|
681
|
+
return commands if commands.empty?
|
|
682
|
+
|
|
683
|
+
choices = commands.each_with_index.map do |cmd, i|
|
|
684
|
+
["#{cmd[:name]}: #{cmd[:command]}", i]
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
idx = prompt.select("Select command to remove:") do |menu|
|
|
688
|
+
choices.each { |label, value| menu.choice label, value }
|
|
689
|
+
menu.choice "Cancel", nil
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
return commands if idx.nil?
|
|
693
|
+
|
|
694
|
+
commands.dup.tap { |c| c.delete_at(idx) }
|
|
695
|
+
end
|
|
696
|
+
|
|
501
697
|
def configure_work_loop_limits
|
|
502
698
|
existing = get([:work_loop]) || {}
|
|
503
699
|
|
|
@@ -508,13 +704,13 @@ module Aidp
|
|
|
508
704
|
(existing[:max_iterations] || 50).to_s
|
|
509
705
|
) { |value| value.to_i }
|
|
510
706
|
|
|
511
|
-
set([
|
|
707
|
+
set(%i[work_loop max_iterations], max_iterations)
|
|
512
708
|
end
|
|
513
709
|
|
|
514
710
|
def configure_output_filtering
|
|
515
711
|
prompt.say("\nš Output filtering configuration")
|
|
516
712
|
prompt.say(" Reduces token consumption by filtering test/lint output")
|
|
517
|
-
existing = get([
|
|
713
|
+
existing = get(%i[work_loop output_filtering]) || {}
|
|
518
714
|
|
|
519
715
|
return unless prompt.yes?("Configure output filtering?", default: false)
|
|
520
716
|
|
|
@@ -524,7 +720,7 @@ module Aidp
|
|
|
524
720
|
)
|
|
525
721
|
|
|
526
722
|
unless enabled
|
|
527
|
-
set([
|
|
723
|
+
set(%i[work_loop output_filtering], {enabled: false})
|
|
528
724
|
return
|
|
529
725
|
end
|
|
530
726
|
|
|
@@ -576,7 +772,7 @@ module Aidp
|
|
|
576
772
|
0
|
|
577
773
|
end
|
|
578
774
|
|
|
579
|
-
set([
|
|
775
|
+
set(%i[work_loop output_filtering], {
|
|
580
776
|
enabled: true,
|
|
581
777
|
test_mode: test_mode,
|
|
582
778
|
lint_mode: lint_mode,
|
|
@@ -611,7 +807,7 @@ module Aidp
|
|
|
611
807
|
end
|
|
612
808
|
|
|
613
809
|
# Check if AI provider is configured
|
|
614
|
-
primary_provider = get([
|
|
810
|
+
primary_provider = get(%i[harness default_provider])
|
|
615
811
|
unless primary_provider
|
|
616
812
|
prompt.warn("ā ļø No AI provider configured. Configure providers first.")
|
|
617
813
|
return
|
|
@@ -655,18 +851,18 @@ module Aidp
|
|
|
655
851
|
end
|
|
656
852
|
end
|
|
657
853
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
854
|
+
return unless filter_definitions.any?
|
|
855
|
+
|
|
856
|
+
set(%i[work_loop output_filtering filter_definitions], filter_definitions)
|
|
857
|
+
prompt.ok("\nā
Generated #{filter_definitions.size} filter definition(s)")
|
|
858
|
+
prompt.say(" These filters will be applied deterministically (no AI calls at runtime)")
|
|
663
859
|
end
|
|
664
860
|
|
|
665
861
|
def collect_commands_for_filtering
|
|
666
862
|
commands = []
|
|
667
863
|
|
|
668
864
|
# Test commands
|
|
669
|
-
test_config = get([
|
|
865
|
+
test_config = get(%i[work_loop test]) || {}
|
|
670
866
|
if test_config[:unit] && !test_config[:unit].start_with?("echo")
|
|
671
867
|
commands << {
|
|
672
868
|
key: "unit_test",
|
|
@@ -693,7 +889,7 @@ module Aidp
|
|
|
693
889
|
end
|
|
694
890
|
|
|
695
891
|
# Lint commands
|
|
696
|
-
lint_config = get([
|
|
892
|
+
lint_config = get(%i[work_loop lint]) || {}
|
|
697
893
|
if lint_config[:command] && !lint_config[:command].start_with?("echo")
|
|
698
894
|
commands << {
|
|
699
895
|
key: "lint",
|
|
@@ -707,30 +903,43 @@ module Aidp
|
|
|
707
903
|
end
|
|
708
904
|
|
|
709
905
|
def create_filter_factory
|
|
710
|
-
# Build
|
|
711
|
-
config
|
|
712
|
-
|
|
906
|
+
# Build in-memory configuration adapters for the factory
|
|
907
|
+
# This enables AGD to work before the config file is written
|
|
908
|
+
config_adapter = build_in_memory_config_adapter
|
|
909
|
+
config_manager = build_in_memory_config_manager
|
|
910
|
+
|
|
911
|
+
# Create provider factory with in-memory config manager
|
|
912
|
+
provider_factory = Aidp::Harness::ProviderFactory.new(config_manager)
|
|
913
|
+
|
|
914
|
+
Aidp.log_debug("setup_wizard", "creating_filter_factory",
|
|
915
|
+
provider: config_adapter.default_provider,
|
|
916
|
+
configured_providers: config_adapter.configured_providers)
|
|
917
|
+
|
|
918
|
+
Aidp::Harness::AIFilterFactory.new(config_adapter, provider_factory: provider_factory)
|
|
713
919
|
end
|
|
714
920
|
|
|
715
|
-
def
|
|
716
|
-
# Create
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
921
|
+
def build_in_memory_config_adapter
|
|
922
|
+
# Create adapter that provides Configuration-like interface from in-memory config
|
|
923
|
+
InMemoryConfigAdapter.new(@config, project_dir)
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
def build_in_memory_config_manager
|
|
927
|
+
# Create ConfigManager-compatible wrapper for in-memory config
|
|
928
|
+
InMemoryConfigManager.new(@config, project_dir)
|
|
722
929
|
end
|
|
723
930
|
|
|
724
931
|
def configure_test_commands
|
|
725
|
-
existing = get([
|
|
932
|
+
existing = get(%i[work_loop test]) || {}
|
|
726
933
|
|
|
727
934
|
unit = ask_with_default("Unit test command", existing[:unit] || detect_unit_test_command)
|
|
728
935
|
integration = ask_with_default("Integration test command", existing[:integration])
|
|
729
936
|
e2e = ask_with_default("End-to-end test command", existing[:e2e])
|
|
730
937
|
|
|
731
|
-
timeout = ask_with_default("Test timeout (seconds)", (existing[:timeout_seconds] || 1800).to_s)
|
|
938
|
+
timeout = ask_with_default("Test timeout (seconds)", (existing[:timeout_seconds] || 1800).to_s) do |value|
|
|
939
|
+
value.to_i
|
|
940
|
+
end
|
|
732
941
|
|
|
733
|
-
set([
|
|
942
|
+
set(%i[work_loop test], {
|
|
734
943
|
unit: unit,
|
|
735
944
|
integration: integration,
|
|
736
945
|
e2e: e2e,
|
|
@@ -740,16 +949,48 @@ module Aidp
|
|
|
740
949
|
validate_command(unit)
|
|
741
950
|
validate_command(integration)
|
|
742
951
|
validate_command(e2e)
|
|
952
|
+
|
|
953
|
+
# Check RSpec persistence configuration for --only-failures optimization
|
|
954
|
+
check_rspec_persistence_configuration(unit, integration, e2e)
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
# Validates RSpec persistence configuration if RSpec is detected
|
|
958
|
+
# Warns user if example_status_persistence_file_path is not configured
|
|
959
|
+
def check_rspec_persistence_configuration(*commands)
|
|
960
|
+
rspec_commands = commands.compact.select { |cmd| cmd.to_s.downcase.include?("rspec") }
|
|
961
|
+
return if rspec_commands.empty?
|
|
962
|
+
|
|
963
|
+
# Check if spec_helper.rb has persistence configured
|
|
964
|
+
require_relative "../harness/rspec_command_optimizer"
|
|
965
|
+
optimizer = Aidp::Harness::RSpecCommandOptimizer.new(project_dir)
|
|
966
|
+
config_check = optimizer.check_persistence_configuration
|
|
967
|
+
|
|
968
|
+
if config_check[:configured]
|
|
969
|
+
prompt.ok("ā
RSpec persistence configured for --only-failures optimization")
|
|
970
|
+
else
|
|
971
|
+
prompt.warn("\nā ļø RSpec --only-failures optimization not available")
|
|
972
|
+
prompt.say(" The work loop can run significantly faster with --only-failures on subsequent iterations.")
|
|
973
|
+
prompt.say(" To enable, add this to your spec/spec_helper.rb:")
|
|
974
|
+
prompt.say("")
|
|
975
|
+
prompt.say(" RSpec.configure do |config|")
|
|
976
|
+
prompt.say(" config.example_status_persistence_file_path = '.rspec_status'")
|
|
977
|
+
prompt.say(" end")
|
|
978
|
+
prompt.say("")
|
|
979
|
+
prompt.say(" Also add .rspec_status to your .gitignore")
|
|
980
|
+
|
|
981
|
+
Aidp.log_info("setup_wizard", "rspec_persistence_missing",
|
|
982
|
+
message: "RSpec --only-failures optimization not available")
|
|
983
|
+
end
|
|
743
984
|
end
|
|
744
985
|
|
|
745
986
|
def configure_linting
|
|
746
|
-
existing = get([
|
|
987
|
+
existing = get(%i[work_loop lint]) || {}
|
|
747
988
|
|
|
748
989
|
lint_cmd = ask_with_default("Lint command", existing[:command] || detect_lint_command)
|
|
749
990
|
format_cmd = ask_with_default("Format command", existing[:format] || detect_format_command)
|
|
750
991
|
autofix = prompt.yes?("Run formatter automatically?", default: existing.fetch(:autofix, false))
|
|
751
992
|
|
|
752
|
-
set([
|
|
993
|
+
set(%i[work_loop lint], {
|
|
753
994
|
command: lint_cmd,
|
|
754
995
|
format: format_cmd,
|
|
755
996
|
autofix: autofix
|
|
@@ -760,23 +1001,29 @@ module Aidp
|
|
|
760
1001
|
end
|
|
761
1002
|
|
|
762
1003
|
def configure_watch_patterns
|
|
763
|
-
existing = get([
|
|
1004
|
+
existing = get(%i[work_loop test watch]) || {}
|
|
764
1005
|
default_patterns = detect_watch_patterns
|
|
765
1006
|
|
|
766
1007
|
watch_patterns = ask_list("Test watch patterns (comma-separated)", existing.fetch(:patterns, default_patterns))
|
|
767
|
-
set([
|
|
1008
|
+
set(%i[work_loop test watch], {patterns: watch_patterns}) if watch_patterns.any?
|
|
768
1009
|
end
|
|
769
1010
|
|
|
770
1011
|
def configure_guards
|
|
771
|
-
existing = get([
|
|
1012
|
+
existing = get(%i[work_loop guards]) || {}
|
|
772
1013
|
|
|
773
1014
|
include_patterns = ask_list("Guard include patterns", existing[:include] || detect_source_patterns)
|
|
774
|
-
exclude_patterns = ask_list("Guard exclude patterns",
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1015
|
+
exclude_patterns = ask_list("Guard exclude patterns",
|
|
1016
|
+
existing[:exclude] || ["node_modules/**", "dist/**", "build/**"])
|
|
1017
|
+
max_lines = ask_with_default("Max lines changed per commit",
|
|
1018
|
+
(existing[:max_lines_changed_per_commit] || 300).to_s) do |value|
|
|
1019
|
+
value.to_i
|
|
1020
|
+
end
|
|
1021
|
+
protected_paths = ask_list("Protected paths (require confirmation)", existing[:protected_paths] || [],
|
|
1022
|
+
allow_empty: true)
|
|
1023
|
+
confirmation_required = prompt.yes?("Require confirmation before editing protected paths?",
|
|
1024
|
+
default: existing.fetch(:confirm_protected, true))
|
|
1025
|
+
|
|
1026
|
+
set(%i[work_loop guards], {
|
|
780
1027
|
include: include_patterns,
|
|
781
1028
|
exclude: exclude_patterns,
|
|
782
1029
|
max_lines_changed_per_commit: max_lines,
|
|
@@ -787,10 +1034,10 @@ module Aidp
|
|
|
787
1034
|
|
|
788
1035
|
def configure_coverage
|
|
789
1036
|
prompt.say("\nš Coverage configuration")
|
|
790
|
-
existing = get([
|
|
1037
|
+
existing = get(%i[work_loop coverage]) || {}
|
|
791
1038
|
|
|
792
1039
|
enabled = prompt.yes?("Enable coverage tracking?", default: existing.fetch(:enabled, false))
|
|
793
|
-
return set([
|
|
1040
|
+
return set(%i[work_loop coverage], {enabled: false}) unless enabled
|
|
794
1041
|
|
|
795
1042
|
coverage_tool_choices = [
|
|
796
1043
|
["SimpleCov (Ruby)", "simplecov"],
|
|
@@ -801,7 +1048,9 @@ module Aidp
|
|
|
801
1048
|
["Other", "other"]
|
|
802
1049
|
]
|
|
803
1050
|
coverage_tool_default = existing[:tool]
|
|
804
|
-
coverage_tool_default_label = coverage_tool_choices.find
|
|
1051
|
+
coverage_tool_default_label = coverage_tool_choices.find do |label, value|
|
|
1052
|
+
value == coverage_tool_default
|
|
1053
|
+
end&.first
|
|
805
1054
|
|
|
806
1055
|
tool = prompt.select("Which coverage tool do you use?", default: coverage_tool_default_label) do |menu|
|
|
807
1056
|
coverage_tool_choices.each { |label, value| menu.choice label, value }
|
|
@@ -812,12 +1061,13 @@ module Aidp
|
|
|
812
1061
|
fail_on_drop = prompt.yes?("Fail on coverage drop?", default: existing.fetch(:fail_on_drop, false))
|
|
813
1062
|
|
|
814
1063
|
minimum_coverage_default = existing[:minimum_coverage]&.to_s
|
|
815
|
-
minimum_coverage_answer = ask_with_default("Minimum coverage % (optional - press enter to skip)",
|
|
1064
|
+
minimum_coverage_answer = ask_with_default("Minimum coverage % (optional - press enter to skip)",
|
|
1065
|
+
minimum_coverage_default)
|
|
816
1066
|
minimum_coverage = if minimum_coverage_answer && !minimum_coverage_answer.to_s.strip.empty?
|
|
817
1067
|
minimum_coverage_answer.to_f
|
|
818
1068
|
end
|
|
819
1069
|
|
|
820
|
-
set([
|
|
1070
|
+
set(%i[work_loop coverage], {
|
|
821
1071
|
enabled: true,
|
|
822
1072
|
tool: tool,
|
|
823
1073
|
run_command: run_command,
|
|
@@ -831,10 +1081,10 @@ module Aidp
|
|
|
831
1081
|
|
|
832
1082
|
def configure_interactive_testing
|
|
833
1083
|
prompt.say("\nšÆ Interactive testing configuration")
|
|
834
|
-
existing = get([
|
|
1084
|
+
existing = get(%i[work_loop interactive_testing]) || {}
|
|
835
1085
|
|
|
836
1086
|
enabled = prompt.yes?("Enable interactive testing tools?", default: existing.fetch(:enabled, false))
|
|
837
|
-
return set([
|
|
1087
|
+
return set(%i[work_loop interactive_testing], {enabled: false}) unless enabled
|
|
838
1088
|
|
|
839
1089
|
app_type_choices = [
|
|
840
1090
|
["Web application", "web"],
|
|
@@ -859,7 +1109,7 @@ module Aidp
|
|
|
859
1109
|
tools[:desktop] = configure_desktop_testing_tools(existing.dig(:tools, :desktop) || {})
|
|
860
1110
|
end
|
|
861
1111
|
|
|
862
|
-
set([
|
|
1112
|
+
set(%i[work_loop interactive_testing], {
|
|
863
1113
|
enabled: true,
|
|
864
1114
|
app_type: app_type,
|
|
865
1115
|
tools: tools
|
|
@@ -869,17 +1119,22 @@ module Aidp
|
|
|
869
1119
|
def configure_web_testing_tools(existing)
|
|
870
1120
|
tools = {}
|
|
871
1121
|
|
|
872
|
-
playwright_enabled = prompt.yes?("Enable Playwright MCP?",
|
|
1122
|
+
playwright_enabled = prompt.yes?("Enable Playwright MCP?",
|
|
1123
|
+
default: existing.dig(:playwright_mcp, :enabled) || false)
|
|
873
1124
|
if playwright_enabled
|
|
874
|
-
playwright_run = ask_with_default("Playwright run command",
|
|
875
|
-
|
|
1125
|
+
playwright_run = ask_with_default("Playwright run command",
|
|
1126
|
+
existing.dig(:playwright_mcp, :run) || "npx playwright test")
|
|
1127
|
+
playwright_specs = ask_with_default("Playwright specs directory",
|
|
1128
|
+
existing.dig(:playwright_mcp, :specs_dir) || ".aidp/tests/web")
|
|
876
1129
|
tools[:playwright_mcp] = {enabled: true, run: playwright_run, specs_dir: playwright_specs}
|
|
877
1130
|
end
|
|
878
1131
|
|
|
879
|
-
chrome_enabled = prompt.yes?("Enable Chrome DevTools MCP?",
|
|
1132
|
+
chrome_enabled = prompt.yes?("Enable Chrome DevTools MCP?",
|
|
1133
|
+
default: existing.dig(:chrome_devtools_mcp, :enabled) || false)
|
|
880
1134
|
if chrome_enabled
|
|
881
1135
|
chrome_run = ask_with_default("Chrome DevTools run command", existing.dig(:chrome_devtools_mcp, :run) || "")
|
|
882
|
-
chrome_specs = ask_with_default("Chrome DevTools specs directory",
|
|
1136
|
+
chrome_specs = ask_with_default("Chrome DevTools specs directory",
|
|
1137
|
+
existing.dig(:chrome_devtools_mcp, :specs_dir) || ".aidp/tests/web")
|
|
883
1138
|
tools[:chrome_devtools_mcp] = {enabled: true, run: chrome_run, specs_dir: chrome_specs}
|
|
884
1139
|
end
|
|
885
1140
|
|
|
@@ -891,8 +1146,10 @@ module Aidp
|
|
|
891
1146
|
|
|
892
1147
|
expect_enabled = prompt.yes?("Enable expect scripts?", default: existing.dig(:expect, :enabled) || false)
|
|
893
1148
|
if expect_enabled
|
|
894
|
-
expect_run = ask_with_default("Expect run command",
|
|
895
|
-
|
|
1149
|
+
expect_run = ask_with_default("Expect run command",
|
|
1150
|
+
existing.dig(:expect, :run) || "expect .aidp/tests/cli/smoke.exp")
|
|
1151
|
+
expect_specs = ask_with_default("Expect specs directory",
|
|
1152
|
+
existing.dig(:expect, :specs_dir) || ".aidp/tests/cli")
|
|
896
1153
|
tools[:expect] = {enabled: true, run: expect_run, specs_dir: expect_specs}
|
|
897
1154
|
end
|
|
898
1155
|
|
|
@@ -902,16 +1159,22 @@ module Aidp
|
|
|
902
1159
|
def configure_desktop_testing_tools(existing)
|
|
903
1160
|
tools = {}
|
|
904
1161
|
|
|
905
|
-
applescript_enabled = prompt.yes?("Enable AppleScript testing?",
|
|
1162
|
+
applescript_enabled = prompt.yes?("Enable AppleScript testing?",
|
|
1163
|
+
default: existing.dig(:applescript, :enabled) || false)
|
|
906
1164
|
if applescript_enabled
|
|
907
|
-
applescript_run = ask_with_default("AppleScript run command",
|
|
908
|
-
|
|
1165
|
+
applescript_run = ask_with_default("AppleScript run command",
|
|
1166
|
+
existing.dig(:applescript,
|
|
1167
|
+
:run) || "osascript .aidp/tests/desktop/smoke.scpt")
|
|
1168
|
+
applescript_specs = ask_with_default("AppleScript specs directory",
|
|
1169
|
+
existing.dig(:applescript, :specs_dir) || ".aidp/tests/desktop")
|
|
909
1170
|
tools[:applescript] = {enabled: true, run: applescript_run, specs_dir: applescript_specs}
|
|
910
1171
|
end
|
|
911
1172
|
|
|
912
|
-
screen_reader_enabled = prompt.yes?("Enable screen reader testing?",
|
|
1173
|
+
screen_reader_enabled = prompt.yes?("Enable screen reader testing?",
|
|
1174
|
+
default: existing.dig(:screen_reader, :enabled) || false)
|
|
913
1175
|
if screen_reader_enabled
|
|
914
|
-
screen_reader_notes = ask_with_default("Screen reader testing notes (optional)",
|
|
1176
|
+
screen_reader_notes = ask_with_default("Screen reader testing notes (optional)",
|
|
1177
|
+
existing.dig(:screen_reader, :notes) || "VoiceOver scripted checks")
|
|
915
1178
|
tools[:screen_reader] = {enabled: true, notes: screen_reader_notes}
|
|
916
1179
|
end
|
|
917
1180
|
|
|
@@ -920,7 +1183,7 @@ module Aidp
|
|
|
920
1183
|
|
|
921
1184
|
def configure_vcs_behavior
|
|
922
1185
|
prompt.say("\nšļø Version control configuration")
|
|
923
|
-
existing = get([
|
|
1186
|
+
existing = get(%i[work_loop version_control]) || {}
|
|
924
1187
|
|
|
925
1188
|
# Detect VCS
|
|
926
1189
|
detected_vcs = detect_vcs_tool
|
|
@@ -933,7 +1196,8 @@ module Aidp
|
|
|
933
1196
|
vcs_default_label = vcs_choices.find { |label, value| value == vcs_default }&.first
|
|
934
1197
|
|
|
935
1198
|
vcs_tool = if detected_vcs
|
|
936
|
-
prompt.select("Detected #{detected_vcs}. Use this version control system?",
|
|
1199
|
+
prompt.select("Detected #{detected_vcs}. Use this version control system?",
|
|
1200
|
+
default: vcs_default_label) do |menu|
|
|
937
1201
|
vcs_choices.each { |label, value| menu.choice label, value }
|
|
938
1202
|
end
|
|
939
1203
|
else
|
|
@@ -942,7 +1206,7 @@ module Aidp
|
|
|
942
1206
|
end
|
|
943
1207
|
end
|
|
944
1208
|
|
|
945
|
-
return set([
|
|
1209
|
+
return set(%i[work_loop version_control], {tool: "none", behavior: "nothing"}) if vcs_tool == "none"
|
|
946
1210
|
|
|
947
1211
|
prompt.say("\nš Commit Behavior (applies to copilot/interactive mode only)")
|
|
948
1212
|
prompt.say("Note: Watch mode and fully automatic daemon mode will always commit changes.")
|
|
@@ -970,7 +1234,7 @@ module Aidp
|
|
|
970
1234
|
{auto_create_pr: false}
|
|
971
1235
|
end
|
|
972
1236
|
|
|
973
|
-
set([
|
|
1237
|
+
set(%i[work_loop version_control], {
|
|
974
1238
|
tool: vcs_tool,
|
|
975
1239
|
behavior: behavior,
|
|
976
1240
|
**commit_config,
|
|
@@ -997,7 +1261,9 @@ module Aidp
|
|
|
997
1261
|
["Emoji (e.g., '⨠feat: add user authentication')", "emoji"]
|
|
998
1262
|
]
|
|
999
1263
|
commit_style_default = existing[:commit_style] || "default"
|
|
1000
|
-
commit_style_default_label = commit_style_choices.find
|
|
1264
|
+
commit_style_default_label = commit_style_choices.find do |label, value|
|
|
1265
|
+
value == commit_style_default
|
|
1266
|
+
end&.first
|
|
1001
1267
|
|
|
1002
1268
|
prompt.select("Conventional commit style:", default: commit_style_default_label) do |menu|
|
|
1003
1269
|
commit_style_choices.each { |label, value| menu.choice label, value }
|
|
@@ -1060,13 +1326,14 @@ module Aidp
|
|
|
1060
1326
|
def configure_branching
|
|
1061
1327
|
prompt.say("\nšæ Branching strategy")
|
|
1062
1328
|
prompt.say("-" * 40)
|
|
1063
|
-
existing = get([
|
|
1329
|
+
existing = get(%i[work_loop branching]) || {}
|
|
1064
1330
|
|
|
1065
1331
|
prefix = ask_with_default("Branch prefix for work loops", existing[:prefix] || "aidp")
|
|
1066
|
-
slug_format = ask_with_default("Slug format (use
|
|
1067
|
-
|
|
1332
|
+
slug_format = ask_with_default("Slug format (use %<id>s and %<title>s)",
|
|
1333
|
+
existing[:slug_format] || "issue-%<id>s-%<title>s")
|
|
1334
|
+
checkpoint_tag = ask_with_default("Checkpoint tag template", existing[:checkpoint_tag] || "aidp-start/%<id>s")
|
|
1068
1335
|
|
|
1069
|
-
set([
|
|
1336
|
+
set(%i[work_loop branching], {
|
|
1070
1337
|
prefix: prefix,
|
|
1071
1338
|
slug_format: slug_format,
|
|
1072
1339
|
checkpoint_tag: checkpoint_tag
|
|
@@ -1076,13 +1343,13 @@ module Aidp
|
|
|
1076
1343
|
def configure_artifacts
|
|
1077
1344
|
prompt.say("\nš Artifact storage")
|
|
1078
1345
|
prompt.say("-" * 40)
|
|
1079
|
-
existing = get([
|
|
1346
|
+
existing = get(%i[work_loop artifacts]) || {}
|
|
1080
1347
|
|
|
1081
1348
|
evidence_dir = ask_with_default("Evidence pack directory", existing[:evidence_dir] || ".aidp/evidence")
|
|
1082
1349
|
logs_dir = ask_with_default("Logs directory", existing[:logs_dir] || ".aidp/logs")
|
|
1083
1350
|
screenshots_dir = ask_with_default("Screenshots directory", existing[:screenshots_dir] || ".aidp/screenshots")
|
|
1084
1351
|
|
|
1085
|
-
set([
|
|
1352
|
+
set(%i[work_loop artifacts], {
|
|
1086
1353
|
evidence_dir: evidence_dir,
|
|
1087
1354
|
logs_dir: logs_dir,
|
|
1088
1355
|
screenshots_dir: screenshots_dir
|
|
@@ -1104,10 +1371,10 @@ module Aidp
|
|
|
1104
1371
|
|
|
1105
1372
|
unless configure
|
|
1106
1373
|
Aidp.log_debug("setup_wizard.nfrs", "opt_out")
|
|
1107
|
-
return set([
|
|
1374
|
+
return set(%i[nfrs configure], false)
|
|
1108
1375
|
end
|
|
1109
1376
|
|
|
1110
|
-
set([
|
|
1377
|
+
set(%i[nfrs configure], true)
|
|
1111
1378
|
categories = %i[performance security reliability accessibility internationalization]
|
|
1112
1379
|
categories.each do |category|
|
|
1113
1380
|
existing = get([:nfrs, category])
|
|
@@ -1126,11 +1393,11 @@ module Aidp
|
|
|
1126
1393
|
prompt.say("\nš Detected stack: #{(stack == :other) ? "Custom" : stack.to_s.capitalize}")
|
|
1127
1394
|
case stack
|
|
1128
1395
|
when :rails
|
|
1129
|
-
set([
|
|
1396
|
+
set(%i[nfrs preferred_libraries rails], configure_rails_libraries)
|
|
1130
1397
|
when :node
|
|
1131
|
-
set([
|
|
1398
|
+
set(%i[nfrs preferred_libraries node], configure_node_libraries)
|
|
1132
1399
|
when :python
|
|
1133
|
-
set([
|
|
1400
|
+
set(%i[nfrs preferred_libraries python], configure_python_libraries)
|
|
1134
1401
|
else
|
|
1135
1402
|
custom_stack = ask_with_default("Name this stack (e.g. go, php)", "custom")
|
|
1136
1403
|
libs = ask_list("Preferred libraries (comma-separated)", [])
|
|
@@ -1154,7 +1421,7 @@ module Aidp
|
|
|
1154
1421
|
end
|
|
1155
1422
|
|
|
1156
1423
|
def configure_rails_libraries
|
|
1157
|
-
existing = get([
|
|
1424
|
+
existing = get(%i[nfrs preferred_libraries rails]) || {}
|
|
1158
1425
|
{
|
|
1159
1426
|
auth: ask_with_default("Authentication gem", existing[:auth] || "devise"),
|
|
1160
1427
|
authz: ask_with_default("Authorization gem", existing[:authz] || "pundit"),
|
|
@@ -1164,7 +1431,7 @@ module Aidp
|
|
|
1164
1431
|
end
|
|
1165
1432
|
|
|
1166
1433
|
def configure_node_libraries
|
|
1167
|
-
existing = get([
|
|
1434
|
+
existing = get(%i[nfrs preferred_libraries node]) || {}
|
|
1168
1435
|
{
|
|
1169
1436
|
validation: ask_with_default("Validation library", existing[:validation] || "zod"),
|
|
1170
1437
|
orm: ask_with_default("ORM/Database", existing[:orm] || "prisma"),
|
|
@@ -1173,7 +1440,7 @@ module Aidp
|
|
|
1173
1440
|
end
|
|
1174
1441
|
|
|
1175
1442
|
def configure_python_libraries
|
|
1176
|
-
existing = get([
|
|
1443
|
+
existing = get(%i[nfrs preferred_libraries python]) || {}
|
|
1177
1444
|
linting = ask_list("Linting tools", existing[:linting] || %w[ruff mypy])
|
|
1178
1445
|
{
|
|
1179
1446
|
validation: ask_with_default("Validation library", existing[:validation] || "pydantic"),
|
|
@@ -1191,9 +1458,9 @@ module Aidp
|
|
|
1191
1458
|
existing = get([:logging]) || {}
|
|
1192
1459
|
|
|
1193
1460
|
log_level_choices = [
|
|
1194
|
-
[
|
|
1195
|
-
[
|
|
1196
|
-
[
|
|
1461
|
+
%w[Debug debug],
|
|
1462
|
+
%w[Info info],
|
|
1463
|
+
%w[Error error]
|
|
1197
1464
|
]
|
|
1198
1465
|
log_level_default = existing[:level] || "info"
|
|
1199
1466
|
log_level_default_label = log_level_choices.find { |label, value| value == log_level_default }&.first
|
|
@@ -1284,9 +1551,11 @@ module Aidp
|
|
|
1284
1551
|
prompt.say("-" * 40)
|
|
1285
1552
|
existing = get([:modes]) || {}
|
|
1286
1553
|
|
|
1287
|
-
background = prompt.yes?("Run in background mode by default?",
|
|
1554
|
+
background = prompt.yes?("Run in background mode by default?",
|
|
1555
|
+
default: existing.fetch(:background_default, false))
|
|
1288
1556
|
watch = prompt.yes?("Enable watch mode integrations?", default: existing.fetch(:watch_enabled, false))
|
|
1289
|
-
quick_mode = prompt.yes?("Enable quick mode (short timeouts) by default?",
|
|
1557
|
+
quick_mode = prompt.yes?("Enable quick mode (short timeouts) by default?",
|
|
1558
|
+
default: existing.fetch(:quick_mode_default, false))
|
|
1290
1559
|
|
|
1291
1560
|
set([:modes], {
|
|
1292
1561
|
background_default: background,
|
|
@@ -1310,7 +1579,7 @@ module Aidp
|
|
|
1310
1579
|
|
|
1311
1580
|
def configure_watch_safety
|
|
1312
1581
|
prompt.say("\nš Watch mode safety settings")
|
|
1313
|
-
existing = get([
|
|
1582
|
+
existing = get(%i[watch safety]) || {}
|
|
1314
1583
|
|
|
1315
1584
|
allow_public_repos = prompt.yes?(
|
|
1316
1585
|
"Allow watch mode on public repositories?",
|
|
@@ -1330,7 +1599,7 @@ module Aidp
|
|
|
1330
1599
|
default: existing.fetch(:require_container, true)
|
|
1331
1600
|
)
|
|
1332
1601
|
|
|
1333
|
-
set([
|
|
1602
|
+
set(%i[watch safety], {
|
|
1334
1603
|
allow_public_repos: allow_public_repos,
|
|
1335
1604
|
author_allowlist: author_allowlist,
|
|
1336
1605
|
require_container: require_container
|
|
@@ -1340,7 +1609,7 @@ module Aidp
|
|
|
1340
1609
|
def configure_watch_labels
|
|
1341
1610
|
prompt.say("\nš·ļø Watch mode label configuration")
|
|
1342
1611
|
prompt.say(" Configure GitHub issue and PR labels that trigger watch mode actions")
|
|
1343
|
-
existing = get([
|
|
1612
|
+
existing = get(%i[watch labels]) || {}
|
|
1344
1613
|
|
|
1345
1614
|
plan_trigger = ask_with_default(
|
|
1346
1615
|
"Label to trigger plan generation",
|
|
@@ -1382,7 +1651,7 @@ module Aidp
|
|
|
1382
1651
|
existing[:change_request_trigger] || "aidp-request-changes"
|
|
1383
1652
|
)
|
|
1384
1653
|
|
|
1385
|
-
set([
|
|
1654
|
+
set(%i[watch labels], {
|
|
1386
1655
|
plan_trigger: plan_trigger,
|
|
1387
1656
|
needs_input: needs_input,
|
|
1388
1657
|
ready_to_build: ready_to_build,
|
|
@@ -1397,7 +1666,7 @@ module Aidp
|
|
|
1397
1666
|
def configure_watch_change_requests
|
|
1398
1667
|
prompt.say("\nš PR Change Request Configuration")
|
|
1399
1668
|
prompt.say(" Configure how AIDP handles automated PR change requests")
|
|
1400
|
-
existing = get([
|
|
1669
|
+
existing = get(%i[watch change_requests]) || {}
|
|
1401
1670
|
|
|
1402
1671
|
max_diff_size = ask_with_default(
|
|
1403
1672
|
"Maximum PR diff size (lines) for change requests",
|
|
@@ -1409,7 +1678,7 @@ module Aidp
|
|
|
1409
1678
|
default: existing.fetch(:post_detection_comments, true)
|
|
1410
1679
|
)
|
|
1411
1680
|
|
|
1412
|
-
set([
|
|
1681
|
+
set(%i[watch change_requests], {
|
|
1413
1682
|
max_diff_size: max_diff_size
|
|
1414
1683
|
})
|
|
1415
1684
|
|
|
@@ -1462,7 +1731,7 @@ module Aidp
|
|
|
1462
1731
|
Aidp.log_debug("setup_wizard.label_creation", "existing_labels_fetched", count: existing_labels.size)
|
|
1463
1732
|
|
|
1464
1733
|
# Get configured label names
|
|
1465
|
-
labels_config = get([
|
|
1734
|
+
labels_config = get(%i[watch labels]) || {}
|
|
1466
1735
|
required_labels = collect_required_labels(labels_config)
|
|
1467
1736
|
|
|
1468
1737
|
# Determine which labels need to be created
|
|
@@ -1534,7 +1803,8 @@ module Aidp
|
|
|
1534
1803
|
# Fetch existing labels from GitHub
|
|
1535
1804
|
def fetch_existing_labels(owner, repo)
|
|
1536
1805
|
require "open3"
|
|
1537
|
-
stdout, stderr, status = Open3.capture3("gh", "label", "list", "-R", "#{owner}/#{repo}", "--json", "name",
|
|
1806
|
+
stdout, stderr, status = Open3.capture3("gh", "label", "list", "-R", "#{owner}/#{repo}", "--json", "name",
|
|
1807
|
+
"--jq", ".[].name")
|
|
1538
1808
|
|
|
1539
1809
|
unless status.success?
|
|
1540
1810
|
Aidp.log_error("setup_wizard.fetch_labels", "gh_failed", error: stderr)
|
|
@@ -1559,26 +1829,18 @@ module Aidp
|
|
|
1559
1829
|
review_trigger: "FBCA04", # Yellow
|
|
1560
1830
|
ci_fix_trigger: "D93F0B", # Red
|
|
1561
1831
|
auto_trigger: "0C8BD6", # Blue (distinct from build)
|
|
1562
|
-
change_request_trigger: "F9D0C4",
|
|
1563
|
-
in_progress: "1D76DB"
|
|
1832
|
+
change_request_trigger: "F9D0C4", # Light pink
|
|
1833
|
+
in_progress: "1D76DB" # Dark blue (internal coordination)
|
|
1564
1834
|
}
|
|
1565
1835
|
|
|
1566
1836
|
required = []
|
|
1567
1837
|
labels_config.each do |key, name|
|
|
1568
1838
|
next if name.nil? || name.to_s.strip.empty?
|
|
1569
1839
|
|
|
1570
|
-
color = default_colors[key] || "EDEDED"
|
|
1840
|
+
color = default_colors[key] || "EDEDED" # Gray fallback
|
|
1571
1841
|
required << {name: name, color: color, key: key}
|
|
1572
1842
|
end
|
|
1573
1843
|
|
|
1574
|
-
# Always include the internal in-progress label for coordination
|
|
1575
|
-
required << {
|
|
1576
|
-
name: "aidp-in-progress",
|
|
1577
|
-
color: default_colors[:in_progress],
|
|
1578
|
-
key: :in_progress,
|
|
1579
|
-
internal: true
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
1844
|
Aidp.log_debug("setup_wizard.collect_labels", "collected", count: required.size)
|
|
1583
1845
|
required
|
|
1584
1846
|
end
|
|
@@ -1728,6 +1990,7 @@ module Aidp
|
|
|
1728
1990
|
|
|
1729
1991
|
if answer.nil? || answer.strip.empty?
|
|
1730
1992
|
return default if default.nil? || !block_given?
|
|
1993
|
+
|
|
1731
1994
|
return yield(default)
|
|
1732
1995
|
end
|
|
1733
1996
|
|
|
@@ -1744,6 +2007,7 @@ module Aidp
|
|
|
1744
2007
|
line = prompt.ask("", default: nil)
|
|
1745
2008
|
break if line.nil? || line.empty?
|
|
1746
2009
|
return nil if line.strip.casecmp("clear").zero?
|
|
2010
|
+
|
|
1747
2011
|
lines << line
|
|
1748
2012
|
end
|
|
1749
2013
|
return default if lines.empty?
|
|
@@ -1786,6 +2050,7 @@ module Aidp
|
|
|
1786
2050
|
return "bundle exec rspec" if project_file?("Gemfile") && Dir.exist?(File.join(project_dir, "spec"))
|
|
1787
2051
|
return "npm test" if project_file?("package.json")
|
|
1788
2052
|
return "pytest" if project_file?("pytest.ini") || Dir.exist?(File.join(project_dir, "tests"))
|
|
2053
|
+
|
|
1789
2054
|
"echo 'No tests configured'"
|
|
1790
2055
|
end
|
|
1791
2056
|
|
|
@@ -1793,6 +2058,7 @@ module Aidp
|
|
|
1793
2058
|
return "bundle exec rubocop" if project_file?(".rubocop.yml")
|
|
1794
2059
|
return "npm run lint" if project_file?("package.json")
|
|
1795
2060
|
return "ruff check ." if project_file?("pyproject.toml")
|
|
2061
|
+
|
|
1796
2062
|
"echo 'No linter configured'"
|
|
1797
2063
|
end
|
|
1798
2064
|
|
|
@@ -1800,6 +2066,7 @@ module Aidp
|
|
|
1800
2066
|
return "bundle exec rubocop -A" if project_file?(".rubocop.yml")
|
|
1801
2067
|
return "npm run format" if project_file?("package.json")
|
|
1802
2068
|
return "ruff format ." if project_file?("pyproject.toml")
|
|
2069
|
+
|
|
1803
2070
|
"echo 'No formatter configured'"
|
|
1804
2071
|
end
|
|
1805
2072
|
|
|
@@ -1862,6 +2129,7 @@ module Aidp
|
|
|
1862
2129
|
def detect_vcs_tool
|
|
1863
2130
|
return "git" if Dir.exist?(File.join(project_dir, ".git"))
|
|
1864
2131
|
return "svn" if Dir.exist?(File.join(project_dir, ".svn"))
|
|
2132
|
+
|
|
1865
2133
|
nil
|
|
1866
2134
|
end
|
|
1867
2135
|
|
|
@@ -1880,7 +2148,8 @@ module Aidp
|
|
|
1880
2148
|
end
|
|
1881
2149
|
|
|
1882
2150
|
def show_provider_summary(primary, fallbacks)
|
|
1883
|
-
Aidp.log_debug("wizard.provider_summary", "displaying provider configuration table", primary: primary,
|
|
2151
|
+
Aidp.log_debug("wizard.provider_summary", "displaying provider configuration table", primary: primary,
|
|
2152
|
+
fallback_count: fallbacks&.size || 0)
|
|
1884
2153
|
prompt.say("\nš Provider Configuration Summary:")
|
|
1885
2154
|
providers_config = get([:providers]) || {}
|
|
1886
2155
|
|
|
@@ -1960,6 +2229,7 @@ module Aidp
|
|
|
1960
2229
|
# Ensure a minimal billing configuration exists for a selected provider (no secrets)
|
|
1961
2230
|
def ensure_provider_billing_config(provider_name, force: false)
|
|
1962
2231
|
return if provider_name.nil? || provider_name == "custom"
|
|
2232
|
+
|
|
1963
2233
|
providers_section = get([:providers]) || {}
|
|
1964
2234
|
existing = providers_section[provider_name.to_sym]
|
|
1965
2235
|
|
|
@@ -1977,6 +2247,10 @@ module Aidp
|
|
|
1977
2247
|
merged = (existing || {}).merge(type: provider_type, model_family: model_family)
|
|
1978
2248
|
set([:providers, provider_name.to_sym], merged)
|
|
1979
2249
|
normalize_existing_model_families!
|
|
2250
|
+
|
|
2251
|
+
# Offer to configure usage limits for usage-based providers
|
|
2252
|
+
configure_usage_limits(provider_name) if provider_type == "usage_based"
|
|
2253
|
+
|
|
1980
2254
|
action_word = if existing
|
|
1981
2255
|
force ? "reconfigured" : "updated"
|
|
1982
2256
|
else
|
|
@@ -1993,9 +2267,7 @@ module Aidp
|
|
|
1993
2267
|
|
|
1994
2268
|
action = prompt.select("What would you like to do with '#{display_name}'?") do |menu|
|
|
1995
2269
|
menu.choice "Edit configuration", :edit
|
|
1996
|
-
unless is_primary
|
|
1997
|
-
menu.choice "Remove from configuration", :remove
|
|
1998
|
-
end
|
|
2270
|
+
menu.choice "Remove from configuration", :remove unless is_primary
|
|
1999
2271
|
menu.choice "Cancel", :cancel
|
|
2000
2272
|
end
|
|
2001
2273
|
|
|
@@ -2015,12 +2287,12 @@ module Aidp
|
|
|
2015
2287
|
|
|
2016
2288
|
def remove_fallback_provider(provider_name, fallbacks)
|
|
2017
2289
|
display_name = discover_available_providers.invert.fetch(provider_name, provider_name)
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2290
|
+
return unless prompt.yes?("Remove '#{display_name}' from fallback providers?", default: false)
|
|
2291
|
+
|
|
2292
|
+
fallbacks.delete(provider_name)
|
|
2293
|
+
set(%i[harness fallback_providers], fallbacks)
|
|
2294
|
+
Aidp.log_info("wizard.remove_provider", "removed fallback provider", provider: provider_name)
|
|
2295
|
+
prompt.ok("Removed '#{display_name}' from fallback providers")
|
|
2024
2296
|
end
|
|
2025
2297
|
|
|
2026
2298
|
def edit_provider_configuration(provider_name)
|
|
@@ -2028,12 +2300,177 @@ module Aidp
|
|
|
2028
2300
|
prompt.say("\nš§ Editing provider '#{provider_name}' (current: type=#{existing[:type] || "unset"}, model_family=#{existing[:model_family] || "unset"})")
|
|
2029
2301
|
new_type = ask_provider_billing_type_with_default(provider_name, existing[:type])
|
|
2030
2302
|
new_family = ask_model_family(provider_name, existing[:model_family] || "auto")
|
|
2031
|
-
|
|
2303
|
+
|
|
2304
|
+
# Merge with existing config to preserve other keys (e.g., usage_limits, custom settings)
|
|
2305
|
+
updated_config = existing.merge(type: new_type, model_family: new_family)
|
|
2306
|
+
|
|
2307
|
+
set([:providers, provider_name.to_sym], updated_config)
|
|
2032
2308
|
# Normalize immediately so tests relying on canonical value see 'claude' rather than label
|
|
2033
2309
|
normalize_existing_model_families!
|
|
2310
|
+
|
|
2311
|
+
# Offer to configure/edit usage limits for usage-based providers
|
|
2312
|
+
configure_usage_limits(provider_name) if new_type == "usage_based"
|
|
2313
|
+
|
|
2034
2314
|
prompt.ok("Updated '#{provider_name}' ā type=#{new_type}, model_family=#{new_family}")
|
|
2035
2315
|
end
|
|
2036
2316
|
|
|
2317
|
+
# Configure usage limits for a usage-based provider
|
|
2318
|
+
# @param provider_name [String] Name of the provider
|
|
2319
|
+
def configure_usage_limits(provider_name)
|
|
2320
|
+
existing = get([:providers, provider_name.to_sym, :usage_limits]) || {}
|
|
2321
|
+
existing_enabled = existing[:enabled] == true
|
|
2322
|
+
|
|
2323
|
+
display_name = discover_available_providers.invert.fetch(provider_name, provider_name)
|
|
2324
|
+
|
|
2325
|
+
if existing_enabled
|
|
2326
|
+
prompt.say("\nš° Usage limits currently enabled for '#{display_name}'")
|
|
2327
|
+
action = prompt.select("Configure usage limits?") do |menu|
|
|
2328
|
+
menu.choice "Keep current settings", :keep
|
|
2329
|
+
menu.choice "Edit usage limits", :edit
|
|
2330
|
+
menu.choice "Disable usage limits", :disable
|
|
2331
|
+
end
|
|
2332
|
+
|
|
2333
|
+
case action
|
|
2334
|
+
when :keep
|
|
2335
|
+
return
|
|
2336
|
+
when :disable
|
|
2337
|
+
set([:providers, provider_name.to_sym, :usage_limits], {enabled: false})
|
|
2338
|
+
prompt.ok("Disabled usage limits for '#{display_name}'")
|
|
2339
|
+
return
|
|
2340
|
+
end
|
|
2341
|
+
else
|
|
2342
|
+
return unless prompt.yes?("\nš° Configure usage limits for '#{display_name}'? (prevents runaway costs)",
|
|
2343
|
+
default: false)
|
|
2344
|
+
end
|
|
2345
|
+
|
|
2346
|
+
# Configure usage limits
|
|
2347
|
+
limits_config = ask_usage_limits_config(provider_name, existing)
|
|
2348
|
+
set([:providers, provider_name.to_sym, :usage_limits], limits_config)
|
|
2349
|
+
|
|
2350
|
+
return unless limits_config[:enabled]
|
|
2351
|
+
|
|
2352
|
+
prompt.ok("Configured usage limits for '#{display_name}'")
|
|
2353
|
+
end
|
|
2354
|
+
|
|
2355
|
+
# Ask for usage limits configuration
|
|
2356
|
+
# @param provider_name [String] Provider name
|
|
2357
|
+
# @param existing [Hash] Existing configuration
|
|
2358
|
+
# @return [Hash] Usage limits configuration
|
|
2359
|
+
def ask_usage_limits_config(provider_name, existing = {})
|
|
2360
|
+
display_name = discover_available_providers.invert.fetch(provider_name, provider_name)
|
|
2361
|
+
|
|
2362
|
+
# Period selection
|
|
2363
|
+
period_choices = [
|
|
2364
|
+
%w[Monthly monthly],
|
|
2365
|
+
%w[Weekly weekly],
|
|
2366
|
+
%w[Daily daily]
|
|
2367
|
+
]
|
|
2368
|
+
period_default = existing[:period] || "monthly"
|
|
2369
|
+
period_default_label = period_choices.find { |label, value| value == period_default }&.first
|
|
2370
|
+
|
|
2371
|
+
period = prompt.select("Billing period for '#{display_name}':", default: period_default_label) do |menu|
|
|
2372
|
+
period_choices.each { |label, value| menu.choice label, value }
|
|
2373
|
+
end
|
|
2374
|
+
|
|
2375
|
+
# Reset day (only for monthly)
|
|
2376
|
+
reset_day = 1
|
|
2377
|
+
if period == "monthly"
|
|
2378
|
+
reset_day = prompt.ask("Day of month for period reset (1-28):",
|
|
2379
|
+
default: (existing[:reset_day] || 1).to_s,
|
|
2380
|
+
convert: :int) do |q|
|
|
2381
|
+
q.validate(->(v) { v.to_i.between?(1, 28) }, "Please enter a day between 1 and 28")
|
|
2382
|
+
end
|
|
2383
|
+
end
|
|
2384
|
+
|
|
2385
|
+
# Limit type selection
|
|
2386
|
+
limit_type = prompt.select("How would you like to set limits?") do |menu|
|
|
2387
|
+
menu.choice "By tier (different limits for mini vs advanced)", :tier
|
|
2388
|
+
menu.choice "Global (single limit for all usage)", :global
|
|
2389
|
+
menu.choice "Skip (no limits)", :skip
|
|
2390
|
+
end
|
|
2391
|
+
|
|
2392
|
+
return {enabled: false} if limit_type == :skip
|
|
2393
|
+
|
|
2394
|
+
if limit_type == :tier
|
|
2395
|
+
ask_tier_limits_config(period, reset_day, existing)
|
|
2396
|
+
else
|
|
2397
|
+
ask_global_limits_config(period, reset_day, existing)
|
|
2398
|
+
end
|
|
2399
|
+
end
|
|
2400
|
+
|
|
2401
|
+
# Ask for global limits configuration
|
|
2402
|
+
def ask_global_limits_config(period, reset_day, existing = {})
|
|
2403
|
+
max_cost = prompt.ask("Maximum cost per #{period} in USD (e.g., 50.00, or blank for no limit):",
|
|
2404
|
+
default: existing[:max_cost]&.to_s) do |q|
|
|
2405
|
+
q.validate(/\A(\d+\.?\d*|)$/, "Please enter a valid number or leave blank")
|
|
2406
|
+
end
|
|
2407
|
+
|
|
2408
|
+
max_tokens = prompt.ask("Maximum tokens per #{period} (e.g., 1000000, or blank for no limit):",
|
|
2409
|
+
default: existing[:max_tokens]&.to_s) do |q|
|
|
2410
|
+
q.validate(/\A(\d+|)$/, "Please enter a valid integer or leave blank")
|
|
2411
|
+
end
|
|
2412
|
+
|
|
2413
|
+
config = {
|
|
2414
|
+
enabled: true,
|
|
2415
|
+
period: period,
|
|
2416
|
+
reset_day: reset_day
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
config[:max_cost] = max_cost.to_f if max_cost && !max_cost.strip.empty?
|
|
2420
|
+
config[:max_tokens] = max_tokens.to_i if max_tokens && !max_tokens.strip.empty?
|
|
2421
|
+
|
|
2422
|
+
config
|
|
2423
|
+
end
|
|
2424
|
+
|
|
2425
|
+
# Ask for tier-based limits configuration
|
|
2426
|
+
def ask_tier_limits_config(period, reset_day, existing = {})
|
|
2427
|
+
existing_tier_limits = existing[:tier_limits] || {}
|
|
2428
|
+
|
|
2429
|
+
prompt.say("\nš Configure limits by tier (mini models typically have higher limits)")
|
|
2430
|
+
|
|
2431
|
+
tier_limits = {}
|
|
2432
|
+
|
|
2433
|
+
# Mini tier
|
|
2434
|
+
prompt.say("\nš¹ Mini tier (fast, cheap models):")
|
|
2435
|
+
mini_limits = ask_single_tier_limits("mini", period,
|
|
2436
|
+
existing_tier_limits[:mini] || existing_tier_limits["mini"])
|
|
2437
|
+
tier_limits[:mini] = mini_limits if mini_limits
|
|
2438
|
+
|
|
2439
|
+
# Advanced tier
|
|
2440
|
+
prompt.say("\nšø Advanced tier (powerful, expensive models):")
|
|
2441
|
+
advanced_limits = ask_single_tier_limits("advanced", period,
|
|
2442
|
+
existing_tier_limits[:advanced] || existing_tier_limits["advanced"])
|
|
2443
|
+
tier_limits[:advanced] = advanced_limits if advanced_limits
|
|
2444
|
+
|
|
2445
|
+
{
|
|
2446
|
+
enabled: true,
|
|
2447
|
+
period: period,
|
|
2448
|
+
reset_day: reset_day,
|
|
2449
|
+
tier_limits: tier_limits
|
|
2450
|
+
}
|
|
2451
|
+
end
|
|
2452
|
+
|
|
2453
|
+
# Ask for limits for a single tier
|
|
2454
|
+
def ask_single_tier_limits(tier, period, existing = {})
|
|
2455
|
+
existing ||= {}
|
|
2456
|
+
|
|
2457
|
+
max_cost = prompt.ask(" Max cost per #{period} for #{tier} tier (USD, blank for no limit):",
|
|
2458
|
+
default: existing[:max_cost]&.to_s) do |q|
|
|
2459
|
+
q.validate(/\A(\d+\.?\d*|)$/, "Please enter a valid number or leave blank")
|
|
2460
|
+
end
|
|
2461
|
+
|
|
2462
|
+
max_tokens = prompt.ask(" Max tokens per #{period} for #{tier} tier (blank for no limit):",
|
|
2463
|
+
default: existing[:max_tokens]&.to_s) do |q|
|
|
2464
|
+
q.validate(/\A(\d+|)$/, "Please enter a valid integer or leave blank")
|
|
2465
|
+
end
|
|
2466
|
+
|
|
2467
|
+
limits = {}
|
|
2468
|
+
limits[:max_cost] = max_cost.to_f if max_cost && !max_cost.strip.empty?
|
|
2469
|
+
limits[:max_tokens] = max_tokens.to_i if max_tokens && !max_tokens.strip.empty?
|
|
2470
|
+
|
|
2471
|
+
limits.empty? ? nil : limits
|
|
2472
|
+
end
|
|
2473
|
+
|
|
2037
2474
|
def ask_provider_billing_type(provider_name)
|
|
2038
2475
|
ask_provider_billing_type_with_default(provider_name, nil)
|
|
2039
2476
|
end
|
|
@@ -2076,7 +2513,7 @@ module Aidp
|
|
|
2076
2513
|
|
|
2077
2514
|
# Try label -> value mapping (case-insensitive)
|
|
2078
2515
|
choices = ProviderRegistry.model_family_choices
|
|
2079
|
-
mapped = choices.find { |label, _| label.
|
|
2516
|
+
mapped = choices.find { |label, _| label.casecmp(value.to_s).zero? }&.last
|
|
2080
2517
|
return mapped if mapped
|
|
2081
2518
|
|
|
2082
2519
|
# Unknown legacy entry -> fallback to auto
|
|
@@ -2086,8 +2523,10 @@ module Aidp
|
|
|
2086
2523
|
def normalize_existing_model_families!
|
|
2087
2524
|
providers_cfg = @config[:providers]
|
|
2088
2525
|
return unless providers_cfg.is_a?(Hash)
|
|
2526
|
+
|
|
2089
2527
|
providers_cfg.each do |prov_name, prov_cfg|
|
|
2090
2528
|
next unless prov_cfg.is_a?(Hash)
|
|
2529
|
+
|
|
2091
2530
|
mf = prov_cfg[:model_family]
|
|
2092
2531
|
# Normalize and write back only if different to avoid unnecessary YAML churn
|
|
2093
2532
|
normalized = normalize_model_family(mf)
|
|
@@ -2122,19 +2561,19 @@ module Aidp
|
|
|
2122
2561
|
combined = []
|
|
2123
2562
|
(Array(existing_models) + Array(new_models)).each do |entry|
|
|
2124
2563
|
next unless entry.is_a?(Hash)
|
|
2564
|
+
|
|
2125
2565
|
provider = entry[:provider]
|
|
2126
2566
|
model = entry[:model]
|
|
2127
2567
|
next unless provider && model
|
|
2128
2568
|
|
|
2129
|
-
unless combined.any? { |m| m[:provider] == provider && m[:model] == model }
|
|
2130
|
-
combined << entry
|
|
2131
|
-
end
|
|
2569
|
+
combined << entry unless combined.any? { |m| m[:provider] == provider && m[:model] == model }
|
|
2132
2570
|
end
|
|
2133
2571
|
combined
|
|
2134
2572
|
end
|
|
2135
2573
|
|
|
2136
2574
|
def load_existing_config
|
|
2137
2575
|
return {} unless File.exist?(config_path)
|
|
2576
|
+
|
|
2138
2577
|
YAML.safe_load_file(config_path, permitted_classes: [Time]) || {}
|
|
2139
2578
|
rescue => e
|
|
2140
2579
|
@warnings << "Failed to parse existing configuration: #{e.message}"
|
|
@@ -2302,7 +2741,7 @@ module Aidp
|
|
|
2302
2741
|
|
|
2303
2742
|
unless manage
|
|
2304
2743
|
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.opt_out")
|
|
2305
|
-
return set([
|
|
2744
|
+
return set(%i[devcontainer manage], false)
|
|
2306
2745
|
end
|
|
2307
2746
|
|
|
2308
2747
|
# Build wizard config and detect ports
|
|
@@ -2340,9 +2779,9 @@ module Aidp
|
|
|
2340
2779
|
end
|
|
2341
2780
|
|
|
2342
2781
|
# Save configuration
|
|
2343
|
-
set([
|
|
2344
|
-
set([
|
|
2345
|
-
set([
|
|
2782
|
+
set(%i[devcontainer manage], true)
|
|
2783
|
+
set(%i[devcontainer custom_ports], custom_ports) if custom_ports.any?
|
|
2784
|
+
set(%i[devcontainer last_generated], Time.now.utc.iso8601)
|
|
2346
2785
|
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.enabled",
|
|
2347
2786
|
custom_port_count: custom_ports.count,
|
|
2348
2787
|
detected_port_count: detected_ports.count)
|
|
@@ -2364,6 +2803,7 @@ module Aidp
|
|
|
2364
2803
|
return "rails_web" if project_file?("config/routes.rb")
|
|
2365
2804
|
return "sinatra" if project_file?("config.ru")
|
|
2366
2805
|
return "express" if project_file?("app.js") && project_file?("package.json")
|
|
2806
|
+
|
|
2367
2807
|
"cli"
|
|
2368
2808
|
end
|
|
2369
2809
|
|