lex-mind-growth 0.1.0 → 0.1.5

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: 811a3676ca7c7fd4168c30606006adeca6020be69398bb94128130ab6da89c34
4
- data.tar.gz: c40ec71e46d799b65100fa378c8840ca55579147159bd39eee9a8b6858b6ab97
3
+ metadata.gz: 3749e6e3fd39af6abe591e437da33b0c159db29b6e3463bd96dfb46f17b65d47
4
+ data.tar.gz: c702f090f36a6279fcded74bc19fccfa16cf8149d549d2603d0d38ed8b706432
5
5
  SHA512:
6
- metadata.gz: 997b9b014e37450333f475b4dd2ab8c70cc4bf7cd355d5cbc73a9873aa73dae4b7394b925f61cd97b9a8102ca038e0b6902bf32bee1a86b119b93f745380d8a7
7
- data.tar.gz: 4765d190fee92ce1c98e20a40955fa85521adc98dfe2b4013817fd4a01b7b4e18348323e665cafc605967196c367180f157cf28894ca70b089a955fb2cb470ef
6
+ metadata.gz: 5a70ad32a8ca26d122eae4d6e176d3d680f8943c08fa9c74379e3e03bbc99f1f7663b1b0abf6ecb5ef3d06f997107fd2c3d04d48ab32306f458a78419f92e39d
7
+ data.tar.gz: 9861dfcd32bad6bcb08b398be0a1a1fc3e3f564e2cb8d17a58cb30cdd43218d1ce1600c7d2f0af559f38c0ce5868f639e14fb59f8dc00fd6ac13b551770cb197
data/Gemfile CHANGED
@@ -12,8 +12,7 @@ group :test do
12
12
  gem 'simplecov'
13
13
  end
14
14
 
15
- gem 'legion-llm', path: '../../legion-llm', require: false
16
- gem 'lex-codegen', path: '../../extensions-core/lex-codegen', require: false
17
- gem 'lex-exec', path: '../../extensions-core/lex-exec', require: false
18
-
19
- gem 'legion-gaia', path: '../../legion-gaia'
15
+ gem 'legion-gaia', path: '../../legion-gaia' if File.directory?(File.expand_path('../../legion-gaia', __dir__))
16
+ gem 'legion-llm', path: '../../legion-llm', require: false if File.directory?(File.expand_path('../../legion-llm', __dir__))
17
+ gem 'lex-codegen', path: '../../extensions-core/lex-codegen', require: false if File.directory?(File.expand_path('../../extensions-core/lex-codegen', __dir__))
18
+ gem 'lex-exec', path: '../../extensions-core/lex-exec', require: false if File.directory?(File.expand_path('../../extensions-core/lex-exec', __dir__))
@@ -25,5 +25,13 @@ Gem::Specification.new do |spec|
25
25
  Dir.glob('{lib,spec}/**/*') + %w[lex-mind-growth.gemspec Gemfile LICENSE]
26
26
  end
27
27
  spec.require_paths = ['lib']
28
- spec.add_development_dependency 'legion-gaia'
28
+ spec.add_dependency 'legion-cache', '>= 1.3.11'
29
+ spec.add_dependency 'legion-crypt', '>= 1.4.9'
30
+ spec.add_dependency 'legion-data', '>= 1.4.17'
31
+ spec.add_dependency 'legion-json', '>= 1.2.1'
32
+ spec.add_dependency 'legion-logging', '>= 1.3.2'
33
+ spec.add_dependency 'legion-settings', '>= 1.3.14'
34
+ spec.add_dependency 'legion-transport', '>= 1.3.9'
35
+
36
+ spec.add_development_dependency 'legion-gaia', '>= 0.9.9'
29
37
  end
@@ -21,6 +21,12 @@ module Legion
21
21
  def advance!(result)
22
22
  return if complete? || failed?
23
23
 
24
+ if timed_out?
25
+ @errors << { stage: @stage, error: 'build timeout exceeded', at: Time.now.utc }
26
+ @stage = :failed
27
+ return
28
+ end
29
+
24
30
  if result[:success]
