legionio 1.6.3 → 1.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72cd45444eead02f6fe56c876cc25c42c4f74111beaf76e5d898cbe94dbf2ea6
4
- data.tar.gz: f9f69b056a77e373eff39813f96f79882509dfdf7273bc81afee92dc556d84f6
3
+ metadata.gz: cd3587ed7997208005a0a8746aca477701811f8e506c1d8d9345a40d180317fc
4
+ data.tar.gz: e805096f6577c2b1317807d50beab27bbb91527e2a02807231596fbd6a1d91ca
5
5
  SHA512:
6
- metadata.gz: e86e061031320558c96fddc8bfabf2380a97cd51ee27a87d61cd7758e7cb432f8dc2b45257447146ad93ba16f4fd7100de3a90b2f3ecb95d41535a08efaf53af
7
- data.tar.gz: 40420f889daf45ddf2f75533e981063b33501423ae330e59114b40dfff899e345c1deeb82c9244183842c78a952b9ad1a39d9b10ad54056afd3a7f7b3a59f0ec
6
+ metadata.gz: 5d6c20753f9803fc34cd4bb577304bc31d708df54f9e49f266dbb32073c3d06835e2d625efbed610244b5888085d823222627fb07ad889c1ff85d37d028f603a
7
+ data.tar.gz: 0a1dbbff5e24580f3b8b97fb434c626f029f449501b138d0a29f9fcb0eb15edd05808c5b34353260a6bdc4808d4f747362d8f4e6e9f3afdd47362b56035c1e0f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.6.8] - 2026-03-26
4
+
5
+ ### Added
6
+ - `legionio bootstrap SOURCE` command: combines `config import`, `config scaffold`, and `setup agentic` into one command
7
+ - Pre-flight checks for klist (Kerberos ticket), brew availability, and legionio binary
8
+ - `--skip-packs`, `--start`, `--force`, `--json` flags for bootstrap command
9
+ - Self-awareness system prompt enrichment: `Context.to_system_prompt` appends live metacognition self-narrative from `lex-agentic-self` when loaded; guarded with `defined?()` and `rescue StandardError`
10
+
11
+ ## [1.6.7] - 2026-03-26
12
+
13
+ ### Fixed
14
+ - `setup_generated_functions` now runs only when `extensions: true` (inside the extensions gate) preventing unexpected boot side-effects in CLI flows that disable extensions
15
+ - Consumer tag entropy upgraded from `SecureRandom.hex(4)` (32-bit) to `SecureRandom.uuid` (122-bit) in both `prepare` and `subscribe` paths of subscription actor, eliminating the theoretical RabbitMQ `NOT_ALLOWED` tag collision
16
+
17
+ ## [1.6.4] - 2026-03-26
18
+
19
+ ### Fixed
20
+ - fix consumer tag collision on boot: subscription actors using `Thread.current.object_id` produced duplicate tags when `FixedThreadPool` reused threads, causing RabbitMQ `NOT_ALLOWED` connection kill and cascading errors; replaced with `SecureRandom.hex(4)`
21
+
3
22
  ## [1.6.3] - 2026-03-26
4
23
 
5
24
  ### Changed
@@ -24,6 +43,12 @@
24
43
  ## [1.6.0] - 2026-03-26
25
44
 
26
45
  ### Added
46
+ - `legion codegen` CLI subcommand (status, list, show, approve, reject, retry, gaps, cycle)
47
+ - `/api/codegen/*` API routes for generated function management
48
+ - Boot loading for generated functions via GeneratedRegistry
49
+ - Function metadata DSL (function_outputs, function_category, function_tags, function_risk_tier, function_idempotent, function_requires, function_expose)
50
+ - ClassMethods for MCP tool exposure (expose_as_mcp_tool, mcp_tool_prefix)
51
+ - End-to-end integration test for self-generating functions
27
52
  - `legion knowledge monitor add/list/remove/status` — multi-directory corpus monitor management
28
53
  - `legion knowledge capture commit` — capture git commit as knowledge (hook-compatible)
