robot_lab 0.2.7 → 0.2.8

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +4 -0
  3. data/.loki +5 -0
  4. data/Archspec.rb +44 -0
  5. data/CHANGELOG.md +17 -1
  6. data/Rakefile +6 -111
  7. data/_typos.toml +21 -0
  8. data/docs/api/index.md +2 -2
  9. data/docs/api/skills.md +53 -149
  10. data/docs/api/support.md +1 -1
  11. data/docs/architecture/index.md +5 -4
  12. data/docs/getting-started/configuration.md +4 -1
  13. data/docs/guides/hooks.md +122 -0
  14. data/docs/guides/using-tools.md +10 -1
  15. data/lib/robot_lab/agent_skill_catalog.rb +1 -0
  16. data/lib/robot_lab/ask_user.rb +2 -0
  17. data/lib/robot_lab/bus_poller.rb +2 -0
  18. data/lib/robot_lab/capabilities.rb +4 -0
  19. data/lib/robot_lab/config.rb +8 -0
  20. data/lib/robot_lab/doom_loop_detector.rb +6 -3
  21. data/lib/robot_lab/history_compressor.rb +5 -0
  22. data/lib/robot_lab/hook.rb +1 -0
  23. data/lib/robot_lab/hook_context.rb +4 -0
  24. data/lib/robot_lab/hook_registry.rb +1 -0
  25. data/lib/robot_lab/hooks.rb +6 -3
  26. data/lib/robot_lab/mcp/client.rb +2 -2
  27. data/lib/robot_lab/mcp/connection_poller.rb +16 -8
  28. data/lib/robot_lab/mcp/server_discovery.rb +1 -0
  29. data/lib/robot_lab/mcp/transports/sse.rb +3 -0
  30. data/lib/robot_lab/mcp/transports/stdio.rb +5 -0
  31. data/lib/robot_lab/mcp/transports/streamable_http.rb +4 -0
  32. data/lib/robot_lab/mcp/transports/websocket.rb +3 -0
  33. data/lib/robot_lab/memory.rb +23 -6
  34. data/lib/robot_lab/memory_change.rb +1 -0
  35. data/lib/robot_lab/message.rb +3 -0
  36. data/lib/robot_lab/names.rb +2 -4
  37. data/lib/robot_lab/network.rb +13 -6
  38. data/lib/robot_lab/robot/agent_skill_matching.rb +2 -0
  39. data/lib/robot_lab/robot/bus_messaging.rb +3 -0
  40. data/lib/robot_lab/robot/history_search.rb +2 -0
  41. data/lib/robot_lab/robot/hooking.rb +3 -0
  42. data/lib/robot_lab/robot/mcp_management.rb +11 -3
  43. data/lib/robot_lab/robot/template_rendering.rb +13 -4
  44. data/lib/robot_lab/robot.rb +58 -17
  45. data/lib/robot_lab/robot_result.rb +3 -0
  46. data/lib/robot_lab/run_config.rb +5 -0
  47. data/lib/robot_lab/script_tool.rb +20 -39
  48. data/lib/robot_lab/state_proxy.rb +1 -0
  49. data/lib/robot_lab/streaming/context.rb +1 -0
  50. data/lib/robot_lab/streaming/events.rb +1 -0
  51. data/lib/robot_lab/task.rb +2 -0
  52. data/lib/robot_lab/tool.rb +4 -0
  53. data/lib/robot_lab/user_message.rb +1 -0
  54. data/lib/robot_lab/utils.rb +2 -0
  55. data/lib/robot_lab/version.rb +1 -1
  56. data/lib/robot_lab/waiter.rb +3 -0
  57. data/lib/robot_lab.rb +6 -3
  58. metadata +7 -7
  59. data/lib/robot_lab/sandbox/null.rb +0 -13
  60. data/lib/robot_lab/sandbox/seatbelt.rb +0 -104
  61. data/lib/robot_lab/sandbox.rb +0 -52
@@ -58,6 +58,9 @@ module RobotLab
58
58
  # memory.results # => []
59
59
  # memory.cache # => RubyLLM::SemanticCache instance
60
60
  #
