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
@@ -2,45 +2,46 @@
2
2
 
3
3
  module AIA
4
4
  class ConfigurationDirectives < Directive
5
+ state_setting! :model
6
+
5
7
  desc "View or set configuration values"
6
8
  def config(args = [], context_manager = nil)
7
9
  args = Array(args)
8
10
 
9
11
  if args.empty?
10
12
  ap AIA.config.to_h
11
- ""
12
13
  elsif args.length == 1
13
14
  config_item = args.first
14
15
  local_cfg = {}
15
- if AIA.config.respond_to?(config_item)
16
- local_cfg[config_item] = AIA.config.send(config_item)
17
- else
18
- local_cfg[config_item] = nil
19
- end
16
+ local_cfg[config_item] = if AIA.config.respond_to?(config_item)
17
+ AIA.config.send(config_item)
18
+ end
20
19
  ap local_cfg
21
- ""
22
20
  else
23
21
  config_item = args.shift
24
- boolean = AIA.respond_to?("#{config_item}?")
25
- new_value = args.join(' ').gsub('=', '').strip
22
+ new_value = args.join(' ').gsub('=', '').strip
26
23
 
24
+ # Resolve the leaf key name regardless of dot-notation (flags.debug → debug)
25
+ leaf_key = config_item.to_s.split('.').last
26
+
27
+ boolean = AIA.respond_to?("#{leaf_key}?")
27
28
  if boolean
28
29
  new_value = %w[true t yes y on 1 yea yeah yep yup].include?(new_value.downcase)
29
30
  end
30
31
 
31
- setter = "#{config_item}="
32
- if AIA.config.respond_to?(setter)
33
- AIA.config.send(setter, new_value)
32
+ if write_config_value(config_item, new_value)
33
+ AIA::LoggerManager.reconfigure_levels!
34
34
  else
35
- warn "Warning: Unknown config option '#{config_item}'"
35
+ $stderr.puts "Warning: Unknown config option '#{config_item}'"
36
36
  AIA::LoggerManager.aia_logger.warn("Unknown config option '#{config_item}'")
37
37
  end
38
- ""
39
38
  end
39
+ ""
40
40
  end
41
- alias_method :cfg, :config
41
+ alias cfg config
42
42
 
43
43
  desc "View or change the AI model"
44
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
44
45
  def model(args, context_manager = nil)
45
46
  if args.empty?
46
47
  puts
@@ -49,7 +50,13 @@ module AIA
49
50
  if models.size == 1
50
51
  puts "Current Model:"
51
52
  puts "=============="
52
- puts AIA.client.model.to_h.pretty_inspect
53
+ model_name = models.first.respond_to?(:name) ? models.first.name : models.first.to_s
54
+ begin
55
+ model_info = RubyLLM::Models.find(model_name)
56
+ puts model_info.to_h.pretty_inspect if model_info
57
+ rescue StandardError
58
+ puts " #{model_name}"
59
+ end
53
60
  else
54
61
  puts "Multi-Model Configuration:"
55
62
  puts "=========================="
@@ -64,7 +71,7 @@ module AIA
64
71
 
65
72
  models.each_with_index do |model_spec, index|
66
73
  model_name = model_spec.respond_to?(:name) ? model_spec.name : model_spec.to_s
67
- puts "#{index + 1}. #{model_name}#{index == 0 ? ' (primary)' : ''}"
74
+ puts "#{index + 1}. #{model_name}#{' (primary)' if index.zero?}"
68
75
 
69
76
  begin
70
77
  model_info = RubyLLM::Models.find(model_name)
@@ -88,22 +95,155 @@ module AIA
88
95
  else
89
96
  model_names = args.join(' ').split(',').map(&:strip).reject(&:empty?)
90
97
  AIA.config.models = AIA::Config::TO_MODEL_SPECS.call(model_names)
91
- AIA.client = AIA.client.class.new
98
+ AIA.client = RobotFactory.rebuild(AIA.config)
92
99
  end
