lex-mind-growth 0.2.7 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8f21cd00a0fb432c27fd057addf7294b2d84425c7927d6298bab0c87496d868
4
- data.tar.gz: c0a512a778e049b13d5442ab3f72bf513ce5104bc0be3632891eda965b1fa668
3
+ metadata.gz: 3fcaa84f5b0e455b0ac70019228ebe5bffdbd1c69228aafcad9bf92d7f91bb52
4
+ data.tar.gz: 4ea8b0c108d6bf0a957cd9709147e4cfaa3c78e0b297afc85e642331f9153e4a
5
5
  SHA512:
6
- metadata.gz: f772d447b870acf41484209df88a82352e4b27b00a7f30335dfb0b5fee1e26c6cc7b06d6194aaf46d2d7506e55cb7e5d264aa9b005872db00c632f3caea1a4bf
7
- data.tar.gz: d4aee3639b1cec1a35015c9a432cb7041715bd3f3ad0571534a9d441ce11e79481a6361e72295076fb06605cc2afd357805e385ff04bf28503302c7f003bcbd5
6
+ metadata.gz: 911dc8e13b4dfe4cb4ee0cd5dd7cd27fc2d93a529aca350cc4d02d4f3f27b9548ac8881945bb57d150ffa8cc8e169e8f62b3590e2bbab4188dbfc0046aa69eb3
7
+ data.tar.gz: d4f849194085b3b398dd41e3499b686c87b323ab4950a392eaca71133aeb943fcfe68557829a5cb6be92428bf781c90ab53d49cf877f26abe5b699562c25d07e
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'time'
4
+
3
5
  module Legion
4
6
  module Extensions
5
7
  module MindGrowth
@@ -59,6 +61,27 @@ module Legion
59
61
  @built_at = Time.now.utc if new_status == :passing
60
62
  end
61
63
 
64
+ def self.from_h(hash)
65
+ h = hash.transform_keys(&:to_sym)
66
+ proposal = allocate
67
+ proposal.instance_variable_set(:@id, h[:id])
68
+ proposal.instance_variable_set(:@name, h[:name])
69
+ proposal.instance_variable_set(:@module_name, h[:module_name])
70
+ proposal.instance_variable_set(:@category, h[:category]&.to_sym)
71
+ proposal.instance_variable_set(:@description, h[:description])
72
+ proposal.instance_variable_set(:@metaphor, h[:metaphor])
73
+ proposal.instance_variable_set(:@helpers, h[:helpers] || [])
74
+ proposal.instance_variable_set(:@runner_methods, h[:runner_methods] || [])
75
+ proposal.instance_variable_set(:@rationale, h[:rationale])
76
+ proposal.instance_variable_set(:@scores, (h[:scores] || {}).transform_keys(&:to_sym))
77
+ proposal.instance_variable_set(:@status, h[:status]&.to_sym || :proposed)
78
+ proposal.instance_variable_set(:@origin, h[:origin]&.to_sym || :proposer)
79
+ proposal.instance_variable_set(:@created_at, h[:created_at] ? Time.parse(h[:created_at].to_s) : Time.now.utc)
80
+ proposal.instance_variable_set(:@evaluated_at, h[:evaluated_at] ? Time.parse(h[:evaluated_at].to_s) : nil)
81
+ proposal.instance_variable_set(:@built_at, h[:built_at] ? Time.parse(h[:built_at].to_s) : nil)
82
+ proposal
83
+ end
84
+
62
85
  def to_h
63
86
  FIELDS.to_h { |f| [f, send(f)] }
64
87
  end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MindGrowth
6
+ module Helpers
7
+ # Cache-backed persistence for proposals and governance votes.
8
+ # Best-effort: degrades to in-memory-only when cache is unavailable.
9
+ class ProposalPersistence
10
+ PROPOSAL_TTL = 604_800 # 7 days
11
+ VOTES_KEY_SUFFIX = ':votes'
12
+ INDEX_KEY_SUFFIX = ':index'
13
+
14
+ def initialize(namespace: 'legion_mind_growth')
15
+ @namespace = namespace
16
+ end
17
+
18
+ def save_proposal(proposal_hash)
19
+ return false unless cache_available?
20
+
21
+ key = proposal_key(proposal_hash[:id])
22
+ Legion::Cache.set_sync(key, serialize(proposal_hash), ttl: PROPOSAL_TTL)
23
+ update_index(proposal_hash[:id], :add)
24
+ true
25
+ rescue StandardError => e
26
+ log.error "[proposal_persistence] save_proposal failed for #{proposal_hash[:id]}: #{e.message}"
27
+ false
28
+ end
29
+
30
+ def load_proposal(id)
31
+ return nil unless cache_available?
32
+
33
+ raw = Legion::Cache.get(proposal_key(id)) # rubocop:disable Legion/HelperMigration/DirectCache
34
+ return nil unless raw
35
+
36
+ deserialize(raw)
37
+ rescue StandardError => e
38
+ log.error "[proposal_persistence] load_proposal failed for #{id}: #{e.message}"
39
+ nil
40
+ end
41
+
42
+ def delete_proposal(id)
43
+ return false unless cache_available?
44
+
45
+ Legion::Cache.delete_sync(proposal_key(id))
46
+ update_index(id, :remove)
47
+ true
48
+ rescue StandardError => e
49
+ log.error "[proposal_persistence] delete_proposal failed for #{id}: #{e.message}"
50
+ false
51
+ end
52
+
53
+ def load_all_proposals
54
+ return {} unless cache_available?
55
+
56
+ ids = load_index
57
+ return {} if ids.empty?
58
+
59
+ ids.each_with_object({}) do |id, result|
60
+ p = load_proposal(id)
61
+ result[id] = p if p
62
+ end
63
+ rescue StandardError => e
64
+ log.error "[proposal_persistence] load_all_proposals failed: #{e.message}"
65
+ {}
66
+ end
67
+
68
+ def delete_all_proposals
69
+ return false unless cache_available?
70
+
71
+ ids = load_index
72
+ ids.each { |id| Legion::Cache.delete_sync(proposal_key(id)) }
73
+ Legion::Cache.delete_sync(index_key)
74
+ true
75
+ rescue StandardError => e
76
+ log.error "[proposal_persistence] delete_all_proposals failed: #{e.message}"
77
+ false
78
+ end
79
+
80
+ def save_votes(votes_hash)
81
+ return false unless cache_available?
82
+
83
+ Legion::Cache.set_sync(votes_key, serialize(votes_hash), ttl: PROPOSAL_TTL)
84
+ true
85
+ rescue StandardError => e
86
+ log.error "[proposal_persistence] save_votes failed: #{e.message}"
87
+ false
88
+ end
89
+
90
+ def load_votes
91
+ return {} unless cache_available?
92
+
93
+ raw = Legion::Cache.get(votes_key) # rubocop:disable Legion/HelperMigration/DirectCache
94
+ return {} unless raw
95
+
96
+ result = deserialize(raw)
97
+ result.is_a?(Hash) ? result : {}
98
+ rescue StandardError => e
99
+ log.error "[proposal_persistence] load_votes failed: #{e.message}"
100
+ {}
101
+ end
102
+
103
+ private
104
+
105
+ def cache_available?
106
+ defined?(Legion::Cache) && Legion::Cache.connected? # rubocop:disable Legion/HelperMigration/DirectCache
107
+ end
108
+
109
+ def proposal_key(id) = "#{@namespace}:proposal:#{id}"
110
+ def votes_key = "#{@namespace}#{VOTES_KEY_SUFFIX}"
111
+ def index_key = "#{@namespace}#{INDEX_KEY_SUFFIX}"
112
+
113
+ def update_index(id, operation)
114
+ ids = load_index
115
+ case operation
116
+ when :add then ids << id unless ids.include?(id)
117
+ when :remove then ids.delete(id)
118
+ end
119
+ Legion::Cache.set_sync(index_key, serialize(ids), ttl: PROPOSAL_TTL)
120
+ end
121
+
122
+ def load_index
123
+ raw = Legion::Cache.get(index_key) # rubocop:disable Legion/HelperMigration/DirectCache
124
+ return [] unless raw
125
+
126
+ result = deserialize(raw)
127
+ result.is_a?(Array) ? result : []
128
+ rescue StandardError => e
129
+ log.error "[proposal_persistence] load_index failed: #{e.message}"
130
+ []
131
+ end
132
+
133
+ def serialize(obj)
134
+ Legion::JSON.dump(obj) # rubocop:disable Legion/HelperMigration/DirectJson
135
+ end
136
+
137
+ def deserialize(raw)
138
+ return raw if raw.is_a?(Hash) || raw.is_a?(Array)
139
+
140
+ Legion::JSON.load(raw) # rubocop:disable Legion/HelperMigration/DirectJson
141
+ rescue StandardError => _e
142
+ nil
143
+ end
144
+
145
+ def log
146
+ Legion::Logging
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -7,15 +7,18 @@ module Legion
7
7
  class ProposalStore
