aia 1.1.1 → 2.0.0.0.pre.beta2
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/.envrc +9 -1
- data/.loki +11 -0
- data/.reek.yml +160 -0
- data/.rubocop.yml +116 -0
- data/.rubocop_strict.yml +15 -0
- data/.version +1 -1
- data/CHANGELOG.md +269 -55
- data/IMPLEMENTATION_PLAN.md +506 -0
- data/README.md +267 -239
- data/Rakefile +5 -5
- data/_typos.toml +10 -0
- data/aia.gemspec +92 -0
- data/architecture_review.md +314 -0
- data/bin/aia +16 -0
- data/config/aia.yml +13 -0
- data/docs/AGENTS.md +40 -0
- data/docs/advanced-prompting.md +67 -3
- data/docs/cli-reference.md +312 -56
- data/docs/configuration.md +130 -19
- data/docs/contributing.md +56 -2
- data/docs/directives-reference.md +593 -78
- data/docs/faq.md +85 -3
- data/docs/guides/available-models.md +1 -1
- data/docs/guides/basic-usage.md +6 -6
- data/docs/guides/chat.md +40 -16
- data/docs/guides/crew.md +239 -0
- data/docs/guides/executable-prompts.md +1 -1
- data/docs/guides/index.md +1 -0
- data/docs/guides/models.md +15 -0
- data/docs/index.md +29 -2
- data/docs/installation.md +44 -17
- data/docs/mcp-integration.md +40 -0
- data/docs/prompt_management.md +85 -86
- data/docs/security.md +47 -0
- data/docs/special_projects_guide.md +386 -0
- data/docs/tools-and-mcp-examples.md +23 -0
- data/docs/workflows-and-pipelines.md +84 -7
- data/examples/.gitignore +1 -0
- data/examples/00_setup_aia.sh +27 -44
- data/examples/11_multi_model.sh +4 -14
- data/examples/12_token_usage.sh +3 -12
- data/examples/18_tools.sh +10 -2
- data/examples/22_chat_mode.sh +0 -10
- data/examples/23_verify.sh +139 -0
- data/examples/24_decompose.sh +139 -0
- data/examples/25_spawn.sh +139 -0
- data/examples/26_debate.sh +97 -0
- data/examples/27_mention_routing.sh +157 -0
- data/examples/28_model_switching.sh +106 -0
- data/examples/29_agent_harness.sh +177 -0
- data/examples/README.md +65 -0
- data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
- data/examples/aia_config.yml +1 -1
- data/examples/aia_config_orchestrator.yml +45 -0
- data/examples/common.sh +18 -6
- data/examples/context/tech_stack.md +2 -2
- data/examples/prompts_dir/roles/orchestrator.md +21 -0
- data/examples/requirements/sinatra_taskflow_app.md +139 -0
- data/examples/rules/01_classify_ruby.rb +16 -0
- data/examples/rules/02_prefer_claude_for_code.rb +19 -0
- data/examples/rules/03_gate_prompt_length.rb +19 -0
- data/examples/rules/04_tool_selection.rb +41 -0
- data/examples/rules/README.md +30 -0
- data/examples/run_all.sh +48 -15
- data/examples/tools/word_count_tool.rb +1 -1
- data/lib/AGENTS.md +57 -0
- data/lib/aia/chat_loop.rb +263 -167
- data/lib/aia/config/cli_parser.rb +217 -145
- data/lib/aia/config/defaults.yml +62 -33
- data/lib/aia/config/mcp_parser.rb +52 -51
- data/lib/aia/config/model_spec.rb +34 -2
- data/lib/aia/config/validator.rb +171 -216
- data/lib/aia/config.rb +111 -145
- data/lib/aia/content_extractor.rb +155 -0
- data/lib/aia/cost_calculator.rb +39 -0
- data/lib/aia/crew.rb +164 -0
- data/lib/aia/debate_handler.rb +174 -0
- data/lib/aia/delegate_handler.rb +116 -0
- data/lib/aia/directive.rb +43 -26
- data/lib/aia/directive_processor.rb +16 -7
- data/lib/aia/directives/configuration_directives.rb +214 -60
- data/lib/aia/directives/context_directives.rb +67 -52
- data/lib/aia/directives/execution_directives.rb +141 -4
- data/lib/aia/directives/model_directives.rb +163 -141
- data/lib/aia/directives/trakflow_directives.rb +62 -0
- data/lib/aia/directives/utility_directives.rb +227 -30
- data/lib/aia/directives/web_and_file_directives.rb +126 -77
- data/lib/aia/errors.rb +15 -0
- data/lib/aia/fact_asserter.rb +27 -0
- data/lib/aia/fzf.rb +9 -31
- data/lib/aia/handler_context.rb +17 -0
- data/lib/aia/handler_protocol.rb +19 -0
- data/lib/aia/history_transfer.rb +55 -0
- data/lib/aia/input_collector.rb +3 -3
- data/lib/aia/layered_orchestrator.rb +471 -0
- data/lib/aia/logger.rb +45 -25
- data/lib/aia/mcp_config_normalizer.rb +35 -0
- data/lib/aia/mcp_connection_manager.rb +315 -0
- data/lib/aia/mcp_discovery.rb +44 -0
- data/lib/aia/mcp_grouper.rb +33 -0
- data/lib/aia/mcp_server_config.rb +30 -0
- data/lib/aia/mcp_utility.rb +60 -0
- data/lib/aia/mention_router.rb +217 -0
- data/lib/aia/model_alias_registry.rb +97 -0
- data/lib/aia/model_switch_handler.rb +100 -0
- data/lib/aia/network_builder.rb +160 -0
- data/lib/aia/network_memory_manager.rb +55 -0
- data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
- data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
- data/lib/aia/pipeline_orchestrator.rb +272 -0
- data/lib/aia/plugin_loader.rb +170 -0
- data/lib/aia/plugin_monitor.rb +211 -0
- data/lib/aia/prompt_decomposer.rb +159 -0
- data/lib/aia/prompt_handler.rb +56 -82
- data/lib/aia/robot_builder.rb +51 -0
- data/lib/aia/robot_factory.rb +338 -0
- data/lib/aia/robot_namer.rb +110 -0
- data/lib/aia/session.rb +87 -17
- data/lib/aia/session_tracker.rb +207 -0
- data/lib/aia/similarity_scorer.rb +41 -0
- data/lib/aia/skill_utils.rb +105 -1
- data/lib/aia/spawn_handler.rb +129 -0
- data/lib/aia/spawn_spec_parser.rb +65 -0
- data/lib/aia/special_mode_handler.rb +322 -0
- data/lib/aia/speech.rb +67 -0
- data/lib/aia/startup_coordinator.rb +151 -0
- data/lib/aia/streaming_runner.rb +172 -0
- data/lib/aia/system_prompt_assembler.rb +92 -0
- data/lib/aia/task_coordinator.rb +207 -0
- data/lib/aia/task_decomposer.rb +57 -0
- data/lib/aia/task_executor.rb +51 -0
- data/lib/aia/tfidf_math.rb +27 -0
- data/lib/aia/timing.rb +15 -0
- data/lib/aia/tool_filter/tfidf.rb +116 -0
- data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
- data/lib/aia/tool_filter.rb +83 -0
- data/lib/aia/tool_filter_registry.rb +30 -0
- data/lib/aia/tool_filter_strategy.rb +146 -0
- data/lib/aia/tool_introspection.rb +17 -0
- data/lib/aia/tool_loader.rb +216 -0
- data/lib/aia/tool_utility.rb +30 -0
- data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
- data/lib/aia/tools/recruit_robot_tool.rb +60 -0
- data/lib/aia/tools/reskill_robot_tool.rb +44 -0
- data/lib/aia/tools/task_board_tool.rb +115 -0
- data/lib/aia/trakflow_bridge.rb +175 -0
- data/lib/aia/turn_state.rb +95 -0
- data/lib/aia/ui_presenter.rb +182 -206
- data/lib/aia/utility.rb +136 -87
- data/lib/aia/{history_manager.rb → variable_input_collector.rb} +9 -9
- data/lib/aia/verification_network.rb +57 -0
- data/lib/aia.rb +124 -63
- data/mkdocs.yml +1 -0
- metadata +187 -58
- data/justfile +0 -215
- data/lib/aia/adapter/chat_execution.rb +0 -242
- data/lib/aia/adapter/error_handler.rb +0 -68
- data/lib/aia/adapter/gem_activator.rb +0 -57
- data/lib/aia/adapter/mcp_connector.rb +0 -274
- data/lib/aia/adapter/modality_handlers.rb +0 -167
- data/lib/aia/adapter/model_registry.rb +0 -81
- data/lib/aia/adapter/multi_model_chat.rb +0 -218
- data/lib/aia/adapter/provider_configurator.rb +0 -59
- data/lib/aia/adapter/tool_filter.rb +0 -85
- data/lib/aia/adapter/tool_loader.rb +0 -90
- data/lib/aia/chat_processor_service.rb +0 -178
- data/lib/aia/prompt_pipeline.rb +0 -183
- data/lib/aia/ruby_llm_adapter.rb +0 -95
- data/lib/extensions/openstruct_merge.rb +0 -48
- data/lib/extensions/ruby_llm/.irbrc +0 -56
- data/lib/extensions/ruby_llm/modalities.rb +0 -36
- data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
- data/lib/refinements/string.rb +0 -16
- data/main.just +0 -76
data/lib/aia/config/validator.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'word_wrapper'
|
|
4
|
-
require_relative '../adapter/gem_activator'
|
|
5
4
|
require_relative '../skill_utils'
|
|
6
5
|
|
|
7
6
|
# lib/aia/config/validator.rb
|
|
@@ -10,12 +9,14 @@ require_relative '../skill_utils'
|
|
|
10
9
|
# Handles prompt ID extraction, context file validation, role processing, etc.
|
|
11
10
|
|
|
12
11
|
module AIA
|
|
12
|
+
# rubocop:disable Metrics/ModuleLength
|
|
13
13
|
module ConfigValidator
|
|
14
14
|
class << self
|
|
15
15
|
# Tailor and validate the configuration
|
|
16
16
|
#
|
|
17
17
|
# @param config [AIA::Config] the configuration to validate
|
|
18
18
|
# @return [AIA::Config] the validated configuration
|
|
19
|
+
# :reek:TooManyStatements -- top-level validation pipeline: one named step per line, executed in fixed order
|
|
19
20
|
def tailor(config)
|
|
20
21
|
remaining_args = config.remaining_args&.dup || []
|
|
21
22
|
config.remaining_args = nil
|
|
@@ -24,8 +25,7 @@ module AIA
|
|
|
24
25
|
stdin_content = process_stdin_content
|
|
25
26
|
config.stdin_content = stdin_content if stdin_content && !stdin_content.strip.empty?
|
|
26
27
|
|
|
27
|
-
#
|
|
28
|
-
# are registered into PM.directives before PromptHandler.new is called.
|
|
28
|
+
# Tool files may define AIA::Directive subclasses used by PromptManager.
|
|
29
29
|
require_tool_files(config)
|
|
30
30
|
|
|
31
31
|
# Process arguments and validate
|
|
@@ -33,30 +33,34 @@ module AIA
|
|
|
33
33
|
validate_and_set_context_files(config, remaining_args)
|
|
34
34
|
handle_executable_prompt(config)
|
|
35
35
|
handle_stdin_as_prompt(config)
|
|
36
|
-
|
|
37
|
-
handle_mcp_list(config)
|
|
38
|
-
handle_list_tools(config)
|
|
39
|
-
handle_list_skills(config)
|
|
40
|
-
handle_completion_script(config)
|
|
36
|
+
return :early_exit if handle_early_exits(config) == :early_exit
|
|
41
37
|
validate_required_prompt_id(config)
|
|
42
38
|
process_role_configuration(config)
|
|
39
|
+
validate_plugins_dir(config)
|
|
43
40
|
handle_fuzzy_search_prompt_id(config)
|
|
44
41
|
normalize_boolean_flags(config)
|
|
45
42
|
validate_final_prompt_requirements(config)
|
|
46
|
-
configure_prompt_manager(config)
|
|
47
43
|
prepare_pipeline(config)
|
|
48
44
|
validate_pipeline_prompts(config)
|
|
49
45
|
|
|
50
46
|
config
|
|
51
47
|
end
|
|
52
48
|
|
|
49
|
+
def handle_early_exits(config)
|
|
50
|
+
return :early_exit if handle_dump_config(config) == :early_exit
|
|
51
|
+
return :early_exit if handle_mcp_list(config) == :early_exit
|
|
52
|
+
return :early_exit if handle_list_tools(config) == :early_exit
|
|
53
|
+
return :early_exit if handle_list_skills(config) == :early_exit
|
|
54
|
+
:early_exit if handle_completion_script(config) == :early_exit
|
|
55
|
+
end
|
|
56
|
+
|
|
53
57
|
def process_stdin_content
|
|
54
58
|
stdin_content = String.new
|
|
55
59
|
|
|
56
|
-
if
|
|
60
|
+
if !$stdin.tty? && !$stdin.closed?
|
|
57
61
|
begin
|
|
58
|
-
stdin_content << "\n" +
|
|
59
|
-
|
|
62
|
+
stdin_content << ("\n" + $stdin.read)
|
|
63
|
+
$stdin.reopen('/dev/tty')
|
|
60
64
|
rescue => _
|
|
61
65
|
# If we can't reopen, continue without error
|
|
62
66
|
end
|
|
@@ -71,9 +75,8 @@ module AIA
|
|
|
71
75
|
maybe_id = remaining_args.first
|
|
72
76
|
maybe_id_plus = File.join(config.prompts.dir, maybe_id + config.prompts.extname)
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
end
|
|
78
|
+
return unless AIA.bad_file?(maybe_id) && AIA.good_file?(maybe_id_plus)
|
|
79
|
+
config.prompt_id = remaining_args.shift
|
|
77
80
|
end
|
|
78
81
|
|
|
79
82
|
def validate_and_set_context_files(config, remaining_args)
|
|
@@ -81,8 +84,16 @@ module AIA
|
|
|
81
84
|
|
|
82
85
|
bad_files = remaining_args.reject { |filename| AIA.good_file?(filename) }
|
|
83
86
|
if bad_files.any?
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
# Detect likely flag typos: "_B" instead of "-B", "_CD" instead of "-CD", etc.
|
|
88
|
+
flag_typos = bad_files.grep(/\A_[A-Za-z]/)
|
|
89
|
+
if flag_typos.any?
|
|
90
|
+
suggestions = flag_typos.map { |f| "-#{f[1..]}" }.join(', ')
|
|
91
|
+
raise AIA::ConfigurationError,
|
|
92
|
+
"Unknown argument(s): #{flag_typos.join(', ')}\n" \
|
|
93
|
+
"Did you mean: #{suggestions}?\n" \
|
|
94
|
+
"Flags require a dash prefix (e.g., -B not _B)."
|
|
95
|
+
end
|
|
96
|
+
raise AIA::ConfigurationError, "The following files do not exist: #{bad_files.join(', ')}"
|
|
86
97
|
end
|
|
87
98
|
|
|
88
99
|
config.context_files ||= []
|
|
@@ -90,70 +101,70 @@ module AIA
|
|
|
90
101
|
end
|
|
91
102
|
|
|
92
103
|
def handle_executable_prompt(config)
|
|
93
|
-
# Auto-detect: no prompt_id, first context_file starts with shebang
|
|
94
104
|
return unless config.prompt_id.nil?
|
|
95
|
-
return unless config.context_files && !config.context_files.empty?
|
|
96
105
|
|
|
97
|
-
|
|
106
|
+
files = config.context_files
|
|
107
|
+
return unless files && !files.empty?
|
|
108
|
+
|
|
109
|
+
candidate = files.first
|
|
98
110
|
return unless File.exist?(candidate) && File.readable?(candidate)
|
|
99
111
|
|
|
100
112
|
first_line = File.open(candidate, &:readline).strip rescue nil
|
|
101
113
|
return unless first_line&.start_with?('#!')
|
|
102
114
|
|
|
103
|
-
|
|
104
|
-
config.context_files.shift
|
|
115
|
+
files.shift
|
|
105
116
|
config.executable_prompt_content = File.read(candidate).lines[1..].join
|
|
106
117
|
config.prompt_id = '__EXECUTABLE_PROMPT__'
|
|
107
118
|
end
|
|
108
119
|
|
|
109
120
|
def handle_stdin_as_prompt(config)
|
|
110
121
|
return unless config.prompt_id.nil?
|
|
111
|
-
return unless config.stdin_content && !config.stdin_content.strip.empty?
|
|
112
|
-
|
|
113
122
|
content = config.stdin_content
|
|
123
|
+
return unless content && !content.strip.empty?
|
|
114
124
|
|
|
115
|
-
# Strip shebang line if present (e.g., piped from an executable prompt)
|
|
116
125
|
if content.lines.first&.strip&.start_with?('#!')
|
|
117
126
|
content = content.lines[1..].join
|
|
118
127
|
end
|
|
119
128
|
|
|
120
129
|
config.executable_prompt_content = content
|
|
121
|
-
config.stdin_content = nil
|
|
130
|
+
config.stdin_content = nil
|
|
122
131
|
config.prompt_id = '__EXECUTABLE_PROMPT__'
|
|
123
132
|
end
|
|
124
133
|
|
|
125
134
|
def validate_required_prompt_id(config)
|
|
126
|
-
return unless config.prompt_id.nil? &&
|
|
135
|
+
return unless config.prompt_id.nil? && config.flags.chat != true && config.flags.fuzzy != true
|
|
127
136
|
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
raise AIA::ConfigurationError,
|
|
138
|
+
"A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
|
|
130
139
|
end
|
|
131
140
|
|
|
132
141
|
def process_role_configuration(config)
|
|
133
|
-
|
|
142
|
+
prompts = config.prompts
|
|
143
|
+
role = prompts.role
|
|
134
144
|
return if role.nil? || role.empty?
|
|
135
145
|
|
|
136
|
-
roles_prefix =
|
|
137
|
-
|
|
146
|
+
roles_prefix = prompts.roles_prefix
|
|
138
147
|
unless AIA::SkillUtils.path_based_id?(role) || roles_prefix.nil? || roles_prefix.empty? || role.start_with?(roles_prefix)
|
|
139
|
-
|
|
140
|
-
role =
|
|
148
|
+
role = "#{roles_prefix}/#{role}"
|
|
149
|
+
prompts.role = role
|
|
141
150
|
end
|
|
142
151
|
|
|
143
|
-
|
|
152
|
+
prompts.roles_dir ||= File.join(prompts.dir, roles_prefix.to_s)
|
|
144
153
|
|
|
145
|
-
# In chat-only mode (no prompt_id), leave the role configured so ChatLoop
|
|
146
|
-
# can inject it as initial context. Promoting it to prompt_id would cause
|
|
147
|
-
# PM to receive the role path as a literal string rather than file content.
|
|
148
154
|
return if config.flags&.chat == true
|
|
149
155
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
return unless config.prompt_id.nil? || config.prompt_id.empty?
|
|
157
|
+
config.prompt_id = role
|
|
158
|
+
config.pipeline.prepend(role)
|
|
159
|
+
prompts.role = ''
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def validate_plugins_dir(config)
|
|
163
|
+
plugins_dir = config.paths&.plugins_dir
|
|
164
|
+
return if plugins_dir.nil? || plugins_dir.to_s.strip.empty?
|
|
165
|
+
return if Dir.exist?(plugins_dir)
|
|
166
|
+
|
|
167
|
+
$stderr.puts "Warning: configured plugins directory does not exist: #{plugins_dir}"
|
|
157
168
|
end
|
|
158
169
|
|
|
159
170
|
def handle_fuzzy_search_prompt_id(config)
|
|
@@ -163,9 +174,10 @@ module AIA
|
|
|
163
174
|
end
|
|
164
175
|
|
|
165
176
|
def normalize_boolean_flags(config)
|
|
166
|
-
|
|
167
|
-
normalize_boolean_flag(
|
|
168
|
-
normalize_boolean_flag(
|
|
177
|
+
flags = config.flags
|
|
178
|
+
normalize_boolean_flag(flags, :chat)
|
|
179
|
+
normalize_boolean_flag(flags, :fuzzy)
|
|
180
|
+
normalize_boolean_flag(flags, :consensus)
|
|
169
181
|
end
|
|
170
182
|
|
|
171
183
|
def normalize_boolean_flag(flags_section, flag)
|
|
@@ -188,12 +200,12 @@ module AIA
|
|
|
188
200
|
return unless config.dump_file
|
|
189
201
|
|
|
190
202
|
dump_config(config, config.dump_file)
|
|
191
|
-
|
|
203
|
+
:early_exit
|
|
192
204
|
end
|
|
193
205
|
|
|
194
206
|
def handle_mcp_list(config)
|
|
195
207
|
return unless config.mcp_list
|
|
196
|
-
return if config.list_tools
|
|
208
|
+
return if config.list_tools
|
|
197
209
|
|
|
198
210
|
servers = filter_mcp_servers(config)
|
|
199
211
|
|
|
@@ -202,18 +214,17 @@ module AIA
|
|
|
202
214
|
else
|
|
203
215
|
label = mcp_filter_active?(config) ? "Active" : "Configured"
|
|
204
216
|
puts "#{label} MCP servers:\n\n"
|
|
205
|
-
servers.each
|
|
206
|
-
name = server[:name] || server['name'] || '(unnamed)'
|
|
207
|
-
command = server[:command] || server['command'] || '(no command)'
|
|
208
|
-
args = server[:args] || server['args'] || []
|
|
209
|
-
args_str = args.empty? ? '' : " #{args.join(' ')}"
|
|
210
|
-
puts " #{name}"
|
|
211
|
-
puts " command: #{command}#{args_str}"
|
|
212
|
-
puts
|
|
213
|
-
end
|
|
217
|
+
servers.each { |server| print_mcp_server(McpServerConfig.from_hash(server)) }
|
|
214
218
|
end
|
|
215
219
|
|
|
216
|
-
|
|
220
|
+
:early_exit
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def print_mcp_server(server_config)
|
|
224
|
+
args_str = server_config.args.empty? ? '' : " #{server_config.args.join(' ')}"
|
|
225
|
+
puts " #{server_config.name || '(unnamed)'}"
|
|
226
|
+
puts " command: #{server_config.command || '(no command)'}#{args_str}"
|
|
227
|
+
puts
|
|
217
228
|
end
|
|
218
229
|
|
|
219
230
|
def handle_list_tools(config)
|
|
@@ -228,7 +239,7 @@ module AIA
|
|
|
228
239
|
|
|
229
240
|
if local_tools.empty? && mcp_tool_groups.empty?
|
|
230
241
|
$stderr.puts "No tools available."
|
|
231
|
-
|
|
242
|
+
return :early_exit
|
|
232
243
|
end
|
|
233
244
|
|
|
234
245
|
if $stdout.tty?
|
|
@@ -237,41 +248,7 @@ module AIA
|
|
|
237
248
|
list_tools_markdown(local_tools, mcp_tool_groups)
|
|
238
249
|
end
|
|
239
250
|
|
|
240
|
-
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def handle_list_skills(config)
|
|
244
|
-
return unless config.list_skills
|
|
245
|
-
|
|
246
|
-
skills_dir = AIA.config.skills.dir
|
|
247
|
-
|
|
248
|
-
unless Dir.exist?(skills_dir)
|
|
249
|
-
$stderr.puts "No skills directory found at #{skills_dir}"
|
|
250
|
-
exit 0
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
skill_dirs = Dir.glob("*/SKILL.md", base: skills_dir).map { |f| File.dirname(f) }.sort
|
|
254
|
-
|
|
255
|
-
if skill_dirs.empty?
|
|
256
|
-
$stderr.puts "No skills found in #{skills_dir}"
|
|
257
|
-
exit 0
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
skill_dirs.each do |skill_name|
|
|
261
|
-
skill_md = File.join(skills_dir, skill_name, 'SKILL.md')
|
|
262
|
-
fm = AIA::SkillUtils.parse_front_matter(skill_md)
|
|
263
|
-
|
|
264
|
-
puts "## #{skill_name}"
|
|
265
|
-
puts
|
|
266
|
-
puts "| Key | Value |"
|
|
267
|
-
puts "|-----|-------|"
|
|
268
|
-
fm.each do |key, value|
|
|
269
|
-
puts "| #{key} | #{value} |"
|
|
270
|
-
end
|
|
271
|
-
puts
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
exit 0
|
|
251
|
+
:early_exit
|
|
275
252
|
end
|
|
276
253
|
|
|
277
254
|
def list_tools_terminal(local_tools, mcp_tool_groups)
|
|
@@ -290,8 +267,8 @@ module AIA
|
|
|
290
267
|
end
|
|
291
268
|
|
|
292
269
|
def print_tool_terminal(tool, width, indent)
|
|
293
|
-
name =
|
|
294
|
-
desc =
|
|
270
|
+
name = ToolIntrospection.tool_name(tool)
|
|
271
|
+
desc = ToolIntrospection.tool_description(tool).strip
|
|
295
272
|
|
|
296
273
|
puts " #{name}"
|
|
297
274
|
unless desc.empty?
|
|
@@ -302,6 +279,7 @@ module AIA
|
|
|
302
279
|
puts
|
|
303
280
|
end
|
|
304
281
|
|
|
282
|
+
# :reek:TooManyStatements -- sequential markdown report: header, local tools section, one section per MCP server
|
|
305
283
|
def list_tools_markdown(local_tools, mcp_tool_groups)
|
|
306
284
|
total = local_tools.size + mcp_tool_groups.values.sum(&:size)
|
|
307
285
|
sources = 1 + mcp_tool_groups.size
|
|
@@ -325,30 +303,25 @@ module AIA
|
|
|
325
303
|
end
|
|
326
304
|
|
|
327
305
|
def print_tool_markdown(tool)
|
|
328
|
-
name =
|
|
329
|
-
desc =
|
|
306
|
+
name = ToolIntrospection.tool_name(tool)
|
|
307
|
+
desc = ToolIntrospection.tool_description(tool).strip
|
|
330
308
|
|
|
331
309
|
puts "### `#{name}`"
|
|
332
310
|
puts
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
end
|
|
311
|
+
return if desc.empty?
|
|
312
|
+
puts nest_markdown_headings(desc, 3)
|
|
313
|
+
puts
|
|
337
314
|
end
|
|
338
315
|
|
|
339
|
-
# Adjusts any markdown headings in text so they nest under the
|
|
340
|
-
# given parent heading level. e.g. with parent_level=3 (###),
|
|
341
|
-
# a "# Foo" becomes "#### Foo" and "## Bar" becomes "##### Bar".
|
|
342
|
-
# Handles headings with optional leading whitespace.
|
|
343
316
|
def nest_markdown_headings(text, parent_level)
|
|
344
|
-
text.gsub(/^[ \t]*(\#{1,6})\s/) do |
|
|
345
|
-
existing =
|
|
346
|
-
"#" * (existing.length + parent_level) + " "
|
|
317
|
+
text.gsub(/^[ \t]*(\#{1,6})\s/) do |_match|
|
|
318
|
+
existing = ::Regexp.last_match(1)
|
|
319
|
+
("#" * (existing.length + parent_level)) + " "
|
|
347
320
|
end
|
|
348
321
|
end
|
|
349
322
|
|
|
350
323
|
def filter_mcp_servers(config)
|
|
351
|
-
servers
|
|
324
|
+
servers = config.mcp_servers || []
|
|
352
325
|
use_list = Array(config.mcp_use)
|
|
353
326
|
skip_list = Array(config.mcp_skip)
|
|
354
327
|
|
|
@@ -371,133 +344,127 @@ module AIA
|
|
|
371
344
|
if File.exist?(expanded)
|
|
372
345
|
require expanded
|
|
373
346
|
else
|
|
374
|
-
|
|
347
|
+
$stderr.puts "Warning: Tool file not found: #{path}"
|
|
375
348
|
end
|
|
376
349
|
rescue LoadError, StandardError => e
|
|
377
|
-
|
|
350
|
+
$stderr.puts "Warning: Failed to load tool '#{path}': #{e.message}"
|
|
378
351
|
end
|
|
379
352
|
end
|
|
380
353
|
|
|
354
|
+
# Canonical --list-skills handler. Runs after config is built (so a -c
|
|
355
|
+
# config file's skills.dir is honored) and resolves skills the same way
|
|
356
|
+
# loading does, so what is listed is always loadable.
|
|
357
|
+
def handle_list_skills(config)
|
|
358
|
+
return unless config.respond_to?(:list_skills) && config.list_skills
|
|
359
|
+
|
|
360
|
+
puts AIA::SkillUtils.list_skills_markdown(config)
|
|
361
|
+
:early_exit
|
|
362
|
+
end
|
|
363
|
+
|
|
381
364
|
def load_local_tools(config)
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
require lib
|
|
387
|
-
Adapter::GemActivator.trigger_tool_loading(lib)
|
|
388
|
-
rescue LoadError => e
|
|
389
|
-
warn "Warning: Failed to require '#{lib}': #{e.message}"
|
|
390
|
-
warn "Hint: Make sure the gem is installed: gem install #{lib}"
|
|
391
|
-
rescue StandardError => e
|
|
392
|
-
warn "Warning: Error in library '#{lib}': #{e.class} - #{e.message}"
|
|
393
|
-
end
|
|
394
|
-
end
|
|
365
|
+
require_configured_libs(config)
|
|
366
|
+
require_tool_files(config)
|
|
367
|
+
instantiable_tool_classes
|
|
368
|
+
end
|
|
395
369
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
warn "Warning: Failed to load tool '#{path}': #{e.message}"
|
|
370
|
+
# :reek:DuplicateMethodCall -- each e.message belongs to a different rescue clause exception; nothing to hoist
|
|
371
|
+
def require_configured_libs(config)
|
|
372
|
+
Array(config.require_libs).each do |lib|
|
|
373
|
+
require lib
|
|
374
|
+
rescue LoadError => e
|
|
375
|
+
$stderr.puts "Warning: Failed to require '#{lib}': #{e.message}"
|
|
376
|
+
$stderr.puts "Hint: Make sure the gem is installed: gem install #{lib}"
|
|
377
|
+
rescue StandardError => e
|
|
378
|
+
$stderr.puts "Warning: Error in library '#{lib}': #{e.class} - #{e.message}"
|
|
406
379
|
end
|
|
380
|
+
end
|
|
407
381
|
|
|
408
|
-
|
|
382
|
+
# Scan ObjectSpace for RubyLLM::Tool subclasses that can be instantiated.
|
|
383
|
+
def instantiable_tool_classes
|
|
409
384
|
ObjectSpace.each_object(Class).select do |klass|
|
|
410
|
-
next false unless klass < RubyLLM::Tool
|
|
385
|
+
next false unless defined?(RubyLLM::Tool) && klass < RubyLLM::Tool
|
|
411
386
|
|
|
412
387
|
begin
|
|
413
388
|
klass.new
|
|
414
389
|
true
|
|
415
|
-
rescue
|
|
390
|
+
rescue StandardError
|
|
416
391
|
false
|
|
417
392
|
end
|
|
418
393
|
end
|
|
419
394
|
end
|
|
420
395
|
|
|
421
396
|
def first_sentences(text, count)
|
|
422
|
-
# Normalize whitespace: collapse newlines and multiple spaces
|
|
423
397
|
normalized = text.gsub(/\s*\n\s*/, ' ').gsub(/\s{2,}/, ' ').strip
|
|
424
398
|
sentences = normalized.scan(/[^.!?]*[.!?]/)
|
|
425
399
|
result = sentences.first(count).join.strip
|
|
426
400
|
result.empty? ? normalized : result
|
|
427
401
|
end
|
|
428
402
|
|
|
403
|
+
# :reek:TooManyStatements -- per-server connect loop with progress prints on each outcome path
|
|
429
404
|
def load_mcp_tools_grouped(config)
|
|
430
405
|
servers = filter_mcp_servers(config)
|
|
431
406
|
return {} if servers.empty?
|
|
432
407
|
|
|
433
|
-
# Suppress MCP logger noise during listing
|
|
434
408
|
quiet_mcp_logger
|
|
435
409
|
|
|
436
|
-
|
|
437
|
-
|
|
410
|
+
servers.each_with_object({}) do |server, groups|
|
|
411
|
+
server_config = McpServerConfig.from_hash(server)
|
|
412
|
+
$stderr.print "MCP: Connecting to #{server_config.name}..."
|
|
413
|
+
$stderr.flush
|
|
438
414
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
415
|
+
tools = collect_server_tools(server_config)
|
|
416
|
+
if tools
|
|
417
|
+
groups[server_config.name] = tools
|
|
418
|
+
$stderr.puts " #{tools.size} tools"
|
|
419
|
+
else
|
|
420
|
+
$stderr.puts " failed"
|
|
421
|
+
end
|
|
422
|
+
rescue StandardError => e
|
|
423
|
+
$stderr.puts " error: #{e.message}"
|
|
424
|
+
end
|
|
425
|
+
end
|
|
444
426
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
427
|
+
# Connect to one server and return its tools, or nil when the client
|
|
428
|
+
# never comes alive.
|
|
429
|
+
def collect_server_tools(server_config)
|
|
430
|
+
mcp_add_client(server_config)
|
|
431
|
+
client = RubyLLM::MCP.clients[server_config.name]
|
|
432
|
+
client.start
|
|
433
|
+
return nil unless client.alive?
|
|
448
434
|
|
|
449
|
-
|
|
450
|
-
|
|
435
|
+
client.tools rescue []
|
|
436
|
+
end
|
|
451
437
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
client = begin
|
|
457
|
-
RubyLLM::MCP.add_client(
|
|
458
|
-
name: name, transport_type: :stdio,
|
|
459
|
-
config: mcp_config, request_timeout: timeout, start: false
|
|
460
|
-
)
|
|
461
|
-
rescue ArgumentError
|
|
462
|
-
RubyLLM::MCP.add_client(
|
|
463
|
-
name: name, transport_type: :stdio,
|
|
464
|
-
config: mcp_config, start: false
|
|
465
|
-
)
|
|
466
|
-
end
|
|
467
|
-
|
|
468
|
-
client = RubyLLM::MCP.clients[name]
|
|
469
|
-
client.start
|
|
470
|
-
|
|
471
|
-
if client.alive?
|
|
472
|
-
server_tools = client.tools rescue []
|
|
473
|
-
groups[name] = server_tools
|
|
474
|
-
$stderr.puts " #{server_tools.size} tools"
|
|
475
|
-
else
|
|
476
|
-
$stderr.puts " failed"
|
|
477
|
-
end
|
|
478
|
-
rescue StandardError => e
|
|
479
|
-
$stderr.puts " error: #{e.message}"
|
|
480
|
-
end
|
|
481
|
-
end
|
|
438
|
+
def mcp_add_client(server_config)
|
|
439
|
+
mcp_config = { command: server_config.command, args: server_config.args }
|
|
440
|
+
mcp_config[:env] = server_config.env unless server_config.env.empty?
|
|
482
441
|
|
|
483
|
-
|
|
442
|
+
RubyLLM::MCP.add_client(
|
|
443
|
+
name: server_config.name, transport_type: :stdio,
|
|
444
|
+
config: mcp_config, request_timeout: server_config.timeout_ms, start: false
|
|
445
|
+
)
|
|
446
|
+
rescue ArgumentError
|
|
447
|
+
# Older ruby_llm-mcp versions do not accept request_timeout
|
|
448
|
+
RubyLLM::MCP.add_client(
|
|
449
|
+
name: server_config.name, transport_type: :stdio,
|
|
450
|
+
config: mcp_config, start: false
|
|
451
|
+
)
|
|
484
452
|
end
|
|
453
|
+
# rubocop:enable Metrics/ModuleLength
|
|
485
454
|
|
|
486
455
|
def quiet_mcp_logger
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
end
|
|
493
|
-
end
|
|
456
|
+
return unless defined?(RubyLLM::MCP) && RubyLLM::MCP.respond_to?(:config)
|
|
457
|
+
mcp_config = RubyLLM::MCP.config
|
|
458
|
+
return unless mcp_config.respond_to?(:logger=)
|
|
459
|
+
quiet = Logger.new(File::NULL)
|
|
460
|
+
mcp_config.logger = quiet
|
|
494
461
|
end
|
|
495
462
|
|
|
496
463
|
def handle_completion_script(config)
|
|
497
464
|
return unless config.completion
|
|
498
465
|
|
|
499
466
|
generate_completion_script(config.completion)
|
|
500
|
-
|
|
467
|
+
:early_exit
|
|
501
468
|
end
|
|
502
469
|
|
|
503
470
|
def generate_completion_script(shell)
|
|
@@ -506,31 +473,25 @@ module AIA
|
|
|
506
473
|
if File.exist?(script_path)
|
|
507
474
|
puts File.read(script_path)
|
|
508
475
|
else
|
|
509
|
-
|
|
476
|
+
$stderr.puts "ERROR: The shell '#{shell}' is not supported or the completion script is missing."
|
|
510
477
|
end
|
|
511
478
|
end
|
|
512
479
|
|
|
513
480
|
def validate_final_prompt_requirements(config)
|
|
514
481
|
chat_mode = config.flags.chat == true
|
|
515
482
|
fuzzy_mode = config.flags.fuzzy == true
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
def configure_prompt_manager(config)
|
|
523
|
-
# PM v1.0.0 uses ERB parameters (<%= param %>) instead of regex-based extraction.
|
|
524
|
-
# parameter_regex is deprecated and ignored.
|
|
525
|
-
if config.prompts.parameter_regex
|
|
526
|
-
warn "Warning: --regex / parameter_regex is deprecated. PM v1.0.0 uses ERB parameters (<%= param %>)."
|
|
527
|
-
end
|
|
483
|
+
no_prompt = config.prompt_id.nil? || config.prompt_id.empty?
|
|
484
|
+
no_context = config.context_files.nil? || config.context_files.empty?
|
|
485
|
+
return unless !chat_mode && !fuzzy_mode && no_prompt && no_context
|
|
486
|
+
raise AIA::ConfigurationError,
|
|
487
|
+
"A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
|
|
528
488
|
end
|
|
529
489
|
|
|
530
490
|
def prepare_pipeline(config)
|
|
531
|
-
|
|
491
|
+
prompt_id = config.prompt_id
|
|
492
|
+
return if prompt_id.nil? || prompt_id.empty? || prompt_id == config.pipeline.first
|
|
532
493
|
|
|
533
|
-
config.pipeline.prepend(
|
|
494
|
+
config.pipeline.prepend(prompt_id)
|
|
534
495
|
end
|
|
535
496
|
|
|
536
497
|
def validate_pipeline_prompts(config)
|
|
@@ -543,24 +504,19 @@ module AIA
|
|
|
543
504
|
|
|
544
505
|
prompt_file_path = File.join(config.prompts.dir, "#{prompt_id}#{config.prompts.extname}")
|
|
545
506
|
unless File.exist?(prompt_file_path)
|
|
546
|
-
|
|
507
|
+
$stderr.puts "Error: Prompt ID '#{prompt_id}' does not exist at #{prompt_file_path}"
|
|
547
508
|
and_exit = true
|
|
548
509
|
end
|
|
549
510
|
end
|
|
550
511
|
|
|
551
|
-
|
|
512
|
+
raise AIA::ConfigurationError, "One or more prompt IDs do not exist." if and_exit
|
|
552
513
|
end
|
|
553
514
|
|
|
554
|
-
# Dump configuration to file
|
|
555
|
-
#
|
|
556
|
-
# @param config [AIA::Config] the configuration to dump
|
|
557
|
-
# @param file [String] the file path to dump to
|
|
558
515
|
def dump_config(config, file)
|
|
559
516
|
ext = File.extname(file).downcase
|
|
560
517
|
|
|
561
518
|
config_hash = config.to_h
|
|
562
519
|
|
|
563
|
-
# Remove runtime keys
|
|
564
520
|
config_hash.delete(:prompt_id)
|
|
565
521
|
config_hash.delete(:dump_file)
|
|
566
522
|
|
|
@@ -575,7 +531,6 @@ module AIA
|
|
|
575
531
|
File.write(file, content)
|
|
576
532
|
puts "Config successfully dumped to #{file}"
|
|
577
533
|
end
|
|
578
|
-
|
|
579
534
|
end
|
|
580
535
|
end
|
|
581
536
|
end
|