29
54
  - `legion knowledge capture session` — capture session summary as knowledge (hook-compatible)
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ class API < Sinatra::Base
5
+ module Routes
6
+ module Codegen
7
+ def self.registered(app) # rubocop:disable Metrics/MethodLength
8
+ app.get '/api/codegen/status' do
9
+ halt 503, json_error('codegen_unavailable', 'codegen not available', status_code: 503) unless defined?(Legion::MCP::SelfGenerate)
10
+
11
+ json_response(Legion::MCP::SelfGenerate.status)
12
+ end
13
+
14
+ app.get '/api/codegen/generated' do
15
+ unless defined?(Legion::Extensions::Codegen::Helpers::GeneratedRegistry)
16
+ halt 503, json_error('codegen_unavailable', 'codegen not available', status_code: 503)
17
+ end
18
+
19
+ status_filter = params[:status]
20
+ records = Legion::Extensions::Codegen::Helpers::GeneratedRegistry.list(status: status_filter)
21
+ json_response(records)
22
+ end
23
+
24
+ app.get '/api/codegen/generated/:id' do |id|
25
+ unless defined?(Legion::Extensions::Codegen::Helpers::GeneratedRegistry)
26
+ halt 503, json_error('codegen_unavailable', 'codegen not available', status_code: 503)
27
+ end
28
+
29
+ record = Legion::Extensions::Codegen::Helpers::GeneratedRegistry.get(id: id)
30
+ halt 404, json_error('not_found', 'record not found', status_code: 404) unless record
31
+
32
+ json_response(record)
33
+ end
34
+
35
+ app.post '/api/codegen/generated/:id/approve' do |id|
36
+ unless defined?(Legion::Extensions::Codegen::Runners::ReviewHandler)
37
+ halt 503, json_error('codegen_unavailable', 'review handler not available', status_code: 503)
38
+ end
39
+
40
+ result = Legion::Extensions::Codegen::Runners::ReviewHandler.handle_verdict(
41
+ review: { generation_id: id, verdict: :approve, confidence: 1.0 }
42
+ )
43
+ json_response(result)
44
+ end
45
+
46
+ app.post '/api/codegen/generated/:id/reject' do |id|
47
+ unless defined?(Legion::Extensions::Codegen::Helpers::GeneratedRegistry)
48
+ halt 503, json_error('codegen_unavailable', 'codegen not available', status_code: 503)
49
+ end
50
+
51
+ Legion::Extensions::Codegen::Helpers::GeneratedRegistry.update_status(id: id, status: 'rejected')
52
+ json_response({ id: id, status: 'rejected' })
53
+ end
54
+
55
+ app.post '/api/codegen/generated/:id/retry' do |id|
56
+ unless defined?(Legion::Extensions::Codegen::Helpers::GeneratedRegistry)
57
+ halt 503, json_error('codegen_unavailable', 'codegen not available', status_code: 503)
58
+ end
59
+
60
+ Legion::Extensions::Codegen::Helpers::GeneratedRegistry.update_status(id: id, status: 'pending')
61
+ json_response({ id: id, status: 'pending' })
62
+ end
63
+
64
+ app.get '/api/codegen/gaps' do
65
+ data = if defined?(Legion::MCP::GapDetector)
66
+ Legion::MCP::GapDetector.detect_gaps
67
+ else
68
+ []
69
+ end
70
+ json_response(data)
71
+ end
72
+
73
+ app.post '/api/codegen/cycle' do
74
+ return json_response({ triggered: false, reason: 'self_generate not available' }) unless defined?(Legion::MCP::SelfGenerate)
75
+
76
+ Legion::MCP::SelfGenerate.instance_variable_set(:@last_cycle_at, nil)
77
+ result = Legion::MCP::SelfGenerate.run_cycle
78
+ json_response(result)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
data/lib/legion/api.rb CHANGED
@@ -46,6 +46,7 @@ require_relative 'api/apollo'
46
46
  require_relative 'api/costs'
47
47
  require_relative 'api/traces'
48
48
  require_relative 'api/stats'
49
+ require_relative 'api/codegen'
49
50
  require_relative 'api/graphql' if defined?(GraphQL)
50
51
 
51
52
  module Legion
@@ -137,6 +138,7 @@ module Legion
137
138
  register Routes::Costs
138
139
  register Routes::Traces
139
140
  register Routes::Stats
141
+ register Routes::Codegen
140
142
  register Routes::GraphQL if defined?(Routes::GraphQL)
141
143
 
142
144
  use Legion::API::Middleware::RequestLogger
