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.
- checksums.yaml +4 -4
- data/README.md +69 -0
- data/lib/aidp/cli.rb +43 -2
- data/lib/aidp/config.rb +9 -14
- data/lib/aidp/execute/prompt_manager.rb +128 -1
- data/lib/aidp/execute/repl_macros.rb +555 -0
- data/lib/aidp/execute/work_loop_runner.rb +108 -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 +0 -1
- data/templates/aidp.yml.example +128 -0
- metadata +14 -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 |  |