aidp 0.17.1 → 0.18.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 +69 -0
  3. data/lib/aidp/cli.rb +43 -2
  4. data/lib/aidp/config.rb +9 -14
  5. data/lib/aidp/execute/prompt_manager.rb +128 -1
  6. data/lib/aidp/execute/repl_macros.rb +555 -0
  7. data/lib/aidp/execute/work_loop_runner.rb +108 -1
  8. data/lib/aidp/harness/ai_decision_engine.rb +376 -0
  9. data/lib/aidp/harness/capability_registry.rb +273 -0
  10. data/lib/aidp/harness/config_schema.rb +305 -1
  11. data/lib/aidp/harness/configuration.rb +452 -0
  12. data/lib/aidp/harness/enhanced_runner.rb +7 -1
  13. data/lib/aidp/harness/provider_factory.rb +0 -2
  14. data/lib/aidp/harness/runner.rb +7 -1
  15. data/lib/aidp/harness/thinking_depth_manager.rb +335 -0
  16. data/lib/aidp/harness/zfc_condition_detector.rb +395 -0
  17. data/lib/aidp/init/devcontainer_generator.rb +274 -0
  18. data/lib/aidp/init/runner.rb +37 -10
  19. data/lib/aidp/init.rb +1 -0
  20. data/lib/aidp/prompt_optimization/context_composer.rb +286 -0
  21. data/lib/aidp/prompt_optimization/optimizer.rb +335 -0
  22. data/lib/aidp/prompt_optimization/prompt_builder.rb +309 -0
  23. data/lib/aidp/prompt_optimization/relevance_scorer.rb +256 -0
  24. data/lib/aidp/prompt_optimization/source_code_fragmenter.rb +308 -0
  25. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +240 -0
  26. data/lib/aidp/prompt_optimization/template_indexer.rb +250 -0
  27. data/lib/aidp/provider_manager.rb +0 -2
  28. data/lib/aidp/providers/anthropic.rb +19 -0
  29. data/lib/aidp/setup/wizard.rb +299 -4
  30. data/lib/aidp/utils/devcontainer_detector.rb +166 -0
  31. data/lib/aidp/version.rb +1 -1
  32. data/lib/aidp/watch/build_processor.rb +72 -6
  33. data/lib/aidp/watch/repository_client.rb +2 -1
  34. data/lib/aidp.rb +0 -1
  35. data/templates/aidp.yml.example +128 -0
  36. metadata +14 -2
  37. data/lib/aidp/providers/macos_ui.rb +0 -102
@@ -92,8 +92,8 @@ module Aidp
92
92
  providers_dir = File.join(__dir__, "../providers")
93
93
  provider_files = Dir.glob("*.rb", base: providers_dir)
94
94
 
95
- # Exclude base classes and utility classes
96
- excluded_files = ["base.rb", "macos_ui.rb"]
95
+ # Exclude base classes
96
+ excluded_files = ["base.rb"]
97
97
  provider_files -= excluded_files
98
98
 
99
99
  providers = {}
@@ -186,6 +186,9 @@ module Aidp
186
186
  configure_linting
187
187
  configure_watch_patterns
188
188
  configure_guards
189
+ configure_coverage
190
+ configure_interactive_testing
191
+ configure_vcs_behavior
189
192
  end
190
193
 
191
194
  def configure_test_commands
@@ -252,6 +255,242 @@ module Aidp
252
255
  })
253
256
  end
254
257
 