8
8
  MAX_PROPOSALS = 500
9
9
 
10
- def initialize
11
- @proposals = {}
12
- @mutex = Mutex.new
10
+ def initialize(rehydrate: true)
11
+ @proposals = {}
12
+ @mutex = Mutex.new
13
+ @persistence = ProposalPersistence.new
14
+ rehydrate_from_cache if rehydrate
13
15
  end
14
16
 
15
17
  def store(proposal)
16
18
  @mutex.synchronize do
17
19
  evict_oldest if @proposals.size >= MAX_PROPOSALS
18
20
  @proposals[proposal.id] = proposal
21
+ @persistence.save_proposal(proposal.to_h)
19
22
  end
20
23
  end
21
24
 
@@ -23,6 +26,13 @@ module Legion
23
26
  @mutex.synchronize { @proposals[id] }
24
27
  end
25
28
 
29
+ def update(proposal)
30
+ @mutex.synchronize do
31
+ @proposals[proposal.id] = proposal
32
+ @persistence.save_proposal(proposal.to_h)
33
+ end
34
+ end
35
+
26
36
  def all
27
37
  @mutex.synchronize { @proposals.values.dup }
28
38
  end
@@ -58,11 +68,36 @@ module Legion
58
68
  @mutex.synchronize { @proposals.clear }
59
69
  end
60
70
 
71
+ def clear_persisted!
72
+ @mutex.synchronize do
73
+ @proposals.clear
74
+ @persistence.delete_all_proposals
75
+ end
76
+ end
77
+
61
78
  private
62
79
 
63
80
  def evict_oldest
64
81
  oldest = @proposals.values.min_by { |p| p.created_at.to_f }
65
- @proposals.delete(oldest.id) if oldest
82
+ return unless oldest
83
+
84
+ @proposals.delete(oldest.id)
85
+ @persistence.delete_proposal(oldest.id)
86
+ end
87
+
88
+ def rehydrate_from_cache
89
+ cached = @persistence.load_all_proposals
90
+ cached.each do |id, hash|
91
+ proposal = ConceptProposal.from_h(hash)
92
+ @proposals[id] = proposal
93
+ end
94
+ log.info "[proposal_store] rehydrated #{cached.size} proposals from cache" unless cached.empty?
95
+ rescue StandardError => e
96
+ log.error "[proposal_store] rehydrate_from_cache failed: #{e.message}"
97
+ end
98
+
99
+ def log
100
+ Legion::Logging
66
101
  end
67
102
  end
68
103
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MindGrowth
6
+ module Hooks
7
+ module GovernanceListener
8
+ module_function
9
+
10
+ def register
11
+ return unless defined?(Legion::Events)
12
+
13
+ Legion::Events.on('governance.quorum_reached') do |event|
14
+ next unless event[:verdict] == :approved
15
+
16
+ base_path = event[:base_path] || GovernanceListener.configured_base_path
17
+ GovernanceListener.log.info "[governance_listener] quorum approved for #{event[:proposal_id]}, triggering build"
18
+ Runners::Governance.governance_resolved(proposal_id: event[:proposal_id], base_path: base_path)
19
+ end
20
+ end
21
+
22
+ def configured_base_path
23
+ return nil unless defined?(Legion::Settings)
24
+
25
+ Legion::Settings.dig(:mind_growth, :base_path)
26
+ rescue StandardError => e
27
+ log.debug "[governance_listener] configured_base_path failed: #{e.message}"
28
+ nil
29
+ end
30
+
31
+ def log
32
+ Legion::Logging
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -16,6 +16,7 @@ module Legion
16
16
 
17
17
  pipeline = Helpers::BuildPipeline.new(proposal)
18
18
  proposal.transition!(:building)
19
+ persist_proposal(proposal)
19
20
  base_path ||= ::Dir.pwd
20
21
 
