soba-cli 0.1.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 (101) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/commands/osoba/add-backlog.md +173 -0
  3. data/.claude/commands/osoba/implement.md +151 -0
  4. data/.claude/commands/osoba/plan.md +217 -0
  5. data/.claude/commands/osoba/review.md +133 -0
  6. data/.claude/commands/osoba/revise.md +176 -0
  7. data/.claude/commands/soba/implement.md +88 -0
  8. data/.claude/commands/soba/plan.md +93 -0
  9. data/.claude/commands/soba/review.md +91 -0
  10. data/.claude/commands/soba/revise.md +76 -0
  11. data/.devcontainer/.env +2 -0
  12. data/.devcontainer/Dockerfile +3 -0
  13. data/.devcontainer/LICENSE +21 -0
  14. data/.devcontainer/README.md +85 -0
  15. data/.devcontainer/bin/devcontainer-common.sh +50 -0
  16. data/.devcontainer/bin/down +35 -0
  17. data/.devcontainer/bin/rebuild +10 -0
  18. data/.devcontainer/bin/up +11 -0
  19. data/.devcontainer/compose.yaml +28 -0
  20. data/.devcontainer/devcontainer.json +53 -0
  21. data/.devcontainer/post-attach.sh +29 -0
  22. data/.devcontainer/post-create.sh +62 -0
  23. data/.devcontainer/setup/01-os-package.sh +19 -0
  24. data/.devcontainer/setup/02-npm-package.sh +22 -0
  25. data/.devcontainer/setup/03-mcp-server.sh +33 -0
  26. data/.devcontainer/setup/04-tool.sh +17 -0
  27. data/.devcontainer/setup/05-soba-setup.sh +66 -0
  28. data/.devcontainer/setup/scripts/functions/install_apt.sh +77 -0
  29. data/.devcontainer/setup/scripts/functions/install_npm.sh +71 -0
  30. data/.devcontainer/setup/scripts/functions/mcp_config.sh +14 -0
  31. data/.devcontainer/setup/scripts/functions/print_message.sh +59 -0
  32. data/.devcontainer/setup/scripts/setup/mcp-markdownify.sh +39 -0
  33. data/.devcontainer/sync-envs.sh +58 -0
  34. data/.envrc.sample +7 -0
  35. data/.rspec +4 -0
  36. data/.rubocop.yml +70 -0
  37. data/.rubocop_airbnb.yml +2 -0
  38. data/.rubocop_todo.yml +74 -0
  39. data/.tool-versions +1 -0
  40. data/CLAUDE.md +20 -0
  41. data/LICENSE +21 -0
  42. data/README.md +384 -0
  43. data/README_ja.md +384 -0
  44. data/Rakefile +18 -0
  45. data/bin/soba +120 -0
  46. data/config/config.yml.example +36 -0
  47. data/docs/business/INDEX.md +6 -0
  48. data/docs/business/overview.md +42 -0
  49. data/docs/business/workflow.md +143 -0
  50. data/docs/development/INDEX.md +10 -0
  51. data/docs/development/architecture.md +69 -0
  52. data/docs/development/coding-standards.md +152 -0
  53. data/docs/development/distribution.md +26 -0
  54. data/docs/development/implementation-guide.md +103 -0
  55. data/docs/development/testing-strategy.md +128 -0
  56. data/docs/development/tmux-management.md +253 -0
  57. data/docs/document_system.md +58 -0
  58. data/lib/soba/commands/config/show.rb +63 -0
  59. data/lib/soba/commands/init.rb +778 -0
  60. data/lib/soba/commands/open.rb +144 -0
  61. data/lib/soba/commands/start.rb +442 -0
  62. data/lib/soba/commands/status.rb +175 -0
  63. data/lib/soba/commands/stop.rb +147 -0
  64. data/lib/soba/config_loader.rb +32 -0
  65. data/lib/soba/configuration.rb +268 -0
  66. data/lib/soba/container.rb +48 -0
  67. data/lib/soba/domain/issue.rb +38 -0
  68. data/lib/soba/domain/phase_strategy.rb +74 -0
  69. data/lib/soba/infrastructure/errors.rb +23 -0
  70. data/lib/soba/infrastructure/github_client.rb +399 -0
  71. data/lib/soba/infrastructure/lock_manager.rb +129 -0
  72. data/lib/soba/infrastructure/tmux_client.rb +331 -0
  73. data/lib/soba/services/ansi_processor.rb +92 -0
  74. data/lib/soba/services/auto_merge_service.rb +133 -0
  75. data/lib/soba/services/closed_issue_window_cleaner.rb +96 -0
  76. data/lib/soba/services/daemon_service.rb +83 -0
  77. data/lib/soba/services/git_workspace_manager.rb +102 -0
  78. data/lib/soba/services/issue_monitor.rb +29 -0
  79. data/lib/soba/services/issue_processor.rb +215 -0
  80. data/lib/soba/services/issue_watcher.rb +193 -0
  81. data/lib/soba/services/pid_manager.rb +87 -0
  82. data/lib/soba/services/process_info.rb +58 -0
  83. data/lib/soba/services/queueing_service.rb +98 -0
  84. data/lib/soba/services/session_logger.rb +111 -0
  85. data/lib/soba/services/session_resolver.rb +72 -0
  86. data/lib/soba/services/slack_notifier.rb +121 -0
  87. data/lib/soba/services/status_manager.rb +74 -0
  88. data/lib/soba/services/test_process_manager.rb +84 -0
  89. data/lib/soba/services/tmux_session_manager.rb +251 -0
  90. data/lib/soba/services/workflow_blocking_checker.rb +73 -0
  91. data/lib/soba/services/workflow_executor.rb +256 -0
  92. data/lib/soba/services/workflow_integrity_checker.rb +151 -0
  93. data/lib/soba/templates/claude_commands/implement.md +88 -0
  94. data/lib/soba/templates/claude_commands/plan.md +93 -0
  95. data/lib/soba/templates/claude_commands/review.md +91 -0
  96. data/lib/soba/templates/claude_commands/revise.md +76 -0
  97. data/lib/soba/version.rb +5 -0
  98. data/lib/soba.rb +44 -0
  99. data/lib/tasks/gem.rake +75 -0
  100. data/soba-cli.gemspec +59 -0
  101. metadata +430 -0
