aidp 0.5.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +128 -151
  3. data/bin/aidp +1 -1
  4. data/lib/aidp/analysis/kb_inspector.rb +471 -0
  5. data/lib/aidp/analysis/seams.rb +159 -0
  6. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +480 -0
  7. data/lib/aidp/analysis/tree_sitter_scan.rb +686 -0
  8. data/lib/aidp/analyze/error_handler.rb +2 -78
  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/analyze/steps.rb +6 -0
  15. data/lib/aidp/cli/jobs_command.rb +103 -435
  16. data/lib/aidp/cli.rb +317 -191
  17. data/lib/aidp/config.rb +298 -10
  18. data/lib/aidp/debug_logger.rb +195 -0
  19. data/lib/aidp/debug_mixin.rb +187 -0
  20. data/lib/aidp/execute/progress.rb +9 -0
  21. data/lib/aidp/execute/runner.rb +221 -40
  22. data/lib/aidp/execute/steps.rb +17 -7
  23. data/lib/aidp/execute/workflow_selector.rb +211 -0
  24. data/lib/aidp/harness/completion_checker.rb +268 -0
  25. data/lib/aidp/harness/condition_detector.rb +1526 -0
  26. data/lib/aidp/harness/config_loader.rb +373 -0
  27. data/lib/aidp/harness/config_manager.rb +382 -0
  28. data/lib/aidp/harness/config_schema.rb +1006 -0
  29. data/lib/aidp/harness/config_validator.rb +355 -0
  30. data/lib/aidp/harness/configuration.rb +477 -0
  31. data/lib/aidp/harness/enhanced_runner.rb +494 -0
  32. data/lib/aidp/harness/error_handler.rb +616 -0
  33. data/lib/aidp/harness/provider_config.rb +423 -0
  34. data/lib/aidp/harness/provider_factory.rb +306 -0
  35. data/lib/aidp/harness/provider_manager.rb +1269 -0
  36. data/lib/aidp/harness/provider_type_checker.rb +88 -0
  37. data/lib/aidp/harness/runner.rb +411 -0
  38. data/lib/aidp/harness/state/errors.rb +28 -0
  39. data/lib/aidp/harness/state/metrics.rb +219 -0
  40. data/lib/aidp/harness/state/persistence.rb +128 -0
  41. data/lib/aidp/harness/state/provider_state.rb +132 -0
  42. data/lib/aidp/harness/state/ui_state.rb +68 -0
  43. data/lib/aidp/harness/state/workflow_state.rb +123 -0
  44. data/lib/aidp/harness/state_manager.rb +586 -0
  45. data/lib/aidp/harness/status_display.rb +888 -0
  46. data/lib/aidp/harness/ui/base.rb +16 -0
  47. data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
  48. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
  49. data/lib/aidp/harness/ui/error_handler.rb +132 -0
  50. data/lib/aidp/harness/ui/frame_manager.rb +361 -0
  51. data/lib/aidp/harness/ui/job_monitor.rb +500 -0
  52. data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
  53. data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
  54. data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
  55. data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
  56. data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
  57. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
  58. data/lib/aidp/harness/ui/progress_display.rb +280 -0
  59. data/lib/aidp/harness/ui/question_collector.rb +141 -0
  60. data/lib/aidp/harness/ui/spinner_group.rb +184 -0
  61. data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
  62. data/lib/aidp/harness/ui/status_manager.rb +312 -0
  63. data/lib/aidp/harness/ui/status_widget.rb +280 -0
  64. data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
  65. data/lib/aidp/harness/user_interface.rb +2381 -0
  66. data/lib/aidp/provider_manager.rb +131 -7
  67. data/lib/aidp/providers/anthropic.rb +28 -109
  68. data/lib/aidp/providers/base.rb +170 -0
  69. data/lib/aidp/providers/cursor.rb +52 -183
  70. data/lib/aidp/providers/gemini.rb +24 -109
  71. data/lib/aidp/providers/macos_ui.rb +99 -5
  72. data/lib/aidp/providers/opencode.rb +194 -0
  73. data/lib/aidp/storage/csv_storage.rb +172 -0
  74. data/lib/aidp/storage/file_manager.rb +214 -0
  75. data/lib/aidp/storage/json_storage.rb +140 -0
  76. data/lib/aidp/version.rb +1 -1
  77. data/lib/aidp.rb +56 -35
  78. data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
  79. data/templates/COMMON/AGENT_BASE.md +11 -0
  80. data/templates/EXECUTE/00_PRD.md +4 -4
  81. data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
  82. data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
  83. data/templates/EXECUTE/08_TASKS.md +4 -4
  84. data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
  85. data/templates/README.md +279 -0
  86. data/templates/aidp-development.yml.example +373 -0
  87. data/templates/aidp-minimal.yml.example +48 -0
  88. data/templates/aidp-production.yml.example +475 -0
  89. data/templates/aidp.yml.example +598 -0
  90. metadata +106 -64
  91. data/lib/aidp/analyze/agent_personas.rb +0 -71
  92. data/lib/aidp/analyze/agent_tool_executor.rb +0 -445
  93. data/lib/aidp/analyze/data_retention_manager.rb +0 -426
  94. data/lib/aidp/analyze/database.rb +0 -260
  95. data/lib/aidp/analyze/dependencies.rb +0 -335
  96. data/lib/aidp/analyze/export_manager.rb +0 -425
  97. data/lib/aidp/analyze/focus_guidance.rb +0 -517
  98. data/lib/aidp/analyze/incremental_analyzer.rb +0 -543
  99. data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
  100. data/lib/aidp/analyze/large_analysis_progress.rb +0 -504
  101. data/lib/aidp/analyze/memory_manager.rb +0 -365
  102. data/lib/aidp/analyze/metrics_storage.rb +0 -336
  103. data/lib/aidp/analyze/parallel_processor.rb +0 -460
  104. data/lib/aidp/analyze/performance_optimizer.rb +0 -694
  105. data/lib/aidp/analyze/repository_chunker.rb +0 -704
  106. data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
  107. data/lib/aidp/analyze/storage.rb +0 -662
  108. data/lib/aidp/analyze/tool_configuration.rb +0 -456
  109. data/lib/aidp/analyze/tool_modernization.rb +0 -750
  110. data/lib/aidp/database/pg_adapter.rb +0 -148
  111. data/lib/aidp/database_config.rb +0 -69
  112. data/lib/aidp/database_connection.rb +0 -72
  113. data/lib/aidp/database_migration.rb +0 -158
  114. data/lib/aidp/job_manager.rb +0 -41
  115. data/lib/aidp/jobs/base_job.rb +0 -47
  116. data/lib/aidp/jobs/provider_execution_job.rb +0 -96
  117. data/lib/aidp/project_detector.rb +0 -117
  118. data/lib/aidp/providers/agent_supervisor.rb +0 -348
  119. data/lib/aidp/providers/supervised_base.rb +0 -317
  120. data/lib/aidp/providers/supervised_cursor.rb +0 -22
  121. data/lib/aidp/sync.rb +0 -13
  122. 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