258
+ def configure_coverage
259
+ prompt.say("\nšŸ“Š Coverage configuration")
260
+ existing = get([:work_loop, :coverage]) || {}
261
+
262
+ enabled = prompt.yes?("Enable coverage tracking?", default: existing.fetch(:enabled, false))
263
+ return set([:work_loop, :coverage], {enabled: false}) unless enabled
264
+
265
+ tool = prompt.select("Which coverage tool do you use?", default: existing[:tool]) do |menu|
266
+ menu.choice "SimpleCov (Ruby)", "simplecov"
267
+ menu.choice "NYC/Istanbul (JavaScript)", "nyc"
268
+ menu.choice "Coverage.py (Python)", "coverage.py"
269
+ menu.choice "go test -cover (Go)", "go-cover"
270
+ menu.choice "Jest (JavaScript)", "jest"
271
+ menu.choice "Other", "other"
272
+ end
273
+
274
+ run_command = ask_with_default("Coverage run command", existing[:run_command] || detect_coverage_command(tool))
275
+ report_paths = ask_list("Coverage report paths", existing[:report_paths] || detect_coverage_report_paths(tool))
276
+ fail_on_drop = prompt.yes?("Fail on coverage drop?", default: existing.fetch(:fail_on_drop, false))
277
+
278
+ minimum_coverage_default = existing[:minimum_coverage]&.to_s
279
+ minimum_coverage_answer = ask_with_default("Minimum coverage % (optional - press enter to skip)", minimum_coverage_default)
280
+ minimum_coverage = if minimum_coverage_answer && !minimum_coverage_answer.to_s.strip.empty?
281
+ minimum_coverage_answer.to_f
282
+ end
283
+
284
+ set([:work_loop, :coverage], {
285
+ enabled: true,
286
+ tool: tool,
287
+ run_command: run_command,
288
+ report_paths: report_paths,
289
+ fail_on_drop: fail_on_drop,
290
+ minimum_coverage: minimum_coverage
291
+ }.compact)
292
+
293
+ validate_command(run_command)
294
+ end
295
+
296
+ def configure_interactive_testing
297
+ prompt.say("\nšŸŽÆ Interactive testing configuration")
298
+ existing = get([:work_loop, :interactive_testing]) || {}
299
+
300
+ enabled = prompt.yes?("Enable interactive testing tools?", default: existing.fetch(:enabled, false))
301
+ return set([:work_loop, :interactive_testing], {enabled: false}) unless enabled
302
+
303
+ app_type = prompt.select("What type of application are you testing?", default: existing[:app_type]) do |menu|
304
+ menu.choice "Web application", "web"
305
+ menu.choice "CLI application", "cli"
306
+ menu.choice "Desktop application", "desktop"
307
+ end
308
+
309
+ tools = {}
310
+
311
+ case app_type
312
+ when "web"
313
+ tools[:web] = configure_web_testing_tools(existing.dig(:tools, :web) || {})
314
+ when "cli"
315
+ tools[:cli] = configure_cli_testing_tools(existing.dig(:tools, :cli) || {})
316
+ when "desktop"
317
+ tools[:desktop] = configure_desktop_testing_tools(existing.dig(:tools, :desktop) || {})
318
+ end
319
+
320
+ set([:work_loop, :interactive_testing], {
321
+ enabled: true,
322
+ app_type: app_type,
323
+ tools: tools
324
+ })
325
+ end
326
+
327
+ def configure_web_testing_tools(existing)
328
+ tools = {}
329
+
330
+ playwright_enabled = prompt.yes?("Enable Playwright MCP?", default: existing.dig(:playwright_mcp, :enabled) || false)
331
+ if playwright_enabled
332
+ playwright_run = ask_with_default("Playwright run command", existing.dig(:playwright_mcp, :run) || "npx playwright test")
333
+ playwright_specs = ask_with_default("Playwright specs directory", existing.dig(:playwright_mcp, :specs_dir) || ".aidp/tests/web")
334
+ tools[:playwright_mcp] = {enabled: true, run: playwright_run, specs_dir: playwright_specs}
335
+ end
336
+
337
+ chrome_enabled = prompt.yes?("Enable Chrome DevTools MCP?", default: existing.dig(:chrome_devtools_mcp, :enabled) || false)
338
+ if chrome_enabled
339
+ chrome_run = ask_with_default("Chrome DevTools run command", existing.dig(:chrome_devtools_mcp, :run) || "")
340
+ chrome_specs = ask_with_default("Chrome DevTools specs directory", existing.dig(:chrome_devtools_mcp, :specs_dir) || ".aidp/tests/web")
341
+ tools[:chrome_devtools_mcp] = {enabled: true, run: chrome_run, specs_dir: chrome_specs}
342
+ end
343
+
344
+ tools
345
+ end
346
+
347
+ def configure_cli_testing_tools(existing)
348
+ tools = {}
349
+
350
+ expect_enabled = prompt.yes?("Enable expect scripts?", default: existing.dig(:expect, :enabled) || false)
351
+ if expect_enabled
352
+ expect_run = ask_with_default("Expect run command", existing.dig(:expect, :run) || "expect .aidp/tests/cli/smoke.exp")
353
+ expect_specs = ask_with_default("Expect specs directory", existing.dig(:expect, :specs_dir) || ".aidp/tests/cli")
354
+ tools[:expect] = {enabled: true, run: expect_run, specs_dir: expect_specs}
355
+ end
356
+
357
+ tools
358
+ end
359
+
360
+ def configure_desktop_testing_tools(existing)
361
+ tools = {}
362
+
363
+ applescript_enabled = prompt.yes?("Enable AppleScript testing?", default: existing.dig(:applescript, :enabled) || false)
364
+ if applescript_enabled
365
+ applescript_run = ask_with_default("AppleScript run command", existing.dig(:applescript, :run) || "osascript .aidp/tests/desktop/smoke.scpt")
366
+ applescript_specs = ask_with_default("AppleScript specs directory", existing.dig(:applescript, :specs_dir) || ".aidp/tests/desktop")
367
+ tools[:applescript] = {enabled: true, run: applescript_run, specs_dir: applescript_specs}
368
+ end
369
+
370
+ screen_reader_enabled = prompt.yes?("Enable screen reader testing?", default: existing.dig(:screen_reader, :enabled) || false)
371
+ if screen_reader_enabled
372
+ screen_reader_notes = ask_with_default("Screen reader testing notes (optional)", existing.dig(:screen_reader, :notes) || "VoiceOver scripted checks")
373
+ tools[:screen_reader] = {enabled: true, notes: screen_reader_notes}
374
+ end
375
+
376
+ tools
377
+ end
378
+
379
+ def configure_vcs_behavior
380
+ prompt.say("\nšŸ—‚ļø Version control configuration")
381
+ existing = get([:work_loop, :version_control]) || {}
382
+
383
+ # Detect VCS
384
+ detected_vcs = detect_vcs_tool
385
+ vcs_tool = if detected_vcs
386
+ prompt.select("Detected #{detected_vcs}. Use this version control system?", default: existing[:tool] || detected_vcs) do |menu|
387
+ menu.choice "git", "git"
388
+ menu.choice "svn", "svn"
389
+ menu.choice "none (no VCS)", "none"
390
+ end
391
+ else
392
+ prompt.select("Which version control system do you use?", default: existing[:tool] || "git") do |menu|
393
+ menu.choice "git", "git"
394
+ menu.choice "svn", "svn"
395
+ menu.choice "none (no VCS)", "none"
396
+ end
397
+ end
398
+
399
+ return set([:work_loop, :version_control], {tool: "none", behavior: "nothing"}) if vcs_tool == "none"
400
+
401
+ prompt.say("\nšŸ“‹ Commit Behavior (applies to copilot/interactive mode only)")
402
+ prompt.say("Note: Watch mode and fully automatic daemon mode will always commit changes.")
403
+ behavior = prompt.select("In copilot mode, should aidp:", default: existing[:behavior] || "nothing") do |menu|
404
+ menu.choice "Do nothing (manual git operations)", "nothing"
405
+ menu.choice "Stage changes only", "stage"
406
+ menu.choice "Stage and commit changes", "commit"
407
+ end
408
+
409
+ # Commit message configuration
410
+ commit_config = configure_commit_messages(existing, behavior)
411
+
412
+ # PR configuration (only relevant for git with remote)
413
+ pr_config = if vcs_tool == "git" && behavior == "commit"
414
+ configure_pull_requests(existing)
415
+ else
416
+ {auto_create_pr: false}
417
+ end
418
+
419
+ set([:work_loop, :version_control], {
420
+ tool: vcs_tool,
421
+ behavior: behavior,
422
+ **commit_config,
423
+ **pr_config
424
+ })
425
+ end
426
+
427
+ def configure_commit_messages(existing, behavior)
428
+ return {} unless behavior == "commit"
429
+
430
+ prompt.say("\nšŸ’¬ Commit Message Configuration")
431
+
432
+ # Conventional commits
433
+ conventional_commits = prompt.yes?(
434
+ "Use conventional commit format (e.g., 'feat:', 'fix:', 'docs:')?",
435
+ default: existing.fetch(:conventional_commits, false)
436
+ )
437
+
438
+ # Commit message style
439
+ commit_style = if conventional_commits
440
+ prompt.select("Conventional commit style:", default: existing[:commit_style] || "default") do |menu|
441
+ menu.choice "Default (e.g., 'feat: add user authentication')", "default"
442
+ menu.choice "Angular (with scope: 'feat(auth): add login')", "angular"
443
+ menu.choice "Emoji (e.g., '✨ feat: add user authentication')", "emoji"
444
+ end
445
+ else
446
+ "default"
447
+ end
448
+
449
+ # Co-authored-by attribution
450
+ co_author = prompt.yes?(
451
+ "Include 'Co-authored-by: <AI Provider>' in commit messages?",
452
+ default: existing.fetch(:co_author_ai, true)
453
+ )
454
+
455
+ {
456
+ conventional_commits: conventional_commits,
457
+ commit_style: commit_style,
458
+ co_author_ai: co_author
459
+ }
460
+ end
461
+
462
+ def configure_pull_requests(existing)
463
+ prompt.say("\nšŸ”€ Pull Request Configuration")
464
+
465
+ # Check if remote exists
466
+ has_remote = system("git remote -v > /dev/null 2>&1")
467
+
468
+ unless has_remote
469
+ prompt.say("No git remote detected. PR creation will be disabled.")
470
+ return {auto_create_pr: false}
471
+ end
472
+
473
+ auto_create_pr = prompt.yes?(
474
+ "Automatically create pull requests after successful builds? (watch/daemon mode only)",
475
+ default: existing.fetch(:auto_create_pr, false)
476
+ )
477
+
478
+ if auto_create_pr
479
+ pr_strategy = prompt.select("PR creation strategy:", default: existing[:pr_strategy] || "draft") do |menu|
480
+ menu.choice "Create as draft PR (safe, allows review before merge)", "draft"
481
+ menu.choice "Create as ready PR (immediately reviewable)", "ready"
482
+ menu.choice "Create and auto-merge (fully autonomous, requires approval rules)", "auto_merge"
483
+ end
484
+
485
+ {
486
+ auto_create_pr: true,
487
+ pr_strategy: pr_strategy
488
+ }
489
+ else
490
+ {auto_create_pr: false}
491
+ end
492
+ end
493
+
255
494
  def configure_branching
