agileflow 2.99.0 → 2.99.2

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.
Files changed (127) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -3
  3. package/lib/dashboard-protocol.js +38 -0
  4. package/lib/dashboard-server.js +197 -7
  5. package/lib/feedback.js +36 -9
  6. package/lib/git-operations.js +4 -1
  7. package/lib/merge-operations.js +25 -0
  8. package/lib/progress.js +7 -6
  9. package/lib/session-operations.js +611 -0
  10. package/lib/session-switching.js +191 -0
  11. package/lib/template-loader.js +4 -2
  12. package/lib/worktree-operations.js +5 -25
  13. package/package.json +1 -1
  14. package/scripts/agileflow-configure.js +13 -0
  15. package/scripts/agileflow-welcome.js +11 -6
  16. package/scripts/batch-pmap-loop.js +11 -4
  17. package/scripts/claude-tmux.sh +186 -103
  18. package/scripts/damage-control-bash.js +33 -3
  19. package/scripts/damage-control-edit.js +33 -3
  20. package/scripts/damage-control-write.js +33 -3
  21. package/scripts/lib/configure-features.js +10 -7
  22. package/scripts/lib/configure-repair.js +12 -2
  23. package/scripts/lib/process-cleanup.js +197 -15
  24. package/scripts/obtain-context.js +5 -0
  25. package/scripts/session-manager.js +156 -932
  26. package/scripts/spawn-parallel.js +15 -11
  27. package/src/core/agents/configuration/archival.md +2 -1
  28. package/src/core/agents/configuration/attribution.md +2 -1
  29. package/src/core/agents/configuration/ci.md +2 -1
  30. package/src/core/agents/configuration/damage-control.md +2 -1
  31. package/src/core/agents/configuration/git-config.md +2 -1
  32. package/src/core/agents/configuration/hooks.md +2 -1
  33. package/src/core/agents/configuration/precompact.md +2 -1
  34. package/src/core/agents/configuration/status-line.md +2 -1
  35. package/src/core/agents/configuration/verify.md +2 -1
  36. package/src/core/commands/adr/list.md +1 -1
  37. package/src/core/commands/adr/update.md +1 -1
  38. package/src/core/commands/adr/view.md +1 -1
  39. package/src/core/commands/adr.md +1 -1
  40. package/src/core/commands/agent.md +1 -1
  41. package/src/core/commands/api.md +1 -1
  42. package/src/core/commands/assign.md +1 -1
  43. package/src/core/commands/audit.md +1 -1
  44. package/src/core/commands/auto.md +1 -1
  45. package/src/core/commands/automate.md +1 -1
  46. package/src/core/commands/babysit.md +1 -1
  47. package/src/core/commands/baseline.md +1 -1
  48. package/src/core/commands/batch.md +1 -1
  49. package/src/core/commands/blockers.md +1 -1
  50. package/src/core/commands/board.md +1 -1
  51. package/src/core/commands/changelog.md +1 -1
  52. package/src/core/commands/choose.md +1 -1
  53. package/src/core/commands/ci.md +1 -1
  54. package/src/core/commands/compress.md +1 -1
  55. package/src/core/commands/configure.md +56 -1
  56. package/src/core/commands/context/export.md +1 -1
  57. package/src/core/commands/context/full.md +1 -1
  58. package/src/core/commands/context/note.md +1 -1
  59. package/src/core/commands/council.md +1 -1
  60. package/src/core/commands/debt.md +1 -1
  61. package/src/core/commands/deploy.md +1 -1
  62. package/src/core/commands/deps.md +1 -1
  63. package/src/core/commands/diagnose.md +1 -1
  64. package/src/core/commands/docs.md +1 -1
  65. package/src/core/commands/epic/list.md +1 -1
  66. package/src/core/commands/epic/view.md +1 -1
  67. package/src/core/commands/epic.md +1 -1
  68. package/src/core/commands/feedback.md +1 -1
  69. package/src/core/commands/handoff.md +1 -1
  70. package/src/core/commands/help.md +4 -190
  71. package/src/core/commands/ideate/history.md +1 -1
  72. package/src/core/commands/ideate/new.md +1 -1
  73. package/src/core/commands/impact.md +1 -1
  74. package/src/core/commands/install.md +1 -1
  75. package/src/core/commands/logic/audit.md +1 -1
  76. package/src/core/commands/maintain.md +1 -1
  77. package/src/core/commands/metrics.md +1 -1
  78. package/src/core/commands/multi-expert.md +1 -1
  79. package/src/core/commands/packages.md +1 -1
  80. package/src/core/commands/pr.md +1 -1
  81. package/src/core/commands/readme-sync.md +1 -1
  82. package/src/core/commands/research/analyze.md +1 -1
  83. package/src/core/commands/research/ask.md +1 -1
  84. package/src/core/commands/research/import.md +1 -1
  85. package/src/core/commands/research/list.md +1 -1
  86. package/src/core/commands/research/synthesize.md +1 -1
  87. package/src/core/commands/research/view.md +1 -1
  88. package/src/core/commands/retro.md +1 -1
  89. package/src/core/commands/review.md +1 -1
  90. package/src/core/commands/rlm.md +1 -1
  91. package/src/core/commands/roadmap/analyze.md +1 -1
  92. package/src/core/commands/rpi.md +1 -1
  93. package/src/core/commands/serve.md +127 -0
  94. package/src/core/commands/session/cleanup.md +1 -1
  95. package/src/core/commands/session/end.md +84 -23
  96. package/src/core/commands/session/history.md +1 -1
  97. package/src/core/commands/session/init.md +1 -1
  98. package/src/core/commands/session/new.md +198 -84
  99. package/src/core/commands/session/resume.md +1 -1
  100. package/src/core/commands/session/spawn.md +1 -1
  101. package/src/core/commands/session/status.md +1 -1
  102. package/src/core/commands/skill/create.md +1 -1
  103. package/src/core/commands/skill/delete.md +1 -1
  104. package/src/core/commands/skill/edit.md +1 -1
  105. package/src/core/commands/skill/list.md +1 -1
  106. package/src/core/commands/skill/test.md +1 -1
  107. package/src/core/commands/skill/upgrade.md +1 -1
  108. package/src/core/commands/sprint.md +1 -1
  109. package/src/core/commands/status.md +1 -1
  110. package/src/core/commands/story/list.md +1 -1
  111. package/src/core/commands/story/view.md +1 -1
  112. package/src/core/commands/story-validate.md +1 -1
  113. package/src/core/commands/story.md +1 -1
  114. package/src/core/commands/team/list.md +1 -1
  115. package/src/core/commands/team/start.md +1 -1
  116. package/src/core/commands/team/status.md +1 -1
  117. package/src/core/commands/team/stop.md +1 -1
  118. package/src/core/commands/template.md +1 -1
  119. package/src/core/commands/tests.md +1 -1
  120. package/src/core/commands/update.md +1 -1
  121. package/src/core/commands/validate-expertise.md +1 -1
  122. package/src/core/commands/velocity.md +1 -1
  123. package/src/core/commands/verify.md +1 -1
  124. package/src/core/commands/whats-new.md +1 -1
  125. package/src/core/commands/workflow.md +1 -1
  126. package/tools/cli/installers/ide/codex.js +12 -4
  127. package/tools/cli/lib/content-injector.js +23 -4