@@ -0,0 +1,399 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require 'json'
5
+ require 'fileutils'
6
+ require 'rbconfig'
7
+ require 'thor'
8
+ require 'legion/cli/output'
9
+
10
+ module Legion
11
+ module CLI
12
+ class Bootstrap < Thor
13
+ namespace 'bootstrap'
14
+
15
+ def self.exit_on_failure?
16
+ true
17
+ end
18
+
19
+ class_option :json, type: :boolean, default: false, desc: 'Machine-readable output'
20
+ class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
21
+ class_option :skip_packs, type: :boolean, default: false, desc: 'Skip gem pack installation (config only)'
22
+ class_option :start, type: :boolean, default: false, desc: 'Start redis + legionio via brew services after bootstrap'
23
+ class_option :force, type: :boolean, default: false, desc: 'Overwrite existing config files'
24
+
25
+ desc 'SOURCE', 'Bootstrap Legion from a URL or local config file (fetch config, scaffold, install packs)'
26
+ long_desc <<~DESC
27
+ Combines three manual steps into one:
28
+
29
+ legionio config import SOURCE (fetch + write config)
30
+ legionio config scaffold (fill gaps with env-detected defaults)
31
+ legionio setup agentic (install cognitive gem packs)
32
+
33
+ SOURCE may be an HTTPS URL or a local file path to a bootstrap JSON file.
34
+ The JSON may include a "packs" array (e.g. ["agentic"]) which controls which
35
+ gem packs are installed. That key is removed before the config is written.
36
+
37
+ Options:
38
+ --skip-packs Skip gem pack installation entirely
39
+ --start After bootstrap, run: brew services start redis && brew services start legionio
40
+ --force Overwrite existing config files
41
+ --json Machine-readable JSON output
42
+ DESC
43
+ def execute(source)
44
+ require_relative 'config_import'
45
+ require_relative 'config_scaffold'
46
+ require_relative 'setup_command'
47
+
48
+ out = formatter
49
+ results = {}
50
+ warns = []
51
+
52
+ # 1. Pre-flight checks
53
+ print_step(out, 'Pre-flight checks')
54
+ results[:preflight] = run_preflight_checks(out, warns)
55
+
56
+ # 2. Fetch + parse config
57
+ print_step(out, "Fetching config from #{source}")
58
+ body = ConfigImport.fetch_source(source)
59
+ config = ConfigImport.parse_payload(body)
60
+
61
+ # 3. Extract packs before writing (bootstrap-only directive)
62
+ pack_names = Array(config.delete(:packs)).map(&:to_s).reject(&:empty?)
63
+ results[:packs_requested] = pack_names
64
+
65
+ # 4. Write config
66
+ path = ConfigImport.write_config(config, force: options[:force])
67
+ results[:config_written] = path
68
+ out.success("Config written to #{path}") unless options[:json]
69
+
70
+ # 5. Scaffold missing subsystem files
71
+ results[:scaffold] = run_scaffold(out)
72
+
73
+ # 6. Install packs (unless --skip-packs)
74
+ results[:packs_installed] = install_packs_step(pack_names, out)
75
+
76
+ # 7. Post-bootstrap summary
77
+ summary = build_summary(config, results, warns)
78
+ results[:summary] = summary
79
+ print_summary(out, summary)
80
+
81
+ # 8. Optional --start
82
+ if options[:start]
83
+ print_step(out, 'Starting services')
84
+ results[:services_started] = start_services(out)
85
+ end
86
+
87
+ out.json(results) if options[:json]
88
+ rescue CLI::Error => e
89
+ formatter.error(e.message)
90
+ raise SystemExit, 1
91
+ end
92
+
93
+ default_task :execute
94
+
95
+ no_commands do # rubocop:disable Metrics/BlockLength
96
+ def formatter
97
+ @formatter ||= Output::Formatter.new(
98
+ json: options[:json],
99
+ color: !options[:no_color]
100
+ )
101
+ end
102
+
103
+ private
104
+
105
+ def print_step(out, message)
106
+ return if options[:json]
107
+
108
+ out.spacer
109
+ out.header(message)
110
+ end
111
+
112
+ # Wraps backtick execution, returning [output, success_bool].
113
+ # Extracted as a method so specs can stub it cleanly.
114
+ def shell_capture(cmd)
115
+ output = `#{cmd} 2>&1`
116
+ [output, $CHILD_STATUS.success?]
117
+ end
118
+
119
+ # -----------------------------------------------------------------------
120
+ # Pre-flight checks
121
+ # -----------------------------------------------------------------------
122
+
123
+ def run_preflight_checks(out, warns)
124
+ {
125
+ klist: check_klist(out, warns),
126
+ brew: check_brew(out, warns),
127
+ legionio: check_legionio_binary(out, warns)
128
+ }
129
+ end
130
+
131
+ def check_klist(out, warns)
132
+ output, success = shell_capture('klist')
133
+ if success && output.match?(/principal|Credentials/i)
134
+ out.success('Kerberos ticket valid') unless options[:json]
135
+ { status: :ok }
136
+ else
137
+ msg = 'No valid Kerberos ticket found. Run `kinit` before bootstrapping.'
138
+ warns << msg
139
+ out.warn(msg) unless options[:json]
140
+ { status: :warn, message: msg }
141
+ end
142
+ rescue StandardError => e
143
+ msg = "klist check failed: #{e.message}"
144
+ warns << msg
145
+ out.warn(msg) unless options[:json]
146
+ { status: :warn, message: msg }
147
+ end
148
+
149
+ def check_brew(out, warns)
150
+ _, success = shell_capture('brew --version')
151
+ if success
152
+ out.success('Homebrew available') unless options[:json]
153
+ { status: :ok }
154
+ else
155
+ msg = 'Homebrew not found. Install from https://brew.sh'
156
+ warns << msg
157
+ out.warn(msg) unless options[:json]
158
+ { status: :warn, message: msg }
159
+ end
160
+ rescue StandardError => e
161
+ msg = "brew check failed: #{e.message}"
162
+ warns << msg
163
+ out.warn(msg) unless options[:json]
164
+ { status: :warn, message: msg }
165
+ end
166
+
167
+ def check_legionio_binary(out, warns)
168
+ _, success = shell_capture('legionio version')
169
+ if success
170
+ out.success('legionio binary works') unless options[:json]
171
+ { status: :ok }
172
+ else
173
+ msg = 'legionio binary not responding. Try reinstalling: brew reinstall legionio'
174
+ warns << msg
175
+ out.warn(msg) unless options[:json]
176
+ { status: :warn, message: msg }
177
+ end
178
+ rescue StandardError => e
179
+ msg = "legionio binary check failed: #{e.message}"
180
+ warns << msg
181
+ out.warn(msg) unless options[:json]
182
+ { status: :warn, message: msg }
183
+ end
184
+
185
+ def run_scaffold(out)
186
+ print_step(out, 'Scaffolding missing subsystem files')
187
+ silent_out = Output::Formatter.new(json: false, color: false)
188
+ scaffold_opts = build_scaffold_opts
189
+ scaffold_opts[:json] = false if options[:json]
190
+ ConfigScaffold.run(options[:json] ? silent_out : out, scaffold_opts)
191
+ :done
192
+ end
193
+
194
+ def install_packs_step(pack_names, out)
195
+ if options[:skip_packs]
196
+ out.warn('Skipping pack installation (--skip-packs)') unless options[:json]
197
+ []
198
+ else
199
+ print_step(out, "Installing packs: #{pack_names.join(', ')}") unless pack_names.empty?
200
+ install_packs(pack_names, out)
201
+ end
202
+ end
203
+
204
+ # -----------------------------------------------------------------------
205
+ # Scaffold options
206
+ # -----------------------------------------------------------------------
207
+
208
+ def build_scaffold_opts
209
+ {
210
+ force: options[:force],
211
+ json: options[:json],
212
+ only: options[:only],
213
+ full: options[:full],
214
+ dir: options[:dir]
215
+ }
216
+ end
217
+
218
+ # -----------------------------------------------------------------------
219
+ # Pack installation
220
+ # -----------------------------------------------------------------------
221
+
222
+ def install_packs(pack_names, out)
223
+ return [] if pack_names.empty?
224
+
225
+ gem_bin = File.join(RbConfig::CONFIG['bindir'], 'gem')
226
+ results = []
227
+
228
+ pack_names.each do |pack_name|
229
+ pack_sym = pack_name.to_sym
230
+ pack = Setup::PACKS[pack_sym]
231
+ unless pack
232
+ out.warn("Unknown pack: #{pack_name} (valid: #{Setup::PACKS.keys.join(', ')})") unless options[:json]
233
+ next
234
+ end
235
+
236
+ out.header("Installing pack: #{pack_name}") unless options[:json]
237
+ gem_results = install_pack_gems(pack[:gems], gem_bin, out)
238
+ Gem::Specification.reset
239
+ results << { pack: pack_name, results: gem_results }
240
+ end
241
+
242
+ results
243
+ end
244
+
245
+ def install_pack_gems(gem_names, gem_bin, out)
246
+ already_installed = []
247
+ to_install = []
248
+
249
+ gem_names.each do |name|
250
+ Gem::Specification.find_by_name(name)
251
+ already_installed << name
252
+ rescue Gem::MissingSpecError
253
+ to_install << name
254
+ end
255
+
256
+ gem_results = to_install.map { |g| install_single_gem(g, gem_bin, out) }
257
+
258
+ already_installed.each do |g|
259
+ out.success(" #{g} already installed") unless options[:json]
260
+ end
261
+
262
+ gem_results
263
+ end
264
+
265
+ def install_single_gem(name, gem_bin, out)
266
+ puts " Installing #{name}..." unless options[:json]
267
+ output, success = shell_capture("#{gem_bin} install #{name} --no-document")
268
+ if success
269
+ out.success(" #{name} installed") unless options[:json]
270
+ { name: name, status: 'installed' }
271
+ else
272
+ out.error(" #{name} failed") unless options[:json]
273
+ { name: name, status: 'failed', error: output.strip.lines.last&.strip }
274
+ end
275
+ end
276
+
277
+ # -----------------------------------------------------------------------
278
+ # Summary
279
+ # -----------------------------------------------------------------------
280
+
281
+ def build_summary(config, results, warns)
282
+ settings_dir = ConfigImport::SETTINGS_DIR
283
+ subsystem_files = ConfigScaffold::SUBSYSTEMS.to_h do |s|
284
+ path = File.join(settings_dir, "#{s}.json")
285
+ [s, File.exist?(path)]
286
+ end
287
+
288
+ {
289
+ config_sections: config.keys.map(&:to_s),
290
+ packs_requested: results[:packs_requested] || [],
291
+ packs_installed: results[:packs_installed] || [],
292
+ subsystem_files: subsystem_files,
293
+ warnings: warns,
294
+ preflight: results[:preflight] || {}
295
+ }
296
+ end
297
+
298
+ def print_summary(out, summary)
299
+ return if options[:json]
300
+
301
+ out.spacer
302
+ out.header('Bootstrap Summary')
303
+ out.spacer
304
+
305
+ print_config_sections(summary)
306
+ print_subsystem_files(summary)
307
+ print_packs_summary(out, summary)
308
+ print_warnings_section(out, summary)
309
+ print_next_steps(out)
310
+ end
311
+
312
+ def print_config_sections(summary)
313
+ puts " Config sections: #{summary[:config_sections].join(', ')}" if summary[:config_sections].any?
314
+ end
315
+
316
+ def print_subsystem_files(summary)
317
+ present = summary[:subsystem_files].select { |_, v| v }.keys
318
+ absent = summary[:subsystem_files].reject { |_, v| v }.keys
319
+ puts " Subsystem files present: #{present.join(', ')}" if present.any?
320
+ puts " Subsystem files missing: #{absent.join(', ')}" if absent.any?
321
+ end
322
+
323
+ def print_packs_summary(out, summary)
324
+ summary[:packs_installed].each do |pack_result|
325
+ successes = (pack_result[:results] || []).count { |r| r[:status] == 'installed' }
326
+ failures = (pack_result[:results] || []).count { |r| r[:status] == 'failed' }
327
+ if failures.zero?
328
+ out.success("Pack #{pack_result[:pack]}: #{successes} gem(s) installed")
329
+ else
330
+ out.warn("Pack #{pack_result[:pack]}: #{successes} installed, #{failures} failed")
331
+ end
332
+ end
333
+ out.warn('Pack installation skipped') if options[:skip_packs]
334
+ end
335
+
336
+ def print_warnings_section(out, summary)
337
+ return unless summary[:warnings].any?
338
+
339
+ out.spacer
340
+ out.header('Attention')
341
+ summary[:warnings].each { |w| out.warn(w) }
342
+ end
343
+
344
+ def print_next_steps(out)
345
+ return if options[:start]
346
+
347
+ out.spacer
348
+ puts ' Next steps:'
349
+ puts ' brew services start redis && brew services start legionio'
350
+ puts ' legion'
351
+ end
352
+
353
+ # -----------------------------------------------------------------------
354
+ # Service startup (--start)
355
+ # -----------------------------------------------------------------------
356
+
357
+ def start_services(out)
358
+ redis_ok = run_brew_service('redis', out)
359
+ legion_ok = run_brew_service('legionio', out)
360
+ poll_daemon_ready(out) if redis_ok && legion_ok
361
+ { redis: redis_ok, legionio: legion_ok }
362
+ end
363
+
364
+ def run_brew_service(service, out)
365
+ output, success = shell_capture("brew services start #{service}")
366
+ if success
367
+ out.success("#{service} started") unless options[:json]
368
+ true
369
+ else
370
+ out.warn("#{service} failed to start: #{output.strip.lines.last&.strip}") unless options[:json]
371
+ false
372
+ end
373
+ rescue StandardError => e
374
+ out.warn("brew services start #{service} raised: #{e.message}") unless options[:json]
375
+ false
376
+ end
377
+
378
+ def poll_daemon_ready(out, port: 4567, timeout: 30)
379
+ require 'net/http'
380
+ deadline = ::Time.now + timeout
381
+ until ::Time.now > deadline
382
+ begin
383
+ resp = Net::HTTP.get_response(URI("http://localhost:#{port}/api/ready"))
384
+ if resp.is_a?(Net::HTTPSuccess)
385
+ out.success("Daemon ready on port #{port}") unless options[:json]
386
+ return true
387
+ end
388
+ rescue StandardError
389
+ # not ready yet — keep polling
390
+ end
391
+ sleep 1
392
+ end
393
+ out.warn("Daemon did not become ready within #{timeout}s") unless options[:json]
394
+ false
395
+ end
396
+ end
397
+ end
398
+ end
399
+ end
@@ -61,6 +61,7 @@ module Legion
61
61
  end
