aidp 0.15.1 → 0.15.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 179466fc22044805c9256aefd1945b799869cf2bbf304c4df049ff5917188c64
4
- data.tar.gz: 5c550222222cca3b14c071e05cf5e52ca9994446d8656481dce758b99cd5cf69
3
+ metadata.gz: '0691816c2748d6704d61bba9ed4c992f1b7d3ac8f70385ac94cb199916b71491'
4
+ data.tar.gz: e0a8b4a7c9ece98972565635894d7312c4253c2adb3341192c7622e8e28d3a12
5
5
  SHA512:
6
- metadata.gz: 5b840f4dff232b7ba80b3a38cc78cd244f7c399455b00aecec622cd9106326f003958dd42be8db73a77d8110e6d4a2a5454e11ec8b77386547898333c6b6a704
7
- data.tar.gz: a51ac5d52712a3a792beb88bcdd0000f9775dc1d744f98aeaedd54b4f6a82f7f8fb09ec58b090cd014fcf07fae5f5c63db292a146fdd7df87748105a9398c843
6
+ metadata.gz: 0d3ffce4b6aa8913658862e4412c165c4f20e8164845267d9d85c574b340d4140e63abbae359c47387255081c2738cbdaae215c159e1aeb4d36e7a84b5405dfc
7
+ data.tar.gz: d2a809ea1b79bbe97e8adfe90903ecb610851837d7b744da208c20e74574346e18e4e3a25ebb136f702fb2501cf5855f1aac0d70d31b6be618c0843a51d5d90f
data/lib/aidp/cli.rb CHANGED
@@ -353,7 +353,7 @@ module Aidp
353
353
  when "mcp" then run_mcp_command(args)
354
354
  when "issue" then run_issue_command(args)
355
355
  when "config" then run_config_command(args)
356
- when "init" then run_init_command
356
+ when "init" then run_init_command(args)
357
357
  when "watch" then run_watch_command(args)
358
358
  else
359
359
  display_message("Unknown command: #{cmd}", type: :info)
@@ -1015,11 +1015,49 @@ module Aidp
1015
1015
  wizard.run
1016
1016
  end
1017
1017
 
1018
- def run_init_command
1019
- runner = Aidp::Init::Runner.new(Dir.pwd, prompt: TTY::Prompt.new)
1018
+ def run_init_command(args = [])
1019
+ options = {}
1020
+
1021
+ until args.empty?
1022
+ token = args.shift
1023
+ case token
1024
+ when "--explain-detection"
1025
+ options[:explain_detection] = true
1026
+ when "--dry-run"
1027
+ options[:dry_run] = true
1028
+ when "--preview"
1029
+ options[:preview] = true
1030
+ when "-h", "--help"
1031
+ display_init_usage
1032
+ return
1033
+ else
1034
+ display_message("Unknown init option: #{token}", type: :error)
1035
+ display_init_usage
1036
+ return
1037
+ end
1038
+ end
1039
+
1040
+ require_relative "init/runner"
1041
+ runner = Aidp::Init::Runner.new(Dir.pwd, prompt: TTY::Prompt.new, options: options)
1020
1042
  runner.run
1021
1043
  end
1022
1044
 
1045
+ def display_init_usage
1046
+ display_message("Usage: aidp init [options]", type: :info)
1047
+ display_message("", type: :info)
1048
+ display_message("Options:", type: :info)
1049
+ display_message(" --explain-detection Show detailed evidence for all detections", type: :info)
1050
+ display_message(" --dry-run Run analysis without generating files", type: :info)
1051
+ display_message(" --preview Show preview before writing files", type: :info)
1052
+ display_message(" -h, --help Show this help message", type: :info)
1053
+ display_message("", type: :info)
1054
+ display_message("Examples:", type: :info)
1055
+ display_message(" aidp init # Run full init workflow", type: :info)
1056
+ display_message(" aidp init --explain-detection # Show detailed detection evidence", type: :info)
1057
+ display_message(" aidp init --dry-run # Preview without writing files", type: :info)
1058
+ display_message(" aidp init --preview # Show preview before writing", type: :info)
1059
+ end
1060
+
1023
1061
  def run_watch_command(args)
