claude_swarm 0.1.17 → 0.1.18

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f0ab99665f31bddf2d3641525481ce2df55e910950af03290137246847bf70d
4
- data.tar.gz: 2435170f5b61604c3131b1680f60ed8727795c58432806d9b61007813d4f5ce3
3
+ metadata.gz: 1aaf0eadabd67afb0bd79738de8c7fcbfb65887d477a2c0c002017cff9de15ec
4
+ data.tar.gz: 777f04c044a2204477d1328dc31b0b5361fa0007d0cf637254d3531260e3149d
5
5
  SHA512:
6
- metadata.gz: ee74c188f6cc7fd31bb8705661f0913674bb687fd1eae6c2973728d2418f62bbad86054a89bd150965f39ec0bbd37ac04b2413dd22340311afd03cd5ec9d5424
7
- data.tar.gz: a6c5fb7db4a8659619c9c7b5de000303ce91b046b85d635381f437adc92b4fd739e8d2142f0e65323346d1ba4d2607bf2b90c7d76fcf7c119bf6b9d5c431a7fc
6
+ metadata.gz: cb39225f7a1f4cfd7c0d7e0e3068ad5527473a04744ed0d18525b37452e7b7d4fe67c83f03fbb5c662591e51ed9c10a2360c5943676dc944ea83680652c977b7
7
+ data.tar.gz: 807b10f87678f4f49df363fbcc53bf3a71d14848adf2ebbad1d701737aad8f9ce6b894ebe7f6e84c45234a1f414ec1c1d171b79189090b6d6de2a71df00e05a7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ ## [0.1.18]
2
+
3
+ ### Added
4
+ - **Before commands**: Execute setup commands before launching the swarm
5
+ - New `before` field in swarm configuration accepts an array of commands
6
+ - Commands are executed in sequence before any Claude instances are launched
7
+ - All commands must succeed (exit code 0) for the swarm to launch
8
+ - Commands are only executed on initial launch, not when restoring sessions
9
+ - Output is logged to the session log file
10
+ - Useful for installing dependencies, starting services, or running setup scripts
11
+ - Example: `before: ["npm install", "docker-compose up -d"]`
12
+
13
+ - **Git worktree support**: Run instances in isolated Git worktrees
14
+ - New `--worktree [NAME]` CLI option creates worktrees for all instances
15
+ - Worktrees are created inside each repository at `.worktrees/NAME`
16
+ - Each worktree gets its own branch (not detached HEAD) for proper Git operations
17
+ - Auto-generated names use session ID: `worktree-SESSION_ID`
18
+ - Per-instance worktree configuration in YAML:
19
+ - `worktree: true` - Use shared worktree name
20
+ - `worktree: false` - Disable worktree for this instance
21
+ - `worktree: "branch-name"` - Use custom worktree name
22
+ - Session restoration automatically restores worktrees
23
+ - Cleanup preserves worktrees with uncommitted changes or unpushed commits
24
+ - Warnings displayed when worktrees are preserved: "⚠️ Warning: Worktree has uncommitted changes"
25
+ - Multiple directories per instance work seamlessly with worktrees
26
+ - `.gitignore` automatically created in `.worktrees/` directory
27
+ - Example: `claude-swarm --worktree feature-branch`
28
+
1
29
  ## [0.1.17]
2
30
 
3
31
  ### Added
data/CLAUDE.md CHANGED
@@ -39,6 +39,47 @@ bundle exec rake release # Release gem to RubyGems.org
39
39
  rake # Runs both tests and RuboCop
40
40
  ```
41
41
 
42
+ ## Git Worktree Support
43
+
44
+ Claude Swarm supports launching instances in Git worktrees to isolate changes:
45
+
46
+ ### CLI Usage
47
+ ```bash
48
+ # Create worktrees with custom name
49
+ claude-swarm --worktree feature-branch
50
+
51
+ # Create worktrees with auto-generated name (worktree-SESSION_ID)
52
+ claude-swarm --worktree
53
+
54
+ # Short form
55
+ claude-swarm -w feature-x
56
+ ```
57
+
58
+ ### Per-Instance Configuration
59
+ Instances can have individual worktree settings that override CLI behavior:
60
+
61
+ ```yaml
62
+ instances:
63
+ main:
64
+ worktree: true # Use shared worktree name (from CLI or auto-generated)
65
+ testing:
66
+ worktree: false # Don't use worktree for this instance
67
+ feature:
68
+ worktree: "feature-x" # Use specific worktree name
69
+ default:
70
+ # No worktree field - follows CLI behavior
71
+ ```
72
+
73
+ ### Worktree Behavior
74
+ - Worktrees are created inside each repository in a `.worktrees/` directory
75
+ - A `.gitignore` file is automatically created inside `.worktrees/` to ignore all contents
76
+ - Each unique Git repository gets its own worktree with the same name
77
+ - All instance directories are mapped to their worktree equivalents
78
+ - Worktrees are automatically cleaned up when the swarm exits
79
+ - Session metadata tracks worktree information for restoration
80
+ - Non-Git directories are used as-is without creating worktrees
81
+ - Existing worktrees with the same name are reused
82
+
42
83
  ## Architecture
43
84
 
44
85
  The gem is fully implemented with the following components:
@@ -49,6 +90,7 @@ The gem is fully implemented with the following components:
49
90
  - **ClaudeSwarm::Configuration** (`lib/claude_swarm/configuration.rb`): YAML parser and validator for swarm configurations
50
91
  - **ClaudeSwarm::McpGenerator** (`lib/claude_swarm/mcp_generator.rb`): Generates MCP JSON configurations for each instance
51
92
  - **ClaudeSwarm::Orchestrator** (`lib/claude_swarm/orchestrator.rb`): Launches the main Claude instance with proper configuration
93
+ - **ClaudeSwarm::WorktreeManager** (`lib/claude_swarm/worktree_manager.rb`): Manages Git worktrees for isolated development
52
94
 
53
95
  ### Key Features
54
96
 
@@ -58,6 +100,7 @@ The gem is fully implemented with the following components:
58
100
  4. **Multiple MCP Types**: Supports both stdio and SSE MCP server types
59
101
  5. **Automatic MCP Generation**: Creates `.claude-swarm/` directory with MCP configs
60
102
  6. **Custom System Prompts**: Each instance can have a custom prompt via `--append-system-prompt`
103
+ 7. **Git Worktree Support**: Run instances in isolated Git worktrees with per-instance configuration
61
104
 
62
105
  ### How It Works
63
106
 
@@ -77,16 +120,20 @@ swarm:
77
120
  main: lead
78
121
  instances:
79
122
  lead:
123
+ description: "Lead developer coordinating the team"
80
124
  directory: .
81
125
  model: opus
82
126
  connections: [frontend, backend]
83
127
  prompt: "You are the lead developer coordinating the team"
84
128
  tools: [Read, Edit, Bash]
129
+ worktree: true # Optional: use worktree for this instance
85
130
  frontend:
131
+ description: "Frontend developer specializing in React"
86
132
  directory: ./frontend
87
133
  model: sonnet
88
134
  prompt: "You specialize in frontend development with React"
89
135
  tools: [Edit, Write, Bash]
136
+ worktree: false # Optional: disable worktree for this instance
90
137
  ```
