aia 1.1.1 → 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 (169) 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 +259 -50
  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 +18 -6
  55. data/examples/context/tech_stack.md +2 -2
  56. data/examples/prompts_dir/roles/orchestrator.md +21 -0
  57. data/examples/requirements/sinatra_taskflow_app.md +139 -0
  58. data/examples/rules/01_classify_ruby.rb +16 -0
  59. data/examples/rules/02_prefer_claude_for_code.rb +19 -0
  60. data/examples/rules/03_gate_prompt_length.rb +19 -0
  61. data/examples/rules/04_tool_selection.rb +41 -0
  62. data/examples/rules/README.md +30 -0
  63. data/examples/run_all.sh +48 -15
  64. data/examples/tools/word_count_tool.rb +1 -1
  65. data/lib/AGENTS.md +57 -0
  66. data/lib/aia/chat_loop.rb +304 -164
  67. data/lib/aia/config/cli_parser.rb +174 -111
  68. data/lib/aia/config/defaults.yml +62 -33
  69. data/lib/aia/config/mcp_parser.rb +39 -46
  70. data/lib/aia/config/model_spec.rb +34 -2
  71. data/lib/aia/config/validator.rb +108 -142
  72. data/lib/aia/config.rb +110 -145
  73. data/lib/aia/content_extractor.rb +153 -0
  74. data/lib/aia/cost_calculator.rb +38 -0
  75. data/lib/aia/crew.rb +164 -0
  76. data/lib/aia/debate_handler.rb +166 -0
  77. data/lib/aia/delegate_handler.rb +112 -0
  78. data/lib/aia/directive.rb +33 -18
  79. data/lib/aia/directive_processor.rb +16 -7
  80. data/lib/aia/directives/configuration_directives.rb +160 -20
  81. data/lib/aia/directives/context_directives.rb +38 -26
  82. data/lib/aia/directives/execution_directives.rb +136 -4
  83. data/lib/aia/directives/model_directives.rb +76 -34
  84. data/lib/aia/directives/trakflow_directives.rb +44 -0
  85. data/lib/aia/directives/utility_directives.rb +203 -6
  86. data/lib/aia/directives/web_and_file_directives.rb +96 -60
  87. data/lib/aia/errors.rb +15 -0
  88. data/lib/aia/fact_asserter.rb +27 -0
  89. data/lib/aia/fzf.rb +9 -31
  90. data/lib/aia/handler_context.rb +17 -0
  91. data/lib/aia/handler_protocol.rb +19 -0
  92. data/lib/aia/history_transfer.rb +55 -0
  93. data/lib/aia/input_collector.rb +3 -3
  94. data/lib/aia/layered_orchestrator.rb +448 -0
  95. data/lib/aia/logger.rb +24 -4
  96. data/lib/aia/mcp_config_normalizer.rb +35 -0
  97. data/lib/aia/mcp_connection_manager.rb +305 -0
  98. data/lib/aia/mcp_discovery.rb +44 -0
  99. data/lib/aia/mcp_grouper.rb +33 -0
  100. data/lib/aia/mcp_utility.rb +57 -0
  101. data/lib/aia/mention_router.rb +260 -0
  102. data/lib/aia/model_alias_registry.rb +97 -0
  103. data/lib/aia/model_switch_handler.rb +100 -0
  104. data/lib/aia/network_builder.rb +155 -0
  105. data/lib/aia/network_memory_manager.rb +55 -0
  106. data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
  107. data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
  108. data/lib/aia/pipeline_orchestrator.rb +262 -0
  109. data/lib/aia/plugin_loader.rb +170 -0
  110. data/lib/aia/plugin_monitor.rb +208 -0
  111. data/lib/aia/prompt_decomposer.rb +157 -0
  112. data/lib/aia/prompt_handler.rb +19 -39
  113. data/lib/aia/robot_builder.rb +51 -0
  114. data/lib/aia/robot_factory.rb +334 -0
  115. data/lib/aia/robot_namer.rb +116 -0
  116. data/lib/aia/session.rb +83 -17
  117. data/lib/aia/session_tracker.rb +209 -0
  118. data/lib/aia/similarity_scorer.rb +39 -0
  119. data/lib/aia/skill_utils.rb +105 -1
  120. data/lib/aia/spawn_handler.rb +129 -0
  121. data/lib/aia/spawn_spec_parser.rb +65 -0
  122. data/lib/aia/special_mode_handler.rb +302 -0
  123. data/lib/aia/startup_coordinator.rb +150 -0
  124. data/lib/aia/streaming_runner.rb +169 -0
  125. data/lib/aia/system_prompt_assembler.rb +88 -0
  126. data/lib/aia/task_coordinator.rb +202 -0
  127. data/lib/aia/task_decomposer.rb +57 -0
  128. data/lib/aia/task_executor.rb +51 -0
  129. data/lib/aia/tfidf_math.rb +27 -0
  130. data/lib/aia/tool_filter/tfidf.rb +116 -0
  131. data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
  132. data/lib/aia/tool_filter.rb +82 -0
  133. data/lib/aia/tool_filter_registry.rb +30 -0
  134. data/lib/aia/tool_filter_strategy.rb +143 -0
  135. data/lib/aia/tool_loader.rb +210 -0
  136. data/lib/aia/tool_utility.rb +30 -0
  137. data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
  138. data/lib/aia/tools/recruit_robot_tool.rb +60 -0
  139. data/lib/aia/tools/reskill_robot_tool.rb +44 -0
  140. data/lib/aia/tools/task_board_tool.rb +114 -0
  141. data/lib/aia/trakflow_bridge.rb +173 -0
  142. data/lib/aia/turn_state.rb +94 -0
  143. data/lib/aia/ui_presenter.rb +166 -198
  144. data/lib/aia/utility.rb +134 -87
  145. data/lib/aia/{history_manager.rb → variable_input_collector.rb} +8 -9
  146. data/lib/aia/verification_network.rb +58 -0
  147. data/lib/aia.rb +108 -63
  148. data/mkdocs.yml +1 -0
  149. metadata +179 -56
  150. data/justfile +0 -215
  151. data/lib/aia/adapter/chat_execution.rb +0 -242
  152. data/lib/aia/adapter/error_handler.rb +0 -68
  153. data/lib/aia/adapter/gem_activator.rb +0 -57
  154. data/lib/aia/adapter/mcp_connector.rb +0 -274
  155. data/lib/aia/adapter/modality_handlers.rb +0 -167
  156. data/lib/aia/adapter/model_registry.rb +0 -81
  157. data/lib/aia/adapter/multi_model_chat.rb +0 -218
  158. data/lib/aia/adapter/provider_configurator.rb +0 -59
  159. data/lib/aia/adapter/tool_filter.rb +0 -85
  160. data/lib/aia/adapter/tool_loader.rb +0 -90
  161. data/lib/aia/chat_processor_service.rb +0 -178
  162. data/lib/aia/prompt_pipeline.rb +0 -183
  163. data/lib/aia/ruby_llm_adapter.rb +0 -95
  164. data/lib/extensions/openstruct_merge.rb +0 -48
  165. data/lib/extensions/ruby_llm/.irbrc +0 -56
  166. data/lib/extensions/ruby_llm/modalities.rb +0 -36
  167. data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
  168. data/lib/refinements/string.rb +0 -16
  169. 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,8 +24,7 @@ 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
