lex-agentic-executive 0.1.12 → 0.2.0

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.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/lib/legion/extensions/agentic/executive/goal_management/client.rb +23 -0
  4. data/lib/legion/extensions/agentic/executive/goal_management/helpers/decomposer.rb +107 -0
  5. data/lib/legion/extensions/agentic/executive/goal_management/helpers/feedback_listener.rb +63 -0
  6. data/lib/legion/extensions/agentic/executive/goal_management/helpers/goal.rb +41 -12
  7. data/lib/legion/extensions/agentic/executive/goal_management/helpers/goal_engine.rb +99 -10
  8. data/lib/legion/extensions/agentic/executive/goal_management/helpers/goal_persistence.rb +145 -0
  9. data/lib/legion/extensions/agentic/executive/goal_management/helpers/task_dispatcher.rb +109 -0
  10. data/lib/legion/extensions/agentic/executive/goal_management/runners/goal_management.rb +96 -36
  11. data/lib/legion/extensions/agentic/executive/goal_management.rb +3 -0
  12. data/lib/legion/extensions/agentic/executive/version.rb +1 -1
  13. data/lib/legion/extensions/agentic/executive/volition/client.rb +3 -1
  14. data/lib/legion/extensions/agentic/executive/volition/helpers/goal_bridge.rb +86 -0
  15. data/lib/legion/extensions/agentic/executive/volition/runners/volition.rb +13 -6
  16. data/lib/legion/extensions/agentic/executive/volition.rb +1 -0
  17. data/spec/integration/autonomous_goal_pipeline_spec.rb +107 -0
  18. data/spec/legion/extensions/agentic/executive/goal_management/helpers/decomposer_spec.rb +152 -0
  19. data/spec/legion/extensions/agentic/executive/goal_management/helpers/feedback_listener_spec.rb +104 -0
  20. data/spec/legion/extensions/agentic/executive/goal_management/helpers/goal_engine_spec.rb +32 -0
  21. data/spec/legion/extensions/agentic/executive/goal_management/helpers/goal_persistence_spec.rb +119 -0
  22. data/spec/legion/extensions/agentic/executive/goal_management/helpers/task_dispatcher_spec.rb +176 -0
  23. data/spec/legion/extensions/agentic/executive/volition/helpers/goal_bridge_spec.rb +58 -0
  24. data/spec/legion/extensions/agentic/executive/volition/runners/volition_spec.rb +26 -0
  25. metadata +12 -1
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Executive
7
+ module GoalManagement
8
+ module Helpers
9
+ class TaskDispatcher
10
+ DOMAIN_RUNNERS = {
11
+ safety: { runner_class: 'Legion::Extensions::MindGrowth::Runners::Monitor',
12
+ function: :health_check,
13
+ args_builder: ->(goal) { { extension: goal[:domain].to_s } } },
14
+ cognition: { runner_class: 'Legion::Extensions::MindGrowth::Runners::Analyzer',
15
+ function: :recommend_priorities,
16
+ args_builder: ->(_goal) { { existing_extensions: [] } } },
17
+ perception: { client_class: 'Legion::Extensions::Agentic::Learning::Curiosity::Client',
18
+ function: :detect_gaps,
19
+ args_builder: ->(_goal) { { prior_results: {} } } },
20
+ introspection: { client_class: 'Legion::Extensions::Agentic::Executive::Volition::Client',
21
+ function: :volition_status,
22
+ args_builder: ->(_goal) { {} } }
23
+ }.freeze
24
+
25
+ def dispatch_goal(goal:)
26
+ domain = goal[:domain]&.to_sym
27
+ mapping = DOMAIN_RUNNERS[domain]
28
+
29
+ return unroutable(goal, domain) unless mapping
30
+ return unroutable_not_loaded(goal, domain) unless runner_class_loaded?(mapping)
31
+
32
+ execute_dispatch(goal: goal, mapping: mapping)
33
+ rescue StandardError => e
34
+ log.error "[task_dispatcher] dispatch error goal=#{goal[:id]} #{e.message}"
35
+ { dispatched: false, error: e.message, goal_id: goal[:id] }
36
+ end
37
+
38
+ private
39
+
40
+ def runner_class_loaded?(mapping)
41
+ class_name = mapping[:runner_class] || mapping[:client_class]
42
+ Kernel.const_get(class_name)
43
+ true
44
+ rescue NameError => e
45
+ log.debug "[task_dispatcher] runner not loaded: #{e.message}"
46
+ false
47
+ end
48
+
49
+ def execute_dispatch(goal:, mapping:)
50
+ function = mapping[:function]
51
+ args = mapping[:args_builder].call(goal)
52
+
53
+ result = if mapping.key?(:client_class)
54
+ dispatch_via_client(mapping[:client_class], function, args)
55
+ else
56
+ dispatch_via_runner(mapping[:runner_class], function, args)
57
+ end
58
+
59
+ success = result[:status] == 'task.completed'
60
+ task_id = result[:task_id]
61
+ runner_key = mapping[:runner_class] || mapping[:client_class]
62
+
63
+ log.info "[task_dispatcher] dispatched goal=#{goal[:id]} runner=#{runner_key} function=#{function}"
64
+
65
+ {
66
+ dispatched: true,
67
+ success: success,
68
+ task_id: task_id,
69
+ runner_mapping: runner_key,
70
+ result: result,
71
+ goal_id: goal[:id]
72
+ }
73
+ end
74
+
75
+ def dispatch_via_runner(runner_class, function, args)
76
+ Legion::Runner.run(
77
+ runner_class: runner_class,
78
+ function: function,
79
+ args: args,
80
+ generate_task: true,
81
+ check_subtask: true
82
+ )
83
+ end
84
+
85
+ def dispatch_via_client(client_class, function, args)
86
+ client = Kernel.const_get(client_class).new
87
+ client.send(function, **args)
88
+ end
89
+
90
+ def log
91
+ Legion::Logging
92
+ end
93
+
94
+ def unroutable(goal, domain)
95
+ log.debug "[task_dispatcher] no runner for domain=#{domain} goal=#{goal[:id]}"
96
+ { dispatched: false, reason: :no_runner, domain: domain, goal_id: goal[:id] }
97
+ end
98
+
99
+ def unroutable_not_loaded(goal, domain)
100
+ log.debug "[task_dispatcher] runner not loaded for domain=#{domain} goal=#{goal[:id]}"
101
+ { dispatched: false, reason: :runner_not_loaded, domain: domain, goal_id: goal[:id] }
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -7,151 +7,203 @@ module Legion
7
7
  module GoalManagement
