agileflow 2.92.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 CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.92.1] - 2026-01-23
11
+
12
+ ### Added
13
+ - Session worktree timeout, progress feedback, and docs folder copy
14
+
10
15
  ## [2.92.0] - 2026-01-23
11
16
 
12
17
  ### Added
package/README.md CHANGED
@@ -3,8 +3,8 @@
3
3
  </p>
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/agileflow?color=brightgreen)](https://www.npmjs.com/package/agileflow)
6
- [![Commands](https://img.shields.io/badge/commands-76-blue)](docs/04-architecture/commands.md)
7
- [![Agents/Experts](https://img.shields.io/badge/agents%2Fexperts-30-orange)](docs/04-architecture/subagents.md)
6
+ [![Commands](https://img.shields.io/badge/commands-77-blue)](docs/04-architecture/commands.md)
7
+ [![Agents/Experts](https://img.shields.io/badge/agents%2Fexperts-31-orange)](docs/04-architecture/subagents.md)
8
8
  [![Skills](https://img.shields.io/badge/skills-dynamic-purple)](docs/04-architecture/skills.md)
9
9
 
10
10
  **AI-driven agile development for Claude Code, Cursor, Windsurf, OpenAI Codex CLI, and more.** Combining Scrum, Kanban, ADRs, and docs-as-code principles into one framework-agnostic system.
@@ -65,8 +65,8 @@ AgileFlow combines three proven methodologies:
65
65
 
66
66
  | Component | Count | Description |
67
67
  |-----------|-------|-------------|
68
- | [Commands](docs/04-architecture/commands.md) | 76 | Slash commands for agile workflows |
69
- | [Agents/Experts](docs/04-architecture/subagents.md) | 30 | Specialized agents with self-improving knowledge bases |
68
+ | [Commands](docs/04-architecture/commands.md) | 77 | Slash commands for agile workflows |
69
+ | [Agents/Experts](docs/04-architecture/subagents.md) | 31 | Specialized agents with self-improving knowledge bases |
70
70
  | [Skills](docs/04-architecture/skills.md) | Dynamic | Generated on-demand with `/agileflow:skill:create` |
71
71
 
72
72
  ---
@@ -76,8 +76,8 @@ AgileFlow combines three proven methodologies:
76
76
  Full documentation lives in [`docs/04-architecture/`](docs/04-architecture/):
77
77
 
78
78
  ### Reference
79
- - [Commands](docs/04-architecture/commands.md) - All 76 slash commands
80
- - [Agents/Experts](docs/04-architecture/subagents.md) - 30 specialized agents with self-improving knowledge
79
+ - [Commands](docs/04-architecture/commands.md) - All 77 slash commands
80
+ - [Agents/Experts](docs/04-architecture/subagents.md) - 31 specialized agents with self-improving knowledge
81
81
  - [Skills](docs/04-architecture/skills.md) - Dynamic skill generator with MCP integration
82
82
 
83
83
  ### Architecture
@@ -643,7 +643,8 @@ function updateIndex(projectRoot, options = {}) {
643
643
  existingIndex.tags[tag].push(filePath);
644
644
  }
645
645
  for (const exp of fileData.exports || []) {
646
- if (!Object.hasOwn(existingIndex.symbols.exports, exp)) existingIndex.symbols.exports[exp] = [];
646
+ if (!Object.hasOwn(existingIndex.symbols.exports, exp))
647
+ existingIndex.symbols.exports[exp] = [];
647
648
  existingIndex.symbols.exports[exp].push(filePath);
648
649
  }
649
650
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.92.0",
3
+ "version": "2.92.1",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -102,10 +102,20 @@ function detectPlatform() {
102
102
  // Try to detect Linux distribution
103
103
  try {
104
104
  const osRelease = fs.readFileSync('/etc/os-release', 'utf8');
105
- if (osRelease.includes('Ubuntu') || osRelease.includes('Debian') || osRelease.includes('Pop!_OS') || osRelease.includes('Mint')) {
105
+ if (
106
+ osRelease.includes('Ubuntu') ||
107
+ osRelease.includes('Debian') ||
108
+ osRelease.includes('Pop!_OS') ||
109
+ osRelease.includes('Mint')
110
+ ) {
106
111
  return { os: 'Ubuntu/Debian', installCmd: 'sudo apt install tmux', hasSudo: true };
107
112
  }
108
- if (osRelease.includes('Fedora') || osRelease.includes('Red Hat') || osRelease.includes('CentOS') || osRelease.includes('Rocky')) {
113
+ if (
114
+ osRelease.includes('Fedora') ||
115
+ osRelease.includes('Red Hat') ||
116
+ osRelease.includes('CentOS') ||
117
+ osRelease.includes('Rocky')
118
+ ) {
109
119
  return { os: 'Fedora/RHEL', installCmd: 'sudo dnf install tmux', hasSudo: true };
110
120
  }
111
121
  if (osRelease.includes('Arch')) {
@@ -662,15 +672,43 @@ function compareVersions(a, b) {
662
672
  * These are the options that can be configured through /agileflow:configure
663
673
  */
664
674
  const ALL_CONFIG_OPTIONS = {
665
- claudeMdReinforcement: { since: '2.92.0', description: 'Add /babysit rules to CLAUDE.md', autoApplyable: true },
666
- sessionStartHook: { since: '2.35.0', description: 'Welcome display on session start', autoApplyable: false },
667
- precompactHook: { since: '2.40.0', description: 'Context preservation during /compact', autoApplyable: false },
668
- damageControlHooks: { since: '2.50.0', description: 'Block destructive commands', autoApplyable: false },
675
+ claudeMdReinforcement: {
676
+ since: '2.92.0',
677
+ description: 'Add /babysit rules to CLAUDE.md',
678
+ autoApplyable: true,
679
+ },
680
+ sessionStartHook: {
681
+ since: '2.35.0',
682
+ description: 'Welcome display on session start',
683
+ autoApplyable: false,
684
+ },
685
+ precompactHook: {
686
+ since: '2.40.0',
687
+ description: 'Context preservation during /compact',
688
+ autoApplyable: false,
689
+ },
690
+ damageControlHooks: {
691
+ since: '2.50.0',
692
+ description: 'Block destructive commands',
693
+ autoApplyable: false,
694
+ },
669
695
  statusLine: { since: '2.35.0', description: 'Custom status bar display', autoApplyable: false },
670
- autoArchival: { since: '2.35.0', description: 'Auto-archive completed stories', autoApplyable: false },
671
- autoUpdate: { since: '2.70.0', description: 'Auto-update on session start', autoApplyable: false },
696
+ autoArchival: {
697
+ since: '2.35.0',
698
+ description: 'Auto-archive completed stories',
699
+ autoApplyable: false,
700
+ },
701
+ autoUpdate: {
702
+ since: '2.70.0',
703
+ description: 'Auto-update on session start',
704
+ autoApplyable: false,
705
+ },
672
706
  ralphLoop: { since: '2.60.0', description: 'Autonomous story processing', autoApplyable: false },
673
- tmuxAutoSpawn: { since: '2.92.0', description: 'Auto-start Claude in tmux session', autoApplyable: true },
707
+ tmuxAutoSpawn: {
708
+ since: '2.92.0',
709
+ description: 'Auto-start Claude in tmux session',
710
+ autoApplyable: true,
711
+ },
674
712
  };
675
713
 
676
714
  /**
@@ -719,7 +757,10 @@ function checkConfigStaleness(rootDir, currentVersion, cache = null) {
719
757
  // Check for unconfigured options in metadata
720
758
  for (const [name, option] of Object.entries(configOptions)) {
721
759
  if (option.configured === false) {
722
- const optionInfo = ALL_CONFIG_OPTIONS[name] || { description: name, autoApplyable: false };
760
+ const optionInfo = ALL_CONFIG_OPTIONS[name] || {
761
+ description: name,
762
+ autoApplyable: false,
763
+ };
723
764
  result.outdated = true;
724
765
  result.newOptionsCount++;
725
766
  result.newOptions.push({
@@ -776,22 +817,24 @@ function isOptionActuallyConfigured(optionName, hooks, settings) {
776
817
  case 'precompactHook':
777
818
  return hooks.PreCompact && hooks.PreCompact.length > 0;
778
819
  case 'damageControlHooks':
779
- return hooks.PreToolUse && hooks.PreToolUse.some(h =>
780
- h.hooks?.some(hk => hk.command?.includes('damage-control'))
820
+ return (
821
+ hooks.PreToolUse &&
822
+ hooks.PreToolUse.some(h => h.hooks?.some(hk => hk.command?.includes('damage-control')))
781
823
  );
782
824
  case 'statusLine':
783
825
  return settings.statusLine && settings.statusLine.command;
784
826
  case 'autoArchival':
785
827
  // Archival is tied to SessionStart hook running archive script
786
- return hooks.SessionStart && hooks.SessionStart.some(h =>
787
- h.hooks?.some(hk => hk.command?.includes('archive'))
828
+ return (
829
+ hooks.SessionStart &&
830
+ hooks.SessionStart.some(h => h.hooks?.some(hk => hk.command?.includes('archive')))
788
831
  );
789
832
  case 'autoUpdate':
790
833
  // Would need to check metadata for autoUpdate setting
791
834
  return false; // Default to not configured
792
835
  case 'ralphLoop':
793
- return hooks.Stop && hooks.Stop.some(h =>
794
- h.hooks?.some(hk => hk.command?.includes('ralph-loop'))
836
+ return (
837
+ hooks.Stop && hooks.Stop.some(h => h.hooks?.some(hk => hk.command?.includes('ralph-loop')))
795
838
  );
796
839
  case 'claudeMdReinforcement':
797
840
  // Check if CLAUDE.md has the marker - can't easily check from here
@@ -848,7 +891,10 @@ ${marker}
848
891
  if (fs.existsSync(metadataPath)) {
849
892
  const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
850
893
  if (!metadata.features) metadata.features = {};
851
- if (!metadata.features.tmuxAutoSpawn || metadata.features.tmuxAutoSpawn.enabled === undefined) {
894
+ if (
895
+ !metadata.features.tmuxAutoSpawn ||
896
+ metadata.features.tmuxAutoSpawn.enabled === undefined
897
+ ) {
852
898
  metadata.features.tmuxAutoSpawn = {
853
899
  enabled: true,
854
900
  version: metadata.version || '2.92.0',
@@ -1636,24 +1682,32 @@ async function main() {
1636
1682
  // Show config auto-apply confirmation (for "full" profile)
1637
1683
  if (configAutoApplied > 0) {
1638
1684
  console.log('');
1639
- console.log(`${c.mintGreen}✨ Auto-applied ${configAutoApplied} new config option(s)${c.reset}`);
1685
+ console.log(
1686
+ `${c.mintGreen}✨ Auto-applied ${configAutoApplied} new config option(s)${c.reset}`
1687
+ );
1640
1688
  console.log(` ${c.slate}Profile "full" enables all new features automatically.${c.reset}`);
1641
1689
  }
1642
1690
 
1643
1691
  // Show config staleness notification (for custom profiles)
1644
1692
  if (configStaleness.outdated && configStaleness.newOptionsCount > 0) {
1645
1693
  console.log('');
1646
- console.log(`${c.amber}⚙️ ${configStaleness.newOptionsCount} new configuration option(s) available${c.reset}`);
1694
+ console.log(
1695
+ `${c.amber}⚙️ ${configStaleness.newOptionsCount} new configuration option(s) available${c.reset}`
1696
+ );
1647
1697
  for (const opt of configStaleness.newOptions.slice(0, 3)) {
1648
1698
  console.log(` ${c.dim}• ${opt.description}${c.reset}`);
1649
1699
  }
1650
- console.log(` ${c.slate}Run ${c.skyBlue}/agileflow:configure${c.reset}${c.slate} to enable them.${c.reset}`);
1700
+ console.log(
1701
+ ` ${c.slate}Run ${c.skyBlue}/agileflow:configure${c.reset}${c.slate} to enable them.${c.reset}`
1702
+ );
1651
1703
  }
1652
1704
 
1653
1705
  // Show tmux installation notice if tmux auto-spawn is enabled but tmux not installed
1654
1706
  if (tmuxAutoSpawnEnabled && !tmuxCheck.available) {
1655
1707
  console.log('');
1656
- console.log(`${c.amber}📦 tmux not installed${c.reset} ${c.dim}(enables parallel sessions in one terminal)${c.reset}`);
1708
+ console.log(
1709
+ `${c.amber}📦 tmux not installed${c.reset} ${c.dim}(enables parallel sessions in one terminal)${c.reset}`
1710
+ );
1657
1711
 
1658
1712
  // Show platform-specific install command
1659
1713
  if (tmuxCheck.platform?.installCmd) {
@@ -1668,7 +1722,9 @@ async function main() {
1668
1722
  console.log(` ${c.dim}• Ubuntu:${c.reset} ${c.cyan}sudo apt install tmux${c.reset}`);
1669
1723
  console.log(` ${c.dim}• No sudo:${c.reset} ${c.cyan}${tmuxCheck.noSudoCmd}${c.reset}`);
1670
1724
  }
1671
- console.log(` ${c.dim}Or disable this notice: ${c.skyBlue}/agileflow:configure --disable=tmuxautospawn${c.reset}`);
1725
+ console.log(
1726
+ ` ${c.dim}Or disable this notice: ${c.skyBlue}/agileflow:configure --disable=tmuxautospawn${c.reset}`
1727
+ );
1672
1728
  }
1673
1729
 
1674
1730
  // Show warning and tip if other sessions are active (vibrant colors)
@@ -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
 
@@ -380,8 +385,157 @@ function getSession(sessionId) {
380
385
  };
381
386
  }
382
387
 
388
+ // Default worktree timeout (2 minutes)
389
+ const DEFAULT_WORKTREE_TIMEOUT_MS = 120000;
390
+
391
+ /**
392
+ * Display progress feedback during long operations.
393
+ * Returns a function to stop the progress indicator.
394
+ *
395
+ * @param {string} message - Progress message
396
+ * @returns {function} Stop function
397
+ */
398
+ function progressIndicator(message) {
399
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
400
+ let frameIndex = 0;
401
+ let elapsed = 0;
402
+
403
+ // For TTY (interactive terminal), show spinner
404
+ if (process.stderr.isTTY) {
405
+ const interval = setInterval(() => {
406
+ process.stderr.write(`\r${frames[frameIndex++ % frames.length]} ${message}`);
407
+ }, 80);
408
+ return () => {
409
+ clearInterval(interval);
410
+ process.stderr.write(`\r${' '.repeat(message.length + 2)}\r`);
411
+ };
412
+ }
413
+
414
+ // For non-TTY (Claude Code, piped output), emit periodic updates to stderr
415
+ process.stderr.write(`⏳ ${message}...\n`);
416
+ const interval = setInterval(() => {
417
+ elapsed += 10;
418
+ process.stderr.write(`⏳ Still working... (${elapsed}s elapsed)\n`);
419
+ }, 10000); // Update every 10 seconds
420
+
421
+ return () => {
422
+ clearInterval(interval);
423
+ };
424
+ }
425
+
426
+ /**
427
+ * Create a git worktree with timeout and progress feedback.
428
+ * Uses async spawn instead of spawnSync for timeout support.
429
+ *
430
+ * @param {string} worktreePath - Path for the new worktree
431
+ * @param {string} branchName - Branch name for the worktree
432
+ * @param {number} timeoutMs - Timeout in milliseconds
433
+ * @returns {Promise<{stdout: string, stderr: string}>}
434
+ */
435
+ function createWorktreeWithTimeout(worktreePath, branchName, timeoutMs = DEFAULT_WORKTREE_TIMEOUT_MS) {
436
+ return new Promise((resolve, reject) => {
437
+ let stdout = '';
438
+ let stderr = '';
439
+ let timedOut = false;
440
+
441
+ const proc = spawn('git', ['worktree', 'add', worktreePath, branchName], {
442
+ cwd: ROOT,
443
+ });
444
+
445
+ const timer = setTimeout(() => {
446
+ timedOut = true;
447
+ proc.kill('SIGTERM');
448
+ // Give it a moment to terminate gracefully, then SIGKILL
449
+ setTimeout(() => {
450
+ try {
451
+ proc.kill('SIGKILL');
452
+ } catch (e) {
453
+ // Process may have already exited
454
+ }
455
+ }, 1000);
456
+ }, timeoutMs);
457
+
458
+ proc.stdout.on('data', (data) => {
459
+ stdout += data.toString();
460
+ });
461
+
462
+ proc.stderr.on('data', (data) => {
463
+ stderr += data.toString();
464
+ });
465
+
466
+ proc.on('error', (err) => {
467
+ clearTimeout(timer);
468
+ reject(new Error(`Failed to spawn git: ${err.message}`));
469
+ });
470
+
471
+ proc.on('close', (code, signal) => {
472
+ clearTimeout(timer);
473
+
474
+ if (timedOut) {
475
+ reject(new Error(`Worktree creation timed out after ${timeoutMs / 1000}s. Try increasing timeout or check disk space.`));
476
+ return;
477
+ }
478
+
479
+ if (signal) {
480
+ reject(new Error(`Worktree creation was terminated by signal: ${signal}`));
481
+ return;
482
+ }
483
+
484
+ if (code === 0) {
485
+ resolve({ stdout, stderr });
486
+ } else {
487
+ reject(new Error(`Failed to create worktree: ${stderr || 'unknown error'}`));
488
+ }
489
+ });
490
+ });
491
+ }
492
+
493
+ /**
494
+ * Clean up partial state after failed worktree creation.
495
+ * Removes partial directory and prunes git worktree registry.
496
+ *
497
+ * @param {string} worktreePath - Path of the failed worktree
498
+ * @param {string} branchName - Branch name that was being used
499
+ * @param {boolean} branchCreatedByUs - Whether we created the branch
500
+ */
501
+ function cleanupFailedWorktree(worktreePath, branchName, branchCreatedByUs = false) {
502
+ // Remove partial worktree directory if it exists
503
+ if (fs.existsSync(worktreePath)) {
504
+ try {
505
+ fs.rmSync(worktreePath, { recursive: true, force: true });
506
+ process.stderr.write(`🧹 Cleaned up partial worktree directory\n`);
507
+ } catch (e) {
508
+ process.stderr.write(`⚠️ Could not remove partial directory: ${e.message}\n`);
509
+ }
510
+ }
511
+
512
+ // Prune git worktree registry to clean up any references
513
+ try {
514
+ spawnSync('git', ['worktree', 'prune'], { cwd: ROOT, encoding: 'utf8' });
515
+ } catch (e) {
516
+ // Non-fatal
517
+ }
518
+
519
+ // If we created the branch and the worktree failed, optionally clean up the branch too
520
+ // But only if it has no commits beyond the parent (i.e., we just created it)
521
+ if (branchCreatedByUs) {
522
+ try {
523
+ // Check if branch exists and has no unique commits
524
+ const result = spawnSync('git', ['branch', '-d', branchName], {
525
+ cwd: ROOT,
526
+ encoding: 'utf8',
527
+ });
528
+ if (result.status === 0) {
529
+ process.stderr.write(`🧹 Cleaned up unused branch: ${branchName}\n`);
530
+ }
531
+ } catch (e) {
532
+ // Non-fatal - branch may have commits or not exist
533
+ }
534
+ }
535
+ }
536
+
383
537
  // Create new session with worktree
384
- function createSession(options = {}) {
538
+ async function createSession(options = {}) {
385
539
  const registry = loadRegistry();
386
540
  const sessionId = String(registry.next_id);
387
541
  const projectName = registry.project_name;
@@ -426,6 +580,7 @@ function createSession(options = {}) {
426
580
  }
427
581
  );
428
582
 
583
+ let branchCreatedByUs = false;
429
584
  if (checkRef.status !== 0) {
430
585
  // Branch doesn't exist, create it
431
586
  const createBranch = spawnSync('git', ['branch', branchName], {
@@ -439,18 +594,25 @@ function createSession(options = {}) {
439
594
  error: `Failed to create branch: ${createBranch.stderr || 'unknown error'}`,
440
595
  };
441
596
  }
597
+ branchCreatedByUs = true;
442
598
  }
443
599
 
444
- // Create worktree (using spawnSync for safety)
445
- const createWorktree = spawnSync('git', ['worktree', 'add', worktreePath, branchName], {
446
- cwd: ROOT,
447
- encoding: 'utf8',
448
- });
600
+ // Get timeout from options (default: 2 minutes)
601
+ const timeoutMs = options.timeout || DEFAULT_WORKTREE_TIMEOUT_MS;
449
602
 
450
- if (createWorktree.status !== 0) {
603
+ // Create worktree with timeout and progress feedback
604
+ const stopProgress = progressIndicator('Creating worktree (this may take a while for large repos)');
605
+ try {
606
+ await createWorktreeWithTimeout(worktreePath, branchName, timeoutMs);
607
+ stopProgress();
608
+ process.stderr.write(`✓ Worktree created successfully\n`);
609
+ } catch (error) {
610
+ stopProgress();
611
+ // Clean up partial state
612
+ cleanupFailedWorktree(worktreePath, branchName, branchCreatedByUs);
451
613
  return {
452
614
  success: false,
453
- error: `Failed to create worktree: ${createWorktree.stderr || 'unknown error'}`,
615
+ error: error.message,
454
616
  };
455
617
  }
456
618
 
@@ -471,9 +633,10 @@ function createSession(options = {}) {
471
633
  }
472
634
  }
473
635
 
474
- // Copy Claude Code and AgileFlow config folders (gitignored contents won't copy with worktree)
636
+ // Copy Claude Code, AgileFlow config, and docs folders (gitignored contents won't copy with worktree)
475
637
  // Note: The folder may exist with some tracked files, but gitignored subfolders (commands/, agents/) won't be there
476
- const configFolders = ['.claude', '.agileflow'];
638
+ // docs/ contains gitignored state files like status.json, session-state.json that need to be shared
639
+ const configFolders = ['.claude', '.agileflow', 'docs'];
477
640
  const copiedFolders = [];
478
641
  for (const folder of configFolders) {
479
642
  const src = path.join(ROOT, folder);
@@ -1115,7 +1278,7 @@ function main() {
1115
1278
  case 'create': {
1116
1279
  const options = {};
1117
1280
  // SECURITY: Only accept whitelisted option keys
1118
- const allowedKeys = ['nickname', 'branch'];
1281
+ const allowedKeys = ['nickname', 'branch', 'timeout'];
1119
1282
  for (let i = 1; i < args.length; i++) {
1120
1283
  const arg = args[i];
1121
1284
  if (arg.startsWith('--')) {
@@ -1133,8 +1296,20 @@ function main() {
1133
1296
  }
1134
1297
  }
1135
1298
  }
1136
- const result = createSession(options);
1137
- console.log(JSON.stringify(result));
1299
+ // Parse timeout as number (milliseconds)
1300
+ if (options.timeout) {
1301
+ options.timeout = parseInt(options.timeout, 10);
1302
+ if (isNaN(options.timeout) || options.timeout < 1000) {
1303
+ console.log(JSON.stringify({ success: false, error: 'Timeout must be a number >= 1000 (milliseconds)' }));
1304
+ return;
1305
+ }
1306
+ }
1307
+ // Handle async createSession
1308
+ createSession(options).then(result => {
1309
+ console.log(JSON.stringify(result));
1310
+ }).catch(err => {
1311
+ console.log(JSON.stringify({ success: false, error: err.message }));
1312
+ });
1138
1313
  break;
1139
1314
  }
1140
1315
 
@@ -1448,7 +1623,7 @@ ${c.brand}${c.bold}Session Manager${c.reset} - Multi-session coordination for Cl
1448
1623
  ${c.cyan}Commands:${c.reset}
1449
1624
  register [nickname] Register current directory as a session
1450
1625
  unregister <id> Unregister a session (remove lock)
1451
- create [--nickname X] Create new session with git worktree
1626
+ create [--nickname X] [--timeout MS] Create session with worktree (default timeout: 120000ms)
1452
1627
  list [--json] List all sessions
1453
1628
  count Count other active sessions
1454
1629
  delete <id> [--remove-worktree] Delete session
@@ -296,10 +296,7 @@ function spawn(args) {
296
296
  });
297
297
 
298
298
  // Show what was copied
299
- const copied = [
300
- ...(result.envFilesCopied || []),
301
- ...(result.foldersCopied || []),
302
- ];
299
+ const copied = [...(result.envFilesCopied || []), ...(result.foldersCopied || [])];
303
300
  const copyInfo = copied.length ? dim(` (copied: ${copied.join(', ')})`) : '';
304
301
  console.log(success(` ✓ Session ${result.sessionId}: ${sessionSpec.nickname}${copyInfo}`));
305
302
  }
@@ -341,16 +338,22 @@ function spawn(args) {
341
338
  console.log(` ${c.cyan}No sudo?${c.reset} conda install -c conda-forge tmux`);
342
339
  console.log('');
343
340
  console.log(dim('Or use --no-tmux to get manual commands instead:'));
344
- console.log(` ${c.cyan}node spawn-parallel.js spawn --count ${createdSessions.length} --no-tmux${c.reset}`);
341
+ console.log(
342
+ ` ${c.cyan}node spawn-parallel.js spawn --count ${createdSessions.length} --no-tmux${c.reset}`
343
+ );
345
344
  console.log('');
346
- console.log(warning('Worktrees created but Claude not spawned. Install tmux or use --no-tmux.'));
345
+ console.log(
346
+ warning('Worktrees created but Claude not spawned. Install tmux or use --no-tmux.')
347
+ );
347
348
  }
348
349
 
349
350
  // Summary
350
351
  console.log(bold('\n📊 Session Summary:'));
351
352
  console.log(dim('─'.repeat(50)));
352
353
  for (const session of createdSessions) {
353
- console.log(` ${c.cyan}${session.sessionId}${c.reset} │ ${session.nickname} │ ${dim(session.branch)}`);
354
+ console.log(
355
+ ` ${c.cyan}${session.sessionId}${c.reset} │ ${session.nickname} │ ${dim(session.branch)}`
356
+ );
354
357
  }
355
358
  console.log(dim('─'.repeat(50)));
356
359
  console.log(`${c.cyan}Use /agileflow:session:status to view all sessions.${c.reset}`);
@@ -397,7 +400,9 @@ function addWindow(args) {
397
400
  const tmuxEnv = process.env.TMUX;
398
401
  if (!tmuxEnv) {
399
402
  console.log(error('\n❌ Not in a tmux session.\n'));
400
- console.log(`${c.cyan}Use /agileflow:session:spawn to create a new tmux session first.${c.reset}`);
403
+ console.log(
404
+ `${c.cyan}Use /agileflow:session:spawn to create a new tmux session first.${c.reset}`
405
+ );
401
406
  console.log(`${dim('Or run: node .agileflow/scripts/spawn-parallel.js spawn --count 1')}`);
402
407
  return { success: false, error: 'Not in tmux' };
403
408
  }
@@ -436,9 +441,13 @@ function addWindow(args) {
436
441
  const cmd = buildClaudeCommand(result.path, {});
437
442
 
438
443
  // Create new window in current tmux session
439
- const newWindowResult = spawnSync('tmux', ['new-window', '-t', currentSession, '-n', windowName], {
440
- encoding: 'utf8',
441
- });
444
+ const newWindowResult = spawnSync(
445
+ 'tmux',
446
+ ['new-window', '-t', currentSession, '-n', windowName],
447
+ {
448
+ encoding: 'utf8',
449
+ }
450
+ );
442
451
 
443
452
  if (newWindowResult.status !== 0) {
444
453
  console.error(error(`Failed to create tmux window: ${newWindowResult.stderr}`));
@@ -453,10 +462,13 @@ function addWindow(args) {
453
462
  // Get window number
454
463
  let windowIndex;
455
464
  try {
456
- windowIndex = execSync(`tmux list-windows -t ${currentSession} -F "#I:#W" | grep ":${windowName}$" | cut -d: -f1`, {
457
- encoding: 'utf8',
458
- stdio: ['pipe', 'pipe', 'pipe'],
459
- }).trim();
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();
460
472
  } catch {
461
473
  windowIndex = '?';
462
474
  }
@@ -493,7 +505,10 @@ function killAll() {
493
505
  stdio: ['pipe', 'pipe', 'pipe'],
494
506
  });
495
507
 
496
- const sessions = result.trim().split('\n').filter(s => s.startsWith('claude-parallel-'));
508
+ const sessions = result
509
+ .trim()
510
+ .split('\n')
511
+ .filter(s => s.startsWith('claude-parallel-'));
497
512
 
498
513
  if (sessions.length === 0) {
499
514
  console.log(`${c.cyan}No claude-parallel tmux sessions found.${c.reset}`);
@@ -226,11 +226,30 @@ To switch to this session, run:
226
226
  - One short command to type
227
227
  - Immediately enables file access to the new session directory
228
228
 
229
+ ## Worktree Creation Timeout
230
+
231
+ By default, worktree creation has a 2-minute (120000ms) timeout. For large repositories with many files, you can increase this:
232
+
233
+ ```bash
234
+ # Increase timeout to 5 minutes (300000ms)
235
+ node .agileflow/scripts/session-manager.js create --timeout 300000
236
+ ```
237
+
238
+ During worktree creation, progress feedback is displayed:
239
+ - **TTY (terminal)**: Animated spinner
240
+ - **Non-TTY (Claude Code)**: Periodic "still working" messages every 10 seconds
241
+
242
+ If worktree creation times out or fails, the script automatically cleans up:
243
+ - Removes any partial worktree directory
244
+ - Prunes git worktree registry
245
+ - Removes the branch if we just created it
246
+
229
247
  ## Error Handling
230
248
 
231
249
  - **Directory exists**: Suggest different name or manual cleanup
232
250
  - **Branch conflict**: Offer to use existing branch or create new one
233
251
  - **Git errors**: Display error message and suggest manual resolution
252
+ - **Timeout**: Suggest increasing timeout for large repos
234
253
 
235
254
  ## Related Commands
236
255
 
@@ -185,7 +185,12 @@ ${claudeMdMarker}
185
185
 
186
186
  // Update metadata with config tracking
187
187
  try {
188
- const metadataPath = path.join(config.directory, config.docsFolder, '00-meta', 'agileflow-metadata.json');
188
+ const metadataPath = path.join(
189
+ config.directory,
190
+ config.docsFolder,
191
+ '00-meta',
192
+ 'agileflow-metadata.json'
193
+ );
189
194
  if (fs.existsSync(metadataPath)) {
190
195
  const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
191
196
  const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
@@ -223,8 +228,12 @@ ${claudeMdMarker}
223
228
  // Shell alias reload reminder
224
229
  if (coreResult.shellAliases?.configured?.length > 0) {
225
230
  console.log(chalk.bold('\nShell aliases:'));
226
- info(`Reload shell to use: ${chalk.cyan('source ~/.bashrc')} or ${chalk.cyan('source ~/.zshrc')}`);
227
- info(`Then run ${chalk.cyan('af')} to start Claude in tmux (or ${chalk.cyan('claude')} for normal)`);
231
+ info(
232
+ `Reload shell to use: ${chalk.cyan('source ~/.bashrc')} or ${chalk.cyan('source ~/.zshrc')}`
233
+ );
234
+ info(
235
+ `Then run ${chalk.cyan('af')} to start Claude in tmux (or ${chalk.cyan('claude')} for normal)`
236
+ );
228
237
  }
229
238
 
230
239
  console.log(chalk.dim(`\nInstalled to: ${coreResult.path}\n`));
@@ -15,7 +15,7 @@ const { BaseIdeSetup } = require('./_base-ide');
15
15
  */
16
16
  class WindsurfSetup extends BaseIdeSetup {
17
17
  constructor() {
18
- super('windsurf', 'Windsurf', true);
18
+ super('windsurf', 'Windsurf', false);
19
19
  this.configDir = '.windsurf';
20
20
  this.workflowsDir = 'workflows';
21
21
  }
@@ -83,7 +83,7 @@ const IDE_REGISTRY = {
83
83
  commandsSubdir: 'workflows',
84
84
  agileflowFolder: 'agileflow',
85
85
  targetSubdir: 'workflows/agileflow', // lowercase
86
- preferred: true,
86
+ preferred: false,
87
87
  description: "Codeium's AI IDE",
88
88
  handler: 'WindsurfSetup',
89
89
  labels: {
@@ -287,9 +287,7 @@ class IdeRegistry {
287
287
  */
288
288
  static getLabels(ideName) {
289
289
  const ide = IDE_REGISTRY[ideName];
290
- return ide && ide.labels
291
- ? ide.labels
292
- : { commands: 'commands', agents: 'agents' };
290
+ return ide && ide.labels ? ide.labels : { commands: 'commands', agents: 'agents' };
293
291
  }
294
292
  }
295
293
 
@@ -174,7 +174,8 @@ async function promptInstall() {
174
174
  {
175
175
  type: 'confirm',
176
176
  name: 'claudeMdReinforcement',
177
- message: 'Add /babysit AskUserQuestion rules to CLAUDE.md? (recommended for context preservation)',
177
+ message:
178
+ 'Add /babysit AskUserQuestion rules to CLAUDE.md? (recommended for context preservation)',
178
179
  default: true,
179
180
  },
180
181
  ]);