legionio 1.8.4 → 1.8.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8cfc2d3d92d0f7d6597d051be30145be9a38864fe9c346b69320d07b6cefdf6
4
- data.tar.gz: e9420d7bdce1733c87dfe9924a4d82f5993d49ffaabcaa44ad96425c9bb1288d
3
+ metadata.gz: b26f7ff8bd0262bc8eedecfad569c5cd412b3c95f8a2f014f4017c21cd62013b
4
+ data.tar.gz: 3404fcfd2f6e7d4dabce66d9645997213ecb355a030a0dddb4df6c681e98cd1f
5
5
  SHA512:
6
- metadata.gz: 25467ee9926361dc83606f260567026ca9688b2b04f0ce16978c9ff9ba703d981e20065b77960d805cd8259a1f2765f56f509e869f51f25ef01ceed087268be7
7
- data.tar.gz: f33bcdbbda291c3e32f7aa5153ca30a22526ca673b7d584303e908346814c4e5f2e6035ec4f2b95c3c322867d3b09a3c0dbe2814f5d8a785ebc18f18cccd4611
6
+ metadata.gz: 406b9987436e8df5ad44a00edee5bfce4e68f8681ec089690d39cf4895fc1dab73cbe3500848ca1283a737c9997dad5c74700d09e719d70cab888b7acf28beef
7
+ data.tar.gz: 39409aeeb616e4e9e5d679f525be36ecdd60433c5154c8a2c5ebc2c936ce1a5a5d40d33731b9fcc9dc00a8bd6cfc0b3f8fbbd80c1bcc7d2e8736a5227182708a
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.8.5] - 2026-04-15
6
+
7
+ ### Fixed
8
+ - `Legion::Extensions::Core#trigger_words` now defaults to `lex_name.split('_')` (e.g. `['github']` for lex-github) instead of `[]`, ensuring extensions auto-surface in TriggerIndex without requiring explicit declaration. Closes #139
9
+ - `Legion::Extensions::Builder::Runners#build_runner_entry` now always populates `trigger_words`, defaulting to `[runner_name]` when the runner module does not define them explicitly. Closes #139
10
+ - `Legion::Tools::Discovery#synthesize_functions` now builds a real JSON Schema from Ruby method reflection data (`Method#parameters`) — required kwargs become required schema properties, optional kwargs become optional properties — so the LLM receives accurate parameter information instead of an empty schema. Closes #140
11
+ - `Legion::Tools::Discovery#synthesize_functions` now uses `definition[:desc]` for tool description when a `definition` DSL entry exists, falling back to the method name rather than `"method_name function"`. Closes #140
12
+ - `Legion::Tools::Discovery#tool_attributes` now reads `definition[:inputs]` when present and non-empty, using it as the input schema in preference to `meta[:options]`. Closes #140
13
+ - `Legion::Tools::Discovery#register_function` fixed asymmetric default: `resolve_exposed` now defaults to `true` when the extension does not respond to `mcp_tools?`, matching the behaviour of `resolve_mcp_tools_enabled`. Closes #140
14
+
5
15
  ## [1.8.4] - 2026-04-14
6
16
 
7
17
  ### Added
@@ -42,7 +42,11 @@ module Legion
42
42
  class_methods: {}
43
43
  }
44
44
  entry[:scheduled_tasks] = loaded_runner.scheduled_tasks if loaded_runner.method_defined?(:scheduled_tasks)
45
- entry[:trigger_words] = loaded_runner.trigger_words if loaded_runner.respond_to?(:trigger_words)
45
+ entry[:trigger_words] = if loaded_runner.respond_to?(:trigger_words) && loaded_runner.trigger_words.any?
46
+ loaded_runner.trigger_words
47
+ else
48
+ [runner_name]
49
+ end
46
50
  entry[:desc] = settings[:runners][runner_name.to_sym][:desc] if settings.key?(:runners) && settings[:runners].key?(runner_name.to_sym)
47
51
  entry
48
52
  end
@@ -128,7 +128,7 @@ module Legion
128
128
  end