93
100
 
94
101
  ''
95
102
  end
103
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
96
104
 
97
105
  desc "Set the temperature parameter for AI responses"
98
106
  def temperature(args, context_manager = nil)
99
107
  config(args.prepend('temperature'), context_manager)
100
108
  end
101
- alias_method :temp, :temperature
109
+ alias temp temperature
102
110
 
103
111
  desc "Set the top_p parameter for AI responses"
104
112
  def top_p(args, context_manager = nil)
105
113
  config(args.prepend('top_p'), context_manager)
106
114
  end
107
- alias_method :topp, :top_p
115
+ alias topp top_p
116
+
117
+ desc "Dump session cost/token metrics as CSV"
118
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
119
+ def cost(args = [], context_manager = nil)
120
+ tracker = AIA.session_tracker
121
+ unless tracker
122
+ puts "No session tracker available."
123
+ return ''
124
+ end
125
+
126
+ turns = tracker.turns.reject { |t| t[:type] == :model_switch }
127
+ if turns.empty?
128
+ puts "No turns recorded yet."
129
+ return ''
130
+ end
131
+
132
+ has_similarity = turns.any? { |t| t.key?(:similarity) }
133
+
134
+ header = %w[model input_tokens output_tokens total_tokens cost elapsed]
135
+ header << 'similarity' if has_similarity
136
+ lines = [header.join(',')]
137
+
138
+ turns.each do |turn|
139
+ input_t = turn[:input_tokens] || 0
140
+ output_t = turn[:output_tokens] || 0
141
+ total_t = input_t + output_t
142
+ cost_val = turn[:cost] || 0.0
143
+ elapsed = turn[:elapsed] || 0
144
+
145
+ row = [
146
+ turn[:model],
147
+ input_t,
148
+ output_t,
149
+ total_t,
150
+ "$#{'%.5f' % cost_val}",
151
+ "%.1fs" % elapsed
152
+ ]
153
+
154
+ if has_similarity
155
+ sim = turn[:similarity]
156
+ row << (sim.nil? ? 'ref' : "%.1f%%" % (sim * 100))
157
+ end
158
+
159
+ lines << row.join(',')
160
+ end
161
+
162
+ # Totals
163
+ total_input = turns.sum { |t| t[:input_tokens] || 0 }
164
+ total_output = turns.sum { |t| t[:output_tokens] || 0 }
165
+ total_tokens = total_input + total_output
166
+ total_cost = turns.sum { |t| t[:cost] || 0.0 }
167
+ max_elapsed = turns.map { |t| t[:elapsed] || 0 }.max
168
+
169
+ totals = [
170
+ 'TOTAL',
171
+ total_input,
172
+ total_output,
173
+ total_tokens,
174
+ "$#{'%.5f' % total_cost}",
175
+ "%.1fs" % max_elapsed
176
+ ]
177
+ totals << '' if has_similarity
178
+ lines << totals.join(',')
179
+
180
+ csv = lines.join("\n")
181
+ puts
182
+ puts csv
183
+ puts
184
+
185
+ out_file = AIA.config.output&.file
186
+ File.open(out_file, 'a') { |f| f.puts csv } if out_file
187
+
188
+ ''
189
+ end
190
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
191
+
192
+ private
193
+
194
+ # Apply a config value by key, supporting three forms:
195
+ # flat CLI key — "debug" → CLI_TO_NESTED_MAP routing
196
+ # dot-notation — "flags.debug" → navigate section.subkey
197
+ # direct setter — "log_level_override" → AIA.config.<key>=
198
+ #
199
+ # Returns true if the value was set, false if the key was unknown.
200
+ def write_config_value(config_item, new_value) # rubocop:disable Naming/PredicateMethod
201
+ key_str = config_item.to_s
202
+
203
+ # Dot-notation: walk the config object by path segments
204
+ if key_str.include?('.')
205
+ parts = key_str.split('.')
206
+ obj = AIA.config
207
+ parents = parts[0..-2]
208
+ leaf = parts.last
209
+
210
+ parents.each do |part|
211
+ obj = obj.respond_to?(part) ? obj.send(part) : nil
212
+ return false if obj.nil?
213
+ end
214
+
215
+ if obj.respond_to?("#{leaf}=")
216
+ obj.send("#{leaf}=", new_value)
217
+ maybe_clear_log_override(leaf, new_value)
218
+ return true
219
+ end
220
+ return false
221
+ end
222
+
223
+ # Flat CLI key via CLI_TO_NESTED_MAP (e.g. "debug" → flags.debug)
224
+ if AIA::Config::CLI_TO_NESTED_MAP.key?(key_str.to_sym) &&
225
+ AIA.config.respond_to?(:apply_overrides)
226
+ AIA.config.apply_overrides({ key_str.to_sym => new_value })
227
+ maybe_clear_log_override(key_str, new_value)
228
+ return true
229
+ end
230
+
231
+ # Direct setter on the config object (e.g. "log_level_override")
232
+ if AIA.config.respond_to?("#{key_str}=")
233
+ AIA.config.send("#{key_str}=", new_value)
234
+ return true
235
+ end
236
+
237
+ false
238
+ end
239
+
240
+ # When debug is disabled, clear log_level_override so the logger
241
+ # drops back to its configured level rather than staying at DEBUG.
242
+ def maybe_clear_log_override(key, value)
243
+ return unless key.to_s == 'debug' && value == false
244
+ return unless AIA.config.respond_to?(:log_level_override=)
245
+
246
+ AIA.config.log_level_override = nil if AIA.config.log_level_override.to_s == 'debug'
247
+ end
108
248
  end