61
+ # :reek:TooManyMethods -- Memory is the deliberately broad shared-state facade
62
+ # (reserved keys, reactive get/subscribe, history); see CLAUDE.md.
63
+ # :reek:RepeatedConditional -- `@backend.key?(key)` is the check-under-mutex idiom; each site is a separate critical section.
61
64
  class Memory
62
65
  include Utils
63
66
 
@@ -89,6 +92,7 @@ module RobotLab
89
92
  #
90
93
  # @example Network-owned memory
91
94
  # Memory.new(network_name: "support_pipeline")
95
+ # :reek:BooleanParameter -- enable_cache is a documented feature toggle in the public API.
92
96
  def initialize(data: {}, results: [], messages: [], session_id: nil, backend: :auto, enable_cache: true,
93
97
  network_name: nil)
94
98
  @backend = select_backend(backend)
@@ -144,6 +148,7 @@ module RobotLab
144
148
  #
145
149
  # @see #set
146
150
  #
151
+ # :reek:TooManyStatements -- one case branch per reserved key; dispatch reads best as a single table.
147
152
  def []=(key, value)
148
153
  key = key.to_sym
149
154
 
@@ -292,6 +297,8 @@ module RobotLab
292
297
  # memory.get(:sentiment, :entities, :keywords, wait: 60)
293
298
  # # => { sentiment: {...}, entities: [...], keywords: [...] }
294
299
  #
300
+ # :reek:BooleanParameter -- `wait` is public API: false, true (block forever) or a numeric timeout.
301
+ # :reek:FeatureEnvy -- normalizing this method's own varargs before dispatching on arity.
295
302
  def get(*keys, wait: false)
296
303
  keys = keys.flatten.map(&:to_sym)
297
304
 
@@ -370,6 +377,8 @@ module RobotLab
370
377
  # @param subscription_id [Object] the subscription identifier from subscribe
371
378
  # @return [Boolean] true if subscription was found and removed
372
379
  #
380
+ # :reek:ControlParameter -- subscription_id is matched against stored ids, not used to select behavior.
381
+ # :reek:NestedIterators -- 2-deep scan of the per-key subscription lists is the natural shape.
373
382
  def unsubscribe(subscription_id)
374
383
  removed = false
375
384
 
@@ -576,6 +585,7 @@ module RobotLab
576
585
  #
577
586
  # @return [self]
578
587
  #
588
+ # :reek:TooManyStatements -- reserved keys are re-seeded one by one inside a single mutex block.
579
589
  def reset
580
590
  cached = get_internal(:cache) # Preserve cache instance
581
591
  @mutex.synchronize do
@@ -695,6 +705,7 @@ module RobotLab
695
705
  RubyLLM::SemanticCache
696
706
  end
697
707
 
708
+ # :reek:ControlParameter -- factory method; the preference symbol is exactly what selects the backend.
698
709
  def select_backend(preference)
699
710
  case preference
700
711
  when :hash
@@ -716,11 +727,11 @@ module RobotLab
716
727
  {}
717
728
  end
718
729
 
719
- def redis_available?
730
+ def redis_available?(config = RobotLab.config)
720
731
  return false unless defined?(Redis)
721
732
 
722
733
  # Check if Redis is configured in RobotLab
723
- redis_config = RobotLab.config.respond_to?(:redis) ? RobotLab.config.redis : nil
734
+ redis_config = config.respond_to?(:redis) ? config.redis : nil
724
735
  redis_config || ENV.fetch("REDIS_URL", nil)
725
736
  end
726
737
 
@@ -766,6 +777,7 @@ module RobotLab
766
777
  wait_for_key(key, timeout: timeout)
767
778
  end
768
779
 
780
+ # :reek:TooManyStatements -- read-under-mutex then wait-for-missing; splitting would separate lock from wait logic.
769
781
  def get_multiple(keys, wait:)
770
782
  results = {}
771
783
  missing = []
@@ -791,6 +803,7 @@ module RobotLab
791
803
  results
792
804
  end
793
805
 
806
+ # :reek:TooManyStatements -- double-check locking plus timeout cleanup must stay together for correctness.
794
807
  def wait_for_key(key, timeout:)
795
808
  waiter = Waiter.new
796
809
 
@@ -813,11 +826,13 @@ module RobotLab
813
826
  result
814
827
  end
