enhance_swarm 1.0.0
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 +7 -0
- data/.enhance_swarm/agent_scripts/frontend_agent.md +39 -0
- data/.enhance_swarm/user_patterns.json +37 -0
- data/CHANGELOG.md +184 -0
- data/LICENSE +21 -0
- data/PRODUCTION_TEST_LOG.md +502 -0
- data/README.md +905 -0
- data/Rakefile +28 -0
- data/USAGE_EXAMPLES.md +477 -0
- data/examples/enhance_workflow.md +346 -0
- data/examples/rails_project.md +253 -0
- data/exe/enhance-swarm +30 -0
- data/lib/enhance_swarm/additional_commands.rb +299 -0
- data/lib/enhance_swarm/agent_communicator.rb +460 -0
- data/lib/enhance_swarm/agent_reviewer.rb +283 -0
- data/lib/enhance_swarm/agent_spawner.rb +462 -0
- data/lib/enhance_swarm/cleanup_manager.rb +245 -0
- data/lib/enhance_swarm/cli.rb +1592 -0
- data/lib/enhance_swarm/command_executor.rb +78 -0
- data/lib/enhance_swarm/configuration.rb +324 -0
- data/lib/enhance_swarm/control_agent.rb +307 -0
- data/lib/enhance_swarm/dependency_validator.rb +195 -0
- data/lib/enhance_swarm/error_recovery.rb +785 -0
- data/lib/enhance_swarm/generator.rb +194 -0
- data/lib/enhance_swarm/interrupt_handler.rb +512 -0
- data/lib/enhance_swarm/logger.rb +106 -0
- data/lib/enhance_swarm/mcp_integration.rb +85 -0
- data/lib/enhance_swarm/monitor.rb +28 -0
- data/lib/enhance_swarm/notification_manager.rb +444 -0
- data/lib/enhance_swarm/orchestrator.rb +313 -0
- data/lib/enhance_swarm/output_streamer.rb +281 -0
- data/lib/enhance_swarm/process_monitor.rb +266 -0
- data/lib/enhance_swarm/progress_tracker.rb +215 -0
- data/lib/enhance_swarm/project_analyzer.rb +612 -0
- data/lib/enhance_swarm/resource_manager.rb +177 -0
- data/lib/enhance_swarm/retry_handler.rb +40 -0
- data/lib/enhance_swarm/session_manager.rb +247 -0
- data/lib/enhance_swarm/signal_handler.rb +95 -0
- data/lib/enhance_swarm/smart_defaults.rb +708 -0
- data/lib/enhance_swarm/task_integration.rb +150 -0
- data/lib/enhance_swarm/task_manager.rb +174 -0
- data/lib/enhance_swarm/version.rb +5 -0
- data/lib/enhance_swarm/visual_dashboard.rb +555 -0
- data/lib/enhance_swarm/web_ui.rb +211 -0
- data/lib/enhance_swarm.rb +69 -0
- data/setup.sh +86 -0
- data/sig/enhance_swarm.rbs +4 -0
- data/templates/claude/CLAUDE.md +160 -0
- data/templates/claude/MCP.md +117 -0
- data/templates/claude/PERSONAS.md +114 -0
- data/templates/claude/RULES.md +221 -0
- data/test_builtin_functionality.rb +121 -0
- data/test_core_components.rb +156 -0
- data/test_real_claude_integration.rb +285 -0
- data/test_security.rb +150 -0
- data/test_smart_defaults.rb +155 -0
- data/test_task_integration.rb +173 -0
- data/test_web_ui.rb +245 -0
- data/web/assets/css/main.css +645 -0
- data/web/assets/js/kanban.js +499 -0
- data/web/assets/js/main.js +525 -0
- data/web/templates/dashboard.html.erb +226 -0
- data/web/templates/kanban.html.erb +193 -0
- metadata +293 -0
@@ -0,0 +1,462 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'tempfile'
|
5
|
+
require_relative 'command_executor'
|
6
|
+
require_relative 'session_manager'
|
7
|
+
require_relative 'logger'
|
8
|
+
require_relative 'resource_manager'
|
9
|
+
|
10
|
+
module EnhanceSwarm
|
11
|
+
class AgentSpawner
|
12
|
+
def initialize
|
13
|
+
@config = EnhanceSwarm.configuration
|
14
|
+
@session_manager = SessionManager.new
|
15
|
+
@resource_manager = ResourceManager.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def spawn_agent(role:, task:, worktree: true)
|
19
|
+
Logger.info("Spawning #{role} agent for task: #{task}")
|
20
|
+
|
21
|
+
# Check resource limits before spawning
|
22
|
+
resource_check = @resource_manager.can_spawn_agent?
|
23
|
+
unless resource_check[:allowed]
|
24
|
+
Logger.error("Cannot spawn agent - resource limits exceeded:")
|
25
|
+
resource_check[:reasons].each { |reason| Logger.error(" - #{reason}") }
|
26
|
+
return false
|
27
|
+
end
|
28
|
+
|
29
|
+
begin
|
30
|
+
# Create worktree if requested
|
31
|
+
worktree_path = nil
|
32
|
+
if worktree
|
33
|
+
worktree_path = create_agent_worktree(role)
|
34
|
+
return false unless worktree_path
|
35
|
+
end
|
36
|
+
|
37
|
+
# Generate agent prompt
|
38
|
+
prompt = build_agent_prompt(task, role, worktree_path)
|
39
|
+
|
40
|
+
# Spawn the agent process
|
41
|
+
pid = spawn_claude_process(prompt, role, worktree_path)
|
42
|
+
return false unless pid
|
43
|
+
|
44
|
+
# Register agent in session
|
45
|
+
success = @session_manager.add_agent(role, pid, worktree_path, task)
|
46
|
+
|
47
|
+
if success
|
48
|
+
Logger.info("Successfully spawned #{role} agent (PID: #{pid})")
|
49
|
+
{ pid: pid, worktree_path: worktree_path, role: role }
|
50
|
+
else
|
51
|
+
Logger.error("Failed to register agent in session")
|
52
|
+
cleanup_failed_spawn(pid, worktree_path)
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
rescue StandardError => e
|
57
|
+
Logger.error("Failed to spawn #{role} agent: #{e.message}")
|
58
|
+
cleanup_failed_spawn(nil, worktree_path)
|
59
|
+
false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def spawn_multiple_agents(agents)
|
64
|
+
results = []
|
65
|
+
|
66
|
+
agents.each_with_index do |agent_config, index|
|
67
|
+
# Add jitter to prevent resource contention
|
68
|
+
sleep(2 + rand(0..2)) if index > 0
|
69
|
+
|
70
|
+
result = spawn_agent(
|
71
|
+
role: agent_config[:role],
|
72
|
+
task: agent_config[:task],
|
73
|
+
worktree: agent_config.fetch(:worktree, true)
|
74
|
+
)
|
75
|
+
|
76
|
+
results << result if result
|
77
|
+
end
|
78
|
+
|
79
|
+
results
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_running_agents
|
83
|
+
@session_manager.check_agent_processes
|
84
|
+
end
|
85
|
+
|
86
|
+
def stop_agent(pid)
|
87
|
+
begin
|
88
|
+
Process.kill('TERM', pid.to_i)
|
89
|
+
@session_manager.update_agent_status(pid, 'stopped', Time.now.iso8601)
|
90
|
+
Logger.info("Stopped agent with PID: #{pid}")
|
91
|
+
true
|
92
|
+
rescue Errno::ESRCH
|
93
|
+
# Process already stopped
|
94
|
+
@session_manager.update_agent_status(pid, 'stopped', Time.now.iso8601)
|
95
|
+
true
|
96
|
+
rescue StandardError => e
|
97
|
+
Logger.error("Failed to stop agent (PID: #{pid}): #{e.message}")
|
98
|
+
false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def stop_all_agents
|
103
|
+
active_agents = @session_manager.get_active_agents
|
104
|
+
stopped_count = 0
|
105
|
+
|
106
|
+
active_agents.each do |agent|
|
107
|
+
if stop_agent(agent[:pid])
|
108
|
+
stopped_count += 1
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
Logger.info("Stopped #{stopped_count}/#{active_agents.length} agents")
|
113
|
+
stopped_count
|
114
|
+
end
|
115
|
+
|
116
|
+
def claude_cli_available?
|
117
|
+
@claude_cli_available ||= begin
|
118
|
+
result = `claude --version 2>/dev/null`
|
119
|
+
$?.success? && result.strip.length > 0
|
120
|
+
rescue StandardError
|
121
|
+
false
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def create_agent_worktree(role)
|
128
|
+
timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
|
129
|
+
worktree_name = "#{role}-#{timestamp}"
|
130
|
+
worktree_path = File.join('.enhance_swarm', 'worktrees', worktree_name)
|
131
|
+
|
132
|
+
begin
|
133
|
+
# Ensure worktrees directory exists
|
134
|
+
worktrees_dir = File.dirname(worktree_path)
|
135
|
+
FileUtils.mkdir_p(worktrees_dir) unless Dir.exist?(worktrees_dir)
|
136
|
+
|
137
|
+
# Check if we have any commits (required for git worktree)
|
138
|
+
ensure_initial_commit
|
139
|
+
|
140
|
+
# Create git worktree
|
141
|
+
CommandExecutor.execute('git', 'worktree', 'add', worktree_path)
|
142
|
+
|
143
|
+
Logger.info("Created worktree for #{role} agent: #{worktree_path}")
|
144
|
+
File.expand_path(worktree_path)
|
145
|
+
|
146
|
+
rescue CommandExecutor::CommandError => e
|
147
|
+
Logger.error("Failed to create worktree for #{role}: #{e.message}")
|
148
|
+
|
149
|
+
# If the error is about no commits, try to create one
|
150
|
+
if e.message.include?('does not have any commits yet')
|
151
|
+
Logger.info("No initial commit found, creating one...")
|
152
|
+
if create_initial_commit
|
153
|
+
retry
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def ensure_initial_commit
|
162
|
+
# Check if we have any commits
|
163
|
+
result = CommandExecutor.execute('git', 'log', '--oneline', '-1')
|
164
|
+
true
|
165
|
+
rescue CommandExecutor::CommandError
|
166
|
+
# No commits exist, create initial commit
|
167
|
+
create_initial_commit
|
168
|
+
end
|
169
|
+
|
170
|
+
def create_initial_commit
|
171
|
+
begin
|
172
|
+
# Add all files to git
|
173
|
+
CommandExecutor.execute('git', 'add', '.')
|
174
|
+
|
175
|
+
# Create initial commit
|
176
|
+
CommandExecutor.execute('git', 'commit', '-m', 'Initial commit - EnhanceSwarm setup')
|
177
|
+
|
178
|
+
Logger.info("Created initial git commit for EnhanceSwarm")
|
179
|
+
true
|
180
|
+
rescue CommandExecutor::CommandError => e
|
181
|
+
Logger.error("Failed to create initial commit: #{e.message}")
|
182
|
+
false
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def spawn_claude_process(prompt, role, worktree_path)
|
187
|
+
begin
|
188
|
+
# Check if Claude CLI is available
|
189
|
+
unless claude_cli_available?
|
190
|
+
Logger.error("Claude CLI not available - falling back to simulation mode")
|
191
|
+
return spawn_simulated_process(role, worktree_path)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Prepare the enhanced prompt for the agent
|
195
|
+
enhanced_prompt = build_enhanced_agent_prompt(prompt, role, worktree_path)
|
196
|
+
|
197
|
+
# Create working directory for the agent
|
198
|
+
agent_dir = worktree_path || Dir.pwd
|
199
|
+
|
200
|
+
# Prepare environment
|
201
|
+
env = build_agent_environment(role, agent_dir)
|
202
|
+
|
203
|
+
# Create a temporary script to handle the Claude interaction
|
204
|
+
script_file = create_agent_script(enhanced_prompt, role, agent_dir)
|
205
|
+
|
206
|
+
# Ensure logs directory exists
|
207
|
+
FileUtils.mkdir_p(File.join('.enhance_swarm', 'logs'))
|
208
|
+
|
209
|
+
# Spawn the Claude process
|
210
|
+
pid = Process.spawn(
|
211
|
+
env,
|
212
|
+
'/bin/bash', script_file,
|
213
|
+
chdir: agent_dir,
|
214
|
+
out: File.join('.enhance_swarm', 'logs', "#{role}_output.log"),
|
215
|
+
err: File.join('.enhance_swarm', 'logs', "#{role}_error.log")
|
216
|
+
)
|
217
|
+
|
218
|
+
# Don't wait for the process - let it run independently
|
219
|
+
Process.detach(pid)
|
220
|
+
|
221
|
+
Logger.info("Spawned Claude agent process: #{role} (PID: #{pid})")
|
222
|
+
pid
|
223
|
+
|
224
|
+
rescue StandardError => e
|
225
|
+
Logger.error("Failed to spawn Claude process for #{role}: #{e.message}")
|
226
|
+
|
227
|
+
# Enhanced debugging information
|
228
|
+
if ENV['ENHANCE_SWARM_DEBUG'] == 'true'
|
229
|
+
Logger.error("Debug info - Error class: #{e.class}")
|
230
|
+
Logger.error("Debug info - Backtrace: #{e.backtrace.first(3).join(', ')}")
|
231
|
+
Logger.error("Debug info - Working directory: #{agent_dir}")
|
232
|
+
Logger.error("Debug info - Claude CLI available: #{claude_cli_available?}")
|
233
|
+
end
|
234
|
+
|
235
|
+
# Fall back to simulation mode
|
236
|
+
spawn_simulated_process(role, worktree_path)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def build_enhanced_agent_prompt(base_prompt, role, worktree_path)
|
241
|
+
config = EnhanceSwarm.configuration
|
242
|
+
|
243
|
+
<<~PROMPT
|
244
|
+
You are a specialized #{role.upcase} agent working as part of an EnhanceSwarm multi-agent team.
|
245
|
+
|
246
|
+
## Your Role: #{role.capitalize}
|
247
|
+
#{get_role_description(role)}
|
248
|
+
|
249
|
+
## Working Context:
|
250
|
+
- Project: #{config.project_name}
|
251
|
+
- Technology Stack: #{config.technology_stack}
|
252
|
+
- Working Directory: #{worktree_path || Dir.pwd}
|
253
|
+
- Code Standards: #{config.code_standards.join(', ')}
|
254
|
+
|
255
|
+
## Your Task:
|
256
|
+
#{base_prompt}
|
257
|
+
|
258
|
+
## Important Instructions:
|
259
|
+
1. Stay focused on your role as a #{role} specialist
|
260
|
+
2. Follow the project's code standards and conventions
|
261
|
+
3. Work autonomously but consider integration with other agents
|
262
|
+
4. Create high-quality, production-ready code
|
263
|
+
5. Include comprehensive tests where appropriate
|
264
|
+
6. Document your changes and decisions
|
265
|
+
7. If you encounter permission issues, provide detailed implementation plans instead
|
266
|
+
8. Always output what you would implement, even if file operations fail
|
267
|
+
|
268
|
+
## Available Tools:
|
269
|
+
You have access to all Claude Code tools for file editing, terminal commands, and project analysis.
|
270
|
+
Note: If file write operations fail due to permissions, focus on providing comprehensive
|
271
|
+
implementation details and code that could be manually applied.
|
272
|
+
|
273
|
+
Begin working on your assigned task now.
|
274
|
+
PROMPT
|
275
|
+
end
|
276
|
+
|
277
|
+
def get_role_description(role)
|
278
|
+
case role.to_s.downcase
|
279
|
+
when 'backend'
|
280
|
+
'You specialize in server-side logic, APIs, database design, models, and business logic implementation.'
|
281
|
+
when 'frontend'
|
282
|
+
'You specialize in user interfaces, client-side code, styling, user experience, and presentation layer.'
|
283
|
+
when 'qa'
|
284
|
+
'You specialize in testing, quality assurance, test automation, edge case analysis, and validation.'
|
285
|
+
when 'ux'
|
286
|
+
'You specialize in user experience design, interaction flows, accessibility, and user-centric improvements.'
|
287
|
+
when 'general'
|
288
|
+
'You are a general-purpose agent capable of handling various development tasks across the full stack.'
|
289
|
+
else
|
290
|
+
"You are a #{role} specialist agent focusing on your area of expertise."
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def create_agent_script(prompt, role, working_dir)
|
295
|
+
# Create a temporary script file that will run Claude
|
296
|
+
script_file = Tempfile.new(['agent_script', '.sh'])
|
297
|
+
|
298
|
+
begin
|
299
|
+
script_content = <<~SCRIPT
|
300
|
+
#!/bin/bash
|
301
|
+
set -e
|
302
|
+
|
303
|
+
# Agent script for #{role} agent
|
304
|
+
echo "Starting #{role} agent in #{working_dir}"
|
305
|
+
|
306
|
+
# Change to working directory
|
307
|
+
cd "#{working_dir}"
|
308
|
+
|
309
|
+
# Create a unique temporary prompt file using PID and timestamp
|
310
|
+
TIMESTAMP=$(date +%s)
|
311
|
+
PROMPT_FILE="/tmp/claude_prompt_#{role}_$${TIMESTAMP}_$$.md"
|
312
|
+
|
313
|
+
# Ensure unique filename by adding counter if needed
|
314
|
+
COUNTER=0
|
315
|
+
while [[ -e "$PROMPT_FILE" ]]; do
|
316
|
+
COUNTER=$((COUNTER + 1))
|
317
|
+
PROMPT_FILE="/tmp/claude_prompt_#{role}_$${TIMESTAMP}_$$_${COUNTER}.md"
|
318
|
+
done
|
319
|
+
|
320
|
+
cat > "$PROMPT_FILE" << 'EOF'
|
321
|
+
#{prompt}
|
322
|
+
EOF
|
323
|
+
|
324
|
+
# Run Claude with the prompt
|
325
|
+
echo "Executing Claude for #{role} agent..."
|
326
|
+
claude --print < "$PROMPT_FILE"
|
327
|
+
|
328
|
+
# Cleanup
|
329
|
+
rm -f "$PROMPT_FILE"
|
330
|
+
|
331
|
+
echo "#{role} agent completed successfully"
|
332
|
+
SCRIPT
|
333
|
+
|
334
|
+
script_file.write(script_content)
|
335
|
+
script_file.flush
|
336
|
+
script_file.close
|
337
|
+
|
338
|
+
# Make the script executable
|
339
|
+
File.chmod(0755, script_file.path)
|
340
|
+
|
341
|
+
script_file.path
|
342
|
+
rescue StandardError => e
|
343
|
+
Logger.error("Failed to create agent script: #{e.message}")
|
344
|
+
script_file.close if script_file && !script_file.closed?
|
345
|
+
raise e
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
def spawn_simulated_process(role, worktree_path)
|
350
|
+
# Fallback simulation when Claude CLI is not available
|
351
|
+
Logger.warn("Using simulation mode for #{role} agent")
|
352
|
+
|
353
|
+
# Create a simple background process that simulates agent work
|
354
|
+
pid = Process.spawn(
|
355
|
+
'/bin/bash', '-c',
|
356
|
+
"sleep 30 && echo 'Simulated #{role} agent completed' > /dev/null",
|
357
|
+
chdir: worktree_path || Dir.pwd
|
358
|
+
)
|
359
|
+
|
360
|
+
Process.detach(pid)
|
361
|
+
Logger.info("Spawned simulated agent: #{role} (PID: #{pid})")
|
362
|
+
pid
|
363
|
+
end
|
364
|
+
|
365
|
+
def build_agent_environment(role, worktree_path)
|
366
|
+
env = ENV.to_h
|
367
|
+
env['ENHANCE_SWARM_ROLE'] = role
|
368
|
+
env['ENHANCE_SWARM_WORKTREE'] = worktree_path if worktree_path
|
369
|
+
env['ENHANCE_SWARM_SESSION'] = @session_manager.read_session&.dig(:session_id)
|
370
|
+
env
|
371
|
+
end
|
372
|
+
|
373
|
+
def build_agent_prompt(task, role, worktree_path)
|
374
|
+
# Sanitize inputs
|
375
|
+
safe_task = sanitize_task_description(task)
|
376
|
+
safe_role = sanitize_role(role)
|
377
|
+
safe_test_command = sanitize_command(@config.test_command)
|
378
|
+
|
379
|
+
timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
|
380
|
+
task_words = safe_task.split.first(5).join(' ')
|
381
|
+
working_dir = worktree_path || Dir.pwd
|
382
|
+
|
383
|
+
base_prompt = <<~PROMPT
|
384
|
+
AUTONOMOUS EXECUTION REQUIRED - #{safe_role.upcase} SPECIALIST
|
385
|
+
|
386
|
+
TASK: #{safe_task}
|
387
|
+
|
388
|
+
WORKING DIRECTORY: #{working_dir}
|
389
|
+
|
390
|
+
CRITICAL INSTRUCTIONS:
|
391
|
+
1. You have FULL PERMISSION to read, write, edit files and run commands
|
392
|
+
2. Work in the directory: #{working_dir}
|
393
|
+
3. DO NOT wait for any permissions - proceed immediately
|
394
|
+
4. Complete the task fully and thoroughly
|
395
|
+
5. Test your implementation using: #{safe_test_command}
|
396
|
+
6. When complete:
|
397
|
+
- Run: git add -A
|
398
|
+
- Run: git commit -m '#{safe_role}: #{task_words}...'
|
399
|
+
- Create completion marker: echo "completed" > .enhance_swarm/completed/#{safe_role}_completed.txt
|
400
|
+
7. Document what was implemented in your final message
|
401
|
+
|
402
|
+
PROJECT CONTEXT:
|
403
|
+
- Technology stack: #{Array(@config.technology_stack).join(', ')}
|
404
|
+
- Test command: #{safe_test_command}
|
405
|
+
- Project type: #{@config.project_name}
|
406
|
+
|
407
|
+
Remember: You are autonomous. Make all decisions needed to complete this task successfully.
|
408
|
+
PROMPT
|
409
|
+
|
410
|
+
# Add role-specific instructions
|
411
|
+
case safe_role
|
412
|
+
when 'ux'
|
413
|
+
base_prompt += "\n\nFOCUS: UI/UX design, templates, user experience, styling, and accessibility."
|
414
|
+
when 'backend'
|
415
|
+
base_prompt += "\n\nFOCUS: Models, services, APIs, business logic, database operations, and security."
|
416
|
+
when 'frontend'
|
417
|
+
base_prompt += "\n\nFOCUS: Controllers, views, JavaScript, forms, user interactions, and integration."
|
418
|
+
when 'qa'
|
419
|
+
base_prompt += "\n\nFOCUS: Comprehensive testing, edge cases, quality assurance, and validation."
|
420
|
+
end
|
421
|
+
|
422
|
+
base_prompt
|
423
|
+
end
|
424
|
+
|
425
|
+
def cleanup_failed_spawn(pid, worktree_path)
|
426
|
+
# Clean up process if it was started
|
427
|
+
if pid
|
428
|
+
begin
|
429
|
+
Process.kill('KILL', pid.to_i)
|
430
|
+
rescue StandardError
|
431
|
+
# Process may not exist, ignore
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
# Clean up worktree if it was created
|
436
|
+
if worktree_path && Dir.exist?(worktree_path)
|
437
|
+
begin
|
438
|
+
CommandExecutor.execute('git', 'worktree', 'remove', '--force', worktree_path)
|
439
|
+
rescue StandardError => e
|
440
|
+
Logger.warn("Failed to cleanup worktree #{worktree_path}: #{e.message}")
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
def sanitize_task_description(task)
|
446
|
+
# Remove potentially dangerous characters while preserving readability
|
447
|
+
task.to_s.gsub(/[`$\\;|&><]/, '').strip
|
448
|
+
end
|
449
|
+
|
450
|
+
def sanitize_role(role)
|
451
|
+
# Only allow known safe roles
|
452
|
+
allowed_roles = %w[ux backend frontend qa general]
|
453
|
+
role = role.to_s.downcase.strip
|
454
|
+
allowed_roles.include?(role) ? role : 'general'
|
455
|
+
end
|
456
|
+
|
457
|
+
def sanitize_command(command)
|
458
|
+
# Basic command sanitization - remove shell metacharacters
|
459
|
+
command.to_s.gsub(/[;&|`$\\]/, '').strip
|
460
|
+
end
|
461
|
+
end
|
462
|
+
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require_relative 'command_executor'
|
5
|
+
require_relative 'logger'
|
6
|
+
|
7
|
+
module EnhanceSwarm
|
8
|
+
class CleanupManager
|
9
|
+
CLEANUP_TIMEOUT = 30 # seconds
|
10
|
+
|
11
|
+
def self.cleanup_failed_operation(operation_id, details = {})
|
12
|
+
Logger.info("Starting cleanup for failed operation: #{operation_id}")
|
13
|
+
|
14
|
+
cleanup_tasks = []
|
15
|
+
|
16
|
+
# Cleanup git worktrees
|
17
|
+
if details[:worktree_path]
|
18
|
+
cleanup_tasks << -> { cleanup_worktree(details[:worktree_path]) }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Cleanup git branches
|
22
|
+
if details[:branch_name]
|
23
|
+
cleanup_tasks << -> { cleanup_branch(details[:branch_name]) }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Cleanup temporary files
|
27
|
+
if details[:temp_files]
|
28
|
+
cleanup_tasks << -> { cleanup_temp_files(details[:temp_files]) }
|
29
|
+
end
|
30
|
+
|
31
|
+
# Kill hanging processes
|
32
|
+
if details[:process_pid]
|
33
|
+
cleanup_tasks << -> { cleanup_process(details[:process_pid]) }
|
34
|
+
end
|
35
|
+
|
36
|
+
# Execute cleanup tasks with timeout protection
|
37
|
+
cleanup_results = execute_cleanup_tasks(cleanup_tasks)
|
38
|
+
|
39
|
+
Logger.log_operation("cleanup_#{operation_id}", 'completed', cleanup_results)
|
40
|
+
cleanup_results
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.cleanup_all_swarm_resources
|
44
|
+
Logger.info("Starting comprehensive swarm resource cleanup")
|
45
|
+
|
46
|
+
results = {
|
47
|
+
worktrees: cleanup_swarm_worktrees,
|
48
|
+
branches: cleanup_swarm_branches,
|
49
|
+
processes: cleanup_swarm_processes,
|
50
|
+
temp_files: cleanup_swarm_temp_files
|
51
|
+
}
|
52
|
+
|
53
|
+
Logger.log_operation('cleanup_all', 'completed', results)
|
54
|
+
results
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def self.execute_cleanup_tasks(tasks)
|
60
|
+
results = []
|
61
|
+
|
62
|
+
tasks.each_with_index do |task, index|
|
63
|
+
begin
|
64
|
+
Timeout.timeout(CLEANUP_TIMEOUT) do
|
65
|
+
result = task.call
|
66
|
+
results << { task: index, status: 'success', result: result }
|
67
|
+
end
|
68
|
+
rescue Timeout::Error
|
69
|
+
Logger.warn("Cleanup task #{index} timed out after #{CLEANUP_TIMEOUT}s")
|
70
|
+
results << { task: index, status: 'timeout', error: 'Cleanup timed out' }
|
71
|
+
rescue StandardError => e
|
72
|
+
Logger.error("Cleanup task #{index} failed: #{e.message}")
|
73
|
+
results << { task: index, status: 'failed', error: e.message }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
results
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.cleanup_worktree(worktree_path)
|
81
|
+
return { status: 'skipped', reason: 'Path not provided' } unless worktree_path
|
82
|
+
|
83
|
+
begin
|
84
|
+
if Dir.exist?(worktree_path)
|
85
|
+
# Force remove worktree
|
86
|
+
CommandExecutor.execute('git', 'worktree', 'remove', '--force', worktree_path)
|
87
|
+
Logger.info("Removed worktree: #{worktree_path}")
|
88
|
+
{ status: 'success', path: worktree_path }
|
89
|
+
else
|
90
|
+
{ status: 'not_found', path: worktree_path }
|
91
|
+
end
|
92
|
+
rescue CommandExecutor::CommandError => e
|
93
|
+
Logger.error("Failed to cleanup worktree #{worktree_path}: #{e.message}")
|
94
|
+
# Try manual cleanup if git command fails
|
95
|
+
FileUtils.rm_rf(worktree_path) if Dir.exist?(worktree_path)
|
96
|
+
{ status: 'manual_cleanup', path: worktree_path, error: e.message }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.cleanup_branch(branch_name)
|
101
|
+
return { status: 'skipped', reason: 'Branch not provided' } unless branch_name
|
102
|
+
|
103
|
+
begin
|
104
|
+
# Check if branch exists
|
105
|
+
CommandExecutor.execute('git', 'rev-parse', '--verify', branch_name)
|
106
|
+
|
107
|
+
# Force delete branch
|
108
|
+
CommandExecutor.execute('git', 'branch', '-D', branch_name)
|
109
|
+
Logger.info("Deleted branch: #{branch_name}")
|
110
|
+
{ status: 'success', branch: branch_name }
|
111
|
+
rescue CommandExecutor::CommandError => e
|
112
|
+
if e.message.include?('does not exist')
|
113
|
+
{ status: 'not_found', branch: branch_name }
|
114
|
+
else
|
115
|
+
Logger.error("Failed to cleanup branch #{branch_name}: #{e.message}")
|
116
|
+
{ status: 'failed', branch: branch_name, error: e.message }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.cleanup_temp_files(file_patterns)
|
122
|
+
return { status: 'skipped', reason: 'No patterns provided' } unless file_patterns
|
123
|
+
|
124
|
+
cleaned_files = []
|
125
|
+
file_patterns.each do |pattern|
|
126
|
+
Dir.glob(pattern).each do |file|
|
127
|
+
begin
|
128
|
+
FileUtils.rm_f(file)
|
129
|
+
cleaned_files << file
|
130
|
+
rescue StandardError => e
|
131
|
+
Logger.error("Failed to remove temp file #{file}: #{e.message}")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
{ status: 'success', files_removed: cleaned_files.size, files: cleaned_files }
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.cleanup_process(pid)
|
140
|
+
return { status: 'skipped', reason: 'No PID provided' } unless pid
|
141
|
+
|
142
|
+
begin
|
143
|
+
# Check if process exists
|
144
|
+
Process.kill(0, pid.to_i)
|
145
|
+
|
146
|
+
# Try graceful termination first
|
147
|
+
Process.kill('TERM', pid.to_i)
|
148
|
+
sleep(2)
|
149
|
+
|
150
|
+
# Force kill if still running
|
151
|
+
begin
|
152
|
+
Process.kill(0, pid.to_i)
|
153
|
+
Process.kill('KILL', pid.to_i)
|
154
|
+
Logger.info("Force killed process: #{pid}")
|
155
|
+
{ status: 'force_killed', pid: pid }
|
156
|
+
rescue Errno::ESRCH
|
157
|
+
Logger.info("Process terminated gracefully: #{pid}")
|
158
|
+
{ status: 'terminated', pid: pid }
|
159
|
+
end
|
160
|
+
rescue Errno::ESRCH
|
161
|
+
{ status: 'not_found', pid: pid }
|
162
|
+
rescue Errno::EPERM
|
163
|
+
Logger.error("Permission denied killing process: #{pid}")
|
164
|
+
{ status: 'permission_denied', pid: pid }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.cleanup_swarm_worktrees
|
169
|
+
begin
|
170
|
+
output = CommandExecutor.execute('git', 'worktree', 'list', '--porcelain')
|
171
|
+
worktrees = parse_worktree_list(output)
|
172
|
+
|
173
|
+
swarm_worktrees = worktrees.select { |wt| wt[:branch]&.start_with?('swarm/') }
|
174
|
+
|
175
|
+
results = swarm_worktrees.map do |worktree|
|
176
|
+
cleanup_worktree(worktree[:path])
|
177
|
+
end
|
178
|
+
|
179
|
+
{ count: results.size, results: results }
|
180
|
+
rescue CommandExecutor::CommandError => e
|
181
|
+
Logger.error("Failed to list worktrees for cleanup: #{e.message}")
|
182
|
+
{ count: 0, error: e.message }
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def self.cleanup_swarm_branches
|
187
|
+
begin
|
188
|
+
output = CommandExecutor.execute('git', 'branch', '-a')
|
189
|
+
branches = output.lines.map(&:strip).reject(&:empty?)
|
190
|
+
|
191
|
+
swarm_branches = branches.select { |b| b.include?('swarm/') }
|
192
|
+
.map { |b| b.gsub(/^\*?\s*/, '').gsub(/^remotes\/origin\//, '') }
|
193
|
+
.uniq
|
194
|
+
|
195
|
+
results = swarm_branches.map do |branch|
|
196
|
+
cleanup_branch(branch)
|
197
|
+
end
|
198
|
+
|
199
|
+
{ count: results.size, results: results }
|
200
|
+
rescue CommandExecutor::CommandError => e
|
201
|
+
Logger.error("Failed to list branches for cleanup: #{e.message}")
|
202
|
+
{ count: 0, error: e.message }
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def self.cleanup_swarm_processes
|
207
|
+
# This is OS-specific and should be implemented based on requirements
|
208
|
+
# For now, just return a placeholder
|
209
|
+
{ count: 0, message: 'Process cleanup not implemented for this platform' }
|
210
|
+
end
|
211
|
+
|
212
|
+
def self.cleanup_swarm_temp_files
|
213
|
+
patterns = [
|
214
|
+
'/tmp/enhance_swarm_*',
|
215
|
+
'*.enhance_swarm.tmp',
|
216
|
+
'.enhance_swarm.lock'
|
217
|
+
]
|
218
|
+
|
219
|
+
cleanup_temp_files(patterns)
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.parse_worktree_list(output)
|
223
|
+
worktrees = []
|
224
|
+
current_worktree = {}
|
225
|
+
|
226
|
+
output.lines.each do |line|
|
227
|
+
line = line.strip
|
228
|
+
next if line.empty?
|
229
|
+
|
230
|
+
case line
|
231
|
+
when /^worktree (.+)$/
|
232
|
+
worktrees << current_worktree unless current_worktree.empty?
|
233
|
+
current_worktree = { path: $1 }
|
234
|
+
when /^branch (.+)$/
|
235
|
+
current_worktree[:branch] = $1.gsub(/^refs\/heads\//, '')
|
236
|
+
when /^HEAD (.+)$/
|
237
|
+
current_worktree[:head] = $1
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
worktrees << current_worktree unless current_worktree.empty?
|
242
|
+
worktrees
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|