aidp 0.3.0 → 0.7.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +191 -5
  3. data/lib/aidp/analysis/kb_inspector.rb +456 -0
  4. data/lib/aidp/analysis/seams.rb +188 -0
  5. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +493 -0
  6. data/lib/aidp/analysis/tree_sitter_scan.rb +703 -0
  7. data/lib/aidp/analyze/agent_personas.rb +1 -1
  8. data/lib/aidp/analyze/agent_tool_executor.rb +5 -11
  9. data/lib/aidp/analyze/data_retention_manager.rb +0 -5
  10. data/lib/aidp/analyze/database.rb +99 -82
  11. data/lib/aidp/analyze/error_handler.rb +12 -79
  12. data/lib/aidp/analyze/export_manager.rb +0 -7
  13. data/lib/aidp/analyze/focus_guidance.rb +2 -2
  14. data/lib/aidp/analyze/incremental_analyzer.rb +1 -11
  15. data/lib/aidp/analyze/large_analysis_progress.rb +0 -5
  16. data/lib/aidp/analyze/memory_manager.rb +34 -60
  17. data/lib/aidp/analyze/metrics_storage.rb +336 -0
  18. data/lib/aidp/analyze/parallel_processor.rb +0 -6
  19. data/lib/aidp/analyze/performance_optimizer.rb +0 -3
  20. data/lib/aidp/analyze/prioritizer.rb +2 -2
  21. data/lib/aidp/analyze/repository_chunker.rb +14 -21
  22. data/lib/aidp/analyze/ruby_maat_integration.rb +6 -102
  23. data/lib/aidp/analyze/runner.rb +107 -191
  24. data/lib/aidp/analyze/steps.rb +35 -30
  25. data/lib/aidp/analyze/storage.rb +233 -178
  26. data/lib/aidp/analyze/tool_configuration.rb +21 -36
  27. data/lib/aidp/cli/jobs_command.rb +489 -0
  28. data/lib/aidp/cli/terminal_io.rb +52 -0
  29. data/lib/aidp/cli.rb +160 -45
  30. data/lib/aidp/core_ext/class_attribute.rb +36 -0
  31. data/lib/aidp/database/pg_adapter.rb +148 -0
  32. data/lib/aidp/database_config.rb +69 -0
  33. data/lib/aidp/database_connection.rb +72 -0
  34. data/lib/aidp/execute/runner.rb +65 -92
  35. data/lib/aidp/execute/steps.rb +81 -82
  36. data/lib/aidp/job_manager.rb +41 -0
  37. data/lib/aidp/jobs/base_job.rb +45 -0
  38. data/lib/aidp/jobs/provider_execution_job.rb +83 -0
  39. data/lib/aidp/provider_manager.rb +25 -0
  40. data/lib/aidp/providers/agent_supervisor.rb +348 -0
  41. data/lib/aidp/providers/anthropic.rb +160 -3
  42. data/lib/aidp/providers/base.rb +153 -6
  43. data/lib/aidp/providers/cursor.rb +245 -43
  44. data/lib/aidp/providers/gemini.rb +164 -3
  45. data/lib/aidp/providers/supervised_base.rb +317 -0
  46. data/lib/aidp/providers/supervised_cursor.rb +22 -0
  47. data/lib/aidp/version.rb +1 -1
  48. data/lib/aidp.rb +31 -34
  49. data/templates/ANALYZE/01_REPOSITORY_ANALYSIS.md +4 -4
  50. data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
  51. metadata +91 -36
data/lib/aidp/cli.rb CHANGED
@@ -8,7 +8,26 @@ module Aidp
8
8
  desc "execute [STEP]", "Run execute mode step(s)"
9
9
  option :force, type: :boolean, desc: "Force execution even if dependencies are not met"
10
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"
11
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
+
12
31
  if step_name
13
32
  runner = Aidp::Execute::Runner.new(project_dir)
14
33
  # Merge Thor options with custom options
@@ -34,7 +53,71 @@ module Aidp
34
53
  DESC
35
54
  option :force, type: :boolean, desc: "Force execution even if dependencies are not met"
36
55
  option :rerun, type: :boolean, desc: "Re-run a completed step"
37
- def analyze(project_dir = Dir.pwd, step_name = nil, custom_options = {})
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
+
38
121
  progress = Aidp::Analyze::Progress.new(project_dir)
39
122
 
40
123
  if step_name
@@ -45,7 +128,19 @@ module Aidp
45
128
  runner = Aidp::Analyze::Runner.new(project_dir)
46
129
  # Merge Thor options with custom options
47
130
  all_options = options.merge(custom_options)