62
62
 
63
63
  parts << cognitive_awareness(directory)
64
+ parts << self_awareness_hint
64
65
 
65
66
  extra_dirs.each do |dir|
66
67
  expanded = File.expand_path(dir)
@@ -181,6 +182,16 @@ module Legion
181
182
  end
182
183
  nil
183
184
  end
185
+
186
+ def self.self_awareness_hint
187
+ return nil unless defined?(Legion::Extensions::Agentic::Self::Metacognition::Runners::Metacognition)
188
+
189
+ result = Legion::Extensions::Agentic::Self::Metacognition::Runners::Metacognition.self_narrative
190
+ narrative = result[:prose] if result.is_a?(Hash) && result[:prose]
191
+ narrative ? "\nCurrent self-awareness:\n#{narrative}" : nil
192
+ rescue StandardError
193
+ nil
194
+ end
184
195
  end
185
196
  end
186
197
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module CLI
5
+ class CodegenCommand < Thor
6
+ namespace :codegen
7
+
8
+ desc 'status', 'Show codegen cycle stats, pending gaps, registry counts'
9
+ def status
10
+ if defined?(Legion::MCP::SelfGenerate)
11
+ data = Legion::MCP::SelfGenerate.status
12
+ say Legion::JSON.dump({ data: data })
13
+ else
14
+ say Legion::JSON.dump({ error: 'codegen not available' })
15
+ end
16
+ end
17
+
18
+ desc 'list', 'List generated functions'
19
+ method_option :status, type: :string, desc: 'Filter by status'
20
+ def list
21
+ unless defined?(Legion::Extensions::Codegen::Helpers::GeneratedRegistry)
22
+ say Legion::JSON.dump({ error: 'codegen registry not available' })
23
+ return
24
+ end
25
+
26
+ records = Legion::Extensions::Codegen::Helpers::GeneratedRegistry.list(status: options[:status])
27
+ say Legion::JSON.dump({ data: records })
28
+ end
29
+
30
+ desc 'show ID', 'Show details of a generated function'
31
+ def show(id)
32
+ unless defined?(Legion::Extensions::Codegen::Helpers::GeneratedRegistry)
33
+ say Legion::JSON.dump({ error: 'codegen registry not available' })
34
+ return
35
+ end
36
+
37
+ record = Legion::Extensions::Codegen::Helpers::GeneratedRegistry.get(id: id)
38
+ if record
39
+ say Legion::JSON.dump({ data: record })
40
+ else
41
+ say Legion::JSON.dump({ error: 'not found' })
42
+ end
43
+ end
44
+
45
+ desc 'approve ID', 'Manually approve a parked generated function'
46
+ def approve(id)
47
+ unless defined?(Legion::Extensions::Codegen::Runners::ReviewHandler)
48
+ say Legion::JSON.dump({ error: 'review handler not available' })
49
+ return
50
+ end
51
+
52
+ result = Legion::Extensions::Codegen::Runners::ReviewHandler.handle_verdict(
53
+ review: { generation_id: id, verdict: :approve, confidence: 1.0 }
54
+ )
55
+ say Legion::JSON.dump({ data: result })
56
+ end
57
+
58
+ desc 'reject ID', 'Manually reject a generated function'
59
+ def reject(id)
60
+ unless defined?(Legion::Extensions::Codegen::Helpers::GeneratedRegistry)
61
+ say Legion::JSON.dump({ error: 'codegen registry not available' })
62
+ return
63
+ end
64
+
65
+ Legion::Extensions::Codegen::Helpers::GeneratedRegistry.update_status(id: id, status: 'rejected')
66
+ say Legion::JSON.dump({ data: { id: id, status: 'rejected' } })
67
+ end
68
+
69
+ desc 'retry ID', 'Re-queue a generated function for regeneration'
70
+ def retry_generation(id)
71
+ unless defined?(Legion::Extensions::Codegen::Helpers::GeneratedRegistry)
72
+ say Legion::JSON.dump({ error: 'codegen registry not available' })
73
+ return
74
+ end
75
+
76
+ Legion::Extensions::Codegen::Helpers::GeneratedRegistry.update_status(id: id, status: 'pending')
77
+ say Legion::JSON.dump({ data: { id: id, status: 'pending' } })
78
+ end
79
+ map 'retry' => :retry_generation
80
+
81
+ desc 'gaps', 'List detected capability gaps with priorities'
82
+ def gaps
83
+ if defined?(Legion::MCP::GapDetector)
84
+ detected = Legion::MCP::GapDetector.detect_gaps
85
+ say Legion::JSON.dump({ data: detected })
86
+ else
87
+ say Legion::JSON.dump({ error: 'gap detector not available' })
88
+ end
89
+ end
90
+
91
+ desc 'cycle', 'Manually trigger a generation cycle (bypass cooldown)'
92
+ def cycle
93
+ unless defined?(Legion::MCP::SelfGenerate)
94
+ say Legion::JSON.dump({ error: 'self_generate not available' })
95
+ return
96
+ end
97
+
98
+ Legion::MCP::SelfGenerate.instance_variable_set(:@last_cycle_at, nil)
99
+ result = Legion::MCP::SelfGenerate.run_cycle
100
+ say Legion::JSON.dump({ data: result })
101
+ end
102
+ end
103
+ end
104
+ end
data/lib/legion/cli.rb CHANGED
@@ -64,6 +64,8 @@ module Legion
64
64
  autoload :TraceCommand, 'legion/cli/trace_command'