8
8
  module Runners
9
9
  module GoalManagement
10
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
- Legion::Extensions::Helpers.const_defined?(:Lex)
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
12
 
13
13
  def add_goal(content:, parent_id: nil, domain: :general,
14
14
  priority: Helpers::Constants::DEFAULT_PRIORITY, deadline: nil, **)
15
- Legion::Logging.debug "[goal_management] runner add_goal domain=#{domain}"
15
+ log.debug "[goal_management] runner add_goal domain=#{domain}"
16
16
  engine.add_goal(content: content, parent_id: parent_id, domain: domain,
17
17
  priority: priority, deadline: deadline)
18
18
  rescue StandardError => e
19
- Legion::Logging.error "[goal_management] add_goal error: #{e.message}"
19
+ log.error "[goal_management] add_goal error: #{e.message}"
20
20
  { success: false, error: e.message }
21
21
  end
22
22
 
23
23
  def decompose_goal(goal_id:, sub_goals:, **)
24
- Legion::Logging.debug "[goal_management] runner decompose_goal parent=#{goal_id}"
24
+ log.debug "[goal_management] runner decompose_goal parent=#{goal_id}"
25
25
  engine.decompose(goal_id: goal_id, sub_goals: sub_goals)
26
26
  rescue StandardError => e
27
- Legion::Logging.error "[goal_management] decompose_goal error: #{e.message}"
27
+ log.error "[goal_management] decompose_goal error: #{e.message}"
28
+ { success: false, error: e.message }
29
+ end
30
+
31
+ def auto_decompose_goal(goal_id:, strategy: :heuristic, **)
32
+ goal_data = engine.goals[goal_id]
33
+ return { success: false, error: :not_found } unless goal_data
34
+
35
+ decomp = Helpers::Decomposer.decompose(goal: goal_data.to_h, strategy: strategy)
36
+ return decomp unless decomp[:success]
37
+
38
+ result = engine.decompose(goal_id: goal_id, sub_goals: decomp[:sub_goals])
39
+ result.merge(strategy_used: decomp[:strategy_used])
40
+ rescue StandardError => e
41
+ log.error "[goal_management] auto_decompose_goal error: #{e.message}"
28
42
  { success: false, error: e.message }
