aidp 0.1.0 → 0.5.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +59 -4
  3. data/bin/aidp +2 -2
  4. data/lib/aidp/analyze/agent_personas.rb +1 -1
  5. data/lib/aidp/analyze/data_retention_manager.rb +2 -2
  6. data/lib/aidp/analyze/database.rb +99 -82
  7. data/lib/aidp/analyze/error_handler.rb +12 -76
  8. data/lib/aidp/analyze/focus_guidance.rb +2 -2
  9. data/lib/aidp/analyze/large_analysis_progress.rb +2 -2
  10. data/lib/aidp/analyze/metrics_storage.rb +336 -0
  11. data/lib/aidp/analyze/prioritizer.rb +4 -4
  12. data/lib/aidp/analyze/repository_chunker.rb +15 -13
  13. data/lib/aidp/analyze/ruby_maat_integration.rb +6 -102
  14. data/lib/aidp/analyze/runner.rb +107 -191
  15. data/lib/aidp/analyze/steps.rb +29 -30
  16. data/lib/aidp/analyze/storage.rb +234 -172
  17. data/lib/aidp/cli/jobs_command.rb +489 -0
  18. data/lib/aidp/cli/terminal_io.rb +52 -0
  19. data/lib/aidp/cli.rb +227 -0
  20. data/lib/aidp/config.rb +33 -0
  21. data/lib/aidp/core_ext/class_attribute.rb +36 -0
  22. data/lib/aidp/database/pg_adapter.rb +148 -0
  23. data/lib/aidp/database_config.rb +69 -0
  24. data/lib/aidp/database_connection.rb +72 -0
  25. data/lib/aidp/database_migration.rb +158 -0
  26. data/lib/aidp/execute/runner.rb +65 -92
  27. data/lib/aidp/execute/steps.rb +81 -82
  28. data/lib/aidp/job_manager.rb +41 -0
  29. data/lib/aidp/jobs/base_job.rb +47 -0
  30. data/lib/aidp/jobs/provider_execution_job.rb +96 -0
  31. data/lib/aidp/project_detector.rb +117 -0
  32. data/lib/aidp/provider_manager.rb +25 -0
  33. data/lib/aidp/providers/agent_supervisor.rb +348 -0
  34. data/lib/aidp/providers/anthropic.rb +187 -0
  35. data/lib/aidp/providers/base.rb +162 -0
  36. data/lib/aidp/providers/cursor.rb +304 -0
  37. data/lib/aidp/providers/gemini.rb +187 -0
  38. data/lib/aidp/providers/macos_ui.rb +24 -0
  39. data/lib/aidp/providers/supervised_base.rb +317 -0
  40. data/lib/aidp/providers/supervised_cursor.rb +22 -0
  41. data/lib/aidp/sync.rb +13 -0
  42. data/lib/aidp/util.rb +39 -0
  43. data/lib/aidp/{shared/version.rb → version.rb} +1 -3
  44. data/lib/aidp/workspace.rb +19 -0
  45. data/lib/aidp.rb +36 -45
  46. data/templates/ANALYZE/01_REPOSITORY_ANALYSIS.md +4 -4
  47. metadata +89 -45
  48. data/lib/aidp/shared/cli.rb +0 -117
  49. data/lib/aidp/shared/config.rb +0 -35
  50. data/lib/aidp/shared/project_detector.rb +0 -119
  51. data/lib/aidp/shared/providers/anthropic.rb +0 -26
  52. data/lib/aidp/shared/providers/base.rb +0 -17
  53. data/lib/aidp/shared/providers/cursor.rb +0 -102
  54. data/lib/aidp/shared/providers/gemini.rb +0 -26
  55. data/lib/aidp/shared/providers/macos_ui.rb +0 -26
  56. data/lib/aidp/shared/sync.rb +0 -15
  57. data/lib/aidp/shared/util.rb +0 -41
  58. data/lib/aidp/shared/workspace.rb +0 -21
