claude_swarm 1.0.1 → 1.0.4

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 (267) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/release.md +1 -1
  3. data/.claude/hooks/lint-code-files.rb +65 -0
  4. data/.rubocop.yml +22 -2
  5. data/CHANGELOG.md +14 -1
  6. data/CLAUDE.md +1 -1
  7. data/CONTRIBUTING.md +69 -0
  8. data/README.md +27 -2
  9. data/Rakefile +71 -3
  10. data/analyze_coverage.rb +94 -0
  11. data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
  12. data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
  13. data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
  14. data/docs/v2/README.md +308 -0
  15. data/docs/v2/guides/claude-code-agents.md +262 -0
  16. data/docs/v2/guides/complete-tutorial.md +3088 -0
  17. data/docs/v2/guides/getting-started.md +1456 -0
  18. data/docs/v2/guides/memory-adapters.md +998 -0
  19. data/docs/v2/guides/plugins.md +816 -0
  20. data/docs/v2/guides/quick-start-cli.md +1745 -0
  21. data/docs/v2/guides/rails-integration.md +1902 -0
  22. data/docs/v2/guides/swarm-memory.md +599 -0
  23. data/docs/v2/reference/cli.md +729 -0
  24. data/docs/v2/reference/ruby-dsl.md +2154 -0
  25. data/docs/v2/reference/yaml.md +1835 -0
  26. data/docs-team-swarm.yml +2222 -0
  27. data/examples/learning-assistant/assistant.md +7 -0
  28. data/examples/learning-assistant/example-memories/concept-example.md +90 -0
  29. data/examples/learning-assistant/example-memories/experience-example.md +66 -0
  30. data/examples/learning-assistant/example-memories/fact-example.md +76 -0
  31. data/examples/learning-assistant/example-memories/memory-index.md +78 -0
  32. data/examples/learning-assistant/example-memories/skill-example.md +168 -0
  33. data/examples/learning-assistant/learning_assistant.rb +34 -0
  34. data/examples/learning-assistant/learning_assistant.yml +20 -0
  35. data/examples/v2/dsl/01_basic.rb +44 -0
  36. data/examples/v2/dsl/02_core_parameters.rb +59 -0
  37. data/examples/v2/dsl/03_capabilities.rb +71 -0
  38. data/examples/v2/dsl/04_llm_parameters.rb +56 -0
  39. data/examples/v2/dsl/05_advanced_flags.rb +73 -0
  40. data/examples/v2/dsl/06_permissions.rb +80 -0
  41. data/examples/v2/dsl/07_mcp_server.rb +62 -0
  42. data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
  43. data/examples/v2/dsl/09_agent_hooks.rb +67 -0
  44. data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
  45. data/examples/v2/dsl/11_delegation.rb +60 -0
  46. data/examples/v2/dsl/12_complete_integration.rb +137 -0
  47. data/examples/v2/file_tools_swarm.yml +102 -0
  48. data/examples/v2/hooks/01_basic_hooks.rb +133 -0
  49. data/examples/v2/hooks/02_usage_tracking.rb +201 -0
  50. data/examples/v2/hooks/03_production_monitoring.rb +429 -0
  51. data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
  52. data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
  53. data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
  54. data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
  55. data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
  56. data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
  57. data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
  58. data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
  59. data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
  60. data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
  61. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
  62. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
  63. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
  64. data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
  65. data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
  66. data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
  67. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
  68. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
  69. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
  70. data/examples/v2/hooks/swarm_summary.sh +44 -0
  71. data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
  72. data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
  73. data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
  74. data/examples/v2/hooks/validate_bash.rb +59 -0
  75. data/examples/v2/multi_directory_permissions.yml +221 -0
  76. data/examples/v2/node_context_demo.rb +127 -0
  77. data/examples/v2/node_workflow.rb +173 -0
  78. data/examples/v2/path_resolution_demo.rb +216 -0
  79. data/examples/v2/simple-swarm-v2.rb +90 -0
  80. data/examples/v2/simple-swarm-v2.yml +62 -0
  81. data/examples/v2/swarm.yml +71 -0
  82. data/examples/v2/swarm_with_hooks.yml +61 -0
  83. data/examples/v2/swarm_with_hooks_simple.yml +25 -0
  84. data/examples/v2/think_tool_demo.rb +62 -0
  85. data/exe/swarm +6 -0
  86. data/lib/claude_swarm/claude_mcp_server.rb +0 -6
  87. data/lib/claude_swarm/cli.rb +10 -3
  88. data/lib/claude_swarm/commands/ps.rb +19 -20
  89. data/lib/claude_swarm/commands/show.rb +1 -1
  90. data/lib/claude_swarm/configuration.rb +10 -12
  91. data/lib/claude_swarm/mcp_generator.rb +10 -1
  92. data/lib/claude_swarm/orchestrator.rb +73 -49
  93. data/lib/claude_swarm/system_utils.rb +37 -11
  94. data/lib/claude_swarm/version.rb +1 -1
  95. data/lib/claude_swarm/worktree_manager.rb +1 -0
  96. data/lib/claude_swarm/yaml_loader.rb +22 -0
  97. data/lib/claude_swarm.rb +7 -2
  98. data/lib/swarm_cli/cli.rb +201 -0
  99. data/lib/swarm_cli/command_registry.rb +61 -0
  100. data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
  101. data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
  102. data/lib/swarm_cli/commands/migrate.rb +55 -0
  103. data/lib/swarm_cli/commands/run.rb +173 -0
  104. data/lib/swarm_cli/config_loader.rb +97 -0
  105. data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
  106. data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
  107. data/lib/swarm_cli/interactive_repl.rb +918 -0
  108. data/lib/swarm_cli/mcp_serve_options.rb +44 -0
  109. data/lib/swarm_cli/mcp_tools_options.rb +59 -0
  110. data/lib/swarm_cli/migrate_options.rb +54 -0
  111. data/lib/swarm_cli/migrator.rb +132 -0
  112. data/lib/swarm_cli/options.rb +151 -0
  113. data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
  114. data/lib/swarm_cli/ui/components/content_block.rb +120 -0
  115. data/lib/swarm_cli/ui/components/divider.rb +57 -0
  116. data/lib/swarm_cli/ui/components/panel.rb +62 -0
  117. data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
  118. data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
  119. data/lib/swarm_cli/ui/formatters/number.rb +58 -0
  120. data/lib/swarm_cli/ui/formatters/text.rb +77 -0
  121. data/lib/swarm_cli/ui/formatters/time.rb +73 -0
  122. data/lib/swarm_cli/ui/icons.rb +59 -0
  123. data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
  124. data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
  125. data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
  126. data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
  127. data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
  128. data/lib/swarm_cli/version.rb +5 -0
  129. data/lib/swarm_cli.rb +44 -0
  130. data/lib/swarm_memory/adapters/base.rb +141 -0
  131. data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
  132. data/lib/swarm_memory/chat_extension.rb +34 -0
  133. data/lib/swarm_memory/cli/commands.rb +306 -0
  134. data/lib/swarm_memory/core/entry.rb +37 -0
  135. data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
  136. data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
  137. data/lib/swarm_memory/core/path_normalizer.rb +75 -0
  138. data/lib/swarm_memory/core/semantic_index.rb +244 -0
  139. data/lib/swarm_memory/core/storage.rb +288 -0
  140. data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
  141. data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
  142. data/lib/swarm_memory/dsl/memory_config.rb +113 -0
  143. data/lib/swarm_memory/embeddings/embedder.rb +36 -0
  144. data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
  145. data/lib/swarm_memory/errors.rb +21 -0
  146. data/lib/swarm_memory/integration/cli_registration.rb +30 -0
  147. data/lib/swarm_memory/integration/configuration.rb +43 -0
  148. data/lib/swarm_memory/integration/registration.rb +31 -0
  149. data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
  150. data/lib/swarm_memory/optimization/analyzer.rb +244 -0
  151. data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
  152. data/lib/swarm_memory/prompts/memory.md.erb +109 -0
  153. data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
  154. data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
  155. data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
  156. data/lib/swarm_memory/search/semantic_search.rb +112 -0
  157. data/lib/swarm_memory/search/text_search.rb +42 -0
  158. data/lib/swarm_memory/search/text_similarity.rb +80 -0
  159. data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
  160. data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
  161. data/lib/swarm_memory/tools/load_skill.rb +313 -0
  162. data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
  163. data/lib/swarm_memory/tools/memory_delete.rb +99 -0
  164. data/lib/swarm_memory/tools/memory_edit.rb +185 -0
  165. data/lib/swarm_memory/tools/memory_glob.rb +160 -0
  166. data/lib/swarm_memory/tools/memory_grep.rb +247 -0
  167. data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
  168. data/lib/swarm_memory/tools/memory_read.rb +123 -0
  169. data/lib/swarm_memory/tools/memory_write.rb +231 -0
  170. data/lib/swarm_memory/utils.rb +50 -0
  171. data/lib/swarm_memory/version.rb +5 -0
  172. data/lib/swarm_memory.rb +166 -0
  173. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  174. data/lib/swarm_sdk/agent/builder.rb +461 -0
  175. data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
  176. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  177. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
  178. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
  179. data/lib/swarm_sdk/agent/chat.rb +1159 -0
  180. data/lib/swarm_sdk/agent/context.rb +112 -0
  181. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  182. data/lib/swarm_sdk/agent/definition.rb +556 -0
  183. data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
  184. data/lib/swarm_sdk/configuration.rb +296 -0
  185. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  186. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  187. data/lib/swarm_sdk/context_compactor.rb +340 -0
  188. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  189. data/lib/swarm_sdk/hooks/context.rb +197 -0
  190. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  191. data/lib/swarm_sdk/hooks/error.rb +29 -0
  192. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  193. data/lib/swarm_sdk/hooks/registry.rb +147 -0
  194. data/lib/swarm_sdk/hooks/result.rb +150 -0
  195. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  196. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  197. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  198. data/lib/swarm_sdk/log_collector.rb +51 -0
  199. data/lib/swarm_sdk/log_stream.rb +69 -0
  200. data/lib/swarm_sdk/markdown_parser.rb +75 -0
  201. data/lib/swarm_sdk/model_aliases.json +5 -0
  202. data/lib/swarm_sdk/models.json +1 -0
  203. data/lib/swarm_sdk/models.rb +120 -0
  204. data/lib/swarm_sdk/node/agent_config.rb +49 -0
  205. data/lib/swarm_sdk/node/builder.rb +439 -0
  206. data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
  207. data/lib/swarm_sdk/node_context.rb +170 -0
  208. data/lib/swarm_sdk/node_orchestrator.rb +384 -0
  209. data/lib/swarm_sdk/permissions/config.rb +239 -0
  210. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  211. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  212. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  213. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  214. data/lib/swarm_sdk/plugin.rb +147 -0
  215. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  216. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
  217. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  218. data/lib/swarm_sdk/result.rb +97 -0
  219. data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
  220. data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
  221. data/lib/swarm_sdk/swarm/builder.rb +586 -0
  222. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  223. data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
  224. data/lib/swarm_sdk/swarm.rb +982 -0
  225. data/lib/swarm_sdk/tools/bash.rb +274 -0
  226. data/lib/swarm_sdk/tools/clock.rb +44 -0
  227. data/lib/swarm_sdk/tools/delegate.rb +164 -0
  228. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  229. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  230. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
  231. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  232. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  233. data/lib/swarm_sdk/tools/edit.rb +150 -0
  234. data/lib/swarm_sdk/tools/glob.rb +158 -0
  235. data/lib/swarm_sdk/tools/grep.rb +228 -0
  236. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  237. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  238. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  239. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  240. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  241. data/lib/swarm_sdk/tools/read.rb +251 -0
  242. data/lib/swarm_sdk/tools/registry.rb +93 -0
  243. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
  244. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
  245. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
  246. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  247. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
  248. data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
  249. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  250. data/lib/swarm_sdk/tools/think.rb +95 -0
  251. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  252. data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
  253. data/lib/swarm_sdk/tools/write.rb +117 -0
  254. data/lib/swarm_sdk/utils.rb +50 -0
  255. data/lib/swarm_sdk/version.rb +5 -0
  256. data/lib/swarm_sdk.rb +157 -0
  257. data/llm.v2.txt +13407 -0
  258. data/rubocop/cop/security/no_reflection_methods.rb +47 -0
  259. data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
  260. data/swarm_cli.gemspec +57 -0
  261. data/swarm_memory.gemspec +28 -0
  262. data/swarm_sdk.gemspec +41 -0
  263. data/team.yml +1 -1
  264. data/team_full.yml +1875 -0
  265. data/{team_v2.yml → team_sdk.yml} +121 -52
  266. metadata +249 -6
  267. data/EXAMPLES.md +0 -164
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Node
5
+ # AgentConfig provides fluent API for configuring agents within a node
6
+ #
7
+ # This class enables the chainable syntax:
8
+ # agent(:backend).delegates_to(:tester, :database)
9
+ #
10
+ # @example Basic delegation
11
+ # agent(:backend).delegates_to(:tester)
12
+ #
13
+ # @example No delegation (solo agent)
14
+ # agent(:planner)
15
+ class AgentConfig
16
+ attr_reader :agent_name
17
+
18
+ def initialize(agent_name, node_builder)
19
+ @agent_name = agent_name
20
+ @node_builder = node_builder
21
+ @delegates_to = []
22
+ @finalized = false
23
+ end
24
+
25
+ # Set delegation targets for this agent
26
+ #
27
+ # @param agent_names [Array<Symbol>] Names of agents to delegate to
28
+ # @return [self] For method chaining
29
+ def delegates_to(*agent_names)
30
+ @delegates_to = agent_names.map(&:to_sym)
31
+ finalize
32
+ self
33
+ end
34
+
35
+ # Finalize agent configuration (called automatically)
36
+ #
37
+ # Registers this agent configuration with the parent node builder.
38
+ # If delegates_to was never called, registers with empty delegation.
39
+ #
40
+ # @return [void]
41
+ def finalize
42
+ return if @finalized
43
+
44
+ @node_builder.register_agent(@agent_name, @delegates_to)
45
+ @finalized = true
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,439 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Node
5
+ # Builder provides DSL for configuring nodes (mini-swarms within a workflow)
6
+ #
7
+ # A node represents a stage in a multi-step workflow where a specific set
8
+ # of agents collaborate. Each node creates an independent swarm execution.
9
+ #
10
+ # @example Solo agent node
11
+ # node :planning do
12
+ # agent(:architect)
13
+ # end
14
+ #
15
+ # @example Multi-agent node with delegation
16
+ # node :implementation do
17
+ # agent(:backend).delegates_to(:tester, :database)
18
+ # agent(:tester).delegates_to(:database)
19
+ # agent(:database)
20
+ #
21
+ # depends_on :planning
22
+ # end
23
+ class Builder
24
+ attr_reader :name,
25
+ :agent_configs,
26
+ :dependencies,
27
+ :lead_override,
28
+ :input_transformer,
29
+ :output_transformer,
30
+ :input_transformer_command,
31
+ :output_transformer_command
32
+
33
+ def initialize(name)
34
+ @name = name
35
+ @agent_configs = []
36
+ @dependencies = []
37
+ @lead_override = nil
38
+ @input_transformer = nil # Ruby block
39
+ @output_transformer = nil # Ruby block
40
+ @input_transformer_command = nil # Bash command
41
+ @output_transformer_command = nil # Bash command
42
+ end
43
+
44
+ # Configure an agent for this node
45
+ #
46
+ # Returns an AgentConfig object that supports fluent delegation syntax.
47
+ # If delegates_to is not called, the agent is registered with no delegation.
48
+ #
49
+ # @param name [Symbol] Agent name
50
+ # @return [AgentConfig] Fluent configuration object
51
+ #
52
+ # @example With delegation
53
+ # agent(:backend).delegates_to(:tester, :database)
54
+ #
55
+ # @example Without delegation
56
+ # agent(:planner)
57
+ def agent(name)
58
+ config = AgentConfig.new(name, self)
59
+
60
+ # Register immediately with empty delegation
61
+ # If delegates_to is called later, it will update this
62
+ register_agent(name, [])
63
+
64
+ config
65
+ end
66
+
67
+ # Register an agent configuration (called by AgentConfig)
68
+ #
69
+ # @param agent_name [Symbol] Agent name
70
+ # @param delegates_to [Array<Symbol>] Delegation targets
71
+ # @return [void]
72
+ def register_agent(agent_name, delegates_to)
73
+ # Check if agent already registered
74
+ existing = @agent_configs.find { |ac| ac[:agent] == agent_name }
75
+
76
+ if existing
77
+ # Update delegation (happens when delegates_to is called after agent())
78
+ existing[:delegates_to] = delegates_to
79
+ else
80
+ # Add new agent configuration
81
+ @agent_configs << { agent: agent_name, delegates_to: delegates_to }
82
+ end
83
+ end
84
+
85
+ # Declare dependencies (nodes that must execute before this one)
86
+ #
87
+ # @param node_names [Array<Symbol>] Names of prerequisite nodes
88
+ # @return [void]
89
+ #
90
+ # @example Single dependency
91
+ # depends_on :planning
92
+ #
93
+ # @example Multiple dependencies
94
+ # depends_on :frontend, :backend
95
+ def depends_on(*node_names)
96
+ @dependencies.concat(node_names.map(&:to_sym))
97
+ end
98
+
99
+ # Override the lead agent (first agent is lead by default)
100
+ #
101
+ # @param agent_name [Symbol] Name of agent to make lead
102
+ # @return [void]
103
+ #
104
+ # @example
105
+ # agent(:backend).delegates_to(:tester)
106
+ # agent(:tester)
107
+ # lead :tester # tester is lead instead of backend
108
+ def lead(agent_name)
109
+ @lead_override = agent_name.to_sym
110
+ end
111
+
112
+ # Define input transformer for this node
113
+ #
114
+ # The transformer receives a NodeContext object with access to:
115
+ # - Previous node's result (convenience: ctx.content)
116
+ # - Original user prompt (ctx.original_prompt)
117
+ # - All previous node results (ctx.all_results[:node_name])
118
+ # - Current node metadata (ctx.node_name, ctx.dependencies)
119
+ #
120
+ # Can also be used for side effects (logging, file I/O) since the block
121
+ # runs at execution time, not declaration time.
122
+ #
123
+ # **Skip Execution**: Return a hash with `skip_execution: true` to skip
124
+ # the node's swarm execution and immediately return the provided content.
125
+ # Useful for caching, validation, or conditional execution.
126
+ #
127
+ # @yield [NodeContext] Context with previous results and metadata
128
+ # @return [String, Hash] Transformed input OR skip hash
129
+ #
130
+ # @example Access previous result and original prompt
131
+ # input do |ctx|
132
+ # # Convenience accessor
133
+ # previous_content = ctx.content
134
+ #
135
+ # # Access original prompt
136
+ # "Original: #{ctx.original_prompt}\nPrevious: #{previous_content}"
137
+ # end
138
+ #
139
+ # @example Access results from specific nodes
140
+ # input do |ctx|
141
+ # plan = ctx.all_results[:planning].content
142
+ # design = ctx.all_results[:design].content
143
+ #
144
+ # "Implement based on:\nPlan: #{plan}\nDesign: #{design}"
145
+ # end
146
+ #
147
+ # @example Skip execution (caching)
148
+ # input do |ctx|
149
+ # cached = check_cache(ctx.content)
150
+ # if cached
151
+ # # Skip LLM call, return cached result
152
+ # { skip_execution: true, content: cached }
153
+ # else
154
+ # ctx.content
155
+ # end
156
+ # end
157
+ #
158
+ # @example Skip execution (validation)
159
+ # input do |ctx|
160
+ # if ctx.content.length > 10000
161
+ # # Fail early without LLM call
162
+ # { skip_execution: true, content: "ERROR: Input too long" }
163
+ # else
164
+ # ctx.content
165
+ # end
166
+ # end
167
+ def input(&block)
168
+ @input_transformer = block
169
+ end
170
+
171
+ # Set input transformer as bash command (YAML API)
172
+ #
173
+ # The command receives NodeContext as JSON on STDIN and outputs transformed content.
174
+ #
175
+ # **Exit codes:**
176
+ # - 0: Success, use STDOUT as transformed content
177
+ # - 1: Skip node execution, use current_input unchanged (STDOUT ignored)
178
+ # - 2: Halt workflow with error, show STDERR (STDOUT ignored)
179
+ #
180
+ # @param command [String] Bash command to execute
181
+ # @param timeout [Integer] Timeout in seconds (default: 60)
182
+ # @return [void]
183
+ #
184
+ # @example
185
+ # input_command("scripts/validate.sh", timeout: 30)
186
+ def input_command(command, timeout: TransformerExecutor::DEFAULT_TIMEOUT)
187
+ @input_transformer_command = { command: command, timeout: timeout }
188
+ end
189
+
190
+ # Define output transformer for this node
191
+ #
192
+ # The transformer receives a NodeContext object with access to:
193
+ # - Current node's result (convenience: ctx.content)
194
+ # - Original user prompt (ctx.original_prompt)
195
+ # - All completed node results (ctx.all_results[:node_name])
196
+ # - Current node metadata (ctx.node_name)
197
+ #
198
+ # Can also be used for side effects (logging, file I/O) since the block
199
+ # runs at execution time, not declaration time.
200
+ #
201
+ # @yield [NodeContext] Context with current result and metadata
202
+ # @return [String] Transformed output
203
+ #
204
+ # @example Transform and save to file
205
+ # output do |ctx|
206
+ # # Side effect: save to file
207
+ # File.write("results/plan.txt", ctx.content)
208
+ #
209
+ # # Return transformed output for next node
210
+ # "Key decisions: #{extract_decisions(ctx.content)}"
211
+ # end
212
+ #
213
+ # @example Access original prompt
214
+ # output do |ctx|
215
+ # # Include original context in output
216
+ # "Task: #{ctx.original_prompt}\nResult: #{ctx.content}"
217
+ # end
218
+ #
219
+ # @example Access multiple node results
220
+ # output do |ctx|
221
+ # plan = ctx.all_results[:planning].content
222
+ # impl = ctx.content
223
+ #
224
+ # "Completed:\nPlan: #{plan}\nImpl: #{impl}"
225
+ # end
226
+ def output(&block)
227
+ @output_transformer = block
228
+ end
229
+
230
+ # Set output transformer as bash command (YAML API)
231
+ #
232
+ # The command receives NodeContext as JSON on STDIN and outputs transformed content.
233
+ #
234
+ # **Exit codes:**
235
+ # - 0: Success, use STDOUT as transformed content
236
+ # - 1: Pass through unchanged, use result.content (STDOUT ignored)
237
+ # - 2: Halt workflow with error, show STDERR (STDOUT ignored)
238
+ #
239
+ # @param command [String] Bash command to execute
240
+ # @param timeout [Integer] Timeout in seconds (default: 60)
241
+ # @return [void]
242
+ #
243
+ # @example
244
+ # output_command("scripts/format.sh", timeout: 30)
245
+ def output_command(command, timeout: TransformerExecutor::DEFAULT_TIMEOUT)
246
+ @output_transformer_command = { command: command, timeout: timeout }
247
+ end
248
+
249
+ # Check if node has any input transformer (block or command)
250
+ #
251
+ # @return [Boolean]
252
+ def has_input_transformer?
253
+ @input_transformer || @input_transformer_command
254
+ end
255
+
256
+ # Check if node has any output transformer (block or command)
257
+ #
258
+ # @return [Boolean]
259
+ def has_output_transformer?
260
+ @output_transformer || @output_transformer_command
261
+ end
262
+
263
+ # Transform input using configured transformer (block or command)
264
+ #
265
+ # Executes either Ruby block or bash command transformer.
266
+ #
267
+ # **Exit code behavior (bash commands only):**
268
+ # - Exit 0: Use STDOUT as transformed content
269
+ # - Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
270
+ # - Exit 2: Halt workflow with error (STDOUT ignored)
271
+ #
272
+ # @param context [NodeContext] Context with previous results and metadata
273
+ # @param current_input [String] Fallback content for exit 1 (skip), also used for halt error context
274
+ # @return [String, Hash] Transformed input OR skip hash `{ skip_execution: true, content: "..." }`
275
+ # @raise [ConfigurationError] If bash transformer halts workflow (exit 2)
276
+ def transform_input(context, current_input:)
277
+ # No transformer configured: return content as-is
278
+ return context.content unless @input_transformer || @input_transformer_command
279
+
280
+ # Ruby block transformer
281
+ # Ruby blocks can return String (transformed content) OR Hash (skip_execution)
282
+ if @input_transformer
283
+ return @input_transformer.call(context)
284
+ end
285
+
286
+ # Bash command transformer
287
+ # Bash commands use exit codes to control behavior:
288
+ # - Exit 0: Success, use STDOUT as transformed content
289
+ # - Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
290
+ # - Exit 2: Halt workflow with error (STDOUT ignored)
291
+ if @input_transformer_command
292
+ result = TransformerExecutor.execute(
293
+ command: @input_transformer_command[:command],
294
+ context: context,
295
+ event: "input",
296
+ node_name: @name,
297
+ fallback_content: current_input, # Used for exit 1 (skip)
298
+ timeout: @input_transformer_command[:timeout],
299
+ )
300
+
301
+ # Handle transformer result based on exit code
302
+ if result.halt?
303
+ # Exit 2: Halt workflow with error
304
+ raise ConfigurationError,
305
+ "Input transformer halted workflow for node '#{@name}': #{result.error_message}"
306
+ elsif result.skip_execution?
307
+ # Exit 1: Skip node execution, return skip hash
308
+ # Content is current_input unchanged (STDOUT was ignored)
309
+ { skip_execution: true, content: result.content }
310
+ else
311
+ # Exit 0: Return transformed content from STDOUT
312
+ result.content
313
+ end
314
+ end
315
+ end
316
+
317
+ # Transform output using configured transformer (block or command)
318
+ #
319
+ # Executes either Ruby block or bash command transformer.
320
+ #
321
+ # **Exit code behavior (bash commands only):**
322
+ # - Exit 0: Use STDOUT as transformed content
323
+ # - Exit 1: Pass through unchanged, use result.content (STDOUT ignored)
324
+ # - Exit 2: Halt workflow with error (STDOUT ignored)
325
+ #
326
+ # @param context [NodeContext] Context with current result and metadata
327
+ # @return [String] Transformed output
328
+ # @raise [ConfigurationError] If bash transformer halts workflow (exit 2)
329
+ def transform_output(context)
330
+ # No transformer configured: return content as-is
331
+ return context.content unless @output_transformer || @output_transformer_command
332
+
333
+ # Ruby block transformer
334
+ # Simply calls the block with context and returns result
335
+ if @output_transformer
336
+ return @output_transformer.call(context)
337
+ end
338
+
339
+ # Bash command transformer
340
+ # Bash commands use exit codes to control behavior:
341
+ # - Exit 0: Success, use STDOUT as transformed content
342
+ # - Exit 1: Pass through unchanged, use result.content (STDOUT ignored)
343
+ # - Exit 2: Halt workflow with error from STDERR (STDOUT ignored)
344
+ if @output_transformer_command
345
+ result = TransformerExecutor.execute(
346
+ command: @output_transformer_command[:command],
347
+ context: context,
348
+ event: "output",
349
+ node_name: @name,
350
+ fallback_content: context.content, # result.content for exit 1
351
+ timeout: @output_transformer_command[:timeout],
352
+ )
353
+
354
+ # Handle transformer result based on exit code
355
+ if result.halt?
356
+ # Exit 2: Halt workflow with error
357
+ raise ConfigurationError,
358
+ "Output transformer halted workflow for node '#{@name}': #{result.error_message}"
359
+ else
360
+ # Exit 0: Return transformed content from STDOUT
361
+ # Exit 1: Return fallback (result.content unchanged)
362
+ result.content
363
+ end
364
+ end
365
+ end
366
+
367
+ # Get the lead agent for this node
368
+ #
369
+ # @return [Symbol] Lead agent name
370
+ def lead_agent
371
+ @lead_override || @agent_configs.first&.dig(:agent)
372
+ end
373
+
374
+ # Check if this is an agent-less (computation-only) node
375
+ #
376
+ # Agent-less nodes run pure Ruby code without LLM execution.
377
+ # They must have at least one transformer (input or output).
378
+ #
379
+ # @return [Boolean]
380
+ def agent_less?
381
+ @agent_configs.empty?
382
+ end
383
+
384
+ # Validate node configuration
385
+ #
386
+ # Also auto-adds agents that are referenced in delegates_to but not explicitly declared.
387
+ # This allows writing: agent(:backend).delegates_to(:verifier)
388
+ # without needing: agent(:verifier)
389
+ #
390
+ # @return [void]
391
+ # @raise [ConfigurationError] If configuration is invalid
392
+ def validate!
393
+ # Auto-add agents mentioned in delegates_to but not explicitly declared
394
+ auto_add_delegate_agents
395
+
396
+ # Agent-less nodes (pure computation) are allowed but need transformers
397
+ if @agent_configs.empty?
398
+ unless has_input_transformer? || has_output_transformer?
399
+ raise ConfigurationError,
400
+ "Agent-less node '#{@name}' must have at least one transformer (input or output). " \
401
+ "Either add agents with agent(:name) or add input/output transformers."
402
+ end
403
+ end
404
+
405
+ # If has agents, validate lead override
406
+ if @lead_override && !@agent_configs.any? { |ac| ac[:agent] == @lead_override }
407
+ raise ConfigurationError,
408
+ "Node '#{@name}' lead agent '#{@lead_override}' not found in node's agents"
409
+ end
410
+ end
411
+
412
+ private
413
+
414
+ # Auto-add agents that are mentioned in delegates_to but not explicitly declared
415
+ #
416
+ # This allows:
417
+ # agent(:backend).delegates_to(:tester)
418
+ # Without needing:
419
+ # agent(:tester)
420
+ #
421
+ # The tester agent is automatically added to the node with no delegation.
422
+ #
423
+ # @return [void]
424
+ def auto_add_delegate_agents
425
+ # Collect all agents mentioned in delegates_to
426
+ all_delegates = @agent_configs.flat_map { |ac| ac[:delegates_to] }.uniq
427
+
428
+ # Find delegates that aren't explicitly declared
429
+ declared_agents = @agent_configs.map { |ac| ac[:agent] }
430
+ missing_delegates = all_delegates - declared_agents
431
+
432
+ # Auto-add missing delegates with empty delegation
433
+ missing_delegates.each do |delegate_name|
434
+ @agent_configs << { agent: delegate_name, delegates_to: [] }
435
+ end
436
+ end
437
+ end
438
+ end
439
+ end