aidp 0.26.0 → 0.28.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.
- checksums.yaml +4 -4
- data/README.md +89 -0
- data/lib/aidp/cli/checkpoint_command.rb +198 -0
- data/lib/aidp/cli/config_command.rb +71 -0
- data/lib/aidp/cli/enhanced_input.rb +2 -0
- data/lib/aidp/cli/first_run_wizard.rb +8 -7
- data/lib/aidp/cli/harness_command.rb +102 -0
- data/lib/aidp/cli/jobs_command.rb +3 -3
- data/lib/aidp/cli/mcp_dashboard.rb +4 -3
- data/lib/aidp/cli/models_command.rb +661 -0
- data/lib/aidp/cli/providers_command.rb +223 -0
- data/lib/aidp/cli.rb +45 -464
- data/lib/aidp/config.rb +54 -0
- data/lib/aidp/daemon/runner.rb +2 -2
- data/lib/aidp/debug_mixin.rb +25 -10
- data/lib/aidp/execute/agent_signal_parser.rb +22 -0
- data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
- data/lib/aidp/execute/checkpoint_display.rb +38 -37
- data/lib/aidp/execute/interactive_repl.rb +2 -1
- data/lib/aidp/execute/prompt_manager.rb +4 -4
- data/lib/aidp/execute/repl_macros.rb +2 -2
- data/lib/aidp/execute/steps.rb +94 -1
- data/lib/aidp/execute/work_loop_runner.rb +238 -19
- data/lib/aidp/execute/workflow_selector.rb +4 -27
- data/lib/aidp/firewall/provider_requirements_collector.rb +262 -0
- data/lib/aidp/harness/ai_decision_engine.rb +35 -2
- data/lib/aidp/harness/config_manager.rb +5 -10
- data/lib/aidp/harness/config_schema.rb +8 -0
- data/lib/aidp/harness/configuration.rb +40 -2
- data/lib/aidp/harness/enhanced_runner.rb +25 -19
- data/lib/aidp/harness/error_handler.rb +23 -73
- data/lib/aidp/harness/model_cache.rb +269 -0
- data/lib/aidp/harness/model_discovery_service.rb +259 -0
- data/lib/aidp/harness/model_registry.rb +201 -0
- data/lib/aidp/harness/provider_factory.rb +11 -2
- data/lib/aidp/harness/runner.rb +5 -0
- data/lib/aidp/harness/state_manager.rb +0 -7
- data/lib/aidp/harness/thinking_depth_manager.rb +202 -7
- data/lib/aidp/harness/ui/enhanced_tui.rb +8 -18
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +0 -18
- data/lib/aidp/harness/ui/progress_display.rb +6 -2
- data/lib/aidp/harness/user_interface.rb +0 -58
- data/lib/aidp/init/runner.rb +7 -2
- data/lib/aidp/message_display.rb +0 -46
- data/lib/aidp/planning/analyzers/feedback_analyzer.rb +365 -0
- data/lib/aidp/planning/builders/agile_plan_builder.rb +387 -0
- data/lib/aidp/planning/builders/project_plan_builder.rb +193 -0
- data/lib/aidp/planning/generators/gantt_generator.rb +190 -0
- data/lib/aidp/planning/generators/iteration_plan_generator.rb +392 -0
- data/lib/aidp/planning/generators/legacy_research_planner.rb +473 -0
- data/lib/aidp/planning/generators/marketing_report_generator.rb +348 -0
- data/lib/aidp/planning/generators/mvp_scope_generator.rb +310 -0
- data/lib/aidp/planning/generators/user_test_plan_generator.rb +373 -0
- data/lib/aidp/planning/generators/wbs_generator.rb +259 -0
- data/lib/aidp/planning/mappers/persona_mapper.rb +163 -0
- data/lib/aidp/planning/parsers/document_parser.rb +141 -0
- data/lib/aidp/planning/parsers/feedback_data_parser.rb +252 -0
- data/lib/aidp/provider_manager.rb +8 -32
- data/lib/aidp/providers/adapter.rb +2 -4
- data/lib/aidp/providers/aider.rb +264 -0
- data/lib/aidp/providers/anthropic.rb +206 -121
- data/lib/aidp/providers/base.rb +123 -3
- data/lib/aidp/providers/capability_registry.rb +0 -1
- data/lib/aidp/providers/codex.rb +75 -70
- data/lib/aidp/providers/cursor.rb +87 -59
- data/lib/aidp/providers/gemini.rb +57 -60
- data/lib/aidp/providers/github_copilot.rb +19 -66
- data/lib/aidp/providers/kilocode.rb +35 -80
- data/lib/aidp/providers/opencode.rb +35 -80
- data/lib/aidp/setup/wizard.rb +555 -8
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +211 -30
- data/lib/aidp/watch/change_request_processor.rb +128 -14
- data/lib/aidp/watch/ci_fix_processor.rb +103 -37
- data/lib/aidp/watch/ci_log_extractor.rb +258 -0
- data/lib/aidp/watch/github_state_extractor.rb +177 -0
- data/lib/aidp/watch/implementation_verifier.rb +284 -0
- data/lib/aidp/watch/plan_generator.rb +95 -52
- data/lib/aidp/watch/plan_processor.rb +7 -6
- data/lib/aidp/watch/repository_client.rb +245 -17
- data/lib/aidp/watch/review_processor.rb +100 -19
- data/lib/aidp/watch/reviewers/base_reviewer.rb +1 -1
- data/lib/aidp/watch/runner.rb +181 -29
- data/lib/aidp/watch/state_store.rb +22 -1
- data/lib/aidp/workflows/definitions.rb +147 -0
- data/lib/aidp/workflows/guided_agent.rb +3 -3
- data/lib/aidp/workstream_cleanup.rb +245 -0
- data/lib/aidp/worktree.rb +19 -0
- data/templates/aidp-development.yml.example +2 -2
- data/templates/aidp-production.yml.example +3 -3
- data/templates/aidp.yml.example +57 -0
- data/templates/implementation/generate_tdd_specs.md +213 -0
- data/templates/implementation/iterative_implementation.md +122 -0
- data/templates/planning/agile/analyze_feedback.md +183 -0
- data/templates/planning/agile/generate_iteration_plan.md +179 -0
- data/templates/planning/agile/generate_legacy_research_plan.md +171 -0
- data/templates/planning/agile/generate_marketing_report.md +162 -0
- data/templates/planning/agile/generate_mvp_scope.md +127 -0
- data/templates/planning/agile/generate_user_test_plan.md +143 -0
- data/templates/planning/agile/ingest_feedback.md +174 -0
- data/templates/planning/assemble_project_plan.md +113 -0
- data/templates/planning/assign_personas.md +108 -0
- data/templates/planning/create_tasks.md +52 -6
- data/templates/planning/generate_gantt.md +86 -0
- data/templates/planning/generate_wbs.md +85 -0
- data/templates/planning/initialize_planning_mode.md +70 -0
- data/templates/skills/README.md +2 -2
- data/templates/skills/marketing_strategist/SKILL.md +279 -0
- data/templates/skills/product_manager/SKILL.md +177 -0
- data/templates/skills/ruby_aidp_planning/SKILL.md +497 -0
- data/templates/skills/ruby_rspec_tdd/SKILL.md +514 -0
- data/templates/skills/ux_researcher/SKILL.md +222 -0
- metadata +47 -1
|
@@ -157,6 +157,36 @@ module Aidp
|
|
|
157
157
|
},
|
|
158
158
|
default_tier: "mini",
|
|
159
159
|
cache_ttl: nil
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
implementation_verification: {
|
|
163
|
+
prompt_template: "{{prompt}}", # Custom prompt provided by caller
|
|
164
|
+
schema: {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
fully_implemented: {
|
|
168
|
+
type: "boolean",
|
|
169
|
+
description: "True if the implementation fully addresses all issue requirements"
|
|
170
|
+
},
|
|
171
|
+
reasoning: {
|
|
172
|
+
type: "string",
|
|
173
|
+
description: "Detailed explanation of the verification decision"
|
|
174
|
+
},
|
|
175
|
+
missing_requirements: {
|
|
176
|
+
type: "array",
|
|
177
|
+
items: {type: "string"},
|
|
178
|
+
description: "List of specific requirements from the issue that are not yet implemented"
|
|
179
|
+
},
|
|
180
|
+
additional_work_needed: {
|
|
181
|
+
type: "array",
|
|
182
|
+
items: {type: "string"},
|
|
183
|
+
description: "List of specific tasks needed to complete the implementation"
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
required: ["fully_implemented", "reasoning", "missing_requirements", "additional_work_needed"]
|
|
187
|
+
},
|
|
188
|
+
default_tier: "mini",
|
|
189
|
+
cache_ttl: nil
|
|
160
190
|
}
|
|
161
191
|
}.freeze
|
|
162
192
|
|
|
@@ -204,9 +234,12 @@ module Aidp
|
|
|
204
234
|
# Select tier
|
|
205
235
|
selected_tier = tier || template[:default_tier]
|
|
206
236
|
|
|
207
|
-
# Get model for tier
|
|
237
|
+
# Get model for tier, using harness default provider
|
|
208
238
|
thinking_manager = ThinkingDepthManager.new(config)
|
|
209
|
-
provider_name, model_name, _model_data = thinking_manager.select_model_for_tier(
|
|
239
|
+
provider_name, model_name, _model_data = thinking_manager.select_model_for_tier(
|
|
240
|
+
selected_tier,
|
|
241
|
+
provider: config.default_provider
|
|
242
|
+
)
|
|
210
243
|
|
|
211
244
|
Aidp.log_debug("ai_decision_engine", "Making AI decision", {
|
|
212
245
|
decision_type: decision_type,
|
|
@@ -103,11 +103,6 @@ module Aidp
|
|
|
103
103
|
}
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
# Get max retries (alias for backward compatibility with ErrorHandler)
|
|
107
|
-
def max_retries(options = {})
|
|
108
|
-
retry_config(options)[:max_attempts]
|
|
109
|
-
end
|
|
110
|
-
|
|
111
106
|
# Get circuit breaker configuration
|
|
112
107
|
def circuit_breaker_config(options = {})
|
|
113
108
|
harness_config = harness_config(options)
|
|
@@ -359,19 +354,19 @@ module Aidp
|
|
|
359
354
|
def load_config_with_options(options)
|
|
360
355
|
# Apply different loading strategies based on options
|
|
361
356
|
if options[:mode]
|
|
362
|
-
@loader.
|
|
357
|
+
@loader.mode_config(options[:mode], options[:force_reload])
|
|
363
358
|
elsif options[:environment]
|
|
364
|
-
@loader.
|
|
359
|
+
@loader.environment_config(options[:environment], options[:force_reload])
|
|
365
360
|
elsif options[:step]
|
|
366
361
|
@loader.get_step_config(options[:step], options[:force_reload])
|
|
367
362
|
elsif options[:features]
|
|
368
|
-
@loader.
|
|
363
|
+
@loader.config_with_features(options[:features], options[:force_reload])
|
|
369
364
|
elsif options[:user]
|
|
370
365
|
@loader.get_user_config(options[:user], options[:force_reload])
|
|
371
366
|
elsif options[:time_based]
|
|
372
|
-
@loader.
|
|
367
|
+
@loader.time_based_config(options[:force_reload])
|
|
373
368
|
elsif options[:overrides]
|
|
374
|
-
@loader.
|
|
369
|
+
@loader.config_with_overrides(options[:overrides])
|
|
375
370
|
else
|
|
376
371
|
@loader.load_config(options[:force_reload])
|
|
377
372
|
end
|
|
@@ -378,6 +378,7 @@ module Aidp
|
|
|
378
378
|
formatter_commands: [],
|
|
379
379
|
build_commands: [],
|
|
380
380
|
documentation_commands: [],
|
|
381
|
+
task_completion_required: true,
|
|
381
382
|
units: {},
|
|
382
383
|
guards: {enabled: false},
|
|
383
384
|
version_control: {tool: "git", behavior: "nothing", conventional_commits: false},
|
|
@@ -432,6 +433,13 @@ module Aidp
|
|
|
432
433
|
# Items can be strings or {command: string, required: boolean}
|
|
433
434
|
# Validation handled in Configuration class for flexibility
|
|
434
435
|
},
|
|
436
|
+
task_completion_required: {
|
|
437
|
+
type: :boolean,
|
|
438
|
+
required: false,
|
|
439
|
+
default: true
|
|
440
|
+
# When true, all tasks for the current session must be completed or
|
|
441
|
+
# explicitly abandoned (with user confirmation) before work loop can finish
|
|
442
|
+
},
|
|
435
443
|
units: {
|
|
436
444
|
type: :hash,
|
|
437
445
|
required: false,
|
|
@@ -166,6 +166,11 @@ module Aidp
|
|
|
166
166
|
work_loop_config[:max_iterations]
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
+
# Check if task completion is required for work loop completion
|
|
170
|
+
def task_completion_required?
|
|
171
|
+
work_loop_config.fetch(:task_completion_required, true)
|
|
172
|
+
end
|
|
173
|
+
|
|
169
174
|
# Get test commands
|
|
170
175
|
def test_commands
|
|
171
176
|
normalize_commands(work_loop_config[:test_commands] || [])
|
|
@@ -383,12 +388,12 @@ module Aidp
|
|
|
383
388
|
|
|
384
389
|
# Get default thinking tier
|
|
385
390
|
def default_tier
|
|
386
|
-
thinking_config[:default_tier] ||
|
|
391
|
+
thinking_config[:default_tier] || default_thinking_config[:default_tier]
|
|
387
392
|
end
|
|
388
393
|
|
|
389
394
|
# Get maximum thinking tier
|
|
390
395
|
def max_tier
|
|
391
|
-
thinking_config[:max_tier] ||
|
|
396
|
+
thinking_config[:max_tier] || default_thinking_config[:max_tier]
|
|
392
397
|
end
|
|
393
398
|
|
|
394
399
|
# Check if provider switching for tier is allowed
|
|
@@ -432,6 +437,39 @@ module Aidp
|
|
|
432
437
|
thinking_overrides[key] || thinking_overrides[key.to_sym]
|
|
433
438
|
end
|
|
434
439
|
|
|
440
|
+
# Get thinking tiers configuration for a specific provider
|
|
441
|
+
# @param provider_name [String] The provider name
|
|
442
|
+
# @return [Hash] The thinking tiers configuration for the provider
|
|
443
|
+
def provider_thinking_tiers(provider_name)
|
|
444
|
+
provider_cfg = provider_config(provider_name)
|
|
445
|
+
provider_cfg[:thinking_tiers] || provider_cfg["thinking_tiers"] || {}
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Get models configured for a specific tier and provider
|
|
449
|
+
# @param tier [String, Symbol] The tier name (mini, standard, thinking, pro, max)
|
|
450
|
+
# @param provider_name [String] The provider name (required)
|
|
451
|
+
# @return [Array<String>] Array of model names for the tier
|
|
452
|
+
def models_for_tier(tier, provider_name)
|
|
453
|
+
return [] unless provider_name
|
|
454
|
+
|
|
455
|
+
tier_config = provider_thinking_tiers(provider_name)[tier] ||
|
|
456
|
+
provider_thinking_tiers(provider_name)[tier.to_sym]
|
|
457
|
+
return [] unless tier_config
|
|
458
|
+
|
|
459
|
+
models = tier_config[:models] || tier_config["models"]
|
|
460
|
+
return [] unless models
|
|
461
|
+
|
|
462
|
+
# Return simple array of model name strings
|
|
463
|
+
Array(models).map(&:to_s).compact
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Get all configured tiers for a provider
|
|
467
|
+
# @param provider_name [String] The provider name
|
|
468
|
+
# @return [Array<String>] Array of tier names
|
|
469
|
+
def configured_tiers(provider_name)
|
|
470
|
+
provider_thinking_tiers(provider_name).keys.map(&:to_s)
|
|
471
|
+
end
|
|
472
|
+
|
|
435
473
|
# Get fallback configuration
|
|
436
474
|
def fallback_config
|
|
437
475
|
harness_config[:fallback] || default_fallback_config
|
|
@@ -6,6 +6,7 @@ require_relative "ui/job_monitor"
|
|
|
6
6
|
require_relative "ui/workflow_controller"
|
|
7
7
|
require_relative "ui/progress_display"
|
|
8
8
|
require_relative "ui/status_widget"
|
|
9
|
+
require_relative "../errors"
|
|
9
10
|
|
|
10
11
|
module Aidp
|
|
11
12
|
module Harness
|
|
@@ -47,27 +48,31 @@ module Aidp
|
|
|
47
48
|
@selected_steps = options[:selected_steps] || []
|
|
48
49
|
@workflow_type = options[:workflow_type] || :default
|
|
49
50
|
|
|
50
|
-
# Initialize enhanced TUI components
|
|
51
|
-
@tui = UI::EnhancedTUI.new
|
|
52
|
-
@workflow_selector = UI::EnhancedWorkflowSelector.new(@tui)
|
|
53
|
-
@job_monitor = UI::JobMonitor.new
|
|
54
|
-
@workflow_controller = UI::WorkflowController.new
|
|
55
|
-
@progress_display = UI::ProgressDisplay.new
|
|
56
|
-
@status_widget = UI::StatusWidget.new
|
|
51
|
+
# Initialize enhanced TUI components (with dependency injection)
|
|
52
|
+
@tui = options[:tui] || UI::EnhancedTUI.new
|
|
53
|
+
@workflow_selector = options[:workflow_selector] || UI::EnhancedWorkflowSelector.new(@tui)
|
|
54
|
+
@job_monitor = options[:job_monitor] || UI::JobMonitor.new
|
|
55
|
+
@workflow_controller = options[:workflow_controller] || UI::WorkflowController.new
|
|
56
|
+
@progress_display = options[:progress_display] || UI::ProgressDisplay.new
|
|
57
|
+
@status_widget = options[:status_widget] || UI::StatusWidget.new
|
|
57
58
|
|
|
58
|
-
# Initialize other components
|
|
59
|
-
@configuration = Configuration.new(project_dir)
|
|
60
|
-
@state_manager = StateManager.new(project_dir, @mode)
|
|
61
|
-
@provider_manager = ProviderManager.new(@configuration, prompt: @prompt)
|
|
59
|
+
# Initialize other components (with dependency injection)
|
|
60
|
+
@configuration = options[:configuration] || Configuration.new(project_dir)
|
|
61
|
+
@state_manager = options[:state_manager] || StateManager.new(project_dir, @mode)
|
|
62
|
+
@provider_manager = options[:provider_manager] || ProviderManager.new(@configuration, prompt: @prompt)
|
|
62
63
|
|
|
63
64
|
# Use ZFC-enabled condition detector
|
|
64
65
|
# ZfcConditionDetector will create its own ProviderFactory if needed
|
|
65
66
|
# Falls back to legacy pattern matching when ZFC is disabled
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
if options[:condition_detector]
|
|
68
|
+
@condition_detector = options[:condition_detector]
|
|
69
|
+
else
|
|
70
|
+
require_relative "zfc_condition_detector"
|
|
71
|
+
@condition_detector = ZfcConditionDetector.new(@configuration)
|
|
72
|
+
end
|
|
68
73
|
|
|
69
|
-
@error_handler = ErrorHandler.new(@provider_manager, @configuration)
|
|
70
|
-
@completion_checker = CompletionChecker.new(@project_dir, @workflow_type)
|
|
74
|
+
@error_handler = options[:error_handler] || ErrorHandler.new(@provider_manager, @configuration)
|
|
75
|
+
@completion_checker = options[:completion_checker] || CompletionChecker.new(@project_dir, @workflow_type)
|
|
71
76
|
end
|
|
72
77
|
|
|
73
78
|
# Get current provider (delegate to provider manager)
|
|
@@ -101,9 +106,6 @@ module Aidp
|
|
|
101
106
|
@tui.show_message("🚀 Starting #{@mode.to_s.capitalize} Mode", :info)
|
|
102
107
|
|
|
103
108
|
begin
|
|
104
|
-
# Start TUI display loop
|
|
105
|
-
@tui.start_display_loop
|
|
106
|
-
|
|
107
109
|
# Load existing state if resuming
|
|
108
110
|
# Temporarily disabled to test
|
|
109
111
|
# load_state if @state_manager.has_state?
|
|
@@ -158,6 +160,10 @@ module Aidp
|
|
|
158
160
|
handle_completion_criteria_not_met(completion_status)
|
|
159
161
|
end
|
|
160
162
|
end
|
|
163
|
+
rescue Aidp::Errors::ConfigurationError
|
|
164
|
+
# Configuration errors should crash immediately (crash-early principle)
|
|
165
|
+
# Re-raise without catching
|
|
166
|
+
raise
|
|
161
167
|
rescue => e
|
|
162
168
|
@state = STATES[:error]
|
|
163
169
|
# Single error message - don't duplicate
|
|
@@ -165,7 +171,7 @@ module Aidp
|
|
|
165
171
|
ensure
|
|
166
172
|
# Save state before exiting
|
|
167
173
|
save_state
|
|
168
|
-
@tui.
|
|
174
|
+
@tui.restore_screen
|
|
169
175
|
cleanup
|
|
170
176
|
end
|
|
171
177
|
|
|
@@ -4,6 +4,7 @@ require "net/http"
|
|
|
4
4
|
require_relative "../debug_mixin"
|
|
5
5
|
require_relative "../concurrency"
|
|
6
6
|
require_relative "../providers/error_taxonomy"
|
|
7
|
+
require_relative "../errors"
|
|
7
8
|
|
|
8
9
|
module Aidp
|
|
9
10
|
module Harness
|
|
@@ -80,7 +81,7 @@ module Aidp
|
|
|
80
81
|
error_type: error_info[:error_type],
|
|
81
82
|
reason: "Retry not applicable or exhausted"
|
|
82
83
|
})
|
|
83
|
-
if
|
|
84
|
+
if error_info[:error_type].to_sym == :auth_expired
|
|
84
85
|
# Mark provider unhealthy to avoid immediate re-selection
|
|
85
86
|
begin
|
|
86
87
|
if @provider_manager.respond_to?(:mark_provider_auth_failure)
|
|
@@ -107,6 +108,10 @@ module Aidp
|
|
|
107
108
|
begin
|
|
108
109
|
attempt += 1
|
|
109
110
|
return yield
|
|
111
|
+
rescue Aidp::Errors::ConfigurationError
|
|
112
|
+
# Configuration errors should crash immediately (crash-early principle)
|
|
113
|
+
# Re-raise without catching
|
|
114
|
+
raise
|
|
110
115
|
rescue => error
|
|
111
116
|
current_provider = current_provider_safely
|
|
112
117
|
|
|
@@ -394,43 +399,6 @@ module Aidp
|
|
|
394
399
|
jitter: false
|
|
395
400
|
},
|
|
396
401
|
|
|
397
|
-
# Legacy aliases for backward compatibility
|
|
398
|
-
network_error: {
|
|
399
|
-
name: "network_error",
|
|
400
|
-
enabled: true,
|
|
401
|
-
max_retries: 3,
|
|
402
|
-
backoff_strategy: :exponential,
|
|
403
|
-
base_delay: 1.0,
|
|
404
|
-
max_delay: 30.0,
|
|
405
|
-
jitter: true
|
|
406
|
-
},
|
|
407
|
-
server_error: {
|
|
408
|
-
name: "server_error",
|
|
409
|
-
enabled: true,
|
|
410
|
-
max_retries: 2,
|
|
411
|
-
backoff_strategy: :linear,
|
|
412
|
-
base_delay: 2.0,
|
|
413
|
-
max_delay: 10.0,
|
|
414
|
-
jitter: true
|
|
415
|
-
},
|
|
416
|
-
timeout: {
|
|
417
|
-
name: "timeout",
|
|
418
|
-
enabled: true,
|
|
419
|
-
max_retries: 2,
|
|
420
|
-
backoff_strategy: :exponential,
|
|
421
|
-
base_delay: 1.0,
|
|
422
|
-
max_delay: 15.0,
|
|
423
|
-
jitter: true
|
|
424
|
-
},
|
|
425
|
-
rate_limit: {
|
|
426
|
-
name: "rate_limit",
|
|
427
|
-
enabled: false,
|
|
428
|
-
max_retries: 0,
|
|
429
|
-
backoff_strategy: :none,
|
|
430
|
-
base_delay: 0.0,
|
|
431
|
-
max_delay: 0.0,
|
|
432
|
-
jitter: false
|
|
433
|
-
},
|
|
434
402
|
authentication: {
|
|
435
403
|
name: "authentication",
|
|
436
404
|
enabled: false,
|
|
@@ -483,7 +451,7 @@ module Aidp
|
|
|
483
451
|
}
|
|
484
452
|
end
|
|
485
453
|
|
|
486
|
-
def attempt_provider_switch(error_info,
|
|
454
|
+
def attempt_provider_switch(error_info, recovery_plan)
|
|
487
455
|
new_provider = @provider_manager.switch_provider_for_error(
|
|
488
456
|
error_info[:error_type],
|
|
489
457
|
error_info[:context]
|
|
@@ -497,6 +465,18 @@ module Aidp
|
|
|
497
465
|
reason: "Error recovery: #{error_info[:error_type]}"
|
|
498
466
|
}
|
|
499
467
|
else
|
|
468
|
+
# If this is an auth error and we have no fallback providers, crash
|
|
469
|
+
if recovery_plan[:crash_if_no_fallback]
|
|
470
|
+
error_msg = "All providers have failed authentication.\n\n" \
|
|
471
|
+
"Last provider: #{error_info[:provider]}\n" \
|
|
472
|
+
"Error: #{error_info[:error]&.message || error_info[:error]}\n\n" \
|
|
473
|
+
"Please check your API credentials for all configured providers.\n" \
|
|
474
|
+
"Run 'aidp config --interactive' to update credentials."
|
|
475
|
+
|
|
476
|
+
raise Aidp::Errors::ConfigurationError, error_msg
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# For non-auth errors, return failure result
|
|
500
480
|
{
|
|
501
481
|
success: false,
|
|
502
482
|
action: :provider_switch_failed,
|
|
@@ -680,12 +660,13 @@ module Aidp
|
|
|
680
660
|
priority: :high
|
|
681
661
|
}
|
|
682
662
|
when :auth_expired
|
|
683
|
-
#
|
|
684
|
-
#
|
|
663
|
+
# Try to switch to another provider. If no providers available, this will
|
|
664
|
+
# be detected in attempt_recovery and we'll crash (crash-early principle)
|
|
685
665
|
{
|
|
686
666
|
action: :switch_provider,
|
|
687
667
|
reason: "Authentication expired – switching provider to continue",
|
|
688
|
-
priority: :critical
|
|
668
|
+
priority: :critical,
|
|
669
|
+
crash_if_no_fallback: true
|
|
689
670
|
}
|
|
690
671
|
when :quota_exceeded
|
|
691
672
|
{
|
|
@@ -705,37 +686,6 @@ module Aidp
|
|
|
705
686
|
reason: "Permanent error, requires manual intervention",
|
|
706
687
|
priority: :critical
|
|
707
688
|
}
|
|
708
|
-
# Legacy error type mappings for backward compatibility
|
|
709
|
-
when :timeout
|
|
710
|
-
{
|
|
711
|
-
action: :switch_model,
|
|
712
|
-
reason: "Timeout error, trying faster model",
|
|
713
|
-
priority: :medium
|
|
714
|
-
}
|
|
715
|
-
when :network_error
|
|
716
|
-
{
|
|
717
|
-
action: :switch_provider,
|
|
718
|
-
reason: "Network error, switching provider",
|
|
719
|
-
priority: :medium
|
|
720
|
-
}
|
|
721
|
-
when :server_error
|
|
722
|
-
{
|
|
723
|
-
action: :switch_provider,
|
|
724
|
-
reason: "Server error, switching provider",
|
|
725
|
-
priority: :medium
|
|
726
|
-
}
|
|
727
|
-
when :authentication, :permission_denied
|
|
728
|
-
{
|
|
729
|
-
action: :switch_provider,
|
|
730
|
-
reason: "Authentication/permission issue – switching provider to continue",
|
|
731
|
-
priority: :critical
|
|
732
|
-
}
|
|
733
|
-
when :rate_limit
|
|
734
|
-
{
|
|
735
|
-
action: :switch_provider,
|
|
736
|
-
reason: "Rate limit reached, switching provider",
|
|
737
|
-
priority: :high
|
|
738
|
-
}
|
|
739
689
|
else
|
|
740
690
|
{
|
|
741
691
|
action: :switch_provider,
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Harness
|
|
8
|
+
# Manages caching of discovered models with TTL support
|
|
9
|
+
#
|
|
10
|
+
# Cache is stored in ~/.aidp/cache/models.json
|
|
11
|
+
# Each provider's models are cached separately with timestamps
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# cache = ModelCache.new
|
|
15
|
+
# cache.cache_models("anthropic", models_array)
|
|
16
|
+
# cached = cache.get_cached_models("anthropic")
|
|
17
|
+
# cache.invalidate("anthropic")
|
|
18
|
+
class ModelCache
|
|
19
|
+
class CacheError < StandardError; end
|
|
20
|
+
|
|
21
|
+
DEFAULT_TTL = 86400 # 24 hours in seconds
|
|
22
|
+
|
|
23
|
+
attr_reader :cache_file
|
|
24
|
+
|
|
25
|
+
def initialize(cache_file: nil, cache_dir: nil)
|
|
26
|
+
@cache_file = determine_cache_file(cache_file, cache_dir)
|
|
27
|
+
@cache_enabled = ensure_cache_directory
|
|
28
|
+
|
|
29
|
+
if @cache_enabled
|
|
30
|
+
Aidp.log_debug("model_cache", "initialized", cache_file: @cache_file)
|
|
31
|
+
else
|
|
32
|
+
Aidp.log_warn("model_cache", "cache disabled due to permission issues")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get cached models for a provider if not expired
|
|
37
|
+
#
|
|
38
|
+
# @param provider [String] Provider name
|
|
39
|
+
# @return [Array<Hash>, nil] Cached models or nil if expired/not found
|
|
40
|
+
def get_cached_models(provider)
|
|
41
|
+
cache_data = load_cache
|
|
42
|
+
provider_cache = cache_data[provider]
|
|
43
|
+
|
|
44
|
+
return nil unless provider_cache
|
|
45
|
+
|
|
46
|
+
cached_at = begin
|
|
47
|
+
Time.parse(provider_cache["cached_at"])
|
|
48
|
+
rescue
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
return nil unless cached_at
|
|
52
|
+
|
|
53
|
+
ttl = provider_cache["ttl"] || DEFAULT_TTL
|
|
54
|
+
expires_at = cached_at + ttl
|
|
55
|
+
|
|
56
|
+
if Time.now > expires_at
|
|
57
|
+
Aidp.log_debug("model_cache", "cache expired",
|
|
58
|
+
provider: provider, cached_at: cached_at, expires_at: expires_at)
|
|
59
|
+
return nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
models = provider_cache["models"]
|
|
63
|
+
# Convert string keys to symbols for consistency with fresh discovery
|
|
64
|
+
models = models.map { |m| m.transform_keys(&:to_sym) } if models
|
|
65
|
+
Aidp.log_debug("model_cache", "cache hit",
|
|
66
|
+
provider: provider, count: models&.size || 0)
|
|
67
|
+
models
|
|
68
|
+
rescue => e
|
|
69
|
+
Aidp.log_error("model_cache", "failed to read cache",
|
|
70
|
+
provider: provider, error: e.message)
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Cache models for a provider with TTL
|
|
75
|
+
#
|
|
76
|
+
# @param provider [String] Provider name
|
|
77
|
+
# @param models [Array<Hash>] Models to cache
|
|
78
|
+
# @param ttl [Integer] Time to live in seconds (default: 24 hours)
|
|
79
|
+
def cache_models(provider, models, ttl: DEFAULT_TTL)
|
|
80
|
+
unless @cache_enabled
|
|
81
|
+
Aidp.log_debug("model_cache", "caching disabled, skipping",
|
|
82
|
+
provider: provider)
|
|
83
|
+
return false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
cache_data = load_cache
|
|
87
|
+
|
|
88
|
+
cache_data[provider] = {
|
|
89
|
+
"cached_at" => Time.now.iso8601,
|
|
90
|
+
"ttl" => ttl,
|
|
91
|
+
"models" => models
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if save_cache(cache_data)
|
|
95
|
+
Aidp.log_info("model_cache", "cached models",
|
|
96
|
+
provider: provider, count: models.size, ttl: ttl)
|
|
97
|
+
true
|
|
98
|
+
else
|
|
99
|
+
Aidp.log_warn("model_cache", "failed to cache models",
|
|
100
|
+
provider: provider)
|
|
101
|
+
false
|
|
102
|
+
end
|
|
103
|
+
rescue => e
|
|
104
|
+
Aidp.log_error("model_cache", "error caching models",
|
|
105
|
+
provider: provider, error: e.message)
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Invalidate cache for a specific provider
|
|
110
|
+
#
|
|
111
|
+
# @param provider [String] Provider name
|
|
112
|
+
def invalidate(provider)
|
|
113
|
+
return false unless @cache_enabled
|
|
114
|
+
|
|
115
|
+
cache_data = load_cache
|
|
116
|
+
cache_data.delete(provider)
|
|
117
|
+
save_cache(cache_data)
|
|
118
|
+
Aidp.log_info("model_cache", "invalidated cache", provider: provider)
|
|
119
|
+
true
|
|
120
|
+
rescue => e
|
|
121
|
+
Aidp.log_error("model_cache", "failed to invalidate cache",
|
|
122
|
+
provider: provider, error: e.message)
|
|
123
|
+
false
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Invalidate all cached models
|
|
127
|
+
def invalidate_all
|
|
128
|
+
return false unless @cache_enabled
|
|
129
|
+
|
|
130
|
+
save_cache({})
|
|
131
|
+
Aidp.log_info("model_cache", "invalidated all caches")
|
|
132
|
+
true
|
|
133
|
+
rescue => e
|
|
134
|
+
Aidp.log_error("model_cache", "failed to invalidate all",
|
|
135
|
+
error: e.message)
|
|
136
|
+
false
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Get list of providers with cached models
|
|
140
|
+
#
|
|
141
|
+
# @return [Array<String>] Provider names with valid caches
|
|
142
|
+
def cached_providers
|
|
143
|
+
cache_data = load_cache
|
|
144
|
+
providers = []
|
|
145
|
+
|
|
146
|
+
cache_data.each do |provider, data|
|
|
147
|
+
cached_at = begin
|
|
148
|
+
Time.parse(data["cached_at"])
|
|
149
|
+
rescue
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
next unless cached_at
|
|
153
|
+
|
|
154
|
+
ttl = data["ttl"] || DEFAULT_TTL
|
|
155
|
+
expires_at = cached_at + ttl
|
|
156
|
+
|
|
157
|
+
providers << provider if Time.now <= expires_at
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
providers
|
|
161
|
+
rescue => e
|
|
162
|
+
Aidp.log_error("model_cache", "failed to get cached providers",
|
|
163
|
+
error: e.message)
|
|
164
|
+
[]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Get cache statistics
|
|
168
|
+
#
|
|
169
|
+
# @return [Hash] Statistics about the cache
|
|
170
|
+
def stats
|
|
171
|
+
cache_data = load_cache
|
|
172
|
+
file_size = begin
|
|
173
|
+
File.size(@cache_file)
|
|
174
|
+
rescue
|
|
175
|
+
0
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
{
|
|
179
|
+
total_providers: cache_data.size,
|
|
180
|
+
cached_providers: cached_providers,
|
|
181
|
+
cache_file_size: file_size
|
|
182
|
+
}
|
|
183
|
+
rescue => e
|
|
184
|
+
Aidp.log_error("model_cache", "failed to get stats",
|
|
185
|
+
error: e.message)
|
|
186
|
+
{total_providers: 0, cached_providers: [], cache_file_size: 0}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def determine_cache_file(cache_file, cache_dir)
|
|
192
|
+
return cache_file if cache_file
|
|
193
|
+
|
|
194
|
+
if cache_dir
|
|
195
|
+
File.join(cache_dir, "models.json")
|
|
196
|
+
else
|
|
197
|
+
default_cache_file
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def default_cache_file
|
|
202
|
+
File.join(Dir.home, ".aidp", "cache", "models.json")
|
|
203
|
+
rescue => e
|
|
204
|
+
# Fallback to temp directory if home directory is not accessible
|
|
205
|
+
Aidp.log_debug("model_cache", "home directory not accessible, using temp",
|
|
206
|
+
error: e.message)
|
|
207
|
+
File.join(Dir.tmpdir, "aidp_cache", "models.json")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def ensure_cache_directory
|
|
211
|
+
cache_dir = File.dirname(@cache_file)
|
|
212
|
+
return true if File.directory?(cache_dir)
|
|
213
|
+
|
|
214
|
+
FileUtils.mkdir_p(cache_dir)
|
|
215
|
+
true
|
|
216
|
+
rescue Errno::EACCES, Errno::EPERM => e
|
|
217
|
+
Aidp.log_warn("model_cache", "permission denied creating cache directory",
|
|
218
|
+
cache_dir: cache_dir, error: e.message)
|
|
219
|
+
|
|
220
|
+
# Try fallback to temp directory
|
|
221
|
+
@cache_file = File.join(Dir.tmpdir, "aidp_cache", "models.json")
|
|
222
|
+
fallback_dir = File.dirname(@cache_file)
|
|
223
|
+
|
|
224
|
+
begin
|
|
225
|
+
FileUtils.mkdir_p(fallback_dir) unless File.directory?(fallback_dir)
|
|
226
|
+
Aidp.log_info("model_cache", "using fallback cache directory",
|
|
227
|
+
cache_file: @cache_file)
|
|
228
|
+
true
|
|
229
|
+
rescue => fallback_error
|
|
230
|
+
Aidp.log_error("model_cache", "failed to create fallback cache directory",
|
|
231
|
+
error: fallback_error.message)
|
|
232
|
+
false
|
|
233
|
+
end
|
|
234
|
+
rescue => e
|
|
235
|
+
Aidp.log_error("model_cache", "failed to create cache directory",
|
|
236
|
+
cache_dir: cache_dir, error: e.message)
|
|
237
|
+
false
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def load_cache
|
|
241
|
+
return {} unless File.exist?(@cache_file)
|
|
242
|
+
|
|
243
|
+
content = File.read(@cache_file)
|
|
244
|
+
JSON.parse(content)
|
|
245
|
+
rescue JSON::ParserError => e
|
|
246
|
+
Aidp.log_warn("model_cache", "corrupted cache file, resetting",
|
|
247
|
+
error: e.message)
|
|
248
|
+
# Reset corrupted cache
|
|
249
|
+
{}
|
|
250
|
+
rescue => e
|
|
251
|
+
Aidp.log_error("model_cache", "failed to load cache",
|
|
252
|
+
error: e.message)
|
|
253
|
+
{}
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def save_cache(data)
|
|
257
|
+
return false unless @cache_enabled
|
|
258
|
+
|
|
259
|
+
ensure_cache_directory
|
|
260
|
+
File.write(@cache_file, JSON.pretty_generate(data))
|
|
261
|
+
true
|
|
262
|
+
rescue => e
|
|
263
|
+
Aidp.log_error("model_cache", "failed to save cache",
|
|
264
|
+
error: e.message, cache_file: @cache_file)
|
|
265
|
+
false
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|