29
43
  end
30
44
 
31
45
  def activate_goal(goal_id:, **)
32
- Legion::Logging.debug "[goal_management] runner activate_goal id=#{goal_id}"
46
+ log.debug "[goal_management] runner activate_goal id=#{goal_id}"
33
47
  engine.activate_goal(goal_id: goal_id)
34
48
  rescue StandardError => e
35
- Legion::Logging.error "[goal_management] activate_goal error: #{e.message}"
49
+ log.error "[goal_management] activate_goal error: #{e.message}"
36
50
  { success: false, error: e.message }
37
51
  end
38
52
 
39
53
  def complete_goal(goal_id:, **)
40
- Legion::Logging.debug "[goal_management] runner complete_goal id=#{goal_id}"
54
+ log.debug "[goal_management] runner complete_goal id=#{goal_id}"
41
55
  engine.complete_goal(goal_id: goal_id)
42
56
  rescue StandardError => e
43
- Legion::Logging.error "[goal_management] complete_goal error: #{e.message}"
57
+ log.error "[goal_management] complete_goal error: #{e.message}"
44
58
  { success: false, error: e.message }
45
59
  end
46
60
 
47
61
  def abandon_goal(goal_id:, **)
48
- Legion::Logging.debug "[goal_management] runner abandon_goal id=#{goal_id}"
62
+ log.debug "[goal_management] runner abandon_goal id=#{goal_id}"
49
63
  engine.abandon_goal(goal_id: goal_id)
50
64
  rescue StandardError => e
51
- Legion::Logging.error "[goal_management] abandon_goal error: #{e.message}"
65
+ log.error "[goal_management] abandon_goal error: #{e.message}"
52
66
  { success: false, error: e.message }
53
67
  end
54
68
 
55
69
  def block_goal(goal_id:, **)
56
- Legion::Logging.debug "[goal_management] runner block_goal id=#{goal_id}"
70
+ log.debug "[goal_management] runner block_goal id=#{goal_id}"
57
71
  engine.block_goal(goal_id: goal_id)
58
72
  rescue StandardError => e
59
- Legion::Logging.error "[goal_management] block_goal error: #{e.message}"
73
+ log.error "[goal_management] block_goal error: #{e.message}"
60
74
  { success: false, error: e.message }
61
75
  end
62
76
 
63
77
  def unblock_goal(goal_id:, **)
64
- Legion::Logging.debug "[goal_management] runner unblock_goal id=#{goal_id}"
78
+ log.debug "[goal_management] runner unblock_goal id=#{goal_id}"
65
79
  engine.unblock_goal(goal_id: goal_id)
66
80
  rescue StandardError => e
67
- Legion::Logging.error "[goal_management] unblock_goal error: #{e.message}"
81
+ log.error "[goal_management] unblock_goal error: #{e.message}"
68
82
  { success: false, error: e.message }
69
83
  end
70
84
 
71
85
  def advance_goal_progress(goal_id:, amount:, **)
72
- Legion::Logging.debug "[goal_management] runner advance_goal_progress id=#{goal_id} amount=#{amount}"
86
+ log.debug "[goal_management] runner advance_goal_progress id=#{goal_id} amount=#{amount}"
73
87
  engine.advance_progress(goal_id: goal_id, amount: amount)
74
88
  rescue StandardError => e
75
- Legion::Logging.error "[goal_management] advance_goal_progress error: #{e.message}"
89
+ log.error "[goal_management] advance_goal_progress error: #{e.message}"
76
90
  { success: false, error: e.message }