65
65
  autoload :Features, 'legion/cli/features_command'
66
66
  autoload :Debug, 'legion/cli/debug_command'
67
+ autoload :CodegenCommand, 'legion/cli/codegen_command'
68
+ autoload :Bootstrap, 'legion/cli/bootstrap_command'
67
69
 
68
70
  module Groups
69
71
  autoload :Ai, 'legion/cli/groups/ai_group'
@@ -235,6 +237,9 @@ module Legion
235
237
  desc 'setup SUBCOMMAND', 'Install feature packs and configure IDE integrations'
236
238
  subcommand 'setup', Legion::CLI::Setup
237
239
 
240
+ desc 'bootstrap SOURCE', 'One-command setup: fetch config, scaffold, and install packs'
241
+ subcommand 'bootstrap', Legion::CLI::Bootstrap
242
+
238
243
  desc 'update', 'Update Legion gems to latest versions'
239
244
  subcommand 'update', Legion::CLI::Update
240
245
 
@@ -242,6 +247,9 @@ module Legion
242
247
  subcommand 'init', Legion::CLI::Init
243
248
 
244
249
  # --- Interactive & shortcuts ---
250
+ desc 'codegen SUBCOMMAND', 'Manage self-generating functions'
251
+ subcommand 'codegen', CodegenCommand
252
+
245
253
  desc 'tty', 'Rich terminal UI (onboarding, AI chat, dashboard)'