- # Require tool files early so AIA::Directive subclasses defined in them
28
- # are registered into PM.directives before PromptHandler.new is called.
27
+ # Tool files may define AIA::Directive subclasses used by PromptManager.
29
28
  require_tool_files(config)
30
29
 
31
30
  # Process arguments and validate
@@ -33,30 +32,34 @@ module AIA
33
32
  validate_and_set_context_files(config, remaining_args)
34
33
  handle_executable_prompt(config)
35
34
  handle_stdin_as_prompt(config)
36
- handle_dump_config(config)
37
- handle_mcp_list(config)
38
- handle_list_tools(config)
39
- handle_list_skills(config)
40
- handle_completion_script(config)
35
+ return :early_exit if handle_early_exits(config) == :early_exit
41
36
  validate_required_prompt_id(config)
42
37
  process_role_configuration(config)
38
+ validate_plugins_dir(config)
43
39
  handle_fuzzy_search_prompt_id(config)
44
40
  normalize_boolean_flags(config)
45
41
  validate_final_prompt_requirements(config)
46
- configure_prompt_manager(config)
47
42
  prepare_pipeline(config)
48
43
  validate_pipeline_prompts(config)
49
44
 
50
45
  config
51
46
  end
52
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
+
53
56
  def process_stdin_content
54
57
  stdin_content = String.new