25
31
  @artifacts[@stage] = result
26
32
  next_idx = STAGES.index(@stage) + 1
@@ -40,6 +46,10 @@ module Legion
40
46
  @stage == :failed
41
47
  end
42
48
 
49
+ def timed_out?
50
+ duration_ms >= Helpers::Constants::BUILD_TIMEOUT_MS
51
+ end
52
+
43
53
  def to_h
44
54
  {
45
55
  proposal_id: @proposal.id,
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MindGrowth
6
+ module Helpers
7
+ module PhaseAllocator
8
+ # Maps cognitive categories to GAIA tick phases
9
+ CATEGORY_PHASE_MAP = {
10
+ perception: :sensory_processing,
11
+ attention: :sensory_processing,
12
+ emotion: :emotional_evaluation,
13
+ affect: :emotional_evaluation,
14
+ memory: :memory_retrieval,
15
+ knowledge: :knowledge_retrieval,
16
+ identity: :identity_entropy_check,
17
+ cognition: :working_memory_integration,
18
+ reasoning: :working_memory_integration,
19
+ safety: :procedural_check,
20
+ defense: :procedural_check,
21
+ prediction: :prediction_engine,
22
+ inference: :prediction_engine,
23
+ communication: :mesh_interface,
24
+ social: :mesh_interface,
25
+ coordination: :mesh_interface,
26
+ motivation: :action_selection,
27
+ executive: :action_selection,
28
+ learning: :memory_consolidation,
29
+ consolidation: :memory_consolidation,
30
+ introspection: :post_tick_reflection,
31
+ self: :post_tick_reflection,
32
+ reflection: :post_tick_reflection
33
+ }.freeze
34
+
35
+ # Dream cycle phase mappings
36
+ DREAM_PHASE_MAP = {
37
+ memory: :memory_audit,
38
+ association: :association_walk,
39
+ conflict: :contradiction_resolution,
40
+ curiosity: :agenda_formation,
41
+ consolidation: :consolidation_commit,
42
+ knowledge: :knowledge_promotion,
43
+ reflection: :dream_reflection,
44
+ narrative: :dream_narration
45
+ }.freeze
46
+
47
+ # Maps method name substrings to inferred GAIA phases for unknown categories
48
+ METHOD_INFERENCE_MAP = [
49
+ [%w[filter sense detect], :sensory_processing],
50
+ [%w[predict forecast estimate], :prediction_engine],
51
+ [%w[reflect evaluate assess], :post_tick_reflection],
52
+ [%w[store retrieve recall], :memory_retrieval],
53
+ [%w[decide select choose], :action_selection]
54
+ ].freeze
55
+
56
+ module_function
57
+
58
+ def allocate_phase(category:, runner_methods: [])
59
+ category_sym = category.to_s.downcase.to_sym
60
+
61
+ # Check active phases first
62
+ phase = CATEGORY_PHASE_MAP[category_sym]
63
+ return { phase: phase, cycle: :active, confidence: :high } if phase
64
+
65
+ # Try to infer from runner method names
66
+ inferred = infer_from_methods(runner_methods)
67
+ return inferred if inferred
68
+
69
+ # Default to working_memory_integration (safest catch-all)
70
+ { phase: :working_memory_integration, cycle: :active, confidence: :low }
71
+ end
72
+
73
+ def allocate_dream_phase(category:)
74
+ category_sym = category.to_s.downcase.to_sym
75
+ phase = DREAM_PHASE_MAP[category_sym]
76
+ return { phase: phase, cycle: :dream, confidence: :high } if phase
77
+
78
+ nil
79
+ end
80
+
81
+ def infer_from_methods(methods)
82
+ method_names = methods.map(&:to_s)
83
+ match = METHOD_INFERENCE_MAP.find do |keywords, _phase|
84
+ method_names.any? { |m| keywords.any? { |kw| m.include?(kw) } }
85
+ end
86
+ return nil unless match
87
+
88
+ { phase: match[1], cycle: :active, confidence: :medium }
89
+ end
90
+
91
+ def valid_phase?(phase)
92
+ CATEGORY_PHASE_MAP.values.include?(phase) || DREAM_PHASE_MAP.values.include?(phase)
93
+ end
94
+
95
+ def phases_for_category(category)
96
+ category_sym = category.to_s.downcase.to_sym
97
+ results = []
98
+ results << CATEGORY_PHASE_MAP[category_sym] if CATEGORY_PHASE_MAP.key?(category_sym)
99
+ results << DREAM_PHASE_MAP[category_sym] if DREAM_PHASE_MAP.key?(category_sym)
100
+ results.compact
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -5,6 +5,9 @@ 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)
10
+
8
11
  extend self
