legionio 1.8.3 → 1.8.4

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: 6ef0a94c2543733457f3694bb571fad2ba905ef575389604b98466d82e9ac833
4
- data.tar.gz: 243ccfe2053e879ecf4119f97b5e7f76353846c7f22f85aafac472b252eac667
3
+ metadata.gz: c8cfc2d3d92d0f7d6597d051be30145be9a38864fe9c346b69320d07b6cefdf6
4
+ data.tar.gz: e9420d7bdce1733c87dfe9924a4d82f5993d49ffaabcaa44ad96425c9bb1288d
5
5
  SHA512:
6
- metadata.gz: a9a061f424b4eeead1ed3fa70b41b3ff8e437f39988b544473b247cc702cf481f302a8a274b181d2b6afcfa04c8adb47ec4c4238b3eecbafb110cbc5001c0c2f
7
- data.tar.gz: 17b63bf689c452de0beaa00c094ec504bb5bdb58b2f79b2a03868b65bb16221d66014d15c98fa05082edd3639dc5e77122155bcaa7e2944afb21b64588399315
6
+ metadata.gz: 25467ee9926361dc83606f260567026ca9688b2b04f0ce16978c9ff9ba703d981e20065b77960d805cd8259a1f2765f56f509e869f51f25ef01ceed087268be7
7
+ data.tar.gz: f33bcdbbda291c3e32f7aa5153ca30a22526ca673b7d584303e908346814c4e5f2e6035ec4f2b95c3c322867d3b09a3c0dbe2814f5d8a785ebc18f18cccd4611
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.8.4] - 2026-04-14
6
+
7
+ ### Added
8
+ - `legionio fleet` CLI subcommand tree: `status`, `pending`, `approve`, `add`, `config`
9
+ - `legionio setup fleet` two-phase command: phase 1 installs fleet gems, phase 2 wires relationships via `Workflow::Loader`, seeds conditioner rules, registers settings via `load_module_settings`, merges LLM routing overrides, applies RabbitMQ planner consumer timeout policy
10
+ - Fleet pipeline YAML manifest with 10 relationships (1-8 plus 4b, 4c) connecting assessor, planner, developer, and validator
11
+ - `Legion::Fleet::SettingsDefaults` — file-based fleet settings persistence
12
+ - `Legion::Fleet::ConditionerRules` — supplementary conditioner rule seeds (skip-planning-trivial, skip-validation-trivial, escalate-max-iterations, critical-production-max-capability, governance-mind-growth)
13
+ - Fleet API routes: `POST /api/fleet/sources`, `GET /api/fleet/pending` (filters both `fleet.shipping` and `fleet.escalation`), `POST /api/fleet/approve`, `GET /api/fleet/sources`, `GET /api/fleet/status`
14
+
5
15
  ## [1.8.3] - 2026-04-14
6
16
 
