aidp 0.11.0 → 0.12.1

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.
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Aidp
6
+ # Centralized path management for all AIDP internal files
7
+ # Ensures consistent file locations and prevents path-related bugs
8
+ module ConfigPaths
9
+ # Get the main AIDP directory for a project
10
+ def self.aidp_dir(project_dir = Dir.pwd)
11
+ File.join(project_dir, ".aidp")
12
+ end
13
+
14
+ # Get the main configuration file path
15
+ def self.config_file(project_dir = Dir.pwd)
16
+ File.join(aidp_dir(project_dir), "aidp.yml")
17
+ end
18
+
19
+ # Get the configuration directory path
20
+ def self.config_dir(project_dir = Dir.pwd)
21
+ aidp_dir(project_dir)
22
+ end
23
+
24
+ # Get the progress directory path
25
+ def self.progress_dir(project_dir = Dir.pwd)
26
+ File.join(aidp_dir(project_dir), "progress")
27
+ end
28
+
29
+ # Get the execute progress file path
30
+ def self.execute_progress_file(project_dir = Dir.pwd)
31
+ File.join(progress_dir(project_dir), "execute.yml")
32
+ end
33
+
34
+ # Get the analyze progress file path
35
+ def self.analyze_progress_file(project_dir = Dir.pwd)
36
+ File.join(progress_dir(project_dir), "analyze.yml")
37
+ end
38
+
39
+ # Get the harness state directory path
40
+ def self.harness_state_dir(project_dir = Dir.pwd)
41
+ File.join(aidp_dir(project_dir), "harness")
42
+ end
43
+
44
+ # Get the harness state file path for a specific mode
45
+ def self.harness_state_file(mode, project_dir = Dir.pwd)
46
+ File.join(harness_state_dir(project_dir), "#{mode}_state.json")
47
+ end
48
+
49
+ # Get the providers directory path
50
+ def self.providers_dir(project_dir = Dir.pwd)
51
+ File.join(aidp_dir(project_dir), "providers")
52
+ end
53
+
54
+ # Get the provider info file path
55
+ def self.provider_info_file(provider_name, project_dir = Dir.pwd)
56
+ File.join(providers_dir(project_dir), "#{provider_name}_info.yml")
57
+ end
58
+
59
+ # Get the jobs directory path
60
+ def self.jobs_dir(project_dir = Dir.pwd)
61
+ File.join(aidp_dir(project_dir), "jobs")
62
+ end
63
+
64
+ # Get the checkpoint file path
65
+ def self.checkpoint_file(project_dir = Dir.pwd)
66
+ File.join(aidp_dir(project_dir), "checkpoint.yml")
67
+ end
68
+
69
+ # Get the checkpoint history file path
70
+ def self.checkpoint_history_file(project_dir = Dir.pwd)
71
+ File.join(aidp_dir(project_dir), "checkpoint_history.jsonl")
72
+ end
73
+
74
+ # Get the JSON storage directory path
75
+ def self.json_storage_dir(project_dir = Dir.pwd)
76
+ File.join(aidp_dir(project_dir), "json")
77
+ end
78
+
79
+ # Check if the main configuration file exists
80
+ def self.config_exists?(project_dir = Dir.pwd)
81
+ File.exist?(config_file(project_dir))
82
+ end
83
+
84
+ # Ensure the main AIDP directory exists
85
+ def self.ensure_aidp_dir(project_dir = Dir.pwd)
86
+ dir = aidp_dir(project_dir)
87
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
88
+ dir
89
+ end
90
+
91
+ # Ensure the configuration directory exists
92
+ def self.ensure_config_dir(project_dir = Dir.pwd)
93
+ ensure_aidp_dir(project_dir)
94
+ end
95
+
96
+ # Ensure the progress directory exists
97
+ def self.ensure_progress_dir(project_dir = Dir.pwd)
98
+ dir = progress_dir(project_dir)
99
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
100
+ dir
101
+ end
102
+
103
+ # Ensure the harness state directory exists
104
+ def self.ensure_harness_state_dir(project_dir = Dir.pwd)
105
+ dir = harness_state_dir(project_dir)
106
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
107
+ dir
108
+ end
109
+
110
+ # Ensure the providers directory exists
111
+ def self.ensure_providers_dir(project_dir = Dir.pwd)
112
+ dir = providers_dir(project_dir)
113
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
114
+ dir
115
+ end
116
+
117
+ # Ensure the jobs directory exists
118
+ def self.ensure_jobs_dir(project_dir = Dir.pwd)
119
+ dir = jobs_dir(project_dir)
120
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
121
+ dir
122
+ end
123
+
124
+ # Ensure the JSON storage directory exists
125
+ def self.ensure_json_storage_dir(project_dir = Dir.pwd)
126
+ dir = json_storage_dir(project_dir)
127
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
128
+ dir
129
+ end
130
+ end
131
+ end
data/lib/aidp/config.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
+ require_relative "config/paths"
4
5
 