77
91
  end
78
92
 
79
93
  def detect_goal_conflicts(goal_id:, **)
80
- Legion::Logging.debug "[goal_management] runner detect_goal_conflicts id=#{goal_id}"
94
+ log.debug "[goal_management] runner detect_goal_conflicts id=#{goal_id}"
81
95
  engine.detect_conflicts(goal_id: goal_id)
82
96
  rescue StandardError => e
83
- Legion::Logging.error "[goal_management] detect_goal_conflicts error: #{e.message}"
97
+ log.error "[goal_management] detect_goal_conflicts error: #{e.message}"
84
98
  { success: false, error: e.message }
85
99
  end
86
100
 
87
101
  def list_active_goals(**)
88
102
  goals = engine.active_goals
89
- Legion::Logging.debug "[goal_management] list_active_goals count=#{goals.size}"
103
+ log.debug "[goal_management] list_active_goals count=#{goals.size}"
90
104
  { success: true, goals: goals.map(&:to_h), count: goals.size }
91
105
  rescue StandardError => e
92
- Legion::Logging.error "[goal_management] list_active_goals error: #{e.message}"
106
+ log.error "[goal_management] list_active_goals error: #{e.message}"
93
107
  { success: false, error: e.message }
94
108
  end
95
109
 
96
110
  def list_blocked_goals(**)
97
111
  goals = engine.blocked_goals
98
- Legion::Logging.debug "[goal_management] list_blocked_goals count=#{goals.size}"
112
+ log.debug "[goal_management] list_blocked_goals count=#{goals.size}"
99
113
  { success: true, goals: goals.map(&:to_h), count: goals.size }
100
114
  rescue StandardError => e
101
- Legion::Logging.error "[goal_management] list_blocked_goals error: #{e.message}"
115
+ log.error "[goal_management] list_blocked_goals error: #{e.message}"
102
116
  { success: false, error: e.message }
103
117
  end
104
118
 
105
119
  def list_overdue_goals(**)
106
120
  goals = engine.overdue_goals
107
- Legion::Logging.debug "[goal_management] list_overdue_goals count=#{goals.size}"
121
+ log.debug "[goal_management] list_overdue_goals count=#{goals.size}"
108
122
  { success: true, goals: goals.map(&:to_h), count: goals.size }
109
123
  rescue StandardError => e
110
- Legion::Logging.error "[goal_management] list_overdue_goals error: #{e.message}"
124
+ log.error "[goal_management] list_overdue_goals error: #{e.message}"
111
125
  { success: false, error: e.message }
112
126
  end
113
127
 
114
128
  def list_completed_goals(**)
115
129
  goals = engine.completed_goals
116
- Legion::Logging.debug "[goal_management] list_completed_goals count=#{goals.size}"
130
+ log.debug "[goal_management] list_completed_goals count=#{goals.size}"
117
131
  { success: true, goals: goals.map(&:to_h), count: goals.size }
118
132
  rescue StandardError => e
119
- Legion::Logging.error "[goal_management] list_completed_goals error: #{e.message}"
133
+ log.error "[goal_management] list_completed_goals error: #{e.message}"
120
134
  { success: false, error: e.message }
121
135
  end
122
136
 
123
137
  def get_goal_tree(goal_id:, **)
124
- Legion::Logging.debug "[goal_management] runner get_goal_tree id=#{goal_id}"
138
+ log.debug "[goal_management] runner get_goal_tree id=#{goal_id}"
125
139
  engine.goal_tree(goal_id: goal_id)
126
140
  rescue StandardError => e
127
- Legion::Logging.error "[goal_management] get_goal_tree error: #{e.message}"
141
+ log.error "[goal_management] get_goal_tree error: #{e.message}"
128
142
  { success: false, error: e.message }
129
143
  end
130
144
 
131
145
  def highest_priority_goals(limit: 5, **)
132
146
  goals = engine.highest_priority(limit: limit)