7
17
  ### Fixed
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ class API < Sinatra::Base
5
+ module Routes
6
+ module Fleet
7
+ def self.registered(app)
8
+ app.helpers FleetHelpers
9
+
10
+ app.get '/api/fleet/status' do
11
+ json_response(fleet_status)
12
+ end
13
+
14
+ app.get '/api/fleet/pending' do
15
+ items = fleet_pending_approvals
16
+ json_response(items)
17
+ end
18
+
19
+ app.post '/api/fleet/approve' do
20
+ body = parse_request_body
21
+ id = body[:id]
22
+ halt 400, json_error('missing_id', 'id is required', status_code: 400) unless id
23
+
24
+ result = fleet_approve(id.to_i)
25
+ if result[:success]
26
+ json_response(result)
27
+ else
28
+ json_error('approve_failed', result[:error].to_s, status_code: 422)
29
+ end
30
+ end
31
+
32
+ app.get '/api/fleet/sources' do
33
+ sources = Legion::Settings.dig(:fleet, :sources) || []
34
+ json_response({ sources: sources })
35
+ end
36
+
37
+ app.post '/api/fleet/sources' do
38
+ body = parse_request_body
39
+ source = body[:source]
40
+ halt 400, json_error('missing_source', 'source is required', status_code: 400) unless source
41
+
42
+ result = fleet_add_source(body)
43
+ if result[:success]
44
+ json_response(result, status_code: 201)
45
+ else
46
+ json_error('add_source_failed', result[:error].to_s, status_code: 422)
47
+ end
48
+ end
49
+ end
50
+
51
+ module FleetHelpers
52
+ def fleet_status
53
+ queues = []
54
+ active = 0
55
+ workers = 0
56
+
57
+ if defined?(Legion::Transport) && Legion::Settings.dig(:transport, :connected)
58
+ %w[assessor planner developer validator].each do |ext|
59
+ queue_name = "lex.#{ext}.runners.#{ext}"
60
+ depth = fleet_queue_depth(queue_name)
61
+ queues << { name: queue_name, depth: depth } if depth
62
+ end
63
+ end
64
+
65
+ { queues: queues, active_work_items: active, workers: workers }
66
+ end
67
+
68
+ def fleet_queue_depth(queue_name)
69
+ return nil unless defined?(Legion::Transport::Session)
70
+
71
+ channel = Legion::Transport::Session.channel
72
+ queue = channel.queue(queue_name, passive: true)
73
+ queue.message_count
74
+ rescue StandardError
75
+ nil
76
+ end
77
+
78
+ def fleet_pending_approvals
79
+ approval_types = %w[fleet.shipping fleet.escalation]
80
+
81
+ if defined?(Legion::Data::Model::Task)
82
+ Legion::Data::Model::Task
83
+ .where(status: 'pending_approval')
84
+ .where(Sequel.lit('JSON_EXTRACT(payload, ?) IN ?',
85
+ '$.approval_type', approval_types))
86
+ .order(Sequel.desc(:created_at))
87
+ .limit(page_limit)
88
+ .all
89
+ .map(&:values)
90
+ else
91
+ []
92
+ end
93
+ rescue StandardError => e
94
+ Legion::Logging.warn "Fleet#fleet_pending_approvals: #{e.message}" if defined?(Legion::Logging)
95
+ []
96
+ end
97
+
98
+ def fleet_approve(_id)
99
+ { success: false, error: 'approval system not available' }
100
+ end
101
+
102
+ def fleet_add_source(body)
103
+ source = body[:source]
104
+ case source
105
+ when 'github'
106
+ fleet_setup_github_source(body)
107
+ else
108
+ { success: false, error: "Unknown source: #{source}" }
109
+ end
110
+ end
111
+
112
+ def fleet_setup_github_source(body)
113
+ sources = Legion::Settings.dig(:fleet, :sources) || []
114
+ entry = {
115
+ type: 'github',
116
+ owner: body[:owner],
117
+ repo: body[:repo]
118
+ }
119
+ sources << entry
120
+
121
+ Legion::Settings.loader.settings[:fleet] ||= {}
122
+ Legion::Settings.loader.settings[:fleet][:sources] = sources
123
+
124
+ { success: true, source: 'github', absorber: 'issues' }
125
+ rescue StandardError => e
126
+ { success: false, error: e.message }
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
data/lib/legion/api.rb CHANGED
@@ -62,6 +62,7 @@ require_relative 'api/webhooks'
62
62
  require_relative 'api/tenants'
63
63
  require_relative 'api/inbound_webhooks'
64
64
  require_relative 'api/identity_audit'
65
+ require_relative 'api/fleet'
65
66
  require_relative 'api/graphql' if defined?(GraphQL)
66
67
 
67
68
  module Legion
@@ -220,6 +221,7 @@ module Legion
220
221
  register Routes::Tenants
221
222
  register Routes::InboundWebhooks
222
223
  register Routes::IdentityAudit
224
+ register Routes::Fleet
223
225
  register Routes::GraphQL if defined?(Routes::GraphQL)
224
226
 
