aidp 0.3.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +59 -4
  3. data/lib/aidp/analyze/agent_personas.rb +1 -1
  4. data/lib/aidp/analyze/database.rb +99 -82
  5. data/lib/aidp/analyze/error_handler.rb +12 -76
  6. data/lib/aidp/analyze/focus_guidance.rb +2 -2
  7. data/lib/aidp/analyze/metrics_storage.rb +336 -0
  8. data/lib/aidp/analyze/prioritizer.rb +2 -2
  9. data/lib/aidp/analyze/ruby_maat_integration.rb +6 -102
  10. data/lib/aidp/analyze/runner.rb +107 -191
  11. data/lib/aidp/analyze/steps.rb +29 -30
  12. data/lib/aidp/analyze/storage.rb +233 -171
  13. data/lib/aidp/cli/jobs_command.rb +489 -0
  14. data/lib/aidp/cli/terminal_io.rb +52 -0
  15. data/lib/aidp/cli.rb +104 -45
  16. data/lib/aidp/core_ext/class_attribute.rb +36 -0
  17. data/lib/aidp/database/pg_adapter.rb +148 -0
  18. data/lib/aidp/database_config.rb +69 -0
  19. data/lib/aidp/database_connection.rb +72 -0
  20. data/lib/aidp/database_migration.rb +158 -0
  21. data/lib/aidp/execute/runner.rb +65 -92
  22. data/lib/aidp/execute/steps.rb +81 -82
  23. data/lib/aidp/job_manager.rb +41 -0
  24. data/lib/aidp/jobs/base_job.rb +47 -0
  25. data/lib/aidp/jobs/provider_execution_job.rb +96 -0
  26. data/lib/aidp/provider_manager.rb +25 -0
  27. data/lib/aidp/providers/agent_supervisor.rb +348 -0
  28. data/lib/aidp/providers/anthropic.rb +166 -3
  29. data/lib/aidp/providers/base.rb +153 -6
  30. data/lib/aidp/providers/cursor.rb +247 -43
  31. data/lib/aidp/providers/gemini.rb +166 -3
  32. data/lib/aidp/providers/supervised_base.rb +317 -0
  33. data/lib/aidp/providers/supervised_cursor.rb +22 -0
  34. data/lib/aidp/version.rb +1 -1
  35. data/lib/aidp.rb +25 -34
  36. data/templates/ANALYZE/01_REPOSITORY_ANALYSIS.md +4 -4
  37. metadata +72 -35
@@ -1,135 +1,108 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "erb"
4
- require "yaml"
5
- require "json"
6
-
7
3
  module Aidp
8
4
  module Execute
9
- # Handles execution logic for execute mode steps
10
5
  class Runner
11
- attr_reader :project_dir, :progress
12
-
13
6
  def initialize(project_dir)
14
7
  @project_dir = project_dir
15
- @progress = Aidp::Execute::Progress.new(project_dir)
16
8
  end
17
9
 
18
- def run_step(step_name, options = {})
19
- raise "Step '#{step_name}' not found in execute mode steps" unless Aidp::Execute::Steps::SPEC.key?(step_name)
10
+ def progress
11
+ @progress ||= Aidp::Execute::Progress.new(@project_dir)
12
+ end
20
13
 
14
+ def run_step(step_name, options = {})
15
+ # Always validate step exists first, even in mock mode
21
16
  step_spec = Aidp::Execute::Steps::SPEC[step_name]
22
- template_name = step_spec["templates"].first
23
-
24
- # Load template
25
- template = find_template(template_name)
26
- raise "Template '#{template_name}' not found" unless template
17
+ raise "Step '#{step_name}' not found" unless step_spec
27
18
 
28
- # Compose prompt
29
- prompt = composed_prompt(template_name, options)
30
-
31
- # Handle error simulation for tests
32
- if options[:simulate_error]
33
- return {
34
- status: "error",
35
- error: options[:simulate_error],
36
- step: step_name
37
- }
19
+ if should_use_mock_mode?(options)
20
+ return options[:simulate_error] ?
21
+ {status: "error", error: options[:simulate_error]} :
22
+ mock_execution_result
38
23
  end