246
254
  subcommand 'tty', Legion::CLI::Tty
247
255
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'base'
4
4
  require 'date'
5
+ require 'securerandom'
5
6
 
6
7
  module Legion
7
8
  module Extensions
@@ -50,7 +51,7 @@ module Legion
50
51
  def prepare # rubocop:disable Metrics/AbcSize
51
52
  @queue = queue.new
52
53
  @queue.channel.prefetch(prefetch) if defined? prefetch
53
- consumer_tag = "#{Legion::Settings[:client][:name]}_#{lex_name}_#{runner_name}_#{Thread.current.object_id}"
54
+ consumer_tag = "#{Legion::Settings[:client][:name]}_#{lex_name}_#{runner_name}_#{SecureRandom.uuid}"
54
55
  @consumer = Bunny::Consumer.new(@queue.channel, @queue, consumer_tag, false, false)
55
56
  @consumer.on_delivery do |delivery_info, metadata, payload|
56
57
  message = process_message(payload, metadata, delivery_info)
@@ -150,7 +151,7 @@ module Legion
150
151
  def subscribe # rubocop:disable Metrics/AbcSize
151
152
  log.info "[Subscription] subscribing: #{lex_name}/#{runner_name}"
152
153
  sleep(delay_start) if delay_start.positive?
