aidp 0.34.0 → 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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -14
  3. data/lib/aidp/agent_harness_adapter.rb +309 -0
  4. data/lib/aidp/cli/issue_importer.rb +20 -6
  5. data/lib/aidp/cli/jobs_command.rb +1 -1
  6. data/lib/aidp/cli/models_command.rb +9 -16
  7. data/lib/aidp/cli/prompts_command.rb +253 -0
  8. data/lib/aidp/cli/security_command.rb +4 -10
  9. data/lib/aidp/cli/storage_command.rb +277 -0
  10. data/lib/aidp/cli/temporal_command.rb +460 -0
  11. data/lib/aidp/cli.rb +24 -2
  12. data/lib/aidp/config.rb +15 -0
  13. data/lib/aidp/config_paths.rb +12 -2
  14. data/lib/aidp/database/migrations.rb +114 -0
  15. data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
  16. data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
  17. data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
  18. data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
  19. data/lib/aidp/database/repositories/job_repository.rb +278 -0
  20. data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
  21. data/lib/aidp/database/repositories/progress_repository.rb +210 -0
  22. data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
  23. data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
  24. data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
  25. data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
  26. data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
  27. data/lib/aidp/database/repositories/task_repository.rb +231 -0
  28. data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
  29. data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
  30. data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
  31. data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
  32. data/lib/aidp/database/repository.rb +166 -0
  33. data/lib/aidp/database/schema.rb +347 -0
  34. data/lib/aidp/database/storage_migrator.rb +693 -0
  35. data/lib/aidp/database.rb +139 -0
  36. data/lib/aidp/debug_mixin.rb +2 -2
  37. data/lib/aidp/execute/checkpoint.rb +26 -0
  38. data/lib/aidp/execute/checkpoint_display.rb +16 -0
  39. data/lib/aidp/execute/prompt_evaluator.rb +30 -57
  40. data/lib/aidp/execute/work_loop_runner.rb +294 -46
  41. data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
  42. data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
  43. data/lib/aidp/harness/ai_decision_engine.rb +78 -296
  44. data/lib/aidp/harness/config_schema.rb +59 -0
  45. data/lib/aidp/harness/configuration.rb +229 -10
  46. data/lib/aidp/harness/deprecation_cache.rb +53 -11
  47. data/lib/aidp/harness/enhanced_runner.rb +1 -1
  48. data/lib/aidp/harness/error_handler.rb +11 -5
  49. data/lib/aidp/harness/provider_config.rb +36 -0
  50. data/lib/aidp/harness/provider_factory.rb +26 -24
  51. data/lib/aidp/harness/provider_info.rb +9 -5
  52. data/lib/aidp/harness/provider_manager.rb +2 -2
  53. data/lib/aidp/harness/provider_metrics.rb +103 -1
  54. data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
  55. data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
  56. data/lib/aidp/harness/runner.rb +7 -2
  57. data/lib/aidp/harness/state_manager.rb +1 -1
  58. data/lib/aidp/harness/test_runner.rb +167 -11
  59. data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
  60. data/lib/aidp/harness/ui.rb +11 -0
  61. data/lib/aidp/harness/usage_limit.rb +179 -0
  62. data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
  63. data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
  64. data/lib/aidp/harness/usage_period.rb +234 -0
  65. data/lib/aidp/init/agent_instructions_generator.rb +317 -0
  66. data/lib/aidp/init/runner.rb +25 -4
  67. data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
  68. data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
  69. data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
  70. data/lib/aidp/interfaces/logger_interface.rb +150 -0
  71. data/lib/aidp/interfaces/ui_interface.rb +254 -0
  72. data/lib/aidp/logger.rb +25 -7
  73. data/lib/aidp/orchestration_adapter.rb +279 -0
  74. data/lib/aidp/pr_worktree_manager.rb +4 -15
  75. data/lib/aidp/prompts/feedback_collector.rb +198 -0
  76. data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
  77. data/lib/aidp/prompts/template_evolver.rb +305 -0
  78. data/lib/aidp/prompts/template_version_manager.rb +325 -0
  79. data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
  80. data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
  81. data/lib/aidp/setup/wizard.rb +616 -176
  82. data/lib/aidp/shell_executor.rb +58 -0
  83. data/lib/aidp/style_guide/selector.rb +56 -1
  84. data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
  85. data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
  86. data/lib/aidp/temporal/activities/base_activity.rb +78 -0
  87. data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
  88. data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
  89. data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
  90. data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
  91. data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
  92. data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
  93. data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
  94. data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
  95. data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
  96. data/lib/aidp/temporal/configuration.rb +169 -0
  97. data/lib/aidp/temporal/connection.rb +93 -0
  98. data/lib/aidp/temporal/worker.rb +119 -0
  99. data/lib/aidp/temporal/workflow_client.rb +171 -0
  100. data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
  101. data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
  102. data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
  103. data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
  104. data/lib/aidp/temporal.rb +120 -0
  105. data/lib/aidp/version.rb +1 -1
  106. data/lib/aidp/watch/base_processor.rb +35 -0
  107. data/lib/aidp/watch/build_processor.rb +132 -0
  108. data/lib/aidp/watch/github_state_extractor.rb +0 -8
  109. data/lib/aidp/watch/rebase_processor.rb +245 -0
  110. data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
  111. data/lib/aidp/watch/runner.rb +366 -285
  112. data/lib/aidp/watch/state_store.rb +80 -0
  113. data/lib/aidp/watch/work_item.rb +96 -0
  114. data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
  115. data/lib/aidp/watch/worktree_reconciler.rb +643 -0
  116. data/lib/aidp/workflows/guided_agent.rb +1 -1
  117. data/lib/aidp/worktree.rb +62 -0
  118. data/templates/aidp.yml.example +70 -0
  119. metadata +118 -16
  120. data/lib/aidp/analyze/json_file_storage.rb +0 -292
  121. data/lib/aidp/execute/future_work_backlog.rb +0 -411
  122. data/lib/aidp/providers/adapter.rb +0 -239
  123. data/lib/aidp/providers/aider.rb +0 -264
  124. data/lib/aidp/providers/anthropic.rb +0 -614
  125. data/lib/aidp/providers/base.rb +0 -617
  126. data/lib/aidp/providers/capability_registry.rb +0 -204
  127. data/lib/aidp/providers/codex.rb +0 -267
  128. data/lib/aidp/providers/cursor.rb +0 -304
  129. data/lib/aidp/providers/error_taxonomy.rb +0 -195
  130. data/lib/aidp/providers/gemini.rb +0 -110
  131. data/lib/aidp/providers/github_copilot.rb +0 -227
  132. data/lib/aidp/providers/kilocode.rb +0 -157
  133. data/lib/aidp/providers/opencode.rb +0 -139