39
24
 
40
- # Execute step (mock for now)
41
- result = {
42
- status: "success",
43
- step: step_name,
44
- output_files: step_spec["outs"],
45
- prompt: prompt
46
- }
25
+ job = Aidp::Jobs::ProviderExecutionJob.enqueue(
26
+ provider_type: "cursor",
27
+ prompt: composed_prompt(step_name, options),
28
+ metadata: {
29
+ step_name: step_name,
30
+ project_dir: @project_dir
31
+ }
32
+ )
47
33
 
48
- # Mark step as completed
49
- @progress.mark_step_completed(step_name)
34
+ wait_for_job_completion(job)
35
+ end
50
36
 
51
- # Generate output files
52
- generate_output_files(step_name, step_spec["outs"], result)
37
+ private
53
38
 
54
- # Generate database export
55
- generate_database_export
39
+ def should_use_mock_mode?(options)
40
+ options[:mock_mode] || ENV["AIDP_MOCK_MODE"] == "1" || ENV["RAILS_ENV"] == "test"
41
+ end
56
42
 
57
- result
43
+ def mock_execution_result
44
+ {
45
+ status: "completed",
46
+ provider: "mock",
47
+ message: "Mock execution",
48
+ output: "Mock execution result"
49
+ }
58
50
  end
59
51
 
60
- private
52
+ def wait_for_job_completion(job_id)
53
+ loop do
54
+ job = Que.execute("SELECT * FROM que_jobs WHERE id = $1", [job_id]).first
55
+ return {status: "completed"} if job && job["finished_at"] && job["error_count"] == 0
56
+ return {status: "failed", error: job["last_error_message"]} if job && job["error_count"] && job["error_count"] > 0
57
+
58
+ if job && job["finished_at"].nil?
59
+ duration = Time.now - job["run_at"]
60
+ minutes = (duration / 60).to_i
61
+ seconds = (duration % 60).to_i
62
+ duration_str = (minutes > 0) ? "#{minutes}m #{seconds}s" : "#{seconds}s"
63
+ print "\r🔄 Job #{job_id} is running (#{duration_str})...".ljust(80)
64
+ else
65
+ print "\r⏳ Job #{job_id} is pending...".ljust(80)
66
+ end
67
+ $stdout.flush
68
+ sleep 1
69
+ end
70
+ ensure
71
+ print "\r" + " " * 80 + "\r"
72
+ end
61
73
 
62
74
  def find_template(template_name)
63
75
  template_search_paths.each do |path|
64
- template_file = File.join(path, template_name)
65
- return File.read(template_file) if File.exist?(template_file)
76
+ template_path = File.join(path, template_name)
77
+ return template_path if File.exist?(template_path)
66
78
  end
67
79
  nil
68
80
  end
69
81
 
70
82
  def template_search_paths
71
83
  [
72
- File.join(@project_dir, "templates"),
73
- File.join(@project_dir, "templates", "COMMON"),
74
- File.join(File.dirname(__FILE__), "..", "..", "..", "templates", "EXECUTE"),
75
- File.join(File.dirname(__FILE__), "..", "..", "..", "templates", "COMMON")
84
+ File.join(@project_dir, "templates", "EXECUTE"),
85
+ File.join(@project_dir, "templates", "COMMON")
76
86
  ]
77
87
  end
78
88
 
79
- def composed_prompt(template_name, options = {})
80
- template = find_template(template_name)
81
- return template unless template
89
+ def composed_prompt(step_name, options = {})
90
+ step_spec = Aidp::Execute::Steps::SPEC[step_name]
91
+ raise "Step '#{step_name}' not found" unless step_spec
92
+
93
+ template_name = step_spec["templates"].first
94
+ template_path = find_template(template_name)
95
+ raise "Template not found for step #{step_name}" unless template_path
82
96
 
83
- # Load agent base template if available
84
- agent_base = find_template("AGENT_BASE.md")
85
- template = "#{agent_base}\n\n#{template}" if agent_base
97
+ template = File.read(template_path)
86
98
 
