lex-mind-growth 0.2.5 → 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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/lib/legion/extensions/mind_growth/actors/growth_cycle.rb +3 -3
  4. data/lib/legion/extensions/mind_growth/helpers/composition_map.rb +4 -4
  5. data/lib/legion/extensions/mind_growth/helpers/concept_proposal.rb +24 -1
  6. data/lib/legion/extensions/mind_growth/helpers/phase_allocator.rb +1 -1
  7. data/lib/legion/extensions/mind_growth/helpers/proposal_persistence.rb +152 -0
  8. data/lib/legion/extensions/mind_growth/helpers/proposal_store.rb +39 -4
  9. data/lib/legion/extensions/mind_growth/hooks/governance_listener.rb +38 -0
  10. data/lib/legion/extensions/mind_growth/runners/builder.rb +72 -22
  11. data/lib/legion/extensions/mind_growth/runners/competitive_evolver.rb +3 -2
  12. data/lib/legion/extensions/mind_growth/runners/composer.rb +3 -3
  13. data/lib/legion/extensions/mind_growth/runners/consensus_builder.rb +4 -3
  14. data/lib/legion/extensions/mind_growth/runners/dashboard.rb +2 -2
  15. data/lib/legion/extensions/mind_growth/runners/dream_ideation.rb +2 -2
  16. data/lib/legion/extensions/mind_growth/runners/evolver.rb +15 -8
  17. data/lib/legion/extensions/mind_growth/runners/governance.rb +50 -4
  18. data/lib/legion/extensions/mind_growth/runners/integration_tester.rb +4 -3
  19. data/lib/legion/extensions/mind_growth/runners/monitor.rb +2 -2
  20. data/lib/legion/extensions/mind_growth/runners/orchestrator.rb +105 -2
  21. data/lib/legion/extensions/mind_growth/runners/proposer.rb +30 -11
  22. data/lib/legion/extensions/mind_growth/runners/retrospective.rb +12 -7
  23. data/lib/legion/extensions/mind_growth/runners/risk_assessor.rb +2 -2
  24. data/lib/legion/extensions/mind_growth/runners/swarm_builder.rb +1 -1
  25. data/lib/legion/extensions/mind_growth/runners/wirer.rb +2 -2
  26. data/lib/legion/extensions/mind_growth/version.rb +1 -1
  27. data/lib/legion/extensions/mind_growth.rb +16 -1
  28. data/spec/legion/extensions/mind_growth/actors/growth_cycle_spec.rb +2 -2
  29. data/spec/legion/extensions/mind_growth/helpers/composition_map_spec.rb +1 -1
  30. data/spec/legion/extensions/mind_growth/helpers/constants_spec.rb +47 -0
  31. data/spec/legion/extensions/mind_growth/helpers/proposal_persistence_spec.rb +120 -0
  32. data/spec/legion/extensions/mind_growth/helpers/proposal_store_spec.rb +2 -2
  33. data/spec/legion/extensions/mind_growth/hooks/governance_callback_spec.rb +69 -0
  34. data/spec/legion/extensions/mind_growth/integration_spec.rb +140 -0
  35. data/spec/legion/extensions/mind_growth/runners/competitive_evolver_spec.rb +1 -1
  36. data/spec/legion/extensions/mind_growth/runners/governance_spec.rb +1 -1
  37. data/spec/legion/extensions/mind_growth/runners/post_build_pipeline_spec.rb +106 -0
  38. metadata +8 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ee2be00d683bb637298c7cd47ade31afe6164e69d00d032577ede07a85e23c7
4
- data.tar.gz: aa60548111e18409e2cf09476e8e37cfa9bd4eb8912a57e174b41d6c1e80bfb0
3
+ metadata.gz: 3fcaa84f5b0e455b0ac70019228ebe5bffdbd1c69228aafcad9bf92d7f91bb52
4
+ data.tar.gz: 4ea8b0c108d6bf0a957cd9709147e4cfaa3c78e0b297afc85e642331f9153e4a
5
5
  SHA512:
6
- metadata.gz: 3f82c6da963f5ff2eea9a603e0b1c509ac643ebac93039bff7855cb3c673f927153e664412dc1ecf97bed571102793ecd3ac0859f58c55ce159e596ca82cb532
7
- data.tar.gz: 2c8517423557a7e7c77c2c76b2ac8adbdddb08e3570d1eb430ba25b479b7da664d39af19f6f7800e2200f33af53d475b25c44a5bfb35ff93ab44631e613507f5
6
+ metadata.gz: 911dc8e13b4dfe4cb4ee0cd5dd7cd27fc2d93a529aca350cc4d02d4f3f27b9548ac8881945bb57d150ffa8cc8e169e8f62b3590e2bbab4188dbfc0046aa69eb3
7
+ data.tar.gz: d4f849194085b3b398dd41e3499b686c87b323ab4950a392eaca71133aeb943fcfe68557829a5cb6be92428bf781c90ab53d49cf877f26abe5b699562c25d07e
data/Gemfile CHANGED
@@ -8,6 +8,7 @@ group :test do
8
8
  gem 'rspec', '~> 3.13'
9
9
  gem 'rspec_junit_formatter'
10
10
  gem 'rubocop', '~> 1.75'
11
+ gem 'rubocop-legion', '~> 0.1.7'
11
12
  gem 'rubocop-rspec'
12
13
  gem 'simplecov'
13
14
  end
@@ -6,7 +6,7 @@ module Legion
6
6
  module Extensions
7
7
  module MindGrowth
8
8
  module Actor
9
- class GrowthCycle < Legion::Extensions::Actors::Every
9
+ class GrowthCycle < Legion::Extensions::Actors::Every # rubocop:disable Legion/Extension/EveryActorRequiresTime
10
10
  def runner_class
11
11
  Legion::Extensions::MindGrowth::Runners::Orchestrator
12
12
  end
@@ -19,7 +19,7 @@ module Legion
19
19
  3600
20
20
  end
21
21
 
22
- def enabled?
22
+ def enabled? # rubocop:disable Legion/Extension/ActorEnabledSideEffects
23
23
  codegen_loaded? || exec_loaded?
24
24
  end
25
25
 
@@ -32,7 +32,7 @@ module Legion
32
32
  end
33
33
 
34
34
  def check_subtask?
35
- false
35
+ true
36
36
  end
37
37
 
38
38
  def generate_task?
@@ -7,7 +7,7 @@ module Legion
7
7
  module CompositionMap
8
8
  module_function
9
9
 
10
- @rules = {}
10
+ @rules = {} # rubocop:disable ThreadSafety/MutableClassInstanceVariable
11
11
  @mutex = Mutex.new
12
12
 
13
13
  def add_rule(source_extension:, output_key:, target_extension:, target_method:, transform: nil, **)
@@ -19,7 +19,7 @@ module Legion
19
19
  target_extension: target_extension.to_s,
20
20
  target_method: target_method.to_sym,
21
21
  transform: transform
22
- }
22
+ }.freeze
23
23
  @mutex.synchronize { @rules[rule_id] = rule }
24
24
  { success: true, rule_id: rule_id }
25
25
  end
@@ -31,7 +31,7 @@ module Legion
31
31
 
32
32
  def rules_for(source_extension:, **)
33
33
  src = source_extension.to_s
34
- @mutex.synchronize { @rules.values.select { |r| r[:source_extension] == src } }
34
+ @mutex.synchronize { @rules.values.select { |r| r[:source_extension] == src }.map(&:dup) }
35
35
  end
36
36
 
37
37
  def all_rules
@@ -41,7 +41,7 @@ module Legion
41
41
  def match_output(source_extension:, output:, **)
42
42
  src = source_extension.to_s
43
43
  out_h = output.is_a?(Hash) ? output : {}
44
- rules = @mutex.synchronize { @rules.values.select { |r| r[:source_extension] == src } }
44
+ rules = @mutex.synchronize { @rules.values.select { |r| r[:source_extension] == src }.map(&:dup) }
45
45
 