@@ -6,14 +6,36 @@ require_relative "../message_display"
6
6
 
7
7
  module Aidp
8
8
  module Harness
9
+ # Custom exception for model availability issues
10
+ class NoModelAvailableError < StandardError
11
+ attr_reader :tier, :provider
12
+
13
+ def initialize(tier:, provider:)
14
+ @tier = tier
15
+ @provider = provider
16
+ super("No model available for tier '#{tier}' with provider '#{provider}'. " \
17
+ "Check your aidp.yml configuration or run 'aidp models discover' to find available models.")
18
+ end
19
+ end
20
+
9
21
  # Manages thinking depth tier selection and escalation
10
22
  # Integrates with CapabilityRegistry and Configuration to select appropriate models
11
23
  class ThinkingDepthManager
12
24
  include Aidp::MessageDisplay
13
25
 
26
+ # Configuration constants for tier management
27
+ MAX_TIER_HISTORY_SIZE = 100
28
+ MAX_COMMENT_LENGTH = 2000
29
+ MAX_REASONING_DISPLAY_LENGTH = 100
30
+ DEFAULT_CONFIDENCE = 0.7
31
+
14
32
  attr_reader :configuration, :registry
15
33
 
16
- def initialize(configuration, registry: nil, root_dir: nil)
34
+ # Issue #375: Model attempt tracking for intelligent escalation
35
+ # Structure: { tier => { provider => { model => { attempts: n, failed: bool } } } }
36
+ attr_reader :model_attempts
37
+
38
+ def initialize(configuration, registry: nil, root_dir: nil, autonomous_mode: false)
17
39
  @configuration = configuration
