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,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ # Adapter for converting Claude Code agent markdown files to SwarmSDK format
5
+ #
6
+ # Claude Code agent files use a different syntax and conventions than SwarmSDK:
7
+ # - Tools are comma-separated strings instead of arrays
8
+ # - Model shortcuts like 'sonnet', 'opus', 'haiku' instead of full model IDs
9
+ # - Tool permissions like 'Write(src/**)' instead of SwarmSDK's permission system
10
+ # - Required 'name' field in frontmatter
11
+ #
12
+ # This adapter:
13
+ # - Detects Claude Code format by checking frontmatter markers
14
+ # - Converts tools from comma-separated strings to arrays
15
+ # - Maps model shortcuts to canonical model IDs
16
+ # - Strips unsupported tool permission syntax with warnings
17
+ # - Sets coding_agent: true by default
18
+ # - Warns about unsupported fields
19
+ #
20
+ # @example Parse a Claude Code agent file
21
+ # content = File.read('.claude/agents/reviewer.md')
22
+ # config = ClaudeCodeAgentAdapter.parse(content, :reviewer)
23
+ # agent = Agent::Definition.new(:reviewer, config)
24
+ #
25
+ class ClaudeCodeAgentAdapter
26
+ # Fields supported in Claude Code agent frontmatter
27
+ SUPPORTED_FIELDS = ["name", "description", "tools", "model"].freeze
28
+
29
+ # SwarmSDK documentation URL for reference
30
+ SWARM_SDK_DOCS_URL = "https://github.com/parruda/claude-swarm/blob/main/docs/v2/README.md"
31
+
32
+ # Pattern to detect tool permission syntax like Write(src/**)
33
+ TOOL_PERMISSION_PATTERN = /^([A-Za-z_]+)\([^)]+\)$/
34
+
35
+ class << self
36
+ # Detect if content appears to be in Claude Code agent format
37
+ #
38
+ # Detection is based on tools field type:
39
+ # - Claude Code: tools is a comma-separated string (e.g., "Read, Write, Bash")
40
+ # - SwarmSDK: tools is an array (e.g., [Read, Write, Bash])
41
+ #
42
+ # Note: The 'name' field alone is not sufficient since SwarmSDK also supports it
43
+ #
44
+ # @param content [String] Markdown content with YAML frontmatter
45
+ # @return [Boolean] true if content appears to be Claude Code format
46
+ def claude_code_format?(content)
47
+ return false unless content =~ /\A---\s*\n(.*?)\n---\s*\n/m
48
+
49
+ frontmatter_yaml = Regexp.last_match(1)
50
+ frontmatter = YAML.safe_load(frontmatter_yaml, permitted_classes: [Symbol], aliases: true)
51
+
52
+ return false unless frontmatter.is_a?(Hash)
53
+
54
+ # Only detect as Claude Code if tools field is a comma-separated string
55
+ # This is the most reliable indicator since SwarmSDK always uses arrays
56
+ frontmatter.key?("tools") && frontmatter["tools"].is_a?(String)
57
+ rescue Psych::SyntaxError
58
+ false
59
+ end
60
+
61
+ # Parse Claude Code agent markdown and convert to SwarmSDK format
62
+ #
63
+ # @param content [String] Markdown content with YAML frontmatter
64
+ # @param agent_name [Symbol, String] Name of the agent
65
+ # @param inherit_model [String, nil] Model to use when frontmatter has 'inherit'
66
+ # @return [Hash] Configuration hash suitable for Agent::Definition.new
67
+ # @raise [ConfigurationError] if content format is invalid
68
+ def parse(content, agent_name, inherit_model: nil)
69
+ new(inherit_model: inherit_model).parse(content, agent_name)
70
+ end
71
+ end
72
+
73
+ # Initialize adapter with optional context
74
+ #
75
+ # @param inherit_model [String, nil] Model to use when frontmatter has 'inherit'
76
+ def initialize(inherit_model: nil)
77
+ @inherit_model = inherit_model
78
+ @warnings = []
79
+ end
80
+
81
+ # Parse Claude Code agent content
82
+ #
83
+ # @param content [String] Markdown content with YAML frontmatter
84
+ # @param agent_name [Symbol, String] Name of the agent
85
+ # @return [Hash] Configuration hash for Agent::Definition
86
+ # @raise [ConfigurationError] if format is invalid
87
+ def parse(content, agent_name)
88
+ unless content =~ /\A---\s*\n(.*?)\n---\s*\n(.*)\z/m
89
+ raise ConfigurationError, "Invalid Claude Code agent format. Expected YAML frontmatter followed by prompt content."
90
+ end
91
+
92
+ frontmatter_yaml = Regexp.last_match(1)
93
+ prompt_content = Regexp.last_match(2).strip
94
+
95
+ frontmatter = YAML.safe_load(frontmatter_yaml, permitted_classes: [Symbol], aliases: true)
96
+
97
+ unless frontmatter.is_a?(Hash)
98
+ raise ConfigurationError, "Invalid frontmatter format in Claude Code agent file"
99
+ end
100
+
101
+ config = build_config(frontmatter, prompt_content, agent_name)
102
+ emit_warnings(agent_name)
103
+ config
104
+ end
105
+
106
+ private
107
+
108
+ # Build SwarmSDK configuration from Claude Code frontmatter
109
+ def build_config(frontmatter, prompt_content, agent_name)
110
+ warn_unknown_fields(frontmatter)
111
+
112
+ config = {
113
+ description: frontmatter["description"],
114
+ system_prompt: prompt_content,
115
+ coding_agent: true, # Default for Claude Code agents
116
+ }
117
+
118
+ # Parse tools if present
119
+ if frontmatter["tools"]
120
+ config[:tools] = parse_tools(frontmatter["tools"])
121
+ end
122
+
123
+ # Parse model if present
124
+ if frontmatter["model"]
125
+ config[:model] = resolve_model(frontmatter["model"])
126
+ end
127
+
128
+ config
129
+ end
130
+
131
+ # Parse tools field - handles both comma-separated string and array
132
+ #
133
+ # @param tools_field [String, Array] Tools from frontmatter
134
+ # @return [Array<String>] Array of tool names
135
+ def parse_tools(tools_field)
136
+ tools_array = if tools_field.is_a?(String)
137
+ tools_field.split(",").map(&:strip)
138
+ else
139
+ Array(tools_field).map(&:to_s)
140
+ end
141
+
142
+ # Clean tool permissions and collect warnings
143
+ tools_array.map { |tool| clean_tool_permissions(tool) }.compact
144
+ end
145
+
146
+ # Strip tool permission syntax and warn if detected
147
+ #
148
+ # @param tool_string [String] Tool name, possibly with permissions like 'Write(src/**)'
149
+ # @return [String, nil] Clean tool name, or nil if invalid
150
+ def clean_tool_permissions(tool_string)
151
+ if tool_string =~ TOOL_PERMISSION_PATTERN
152
+ tool_name = Regexp.last_match(1)
153
+ @warnings << "Tool permission syntax '#{tool_string}' detected in agent file. SwarmSDK supports permissions but uses different syntax. Using '#{tool_name}' without restrictions for now. See SwarmSDK documentation for permission configuration: #{SWARM_SDK_DOCS_URL}"
154
+ tool_name
155
+ else
156
+ tool_string
157
+ end
158
+ end
159
+
160
+ # Resolve model shortcuts to canonical model IDs
161
+ #
162
+ # Uses SwarmSDK::Models.resolve_alias to map shortcuts like 'sonnet'
163
+ # to the latest model IDs from model_aliases.json.
164
+ #
165
+ # @param model_field [String] Model from frontmatter
166
+ # @return [String, Symbol] Canonical model ID or :inherit symbol
167
+ def resolve_model(model_field)
168
+ model_str = model_field.to_s.strip
169
+
170
+ # Handle 'inherit' keyword
171
+ return :inherit if model_str == "inherit"
172
+
173
+ # Resolve using SwarmSDK model aliases
174
+ # This maps 'sonnet' → 'claude-sonnet-4-5-20250929', etc.
175
+ SwarmSDK::Models.resolve_alias(model_str)
176
+ end
177
+
178
+ # Warn about unknown frontmatter fields
179
+ def warn_unknown_fields(frontmatter)
180
+ unknown_fields = frontmatter.keys - SUPPORTED_FIELDS
181
+
182
+ unknown_fields.each do |field|
183
+ @warnings << case field
184
+ when "hooks"
185
+ "Hooks configuration detected in agent frontmatter. SwarmSDK handles hooks at the swarm level. See: #{SWARM_SDK_DOCS_URL}"
186
+ else
187
+ "Unknown field '#{field}' in Claude Code agent file. Ignoring. Supported fields: #{SUPPORTED_FIELDS.join(", ")}"
188
+ end
189
+ end
190
+ end
191
+
192
+ # Emit all collected warnings via LogCollector
193
+ def emit_warnings(agent_name)
194
+ return if @warnings.empty?
195
+
196
+ @warnings.each do |warning|
197
+ LogCollector.emit(
198
+ type: "claude_code_conversion_warning",
199
+ agent: agent_name,
200
+ message: warning,
201
+ )
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ class Configuration
5
+ ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
6
+
7
+ attr_reader :config_path, :swarm_name, :lead_agent, :agents, :all_agents_config, :swarm_hooks, :all_agents_hooks, :scratchpad_enabled
8
+
9
+ class << self
10
+ def load(path)
11
+ new(path).tap(&:load_and_validate)
12
+ end
13
+ end
14
+
15
+ def initialize(config_path)
16
+ @config_path = Pathname.new(config_path).expand_path
17
+ @config_dir = @config_path.dirname
18
+ @agents = {}
19
+ @all_agents_config = {} # Settings applied to all agents
20
+ @swarm_hooks = {} # Swarm-level hooks (swarm_start, swarm_stop)
21
+ @all_agents_hooks = {} # Hooks applied to all agents
22
+ end
23
+
24
+ def load_and_validate
25
+ @config = YAML.load_file(@config_path, aliases: true)
26
+
27
+ unless @config.is_a?(Hash)
28
+ raise ConfigurationError, "Invalid YAML syntax: configuration must be a Hash"
29
+ end
30
+
31
+ @config = Utils.symbolize_keys(@config)
32
+ interpolate_env_vars!(@config)
33
+ validate_version
34
+ load_all_agents_config
35
+ load_hooks_config
36
+ validate_swarm
37
+ load_agents
38
+ detect_circular_dependencies
39
+ self
40
+ rescue Errno::ENOENT
41
+ raise ConfigurationError, "Configuration file not found: #{@config_path}"
42
+ rescue Psych::SyntaxError => e
43
+ raise ConfigurationError, "Invalid YAML syntax: #{e.message}"
44
+ end
45
+
46
+ def agent_names
47
+ @agents.keys
48
+ end
49
+
50
+ def connections_for(agent_name)
51
+ @agents[agent_name]&.delegates_to || []
52
+ end
53
+
54
+ # Convert configuration to Swarm instance using Ruby API
55
+ #
56
+ # This method bridges YAML configuration to the Ruby API, making YAML
57
+ # a thin convenience layer over the programmatic interface.
58
+ #
59
+ # @return [Swarm] Configured swarm instance
60
+ def to_swarm
61
+ swarm = Swarm.new(
62
+ name: @swarm_name,
63
+ global_concurrency: Swarm::DEFAULT_GLOBAL_CONCURRENCY,
64
+ default_local_concurrency: Swarm::DEFAULT_LOCAL_CONCURRENCY,
65
+ scratchpad_enabled: @scratchpad_enabled,
66
+ )
67
+
68
+ # Add all agents - pass definitions directly
69
+ @agents.each do |_name, agent_def|
70
+ swarm.add_agent(agent_def)
71
+ end
72
+
73
+ # Set lead agent
74
+ swarm.lead = @lead_agent
75
+
76
+ swarm
77
+ end
78
+
79
+ private
80
+
81
+ def interpolate_env_vars!(obj)
82
+ case obj
83
+ when String
84
+ interpolate_env_string(obj)
85
+ when Hash
86
+ obj.transform_values! { |v| interpolate_env_vars!(v) }
87
+ when Array
88
+ obj.map! { |v| interpolate_env_vars!(v) }
89
+ else
90
+ obj
91
+ end
92
+ end
93
+
94
+ def interpolate_env_string(str)
95
+ str.gsub(ENV_VAR_WITH_DEFAULT_PATTERN) do |_match|
96
+ env_var = Regexp.last_match(1)
97
+ has_default = Regexp.last_match(2)
98
+ default_value = Regexp.last_match(3)
99
+
100
+ if ENV.key?(env_var)
101
+ ENV[env_var]
102
+ elsif has_default
103
+ default_value || ""
104
+ else
105
+ raise ConfigurationError, "Environment variable '#{env_var}' is not set"
106
+ end
107
+ end
108
+ end
109
+
110
+ def validate_version
111
+ version = @config[:version]
112
+ raise ConfigurationError, "Missing 'version' field in configuration" unless version
113
+ raise ConfigurationError, "SwarmSDK requires version: 2 configuration. Got version: #{version}" unless version == 2
114
+ end
115
+
116
+ def load_all_agents_config
117
+ return unless @config[:swarm]
118
+
119
+ @all_agents_config = @config[:swarm][:all_agents] || {}
120
+
121
+ # Convert disable_default_tools array elements to symbols
122
+ if @all_agents_config[:disable_default_tools].is_a?(Array)
123
+ @all_agents_config[:disable_default_tools] = @all_agents_config[:disable_default_tools].map(&:to_sym)
124
+ end
125
+ end
126
+
127
+ def load_hooks_config
128
+ return unless @config[:swarm]
129
+
130
+ # Load swarm-level hooks (only swarm_start, swarm_stop allowed)
131
+ @swarm_hooks = Utils.symbolize_keys(@config[:swarm][:hooks] || {})
132
+
133
+ # Load all_agents hooks (applied as swarm defaults)
134
+ if @config[:swarm][:all_agents]
135
+ @all_agents_hooks = Utils.symbolize_keys(@config[:swarm][:all_agents][:hooks] || {})
136
+ end
137
+ end
138
+
139
+ def validate_swarm
140
+ raise ConfigurationError, "Missing 'swarm' field in configuration" unless @config[:swarm]
141
+
142
+ swarm = @config[:swarm]
143
+ raise ConfigurationError, "Missing 'name' field in swarm configuration" unless swarm[:name]
144
+ raise ConfigurationError, "Missing 'agents' field in swarm configuration" unless swarm[:agents]
145
+ raise ConfigurationError, "Missing 'lead' field in swarm configuration" unless swarm[:lead]
146
+ raise ConfigurationError, "No agents defined" if swarm[:agents].empty?
147
+
148
+ @swarm_name = swarm[:name]
149
+ @lead_agent = swarm[:lead].to_sym # Convert to symbol for consistency
150
+ @scratchpad_enabled = swarm[:use_scratchpad].nil? ? true : swarm[:use_scratchpad] # Default: enabled
151
+ end
152
+
153
+ def load_agents
154
+ swarm_agents = @config[:swarm][:agents]
155
+
156
+ swarm_agents.each do |name, agent_config|
157
+ # Support three formats:
158
+ # 1. String: assistant: "agents/assistant.md" (file path)
159
+ # 2. Hash with agent_file: assistant: { agent_file: "..." }
160
+ # 3. Hash with inline definition: assistant: { description: "...", model: "..." }
161
+
162
+ if agent_config.is_a?(String)
163
+ # Format 1: Direct file path as string
164
+ file_path = agent_config
165
+ merged_config = merge_all_agents_config({})
166
+ @agents[name] = load_agent_from_file(name, file_path, merged_config)
167
+ else
168
+ # Format 2 or 3: Hash configuration
169
+ agent_config ||= {}
170
+
171
+ # Merge all_agents_config into agent config
172
+ # Agent-specific config overrides all_agents config
173
+ merged_config = merge_all_agents_config(agent_config)
174
+
175
+ @agents[name] = if agent_config[:agent_file]
176
+ # Format 2: Hash with agent_file key
177
+ load_agent_from_file(name, agent_config[:agent_file], merged_config)
178
+ else
179
+ # Format 3: Inline definition
180
+ Agent::Definition.new(name, merged_config)
181
+ end
182
+ end
183
+ end
184
+
185
+ unless @agents.key?(@lead_agent)
186
+ raise ConfigurationError, "Lead agent '#{@lead_agent}' not found in agents"
187
+ end
188
+ end
189
+
190
+ # Merge all_agents config with agent-specific config
191
+ # Agent config takes precedence over all_agents config
192
+ #
193
+ # Merge strategy:
194
+ # - Arrays (tools, delegates_to): Concatenate
195
+ # - Hashes (parameters, headers): Merge (agent values override)
196
+ # - Scalars (model, provider, base_url, timeout, coding_agent): Agent overrides
197
+ #
198
+ # @param agent_config [Hash] Agent-specific configuration
199
+ # @return [Hash] Merged configuration
200
+ def merge_all_agents_config(agent_config)
201
+ merged = @all_agents_config.dup
202
+
203
+ # For arrays, concatenate
204
+ # For hashes, merge (agent values override)
205
+ # For scalars, agent value overrides
206
+ agent_config.each do |key, value|
207
+ case key
208
+ when :tools
209
+ # Concatenate tools: all_agents.tools + agent.tools
210
+ merged[:tools] = Array(merged[:tools]) + Array(value)
211
+ when :delegates_to
212
+ # Concatenate delegates_to
213
+ merged[:delegates_to] = Array(merged[:delegates_to]) + Array(value)
214
+ when :parameters
215
+ # Merge parameters: all_agents.parameters + agent.parameters
216
+ # Agent values override all_agents values for same keys
217
+ merged[:parameters] = (merged[:parameters] || {}).merge(value || {})
218
+ when :headers
219
+ # Merge headers: all_agents.headers + agent.headers
220
+ # Agent values override all_agents values for same keys
221
+ merged[:headers] = (merged[:headers] || {}).merge(value || {})
222
+ when :disable_default_tools
223
+ # Convert array elements to symbols if it's an array
224
+ merged[key] = value.is_a?(Array) ? value.map(&:to_sym) : value
225
+ else
226
+ # For everything else (model, provider, base_url, timeout, coding_agent, etc.),
227
+ # agent value overrides all_agents value
228
+ merged[key] = value
229
+ end
230
+ end
231
+
232
+ # Pass all_agents permissions as default_permissions for backward compat with AgentDefinition
233
+ if @all_agents_config[:permissions]
234
+ merged[:default_permissions] = @all_agents_config[:permissions]
235
+ end
236
+
237
+ merged
238
+ end
239
+
240
+ def load_agent_from_file(name, file_path, merged_config)
241
+ agent_file_path = resolve_agent_file_path(file_path)
242
+
243
+ unless File.exist?(agent_file_path)
244
+ raise ConfigurationError, "Agent file not found: #{agent_file_path}"
245
+ end
246
+
247
+ content = File.read(agent_file_path)
248
+ # Parse markdown and merge with YAML config
249
+ agent_def_from_file = MarkdownParser.parse(content, name)
250
+
251
+ # Merge: YAML config overrides markdown file (YAML takes precedence)
252
+ # This allows YAML to override any settings from the markdown file
253
+ final_config = agent_def_from_file.to_h.compact.merge(merged_config.compact)
254
+
255
+ Agent::Definition.new(name, final_config)
256
+ rescue StandardError => e
257
+ raise ConfigurationError, "Error loading agent '#{name}' from file '#{file_path}': #{e.message}"
258
+ end
259
+
260
+ def resolve_agent_file_path(file_path)
261
+ return file_path if Pathname.new(file_path).absolute?
262
+
263
+ @config_dir.join(file_path).to_s
264
+ end
265
+
266
+ def detect_circular_dependencies
267
+ @agents.each_key do |agent_name|
268
+ visited = Set.new
269
+ path = []
270
+ detect_cycle_from(agent_name, visited, path)
271
+ end
272
+ end
273
+
274
+ def detect_cycle_from(agent_name, visited, path)
275
+ return if visited.include?(agent_name)
276
+
277
+ if path.include?(agent_name)
278
+ cycle_start = path.index(agent_name)
279
+ cycle = path[cycle_start..] + [agent_name]
280
+ raise CircularDependencyError, "Circular dependency detected: #{cycle.join(" -> ")}"
281
+ end
282
+
283
+ path.push(agent_name)
284
+ connections_for(agent_name).each do |connection|
285
+ connection_sym = connection.to_sym # Convert to symbol for lookup
286
+ unless @agents.key?(connection_sym)
287
+ raise ConfigurationError, "Agent '#{agent_name}' has connection to unknown agent '#{connection}'"
288
+ end
289
+
290
+ detect_cycle_from(connection_sym, visited, path)
291
+ end
292
+ path.pop
293
+ visited.add(agent_name)
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ class ContextCompactor
5
+ # Metrics tracks compression statistics
6
+ #
7
+ # Provides detailed information about the compression operation:
8
+ # - Message counts (before/after)
9
+ # - Token counts (before/after)
10
+ # - Compression ratio
11
+ # - Time taken
12
+ # - Summary of changes
13
+ #
14
+ # ## Usage
15
+ #
16
+ # metrics = agent.compact_context
17
+ # puts metrics.summary
18
+ # puts "Compressed from #{metrics.original_tokens} to #{metrics.compressed_tokens} tokens"
19
+ # puts "Compression ratio: #{(metrics.compression_ratio * 100).round(1)}%"
20
+ #
21
+ class Metrics
22
+ attr_reader :original_messages, :compressed_messages, :time_taken
23
+
24
+ # Initialize metrics from compression operation
25
+ #
26
+ # @param original_messages [Array<RubyLLM::Message>] Messages before compression
27
+ # @param compressed_messages [Array<RubyLLM::Message>] Messages after compression
28
+ # @param time_taken [Float] Time taken in seconds
29
+ def initialize(original_messages:, compressed_messages:, time_taken:)
30
+ @original_messages = original_messages
31
+ @compressed_messages = compressed_messages
32
+ @time_taken = time_taken
33
+ end
34
+
35
+ # Number of messages before compression
36
+ #
37
+ # @return [Integer] Original message count
38
+ def original_message_count
39
+ @original_messages.size
40
+ end
41
+
42
+ # Number of messages after compression
43
+ #
44
+ # @return [Integer] Compressed message count
45
+ def compressed_message_count
46
+ @compressed_messages.size
47
+ end
48
+
49
+ # Number of messages removed
50
+ #
51
+ # @return [Integer] Messages removed
52
+ def messages_removed
53
+ original_message_count - compressed_message_count
54
+ end
55
+
56
+ # Number of checkpoint summary messages created
57
+ #
58
+ # @return [Integer] Checkpoint messages
59
+ def messages_summarized
60
+ @compressed_messages.count do |msg|
61
+ msg.role == :system && msg.content.to_s.include?("CONVERSATION CHECKPOINT")
62
+ end
63
+ end
64
+
65
+ # Estimated tokens before compression
66
+ #
67
+ # @return [Integer] Original token count
68
+ def original_tokens
69
+ @original_tokens ||= TokenCounter.estimate_messages(@original_messages)
70
+ end
71
+
72
+ # Estimated tokens after compression
73
+ #
74
+ # @return [Integer] Compressed token count
75
+ def compressed_tokens
76
+ @compressed_tokens ||= TokenCounter.estimate_messages(@compressed_messages)
77
+ end
78
+
79
+ # Number of tokens removed
80
+ #
81
+ # @return [Integer] Tokens removed
82
+ def tokens_removed
83
+ original_tokens - compressed_tokens
84
+ end
85
+
86
+ # Compression ratio (compressed / original)
87
+ #
88
+ # @return [Float] Ratio between 0.0 and 1.0
89
+ def compression_ratio
90
+ return 0.0 if original_tokens.zero?
91
+
92
+ compressed_tokens.to_f / original_tokens
93
+ end
94
+
95
+ # Compression factor (original / compressed)
96
+ #
97
+ # e.g., 5.0 means compressed to 1/5th of original size
98
+ #
99
+ # @return [Float] Compression factor
100
+ def compression_factor
101
+ return 0.0 if compressed_tokens.zero?
102
+
103
+ original_tokens.to_f / compressed_tokens
104
+ end
105
+
106
+ # Compression percentage
107
+ #
108
+ # @return [Float] Percentage of original size (0-100)
109
+ def compression_percentage
110
+ (compression_ratio * 100).round(2)
111
+ end
112
+
113
+ # Generate a human-readable summary
114
+ #
115
+ # @return [String] Summary text
116
+ def summary
117
+ <<~SUMMARY
118
+ Context Compression Results:
119
+ - Messages: #{original_message_count} → #{compressed_message_count} (-#{messages_removed})
120
+ - Estimated tokens: #{original_tokens} → #{compressed_tokens} (-#{tokens_removed})
121
+ - Compression ratio: #{compression_factor.round(1)}:1 (#{compression_percentage}%)
122
+ - Checkpoints created: #{messages_summarized}
123
+ - Time taken: #{time_taken.round(3)}s
124
+ SUMMARY
125
+ end
126
+
127
+ # Convert metrics to hash for logging
128
+ #
129
+ # @return [Hash] Metrics as hash
130
+ def to_h
131
+ {
132
+ original_message_count: original_message_count,
133
+ compressed_message_count: compressed_message_count,
134
+ messages_removed: messages_removed,
135
+ messages_summarized: messages_summarized,
136
+ original_tokens: original_tokens,
137
+ compressed_tokens: compressed_tokens,
138
+ tokens_removed: tokens_removed,
139
+ compression_ratio: compression_ratio.round(4),
140
+ compression_factor: compression_factor.round(2),
141
+ compression_percentage: compression_percentage,
142
+ time_taken: time_taken.round(3),
143
+ }
144
+ end
145
+ end
146
+ end
147
+ end