256
495
  prompt.say("\n🌿 Branching strategy")
257
496
  prompt.say("-" * 40)
@@ -604,6 +843,46 @@ module Aidp
604
843
  end
605
844
  end
606
845
 
846
+ def detect_coverage_command(tool)
847
+ case tool
848
+ when "simplecov"
849
+ "bundle exec rspec"
850
+ when "nyc", "istanbul"
851
+ "nyc npm test"
852
+ when "coverage.py"
853
+ "coverage run -m pytest"
854
+ when "go-cover"
855
+ "go test -cover ./..."
856
+ when "jest"
857
+ "jest --coverage"
858
+ else
859
+ "echo 'Configure coverage command'"
860
+ end
861
+ end
862
+
863
+ def detect_coverage_report_paths(tool)
864
+ case tool
865
+ when "simplecov"
866
+ ["coverage/index.html", "coverage/.resultset.json"]
867
+ when "nyc", "istanbul"
868
+ ["coverage/lcov-report/index.html", "coverage/lcov.info"]
869
+ when "coverage.py"
870
+ [".coverage", "htmlcov/index.html"]
871
+ when "go-cover"
872
+ ["coverage.out"]
873
+ when "jest"
874
+ ["coverage/lcov-report/index.html"]
875
+ else
876
+ []
877
+ end
878
+ end
879
+
880
+ def detect_vcs_tool
881
+ return "git" if Dir.exist?(File.join(project_dir, ".git"))
882
+ return "svn" if Dir.exist?(File.join(project_dir, ".svn"))
883
+ nil
884
+ end
885
+
607
886
  def detect_stack
