aia 1.1.0 → 2.0.0.0.pre.alpha

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