aidp 0.13.0 → 0.14.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 +7 -0
  3. data/lib/aidp/cli/first_run_wizard.rb +28 -303
  4. data/lib/aidp/cli/issue_importer.rb +359 -0
  5. data/lib/aidp/cli.rb +151 -3
  6. data/lib/aidp/daemon/process_manager.rb +146 -0
  7. data/lib/aidp/daemon/runner.rb +232 -0
  8. data/lib/aidp/execute/async_work_loop_runner.rb +216 -0
  9. data/lib/aidp/execute/future_work_backlog.rb +411 -0
  10. data/lib/aidp/execute/guard_policy.rb +246 -0
  11. data/lib/aidp/execute/instruction_queue.rb +131 -0
  12. data/lib/aidp/execute/interactive_repl.rb +335 -0
  13. data/lib/aidp/execute/repl_macros.rb +651 -0
  14. data/lib/aidp/execute/steps.rb +8 -0
  15. data/lib/aidp/execute/work_loop_runner.rb +322 -36
  16. data/lib/aidp/execute/work_loop_state.rb +162 -0
  17. data/lib/aidp/harness/config_schema.rb +88 -0
  18. data/lib/aidp/harness/configuration.rb +48 -1
  19. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +2 -0
  20. data/lib/aidp/init/doc_generator.rb +256 -0
  21. data/lib/aidp/init/project_analyzer.rb +343 -0
  22. data/lib/aidp/init/runner.rb +83 -0
  23. data/lib/aidp/init.rb +5 -0
  24. data/lib/aidp/logger.rb +279 -0
  25. data/lib/aidp/setup/wizard.rb +777 -0
  26. data/lib/aidp/tooling_detector.rb +115 -0
  27. data/lib/aidp/version.rb +1 -1
  28. data/lib/aidp/watch/build_processor.rb +282 -0
  29. data/lib/aidp/watch/plan_generator.rb +166 -0
  30. data/lib/aidp/watch/plan_processor.rb +83 -0
  31. data/lib/aidp/watch/repository_client.rb +243 -0
  32. data/lib/aidp/watch/runner.rb +93 -0
  33. data/lib/aidp/watch/state_store.rb +105 -0
  34. data/lib/aidp/watch.rb +9 -0
  35. data/lib/aidp.rb +14 -0
  36. data/templates/implementation/simple_task.md +36 -0
  37. metadata +26 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f6484f93a1d57d07f223f49abab2f1a919490d4cb25e6623e76ca85b430a0be
4
- data.tar.gz: 6f765c07f09d77cf3ffab88c36aa9a04acf90c39fc44d326a594b7ff4b67759b
3
+ metadata.gz: 75047af72249152053d5dfe5b5e7800c0c8c844c7a513cf227601b8adb006af2
4
+ data.tar.gz: 574d521b5065f17afa618cfd6b672ed834badca459631011dddde7888796d7c6
5
5
  SHA512:
6
- metadata.gz: e3219a59423ae33fc74bfaf38ac3ecebc3a157ac4f789c22252527652f02f63a1d98078de3db8b277d323073bf5cec8b86c97e9e7749f9a4e3097c6d849a4c2e
7
- data.tar.gz: 3ec1273d7a35839fef07a02fa3a77e9340c008146c87ae790c8829f851f668af5fe1a1df12e28244b2b78c3313dd4ca667cb5e794232a3c4507f09f540d46cd9
6
+ metadata.gz: 323d8e0cfe1f37934d703488c00cbb4d1769bc0dea9d6f2d826a1a995861e6a8c84d697963a5e9880e5555e1d3e3f32f551f5db8c7ca273de8d79514dbad0e70
7
+ data.tar.gz: 98c37900f0672cade4a7fa838ae94282355e8645fc0ea9a0e2a2e6b2c3d78d265f91e4f66c0f592cf1b8d3297984fdc6fdc26d8efd584df8a0b0c2b543c1f3b8
data/README.md CHANGED
@@ -11,6 +11,13 @@ gem install aidp
11
11
  # Navigate to your project
12
12
  cd /your/project
13
13
 
14
+ # Launch the interactive configuration wizard
15
+ aidp config --interactive
16
+
17
+ # Analyze and bootstrap project docs
18
+ aidp init
19
+ # Creates LLM_STYLE_GUIDE.md, PROJECT_ANALYSIS.md, CODE_QUALITY_PLAN.md
20
+
14
21
  # Start an interactive workflow
15
22
  aidp execute
16
23
 
@@ -1,44 +1,36 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "yaml"
4
+ require "time"
5
5
  require "tty-prompt"
6
- require_relative "../harness/provider_factory"
7
- require_relative "../config/paths"
8
6
 
9
7
  module Aidp
10
8
  class CLI