608
887
  return :rails if project_file?("Gemfile") && project_file?("config/application.rb")
609
888
  return :node if project_file?("package.json")
@@ -626,12 +905,18 @@ module Aidp
626
905
 
627
906
  if existing && existing[:type]
628
907
  prompt.say(" • Provider '#{provider_name}' already configured (type: #{existing[:type]})")
908
+ # Still ask for model family if not set
909
+ unless existing[:model_family]
910
+ model_family = ask_model_family(provider_name, existing[:model_family])
911
+ set([:providers, provider_name.to_sym, :model_family], model_family)
912
+ end
629
913
  return
630
914
  end
631
915
 
632
916
  provider_type = ask_provider_billing_type(provider_name)
633
- set([:providers, provider_name.to_sym], {type: provider_type})
634
- prompt.say(" • Added provider '#{provider_name}' with billing type '#{provider_type}' (no secrets stored)")
917
+ model_family = ask_model_family(provider_name)
918
+ set([:providers, provider_name.to_sym], {type: provider_type, model_family: model_family})
919
+ prompt.say(" • Added provider '#{provider_name}' with billing type '#{provider_type}' and model family '#{model_family}' (no secrets stored)")
635
920
  end
636
921
 
637
922
  def ask_provider_billing_type(provider_name)
@@ -643,6 +928,16 @@ module Aidp
643
928
  end
