aidp 0.11.0 → 0.12.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/lib/aidp/cli/mcp_dashboard.rb +205 -0
- data/lib/aidp/cli.rb +206 -6
- data/lib/aidp/harness/provider_info.rb +366 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +22 -1
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/workflows/guided_agent.rb +400 -0
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5c07df32790749c9d60716d1ae3133f0a232cda036e7523b74b8c0725a0b066
|
4
|
+
data.tar.gz: b15140f57f3d8cc58dc4b4e79200d3b0ef50152c5f6bcd6f45b030aeca8c863e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e5e5b0f9c209dea98509a29b4004f264cb49706d961cafe42ad2867a40abc6ecd9e3cf279a541e714c914f975ec25f6ed898a7be7549dc6ae1292e369eaa7b9
|
7
|
+
data.tar.gz: 5f7a286a65cd9e1ecbdbd167182fa1471e9407171a2fc028cfa144d9204606d8c8e3a7138348c8406133713aa1724749e7abcc0fa6904fca64b65b43355bbfd8
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-table"
|
4
|
+
require_relative "../harness/provider_info"
|
5
|
+
require_relative "../harness/configuration"
|
6
|
+
|
7
|
+
module Aidp
|
8
|
+
class CLI
|
9
|
+
# Dashboard for viewing MCP servers across all providers
|
10
|
+
class McpDashboard
|
11
|
+
include Aidp::MessageDisplay
|
12
|
+
|
13
|
+
def initialize(root_dir = nil)
|
14
|
+
@root_dir = root_dir || Dir.pwd
|
15
|
+
@configuration = Aidp::Harness::Configuration.new(@root_dir)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Display MCP dashboard showing all servers across all providers
|
19
|
+
def display_dashboard(options = {})
|
20
|
+
no_color = options[:no_color] || false
|
21
|
+
|
22
|
+
# Gather MCP server information from all providers
|
23
|
+
server_matrix = build_server_matrix
|
24
|
+
|
25
|
+
# Display summary
|
26
|
+
display_message("MCP Server Dashboard", type: :highlight)
|
27
|
+
display_message("=" * 80, type: :muted)
|
28
|
+
display_message("", type: :info)
|
29
|
+
|
30
|
+
# Check if there are any MCP-capable providers
|
31
|
+
if server_matrix[:providers].empty?
|
32
|
+
display_message("No providers with MCP support configured.", type: :info)
|
33
|
+
display_message("MCP is supported by providers like: claude", type: :muted)
|
34
|
+
display_message("\n" + "=" * 80, type: :muted)
|
35
|
+
return
|
36
|
+
end
|
37
|
+
|
38
|
+
# Check if there are any MCP servers configured
|
39
|
+
if server_matrix[:servers].empty?
|
40
|
+
display_message("No MCP servers configured across any providers.", type: :info)
|
41
|
+
display_message("Add MCP servers with: claude mcp add <name> -- <command>", type: :muted)
|
42
|
+
display_message("\n" + "=" * 80, type: :muted)
|
43
|
+
return
|
44
|
+
end
|
45
|
+
|
46
|
+
# Display the main table
|
47
|
+
display_server_table(server_matrix, no_color)
|
48
|
+
|
49
|
+
# Display eligibility warnings
|
50
|
+
display_eligibility_warnings(server_matrix)
|
51
|
+
|
52
|
+
display_message("\n" + "=" * 80, type: :muted)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get MCP server availability for a specific task requirement
|
56
|
+
def check_task_eligibility(required_servers)
|
57
|
+
server_matrix = build_server_matrix
|
58
|
+
eligible_providers = []
|
59
|
+
|
60
|
+
@configuration.configured_providers.each do |provider|
|
61
|
+
provider_servers = server_matrix[:provider_servers][provider] || []
|
62
|
+
enabled_servers = provider_servers.select { |s| s[:enabled] }.map { |s| s[:name] }
|
63
|
+
|
64
|
+
# Check if provider has all required servers
|
65
|
+
if required_servers.all? { |req| enabled_servers.include?(req) }
|
66
|
+
eligible_providers << provider
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
{
|
71
|
+
required_servers: required_servers,
|
72
|
+
eligible_providers: eligible_providers,
|
73
|
+
total_providers: @configuration.configured_providers.size
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
# Display eligibility check for specific servers
|
78
|
+
def display_task_eligibility(required_servers)
|
79
|
+
result = check_task_eligibility(required_servers)
|
80
|
+
|
81
|
+
display_message("\nTask Eligibility Check", type: :highlight)
|
82
|
+
display_message("Required MCP Servers: #{required_servers.join(", ")}", type: :info)
|
83
|
+
display_message("", type: :info)
|
84
|
+
|
85
|
+
if result[:eligible_providers].any?
|
86
|
+
display_message("✓ Eligible Providers (#{result[:eligible_providers].size}/#{result[:total_providers]}):", type: :success)
|
87
|
+
result[:eligible_providers].each do |provider|
|
88
|
+
display_message(" • #{provider}", type: :success)
|
89
|
+
end
|
90
|
+
else
|
91
|
+
display_message("✗ No providers have all required MCP servers", type: :error)
|
92
|
+
display_message(" Consider configuring MCP servers for at least one provider", type: :warning)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def build_server_matrix
|
99
|
+
providers = @configuration.configured_providers
|
100
|
+
all_servers = {} # server_name => {providers: {provider_name => server_info}}
|
101
|
+
provider_servers = {} # provider_name => [server_info]
|
102
|
+
|
103
|
+
providers.each do |provider|
|
104
|
+
provider_info = Aidp::Harness::ProviderInfo.new(provider, @root_dir)
|
105
|
+
info = provider_info.get_info
|
106
|
+
|
107
|
+
next unless info[:mcp_support]
|
108
|
+
|
109
|
+
servers = info[:mcp_servers] || []
|
110
|
+
provider_servers[provider] = servers
|
111
|
+
|
112
|
+
servers.each do |server|
|
113
|
+
server_name = server[:name]
|
114
|
+
all_servers[server_name] ||= {providers: {}}
|
115
|
+
all_servers[server_name][:providers][provider] = server
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
{
|
120
|
+
servers: all_servers,
|
121
|
+
provider_servers: provider_servers,
|
122
|
+
providers: providers.select { |p| provider_servers.key?(p) }
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
def display_server_table(matrix, no_color)
|
127
|
+
# Check if we have any providers with MCP support
|
128
|
+
if matrix[:providers].empty?
|
129
|
+
display_message("No providers with MCP support configured.", type: :info)
|
130
|
+
return
|
131
|
+
end
|
132
|
+
|
133
|
+
# Build table rows
|
134
|
+
headers = ["MCP Server"] + matrix[:providers].map { |p| normalize_provider_name(p) }
|
135
|
+
rows = []
|
136
|
+
|
137
|
+
matrix[:servers].keys.sort.each do |server_name|
|
138
|
+
row = [server_name]
|
139
|
+
|
140
|
+
matrix[:providers].each do |provider|
|
141
|
+
server = matrix[:servers][server_name][:providers][provider]
|
142
|
+
cell = if server
|
143
|
+
format_server_status(server, no_color)
|
144
|
+
else
|
145
|
+
(no_color || !$stdout.tty?) ? "-" : "\e[90m-\e[0m"
|
146
|
+
end
|
147
|
+
row << cell
|
148
|
+
end
|
149
|
+
|
150
|
+
rows << row
|
151
|
+
end
|
152
|
+
|
153
|
+
# Create and display table
|
154
|
+
table = TTY::Table.new(headers, rows)
|
155
|
+
display_message(table.render(:basic), type: :info)
|
156
|
+
display_message("", type: :info)
|
157
|
+
|
158
|
+
# Legend
|
159
|
+
display_legend(no_color)
|
160
|
+
end
|
161
|
+
|
162
|
+
def format_server_status(server, no_color)
|
163
|
+
if no_color || !$stdout.tty?
|
164
|
+
server[:enabled] ? "✓" : "✗"
|
165
|
+
elsif server[:enabled]
|
166
|
+
"\e[32m✓\e[0m" # Green checkmark
|
167
|
+
else
|
168
|
+
"\e[31m✗\e[0m" # Red X
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def display_legend(no_color)
|
173
|
+
if no_color || !$stdout.tty?
|
174
|
+
display_message("Legend: ✓ = Enabled ✗ = Error/Disabled - = Not configured", type: :muted)
|
175
|
+
else
|
176
|
+
display_message("Legend: \e[32m✓\e[0m = Enabled \e[31m✗\e[0m = Error/Disabled \e[90m-\e[0m = Not configured", type: :muted)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def display_eligibility_warnings(matrix)
|
181
|
+
# Find servers that are only configured on some providers
|
182
|
+
partially_configured = matrix[:servers].select do |_name, info|
|
183
|
+
configured_count = info[:providers].size
|
184
|
+
configured_count > 0 && configured_count < matrix[:providers].size
|
185
|
+
end
|
186
|
+
|
187
|
+
return if partially_configured.empty?
|
188
|
+
|
189
|
+
display_message("\n⚠ Eligibility Warnings:", type: :warning)
|
190
|
+
partially_configured.each do |server_name, info|
|
191
|
+
missing_providers = matrix[:providers] - info[:providers].keys
|
192
|
+
if missing_providers.any?
|
193
|
+
display_message(" • '#{server_name}' not configured on: #{missing_providers.join(", ")}", type: :warning)
|
194
|
+
display_message(" These providers won't be eligible for tasks requiring this MCP server", type: :muted)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def normalize_provider_name(name)
|
200
|
+
return "claude" if name == "anthropic"
|
201
|
+
name
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
data/lib/aidp/cli.rb
CHANGED
@@ -183,7 +183,7 @@ module Aidp
|
|
183
183
|
|
184
184
|
# Initialize the enhanced TUI
|
185
185
|
tui = Aidp::Harness::UI::EnhancedTUI.new
|
186
|
-
workflow_selector = Aidp::Harness::UI::EnhancedWorkflowSelector.new(tui)
|
186
|
+
workflow_selector = Aidp::Harness::UI::EnhancedWorkflowSelector.new(tui, project_dir: Dir.pwd)
|
187
187
|
|
188
188
|
# Start TUI display loop
|
189
189
|
tui.start_display_loop
|
@@ -245,6 +245,11 @@ module Aidp
|
|
245
245
|
opts.separator " metrics - Show detailed metrics"
|
246
246
|
opts.separator " clear [--force] - Clear checkpoint data"
|
247
247
|
opts.separator " providers Show provider health dashboard"
|
248
|
+
opts.separator " info <name> - Show detailed provider information"
|
249
|
+
opts.separator " refresh [name] - Refresh provider capabilities info"
|
250
|
+
opts.separator " mcp MCP server dashboard and management"
|
251
|
+
opts.separator " dashboard - Show all MCP servers across providers"
|
252
|
+
opts.separator " check <servers...> - Check provider eligibility for servers"
|
248
253
|
opts.separator " harness Manage harness state"
|
249
254
|
opts.separator " status - Show harness status"
|
250
255
|
opts.separator " reset - Reset harness state"
|
@@ -274,6 +279,10 @@ module Aidp
|
|
274
279
|
opts.separator ""
|
275
280
|
opts.separator " # Other commands"
|
276
281
|
opts.separator " aidp providers # Check provider health"
|
282
|
+
opts.separator " aidp providers info claude # Show detailed provider info"
|
283
|
+
opts.separator " aidp providers refresh # Refresh all provider info"
|
284
|
+
opts.separator " aidp mcp # Show MCP server dashboard"
|
285
|
+
opts.separator " aidp mcp check dash-api filesystem # Check provider eligibility"
|
277
286
|
opts.separator " aidp checkpoint history 20 # Show last 20 checkpoints"
|
278
287
|
opts.separator ""
|
279
288
|
opts.separator "For more information, visit: https://github.com/viamin/aidp"
|
@@ -287,7 +296,7 @@ module Aidp
|
|
287
296
|
# Determine if the invocation is a subcommand style call
|
288
297
|
def subcommand?(args)
|
289
298
|
return false if args.nil? || args.empty?
|
290
|
-
%w[status jobs kb harness execute analyze providers checkpoint].include?(args.first)
|
299
|
+
%w[status jobs kb harness execute analyze providers checkpoint mcp].include?(args.first)
|
291
300
|
end
|
292
301
|
|
293
302
|
def run_subcommand(args)
|
@@ -301,6 +310,7 @@ module Aidp
|
|
301
310
|
when "analyze" then run_execute_command(args, mode: :analyze) # symmetry
|
302
311
|
when "providers" then run_providers_command(args)
|
303
312
|
when "checkpoint" then run_checkpoint_command(args)
|
313
|
+
when "mcp" then run_mcp_command(args)
|
304
314
|
else
|
305
315
|
display_message("Unknown command: #{cmd}", type: :info)
|
306
316
|
return 1
|
@@ -585,6 +595,19 @@ module Aidp
|
|
585
595
|
end
|
586
596
|
|
587
597
|
def run_providers_command(args)
|
598
|
+
subcommand = args.first if args.first && !args.first.start_with?("--")
|
599
|
+
|
600
|
+
case subcommand
|
601
|
+
when "info"
|
602
|
+
args.shift # Remove 'info'
|
603
|
+
run_providers_info_command(args)
|
604
|
+
return
|
605
|
+
when "refresh"
|
606
|
+
args.shift # Remove 'refresh'
|
607
|
+
run_providers_refresh_command(args)
|
608
|
+
return
|
609
|
+
end
|
610
|
+
|
588
611
|
# Accept flags directly on `aidp providers` now (health is implicit)
|
589
612
|
no_color = false
|
590
613
|
args.reject! do |a|
|
@@ -660,6 +683,180 @@ module Aidp
|
|
660
683
|
display_message("Failed to display provider health: #{e.message}", type: :error)
|
661
684
|
end
|
662
685
|
|
686
|
+
def run_providers_info_command(args)
|
687
|
+
require_relative "harness/provider_info"
|
688
|
+
|
689
|
+
provider_name = args.shift
|
690
|
+
unless provider_name
|
691
|
+
display_message("Usage: aidp providers info <provider_name>", type: :info)
|
692
|
+
display_message("Example: aidp providers info claude", type: :info)
|
693
|
+
return
|
694
|
+
end
|
695
|
+
|
696
|
+
force_refresh = args.include?("--refresh")
|
697
|
+
|
698
|
+
display_message("Provider Information: #{provider_name}", type: :highlight)
|
699
|
+
display_message("=" * 60, type: :muted)
|
700
|
+
|
701
|
+
provider_info = Aidp::Harness::ProviderInfo.new(provider_name, Dir.pwd)
|
702
|
+
info = provider_info.get_info(force_refresh: force_refresh)
|
703
|
+
|
704
|
+
if info.nil?
|
705
|
+
display_message("No information available for provider: #{provider_name}", type: :error)
|
706
|
+
return
|
707
|
+
end
|
708
|
+
|
709
|
+
# Display basic info
|
710
|
+
display_message("Last Checked: #{info[:last_checked]}", type: :info)
|
711
|
+
display_message("CLI Available: #{info[:cli_available] ? "Yes" : "No"}", type: info[:cli_available] ? :success : :error)
|
712
|
+
|
713
|
+
# Display authentication
|
714
|
+
if info[:auth_method]
|
715
|
+
display_message("\nAuthentication Method: #{info[:auth_method]}", type: :info)
|
716
|
+
end
|
717
|
+
|
718
|
+
# Display MCP support
|
719
|
+
display_message("\nMCP Support: #{info[:mcp_support] ? "Yes" : "No"}", type: info[:mcp_support] ? :success : :info)
|
720
|
+
|
721
|
+
# Display MCP servers if available
|
722
|
+
if info[:mcp_servers]&.any?
|
723
|
+
display_message("\nMCP Servers: (#{info[:mcp_servers].size} configured)", type: :highlight)
|
724
|
+
info[:mcp_servers].each do |server|
|
725
|
+
status_symbol = server[:enabled] ? "✓" : "○"
|
726
|
+
display_message(" #{status_symbol} #{server[:name]} (#{server[:status]})", type: server[:enabled] ? :success : :muted)
|
727
|
+
display_message(" #{server[:description]}", type: :muted) if server[:description]
|
728
|
+
end
|
729
|
+
elsif info[:mcp_support]
|
730
|
+
display_message("\nMCP Servers: None configured", type: :muted)
|
731
|
+
end
|
732
|
+
|
733
|
+
# Display permission modes
|
734
|
+
if info[:permission_modes]&.any?
|
735
|
+
display_message("\nPermission Modes:", type: :highlight)
|
736
|
+
info[:permission_modes].each do |mode|
|
737
|
+
display_message(" - #{mode}", type: :info)
|
738
|
+
end
|
739
|
+
end
|
740
|
+
|
741
|
+
# Display capabilities
|
742
|
+
if info[:capabilities]&.any?
|
743
|
+
display_message("\nCapabilities:", type: :highlight)
|
744
|
+
info[:capabilities].each do |cap, value|
|
745
|
+
next unless value
|
746
|
+
|
747
|
+
display_message(" ✓ #{cap.to_s.split("_").map(&:capitalize).join(" ")}", type: :success)
|
748
|
+
end
|
749
|
+
end
|
750
|
+
|
751
|
+
# Display notable flags
|
752
|
+
if info[:flags]&.any?
|
753
|
+
display_message("\nNotable Flags: (#{info[:flags].size} total)", type: :highlight)
|
754
|
+
# Show first 10 flags
|
755
|
+
info[:flags].take(10).each do |name, flag_info|
|
756
|
+
display_message(" #{flag_info[:flag]}", type: :info)
|
757
|
+
display_message(" #{flag_info[:description][0..80]}...", type: :muted) if flag_info[:description]
|
758
|
+
end
|
759
|
+
|
760
|
+
if info[:flags].size > 10
|
761
|
+
display_message("\n ... and #{info[:flags].size - 10} more flags", type: :muted)
|
762
|
+
display_message(" Run '#{get_binary_name(provider_name)} --help' for full details", type: :muted)
|
763
|
+
end
|
764
|
+
end
|
765
|
+
|
766
|
+
display_message("\n" + "=" * 60, type: :muted)
|
767
|
+
display_message("Tip: Use --refresh to update this information", type: :muted)
|
768
|
+
end
|
769
|
+
|
770
|
+
def run_providers_refresh_command(args)
|
771
|
+
require_relative "harness/provider_info"
|
772
|
+
require "tty-spinner"
|
773
|
+
|
774
|
+
provider_name = args.shift
|
775
|
+
configuration = Aidp::Harness::Configuration.new(Dir.pwd)
|
776
|
+
providers_to_refresh = if provider_name
|
777
|
+
[provider_name]
|
778
|
+
else
|
779
|
+
configuration.configured_providers
|
780
|
+
end
|
781
|
+
|
782
|
+
display_message("Refreshing provider information...", type: :info)
|
783
|
+
display_message("", type: :info)
|
784
|
+
|
785
|
+
providers_to_refresh.each do |prov|
|
786
|
+
spinner = TTY::Spinner.new("[:spinner] #{prov}...", format: :dots)
|
787
|
+
spinner.auto_spin
|
788
|
+
|
789
|
+
provider_info = Aidp::Harness::ProviderInfo.new(prov, Dir.pwd)
|
790
|
+
info = provider_info.gather_info
|
791
|
+
|
792
|
+
if info[:cli_available]
|
793
|
+
spinner.success("(available)")
|
794
|
+
else
|
795
|
+
spinner.error("(unavailable)")
|
796
|
+
end
|
797
|
+
end
|
798
|
+
|
799
|
+
display_message("\n✓ Provider information refreshed", type: :success)
|
800
|
+
display_message("Use 'aidp providers info <name>' to view details", type: :muted)
|
801
|
+
end
|
802
|
+
|
803
|
+
def run_mcp_command(args)
|
804
|
+
require_relative "cli/mcp_dashboard"
|
805
|
+
|
806
|
+
subcommand = args.shift
|
807
|
+
|
808
|
+
dashboard = Aidp::CLI::McpDashboard.new(Dir.pwd)
|
809
|
+
|
810
|
+
case subcommand
|
811
|
+
when "dashboard", "list", nil
|
812
|
+
# Extract flags
|
813
|
+
no_color = args.include?("--no-color")
|
814
|
+
dashboard.display_dashboard(no_color: no_color)
|
815
|
+
|
816
|
+
when "check"
|
817
|
+
# Check eligibility for specific servers
|
818
|
+
required_servers = args
|
819
|
+
if required_servers.empty?
|
820
|
+
display_message("Usage: aidp mcp check <server1> [server2] ...", type: :info)
|
821
|
+
display_message("Example: aidp mcp check filesystem brave-search", type: :info)
|
822
|
+
return
|
823
|
+
end
|
824
|
+
|
825
|
+
dashboard.display_task_eligibility(required_servers)
|
826
|
+
|
827
|
+
else
|
828
|
+
display_message("Usage: aidp mcp <command>", type: :info)
|
829
|
+
display_message("", type: :info)
|
830
|
+
display_message("Commands:", type: :info)
|
831
|
+
display_message(" dashboard, list Show MCP servers across all providers (default)", type: :info)
|
832
|
+
display_message(" check <servers...> Check which providers have required MCP servers", type: :info)
|
833
|
+
display_message("", type: :info)
|
834
|
+
display_message("Examples:", type: :info)
|
835
|
+
display_message(" aidp mcp # Show dashboard", type: :info)
|
836
|
+
display_message(" aidp mcp dashboard --no-color # Show without colors", type: :info)
|
837
|
+
display_message(" aidp mcp check filesystem dash-api # Check provider eligibility", type: :info)
|
838
|
+
end
|
839
|
+
end
|
840
|
+
|
841
|
+
def get_binary_name(provider_name)
|
842
|
+
case provider_name
|
843
|
+
when "claude", "anthropic"
|
844
|
+
"claude"
|
845
|
+
when "cursor"
|
846
|
+
"cursor"
|
847
|
+
when "gemini"
|
848
|
+
"gemini"
|
849
|
+
when "codex"
|
850
|
+
"codex"
|
851
|
+
when "github_copilot"
|
852
|
+
"gh"
|
853
|
+
when "opencode"
|
854
|
+
"opencode"
|
855
|
+
else
|
856
|
+
provider_name
|
857
|
+
end
|
858
|
+
end
|
859
|
+
|
663
860
|
def extract_mode_option(args)
|
664
861
|
mode = nil
|
665
862
|
args.each do |arg|
|
@@ -677,17 +874,20 @@ module Aidp
|
|
677
874
|
|
678
875
|
def select_mode_interactive(tui)
|
679
876
|
mode_options = [
|
877
|
+
"🤖 Guided Workflow (Copilot) - AI helps you choose the right workflow",
|
680
878
|
"🔬 Analyze Mode - Analyze your codebase for insights and recommendations",
|
681
879
|
"🏗️ Execute Mode - Build new features with guided development workflow"
|
682
880
|
]
|
683
881
|
selected = tui.single_select("Welcome to AI Dev Pipeline! Choose your mode", mode_options, default: 1)
|
684
882
|
# Announce mode explicitly in headless contexts (handled internally otherwise)
|
685
883
|
if (defined?(RSpec) || ENV["RSPEC_RUNNING"]) && tui.respond_to?(:announce_mode)
|
686
|
-
tui.announce_mode(:
|
687
|
-
tui.announce_mode(:
|
884
|
+
tui.announce_mode(:guided) if selected == mode_options[0]
|
885
|
+
tui.announce_mode(:analyze) if selected == mode_options[1]
|
886
|
+
tui.announce_mode(:execute) if selected == mode_options[2]
|
688
887
|
end
|
689
|
-
return :
|
690
|
-
return :
|
888
|
+
return :guided if selected == mode_options[0]
|
889
|
+
return :analyze if selected == mode_options[1]
|
890
|
+
return :execute if selected == mode_options[2]
|
691
891
|
:analyze
|
692
892
|
end
|
693
893
|
|
@@ -0,0 +1,366 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "yaml"
|
5
|
+
require "fileutils"
|
6
|
+
|
7
|
+
module Aidp
|
8
|
+
module Harness
|
9
|
+
# Stores detailed information about AI providers gathered from their CLI tools
|
10
|
+
class ProviderInfo
|
11
|
+
attr_reader :provider_name, :info_file_path
|
12
|
+
|
13
|
+
def initialize(provider_name, root_dir = nil)
|
14
|
+
@provider_name = provider_name
|
15
|
+
@root_dir = root_dir || Dir.pwd
|
16
|
+
@info_file_path = File.join(@root_dir, ".aidp", "providers", "#{provider_name}_info.yml")
|
17
|
+
ensure_directory_exists
|
18
|
+
end
|
19
|
+
|
20
|
+
# Gather information about the provider by introspecting its CLI
|
21
|
+
def gather_info
|
22
|
+
info = {
|
23
|
+
provider: @provider_name,
|
24
|
+
last_checked: Time.now.iso8601,
|
25
|
+
cli_available: false,
|
26
|
+
help_output: nil,
|
27
|
+
capabilities: {},
|
28
|
+
permission_modes: [],
|
29
|
+
mcp_support: false,
|
30
|
+
mcp_servers: [],
|
31
|
+
auth_method: nil,
|
32
|
+
flags: {}
|
33
|
+
}
|
34
|
+
|
35
|
+
# Try to get help output from the provider CLI
|
36
|
+
help_output = fetch_help_output
|
37
|
+
if help_output
|
38
|
+
info[:cli_available] = true
|
39
|
+
info[:help_output] = help_output
|
40
|
+
info.merge!(parse_help_output(help_output))
|
41
|
+
end
|
42
|
+
|
43
|
+
# Try to get MCP server list if supported
|
44
|
+
if info[:mcp_support]
|
45
|
+
mcp_servers = fetch_mcp_servers
|
46
|
+
info[:mcp_servers] = mcp_servers if mcp_servers
|
47
|
+
end
|
48
|
+
|
49
|
+
save_info(info)
|
50
|
+
info
|
51
|
+
end
|
52
|
+
|
53
|
+
# Load stored provider info
|
54
|
+
def load_info
|
55
|
+
return nil unless File.exist?(@info_file_path)
|
56
|
+
|
57
|
+
YAML.safe_load_file(@info_file_path, permitted_classes: [Time, Symbol])
|
58
|
+
rescue => e
|
59
|
+
warn "Failed to load provider info for #{@provider_name}: #{e.message}"
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
# Get provider info, refreshing if needed
|
64
|
+
def get_info(force_refresh: false, max_age: 86400)
|
65
|
+
existing_info = load_info
|
66
|
+
|
67
|
+
# Refresh if forced, missing, or stale
|
68
|
+
if force_refresh || existing_info.nil? || info_stale?(existing_info, max_age)
|
69
|
+
gather_info
|
70
|
+
else
|
71
|
+
existing_info
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Check if provider supports MCP servers
|
76
|
+
def supports_mcp?
|
77
|
+
info = load_info
|
78
|
+
return false unless info
|
79
|
+
|
80
|
+
info[:mcp_support] == true
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get permission modes available
|
84
|
+
def permission_modes
|
85
|
+
info = load_info
|
86
|
+
return [] unless info
|
87
|
+
|
88
|
+
info[:permission_modes] || []
|
89
|
+
end
|
90
|
+
|
91
|
+
# Get authentication method
|
92
|
+
def auth_method
|
93
|
+
info = load_info
|
94
|
+
return nil unless info
|
95
|
+
|
96
|
+
info[:auth_method]
|
97
|
+
end
|
98
|
+
|
99
|
+
# Get available flags/options
|
100
|
+
def available_flags
|
101
|
+
info = load_info
|
102
|
+
return {} unless info
|
103
|
+
|
104
|
+
info[:flags] || {}
|
105
|
+
end
|
106
|
+
|
107
|
+
# Get configured MCP servers
|
108
|
+
def mcp_servers
|
109
|
+
info = load_info
|
110
|
+
return [] unless info
|
111
|
+
|
112
|
+
info[:mcp_servers] || []
|
113
|
+
end
|
114
|
+
|
115
|
+
# Check if provider has MCP servers configured
|
116
|
+
def has_mcp_servers?
|
117
|
+
mcp_servers.any?
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def ensure_directory_exists
|
123
|
+
dir = File.dirname(@info_file_path)
|
124
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
125
|
+
end
|
126
|
+
|
127
|
+
def save_info(info)
|
128
|
+
File.write(@info_file_path, YAML.dump(info))
|
129
|
+
end
|
130
|
+
|
131
|
+
def info_stale?(info, max_age)
|
132
|
+
return true unless info[:last_checked]
|
133
|
+
|
134
|
+
last_checked = Time.parse(info[:last_checked].to_s)
|
135
|
+
(Time.now - last_checked) > max_age
|
136
|
+
rescue
|
137
|
+
true
|
138
|
+
end
|
139
|
+
|
140
|
+
def fetch_help_output
|
141
|
+
execute_provider_command("--help")
|
142
|
+
end
|
143
|
+
|
144
|
+
def fetch_mcp_servers
|
145
|
+
binary = get_binary_name
|
146
|
+
return nil unless binary
|
147
|
+
|
148
|
+
# Try different MCP list commands based on provider
|
149
|
+
mcp_output = case @provider_name
|
150
|
+
when "claude", "anthropic"
|
151
|
+
execute_provider_command("mcp", "list")
|
152
|
+
end
|
153
|
+
|
154
|
+
return nil unless mcp_output
|
155
|
+
|
156
|
+
parse_mcp_servers(mcp_output)
|
157
|
+
end
|
158
|
+
|
159
|
+
def execute_provider_command(*args)
|
160
|
+
binary = get_binary_name
|
161
|
+
return nil unless binary
|
162
|
+
|
163
|
+
# Try to find the binary
|
164
|
+
path = begin
|
165
|
+
Aidp::Util.which(binary)
|
166
|
+
rescue
|
167
|
+
nil
|
168
|
+
end
|
169
|
+
return nil unless path
|
170
|
+
|
171
|
+
# Execute command with timeout
|
172
|
+
begin
|
173
|
+
r, w = IO.pipe
|
174
|
+
pid = Process.spawn(binary, *args, out: w, err: w)
|
175
|
+
w.close
|
176
|
+
|
177
|
+
# Wait with timeout
|
178
|
+
deadline = Time.now + 5
|
179
|
+
status = nil
|
180
|
+
while Time.now < deadline
|
181
|
+
pid_done, status = Process.waitpid2(pid, Process::WNOHANG)
|
182
|
+
break if pid_done
|
183
|
+
sleep 0.05
|
184
|
+
end
|
185
|
+
|
186
|
+
# Kill if timed out
|
187
|
+
unless status
|
188
|
+
begin
|
189
|
+
Process.kill("TERM", pid)
|
190
|
+
sleep 0.1
|
191
|
+
Process.kill("KILL", pid)
|
192
|
+
rescue
|
193
|
+
nil
|
194
|
+
end
|
195
|
+
return nil
|
196
|
+
end
|
197
|
+
|
198
|
+
output = r.read
|
199
|
+
r.close
|
200
|
+
output
|
201
|
+
rescue
|
202
|
+
nil
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def parse_mcp_servers(output)
|
207
|
+
servers = []
|
208
|
+
return servers unless output
|
209
|
+
|
210
|
+
# Parse MCP server list output
|
211
|
+
# Claude format (as of 2025):
|
212
|
+
# dash-api: uvx --from git+https://... - ✓ Connected
|
213
|
+
# or
|
214
|
+
# server-name: command - ✗ Error message
|
215
|
+
#
|
216
|
+
# Legacy format:
|
217
|
+
# Name Status Description
|
218
|
+
# filesystem enabled File system access
|
219
|
+
|
220
|
+
lines = output.lines
|
221
|
+
|
222
|
+
# Skip header lines
|
223
|
+
lines.reject! { |line| /checking mcp server health/i.match?(line) }
|
224
|
+
|
225
|
+
lines.each do |line|
|
226
|
+
line = line.strip
|
227
|
+
next if line.empty?
|
228
|
+
|
229
|
+
# Try to parse new Claude format: "name: command - ✓ Connected"
|
230
|
+
if line =~ /^([^:]+):\s*(.+?)\s*-\s*(✓|✗)\s*(.+)$/
|
231
|
+
name = Regexp.last_match(1).strip
|
232
|
+
command = Regexp.last_match(2).strip
|
233
|
+
status_symbol = Regexp.last_match(3)
|
234
|
+
status_text = Regexp.last_match(4).strip
|
235
|
+
|
236
|
+
servers << {
|
237
|
+
name: name,
|
238
|
+
status: (status_symbol == "✓") ? "connected" : "error",
|
239
|
+
description: command,
|
240
|
+
enabled: status_symbol == "✓",
|
241
|
+
error: (status_symbol == "✗") ? status_text : nil
|
242
|
+
}
|
243
|
+
next
|
244
|
+
end
|
245
|
+
|
246
|
+
# Try to parse legacy table format
|
247
|
+
# Skip header line
|
248
|
+
next if /Name.*Status/i.match?(line)
|
249
|
+
next if /^[-=]+$/.match?(line) # Skip separator lines
|
250
|
+
|
251
|
+
# Parse table format: columns separated by multiple spaces
|
252
|
+
parts = line.split(/\s{2,}/)
|
253
|
+
next if parts.size < 2
|
254
|
+
|
255
|
+
name = parts[0]&.strip
|
256
|
+
status = parts[1]&.strip
|
257
|
+
description = parts[2..]&.join(" ")&.strip
|
258
|
+
|
259
|
+
next unless name && !name.empty?
|
260
|
+
|
261
|
+
servers << {
|
262
|
+
name: name,
|
263
|
+
status: status || "unknown",
|
264
|
+
description: description,
|
265
|
+
enabled: status&.downcase == "enabled" || status&.downcase == "connected"
|
266
|
+
}
|
267
|
+
end
|
268
|
+
|
269
|
+
servers
|
270
|
+
end
|
271
|
+
|
272
|
+
def get_binary_name
|
273
|
+
case @provider_name
|
274
|
+
when "claude", "anthropic"
|
275
|
+
"claude"
|
276
|
+
when "cursor"
|
277
|
+
"cursor"
|
278
|
+
when "gemini"
|
279
|
+
"gemini"
|
280
|
+
when "codex"
|
281
|
+
"codex"
|
282
|
+
when "github_copilot"
|
283
|
+
"gh"
|
284
|
+
when "opencode"
|
285
|
+
"opencode"
|
286
|
+
else
|
287
|
+
@provider_name
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def parse_help_output(help_text)
|
292
|
+
parsed = {
|
293
|
+
capabilities: {},
|
294
|
+
permission_modes: [],
|
295
|
+
mcp_support: false,
|
296
|
+
auth_method: nil,
|
297
|
+
flags: {}
|
298
|
+
}
|
299
|
+
|
300
|
+
# Check for MCP support
|
301
|
+
parsed[:mcp_support] = !!(help_text =~ /mcp|MCP|Model Context Protocol/i)
|
302
|
+
|
303
|
+
# Extract permission modes
|
304
|
+
if help_text =~ /--permission-mode\s+<mode>\s+.*?\(choices:\s*([^)]+)\)/m
|
305
|
+
modes = Regexp.last_match(1).split(",").map(&:strip).map { |m| m.gsub(/["']/, "") }
|
306
|
+
parsed[:permission_modes] = modes
|
307
|
+
end
|
308
|
+
|
309
|
+
# Check for dangerous skip permissions
|
310
|
+
parsed[:capabilities][:bypass_permissions] = !!(help_text =~ /--dangerously-skip-permissions/)
|
311
|
+
|
312
|
+
# Check for API key / subscription patterns
|
313
|
+
if /--api-key|API_KEY|setup-token|subscription/i.match?(help_text)
|
314
|
+
parsed[:auth_method] = if /setup-token|subscription/i.match?(help_text)
|
315
|
+
"subscription"
|
316
|
+
else
|
317
|
+
"api_key"
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# Extract model configuration
|
322
|
+
parsed[:capabilities][:model_selection] = !!(help_text =~ /--model\s+<model>/)
|
323
|
+
|
324
|
+
# Extract MCP configuration
|
325
|
+
parsed[:capabilities][:mcp_config] = !!(help_text =~ /--mcp-config/)
|
326
|
+
|
327
|
+
# Extract allowed/disallowed tools
|
328
|
+
parsed[:capabilities][:tool_restrictions] = !!(help_text =~ /--allowed-tools|--disallowed-tools/)
|
329
|
+
|
330
|
+
# Extract session management
|
331
|
+
parsed[:capabilities][:session_management] = !!(help_text =~ /--continue|--resume|--fork-session/)
|
332
|
+
|
333
|
+
# Extract output formats
|
334
|
+
if help_text =~ /--output-format\s+.*?\(choices:\s*([^)]+)\)/m
|
335
|
+
formats = Regexp.last_match(1).split(",").map(&:strip).map { |f| f.gsub(/["']/, "") }
|
336
|
+
parsed[:capabilities][:output_formats] = formats
|
337
|
+
end
|
338
|
+
|
339
|
+
# Extract notable flags
|
340
|
+
extract_flags(help_text, parsed[:flags])
|
341
|
+
|
342
|
+
parsed
|
343
|
+
end
|
344
|
+
|
345
|
+
def extract_flags(help_text, flags_hash)
|
346
|
+
# Extract all flags with their descriptions
|
347
|
+
help_text.scan(/^\s+(--[\w-]+(?:\s+<\w+>)?)\s+(.+?)(?=^\s+(?:--|\w|$))/m).each do |flag, desc|
|
348
|
+
flag_name = flag.split.first.gsub(/^--/, "")
|
349
|
+
flags_hash[flag_name] = {
|
350
|
+
flag: flag.strip,
|
351
|
+
description: desc.strip.gsub(/\s+/, " ")
|
352
|
+
}
|
353
|
+
end
|
354
|
+
|
355
|
+
# Also capture short flags
|
356
|
+
help_text.scan(/^\s+(-\w),\s+(--[\w-]+(?:\s+<\w+>)?)\s+(.+?)(?=^\s+(?:--|-\w|$))/m).each do |short, long, desc|
|
357
|
+
flag_name = long.split.first.gsub(/^--/, "")
|
358
|
+
flags_hash[flag_name] ||= {}
|
359
|
+
flags_hash[flag_name][:short] = short
|
360
|
+
flags_hash[flag_name][:flag] = long.strip
|
361
|
+
flags_hash[flag_name][:description] = desc.strip.gsub(/\s+/, " ")
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative "enhanced_tui"
|
4
4
|
require_relative "../../workflows/selector"
|
5
|
+
require_relative "../../workflows/guided_agent"
|
5
6
|
|
6
7
|
module Aidp
|
7
8
|
module Harness
|
@@ -10,10 +11,11 @@ module Aidp
|
|
10
11
|
class EnhancedWorkflowSelector
|
11
12
|
class WorkflowError < StandardError; end
|
12
13
|
|
13
|
-
def initialize(tui = nil)
|
14
|
+
def initialize(tui = nil, project_dir: Dir.pwd)
|
14
15
|
@tui = tui || EnhancedTUI.new
|
15
16
|
@user_input = {}
|
16
17
|
@workflow_selector = Aidp::Workflows::Selector.new
|
18
|
+
@project_dir = project_dir
|
17
19
|
end
|
18
20
|
|
19
21
|
def select_workflow(harness_mode: false, mode: :analyze)
|
@@ -28,6 +30,8 @@ module Aidp
|
|
28
30
|
|
29
31
|
def select_workflow_interactive(mode)
|
30
32
|
case mode
|
33
|
+
when :guided
|
34
|
+
select_guided_workflow
|
31
35
|
when :analyze
|
32
36
|
select_analyze_workflow_interactive
|
33
37
|
when :execute
|
@@ -243,6 +247,23 @@ module Aidp
|
|
243
247
|
"16_IMPLEMENTATION"
|
244
248
|
]
|
245
249
|
end
|
250
|
+
|
251
|
+
def select_guided_workflow
|
252
|
+
# Use the guided agent to help user select workflow
|
253
|
+
guided_agent = Aidp::Workflows::GuidedAgent.new(@project_dir, prompt: @tui.instance_variable_get(:@prompt))
|
254
|
+
result = guided_agent.select_workflow
|
255
|
+
|
256
|
+
# Store user input for later use
|
257
|
+
@user_input = result[:user_input]
|
258
|
+
|
259
|
+
# Return in the expected format
|
260
|
+
{
|
261
|
+
workflow_type: result[:workflow_type],
|
262
|
+
steps: result[:steps],
|
263
|
+
user_input: @user_input,
|
264
|
+
workflow: result[:workflow]
|
265
|
+
}
|
266
|
+
end
|
246
267
|
end
|
247
268
|
end
|
248
269
|
end
|
data/lib/aidp/version.rb
CHANGED
@@ -0,0 +1,400 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../harness/provider_manager"
|
4
|
+
require_relative "../harness/provider_factory"
|
5
|
+
require_relative "../harness/configuration"
|
6
|
+
require_relative "definitions"
|
7
|
+
require_relative "../message_display"
|
8
|
+
|
9
|
+
module Aidp
|
10
|
+
module Workflows
|
11
|
+
# Guided workflow agent that uses AI to help users select appropriate workflows
|
12
|
+
# Acts as a copilot to match user intent to AIDP capabilities
|
13
|
+
class GuidedAgent
|
14
|
+
include Aidp::MessageDisplay
|
15
|
+
|
16
|
+
class ConversationError < StandardError; end
|
17
|
+
|
18
|
+
def initialize(project_dir, prompt: nil)
|
19
|
+
@project_dir = project_dir
|
20
|
+
@prompt = prompt || TTY::Prompt.new
|
21
|
+
@configuration = Aidp::Harness::Configuration.new(project_dir)
|
22
|
+
@provider_manager = Aidp::Harness::ProviderManager.new(@configuration, prompt: @prompt)
|
23
|
+
@conversation_history = []
|
24
|
+
@user_input = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Main entry point for guided workflow selection
|
28
|
+
def select_workflow
|
29
|
+
display_message("\n🤖 Welcome to AIDP Guided Workflow!", type: :highlight)
|
30
|
+
display_message("I'll help you choose the right workflow for your needs.\n", type: :info)
|
31
|
+
|
32
|
+
# Step 1: Get user's high-level goal
|
33
|
+
user_goal = get_user_goal
|
34
|
+
|
35
|
+
# Step 2: Use AI to analyze intent and recommend workflow
|
36
|
+
recommendation = analyze_user_intent(user_goal)
|
37
|
+
|
38
|
+
# Step 3: Present recommendation and get confirmation
|
39
|
+
workflow_selection = present_recommendation(recommendation)
|
40
|
+
|
41
|
+
# Step 4: Collect any additional required information
|
42
|
+
collect_workflow_details(workflow_selection)
|
43
|
+
|
44
|
+
workflow_selection
|
45
|
+
rescue => e
|
46
|
+
raise ConversationError, "Failed to guide workflow selection: #{e.message}"
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def get_user_goal
|
52
|
+
display_message("What would you like to do?", type: :highlight)
|
53
|
+
display_message("Examples:", type: :muted)
|
54
|
+
display_message(" • Build a new feature for user authentication", type: :muted)
|
55
|
+
display_message(" • Understand how this codebase handles payments", type: :muted)
|
56
|
+
display_message(" • Improve test coverage in my API layer", type: :muted)
|
57
|
+
display_message(" • Create a quick prototype for data export\n", type: :muted)
|
58
|
+
|
59
|
+
goal = @prompt.ask("Your goal:", required: true)
|
60
|
+
@user_input[:original_goal] = goal
|
61
|
+
goal
|
62
|
+
end
|
63
|
+
|
64
|
+
def analyze_user_intent(user_goal)
|
65
|
+
display_message("\n🔍 Analyzing your request...", type: :info)
|
66
|
+
|
67
|
+
# Build the system prompt with AIDP capabilities
|
68
|
+
system_prompt = build_system_prompt
|
69
|
+
|
70
|
+
# Build the user prompt
|
71
|
+
user_prompt = build_analysis_prompt(user_goal)
|
72
|
+
|
73
|
+
# Call provider to analyze intent
|
74
|
+
response = call_provider_for_analysis(system_prompt, user_prompt)
|
75
|
+
|
76
|
+
# Parse the response
|
77
|
+
parse_recommendation(response)
|
78
|
+
end
|
79
|
+
|
80
|
+
def build_system_prompt
|
81
|
+
# Try to load from project dir first, fall back to gem's docs
|
82
|
+
capabilities_path = File.join(@project_dir, "docs", "AIDP_CAPABILITIES.md")
|
83
|
+
unless File.exist?(capabilities_path)
|
84
|
+
# Use the gem's copy
|
85
|
+
gem_root = File.expand_path("../../..", __dir__)
|
86
|
+
capabilities_path = File.join(gem_root, "docs", "AIDP_CAPABILITIES.md")
|
87
|
+
end
|
88
|
+
|
89
|
+
capabilities_doc = File.read(capabilities_path)
|
90
|
+
|
91
|
+
<<~PROMPT
|
92
|
+
You are an expert AI assistant helping users select the right AIDP workflow for their needs.
|
93
|
+
|
94
|
+
Your role:
|
95
|
+
1. Understand what the user wants to accomplish
|
96
|
+
2. Match their intent to AIDP's capabilities
|
97
|
+
3. Recommend the most appropriate workflow
|
98
|
+
4. Explain why this workflow fits their needs
|
99
|
+
5. Identify if custom steps or templates are needed
|
100
|
+
|
101
|
+
AIDP Capabilities Reference:
|
102
|
+
#{capabilities_doc}
|
103
|
+
|
104
|
+
Response Format:
|
105
|
+
Provide a JSON response with:
|
106
|
+
{
|
107
|
+
"mode": "analyze|execute|hybrid",
|
108
|
+
"workflow_key": "specific_workflow_key",
|
109
|
+
"reasoning": "brief explanation of why this fits",
|
110
|
+
"additional_steps": ["any", "custom", "steps", "if", "needed"],
|
111
|
+
"questions": ["any", "clarifying", "questions"],
|
112
|
+
"confidence": "high|medium|low"
|
113
|
+
}
|
114
|
+
|
115
|
+
Be concise to preserve the user's context window.
|
116
|
+
PROMPT
|
117
|
+
end
|
118
|
+
|
119
|
+
def build_analysis_prompt(user_goal)
|
120
|
+
<<~PROMPT
|
121
|
+
User Goal: #{user_goal}
|
122
|
+
|
123
|
+
Analyze this goal and recommend the most appropriate AIDP workflow.
|
124
|
+
Consider:
|
125
|
+
- Is this analysis or development?
|
126
|
+
- What level of rigor is needed?
|
127
|
+
- Are there any gaps in existing workflows?
|
128
|
+
|
129
|
+
Provide your recommendation in JSON format.
|
130
|
+
PROMPT
|
131
|
+
end
|
132
|
+
|
133
|
+
def call_provider_for_analysis(system_prompt, user_prompt)
|
134
|
+
# Get current provider from provider manager
|
135
|
+
provider_name = @provider_manager.current_provider
|
136
|
+
|
137
|
+
unless provider_name
|
138
|
+
raise ConversationError, "No provider configured for guided workflow"
|
139
|
+
end
|
140
|
+
|
141
|
+
# Create provider instance using ProviderFactory
|
142
|
+
provider_factory = Aidp::Harness::ProviderFactory.new(@configuration)
|
143
|
+
provider = provider_factory.create_provider(provider_name, prompt: @prompt)
|
144
|
+
|
145
|
+
unless provider
|
146
|
+
raise ConversationError, "Failed to create provider instance for #{provider_name}"
|
147
|
+
end
|
148
|
+
|
149
|
+
# Make the request - combine system and user prompts
|
150
|
+
combined_prompt = "#{system_prompt}\n\n#{user_prompt}"
|
151
|
+
|
152
|
+
result = provider.send(prompt: combined_prompt)
|
153
|
+
|
154
|
+
unless result[:status] == :success
|
155
|
+
raise ConversationError, "Provider request failed: #{result[:error]}"
|
156
|
+
end
|
157
|
+
|
158
|
+
result[:content]
|
159
|
+
end
|
160
|
+
|
161
|
+
def parse_recommendation(response_text)
|
162
|
+
# Extract JSON from response (might be wrapped in markdown code blocks)
|
163
|
+
json_match = response_text.match(/```json\s*(\{.*?\})\s*```/m) ||
|
164
|
+
response_text.match(/(\{.*\})/m)
|
165
|
+
|
166
|
+
unless json_match
|
167
|
+
raise ConversationError, "Could not parse recommendation from response"
|
168
|
+
end
|
169
|
+
|
170
|
+
JSON.parse(json_match[1], symbolize_names: true)
|
171
|
+
rescue JSON::ParserError => e
|
172
|
+
raise ConversationError, "Invalid JSON in recommendation: #{e.message}"
|
173
|
+
end
|
174
|
+
|
175
|
+
def present_recommendation(recommendation)
|
176
|
+
display_message("\n✨ Recommendation", type: :highlight)
|
177
|
+
display_message("─" * 60, type: :muted)
|
178
|
+
|
179
|
+
mode = recommendation[:mode].to_sym
|
180
|
+
workflow_key = recommendation[:workflow_key].to_sym
|
181
|
+
|
182
|
+
# Get workflow details
|
183
|
+
workflow = Definitions.get_workflow(mode, workflow_key)
|
184
|
+
|
185
|
+
unless workflow
|
186
|
+
# Handle custom workflow or error
|
187
|
+
return handle_custom_workflow(recommendation)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Display the recommendation
|
191
|
+
display_message("Mode: #{mode.to_s.capitalize}", type: :info)
|
192
|
+
display_message("Workflow: #{workflow[:name]}", type: :info)
|
193
|
+
display_message("\n#{workflow[:description]}", type: :muted)
|
194
|
+
display_message("\nReasoning: #{recommendation[:reasoning]}\n", type: :info)
|
195
|
+
|
196
|
+
# Show what's included
|
197
|
+
display_message("This workflow includes:", type: :highlight)
|
198
|
+
workflow[:details].each do |detail|
|
199
|
+
display_message(" • #{detail}", type: :info)
|
200
|
+
end
|
201
|
+
|
202
|
+
# Handle additional steps if needed
|
203
|
+
steps = workflow[:steps]
|
204
|
+
if recommendation[:additional_steps]&.any?
|
205
|
+
display_message("\nAdditional custom steps recommended:", type: :highlight)
|
206
|
+
recommendation[:additional_steps].each do |step|
|
207
|
+
display_message(" • #{step}", type: :info)
|
208
|
+
end
|
209
|
+
steps = workflow[:steps] + recommendation[:additional_steps]
|
210
|
+
end
|
211
|
+
|
212
|
+
# Ask for confirmation
|
213
|
+
display_message("")
|
214
|
+
confirmed = @prompt.yes?("Does this workflow fit your needs?")
|
215
|
+
|
216
|
+
if confirmed
|
217
|
+
{
|
218
|
+
mode: mode,
|
219
|
+
workflow_key: workflow_key,
|
220
|
+
workflow_type: workflow_key,
|
221
|
+
steps: steps,
|
222
|
+
user_input: @user_input,
|
223
|
+
workflow: workflow
|
224
|
+
}
|
225
|
+
else
|
226
|
+
# Offer alternatives
|
227
|
+
offer_alternatives(mode)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def handle_custom_workflow(recommendation)
|
232
|
+
display_message("\n💡 Custom Workflow Needed", type: :highlight)
|
233
|
+
display_message("The AI recommends creating a custom workflow:\n", type: :info)
|
234
|
+
display_message(recommendation[:reasoning], type: :muted)
|
235
|
+
|
236
|
+
if recommendation[:questions]&.any?
|
237
|
+
display_message("\nLet me gather some more information:\n", type: :highlight)
|
238
|
+
recommendation[:questions].each do |question|
|
239
|
+
answer = @prompt.ask(question)
|
240
|
+
@user_input[question.downcase.gsub(/\s+/, "_").to_sym] = answer
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Let user select from available steps
|
245
|
+
mode = recommendation[:mode].to_sym
|
246
|
+
display_message("\nLet's build a custom workflow by selecting specific steps:", type: :info)
|
247
|
+
|
248
|
+
steps = select_custom_steps(mode, recommendation[:additional_steps])
|
249
|
+
|
250
|
+
{
|
251
|
+
mode: mode,
|
252
|
+
workflow_key: :custom,
|
253
|
+
workflow_type: :custom,
|
254
|
+
steps: steps,
|
255
|
+
user_input: @user_input,
|
256
|
+
workflow: {
|
257
|
+
name: "Custom Workflow",
|
258
|
+
description: recommendation[:reasoning],
|
259
|
+
details: steps
|
260
|
+
}
|
261
|
+
}
|
262
|
+
end
|
263
|
+
|
264
|
+
def select_custom_steps(mode, suggested_steps = [])
|
265
|
+
# Get available steps for the mode
|
266
|
+
spec = case mode
|
267
|
+
when :analyze
|
268
|
+
Aidp::Analyze::Steps::SPEC
|
269
|
+
when :execute
|
270
|
+
Aidp::Execute::Steps::SPEC
|
271
|
+
when :hybrid
|
272
|
+
# Combine both
|
273
|
+
Aidp::Analyze::Steps::SPEC.merge(Aidp::Execute::Steps::SPEC)
|
274
|
+
else
|
275
|
+
{}
|
276
|
+
end
|
277
|
+
|
278
|
+
step_choices = spec.map do |step_key, step_spec|
|
279
|
+
{
|
280
|
+
name: "#{step_key} - #{step_spec["description"]}",
|
281
|
+
value: step_key
|
282
|
+
}
|
283
|
+
end
|
284
|
+
|
285
|
+
# Pre-select suggested steps
|
286
|
+
default_indices = suggested_steps.map do |step|
|
287
|
+
step_choices.index { |choice| choice[:value].to_s == step.to_s }
|
288
|
+
end.compact
|
289
|
+
|
290
|
+
selected_steps = @prompt.multi_select(
|
291
|
+
"Select steps for your custom workflow:",
|
292
|
+
step_choices,
|
293
|
+
default: default_indices,
|
294
|
+
per_page: 20
|
295
|
+
)
|
296
|
+
|
297
|
+
selected_steps.empty? ? suggested_steps : selected_steps
|
298
|
+
end
|
299
|
+
|
300
|
+
def offer_alternatives(current_mode)
|
301
|
+
display_message("\n🔄 Let's find a better fit", type: :highlight)
|
302
|
+
|
303
|
+
choices = [
|
304
|
+
{name: "Try a different #{current_mode} workflow", value: :different_workflow},
|
305
|
+
{name: "Switch to a different mode", value: :different_mode},
|
306
|
+
{name: "Build a custom workflow", value: :custom},
|
307
|
+
{name: "Start over", value: :restart}
|
308
|
+
]
|
309
|
+
|
310
|
+
choice = @prompt.select("What would you like to do?", choices)
|
311
|
+
|
312
|
+
case choice
|
313
|
+
when :different_workflow
|
314
|
+
select_manual_workflow(current_mode)
|
315
|
+
when :different_mode
|
316
|
+
# Let user pick mode manually then workflow
|
317
|
+
new_mode = @prompt.select(
|
318
|
+
"Select mode:",
|
319
|
+
{
|
320
|
+
"🔬 Analyze Mode" => :analyze,
|
321
|
+
"🏗️ Execute Mode" => :execute,
|
322
|
+
"🔀 Hybrid Mode" => :hybrid
|
323
|
+
}
|
324
|
+
)
|
325
|
+
select_manual_workflow(new_mode)
|
326
|
+
when :custom
|
327
|
+
select_custom_steps(current_mode)
|
328
|
+
when :restart
|
329
|
+
select_workflow # Recursive call to start over
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def select_manual_workflow(mode)
|
334
|
+
workflows = Definitions.workflows_for_mode(mode)
|
335
|
+
|
336
|
+
choices = workflows.map do |key, workflow|
|
337
|
+
{
|
338
|
+
name: "#{workflow[:icon]} #{workflow[:name]} - #{workflow[:description]}",
|
339
|
+
value: key
|
340
|
+
}
|
341
|
+
end
|
342
|
+
|
343
|
+
selected_key = @prompt.select("Choose a workflow:", choices, per_page: 15)
|
344
|
+
workflow = workflows[selected_key]
|
345
|
+
|
346
|
+
{
|
347
|
+
mode: mode,
|
348
|
+
workflow_key: selected_key,
|
349
|
+
workflow_type: selected_key,
|
350
|
+
steps: workflow[:steps],
|
351
|
+
user_input: @user_input,
|
352
|
+
workflow: workflow
|
353
|
+
}
|
354
|
+
end
|
355
|
+
|
356
|
+
def collect_workflow_details(workflow_selection)
|
357
|
+
# Collect additional information based on mode
|
358
|
+
case workflow_selection[:mode]
|
359
|
+
when :execute
|
360
|
+
collect_execute_details
|
361
|
+
when :analyze
|
362
|
+
# Analyze mode typically doesn't need much user input
|
363
|
+
@user_input[:analysis_goal] = @user_input[:original_goal]
|
364
|
+
when :hybrid
|
365
|
+
collect_execute_details # Hybrid often needs project details
|
366
|
+
end
|
367
|
+
|
368
|
+
# Update the workflow selection with collected input
|
369
|
+
workflow_selection[:user_input] = @user_input
|
370
|
+
end
|
371
|
+
|
372
|
+
def collect_execute_details
|
373
|
+
return if @user_input[:project_description] # Already collected
|
374
|
+
|
375
|
+
display_message("\n📝 Project Information", type: :highlight)
|
376
|
+
display_message("Let me gather some details for the PRD:\n", type: :info)
|
377
|
+
|
378
|
+
@user_input[:project_description] = @prompt.ask(
|
379
|
+
"Describe what you're building (can reference your original goal):",
|
380
|
+
default: @user_input[:original_goal]
|
381
|
+
)
|
382
|
+
|
383
|
+
@user_input[:tech_stack] = @prompt.ask(
|
384
|
+
"Tech stack (e.g., Ruby/Rails, Node.js, Python)? [optional]",
|
385
|
+
required: false
|
386
|
+
)
|
387
|
+
|
388
|
+
@user_input[:target_users] = @prompt.ask(
|
389
|
+
"Who will use this? [optional]",
|
390
|
+
required: false
|
391
|
+
)
|
392
|
+
|
393
|
+
@user_input[:success_criteria] = @prompt.ask(
|
394
|
+
"How will you measure success? [optional]",
|
395
|
+
required: false
|
396
|
+
)
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aidp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.12.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bart Agapinan
|
@@ -250,6 +250,7 @@ files:
|
|
250
250
|
- lib/aidp/cli/checkpoint_command.rb
|
251
251
|
- lib/aidp/cli/first_run_wizard.rb
|
252
252
|
- lib/aidp/cli/jobs_command.rb
|
253
|
+
- lib/aidp/cli/mcp_dashboard.rb
|
253
254
|
- lib/aidp/cli/terminal_io.rb
|
254
255
|
- lib/aidp/config.rb
|
255
256
|
- lib/aidp/core_ext/class_attribute.rb
|
@@ -274,6 +275,7 @@ files:
|
|
274
275
|
- lib/aidp/harness/error_handler.rb
|
275
276
|
- lib/aidp/harness/provider_config.rb
|
276
277
|
- lib/aidp/harness/provider_factory.rb
|
278
|
+
- lib/aidp/harness/provider_info.rb
|
277
279
|
- lib/aidp/harness/provider_manager.rb
|
278
280
|
- lib/aidp/harness/provider_type_checker.rb
|
279
281
|
- lib/aidp/harness/runner.rb
|
@@ -324,6 +326,7 @@ files:
|
|
324
326
|
- lib/aidp/util.rb
|
325
327
|
- lib/aidp/version.rb
|
326
328
|
- lib/aidp/workflows/definitions.rb
|
329
|
+
- lib/aidp/workflows/guided_agent.rb
|
327
330
|
- lib/aidp/workflows/selector.rb
|
328
331
|
- templates/COMMON/AGENT_BASE.md
|
329
332
|
- templates/COMMON/CONVENTIONS.md
|