11
- # Handles interactive first-time project setup when no aidp.yml exists
9
+ # Wrapper around Aidp::Setup::Wizard to preserve existing CLI entry points.
12
10
  class FirstRunWizard
13
11
  include Aidp::MessageDisplay
14
12
 
15
- TEMPLATES_DIR = File.expand_path(File.join(__dir__, "..", "..", "..", "templates"))
16
-
17
13
  def self.ensure_config(project_dir, non_interactive: false, prompt: TTY::Prompt.new)
18
14
  return true if Aidp::Config.config_exists?(project_dir)
19
15
 
20
16
  wizard = new(project_dir, prompt: prompt)
21
17
 
22
18
  if non_interactive
23
- # Non-interactive environment - create minimal config silently
24
- path = wizard.send(:write_minimal_config, project_dir)
25
- wizard.send(:display_message, "Created minimal configuration at #{wizard.send(:relative, path)} (non-interactive default)", type: :success)
26
- return true
19
+ wizard.create_minimal_config
20
+ wizard.send(:display_message, "Created minimal configuration (non-interactive default)", type: :success)
21
+ true
22
+ else
23
+ wizard.run
27
24
  end
28
-
29
- wizard.run
30
25
  end
31
26
 
32
27
  def self.setup_config(project_dir, non_interactive: false, prompt: TTY::Prompt.new)
33
- wizard = new(project_dir, prompt: prompt)
34
-
35
28
  if non_interactive
36
- # Non-interactive environment - skip setup
37
- wizard.send(:display_message, "Configuration setup skipped in non-interactive environment", type: :info)
29
+ new(project_dir, prompt: prompt).send(:display_message, "Configuration setup skipped in non-interactive environment", type: :info)
38
30
  return true
39
31
  end
40
32
 
41
- wizard.run_setup_config
33
+ new(project_dir, prompt: prompt).run
42
34
  end
43
35
 
44
36
  def initialize(project_dir, prompt: TTY::Prompt.new)
@@ -47,300 +39,33 @@ module Aidp
47
39
  end
48
40
 
49
41
  def run
50
- banner
51
- # Always run the full interactive custom configuration flow.
52
- finish(run_custom)
53
- end
54
-
55
- def run_setup_config
56
- @prompt.say("🔧 Configuration Setup", color: :blue)
57
- @prompt.say("Setting up your configuration file with current values as defaults.")
58
- @prompt.say("")
59
-
60
- # Load existing config to use as defaults (if it exists)
61
- existing_config = load_existing_config
62
-
63
- if existing_config
64
- # Run custom configuration with existing values as defaults
65
- finish(run_custom_with_defaults(existing_config))
66
- else
67
- # No existing config, run the normal setup flow
68
- @prompt.say("No existing configuration found. Running first-time setup...")
69
- @prompt.say("")
70
- run
71
- end
72
- end
73
-
74
- private
75
-
76
- def banner
77
- display_message("\n🚀 First-time setup detected", type: :highlight)
78
- display_message("No 'aidp.yml' configuration file found in #{relative(@project_dir)}.")
79
- display_message("Creating a configuration so you can start using AI Dev Pipeline.")
80
- display_message("")
81
- end
82
-
83
- def finish(path)
84
- if path
85
- display_message("\n✅ Configuration created at #{relative(path)}", type: :success)
86
- display_message("You can edit this file anytime. Continuing startup...\n")
87
- true
88
- else
89
- display_message("❌ Failed to create configuration file.", type: :error)
90
- false
91
- end
42
+ wizard = Aidp::Setup::Wizard.new(@project_dir, prompt: @prompt)
43
+ wizard.run
92
44
  end
93
45
 
94
- def copy_template(filename)
95
- src = File.join(TEMPLATES_DIR, filename)
96
- unless File.exist?(src)
97
- display_message("Template not found: #{filename}", type: :error)
98
- return nil
99
- end
100
- dest = Aidp::ConfigPaths.config_file(@project_dir)
46
+ def create_minimal_config
101
47
  Aidp::ConfigPaths.ensure_config_dir(@project_dir)