21
22
  run_stage(pipeline, :scaffold, -> { scaffold_stage(proposal, base_path) })
@@ -25,6 +26,7 @@ module Legion
25
26
  run_stage(pipeline, :register, -> { register_stage(proposal) }) unless pipeline.failed?
26
27
 
27
28
  proposal.transition!(pipeline.complete? ? :passing : :build_failed)
29
+ persist_proposal(proposal)
28
30
  log.info "[mind_growth:builder] #{proposal.name}: #{pipeline.stage}"
29
31
  { success: pipeline.complete?, pipeline: pipeline.to_h, proposal: proposal.to_h }
30
32
  rescue ArgumentError => e
@@ -46,6 +48,12 @@ module Legion
46
48
  Runners::Proposer.get_proposal_object(proposal_id)
47
49
  end
48
50
 
51
+ def persist_proposal(proposal)
52
+ return unless defined?(Runners::Proposer) && Runners::Proposer.respond_to?(:persist_proposal)
53
+
54
+ Runners::Proposer.persist_proposal(proposal)
55
+ end
56
+
49
57
  def run_stage(pipeline, stage, callable)
50
58
  return if pipeline.stage != stage
51
59
 
@@ -19,6 +19,7 @@ module Legion
19
19
  return { success: false, error: :invalid_status, current_status: proposal.status } unless %i[proposed evaluating].include?(proposal.status)
20
20
 
21
21
  proposal.transition!(:evaluating)
22
+ Runners::Proposer.persist_proposal(proposal)
22
23
  { success: true, proposal_id: proposal_id, status: :evaluating }
23
24
  rescue ArgumentError => e
24
25
  { success: false, error: e.message }
@@ -28,15 +29,54 @@ module Legion
28
29
  vote_sym = vote.to_sym
29
30
  return { success: false, error: :invalid_vote } unless VOTE_VALUES.include?(vote_sym)
30
31
 
31
- votes_mutex.synchronize do
32
+ snapshot = votes_mutex.synchronize do
32
33
  votes_store[proposal_id] ||= []
33
34
  votes_store[proposal_id] << { vote: vote_sym, agent_id: agent_id.to_s, rationale: rationale,
34
35
  cast_at: Time.now.utc }
36
+ votes_store.transform_values(&:dup)
37
+ end
38
+
39
+ Helpers::ProposalPersistence.new.save_votes(snapshot)
40
+
41
+ tally = tally_votes(proposal_id: proposal_id)
42
+ if tally[:verdict] != :pending
43
+ log.info "[governance] quorum reached for #{proposal_id}: #{tally[:verdict]}"
44
+ if defined?(Legion::Events)
45
+ Legion::Events.emit('governance.quorum_reached',
46
+ proposal_id: proposal_id, verdict: tally[:verdict])
47
+ end
48
+ return { success: true, proposal_id: proposal_id, vote: vote_sym,
49
+ agent_id: agent_id.to_s, verdict: tally[:verdict], quorum_reached: true }
35
50
  end
36
51
 
37
52
  { success: true, proposal_id: proposal_id, vote: vote_sym, agent_id: agent_id.to_s }
38
53
  end
39
54
 
55
+ def governance_resolved(proposal_id:, base_path: nil, **)
56
+ tally = tally_votes(proposal_id: proposal_id)
57
+ return { action: :tally_failed } unless tally[:success]
58
+
59
+ case tally[:verdict]
60
+ when :approved
61
+ log.info "[governance] build triggered for #{proposal_id}"
62
+ build_result = Runners::Builder.build_extension(proposal_id: proposal_id, base_path: base_path)
63
+ { action: :build_triggered, build: build_result, tally: tally }
64
+ when :rejected
65
+ log.info "[governance] proposal rejected: #{proposal_id}"
66
+ proposal = Runners::Proposer.get_proposal_object(proposal_id)
67
+ if proposal
68
+ proposal.transition!(:rejected)
69
+ Runners::Proposer.persist_proposal(proposal)
70
+ end
71
+ { action: :rejected, tally: tally }
72
+ else
73
+ { action: :pending, tally: tally }
74
+ end
75
+ rescue StandardError => e
76
+ log.error "[governance] governance_resolved failed for #{proposal_id}: #{e.message}"
77
+ { action: :error, error: e.message }
78
+ end
79
+
40
80
  def tally_votes(proposal_id:, **)
41
81
  ballots = votes_mutex.synchronize { (votes_store[proposal_id] || []).dup }
42
82
 
@@ -61,6 +101,7 @@ module Legion
61
101
  return { success: false, error: :not_found } unless proposal
62
102
 
63
103
  proposal.transition!(:approved)
104
+ Runners::Proposer.persist_proposal(proposal)
64
105
  { success: true, proposal_id: proposal_id, status: :approved }
65
106
  rescue ArgumentError => e
66
107
  { success: false, error: e.message }
@@ -71,6 +112,7 @@ module Legion
71
112
  return { success: false, error: :not_found } unless proposal
72
113
 
73
114
  proposal.transition!(:rejected)
115
+ Runners::Proposer.persist_proposal(proposal)
74
116
  { success: true, proposal_id: proposal_id, status: :rejected, reason: reason }
75
117
  rescue ArgumentError => e
76
118
  { success: false, error: e.message }
@@ -109,7 +151,11 @@ module Legion
109
151
  private
110
152
 
111
153
  def votes_store
112
- @votes_store ||= {}
154
+ @votes_store ||= begin
155
+ persistence = Helpers::ProposalPersistence.new
156
+ cached = persistence.load_votes
157
+ cached.empty? ? {} : cached.transform_keys(&:to_s)
158
+ end
113
159
  end
114
160
 
115
161
  def votes_mutex
@@ -76,8 +76,104 @@ module Legion
76
76
  model_coverage: profile[:model_coverage]&.map { |m| { model: m[:model], coverage: m[:coverage] } } }
77
77
  end
78
78
 