9
12
 
10
13
  def build_extension(proposal_id:, base_path: nil, **)
@@ -22,7 +25,7 @@ module Legion
22
25
  run_stage(pipeline, :register, -> { register_stage(proposal) }) unless pipeline.failed?
23
26
 
24
27
  proposal.transition!(pipeline.complete? ? :passing : :build_failed)
25
- Legion::Logging.info "[mind_growth:builder] #{proposal.name}: #{pipeline.stage}" if defined?(Legion::Logging)
28
+ log.info "[mind_growth:builder] #{proposal.name}: #{pipeline.stage}"
26
29
  { success: pipeline.complete?, pipeline: pipeline.to_h, proposal: proposal.to_h }
27
30
  rescue ArgumentError => e
28
31
  { success: false, error: e.message }
@@ -178,7 +181,7 @@ module Legion
178
181
  def implement_file(file_path, proposal)
179
182
  stub_content = ::File.read(file_path)
180
183
 
181
- chat = Legion::LLM.chat
184
+ chat = Legion::LLM.chat(caller: { extension: 'lex-mind-growth', operation: 'build' }, intent: { capability: :reasoning })
182
185
  chat.with_instructions(implementation_instructions)
183
186
  response = chat.ask(file_implementation_prompt(stub_content, proposal))
