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 +4 -4
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +48 -0
- data/README.md +133 -1
- data/claude-swarm.yml +2 -14
- data/examples/with-before-commands.yml +30 -0
- data/lib/claude_swarm/cli.rb +23 -3
- data/lib/claude_swarm/configuration.rb +14 -1
- data/lib/claude_swarm/orchestrator.rb +165 -2
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm/worktree_manager.rb +353 -0
- data/llms.txt +2 -2
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1aaf0eadabd67afb0bd79738de8c7fcbfb65887d477a2c0c002017cff9de15ec
|
4
|
+
data.tar.gz: 777f04c044a2204477d1328dc31b0b5361fa0007d0cf637254d3531260e3149d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
[](https://badge.fury.io/rb/claude_swarm)
|
4
4
|
[](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
|
-
|
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]
|
data/lib/claude_swarm/cli.rb
CHANGED
@@ -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
|
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
|
-
#
|
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
|
data/lib/claude_swarm/version.rb
CHANGED
@@ -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.
|
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: []
|