133
- Legion::Logging.debug "[goal_management] highest_priority_goals limit=#{limit} count=#{goals.size}"
147
+ log.debug "[goal_management] highest_priority_goals limit=#{limit} count=#{goals.size}"
134
148
  { success: true, goals: goals.map(&:to_h), count: goals.size }
135
149
  rescue StandardError => e
136
- Legion::Logging.error "[goal_management] highest_priority_goals error: #{e.message}"
150
+ log.error "[goal_management] highest_priority_goals error: #{e.message}"
137
151
  { success: false, error: e.message }
138
152
  end
139
153
 
140
154
  def decay_priorities(**)
141
155
  result = engine.decay_all_priorities!
142
- Legion::Logging.debug "[goal_management] decay_priorities decayed=#{result[:decayed]}"
156
+ log.debug "[goal_management] decay_priorities decayed=#{result[:decayed]}"
143
157
  result.merge(success: true)
144
158
  rescue StandardError => e
145
- Legion::Logging.error "[goal_management] decay_priorities error: #{e.message}"
159
+ log.error "[goal_management] decay_priorities error: #{e.message}"
146
160
  { success: false, error: e.message }
147
161
  end
148
162
 
149
163
  def goal_status(**)
150
164
  report = engine.goal_report
151
- Legion::Logging.debug "[goal_management] goal_status total=#{report[:total]}"
165
+ log.debug "[goal_management] goal_status total=#{report[:total]}"
152
166
  { success: true }.merge(report)
153
167
  rescue StandardError => e
154
- Legion::Logging.error "[goal_management] goal_status error: #{e.message}"
168
+ log.error "[goal_management] goal_status error: #{e.message}"
169
+ { success: false, error: e.message }
170
+ end
171
+
172
+ def dispatch_leaf_goals(**)
173
+ dispatcher = Helpers::TaskDispatcher.new
174
+ engine.goals.each_value do |g|
175
+ engine.activate_goal(goal_id: g.id) if g.leaf? && g.status == :proposed
176
+ end
177
+ leaves = engine.active_goals.select(&:leaf?)
178
+ results = leaves.reject(&:task_id).map do |goal|
179
+ dispatch = dispatcher.dispatch_goal(goal: goal.to_h)
180
+ if dispatch[:dispatched] && dispatch[:task_id]
181
+ engine.assign_task_to_goal(goal_id: goal.id,
182
+ task_id: dispatch[:task_id],
183
+ runner_mapping: dispatch[:runner_mapping])
184
+ end
185
+ { goal_id: goal.id, dispatch: dispatch }
186
+ end
187
+ dispatched_count = results.count { |r| r[:dispatch][:dispatched] }
188
+ log.debug "[goal_management] dispatch_leaf_goals dispatched=#{dispatched_count} total=#{results.size}"
189
+ { success: true, dispatched: dispatched_count, total: results.size, results: results }
190
+ rescue StandardError => e
191
+ log.error "[goal_management] dispatch_leaf_goals error: #{e.message}"
192
+ { success: false, error: e.message }
193
+ end
194
+
195
+ def update_goal_from_task(task_id:, status:, result: nil, **)
196
+ engine.update_from_task_event(task_id: task_id, status: status, result: result)
197
+ rescue StandardError => e
198
+ log.error "[goal_management] update_goal_from_task error: #{e.message}"
199
+ { success: false, error: e.message }
200
+ end
201
+
202
+ def start_feedback_listener(**)
203
+ feedback_listener.start_listening
204
+ { success: true, listening: feedback_listener.listening? }
205
+ rescue StandardError => e
206
+ log.error "[goal_management] start_feedback_listener error: #{e.message}"
155
207
  { success: false, error: e.message }
156
208
  end
157
209
 
@@ -160,6 +212,14 @@ module Legion
160
212
  def engine
161
213
  @engine ||= Helpers::GoalEngine.new
162
214
  end
215
+
216
+ def feedback_listener
217
+ @feedback_listener ||= Helpers::FeedbackListener.new(engine: engine)
218
+ end
219
+
220
+ def log
221
+ Legion::Logging
222
+ end
163
223
  end
164
224
  end
165
225
  end
