aidp 0.16.0 → 0.17.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aidp/analyze/error_handler.rb +32 -13
  3. data/lib/aidp/analyze/kb_inspector.rb +2 -3
  4. data/lib/aidp/analyze/progress.rb +6 -11
  5. data/lib/aidp/cli/mcp_dashboard.rb +1 -1
  6. data/lib/aidp/cli.rb +300 -33
  7. data/lib/aidp/config.rb +1 -1
  8. data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
  9. data/lib/aidp/execute/checkpoint.rb +1 -1
  10. data/lib/aidp/execute/future_work_backlog.rb +1 -1
  11. data/lib/aidp/execute/progress.rb +6 -9
  12. data/lib/aidp/execute/repl_macros.rb +79 -10
  13. data/lib/aidp/harness/config_loader.rb +2 -2
  14. data/lib/aidp/harness/config_validator.rb +1 -1
  15. data/lib/aidp/harness/enhanced_runner.rb +16 -7
  16. data/lib/aidp/harness/error_handler.rb +12 -5
  17. data/lib/aidp/harness/provider_manager.rb +4 -19
  18. data/lib/aidp/harness/runner.rb +2 -2
  19. data/lib/aidp/harness/state/persistence.rb +9 -10
  20. data/lib/aidp/harness/state/workflow_state.rb +3 -2
  21. data/lib/aidp/harness/state_manager.rb +33 -97
  22. data/lib/aidp/harness/status_display.rb +22 -12
  23. data/lib/aidp/harness/ui/enhanced_tui.rb +3 -4
  24. data/lib/aidp/harness/user_interface.rb +11 -6
  25. data/lib/aidp/jobs/background_runner.rb +8 -2
  26. data/lib/aidp/logger.rb +1 -1
  27. data/lib/aidp/message_display.rb +9 -2
  28. data/lib/aidp/providers/anthropic.rb +1 -1
  29. data/lib/aidp/providers/base.rb +4 -4
  30. data/lib/aidp/providers/codex.rb +1 -1
  31. data/lib/aidp/providers/cursor.rb +1 -1
  32. data/lib/aidp/providers/gemini.rb +1 -1
  33. data/lib/aidp/providers/github_copilot.rb +1 -1
  34. data/lib/aidp/providers/macos_ui.rb +1 -1
  35. data/lib/aidp/providers/opencode.rb +1 -1
  36. data/lib/aidp/skills/registry.rb +31 -29
  37. data/lib/aidp/skills/router.rb +178 -0
  38. data/lib/aidp/skills/wizard/builder.rb +141 -0
  39. data/lib/aidp/skills/wizard/controller.rb +145 -0
  40. data/lib/aidp/skills/wizard/differ.rb +232 -0
  41. data/lib/aidp/skills/wizard/prompter.rb +317 -0
  42. data/lib/aidp/skills/wizard/template_library.rb +164 -0
  43. data/lib/aidp/skills/wizard/writer.rb +105 -0
  44. data/lib/aidp/version.rb +1 -1
  45. data/lib/aidp/watch/plan_generator.rb +1 -1
  46. data/lib/aidp/watch/repository_client.rb +13 -9
  47. data/lib/aidp/workflows/guided_agent.rb +2 -312
  48. data/lib/aidp/workstream_executor.rb +8 -2
  49. data/templates/skills/README.md +334 -0
  50. data/templates/skills/architecture_analyst/SKILL.md +173 -0
  51. data/templates/skills/product_strategist/SKILL.md +141 -0
  52. data/templates/skills/repository_analyst/SKILL.md +117 -0
  53. data/templates/skills/test_analyzer/SKILL.md +213 -0
  54. metadata +13 -1