153
- consumer_tag = "#{Legion::Settings[:client][:name]}_#{lex_name}_#{runner_name}_#{Thread.current.object_id}"
154
+ consumer_tag = "#{Legion::Settings[:client][:name]}_#{lex_name}_#{runner_name}_#{SecureRandom.uuid}"
154
155
  on_cancellation = block { cancel }
155
156
 
156
157
  @consumer = @queue.subscribe(manual_ack: manual_ack, block: false, consumer_tag: consumer_tag, on_cancellation: on_cancellation) do |*rmq_message|
@@ -126,6 +126,7 @@ module Legion
126
126
  if extensions
127
127
  load_extensions
128
128
  Legion::Readiness.mark_ready(:extensions)
129
+ setup_generated_functions
129
130
  end
130
131
 
131
132
  Legion::Gaia.registry&.rediscover if gaia && defined?(Legion::Gaia) && Legion::Gaia.started?
@@ -612,6 +613,15 @@ module Legion
612
613
  Legion::Extensions.hook_extensions
613
614
  end
614
615
 
616
+ def setup_generated_functions
617
+ return unless defined?(Legion::Extensions::Codegen::Helpers::GeneratedRegistry)
618
+
619
+ loaded = Legion::Extensions::Codegen::Helpers::GeneratedRegistry.load_on_boot
620
+ Legion::Logging.info("Loaded #{loaded} generated functions") if defined?(Legion::Logging) && loaded.to_i.positive?
621
+ rescue StandardError => e
622
+ Legion::Logging.warn("setup_generated_functions failed: #{e.message}") if defined?(Legion::Logging)
623
+ end
624
+
615
625
  def setup_mtls_rotation