79
+ def post_build_pipeline(proposal_id:, base_path: nil, **) # rubocop:disable Lint/UnusedMethodArgument
80
+ proposal = Runners::Proposer.get_proposal_object(proposal_id)
81
+ return { success: false, skipped: true, reason: 'proposal not found' } unless proposal
82
+ return { success: false, skipped: true, reason: "not in :passing state (is #{proposal.status})" } unless proposal.status == :passing
83
+
84
+ result = { proposal_id: proposal_id }
85
+
86
+ log.info "[orchestrator] wiring proposal #{proposal_id}"
87
+ wire_result = wire_proposal(proposal)
88
+ result[:wire] = wire_result
89
+
90
+ gaia_wire_skip = wire_result[:reason] == :gaia_not_available
91
+ wire_failed = !gaia_wire_skip && wire_result[:success] == false
92
+
93
+ if wire_failed
94
+ log.warn "[orchestrator] wiring failed for #{proposal_id}: #{wire_result[:reason]}"
95
+ result[:activated] = false
96
+ result[:success] = false
97
+ result[:reason] = :wire_failed
98
+ return result
99
+ end
100
+
101
+ if gaia_wire_skip
102
+ log.info "[orchestrator] wiring skipped for #{proposal_id} (GAIA unavailable)"
103
+ else
104
+ proposal.transition!(:wired)
105
+ Runners::Proposer.persist_proposal(proposal)
106
+ end
107
+
108
+ log.info "[orchestrator] testing proposal #{proposal_id}"
109
+ test_result = test_proposal(proposal)
110
+ result[:integration_test] = test_result
111
+
112
+ if test_result[:reason] == :gaia_not_available
113
+ log.info "[orchestrator] integration test skipped for #{proposal_id} (GAIA unavailable)"
114
+ result[:success] = false
115
+ result[:activated] = false
116
+ result[:reason] = :gaia_not_available
117
+ return result
118
+ end
119
+
120
+ if test_result[:success] == false
121
+ proposal.transition!(:degraded)
122
+ Runners::Proposer.persist_proposal(proposal)
123
+ result[:activated] = false
124
+ result[:success] = false
125
+ log.info "[orchestrator] proposal #{proposal_id} degraded after integration test"
126
+ else
127
+ proposal.transition!(:active)
128
+ Runners::Proposer.persist_proposal(proposal)
129
+ result[:activated] = true
130
+ result[:success] = true
131
+ log.info "[orchestrator] proposal #{proposal_id} activated"
132
+ end
133
+
134
+ result
135
+ rescue StandardError => e
136
+ log.error "[orchestrator] post_build_pipeline failed for #{proposal_id}: #{e.message}"
137
+ { success: false, error: e.message }
138
+ end
139
+
79
140
  private
80
141
 
142
+ def wire_proposal(proposal)
143
+ phase = Helpers::PhaseAllocator.allocate_phase(
144
+ category: proposal.category,
145
+ runner_methods: proposal.runner_methods
146
+ )
147
+ log.info "[orchestrator] wiring #{proposal.name} into tick phase #{phase[:phase]}"
148
+ Runners::Wirer.wire_extension(
149
+ extension_name: proposal.name,
150
+ ext_module: proposal.module_name,
151
+ runner_module: "#{proposal.module_name}::Runners",
152
+ fn: proposal.runner_methods&.first&.dig(:name) || 'status',
153
+ phase: phase[:phase]
154
+ )
155
+ rescue StandardError => e
156
+ log.error "[orchestrator] wire_proposal failed for #{proposal.name}: #{e.message}"
157
+ { success: false, reason: e.message }
158
+ end
159
+
160
+ def test_proposal(proposal)
161
+ log.info "[orchestrator] running integration test for #{proposal.name}"
162
+ phase = Helpers::PhaseAllocator.allocate_phase(
163
+ category: proposal.category,
164
+ runner_methods: proposal.runner_methods
165
+ )
166
+ Runners::IntegrationTester.test_extension_in_tick(
167
+ ext_module: proposal.module_name,
168
+ runner_module: "#{proposal.module_name}::Runners",
169
+ fn: proposal.runner_methods&.first&.dig(:name) || 'status',
170
+ phase: phase[:phase]
171
+ )
172
+ rescue StandardError => e
173
+ log.error "[orchestrator] test_proposal failed for #{proposal.name}: #{e.message}"
174
+ { success: false, reason: e.message }
175
+ end
176
+
81
177
  def propose_from_priorities(priorities, max)
82
178
  priorities.first(max).filter_map do |priority_name|
83
179
  name = "lex-#{priority_name.to_s.tr('_', '-')}"
@@ -126,6 +222,13 @@ module Legion
126
222
  succeeded: builds.count { |b| b[:success] },
127
223
  failed: builds.count { |b| !b[:success] },
128
224
  held: force ? 0 : held_count }
225
+ builds.each do |build|
226
+ next unless build[:success]
227
+
228
+ proposal_id = build[:proposal]&.dig(:id) || build[:proposal_id]
229
+ post_result = post_build_pipeline(proposal_id: proposal_id, base_path: base_path)
230
+ trace[:steps] << { step: :post_build, proposal_id: proposal_id, result: post_result }
231
+ end
129
232
  nil
130
233
  end
131
234
  end
@@ -61,6 +61,7 @@ module Legion
61
61
 
62
62
  eval_scores = scores || score_with_llm(proposal) || default_scores
63
63
  proposal.evaluate!(eval_scores)
64
+ proposal_store.update(proposal)
64
65
  log.info "[mind_growth:proposer] evaluated #{proposal.name}: #{proposal.status}"
65
66
  { success: true, proposal: proposal.to_h, approved: proposal.status == :approved,
66
67
  auto_approved: proposal.auto_approvable? }
@@ -81,6 +82,10 @@ module Legion
81
82
  proposal_store.get(id)
82
83
  end
83
84
 
85
+ def persist_proposal(proposal)
86
+ proposal_store.update(proposal)
87
+ end
88
+
84
89
  private
85
90
 
86
91
  def proposal_store
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module MindGrowth
6
- VERSION = '0.2.7'
6
+ VERSION = '0.3.0'
7
7
  end
8
8
  end
9
9
  end
@@ -5,6 +5,7 @@ require 'securerandom'
5
5
  require 'legion/extensions/mind_growth/version'
6
6
  require 'legion/extensions/mind_growth/helpers/constants'
7
7
  require 'legion/extensions/mind_growth/helpers/concept_proposal'
8
+ require 'legion/extensions/mind_growth/helpers/proposal_persistence'
8
9
  require 'legion/extensions/mind_growth/helpers/proposal_store'
9
10
  require 'legion/extensions/mind_growth/helpers/cognitive_models'
10
11
  require 'legion/extensions/mind_growth/helpers/build_pipeline'
@@ -19,6 +20,8 @@ require 'legion/extensions/mind_growth/runners/wirer'
19
20
  require 'legion/extensions/mind_growth/runners/integration_tester'
