agileflow 2.51.0 → 2.56.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 (90) hide show
  1. package/README.md +80 -460
  2. package/package.json +18 -3
  3. package/scripts/agileflow-configure.js +134 -63
  4. package/scripts/agileflow-welcome.js +161 -31
  5. package/scripts/generators/agent-registry.js +45 -57
  6. package/scripts/generators/command-registry.js +48 -32
  7. package/scripts/generators/index.js +2 -6
  8. package/scripts/generators/inject-babysit.js +9 -2
  9. package/scripts/generators/inject-help.js +3 -1
  10. package/scripts/generators/inject-readme.js +7 -3
  11. package/scripts/generators/skill-registry.js +60 -33
  12. package/scripts/get-env.js +13 -12
  13. package/scripts/lib/frontmatter-parser.js +82 -0
  14. package/scripts/obtain-context.js +79 -26
  15. package/scripts/session-coordinator.sh +232 -0
  16. package/scripts/session-manager.js +512 -0
  17. package/src/core/agents/orchestrator.md +275 -0
  18. package/src/core/commands/adr.md +38 -16
  19. package/src/core/commands/agent.md +39 -22
  20. package/src/core/commands/assign.md +17 -0
  21. package/src/core/commands/auto.md +60 -46
  22. package/src/core/commands/babysit.md +302 -637
  23. package/src/core/commands/baseline.md +20 -0
  24. package/src/core/commands/blockers.md +33 -48
  25. package/src/core/commands/board.md +19 -0
  26. package/src/core/commands/changelog.md +20 -0
  27. package/src/core/commands/ci.md +17 -0
  28. package/src/core/commands/context.md +43 -40
  29. package/src/core/commands/debt.md +76 -45
  30. package/src/core/commands/deploy.md +20 -0
  31. package/src/core/commands/deps.md +40 -46
  32. package/src/core/commands/diagnose.md +24 -18
  33. package/src/core/commands/docs.md +18 -0
  34. package/src/core/commands/epic.md +31 -0
  35. package/src/core/commands/feedback.md +33 -21
  36. package/src/core/commands/handoff.md +29 -0
  37. package/src/core/commands/help.md +16 -7
  38. package/src/core/commands/impact.md +31 -61
  39. package/src/core/commands/metrics.md +17 -35
  40. package/src/core/commands/packages.md +21 -0
  41. package/src/core/commands/pr.md +15 -0
  42. package/src/core/commands/readme-sync.md +42 -9
  43. package/src/core/commands/research.md +58 -11
  44. package/src/core/commands/retro.md +42 -50
  45. package/src/core/commands/review.md +22 -27
  46. package/src/core/commands/session/end.md +53 -297
  47. package/src/core/commands/session/history.md +38 -257
  48. package/src/core/commands/session/init.md +44 -446
  49. package/src/core/commands/session/new.md +152 -0
  50. package/src/core/commands/session/resume.md +51 -447
  51. package/src/core/commands/session/status.md +32 -244
  52. package/src/core/commands/sprint.md +33 -0
  53. package/src/core/commands/status.md +18 -0
  54. package/src/core/commands/story-validate.md +32 -0
  55. package/src/core/commands/story.md +21 -6
  56. package/src/core/commands/template.md +18 -0
  57. package/src/core/commands/tests.md +22 -0
  58. package/src/core/commands/update.md +72 -58
  59. package/src/core/commands/validate-expertise.md +25 -37
  60. package/src/core/commands/velocity.md +33 -74
  61. package/src/core/commands/verify.md +16 -0
  62. package/src/core/experts/documentation/expertise.yaml +16 -2
  63. package/src/core/skills/agileflow-retro-facilitator/SKILL.md +57 -219
  64. package/src/core/skills/agileflow-retro-facilitator/cookbook/4ls.md +86 -0
  65. package/src/core/skills/agileflow-retro-facilitator/cookbook/glad-sad-mad.md +79 -0
  66. package/src/core/skills/agileflow-retro-facilitator/cookbook/start-stop-continue.md +142 -0
  67. package/src/core/skills/agileflow-retro-facilitator/prompts/action-items.md +83 -0
  68. package/src/core/skills/writing-skills/SKILL.md +352 -0
  69. package/src/core/skills/writing-skills/testing-skills-with-subagents.md +232 -0
  70. package/tools/cli/agileflow-cli.js +4 -2
  71. package/tools/cli/commands/config.js +20 -13
  72. package/tools/cli/commands/doctor.js +25 -9
  73. package/tools/cli/commands/list.js +10 -6
  74. package/tools/cli/commands/setup.js +54 -3
  75. package/tools/cli/commands/status.js +6 -8
  76. package/tools/cli/commands/uninstall.js +5 -5
  77. package/tools/cli/commands/update.js +51 -7
  78. package/tools/cli/installers/core/installer.js +8 -4
  79. package/tools/cli/installers/ide/_base-ide.js +58 -1
  80. package/tools/cli/installers/ide/claude-code.js +3 -61
  81. package/tools/cli/installers/ide/codex.js +440 -0
  82. package/tools/cli/installers/ide/cursor.js +21 -51
  83. package/tools/cli/installers/ide/manager.js +2 -6
  84. package/tools/cli/installers/ide/windsurf.js +20 -50
  85. package/tools/cli/lib/content-injector.js +26 -49
  86. package/tools/cli/lib/docs-setup.js +3 -2
  87. package/tools/cli/lib/npm-utils.js +39 -12
  88. package/tools/cli/lib/ui.js +31 -10
  89. package/tools/cli/lib/version-checker.js +3 -3
  90. package/tools/postinstall.js +2 -3