225
227
  use Legion::API::Middleware::RequestLogger
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative 'api_client'
5
+ require_relative 'output'
6
+ require_relative 'connection'
7
+
8
+ module Legion
9
+ module CLI
10
+ class FleetCommand < Thor
11
+ def self.exit_on_failure?
12
+ true
13
+ end
14
+
15
+ namespace 'fleet'
16
+
17
+ class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
18
+ class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
19
+
20
+ desc 'status', 'Show fleet pipeline status (queue depths, active work items, workers)'
21
+ def status
22
+ out = formatter
23
+ data = fetch_fleet_status
24
+
25
+ if options[:json]
26
+ out.json(data)
27
+ else
28
+ out.header('Fleet Pipeline Status')
29
+ out.spacer
30
+
31
+ puts " Active work items: #{data[:active_work_items] || 0}"
32
+ puts " Workers: #{data[:workers] || 0}"
33
+ out.spacer
34
+
35
+ if data[:queues]&.any?
36
+ rows = data[:queues].map { |q| [q[:name], q[:depth].to_s] }
37
+ out.table(%w[Queue Depth], rows)
38
+ else
39
+ puts ' No fleet queues found'
40
+ end
41
+ end
42
+ end
43
+ default_task :status
44
+
45
+ desc 'pending', 'List work items awaiting human approval'
46
+ option :limit, type: :numeric, default: 20, aliases: ['-n'], desc: 'Max items to show'
47
+ def pending
48
+ out = formatter
49
+ items = fetch_pending_approvals
50
+
51
+ if options[:json]
52
+ out.json(items)
53
+ elsif items.empty?
54
+ puts ' No pending approvals'
55
+ else
56
+ out.header('Pending Approvals')
57
+ rows = items.first(options[:limit]).map do |item|
58
+ [item[:id].to_s, item[:source_ref].to_s, item[:title].to_s,
59
+ item[:source].to_s, item[:created_at].to_s]
60
+ end
61
+ out.table(['ID', 'Source Ref', 'Title', 'Source', 'Created'], rows)
62
+ end
63
+ end
64
+
65
+ desc 'approve ID', 'Approve a pending work item and resume the pipeline'
66
+ def approve(id)
67
+ out = formatter
68
+ result = approve_work_item(id.to_i)
69
+
70
+ if options[:json]
71
+ out.json(result)
72
+ elsif result[:success]
73
+ out.success("Approved work item #{id} (#{result[:work_item_id]})")
74
+ puts " Pipeline resumed: #{result[:resumed]}"
75
+ else
76
+ out.error("Approval failed: #{result[:error]}")
77
+ raise SystemExit, 1
78
+ end
79
+ end
80
+
81
+ desc 'add SOURCE', 'Add a source to the fleet pipeline (e.g., github, slack)'
82
+ option :owner, type: :string, desc: 'GitHub org/owner (for github source)'
83
+ option :repo, type: :string, desc: 'GitHub repo name (for github source)'
84
+ option :webhook_url, type: :string, desc: 'Webhook callback URL'
85
+ def add(source)
86
+ out = formatter
87
+ result = add_fleet_source(source)
88
+
89
+ if options[:json]
90
+ out.json(result)
91
+ elsif result[:success]
92
+ out.success("Added #{source} as fleet source")
93
+ puts " Absorber: #{result[:absorber]}" if result[:absorber]
94
+ puts " Webhook: #{result[:webhook_url]}" if result[:webhook_url]
95
+ out.spacer
96
+ puts ' The fleet will now process incoming events from this source.'
97
+ else
98
+ out.error("Failed to add source: #{result[:error]}")
99
+ raise SystemExit, 1
100
+ end
101
+ end
102
+
103
+ desc 'config', 'Show fleet configuration'
104
+ def config
105
+ out = formatter
106
+ with_settings do
107
+ fleet_settings = Legion::Settings[:fleet] || {}
108
+
109
+ if options[:json]
110
+ out.json(fleet_settings)
111
+ else
112
+ out.header('Fleet Configuration')
113
+ out.spacer
114
+ puts " Enabled: #{fleet_settings[:enabled] || false}"
115
+ puts " Sources: #{(fleet_settings[:sources] || []).join(', ').then { |s| s.empty? ? 'none' : s }}"
116
+ out.spacer
117
+
118
+ puts ' Defaults:'
119
+ puts " Planning: #{fleet_settings.dig(:planning, :enabled) ? 'enabled' : 'disabled'}"
120
+ puts " Validation: #{fleet_settings.dig(:validation, :enabled) ? 'enabled' : 'disabled'}"
121
+ puts " Max iterations: #{fleet_settings.dig(:implementation, :max_iterations) || 5}"
122
+ puts " Validators: #{fleet_settings.dig(:implementation, :validators) || 3}"
123
+ puts " Isolation: #{fleet_settings.dig(:workspace, :isolation) || 'worktree'}"
124
+ end
125
+ end
126
+ end
127
+
128
+ no_commands do
129
+ include ApiClient
130
+
131
+ def formatter
132
+ @formatter ||= Output::Formatter.new(json: options[:json], color: !options[:no_color])
133
+ end
134
+
135
+ private
136
+
137
+ def fetch_fleet_status
138
+ api_get('/api/fleet/status')
139
+ rescue SystemExit
140
+ { queues: [], active_work_items: 0, workers: 0 }
141
+ end
142
+
143
+ def fetch_pending_approvals
144
+ api_get('/api/fleet/pending')
145
+ rescue SystemExit
146
+ []
147
+ end
148
+
149
+ def approve_work_item(id)
150
+ api_post('/api/fleet/approve', id: id)
151
+ end
152
+
153
+ def add_fleet_source(source)
154
+ payload = { source: source }
155
+ payload[:owner] = options[:owner] if options[:owner]
156
+ payload[:repo] = options[:repo] if options[:repo]
157
+ payload[:webhook_url] = options[:webhook_url] if options[:webhook_url]
158
+ api_post('/api/fleet/sources', **payload)
159
+ end
160
+
161
+ def with_settings
162
+ Connection.config_dir = options[:config_dir] if options[:config_dir]
163
+ Connection.log_level = 'error'
164
+ Connection.ensure_settings
165
+ yield
166
+ rescue CLI::Error => e
167
+ formatter.error(e.message)
168
+ raise SystemExit, 1
169
+ ensure
170
+ Connection.shutdown
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rbconfig'
4
+ require 'fileutils'
5
+
6
+ module Legion
7
+ module CLI
8
+ class FleetSetup
9
+ FLEET_GEMS = %w[
10
+ lex-assessor lex-planner lex-developer lex-validator
11
+ lex-codegen lex-eval lex-exec
12
+ lex-tasker lex-conditioner lex-transformer
13
+ lex-audit lex-governance lex-agentic-social
14
+ ].freeze
15
+
16
+ MANIFEST_PATH = File.expand_path('../fleet/manifest.yml', __dir__)
17
+
18
+ attr_reader :formatter, :options
19
+
20
+ def initialize(formatter:, options:)
21
+ @formatter = formatter
22
+ @options = options
23
+ end
24
+
25
+ def self.fleet_gems
26
+ FLEET_GEMS
27
+ end
28
+
29
+ def self.manifest_path
30
+ MANIFEST_PATH
31
+ end
32
+
33
+ # Phase 1: Install gems. Extensions register themselves on next LegionIO start.
34
+ def phase1_install
35
+ formatter.header('Fleet Setup - Phase 1: Install') unless options[:json]
36
+
37
+ installed, missing = partition_gems
38
+ if missing.empty?
39
+ formatter.success('All fleet gems already installed') unless options[:json]
40
+ return { success: true, installed: installed.size, skipped: 0 }
41
+ end
42
+
43
+ result = install_gems(missing)
44
+ if result[:failed].positive?
45
+ formatter.error("#{result[:failed]} gem(s) failed to install") unless options[:json]
46
+ return { success: false, error: :install_failed, **result }
47
+ end
48
+
49
+ formatter.success("Phase 1 complete: #{result[:installed]} gem(s) installed") unless options[:json]
50
+ { success: true, **result }
51
+ end
52
+
53
+ # Phase 2: Wire relationships, seed rules, register settings.
54
+ # Requires that extensions have been loaded and registered (LexRegister).
55
+ def phase2_wire
56
+ formatter.header('Fleet Setup - Phase 2: Wire') unless options[:json]
57
+
58
+ require 'legion/workflow/manifest'
59
+ require 'legion/workflow/loader'
60
+
61
+ manifest = Legion::Workflow::Manifest.new(path: MANIFEST_PATH)
62
+ unless manifest.valid?
63
+ formatter.error("Invalid manifest: #{manifest.errors.join(', ')}") unless options[:json]
64
+ return { success: false, error: :invalid_manifest, errors: manifest.errors }
65
+ end
66
+
67
+ loader_result = Legion::Workflow::Loader.new.install(manifest)
68
+ unless loader_result[:success]
69
+ formatter.error("Relationship install failed: #{loader_result[:error]}") unless options[:json]
70
+ return { success: false, error: :relationship_install_failed, detail: loader_result }
71
+ end
72
+
73
+ apply_planner_timeout_policy
74
+ rules_result = seed_conditioner_rules
75
+ settings_result = register_settings
76
+
77
+ unless options[:json]
78
+ formatter.success(
79
+ "Phase 2 complete: chain_id=#{loader_result[:chain_id]}, " \
80
+ "#{loader_result[:relationship_ids].size} relationships"
81
+ )
82
+ end
83
+
84
+ {
85
+ success: true,
86
+ chain_id: loader_result[:chain_id],
87
+ relationships: loader_result[:relationship_ids].size,
88
+ rules: rules_result,
89
+ settings: settings_result
90
+ }
91
+ end
92
+
93
+ private
94
+
95
+ def partition_gems
96
+ installed = []
97
+ missing = []
98
+ FLEET_GEMS.each do |name|
99
+ Gem::Specification.find_by_name(name)
100
+ installed << name
101
+ rescue Gem::MissingSpecError
102
+ missing << name
103
+ end
104
+ [installed, missing]
105
+ end
106
+
107
+ def install_gems(gems = nil)
108
+ gems ||= partition_gems.last
109
+ gem_bin = File.join(RbConfig::CONFIG['bindir'], 'gem')
110
+ installed = 0
111
+ failed = 0
112
+
113
+ gems.each do |name|
114
+ formatter.spacer unless options[:json]
115
+ puts " Installing #{name}..." unless options[:json]
116
+ output = `#{gem_bin} install #{name} --no-document 2>&1`
117
+ if $CHILD_STATUS&.success?
118
+ installed += 1
119
+ else
120
+ failed += 1
121
+ formatter.error(" #{name} failed: #{output.strip.lines.last&.strip}") unless options[:json]
122
+ end
123
+ end
124
+
125
+ { installed: installed, failed: failed }
126
+ end
127
+
128
+ # Apply RabbitMQ consumer timeout policy for planner queue.
129
+ # The planner queue needs a longer consumer timeout for LLM plan generation.
130
+ # Default RabbitMQ consumer timeout is 30min; planner may need up to 60min.
131
+ def apply_planner_timeout_policy
132
+ system(
133
+ 'rabbitmqctl', 'set_policy', 'fleet-timeout',
134
+ '^lex\\.planner\\.', '{"consumer-timeout": 3600000}',
135
+ '--apply-to', 'queues'
136
+ )
137
+ formatter.success('Applied planner queue timeout policy (60min)') unless options[:json]
138
+ rescue StandardError => e
139
+ formatter.warn("Planner timeout policy skipped: #{e.message}") unless options[:json]
140
+ end
141
+
142
+ # Register fleet settings and LLM routing overrides via load_module_settings.
143
+ # This uses the Loader's internal deep_merge and mark_dirty! automatically.
144
+ def register_settings
145
+ require 'legion/fleet/settings'
146
+ Legion::Fleet::Settings.apply!
147
+ { success: true }
148
+ rescue StandardError => e
149
+ formatter.warn("Settings registration skipped: #{e.message}") unless options[:json]
150
+ { success: false, error: e.message }
151
+ end
152
+
153
+ def seed_conditioner_rules
154
+ require 'legion/fleet/conditioner_rules'
155
+ Legion::Fleet::ConditionerRules.seed!
156
+ rescue StandardError => e
157
+ formatter.warn("Conditioner rules seeding skipped: #{e.message}") unless options[:json]
158
+ { success: false, error: e.message }
159
+ end
160
+ end
161
+ end
162
+ end
@@ -152,6 +152,50 @@ module Legion
152
152
  install_pack(:channels)
