legionio 1.6.7 → 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: e025887526f8b9e258eb168a14a83d5c34e60668eadbe9e1ccd5c995e27d725c
4
- data.tar.gz: a85e52dc5ec39293461e7df402f2ea8f1a410d293c28141fc935cfb6023b71df
3
+ metadata.gz: cd3587ed7997208005a0a8746aca477701811f8e506c1d8d9345a40d180317fc
4
+ data.tar.gz: e805096f6577c2b1317807d50beab27bbb91527e2a02807231596fbd6a1d91ca
5
5
  SHA512:
6
- metadata.gz: 6ecefca3ab028b370f8e590cf4fa81290e3a9e106f53a5655d11928f40ec99a0d48451ab94c9516086fd81bd5c42d4523eae4d975b9df667d8db7010656884c6
7
- data.tar.gz: c5f4aa31b5431e6fb012991fe0048ac75c0d25d95e6402983590d7954491ad53e78b18d2911cb7969427cd60e63a47975d7ca89ff7642857cd857328dec559e3
6
+ metadata.gz: 5d6c20753f9803fc34cd4bb577304bc31d708df54f9e49f266dbb32073c3d06835e2d625efbed610244b5888085d823222627fb07ad889c1ff85d37d028f603a
7
+ data.tar.gz: 0a1dbbff5e24580f3b8b97fb434c626f029f449501b138d0a29f9fcb0eb15edd05808c5b34353260a6bdc4808d4f747362d8f4e6e9f3afdd47362b56035c1e0f
data/CHANGELOG.md CHANGED
@@ -1,27 +1,18 @@
1
1
  # Legion Changelog
2
2
 
3
- ## [1.6.7] - 2026-03-26
4
-
5
- ### Fixed
6
- - `setup_generated_functions` now runs only when `extensions: true` (inside the extensions gate) preventing unexpected boot side-effects in CLI flows that disable extensions
7
- - 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
8
-
9
- ## [1.6.6] - 2026-03-26
3
+ ## [1.6.8] - 2026-03-26
10
4
 
11
5
  ### Added
12
6
  - `legionio bootstrap SOURCE` command: combines `config import`, `config scaffold`, and `setup agentic` into one command
13
7
  - Pre-flight checks for klist (Kerberos ticket), brew availability, and legionio binary
14
- - `--skip-packs` flag to skip gem pack installation (config-only mode)
15
- - `--start` flag to start redis + legionio via brew services after bootstrap
16
- - `--force` flag to overwrite existing config files during bootstrap
17
- - `--json` flag for machine-readable bootstrap output
18
- - `shell_capture` helper extracted to make shell invocations stubbable in specs
19
- - 62 specs covering preflight checks, pack extraction, config fetch/write delegation, pack install, summary output, all flags, and error handling
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`
20
10
 
21
- ## [1.6.5] - 2026-03-26
11
+ ## [1.6.7] - 2026-03-26
22
12
 
23
- ### Added
24
- - `Context.to_system_prompt` appends a live self-awareness section from `lex-agentic-self` Metacognition when the gem is loaded; logic extracted into `self_awareness_hint` helper to keep `to_system_prompt` within Metrics/CyclomaticComplexity limits; guarded with `defined?()` and `rescue StandardError`
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
25
16
 
26
17
  ## [1.6.4] - 2026-03-26
27
18
 
@@ -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
data/lib/legion/cli.rb CHANGED
@@ -65,6 +65,7 @@ module Legion
65
65
  autoload :Features, 'legion/cli/features_command'
66
66
  autoload :Debug, 'legion/cli/debug_command'
67
67
  autoload :CodegenCommand, 'legion/cli/codegen_command'
68
+ autoload :Bootstrap, 'legion/cli/bootstrap_command'
68
69
 
69
70
  module Groups
70
71
  autoload :Ai, 'legion/cli/groups/ai_group'
@@ -236,6 +237,9 @@ module Legion
236
237
  desc 'setup SUBCOMMAND', 'Install feature packs and configure IDE integrations'
237
238
  subcommand 'setup', Legion::CLI::Setup
238
239
 
240
+ desc 'bootstrap SOURCE', 'One-command setup: fetch config, scaffold, and install packs'
241
+ subcommand 'bootstrap', Legion::CLI::Bootstrap
242
+
239
243
  desc 'update', 'Update Legion gems to latest versions'
240
244
  subcommand 'update', Legion::CLI::Update
241
245
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.6.7'
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.7
4
+ version: 1.6.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -512,6 +512,7 @@ files:
512
512
  - lib/legion/cli/apollo_command.rb
513
513
  - lib/legion/cli/audit_command.rb
514
514
  - lib/legion/cli/auth_command.rb
515
+ - lib/legion/cli/bootstrap_command.rb
515
516
  - lib/legion/cli/chain.rb
516
517
  - lib/legion/cli/chain_command.rb
517
518
  - lib/legion/cli/chat/agent_delegator.rb