aidp 0.17.0 → 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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -0
  3. data/lib/aidp/analyze/kb_inspector.rb +2 -3
  4. data/lib/aidp/analyze/progress.rb +5 -10
  5. data/lib/aidp/cli/mcp_dashboard.rb +1 -1
  6. data/lib/aidp/cli.rb +64 -29
  7. data/lib/aidp/config.rb +9 -14
  8. data/lib/aidp/execute/progress.rb +5 -8
  9. data/lib/aidp/execute/prompt_manager.rb +128 -1
  10. data/lib/aidp/execute/repl_macros.rb +555 -0
  11. data/lib/aidp/execute/work_loop_runner.rb +108 -1
  12. data/lib/aidp/harness/ai_decision_engine.rb +376 -0
  13. data/lib/aidp/harness/capability_registry.rb +273 -0
  14. data/lib/aidp/harness/config_loader.rb +2 -2
  15. data/lib/aidp/harness/config_schema.rb +305 -1
  16. data/lib/aidp/harness/configuration.rb +452 -0
  17. data/lib/aidp/harness/enhanced_runner.rb +23 -8
  18. data/lib/aidp/harness/error_handler.rb +12 -5
  19. data/lib/aidp/harness/provider_factory.rb +0 -2
  20. data/lib/aidp/harness/provider_manager.rb +4 -19
  21. data/lib/aidp/harness/runner.rb +9 -3
  22. data/lib/aidp/harness/state/persistence.rb +9 -10
  23. data/lib/aidp/harness/state/workflow_state.rb +3 -2
  24. data/lib/aidp/harness/state_manager.rb +33 -97
  25. data/lib/aidp/harness/status_display.rb +22 -12
  26. data/lib/aidp/harness/thinking_depth_manager.rb +335 -0
  27. data/lib/aidp/harness/ui/enhanced_tui.rb +3 -4
  28. data/lib/aidp/harness/user_interface.rb +11 -6
  29. data/lib/aidp/harness/zfc_condition_detector.rb +395 -0
  30. data/lib/aidp/init/devcontainer_generator.rb +274 -0
  31. data/lib/aidp/init/runner.rb +37 -10
  32. data/lib/aidp/init.rb +1 -0
  33. data/lib/aidp/jobs/background_runner.rb +7 -1
  34. data/lib/aidp/logger.rb +1 -1
  35. data/lib/aidp/message_display.rb +9 -2
  36. data/lib/aidp/prompt_optimization/context_composer.rb +286 -0
  37. data/lib/aidp/prompt_optimization/optimizer.rb +335 -0
  38. data/lib/aidp/prompt_optimization/prompt_builder.rb +309 -0
  39. data/lib/aidp/prompt_optimization/relevance_scorer.rb +256 -0
  40. data/lib/aidp/prompt_optimization/source_code_fragmenter.rb +308 -0
  41. data/lib/aidp/prompt_optimization/style_guide_indexer.rb +240 -0
  42. data/lib/aidp/prompt_optimization/template_indexer.rb +250 -0
  43. data/lib/aidp/provider_manager.rb +0 -2
  44. data/lib/aidp/providers/anthropic.rb +20 -1
  45. data/lib/aidp/providers/base.rb +4 -4
  46. data/lib/aidp/providers/codex.rb +1 -1
  47. data/lib/aidp/providers/cursor.rb +1 -1
  48. data/lib/aidp/providers/gemini.rb +1 -1
  49. data/lib/aidp/providers/github_copilot.rb +1 -1
  50. data/lib/aidp/providers/opencode.rb +1 -1
  51. data/lib/aidp/setup/wizard.rb +299 -4
  52. data/lib/aidp/skills/wizard/prompter.rb +2 -2
  53. data/lib/aidp/utils/devcontainer_detector.rb +166 -0
  54. data/lib/aidp/version.rb +1 -1
  55. data/lib/aidp/watch/build_processor.rb +72 -6
  56. data/lib/aidp/watch/plan_generator.rb +1 -1
  57. data/lib/aidp/watch/repository_client.rb +15 -10
  58. data/lib/aidp/workflows/guided_agent.rb +2 -312
  59. data/lib/aidp/workstream_executor.rb +8 -2
  60. data/lib/aidp.rb +0 -1
  61. data/templates/aidp.yml.example +128 -0
  62. metadata +14 -2
  63. data/lib/aidp/providers/macos_ui.rb +0 -102
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84451cdcc0886f1a70e799ab53d9b8898633f324cdd80c5f07a5f204848a3de2
4
- data.tar.gz: e93982382c700564db24fac6ff1de2276e5b0301b91f6cade19aa8dfacd3f7d3
3
+ metadata.gz: 95269030b5c3fe91c2cfb6e29066a4f37afbcf1903e930a377ec02dd1e8a738c
4
+ data.tar.gz: 6c343d45e804a73751c99e0e63491703c1eb9ba18fb2acf4e828198e774c2c92
5
5
  SHA512:
6
- metadata.gz: 3de783cb25c3eedc9056f77c2b294918a2f5ab46c009280897b6094dd157c45c681fc5c8fe22c6eea401ba74e0c4151c132a85e3c8ceedfef7722e9580f96f43
7
- data.tar.gz: de86c77e08a8f9b837d501d8e69786b6f969c0e2a1538f1a9d2062267f01d6e8c855f0885c31d0bdc3f2c9362fa78439fccdcfe5e09a12cd1ab61282365cdc4a
6
+ metadata.gz: dc54a6f0b9424c18ebdd8e6df75e77f0f233db98dd59543a9dd6152eacd835d8820c437ccd220c3dbcfb43cf91bfeb47e40ce415773e8cd0e24783770b2ac033
7
+ data.tar.gz: f89cf387121e7edc3c384cead9369bc6ae5be97e639969fa036978a249779152ccd7a50832897be97d7fd04f4083ccf744e23e93c1117c6ed5d1cc7d8529615d
data/README.md CHANGED
@@ -42,6 +42,75 @@ You can re-run the wizard manually:
42
42
  aidp --setup-config
43
43
  ```
44
44
 
45
+ ## Devcontainer Support
46
+
47
+ AIDP provides first-class devcontainer support for sandboxed, secure AI agent execution. Devcontainers offer:
48
+
49
+ - **Network Security**: Strict firewall with allowlisted domains only
50
+ - **Sandboxed Environment**: Isolated from your host system
51
+ - **Elevated Permissions**: AI agents can run with full permissions inside the container
52
+ - **Consistent Setup**: Same environment across all developers
53
+
54
+ ### For AIDP Development
55
+
56
+ This repository includes a `.devcontainer/` setup for developing AIDP itself:
57
+
58
+ ```bash
59
+ # Open in VS Code
60
+ code .
61
+
62
+ # Press F1 → "Dev Containers: Reopen in Container"
63
+ # Container builds automatically with Ruby 3.4.5, all tools, and firewall
64
+
65
+ # Run tests inside container
66
+ bundle exec rspec
67
+
68
+ # Run AIDP inside container
69
+ bundle exec aidp
70
+ ```
71
+
72
+ See [.devcontainer/README.md](.devcontainer/README.md) for complete documentation.
73
+
74
+ ### Generating Devcontainers for Your Projects
75
+
76
+ Use `aidp init` to generate a devcontainer for any project:
77
+
78
+ ```bash
79
+ # Initialize project with devcontainer
80
+ aidp init
81
+
82
+ # When prompted:
83
+ # "Generate devcontainer configuration for sandboxed development?" → Yes
84
+
85
+ # Or use the flag directly
86
+ aidp init --with-devcontainer
87
+ ```
88
+
89
+ This creates:
90
+
91
+ - `.devcontainer/Dockerfile` - Customized for your project's language/framework
92
+ - `.devcontainer/devcontainer.json` - VS Code configuration and extensions
93
+ - `.devcontainer/init-firewall.sh` - Network security rules
94
+ - `.devcontainer/README.md` - Setup and usage documentation
95
+
96
+ ### Elevated Permissions in Devcontainers
97
+
98
+ When running inside a devcontainer, you can enable elevated permissions for AI agents:
99
+
100
+ ```yaml
101
+ # aidp.yml
102
+ devcontainer:
103
+ enabled: true
104
+ full_permissions_when_in_devcontainer: true # Run all providers with full permissions
105
+
106
+ # Or enable per-provider
107
+ permissions:
108
+ skip_permission_checks:
109
+ - claude # Adds --dangerously-skip-permissions for Claude Code
110
+ ```
111
+
112
+ AIDP automatically detects when it's running in a devcontainer and adjusts agent permissions accordingly. This is safe because the container is sandboxed from your host system.
113
+
45
114
  ## Core Features
46
115
 
47
116
  ### Work Loops
@@ -80,7 +80,7 @@ module Aidp
80
80
  display_message(box)
81
81
  end
82
82
 
83
- def load_kb_data
83
+ def load_kb_data(suppress_parse_warnings: false)
84
84
  data = {}
85
85
 
86
86
  %w[symbols imports calls metrics seams hotspots tests cycles].each do |type|
@@ -89,8 +89,7 @@ module Aidp
89
89
  begin
90
90
  data[type.to_sym] = JSON.parse(File.read(file_path), symbolize_names: true)
91
91
  rescue JSON::ParserError => e
92
- # Suppress warnings in test mode to avoid CI failures
93
- unless ENV["RACK_ENV"] == "test" || defined?(RSpec)
92
+ unless suppress_parse_warnings
94
93
  display_message("Warning: Could not parse #{file_path}: #{e.message}", type: :warn)
95
94
  end
96
95
  data[type.to_sym] = []
@@ -9,9 +9,10 @@ module Aidp
9
9
  class Progress
10
10
  attr_reader :project_dir, :progress_file
11
11
 
12
- def initialize(project_dir)
12
+ def initialize(project_dir, skip_persistence: false)
13
13
  @project_dir = project_dir
14
14
  @progress_file = File.join(project_dir, ".aidp", "progress", "analyze.yml")
15
+ @skip_persistence = skip_persistence
15
16
  load_progress
16
17
  end
17
18
 
@@ -60,26 +61,20 @@ module Aidp
60
61
  private
61
62
 
62
63
  def load_progress
63
- # In test mode, only skip file operations if no progress file exists
64
- if (ENV["RACK_ENV"] == "test" || defined?(RSpec)) && !File.exist?(@progress_file)
64
+ if @skip_persistence && !File.exist?(@progress_file)
65
65
  @progress = {}
66
66
  return
67
67
  end
68
-
69
- @progress = if File.exist?(@progress_file)
68
+ @progress = if !@skip_persistence && File.exist?(@progress_file)
70
69
  YAML.safe_load_file(@progress_file, permitted_classes: [Date, Time, Symbol], aliases: true) || {}
71
70
  else
72
71
  {}
73
72
  end
74
-
75
- # Ensure @progress is never nil
76
73
  @progress = {} if @progress.nil?
77
74
  end
78
75
 
79
76
  def save_progress
80
- # In test mode, skip file operations to avoid hanging
81
- return if ENV["RACK_ENV"] == "test" || defined?(RSpec)
82
-
77
+ return if @skip_persistence
83
78
  FileUtils.mkdir_p(File.dirname(@progress_file))
84
79
  File.write(@progress_file, @progress.to_yaml)
85
80
  end
@@ -102,7 +102,7 @@ module Aidp
102
102
 
103
103
  providers.each do |provider|
104
104
  provider_info = Aidp::Harness::ProviderInfo.new(provider, @root_dir)
105
- info = provider_info.get_info
105
+ info = provider_info.info
106
106
 
107
107
  next unless info[:mcp_support]
108
108
 
data/lib/aidp/cli.rb CHANGED
@@ -147,6 +147,10 @@ module Aidp
147
147
  class << self
148
148
  extend Aidp::MessageDisplay::ClassMethods
149
149
 
150
+ def create_prompt
151
+ ::TTY::Prompt.new
152
+ end
153
+
150
154
  def run(args = ARGV)
151
155
  # Handle subcommands first (status, jobs, kb, harness)
152
156
  return run_subcommand(args) if subcommand?(args)
@@ -173,7 +177,7 @@ module Aidp
173
177
 
174
178
  # Handle configuration setup
175
179
  # Create a prompt for the wizard
176
- prompt = TTY::Prompt.new
180
+ prompt = create_prompt
177
181
 
178
182
  if options[:setup_config]
179
183
  # Force setup/reconfigure even if config exists
@@ -398,7 +402,7 @@ module Aidp
398
402
 
399
403
  def run_jobs_command(args = [])
400
404
  require_relative "cli/jobs_command"
401
- jobs_cmd = Aidp::CLI::JobsCommand.new(prompt: TTY::Prompt.new)
405
+ jobs_cmd = Aidp::CLI::JobsCommand.new(prompt: create_prompt)
402
406
  subcommand = args.shift
403
407
  jobs_cmd.run(subcommand, args)
404
408
  end
@@ -504,21 +508,6 @@ module Aidp
504
508
  if step
505
509
  display_message("Running #{mode} step '#{step}' with enhanced TUI harness", type: :highlight)
506
510
  display_message("progress indicators", type: :info)
507
- if step.start_with?("00_PRD") && (defined?(RSpec) || ENV["RSPEC_RUNNING"])
508
- # Simulate questions & completion similar to TUI test mode
509
- root = ENV["AIDP_ROOT"] || Dir.pwd
510
- file = Dir.glob(File.join(root, "templates", (mode == :execute) ? "EXECUTE" : "ANALYZE", "00_PRD*.md")).first
511
- if file && File.file?(file)
512
- content = File.read(file)
513
- questions_section = content.split(/## Questions/i)[1]
514
- if questions_section
515
- questions_section.lines.select { |l| l.strip.start_with?("-") }.each do |line|
516
- display_message(line.strip.sub(/^-\s*/, ""), type: :info)
517
- end
518
- end
519
- end
520
- display_message("PRD completed", type: :success)
521
- end
522
511
  return
523
512
  end
524
513
  display_message("Starting enhanced TUI harness", type: :highlight)
@@ -601,7 +590,7 @@ module Aidp
601
590
  when "clear"
602
591
  force = args.include?("--force")
603
592
  unless force
604
- prompt = TTY::Prompt.new
593
+ prompt = create_prompt
605
594
  confirm = prompt.yes?("Are you sure you want to clear all checkpoint data?")
606
595
  return unless confirm
607
596
  end
@@ -696,7 +685,7 @@ module Aidp
696
685
  end
697
686
  end
698
687
  config_manager = Aidp::Harness::ConfigManager.new(Dir.pwd)
699
- pm = Aidp::Harness::ProviderManager.new(config_manager, prompt: TTY::Prompt.new)
688
+ pm = Aidp::Harness::ProviderManager.new(config_manager, prompt: create_prompt)
700
689
 
701
690
  # Use TTY::Spinner for progress indication
702
691
  require "tty-spinner"
@@ -738,7 +727,12 @@ module Aidp
738
727
  end
739
728
  tokens = (r[:total_tokens].to_i > 0) ? r[:total_tokens].to_s : "0"
740
729
  reason = r[:unhealthy_reason] || "-"
741
- if no_color || !$stdout.tty?
730
+ is_tty = begin
731
+ $stdout.respond_to?(:tty?) && $stdout.tty?
732
+ rescue
733
+ false
734
+ end
735
+ if no_color || !is_tty
742
736
  [r[:provider], r[:status], (r[:available] ? "yes" : "no"), cb, rl, tokens, last_used, reason]
743
737
  else
744
738
  [
@@ -757,7 +751,7 @@ module Aidp
757
751
  table = TTY::Table.new header, table_rows
758
752
  display_message(table.render(:basic), type: :info)
759
753
  rescue => e
760
- log_rescue(e, component: "cli", action: "display_provider_health", fallback: "error_message")
754
+ Aidp.logger.warn("cli", "Failed to display provider health", error_class: e.class.name, error_message: e.message)
761
755
  display_message("Failed to display provider health: #{e.message}", type: :error)
762
756
  end
763
757
 
@@ -765,9 +759,10 @@ module Aidp
765
759
  require_relative "harness/provider_info"
766
760
 
767
761
  provider_name = args.shift
762
+
763
+ # If no provider specified, show models catalog table
768
764
  unless provider_name
769
- display_message("Usage: aidp providers info <provider_name>", type: :info)
770
- display_message("Example: aidp providers info claude", type: :info)
765
+ run_providers_models_catalog
771
766
  return
772
767
  end
773
768
 
@@ -777,7 +772,7 @@ module Aidp
777
772
  display_message("=" * 60, type: :muted)
778
773
 
779
774
  provider_info = Aidp::Harness::ProviderInfo.new(provider_name, Dir.pwd)
780
- info = provider_info.get_info(force_refresh: force_refresh)
775
+ info = provider_info.info(force_refresh: force_refresh)
781
776
 
782
777
  if info.nil?
783
778
  display_message("No information available for provider: #{provider_name}", type: :error)
@@ -845,6 +840,46 @@ module Aidp
845
840
  display_message("Tip: Use --refresh to update this information", type: :muted)
846
841
  end
847
842
 
843
+ def run_providers_models_catalog
844
+ require_relative "harness/capability_registry"
845
+ require "tty-table"
846
+
847
+ display_message("Models Catalog - Thinking Depth Tiers", type: :highlight)
848
+ display_message("=" * 80, type: :muted)
849
+
850
+ registry = Aidp::Harness::CapabilityRegistry.new
851
+ unless registry.load_catalog
852
+ display_message("No models catalog found. Create .aidp/models_catalog.yml first.", type: :error)
853
+ return
854
+ end
855
+
856
+ rows = []
857
+ registry.provider_names.sort.each do |provider|
858
+ models = registry.models_for_provider(provider)
859
+ models.each do |model_name, model_data|
860
+ tier = model_data["tier"] || "-"
861
+ context = model_data["context_window"] ? "#{model_data["context_window"] / 1000}k" : "-"
862
+ tools = model_data["supports_tools"] ? "yes" : "no"
863
+ cost_input = model_data["cost_per_mtok_input"]
864
+ cost = cost_input ? "$#{cost_input}/MTok" : "-"
865
+
866
+ rows << [provider, model_name, tier, context, tools, cost]
867
+ end
868
+ end
869
+
870
+ if rows.empty?
871
+ display_message("No models found in catalog", type: :info)
872
+ return
873
+ end
874
+
875
+ header = ["Provider", "Model", "Tier", "Context", "Tools", "Cost"]
876
+ table = TTY::Table.new(header, rows)
877
+ display_message(table.render(:basic), type: :info)
878
+
879
+ display_message("\n" + "=" * 80, type: :muted)
880
+ display_message("Use '/thinking show' in REPL to see current tier configuration", type: :muted)
881
+ end
882
+
848
883
  def run_providers_refresh_command(args)
849
884
  require_relative "harness/provider_info"
850
885
  require "tty-spinner"
@@ -1044,7 +1079,7 @@ module Aidp
1044
1079
  return
1045
1080
  end
1046
1081
 
1047
- wizard = Aidp::Setup::Wizard.new(Dir.pwd, prompt: TTY::Prompt.new, dry_run: dry_run)
1082
+ wizard = Aidp::Setup::Wizard.new(Dir.pwd, prompt: create_prompt, dry_run: dry_run)
1048
1083
  wizard.run
1049
1084
  end
1050
1085
 
@@ -1071,7 +1106,7 @@ module Aidp
1071
1106
  end
1072
1107
 
1073
1108
  require_relative "init/runner"
1074
- runner = Aidp::Init::Runner.new(Dir.pwd, prompt: TTY::Prompt.new, options: options)
1109
+ runner = Aidp::Init::Runner.new(Dir.pwd, prompt: create_prompt, options: options)
1075
1110
  runner.run
1076
1111
  end
1077
1112
 
@@ -1127,7 +1162,7 @@ module Aidp
1127
1162
  project_dir: Dir.pwd,
1128
1163
  once: once,
1129
1164
  use_workstreams: use_workstreams,
1130
- prompt: TTY::Prompt.new
1165
+ prompt: create_prompt
1131
1166
  )
1132
1167
  runner.start
1133
1168
  rescue ArgumentError => e
@@ -1244,7 +1279,7 @@ module Aidp
1244
1279
 
1245
1280
  # Confirm removal unless --force
1246
1281
  unless force
1247
- prompt = TTY::Prompt.new
1282
+ prompt = create_prompt
1248
1283
  confirm = prompt.yes?("Remove workstream '#{slug}'?#{" (will also delete branch)" if delete_branch}")
1249
1284
  return unless confirm
1250
1285
  end
@@ -2124,7 +2159,7 @@ module Aidp
2124
2159
 
2125
2160
  # Confirm deletion
2126
2161
  require "tty-prompt"
2127
- prompt = TTY::Prompt.new
2162
+ prompt = create_prompt
2128
2163
  confirmed = prompt.yes?("Delete skill '#{skill.name}' (#{skill_id})? This cannot be undone.")
2129
2164
 
2130
2165
  unless confirmed
data/lib/aidp/config.rb CHANGED
@@ -15,8 +15,7 @@ module Aidp
15
15
  no_api_keys_required: false,
16
16
  provider_weights: {
17
17
  "cursor" => 3,
18
- "anthropic" => 2,
19
- "macos" => 1
18
+ "anthropic" => 2
20
19
  },
21
20
  circuit_breaker: {
22
21
  enabled: true,
@@ -74,6 +73,7 @@ module Aidp
74
73
  cursor: {
75
74
  type: "subscription",
76
75
  priority: 1,
76
+ model_family: "auto",
77
77
  default_flags: [],
78
78
  models: ["cursor-default", "cursor-fast", "cursor-precise"],
79
79
  model_weights: {
@@ -108,6 +108,7 @@ module Aidp
108
108
  anthropic: {
109
109
  type: "usage_based",
110
110
  priority: 2,
111
+ model_family: "claude",
111
112
  max_tokens: 100_000,
112
113
  default_flags: ["--dangerously-skip-permissions"],
113
114
  models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"],
@@ -149,18 +150,6 @@ module Aidp
149
150
  enabled: true,
150
151
  metrics_interval: 60
151
152
  }
152
- },
153
- macos: {
154
- type: "passthrough",
155
- priority: 4,
156
- underlying_service: "cursor",
157
- models: ["cursor-chat"],
158
- features: {
159
- file_upload: false,
160
- code_generation: true,
161
- analysis: true,
162
- interactive: true
163
- }
164
153
  }
165
154
  },
166
155
  skills: {
@@ -342,6 +331,12 @@ module Aidp
342
331
  merged[:skills] = merged[:skills].merge(symbolize_keys(skills_section))
343
332
  end
344
333
 
334
+ # Deep merge thinking config
335
+ if config[:thinking] || config["thinking"]
336
+ thinking_section = config[:thinking] || config["thinking"]
337
+ merged[:thinking] = symbolize_keys(thinking_section)
338
+ end
339
+
345
340
  merged
346
341
  end
347
342
 
@@ -9,9 +9,10 @@ module Aidp
9
9
  class Progress
10
10
  attr_reader :project_dir, :progress_file
11
11
 
12
- def initialize(project_dir)
12
+ def initialize(project_dir, skip_persistence: false)
13
13
  @project_dir = project_dir
14
14
  @progress_file = File.join(project_dir, ".aidp", "progress", "execute.yml")
15
+ @skip_persistence = skip_persistence
15
16
  load_progress
16
17
  end
17
18
 
@@ -61,13 +62,11 @@ module Aidp
61
62
  private
62
63
 
63
64
  def load_progress
64
- # In test mode, only skip file operations if no progress file exists
65
- if (ENV["RACK_ENV"] == "test" || defined?(RSpec)) && !File.exist?(@progress_file)
65
+ if @skip_persistence && !File.exist?(@progress_file)
66
66
  @progress = {}
67
67
  return
68
68
  end
69
-
70
- @progress = if File.exist?(@progress_file)
69
+ @progress = if !@skip_persistence && File.exist?(@progress_file)
71
70
  YAML.safe_load_file(@progress_file, permitted_classes: [Date, Time, Symbol], aliases: true) || {}
72
71
  else
73
72
  {}
@@ -75,9 +74,7 @@ module Aidp
75
74
  end
76
75
 
77
76
  def save_progress
78
- # In test mode, skip file operations to avoid hanging
79
- return if ENV["RACK_ENV"] == "test" || defined?(RSpec)
80
-
77
+ return if @skip_persistence
81
78
  FileUtils.mkdir_p(File.dirname(@progress_file))
82
79
  File.write(@progress_file, @progress.to_yaml)
83
80
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require_relative "../prompt_optimization/optimizer"
4
5
 
5
6
  module Aidp
6
7
  module Execute
@@ -9,21 +10,91 @@ module Aidp
9
10
  # - Read/write PROMPT.md
10
11
  # - Check existence
11
12
  # - Archive completed prompts
13
+ # - Optionally optimize prompts using intelligent fragment selection (ZFC)
12
14
  class PromptManager
13
15
  PROMPT_FILENAME = "PROMPT.md"
14
16
  ARCHIVE_DIR = ".aidp/prompt_archive"
15
17
 
16
- def initialize(project_dir)
18
+ attr_reader :optimizer, :last_optimization_stats
19
+
20
+ def initialize(project_dir, config: nil)
17
21
  @project_dir = project_dir
18
22
  @prompt_path = File.join(project_dir, PROMPT_FILENAME)
19
23
  @archive_dir = File.join(project_dir, ARCHIVE_DIR)
24
+ @config = config
25
+ @optimizer = nil
26
+ @last_optimization_stats = nil
27
+
28
+ # Initialize optimizer if enabled
29
+ if config&.respond_to?(:prompt_optimization_enabled?) && config.prompt_optimization_enabled?
30
+ @optimizer = Aidp::PromptOptimization::Optimizer.new(
31
+ project_dir: project_dir,
32
+ config: config.prompt_optimization_config
33
+ )
34
+ end
20
35
  end
21
36
 
22
37
  # Write content to PROMPT.md
38
+ # If optimization is enabled, stores the content but doesn't write yet
39
+ # (use write_optimized instead)
23
40
  def write(content)
24
41
  File.write(@prompt_path, content)
25
42
  end
26
43
 
44
+ # Write optimized prompt using intelligent fragment selection
45
+ #
46
+ # Uses Zero Framework Cognition to select only the most relevant fragments
47
+ # from style guides, templates, and source code based on task context.
48
+ #
49
+ # @param task_context [Hash] Context about the current task
50
+ # @option task_context [Symbol] :task_type Type of task (:feature, :bugfix, etc.)
51
+ # @option task_context [String] :description Task description
52
+ # @option task_context [Array<String>] :affected_files Files being modified
53
+ # @option task_context [String] :step_name Current work loop step
54
+ # @option task_context [Array<String>] :tags Additional context tags
55
+ # @param options [Hash] Optimization options
56
+ # @option options [Boolean] :include_metadata Include debug metadata
57
+ # @return [Boolean] True if optimization was used, false if fallback to regular write
58
+ def write_optimized(task_context, options = {})
59
+ unless @optimizer
60
+ Aidp.logger.warn("prompt_manager", "Optimization requested but not enabled")
61
+ return false
62
+ end
63
+
64
+ begin
65
+ # Use optimizer to build intelligent prompt
66
+ result = @optimizer.optimize_prompt(
67
+ task_type: task_context[:task_type],
68
+ description: task_context[:description],
69
+ affected_files: task_context[:affected_files] || [],
70
+ step_name: task_context[:step_name],
71
+ tags: task_context[:tags] || [],
72
+ options: options
73
+ )
74
+
75
+ # Write optimized prompt
76
+ result.write_to_file(@prompt_path)
77
+
78
+ # Store statistics for inspection
79
+ @last_optimization_stats = result.composition_result
80
+
81
+ # Log optimization results
82
+ Aidp.logger.info(
83
+ "prompt_manager",
84
+ "Optimized prompt written",
85
+ selected_fragments: result.composition_result.selected_count,
86
+ excluded_fragments: result.composition_result.excluded_count,
87
+ tokens: result.estimated_tokens,
88
+ budget_utilization: result.composition_result.budget_utilization
89
+ )
90
+
91
+ true
92
+ rescue => e
93
+ Aidp.logger.error("prompt_manager", "Optimization failed, using fallback", error: e.message)
94
+ false
95
+ end
96
+ end
97
+
27
98
  # Read content from PROMPT.md
28
99
  def read
29
100
  return nil unless exists?
@@ -57,6 +128,62 @@ module Aidp
57
128
  def path
58
129
  @prompt_path
59
130
  end
131
+
132
+ # Get optimization report for last optimization
133
+ #
134
+ # @return [String, nil] Markdown report or nil if no optimization performed
135
+ def optimization_report
136
+ return nil unless @last_optimization_stats
137
+
138
+ # Build report from composition result
139
+ lines = []
140
+ lines << "# Prompt Optimization Report"
141
+ lines << ""
142
+ lines << "## Statistics"
143
+ lines << "- **Selected Fragments**: #{@last_optimization_stats.selected_count}"
144
+ lines << "- **Excluded Fragments**: #{@last_optimization_stats.excluded_count}"
145
+ lines << "- **Total Tokens**: #{@last_optimization_stats.total_tokens} / #{@last_optimization_stats.budget}"
146
+ lines << "- **Budget Utilization**: #{@last_optimization_stats.budget_utilization.round(1)}%"
147
+ lines << "- **Average Relevance Score**: #{(@last_optimization_stats.average_score * 100).round(1)}%"
148
+ lines << ""
149
+ lines << "## Selected Fragments"
150
+ @last_optimization_stats.selected_fragments.each do |scored|
151
+ fragment = scored[:fragment]
152
+ score = scored[:score]
153
+ lines << "- #{fragment_name(fragment)} (#{(score * 100).round(0)}%)"
154
+ end
155
+
156
+ lines.join("\n")
157
+ end
158
+
159
+ # Check if optimization is enabled
160
+ #
161
+ # @return [Boolean] True if optimizer is available
162
+ def optimization_enabled?
163
+ !@optimizer.nil?
164
+ end
165
+
166
+ # Get optimizer statistics
167
+ #
168
+ # @return [Hash, nil] Statistics hash or nil if optimizer not available
169
+ def optimizer_stats
170
+ @optimizer&.statistics
171
+ end
172
+
173
+ private
174
+
175
+ # Get human-readable name for a fragment
176
+ def fragment_name(fragment)
177
+ if fragment.respond_to?(:heading)
178
+ fragment.heading
179
+ elsif fragment.respond_to?(:name)
180
+ fragment.name
181
+ elsif fragment.respond_to?(:id)
182
+ fragment.id
183
+ else
184
+ "Unknown fragment"
185
+ end
186
+ end
60
187
  end
61
188
  end
62
189
  end