616
626
  enabled = Legion::Settings[:security]&.dig(:mtls, :enabled)
617
627
  return unless enabled
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.6.3'
4
+ VERSION = '1.6.8'
5
5
  end
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.6.3
4
+ version: 1.6.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -447,6 +447,7 @@ files:
447
447
  - lib/legion/api/capacity.rb
448
448
  - lib/legion/api/catalog.rb
449
449
  - lib/legion/api/chains.rb
450
+ - lib/legion/api/codegen.rb
450
451
  - lib/legion/api/coldstart.rb
451
452
  - lib/legion/api/costs.rb
452
453
  - lib/legion/api/events.rb
@@ -511,6 +512,7 @@ files:
511
512
  - lib/legion/cli/apollo_command.rb
512
513
  - lib/legion/cli/audit_command.rb
513
514
  - lib/legion/cli/auth_command.rb
515
+ - lib/legion/cli/bootstrap_command.rb
514
516
  - lib/legion/cli/chain.rb
515
517
  - lib/legion/cli/chain_command.rb
516
518
  - lib/legion/cli/chat/agent_delegator.rb
@@ -576,6 +578,7 @@ files:
576
578
  - lib/legion/cli/chat_command.rb
577
579
  - lib/legion/cli/check/privacy_check.rb
578
580
  - lib/legion/cli/check_command.rb
581
+ - lib/legion/cli/codegen_command.rb
579
582
  - lib/legion/cli/cohort.rb
580
583
  - lib/legion/cli/coldstart_command.rb
581
584
  - lib/legion/cli/commit_command.rb