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,708 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'yaml'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module EnhanceSwarm
|
8
|
+
class SmartDefaults
|
9
|
+
include Singleton
|
10
|
+
|
11
|
+
DEFAULT_SETTINGS_FILE = '.enhance_swarm/smart_defaults.yml'
|
12
|
+
USER_PATTERNS_FILE = '.enhance_swarm/user_patterns.json'
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@settings = load_settings
|
16
|
+
@user_patterns = load_user_patterns
|
17
|
+
@project_context = analyze_project_context
|
18
|
+
ensure_settings_directory
|
19
|
+
end
|
20
|
+
|
21
|
+
# Detect optimal role for a given task
|
22
|
+
def suggest_role_for_task(task_description)
|
23
|
+
task_lower = task_description.downcase
|
24
|
+
|
25
|
+
# Check for explicit role keywords
|
26
|
+
role_keywords = {
|
27
|
+
'backend' => %w[api server database model migration schema endpoint route controller service auth jwt],
|
28
|
+
'frontend' => %w[ui ux component view page template css html javascript react vue angular design layout],
|
29
|
+
'qa' => %w[test testing spec unit integration e2e selenium cypress jest rspec quality assurance bug],
|
30
|
+
'ux' => %w[design user experience wireframe mockup prototype accessibility usability flow journey]
|
31
|
+
}
|
32
|
+
|
33
|
+
# Calculate keyword matches for each role
|
34
|
+
role_scores = role_keywords.transform_values do |keywords|
|
35
|
+
keywords.count { |keyword| task_lower.include?(keyword) }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Add context from user patterns
|
39
|
+
if @user_patterns['role_preferences']
|
40
|
+
@user_patterns['role_preferences'].each do |role, weight|
|
41
|
+
role_scores[role] = (role_scores[role] || 0) + weight.to_f
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return the role with highest score, or 'general' if tied
|
46
|
+
best_role = role_scores.max_by { |_, score| score }
|
47
|
+
best_role && best_role[1] > 0 ? best_role[0] : 'general'
|
48
|
+
end
|
49
|
+
|
50
|
+
# Suggest optimal configuration based on project type
|
51
|
+
def suggest_configuration
|
52
|
+
config = {}
|
53
|
+
|
54
|
+
# Detect project type and suggest stack
|
55
|
+
project_type = detect_project_type
|
56
|
+
config[:project_type] = project_type
|
57
|
+
config[:technology_stack] = suggest_technology_stack(project_type)
|
58
|
+
|
59
|
+
# Suggest commands based on project files
|
60
|
+
config[:commands] = suggest_commands(project_type)
|
61
|
+
|
62
|
+
# Suggest orchestration settings
|
63
|
+
config[:orchestration] = suggest_orchestration_settings
|
64
|
+
|
65
|
+
# Add MCP tool suggestions
|
66
|
+
config[:mcp_tools] = suggest_mcp_tools(project_type)
|
67
|
+
|
68
|
+
config
|
69
|
+
end
|
70
|
+
|
71
|
+
# Auto-retry with intelligent backoff
|
72
|
+
def auto_retry_with_backoff(operation, max_retries: 3, base_delay: 1)
|
73
|
+
attempt = 0
|
74
|
+
last_error = nil
|
75
|
+
|
76
|
+
loop do
|
77
|
+
attempt += 1
|
78
|
+
|
79
|
+
begin
|
80
|
+
result = yield
|
81
|
+
|
82
|
+
# Record successful pattern
|
83
|
+
record_success_pattern(operation, attempt)
|
84
|
+
return result
|
85
|
+
|
86
|
+
rescue StandardError => e
|
87
|
+
last_error = e
|
88
|
+
|
89
|
+
if attempt >= max_retries
|
90
|
+
record_failure_pattern(operation, attempt, e)
|
91
|
+
raise e
|
92
|
+
end
|
93
|
+
|
94
|
+
# Calculate exponential backoff with jitter
|
95
|
+
delay = base_delay * (2 ** (attempt - 1)) + rand(0.5)
|
96
|
+
delay = [delay, 30].min # Cap at 30 seconds
|
97
|
+
|
98
|
+
Logger.info("Retry #{attempt}/#{max_retries} for #{operation} in #{delay.round(1)}s: #{e.message}")
|
99
|
+
sleep(delay)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Suggest next actions based on current state
|
105
|
+
def suggest_next_actions(current_context = {})
|
106
|
+
suggestions = []
|
107
|
+
|
108
|
+
# Check for common issues and suggest fixes
|
109
|
+
if stale_worktrees_detected?
|
110
|
+
suggestions << {
|
111
|
+
action: 'cleanup',
|
112
|
+
command: 'enhance-swarm cleanup --all',
|
113
|
+
reason: 'Stale worktrees detected',
|
114
|
+
priority: :medium
|
115
|
+
}
|
116
|
+
end
|
117
|
+
|
118
|
+
if pending_agent_messages?
|
119
|
+
suggestions << {
|
120
|
+
action: 'communicate',
|
121
|
+
command: 'enhance-swarm communicate --interactive',
|
122
|
+
reason: 'Pending agent messages need responses',
|
123
|
+
priority: :high
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
# Suggest based on project state
|
128
|
+
if tests_need_running?
|
129
|
+
suggestions << {
|
130
|
+
action: 'test',
|
131
|
+
command: determine_test_command,
|
132
|
+
reason: 'Code changes detected, tests should be run',
|
133
|
+
priority: :medium
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
# Suggest based on user patterns
|
138
|
+
time_based_suggestions.each { |suggestion| suggestions << suggestion }
|
139
|
+
|
140
|
+
suggestions.sort_by { |s| priority_weight(s[:priority]) }.reverse
|
141
|
+
end
|
142
|
+
|
143
|
+
# Auto-cleanup stale resources
|
144
|
+
def auto_cleanup_if_needed
|
145
|
+
cleanup_actions = []
|
146
|
+
|
147
|
+
# Check for stale worktrees (older than 1 day)
|
148
|
+
stale_worktrees = find_stale_worktrees
|
149
|
+
if stale_worktrees.count > 3
|
150
|
+
cleanup_actions << proc do
|
151
|
+
CleanupManager.cleanup_stale_worktrees
|
152
|
+
Logger.info("Auto-cleaned #{stale_worktrees.count} stale worktrees")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Check for old communication files (older than 7 days)
|
157
|
+
old_comm_files = find_old_communication_files
|
158
|
+
if old_comm_files.count > 20
|
159
|
+
cleanup_actions << proc do
|
160
|
+
AgentCommunicator.instance.cleanup_old_messages(7)
|
161
|
+
Logger.info("Auto-cleaned #{old_comm_files.count} old communication files")
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Check for old notification history (older than 14 days)
|
166
|
+
if notification_history_size > 100
|
167
|
+
cleanup_actions << proc do
|
168
|
+
NotificationManager.instance.clear_history
|
169
|
+
Logger.info("Auto-cleaned notification history")
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Execute cleanup actions
|
174
|
+
cleanup_actions.each(&:call)
|
175
|
+
|
176
|
+
cleanup_actions.count
|
177
|
+
end
|
178
|
+
|
179
|
+
# Learn from user actions and update patterns
|
180
|
+
def learn_from_action(action_type, details = {})
|
181
|
+
@user_patterns['actions'] ||= {}
|
182
|
+
@user_patterns['actions'][action_type] ||= {
|
183
|
+
'count' => 0,
|
184
|
+
'last_used' => nil,
|
185
|
+
'success_rate' => 1.0,
|
186
|
+
'preferences' => {}
|
187
|
+
}
|
188
|
+
|
189
|
+
action_data = @user_patterns['actions'][action_type]
|
190
|
+
action_data['count'] += 1
|
191
|
+
action_data['last_used'] = Time.now.iso8601
|
192
|
+
|
193
|
+
# Update preferences based on details
|
194
|
+
details.each do |key, value|
|
195
|
+
action_data['preferences'][key.to_s] ||= {}
|
196
|
+
action_data['preferences'][key.to_s][value.to_s] ||= 0
|
197
|
+
action_data['preferences'][key.to_s][value.to_s] += 1
|
198
|
+
end
|
199
|
+
|
200
|
+
save_user_patterns
|
201
|
+
end
|
202
|
+
|
203
|
+
# Get context-aware command suggestions
|
204
|
+
def suggest_commands_for_context(context = {})
|
205
|
+
suggestions = []
|
206
|
+
|
207
|
+
# Based on current directory contents
|
208
|
+
if File.exist?('package.json')
|
209
|
+
suggestions << 'npm test' if context[:changed_files]&.any? { |f| f.end_with?('.js', '.ts') }
|
210
|
+
suggestions << 'npm run build' if context[:action] == 'deploy'
|
211
|
+
end
|
212
|
+
|
213
|
+
if File.exist?('Gemfile')
|
214
|
+
suggestions << 'bundle exec rspec' if context[:changed_files]&.any? { |f| f.end_with?('.rb') }
|
215
|
+
suggestions << 'bundle exec rubocop' if context[:action] == 'lint'
|
216
|
+
end
|
217
|
+
|
218
|
+
# Based on git status
|
219
|
+
if context[:git_status]
|
220
|
+
if context[:git_status][:modified_files] && context[:git_status][:modified_files] > 0
|
221
|
+
suggestions << 'enhance-swarm review'
|
222
|
+
end
|
223
|
+
|
224
|
+
if context[:git_status][:untracked_files] && context[:git_status][:untracked_files] > 0
|
225
|
+
suggestions << 'git add .'
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Based on user patterns
|
230
|
+
frequent_commands.each { |cmd| suggestions << cmd }
|
231
|
+
|
232
|
+
suggestions.uniq
|
233
|
+
end
|
234
|
+
|
235
|
+
# Auto-detect optimal concurrency settings
|
236
|
+
def suggest_concurrency_settings
|
237
|
+
# Base on system resources
|
238
|
+
cpu_cores = detect_cpu_cores
|
239
|
+
available_memory_gb = detect_available_memory
|
240
|
+
|
241
|
+
# Conservative defaults
|
242
|
+
max_agents = [cpu_cores / 2, 4].min
|
243
|
+
max_agents = [max_agents, 2].max # At least 2
|
244
|
+
|
245
|
+
# Adjust based on memory
|
246
|
+
if available_memory_gb < 4
|
247
|
+
max_agents = [max_agents, 2].min
|
248
|
+
elsif available_memory_gb > 16
|
249
|
+
max_agents = [max_agents + 2, 8].min
|
250
|
+
end
|
251
|
+
|
252
|
+
{
|
253
|
+
max_concurrent_agents: max_agents,
|
254
|
+
monitor_interval: max_agents > 4 ? 15 : 30,
|
255
|
+
timeout_multiplier: available_memory_gb < 8 ? 1.5 : 1.0
|
256
|
+
}
|
257
|
+
end
|
258
|
+
|
259
|
+
private
|
260
|
+
|
261
|
+
def load_settings
|
262
|
+
return default_settings unless File.exist?(DEFAULT_SETTINGS_FILE)
|
263
|
+
|
264
|
+
YAML.load_file(DEFAULT_SETTINGS_FILE)
|
265
|
+
rescue StandardError
|
266
|
+
default_settings
|
267
|
+
end
|
268
|
+
|
269
|
+
def load_user_patterns
|
270
|
+
return {} unless File.exist?(USER_PATTERNS_FILE)
|
271
|
+
|
272
|
+
JSON.parse(File.read(USER_PATTERNS_FILE))
|
273
|
+
rescue StandardError
|
274
|
+
{}
|
275
|
+
end
|
276
|
+
|
277
|
+
def save_user_patterns
|
278
|
+
ensure_settings_directory
|
279
|
+
File.write(USER_PATTERNS_FILE, JSON.pretty_generate(@user_patterns))
|
280
|
+
end
|
281
|
+
|
282
|
+
def ensure_settings_directory
|
283
|
+
dir = File.dirname(DEFAULT_SETTINGS_FILE)
|
284
|
+
FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
|
285
|
+
end
|
286
|
+
|
287
|
+
def default_settings
|
288
|
+
{
|
289
|
+
'auto_cleanup' => true,
|
290
|
+
'auto_retry' => true,
|
291
|
+
'smart_suggestions' => true,
|
292
|
+
'learning_enabled' => true,
|
293
|
+
'cleanup_threshold_days' => 7
|
294
|
+
}
|
295
|
+
end
|
296
|
+
|
297
|
+
def analyze_project_context
|
298
|
+
context = {}
|
299
|
+
|
300
|
+
# Git information
|
301
|
+
if Dir.exist?('.git')
|
302
|
+
context[:git] = {
|
303
|
+
branch: `git branch --show-current`.strip,
|
304
|
+
status: git_status_summary,
|
305
|
+
recent_commits: recent_commit_count
|
306
|
+
}
|
307
|
+
end
|
308
|
+
|
309
|
+
# Project files
|
310
|
+
context[:files] = {
|
311
|
+
package_json: File.exist?('package.json'),
|
312
|
+
gemfile: File.exist?('Gemfile'),
|
313
|
+
dockerfile: File.exist?('Dockerfile'),
|
314
|
+
config_files: Dir.glob('*.{yml,yaml,json,toml}').count
|
315
|
+
}
|
316
|
+
|
317
|
+
# Directory structure
|
318
|
+
context[:structure] = {
|
319
|
+
src_dirs: Dir.glob('{src,lib,app}').count,
|
320
|
+
test_dirs: Dir.glob('{test,spec,__tests__}').count,
|
321
|
+
has_nested_structure: Dir.glob('*/*').count > 10
|
322
|
+
}
|
323
|
+
|
324
|
+
context
|
325
|
+
end
|
326
|
+
|
327
|
+
def detect_project_type
|
328
|
+
return 'rails' if File.exist?('Gemfile') && File.exist?('config/application.rb')
|
329
|
+
return 'node' if File.exist?('package.json')
|
330
|
+
return 'python' if File.exist?('requirements.txt') || File.exist?('pyproject.toml')
|
331
|
+
return 'docker' if File.exist?('Dockerfile')
|
332
|
+
return 'static' if Dir.glob('*.html').any?
|
333
|
+
|
334
|
+
'general'
|
335
|
+
end
|
336
|
+
|
337
|
+
def suggest_technology_stack(project_type)
|
338
|
+
case project_type
|
339
|
+
when 'rails'
|
340
|
+
['Ruby on Rails', 'PostgreSQL', 'Redis', 'Sidekiq']
|
341
|
+
when 'node'
|
342
|
+
detect_node_stack
|
343
|
+
when 'python'
|
344
|
+
detect_python_stack
|
345
|
+
when 'docker'
|
346
|
+
['Docker', 'Docker Compose']
|
347
|
+
else
|
348
|
+
[]
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def detect_node_stack
|
353
|
+
stack = ['Node.js']
|
354
|
+
|
355
|
+
if File.exist?('package.json')
|
356
|
+
package_json = JSON.parse(File.read('package.json'))
|
357
|
+
deps = package_json['dependencies'] || {}
|
358
|
+
dev_deps = package_json['devDependencies'] || {}
|
359
|
+
all_deps = deps.merge(dev_deps)
|
360
|
+
|
361
|
+
stack << 'React' if all_deps.key?('react')
|
362
|
+
stack << 'Vue.js' if all_deps.key?('vue')
|
363
|
+
stack << 'Angular' if all_deps.key?('@angular/core')
|
364
|
+
stack << 'Express' if all_deps.key?('express')
|
365
|
+
stack << 'TypeScript' if all_deps.key?('typescript')
|
366
|
+
stack << 'Webpack' if all_deps.key?('webpack')
|
367
|
+
stack << 'Jest' if all_deps.key?('jest')
|
368
|
+
end
|
369
|
+
|
370
|
+
stack
|
371
|
+
end
|
372
|
+
|
373
|
+
def detect_python_stack
|
374
|
+
stack = ['Python']
|
375
|
+
|
376
|
+
requirements_files = ['requirements.txt', 'pyproject.toml', 'Pipfile']
|
377
|
+
req_content = ''
|
378
|
+
|
379
|
+
requirements_files.each do |file|
|
380
|
+
if File.exist?(file)
|
381
|
+
req_content += File.read(file).downcase
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
stack << 'Django' if req_content.include?('django')
|
386
|
+
stack << 'Flask' if req_content.include?('flask')
|
387
|
+
stack << 'FastAPI' if req_content.include?('fastapi')
|
388
|
+
stack << 'SQLAlchemy' if req_content.include?('sqlalchemy')
|
389
|
+
stack << 'PostgreSQL' if req_content.include?('psycopg')
|
390
|
+
stack << 'Redis' if req_content.include?('redis')
|
391
|
+
|
392
|
+
stack
|
393
|
+
end
|
394
|
+
|
395
|
+
def suggest_commands(project_type)
|
396
|
+
commands = {}
|
397
|
+
|
398
|
+
case project_type
|
399
|
+
when 'rails'
|
400
|
+
commands = {
|
401
|
+
'test' => 'bundle exec rspec',
|
402
|
+
'lint' => 'bundle exec rubocop',
|
403
|
+
'migrate' => 'bundle exec rails db:migrate',
|
404
|
+
'console' => 'bundle exec rails console'
|
405
|
+
}
|
406
|
+
when 'node'
|
407
|
+
package_json = File.exist?('package.json') ? JSON.parse(File.read('package.json')) : {}
|
408
|
+
scripts = package_json['scripts'] || {}
|
409
|
+
|
410
|
+
commands['test'] = scripts['test'] || 'npm test'
|
411
|
+
commands['build'] = scripts['build'] || 'npm run build'
|
412
|
+
commands['start'] = scripts['start'] || 'npm start'
|
413
|
+
commands['lint'] = scripts['lint'] || 'npm run lint'
|
414
|
+
when 'python'
|
415
|
+
commands = {
|
416
|
+
'test' => 'pytest',
|
417
|
+
'lint' => 'flake8 .',
|
418
|
+
'format' => 'black .',
|
419
|
+
'install' => 'pip install -r requirements.txt'
|
420
|
+
}
|
421
|
+
end
|
422
|
+
|
423
|
+
commands
|
424
|
+
end
|
425
|
+
|
426
|
+
def suggest_orchestration_settings
|
427
|
+
concurrency = suggest_concurrency_settings
|
428
|
+
|
429
|
+
{
|
430
|
+
'max_concurrent_agents' => concurrency[:max_concurrent_agents],
|
431
|
+
'monitor_interval' => concurrency[:monitor_interval],
|
432
|
+
'timeout_per_agent' => 300,
|
433
|
+
'auto_cleanup' => true
|
434
|
+
}
|
435
|
+
end
|
436
|
+
|
437
|
+
def suggest_mcp_tools(project_type)
|
438
|
+
tools = {
|
439
|
+
'desktop_commander' => false,
|
440
|
+
'gemini_cli' => true
|
441
|
+
}
|
442
|
+
|
443
|
+
# Enable specific tools based on project type
|
444
|
+
case project_type
|
445
|
+
when 'rails', 'node', 'python'
|
446
|
+
tools['gemini_cli'] = true # Good for large codebases
|
447
|
+
when 'docker'
|
448
|
+
tools['desktop_commander'] = true # May need system operations
|
449
|
+
end
|
450
|
+
|
451
|
+
tools
|
452
|
+
end
|
453
|
+
|
454
|
+
def stale_worktrees_detected?
|
455
|
+
find_stale_worktrees.count > 2
|
456
|
+
end
|
457
|
+
|
458
|
+
def find_stale_worktrees
|
459
|
+
return [] unless Dir.exist?('.git')
|
460
|
+
|
461
|
+
begin
|
462
|
+
worktree_output = `git worktree list 2>/dev/null`
|
463
|
+
stale_worktrees = []
|
464
|
+
|
465
|
+
worktree_output.lines.each do |line|
|
466
|
+
next unless line.include?('swarm/')
|
467
|
+
|
468
|
+
parts = line.split
|
469
|
+
path = parts[0]
|
470
|
+
|
471
|
+
if Dir.exist?(path)
|
472
|
+
# Check if worktree is old (no activity in 24 hours)
|
473
|
+
last_modified = Dir.glob(File.join(path, '**/*')).map { |f| File.mtime(f) rescue Time.now }.max
|
474
|
+
if last_modified && (Time.now - last_modified) > 86400 # 24 hours
|
475
|
+
stale_worktrees << path
|
476
|
+
end
|
477
|
+
else
|
478
|
+
stale_worktrees << path # Broken worktree reference
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
stale_worktrees
|
483
|
+
rescue StandardError
|
484
|
+
[]
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
def pending_agent_messages?
|
489
|
+
return false unless defined?(AgentCommunicator)
|
490
|
+
|
491
|
+
AgentCommunicator.instance.pending_messages.any?
|
492
|
+
end
|
493
|
+
|
494
|
+
def tests_need_running?
|
495
|
+
return false unless @project_context[:git]
|
496
|
+
|
497
|
+
# Check if there are unstaged changes in source files
|
498
|
+
status = `git status --porcelain`.lines
|
499
|
+
source_files_changed = status.any? do |line|
|
500
|
+
file = line[3..-1]&.strip
|
501
|
+
file&.match?(/\.(rb|js|ts|py|java|go|rs)$/)
|
502
|
+
end
|
503
|
+
|
504
|
+
source_files_changed
|
505
|
+
end
|
506
|
+
|
507
|
+
def determine_test_command
|
508
|
+
project_type = detect_project_type
|
509
|
+
|
510
|
+
case project_type
|
511
|
+
when 'rails' then 'bundle exec rspec'
|
512
|
+
when 'node' then 'npm test'
|
513
|
+
when 'python' then 'pytest'
|
514
|
+
else 'echo "No test command configured"'
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
def time_based_suggestions
|
519
|
+
suggestions = []
|
520
|
+
current_hour = Time.now.hour
|
521
|
+
|
522
|
+
# Morning suggestions (8-10 AM)
|
523
|
+
if current_hour.between?(8, 10)
|
524
|
+
suggestions << {
|
525
|
+
action: 'status',
|
526
|
+
command: 'enhance-swarm status',
|
527
|
+
reason: 'Morning status check',
|
528
|
+
priority: :low
|
529
|
+
}
|
530
|
+
end
|
531
|
+
|
532
|
+
# End of day suggestions (5-7 PM)
|
533
|
+
if current_hour.between?(17, 19)
|
534
|
+
suggestions << {
|
535
|
+
action: 'cleanup',
|
536
|
+
command: 'enhance-swarm cleanup --all',
|
537
|
+
reason: 'End of day cleanup',
|
538
|
+
priority: :low
|
539
|
+
}
|
540
|
+
end
|
541
|
+
|
542
|
+
suggestions
|
543
|
+
end
|
544
|
+
|
545
|
+
def priority_weight(priority)
|
546
|
+
case priority
|
547
|
+
when :critical then 4
|
548
|
+
when :high then 3
|
549
|
+
when :medium then 2
|
550
|
+
when :low then 1
|
551
|
+
else 0
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
def find_old_communication_files
|
556
|
+
comm_dir = '.enhance_swarm/communication'
|
557
|
+
return [] unless Dir.exist?(comm_dir)
|
558
|
+
|
559
|
+
cutoff = Time.now - (7 * 24 * 60 * 60) # 7 days ago
|
560
|
+
|
561
|
+
Dir.glob(File.join(comm_dir, '*.json')).select do |file|
|
562
|
+
File.mtime(file) < cutoff
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
def notification_history_size
|
567
|
+
return 0 unless defined?(NotificationManager)
|
568
|
+
|
569
|
+
NotificationManager.instance.recent_notifications(1000).count
|
570
|
+
end
|
571
|
+
|
572
|
+
def frequent_commands
|
573
|
+
return [] unless @user_patterns['actions']
|
574
|
+
|
575
|
+
@user_patterns['actions']
|
576
|
+
.select { |_, data| data['count'] > 5 }
|
577
|
+
.sort_by { |_, data| -data['count'] }
|
578
|
+
.first(5)
|
579
|
+
.map { |action, _| action }
|
580
|
+
end
|
581
|
+
|
582
|
+
def record_success_pattern(operation, attempt)
|
583
|
+
@user_patterns['retry_patterns'] ||= {}
|
584
|
+
@user_patterns['retry_patterns'][operation.to_s] ||= {
|
585
|
+
'total_attempts' => 0,
|
586
|
+
'success_attempts' => 0,
|
587
|
+
'avg_attempts_to_success' => 1.0
|
588
|
+
}
|
589
|
+
|
590
|
+
pattern = @user_patterns['retry_patterns'][operation.to_s]
|
591
|
+
pattern['total_attempts'] += attempt
|
592
|
+
pattern['success_attempts'] += 1
|
593
|
+
pattern['avg_attempts_to_success'] = pattern['total_attempts'].to_f / pattern['success_attempts']
|
594
|
+
|
595
|
+
save_user_patterns
|
596
|
+
end
|
597
|
+
|
598
|
+
def record_failure_pattern(operation, attempt, error)
|
599
|
+
@user_patterns['failure_patterns'] ||= {}
|
600
|
+
@user_patterns['failure_patterns'][operation.to_s] ||= {
|
601
|
+
'count' => 0,
|
602
|
+
'last_error' => nil,
|
603
|
+
'common_errors' => {}
|
604
|
+
}
|
605
|
+
|
606
|
+
pattern = @user_patterns['failure_patterns'][operation.to_s]
|
607
|
+
pattern['count'] += 1
|
608
|
+
pattern['last_error'] = error.message
|
609
|
+
pattern['common_errors'][error.class.name] ||= 0
|
610
|
+
pattern['common_errors'][error.class.name] += 1
|
611
|
+
|
612
|
+
save_user_patterns
|
613
|
+
end
|
614
|
+
|
615
|
+
def git_status_summary
|
616
|
+
return {} unless Dir.exist?('.git')
|
617
|
+
|
618
|
+
begin
|
619
|
+
status_output = `git status --porcelain`
|
620
|
+
{
|
621
|
+
modified_files: status_output.lines.count { |line| line.start_with?(' M', 'M ') },
|
622
|
+
untracked_files: status_output.lines.count { |line| line.start_with?('??') },
|
623
|
+
staged_files: status_output.lines.count { |line| line.start_with?('A ', 'M ') }
|
624
|
+
}
|
625
|
+
rescue StandardError
|
626
|
+
{}
|
627
|
+
end
|
628
|
+
end
|
629
|
+
|
630
|
+
def recent_commit_count
|
631
|
+
return 0 unless Dir.exist?('.git')
|
632
|
+
|
633
|
+
begin
|
634
|
+
`git log --oneline --since="1 week ago"`.lines.count
|
635
|
+
rescue StandardError
|
636
|
+
0
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
def detect_cpu_cores
|
641
|
+
case RUBY_PLATFORM
|
642
|
+
when /linux/
|
643
|
+
`nproc`.to_i
|
644
|
+
when /darwin/
|
645
|
+
`sysctl -n hw.ncpu`.to_i
|
646
|
+
else
|
647
|
+
4 # Fallback
|
648
|
+
end
|
649
|
+
rescue StandardError
|
650
|
+
4
|
651
|
+
end
|
652
|
+
|
653
|
+
def detect_available_memory
|
654
|
+
case RUBY_PLATFORM
|
655
|
+
when /linux/
|
656
|
+
# Parse /proc/meminfo
|
657
|
+
meminfo = File.read('/proc/meminfo')
|
658
|
+
available_kb = meminfo[/MemAvailable:\s*(\d+)/, 1].to_i
|
659
|
+
available_kb / 1024.0 / 1024.0 # Convert to GB
|
660
|
+
when /darwin/
|
661
|
+
# Use vm_stat
|
662
|
+
vm_stat = `vm_stat`
|
663
|
+
page_size = 4096
|
664
|
+
pages_free = vm_stat[/Pages free:\s*(\d+)/, 1].to_i
|
665
|
+
(pages_free * page_size) / 1024.0 / 1024.0 / 1024.0 # Convert to GB
|
666
|
+
else
|
667
|
+
8.0 # Fallback
|
668
|
+
end
|
669
|
+
rescue StandardError
|
670
|
+
8.0
|
671
|
+
end
|
672
|
+
|
673
|
+
# Class methods for singleton access
|
674
|
+
class << self
|
675
|
+
def instance
|
676
|
+
@instance ||= new
|
677
|
+
end
|
678
|
+
|
679
|
+
def suggest_role_for_task(*args)
|
680
|
+
instance.suggest_role_for_task(*args)
|
681
|
+
end
|
682
|
+
|
683
|
+
def suggest_configuration
|
684
|
+
instance.suggest_configuration
|
685
|
+
end
|
686
|
+
|
687
|
+
def auto_retry_with_backoff(*args, &block)
|
688
|
+
instance.auto_retry_with_backoff(*args, &block)
|
689
|
+
end
|
690
|
+
|
691
|
+
def suggest_next_actions(*args)
|
692
|
+
instance.suggest_next_actions(*args)
|
693
|
+
end
|
694
|
+
|
695
|
+
def auto_cleanup_if_needed
|
696
|
+
instance.auto_cleanup_if_needed
|
697
|
+
end
|
698
|
+
|
699
|
+
def learn_from_action(*args)
|
700
|
+
instance.learn_from_action(*args)
|
701
|
+
end
|
702
|
+
|
703
|
+
def get_suggestions(context = {})
|
704
|
+
instance.suggest_next_actions(context)
|
705
|
+
end
|
706
|
+
end
|
707
|
+
end
|
708
|
+
end
|