109
249
  end
@@ -5,6 +5,8 @@
5
5
 
6
6
  module AIA
7
7
  class ContextDirectives < Directive
8
+ state_setting! :clear, :checkpoint, :ckp, :cp, :restore, :review, :context
9
+
8
10
  attr_accessor :checkpoint_store, :checkpoint_counter, :last_checkpoint_name
9
11
 
10
12
  def initialize
@@ -32,9 +34,9 @@ module AIA
32
34
 
33
35
  first_chat_messages = chats.values.first&.messages || []
34
36
  @checkpoint_store[name] = {
35
- messages: chats.transform_values { |chat|
37
+ messages: chats.transform_values do |chat|
36
38
  chat.messages.map { |msg| deep_copy_message(msg) }
37
- },
39
+ end,
38
40
  position: first_chat_messages.size,
39
41
  created_at: Time.now,
40
42
  topic_preview: extract_last_user_message(first_chat_messages)
@@ -44,8 +46,8 @@ module AIA
44
46
  puts "Checkpoint '#{name}' created at position #{@checkpoint_store[name][:position]}."
45
47
  ""
46
48
  end
47
- alias_method :ckp, :checkpoint
48
- alias_method :cp, :checkpoint
49
+ alias ckp checkpoint
50
+ alias cp checkpoint
49
51
 
50
52
  desc "Restore context to a previous checkpoint"
51
53
  def restore(args, _unused = nil)
@@ -64,16 +66,16 @@ module AIA
64
66
  end
65
67
 
66
68
  checkpoint_data = @checkpoint_store[name]
67
- chats = get_chats
69
+ robots = get_robots
68
70
 
69
- return "Error: No active chat sessions found." if chats.nil? || chats.empty?
71
+ return "Error: No active chat sessions found." if robots.nil? || robots.empty?
70
72
 
71
73
  checkpoint_data[:messages].each do |model_id, saved_messages|
72
- chat = chats[model_id]
73
- next unless chat
74
+ robot = robots[model_id]
75
+ next unless robot
74
76
 
75
77
  restored_messages = saved_messages.map { |msg| deep_copy_message(msg) }
76
- chat.instance_variable_set(:@messages, restored_messages)
78
+ robot.replace_messages(restored_messages)
77
79
  end
78
80
 
79
81
  restored_position = checkpoint_data[:position]
@@ -82,7 +84,7 @@ module AIA
82
84
  @last_checkpoint_name = name
83
85
 
84
86
  msg = "Context restored to checkpoint '#{name}' (position #{restored_position})."
85
- msg += " Removed #{removed_count} checkpoint(s) that were beyond this position." if removed_count > 0
87
+ msg += " Removed #{removed_count} checkpoint(s) that were beyond this position." if removed_count.positive?
86
88
  msg
87
89
  end
88
90
 
@@ -90,17 +92,11 @@ module AIA
90
92
  def clear(args, _unused = nil)
91
93
  keep_system = !args.include?('--all')
92
94
 
93
- chats = get_chats
94
- return "Error: No active chat sessions found." if chats.nil? || chats.empty?
95
+ robots = get_robots
96
+ return "Error: No active chat sessions found." if robots.nil? || robots.empty?
95
97
 
96
- chats.each do |_model_id, chat|
97
- if keep_system
98
- system_msg = chat.messages.find { |m| m.role == :system }
99
- chat.instance_variable_set(:@messages, [])
100
- chat.add_message(system_msg) if system_msg
101
- else
102
- chat.instance_variable_set(:@messages, [])
103
- end
98
+ robots.each_value do |robot|
99
+ robot.clear_messages(keep_system: keep_system)
104
100
  end
105
101
 
106
102
  @checkpoint_store.clear
@@ -111,6 +107,7 @@ module AIA
111
107
  end
112
108
 
113
109
  desc "Display the current conversation context with checkpoint markers"
110
+ # rubocop:disable Metrics/AbcSize
114
111
  def review(args, _unused = nil)
115
112
  chats = get_chats
116
113
  return "Error: No active chat sessions found." if chats.nil? || chats.empty?
@@ -147,7 +144,8 @@ module AIA
147
144
  puts "=== End of Context ==="
148
145
  ""
149
146
  end
150
- alias_method :context, :review
147
+ # rubocop:enable Metrics/AbcSize
148
+ alias context review
151
149
 
152
150
  desc "List all available checkpoints"
153
151
  def checkpoints_list(args, _unused = nil)
@@ -167,7 +165,7 @@ module AIA
167
165
  puts "=== End of Checkpoints ==="
168
166
  ""
169
167
  end
170
- alias_method :checkpoints, :checkpoints_list
168
+ alias checkpoints checkpoints_list
171
169
 
172
170
  # --- helpers (no desc → not registered as directives) ---
173
171
 
@@ -200,9 +198,23 @@ module AIA
200
198
 
201
199
  private
202
200
 
201
+ # In v2, AIA.client is a RobotLab::Robot which wraps a RubyLLM::Chat.
202
+ # Returns a hash of { name => Robot } for checkpoint operations.
203
+ def get_robots
204
+ robot = AIA.client
205
+ return nil unless robot
206
+
207
+ name = robot.respond_to?(:name) ? robot.name : "default"
208
+ { name => robot }
209
+ end
210
+
211
+ # Return { name => RubyLLM::Chat } for operations that need direct chat access.
212
+ # Uses Robot#chat public API.
203
213
  def get_chats
204
- return nil unless AIA.client.respond_to?(:chats)
205
- AIA.client.chats
214
+ robots = get_robots
215
+ return nil unless robots
216
+
217
+ robots.transform_values(&:chat)
206
218
  end
207
219
 
208
220
  def deep_copy_message(msg)
@@ -233,12 +245,12 @@ module AIA
233
245
  def extract_last_user_message(messages, max_length: 70)
234
246
  return "" if messages.nil? || messages.empty?
235
247
 
236
- last_user_msg = messages.reverse.find { |msg| msg.role == :user }
248
+ last_user_msg = messages.rfind { |msg| msg.role == :user }
237
249
  return "" unless last_user_msg
238
250
 
239
251
  content = last_user_msg.content.to_s.strip
240
252
  content = content.gsub(/\s+/, ' ')
241
- content.length > max_length ? "#{content[0..max_length - 4]}..." : content
253
+ content.length > max_length ? "#{content[0..(max_length - 4)]}..." : content
242
254
  end
243
255
  end
244
256
  end
@@ -1,15 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/aia/directives/execution_directives.rb
2
4
 
3
5
  require 'shellwords'
4
6
 
5
7
  module AIA
6
8
  class ExecutionDirectives < Directive
7
- desc "Execute Ruby code"
9
+ state_setting! :concurrent, :conc, :verify, :decompose, :debate,
10
+ :delegate, :del, :spawn, :orchestrate, :orch,
11
+ :add_recruit, :add, :drop_recruit, :drop, :reskill
12
+
13
+ desc "Execute Ruby code (requires allow_ruby_eval: true in config)"
8
14
  def ruby(args, context_manager = nil)
15
+ unless AIA.config.flags.allow_ruby_eval
16
+ return "ERROR: /ruby is disabled. Set `allow_ruby_eval: true` in " \
17
+ "~/.aia/config.yml or pass --allow-ruby-eval to enable it. " \
18
+ "Note: this executes arbitrary Ruby with full process privileges."
19
+ end
20
+
9
21
  ruby_code = args.join(' ')
22
+ $stderr.puts "WARNING: /ruby executing: #{ruby_code}"
10
23
 
11
24
  begin
12
- String(eval(ruby_code))
25
+ String(eval(ruby_code)) # rubocop:disable Security/Eval
13
26
  rescue StandardError => e
14
27
  <<~ERROR
15
28
  This ruby code failed: #{ruby_code}
@@ -17,12 +30,131 @@ module AIA
17
30
  ERROR
18
31
  end
19
32
  end
20
- alias_method :rb, :ruby
33
+ alias rb ruby
21
34
 
22
35
  desc "Use text-to-speech to speak the text"
23
36
  def say(args, context_manager = nil)
24
- system('say', *args)
37
+ audio = AIA.config.audio
38
+ env = {}
39
+ env['SPEECH_MODEL'] = audio.speech_model if audio.speech_model
40
+ if audio.voice && !audio.voice.strip.empty?
41
+ system(env, 'say', '-v', audio.voice, *args)
42
+ else
43
+ system(env, 'say', *args)
44
+ end
25
45
  ""
26
46
  end
47
+
48
+ desc "Execute next prompt with concurrent MCP server access"
49
+ def concurrent(args, context_manager = nil)
50
+ AIA.turn_state.force_concurrent_mcp = true
51
+ "Concurrent MCP mode enabled for next prompt."
52
+ end
53
+ alias conc concurrent
54
+
55
+ desc "Run next prompt through verification (two independent answers + reconciliation)"
56
+ def verify(args, context_manager = nil)
57
+ AIA.turn_state.force_verify = true
58
+ "Verification mode enabled for next prompt."
59
+ end
60
+
61
+ desc "Decompose next prompt into parallel sub-tasks"
62
+ def decompose(args, context_manager = nil)
63
+ AIA.turn_state.force_decompose = true
64
+ "Decomposition mode enabled for next prompt."
65
+ end
66
+
67
+ desc "Multi-round debate between robots in the network"
68
+ def debate(args, context_manager = nil)
69
+ AIA.turn_state.force_debate = true
70
+ "Debate mode enabled for next prompt."
71
+ end
72
+
73
+ desc "Delegate subtasks to specific robots via TrakFlow plan"
74
+ def delegate(args, context_manager = nil)
75
+ AIA.turn_state.force_delegate = true
76
+ "Delegation mode enabled for next prompt."
77
+ end
78
+ alias del delegate
79
+
80
+ desc "Spawn a specialist robot for the next prompt. " \
81
+ "Usage: /spawn | /spawn <type> | /spawn <name> <provider/model> <system prompt>"
82
+ def spawn(args, context_manager = nil)
83
+ AIA.turn_state.force_spawn = true
84
+
85
+ if args.size >= 2
86
+ spec = AIA::SpawnSpecParser.parse(args)
87
+ AIA.turn_state.spawn_spec = spec
88
+ AIA.turn_state.spawn_type = nil
89
+ "Spawn mode enabled: '#{spec[:name]}' (#{spawn_model_summary(spec)}) for next prompt."
90
+ else
91
+ AIA.turn_state.spawn_spec = nil
92
+ AIA.turn_state.spawn_type = args.first
93
+ type_msg = args.first ? " (#{args.first})" : " (auto-detect)"
94
+ "Spawn mode enabled#{type_msg} for next prompt."
95
+ end
96
+ end
97
+
98
+ desc "Recruit a robot into the crew (persists, @mention-able). Usage: " \
99
+ "/add_recruit <name> [provider/model] [skill:<id>...] [system prompt] (alias: /add)"
100
+ def add_recruit(args, context_manager = nil)
101
+ return "Usage: /add_recruit <name> [provider/model] [skill:<id> ...] [system prompt]" if args.empty?
102
+
103
+ spec = args.size >= 2 ? AIA::SpawnSpecParser.parse(args) : { name: args[0] }
104
+ robot = AIA::Crew.recruit(spec)
105
+ "Recruited '#{robot.name}' into the crew (#{recruit_summary(spec)})."
106
+ rescue AIA::CrewError => e
107
+ "Recruit failed: #{e.message}"
108
+ end
109
+ alias add add_recruit
110
+
111
+ desc "Drop a robot from the crew. Usage: /drop_recruit <name> (alias: /drop)"
112
+ def drop_recruit(args, context_manager = nil)
113
+ return "Usage: /drop_recruit <name>" if args.empty?
114
+
115
+ AIA::Crew.drop(args.first)
116
+ "Dropped '#{args.first}' from the crew."
117
+ rescue AIA::CrewError => e
118
+ "Drop failed: #{e.message}"
119
+ end
120
+ alias drop drop_recruit
121
+
122
+ desc "Reset a crew member to a clean slate and re-skill it. " \
123
+ "Usage: /reskill <name> [skill:<id>...] [system prompt]"
124
+ def reskill(args, context_manager = nil)
125
+ return "Usage: /reskill <name> [skill:<id> ...] [system prompt]" if args.empty?
126
+
127
+ skills, rest = AIA::SpawnSpecParser.extract_skills(args[1..].to_a)
128
+ prompt = rest.join(' ').strip
129
+ robot = AIA::Crew.reskill(args.first, skills: skills, system_prompt: (prompt.empty? ? nil : prompt))
130
+ suffix = skills.empty? ? '' : " with #{skills.join(', ')}"
131
+ "Reskilled '#{robot.name}'#{suffix} (clean slate)."
132
+ rescue AIA::CrewError => e
133
+ "Reskill failed: #{e.message}"
134
+ end
135
+
136
+ desc "3-tier layered orchestration: orchestrator → lead agents → specialists"
137
+ def orchestrate(args, context_manager = nil)
138
+ AIA.turn_state.force_orchestrate = true
139
+ "Orchestration mode enabled. Your next prompt is the application requirements."
140
+ end
141
+ alias orch orchestrate
142
+
143
+ private
144
+
145
+ # Human-readable model description for the /spawn confirmation message.
146
+ def spawn_model_summary(spec)
147
+ return 'inherited model' unless spec[:model]
148
+
149
+ spec[:provider] ? "#{spec[:provider]}/#{spec[:model]}" : spec[:model]
150
+ end
151
+
152
+ # Model plus any assigned skills, for the /add_recruit confirmation message.
153
+ def recruit_summary(spec)
154
+ parts = [spawn_model_summary(spec)]
155
+ skills = Array(spec[:skills])
156
+ parts << "skills: #{skills.join(', ')}" unless skills.empty?
157
+ parts.join('; ')
158
+ end
27
159
  end
28
160
  end