55
58
 
56
- if !STDIN.tty? && !STDIN.closed?
59
+ if !$stdin.tty? && !$stdin.closed?
57
60
  begin
58
- stdin_content << "\n" + STDIN.read
59
- STDIN.reopen('/dev/tty')
61
+ stdin_content << ("\n" + $stdin.read)
62
+ $stdin.reopen('/dev/tty')
60
63
  rescue => _
61
64
  # If we can't reopen, continue without error
62
65
  end
@@ -71,9 +74,8 @@ module AIA
71
74
  maybe_id = remaining_args.first
72
75
  maybe_id_plus = File.join(config.prompts.dir, maybe_id + config.prompts.extname)
73
76
 
74
- if AIA.bad_file?(maybe_id) && AIA.good_file?(maybe_id_plus)
75
- config.prompt_id = remaining_args.shift
76
- end
77
+ return unless AIA.bad_file?(maybe_id) && AIA.good_file?(maybe_id_plus)
78
+ config.prompt_id = remaining_args.shift
77
79
  end
78
80
 
79
81
  def validate_and_set_context_files(config, remaining_args)
@@ -81,8 +83,16 @@ module AIA
81
83
 
82
84
  bad_files = remaining_args.reject { |filename| AIA.good_file?(filename) }
83
85
  if bad_files.any?
84
- warn "Error: The following files do not exist: #{bad_files.join(', ')}"
85
- 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(', ')}"
86
96
  end
87
97
 
88
98
  config.context_files ||= []
@@ -90,7 +100,6 @@ module AIA
90
100
  end
91
101
 
92
102
  def handle_executable_prompt(config)
93
- # Auto-detect: no prompt_id, first context_file starts with shebang
94
103
  return unless config.prompt_id.nil?
95
104
  return unless config.context_files && !config.context_files.empty?
96
105
 
@@ -100,7 +109,6 @@ module AIA
100
109
  first_line = File.open(candidate, &:readline).strip rescue nil
101
110
  return unless first_line&.start_with?('#!')
102
111
 
103
- # This is an executable prompt — the file content IS the prompt
104
112
  config.context_files.shift
105
113
  config.executable_prompt_content = File.read(candidate).lines[1..].join
106
114
  config.prompt_id = '__EXECUTABLE_PROMPT__'
@@ -112,21 +120,20 @@ module AIA
112
120
 
113
121
  content = config.stdin_content
114
122
 
115
- # Strip shebang line if present (e.g., piped from an executable prompt)
116
123
  if content.lines.first&.strip&.start_with?('#!')
117
124
  content = content.lines[1..].join
118
125
  end
119
126
 
120
127
  config.executable_prompt_content = content
121
- config.stdin_content = nil # prevent double-processing in build_prompt_text
128
+ config.stdin_content = nil
122
129
  config.prompt_id = '__EXECUTABLE_PROMPT__'
123
130
  end
124
131
 
125
132
  def validate_required_prompt_id(config)
126
- 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
127
134
 
128
- warn "Error: A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
129
- 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."
130
137
  end
131
138
 
132
139
  def process_role_configuration(config)
@@ -134,7 +141,6 @@ module AIA
134
141
  return if role.nil? || role.empty?
135
142
 
136
143
  roles_prefix = config.prompts.roles_prefix
137
-
138
144
  unless AIA::SkillUtils.path_based_id?(role) || roles_prefix.nil? || roles_prefix.empty? || role.start_with?(roles_prefix)
139
145
  config.prompts.role = "#{roles_prefix}/#{role}"
140
146
  role = config.prompts.role
@@ -142,18 +148,21 @@ module AIA
142
148
 
143
149
  config.prompts.roles_dir ||= File.join(config.prompts.dir, roles_prefix.to_s)
144
150
 
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
151
  return if config.flags&.chat == true
149
152
 
150
- if config.prompt_id.nil? || config.prompt_id.empty?
151
- unless role.nil? || role.empty?
152
- config.prompt_id = role
153
- config.pipeline.prepend(config.prompt_id)
154
- config.prompts.role = ''
155
- end
156
- 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}"
157
166
  end
158
167
 
159
168
  def handle_fuzzy_search_prompt_id(config)
@@ -188,12 +197,12 @@ module AIA
188
197
  return unless config.dump_file
189
198
 
190
199
  dump_config(config, config.dump_file)