129
129
 
130
130
  def trigger_words
131
- []
131
+ lex_name.split('_')
132
132
  end
133
133
 
134
134
  # Auto-generate AMQP message classes for each runner method that has a definition.
@@ -80,14 +80,40 @@ module Legion
80
80
  return {} unless runner_entry&.dig(:class_methods).is_a?(Hash)
81
81
 
82
82
  runner_entry[:class_methods].each_with_object({}) do |(method_name, method_info), funcs|
83
- funcs[method_name] = { desc: "#{method_name} function", options: {}, args: method_info[:args] }
83
+ defn = runner_mod.respond_to?(:definition_for) ? runner_mod.definition_for(method_name) : nil
84
+ funcs[method_name] = {
85
+ desc: defn&.dig(:desc) || method_name.to_s,
86
+ options: build_schema_from_args(method_info[:args]),
87
+ args: method_info[:args]
88
+ }
84
89
  end
85
90
  end
86
91
 
92
+ def build_schema_from_args(args)
93
+ return {} if args.nil? || args.empty?
94
+
95
+ properties = {}
96
+ required = []
97
+
98
+ args.each do |type, name|
99
+ next if name.nil? || %i[** * block].include?(name)
100
+
101
+ param_name = name.to_s
102
+ properties[param_name] = { type: 'string' }
103
+ required << param_name if type == :req
104
+ end
105
+
106
+ return {} if properties.empty?
107
+
108
+ schema = { properties: properties }
109
+ schema[:required] = required unless required.empty?
110
+ schema
111
+ end
112
+
87
113
  def register_function(ext, runner_mod, func_name, meta, is_deferred)
88
114
  defn = runner_mod.respond_to?(:definition_for) ? runner_mod.definition_for(func_name) : nil
89
115
 
90
- ext_default = ext.respond_to?(:mcp_tools?) ? ext.mcp_tools? : false
116
+ ext_default = ext.respond_to?(:mcp_tools?) ? ext.mcp_tools? : true
91
117
  return unless resolve_exposed(defn, meta, ext_default)
92
118
 
93
119
  requires = defn&.dig(:requires)&.map(&:to_s) || meta[:requires]
