agileflow 2.99.0 → 2.99.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.99.1] - 2026-02-08
11
+
12
+ ### Added
13
+ - Smart session management with context-aware naming and tmux quick-create keybinds
14
+
10
15
  ## [2.99.0] - 2026-02-08
11
16
 
12
17
  ### Added
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  </p>
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/agileflow?color=brightgreen)](https://www.npmjs.com/package/agileflow)
6
- [![Commands](https://img.shields.io/badge/commands-90-blue)](docs/04-architecture/commands.md)
6
+ [![Commands](https://img.shields.io/badge/commands-91-blue)](docs/04-architecture/commands.md)
7
7
  [![Agents/Experts](https://img.shields.io/badge/agents%2Fexperts-47-orange)](docs/04-architecture/subagents.md)
8
8
  [![Skills](https://img.shields.io/badge/skills-dynamic-purple)](docs/04-architecture/skills.md)
9
9
 
@@ -65,7 +65,7 @@ AgileFlow combines three proven methodologies:
65
65
 
66
66
  | Component | Count | Description |
67
67
  |-----------|-------|-------------|
68
- | [Commands](docs/04-architecture/commands.md) | 90 | Slash commands for agile workflows |
68
+ | [Commands](docs/04-architecture/commands.md) | 91 | Slash commands for agile workflows |
69
69
  | [Agents/Experts](docs/04-architecture/subagents.md) | 47 | 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
 
@@ -76,7 +76,7 @@ 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 90 slash commands
79
+ - [Commands](docs/04-architecture/commands.md) - All 91 slash commands
80
80
  - [Agents/Experts](docs/04-architecture/subagents.md) - 47 specialized agents with self-improving knowledge
81
81
  - [Skills](docs/04-architecture/skills.md) - Dynamic skill generator with MCP integration
82
82
 
@@ -78,6 +78,9 @@ const OutboundMessageType = {
78
78
  // User Interaction
79
79
  ASK_USER_QUESTION: 'ask_user_question', // Claude is asking user a question
80
80
 
81
+ // Sessions
82
+ SESSION_LIST: 'session_list', // Session list with sync status
83
+
81
84
  // Errors
82
85
  ERROR: 'error', // General error
83
86
  };
@@ -123,6 +126,9 @@ const InboundMessageType = {
123
126
  INBOX_LIST_REQUEST: 'inbox_list_request', // Request inbox list
124
127
  INBOX_ACTION: 'inbox_action', // Accept/dismiss inbox item
125
128
 
129
+ // File Operations
130
+ OPEN_FILE: 'open_file', // Open file in editor
131
+
126
132
  // User Interaction Response
127
133
  USER_ANSWER: 'user_answer', // User's answer to AskUserQuestion
128
134
  };
@@ -435,6 +441,36 @@ function createInboxItem(item) {
435
441
  };
436
442
  }
437
443
 
444
+ /**
445
+ * Create a project status update message
446
+ * @param {Object} summary - Status summary
447
+ * @param {number} summary.total - Total stories
448
+ * @param {number} summary.done - Completed stories
449
+ * @param {number} summary.inProgress - In-progress stories
450
+ * @param {number} summary.ready - Ready stories
451
+ * @param {number} summary.blocked - Blocked stories
452
+ * @param {Object[]} summary.epics - Epic summaries
453
+ */
454
+ function createStatusUpdate(summary) {
455
+ return {
456
+ type: OutboundMessageType.STATUS_UPDATE,
457
+ ...summary,
458
+ timestamp: new Date().toISOString(),
459
+ };
460
+ }
461
+
462
+ /**
463
+ * Create a session list message with sync status
464
+ * @param {Object[]} sessions - Array of session objects with sync info
465
+ */
466
+ function createSessionList(sessions) {
467
+ return {
468
+ type: OutboundMessageType.SESSION_LIST,
469
+ sessions,
470
+ timestamp: new Date().toISOString(),
471
+ };
472
+ }
473
+
438
474
  /**
439
475
  * Create an AskUserQuestion message
440
476
  * @param {string} toolId - Tool call ID for response correlation
@@ -533,6 +569,8 @@ module.exports = {
533
569
  createInboxList,
534
570
  createInboxItem,
535
571
  createAskUserQuestion,
572
+ createStatusUpdate,
573
+ createSessionList,
536
574
 
537
575
  // Parsing
538
576
  parseInboundMessage,
@@ -38,6 +38,8 @@ const {
38
38
  createAutomationResult,
39
39
  createInboxList,
40
40
  createInboxItem,
41
+ createStatusUpdate,
42
+ createSessionList,
41
43
  parseInboundMessage,
42
44
  serializeMessage,
43
45
  } = require('./dashboard-protocol');
@@ -510,7 +512,8 @@ class DashboardServer extends EventEmitter {
510
512
  // Auth is on by default - auto-generate key if not provided
511
513
  // Set requireAuth: false explicitly to disable
512
514
  this.requireAuth = options.requireAuth !== false;
513
- this.apiKey = options.apiKey || (this.requireAuth ? crypto.randomBytes(32).toString('hex') : null);
515
+ this.apiKey =
516
+ options.apiKey || (this.requireAuth ? crypto.randomBytes(32).toString('hex') : null);
514
517
 
515
518
  // Session management
516
519
  this.sessions = new Map();
@@ -708,8 +711,10 @@ class DashboardServer extends EventEmitter {
708
711
  // Use timing-safe comparison to prevent timing attacks
709
712
  const keyBuffer = Buffer.from(this.apiKey, 'utf8');
710
713
  const providedBuffer = Buffer.from(providedKey, 'utf8');
711
- if (keyBuffer.length !== providedBuffer.length ||
712
- !crypto.timingSafeEqual(keyBuffer, providedBuffer)) {
714
+ if (
715
+ keyBuffer.length !== providedBuffer.length ||
716
+ !crypto.timingSafeEqual(keyBuffer, providedBuffer)
717
+ ) {
713
718
  socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
714
719
  socket.destroy();
715
720
  return;
@@ -720,9 +725,12 @@ class DashboardServer extends EventEmitter {
720
725
  const origin = req.headers.origin;
721
726
  if (origin) {
722
727
  const LOCALHOST_ORIGINS = [
723
- 'http://localhost', 'https://localhost',
724
- 'http://127.0.0.1', 'https://127.0.0.1',
725
- 'http://[::1]', 'https://[::1]',
728
+ 'http://localhost',
729
+ 'https://localhost',
730
+ 'http://127.0.0.1',
731
+ 'https://127.0.0.1',
732
+ 'http://[::1]',
733
+ 'https://[::1]',
726
734
  ];
727
735
  const isLocalhost = LOCALHOST_ORIGINS.some(
728
736
  allowed => origin === allowed || origin.startsWith(allowed + ':')
@@ -803,6 +811,12 @@ class DashboardServer extends EventEmitter {
803
811
  // Send initial git status
804
812
  this.sendGitStatus(session);
805
813
 
814
+ // Send project status (stories/epics)
815
+ this.sendStatusUpdate(session);
816
+
817
+ // Send session list with sync info
818
+ this.sendSessionList(session);
819
+
806
820
  // Send initial automation list and inbox
807
821
  this.sendAutomationList(session);
808
822
  this.sendInboxList(session);
@@ -936,6 +950,10 @@ class DashboardServer extends EventEmitter {
936
950
  this.handleInboxAction(session, message);
937
951
  break;
938
952
 
953
+ case InboundMessageType.OPEN_FILE:
954
+ this.handleOpenFile(session, message);
955
+ break;
956
+
939
957
  default:
940
958
  console.log(`[Session ${session.id}] Unhandled message type: ${message.type}`);
941
959
  this.emit('message', session, message);
@@ -985,8 +1003,12 @@ class DashboardServer extends EventEmitter {
985
1003
  this.emit('refresh:tasks', session);
986
1004
  break;
987
1005
  case 'status':
1006
+ this.sendStatusUpdate(session);
988
1007
  this.emit('refresh:status', session);
989
1008
  break;
1009
+ case 'sessions':
1010
+ this.sendSessionList(session);
1011
+ break;
990
1012
  case 'automations':
991
1013
  this.sendAutomationList(session);
992
1014
  break;
@@ -995,6 +1017,8 @@ class DashboardServer extends EventEmitter {
995
1017
  break;
996
1018
  default:
997
1019
  this.sendGitStatus(session);
1020
+ this.sendStatusUpdate(session);
1021
+ this.sendSessionList(session);
998
1022
  this.sendAutomationList(session);
999
1023
  this.sendInboxList(session);
1000
1024
  this.emit('refresh:all', session);
@@ -1260,6 +1284,162 @@ class DashboardServer extends EventEmitter {
1260
1284
  return { additions, deletions };
1261
1285
  }
1262
1286
 
1287
+ /**
1288
+ * Send project status update (stories/epics summary) to session
1289
+ */
1290
+ sendStatusUpdate(session) {
1291
+ const path = require('path');
1292
+ const fs = require('fs');
1293
+ const statusPath = path.join(this.projectRoot, 'docs', '09-agents', 'status.json');
1294
+ if (!fs.existsSync(statusPath)) return;
1295
+
1296
+ try {
1297
+ const data = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
1298
+ const stories = data.stories || {};
1299
+ const epics = data.epics || {};
1300
+
1301
+ const storyValues = Object.values(stories);
1302
+ const summary = {
1303
+ total: storyValues.length,
1304
+ done: storyValues.filter(s => s.status === 'done' || s.status === 'completed').length,
1305
+ inProgress: storyValues.filter(s => s.status === 'in-progress').length,
1306
+ ready: storyValues.filter(s => s.status === 'ready').length,
1307
+ blocked: storyValues.filter(s => s.status === 'blocked').length,
1308
+ epics: Object.entries(epics).map(([id, e]) => ({
1309
+ id,
1310
+ title: e.title || id,
1311
+ status: e.status || 'unknown',
1312
+ storyCount: (e.stories || []).length,
1313
+ doneCount: (e.stories || []).filter(sid =>
1314
+ stories[sid] && (stories[sid].status === 'done' || stories[sid].status === 'completed')
1315
+ ).length,
1316
+ })),
1317
+ };
1318
+
1319
+ session.send(createStatusUpdate(summary));
1320
+ } catch (error) {
1321
+ console.error('[Status Update Error]', error.message);
1322
+ }
1323
+ }
1324
+
1325
+ /**
1326
+ * Send session list with sync status to dashboard
1327
+ */
1328
+ sendSessionList(session) {
1329
+ const sessions = [];
1330
+
1331
+ for (const [id, s] of this.sessions) {
1332
+ const entry = {
1333
+ id,
1334
+ name: s.metadata.name || id,
1335
+ type: s.metadata.type || 'local',
1336
+ status: s.state === 'connected' ? 'active' : s.state === 'disconnected' ? 'idle' : s.state,
1337
+ branch: null,
1338
+ messageCount: s.messages.length,
1339
+ lastActivity: s.lastActivity.toISOString(),
1340
+ syncStatus: 'offline',
1341
+ ahead: 0,
1342
+ behind: 0,
1343
+ };
1344
+
1345
+ // Get branch and sync status via git
1346
+ try {
1347
+ const cwd = s.metadata.worktreePath || this.projectRoot;
1348
+ entry.branch = execFileSync('git', ['branch', '--show-current'], {
1349
+ cwd,
1350
+ encoding: 'utf8',
1351
+ stdio: ['pipe', 'pipe', 'pipe'],
1352
+ }).trim();
1353
+
1354
+ // Get ahead/behind counts relative to upstream
1355
+ try {
1356
+ const counts = execFileSync('git', ['rev-list', '--left-right', '--count', 'HEAD...@{u}'], {
1357
+ cwd,
1358
+ encoding: 'utf8',
1359
+ stdio: ['pipe', 'pipe', 'pipe'],
1360
+ }).trim();
1361
+ const [ahead, behind] = counts.split(/\s+/).map(Number);
1362
+ entry.ahead = ahead || 0;
1363
+ entry.behind = behind || 0;
1364
+
1365
+ if (ahead > 0 && behind > 0) {
1366
+ entry.syncStatus = 'diverged';
1367
+ } else if (ahead > 0) {
1368
+ entry.syncStatus = 'ahead';
1369
+ } else if (behind > 0) {
1370
+ entry.syncStatus = 'behind';
1371
+ } else {
1372
+ entry.syncStatus = 'synced';
1373
+ }
1374
+ } catch {
1375
+ // No upstream configured
1376
+ entry.syncStatus = 'synced';
1377
+ }
1378
+ } catch {
1379
+ entry.syncStatus = 'offline';
1380
+ }
1381
+
1382
+ sessions.push(entry);
1383
+ }
1384
+
1385
+ session.send(createSessionList(sessions));
1386
+ }
1387
+
1388
+ /**
1389
+ * Handle open file in editor request
1390
+ */
1391
+ handleOpenFile(session, message) {
1392
+ const { path: filePath, line } = message;
1393
+
1394
+ if (!filePath || typeof filePath !== 'string') {
1395
+ session.send(createError('INVALID_REQUEST', 'File path is required'));
1396
+ return;
1397
+ }
1398
+
1399
+ // Validate the path stays within project root
1400
+ const pathResult = validatePath(filePath, this.projectRoot, { allowSymlinks: true });
1401
+ if (!pathResult.ok) {
1402
+ session.send(createError('OPEN_FILE_ERROR', 'File path outside project'));
1403
+ return;
1404
+ }
1405
+
1406
+ const fullPath = pathResult.resolvedPath;
1407
+
1408
+ // Detect editor from environment
1409
+ const editor = process.env.VISUAL || process.env.EDITOR || 'code';
1410
+ const editorBase = require('path').basename(editor).toLowerCase();
1411
+
1412
+ try {
1413
+ const lineNum = Number.isFinite(line) && line > 0 ? line : null;
1414
+
1415
+ switch (editorBase) {
1416
+ case 'code':
1417
+ case 'cursor':
1418
+ case 'windsurf': {
1419
+ const gotoArg = lineNum ? `${fullPath}:${lineNum}` : fullPath;
1420
+ spawn(editor, ['--goto', gotoArg], { detached: true, stdio: 'ignore' }).unref();
1421
+ break;
1422
+ }
1423
+ case 'subl':
1424
+ case 'sublime_text': {
1425
+ const sublArg = lineNum ? `${fullPath}:${lineNum}` : fullPath;
1426
+ spawn(editor, [sublArg], { detached: true, stdio: 'ignore' }).unref();
1427
+ break;
1428
+ }
1429
+ default: {
1430
+ // Generic: just open the file
1431
+ spawn(editor, [fullPath], { detached: true, stdio: 'ignore' }).unref();
1432
+ break;
1433
+ }
1434
+ }
1435
+
1436
+ session.send(createNotification('info', 'Editor', `Opened ${require('path').basename(fullPath)}`));
1437
+ } catch (error) {
1438
+ console.error('[Open File Error]', error.message);
1439
+ session.send(createError('OPEN_FILE_ERROR', `Failed to open file: ${error.message}`));
1440
+ }
1441
+ }
1442
+
1263
1443
  /**
1264
1444
  * Handle terminal spawn request
1265
1445
  */
@@ -1271,7 +1451,9 @@ class DashboardServer extends EventEmitter {
1271
1451
  if (cwd) {
1272
1452
  const cwdResult = validatePath(cwd, this.projectRoot, { allowSymlinks: true });
1273
1453
  if (!cwdResult.ok) {
1274
- session.send(createError('TERMINAL_ERROR', 'Working directory must be within project root'));
1454
+ session.send(
1455
+ createError('TERMINAL_ERROR', 'Working directory must be within project root')
1456
+ );
1275
1457
  return;
1276
1458
  }
1277
1459
  safeCwd = cwdResult.resolvedPath;
package/lib/feedback.js CHANGED
@@ -72,7 +72,9 @@ class Feedback {
72
72
  constructor(options = {}) {
73
73
  this.isTTY = options.isTTY !== undefined ? options.isTTY : process.stdout.isTTY;
74
74
  this.indent = options.indent || 0;
75
- this.quiet = options.quiet || false;
75
+ this.quiet = options.quiet !== undefined
76
+ ? options.quiet
77
+ : (process.env.AGILEFLOW_QUIET === '1' || process.env.AGILEFLOW_QUIET === 'true');
76
78
  this.verbose = options.verbose || false;
77
79
  }
78
80
 
@@ -274,7 +276,8 @@ class Feedback {
274
276
  class FeedbackSpinner {
275
277
  constructor(message, options = {}) {
276
278
  this.message = message;
277
- this.isTTY = options.isTTY !== undefined ? options.isTTY : process.stdout.isTTY;
279
+ this.stream = options.stream || process.stdout;
280
+ this.isTTY = options.isTTY !== undefined ? options.isTTY : this.stream.isTTY;
278
281
  this.indent = options.indent || 0;
279
282
  this.interval = options.interval || 80;
280
283
  this.frameIndex = 0;
@@ -286,13 +289,26 @@ class FeedbackSpinner {
286
289
  return ' '.repeat(this.indent);
287
290
  }
288
291
 
292
+ /**
293
+ * Write a line to the appropriate stream
294
+ * @param {string} text - Text to write
295
+ * @private
296
+ */
297
+ _writeln(text) {
298
+ if (this.stream !== process.stdout) {
299
+ this.stream.write(text + '\n');
300
+ } else {
301
+ console.log(text);
302
+ }
303
+ }
304
+
289
305
  /**
290
306
  * Start the spinner
291
307
  * @returns {FeedbackSpinner}
292
308
  */
293
309
  start() {
294
310
  if (!this.isTTY) {
295
- console.log(`${this._prefix()}${c.dim}${this.message}${c.reset}`);
311
+ this._writeln(`${this._prefix()}${c.dim}${this.message}${c.reset}`);
296
312
  return this;
297
313
  }
298
314
 
@@ -327,9 +343,9 @@ class FeedbackSpinner {
327
343
  if (!this.isTTY) return;
328
344
  const frame = SPINNER_FRAMES[this.frameIndex];
329
345
  const line = `${this._prefix()}${c.cyan}${frame}${c.reset} ${this.message}`;
330
- process.stdout.clearLine(0);
331
- process.stdout.cursorTo(0);
332
- process.stdout.write(line);
346
+ this.stream.clearLine(0);
347
+ this.stream.cursorTo(0);
348
+ this.stream.write(line);
333
349
  }
334
350
 
335
351
  /**
@@ -346,8 +362,8 @@ class FeedbackSpinner {
346
362
  }
347
363
 
348
364
  if (this.isTTY) {
349
- process.stdout.clearLine(0);
350
- process.stdout.cursorTo(0);
365
+ this.stream.clearLine(0);
366
+ this.stream.cursorTo(0);
351
367
  }
352
368
 
353
369
  const elapsed = this.startTime ? Date.now() - this.startTime : 0;
@@ -355,7 +371,7 @@ class FeedbackSpinner {
355
371
  elapsed > DOHERTY_THRESHOLD_MS ? ` ${c.dim}(${formatDuration(elapsed)})${c.reset}` : '';
356
372
  const msg = message || this.message;
357
373
 
358
- console.log(`${this._prefix()}${color}${symbol}${c.reset} ${msg}${suffix}`);
374
+ this._writeln(`${this._prefix()}${color}${symbol}${c.reset} ${msg}${suffix}`);
359
375
  return this;
360
376
  }
361
377
 
@@ -548,12 +564,22 @@ class FeedbackProgressBar {
548
564
  // Singleton instance for convenience
549
565
  const feedback = new Feedback();
550
566
 
567
+ /**
568
+ * Factory function to create a configured Feedback instance
569
+ * @param {Object} [options={}] - Feedback options
570
+ * @returns {Feedback}
571
+ */
572
+ function createFeedback(options = {}) {
573
+ return new Feedback(options);
574
+ }
575
+
551
576
  module.exports = {
552
577
  Feedback,
553
578
  FeedbackSpinner,
554
579
  FeedbackTask,
555
580
  FeedbackProgressBar,
556
581
  feedback,
582
+ createFeedback,
557
583
  SYMBOLS,
558
584
  SPINNER_FRAMES,
559
585
  DOHERTY_THRESHOLD_MS,
@@ -80,7 +80,10 @@ function getCurrentBranch(cwd = ROOT) {
80
80
  if (cached !== null) return cached;
81
81
 
82
82
  try {
83
- const branch = execFileSync('git', ['branch', '--show-current'], { cwd, encoding: 'utf8' }).trim();
83
+ const branch = execFileSync('git', ['branch', '--show-current'], {
84
+ cwd,
85
+ encoding: 'utf8',
86
+ }).trim();
84
87
  gitCache.set(cacheKey, branch);
85
88
  return branch;
86
89
  } catch (e) {
@@ -263,6 +263,22 @@ function integrateSession(sessionId, options = {}, loadRegistry, saveRegistry, r
263
263
  mainPath: ROOT,
264
264
  };
265
265
 
266
+ // Write merge notification for other sessions to pick up
267
+ try {
268
+ const notifyDir = path.join(getAgileflowDir(ROOT), 'sessions');
269
+ if (!fs.existsSync(notifyDir)) {
270
+ fs.mkdirSync(notifyDir, { recursive: true });
271
+ }
272
+ const notifyPath = path.join(notifyDir, 'last-merge.json');
273
+ fs.writeFileSync(notifyPath, JSON.stringify({
274
+ merged_at: new Date().toISOString(),
275
+ session_id: sessionId,
276
+ branch: branchName,
277
+ strategy,
278
+ commit_message: commitMessage,
279
+ }, null, 2));
280
+ } catch (e) { /* ignore notification write failures */ }
281
+
266
282
  // Delete worktree first (before branch, as worktree holds ref)
267
283
  if (deleteWorktree && session.path !== ROOT && fs.existsSync(session.path)) {
268
284
  try {
package/lib/progress.js CHANGED
@@ -34,7 +34,8 @@ class Spinner {
34
34
  constructor(message, options = {}) {
35
35
  this.message = message;
36
36
  this.interval = options.interval || 80;
37
- this.enabled = options.enabled !== false && process.stdout.isTTY;
37
+ this.stream = options.stream || process.stdout;
38
+ this.enabled = options.enabled !== false && this.stream.isTTY;
38
39
  this.frameIndex = 0;
39
40
  this.timer = null;
40
41
  this.startTime = null;
@@ -108,9 +109,9 @@ class Spinner {
108
109
  const line = `${c.cyan}${frame}${c.reset} ${this.message}`;
109
110
 
110
111
  // Clear line and write new content
111
- process.stdout.clearLine(0);
112
- process.stdout.cursorTo(0);
113
- process.stdout.write(line);
112
+ this.stream.clearLine(0);
113
+ this.stream.cursorTo(0);
114
+ this.stream.write(line);
114
115
  }
115
116
 
116
117
  /**
@@ -163,8 +164,8 @@ class Spinner {
163
164
  }
164
165
 
165
166
  if (this.enabled) {
166
- process.stdout.clearLine(0);
167
- process.stdout.cursorTo(0);
167
+ this.stream.clearLine(0);
168
+ this.stream.cursorTo(0);
168
169
  }
169
170
 
170
171
  const elapsed = this.startTime ? Date.now() - this.startTime : 0;