data/lib/aidp/cli.rb ADDED
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Aidp
6
+ # CLI interface for both execute and analyze modes
7
+ class CLI < Thor
8
+ desc "execute [STEP]", "Run execute mode step(s)"
9
+ option :force, type: :boolean, desc: "Force execution even if dependencies are not met"
10
+ option :rerun, type: :boolean, desc: "Re-run a completed step"
11
+ option :approve, type: :string, desc: "Approve a completed execute gate step"
12
+ option :reset, type: :boolean, desc: "Reset execute mode progress"
13
+ def execute(project_dir = Dir.pwd, step_name = nil, custom_options = {})
14
+ # Handle reset flag
15
+ if options[:reset] || options["reset"]
16
+ progress = Aidp::Execute::Progress.new(project_dir)
17
+ progress.reset
18
+ puts "🔄 Reset execute mode progress"
19
+ return {status: "success", message: "Progress reset"}
20
+ end
21
+
22
+ # Handle approve flag
23
+ if options[:approve] || options["approve"]
24
+ step_name = options[:approve] || options["approve"]
25
+ progress = Aidp::Execute::Progress.new(project_dir)
26
+ progress.mark_step_completed(step_name)
27
+ puts "✅ Approved execute step: #{step_name}"
28
+ return {status: "success", step: step_name}
29
+ end
30
+
31
+ if step_name
32
+ runner = Aidp::Execute::Runner.new(project_dir)
33
+ # Merge Thor options with custom options
34
+ all_options = options.merge(custom_options)
35
+ runner.run_step(step_name, all_options)
36
+ else
37
+ puts "Available execute steps:"
38
+ Aidp::Execute::Steps::SPEC.keys.each { |step| puts " - #{step}" }
39
+ progress = Aidp::Execute::Progress.new(project_dir)
40
+ next_step = progress.next_step
41
+ {status: "success", message: "Available steps listed", next_step: next_step}
42
+ end
43
+ end
44
+
45
+ desc "analyze [STEP]", "Run analyze mode step(s)"
46
+ long_desc <<~DESC
47
+ Run analyze mode steps. STEP can be:
48
+ - A full step name (e.g., 01_REPOSITORY_ANALYSIS)
49
+ - A step number (e.g., 01, 02, 03)
50
+ - 'next' to run the next unfinished step
51
+ - 'current' to run the current step
52
+ - Empty to list available steps
53
+ DESC
54
+ option :force, type: :boolean, desc: "Force execution even if dependencies are not met"
55
+ option :rerun, type: :boolean, desc: "Re-run a completed step"
56
+ option :background, type: :boolean, desc: "Run analysis in background jobs (requires database setup)"
57
+ option :approve, type: :string, desc: "Approve a completed analyze gate step"
58
+ option :reset, type: :boolean, desc: "Reset analyze mode progress"
59
+ def analyze(*args)
60
+ # Handle reset flag
61
+ if options[:reset] || options["reset"]
62
+ project_dir = Dir.pwd
63
+ progress = Aidp::Analyze::Progress.new(project_dir)
64
+ progress.reset
65
+ puts "🔄 Reset analyze mode progress"
66
+ return {status: "success", message: "Progress reset"}
67
+ end
68
+
69
+ # Handle approve flag
70
+ if options[:approve] || options["approve"]
71
+ project_dir = Dir.pwd
72
+ step_name = options[:approve] || options["approve"]
73
+ progress = Aidp::Analyze::Progress.new(project_dir)
74
+ progress.mark_step_completed(step_name)
75
+ puts "✅ Approved analyze step: #{step_name}"
76
+ return {status: "success", step: step_name}
77
+ end
78
+
79
+ # Handle both old and new calling patterns for backwards compatibility
80
+ case args.length
81
+ when 0
82
+ # analyze() - list steps
83
+ project_dir = Dir.pwd
84
+ step_name = nil
85
+ custom_options = {}
86
+ when 1
87
+ # analyze(step_name) - new CLI pattern
88
+ if args[0].is_a?(String) && Dir.exist?(args[0])
89
+ # analyze(project_dir) - old test pattern
90
+ project_dir = args[0]
91
+ step_name = nil
92
+ else
93
+ # analyze(step_name) - new CLI pattern
94
+ project_dir = Dir.pwd
95
+ step_name = args[0]
96
+ end
97
+ custom_options = {}
98
+ when 2
99
+ # analyze(project_dir, step_name) - old test pattern
100
+ # or analyze(step_name, options) - new CLI pattern
101
+ if Dir.exist?(args[0])
102
+ # analyze(project_dir, step_name)
103
+ project_dir = args[0]
104
+ step_name = args[1]
105
+ custom_options = {}
106
+ else
107
+ # analyze(step_name, options)
108
+ project_dir = Dir.pwd
109
+ step_name = args[0]
110
+ custom_options = args[1] || {}
111
+ end
112
+ when 3
113
+ # analyze(project_dir, step_name, options) - old test pattern
114
+ project_dir = args[0]
115
+ step_name = args[1]
116
+ custom_options = args[2] || {}
117
+ else
118
+ raise ArgumentError, "Wrong number of arguments (given #{args.length}, expected 0..3)"
119
+ end
120
+
121
+ progress = Aidp::Analyze::Progress.new(project_dir)
122
+
123
+ if step_name
124
+ # Resolve the step name
125
+ resolved_step = resolve_analyze_step(step_name, progress)
126
+
127
+ if resolved_step
128
+ runner = Aidp::Analyze::Runner.new(project_dir)
129
+ # Merge Thor options with custom options
130
+ all_options = options.merge(custom_options)
131
+ result = runner.run_step(resolved_step, all_options)
132
+
133
+ # Display the result
134
+ if result[:status] == "completed"
135
+ puts "✅ Step '#{resolved_step}' completed successfully"
136
+ puts " Provider: #{result[:provider]}"
137
+ puts " Message: #{result[:message]}" if result[:message]
138
+ elsif result[:status] == "error"
139
+ puts "❌ Step '#{resolved_step}' failed"
140
+ puts " Error: #{result[:error]}" if result[:error]
141
+ end
142
+
143
+ result
144
+ else
145
+ puts "❌ Step '#{step_name}' not found or not available"
146
+ puts "\nAvailable steps:"
147
+ Aidp::Analyze::Steps::SPEC.keys.each_with_index do |step, index|
148
+ status = progress.step_completed?(step) ? "✅" : "⏳"
149
+ puts " #{status} #{sprintf("%02d", index + 1)}: #{step}"
150
+ end
151
+ {status: "error", message: "Step not found"}
152
+ end
153
+ else
154
+ puts "Available analyze steps:"
155
+ Aidp::Analyze::Steps::SPEC.keys.each_with_index do |step, index|
156
+ status = progress.step_completed?(step) ? "✅" : "⏳"
157
+ puts " #{status} #{sprintf("%02d", index + 1)}: #{step}"
158
+ end
159
+
160
+ next_step = progress.next_step
161
+ if next_step
162
+ puts "\n💡 Run 'aidp analyze next' or 'aidp analyze #{next_step.match(/^(\d+)/)[1]}' to run the next step"
163
+ end
164
+
165
+ {status: "success", message: "Available steps listed", next_step: next_step,
166
+ completed_steps: progress.completed_steps}
167
+ end
168
+ end
169
+
170
+ desc "status", "Show current progress for both modes"
171
+ def status
172
+ puts "\n📊 AI Dev Pipeline Status"
173
+ puts "=" * 50
174
+
175
+ # Execute mode status
176
+ execute_progress = Aidp::Execute::Progress.new(Dir.pwd)
177
+ puts "\n🔧 Execute Mode:"
178
+ Aidp::Execute::Steps::SPEC.keys.each do |step|
179
+ status = execute_progress.step_completed?(step) ? "✅" : "⏳"
180
+ puts " #{status} #{step}"
181
+ end
182
+
183
+ # Analyze mode status
184
+ analyze_progress = Aidp::Analyze::Progress.new(Dir.pwd)
185
+ puts "\n🔍 Analyze Mode:"
186
+ Aidp::Analyze::Steps::SPEC.keys.each do |step|
187
+ status = analyze_progress.step_completed?(step) ? "✅" : "⏳"
188
+ puts " #{status} #{step}"
189
+ end
190
+ end
191
+
192
+ desc "jobs", "Show and manage background jobs"
193
+ def jobs
194
+ require_relative "cli/jobs_command"
195
+ command = Aidp::CLI::JobsCommand.new
196
+ command.run
197
+ end
198
+
199
+ desc "version", "Show version information"
200
+ def version
201
+ puts "Aidp version #{Aidp::VERSION}"
202
+ end
203
+
204
+ private
205
+
206
+ def resolve_analyze_step(step_input, progress)
207
+ step_input = step_input.to_s.downcase.strip
208
+
209
+ case step_input
210
+ when "next"
211
+ progress.next_step
212
+ when "current"
213
+ progress.current_step || progress.next_step
214
+ else
215
+ # Check if it's a step number (e.g., "01", "02", "1", "2")
216
+ if step_input.match?(/^\d{1,2}$/)
217
+ step_number = sprintf("%02d", step_input.to_i)
218
+ # Find step that starts with this number
219
+ Aidp::Analyze::Steps::SPEC.keys.find { |step| step.start_with?(step_number) }
220
+ else
221
+ # Check if it's a full step name (case insensitive)
222
+ Aidp::Analyze::Steps::SPEC.keys.find { |step| step.downcase == step_input }
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Aidp
6
+ # Configuration management for both execute and analyze modes
7
+ class Config
8
+ def self.load(project_dir = Dir.pwd)
9
+ config_file = File.join(project_dir, ".aidp.yml")
10
+ if File.exist?(config_file)
11
+ YAML.load_file(config_file) || {}
12
+ else
13
+ {}
14
+ end
15
+ end
16
+
17
+ def self.templates_root
18
+ File.join(Dir.pwd, "templates")
19
+ end
20
+
21
+ def self.analyze_templates_root
22
+ File.join(Dir.pwd, "templates", "ANALYZE")
23
+ end
24
+
25
+ def self.execute_templates_root
26
+ File.join(Dir.pwd, "templates", "EXECUTE")
27
+ end
28
+
29
+ def self.common_templates_root
30
+ File.join(Dir.pwd, "templates", "COMMON")
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module CoreExt
5
+ module ClassAttribute
6
+ def class_attribute(*attrs)
7
+ attrs.each do |name|
8
+ # Define class instance variable
9
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
10
+ @#{name} = nil
11
+
12
+ def self.#{name}
13
+ return @#{name} if defined?(@#{name})
14
+ return superclass.#{name} if superclass.respond_to?(:#{name})
15
+ nil
16
+ end
17
+
18
+ def self.#{name}=(val)
19
+ @#{name} = val
20
+ end
21
+
22
+ def #{name}
23
+ self.class.#{name}
24
+ end
25
+
26
+ def #{name}=(val)
27
+ raise "#{name} is a class attribute, cannot be set on instance"
28
+ end
29
+ RUBY
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ Class.include Aidp::CoreExt::ClassAttribute
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Database
5
+ class PgAdapter
6
+ def initialize(connection)
7
+ @connection = connection
8
+ end
9
+
10
+ def execute(sql, params = [])
11
+ result = @connection.exec_params(sql, params)
12
+ result.to_a.map { |row| row.transform_keys(&:to_sym) }
13
+ end
14
+
15
+ def in_transaction?
16
+ @connection.transaction_status != PG::PQTRANS_IDLE
17
+ end
18
+
19
+ def checkout
20
+ yield self
21
+ end
22
+
23
+ def after_commit
24
+ yield
25
+ end
26
+
27
+ def server_version
28
+ @connection.server_version
29
+ end
30
+
31
+ def transaction_status
32
+ @connection.transaction_status
33
+ end
34
+
35
+ def transaction
36
+ @connection.transaction do
37
+ yield
38
+ end
39
+ end
40
+
41
+ def quote_table_name(name)
42
+ "\"#{name}\""
43
+ end
44
+
45
+ def quote_identifier(name)
46
+ "\"#{name}\""
47
+ end
48
+
49
+ def quote_string(string)
50
+ "'#{string.gsub("'", "''")}'"
51
+ end
52
+
53
+ def quote_date(date)
54
+ date.strftime("%Y-%m-%d")
55
+ end
56
+
57
+ def quote_time(time)
58
+ time.strftime("%Y-%m-%d %H:%M:%S.%6N %z")
59
+ end
60
+
61
+ # Additional methods required by Que
62
+ def async_connection
63
+ self
64
+ end
65
+
66
+ def wait_for_notify(timeout = nil)
67
+ @connection.wait_for_notify(timeout)
68
+ end
69
+
70
+ def listen(channel)
71
+ @connection.exec("LISTEN #{quote_identifier(channel)}")
72
+ end
73
+
74
+ def unlisten(channel)
75
+ @connection.exec("UNLISTEN #{quote_identifier(channel)}")
76
+ end
77
+
78
+ def unlisten_all
79
+ @connection.exec("UNLISTEN *")
80
+ end
81
+
82
+ def notifications
83
+ @connection.notifications
84
+ end
85
+
86
+ def reset
87
+ @connection.reset
88
+ end
89
+
90
+ def type_map_for_queries
91
+ @connection.type_map_for_queries
92
+ end
93
+
94
+ def type_map_for_results
95
+ @connection.type_map_for_results
96
+ end
97
+
98
+ # Additional methods for Que compatibility
99
+ def adapter_name
100
+ "pg"
101
+ end
102
+
103
+ def active?
104
+ @connection.status == PG::CONNECTION_OK
105
+ end
106
+
107
+ def disconnect!
108
+ @connection.close
109
+ end
110
+
111
+ def reconnect!
112
+ @connection.reset
113
+ end
114
+
115
+ def raw_connection
116
+ @connection
117
+ end
118
+
119
+ def schema_search_path
120
+ execute("SHOW search_path")[0][:search_path]
121
+ end
122
+
123
+ def schema_search_path=(path)
124
+ execute("SET search_path TO #{path}")
125
+ end
126
+
127
+ def table_exists?(name)
128
+ execute(<<~SQL, [name]).any?
129
+ SELECT 1
130
+ FROM pg_tables
131
+ WHERE tablename = $1
132
+ SQL
133
+ end
134
+
135
+ def advisory_lock(id)
136
+ execute("SELECT pg_advisory_lock($1)", [id])
137
+ end
138
+
139
+ def advisory_unlock(id)
140
+ execute("SELECT pg_advisory_unlock($1)", [id])
141
+ end
142
+
143
+ def advisory_unlock_all
144
+ execute("SELECT pg_advisory_unlock_all()")
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+
6
+ module Aidp
7
+ class DatabaseConfig
8
+ DEFAULT_CONFIG = {
9
+ "database" => {
10
+ "adapter" => "postgresql",
11
+ "host" => "localhost",
12
+ "port" => 5432,
13
+ "database" => "aidp",
14
+ "username" => ENV["USER"],
15
+ "password" => nil,
16
+ "pool" => 5,
17
+ "timeout" => 5000
18
+ }
19
+ }.freeze
20
+
21
+ def self.load(project_dir = Dir.pwd)
22
+ new(project_dir).load
23
+ end
24
+
25
+ def initialize(project_dir)
26
+ @project_dir = project_dir
27
+ @config_file = File.join(project_dir, ".aidp-config.yml")
28
+ end
29
+
30
+ def load
31
+ ensure_config_exists
32
+ config = YAML.load_file(@config_file)
33
+ validate_config(config)
34
+ config["database"]
35
+ end
36
+
37
+ private
38
+
39
+ def ensure_config_exists
40
+ return if File.exist?(@config_file)
41
+
42
+ # Create config directory if it doesn't exist
43
+ FileUtils.mkdir_p(File.dirname(@config_file))
44
+
45
+ # Write default config
46
+ File.write(@config_file, YAML.dump(DEFAULT_CONFIG))
47
+
48
+ puts "Created default database configuration at #{@config_file}"
49
+ puts "Please update the configuration with your database settings"
50
+ end
51
+
52
+ def validate_config(config)
53
+ unless config.is_a?(Hash) && config["database"].is_a?(Hash)
54
+ raise "Invalid configuration format in #{@config_file}"
55
+ end
56
+
57
+ required_keys = %w[adapter host port database username]
58
+ missing_keys = required_keys - config["database"].keys
59
+
60
+ unless missing_keys.empty?
61
+ raise "Missing required configuration keys: #{missing_keys.join(", ")}"
62
+ end
63
+
64
+ unless config["database"]["adapter"] == "postgresql"
65
+ raise "Only PostgreSQL is supported as a database adapter"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg"
4
+ require "que"
5
+ require "sequel"
6
+
7
+ module Aidp
8
+ class DatabaseConnection
9
+ class << self
10
+ def initialize_mutex
11
+ @mutex ||= Mutex.new
12
+ end
13
+
14
+ def establish_connection
15
+ initialize_mutex
16
+ @mutex.synchronize do
17
+ # Return existing connection if already established
18
+ return @connection if @connection && !@connection.finished?
19
+ @connection = PG.connect(connection_params)
20
+ @sequel_db = Sequel.connect(
21
+ adapter: "postgres",
22
+ host: ENV["AIDP_DB_HOST"] || "localhost",
23
+ port: (ENV["AIDP_DB_PORT"] || 5432).to_i,
24
+ database: ENV["AIDP_DB_NAME"] || "aidp",
25
+ user: ENV["AIDP_DB_USER"] || ENV["USER"],
26
+ password: ENV["AIDP_DB_PASSWORD"]
27
+ )
28
+ Que.connection = @sequel_db
29
+ Que.migrate!(version: Que::Migrations::CURRENT_VERSION)
30
+ @connection
31
+ end
32
+ end
33
+
34
+ def connection
35
+ return @connection if @connection && !@connection.finished?
36
+ establish_connection
37
+ end
38
+
39
+ def disconnect
40
+ initialize_mutex
41
+ @mutex.synchronize do
42
+ return unless @connection
43
+
44
+ # Safely disconnect in reverse order
45
+ begin
46
+ Que.connection = nil
47
+ rescue => e
48
+ # Log but don't fail on Que disconnection issues
49
+ puts "Warning: Error setting Que.connection to nil: #{e.message}" if ENV["AIDP_DEBUG"]
50
+ end
51
+
52
+ @sequel_db&.disconnect
53
+ @connection&.close
54
+ @connection = nil
55
+ @sequel_db = nil
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def connection_params
62
+ {
63
+ host: ENV["AIDP_DB_HOST"] || "localhost",
64
+ port: (ENV["AIDP_DB_PORT"] || 5432).to_i,
65
+ dbname: ENV["AIDP_DB_NAME"] || "aidp",
66
+ user: ENV["AIDP_DB_USER"] || ENV["USER"],
67
+ password: ENV["AIDP_DB_PASSWORD"]
68
+ }
69
+ end
70
+ end
71
+ end
72
+ end