87
- # Replace placeholders
99
+ # Replace template variables in the format {{key}} with option values
88
100
  options.each do |key, value|
89
101
  template = template.gsub("{{#{key}}}", value.to_s)
90
102
  end
91
103
 
92
104
  template
93
105
  end
94
-
95
- def generate_output_files(step_name, output_files, result)
96
- output_files.each do |output_file|
97
- file_path = File.join(@project_dir, output_file)
98
- content = generate_output_content(step_name, output_file, result)
99
- File.write(file_path, content)
100
- end
101
- end
102
-
103
- def generate_output_content(step_name, output_file, result)
104
- case output_file
105
- when /\.md$/
106
- "# #{step_name} Output\n\nGenerated on #{Time.now}\n\n## Result\n\n#{result[:status]}"
107
- when /\.json$/
108
- result.to_json
109
- else
110
- "Output for #{step_name}: #{result[:status]}"
111
- end
112
- end
113
-
114
- def generate_database_export
115
- database_file = File.join(@project_dir, ".aidp.db")
116
- require "sqlite3"
117
-
118
- begin
119
- db = SQLite3::Database.new(database_file)
120
- db.execute("CREATE TABLE IF NOT EXISTS execute_results (step TEXT, status TEXT, completed_at TEXT)")
121
-
122
- Aidp::Execute::Steps::SPEC.keys.each do |step|
123
- if @progress.step_completed?(step)
124
- db.execute("INSERT INTO execute_results (step, status, completed_at) VALUES (?, ?, ?)",
125
- [step, "success", Time.now.iso8601])
126
- end
127
- end
128
- rescue => e
129
- # Log the error but don't fail the execution
130
- puts "Warning: Database export failed: #{e.message}"
131
- end
132
- end
133
106
  end
134
107
  end
135
108
  end
@@ -2,110 +2,109 @@
2
2
 
3
3
  module Aidp
4
4
  module Execute
