agileflow 2.91.0 → 2.92.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 (99) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +3 -3
  3. package/lib/README.md +178 -0
  4. package/lib/codebase-indexer.js +31 -23
  5. package/lib/colors.js +190 -12
  6. package/lib/consent.js +232 -0
  7. package/lib/correlation.js +277 -0
  8. package/lib/error-codes.js +46 -0
  9. package/lib/errors.js +48 -6
  10. package/lib/file-cache.js +182 -0
  11. package/lib/format-error.js +156 -0
  12. package/lib/path-resolver.js +155 -7
  13. package/lib/paths.js +212 -20
  14. package/lib/placeholder-registry.js +205 -0
  15. package/lib/registry-di.js +358 -0
  16. package/lib/result-schema.js +363 -0
  17. package/lib/result.js +210 -0
  18. package/lib/session-registry.js +13 -0
  19. package/lib/session-state-machine.js +465 -0
  20. package/lib/validate-commands.js +308 -0
  21. package/lib/validate.js +116 -52
  22. package/package.json +1 -1
  23. package/scripts/af +34 -0
  24. package/scripts/agent-loop.js +63 -9
  25. package/scripts/agileflow-configure.js +2 -2
  26. package/scripts/agileflow-welcome.js +435 -23
  27. package/scripts/archive-completed-stories.sh +57 -11
  28. package/scripts/claude-tmux.sh +102 -0
  29. package/scripts/damage-control-bash.js +3 -70
  30. package/scripts/damage-control-edit.js +3 -20
  31. package/scripts/damage-control-write.js +3 -20
  32. package/scripts/dependency-check.js +310 -0
  33. package/scripts/get-env.js +11 -4
  34. package/scripts/lib/configure-detect.js +23 -1
  35. package/scripts/lib/configure-features.js +43 -2
  36. package/scripts/lib/context-formatter.js +771 -0
  37. package/scripts/lib/context-loader.js +699 -0
  38. package/scripts/lib/damage-control-utils.js +107 -0
  39. package/scripts/lib/json-utils.sh +162 -0
  40. package/scripts/lib/state-migrator.js +353 -0
  41. package/scripts/lib/story-state-machine.js +437 -0
  42. package/scripts/obtain-context.js +80 -1248
  43. package/scripts/pre-push-check.sh +46 -0
  44. package/scripts/precompact-context.sh +23 -10
  45. package/scripts/query-codebase.js +122 -14
  46. package/scripts/ralph-loop.js +5 -5
  47. package/scripts/session-manager.js +220 -42
  48. package/scripts/spawn-parallel.js +651 -0
  49. package/scripts/tui/blessed/data/watcher.js +20 -15
  50. package/scripts/tui/blessed/index.js +2 -2
  51. package/scripts/tui/blessed/panels/output.js +14 -8
  52. package/scripts/tui/blessed/panels/sessions.js +22 -15
  53. package/scripts/tui/blessed/panels/trace.js +14 -8
  54. package/scripts/tui/blessed/ui/help.js +3 -3
  55. package/scripts/tui/blessed/ui/screen.js +4 -4
  56. package/scripts/tui/blessed/ui/statusbar.js +5 -9
  57. package/scripts/tui/blessed/ui/tabbar.js +11 -11
  58. package/scripts/validators/component-validator.js +41 -14
  59. package/scripts/validators/json-schema-validator.js +11 -4
  60. package/scripts/validators/markdown-validator.js +1 -2
  61. package/scripts/validators/migration-validator.js +17 -5
  62. package/scripts/validators/security-validator.js +137 -33
  63. package/scripts/validators/story-format-validator.js +31 -10
  64. package/scripts/validators/test-result-validator.js +19 -4
  65. package/scripts/validators/workflow-validator.js +12 -5
  66. package/src/core/agents/codebase-query.md +24 -0
  67. package/src/core/commands/adr.md +114 -0
  68. package/src/core/commands/agent.md +120 -0
  69. package/src/core/commands/assign.md +145 -0
  70. package/src/core/commands/babysit.md +32 -5
  71. package/src/core/commands/changelog.md +118 -0
  72. package/src/core/commands/configure.md +42 -6
  73. package/src/core/commands/diagnose.md +114 -0
  74. package/src/core/commands/epic.md +113 -0
  75. package/src/core/commands/handoff.md +128 -0
  76. package/src/core/commands/help.md +75 -0
  77. package/src/core/commands/pr.md +96 -0
  78. package/src/core/commands/roadmap/analyze.md +400 -0
  79. package/src/core/commands/session/new.md +113 -6
  80. package/src/core/commands/session/spawn.md +197 -0
  81. package/src/core/commands/sprint.md +22 -0
  82. package/src/core/commands/status.md +74 -0
  83. package/src/core/commands/story.md +143 -4
  84. package/src/core/templates/agileflow-metadata.json +55 -2
  85. package/src/core/templates/plan-template.md +125 -0
  86. package/src/core/templates/story-lifecycle.md +213 -0
  87. package/src/core/templates/story-template.md +4 -0
  88. package/src/core/templates/tdd-test-template.js +241 -0
  89. package/tools/cli/commands/setup.js +86 -0
  90. package/tools/cli/installers/core/installer.js +94 -0
  91. package/tools/cli/installers/ide/_base-ide.js +20 -11
  92. package/tools/cli/installers/ide/codex.js +29 -47
  93. package/tools/cli/lib/config-manager.js +17 -2
  94. package/tools/cli/lib/content-transformer.js +271 -0
  95. package/tools/cli/lib/error-handler.js +14 -22
  96. package/tools/cli/lib/ide-error-factory.js +421 -0
  97. package/tools/cli/lib/ide-health-monitor.js +364 -0
  98. package/tools/cli/lib/ide-registry.js +114 -1
  99. package/tools/cli/lib/ui.js +14 -25
