aia 1.1.1 → 2.0.0.0.pre.beta2

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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +9 -1
  3. data/.loki +11 -0
  4. data/.reek.yml +160 -0
  5. data/.rubocop.yml +116 -0
  6. data/.rubocop_strict.yml +15 -0
  7. data/.version +1 -1
  8. data/CHANGELOG.md +269 -55
  9. data/IMPLEMENTATION_PLAN.md +506 -0
  10. data/README.md +267 -239
  11. data/Rakefile +5 -5
  12. data/_typos.toml +10 -0
  13. data/aia.gemspec +92 -0
  14. data/architecture_review.md +314 -0
  15. data/bin/aia +16 -0
  16. data/config/aia.yml +13 -0
  17. data/docs/AGENTS.md +40 -0
  18. data/docs/advanced-prompting.md +67 -3
  19. data/docs/cli-reference.md +312 -56
  20. data/docs/configuration.md +130 -19
  21. data/docs/contributing.md +56 -2
  22. data/docs/directives-reference.md +593 -78
  23. data/docs/faq.md +85 -3
  24. data/docs/guides/available-models.md +1 -1
  25. data/docs/guides/basic-usage.md +6 -6
  26. data/docs/guides/chat.md +40 -16
  27. data/docs/guides/crew.md +239 -0
  28. data/docs/guides/executable-prompts.md +1 -1
  29. data/docs/guides/index.md +1 -0
  30. data/docs/guides/models.md +15 -0
  31. data/docs/index.md +29 -2
  32. data/docs/installation.md +44 -17
  33. data/docs/mcp-integration.md +40 -0
  34. data/docs/prompt_management.md +85 -86
  35. data/docs/security.md +47 -0
  36. data/docs/special_projects_guide.md +386 -0
  37. data/docs/tools-and-mcp-examples.md +23 -0
  38. data/docs/workflows-and-pipelines.md +84 -7
  39. data/examples/.gitignore +1 -0
  40. data/examples/00_setup_aia.sh +27 -44
  41. data/examples/11_multi_model.sh +4 -14
  42. data/examples/12_token_usage.sh +3 -12
  43. data/examples/18_tools.sh +10 -2
  44. data/examples/22_chat_mode.sh +0 -10
  45. data/examples/23_verify.sh +139 -0
  46. data/examples/24_decompose.sh +139 -0
  47. data/examples/25_spawn.sh +139 -0
  48. data/examples/26_debate.sh +97 -0
  49. data/examples/27_mention_routing.sh +157 -0
  50. data/examples/28_model_switching.sh +106 -0
  51. data/examples/29_agent_harness.sh +177 -0
  52. data/examples/README.md +65 -0
  53. data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
  54. data/examples/aia_config.yml +1 -1
  55. data/examples/aia_config_orchestrator.yml +45 -0
  56. data/examples/common.sh +18 -6
  57. data/examples/context/tech_stack.md +2 -2
  58. data/examples/prompts_dir/roles/orchestrator.md +21 -0
  59. data/examples/requirements/sinatra_taskflow_app.md +139 -0
  60. data/examples/rules/01_classify_ruby.rb +16 -0
  61. data/examples/rules/02_prefer_claude_for_code.rb +19 -0
  62. data/examples/rules/03_gate_prompt_length.rb +19 -0
  63. data/examples/rules/04_tool_selection.rb +41 -0
  64. data/examples/rules/README.md +30 -0
  65. data/examples/run_all.sh +48 -15
  66. data/examples/tools/word_count_tool.rb +1 -1
  67. data/lib/AGENTS.md +57 -0
  68. data/lib/aia/chat_loop.rb +263 -167
  69. data/lib/aia/config/cli_parser.rb +217 -145
  70. data/lib/aia/config/defaults.yml +62 -33
  71. data/lib/aia/config/mcp_parser.rb +52 -51
  72. data/lib/aia/config/model_spec.rb +34 -2
  73. data/lib/aia/config/validator.rb +171 -216
  74. data/lib/aia/config.rb +111 -145
  75. data/lib/aia/content_extractor.rb +155 -0
  76. data/lib/aia/cost_calculator.rb +39 -0
  77. data/lib/aia/crew.rb +164 -0
  78. data/lib/aia/debate_handler.rb +174 -0
  79. data/lib/aia/delegate_handler.rb +116 -0
  80. data/lib/aia/directive.rb +43 -26
  81. data/lib/aia/directive_processor.rb +16 -7
  82. data/lib/aia/directives/configuration_directives.rb +214 -60
  83. data/lib/aia/directives/context_directives.rb +67 -52
  84. data/lib/aia/directives/execution_directives.rb +141 -4
  85. data/lib/aia/directives/model_directives.rb +163 -141
  86. data/lib/aia/directives/trakflow_directives.rb +62 -0
  87. data/lib/aia/directives/utility_directives.rb +227 -30
  88. data/lib/aia/directives/web_and_file_directives.rb +126 -77
  89. data/lib/aia/errors.rb +15 -0
  90. data/lib/aia/fact_asserter.rb +27 -0
  91. data/lib/aia/fzf.rb +9 -31
  92. data/lib/aia/handler_context.rb +17 -0
  93. data/lib/aia/handler_protocol.rb +19 -0
  94. data/lib/aia/history_transfer.rb +55 -0
  95. data/lib/aia/input_collector.rb +3 -3
  96. data/lib/aia/layered_orchestrator.rb +471 -0
  97. data/lib/aia/logger.rb +45 -25
  98. data/lib/aia/mcp_config_normalizer.rb +35 -0
  99. data/lib/aia/mcp_connection_manager.rb +315 -0
  100. data/lib/aia/mcp_discovery.rb +44 -0
  101. data/lib/aia/mcp_grouper.rb +33 -0
  102. data/lib/aia/mcp_server_config.rb +30 -0
  103. data/lib/aia/mcp_utility.rb +60 -0
  104. data/lib/aia/mention_router.rb +217 -0
  105. data/lib/aia/model_alias_registry.rb +97 -0
  106. data/lib/aia/model_switch_handler.rb +100 -0
  107. data/lib/aia/network_builder.rb +160 -0
  108. data/lib/aia/network_memory_manager.rb +55 -0
  109. data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
  110. data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
  111. data/lib/aia/pipeline_orchestrator.rb +272 -0
  112. data/lib/aia/plugin_loader.rb +170 -0
  113. data/lib/aia/plugin_monitor.rb +211 -0
  114. data/lib/aia/prompt_decomposer.rb +159 -0
  115. data/lib/aia/prompt_handler.rb +56 -82
  116. data/lib/aia/robot_builder.rb +51 -0
  117. data/lib/aia/robot_factory.rb +338 -0
  118. data/lib/aia/robot_namer.rb +110 -0
  119. data/lib/aia/session.rb +87 -17
  120. data/lib/aia/session_tracker.rb +207 -0
  121. data/lib/aia/similarity_scorer.rb +41 -0
  122. data/lib/aia/skill_utils.rb +105 -1
  123. data/lib/aia/spawn_handler.rb +129 -0
  124. data/lib/aia/spawn_spec_parser.rb +65 -0
  125. data/lib/aia/special_mode_handler.rb +322 -0
  126. data/lib/aia/speech.rb +67 -0
  127. data/lib/aia/startup_coordinator.rb +151 -0
  128. data/lib/aia/streaming_runner.rb +172 -0
  129. data/lib/aia/system_prompt_assembler.rb +92 -0
  130. data/lib/aia/task_coordinator.rb +207 -0
  131. data/lib/aia/task_decomposer.rb +57 -0
  132. data/lib/aia/task_executor.rb +51 -0
  133. data/lib/aia/tfidf_math.rb +27 -0
  134. data/lib/aia/timing.rb +15 -0
  135. data/lib/aia/tool_filter/tfidf.rb +116 -0
  136. data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
  137. data/lib/aia/tool_filter.rb +83 -0
  138. data/lib/aia/tool_filter_registry.rb +30 -0
  139. data/lib/aia/tool_filter_strategy.rb +146 -0
  140. data/lib/aia/tool_introspection.rb +17 -0
  141. data/lib/aia/tool_loader.rb +216 -0
  142. data/lib/aia/tool_utility.rb +30 -0
  143. data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
  144. data/lib/aia/tools/recruit_robot_tool.rb +60 -0
  145. data/lib/aia/tools/reskill_robot_tool.rb +44 -0
  146. data/lib/aia/tools/task_board_tool.rb +115 -0
  147. data/lib/aia/trakflow_bridge.rb +175 -0
  148. data/lib/aia/turn_state.rb +95 -0
  149. data/lib/aia/ui_presenter.rb +182 -206
  150. data/lib/aia/utility.rb +136 -87
  151. data/lib/aia/{history_manager.rb → variable_input_collector.rb} +9 -9
  152. data/lib/aia/verification_network.rb +57 -0
  153. data/lib/aia.rb +124 -63
  154. data/mkdocs.yml +1 -0
  155. metadata +187 -58
  156. data/justfile +0 -215
  157. data/lib/aia/adapter/chat_execution.rb +0 -242
  158. data/lib/aia/adapter/error_handler.rb +0 -68
  159. data/lib/aia/adapter/gem_activator.rb +0 -57
  160. data/lib/aia/adapter/mcp_connector.rb +0 -274
  161. data/lib/aia/adapter/modality_handlers.rb +0 -167
  162. data/lib/aia/adapter/model_registry.rb +0 -81
  163. data/lib/aia/adapter/multi_model_chat.rb +0 -218
  164. data/lib/aia/adapter/provider_configurator.rb +0 -59
  165. data/lib/aia/adapter/tool_filter.rb +0 -85
  166. data/lib/aia/adapter/tool_loader.rb +0 -90
  167. data/lib/aia/chat_processor_service.rb +0 -178
  168. data/lib/aia/prompt_pipeline.rb +0 -183
  169. data/lib/aia/ruby_llm_adapter.rb +0 -95
  170. data/lib/extensions/openstruct_merge.rb +0 -48
  171. data/lib/extensions/ruby_llm/.irbrc +0 -56
  172. data/lib/extensions/ruby_llm/modalities.rb +0 -36
  173. data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
  174. data/lib/refinements/string.rb +0 -16
  175. data/main.just +0 -76
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/tool_loader.rb
4
+ #
5
+ # Tool loading, caching, filtering, and discovery.
6
+ # Extracted from RobotFactory to isolate the tool concern.
7
+ #
8
+ # ToolLoader is a class with instance-level cache state so that two instances
9
+ # have independent caches. Class-level convenience methods delegate to a
10
+ # resettable default instance (reset via ToolLoader.reset_instance! or AIA.reset!).
11
+
12
+ module AIA
13
+ class ToolLoader
14
+ class << self
15
+ # Returns the shared default instance, creating it on first call.
16
+ def instance
17
+ @instance ||= new
18
+ end
19
+
20
+ # Discard the default instance. The next call to .instance creates a fresh one.
21
+ # Called from AIA.reset! for test isolation.
22
+ def reset_instance!
23
+ @instance = nil
24
+ end
25
+
26
+ # Class-level convenience wrappers — delegate to the default instance.
27
+ def cached_tools = instance.cached_tools
28
+ def clear_cache! = instance.clear_cache!
29
+ def load_tools(config) = instance.load_tools(config)
30
+ def filtered_tools(config) = instance.filtered_tools(config)
31
+ def discover_tools = instance.discover_tools
32
+ def eager_load_gem_tools = instance.eager_load_gem_tools
33
+
34
+ def activate_unbundled_gem(name)
35
+ instance.activate_unbundled_gem(name)
36
+ end
37
+ end
38
+
39
+ def initialize
40
+ @tool_cache = nil
41
+ end
42
+
43
+ # Clear the cached tool discovery results.
44
+ # Call when tool paths change via /config directive.
45
+ def clear_cache!
46
+ @tool_cache = nil
47
+ end
48
+
49
+ # Return the current tool cache (nil if not yet populated).
50
+ def cached_tools
51
+ @tool_cache
52
+ end
53
+
54
+ # Load tools from require_libs and tool paths, then cache the result.
55
+ # Subsequent calls to RobotFactory.build skip this entirely if cache exists.
56
+ # :reek:TooManyStatements -- sequential phases: require_libs with activation retry, tool files, eager loads, discovery cache
57
+ def load_tools(config)
58
+ Array(config.require_libs).each do |lib|
59
+ require lib
60
+ rescue LoadError
61
+ # Not yet active — activate it (and its deps) via RubyGems, then retry
62
+ if activate_unbundled_gem(lib)
63
+ require lib rescue warn("Warning: Failed to require '#{lib}' after activation")
64
+ else
65
+ $stderr.puts "Warning: Failed to require '#{lib}': gem not found"
66
+ end
67
+ end
68
+
69
+ # Load tool files from paths
70
+ Array(config.tools&.paths).each do |path|
71
+ expanded = File.expand_path(path)
72
+ if File.exist?(expanded)
73
+ require expanded
74
+ else
75
+ $stderr.puts "Warning: Tool file not found: #{path}"
76
+ end
77
+ rescue LoadError, StandardError => e
78
+ $stderr.puts "Warning: Failed to load tool '#{path}': #{e.message}"
79
+ end
80
+
81
+ # Eagerly load tools from gems that use zeitwerk lazy loading
82
+ # (e.g., shared_tools provides .load_all_tools for this purpose)
83
+ eager_load_gem_tools
84
+
85
+ # Re-register PM directives so any AIA::Directive subclasses defined
86
+ # in just-loaded tool files become available in ERB prompt rendering.
87
+ PM::Directive.register_all if defined?(PM::Directive)
88
+
89
+ # Discover loaded tools via ObjectSpace and cache the result
90
+ tools = discover_tools
91
+ @tool_cache = tools
92
+ config.loaded_tools = tools
93
+ config.tool_names = tools.map { |t| tool_name(t) }.join(', ')
94
+ end
95
+
96
+ # A tool's display name: instances expose #name; bare classes fall back to the class name.
97
+ def tool_name(tool)
98
+ tool.respond_to?(:name) ? tool.name : tool.class.name
99
+ end
100
+
101
+ # Filter tools based on allowed/rejected lists and KBS decisions.
102
+ # :reek:TooManyStatements -- allowed filter, rejected filter, then a name-dedup pass over the tool list
103
+ # :reek:DuplicateMethodCall -- tool_name(t) is applied per element inside three separate filter blocks; no shared scope to hoist into
104
+ def filtered_tools(config)
105
+ tools = config.loaded_tools || []
106
+ allowed = config.tools&.allowed
107
+ rejected = config.tools&.rejected
108
+
109
+ if allowed && !allowed.empty?
110
+ allowed_list = Array(allowed).map(&:strip).map(&:downcase)
111
+ tools = tools.select do |t|
112
+ name = tool_name(t).downcase
113
+ # name is a String; Array#intersect? would raise TypeError
114
+ allowed_list.any? { |a| name.include?(a) }
115
+ end
116
+ end
117
+
118
+ if rejected && !rejected.empty?
119
+ rejected_list = Array(rejected).map(&:strip).map(&:downcase)
120
+ tools = tools.reject do |t|
121
+ name = tool_name(t).downcase
122
+ # name is a String; Array#intersect? would raise TypeError
123
+ rejected_list.any? { |r| name.include?(r) }
124
+ end
125
+ end
126
+
127
+ seen = {}
128
+ tools.select do |t|
129
+ name = tool_name(t)
130
+ if seen[name]
131
+ false
132
+ else
133
+ seen[name] = true
134
+ true
135
+ end
136
+ end
137
+ end
138
+
139
+ # Discover RubyLLM::Tool subclasses from ObjectSpace.
140
+ # Skips tools that report themselves as unavailable via #available?.
141
+ def discover_tools
142
+ ObjectSpace.each_object(Class).select do |klass|
143
+ next false unless defined?(RubyLLM::Tool) && klass < RubyLLM::Tool
144
+ begin
145
+ instance = klass.new
146
+ if instance.respond_to?(:available?) && !instance.available?
147
+ tool_name = instance.respond_to?(:name) ? instance.name : klass.name
148
+ $stderr.puts "Info: Tool '#{tool_name}' is not available, skipping"
149
+ next false
150
+ end
151
+ true
152
+ rescue LoadError, StandardError
153
+ false
154
+ end
155
+ end
156
+ end
157
+
158
+ # Activate a gem that isn't yet active by asking RubyGems to activate it.
159
+ # This adds its lib paths to $LOAD_PATH AND activates its dependencies,
160
+ # unlike a manual $LOAD_PATH manipulation which would leave deps unresolved.
161
+ #
162
+ # Under bundler/setup (dev mode), `gem name` raises Gem::LoadError for any
163
+ # gem not in the bundle. The fallback searches known gem install paths directly
164
+ # and adds the lib dir to $LOAD_PATH so Zeitwerk-based gems still load.
165
+ def activate_unbundled_gem(name)
166
+ gem name
167
+ true
168
+ rescue Gem::LoadError
169
+ lib_dir = [Gem.default_dir, Gem.user_dir].compact.flat_map do |d|
170
+ Dir.glob("#{d}/gems/#{name}-*/lib")
171
+ end.max
172
+ return false unless lib_dir
173
+ $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
174
+ true
175
+ rescue StandardError
176
+ false
177
+ end
178
+
179
+ # Eagerly load tool classes from any required gem that uses lazy loading
180
+ # (e.g. Zeitwerk). Calls .load_all_tools on every module that responds to it,
181
+ # so any gem following that convention works — not just SharedTools.
182
+ #
183
+ # Gem::LoadError (a LoadError, not a StandardError) can fire when a tool's
184
+ # dependency has a version conflict with an already-activated gem. The inner
185
+ # rescue handles that per-module; the fallback loads constants one-at-a-time
186
+ # so tools without conflicting deps still become available.
187
+ def eager_load_gem_tools
188
+ ObjectSpace.each_object(Module) do |mod|
189
+ if mod.respond_to?(:load_all_tools)
190
+ begin
191
+ mod.load_all_tools
192
+ rescue LoadError, StandardError
193
+ eager_load_namespace_fallback(mod)
194
+ end
195
+ end
196
+ rescue LoadError, StandardError
197
+ next
198
+ end
199
+ rescue LoadError, StandardError => e
200
+ $stderr.puts "Warning: Failed to eager-load gem tools: #{e.message}"
201
+ end
202
+
203
+ # Fallback when bulk load_all_tools fails: walk the module's namespace
204
+ # recursively (up to 3 levels) and trigger each constant's autoload
205
+ # individually, skipping any that raise a load error.
206
+ def eager_load_namespace_fallback(mod, depth = 0)
207
+ return if depth > 3
208
+ mod.constants.each do |const_name|
209
+ child = mod.const_get(const_name)
210
+ eager_load_namespace_fallback(child, depth + 1) if child.is_a?(Module)
211
+ rescue LoadError, StandardError
212
+ next
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/tool_utility.rb
4
+ #
5
+ # Pure tool state-query methods mixed into AIA::Utility via `class << self include`.
6
+ # No display or formatting concerns — those live in Utility directly.
7
+
8
+ module AIA
9
+ module ToolUtility
10
+ def tools?
11
+ return true if AIA.config&.tool_names && !AIA.config.tool_names.empty?
12
+ total_tool_count.positive?
13
+ end
14
+
15
+ def total_tool_count
16
+ local = Array(AIA.config&.loaded_tools).size
17
+ return local if AIA.config&.flags&.no_mcp
18
+ mcp = defined?(RubyLLM::MCP) ? RubyLLM::MCP.clients.sum { |_, c| c.tools.count } : 0
19
+ local + mcp
20
+ end
21
+
22
+ def user_tools?
23
+ AIA.config&.tools&.paths&.any?
24
+ end
25
+
26
+ def supports_tools?
27
+ AIA.client&.model&.supports_functions? || false
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/tools/delegate_to_foreman_tool.rb
4
+ #
5
+ # Lightweight tool given to Tobor (and other primary robots) that
6
+ # delegates task board operations to Foreman, a specialist robot
7
+ # with full TaskBoardTool access. Foreman is built on first use
8
+ # and cached for the session.
9
+
10
+ module AIA
11
+ class DelegateToForemanTool < RubyLLM::Tool
12
+ description "Delegate a task management request to Foreman, the task board manager. " \
13
+ "Use this when you need to create, assign, track, or manage tasks. " \
14
+ "Pass a natural language description of what you need done on the task board."
15
+
16
+ param :request, type: :string, required: true,
17
+ desc: "Natural language description of the task board operation " \
18
+ "(e.g., 'create a research task about recessions assigned to Tobor with labels research, economics')"
19
+
20
+ def available?
21
+ AIA.task_coordinator&.available? || false
22
+ end
23
+
24
+ def execute(request:, **_params)
25
+ foreman = self.class.foreman
26
+ result = foreman.run(request)
27
+ result.respond_to?(:reply) ? result.reply : result.to_s
28
+ rescue StandardError => e
29
+ "Foreman error: #{e.message}"
30
+ end
31
+
32
+ class << self
33
+ def foreman
34
+ @foreman ||= build_foreman
35
+ end
36
+
37
+ def reset!
38
+ @foreman = nil
39
+ end
40
+
41
+ private
42
+
43
+ def build_foreman
44
+ model_spec = AIA.config.models.first
45
+ run_config = RobotFactory.build_run_config(AIA.config)
46
+
47
+ RobotFactory.build_robot(
48
+ model_spec,
49
+ name: 'Foreman',
50
+ system_prompt: foreman_system_prompt,
51
+ local_tools: [TaskBoardTool],
52
+ config: run_config
53
+ )
54
+ end
55
+
56
+ def foreman_system_prompt
57
+ <<~PROMPT
58
+ You are Foreman, the task board manager. You manage a shared TrakFlow
59
+ task board where robots can create, claim, and complete tasks.
60
+
61
+ When given a request, use the task_board tool to execute it. Be precise
62
+ and efficient — execute the operation and report the result concisely.
63
+
64
+ You can: create tasks, plan multi-step work, check ready tasks, claim
65
+ tasks, mark tasks complete, block tasks, and report board status.
66
+ PROMPT
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/tools/recruit_robot_tool.rb
4
+ #
5
+ # RubyLLM::Tool that lets the crew chief grow its own crew. The chief can
6
+ # name a new robot, pick its provider/model (or inherit the chief's), and
7
+ # write its system prompt — all through standard tool-use protocol. The
8
+ # newcomer joins the running crew and answers to @name immediately.
9
+
10
+ module AIA
11
+ class RecruitRobotTool < RubyLLM::Tool
12
+ description "Recruit a new robot into the crew. Give it a name, an " \
13
+ "optional provider/model (omit to inherit yours), optional " \
14
+ "skills that define its role, and/or a system prompt. The " \
15
+ "newcomer joins the crew right away and can be addressed with @name."
16
+
17
+ param :name, type: :string, required: true,
18
+ desc: "Unique name for the new robot, e.g. 'researcher'. Reserved: 'crew'."
19
+ param :model, type: :string,
20
+ desc: "Optional provider/model, e.g. 'ollama/qwen3.6:latest' or 'gpt-4o'. " \
21
+ "Omit (or pass '-') to inherit the chief's model and provider."
22
+ param :skills, type: :string,
23
+ desc: "Optional comma-separated skill ids to assign as the robot's role " \
24
+ "(e.g. 'security-review,ruby-style'). Each skill's content becomes " \
25
+ "part of its system prompt. Use this to give crew members distinct jobs."
26
+ param :system_prompt, type: :string,
27
+ desc: "Optional extra system prompt, appended after any skills — its role, " \
28
+ "persona, and instructions."
29
+
30
+ def execute(name:, model: nil, skills: nil, system_prompt: nil)
31
+ robot = AIA::Crew.recruit(build_spec(name, model, skills, system_prompt))
32
+ "Recruited '#{robot.name}' into the crew on #{robot.model || 'the chief\'s model'}. " \
33
+ "Address it with @#{robot.name}."
34
+ rescue AIA::CrewError => e
35
+ "Could not recruit '#{name}': #{e.message}"
36
+ end
37
+
38
+ private
39
+
40
+ # Translate the tool's flat params into a Crew spec, splitting a
41
+ # "provider/model" string into its parts (nil model => inherit the chief's)
42
+ # and a comma-separated skills string into a list.
43
+ def build_spec(name, model, skills, system_prompt)
44
+ parsed_model, provider = SpawnSpecParser.parse_model(model)
45
+ prompt = system_prompt.to_s.strip
46
+ {
47
+ name: name,
48
+ model: parsed_model,
49
+ provider: provider,
50
+ skills: split_skills(skills),
51
+ system_prompt: prompt.empty? ? nil : prompt
52
+ }
53
+ end
54
+
55
+ # "security, ruby-style" => ["security", "ruby-style"]
56
+ def split_skills(skills)
57
+ skills.to_s.split(',').map(&:strip).reject(&:empty?)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/tools/reskill_robot_tool.rb
4
+ #
5
+ # RubyLLM::Tool that lets the crew chief reset one of its crew members to a
6
+ # clean slate and give it a different role. The member keeps its name and model
7
+ # but gets a fresh conversation and a new skill/system prompt — useful when a
8
+ # recruit drifted off task or needs to be repurposed for the next phase.
9
+
10
+ module AIA
11
+ class ReskillRobotTool < RubyLLM::Tool
12
+ description "Reset a crew member to a clean slate and re-skill it. The robot " \
13
+ "keeps its @name and model but gets a fresh conversation and a new " \
14
+ "role from the given skills and/or system prompt. Use this to " \
15
+ "repurpose a member or recover one that went off task."
16
+
17
+ param :name, type: :string, required: true,
18
+ desc: "Name of the existing crew member to reset (cannot be the chief)."
19
+ param :skills, type: :string,
20
+ desc: "Optional comma-separated skill ids to assign as the new role " \
21
+ "(e.g. 'testing,performance')."
22
+ param :system_prompt, type: :string,
23
+ desc: "Optional extra system prompt, appended after any skills."
24
+
25
+ def execute(name:, skills: nil, system_prompt: nil)
26
+ prompt = system_prompt.to_s.strip
27
+ robot = AIA::Crew.reskill(
28
+ name,
29
+ skills: split_skills(skills),
30
+ system_prompt: prompt.empty? ? nil : prompt
31
+ )
32
+ "Reset '#{robot.name}' to a clean slate with its new role. Address it with @#{robot.name}."
33
+ rescue AIA::CrewError => e
34
+ "Could not reskill '#{name}': #{e.message}"
35
+ end
36
+
37
+ private
38
+
39
+ # "testing, performance" => ["testing", "performance"]
40
+ def split_skills(skills)
41
+ skills.to_s.split(',').map(&:strip).reject(&:empty?)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/tools/task_board_tool.rb
4
+ #
5
+ # RubyLLM::Tool that lets robots manage a shared TrakFlow task board.
6
+ # Robots can create tasks for each other, check what's ready, claim
7
+ # work, and report completion — all through standard tool-use protocol.
8
+
9
+ require "json"
10
+
11
+ module AIA
12
+ # :reek:DataClump -- each execute_<action> dispatch target receives the same LLM tool-call context
13
+ class TaskBoardTool < RubyLLM::Tool
14
+ description "Manage a shared task board. Create tasks for other robots, " \
15
+ "check what's ready, claim tasks, and report completion."
16
+
17
+ param :action, type: :string, required: true,
18
+ desc: "One of: create, plan, ready, claim, complete, block, status"
19
+ param :title, type: :string,
20
+ desc: "Task title (for create/plan actions)"
21
+ param :assignee, type: :string,
22
+ desc: "Robot name to assign to (for create/claim/ready)"
23
+ param :labels, type: :string,
24
+ desc: "Comma-separated labels (for create)"
25
+ param :blocked_by, type: :string,
26
+ desc: "Comma-separated blocker task IDs (for create)"
27
+ param :steps, type: :string,
28
+ desc: "JSON array of step objects [{title:, assignee:}] (for plan)"
29
+ param :task_id, type: :string,
30
+ desc: "Task ID (for claim/complete/block)"
31
+ param :result, type: :string,
32
+ desc: "Result summary (for complete) or reason (for block)"
33
+
34
+ # Never auto-discovered onto primary robots. Only Foreman
35
+ # (built by DelegateToForemanTool) receives this tool explicitly.
36
+ def available?
37
+ false
38
+ end
39
+
40
+ def execute(action:, **params)
41
+ coordinator = AIA.task_coordinator
42
+ return "Task board unavailable (TrakFlow not initialized)" unless coordinator&.available?
43
+
44
+ robot_name = params[:_robot_name] || "unknown"
45
+
46
+ case action
47
+ when "create" then execute_create(coordinator, robot_name, params)
48
+ when "plan" then execute_plan(coordinator, robot_name, params)
49
+ when "ready" then execute_ready(coordinator, params)
50
+ when "claim" then execute_claim(coordinator, robot_name, params)
51
+ when "complete" then execute_complete(coordinator, robot_name, params)
52
+ when "block" then execute_block(coordinator, robot_name, params)
53
+ when "status" then coordinator.status_summary || "No tasks"
54
+ else
55
+ "Unknown action: #{action}. Use: create, plan, ready, claim, complete, block, status"
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def execute_create(coordinator, robot_name, params)
62
+ task = coordinator.create_task(
63
+ params[:title],
64
+ assignee: params[:assignee],
65
+ labels: parse_csv(params[:labels]),
66
+ blocked_by: parse_csv(params[:blocked_by]),
67
+ creator: robot_name
68
+ )
69
+ task ? "Created [#{task.id}] '#{task.title}'" : "Failed to create task"
70
+ end
71
+
72
+ def execute_plan(coordinator, robot_name, params)
73
+ steps = JSON.parse(params[:steps] || "[]", symbolize_names: true)
74
+ result = coordinator.create_plan(
75
+ params[:title], steps: steps, creator: robot_name
76
+ )
77
+ result ? "Plan created with #{result[:steps].size} steps" : "Failed to create plan"
78
+ rescue JSON::ParserError => e
79
+ "Invalid steps JSON: #{e.message}"
80
+ end
81
+
82
+ def execute_ready(coordinator, params)
83
+ tasks = coordinator.ready_tasks(robot_name: params[:assignee])
84
+ return "No ready tasks" if tasks.empty?
85
+ tasks.map { |t| "[#{t.id}] #{t.title} (#{t.assignee || 'unassigned'})" }.join("\n")
86
+ end
87
+
88
+ def execute_claim(coordinator, robot_name, params)
89
+ coordinator.claim_task(params[:task_id], robot_name)
90
+ "Claimed task #{params[:task_id]}"
91
+ end
92
+
93
+ def execute_complete(coordinator, robot_name, params)
94
+ coordinator.complete_task(
95
+ params[:task_id],
96
+ result: params[:result] || "Done",
97
+ robot_name: robot_name
98
+ )
99
+ "Completed task #{params[:task_id]}"
100
+ end
101
+
102
+ def execute_block(coordinator, robot_name, params)
103
+ coordinator.block_task(
104
+ params[:task_id],
105
+ reason: params[:result] || "Blocked",
106
+ robot_name: robot_name
107
+ )
108
+ "Blocked task #{params[:task_id]}"
109
+ end
110
+
111
+ def parse_csv(value)
112
+ (value || "").split(",").map(&:strip).reject(&:empty?)
113
+ end
114
+ end
115
+ end