aia 1.1.0 → 2.0.0.0.pre.alpha
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 +4 -4
- data/.envrc +5 -1
- data/.loki +231 -0
- data/.quality/flay_baseline.txt +1 -0
- data/.quality/flog_baseline.txt +29 -0
- data/.quality/reek_baseline.txt +80 -0
- data/.rubocop.yml +116 -0
- data/.version +1 -1
- data/CHANGELOG.md +266 -42
- data/IMPLEMENTATION_PLAN.md +506 -0
- data/README.md +266 -238
- data/Rakefile +118 -5
- data/architecture_review.md +314 -0
- data/bin/aia +16 -0
- data/docs/AGENTS.md +40 -0
- data/docs/advanced-prompting.md +67 -3
- data/docs/cli-reference.md +312 -56
- data/docs/configuration.md +130 -19
- data/docs/contributing.md +56 -2
- data/docs/directives-reference.md +593 -78
- data/docs/faq.md +85 -3
- data/docs/guides/available-models.md +1 -1
- data/docs/guides/basic-usage.md +6 -6
- data/docs/guides/chat.md +40 -16
- data/docs/guides/crew.md +239 -0
- data/docs/guides/executable-prompts.md +1 -1
- data/docs/guides/index.md +1 -0
- data/docs/guides/models.md +15 -0
- data/docs/index.md +29 -2
- data/docs/installation.md +44 -17
- data/docs/mcp-integration.md +40 -0
- data/docs/prompt_management.md +85 -86
- data/docs/security.md +47 -0
- data/docs/special_projects_guide.md +386 -0
- data/docs/tools-and-mcp-examples.md +23 -0
- data/docs/workflows-and-pipelines.md +84 -7
- data/examples/.gitignore +1 -0
- data/examples/00_setup_aia.sh +27 -44
- data/examples/11_multi_model.sh +4 -14
- data/examples/12_token_usage.sh +3 -12
- data/examples/18_tools.sh +10 -2
- data/examples/22_chat_mode.sh +0 -10
- data/examples/23_verify.sh +139 -0
- data/examples/24_decompose.sh +139 -0
- data/examples/25_spawn.sh +139 -0
- data/examples/26_debate.sh +97 -0
- data/examples/27_mention_routing.sh +157 -0
- data/examples/28_model_switching.sh +106 -0
- data/examples/29_agent_harness.sh +177 -0
- data/examples/README.md +65 -0
- data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
- data/examples/aia_config.yml +1 -1
- data/examples/aia_config_orchestrator.yml +45 -0
- data/examples/common.sh +19 -0
- data/examples/context/tech_stack.md +2 -2
- data/examples/prompts_dir/project_summary +2 -2
- data/examples/prompts_dir/roles/orchestrator.md +21 -0
- data/examples/requirements/sinatra_taskflow_app.md +139 -0
- data/examples/rules/01_classify_ruby.rb +16 -0
- data/examples/rules/02_prefer_claude_for_code.rb +19 -0
- data/examples/rules/03_gate_prompt_length.rb +19 -0
- data/examples/rules/04_tool_selection.rb +41 -0
- data/examples/rules/README.md +30 -0
- data/examples/run_all.sh +48 -15
- data/examples/tools/word_count_tool.rb +1 -1
- data/lib/AGENTS.md +57 -0
- data/lib/aia/chat_loop.rb +306 -159
- data/lib/aia/config/cli_parser.rb +174 -111
- data/lib/aia/config/defaults.yml +62 -33
- data/lib/aia/config/mcp_parser.rb +39 -46
- data/lib/aia/config/model_spec.rb +34 -2
- data/lib/aia/config/validator.rb +121 -138
- data/lib/aia/config.rb +110 -145
- data/lib/aia/content_extractor.rb +153 -0
- data/lib/aia/cost_calculator.rb +38 -0
- data/lib/aia/crew.rb +164 -0
- data/lib/aia/debate_handler.rb +166 -0
- data/lib/aia/delegate_handler.rb +112 -0
- data/lib/aia/directive.rb +33 -18
- data/lib/aia/directive_processor.rb +16 -7
- data/lib/aia/directives/configuration_directives.rb +160 -20
- data/lib/aia/directives/context_directives.rb +38 -26
- data/lib/aia/directives/execution_directives.rb +136 -4
- data/lib/aia/directives/model_directives.rb +76 -34
- data/lib/aia/directives/trakflow_directives.rb +44 -0
- data/lib/aia/directives/utility_directives.rb +203 -6
- data/lib/aia/directives/web_and_file_directives.rb +96 -60
- data/lib/aia/errors.rb +15 -0
- data/lib/aia/fact_asserter.rb +27 -0
- data/lib/aia/fzf.rb +9 -31
- data/lib/aia/handler_context.rb +17 -0
- data/lib/aia/handler_protocol.rb +19 -0
- data/lib/aia/history_transfer.rb +55 -0
- data/lib/aia/input_collector.rb +3 -3
- data/lib/aia/layered_orchestrator.rb +448 -0
- data/lib/aia/logger.rb +24 -4
- data/lib/aia/mcp_config_normalizer.rb +35 -0
- data/lib/aia/mcp_connection_manager.rb +305 -0
- data/lib/aia/mcp_discovery.rb +44 -0
- data/lib/aia/mcp_grouper.rb +33 -0
- data/lib/aia/mcp_utility.rb +57 -0
- data/lib/aia/mention_router.rb +260 -0
- data/lib/aia/model_alias_registry.rb +97 -0
- data/lib/aia/model_switch_handler.rb +100 -0
- data/lib/aia/network_builder.rb +155 -0
- data/lib/aia/network_memory_manager.rb +55 -0
- data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
- data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
- data/lib/aia/pipeline_orchestrator.rb +262 -0
- data/lib/aia/plugin_loader.rb +170 -0
- data/lib/aia/plugin_monitor.rb +208 -0
- data/lib/aia/prompt_decomposer.rb +157 -0
- data/lib/aia/prompt_handler.rb +19 -39
- data/lib/aia/robot_builder.rb +51 -0
- data/lib/aia/robot_factory.rb +334 -0
- data/lib/aia/robot_namer.rb +116 -0
- data/lib/aia/session.rb +83 -17
- data/lib/aia/session_tracker.rb +209 -0
- data/lib/aia/similarity_scorer.rb +39 -0
- data/lib/aia/skill_utils.rb +105 -1
- data/lib/aia/spawn_handler.rb +129 -0
- data/lib/aia/spawn_spec_parser.rb +65 -0
- data/lib/aia/special_mode_handler.rb +302 -0
- data/lib/aia/startup_coordinator.rb +150 -0
- data/lib/aia/streaming_runner.rb +169 -0
- data/lib/aia/system_prompt_assembler.rb +88 -0
- data/lib/aia/task_coordinator.rb +202 -0
- data/lib/aia/task_decomposer.rb +57 -0
- data/lib/aia/task_executor.rb +51 -0
- data/lib/aia/tfidf_math.rb +27 -0
- data/lib/aia/tool_filter/tfidf.rb +116 -0
- data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
- data/lib/aia/tool_filter.rb +82 -0
- data/lib/aia/tool_filter_registry.rb +30 -0
- data/lib/aia/tool_filter_strategy.rb +143 -0
- data/lib/aia/tool_loader.rb +210 -0
- data/lib/aia/tool_utility.rb +30 -0
- data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
- data/lib/aia/tools/recruit_robot_tool.rb +60 -0
- data/lib/aia/tools/reskill_robot_tool.rb +44 -0
- data/lib/aia/tools/task_board_tool.rb +114 -0
- data/lib/aia/trakflow_bridge.rb +173 -0
- data/lib/aia/turn_state.rb +94 -0
- data/lib/aia/ui_presenter.rb +166 -198
- data/lib/aia/utility.rb +134 -87
- data/lib/aia/{history_manager.rb → variable_input_collector.rb} +8 -9
- data/lib/aia/verification_network.rb +58 -0
- data/lib/aia.rb +108 -63
- data/mkdocs.yml +1 -0
- metadata +179 -56
- data/justfile +0 -215
- data/lib/aia/adapter/chat_execution.rb +0 -242
- data/lib/aia/adapter/error_handler.rb +0 -68
- data/lib/aia/adapter/gem_activator.rb +0 -57
- data/lib/aia/adapter/mcp_connector.rb +0 -274
- data/lib/aia/adapter/modality_handlers.rb +0 -167
- data/lib/aia/adapter/model_registry.rb +0 -81
- data/lib/aia/adapter/multi_model_chat.rb +0 -218
- data/lib/aia/adapter/provider_configurator.rb +0 -59
- data/lib/aia/adapter/tool_filter.rb +0 -85
- data/lib/aia/adapter/tool_loader.rb +0 -90
- data/lib/aia/chat_processor_service.rb +0 -164
- data/lib/aia/prompt_pipeline.rb +0 -183
- data/lib/aia/ruby_llm_adapter.rb +0 -95
- data/lib/extensions/openstruct_merge.rb +0 -48
- data/lib/extensions/ruby_llm/.irbrc +0 -56
- data/lib/extensions/ruby_llm/modalities.rb +0 -36
- data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
- data/lib/refinements/string.rb +0 -16
- data/main.just +0 -76
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/layered_orchestrator.rb
|
|
4
|
+
#
|
|
5
|
+
# Three-tier agent orchestration for complex application builds.
|
|
6
|
+
#
|
|
7
|
+
# Tier 1 — Orchestrator (Tobor)
|
|
8
|
+
# Receives full application requirements. Decomposes them into
|
|
9
|
+
# independent architectural layers (e.g. infrastructure, data models,
|
|
10
|
+
# auth, routes, views). Each layer is a distinct technical concern.
|
|
11
|
+
#
|
|
12
|
+
# Tier 2 — Lead Agents (one per layer, run in parallel via Async::Barrier)
|
|
13
|
+
# Each lead agent receives its layer's requirements and breaks them
|
|
14
|
+
# into specific implementation tasks, assigning a specialist type to
|
|
15
|
+
# each task.
|
|
16
|
+
#
|
|
17
|
+
# Tier 3 — Specialist Robots (one per task, all run in parallel via Async::Barrier)
|
|
18
|
+
# Each specialist receives a focused, concrete task and produces the
|
|
19
|
+
# actual implementation artifact: a code file, migration, spec, or
|
|
20
|
+
# configuration block.
|
|
21
|
+
#
|
|
22
|
+
# After all layers complete, Tobor synthesizes a final integration
|
|
23
|
+
# summary showing how the pieces fit together.
|
|
24
|
+
|
|
25
|
+
require 'json'
|
|
26
|
+
require 'fileutils'
|
|
27
|
+
require 'async'
|
|
28
|
+
|
|
29
|
+
module AIA
|
|
30
|
+
class LayeredOrchestrator
|
|
31
|
+
include ContentExtractor
|
|
32
|
+
include HandlerProtocol
|
|
33
|
+
|
|
34
|
+
MAX_LAYERS = 5
|
|
35
|
+
MAX_TASKS_PER_LAYER = 4
|
|
36
|
+
|
|
37
|
+
# Tier 1 prompt: decompose requirements into layers
|
|
38
|
+
# Deliberately short and directive to minimise qwen3 think-block noise.
|
|
39
|
+
LAYER_DECOMPOSE_PROMPT = <<~PROMPT
|
|
40
|
+
TASK: Decompose the application requirements below into architectural layers.
|
|
41
|
+
RESPOND WITH ONLY A JSON ARRAY. No explanation. No markdown. No code fences.
|
|
42
|
+
|
|
43
|
+
JSON schema (array of objects):
|
|
44
|
+
name - snake_case layer identifier
|
|
45
|
+
title - short human-readable title
|
|
46
|
+
description - one sentence describing what this layer covers
|
|
47
|
+
requirements - comma-separated list of things to implement in this layer
|
|
48
|
+
|
|
49
|
+
Rules: 3 to %{max_layers} layers. Infrastructure first, UI last.
|
|
50
|
+
|
|
51
|
+
APPLICATION REQUIREMENTS:
|
|
52
|
+
%{requirements}
|
|
53
|
+
|
|
54
|
+
JSON ARRAY RESPONSE:
|
|
55
|
+
PROMPT
|
|
56
|
+
|
|
57
|
+
# Tier 2 prompt: lead agent decomposes its layer into tasks
|
|
58
|
+
TASK_DECOMPOSE_PROMPT = <<~PROMPT
|
|
59
|
+
TASK: Break the %{layer_title} layer into implementation tasks.
|
|
60
|
+
RESPOND WITH ONLY A JSON ARRAY. No explanation. No markdown. No code fences.
|
|
61
|
+
|
|
62
|
+
JSON schema (array of objects):
|
|
63
|
+
title - short task title
|
|
64
|
+
specialist - specialist role name (e.g. sequel-migration-writer)
|
|
65
|
+
artifact - exact filename to produce (e.g. db/migrations/001_create_users.rb)
|
|
66
|
+
prompt - self-contained instruction for the specialist
|
|
67
|
+
|
|
68
|
+
Rules: 2 to %{max_tasks} tasks. Each task produces exactly one file.
|
|
69
|
+
|
|
70
|
+
LAYER: %{layer_title}
|
|
71
|
+
REQUIREMENTS: %{requirements}
|
|
72
|
+
|
|
73
|
+
JSON ARRAY RESPONSE:
|
|
74
|
+
PROMPT
|
|
75
|
+
|
|
76
|
+
# Layer synthesis prompt
|
|
77
|
+
LAYER_SYNTHESIS_PROMPT = <<~PROMPT
|
|
78
|
+
You implemented the %{layer_title} layer. Here are the artifacts produced
|
|
79
|
+
by your specialist team:
|
|
80
|
+
|
|
81
|
+
%{task_results}
|
|
82
|
+
|
|
83
|
+
Write a brief (3-5 sentence) integration summary: what was built, how the
|
|
84
|
+
artifacts fit together, and what the next layer depends on from this one.
|
|
85
|
+
PROMPT
|
|
86
|
+
|
|
87
|
+
# Final synthesis prompt for Tobor
|
|
88
|
+
FINAL_SYNTHESIS_PROMPT = <<~PROMPT
|
|
89
|
+
All architectural layers of the application have been built by the agent teams.
|
|
90
|
+
Here is what each layer produced:
|
|
91
|
+
|
|
92
|
+
%{layer_summaries}
|
|
93
|
+
|
|
94
|
+
Original requirements excerpt:
|
|
95
|
+
%{requirements_excerpt}
|
|
96
|
+
|
|
97
|
+
Provide a final integration summary:
|
|
98
|
+
1. What was built overall
|
|
99
|
+
2. How the layers connect (what each layer depends on from the layers below it)
|
|
100
|
+
3. The minimal steps to make the application runnable (config, migrations, startup)
|
|
101
|
+
PROMPT
|
|
102
|
+
|
|
103
|
+
def initialize(robot:, ui_presenter:, tracker:, build_dir: nil)
|
|
104
|
+
@robot = robot
|
|
105
|
+
@ui_presenter = ui_presenter
|
|
106
|
+
@tracker = tracker
|
|
107
|
+
@build_dir = build_dir || default_build_dir
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def default_build_dir
|
|
111
|
+
File.join(Dir.pwd, "orchestrated_build_#{Time.now.strftime('%Y%m%d_%H%M%S')}")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Print orchestration progress to stdout so it appears inline with the chat.
|
|
115
|
+
# (display_info uses $stderr which is invisible in interactive sessions.)
|
|
116
|
+
def say(msg)
|
|
117
|
+
$stdout.puts msg
|
|
118
|
+
$stdout.flush
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
attr_writer :robot
|
|
122
|
+
|
|
123
|
+
# Entry point called by SpecialModeHandler.
|
|
124
|
+
#
|
|
125
|
+
# @param context [HandlerContext] — reads context.prompt as requirements text
|
|
126
|
+
# @return [String, nil] final synthesis or nil on failure
|
|
127
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
128
|
+
def handle(context)
|
|
129
|
+
requirements = context.prompt
|
|
130
|
+
primary = @robot.chief
|
|
131
|
+
|
|
132
|
+
FileUtils.mkdir_p(@build_dir)
|
|
133
|
+
say("")
|
|
134
|
+
say("╔══════════════════════════════════════════════════════════╗")
|
|
135
|
+
say("║ LAYERED ORCHESTRATION — 3-TIER BUILD ║")
|
|
136
|
+
say("╚══════════════════════════════════════════════════════════╝")
|
|
137
|
+
say("Requirements: #{requirements.lines.first.strip}")
|
|
138
|
+
say("Build output: #{@build_dir}")
|
|
139
|
+
say("")
|
|
140
|
+
|
|
141
|
+
# Tier 1: Tobor decomposes requirements into layers
|
|
142
|
+
layers = decompose_to_layers(primary, requirements)
|
|
143
|
+
if layers.empty?
|
|
144
|
+
say("Could not decompose requirements into layers. Aborting orchestration.")
|
|
145
|
+
return nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
display_layer_plan(layers)
|
|
149
|
+
|
|
150
|
+
# Tier 2: All lead agents run in parallel (Async::Barrier)
|
|
151
|
+
say("Running #{layers.size} lead agents in parallel...")
|
|
152
|
+
layer_task_map = run_leads_wave(layers)
|
|
153
|
+
|
|
154
|
+
if layer_task_map.values.all?(&:empty?)
|
|
155
|
+
say("All lead agents failed. Aborting orchestration.")
|
|
156
|
+
raise OrchestratorError, "All lead agents failed to produce tasks"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Tier 3: All specialists run in parallel (Async::Barrier)
|
|
160
|
+
say("")
|
|
161
|
+
say("Running specialists in parallel...")
|
|
162
|
+
specialist_results = run_specialists_wave(layer_task_map)
|
|
163
|
+
|
|
164
|
+
# Assemble layer_results for synthesis (same shape as the former sequential approach)
|
|
165
|
+
layer_results = build_layer_results(layers, layer_task_map, specialist_results)
|
|
166
|
+
|
|
167
|
+
# Final synthesis by Tobor
|
|
168
|
+
say("")
|
|
169
|
+
say("━━━ Tobor synthesizing all #{layers.size} layers ━━━")
|
|
170
|
+
final_result = synthesize_all(primary, requirements, layers, layer_results)
|
|
171
|
+
final_text = extract_content(final_result)
|
|
172
|
+
|
|
173
|
+
save_final_report(final_text)
|
|
174
|
+
say("Build complete: #{@build_dir}")
|
|
175
|
+
|
|
176
|
+
@tracker.record_turn(
|
|
177
|
+
model: AIA.config.models.first.name,
|
|
178
|
+
input: requirements,
|
|
179
|
+
result: final_text
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
final_text
|
|
183
|
+
rescue StandardError => e
|
|
184
|
+
say("✗ Orchestration error: #{e.class}: #{e.message}")
|
|
185
|
+
e.backtrace&.first(5)&.each { |line| say(" #{line}") }
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
# Tier 1: use a probe robot to decompose requirements into layer specs
|
|
193
|
+
def decompose_to_layers(robot, requirements)
|
|
194
|
+
say("Tier 1 ▶ #{robot.name} decomposing requirements into layers...")
|
|
195
|
+
probe = build_probe(robot.name + "-layer-probe")
|
|
196
|
+
prompt = LAYER_DECOMPOSE_PROMPT % {
|
|
197
|
+
requirements: requirements,
|
|
198
|
+
max_layers: MAX_LAYERS
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
result = probe.run(prompt, mcp: :none, tools: :none)
|
|
202
|
+
content = extract_content(result)
|
|
203
|
+
layers = parse_json_array(content)
|
|
204
|
+
|
|
205
|
+
if layers.empty?
|
|
206
|
+
preview = content.to_s.gsub(%r{<think>.*?</think>}m, '').strip[0, 300]
|
|
207
|
+
say(" ⚠ Layer decomposition parse failed.")
|
|
208
|
+
say(" Raw response preview: #{preview}")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
layers.first(MAX_LAYERS)
|
|
212
|
+
rescue StandardError => e
|
|
213
|
+
say(" ✗ decompose_to_layers failed: #{e.class}: #{e.message}")
|
|
214
|
+
e.backtrace&.first(3)&.each { |line| say(" #{line}") }
|
|
215
|
+
[]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def display_layer_plan(layers)
|
|
219
|
+
say("Tier 1 ▶ #{layers.size} layers identified:")
|
|
220
|
+
layers.each_with_index do |layer, i|
|
|
221
|
+
say(" #{i + 1}. #{layer['title']}: #{layer['description']}")
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Tier 2 Wave: run all lead agents in parallel via Async::Barrier.
|
|
226
|
+
# Each lead decomposes its layer into task specifications.
|
|
227
|
+
#
|
|
228
|
+
# @param layers [Array<Hash>] layer specs from Tier 1
|
|
229
|
+
# @return [Hash{ String => Array<Hash> }] layer_name => task list ([] on failure)
|
|
230
|
+
def run_leads_wave(layers)
|
|
231
|
+
results = {}
|
|
232
|
+
|
|
233
|
+
Sync do
|
|
234
|
+
barrier = Async::Barrier.new
|
|
235
|
+
layers.each do |layer|
|
|
236
|
+
barrier.async do
|
|
237
|
+
probe = build_probe("#{layer['name']}-lead")
|
|
238
|
+
prompt = TASK_DECOMPOSE_PROMPT % {
|
|
239
|
+
layer_title: layer['title'],
|
|
240
|
+
requirements: layer['requirements'].to_s,
|
|
241
|
+
max_tasks: MAX_TASKS_PER_LAYER
|
|
242
|
+
}
|
|
243
|
+
result = probe.run(prompt, mcp: :none, tools: :none)
|
|
244
|
+
tasks = parse_json_array(extract_content(result)).first(MAX_TASKS_PER_LAYER)
|
|
245
|
+
say(" ✓ #{layer['title']} lead: #{tasks.size} task(s)")
|
|
246
|
+
results[layer['name']] = tasks
|
|
247
|
+
rescue => e
|
|
248
|
+
say(" ✗ #{layer['title']} lead failed: #{e.message}")
|
|
249
|
+
results[layer['name']] = []
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
barrier.wait
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
results
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Tier 3 Wave: run all specialists in parallel via Async::Barrier.
|
|
259
|
+
# Flattens tasks from all layers into a single barrier so specialists
|
|
260
|
+
# across all layers execute concurrently.
|
|
261
|
+
#
|
|
262
|
+
# @param layer_task_map [Hash{ String => Array<Hash> }] output of run_leads_wave
|
|
263
|
+
# @return [Hash{ String => Hash }] "layer_name|artifact_path" => result hash
|
|
264
|
+
def run_specialists_wave(layer_task_map)
|
|
265
|
+
jobs = layer_task_map.flat_map do |layer_name, tasks|
|
|
266
|
+
tasks.map { |task| { layer_name: layer_name, task: task } }
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
results = {}
|
|
270
|
+
|
|
271
|
+
# rubocop:disable Metrics/BlockLength
|
|
272
|
+
Sync do
|
|
273
|
+
barrier = Async::Barrier.new
|
|
274
|
+
jobs.each do |job|
|
|
275
|
+
barrier.async do
|
|
276
|
+
task = job[:task]
|
|
277
|
+
key = "#{job[:layer_name]}|#{task['artifact']}"
|
|
278
|
+
probe = build_probe("#{task['specialist']}-specialist")
|
|
279
|
+
|
|
280
|
+
full_prompt = "Artifact to produce: #{task['artifact']}\n\n#{task['prompt']}"
|
|
281
|
+
result = probe.run(full_prompt, mcp: :none, tools: :none)
|
|
282
|
+
output = extract_content(result)
|
|
283
|
+
|
|
284
|
+
save_artifact(task['artifact'], output)
|
|
285
|
+
say(" ✓ #{task['artifact']}")
|
|
286
|
+
|
|
287
|
+
results[key] = {
|
|
288
|
+
task: task['title'],
|
|
289
|
+
specialist: task['specialist'],
|
|
290
|
+
artifact: task['artifact'],
|
|
291
|
+
output: output
|
|
292
|
+
}
|
|
293
|
+
rescue => e
|
|
294
|
+
task = job[:task]
|
|
295
|
+
key = "#{job[:layer_name]}|#{task['artifact']}"
|
|
296
|
+
say(" ✗ #{task['artifact']} failed: #{e.message}")
|
|
297
|
+
results[key] = {
|
|
298
|
+
task: task['title'],
|
|
299
|
+
specialist: task['specialist'],
|
|
300
|
+
artifact: task['artifact'],
|
|
301
|
+
output: "[FAILED: #{e.message}]"
|
|
302
|
+
}
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
# rubocop:enable Metrics/BlockLength
|
|
306
|
+
barrier.wait
|
|
307
|
+
end
|
|
308
|
+
results
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Assemble the layer_results array expected by synthesize_all.
|
|
312
|
+
# Shape: [{ layer:, tasks: [result_hashes], summary: }]
|
|
313
|
+
#
|
|
314
|
+
# @param layers [Array<Hash>] original layer specs
|
|
315
|
+
# @param layer_task_map [Hash] output of run_leads_wave
|
|
316
|
+
# @param specialist_results [Hash] output of run_specialists_wave
|
|
317
|
+
# @return [Array<Hash>]
|
|
318
|
+
def build_layer_results(layers, layer_task_map, specialist_results)
|
|
319
|
+
layers.map do |layer|
|
|
320
|
+
tasks_for_layer = layer_task_map[layer['name']] || []
|
|
321
|
+
task_results = tasks_for_layer.map do |task|
|
|
322
|
+
key = "#{layer['name']}|#{task['artifact']}"
|
|
323
|
+
specialist_results[key] || {
|
|
324
|
+
task: task['title'],
|
|
325
|
+
specialist: task['specialist'],
|
|
326
|
+
artifact: task['artifact'],
|
|
327
|
+
output: "[FAILED: no result]"
|
|
328
|
+
}
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
summary = synthesize_layer_from_results(layer['title'], task_results)
|
|
332
|
+
say(" ✓ #{layer['title']} synthesis complete")
|
|
333
|
+
{ layer: layer['title'], tasks: task_results, summary: summary }
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Synthesize all task outputs for a layer into an integration summary.
|
|
338
|
+
# Uses a fresh probe robot (no conversation history).
|
|
339
|
+
def synthesize_layer_from_results(layer_title, task_results)
|
|
340
|
+
results_text = task_results.map do |r|
|
|
341
|
+
"### #{r[:task]} — #{r[:artifact]}\n#{r[:output]}"
|
|
342
|
+
end.join("\n\n---\n\n")
|
|
343
|
+
|
|
344
|
+
prompt = LAYER_SYNTHESIS_PROMPT % {
|
|
345
|
+
layer_title: layer_title,
|
|
346
|
+
task_results: results_text
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
probe = build_probe("#{layer_title.downcase.gsub(/\s+/, '-')}-synthesis")
|
|
350
|
+
result = probe.run(prompt, mcp: :none, tools: :none)
|
|
351
|
+
extract_content(result)
|
|
352
|
+
rescue StandardError
|
|
353
|
+
"(layer synthesis failed)"
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Tobor synthesizes all layer summaries into a final integration report
|
|
357
|
+
def synthesize_all(primary, requirements, layers, layer_results)
|
|
358
|
+
summaries = layer_results.each_with_index.map do |lr, i|
|
|
359
|
+
header = "## Layer #{i + 1}: #{lr[:layer]}"
|
|
360
|
+
tasks = Array(lr[:tasks]).map { |t| "- #{t[:artifact]}: #{t[:task]}" }.join("\n")
|
|
361
|
+
"#{header}\n#{tasks}\n\nSummary: #{lr[:summary]}"
|
|
362
|
+
end.join("\n\n")
|
|
363
|
+
|
|
364
|
+
primary.run(
|
|
365
|
+
FINAL_SYNTHESIS_PROMPT % {
|
|
366
|
+
layer_summaries: summaries,
|
|
367
|
+
requirements_excerpt: requirements.lines.first(15).join
|
|
368
|
+
},
|
|
369
|
+
mcp: :none, tools: :none
|
|
370
|
+
)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Parse JSON array from model output.
|
|
374
|
+
# Tries multiple extraction strategies to handle think blocks,
|
|
375
|
+
# markdown fences, prose before/after the JSON, and partial wrapping.
|
|
376
|
+
def parse_json_array(content)
|
|
377
|
+
return [] if content.nil? || content.strip.empty?
|
|
378
|
+
|
|
379
|
+
# Strip think blocks (qwen3 reasoning models wrap output in <think>...)
|
|
380
|
+
text = content.gsub(%r{<think>.*?</think>}m, '').strip
|
|
381
|
+
|
|
382
|
+
# Strategy 1: extract content from markdown code fences
|
|
383
|
+
if (m = text.match(/```(?:json)?\s*\n?(.*?)```/m))
|
|
384
|
+
candidate = m[1].strip
|
|
385
|
+
result = try_json_parse(candidate)
|
|
386
|
+
return result unless result.empty?
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Strategy 2: find the first [...] block in the response
|
|
390
|
+
if (m = text.match(/(\[[\s\S]*\])/m))
|
|
391
|
+
result = try_json_parse(m[1])
|
|
392
|
+
return result unless result.empty?
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Strategy 3: parse the whole stripped text
|
|
396
|
+
try_json_parse(text)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def try_json_parse(text)
|
|
400
|
+
result = JSON.parse(text.strip)
|
|
401
|
+
result.is_a?(Array) ? result.grep(Hash) : []
|
|
402
|
+
rescue JSON::ParserError
|
|
403
|
+
[]
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Build a short-lived probe robot with no conversation history.
|
|
407
|
+
# Uses the primary model spec so the provider is correctly wired.
|
|
408
|
+
def build_probe(name)
|
|
409
|
+
config = AIA.config
|
|
410
|
+
run_config = RobotFactory.build_run_config(config)
|
|
411
|
+
model_spec = config.models.first
|
|
412
|
+
|
|
413
|
+
RobotFactory.build_robot(
|
|
414
|
+
model_spec,
|
|
415
|
+
name: name,
|
|
416
|
+
system_prompt: nil,
|
|
417
|
+
config: run_config
|
|
418
|
+
)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Write an artifact to the build directory.
|
|
422
|
+
# Strips markdown code fences if the entire output is wrapped in them.
|
|
423
|
+
def save_artifact(artifact_path, content)
|
|
424
|
+
return unless content && !content.strip.empty?
|
|
425
|
+
|
|
426
|
+
# Strip a single wrapping code fence if the whole content is fenced
|
|
427
|
+
code = content.strip
|
|
428
|
+
if (m = code.match(/\A```\w*\n(.*)\n```\z/m))
|
|
429
|
+
code = m[1]
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
dest = File.join(@build_dir, artifact_path)
|
|
433
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
434
|
+
File.write(dest, code)
|
|
435
|
+
rescue StandardError => e
|
|
436
|
+
say(" ⚠ Could not save #{artifact_path}: #{e.message}")
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Write the integration report to BUILD_DIR/INTEGRATION_REPORT.md
|
|
440
|
+
def save_final_report(text)
|
|
441
|
+
dest = File.join(@build_dir, "INTEGRATION_REPORT.md")
|
|
442
|
+
File.write(dest, text)
|
|
443
|
+
say("Integration report: #{dest}")
|
|
444
|
+
rescue StandardError
|
|
445
|
+
# best-effort
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
data/lib/aia/logger.rb
CHANGED
|
@@ -124,10 +124,9 @@ module AIA
|
|
|
124
124
|
config.log_file = resolve_log_file_io(file)
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
end
|
|
127
|
+
return unless config.respond_to?(:log_level=)
|
|
128
|
+
level = effective_log_level(logger_config_for(:mcp))
|
|
129
|
+
config.log_level = LOG_LEVELS.fetch(level, Lumberjack::Severity::WARN)
|
|
131
130
|
end
|
|
132
131
|
|
|
133
132
|
# Convert log file specification to IO object or file path
|
|
@@ -160,6 +159,27 @@ module AIA
|
|
|
160
159
|
@test_mode = false
|
|
161
160
|
end
|
|
162
161
|
|
|
162
|
+
# Update existing loggers' levels in-place from current config.
|
|
163
|
+
# Call this after runtime config changes (e.g. /config debug = false)
|
|
164
|
+
# so the log level takes effect immediately without recreating loggers.
|
|
165
|
+
def reconfigure_levels!
|
|
166
|
+
return if test_mode?
|
|
167
|
+
|
|
168
|
+
%i[aia llm mcp].each do |system|
|
|
169
|
+
logger = instance_variable_get(:"@#{system}_logger")
|
|
170
|
+
next unless logger
|
|
171
|
+
|
|
172
|
+
config = logger_config_for(system)
|
|
173
|
+
level = effective_log_level(config, system)
|
|
174
|
+
numeric = LOG_LEVELS.fetch(level, Lumberjack::Severity::WARN)
|
|
175
|
+
logger.level = numeric
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Keep RubyLLM / MCP configs in sync so they don't override us
|
|
179
|
+
configure_llm_logger
|
|
180
|
+
configure_mcp_logger
|
|
181
|
+
end
|
|
182
|
+
|
|
163
183
|
# =======================================================================
|
|
164
184
|
# Test Mode Support
|
|
165
185
|
# =======================================================================
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/mcp_config_normalizer.rb
|
|
4
|
+
#
|
|
5
|
+
# Normalizes a single MCP server config from AIA flat format to the nested
|
|
6
|
+
# transport format expected by robot_lab. Server selection/filtering is
|
|
7
|
+
# the responsibility of MCPDiscovery — this class only transforms shape.
|
|
8
|
+
|
|
9
|
+
module AIA
|
|
10
|
+
class MCPConfigNormalizer
|
|
11
|
+
class << self
|
|
12
|
+
# Normalize a single MCP server config to robot_lab's nested transport format.
|
|
13
|
+
#
|
|
14
|
+
# @param server [Hash]
|
|
15
|
+
# @return [Hash]
|
|
16
|
+
def normalize(server)
|
|
17
|
+
server = server.is_a?(Hash) ? server.transform_keys(&:to_sym) : server.to_h.transform_keys(&:to_sym)
|
|
18
|
+
|
|
19
|
+
# Already in robot_lab format — pass through
|
|
20
|
+
return server if server[:transport]
|
|
21
|
+
|
|
22
|
+
# Legacy flat format: wrap command/args/env into transport
|
|
23
|
+
name = server[:name]
|
|
24
|
+
transport = { type: server[:type] || 'stdio' }
|
|
25
|
+
transport[:command] = server[:command] if server[:command]
|
|
26
|
+
transport[:args] = Array(server[:args]) if server[:args]
|
|
27
|
+
transport[:env] = server[:env] if server[:env]
|
|
28
|
+
|
|
29
|
+
result = { name: name, transport: transport }
|
|
30
|
+
result[:timeout] = server[:timeout] if server[:timeout]
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|