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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.93.0",
3
+ "version": "2.94.0",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -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
- 'node',
1761
- [SESSION_MANAGER_PATH, 'health'],
1762
- { encoding: 'utf8', timeout: 10000 }
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((sess) => {
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((change) => {
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: [], // Sessions with no activity > staleDays
298
- uncommitted: [], // Sessions with uncommitted git changes
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((line) => line.length > 0);
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((f) => f.trivial);
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((line) => line.startsWith('worktree '))
369
- .map((line) => line.replace('worktree ', ''));
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((s) => s.path === wtPath);
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, AgileFlow config, and docs folders (gitignored contents won't copy with worktree)
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
- // docs/ contains gitignored state files like status.json, session-state.json that need to be shared
804
- const configFolders = ['.claude', '.agileflow', 'docs'];
803
+ const configFoldersToCopy = ['.claude', '.agileflow'];
805
804
  const copiedFolders = [];
806
- for (const folder of configFolders) {
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((a) => /^\d+$/.test(a));
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 { init = false, dangerous = false, prompt = null, claudeArgs = null, noClaude = false } = options;
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, { init, dangerous, prompt, claudeArgs, noClaude });
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}} -->': 'Session harness protocol (auto-detects agent ID from frontmatter)',
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}} -->': 'Quality gate priorities with CRITICAL/HIGH/MEDIUM levels',
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',