@@ -0,0 +1,317 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+
5
+ module Aidp
6
+ module Skills
7
+ module Wizard
8
+ # Interactive prompter for the skill wizard
9
+ #
10
+ # Uses TTY::Prompt to gather user input through guided questions.
11
+ #
12
+ # @example Basic usage
13
+ # prompter = Prompter.new
14
+ # responses = prompter.gather_responses(template_library)
15
+ class Prompter
16
+ attr_reader :prompt
17
+
18
+ def initialize(prompt: TTY::Prompt.new)
19
+ @prompt = prompt
20
+ end
21
+
22
+ # Gather all responses for creating a skill
23
+ #
24
+ # @param template_library [TemplateLibrary] Library for template selection
25
+ # @param options [Hash] Options for the wizard
26
+ # @option options [String] :id Pre-filled skill ID
27
+ # @option options [String] :name Pre-filled skill name
28
+ # @option options [Boolean] :minimal Skip optional sections
29
+ # @option options [String] :from_template Template ID to inherit from
30
+ # @option options [String] :clone Skill ID to clone
31
+ # @return [Hash] Complete set of responses
32
+ def gather_responses(template_library, options: {})
33
+ responses = {}
34
+
35
+ # Step 1: Template selection
36
+ if options[:from_template]
37
+ responses[:base_skill] = template_library.find(options[:from_template])
38
+ unless responses[:base_skill]
39
+ raise Aidp::Errors::ValidationError, "Template not found: #{options[:from_template]}"
40
+ end
41
+ elsif options[:clone]
42
+ responses[:base_skill] = template_library.find(options[:clone])
43
+ unless responses[:base_skill]
44
+ raise Aidp::Errors::ValidationError, "Skill not found: #{options[:clone]}"
45
+ end
46
+ elsif !options[:minimal]
47
+ responses[:base_skill] = prompt_template_selection(template_library)
48
+ end
49
+
50
+ # Step 2: Identity & Metadata
51
+ responses.merge!(prompt_identity(options))
52
+
53
+ # Step 3: Expertise & Keywords
54
+ responses.merge!(prompt_expertise) unless options[:minimal]
55
+
56
+ # Step 4: When to use
57
+ responses.merge!(prompt_when_to_use) unless options[:minimal]
58
+
59
+ # Step 5: Compatible providers
60
+ responses[:compatible_providers] = prompt_providers unless options[:minimal]
61
+
62
+ # Step 6: Content
63
+ responses[:content] = prompt_content(responses[:name], responses[:base_skill])
64
+
65
+ responses
66
+ end
67
+
68
+ private
69
+
70
+ # Prompt for template selection
71
+ #
72
+ # @param template_library [TemplateLibrary] Available templates
73
+ # @return [Skill, nil] Selected base skill or nil
74
+ def prompt_template_selection(template_library)
75
+ prompt.say("\n" + "=" * 60)
76
+ prompt.say("Create New Skill")
77
+ prompt.say("=" * 60 + "\n")
78
+
79
+ choice = prompt.select("How would you like to create your skill?") do |menu|
80
+ menu.choice "Start from scratch", :from_scratch
81
+ menu.choice "Inherit from a template", :inherit
82
+ menu.choice "Clone an existing skill", :clone
83
+ end
84
+
85
+ case choice
86
+ when :from_scratch
87
+ nil
88
+ when :inherit, :clone
89
+ select_base_skill(template_library)
90
+ end
91
+ end
92
+
93
+ # Select a base skill from templates
94
+ #
95
+ # @param template_library [TemplateLibrary] Available templates
96
+ # @return [Skill, nil] Selected skill
97
+ def select_base_skill(template_library)
98
+ skills = template_library.skill_list
99
+
100
+ if skills.empty?
101
+ prompt.warn("No templates available")
102
+ return nil
103
+ end
104
+
105
+ choices = skills.map do |skill_info|
106
+ source_label = (skill_info[:source] == :template) ? "(template)" : "(project)"
107
+ {
108
+ name: "#{skill_info[:name]} #{source_label} - #{skill_info[:description]}",
109
+ value: skill_info[:id]
110
+ }
111
+ end
112
+
113
+ selected_id = prompt.select("Select a base skill:", choices, per_page: 10)
114
+ template_library.find(selected_id)
115
+ end
116
+
117
+ # Prompt for identity and metadata
118
+ #
119
+ # @param options [Hash] Pre-filled values
120
+ # @return [Hash] Identity responses
121
+ def prompt_identity(options)
122
+ responses = {}
123
+
124
+ # Name
125
+ default_name = options[:name]
126
+ responses[:name] = prompt.ask("Skill Name:", default: default_name) do |q|
127
+ q.required true
128
+ q.modify :strip
129
+ end
130
+
131
+ # ID (auto-suggest from name)
132
+ suggested_id = options[:id] || slugify(responses[:name])
133
+ responses[:id] = prompt.ask("Skill ID:", default: suggested_id) do |q|
134
+ q.required true
135
+ q.modify :strip, :down
136
+ q.validate(/\A[a-z0-9_]+\z/, "ID must be lowercase alphanumeric with underscores only")
137
+ end
138
+
139
+ # Description
140
+ responses[:description] = prompt.ask("Description (one-line summary):") do |q|
141
+ q.required true
142
+ q.modify :strip
143
+ end
144
+
145
+ # Version
146
+ responses[:version] = prompt.ask("Version:", default: "1.0.0") do |q|
147
+ q.validate(/\A\d+\.\d+\.\d+\z/, "Must be semantic version (X.Y.Z)")
148
+ end
149
+
150
+ responses
151
+ end
152
+
153
+ # Prompt for expertise and keywords
154
+ #
155
+ # @return [Hash] Expertise responses
156
+ def prompt_expertise
157
+ responses = {}
158
+
159
+ # Expertise areas
160
+ prompt.say("\nExpertise Areas:")
161
+ prompt.say("(Enter expertise areas, one per line. Leave blank and press Enter when done)")
162
+
163
+ expertise = []
164
+ loop do
165
+ area = prompt.ask(" Area #{expertise.size + 1}:", required: false) do |q|
166
+ q.modify :strip
167
+ end
168
+ break if area.nil? || area.empty?
169
+ expertise << area
170
+ end
171
+ responses[:expertise] = expertise
172
+
173
+ # Keywords
174
+ keywords_input = prompt.ask("\nKeywords (comma-separated):", required: false) do |q|
175
+ q.modify :strip
176
+ end
177
+ responses[:keywords] = keywords_input ? keywords_input.split(",").map(&:strip).reject(&:empty?) : []
178
+
179
+ responses
180
+ end
181
+
182
+ # Prompt for when to use guidance
183
+ #
184
+ # @return [Hash] When to use responses
185
+ def prompt_when_to_use
186
+ responses = {}
187
+
188
+ # When to use
189
+ prompt.say("\nWhen to Use This Skill:")
190
+ prompt.say("(Enter use cases, one per line. Leave blank and press Enter when done)")
191
+
192
+ when_to_use = []
193
+ loop do
194
+ use_case = prompt.ask(" Use case #{when_to_use.size + 1}:", required: false) do |q|
195
+ q.modify :strip
196
+ end
197
+ break if use_case.nil? || use_case.empty?
198
+ when_to_use << use_case
199
+ end
200
+ responses[:when_to_use] = when_to_use
201
+
202
+ # When NOT to use
203
+ prompt.say("\nWhen NOT to Use This Skill:")
204
+ prompt.say("(Leave blank and press Enter when done)")
205
+
206
+ when_not_to_use = []
207
+ loop do
208
+ use_case = prompt.ask(" Avoid when #{when_not_to_use.size + 1}:", required: false) do |q|
209
+ q.modify :strip
210
+ end
211
+ break if use_case.nil? || use_case.empty?
212
+ when_not_to_use << use_case
213
+ end
214
+ responses[:when_not_to_use] = when_not_to_use
215
+
216
+ responses
217
+ end
218
+
219
+ # Prompt for compatible providers
220
+ #
221
+ # @return [Array<String>] Selected providers
222
+ def prompt_providers
223
+ choices = [
224
+ {name: "All providers (default)", value: []},
225
+ {name: "Anthropic only", value: ["anthropic"]},
226
+ {name: "OpenAI only", value: ["openai"]},
227
+ {name: "Codex only", value: ["codex"]},
228
+ {name: "Cursor only", value: ["cursor"]},
229
+ {name: "Custom selection", value: :custom}
230
+ ]
231
+
232
+ selection = prompt.select("\nCompatible AI Providers:", choices)
233
+
234
+ if selection == :custom
235
+ prompt.multi_select("Select providers:") do |menu|
236
+ menu.choice "Anthropic", "anthropic"
237
+ menu.choice "OpenAI", "openai"
238
+ menu.choice "Codex", "codex"
239
+ menu.choice "Cursor", "cursor"
240
+ end
241
+ else
242
+ selection
243
+ end
244
+ end
245
+
246
+ # Prompt for skill content
247
+ #
248
+ # @param skill_name [String] Name of the skill
249
+ # @param base_skill [Skill, nil] Base skill if inheriting
250
+ # @return [String] Markdown content
251
+ def prompt_content(skill_name, base_skill)
252
+ if base_skill
253
+ prompt.say("\nContent will be inherited from: #{base_skill.name}")
254
+ if prompt.yes?("Would you like to customize the content?")
255
+ prompt_custom_content(skill_name)
256
+ else
257
+ base_skill.content
258
+ end
259
+ else
260
+ prompt_custom_content(skill_name)
261
+ end
262
+ end
263
+
264
+ # Prompt for custom content
265
+ #
266
+ # @param skill_name [String] Name of the skill
267
+ # @return [String] Markdown content
268
+ def prompt_custom_content(skill_name)
269
+ prompt.say("\nContent:")
270
+ lines = prompt.multiline("Enter the skill content (markdown):")
271
+
272
+ if lines.empty?
273
+ # Provide a minimal template
274
+ generate_default_content(skill_name)
275
+ else
276
+ lines.join("\n")
277
+ end
278
+ end
279
+
280
+ # Generate default content template
281
+ #
282
+ # @param skill_name [String] Name of the skill
283
+ # @return [String] Default markdown content
284
+ def generate_default_content(skill_name)
285
+ <<~MARKDOWN
286
+ # #{skill_name}
287
+
288
+ You are a **#{skill_name}**, an expert in [describe expertise area].
289
+
290
+ ## Your Core Capabilities
291
+
292
+ - [Capability 1]
293
+ - [Capability 2]
294
+ - [Capability 3]
295
+
296
+ ## Your Approach
297
+
298
+ [Describe your philosophy and approach to tasks]
299
+ MARKDOWN
300
+ end
301
+
302
+ # Convert a name to a slug (for ID suggestion)
303
+ #
304
+ # @param name [String] Human-readable name
305
+ # @return [String] Slugified ID
306
+ def slugify(name)
307
+ name.to_s
308
+ .downcase
309
+ .gsub(/[^a-z0-9\s_-]/, "")
310
+ .gsub(/\s+/, "_")
311
+ .gsub(/-+/, "_").squeeze("_")
312
+ .gsub(/\A_|_\z/, "")
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../loader"
4
+
5
+ module Aidp
6
+ module Skills
7
+ module Wizard
8
+ # Manages skill templates for the wizard
9
+ #
10
+ # Loads and provides access to template skills from the gem's templates/skills/ directory
11
+ # and existing project skills from .aidp/skills/ for cloning.
12
+ #
13
+ # @example Loading templates
14
+ # library = TemplateLibrary.new(project_dir: "/path/to/project")
15
+ # templates = library.templates
16
+ # template = library.find("base_developer")
17
+ class TemplateLibrary
18
+ attr_reader :project_dir
19
+
20
+ # Initialize template library
21
+ #
22
+ # @param project_dir [String] Root directory of the project
23
+ def initialize(project_dir:)
24
+ @project_dir = project_dir
25
+ @templates = nil
26
+ @project_skills = nil
27
+ end
28
+
29
+ # Get all available templates
30
+ #
31
+ # @return [Array<Skill>] Array of template skills
32
+ def templates
33
+ load_templates unless loaded?
34
+ @templates
35
+ end
36
+
37
+ # Get all project skills (for cloning)
38
+ #
39
+ # @return [Array<Skill>] Array of project-specific skills
40
+ def project_skills
41
+ load_project_skills unless project_skills_loaded?
42
+ @project_skills
43
+ end
44
+
45
+ # Get all skills (templates + project skills)
46
+ #
47
+ # @return [Array<Skill>] Combined array of all skills
48
+ def all
49
+ templates + project_skills
50
+ end
51
+
52
+ # Find a template or project skill by ID
53
+ #
54
+ # @param skill_id [String] Skill identifier
55
+ # @return [Skill, nil] Found skill or nil
56
+ def find(skill_id)
57
+ all.find { |skill| skill.id == skill_id.to_s }
58
+ end
59
+
60
+ # Check if a skill ID exists
61
+ #
62
+ # @param skill_id [String] Skill identifier
63
+ # @return [Boolean] True if skill exists
64
+ def exists?(skill_id)
65
+ !find(skill_id).nil?
66
+ end
67
+
68
+ # Get template names for display
69
+ #
70
+ # @return [Array<Hash>] Array of {id, name, description} hashes
71
+ def template_list
72
+ templates.map do |skill|
73
+ {
74
+ id: skill.id,
75
+ name: skill.name,
76
+ description: skill.description,
77
+ source: :template
78
+ }
79
+ end
80
+ end
81
+
82
+ # Get project skill names for display
83
+ #
84
+ # @return [Array<Hash>] Array of {id, name, description} hashes
85
+ def project_skill_list
86
+ project_skills.map do |skill|
87
+ {
88
+ id: skill.id,
89
+ name: skill.name,
90
+ description: skill.description,
91
+ source: :project
92
+ }
93
+ end
94
+ end
95
+
96
+ # Get combined list for selection
97
+ #
98
+ # @return [Array<Hash>] Array of {id, name, description, source} hashes
99
+ def skill_list
100
+ template_list + project_skill_list
101
+ end
102
+
103
+ private
104
+
105
+ # Load templates from gem directory
106
+ def load_templates
107
+ @templates = Loader.load_from_directory(templates_path)
108
+ Aidp.log_debug(
109
+ "wizard",
110
+ "Loaded templates",
111
+ count: @templates.size,
112
+ path: templates_path
113
+ )
114
+ end
115
+
116
+ # Load project skills
117
+ def load_project_skills
118
+ @project_skills = if Dir.exist?(project_skills_path)
119
+ Loader.load_from_directory(project_skills_path)
120
+ else
121
+ []
122
+ end
123
+
124
+ Aidp.log_debug(
125
+ "wizard",
126
+ "Loaded project skills",
127
+ count: @project_skills.size,
128
+ path: project_skills_path
129
+ )
130
+ end
131
+
132
+ # Check if templates are loaded
133
+ #
134
+ # @return [Boolean] True if loaded
135
+ def loaded?
136
+ !@templates.nil?
137
+ end
138
+
139
+ # Check if project skills are loaded
140
+ #
141
+ # @return [Boolean] True if loaded
142
+ def project_skills_loaded?
143
+ !@project_skills.nil?
144
+ end
145
+
146
+ # Get path to templates directory
147
+ #
148
+ # @return [String] Path to templates/skills directory
149
+ def templates_path
150
+ # Get the gem root directory (go up 4 levels from lib/aidp/skills/wizard/template_library.rb)
151
+ gem_root = File.expand_path("../../../..", __dir__)
152
+ File.join(gem_root, "templates", "skills")
153
+ end
154
+
155
+ # Get path to project skills directory
156
+ #
157
+ # @return [String] Path to .aidp/skills directory
158
+ def project_skills_path
159
+ File.join(project_dir, ".aidp", "skills")
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Aidp
6
+ module Skills
7
+ module Wizard
8
+ # Writes skills to the filesystem
9
+ #
10
+ # Handles creating directories, writing SKILL.md files, and creating backups.
11
+ #
12
+ # @example Writing a new skill
13
+ # writer = Writer.new(project_dir: "/path/to/project")
14
+ # writer.write(skill, content: "...", dry_run: false)
15
+ #
16
+ # @example Dry-run mode
17
+ # writer.write(skill, content: "...", dry_run: true) # Returns path without writing
18
+ class Writer
19
+ attr_reader :project_dir
20
+
21
+ # Initialize writer
22
+ #
23
+ # @param project_dir [String] Root directory of the project
24
+ def initialize(project_dir:)
25
+ @project_dir = project_dir
26
+ end
27
+
28
+ # Write a skill to disk
29
+ #
30
+ # @param skill [Skill] Skill object
31
+ # @param content [String] Complete SKILL.md content
32
+ # @param dry_run [Boolean] If true, don't actually write
33
+ # @param backup [Boolean] If true, create backup of existing file
34
+ # @return [String] Path to written file
35
+ def write(skill, content:, dry_run: false, backup: true)
36
+ skill_path = path_for_skill(skill.id)
37
+ skill_dir = File.dirname(skill_path)
38
+
39
+ if dry_run
40
+ Aidp.log_debug("wizard", "Dry-run mode, would write to", path: skill_path)
41
+ return skill_path
42
+ end
43
+
44
+ # Create backup if file exists and backup is requested
45
+ create_backup(skill_path) if backup && File.exist?(skill_path)
46
+
47
+ # Create directory if it doesn't exist
48
+ FileUtils.mkdir_p(skill_dir) unless Dir.exist?(skill_dir)
49
+
50
+ # Write file
51
+ File.write(skill_path, content)
52
+
53
+ Aidp.log_info(
54
+ "wizard",
55
+ "Wrote skill",
56
+ skill_id: skill.id,
57
+ path: skill_path,
58
+ size: content.bytesize
59
+ )
60
+
61
+ skill_path
62
+ end
63
+
64
+ # Get the path where a skill would be written
65
+ #
66
+ # @param skill_id [String] Skill identifier
67
+ # @return [String] Full path to SKILL.md file
68
+ def path_for_skill(skill_id)
69
+ File.join(project_dir, ".aidp", "skills", skill_id, "SKILL.md")
70
+ end
71
+
72
+ # Check if a skill already exists
73
+ #
74
+ # @param skill_id [String] Skill identifier
75
+ # @return [Boolean] True if skill file exists
76
+ def exists?(skill_id)
77
+ File.exist?(path_for_skill(skill_id))
78
+ end
79
+
80
+ private
81
+
82
+ # Create a backup of an existing file
83
+ #
84
+ # @param file_path [String] Path to file to backup
85
+ def create_backup(file_path)
86
+ backup_path = "#{file_path}.backup"
87
+ timestamp_backup_path = "#{file_path}.#{Time.now.strftime("%Y%m%d_%H%M%S")}.backup"
88
+
89
+ # Create timestamped backup
90
+ FileUtils.cp(file_path, timestamp_backup_path)
91
+
92
+ # Also create/update .backup for convenience
93
+ FileUtils.cp(file_path, backup_path)
94
+
95
+ Aidp.log_debug(
96
+ "wizard",
97
+ "Created backup",
98
+ original: file_path,
99
+ backup: timestamp_backup_path
100
+ )
101
+ end
102
+ end
103
+ end
104
+ end
105
+ 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.16.0"
4
+ VERSION = "0.17.1"
5
5
  end