@@ -0,0 +1,651 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * spawn-parallel.js - Spawn multiple parallel Claude Code sessions in git worktrees
4
+ *
5
+ * Creates worktrees using session-manager.js and optionally spawns Claude Code
6
+ * instances in a terminal multiplexer (tmux/screen).
7
+ *
8
+ * Usage:
9
+ * node scripts/spawn-parallel.js spawn --count 4
10
+ * node scripts/spawn-parallel.js spawn --branches "auth,dashboard,api"
11
+ * node scripts/spawn-parallel.js spawn --from-epic EP-0025
12
+ * node scripts/spawn-parallel.js list
13
+ * node scripts/spawn-parallel.js kill-all
14
+ *
15
+ * Options:
16
+ * --count N Create N worktrees with auto-generated names
17
+ * --branches "a,b" Create worktrees for specific branch names
18
+ * --from-epic ID Create worktrees for ready stories in epic
19
+ * --init Run 'claude init' in each worktree (default: false)
20
+ * --dangerous Use --dangerouslySkipPermissions (default: false)
21
+ * --no-tmux Just create worktrees, output commands without spawning
22
+ * --prompt TEXT Initial prompt to send to each Claude instance
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const { execSync, spawnSync } = require('child_process');
28
+
29
+ // Shared utilities
30
+ const { c, success, warning, error, dim, bold } = require('../lib/colors');
31
+ const { getProjectRoot, getStatusPath } = require('../lib/paths');
32
+ const { safeReadJSON } = require('../lib/errors');
33
+
34
+ // Import session manager functions
35
+ const sessionManager = require('./session-manager');
36
+
37
+ const ROOT = getProjectRoot();
38
+
39
+ /**
40
+ * Check if tmux is available
41
+ */
42
+ function hasTmux() {
43
+ try {
44
+ execSync('which tmux', { encoding: 'utf8', stdio: 'pipe' });
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Check if screen is available
53
+ */
54
+ function hasScreen() {
55
+ try {
56
+ execSync('which screen', { encoding: 'utf8', stdio: 'pipe' });
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Build the Claude command for a session
65
+ */
66
+ function buildClaudeCommand(sessionPath, options = {}) {
67
+ const { init = false, dangerous = false, prompt = null } = options;
68
+ const parts = [`cd "${sessionPath}"`];
69
+
70
+ if (init) {
71
+ parts.push('claude init --yes 2>/dev/null || true');
72
+ }
73
+
74
+ let claudeCmd = 'claude';
75
+ if (dangerous) {
76
+ claudeCmd = 'claude --dangerouslySkipPermissions';
77
+ }
78
+
79
+ if (prompt) {
80
+ // Escape the prompt for shell
81
+ const escapedPrompt = prompt.replace(/'/g, "'\\''");
82
+ parts.push(`echo '${escapedPrompt}' | ${claudeCmd}`);
83
+ } else {
84
+ parts.push(claudeCmd);
85
+ }
86
+
87
+ return parts.join(' && ');
88
+ }
89
+
90
+ /**
91
+ * Spawn sessions in tmux
92
+ */
93
+ function spawnInTmux(sessions, options = {}) {
94
+ const timestamp = Date.now();
95
+ const sessionName = `claude-parallel-${timestamp}`;
96
+
97
+ // Create new tmux session (detached)
98
+ const createResult = spawnSync('tmux', ['new-session', '-d', '-s', sessionName], {
99
+ encoding: 'utf8',
100
+ });
101
+
102
+ if (createResult.status !== 0) {
103
+ console.error(error(`Failed to create tmux session: ${createResult.stderr}`));
104
+ return { success: false, error: createResult.stderr };
105
+ }
106
+
107
+ // Configure clean, user-friendly tmux settings
108
+ const tmuxOpts = (opt, value) => {
109
+ spawnSync('tmux', ['set-option', '-t', sessionName, opt, value], { encoding: 'utf8' });
110
+ };
111
+
112
+ // Clean, minimal status bar
113
+ tmuxOpts('status', 'on');
114
+ tmuxOpts('status-position', 'bottom');
115
+ tmuxOpts('status-style', 'bg=#282c34,fg=#abb2bf');
116
+ tmuxOpts('status-left', '#[fg=#61afef,bold] Parallel ');
117
+ tmuxOpts('status-left-length', '15');
118
+ tmuxOpts('status-right', '#[fg=#98c379] Alt+1/2/3 to switch │ q=quit ');
119
+ tmuxOpts('status-right-length', '45');
120
+ tmuxOpts('window-status-format', '#[fg=#5c6370] [#I] #W ');
121
+ tmuxOpts('window-status-current-format', '#[fg=#61afef,bold,bg=#3e4452] [#I] #W ');
122
+ tmuxOpts('window-status-separator', '');
123
+
124
+ // Simple keybindings - Alt+number to switch windows
125
+ for (let w = 1; w <= 9; w++) {
126
+ spawnSync('tmux', ['bind-key', '-n', `M-${w}`, 'select-window', '-t', `:${w - 1}`], {
127
+ encoding: 'utf8',
128
+ });
129
+ }
130
+ spawnSync('tmux', ['bind-key', '-n', 'q', 'detach-client'], { encoding: 'utf8' });
131
+
132
+ for (let i = 0; i < sessions.length; i++) {
133
+ const session = sessions[i];
134
+ const cmd = buildClaudeCommand(session.path, options);
135
+ const windowName = session.nickname || `session-${session.sessionId}`;
136
+
137
+ if (i === 0) {
138
+ // First window already exists, just rename and send command
139
+ spawnSync('tmux', ['rename-window', '-t', `${sessionName}:0`, windowName], {
140
+ encoding: 'utf8',
141
+ });
142
+ spawnSync('tmux', ['send-keys', '-t', sessionName, cmd, 'Enter'], {
143
+ encoding: 'utf8',
144
+ });
145
+ } else {
146
+ // Create new window for subsequent sessions
147
+ spawnSync('tmux', ['new-window', '-t', sessionName, '-n', windowName], {
148
+ encoding: 'utf8',
149
+ });
150
+ spawnSync('tmux', ['send-keys', '-t', `${sessionName}:${windowName}`, cmd, 'Enter'], {
151
+ encoding: 'utf8',
152
+ });
153
+ }
154
+ }
155
+
156
+ return {
157
+ success: true,
158
+ sessionName,
159
+ windowCount: sessions.length,
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Output commands without spawning (for --no-tmux mode)
165
+ */
166
+ function outputCommands(sessions, options = {}) {
167
+ console.log(bold('\n📋 Commands to run manually:\n'));
168
+
169
+ for (const session of sessions) {
170
+ const cmd = buildClaudeCommand(session.path, options);
171
+ console.log(dim(`# Session ${session.sessionId} (${session.nickname || session.branch})`));
172
+ console.log(cmd);
173
+ console.log('');
174
+ }
175
+
176
+ console.log(dim('─'.repeat(50)));
177
+ console.log(`${c.cyan}Copy these commands to separate terminals to run in parallel.${c.reset}`);
178
+ }
179
+
180
+ /**
181
+ * Get ready stories from an epic
182
+ */
183
+ function getReadyStoriesFromEpic(epicId) {
184
+ const statusPath = getStatusPath(ROOT);
185
+ const result = safeReadJSON(statusPath, { defaultValue: { stories: {}, epics: {} } });
186
+
187
+ if (!result.ok) {
188
+ return { ok: false, error: 'Could not read status.json' };
189
+ }
190
+
191
+ const status = result.data;
192
+ const epic = status.epics?.[epicId];
193
+
194
+ if (!epic) {
195
+ return { ok: false, error: `Epic ${epicId} not found` };
196
+ }
197
+
198
+ const readyStories = [];
199
+ const storyIds = epic.stories || [];
200
+
201
+ for (const storyId of storyIds) {
202
+ const story = status.stories?.[storyId];
203
+ if (story && story.status === 'ready') {
204
+ readyStories.push({
205
+ id: storyId,
206
+ title: story.title,
207
+ owner: story.owner,
208
+ });
209
+ }
210
+ }
211
+
212
+ return { ok: true, stories: readyStories, epicTitle: epic.title };
213
+ }
214
+
215
+ /**
216
+ * Main spawn command
217
+ */
218
+ function spawn(args) {
219
+ const count = args.count ? parseInt(args.count, 10) : null;
220
+ const branches = args.branches ? args.branches.split(',').map(b => b.trim()) : null;
221
+ const fromEpic = args['from-epic'] || args.fromEpic;
222
+ const noTmux = args['no-tmux'] || args.noTmux;
223
+ const init = args.init || false;
224
+ const dangerous = args.dangerous || false;
225
+ const prompt = args.prompt || null;
226
+
227
+ // Determine what to create
228
+ let sessionsToCreate = [];
229
+
230
+ if (fromEpic) {
231
+ // Get ready stories from epic
232
+ const epicResult = getReadyStoriesFromEpic(fromEpic);
233
+ if (!epicResult.ok) {
234
+ console.error(error(epicResult.error));
235
+ process.exit(1);
236
+ }
237
+
238
+ if (epicResult.stories.length === 0) {
239
+ console.log(warning(`No ready stories found in epic ${fromEpic}`));
240
+ process.exit(0);
241
+ }
242
+
243
+ console.log(bold(`\n📋 Epic: ${epicResult.epicTitle}`));
244
+ console.log(`${c.cyan}Found ${epicResult.stories.length} ready stories${c.reset}\n`);
245
+
246
+ for (const story of epicResult.stories) {
247
+ sessionsToCreate.push({
248
+ nickname: story.id.toLowerCase(),
249
+ branch: `feature/${story.id.toLowerCase()}`,
250
+ story: story.id,
251
+ });
252
+ }
253
+ } else if (branches) {
254
+ // Create sessions for specific branches
255
+ for (const branch of branches) {
256
+ sessionsToCreate.push({
257
+ nickname: branch,
258
+ branch: `feature/${branch}`,
259
+ });
260
+ }
261
+ } else if (count) {
262
+ // Create N generic sessions
263
+ for (let i = 1; i <= count; i++) {
264
+ sessionsToCreate.push({
265
+ nickname: `parallel-${i}`,
266
+ branch: `parallel-${i}`,
267
+ });
268
+ }
269
+ } else {
270
+ console.error(error('Must specify --count, --branches, or --from-epic'));
271
+ process.exit(1);
272
+ }
273
+
274
+ // Create the sessions
275
+ console.log(bold(`\n🚀 Creating ${sessionsToCreate.length} parallel sessions...\n`));
276
+
277
+ const createdSessions = [];
278
+ for (const sessionSpec of sessionsToCreate) {
279
+ const result = sessionManager.createSession({
280
+ nickname: sessionSpec.nickname,
281
+ branch: sessionSpec.branch,
282
+ });
283
+
284
+ if (!result.success) {
285
+ console.error(error(`Failed to create session ${sessionSpec.nickname}: ${result.error}`));
286
+ continue;
287
+ }
288
+
289
+ createdSessions.push({
290
+ sessionId: result.sessionId,
291
+ path: result.path,
292
+ branch: result.branch,
293
+ nickname: sessionSpec.nickname,
294
+ envFilesCopied: result.envFilesCopied || [],
295
+ foldersCopied: result.foldersCopied || [],
296
+ });
297
+
298
+ // Show what was copied
299
+ const copied = [
300
+ ...(result.envFilesCopied || []),
301
+ ...(result.foldersCopied || []),
302
+ ];
303
+ const copyInfo = copied.length ? dim(` (copied: ${copied.join(', ')})`) : '';
304
+ console.log(success(` ✓ Session ${result.sessionId}: ${sessionSpec.nickname}${copyInfo}`));
305
+ }
306
+
307
+ if (createdSessions.length === 0) {
308
+ console.error(error('\nNo sessions were created.'));
309
+ process.exit(1);
310
+ }
311
+
312
+ console.log('');
313
+
314
+ // Spawn in tmux or output commands
315
+ if (noTmux) {
316
+ // User explicitly requested manual mode
317
+ outputCommands(createdSessions, { init, dangerous, prompt });
318
+ } else if (hasTmux()) {
319
+ // Tmux available - use it
320
+ const tmuxResult = spawnInTmux(createdSessions, { init, dangerous, prompt });
321
+
322
+ if (tmuxResult.success) {
323
+ console.log(success(`\n✅ Tmux session created: ${tmuxResult.sessionName}`));
324
+ console.log(`${c.cyan} ${tmuxResult.windowCount} windows ready${c.reset}\n`);
325
+ console.log(bold('📺 Controls:'));
326
+ console.log(` ${c.cyan}tmux attach -t ${tmuxResult.sessionName}${c.reset} - Attach`);
327
+ console.log(` ${dim('Alt+1/2/3')} Switch to window 1, 2, 3`);
328
+ console.log(` ${dim('q')} Quit (sessions keep running)`);
329
+ console.log('');
330
+ } else {
331
+ console.error(error(`Failed to create tmux session: ${tmuxResult.error}`));
332
+ outputCommands(createdSessions, { init, dangerous, prompt });
333
+ }
334
+ } else {
335
+ // Tmux NOT available - require it or use --no-tmux
336
+ console.log(error('\n❌ tmux is required but not installed.\n'));
337
+ console.log(bold('Install tmux:'));
338
+ console.log(` ${c.cyan}macOS:${c.reset} brew install tmux`);
339
+ console.log(` ${c.cyan}Ubuntu/Debian:${c.reset} sudo apt install tmux`);
340
+ console.log(` ${c.cyan}Fedora/RHEL:${c.reset} sudo dnf install tmux`);
341
+ console.log(` ${c.cyan}No sudo?${c.reset} conda install -c conda-forge tmux`);
342
+ console.log('');
343
+ console.log(dim('Or use --no-tmux to get manual commands instead:'));
344
+ console.log(` ${c.cyan}node spawn-parallel.js spawn --count ${createdSessions.length} --no-tmux${c.reset}`);
345
+ console.log('');
346
+ console.log(warning('Worktrees created but Claude not spawned. Install tmux or use --no-tmux.'));
347
+ }
348
+
349
+ // Summary
350
+ console.log(bold('\n📊 Session Summary:'));
351
+ console.log(dim('─'.repeat(50)));
352
+ for (const session of createdSessions) {
353
+ console.log(` ${c.cyan}${session.sessionId}${c.reset} │ ${session.nickname} │ ${dim(session.branch)}`);
354
+ }
355
+ console.log(dim('─'.repeat(50)));
356
+ console.log(`${c.cyan}Use /agileflow:session:status to view all sessions.${c.reset}`);
357
+ console.log(`${c.cyan}Use /agileflow:session:end <id> to end and merge a session.${c.reset}\n`);
358
+ }
359
+
360
+ /**
361
+ * List all parallel sessions
362
+ */
363
+ function list() {
364
+ const result = sessionManager.getSessions();
365
+
366
+ if (result.sessions.length === 0) {
367
+ console.log(`${c.cyan}No sessions registered.${c.reset}`);
368
+ return;
369
+ }
370
+
371
+ console.log(bold('\n📋 Parallel Sessions:\n'));
372
+
373
+ const parallelSessions = result.sessions.filter(s => !s.is_main);
374
+
375
+ if (parallelSessions.length === 0) {
376
+ console.log(`${c.cyan}No parallel sessions (only main session exists).${c.reset}`);
377
+ return;
378
+ }
379
+
380
+ for (const session of parallelSessions) {
381
+ const statusStr = session.active ? success('● active') : dim('○ inactive');
382
+ const nickname = session.nickname ? `${c.cyan}${session.nickname}${c.reset}` : dim('no-name');
383
+ console.log(` ${session.id} │ ${nickname} │ ${session.branch} │ ${statusStr}`);
384
+ }
385
+
386
+ console.log('');
387
+ }
388
+
389
+ /**
390
+ * Add a new window to an existing tmux session
391
+ */
392
+ function addWindow(args) {
393
+ const nickname = args.nickname || args.name || null;
394
+ const branch = args.branch || null;
395
+
396
+ // Check if we're inside a tmux session
397
+ const tmuxEnv = process.env.TMUX;
398
+ if (!tmuxEnv) {
399
+ console.log(error('\n❌ Not in a tmux session.\n'));
400
+ console.log(`${c.cyan}Use /agileflow:session:spawn to create a new tmux session first.${c.reset}`);
401
+ console.log(`${dim('Or run: node .agileflow/scripts/spawn-parallel.js spawn --count 1')}`);
402
+ return { success: false, error: 'Not in tmux' };
403
+ }
404
+
405
+ // Get current tmux session name
406
+ let currentSession;
407
+ try {
408
+ currentSession = execSync('tmux display-message -p "#S"', {
409
+ encoding: 'utf8',
410
+ stdio: ['pipe', 'pipe', 'pipe'],
411
+ }).trim();
412
+ } catch {
413
+ console.log(error('Failed to get current tmux session name.'));
414
+ return { success: false, error: 'Failed to get tmux session' };
415
+ }
416
+
417
+ console.log(bold(`\n🚀 Adding new window to tmux session: ${currentSession}\n`));
418
+
419
+ // Create a new session/worktree
420
+ const sessionSpec = {
421
+ nickname: nickname || `parallel-${Date.now()}`,
422
+ branch: branch || `parallel-${Date.now()}`,
423
+ };
424
+
425
+ const result = sessionManager.createSession({
426
+ nickname: sessionSpec.nickname,
427
+ branch: sessionSpec.branch,
428
+ });
429
+
430
+ if (!result.success) {
431
+ console.error(error(`Failed to create session: ${result.error}`));
432
+ return { success: false, error: result.error };
433
+ }
434
+
435
+ const windowName = sessionSpec.nickname;
436
+ const cmd = buildClaudeCommand(result.path, {});
437
+
438
+ // Create new window in current tmux session
439
+ const newWindowResult = spawnSync('tmux', ['new-window', '-t', currentSession, '-n', windowName], {
440
+ encoding: 'utf8',
441
+ });
442
+
443
+ if (newWindowResult.status !== 0) {
444
+ console.error(error(`Failed to create tmux window: ${newWindowResult.stderr}`));
445
+ return { success: false, error: newWindowResult.stderr };
446
+ }
447
+
448
+ // Send command to the new window
449
+ spawnSync('tmux', ['send-keys', '-t', `${currentSession}:${windowName}`, cmd, 'Enter'], {
450
+ encoding: 'utf8',
451
+ });
452
+
453
+ // Get window number
454
+ let windowIndex;
455
+ try {
456
+ windowIndex = execSync(`tmux list-windows -t ${currentSession} -F "#I:#W" | grep ":${windowName}$" | cut -d: -f1`, {
457
+ encoding: 'utf8',
458
+ stdio: ['pipe', 'pipe', 'pipe'],
459
+ }).trim();
460
+ } catch {
461
+ windowIndex = '?';
462
+ }
463
+
464
+ // Show what was copied
465
+ const copied = [...(result.envFilesCopied || []), ...(result.foldersCopied || [])];
466
+ const copyInfo = copied.length ? dim(` (copied: ${copied.join(', ')})`) : '';
467
+
468
+ console.log(success(` ✓ Created session ${result.sessionId}: ${windowName}${copyInfo}`));
469
+ console.log(` ${dim('Path:')} ${result.path}`);
470
+ console.log(` ${dim('Branch:')} ${result.branch}`);
471
+ console.log('');
472
+ console.log(success(`✅ Added window [${windowIndex}] "${windowName}" to tmux session`));
473
+ console.log(`\n${c.cyan}Press Alt+${windowIndex} to switch to the new window${c.reset}`);
474
+ console.log(`${dim('Or use Ctrl+b then the window number')}\n`);
475
+
476
+ return {
477
+ success: true,
478
+ sessionId: result.sessionId,
479
+ windowName,
480
+ windowIndex,
481
+ path: result.path,
482
+ branch: result.branch,
483
+ };
484
+ }
485
+
486
+ /**
487
+ * Kill all tmux claude-parallel sessions
488
+ */
489
+ function killAll() {
490
+ try {
491
+ const result = execSync('tmux list-sessions -F "#{session_name}"', {
492
+ encoding: 'utf8',
493
+ stdio: ['pipe', 'pipe', 'pipe'],
494
+ });
495
+
496
+ const sessions = result.trim().split('\n').filter(s => s.startsWith('claude-parallel-'));
497
+
498
+ if (sessions.length === 0) {
499
+ console.log(`${c.cyan}No claude-parallel tmux sessions found.${c.reset}`);
500
+ return;
501
+ }
502
+
503
+ for (const session of sessions) {
504
+ spawnSync('tmux', ['kill-session', '-t', session], { encoding: 'utf8' });
505
+ console.log(success(` ✓ Killed ${session}`));
506
+ }
507
+
508
+ console.log(success(`\n✅ Killed ${sessions.length} tmux session(s).`));
509
+ } catch (e) {
510
+ if (e.message.includes('no server running')) {
511
+ console.log(`${c.cyan}No tmux server running.${c.reset}`);
512
+ } else {
513
+ console.error(error(`Error: ${e.message}`));
514
+ }
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Show help
520
+ */
521
+ function showHelp() {
522
+ console.log(`
523
+ ${bold('spawn-parallel.js')} - Spawn parallel Claude Code sessions in git worktrees
524
+
525
+ ${c.cyan}USAGE:${c.reset}
526
+ node scripts/spawn-parallel.js <command> [options]
527
+
528
+ ${c.cyan}COMMANDS:${c.reset}
529
+ spawn Create worktrees and optionally spawn Claude instances
530
+ add-window Add a new window to current tmux session (when in tmux)
531
+ list List all parallel sessions
532
+ kill-all Kill all claude-parallel tmux sessions
533
+
534
+ ${c.cyan}SPAWN OPTIONS:${c.reset}
535
+ --count N Create N worktrees with auto-generated names
536
+ --branches "a,b,c" Create worktrees for specific branch names
537
+ --from-epic EP-XXX Create worktrees for ready stories in epic
538
+ --init Run 'claude init' in each worktree
539
+ --dangerous Use --dangerouslySkipPermissions
540
+ --no-tmux Output commands without spawning in tmux
541
+ --prompt "TEXT" Initial prompt to send to each Claude instance
542
+
543
+ ${c.cyan}EXAMPLES:${c.reset}
544
+ ${dim('# Create 4 parallel sessions')}
545
+ node scripts/spawn-parallel.js spawn --count 4
546
+
547
+ ${dim('# Create sessions for specific features')}
548
+ node scripts/spawn-parallel.js spawn --branches "auth,dashboard,api"
549
+
550
+ ${dim('# Create sessions from epic stories')}
551
+ node scripts/spawn-parallel.js spawn --from-epic EP-0025
552
+
553
+ ${dim('# Create with claude init')}
554
+ node scripts/spawn-parallel.js spawn --count 2 --init
555
+
556
+ ${dim('# Just output commands (no tmux)')}
557
+ node scripts/spawn-parallel.js spawn --count 4 --no-tmux
558
+
559
+ ${c.cyan}ADD-WINDOW OPTIONS:${c.reset}
560
+ --name NAME Name for the new session/window
561
+ --nickname NAME Alias for --name
562
+ --branch BRANCH Use specific branch name
563
+
564
+ ${c.cyan}ADD-WINDOW EXAMPLES:${c.reset}
565
+ ${dim('# Add window with auto-generated name (when in tmux)')}
566
+ node scripts/spawn-parallel.js add-window
567
+
568
+ ${dim('# Add named window')}
569
+ node scripts/spawn-parallel.js add-window --name auth
570
+ `);
571
+ }
572
+
573
+ /**
574
+ * Parse command line arguments
575
+ */
576
+ function parseArgs(argv) {
577
+ const args = {};
578
+ let command = null;
579
+
580
+ for (let i = 0; i < argv.length; i++) {
581
+ const arg = argv[i];
582
+
583
+ if (!arg.startsWith('-') && !command) {
584
+ command = arg;
585
+ } else if (arg.startsWith('--')) {
586
+ const key = arg.slice(2);
587
+ const nextArg = argv[i + 1];
588
+
589
+ if (nextArg && !nextArg.startsWith('-')) {
590
+ args[key] = nextArg;
591
+ i++;
592
+ } else {
593
+ args[key] = true;
594
+ }
595
+ }
596
+ }
597
+
598
+ return { command, args };
599
+ }
600
+
601
+ /**
602
+ * Main entry point
603
+ */
604
+ function main() {
605
+ const { command, args } = parseArgs(process.argv.slice(2));
606
+
607
+ switch (command) {
608
+ case 'spawn':
609
+ spawn(args);
610
+ break;
611
+ case 'add-window':
612
+ case 'add':
613
+ addWindow(args);
614
+ break;
615
+ case 'list':
616
+ list();
617
+ break;
618
+ case 'kill-all':
619
+ killAll();
620
+ break;
621
+ case 'help':
622
+ case '--help':
623
+ case '-h':
624
+ showHelp();
625
+ break;
626
+ default:
627
+ if (command) {
628
+ console.error(c.error(`Unknown command: ${command}`));
629
+ }
630
+ showHelp();
631
+ process.exit(command ? 1 : 0);
632
+ }
633
+ }
634
+
635
+ // Run if called directly
636
+ if (require.main === module) {
637
+ main();
638
+ }
639
+
640
+ // Export for testing
641
+ module.exports = {
642
+ spawn,
643
+ addWindow,
644
+ list,
645
+ killAll,
646
+ buildClaudeCommand,
647
+ spawnInTmux,
648
+ getReadyStoriesFromEpic,
649
+ hasTmux,
650
+ hasScreen,
651
+ };