aia 2.0.0.0.pre.alpha → 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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +5 -1
  3. data/.loki +3 -223
  4. data/.reek.yml +160 -0
  5. data/.rubocop_strict.yml +15 -0
  6. data/.version +1 -1
  7. data/CHANGELOG.md +10 -5
  8. data/README.md +1 -1
  9. data/Rakefile +0 -113
  10. data/_typos.toml +10 -0
  11. data/aia.gemspec +92 -0
  12. data/config/aia.yml +13 -0
  13. data/lib/aia/chat_loop.rb +8 -52
  14. data/lib/aia/config/cli_parser.rb +66 -57
  15. data/lib/aia/config/mcp_parser.rb +27 -19
  16. data/lib/aia/config/validator.rb +83 -94
  17. data/lib/aia/config.rb +3 -2
  18. data/lib/aia/content_extractor.rb +2 -0
  19. data/lib/aia/cost_calculator.rb +1 -0
  20. data/lib/aia/debate_handler.rb +28 -20
  21. data/lib/aia/delegate_handler.rb +7 -3
  22. data/lib/aia/directive.rb +10 -8
  23. data/lib/aia/directives/configuration_directives.rb +115 -101
  24. data/lib/aia/directives/context_directives.rb +31 -28
  25. data/lib/aia/directives/execution_directives.rb +18 -13
  26. data/lib/aia/directives/model_directives.rb +134 -154
  27. data/lib/aia/directives/trakflow_directives.rb +27 -9
  28. data/lib/aia/directives/utility_directives.rb +71 -71
  29. data/lib/aia/directives/web_and_file_directives.rb +63 -50
  30. data/lib/aia/layered_orchestrator.rb +66 -43
  31. data/lib/aia/logger.rb +21 -21
  32. data/lib/aia/mcp_connection_manager.rb +50 -40
  33. data/lib/aia/mcp_server_config.rb +30 -0
  34. data/lib/aia/mcp_utility.rb +7 -4
  35. data/lib/aia/mention_router.rb +7 -50
  36. data/lib/aia/network_builder.rb +10 -5
  37. data/lib/aia/network_memory_manager.rb +6 -6
  38. data/lib/aia/pipeline_orchestrator.rb +20 -10
  39. data/lib/aia/plugin_monitor.rb +9 -6
  40. data/lib/aia/prompt_decomposer.rb +5 -3
  41. data/lib/aia/prompt_handler.rb +43 -49
  42. data/lib/aia/robot_factory.rb +12 -8
  43. data/lib/aia/robot_namer.rb +2 -8
  44. data/lib/aia/session.rb +7 -3
  45. data/lib/aia/session_tracker.rb +39 -41
  46. data/lib/aia/similarity_scorer.rb +5 -3
  47. data/lib/aia/special_mode_handler.rb +52 -32
  48. data/lib/aia/speech.rb +67 -0
  49. data/lib/aia/startup_coordinator.rb +1 -0
  50. data/lib/aia/streaming_runner.rb +6 -3
  51. data/lib/aia/system_prompt_assembler.rb +11 -7
  52. data/lib/aia/task_coordinator.rb +10 -5
  53. data/lib/aia/timing.rb +15 -0
  54. data/lib/aia/tool_filter.rb +1 -0
  55. data/lib/aia/tool_filter_strategy.rb +28 -25
  56. data/lib/aia/tool_introspection.rb +17 -0
  57. data/lib/aia/tool_loader.rb +12 -6
  58. data/lib/aia/tools/task_board_tool.rb +1 -0
  59. data/lib/aia/trakflow_bridge.rb +5 -3
  60. data/lib/aia/turn_state.rb +1 -0
  61. data/lib/aia/ui_presenter.rb +67 -59
  62. data/lib/aia/utility.rb +5 -3
  63. data/lib/aia/variable_input_collector.rb +1 -0
  64. data/lib/aia/verification_network.rb +1 -2
  65. data/lib/aia.rb +16 -0
  66. metadata +15 -9
  67. data/.quality/flay_baseline.txt +0 -1
  68. data/.quality/flog_baseline.txt +0 -29
  69. data/.quality/reek_baseline.txt +0 -80
@@ -5,16 +5,18 @@ module AIA
5
5
  state_setting! :model
6
6
 
7
7
  desc "View or set configuration values"
8
+ # :reek:TooManyStatements -- view/set dispatch on arg count, coercing booleans and dot-notation keys before writing
8
9
  def config(args = [], context_manager = nil)
9
10
  args = Array(args)
11
+ cfg = AIA.config
10
12
 
11
13
  if args.empty?