48
- runner.run_step(resolved_step, all_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
49
144
  else
50
145
  puts "❌ Step '#{step_name}' not found or not available"
51
146
  puts "\nAvailable steps:"
@@ -72,49 +167,6 @@ module Aidp
72
167
  end
73
168
  end
74
169
 
75
- desc "analyze-approve STEP", "Approve a completed analyze gate step"
76
- def analyze_approve(project_dir = Dir.pwd, step_name = nil)
77
- progress = Aidp::Analyze::Progress.new(project_dir)
78
- progress.mark_step_completed(step_name)
79
- puts "✅ Approved analyze step: #{step_name}"
80
- {status: "success", step: step_name}
81
- end
82
-
83
- desc "analyze-reset", "Reset analyze mode progress"
84
- def analyze_reset(project_dir = Dir.pwd)
85
- progress = Aidp::Analyze::Progress.new(project_dir)
86
- progress.reset
87
- puts "🔄 Reset analyze mode progress"
88
- {status: "success", message: "Progress reset"}
89
- end
90
-
91
- desc "execute-approve STEP", "Approve a completed execute gate step"
92
- def execute_approve(project_dir = Dir.pwd, step_name = nil)
93
- progress = Aidp::Execute::Progress.new(project_dir)
94
- progress.mark_step_completed(step_name)
95
- puts "✅ Approved execute step: #{step_name}"
96
- {status: "success", step: step_name}
97
- end
98
-
99
- desc "execute-reset", "Reset execute mode progress"
100
- def execute_reset(project_dir = Dir.pwd)
101
- progress = Aidp::Execute::Progress.new(project_dir)
102
- progress.reset
103
- puts "🔄 Reset execute mode progress"
104
- {status: "success", message: "Progress reset"}
105
- end
106
-
107
- # Backward compatibility aliases
108
- desc "approve STEP", "Approve a completed execute gate step (alias for execute-approve)"
109
- def approve(project_dir = Dir.pwd, step_name = nil)
110
- execute_approve(project_dir, step_name)
111
- end
112
-
113
- desc "reset", "Reset execute mode progress (alias for execute-reset)"
114
- def reset(project_dir = Dir.pwd)
115
- execute_reset(project_dir)
116
- end
117
-
118
170
  desc "status", "Show current progress for both modes"
119
171
  def status
120
172
  puts "\n📊 AI Dev Pipeline Status"
@@ -137,6 +189,69 @@ module Aidp
137
189
  end
138
190
  end
139
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 "analyze code", "Run Tree-sitter static analysis to build knowledge base"
200
+ option :langs, type: :string, desc: "Comma-separated list of languages to analyze (default: ruby)"
201
+ option :threads, type: :numeric, desc: "Number of threads for parallel processing (default: CPU count)"
202
+ option :rebuild, type: :boolean, desc: "Rebuild knowledge base from scratch"
203
+ option :kb_dir, type: :string, desc: "Knowledge base directory (default: .aidp/kb)"
204
+ def analyze_code
205
+ require_relative "analysis/tree_sitter_scan"
206
+
207
+ langs = options[:langs] ? options[:langs].split(",").map(&:strip) : %w[ruby]
208
+ threads = options[:threads] || Etc.nprocessors
209
+ kb_dir = options[:kb_dir] || ".aidp/kb"
210
+
211
+ if options[:rebuild]
212
+ kb_path = File.expand_path(kb_dir, Dir.pwd)
213
+ FileUtils.rm_rf(kb_path) if File.exist?(kb_path)
214
+ puts "🗑️ Rebuilt knowledge base directory"
215
+ end
216
+
217
+ scanner = Aidp::Analysis::TreeSitterScan.new(
218
+ root: Dir.pwd,
219
+ kb_dir: kb_dir,
220
+ langs: langs,
221
+ threads: threads
222
+ )
223
+
224
+ scanner.run
225
+ end
226
+
227
+ desc "kb show [TYPE]", "Show knowledge base contents"
228
+ option :format, type: :string, desc: "Output format (json, table, summary)"
229
+ option :kb_dir, type: :string, desc: "Knowledge base directory (default: .aidp/kb)"
230
+ def kb_show(type = "summary")
231
+ require_relative "analysis/kb_inspector"
232
+
233
+ kb_dir = options[:kb_dir] || ".aidp/kb"
234
+ format = options[:format] || "summary"
235
+
236
+ inspector = Aidp::Analysis::KBInspector.new(kb_dir)
237
+ inspector.show(type, format: format)
238
+ end
239
+
240
+ desc "kb graph [TYPE]", "Generate graph visualization from knowledge base"
241
+ option :format, type: :string, desc: "Graph format (dot, json, mermaid)"
242
+ option :output, type: :string, desc: "Output file path"
243
+ option :kb_dir, type: :string, desc: "Knowledge base directory (default: .aidp/kb)"
244
+ def kb_graph(type = "imports")
245
+ require_relative "analysis/kb_inspector"
246
+
247
+ kb_dir = options[:kb_dir] || ".aidp/kb"
248
+ format = options[:format] || "dot"
249
+ output = options[:output]
250
+
251
+ inspector = Aidp::Analysis::KBInspector.new(kb_dir)
252
+ inspector.generate_graph(type, format: format, output: output)
253
+ end
254
+
140
255
  desc "version", "Show version information"
141
256
  def version
142
257
  puts "Aidp version #{Aidp::VERSION}"
@@ -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