18
40
  @registry = registry || CapabilityRegistry.new(root_dir: root_dir || configuration.project_dir)
19
41
  @current_tier = nil
@@ -21,9 +43,64 @@ module Aidp
21
43
  @tier_history = []
22
44
  @escalation_count = 0
23
45
 
46
+ # Issue #375: Track model attempts for intelligent escalation
47
+ @model_attempts = {}
48
+ @total_attempts_in_tier = 0
49
+ @autonomous_mode = autonomous_mode
50
+ @model_denylist = [] # Models to skip (e.g., denylisted by user)
51
+
24
52
  Aidp.log_debug("thinking_depth_manager", "Initialized",
25
53
  default_tier: default_tier,
26
- max_tier: max_tier)
54
+ max_tier: max_tier,
55
+ autonomous_max_tier: autonomous_max_tier,
56
+ autonomous_mode: autonomous_mode)
57
+ end
58
+
59
+ # Enable autonomous mode (restricts max tier, enables model-level tracking)
60
+ # Should be called when entering watch mode or work loops
61
+ def enable_autonomous_mode
62
+ @autonomous_mode = true
63
+ reset_model_tracking
64
+
65
+ # Cap current tier at autonomous max if needed
66
+ if @registry.compare_tiers(current_tier, autonomous_max_tier) > 0
67
+ old_tier = current_tier
68
+ @current_tier = autonomous_max_tier
69
+ log_tier_change(old_tier, @current_tier, "autonomous_mode_cap")
70
+
71
+ Aidp.log_info("thinking_depth_manager", "Tier capped for autonomous mode",
72
+ old: old_tier,
73
+ new: @current_tier,
74
+ autonomous_max: autonomous_max_tier)
75
+ end
76
+
77
+ Aidp.log_debug("thinking_depth_manager", "Autonomous mode enabled",
78
+ max_tier: autonomous_max_tier)
79
+ end
80
+
81
+ # Disable autonomous mode (restores normal max tier)
82
+ def disable_autonomous_mode
83
+ @autonomous_mode = false
84
+ Aidp.log_debug("thinking_depth_manager", "Autonomous mode disabled")
85
+ end
86
+
87
+ # Check if in autonomous mode
88
+ def autonomous_mode?
89
+ @autonomous_mode
90
+ end
91
+
92
+ # Get maximum tier for autonomous operations (issue #375)
93
+ def autonomous_max_tier
94
+ @session_autonomous_max_tier || configuration.autonomous_max_tier
95
+ end
96
+
97
+ # Set autonomous max tier for this session
98
+ def autonomous_max_tier=(tier)
99
+ validate_tier!(tier)
100
+ @session_autonomous_max_tier = tier
101
+
102
+ Aidp.log_info("thinking_depth_manager", "Autonomous max tier updated",
103
+ new: tier)
27
104
  end
28
105
 
29
106
  # Get current tier (defaults to config default_tier if not set)
@@ -49,11 +126,33 @@ module Aidp
49
126
  end
50
127
  end
51
128
 
52
- # Get maximum allowed tier (respects session override)
53
- def max_tier
129
+ # Get the base maximum tier (from session override or config, ignoring autonomous mode)
130
+ # This is the "raw" max tier before autonomous mode restrictions are applied
131
+ def base_max_tier
54
132
  @session_max_tier || configuration.max_tier
55
133
  end
56
134
 
135
+ # Get effective maximum tier (applies autonomous mode restrictions)
136
+ # Issue #375: In autonomous mode, respects autonomous_max_tier
137
+ def max_tier
138
+ apply_autonomous_tier_cap(base_max_tier)
139
+ end
140
+
141
+ # Apply autonomous mode tier cap if active
142
+ # Returns the tier capped at autonomous_max_tier when in autonomous mode
143
+ # @param tier [String] The tier to potentially cap
144
+ # @return [String] The effective tier (capped if in autonomous mode)
145
+ def apply_autonomous_tier_cap(tier)
146
+ return tier unless @autonomous_mode
147
+
148
+ auto_max = autonomous_max_tier
149
+ if @registry.compare_tiers(auto_max, tier) < 0
150
+ auto_max
151
+ else
152
+ tier
153
+ end
154
+ end
155
+
57
156
  # Set maximum tier for this session (temporary override)
