agileflow 2.92.0 โ†’ 2.93.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 (123) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -6
  3. package/lib/codebase-indexer.js +2 -1
  4. package/package.json +1 -1
  5. package/scripts/agileflow-statusline.sh +106 -0
  6. package/scripts/agileflow-welcome.js +135 -22
  7. package/scripts/document-repl.js +793 -0
  8. package/scripts/lib/configure-features.js +8 -1
  9. package/scripts/lib/context-loader.js +16 -16
  10. package/scripts/query-codebase.js +8 -3
  11. package/scripts/session-manager.js +374 -16
  12. package/scripts/spawn-parallel.js +72 -30
  13. package/src/core/agents/accessibility.md +19 -125
  14. package/src/core/agents/adr-writer.md +18 -1
  15. package/src/core/agents/analytics.md +19 -125
  16. package/src/core/agents/api.md +5 -130
  17. package/src/core/agents/ci.md +26 -131
  18. package/src/core/agents/compliance.md +21 -125
  19. package/src/core/agents/database.md +20 -125
  20. package/src/core/agents/datamigration.md +20 -125
  21. package/src/core/agents/design.md +19 -125
  22. package/src/core/agents/devops.md +12 -129
  23. package/src/core/agents/documentation.md +18 -1
  24. package/src/core/agents/epic-planner.md +31 -10
  25. package/src/core/agents/integrations.md +19 -125
  26. package/src/core/agents/mobile.md +19 -125
  27. package/src/core/agents/monitoring.md +19 -125
  28. package/src/core/agents/performance.md +19 -125
  29. package/src/core/agents/product.md +18 -1
  30. package/src/core/agents/qa.md +21 -125
  31. package/src/core/agents/readme-updater.md +18 -1
  32. package/src/core/agents/refactor.md +19 -125
  33. package/src/core/agents/research.md +3 -1
  34. package/src/core/agents/rlm-subcore.md +202 -0
  35. package/src/core/agents/security.md +7 -125
  36. package/src/core/agents/testing.md +20 -125
  37. package/src/core/agents/ui.md +14 -135
  38. package/src/core/commands/adr/list.md +20 -0
  39. package/src/core/commands/adr/update.md +24 -1
  40. package/src/core/commands/adr/view.md +23 -1
  41. package/src/core/commands/adr.md +2 -2
  42. package/src/core/commands/agent.md +11 -1
  43. package/src/core/commands/assign.md +15 -6
  44. package/src/core/commands/auto.md +11 -1
  45. package/src/core/commands/babysit.md +15 -4
  46. package/src/core/commands/baseline.md +11 -1
  47. package/src/core/commands/batch.md +11 -1
  48. package/src/core/commands/blockers.md +11 -1
  49. package/src/core/commands/board.md +11 -1
  50. package/src/core/commands/changelog.md +11 -0
  51. package/src/core/commands/choose.md +16 -1
  52. package/src/core/commands/ci.md +11 -1
  53. package/src/core/commands/configure.md +73 -2
  54. package/src/core/commands/context/export.md +8 -0
  55. package/src/core/commands/context/full.md +8 -0
  56. package/src/core/commands/context/note.md +8 -0
  57. package/src/core/commands/debt.md +11 -0
  58. package/src/core/commands/deploy.md +10 -0
  59. package/src/core/commands/deps.md +11 -1
  60. package/src/core/commands/diagnose.md +10 -0
  61. package/src/core/commands/docs.md +12 -2
  62. package/src/core/commands/epic/list.md +20 -0
  63. package/src/core/commands/epic/view.md +25 -0
  64. package/src/core/commands/epic.md +5 -6
  65. package/src/core/commands/feedback.md +11 -0
  66. package/src/core/commands/handoff.md +12 -2
  67. package/src/core/commands/help.md +10 -0
  68. package/src/core/commands/ideate.md +10 -0
  69. package/src/core/commands/impact.md +11 -1
  70. package/src/core/commands/metrics.md +11 -1
  71. package/src/core/commands/multi-expert.md +11 -1
  72. package/src/core/commands/packages.md +11 -0
  73. package/src/core/commands/pr.md +10 -0
  74. package/src/core/commands/readme-sync.md +10 -5
  75. package/src/core/commands/research/analyze.md +60 -3
  76. package/src/core/commands/research/ask.md +9 -1
  77. package/src/core/commands/research/import.md +8 -0
  78. package/src/core/commands/research/list.md +8 -0
  79. package/src/core/commands/research/synthesize.md +9 -1
  80. package/src/core/commands/research/view.md +8 -0
  81. package/src/core/commands/retro.md +12 -2
  82. package/src/core/commands/review.md +11 -1
  83. package/src/core/commands/rlm.md +363 -0
  84. package/src/core/commands/roadmap/analyze.md +1 -1
  85. package/src/core/commands/rpi.md +9 -1
  86. package/src/core/commands/session/cleanup.md +250 -0
  87. package/src/core/commands/session/end.md +10 -0
  88. package/src/core/commands/session/history.md +11 -1
  89. package/src/core/commands/session/init.md +10 -0
  90. package/src/core/commands/session/new.md +132 -13
  91. package/src/core/commands/session/resume.md +10 -0
  92. package/src/core/commands/session/spawn.md +8 -0
  93. package/src/core/commands/session/status.md +10 -0
  94. package/src/core/commands/skill/create.md +1 -1
  95. package/src/core/commands/skill/delete.md +11 -1
  96. package/src/core/commands/skill/edit.md +11 -1
  97. package/src/core/commands/skill/test.md +11 -1
  98. package/src/core/commands/skill/upgrade.md +11 -1
  99. package/src/core/commands/sprint.md +14 -3
  100. package/src/core/commands/status.md +15 -6
  101. package/src/core/commands/story/list.md +23 -0
  102. package/src/core/commands/story/view.md +24 -0
  103. package/src/core/commands/story.md +4 -5
  104. package/src/core/commands/template.md +10 -0
  105. package/src/core/commands/tests.md +10 -0
  106. package/src/core/commands/update.md +10 -0
  107. package/src/core/commands/validate-expertise.md +10 -1
  108. package/src/core/commands/velocity.md +11 -1
  109. package/src/core/commands/verify.md +13 -1
  110. package/src/core/commands/whats-new.md +8 -0
  111. package/src/core/commands/workflow.md +16 -1
  112. package/src/core/templates/agent-coordination-pattern.md +38 -0
  113. package/src/core/templates/agileflow-metadata.json +25 -0
  114. package/src/core/templates/preserve-rules-common.md +107 -0
  115. package/src/core/templates/preserve-rules.json +42 -0
  116. package/src/core/templates/proactive-action-spec.md +29 -0
  117. package/src/core/templates/quality-gate-priorities.md +34 -0
  118. package/src/core/templates/session-harness-protocol.md +128 -0
  119. package/tools/cli/commands/setup.js +12 -3
  120. package/tools/cli/installers/ide/windsurf.js +1 -1
  121. package/tools/cli/lib/content-injector.js +336 -0
  122. package/tools/cli/lib/ide-registry.js +2 -4
  123. package/tools/cli/lib/ui.js +2 -1