91
138
 
92
139
  ## Testing
@@ -98,6 +145,7 @@ The gem includes comprehensive tests covering:
98
145
  - CLI command functionality
99
146
  - Session restoration
100
147
  - Vibe mode behavior
148
+ - Worktree management and per-instance configuration
101
149
 
102
150
  ## Dependencies
103
151
 
data/README.md CHANGED
@@ -1,10 +1,30 @@
1
1
  # Claude Swarm
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/claude_swarm.svg?cache_bust=0.1.15)](https://badge.fury.io/rb/claude_swarm)
3
+ [![Gem Version](https://badge.fury.io/rb/claude_swarm.svg?cache_bust=0.1.17)](https://badge.fury.io/rb/claude_swarm)
4
4
  [![CI](https://github.com/parruda/claude-swarm/actions/workflows/ci.yml/badge.svg)](https://github.com/parruda/claude-swarm/actions/workflows/ci.yml)
5
5
 
6
6
  Claude Swarm orchestrates multiple Claude Code instances as a collaborative AI development team. It enables running AI agents with specialized roles, tools, and directory contexts, communicating via MCP (Model Context Protocol) in a tree-like hierarchy. Define your swarm topology in simple YAML and let Claude instances delegate tasks through connected instances. Perfect for complex projects requiring specialized AI agents for frontend, backend, testing, DevOps, or research tasks.
7
7
 
8
+ ## Table of Contents
9
+
10
+ - [Installation](#installation)
11
+ - [Prerequisites](#prerequisites)
12
+ - [Usage](#usage)
13
+ - [Quick Start](#quick-start)
14
+ - [Configuration Format](#configuration-format)
15
+ - [MCP Server Types](#mcp-server-types)
16
+ - [Tools](#tools)
17
+ - [Examples](#examples)
18
+ - [Command Line Options](#command-line-options)
19
+ - [Session Monitoring](#session-monitoring)
20
+ - [Session Management and Restoration](#session-management-and-restoration-experimental)
21
+ - [How It Works](#how-it-works)
22
+ - [Troubleshooting](#troubleshooting)
23
+ - [Architecture](#architecture)
24
+ - [Development](#development)
25
+ - [Contributing](#contributing)
26
+ - [License](#license)
27
+
8
28
  ## Installation
9
29
 
10
30
  Install the gem by executing:
@@ -191,6 +211,10 @@ version: 1 # Required, currently only version 1 is supported
191
211
  swarm:
192
212
  name: "Swarm Name" # Display name for your swarm
193
213
  main: instance_key # Which instance to launch as the main interface
214
+ before: # Optional: commands to run before launching the swarm
215
+ - "echo 'Setting up environment...'"
216
+ - "npm install"
217
+ - "docker-compose up -d"
194
218
  instances:
195
219
  # Instance definitions...
196
220
  ```
@@ -211,6 +235,7 @@ Each instance can have:
211
235
  - **mcps**: Array of additional MCP servers to connect
212
236
  - **prompt**: Custom system prompt to append to the instance
213
237
  - **vibe**: Enable vibe mode (--dangerously-skip-permissions) for this instance (default: false)
238
+ - **worktree**: Configure Git worktree usage for this instance (true/false/string)
214
239
 
215
240
  ```yaml
216
241
  instance_name:
@@ -428,6 +453,42 @@ When using multiple directories:
428
453
  - Additional directories are accessible via the `--add-dir` flag in Claude
429
454
  - All directories must exist or the configuration will fail validation
430
455
 
456
+ #### Before Commands
457
+
458
+ You can specify commands to run before launching the swarm using the `before` field:
459
+
460
+ ```yaml
461
+ version: 1
462
+ swarm:
463
+ name: "Development Environment"
464
+ main: lead_developer
465
+ before:
466
+ - "echo '🚀 Setting up development environment...'"
467
+ - "npm install"
468
+ - "docker-compose up -d"
469
+ - "bundle install"
470
+ instances:
471
+ lead_developer:
472
+ description: "Lead developer coordinating the team"
473
+ directory: .
474
+ model: opus
475
+ allowed_tools: [Read, Edit, Write, Bash]
476
+ ```
477
+
478
+ The `before` commands:
479
+ - Are executed in sequence before launching any Claude instances
480
+ - Must all succeed for the swarm to launch (exit code 0)
481
+ - Are only executed on initial swarm launch, not when restoring sessions
482
+ - Have their output logged to the session log file
483
+ - Will abort the swarm launch if any command fails
484
+
485
+ This is useful for:
486
+ - Installing dependencies
487
+ - Starting required services (databases, Docker containers, etc.)
488
+ - Setting up the development environment
489
+ - Running any prerequisite setup scripts
490
+
491
+
431
492
  #### Mixed Permission Modes
432
493
 
433
494
  You can have different permission modes for different instances:
@@ -459,6 +520,72 @@ swarm:
459
520
  allowed_tools: [] # Tools list ignored when vibe: true
460
521
  ```
461
522
 
523
+ #### Git Worktrees
524
+
525
+ Claude Swarm supports running instances in Git worktrees, allowing isolated work without affecting your main repository state. Worktrees are created inside each repository in a `.worktrees/` directory following Git best practices.
526
+
527
+ **Example Structure:**
528
+ ```
529
+ my-repo/
530
+ ├── .git/
531
+ ├── .worktrees/ (created by Claude Swarm)
532
+ │ ├── .gitignore (auto-created, contains "*")
533
+ │ └── feature-x/ (worktree for feature-x branch)
534
+ ├── src/
535
+ └── tests/
536
+ ```
537
+
538
+ **CLI Option:**
539
+ ```bash
540
+ # Create worktrees with auto-generated name (worktree-SESSION_ID)
541
+ claude-swarm --worktree
542
+
543
+ # Create worktrees with custom name
544
+ claude-swarm --worktree feature-branch
545
+
546
+ # Short form
547
+ claude-swarm -w
548
+ ```
549
+
550
+ **Per-Instance Configuration:**
551
+ ```yaml
552
+ version: 1
553
+ swarm:
554
+ name: "Worktree Example"
555
+ main: lead
556
+ instances:
557
+ lead:
558
+ description: "Lead developer"
559
+ directory: .
560
+ worktree: true # Use shared worktree name from CLI (or auto-generate)
561
+
562
+ testing:
563
+ description: "Test developer"
564
+ directory: ./tests
565
+ worktree: false # Don't use worktree for this instance
566
+
567
+ feature_dev:
568
+ description: "Feature developer"
569
+ directory: ./features
570
+ worktree: "feature-x" # Use specific worktree name
571
+ ```
572
+
573
+ **Worktree Behavior:**
574
+ - `worktree: true` - Uses the shared worktree name (from CLI or auto-generated)
575
+ - `worktree: false` - Disables worktree for this instance
576
+ - `worktree: "name"` - Uses a specific worktree name
577
+ - Omitted - Follows CLI behavior (use worktree if `--worktree` is specified)
578
+
579
+ **Notes:**
580
+ - Worktrees are created inside each repository in a `.worktrees/` directory
581
+ - Auto-generated worktree names use the session ID (e.g., `worktree-20241206_143022`)
582
+ - This makes it easy to correlate worktrees with their Claude Swarm sessions
583
+ - A `.gitignore` file is automatically created inside `.worktrees/` to ignore all worktree contents
584
+ - All worktrees are automatically cleaned up when the swarm exits
585
+ - Worktrees with the same name across different repositories share that name
586
+ - Non-Git directories are unaffected by worktree settings
587
+ - Existing worktrees with the same name are reused
588
+
462
589
  ### Command Line Options
463
590
 
464
591
  ```bash
@@ -480,6 +607,11 @@ claude-swarm --prompt "Fix the bug in the payment module"
480
607
  claude-swarm --session-id 20241206_143022
481
608
  claude-swarm --session-id ~/path/to/session
482
609
 
610
+ # Run all instances in Git worktrees
611
+ claude-swarm --worktree # Auto-generated name (worktree-SESSION_ID)
612
+ claude-swarm --worktree feature-branch # Custom worktree name
613
+ claude-swarm -w # Short form
614
+
483
615
  # Show version
484
616
  claude-swarm version
485
617
 
data/claude-swarm.yml CHANGED
@@ -5,22 +5,10 @@ swarm:
5
5
  instances:
6
6
  claude_swarm_architect:
7
7
  description: "Lead architect"
8
- directory:
9
- - .
10
- - /Users/paulo/src/github.com/shopify-playground/claudeception
8
+ directory: .
11
9
  model: opus
12
10
  prompt: "You are an expert in Claude swarm architecture"
13
11
  vibe: true
14
12
  connections: [claudeception_architect]
15
13
 
16
- # Example instances (uncomment and modify as needed):
17
-
18
- claudeception_architect:
19
- description: "You are an expert in Claudeception architecture"
20
- directory:
21
- - .
22
- - /Users/paulo/src/github.com/shopify-playground/claudeception
23
- model: opus
24
- prompt: "You are an expert in Claudeception architecture"
25
- vibe: true
26
-
14
+
@@ -0,0 +1,30 @@
1
+ version: 1
2
+ swarm:
3
+ name: "Development Environment"
4
+ main: lead_developer
5
+ before:
6
+ - "echo '🚀 Setting up development environment...'"
7
+ - "echo '📦 Installing dependencies...'"
8
+ - "echo '🐳 Starting Docker containers...'"
9
+ instances:
10
+ lead_developer:
11
+ description: "Lead developer who coordinates the team"
12
+ directory: .
13
+ model: sonnet
14
+ prompt: "You are the lead developer coordinating the team"
15
+ allowed_tools: [Read, Edit, Bash, Write]
16
+ connections: [frontend_dev, backend_dev]
17
+
18
+ frontend_dev:
19
+ description: "Frontend developer specializing in React"
20
+ directory: ./frontend
21
+ model: sonnet
22
+ prompt: "You specialize in frontend development with React"
23
+ allowed_tools: [Read, Edit, Write]
24
+
25
+ backend_dev:
26
+ description: "Backend developer focusing on APIs"
27
+ directory: ./backend
28
+ model: sonnet
29
+ prompt: "You specialize in backend development and APIs"
30
+ allowed_tools: [Read, Edit, Write, Bash]
@@ -26,6 +26,9 @@ module ClaudeSwarm
26
26
  desc: "Enable debug output"
27
27
  method_option :session_id, type: :string,
28
28
  desc: "Resume a previous session by ID or path"
29
+ method_option :worktree, type: :string, aliases: "-w",
30
+ desc: "Create instances in Git worktrees with the given name (auto-generated if true)",
31
+ banner: "[NAME]"
29
32
  def start(config_file = nil)
30
33
  # Handle session restoration
31
34
  if options[:session_id]
@@ -54,7 +57,8 @@ module ClaudeSwarm
54
57
  vibe: options[:vibe],
55
58
  prompt: options[:prompt],
56
59
  stream_logs: options[:stream_logs],
57
- debug: options[:debug])
60
+ debug: options[:debug],
61
+ worktree: options[:worktree])
58
62
  orchestrator.start
59
63
  rescue Error => e
60
64
  error e.message
@@ -147,6 +151,10 @@ module ClaudeSwarm
147
151
  swarm:
148
152
  name: "Swarm Name"
149
153
  main: lead_developer
154
+ # before: # Optional: commands to run before launching swarm (executed in sequence)
155
+ # - "echo 'Setting up environment...'"
156
+ # - "npm install"
157
+ # - "docker-compose up -d"
150
158
  instances:
151
159
  lead_developer:
152
160
  description: "Lead developer who coordinates the team and makes architectural decisions"
@@ -241,7 +249,7 @@ module ClaudeSwarm
241
249
  end
242
250
  end
243
251
 
244
- say "Cleaned #{cleaned} stale session#{cleaned == 1 ? "" : "s"}", :green
252
+ say "Cleaned #{cleaned} stale session#{"s" unless cleaned == 1}", :green
245
253
  end
246
254
 
247
255
  desc "watch SESSION_ID", "Watch session logs"
@@ -395,6 +403,17 @@ module ClaudeSwarm
395
403
 
396
404
  config = Configuration.new(config_file, base_dir: Dir.pwd)
397
405
 
406
+ # Load session metadata if it exists to check for worktree info
407
+ session_metadata_file = File.join(session_path, "session_metadata.json")
408
+ worktree_name = nil
409
+ if File.exist?(session_metadata_file)
410
+ metadata = JSON.parse(File.read(session_metadata_file))
411
+ if metadata["worktree"] && metadata["worktree"]["enabled"]
412
+ worktree_name = metadata["worktree"]["name"]
413
+ say "Restoring with worktree: #{worktree_name}", :green unless options[:prompt]
414
+ end
415
+ end
416
+
398
417
  # Create orchestrator with restoration mode
399
418
  generator = McpGenerator.new(config, vibe: options[:vibe], restore_session_path: session_path)
400
419
  orchestrator = Orchestrator.new(config, generator,
@@ -402,7 +421,8 @@ module ClaudeSwarm
402
421
  prompt: options[:prompt],
403
422
  stream_logs: options[:stream_logs],
404
423
  debug: options[:debug],
405
- restore_session_path: session_path)
424
+ restore_session_path: session_path,
425
+ worktree: worktree_name)
406
426
  orchestrator.start
407
427
  rescue StandardError => e
408
428
  error "Failed to restore session: #{e.message}"
@@ -26,6 +26,10 @@ module ClaudeSwarm
26
26
  instances[instance_name][:connections] || []
27
27
  end
28
28
 
29
+ def before_commands
30
+ @swarm["before"] || []
31
+ end
32
+
29
33
  private
30
34
 
31
35
  def load_and_validate
@@ -101,7 +105,8 @@ module ClaudeSwarm
101
105
  mcps: parse_mcps(config["mcps"] || []),
102
106
  prompt: config["prompt"],
103
107
  description: config["description"],
104
- vibe: config["vibe"] || false
108
+ vibe: config["vibe"] || false,
109
+ worktree: parse_worktree_value(config["worktree"])
105
110
  }
106
111
  end
107
112
 
@@ -188,5 +193,13 @@ module ClaudeSwarm
188
193
  def expand_path(path)
189
194
  Pathname.new(path).expand_path(@base_dir).to_s
190
195
  end
196
+
197
+ def parse_worktree_value(value)
198
+ return nil if value.nil? # Omitted means follow CLI behavior
199
+ return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
200
+ return value.to_s if value.is_a?(String) && !value.empty?
201
+
202
+ raise Error, "Invalid worktree value: #{value.inspect}. Must be true, false, or a non-empty string"
203
+ end
191
204
  end
192
205
  end
@@ -1,17 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "English"
3
4
  require "shellwords"
4
5
  require "json"
5
6
  require "fileutils"
6
7
  require_relative "session_path"
7
8
  require_relative "process_tracker"
9
+ require_relative "worktree_manager"
8
10
 
9
11
  module ClaudeSwarm
10
12
  class Orchestrator
11
13
  RUN_DIR = File.expand_path("~/.claude-swarm/run")
12
14
 
13
15
  def initialize(configuration, mcp_generator, vibe: false, prompt: nil, stream_logs: false, debug: false,
14
- restore_session_path: nil)
16
+ restore_session_path: nil, worktree: nil)
15
17
  @config = configuration
16
18
  @generator = mcp_generator
17
19
  @vibe = vibe
@@ -20,6 +22,15 @@ module ClaudeSwarm
20
22
  @debug = debug
21
23
  @restore_session_path = restore_session_path
22
24
  @session_path = nil
25
+ # Store worktree option for later use
26
+ @worktree_option = worktree
27
+ @needs_worktree_manager = worktree.is_a?(String) || worktree == "" ||
28
+ configuration.instances.values.any? { |inst| !inst[:worktree].nil? }
29
+ # Store modified instances after worktree setup
30
+ @modified_instances = nil
31
+
32
+ # Set environment variable for prompt mode to suppress output
33
+ ENV["CLAUDE_SWARM_PROMPT"] = "1" if @prompt
23
34
  end
24
35
 
25
36
  def start
@@ -50,6 +61,9 @@ module ClaudeSwarm
50
61
  # Set up signal handlers to clean up child processes
51
62
  setup_signal_handlers
52
63
 
64
+ # Check if the original session used worktrees
65
+ restore_worktrees_if_needed(session_path)
66
+
53
67
  # Regenerate MCP configurations with session IDs for restoration
54
68
  @generator.generate_all
55
69
  unless @prompt
@@ -68,6 +82,9 @@ module ClaudeSwarm
68
82
  SessionPath.ensure_directory(session_path)
69
83
  @session_path = session_path
70
84
 
85
+ # Extract session ID from path (the timestamp part)
86
+ @session_id = File.basename(session_path)
87
+
71
88
  ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
72
89
  ENV["CLAUDE_SWARM_START_DIR"] = Dir.pwd
73
90
 
@@ -85,6 +102,24 @@ module ClaudeSwarm
85
102
  # Set up signal handlers to clean up child processes
86
103
  setup_signal_handlers
87
104
 
105
+ # Create WorktreeManager if needed with session ID
106
+ if @needs_worktree_manager
107
+ cli_option = @worktree_option.is_a?(String) && !@worktree_option.empty? ? @worktree_option : nil
108
+ @worktree_manager = WorktreeManager.new(cli_option, session_id: @session_id)
109
+ puts "🌳 Setting up Git worktrees..." unless @prompt
110
+
111
+ # Get all instances for worktree setup
112
+ # Note: instances.values already includes the main instance
113
+ all_instances = @config.instances.values
114
+
115
+ @worktree_manager.setup_worktrees(all_instances)
116
+
117
+ unless @prompt
118
+ puts "✓ Worktrees created with branch: #{@worktree_manager.worktree_name}"
119
+ puts
120
+ end
121
+ end
122
+
88
123
  # Generate all MCP configuration files
89
124
  @generator.generate_all
90
125
  unless @prompt
@@ -96,7 +131,30 @@ module ClaudeSwarm
96
131
  save_swarm_config_path(session_path)
97
132
  end
98
133
 
99
- # Launch the main instance
134
+ # Execute before commands if specified
135
+ before_commands = @config.before_commands
136
+ if before_commands.any? && !@restore_session_path
137
+ unless @prompt
138
+ puts "⚙️ Executing before commands..."
139
+ puts
140
+ end
141
+
142
+ success = execute_before_commands(before_commands)
143
+ unless success
144
+ puts "❌ Before commands failed. Aborting swarm launch." unless @prompt
145
+ cleanup_processes
146
+ cleanup_run_symlink
147
+ cleanup_worktrees
148
+ exit 1
149
+ end
150
+
151
+ unless @prompt
152
+ puts "✓ Before commands completed successfully"
153
+ puts
154
+ end
155
+ end
156
+
157
+ # Launch the main instance (fetch after worktree setup to get modified paths)
100
158
  main_instance = @config.main_instance_config
101
159
  unless @prompt
102
160
  puts "🚀 Launching main instance: #{@config.main_instance}"
@@ -138,10 +196,66 @@ module ClaudeSwarm
138
196
  # Clean up child processes and run symlink
139
197
  cleanup_processes
140
198
  cleanup_run_symlink
199
+ cleanup_worktrees
141
200
  end
142
201
 
143
202
  private
144
203
 
204
+ def execute_before_commands(commands)
205
+ log_file = File.join(@session_path, "session.log") if @session_path
206
+
207
+ commands.each_with_index do |command, index|
208
+ # Log the command execution to session log
209
+ if @session_path
210
+ File.open(log_file, "a") do |f|
211
+ f.puts "[#{Time.now.strftime("%Y-%m-%d %H:%M:%S")}] Executing before command #{index + 1}/#{commands.size}: #{command}"
212
+ end
213
+ end
214
+
215
+ # Execute the command and capture output
216
+ begin
217
+ puts "Debug: Executing command #{index + 1}/#{commands.size}: #{command}" if @debug && !@prompt
218
+
219
+ # Use system with output capture
220
+ output = `#{command} 2>&1`
221
+ success = $CHILD_STATUS.success?
222
+
223
+ # Log the output
224
+ if @session_path
225
+ File.open(log_file, "a") do |f|
226
+ f.puts "Command output:"
227
+ f.puts output
228
+ f.puts "Exit status: #{$CHILD_STATUS.exitstatus}"
229
+ f.puts "-" * 80
230
+ end
231
+ end
232
+
233
+ # Show output if in debug mode or if command failed
234
+ if (@debug || !success) && !@prompt
235
+ puts "Command #{index + 1} output:"
236
+ puts output
237
+ puts "Exit status: #{$CHILD_STATUS.exitstatus}"
238
+ end
239
+
240
+ unless success
241
+ puts "❌ Before command #{index + 1} failed: #{command}" unless @prompt
242
+ return false
243
+ end
244
+ rescue StandardError => e
245
+ puts "Error executing before command #{index + 1}: #{e.message}" unless @prompt
246
+ if @session_path
247
+ File.open(log_file, "a") do |f|
248
+ f.puts "Error: #{e.message}"
249
+ f.puts "-" * 80
250
+ end
251
+ end
252
+ return false
253
+ end
254
+ end
255
+
256
+ true
257
+ end
258
+
145
259
  def save_swarm_config_path(session_path)
146
260
  # Copy the YAML config file to the session directory
147
261
  config_copy_path = File.join(session_path, "config.yml")
@@ -150,6 +264,20 @@ module ClaudeSwarm
150
264
  # Save the original working directory
151
265
  start_dir_file = File.join(session_path, "start_directory")
152
266
  File.write(start_dir_file, Dir.pwd)
267
+
268
+ # Save session metadata
269
+ metadata = {
270
+ "start_directory" => Dir.pwd,
271
+ "timestamp" => Time.now.utc.iso8601,
272
+ "swarm_name" => @config.swarm_name,
273
+ "claude_swarm_version" => VERSION
274
+ }
275
+
276
+ # Add worktree info if applicable
277
+ metadata["worktree"] = @worktree_manager.session_metadata if @worktree_manager
278
+
279
+ metadata_file = File.join(session_path, "session_metadata.json")
280
+ File.write(metadata_file, JSON.pretty_generate(metadata))
153
281
  end
154
282
 
155
283
  def setup_signal_handlers
@@ -158,6 +286,7 @@ module ClaudeSwarm
158
286
  puts "\n🛑 Received #{signal} signal, cleaning up..."
159
287
  cleanup_processes
160
288
  cleanup_run_symlink
289
+ cleanup_worktrees
161
290
  exit
162
291
  end
163
292
  end
@@ -170,6 +299,14 @@ module ClaudeSwarm
170
299
  puts "⚠️ Error during cleanup: #{e.message}"
171
300
  end
172
301
 
302
+ def cleanup_worktrees
303
+ return unless @worktree_manager
304
+
305
+ @worktree_manager.cleanup_worktrees
306
+ rescue StandardError => e
307
+ puts "⚠️ Error during worktree cleanup: #{e.message}"
308
+ end
309
+
173
310
  def create_run_symlink
174
311
  return unless @session_path
175
312
 
@@ -304,5 +441,31 @@ module ClaudeSwarm
304
441
  parts << "#{instance[:prompt]}\n\nNow just say 'I am ready to start'"
305
442
  end
306
443
  end
444
+
445
+ def restore_worktrees_if_needed(session_path)
446
+ metadata_file = File.join(session_path, "session_metadata.json")
447
+ return unless File.exist?(metadata_file)
448
+
449
+ metadata = JSON.parse(File.read(metadata_file))
450
+ worktree_data = metadata["worktree"]
451
+ return unless worktree_data && worktree_data["enabled"]
452
+
453
+ unless @prompt
454
+ puts "🌳 Restoring Git worktrees..."
455
+ puts
456
+ end
457
+
458
+ # Restore worktrees using the saved configuration
459
+ @worktree_manager = WorktreeManager.new(worktree_data["shared_name"])
460
+
461
+ # Get all instances and restore their worktree paths
462
+ all_instances = @config.instances.values
463
+ @worktree_manager.setup_worktrees(all_instances)
464
+
465
+ return if @prompt
466
+
467
+ puts "✓ Worktrees restored with branch: #{@worktree_manager.worktree_name}"
468
+ puts
469
+ end
307
470
  end
308
471
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeSwarm
4
- VERSION = "0.1.17"
4
+ VERSION = "0.1.18"
5
5
  end
@@ -0,0 +1,353 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "fileutils"
5
+ require "json"
6
+ require "pathname"
7
+ require "securerandom"
8
+
9
+ module ClaudeSwarm
10
+ class WorktreeManager
11
+ attr_reader :shared_worktree_name, :created_worktrees
12
+
13
+ def initialize(cli_worktree_option = nil, session_id: nil)
14
+ @cli_worktree_option = cli_worktree_option
15
+ @session_id = session_id
16
+ # Generate a name based on session ID if no option given, empty string, or default "worktree" from Thor
17
+ @shared_worktree_name = if cli_worktree_option.nil? || cli_worktree_option.empty? || cli_worktree_option == "worktree"
18
+ generate_worktree_name
19
+ else
20
+ cli_worktree_option
21
+ end
22
+ @created_worktrees = {} # Maps "repo_root:worktree_name" to worktree_path
23
+ @instance_worktree_configs = {} # Stores per-instance worktree settings
24
+ end
25
+
26
+ def setup_worktrees(instances)
27
+ # First pass: determine worktree configuration for each instance
28
+ instances.each do |instance|
29
+ worktree_config = determine_worktree_config(instance)
30
+ @instance_worktree_configs[instance[:name]] = worktree_config
31
+ end
32
+
33
+ # Second pass: create necessary worktrees
34
+ worktrees_to_create = collect_worktrees_to_create(instances)
35
+ worktrees_to_create.each do |repo_root, worktree_name|
36
+ create_worktree(repo_root, worktree_name)
37
+ end
38
+
39
+ # Third pass: map instance directories to worktree paths
40
+ instances.each do |instance|
41
+ worktree_config = @instance_worktree_configs[instance[:name]]
42
+
43
+ if ENV["CLAUDE_SWARM_DEBUG"]
44
+ puts "Debug [WorktreeManager]: Processing instance #{instance[:name]}"
45
+ puts "Debug [WorktreeManager]: Worktree config: #{worktree_config.inspect}"
46
+ end
47
+
48
+ next if worktree_config[:skip]
49
+
50
+ worktree_name = worktree_config[:name]
51
+ original_dirs = instance[:directories] || [instance[:directory]]
52
+ mapped_dirs = original_dirs.map { |dir| map_to_worktree_path(dir, worktree_name) }
53
+
54
+ if ENV["CLAUDE_SWARM_DEBUG"]
55
+ puts "Debug [WorktreeManager]: Original dirs: #{original_dirs.inspect}"
56
+ puts "Debug [WorktreeManager]: Mapped dirs: #{mapped_dirs.inspect}"
57
+ end
58
+
59
+ if instance[:directories]
60
+ instance[:directories] = mapped_dirs
61
+ # Also update the single directory field for backward compatibility
62
+ instance[:directory] = mapped_dirs.first
63
+ else
64
+ instance[:directory] = mapped_dirs.first
65
+ end
66
+
67
+ puts "Debug [WorktreeManager]: Updated instance[:directory] to: #{instance[:directory]}" if ENV["CLAUDE_SWARM_DEBUG"]
68
+ end
69
+ end
70
+
71
+ def map_to_worktree_path(original_path, worktree_name)
72
+ return original_path unless original_path
73
+
74
+ expanded_path = File.expand_path(original_path)
75
+ repo_root = find_git_root(expanded_path)
76
+
77
+ if ENV["CLAUDE_SWARM_DEBUG"]
78
+ puts "Debug [map_to_worktree_path]: Original path: #{original_path}"
79
+ puts "Debug [map_to_worktree_path]: Expanded path: #{expanded_path}"
80
+ puts "Debug [map_to_worktree_path]: Repo root: #{repo_root}"
81
+ end
82
+
83
+ return original_path unless repo_root
84
+
85
+ # Check if we have a worktree for this repo and name
86
+ worktree_key = "#{repo_root}:#{worktree_name}"
87
+ worktree_path = @created_worktrees[worktree_key]
88
+
89
+ if ENV["CLAUDE_SWARM_DEBUG"]
90
+ puts "Debug [map_to_worktree_path]: Worktree key: #{worktree_key}"
91
+ puts "Debug [map_to_worktree_path]: Worktree path: #{worktree_path}"
92
+ puts "Debug [map_to_worktree_path]: Created worktrees: #{@created_worktrees.inspect}"
93
+ end
94
+
95
+ return original_path unless worktree_path
96
+
97
+ # Calculate relative path from repo root
98
+ relative_path = Pathname.new(expanded_path).relative_path_from(Pathname.new(repo_root)).to_s
99
+
100
+ # Return the equivalent path in the worktree
101
+ result = if relative_path == "."
102
+ worktree_path
103
+ else
104
+ File.join(worktree_path, relative_path)
105
+ end
106
+
107
+ puts "Debug [map_to_worktree_path]: Result: #{result}" if ENV["CLAUDE_SWARM_DEBUG"]
108
+
109
+ result
110
+ end
111
+
112
+ def cleanup_worktrees
113
+ @created_worktrees.each do |worktree_key, worktree_path|
114
+ repo_root = worktree_key.split(":", 2).first
115
+ next unless File.exist?(worktree_path)
116
+
117
+ # Check for uncommitted changes
118
+ if has_uncommitted_changes?(worktree_path)
119
+ puts "⚠️ Warning: Worktree has uncommitted changes, skipping cleanup: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
120
+ next
121
+ end
122
+
123
+ # Check for unpushed commits
124
+ if has_unpushed_commits?(worktree_path)
125
+ puts "⚠️ Warning: Worktree has unpushed commits, skipping cleanup: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
126
+ next
127
+ end
128
+
129
+ puts "Removing worktree: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
130
+
131
+ # Remove the worktree
132
+ output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "remove", worktree_path)
133
+ next if status.success?
134
+
135
+ puts "Warning: Failed to remove worktree: #{output}"
136
+ # Try force remove
137
+ output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "remove", "--force", worktree_path)
138
+ puts "Force remove result: #{output}" unless status.success?
139
+ end
140
+ rescue StandardError => e
141
+ puts "Error during worktree cleanup: #{e.message}"
142
+ end
143
+
144
+ def session_metadata
145
+ {
146
+ enabled: true,
147
+ shared_name: @shared_worktree_name,
148
+ created_paths: @created_worktrees.dup,
149
+ instance_configs: @instance_worktree_configs.dup
150
+ }
151
+ end
152
+
153
+ # Deprecated method for backward compatibility
154
+ def worktree_name
155
+ @shared_worktree_name
156
+ end
157
+
158
+ private
159
+
160
+ def generate_worktree_name
161
+ # Use session ID if available, otherwise generate a random suffix
162
+ if @session_id
163
+ "worktree-#{@session_id}"
164
+ else
165
+ # Fallback to random suffix for tests or when session ID is not available
166
+ random_suffix = SecureRandom.alphanumeric(5).downcase
167
+ "worktree-#{random_suffix}"
168
+ end
169
+ end
170
+
171
+ def determine_worktree_config(instance)
172
+ # Check instance-level worktree setting
173
+ instance_worktree = instance[:worktree]
174
+
175
+ if instance_worktree.nil?
176
+ # No instance-level setting, follow CLI behavior
177
+ if @cli_worktree_option.nil?
178
+ { skip: true }
179
+ else
180
+ { skip: false, name: @shared_worktree_name }
181
+ end
182
+ elsif instance_worktree == false
183
+ # Explicitly disabled for this instance
184
+ { skip: true }
185
+ elsif instance_worktree == true
186
+ # Use shared worktree (either from CLI or auto-generated)
187
+ { skip: false, name: @shared_worktree_name }
188
+ elsif instance_worktree.is_a?(String)
189
+ # Use custom worktree name
190
+ { skip: false, name: instance_worktree }
191
+ else
192
+ raise Error, "Invalid worktree configuration for instance '#{instance[:name]}': #{instance_worktree.inspect}"
193
+ end
194
+ end
195
+
196
+ def collect_worktrees_to_create(instances)
197
+ worktrees_needed = {}
198
+
199
+ instances.each do |instance|
200
+ worktree_config = @instance_worktree_configs[instance[:name]]
201
+ next if worktree_config[:skip]
202
+
203
+ worktree_name = worktree_config[:name]
204
+ directories = instance[:directories] || [instance[:directory]]
205
+
206
+ directories.each do |dir|
207
+ next unless dir
208
+
209
+ expanded_dir = File.expand_path(dir)
210
+ repo_root = find_git_root(expanded_dir)
211
+ next unless repo_root
212
+
213
+ # Track unique repo_root:worktree_name combinations
214
+ worktrees_needed[repo_root] ||= Set.new
215
+ worktrees_needed[repo_root].add(worktree_name)
216
+ end
217
+ end
218
+
219
+ # Convert to array of [repo_root, worktree_name] pairs
220
+ result = []
221
+ worktrees_needed.each do |repo_root, worktree_names|
222
+ worktree_names.each do |worktree_name|
223
+ result << [repo_root, worktree_name]
224
+ end
225
+ end
226
+ result
227
+ end
228
+
229
+ def find_git_root(path)
230
+ current = File.expand_path(path)
231
+
232
+ while current != "/"
233
+ return current if File.exist?(File.join(current, ".git"))
234
+
235
+ current = File.dirname(current)
236
+ end
237
+
238
+ nil
239
+ end
240
+
241
+ def create_worktree(repo_root, worktree_name)
242
+ worktree_key = "#{repo_root}:#{worktree_name}"
243
+ # Create worktrees inside the repository in a .worktrees directory
244
+ worktree_base_dir = File.join(repo_root, ".worktrees")
245
+ worktree_path = File.join(worktree_base_dir, worktree_name)
246
+
247
+ # Check if worktree already exists
248
+ if File.exist?(worktree_path)
249
+ puts "Using existing worktree: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
250
+ @created_worktrees[worktree_key] = worktree_path
251
+ return
252
+ end
253
+
254
+ # Ensure .worktrees directory exists
255
+ FileUtils.mkdir_p(worktree_base_dir)
256
+
257
+ # Create .gitignore inside .worktrees to ignore all contents
258
+ gitignore_path = File.join(worktree_base_dir, ".gitignore")
259
+ File.write(gitignore_path, "# Ignore all worktree contents\n*\n") unless File.exist?(gitignore_path)
260
+
261
+ # Get current branch
262
+ output, status = Open3.capture2e("git", "-C", repo_root, "rev-parse", "--abbrev-ref", "HEAD")
263
+ raise Error, "Failed to get current branch in #{repo_root}: #{output}" unless status.success?
264
+
265
+ current_branch = output.strip
266
+
267
+ # Create worktree with a new branch based on current branch
268
+ branch_name = worktree_name
269
+ puts "Creating worktree: #{worktree_path} with branch: #{branch_name}" unless ENV["CLAUDE_SWARM_PROMPT"]
270
+
271
+ # Create worktree with a new branch
272
+ output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "add", "-b", branch_name, worktree_path, current_branch)
273
+
274
+ # If branch already exists, try without -b flag
275
+ if !status.success? && output.include?("already exists")
276
+ puts "Branch #{branch_name} already exists, using existing branch" unless ENV["CLAUDE_SWARM_PROMPT"]
277
+ output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "add", worktree_path, branch_name)
278
+ end
279
+
280
+ raise Error, "Failed to create worktree: #{output}" unless status.success?
281
+
282
+ @created_worktrees[worktree_key] = worktree_path
283
+ end
284
+
285
+ def has_uncommitted_changes?(worktree_path)
286
+ # Check if there are any uncommitted changes (staged or unstaged)
287
+ output, status = Open3.capture2e("git", "-C", worktree_path, "status", "--porcelain")
288
+ return false unless status.success?
289
+
290
+ # If output is not empty, there are changes
291
+ !output.strip.empty?
292
+ end
293
+
294
+ def has_unpushed_commits?(worktree_path)
295
+ # Get the current branch
296
+ branch_output, branch_status = Open3.capture2e("git", "-C", worktree_path, "rev-parse", "--abbrev-ref", "HEAD")
297
+ return false unless branch_status.success?
298
+
299
+ current_branch = branch_output.strip
300
+
301
+ # Check if the branch has an upstream
302
+ _, upstream_status = Open3.capture2e("git", "-C", worktree_path, "rev-parse", "--abbrev-ref", "#{current_branch}@{upstream}")
303
+
304
+ # If no upstream, check if there are any commits on this branch
305
+ unless upstream_status.success?
306
+ # Get the base branch (usually main or master)
307
+ base_branch = find_base_branch(worktree_path)
308
+
309
+ # If we can't find a base branch or this IS the base branch, check if there are any commits at all
310
+ if base_branch.nil? || current_branch == base_branch
311
+ # Check if this branch has any commits
312
+ commits_output, commits_status = Open3.capture2e("git", "-C", worktree_path, "rev-list", "--count", "HEAD")
313
+ return false unless commits_status.success?
314
+
315
+ # If there's more than 0 commits and no upstream, they're unpushed
316
+ return commits_output.strip.to_i.positive?
317
+ end
318
+
319
+ # Check if this branch has any commits not on the base branch
320
+ commits_output, commits_status = Open3.capture2e("git", "-C", worktree_path, "rev-list", "HEAD", "^#{base_branch}")
321
+ return false unless commits_status.success?
322
+
323
+ # If there are commits, they're unpushed (no upstream set)
324
+ return !commits_output.strip.empty?
325
+ end
326
+
327
+ # Check for unpushed commits
328
+ unpushed_output, unpushed_status = Open3.capture2e("git", "-C", worktree_path, "rev-list", "HEAD", "^#{current_branch}@{upstream}")
329
+ return false unless unpushed_status.success?
330
+
331
+ # If output is not empty, there are unpushed commits
332
+ !unpushed_output.strip.empty?
333
+ end
334
+
335
+ def find_base_branch(repo_path)
336
+ # Try to find the base branch - check for main, master, or the default branch
337
+ %w[main master].each do |branch|
338
+ _, status = Open3.capture2e("git", "-C", repo_path, "rev-parse", "--verify", "refs/heads/#{branch}")
339
+ return branch if status.success?
340
+ end
341
+
342
+ # Try to get the default branch from HEAD
343
+ output, status = Open3.capture2e("git", "-C", repo_path, "symbolic-ref", "refs/remotes/origin/HEAD")
344
+ if status.success?
345
+ # Extract branch name from refs/remotes/origin/main
346
+ branch_match = output.strip.match(%r{refs/remotes/origin/(.+)$})
347
+ return branch_match[1] if branch_match
348
+ end
349
+
350
+ nil
351
+ end
352
+ end
353
+ end
data/llms.txt CHANGED
@@ -57,7 +57,7 @@ A collection of Claude instances (agents) working together. One instance is desi
57
57
  An individual Claude Code agent with:
58
58
  - **description** (required): Role and responsibilities
59
59
  - **directory**: Working directory context
60
- - **model**: Claude model (opus/sonnet/haiku)
60
+ - **model**: Claude model (opus/sonnet/claude-3-5-haiku-20241022)
61
61
  - **connections**: Other instances it can delegate to
62
62
  - **allowed_tools**: Tools this instance can use
63
63
  - **disallowed_tools**: Explicitly denied tools (override allowed)
@@ -79,7 +79,7 @@ swarm:
79
79
  instance_name:
80
80
  description: "Agent role description" # REQUIRED
81
81
  directory: ~/path/to/dir # Working directory
82
- model: opus # opus/sonnet/haiku
82
+ model: opus # opus/sonnet/claude-3-5-haiku-20241022
83
83
  connections: [other1, other2] # Connected instances
84
84
  prompt: "Custom system prompt" # Additional instructions
85
85
  vibe: false # Skip permissions (default: false)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude_swarm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.17
4
+ version: 0.1.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda
@@ -67,6 +67,7 @@ files:
67
67
  - example/test-generation.yml
68
68
  - examples/monitoring-demo.yml
69
69
  - examples/multi-directory.yml
70
+ - examples/with-before-commands.yml
70
71
  - exe/claude-swarm
71
72
  - lib/claude_swarm.rb
72
73
  - lib/claude_swarm/claude_code_executor.rb
@@ -83,6 +84,7 @@ files:
83
84
  - lib/claude_swarm/session_path.rb
84
85
  - lib/claude_swarm/task_tool.rb
85
86
  - lib/claude_swarm/version.rb
87
+ - lib/claude_swarm/worktree_manager.rb
86
88
  - llms.txt
87
89
  homepage: https://github.com/parruda/claude-swarm
88
90
  licenses: []