46
46
  rules.filter_map do |rule|
47
47
  key = rule[:output_key]
@@ -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
@@ -12,7 +14,7 @@ module Legion
12
14
 
13
15
  attr_reader(*FIELDS)
14
16
 
15
- def initialize(name:, module_name:, category:, description:, metaphor: nil, helpers: [],
17
+ def initialize(name:, module_name:, category:, description:, metaphor: nil, helpers: [], # rubocop:disable Metrics/ParameterLists
16
18
  runner_methods: [], rationale: nil, origin: :proposer)
17
19
  @id = SecureRandom.uuid
18
20
  @name = name
@@ -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
@@ -89,7 +89,7 @@ module Legion
89
89
  end
90
90
 
91
91
  def valid_phase?(phase)
92
- CATEGORY_PHASE_MAP.values.include?(phase) || DREAM_PHASE_MAP.values.include?(phase)
92
+ CATEGORY_PHASE_MAP.value?(phase) || DREAM_PHASE_MAP.value?(phase)
93
93
  end
94
94
 
95
95
  def phases_for_category(category)
@@ -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
@@ -5,8 +5,8 @@ module Legion
5
5
  module MindGrowth
6
6
  module Runners
7
7
  module Builder
8
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
- Legion::Extensions::Helpers.const_defined?(:Lex)
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
10
10
 
11
11
  extend self
12
12
 
@@ -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
 
@@ -59,13 +67,21 @@ module Legion
59
67
  end
60
68
 
61
69
  def strip_lex_prefix(name)
62
- name.to_s.sub(/\Alex-/, '')
70
+ name.to_s.delete_prefix('lex-')
71
+ end
72
+
73
+ def proposal_id_for(proposal)
74
+ proposal.id if proposal.respond_to?(:id)
63
75
  end
64
76
 
65
77
  # --- Scaffold Stage ---
66
78
  # Delegates to lex-codegen when loaded; stubs otherwise
67
79
  def scaffold_stage(proposal, base_path)
68
- return { success: true, stage: :scaffold, files: 0, message: 'scaffold requires lex-codegen' } unless codegen_available?
80
+ unless codegen_available?
81
+ return { success: true, stage: :scaffold, files: 0, message: 'scaffold requires lex-codegen',
82
+ proposal_id: proposal_id_for(proposal),
83
+ path: ext_path(proposal, base_path) }
84
+ end
69
85
 
70
86
  name = strip_lex_prefix(proposal.name)
71
87
  result = Legion::Extensions::Codegen::Runners::Generate.scaffold_extension(
@@ -79,14 +95,18 @@ module Legion
79
95
  )
80
96
 
81
97
  { success: result[:success], stage: :scaffold, files: result[:files_created] || 0,
82
- path: result[:path], error: result[:error] }
98
+ path: result[:path] || ext_path(proposal, base_path), error: result[:error],
99
+ proposal_id: proposal_id_for(proposal) }
83
100
  end
84
101
 
85
102
  # --- Implement Stage ---
86
103
  # Delegates to lex-codegen FromGap when loaded; falls back to legion-llm; stubs otherwise
87
104
  def implement_stage(proposal, base_path)
88
105
  unless llm_available? || codegen_from_gap_available?
89
- return { success: true, stage: :implement, message: 'implementation requires legion-llm or lex-codegen' }
106
+ return { success: true, stage: :implement,
107
+ message: 'implementation requires legion-llm or lex-codegen',
108
+ proposal_id: proposal_id_for(proposal),
109
+ path: ext_path(proposal, base_path) }
90
110
  end
91
111
 
92
112
  path = ext_path(proposal, base_path)
@@ -94,7 +114,9 @@ module Legion
94
114
 
95
115
  if target_files.empty?
96
116
  return { success: true, stage: :implement, files_implemented: 0,
97
- message: 'no implementation targets found' }
117
+ message: 'no implementation targets found',
118
+ proposal_id: proposal_id_for(proposal),
119
+ path: path }
98
120
  end
99
121
 
100
122
  files_implemented = 0
