lex-mind-growth 0.1.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 (33) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +19 -0
  3. data/LICENSE +21 -0
  4. data/lex-mind-growth.gemspec +29 -0
  5. data/lib/legion/extensions/mind_growth/actors/growth_cycle.rb +55 -0
  6. data/lib/legion/extensions/mind_growth/client.rb +36 -0
  7. data/lib/legion/extensions/mind_growth/helpers/build_pipeline.rb +63 -0
  8. data/lib/legion/extensions/mind_growth/helpers/cognitive_models.rb +60 -0
  9. data/lib/legion/extensions/mind_growth/helpers/concept_proposal.rb +69 -0
  10. data/lib/legion/extensions/mind_growth/helpers/constants.rb +55 -0
  11. data/lib/legion/extensions/mind_growth/helpers/fitness_evaluator.rb +58 -0
  12. data/lib/legion/extensions/mind_growth/helpers/proposal_store.rb +71 -0
  13. data/lib/legion/extensions/mind_growth/runners/analyzer.rb +52 -0
  14. data/lib/legion/extensions/mind_growth/runners/builder.rb +254 -0
  15. data/lib/legion/extensions/mind_growth/runners/orchestrator.rb +178 -0
  16. data/lib/legion/extensions/mind_growth/runners/proposer.rb +269 -0
  17. data/lib/legion/extensions/mind_growth/runners/validator.rb +57 -0
  18. data/lib/legion/extensions/mind_growth/version.rb +9 -0
  19. data/lib/legion/extensions/mind_growth.rb +25 -0
  20. data/spec/legion/extensions/mind_growth/actors/growth_cycle_spec.rb +81 -0
  21. data/spec/legion/extensions/mind_growth/client_spec.rb +111 -0
  22. data/spec/legion/extensions/mind_growth/helpers/build_pipeline_spec.rb +190 -0
  23. data/spec/legion/extensions/mind_growth/helpers/cognitive_models_spec.rb +106 -0
  24. data/spec/legion/extensions/mind_growth/helpers/concept_proposal_spec.rb +209 -0
  25. data/spec/legion/extensions/mind_growth/helpers/fitness_evaluator_spec.rb +123 -0
  26. data/spec/legion/extensions/mind_growth/helpers/proposal_store_spec.rb +203 -0
  27. data/spec/legion/extensions/mind_growth/runners/analyzer_spec.rb +108 -0
  28. data/spec/legion/extensions/mind_growth/runners/builder_spec.rb +347 -0
  29. data/spec/legion/extensions/mind_growth/runners/orchestrator_spec.rb +185 -0
  30. data/spec/legion/extensions/mind_growth/runners/proposer_spec.rb +493 -0
  31. data/spec/legion/extensions/mind_growth/runners/validator_spec.rb +120 -0
  32. data/spec/spec_helper.rb +22 -0
  33. metadata +91 -0
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MindGrowth
6
+ module Runners
7
+ module Builder
8
+ extend self
9
+
10
+ def build_extension(proposal_id:, base_path: nil, **)
11
+ proposal = find_proposal(proposal_id)
12
+ return { success: false, error: :not_found } unless proposal
13
+
14
+ pipeline = Helpers::BuildPipeline.new(proposal)
15
+ proposal.transition!(:building)
16
+ base_path ||= ::Dir.pwd
17
+
18
+ run_stage(pipeline, :scaffold, -> { scaffold_stage(proposal, base_path) })
19
+ run_stage(pipeline, :implement, -> { implement_stage(proposal, base_path) }) unless pipeline.failed?
20
+ run_stage(pipeline, :test, -> { test_stage(proposal, base_path) }) unless pipeline.failed?
21
+ run_stage(pipeline, :validate, -> { validate_stage(proposal, base_path) }) unless pipeline.failed?
22
+ run_stage(pipeline, :register, -> { register_stage(proposal) }) unless pipeline.failed?
23
+
24
+ proposal.transition!(pipeline.complete? ? :passing : :build_failed)
25
+ Legion::Logging.info "[mind_growth:builder] #{proposal.name}: #{pipeline.stage}" if defined?(Legion::Logging)
26
+ { success: pipeline.complete?, pipeline: pipeline.to_h, proposal: proposal.to_h }
27
+ rescue ArgumentError => e
28
+ { success: false, error: e.message }
29
+ end
30
+
31
+ def build_status(proposal_id:, **)
32
+ proposal = find_proposal(proposal_id)
33
+ return { success: false, error: :not_found } unless proposal
34
+
35
+ { success: true, name: proposal.name, status: proposal.status }
36
+ end
37
+
38
+ private
39
+
40
+ def find_proposal(proposal_id)
41
+ return nil unless defined?(Runners::Proposer) && Runners::Proposer.respond_to?(:get_proposal_object)
42
+
43
+ Runners::Proposer.get_proposal_object(proposal_id)
44
+ end
45
+
46
+ def run_stage(pipeline, stage, callable)
47
+ return if pipeline.stage != stage
48
+
49
+ result = callable.call
50
+ pipeline.advance!(result)
51
+ end
52
+
53
+ def ext_path(proposal, base_path)
54
+ name = strip_lex_prefix(proposal.name)
55
+ ::File.join(base_path, "lex-#{name}")
56
+ end
57
+
58
+ def strip_lex_prefix(name)
59
+ name.to_s.sub(/\Alex-/, '')
60
+ end
61
+
62
+ # --- Scaffold Stage ---
63
+ # Delegates to lex-codegen when loaded; stubs otherwise
64
+ def scaffold_stage(proposal, base_path)
65
+ return { success: true, stage: :scaffold, files: 0, message: 'scaffold requires lex-codegen' } unless codegen_available?
66
+
67
+ name = strip_lex_prefix(proposal.name)
68
+ result = Legion::Extensions::Codegen::Runners::Generate.scaffold_extension(
69
+ name: name,
70
+ module_name: proposal.module_name,
71
+ description: proposal.description || "#{proposal.name} cognitive extension",
72
+ category: proposal.category,
73
+ helpers: proposal.helpers || [],
74
+ runner_methods: proposal.runner_methods || [],
75
+ base_path: base_path
76
+ )
77
+
78
+ { success: result[:success], stage: :scaffold, files: result[:files_created] || 0,
79
+ path: result[:path], error: result[:error] }
80
+ end
81
+
82
+ # --- Implement Stage ---
83
+ # Delegates to legion-llm when loaded and started; stubs otherwise
84
+ def implement_stage(proposal, base_path)
85
+ return { success: true, stage: :implement, message: 'implementation requires legion-llm' } unless llm_available?
86
+
87
+ path = ext_path(proposal, base_path)
88
+ target_files = implementation_targets(path)
89
+
90
+ if target_files.empty?
91
+ return { success: true, stage: :implement, files_implemented: 0,
92
+ message: 'no implementation targets found' }
93
+ end
94
+
95
+ files_implemented = 0
96
+ errors = []
97
+
98
+ target_files.each do |file_path|
99
+ result = implement_file(file_path, proposal)
100
+ if result[:success]
101
+ files_implemented += 1
102
+ else
103
+ errors << "#{::File.basename(file_path)}: #{result[:error]}"
104
+ end
105
+ end
106
+
107
+ success = errors.empty?
108
+ { success: success, stage: :implement, files_implemented: files_implemented,
109
+ total_files: target_files.size, error: success ? nil : errors.join('; ') }
110
+ end
111
+
112
+ # --- Test Stage ---
113
+ # Delegates to lex-exec bundler runners when loaded; stubs otherwise
114
+ def test_stage(proposal, base_path)
115
+ return { success: true, stage: :test, message: 'testing requires lex-exec' } unless exec_available?
116
+
117
+ path = ext_path(proposal, base_path)
118
+
119
+ install = Legion::Extensions::Exec::Runners::Bundler.install(path: path)
120
+ unless install[:success]
121
+ return { success: false, stage: :test, step: :install,
122
+ error: install[:stderr] || install[:error] }
123
+ end
124
+
125
+ rspec = Legion::Extensions::Exec::Runners::Bundler.exec_rspec(path: path)
126
+ rubocop = Legion::Extensions::Exec::Runners::Bundler.exec_rubocop(path: path)
127
+
128
+ rspec_ok = rspec[:success] && (rspec.dig(:parsed, :failures) || 0).zero?
129
+ rubocop_ok = rubocop[:success]
130
+
131
+ errors = [
132
+ (rspec_ok ? nil : "rspec: #{rspec[:parsed] || rspec[:stderr]}"),
133
+ (rubocop_ok ? nil : "rubocop: #{rubocop[:parsed] || rubocop[:stderr]}")
134
+ ].compact.join('; ')
135
+
136
+ { success: rspec_ok && rubocop_ok, stage: :test,
137
+ rspec: rspec[:parsed] || { raw: rspec[:stdout] },
138
+ rubocop: rubocop[:parsed] || { raw: rubocop[:stdout] },
139
+ error: errors.empty? ? nil : errors }
140
+ end
141
+
142
+ # --- Validate Stage ---
143
+ # Delegates to lex-codegen validators when loaded; stubs otherwise
144
+ def validate_stage(proposal, base_path)
145
+ return { success: true, stage: :validate, message: 'validation requires lex-codegen' } unless codegen_available?
146
+
147
+ path = ext_path(proposal, base_path)
148
+ structure = Legion::Extensions::Codegen::Runners::Validate.validate_structure(path: path)
149
+ gemspec = Legion::Extensions::Codegen::Runners::Validate.validate_gemspec(path: path)
150
+
151
+ valid = structure[:valid] && gemspec[:valid]
152
+ { success: valid, stage: :validate, structure: structure, gemspec: gemspec,
153
+ error: valid ? nil : "structure: #{structure[:missing]}, gemspec: #{gemspec[:issues]}" }
154
+ end
155
+
156
+ # --- Register Stage ---
157
+ # Delegates to lex-metacognition registry when loaded; stubs otherwise
158
+ def register_stage(proposal)
159
+ return { success: true, stage: :register, message: 'registration requires lex-metacognition registry' } unless registry_available?
160
+
161
+ result = Legion::Extensions::Metacognition::Runners::Registry.register_extension(
162
+ name: proposal.name,
163
+ module_name: proposal.module_name,
164
+ category: proposal.category.to_s,
165
+ description: proposal.description
166
+ )
167
+
168
+ { success: result[:success], stage: :register, error: result[:error] }
169
+ end
170
+
171
+ # --- LLM implementation helpers ---
172
+ def implementation_targets(path)
173
+ runners = ::Dir.glob(::File.join(path, 'lib/**/runners/*.rb'))
174
+ helpers = ::Dir.glob(::File.join(path, 'lib/**/helpers/*.rb'))
175
+ (runners + helpers).reject { |f| f.end_with?('version.rb', 'client.rb') }
176
+ end
177
+
178
+ def implement_file(file_path, proposal)
179
+ stub_content = ::File.read(file_path)
180
+
181
+ chat = Legion::LLM.chat
182
+ chat.with_instructions(implementation_instructions)
183
+ response = chat.ask(file_implementation_prompt(stub_content, proposal))
184
+ code = extract_ruby_code(response.content)
185
+
186
+ ::File.write(file_path, code)
187
+ { success: true, path: file_path }
188
+ rescue StandardError => e
189
+ { success: false, error: e.message }
190
+ end
191
+
192
+ def implementation_instructions
193
+ <<~INSTRUCTIONS
194
+ You are a Ruby code generator for LegionIO cognitive extensions.
195
+ You receive a stub Ruby file and a description of the extension's purpose.
196
+ Replace stub method bodies with real implementations.
197
+
198
+ Rules:
199
+ - Return ONLY the complete Ruby file content, no markdown fencing, no explanation
200
+ - Keep the exact module/class/method structure and signatures
201
+ - Keep `# frozen_string_literal: true` on line 1
202
+ - Runner methods must return `{ success: true/false, ... }` hashes
203
+ - Use in-memory state only (instance variables, no database, no external APIs)
204
+ - Helper classes may use initialize for state setup
205
+ - Follow Ruby style: 2-space indent, snake_case methods
206
+ - Do not add require statements
207
+ - Do not add comments unless the logic is non-obvious
208
+ INSTRUCTIONS
209
+ end
210
+
211
+ def file_implementation_prompt(stub_content, proposal)
212
+ parts = ['Implement this LegionIO extension file.']
213
+ parts << "Extension: #{proposal.name}"
214
+ parts << "Category: #{proposal.category}"
215
+ parts << "Description: #{proposal.description}"
216
+ parts << "Metaphor: #{proposal.metaphor}" if proposal.metaphor
217
+ parts << ''
218
+ parts << 'Current stub:'
219
+ parts << stub_content
220
+ parts.join("\n")
221
+ end
222
+
223
+ def extract_ruby_code(content)
224
+ code = if content.match?(/```ruby\s*\n/)
225
+ content.match(/```ruby\s*\n(.*?)```/m)&.captures&.first || content
226
+ elsif content.match?(/```\s*\n/)
227
+ content.match(/```\s*\n(.*?)```/m)&.captures&.first || content
228
+ else
229
+ content
230
+ end
231
+ "#{code.strip}\n"
232
+ end
233
+
234
+ # --- Dependency availability checks ---
235
+ def codegen_available?
236
+ defined?(Legion::Extensions::Codegen::Runners::Generate)
237
+ end
238
+
239
+ def exec_available?
240
+ defined?(Legion::Extensions::Exec::Runners::Bundler)
241
+ end
242
+
243
+ def registry_available?
244
+ defined?(Legion::Extensions::Metacognition::Runners::Registry)
245
+ end
246
+
247
+ def llm_available?
248
+ defined?(Legion::LLM) && Legion::LLM.respond_to?(:started?) && Legion::LLM.started?
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module MindGrowth
6
+ module Runners
7
+ module Orchestrator
8
+ extend self
9
+
10
+ def run_growth_cycle(existing_extensions: nil, base_path: nil, max_proposals: 3, force: false, **)
11
+ trace = { started_at: Time.now.utc, steps: [] }
12
+
13
+ # Step 1: Analyze gaps in the cognitive ecosystem
14
+ gaps = Runners::Analyzer.recommend_priorities(existing_extensions: existing_extensions)
15
+ trace[:steps] << { step: :analyze, result: gaps }
16
+ return failure(trace, 'gap analysis failed') unless gaps[:success]
17
+ return failure(trace, 'no priorities identified') if gaps[:priorities].empty?
18
+
19
+ # Step 2: Propose concepts for the top priorities
20
+ proposals = propose_from_priorities(gaps[:priorities], max_proposals)
21
+ trace[:steps] << { step: :propose, count: proposals.size, proposals: proposals.map { |p| p[:proposal][:id] } }
22
+ return failure(trace, 'no proposals created') if proposals.empty?
23
+
24
+ # Step 3: Evaluate each proposal
25
+ classify_and_trace_evaluations(evaluate_proposals(proposals), trace)
26
+
27
+ # Step 4: Build — auto-approved build immediately; regular approved only when forced
28
+ result = execute_build_step(trace, base_path, force)
29
+ return result if result
30
+
31
+ trace[:completed_at] = Time.now.utc
32
+ trace[:duration_ms] = ((trace[:completed_at] - trace[:started_at]) * 1000).round
33
+
34
+ log_cycle_summary(trace)
35
+ { success: true, trace: trace }
36
+ rescue StandardError => e
37
+ { success: false, error: e.message, trace: trace }
38
+ end
39
+
40
+ REQUIREMENT_CATEGORIES = {
41
+ attention: :perception,
42
+ global_workspace: :cognition,
43
+ broadcasting: :communication,
44
+ working_memory: :memory,
45
+ consciousness: :introspection,
46
+ prediction: :cognition,
47
+ free_energy: :cognition,
48
+ predictive_coding: :cognition,
49
+ belief_revision: :cognition,
50
+ active_inference: :cognition,
51
+ error_monitoring: :safety,
52
+ intuition: :cognition,
53
+ dual_process: :cognition,
54
+ inhibition: :safety,
55
+ executive_function: :cognition,
56
+ cognitive_control: :cognition,
57
+ emotion: :introspection,
58
+ somatic_marker: :introspection,
59
+ interoception: :perception,
60
+ appraisal: :introspection,
61
+ embodied_simulation: :perception,
62
+ episodic_buffer: :memory,
63
+ cognitive_load: :introspection
64
+ }.freeze
65
+
66
+ def growth_status(**)
67
+ stats = Runners::Proposer.proposal_stats
68
+ profile = Runners::Analyzer.cognitive_profile
69
+
70
+ { success: true,
71
+ proposals: stats[:stats],
72
+ coverage: profile[:overall_coverage],
73
+ model_coverage: profile[:model_coverage]&.map { |m| { model: m[:model], coverage: m[:coverage] } } }
74
+ end
75
+
76
+ private
77
+
78
+ def propose_from_priorities(priorities, max)
79
+ priorities.first(max).filter_map do |priority_name|
80
+ name = "lex-#{priority_name.to_s.tr('_', '-')}"
81
+ result = Runners::Proposer.propose_concept(
82
+ name: name,
83
+ category: category_for_requirement(priority_name),
84
+ description: "Cognitive extension for #{priority_name} (recommended by gap analysis)"
85
+ )
86
+ result if result[:success]
87
+ end
88
+ end
89
+
90
+ def category_for_requirement(requirement)
91
+ REQUIREMENT_CATEGORIES[requirement.to_sym]
92
+ end
93
+
94
+ def classify_and_trace_evaluations(evaluated, trace)
95
+ auto_approved = evaluated.select { |e| e[:auto_approved] }
96
+ approved = evaluated.select { |e| e[:approved] && !e[:auto_approved] }
97
+ rejected = evaluated.reject { |e| e[:approved] }
98
+ trace[:steps] << { step: :evaluate, evaluated: evaluated.size,
99
+ auto_approved: auto_approved.size,
100
+ approved: approved.size,
101
+ rejected: rejected.size,
102
+ held_for_review: approved.size }
103
+ end
104
+
105
+ def execute_build_step(trace, base_path, force)
106
+ eval_step = trace[:steps].find { |s| s[:step] == :evaluate }
107
+ held_count = eval_step[:approved]
108
+
109
+ # Collect buildable proposals from the store
110
+ all_evaluated = proposal_ids_from_trace(trace)
111
+ buildable = select_buildable(all_evaluated, force)
112
+
113
+ if buildable.empty? && held_count.positive?
114
+ trace[:steps] << { step: :build, attempted: 0, succeeded: 0, failed: 0,
115
+ held: held_count,
116
+ message: 'approved proposals held for governance review' }
117
+ nil
118
+ elsif buildable.empty?
119
+ failure(trace, 'no proposals approved')
120
+ else
121
+ builds = build_proposals(buildable, base_path)
122
+ trace[:steps] << { step: :build, attempted: builds.size,
123
+ succeeded: builds.count { |b| b[:success] },
124
+ failed: builds.count { |b| !b[:success] },
125
+ held: force ? 0 : held_count }
126
+ nil
127
+ end
128
+ end
129
+
130
+ def proposal_ids_from_trace(trace)
131
+ propose_step = trace[:steps].find { |s| s[:step] == :propose }
132
+ propose_step[:proposals]
133
+ end
134
+
135
+ def select_buildable(proposal_ids, force)
136
+ proposal_ids.filter_map do |id|
137
+ proposal = Runners::Proposer.get_proposal_object(id)
138
+ next unless proposal&.status == :approved
139
+
140
+ { proposal: proposal.to_h } if force || proposal.auto_approvable?
141
+ end
142
+ end
143
+
144
+ def evaluate_proposals(proposals)
145
+ proposals.filter_map do |p|
146
+ result = Runners::Proposer.evaluate_proposal(proposal_id: p[:proposal][:id])
147
+ result if result[:success]
148
+ end
149
+ end
150
+
151
+ def build_proposals(proposals, base_path)
152
+ proposals.map do |a|
153
+ Runners::Builder.build_extension(
154
+ proposal_id: a[:proposal][:id],
155
+ base_path: base_path
156
+ )
157
+ end
158
+ end
159
+
160
+ def failure(trace, reason)
161
+ trace[:completed_at] = Time.now.utc
162
+ trace[:duration_ms] = ((trace[:completed_at] - trace[:started_at]) * 1000).round
163
+ trace[:failure_reason] = reason
164
+ { success: false, trace: trace, error: reason }
165
+ end
166
+
167
+ def log_cycle_summary(trace)
168
+ return unless defined?(Legion::Logging)
169
+
170
+ build_step = trace[:steps].find { |s| s[:step] == :build }
171
+ succeeded = build_step ? build_step[:succeeded] : 0
172
+ Legion::Logging.info "[mind_growth:orchestrator] cycle complete: #{succeeded} extensions built"
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end