claude_swarm 0.1.12 → 0.1.13

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: c468e30d7de839a1b7bb88c2e2ee1c9bfa58b8bb0f0b0375b3495ebf5de340f0
4
- data.tar.gz: 7234799e8b24cc2b0a691f188a890ee84d0780a77a85723307bdc9f17ad71545
3
+ metadata.gz: 57e09297e72fe54ef127b5f7b5789c9e9d825279404ffa96c29f2ff0e7fbc622
4
+ data.tar.gz: 6e1d719d97d2f8fefa4ec8d0e9396e5fba867012b94d88d915247aea18690523
5
5
  SHA512:
6
- metadata.gz: 7ed4d3460c8e686f683513796939827419d254d9b4a168a211d24d89cb18aaeb4cacf42a2dd90d1b88ecbc98ab97f7391b356ed24f0beb05da4bab6c62ce5332
7
- data.tar.gz: ced867554fa8b43c6ce9f327f791212dcc04a87c378d78e99c87f41776074eba7f2a1bf7691b6b9daaf34fcd7923b057cefe453171d5838f2981ee87d67237f1
6
+ metadata.gz: 8ad9a411c8bc503829927ea534fed74e9f525dd62d68fc6eb9978667afdb40435b9bac0c13320c097f1f41556a8d50215c723c8066dec7366cf7d847d07a7e9e
7
+ data.tar.gz: 490824249ac673b7904f05a084d2ff0190a1034529a9302f423d00e965cd7c793b519b1d8e318a36a1e58af80706cb1fda80dd507af525b45f7df26299abd3f9
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ ## [Unreleased]
2
+
3
+ ### Added
4
+ - **Session restoration support (Experimental)**: Session management with the ability to resume previous Claude Swarm sessions. Note: This is an experimental feature with limitations - the main instance's conversation context is not fully restored
5
+ - New `--session-id` flag to resume a session by ID or path
6
+ - New `list-sessions` command to view available sessions with metadata
7
+ - Automatic capture and persistence of Claude session IDs for all instances
8
+ - Individual instance states stored in `state/` directory with instance ID as filename (e.g., `state/lead_abc123.json`)
9
+ - Swarm configuration copied to session directory as `config.yml` for restoration
10
+ - **Instance ID tracking**: Each instance now gets a unique ID in the format `instance_name_<hex>` for better identification in logs
11
+ - **Enhanced logging with instance IDs**: All log messages now include instance IDs when available (e.g., `lead (lead_1234abcd) -> backend (backend_5678efgh)`)
12
+ - **Calling instance ID propagation**: When one instance calls another, both the calling instance name and ID are passed for complete tracking
13
+ - Instance IDs are stored in MCP configuration files with `instance_id` and `instance_name` fields
14
+ - New CLI options: `--instance-id` and `--calling-instance-id` for the `mcp-serve` command
15
+ - ClaudeCodeExecutor now tracks and logs both instance and calling instance IDs
16
+ - **Process tracking and cleanup**: Added automatic tracking and cleanup of child MCP server processes
17
+ - New `ProcessTracker` class creates individual PID files in a `pids/` directory within the session path
18
+ - Signal handlers (INT, TERM, QUIT) ensure all child processes are terminated when the main instance exits
19
+ - Prevents orphaned MCP server processes from continuing to run after swarm termination
20
+
21
+ ### Changed
22
+ - Human-readable logs improved to show instance IDs in parentheses after instance names for easier tracking of multi-instance interactions
23
+ - `log_request` method enhanced to include instance IDs in structured JSON logs
24
+ - Configuration class now accepts optional `base_dir` parameter to support session restoration from different directories
25
+
26
+ ### Fixed
27
+ - Fixed issue where child MCP server processes would continue running after the main instance exits
28
+
1
29
  ## [0.1.12]
2
30
  ### Added
