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,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+ require_relative "base"
5
+
6
+ module Aidp
7
+ module Harness
8
+ module UI
9
+ # Handles interactive question collection using CLI UI prompts
10
+ class QuestionCollector < Base
11
+ class QuestionError < StandardError; end
12
+ class ValidationError < QuestionError; end
13
+ class CollectionError < QuestionError; end
14
+
15
+ def initialize(ui_components = {})
16
+ super()
17
+ @prompt = ui_components[:prompt] || TTY::Prompt.new
18
+ @validator = ui_components[:validator] || QuestionValidator.new
19
+ end
20
+
21
+ def collect_questions(questions)
22
+ validate_questions_input(questions)
23
+
24
+ # Validate all questions first
25
+ errors = get_validation_errors(questions)
26
+ raise ValidationError, errors.join("\n") unless errors.empty?
27
+
28
+ responses = {}
29
+ questions.each_with_index do |question, index|
30
+ question_key = question[:key] || "question_#{index + 1}"
31
+ responses[question_key] = collect_single_question(question, index + 1)
32
+ end
33
+ responses
34
+ rescue ValidationError => e
35
+ raise e
36
+ rescue => e
37
+ raise CollectionError, "Failed to collect questions: #{e.message}"
38
+ end
39
+
40
+ def collect_single_question(question, number)
41
+ validate_question_format(question)
42
+
43
+ question_text = format_question_text(question, number)
44
+ response = prompt_for_response(question_text, question)
45
+
46
+ validate_response(response, question)
47
+ response
48
+ rescue => e
49
+ raise QuestionError, "Failed to collect question #{number}: #{e.message}"
50
+ end
51
+
52
+ def validate_questions(questions)
53
+ return true if questions.empty?
54
+
55
+ questions.all? do |question|
56
+ validate_question_format(question)
57
+ true
58
+ rescue ValidationError
59
+ false
60
+ end
61
+ end
62
+
63
+ def get_validation_errors(questions)
64
+ errors = []
65
+
66
+ questions.each_with_index do |question, index|
67
+ validate_question_format(question)
68
+ rescue ValidationError => e
69
+ errors << "Question #{index + 1}: #{e.message}"
70
+ end
71
+
72
+ errors
73
+ end
74
+
75
+ def validate_question_format(question)
76
+ raise ValidationError, "Question must be a hash" unless question.is_a?(Hash)
77
+ raise ValidationError, "Question must have :text key" unless question.key?(:text)
78
+ raise ValidationError, "Question text cannot be empty" if question[:text].to_s.strip.empty?
79
+ unless question.key?(:type) && question.key?(:required)
80
+ raise ValidationError, "Question missing required fields"
81
+ end
82
+ end
83
+
84
+ def validate_questions_input(questions)
85
+ raise ValidationError, "Questions must be an array" unless questions.is_a?(Array)
86
+ # Allow empty array - return empty hash
87
+ end
88
+
89
+ private
90
+
91
+ def validate_response(response, question)
92
+ @validator.validate(response, question)
93
+ end
94
+
95
+ def format_question_text(question, number)
96
+ "Question #{number}: #{question[:text]}"
97
+ end
98
+
99
+ def prompt_for_response(question_text, question)
100
+ @prompt.ask(question_text) do |handler|
101
+ add_question_options(handler, question)
102
+ end
103
+ end
104
+
105
+ def add_question_options(handler, question)
106
+ return unless question[:options]
107
+
108
+ question[:options].each { |option| handler.option(option) }
109
+ end
110
+ end
111
+
112
+ # Validates question responses
113
+ class QuestionValidator
114
+ def validate(response, question)
115
+ validate_required(response, question)
116
+ validate_format(response, question)
117
+ validate_options(response, question)
118
+ end
119
+
120
+ private
121
+
122
+ def validate_required(response, question)
123
+ return unless question[:required]
124
+ raise ValidationError, "Response is required" if response.nil? || response.to_s.strip.empty?
125
+ end
126
+
127
+ def validate_format(response, question)
128
+ return unless question[:format]
129
+ return if response.to_s.match?(question[:format])
130
+ raise ValidationError, "Response format is invalid"
131
+ end
132
+
133
+ def validate_options(response, question)
134
+ return unless question[:options]
135
+ return if question[:options].include?(response)
136
+ raise ValidationError, "Response must be one of: #{question[:options].join(", ")}"
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-spinner"
4
+ require_relative "base"
5
+
6
+ module Aidp
7
+ module Harness
8
+ module UI
9
+ # Handles concurrent operations using CLI UI spinner groups
10
+ class SpinnerGroup < Base
11
+ class SpinnerGroupError < StandardError; end
12
+ class InvalidOperationError < SpinnerGroupError; end
13
+ class ExecutionError < SpinnerGroupError; end
14
+
15
+ def initialize(ui_components = {})
16
+ super()
17
+ @spinner_class = ui_components[:spinner_class] || TTY::Spinner
18
+ @formatter = ui_components[:formatter] || SpinnerGroupFormatter.new
19
+ @spinners = {}
20
+ end
21
+
22
+ def run_concurrent_operations(operations)
23
+ validate_operations(operations)
24
+
25
+ # Create individual spinners for each operation
26
+ operations.each do |operation|
27
+ spinner = @spinner_class.new(
28
+ "#{operation[:name]}...",
29
+ format: :dots,
30
+ success_mark: "✓",
31
+ error_mark: "✗"
32
+ )
33
+ @spinners[operation[:id]] = spinner
34
+ spinner.start
35
+ end
36
+
37
+ # Execute operations
38
+ operations.each do |operation|
39
+ operation[:block].call
40
+ @spinners[operation[:id]].success(operation[:name])
41
+ rescue => e
42
+ @spinners[operation[:id]].error(operation[:name])
43
+ raise e
44
+ end
45
+ rescue => e
46
+ raise ExecutionError, "Failed to run concurrent operations: #{e.message}"
47
+ end
48
+
49
+ def run_workflow_steps(steps)
50
+ validate_steps(steps)
51
+
52
+ operations = convert_steps_to_operations(steps)
53
+ run_concurrent_operations(operations)
54
+ rescue => e
55
+ raise ExecutionError, "Failed to run workflow steps: #{e.message}"
56
+ end
57
+
58
+ def run_analysis_tasks(tasks)
59
+ validate_tasks(tasks)
60
+
61
+ operations = convert_tasks_to_operations(tasks)
62
+ run_concurrent_operations(operations)
63
+ rescue => e
64
+ raise ExecutionError, "Failed to run analysis tasks: #{e.message}"
65
+ end
66
+
67
+ def run_provider_operations(provider_operations)
68
+ validate_provider_operations(provider_operations)
69
+
70
+ operations = convert_provider_operations(provider_operations)
71
+ run_concurrent_operations(operations)
72
+ rescue => e
73
+ raise ExecutionError, "Failed to run provider operations: #{e.message}"
74
+ end
75
+
76
+ private
77
+
78
+ def validate_operations(operations)
79
+ raise InvalidOperationError, "Operations must be an array" unless operations.is_a?(Array)
80
+ raise InvalidOperationError, "Operations array cannot be empty" if operations.empty?
81
+
82
+ operations.each_with_index do |operation, index|
83
+ validate_operation(operation, index)
84
+ end
85
+ end
86
+
87
+ def validate_operation(operation, index)
88
+ raise InvalidOperationError, "Operation #{index} must be a hash" unless operation.is_a?(Hash)
89
+ raise InvalidOperationError, "Operation #{index} must have :title" unless operation.key?(:title)
90
+ raise InvalidOperationError, "Operation #{index} must have :block" unless operation.key?(:block)
91
+ raise InvalidOperationError, "Operation #{index} title cannot be empty" if operation[:title].to_s.strip.empty?
92
+ raise InvalidOperationError, "Operation #{index} block must be callable" unless operation[:block].respond_to?(:call)
93
+ end
94
+
95
+ def validate_steps(steps)
96
+ raise InvalidOperationError, "Steps must be an array" unless steps.is_a?(Array)
97
+ raise InvalidOperationError, "Steps array cannot be empty" if steps.empty?
98
+ end
99
+
100
+ def validate_tasks(tasks)
101
+ raise InvalidOperationError, "Tasks must be an array" unless tasks.is_a?(Array)
102
+ raise InvalidOperationError, "Tasks array cannot be empty" if tasks.empty?
103
+ end
104
+
105
+ def validate_provider_operations(provider_operations)
106
+ raise InvalidOperationError, "Provider operations must be an array" unless provider_operations.is_a?(Array)
107
+ raise InvalidOperationError, "Provider operations array cannot be empty" if provider_operations.empty?
108
+ end
109
+
110
+ def add_operation(spin_group, operation)
111
+ formatted_title = @formatter.format_operation_title(operation[:title])
112
+ spin_group.add(formatted_title) do |spinner|
113
+ execute_operation_with_error_handling(operation, spinner)
114
+ end
115
+ end
116
+
117
+ def execute_operation_with_error_handling(operation, spinner)
118
+ operation[:block].call(spinner)
119
+ rescue => e
120
+ spinner.update_title(@formatter.format_error_title(operation[:title], e.message))
121
+ raise
122
+ end
123
+
124
+ def convert_steps_to_operations(steps)
125
+ steps.map do |step|
126
+ {
127
+ title: @formatter.format_step_title(step[:name]),
128
+ block: step[:block]
129
+ }
130
+ end
131
+ end
132
+
133
+ def convert_tasks_to_operations(tasks)
134
+ tasks.map do |task|
135
+ {
136
+ title: @formatter.format_task_title(task[:name]),
137
+ block: task[:block]
138
+ }
139
+ end
140
+ end
141
+
142
+ def convert_provider_operations(provider_operations)
143
+ provider_operations.map do |op|
144
+ {
145
+ title: @formatter.format_provider_title(op[:provider], op[:operation]),
146
+ block: op[:block]
147
+ }
148
+ end
149
+ end
150
+ end
151
+
152
+ # Formats spinner group display text
153
+ class SpinnerGroupFormatter
154
+ def format_operation_title(title)
155
+ "🔄 #{title}"
156
+ end
157
+
158
+ def format_step_title(step_name)
159
+ "⚡ #{step_name}"
160
+ end
161
+
162
+ def format_task_title(task_name)
163
+ "📋 #{task_name}"
164
+ end
165
+
166
+ def format_provider_title(provider_name, operation)
167
+ "🤖 #{provider_name}: #{operation}"
168
+ end
169
+
170
+ def format_error_title(original_title, error_message)
171
+ "❌ #{original_title} (Error: #{error_message})"
172
+ end
173
+
174
+ def format_success_title(original_title)
175
+ "✅ #{original_title}"
176
+ end
177
+
178
+ def format_progress_title(title, current, total)
179
+ "📊 #{title} (#{current}/#{total})"
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-spinner"
4
+ require "pastel"
5
+
6
+ module Aidp
7
+ module Harness
8
+ module UI
9
+ # Unified spinner helper that automatically manages TTY::Spinner lifecycle
10
+ # Usage: with_spinner("Loading...") { some_operation }
11
+ class SpinnerHelper
12
+ class SpinnerError < StandardError; end
13
+
14
+ def initialize
15
+ @pastel = Pastel.new
16
+ @active_spinners = []
17
+ end
18
+
19
+ # Main method: automatically manages spinner around a block
20
+ def with_spinner(message, format: :dots, success_message: nil, error_message: nil, &block)
21
+ raise ArgumentError, "Block required for with_spinner" unless block_given?
22
+
23
+ spinner = create_spinner(message, format)
24
+ start_spinner(spinner)
25
+
26
+ begin
27
+ result = yield(spinner)
28
+ success_spinner(spinner, success_message || message)
29
+ result
30
+ rescue => e
31
+ error_spinner(spinner, error_message || "Failed: #{e.message}")
32
+ raise e
33
+ ensure
34
+ cleanup_spinner(spinner)
35
+ end
36
+ end
37
+
38
+ # Convenience methods for common patterns
39
+ def with_loading_spinner(message, &block)
40
+ with_spinner("⏳ #{message}", format: :dots, &block)
41
+ end
42
+
43
+ def with_processing_spinner(message, &block)
44
+ with_spinner("🔄 #{message}", format: :pulse, &block)
45
+ end
46
+
47
+ def with_saving_spinner(message, &block)
48
+ with_spinner("💾 #{message}", format: :dots, &block)
49
+ end
50
+
51
+ def with_analyzing_spinner(message, &block)
52
+ with_spinner("🔍 #{message}", format: :dots, &block)
53
+ end
54
+
55
+ def with_building_spinner(message, &block)
56
+ with_spinner("🏗️ #{message}", format: :dots, &block)
57
+ end
58
+
59
+ # For operations that might take a while
60
+ def with_long_operation_spinner(message, &block)
61
+ with_spinner("⏳ #{message}", format: :pulse, &block)
62
+ end
63
+
64
+ # For quick operations
65
+ def with_quick_spinner(message, &block)
66
+ with_spinner("⚡ #{message}", format: :dots, &block)
67
+ end
68
+
69
+ # Update spinner message during operation
70
+ def update_spinner_message(spinner, new_message)
71
+ spinner.update_title(new_message)
72
+ end
73
+
74
+ # Check if any spinners are active
75
+ def any_active?
76
+ @active_spinners.any?(&:spinning?)
77
+ end
78
+
79
+ # Get count of active spinners
80
+ def active_count
81
+ @active_spinners.count(&:spinning?)
82
+ end
83
+
84
+ # Force stop all spinners (emergency cleanup)
85
+ def stop_all
86
+ @active_spinners.each do |spinner|
87
+ spinner.stop if spinner.spinning?
88
+ end
89
+ @active_spinners.clear
90
+ end
91
+
92
+ private
93
+
94
+ def create_spinner(message, format)
95
+ TTY::Spinner.new(
96
+ "#{message} :spinner",
97
+ format: format,
98
+ success_mark: @pastel.green("✓"),
99
+ error_mark: @pastel.red("✗"),
100
+ hide_cursor: true
101
+ )
102
+ end
103
+
104
+ def start_spinner(spinner)
105
+ @active_spinners << spinner
106
+ spinner.start
107
+ end
108
+
109
+ def success_spinner(spinner, message)
110
+ spinner.success(@pastel.green("✓ #{message}"))
111
+ end
112
+
113
+ def error_spinner(spinner, message)
114
+ spinner.error(@pastel.red("✗ #{message}"))
115
+ end
116
+
117
+ def cleanup_spinner(spinner)
118
+ @active_spinners.delete(spinner)
119
+ # TTY::Spinner handles its own cleanup
120
+ end
121
+ end
122
+
123
+ # Global instance for easy access
124
+ SPINNER = SpinnerHelper.new
125
+
126
+ # Convenience methods for global access
127
+ def self.with_spinner(message, **options, &block)
128
+ SPINNER.with_spinner(message, **options, &block)
129
+ end
130
+
131
+ def self.with_loading_spinner(message, &block)
132
+ SPINNER.with_loading_spinner(message, &block)
133
+ end
134
+
135
+ def self.with_processing_spinner(message, &block)
136
+ SPINNER.with_processing_spinner(message, &block)
137
+ end
138
+
139
+ def self.with_saving_spinner(message, &block)
140
+ SPINNER.with_saving_spinner(message, &block)
141
+ end
142
+
143
+ def self.with_analyzing_spinner(message, &block)
144
+ SPINNER.with_analyzing_spinner(message, &block)
145
+ end
146
+
147
+ def self.with_building_spinner(message, &block)
148
+ SPINNER.with_building_spinner(message, &block)
149
+ end
150
+ end
151
+ end
152
+ end