@@ -111,43 +133,64 @@ module Legion
111
133
 
112
134
  success = errors.empty?
113
135
  { success: success, stage: :implement, files_implemented: files_implemented,
114
- total_files: target_files.size, error: success ? nil : errors.join('; ') }
136
+ total_files: target_files.size, error: success ? nil : errors.join('; '),
137
+ proposal_id: proposal_id_for(proposal),
138
+ path: path }
115
139
  end
116
140
 
117
141
  # --- Test Stage ---
118
142
  # Delegates to lex-exec bundler runners when loaded; stubs otherwise
119
143
  def test_stage(proposal, base_path)
120
- return { success: true, stage: :test, message: 'testing requires lex-exec' } unless exec_available?
121
-
122
- path = ext_path(proposal, base_path)
144
+ unless exec_available?
145
+ return { success: true, stage: :test, message: 'testing requires lex-exec',
146
+ proposal_id: proposal_id_for(proposal),
147
+ path: ext_path(proposal, base_path) }
148
+ end
123
149
 
150
+ path = ext_path(proposal, base_path)
151
+ pid = proposal_id_for(proposal)
124
152
  install = Legion::Extensions::Exec::Runners::Bundler.install(path: path)
125
153
  unless install[:success]
126
154
  return { success: false, stage: :test, step: :install,
127
- error: install[:stderr] || install[:error] }
155
+ error: install[:stderr] || install[:error],
156
+ proposal_id: pid, path: path }
128
157
  end
129
158
 
159
+ run_test_suite(path: path, pid: pid)
160
+ end
161
+
162
+ def run_test_suite(path:, pid:)
130
163
  rspec = Legion::Extensions::Exec::Runners::Bundler.exec_rspec(path: path)
131
164
  rubocop = Legion::Extensions::Exec::Runners::Bundler.exec_rubocop(path: path)
132
165
 
133
166
  rspec_ok = rspec[:success] && (rspec.dig(:parsed, :failures) || 0).zero?
134
167
  rubocop_ok = rubocop[:success]
135
168
 
136
- errors = [
137
- (rspec_ok ? nil : "rspec: #{rspec[:parsed] || rspec[:stderr]}"),
138
- (rubocop_ok ? nil : "rubocop: #{rubocop[:parsed] || rubocop[:stderr]}")
139
- ].compact.join('; ')
169
+ errors = build_test_errors(rspec: rspec, rubocop: rubocop, rspec_ok: rspec_ok, rubocop_ok: rubocop_ok)
140
170
 
141
171
  { success: rspec_ok && rubocop_ok, stage: :test,
142
172
  rspec: rspec[:parsed] || { raw: rspec[:stdout] },
143
173
  rubocop: rubocop[:parsed] || { raw: rubocop[:stdout] },
144
- error: errors.empty? ? nil : errors }
174
+ error: errors.empty? ? nil : errors,
175
+ proposal_id: pid,
176
+ path: path }
177
+ end
178
+
179
+ def build_test_errors(rspec:, rubocop:, rspec_ok:, rubocop_ok:)
180
+ [
181
+ (rspec_ok ? nil : "rspec: #{rspec[:parsed] || rspec[:stderr]}"),
182
+ (rubocop_ok ? nil : "rubocop: #{rubocop[:parsed] || rubocop[:stderr]}")
183
+ ].compact.join('; ')
145
184
  end
146
185
 
147
186
  # --- Validate Stage ---
148
187
  # Delegates to lex-codegen validators when loaded; stubs otherwise
149
188
  def validate_stage(proposal, base_path)
150
- return { success: true, stage: :validate, message: 'validation requires lex-codegen' } unless codegen_available?
189
+ unless codegen_available?
190
+ return { success: true, stage: :validate, message: 'validation requires lex-codegen',
191
+ proposal_id: proposal_id_for(proposal),
192
+ path: ext_path(proposal, base_path) }
193
+ end
151
194
 
152
195
  path = ext_path(proposal, base_path)
153
196
  structure = Legion::Extensions::Codegen::Runners::Validate.validate_structure(path: path)