1024
1062
  if args.empty?
1025
1063
  display_message("Usage: aidp watch <issues_url> [--interval SECONDS] [--provider NAME] [--once]", type: :info)
@@ -283,7 +283,7 @@ module Aidp
283
283
  when "codex"
284
284
  "codex"
285
285
  when "github_copilot"
286
- "gh"
286
+ "copilot"
287
287
  when "opencode"
288
288
  "opencode"
289
289
  else
@@ -71,19 +71,20 @@ module Aidp
71
71
 
72
72
  # Switch to next available provider with sophisticated fallback logic
73
73
  def switch_provider(reason = "manual_switch", context = {})
74
- Aidp.logger.info("provider_manager", "Attempting provider switch", reason: reason, current: current_provider, **context)
74
+ old_provider = current_provider
75
+ Aidp.logger.info("provider_manager", "Attempting provider switch", reason: reason, current: old_provider, **context)
75
76
 
76
77
  # Get fallback chain for current provider
77
- provider_fallback_chain = fallback_chain(current_provider)
78
+ provider_fallback_chain = fallback_chain(old_provider)
78
79
 
79
80
  # Find next healthy provider in fallback chain
80
- next_provider = find_next_healthy_provider(provider_fallback_chain, current_provider)
81
+ next_provider = find_next_healthy_provider(provider_fallback_chain, old_provider)
81
82
 
82
83
  if next_provider
83
84
  success = set_current_provider(next_provider, reason, context)
84
85
  if success
85
- log_provider_switch(current_provider, next_provider, reason, context)
86
- Aidp.logger.info("provider_manager", "Provider switched successfully", from: current_provider, to: next_provider, reason: reason)
86
+ log_provider_switch(old_provider, next_provider, reason, context)
87
+ Aidp.logger.info("provider_manager", "Provider switched successfully", from: old_provider, to: next_provider, reason: reason)
87
88
  return next_provider
88
89
  else
89
90
  Aidp.logger.warn("provider_manager", "Failed to switch to provider", provider: next_provider, reason: reason)
@@ -96,7 +97,7 @@ module Aidp
96
97
  if next_provider
97
98
  success = set_current_provider(next_provider, reason, context)
98
99
  if success
99
- log_provider_switch(current_provider, next_provider, reason, context)
100
+ log_provider_switch(old_provider, next_provider, reason, context)
100
101
  return next_provider
101
102
  end
102
103
  end
@@ -107,14 +108,14 @@ module Aidp
107
108
  if next_provider
108
109
  success = set_current_provider(next_provider, reason, context)
109
110
  if success
110
- log_provider_switch(current_provider, next_provider, reason, context)
111
+ log_provider_switch(old_provider, next_provider, reason, context)
111
112
  return next_provider
112
113
  end
113
114
  end
114
115
 
115
116
  # No providers available
116
117
  log_no_providers_available(reason, context)
117
- Aidp.logger.error("provider_manager", "No providers available for fallback", reason: reason, **context)
118
+ Aidp.logger.error("provider_manager", "No providers available for fallback", reason: reason, provider: old_provider)
118
119
  nil
119
120
  end
120
121
 
@@ -32,10 +32,11 @@ module Aidp
32
32
 
33
33
  def write_style_guide(analysis, preferences)
34
34
  languages = format_list(analysis[:languages].keys)
35
- frameworks = format_list(analysis[:frameworks])
36
- test_frameworks = format_list(analysis[:test_frameworks])
35
+ # Extract high-confidence frameworks (>= 0.7)
36
+ frameworks = extract_confident_names(analysis[:frameworks], threshold: 0.7)
37
+ test_frameworks = extract_confident_names(analysis[:test_frameworks], threshold: 0.7)
37
38
  key_dirs = format_list(analysis[:key_directories])
38
- tooling = analysis[:tooling].keys.map { |tool| format_tool(tool) }.sort
39
+ tooling = extract_confident_names(analysis[:tooling], threshold: 0.7, key: :tool).map { |tool| format_tool(tool) }.sort
39
40
 
40
41
  adoption_note = if truthy?(preferences[:adopt_new_conventions])