191
- exit 0
200
+ :early_exit
192
201
  end
193
202
 
194
203
  def handle_mcp_list(config)
195
204
  return unless config.mcp_list
196
- return if config.list_tools # defer to handle_list_tools for combined output
205
+ return if config.list_tools
197
206
 
198
207
  servers = filter_mcp_servers(config)
199
208
 
@@ -203,17 +212,18 @@ module AIA
203
212
  label = mcp_filter_active?(config) ? "Active" : "Configured"
204
213
  puts "#{label} MCP servers:\n\n"
205
214
  servers.each do |server|
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(' ')}"
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(' ')}"
210
220
  puts " #{name}"
211
221
  puts " command: #{command}#{args_str}"
212
222
  puts
213
223
  end
214
224
  end
215
225
 
216
- exit 0
226
+ :early_exit
217
227
  end
218
228
 
219
229
  def handle_list_tools(config)
@@ -228,7 +238,7 @@ module AIA
228
238
 
229
239
  if local_tools.empty? && mcp_tool_groups.empty?
230
240
  $stderr.puts "No tools available."
231
- exit 0
241
+ return :early_exit
232
242
  end
233
243
 
234
244
  if $stdout.tty?
@@ -237,41 +247,7 @@ module AIA
237
247
  list_tools_markdown(local_tools, mcp_tool_groups)
238
248
  end
239
249
 
240
- exit 0
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
250
+ :early_exit
275
251
  end
276
252
 
277
253
  def list_tools_terminal(local_tools, mcp_tool_groups)
@@ -330,25 +306,20 @@ module AIA
330
306
 
331
307
  puts "### `#{name}`"
332
308
  puts
333
- unless desc.empty?
334
- puts nest_markdown_headings(desc, 3)
335
- puts
336
- end
309
+ return if desc.empty?
310
+ puts nest_markdown_headings(desc, 3)
311
+ puts
337
312
  end
338
313
 
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
314
  def nest_markdown_headings(text, parent_level)