20
21
  require 'legion/extensions/mind_growth/runners/retrospective'
21
22
  require 'legion/extensions/mind_growth/runners/governance'
23
+ require 'legion/extensions/mind_growth/hooks/governance_listener'
24
+ Legion::Extensions::MindGrowth::Hooks::GovernanceListener.register
22
25
  require 'legion/extensions/mind_growth/runners/risk_assessor'
23
26
  require 'legion/extensions/mind_growth/helpers/composition_map'
24
27
  require 'legion/extensions/mind_growth/runners/monitor'
@@ -39,6 +42,18 @@ module Legion
39
42
  def self.remote_invocable?
40
43
  false
41
44
  end
45
+
46
+ def self.mcp_tools?
47
+ false
48
+ end
49
+
50
+ def self.mcp_tools_deferred?
51
+ false
52
+ end
53
+
54
+ def self.transport_required?
55
+ false
56
+ end
42
57
  end
43
58
  end
44
59
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::MindGrowth::Helpers::Constants do
4
+ describe 'PROPOSAL_STATUSES' do
5
+ subject(:statuses) { described_class::PROPOSAL_STATUSES }
6
+
7
+ it 'is frozen' do
8
+ expect(statuses).to be_frozen
9
+ end
10
+
11
+ it 'contains only symbols' do
12
+ expect(statuses).to all(be_a(Symbol))
13
+ end
14
+
15
+ # Full lifecycle:
16
+ # proposed → evaluating → approved → building → testing → passing → wired → active
17
+ # → rejected → build_failed
18
+ # → degraded
19
+ # → pruned
20
+ let(:full_lifecycle_states) do
21
+ %i[proposed evaluating approved rejected building testing passing wired active degraded pruned build_failed]
22
+ end
23
+
24
+ it 'includes every state in the full proposal lifecycle' do
25
+ full_lifecycle_states.each do |state|
26
+ expect(statuses).to include(state), "expected PROPOSAL_STATUSES to include :#{state}"
27
+ end
28
+ end
29
+
30
+ it 'includes the primary happy-path states in order' do
31
+ happy_path = %i[proposed evaluating approved building testing passing wired active]
32
+ expect(statuses).to include(*happy_path)
33
+ end
34
+
35
+ it 'includes the rejection terminal state' do
36
+ expect(statuses).to include(:rejected)
37
+ end
38
+
39
+ it 'includes the build_failed terminal state' do
40
+ expect(statuses).to include(:build_failed)
41
+ end
42
+
43
+ it 'includes degraded and pruned operational states' do
44
+ expect(statuses).to include(:degraded, :pruned)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::MindGrowth::Helpers::ProposalPersistence do
4
+ subject(:persistence) { described_class.new(namespace: 'test_proposals') }
5
+
6
+ before do
7
+ stub_const('Legion::Cache', Class.new do
8
+ def self.connected? = true
9
+ def self.get(key) = (@store ||= {})[key]
10
+ def self.set_sync(key, value, **) = ((@store ||= {})[key] = value)
11
+ def self.delete_sync(key) = (@store ||= {}).delete(key)
12
+ def self.flush = (@store = {})
13
+ end)
14
+ Legion::Cache.flush
15
+ end
16
+
17
+ describe '#save_proposal / #load_proposal' do
18
+ it 'round-trips a proposal hash' do
19
+ proposal_hash = { id: 'p-001', name: 'lex-test', module_name: 'Test',
20
+ category: :cognition, description: 'test', status: :approved,
21
+ scores: { novelty: 0.8 }, created_at: Time.now.utc.to_s }
22
+ persistence.save_proposal(proposal_hash)
23
+ loaded = persistence.load_proposal('p-001')
24
+ expect(loaded[:name]).to eq('lex-test')
25
+ # JSON round-trip preserves string values; from_h converts to symbols at rehydration
26
+ expect(loaded[:status].to_sym).to eq(:approved)
27
+ end
28
+
29
+ it 'returns nil for an unknown id' do
30
+ expect(persistence.load_proposal('no-such-id')).to be_nil
31
+ end
32
+
33
+ it 'adds the id to the index on save' do
34
+ persistence.save_proposal({ id: 'p-001', name: 'one' })
35
+ all = persistence.load_all_proposals
36
+ # IDs are stored as strings; key may be string or symbol depending on JSON parse
37
+ expect(all.transform_keys(&:to_s)).to have_key('p-001')
38
+ end
39
+ end
40
+
41
+ describe '#delete_proposal' do
42
+ it 'removes the proposal from cache' do
43
+ persistence.save_proposal({ id: 'p-del', name: 'delete-me' })
44
+ persistence.delete_proposal('p-del')
45
+ expect(persistence.load_proposal('p-del')).to be_nil
46
+ end
47
+
48
+ it 'removes the id from the index' do
49
+ persistence.save_proposal({ id: 'p-del', name: 'delete-me' })
50
+ persistence.delete_proposal('p-del')
51
+ all_str = persistence.load_all_proposals.transform_keys(&:to_s)
52
+ expect(all_str).not_to have_key('p-del')
53
+ end
54
+ end
55
+
56
+ describe '#save_votes / #load_votes' do
57
+ it 'round-trips governance votes' do
58
+ votes = { 'p-001' => [{ vote: :approve, agent_id: 'a1', cast_at: Time.now.utc.to_s }] }
59
+ persistence.save_votes(votes)
60
+ loaded = persistence.load_votes
61
+ # JSON round-trip: top-level keys become symbols, vote values are strings
62
+ ballot = (loaded[:'p-001'] || loaded['p-001']).first
63
+ expect(ballot[:vote].to_sym).to eq(:approve)
64
+ end
65
+
66
+ it 'returns empty hash when no votes are stored' do
67
+ expect(persistence.load_votes).to eq({})
68
+ end
69
+ end
70
+
71
+ describe '#load_all_proposals' do
72
+ it 'returns all persisted proposals' do
73
+ persistence.save_proposal({ id: 'p-001', name: 'one', status: :approved })
74
+ persistence.save_proposal({ id: 'p-002', name: 'two', status: :proposed })
75
+ all = persistence.load_all_proposals
76
+ expect(all.size).to eq(2)
77
+ end
78
+
79
+ it 'returns an empty hash when nothing is stored' do
80
+ expect(persistence.load_all_proposals).to eq({})
81
+ end
82
+
83
+ it 'does not include deleted proposals' do
84
+ persistence.save_proposal({ id: 'p-001', name: 'one' })
85
+ persistence.save_proposal({ id: 'p-002', name: 'two' })
86
+ persistence.delete_proposal('p-001')
87
+ all = persistence.load_all_proposals
88
+ expect(all.size).to eq(1)
89
+ expect(all.transform_keys(&:to_s)).to have_key('p-002')
90
+ end
91
+ end
92
+
93
+ describe 'when cache unavailable' do
94
+ before { allow(Legion::Cache).to receive(:connected?).and_return(false) }
95
+
96
+ it 'degrades gracefully on load_proposal' do
97
+ expect(persistence.load_proposal('p-001')).to be_nil
98
+ end
99
+
100
+ it 'degrades gracefully on save_proposal' do
101
+ expect(persistence.save_proposal({ id: 'x' })).to be false
102
+ end
103
+
104
+ it 'degrades gracefully on delete_proposal' do
105
+ expect(persistence.delete_proposal('x')).to be false
106
+ end
107
+
108
+ it 'degrades gracefully on load_votes' do
109
+ expect(persistence.load_votes).to eq({})
110
+ end
111
+
112
+ it 'degrades gracefully on save_votes' do
113
+ expect(persistence.save_votes({ 'p' => [] })).to be false
114
+ end
115
+
116
+ it 'degrades gracefully on load_all_proposals' do
117
+ expect(persistence.load_all_proposals).to eq({})
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe 'Governance callback triggers build' do
4
+ let(:governance) { Legion::Extensions::MindGrowth::Runners::Governance }
5
+ let(:proposer) { Legion::Extensions::MindGrowth::Runners::Proposer }
6
+
7
+ before { proposer.instance_variable_set(:@proposal_store, nil) }
8
+
9
+ describe '.governance_resolved' do
10
+ it 'triggers build when quorum approves' do
11
+ proposal = proposer.propose_concept(name: 'lex-test-governance', category: :cognition,
12
+ description: 'test governance callback', enrich: false)
13
+ proposal_id = proposal[:proposal][:id]
14
+ proposer.evaluate_proposal(proposal_id: proposal_id,
15
+ scores: { novelty: 0.7, fit: 0.7, cognitive_value: 0.7,
16
+ implementability: 0.7, composability: 0.7 })
17
+ governance.instance_variable_set(:@votes_store, {})
18
+ governance.vote_on_proposal(proposal_id: proposal_id, vote: :approve, agent_id: 'agent-1')
19
+ governance.vote_on_proposal(proposal_id: proposal_id, vote: :approve, agent_id: 'agent-2')
20
+ governance.vote_on_proposal(proposal_id: proposal_id, vote: :approve, agent_id: 'agent-3')
21
+
22
+ resolved = governance.governance_resolved(proposal_id: proposal_id)
23
+ expect(resolved[:action]).to eq(:build_triggered)
24
+ end
25
+
26
+ it 'does not trigger build when quorum rejects' do
27
+ proposal = proposer.propose_concept(name: 'lex-test-rejected', category: :cognition,
28
+ description: 'test rejected', enrich: false)
29
+ proposal_id = proposal[:proposal][:id]
30
+ proposer.evaluate_proposal(proposal_id: proposal_id,
31
+ scores: { novelty: 0.7, fit: 0.7, cognitive_value: 0.7,
32
+ implementability: 0.7, composability: 0.7 })
33
+ governance.instance_variable_set(:@votes_store, {})
34
+ 3.times { |i| governance.vote_on_proposal(proposal_id: proposal_id, vote: :reject, agent_id: "a#{i}") }
35
+ resolved = governance.governance_resolved(proposal_id: proposal_id)
36
+ expect(resolved[:action]).to eq(:rejected)
37
+ end
38
+
39
+ it 'returns pending when quorum not reached' do
40
+ governance.instance_variable_set(:@votes_store, {})
41
+ governance.vote_on_proposal(proposal_id: 'fake-id', vote: :approve, agent_id: 'agent-1')
42
+ resolved = governance.governance_resolved(proposal_id: 'fake-id')
43
+ expect(resolved[:action]).to eq(:pending)
44
+ end
45
+ end
46
+
47
+ describe 'event listener registration' do
48
+ it 'registers a listener for governance.quorum_reached' do
49
+ events_spy = Class.new do
50
+ attr_reader :registered
51
+
52
+ def initialize
53
+ @registered = []
54
+ end
55
+
56
+ def on(event, &block)
57
+ @registered << { event: event, block: block }
58
+ end
59
+
60
+ def emit(*)
61
+ nil
62
+ end
63
+ end.new
64
+ stub_const('Legion::Events', events_spy)
65
+ Legion::Extensions::MindGrowth::Hooks::GovernanceListener.register
66
+ expect(events_spy.registered.map { |r| r[:event] }).to include('governance.quorum_reached')
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'Self-Improvement Pipeline Integration' do
6
+ let(:orchestrator) { Legion::Extensions::MindGrowth::Runners::Orchestrator }
7
+ let(:proposer) { Legion::Extensions::MindGrowth::Runners::Proposer }
8
+ let(:governance) { Legion::Extensions::MindGrowth::Runners::Governance }
9
+
10
+ before do
11
+ proposer.instance_variable_set(:@proposal_store, nil)
12
+ governance.instance_variable_set(:@votes_store, nil)
13
+ governance.instance_variable_set(:@votes_mutex, nil)
14
+ end
15
+
16
+ describe 'auto-approve path (force: true bypasses governance)' do
17
+ it 'runs analyze → propose → evaluate → build' do
18
+ result = orchestrator.run_growth_cycle(
19
+ existing_extensions: [],
20
+ max_proposals: 1,
21
+ force: true
22
+ )
23
+
24
+ expect(result[:success]).to be true
25
+ trace = result[:trace]
26
+
27
+ step_names = trace[:steps].map { |s| s[:step] }
28
+ expect(step_names).to include(:analyze)
29
+ expect(step_names).to include(:propose)
30
+ expect(step_names).to include(:evaluate)
31
+ expect(step_names).to include(:build)
32
+ end
33
+
34
+ it 'build step attempts at least one build when force: true' do
35
+ result = orchestrator.run_growth_cycle(
36
+ existing_extensions: [],
37
+ max_proposals: 1,
38
+ force: true
39
+ )
40
+ build_step = result[:trace][:steps].find { |s| s[:step] == :build }
41
+ expect(build_step[:attempted]).to be > 0
42
+ end
43
+
44
+ it 'holds proposals for governance review when force is false (default scores 0.7)' do
45
+ result = orchestrator.run_growth_cycle(
46
+ existing_extensions: [],
47
+ max_proposals: 1,
48
+ force: false
49
+ )
50
+
51
+ expect(result[:success]).to be true
52
+ build_step = result[:trace][:steps].find { |s| s[:step] == :build }
53
+ expect(build_step[:held]).to be > 0
54
+ expect(build_step[:attempted]).to eq(0)
55
+ end
56
+ end
57
+
58
+ describe 'governance path (scores 0.6-0.9)' do
59
+ it 'holds proposal, then resolves to build_triggered on approve quorum' do
60
+ prop = proposer.propose_concept(
61
+ name: 'lex-gov-test', category: :cognition,
62
+ description: 'governance path test', enrich: false
63
+ )
64
+ proposal_id = prop[:proposal][:id]
65
+
66
+ proposer.evaluate_proposal(
67
+ proposal_id: proposal_id,
68
+ scores: { novelty: 0.7, fit: 0.7, cognitive_value: 0.7,
69
+ implementability: 0.7, composability: 0.7 }
70
+ )
71
+
72
+ proposal = proposer.get_proposal_object(proposal_id)
73
+ expect(proposal.status).to eq(:approved)
74
+ expect(proposal.auto_approvable?).to be false
75
+
76
+ governance.vote_on_proposal(proposal_id: proposal_id, vote: :approve, agent_id: 'a1')
77
+ governance.vote_on_proposal(proposal_id: proposal_id, vote: :approve, agent_id: 'a2')
78
+ governance.vote_on_proposal(proposal_id: proposal_id, vote: :approve, agent_id: 'a3')
79
+
80
+ resolved = governance.governance_resolved(proposal_id: proposal_id)
81
+ expect(resolved[:action]).to eq(:build_triggered)
82
+ end
83
+ end
84
+
85
+ describe 'post-build pipeline' do
86
+ it 'transitions from passing through wire to activation' do
87
+ prop = proposer.propose_concept(
88
+ name: 'lex-postbuild-test', category: :safety,
89
+ description: 'post-build test', enrich: false
90
+ )
91
+ proposal_id = prop[:proposal][:id]
92
+
93
+ proposer.evaluate_proposal(proposal_id: proposal_id,
94
+ scores: { novelty: 0.95, fit: 0.95, cognitive_value: 0.95,
95
+ implementability: 0.95, composability: 0.95 })
96
+
97
+ proposal = proposer.get_proposal_object(proposal_id)
98
+ proposal.transition!(:building)
99
+ proposal.transition!(:passing)
100
+
101
+ allow(Legion::Extensions::MindGrowth::Runners::Wirer)
102
+ .to receive(:wire_extension)
103
+ .and_return({ success: true, extension: proposal.name, phase: :safety })
104
+ allow(Legion::Extensions::MindGrowth::Runners::IntegrationTester)
105
+ .to receive(:test_extension_in_tick)
106
+ .and_return({ success: true, phase: :safety_integration })
107
+
108
+ result = orchestrator.post_build_pipeline(proposal_id: proposal_id)
109
+ expect(result).to have_key(:wire)
110
+ expect(result).to have_key(:integration_test)
111
+
112
+ proposal = proposer.get_proposal_object(proposal_id)
113
+ expect(proposal.status).to eq(:active)
114
+ end
115
+ end
116
+
117
+ describe 'rejection path' do
118
+ it 'rejects proposal when governance votes against' do
119
+ prop = proposer.propose_concept(
120
+ name: 'lex-reject-test', category: :cognition,
121
+ description: 'rejection test', enrich: false
122
+ )
123
+ proposal_id = prop[:proposal][:id]
124
+
125
+ proposer.evaluate_proposal(proposal_id: proposal_id,
126
+ scores: { novelty: 0.7, fit: 0.7, cognitive_value: 0.7,
127
+ implementability: 0.7, composability: 0.7 })
128
+
129
+ governance.vote_on_proposal(proposal_id: proposal_id, vote: :reject, agent_id: 'a1')
130
+ governance.vote_on_proposal(proposal_id: proposal_id, vote: :reject, agent_id: 'a2')
131
+ governance.vote_on_proposal(proposal_id: proposal_id, vote: :reject, agent_id: 'a3')
132
+
133
+ resolved = governance.governance_resolved(proposal_id: proposal_id)
134
+ expect(resolved[:action]).to eq(:rejected)
135
+
136
+ proposal = proposer.get_proposal_object(proposal_id)
137
+ expect(proposal.status).to eq(:rejected)
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe 'Orchestrator full cycle with wire + test + activate' do
4
+ let(:orchestrator) { Legion::Extensions::MindGrowth::Runners::Orchestrator }
5
+ let(:proposer) { Legion::Extensions::MindGrowth::Runners::Proposer }
6
+
7
+ before { proposer.instance_variable_set(:@proposal_store, nil) }
8
+
9
+ describe '.post_build_pipeline' do
10
+ it 'calls wire_extension after successful build' do
11
+ prop_result = proposer.propose_concept(name: 'lex-test-wire', category: :cognition,
12
+ description: 'test wiring', enrich: false)
13
+ proposal_id = prop_result[:proposal][:id]
14
+ proposer.evaluate_proposal(
15
+ proposal_id: proposal_id,
16
+ scores: { novelty: 0.95, fit: 0.95, cognitive_value: 0.95,
17
+ implementability: 0.95, composability: 0.95 }
18
+ )
19
+ proposal = proposer.get_proposal_object(proposal_id)
20
+ proposal.transition!(:building)
21
+ proposal.transition!(:passing)
22
+
23
+ result = orchestrator.post_build_pipeline(proposal_id: proposal_id)
24
+ expect(result).to have_key(:wire)
25
+ expect(result).to have_key(:integration_test)
26
+ end
27
+
28
+ it 'skips wire for non-passing proposals' do
29
+ prop_result = proposer.propose_concept(name: 'lex-test-skip', category: :cognition,
30
+ description: 'test skip', enrich: false)
31
+ proposal_id = prop_result[:proposal][:id]
32
+ result = orchestrator.post_build_pipeline(proposal_id: proposal_id)
33
+ expect(result[:skipped]).to be true
34
+ expect(result[:reason]).to include('not in :passing')
35
+ end
36
+
37
+ it 'returns skipped: true for unknown proposal_id' do
38
+ result = orchestrator.post_build_pipeline(proposal_id: 'nonexistent-id')
39
+ expect(result[:skipped]).to be true
40
+ expect(result[:reason]).to include('proposal not found')
41
+ end
42
+
43
+ it 'includes proposal_id in the result' do
44
+ prop_result = proposer.propose_concept(name: 'lex-test-id', category: :cognition,
45
+ description: 'test id tracking', enrich: false)
46
+ proposal_id = prop_result[:proposal][:id]
47
+ proposal = proposer.get_proposal_object(proposal_id)
48
+ proposal.transition!(:building)
49
+ proposal.transition!(:passing)
50
+
51
+ result = orchestrator.post_build_pipeline(proposal_id: proposal_id)
52
+ expect(result[:proposal_id]).to eq(proposal_id)
53
+ end
54
+
55
+ it 'transitions to :active when integration test succeeds' do
56
+ prop_result = proposer.propose_concept(name: 'lex-test-active', category: :cognition,
57
+ description: 'test activation', enrich: false)
58
+ proposal_id = prop_result[:proposal][:id]
59
+ proposal = proposer.get_proposal_object(proposal_id)
60
+ proposal.transition!(:building)
61
+ proposal.transition!(:passing)
62
+
63
+ allow(Legion::Extensions::MindGrowth::Runners::Wirer)
64
+ .to receive(:wire_extension)
65
+ .and_return({ success: true, extension: proposal.name, phase: :working_memory })
66
+ allow(Legion::Extensions::MindGrowth::Runners::IntegrationTester)
67
+ .to receive(:test_extension_in_tick)
68
+ .and_return({ success: true, phase: :working_memory_integration })
69
+
70
+ result = orchestrator.post_build_pipeline(proposal_id: proposal_id)
71
+ expect(result[:activated]).to be true
72
+ expect(proposal.status).to eq(:active)
73
+ end
74
+
75
+ it 'transitions to :degraded when integration test fails' do
76
+ prop_result = proposer.propose_concept(name: 'lex-test-degraded', category: :cognition,
77
+ description: 'test degraded path', enrich: false)
78
+ proposal_id = prop_result[:proposal][:id]
79
+ proposal = proposer.get_proposal_object(proposal_id)
80
+ proposal.transition!(:building)
81
+ proposal.transition!(:passing)
82
+
83
+ allow(Legion::Extensions::MindGrowth::Runners::Wirer)
84
+ .to receive(:wire_extension)
85
+ .and_return({ success: true, extension: proposal.name, phase: :working_memory })
86
+ allow(Legion::Extensions::MindGrowth::Runners::IntegrationTester)
87
+ .to receive(:test_extension_in_tick)
88
+ .and_return({ success: false, reason: :runner_not_found })
89
+
90
+ result = orchestrator.post_build_pipeline(proposal_id: proposal_id)
91
+ expect(result[:activated]).to be false
92
+ expect(proposal.status).to eq(:degraded)
93
+ end
94
+
95
+ it 'ignores unknown keyword arguments' do
96
+ prop_result = proposer.propose_concept(name: 'lex-test-kwargs', category: :cognition,
97
+ description: 'test kwargs', enrich: false)
98
+ proposal_id = prop_result[:proposal][:id]
99
+ proposal = proposer.get_proposal_object(proposal_id)
100
+ proposal.transition!(:building)
101
+ proposal.transition!(:passing)
102
+
103
+ expect { orchestrator.post_build_pipeline(proposal_id: proposal_id, unknown: :val) }.not_to raise_error
104
+ end
105
+ end
106
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-mind-growth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -141,7 +141,9 @@ files:
141
141
  - lib/legion/extensions/mind_growth/helpers/constants.rb