@@ -149,7 +175,7 @@ module Legion
149
175
  {
150
176
  tool_name: defn&.dig(:mcp_prefix) || "legion-#{ext_name}-#{runner_snake}-#{func_name}",
151
177
  description: meta[:desc] || defn&.dig(:desc) || "#{ext_name}##{func_name}",
152
- input_schema: normalize_schema(meta[:options]),
178
+ input_schema: normalize_schema(defn&.dig(:inputs)&.any? ? defn[:inputs] : meta[:options]),
153
179
  mcp_category: defn&.dig(:mcp_category),
154
180
  mcp_tier: defn&.dig(:mcp_tier),
155
181
  deferred: deferred,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.8.4'
4
+ VERSION = '1.8.5'
5
5
  end
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Fleet Pipeline Smoke Test
5
+ # =========================
6
+ # Runs against a live RabbitMQ instance to verify exchange/queue topology
7
+ # and basic message flow.
8
+ #
9
+ # Prerequisites:
10
+ # - RabbitMQ running on localhost:5672 (or set RABBITMQ_URL)
11
+ # - Legion gems installed: legion-transport, legion-settings, legion-json
12
+ # - Fleet extensions deployed: lex-assessor, lex-planner, lex-developer, lex-validator
13
+ #
14
+ # Usage:
15
+ # ruby scripts/fleet_smoke_test.rb
16
+ # RABBITMQ_URL=amqp://user:pass@host:5672 ruby scripts/fleet_smoke_test.rb
17
+
18
+ require 'json'
19
+ require 'securerandom'
20
+ require 'timeout'
21
+
22
+ # Suppress legion logging noise
23
+ ENV['LEGION_LOG_LEVEL'] ||= 'error'
24
+
25
+ class FleetSmokeTest
26
+ FLEET_EXCHANGES = %w[
27
+ lex.assessor lex.planner lex.developer lex.validator
28
+ ].freeze
29
+
30
+ FLEET_QUEUES = %w[
31
+ lex.assessor.runners.assessor
32
+ lex.planner.runners.planner
33
+ lex.developer.runners.developer
34
+ lex.developer.runners.ship
35
+ lex.validator.runners.validator
36
+ ].freeze
37
+
38
+ ABSORBER_QUEUES = %w[
39
+ lex.github.absorbers.issues.absorb
40
+ ].freeze
41
+
42
+ attr_reader :results
43
+
44
+ def initialize
45
+ @results = []
46
+ @passed = 0
47
+ @failed = 0
48
+ end
49
+
50
+ def run
51
+ puts '=' * 60
52
+ puts 'Fleet Pipeline Smoke Test'
53
+ puts '=' * 60
54
+ puts
55
+
56
+ check_dependencies
57
+ setup_transport
58
+ check_exchanges
59
+ check_queues
60
+ check_absorber_queues
61
+ test_publish_consume
62
+ teardown
63
+
64
+ report
65
+ end
66
+
67
+ private
68
+
69
+ def check_dependencies
70
+ section('Checking dependencies')
71
+
72
+ %w[legion-transport legion-settings legion-json].each do |gem_name|
73
+ Gem::Specification.find_by_name(gem_name)
74
+ pass("#{gem_name} installed")
75
+ rescue Gem::MissingSpecError
76
+ fail_test("#{gem_name} not installed")
77
+ end
78
+ end
79
+
80
+ def setup_transport
81
+ section('Connecting to RabbitMQ')
82
+
83
+ require 'legion/settings'
84
+ require 'legion/logging'
85
+ require 'legion/transport'
86
+
87
+ Legion::Logging.setup(log_level: 'error', level: 'error', trace: false)
88
+ Legion::Settings.load
89
+
90
+ if ENV['RABBITMQ_URL']
91
+ Legion::Settings.loader.settings[:transport] ||= {}
92
+ Legion::Settings.loader.settings[:transport][:url] = ENV.fetch('RABBITMQ_URL', nil)
93
+ end
94
+
95
+ Legion::Settings.merge_settings('transport', Legion::Transport::Settings.default)
96
+ Legion::Transport::Connection.setup
97
+ pass('Connected to RabbitMQ')
98
+ rescue StandardError => e
99
+ fail_test("RabbitMQ connection failed: #{e.message}")
100
+ puts "\n Set RABBITMQ_URL or configure transport in ~/.legionio/settings/"
101
+ exit 1
102
+ end
103
+
104
+ def check_exchanges
105
+ section('Checking fleet exchanges')
106
+
107
+ channel = Legion::Transport::Connection.session.create_channel
108
+ FLEET_EXCHANGES.each do |name|
109
+ check_or_create_exchange(channel, name)
110
+ channel = Legion::Transport::Connection.session.create_channel
111
+ end
112
+ end
113
+
114
+ def check_or_create_exchange(channel, name)
115
+ channel.exchange_declare(name, 'topic', passive: true)
116
+ pass("Exchange #{name} exists")
117
+ rescue Bunny::NotFound
118
+ channel = Legion::Transport::Connection.session.create_channel
119
+ channel.exchange_declare(name, 'topic', durable: true)
120
+ pass("Exchange #{name} created")
121
+ rescue StandardError => e
122
+ fail_test("Exchange #{name} check failed: #{e.message}")
123
+ end
124
+
125
+ def check_queues
126
+ section('Checking fleet queues')
127
+
128
+ channel = Legion::Transport::Connection.session.create_channel
129
+ FLEET_QUEUES.each do |name|
130
+ check_or_create_queue(channel, name)
131
+ channel = Legion::Transport::Connection.session.create_channel
132
+ end
133
+ end
134
+
135
+ def check_absorber_queues
136
+ section('Checking absorber queues')
137
+
138
+ channel = Legion::Transport::Connection.session.create_channel
139
+ ABSORBER_QUEUES.each do |name|
140
+ check_or_create_queue(channel, name, prefix: 'Absorber queue')
141
+ channel = Legion::Transport::Connection.session.create_channel
142
+ end
143
+ end
144
+
145
+ def check_or_create_queue(channel, name, prefix: 'Queue')
146
+ q = channel.queue(name, durable: true, passive: true)
147
+ pass("#{prefix} #{name} exists (depth: #{q.message_count})")
148
+ rescue Bunny::NotFound
149
+ channel = Legion::Transport::Connection.session.create_channel
150
+ channel.queue(name, durable: true)
151
+ pass("#{prefix} #{name} created")
152
+ rescue StandardError => e
153
+ fail_test("#{prefix} #{name} check failed: #{e.message}")
154
+ end
155
+
156
+ def test_publish_consume
157
+ section('Testing publish/consume round-trip')
158
+
159
+ channel = Legion::Transport::Connection.session.create_channel
160
+ test_queue_name = "fleet.smoke_test.#{SecureRandom.hex(4)}"
161
+
162
+ exchange = channel.topic('lex.assessor', durable: true)
163
+ queue = channel.queue(test_queue_name, durable: false, auto_delete: true)
164
+ queue.bind(exchange, routing_key: "#{test_queue_name}.#")
165
+
166
+ test_payload = {
167
+ work_item_id: SecureRandom.uuid,
168
+ source: 'smoke_test',
169
+ title: 'Fleet smoke test message',
170
+ timestamp: Time.now.utc.iso8601
171
+ }
172
+
173
+ exchange.publish(
174
+ JSON.generate(test_payload),
175
+ routing_key: "#{test_queue_name}.test",
176
+ content_type: 'application/json',
177
+ persistent: false
178
+ )
179
+
180
+ received = nil
181
+ Timeout.timeout(5) do
182
+ _, _, body = queue.pop
183
+ received = body ? JSON.parse(body, symbolize_names: true) : nil
184
+ end
185
+
186
+ if received && received[:work_item_id] == test_payload[:work_item_id]
187
+ pass('Publish/consume round-trip successful')
188
+ else
189
+ fail_test('Message not received or payload mismatch')
190
+ end
191
+ rescue Timeout::Error
192
+ fail_test('Publish/consume timed out after 5 seconds')
193
+ rescue StandardError => e
194
+ fail_test("Publish/consume failed: #{e.message}")
195
+ ensure
196
+ queue&.delete
197
+ end
198
+
199
+ def teardown
200
+ Legion::Transport::Connection.shutdown
201
+ rescue StandardError
202
+ nil
203
+ end
204
+
205
+ def section(title)
206
+ puts
207
+ puts "--- #{title} ---"
208
+ end
209
+
210
+ def pass(message)
211
+ @passed += 1
212
+ @results << { status: :pass, message: message }
213
+ puts " [PASS] #{message}"
214
+ end
215
+
216
+ def fail_test(message)
217
+ @failed += 1
218
+ @results << { status: :fail, message: message }
219
+ puts " [FAIL] #{message}"
220
+ end
221
+
222
+ def report
223
+ puts
224
+ puts '=' * 60
225
+ total = @passed + @failed
226
+ if @failed.zero?
227
+ puts "ALL #{total} CHECKS PASSED"
228
+ else
229
+ puts "#{@passed}/#{total} passed, #{@failed} FAILED"
230
+ end
231
+ puts '=' * 60
232
+
233
+ exit(@failed.zero? ? 0 : 1)
234
+ end
235
+ end
236
+
237
+ FleetSmokeTest.new.run if $PROGRAM_NAME == __FILE__
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legionio
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.4
4
+ version: 1.8.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -926,6 +926,7 @@ files:
926
926
  - lib/legion/workflow/manifest.rb
927
927
  - public/governance/index.html
928
928
  - public/workflow/index.html
929
+ - scripts/fleet_smoke_test.rb
929
930
  - scripts/rollout-ci-workflow.sh
930
931
  - scripts/sync-github-labels-topics.sh
931
932
  - workflows/autofix-pipeline.yml