184
187
  code = extract_ruby_code(response.content)
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MindGrowth
6
+ module Runners
7
+ module IntegrationTester
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ extend self
12
+
13
+ TICK_BUDGET_MS = 5000
14
+
15
+ def test_extension_in_tick(ext_module:, runner_module:, fn:, phase:, test_args: {}, **) # rubocop:disable Naming/MethodParameterName
16
+ return { success: false, reason: :gaia_not_available } unless gaia_available?
17
+
18
+ runner_class = resolve_runner_class(ext_module, runner_module)
19
+ return { success: false, reason: :runner_not_found } unless runner_class
20
+
21
+ # Test 1: Method exists and is callable
22
+ method_check = test_method_callable(runner_class, fn)
23
+ return method_check unless method_check[:success]
24
+
25
+ # Test 2: Method returns valid response hash
26
+ response_check = test_valid_response(runner_class, fn, test_args)
27
+ return response_check unless response_check[:success]
28
+
29
+ # Test 3: Method completes within budget
30
+ perf_check = test_performance(runner_class, fn, test_args)
31
+
32
+ {
33
+ success: true,
34
+ method_callable: method_check[:success],
35
+ valid_response: response_check[:success],
36
+ performance: perf_check,
37
+ phase: phase
38
+ }
39
+ rescue StandardError => e
40
+ { success: false, reason: :exception, error: e.message }
41
+ end
42
+
43
+ def benchmark_tick(with_extension: nil, iterations: 5, **)
44
+ return { success: false, reason: :gaia_not_available } unless gaia_available?
45
+
46
+ timings = iterations.times.map do
47
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
48
+ Legion::Gaia.heartbeat if Legion::Gaia.respond_to?(:heartbeat)
49
+ finish = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
50
+ ((finish - start) * 1000).round(2)
51
+ end
52
+
53
+ {
54
+ success: true,
55
+ with_extension: with_extension,
56
+ iterations: iterations,
57
+ avg_ms: (timings.sum / timings.size).round(2),
58
+ max_ms: timings.max,
59
+ min_ms: timings.min,
60
+ within_budget: timings.max <= TICK_BUDGET_MS
61
+ }
62
+ rescue StandardError => e
63
+ { success: false, reason: :benchmark_failed, error: e.message }
64
+ end
65
+
66
+ private
67
+
68
+ def gaia_available?
69
+ defined?(Legion::Gaia)
70
+ end
71
+
72
+ def resolve_runner_class(ext_module, runner_module)
73
+ return nil unless defined?(Legion::Gaia::PhaseWiring)
74
+
75
+ Legion::Gaia::PhaseWiring.resolve_runner_class(ext_module.to_sym, runner_module.to_sym)
76
+ end
77
+
78
+ def test_method_callable(runner_class, fn) # rubocop:disable Naming/MethodParameterName
79
+ if runner_class.method_defined?(fn) || runner_class.respond_to?(fn)
80
+ { success: true, method: fn }
81
+ else
82
+ { success: false, reason: :method_not_defined, method: fn }
83
+ end
84
+ end
85
+
86
+ def test_valid_response(runner_class, fn, args) # rubocop:disable Naming/MethodParameterName
87
+ host = Object.new.extend(runner_class)
88
+ result = host.send(fn, **args)
89
+
90
+ if result.is_a?(Hash)
91
+ { success: true, response_type: :hash, keys: result.keys }
92
+ elsif result.nil?
93
+ { success: true, response_type: :nil }
94
+ else
95
+ { success: true, response_type: result.class.name }
96
+ end
97
+ rescue StandardError => e
98
+ { success: false, reason: :invocation_error, error: e.message }
99
+ end
100
+
101
+ def test_performance(runner_class, fn, args) # rubocop:disable Naming/MethodParameterName
102
+ host = Object.new.extend(runner_class)
103
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
104
+ host.send(fn, **args)
105
+ finish = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
106
+ duration_ms = ((finish - start) * 1000).round(2)
107
+
108
+ { duration_ms: duration_ms, within_budget: duration_ms <= TICK_BUDGET_MS }
109
+ rescue StandardError => e
110
+ { duration_ms: nil, error: e.message, within_budget: false }
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -5,6 +5,9 @@ module Legion
5
5
  module MindGrowth
6
6
  module Runners
7
7
  module Orchestrator
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
8
11
  extend self
9
12
 
10
13
  def run_growth_cycle(existing_extensions: nil, base_path: nil, max_proposals: 3, force: false, **)
@@ -165,11 +168,9 @@ module Legion
165
168
  end
166
169
 
167
170
  def log_cycle_summary(trace)
168
- return unless defined?(Legion::Logging)
169
-
170
171
  build_step = trace[:steps].find { |s| s[:step] == :build }
171
172
  succeeded = build_step ? build_step[:succeeded] : 0
172
- Legion::Logging.info "[mind_growth:orchestrator] cycle complete: #{succeeded} extensions built"
173
+ log.info "[mind_growth:orchestrator] cycle complete: #{succeeded} extensions built"
173
174
  end
174
175
  end
175
176
  end
@@ -7,13 +7,16 @@ module Legion
7
7
  module MindGrowth
8
8
  module Runners
9
9
  module Proposer
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex)
12
+
10
13
  extend self
11
14
 
12
15
  def analyze_gaps(existing_extensions: nil, **)
13
16
  extensions = existing_extensions || current_extensions
14
17
  analysis = Helpers::CognitiveModels.gap_analysis(extensions)
15
18
  recommendations = Helpers::CognitiveModels.recommend_from_gaps(analysis)
16
- Legion::Logging.debug "[mind_growth:proposer] gap analysis: #{recommendations.size} recommendations" if defined?(Legion::Logging)
19
+ log.debug "[mind_growth:proposer] gap analysis: #{recommendations.size} recommendations"
17
20
  { success: true, models: analysis, recommendations: recommendations.first(10) }
18
21
  rescue ArgumentError => e
19
22
  { success: false, error: e.message }
@@ -27,7 +30,7 @@ module Legion
27
30
 
28
31
  redundancy = check_redundancy(gem_name, desc)