344
- text.gsub(/^[ \t]*(\#{1,6})\s/) do |match|
345
- existing = $1
346
- "#" * (existing.length + parent_level) + " "
315
+ text.gsub(/^[ \t]*(\#{1,6})\s/) do |_match|
316
+ existing = ::Regexp.last_match(1)
317
+ ("#" * (existing.length + parent_level)) + " "
347
318
  end
348
319
  end
349
320
 
350
321
  def filter_mcp_servers(config)
351
- servers = config.mcp_servers || []
322
+ servers = config.mcp_servers || []
352
323
  use_list = Array(config.mcp_use)
353
324
  skip_list = Array(config.mcp_skip)
354
325
 
@@ -371,26 +342,32 @@ module AIA
371
342
  if File.exist?(expanded)
372
343
  require expanded
373
344
  else
374
- warn "Warning: Tool file not found: #{path}"
345
+ $stderr.puts "Warning: Tool file not found: #{path}"
375
346
  end
376
347
  rescue LoadError, StandardError => e
377
- warn "Warning: Failed to load tool '#{path}': #{e.message}"
348
+ $stderr.puts "Warning: Failed to load tool '#{path}': #{e.message}"
378
349
  end
379
350
  end
380
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
+
381
362
  def load_local_tools(config)
382
- # Load required libraries (with gem activation and lazy-load triggering)
363
+ # Load required libraries
383
364
  Array(config.require_libs).each do |lib|
384
- begin
385
- Adapter::GemActivator.activate_gem_for_require(lib)
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
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}"
394
371
  end
395
372
 
396
373
  # Load tool files
@@ -399,48 +376,49 @@ module AIA
399
376
  if File.exist?(expanded)
400
377
  require expanded
401
378
  else
402
- warn "Warning: Tool file not found: #{path}"
379
+ $stderr.puts "Warning: Tool file not found: #{path}"
403
380
  end
404
381
  rescue LoadError, StandardError => e
405
- warn "Warning: Failed to load tool '#{path}': #{e.message}"
382
+ $stderr.puts "Warning: Failed to load tool '#{path}': #{e.message}"
406
383
  end
407
384
 
408
385
  # Scan ObjectSpace for RubyLLM::Tool subclasses
409
386
  ObjectSpace.each_object(Class).select do |klass|
410
- next false unless klass < RubyLLM::Tool
387
+ next false unless defined?(RubyLLM::Tool) && klass < RubyLLM::Tool
411
388
 
412
389
  begin
413
390
  klass.new
414
391
  true
415
- rescue ArgumentError, LoadError, StandardError
392
+ rescue StandardError
416
393
  false
417
394
  end
418
395
  end
419
396
  end
420
397
 
421
398
  def first_sentences(text, count)
422
- # Normalize whitespace: collapse newlines and multiple spaces
423
399
  normalized = text.gsub(/\s*\n\s*/, ' ').gsub(/\s{2,}/, ' ').strip
424
400
  sentences = normalized.scan(/[^.!?]*[.!?]/)
425
401
  result = sentences.first(count).join.strip
426
402
  result.empty? ? normalized : result
427
403
  end
428
404
 
405
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
429
406
  def load_mcp_tools_grouped(config)
430
407
  servers = filter_mcp_servers(config)
431
408
  return {} if servers.empty?
432
409
 
433
- # Suppress MCP logger noise during listing
434
410
  quiet_mcp_logger
435
411
 
436
412
  groups = {}
437
413
  default_timeout = 8_000
438
414
 
415
+ # rubocop:disable Metrics/BlockLength
439
416
  servers.each do |server|
440
- name = server[:name] || server['name']
441
- command = server[:command] || server['command']
442
- args = server[:args] || server['args'] || []
443
- 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'] || {}
444
422
 
445
423
  raw_timeout = server[:timeout] || server['timeout'] || default_timeout
446
424
  timeout = raw_timeout.to_i < 1000 ? (raw_timeout.to_i * 1000) : raw_timeout.to_i
@@ -478,26 +456,27 @@ module AIA
478
456
  rescue StandardError => e
479
457
  $stderr.puts " error: #{e.message}"
480
458
  end
459
+ # rubocop:enable Metrics/BlockLength
481
460
  end
461
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
482
462
 
483
463
  groups
484
464
  end
465
+ # rubocop:enable Metrics/ModuleLength
485
466
 
486
467
  def quiet_mcp_logger
487
- if defined?(RubyLLM::MCP) && RubyLLM::MCP.respond_to?(:config)
488
- mcp_config = RubyLLM::MCP.config
489
- if mcp_config.respond_to?(:logger=)
490
- quiet = Logger.new(File::NULL)
491
- mcp_config.logger = quiet
492
- end
493
- 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
494
473
  end
495
474
 
496
475
  def handle_completion_script(config)
497
476
  return unless config.completion
498
477
 
499
478
  generate_completion_script(config.completion)
500
- exit
479
+ :early_exit
501
480
  end
502
481
 
503
482
  def generate_completion_script(shell)
@@ -506,25 +485,18 @@ module AIA
506
485
  if File.exist?(script_path)
507
486
  puts File.read(script_path)
508
487
  else
509
- 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."
510
489
  end
511
490
  end
512
491
 
513
492
  def validate_final_prompt_requirements(config)
514
493
  chat_mode = config.flags.chat == true
515
494
  fuzzy_mode = config.flags.fuzzy == true
516
- if !chat_mode && !fuzzy_mode && (config.prompt_id.nil? || config.prompt_id.empty?) && (config.context_files.nil? || config.context_files.empty?)
517
- warn "Error: A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
518
- exit 1
519
- end
520
- end
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
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."
528
500
  end
529
501
 
530
502
  def prepare_pipeline(config)
@@ -543,24 +515,19 @@ module AIA
543
515
 
544
516
  prompt_file_path = File.join(config.prompts.dir, "#{prompt_id}#{config.prompts.extname}")
545
517
  unless File.exist?(prompt_file_path)
546
- 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}"
547
519
  and_exit = true
548
520
  end
549
521
  end
550
522
 
551
- exit(1) if and_exit
523
+ raise AIA::ConfigurationError, "One or more prompt IDs do not exist." if and_exit
552
524
  end
553
525
 
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
526
  def dump_config(config, file)
559
527
  ext = File.extname(file).downcase
560
528
 
561
529
  config_hash = config.to_h
562
530
 
563
- # Remove runtime keys
564
531
  config_hash.delete(:prompt_id)
565
532
  config_hash.delete(:dump_file)
566
533
 
@@ -575,7 +542,6 @@ module AIA
575
542
  File.write(file, content)
576
543
  puts "Config successfully dumped to #{file}"
577
544
  end
578
-
579
545
  end
580
546
  end
581
547
  end