@@ -3,7 +3,10 @@
3
3
  require 'legion/extensions/agentic/executive/goal_management/version'
4
4
  require 'legion/extensions/agentic/executive/goal_management/helpers/constants'
5
5
  require 'legion/extensions/agentic/executive/goal_management/helpers/goal'
6
+ require 'legion/extensions/agentic/executive/goal_management/helpers/goal_persistence'
6
7
  require 'legion/extensions/agentic/executive/goal_management/helpers/goal_engine'
8
+ require 'legion/extensions/agentic/executive/goal_management/helpers/task_dispatcher'
9
+ require 'legion/extensions/agentic/executive/goal_management/helpers/decomposer'
7
10
  require 'legion/extensions/agentic/executive/goal_management/runners/goal_management'
8
11
  require 'legion/extensions/agentic/executive/goal_management/client'
9
12
 
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Executive
7
- VERSION = '0.1.12'
7
+ VERSION = '0.2.0'
8
8
  end
9
9
  end
10
10
  end
@@ -4,6 +4,7 @@ require 'legion/extensions/agentic/executive/volition/helpers/constants'
4
4
  require 'legion/extensions/agentic/executive/volition/helpers/intention'
5
5
  require 'legion/extensions/agentic/executive/volition/helpers/intention_stack'
6
6
  require 'legion/extensions/agentic/executive/volition/helpers/drive_synthesizer'
7
+ require 'legion/extensions/agentic/executive/volition/helpers/goal_bridge'
7
8
  require 'legion/extensions/agentic/executive/volition/runners/volition'
8
9
 
9
10
  module Legion
@@ -16,8 +17,9 @@ module Legion
16
17
 
17
18
  attr_reader :intention_stack
18
19
 
19
- def initialize(stack: nil, **)
20
+ def initialize(stack: nil, goal_bridge: nil, **)
20
21
  @intention_stack = stack || Helpers::IntentionStack.new
22
+ @goal_bridge = goal_bridge
21
23
  end
22
24
  end
23
25
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/agentic/executive/goal_management/helpers/goal_persistence'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Agentic
8
+ module Executive
9
+ module Volition
10
+ module Helpers
11
+ class GoalBridge
12
+ def initialize(goal_client:, persistence: nil)
13
+ @goal_client = goal_client
14
+ @persistence = persistence || default_persistence
15
+ @bridged_intentions = @persistence&.load_bridge_state || {}
16
+ @mutex = Mutex.new
17
+ end
18
+
19
+ def bridge_intentions(intentions)
20
+ result = { bridged: 0, skipped: 0, duplicates: 0, goal_ids: [], errors: [] }
21
+ return result if intentions.nil? || intentions.empty?
22
+
23
+ intentions.each do |intention|
24
+ outcome = bridge_one(intention)
25
+ case outcome[:status]
26
+ when :bridged
27
+ result[:bridged] += 1
28
+ result[:goal_ids] << outcome[:goal_id]
29
+ when :skipped then result[:skipped] += 1
30
+ when :duplicate then result[:duplicates] += 1
31
+ when :error then result[:errors] << outcome[:error]
32
+ end
33
+ end
34
+
35
+ persist_bridge_state
36
+ log.info "[goal_bridge] bridged #{result[:bridged]} intentions to goals"
37
+ result
38
+ end
39
+
40
+ private
41
+
42
+ def log
43
+ Legion::Logging
44
+ end
45
+
46
+ def default_persistence
47
+ GoalManagement::Helpers::GoalPersistence.new
48
+ rescue StandardError => e
49
+ log.error "GoalBridge: #{e.message}"
50
+ nil
51
+ end
52
+
53
+ def persist_bridge_state
54
+ @persistence&.save_bridge_state(@bridged_intentions)
55
+ end
56
+
57
+ def bridge_one(intention)
58
+ return { status: :skipped } unless intention[:state] == :active
59
+
60
+ intent_key = "#{intention[:drive]}:#{intention[:domain]}:#{intention[:goal]}"
61
+ @mutex.synchronize do
62
+ return { status: :duplicate } if @bridged_intentions.key?(intent_key)
63
+
64
+ goal_result = @goal_client.add_goal(
65
+ content: intention[:goal],
66
+ parent_id: nil,
67
+ domain: intention[:domain],
68
+ priority: intention[:salience],
69
+ deadline: nil
70
+ )
71
+
72
+ if goal_result[:success]
73
+ @bridged_intentions[intent_key] = goal_result[:goal][:id]
74
+ { status: :bridged, goal_id: goal_result[:goal][:id] }
75
+ else
76
+ { status: :error, error: goal_result[:error] }
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -17,10 +17,10 @@ module Legion
17
17
  )