815
828
 
829
+ # :reek:UncommunicativeVariableName -- single-char block vars are accepted style here (RuboCop allows them).
816
830
  def wake_waiters(key, value)
817
831
  waiters = @waiter_mutex.synchronize { @waiters.delete(key) || [] }
818
832
  waiters.each { |w| w.signal(value) }
819
833
  end
820
834
 
835
+ # :reek:TooManyStatements -- collect/build/coalesce steps share the change object and the scheduling flag.
821
836
  def notify_subscribers_async(key, value, old_value)
822
837
  # Collect all matching subscribers
823
838
  callbacks = []
@@ -860,6 +875,8 @@ module RobotLab
860
875
  # Drain all pending notification batches in a single fiber.
861
876
  # Loops until the queue is empty, then resets the drainer flag.
862
877
  # If new items arrive just before the flag resets, reschedules itself.
878
+ # :reek:TooManyStatements :reek:NestedIterators -- the drain loop and its ensure-reschedule race guard are one
879
+ # atomic unit; batch-of-callbacks iteration is inherently 2-deep.
863
880
  def drain_notification_queue
864
881
  loop do
865
882
  batch = @notification_queue_mutex.synchronize do
@@ -908,8 +925,8 @@ module RobotLab
908
925
  #
909
926
  # @api private
910
927
  class RedisBackend
911
- def initialize
912
- @redis = create_redis_connection
928
+ def initialize(config = RobotLab.config)
929
+ @redis = create_redis_connection(config)
913
930
  @namespace = "robot_lab:memory:#{SecureRandom.uuid}"
914
931
  end
915
932
 
@@ -945,8 +962,8 @@ module RobotLab
945
962
 
946
963
  private
947
964
 
948
- def create_redis_connection
949
- redis_config = RobotLab.config.respond_to?(:redis) ? RobotLab.config.redis : nil
965
+ def create_redis_connection(config = RobotLab.config)
966
+ redis_config = config.respond_to?(:redis) ? config.redis : nil
950
967
 
951
968
  if redis_config.is_a?(Hash)
952
969
  Redis.new(**redis_config)
@@ -43,6 +43,7 @@ module RobotLab
43
43
  # @param timestamp [Time] when the change occurred (defaults to now)
44
44
  # @param correlation_id [String, nil] optional correlation ID
45
45
  #
46
+ # :reek:ControlParameter -- `timestamp || Time.now` is a nil-safe default, not behavior selection.
46
47
  def initialize(key:, value:, previous: nil, writer: nil, network_name: nil, timestamp: nil, correlation_id: nil)
47
48
  @key = key.to_sym
48
49
  @value = value
@@ -178,6 +178,7 @@ module RobotLab
178
178
  # @param id [String] the unique identifier for this tool call
179
179
  # @param name [String] the name of the tool
180
180
  # @param input [Hash, nil] the input arguments
181
+ # :reek:ControlParameter -- `input || {}` is a nil-safe default, not behavior selection.
181
182
  def initialize(id:, name:, input:)
182
183
  @id = id
183
184
  @name = name
@@ -238,6 +239,7 @@ module RobotLab
238
239
  # @param role [String, Symbol] the message role (usually assistant)
239
240
  # @param tools [Array<ToolMessage, Hash>] the tool calls
240
241
  # @param stop_reason [String, Symbol, nil] the stop reason (defaults to "tool")
242
+ # :reek:ControlParameter -- `stop_reason || "tool"` is a nil-safe default, not behavior selection.
241
243
  def initialize(role:, tools:, stop_reason: nil)
242
244
  @tools = normalize_tools(tools)
243
245
  super(type: "tool_call", role: role, content: nil, stop_reason: stop_reason || "tool")
@@ -295,6 +297,7 @@ module RobotLab
295
297
  # @param tool [ToolMessage, Hash] the tool call that was executed
296
298
  # @param content [Hash] the result content (with :data or :error key)
297
299
  # @param stop_reason [String, Symbol, nil] the stop reason (defaults to "tool")
300
+ # :reek:ControlParameter -- `stop_reason || "tool"` is a nil-safe default, not behavior selection.
298
301
  def initialize(tool:, content:, stop_reason: nil)
299
302
  @tool = normalize_tool(tool)