644
929
  end
645
930
 
931
+ def ask_model_family(provider_name, default = "auto")
932
+ prompt.select("Preferred model family for #{provider_name}:", default: default) do |menu|
933
+ menu.choice "Auto (let provider decide)", "auto"
934
+ menu.choice "OpenAI o-series (reasoning models)", "openai_o"
935
+ menu.choice "Anthropic Claude (balanced)", "claude"
936
+ menu.choice "Mistral (European/open)", "mistral"
937
+ menu.choice "Local LLM (self-hosted)", "local"
938
+ end
939
+ end
940
+
646
941
  def load_existing_config
647
942
  return {} unless File.exist?(config_path)
648
943
  YAML.safe_load_file(config_path, permitted_classes: [Time]) || {}
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Utils
5
+ # Detects if AIDP is running inside a devcontainer
6
+ #
7
+ # Uses multiple heuristics to determine container environment:
8
+ # - Environment variables (REMOTE_CONTAINERS, CODESPACES)
9
+ # - Filesystem markers (/.dockerenv, /run/.containerenv)
10
+ # - Hostname patterns
11
+ # - cgroup information
12
+ #
13
+ # @example
14
+ # if DevcontainerDetector.in_devcontainer?
15
+ # puts "Running in devcontainer with elevated permissions"
16
+ # end
17
+ class DevcontainerDetector
18
+ class << self
19
+ # Check if running inside a devcontainer
20
+ #
21
+ # @return [Boolean] true if inside a devcontainer
22
+ def in_devcontainer?
23
+ @in_devcontainer ||= detect_devcontainer
24
+ end
25
+
26
+ # Check if running inside any container (Docker, Podman, etc.)
27
+ #
28
+ # @return [Boolean] true if inside any container
29
+ def in_container?
30
+ @in_container ||= detect_container
31
+ end
32
+
33
+ # Check if running in GitHub Codespaces
34
+ #
35
+ # @return [Boolean] true if in Codespaces
36
+ def in_codespaces?
37
+ ENV["CODESPACES"] == "true"
38
+ end
39
+
40
+ # Check if running in VS Code Remote Containers
41
+ #
42
+ # @return [Boolean] true if in VS Code Remote Containers
43
+ def in_vscode_remote?
44
+ ENV["REMOTE_CONTAINERS"] == "true" || ENV["VSCODE_REMOTE_CONTAINERS"] == "true"
45
+ end
46
+
47
+ # Get container type (docker, podman, codespaces, vscode, unknown)
48
+ #
49
+ # @return [Symbol] container type
50
+ def container_type
51
+ return :codespaces if in_codespaces?
52
+ return :vscode if in_vscode_remote?
53
+ return :docker if docker_container?
54
+ return :podman if podman_container?
55
+ return :unknown if in_container?
56
+ :none
57
+ end
58
+
59
+ # Get detailed container information
60
+ #
61
+ # @return [Hash] container information
62
+ def container_info
63
+ {
64
+ in_devcontainer: in_devcontainer?,
65
+ in_container: in_container?,
66
+ container_type: container_type,
67
+ hostname: hostname,
68
+ docker_env: File.exist?("/.dockerenv"),
69
+ container_env: File.exist?("/run/.containerenv"),
70
+ cgroup_docker: cgroup_contains?("docker"),
71
+ cgroup_containerd: cgroup_contains?("containerd"),
72
+ remote_containers_env: ENV["REMOTE_CONTAINERS"],
73
+ codespaces_env: ENV["CODESPACES"]
74
+ }
75
+ end
76
+
77
+ # Reset cached detection (useful for testing)
78
+ def reset!
79
+ @in_devcontainer = nil
80
+ @in_container = nil
81
+ end
82
+
83
+ private
84
+
85
+ def detect_devcontainer
86
+ # Check for VS Code Remote Containers or Codespaces
87
+ return true if in_vscode_remote?
88
+ return true if in_codespaces?
89
+
90
+ # Check for devcontainer-specific environment markers
91
+ return true if ENV["AIDP_ENV"] == "development" && in_container?
92
+
93
+ # Generic container detection with additional heuristics
94
+ in_container? && likely_dev_environment?
95
+ end
96
+
97
+ def detect_container
98
+ # Check environment variable
99
+ return true if ENV["container"]
100
+
101
+ # Check for Docker environment file
102
+ return true if File.exist?("/.dockerenv")
103
+
104
+ # Check for Podman/containers environment file
105
+ return true if File.exist?("/run/.containerenv")
106
+
107
+ # Check cgroup for container indicators
108
+ return true if cgroup_indicates_container?
109
+
110
+ # Check hostname patterns (containers often have short hex hostnames)
111
+ return true if hostname_indicates_container?
112
+
113
+ false
114
+ end
115
+
116
+ def docker_container?
117
+ File.exist?("/.dockerenv") || cgroup_contains?("docker")
118
+ end
119
+
120
+ def podman_container?
121
+ File.exist?("/run/.containerenv") || cgroup_contains?("podman")
122
+ end
123
+
124
+ def cgroup_indicates_container?
125
+ return false unless File.exist?("/proc/1/cgroup")
126
+
127
+ File.readlines("/proc/1/cgroup").any? do |line|
128
+ line.include?("docker") ||
129
+ line.include?("lxc") ||
130
+ line.include?("containerd") ||
131
+ line.include?("podman")
132
+ end
133
+ rescue
134
+ false
135
+ end
136
+
137
+ def cgroup_contains?(pattern)
138
+ return false unless File.exist?("/proc/1/cgroup")
139
+
140
+ File.readlines("/proc/1/cgroup").any? { |line| line.include?(pattern) }
141
+ rescue
142
+ false
143
+ end
144
+
145
+ def hostname
146
+ ENV["HOSTNAME"] || `hostname`.strip
147
+ rescue
148
+ "unknown"
149
+ end
150
+
151
+ def hostname_indicates_container?
152
+ host = hostname
153
+ # Containers often have short hex hostnames (12 chars) or specific patterns
154
+ host.length == 12 && host.match?(/^[0-9a-f]+$/)
155
+ end
156
+
157
+ def likely_dev_environment?
158
+ # Check for common development tools and patterns
159
+ File.exist?("/workspace") ||
160
+ ENV["TERM_PROGRAM"] == "vscode" ||
161
+ ENV["EDITOR"]&.include?("code")
162
+ end
163
+ end
164
+ end
165
+ end
166
+ 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.17.1"
4
+ VERSION = "0.18.0"
5
5
  end