29
32
  if redundancy[:redundant]
30
- Legion::Logging.info "[mind_growth:proposer] rejected redundant: #{gem_name} (#{redundancy[:score]})" if defined?(Legion::Logging)
33
+ log.info "[mind_growth:proposer] rejected redundant: #{gem_name} (#{redundancy[:score]})"
31
34
  return { success: false, error: :redundant, similar_to: redundancy[:similar_to],
32
35
  score: redundancy[:score] }
33
36
  end
@@ -46,7 +49,7 @@ module Legion
46
49
  origin: :manual
47
50
  )
48
51
  proposal_store.store(proposal)
49
- Legion::Logging.info "[mind_growth:proposer] proposed: #{proposal.name} (#{cat})" if defined?(Legion::Logging)
52
+ log.info "[mind_growth:proposer] proposed: #{proposal.name} (#{cat})"
50
53
  { success: true, proposal: proposal.to_h }
51
54
  rescue ArgumentError => e
52
55
  { success: false, error: e.message }
@@ -58,7 +61,7 @@ module Legion
58
61
 
59
62
  eval_scores = scores || score_with_llm(proposal) || default_scores
60
63
  proposal.evaluate!(eval_scores)
61
- Legion::Logging.info "[mind_growth:proposer] evaluated #{proposal.name}: #{proposal.status}" if defined?(Legion::Logging)
64
+ log.info "[mind_growth:proposer] evaluated #{proposal.name}: #{proposal.status}"
62
65
  { success: true, proposal: proposal.to_h, approved: proposal.status == :approved,
63
66
  auto_approved: proposal.auto_approvable? }
64
67
  rescue ArgumentError => e
@@ -114,10 +117,11 @@ module Legion
114
117
  def enrich_proposal(name, category, description)
115
118
  return {} unless llm_available?
116
119
 
117
- response = Legion::LLM.chat.ask(enrichment_prompt(name, category, description))
120
+ response = Legion::LLM.chat(caller: { extension: 'lex-mind-growth', operation: 'propose',
121
+ phase: 'capability' }).ask(enrichment_prompt(name, category, description))
118
122
  parse_enrichment(response.content)
119
123
  rescue StandardError => e
120
- Legion::Logging.debug "[mind_growth:proposer] LLM enrichment failed: #{e.message}" if defined?(Legion::Logging)
124
+ log.debug "[mind_growth:proposer] LLM enrichment failed: #{e.message}"
121
125
  {}
122
126
  end
123
127
 
@@ -158,10 +162,10 @@ module Legion
158
162
  def score_with_llm(proposal)
159
163
  return nil unless llm_available?
160
164
 
161
- response = Legion::LLM.chat.ask(scoring_prompt(proposal))
165
+ response = Legion::LLM.chat(caller: { extension: 'lex-mind-growth', operation: 'propose', phase: 'score' }).ask(scoring_prompt(proposal))
162
166
  parse_scores(response.content)
163
167
  rescue StandardError => e
164
- Legion::Logging.debug "[mind_growth:proposer] LLM scoring failed: #{e.message}" if defined?(Legion::Logging)
168
+ log.debug "[mind_growth:proposer] LLM scoring failed: #{e.message}"
165
169
  nil
166
170
  end
167
171
 
@@ -221,10 +225,11 @@ module Legion
221
225
  return nil unless llm_available?
222
226
 
223
227
  candidates = existing.last(20).map { |p| { name: p.name, description: p.description } }
224
- response = Legion::LLM.chat.ask(redundancy_prompt(name, description, candidates))
228
+ response = Legion::LLM.chat(caller: { extension: 'lex-mind-growth', operation: 'propose',
229
+ phase: 'validate' }).ask(redundancy_prompt(name, description, candidates))
225
230
  parse_redundancy(response.content)
226
231
  rescue StandardError => e
227
- Legion::Logging.debug "[mind_growth:proposer] LLM redundancy check failed: #{e.message}" if defined?(Legion::Logging)
232
+ log.debug "[mind_growth:proposer] LLM redundancy check failed: #{e.message}"
228
233
  nil