300
303
  super(type: "tool_result", role: "tool_result", content: content, stop_reason: stop_reason || "tool")
@@ -2,10 +2,10 @@
2
2
 
3
3
  # A large curated word list, not application logic — kept as one inline
4
4
  # module for lookup speed, so the size/length cops below are false positives.
5
- # rubocop:disable Metrics/ModuleLength
5
+ # rubocop:disable-next Metrics/ModuleLength
6
6
  module RobotLab
7
7
  # Fictional robot names
8
- # rubocop:disable Metrics/CollectionLiteralLength
8
+ # rubocop:disable-next Metrics/CollectionLiteralLength
9
9
  NAMES = %w[
10
10
  R_7723
11
11
  R_790
@@ -394,9 +394,7 @@ module RobotLab
394
394
  Zhora
395
395
  Zoromes
396
396
  ].freeze
397
- # rubocop:enable Metrics/CollectionLiteralLength
398
397
 
399
398
  def self.name = names.first
400
399
  def self.names(how_many = 1) = NAMES.sample(how_many).sort
401
400
  end
402
- # rubocop:enable Metrics/ModuleLength
@@ -87,6 +87,7 @@ module RobotLab
87
87
  # task :billing, billing_robot, context: { dept: "billing" }, depends_on: :optional
88
88
  # end
89
89
  #
90
+ # :reek:ControlParameter -- `memory || ...` and `config || ...` are nil-safe defaults, not behavior selection.
90
91
  def initialize(name:, concurrency: :auto, memory: nil, config: nil, parallel_mode: :async, &)
91
92
  @name = name.to_s
92
93
  @robots = {}
@@ -125,6 +126,7 @@ module RobotLab
125
126
  # @example Task with dependencies
126
127
  # task :writer, writer_robot, depends_on: [:analyst]
127
128
  #
129
+ # :reek:LongParameterList -- one keyword per documented per-task option.
128
130
  def task(name, robot, context: {}, mcp: :none, tools: :none, memory: nil, config: nil, depends_on: :none,
129
131
  poller_group: :default)
130
132
  task_wrapper = Task.new(
@@ -180,6 +182,7 @@ module RobotLab
180
182
  # result.value # => RobotResult from last robot
181
183
  # result.context[:classifier] # => RobotResult from classifier
182
184
  #
185
+ # :reek:TooManyStatements -- linear assembly of run params and hook context before dispatch.
183
186
  def run(message = nil, **run_context)
184
187
  # Runnable protocol: accept a positional message like Robot#run does, so
185
188
  # callers can `run(msg, ...)` uniformly. `run(message: msg)` still works.
@@ -200,12 +203,13 @@ module RobotLab
200
203
  )
201
204
 
202
205
  RobotLab::Hooks.run(:network_run, context, registries: [RobotLab.hooks, @hooks]) do
206
+ run_params = context.context
203
207
  if @parallel_mode == :ractor
204
- run_with_ractor_scheduler(context.context)
208
+ run_with_ractor_scheduler(run_params)
205
209
  else
206
210
  initial_result = SimpleFlow::Result.new(
207
- context.context,
208
- context: { run_params: context.context }
211
+ run_params,
212
+ context: { run_params: run_params }
209
213
  )
210
214
  @pipeline.call_parallel(initial_result, max_concurrent: @config.max_concurrent_robots)
211
215
  end
@@ -329,11 +333,12 @@ module RobotLab
329
333
  # @raise [ArgumentError] if a robot with the same name already exists
330
334
  #
331
335
  def add_robot(robot)
332
- if @robots.key?(robot.name)
333
- raise ArgumentError, "Robot '#{robot.name}' already exists in network '#{@name}'"
336
+ name = robot.name
337
+ if @robots.key?(name)
338
+ raise ArgumentError, "Robot '#{name}' already exists in network '#{@name}'"
334
339
  end
335
340
 
336
- @robots[robot.name] = robot
341
+ @robots[name] = robot
337
342
  self
338
343
  end
339
344
 
@@ -398,6 +403,7 @@ module RobotLab
398
403
 
399
404
  private
400
405
 
406
+ # :reek:TooManyStatements -- linear build-specs/run/shutdown sequence for the ractor scheduler.
401
407
  def run_with_ractor_scheduler(run_context)