5
6
  module Aidp
6
7
  # Configuration management for both execute and analyze modes
@@ -165,7 +166,7 @@ module Aidp
165
166
  }.freeze
166
167
 
167
168
  def self.load(project_dir = Dir.pwd)
168
- config_file = File.join(project_dir, ".aidp", "aidp.yml")
169
+ config_file = ConfigPaths.config_file(project_dir)
169
170
 
170
171
  if File.exist?(config_file)
171
172
  load_yaml_config(config_file)
@@ -244,15 +245,15 @@ module Aidp
244
245
 
245
246
  # Check if configuration file exists
246
247
  def self.config_exists?(project_dir = Dir.pwd)
247
- File.exist?(File.join(project_dir, ".aidp", "aidp.yml"))
248
+ ConfigPaths.config_exists?(project_dir)
248
249
  end
249
250
 
250
251
  # Create example configuration file
251
252
  def self.create_example_config(project_dir = Dir.pwd)
252
- config_path = File.join(project_dir, ".aidp", "aidp.yml")
253
+ config_path = ConfigPaths.config_file(project_dir)
253
254
  return false if File.exist?(config_path)
254
255
 
255
- FileUtils.mkdir_p(File.dirname(config_path))
256
+ ConfigPaths.ensure_config_dir(project_dir)
256
257
 