package/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.99.2] - 2026-02-09
11
+
12
+ ### Added
13
+ - Documentation overhaul with teams commands, frontmatter fixes, and damage control resilience
14
+
15
+ ## [2.99.1] - 2026-02-08
16
+
17
+ ### Added
18
+ - Smart session management with context-aware naming and tmux quick-create keybinds
19
+
10
20
  ## [2.99.0] - 2026-02-08
11
21
 
12
22
  ### 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,170 @@ 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(
1314
+ sid =>
1315
+ stories[sid] &&
1316
+ (stories[sid].status === 'done' || stories[sid].status === 'completed')
1317
+ ).length,
1318
+ })),
1319
+ };
1320
+
1321
+ session.send(createStatusUpdate(summary));
1322
+ } catch (error) {
1323
+ console.error('[Status Update Error]', error.message);
1324
+ }
1325
+ }
1326
+
1327
+ /**
1328
+ * Send session list with sync status to dashboard
1329
+ */
1330
+ sendSessionList(session) {
1331
+ const sessions = [];
1332
+
1333
+ for (const [id, s] of this.sessions) {
1334
+ const entry = {
1335
+ id,
1336
+ name: s.metadata.name || id,
1337
+ type: s.metadata.type || 'local',
1338
+ status: s.state === 'connected' ? 'active' : s.state === 'disconnected' ? 'idle' : s.state,
1339
+ branch: null,
1340
+ messageCount: s.messages.length,
1341
+ lastActivity: s.lastActivity.toISOString(),
1342
+ syncStatus: 'offline',
1343
+ ahead: 0,
1344
+ behind: 0,
1345
+ };
1346
+
1347
+ // Get branch and sync status via git
1348
+ try {
1349
+ const cwd = s.metadata.worktreePath || this.projectRoot;
1350
+ entry.branch = execFileSync('git', ['branch', '--show-current'], {
1351
+ cwd,
1352
+ encoding: 'utf8',
1353
+ stdio: ['pipe', 'pipe', 'pipe'],
1354
+ }).trim();
1355
+
1356
+ // Get ahead/behind counts relative to upstream
1357
+ try {
1358
+ const counts = execFileSync(
1359
+ 'git',
1360
+ ['rev-list', '--left-right', '--count', 'HEAD...@{u}'],
1361
+ {
1362
+ cwd,
1363
+ encoding: 'utf8',
1364
+ stdio: ['pipe', 'pipe', 'pipe'],
1365
+ }
1366
+ ).trim();
1367
+ const [ahead, behind] = counts.split(/\s+/).map(Number);
1368
+ entry.ahead = ahead || 0;
1369
+ entry.behind = behind || 0;
1370
+
1371
+ if (ahead > 0 && behind > 0) {
1372
+ entry.syncStatus = 'diverged';
1373
+ } else if (ahead > 0) {
1374
+ entry.syncStatus = 'ahead';
1375
+ } else if (behind > 0) {
1376
+ entry.syncStatus = 'behind';
1377
+ } else {
1378
+ entry.syncStatus = 'synced';
1379
+ }
1380
+ } catch {
1381
+ // No upstream configured
1382
+ entry.syncStatus = 'synced';
1383
+ }
1384
+ } catch {
1385
+ entry.syncStatus = 'offline';
1386
+ }
1387
+
1388
+ sessions.push(entry);
1389
+ }
1390
+
1391
+ session.send(createSessionList(sessions));
1392
+ }
1393
+
1394
+ /**
1395
+ * Handle open file in editor request
1396
+ */
1397
+ handleOpenFile(session, message) {
1398
+ const { path: filePath, line } = message;
1399
+
1400
+ if (!filePath || typeof filePath !== 'string') {
1401
+ session.send(createError('INVALID_REQUEST', 'File path is required'));
1402
+ return;
1403
+ }
1404
+
1405
+ // Validate the path stays within project root
1406
+ const pathResult = validatePath(filePath, this.projectRoot, { allowSymlinks: true });
1407
+ if (!pathResult.ok) {
1408
+ session.send(createError('OPEN_FILE_ERROR', 'File path outside project'));
1409
+ return;
1410
+ }
1411
+
1412
+ const fullPath = pathResult.resolvedPath;
1413
+
1414
+ // Detect editor from environment
1415
+ const editor = process.env.VISUAL || process.env.EDITOR || 'code';
1416
+ const editorBase = require('path').basename(editor).toLowerCase();
1417
+
1418
+ try {
1419
+ const lineNum = Number.isFinite(line) && line > 0 ? line : null;
1420
+
1421
+ switch (editorBase) {
1422
+ case 'code':
1423
+ case 'cursor':
1424
+ case 'windsurf': {
1425
+ const gotoArg = lineNum ? `${fullPath}:${lineNum}` : fullPath;
1426
+ spawn(editor, ['--goto', gotoArg], { detached: true, stdio: 'ignore' }).unref();
1427
+ break;
1428
+ }
1429
+ case 'subl':
1430
+ case 'sublime_text': {
1431
+ const sublArg = lineNum ? `${fullPath}:${lineNum}` : fullPath;
1432
+ spawn(editor, [sublArg], { detached: true, stdio: 'ignore' }).unref();
1433
+ break;
1434
+ }
1435
+ default: {
1436
+ // Generic: just open the file
1437
+ spawn(editor, [fullPath], { detached: true, stdio: 'ignore' }).unref();
1438
+ break;
1439
+ }
1440
+ }
1441
+
1442
+ session.send(
1443
+ createNotification('info', 'Editor', `Opened ${require('path').basename(fullPath)}`)
1444
+ );
1445
+ } catch (error) {
1446
+ console.error('[Open File Error]', error.message);
1447
+ session.send(createError('OPEN_FILE_ERROR', `Failed to open file: ${error.message}`));
1448
+ }
1449
+ }
1450
+
1263
1451
  /**
1264
1452
  * Handle terminal spawn request
1265
1453
  */