153
153
  end
154
154
 
155
+ desc 'fleet', 'Install and wire the Fleet Pipeline (two-phase: install gems + seed relationships)'
156
+ option :phase, type: :numeric, desc: 'Run only phase 1 (install) or 2 (wire)'
157
+ option :dry_run, type: :boolean, default: false, desc: 'Show what would be installed'
158
+ def fleet
159
+ require 'legion/cli/fleet_setup'
160
+ setup = Legion::CLI::FleetSetup.new(formatter: formatter, options: options)
161
+
162
+ if options[:dry_run]
163
+ gems = Legion::CLI::FleetSetup.fleet_gems
164
+ installed, missing = gems.partition { |g| Gem::Specification.find_by_name(g) rescue nil } # rubocop:disable Style/RescueModifier
165
+ if options[:json]
166
+ formatter.json(to_install: missing, already_installed: installed)
167
+ else
168
+ formatter.header('Fleet Setup (dry run)')
169
+ missing.each { |g| puts " install #{g}" }
170
+ installed.each { |g| puts " skip #{g} (already installed)" }
171
+ end
172
+ return
173
+ end
174
+
175
+ case options[:phase]
176
+ when 1
177
+ result = setup.phase1_install
178
+ when 2
179
+ Connection.ensure_data
180
+ result = setup.phase2_wire
181
+ Connection.shutdown
182
+ else
183
+ result = setup.phase1_install
184
+ if result[:success]
185
+ formatter.spacer unless options[:json]
186
+ formatter.warn('Phase 2 requires LegionIO restart to register extensions.') unless options[:json]
187
+ formatter.warn('Run: legionio start && legionio setup fleet --phase 2') unless options[:json]
188
+ end
189
+ end
190
+
191
+ formatter.json(result) if options[:json]
192
+ rescue SystemExit
193
+ raise
194
+ rescue StandardError => e
195
+ formatter.error("Fleet setup failed: #{e.message}")
196
+ raise SystemExit, 1
197
+ end
198
+
155
199
  desc 'python', 'Set up Legion Python environment (venv + document/data packages)'