58
157
  def max_tier=(tier)
59
158
  validate_tier!(tier)
@@ -147,6 +246,322 @@ module Aidp
147
246
  prev_tier
148
247
  end
149
248
 
249
+ # ============================================================
250
+ # Issue #375: Intelligent model-level escalation for autonomous mode
251
+ # ============================================================
252
+
253
+ # Record an attempt with a specific model
254
+ # @param provider [String] Provider name
255
+ # @param model [String] Model name
256
+ # @param success [Boolean] Whether the attempt succeeded
257
+ def record_model_attempt(provider:, model:, success:)
258
+ tier = current_tier
259
+ @model_attempts[tier] ||= {}
260
+ @model_attempts[tier][provider] ||= {}
261
+ @model_attempts[tier][provider][model] ||= {attempts: 0, failed: false, last_attempt_at: nil}
262
+
263
+ @model_attempts[tier][provider][model][:attempts] += 1
264
+ @model_attempts[tier][provider][model][:last_attempt_at] = Time.now
265
+ @model_attempts[tier][provider][model][:failed] = !success
266
+
267
+ @total_attempts_in_tier += 1
268
+
269
+ Aidp.log_debug("thinking_depth_manager", "Recorded model attempt",
270
+ tier: tier,
271
+ provider: provider,
272
+ model: model,
273
+ success: success,
274
+ total_attempts: @model_attempts[tier][provider][model][:attempts],
275
+ tier_total: @total_attempts_in_tier)
276
+ end
277
+
278
+ # Get attempts for a specific model
279
+ def model_attempt_count(provider:, model:)
280
+ tier = current_tier
281
+ @model_attempts.dig(tier, provider, model, :attempts) || 0
282
+ end
283
+
284
+ # Check if a model has been marked as failed
285
+ def model_failed?(provider:, model:)
286
+ tier = current_tier
287
+ @model_attempts.dig(tier, provider, model, :failed) || false
288
+ end
289
+
290
+ # Add model to denylist
291
+ def denylist_model(model)
292
+ @model_denylist << model unless @model_denylist.include?(model)
293
+ Aidp.log_debug("thinking_depth_manager", "Model denylisted", model: model)
294
+ end
295
+
296
+ # Check if model is denylisted
297
+ def model_denylisted?(model)
298
+ @model_denylist.include?(model)
299
+ end
300
+
301
+ # Get all available models for current tier and provider
302
+ # @param provider [String] Provider name
303
+ # @return [Array<String>] List of model names
304
+ def available_models_for_tier(provider:)
305
+ tier = current_tier
306
+
307
+ # First try user-configured models
308
+ configured = configuration.models_for_tier(tier, provider)
309
+
310
+ if configured.any?
311
+ # Filter out denylisted models
312
+ return configured.reject { |m| model_denylisted?(m) }
313
+ end
314
+
315
+ # Fall back to catalog models
316
+ model_name, _data = @registry.best_model_for_tier(tier, provider)
317
+ return [] unless model_name
318
+
319
+ [model_name].reject { |m| model_denylisted?(m) }
320
+ end
321
+
322
+ # Check if any models are configured for the current tier and provider
323
+ # @param provider [String] Provider name
324
+ # @return [Boolean] true if models are available, false if none configured
325
+ def models_configured_for_tier?(provider:)
326
+ available_models_for_tier(provider: provider).any?
327
+ end
328
+
329
+ # Select the next model to try in current tier
330
+ # Issue #375: Tries all models before escalating, respects min attempts per model
331
+ # @param provider [String] Provider name
332
+ # @return [String, nil] Model name or nil if should escalate
333
+ def select_next_model(provider:)
334
+ tier = current_tier
335
+ models = available_models_for_tier(provider: provider)
336
+
337
+ if models.empty?
338
+ Aidp.log_debug("thinking_depth_manager", "No models configured for tier",
339
+ tier: tier,
340
+ provider: provider,
341
+ reason: "no_models_configured")
342
+ return nil
343
+ end
344
+
345
+ min_attempts = configuration.min_attempts_per_model
346
+
347
+ # First pass: find any model with under-min-attempts (must reach min before retry)
348
+ # This ensures every model gets minimum attempts before we consider retrying
349
+ models.each do |model|
350
+ attempts = model_attempt_count(provider: provider, model: model)
351
+ if attempts < min_attempts
352
+ Aidp.log_debug("thinking_depth_manager", "Selected under-min-attempts model",
353
+ model: model,
354
+ attempts: attempts,
355
+ min_required: min_attempts)
356
+ return model
357
+ end
358
+ end
359
+
360
+ # Second pass: retry models that have met min attempts (if retry enabled)
361
+ # Prioritize non-failed models first, then retry failed models
362
+ if configuration.retry_failed_models?
363
+ # First try non-failed models that have met min attempts
364
+ models.each do |model|
365
+ attempts = model_attempt_count(provider: provider, model: model)
366
+ if attempts >= min_attempts && !model_failed?(provider: provider, model: model)
367
+ Aidp.log_debug("thinking_depth_manager", "Selected non-failed model for retry",
368
+ model: model,
369
+ attempts: attempts)
370
+ return model
371
+ end
372
+ end
373
+
374
+ # Then retry previously failed models that have met min attempts
375
+ models.each do |model|
376
+ attempts = model_attempt_count(provider: provider, model: model)
377
+ if attempts >= min_attempts && model_failed?(provider: provider, model: model)
378
+ Aidp.log_debug("thinking_depth_manager", "Retrying previously failed model",
379
+ model: model,
380
+ attempts: attempts)
381
+ return model
382
+ end
383
+ end
384
+ end
385
+
386
+ # All models exhausted in this tier
387
+ Aidp.log_debug("thinking_depth_manager", "All models exhausted in tier",
388
+ tier: tier,
389
+ models_tried: models.size)
390
+ nil
391
+ end
392
+
393
+ # Check if we should escalate tier based on model exhaustion
394
+ # Issue #375: Requires minimum total attempts and trying all models first
395
+ # @param provider [String] Provider name
396
+ # @return [Hash] {should_escalate: bool, reason: string}
397
+ def should_escalate_tier?(provider:)
398
+ return {should_escalate: false, reason: "not_autonomous"} unless @autonomous_mode
399
+
400
+ min_total = configuration.min_total_attempts_before_escalation
401
+ min_per_model = configuration.min_attempts_per_model
402
+ models = available_models_for_tier(provider: provider)
403
+
404
+ # Check if we have any models below minimum attempts threshold
405
+ models_below_min = models.select do |model|
406
+ model_attempt_count(provider: provider, model: model) < min_per_model
407
+ end
408
+
409
+ if models_below_min.any?
410
+ return {
411
+ should_escalate: false,
412
+ reason: "models_below_min_attempts",
413
+ remaining_count: models_below_min.size
414
+ }
415
+ end
416
+
417
+ # Check minimum total attempts
418
+ # Relax if tier lacks sufficient models (each model needs min 2 tries)
419
+ effective_min_total = [min_total, models.size * min_per_model].min
420
+
421
+ if @total_attempts_in_tier < effective_min_total
422
+ return {
423
+ should_escalate: false,
424
+ reason: "below_min_attempts",
425
+ current: @total_attempts_in_tier,
426
+ required: effective_min_total
427
+ }
428
+ end
429
+
430
+ # Check if all models have failed
431
+ # Only escalate if ALL models have failed - don't escalate just because min attempts reached
432
+ # if some models are still succeeding
433
+ all_failed = models.all? { |m| model_failed?(provider: provider, model: m) }
434
+
435
+ if all_failed
436
+ return {
437
+ should_escalate: true,
438
+ reason: "all_models_failed",
439
+ total_attempts: @total_attempts_in_tier
440
+ }
441
+ end
442
+
443
+ {should_escalate: false, reason: "continue_current_tier"}
444
+ end
445
+
446
+ # Escalate tier with intelligent model tracking (issue #375)
447
+ # Only escalates if all models in current tier have been tried
448
+ # @param provider [String] Provider name
449
+ # @param reason [String, nil] Reason for escalation
450
+ # @return [String, nil] New tier or nil if cannot escalate
451
+ def escalate_tier_intelligent(provider:, reason: nil)
452
+ escalation_check = should_escalate_tier?(provider: provider)
453
+
454
+ unless escalation_check[:should_escalate]
455
+ Aidp.log_debug("thinking_depth_manager", "Intelligent escalation blocked",
456
+ reason: escalation_check[:reason],
457
+ details: escalation_check)
458
+ return nil
459
+ end
460
+
461
+ # Reset model tracking for new tier
462
+ old_tier = current_tier
463
+ new_tier = escalate_tier(reason: reason || escalation_check[:reason])
464
+
465
+ if new_tier
466
+ attempts_before_reset = @total_attempts_in_tier
467
+ reset_model_tracking
468
+ Aidp.log_info("thinking_depth_manager", "Intelligent tier escalation",
469
+ from: old_tier,
470
+ to: new_tier,
471
+ reason: reason || escalation_check[:reason],
472
+ total_attempts_in_old_tier: attempts_before_reset)
473
+ end
474
+
475
+ new_tier
476
+ end
477
+
478
+ # Reset model tracking (call when changing tiers or starting new work)
479
+ # Clears current tier's data for consistency; preserves other tiers' history for analysis
480
+ def reset_model_tracking
481
+ tier = current_tier
482
+ # Ensure hash exists then clear it for consistency with counter reset
483
+ @model_attempts[tier] ||= {}
484
+ @model_attempts[tier].clear
485
+ @total_attempts_in_tier = 0
486
+
487
+ Aidp.log_debug("thinking_depth_manager", "Model tracking reset for tier", tier: tier)
488
+ end
489
+
490
+ # Get summary of model attempts in current tier
491
+ def model_attempts_summary
492
+ tier = current_tier
493
+ tier_attempts = @model_attempts[tier] || {}
494
+
495
+ summary = {
496
+ tier: tier,
497
+ total_attempts: @total_attempts_in_tier,
498
+ providers: {}
499
+ }
500
+
501
+ tier_attempts.each do |provider, models|
502
+ summary[:providers][provider] = models.map do |model, data|
503
+ {
504
+ model: model,
505
+ attempts: data[:attempts],
506
+ failed: data[:failed]
507
+ }
508
+ end
509
+ end
510
+
511
+ summary
512
+ end
513
+
514
+ # ============================================================
515
+ # Issue #375: ZFC-based tier determination from issue comments
516
+ # ============================================================
517
+
518
+ # Determine appropriate tier from issue/PR comment content using ZFC
519
+ # @param comment_text [String] The issue or PR comment text
520
+ # @param provider_manager [ProviderManager] Provider manager for AI calls
521
+ # @param labels [Array<String>] Optional labels on the issue/PR
522
+ # @return [Hash] {tier: String, confidence: Float, reasoning: String}
523
+ def determine_tier_from_comment(comment_text:, provider_manager:, labels: [])
524
+ # Check for explicit tier labels first (fast path)
525
+ tier_from_labels = extract_tier_from_labels(labels)
526
+ if tier_from_labels
527
+ tier = tier_from_labels
528
+ reasoning = "Explicit tier label found: #{tier_from_labels}"
529
+
530
+ # In autonomous mode, cap at autonomous_max_tier (same as ZFC path)
531
+ if @autonomous_mode && @registry.compare_tiers(tier, autonomous_max_tier) > 0
532
+ original_tier = tier
533
+ tier = autonomous_max_tier
534
+ reasoning += " (capped from #{original_tier} due to autonomous mode)"
535
+
536
+ Aidp.log_debug("thinking_depth_manager", "Label tier capped for autonomous mode",
537
+ original: original_tier,
538
+ capped_to: tier)
539
+ end
540
+
541
+ return {
542
+ tier: tier,
543
+ confidence: 1.0,
544
+ reasoning: reasoning,
545
+ source: "label"
546
+ }
547
+ end
548
+
549
+ # Use ZFC to determine tier from comment content
550
+ determine_tier_via_zfc(comment_text, provider_manager)
551
+ rescue => e
552
+ Aidp.log_warn("thinking_depth_manager", "ZFC tier determination failed, using default",
553
+ error: e.message,
554
+ error_class: e.class.name)
555
+
556
+ # Return conservative default on error
557
+ {
558
+ tier: "mini",
559
+ confidence: 0.5,
560
+ reasoning: "ZFC determination failed, using conservative default",
561
+ source: "fallback"
562
+ }
563
+ end
564
+
150
565
  # Select best model for current tier and provider
