agileflow 2.66.0 → 2.68.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/README.md CHANGED
@@ -15,16 +15,15 @@
15
15
 
16
16
  ### Installation
17
17
 
18
- **Global (Recommended):**
19
18
  ```bash
20
- npm install -g agileflow
21
- agileflow setup
19
+ npx agileflow@latest setup
22
20
  ```
23
21
 
24
- **Project-Level:**
22
+ That's it! The `npx` command always fetches the latest version.
23
+
24
+ **Updates:**
25
25
  ```bash
26
- npm install agileflow
27
- npx agileflow setup
26
+ npx agileflow@latest update
28
27
  ```
29
28
 
30
29
  ### After Setup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.66.0",
3
+ "version": "2.68.0",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -23,7 +23,7 @@
23
23
  * --detect Show current status
24
24
  * --help Show help
25
25
  *
26
- * Features: sessionstart, precompact, stop, archival, statusline, autoupdate
26
+ * Features: sessionstart, precompact, archival, statusline, autoupdate
27
27
  */
28
28
 
29
29
  const fs = require('fs');
@@ -39,7 +39,7 @@ const VERSION = '2.41.0';
39
39
  const FEATURES = {
40
40
  sessionstart: { hook: 'SessionStart', script: 'agileflow-welcome.js', type: 'node' },
41
41
  precompact: { hook: 'PreCompact', script: 'precompact-context.sh', type: 'bash' },
42
- stop: { hook: 'Stop', script: 'agileflow-stop.sh', type: 'bash' },
42
+ // Note: Stop hook removed due to Claude Code reliability issues (see GitHub issues #6974, #11544)
43
43
  archival: { script: 'archive-completed-stories.sh', requiresHook: 'sessionstart' },
44
44
  statusline: { script: 'agileflow-statusline.sh' },
45
45
  autoupdate: { metadataOnly: true }, // Stored in metadata.updates.autoUpdate
@@ -60,24 +60,24 @@ const STATUSLINE_COMPONENTS = [
60
60
  const PROFILES = {
61
61
  full: {
62
62
  description: 'All features enabled',
63
- enable: ['sessionstart', 'precompact', 'stop', 'archival', 'statusline'],
63
+ enable: ['sessionstart', 'precompact', 'archival', 'statusline'],
64
64
  archivalDays: 7,
65
65
  },
66
66
  basic: {
67
67
  description: 'Essential hooks + archival (SessionStart + PreCompact + Archival)',
68
68
  enable: ['sessionstart', 'precompact', 'archival'],
69
- disable: ['stop', 'statusline'],
69
+ disable: ['statusline'],
70
70
  archivalDays: 7,
71
71
  },
72
72
  minimal: {
73
73
  description: 'SessionStart + archival only',
74
74
  enable: ['sessionstart', 'archival'],
75
- disable: ['precompact', 'stop', 'statusline'],
75
+ disable: ['precompact', 'statusline'],
76
76
  archivalDays: 7,
77
77
  },
78
78
  none: {
79
79
  description: 'Disable all AgileFlow features',
80
- disable: ['sessionstart', 'precompact', 'stop', 'archival', 'statusline'],
80
+ disable: ['sessionstart', 'precompact', 'archival', 'statusline'],
81
81
  },
82
82
  };
83
83
 
@@ -154,7 +154,6 @@ function detectConfig() {
154
154
  features: {
155
155
  sessionstart: { enabled: false, valid: true, issues: [] },
156
156
  precompact: { enabled: false, valid: true, issues: [] },
157
- stop: { enabled: false, valid: true, issues: [] },
158
157
  archival: { enabled: false, threshold: null },
159
158
  statusline: { enabled: false, valid: true, issues: [] },
160
159
  },
@@ -221,23 +220,7 @@ function detectConfig() {
221
220
  }
222
221
  }
223
222
 
224
- // Stop
225
- if (settings.hooks.Stop) {
226
- if (Array.isArray(settings.hooks.Stop) && settings.hooks.Stop.length > 0) {
227
- const hook = settings.hooks.Stop[0];
228
- if (hook.matcher !== undefined && hook.hooks) {
229
- status.features.stop.enabled = true;
230
- } else {
231
- status.features.stop.enabled = true;
232
- status.features.stop.valid = false;
233
- status.features.stop.issues.push('Old format - needs migration');
234
- }
235
- } else if (typeof settings.hooks.Stop === 'string') {
236
- status.features.stop.enabled = true;
237
- status.features.stop.valid = false;
238
- status.features.stop.issues.push('String format - needs migration');
239
- }
240
- }
223
+ // Note: Stop hook removed due to reliability issues
241
224
  }
242
225
 
243
226
  // StatusLine
@@ -313,7 +296,6 @@ function printStatus(status) {
313
296
 
314
297
  printFeature('sessionstart', 'SessionStart Hook');
315
298
  printFeature('precompact', 'PreCompact Hook');
316
- printFeature('stop', 'Stop Hook');
317
299
 
318
300
  const arch = status.features.archival;
319
301
  log(
@@ -357,9 +339,9 @@ function migrateSettings() {
357
339
 
358
340
  let migrated = false;
359
341
 
360
- // Migrate hooks
342
+ // Migrate hooks (Stop hook removed due to reliability issues)
361
343
  if (settings.hooks) {
362
- ['SessionStart', 'PreCompact', 'Stop', 'UserPromptSubmit'].forEach(hookName => {
344
+ ['SessionStart', 'PreCompact', 'UserPromptSubmit'].forEach(hookName => {
363
345
  const hook = settings.hooks[hookName];
364
346
  if (!hook) return;
365
347
 
@@ -472,15 +454,6 @@ function enableFeature(feature, options = {}) {
472
454
  );
473
455
  } else if (feature === 'precompact') {
474
456
  fs.writeFileSync(scriptPath, '#!/bin/bash\necho "PreCompact: preserving context"\n');
475
- } else if (feature === 'stop') {
476
- fs.writeFileSync(
477
- scriptPath,
478
- `#!/bin/bash
479
- git rev-parse --git-dir > /dev/null 2>&1 || exit 0
480
- CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
481
- [ "$CHANGES" -gt 0 ] && echo -e "\\n\\033[33m$CHANGES uncommitted change(s)\\033[0m"
482
- `
483
- );
484
457
  }
485
458
  try {
486
459
  fs.chmodSync(scriptPath, '755');
@@ -862,7 +835,7 @@ ${c.cyan}Feature Control:${c.reset}
862
835
  --enable=<list> Enable features (comma-separated)
863
836
  --disable=<list> Disable features (comma-separated)
864
837
 
865
- Features: sessionstart, precompact, stop, archival, statusline
838
+ Features: sessionstart, precompact, archival, statusline
866
839
 
867
840
  ${c.cyan}Statusline Components:${c.reset}
868
841
  --show=<list> Show statusline components (comma-separated)
@@ -884,7 +857,7 @@ ${c.cyan}Examples:${c.reset}
884
857
  node scripts/agileflow-configure.js --profile=full
885
858
 
886
859
  # Enable specific features
887
- node scripts/agileflow-configure.js --enable=sessionstart,precompact,stop
860
+ node scripts/agileflow-configure.js --enable=sessionstart,precompact,archival
888
861
 
889
862
  # Disable a feature
890
863
  node scripts/agileflow-configure.js --disable=statusline
@@ -86,15 +86,26 @@ function getProjectInfo(rootDir) {
86
86
  currentStory: null,
87
87
  };
88
88
 
89
- // Get package info
89
+ // Get AgileFlow version (check multiple sources in priority order)
90
+ // 1. AgileFlow metadata (installed user projects)
91
+ // 2. packages/cli/package.json (AgileFlow dev project)
92
+ // 3. .agileflow/package.json (fallback)
90
93
  try {
91
- const pkg = JSON.parse(
92
- fs.readFileSync(path.join(rootDir, 'packages/cli/package.json'), 'utf8')
93
- );
94
- info.version = pkg.version || info.version;
94
+ const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
95
+ if (fs.existsSync(metadataPath)) {
96
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
97
+ info.version = metadata.version || info.version;
98
+ } else {
99
+ // Dev project: check packages/cli/package.json
100
+ const pkg = JSON.parse(
101
+ fs.readFileSync(path.join(rootDir, 'packages/cli/package.json'), 'utf8')
102
+ );
103
+ info.version = pkg.version || info.version;
104
+ }
95
105
  } catch (e) {
106
+ // Fallback: check .agileflow/package.json
96
107
  try {
97
- const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'));
108
+ const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, '.agileflow/package.json'), 'utf8'));
98
109
  info.version = pkg.version || info.version;
99
110
  } catch (e2) {}
100
111
  }
@@ -11,7 +11,7 @@
11
11
  * - RECONFIGURE: Change settings (archival days, etc.)
12
12
  *
13
13
  * Usage:
14
- * node .agileflow/scripts/agileflow-configure.js [options]
14
+ * node scripts/agileflow-configure.js [options]
15
15
  *
16
16
  * Options:
17
17
  * --profile=full|basic|minimal|none Apply a preset
@@ -23,7 +23,7 @@
23
23
  * --detect Show current status
24
24
  * --help Show help
25
25
  *
26
- * Features: sessionstart, precompact, stop, archival, statusline
26
+ * Features: sessionstart, precompact, archival, statusline, autoupdate
27
27
  */
28
28
 
29
29
  const fs = require('fs');
@@ -39,36 +39,46 @@ const VERSION = '2.41.0';
39
39
  const FEATURES = {
40
40
  sessionstart: { hook: 'SessionStart', script: 'agileflow-welcome.js', type: 'node' },
41
41
  precompact: { hook: 'PreCompact', script: 'precompact-context.sh', type: 'bash' },
42
- stop: { hook: 'Stop', script: 'agileflow-stop.sh', type: 'bash' },
42
+ // Note: Stop hook removed due to Claude Code reliability issues (see GitHub issues #6974, #11544)
43
43
  archival: { script: 'archive-completed-stories.sh', requiresHook: 'sessionstart' },
44
- statusline: { script: 'agileflow-statusline.sh' }
44
+ statusline: { script: 'agileflow-statusline.sh' },
45
+ autoupdate: { metadataOnly: true }, // Stored in metadata.updates.autoUpdate
45
46
  };
46
47
 
47
48
  // Statusline component names
48
- const STATUSLINE_COMPONENTS = ['agileflow', 'model', 'story', 'epic', 'wip', 'context', 'cost', 'git'];
49
+ const STATUSLINE_COMPONENTS = [
50
+ 'agileflow',
51
+ 'model',
52
+ 'story',
53
+ 'epic',
54
+ 'wip',
55
+ 'context',
56
+ 'cost',
57
+ 'git',
58
+ ];
49
59
 
50
60
  const PROFILES = {
51
61
  full: {
52
62
  description: 'All features enabled',
53
- enable: ['sessionstart', 'precompact', 'stop', 'archival', 'statusline'],
54
- archivalDays: 7
63
+ enable: ['sessionstart', 'precompact', 'archival', 'statusline'],
64
+ archivalDays: 7,
55
65
  },
56
66
  basic: {
57
67
  description: 'Essential hooks + archival (SessionStart + PreCompact + Archival)',
58
68
  enable: ['sessionstart', 'precompact', 'archival'],
59
- disable: ['stop', 'statusline'],
60
- archivalDays: 7
69
+ disable: ['statusline'],
70
+ archivalDays: 7,
61
71
  },
62
72
  minimal: {
63
73
  description: 'SessionStart + archival only',
64
74
  enable: ['sessionstart', 'archival'],
65
- disable: ['precompact', 'stop', 'statusline'],
66
- archivalDays: 7
75
+ disable: ['precompact', 'statusline'],
76
+ archivalDays: 7,
67
77
  },
68
78
  none: {
69
79
  description: 'Disable all AgileFlow features',
70
- disable: ['sessionstart', 'precompact', 'stop', 'archival', 'statusline']
71
- }
80
+ disable: ['sessionstart', 'precompact', 'archival', 'statusline'],
81
+ },
72
82
  };
73
83
 
74
84
  // ============================================================================
@@ -76,8 +86,13 @@ const PROFILES = {
76
86
  // ============================================================================
77
87
 
78
88
  const c = {
79
- reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m',
80
- green: '\x1b[32m', yellow: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m'
89
+ reset: '\x1b[0m',
90
+ dim: '\x1b[2m',
91
+ bold: '\x1b[1m',
92
+ green: '\x1b[32m',
93
+ yellow: '\x1b[33m',
94
+ red: '\x1b[31m',
95
+ cyan: '\x1b[36m',
81
96
  };
82
97
 
83
98
  const log = (msg, color = '') => console.log(`${color}${msg}${c.reset}`);
@@ -91,11 +106,16 @@ const header = msg => log(`\n${msg}`, c.bold + c.cyan);
91
106
  // FILE UTILITIES
92
107
  // ============================================================================
93
108
 
94
- const ensureDir = dir => { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); };
109
+ const ensureDir = dir => {
110
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
111
+ };
95
112
 
96
113
  const readJSON = filePath => {
97
- try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
98
- catch { return null; }
114
+ try {
115
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
116
+ } catch {
117
+ return null;
118
+ }
99
119
  };
100
120
 
101
121
  const writeJSON = (filePath, data) => {
@@ -107,12 +127,14 @@ const copyTemplate = (templateName, destPath) => {
107
127
  const sources = [
108
128
  path.join(process.cwd(), '.agileflow', 'templates', templateName),
109
129
  path.join(__dirname, templateName),
110
- path.join(__dirname, '..', 'templates', templateName)
130
+ path.join(__dirname, '..', 'templates', templateName),
111
131
  ];
112
132
  for (const src of sources) {
113
133
  if (fs.existsSync(src)) {
114
134
  fs.copyFileSync(src, destPath);
115
- try { fs.chmodSync(destPath, '755'); } catch {}
135
+ try {
136
+ fs.chmodSync(destPath, '755');
137
+ } catch {}
116
138
  return true;
117
139
  }
118
140
  }
@@ -132,18 +154,19 @@ function detectConfig() {
132
154
  features: {
133
155
  sessionstart: { enabled: false, valid: true, issues: [] },
134
156
  precompact: { enabled: false, valid: true, issues: [] },
135
- stop: { enabled: false, valid: true, issues: [] },
136
157
  archival: { enabled: false, threshold: null },
137
- statusline: { enabled: false, valid: true, issues: [] }
158
+ statusline: { enabled: false, valid: true, issues: [] },
138
159
  },
139
- metadata: { exists: false, version: null }
160
+ metadata: { exists: false, version: null },
140
161
  };
141
162
 
142
163
  // Git
143
164
  if (fs.existsSync('.git')) {
144
165
  status.git.initialized = true;
145
166
  try {
146
- status.git.remote = execSync('git remote get-url origin 2>/dev/null', { encoding: 'utf8' }).trim();
167
+ status.git.remote = execSync('git remote get-url origin 2>/dev/null', {
168
+ encoding: 'utf8',
169
+ }).trim();
147
170
  } catch {}
148
171
  }
149
172
 
@@ -160,7 +183,10 @@ function detectConfig() {
160
183
  if (settings.hooks) {
161
184
  // SessionStart
162
185
  if (settings.hooks.SessionStart) {
163
- if (Array.isArray(settings.hooks.SessionStart) && settings.hooks.SessionStart.length > 0) {
186
+ if (
187
+ Array.isArray(settings.hooks.SessionStart) &&
188
+ settings.hooks.SessionStart.length > 0
189
+ ) {
164
190
  const hook = settings.hooks.SessionStart[0];
165
191
  if (hook.matcher !== undefined && hook.hooks) {
166
192
  status.features.sessionstart.enabled = true;
@@ -194,23 +220,7 @@ function detectConfig() {
194
220
  }
195
221
  }
196
222
 
197
- // Stop
198
- if (settings.hooks.Stop) {
199
- if (Array.isArray(settings.hooks.Stop) && settings.hooks.Stop.length > 0) {
200
- const hook = settings.hooks.Stop[0];
201
- if (hook.matcher !== undefined && hook.hooks) {
202
- status.features.stop.enabled = true;
203
- } else {
204
- status.features.stop.enabled = true;
205
- status.features.stop.valid = false;
206
- status.features.stop.issues.push('Old format - needs migration');
207
- }
208
- } else if (typeof settings.hooks.Stop === 'string') {
209
- status.features.stop.enabled = true;
210
- status.features.stop.valid = false;
211
- status.features.stop.issues.push('String format - needs migration');
212
- }
213
- }
223
+ // Note: Stop hook removed due to reliability issues
214
224
  }
215
225
 
216
226
  // StatusLine
@@ -248,8 +258,10 @@ function printStatus(status) {
248
258
  header('📊 Current Configuration');
249
259
 
250
260
  // Git
251
- log(`Git: ${status.git.initialized ? '✅' : '❌'} ${status.git.initialized ? 'initialized' : 'not initialized'}${status.git.remote ? ` (${status.git.remote})` : ''}`,
252
- status.git.initialized ? c.green : c.dim);
261
+ log(
262
+ `Git: ${status.git.initialized ? '✅' : '❌'} ${status.git.initialized ? 'initialized' : 'not initialized'}${status.git.remote ? ` (${status.git.remote})` : ''}`,
263
+ status.git.initialized ? c.green : c.dim
264
+ );
253
265
 
254
266
  // Settings
255
267
  if (!status.settingsExists) {
@@ -284,11 +296,12 @@ function printStatus(status) {
284
296
 
285
297
  printFeature('sessionstart', 'SessionStart Hook');
286
298
  printFeature('precompact', 'PreCompact Hook');
287
- printFeature('stop', 'Stop Hook');
288
299
 
289
300
  const arch = status.features.archival;
290
- log(` ${arch.enabled ? '✅' : '❌'} Archival: ${arch.enabled ? `${arch.threshold} days` : 'disabled'}`,
291
- arch.enabled ? c.green : c.dim);
301
+ log(
302
+ ` ${arch.enabled ? '✅' : '❌'} Archival: ${arch.enabled ? `${arch.threshold} days` : 'disabled'}`,
303
+ arch.enabled ? c.green : c.dim
304
+ );
292
305
 
293
306
  printFeature('statusline', 'Status Line');
294
307
 
@@ -326,19 +339,21 @@ function migrateSettings() {
326
339
 
327
340
  let migrated = false;
328
341
 
329
- // Migrate hooks
342
+ // Migrate hooks (Stop hook removed due to reliability issues)
330
343
  if (settings.hooks) {
331
- ['SessionStart', 'PreCompact', 'Stop', 'UserPromptSubmit'].forEach(hookName => {
344
+ ['SessionStart', 'PreCompact', 'UserPromptSubmit'].forEach(hookName => {
332
345
  const hook = settings.hooks[hookName];
333
346
  if (!hook) return;
334
347
 
335
348
  // String format → array format
336
349
  if (typeof hook === 'string') {
337
350
  const isNode = hook.includes('node ') || hook.endsWith('.js');
338
- settings.hooks[hookName] = [{
339
- matcher: '',
340
- hooks: [{ type: 'command', command: isNode ? hook : `bash ${hook}` }]
341
- }];
351
+ settings.hooks[hookName] = [
352
+ {
353
+ matcher: '',
354
+ hooks: [{ type: 'command', command: isNode ? hook : `bash ${hook}` }],
355
+ },
356
+ ];
342
357
  success(`Migrated ${hookName} from string format`);
343
358
  migrated = true;
344
359
  }
@@ -348,19 +363,23 @@ function migrateSettings() {
348
363
  if (first.enabled !== undefined || first.command !== undefined) {
349
364
  // Old format with enabled/command
350
365
  if (first.command) {
351
- settings.hooks[hookName] = [{
352
- matcher: '',
353
- hooks: [{ type: 'command', command: first.command }]
354
- }];
366
+ settings.hooks[hookName] = [
367
+ {
368
+ matcher: '',
369
+ hooks: [{ type: 'command', command: first.command }],
370
+ },
371
+ ];
355
372
  success(`Migrated ${hookName} from old object format`);
356
373
  migrated = true;
357
374
  }
358
375
  } else if (first.matcher === undefined) {
359
376
  // Missing matcher
360
- settings.hooks[hookName] = [{
361
- matcher: '',
362
- hooks: first.hooks || [{ type: 'command', command: 'echo "hook"' }]
363
- }];
377
+ settings.hooks[hookName] = [
378
+ {
379
+ matcher: '',
380
+ hooks: first.hooks || [{ type: 'command', command: 'echo "hook"' }],
381
+ },
382
+ ];
364
383
  success(`Migrated ${hookName} - added matcher`);
365
384
  migrated = true;
366
385
  }
@@ -374,7 +393,7 @@ function migrateSettings() {
374
393
  settings.statusLine = {
375
394
  type: 'command',
376
395
  command: settings.statusLine,
377
- padding: 0
396
+ padding: 0,
378
397
  };
379
398
  success('Migrated statusLine from string format');
380
399
  migrated = true;
@@ -417,7 +436,7 @@ function enableFeature(feature, options = {}) {
417
436
  ensureDir('.claude');
418
437
  ensureDir('scripts');
419
438
 
420
- let settings = readJSON('.claude/settings.json') || {};
439
+ const settings = readJSON('.claude/settings.json') || {};
421
440
  settings.hooks = settings.hooks || {};
422
441
  settings.permissions = settings.permissions || { allow: [], deny: [], ask: [] };
423
442
 
@@ -429,31 +448,30 @@ function enableFeature(feature, options = {}) {
429
448
  if (!copyTemplate(config.script, scriptPath)) {
430
449
  // Create minimal version
431
450
  if (feature === 'sessionstart') {
432
- fs.writeFileSync(scriptPath, `#!/usr/bin/env node\nconsole.log('AgileFlow v${VERSION} loaded');\n`);
451
+ fs.writeFileSync(
452
+ scriptPath,
453
+ `#!/usr/bin/env node\nconsole.log('AgileFlow v${VERSION} loaded');\n`
454
+ );
433
455
  } else if (feature === 'precompact') {
434
456
  fs.writeFileSync(scriptPath, '#!/bin/bash\necho "PreCompact: preserving context"\n');
435
- } else if (feature === 'stop') {
436
- fs.writeFileSync(scriptPath, `#!/bin/bash
437
- git rev-parse --git-dir > /dev/null 2>&1 || exit 0
438
- CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
439
- [ "$CHANGES" -gt 0 ] && echo -e "\\n\\033[33m$CHANGES uncommitted change(s)\\033[0m"
440
- `);
441
457
  }
442
- try { fs.chmodSync(scriptPath, '755'); } catch {}
458
+ try {
459
+ fs.chmodSync(scriptPath, '755');
460
+ } catch {}
443
461
  warn(`Created minimal ${config.script}`);
444
462
  } else {
445
463
  success(`Deployed ${config.script}`);
446
464
  }
447
465
 
448
466
  // Configure hook
449
- const command = config.type === 'node'
450
- ? `node ${scriptPath}`
451
- : `bash ${scriptPath}`;
452
-
453
- settings.hooks[config.hook] = [{
454
- matcher: '',
455
- hooks: [{ type: 'command', command }]
456
- }];
467
+ const command = config.type === 'node' ? `node ${scriptPath}` : `bash ${scriptPath}`;
468
+
469
+ settings.hooks[config.hook] = [
470
+ {
471
+ matcher: '',
472
+ hooks: [{ type: 'command', command }],
473
+ },
474
+ ];
457
475
  success(`${config.hook} hook enabled`);
458
476
  }
459
477
 
@@ -470,13 +488,13 @@ CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
470
488
 
471
489
  // Add to SessionStart hook
472
490
  if (settings.hooks.SessionStart?.[0]?.hooks) {
473
- const hasArchival = settings.hooks.SessionStart[0].hooks.some(
474
- h => h.command?.includes('archive-completed-stories')
491
+ const hasArchival = settings.hooks.SessionStart[0].hooks.some(h =>
492
+ h.command?.includes('archive-completed-stories')
475
493
  );
476
494
  if (!hasArchival) {
477
495
  settings.hooks.SessionStart[0].hooks.push({
478
496
  type: 'command',
479
- command: 'bash .agileflow/scripts/archive-completed-stories.sh --quiet'
497
+ command: 'bash scripts/archive-completed-stories.sh --quiet',
480
498
  });
481
499
  }
482
500
  }
@@ -491,12 +509,17 @@ CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
491
509
  const scriptPath = 'scripts/agileflow-statusline.sh';
492
510
 
493
511
  if (!copyTemplate('agileflow-statusline.sh', scriptPath)) {
494
- fs.writeFileSync(scriptPath, `#!/bin/bash
512
+ fs.writeFileSync(
513
+ scriptPath,
514
+ `#!/bin/bash
495
515
  input=$(cat)
496
516
  MODEL=$(echo "$input" | jq -r '.model.display_name // "Claude"')
497
517
  echo "[$MODEL] AgileFlow"
498
- `);
499
- try { fs.chmodSync(scriptPath, '755'); } catch {}
518
+ `
519
+ );
520
+ try {
521
+ fs.chmodSync(scriptPath, '755');
522
+ } catch {}
500
523
  warn('Created minimal statusline script');
501
524
  } else {
502
525
  success('Deployed agileflow-statusline.sh');
@@ -504,14 +527,31 @@ echo "[$MODEL] AgileFlow"
504
527
 
505
528
  settings.statusLine = {
506
529
  type: 'command',
507
- command: 'bash .agileflow/scripts/agileflow-statusline.sh',
508
- padding: 0
530
+ command: 'bash scripts/agileflow-statusline.sh',
531
+ padding: 0,
509
532
  };
510
533
  success('Status line enabled');
511
534
  }
512
535
 
536
+ // Handle autoupdate (metadata only, no hooks needed)
537
+ if (feature === 'autoupdate') {
538
+ const frequency = options.checkFrequency || 'daily';
539
+ updateMetadata({
540
+ updates: {
541
+ autoUpdate: true,
542
+ checkFrequency: frequency,
543
+ showChangelog: true,
544
+ },
545
+ });
546
+ success(`Auto-update enabled (check frequency: ${frequency})`);
547
+ info('AgileFlow will automatically update on session start');
548
+ return true; // Skip settings.json write for this feature
549
+ }
550
+
513
551
  writeJSON('.claude/settings.json', settings);
514
- updateMetadata({ features: { [feature]: { enabled: true, version: VERSION, at: new Date().toISOString() } } });
552
+ updateMetadata({
553
+ features: { [feature]: { enabled: true, version: VERSION, at: new Date().toISOString() } },
554
+ });
515
555
  updateGitignore();
516
556
 
517
557
  return true;
@@ -556,8 +596,21 @@ function disableFeature(feature) {
556
596
  success('Status line disabled');
557
597
  }
558
598
 
599
+ // Disable autoupdate
600
+ if (feature === 'autoupdate') {
601
+ updateMetadata({
602
+ updates: {
603
+ autoUpdate: false,
604
+ },
605
+ });
606
+ success('Auto-update disabled');
607
+ return true; // Skip settings.json write for this feature
608
+ }
609
+
559
610
  writeJSON('.claude/settings.json', settings);
560
- updateMetadata({ features: { [feature]: { enabled: false, version: VERSION, at: new Date().toISOString() } } });
611
+ updateMetadata({
612
+ features: { [feature]: { enabled: false, version: VERSION, at: new Date().toISOString() } },
613
+ });
561
614
 
562
615
  return true;
563
616
  }
@@ -586,6 +639,9 @@ function updateMetadata(updates) {
586
639
  meta.features[key] = { ...meta.features[key], ...value };
587
640
  });
588
641
  }
642
+ if (updates.updates) {
643
+ meta.updates = { ...meta.updates, ...updates.updates };
644
+ }
589
645
 
590
646
  meta.version = VERSION;
591
647
  meta.updated = new Date().toISOString();
@@ -600,7 +656,7 @@ function updateGitignore() {
600
656
  '.claude/context.log',
601
657
  '.claude/hook.log',
602
658
  '.claude/prompt-log.txt',
603
- '.claude/session.log'
659
+ '.claude/session.log',
604
660
  ];
605
661
 
606
662
  let content = fs.existsSync('.gitignore') ? fs.readFileSync('.gitignore', 'utf8') : '';
@@ -718,7 +774,9 @@ function applyProfile(profileName, options = {}) {
718
774
 
719
775
  // Enable features
720
776
  if (profile.enable) {
721
- profile.enable.forEach(f => enableFeature(f, { archivalDays: profile.archivalDays || options.archivalDays }));
777
+ profile.enable.forEach(f =>
778
+ enableFeature(f, { archivalDays: profile.archivalDays || options.archivalDays })
779
+ );
722
780
  }
723
781
 
724
782
  // Disable features
@@ -765,7 +823,7 @@ function printHelp() {
765
823
  ${c.bold}AgileFlow Configure${c.reset} - Manage AgileFlow features
766
824
 
767
825
  ${c.cyan}Usage:${c.reset}
768
- node .agileflow/scripts/agileflow-configure.js [options]
826
+ node scripts/agileflow-configure.js [options]
769
827
 
770
828
  ${c.cyan}Profiles:${c.reset}
771
829
  --profile=full All features (hooks, archival, statusline)
@@ -777,7 +835,7 @@ ${c.cyan}Feature Control:${c.reset}
777
835
  --enable=<list> Enable features (comma-separated)
778
836
  --disable=<list> Disable features (comma-separated)
779
837
 
780
- Features: sessionstart, precompact, stop, archival, statusline
838
+ Features: sessionstart, precompact, archival, statusline
781
839
 
782
840
  ${c.cyan}Statusline Components:${c.reset}
783
841
  --show=<list> Show statusline components (comma-separated)
@@ -796,28 +854,28 @@ ${c.cyan}Maintenance:${c.reset}
796
854
 
797
855
  ${c.cyan}Examples:${c.reset}
798
856
  # Quick setup with all features
799
- node .agileflow/scripts/agileflow-configure.js --profile=full
857
+ node scripts/agileflow-configure.js --profile=full
800
858
 
801
859
  # Enable specific features
802
- node .agileflow/scripts/agileflow-configure.js --enable=sessionstart,precompact,stop
860
+ node scripts/agileflow-configure.js --enable=sessionstart,precompact,archival
803
861
 
804
862
  # Disable a feature
805
- node .agileflow/scripts/agileflow-configure.js --disable=statusline
863
+ node scripts/agileflow-configure.js --disable=statusline
806
864
 
807
865
  # Show only agileflow branding and context in statusline
808
- node .agileflow/scripts/agileflow-configure.js --hide=model,story,epic,wip,cost,git
866
+ node scripts/agileflow-configure.js --hide=model,story,epic,wip,cost,git
809
867
 
810
868
  # Re-enable git branch in statusline
811
- node .agileflow/scripts/agileflow-configure.js --show=git
869
+ node scripts/agileflow-configure.js --show=git
812
870
 
813
871
  # List component status
814
- node .agileflow/scripts/agileflow-configure.js --components
872
+ node scripts/agileflow-configure.js --components
815
873
 
816
874
  # Fix format issues
817
- node .agileflow/scripts/agileflow-configure.js --migrate
875
+ node scripts/agileflow-configure.js --migrate
818
876
 
819
877
  # Check current status
820
- node .agileflow/scripts/agileflow-configure.js --detect
878
+ node scripts/agileflow-configure.js --detect
821
879
  `);
822
880
  }
823
881
 
@@ -842,10 +900,26 @@ function main() {
842
900
 
843
901
  args.forEach(arg => {
844
902
  if (arg.startsWith('--profile=')) profile = arg.split('=')[1];
845
- else if (arg.startsWith('--enable=')) enable = arg.split('=')[1].split(',').map(s => s.trim().toLowerCase());
846
- else if (arg.startsWith('--disable=')) disable = arg.split('=')[1].split(',').map(s => s.trim().toLowerCase());
847
- else if (arg.startsWith('--show=')) show = arg.split('=')[1].split(',').map(s => s.trim().toLowerCase());
848
- else if (arg.startsWith('--hide=')) hide = arg.split('=')[1].split(',').map(s => s.trim().toLowerCase());
903
+ else if (arg.startsWith('--enable='))
904
+ enable = arg
905
+ .split('=')[1]
906
+ .split(',')
907
+ .map(s => s.trim().toLowerCase());
908
+ else if (arg.startsWith('--disable='))
909
+ disable = arg
910
+ .split('=')[1]
911
+ .split(',')
912
+ .map(s => s.trim().toLowerCase());
913
+ else if (arg.startsWith('--show='))
914
+ show = arg
915
+ .split('=')[1]
916
+ .split(',')
917
+ .map(s => s.trim().toLowerCase());
918
+ else if (arg.startsWith('--hide='))
919
+ hide = arg
920
+ .split('=')[1]
921
+ .split(',')
922
+ .map(s => s.trim().toLowerCase());
849
923
  else if (arg.startsWith('--archival-days=')) archivalDays = parseInt(arg.split('=')[1]) || 7;
850
924
  else if (arg === '--migrate') migrate = true;
851
925
  else if (arg === '--detect' || arg === '--validate') detect = true;
@@ -13,7 +13,18 @@
13
13
 
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
- const { execSync } = require('child_process');
16
+ const { execSync, spawnSync } = require('child_process');
17
+
18
+ // Session manager path (relative to script location)
19
+ const SESSION_MANAGER_PATH = path.join(__dirname, 'session-manager.js');
20
+
21
+ // Update checker module
22
+ let updateChecker;
23
+ try {
24
+ updateChecker = require('./check-update.js');
25
+ } catch (e) {
26
+ // Update checker not available
27
+ }
17
28
 
18
29
  // ANSI color codes
19
30
  const c = {
@@ -39,9 +50,16 @@ const c = {
39
50
 
40
51
  // Box drawing characters
41
52
  const box = {
42
- tl: '╭', tr: '╮', bl: '╰', br: '╯',
43
- h: '', v: '│',
44
- lT: '', rT: '┤', tT: '┬', bT: '┴',
53
+ tl: '╭',
54
+ tr: '',
55
+ bl: '',
56
+ br: '╯',
57
+ h: '─',
58
+ v: '│',
59
+ lT: '├',
60
+ rT: '┤',
61
+ tT: '┬',
62
+ bT: '┴',
45
63
  cross: '┼',
46
64
  };
47
65
 
@@ -68,13 +86,26 @@ function getProjectInfo(rootDir) {
68
86
  currentStory: null,
69
87
  };
70
88
 
71
- // Get package info
89
+ // Get AgileFlow version (check multiple sources in priority order)
90
+ // 1. AgileFlow metadata (installed user projects)
91
+ // 2. packages/cli/package.json (AgileFlow dev project)
92
+ // 3. .agileflow/package.json (fallback)
72
93
  try {
73
- const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'packages/cli/package.json'), 'utf8'));
74
- info.version = pkg.version || info.version;
94
+ const metadataPath = path.join(rootDir, 'docs/00-meta/agileflow-metadata.json');
95
+ if (fs.existsSync(metadataPath)) {
96
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
97
+ info.version = metadata.version || info.version;
98
+ } else {
99
+ // Dev project: check packages/cli/package.json
100
+ const pkg = JSON.parse(
101
+ fs.readFileSync(path.join(rootDir, 'packages/cli/package.json'), 'utf8')
102
+ );
103
+ info.version = pkg.version || info.version;
104
+ }
75
105
  } catch (e) {
106
+ // Fallback: check .agileflow/package.json
76
107
  try {
77
- const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'));
108
+ const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, '.agileflow/package.json'), 'utf8'));
78
109
  info.version = pkg.version || info.version;
79
110
  } catch (e2) {}
80
111
  }
@@ -83,7 +114,10 @@ function getProjectInfo(rootDir) {
83
114
  try {
84
115
  info.branch = execSync('git branch --show-current', { cwd: rootDir, encoding: 'utf8' }).trim();
85
116
  info.commit = execSync('git rev-parse --short HEAD', { cwd: rootDir, encoding: 'utf8' }).trim();
86
- info.lastCommit = execSync('git log -1 --format="%s"', { cwd: rootDir, encoding: 'utf8' }).trim();
117
+ info.lastCommit = execSync('git log -1 --format="%s"', {
118
+ cwd: rootDir,
119
+ encoding: 'utf8',
120
+ }).trim();
87
121
  } catch (e) {}
88
122
 
89
123
  // Get status info
@@ -152,10 +186,10 @@ function runArchival(rootDir) {
152
186
  if (toArchiveCount > 0) {
153
187
  // Run archival
154
188
  try {
155
- execSync('bash .agileflow/scripts/archive-completed-stories.sh', {
189
+ execSync('bash scripts/archive-completed-stories.sh', {
156
190
  cwd: rootDir,
157
191
  encoding: 'utf8',
158
- stdio: 'pipe'
192
+ stdio: 'pipe',
159
193
  });
160
194
  result.archived = toArchiveCount;
161
195
  result.remaining -= toArchiveCount;
@@ -201,6 +235,60 @@ function clearActiveCommands(rootDir) {
201
235
  return result;
202
236
  }
203
237
 
238
+ function checkParallelSessions(rootDir) {
239
+ const result = {
240
+ available: false,
241
+ registered: false,
242
+ otherActive: 0,
243
+ currentId: null,
244
+ cleaned: 0,
245
+ };
246
+
247
+ try {
248
+ // Check if session manager exists
249
+ const managerPath = path.join(rootDir, '.agileflow', 'scripts', 'session-manager.js');
250
+ if (!fs.existsSync(managerPath) && !fs.existsSync(SESSION_MANAGER_PATH)) {
251
+ return result;
252
+ }
253
+
254
+ result.available = true;
255
+
256
+ // Try to register current session and get status
257
+ const scriptPath = fs.existsSync(managerPath) ? managerPath : SESSION_MANAGER_PATH;
258
+
259
+ // Register this session
260
+ try {
261
+ const registerOutput = execSync(`node "${scriptPath}" register`, {
262
+ cwd: rootDir,
263
+ encoding: 'utf8',
264
+ stdio: ['pipe', 'pipe', 'pipe'],
265
+ });
266
+ const registerData = JSON.parse(registerOutput);
267
+ result.registered = true;
268
+ result.currentId = registerData.id;
269
+ } catch (e) {
270
+ // Registration failed, continue anyway
271
+ }
272
+
273
+ // Get count of other active sessions
274
+ try {
275
+ const countOutput = execSync(`node "${scriptPath}" count`, {
276
+ cwd: rootDir,
277
+ encoding: 'utf8',
278
+ stdio: ['pipe', 'pipe', 'pipe'],
279
+ });
280
+ const countData = JSON.parse(countOutput);
281
+ result.otherActive = countData.count || 0;
282
+ } catch (e) {
283
+ // Count failed
284
+ }
285
+ } catch (e) {
286
+ // Session system not available
287
+ }
288
+
289
+ return result;
290
+ }
291
+
204
292
  function checkPreCompact(rootDir) {
205
293
  const result = { configured: false, scriptExists: false, version: null, outdated: false };
206
294
 
@@ -253,12 +341,104 @@ function compareVersions(a, b) {
253
341
  return 0;
254
342
  }
255
343
 
344
+ // Check for updates (async but we'll use sync approach for welcome)
345
+ async function checkUpdates() {
346
+ const result = {
347
+ available: false,
348
+ installed: null,
349
+ latest: null,
350
+ justUpdated: false,
351
+ previousVersion: null,
352
+ autoUpdate: false,
353
+ changelog: [],
354
+ };
355
+
356
+ if (!updateChecker) return result;
357
+
358
+ try {
359
+ const updateInfo = await updateChecker.checkForUpdates();
360
+ result.installed = updateInfo.installed;
361
+ result.latest = updateInfo.latest;
362
+ result.available = updateInfo.updateAvailable;
363
+ result.justUpdated = updateInfo.justUpdated;
364
+ result.previousVersion = updateInfo.previousVersion;
365
+ result.autoUpdate = updateInfo.autoUpdate;
366
+
367
+ // If just updated, try to get changelog entries
368
+ if (result.justUpdated && result.installed) {
369
+ result.changelog = getChangelogEntries(result.installed);
370
+ }
371
+ } catch (e) {
372
+ // Silently fail - update check is non-critical
373
+ }
374
+
375
+ return result;
376
+ }
377
+
378
+ // Parse CHANGELOG.md for entries of a specific version
379
+ function getChangelogEntries(version) {
380
+ const entries = [];
381
+
382
+ try {
383
+ // Look for CHANGELOG.md in .agileflow or package location
384
+ const possiblePaths = [
385
+ path.join(__dirname, '..', 'CHANGELOG.md'),
386
+ path.join(__dirname, 'CHANGELOG.md'),
387
+ ];
388
+
389
+ let changelogContent = null;
390
+ for (const p of possiblePaths) {
391
+ if (fs.existsSync(p)) {
392
+ changelogContent = fs.readFileSync(p, 'utf8');
393
+ break;
394
+ }
395
+ }
396
+
397
+ if (!changelogContent) return entries;
398
+
399
+ // Find the section for this version
400
+ const versionPattern = new RegExp(`## \\[${version}\\].*?\\n([\\s\\S]*?)(?=## \\[|$)`);
401
+ const match = changelogContent.match(versionPattern);
402
+
403
+ if (match) {
404
+ // Extract bullet points from Added/Changed/Fixed sections
405
+ const lines = match[1].split('\n');
406
+ for (const line of lines) {
407
+ const bulletMatch = line.match(/^- (.+)$/);
408
+ if (bulletMatch && entries.length < 3) {
409
+ entries.push(bulletMatch[1]);
410
+ }
411
+ }
412
+ }
413
+ } catch (e) {
414
+ // Silently fail
415
+ }
416
+
417
+ return entries;
418
+ }
419
+
420
+ // Run auto-update if enabled
421
+ async function runAutoUpdate(rootDir) {
422
+ try {
423
+ console.log(`${c.cyan}Updating AgileFlow...${c.reset}`);
424
+ execSync('npx agileflow update', {
425
+ cwd: rootDir,
426
+ encoding: 'utf8',
427
+ stdio: 'inherit',
428
+ });
429
+ return true;
430
+ } catch (e) {
431
+ console.log(`${c.yellow}Auto-update failed. Run manually: npx agileflow update${c.reset}`);
432
+ return false;
433
+ }
434
+ }
435
+
256
436
  function getFeatureVersions(rootDir) {
257
437
  const result = {
258
438
  hooks: { version: null, outdated: false },
259
439
  archival: { version: null, outdated: false },
260
440
  statusline: { version: null, outdated: false },
261
- precompact: { version: null, outdated: false }
441
+ precompact: { version: null, outdated: false },
262
442
  };
263
443
 
264
444
  // Minimum compatible versions for each feature
@@ -266,7 +446,7 @@ function getFeatureVersions(rootDir) {
266
446
  hooks: '2.35.0',
267
447
  archival: '2.35.0',
268
448
  statusline: '2.35.0',
269
- precompact: '2.40.0' // Multi-command support
449
+ precompact: '2.40.0', // Multi-command support
270
450
  };
271
451
 
272
452
  try {
@@ -277,7 +457,8 @@ function getFeatureVersions(rootDir) {
277
457
  for (const feature of Object.keys(result)) {
278
458
  if (metadata.features?.[feature]?.configured_version) {
279
459
  result[feature].version = metadata.features[feature].configured_version;
280
- result[feature].outdated = compareVersions(result[feature].version, minVersions[feature]) < 0;
460
+ result[feature].outdated =
461
+ compareVersions(result[feature].version, minVersions[feature]) < 0;
281
462
  }
282
463
  }
283
464
  }
@@ -291,7 +472,8 @@ function pad(str, len, align = 'left') {
291
472
  const diff = len - stripped.length;
292
473
  if (diff <= 0) return str;
293
474
  if (align === 'right') return ' '.repeat(diff) + str;
294
- if (align === 'center') return ' '.repeat(Math.floor(diff/2)) + str + ' '.repeat(Math.ceil(diff/2));
475
+ if (align === 'center')
476
+ return ' '.repeat(Math.floor(diff / 2)) + str + ' '.repeat(Math.ceil(diff / 2));
295
477
  return str + ' '.repeat(diff);
296
478
  }
297
479
 
@@ -323,7 +505,7 @@ function truncate(str, maxLen, suffix = '..') {
323
505
  return str.substring(0, cutIndex) + suffix;
324
506
  }
325
507
 
326
- function formatTable(info, archival, session, precompact) {
508
+ function formatTable(info, archival, session, precompact, parallelSessions, updateInfo = {}) {
327
509
  const W = 58; // inner width
328
510
  const R = W - 24; // right column width (34 chars)
329
511
  const lines = [];
@@ -336,29 +518,101 @@ function formatTable(info, archival, session, precompact) {
336
518
  return `${c.dim}${box.v}${c.reset} ${pad(leftStr, 20)} ${c.dim}${box.v}${c.reset} ${pad(rightStr, R)} ${c.dim}${box.v}${c.reset}`;
337
519
  };
338
520
 
339
- const divider = () => `${c.dim}${box.lT}${box.h.repeat(22)}${box.cross}${box.h.repeat(W - 22)}${box.rT}${c.reset}`;
521
+ // Helper for full-width row (spans both columns)
522
+ const fullRow = (content, color = '') => {
523
+ const contentStr = `${color}${content}${color ? c.reset : ''}`;
524
+ return `${c.dim}${box.v}${c.reset} ${pad(contentStr, W - 1)} ${c.dim}${box.v}${c.reset}`;
525
+ };
526
+
527
+ const divider = () =>
528
+ `${c.dim}${box.lT}${box.h.repeat(22)}${box.cross}${box.h.repeat(W - 22)}${box.rT}${c.reset}`;
529
+ const fullDivider = () =>
530
+ `${c.dim}${box.lT}${box.h.repeat(W)}${box.rT}${c.reset}`;
340
531
  const topBorder = `${c.dim}${box.tl}${box.h.repeat(22)}${box.tT}${box.h.repeat(W - 22)}${box.tr}${c.reset}`;
341
532
  const bottomBorder = `${c.dim}${box.bl}${box.h.repeat(22)}${box.bT}${box.h.repeat(W - 22)}${box.br}${c.reset}`;
342
533
 
343
- // Header (truncate branch name if too long)
344
- const branchColor = info.branch === 'main' ? c.green : info.branch.startsWith('fix') ? c.red : c.cyan;
345
- // Fixed parts: "agileflow " (10) + "v" (1) + version + " " (2) + " (" (2) + commit (7) + ")" (1) = 23 + version.length
346
- const maxBranchLen = (W - 1) - 23 - info.version.length;
347
- const branchDisplay = info.branch.length > maxBranchLen
348
- ? info.branch.substring(0, maxBranchLen - 2) + '..'
349
- : info.branch;
350
- const header = `${c.brand}${c.bold}agileflow${c.reset} ${c.dim}v${info.version}${c.reset} ${branchColor}${branchDisplay}${c.reset} ${c.dim}(${info.commit})${c.reset}`;
534
+ // Header with version and optional update indicator
535
+ const branchColor =
536
+ info.branch === 'main' ? c.green : info.branch.startsWith('fix') ? c.red : c.cyan;
537
+
538
+ // Build version string with update status
539
+ let versionStr = `v${info.version}`;
540
+ if (updateInfo.justUpdated && updateInfo.previousVersion) {
541
+ versionStr = `v${info.version} ${c.green}✓${c.reset}${c.dim} (was v${updateInfo.previousVersion})`;
542
+ } else if (updateInfo.available && updateInfo.latest) {
543
+ versionStr = `v${info.version} ${c.yellow}↑${updateInfo.latest}${c.reset}`;
544
+ }
545
+
546
+ // Calculate remaining space for branch
547
+ const versionVisibleLen = updateInfo.justUpdated
548
+ ? info.version.length + 20 + (updateInfo.previousVersion?.length || 0)
549
+ : updateInfo.available
550
+ ? info.version.length + 3 + (updateInfo.latest?.length || 0)
551
+ : info.version.length;
552
+ const maxBranchLen = W - 1 - 15 - versionVisibleLen;
553
+ const branchDisplay =
554
+ info.branch.length > maxBranchLen
555
+ ? info.branch.substring(0, Math.max(5, maxBranchLen - 2)) + '..'
556
+ : info.branch;
557
+
558
+ const header = `${c.brand}${c.bold}agileflow${c.reset} ${c.dim}${versionStr}${c.reset} ${branchColor}${branchDisplay}${c.reset} ${c.dim}(${info.commit})${c.reset}`;
351
559
  const headerLine = `${c.dim}${box.v}${c.reset} ${pad(header, W - 1)} ${c.dim}${box.v}${c.reset}`;
352
560
 
353
561
  lines.push(topBorder);
354
562
  lines.push(headerLine);
563
+
564
+ // Show update available notification
565
+ if (updateInfo.available && updateInfo.latest && !updateInfo.justUpdated) {
566
+ lines.push(fullDivider());
567
+ lines.push(fullRow(`↑ Update available: v${updateInfo.latest}`, c.yellow));
568
+ lines.push(fullRow(` Run: npx agileflow update`, c.dim));
569
+ }
570
+
571
+ // Show "just updated" changelog
572
+ if (updateInfo.justUpdated && updateInfo.changelog && updateInfo.changelog.length > 0) {
573
+ lines.push(fullDivider());
574
+ lines.push(fullRow(`What's new in v${info.version}:`, c.green));
575
+ for (const entry of updateInfo.changelog.slice(0, 2)) {
576
+ lines.push(fullRow(`• ${truncate(entry, W - 4)}`, c.dim));
577
+ }
578
+ lines.push(fullRow(`Run /agileflow:whats-new for full changelog`, c.dim));
579
+ }
580
+
355
581
  lines.push(divider());
356
582
 
357
583
  // Stories section
358
- lines.push(row('In Progress', info.wipCount > 0 ? `${info.wipCount}` : '0', c.dim, info.wipCount > 0 ? c.yellow : c.dim));
359
- lines.push(row('Blocked', info.blockedCount > 0 ? `${info.blockedCount}` : '0', c.dim, info.blockedCount > 0 ? c.red : c.dim));
360
- lines.push(row('Ready', info.readyCount > 0 ? `${info.readyCount}` : '0', c.dim, info.readyCount > 0 ? c.cyan : c.dim));
361
- lines.push(row('Completed', info.completedCount > 0 ? `${info.completedCount}` : '0', c.dim, info.completedCount > 0 ? c.green : c.dim));
584
+ lines.push(
585
+ row(
586
+ 'In Progress',
587
+ info.wipCount > 0 ? `${info.wipCount}` : '0',
588
+ c.dim,
589
+ info.wipCount > 0 ? c.yellow : c.dim
590
+ )
591
+ );
592
+ lines.push(
593
+ row(
594
+ 'Blocked',
595
+ info.blockedCount > 0 ? `${info.blockedCount}` : '0',
596
+ c.dim,
597
+ info.blockedCount > 0 ? c.red : c.dim
598
+ )
599
+ );
600
+ lines.push(
601
+ row(
602
+ 'Ready',
603
+ info.readyCount > 0 ? `${info.readyCount}` : '0',
604
+ c.dim,
605
+ info.readyCount > 0 ? c.cyan : c.dim
606
+ )
607
+ );
608
+ lines.push(
609
+ row(
610
+ 'Completed',
611
+ info.completedCount > 0 ? `${info.completedCount}` : '0',
612
+ c.dim,
613
+ info.completedCount > 0 ? c.green : c.dim
614
+ )
615
+ );
362
616
 
363
617
  lines.push(divider());
364
618
 
@@ -366,16 +620,15 @@ function formatTable(info, archival, session, precompact) {
366
620
  if (archival.disabled) {
367
621
  lines.push(row('Auto-archival', 'disabled', c.dim, c.dim));
368
622
  } else {
369
- const archivalStatus = archival.archived > 0
370
- ? `archived ${archival.archived} stories`
371
- : `nothing to archive`;
372
- lines.push(row('Auto-archival', archivalStatus, c.dim, archival.archived > 0 ? c.green : c.dim));
623
+ const archivalStatus =
624
+ archival.archived > 0 ? `archived ${archival.archived} stories` : `nothing to archive`;
625
+ lines.push(
626
+ row('Auto-archival', archivalStatus, c.dim, archival.archived > 0 ? c.green : c.dim)
627
+ );
373
628
  }
374
629
 
375
630
  // Session cleanup
376
- const sessionStatus = session.cleared > 0
377
- ? `cleared ${session.cleared} command(s)`
378
- : `clean`;
631
+ const sessionStatus = session.cleared > 0 ? `cleared ${session.cleared} command(s)` : `clean`;
379
632
  lines.push(row('Session state', sessionStatus, c.dim, session.cleared > 0 ? c.green : c.dim));
380
633
 
381
634
  // PreCompact status with version check
@@ -396,11 +649,31 @@ function formatTable(info, archival, session, precompact) {
396
649
  lines.push(row('Context preserve', 'not configured', c.dim, c.dim));
397
650
  }
398
651
 
652
+ // Parallel sessions status
653
+ if (parallelSessions && parallelSessions.available) {
654
+ if (parallelSessions.otherActive > 0) {
655
+ const sessionStr = `⚠️ ${parallelSessions.otherActive} other active`;
656
+ lines.push(row('Sessions', sessionStr, c.dim, c.yellow));
657
+ } else {
658
+ const sessionStr = parallelSessions.currentId
659
+ ? `✓ Session ${parallelSessions.currentId} (only)`
660
+ : '✓ Only session';
661
+ lines.push(row('Sessions', sessionStr, c.dim, c.green));
662
+ }
663
+ }
664
+
399
665
  lines.push(divider());
400
666
 
401
667
  // Current story (if any) - row() auto-truncates
402
668
  if (info.currentStory) {
403
- lines.push(row('Current', `${c.blue}${info.currentStory.id}${c.reset}: ${info.currentStory.title}`, c.dim, ''));
669
+ lines.push(
670
+ row(
671
+ 'Current',
672
+ `${c.blue}${info.currentStory.id}${c.reset}: ${info.currentStory.title}`,
673
+ c.dim,
674
+ ''
675
+ )
676
+ );
404
677
  } else {
405
678
  lines.push(row('Current', 'No active story', c.dim, c.dim));
406
679
  }
@@ -414,14 +687,45 @@ function formatTable(info, archival, session, precompact) {
414
687
  }
415
688
 
416
689
  // Main
417
- function main() {
690
+ async function main() {
418
691
  const rootDir = getProjectRoot();
419
692
  const info = getProjectInfo(rootDir);
420
693
  const archival = runArchival(rootDir);
421
694
  const session = clearActiveCommands(rootDir);
422
695
  const precompact = checkPreCompact(rootDir);
696
+ const parallelSessions = checkParallelSessions(rootDir);
423
697
 
424
- console.log(formatTable(info, archival, session, precompact));
698
+ // Check for updates (async, cached)
699
+ let updateInfo = {};
700
+ try {
701
+ updateInfo = await checkUpdates();
702
+
703
+ // If auto-update is enabled and update available, run it
704
+ if (updateInfo.available && updateInfo.autoUpdate) {
705
+ const updated = await runAutoUpdate(rootDir);
706
+ if (updated) {
707
+ // Re-run welcome after update (the new version will show changelog)
708
+ return;
709
+ }
710
+ }
711
+
712
+ // Mark current version as seen to track for next update
713
+ if (updateInfo.justUpdated && updateChecker) {
714
+ updateChecker.markVersionSeen(info.version);
715
+ }
716
+ } catch (e) {
717
+ // Update check failed - continue without it
718
+ }
719
+
720
+ console.log(formatTable(info, archival, session, precompact, parallelSessions, updateInfo));
721
+
722
+ // Show warning and tip if other sessions are active
723
+ if (parallelSessions.otherActive > 0) {
724
+ console.log('');
725
+ console.log(`${c.yellow}⚠️ Other Claude session(s) active in this repo.${c.reset}`);
726
+ console.log(`${c.dim} Run /agileflow:session:status to see all sessions.${c.reset}`);
727
+ console.log(`${c.dim} Run /agileflow:session:new to create isolated workspace.${c.reset}`);
728
+ }
425
729
  }
426
730
 
427
- main();
731
+ main().catch(console.error);
@@ -105,15 +105,20 @@ module.exports = {
105
105
  // Check if CLI itself is still outdated (only if self-update was disabled)
106
106
  if (npmLatestVersion && semver.lt(localCliVersion, npmLatestVersion) && !shouldSelfUpdate) {
107
107
  console.log();
108
- warning('Your CLI is outdated!');
109
- console.log(chalk.dim(` To update your installation, run:\n`));
110
- console.log(chalk.cyan(` npx agileflow@latest update\n`));
108
+ warning('Your global CLI is outdated!');
109
+ console.log(
110
+ chalk.dim(` You have a global installation at v${localCliVersion}, but v${npmLatestVersion} is available.\n`)
111
+ );
112
+ console.log(chalk.dim(` Options:`));
113
+ console.log(chalk.dim(` 1. Cancel and run: `) + chalk.cyan(`npx agileflow@latest update`));
114
+ console.log(chalk.dim(` 2. Remove global: `) + chalk.cyan(`npm uninstall -g agileflow`) + chalk.dim(` (recommended)`));
115
+ console.log(chalk.dim(` 3. Update global: `) + chalk.cyan(`npm install -g agileflow@latest\n`));
111
116
 
112
117
  const useOutdated = options.force
113
118
  ? true
114
119
  : await confirm('Continue with outdated CLI anyway?');
115
120
  if (!useOutdated) {
116
- console.log(chalk.dim('\nUpdate cancelled\n'));
121
+ console.log(chalk.dim('\nUpdate cancelled. Run: npx agileflow@latest update\n'));
117
122
  process.exit(0);
118
123
  }
119
124
  }
@@ -209,6 +214,19 @@ module.exports = {
209
214
 
210
215
  console.log(chalk.green(`\n✨ Update complete! (${status.version} → ${latestVersion})\n`));
211
216
 
217
+ // If running from outdated global installation, remind user to update it
218
+ if (
219
+ npmLatestVersion &&
220
+ semver.lt(localCliVersion, npmLatestVersion) &&
221
+ !options.selfUpdated
222
+ ) {
223
+ console.log(chalk.yellow('💡 Tip: Your global AgileFlow CLI is outdated.'));
224
+ console.log(chalk.dim(' To avoid this message, either:'));
225
+ console.log(chalk.dim(' • Remove global: npm uninstall -g agileflow (recommended)'));
226
+ console.log(chalk.dim(' • Use npx: npx agileflow@latest update'));
227
+ console.log(chalk.dim(' • Update global: npm install -g agileflow@latest\n'));
228
+ }
229
+
212
230
  process.exit(0);
213
231
  } catch (err) {
214
232
  console.error(chalk.red('Update failed:'), err.message);
@@ -1,13 +0,0 @@
1
- #!/bin/bash
2
- # agileflow-stop.sh - Session wrap-up hook
3
-
4
- # Only run if we're in a git repo
5
- git rev-parse --git-dir > /dev/null 2>&1 || exit 0
6
-
7
- # Check for uncommitted changes
8
- CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
9
-
10
- if [ "$CHANGES" -gt 0 ]; then
11
- echo ""
12
- echo -e "\033[0;33m$CHANGES uncommitted change(s)\033[0m"
13
- fi
@@ -1,13 +0,0 @@
1
- #!/bin/bash
2
- # agileflow-stop.sh - Session wrap-up hook
3
-
4
- # Only run if we're in a git repo
5
- git rev-parse --git-dir > /dev/null 2>&1 || exit 0
6
-
7
- # Check for uncommitted changes
8
- CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
9
-
10
- if [ "$CHANGES" -gt 0 ]; then
11
- echo ""
12
- echo -e "\033[0;33m$CHANGES uncommitted change(s)\033[0m"
13
- fi