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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.enhance_swarm/agent_scripts/frontend_agent.md +39 -0
  3. data/.enhance_swarm/user_patterns.json +37 -0
  4. data/CHANGELOG.md +184 -0
  5. data/LICENSE +21 -0
  6. data/PRODUCTION_TEST_LOG.md +502 -0
  7. data/README.md +905 -0
  8. data/Rakefile +28 -0
  9. data/USAGE_EXAMPLES.md +477 -0
  10. data/examples/enhance_workflow.md +346 -0
  11. data/examples/rails_project.md +253 -0
  12. data/exe/enhance-swarm +30 -0
  13. data/lib/enhance_swarm/additional_commands.rb +299 -0
  14. data/lib/enhance_swarm/agent_communicator.rb +460 -0
  15. data/lib/enhance_swarm/agent_reviewer.rb +283 -0
  16. data/lib/enhance_swarm/agent_spawner.rb +462 -0
  17. data/lib/enhance_swarm/cleanup_manager.rb +245 -0
  18. data/lib/enhance_swarm/cli.rb +1592 -0
  19. data/lib/enhance_swarm/command_executor.rb +78 -0
  20. data/lib/enhance_swarm/configuration.rb +324 -0
  21. data/lib/enhance_swarm/control_agent.rb +307 -0
  22. data/lib/enhance_swarm/dependency_validator.rb +195 -0
  23. data/lib/enhance_swarm/error_recovery.rb +785 -0
  24. data/lib/enhance_swarm/generator.rb +194 -0
  25. data/lib/enhance_swarm/interrupt_handler.rb +512 -0
  26. data/lib/enhance_swarm/logger.rb +106 -0
  27. data/lib/enhance_swarm/mcp_integration.rb +85 -0
  28. data/lib/enhance_swarm/monitor.rb +28 -0
  29. data/lib/enhance_swarm/notification_manager.rb +444 -0
  30. data/lib/enhance_swarm/orchestrator.rb +313 -0
  31. data/lib/enhance_swarm/output_streamer.rb +281 -0
  32. data/lib/enhance_swarm/process_monitor.rb +266 -0
  33. data/lib/enhance_swarm/progress_tracker.rb +215 -0
  34. data/lib/enhance_swarm/project_analyzer.rb +612 -0
  35. data/lib/enhance_swarm/resource_manager.rb +177 -0
  36. data/lib/enhance_swarm/retry_handler.rb +40 -0
  37. data/lib/enhance_swarm/session_manager.rb +247 -0
  38. data/lib/enhance_swarm/signal_handler.rb +95 -0
  39. data/lib/enhance_swarm/smart_defaults.rb +708 -0
  40. data/lib/enhance_swarm/task_integration.rb +150 -0
  41. data/lib/enhance_swarm/task_manager.rb +174 -0
  42. data/lib/enhance_swarm/version.rb +5 -0
  43. data/lib/enhance_swarm/visual_dashboard.rb +555 -0
  44. data/lib/enhance_swarm/web_ui.rb +211 -0
  45. data/lib/enhance_swarm.rb +69 -0
  46. data/setup.sh +86 -0
  47. data/sig/enhance_swarm.rbs +4 -0
  48. data/templates/claude/CLAUDE.md +160 -0
  49. data/templates/claude/MCP.md +117 -0
  50. data/templates/claude/PERSONAS.md +114 -0
  51. data/templates/claude/RULES.md +221 -0
  52. data/test_builtin_functionality.rb +121 -0
  53. data/test_core_components.rb +156 -0
  54. data/test_real_claude_integration.rb +285 -0
  55. data/test_security.rb +150 -0
  56. data/test_smart_defaults.rb +155 -0
  57. data/test_task_integration.rb +173 -0
  58. data/test_web_ui.rb +245 -0
  59. data/web/assets/css/main.css +645 -0
  60. data/web/assets/js/kanban.js +499 -0
  61. data/web/assets/js/main.js +525 -0
  62. data/web/templates/dashboard.html.erb +226 -0
  63. data/web/templates/kanban.html.erb +193 -0
  64. metadata +293 -0
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'logger'
4
+ require_relative 'configuration'
5
+
6
+ module EnhanceSwarm
7
+ # Manages system resources and enforces limits for agent spawning
8
+ class ResourceManager
9
+ MAX_CONCURRENT_AGENTS = 10
10
+ MAX_MEMORY_MB = 2048
11
+ MAX_DISK_MB = 1024
12
+
13
+ def initialize
14
+ @config = EnhanceSwarm.configuration
15
+ end
16
+
17
+ def can_spawn_agent?
18
+ result = {
19
+ allowed: true,
20
+ reasons: []
21
+ }
22
+
23
+ # Check concurrent agent limit
24
+ current_agents = count_active_agents
25
+ max_agents = @config.max_concurrent_agents || MAX_CONCURRENT_AGENTS
26
+
27
+ if current_agents >= max_agents
28
+ result[:allowed] = false
29
+ result[:reasons] << "Maximum concurrent agents reached (#{current_agents}/#{max_agents})"
30
+ end
31
+
32
+ # Check system memory
33
+ if memory_usage_too_high?
34
+ result[:allowed] = false
35
+ result[:reasons] << "System memory usage too high"
36
+ end
37
+
38
+ # Check disk space
39
+ if disk_usage_too_high?
40
+ result[:allowed] = false
41
+ result[:reasons] << "Insufficient disk space"
42
+ end
43
+
44
+ # Check system load
45
+ if system_load_too_high?
46
+ result[:allowed] = false
47
+ result[:reasons] << "System load too high"
48
+ end
49
+
50
+ result
51
+ end
52
+
53
+ def get_resource_stats
54
+ {
55
+ active_agents: count_active_agents,
56
+ max_agents: @config.max_concurrent_agents || MAX_CONCURRENT_AGENTS,
57
+ memory_usage_mb: get_memory_usage_mb,
58
+ disk_usage_mb: get_disk_usage_mb,
59
+ system_load: get_system_load
60
+ }
61
+ end
62
+
63
+ def enforce_limits!
64
+ stats = get_resource_stats
65
+
66
+ if stats[:active_agents] > stats[:max_agents]
67
+ Logger.warn("Agent limit exceeded: #{stats[:active_agents]}/#{stats[:max_agents]}")
68
+ cleanup_oldest_agents(stats[:active_agents] - stats[:max_agents])
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def count_active_agents
75
+ # Count running enhance-swarm processes
76
+ begin
77
+ ps_output = `ps aux | grep -i enhance-swarm | grep -v grep | wc -l`.strip.to_i
78
+ # Subtract 1 for the current process
79
+ [ps_output - 1, 0].max
80
+ rescue StandardError
81
+ 0
82
+ end
83
+ end
84
+
85
+ def memory_usage_too_high?
86
+ current_usage = get_memory_usage_mb
87
+ max_usage = @config.max_memory_mb || MAX_MEMORY_MB
88
+ current_usage > max_usage
89
+ end
90
+
91
+ def disk_usage_too_high?
92
+ current_usage = get_disk_usage_mb
93
+ max_usage = @config.max_disk_mb || MAX_DISK_MB
94
+ current_usage > max_usage
95
+ end
96
+
97
+ def system_load_too_high?
98
+ load_avg = get_system_load
99
+ # Consider load too high if 1-minute average > number of CPU cores
100
+ cpu_count = get_cpu_count
101
+ load_avg > cpu_count * 1.5
102
+ end
103
+
104
+ def get_memory_usage_mb
105
+ begin
106
+ # Get RSS memory usage of all enhance-swarm processes
107
+ ps_output = `ps aux | grep -i enhance-swarm | grep -v grep | awk '{sum += $6} END {print sum}'`.strip.to_i
108
+ # Convert from KB to MB
109
+ ps_output / 1024
110
+ rescue StandardError
111
+ 0
112
+ end
113
+ end
114
+
115
+ def get_disk_usage_mb
116
+ begin
117
+ # Check disk usage of .enhance_swarm directory
118
+ if Dir.exist?('.enhance_swarm')
119
+ du_output = `du -sm .enhance_swarm 2>/dev/null | awk '{print $1}'`.strip.to_i
120
+ return du_output
121
+ end
122
+ 0
123
+ rescue StandardError
124
+ 0
125
+ end
126
+ end
127
+
128
+ def get_system_load
129
+ begin
130
+ # Get 1-minute load average
131
+ uptime_output = `uptime`.strip
132
+ load_match = uptime_output.match(/load averages?: ([\d.]+)/)
133
+ load_match ? load_match[1].to_f : 0.0
134
+ rescue StandardError
135
+ 0.0
136
+ end
137
+ end
138
+
139
+ def get_cpu_count
140
+ begin
141
+ # Get number of CPU cores
142
+ if RUBY_PLATFORM.include?('darwin') # macOS
143
+ `sysctl -n hw.ncpu`.strip.to_i
144
+ else # Linux
145
+ `nproc`.strip.to_i
146
+ end
147
+ rescue StandardError
148
+ 4 # Default fallback
149
+ end
150
+ end
151
+
152
+ def cleanup_oldest_agents(count)
153
+ Logger.info("Cleaning up #{count} oldest agents to enforce limits")
154
+
155
+ begin
156
+ # Get list of enhance-swarm processes sorted by start time
157
+ ps_output = `ps aux | grep -i enhance-swarm | grep -v grep | sort -k9`
158
+ pids_to_kill = ps_output.lines.first(count).map do |line|
159
+ line.split[1].to_i # Get PID
160
+ end
161
+
162
+ pids_to_kill.each do |pid|
163
+ begin
164
+ Process.kill('TERM', pid)
165
+ Logger.info("Terminated agent process: #{pid}")
166
+ rescue Errno::ESRCH
167
+ # Process already terminated
168
+ rescue StandardError => e
169
+ Logger.error("Failed to terminate process #{pid}: #{e.message}")
170
+ end
171
+ end
172
+ rescue StandardError => e
173
+ Logger.error("Failed to cleanup agents: #{e.message}")
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnhanceSwarm
4
+ class RetryHandler
5
+ class RetryError < StandardError; end
6
+
7
+ def self.with_retry(max_retries: 3, base_delay: 1, max_delay: 30, &block)
8
+ retries = 0
9
+
10
+ begin
11
+ yield
12
+ rescue StandardError => e
13
+ retries += 1
14
+
15
+ if retries <= max_retries && retryable_error?(e)
16
+ delay = [base_delay * (2 ** (retries - 1)), max_delay].min
17
+ puts "Attempt #{retries} failed: #{e.message}. Retrying in #{delay}s...".colorize(:yellow)
18
+ sleep(delay)
19
+ retry
20
+ end
21
+
22
+ raise RetryError.new("Operation failed after #{max_retries} retries: #{e.message}")
23
+ end
24
+ end
25
+
26
+ def self.retryable_error?(error)
27
+ case error
28
+ when CommandExecutor::CommandError
29
+ # Retry on timeout or command not found, but not on validation errors
30
+ error.message.include?('timed out') || error.message.include?('not found')
31
+ when Errno::ENOENT, Errno::ECONNREFUSED, Errno::ETIMEDOUT
32
+ true
33
+ when IOError, SystemCallError
34
+ true
35
+ else
36
+ false
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'securerandom'
6
+ require_relative 'logger'
7
+
8
+ module EnhanceSwarm
9
+ class SessionManager
10
+ SESSION_DIR = '.enhance_swarm'
11
+ SESSION_FILE = 'session.json'
12
+
13
+ def initialize
14
+ @session_path = File.join(Dir.pwd, SESSION_DIR, SESSION_FILE)
15
+ ensure_session_directory
16
+ end
17
+
18
+ def create_session(task_description = nil)
19
+ session_data = {
20
+ session_id: generate_session_id,
21
+ start_time: Time.now.iso8601,
22
+ task_description: task_description,
23
+ agents: [],
24
+ status: 'active'
25
+ }
26
+
27
+ write_session(session_data)
28
+ Logger.info("Created new session: #{session_data[:session_id]}")
29
+ session_data
30
+ end
31
+
32
+ def add_agent(role, pid, worktree_path, task = nil)
33
+ session = read_session
34
+ return false unless session
35
+
36
+ agent_data = {
37
+ role: role,
38
+ pid: pid,
39
+ worktree_path: worktree_path,
40
+ task: task,
41
+ start_time: Time.now.iso8601,
42
+ status: 'running'
43
+ }
44
+
45
+ session[:agents] << agent_data
46
+ write_session(session)
47
+ Logger.info("Added agent to session: #{role} (PID: #{pid})")
48
+ true
49
+ end
50
+
51
+ def update_agent_status(pid, status, completion_time = nil)
52
+ session = read_session
53
+ return false unless session
54
+
55
+ agent = session[:agents].find { |a| a[:pid] == pid }
56
+ return false unless agent
57
+
58
+ agent[:status] = status
59
+ agent[:completion_time] = completion_time if completion_time
60
+
61
+ write_session(session)
62
+ Logger.info("Updated agent status: PID #{pid} -> #{status}")
63
+ true
64
+ end
65
+
66
+ def remove_agent(pid)
67
+ session = read_session
68
+ return false unless session
69
+
70
+ initial_count = session[:agents].length
71
+ session[:agents].reject! { |a| a[:pid] == pid }
72
+
73
+ if session[:agents].length < initial_count
74
+ write_session(session)
75
+ Logger.info("Removed agent from session: PID #{pid}")
76
+ true
77
+ else
78
+ false
79
+ end
80
+ end
81
+
82
+ def get_active_agents
83
+ session = read_session
84
+ return [] unless session
85
+
86
+ session[:agents].select { |a| a[:status] == 'running' }
87
+ end
88
+
89
+ def get_all_agents
90
+ session = read_session
91
+ return [] unless session
92
+
93
+ session[:agents] || []
94
+ end
95
+
96
+ def session_exists?
97
+ File.exist?(@session_path)
98
+ end
99
+
100
+ def read_session
101
+ return nil unless session_exists?
102
+
103
+ begin
104
+ content = File.read(@session_path)
105
+ JSON.parse(content, symbolize_names: true)
106
+ rescue JSON::ParserError, StandardError => e
107
+ Logger.error("Failed to read session file: #{e.message}")
108
+ nil
109
+ end
110
+ end
111
+
112
+ def session_status
113
+ session = read_session
114
+ return { exists: false } unless session
115
+
116
+ active_agents = get_active_agents
117
+ all_agents = get_all_agents
118
+
119
+ {
120
+ exists: true,
121
+ session_id: session[:session_id],
122
+ start_time: session[:start_time],
123
+ task_description: session[:task_description],
124
+ status: session[:status],
125
+ total_agents: all_agents.length,
126
+ active_agents: active_agents.length,
127
+ completed_agents: all_agents.count { |a| a[:status] == 'completed' },
128
+ failed_agents: all_agents.count { |a| a[:status] == 'failed' },
129
+ agents: all_agents
130
+ }
131
+ end
132
+
133
+ def close_session
134
+ session = read_session
135
+ return false unless session
136
+
137
+ session[:status] = 'completed'
138
+ session[:end_time] = Time.now.iso8601
139
+ write_session(session)
140
+ Logger.info("Closed session: #{session[:session_id]}")
141
+ true
142
+ end
143
+
144
+ def cleanup_session
145
+ return false unless session_exists?
146
+
147
+ # Archive the session before removing
148
+ if archive_session
149
+ File.delete(@session_path)
150
+ Logger.info("Cleaned up session file")
151
+ true
152
+ else
153
+ false
154
+ end
155
+ end
156
+
157
+ def check_agent_processes
158
+ session = read_session
159
+ return [] unless session
160
+
161
+ updated_agents = []
162
+ session_changed = false
163
+
164
+ session[:agents].each do |agent|
165
+ next unless agent[:status] == 'running'
166
+
167
+ # Check if process is still running
168
+ if process_running?(agent[:pid])
169
+ updated_agents << agent
170
+ else
171
+ # Process is no longer running, update status
172
+ agent[:status] = 'stopped'
173
+ agent[:completion_time] = Time.now.iso8601
174
+ session_changed = true
175
+ Logger.info("Agent process stopped: #{agent[:role]} (PID: #{agent[:pid]})")
176
+ end
177
+ end
178
+
179
+ write_session(session) if session_changed
180
+ updated_agents
181
+ end
182
+
183
+ private
184
+
185
+ def ensure_session_directory
186
+ session_dir = File.dirname(@session_path)
187
+ FileUtils.mkdir_p(session_dir) unless Dir.exist?(session_dir)
188
+ end
189
+
190
+ def write_session(session_data)
191
+ begin
192
+ File.write(@session_path, JSON.pretty_generate(session_data))
193
+ true
194
+ rescue StandardError => e
195
+ Logger.error("Failed to write session file: #{e.message}")
196
+ false
197
+ end
198
+ end
199
+
200
+ def generate_session_id
201
+ Time.now.to_i.to_s + '_' + SecureRandom.hex(4)
202
+ end
203
+
204
+ def archive_session
205
+ return true unless session_exists?
206
+
207
+ begin
208
+ session = read_session
209
+ return false unless session
210
+
211
+ # Create archives directory
212
+ archive_dir = File.join(Dir.pwd, SESSION_DIR, 'archives')
213
+ FileUtils.mkdir_p(archive_dir)
214
+
215
+ # Create archive filename with session ID and timestamp
216
+ archive_name = "session_#{session[:session_id]}_#{Time.now.strftime('%Y%m%d_%H%M%S')}.json"
217
+ archive_path = File.join(archive_dir, archive_name)
218
+
219
+ FileUtils.cp(@session_path, archive_path)
220
+ Logger.info("Archived session to: #{archive_path}")
221
+ true
222
+ rescue StandardError => e
223
+ Logger.error("Failed to archive session: #{e.message}")
224
+ false
225
+ end
226
+ end
227
+
228
+ def process_running?(pid)
229
+ begin
230
+ # Use Process.kill(0, pid) to check if process exists
231
+ # This doesn't actually kill the process, just checks if it's running
232
+ Process.kill(0, pid.to_i)
233
+ true
234
+ rescue Errno::ESRCH
235
+ # Process doesn't exist
236
+ false
237
+ rescue Errno::EPERM
238
+ # Process exists but we don't have permission to signal it
239
+ # This means it's running but owned by another user
240
+ true
241
+ rescue StandardError
242
+ # Any other error, assume process is not running
243
+ false
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'logger'
4
+ require_relative 'cleanup_manager'
5
+
6
+ module EnhanceSwarm
7
+ class SignalHandler
8
+ def self.setup
9
+ @shutdown_requested = false
10
+ @active_operations = {}
11
+
12
+ # Handle graceful shutdown signals
13
+ Signal.trap('INT') { handle_shutdown('SIGINT') }
14
+ Signal.trap('TERM') { handle_shutdown('SIGTERM') }
15
+
16
+ # Handle info signal for status
17
+ if Signal.list.key?('USR1')
18
+ Signal.trap('USR1') { handle_status_request }
19
+ end
20
+ end
21
+
22
+ def self.handle_shutdown(signal)
23
+ return if @shutdown_requested
24
+
25
+ @shutdown_requested = true
26
+ Logger.info("Received #{signal}, initiating graceful shutdown...")
27
+
28
+ begin
29
+ # Stop accepting new operations
30
+ puts "\nšŸ›‘ Graceful shutdown initiated...".colorize(:yellow)
31
+
32
+ # Clean up any active operations
33
+ cleanup_active_operations
34
+
35
+ # Perform final cleanup
36
+ CleanupManager.cleanup_all_swarm_resources
37
+
38
+ Logger.info("Graceful shutdown completed")
39
+ puts "āœ… Shutdown complete".colorize(:green)
40
+
41
+ exit(0)
42
+ rescue StandardError => e
43
+ Logger.error("Error during shutdown: #{e.message}")
44
+ puts "āŒ Error during shutdown: #{e.message}".colorize(:red)
45
+ exit(1)
46
+ end
47
+ end
48
+
49
+ def self.handle_status_request
50
+ Logger.info("Status request received via USR1")
51
+
52
+ status = {
53
+ active_operations: @active_operations.size,
54
+ shutdown_requested: @shutdown_requested,
55
+ timestamp: Time.now.iso8601
56
+ }
57
+
58
+ puts JSON.pretty_generate(status)
59
+ end
60
+
61
+ def self.register_operation(operation_id, details = {})
62
+ @active_operations ||= {}
63
+ @active_operations[operation_id] = {
64
+ started_at: Time.now,
65
+ details: details
66
+ }
67
+ end
68
+
69
+ def self.unregister_operation(operation_id)
70
+ @active_operations&.delete(operation_id)
71
+ end
72
+
73
+ def self.shutdown_requested?
74
+ @shutdown_requested
75
+ end
76
+
77
+ private
78
+
79
+ def self.cleanup_active_operations
80
+ return unless @active_operations&.any?
81
+
82
+ Logger.info("Cleaning up #{@active_operations.size} active operations")
83
+
84
+ @active_operations.each do |operation_id, details|
85
+ begin
86
+ CleanupManager.cleanup_failed_operation(operation_id, details[:details])
87
+ rescue StandardError => e
88
+ Logger.error("Failed to cleanup operation #{operation_id}: #{e.message}")
89
+ end
90
+ end
91
+
92
+ @active_operations.clear
93
+ end
94
+ end
95
+ end