claude_swarm 1.0.0 → 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.
- checksums.yaml +4 -4
- data/.claude/commands/release.md +1 -1
- data/.claude/hooks/lint-code-files.rb +65 -0
- data/.rubocop.yml +22 -2
- data/CHANGELOG.md +21 -1
- data/CLAUDE.md +1 -1
- data/CONTRIBUTING.md +69 -0
- data/README.md +27 -2
- data/Rakefile +71 -3
- data/analyze_coverage.rb +94 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
- data/docs/v2/README.md +308 -0
- data/docs/v2/guides/claude-code-agents.md +262 -0
- data/docs/v2/guides/complete-tutorial.md +3088 -0
- data/docs/v2/guides/getting-started.md +1456 -0
- data/docs/v2/guides/memory-adapters.md +998 -0
- data/docs/v2/guides/plugins.md +816 -0
- data/docs/v2/guides/quick-start-cli.md +1745 -0
- data/docs/v2/guides/rails-integration.md +1902 -0
- data/docs/v2/guides/swarm-memory.md +599 -0
- data/docs/v2/reference/cli.md +729 -0
- data/docs/v2/reference/ruby-dsl.md +2154 -0
- data/docs/v2/reference/yaml.md +1835 -0
- data/docs-team-swarm.yml +2222 -0
- data/examples/learning-assistant/assistant.md +7 -0
- data/examples/learning-assistant/example-memories/concept-example.md +90 -0
- data/examples/learning-assistant/example-memories/experience-example.md +66 -0
- data/examples/learning-assistant/example-memories/fact-example.md +76 -0
- data/examples/learning-assistant/example-memories/memory-index.md +78 -0
- data/examples/learning-assistant/example-memories/skill-example.md +168 -0
- data/examples/learning-assistant/learning_assistant.rb +34 -0
- data/examples/learning-assistant/learning_assistant.yml +20 -0
- data/examples/v2/dsl/01_basic.rb +44 -0
- data/examples/v2/dsl/02_core_parameters.rb +59 -0
- data/examples/v2/dsl/03_capabilities.rb +71 -0
- data/examples/v2/dsl/04_llm_parameters.rb +56 -0
- data/examples/v2/dsl/05_advanced_flags.rb +73 -0
- data/examples/v2/dsl/06_permissions.rb +80 -0
- data/examples/v2/dsl/07_mcp_server.rb +62 -0
- data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
- data/examples/v2/dsl/09_agent_hooks.rb +67 -0
- data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
- data/examples/v2/dsl/11_delegation.rb +60 -0
- data/examples/v2/dsl/12_complete_integration.rb +137 -0
- data/examples/v2/file_tools_swarm.yml +102 -0
- data/examples/v2/hooks/01_basic_hooks.rb +133 -0
- data/examples/v2/hooks/02_usage_tracking.rb +201 -0
- data/examples/v2/hooks/03_production_monitoring.rb +429 -0
- data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
- data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
- data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
- data/examples/v2/hooks/swarm_summary.sh +44 -0
- data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
- data/examples/v2/hooks/validate_bash.rb +59 -0
- data/examples/v2/multi_directory_permissions.yml +221 -0
- data/examples/v2/node_context_demo.rb +127 -0
- data/examples/v2/node_workflow.rb +173 -0
- data/examples/v2/path_resolution_demo.rb +216 -0
- data/examples/v2/simple-swarm-v2.rb +90 -0
- data/examples/v2/simple-swarm-v2.yml +62 -0
- data/examples/v2/swarm.yml +71 -0
- data/examples/v2/swarm_with_hooks.yml +61 -0
- data/examples/v2/swarm_with_hooks_simple.yml +25 -0
- data/examples/v2/think_tool_demo.rb +62 -0
- data/exe/swarm +6 -0
- data/lib/claude_swarm/claude_mcp_server.rb +0 -6
- data/lib/claude_swarm/cli.rb +10 -3
- data/lib/claude_swarm/commands/ps.rb +19 -20
- data/lib/claude_swarm/commands/show.rb +1 -1
- data/lib/claude_swarm/configuration.rb +10 -12
- data/lib/claude_swarm/mcp_generator.rb +10 -1
- data/lib/claude_swarm/orchestrator.rb +73 -49
- data/lib/claude_swarm/system_utils.rb +37 -11
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm/worktree_manager.rb +1 -0
- data/lib/claude_swarm/yaml_loader.rb +22 -0
- data/lib/claude_swarm.rb +7 -3
- data/lib/swarm_cli/cli.rb +201 -0
- data/lib/swarm_cli/command_registry.rb +61 -0
- data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
- data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
- data/lib/swarm_cli/commands/migrate.rb +55 -0
- data/lib/swarm_cli/commands/run.rb +173 -0
- data/lib/swarm_cli/config_loader.rb +97 -0
- data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
- data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
- data/lib/swarm_cli/interactive_repl.rb +918 -0
- data/lib/swarm_cli/mcp_serve_options.rb +44 -0
- data/lib/swarm_cli/mcp_tools_options.rb +59 -0
- data/lib/swarm_cli/migrate_options.rb +54 -0
- data/lib/swarm_cli/migrator.rb +132 -0
- data/lib/swarm_cli/options.rb +151 -0
- data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
- data/lib/swarm_cli/ui/components/content_block.rb +120 -0
- data/lib/swarm_cli/ui/components/divider.rb +57 -0
- data/lib/swarm_cli/ui/components/panel.rb +62 -0
- data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
- data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
- data/lib/swarm_cli/ui/formatters/number.rb +58 -0
- data/lib/swarm_cli/ui/formatters/text.rb +77 -0
- data/lib/swarm_cli/ui/formatters/time.rb +73 -0
- data/lib/swarm_cli/ui/icons.rb +59 -0
- data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
- data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
- data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
- data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
- data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
- data/lib/swarm_cli/version.rb +5 -0
- data/lib/swarm_cli.rb +44 -0
- data/lib/swarm_memory/adapters/base.rb +141 -0
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
- data/lib/swarm_memory/chat_extension.rb +34 -0
- data/lib/swarm_memory/cli/commands.rb +306 -0
- data/lib/swarm_memory/core/entry.rb +37 -0
- data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
- data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
- data/lib/swarm_memory/core/path_normalizer.rb +75 -0
- data/lib/swarm_memory/core/semantic_index.rb +244 -0
- data/lib/swarm_memory/core/storage.rb +288 -0
- data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
- data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
- data/lib/swarm_memory/dsl/memory_config.rb +113 -0
- data/lib/swarm_memory/embeddings/embedder.rb +36 -0
- data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
- data/lib/swarm_memory/errors.rb +21 -0
- data/lib/swarm_memory/integration/cli_registration.rb +30 -0
- data/lib/swarm_memory/integration/configuration.rb +43 -0
- data/lib/swarm_memory/integration/registration.rb +31 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
- data/lib/swarm_memory/optimization/analyzer.rb +244 -0
- data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
- data/lib/swarm_memory/prompts/memory.md.erb +109 -0
- data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
- data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
- data/lib/swarm_memory/search/semantic_search.rb +112 -0
- data/lib/swarm_memory/search/text_search.rb +42 -0
- data/lib/swarm_memory/search/text_similarity.rb +80 -0
- data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
- data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
- data/lib/swarm_memory/tools/load_skill.rb +313 -0
- data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
- data/lib/swarm_memory/tools/memory_delete.rb +99 -0
- data/lib/swarm_memory/tools/memory_edit.rb +185 -0
- data/lib/swarm_memory/tools/memory_glob.rb +160 -0
- data/lib/swarm_memory/tools/memory_grep.rb +247 -0
- data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
- data/lib/swarm_memory/tools/memory_read.rb +123 -0
- data/lib/swarm_memory/tools/memory_write.rb +231 -0
- data/lib/swarm_memory/utils.rb +50 -0
- data/lib/swarm_memory/version.rb +5 -0
- data/lib/swarm_memory.rb +166 -0
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
- data/lib/swarm_sdk/agent/builder.rb +461 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
- data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
- data/lib/swarm_sdk/agent/chat.rb +1159 -0
- data/lib/swarm_sdk/agent/context.rb +112 -0
- data/lib/swarm_sdk/agent/context_manager.rb +309 -0
- data/lib/swarm_sdk/agent/definition.rb +556 -0
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
- data/lib/swarm_sdk/configuration.rb +296 -0
- data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
- data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
- data/lib/swarm_sdk/context_compactor.rb +340 -0
- data/lib/swarm_sdk/hooks/adapter.rb +359 -0
- data/lib/swarm_sdk/hooks/context.rb +197 -0
- data/lib/swarm_sdk/hooks/definition.rb +80 -0
- data/lib/swarm_sdk/hooks/error.rb +29 -0
- data/lib/swarm_sdk/hooks/executor.rb +146 -0
- data/lib/swarm_sdk/hooks/registry.rb +147 -0
- data/lib/swarm_sdk/hooks/result.rb +150 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
- data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
- data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
- data/lib/swarm_sdk/log_collector.rb +51 -0
- data/lib/swarm_sdk/log_stream.rb +69 -0
- data/lib/swarm_sdk/markdown_parser.rb +75 -0
- data/lib/swarm_sdk/model_aliases.json +5 -0
- data/lib/swarm_sdk/models.json +1 -0
- data/lib/swarm_sdk/models.rb +120 -0
- data/lib/swarm_sdk/node/agent_config.rb +49 -0
- data/lib/swarm_sdk/node/builder.rb +439 -0
- data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
- data/lib/swarm_sdk/node_context.rb +170 -0
- data/lib/swarm_sdk/node_orchestrator.rb +384 -0
- data/lib/swarm_sdk/permissions/config.rb +239 -0
- data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
- data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
- data/lib/swarm_sdk/permissions/validator.rb +173 -0
- data/lib/swarm_sdk/permissions_builder.rb +122 -0
- data/lib/swarm_sdk/plugin.rb +147 -0
- data/lib/swarm_sdk/plugin_registry.rb +101 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
- data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
- data/lib/swarm_sdk/result.rb +97 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
- data/lib/swarm_sdk/swarm/builder.rb +586 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
- data/lib/swarm_sdk/swarm.rb +982 -0
- data/lib/swarm_sdk/tools/bash.rb +274 -0
- data/lib/swarm_sdk/tools/clock.rb +44 -0
- data/lib/swarm_sdk/tools/delegate.rb +164 -0
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
- data/lib/swarm_sdk/tools/edit.rb +150 -0
- data/lib/swarm_sdk/tools/glob.rb +158 -0
- data/lib/swarm_sdk/tools/grep.rb +228 -0
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
- data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
- data/lib/swarm_sdk/tools/read.rb +251 -0
- data/lib/swarm_sdk/tools/registry.rb +93 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
- data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
- data/lib/swarm_sdk/tools/think.rb +95 -0
- data/lib/swarm_sdk/tools/todo_write.rb +216 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
- data/lib/swarm_sdk/tools/write.rb +117 -0
- data/lib/swarm_sdk/utils.rb +50 -0
- data/lib/swarm_sdk/version.rb +5 -0
- data/lib/swarm_sdk.rb +157 -0
- data/llm.v2.txt +13407 -0
- data/rubocop/cop/security/no_reflection_methods.rb +47 -0
- data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
- data/swarm_cli.gemspec +57 -0
- data/swarm_memory.gemspec +28 -0
- data/swarm_sdk.gemspec +41 -0
- data/team.yml +1 -1
- data/team_full.yml +1875 -0
- data/{team_v2.yml → team_sdk.yml} +121 -52
- metadata +247 -4
- data/EXAMPLES.md +0 -164
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
swarm:
|
|
4
|
+
name: "Simple Swarm"
|
|
5
|
+
# all_agents:
|
|
6
|
+
# permissions:
|
|
7
|
+
# Bash:
|
|
8
|
+
# denied_commands: ["^ls.*"]
|
|
9
|
+
# allowed_paths: ["tmp/LOL/**/*"]
|
|
10
|
+
# Read:
|
|
11
|
+
# denied_paths: ["lib/**/*"]
|
|
12
|
+
lead: coordinator
|
|
13
|
+
|
|
14
|
+
agents:
|
|
15
|
+
# Coordinator agent - orchestrates file operations and delegates to worker
|
|
16
|
+
coordinator:
|
|
17
|
+
description: "A helpful assistant"
|
|
18
|
+
model: claude-sonnet-4-5-20250929
|
|
19
|
+
provider: openai
|
|
20
|
+
parameters:
|
|
21
|
+
temperature: 1.0
|
|
22
|
+
mcp_servers:
|
|
23
|
+
- name: filesystem
|
|
24
|
+
type: stdio
|
|
25
|
+
command: npx
|
|
26
|
+
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
|
27
|
+
env: {}
|
|
28
|
+
timeout: 30 # seconds
|
|
29
|
+
# api_version: "v1/responses"
|
|
30
|
+
|
|
31
|
+
# Coordinator has read-only tools plus ability to delegate
|
|
32
|
+
# TodoWrite helps track progress on complex multi-step tasks
|
|
33
|
+
tools:
|
|
34
|
+
- Bash
|
|
35
|
+
# - Write
|
|
36
|
+
|
|
37
|
+
# Can delegate work to the worker agent
|
|
38
|
+
delegates_to:
|
|
39
|
+
- worker
|
|
40
|
+
|
|
41
|
+
directory: .
|
|
42
|
+
|
|
43
|
+
system_prompt: |
|
|
44
|
+
You are a helpful assistant.
|
|
45
|
+
|
|
46
|
+
# Worker agent - performs file write/edit operations in tmp/ directory only
|
|
47
|
+
worker:
|
|
48
|
+
description: "A helpful assistant"
|
|
49
|
+
model: claude-sonnet-4-5-20250929
|
|
50
|
+
provider: openai
|
|
51
|
+
# api_version: "v1/responses"
|
|
52
|
+
|
|
53
|
+
# Worker has all tools including Write and Edit
|
|
54
|
+
# TodoWrite helps track progress, MultiEdit enables multiple replacements
|
|
55
|
+
tools:
|
|
56
|
+
- Read
|
|
57
|
+
# - Write
|
|
58
|
+
# - Edit
|
|
59
|
+
# - MultiEdit
|
|
60
|
+
- Grep
|
|
61
|
+
- Glob
|
|
62
|
+
- TodoWrite
|
|
63
|
+
|
|
64
|
+
# Worker doesn't delegate - it's a leaf node
|
|
65
|
+
delegates_to: []
|
|
66
|
+
|
|
67
|
+
directory: .
|
|
68
|
+
|
|
69
|
+
system_prompt: |
|
|
70
|
+
You are a helpful assistant.
|
|
71
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# SwarmSDK Example: YAML Hooks Feature
|
|
2
|
+
#
|
|
3
|
+
# This example demonstrates YAML hooks that execute shell commands at various
|
|
4
|
+
# lifecycle points. Both YAML and Ruby DSL support hooks using the same terminology.
|
|
5
|
+
#
|
|
6
|
+
# YAML hooks: Shell commands defined in YAML files
|
|
7
|
+
# DSL hooks: Ruby blocks defined in code
|
|
8
|
+
#
|
|
9
|
+
# Setup: export OPENAI_API_KEY=your-api-key
|
|
10
|
+
# Run: swarm run lib/swarm_sdk/examples/swarm_with_hooks.yml -p "your prompt"
|
|
11
|
+
#
|
|
12
|
+
# Or programmatically:
|
|
13
|
+
# swarm = SwarmSDK::Swarm.load("lib/swarm_sdk/examples/swarm_with_hooks.yml")
|
|
14
|
+
# result = swarm.execute("your prompt")
|
|
15
|
+
|
|
16
|
+
version: 2
|
|
17
|
+
|
|
18
|
+
swarm:
|
|
19
|
+
name: "Development Team with Hooks"
|
|
20
|
+
lead: backend
|
|
21
|
+
|
|
22
|
+
# Swarm-level hooks (only swarm_start and swarm_stop)
|
|
23
|
+
# These run at swarm lifecycle events
|
|
24
|
+
hooks:
|
|
25
|
+
swarm_start:
|
|
26
|
+
- type: command
|
|
27
|
+
command: "echo 'Swarm starting: Development Team' > tmp/log.txt"
|
|
28
|
+
timeout: 5
|
|
29
|
+
|
|
30
|
+
swarm_stop:
|
|
31
|
+
- type: command
|
|
32
|
+
command: "echo 'Swarm completed successfully' >> tmp/log.txt"
|
|
33
|
+
timeout: 5
|
|
34
|
+
|
|
35
|
+
agents:
|
|
36
|
+
backend:
|
|
37
|
+
description: "Backend developer with custom hooks"
|
|
38
|
+
model: gpt-5
|
|
39
|
+
provider: openai
|
|
40
|
+
system_prompt: "You are a backend developer. Build APIs and databases."
|
|
41
|
+
tools:
|
|
42
|
+
- Read
|
|
43
|
+
- Glob
|
|
44
|
+
hooks:
|
|
45
|
+
user_prompt:
|
|
46
|
+
- type: command
|
|
47
|
+
command: "echo 'Cannot proceed with this prompt' >&2;exit 2"
|
|
48
|
+
timeout: 10
|
|
49
|
+
pre_tool_use:
|
|
50
|
+
- matcher: "Read"
|
|
51
|
+
type: command
|
|
52
|
+
command: "echo \"you can't read files\" >&2; echo \"Can't read files!\" >> tmp/log.txt; exit 2"
|
|
53
|
+
timeout: 10
|
|
54
|
+
- matcher: "Glob"
|
|
55
|
+
type: command
|
|
56
|
+
command: "echo \"you can't use Glob, not authorized\" >&2; echo cannot use Glob, not authorized >> tmp/log.txt; exit 2"
|
|
57
|
+
timeout: 10
|
|
58
|
+
- matcher: "Grep"
|
|
59
|
+
type: command
|
|
60
|
+
command: "echo \"you can't use Grep, not authorized\" >&2; echo cannot use Grep, not authorized >> tmp/log.txt; exit 2"
|
|
61
|
+
timeout: 10
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
swarm:
|
|
4
|
+
name: "Simple Hooks Test"
|
|
5
|
+
lead: backend
|
|
6
|
+
|
|
7
|
+
hooks:
|
|
8
|
+
swarm_start:
|
|
9
|
+
- type: command
|
|
10
|
+
command: "echo 'Swarm starting' >&2"
|
|
11
|
+
|
|
12
|
+
agents:
|
|
13
|
+
backend:
|
|
14
|
+
description: "Backend developer"
|
|
15
|
+
model: gpt-5
|
|
16
|
+
system_prompt: "You are a backend developer."
|
|
17
|
+
tools:
|
|
18
|
+
- Read
|
|
19
|
+
- Bash
|
|
20
|
+
|
|
21
|
+
hooks:
|
|
22
|
+
pre_tool_use:
|
|
23
|
+
- matcher: "Bash"
|
|
24
|
+
type: command
|
|
25
|
+
command: "python3 lib/swarm_sdk/examples/hooks/validate_bash.py"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Think Tool Demonstration
|
|
4
|
+
#
|
|
5
|
+
# This example demonstrates how to use the Think tool for explicit reasoning.
|
|
6
|
+
# The Think tool allows agents to "think out loud" by recording thoughts that
|
|
7
|
+
# become part of the conversation context, leading to better reasoning and
|
|
8
|
+
# problem-solving outcomes.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# swarm run examples/v2/think_tool_demo.rb
|
|
12
|
+
# swarm run examples/v2/think_tool_demo.rb "Calculate the optimal caching strategy for this API"
|
|
13
|
+
# swarm run examples/v2/think_tool_demo.rb -p "Design a database schema for an e-commerce system"
|
|
14
|
+
|
|
15
|
+
SwarmSDK.build do
|
|
16
|
+
name "Think Tool Demo"
|
|
17
|
+
lead :problem_solver
|
|
18
|
+
|
|
19
|
+
# Problem solver agent that uses the Think tool frequently
|
|
20
|
+
agent :problem_solver do
|
|
21
|
+
coding_agent(true)
|
|
22
|
+
description "Problem solver who thinks through solutions step-by-step"
|
|
23
|
+
model "gpt-5-mini"
|
|
24
|
+
provider "openai"
|
|
25
|
+
|
|
26
|
+
system_prompt <<~PROMPT
|
|
27
|
+
You are a thoughtful problem solver who uses explicit reasoning to tackle complex tasks.
|
|
28
|
+
|
|
29
|
+
**IMPORTANT: Use the Think tool frequently throughout your work!**
|
|
30
|
+
|
|
31
|
+
The Think tool allows you to write down your thoughts, plans, and intermediate
|
|
32
|
+
calculations. This leads to significantly better outcomes because:
|
|
33
|
+
|
|
34
|
+
1. It helps you break down complex problems into manageable steps
|
|
35
|
+
2. It allows you to track your progress and plan next actions
|
|
36
|
+
3. It helps with arithmetic and calculations
|
|
37
|
+
4. It maintains context across multiple steps
|
|
38
|
+
|
|
39
|
+
**Recommended usage pattern:**
|
|
40
|
+
1. THINK before starting any task - understand the problem and create a plan
|
|
41
|
+
2. THINK after reading files or getting information - process what you learned
|
|
42
|
+
3. THINK between steps - track progress and decide next actions
|
|
43
|
+
4. THINK when doing calculations - work through math step by step
|
|
44
|
+
5. THINK when encountering complexity - break down the problem
|
|
45
|
+
|
|
46
|
+
Example workflow for a coding task:
|
|
47
|
+
1. Think: "User wants X. Let me break this into: 1) Read code, 2) Identify changes, 3) Implement, 4) Test"
|
|
48
|
+
2. Read relevant files
|
|
49
|
+
3. Think: "I see the structure. Key files are A, B. I need to modify B's function foo()"
|
|
50
|
+
4. Make changes
|
|
51
|
+
5. Think: "Changes made. Next: verify the tests pass"
|
|
52
|
+
6. Run tests
|
|
53
|
+
7. Think: "Tests pass. Task complete."
|
|
54
|
+
|
|
55
|
+
For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
|
|
56
|
+
|
|
57
|
+
Remember: Successful agents use Think 5-10 times per task on average!
|
|
58
|
+
PROMPT
|
|
59
|
+
|
|
60
|
+
tools :Think, :Read, :Write, :Edit, :Bash, :Grep, :Glob
|
|
61
|
+
end
|
|
62
|
+
end
|
data/exe/swarm
ADDED
|
@@ -52,12 +52,6 @@ module ClaudeSwarm
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def start
|
|
55
|
-
# Track this process
|
|
56
|
-
if @executor.session_path && File.exist?(@executor.session_path)
|
|
57
|
-
tracker = ProcessTracker.new(@executor.session_path)
|
|
58
|
-
tracker.track_pid(Process.pid, "mcp_#{@instance_config[:name]}")
|
|
59
|
-
end
|
|
60
|
-
|
|
61
55
|
server = FastMcp::Server.new(
|
|
62
56
|
name: @instance_config[:name],
|
|
63
57
|
version: "1.0.0",
|
data/lib/claude_swarm/cli.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module ClaudeSwarm
|
|
4
4
|
class CLI < Thor
|
|
5
5
|
include SystemUtils
|
|
6
|
+
|
|
6
7
|
class << self
|
|
7
8
|
def exit_on_failure?
|
|
8
9
|
true
|
|
@@ -458,9 +459,15 @@ module ClaudeSwarm
|
|
|
458
459
|
next unless File.exist?(config_file)
|
|
459
460
|
|
|
460
461
|
# Load the config to get swarm info
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
462
|
+
begin
|
|
463
|
+
config_data = YamlLoader.load_config_file(config_file)
|
|
464
|
+
swarm_name = config_data.dig("swarm", "name") || "Unknown"
|
|
465
|
+
main_instance = config_data.dig("swarm", "main") || "Unknown"
|
|
466
|
+
rescue ClaudeSwarm::Error => e
|
|
467
|
+
# Warn about corrupted config files but continue
|
|
468
|
+
say_error("⚠️ Skipping session #{session_id} - #{e.message}")
|
|
469
|
+
next
|
|
470
|
+
end
|
|
464
471
|
|
|
465
472
|
mcp_files = Dir.glob(File.join(session_path, "*.mcp.json"))
|
|
466
473
|
|
|
@@ -10,22 +10,9 @@ module ClaudeSwarm
|
|
|
10
10
|
return
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Dir.glob("#{run_dir}/*").each do |symlink|
|
|
17
|
-
next unless File.symlink?(symlink)
|
|
18
|
-
|
|
19
|
-
begin
|
|
20
|
-
session_dir = File.readlink(symlink)
|
|
21
|
-
# Skip if target doesn't exist (stale symlink)
|
|
22
|
-
next unless Dir.exist?(session_dir)
|
|
23
|
-
|
|
24
|
-
session_info = parse_session_info(session_dir)
|
|
25
|
-
sessions << session_info if session_info
|
|
26
|
-
rescue StandardError
|
|
27
|
-
# Skip problematic symlinks
|
|
28
|
-
end
|
|
13
|
+
# Read all symlinks in run directory and process them
|
|
14
|
+
sessions = Dir.glob("#{run_dir}/*").filter_map do |symlink|
|
|
15
|
+
process_symlink(symlink)
|
|
29
16
|
end
|
|
30
17
|
|
|
31
18
|
if sessions.empty?
|
|
@@ -83,14 +70,28 @@ module ClaudeSwarm
|
|
|
83
70
|
|
|
84
71
|
private
|
|
85
72
|
|
|
86
|
-
def
|
|
73
|
+
def process_symlink(symlink)
|
|
74
|
+
session_dir = File.readlink(symlink)
|
|
87
75
|
session_id = File.basename(session_dir)
|
|
76
|
+
# Skip if target doesn't exist (stale symlink)
|
|
77
|
+
return unless Dir.exist?(session_dir)
|
|
88
78
|
|
|
79
|
+
parse_session_info(session_id, session_dir)
|
|
80
|
+
rescue Errno::EINVAL
|
|
81
|
+
# Not a symlink, skip it
|
|
82
|
+
nil
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
# Try to get session_id if we have session_dir
|
|
85
|
+
warn("⚠️ Skipping session #{session_id}: #{e.message}")
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_session_info(session_id, session_dir)
|
|
89
90
|
# Load config for swarm name and main directory
|
|
90
91
|
config_file = File.join(session_dir, "config.yml")
|
|
91
92
|
return unless File.exist?(config_file)
|
|
92
93
|
|
|
93
|
-
config =
|
|
94
|
+
config = YamlLoader.load_config_file(config_file)
|
|
94
95
|
swarm_name = config.dig("swarm", "name") || "Unknown"
|
|
95
96
|
main_instance = config.dig("swarm", "main")
|
|
96
97
|
|
|
@@ -139,8 +140,6 @@ module ClaudeSwarm
|
|
|
139
140
|
directory: directories_str,
|
|
140
141
|
start_time: start_time,
|
|
141
142
|
}
|
|
142
|
-
rescue StandardError
|
|
143
|
-
nil
|
|
144
143
|
end
|
|
145
144
|
|
|
146
145
|
def get_start_time(session_dir)
|
|
@@ -11,7 +11,7 @@ module ClaudeSwarm
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
# Load config to get main instance name
|
|
14
|
-
config =
|
|
14
|
+
config = YamlLoader.load_config_file(File.join(session_path, "config.yml"))
|
|
15
15
|
main_instance_name = config.dig("swarm", "main")
|
|
16
16
|
|
|
17
17
|
# Parse all events to build instance data
|
|
@@ -11,14 +11,15 @@ module ClaudeSwarm
|
|
|
11
11
|
# Regex patterns
|
|
12
12
|
ENV_VAR_PATTERN = /\$\{([^}]+)\}/
|
|
13
13
|
ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
|
|
14
|
-
O_SERIES_MODEL_PATTERN = /^o\d+(\s+(Preview|preview))?(-pro|-mini|-deep-research|-mini-deep-research)
|
|
14
|
+
O_SERIES_MODEL_PATTERN = /^(o\d+(\s+(Preview|preview))?(-pro|-mini|-deep-research|-mini-deep-research)?|gpt-5(-mini|-nano)?)$/
|
|
15
15
|
|
|
16
|
-
attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances
|
|
16
|
+
attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances, :root_directory
|
|
17
17
|
|
|
18
18
|
def initialize(config_path, base_dir: nil, options: {})
|
|
19
19
|
@config_path = Pathname.new(config_path).expand_path
|
|
20
20
|
@config_dir = @config_path.dirname
|
|
21
21
|
@base_dir = base_dir || @config_dir
|
|
22
|
+
@root_directory = @base_dir
|
|
22
23
|
@options = options
|
|
23
24
|
load_and_validate
|
|
24
25
|
end
|
|
@@ -59,7 +60,7 @@ module ClaudeSwarm
|
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
def load_and_validate
|
|
62
|
-
@config =
|
|
63
|
+
@config = YamlLoader.load_config_file(@config_path)
|
|
63
64
|
interpolate_env_vars!(@config)
|
|
64
65
|
validate_version
|
|
65
66
|
validate_swarm
|
|
@@ -67,10 +68,6 @@ module ClaudeSwarm
|
|
|
67
68
|
# Skip directory validation if before commands are present
|
|
68
69
|
# They might create the directories
|
|
69
70
|
validate_directories unless has_before_commands?
|
|
70
|
-
rescue Errno::ENOENT
|
|
71
|
-
raise Error, "Configuration file not found: #{@config_path}"
|
|
72
|
-
rescue Psych::SyntaxError => e
|
|
73
|
-
raise Error, "Invalid YAML syntax: #{e.message}"
|
|
74
71
|
end
|
|
75
72
|
|
|
76
73
|
def interpolate_env_vars!(obj)
|
|
@@ -164,16 +161,16 @@ module ClaudeSwarm
|
|
|
164
161
|
raise Error, "Instance '#{name}' has invalid reasoning_effort '#{config["reasoning_effort"]}'. Must be 'low', 'medium', or 'high'"
|
|
165
162
|
end
|
|
166
163
|
|
|
167
|
-
# Validate it's only used with o-series models
|
|
168
|
-
# Support patterns like: o1, o1-mini, o1-pro, o1 Preview, o3-deep-research, o4-mini-deep-research, etc.
|
|
164
|
+
# Validate it's only used with o-series or gpt-5 models
|
|
165
|
+
# Support patterns like: o1, o1-mini, o1-pro, o1 Preview, o3-deep-research, o4-mini-deep-research, gpt-5, gpt-5-mini, gpt-5-nano, etc.
|
|
169
166
|
unless model&.match?(O_SERIES_MODEL_PATTERN)
|
|
170
|
-
raise Error, "Instance '#{name}' has reasoning_effort but model '#{model}' is not an o-series model (o1, o1 Preview, o1-mini, o1-pro, o3, o3-mini, o3-pro, o3-deep-research, o4-mini, o4-mini-deep-research, etc.)"
|
|
167
|
+
raise Error, "Instance '#{name}' has reasoning_effort but model '#{model}' is not an o-series or gpt-5 model (o1, o1 Preview, o1-mini, o1-pro, o3, o3-mini, o3-pro, o3-deep-research, o4-mini, o4-mini-deep-research, gpt-5, gpt-5-mini, gpt-5-nano, etc.)"
|
|
171
168
|
end
|
|
172
169
|
end
|
|
173
170
|
|
|
174
|
-
# Validate temperature is not used with o-series models when provider is openai
|
|
171
|
+
# Validate temperature is not used with o-series or gpt-5 models when provider is openai
|
|
175
172
|
if provider == "openai" && config["temperature"] && model&.match?(O_SERIES_MODEL_PATTERN)
|
|
176
|
-
raise Error, "Instance '#{name}' has temperature parameter but model '#{model}' is an o-series model. O-series models use deterministic reasoning and don't accept temperature settings"
|
|
173
|
+
raise Error, "Instance '#{name}' has temperature parameter but model '#{model}' is an o-series or gpt-5 model. O-series and gpt-5 models use deterministic reasoning and don't accept temperature settings"
|
|
177
174
|
end
|
|
178
175
|
|
|
179
176
|
# Validate OpenAI-specific fields only when provider is not "openai"
|
|
@@ -211,6 +208,7 @@ module ClaudeSwarm
|
|
|
211
208
|
disallowed_tools: Array(config["disallowed_tools"]),
|
|
212
209
|
mcps: parse_mcps(config["mcps"] || []),
|
|
213
210
|
prompt: config["prompt"],
|
|
211
|
+
prompt_file: config["prompt_file"],
|
|
214
212
|
description: config["description"],
|
|
215
213
|
vibe: config["vibe"],
|
|
216
214
|
worktree: parse_worktree_value(config["worktree"]),
|
|
@@ -134,7 +134,16 @@ module ClaudeSwarm
|
|
|
134
134
|
args.push("--directories", *instance[:directories]) if instance[:directories] && instance[:directories].size > 1
|
|
135
135
|
|
|
136
136
|
# Add optional arguments
|
|
137
|
-
|
|
137
|
+
# Handle prompt_file by reading the file contents
|
|
138
|
+
if instance[:prompt_file]
|
|
139
|
+
prompt_file_path = File.join(@config.root_directory, instance[:prompt_file])
|
|
140
|
+
if File.exist?(prompt_file_path)
|
|
141
|
+
prompt_content = File.read(prompt_file_path)
|
|
142
|
+
args.push("--prompt", prompt_content)
|
|
143
|
+
end
|
|
144
|
+
elsif instance[:prompt]
|
|
145
|
+
args.push("--prompt", instance[:prompt])
|
|
146
|
+
end
|
|
138
147
|
|
|
139
148
|
args.push("--description", instance[:description]) if instance[:description]
|
|
140
149
|
|
|
@@ -6,12 +6,6 @@ module ClaudeSwarm
|
|
|
6
6
|
|
|
7
7
|
attr_reader :config, :session_path, :session_log_path
|
|
8
8
|
|
|
9
|
-
["INT", "TERM", "QUIT"].each do |signal|
|
|
10
|
-
Signal.trap(signal) do
|
|
11
|
-
puts "\n🛑 Received #{signal} signal."
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
9
|
def initialize(configuration, mcp_generator, vibe: false, prompt: nil, interactive_prompt: nil, stream_logs: false, debug: false,
|
|
16
10
|
restore_session_path: nil, worktree: nil, session_id: nil)
|
|
17
11
|
@config = configuration
|
|
@@ -72,19 +66,40 @@ module ClaudeSwarm
|
|
|
72
66
|
# Track start time
|
|
73
67
|
@start_time = Time.now
|
|
74
68
|
|
|
69
|
+
# Setup signal handlers for graceful shutdown
|
|
70
|
+
setup_signal_handlers do
|
|
71
|
+
@signal_received = true
|
|
72
|
+
cleanup_all
|
|
73
|
+
end
|
|
74
|
+
|
|
75
75
|
begin
|
|
76
76
|
start_internal
|
|
77
77
|
rescue StandardError => e
|
|
78
78
|
# Ensure cleanup happens even on unexpected errors
|
|
79
|
-
|
|
80
|
-
cleanup_run_symlink
|
|
81
|
-
cleanup_worktrees
|
|
79
|
+
cleanup_all
|
|
82
80
|
raise e
|
|
83
81
|
end
|
|
84
82
|
end
|
|
85
83
|
|
|
86
84
|
private
|
|
87
85
|
|
|
86
|
+
def cleanup_all
|
|
87
|
+
execute_after_commands_once
|
|
88
|
+
cleanup_processes
|
|
89
|
+
cleanup_run_symlink
|
|
90
|
+
cleanup_worktrees
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def setup_signal_handlers(&cleanup_block)
|
|
94
|
+
["INT", "TERM", "QUIT", "HUP"].each do |signal|
|
|
95
|
+
Signal.trap(signal) do
|
|
96
|
+
puts "\n🛑 Received #{signal} signal. Shutting down gracefully..."
|
|
97
|
+
cleanup_block&.call
|
|
98
|
+
exit(0)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
88
103
|
def start_internal
|
|
89
104
|
if @restore_session_path
|
|
90
105
|
non_interactive_output do
|
|
@@ -142,9 +157,7 @@ module ClaudeSwarm
|
|
|
142
157
|
end
|
|
143
158
|
rescue StandardError => e
|
|
144
159
|
non_interactive_output { print("❌ Failed to setup worktrees: #{e.message}") }
|
|
145
|
-
|
|
146
|
-
cleanup_run_symlink
|
|
147
|
-
cleanup_worktrees
|
|
160
|
+
cleanup_all
|
|
148
161
|
raise
|
|
149
162
|
end
|
|
150
163
|
end
|
|
@@ -226,9 +239,7 @@ module ClaudeSwarm
|
|
|
226
239
|
success = execute_before_commands?(before_commands)
|
|
227
240
|
unless success
|
|
228
241
|
non_interactive_output { print("❌ Before commands failed. Aborting swarm launch.") }
|
|
229
|
-
|
|
230
|
-
cleanup_run_symlink
|
|
231
|
-
cleanup_worktrees
|
|
242
|
+
cleanup_all
|
|
232
243
|
exit(1)
|
|
233
244
|
end
|
|
234
245
|
end
|
|
@@ -245,9 +256,7 @@ module ClaudeSwarm
|
|
|
245
256
|
end
|
|
246
257
|
rescue ClaudeSwarm::Error => e
|
|
247
258
|
non_interactive_output { print("❌ Directory validation failed: #{e.message}") }
|
|
248
|
-
|
|
249
|
-
cleanup_run_symlink
|
|
250
|
-
cleanup_worktrees
|
|
259
|
+
cleanup_all
|
|
251
260
|
exit(1)
|
|
252
261
|
end
|
|
253
262
|
end
|
|
@@ -261,7 +270,12 @@ module ClaudeSwarm
|
|
|
261
270
|
if @non_interactive_prompt
|
|
262
271
|
stream_to_session_log(*command)
|
|
263
272
|
else
|
|
264
|
-
|
|
273
|
+
system_with_pid!(*command) do |pid|
|
|
274
|
+
@process_tracker.track_pid(pid, "claude_#{@config.main_instance}")
|
|
275
|
+
non_interactive_output do
|
|
276
|
+
puts "✓ Claude instance started with PID: #{pid}"
|
|
277
|
+
end
|
|
278
|
+
end
|
|
265
279
|
end
|
|
266
280
|
end
|
|
267
281
|
end
|
|
@@ -279,37 +293,10 @@ module ClaudeSwarm
|
|
|
279
293
|
display_summary
|
|
280
294
|
|
|
281
295
|
# Execute after commands if specified
|
|
282
|
-
|
|
283
|
-
after_commands = @config.after_commands
|
|
284
|
-
if after_commands.any? && !@restore_session_path
|
|
285
|
-
# Determine where to run after commands (same logic as before commands)
|
|
286
|
-
if File.exist?(main_instance[:directory])
|
|
287
|
-
# Directory exists, run commands in it
|
|
288
|
-
after_commands_dir = main_instance[:directory]
|
|
289
|
-
else
|
|
290
|
-
# Directory doesn't exist (shouldn't happen after main instance runs, but be safe)
|
|
291
|
-
parent_dir = File.dirname(File.expand_path(main_instance[:directory]))
|
|
292
|
-
after_commands_dir = parent_dir
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
Dir.chdir(after_commands_dir) do
|
|
296
|
-
non_interactive_output do
|
|
297
|
-
print("⚙️ Executing after commands...")
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
success = execute_after_commands?(after_commands)
|
|
301
|
-
unless success
|
|
302
|
-
non_interactive_output do
|
|
303
|
-
puts "⚠️ Some after commands failed"
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
end
|
|
296
|
+
execute_after_commands_once
|
|
308
297
|
|
|
309
298
|
# Clean up child processes and run symlink
|
|
310
|
-
|
|
311
|
-
cleanup_run_symlink
|
|
312
|
-
cleanup_worktrees
|
|
299
|
+
cleanup_all
|
|
313
300
|
end
|
|
314
301
|
|
|
315
302
|
def non_interactive_output
|
|
@@ -327,6 +314,42 @@ module ClaudeSwarm
|
|
|
327
314
|
execute_commands(commands, phase: "after", fail_fast: false)
|
|
328
315
|
end
|
|
329
316
|
|
|
317
|
+
def execute_after_commands_once
|
|
318
|
+
# Ensure after commands are only executed once
|
|
319
|
+
return if @after_commands_executed
|
|
320
|
+
|
|
321
|
+
@after_commands_executed = true
|
|
322
|
+
|
|
323
|
+
# Use the same logic as before commands for consistency
|
|
324
|
+
after_commands = @config.after_commands
|
|
325
|
+
return if after_commands.empty? || @restore_session_path
|
|
326
|
+
|
|
327
|
+
main_instance = @config.main_instance_config
|
|
328
|
+
|
|
329
|
+
# Determine where to run after commands (same logic as before commands)
|
|
330
|
+
if File.exist?(main_instance[:directory])
|
|
331
|
+
# Directory exists, run commands in it
|
|
332
|
+
after_commands_dir = main_instance[:directory]
|
|
333
|
+
else
|
|
334
|
+
# Directory doesn't exist (shouldn't happen after main instance runs, but be safe)
|
|
335
|
+
parent_dir = File.dirname(File.expand_path(main_instance[:directory]))
|
|
336
|
+
after_commands_dir = parent_dir
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
Dir.chdir(after_commands_dir) do
|
|
340
|
+
non_interactive_output do
|
|
341
|
+
print("⚙️ Executing after commands...")
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
success = execute_after_commands?(after_commands)
|
|
345
|
+
unless success
|
|
346
|
+
non_interactive_output do
|
|
347
|
+
puts "⚠️ Some after commands failed"
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
330
353
|
def save_swarm_config_path(session_path)
|
|
331
354
|
# Copy the YAML config file to the session directory
|
|
332
355
|
config_copy_path = File.join(session_path, "config.yml")
|
|
@@ -800,7 +823,8 @@ module ClaudeSwarm
|
|
|
800
823
|
all_succeeded = true
|
|
801
824
|
|
|
802
825
|
# Setup logger for session logging if we have a session path
|
|
803
|
-
|
|
826
|
+
log_dev = @signal_received ? nil : @session_log_path
|
|
827
|
+
logger = Logger.new(log_dev, level: :info)
|
|
804
828
|
|
|
805
829
|
commands.each_with_index do |command, index|
|
|
806
830
|
# Log the command execution to session log
|
|
@@ -3,18 +3,44 @@
|
|
|
3
3
|
module ClaudeSwarm
|
|
4
4
|
module SystemUtils
|
|
5
5
|
def system!(*args)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
6
|
+
system(*args)
|
|
7
|
+
handle_command_failure(last_status, args)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def system_with_pid!(*args)
|
|
11
|
+
# Spawn the process - by default, inherits the parent's I/O
|
|
12
|
+
pid = Process.spawn(*args)
|
|
13
|
+
|
|
14
|
+
# Yield the PID to the block if given
|
|
15
|
+
yield(pid) if block_given?
|
|
16
|
+
|
|
17
|
+
# Wait for the process to complete
|
|
18
|
+
_, status = Process.wait2(pid)
|
|
19
|
+
|
|
20
|
+
# Check the exit status
|
|
21
|
+
handle_command_failure(status, args)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def last_status
|
|
25
|
+
$CHILD_STATUS
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def handle_command_failure(status, args) # rubocop:disable Naming/PredicateMethod
|
|
31
|
+
return true if status&.success?
|
|
32
|
+
|
|
33
|
+
exit_status = status&.exitstatus || 1
|
|
34
|
+
command_str = args.size == 1 ? args.first : args.join(" ")
|
|
35
|
+
|
|
36
|
+
if exit_status == 143 # timeout command exit status = 128 + 15 (SIGTERM)
|
|
37
|
+
warn("⏱️ Command timeout: #{command_str}")
|
|
38
|
+
else
|
|
39
|
+
warn("❌ Command failed with exit status: #{exit_status}")
|
|
40
|
+
raise Error, "Command failed with exit status #{exit_status}: #{command_str}"
|
|
16
41
|
end
|
|
17
|
-
|
|
42
|
+
|
|
43
|
+
false
|
|
18
44
|
end
|
|
19
45
|
end
|
|
20
46
|
end
|