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.
- checksums.yaml +7 -0
- data/Gemfile +19 -0
- data/LICENSE +21 -0
- data/lex-mind-growth.gemspec +29 -0
- data/lib/legion/extensions/mind_growth/actors/growth_cycle.rb +55 -0
- data/lib/legion/extensions/mind_growth/client.rb +36 -0
- data/lib/legion/extensions/mind_growth/helpers/build_pipeline.rb +63 -0
- data/lib/legion/extensions/mind_growth/helpers/cognitive_models.rb +60 -0
- data/lib/legion/extensions/mind_growth/helpers/concept_proposal.rb +69 -0
- data/lib/legion/extensions/mind_growth/helpers/constants.rb +55 -0
- data/lib/legion/extensions/mind_growth/helpers/fitness_evaluator.rb +58 -0
- data/lib/legion/extensions/mind_growth/helpers/proposal_store.rb +71 -0
- data/lib/legion/extensions/mind_growth/runners/analyzer.rb +52 -0
- data/lib/legion/extensions/mind_growth/runners/builder.rb +254 -0
- data/lib/legion/extensions/mind_growth/runners/orchestrator.rb +178 -0
- data/lib/legion/extensions/mind_growth/runners/proposer.rb +269 -0
- data/lib/legion/extensions/mind_growth/runners/validator.rb +57 -0
- data/lib/legion/extensions/mind_growth/version.rb +9 -0
- data/lib/legion/extensions/mind_growth.rb +25 -0
- data/spec/legion/extensions/mind_growth/actors/growth_cycle_spec.rb +81 -0
- data/spec/legion/extensions/mind_growth/client_spec.rb +111 -0
- data/spec/legion/extensions/mind_growth/helpers/build_pipeline_spec.rb +190 -0
- data/spec/legion/extensions/mind_growth/helpers/cognitive_models_spec.rb +106 -0
- data/spec/legion/extensions/mind_growth/helpers/concept_proposal_spec.rb +209 -0
- data/spec/legion/extensions/mind_growth/helpers/fitness_evaluator_spec.rb +123 -0
- data/spec/legion/extensions/mind_growth/helpers/proposal_store_spec.rb +203 -0
- data/spec/legion/extensions/mind_growth/runners/analyzer_spec.rb +108 -0
- data/spec/legion/extensions/mind_growth/runners/builder_spec.rb +347 -0
- data/spec/legion/extensions/mind_growth/runners/orchestrator_spec.rb +185 -0
- data/spec/legion/extensions/mind_growth/runners/proposer_spec.rb +493 -0
- data/spec/legion/extensions/mind_growth/runners/validator_spec.rb +120 -0
- data/spec/spec_helper.rb +22 -0
- 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
|