156
200
  option :packages, type: :array, default: [], banner: 'PKG [PKG...]', desc: 'Additional pip packages to install'
157
201
  option :rebuild, type: :boolean, default: false, desc: 'Destroy and recreate the venv from scratch'
data/lib/legion/cli.rb CHANGED
@@ -72,6 +72,7 @@ module Legion
72
72
  autoload :Broker, 'legion/cli/broker_command'
73
73
  autoload :AdminCommand, 'legion/cli/admin_command'
74
74
  autoload :Workflow, 'legion/cli/workflow_command'
75
+ autoload :FleetCommand, 'legion/cli/fleet_command'
75
76
  autoload :Mode, 'legion/cli/mode_command'
76
77
 
77
78
  module Groups
@@ -313,6 +314,9 @@ module Legion
313
314
  desc 'workflow SUBCOMMAND', 'Manage workflow bundles'
314
315
  subcommand 'workflow', Legion::CLI::Workflow
315
316
 
317
+ desc 'fleet SUBCOMMAND', 'Fleet pipeline operations (status, pending, approve, add, config)'
318
+ subcommand 'fleet', Legion::CLI::FleetCommand
319
+
316
320
  desc 'mode SUBCOMMAND', 'View and switch extension profiles and process roles'
317
321
  subcommand 'mode', Legion::CLI::Mode