@@ -65,7 +65,14 @@ const PROFILES = {
65
65
  minimal: {
66
66
  description: 'SessionStart + archival only',
67
67
  enable: ['sessionstart', 'archival'],
68
- disable: ['precompact', 'statusline', 'ralphloop', 'selfimprove', 'askuserquestion', 'tmuxautospawn'],
68
+ disable: [
69
+ 'precompact',
70
+ 'statusline',
71
+ 'ralphloop',
72
+ 'selfimprove',
73
+ 'askuserquestion',
74
+ 'tmuxautospawn',
75
+ ],
69
76
  archivalDays: 30,
70
77
  },
71
78
  none: {
@@ -63,22 +63,22 @@ const SAFEEXEC_ALLOWED_COMMANDS = [
63
63
  * Dangerous patterns that should never be executed
64
64
  */
65
65
  const SAFEEXEC_BLOCKED_PATTERNS = [
66
- /\|/, // Pipe
67
- /;/, // Command separator
68
- /&&/, // AND operator
69
- /\|\|/, // OR operator
70
- /`/, // Backticks
71
- /\$\(/, // Command substitution
72
- />/, // Redirect output
73
- /</, // Redirect input
74
- /\bsudo\b/, // Sudo
75
- /\brm\b/, // Remove
76
- /\bmv\b/, // Move
77
- /\bcp\b/, // Copy
78
- /\bchmod\b/, // Change permissions
79
- /\bchown\b/, // Change owner
80
- /\bcurl\b/, // curl (network)
81
- /\bwget\b/, // wget (network)
66
+ /\|/, // Pipe
67
+ /;/, // Command separator
68
+ /&&/, // AND operator
69
+ /\|\|/, // OR operator
70
+ /`/, // Backticks
71
+ /\$\(/, // Command substitution
72
+ />/, // Redirect output
73
+ /</, // Redirect input
74
+ /\bsudo\b/, // Sudo
75
+ /\brm\b/, // Remove
76
+ /\bmv\b/, // Move
77
+ /\bcp\b/, // Copy
78
+ /\bchmod\b/, // Change permissions
79
+ /\bchown\b/, // Change owner
80
+ /\bcurl\b/, // curl (network)
81
+ /\bwget\b/, // wget (network)
82
82
  ];
83
83
 
84
84
  /**
@@ -114,7 +114,7 @@ function explainWorkflow(queryType, queryValue, projectRoot) {
114
114
  lines.push('# This tool adds: index awareness, budget truncation, structured output.');
115
115
  break;
116
116
 
117
- case 'tag':
117
+ case 'tag': {
118
118
  const tagPatterns = {
119
119
  api: '/api/|/routes/|/controllers/',
120
120
  ui: '/components/|/views/|/pages/',
@@ -123,10 +123,13 @@ function explainWorkflow(queryType, queryValue, projectRoot) {
123
123
  test: '/test/|/__tests__/|/spec/',
124
124
  };
125
125
  lines.push('# Equivalent to find with path patterns:');
126
- lines.push(`find ${projectRoot} -type f | grep -E "${tagPatterns[queryValue] || queryValue}"`);
126
+ lines.push(
127
+ `find ${projectRoot} -type f | grep -E "${tagPatterns[queryValue] || queryValue}"`
128
+ );
127
129
  lines.push('');
128
130
  lines.push('# This tool uses pre-indexed tags for instant lookup.');
129
131
  break;
132
+ }
130
133
 
131
134
  case 'export':
132
135
  lines.push('# Equivalent to grep for export statements:');
@@ -140,7 +143,9 @@ function explainWorkflow(queryType, queryValue, projectRoot) {
140
143
  lines.push(`grep -n "import.*from" ${queryValue}`);
141
144
  lines.push('');
142
145
  lines.push('# Plus reverse search for files importing this one:');
143
- lines.push(`grep -rl "${path.basename(queryValue, path.extname(queryValue))}" ${projectRoot}/src/`);
146
+ lines.push(
147
+ `grep -rl "${path.basename(queryValue, path.extname(queryValue))}" ${projectRoot}/src/`
148
+ );
144
149
  lines.push('');
145
150
  lines.push('# This tool tracks bidirectional dependencies in index.');
146
151
  break;
@@ -11,11 +11,16 @@
11
11
 
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
- const { execSync, spawnSync } = require('child_process');
14
+ const { execSync, spawnSync, spawn } = require('child_process');
15
15
 
16
16
  // Shared utilities
17
17
  const { c } = require('../lib/colors');
18
- const { getProjectRoot, getStatusPath, getSessionStatePath, getAgileflowDir } = require('../lib/paths');
18
+ const {
19
+ getProjectRoot,
20
+ getStatusPath,
21
+ getSessionStatePath,
22
+ getAgileflowDir,
23
+ } = require('../lib/paths');
19
24
  const { safeReadJSON } = require('../lib/errors');
20
25
  const { isValidBranchName, isValidSessionNickname } = require('../lib/validate');
21
26
 
@@ -226,6 +231,161 @@ async function cleanupStaleLocksAsync(registry, options = {}) {
226
231
  return { count: cleanedSessions.length, sessions: cleanedSessions };
227
232
  }
228
233
 
234
+ /**
235
+ * Get detailed file information for a session's changes
236
+ * @param {string} sessionPath - Path to session worktree
237
+ * @param {string[]} changes - Array of git status lines
238
+ * @returns {Object[]} Array of file details with analysis
239
+ */
240
+ function getFileDetails(sessionPath, changes) {
241
+ return changes.map((change) => {
242
+ const status = change.substring(0, 2).trim();
243
+ const file = change.substring(3);
244
+
245
+ const detail = { status, file, trivial: false, existsInMain: false, diffLines: 0 };
246
+
247
+ // For modified files, get diff stats
248
+ if (status === 'M') {
249
+ try {
250
+ const diffStat = spawnSync('git', ['diff', '--numstat', file], {
251
+ cwd: sessionPath,
252
+ encoding: 'utf8',
253
+ timeout: 3000,
254
+ });
255
+ if (diffStat.stdout) {
256
+ const parts = diffStat.stdout.trim().split('\t');
257
+ const added = parseInt(parts[0], 10) || 0;
258
+ const removed = parseInt(parts[1], 10) || 0;
259
+ detail.diffLines = added + removed;
260
+ // Trivial if only 1-2 lines changed (likely whitespace)
261
+ detail.trivial = detail.diffLines <= 2;
262
+ }
263
+ } catch (e) {
264
+ // Can't get diff, assume not trivial
265
+ }
266
+ }
267
+
268
+ // For untracked files, check if exists in main
269
+ if (status === '??') {
270
+ detail.existsInMain = fs.existsSync(path.join(ROOT, file));
271
+ // Trivial if it's a duplicate
272
+ detail.trivial = detail.existsInMain;
273
+ }
274
+
275
+ // Config/cache files are trivial
276
+ if (file.includes('.claude/') || file.includes('.agileflow/cache')) {
277
+ detail.trivial = true;
278
+ }
279
+
280
+ return detail;
281
+ });
282
+ }
283
+
284
+ /**
285
+ * Get health status for all sessions
286
+ * Detects: stale sessions, uncommitted changes, orphaned entries
287
+ * @param {Object} options - { staleDays: 7, detailed: false }
288
+ * @returns {Object} Health report
289
+ */
290
+ function getSessionsHealth(options = {}) {
291
+ const { staleDays = 7, detailed = false } = options;
292
+ const registry = loadRegistry();
293
+ const now = Date.now();
294
+ const staleThreshold = staleDays * 24 * 60 * 60 * 1000;
295
+
296
+ const health = {
297
+ stale: [], // Sessions with no activity > staleDays
298
+ uncommitted: [], // Sessions with uncommitted git changes
299
+ orphanedRegistry: [], // Registry entries where path doesn't exist
300
+ orphanedWorktrees: [], // Worktrees not in registry
301
+ healthy: 0,
302
+ };
303
+
304
+ // Check each registered session
305
+ for (const [id, session] of Object.entries(registry.sessions)) {
306
+ if (session.is_main) continue; // Skip main session
307
+
308
+ const age = now - new Date(session.last_active).getTime();
309
+ const pathExists = fs.existsSync(session.path);
310
+
311
+ // Check for orphaned registry entry (path missing)
312
+ if (!pathExists) {
313
+ health.orphanedRegistry.push({ id, ...session, reason: 'path_missing' });
314
+ continue;
315
+ }
316
+
317
+ // Check for stale session
318
+ if (age > staleThreshold) {
319
+ health.stale.push({
320
+ id,
321
+ ...session,
322
+ ageDays: Math.floor(age / (24 * 60 * 60 * 1000)),
323
+ });
324
+ }
325
+
326
+ // Check for uncommitted changes
327
+ try {
328
+ const result = spawnSync('git', ['status', '--porcelain'], {
329
+ cwd: session.path,
330
+ encoding: 'utf8',
331
+ timeout: 5000,
332
+ });
333
+ if (result.stdout && result.stdout.trim()) {
334
+ // Don't use trim() on the whole string - it removes leading space from first status
335
+ // Split by newline and filter empty lines instead
336
+ const changes = result.stdout.split('\n').filter((line) => line.length > 0);
337
+ const sessionData = {
338
+ id,
339
+ ...session,
340
+ changeCount: changes.length,
341
+ changes: detailed ? changes : changes.slice(0, 5), // All or first 5
342
+ };
343
+
344
+ // Add detailed file analysis if requested
345
+ if (detailed) {
346
+ sessionData.fileDetails = getFileDetails(session.path, changes);
347
+ // Calculate if session is safe to delete (all changes trivial)
348
+ sessionData.allTrivial = sessionData.fileDetails.every((f) => f.trivial);
349
+ }
350
+
351
+ health.uncommitted.push(sessionData);
352
+ } else {
353
+ health.healthy++;
354
+ }
355
+ } catch (e) {
356
+ // Can't check, skip
357
+ }
358
+ }
359
+
360
+ // Check for orphaned worktrees (directories not in registry)
361
+ try {
362
+ const worktreeList = spawnSync('git', ['worktree', 'list', '--porcelain'], {
363
+ encoding: 'utf8',
364
+ });
365
+ if (worktreeList.stdout) {
366
+ const worktrees = worktreeList.stdout
367
+ .split('\n')
368
+ .filter((line) => line.startsWith('worktree '))
369
+ .map((line) => line.replace('worktree ', ''));
370
+
371
+ const mainPath = ROOT;
372
+ for (const wtPath of worktrees) {
373
+ const inRegistry = Object.values(registry.sessions).some((s) => s.path === wtPath);
374
+ if (!inRegistry && wtPath !== mainPath) {
375
+ // Check if it's an AgileFlow worktree (has .agileflow folder)
376
+ if (fs.existsSync(path.join(wtPath, '.agileflow'))) {
377
+ health.orphanedWorktrees.push({ path: wtPath });
378
+ }
379
+ }
380
+ }
381
+ }
382
+ } catch (e) {
383
+ // Can't list worktrees, skip
384
+ }
385
+
386
+ return health;
387
+ }
388
+
229
389
  // Git command cache (10 second TTL to avoid stale data)
230
390
  const gitCache = {
231
391
  data: new Map(),
@@ -380,8 +540,165 @@ function getSession(sessionId) {
380
540
  };
381
541
  }
382
542
 
543
+ // Default worktree timeout (2 minutes)
544
+ const DEFAULT_WORKTREE_TIMEOUT_MS = 120000;
545
+
546
+ /**
547
+ * Display progress feedback during long operations.
548
+ * Returns a function to stop the progress indicator.
549
+ *
550
+ * @param {string} message - Progress message
551
+ * @returns {function} Stop function
552
+ */
553
+ function progressIndicator(message) {
554
+ const frames = ['โ ‹', 'โ ™', 'โ น', 'โ ธ', 'โ ผ', 'โ ด', 'โ ฆ', 'โ ง', 'โ ‡', 'โ '];
555
+ let frameIndex = 0;
556
+ let elapsed = 0;
557
+
558
+ // For TTY (interactive terminal), show spinner
559
+ if (process.stderr.isTTY) {
560
+ const interval = setInterval(() => {
561
+ process.stderr.write(`\r${frames[frameIndex++ % frames.length]} ${message}`);
562
+ }, 80);
563
+ return () => {
564
+ clearInterval(interval);
565
+ process.stderr.write(`\r${' '.repeat(message.length + 2)}\r`);
566
+ };
567
+ }
568
+
569
+ // For non-TTY (Claude Code, piped output), emit periodic updates to stderr
570
+ process.stderr.write(`โณ ${message}...\n`);
571
+ const interval = setInterval(() => {
572
+ elapsed += 10;
573
+ process.stderr.write(`โณ Still working... (${elapsed}s elapsed)\n`);
574
+ }, 10000); // Update every 10 seconds
575
+
576
+ return () => {
577
+ clearInterval(interval);
578
+ };
579
+ }
580
+
581
+ /**
582
+ * Create a git worktree with timeout and progress feedback.
583
+ * Uses async spawn instead of spawnSync for timeout support.
584
+ *
585
+ * @param {string} worktreePath - Path for the new worktree
586
+ * @param {string} branchName - Branch name for the worktree
587
+ * @param {number} timeoutMs - Timeout in milliseconds
588
+ * @returns {Promise<{stdout: string, stderr: string}>}
589
+ */
590
+ function createWorktreeWithTimeout(
591
+ worktreePath,
592
+ branchName,
593
+ timeoutMs = DEFAULT_WORKTREE_TIMEOUT_MS
594
+ ) {
595
+ return new Promise((resolve, reject) => {
596
+ let stdout = '';
597
+ let stderr = '';
598
+ let timedOut = false;
599
+
600
+ const proc = spawn('git', ['worktree', 'add', worktreePath, branchName], {
601
+ cwd: ROOT,
602
+ });
603
+
604
+ const timer = setTimeout(() => {
605
+ timedOut = true;
606
+ proc.kill('SIGTERM');
607
+ // Give it a moment to terminate gracefully, then SIGKILL
608
+ setTimeout(() => {
609
+ try {
610
+ proc.kill('SIGKILL');
611
+ } catch (e) {
612
+ // Process may have already exited
613
+ }
614
+ }, 1000);
615
+ }, timeoutMs);
616
+
617
+ proc.stdout.on('data', data => {
618
+ stdout += data.toString();
619
+ });
620
+
621
+ proc.stderr.on('data', data => {
622
+ stderr += data.toString();
623
+ });
624
+
625
+ proc.on('error', err => {
626
+ clearTimeout(timer);
627
+ reject(new Error(`Failed to spawn git: ${err.message}`));
628
+ });
629
+
630
+ proc.on('close', (code, signal) => {
631
+ clearTimeout(timer);
632
+
633
+ if (timedOut) {
634
+ reject(
635
+ new Error(
636
+ `Worktree creation timed out after ${timeoutMs / 1000}s. Try increasing timeout or check disk space.`
637
+ )
638
+ );
639
+ return;
640
+ }
641
+
642
+ if (signal) {
643
+ reject(new Error(`Worktree creation was terminated by signal: ${signal}`));
644
+ return;
645
+ }
646
+
647
+ if (code === 0) {
648
+ resolve({ stdout, stderr });
649
+ } else {
650
+ reject(new Error(`Failed to create worktree: ${stderr || 'unknown error'}`));
651
+ }
652
+ });
653
+ });
654
+ }
655
+
656
+ /**
657
+ * Clean up partial state after failed worktree creation.
658
+ * Removes partial directory and prunes git worktree registry.
659
+ *
660
+ * @param {string} worktreePath - Path of the failed worktree
661
+ * @param {string} branchName - Branch name that was being used
662
+ * @param {boolean} branchCreatedByUs - Whether we created the branch
663
+ */
664
+ function cleanupFailedWorktree(worktreePath, branchName, branchCreatedByUs = false) {
665
+ // Remove partial worktree directory if it exists
666
+ if (fs.existsSync(worktreePath)) {
667
+ try {
668
+ fs.rmSync(worktreePath, { recursive: true, force: true });
669
+ process.stderr.write(`๐Ÿงน Cleaned up partial worktree directory\n`);
670
+ } catch (e) {
671
+ process.stderr.write(`โš ๏ธ Could not remove partial directory: ${e.message}\n`);
672
+ }
673
+ }
674
+
675
+ // Prune git worktree registry to clean up any references
676
+ try {
677
+ spawnSync('git', ['worktree', 'prune'], { cwd: ROOT, encoding: 'utf8' });
678
+ } catch (e) {
679
+ // Non-fatal
680
+ }
681
+
682
+ // If we created the branch and the worktree failed, optionally clean up the branch too
683
+ // But only if it has no commits beyond the parent (i.e., we just created it)
684
+ if (branchCreatedByUs) {
685
+ try {
686
+ // Check if branch exists and has no unique commits
687
+ const result = spawnSync('git', ['branch', '-d', branchName], {
688
+ cwd: ROOT,
689
+ encoding: 'utf8',
690
+ });
691
+ if (result.status === 0) {
692
+ process.stderr.write(`๐Ÿงน Cleaned up unused branch: ${branchName}\n`);
693
+ }
694
+ } catch (e) {
695
+ // Non-fatal - branch may have commits or not exist
696
+ }
697
+ }
698
+ }
699
+
383
700
  // Create new session with worktree
384
- function createSession(options = {}) {
701
+ async function createSession(options = {}) {
385
702
  const registry = loadRegistry();
386
703
  const sessionId = String(registry.next_id);
387
704
  const projectName = registry.project_name;
@@ -426,6 +743,7 @@ function createSession(options = {}) {
426
743
  }
427
744
  );
428
745
 
746
+ let branchCreatedByUs = false;
429
747
  if (checkRef.status !== 0) {
430
748
  // Branch doesn't exist, create it
431
749
  const createBranch = spawnSync('git', ['branch', branchName], {
@@ -439,18 +757,27 @@ function createSession(options = {}) {
439
757
  error: `Failed to create branch: ${createBranch.stderr || 'unknown error'}`,
440
758
  };
441
759
  }
760
+ branchCreatedByUs = true;
442
761
  }
443
762
 
444
- // Create worktree (using spawnSync for safety)
445
- const createWorktree = spawnSync('git', ['worktree', 'add', worktreePath, branchName], {
446
- cwd: ROOT,
447
- encoding: 'utf8',
448
- });
763
+ // Get timeout from options (default: 2 minutes)
764
+ const timeoutMs = options.timeout || DEFAULT_WORKTREE_TIMEOUT_MS;
449
765
 
450
- if (createWorktree.status !== 0) {
766
+ // Create worktree with timeout and progress feedback
767
+ const stopProgress = progressIndicator(
768
+ 'Creating worktree (this may take a while for large repos)'
769
+ );
770
+ try {
771
+ await createWorktreeWithTimeout(worktreePath, branchName, timeoutMs);
772
+ stopProgress();
773
+ process.stderr.write(`โœ“ Worktree created successfully\n`);
774
+ } catch (error) {
775
+ stopProgress();
776
+ // Clean up partial state
777
+ cleanupFailedWorktree(worktreePath, branchName, branchCreatedByUs);
451
778
  return {
452
779
  success: false,
453
- error: `Failed to create worktree: ${createWorktree.stderr || 'unknown error'}`,
780
+ error: error.message,
454
781
  };
455
782
  }
456
783
 
@@ -471,9 +798,10 @@ function createSession(options = {}) {
471
798
  }
472
799
  }
473
800
 
474
- // Copy Claude Code and AgileFlow config folders (gitignored contents won't copy with worktree)
801
+ // Copy Claude Code, AgileFlow config, and docs folders (gitignored contents won't copy with worktree)
475
802
  // Note: The folder may exist with some tracked files, but gitignored subfolders (commands/, agents/) won't be there
476
- const configFolders = ['.claude', '.agileflow'];
803
+ // docs/ contains gitignored state files like status.json, session-state.json that need to be shared
804
+ const configFolders = ['.claude', '.agileflow', 'docs'];
477
805
  const copiedFolders = [];
478
806
  for (const folder of configFolders) {
479
807
  const src = path.join(ROOT, folder);
@@ -1115,7 +1443,7 @@ function main() {
1115
1443
  case 'create': {
1116
1444
  const options = {};
1117
1445
  // SECURITY: Only accept whitelisted option keys
1118
- const allowedKeys = ['nickname', 'branch'];
1446
+ const allowedKeys = ['nickname', 'branch', 'timeout'];
1119
1447
  for (let i = 1; i < args.length; i++) {
1120
1448
  const arg = args[i];
1121
1449
  if (arg.startsWith('--')) {
@@ -1133,8 +1461,27 @@ function main() {
1133
1461
  }
1134
1462
  }
1135
1463
  }
1136
- const result = createSession(options);
1137
- console.log(JSON.stringify(result));
1464
+ // Parse timeout as number (milliseconds)
1465
+ if (options.timeout) {
1466
+ options.timeout = parseInt(options.timeout, 10);
1467
+ if (isNaN(options.timeout) || options.timeout < 1000) {
1468
+ console.log(
1469
+ JSON.stringify({
1470
+ success: false,
1471
+ error: 'Timeout must be a number >= 1000 (milliseconds)',
1472
+ })
1473
+ );
1474
+ return;
1475
+ }
1476
+ }
1477
+ // Handle async createSession
1478
+ createSession(options)
1479
+ .then(result => {
1480
+ console.log(JSON.stringify(result));
1481
+ })
1482
+ .catch(err => {
1483
+ console.log(JSON.stringify({ success: false, error: err.message }));
1484
+ });
1138
1485
  break;
1139
1486
  }
1140
1487
 
@@ -1186,6 +1533,17 @@ function main() {
1186
1533
  break;
1187
1534
  }
1188
1535
 
1536
+ case 'health': {
1537
+ // Get health status for all sessions
1538
+ // Usage: health [staleDays] [--detailed]
1539
+ const staleDaysArg = args.find((a) => /^\d+$/.test(a));
1540
+ const staleDays = staleDaysArg ? parseInt(staleDaysArg, 10) : 7;
1541
+ const detailed = args.includes('--detailed');
1542
+ const health = getSessionsHealth({ staleDays, detailed });
1543
+ console.log(JSON.stringify(health));
1544
+ break;
1545
+ }
1546
+
1189
1547
  case 'get': {
1190
1548
  const sessionId = args[1];
1191
1549
  if (!sessionId) {
@@ -1448,7 +1806,7 @@ ${c.brand}${c.bold}Session Manager${c.reset} - Multi-session coordination for Cl
1448
1806
  ${c.cyan}Commands:${c.reset}
1449
1807
  register [nickname] Register current directory as a session
1450
1808
  unregister <id> Unregister a session (remove lock)
1451
- create [--nickname X] Create new session with git worktree
1809
+ create [--nickname X] [--timeout MS] Create session with worktree (default timeout: 120000ms)
1452
1810
  list [--json] List all sessions
1453
1811
  count Count other active sessions
1454
1812
  delete <id> [--remove-worktree] Delete session