142
142
  - lib/legion/extensions/mind_growth/helpers/fitness_evaluator.rb
143
143
  - lib/legion/extensions/mind_growth/helpers/phase_allocator.rb
144
+ - lib/legion/extensions/mind_growth/helpers/proposal_persistence.rb
144
145
  - lib/legion/extensions/mind_growth/helpers/proposal_store.rb
146
+ - lib/legion/extensions/mind_growth/hooks/governance_listener.rb
145
147
  - lib/legion/extensions/mind_growth/runners/analyzer.rb
146
148
  - lib/legion/extensions/mind_growth/runners/builder.rb
147
149
  - lib/legion/extensions/mind_growth/runners/competitive_evolver.rb
@@ -167,9 +169,13 @@ files:
167
169
  - spec/legion/extensions/mind_growth/helpers/cognitive_models_spec.rb
168
170
  - spec/legion/extensions/mind_growth/helpers/composition_map_spec.rb
169
171
  - spec/legion/extensions/mind_growth/helpers/concept_proposal_spec.rb
172
+ - spec/legion/extensions/mind_growth/helpers/constants_spec.rb
170
173
  - spec/legion/extensions/mind_growth/helpers/fitness_evaluator_spec.rb
171
174
  - spec/legion/extensions/mind_growth/helpers/phase_allocator_spec.rb