41
42
  "This project has opted to adopt new conventions recommended by aidp init. When in doubt, prefer the rules below over legacy patterns."
@@ -43,14 +44,17 @@ module Aidp
43
44
  "Retain existing conventions when they do not conflict with the guidance below."
44
45
  end
45
46
 
47
+ frameworks_text = frameworks.empty? ? "None detected" : frameworks.join(", ")
48
+ test_frameworks_text = test_frameworks.empty? ? "Unknown" : test_frameworks.join(", ")
49
+
46
50
  content = <<~GUIDE
47
51
  # Project LLM Style Guide
48
52
 
49
53
  > Generated automatically by `aidp init` on #{Time.now.utc.iso8601}.
50
54
  >
51
55
  > Detected languages: #{languages}
52
- > Framework hints: #{frameworks.empty? ? "None detected" : frameworks}
53
- > Primary test frameworks: #{test_frameworks.empty? ? "Unknown" : test_frameworks}
56
+ > Framework hints: #{frameworks_text}
57
+ > Primary test frameworks: #{test_frameworks_text}
54
58
  > Key directories: #{key_dirs.empty? ? "Standard structure" : key_dirs}
55
59
 
56
60
  #{adoption_note}
@@ -115,9 +119,10 @@ module Aidp
115
119
 
116
120
  def write_project_analysis(analysis)
117
121
  languages = format_language_breakdown(analysis[:languages])
118
- frameworks = bullet_list(analysis[:frameworks], default: "_None detected_")
122
+ frameworks = format_framework_detection(analysis[:frameworks])
123
+ test_frameworks = format_framework_detection(analysis[:test_frameworks])
119
124
  config_files = bullet_list(analysis[:config_files], default: "_No dedicated configuration files discovered_")
120
- tooling = format_tooling_section(analysis[:tooling])
125
+ tooling = format_tooling_detection(analysis[:tooling])
121
126
 
122
127
  stats = analysis[:repo_stats]