@@ -1271,7 +1459,9 @@ class DashboardServer extends EventEmitter {
1271
1459
  if (cwd) {
1272
1460
  const cwdResult = validatePath(cwd, this.projectRoot, { allowSymlinks: true });
1273
1461
  if (!cwdResult.ok) {
1274
- session.send(createError('TERMINAL_ERROR', 'Working directory must be within project root'));
1462
+ session.send(
1463
+ createError('TERMINAL_ERROR', 'Working directory must be within project root')
1464
+ );
1275
1465
  return;
1276
1466
  }
1277
1467
  safeCwd = cwdResult.resolvedPath;
package/lib/feedback.js CHANGED
@@ -72,7 +72,10 @@ 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 =
76
+ options.quiet !== undefined
77
+ ? options.quiet
78
+ : process.env.AGILEFLOW_QUIET === '1' || process.env.AGILEFLOW_QUIET === 'true';
76
79
  this.verbose = options.verbose || false;
77
80
  }
78
81
 
@@ -274,7 +277,8 @@ class Feedback {
274
277
  class FeedbackSpinner {
275
278
  constructor(message, options = {}) {
276
279
  this.message = message;
277
- this.isTTY = options.isTTY !== undefined ? options.isTTY : process.stdout.isTTY;
280
+ this.stream = options.stream || process.stdout;
281
+ this.isTTY = options.isTTY !== undefined ? options.isTTY : this.stream.isTTY;
278
282
  this.indent = options.indent || 0;
279
283
  this.interval = options.interval || 80;
280
284
  this.frameIndex = 0;
@@ -286,13 +290,26 @@ class FeedbackSpinner {
286
290
  return ' '.repeat(this.indent);
287
291
  }
288
292
 
293
+ /**
294
+ * Write a line to the appropriate stream
295
+ * @param {string} text - Text to write
296
+ * @private
297
+ */
298
+ _writeln(text) {
299
+ if (this.stream !== process.stdout) {
300
+ this.stream.write(text + '\n');
301
+ } else {
302
+ console.log(text);
303
+ }
304
+ }
305
+
289
306
  /**
290
307
  * Start the spinner
291
308
  * @returns {FeedbackSpinner}
292
309
  */
293
310
  start() {
294
311
  if (!this.isTTY) {
295
- console.log(`${this._prefix()}${c.dim}${this.message}${c.reset}`);
312
+ this._writeln(`${this._prefix()}${c.dim}${this.message}${c.reset}`);
296
313
  return this;
297
314
  }
298
315
 
@@ -327,9 +344,9 @@ class FeedbackSpinner {
327
344
  if (!this.isTTY) return;
328
345
  const frame = SPINNER_FRAMES[this.frameIndex];
329
346
  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);
347
+ this.stream.clearLine(0);
348
+ this.stream.cursorTo(0);
349
+ this.stream.write(line);
333
350
  }
334
351
 
335
352
  /**
@@ -346,8 +363,8 @@ class FeedbackSpinner {
346
363
  }
347
364
 
348
365
  if (this.isTTY) {
349
- process.stdout.clearLine(0);
350
- process.stdout.cursorTo(0);
366
+ this.stream.clearLine(0);
367
+ this.stream.cursorTo(0);
351
368
  }
352
369
 
353
370
  const elapsed = this.startTime ? Date.now() - this.startTime : 0;
@@ -355,7 +372,7 @@ class FeedbackSpinner {
355
372
  elapsed > DOHERTY_THRESHOLD_MS ? ` ${c.dim}(${formatDuration(elapsed)})${c.reset}` : '';
356
373
  const msg = message || this.message;
357
374
 
358
- console.log(`${this._prefix()}${color}${symbol}${c.reset} ${msg}${suffix}`);
375
+ this._writeln(`${this._prefix()}${color}${symbol}${c.reset} ${msg}${suffix}`);
359
376
  return this;
360
377
  }
361
378
 
@@ -548,12 +565,22 @@ class FeedbackProgressBar {
548
565
  // Singleton instance for convenience
549
566
  const feedback = new Feedback();
550
567
 
568
+ /**
569
+ * Factory function to create a configured Feedback instance
570
+ * @param {Object} [options={}] - Feedback options
571
+ * @returns {Feedback}
572
+ */
573
+ function createFeedback(options = {}) {
574
+ return new Feedback(options);
575
+ }
576
+
551
577
  module.exports = {
552
578
  Feedback,
553
579
  FeedbackSpinner,
554
580
  FeedbackTask,
555
581
  FeedbackProgressBar,
556
582
  feedback,
583
+ createFeedback,
557
584
  SYMBOLS,
558
585
  SPINNER_FRAMES,
559
586
  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,31 @@ 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(
274
+ notifyPath,
275
+ JSON.stringify(
276
+ {
277
+ merged_at: new Date().toISOString(),
278
+ session_id: sessionId,
279
+ branch: branchName,
280
+ strategy,
281
+ commit_message: commitMessage,
282
+ },
283
+ null,
284
+ 2
285
+ )
286
+ );
287
+ } catch (e) {
288
+ /* ignore notification write failures */
289
+ }
290
+
266
291
  // Delete worktree first (before branch, as worktree holds ref)
267
292
  if (deleteWorktree && session.path !== ROOT && fs.existsSync(session.path)) {
268
293
  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;