402
408
  unless RobotLab.extension_loaded?(:ractor)
403
409
  raise RobotLab::DependencyError,
@@ -419,6 +425,7 @@ module RobotLab
419
425
  results
420
426
  end
421
427
 
428
+ # :reek:FeatureEnvy -- snapshotting a robot's identity fields into a Ractor-shareable spec is this method's job.
422
429
  def build_robot_spec(task_wrapper)
423
430
  robot = task_wrapper.robot
424
431
  RobotSpec.new(
@@ -40,6 +40,8 @@ module RobotLab
40
40
  # @param message [String]
41
41
  # @param threshold [Float] cosine similarity cutoff (default SIMILARITY_THRESHOLD)
42
42
  # @return [Array<AgentSkill>]
43
+ # :reek:TooManyStatements -- linear search/filter with a best-effort rescue fallback.
44
+ # :reek:NestedIterators -- 2-deep filter_map/find joining search hits back to pending skills.
43
45
  def match_agent_skills(message, threshold: SIMILARITY_THRESHOLD)
44
46
  return [] if @pending_agent_skills.nil? || @pending_agent_skills.empty?
45
47
 
@@ -91,6 +91,7 @@ module RobotLab
91
91
  # @param auto_reply [Boolean] send the responder's result back to the sender
92
92
  # @yield [message] the inbound task; return the reply content (nil => no reply)
93
93
  # @return [self]
94
+ # :reek:BooleanParameter :reek:ControlParameter -- auto_reply is a documented public API toggle for reply behavior.
94
95
  def respond_to_tasks(auto_reply: true, &responder)
95
96
  on_message do |message|
96
97
  next if message.reply?
@@ -107,6 +108,7 @@ module RobotLab
107
108
  #
108
109
  # @param auto_reply [Boolean]
109
110
  # @return [self]
111
+ # :reek:BooleanParameter -- auto_reply is a documented public API toggle, forwarded to respond_to_tasks.
110
112
  def serve(auto_reply: true)
111
113
  respond_to_tasks(auto_reply: auto_reply) { |message| run(bus_task_content(message)).reply }
112
114
  end
@@ -161,6 +163,7 @@ module RobotLab
161
163
  # @param bus [TypedBus::MessageBus, nil] bus to join (creates one if nil)
162
164
  # @return [self]
163
165
  #
166
+ # :reek:ControlParameter -- `bus || @bus || new` implements the documented join-or-create semantics.
164
167
  def with_bus(bus = nil)
165
168
  return self if bus && @bus == bus
166
169
 
@@ -32,6 +32,8 @@ module RobotLab
32
32
  # @param limit [Integer] maximum number of results to return (default 5)
33
33
  # @return [Array<HistoryResult>] results sorted by score descending
34
34
  # @raise [RobotLab::DependencyError] if the 'classifier' gem is not installed
35
+ # :reek:TooManyStatements -- linear vectorize/score/collect scan over the chat history.
36
+ # :reek:FeatureEnvy -- scoring each message's extracted text against the query is the search itself.
35
37
  def search_history(query, limit: 5)
36
38
  TextAnalysis.require_classifier!
37
39
 
@@ -3,6 +3,9 @@
3
3
  module RobotLab
4
4
  class Robot < RubyLLM::Agent
5
5
  module Hooking
6
+ # :reek:LongParameterList -- the documented Robot#run public API: each keyword is a distinct run-scoped override.
7
+ # :reek:TooManyStatements -- the run lifecycle (memory writer swap, hook wrap, budget, cleanup) is one
8
+ # deliberate orchestrator; flog gates its complexity.
6
9
  def run(message = nil, network: nil, task: nil, network_memory: nil, network_config: nil,
7
10
  memory: nil, mcp: :none, tools: :none, hooks: nil, **kwargs, &block)
8
11
  run_memory = resolve_run_memory(memory, network: network, network_memory: network_memory)
@@ -11,6 +11,7 @@ module RobotLab
11
11
  private
12
12
 
13
13
  # Resolve MCP hierarchy: runtime -> robot build -> network -> config
14
+ # :reek:ControlParameter -- `network_config&.mcp || ...` is the documented fallback cascade, not a mode switch.
14
15
  def resolve_mcp_hierarchy(runtime_value, network: nil, network_config: nil)
15
16
  parent_value = network_config&.mcp || network_parent_config(network)&.mcp || RobotLab.config.mcp
16
17
  build_resolved = ToolConfig.resolve_mcp(@mcp_config, parent_value: parent_value)
@@ -18,6 +19,7 @@ module RobotLab
18
19
  end
19
20
 
20
21
  # Resolve tools hierarchy: runtime -> robot build -> network -> config
22
+ # :reek:ControlParameter -- `network_config&.tools || ...` is the documented fallback cascade, not a mode switch.
21
23
  def resolve_tools_hierarchy(runtime_value, network: nil, network_config: nil)
22
24
  parent_value = network_config&.tools || network_parent_config(network)&.tools || RobotLab.config.tools
23
25
  build_resolved = ToolConfig.resolve_tools(@tools_config, parent_value: parent_value)
@@ -32,6 +34,7 @@ module RobotLab
32
34
 
33
35
  # Ensure MCP clients are initialized for the given server configs.
34
36
  # On subsequent calls, retries any servers that previously failed to connect.
37
+ # :reek:TooManyStatements -- first-run init vs retry paths share the needed-server list; linear either way.
35
38
  def ensure_mcp_clients(mcp_servers)
36
39
  return if mcp_servers.empty?
37
40
 
@@ -54,6 +57,7 @@ module RobotLab
54
57
  @mcp_initialized = true
55
58
  end
56
59
 
60
+ # :reek:TooManyStatements -- connect success/failure bookkeeping plus the rescue path for one server.
57
61
  def init_mcp_client(server_config)
58
62
  client = MCP::Client.new(server_config)
59
63
  client.connect
@@ -78,6 +82,7 @@ module RobotLab
78
82
  end
79
83
 
80
84
  # Retry connecting to servers that previously failed
85
+ # :reek:TooManyStatements -- per-server retry with success bookkeeping and a best-effort rescue.
81
86
  def retry_failed_servers(_mcp_servers, needed_servers)
82
87
  return if @failed_mcp_configs.nil? || @failed_mcp_configs.empty?
83
88
 
@@ -85,8 +90,9 @@ module RobotLab
85
90
  to_retry = @failed_mcp_configs.slice(*needed_servers)
86
91
  return if to_retry.empty?
87
92
 
93
+ logger = RobotLab.config.logger
88
94
  to_retry.each do |name, server_config|
89
- RobotLab.config.logger.info(
95
+ logger.info(
90
96
  "Robot '#{@name}' retrying MCP server: #{name}"
91
97
  )
92
98
 
@@ -97,17 +103,19 @@ module RobotLab
97
103
  @mcp_clients[name] = client
98
104
  @failed_mcp_configs.delete(name)
99
105
  discover_mcp_tools(client, name)
100
- RobotLab.config.logger.info(
106
+ logger.info(
101
107
  "Robot '#{@name}' successfully connected to MCP server '#{name}' on retry"
102
108
  )
103
109
  end
104
110
  rescue StandardError => e
105
- RobotLab.config.logger.warn(
111
+ logger.warn(
106
112
  "Robot '#{@name}' retry failed for MCP server '#{name}': #{e.message}"
107
113
  )
108
114
  end
109
115
  end
110
116
 
117
+ # :reek:FeatureEnvy -- adapting each MCP tool definition into a local Tool is exactly this method's purpose.
118
+ # :reek:NestedIterators -- the inner block is the tool's execution closure, not an iteration.
111
119
  def discover_mcp_tools(client, server_name)
112
120
  tools = client.list_tools
113
121
 
@@ -35,6 +35,7 @@ module RobotLab
35
35
  # Apply a prompt_manager template to the persistent chat.
36
36
  # If required parameters are missing, applies front matter config but
37
37
  # defers rendering until run time when all values are available.
38
+ # :reek:TooManyStatements -- linear parse/merge/apply/render sequence with a documented deferred-render rescue.
38
39
  def apply_template_to_chat(context)
39
40
  parsed = PM.parse(@template)
40
41
 
@@ -68,6 +69,7 @@ module RobotLab
68
69
  # Re-rendering replaces the system message, so the inline system_prompt must be
69
70
  # re-appended here exactly as apply_system_prompt does at construction --
70
71
  # otherwise it would be silently dropped on any run that supplies context.
72
+ # :reek:TooManyStatements -- must rebuild skills + template + inline prompt in one pass (see comment above).
71
73
  def rerender_template(run_context)
72
74
  merged = (@build_context || {}).merge(run_context)
73
75
  resolved_ctx = resolve_context(merged, network: nil)
@@ -116,6 +118,7 @@ module RobotLab
116
118
  # Pure computation — reads ivars but does not mutate @chat.
117
119
  #
118
120
  # @return [Array(Array<String>, RunConfig, Hash)] bodies, merged config, extras hash
121
+ # :reek:TooManyStatements -- accumulates bodies/config/extras across skills then the main template in one pure pass.
119
122
  def collect_prompt_content(skill_ids, context)
120
123
  visited = Set.new
121
124
  visited.add(@template) if @template
@@ -128,16 +131,18 @@ module RobotLab
128
131
 
129
132
  @expanded_skills.each do |skill_id|
130
133
  parsed = PM.parse(skill_id)
131
- accumulate_extras(parsed.metadata, extras)
132
- accumulated_config = accumulated_config.merge(RunConfig.from_front_matter(parsed.metadata))
134
+ metadata = parsed.metadata
135
+ accumulate_extras(metadata, extras)
136
+ accumulated_config = accumulated_config.merge(RunConfig.from_front_matter(metadata))
133
137
  body = render_body(parsed, resolved_ctx)
134
138
  bodies << body if body
135
139
  end
136
140
 
137
141
  if @template
138
142
  parsed = PM.parse(@template)
139
- accumulate_extras(parsed.metadata, extras)
140
- accumulated_config = accumulated_config.merge(RunConfig.from_front_matter(parsed.metadata))
143
+ metadata = parsed.metadata
144
+ accumulate_extras(metadata, extras)
145
+ accumulated_config = accumulated_config.merge(RunConfig.from_front_matter(metadata))
141
146
  body = render_body(parsed, resolved_ctx)
142
147
  bodies << body if body
143
148
  end
@@ -181,6 +186,7 @@ module RobotLab
181
186
  # @param visited [Set<Symbol>] already-visited IDs for cycle detection
182
187
  # @param catalog [AgentSkillCatalog] catalog to check first
183
188
  # @return [Array<Symbol>] flat ordered list of PM-based skill IDs
189
+ # :reek:TooManyStatements -- depth-first skill expansion with cycle guard and catalog-vs-PM branching.
184
190
  def expand_skills_with_catalog(skill_ids, visited, catalog)
185
191
  result = []
186
192
 
@@ -226,6 +232,7 @@ module RobotLab
226
232
  #
227
233
  # @param metadata [PM::Metadata] front matter metadata
228
234
  # @return [Array<Symbol>]
235
+ # :reek:FeatureEnvy -- reading the metadata argument's skills list is the extraction itself.
229
236
  def extract_skills_from_metadata(metadata)
230
237
  return [] unless metadata.respond_to?(:skills) && metadata.skills
231
238
 
@@ -276,6 +283,7 @@ module RobotLab
276
283
 
277
284
  # Extract identity and capability keys from front matter metadata.
278
285
  # Constructor-provided values take precedence over frontmatter.
286
+ # :reek:FeatureEnvy -- copying front-matter metadata fields into this robot's ivars is the method's purpose.
279
287
  def apply_front_matter_extras(metadata)
280
288
  if metadata.respond_to?(:robot_name) && metadata.robot_name && !@name_from_constructor
281
289
  @name = metadata.robot_name.to_s
@@ -309,6 +317,7 @@ module RobotLab
309
317
  # Resolve string tool names from frontmatter to Ruby constants.
310
318
  # Tool subclasses are instantiated; instances are used as-is.
311
319
  # Unresolvable names are skipped with a warning.
320
+ # :reek:FeatureEnvy -- inspecting each resolved constant to decide instantiate-vs-use-as-is.
312
321
  def resolve_frontmatter_tools(tool_names)
313
322
  tool_names.filter_map do |name|
314
323
  case name