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.
@@ -13,76 +13,50 @@
13
13
  * - lib/merge-operations.js - Merge, conflict resolution, smart-merge
14
14
  * - lib/worktree-operations.js - Worktree creation, cleanup, thread types
15
15
  * - lib/session-display.js - Kanban, table formatting, health checks
16
+ * - lib/session-operations.js - Session CRUD, listing, stale lock cleanup
17
+ * - lib/session-switching.js - Session switching, thread type management
16
18
  */
17
19
 
18
20
  const fs = require('fs');
19
21
  const path = require('path');
20
- const { spawnSync } = require('child_process');
21
22
 
22
23
  // Shared utilities
23
24
  const { c } = require('../lib/colors');
24
- const {
25
- getProjectRoot,
26
- getStatusPath,
27
- getSessionStatePath,
28
- getAgileflowDir,
29
- } = require('../lib/paths');
30
- const { safeReadJSON } = require('../lib/errors');
31
- const { isValidBranchName, isValidSessionNickname } = require('../lib/validate');
25
+ const { getProjectRoot, getAgileflowDir } = require('../lib/paths');
32
26
 
33
27
  // Session registry
34
28
  const { SessionRegistry } = require('../lib/session-registry');
35
- const { sessionThreadMachine } = require('../lib/state-machine');
36
29
 
37
30
  // Lock file operations
38
31
  const {
39
- getLockPath: _getLockPath,
40
32
  readLock: _readLock,
41
33
  readLockAsync: _readLockAsync,
42
34
  writeLock: _writeLock,
43
35
  removeLock: _removeLock,
44
- isPidAlive,
45
36
  isSessionActive: _isSessionActive,
46
37
  isSessionActiveAsync: _isSessionActiveAsync,
47
38
  } = require('../lib/lock-file');
48
39
 
49
- // Flag detection for session propagation
50
- const { getInheritedFlags, detectParentSessionFlags } = require('../lib/flag-detection');
51
-
52
40
  // Git operations module
53
41
  const gitOps = require('../lib/git-operations');
54
42
  const {
55
43
  gitCache,
56
44
  execGitAsync,
57
- getCurrentBranch,
58
45
  getMainBranch,
59
46
  SESSION_PHASES,
60
- determinePhaseFromGitState,
61
- getSessionPhaseEarlyExit,
62
47
  getSessionPhase,
63
48
  getSessionPhaseAsync,
64
49
  getSessionPhasesAsync,
65
50
  } = gitOps;
66
51
 
67
52
  // Worktree operations module
68
- const worktreeOps = require('../lib/worktree-operations');
69
- const {
70
- THREAD_TYPES,
71
- DEFAULT_WORKTREE_TIMEOUT_MS,
72
- isGitWorktree,
73
- detectThreadType,
74
- progressIndicator,
75
- createWorktreeWithTimeout,
76
- cleanupFailedWorktree,
77
- } = worktreeOps;
53
+ const { THREAD_TYPES, detectThreadType } = require('../lib/worktree-operations');
78
54
 
79
55
  // Session display module
80
56
  const displayOps = require('../lib/session-display');
