claude_swarm 1.0.1 → 1.0.2

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 +6 -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 +247 -4
  267. data/EXAMPLES.md +0 -164
@@ -0,0 +1,586 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ class Swarm
5
+ # Builder provides a beautiful Ruby DSL for building swarms
6
+ #
7
+ # The DSL combines YAML simplicity with Ruby power, enabling:
8
+ # - Fluent, chainable configuration
9
+ # - Hooks as Ruby blocks OR shell commands
10
+ # - Full Ruby language features (variables, conditionals, loops)
11
+ # - Type-safe, IDE-friendly API
12
+ #
13
+ # @example Basic usage
14
+ # swarm = SwarmSDK.build do
15
+ # name "Dev Team"
16
+ # lead :backend
17
+ #
18
+ # agent :backend do
19
+ # model "gpt-5"
20
+ # prompt "You build APIs"
21
+ # tools :Read, :Write, :Bash
22
+ #
23
+ # # Hook as Ruby block - inline logic!
24
+ # hook :pre_tool_use, matcher: "Bash" do |ctx|
25
+ # SwarmSDK::Hooks::Result.halt("Blocked!") if ctx.tool_call.parameters[:command].include?("rm -rf")
26
+ # end
27
+ # end
28
+ # end
29
+ #
30
+ # swarm.execute("Build auth API")
31
+ class Builder
32
+ # Main entry point for DSL
33
+ #
34
+ # @example
35
+ # swarm = SwarmSDK.build do
36
+ # name "Team"
37
+ # agent :backend { ... }
38
+ # end
39
+ class << self
40
+ def build(&block)
41
+ builder = new
42
+ builder.instance_eval(&block)
43
+ builder.build_swarm
44
+ end
45
+ end
46
+
47
+ def initialize
48
+ @swarm_name = nil
49
+ @lead_agent = nil
50
+ @agents = {}
51
+ @all_agents_config = nil
52
+ @swarm_hooks = []
53
+ @nodes = {}
54
+ @start_node = nil
55
+ @scratchpad_enabled = true # Default: enabled
56
+ end
57
+
58
+ # Set swarm name
59
+ def name(swarm_name)
60
+ @swarm_name = swarm_name
61
+ end
62
+
63
+ # Set lead agent
64
+ def lead(agent_name)
65
+ @lead_agent = agent_name
66
+ end
67
+
68
+ # Enable or disable shared scratchpad
69
+ #
70
+ # @param enabled [Boolean] Whether to enable scratchpad tools
71
+ def use_scratchpad(enabled)
72
+ @scratchpad_enabled = enabled
73
+ end
74
+
75
+ # Define an agent with fluent API or load from markdown content
76
+ #
77
+ # Supports two forms:
78
+ # 1. Inline DSL: agent :name do ... end
79
+ # 2. Markdown content: agent :name, <<~MD ... MD
80
+ #
81
+ # The name parameter is always required. If the markdown has a name field
82
+ # in frontmatter, it will be replaced by the name parameter.
83
+ #
84
+ # @example Inline DSL
85
+ # agent :backend do
86
+ # model "gpt-5"
87
+ # system_prompt "You build APIs"
88
+ # tools :Read, :Write
89
+ #
90
+ # hook :pre_tool_use, matcher: "Bash" do |ctx|
91
+ # # Inline validation logic!
92
+ # end
93
+ # end
94
+ #
95
+ # @example Markdown content
96
+ # agent :backend, <<~MD
97
+ # ---
98
+ # description: "Backend developer"
99
+ # model: "gpt-4"
100
+ # ---
101
+ #
102
+ # You build APIs.
103
+ # MD
104
+ def agent(name, content = nil, &block)
105
+ # Case 1: agent :name, <<~MD do ... end (markdown + overrides)
106
+ if content.is_a?(String) && block_given? && markdown_content?(content)
107
+ load_agent_from_markdown_with_overrides(content, name, &block)
108
+ # Case 2: agent :name, <<~MD (markdown only)
109
+ elsif content.is_a?(String) && !block_given? && markdown_content?(content)
110
+ load_agent_from_markdown(content, name)
111
+ # Case 3: agent :name do ... end (inline DSL)
112
+ elsif block_given?
113
+ builder = Agent::Builder.new(name)
114
+ builder.instance_eval(&block)
115
+ @agents[name] = builder
116
+ else
117
+ raise ArgumentError, "Invalid agent definition. Use: agent :name { ... } OR agent :name, <<~MD ... MD OR agent :name, <<~MD do ... end"
118
+ end
119
+ end
120
+
121
+ # Add swarm-level hook (swarm_start, swarm_stop only)
122
+ #
123
+ # @example Shell command
124
+ # hook :swarm_start, command: "echo 'Starting' >> log.txt"
125
+ #
126
+ # @example Ruby block
127
+ # hook :swarm_start do |ctx|
128
+ # puts "Swarm starting: #{ctx.metadata[:prompt]}"
129
+ # end
130
+ def hook(event, command: nil, timeout: nil, &block)
131
+ # Validate swarm-level events
132
+ unless [:swarm_start, :swarm_stop].include?(event)
133
+ raise ArgumentError, "Invalid swarm-level hook: #{event}. Only :swarm_start and :swarm_stop allowed at swarm level. Use all_agents { hook ... } or agent { hook ... } for other events."
134
+ end
135
+
136
+ @swarm_hooks << { event: event, command: command, timeout: timeout, block: block }
137
+ end
138
+
139
+ # Configure all agents with a block
140
+ #
141
+ # @example
142
+ # all_agents do
143
+ # tools :Read, :Write
144
+ #
145
+ # hook :pre_tool_use, matcher: "Write" do |ctx|
146
+ # # Validation for all agents
147
+ # end
148
+ # end
149
+ def all_agents(&block)
150
+ builder = AllAgentsBuilder.new
151
+ builder.instance_eval(&block)
152
+ @all_agents_config = builder
153
+ end
154
+
155
+ # Define a node (mini-swarm execution stage)
156
+ #
157
+ # Nodes enable multi-stage workflows where different agent teams
158
+ # collaborate in sequence. Each node is an independent swarm execution.
159
+ #
160
+ # @param name [Symbol] Node name
161
+ # @yield Block for node configuration
162
+ # @return [void]
163
+ #
164
+ # @example Solo agent node
165
+ # node :planning do
166
+ # agent(:architect)
167
+ # end
168
+ #
169
+ # @example Multi-agent node with delegation
170
+ # node :implementation do
171
+ # agent(:backend).delegates_to(:tester, :database)
172
+ # agent(:tester).delegates_to(:database)
173
+ # agent(:database)
174
+ # after :planning
175
+ # end
176
+ def node(name, &block)
177
+ builder = Node::Builder.new(name)
178
+ builder.instance_eval(&block)
179
+ @nodes[name] = builder
180
+ end
181
+
182
+ # Set the starting node for workflow execution
183
+ #
184
+ # Required when nodes are defined. Specifies which node to execute first.
185
+ #
186
+ # @param name [Symbol] Name of starting node
187
+ # @return [void]
188
+ #
189
+ # @example
190
+ # start_node :planning
191
+ def start_node(name)
192
+ @start_node = name.to_sym
193
+ end
194
+
195
+ # Build the actual Swarm instance or NodeOrchestrator
196
+ def build_swarm
197
+ raise ConfigurationError, "Swarm name not set. Use: name 'My Swarm'" unless @swarm_name
198
+
199
+ # Check if nodes are defined
200
+ if @nodes.any?
201
+ # Node-based workflow (agents optional for agent-less workflows)
202
+ build_node_orchestrator
203
+ else
204
+ # Traditional single-swarm execution (requires agents and lead)
205
+ raise ConfigurationError, "No agents defined. Use: agent :name { ... }" if @agents.empty?
206
+ raise ConfigurationError, "Lead agent not set. Use: lead :agent_name" unless @lead_agent
207
+
208
+ build_single_swarm
209
+ end
210
+ end
211
+
212
+ private
213
+
214
+ # Check if a string is markdown content (has frontmatter)
215
+ #
216
+ # @param str [String] String to check
217
+ # @return [Boolean] true if string contains markdown frontmatter
218
+ def markdown_content?(str)
219
+ str.start_with?("---") || str.include?("\n---\n")
220
+ end
221
+
222
+ # Load an agent from markdown content
223
+ #
224
+ # Returns a hash of the agent config (not a Definition yet) so that
225
+ # all_agents config can be applied later in the build process.
226
+ #
227
+ # @param content [String] Markdown content with frontmatter
228
+ # @param name_override [Symbol, nil] Optional name to override frontmatter name
229
+ # @return [void]
230
+ def load_agent_from_markdown(content, name_override = nil)
231
+ # Parse markdown content - will extract name from frontmatter if not overridden
232
+ definition = MarkdownParser.parse(content, name_override)
233
+
234
+ # Store the config hash (not Definition) so all_agents can be applied
235
+ # We'll wrap this in a special marker so we know it came from markdown
236
+ @agents[definition.name] = { __file_config__: definition.to_h }
237
+ end
238
+
239
+ # Load an agent from markdown content with DSL overrides
240
+ #
241
+ # This allows loading from a file and then overriding specific settings:
242
+ # agent :reviewer, File.read("reviewer.md") do
243
+ # provider :openai
244
+ # model "gpt-4o"
245
+ # end
246
+ #
247
+ # @param content [String] Markdown content with frontmatter
248
+ # @param name_override [Symbol, nil] Optional name to override frontmatter name
249
+ # @yield Block with DSL overrides
250
+ # @return [void]
251
+ def load_agent_from_markdown_with_overrides(content, name_override = nil, &block)
252
+ # Parse markdown content first
253
+ definition = MarkdownParser.parse(content, name_override)
254
+
255
+ # Create a builder with the markdown config
256
+ builder = Agent::Builder.new(definition.name)
257
+
258
+ # Apply markdown settings to builder (these become the base)
259
+ apply_definition_to_builder(builder, definition.to_h)
260
+
261
+ # Apply DSL overrides (these override the markdown settings)
262
+ builder.instance_eval(&block)
263
+
264
+ # Store the builder (not file config) so overrides are preserved
265
+ @agents[definition.name] = builder
266
+ end
267
+
268
+ # Apply agent definition hash to a builder
269
+ #
270
+ # @param builder [Agent::Builder] Builder to configure
271
+ # @param config [Hash] Configuration hash from definition
272
+ # @return [void]
273
+ def apply_definition_to_builder(builder, config)
274
+ builder.description(config[:description]) if config[:description]
275
+ builder.model(config[:model]) if config[:model]
276
+ builder.provider(config[:provider]) if config[:provider]
277
+ builder.base_url(config[:base_url]) if config[:base_url]
278
+ builder.api_version(config[:api_version]) if config[:api_version]
279
+ builder.context_window(config[:context_window]) if config[:context_window]
280
+ builder.system_prompt(config[:system_prompt]) if config[:system_prompt]
281
+ builder.directory(config[:directory]) if config[:directory]
282
+ builder.timeout(config[:timeout]) if config[:timeout]
283
+ builder.parameters(config[:parameters]) if config[:parameters]
284
+ builder.headers(config[:headers]) if config[:headers]
285
+ builder.coding_agent(config[:coding_agent]) unless config[:coding_agent].nil?
286
+ # Don't apply assume_model_exists from markdown - let DSL overrides or auto-enable handle it
287
+ # builder.assume_model_exists(config[:assume_model_exists]) unless config[:assume_model_exists].nil?
288
+ builder.bypass_permissions(config[:bypass_permissions]) if config[:bypass_permissions]
289
+ builder.disable_default_tools(config[:disable_default_tools]) unless config[:disable_default_tools].nil?
290
+
291
+ # Add tools from markdown
292
+ if config[:tools]&.any?
293
+ # Extract tool names from the tools array (which may be hashes with permissions)
294
+ tool_names = config[:tools].map do |tool|
295
+ tool.is_a?(Hash) ? tool[:name] : tool
296
+ end
297
+ builder.tools(*tool_names)
298
+ end
299
+
300
+ # Add delegates_to
301
+ builder.delegates_to(*config[:delegates_to]) if config[:delegates_to]&.any?
302
+
303
+ # Add MCP servers
304
+ config[:mcp_servers]&.each do |server|
305
+ builder.mcp_server(server[:name], **server.except(:name))
306
+ end
307
+ end
308
+
309
+ # Build a traditional single-swarm execution
310
+ #
311
+ # @return [Swarm] Configured swarm instance
312
+ def build_single_swarm
313
+ # Create swarm using SDK
314
+ swarm = Swarm.new(name: @swarm_name, scratchpad_enabled: @scratchpad_enabled)
315
+
316
+ # Merge all_agents config into each agent (including file-loaded ones)
317
+ merge_all_agents_config_into_agents if @all_agents_config
318
+
319
+ # Build definitions and add to swarm
320
+ # Handle both Agent::Builder (inline DSL) and file configs (from files)
321
+ @agents.each do |agent_name, agent_builder_or_config|
322
+ definition = if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
323
+ # File-loaded agent config (with all_agents merged)
324
+ Agent::Definition.new(agent_name, agent_builder_or_config[:__file_config__])
325
+ else
326
+ # Builder object (from inline DSL) - convert to definition
327
+ agent_builder_or_config.to_definition
328
+ end
329
+
330
+ swarm.add_agent(definition)
331
+ end
332
+
333
+ # Set lead
334
+ swarm.lead = @lead_agent
335
+
336
+ # Apply swarm hooks (Ruby blocks)
337
+ # These are swarm-level hooks (swarm_start, swarm_stop)
338
+ @swarm_hooks.each do |hook_config|
339
+ apply_swarm_hook(swarm, hook_config)
340
+ end
341
+
342
+ # Apply all_agents hooks (Ruby blocks)
343
+ # These become swarm-level default callbacks that apply to all agents
344
+ @all_agents_config&.hooks&.each do |hook_config|
345
+ apply_all_agents_hook(swarm, hook_config)
346
+ end
347
+
348
+ # NOTE: Agent-specific hooks are already stored in Agent::Definition.callbacks
349
+ # They'll be applied automatically during agent initialization (pass_4_configure_hooks)
350
+ # This ensures they're applied at the right time, after LogStream is set up
351
+
352
+ swarm
353
+ end
354
+
355
+ # Build a node-based workflow orchestrator
356
+ #
357
+ # @return [NodeOrchestrator] Configured orchestrator
358
+ def build_node_orchestrator
359
+ raise ConfigurationError, "start_node required when nodes are defined. Use: start_node :name" unless @start_node
360
+
361
+ # Merge all_agents config into each agent (applies to all nodes)
362
+ merge_all_agents_config_into_agents if @all_agents_config
363
+
364
+ # Build agent definitions
365
+ # Handle both Agent::Builder (inline DSL) and file configs (from files)
366
+ agent_definitions = {}
367
+ @agents.each do |agent_name, agent_builder_or_config|
368
+ agent_definitions[agent_name] = if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
369
+ # File-loaded agent config (with all_agents merged)
370
+ Agent::Definition.new(agent_name, agent_builder_or_config[:__file_config__])
371
+ else
372
+ # Builder object (from inline DSL) - convert to definition
373
+ agent_builder_or_config.to_definition
374
+ end
375
+ end
376
+
377
+ # Create node orchestrator
378
+ NodeOrchestrator.new(
379
+ swarm_name: @swarm_name,
380
+ agent_definitions: agent_definitions,
381
+ nodes: @nodes,
382
+ start_node: @start_node,
383
+ )
384
+ end
385
+
386
+ # Merge all_agents configuration into each agent
387
+ #
388
+ # All_agents values are used as defaults - agent-specific values override.
389
+ # This applies to both inline DSL agents (Builder) and file-loaded agents (config hash).
390
+ #
391
+ # @return [void]
392
+ def merge_all_agents_config_into_agents
393
+ return unless @all_agents_config
394
+
395
+ all_agents_hash = @all_agents_config.to_h
396
+
397
+ @agents.each_value do |agent_builder_or_config|
398
+ if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
399
+ # File-loaded agent - merge into the config hash
400
+ file_config = agent_builder_or_config[:__file_config__]
401
+
402
+ # Merge all_agents into file config (file config overrides)
403
+ # Use same merge strategy as Configuration class
404
+ merged_config = merge_all_agents_into_config(all_agents_hash, file_config)
405
+
406
+ # Update the stored config
407
+ agent_builder_or_config[:__file_config__] = merged_config
408
+ else
409
+ # Builder object (inline DSL agent)
410
+ agent_builder = agent_builder_or_config
411
+
412
+ # Apply all_agents defaults that haven't been set at agent level
413
+ # Agent values override all_agents values
414
+ apply_all_agents_defaults(agent_builder, all_agents_hash)
415
+
416
+ # Merge tools (prepend all_agents tools)
417
+ all_agents_tools = @all_agents_config.tools_list
418
+ agent_builder.prepend_tools(*all_agents_tools) if all_agents_tools.any?
419
+
420
+ # Pass all_agents permissions as default_permissions
421
+ if @all_agents_config.permissions_config.any?
422
+ agent_builder.default_permissions = @all_agents_config.permissions_config
423
+ end
424
+ end
425
+ end
426
+ end
427
+
428
+ # Merge all_agents config into file-loaded agent config
429
+ #
430
+ # Follows same merge strategy as Configuration class:
431
+ # - Arrays (tools, delegates_to): Concatenate (all_agents + file)
432
+ # - Hashes (parameters, headers): Merge (file values override)
433
+ # - Scalars (model, provider, etc.): File overrides
434
+ #
435
+ # @param all_agents_hash [Hash] All_agents configuration
436
+ # @param file_config [Hash] File-loaded agent configuration
437
+ # @return [Hash] Merged configuration
438
+ def merge_all_agents_into_config(all_agents_hash, file_config)
439
+ merged = all_agents_hash.dup
440
+
441
+ file_config.each do |key, value|
442
+ case key
443
+ when :tools
444
+ # Concatenate tools: all_agents.tools + file.tools
445
+ merged[:tools] = Array(merged[:tools]) + Array(value)
446
+ when :delegates_to
447
+ # Concatenate delegates_to
448
+ merged[:delegates_to] = Array(merged[:delegates_to]) + Array(value)
449
+ when :parameters
450
+ # Merge parameters: file values override all_agents
451
+ merged[:parameters] = (merged[:parameters] || {}).merge(value || {})
452
+ when :headers
453
+ # Merge headers: file values override all_agents
454
+ merged[:headers] = (merged[:headers] || {}).merge(value || {})
455
+ else
456
+ # For everything else, file value overrides all_agents value
457
+ merged[key] = value
458
+ end
459
+ end
460
+
461
+ # Pass all_agents permissions as default_permissions
462
+ if all_agents_hash[:permissions] && !merged[:default_permissions]
463
+ merged[:default_permissions] = all_agents_hash[:permissions]
464
+ end
465
+
466
+ merged
467
+ end
468
+
469
+ # Apply all_agents defaults to an agent builder
470
+ #
471
+ # Only sets values that haven't been explicitly set at the agent level.
472
+ # This implements the override semantics: agent values take precedence.
473
+ #
474
+ # @param agent_builder [Agent::Builder] The agent builder to configure
475
+ # @param all_agents_hash [Hash] All_agents configuration
476
+ # @return [void]
477
+ def apply_all_agents_defaults(agent_builder, all_agents_hash)
478
+ # Model: only set if agent hasn't explicitly set it
479
+ if all_agents_hash[:model] && !agent_builder.model_set?
480
+ agent_builder.model(all_agents_hash[:model])
481
+ end
482
+
483
+ # Provider: only set if agent hasn't set it
484
+ if all_agents_hash[:provider] && !agent_builder.provider_set?
485
+ agent_builder.provider(all_agents_hash[:provider])
486
+ end
487
+
488
+ # Base URL: only set if agent hasn't set it
489
+ if all_agents_hash[:base_url] && !agent_builder.base_url_set?
490
+ agent_builder.base_url(all_agents_hash[:base_url])
491
+ end
492
+
493
+ # API Version: only set if agent hasn't set it
494
+ if all_agents_hash[:api_version] && !agent_builder.api_version_set?
495
+ agent_builder.api_version(all_agents_hash[:api_version])
496
+ end
497
+
498
+ # Timeout: only set if agent hasn't set it
499
+ if all_agents_hash[:timeout] && !agent_builder.timeout_set?
500
+ agent_builder.timeout(all_agents_hash[:timeout])
501
+ end
502
+
503
+ # Parameters: merge (all_agents + agent, agent values override)
504
+ if all_agents_hash[:parameters]
505
+ merged_params = all_agents_hash[:parameters].merge(agent_builder.parameters)
506
+ agent_builder.parameters(merged_params)
507
+ end
508
+
509
+ # Headers: merge (all_agents + agent, agent values override)
510
+ if all_agents_hash[:headers]
511
+ merged_headers = all_agents_hash[:headers].merge(agent_builder.headers)
512
+ agent_builder.headers(merged_headers)
513
+ end
514
+
515
+ # Coding_agent: only set if agent hasn't set it
516
+ if !all_agents_hash[:coding_agent].nil? && !agent_builder.coding_agent_set?
517
+ agent_builder.coding_agent(all_agents_hash[:coding_agent])
518
+ end
519
+ end
520
+
521
+ def apply_swarm_hook(swarm, config)
522
+ event = config[:event]
523
+
524
+ if config[:block]
525
+ # Ruby block hook - register directly
526
+ swarm.add_default_callback(event, &config[:block])
527
+ elsif config[:command]
528
+ # Shell command hook - use ShellExecutor
529
+ swarm.add_default_callback(event) do |context|
530
+ input_json = build_hook_input(context, event)
531
+ Hooks::ShellExecutor.execute(
532
+ command: config[:command],
533
+ input_json: input_json,
534
+ timeout: config[:timeout] || 60,
535
+ swarm_name: swarm.name,
536
+ event: event,
537
+ )
538
+ end
539
+ end
540
+ end
541
+
542
+ def apply_all_agents_hook(swarm, config)
543
+ event = config[:event]
544
+ matcher = config[:matcher]
545
+
546
+ if config[:block]
547
+ # Ruby block hook
548
+ swarm.add_default_callback(event, matcher: matcher, &config[:block])
549
+ elsif config[:command]
550
+ # Shell command hook
551
+ swarm.add_default_callback(event, matcher: matcher) do |context|
552
+ input_json = build_hook_input(context, event)
553
+ Hooks::ShellExecutor.execute(
554
+ command: config[:command],
555
+ input_json: input_json,
556
+ timeout: config[:timeout] || 60,
557
+ agent_name: context.agent_name,
558
+ swarm_name: swarm.name,
559
+ event: event,
560
+ )
561
+ end
562
+ end
563
+ end
564
+
565
+ def build_hook_input(context, event)
566
+ # Build JSON input for shell hooks (similar to HooksAdapter)
567
+ base = { event: event.to_s }
568
+
569
+ case event
570
+ when :pre_tool_use
571
+ base.merge(tool: context.tool_call.name, parameters: context.tool_call.parameters)
572
+ when :post_tool_use
573
+ base.merge(result: context.tool_result.content, success: context.tool_result.success?)
574
+ when :user_prompt
575
+ base.merge(prompt: context.metadata[:prompt])
576
+ when :swarm_start
577
+ base.merge(prompt: context.metadata[:prompt])
578
+ when :swarm_stop
579
+ base.merge(success: context.metadata[:success], duration: context.metadata[:duration])
580
+ else
581
+ base
582
+ end
583
+ end
584
+ end
585
+ end
586
+ end