aidp 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -214
  3. data/bin/aidp +1 -1
  4. data/lib/aidp/analysis/kb_inspector.rb +38 -23
  5. data/lib/aidp/analysis/seams.rb +2 -31
  6. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +0 -13
  7. data/lib/aidp/analysis/tree_sitter_scan.rb +3 -20
  8. data/lib/aidp/analyze/error_handler.rb +2 -75
  9. data/lib/aidp/analyze/json_file_storage.rb +292 -0
  10. data/lib/aidp/analyze/progress.rb +12 -0
  11. data/lib/aidp/analyze/progress_visualizer.rb +12 -17
  12. data/lib/aidp/analyze/ruby_maat_integration.rb +13 -31
  13. data/lib/aidp/analyze/runner.rb +256 -87
  14. data/lib/aidp/cli/jobs_command.rb +100 -432
  15. data/lib/aidp/cli.rb +309 -239
  16. data/lib/aidp/config.rb +298 -10
  17. data/lib/aidp/debug_logger.rb +195 -0
  18. data/lib/aidp/debug_mixin.rb +187 -0
  19. data/lib/aidp/execute/progress.rb +9 -0
  20. data/lib/aidp/execute/runner.rb +221 -40
  21. data/lib/aidp/execute/steps.rb +17 -7
  22. data/lib/aidp/execute/workflow_selector.rb +211 -0
  23. data/lib/aidp/harness/completion_checker.rb +268 -0
  24. data/lib/aidp/harness/condition_detector.rb +1526 -0
  25. data/lib/aidp/harness/config_loader.rb +373 -0
  26. data/lib/aidp/harness/config_manager.rb +382 -0
  27. data/lib/aidp/harness/config_schema.rb +1006 -0
  28. data/lib/aidp/harness/config_validator.rb +355 -0
  29. data/lib/aidp/harness/configuration.rb +477 -0
  30. data/lib/aidp/harness/enhanced_runner.rb +494 -0
  31. data/lib/aidp/harness/error_handler.rb +616 -0
  32. data/lib/aidp/harness/provider_config.rb +423 -0
  33. data/lib/aidp/harness/provider_factory.rb +306 -0
  34. data/lib/aidp/harness/provider_manager.rb +1269 -0
  35. data/lib/aidp/harness/provider_type_checker.rb +88 -0
  36. data/lib/aidp/harness/runner.rb +411 -0
  37. data/lib/aidp/harness/state/errors.rb +28 -0
  38. data/lib/aidp/harness/state/metrics.rb +219 -0
  39. data/lib/aidp/harness/state/persistence.rb +128 -0
  40. data/lib/aidp/harness/state/provider_state.rb +132 -0
  41. data/lib/aidp/harness/state/ui_state.rb +68 -0
  42. data/lib/aidp/harness/state/workflow_state.rb +123 -0
  43. data/lib/aidp/harness/state_manager.rb +586 -0
  44. data/lib/aidp/harness/status_display.rb +888 -0
  45. data/lib/aidp/harness/ui/base.rb +16 -0
  46. data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
  47. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
  48. data/lib/aidp/harness/ui/error_handler.rb +132 -0
  49. data/lib/aidp/harness/ui/frame_manager.rb +361 -0
  50. data/lib/aidp/harness/ui/job_monitor.rb +500 -0
  51. data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
  52. data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
  53. data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
  54. data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
  55. data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
  56. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
  57. data/lib/aidp/harness/ui/progress_display.rb +280 -0
  58. data/lib/aidp/harness/ui/question_collector.rb +141 -0
  59. data/lib/aidp/harness/ui/spinner_group.rb +184 -0
  60. data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
  61. data/lib/aidp/harness/ui/status_manager.rb +312 -0
  62. data/lib/aidp/harness/ui/status_widget.rb +280 -0
  63. data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
  64. data/lib/aidp/harness/user_interface.rb +2381 -0
  65. data/lib/aidp/provider_manager.rb +131 -7
  66. data/lib/aidp/providers/anthropic.rb +28 -103
  67. data/lib/aidp/providers/base.rb +170 -0
  68. data/lib/aidp/providers/cursor.rb +52 -181
  69. data/lib/aidp/providers/gemini.rb +24 -107
  70. data/lib/aidp/providers/macos_ui.rb +99 -5
  71. data/lib/aidp/providers/opencode.rb +194 -0
  72. data/lib/aidp/storage/csv_storage.rb +172 -0
  73. data/lib/aidp/storage/file_manager.rb +214 -0
  74. data/lib/aidp/storage/json_storage.rb +140 -0
  75. data/lib/aidp/version.rb +1 -1
  76. data/lib/aidp.rb +54 -39
  77. data/templates/COMMON/AGENT_BASE.md +11 -0
  78. data/templates/EXECUTE/00_PRD.md +4 -4
  79. data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
  80. data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
  81. data/templates/EXECUTE/08_TASKS.md +4 -4
  82. data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
  83. data/templates/README.md +279 -0
  84. data/templates/aidp-development.yml.example +373 -0
  85. data/templates/aidp-minimal.yml.example +48 -0
  86. data/templates/aidp-production.yml.example +475 -0
  87. data/templates/aidp.yml.example +598 -0
  88. metadata +93 -69
  89. data/lib/aidp/analyze/agent_personas.rb +0 -71
  90. data/lib/aidp/analyze/agent_tool_executor.rb +0 -439
  91. data/lib/aidp/analyze/data_retention_manager.rb +0 -421
  92. data/lib/aidp/analyze/database.rb +0 -260
  93. data/lib/aidp/analyze/dependencies.rb +0 -335
  94. data/lib/aidp/analyze/export_manager.rb +0 -418
  95. data/lib/aidp/analyze/focus_guidance.rb +0 -517
  96. data/lib/aidp/analyze/incremental_analyzer.rb +0 -533
  97. data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
  98. data/lib/aidp/analyze/large_analysis_progress.rb +0 -499
  99. data/lib/aidp/analyze/memory_manager.rb +0 -339
  100. data/lib/aidp/analyze/metrics_storage.rb +0 -336
  101. data/lib/aidp/analyze/parallel_processor.rb +0 -454
  102. data/lib/aidp/analyze/performance_optimizer.rb +0 -691
  103. data/lib/aidp/analyze/repository_chunker.rb +0 -697
  104. data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
  105. data/lib/aidp/analyze/storage.rb +0 -655
  106. data/lib/aidp/analyze/tool_configuration.rb +0 -441
  107. data/lib/aidp/analyze/tool_modernization.rb +0 -750
  108. data/lib/aidp/database/pg_adapter.rb +0 -148
  109. data/lib/aidp/database_config.rb +0 -69
  110. data/lib/aidp/database_connection.rb +0 -72
  111. data/lib/aidp/job_manager.rb +0 -41
  112. data/lib/aidp/jobs/base_job.rb +0 -45
  113. data/lib/aidp/jobs/provider_execution_job.rb +0 -83
  114. data/lib/aidp/project_detector.rb +0 -117
  115. data/lib/aidp/providers/agent_supervisor.rb +0 -348
  116. data/lib/aidp/providers/supervised_base.rb +0 -317
  117. data/lib/aidp/providers/supervised_cursor.rb +0 -22
  118. data/lib/aidp/sync.rb +0 -13
  119. data/lib/aidp/workspace.rb +0 -19
