agileflow 2.93.0 → 2.94.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.
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.94.0] - 2026-01-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Shared docs/ across sessions via symlink for multi-session coordination
|
|
14
|
+
|
|
10
15
|
## [2.93.0] - 2026-01-24
|
|
11
16
|
|
|
12
17
|
### Added
|
package/package.json
CHANGED
|
@@ -1756,11 +1756,10 @@ async function main() {
|
|
|
1756
1756
|
// === SESSION HEALTH WARNINGS ===
|
|
1757
1757
|
// Check for forgotten sessions with uncommitted changes, stale sessions, orphaned entries
|
|
1758
1758
|
try {
|
|
1759
|
-
const healthResult = spawnSync(
|
|
1760
|
-
'
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
);
|
|
1759
|
+
const healthResult = spawnSync('node', [SESSION_MANAGER_PATH, 'health'], {
|
|
1760
|
+
encoding: 'utf8',
|
|
1761
|
+
timeout: 10000,
|
|
1762
|
+
});
|
|
1764
1763
|
|
|
1765
1764
|
if (healthResult.stdout) {
|
|
1766
1765
|
const health = JSON.parse(healthResult.stdout);
|
|
@@ -1777,14 +1776,12 @@ async function main() {
|
|
|
1777
1776
|
console.log(
|
|
1778
1777
|
`${c.coral}⚠️ ${health.uncommitted.length} session(s) have uncommitted changes:${c.reset}`
|
|
1779
1778
|
);
|
|
1780
|
-
health.uncommitted.slice(0, 3).forEach(
|
|
1779
|
+
health.uncommitted.slice(0, 3).forEach(sess => {
|
|
1781
1780
|
const name = sess.nickname ? `"${sess.nickname}"` : `Session ${sess.id}`;
|
|
1782
1781
|
console.log(`${c.dim} └─ ${name}: ${sess.changeCount} file(s)${c.reset}`);
|
|
1783
1782
|
});
|
|
1784
1783
|
if (health.uncommitted.length > 3) {
|
|
1785
|
-
console.log(
|
|
1786
|
-
`${c.dim} └─ ... and ${health.uncommitted.length - 3} more${c.reset}`
|
|
1787
|
-
);
|
|
1784
|
+
console.log(`${c.dim} └─ ... and ${health.uncommitted.length - 3} more${c.reset}`);
|
|
1788
1785
|
}
|
|
1789
1786
|
console.log(
|
|
1790
1787
|
`${c.slate} Run: ${c.skyBlue}/agileflow:session:status${c.slate} to see details${c.reset}`
|
|
@@ -238,7 +238,7 @@ async function cleanupStaleLocksAsync(registry, options = {}) {
|
|
|
238
238
|
* @returns {Object[]} Array of file details with analysis
|
|
239
239
|
*/
|
|
240
240
|
function getFileDetails(sessionPath, changes) {
|
|
241
|
-
return changes.map(
|
|
241
|
+
return changes.map(change => {
|
|
242
242
|
const status = change.substring(0, 2).trim();
|
|
243
243
|
const file = change.substring(3);
|
|
244
244
|
|
|
@@ -294,8 +294,8 @@ function getSessionsHealth(options = {}) {
|
|
|
294
294
|
const staleThreshold = staleDays * 24 * 60 * 60 * 1000;
|
|
295
295
|
|
|
296
296
|
const health = {
|
|
297
|
-
stale: [],
|
|
298
|
-
uncommitted: [],
|
|
297
|
+
stale: [], // Sessions with no activity > staleDays
|
|
298
|
+
uncommitted: [], // Sessions with uncommitted git changes
|
|
299
299
|
orphanedRegistry: [], // Registry entries where path doesn't exist
|
|
300
300
|
orphanedWorktrees: [], // Worktrees not in registry
|
|
301
301
|
healthy: 0,
|
|
@@ -333,7 +333,7 @@ function getSessionsHealth(options = {}) {
|
|
|
333
333
|
if (result.stdout && result.stdout.trim()) {
|
|
334
334
|
// Don't use trim() on the whole string - it removes leading space from first status
|
|
335
335
|
// Split by newline and filter empty lines instead
|
|
336
|
-
const changes = result.stdout.split('\n').filter(
|
|
336
|
+
const changes = result.stdout.split('\n').filter(line => line.length > 0);
|
|
337
337
|
const sessionData = {
|
|
338
338
|
id,
|
|
339
339
|
...session,
|
|
@@ -345,7 +345,7 @@ function getSessionsHealth(options = {}) {
|
|
|
345
345
|
if (detailed) {
|
|
346
346
|
sessionData.fileDetails = getFileDetails(session.path, changes);
|
|
347
347
|
// Calculate if session is safe to delete (all changes trivial)
|
|
348
|
-
sessionData.allTrivial = sessionData.fileDetails.every(
|
|
348
|
+
sessionData.allTrivial = sessionData.fileDetails.every(f => f.trivial);
|
|
349
349
|
}
|
|
350
350
|
|
|
351
351
|
health.uncommitted.push(sessionData);
|
|
@@ -365,12 +365,12 @@ function getSessionsHealth(options = {}) {
|
|
|
365
365
|
if (worktreeList.stdout) {
|
|
366
366
|
const worktrees = worktreeList.stdout
|
|
367
367
|
.split('\n')
|
|
368
|
-
.filter(
|
|
369
|
-
.map(
|
|
368
|
+
.filter(line => line.startsWith('worktree '))
|
|
369
|
+
.map(line => line.replace('worktree ', ''));
|
|
370
370
|
|
|
371
371
|
const mainPath = ROOT;
|
|
372
372
|
for (const wtPath of worktrees) {
|
|
373
|
-
const inRegistry = Object.values(registry.sessions).some(
|
|
373
|
+
const inRegistry = Object.values(registry.sessions).some(s => s.path === wtPath);
|
|
374
374
|
if (!inRegistry && wtPath !== mainPath) {
|
|
375
375
|
// Check if it's an AgileFlow worktree (has .agileflow folder)
|
|
376
376
|
if (fs.existsSync(path.join(wtPath, '.agileflow'))) {
|
|
@@ -798,12 +798,11 @@ async function createSession(options = {}) {
|
|
|
798
798
|
}
|
|
799
799
|
}
|
|
800
800
|
|
|
801
|
-
// Copy Claude Code
|
|
801
|
+
// Copy Claude Code and AgileFlow config folders (gitignored contents won't copy with worktree)
|
|
802
802
|
// Note: The folder may exist with some tracked files, but gitignored subfolders (commands/, agents/) won't be there
|
|
803
|
-
|
|
804
|
-
const configFolders = ['.claude', '.agileflow', 'docs'];
|
|
803
|
+
const configFoldersToCopy = ['.claude', '.agileflow'];
|
|
805
804
|
const copiedFolders = [];
|
|
806
|
-
for (const folder of
|
|
805
|
+
for (const folder of configFoldersToCopy) {
|
|
807
806
|
const src = path.join(ROOT, folder);
|
|
808
807
|
const dest = path.join(worktreePath, folder);
|
|
809
808
|
if (fs.existsSync(src)) {
|
|
@@ -818,6 +817,37 @@ async function createSession(options = {}) {
|
|
|
818
817
|
}
|
|
819
818
|
}
|
|
820
819
|
|
|
820
|
+
// Symlink docs/ to main project docs (shared state: status.json, session-state.json, bus/)
|
|
821
|
+
// This enables story claiming, status bus, and session coordination across worktrees
|
|
822
|
+
const foldersToSymlink = ['docs'];
|
|
823
|
+
const symlinkedFolders = [];
|
|
824
|
+
for (const folder of foldersToSymlink) {
|
|
825
|
+
const src = path.join(ROOT, folder);
|
|
826
|
+
const dest = path.join(worktreePath, folder);
|
|
827
|
+
if (fs.existsSync(src)) {
|
|
828
|
+
try {
|
|
829
|
+
// Remove if exists (worktree may have empty/partial tracked folder)
|
|
830
|
+
if (fs.existsSync(dest)) {
|
|
831
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Create relative symlink (works across project moves)
|
|
835
|
+
const relPath = path.relative(worktreePath, src);
|
|
836
|
+
fs.symlinkSync(relPath, dest, 'dir');
|
|
837
|
+
symlinkedFolders.push(folder);
|
|
838
|
+
} catch (e) {
|
|
839
|
+
// Fallback to copy if symlink fails (e.g., Windows without dev mode)
|
|
840
|
+
console.warn(`Warning: Could not symlink ${folder}, copying instead: ${e.message}`);
|
|
841
|
+
try {
|
|
842
|
+
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
843
|
+
copiedFolders.push(folder);
|
|
844
|
+
} catch (copyErr) {
|
|
845
|
+
console.warn(`Warning: Could not copy ${folder}: ${copyErr.message}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
821
851
|
// Register session - worktree sessions are always parallel threads
|
|
822
852
|
registry.next_id++;
|
|
823
853
|
registry.sessions[sessionId] = {
|
|
@@ -842,6 +872,7 @@ async function createSession(options = {}) {
|
|
|
842
872
|
command: `cd "${worktreePath}" && claude`,
|
|
843
873
|
envFilesCopied: copiedEnvFiles,
|
|
844
874
|
foldersCopied: copiedFolders,
|
|
875
|
+
foldersSymlinked: symlinkedFolders,
|
|
845
876
|
};
|
|
846
877
|
}
|
|
847
878
|
|
|
@@ -1536,7 +1567,7 @@ function main() {
|
|
|
1536
1567
|
case 'health': {
|
|
1537
1568
|
// Get health status for all sessions
|
|
1538
1569
|
// Usage: health [staleDays] [--detailed]
|
|
1539
|
-
const staleDaysArg = args.find(
|
|
1570
|
+
const staleDaysArg = args.find(a => /^\d+$/.test(a));
|
|
1540
1571
|
const staleDays = staleDaysArg ? parseInt(staleDaysArg, 10) : 7;
|
|
1541
1572
|
const detailed = args.includes('--detailed');
|
|
1542
1573
|
const health = getSessionsHealth({ staleDays, detailed });
|
|
@@ -64,7 +64,13 @@ function hasScreen() {
|
|
|
64
64
|
* Build the Claude command for a session
|
|
65
65
|
*/
|
|
66
66
|
function buildClaudeCommand(sessionPath, options = {}) {
|
|
67
|
-
const {
|
|
67
|
+
const {
|
|
68
|
+
init = false,
|
|
69
|
+
dangerous = false,
|
|
70
|
+
prompt = null,
|
|
71
|
+
claudeArgs = null,
|
|
72
|
+
noClaude = false,
|
|
73
|
+
} = options;
|
|
68
74
|
const parts = [`cd "${sessionPath}"`];
|
|
69
75
|
|
|
70
76
|
if (init) {
|
|
@@ -324,7 +330,13 @@ async function spawn(args) {
|
|
|
324
330
|
outputCommands(createdSessions, { init, dangerous, prompt, claudeArgs, noClaude });
|
|
325
331
|
} else if (hasTmux()) {
|
|
326
332
|
// Tmux available - use it
|
|
327
|
-
const tmuxResult = spawnInTmux(createdSessions, {
|
|
333
|
+
const tmuxResult = spawnInTmux(createdSessions, {
|
|
334
|
+
init,
|
|
335
|
+
dangerous,
|
|
336
|
+
prompt,
|
|
337
|
+
claudeArgs,
|
|
338
|
+
noClaude,
|
|
339
|
+
});
|
|
328
340
|
|
|
329
341
|
if (tmuxResult.success) {
|
|
330
342
|
console.log(success(`\n✅ Tmux session created: ${tmuxResult.sessionName}`));
|
|
@@ -711,9 +711,11 @@ function getPlaceholderDocs() {
|
|
|
711
711
|
'<!-- {{COMMAND_LIST}} -->': 'Full formatted command list',
|
|
712
712
|
},
|
|
713
713
|
templates: {
|
|
714
|
-
'<!-- {{SESSION_HARNESS}} -->':
|
|
714
|
+
'<!-- {{SESSION_HARNESS}} -->':
|
|
715
|
+
'Session harness protocol (auto-detects agent ID from frontmatter)',
|
|
715
716
|
'<!-- {{SESSION_HARNESS:AG-API}} -->': 'Session harness protocol with explicit agent ID',
|
|
716
|
-
'<!-- {{QUALITY_GATE_PRIORITIES}} -->':
|
|
717
|
+
'<!-- {{QUALITY_GATE_PRIORITIES}} -->':
|
|
718
|
+
'Quality gate priorities with CRITICAL/HIGH/MEDIUM levels',
|
|
717
719
|
},
|
|
718
720
|
preserve_rules: {
|
|
719
721
|
'{{RULES:json_operations}}': 'Rules for safe JSON file modifications',
|