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.
- package/CHANGELOG.md +10 -0
- package/README.md +6 -6
- package/lib/README.md +178 -0
- package/lib/codebase-indexer.js +32 -23
- package/lib/colors.js +190 -12
- package/lib/consent.js +232 -0
- package/lib/correlation.js +277 -0
- package/lib/error-codes.js +46 -0
- package/lib/errors.js +48 -6
- package/lib/file-cache.js +182 -0
- package/lib/format-error.js +156 -0
- package/lib/path-resolver.js +155 -7
- package/lib/paths.js +212 -20
- package/lib/placeholder-registry.js +205 -0
- package/lib/registry-di.js +358 -0
- package/lib/result-schema.js +363 -0
- package/lib/result.js +210 -0
- package/lib/session-registry.js +13 -0
- package/lib/session-state-machine.js +465 -0
- package/lib/validate-commands.js +308 -0
- package/lib/validate.js +116 -52
- package/package.json +1 -1
- package/scripts/af +34 -0
- package/scripts/agent-loop.js +63 -9
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +491 -23
- package/scripts/archive-completed-stories.sh +57 -11
- package/scripts/claude-tmux.sh +102 -0
- package/scripts/damage-control-bash.js +3 -70
- package/scripts/damage-control-edit.js +3 -20
- package/scripts/damage-control-write.js +3 -20
- package/scripts/dependency-check.js +310 -0
- package/scripts/get-env.js +11 -4
- package/scripts/lib/configure-detect.js +23 -1
- package/scripts/lib/configure-features.js +50 -2
- package/scripts/lib/context-formatter.js +771 -0
- package/scripts/lib/context-loader.js +699 -0
- package/scripts/lib/damage-control-utils.js +107 -0
- package/scripts/lib/json-utils.sh +162 -0
- package/scripts/lib/state-migrator.js +353 -0
- package/scripts/lib/story-state-machine.js +437 -0
- package/scripts/obtain-context.js +80 -1248
- package/scripts/pre-push-check.sh +46 -0
- package/scripts/precompact-context.sh +23 -10
- package/scripts/query-codebase.js +127 -14
- package/scripts/ralph-loop.js +5 -5
- package/scripts/session-manager.js +408 -55
- package/scripts/spawn-parallel.js +666 -0
- package/scripts/tui/blessed/data/watcher.js +20 -15
- package/scripts/tui/blessed/index.js +2 -2
- package/scripts/tui/blessed/panels/output.js +14 -8
- package/scripts/tui/blessed/panels/sessions.js +22 -15
- package/scripts/tui/blessed/panels/trace.js +14 -8
- package/scripts/tui/blessed/ui/help.js +3 -3
- package/scripts/tui/blessed/ui/screen.js +4 -4
- package/scripts/tui/blessed/ui/statusbar.js +5 -9
- package/scripts/tui/blessed/ui/tabbar.js +11 -11
- package/scripts/validators/component-validator.js +41 -14
- package/scripts/validators/json-schema-validator.js +11 -4
- package/scripts/validators/markdown-validator.js +1 -2
- package/scripts/validators/migration-validator.js +17 -5
- package/scripts/validators/security-validator.js +137 -33
- package/scripts/validators/story-format-validator.js +31 -10
- package/scripts/validators/test-result-validator.js +19 -4
- package/scripts/validators/workflow-validator.js +12 -5
- package/src/core/agents/codebase-query.md +24 -0
- package/src/core/commands/adr.md +114 -0
- package/src/core/commands/agent.md +120 -0
- package/src/core/commands/assign.md +145 -0
- package/src/core/commands/babysit.md +32 -5
- package/src/core/commands/changelog.md +118 -0
- package/src/core/commands/configure.md +42 -6
- package/src/core/commands/diagnose.md +114 -0
- package/src/core/commands/epic.md +113 -0
- package/src/core/commands/handoff.md +128 -0
- package/src/core/commands/help.md +75 -0
- package/src/core/commands/pr.md +96 -0
- package/src/core/commands/roadmap/analyze.md +400 -0
- package/src/core/commands/session/new.md +132 -6
- package/src/core/commands/session/spawn.md +197 -0
- package/src/core/commands/sprint.md +22 -0
- package/src/core/commands/status.md +74 -0
- package/src/core/commands/story.md +143 -4
- package/src/core/templates/agileflow-metadata.json +55 -2
- package/src/core/templates/plan-template.md +125 -0
- package/src/core/templates/story-lifecycle.md +213 -0
- package/src/core/templates/story-template.md +4 -0
- package/src/core/templates/tdd-test-template.js +241 -0
- package/tools/cli/commands/setup.js +95 -0
- package/tools/cli/installers/core/installer.js +94 -0
- package/tools/cli/installers/ide/_base-ide.js +20 -11
- package/tools/cli/installers/ide/codex.js +29 -47
- package/tools/cli/installers/ide/windsurf.js +1 -1
- package/tools/cli/lib/config-manager.js +17 -2
- package/tools/cli/lib/content-transformer.js +271 -0
- package/tools/cli/lib/error-handler.js +14 -22
- package/tools/cli/lib/ide-error-factory.js +421 -0
- package/tools/cli/lib/ide-health-monitor.js +364 -0
- package/tools/cli/lib/ide-registry.js +113 -2
- 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
|
+
};
|