3
31
  - **Circular dependency detection**: Configuration validation now detects and reports circular dependencies between instances
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Claude Swarm
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/claude_swarm.svg?cache_bust=0.1.11)](https://badge.fury.io/rb/claude_swarm)
3
+ [![Gem Version](https://badge.fury.io/rb/claude_swarm.svg?cache_bust=0.1.12)](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.
@@ -80,7 +80,7 @@ claude-swarm --vibe # That will allow ALL tools for all instances! Be Careful!
80
80
  This will:
81
81
  - Launch the main instance (lead) with connections to other instances
82
82
  - The lead instance can communicate with the other instances via MCP
83
- - All session files are stored in `.claude-swarm/sessions/{timestamp}/`
83
+ - All session files are stored in `~/.claude-swarm/sessions/{project}/{timestamp}/` (customizable via `CLAUDE_SWARM_HOME`)
84
84
 
85
85
  #### Multi-Level Swarm Example
86
86
 
@@ -446,6 +446,14 @@ claude-swarm --vibe
446
446
  claude-swarm -p "Implement the new user authentication feature"
447
447
  claude-swarm --prompt "Fix the bug in the payment module"
448
448
 
449
+ # Resume a previous session by ID
450
+ claude-swarm --session-id 20241206_143022
451
+ claude-swarm --session-id ~/path/to/session
452
+
453
+ # List available sessions
454
+ claude-swarm list-sessions
455
+ claude-swarm list-sessions --limit 20
456
+
449
457
  # Show version
450
458
  claude-swarm version
451
459
 
@@ -457,6 +465,70 @@ claude-swarm tools-mcp --allowed-tools 'Read,Edit' --disallowed-tools 'Edit(*.lo
457
465
  claude-swarm mcp-serve INSTANCE_NAME --config CONFIG_FILE --session-timestamp TIMESTAMP
458
466
  ```
459
467
 
468
+ ### Session Management and Restoration (Experimental)
469
+
470
+ Claude Swarm provides experimental session management with restoration capabilities. **Note: This feature is experimental and has limitations - the main instance's conversation context is not fully restored.**
471
+
472
+ #### Session Structure
473
+ All session files are organized in `~/.claude-swarm/sessions/{project}/{timestamp}/`:
474
+ - `config.yml`: Copy of the original swarm configuration
475
+ - `state/`: Directory containing individual instance states
476
+ - `{instance_id}.json`: Claude session ID and status for each instance (e.g., `lead_abc123.json`)
477
+ - `{instance_name}.mcp.json`: MCP configuration files
478
+ - `session.log`: Human-readable request/response tracking
479
+ - `session.log.json`: All events in JSONL format (one JSON per line)
480
+ - `permissions.log`: Permission checks and decisions
481
+
482
+ #### Listing Sessions
483
+ View your previous Claude Swarm sessions:
484
+
485
+ ```bash
486
+ # List recent sessions (default: 10)
487
+ claude-swarm list-sessions
488
+
489
+ # List more sessions
490
+ claude-swarm list-sessions --limit 20
491
+ ```
492
+
493
+ Output shows:
494
+ - Session ID (timestamp)
495
+ - Creation time
496
+ - Main instance name
497
+ - Number of instances
498
+ - Configuration file used
499
+ - Full session path
500
+
501
+ #### Resuming Sessions
502
+ Resume a previous session with all instances restored to their Claude session states:
503
+
504
+ ```bash
505
+ # Resume by session ID
506
+ claude-swarm --session-id 20241206_143022
507
+
508
+ # Resume by full path
509
+ claude-swarm --session-id ~/.claude-swarm/sessions/my-project/20241206_143022
510
+ ```
511
+
512
+ This will:
513
+ 1. Load the session manifest and instance states
514
+ 2. Restore the original swarm configuration
515
+ 3. Resume the main instance with its Claude session ID
516
+ 4. Restore all connected instances with their session IDs
517
+ 5. Maintain the same working directories and tool permissions
518
+
519
+ #### How Session Restoration Works
520
+ - Each instance's Claude session ID is automatically captured and persisted
521
+ - Instance states are stored in separate files named by instance ID to prevent concurrency issues
522
+ - MCP configurations are regenerated with the saved session IDs
523
+ - The main instance uses Claude's `--resume` flag (limited effectiveness)
524
+ - Connected instances receive their session IDs via `--claude-session-id`
525
+
526
+ **Important Limitations:**
527
+ - The main instance's conversation history and context are not fully restored
528
+ - Only the session ID is preserved, not the actual conversation state
529
+ - Connected instances restore more reliably than the main instance
530
+ - This is an experimental feature and may not work as expected
531
+
460
532
  ## How It Works
461
533
 
462
534
  1. **Configuration Parsing**: Claude Swarm reads your YAML configuration and validates it
@@ -471,20 +543,17 @@ claude-swarm mcp-serve INSTANCE_NAME --config CONFIG_FILE --session-timestamp TI
471
543
  - Eliminates the need to manually accept each tool or use global `--vibe` mode
472
544
  - Per-instance `vibe: true` skips all permission checks for that specific instance
473
545
  - The permission MCP uses `--permission-prompt-tool` to check tool access
474
- - Permission decisions are logged to `.claude-swarm/sessions/{timestamp}/permissions.log`
475
- 4. **Session Management**: Claude Swarm maintains session continuity:
476
- - Generates a shared session timestamp for all instances
477
- - Each instance can maintain its own Claude session ID
478
- - Sessions can be reset via the MCP server interface
546
+ - Permission decisions are logged to `~/.claude-swarm/sessions/{project}/{timestamp}/permissions.log`
547
+ 4. **Session Persistence**: Claude Swarm automatically tracks session state:
548
+ - Generates a shared session path for all instances
549
+ - Each instance's Claude session ID is captured and saved
550
+ - Instance states are stored using instance IDs as filenames to avoid conflicts
551
+ - Sessions can be fully restored with all instances reconnected
479
552
  5. **Main Instance Launch**: The main instance is launched with its MCP configuration, giving it access to all connected instances
480
553
  6. **Inter-Instance Communication**: Connected instances expose themselves as MCP servers with these tools:
481
554
  - **task**: Execute tasks using Claude Code with configurable tools and return results. The tool description includes the instance name and description (e.g., "Execute a task using Agent frontend_dev. Frontend developer specializing in React and TypeScript")
482
555
  - **session_info**: Get current Claude session information including ID and working directory
483
556
  - **reset_session**: Reset the Claude session for a fresh start
484
- 7. **Session Management**: All session files are organized in `.claude-swarm/sessions/{timestamp}/`:
485
- - MCP configuration files: `{instance_name}.mcp.json`
486
- - Session log: `session.log` with detailed request/response tracking
487
- - Permission log: `permissions.log` with all permission checks and decisions
488
557
 
489
558
  ## Troubleshooting
490
559
 
@@ -507,14 +576,15 @@ claude-swarm mcp-serve INSTANCE_NAME --config CONFIG_FILE --session-timestamp TI
507
576
  ### Debug Output
508
577
 
509
578
  The swarm will display:
510
- - Session directory location (`.claude-swarm/sessions/{timestamp}/`)
579
+ - Session directory location (`~/.claude-swarm/sessions/{project}/{timestamp}/`)
511
580
  - Main instance details (model, directory, tools, connections)
512
581
  - The exact command being run
513
582
 
514
583
  ### Session Files
515
584
 
516
- Check the session directory `.claude-swarm/sessions/{timestamp}/` for:
517
- - `session.log`: Detailed logs with request/response tracking
585
+ Check the session directory `~/.claude-swarm/sessions/{project}/{timestamp}/` for:
586
+ - `session.log`: Human-readable logs with request/response tracking
587
+ - `session.log.json`: All events in JSONL format (one JSON object per line)
518
588
  - `{instance}.mcp.json`: MCP configuration for each instance
519
589
  - All files for a session are kept together for easy review
520
590
 
@@ -0,0 +1,19 @@
1
+ version: 1
2
+ swarm:
3
+ name: "Session Restoration Demo"
4
+ main: coordinator
5
+ instances:
6
+ coordinator:
7
+ description: "Main coordinator that manages the team"
8
+ directory: .
9
+ model: sonnet
10
+ prompt: "You are the team coordinator. You help manage tasks and coordinate between team members."
11
+ allowed_tools: [Read, Edit, Bash]
12
+ connections: [developer]
13
+
14
+ developer:
15
+ description: "Developer who writes code"
16
+ directory: .
17
+ model: sonnet
18
+ prompt: "You are a skilled developer who writes clean, well-tested code."
19
+ allowed_tools: [Read, Edit, Write, Bash]
@@ -10,15 +10,19 @@ module ClaudeSwarm
10
10
  class ClaudeCodeExecutor
11
11
  attr_reader :session_id, :last_response, :working_directory, :logger, :session_path
12
12
 
13
- def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false, instance_name: nil, calling_instance: nil)
13
+ def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false,
14
+ instance_name: nil, instance_id: nil, calling_instance: nil, calling_instance_id: nil,
15
+ claude_session_id: nil)
14
16
  @working_directory = working_directory
15
17
  @model = model
16
18
  @mcp_config = mcp_config
17
19
  @vibe = vibe
18
- @session_id = nil
20
+ @session_id = claude_session_id
19
21
  @last_response = nil
20
22
  @instance_name = instance_name
23
+ @instance_id = instance_id
21
24
  @calling_instance = calling_instance
25
+ @calling_instance_id = calling_instance_id
22
26
 
23
27
  # Setup logging
24
28
  setup_logging
@@ -51,7 +55,10 @@ module ClaudeSwarm
51
55
  log_streaming_event(json_data)
52
56
 
53
57
  # Capture session_id from system init
54
- @session_id = json_data["session_id"] if json_data["type"] == "system" && json_data["subtype"] == "init"
58
+ if json_data["type"] == "system" && json_data["subtype"] == "init"
59
+ @session_id = json_data["session_id"]
60
+ write_instance_state
61
+ end
55
62
 
56
63
  # Capture the final result
57
64
  result_response = json_data if json_data["type"] == "result"
@@ -92,6 +99,27 @@ module ClaudeSwarm
92
99
 
93
100
  private
94
101
 
102
+ def write_instance_state
103
+ return unless @instance_id && @session_id
104
+
105
+ state_dir = File.join(@session_path, "state")
106
+ FileUtils.mkdir_p(state_dir)
107
+
108
+ state_file = File.join(state_dir, "#{@instance_id}.json")
109
+ state_data = {
110
+ instance_name: @instance_name,
111
+ instance_id: @instance_id,
112
+ claude_session_id: @session_id,
113
+ status: "active",
114
+ updated_at: Time.now.iso8601
115
+ }
116
+
117
+ File.write(state_file, JSON.pretty_generate(state_data))
118
+ @logger.info("Wrote instance state for #{@instance_name} (#{@instance_id}) with session ID: #{@session_id}")
119
+ rescue StandardError => e
120
+ @logger.error("Failed to write instance state for #{@instance_name} (#{@instance_id}): #{e.message}")
121
+ end
122
+
95
123
  def setup_logging
96
124
  # Use session path from environment (required)
97
125
  @session_path = SessionPath.from_env
@@ -108,16 +136,41 @@ module ClaudeSwarm
108
136
  "[#{datetime.strftime("%Y-%m-%d %H:%M:%S.%L")}] [#{severity}] #{msg}\n"
109
137
  end
110
138
 
111
- @logger.info("Started Claude Code executor for instance: #{@instance_name}") if @instance_name
139
+ return unless @instance_name
140
+
141
+ instance_info = @instance_name
142
+ instance_info += " (#{@instance_id})" if @instance_id
143
+ @logger.info("Started Claude Code executor for instance: #{instance_info}")
112
144
  end
113
145
 
114
146
  def log_request(prompt)
115
- @logger.info("#{@calling_instance} -> #{@instance_name}: \n---\n#{prompt}\n---")
147
+ caller_info = @calling_instance
148
+ caller_info += " (#{@calling_instance_id})" if @calling_instance_id
149
+ instance_info = @instance_name
150
+ instance_info += " (#{@instance_id})" if @instance_id
151
+ @logger.info("#{caller_info} -> #{instance_info}: \n---\n#{prompt}\n---")
152
+
153
+ # Build event hash for JSON logging
154
+ event = {
155
+ type: "request",
156
+ from_instance: @calling_instance,
157
+ from_instance_id: @calling_instance_id,
158
+ to_instance: @instance_name,
159
+ to_instance_id: @instance_id,
160
+ prompt: prompt,
161
+ timestamp: Time.now.iso8601
162
+ }
163
+
164
+ append_to_session_json(event)
116
165
  end
117
166
 
118
167
  def log_response(response)
168
+ caller_info = @calling_instance
169
+ caller_info += " (#{@calling_instance_id})" if @calling_instance_id
170
+ instance_info = @instance_name
171
+ instance_info += " (#{@instance_id})" if @instance_id
119
172
  @logger.info(
120
- "($#{response["total_cost"]} - #{response["duration_ms"]}ms) #{@instance_name} -> #{@calling_instance}: \n---\n#{response["result"]}\n---"
173
+ "($#{response["total_cost"]} - #{response["duration_ms"]}ms) #{instance_info} -> #{caller_info}: \n---\n#{response["result"]}\n---"
121
174
  )
122
175
  end
123
176
 
@@ -151,14 +204,18 @@ module ClaudeSwarm
151
204
  arguments = tool_call["input"].to_json
152
205
  arguments = "#{arguments[0..300]} ...}" if arguments.length > 300
153
206
 
207
+ instance_info = @instance_name
208
+ instance_info += " (#{@instance_id})" if @instance_id
154
209
  @logger.info(
155
- "Tool call from #{@instance_name} -> Tool: #{tool_call["name"]}, ID: #{tool_call["id"]}, Arguments: #{arguments}"
210
+ "Tool call from #{instance_info} -> Tool: #{tool_call["name"]}, ID: #{tool_call["id"]}, Arguments: #{arguments}"
156
211
  )
157
212
  end
158
213
 
159
214
  text = content.select { |c| c["type"] == "text" }
160
215
  text.each do |t|
161
- @logger.info("#{@instance_name} is thinking:\n---\n#{t["text"]}\n---")
216
+ instance_info = @instance_name
217
+ instance_info += " (#{@instance_id})" if @instance_id
218
+ @logger.info("#{instance_info} is thinking:\n---\n#{t["text"]}\n---")
162
219
  end
163
220
  end
164
221
 
@@ -177,7 +234,9 @@ module ClaudeSwarm
177
234
  # Create entry with metadata
178
235
  entry = {
179
236
  instance: @instance_name,
237
+ instance_id: @instance_id,
180
238
  calling_instance: @calling_instance,
239
+ calling_instance_id: @calling_instance_id,
181
240
  timestamp: Time.now.iso8601,
182
241
  event: event
183
242
  }
@@ -6,24 +6,29 @@ require_relative "claude_code_executor"
6
6
  require_relative "task_tool"
7
7
  require_relative "session_info_tool"
8
8
  require_relative "reset_session_tool"
9
+ require_relative "process_tracker"
9
10
 
10
11
  module ClaudeSwarm
11
12
  class ClaudeMcpServer
12
13
  # Class variables to share state with tool classes
13
14
  class << self
14
- attr_accessor :executor, :instance_config, :logger, :session_path, :calling_instance
15
+ attr_accessor :executor, :instance_config, :logger, :session_path, :calling_instance, :calling_instance_id
15
16
  end
16
17
 
17
- def initialize(instance_config, calling_instance:)
18
+ def initialize(instance_config, calling_instance:, calling_instance_id: nil)
18
19
  @instance_config = instance_config
19
20
  @calling_instance = calling_instance
21
+ @calling_instance_id = calling_instance_id
20
22
  @executor = ClaudeCodeExecutor.new(
21
23
  working_directory: instance_config[:directory],
22
24
  model: instance_config[:model],
23
25
  mcp_config: instance_config[:mcp_config_path],
24
26
  vibe: instance_config[:vibe],
25
27
  instance_name: instance_config[:name],
26
- calling_instance: calling_instance
28
+ instance_id: instance_config[:instance_id],
29
+ calling_instance: calling_instance,
30
+ calling_instance_id: calling_instance_id,
31
+ claude_session_id: instance_config[:claude_session_id]
27
32
  )
28
33
 
29
34
  # Set class variables so tools can access them
@@ -32,9 +37,16 @@ module ClaudeSwarm
32
37
  self.class.logger = @executor.logger
33
38
  self.class.session_path = @executor.session_path
34
39
  self.class.calling_instance = @calling_instance
40
+ self.class.calling_instance_id = @calling_instance_id
35
41
  end
36
42
 
37
43
  def start
44
+ # Track this process
45
+ if @executor.session_path && File.exist?(@executor.session_path)
46
+ tracker = ProcessTracker.new(@executor.session_path)
47
+ tracker.track_pid(Process.pid, "mcp_#{@instance_config[:name]}")
48
+ end
49
+
38
50
  server = FastMcp::Server.new(
39
51
  name: @instance_config[:name],
40
52
  version: "1.0.0"
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
+ require "json"
4
5
  require_relative "configuration"
5
6
  require_relative "mcp_generator"
6
7
  require_relative "orchestrator"
@@ -24,7 +25,15 @@ module ClaudeSwarm
24
25
  desc: "Stream session logs to stdout (only works with -p)"
25
26
  method_option :debug, type: :boolean, default: false,
26
27
  desc: "Enable debug output"
28
+ method_option :session_id, type: :string,
29
+ desc: "Resume a previous session by ID or path"
27
30
  def start(config_file = nil)
31
+ # Handle session restoration
32
+ if options[:session_id]
33
+ restore_session(options[:session_id])
34
+ return
35
+ end
36
+
28
37
  config_path = config_file || options[:config]
29
38
  unless File.exist?(config_path)
30
39
  error "Configuration file not found: #{config_path}"
@@ -81,6 +90,12 @@ module ClaudeSwarm
81
90
  desc: "Run with --dangerously-skip-permissions"
82
91
  method_option :calling_instance, type: :string, required: true,
83
92
  desc: "Name of the instance that launched this MCP server"
93
+ method_option :calling_instance_id, type: :string,
94
+ desc: "Unique ID of the instance that launched this MCP server"
95
+ method_option :instance_id, type: :string,
96
+ desc: "Unique ID of this instance"
97
+ method_option :claude_session_id, type: :string,
98
+ desc: "Claude session ID to resume"
84
99
  def mcp_serve
85
100
  instance_config = {
86
101
  name: options[:name],
@@ -91,11 +106,17 @@ module ClaudeSwarm
91
106
  tools: options[:tools] || [],
92
107
  disallowed_tools: options[:disallowed_tools] || [],
93
108
  mcp_config_path: options[:mcp_config_path],
94
- vibe: options[:vibe]
109
+ vibe: options[:vibe],
110
+ instance_id: options[:instance_id],
111
+ claude_session_id: options[:claude_session_id]
95
112
  }
96
113
 
97
114
  begin
98
- server = ClaudeMcpServer.new(instance_config, calling_instance: options[:calling_instance])
115
+ server = ClaudeMcpServer.new(
116
+ instance_config,
117
+ calling_instance: options[:calling_instance],
118
+ calling_instance_id: options[:calling_instance_id]
119
+ )
99
120
  server.start
100
121
  rescue StandardError => e
101
122
  error "Error starting MCP server: #{e.message}"
@@ -171,6 +192,79 @@ module ClaudeSwarm
171
192
  say "Claude Swarm #{VERSION}"
172
193
  end
173
194
 
195
+ desc "list-sessions", "List all available Claude Swarm sessions"
196
+ method_option :limit, aliases: "-l", type: :numeric, default: 10,
197
+ desc: "Maximum number of sessions to display"
198
+ def list_sessions
199
+ sessions_dir = File.expand_path("~/.claude-swarm/sessions")
200
+ unless Dir.exist?(sessions_dir)
201
+ say "No sessions found", :yellow
202
+ return
203
+ end
204
+
205
+ # Find all sessions with MCP configs
206
+ sessions = []
207
+ Dir.glob("#{sessions_dir}/*/*/*.mcp.json").each do |mcp_path|
208
+ session_path = File.dirname(mcp_path)
209
+ session_id = File.basename(session_path)
210
+ project_name = File.basename(File.dirname(session_path))
211
+
212
+ # Skip if we've already processed this session
213
+ next if sessions.any? { |s| s[:path] == session_path }
214
+
215
+ # Try to load session info
216
+ config_file = File.join(session_path, "config.yml")
217
+ next unless File.exist?(config_file)
218
+
219
+ # Load the config to get swarm info
220
+ config_data = YAML.load_file(config_file)
221
+ swarm_name = config_data.dig("swarm", "name") || "Unknown"
222
+ main_instance = config_data.dig("swarm", "main") || "Unknown"
223
+
224
+ mcp_files = Dir.glob(File.join(session_path, "*.mcp.json"))
225
+
226
+ # Get creation time from directory
227
+ created_at = File.stat(session_path).ctime
228
+
229
+ sessions << {
230
+ path: session_path,
231
+ id: session_id,
232
+ project: project_name,
233
+ created_at: created_at,
234
+ main_instance: main_instance,
235
+ instances_count: mcp_files.size,
236
+ swarm_name: swarm_name,
237
+ config_path: config_file
238
+ }
239
+ rescue StandardError
240
+ # Skip invalid manifests
241
+ next
242
+ end
243
+
244
+ if sessions.empty?
245
+ say "No sessions found", :yellow
246
+ return
247
+ end
248
+
249
+ # Sort by creation time (newest first)
250
+ sessions.sort_by! { |s| -s[:created_at].to_i }
251
+ sessions = sessions.first(options[:limit])
252
+
253
+ # Display sessions
254
+ say "\nAvailable sessions (newest first):\n", :bold
255
+ sessions.each do |session|
256
+ say "\n#{session[:project]}/#{session[:id]}", :green
257
+ say " Created: #{session[:created_at].strftime("%Y-%m-%d %H:%M:%S")}"
258
+ say " Main: #{session[:main_instance]}"
259
+ say " Instances: #{session[:instances_count]}"
260
+ say " Swarm: #{session[:swarm_name]}"
261
+ say " Config: #{session[:config_path]}", :cyan
262
+ end
263
+
264
+ say "\nTo resume a session, run:", :bold
265
+ say " claude-swarm --session-id <session-id>", :cyan
266
+ end
267
+
174
268
  desc "tools-mcp", "Start a permission management MCP server for tool access control"
175
269
  method_option :allowed_tools, aliases: "-t", type: :string,
176
270
  desc: "Comma-separated list of allowed tool patterns (supports wildcards)"
@@ -194,5 +288,77 @@ module ClaudeSwarm
194
288
  def error(message)
195
289
  say message, :red
196
290
  end
291
+
292
+ def restore_session(session_id)
293
+ say "Restoring session: #{session_id}", :green
294
+
295
+ # Find the session path
296
+ session_path = find_session_path(session_id)
297
+ unless session_path
298
+ error "Session not found: #{session_id}"
299
+ exit 1
300
+ end
301
+
302
+ begin
303
+ # Load session info from instance ID in MCP config
304
+ mcp_files = Dir.glob(File.join(session_path, "*.mcp.json"))
305
+ if mcp_files.empty?
306
+ error "No MCP configuration files found in session"
307
+ exit 1
308
+ end
309
+
310
+ # Load the configuration from the session directory
311
+ config_file = File.join(session_path, "config.yml")
312
+
313
+ unless File.exist?(config_file)
314
+ error "Configuration file not found in session"
315
+ exit 1
316
+ end
317
+
318
+ # Change to the original start directory if it exists
319
+ start_dir_file = File.join(session_path, "start_directory")
320
+ if File.exist?(start_dir_file)
321
+ original_dir = File.read(start_dir_file).strip
322
+ if Dir.exist?(original_dir)
323
+ Dir.chdir(original_dir)
324
+ say "Changed to original directory: #{original_dir}", :green unless options[:prompt]
325
+ else
326
+ error "Original directory no longer exists: #{original_dir}"
327
+ exit 1
328
+ end
329
+ end
330
+
331
+ config = Configuration.new(config_file, base_dir: Dir.pwd)
332
+
333
+ # Create orchestrator with restoration mode
334
+ generator = McpGenerator.new(config, vibe: options[:vibe], restore_session_path: session_path)
335
+ orchestrator = Orchestrator.new(config, generator,
336
+ vibe: options[:vibe],
337
+ prompt: options[:prompt],
338
+ stream_logs: options[:stream_logs],
339
+ debug: options[:debug],
340
+ restore_session_path: session_path)
341
+ orchestrator.start
342
+ rescue StandardError => e
343
+ error "Failed to restore session: #{e.message}"
344
+ error e.backtrace.join("\n") if options[:debug]
345
+ exit 1
346
+ end
347
+ end
348
+
349
+ def find_session_path(session_id)
350
+ sessions_dir = File.expand_path("~/.claude-swarm/sessions")
351
+
352
+ # Check if it's a full path
353
+ return session_id if File.exist?(File.join(session_id, "config.yml"))
354
+
355
+ # Search for the session ID in all projects
356
+ Dir.glob("#{sessions_dir}/*/#{session_id}").each do |path|
357
+ config_path = File.join(path, "config.yml")
358
+ return path if File.exist?(config_path)
359
+ end
360
+
361
+ nil
362
+ end
197
363
  end
198
364
  end
@@ -5,11 +5,11 @@ require "pathname"
5
5
 
6
6
  module ClaudeSwarm
7
7
  class Configuration
8
- attr_reader :config, :swarm_name, :main_instance, :instances
8
+ attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances
9
9
 
10
- def initialize(config_path)
10
+ def initialize(config_path, base_dir: nil)
11
11
  @config_path = Pathname.new(config_path).expand_path
12
- @config_dir = @config_path.dirname
12
+ @config_dir = base_dir || @config_path.dirname
13
13
  load_and_validate
14
14
  end
15
15
 
@@ -60,11 +60,11 @@ module ClaudeSwarm
60
60
  end
61
61
 
62
62
  def parse_swarm
63
- swarm = @config["swarm"]
64
- @swarm_name = swarm["name"]
65
- @main_instance = swarm["main"]
63
+ @swarm = @config["swarm"]
64
+ @swarm_name = @swarm["name"]
65
+ @main_instance = @swarm["main"]
66
66
  @instances = {}
67
- swarm["instances"].each do |name, config|
67
+ @swarm["instances"].each do |name, config|
68
68
  @instances[name] = parse_instance(name, config)
69
69
  end
70
70
  validate_connections
@@ -3,19 +3,33 @@
3
3
  require "json"
4
4
  require "fileutils"
5
5
  require "shellwords"
6
+ require "securerandom"
6
7
  require_relative "session_path"
7
8
 
8
9
  module ClaudeSwarm
9
10
  class McpGenerator
10
- def initialize(configuration, vibe: false)
11
+ def initialize(configuration, vibe: false, restore_session_path: nil)
11
12
  @config = configuration
12
13
  @vibe = vibe
14
+ @restore_session_path = restore_session_path
13
15
  @session_path = nil # Will be set when needed
16
+ @instance_ids = {} # Store instance IDs for all instances
17
+ @restore_states = {} # Store loaded state data during restoration
14
18
  end
15
19
 
16
20
  def generate_all
17
21
  ensure_swarm_directory
18
22
 
23
+ if @restore_session_path
24
+ # Load existing instance IDs and states from state files
25
+ load_instance_states
26
+ else
27
+ # Generate new instance IDs
28
+ @config.instances.each_key do |name|
29
+ @instance_ids[name] = "#{name}_#{SecureRandom.hex(4)}"
30
+ end
31
+ end
32
+
19
33
  @config.instances.each do |name, instance|
20
34
  generate_mcp_config(name, instance)
21
35
  end
@@ -48,13 +62,18 @@ module ClaudeSwarm
48
62
  # Add connection MCPs for other instances
49
63
  instance[:connections].each do |connection_name|
50
64
  connected_instance = @config.instances[connection_name]
51
- mcp_servers[connection_name] = build_instance_mcp_config(connection_name, connected_instance, calling_instance: name)
65
+ mcp_servers[connection_name] = build_instance_mcp_config(
66
+ connection_name, connected_instance,
67
+ calling_instance: name, calling_instance_id: @instance_ids[name]
68
+ )
52
69
  end
53
70
 
54
71
  # Add permission MCP server if not in vibe mode (global or instance-specific)
55
72
  mcp_servers["permissions"] = build_permission_mcp_config(instance[:tools], instance[:disallowed_tools]) unless @vibe || instance[:vibe]
56
73
 
57
74
  config = {
75
+ "instance_id" => @instance_ids[name],
76
+ "instance_name" => name,
58
77
  "mcpServers" => mcp_servers
59
78
  }
60
79
 
@@ -79,7 +98,7 @@ module ClaudeSwarm
79
98
  end
80
99
  end
81
100
 
82
- def build_instance_mcp_config(name, instance, calling_instance:)
101
+ def build_instance_mcp_config(name, instance, calling_instance:, calling_instance_id:)
83
102
  # Get the path to the claude-swarm executable
84
103
  exe_path = "claude-swarm"
85
104
 
@@ -104,8 +123,18 @@ module ClaudeSwarm
104
123
 
105
124
  args.push("--calling-instance", calling_instance) if calling_instance
106
125
 
126
+ args.push("--calling-instance-id", calling_instance_id) if calling_instance_id
127
+
128
+ args.push("--instance-id", @instance_ids[name]) if @instance_ids[name]
129
+
107
130
  args.push("--vibe") if @vibe || instance[:vibe]
108
131
 
132
+ # Add claude session ID if restoring
133
+ if @restore_states[name.to_s]
134
+ claude_session_id = @restore_states[name.to_s]["claude_session_id"]
135
+ args.push("--claude-session-id", claude_session_id) if claude_session_id
136
+ end
137
+
109
138
  {
110
139
  "type" => "stdio",
111
140
  "command" => exe_path,
@@ -113,6 +142,27 @@ module ClaudeSwarm
113
142
  }
114
143
  end
115
144
 
145
+ def load_instance_states
146
+ state_dir = File.join(@restore_session_path, "state")
147
+ return unless Dir.exist?(state_dir)
148
+
149
+ Dir.glob(File.join(state_dir, "*.json")).each do |state_file|
150
+ data = JSON.parse(File.read(state_file))
151
+ instance_name = data["instance_name"]
152
+ instance_id = data["instance_id"]
153
+
154
+ # Check both string and symbol keys since config instances might have either
155
+ if instance_name && (@config.instances.key?(instance_name) || @config.instances.key?(instance_name.to_sym))
156
+ # Store with the same key type as in @config.instances
157
+ key = @config.instances.key?(instance_name) ? instance_name : instance_name.to_sym
158
+ @instance_ids[key] = instance_id
159
+ @restore_states[instance_name] = data
160
+ end
161
+ rescue StandardError
162
+ # Skip invalid state files
163
+ end
164
+ end
165
+
116
166
  def build_permission_mcp_config(allowed_tools, disallowed_tools)
117
167
  exe_path = "claude-swarm"
118
168
 
@@ -1,43 +1,88 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "shellwords"
4
+ require "json"
5
+ require "fileutils"
4
6
  require_relative "session_path"
7
+ require_relative "process_tracker"
5
8
 
6
9
  module ClaudeSwarm
7
10
  class Orchestrator
8
- def initialize(configuration, mcp_generator, vibe: false, prompt: nil, stream_logs: false, debug: false)
11
+ def initialize(configuration, mcp_generator, vibe: false, prompt: nil, stream_logs: false, debug: false,
12
+ restore_session_path: nil)
9
13
  @config = configuration
10
14
  @generator = mcp_generator
11
15
  @vibe = vibe
12
16
  @prompt = prompt
13
17
  @stream_logs = stream_logs
14
18
  @debug = debug
19
+ @restore_session_path = restore_session_path
15
20
  end
16
21
 
17
22
  def start
18
- unless @prompt
19
- puts "🐝 Starting Claude Swarm: #{@config.swarm_name}"
20
- puts "😎 Vibe mode ON" if @vibe
21
- puts
22
- end
23
+ if @restore_session_path
24
+ unless @prompt
25
+ puts "🔄 Restoring Claude Swarm: #{@config.swarm_name}"
26
+ puts "😎 Vibe mode ON" if @vibe
27
+ puts
28
+ end
23
29
 
24
- # Generate and set session path for all instances
25
- session_path = SessionPath.generate(working_dir: Dir.pwd)
26
- SessionPath.ensure_directory(session_path)
30
+ # Use existing session path
31
+ session_path = @restore_session_path
32
+ ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
33
+ ENV["CLAUDE_SWARM_START_DIR"] = Dir.pwd
27
34
 
28
- ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
29
- ENV["CLAUDE_SWARM_START_DIR"] = Dir.pwd
35
+ unless @prompt
36
+ puts "📝 Using existing session: #{session_path}/"
37
+ puts
38
+ end
30
39
 
31
- unless @prompt
32
- puts "📝 Session files will be saved to: #{session_path}/"
33
- puts
34
- end
40
+ # Initialize process tracker
41
+ @process_tracker = ProcessTracker.new(session_path)
35
42
 
36
- # Generate all MCP configuration files
37
- @generator.generate_all
38
- unless @prompt
39
- puts "✓ Generated MCP configurations in session directory"
40
- puts
43
+ # Set up signal handlers to clean up child processes
44
+ setup_signal_handlers
45
+
46
+ # Regenerate MCP configurations with session IDs for restoration
47
+ @generator.generate_all
48
+ unless @prompt
49
+ puts "✓ Regenerated MCP configurations with session IDs"
50
+ puts
51
+ end
52
+ else
53
+ unless @prompt
54
+ puts "🐝 Starting Claude Swarm: #{@config.swarm_name}"
55
+ puts "😎 Vibe mode ON" if @vibe
56
+ puts
57
+ end
58
+
59
+ # Generate and set session path for all instances
60
+ session_path = SessionPath.generate(working_dir: Dir.pwd)
61
+ SessionPath.ensure_directory(session_path)
62
+
63
+ ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
64
+ ENV["CLAUDE_SWARM_START_DIR"] = Dir.pwd
65
+
66
+ unless @prompt
67
+ puts "📝 Session files will be saved to: #{session_path}/"
68
+ puts
69
+ end
70
+
71
+ # Initialize process tracker
72
+ @process_tracker = ProcessTracker.new(session_path)
73
+
74
+ # Set up signal handlers to clean up child processes
75
+ setup_signal_handlers
76
+
77
+ # Generate all MCP configuration files
78
+ @generator.generate_all
79
+ unless @prompt
80
+ puts "✓ Generated MCP configurations in session directory"
81
+ puts
82
+ end
83
+
84
+ # Save swarm config path for restoration
85
+ save_swarm_config_path(session_path)
41
86
  end
42
87
 
43
88
  # Launch the main instance
@@ -69,14 +114,44 @@ module ClaudeSwarm
69
114
  end
70
115
 
71
116
  # Clean up log streaming thread
72
- return unless log_thread
117
+ if log_thread
118
+ log_thread.terminate
119
+ log_thread.join
120
+ end
73
121
 
74
- log_thread.terminate
75
- log_thread.join
122
+ # Clean up child processes
123
+ cleanup_processes
76
124
  end
77
125
 
78
126
  private
79
127
 
128
+ def save_swarm_config_path(session_path)
129
+ # Copy the YAML config file to the session directory
130
+ config_copy_path = File.join(session_path, "config.yml")
131
+ FileUtils.cp(@config.config_path, config_copy_path)
132
+
133
+ # Save the original working directory
134
+ start_dir_file = File.join(session_path, "start_directory")
135
+ File.write(start_dir_file, Dir.pwd)
136
+ end
137
+
138
+ def setup_signal_handlers
139
+ %w[INT TERM QUIT].each do |signal|
140
+ Signal.trap(signal) do
141
+ puts "\n🛑 Received #{signal} signal, cleaning up..."
142
+ cleanup_processes
143
+ exit
144
+ end
145
+ end
146
+ end
147
+
148
+ def cleanup_processes
149
+ @process_tracker.cleanup_all
150
+ puts "✓ Cleanup complete"
151
+ rescue StandardError => e
152
+ puts "⚠️ Error during cleanup: #{e.message}"
153
+ end
154
+
80
155
  def start_log_streaming
81
156
  Thread.new do
82
157
  session_log_path = File.join(ENV.fetch("CLAUDE_SWARM_SESSION_PATH", nil), "session.log")
@@ -110,6 +185,26 @@ module ClaudeSwarm
110
185
  instance[:model]
111
186
  ]
112
187
 
188
+ # Add resume flag if restoring session
189
+ if @restore_session_path
190
+ # Look for main instance state file
191
+ main_instance_name = @config.main_instance
192
+ state_files = Dir.glob(File.join(@restore_session_path, "state", "*.json"))
193
+
194
+ # Find the state file for the main instance
195
+ state_files.each do |state_file|
196
+ state_data = JSON.parse(File.read(state_file))
197
+ next unless state_data["instance_name"] == main_instance_name
198
+
199
+ claude_session_id = state_data["claude_session_id"]
200
+ if claude_session_id
201
+ parts << "--resume"
202
+ parts << claude_session_id
203
+ end
204
+ break
205
+ end
206
+ end
207
+
113
208
  if @vibe || instance[:vibe]
114
209
  parts << "--dangerously-skip-permissions"
115
210
  else
@@ -6,6 +6,7 @@ require "logger"
6
6
  require "fileutils"
7
7
  require_relative "permission_tool"
8
8
  require_relative "session_path"
9
+ require_relative "process_tracker"
9
10
 
10
11
  module ClaudeSwarm
11
12
  class PermissionMcpServer
@@ -46,6 +47,13 @@ module ClaudeSwarm
46
47
  end
47
48
 
48
49
  def create_and_start_server
50
+ # Track this process
51
+ session_path = SessionPath.from_env
52
+ if session_path && File.exist?(session_path)
53
+ tracker = ProcessTracker.new(session_path)
54
+ tracker.track_pid(Process.pid, "mcp_permissions")
55
+ end
56
+
49
57
  server = FastMcp::Server.new(
50
58
  name: SERVER_NAME,
51
59
  version: SERVER_VERSION
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module ClaudeSwarm
6
+ class ProcessTracker
7
+ PIDS_DIR = "pids"
8
+
9
+ def initialize(session_path)
10
+ @session_path = session_path
11
+ @pids_dir = File.join(@session_path, PIDS_DIR)
12
+ ensure_pids_directory
13
+ end
14
+
15
+ def track_pid(pid, name)
16
+ pid_file = File.join(@pids_dir, pid.to_s)
17
+ File.write(pid_file, name)
18
+ end
19
+
20
+ def cleanup_all
21
+ return unless Dir.exist?(@pids_dir)
22
+
23
+ # Get all PID files
24
+ pid_files = Dir.glob(File.join(@pids_dir, "*"))
25
+
26
+ pid_files.each do |pid_file|
27
+ pid = File.basename(pid_file).to_i
28
+ name = begin
29
+ File.read(pid_file).strip
30
+ rescue StandardError
31
+ "unknown"
32
+ end
33
+
34
+ begin
35
+ # Check if process is still running
36
+ Process.kill(0, pid)
37
+ # If we get here, process is running, so kill it
38
+ Process.kill("TERM", pid)
39
+ puts "✓ Terminated MCP server: #{name} (PID: #{pid})"
40
+
41
+ # Give it a moment to terminate gracefully
42
+ sleep 0.1
43
+
44
+ # Force kill if still running
45
+ begin
46
+ Process.kill(0, pid)
47
+ Process.kill("KILL", pid)
48
+ puts " → Force killed #{name} (PID: #{pid})"
49
+ rescue Errno::ESRCH
50
+ # Process is gone, which is what we want
51
+ end
52
+ rescue Errno::ESRCH
53
+ # Process not found, already terminated
54
+ puts " → MCP server #{name} (PID: #{pid}) already terminated"
55
+ rescue Errno::EPERM
56
+ # Permission denied
57
+ puts " ⚠️ No permission to terminate #{name} (PID: #{pid})"
58
+ end
59
+ end
60
+
61
+ # Clean up the pids directory
62
+ FileUtils.rm_rf(@pids_dir)
63
+ end
64
+
65
+ def self.cleanup_session(session_path)
66
+ return unless Dir.exist?(File.join(session_path, PIDS_DIR))
67
+
68
+ tracker = new(session_path)
69
+ tracker.cleanup_all
70
+ end
71
+
72
+ private
73
+
74
+ def ensure_pids_directory
75
+ FileUtils.mkdir_p(@pids_dir)
76
+ end
77
+ end
78
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeSwarm
4
- VERSION = "0.1.12"
4
+ VERSION = "0.1.13"
5
5
  end
data/lib/claude_swarm.rb CHANGED
@@ -2,10 +2,18 @@
2
2
 
3
3
  require_relative "claude_swarm/version"
4
4
  require_relative "claude_swarm/cli"
5
+ require_relative "claude_swarm/configuration"
6
+ require_relative "claude_swarm/mcp_generator"
7
+ require_relative "claude_swarm/orchestrator"
5
8
  require_relative "claude_swarm/claude_code_executor"
6
9
  require_relative "claude_swarm/claude_mcp_server"
7
10
  require_relative "claude_swarm/permission_tool"
8
11
  require_relative "claude_swarm/permission_mcp_server"
12
+ require_relative "claude_swarm/session_path"
13
+ require_relative "claude_swarm/session_info_tool"
14
+ require_relative "claude_swarm/reset_session_tool"
15
+ require_relative "claude_swarm/task_tool"
16
+ require_relative "claude_swarm/process_tracker"
9
17
 
10
18
  module ClaudeSwarm
11
19
  class Error < StandardError; end
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.12
4
+ version: 0.1.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda
@@ -61,6 +61,7 @@ files:
61
61
  - claude-swarm.yml
62
62
  - example/claude-swarm.yml
63
63
  - example/microservices-team.yml
64
+ - example/session-restoration-demo.yml
64
65
  - example/test-generation.yml
65
66
  - exe/claude-swarm
66
67
  - lib/claude_swarm.rb
@@ -72,6 +73,7 @@ files:
72
73
  - lib/claude_swarm/orchestrator.rb
73
74
  - lib/claude_swarm/permission_mcp_server.rb
74
75
  - lib/claude_swarm/permission_tool.rb
76
+ - lib/claude_swarm/process_tracker.rb
75
77
  - lib/claude_swarm/reset_session_tool.rb
76
78
  - lib/claude_swarm/session_info_tool.rb
77
79
  - lib/claude_swarm/session_path.rb