12
- ap AIA.config.to_h
14
+ ap cfg.to_h
13
15
  elsif args.length == 1
14
16
  config_item = args.first
15
17
  local_cfg = {}
16
- local_cfg[config_item] = if AIA.config.respond_to?(config_item)
17
- AIA.config.send(config_item)
18
+ local_cfg[config_item] = if cfg.respond_to?(config_item)
19
+ cfg.send(config_item)
18
20
  end
19
21
  ap local_cfg
20
22
  else
@@ -41,66 +43,22 @@ module AIA
41
43
  alias cfg config
42
44
 
43
45
  desc "View or change the AI model"
44
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
45
46
  def model(args, context_manager = nil)
47
+ cfg = AIA.config
48
+
46
49
  if args.empty?
47
50
  puts
48
- models = AIA.config.models
49
-
50
- if models.size == 1
51
- puts "Current Model:"
52
- puts "=============="
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
60
- else
61
- puts "Multi-Model Configuration:"
62
- puts "=========================="
63
- puts "Model count: #{models.size}"
64
- first_model = models.first.respond_to?(:name) ? models.first.name : models.first.to_s
65
- puts "Primary model: #{first_model} (used for consensus when --consensus flag is enabled)"
66
- consensus = AIA.config.flags.consensus
67
- puts "Consensus mode: #{consensus.nil? ? 'auto-detect (disabled by default)' : consensus}"
68
- puts
69
- puts "Model Details:"
70
- puts "-" * 50
71
-
72
- models.each_with_index do |model_spec, index|
73
- model_name = model_spec.respond_to?(:name) ? model_spec.name : model_spec.to_s
74
- puts "#{index + 1}. #{model_name}#{' (primary)' if index.zero?}"
75
-
76
- begin
77
- model_info = RubyLLM::Models.find(model_name)
78
- if model_info
79
- puts " Provider: #{model_info.provider || 'Unknown'}"
80
- puts " Context window: #{model_info.context_window || 'Unknown'}"
81
- puts " Input cost: $#{model_info.input_cost || 'Unknown'}"
82
- puts " Output cost: $#{model_info.output_cost || 'Unknown'}"
83
- puts " Mode: #{model_info.modalities || 'Unknown'}"
84
- puts " Capabilities: #{(model_info.capabilities || []).join(', ')}" if model_info.capabilities&.any?
85
- else
86
- puts " Details: Model not found in registry"
87
- end
88
- rescue StandardError => e
89
- puts " Details: Unable to fetch (#{e.class.name}: #{e.message})"
90
- end
91
- puts
92
- end
93
- end
51
+ models = cfg.models
52
+ models.size == 1 ? show_single_model(models) : show_multi_model(models)
94
53
  puts
95
54
  else
96
55
  model_names = args.join(' ').split(',').map(&:strip).reject(&:empty?)
97
- AIA.config.models = AIA::Config::TO_MODEL_SPECS.call(model_names)
98
- AIA.client = RobotFactory.rebuild(AIA.config)
56
+ cfg.models = AIA::Config::TO_MODEL_SPECS.call(model_names)
57
+ AIA.client = RobotFactory.rebuild(cfg)
99
58
  end
100
59
 
101
60
  ''
102
61
  end
103
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
104
62
 
105
63
  desc "Set the temperature parameter for AI responses"
106
64
  def temperature(args, context_manager = nil)
@@ -115,7 +73,7 @@ module AIA
115
73
  alias topp top_p
116
74
 
117
75
  desc "Dump session cost/token metrics as CSV"
118
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
76
+ # :reek:TooManyStatements -- CSV assembly: guards, header, rows, totals, print, optional file append
119
77
  def cost(args = [], context_manager = nil)
120
78
  tracker = AIA.session_tracker
121
79
  unless tracker
@@ -133,63 +91,115 @@ module AIA
133
91
 
134
92
  header = %w[model input_tokens output_tokens total_tokens cost elapsed]
135
93
  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
94
+ lines = [header.join(',')]
95
+ turns.each { |turn| lines << cost_row(turn, has_similarity).join(',') }
96
+ lines << cost_totals_row(turns, has_similarity).join(',')
97
+
98
+ csv = lines.join("\n")
99
+ puts
100
+ puts csv
101
+ puts
102
+
103
+ out_file = AIA.config.output&.file
104
+ File.open(out_file, 'a') { |f| f.puts csv } if out_file
105
+
106
+ ''
107
+ end
158
108
 