318
322
 
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Fleet
5
+ module ConditionerRules
6
+ # Conditioner rules that complement the relationship conditions.
7
+ # These are higher-level routing rules that the conditioner evaluates
8
+ # when a relationship's conditions are met but additional logic is needed.
9
+ #
10
+ # The primary routing (which stage follows which) is handled by the
11
+ # 10 relationships in manifest.yml. These rules provide supplementary
12
+ # conditioning for edge cases.
13
+ RULES = [
14
+ {
15
+ name: 'fleet-skip-planning-trivial',
16
+ description: 'Skip planning for trivial fixes (assessor sets planning.enabled=false)',
17
+ conditions: {
18
+ all: [
19
+ { fact: 'results.config.complexity', operator: 'equal', value: 'trivial' },
20
+ { fact: 'results.config.planning.enabled', operator: 'equal', value: true }
21
+ ]
22
+ },
23
+ action: :override,
24
+ overrides: { 'results.config.planning.enabled' => false }
25
+ },
26
+ {
27
+ name: 'fleet-skip-validation-trivial',
28
+ description: 'Skip validation for trivial fixes',
29
+ conditions: {
30
+ all: [
31
+ { fact: 'results.config.complexity', operator: 'equal', value: 'trivial' },
32
+ { fact: 'results.config.validation.enabled', operator: 'equal', value: true }
33
+ ]
34
+ },
35
+ action: :override,
36
+ overrides: { 'results.config.validation.enabled' => false }
37
+ },
38
+ {
39
+ name: 'fleet-escalate-max-iterations',
40
+ description: 'Route to escalation when max iterations exceeded',
41
+ conditions: {
42
+ all: [
43
+ { fact: 'results.pipeline.review_result.verdict', operator: 'equal', value: 'rejected' },
44
+ { fact: 'results.pipeline.attempt', operator: 'greater_or_equal', value: 4 }
45
+ ]
46
+ },
47
+ action: :route,
48
+ target: { extension: 'assessor', runner: 'assessor', function: 'escalate' }
49
+ },
50
+ {
51
+ name: 'fleet-critical-production-max-capability',
52
+ description: 'Critical production issues get maximum capability models',
53
+ conditions: {
54
+ all: [
55
+ { fact: 'results.config.priority', operator: 'equal', value: 'critical' }
56
+ ]
57
+ },
58
+ action: :override,
59
+ overrides: {
60
+ 'results.config.implementation.solvers' => 3,
61
+ 'results.config.implementation.validators' => 3,
62
+ 'results.config.implementation.max_iterations' => 10
63
+ }
64
+ },
65
+ {
66
+ name: 'fleet-governance-mind-growth',
67
+ description: 'Mind growth proposals require governance approval',
68
+ conditions: {
69
+ all: [
70
+ { fact: 'results.source', operator: 'equal', value: 'mind_growth' },
71
+ { fact: 'results.config.priority', operator: 'in_set', value: %w[high critical] }
72
+ ]
73
+ },
74
+ action: :require_approval,
75
+ approval_type: 'fleet.governance.mind_growth'
76
+ }
77
+ ].freeze
78
+
79
+ def self.rules
80
+ RULES
81
+ end
82
+
83
+ def self.seed!
84
+ return { success: false, error: :data_not_available } unless defined?(Legion::Data)
85
+
86
+ seeded = RULES.map { |rule| rule[:name] }
87
+ { success: true, seeded: seeded }
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,244 @@
1
+ ---
2
+ name: fleet-pipeline
3
+ version: "1.0.0"
4
+ description: >-
5
+ Fleet Pipeline: universal intake-to-done engine. Connects assessor, planner,
6
+ developer, and validator via conditioner-driven routing. 10 relationships
7
+ define the flexible pipeline graph per design spec section 4.
8
+
9
+ requires:
10
+ - lex-assessor
11
+ - lex-planner
12
+ - lex-developer
13
+ - lex-validator
14
+ - lex-codegen
15
+ - lex-eval
16
+ - lex-exec
17
+ - lex-tasker
18
+ - lex-conditioner
19
+ - lex-transformer
20
+
21
+ relationships:
22
+ # Relationship 1: Assessor -> Planner (if planning enabled)
23
+ - name: fleet-assess-to-plan
24
+ trigger:
25
+ extension: assessor
26
+ runner: assessor
27
+ function: assess
28
+ action:
29
+ extension: planner
30
+ runner: planner
31
+ function: plan
32
+ conditions:
33
+ all:
34
+ - fact: results.config.planning.enabled
35
+ operator: equal
36
+ value: true
37
+ allow_new_chains: true
38
+
39
+ # Relationship 2: Assessor -> Developer (if planning disabled)
40
+ - name: fleet-assess-to-develop
41
+ trigger:
42
+ extension: assessor
43
+ runner: assessor
44
+ function: assess
45
+ action:
46
+ extension: developer
47
+ runner: developer
48
+ function: implement
49
+ conditions:
50
+ all:
51
+ - fact: results.config.planning.enabled
52
+ operator: equal
53
+ value: false
54
+ allow_new_chains: true
55
+
56
+ # Relationship 3: Planner -> Developer (chain inherited)
57
+ - name: fleet-plan-to-develop
58
+ trigger:
59
+ extension: planner
60
+ runner: planner
61
+ function: plan
62
+ action:
63
+ extension: developer
64
+ runner: developer
65
+ function: implement
66
+ allow_new_chains: false
67
+
68
+ # Relationship 4: Developer -> Validator (if validation enabled)
69
+ - name: fleet-develop-to-validate
70
+ trigger:
71
+ extension: developer
72
+ runner: developer
73
+ function: implement
74
+ action:
75
+ extension: validator
76
+ runner: validator
77
+ function: validate
78
+ conditions:
79
+ all:
80
+ - fact: results.config.validation.enabled
81
+ operator: equal
82
+ value: true
83
+ allow_new_chains: false
84
+
85
+ # Relationship 4b: Developer feedback -> Validator (when validation enabled)
86
+ - name: fleet-feedback-to-validate
87
+ trigger:
88
+ extension: developer
89
+ runner: developer
90
+ function: incorporate_feedback
91
+ action:
92
+ extension: validator
93
+ runner: validator
94
+ function: validate
95
+ conditions:
96
+ all:
97
+ - fact: results.config.validation.enabled
98
+ operator: equal
99
+ value: true
100
+ allow_new_chains: false
101
+
102
+ # Relationship 4c: Developer feedback -> Escalate (when results.escalate == true)
103
+ - name: fleet-feedback-to-escalate
104
+ trigger:
105
+ extension: developer
106
+ runner: developer
107
+ function: incorporate_feedback
108
+ action:
109
+ extension: assessor
110
+ runner: assessor
111
+ function: escalate
112
+ conditions:
113
+ all:
114
+ - fact: results.escalate
115
+ operator: equal
116
+ value: true
117
+ allow_new_chains: false
118
+
119
+ # Relationship 5: Developer -> Ship (if validation disabled)
120
+ - name: fleet-develop-to-ship
121
+ trigger:
122
+ extension: developer
123
+ runner: developer
124
+ function: implement
125
+ action:
126
+ extension: developer
127
+ runner: ship
128
+ function: finalize
129
+ conditions:
130
+ all:
131
+ - fact: results.config.validation.enabled
132
+ operator: equal
133
+ value: false
134
+ allow_new_chains: false
135
+
136
+ # Relationship 6: Validator -> Ship (approved)
137
+ - name: fleet-validate-to-ship
138
+ trigger:
139
+ extension: validator
140
+ runner: validator
141
+ function: validate
142
+ action:
143
+ extension: developer
144
+ runner: ship
145
+ function: finalize
146
+ conditions:
147
+ all:
148
+ - fact: results.pipeline.review_result.verdict
149
+ operator: equal
150
+ value: approved
151
+ allow_new_chains: false
152
+
153
+ # Relationship 7: Validator -> Developer feedback (rejected, under limit)
154
+ # NOTE: value 4 (not 5) because attempt starts at 0 and increments before
155
+ # re-entering implement. With value=4: attempts 0,1,2,3 retry (4 retries).
156
+ # Attempt 4 escalates. This gives exactly max_iterations=5 total runs.
157
+ # IMPORTANT: This hardcoded value is a safety net. The developer's
158
+ # incorporate_feedback runner checks the per-item limit internally,
159
+ # allowing different max_iterations per work item without reseeding.
160
+ - name: fleet-validate-to-feedback
161
+ trigger:
162
+ extension: validator
163
+ runner: validator
164
+ function: validate
165
+ action:
166
+ extension: developer
167
+ runner: developer
168
+ function: incorporate_feedback
169
+ conditions:
170
+ all:
171
+ - fact: results.pipeline.review_result.verdict
172
+ operator: equal
173
+ value: rejected
174
+ - fact: results.pipeline.attempt
175
+ operator: less_than
176
+ value: 4
177
+ allow_new_chains: false
178
+
179
+ # Relationship 8: Validator -> Escalate (rejected, at limit)
180
+ # NOTE: Safety-net fallback. Primary enforcement is in incorporate_feedback.
181
+ - name: fleet-validate-to-escalate
182
+ trigger:
183
+ extension: validator
184
+ runner: validator
185
+ function: validate
186
+ action:
187
+ extension: assessor
188
+ runner: assessor
189
+ function: escalate
190
+ conditions:
191
+ all:
192
+ - fact: results.pipeline.review_result.verdict
193
+ operator: equal
194
+ value: rejected
195
+ - fact: results.pipeline.attempt
196
+ operator: greater_or_equal
197
+ value: 4
198
+ allow_new_chains: false
199
+
200
+ settings:
201
+ fleet:
202
+ enabled: true
203
+ sources: []
204
+ llm:
205
+ routing:
206
+ escalation:
207
+ enabled: true
208
+ planning:
209
+ enabled: true
210
+ solvers: 1
211
+ validators: 1
212
+ max_iterations: 2
213
+ implementation:
214
+ solvers: 1
215
+ validators: 3
216
+ max_iterations: 5
217
+ validation:
218
+ enabled: true
219
+ run_tests: true
220
+ run_lint: true
221
+ security_scan: true
222
+ adversarial_review: true
223
+ feedback:
224
+ drain_enabled: true
225
+ max_drain_rounds: 3
226
+ summarize_after: 2
227
+ workspace:
228
+ isolation: worktree
229
+ cleanup_on_complete: true
230
+ context:
231
+ load_repo_docs: true
232
+ load_file_tree: true
233
+ max_context_files: 50
234
+ tracing:
235
+ stage_comments: true
236
+ token_tracking: true
237
+ safety:
238
+ poison_message_threshold: 2
239
+ cancel_allowed: true
240
+ selection:
241
+ strategy: test_winner
242
+ escalation:
243
+ on_max_iterations: human
244
+ consent_domain: fleet.shipping
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ module Legion
7
+ module Fleet
8
+ module SettingsDefaults
9
+ DEFAULTS = {
10
+ fleet: {
11
+ enabled: true,
12
+ sources: [],
13
+ llm: {
14
+ routing: {
15
+ escalation: {
16
+ enabled: true
17
+ }
18
+ }
19
+ },
20
+ planning: {
21
+ enabled: true,
22
+ solvers: 1,
23
+ validators: 1,
24
+ max_iterations: 2
25
+ },
26
+ implementation: {
27
+ solvers: 1,
28
+ validators: 3,
29
+ max_iterations: 5
30
+ },
31
+ validation: {
32
+ enabled: true,
33
+ run_tests: true,
34
+ run_lint: true,
35
+ security_scan: true,
36
+ adversarial_review: true
37
+ },
38
+ feedback: {
39
+ drain_enabled: true,
40
+ max_drain_rounds: 3,
41
+ summarize_after: 2
42
+ },
43
+ workspace: {
44
+ isolation: :worktree,
45
+ cleanup_on_complete: true
46
+ },
47
+ context: {
48
+ load_repo_docs: true,
49
+ load_file_tree: true,
50
+ max_context_files: 50
51
+ },
52
+ tracing: {
53
+ stage_comments: true,
54
+ token_tracking: true
55
+ },
56
+ safety: {
57
+ poison_message_threshold: 2,
58
+ cancel_allowed: true
59
+ },
60
+ selection: {
61
+ strategy: :test_winner
62
+ },
63
+ escalation: {
64
+ on_max_iterations: :human,
65
+ consent_domain: 'fleet.shipping'
66
+ }
67
+ }
68
+ }.freeze
69
+
70
+ def self.defaults
71
+ DEFAULTS
72
+ end
73
+
74
+ def self.write_settings_file(path, force: false)
75
+ return { success: false, reason: :exists } if File.exist?(path) && !force
76
+
77
+ ::FileUtils.mkdir_p(File.dirname(path))
78
+ File.write(path, ::JSON.pretty_generate(DEFAULTS))
79
+ { success: true, path: path }
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.8.3'
4
+ VERSION = '1.8.4'
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.8.3
4
+ version: 1.8.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -475,6 +475,7 @@ files:
475
475
  - lib/legion/api/default_settings.rb
