aidp 0.48.0 ā 0.49.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/devcontainer_commands.rb +33 -1
- data/lib/aidp/harness/configuration.rb +12 -7
- data/lib/aidp/setup/wizard.rb +625 -68
- data/lib/aidp/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c5a91840d333d7b33122434b8cc2efcad0149becf640e3932d5d1ea6fd799f1
|
|
4
|
+
data.tar.gz: 0604c7d3798c6dc90aa1ccf543916e73e63fabdd4bcc58ae1a0864cbb0277a0a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8dbaefb32e45cd9687524dea4cd72dc4c38eb13bb766f7e5b476e67481338ad76e6b6c72c0a80cd1e479487701266036c8d1b8cbab74ae288ff44ccaa5d891ac
|
|
7
|
+
data.tar.gz: b2de14911ec9b019235a4966d74208a61d5e2970a9fd710b31d0b58d2ec6f741bbdd6eb0a8314a6fbc7916bbd3c24c6352efa577080d356001dbd583b4aed6f3
|
|
@@ -280,13 +280,45 @@ module Aidp
|
|
|
280
280
|
# Extract wizard-compatible config including custom_ports
|
|
281
281
|
{
|
|
282
282
|
providers: aidp_config.dig("providers")&.keys,
|
|
283
|
-
test_framework: aidp_config
|
|
283
|
+
test_framework: extract_test_framework_for_generation(aidp_config),
|
|
284
284
|
linters: aidp_config.dig("work_loop", "linting", "tools"),
|
|
285
285
|
watch_mode: aidp_config.dig("work_loop", "watch", "enabled"),
|
|
286
286
|
custom_ports: aidp_config.dig("devcontainer", "custom_ports")
|
|
287
287
|
}.compact
|
|
288
288
|
end
|
|
289
289
|
|
|
290
|
+
def extract_test_framework_for_generation(aidp_config)
|
|
291
|
+
framework_from_work_loop_commands(aidp_config) ||
|
|
292
|
+
aidp_config.dig("work_loop", "test_commands", 0, "framework")
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def framework_from_work_loop_commands(aidp_config)
|
|
296
|
+
candidates = Array(aidp_config.dig("work_loop", "commands"))
|
|
297
|
+
command_text = tagged_test_command(candidates) || first_recognized_string_command(candidates)
|
|
298
|
+
framework = Aidp::ToolingDetector.framework_from_command(command_text)
|
|
299
|
+
return if framework == :unknown
|
|
300
|
+
|
|
301
|
+
framework.to_s
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# `work_loop.commands` entries may be hashes with a category, or bare
|
|
305
|
+
# command strings (no category available). Prefer an explicitly tagged
|
|
306
|
+
# test command; when only bare strings are present, fall back to the
|
|
307
|
+
# first one whose command text matches a known test framework.
|
|
308
|
+
def tagged_test_command(candidates)
|
|
309
|
+
candidates.find do |candidate|
|
|
310
|
+
candidate.is_a?(Hash) &&
|
|
311
|
+
# `generate_yaml` writes category as a symbol (e.g. `:test`); YAML
|
|
312
|
+
# preserves symbols on load, so coerce to string before comparing.
|
|
313
|
+
candidate["category"].to_s == "test"
|
|
314
|
+
end&.dig("command")
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def first_recognized_string_command(candidates)
|
|
318
|
+
candidates.select { |candidate| candidate.is_a?(String) }
|
|
319
|
+
.find { |command| Aidp::ToolingDetector.framework_from_command(command) != :unknown }
|
|
320
|
+
end
|
|
321
|
+
|
|
290
322
|
def build_config_from_aidp_yml(devcontainer_config)
|
|
291
323
|
config = {}
|
|
292
324
|
|
|
@@ -909,23 +909,28 @@ module Aidp
|
|
|
909
909
|
# Load commands from configuration, supporting both new generic format
|
|
910
910
|
# and legacy category-specific format for backwards compatibility
|
|
911
911
|
def load_commands
|
|
912
|
-
commands = []
|
|
913
|
-
|
|
914
912
|
# Load from new generic commands array if present
|
|
915
913
|
raw_commands = work_loop_config[:commands] || []
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
commands.concat(load_legacy_commands)
|
|
914
|
+
generic_commands = normalize_generic_commands(raw_commands)
|
|
915
|
+
legacy_commands = load_legacy_commands
|
|
916
|
+
commands = deduplicated_commands(generic_commands, legacy_commands)
|
|
920
917
|
|
|
921
918
|
Aidp.log_debug("configuration", "loaded_commands",
|
|
922
919
|
total: commands.size,
|
|
923
920
|
from_generic: raw_commands.size,
|
|
924
|
-
from_legacy:
|
|
921
|
+
from_legacy: legacy_commands.size)
|
|
925
922
|
|
|
926
923
|
commands
|
|
927
924
|
end
|
|
928
925
|
|
|
926
|
+
def deduplicated_commands(*command_sets)
|
|
927
|
+
command_sets.flatten.uniq { |command| deduplication_key(command) }
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
def deduplication_key(command)
|
|
931
|
+
command.slice(:command, :required, :run_after, :category, :timeout_seconds)
|
|
932
|
+
end
|
|
933
|
+
|
|
929
934
|
# Normalize generic command format
|
|
930
935
|
# @param commands [Array] Raw command configurations
|
|
931
936
|
# @return [Array<Hash>] Normalized command configs
|
data/lib/aidp/setup/wizard.rb
CHANGED
|
@@ -9,6 +9,7 @@ require "json"
|
|
|
9
9
|
require "ostruct"
|
|
10
10
|
require_relative "in_memory_config_adapter"
|
|
11
11
|
require_relative "in_memory_config_manager"
|
|
12
|
+
require_relative "../tooling_detector"
|
|
12
13
|
require_relative "../security"
|
|
13
14
|
|
|
14
15
|
module Aidp
|
|
@@ -34,6 +35,20 @@ module Aidp
|
|
|
34
35
|
balanced: 10,
|
|
35
36
|
quick_sketch: 3
|
|
36
37
|
}.freeze
|
|
38
|
+
PHASE_TWO_SUGGESTION_SCHEMA = {
|
|
39
|
+
"commands" => [
|
|
40
|
+
{
|
|
41
|
+
"name" => "identifier",
|
|
42
|
+
"command" => "shell command",
|
|
43
|
+
"category" => "test|lint|formatter|build|documentation|custom",
|
|
44
|
+
"run_after" => "each_unit|full_loop|on_completion",
|
|
45
|
+
"required" => true
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"watch_patterns" => ["spec/**/*_spec.rb"],
|
|
49
|
+
"guard_include_patterns" => ["lib/**/*"],
|
|
50
|
+
"guard_exclude_patterns" => ["node_modules/**"]
|
|
51
|
+
}.freeze
|
|
37
52
|
|
|
38
53
|
attr_reader :project_dir, :prompt, :dry_run
|
|
39
54
|
# Expose state for testability
|
|
@@ -46,47 +61,50 @@ module Aidp
|
|
|
46
61
|
@warnings = []
|
|
47
62
|
@existing_config = load_existing_config
|
|
48
63
|
@config = deep_symbolize(@existing_config)
|
|
64
|
+
@original_config_content = File.exist?(config_path) ? File.read(config_path) : nil
|
|
49
65
|
@saved = false
|
|
66
|
+
@phase_one_persisted = false
|
|
50
67
|
end
|
|
51
68
|
|
|
52
69
|
def run
|
|
70
|
+
final_save_succeeded = false
|
|
53
71
|
display_welcome
|
|
54
72
|
# Normalize any legacy tier/model_family entries before prompting
|
|
55
73
|
normalize_existing_model_families!
|
|
56
74
|
normalize_existing_thinking_tiers!
|
|
57
75
|
return @saved if skip_wizard?
|
|
58
76
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
configure_work_loop
|
|
63
|
-
configure_prd_generation
|
|
64
|
-
configure_branching
|
|
65
|
-
configure_artifacts
|
|
66
|
-
configure_nfrs
|
|
67
|
-
configure_logging
|
|
68
|
-
configure_auto_update
|
|
69
|
-
configure_modes
|
|
70
|
-
configure_devcontainer
|
|
77
|
+
run_phase_one
|
|
78
|
+
persist_phase_one_config unless dry_run
|
|
79
|
+
run_phase_two
|
|
71
80
|
|
|
72
81
|
yaml_content = generate_yaml
|
|
73
82
|
display_preview(yaml_content)
|
|
74
|
-
display_diff(yaml_content) if @
|
|
83
|
+
display_diff(yaml_content) if @original_config_content
|
|
75
84
|
|
|
76
85
|
return true if dry_run_mode?(yaml_content)
|
|
77
86
|
|
|
78
87
|
if prompt.yes?("Save this configuration?", default: true)
|
|
79
88
|
save_config(yaml_content)
|
|
89
|
+
final_save_succeeded = true
|
|
80
90
|
prompt.ok("ā
Configuration saved to #{relative_config_path}")
|
|
81
91
|
show_next_steps
|
|
82
92
|
display_warnings
|
|
83
93
|
@saved = true
|
|
84
94
|
else
|
|
95
|
+
rollback_phase_one_config
|
|
85
96
|
prompt.warn("Configuration not saved")
|
|
86
97
|
display_warnings
|
|
87
98
|
end
|
|
88
99
|
|
|
89
100
|
@saved
|
|
101
|
+
ensure
|
|
102
|
+
# Only roll back when the phase-one snapshot has not been replaced
|
|
103
|
+
# by a successful final write. save_config clears @phase_one_persisted
|
|
104
|
+
# once the YAML is on disk, so a post-write artifact failure leaves
|
|
105
|
+
# the user's chosen config intact instead of being overwritten by
|
|
106
|
+
# the original.
|
|
107
|
+
rollback_phase_one_config if @phase_one_persisted && !final_save_succeeded
|
|
90
108
|
end
|
|
91
109
|
|
|
92
110
|
def saved?
|
|
@@ -117,6 +135,25 @@ module Aidp
|
|
|
117
135
|
# -------------------------------------------
|
|
118
136
|
# Provider configuration
|
|
119
137
|
# -------------------------------------------
|
|
138
|
+
def run_phase_one
|
|
139
|
+
configure_providers
|
|
140
|
+
configure_harness_settings
|
|
141
|
+
configure_work_loop_phase_one
|
|
142
|
+
configure_prd_generation
|
|
143
|
+
configure_branching
|
|
144
|
+
configure_artifacts
|
|
145
|
+
configure_nfrs
|
|
146
|
+
configure_logging
|
|
147
|
+
configure_auto_update
|
|
148
|
+
configure_modes
|
|
149
|
+
configure_devcontainer
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def run_phase_two
|
|
153
|
+
configure_thinking_tiers
|
|
154
|
+
configure_work_loop_phase_two
|
|
155
|
+
end
|
|
156
|
+
|
|
120
157
|
def discover_available_providers
|
|
121
158
|
require "agent_harness"
|
|
122
159
|
|
|
@@ -494,16 +531,24 @@ module Aidp
|
|
|
494
531
|
prompt.say("\nāļø Work loop configuration")
|
|
495
532
|
prompt.say("-" * 40)
|
|
496
533
|
|
|
534
|
+
configure_work_loop_phase_one
|
|
535
|
+
configure_work_loop_phase_two
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def configure_work_loop_phase_one
|
|
497
539
|
configure_work_loop_limits
|
|
498
|
-
configure_output_filtering
|
|
499
|
-
configure_commands
|
|
500
|
-
configure_watch_patterns
|
|
501
|
-
configure_guards
|
|
502
540
|
configure_coverage
|
|
503
541
|
configure_interactive_testing
|
|
504
542
|
configure_vcs_behavior
|
|
505
543
|
end
|
|
506
544
|
|
|
545
|
+
def configure_work_loop_phase_two
|
|
546
|
+
configure_commands
|
|
547
|
+
configure_watch_patterns
|
|
548
|
+
configure_guards
|
|
549
|
+
configure_output_filtering
|
|
550
|
+
end
|
|
551
|
+
|
|
507
552
|
def configure_prd_generation
|
|
508
553
|
prompt.say("\nš§ PRD generation style")
|
|
509
554
|
prompt.say("-" * 40)
|
|
@@ -532,7 +577,7 @@ module Aidp
|
|
|
532
577
|
prompt.say(" Commands run automatically during work loops to validate changes")
|
|
533
578
|
prompt.say(" Commands can run: after each unit, at full loop end, or on completion")
|
|
534
579
|
|
|
535
|
-
existing_commands =
|
|
580
|
+
existing_commands = effective_work_loop_commands
|
|
536
581
|
|
|
537
582
|
# If user has existing commands, offer to edit or start fresh
|
|
538
583
|
if existing_commands.any?
|
|
@@ -545,7 +590,10 @@ module Aidp
|
|
|
545
590
|
end
|
|
546
591
|
|
|
547
592
|
case action
|
|
548
|
-
when :
|
|
593
|
+
when :keep
|
|
594
|
+
save_work_loop_commands(existing_commands)
|
|
595
|
+
return
|
|
596
|
+
when :skip, nil
|
|
549
597
|
return
|
|
550
598
|
when :replace
|
|
551
599
|
existing_commands = []
|
|
@@ -557,6 +605,7 @@ module Aidp
|
|
|
557
605
|
end
|
|
558
606
|
else
|
|
559
607
|
return unless prompt.yes?("Configure deterministic commands?", default: true)
|
|
608
|
+
existing_commands = phase_two_suggestions[:commands]
|
|
560
609
|
end
|
|
561
610
|
|
|
562
611
|
commands = existing_commands.dup
|
|
@@ -589,7 +638,7 @@ module Aidp
|
|
|
589
638
|
end
|
|
590
639
|
end
|
|
591
640
|
|
|
592
|
-
|
|
641
|
+
save_work_loop_commands(commands)
|
|
593
642
|
prompt.say("ā
Configured #{commands.size} command(s)")
|
|
594
643
|
end
|
|
595
644
|
|
|
@@ -892,56 +941,27 @@ module Aidp
|
|
|
892
941
|
end
|
|
893
942
|
|
|
894
943
|
def collect_commands_for_filtering
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
test_config = get(%i[work_loop test]) || {}
|
|
899
|
-
if test_config[:unit] && !test_config[:unit].start_with?("echo")
|
|
900
|
-
commands << {
|
|
901
|
-
key: "unit_test",
|
|
902
|
-
name: "Unit Tests",
|
|
903
|
-
command: test_config[:unit],
|
|
904
|
-
type: :test
|
|
905
|
-
}
|
|
906
|
-
end
|
|
907
|
-
if test_config[:integration] && !test_config[:integration].to_s.empty? && !test_config[:integration].start_with?("echo")
|
|
908
|
-
commands << {
|
|
909
|
-
key: "integration_test",
|
|
910
|
-
name: "Integration Tests",
|
|
911
|
-
command: test_config[:integration],
|
|
912
|
-
type: :test
|
|
913
|
-
}
|
|
914
|
-
end
|
|
915
|
-
if test_config[:e2e] && !test_config[:e2e].to_s.empty? && !test_config[:e2e].start_with?("echo")
|
|
916
|
-
commands << {
|
|
917
|
-
key: "e2e_test",
|
|
918
|
-
name: "E2E Tests",
|
|
919
|
-
command: test_config[:e2e],
|
|
920
|
-
type: :test
|
|
921
|
-
}
|
|
922
|
-
end
|
|
944
|
+
effective_work_loop_commands.filter_map do |command|
|
|
945
|
+
next unless %i[test lint].include?(command[:category].to_sym)
|
|
946
|
+
next if command[:command].to_s.start_with?("echo")
|
|
923
947
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
name: "Linter",
|
|
930
|
-
command: lint_config[:command],
|
|
931
|
-
type: :lint
|
|
948
|
+
{
|
|
949
|
+
key: command[:name].to_s.gsub(/[^a-z0-9]+/i, "_").downcase,
|
|
950
|
+
name: command[:name].to_s.tr("_", " ").split.map(&:capitalize).join(" "),
|
|
951
|
+
command: command[:command],
|
|
952
|
+
type: command[:category].to_sym
|
|
932
953
|
}
|
|
933
954
|
end
|
|
934
|
-
|
|
935
|
-
commands
|
|
936
955
|
end
|
|
937
956
|
|
|
938
957
|
def create_filter_factory
|
|
939
|
-
#
|
|
940
|
-
#
|
|
958
|
+
# Always use in-memory config objects here. Phase-two edits
|
|
959
|
+
# (e.g. configure_thinking_tiers) live only in @config until the
|
|
960
|
+
# final save; switching to disk-backed Configuration would miss
|
|
961
|
+
# those user choices and pick a stale model via ThinkingDepthManager.
|
|
941
962
|
config_adapter = build_in_memory_config_adapter
|
|
942
963
|
config_manager = build_in_memory_config_manager
|
|
943
964
|
|
|
944
|
-
# Create provider factory with in-memory config manager
|
|
945
965
|
provider_factory = Aidp::Harness::ProviderFactory.new(config_manager)
|
|
946
966
|
|
|
947
967
|
Aidp.log_debug("setup_wizard", "creating_filter_factory",
|
|
@@ -1035,18 +1055,19 @@ module Aidp
|
|
|
1035
1055
|
|
|
1036
1056
|
def configure_watch_patterns
|
|
1037
1057
|
existing = get(%i[work_loop test watch]) || {}
|
|
1038
|
-
default_patterns = detect_watch_patterns
|
|
1058
|
+
default_patterns = existing[:patterns] || detect_watch_patterns
|
|
1039
1059
|
|
|
1040
|
-
watch_patterns = ask_list("Test watch patterns (comma-separated)",
|
|
1060
|
+
watch_patterns = ask_list("Test watch patterns (comma-separated)", default_patterns)
|
|
1041
1061
|
set(%i[work_loop test watch], {patterns: watch_patterns}) if watch_patterns.any?
|
|
1042
1062
|
end
|
|
1043
1063
|
|
|
1044
1064
|
def configure_guards
|
|
1045
1065
|
existing = get(%i[work_loop guards]) || {}
|
|
1046
1066
|
|
|
1047
|
-
include_patterns = ask_list("Guard include patterns",
|
|
1067
|
+
include_patterns = ask_list("Guard include patterns",
|
|
1068
|
+
existing[:include] || detect_source_patterns)
|
|
1048
1069
|
exclude_patterns = ask_list("Guard exclude patterns",
|
|
1049
|
-
existing[:exclude] ||
|
|
1070
|
+
existing[:exclude] || detect_guard_exclude_patterns)
|
|
1050
1071
|
max_lines = ask_with_default("Max lines changed per commit",
|
|
1051
1072
|
(existing[:max_lines_changed_per_commit] || 300).to_s) do |value|
|
|
1052
1073
|
value.to_i
|
|
@@ -1976,7 +1997,7 @@ module Aidp
|
|
|
1976
1997
|
end
|
|
1977
1998
|
|
|
1978
1999
|
def display_diff(yaml_content)
|
|
1979
|
-
existing_yaml = File.read(config_path)
|
|
2000
|
+
existing_yaml = @original_config_content || File.read(config_path)
|
|
1980
2001
|
diff_lines = line_diff(existing_yaml, yaml_content)
|
|
1981
2002
|
return if diff_lines.empty?
|
|
1982
2003
|
|
|
@@ -2000,20 +2021,47 @@ module Aidp
|
|
|
2000
2021
|
return false unless dry_run
|
|
2001
2022
|
|
|
2002
2023
|
prompt.ok("Dry run mode active ā configuration was NOT written.")
|
|
2024
|
+
rollback_phase_one_config
|
|
2003
2025
|
display_warnings
|
|
2004
2026
|
@saved = false
|
|
2005
2027
|
true
|
|
2006
2028
|
end
|
|
2007
2029
|
|
|
2008
|
-
def save_config(yaml_content)
|
|
2030
|
+
def save_config(yaml_content, generate_artifacts: true)
|
|
2009
2031
|
Aidp::ConfigPaths.ensure_config_dir(project_dir)
|
|
2010
2032
|
File.write(config_path, yaml_content)
|
|
2033
|
+
# Once the YAML is on disk, treat the final save as committed even if
|
|
2034
|
+
# artifact generation fails afterwards. Clearing @phase_one_persisted
|
|
2035
|
+
# here keeps a post-write raise from triggering the run-level rollback
|
|
2036
|
+
# in #run's ensure block, which would otherwise overwrite the user's
|
|
2037
|
+
# just-saved configuration with the original phase-one snapshot.
|
|
2038
|
+
@phase_one_persisted = false
|
|
2039
|
+
return unless generate_artifacts
|
|
2040
|
+
|
|
2011
2041
|
generate_mcp_risk_profile
|
|
2012
2042
|
|
|
2013
2043
|
# Generate devcontainer if managed
|
|
2014
2044
|
generate_devcontainer_file
|
|
2015
2045
|
end
|
|
2016
2046
|
|
|
2047
|
+
def persist_phase_one_config
|
|
2048
|
+
save_config(generate_yaml, generate_artifacts: false)
|
|
2049
|
+
reload_config_from_disk
|
|
2050
|
+
@phase_one_persisted = true
|
|
2051
|
+
end
|
|
2052
|
+
|
|
2053
|
+
def rollback_phase_one_config
|
|
2054
|
+
return unless @phase_one_persisted
|
|
2055
|
+
|
|
2056
|
+
if @original_config_content
|
|
2057
|
+
File.write(config_path, @original_config_content)
|
|
2058
|
+
elsif File.exist?(config_path)
|
|
2059
|
+
File.delete(config_path)
|
|
2060
|
+
end
|
|
2061
|
+
|
|
2062
|
+
@phase_one_persisted = false
|
|
2063
|
+
end
|
|
2064
|
+
|
|
2017
2065
|
def generate_mcp_risk_profile
|
|
2018
2066
|
classifier = Aidp::Security::McpToolRiskClassifier.new(
|
|
2019
2067
|
build_in_memory_config_adapter,
|
|
@@ -2107,6 +2155,9 @@ module Aidp
|
|
|
2107
2155
|
end
|
|
2108
2156
|
|
|
2109
2157
|
def detect_unit_test_command
|
|
2158
|
+
command = detect_tooling.test_commands.first
|
|
2159
|
+
return command if command
|
|
2160
|
+
|
|
2110
2161
|
return "bundle exec rspec" if project_file?("Gemfile") && Dir.exist?(File.join(project_dir, "spec"))
|
|
2111
2162
|
return "npm test" if project_file?("package.json")
|
|
2112
2163
|
return "pytest" if project_file?("pytest.ini") || Dir.exist?(File.join(project_dir, "tests"))
|
|
@@ -2115,6 +2166,9 @@ module Aidp
|
|
|
2115
2166
|
end
|
|
2116
2167
|
|
|
2117
2168
|
def detect_lint_command
|
|
2169
|
+
command = detect_tooling.lint_commands.first
|
|
2170
|
+
return command if command
|
|
2171
|
+
|
|
2118
2172
|
return "bundle exec rubocop" if project_file?(".rubocop.yml")
|
|
2119
2173
|
return "npm run lint" if project_file?("package.json")
|
|
2120
2174
|
return "ruff check ." if project_file?("pyproject.toml")
|
|
@@ -2123,6 +2177,9 @@ module Aidp
|
|
|
2123
2177
|
end
|
|
2124
2178
|
|
|
2125
2179
|
def detect_format_command
|
|
2180
|
+
command = detect_tooling.formatter_commands.first
|
|
2181
|
+
return command if command
|
|
2182
|
+
|
|
2126
2183
|
return "bundle exec rubocop -A" if project_file?(".rubocop.yml")
|
|
2127
2184
|
return "npm run format" if project_file?("package.json")
|
|
2128
2185
|
return "ruff format ." if project_file?("pyproject.toml")
|
|
@@ -2131,6 +2188,9 @@ module Aidp
|
|
|
2131
2188
|
end
|
|
2132
2189
|
|
|
2133
2190
|
def detect_watch_patterns
|
|
2191
|
+
suggestions = phase_two_suggestions[:watch_patterns]
|
|
2192
|
+
return suggestions if suggestions.any?
|
|
2193
|
+
|
|
2134
2194
|
if project_file?("Gemfile")
|
|
2135
2195
|
["spec/**/*_spec.rb", "lib/**/*.rb"]
|
|
2136
2196
|
elsif project_file?("package.json")
|
|
@@ -2141,6 +2201,9 @@ module Aidp
|
|
|
2141
2201
|
end
|
|
2142
2202
|
|
|
2143
2203
|
def detect_source_patterns
|
|
2204
|
+
suggestions = phase_two_suggestions[:guard_include_patterns]
|
|
2205
|
+
return suggestions if suggestions.any?
|
|
2206
|
+
|
|
2144
2207
|
if project_file?("Gemfile")
|
|
2145
2208
|
%w[app/**/* lib/**/*]
|
|
2146
2209
|
elsif project_file?("package.json")
|
|
@@ -2152,6 +2215,13 @@ module Aidp
|
|
|
2152
2215
|
end
|
|
2153
2216
|
end
|
|
2154
2217
|
|
|
2218
|
+
def detect_guard_exclude_patterns
|
|
2219
|
+
suggestions = phase_two_suggestions[:guard_exclude_patterns]
|
|
2220
|
+
return suggestions if suggestions.any?
|
|
2221
|
+
|
|
2222
|
+
["node_modules/**", "dist/**", "build/**"]
|
|
2223
|
+
end
|
|
2224
|
+
|
|
2155
2225
|
def detect_coverage_command(tool)
|
|
2156
2226
|
case tool
|
|
2157
2227
|
when "simplecov"
|
|
@@ -2644,6 +2714,11 @@ module Aidp
|
|
|
2644
2714
|
Aidp::ConfigPaths.config_file(project_dir)
|
|
2645
2715
|
end
|
|
2646
2716
|
|
|
2717
|
+
def reload_config_from_disk
|
|
2718
|
+
@existing_config = load_existing_config
|
|
2719
|
+
@config = deep_symbolize(@existing_config)
|
|
2720
|
+
end
|
|
2721
|
+
|
|
2647
2722
|
def relative_config_path
|
|
2648
2723
|
config_path.sub("#{project_dir}/", "")
|
|
2649
2724
|
end
|
|
@@ -2673,6 +2748,35 @@ module Aidp
|
|
|
2673
2748
|
parent.delete(path.last.to_sym)
|
|
2674
2749
|
end
|
|
2675
2750
|
|
|
2751
|
+
def save_work_loop_commands(commands)
|
|
2752
|
+
set(%i[work_loop commands], commands)
|
|
2753
|
+
delete_legacy_work_loop_command_keys
|
|
2754
|
+
delete_structured_legacy_work_loop_command_keys
|
|
2755
|
+
end
|
|
2756
|
+
|
|
2757
|
+
def delete_legacy_work_loop_command_keys
|
|
2758
|
+
%i[test_commands lint_commands formatter_commands build_commands documentation_commands].each do |key|
|
|
2759
|
+
delete_path([:work_loop, key])
|
|
2760
|
+
end
|
|
2761
|
+
end
|
|
2762
|
+
|
|
2763
|
+
def delete_structured_legacy_work_loop_command_keys
|
|
2764
|
+
delete_structured_legacy_command_fields(:test, %i[unit integration e2e timeout_seconds])
|
|
2765
|
+
delete_structured_legacy_command_fields(:lint, %i[command format])
|
|
2766
|
+
end
|
|
2767
|
+
|
|
2768
|
+
def delete_structured_legacy_command_fields(section, fields)
|
|
2769
|
+
config = get([:work_loop, section])
|
|
2770
|
+
return unless config.is_a?(Hash)
|
|
2771
|
+
|
|
2772
|
+
fields.each do |field|
|
|
2773
|
+
config.delete(field)
|
|
2774
|
+
config.delete(field.to_s)
|
|
2775
|
+
end
|
|
2776
|
+
|
|
2777
|
+
delete_path([:work_loop, section]) if config.empty?
|
|
2778
|
+
end
|
|
2779
|
+
|
|
2676
2780
|
def deep_symbolize(object)
|
|
2677
2781
|
case object
|
|
2678
2782
|
when Hash
|
|
@@ -2771,6 +2875,421 @@ module Aidp
|
|
|
2771
2875
|
%w[mini standard thinking pro max]
|
|
2772
2876
|
end
|
|
2773
2877
|
|
|
2878
|
+
def detect_tooling
|
|
2879
|
+
@detect_tooling ||= Aidp::ToolingDetector.detect(project_dir)
|
|
2880
|
+
end
|
|
2881
|
+
|
|
2882
|
+
def phase_two_suggestions
|
|
2883
|
+
@phase_two_suggestions ||= build_phase_two_suggestions
|
|
2884
|
+
end
|
|
2885
|
+
|
|
2886
|
+
def build_phase_two_suggestions
|
|
2887
|
+
defaults = {
|
|
2888
|
+
commands: default_phase_two_commands,
|
|
2889
|
+
watch_patterns: [],
|
|
2890
|
+
guard_include_patterns: [],
|
|
2891
|
+
guard_exclude_patterns: ["node_modules/**", "dist/**", "build/**"]
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
tooling_detected = detect_tooling.test_commands.any? ||
|
|
2895
|
+
detect_tooling.lint_commands.any? ||
|
|
2896
|
+
detect_tooling.formatter_commands.any?
|
|
2897
|
+
|
|
2898
|
+
generated = if tooling_detected
|
|
2899
|
+
generate_phase_two_suggestions_with_ai
|
|
2900
|
+
else
|
|
2901
|
+
generate_phase_two_suggestions_with_ai(collect_phase_two_tooling_context)
|
|
2902
|
+
end
|
|
2903
|
+
|
|
2904
|
+
defaults.merge(generated) { |_key, fallback, suggested| normalize_phase_two_value(suggested, fallback) }
|
|
2905
|
+
rescue => e
|
|
2906
|
+
Aidp.log_warn("setup_wizard", "phase_two_suggestions_failed", error: e.message)
|
|
2907
|
+
defaults
|
|
2908
|
+
end
|
|
2909
|
+
|
|
2910
|
+
def collect_phase_two_tooling_context
|
|
2911
|
+
@phase_two_tooling_context ||= begin
|
|
2912
|
+
stack = select_phase_two_stack
|
|
2913
|
+
|
|
2914
|
+
{
|
|
2915
|
+
stack: stack,
|
|
2916
|
+
test_tool: ask_with_default("Primary test tool or command (optional)"),
|
|
2917
|
+
lint_tool: ask_with_default("Primary lint tool or command (optional)"),
|
|
2918
|
+
formatter_tool: ask_with_default("Primary formatter tool or command (optional)")
|
|
2919
|
+
}.compact
|
|
2920
|
+
end
|
|
2921
|
+
end
|
|
2922
|
+
|
|
2923
|
+
def select_phase_two_stack
|
|
2924
|
+
default_stack = detect_stack
|
|
2925
|
+
default_label = phase_two_stack_options.find { |label, value| value == default_stack }&.first
|
|
2926
|
+
|
|
2927
|
+
selected_stack = prompt.select(
|
|
2928
|
+
"Tooling detection came up empty. Which stack best matches this repo?",
|
|
2929
|
+
default: default_label
|
|
2930
|
+
) do |menu|
|
|
2931
|
+
phase_two_stack_options.each do |label, value|
|
|
2932
|
+
menu.choice label, value
|
|
2933
|
+
end
|
|
2934
|
+
end
|
|
2935
|
+
|
|
2936
|
+
return ask_with_default("Name this stack", "custom") if selected_stack == :other
|
|
2937
|
+
|
|
2938
|
+
selected_stack.to_s
|
|
2939
|
+
end
|
|
2940
|
+
|
|
2941
|
+
def phase_two_stack_options
|
|
2942
|
+
[
|
|
2943
|
+
["Rails / Ruby", :rails],
|
|
2944
|
+
["Node / JavaScript", :node],
|
|
2945
|
+
["Python", :python],
|
|
2946
|
+
["Other / Custom", :other]
|
|
2947
|
+
]
|
|
2948
|
+
end
|
|
2949
|
+
|
|
2950
|
+
def default_phase_two_commands
|
|
2951
|
+
commands = []
|
|
2952
|
+
|
|
2953
|
+
detect_tooling.test_commands.each_with_index do |command, index|
|
|
2954
|
+
commands << {
|
|
2955
|
+
name: index.zero? ? "test" : "test_#{index + 1}",
|
|
2956
|
+
command: command,
|
|
2957
|
+
category: :test,
|
|
2958
|
+
run_after: :each_unit,
|
|
2959
|
+
required: true,
|
|
2960
|
+
timeout_seconds: 1800
|
|
2961
|
+
}
|
|
2962
|
+
end
|
|
2963
|
+
|
|
2964
|
+
detect_tooling.lint_commands.each_with_index do |command, index|
|
|
2965
|
+
commands << {
|
|
2966
|
+
name: index.zero? ? "lint" : "lint_#{index + 1}",
|
|
2967
|
+
command: command,
|
|
2968
|
+
category: :lint,
|
|
2969
|
+
run_after: :each_unit,
|
|
2970
|
+
required: true,
|
|
2971
|
+
timeout_seconds: 300
|
|
2972
|
+
}
|
|
2973
|
+
end
|
|
2974
|
+
|
|
2975
|
+
detect_tooling.formatter_commands.each_with_index do |command, index|
|
|
2976
|
+
commands << {
|
|
2977
|
+
name: index.zero? ? "format" : "format_#{index + 1}",
|
|
2978
|
+
command: command,
|
|
2979
|
+
category: :formatter,
|
|
2980
|
+
run_after: :on_completion,
|
|
2981
|
+
required: false,
|
|
2982
|
+
timeout_seconds: 120
|
|
2983
|
+
}
|
|
2984
|
+
end
|
|
2985
|
+
|
|
2986
|
+
commands
|
|
2987
|
+
end
|
|
2988
|
+
|
|
2989
|
+
def generate_phase_two_suggestions_with_ai(tooling_context = nil)
|
|
2990
|
+
config_manager = build_in_memory_config_manager
|
|
2991
|
+
provider_name = config_manager.default_provider
|
|
2992
|
+
return {} unless provider_name
|
|
2993
|
+
|
|
2994
|
+
provider_factory = Aidp::Harness::ProviderFactory.new(config_manager)
|
|
2995
|
+
provider = provider_factory.create_provider(provider_name, {})
|
|
2996
|
+
response = provider.send_message(prompt: phase_two_suggestion_prompt(tooling_context), session: nil)
|
|
2997
|
+
|
|
2998
|
+
parse_phase_two_suggestions(response)
|
|
2999
|
+
rescue => e
|
|
3000
|
+
Aidp.log_warn("setup_wizard", "phase_two_suggestions_ai_failed",
|
|
3001
|
+
error: e.message, provider: provider_name)
|
|
3002
|
+
{}
|
|
3003
|
+
end
|
|
3004
|
+
|
|
3005
|
+
def phase_two_suggestion_prompt(tooling_context = nil)
|
|
3006
|
+
<<~PROMPT
|
|
3007
|
+
Generate deterministic work loop defaults for this repository.
|
|
3008
|
+
Use the detected tooling and frameworks. Prefer concise, safe defaults.
|
|
3009
|
+
Return ONLY JSON matching this schema:
|
|
3010
|
+
#{JSON.pretty_generate(PHASE_TWO_SUGGESTION_SCHEMA)}
|
|
3011
|
+
|
|
3012
|
+
Detected test commands: #{detect_tooling.test_commands.to_json}
|
|
3013
|
+
Detected lint commands: #{detect_tooling.lint_commands.to_json}
|
|
3014
|
+
Detected formatter commands: #{detect_tooling.formatter_commands.to_json}
|
|
3015
|
+
Detected frameworks: #{detect_tooling.frameworks.values.uniq.to_json}
|
|
3016
|
+
User-supplied tooling hints: #{tooling_context.to_json}
|
|
3017
|
+
|
|
3018
|
+
Rules:
|
|
3019
|
+
- Keep command values close to the detected commands.
|
|
3020
|
+
- When detection is empty, use the user-supplied tooling hints as the primary signal.
|
|
3021
|
+
- Use category values from the schema.
|
|
3022
|
+
- Use run_after each_unit for tests and linters, on_completion for formatters.
|
|
3023
|
+
- Watch patterns should focus on test and source files for detected tools.
|
|
3024
|
+
- Guard include patterns should focus on source paths.
|
|
3025
|
+
- Guard exclude patterns should include generated/vendor directories when relevant.
|
|
3026
|
+
PROMPT
|
|
3027
|
+
end
|
|
3028
|
+
|
|
3029
|
+
def parse_phase_two_suggestions(response)
|
|
3030
|
+
response_text = response.is_a?(String) ? response : response.to_s
|
|
3031
|
+
parsed = parse_phase_two_suggestion_payload(response_text)
|
|
3032
|
+
return {} unless parsed.is_a?(Hash)
|
|
3033
|
+
|
|
3034
|
+
{
|
|
3035
|
+
commands: normalize_suggested_commands(parsed[:commands]),
|
|
3036
|
+
watch_patterns: normalize_suggested_patterns(parsed[:watch_patterns]),
|
|
3037
|
+
guard_include_patterns: normalize_suggested_patterns(parsed[:guard_include_patterns]),
|
|
3038
|
+
guard_exclude_patterns: normalize_suggested_patterns(parsed[:guard_exclude_patterns])
|
|
3039
|
+
}
|
|
3040
|
+
rescue JSON::ParserError => e
|
|
3041
|
+
Aidp.log_warn("setup_wizard", "phase_two_suggestions_parse_failed", error: e.message)
|
|
3042
|
+
{}
|
|
3043
|
+
end
|
|
3044
|
+
|
|
3045
|
+
def normalize_suggested_patterns(patterns)
|
|
3046
|
+
Array(patterns).select { |pattern| pattern.is_a?(String) && !pattern.empty? }
|
|
3047
|
+
end
|
|
3048
|
+
|
|
3049
|
+
def normalize_suggested_commands(commands)
|
|
3050
|
+
Array(commands).filter_map do |command|
|
|
3051
|
+
next unless command.is_a?(Hash)
|
|
3052
|
+
|
|
3053
|
+
category = command[:category].to_s
|
|
3054
|
+
run_after = command[:run_after].to_s
|
|
3055
|
+
command_value = command[:command]
|
|
3056
|
+
timeout_seconds = normalize_timeout_seconds(command[:timeout_seconds] || command["timeout_seconds"],
|
|
3057
|
+
invalid: :invalid)
|
|
3058
|
+
next unless command_value.is_a?(String) && !command_value.empty?
|
|
3059
|
+
next unless %w[test lint formatter build documentation custom].include?(category)
|
|
3060
|
+
next unless %w[each_unit full_loop on_completion].include?(run_after)
|
|
3061
|
+
next if timeout_seconds == :invalid
|
|
3062
|
+
|
|
3063
|
+
{
|
|
3064
|
+
name: command[:name].to_s.empty? ? "command" : command[:name].to_s,
|
|
3065
|
+
command: command_value,
|
|
3066
|
+
category: category.to_sym,
|
|
3067
|
+
run_after: run_after.to_sym,
|
|
3068
|
+
required: command[:required] != false,
|
|
3069
|
+
timeout_seconds: timeout_seconds
|
|
3070
|
+
}
|
|
3071
|
+
end
|
|
3072
|
+
end
|
|
3073
|
+
|
|
3074
|
+
def effective_work_loop_commands
|
|
3075
|
+
generic_commands = normalize_generic_work_loop_commands(get(%i[work_loop commands]))
|
|
3076
|
+
legacy_commands = normalize_legacy_work_loop_commands
|
|
3077
|
+
structured_legacy_commands = normalize_structured_legacy_work_loop_commands
|
|
3078
|
+
|
|
3079
|
+
deduplicated_work_loop_commands(generic_commands, legacy_commands, structured_legacy_commands)
|
|
3080
|
+
end
|
|
3081
|
+
|
|
3082
|
+
def deduplicated_work_loop_commands(*command_sets)
|
|
3083
|
+
command_sets.flatten.uniq { |command| deduplication_key(command) }
|
|
3084
|
+
end
|
|
3085
|
+
|
|
3086
|
+
def deduplication_key(command)
|
|
3087
|
+
command.slice(:command, :required, :run_after, :category, :timeout_seconds)
|
|
3088
|
+
end
|
|
3089
|
+
|
|
3090
|
+
def normalize_generic_work_loop_commands(commands)
|
|
3091
|
+
Array(commands).filter_map.with_index do |command, index|
|
|
3092
|
+
normalize_generic_work_loop_command(command, index)
|
|
3093
|
+
end
|
|
3094
|
+
end
|
|
3095
|
+
|
|
3096
|
+
def normalize_generic_work_loop_command(command, index)
|
|
3097
|
+
case command
|
|
3098
|
+
when String
|
|
3099
|
+
{
|
|
3100
|
+
name: "command_#{index}",
|
|
3101
|
+
command: command,
|
|
3102
|
+
category: :custom,
|
|
3103
|
+
run_after: :each_unit,
|
|
3104
|
+
required: true,
|
|
3105
|
+
timeout_seconds: nil
|
|
3106
|
+
}
|
|
3107
|
+
when Hash
|
|
3108
|
+
category = (command[:category] || command["category"] || :custom).to_sym
|
|
3109
|
+
{
|
|
3110
|
+
name: (command[:name] || command["name"] || "command_#{index}").to_s,
|
|
3111
|
+
command: (command[:command] || command["command"]).to_s,
|
|
3112
|
+
category: category,
|
|
3113
|
+
run_after: normalize_run_after(command[:run_after] || command["run_after"]),
|
|
3114
|
+
required: required_value(command),
|
|
3115
|
+
timeout_seconds: normalize_timeout_seconds(command[:timeout_seconds] || command["timeout_seconds"])
|
|
3116
|
+
}
|
|
3117
|
+
end
|
|
3118
|
+
end
|
|
3119
|
+
|
|
3120
|
+
def normalize_legacy_work_loop_commands
|
|
3121
|
+
legacy_command_mappings.flat_map do |config_key, defaults|
|
|
3122
|
+
Array(get([:work_loop, config_key])).filter_map.with_index do |command, index|
|
|
3123
|
+
normalize_legacy_work_loop_command(command, defaults, index)
|
|
3124
|
+
end
|
|
3125
|
+
end
|
|
3126
|
+
end
|
|
3127
|
+
|
|
3128
|
+
def normalize_structured_legacy_work_loop_commands
|
|
3129
|
+
normalize_structured_legacy_test_commands + normalize_structured_legacy_lint_commands
|
|
3130
|
+
end
|
|
3131
|
+
|
|
3132
|
+
def normalize_structured_legacy_test_commands
|
|
3133
|
+
test_config = get(%i[work_loop test]) || {}
|
|
3134
|
+
timeout_seconds = normalize_timeout_seconds(test_config[:timeout_seconds] || test_config["timeout_seconds"])
|
|
3135
|
+
|
|
3136
|
+
[
|
|
3137
|
+
normalize_structured_legacy_command(test_config[:unit] || test_config["unit"],
|
|
3138
|
+
name: "unit_test", category: :test, run_after: :each_unit, timeout_seconds: timeout_seconds),
|
|
3139
|
+
normalize_structured_legacy_command(test_config[:integration] || test_config["integration"],
|
|
3140
|
+
name: "integration_test", category: :test, run_after: :full_loop, timeout_seconds: timeout_seconds),
|
|
3141
|
+
normalize_structured_legacy_command(test_config[:e2e] || test_config["e2e"],
|
|
3142
|
+
name: "e2e_test", category: :test, run_after: :full_loop, timeout_seconds: timeout_seconds)
|
|
3143
|
+
].compact
|
|
3144
|
+
end
|
|
3145
|
+
|
|
3146
|
+
def normalize_structured_legacy_lint_commands
|
|
3147
|
+
lint_config = get(%i[work_loop lint]) || {}
|
|
3148
|
+
|
|
3149
|
+
[
|
|
3150
|
+
normalize_structured_legacy_command(lint_config[:command] || lint_config["command"],
|
|
3151
|
+
name: "lint", category: :lint, run_after: :each_unit),
|
|
3152
|
+
normalize_structured_legacy_command(lint_config[:format] || lint_config["format"],
|
|
3153
|
+
name: "format", category: :formatter, run_after: :on_completion, required: false)
|
|
3154
|
+
].compact
|
|
3155
|
+
end
|
|
3156
|
+
|
|
3157
|
+
def normalize_structured_legacy_command(command, name:, category:, run_after:, required: true,
|
|
3158
|
+
timeout_seconds: nil)
|
|
3159
|
+
return if command.nil? || command.to_s.empty?
|
|
3160
|
+
|
|
3161
|
+
{
|
|
3162
|
+
name: name,
|
|
3163
|
+
command: command.to_s,
|
|
3164
|
+
category: category,
|
|
3165
|
+
run_after: run_after,
|
|
3166
|
+
required: required,
|
|
3167
|
+
timeout_seconds: timeout_seconds
|
|
3168
|
+
}
|
|
3169
|
+
end
|
|
3170
|
+
|
|
3171
|
+
def normalize_legacy_work_loop_command(command, defaults, index)
|
|
3172
|
+
case command
|
|
3173
|
+
when String
|
|
3174
|
+
{
|
|
3175
|
+
name: "#{defaults[:category]}_#{index}",
|
|
3176
|
+
command: command,
|
|
3177
|
+
category: defaults[:category],
|
|
3178
|
+
run_after: defaults[:run_after],
|
|
3179
|
+
required: true,
|
|
3180
|
+
timeout_seconds: nil
|
|
3181
|
+
}
|
|
3182
|
+
when Hash
|
|
3183
|
+
{
|
|
3184
|
+
name: (command[:name] || command["name"] || "#{defaults[:category]}_#{index}").to_s,
|
|
3185
|
+
command: (command[:command] || command["command"]).to_s,
|
|
3186
|
+
category: defaults[:category],
|
|
3187
|
+
run_after: defaults[:run_after],
|
|
3188
|
+
required: required_value(command),
|
|
3189
|
+
timeout_seconds: normalize_timeout_seconds(command[:timeout_seconds] || command["timeout_seconds"])
|
|
3190
|
+
}
|
|
3191
|
+
end
|
|
3192
|
+
end
|
|
3193
|
+
|
|
3194
|
+
def legacy_command_mappings
|
|
3195
|
+
{
|
|
3196
|
+
test_commands: {category: :test, run_after: :each_unit},
|
|
3197
|
+
lint_commands: {category: :lint, run_after: :each_unit},
|
|
3198
|
+
formatter_commands: {category: :formatter, run_after: :on_completion},
|
|
3199
|
+
build_commands: {category: :build, run_after: :each_unit},
|
|
3200
|
+
documentation_commands: {category: :documentation, run_after: :on_completion}
|
|
3201
|
+
}
|
|
3202
|
+
end
|
|
3203
|
+
|
|
3204
|
+
def normalize_run_after(value)
|
|
3205
|
+
case value.to_s
|
|
3206
|
+
when "full_loop", "loop"
|
|
3207
|
+
:full_loop
|
|
3208
|
+
when "on_completion", "completion"
|
|
3209
|
+
:on_completion
|
|
3210
|
+
else
|
|
3211
|
+
:each_unit
|
|
3212
|
+
end
|
|
3213
|
+
end
|
|
3214
|
+
|
|
3215
|
+
def required_value(command)
|
|
3216
|
+
return command[:required] if command.key?(:required)
|
|
3217
|
+
return command["required"] if command.key?("required")
|
|
3218
|
+
|
|
3219
|
+
true
|
|
3220
|
+
end
|
|
3221
|
+
|
|
3222
|
+
def normalize_timeout_seconds(value, invalid: nil)
|
|
3223
|
+
return nil if value.nil?
|
|
3224
|
+
return nil if value.respond_to?(:empty?) && value.empty?
|
|
3225
|
+
|
|
3226
|
+
timeout_seconds = Integer(value, exception: false)
|
|
3227
|
+
return invalid unless timeout_seconds&.positive?
|
|
3228
|
+
|
|
3229
|
+
timeout_seconds
|
|
3230
|
+
end
|
|
3231
|
+
|
|
3232
|
+
def parse_phase_two_suggestion_payload(response_text)
|
|
3233
|
+
JSON.parse(response_text, symbolize_names: true)
|
|
3234
|
+
rescue JSON::ParserError
|
|
3235
|
+
json_payload = extract_phase_two_suggestion_json(response_text)
|
|
3236
|
+
return unless json_payload
|
|
3237
|
+
|
|
3238
|
+
JSON.parse(json_payload, symbolize_names: true)
|
|
3239
|
+
end
|
|
3240
|
+
|
|
3241
|
+
def extract_phase_two_suggestion_json(response_text)
|
|
3242
|
+
extract_fenced_json_object(response_text) || extract_balanced_json_object(response_text)
|
|
3243
|
+
end
|
|
3244
|
+
|
|
3245
|
+
def extract_fenced_json_object(response_text)
|
|
3246
|
+
match = response_text.match(/```(?:json)?\s*(\{.*?\})\s*```/mi)
|
|
3247
|
+
match && match[1]
|
|
3248
|
+
end
|
|
3249
|
+
|
|
3250
|
+
def extract_balanced_json_object(response_text)
|
|
3251
|
+
start_index = response_text.index("{")
|
|
3252
|
+
return unless start_index
|
|
3253
|
+
|
|
3254
|
+
depth = 0
|
|
3255
|
+
in_string = false
|
|
3256
|
+
escaped = false
|
|
3257
|
+
|
|
3258
|
+
response_text.each_char.with_index do |char, index|
|
|
3259
|
+
next if index < start_index
|
|
3260
|
+
|
|
3261
|
+
if in_string
|
|
3262
|
+
if escaped
|
|
3263
|
+
escaped = false
|
|
3264
|
+
elsif char == "\\"
|
|
3265
|
+
escaped = true
|
|
3266
|
+
elsif char == '"'
|
|
3267
|
+
in_string = false
|
|
3268
|
+
end
|
|
3269
|
+
next
|
|
3270
|
+
end
|
|
3271
|
+
|
|
3272
|
+
case char
|
|
3273
|
+
when '"'
|
|
3274
|
+
in_string = true
|
|
3275
|
+
when "{"
|
|
3276
|
+
depth += 1
|
|
3277
|
+
when "}"
|
|
3278
|
+
depth -= 1
|
|
3279
|
+
return response_text[start_index..index] if depth.zero?
|
|
3280
|
+
end
|
|
3281
|
+
end
|
|
3282
|
+
|
|
3283
|
+
nil
|
|
3284
|
+
end
|
|
3285
|
+
|
|
3286
|
+
def normalize_phase_two_value(value, fallback)
|
|
3287
|
+
return fallback if value.nil?
|
|
3288
|
+
return fallback if value.respond_to?(:empty?) && value.empty?
|
|
3289
|
+
|
|
3290
|
+
value
|
|
3291
|
+
end
|
|
3292
|
+
|
|
2774
3293
|
def configure_devcontainer
|
|
2775
3294
|
prompt.say("\nš³ Devcontainer Configuration")
|
|
2776
3295
|
Aidp.log_debug(DEVCONTAINER_COMPONENT, "configure.start")
|
|
@@ -2850,7 +3369,7 @@ module Aidp
|
|
|
2850
3369
|
def build_wizard_config_for_devcontainer
|
|
2851
3370
|
{
|
|
2852
3371
|
providers: @config[:providers]&.keys,
|
|
2853
|
-
test_framework:
|
|
3372
|
+
test_framework: devcontainer_test_framework,
|
|
2854
3373
|
linters: @config.dig(:work_loop, :linting, :tools),
|
|
2855
3374
|
watch_mode: @config.dig(:work_loop, :watch, :enabled),
|
|
2856
3375
|
app_type: detect_app_type,
|
|
@@ -2859,6 +3378,44 @@ module Aidp
|
|
|
2859
3378
|
}.compact
|
|
2860
3379
|
end
|
|
2861
3380
|
|
|
3381
|
+
def devcontainer_test_framework
|
|
3382
|
+
command = first_work_loop_command_for(:test)
|
|
3383
|
+
framework = framework_for_work_loop_command(command)
|
|
3384
|
+
return framework if framework
|
|
3385
|
+
|
|
3386
|
+
detected_framework = framework_from_detected_tooling(command)
|
|
3387
|
+
return detected_framework if detected_framework
|
|
3388
|
+
|
|
3389
|
+
@config.dig(:work_loop, :test_commands)&.first&.dig(:framework)&.to_s
|
|
3390
|
+
end
|
|
3391
|
+
|
|
3392
|
+
def first_work_loop_command_for(category)
|
|
3393
|
+
effective_work_loop_commands.find do |command|
|
|
3394
|
+
command[:category].to_sym == category
|
|
3395
|
+
end
|
|
3396
|
+
end
|
|
3397
|
+
|
|
3398
|
+
def framework_for_work_loop_command(command)
|
|
3399
|
+
framework = Aidp::ToolingDetector.framework_from_command(command&.dig(:command))
|
|
3400
|
+
return if framework == :unknown
|
|
3401
|
+
|
|
3402
|
+
framework.to_s
|
|
3403
|
+
end
|
|
3404
|
+
|
|
3405
|
+
def framework_from_detected_tooling(command)
|
|
3406
|
+
candidates = [
|
|
3407
|
+
command&.dig(:command),
|
|
3408
|
+
detect_tooling.test_commands.first
|
|
3409
|
+
].compact.uniq
|
|
3410
|
+
|
|
3411
|
+
candidates.each do |candidate|
|
|
3412
|
+
framework = detect_tooling.framework_for_command(candidate)
|
|
3413
|
+
return framework.to_s if framework != :unknown
|
|
3414
|
+
end
|
|
3415
|
+
|
|
3416
|
+
nil
|
|
3417
|
+
end
|
|
3418
|
+
|
|
2862
3419
|
def detect_app_type
|
|
2863
3420
|
return "rails_web" if project_file?("config/routes.rb")
|
|
2864
3421
|
return "sinatra" if project_file?("config.ru")
|
data/lib/aidp/version.rb
CHANGED