@@ -67,7 +67,7 @@ module Aidp
67
67
 
68
68
  def generate_with_provider(provider, issue)
69
69
  payload = build_prompt(issue)
70
- response = provider.send(prompt: payload)
70
+ response = provider.send_message(prompt: payload)
71
71
  parsed = parse_structured_response(response)
72
72
 
73
73
  return parsed if parsed
@@ -11,6 +11,16 @@ module Aidp
11
11
  # (works for private repositories) and falls back to public REST endpoints
12
12
  # when the CLI is unavailable.
13
13
  class RepositoryClient
14
+ # Binary availability checker for testing
15
+ class BinaryChecker
16
+ def gh_cli_available?
17
+ _stdout, _stderr, status = Open3.capture3("gh", "--version")
18
+ status.success?
19
+ rescue Errno::ENOENT
20
+ false
21
+ end
22
+ end
23
+
14
24
  attr_reader :owner, :repo
15
25
 
16
26
  def self.parse_issues_url(issues_url)
@@ -24,10 +34,11 @@ module Aidp
24
34
  end
25
35
  end
26
36
 
27
- def initialize(owner:, repo:, gh_available: nil)
37
+ def initialize(owner:, repo:, gh_available: nil, binary_checker: BinaryChecker.new)
28
38
  @owner = owner
29
39
  @repo = repo
30
- @gh_available = gh_available.nil? ? gh_cli_available? : gh_available
40
+ @binary_checker = binary_checker
41
+ @gh_available = gh_available.nil? ? @binary_checker.gh_cli_available? : gh_available
31
42
  end
32
43
 
33
44
  def gh_available?
@@ -56,13 +67,6 @@ module Aidp
56
67
 
57
68
  private
58
69
 
59
- def gh_cli_available?
60
- _stdout, _stderr, status = Open3.capture3("gh", "--version")
61
- status.success?
62
- rescue Errno::ENOENT
63
- false
64
- end
65
-
66
70
  def list_issues_via_gh(labels:, state:)
67
71
  json_fields = %w[number title labels updatedAt state url assignees]
68
72
  cmd = ["gh", "issue", "list", "--repo", full_repo, "--state", state, "--json", json_fields.join(",")]