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 +5 -0
- package/README.md +6 -6
- package/lib/codebase-indexer.js +2 -1
- package/package.json +1 -1
- package/scripts/agileflow-welcome.js +78 -22
- package/scripts/lib/configure-features.js +8 -1
- package/scripts/lib/context-loader.js +16 -16
- package/scripts/query-codebase.js +8 -3
- package/scripts/session-manager.js +191 -16
- package/scripts/spawn-parallel.js +31 -16
- package/src/core/commands/session/new.md +19 -0
- package/tools/cli/commands/setup.js +12 -3
- package/tools/cli/installers/ide/windsurf.js +1 -1
- package/tools/cli/lib/ide-registry.js +2 -4
- package/tools/cli/lib/ui.js +2 -1
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
|
[](https://www.npmjs.com/package/agileflow)
|
|
6
|
-
[](docs/04-architecture/commands.md)
|
|
7
|
+
[](docs/04-architecture/subagents.md)
|
|
8
8
|
[](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) |
|
|
69
|
-
| [Agents/Experts](docs/04-architecture/subagents.md) |
|
|
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
|
|
80
|
-
- [Agents/Experts](docs/04-architecture/subagents.md) -
|
|
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
|
package/lib/codebase-indexer.js
CHANGED
|
@@ -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))
|
|
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
|
@@ -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 (
|
|
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 (
|
|
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: {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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: {
|
|
671
|
-
|
|
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: {
|
|
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] || {
|
|
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
|
|
780
|
-
|
|
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
|
|
787
|
-
|
|
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
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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: [
|
|
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
|
-
/\|/,
|
|
67
|
-
/;/,
|
|
68
|
-
/&&/,
|
|
69
|
-
/\|\|/,
|
|
70
|
-
/`/,
|
|
71
|
-
/\$\(/,
|
|
72
|
-
/>/,
|
|
73
|
-
/</,
|
|
74
|
-
/\bsudo\b/,
|
|
75
|
-
/\brm\b/,
|
|
76
|
-
/\bmv\b/,
|
|
77
|
-
/\bcp\b/,
|
|
78
|
-
/\bchmod\b/,
|
|
79
|
-
/\bchown\b/,
|
|
80
|
-
/\bcurl\b/,
|
|
81
|
-
/\bwget\b/,
|
|
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(
|
|
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(
|
|
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 {
|
|
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
|
-
//
|
|
445
|
-
const
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1137
|
-
|
|
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]
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
440
|
-
|
|
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(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
|
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(
|
|
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(
|
|
227
|
-
|
|
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',
|
|
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:
|
|
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
|
|
package/tools/cli/lib/ui.js
CHANGED
|
@@ -174,7 +174,8 @@ async function promptInstall() {
|
|
|
174
174
|
{
|
|
175
175
|
type: 'confirm',
|
|
176
176
|
name: 'claudeMdReinforcement',
|
|
177
|
-
message:
|
|
177
|
+
message:
|
|
178
|
+
'Add /babysit AskUserQuestion rules to CLAUDE.md? (recommended for context preservation)',
|
|
178
179
|
default: true,
|
|
179
180
|
},
|
|
180
181
|
]);
|