102
- File.write(dest, File.read(src))
103
- dest
104
- end
105
-
106
- def write_minimal_config(project_dir)
107
- dest = Aidp::ConfigPaths.config_file(project_dir)
108
- return dest if File.exist?(dest)
109
- data = {
110
- "harness" => {
111
- "max_retries" => 2,
112
- "default_provider" => "cursor",
113
- "fallback_providers" => ["cursor"],
114
- "no_api_keys_required" => false
115
- },
48
+ minimal = {
49
+ "schema_version" => Aidp::Setup::Wizard::SCHEMA_VERSION,
50
+ "generated_by" => "aidp setup wizard minimal",
51
+ "generated_at" => Time.now.utc.iso8601,
116
52
  "providers" => {
117
- "cursor" => {
118
- "type" => "subscription",
119
- "default_flags" => []
53
+ "llm" => {
54
+ "name" => "cursor",
55
+ "model" => "cursor-agent",
56
+ "temperature" => 0.2,
57
+ "max_tokens" => 1024
120
58
  }
121
- }
122
- }
123
- Aidp::ConfigPaths.ensure_config_dir(project_dir)
124
- File.write(dest, YAML.dump(data))
125
- dest
126
- end
127
-
128
- def write_example_config(project_dir)
129
- Aidp::Config.create_example_config(project_dir)
130
- Aidp::ConfigPaths.config_file(project_dir)
131
- end
132
-
133
- def run_custom
134
- dest = Aidp::ConfigPaths.config_file(@project_dir)
135
- return dest if File.exist?(dest)
136
-
137
- @prompt.say("Interactive custom configuration: press Enter to accept defaults shown in [brackets].")
138
- @prompt.say("")
139
-
140
- # Use TTY::Prompt select for primary provider
141
- # Find the formatted string that matches the default
142
- default_option = available_providers.find { |option| option.start_with?("cursor -") } || available_providers.first
143
- default_provider = @prompt.select("Default provider?", available_providers, default: default_option)
144
-
145
- # Extract just the provider name from the formatted string
146
- provider_name = default_provider.split(" - ").first
147
-
148
- # Validate fallback providers
149
- fallback_providers = select_fallback_providers(available_providers, provider_name)
150
-
151
- restrict = @prompt.yes?("Only use providers that don't require API keys?", default: false)
152
-
153
- # Process providers preserving order
154
- providers = [provider_name] + fallback_providers
155
- providers.uniq!
156
-
157
- provider_section = {}
158
- providers.each do |prov|
159
- provider_section[prov] = {"type" => (prov == "cursor") ? "subscription" : "usage_based", "default_flags" => []}
160
- end
161
-
162
- data = {
163
- "harness" => {
164
- "max_retries" => 2,
165
- "default_provider" => provider_name,
166
- "fallback_providers" => fallback_providers,
167
- "no_api_keys_required" => restrict
168
- },
169
- "providers" => provider_section
170
- }
171
- Aidp::ConfigPaths.ensure_config_dir(@project_dir)
172
- File.write(dest, YAML.dump(data))
173
- dest
174
- end
175
-
176
- def run_custom_with_defaults(existing_config)
177
- dest = Aidp::ConfigPaths.config_file(@project_dir)
178
-
179
- # Extract current values from existing config
180
- harness_config = existing_config[:harness] || existing_config["harness"] || {}
181
- providers_config = existing_config[:providers] || existing_config["providers"] || {}
182
-
183
- current_default = harness_config[:default_provider] || harness_config["default_provider"] || "cursor"
184
- current_fallbacks = harness_config[:fallback_providers] || harness_config["fallback_providers"] || [current_default]
185
- current_restrict = harness_config[:no_api_keys_required] || harness_config["no_api_keys_required"] || false
186
-
187
- # Use TTY::Prompt for interactive configuration
188
- @prompt.say("Interactive configuration update: press Enter to keep current values shown in [brackets].")
189
- @prompt.say("")
190
-
191
- # Use TTY::Prompt select for primary provider
192
- # Find the formatted string that matches the current default
193
- default_option = available_providers.find { |option| option.start_with?("#{current_default} -") } || available_providers.first
194
- default_provider = @prompt.select("Default provider?", available_providers, default: default_option)
195
-
196
- # Extract just the provider name from the formatted string
197
- provider_name = default_provider.split(" - ").first
198
-
199
- # Validate fallback providers
200
- fallback_providers = select_fallback_providers(available_providers, provider_name, preselected: current_fallbacks - [provider_name])
201
-
202
- restrict_input = @prompt.yes?("Only use providers that don't require API keys?", default: current_restrict)
203
-
204
- # Process providers preserving order
205
- providers = [provider_name] + fallback_providers
206
- providers.uniq!
207
-
208
- # Build provider section
209
- provider_section = {}
210
- providers.each do |prov|
211
- # Try to preserve existing provider config if it exists
212
- existing_provider = providers_config[prov.to_sym] || providers_config[prov.to_s]
213
- if existing_provider
214
- # Convert existing provider config to string keys
215
- converted_provider = {}
216
- existing_provider.each { |k, v| converted_provider[k.to_s] = v }
217
- # Ensure the type is correct (fix old "package" and "api" types)
218
- if converted_provider["type"] == "package"
219
- converted_provider["type"] = "subscription"
220
- elsif converted_provider["type"] == "api"
221
- converted_provider["type"] = "usage_based"
222
- end
223
- provider_section[prov] = converted_provider
224
- else
225
- provider_section[prov] = {"type" => (prov == "cursor") ? "subscription" : "usage_based", "default_flags" => []}
226
- end
227
- end
228
-
229
- # Build the new config
230
- data = {
231
- "harness" => {
232
- "max_retries" => harness_config[:max_retries] || harness_config["max_retries"] || 2,
233
- "default_provider" => provider_name,
234
- "fallback_providers" => fallback_providers,
235
- "no_api_keys_required" => restrict_input
236
59
  },
237
- "providers" => provider_section
60
+ "work_loop" => {
61
+ "test" => {
62
+ "unit" => "bundle exec rspec",
63
+ "timeout_seconds" => 1800
64
+ }
65
+ }
238
66
  }
239
67
 
240
- Aidp::ConfigPaths.ensure_config_dir(@project_dir)
241
- File.write(dest, YAML.dump(data))
242
- dest
243
- end
244
-
245
- def load_existing_config
246
- config_file = Aidp::ConfigPaths.config_file(@project_dir)
247
- return nil unless File.exist?(config_file)
248
-
249
- begin
250
- YAML.load_file(config_file) || {}
251
- rescue => e
252
- @prompt.say("❌ Failed to load existing configuration: #{e.message}", color: :red)
253
- nil
254
- end
255
- end
256
-
257
- def ask(prompt, default: nil)
258
- if default
259
- @prompt.ask("#{prompt}:", default: default)
260
- else
261
- @prompt.ask("#{prompt}:")
262
- end
263
- end
264
-
265
- def relative(path)
266
- pn = Pathname.new(path)
267
- wd = Pathname.new(@project_dir)
268
- rel = pn.relative_path_from(wd).to_s
269
- rel.start_with?("..") ? path : rel
270
- rescue
271
- path
272
- end
273
-
274
- # Get available providers for validation
275
- def available_providers
276
- # Get all supported providers from the factory (single source of truth)
277
- all_providers = Aidp::Harness::ProviderFactory::PROVIDER_CLASSES.keys
278
-
279
- # Filter out providers we don't want to show in the wizard
280
- # - "anthropic" is an internal name, we show "claude" instead
281
- # - "macos" is disabled (as per issue #73)
282
- excluded = ["anthropic", "macos"]
283
- available = all_providers - excluded
284
-
285
- # Get display names from the providers themselves
286
- available.map do |provider_name|
287
- provider_class = Aidp::Harness::ProviderFactory::PROVIDER_CLASSES[provider_name]
288
- if provider_class
289
- # Instantiate to get display name
290
- instance = provider_class.new
291
- display_name = instance.display_name
292
- "#{provider_name} - #{display_name}"
293
- else
294
- provider_name
295
- end
296
- end
297
- end
298
-
299
- # Validate provider list input
300
- def validate_provider_list(input, available_providers)
301
- return true if input.nil? || input.empty?
302
-
303
- # Extract provider names from the input
304
- providers = input.split(/\s*,\s*/).map(&:strip).reject(&:empty?)
305
-
306
- # Check if all providers are valid
307
- valid_providers = available_providers.map { |p| p.split(" - ").first }
308
- providers.all? { |provider| valid_providers.include?(provider) }
309
- end
310
-
311
- # Interactive ordered multi-select for fallback providers
312
- def select_fallback_providers(available_with_labels, default_provider, preselected: [])
313
- # Extract provider names and exclude the already chosen default
314
- options = available_with_labels.map { |o| o.split(" - ").first }
315
- candidates = options.reject { |p| p == default_provider }
316
-
317
- return [] if candidates.empty?
318
-
319
- selected = preselected.select { |p| candidates.include?(p) }
320
-
321
- loop do
322
- display_message("\nSelect fallback providers in order of preference (first = highest priority).", type: :info)
323
- display_message("Current order: #{selected.empty? ? "(none)" : selected.join(" > ")}", type: :muted)
324
- choice = @prompt.select("Add provider, or choose an action:", cycle: true) do |menu|
325
- (candidates - selected).each { |prov| menu.choice("Add #{prov}", prov) }
326
- menu.choice("Done", :done)
327
- menu.choice("Clear", :clear) unless selected.empty?
328
- menu.choice("Remove last (#{selected.last})", :remove) unless selected.empty?
329
- end
330
-
331
- case choice
332
- when :done
333
- break
334
- when :clear
335
- selected.clear
336
- when :remove
337
- selected.pop
338
- else
339
- selected << choice unless selected.include?(choice)
340
- end
341
- end
342
-
343
- selected
68
+ File.write(Aidp::ConfigPaths.config_file(@project_dir), minimal.to_yaml)
344
69
  end
345
70
  end
346
71
  end