@@ -226,13 +226,24 @@ module Aidp
226
226
 
227
227
  def handle_success(issue:, slug:, branch_name:, base_branch:, plan_data:, working_dir:)
228
228
  stage_and_commit(issue, working_dir: working_dir)
229
- pr_url = create_pull_request(issue: issue, branch_name: branch_name, base_branch: base_branch, working_dir: working_dir)
229
+
230
+ # Check if PR should be created based on VCS preferences
231
+ vcs_config = config.dig(:work_loop, :version_control) || {}
232
+ auto_create_pr = vcs_config.fetch(:auto_create_pr, false)
233
+
234
+ pr_url = if auto_create_pr
235
+ create_pull_request(issue: issue, branch_name: branch_name, base_branch: base_branch, working_dir: working_dir)
236
+ else
237
+ display_message("ā„¹ļø Skipping PR creation (disabled in VCS preferences)", type: :muted)
238
+ nil
239
+ end
230
240
 
231
241
  workstream_note = @use_workstreams ? "\n- Workstream: `#{slug}`" : ""
242
+ pr_line = pr_url ? "\n- Pull Request: #{pr_url}" : ""
243
+
232
244
  comment = <<~COMMENT
233
245
  āœ… Implementation complete for ##{issue[:number]}.
234
- - Branch: `#{branch_name}`#{workstream_note}
235
- - Pull Request: #{pr_url}
246
+ - Branch: `#{branch_name}`#{workstream_note}#{pr_line}
236
247
 
237
248
  Summary:
238
249
  #{plan_value(plan_data, "summary")}
@@ -281,9 +292,58 @@ module Aidp
281
292
  end