159
- lines << row.join(',')
109
+ private
110
+
111
+ # One CSV row for a single recorded turn.
112
+ def cost_row(turn, has_similarity)
113
+ input_t = turn[:input_tokens] || 0
114
+ output_t = turn[:output_tokens] || 0
115
+
116
+ row = [
117
+ turn[:model],
118
+ input_t,
119
+ output_t,
120
+ input_t + output_t,
121
+ "$#{'%.5f' % (turn[:cost] || 0.0)}",
122
+ "%.1fs" % (turn[:elapsed] || 0)
123
+ ]
124
+
125
+ if has_similarity
126
+ sim = turn[:similarity]
127
+ row << (sim.nil? ? 'ref' : "%.1f%%" % (sim * 100))
160
128
  end
129
+ row
130
+ end
161
131
 
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
132
+ # The TOTAL summary row across all turns.
133
+ # :reek:DuplicateMethodCall -- each turns.sum aggregates a different field; there is no shared value to hoist
134
+ def cost_totals_row(turns, has_similarity)
135
+ total_input = turns.sum { |t| t[:input_tokens] || 0 }
136
+ total_output = turns.sum { |t| t[:output_tokens] || 0 }
137
+ total_cost = turns.sum { |t| t[:cost] || 0.0 }
138
+ max_elapsed = turns.map { |t| t[:elapsed] || 0 }.max
168
139
 
169
140
  totals = [
170
141
  'TOTAL',
171
142
  total_input,
172
143
  total_output,
173
- total_tokens,
144
+ total_input + total_output,
174
145
  "$#{'%.5f' % total_cost}",
175
146
  "%.1fs" % max_elapsed
176
147
  ]
177
148
  totals << '' if has_similarity
178
- lines << totals.join(',')
149
+ totals
150
+ end
179
151
 
180
- csv = lines.join("\n")
181
- puts
182
- puts csv
183
- puts
152
+ # Format a model spec's display name whether it is a ModelSpec or a bare string.
153
+ def model_display_name(spec)
154
+ spec.respond_to?(:name) ? spec.name : spec.to_s
155
+ end
184
156
 
185
- out_file = AIA.config.output&.file
186
- File.open(out_file, 'a') { |f| f.puts csv } if out_file
157
+ def show_single_model(models)
158
+ puts "Current Model:"
159
+ puts "=============="
160
+ model_name = model_display_name(models.first)
161
+ begin
162
+ model_info = RubyLLM::Models.find(model_name)
163
+ puts model_info.to_h.pretty_inspect if model_info
164
+ rescue StandardError
165
+ puts " #{model_name}"
166
+ end
167
+ end
187
168
 
188
- ''
169
+ # :reek:TooManyStatements -- sequential terminal report: summary lines then a details block per model
170
+ def show_multi_model(models)
171
+ puts "Multi-Model Configuration:"
172
+ puts "=========================="
173
+ puts "Model count: #{models.size}"
174
+ puts "Primary model: #{model_display_name(models.first)} (used for consensus when --consensus flag is enabled)"
175
+ consensus = AIA.config.flags.consensus
176
+ puts "Consensus mode: #{consensus.nil? ? 'auto-detect (disabled by default)' : consensus}"
177
+ puts
178
+ puts "Model Details:"
179
+ puts "-" * 50
180
+
181
+ models.each_with_index do |model_spec, index|
182
+ puts "#{index + 1}. #{model_display_name(model_spec)}#{' (primary)' if index.zero?}"
183
+ show_model_details(model_display_name(model_spec))
184
+ puts
185
+ end
189
186
  end
190
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
191
187
 
192
- private
188
+ def show_model_details(model_name)
189
+ model_info = RubyLLM::Models.find(model_name)
190
+ if model_info
191
+ puts " Provider: #{model_info.provider || 'Unknown'}"
192
+ puts " Context window: #{model_info.context_window || 'Unknown'}"
193
+ puts " Input cost: $#{model_info.input_cost || 'Unknown'}"
194
+ puts " Output cost: $#{model_info.output_cost || 'Unknown'}"
195
+ puts " Mode: #{model_info.modalities || 'Unknown'}"
196
+ puts " Capabilities: #{(model_info.capabilities || []).join(', ')}" if model_info.capabilities&.any?
197
+ else
198
+ puts " Details: Model not found in registry"
199
+ end
200
+ rescue StandardError => e
201
+ puts " Details: Unable to fetch (#{e.class.name}: #{e.message})"
202
+ end
193
203
 
194
204
  # Apply a config value by key, supporting three forms:
195
205
  # flat CLI key — "debug" → CLI_TO_NESTED_MAP routing
@@ -197,13 +207,15 @@ module AIA
197
207
  # direct setter — "log_level_override" → AIA.config.<key>=