@@ -0,0 +1,778 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/blank"
4
+ require "active_support/core_ext/string/exclude"
5
+ require "active_support/core_ext/object/deep_dup"
6
+ require "pathname"
7
+ require "yaml"
8
+ require "io/console"
9
+ require_relative "../infrastructure/github_client"
10
+
11
+ module Soba
12
+ module Commands
13
+ class Init
14
+ DEFAULT_CONFIG = {
15
+ 'github' => {
16
+ 'token' => '${GITHUB_TOKEN}',
17
+ },
18
+ 'workflow' => {
19
+ 'interval' => 20,
20
+ 'auto_merge_enabled' => true,
21
+ 'closed_issue_cleanup_enabled' => true,
22
+ 'closed_issue_cleanup_interval' => 300,
23
+ 'tmux_command_delay' => 3,
24
+ 'phase_labels' => {
25
+ 'todo' => 'soba:todo',
26
+ 'queued' => 'soba:queued',
27
+ 'planning' => 'soba:planning',
28
+ 'ready' => 'soba:ready',
29
+ 'doing' => 'soba:doing',
30
+ 'review_requested' => 'soba:review-requested',
31
+ 'reviewing' => 'soba:reviewing',
32
+ 'done' => 'soba:done',
33
+ 'requires_changes' => 'soba:requires-changes',
34
+ 'revising' => 'soba:revising',
35
+ 'merged' => 'soba:merged',
36
+ },
37
+ },
38
+ 'slack' => {
39
+ 'webhook_url' => '${SLACK_WEBHOOK_URL}',
40
+ 'notifications_enabled' => false,
41
+ },
42
+ }.freeze
43
+
44
+ LABEL_COLORS = {
45
+ 'todo' => '808080', # Gray
46
+ 'queued' => '9370db', # Medium Purple
47
+ 'planning' => '1e90ff', # Blue
48
+ 'ready' => '228b22', # Green
49
+ 'doing' => 'ffd700', # Yellow
50
+ 'review_requested' => 'ff8c00', # Orange
51
+ 'reviewing' => 'ff6347', # Tomato
52
+ 'done' => '32cd32', # Lime Green
53
+ 'requires_changes' => 'dc143c', # Crimson
54
+ 'revising' => 'ff1493', # Deep Pink
55
+ 'merged' => '6b8e23', # Olive Drab
56
+ 'lgtm' => '00ff00', # Pure Green
57
+ }.freeze
58
+
59
+ LABEL_DESCRIPTIONS = {
60
+ 'todo' => 'To-do task waiting to be queued',
61
+ 'queued' => 'Queued for processing',
62
+ 'planning' => 'Planning phase',
63
+ 'ready' => 'Ready for implementation',
64
+ 'doing' => 'In progress',
65
+ 'review_requested' => 'Review requested',
66
+ 'reviewing' => 'Under review',
67
+ 'done' => 'Review completed',
68
+ 'requires_changes' => 'Changes requested',
69
+ 'revising' => 'Revising based on review feedback',
70
+ 'merged' => 'PR merged and issue closed',
71
+ 'lgtm' => 'PR approved for auto-merge',
72
+ }.freeze
73
+
74
+ DEFAULT_PHASE_CONFIG = {
75
+ 'plan' => {
76
+ 'command' => 'claude',
77
+ 'options' => ['--dangerously-skip-permissions'],
78
+ 'parameter' => '/soba:plan {{issue-number}}',
79
+ },
80
+ 'implement' => {
81
+ 'command' => 'claude',
82
+ 'options' => ['--dangerously-skip-permissions'],
83
+ 'parameter' => '/soba:implement {{issue-number}}',
84
+ },
85
+ 'review' => {
86
+ 'command' => 'claude',
87
+ 'options' => ['--dangerously-skip-permissions'],
88
+ 'parameter' => '/soba:review {{issue-number}}',
89
+ },
90
+ 'revise' => {
91
+ 'command' => 'claude',
92
+ 'options' => ['--dangerously-skip-permissions'],
93
+ 'parameter' => '/soba:revise {{issue-number}}',
94
+ },
95
+ }.freeze
96
+
97
+ def initialize(interactive: false)
98
+ @interactive = interactive
99
+ end
100
+
101
+ def execute
102
+ puts "🚀 Initializing soba configuration..."
103
+ puts ""
104
+
105
+ config_path = Pathname.pwd.join('.soba', 'config.yml')
106
+
107
+ if config_path.exist?
108
+ puts "⚠️ Configuration file already exists at: #{config_path}"
109
+ print "Do you want to overwrite it? (y/N): "
110
+ response = $stdin.gets.chomp.downcase
111
+ if response != 'y' && response != 'yes'
112
+ puts "✅ Configuration unchanged."
113
+ return
114
+ end
115
+ end
116
+
117
+ if @interactive
118
+ execute_interactive(config_path)
119
+ else
120
+ execute_non_interactive(config_path)
121
+ end
122
+ rescue Interrupt
123
+ puts "\n\n❌ Setup cancelled."
124
+ raise Soba::CommandError, "Setup cancelled"
125
+ rescue StandardError => e
126
+ puts "\n❌ Error: #{e.message}"
127
+ raise
128
+ end
129
+
130
+ private
131
+
132
+ def execute_non_interactive(config_path)
133
+ # GitHub repository - auto-detect from git remote
134
+ repository = detect_github_repository
135
+
136
+ unless repository
137
+ puts "❌ Error: Cannot detect GitHub repository from git remote."
138
+ puts " Please run 'soba init --interactive' for manual setup."
139
+ raise Soba::CommandError, "Cannot detect GitHub repository"
140
+ end
141
+
142
+ # Create configuration with default values
143
+ config = DEFAULT_CONFIG.deep_dup
144
+ config['github']['repository'] = repository
145
+
146
+ # Add default phase configuration
147
+ config['phase'] = DEFAULT_PHASE_CONFIG.deep_dup
148
+
149
+ # Slack configuration is already included in DEFAULT_CONFIG
150
+
151
+ # Write configuration file
152
+ write_config_file(config_path, config)
153
+
154
+ puts ""
155
+ puts "✅ Configuration created successfully!"
156
+ puts "📁 Location: #{config_path}"
157
+ puts "📦 Repository: #{repository}"
158
+
159
+ check_github_token(token: '${GITHUB_TOKEN}')
160
+ check_slack_webhook_url(config: config)
161
+ deploy_claude_templates
162
+ handle_gitignore
163
+ create_github_labels(config)
164
+
165
+ puts ""
166
+ puts "🎉 Setup complete! You can now use:"
167
+ puts " soba config - View current configuration"
168
+ puts " soba issue list #{config['github']['repository']} - List repository issues"
169
+ end
170
+
171
+ def execute_interactive(config_path)
172
+ # Collect configuration values
173
+ puts "Let's set up your GitHub configuration:"
174
+ puts ""
175
+
176
+ # GitHub repository - auto-detect from git remote
177
+ default_repo = detect_github_repository
178
+
179
+ if default_repo
180
+ print "Enter GitHub repository (format: owner/repo) [#{default_repo}]: "
181
+ else
182
+ print "Enter GitHub repository (format: owner/repo): "
183
+ end
184
+
185
+ repository = $stdin.gets.chomp
186
+ if repository.empty? && default_repo
187
+ repository = default_repo
188
+ end
189
+
190
+ while repository.blank? || repository.exclude?('/')
191
+ puts "❌ Invalid format. Please use: owner/repo"
192
+ print "Enter GitHub repository: "
193
+ repository = $stdin.gets.chomp
194
+ end
195
+
196
+ # GitHub token
197
+ puts ""
198
+ puts "GitHub Personal Access Token (PAT) setup:"
199
+ puts " 1. Use environment variable ${GITHUB_TOKEN} (recommended)"
200
+ puts " 2. Enter token directly (will be visible in config file)"
201
+ print "Choose option (1-2) [1]: "
202
+ token_option = $stdin.gets.chomp
203
+ token_option = '1' if token_option.empty?
204
+
205
+ token = if token_option == '2'
206
+ print "Enter GitHub token: "
207
+ # Hide input for security
208
+ $stdin.noecho(&:gets).chomp.tap { puts }
209
+ else
210
+ '${GITHUB_TOKEN}'
211
+ end
212
+
213
+ # Polling interval
214
+ puts ""
215
+ print "Enter polling interval in seconds [20]: "
216
+ interval = $stdin.gets.chomp
217
+ interval = '20' if interval.empty?
218
+ interval = interval.to_i
219
+ interval = 20 if interval <= 0
220
+
221
+ # Phase labels configuration
222
+ puts ""
223
+ puts "Phase labels configuration:"
224
+ puts "These labels are used to track issue progress through the workflow"
225
+ puts ""
226
+
227
+ # Planning phase label
228
+ print "Enter planning phase label [soba:planning]: "
229
+ planning_label = $stdin.gets.chomp
230
+ planning_label = 'soba:planning' if planning_label.empty?
231
+
232
+ # Ready phase label
233
+ print "Enter ready phase label [soba:ready]: "
234
+ ready_label = $stdin.gets.chomp
235
+ ready_label = 'soba:ready' if ready_label.empty?
236
+
237
+ # Doing phase label
238
+ print "Enter doing phase label [soba:doing]: "
239
+ doing_label = $stdin.gets.chomp
240
+ doing_label = 'soba:doing' if doing_label.empty?
241
+
242
+ # Review requested phase label
243
+ print "Enter review requested phase label [soba:review-requested]: "
244
+ review_label = $stdin.gets.chomp
245
+ review_label = 'soba:review-requested' if review_label.empty?
246
+
247
+ # Auto-merge configuration
248
+ puts ""
249
+ print "Enable auto-merge for PRs with soba:lgtm label? (y/n) [y]: "
250
+ auto_merge = $stdin.gets.chomp.downcase
251
+ auto_merge = 'y' if auto_merge.empty?
252
+ auto_merge_enabled = auto_merge != 'n'
253
+
254
+ # Slack notification configuration
255
+ puts ""
256
+ print "Enable Slack notifications for phase starts? (y/N): "
257
+ slack_enabled = $stdin.gets.chomp.downcase
258
+ slack_enabled = 'n' if slack_enabled.empty?
259
+ slack_notifications_enabled = slack_enabled == 'y'
260
+
261
+ slack_webhook_url = '${SLACK_WEBHOOK_URL}'
262
+ if slack_notifications_enabled
263
+ puts "Slack Webhook URL setup:"
264
+ puts " 1. Use environment variable ${SLACK_WEBHOOK_URL} (recommended)"
265
+ puts " 2. Enter webhook URL directly (will be visible in config file)"
266
+ print "Choose option (1-2) [1]: "
267
+ slack_option = $stdin.gets.chomp
268
+ slack_option = '1' if slack_option.empty?
269
+
270
+ if slack_option == '2'
271
+ print "Enter Slack webhook URL: "
272
+ # Hide input for security
273
+ slack_webhook_url = $stdin.noecho(&:gets).chomp.tap { puts }
274
+ end
275
+ end
276
+
277
+ # Workflow commands configuration
278
+ puts ""
279
+ puts "Workflow commands configuration (optional):"
280
+ puts "These commands will be executed during each phase"
281
+ puts ""
282
+
283
+ # Plan phase command
284
+ puts "Planning phase command:"
285
+ print "Enter command (e.g., claude) [#{DEFAULT_PHASE_CONFIG['plan']['command']}]: "
286
+ plan_command = $stdin.gets.chomp
287
+ plan_command = DEFAULT_PHASE_CONFIG['plan']['command'] if plan_command.empty?
288
+ if plan_command.downcase == 'skip'
289
+ plan_command = nil
290
+ end
291
+
292
+ plan_options = []
293
+ plan_parameter = nil
294
+ if plan_command
295
+ default_options = DEFAULT_PHASE_CONFIG['plan']['options'].join(',')
296
+ print "Enter options (comma-separated, e.g., --dangerously-skip-permissions) [#{default_options}]: "
297
+ options_input = $stdin.gets.chomp
298
+ if options_input.empty?
299
+ plan_options = DEFAULT_PHASE_CONFIG['plan']['options']
300
+ else
301
+ plan_options = options_input.split(',').map(&:strip).reject(&:empty?)
302
+ end
303
+
304
+ default_param = DEFAULT_PHASE_CONFIG['plan']['parameter']
305
+ print "Enter parameter (use {{issue-number}} for issue number) [#{default_param}]: "
306
+ plan_parameter = $stdin.gets.chomp
307
+ plan_parameter = DEFAULT_PHASE_CONFIG['plan']['parameter'] if plan_parameter.empty?
308
+ end
309
+
310
+ # Implement phase command
311
+ puts ""
312
+ puts "Implementation phase command:"
313
+ print "Enter command (e.g., claude) [#{DEFAULT_PHASE_CONFIG['implement']['command']}]: "
314
+ implement_command = $stdin.gets.chomp
315
+ implement_command = DEFAULT_PHASE_CONFIG['implement']['command'] if implement_command.empty?
316
+ if implement_command.downcase == 'skip'
317
+ implement_command = nil
318
+ end
319
+
320
+ implement_options = []
321
+ implement_parameter = nil
322
+ if implement_command
323
+ default_options = DEFAULT_PHASE_CONFIG['implement']['options'].join(',')
324
+ print "Enter options (comma-separated, e.g., --dangerously-skip-permissions) [#{default_options}]: "
325
+ options_input = $stdin.gets.chomp
326
+ if options_input.empty?
327
+ implement_options = DEFAULT_PHASE_CONFIG['implement']['options']
328
+ else
329
+ implement_options = options_input.split(',').map(&:strip).reject(&:empty?)
330
+ end
331
+
332
+ default_param = DEFAULT_PHASE_CONFIG['implement']['parameter']
333
+ print "Enter parameter (use {{issue-number}} for issue number) [#{default_param}]: "
334
+ implement_parameter = $stdin.gets.chomp
335
+ implement_parameter = DEFAULT_PHASE_CONFIG['implement']['parameter'] if implement_parameter.empty?
336
+ end
337
+
338
+ # Review phase command
339
+ puts ""
340
+ puts "Review phase command:"
341
+ print "Enter command (e.g., claude) [#{DEFAULT_PHASE_CONFIG['review']['command']}]: "
342
+ review_command = $stdin.gets.chomp
343
+ review_command = DEFAULT_PHASE_CONFIG['review']['command'] if review_command.empty?
344
+ if review_command.downcase == 'skip'
345
+ review_command = nil
346
+ end
347
+
348
+ review_options = []
349
+ review_parameter = nil
350
+ if review_command
351
+ default_options = DEFAULT_PHASE_CONFIG['review']['options'].join(',')
352
+ print "Enter options (comma-separated, e.g., --dangerously-skip-permissions) [#{default_options}]: "
353
+ options_input = $stdin.gets.chomp
354
+ if options_input.empty?
355
+ review_options = DEFAULT_PHASE_CONFIG['review']['options']
356
+ else
357
+ review_options = options_input.split(',').map(&:strip).reject(&:empty?)
358
+ end
359
+
360
+ default_param = DEFAULT_PHASE_CONFIG['review']['parameter']
361
+ print "Enter parameter (use {{issue-number}} for issue number) [#{default_param}]: "
362
+ review_parameter = $stdin.gets.chomp
363
+ review_parameter = DEFAULT_PHASE_CONFIG['review']['parameter'] if review_parameter.empty?
364
+ end
365
+
366
+ # Create configuration
367
+ config = {
368
+ 'github' => {
369
+ 'token' => token,
370
+ 'repository' => repository,
371
+ },
372
+ 'workflow' => {
373
+ 'interval' => interval,
374
+ 'auto_merge_enabled' => auto_merge_enabled,
375
+ 'closed_issue_cleanup_enabled' => true,
376
+ 'closed_issue_cleanup_interval' => 300,
377
+ 'tmux_command_delay' => 3,
378
+ 'phase_labels' => {
379
+ 'todo' => 'soba:todo',
380
+ 'queued' => 'soba:queued',
381
+ 'planning' => planning_label,
382
+ 'ready' => ready_label,
383
+ 'doing' => doing_label,
384
+ 'review_requested' => review_label,
385
+ 'reviewing' => 'soba:reviewing',
386
+ 'done' => 'soba:done',
387
+ 'requires_changes' => 'soba:requires-changes',
388
+ 'revising' => 'soba:revising',
389
+ 'merged' => 'soba:merged',
390
+ },
391
+ },
392
+ 'slack' => {
393
+ 'webhook_url' => slack_webhook_url,
394
+ 'notifications_enabled' => slack_notifications_enabled,
395
+ },
396
+ }
397
+
398
+ # Add phase configuration if provided
399
+ if plan_command || implement_command || review_command
400
+ config['phase'] = {}
401
+
402
+ if plan_command
403
+ config['phase']['plan'] = {
404
+ 'command' => plan_command,
405
+ 'options' => plan_options,
406
+ 'parameter' => plan_parameter,
407
+ }
408
+ end
409
+
410
+ if implement_command
411
+ config['phase']['implement'] = {
412
+ 'command' => implement_command,
413
+ 'options' => implement_options,
414
+ 'parameter' => implement_parameter,
415
+ }
416
+ end
417
+
418
+ if review_command
419
+ config['phase']['review'] = {
420
+ 'command' => review_command,
421
+ 'options' => review_options,
422
+ 'parameter' => review_parameter,
423
+ }
424
+ end
425
+ end
426
+
427
+ # Write configuration file
428
+ write_config_file(config_path, config)
429
+
430
+ puts ""
431
+ puts "✅ Configuration created successfully!"
432
+ puts "📁 Location: #{config_path}"
433
+
434
+ check_github_token(token: token)
435
+ check_slack_webhook_url(config: config)
436
+ deploy_claude_templates
437
+ handle_gitignore
438
+ create_github_labels(config)
439
+
440
+ puts ""
441
+ puts "🎉 Setup complete! You can now use:"
442
+ puts " soba config - View current configuration"
443
+ puts " soba issue list #{config['github']['repository']} - List repository issues"
444
+ end
445
+
446
+ def write_config_file(config_path, config)
447
+ config_path.dirname.mkpath
448
+ config_content = <<~YAML
449
+ # soba CLI configuration
450
+ # Generated by: soba init
451
+ # Date: #{Time.now}
452
+
453
+ github:
454
+ # GitHub Personal Access Token
455
+ # Can use environment variable: ${GITHUB_TOKEN}
456
+ token: #{config['github']['token']}
457
+
458
+ # Target repository (format: owner/repo)
459
+ repository: #{config['github']['repository']}
460
+
461
+ workflow:
462
+ # Issue polling interval in seconds
463
+ interval: #{config['workflow']['interval']}
464
+
465
+ # Enable automatic merging of PRs with soba:lgtm label
466
+ auto_merge_enabled: #{config['workflow']['auto_merge_enabled']}
467
+
468
+ # Enable automatic cleanup of tmux windows for closed issues
469
+ closed_issue_cleanup_enabled: #{config['workflow']['closed_issue_cleanup_enabled']}
470
+
471
+ # Cleanup check interval in seconds
472
+ closed_issue_cleanup_interval: #{config['workflow']['closed_issue_cleanup_interval']}
473
+
474
+ # Delay (in seconds) before sending commands to new tmux panes/windows
475
+ tmux_command_delay: #{config['workflow']['tmux_command_delay']}
476
+
477
+ # Phase labels for tracking issue progress
478
+ phase_labels:
479
+ todo: #{config['workflow']['phase_labels']['todo']}
480
+ queued: #{config['workflow']['phase_labels']['queued']}
481
+ planning: #{config['workflow']['phase_labels']['planning']}
482
+ ready: #{config['workflow']['phase_labels']['ready']}
483
+ doing: #{config['workflow']['phase_labels']['doing']}
484
+ review_requested: #{config['workflow']['phase_labels']['review_requested']}
485
+ reviewing: #{config['workflow']['phase_labels']['reviewing']}
486
+ done: #{config['workflow']['phase_labels']['done']}
487
+ requires_changes: #{config['workflow']['phase_labels']['requires_changes']}
488
+ revising: #{config['workflow']['phase_labels']['revising']}
489
+ merged: #{config['workflow']['phase_labels']['merged']}
490
+
491
+ # Slack notification configuration
492
+ slack:
493
+ # Slack Webhook URL for notifications
494
+ # Can use environment variable: ${SLACK_WEBHOOK_URL}
495
+ webhook_url: #{config['slack']['webhook_url']}
496
+
497
+ # Enable Slack notifications for phase starts
498
+ notifications_enabled: #{config['slack']['notifications_enabled']}
499
+ YAML
500
+
501
+ # Add phase configuration if present
502
+ if config['phase']
503
+ phase_content = "\n # Phase command configuration\n phase:\n"
504
+
505
+ if config['phase']['plan']
506
+ phase_content += " plan:\n"
507
+ phase_content += " command: #{config['phase']['plan']['command']}\n"
508
+ if config['phase']['plan']['options'].present?
509
+ phase_content += " options:\n"
510
+ config['phase']['plan']['options'].each do |opt|
511
+ phase_content += " - #{opt}\n"
512
+ end
513
+ end
514
+ if config['phase']['plan']['parameter']
515
+ phase_content += " parameter: '#{config['phase']['plan']['parameter']}'\n"
516
+ end
517
+ end
518
+
519
+ if config['phase']['implement']
520
+ phase_content += " implement:\n"
521
+ phase_content += " command: #{config['phase']['implement']['command']}\n"
522
+ if config['phase']['implement']['options'].present?
523
+ phase_content += " options:\n"
524
+ config['phase']['implement']['options'].each do |opt|
525
+ phase_content += " - #{opt}\n"
526
+ end
527
+ end
528
+ if config['phase']['implement']['parameter']
529
+ phase_content += " parameter: '#{config['phase']['implement']['parameter']}'\n"
530
+ end
531
+ end
532
+
533
+ if config['phase']['review']
534
+ phase_content += " review:\n"
535
+ phase_content += " command: #{config['phase']['review']['command']}\n"
536
+ if config['phase']['review']['options'].present?
537
+ phase_content += " options:\n"
538
+ config['phase']['review']['options'].each do |opt|
539
+ phase_content += " - #{opt}\n"
540
+ end
541
+ end
542
+ if config['phase']['review']['parameter']
543
+ phase_content += " parameter: '#{config['phase']['review']['parameter']}'\n"
544
+ end
545
+ end
546
+
547
+ if config['phase']['revise']
548
+ phase_content += " revise:\n"
549
+ phase_content += " command: #{config['phase']['revise']['command']}\n"
550
+ if config['phase']['revise']['options'].present?
551
+ phase_content += " options:\n"
552
+ config['phase']['revise']['options'].each do |opt|
553
+ phase_content += " - #{opt}\n"
554
+ end
555
+ end
556
+ if config['phase']['revise']['parameter']
557
+ phase_content += " parameter: '#{config['phase']['revise']['parameter']}'\n"
558
+ end
559
+ end
560
+
561
+ # Remove extra indentation to match YAML structure
562
+ phase_content = phase_content.gsub(/^ /, '')
563
+ config_content += phase_content
564
+ end
565
+
566
+ File.write(config_path, config_content)
567
+ end
568
+
569
+ def check_github_token(token: '${GITHUB_TOKEN}')
570
+ # Verify token if environment variable is used
571
+ if token == '${GITHUB_TOKEN}'
572
+ puts ""
573
+ if ENV['GITHUB_TOKEN']
574
+ puts "✅ GITHUB_TOKEN environment variable is set"
575
+ else
576
+ puts "⚠️ GITHUB_TOKEN environment variable is not set"
577
+ puts " Please set it before running soba commands:"
578
+ puts " export GITHUB_TOKEN='your-token-here'"
579
+ end
580
+ end
581
+ end
582
+
583
+ def check_slack_webhook_url(config:)
584
+ # Verify Slack webhook URL if environment variable is used and notifications are enabled
585
+ if config['slack'] && config['slack']['notifications_enabled']
586
+ webhook_url = config['slack']['webhook_url']
587
+ if webhook_url == '${SLACK_WEBHOOK_URL}'
588
+ puts ""
589
+ if ENV['SLACK_WEBHOOK_URL']
590
+ puts "✅ SLACK_WEBHOOK_URL environment variable is set"
591
+ else
592
+ puts "⚠️ SLACK_WEBHOOK_URL environment variable is not set"
593
+ puts " Slack notifications are enabled but webhook URL is missing."
594
+ puts " Please set it before running soba commands:"
595
+ puts " export SLACK_WEBHOOK_URL='your-webhook-url-here'"
596
+ end
597
+ end
598
+ end
599
+ end
600
+
601
+ def handle_gitignore
602
+ # Add .soba to .gitignore if needed
603
+ gitignore_path = Pathname.pwd.join('.gitignore')
604
+ if gitignore_path.exist?
605
+ gitignore_content = File.read(gitignore_path)
606
+ unless gitignore_content.include?('.soba')
607
+ puts ""
608
+ print "Add .soba/ to .gitignore? (Y/n): "
609
+ response = $stdin.gets.chomp.downcase
610
+ if response != 'n' && response != 'no'
611
+ File.open(gitignore_path, 'a') do |f|
612
+ f.puts "" unless gitignore_content.end_with?("\n")
613
+ f.puts "# soba configuration directory"
614
+ f.puts ".soba/"
615
+ end
616
+ puts "✅ Added .soba/ to .gitignore"
617
+ end
618
+ end
619
+ end
620
+ end
621
+
622
+ def deploy_claude_templates
623
+ # Deploy Claude command templates to .claude/commands/soba/
624
+ claude_dir = Pathname.pwd.join('.claude', 'commands', 'soba')
625
+ template_dir = Pathname.new(__dir__).join('..', 'templates', 'claude_commands')
626
+
627
+ # Create directory if it doesn't exist
628
+ claude_dir.mkpath
629
+
630
+ # List of template files to deploy
631
+ template_files = ['plan.md', 'implement.md', 'review.md', 'revise.md']
632
+
633
+ template_files.each do |filename|
634
+ source_file = template_dir.join(filename)
635
+ target_file = claude_dir.join(filename)
636
+
637
+ # Check if file already exists
638
+ if target_file.exist?
639
+ if @interactive
640
+ puts ""
641
+ puts "⚠️ Claude command template already exists: #{target_file.relative_path_from(Pathname.pwd)}"
642
+ print "Do you want to overwrite it? (y/N): "
643
+ response = $stdin.gets
644
+ next unless response # Skip if no response
645
+ response = response.chomp.downcase
646
+ if response != 'y' && response != 'yes'
647
+ puts "✅ Keeping existing template: #{filename}"
648
+ next
649
+ else
650
+ puts "✅ Overwriting template: #{filename}"
651
+ end
652
+ end
653
+ end
654
+
655
+ # Copy the template file
656
+ FileUtils.cp(source_file, target_file)
657
+ Soba.logger.debug "Deployed Claude template: #{filename}"
658
+ end
659
+
660
+ puts ""
661
+ puts "✅ Claude command templates deployed to .claude/commands/soba/"
662
+ end
663
+
664
+ def detect_github_repository
665
+ return nil unless Dir.exist?('.git')
666
+
667
+ # Try to get remote origin URL
668
+ remote_url = `git config --get remote.origin.url 2>/dev/null`.chomp
669
+ return nil if remote_url.empty?
670
+
671
+ # Parse GitHub repository from various URL formats
672
+ # https://github.com/owner/repo.git
673
+ # git@github.com:owner/repo.git
674
+ # ssh://git@github.com/owner/repo.git
675
+ case remote_url
676
+ when %r{github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$}
677
+ "#{Regexp.last_match(1)}/#{Regexp.last_match(2)}"
678
+ when %r{^git@github\.com:([^/]+)/([^/]+?)(?:\.git)?$}
679
+ "#{Regexp.last_match(1)}/#{Regexp.last_match(2)}"
680
+ else
681
+ nil
682
+ end
683
+ rescue StandardError
684
+ nil
685
+ end
686
+
687
+ def create_github_labels(config)
688
+ repository = config['github']['repository']
689
+ phase_labels = config['workflow']['phase_labels']
690
+
691
+ # Only ask for confirmation in interactive mode
692
+ if @interactive
693
+ puts ""
694
+ print "Create GitHub labels for workflow phases? (Y/n): "
695
+ response = $stdin.gets
696
+ return unless response
697
+ response = response.chomp.downcase
698
+ if response == 'n' || response == 'no'
699
+ puts "✅ Skipping label creation."
700
+ return
701
+ end
702
+ end
703
+
704
+ puts ""
705
+ puts "🏷️ Creating GitHub labels..."
706
+
707
+ begin
708
+ # Initialize GitHub client
709
+ github_client = Infrastructure::GitHubClient.new
710
+
711
+ # Get existing labels
712
+ existing_labels = github_client.list_labels(repository)
713
+ existing_label_names = existing_labels.map { |label| label[:name] }
714
+
715
+ # Create labels for each phase
716
+ created_count = 0
717
+ skipped_count = 0
718
+
719
+ # Create phase labels
720
+ phase_labels.each do |phase, label_name|
721
+ if existing_label_names.include?(label_name)
722
+ Soba.logger.debug "Label '#{label_name}' already exists, skipping"
723
+ skipped_count += 1
724
+ else
725
+ color = LABEL_COLORS[phase]
726
+ description = LABEL_DESCRIPTIONS[phase]
727
+
728
+ result = github_client.create_label(repository, label_name, color, description)
729
+ if result
730
+ Soba.logger.info "Label '#{label_name}' created"
731
+ created_count += 1
732
+ else
733
+ Soba.logger.warn "Label '#{label_name}' could not be created (may already exist)"
734
+ skipped_count += 1
735
+ end
736
+ end
737
+ end
738
+
739
+ # Create additional PR label
740
+ additional_labels = [
741
+ { name: 'soba:lgtm', phase: 'lgtm' },
742
+ ]
743
+
744
+ additional_labels.each do |label_info|
745
+ label_name = label_info[:name]
746
+ if existing_label_names.include?(label_name)
747
+ Soba.logger.debug "Label '#{label_name}' already exists, skipping"
748
+ skipped_count += 1
749
+ else
750
+ color = LABEL_COLORS[label_info[:phase]]
751
+ description = LABEL_DESCRIPTIONS[label_info[:phase]]
752
+
753
+ result = github_client.create_label(repository, label_name, color, description)
754
+ if result
755
+ Soba.logger.info "Label '#{label_name}' created"
756
+ created_count += 1
757
+ else
758
+ Soba.logger.warn "Label '#{label_name}' could not be created (may already exist)"
759
+ skipped_count += 1
760
+ end
761
+ end
762
+ end
763
+
764
+ puts ""
765
+ puts "✅ Label creation complete: #{created_count} created, #{skipped_count} skipped"
766
+ rescue Infrastructure::AuthenticationError => e
767
+ Soba.logger.error "Authentication failed: #{e.message}"
768
+ Soba.logger.error "Please ensure your GitHub token has 'repo' permission"
769
+ rescue Infrastructure::GitHubClientError => e
770
+ Soba.logger.error "Failed to create labels: #{e.message}"
771
+ Soba.logger.error "Please check your repository permissions"
772
+ rescue StandardError => e
773
+ Soba.logger.error "Unexpected error: #{e.message}"
774
+ end
775
+ end
776
+ end
777
+ end
778
+ end