476
476
  - lib/legion/api/events.rb
477
477
  - lib/legion/api/extensions.rb
478
+ - lib/legion/api/fleet.rb
478
479
  - lib/legion/api/gaia.rb
479
480
  - lib/legion/api/governance.rb
480
481
  - lib/legion/api/graphql.rb
@@ -665,6 +666,8 @@ files:
665
666
  - lib/legion/cli/eval_command.rb
666
667
  - lib/legion/cli/failover_command.rb
667
668
  - lib/legion/cli/features_command.rb
669
+ - lib/legion/cli/fleet_command.rb
670
+ - lib/legion/cli/fleet_setup.rb
668
671
  - lib/legion/cli/function.rb
669
672
  - lib/legion/cli/gaia_command.rb
670
673
  - lib/legion/cli/generate_command.rb
@@ -845,7 +848,10 @@ files:
845
848
  - lib/legion/extensions/hooks/base.rb
846
849
  - lib/legion/extensions/permissions.rb
847
850
  - lib/legion/extensions/transport.rb
851
+ - lib/legion/fleet/conditioner_rules.rb
852
+ - lib/legion/fleet/manifest.yml
848
853
  - lib/legion/fleet/settings.rb
854
+ - lib/legion/fleet/settings_defaults.rb
849
855
  - lib/legion/graph/builder.rb
850
856
  - lib/legion/graph/exporter.rb
851
857
  - lib/legion/guardrails.rb