5
- # Defines the steps, templates, outputs, and associated AI agents for execute mode
6
- class Steps
5
+ module Steps
7
6
  SPEC = {
8
7
  "00_PRD" => {
9
- "templates" => ["00_PRD.md"],
10
- "outs" => ["00_PRD.md"],
11
- "gate" => false,
12
- "agent" => "Product Manager"
8
+ "templates" => ["prd.md"],
9
+ "description" => "Generate Product Requirements Document",
10
+ "outs" => ["docs/prd.md"],
11
+ "gate" => true
13
12
  },
14
13
  "01_NFRS" => {
15
- "templates" => ["01_NFRS.md"],
16
- "outs" => ["01_NFRS.md"],
17
- "gate" => false,
18
- "agent" => "Architect"
14
+ "templates" => ["nfrs.md"],
15
+ "description" => "Define Non-Functional Requirements",
16
+ "outs" => ["docs/nfrs.md"],
17
+ "gate" => true
19
18
  },
20
19
  "02_ARCHITECTURE" => {
21
- "templates" => ["02_ARCHITECTURE.md"],
22
- "outs" => ["02_ARCHITECTURE.md"],
23
- "gate" => false,
24
- "agent" => "Architect"
20
+ "templates" => ["architecture.md"],
21
+ "description" => "Design System Architecture",
22
+ "outs" => ["docs/architecture.md"],
23
+ "gate" => true
25
24
  },
26
25
  "02A_ARCH_GATE_QUESTIONS" => {
27
- "templates" => ["02A_ARCH_GATE_QUESTIONS.md"],
28
- "outs" => ["02A_ARCH_GATE_QUESTIONS.md"],
29
- "gate" => true,
30
- "agent" => "Architect"
26
+ "templates" => ["arch_gate_questions.md"],
27
+ "description" => "Architecture Gate Questions",
28
+ "outs" => ["docs/arch_gate_questions.md"],
29
+ "gate" => true
31
30
  },
32
31
  "03_ADR_FACTORY" => {
33
- "templates" => ["03_ADR_FACTORY.md"],
34
- "outs" => ["03_ADR_FACTORY.md"],
35
- "gate" => false,
36
- "agent" => "Architect"
32
+ "templates" => ["adr_factory.md"],
33
+ "description" => "Generate Architecture Decision Records",
34
+ "outs" => ["docs/adr/*.md"],
35
+ "gate" => false
37
36
  },
38
37
  "04_DOMAIN_DECOMPOSITION" => {
39
- "templates" => ["04_DOMAIN_DECOMPOSITION.md"],
40
- "outs" => ["04_DOMAIN_DECOMPOSITION.md"],
41
- "gate" => false,
42
- "agent" => "Architect"
43
- },
44
- "05_CONTRACTS" => {
45
- "templates" => ["05_CONTRACTS.md"],
46
- "outs" => ["05_CONTRACTS.md"],
47
- "gate" => false,
48
- "agent" => "Architect"
49
- },
50
- "06_THREAT_MODEL" => {
51
- "templates" => ["06_THREAT_MODEL.md"],
52
- "outs" => ["06_THREAT_MODEL.md"],
53
- "gate" => false,
54
- "agent" => "Security Expert"
55
- },
56
- "07_TEST_PLAN" => {
57
- "templates" => ["07_TEST_PLAN.md"],
58
- "outs" => ["07_TEST_PLAN.md"],
59
- "gate" => false,
60
- "agent" => "Test Engineer"
61
- },
62
- "08_TASKS" => {
63
- "templates" => ["08_TASKS.md"],
64
- "outs" => ["08_TASKS.md"],
65
- "gate" => false,
66
- "agent" => "Project Manager"
67
- },
68
- "09_SCAFFOLDING_DEVEX" => {
69
- "templates" => ["09_SCAFFOLDING_DEVEX.md"],
70
- "outs" => ["09_SCAFFOLDING_DEVEX.md"],
71
- "gate" => false,
72
- "agent" => "DevOps Engineer"
73
- },
74
- "10_IMPLEMENTATION_AGENT" => {
75
- "templates" => ["10_IMPLEMENTATION_AGENT.md"],
76
- "outs" => ["10_IMPLEMENTATION_AGENT.md"],
77
- "gate" => false,
78
- "agent" => "Implementation Specialist"
38
+ "templates" => ["domain_decomposition.md"],
39
+ "description" => "Decompose Domain into Components",
40
+ "outs" => ["docs/domain_decomposition.md"],
41
+ "gate" => true
42
+ },
43
+ "05_API_DESIGN" => {
44
+ "templates" => ["api_design.md"],
45
+ "description" => "Design APIs and Interfaces",
46
+ "outs" => ["docs/api_design.md"],
47
+ "gate" => true
48
+ },
49
+ "06_DATA_MODEL" => {
50
+ "templates" => ["data_model.md"],
51
+ "description" => "Design Data Model",
52
+ "outs" => ["docs/data_model.md"],
53
+ "gate" => true
54
+ },
55
+ "07_SECURITY_REVIEW" => {
56
+ "templates" => ["security_review.md"],
57
+ "description" => "Security Review and Threat Model",
58
+ "outs" => ["docs/security_review.md"],
59
+ "gate" => true
60
+ },
61
+ "08_PERFORMANCE_REVIEW" => {
62
+ "templates" => ["performance_review.md"],
63
+ "description" => "Performance Review and Optimization",
64
+ "outs" => ["docs/performance_review.md"],
65
+ "gate" => true
66
+ },
67
+ "09_RELIABILITY_REVIEW" => {
68
+ "templates" => ["reliability_review.md"],
69
+ "description" => "Reliability Review and SLOs",
70
+ "outs" => ["docs/reliability_review.md"],
71
+ "gate" => true
72
+ },
73
+ "10_TESTING_STRATEGY" => {
74
+ "templates" => ["testing_strategy.md"],
75
+ "description" => "Define Testing Strategy",
76
+ "outs" => ["docs/testing_strategy.md"],
77
+ "gate" => true
79
78
  },
80
79
  "11_STATIC_ANALYSIS" => {
81
- "templates" => ["11_STATIC_ANALYSIS.md"],
82
- "outs" => ["11_STATIC_ANALYSIS.md"],
83
- "gate" => false,
84
- "agent" => "Code Quality Expert"
80
+ "templates" => ["static_analysis.md"],
81
+ "description" => "Static Code Analysis",
82
+ "outs" => ["docs/static_analysis.md"],
83
+ "gate" => false
85
84
  },
86
85
  "12_OBSERVABILITY_SLOS" => {
87
- "templates" => ["12_OBSERVABILITY_SLOS.md"],
88
- "outs" => ["12_OBSERVABILITY_SLOS.md"],
89
- "gate" => false,
90
- "agent" => "SRE Engineer"
86
+ "templates" => ["observability_slos.md"],
87
+ "description" => "Define Observability and SLOs",
88
+ "outs" => ["docs/observability_slos.md"],
89
+ "gate" => true
91
90
  },
92
91
  "13_DELIVERY_ROLLOUT" => {
93
- "templates" => ["13_DELIVERY_ROLLOUT.md"],
94
- "outs" => ["13_DELIVERY_ROLLOUT.md"],
95
- "gate" => false,
96
- "agent" => "DevOps Engineer"
92
+ "templates" => ["delivery_rollout.md"],
93
+ "description" => "Plan Delivery and Rollout",
94
+ "outs" => ["docs/delivery_rollout.md"],
95
+ "gate" => true
97
96
  },
98
97
  "14_DOCS_PORTAL" => {
99
- "templates" => ["14_DOCS_PORTAL.md"],
100
- "outs" => ["14_DOCS_PORTAL.md"],
101
- "gate" => false,
102
- "agent" => "Technical Writer"
98
+ "templates" => ["docs_portal.md"],
99
+ "description" => "Documentation Portal",
100
+ "outs" => ["docs/docs_portal.md"],
101
+ "gate" => false
103
102
  },
104
103
  "15_POST_RELEASE" => {
105
- "templates" => ["15_POST_RELEASE.md"],
106
- "outs" => ["15_POST_RELEASE.md"],
107
- "gate" => false,
108
- "agent" => "Project Manager"
104
+ "templates" => ["post_release.md"],
105
+ "description" => "Post-Release Review",
106
+ "outs" => ["docs/post_release.md"],
107
+ "gate" => true
109
108
  }
110
109
  }.freeze