@@ -155,13 +198,19 @@ module Legion
155
198
 
156
199
  valid = structure[:valid] && gemspec[:valid]
157
200
  { success: valid, stage: :validate, structure: structure, gemspec: gemspec,
158
- error: valid ? nil : "structure: #{structure[:missing]}, gemspec: #{gemspec[:issues]}" }
201
+ error: valid ? nil : "structure: #{structure[:missing]}, gemspec: #{gemspec[:issues]}",
202
+ proposal_id: proposal_id_for(proposal),
203
+ path: path }
159
204
  end
160
205
 
161
206
  # --- Register Stage ---
162
207
  # Delegates to lex-metacognition registry when loaded; stubs otherwise
163
208
  def register_stage(proposal)
164
- return { success: true, stage: :register, message: 'registration requires lex-metacognition registry' } unless registry_available?
209
+ unless registry_available?
210
+ return { success: true, stage: :register,
211
+ message: 'registration requires lex-metacognition registry',
212
+ proposal_id: proposal_id_for(proposal) }
213
+ end
165
214
 
166
215
  result = Legion::Extensions::Metacognition::Runners::Registry.register_extension(
167
216
  name: proposal.name,
@@ -170,7 +219,8 @@ module Legion
170
219
  description: proposal.description
171
220
  )
172
221
 
173
- { success: result[:success], stage: :register, error: result[:error] }
222
+ { success: result[:success], stage: :register, error: result[:error],
223
+ proposal_id: proposal_id_for(proposal) }
174
224
  end
175
225
 
176
226
  # --- LLM implementation helpers ---
@@ -205,7 +255,7 @@ module Legion
205
255
  def legacy_implement_file(file_path, proposal)
206
256
  stub_content = ::File.read(file_path)
207
257
 
208
- chat = Legion::LLM.chat(caller: { extension: 'lex-mind-growth', operation: 'build' }, intent: { capability: :reasoning })
258
+ chat = Legion::LLM.chat(caller: { extension: 'lex-mind-growth', operation: 'build' }, intent: { capability: :reasoning }) # rubocop:disable Legion/HelperMigration/DirectLlm
209
259
  chat.with_instructions(implementation_instructions)
210
260
  response = chat.ask(file_implementation_prompt(stub_content, proposal))
211
261
  code = extract_ruby_code(response.content)
@@ -7,7 +7,8 @@ module Legion
7
7
  module CompetitiveEvolver
8
8
  extend self
9
9
 
10
- COMPETITION_STATUSES = %i[pending active evaluating decided cancelled].freeze
10
+ COMPETITION_STATUSES = %i[pending active evaluating decided cancelled].freeze
11
+ ACTIVE_STATUSES = %i[pending active evaluating].freeze
11
12
  MIN_TRIAL_ITERATIONS = 10
12
13
 
13
14
  def create_competition(gap:, proposal_ids:, **)
@@ -108,7 +109,7 @@ module Legion
108
109
  end
109
110
 
110
111
  def active_competitions(**)
111
- comps = all_competitions.select { |c| %i[pending active evaluating].include?(c[:status]) }
112
+ comps = all_competitions.select { |c| ACTIVE_STATUSES.include?(c[:status]) }
112
113
  { success: true, competitions: comps.map { |c| { id: c[:id], gap: c[:gap], status: c[:status] } },
113
114
  count: comps.size }
114
115
  end
@@ -5,8 +5,8 @@ module Legion
5
5
  module MindGrowth
6
6
  module Runners
7
7
  module Composer
8
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
- Legion::Extensions::Helpers.const_defined?(:Lex)
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
10
10
 
11
11
  extend self
12
12
 
@@ -108,7 +108,7 @@ module Legion
108
108
  def suggest_with_llm(extensions)
109
109
  suggestions = heuristic_suggestions(extensions)
110
110
  { success: true, suggestions: suggestions, count: suggestions.size }
111
- rescue StandardError
111
+ rescue StandardError => _e
112
112
  { success: true, suggestions: [], count: 0 }
113
113
  end
114
114
  end