agileflow 2.91.0 → 2.92.1

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 (100) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -6
  3. package/lib/README.md +178 -0
  4. package/lib/codebase-indexer.js +32 -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 +491 -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 +50 -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 +127 -14
  46. package/scripts/ralph-loop.js +5 -5
  47. package/scripts/session-manager.js +408 -55
  48. package/scripts/spawn-parallel.js +666 -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 +132 -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 +95 -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/installers/ide/windsurf.js +1 -1
  94. package/tools/cli/lib/config-manager.js +17 -2
  95. package/tools/cli/lib/content-transformer.js +271 -0
  96. package/tools/cli/lib/error-handler.js +14 -22
  97. package/tools/cli/lib/ide-error-factory.js +421 -0
  98. package/tools/cli/lib/ide-health-monitor.js +364 -0
  99. package/tools/cli/lib/ide-registry.js +113 -2
  100. package/tools/cli/lib/ui.js +15 -25
@@ -0,0 +1,666 @@
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 = [...(result.envFilesCopied || []), ...(result.foldersCopied || [])];
300
+ const copyInfo = copied.length ? dim(` (copied: ${copied.join(', ')})`) : '';
301
+ console.log(success(` ✓ Session ${result.sessionId}: ${sessionSpec.nickname}${copyInfo}`));
302
+ }
303
+
304
+ if (createdSessions.length === 0) {
305
+ console.error(error('\nNo sessions were created.'));
306
+ process.exit(1);
307
+ }
308
+
309
+ console.log('');
310
+
311
+ // Spawn in tmux or output commands
312
+ if (noTmux) {
313
+ // User explicitly requested manual mode
314
+ outputCommands(createdSessions, { init, dangerous, prompt });
315
+ } else if (hasTmux()) {
316
+ // Tmux available - use it
317
+ const tmuxResult = spawnInTmux(createdSessions, { init, dangerous, prompt });
318
+
319
+ if (tmuxResult.success) {
320
+ console.log(success(`\n✅ Tmux session created: ${tmuxResult.sessionName}`));
321
+ console.log(`${c.cyan} ${tmuxResult.windowCount} windows ready${c.reset}\n`);
322
+ console.log(bold('📺 Controls:'));
323
+ console.log(` ${c.cyan}tmux attach -t ${tmuxResult.sessionName}${c.reset} - Attach`);
324
+ console.log(` ${dim('Alt+1/2/3')} Switch to window 1, 2, 3`);
325
+ console.log(` ${dim('q')} Quit (sessions keep running)`);
326
+ console.log('');
327
+ } else {
328
+ console.error(error(`Failed to create tmux session: ${tmuxResult.error}`));
329
+ outputCommands(createdSessions, { init, dangerous, prompt });
330
+ }
331
+ } else {
332
+ // Tmux NOT available - require it or use --no-tmux
333
+ console.log(error('\n❌ tmux is required but not installed.\n'));
334
+ console.log(bold('Install tmux:'));
335
+ console.log(` ${c.cyan}macOS:${c.reset} brew install tmux`);
336
+ console.log(` ${c.cyan}Ubuntu/Debian:${c.reset} sudo apt install tmux`);
337
+ console.log(` ${c.cyan}Fedora/RHEL:${c.reset} sudo dnf install tmux`);
338
+ console.log(` ${c.cyan}No sudo?${c.reset} conda install -c conda-forge tmux`);
339
+ console.log('');
340
+ console.log(dim('Or use --no-tmux to get manual commands instead:'));
341
+ console.log(
342
+ ` ${c.cyan}node spawn-parallel.js spawn --count ${createdSessions.length} --no-tmux${c.reset}`
343
+ );
344
+ console.log('');
345
+ console.log(
346
+ warning('Worktrees created but Claude not spawned. Install tmux or use --no-tmux.')
347
+ );
348
+ }
349
+
350
+ // Summary
351
+ console.log(bold('\n📊 Session Summary:'));
352
+ console.log(dim('─'.repeat(50)));
353
+ for (const session of createdSessions) {
354
+ console.log(
355
+ ` ${c.cyan}${session.sessionId}${c.reset} │ ${session.nickname} │ ${dim(session.branch)}`
356
+ );
357
+ }
358
+ console.log(dim('─'.repeat(50)));
359
+ console.log(`${c.cyan}Use /agileflow:session:status to view all sessions.${c.reset}`);
360
+ console.log(`${c.cyan}Use /agileflow:session:end <id> to end and merge a session.${c.reset}\n`);
361
+ }
362
+
363
+ /**
364
+ * List all parallel sessions
365
+ */
366
+ function list() {
367
+ const result = sessionManager.getSessions();
368
+
369
+ if (result.sessions.length === 0) {
370
+ console.log(`${c.cyan}No sessions registered.${c.reset}`);
371
+ return;
372
+ }
373
+
374
+ console.log(bold('\n📋 Parallel Sessions:\n'));
375
+
376
+ const parallelSessions = result.sessions.filter(s => !s.is_main);
377
+
378
+ if (parallelSessions.length === 0) {
379
+ console.log(`${c.cyan}No parallel sessions (only main session exists).${c.reset}`);
380
+ return;
381
+ }
382
+
383
+ for (const session of parallelSessions) {
384
+ const statusStr = session.active ? success('● active') : dim('○ inactive');
385
+ const nickname = session.nickname ? `${c.cyan}${session.nickname}${c.reset}` : dim('no-name');
386
+ console.log(` ${session.id} │ ${nickname} │ ${session.branch} │ ${statusStr}`);
387
+ }
388
+
389
+ console.log('');
390
+ }
391
+
392
+ /**
393
+ * Add a new window to an existing tmux session
394
+ */
395
+ function addWindow(args) {
396
+ const nickname = args.nickname || args.name || null;
397
+ const branch = args.branch || null;
398
+
399
+ // Check if we're inside a tmux session
400
+ const tmuxEnv = process.env.TMUX;
401
+ if (!tmuxEnv) {
402
+ console.log(error('\n❌ Not in a tmux session.\n'));
403
+ console.log(
404
+ `${c.cyan}Use /agileflow:session:spawn to create a new tmux session first.${c.reset}`
405
+ );
406
+ console.log(`${dim('Or run: node .agileflow/scripts/spawn-parallel.js spawn --count 1')}`);
407
+ return { success: false, error: 'Not in tmux' };
408
+ }
409
+
410
+ // Get current tmux session name
411
+ let currentSession;
412
+ try {
413
+ currentSession = execSync('tmux display-message -p "#S"', {
414
+ encoding: 'utf8',
415
+ stdio: ['pipe', 'pipe', 'pipe'],
416
+ }).trim();
417
+ } catch {
418
+ console.log(error('Failed to get current tmux session name.'));
419
+ return { success: false, error: 'Failed to get tmux session' };
420
+ }
421
+
422
+ console.log(bold(`\n🚀 Adding new window to tmux session: ${currentSession}\n`));
423
+
424
+ // Create a new session/worktree
425
+ const sessionSpec = {
426
+ nickname: nickname || `parallel-${Date.now()}`,
427
+ branch: branch || `parallel-${Date.now()}`,
428
+ };
429
+
430
+ const result = sessionManager.createSession({
431
+ nickname: sessionSpec.nickname,
432
+ branch: sessionSpec.branch,
433
+ });
434
+
435
+ if (!result.success) {
436
+ console.error(error(`Failed to create session: ${result.error}`));
437
+ return { success: false, error: result.error };
438
+ }
439
+
440
+ const windowName = sessionSpec.nickname;
441
+ const cmd = buildClaudeCommand(result.path, {});
442
+
443
+ // Create new window in current tmux session
444
+ const newWindowResult = spawnSync(
445
+ 'tmux',
446
+ ['new-window', '-t', currentSession, '-n', windowName],
447
+ {
448
+ encoding: 'utf8',
449
+ }
450
+ );
451
+
452
+ if (newWindowResult.status !== 0) {
453
+ console.error(error(`Failed to create tmux window: ${newWindowResult.stderr}`));
454
+ return { success: false, error: newWindowResult.stderr };
455
+ }
456
+
457
+ // Send command to the new window
458
+ spawnSync('tmux', ['send-keys', '-t', `${currentSession}:${windowName}`, cmd, 'Enter'], {
459
+ encoding: 'utf8',
460
+ });
461
+
462
+ // Get window number
463
+ let windowIndex;
464
+ try {
465
+ windowIndex = execSync(
466
+ `tmux list-windows -t ${currentSession} -F "#I:#W" | grep ":${windowName}$" | cut -d: -f1`,
467
+ {
468
+ encoding: 'utf8',
469
+ stdio: ['pipe', 'pipe', 'pipe'],
470
+ }
471
+ ).trim();
472
+ } catch {
473
+ windowIndex = '?';
474
+ }
475
+
476
+ // Show what was copied
477
+ const copied = [...(result.envFilesCopied || []), ...(result.foldersCopied || [])];
478
+ const copyInfo = copied.length ? dim(` (copied: ${copied.join(', ')})`) : '';
479
+
480
+ console.log(success(` ✓ Created session ${result.sessionId}: ${windowName}${copyInfo}`));
481
+ console.log(` ${dim('Path:')} ${result.path}`);
482
+ console.log(` ${dim('Branch:')} ${result.branch}`);
483
+ console.log('');
484
+ console.log(success(`✅ Added window [${windowIndex}] "${windowName}" to tmux session`));
485
+ console.log(`\n${c.cyan}Press Alt+${windowIndex} to switch to the new window${c.reset}`);
486
+ console.log(`${dim('Or use Ctrl+b then the window number')}\n`);
487
+
488
+ return {
489
+ success: true,
490
+ sessionId: result.sessionId,
491
+ windowName,
492
+ windowIndex,
493
+ path: result.path,
494
+ branch: result.branch,
495
+ };
496
+ }
497
+
498
+ /**
499
+ * Kill all tmux claude-parallel sessions
500
+ */
501
+ function killAll() {
502
+ try {
503
+ const result = execSync('tmux list-sessions -F "#{session_name}"', {
504
+ encoding: 'utf8',
505
+ stdio: ['pipe', 'pipe', 'pipe'],
506
+ });
507
+
508
+ const sessions = result
509
+ .trim()
510
+ .split('\n')
511
+ .filter(s => s.startsWith('claude-parallel-'));
512
+
513
+ if (sessions.length === 0) {
514
+ console.log(`${c.cyan}No claude-parallel tmux sessions found.${c.reset}`);
515
+ return;
516
+ }
517
+
518
+ for (const session of sessions) {
519
+ spawnSync('tmux', ['kill-session', '-t', session], { encoding: 'utf8' });
520
+ console.log(success(` ✓ Killed ${session}`));
521
+ }
522
+
523
+ console.log(success(`\n✅ Killed ${sessions.length} tmux session(s).`));
524
+ } catch (e) {
525
+ if (e.message.includes('no server running')) {
526
+ console.log(`${c.cyan}No tmux server running.${c.reset}`);
527
+ } else {
528
+ console.error(error(`Error: ${e.message}`));
529
+ }
530
+ }
531
+ }
532
+
533
+ /**
534
+ * Show help
535
+ */
536
+ function showHelp() {
537
+ console.log(`
538
+ ${bold('spawn-parallel.js')} - Spawn parallel Claude Code sessions in git worktrees
539
+
540
+ ${c.cyan}USAGE:${c.reset}
541
+ node scripts/spawn-parallel.js <command> [options]
542
+
543
+ ${c.cyan}COMMANDS:${c.reset}
544
+ spawn Create worktrees and optionally spawn Claude instances
545
+ add-window Add a new window to current tmux session (when in tmux)
546
+ list List all parallel sessions
547
+ kill-all Kill all claude-parallel tmux sessions
548
+
549
+ ${c.cyan}SPAWN OPTIONS:${c.reset}
550
+ --count N Create N worktrees with auto-generated names
551
+ --branches "a,b,c" Create worktrees for specific branch names
552
+ --from-epic EP-XXX Create worktrees for ready stories in epic
553
+ --init Run 'claude init' in each worktree
554
+ --dangerous Use --dangerouslySkipPermissions
555
+ --no-tmux Output commands without spawning in tmux
556
+ --prompt "TEXT" Initial prompt to send to each Claude instance
557
+
558
+ ${c.cyan}EXAMPLES:${c.reset}
559
+ ${dim('# Create 4 parallel sessions')}
560
+ node scripts/spawn-parallel.js spawn --count 4
561
+
562
+ ${dim('# Create sessions for specific features')}
563
+ node scripts/spawn-parallel.js spawn --branches "auth,dashboard,api"
564
+
565
+ ${dim('# Create sessions from epic stories')}
566
+ node scripts/spawn-parallel.js spawn --from-epic EP-0025
567
+
568
+ ${dim('# Create with claude init')}
569
+ node scripts/spawn-parallel.js spawn --count 2 --init
570
+
571
+ ${dim('# Just output commands (no tmux)')}
572
+ node scripts/spawn-parallel.js spawn --count 4 --no-tmux
573
+
574
+ ${c.cyan}ADD-WINDOW OPTIONS:${c.reset}
575
+ --name NAME Name for the new session/window
576
+ --nickname NAME Alias for --name
577
+ --branch BRANCH Use specific branch name
578
+
579
+ ${c.cyan}ADD-WINDOW EXAMPLES:${c.reset}
580
+ ${dim('# Add window with auto-generated name (when in tmux)')}
581
+ node scripts/spawn-parallel.js add-window
582
+
583
+ ${dim('# Add named window')}
584
+ node scripts/spawn-parallel.js add-window --name auth
585
+ `);
586
+ }
587
+
588
+ /**
589
+ * Parse command line arguments
590
+ */
591
+ function parseArgs(argv) {
592
+ const args = {};
593
+ let command = null;
594
+
595
+ for (let i = 0; i < argv.length; i++) {
596
+ const arg = argv[i];
597
+
598
+ if (!arg.startsWith('-') && !command) {
599
+ command = arg;
600
+ } else if (arg.startsWith('--')) {
601
+ const key = arg.slice(2);
602
+ const nextArg = argv[i + 1];
603
+
604
+ if (nextArg && !nextArg.startsWith('-')) {
605
+ args[key] = nextArg;
606
+ i++;
607
+ } else {
608
+ args[key] = true;
609
+ }
610
+ }
611
+ }
612
+
613
+ return { command, args };
614
+ }
615
+
616
+ /**
617
+ * Main entry point
618
+ */
619
+ function main() {
620
+ const { command, args } = parseArgs(process.argv.slice(2));
621
+
622
+ switch (command) {
623
+ case 'spawn':
624
+ spawn(args);
625
+ break;
626
+ case 'add-window':
627
+ case 'add':
628
+ addWindow(args);
629
+ break;
630
+ case 'list':
631
+ list();
632
+ break;
633
+ case 'kill-all':
634
+ killAll();
635
+ break;
636
+ case 'help':
637
+ case '--help':
638
+ case '-h':
639
+ showHelp();
640
+ break;
641
+ default:
642
+ if (command) {
643
+ console.error(c.error(`Unknown command: ${command}`));
644
+ }
645
+ showHelp();
646
+ process.exit(command ? 1 : 0);
647
+ }
648
+ }
649
+
650
+ // Run if called directly
651
+ if (require.main === module) {
652
+ main();
653
+ }
654
+
655
+ // Export for testing
656
+ module.exports = {
657
+ spawn,
658
+ addWindow,
659
+ list,
660
+ killAll,
661
+ buildClaudeCommand,
662
+ spawnInTmux,
663
+ getReadyStoriesFromEpic,
664
+ hasTmux,
665
+ hasScreen,
666
+ };