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 +4 -4
- data/CHANGELOG.md +25 -0
- data/lib/legion/api/codegen.rb +84 -0
- data/lib/legion/api.rb +2 -0
- data/lib/legion/cli/bootstrap_command.rb +399 -0
- data/lib/legion/cli/chat/context.rb +11 -0
- data/lib/legion/cli/codegen_command.rb +104 -0
- data/lib/legion/cli.rb +8 -0
- data/lib/legion/extensions/actors/subscription.rb +3 -2
- data/lib/legion/service.rb +10 -0
- data/lib/legion/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd3587ed7997208005a0a8746aca477701811f8e506c1d8d9345a40d180317fc
|
|
4
|
+
data.tar.gz: e805096f6577c2b1317807d50beab27bbb91527e2a02807231596fbd6a1d91ca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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}_#{
|
|
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}_#{
|
|
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|
|
data/lib/legion/service.rb
CHANGED
|
@@ -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
|
data/lib/legion/version.rb
CHANGED
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.
|
|
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
|