229
234
  end
230
235
 
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MindGrowth
6
+ module Runners
7
+ module Wirer
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ extend self
12
+
13
+ def analyze_fit(extension_name:, category:, runner_module: nil, runner_methods: [], **) # rubocop:disable Lint/UnusedMethodArgument
14
+ allocation = Helpers::PhaseAllocator.allocate_phase(
15
+ category: category,
16
+ runner_methods: runner_methods
17
+ )
18
+
19
+ dream_allocation = Helpers::PhaseAllocator.allocate_dream_phase(category: category)
20
+
21
+ {
22
+ success: true,
23
+ extension: extension_name,
24
+ active_phase: allocation,
25
+ dream_phase: dream_allocation,
26
+ recommendation: build_recommendation(allocation, dream_allocation)
27
+ }
28
+ end
29
+
30
+ def wire_extension(extension_name:, ext_module:, runner_module:, fn:, phase:, **) # rubocop:disable Naming/MethodParameterName
31
+ return { success: false, reason: :gaia_not_available } unless gaia_available?
32
+ return { success: false, reason: :invalid_phase } unless Helpers::PhaseAllocator.valid_phase?(phase)
33
+
34
+ # Verify the runner class exists and has the method
35
+ runner_class = resolve_runner(ext_module: ext_module, runner_module: runner_module)
36
+ return { success: false, reason: :runner_not_found } unless runner_class
37
+
38
+ return { success: false, reason: :method_not_found, method: fn } unless runner_class.method_defined?(fn) || runner_class.respond_to?(fn)
39
+
40
+ # Record the wiring in our registry
41
+ wiring_registry[extension_name] = {
42
+ ext_module: ext_module,
43
+ runner_module: runner_module,
44
+ fn: fn,
45
+ phase: phase,
46
+ wired_at: Time.now,
47
+ enabled: true
48
+ }
49
+
50
+ # Trigger GAIA rediscovery to pick up the new wiring
51
+ rediscover_result = trigger_rediscovery
52
+
53
+ {
54
+ success: true,
55
+ extension: extension_name,
56
+ phase: phase,
57
+ fn: fn,
58
+ rediscovery: rediscover_result
59
+ }
60
+ end
61
+
62
+ def unwire_extension(extension_name:, **)
63
+ entry = wiring_registry.delete(extension_name)
64
+ return { success: false, reason: :not_wired } unless entry
65
+
66
+ trigger_rediscovery
67
+
68
+ { success: true, extension: extension_name, unwired_phase: entry[:phase] }
69
+ end
70
+
71
+ def disable_extension(extension_name:, **)
72
+ entry = wiring_registry[extension_name]
73
+ return { success: false, reason: :not_wired } unless entry
74
+
75
+ entry[:enabled] = false
76
+ { success: true, extension: extension_name, status: :disabled }
77
+ end
78
+
79
+ def enable_extension(extension_name:, **)
80
+ entry = wiring_registry[extension_name]
81
+ return { success: false, reason: :not_wired } unless entry
82
+
83
+ entry[:enabled] = true
84
+ trigger_rediscovery
85
+ { success: true, extension: extension_name, status: :enabled }
86
+ end
87
+
88
+ def wiring_status(**)
89
+ {
90
+ success: true,
91
+ wired_count: wiring_registry.size,
92
+ enabled_count: wiring_registry.count { |_, v| v[:enabled] },
93
+ extensions: wiring_registry.transform_values { |v| v.slice(:phase, :fn, :enabled, :wired_at) }
94
+ }
95
+ end
96
+
97
+ def rewire_all(**)
98
+ return { success: false, reason: :gaia_not_available } unless gaia_available?
99
+
100
+ result = trigger_rediscovery
101
+ { success: true, rediscovery: result }
102
+ end
103
+
104
+ private
105
+
106
+ def gaia_available?
107
+ defined?(Legion::Gaia) && Legion::Gaia.respond_to?(:registry)
108
+ end
109
+
110
+ def resolve_runner(ext_module:, runner_module:)
111
+ return nil unless defined?(Legion::Gaia::PhaseWiring)
112
+
113
+ Legion::Gaia::PhaseWiring.resolve_runner_class(ext_module.to_sym, runner_module.to_sym)
114
+ end
115
+
116
+ def trigger_rediscovery
117
+ return { rediscovered: false, reason: :gaia_not_available } unless gaia_available?
118
+
119
+ Legion::Gaia.registry.rediscover
120
+ rescue StandardError => e
121
+ { rediscovered: false, error: e.message }
122
+ end
123
+
124
+ def wiring_registry
125
+ @wiring_registry ||= {}
126
+ end
127
+
128
+ def build_recommendation(active_alloc, dream_alloc)
129
+ parts = []
130
+ parts << case active_alloc[:confidence]
131
+ when :high
132
+ "Wire to #{active_alloc[:phase]} (high confidence)"
133
+ when :medium
134
+ "Suggest #{active_alloc[:phase]} (medium confidence, verify manually)"
135
+ else
136
+ "Default to #{active_alloc[:phase]} (low confidence, manual review recommended)"
137
+ end
138
+ parts << "Also wire dream phase: #{dream_alloc[:phase]}" if dream_alloc
139
+ parts.join('. ')
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module MindGrowth
6
- VERSION = '0.1.0'
6
+ VERSION = '0.1.5'
7
7
  end