@@ -0,0 +1,512 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * session-manager.js - Multi-session coordination for Claude Code
4
+ *
5
+ * Manages parallel Claude Code sessions with:
6
+ * - Numbered session IDs (1, 2, 3...)
7
+ * - PID-based liveness detection
8
+ * - Git worktree automation
9
+ * - Registry persistence
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execSync, spawnSync } = require('child_process');
15
+
16
+ // ANSI colors
17
+ const c = {
18
+ reset: '\x1b[0m',
19
+ bold: '\x1b[1m',
20
+ dim: '\x1b[2m',
21
+ red: '\x1b[31m',
22
+ green: '\x1b[32m',
23
+ yellow: '\x1b[33m',
24
+ blue: '\x1b[34m',
25
+ cyan: '\x1b[36m',
26
+ brand: '\x1b[38;2;232;104;58m',
27
+ };
28
+
29
+ // Find project root (has .agileflow or .git)
30
+ function getProjectRoot() {
31
+ let dir = process.cwd();
32
+ while (dir !== '/') {
33
+ if (fs.existsSync(path.join(dir, '.agileflow')) || fs.existsSync(path.join(dir, '.git'))) {
34
+ return dir;
35
+ }
36
+ dir = path.dirname(dir);
37
+ }
38
+ return process.cwd();
39
+ }
40
+
41
+ const ROOT = getProjectRoot();
42
+ const SESSIONS_DIR = path.join(ROOT, '.agileflow', 'sessions');
43
+ const REGISTRY_PATH = path.join(SESSIONS_DIR, 'registry.json');
44
+
45
+ // Ensure sessions directory exists
46
+ function ensureSessionsDir() {
47
+ if (!fs.existsSync(SESSIONS_DIR)) {
48
+ fs.mkdirSync(SESSIONS_DIR, { recursive: true });
49
+ }
50
+ }
51
+
52
+ // Load or create registry
53
+ function loadRegistry() {
54
+ ensureSessionsDir();
55
+
56
+ if (fs.existsSync(REGISTRY_PATH)) {
57
+ try {
58
+ return JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));
59
+ } catch (e) {
60
+ console.error(`${c.red}Error loading registry: ${e.message}${c.reset}`);
61
+ }
62
+ }
63
+
64
+ // Create default registry
65
+ const registry = {
66
+ schema_version: '1.0.0',
67
+ next_id: 1,
68
+ project_name: path.basename(ROOT),
69
+ sessions: {},
70
+ };
71
+
72
+ saveRegistry(registry);
73
+ return registry;
74
+ }
75
+
76
+ // Save registry
77
+ function saveRegistry(registry) {
78
+ ensureSessionsDir();
79
+ registry.updated = new Date().toISOString();
80
+ fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
81
+ }
82
+
83
+ // Check if PID is alive
84
+ function isPidAlive(pid) {
85
+ if (!pid) return false;
86
+ try {
87
+ process.kill(pid, 0);
88
+ return true;
89
+ } catch (e) {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ // Get lock file path for session
95
+ function getLockPath(sessionId) {
96
+ return path.join(SESSIONS_DIR, `${sessionId}.lock`);
97
+ }
98
+
99
+ // Read lock file
100
+ function readLock(sessionId) {
101
+ const lockPath = getLockPath(sessionId);
102
+ if (!fs.existsSync(lockPath)) return null;
103
+
104
+ try {
105
+ const content = fs.readFileSync(lockPath, 'utf8');
106
+ const lock = {};
107
+ content.split('\n').forEach(line => {
108
+ const [key, value] = line.split('=');
109
+ if (key && value) lock[key.trim()] = value.trim();
110
+ });
111
+ return lock;
112
+ } catch (e) {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ // Write lock file
118
+ function writeLock(sessionId, pid) {
119
+ const lockPath = getLockPath(sessionId);
120
+ const content = `pid=${pid}\nstarted=${Math.floor(Date.now() / 1000)}\n`;
121
+ fs.writeFileSync(lockPath, content);
122
+ }
123
+
124
+ // Remove lock file
125
+ function removeLock(sessionId) {
126
+ const lockPath = getLockPath(sessionId);
127
+ if (fs.existsSync(lockPath)) {
128
+ fs.unlinkSync(lockPath);
129
+ }
130
+ }
131
+
132
+ // Check if session is active (has lock with alive PID)
133
+ function isSessionActive(sessionId) {
134
+ const lock = readLock(sessionId);
135
+ if (!lock || !lock.pid) return false;
136
+ return isPidAlive(parseInt(lock.pid, 10));
137
+ }
138
+
139
+ // Clean up stale locks
140
+ function cleanupStaleLocks(registry) {
141
+ let cleaned = 0;
142
+
143
+ for (const [id, session] of Object.entries(registry.sessions)) {
144
+ const lock = readLock(id);
145
+ if (lock && !isPidAlive(parseInt(lock.pid, 10))) {
146
+ removeLock(id);
147
+ cleaned++;
148
+ }
149
+ }
150
+
151
+ return cleaned;
152
+ }
153
+
154
+ // Get current git branch
155
+ function getCurrentBranch() {
156
+ try {
157
+ return execSync('git branch --show-current', { cwd: ROOT, encoding: 'utf8' }).trim();
158
+ } catch (e) {
159
+ return 'unknown';
160
+ }
161
+ }
162
+
163
+ // Get current story from status.json
164
+ function getCurrentStory() {
165
+ try {
166
+ const statusPath = path.join(ROOT, 'docs', '09-agents', 'status.json');
167
+ if (!fs.existsSync(statusPath)) return null;
168
+
169
+ const status = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
170
+ for (const [id, story] of Object.entries(status.stories || {})) {
171
+ if (story.status === 'in_progress') {
172
+ return { id, title: story.title };
173
+ }
174
+ }
175
+ } catch (e) {}
176
+ return null;
177
+ }
178
+
179
+ // Register current session (called on startup)
180
+ function registerSession(nickname = null) {
181
+ const registry = loadRegistry();
182
+ const cwd = process.cwd();
183
+ const branch = getCurrentBranch();
184
+ const story = getCurrentStory();
185
+ const pid = process.ppid || process.pid; // Parent PID (Claude Code) or current
186
+
187
+ // Check if this path already has a session
188
+ let existingId = null;
189
+ for (const [id, session] of Object.entries(registry.sessions)) {
190
+ if (session.path === cwd) {
191
+ existingId = id;
192
+ break;
193
+ }
194
+ }
195
+
196
+ if (existingId) {
197
+ // Update existing session
198
+ registry.sessions[existingId].branch = branch;
199
+ registry.sessions[existingId].story = story ? story.id : null;
200
+ registry.sessions[existingId].last_active = new Date().toISOString();
201
+ if (nickname) registry.sessions[existingId].nickname = nickname;
202
+
203
+ writeLock(existingId, pid);
204
+ saveRegistry(registry);
205
+
206
+ return { id: existingId, isNew: false };
207
+ }
208
+
209
+ // Create new session
210
+ const sessionId = String(registry.next_id);
211
+ registry.next_id++;
212
+
213
+ registry.sessions[sessionId] = {
214
+ path: cwd,
215
+ branch,
216
+ story: story ? story.id : null,
217
+ nickname: nickname || null,
218
+ created: new Date().toISOString(),
219
+ last_active: new Date().toISOString(),
220
+ is_main: cwd === ROOT,
221
+ };
222
+
223
+ writeLock(sessionId, pid);
224
+ saveRegistry(registry);
225
+
226
+ return { id: sessionId, isNew: true };
227
+ }
228
+
229
+ // Unregister session (called on exit)
230
+ function unregisterSession(sessionId) {
231
+ const registry = loadRegistry();
232
+
233
+ if (registry.sessions[sessionId]) {
234
+ registry.sessions[sessionId].last_active = new Date().toISOString();
235
+ removeLock(sessionId);
236
+ saveRegistry(registry);
237
+ }
238
+ }
239
+
240
+ // Create new session with worktree
241
+ function createSession(options = {}) {
242
+ const registry = loadRegistry();
243
+ const sessionId = String(registry.next_id);
244
+ const projectName = registry.project_name;
245
+
246
+ const nickname = options.nickname || null;
247
+ const branchName = options.branch || `session-${sessionId}`;
248
+ const dirName = nickname || sessionId;
249
+ const worktreePath = path.resolve(ROOT, '..', `${projectName}-${dirName}`);
250
+
251
+ // Check if directory already exists
252
+ if (fs.existsSync(worktreePath)) {
253
+ return {
254
+ success: false,
255
+ error: `Directory already exists: ${worktreePath}`,
256
+ };
257
+ }
258
+
259
+ // Create branch if it doesn't exist
260
+ try {
261
+ execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, { cwd: ROOT });
262
+ } catch (e) {
263
+ // Branch doesn't exist, create it
264
+ try {
265
+ execSync(`git branch ${branchName}`, { cwd: ROOT, encoding: 'utf8' });
266
+ } catch (e2) {
267
+ return { success: false, error: `Failed to create branch: ${e2.message}` };
268
+ }
269
+ }
270
+
271
+ // Create worktree
272
+ try {
273
+ execSync(`git worktree add "${worktreePath}" ${branchName}`, {
274
+ cwd: ROOT,
275
+ encoding: 'utf8',
276
+ stdio: 'pipe',
277
+ });
278
+ } catch (e) {
279
+ return { success: false, error: `Failed to create worktree: ${e.message}` };
280
+ }
281
+
282
+ // Register session
283
+ registry.next_id++;
284
+ registry.sessions[sessionId] = {
285
+ path: worktreePath,
286
+ branch: branchName,
287
+ story: null,
288
+ nickname,
289
+ created: new Date().toISOString(),
290
+ last_active: new Date().toISOString(),
291
+ is_main: false,
292
+ };
293
+
294
+ saveRegistry(registry);
295
+
296
+ return {
297
+ success: true,
298
+ sessionId,
299
+ path: worktreePath,
300
+ branch: branchName,
301
+ command: `cd "${worktreePath}" && claude`,
302
+ };
303
+ }
304
+
305
+ // Get all sessions with status
306
+ function getSessions() {
307
+ const registry = loadRegistry();
308
+ const cleaned = cleanupStaleLocks(registry);
309
+
310
+ const sessions = [];
311
+ for (const [id, session] of Object.entries(registry.sessions)) {
312
+ sessions.push({
313
+ id,
314
+ ...session,
315
+ active: isSessionActive(id),
316
+ current: session.path === process.cwd(),
317
+ });
318
+ }
319
+
320
+ // Sort by ID (numeric)
321
+ sessions.sort((a, b) => parseInt(a.id) - parseInt(b.id));
322
+
323
+ return { sessions, cleaned };
324
+ }
325
+
326
+ // Get count of active sessions (excluding current)
327
+ function getActiveSessionCount() {
328
+ const { sessions } = getSessions();
329
+ const cwd = process.cwd();
330
+ return sessions.filter(s => s.active && s.path !== cwd).length;
331
+ }
332
+
333
+ // Delete session (and optionally worktree)
334
+ function deleteSession(sessionId, removeWorktree = false) {
335
+ const registry = loadRegistry();
336
+ const session = registry.sessions[sessionId];
337
+
338
+ if (!session) {
339
+ return { success: false, error: `Session ${sessionId} not found` };
340
+ }
341
+
342
+ if (session.is_main) {
343
+ return { success: false, error: 'Cannot delete main session' };
344
+ }
345
+
346
+ // Remove lock
347
+ removeLock(sessionId);
348
+
349
+ // Remove worktree if requested
350
+ if (removeWorktree && fs.existsSync(session.path)) {
351
+ try {
352
+ execSync(`git worktree remove "${session.path}"`, { cwd: ROOT, encoding: 'utf8' });
353
+ } catch (e) {
354
+ // Try force remove
355
+ try {
356
+ execSync(`git worktree remove --force "${session.path}"`, { cwd: ROOT, encoding: 'utf8' });
357
+ } catch (e2) {
358
+ return { success: false, error: `Failed to remove worktree: ${e2.message}` };
359
+ }
360
+ }
361
+ }
362
+
363
+ // Remove from registry
364
+ delete registry.sessions[sessionId];
365
+ saveRegistry(registry);
366
+
367
+ return { success: true };
368
+ }
369
+
370
+ // Format sessions for display
371
+ function formatSessionsTable(sessions) {
372
+ const lines = [];
373
+
374
+ lines.push(`${c.cyan}Active Sessions:${c.reset}`);
375
+ lines.push(`${'─'.repeat(70)}`);
376
+
377
+ for (const session of sessions) {
378
+ const status = session.active ? `${c.green}●${c.reset}` : `${c.dim}○${c.reset}`;
379
+ const current = session.current ? ` ${c.yellow}(current)${c.reset}` : '';
380
+ const name = session.nickname ? `"${session.nickname}"` : session.branch;
381
+ const story = session.story ? `${c.blue}${session.story}${c.reset}` : `${c.dim}-${c.reset}`;
382
+
383
+ lines.push(` ${status} [${c.bold}${session.id}${c.reset}] ${name}${current}`);
384
+ lines.push(` ${c.dim}Story:${c.reset} ${story} ${c.dim}│ Path:${c.reset} ${session.path}`);
385
+ }
386
+
387
+ lines.push(`${'─'.repeat(70)}`);
388
+
389
+ return lines.join('\n');
390
+ }
391
+
392
+ // CLI interface
393
+ function main() {
394
+ const args = process.argv.slice(2);
395
+ const command = args[0];
396
+
397
+ switch (command) {
398
+ case 'register': {
399
+ const nickname = args[1] || null;
400
+ const result = registerSession(nickname);
401
+ console.log(JSON.stringify(result));
402
+ break;
403
+ }
404
+
405
+ case 'unregister': {
406
+ const sessionId = args[1];
407
+ if (sessionId) {
408
+ unregisterSession(sessionId);
409
+ console.log(JSON.stringify({ success: true }));
410
+ } else {
411
+ console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
412
+ }
413
+ break;
414
+ }
415
+
416
+ case 'create': {
417
+ const options = {};
418
+ for (let i = 1; i < args.length; i += 2) {
419
+ const key = args[i].replace('--', '');
420
+ const value = args[i + 1];
421
+ options[key] = value;
422
+ }
423
+ const result = createSession(options);
424
+ console.log(JSON.stringify(result));
425
+ break;
426
+ }
427
+
428
+ case 'list': {
429
+ const { sessions, cleaned } = getSessions();
430
+ if (args.includes('--json')) {
431
+ console.log(JSON.stringify({ sessions, cleaned }));
432
+ } else {
433
+ console.log(formatSessionsTable(sessions));
434
+ if (cleaned > 0) {
435
+ console.log(`${c.dim}Cleaned ${cleaned} stale lock(s)${c.reset}`);
436
+ }
437
+ }
438
+ break;
439
+ }
440
+
441
+ case 'count': {
442
+ const count = getActiveSessionCount();
443
+ console.log(JSON.stringify({ count }));
444
+ break;
445
+ }
446
+
447
+ case 'delete': {
448
+ const sessionId = args[1];
449
+ const removeWorktree = args.includes('--remove-worktree');
450
+ const result = deleteSession(sessionId, removeWorktree);
451
+ console.log(JSON.stringify(result));
452
+ break;
453
+ }
454
+
455
+ case 'status': {
456
+ const { sessions } = getSessions();
457
+ const cwd = process.cwd();
458
+ const current = sessions.find(s => s.path === cwd);
459
+ const others = sessions.filter(s => s.active && s.path !== cwd);
460
+
461
+ console.log(
462
+ JSON.stringify({
463
+ current: current || null,
464
+ otherActive: others.length,
465
+ total: sessions.length,
466
+ })
467
+ );
468
+ break;
469
+ }
470
+
471
+ case 'help':
472
+ default:
473
+ console.log(`
474
+ ${c.brand}${c.bold}Session Manager${c.reset} - Multi-session coordination for Claude Code
475
+
476
+ ${c.cyan}Commands:${c.reset}
477
+ register [nickname] Register current directory as a session
478
+ unregister <id> Unregister a session (remove lock)
479
+ create [--nickname X] Create new session with git worktree
480
+ list [--json] List all sessions
481
+ count Count other active sessions
482
+ delete <id> [--remove-worktree] Delete session
483
+ status Get current session status
484
+ help Show this help
485
+
486
+ ${c.cyan}Examples:${c.reset}
487
+ node session-manager.js register
488
+ node session-manager.js create --nickname auth
489
+ node session-manager.js list
490
+ node session-manager.js delete 2 --remove-worktree
491
+ `);
492
+ }
493
+ }
494
+
495
+ // Export for use as module
496
+ module.exports = {
497
+ loadRegistry,
498
+ saveRegistry,
499
+ registerSession,
500
+ unregisterSession,
501
+ createSession,
502
+ getSessions,
503
+ getActiveSessionCount,
504
+ deleteSession,
505
+ isSessionActive,
506
+ cleanupStaleLocks,
507
+ };
508
+
509
+ // Run CLI if executed directly
510
+ if (require.main === module) {
511
+ main();
512
+ }