81
57
  const {
82
58
  getFileDetails,
83
59
  getSessionsHealth: _getSessionsHealth,
84
- formatKanbanBoard,
85
- groupSessionsByPhase,
86
60
  renderKanbanBoard,
87
61
  renderKanbanBoardAsync,
88
62
  formatSessionsTable,
@@ -91,42 +65,19 @@ const {
91
65
  // Merge operations module
92
66
  const mergeOps = require('../lib/merge-operations');
93
67
 
94
- // Agent Teams integration (lazy-loaded)
95
- let _featureFlags, _teamManager;
96
- function getFeatureFlags() {
97
- if (!_featureFlags) {
98
- try {
99
- _featureFlags = require('../lib/feature-flags');
100
- } catch (e) {
101
- _featureFlags = null;
102
- }
103
- }
104
- return _featureFlags;
105
- }
106
- function getTeamManager() {
107
- if (!_teamManager) {
108
- try {
109
- _teamManager = require('./team-manager');
110
- } catch (e) {
111
- _teamManager = null;
112
- }
113
- }
114
- return _teamManager;
115
- }
68
+ // Extracted modules
69
+ const { createSessionOperations } = require('../lib/session-operations');
70
+ const { createSessionSwitching } = require('../lib/session-switching');
116
71
 
117
72
  // Constants
118
73
  const ROOT = getProjectRoot();
119
74
  const SESSIONS_DIR = path.join(getAgileflowDir(ROOT), 'sessions');
120
- const REGISTRY_PATH = path.join(SESSIONS_DIR, 'registry.json');
121
75
 
122
76
  // Injectable registry instance for testing
123
77
  let _registryInstance = null;
124
78
  let _registryInitialized = false;
125
79
 
126
- // ============================================================================
127
- // Registry Management
128
- // ============================================================================
129
-
80
+ // --- Registry Management ---
130
81
  function getRegistryInstance() {
131
82
  if (!_registryInstance) {
132
83
  _registryInstance = new SessionRegistry(ROOT);
@@ -146,12 +97,6 @@ function resetRegistryCache() {
146
97
  }
147
98
  }
148
99
 
149
- function ensureSessionsDir() {
150
- if (!fs.existsSync(SESSIONS_DIR)) {
151
- fs.mkdirSync(SESSIONS_DIR, { recursive: true });
152
- }
153
- }
154
-
155
100
  function loadRegistry() {
156
101
  const registryInstance = getRegistryInstance();
157
102
  if (!_registryInitialized) {
@@ -171,13 +116,7 @@ function saveRegistry(registryData) {
171
116
  return registry.saveSync(registryData);
172
117
  }
173
118
 
174
- // ============================================================================
175
- // Lock File Wrappers (bind to SESSIONS_DIR)
176
- // ============================================================================
177
-
178
- function getLockPath(sessionId) {
179
- return _getLockPath(SESSIONS_DIR, sessionId);
180
- }
119
+ // --- Lock File Wrappers (bind to SESSIONS_DIR) ---
181
120
  function readLock(sessionId) {
182
121
  return _readLock(SESSIONS_DIR, sessionId);
183
122
  }
@@ -197,655 +136,103 @@ async function isSessionActiveAsync(sessionId) {
197
136
  return _isSessionActiveAsync(SESSIONS_DIR, sessionId);
198
137
  }
199
138
 
200
- // ============================================================================
201
- // Stale Lock Cleanup
202
- // ============================================================================
203
-
204
- function processStalelock(id, session, lock, dryRun) {
205
- if (!lock) return null;
206
- const pid = parseInt(lock.pid, 10);
207
- if (isPidAlive(pid)) return null;
208
- if (!dryRun) removeLock(id);
209
- return {
210
- id,
211
- nickname: session.nickname,
212
- branch: session.branch,
213
- pid,
214
- reason: 'pid_dead',
215
- path: session.path,
216
- };
217
- }
218
-
219
- function cleanupStaleLocks(registry, options = {}) {
220
- const { dryRun = false } = options;
221
- const cleanedSessions = [];
222
- for (const [id, session] of Object.entries(registry.sessions)) {
223
- const result = processStalelock(id, session, readLock(id), dryRun);
224
- if (result) cleanedSessions.push(result);
225
- }
226
- return { count: cleanedSessions.length, sessions: cleanedSessions };
227
- }
228
-
229
- async function cleanupStaleLocksAsync(registry, options = {}) {
230
- const { dryRun = false } = options;
231
- const sessionEntries = Object.entries(registry.sessions);
232
- if (sessionEntries.length === 0) return { count: 0, sessions: [] };
233
-
234
- const lockResults = await Promise.all(
235
- sessionEntries.map(async ([id, session]) => ({
236
- id,
237
- session,
238
- lock: await readLockAsync(id),
239
- }))
240
- );
139
+ // --- Instantiate extracted modules ---
140
+ const sessionOps = createSessionOperations({
141
+ ROOT,
142
+ loadRegistry,
143
+ saveRegistry,
144
+ readLock,
145
+ readLockAsync,
146
+ writeLock,
147
+ removeLock,
148
+ isSessionActive,
149
+ isSessionActiveAsync,
150
+ c,
151
+ });
241
152
 
242
- const cleanedSessions = lockResults
243
- .map(({ id, session, lock }) => processStalelock(id, session, lock, dryRun))
244
- .filter(Boolean);
153
+ const sessionSwitchOps = createSessionSwitching({
154
+ ROOT,
155
+ loadRegistry,
156
+ saveRegistry,
157
+ });
245
158
 
246
- return { count: cleanedSessions.length, sessions: cleanedSessions };
247
- }
159
+ // Destructure for local use and re-export
160
+ const {
161
+ cleanupStaleLocks,
162
+ cleanupStaleLocksAsync,
163
+ registerSession,
164
+ unregisterSession,
165
+ getSession,
166
+ createSession,
167
+ createTeamSession,
168
+ getSessions,
169
+ getSessionsAsync,
170
+ getActiveSessionCount,
171
+ fullStatus,
172
+ deleteSession,
173
+ } = sessionOps;
248
174
 
249
- // ============================================================================
250
- // Session Health (wrapper for display module)
251
- // ============================================================================
175
+ const {
176
+ switchSession,
177
+ clearActiveSession,
178
+ getActiveSession,
179
+ getSessionThreadType,
180
+ setSessionThreadType,
181
+ transitionThread,
182
+ getValidThreadTransitions,
183
+ } = sessionSwitchOps;
252
184
 
185
+ // --- Session Health ---
253
186
  function getSessionsHealth(options = {}) {
254
187
  return _getSessionsHealth(options, loadRegistry);
255
188
  }
256
189
 
257
- // ============================================================================
258
- // Current Story Helper
259
- // ============================================================================
260
-
261
- function getCurrentStory() {
262
- const statusPath = getStatusPath(ROOT);
263
- const result = safeReadJSON(statusPath, { defaultValue: null });
264
- if (!result.ok || !result.data) return null;
265
- for (const [id, story] of Object.entries(result.data.stories || {})) {
266
- if (story.status === 'in_progress') return { id, title: story.title };
267
- }
268
- return null;
269
- }
270
-
271
- // ============================================================================
272
- // Session CRUD Operations
273
- // ============================================================================
274
-
275
- function registerSession(nickname = null, threadType = null) {
276
- const registry = loadRegistry();
277
- const cwd = process.cwd();
278
- const branch = getCurrentBranch();
279
- const story = getCurrentStory();
280
- const pid = process.ppid || process.pid;
281
-
282
- let existingId = null;
283
- for (const [id, session] of Object.entries(registry.sessions)) {
284
- if (session.path === cwd) {
285
- existingId = id;
286
- break;
287
- }
288
- }
289
-
290
- if (existingId) {
291
- registry.sessions[existingId].branch = branch;
292
- registry.sessions[existingId].story = story ? story.id : null;
293
- registry.sessions[existingId].last_active = new Date().toISOString();
294
- if (nickname) registry.sessions[existingId].nickname = nickname;
295
- if (threadType && THREAD_TYPES.includes(threadType)) {
296
- registry.sessions[existingId].thread_type = threadType;
297
- }
298
- writeLock(existingId, pid);
299
- saveRegistry(registry);
300
- return { id: existingId, isNew: false };
301
- }
302
-
303
- const sessionId = String(registry.next_id);
304
- registry.next_id++;
305
- const isMain = cwd === ROOT && !isGitWorktree(cwd);
306
- const detectedType =
307
- threadType && THREAD_TYPES.includes(threadType) ? threadType : detectThreadType(null, !isMain);
308
-
309
- registry.sessions[sessionId] = {
310
- path: cwd,
311
- branch,
312
- story: story ? story.id : null,
313
- nickname: nickname || null,
314
- created: new Date().toISOString(),
315
- last_active: new Date().toISOString(),
316
- is_main: isMain,
317
- thread_type: detectedType,
318
- };
319
-
320
- writeLock(sessionId, pid);
321
- saveRegistry(registry);
322
- return { id: sessionId, isNew: true, thread_type: detectedType };
323
- }
324
-
325
- function unregisterSession(sessionId) {
326
- const registry = loadRegistry();
327
- if (registry.sessions[sessionId]) {
328
- registry.sessions[sessionId].last_active = new Date().toISOString();
329
- removeLock(sessionId);
330
- saveRegistry(registry);
331
- }
332
- }
333
-
334
- function getSession(sessionId) {
335
- const registry = loadRegistry();
336
- const session = registry.sessions[sessionId];
337
- if (!session) return null;
338
- const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
339
- return { id: sessionId, ...session, thread_type: threadType, active: isSessionActive(sessionId) };
340
- }
341
-
342
- async function createSession(options = {}) {
343
- const registry = loadRegistry();
344
- const sessionId = String(registry.next_id);
345
- const projectName = registry.project_name;
346
-
347
- const nickname = options.nickname || null;
348
- const branchName = options.branch || `session-${sessionId}`;
349
- const dirName = nickname || sessionId;
350
-
351
- if (!isValidBranchName(branchName)) {
352
- return {
353
- success: false,
354
- error: `Invalid branch name: "${branchName}". Use only letters, numbers, hyphens, underscores, and forward slashes.`,
355
- };
356
- }
357
- if (nickname && !isValidSessionNickname(nickname)) {
358
- return {
359
- success: false,
360
- error: `Invalid nickname: "${nickname}". Use only letters, numbers, hyphens, and underscores.`,
361
- };
362
- }
363
-
364
- const worktreePath = path.resolve(ROOT, '..', `${projectName}-${dirName}`);
365
- if (fs.existsSync(worktreePath)) {
366
- return { success: false, error: `Directory already exists: ${worktreePath}` };
367
- }
368
-
369
- // Create branch if needed
370
- const checkRef = spawnSync(
371
- 'git',
372
- ['show-ref', '--verify', '--quiet', `refs/heads/${branchName}`],
373
- { cwd: ROOT, encoding: 'utf8' }
374
- );
375
- let branchCreatedByUs = false;
376
- if (checkRef.status !== 0) {
377
- const createBranch = spawnSync('git', ['branch', branchName], { cwd: ROOT, encoding: 'utf8' });
378
- if (createBranch.status !== 0) {
379
- return {
380
- success: false,
381
- error: `Failed to create branch: ${createBranch.stderr || 'unknown error'}`,
382
- };
383
- }
384
- branchCreatedByUs = true;
385
- }
386
-
387
- const timeoutMs = options.timeout || DEFAULT_WORKTREE_TIMEOUT_MS;
388
- const stopProgress = progressIndicator(
389
- 'Creating worktree (this may take a while for large repos)'
390
- );
391
-
392
- try {
393
- await createWorktreeWithTimeout(worktreePath, branchName, timeoutMs);
394
- stopProgress();
395
- process.stderr.write(`✓ Worktree created successfully\n`);
396
- } catch (error) {
397
- stopProgress();
398
- cleanupFailedWorktree(worktreePath, branchName, branchCreatedByUs);
399
- return { success: false, error: error.message };
400
- }
401
-
402
- // Copy env files
403
- const envFiles = ['.env', '.env.local', '.env.development', '.env.test', '.env.production'];
404
- const copiedEnvFiles = [];
405
- for (const envFile of envFiles) {
406
- const src = path.join(ROOT, envFile);
407
- const dest = path.join(worktreePath, envFile);
408
- if (fs.existsSync(src) && !fs.existsSync(dest)) {
409
- try {
410
- fs.copyFileSync(src, dest);
411
- copiedEnvFiles.push(envFile);
412
- } catch (e) {
413
- /* ignore */
414
- }
415
- }
416
- }
417
-
418
- // Copy config folders
419
- const configFoldersToCopy = ['.claude', '.agileflow'];
420
- const copiedFolders = [];
421
- for (const folder of configFoldersToCopy) {
422
- const src = path.join(ROOT, folder);
423
- const dest = path.join(worktreePath, folder);
424
- if (fs.existsSync(src)) {
425
- try {
426
- fs.cpSync(src, dest, { recursive: true, force: true });
427
- copiedFolders.push(folder);
428
- } catch (e) {
429
- /* ignore */
430
- }
431
- }
432
- }
433
-
434
- // Symlink sessions directory
435
- const sessionsSymlinkSrc = path.join(ROOT, '.agileflow', 'sessions');
436
- const sessionsSymlinkDest = path.join(worktreePath, '.agileflow', 'sessions');
437
- if (fs.existsSync(sessionsSymlinkSrc)) {
438
- try {
439
- if (fs.existsSync(sessionsSymlinkDest))
440
- fs.rmSync(sessionsSymlinkDest, { recursive: true, force: true });
441
- const relPath = path.relative(path.dirname(sessionsSymlinkDest), sessionsSymlinkSrc);
442
- fs.symlinkSync(relPath, sessionsSymlinkDest, 'dir');
443
- } catch (e) {
444
- /* ignore */
445
- }
446
- }
447
-
448
- // Symlink docs
449
- const foldersToSymlink = ['docs'];
450
- const symlinkedFolders = [];
451
- for (const folder of foldersToSymlink) {
452
- const src = path.join(ROOT, folder);
453
- const dest = path.join(worktreePath, folder);
454
- if (fs.existsSync(src)) {
455
- try {
456
- if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
457
- const relPath = path.relative(worktreePath, src);
458
- fs.symlinkSync(relPath, dest, 'dir');
459
- symlinkedFolders.push(folder);
460
- } catch (e) {
461
- try {
462
- fs.cpSync(src, dest, { recursive: true, force: true });
463
- copiedFolders.push(folder);
464
- } catch (copyErr) {
465
- /* ignore */
466
- }
467
- }
468
- }
469
- }
470
-
471
- // Detect inherited flags from parent Claude session
472
- const inheritedFlags = options.inheritFlags !== false ? getInheritedFlags() : '';
473
-
474
- registry.next_id++;
475
- registry.sessions[sessionId] = {
476
- path: worktreePath,
477
- branch: branchName,
478
- story: null,
479
- nickname,
480
- created: new Date().toISOString(),
481
- last_active: new Date().toISOString(),
482
- is_main: false,
483
- thread_type: options.thread_type || 'parallel',
484
- inherited_flags: inheritedFlags || null,
485
- };
486
- saveRegistry(registry);
487
-
488
- // Build the command with inherited flags
489
- const claudeCmd = inheritedFlags ? `claude ${inheritedFlags}` : 'claude';
490
-
491
- return {
492
- success: true,
493
- sessionId,
494
- path: worktreePath,
495
- branch: branchName,
496
- thread_type: registry.sessions[sessionId].thread_type,
497
- command: `cd "${worktreePath}" && ${claudeCmd}`,
498
- inheritedFlags: inheritedFlags || null,
499
- envFilesCopied: copiedEnvFiles,
500
- foldersCopied: copiedFolders,
501
- foldersSymlinked: symlinkedFolders,
502
- };
503
- }
504
-
505
- /**
506
- * Create a native Agent Teams session instead of a worktree session.
507
- * Falls back to worktree mode if Agent Teams is not enabled.
508
- *
509
- * @param {object} options - { template, nickname }
510
- * @returns {object} Result with session info
511
- */
512
- function createTeamSession(options = {}) {
513
- const ff = getFeatureFlags();
514
- const templateName = options.template || 'fullstack';
515
-
516
- // Check if Agent Teams is enabled
517
- if (!ff || !ff.isAgentTeamsEnabled({ rootDir: ROOT })) {
518
- console.error(`${c.yellow}Agent Teams not enabled. Falling back to worktree mode.${c.reset}`);
519
- return createSession({
520
- nickname: options.nickname || `team-${templateName}`,
521
- thread_type: 'parallel',
522
- });
523
- }
524
-
525
- // Use team-manager to start the team
526
- const tm = getTeamManager();
527
- if (!tm) {
528
- return { success: false, error: 'team-manager module not available' };
529
- }
530
-
531
- const teamResult = tm.startTeam(ROOT, templateName);
532
- if (!teamResult.ok) {
533
- return { success: false, error: teamResult.error || 'Failed to start team' };
534
- }
535
-
536
- // Register as a session in the registry
537
- const registry = loadRegistry();
538
- const sessionId = String(registry.next_id);
539
- registry.next_id++;
540
-
541
- registry.sessions[sessionId] = {
542
- path: ROOT,
543
- branch: getCurrentBranch(),
544
- story: null,
545
- nickname: options.nickname || `team-${templateName}`,
546
- created: new Date().toISOString(),
547
- last_active: new Date().toISOString(),
548
- is_main: true,
549
- type: 'team',
550
- thread_type: 'team',
551
- team_name: templateName,
552
- team_lead: teamResult.lead || null,
553
- teammates: teamResult.teammates || [],
554
- };
555
- saveRegistry(registry);
556
-
557
- return {
558
- success: true,
559
- sessionId,
560
- type: 'team',
561
- template: templateName,
562
- mode: teamResult.mode,
563
- teammates: teamResult.teammates || [],
564
- path: ROOT,
565
- branch: getCurrentBranch(),
566
- };
567
- }
568
-
569
- function buildSessionsList(registrySessions, activeChecks, cwd) {
570
- const sessions = Object.entries(registrySessions).map(([id, session]) => ({
571
- id,
572
- ...session,
573
- active: activeChecks[id] || false,
574
- current: session.path === cwd,
575
- }));
576
- sessions.sort((a, b) => parseInt(a.id) - parseInt(b.id));
577
- return sessions;
578
- }
579
-
580
- function getSessions() {
581
- const registry = loadRegistry();
582
- const cleanupResult = cleanupStaleLocks(registry);
583
- const cwd = process.cwd();
584
- const activeChecks = {};
585
- for (const id of Object.keys(registry.sessions)) activeChecks[id] = isSessionActive(id);
586
- return {
587
- sessions: buildSessionsList(registry.sessions, activeChecks, cwd),
588
- cleaned: cleanupResult.count,
589
- cleanedSessions: cleanupResult.sessions,
590
- };
591
- }
592
-
593
- async function getSessionsAsync() {
594
- const registry = loadRegistry();
595
- const cleanupResult = await cleanupStaleLocksAsync(registry);
596
- const sessionEntries = Object.entries(registry.sessions);
597
- const cwd = process.cwd();
598
- const activeResults = await Promise.all(
599
- sessionEntries.map(async ([id]) => [id, await isSessionActiveAsync(id)])
600
- );
601
- const activeChecks = Object.fromEntries(activeResults);
602
- return {
603
- sessions: buildSessionsList(registry.sessions, activeChecks, cwd),
604
- cleaned: cleanupResult.count,
605
- cleanedSessions: cleanupResult.sessions,
606
- };
607
- }
608
-
609
- function getActiveSessionCount() {
610
- const { sessions } = getSessions();
611
- const cwd = process.cwd();
612
- return sessions.filter(s => s.active && s.path !== cwd).length;
613
- }
614
-
615
- function deleteSession(sessionId, removeWorktree = false) {
616
- const registry = loadRegistry();
617
- const session = registry.sessions[sessionId];
618
- if (!session) return { success: false, error: `Session ${sessionId} not found` };
619
- if (session.is_main) return { success: false, error: 'Cannot delete main session' };
620
-
621
- removeLock(sessionId);
622
- if (removeWorktree && fs.existsSync(session.path)) {
623
- const { execFileSync } = require('child_process');
624
- try {
625
- execFileSync('git', ['worktree', 'remove', session.path], { cwd: ROOT, encoding: 'utf8' });
626
- } catch (e) {
627
- try {
628
- execFileSync('git', ['worktree', 'remove', '--force', session.path], {
629
- cwd: ROOT,
630
- encoding: 'utf8',
631
- });
632
- } catch (e2) {
633
- return { success: false, error: `Failed to remove worktree: ${e2.message}` };
634
- }
635
- }
636
- }
637
- delete registry.sessions[sessionId];
638
- saveRegistry(registry);
639
- return { success: true };
640
- }
641
-
642
- // ============================================================================
643
- // Session Switching
644
- // ============================================================================
645
-
646
- const SESSION_STATE_PATH = getSessionStatePath(ROOT);
647
-
648
- function switchSession(sessionIdOrNickname) {
649
- const registry = loadRegistry();
650
- let targetSession = null,
651
- targetId = null;
652
- for (const [id, session] of Object.entries(registry.sessions)) {
653
- if (id === sessionIdOrNickname || session.nickname === sessionIdOrNickname) {
654
- targetSession = session;
655
- targetId = id;
656
- break;
657
- }
658
- }
659
- if (!targetSession)
660
- return { success: false, error: `Session "${sessionIdOrNickname}" not found` };
661
- if (!fs.existsSync(targetSession.path))
662
- return { success: false, error: `Session directory does not exist: ${targetSession.path}` };
190
+ // Merge operation wrappers (delegate to merge-operations module)
191
+ const checkMergeability = id => mergeOps.checkMergeability(id, loadRegistry);
192
+ const getMergePreview = id => mergeOps.getMergePreview(id, loadRegistry);
193
+ const integrateSession = (id, opts = {}) =>
194
+ mergeOps.integrateSession(id, opts, loadRegistry, saveRegistry, removeLock);
195
+ const commitChanges = (id, opts = {}) => mergeOps.commitChanges(id, opts, loadRegistry);
196
+ const stashChanges = id => mergeOps.stashChanges(id, loadRegistry);
197
+ const unstashChanges = id => mergeOps.unstashChanges(id);
198
+ const discardChanges = id => mergeOps.discardChanges(id, loadRegistry);
199
+ const categorizeFile = fp => mergeOps.categorizeFile(fp);
200
+ const getMergeStrategy = cat => mergeOps.getMergeStrategy(cat);
201
+ const getConflictingFiles = id => mergeOps.getConflictingFiles(id, loadRegistry);
202
+ const getMergeHistory = () => mergeOps.getMergeHistory();
203
+ const smartMerge = (id, opts = {}) =>
204
+ mergeOps.smartMerge(id, opts, loadRegistry, saveRegistry, removeLock, unregisterSession);
205
+
206
+ // --- CLI Interface ---
207
+ function main() {
208
+ const args = process.argv.slice(2);
209
+ const command = args[0];
663
210
 
664
- let sessionState = {};
665
- if (fs.existsSync(SESSION_STATE_PATH)) {
666
- try {
667
- sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
668
- } catch (e) {
669
- /* start fresh */
211
+ function requireId(label = 'Session ID') {
212
+ if (!args[1]) {
213
+ console.log(JSON.stringify({ success: false, error: `${label} required` }));
214
+ return null;
670
215
  }
216
+ return args[1];
671
217
  }
672
218
 
673
- sessionState.active_session = {
674
- id: targetId,
675
- nickname: targetSession.nickname,
676
- path: targetSession.path,
677
- branch: targetSession.branch,
678
- switched_at: new Date().toISOString(),
679
- original_cwd: ROOT,
680
- };
681
-
682
- const stateDir = path.dirname(SESSION_STATE_PATH);
683
- if (!fs.existsSync(stateDir)) fs.mkdirSync(stateDir, { recursive: true });
684
- fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(sessionState, null, 2) + '\n');
685
-
686
- registry.sessions[targetId].last_active = new Date().toISOString();
687
- saveRegistry(registry);
688
-
689
- return {
690
- success: true,
691
- session: {
692
- id: targetId,
693
- nickname: targetSession.nickname,
694
- path: targetSession.path,
695
- branch: targetSession.branch,
696
- },
697
- path: targetSession.path,
698
- addDirCommand: `/add-dir ${targetSession.path}`,
699
- };
700
- }
701
-
702
- function clearActiveSession() {
703
- if (!fs.existsSync(SESSION_STATE_PATH)) return { success: true };
704
- try {
705
- const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
706
- delete sessionState.active_session;
707
- fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(sessionState, null, 2) + '\n');
708
- return { success: true };
709
- } catch (e) {
710
- return { success: false, error: e.message };
711
- }
712
- }
713
-
714
- function getActiveSession() {
715
- if (!fs.existsSync(SESSION_STATE_PATH)) return { active: false };
716
- try {
717
- const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
718
- return sessionState.active_session
719
- ? { active: true, session: sessionState.active_session }
720
- : { active: false };
721
- } catch (e) {
722
- return { active: false };
723
- }
724
- }
725
-
726
- // ============================================================================
727
- // Thread Type Management
728
- // ============================================================================
729
-
730
- function getSessionThreadType(sessionId = null) {
731
- const registry = loadRegistry();
732
- const cwd = process.cwd();
733
- let targetId = sessionId;
734
- if (!targetId) {
735
- for (const [id, session] of Object.entries(registry.sessions)) {
736
- if (session.path === cwd) {
737
- targetId = id;
738
- break;
219
+ function parseOpts(startIdx, allowedKeys, boolKeys = []) {
220
+ const options = {};
221
+ for (let i = startIdx; i < args.length; i++) {
222
+ const arg = args[i];
223
+ if (!arg.startsWith('--')) continue;
224
+ const eqIndex = arg.indexOf('=');
225
+ let key, value;
226
+ if (eqIndex !== -1) { key = arg.slice(2, eqIndex); value = arg.slice(eqIndex + 1); }
227
+ else { key = arg.slice(2); value = args[++i]; }
228
+ if (!allowedKeys.includes(key)) {
229
+ console.log(JSON.stringify({ success: false, error: `Unknown option: --${key}` }));
230
+ return null;
739
231
  }
232
+ options[key] = boolKeys.includes(key) ? value !== 'false' : value;
740
233
  }
234
+ return options;
741
235
  }
742
- if (!targetId || !registry.sessions[targetId])
743
- return { success: false, error: 'Session not found' };
744
- const session = registry.sessions[targetId];
745
- const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
746
- return { success: true, thread_type: threadType, session_id: targetId, is_main: session.is_main };
747
- }
748
-
749
- function setSessionThreadType(sessionId, threadType) {
750
- if (!THREAD_TYPES.includes(threadType)) {
751
- return {
752
- success: false,
753
- error: `Invalid thread type: ${threadType}. Valid: ${THREAD_TYPES.join(', ')}`,
754
- };
755
- }
756
- const registry = loadRegistry();
757
- if (!registry.sessions[sessionId])
758
- return { success: false, error: `Session ${sessionId} not found` };
759
- registry.sessions[sessionId].thread_type = threadType;
760
- saveRegistry(registry);
761
- return { success: true, thread_type: threadType };
762
- }
763
-
764
- function transitionThread(sessionId, targetType, options = {}) {
765
- const { force = false } = options;
766
- const registry = loadRegistry();
767
- const session = registry.sessions[sessionId];
768
- if (!session) return { success: false, error: `Session ${sessionId} not found` };
769
-
770
- const currentType = session.thread_type || (session.is_main ? 'base' : 'parallel');
771
- const result = sessionThreadMachine.transition(currentType, targetType, { force });
772
- if (!result.success)
773
- return { success: false, from: currentType, to: targetType, error: result.error };
774
- if (result.noop) return { success: true, from: currentType, to: targetType, noop: true };
775
-
776
- registry.sessions[sessionId].thread_type = targetType;
777
- registry.sessions[sessionId].thread_transitioned_at = new Date().toISOString();
778
- saveRegistry(registry);
779
- return { success: true, from: currentType, to: targetType, forced: result.forced || false };
780
- }
781
-
782
- function getValidThreadTransitions(sessionId) {
783
- const registry = loadRegistry();
784
- const session = registry.sessions[sessionId];
785
- if (!session) return { success: false, error: `Session ${sessionId} not found` };
786
- const currentType = session.thread_type || (session.is_main ? 'base' : 'parallel');
787
- const validTransitions = sessionThreadMachine.getValidTransitions(currentType);
788
- return { success: true, current: currentType, validTransitions };
789
- }
790
-
791
- // ============================================================================
792
- // Merge Operation Wrappers (delegate to merge-operations module)
793
- // ============================================================================
794
-
795
- function checkMergeability(sessionId) {
796
- return mergeOps.checkMergeability(sessionId, loadRegistry);
797
- }
798
- function getMergePreview(sessionId) {
799
- return mergeOps.getMergePreview(sessionId, loadRegistry);
800
- }
801
- function integrateSession(sessionId, options = {}) {
802
- return mergeOps.integrateSession(sessionId, options, loadRegistry, saveRegistry, removeLock);
803
- }
804
- function generateCommitMessage(session) {
805
- return mergeOps.generateCommitMessage(session);
806
- }
807
- function commitChanges(sessionId, options = {}) {
808
- return mergeOps.commitChanges(sessionId, options, loadRegistry);
809
- }
810
- function stashChanges(sessionId) {
811
- return mergeOps.stashChanges(sessionId, loadRegistry);
812
- }
813
- function unstashChanges(sessionId) {
814
- return mergeOps.unstashChanges(sessionId);
815
- }
816
- function discardChanges(sessionId) {
817
- return mergeOps.discardChanges(sessionId, loadRegistry);
818
- }
819
- function categorizeFile(filePath) {
820
- return mergeOps.categorizeFile(filePath);
821
- }
822
- function getMergeStrategy(category) {
823
- return mergeOps.getMergeStrategy(category);
824
- }
825
- function getConflictingFiles(sessionId) {
826
- return mergeOps.getConflictingFiles(sessionId, loadRegistry);
827
- }
828
- function getMergeHistory() {
829
- return mergeOps.getMergeHistory();
830
- }
831
- function smartMerge(sessionId, options = {}) {
832
- return mergeOps.smartMerge(
833
- sessionId,
834
- options,
835
- loadRegistry,
836
- saveRegistry,
837
- removeLock,
838
- unregisterSession
839
- );
840
- }
841
-
842
- // ============================================================================
843
- // CLI Interface
844
- // ============================================================================
845
-
846
- function main() {
847
- const args = process.argv.slice(2);
848
- const command = args[0];
849
236
 
850
237
  switch (command) {
851
238
  case 'register': {
@@ -856,50 +243,25 @@ function main() {
856
243
  }
857
244
 
858
245
  case 'unregister': {
859
- const sessionId = args[1];
860
- if (sessionId) {
861
- unregisterSession(sessionId);
862
- console.log(JSON.stringify({ success: true }));
863
- } else console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
246
+ const id = requireId(); if (!id) return;
247
+ unregisterSession(id);
248
+ console.log(JSON.stringify({ success: true }));
864
249
  break;
865
250
  }
866
251
 
867
252
  case 'create': {
868
- const options = {};
869
- const allowedKeys = ['nickname', 'branch', 'timeout', 'mode', 'template'];
870
- for (let i = 1; i < args.length; i++) {
871
- const arg = args[i];
872
- if (arg.startsWith('--')) {
873
- const key = arg.slice(2).split('=')[0];
874
- if (!allowedKeys.includes(key)) {
875
- console.log(JSON.stringify({ success: false, error: `Unknown option: --${key}` }));
876
- return;
877
- }
878
- const eqIndex = arg.indexOf('=');
879
- if (eqIndex !== -1) options[key] = arg.slice(eqIndex + 1);
880
- else if (args[i + 1] && !args[i + 1].startsWith('--')) options[key] = args[++i];
881
- }
882
- }
883
-
884
- // Team mode: create a native Agent Teams session
253
+ const options = parseOpts(1, ['nickname', 'branch', 'timeout', 'mode', 'template']);
254
+ if (!options) return;
885
255
  if (options.mode === 'team') {
886
- const result = createTeamSession({
887
- template: options.template || 'fullstack',
888
- nickname: options.nickname,
889
- });
890
- console.log(JSON.stringify(result));
256
+ console.log(JSON.stringify(createTeamSession({
257
+ template: options.template || 'fullstack', nickname: options.nickname,
258
+ })));
891
259
  break;
892
260
  }
893
-
894
261
  if (options.timeout) {
895
262
  options.timeout = parseInt(options.timeout, 10);
896
263
  if (isNaN(options.timeout) || options.timeout < 1000) {
897
- console.log(
898
- JSON.stringify({
899
- success: false,
900
- error: 'Timeout must be a number >= 1000 (milliseconds)',
901
- })
902
- );
264
+ console.log(JSON.stringify({ success: false, error: 'Timeout must be a number >= 1000 (milliseconds)' }));
903
265
  return;
904
266
  }
905
267
  }
@@ -958,234 +320,71 @@ function main() {
958
320
  }
959
321
 
960
322
  case 'get': {
961
- const sessionId = args[1];
962
- if (!sessionId) {
963
- console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
964
- return;
965
- }
966
- const session = getSession(sessionId);
967
- if (!session) {
968
- console.log(JSON.stringify({ success: false, error: `Session ${sessionId} not found` }));
969
- return;
970
- }
323
+ const id = requireId(); if (!id) return;
324
+ const session = getSession(id);
325
+ if (!session) { console.log(JSON.stringify({ success: false, error: `Session ${id} not found` })); return; }
971
326
  console.log(JSON.stringify({ success: true, ...session }));
972
327
  break;
973
328
  }
974
329
 
975
330
  case 'full-status': {
976
- const nickname = args[1] || null;
977
- const cwd = process.cwd();
978
- const registry = loadRegistry();
979
- const branch = getCurrentBranch();
980
- const story = getCurrentStory();
981
- const pid = process.ppid || process.pid;
982
-
983
- let sessionId = null,
984
- isNew = false;
985
- for (const [id, session] of Object.entries(registry.sessions)) {
986
- if (session.path === cwd) {
987
- sessionId = id;
988
- break;
989
- }
990
- }
991
-
992
- if (sessionId) {
993
- registry.sessions[sessionId].branch = branch;
994
- registry.sessions[sessionId].story = story ? story.id : null;
995
- registry.sessions[sessionId].last_active = new Date().toISOString();
996
- if (nickname) registry.sessions[sessionId].nickname = nickname;
997
- if (!registry.sessions[sessionId].thread_type)
998
- registry.sessions[sessionId].thread_type = registry.sessions[sessionId].is_main
999
- ? 'base'
1000
- : 'parallel';
1001
- writeLock(sessionId, pid);
1002
- } else {
1003
- sessionId = String(registry.next_id);
1004
- registry.next_id++;
1005
- const isMain = cwd === ROOT && !isGitWorktree(cwd);
1006
- registry.sessions[sessionId] = {
1007
- path: cwd,
1008
- branch,
1009
- story: story ? story.id : null,
1010
- nickname: nickname || null,
1011
- created: new Date().toISOString(),
1012
- last_active: new Date().toISOString(),
1013
- is_main: isMain,
1014
- thread_type: isMain ? 'base' : 'parallel',
1015
- };
1016
- writeLock(sessionId, pid);
1017
- isNew = true;
1018
- }
1019
- saveRegistry(registry);
1020
-
1021
- const cleanupResult = cleanupStaleLocks(registry);
1022
- const filteredCleanup = {
1023
- count: cleanupResult.sessions.filter(s => String(s.id) !== String(sessionId)).length,
1024
- sessions: cleanupResult.sessions.filter(s => String(s.id) !== String(sessionId)),
1025
- };
1026
-
1027
- const sessions = [];
1028
- let otherActive = 0;
1029
- for (const [id, session] of Object.entries(registry.sessions)) {
1030
- const active = isSessionActive(id);
1031
- const isCurrent = session.path === cwd;
1032
- sessions.push({ id, ...session, active, current: isCurrent });
1033
- if (active && !isCurrent) otherActive++;
1034
- }
1035
-
1036
- console.log(
1037
- JSON.stringify({
1038
- registered: true,
1039
- id: sessionId,
1040
- isNew,
1041
- current: sessions.find(s => s.current) || null,
1042
- otherActive,
1043
- total: sessions.length,
1044
- cleaned: filteredCleanup.count,
1045
- cleanedSessions: filteredCleanup.sessions,
1046
- })
1047
- );
331
+ console.log(JSON.stringify(fullStatus(args[1] || null)));
1048
332
  break;
1049
333
  }
1050
334
 
1051
335
  case 'check-merge': {
1052
- const sessionId = args[1];
1053
- if (!sessionId) {
1054
- console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
1055
- return;
1056
- }
1057
- console.log(JSON.stringify(checkMergeability(sessionId)));
336
+ const id = requireId(); if (!id) return;
337
+ console.log(JSON.stringify(checkMergeability(id)));
1058
338
  break;
1059
339
  }
1060
340
  case 'merge-preview': {
1061
- const sessionId = args[1];
1062
- if (!sessionId) {
1063
- console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
1064
- return;
1065
- }
1066
- console.log(JSON.stringify(getMergePreview(sessionId)));
341
+ const id = requireId(); if (!id) return;
342
+ console.log(JSON.stringify(getMergePreview(id)));
1067
343
  break;
1068
344
  }
1069
-
1070
345
  case 'integrate': {
1071
- const sessionId = args[1];
1072
- if (!sessionId) {
1073
- console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
1074
- return;
1075
- }
1076
- const options = {};
1077
- const allowedKeys = ['strategy', 'deleteBranch', 'deleteWorktree', 'message'];
1078
- for (let i = 2; i < args.length; i++) {
1079
- const arg = args[i];
1080
- if (arg.startsWith('--')) {
1081
- const eqIndex = arg.indexOf('=');
1082
- let key, value;
1083
- if (eqIndex !== -1) {
1084
- key = arg.slice(2, eqIndex);
1085
- value = arg.slice(eqIndex + 1);
1086
- } else {
1087
- key = arg.slice(2);
1088
- value = args[++i];
1089
- }
1090
- if (!allowedKeys.includes(key)) {
1091
- console.log(JSON.stringify({ success: false, error: `Unknown option: --${key}` }));
1092
- return;
1093
- }
1094
- if (key === 'deleteBranch' || key === 'deleteWorktree') options[key] = value !== 'false';
1095
- else options[key] = value;
1096
- }
1097
- }
1098
- console.log(JSON.stringify(integrateSession(sessionId, options)));
346
+ const id = requireId(); if (!id) return;
347
+ const opts = parseOpts(2, ['strategy', 'deleteBranch', 'deleteWorktree', 'message'], ['deleteBranch', 'deleteWorktree']);
348
+ if (!opts) return;
349
+ console.log(JSON.stringify(integrateSession(id, opts)));
1099
350
  break;
1100
351
  }
1101
-
1102
352
  case 'commit-changes': {
1103
- const sessionId = args[1];
1104
- if (!sessionId) {
1105
- console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
1106
- return;
1107
- }
1108
- const options = {};
1109
- for (let i = 2; i < args.length; i++) {
1110
- const arg = args[i];
1111
- if (arg.startsWith('--message=')) options.message = arg.slice(10);
1112
- else if (arg === '--message' && args[i + 1]) options.message = args[++i];
1113
- }
1114
- console.log(JSON.stringify(commitChanges(sessionId, options)));
353
+ const id = requireId(); if (!id) return;
354
+ const opts = parseOpts(2, ['message']);
355
+ if (!opts) return;
356
+ console.log(JSON.stringify(commitChanges(id, opts)));
1115
357
  break;
1116
358
  }
1117
-
1118
359
  case 'stash': {
1119
- const sessionId = args[1];
1120
- if (!sessionId) {
1121
- console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
1122
- return;
1123
- }
1124
- console.log(JSON.stringify(stashChanges(sessionId)));
360
+ const id = requireId(); if (!id) return;
361
+ console.log(JSON.stringify(stashChanges(id)));
1125
362
  break;
1126
363
  }
1127
364
  case 'unstash': {
1128
- const sessionId = args[1];
1129
- if (!sessionId) {
1130
- console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
1131
- return;
1132
- }
1133
- console.log(JSON.stringify(unstashChanges(sessionId)));
365
+ const id = requireId(); if (!id) return;
366
+ console.log(JSON.stringify(unstashChanges(id)));
1134
367
  break;
1135
368
  }
1136
369
  case 'discard-changes': {
1137
- const sessionId = args[1];
1138
- if (!sessionId) {
1139
- console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
1140
- return;
1141
- }
1142
- console.log(JSON.stringify(discardChanges(sessionId)));
370
+ const id = requireId(); if (!id) return;
371
+ console.log(JSON.stringify(discardChanges(id)));
1143
372
  break;
1144
373
  }
1145
-
1146
374
  case 'smart-merge': {
1147
- const sessionId = args[1];
1148
- if (!sessionId) {
1149
- console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
1150
- return;
1151
- }
1152
- const options = {};
1153
- const allowedKeys = ['strategy', 'deleteBranch', 'deleteWorktree', 'message'];
1154
- for (let i = 2; i < args.length; i++) {
1155
- const arg = args[i];
1156
- if (arg.startsWith('--')) {
1157
- const eqIndex = arg.indexOf('=');
1158
- let key, value;
1159
- if (eqIndex !== -1) {
1160
- key = arg.slice(2, eqIndex);
1161
- value = arg.slice(eqIndex + 1);
1162
- } else {
1163
- key = arg.slice(2);
1164
- value = args[++i];
1165
- }
1166
- if (!allowedKeys.includes(key)) {
1167
- console.log(JSON.stringify({ success: false, error: `Unknown option: --${key}` }));
1168
- return;
1169
- }
1170
- if (key === 'deleteBranch' || key === 'deleteWorktree') options[key] = value !== 'false';
1171
- else options[key] = value;
1172
- }
1173
- }
1174
- console.log(JSON.stringify(smartMerge(sessionId, options), null, 2));
375
+ const id = requireId(); if (!id) return;
376
+ const opts = parseOpts(2, ['strategy', 'deleteBranch', 'deleteWorktree', 'message'], ['deleteBranch', 'deleteWorktree']);
377
+ if (!opts) return;
378
+ console.log(JSON.stringify(smartMerge(id, opts), null, 2));
1175
379
  break;
1176
380
  }
1177
-
1178
381
  case 'merge-history': {
1179
382
  console.log(JSON.stringify(getMergeHistory(), null, 2));
1180
383
  break;
1181
384
  }
1182
385
  case 'switch': {
1183
- const sessionIdOrNickname = args[1];
1184
- if (!sessionIdOrNickname) {
1185
- console.log(JSON.stringify({ success: false, error: 'Session ID or nickname required' }));
1186
- return;
1187
- }
1188
- console.log(JSON.stringify(switchSession(sessionIdOrNickname), null, 2));
386
+ const id = requireId('Session ID or nickname'); if (!id) return;
387
+ console.log(JSON.stringify(switchSession(id), null, 2));
1189
388
  break;
1190
389
  }
1191
390
  case 'active': {
@@ -1252,71 +451,23 @@ ${c.cyan}Commands:${c.reset}
1252
451
  }
1253
452
  }
1254
453
 
1255
- // ============================================================================
1256
- // Exports
1257
- // ============================================================================
1258
-
454
+ // --- Exports ---
1259
455
  module.exports = {
1260
- // Registry injection (for testing)
1261
- injectRegistry,
1262
- getRegistryInstance,
1263
- resetRegistryCache,
1264
- // Registry access
1265
- loadRegistry,
1266
- saveRegistry,
1267
- // Session management
1268
- registerSession,
1269
- unregisterSession,
1270
- getSession,
1271
- createSession,
1272
- createTeamSession,
1273
- getSessions,
1274
- getSessionsAsync,
1275
- getActiveSessionCount,
1276
- deleteSession,
1277
- isSessionActive,
1278
- isSessionActiveAsync,
1279
- cleanupStaleLocks,
1280
- cleanupStaleLocksAsync,
1281
- // Session switching
1282
- switchSession,
1283
- clearActiveSession,
1284
- getActiveSession,
1285
- // Thread type tracking
1286
- THREAD_TYPES,
1287
- detectThreadType,
1288
- getSessionThreadType,
1289
- setSessionThreadType,
1290
- transitionThread,
1291
- getValidThreadTransitions,
1292
- // Merge operations (delegated to module)
1293
- getMainBranch,
1294
- checkMergeability,
1295
- getMergePreview,
1296
- integrateSession,
1297
- commitChanges,
1298
- stashChanges,
1299
- unstashChanges,
1300
- discardChanges,
1301
- smartMerge,
1302
- getConflictingFiles,
1303
- categorizeFile,
1304
- getMergeStrategy,
1305
- getMergeHistory,
1306
- // Kanban visualization
1307
- SESSION_PHASES,
1308
- getSessionPhase,
1309
- getSessionPhaseAsync,
1310
- getSessionPhasesAsync,
1311
- renderKanbanBoard,
1312
- renderKanbanBoardAsync,
1313
- // Display
1314
- formatSessionsTable,
1315
- getFileDetails,
1316
- getSessionsHealth,
1317
- // Internal utilities (for testing)
1318
- execGitAsync,
1319
- gitCache,
456
+ injectRegistry, getRegistryInstance, resetRegistryCache,
457
+ loadRegistry, saveRegistry,
458
+ registerSession, unregisterSession, getSession, createSession, createTeamSession,
459
+ getSessions, getSessionsAsync, getActiveSessionCount, deleteSession,
460
+ isSessionActive, isSessionActiveAsync, cleanupStaleLocks, cleanupStaleLocksAsync,
461
+ switchSession, clearActiveSession, getActiveSession,
462
+ THREAD_TYPES, detectThreadType, getSessionThreadType, setSessionThreadType,
463
+ transitionThread, getValidThreadTransitions,
464
+ getMainBranch, checkMergeability, getMergePreview, integrateSession,
465
+ commitChanges, stashChanges, unstashChanges, discardChanges,
466
+ smartMerge, getConflictingFiles, categorizeFile, getMergeStrategy, getMergeHistory,
467
+ SESSION_PHASES, getSessionPhase, getSessionPhaseAsync, getSessionPhasesAsync,
468
+ renderKanbanBoard, renderKanbanBoardAsync,
469
+ formatSessionsTable, getFileDetails, getSessionsHealth,
470
+ execGitAsync, gitCache,
1320
471
  };
1321
472
 
1322
473
  if (require.main === module) main();