123
128
  stats_lines = [
@@ -146,7 +151,7 @@ module Aidp
146
151
  #{config_files}
147
152
 
148
153
  ## Test & Quality Signals
149
- #{bullet_list(analysis[:test_frameworks], prefix: "- Detected test suite: ", default: "_Unable to infer test suite_")}
154
+ #{test_frameworks}
150
155
 
151
156
  ## Local Quality Toolchain
152
157
  #{tooling}
@@ -175,13 +180,19 @@ module Aidp
175
180
  "- Keep legacy style deviations documented until dedicated refactors are scheduled.\n"
176
181
  end
177
182
 
183
+ tooling_section = if tooling.empty?
184
+ "_No linting/formatting tools detected. Consider adding RuboCop, ESLint, or Prettier based on the primary language._"
185
+ else
186
+ format_tooling_detection_table(tooling)
187
+ end
188
+
178
189
  content = <<~PLAN
179
190
  # Code Quality Plan
180
191
 
181
192
  This plan captures the current tooling landscape and proposes next steps for keeping the codebase healthy. Generated by `aidp init` on #{Time.now.utc.iso8601}.
182
193
 
183
194
  ## Local Quality Toolchain
184
- #{tooling.empty? ? "_No linting/formatting tools detected. Consider adding RuboCop, ESLint, or Prettier based on the primary language._" : format_tooling_table(tooling)}
195
+ #{tooling_section}
185
196
 
186
197
  ## Immediate Actions
187
198
  #{proactive}#{migration}- Document onboarding steps in `docs/` to ensure future contributors follow the agreed workflow.
@@ -189,7 +200,7 @@ module Aidp
189
200
  ## Long-Term Improvements
190
201
  - Keep the style guide in sync with real-world code changes; regenerate with `aidp init` after major rewrites.
191
202
  - Automate test and lint runs via CI (detected: #{analysis.dig(:repo_stats, :has_ci_config) ? "yes" : "no"}).
192
- - Track flaky tests or unstable tooling in `PROJECT_ANALYSIS.md` under a Health Log section.
203
+ - Track flaky tests or unstable tooling in `PROJECT_ANALYSIS.md` under a "Health Log" section.
193
204
 
194
205
  ---
195
206
  Based on templates: `analysis/analyze_static_code.md`, `analysis/analyze_tests.md`.
@@ -251,6 +262,60 @@ module Aidp
251
262
  rescue
252
263
  false
253
264
  end
265
+
266
+ # Extract confident names from detection results
267
+ def extract_confident_names(detections, threshold: 0.7, key: :name)
268
+ return [] if detections.nil? || detections.empty?
269
+
270
+ detections.select { |d| d[:confidence] >= threshold }.map { |d| d[key] }
271
+ end
272
+
273
+ # Format framework detection with confidence levels
274
+ def format_framework_detection(detections)
275
+ return "_None detected_" if detections.nil? || detections.empty?
276
+
277
+ lines = detections.map do |detection|
278
+ name = detection[:name]
279
+ confidence = (detection[:confidence] * 100).round
280
+ evidence = detection[:evidence].join("; ")
281
+ "- **#{name}** (#{confidence}% confidence)\n - Evidence: #{evidence}"
282
+ end
283
+
284
+ lines.join("\n")
285
+ end
286
+
287
+ # Format tooling detection with confidence levels
288
+ def format_tooling_detection(tooling)
289
+ return "_No tooling detected._" if tooling.nil? || tooling.empty?
290
+
291
+ header = "| Tool | Confidence | Evidence |\n|------|------------|----------|"
292
+ rows = tooling.map do |tool_data|
293
+ tool_name = format_tool(tool_data[:tool])
294
+ confidence = "#{(tool_data[:confidence] * 100).round}%"
295
+ evidence = tool_data[:evidence].join(", ")
296
+ "| #{tool_name} | #{confidence} | #{evidence} |"
297
+ end
298
+
299
+ ([header] + rows).join("\n")
300
+ end
301
+
302
+ # Format tooling detection table for quality plan
303
+ def format_tooling_detection_table(tooling)
304
+ return "_No tooling detected._" if tooling.nil? || tooling.empty?
305
+
306
+ header = "| Tool | Evidence |\n|------|----------|"
307
+ rows = tooling.select { |t| t[:confidence] >= 0.7 }.map do |tool_data|
308
+ tool_name = format_tool(tool_data[:tool])
309
+ evidence = tool_data[:evidence].join(", ")
310
+ "| #{tool_name} | #{evidence} |"
311
+ end
312
+
313
+ if rows.empty?
314
+ "_No high-confidence tooling detected. Consider adding linting and formatting tools._"
315
+ else
316
+ ([header] + rows).join("\n")
317
+ end
318
+ end
254
319
  end
255
320
  end
256
321
  end
@@ -155,7 +155,9 @@ module Aidp
155
155
  @project_dir = project_dir
156
156
  end
157
157
 
158
- def analyze
158
+ def analyze(options = {})
159
+ @explain_detection = options[:explain_detection] || false
160
+
159
161
  {
160
162
  languages: detect_languages,
161
163
  frameworks: detect_frameworks,
@@ -185,21 +187,57 @@ module Aidp
185
187
  end
186
188
 
187
189
  def detect_frameworks
188
- frameworks = Set.new
190
+ results = []
189
191
 
190
192
  FRAMEWORK_HINTS.each do |framework, rules|
191
- if rules[:files]&.any? { |file| project_glob?(file) }
192
- if rules[:contents]
193
- frameworks << framework if rules[:contents].any? { |pattern| search_files_for_pattern(rules[:files], pattern) }
194
- else
195
- frameworks << framework
193
+ evidence = []
194
+ confidence = 0.0
195
+
196
+ # Check for required files
197
+ matched_files = []
198
+ if rules[:files]
199
+ matched_files = rules[:files].select { |file| project_glob?(file) }
200
+ if matched_files.any?
201
+ evidence << "Found files: #{matched_files.join(", ")}"
202
+ confidence += 0.3
203
+ end
204
+ end
205
+
206
+ # Check for content patterns in specific files or across project
207
+ if rules[:contents]
208
+ rules[:contents].each do |pattern|
209
+ if matched_files.any?
210
+ # Search only in the matched files
211
+ if search_files_for_pattern(matched_files, pattern)
212
+ evidence << "Found pattern '#{pattern.inspect}' in #{matched_files.join(", ")}"
213
+ confidence += 0.7
214
+ end
215
+ elsif search_project_for_pattern(pattern)
216
+ # Search across all project files (less confident)
217
+ evidence << "Found pattern '#{pattern.inspect}' in project files"
218
+ confidence += 0.4
219
+ end
196
220
  end
197
- elsif rules[:contents]&.any? { |pattern| search_project_for_pattern(pattern) }
198
- frameworks << framework
221
+ elsif matched_files.any?
222
+ # Files exist but no content check needed - moderately confident
223
+ confidence = 0.6
224
+ end
225
+
226
+ # Only include frameworks with evidence
227
+ if evidence.any? && confidence > 0.0
228
+ # Cap confidence at 1.0
229
+ confidence = [confidence, 1.0].min
230
+
231
+ results << {
232
+ name: framework,
233
+ confidence: confidence,
234
+ evidence: evidence
235
+ }
199
236
  end
200
237
  end
201
238
 
202
- frameworks.to_a.sort
239
+ # Sort by confidence (descending) then name
240
+ results.sort_by { |r| [-r[:confidence], r[:name]] }
203
241
  end
204
242
 
205
243
  def detect_key_directories
@@ -212,28 +250,82 @@ module Aidp
212
250
 
213
251
  def detect_test_frameworks
214
252
  results = []
253
+ dependency_files = ["Gemfile", "Gemfile.lock", "package.json", "pyproject.toml", "requirements.txt", "go.mod", "mix.exs", "composer.json", "Cargo.toml"]
215
254
 
216
255
  TEST_FRAMEWORK_HINTS.each do |framework, hints|
217
- has_directory = hints[:directories]&.any? { |dir| Dir.exist?(File.join(project_dir, dir)) }
218
- has_dependency = hints[:dependencies]&.any? { |pattern| search_project_for_pattern(pattern, limit_files: ["Gemfile", "Gemfile.lock", "package.json", "pyproject.toml", "requirements.txt", "go.mod", "mix.exs", "composer.json", "Cargo.toml"]) }
219
- has_files = hints[:files]&.any? { |glob| project_glob?(glob) }
256
+ evidence = []
257
+ confidence = 0.0
258
+
259
+ # Check for test directories
260
+ if hints[:directories]
261
+ found_dirs = hints[:directories].select { |dir| Dir.exist?(File.join(project_dir, dir)) }
262
+ if found_dirs.any?
263
+ evidence << "Found directories: #{found_dirs.join(", ")}"
264
+ confidence += 0.5
265
+ end
266
+ end
267
+
268
+ # Check for dependencies in lockfiles/manifests
269
+ hints[:dependencies]&.each do |pattern|
270
+ matched_files = []
271
+ dependency_files.each do |dep_file|
272
+ path = File.join(project_dir, dep_file)
273
+ next unless File.exist?(path)
274
+
275
+ begin
276
+ if File.read(path).match?(pattern)
277
+ matched_files << dep_file
278
+ end
279
+ rescue Errno::ENOENT, ArgumentError, Encoding::InvalidByteSequenceError
280
+ next
281
+ end
282
+ end
283
+
284
+ if matched_files.any?
285
+ evidence << "Found dependency pattern '#{pattern.inspect}' in #{matched_files.join(", ")}"
286
+ confidence += 0.6
287
+ end
288
+ end
220
289
 
221
- results << framework if has_directory || has_dependency || has_files
290
+ # Check for test files
291
+ if hints[:files]
292
+ matched_globs = hints[:files].select { |glob| project_glob?(glob) }
293
+ if matched_globs.any?
294
+ evidence << "Found test files matching: #{matched_globs.join(", ")}"
295
+ confidence += 0.4
296
+ end
297
+ end
298
+
299
+ # Only include test frameworks with evidence
300
+ if evidence.any? && confidence > 0.0
301
+ confidence = [confidence, 1.0].min
302
+
303
+ results << {
304
+ name: framework,
305
+ confidence: confidence,
306
+ evidence: evidence
307
+ }
308
+ end
222
309
  end
223
310
 
224
- results.uniq.sort
311
+ # Sort by confidence (descending) then name
312
+ results.sort_by { |r| [-r[:confidence], r[:name]] }
225
313
  end
226
314
 
227
315
  def detect_tooling
228
- tooling = Hash.new { |hash, key| hash[key] = [] }
316
+ results = []
229
317
 
230
318
  TOOLING_HINTS.each do |tool, indicators|
231
- hit = indicators.any? do |indicator|
232
- if indicator.include?("*")
233
- end
234
- project_glob?(indicator)
319
+ evidence = []
320
+ confidence = 0.0
321
+
322
+ matched_indicators = indicators.select { |indicator| project_glob?(indicator) }
323
+ if matched_indicators.any?
324
+ evidence << "Found config files: #{matched_indicators.join(", ")}"
325
+ confidence += 0.8
235
326
  end
236
- tooling[tool] << "config" if hit
327
+
328
+ results << {tool: tool, evidence: evidence, confidence: confidence} if evidence.any?
237
329
  end
238
330
 
239
331
  # Post-process for package.json to extract scripts referencing linters
@@ -242,15 +334,31 @@ module Aidp
242
334
  begin
243
335
  json = JSON.parse(File.read(package_json_path))
244
336
  scripts = json.fetch("scripts", {})
245
- tooling[:eslint] << "package.json scripts" if scripts.values.any? { |cmd| cmd.include?("eslint") }
246
- tooling[:prettier] << "package.json scripts" if scripts.values.any? { |cmd| cmd.include?("prettier") }
247
- tooling[:jest] << "package.json scripts" if scripts.values.any? { |cmd| cmd.include?("jest") }
337
+
338
+ [:eslint, :prettier, :jest].each do |tool|
339
+ tool_str = tool.to_s
340
+ if scripts.values.any? { |cmd| cmd.include?(tool_str) }
341
+ # Check if we already have this tool from config detection
342
+ existing = results.find { |r| r[:tool] == tool }
343
+ if existing
344
+ existing[:evidence] << "Referenced in package.json scripts"
345
+ existing[:confidence] = [existing[:confidence] + 0.3, 1.0].min
346
+ else
347
+ results << {
348
+ tool: tool,
349
+ evidence: ["Referenced in package.json scripts"],
350
+ confidence: 0.6
351
+ }
352
+ end
353
+ end
354
+ end
248
355
  rescue JSON::ParserError
249
356
  # ignore malformed package.json
250
357
  end
251
358
  end
252
359
 
253
- tooling.delete_if { |_tool, evidence| evidence.empty? }
360
+ # Sort by confidence (descending) then tool name
361
+ results.sort_by { |r| [-r[:confidence], r[:tool].to_s] }
254
362
  end
255
363
 
256
364
  def collect_repo_stats
@@ -306,13 +414,33 @@ module Aidp
306
414
  def search_files_for_pattern(files, pattern)
307
415
  files.any? do |file|
308
416
  Dir.glob(File.join(project_dir, file)).any? do |path|
309
- File.read(path).match?(pattern)
417
+ # Special handling for package.json - only search in dependencies
418
+ if File.basename(path) == "package.json"
419
+ check_package_json_dependency(path, pattern)
420
+ else
421
+ File.read(path).match?(pattern)
422
+ end
310
423
  rescue Errno::ENOENT, Errno::EISDIR
311
424
  false
312
425
  end
313
426
  end
314
427
  end
315
428
 
429
+ def check_package_json_dependency(path, pattern)
430
+ json = JSON.parse(File.read(path))
431
+ deps = json.fetch("dependencies", {})
432
+ dev_deps = json.fetch("devDependencies", {})
433
+ peer_deps = json.fetch("peerDependencies", {})
434
+
435
+ all_deps = deps.keys + dev_deps.keys + peer_deps.keys
436
+ all_deps.any? { |dep| dep.match?(pattern) }
437
+ rescue JSON::ParserError, Errno::ENOENT
438
+ # Fallback to simple text search if JSON parsing fails
439
+ File.read(path).match?(pattern)
440
+ rescue
441
+ false
442
+ end
443
+
316
444
  def search_project_for_pattern(pattern, limit_files: nil)
317
445
  if limit_files
318
446
  limit_files.any? do |file|