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
@@ -0,0 +1,611 @@
1
+ /**
2
+ * session-operations.js - Session CRUD, listing, and cleanup operations
3
+ *
4
+ * Extracted from session-manager.js to reduce file size.
5
+ * Uses factory pattern for dependency injection.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { spawnSync } = require('child_process');
11
+
12
+ const { getStatusPath } = require('./paths');
13
+ const { safeReadJSON } = require('./errors');
14
+ const { isValidBranchName, isValidSessionNickname } = require('./validate');
15
+ const { getInheritedFlags } = require('./flag-detection');
16
+
17
+ const {
18
+ THREAD_TYPES,
19
+ DEFAULT_WORKTREE_TIMEOUT_MS,
20
+ isGitWorktree,
21
+ detectThreadType,
22
+ progressIndicator,
23
+ createWorktreeWithTimeout,
24
+ cleanupFailedWorktree,
25
+ } = require('./worktree-operations');
26
+
27
+ const { isPidAlive } = require('./lock-file');
28
+ const { getCurrentBranch } = require('./git-operations');
29
+
30
+ // Agent Teams integration (lazy-loaded)
31
+ let _featureFlags, _teamManager;
32
+ function getFeatureFlags() {
33
+ if (!_featureFlags) {
34
+ try {
35
+ _featureFlags = require('./feature-flags');
36
+ } catch (e) {
37
+ _featureFlags = null;
38
+ }
39
+ }
40
+ return _featureFlags;
41
+ }
42
+ function getTeamManager() {
43
+ if (!_teamManager) {
44
+ try {
45
+ _teamManager = require('../scripts/team-manager');
46
+ } catch (e) {
47
+ _teamManager = null;
48
+ }
49
+ }
50
+ return _teamManager;
51
+ }
52
+
53
+ /**
54
+ * Create session operations bound to the given dependencies.
55
+ *
56
+ * @param {object} deps
57
+ * @param {string} deps.ROOT - Project root path
58
+ * @param {Function} deps.loadRegistry - Load registry data
59
+ * @param {Function} deps.saveRegistry - Save registry data
60
+ * @param {Function} deps.readLock - Read lock file for session
61
+ * @param {Function} deps.readLockAsync - Async read lock file
62
+ * @param {Function} deps.writeLock - Write lock file for session
63
+ * @param {Function} deps.removeLock - Remove lock file for session
64
+ * @param {Function} deps.isSessionActive - Check if session is active
65
+ * @param {Function} deps.isSessionActiveAsync - Async check if session is active
66
+ * @param {object} deps.c - Color utilities
67
+ */
68
+ function createSessionOperations(deps) {
69
+ const {
70
+ ROOT,
71
+ loadRegistry,
72
+ saveRegistry,
73
+ readLock,
74
+ readLockAsync,
75
+ writeLock,
76
+ removeLock,
77
+ isSessionActive,
78
+ isSessionActiveAsync,
79
+ c,
80
+ } = deps;
81
+
82
+ // ============================================================================
83
+ // Stale Lock Cleanup
84
+ // ============================================================================
85
+
86
+ function processStalelock(id, session, lock, dryRun) {
87
+ if (!lock) return null;
88
+ const pid = parseInt(lock.pid, 10);
89
+ if (isPidAlive(pid)) return null;
90
+ if (!dryRun) removeLock(id);
91
+ return {
92
+ id,
93
+ nickname: session.nickname,
94
+ branch: session.branch,
95
+ pid,
96
+ reason: 'pid_dead',
97
+ path: session.path,
98
+ };
99
+ }
100
+
101
+ function cleanupStaleLocks(registry, options = {}) {
102
+ const { dryRun = false } = options;
103
+ const cleanedSessions = [];
104
+ for (const [id, session] of Object.entries(registry.sessions)) {
105
+ const result = processStalelock(id, session, readLock(id), dryRun);
106
+ if (result) cleanedSessions.push(result);
107
+ }
108
+ return { count: cleanedSessions.length, sessions: cleanedSessions };
109
+ }
110
+
111
+ async function cleanupStaleLocksAsync(registry, options = {}) {
112
+ const { dryRun = false } = options;
113
+ const sessionEntries = Object.entries(registry.sessions);
114
+ if (sessionEntries.length === 0) return { count: 0, sessions: [] };
115
+
116
+ const lockResults = await Promise.all(
117
+ sessionEntries.map(async ([id, session]) => ({
118
+ id,
119
+ session,
120
+ lock: await readLockAsync(id),
121
+ }))
122
+ );
123
+
124
+ const cleanedSessions = lockResults
125
+ .map(({ id, session, lock }) => processStalelock(id, session, lock, dryRun))
126
+ .filter(Boolean);
127
+
128
+ return { count: cleanedSessions.length, sessions: cleanedSessions };
129
+ }
130
+
131
+ // ============================================================================
132
+ // Current Story Helper
133
+ // ============================================================================
134
+
135
+ function getCurrentStory() {
136
+ const statusPath = getStatusPath(ROOT);
137
+ const result = safeReadJSON(statusPath, { defaultValue: null });
138
+ if (!result.ok || !result.data) return null;
139
+ for (const [id, story] of Object.entries(result.data.stories || {})) {
140
+ if (story.status === 'in_progress') return { id, title: story.title };
141
+ }
142
+ return null;
143
+ }
144
+
145
+ // ============================================================================
146
+ // Session CRUD Operations
147
+ // ============================================================================
148
+
149
+ function registerSession(nickname = null, threadType = null) {
150
+ const registry = loadRegistry();
151
+ const cwd = process.cwd();
152
+ const branch = getCurrentBranch();
153
+ const story = getCurrentStory();
154
+ const pid = process.ppid || process.pid;
155
+
156
+ let existingId = null;
157
+ for (const [id, session] of Object.entries(registry.sessions)) {
158
+ if (session.path === cwd) {
159
+ existingId = id;
160
+ break;
161
+ }
162
+ }
163
+
164
+ if (existingId) {
165
+ registry.sessions[existingId].branch = branch;
166
+ registry.sessions[existingId].story = story ? story.id : null;
167
+ registry.sessions[existingId].last_active = new Date().toISOString();
168
+ if (nickname) registry.sessions[existingId].nickname = nickname;
169
+ if (threadType && THREAD_TYPES.includes(threadType)) {
170
+ registry.sessions[existingId].thread_type = threadType;
171
+ }
172
+ writeLock(existingId, pid);
173
+ saveRegistry(registry);
174
+ return { id: existingId, isNew: false };
175
+ }
176
+
177
+ const sessionId = String(registry.next_id);
178
+ registry.next_id++;
179
+ const isMain = cwd === ROOT && !isGitWorktree(cwd);
180
+ const detectedType =
181
+ threadType && THREAD_TYPES.includes(threadType)
182
+ ? threadType
183
+ : detectThreadType(null, !isMain);
184
+
185
+ registry.sessions[sessionId] = {
186
+ path: cwd,
187
+ branch,
188
+ story: story ? story.id : null,
189
+ nickname: nickname || null,
190
+ created: new Date().toISOString(),
191
+ last_active: new Date().toISOString(),
192
+ is_main: isMain,
193
+ thread_type: detectedType,
194
+ };
195
+
196
+ writeLock(sessionId, pid);
197
+ saveRegistry(registry);
198
+ return { id: sessionId, isNew: true, thread_type: detectedType };
199
+ }
200
+
201
+ function unregisterSession(sessionId) {
202
+ const registry = loadRegistry();
203
+ if (registry.sessions[sessionId]) {
204
+ registry.sessions[sessionId].last_active = new Date().toISOString();
205
+ removeLock(sessionId);
206
+ saveRegistry(registry);
207
+ }
208
+ }
209
+
210
+ function getSession(sessionId) {
211
+ const registry = loadRegistry();
212
+ const session = registry.sessions[sessionId];
213
+ if (!session) return null;
214
+ const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
215
+ return {
216
+ id: sessionId,
217
+ ...session,
218
+ thread_type: threadType,
219
+ active: isSessionActive(sessionId),
220
+ };
221
+ }
222
+
223
+ async function createSession(options = {}) {
224
+ const registry = loadRegistry();
225
+ const sessionId = String(registry.next_id);
226
+ const projectName = registry.project_name;
227
+
228
+ const nickname = options.nickname || null;
229
+ const branchName = options.branch || `session-${sessionId}`;
230
+ const dirName = nickname || sessionId;
231
+
232
+ if (!isValidBranchName(branchName)) {
233
+ return {
234
+ success: false,
235
+ error: `Invalid branch name: "${branchName}". Use only letters, numbers, hyphens, underscores, and forward slashes.`,
236
+ };
237
+ }
238
+ if (nickname && !isValidSessionNickname(nickname)) {
239
+ return {
240
+ success: false,
241
+ error: `Invalid nickname: "${nickname}". Use only letters, numbers, hyphens, and underscores.`,
242
+ };
243
+ }
244
+
245
+ const worktreePath = path.resolve(ROOT, '..', `${projectName}-${dirName}`);
246
+ if (fs.existsSync(worktreePath)) {
247
+ return { success: false, error: `Directory already exists: ${worktreePath}` };
248
+ }
249
+
250
+ // Create branch if needed
251
+ const checkRef = spawnSync(
252
+ 'git',
253
+ ['show-ref', '--verify', '--quiet', `refs/heads/${branchName}`],
254
+ { cwd: ROOT, encoding: 'utf8' }
255
+ );
256
+ let branchCreatedByUs = false;
257
+ if (checkRef.status !== 0) {
258
+ const createBranch = spawnSync('git', ['branch', branchName], {
259
+ cwd: ROOT,
260
+ encoding: 'utf8',
261
+ });
262
+ if (createBranch.status !== 0) {
263
+ return {
264
+ success: false,
265
+ error: `Failed to create branch: ${createBranch.stderr || 'unknown error'}`,
266
+ };
267
+ }
268
+ branchCreatedByUs = true;
269
+ }
270
+
271
+ const timeoutMs = options.timeout || DEFAULT_WORKTREE_TIMEOUT_MS;
272
+ const stopProgress = progressIndicator(
273
+ 'Creating worktree (this may take a while for large repos)'
274
+ );
275
+
276
+ try {
277
+ await createWorktreeWithTimeout(worktreePath, branchName, timeoutMs);
278
+ stopProgress();
279
+ process.stderr.write(`✓ Worktree created successfully\n`);
280
+ } catch (error) {
281
+ stopProgress();
282
+ cleanupFailedWorktree(worktreePath, branchName, branchCreatedByUs);
283
+ return { success: false, error: error.message };
284
+ }
285
+
286
+ // Copy env files
287
+ const envFiles = ['.env', '.env.local', '.env.development', '.env.test', '.env.production'];
288
+ const copiedEnvFiles = [];
289
+ for (const envFile of envFiles) {
290
+ const src = path.join(ROOT, envFile);
291
+ const dest = path.join(worktreePath, envFile);
292
+ if (fs.existsSync(src) && !fs.existsSync(dest)) {
293
+ try {
294
+ fs.copyFileSync(src, dest);
295
+ copiedEnvFiles.push(envFile);
296
+ } catch (e) {
297
+ /* ignore */
298
+ }
299
+ }
300
+ }
301
+
302
+ // Copy config folders
303
+ const configFoldersToCopy = ['.claude', '.agileflow'];
304
+ const copiedFolders = [];
305
+ for (const folder of configFoldersToCopy) {
306
+ const src = path.join(ROOT, folder);
307
+ const dest = path.join(worktreePath, folder);
308
+ if (fs.existsSync(src)) {
309
+ try {
310
+ fs.cpSync(src, dest, { recursive: true, force: true });
311
+ copiedFolders.push(folder);
312
+ } catch (e) {
313
+ /* ignore */
314
+ }
315
+ }
316
+ }
317
+
318
+ // Symlink sessions directory
319
+ const sessionsSymlinkSrc = path.join(ROOT, '.agileflow', 'sessions');
320
+ const sessionsSymlinkDest = path.join(worktreePath, '.agileflow', 'sessions');
321
+ if (fs.existsSync(sessionsSymlinkSrc)) {
322
+ try {
323
+ if (fs.existsSync(sessionsSymlinkDest))
324
+ fs.rmSync(sessionsSymlinkDest, { recursive: true, force: true });
325
+ const relPath = path.relative(path.dirname(sessionsSymlinkDest), sessionsSymlinkSrc);
326
+ fs.symlinkSync(relPath, sessionsSymlinkDest, 'dir');
327
+ } catch (e) {
328
+ /* ignore */
329
+ }
330
+ }
331
+
332
+ // Symlink docs
333
+ const foldersToSymlink = ['docs'];
334
+ const symlinkedFolders = [];
335
+ for (const folder of foldersToSymlink) {
336
+ const src = path.join(ROOT, folder);
337
+ const dest = path.join(worktreePath, folder);
338
+ if (fs.existsSync(src)) {
339
+ try {
340
+ if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
341
+ const relPath = path.relative(worktreePath, src);
342
+ fs.symlinkSync(relPath, dest, 'dir');
343
+ symlinkedFolders.push(folder);
344
+ } catch (e) {
345
+ try {
346
+ fs.cpSync(src, dest, { recursive: true, force: true });
347
+ copiedFolders.push(folder);
348
+ } catch (copyErr) {
349
+ /* ignore */
350
+ }
351
+ }
352
+ }
353
+ }
354
+
355
+ // Detect inherited flags from parent Claude session
356
+ const inheritedFlags = options.inheritFlags !== false ? getInheritedFlags() : '';
357
+
358
+ registry.next_id++;
359
+ registry.sessions[sessionId] = {
360
+ path: worktreePath,
361
+ branch: branchName,
362
+ story: null,
363
+ nickname,
364
+ created: new Date().toISOString(),
365
+ last_active: new Date().toISOString(),
366
+ is_main: false,
367
+ thread_type: options.thread_type || 'parallel',
368
+ inherited_flags: inheritedFlags || null,
369
+ };
370
+ saveRegistry(registry);
371
+
372
+ // Build the command with inherited flags
373
+ const claudeCmd = inheritedFlags ? `claude ${inheritedFlags}` : 'claude';
374
+
375
+ return {
376
+ success: true,
377
+ sessionId,
378
+ path: worktreePath,
379
+ branch: branchName,
380
+ thread_type: registry.sessions[sessionId].thread_type,
381
+ command: `cd "${worktreePath}" && ${claudeCmd}`,
382
+ inheritedFlags: inheritedFlags || null,
383
+ envFilesCopied: copiedEnvFiles,
384
+ foldersCopied: copiedFolders,
385
+ foldersSymlinked: symlinkedFolders,
386
+ };
387
+ }
388
+
389
+ function createTeamSession(options = {}) {
390
+ const ff = getFeatureFlags();
391
+ const templateName = options.template || 'fullstack';
392
+
393
+ if (!ff || !ff.isAgentTeamsEnabled({ rootDir: ROOT })) {
394
+ console.error(`${c.yellow}Agent Teams not enabled. Falling back to worktree mode.${c.reset}`);
395
+ return createSession({
396
+ nickname: options.nickname || `team-${templateName}`,
397
+ thread_type: 'parallel',
398
+ });
399
+ }
400
+
401
+ const tm = getTeamManager();
402
+ if (!tm) {
403
+ return { success: false, error: 'team-manager module not available' };
404
+ }
405
+
406
+ const teamResult = tm.startTeam(ROOT, templateName);
407
+ if (!teamResult.ok) {
408
+ return { success: false, error: teamResult.error || 'Failed to start team' };
409
+ }
410
+
411
+ const registry = loadRegistry();
412
+ const sessionId = String(registry.next_id);
413
+ registry.next_id++;
414
+
415
+ registry.sessions[sessionId] = {
416
+ path: ROOT,
417
+ branch: getCurrentBranch(),
418
+ story: null,
419
+ nickname: options.nickname || `team-${templateName}`,
420
+ created: new Date().toISOString(),
421
+ last_active: new Date().toISOString(),
422
+ is_main: true,
423
+ type: 'team',
424
+ thread_type: 'team',
425
+ team_name: templateName,
426
+ team_lead: teamResult.lead || null,
427
+ teammates: teamResult.teammates || [],
428
+ };
429
+ saveRegistry(registry);
430
+
431
+ return {
432
+ success: true,
433
+ sessionId,
434
+ type: 'team',
435
+ template: templateName,
436
+ mode: teamResult.mode,
437
+ teammates: teamResult.teammates || [],
438
+ path: ROOT,
439
+ branch: getCurrentBranch(),
440
+ };
441
+ }
442
+
443
+ // ============================================================================
444
+ // Session Listing
445
+ // ============================================================================
446
+
447
+ function buildSessionsList(registrySessions, activeChecks, cwd) {
448
+ const sessions = Object.entries(registrySessions).map(([id, session]) => ({
449
+ id,
450
+ ...session,
451
+ active: activeChecks[id] || false,
452
+ current: session.path === cwd,
453
+ }));
454
+ sessions.sort((a, b) => parseInt(a.id) - parseInt(b.id));
455
+ return sessions;
456
+ }
457
+
458
+ function getSessions() {
459
+ const registry = loadRegistry();
460
+ const cleanupResult = cleanupStaleLocks(registry);
461
+ const cwd = process.cwd();
462
+ const activeChecks = {};
463
+ for (const id of Object.keys(registry.sessions)) activeChecks[id] = isSessionActive(id);
464
+ return {
465
+ sessions: buildSessionsList(registry.sessions, activeChecks, cwd),
466
+ cleaned: cleanupResult.count,
467
+ cleanedSessions: cleanupResult.sessions,
468
+ };
469
+ }
470
+
471
+ async function getSessionsAsync() {
472
+ const registry = loadRegistry();
473
+ const cleanupResult = await cleanupStaleLocksAsync(registry);
474
+ const sessionEntries = Object.entries(registry.sessions);
475
+ const cwd = process.cwd();
476
+ const activeResults = await Promise.all(
477
+ sessionEntries.map(async ([id]) => [id, await isSessionActiveAsync(id)])
478
+ );
479
+ const activeChecks = Object.fromEntries(activeResults);
480
+ return {
481
+ sessions: buildSessionsList(registry.sessions, activeChecks, cwd),
482
+ cleaned: cleanupResult.count,
483
+ cleanedSessions: cleanupResult.sessions,
484
+ };
485
+ }
486
+
487
+ function getActiveSessionCount() {
488
+ const { sessions } = getSessions();
489
+ const cwd = process.cwd();
490
+ return sessions.filter(s => s.active && s.path !== cwd).length;
491
+ }
492
+
493
+ function fullStatus(nickname = null) {
494
+ const cwd = process.cwd();
495
+ const registry = loadRegistry();
496
+ const branch = getCurrentBranch();
497
+ const story = getCurrentStory();
498
+ const pid = process.ppid || process.pid;
499
+
500
+ let sessionId = null,
501
+ isNew = false;
502
+ for (const [id, session] of Object.entries(registry.sessions)) {
503
+ if (session.path === cwd) {
504
+ sessionId = id;
505
+ break;
506
+ }
507
+ }
508
+
509
+ if (sessionId) {
510
+ registry.sessions[sessionId].branch = branch;
511
+ registry.sessions[sessionId].story = story ? story.id : null;
512
+ registry.sessions[sessionId].last_active = new Date().toISOString();
513
+ if (nickname) registry.sessions[sessionId].nickname = nickname;
514
+ if (!registry.sessions[sessionId].thread_type)
515
+ registry.sessions[sessionId].thread_type = registry.sessions[sessionId].is_main
516
+ ? 'base'
517
+ : 'parallel';
518
+ writeLock(sessionId, pid);
519
+ } else {
520
+ sessionId = String(registry.next_id);
521
+ registry.next_id++;
522
+ const isMain = cwd === ROOT && !isGitWorktree(cwd);
523
+ registry.sessions[sessionId] = {
524
+ path: cwd,
525
+ branch,
526
+ story: story ? story.id : null,
527
+ nickname: nickname || null,
528
+ created: new Date().toISOString(),
529
+ last_active: new Date().toISOString(),
530
+ is_main: isMain,
531
+ thread_type: isMain ? 'base' : 'parallel',
532
+ };
533
+ writeLock(sessionId, pid);
534
+ isNew = true;
535
+ }
536
+ saveRegistry(registry);
537
+
538
+ const cleanupResult = cleanupStaleLocks(registry);
539
+ const filteredCleanup = {
540
+ count: cleanupResult.sessions.filter(s => String(s.id) !== String(sessionId)).length,
541
+ sessions: cleanupResult.sessions.filter(s => String(s.id) !== String(sessionId)),
542
+ };
543
+
544
+ const sessions = [];
545
+ let otherActive = 0;
546
+ for (const [id, session] of Object.entries(registry.sessions)) {
547
+ const active = isSessionActive(id);
548
+ const isCurrent = session.path === cwd;
549
+ sessions.push({ id, ...session, active, current: isCurrent });
550
+ if (active && !isCurrent) otherActive++;
551
+ }
552
+
553
+ return {
554
+ registered: true,
555
+ id: sessionId,
556
+ isNew,
557
+ current: sessions.find(s => s.current) || null,
558
+ otherActive,
559
+ total: sessions.length,
560
+ cleaned: filteredCleanup.count,
561
+ cleanedSessions: filteredCleanup.sessions,
562
+ };
563
+ }
564
+
565
+ function deleteSession(sessionId, removeWorktree = false) {
566
+ const registry = loadRegistry();
567
+ const session = registry.sessions[sessionId];
568
+ if (!session) return { success: false, error: `Session ${sessionId} not found` };
569
+ if (session.is_main) return { success: false, error: 'Cannot delete main session' };
570
+
571
+ removeLock(sessionId);
572
+ if (removeWorktree && fs.existsSync(session.path)) {
573
+ const { execFileSync } = require('child_process');
574
+ try {
575
+ execFileSync('git', ['worktree', 'remove', session.path], { cwd: ROOT, encoding: 'utf8' });
576
+ } catch (e) {
577
+ try {
578
+ execFileSync('git', ['worktree', 'remove', '--force', session.path], {
579
+ cwd: ROOT,
580
+ encoding: 'utf8',
581
+ });
582
+ } catch (e2) {
583
+ return { success: false, error: `Failed to remove worktree: ${e2.message}` };
584
+ }
585
+ }
586
+ }
587
+ delete registry.sessions[sessionId];
588
+ saveRegistry(registry);
589
+ return { success: true };
590
+ }
591
+
592
+ return {
593
+ processStalelock,
594
+ cleanupStaleLocks,
595
+ cleanupStaleLocksAsync,
596
+ getCurrentStory,
597
+ registerSession,
598
+ unregisterSession,
599
+ getSession,
600
+ createSession,
601
+ createTeamSession,
602
+ buildSessionsList,
603
+ getSessions,
604
+ getSessionsAsync,
605
+ getActiveSessionCount,
606
+ fullStatus,
607
+ deleteSession,
608
+ };
609
+ }
610
+
611
+ module.exports = { createSessionOperations };