111
110
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ class JobManager
5
+ def initialize(project_dir)
6
+ @project_dir = project_dir
7
+ end
8
+
9
+ def create_job(job_class, args = {})
10
+ # Create a new job and return job ID
11
+ # This is a placeholder implementation
12
+ job_id = rand(1000..9999)
13
+
14
+ # Store job metadata for testing
15
+ @jobs ||= {}
16
+ @jobs[job_id] = {
17
+ id: job_id,
18
+ job_class: job_class,
19
+ args: args,
20
+ status: "queued",
21
+ created_at: Time.now
22
+ }
23
+
24
+ job_id
25
+ end
26
+
27
+ def get_job(job_id)
28
+ @jobs ||= {}
29
+ @jobs[job_id]
30
+ end
31
+
32
+ def update_job_status(job_id, status, error: nil)
33
+ @jobs ||= {}
34
+ return unless @jobs[job_id]
35
+
36
+ @jobs[job_id][:status] = status
37
+ @jobs[job_id][:error] = error if error
38
+ @jobs[job_id][:updated_at] = Time.now
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "que"
4
+
5
+ module Aidp
6
+ module Jobs
7
+ class BaseJob < Que::Job
8
+ # Default settings
9
+ self.retry_interval = 30.0 # 30 seconds between retries
10
+ self.maximum_retry_count = 3
11
+
12
+ # Error tracking
13
+ class_attribute :error_handlers
14
+ self.error_handlers = []
15
+
16
+ def self.on_error(&block)
17
+ error_handlers << block
18
+ end
19
+
20
+ protected
21
+
22
+ def log_info(message)
23
+ Que.logger.info "[#{self.class.name}] #{message}"
24
+ end
25
+
26
+ def log_error(message)
27
+ Que.logger.error "[#{self.class.name}] #{message}"
28
+ end
29
+
30
+ def handle_error(error)
31
+ self.class.error_handlers.each do |handler|
32
+ handler.call(error, self)
33
+ rescue => e
34
+ log_error "Error handler failed: #{e.message}"
35
+ end
36
+ end
37
+
38
+ # Override run to add error handling
39
+ def run(*args)
40
+ raise NotImplementedError, "#{self.class} must implement #run"
41
+ rescue => error
42
+ handle_error(error)
43
+ raise # Re-raise to trigger Que's retry mechanism
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Jobs
5
+ class ProviderExecutionJob < BaseJob
6
+ def self.enqueue(provider_type:, prompt:, session: nil, metadata: {})
7
+ job = super
8
+ # Extract job ID explicitly for better readability and debugging
9
+ job_id = job.que_attrs[:job_id]
10
+ raise "Failed to enqueue job: no job ID returned" unless job_id
11
+ job_id
12
+ end
13
+
14
+ def run(provider_type:, prompt:, session: nil, metadata: {})
15
+ start_time = Time.now
16
+
17
+ # Get provider instance
18
+ provider = Aidp::ProviderManager.get_provider(provider_type)
19
+ raise "Provider #{provider_type} not available" unless provider
20
+
21
+ begin
22
+ # Execute provider
23
+ result = provider.send(prompt: prompt, session: session)
24
+
25
+ # Store result
26
+ store_result(result, metadata)
27
+
28
+ # Record metrics
29
+ record_metrics(
30
+ provider_type: provider_type,
31
+ duration: Time.now - start_time,
32
+ success: true,
33
+ error: nil
34
+ )
35
+ rescue => error
36
+ # Record metrics
37
+ record_metrics(
38
+ provider_type: provider_type,
39
+ duration: Time.now - start_time,
40
+ success: false,
41
+ error: error.message
42
+ )
43
+
44
+ # Re-raise error to trigger Que's retry mechanism
45
+ raise
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def store_result(result, metadata)
52
+ return unless metadata[:step_name]
53
+
54
+ Aidp::DatabaseConnection.connection.exec_params(
55
+ <<~SQL,
56
+ INSERT INTO analysis_results (step_name, data, metadata, created_at, updated_at)
57
+ VALUES ($1, $2, $3, $4, $5)
58
+ ON CONFLICT (step_name)
59
+ DO UPDATE SET
60
+ data = EXCLUDED.data,
61
+ metadata = EXCLUDED.metadata,
62
+ updated_at = EXCLUDED.updated_at
63
+ SQL
64
+ [
65
+ metadata[:step_name],
66
+ result.to_json,
67
+ metadata.to_json,
68
+ Time.now,
69
+ Time.now
70
+ ]
71
+ )
72
+ end
73
+
74
+ def record_metrics(provider_type:, duration:, success:, error: nil)
75
+ Aidp::DatabaseConnection.connection.exec_params(
76
+ <<~SQL,
77
+ INSERT INTO provider_metrics (
78
+ provider_type, duration, success, error,
79
+ job_id, attempt, created_at
80
+ )
81
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
82
+ SQL
83
+ [
84
+ provider_type,
85
+ duration,
86
+ success,
87
+ error,
88
+ que_attrs[:job_id],
89
+ que_attrs[:error_count] + 1,
90
+ Time.now
91
+ ]
92
+ )
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ class ProviderManager
5
+ class << self
6
+ def get_provider(provider_type)
7
+ case provider_type
8
+ when "cursor"
9
+ Aidp::Providers::Cursor.new
10
+ when "anthropic"
11
+ Aidp::Providers::Anthropic.new
12
+ when "gemini"
13
+ Aidp::Providers::Gemini.new
14
+ when "macos_ui"
15
+ Aidp::Providers::MacosUI.new
16
+ end
17
+ end
18
+
19
+ def load_from_config(config = {})
20
+ provider_type = config["provider"] || "cursor"
21
+ get_provider(provider_type)
22
+ end
23
+ end
24
+ end
25
+ end