282
293
 
283
294
  run_git(%w[add -A])
284
- commit_message = "feat: implement ##{issue[:number]} #{issue[:title]}"
295
+ commit_message = build_commit_message(issue)
285
296
  run_git(["commit", "-m", commit_message])
286
- display_message("šŸ’¾ Created commit: #{commit_message}", type: :info)
297
+ display_message("šŸ’¾ Created commit: #{commit_message.lines.first.strip}", type: :info)
298
+ end
299
+ end
300
+
301
+ def build_commit_message(issue)
302
+ vcs_config = config.dig(:work_loop, :version_control) || {}
303
+
304
+ # Base message components
305
+ issue_ref = "##{issue[:number]}"
306
+ title = issue[:title]
307
+
308
+ # Determine commit prefix based on configuration
309
+ prefix = if vcs_config[:conventional_commits]
310
+ commit_style = vcs_config[:commit_style] || "default"
311
+ emoji = (commit_style == "emoji") ? "✨ " : ""
312
+ scope = (commit_style == "angular") ? "(implementation)" : ""
313
+ "#{emoji}feat#{scope}: "
314
+ else
315
+ ""
316
+ end
317
+
318
+ # Build main message
319
+ main_message = "#{prefix}implement #{issue_ref} #{title}"
320
+
321
+ # Add co-author attribution if configured
322
+ if vcs_config.fetch(:co_author_ai, true)
323
+ provider_name = detect_current_provider || "AI Agent"
324
+ co_author = "\n\nCo-authored-by: #{provider_name} <ai@aidp.dev>"
325
+ main_message + co_author
326
+ else
327
+ main_message
328
+ end
329
+ end
330
+
331
+ def detect_current_provider
332
+ # Attempt to detect which provider is being used
333
+ # This is a best-effort detection
334
+ config_manager = Aidp::Harness::ConfigManager.new(@project_dir)
335
+ default_provider = config_manager.config.dig(:harness, :default_provider)
336
+ default_provider&.capitalize
337
+ rescue
338
+ nil
339
+ end
340
+
341
+ def config
342
+ @config ||= begin
343
+ config_manager = Aidp::Harness::ConfigManager.new(@project_dir)
344
+ config_manager.config || {}
345
+ rescue
346
+ {}
287
347
  end
288
348
  end
289
349
 
@@ -298,12 +358,18 @@ module Aidp
298
358
  #{test_summary}
299
359
  BODY
300
360
 
361
+ # Determine if PR should be draft based on VCS preferences
362
+ vcs_config = config.dig(:work_loop, :version_control) || {}
363
+ pr_strategy = vcs_config[:pr_strategy] || "draft"
364
+ draft = (pr_strategy == "draft")
365
+
301
366
  output = @repository_client.create_pull_request(
302
367
  title: title,
303
368
  body: body,
304
369
  head: branch_name,
305
370
  base: base_branch,
306
- issue_number: issue[:number]
371
+ issue_number: issue[:number],
372
+ draft: draft
307
373
  )
308
374
 
309
375
  extract_pr_url(output)
@@ -162,7 +162,7 @@ module Aidp
162
162
  response.body
163
163
  end
164
164
 
165
- def create_pull_request_via_gh(title:, body:, head:, base:, issue_number:)
165
+ def create_pull_request_via_gh(title:, body:, head:, base:, issue_number:, draft: false)
166
166
  cmd = [
167
167
  "gh", "pr", "create",
168
168
  "--repo", full_repo,
@@ -172,6 +172,7 @@ module Aidp
172
172
  "--base", base
173
173
  ]
174
174
  cmd += ["--issue", issue_number.to_s] if issue_number
175
+ cmd += ["--draft"] if draft
175
176
 
176
177
  stdout, stderr, status = Open3.capture3(*cmd)
177
178
  raise "Failed to create PR via gh: #{stderr.strip}" unless status.success?
data/lib/aidp.rb CHANGED
@@ -25,7 +25,6 @@ require_relative "aidp/providers/base"
25
25
  require_relative "aidp/providers/cursor"
26
26
  require_relative "aidp/providers/anthropic"
27
27
  require_relative "aidp/providers/gemini"
28
- require_relative "aidp/providers/macos_ui"
29
28
  # Supervised providers removed - using direct execution model
30
29
  require_relative "aidp/provider_manager"
31
30