8
8
  end
9
9
  end
@@ -9,11 +9,14 @@ require 'legion/extensions/mind_growth/helpers/proposal_store'
9
9
  require 'legion/extensions/mind_growth/helpers/cognitive_models'
10
10
  require 'legion/extensions/mind_growth/helpers/build_pipeline'
11
11
  require 'legion/extensions/mind_growth/helpers/fitness_evaluator'
12
+ require 'legion/extensions/mind_growth/helpers/phase_allocator'
12
13
  require 'legion/extensions/mind_growth/runners/proposer'
13
14
  require 'legion/extensions/mind_growth/runners/analyzer'
14
15
  require 'legion/extensions/mind_growth/runners/builder'
15
16
  require 'legion/extensions/mind_growth/runners/validator'
16
17
  require 'legion/extensions/mind_growth/runners/orchestrator'
18
+ require 'legion/extensions/mind_growth/runners/wirer'
19
+ require 'legion/extensions/mind_growth/runners/integration_tester'
17
20
  require 'legion/extensions/mind_growth/client'
18
21
 
19
22
  module Legion
@@ -118,6 +118,35 @@ RSpec.describe Legion::Extensions::MindGrowth::Helpers::BuildPipeline do
118
118
  end
119
119
  end
120
120
 
121
+ describe '#timed_out?' do
122
+ it 'returns false when within time budget' do
123
+ expect(pipeline.timed_out?).to be false
124
+ end
125
+
126
+ it 'returns true when duration exceeds BUILD_TIMEOUT_MS' do
127
+ pipeline.instance_variable_set(:@started_at, Time.now.utc - 700)
128
+ expect(pipeline.timed_out?).to be true
129
+ end
130
+
131
+ it 'transitions to failed on advance! when timed out' do
132
+ pipeline.instance_variable_set(:@started_at, Time.now.utc - 700)
133
+ pipeline.advance!({ success: true })
134
+ expect(pipeline.stage).to eq(:failed)
135
+ end
136
+
137
+ it 'records timeout error message' do
138
+ pipeline.instance_variable_set(:@started_at, Time.now.utc - 700)
139
+ pipeline.advance!({ success: true })
140
+ expect(pipeline.errors.last[:error]).to eq('build timeout exceeded')
141
+ end
142
+
143
+ it 'records the stage where timeout occurred' do
144
+ pipeline.instance_variable_set(:@started_at, Time.now.utc - 700)
145
+ pipeline.advance!({ success: true })
146
+ expect(pipeline.errors.last[:stage]).to eq(:scaffold)
147
+ end
148
+ end
149
+
121
150
  describe '#complete?' do
122
151
  it 'returns false initially' do
123
152
  expect(pipeline.complete?).to be false