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 +5 -0
- package/README.md +3 -3
- package/lib/dashboard-protocol.js +38 -0
- package/lib/dashboard-server.js +189 -7
- package/lib/feedback.js +35 -9
- package/lib/git-operations.js +4 -1
- package/lib/merge-operations.js +16 -0
- package/lib/progress.js +7 -6
- package/lib/session-operations.js +601 -0
- package/lib/session-switching.js +186 -0
- package/lib/template-loader.js +4 -2
- package/lib/worktree-operations.js +5 -25
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +12 -0
- package/scripts/batch-pmap-loop.js +11 -4
- package/scripts/claude-tmux.sh +186 -103
- package/scripts/lib/configure-features.js +6 -4
- package/scripts/lib/configure-repair.js +11 -2
- package/scripts/lib/process-cleanup.js +9 -5
- package/scripts/obtain-context.js +5 -0
- package/scripts/session-manager.js +144 -993
- package/scripts/spawn-parallel.js +15 -11
- package/src/core/commands/configure.md +55 -0
- package/src/core/commands/serve.md +127 -0
- package/src/core/commands/session/end.md +83 -22
- package/src/core/commands/session/new.md +197 -83
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
|
[](https://www.npmjs.com/package/agileflow)
|
|
6
|
-
[](docs/04-architecture/commands.md)
|
|
7
7
|
[](docs/04-architecture/subagents.md)
|
|
8
8
|
[](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) |
|
|
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
|
|
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,
|
package/lib/dashboard-server.js
CHANGED
|
@@ -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 =
|
|
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 (
|
|
712
|
-
|
|
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',
|
|
724
|
-
'
|
|
725
|
-
'http://
|
|
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(
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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,
|
package/lib/git-operations.js
CHANGED
|
@@ -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'], {
|
|
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) {
|
package/lib/merge-operations.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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;
|