@@ -0,0 +1,1006 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Aidp
6
+ module Harness
7
+ # Configuration schema and validation for harness
8
+ class ConfigSchema
9
+ # Define the complete configuration schema
10
+ SCHEMA = {
11
+ harness: {
12
+ type: :hash,
13
+ required: true,
14
+ properties: {
15
+ max_retries: {
16
+ type: :integer,
17
+ required: false,
18
+ default: 2,
19
+ min: 0,
20
+ max: 10
21
+ },
22
+ default_provider: {
23
+ type: :string,
24
+ required: true,
25
+ pattern: /^[a-zA-Z0-9_-]+$/
26
+ },
27
+ fallback_providers: {
28
+ type: :array,
29
+ required: false,
30
+ default: [],
31
+ items: {
32
+ type: :string,
33
+ pattern: /^[a-zA-Z0-9_-]+$/
34
+ }
35
+ },
36
+ restrict_to_non_byok: {
37
+ type: :boolean,
38
+ required: false,
39
+ default: false
40
+ },
41
+ provider_weights: {
42
+ type: :hash,
43
+ required: false,
44
+ default: {},
45
+ pattern_properties: {
46
+ /^[a-zA-Z0-9_-]+$/ => {
47
+ type: :integer,
48
+ min: 1,
49
+ max: 10
50
+ }
51
+ }
52
+ },
53
+ circuit_breaker: {
54
+ type: :hash,
55
+ required: false,
56
+ default: {
57
+ enabled: true,
58
+ failure_threshold: 5,
59
+ timeout: 300,
60
+ half_open_max_calls: 3
61
+ },
62
+ properties: {
63
+ enabled: {
64
+ type: :boolean,
65
+ required: false,
66
+ default: true
67
+ },
68
+ failure_threshold: {
69
+ type: :integer,
70
+ required: false,
71
+ default: 5,
72
+ min: 1,
73
+ max: 100
74
+ },
75
+ timeout: {
76
+ type: :integer,
77
+ required: false,
78
+ default: 300,
79
+ min: 60,
80
+ max: 3600
81
+ },
82
+ half_open_max_calls: {
83
+ type: :integer,
84
+ required: false,
85
+ default: 3,
86
+ min: 1,
87
+ max: 10
88
+ }
89
+ }
90
+ },
91
+ retry: {
92
+ type: :hash,
93
+ required: false,
94
+ default: {
95
+ enabled: true,
96
+ max_attempts: 3,
97
+ base_delay: 1.0,
98
+ max_delay: 60.0,
99
+ exponential_base: 2.0,
100
+ jitter: true
101
+ },
102
+ properties: {
103
+ enabled: {
104
+ type: :boolean,
105
+ required: false,
106
+ default: true
107
+ },
108
+ max_attempts: {
109
+ type: :integer,
110
+ required: false,
111
+ default: 3,
112
+ min: 1,
113
+ max: 10
114
+ },
115
+ base_delay: {
116
+ type: :number,
117
+ required: false,
118
+ default: 1.0,
119
+ min: 0.1,
120
+ max: 60.0
121
+ },
122
+ max_delay: {
123
+ type: :number,
124
+ required: false,
125
+ default: 60.0,
126
+ min: 1.0,
127
+ max: 3600.0
128
+ },
129
+ exponential_base: {
130
+ type: :number,
131
+ required: false,
132
+ default: 2.0,
133
+ min: 1.1,
134
+ max: 5.0
135
+ },
136
+ jitter: {
137
+ type: :boolean,
138
+ required: false,
139
+ default: true
140
+ }
141
+ }
142
+ },
143
+ rate_limit: {
144
+ type: :hash,
145
+ required: false,
146
+ default: {
147
+ enabled: true,
148
+ default_reset_time: 3600,
149
+ burst_limit: 10,
150
+ sustained_limit: 5
151
+ },
152
+ properties: {
153
+ enabled: {
154
+ type: :boolean,
155
+ required: false,
156
+ default: true
157
+ },
158
+ default_reset_time: {
159
+ type: :integer,
160
+ required: false,
161
+ default: 3600,
162
+ min: 60,
163
+ max: 86400
164
+ },
165
+ burst_limit: {
166
+ type: :integer,
167
+ required: false,
168
+ default: 10,
169
+ min: 1,
170
+ max: 1000
171
+ },
172
+ sustained_limit: {
173
+ type: :integer,
174
+ required: false,
175
+ default: 5,
176
+ min: 1,
177
+ max: 100
178
+ }
179
+ }
180
+ },
181
+ load_balancing: {
182
+ type: :hash,
183
+ required: false,
184
+ default: {
185
+ enabled: true,
186
+ strategy: "weighted_round_robin",
187
+ health_check_interval: 30,
188
+ unhealthy_threshold: 3
189
+ },
190
+ properties: {
191
+ enabled: {
192
+ type: :boolean,
193
+ required: false,
194
+ default: true
195
+ },
196
+ strategy: {
197
+ type: :string,
198
+ required: false,
199
+ default: "weighted_round_robin",
200
+ enum: ["round_robin", "weighted_round_robin", "least_connections", "random"]
201
+ },
202
+ health_check_interval: {
203
+ type: :integer,
204
+ required: false,
205
+ default: 30,
206
+ min: 10,
207
+ max: 300
208
+ },
209
+ unhealthy_threshold: {
210
+ type: :integer,
211
+ required: false,
212
+ default: 3,
213
+ min: 1,
214
+ max: 10
215
+ }
216
+ }
217
+ },
218
+ model_switching: {
219
+ type: :hash,
220
+ required: false,
221
+ default: {
222
+ enabled: true,
223
+ auto_switch_on_error: true,
224
+ auto_switch_on_rate_limit: true,
225
+ fallback_strategy: "sequential"
226
+ },
227
+ properties: {
228
+ enabled: {
229
+ type: :boolean,
230
+ required: false,
231
+ default: true
232
+ },
233
+ auto_switch_on_error: {
234
+ type: :boolean,
235
+ required: false,
236
+ default: true
237
+ },
238
+ auto_switch_on_rate_limit: {
239
+ type: :boolean,
240
+ required: false,
241
+ default: true
242
+ },
243
+ fallback_strategy: {
244
+ type: :string,
245
+ required: false,
246
+ default: "sequential",
247
+ enum: ["sequential", "random", "weighted"]
248
+ }
249
+ }
250
+ },
251
+ health_check: {
252
+ type: :hash,
253
+ required: false,
254
+ default: {
255
+ enabled: true,
256
+ interval: 60,
257
+ timeout: 10,
258
+ failure_threshold: 3,
259
+ success_threshold: 2
260
+ },
261
+ properties: {
262
+ enabled: {
263
+ type: :boolean,
264
+ required: false,
265
+ default: true
266
+ },
267
+ interval: {
268
+ type: :integer,
269
+ required: false,
270
+ default: 60,
271
+ min: 10,
272
+ max: 600
273
+ },
274
+ timeout: {
275
+ type: :integer,
276
+ required: false,
277
+ default: 10,
278
+ min: 1,
279
+ max: 60
280
+ },
281
+ failure_threshold: {
282
+ type: :integer,
283
+ required: false,
284
+ default: 3,
285
+ min: 1,
286
+ max: 10
287
+ },
288
+ success_threshold: {
289
+ type: :integer,
290
+ required: false,
291
+ default: 2,
292
+ min: 1,
293
+ max: 5
294
+ }
295
+ }
296
+ },
297
+ metrics: {
298
+ type: :hash,
299
+ required: false,
300
+ default: {
301
+ enabled: true,
302
+ retention_days: 30,
303
+ aggregation_interval: 300,
304
+ export_interval: 3600
305
+ },
306
+ properties: {
307
+ enabled: {
308
+ type: :boolean,
309
+ required: false,
310
+ default: true
311
+ },
312
+ retention_days: {
313
+ type: :integer,
314
+ required: false,
315
+ default: 30,
316
+ min: 1,
317
+ max: 365
318
+ },
319
+ aggregation_interval: {
320
+ type: :integer,
321
+ required: false,
322
+ default: 300,
323
+ min: 60,
324
+ max: 3600
325
+ },
326
+ export_interval: {
327
+ type: :integer,
328
+ required: false,
329
+ default: 3600,
330
+ min: 300,
331
+ max: 86400
332
+ }
333
+ }
334
+ },
335
+ session: {
336
+ type: :hash,
337
+ required: false,
338
+ default: {
339
+ enabled: true,
340
+ timeout: 1800,
341
+ sticky_sessions: true,
342
+ session_affinity: "provider_model"
343
+ },
344
+ properties: {
345
+ enabled: {
346
+ type: :boolean,
347
+ required: false,
348
+ default: true
349
+ },
350
+ timeout: {
351
+ type: :integer,
352
+ required: false,
353
+ default: 1800,
354
+ min: 300,
355
+ max: 7200
356
+ },
357
+ sticky_sessions: {
358
+ type: :boolean,
359
+ required: false,
360
+ default: true
361
+ },
362
+ session_affinity: {
363
+ type: :string,
364
+ required: false,
365
+ default: "provider_model",
366
+ enum: ["provider_model", "provider", "model", "none"]
367
+ }
368
+ }
369
+ }
370
+ }
371
+ },
372
+ providers: {
373
+ type: :hash,
374
+ required: false,
375
+ default: {},
376
+ pattern_properties: {
377
+ /^[a-zA-Z0-9_-]+$/ => {
378
+ type: :hash,
379
+ properties: {
380
+ type: {
381
+ type: :string,
382
+ required: true,
383
+ enum: ["usage_based", "subscription", "passthrough"]
384
+ },
385
+ priority: {
386
+ type: :integer,
387
+ required: false,
388
+ default: 1,
389
+ min: 1,
390
+ max: 10
391
+ },
392
+ max_tokens: {
393
+ type: :integer,
394
+ required: false,
395
+ min: 1000,
396
+ max: 1_000_000
397
+ },
398
+ default_flags: {
399
+ type: :array,
400
+ required: false,
401
+ default: [],
402
+ items: {
403
+ type: :string
404
+ }
405
+ },
406
+ models: {
407
+ type: :array,
408
+ required: false,
409
+ default: [],
410
+ items: {
411
+ type: :string,
412
+ pattern: /^[a-zA-Z0-9._-]+$/
413
+ }
414
+ },
415
+ model_weights: {
416
+ type: :hash,
417
+ required: false,
418
+ default: {},
419
+ pattern_properties: {
420
+ /^[a-zA-Z0-9._-]+$/ => {
421
+ type: :integer,
422
+ min: 1,
423
+ max: 10
424
+ }
425
+ }
426
+ },
427
+ models_config: {
428
+ type: :hash,
429
+ required: false,
430
+ default: {},
431
+ pattern_properties: {
432
+ /^[a-zA-Z0-9._-]+$/ => {
433
+ type: :hash,
434
+ properties: {
435
+ flags: {
436
+ type: :array,
437
+ required: false,
438
+ default: [],
439
+ items: {
440
+ type: :string
441
+ }
442
+ },
443
+ max_tokens: {
444
+ type: :integer,
445
+ required: false,
446
+ min: 1000,
447
+ max: 1_000_000
448
+ },
449
+ timeout: {
450
+ type: :integer,
451
+ required: false,
452
+ min: 30,
453
+ max: 3600
454
+ }
455
+ }
456
+ }
457
+ }
458
+ },
459
+ auth: {
460
+ type: :hash,
461
+ required: false,
462
+ default: {},
463
+ properties: {
464
+ api_key_env: {
465
+ type: :string,
466
+ required: false,
467
+ pattern: /^[A-Z_][A-Z0-9_]*$/
468
+ },
469
+ api_key: {
470
+ type: :string,
471
+ required: false
472
+ }
473
+ }
474
+ },
475
+ endpoints: {
476
+ type: :hash,
477
+ required: false,
478
+ default: {},
479
+ properties: {
480
+ default: {
481
+ type: :string,
482
+ required: false,
483
+ format: :uri
484
+ }
485
+ }
486
+ },
487
+ features: {
488
+ type: :hash,
489
+ required: false,
490
+ default: {},
491
+ properties: {
492
+ file_upload: {
493
+ type: :boolean,
494
+ required: false,
495
+ default: false
496
+ },
497
+ code_generation: {
498
+ type: :boolean,
499
+ required: false,
500
+ default: true
501
+ },
502
+ analysis: {
503
+ type: :boolean,
504
+ required: false,
505
+ default: true
506
+ },
507
+ vision: {
508
+ type: :boolean,
509
+ required: false,
510
+ default: false
511
+ }
512
+ }
513
+ },
514
+ monitoring: {
515
+ type: :hash,
516
+ required: false,
517
+ default: {
518
+ enabled: true,
519
+ metrics_interval: 60
520
+ },
521
+ properties: {
522
+ enabled: {
523
+ type: :boolean,
524
+ required: false,
525
+ default: true
526
+ },
527
+ metrics_interval: {
528
+ type: :integer,
529
+ required: false,
530
+ default: 60,
531
+ min: 10,
532
+ max: 3600
533
+ }
534
+ }
535
+ }
536
+ }
537
+ }
538
+ }
539
+ }
540
+ }.freeze
541
+
542
+ # Validate configuration against schema
543
+ def self.validate(config)
544
+ errors = []
545
+ warnings = []
546
+
547
+ # Validate top-level structure
548
+ unless config.is_a?(Hash)
549
+ errors << "Configuration must be a hash"
550
+ return {valid: false, errors: errors, warnings: warnings}
551
+ end
552
+
553
+ # Validate harness section
554
+ if config.key?(:harness) || config.key?("harness")
555
+ harness_errors, harness_warnings = validate_section(
556
+ config[:harness] || config["harness"],
557
+ SCHEMA[:harness],
558
+ "harness"
559
+ )
560
+ errors.concat(harness_errors)
561
+ warnings.concat(harness_warnings)
562
+ elsif SCHEMA[:harness][:required]
563
+ errors << "harness: section is required"
564
+ end
565
+
566
+ # Validate providers section
567
+ if config.key?(:providers) || config.key?("providers")
568
+ providers_errors, providers_warnings = validate_section(
569
+ config[:providers] || config["providers"],
570
+ SCHEMA[:providers],
571
+ "providers"
572
+ )
573
+ errors.concat(providers_errors)
574
+ warnings.concat(providers_warnings)
575
+ end
576
+
577
+ # Cross-validation
578
+ cross_validation_errors, cross_validation_warnings = cross_validate(config)
579
+ errors.concat(cross_validation_errors)
580
+ warnings.concat(cross_validation_warnings)
581
+
582
+ {
583
+ valid: errors.empty?,
584
+ errors: errors,
585
+ warnings: warnings
586
+ }
587
+ end
588
+
589
+ # Apply defaults to configuration
590
+ def self.apply_defaults(config)
591
+ result = deep_dup(config)
592
+
593
+ # Apply harness defaults
594
+ if result.key?(:harness) || result.key?("harness")
595
+ harness_section = result[:harness] || result["harness"]
596
+ result[:harness] = apply_section_defaults(harness_section, SCHEMA[:harness])
597
+ else
598
+ result[:harness] = apply_section_defaults({}, SCHEMA[:harness])
599
+ end
600
+
601
+ # Apply provider defaults
602
+ if result.key?(:providers) || result.key?("providers")
603
+ providers_section = result[:providers] || result["providers"]
604
+ result[:providers] = apply_providers_defaults(providers_section)
605
+ else
606
+ result[:providers] = {}
607
+ end
608
+
609
+ result
610
+ end
611
+
612
+ # Generate example configuration
613
+ def self.generate_example
614
+ {
615
+ harness: {
616
+ max_retries: 2,
617
+ default_provider: "cursor",
618
+ fallback_providers: ["cursor"],
619
+ restrict_to_non_byok: false,
620
+ provider_weights: {
621
+ "cursor" => 3,
622
+ "anthropic" => 2,
623
+ "macos" => 1
624
+ },
625
+ circuit_breaker: {
626
+ enabled: true,
627
+ failure_threshold: 5,
628
+ timeout: 300,
629
+ half_open_max_calls: 3
630
+ },
631
+ retry: {
632
+ enabled: true,
633
+ max_attempts: 3,
634
+ base_delay: 1.0,
635
+ max_delay: 60.0,
636
+ exponential_base: 2.0,
637
+ jitter: true
638
+ },
639
+ rate_limit: {
640
+ enabled: true,
641
+ default_reset_time: 3600,
642
+ burst_limit: 10,
643
+ sustained_limit: 5
644
+ },
645
+ load_balancing: {
646
+ enabled: true,
647
+ strategy: "weighted_round_robin",
648
+ health_check_interval: 30,
649
+ unhealthy_threshold: 3
650
+ },
651
+ model_switching: {
652
+ enabled: true,
653
+ auto_switch_on_error: true,
654
+ auto_switch_on_rate_limit: true,
655
+ fallback_strategy: "sequential"
656
+ },
657
+ health_check: {
658
+ enabled: true,
659
+ interval: 60,
660
+ timeout: 10,
661
+ failure_threshold: 3,
662
+ success_threshold: 2
663
+ },
664
+ metrics: {
665
+ enabled: true,
666
+ retention_days: 30,
667
+ aggregation_interval: 300,
668
+ export_interval: 3600
669
+ },
670
+ session: {
671
+ enabled: true,
672
+ timeout: 1800,
673
+ sticky_sessions: true,
674
+ session_affinity: "provider_model"
675
+ }
676
+ },
677
+ providers: {
678
+ cursor: {
679
+ type: "subscription",
680
+ priority: 1,
681
+ default_flags: [],
682
+ models: ["cursor-default", "cursor-fast", "cursor-precise"],
683
+ model_weights: {
684
+ "cursor-default" => 3,
685
+ "cursor-fast" => 2,
686
+ "cursor-precise" => 1
687
+ },
688
+ models_config: {
689
+ "cursor-default" => {
690
+ flags: [],
691
+ timeout: 600
692
+ },
693
+ "cursor-fast" => {
694
+ flags: ["--fast"],
695
+ timeout: 300
696
+ },
697
+ "cursor-precise" => {
698
+ flags: ["--precise"],
699
+ timeout: 900
700
+ }
701
+ },
702
+ features: {
703
+ file_upload: true,
704
+ code_generation: true,
705
+ analysis: true
706
+ },
707
+ monitoring: {
708
+ enabled: true,
709
+ metrics_interval: 60
710
+ }
711
+ },
712
+ anthropic: {
713
+ type: "usage_based",
714
+ priority: 2,
715
+ max_tokens: 100_000,
716
+ default_flags: ["--dangerously-skip-permissions"],
717
+ models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022"],
718
+ model_weights: {
719
+ "claude-3-5-sonnet-20241022" => 3,
720
+ "claude-3-5-haiku-20241022" => 2
721
+ },
722
+ models_config: {
723
+ "claude-3-5-sonnet-20241022" => {
724
+ flags: ["--dangerously-skip-permissions"],
725
+ max_tokens: 200_000,
726
+ timeout: 300
727
+ },
728
+ "claude-3-5-haiku-20241022" => {
729
+ flags: ["--dangerously-skip-permissions"],
730
+ max_tokens: 200_000,
731
+ timeout: 180
732
+ }
733
+ },
734
+ auth: {
735
+ api_key_env: "ANTHROPIC_API_KEY"
736
+ },
737
+ endpoints: {
738
+ default: "https://api.anthropic.com/v1/messages"
739
+ },
740
+ features: {
741
+ file_upload: true,
742
+ code_generation: true,
743
+ analysis: true,
744
+ vision: true
745
+ },
746
+ monitoring: {
747
+ enabled: true,
748
+ metrics_interval: 60
749
+ }
750
+ },
751
+ macos: {
752
+ type: "passthrough",
753
+ priority: 4,
754
+ underlying_service: "cursor",
755
+ models: ["cursor-chat"],
756
+ features: {
757
+ file_upload: false,
758
+ code_generation: true,
759
+ analysis: true,
760
+ interactive: true
761
+ }
762
+ }
763
+ }
764
+ }
765
+ end
766
+
767
+ def self.validate_section(data, schema, path)
768
+ errors = []
769
+ warnings = []
770
+
771
+ # Check if section is required
772
+ if schema[:required] && data.nil?
773
+ errors << "#{path}: section is required"
774
+ return [errors, warnings]
775
+ end
776
+
777
+ # Check if string is empty and required
778
+ if schema[:required] && schema[:type] == :string && data.is_a?(String) && data.empty?
779
+ errors << "#{path}: is required"
780
+ return [errors, warnings]
781
+ end
782
+
783
+ # For non-hash types, validate the type and return
784
+ unless data.is_a?(Hash)
785
+ # Validate type
786
+ if schema[:type] == :array && !data.is_a?(Array)
787
+ errors << "#{path}: must be an array"
788
+ elsif schema[:type] == :string && !data.is_a?(String)
789
+ errors << "#{path}: must be a string"
790
+ elsif schema[:type] == :integer && !data.is_a?(Integer)
791
+ errors << "#{path}: must be an integer"
792
+ elsif schema[:type] == :number && !data.is_a?(Numeric)
793
+ errors << "#{path}: must be a number"
794
+ elsif schema[:type] == :boolean && !data.is_a?(TrueClass) && !data.is_a?(FalseClass)
795
+ errors << "#{path}: must be a boolean"
796
+ end
797
+
798
+ # Validate string pattern if specified
799
+ if schema[:type] == :string && data.is_a?(String) && schema[:pattern] && !data.match?(schema[:pattern])
800
+ errors << "#{path}: must match pattern"
801
+ end
802
+
803
+ # Validate enum values
804
+ if schema[:enum] && !schema[:enum].include?(data)
805
+ errors << "#{path}: must be one of #{schema[:enum].join(", ")}"
806
+ end
807
+
808
+ # Validate numeric constraints
809
+ if schema[:type] == :integer && data.is_a?(Integer)
810
+ if schema[:min] && data < schema[:min]
811
+ errors << "#{path}: must be >= #{schema[:min]}"
812
+ end
813
+ if schema[:max] && data > schema[:max]
814
+ errors << "#{path}: must be <= #{schema[:max]}"
815
+ end
816
+ end
817
+
818
+ return [errors, warnings]
819
+ end
820
+
821
+ # Validate hash type
822
+ if schema[:type] == :hash && !data.is_a?(Hash)
823
+ errors << "#{path}: must be a hash"
824
+ return [errors, warnings]
825
+ end
826
+
827
+ # Validate properties for hash types
828
+ if schema[:type] == :hash && schema[:properties]
829
+ schema[:properties].each do |prop_name, prop_schema|
830
+ prop_path = "#{path}.#{prop_name}"
831
+
832
+ if data.key?(prop_name) || data.key?(prop_name.to_s)
833
+ prop_value = data.key?(prop_name) ? data[prop_name] : data[prop_name.to_s]
834
+ prop_errors, prop_warnings = validate_section(prop_value, prop_schema, prop_path)
835
+ errors.concat(prop_errors)
836
+ warnings.concat(prop_warnings)
837
+ elsif prop_schema[:required]
838
+ errors << "#{prop_path}: is required"
839
+ end
840
+ end
841
+ end
842
+
843
+ # Validate pattern properties for hash types
844
+ if schema[:type] == :hash && schema[:pattern_properties]
845
+ data.each do |key, value|
846
+ # Find matching pattern
847
+ matching_pattern = nil
848
+ matching_schema = nil
849
+
850
+ schema[:pattern_properties].each do |pattern, pattern_schema|
851
+ if key.to_s.match?(pattern)
852
+ matching_pattern = pattern
853
+ matching_schema = pattern_schema
854
+ break
855
+ end
856
+ end
857
+
858
+ if matching_schema
859
+ prop_path = "#{path}.#{key}"
860
+ prop_errors, prop_warnings = validate_section(value, matching_schema, prop_path)
861
+ errors.concat(prop_errors)
862
+ warnings.concat(prop_warnings)
863
+ end
864
+ end
865
+ end
866
+
867
+ # Validate array items
868
+ if schema[:type] == :array && schema[:items] && data.is_a?(Array)
869
+ data.each_with_index do |item, index|
870
+ item_errors, item_warnings = validate_section(item, schema[:items], "#{path}[#{index}]")
871
+ errors.concat(item_errors)
872
+ warnings.concat(item_warnings)
873
+ end
874
+ end
875
+
876
+ # Validate constraints
877
+ if data.is_a?(Numeric)
878
+ if schema[:min] && data < schema[:min]
879
+ errors << "#{path}: must be >= #{schema[:min]}"
880
+ end
881
+ if schema[:max] && data > schema[:max]
882
+ errors << "#{path}: must be <= #{schema[:max]}"
883
+ end
884
+ end
885
+
886
+ if data.is_a?(String)
887
+ if schema[:pattern] && !data.match?(schema[:pattern])
888
+ errors << "#{path}: must match pattern #{schema[:pattern]}"
889
+ end
890
+ if schema[:enum] && !schema[:enum].include?(data)
891
+ errors << "#{path}: must be one of #{schema[:enum].join(", ")}"
892
+ end
893
+ if schema[:format] == :uri && !valid_uri?(data)
894
+ errors << "#{path}: must be a valid URI"
895
+ end
896
+ end
897
+
898
+ [errors, warnings]
899
+ end
900
+
901
+ def self.cross_validate(config)
902
+ errors = []
903
+ warnings = []
904
+
905
+ # Validate that default_provider exists in providers
906
+ harness_config = config[:harness] || config["harness"]
907
+ providers_config = config[:providers] || config["providers"]
908
+
909
+ if harness_config && providers_config
910
+ default_provider = harness_config[:default_provider] || harness_config["default_provider"]
911
+ if default_provider
912
+ unless providers_config.key?(default_provider) || providers_config.key?(default_provider.to_sym)
913
+ errors << "Default provider '#{default_provider}' not found in providers configuration"
914
+ end
915
+ end
916
+
917
+ # Validate fallback providers exist
918
+ fallback_providers = harness_config[:fallback_providers] || harness_config["fallback_providers"] || []
919
+ fallback_providers.each do |provider|
920
+ unless providers_config.key?(provider) || providers_config.key?(provider.to_sym)
921
+ errors << "Fallback provider '#{provider}' not found in providers configuration"
922
+ end
923
+ end
924
+
925
+ # Validate provider weights reference existing providers
926
+ provider_weights = harness_config[:provider_weights] || harness_config["provider_weights"] || {}
927
+ provider_weights.each do |provider, _weight|
928
+ unless providers_config.key?(provider) || providers_config.key?(provider.to_sym)
929
+ warnings << "Provider weight specified for non-existent provider '#{provider}'"
930
+ end
931
+ end
932
+ end
933
+
934
+ # Validate that models in model_weights exist in models array
935
+ providers_config&.each do |provider_name, provider_config|
936
+ models = provider_config[:models] || provider_config["models"] || []
937
+ model_weights = provider_config[:model_weights] || provider_config["model_weights"] || {}
938
+
939
+ model_weights.each do |model, _weight|
940
+ unless models.include?(model)
941
+ warnings << "Model weight specified for model '#{model}' not in models array for provider '#{provider_name}'"
942
+ end
943
+ end
944
+ end
945
+
946
+ [errors, warnings]
947
+ end
948
+
949
+ def self.apply_section_defaults(data, schema)
950
+ result = data.dup
951
+
952
+ schema[:properties]&.each do |prop_name, prop_schema|
953
+ if result.key?(prop_name) || result.key?(prop_name.to_s)
954
+ prop_value = result[prop_name] || result[prop_name.to_s]
955
+ if prop_schema[:type] == :hash && prop_schema[:properties]
956
+ result[prop_name] = apply_section_defaults(prop_value, prop_schema)
957
+ end
958
+ elsif prop_schema[:default]
959
+ result[prop_name] = prop_schema[:default]
960
+ elsif prop_schema[:type] == :hash && prop_schema[:properties]
961
+ result[prop_name] = apply_section_defaults({}, prop_schema)
962
+ end
963
+ end
964
+
965
+ result
966
+ end
967
+
968
+ def self.apply_providers_defaults(providers_data)
969
+ result = providers_data.dup
970
+
971
+ providers_schema = SCHEMA[:providers][:pattern_properties][/^[a-zA-Z0-9_-]+$/]
972
+
973
+ result.each do |provider_name, provider_config|
974
+ result[provider_name] = apply_section_defaults(provider_config, providers_schema)
975
+ end
976
+
977
+ result
978
+ end
979
+
980
+ def self.valid_uri?(uri_string)
981
+ require "uri"
982
+ URI.parse(uri_string)
983
+ true
984
+ rescue URI::InvalidURIError
985
+ false
986
+ end
987
+
988
+ def self.deep_dup(obj)
989
+ case obj
990
+ when Hash
991
+ obj.transform_values { |v| deep_dup(v) }
992
+ when Array
993
+ obj.map { |v| deep_dup(v) }
994
+ else
995
+ begin
996
+ obj.dup
997
+ rescue
998
+ obj
999
+ end
1000
+ end
1001
+ end
1002
+
1003
+ private_class_method :validate_section, :apply_providers_defaults, :valid_uri?, :deep_dup
1004
+ end
1005
+ end
1006
+ end