18
18
 
19
19
  new_intentions = Helpers::DriveSynthesizer.generate_intentions(drives, cognitive_state: cognitive_state)
20
- pushed = 0
20
+ pushed_intentions = []
21
21
  new_intentions.each do |intention|
22
- result = intention_stack.push(intention)
23
- pushed += 1 if result == :pushed
22
+ outcome = intention_stack.push(intention)
23
+ pushed_intentions << intention if outcome == :pushed
24
24
  end
25
25
 
26
26
  expired = intention_stack.decay_all
@@ -28,18 +28,25 @@ module Legion
28
28
  current = intention_stack.top
29
29
  proactive = evaluate_proactive_outreach(tick_results, bond_state)
30
30
 
31
- log.debug "[volition] drives=#{format_drives(drives)} pushed=#{pushed} expired=#{expired} " \
31
+ log.debug "[volition] drives=#{format_drives(drives)} pushed=#{pushed_intentions.size} expired=#{expired} " \
32
32
  "active=#{intention_stack.active_count} top=#{current&.dig(:goal)}"
33
33
 
34
- {
34
+ result = {
35
35
  drives: drives,
36
36
  dominant_drive: dominant,
37
- new_intentions: pushed,
37
+ new_intentions: pushed_intentions.map { |i| format_intention(i) },
38
38
  expired: expired,
39
39
  active_intentions: intention_stack.active_count,
40
40
  current_intention: format_intention(current),
41
41
  proactive_outreach: proactive
42
42
  }
43
+
44
+ if defined?(@goal_bridge) && @goal_bridge
45
+ bridge_result = @goal_bridge.bridge_intentions(pushed_intentions)
46
+ result[:bridge_result] = bridge_result
47
+ end
48
+
49
+ result
43
50
  end
44
51
 
45
52
  def current_intention(**)
@@ -5,6 +5,7 @@ require 'legion/extensions/agentic/executive/volition/helpers/constants'
5
5
  require 'legion/extensions/agentic/executive/volition/helpers/intention'
6
6
  require 'legion/extensions/agentic/executive/volition/helpers/intention_stack'
7
7
  require 'legion/extensions/agentic/executive/volition/helpers/drive_synthesizer'
8
+ require 'legion/extensions/agentic/executive/volition/helpers/goal_bridge'
8
9
  require 'legion/extensions/agentic/executive/volition/runners/volition'
9
10
  require 'legion/extensions/agentic/executive/volition/client'
10
11
 
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'Autonomous Goal Pipeline Integration' do
6
+ let(:goal_client) { Legion::Extensions::Agentic::Executive::GoalManagement::Client.new }
7
+ let(:bridge) do
8
+ Legion::Extensions::Agentic::Executive::Volition::Helpers::GoalBridge.new(
9
+ goal_client: goal_client
10
+ )
11
+ end
12
+ let(:volition_client) do
13
+ Legion::Extensions::Agentic::Executive::Volition::Client.new(
14
+ stack: Legion::Extensions::Agentic::Executive::Volition::Helpers::IntentionStack.new,
15
+ goal_bridge: bridge
16
+ )
17
+ end
18
+
19
+ let(:tick_results) do
20
+ { working_memory_integration: { curiosity: { intensity: 0.9, count: 5 } } }
21
+ end
22
+ let(:cognitive_state) do
23
+ { health: 0.6, pending_goals: 0, arousal: 0.5, gut: { signal: :explore },
24
+ confidence: 0.4, pending_questions: 3, peer_interactions: 0,
25
+ trust: { avg_composite: 0.5 } }
26
+ end
27
+ let(:bond_state) { {} }
28
+
29
+ it 'flows from intention through goal to dispatch-ready state' do
30
+ # Step 1: Form intentions (triggers goal bridge)
31
+ volition_result = volition_client.form_intentions(
32
+ tick_results: tick_results, cognitive_state: cognitive_state, bond_state: bond_state
33
+ )
34
+ expect(volition_result[:bridge_result][:bridged]).to be >= 1
35
+
36
+ # Step 2: Verify goals were created
37
+ goal_ids = volition_result[:bridge_result][:goal_ids]
38
+ expect(goal_ids).not_to be_empty
39
+
40
+ # Step 3: Auto-decompose the first goal
41
+ first_goal_id = goal_ids.first
42
+ goal_client.activate_goal(goal_id: first_goal_id)
43
+ decomp = goal_client.auto_decompose_goal(goal_id: first_goal_id, strategy: :heuristic)
44
+ expect(decomp[:success]).to be true
45
+
46
+ # Step 4: Verify goal tree has children
47
+ tree = goal_client.get_goal_tree(goal_id: first_goal_id)
48
+ expect(tree[:tree]).not_to be_nil
49
+
50
+ # Step 5: Check goal status shows goals exist
51
+ status = goal_client.goal_status
52
+ expect(status[:total]).to be >= 1
53
+ end
54
+
55
+ it 'updates goal on simulated task completion' do
56
+ # Create and activate a leaf goal with task assignment
57
+ add_result = goal_client.add_goal(
58
+ content: 'simple leaf goal', domain: :general, priority: 0.5,
59
+ parent_id: nil, deadline: nil
60
+ )
61
+ goal_id = add_result[:goal][:id]
62
+ goal_client.activate_goal(goal_id: goal_id)
63
+
64
+ engine = goal_client.send(:engine)
65
+ goal = engine.goals[goal_id]
66
+ goal.assign_task!(task_id: 'task-sim-001', runner_mapping: { runner_class: 'Test', function: :test })
67
+
68
+ # Simulate task completion
69
+ update = engine.update_from_task_event(task_id: 'task-sim-001', status: 'task.completed')
70
+ expect(update[:found]).to be true
71
+ expect(update[:new_status]).to eq(:completed)
72
+ expect(goal.progress).to eq(1.0)
73
+ end
74
+
75
+ it 'blocks goal on simulated task failure' do
76
+ add_result = goal_client.add_goal(
77
+ content: 'failing goal', domain: :general, priority: 0.5,
78
+ parent_id: nil, deadline: nil
79
+ )
80
+ goal_id = add_result[:goal][:id]
81
+ goal_client.activate_goal(goal_id: goal_id)
82
+
83
+ engine = goal_client.send(:engine)
84
+ goal = engine.goals[goal_id]
85
+ goal.assign_task!(task_id: 'task-fail-001', runner_mapping: { runner_class: 'Test', function: :test })
86
+
87
+ update = engine.update_from_task_event(task_id: 'task-fail-001', status: 'task.exception')
88
+ expect(update[:found]).to be true
89
+ expect(update[:new_status]).to eq(:blocked)
90
+ end
91
+
92
+ it 'reprioritizes goals on urgency signal' do
93
+ goal_client.add_goal(content: 'safety issue', domain: :safety, priority: 0.4, parent_id: nil, deadline: nil)
94
+ goal_client.add_goal(content: 'general task', domain: :general, priority: 0.4, parent_id: nil, deadline: nil)
95
+
96
+ # Activate both
97
+ engine = goal_client.send(:engine)
98
+ engine.goals.each_value(&:activate!)
99
+
100
+ result = engine.reprioritize!(signal: { domain: :safety, urgency: :critical })
101
+ expect(result[:adjusted]).to eq(1)
102
+
103
+ safety = engine.goals.values.find { |g| g.domain == :safety }
104
+ general = engine.goals.values.find { |g| g.domain == :general }
105
+ expect(safety.priority).to be > general.priority
106
+ end
107
+ end