151
566
  # Returns [provider_name, model_name, model_data] or nil
152
567
  def select_model_for_tier(tier = nil, provider: nil)
@@ -432,7 +847,143 @@ module Aidp
432
847
  @tier_history << entry
433
848
 
434
849
  # Keep history bounded
435
- @tier_history.shift if @tier_history.size > 100
850
+ @tier_history.shift if @tier_history.size > MAX_TIER_HISTORY_SIZE
851
+ end
852
+
853
+ # ============================================================
854
+ # Issue #375: ZFC helper methods for tier determination
855
+ # ============================================================
856
+
857
+ # Extract tier from issue labels (fast path, no AI needed)
858
+ # @param labels [Array<String>] Issue labels
859
+ # @return [String, nil] Tier name or nil if no tier label found
860
+ def extract_tier_from_labels(labels)
861
+ return nil unless labels.is_a?(Array)
862
+
863
+ tier_label_patterns = {
864
+ /\btier[:\-_]?mini\b/i => "mini",
865
+ /\btier[:\-_]?standard\b/i => "standard",
866
+ /\btier[:\-_]?thinking\b/i => "thinking",
867
+ /\btier[:\-_]?pro\b/i => "pro",
868
+ /\btier[:\-_]?max\b/i => "max",
869
+ /\bcomplexity[:\-_]?low\b/i => "mini",
870
+ /\bcomplexity[:\-_]?medium\b/i => "standard",
871
+ /\bcomplexity[:\-_]?high\b/i => "pro"
872
+ }
873
+
874
+ labels.each do |label|
875
+ tier_label_patterns.each do |pattern, tier|
876
+ return tier if label.match?(pattern)
877
+ end
878
+ end
879
+
880
+ nil
881
+ end
882
+
883
+ # Use Zero Framework Cognition to determine tier from comment content
884
+ # @param comment_text [String] The comment text to analyze
885
+ # @param provider_manager [ProviderManager] Provider manager for AI calls
886
+ # @return [Hash] Tier determination result
887
+ def determine_tier_via_zfc(comment_text, provider_manager)
888
+ prompt = build_tier_determination_prompt(comment_text)
889
+
890
+ # Use mini tier for the ZFC decision itself (cost efficiency)
891
+ provider_name, model_name, _data = select_model_for_tier("mini", provider: configuration.default_provider)
892
+
893
+ # Handle case where no model is available for tier determination
894
+ if provider_name.nil?
895
+ Aidp.log_warn("thinking_depth_manager", "No model available for ZFC tier determination",
896
+ tier: "mini",
897
+ provider: configuration.default_provider)
898
+ raise NoModelAvailableError.new(tier: "mini", provider: configuration.default_provider)
899
+ end
900
+
901
+ result = provider_manager.execute_with_provider(
902
+ provider_name,
903
+ prompt,
904
+ {
905
+ model: model_name,
906
+ mode: :tier_determination,
907
+ max_tokens: 500 # Keep response short
908
+ }
909
+ )
910
+
911
+ parse_tier_determination_response(result[:output])
912
+ end
913
+
914
+ # Build prompt for ZFC tier determination
915
+ def build_tier_determination_prompt(comment_text)
916
+ max_length = MAX_COMMENT_LENGTH
917
+ truncated = comment_text && comment_text.length > max_length
918
+ truncation_note = truncated ? "\n\n[Note: Comment was truncated from #{comment_text.length} to #{max_length} characters]" : ""
919
+
920
+ <<~PROMPT
921
+ Analyze the following issue/PR comment and determine the appropriate thinking tier for an AI agent to address it.
922
+
923
+ ## Available Tiers (lowest to highest capability/cost):
924
+ - **mini**: Simple fixes, typos, minor changes, documentation updates
925
+ - **standard**: Normal features, bug fixes, moderate complexity changes
926
+ - **thinking**: Complex problems requiring extended reasoning, multi-step solutions
927
+ - **pro**: Highly complex tasks, architectural decisions, security-sensitive work
928
+ - **max**: Extreme complexity, requires maximum reasoning capability (rarely needed)
929
+
930
+ ## Comment to Analyze:
931
+ ```
932
+ #{truncate_string(comment_text, max_length)}
933
+ ```#{truncation_note}
934
+
935
+ ## Your Task:
936
+ Determine which tier is most appropriate for handling this work.
937
+ Consider:
938
+ 1. Task complexity (simple fix vs architectural change)
939
+ 2. Reasoning depth required
940
+ 3. Risk level (security, data integrity)
941
+ 4. Domain expertise needed
942
+
943
+ Respond in this exact format:
944
+ TIER: <mini|standard|thinking|pro|max>
945
+ CONFIDENCE: <0.0-1.0>
946
+ REASONING: <brief explanation>
947
+ PROMPT
948
+ end
949
+
950
+ # Parse the ZFC response for tier determination
951
+ def parse_tier_determination_response(response)
952
+ tier_match = response.match(/TIER:\s*(\w+)/i)
953
+ confidence_match = response.match(/CONFIDENCE:\s*([-\d.]+)/i)
954
+ reasoning_match = response.match(/REASONING:\s*(.+)/mi)
955
+
956
+ tier = tier_match&.[](1)&.downcase || "standard"
957
+ raw_confidence = confidence_match&.[](1)&.to_f || DEFAULT_CONFIDENCE
958
+ # Clamp confidence to valid 0.0-1.0 range
959
+ confidence = raw_confidence.clamp(0.0, 1.0)
960
+ reasoning = reasoning_match&.[](1)&.strip || "No reasoning provided"
961
+
962
+ # Validate tier is in allowed list
963
+ tier = "standard" unless CapabilityRegistry::VALID_TIERS.include?(tier)
964
+
965
+ # In autonomous mode, cap at autonomous_max_tier
966
+ if @autonomous_mode && @registry.compare_tiers(tier, autonomous_max_tier) > 0
967
+ original_tier = tier
968
+ tier = autonomous_max_tier
969
+ reasoning += " (capped from #{original_tier} due to autonomous mode)"
970
+
971
+ Aidp.log_debug("thinking_depth_manager", "ZFC tier capped for autonomous mode",
972
+ original: original_tier,
973
+ capped_to: tier)
974
+ end
975
+
976
+ Aidp.log_info("thinking_depth_manager", "ZFC tier determination",
977
+ tier: tier,
978
+ confidence: confidence,
979
+ reasoning: truncate_string(reasoning, MAX_REASONING_DISPLAY_LENGTH))
980
+
981
+ {
982
+ tier: tier,
983
+ confidence: confidence,
984
+ reasoning: reasoning,
985
+ source: "zfc"
986
+ }
436
987
  end
437
988
 
438
989
  # Try to find a model in fallback tiers when requested tier has no models
@@ -583,6 +1134,14 @@ module Aidp
583
1134
  display_message(" 2. Run 'aidp models list --tier=#{tier}' to see models for this tier", type: :info)
584
1135
  display_message(" 3. Run 'aidp models validate' to check your configuration\n", type: :info)
585
1136
  end
1137
+
1138
+ # Truncate string to max_length (plain Ruby replacement for ActiveSupport truncate)
1139
+ def truncate_string(string, max_length)
1140
+ return "" if string.nil?
1141
+ return string if string.length <= max_length
1142
+
1143
+ "#{string[0, max_length - 3]}..."
1144
+ end
586
1145
  end
587
1146
  end
588
1147
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ui/spinner_helper"
4
+
5
+ module Aidp
6
+ module Harness
7
+ module UI
8
+ # This module provides global UI helpers (see spinner_helper.rb)
9
+ end
10
+ end
11
+ end