198
208
  #
199
209
  # Returns true if the value was set, false if the key was unknown.
210
+ # :reek:TooManyStatements -- three key-addressing forms (dot-notation walk, CLI map, direct setter) tried in order
200
211
  def write_config_value(config_item, new_value) # rubocop:disable Naming/PredicateMethod
201
212
  key_str = config_item.to_s
213
+ cfg = AIA.config
202
214
 
203
215
  # Dot-notation: walk the config object by path segments
204
216
  if key_str.include?('.')
205
217
  parts = key_str.split('.')
206
- obj = AIA.config
218
+ obj = cfg
207
219
  parents = parts[0..-2]
208
220
  leaf = parts.last
209
221
 
@@ -222,15 +234,15 @@ module AIA
222
234
 
223
235
  # Flat CLI key via CLI_TO_NESTED_MAP (e.g. "debug" → flags.debug)
224
236
  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 })
237
+ cfg.respond_to?(:apply_overrides)
238
+ cfg.apply_overrides({ key_str.to_sym => new_value })
227
239
  maybe_clear_log_override(key_str, new_value)
228
240
  return true
229
241
  end
230
242
 
231
243
  # 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)
244
+ if cfg.respond_to?("#{key_str}=")
245
+ cfg.send("#{key_str}=", new_value)
234
246
  return true
235
247
  end
236
248
 
@@ -241,9 +253,11 @@ module AIA
241
253
  # drops back to its configured level rather than staying at DEBUG.
242
254
  def maybe_clear_log_override(key, value)
243
255
  return unless key.to_s == 'debug' && value == false
244
- return unless AIA.config.respond_to?(:log_level_override=)
245
256
 
246
- AIA.config.log_level_override = nil if AIA.config.log_level_override.to_s == 'debug'
257
+ cfg = AIA.config
258
+ return unless cfg.respond_to?(:log_level_override=)
259
+
260
+ cfg.log_level_override = nil if cfg.log_level_override.to_s == 'debug'
247
261
  end
248
262
  end
249
263
  end
@@ -21,6 +21,7 @@ module AIA
21
21
  end
22
22
 
23
23
  desc "Create a named checkpoint of the current context"
24
+ # :reek:TooManyStatements -- one snapshot build: name resolution, deep copy of every chat message list, store entry, feedback
24
25
  def checkpoint(args, _unused = nil)
25
26
  name = args.empty? ? nil : args.join(' ').strip
26
27
 
@@ -50,6 +51,7 @@ module AIA
50
51
  alias cp checkpoint
51
52
 
52
53
  desc "Restore context to a previous checkpoint"
54
+ # :reek:TooManyStatements -- restore flow: name resolution, validation, per-chat message replacement, user feedback
53
55
  def restore(args, _unused = nil)
54
56
  name = args.empty? ? nil : args.join(' ').strip
55
57
 
@@ -107,44 +109,23 @@ module AIA
107
109
  end
108
110
 
109
111
  desc "Display the current conversation context with checkpoint markers"
110
- # rubocop:disable Metrics/AbcSize
111
112
  def review(args, _unused = nil)
112
113
  chats = get_chats
113
114
  return "Error: No active chat sessions found." if chats.nil? || chats.empty?
114
115
 
115
- first_chat = chats.values.first
116
- messages = first_chat&.messages || []
117
-
118
- puts "\n=== Chat Context (RubyLLM) ==="
119
- puts "Total messages: #{messages.size}"
120
- puts "Models: #{chats.keys.join(', ')}"
121
- puts "Checkpoints: #{checkpoint_names.join(', ')}" if checkpoint_names.any?
122
- puts
116
+ messages = chats.values.first&.messages || []
117
+ print_review_header(chats, messages.size)
123
118
 
124
119
  positions = checkpoint_positions
125
-
126
120
  messages.each_with_index do |msg, index|
127
- if positions[index]
128
- puts "📍 [Checkpoint: #{positions[index].join(', ')}]"
129
- puts "-" * 40
130
- end
131
-
132
- role = msg.role.to_s.capitalize
133
- content = format_message_content(msg)
134
-
135
- puts "#{index + 1}. [#{role}]: #{content}"
136
- puts
137
- end
138
-
139
- if positions[messages.size]
140
- puts "📍 [Checkpoint: #{positions[messages.size].join(', ')}]"
141
- puts "-" * 40
121
+ print_checkpoint_marker(positions[index])
122
+ print_review_message(msg, index)
142
123
  end
124
+ print_checkpoint_marker(positions[messages.size])
143
125
 