175
+ - spec/legion/extensions/mind_growth/helpers/proposal_persistence_spec.rb
172
176
  - spec/legion/extensions/mind_growth/helpers/proposal_store_spec.rb
177
+ - spec/legion/extensions/mind_growth/hooks/governance_callback_spec.rb
178
+ - spec/legion/extensions/mind_growth/integration_spec.rb
173
179
  - spec/legion/extensions/mind_growth/runners/analyzer_spec.rb
174
180
  - spec/legion/extensions/mind_growth/runners/builder_spec.rb
175
181
  - spec/legion/extensions/mind_growth/runners/competitive_evolver_spec.rb
@@ -182,6 +188,7 @@ files:
182
188
  - spec/legion/extensions/mind_growth/runners/integration_tester_spec.rb
183
189
  - spec/legion/extensions/mind_growth/runners/monitor_spec.rb
184
190
  - spec/legion/extensions/mind_growth/runners/orchestrator_spec.rb
191
+ - spec/legion/extensions/mind_growth/runners/post_build_pipeline_spec.rb
185
192
  - spec/legion/extensions/mind_growth/runners/proposer_spec.rb
186
193
  - spec/legion/extensions/mind_growth/runners/retrospective_spec.rb
187
194
  - spec/legion/extensions/mind_growth/runners/risk_assessor_spec.rb