257
258
  example_config = {
258
259
  harness: {
@@ -283,6 +284,19 @@ module Aidp
283
284
  true
284
285
  end
285
286
 
287
+ # Expose path methods for convenience
288
+ def self.config_file(project_dir = Dir.pwd)
289
+ ConfigPaths.config_file(project_dir)
290
+ end
291
+
292
+ def self.config_dir(project_dir = Dir.pwd)
293
+ ConfigPaths.config_dir(project_dir)
294
+ end
295
+
296
+ def self.aidp_dir(project_dir = Dir.pwd)
297
+ ConfigPaths.aidp_dir(project_dir)
298
+ end
299
+
286
300
  private_class_method def self.load_yaml_config(config_file)
287
301
  YAML.load_file(config_file) || {}
288
302
  rescue => e
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "yaml"
4
4
  require_relative "config_schema"
5
+ require_relative "../config/paths"
5
6
 
6
7
  module Aidp
7
8
  module Harness
@@ -76,9 +77,9 @@ module Aidp
76
77
  return false if config_exists?
77
78
 
78
79
  example_config = ConfigSchema.generate_example
79
- config_path = File.join(@project_dir, ".aidp", "aidp.yml")
80
+ config_path = Aidp::ConfigPaths.config_file(@project_dir)
80
81
 
81
- FileUtils.mkdir_p(File.dirname(config_path))
82
+ Aidp::ConfigPaths.ensure_config_dir(@project_dir)
82
83
  File.write(config_path, YAML.dump(example_config))
83
84
  true
84
85
  end
@@ -244,7 +245,7 @@ module Aidp
244
245
  private
245
246
 
246
247
  def find_config_file
247
- config_file = File.join(@project_dir, ".aidp", "aidp.yml")
248
+ config_file = Aidp::ConfigPaths.config_file(@project_dir)
248
249
 
249
250
  if File.exist?(config_file)
250
251
  config_file
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../config"
4
+ require_relative "../config/paths"
4
5
 
5
6
  module Aidp
6
7
  module Harness
@@ -211,7 +212,7 @@ module Aidp
211
212
 
212
213
  # Get configuration path
213
214
  def config_path
214
- File.join(@project_dir, ".aidp", "aidp.yml")
215
+ Aidp::ConfigPaths.config_file(@project_dir)
215
216
  end
216
217
 
217
218
  # Get logging configuration
@@ -0,0 +1,366 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+ require "fileutils"
6
+
7
+ module Aidp
8
+ module Harness
9
+ # Stores detailed information about AI providers gathered from their CLI tools
10
+ class ProviderInfo
11
+ attr_reader :provider_name, :info_file_path
12
+
13
+ def initialize(provider_name, root_dir = nil)
14
+ @provider_name = provider_name
15
+ @root_dir = root_dir || Dir.pwd
16
+ @info_file_path = File.join(@root_dir, ".aidp", "providers", "#{provider_name}_info.yml")
17
+ ensure_directory_exists
18
+ end
19
+
20
+ # Gather information about the provider by introspecting its CLI
21
+ def gather_info
22
+ info = {
23
+ provider: @provider_name,
24
+ last_checked: Time.now.iso8601,
25
+ cli_available: false,
26
+ help_output: nil,
27
+ capabilities: {},
28
+ permission_modes: [],
29
+ mcp_support: false,
30
+ mcp_servers: [],
31
+ auth_method: nil,
32
+ flags: {}
33
+ }
34
+
35
+ # Try to get help output from the provider CLI
36
+ help_output = fetch_help_output
37
+ if help_output
38
+ info[:cli_available] = true
39
+ info[:help_output] = help_output
40
+ info.merge!(parse_help_output(help_output))
41
+ end
42
+
43
+ # Try to get MCP server list if supported
44
+ if info[:mcp_support]
45
+ mcp_servers = fetch_mcp_servers
46
+ info[:mcp_servers] = mcp_servers if mcp_servers
47
+ end
48
+
49
+ save_info(info)
50
+ info
51
+ end
52
+
53
+ # Load stored provider info
54
+ def load_info
55
+ return nil unless File.exist?(@info_file_path)
56
+
57
+ YAML.safe_load_file(@info_file_path, permitted_classes: [Time, Symbol])
58
+ rescue => e
59
+ warn "Failed to load provider info for #{@provider_name}: #{e.message}"
60
+ nil
61
+ end
62
+
63
+ # Get provider info, refreshing if needed
64
+ def get_info(force_refresh: false, max_age: 86400)
65
+ existing_info = load_info
66
+
67
+ # Refresh if forced, missing, or stale
68
+ if force_refresh || existing_info.nil? || info_stale?(existing_info, max_age)
69
+ gather_info
70
+ else
71
+ existing_info
72
+ end
73
+ end
74
+
75
+ # Check if provider supports MCP servers
76
+ def supports_mcp?
77
+ info = load_info
78
+ return false unless info
79
+
80
+ info[:mcp_support] == true
81
+ end
82
+
83
+ # Get permission modes available
84
+ def permission_modes
85
+ info = load_info
86
+ return [] unless info
87
+
88
+ info[:permission_modes] || []
89
+ end
90
+
91
+ # Get authentication method
92
+ def auth_method
93
+ info = load_info
94
+ return nil unless info
95
+
96
+ info[:auth_method]
97
+ end
98
+
99
+ # Get available flags/options
100
+ def available_flags
101
+ info = load_info
102
+ return {} unless info
103
+
104
+ info[:flags] || {}
105
+ end
106
+
107
+ # Get configured MCP servers
108
+ def mcp_servers
109
+ info = load_info
110
+ return [] unless info
111
+
112
+ info[:mcp_servers] || []
113
+ end
114
+
115
+ # Check if provider has MCP servers configured
116
+ def has_mcp_servers?
117
+ mcp_servers.any?
118
+ end
119
+
120
+ private
121
+
122
+ def ensure_directory_exists
123
+ dir = File.dirname(@info_file_path)
124
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
125
+ end
126
+
127
+ def save_info(info)
128
+ File.write(@info_file_path, YAML.dump(info))
129
+ end
130
+
131
+ def info_stale?(info, max_age)
132
+ return true unless info[:last_checked]
133
+
134
+ last_checked = Time.parse(info[:last_checked].to_s)
135
+ (Time.now - last_checked) > max_age
136
+ rescue
137
+ true
138
+ end
139
+
140
+ def fetch_help_output
141
+ execute_provider_command("--help")
142
+ end
143
+
144
+ def fetch_mcp_servers
145
+ binary = get_binary_name
146
+ return nil unless binary
147
+
148
+ # Try different MCP list commands based on provider
149
+ mcp_output = case @provider_name
150
+ when "claude", "anthropic"
151
+ execute_provider_command("mcp", "list")
152
+ end
153
+
154
+ return nil unless mcp_output
155
+
156
+ parse_mcp_servers(mcp_output)
157
+ end
158
+
159
+ def execute_provider_command(*args)
160
+ binary = get_binary_name
161
+ return nil unless binary
162
+
163
+ # Try to find the binary
164
+ path = begin
165
+ Aidp::Util.which(binary)
166
+ rescue
167
+ nil
168
+ end
169
+ return nil unless path
170
+
171
+ # Execute command with timeout
172
+ begin
173
+ r, w = IO.pipe
174
+ pid = Process.spawn(binary, *args, out: w, err: w)
175
+ w.close
176
+
177
+ # Wait with timeout
178
+ deadline = Time.now + 5
179
+ status = nil
180
+ while Time.now < deadline
181
+ pid_done, status = Process.waitpid2(pid, Process::WNOHANG)
182
+ break if pid_done
183
+ sleep 0.05
184
+ end
185
+
186
+ # Kill if timed out
187
+ unless status
188
+ begin
189
+ Process.kill("TERM", pid)
190
+ sleep 0.1
191
+ Process.kill("KILL", pid)
192
+ rescue
193
+ nil
194
+ end
195
+ return nil
196
+ end
197
+
198
+ output = r.read
199
+ r.close
200
+ output
201
+ rescue
202
+ nil
203
+ end
204
+ end
205
+
206
+ def parse_mcp_servers(output)
207
+ servers = []
208
+ return servers unless output
209
+
210
+ # Parse MCP server list output
211
+ # Claude format (as of 2025):
212
+ # dash-api: uvx --from git+https://... - ✓ Connected
213
+ # or
214
+ # server-name: command - ✗ Error message
215
+ #
216
+ # Legacy format:
217
+ # Name Status Description
218
+ # filesystem enabled File system access
219
+
220
+ lines = output.lines
221
+
222
+ # Skip header lines
223
+ lines.reject! { |line| /checking mcp server health/i.match?(line) }
224
+
225
+ lines.each do |line|
226
+ line = line.strip
227
+ next if line.empty?
228
+
229
+ # Try to parse new Claude format: "name: command - ✓ Connected"
230
+ if line =~ /^([^:]+):\s*(.+?)\s*-\s*(✓|✗)\s*(.+)$/
231
+ name = Regexp.last_match(1).strip
232
+ command = Regexp.last_match(2).strip
233
+ status_symbol = Regexp.last_match(3)
234
+ status_text = Regexp.last_match(4).strip
235
+
236
+ servers << {
237
+ name: name,
238
+ status: (status_symbol == "✓") ? "connected" : "error",
239
+ description: command,
240
+ enabled: status_symbol == "✓",
241
+ error: (status_symbol == "✗") ? status_text : nil
242
+ }
243
+ next
244
+ end
245
+
246
+ # Try to parse legacy table format
247
+ # Skip header line
248
+ next if /Name.*Status/i.match?(line)
249
+ next if /^[-=]+$/.match?(line) # Skip separator lines
250
+
251
+ # Parse table format: columns separated by multiple spaces
252
+ parts = line.split(/\s{2,}/)
253
+ next if parts.size < 2
254
+
255
+ name = parts[0]&.strip
256
+ status = parts[1]&.strip
257
+ description = parts[2..]&.join(" ")&.strip
258
+
259
+ next unless name && !name.empty?
260
+
261
+ servers << {
262
+ name: name,
263
+ status: status || "unknown",
264
+ description: description,
265
+ enabled: status&.downcase == "enabled" || status&.downcase == "connected"
266
+ }
267
+ end
268
+
269
+ servers
270
+ end
271
+
272
+ def get_binary_name
273
+ case @provider_name
274
+ when "claude", "anthropic"
275
+ "claude"
276
+ when "cursor"
277
+ "cursor"
278
+ when "gemini"
279
+ "gemini"
280
+ when "codex"
281
+ "codex"
282
+ when "github_copilot"
283
+ "gh"
284
+ when "opencode"
285
+ "opencode"
286
+ else
287
+ @provider_name
288
+ end
289
+ end
290
+
291
+ def parse_help_output(help_text)
292
+ parsed = {
293
+ capabilities: {},
294
+ permission_modes: [],
295
+ mcp_support: false,
296
+ auth_method: nil,
297
+ flags: {}
298
+ }
299
+
300
+ # Check for MCP support
301
+ parsed[:mcp_support] = !!(help_text =~ /mcp|MCP|Model Context Protocol/i)
302
+
303
+ # Extract permission modes
304
+ if help_text =~ /--permission-mode\s+<mode>\s+.*?\(choices:\s*([^)]+)\)/m
305
+ modes = Regexp.last_match(1).split(",").map(&:strip).map { |m| m.gsub(/["']/, "") }
306
+ parsed[:permission_modes] = modes
307
+ end
308
+
309
+ # Check for dangerous skip permissions
310
+ parsed[:capabilities][:bypass_permissions] = !!(help_text =~ /--dangerously-skip-permissions/)
311
+
312
+ # Check for API key / subscription patterns
313
+ if /--api-key|API_KEY|setup-token|subscription/i.match?(help_text)
314
+ parsed[:auth_method] = if /setup-token|subscription/i.match?(help_text)
315
+ "subscription"
316
+ else
317
+ "api_key"
318
+ end
319
+ end
320
+
321
+ # Extract model configuration
322
+ parsed[:capabilities][:model_selection] = !!(help_text =~ /--model\s+<model>/)
323
+
324
+ # Extract MCP configuration
325
+ parsed[:capabilities][:mcp_config] = !!(help_text =~ /--mcp-config/)
326
+
327
+ # Extract allowed/disallowed tools
328
+ parsed[:capabilities][:tool_restrictions] = !!(help_text =~ /--allowed-tools|--disallowed-tools/)
329
+
330
+ # Extract session management
331
+ parsed[:capabilities][:session_management] = !!(help_text =~ /--continue|--resume|--fork-session/)
332
+
333
+ # Extract output formats
334
+ if help_text =~ /--output-format\s+.*?\(choices:\s*([^)]+)\)/m
335
+ formats = Regexp.last_match(1).split(",").map(&:strip).map { |f| f.gsub(/["']/, "") }
336
+ parsed[:capabilities][:output_formats] = formats
337
+ end
338
+
339
+ # Extract notable flags
340
+ extract_flags(help_text, parsed[:flags])
341
+
342
+ parsed
343
+ end
344
+
345
+ def extract_flags(help_text, flags_hash)
346
+ # Extract all flags with their descriptions
347
+ help_text.scan(/^\s+(--[\w-]+(?:\s+<\w+>)?)\s+(.+?)(?=^\s+(?:--|\w|$))/m).each do |flag, desc|
348
+ flag_name = flag.split.first.gsub(/^--/, "")
349
+ flags_hash[flag_name] = {
350
+ flag: flag.strip,
351
+ description: desc.strip.gsub(/\s+/, " ")
352
+ }
353
+ end
354
+
355
+ # Also capture short flags
356
+ help_text.scan(/^\s+(-\w),\s+(--[\w-]+(?:\s+<\w+>)?)\s+(.+?)(?=^\s+(?:--|-\w|$))/m).each do |short, long, desc|
357
+ flag_name = long.split.first.gsub(/^--/, "")
358
+ flags_hash[flag_name] ||= {}
359
+ flags_hash[flag_name][:short] = short
360
+ flags_hash[flag_name][:flag] = long.strip
361
+ flags_hash[flag_name][:description] = desc.strip.gsub(/\s+/, " ")
362
+ end
363
+ end
364
+ end
365
+ end
366
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "enhanced_tui"
4
4
  require_relative "../../workflows/selector"
5
+ require_relative "../../workflows/guided_agent"
5
6
 
6
7
  module Aidp
7
8
  module Harness
@@ -10,10 +11,11 @@ module Aidp
10
11
  class EnhancedWorkflowSelector
11
12
  class WorkflowError < StandardError; end
12
13
 
13
- def initialize(tui = nil)
14
+ def initialize(tui = nil, project_dir: Dir.pwd)
14
15
  @tui = tui || EnhancedTUI.new
15
16
  @user_input = {}
16
17
  @workflow_selector = Aidp::Workflows::Selector.new
18
+ @project_dir = project_dir
17
19
  end
18
20
 
19
21
  def select_workflow(harness_mode: false, mode: :analyze)
@@ -28,6 +30,8 @@ module Aidp
28
30
 
29
31
  def select_workflow_interactive(mode)
30
32
  case mode
33
+ when :guided
34
+ select_guided_workflow
31
35
  when :analyze
32
36
  select_analyze_workflow_interactive
33
37
  when :execute
@@ -243,6 +247,23 @@ module Aidp
243
247
  "16_IMPLEMENTATION"
244
248
  ]
245
249
  end
250
+
251
+ def select_guided_workflow
252
+ # Use the guided agent to help user select workflow
253
+ guided_agent = Aidp::Workflows::GuidedAgent.new(@project_dir, prompt: @tui.instance_variable_get(:@prompt))
254
+ result = guided_agent.select_workflow
255
+
256
+ # Store user input for later use
257
+ @user_input = result[:user_input]
258
+
259
+ # Return in the expected format
260
+ {
261
+ workflow_type: result[:workflow_type],
262
+ steps: result[:steps],
263
+ user_input: @user_input,
264
+ workflow: result[:workflow]
265
+ }
266
+ end
246
267
  end
247
268
  end
248
269
  end
data/lib/aidp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.11.0"
4
+ VERSION = "0.12.1"
5
5
  end