144
126
  puts "=== End of Context ==="
145
127
  ""
146
128
  end
147
- # rubocop:enable Metrics/AbcSize
148
129
  alias context review
149
130
 
150
131
  desc "List all available checkpoints"
@@ -157,9 +138,10 @@ module AIA
157
138
  puts "\n=== Available Checkpoints ==="
158
139
  @checkpoint_store.each do |name, data|
159
140
  created = data[:created_at]&.strftime('%H:%M:%S') || 'unknown'
141
+ preview = data[:topic_preview]
160
142
  puts " #{name}: position #{data[:position]}, created #{created}"
161
- if data[:topic_preview] && !data[:topic_preview].empty?
162
- puts " → \"#{data[:topic_preview]}\""
143
+ if preview && !preview.empty?
144
+ puts " → \"#{preview}\""
163
145
  end
164
146
  end
165
147
  puts "=== End of Checkpoints ==="
@@ -173,6 +155,27 @@ module AIA
173
155
  @checkpoint_store.keys
174
156
  end
175
157
 
158
+ def print_review_header(chats, total)
159
+ puts "\n=== Chat Context (RubyLLM) ==="
160
+ puts "Total messages: #{total}"
161
+ puts "Models: #{chats.keys.join(', ')}"
162
+ puts "Checkpoints: #{checkpoint_names.join(', ')}" if checkpoint_names.any?
163
+ puts
164
+ end
165
+
166
+ # Print the marker block when checkpoint names exist at this position.
167
+ def print_checkpoint_marker(names)
168
+ return unless names
169
+
170
+ puts "📍 [Checkpoint: #{names.join(', ')}]"
171
+ puts "-" * 40
172
+ end
173
+
174
+ def print_review_message(msg, index)
175
+ puts "#{index + 1}. [#{msg.role.to_s.capitalize}]: #{format_message_content(msg)}"
176
+ puts
177
+ end
178
+
176
179
  def checkpoint_positions
177
180
  positions = {}
178
181
  @checkpoint_store.each do |name, data|
@@ -13,9 +13,10 @@ module AIA
13
13
  desc "Execute Ruby code (requires allow_ruby_eval: true in config)"
14
14
  def ruby(args, context_manager = nil)
15
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."
16
+ puts "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
+ return ""
19
20
  end
20
21
 
21
22
  ruby_code = args.join(' ')
@@ -35,10 +36,11 @@ module AIA
35
36
  desc "Use text-to-speech to speak the text"
36
37
  def say(args, context_manager = nil)
37
38
  audio = AIA.config.audio
39
+ voice = audio.voice
38
40
  env = {}
39
41
  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
+ if voice && !voice.strip.empty?
43
+ system(env, 'say', '-v', voice, *args)
42
44
  else
43
45
  system(env, 'say', *args)
44
46
  end
@@ -80,17 +82,19 @@ module AIA
80
82
  desc "Spawn a specialist robot for the next prompt. " \
81
83
  "Usage: /spawn | /spawn <type> | /spawn <name> <provider/model> <system prompt>"
82
84
  def spawn(args, context_manager = nil)
83
- AIA.turn_state.force_spawn = true
85
+ state = AIA.turn_state
86
+ state.force_spawn = true
84
87
 
85
88
  if args.size >= 2
86
89
  spec = AIA::SpawnSpecParser.parse(args)
87
- AIA.turn_state.spawn_spec = spec
88
- AIA.turn_state.spawn_type = nil
90
+ state.spawn_spec = spec
91
+ state.spawn_type = nil
89
92
  "Spawn mode enabled: '#{spec[:name]}' (#{spawn_model_summary(spec)}) for next prompt."
90
93
  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
+ type = args.first
95
+ state.spawn_spec = nil
96
+ state.spawn_type = type
97
+ type_msg = type ? " (#{type})" : " (auto-detect)"
94
98
  "Spawn mode enabled#{type_msg} for next prompt."
95
99
  end
96
100
  end
@@ -144,9 +148,10 @@ module AIA
144
148
 
145
149
  # Human-readable model description for the /spawn confirmation message.
146
150
  def spawn_model_summary(spec)
147
- return 'inherited model' unless spec[:model]
151
+ model = spec[:model]
152
+ return 'inherited model' unless model
148
153
 
149
- spec[:provider] ? "#{spec[:provider]}/#{spec[:model]}" : spec[:model]
154
+ spec[:provider] ? "#{spec[:provider]}/#{model}" : model
150
155
  end
151
156
 
152
157
  # Model plus any assigned skills, for the /add_recruit confirmation message.