aidp 0.17.1 ā 0.19.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.
- checksums.yaml +4 -4
- data/README.md +69 -0
- data/lib/aidp/cli/terminal_io.rb +5 -2
- data/lib/aidp/cli.rb +43 -2
- data/lib/aidp/config.rb +9 -14
- data/lib/aidp/execute/agent_signal_parser.rb +20 -0
- data/lib/aidp/execute/persistent_tasklist.rb +220 -0
- data/lib/aidp/execute/prompt_manager.rb +128 -1
- data/lib/aidp/execute/repl_macros.rb +719 -0
- data/lib/aidp/execute/work_loop_runner.rb +162 -1
- data/lib/aidp/harness/ai_decision_engine.rb +376 -0
- data/lib/aidp/harness/capability_registry.rb +273 -0
- data/lib/aidp/harness/config_schema.rb +305 -1
- data/lib/aidp/harness/configuration.rb +452 -0
- data/lib/aidp/harness/enhanced_runner.rb +7 -1
- data/lib/aidp/harness/provider_factory.rb +0 -2
- data/lib/aidp/harness/runner.rb +7 -1
- data/lib/aidp/harness/thinking_depth_manager.rb +335 -0
- data/lib/aidp/harness/zfc_condition_detector.rb +395 -0
- data/lib/aidp/init/devcontainer_generator.rb +274 -0
- data/lib/aidp/init/runner.rb +37 -10
- data/lib/aidp/init.rb +1 -0
- data/lib/aidp/prompt_optimization/context_composer.rb +286 -0
- data/lib/aidp/prompt_optimization/optimizer.rb +335 -0
- data/lib/aidp/prompt_optimization/prompt_builder.rb +309 -0
- data/lib/aidp/prompt_optimization/relevance_scorer.rb +256 -0
- data/lib/aidp/prompt_optimization/source_code_fragmenter.rb +308 -0
- data/lib/aidp/prompt_optimization/style_guide_indexer.rb +240 -0
- data/lib/aidp/prompt_optimization/template_indexer.rb +250 -0
- data/lib/aidp/provider_manager.rb +0 -2
- data/lib/aidp/providers/anthropic.rb +19 -0
- data/lib/aidp/setup/wizard.rb +299 -4
- data/lib/aidp/utils/devcontainer_detector.rb +166 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +72 -6
- data/lib/aidp/watch/repository_client.rb +2 -1
- data/lib/aidp.rb +1 -1
- data/templates/aidp.yml.example +128 -0
- metadata +15 -2
- data/lib/aidp/providers/macos_ui.rb +0 -102
data/lib/aidp/setup/wizard.rb
CHANGED
|
@@ -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
|
|
96
|
-
excluded_files = ["base.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
|
-
|
|
634
|
-
|
|
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
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
|
@@ -60,6 +59,7 @@ require_relative "aidp/execute/checkpoint"
|
|
|
60
59
|
require_relative "aidp/execute/checkpoint_display"
|
|
61
60
|
require_relative "aidp/execute/work_loop_state"
|
|
62
61
|
require_relative "aidp/execute/instruction_queue"
|
|
62
|
+
require_relative "aidp/execute/persistent_tasklist"
|
|
63
63
|
require_relative "aidp/execute/async_work_loop_runner"
|
|
64
64
|
require_relative "aidp/execute/interactive_repl"
|
|
65
65
|
|