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
@@ -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,108 @@ 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
- }
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];
447
210
 
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
- }
211
+ function requireId(label = 'Session ID') {
212
+ if (!args[1]) {
213
+ console.log(JSON.stringify({ success: false, error: `${label} required` }));
214
+ return null;
468
215
  }
216
+ return args[1];
469
217
  }
470
218
 
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}` };
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) {
227
+ key = arg.slice(2, eqIndex);
228
+ value = arg.slice(eqIndex + 1);
229
+ } else {
230
+ key = arg.slice(2);
231
+ value = args[++i];
634
232
  }
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}` };
663
-
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 */
670
- }
671
- }
672
-
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;
233
+ if (!allowedKeys.includes(key)) {
234
+ console.log(JSON.stringify({ success: false, error: `Unknown option: --${key}` }));
235
+ return null;
739
236
  }
237
+ options[key] = boolKeys.includes(key) ? value !== 'false' : value;
740
238
  }
239
+ return options;
741
240
  }
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
241
 
850
242
  switch (command) {
851
243
  case 'register': {
@@ -856,41 +248,27 @@ function main() {
856
248
  }
857
249
 
858
250
  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' }));
251
+ const id = requireId();
252
+ if (!id) return;
253
+ unregisterSession(id);
254
+ console.log(JSON.stringify({ success: true }));
864
255
  break;
865
256
  }
866
257
 
867
258
  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
259
+ const options = parseOpts(1, ['nickname', 'branch', 'timeout', 'mode', 'template']);
260
+ if (!options) return;
885
261
  if (options.mode === 'team') {
886
- const result = createTeamSession({
887
- template: options.template || 'fullstack',
888
- nickname: options.nickname,
889
- });
890
- console.log(JSON.stringify(result));
262
+ console.log(
263
+ JSON.stringify(
264
+ createTeamSession({
265
+ template: options.template || 'fullstack',
266
+ nickname: options.nickname,
267
+ })
268
+ )
269
+ );
891
270
  break;
892
271
  }
893
-
894
272
  if (options.timeout) {
895
273
  options.timeout = parseInt(options.timeout, 10);
896
274
  if (isNaN(options.timeout) || options.timeout < 1000) {
@@ -958,14 +336,11 @@ function main() {
958
336
  }
959
337
 
960
338
  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);
339
+ const id = requireId();
340
+ if (!id) return;
341
+ const session = getSession(id);
967
342
  if (!session) {
968
- console.log(JSON.stringify({ success: false, error: `Session ${sessionId} not found` }));
343
+ console.log(JSON.stringify({ success: false, error: `Session ${id} not found` }));
969
344
  return;
970
345
  }
971
346
  console.log(JSON.stringify({ success: true, ...session }));
@@ -973,219 +348,80 @@ function main() {
973
348
  }
974
349
 
975
350
  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
- );
351
+ console.log(JSON.stringify(fullStatus(args[1] || null)));
1048
352
  break;
1049
353
  }
1050
354
 
1051
355
  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)));
356
+ const id = requireId();
357
+ if (!id) return;
358
+ console.log(JSON.stringify(checkMergeability(id)));
1058
359
  break;
1059
360
  }
1060
361
  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)));
362
+ const id = requireId();
363
+ if (!id) return;
364
+ console.log(JSON.stringify(getMergePreview(id)));
1067
365
  break;
1068
366
  }
1069
-
1070
367
  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)));
368
+ const id = requireId();
369
+ if (!id) return;
370
+ const opts = parseOpts(
371
+ 2,
372
+ ['strategy', 'deleteBranch', 'deleteWorktree', 'message'],
373
+ ['deleteBranch', 'deleteWorktree']
374
+ );
375
+ if (!opts) return;
376
+ console.log(JSON.stringify(integrateSession(id, opts)));
1099
377
  break;
1100
378
  }
1101
-
1102
379
  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)));
380
+ const id = requireId();
381
+ if (!id) return;
382
+ const opts = parseOpts(2, ['message']);
383
+ if (!opts) return;
384
+ console.log(JSON.stringify(commitChanges(id, opts)));
1115
385
  break;
1116
386
  }
1117
-
1118
387
  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)));
388
+ const id = requireId();
389
+ if (!id) return;
390
+ console.log(JSON.stringify(stashChanges(id)));
1125
391
  break;
1126
392
  }
1127
393
  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)));
394
+ const id = requireId();
395
+ if (!id) return;
396
+ console.log(JSON.stringify(unstashChanges(id)));
1134
397
  break;
1135
398
  }
1136
399
  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)));
400
+ const id = requireId();
401
+ if (!id) return;
402
+ console.log(JSON.stringify(discardChanges(id)));
1143
403
  break;
1144
404
  }
1145
-
1146
405
  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));
406
+ const id = requireId();
407
+ if (!id) return;
408
+ const opts = parseOpts(
409
+ 2,
410
+ ['strategy', 'deleteBranch', 'deleteWorktree', 'message'],
411
+ ['deleteBranch', 'deleteWorktree']
412
+ );
413
+ if (!opts) return;
414
+ console.log(JSON.stringify(smartMerge(id, opts), null, 2));
1175
415
  break;
1176
416
  }
1177
-
1178
417
  case 'merge-history': {
1179
418
  console.log(JSON.stringify(getMergeHistory(), null, 2));
1180
419
  break;
1181
420
  }
1182
421
  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));
422
+ const id = requireId('Session ID or nickname');
423
+ if (!id) return;
424
+ console.log(JSON.stringify(switchSession(id), null, 2));
1189
425
  break;
1190
426
  }
1191
427
  case 'active': {
@@ -1252,19 +488,13 @@ ${c.cyan}Commands:${c.reset}
1252
488
  }
1253
489
  }
1254
490
 
1255
- // ============================================================================
1256
- // Exports
1257
- // ============================================================================
1258
-
491
+ // --- Exports ---
1259
492
  module.exports = {
1260
- // Registry injection (for testing)
1261
493
  injectRegistry,
1262
494
  getRegistryInstance,
1263
495
  resetRegistryCache,
1264
- // Registry access
1265
496
  loadRegistry,
1266
497
  saveRegistry,
1267
- // Session management
1268
498
  registerSession,
1269
499
  unregisterSession,
1270
500
  getSession,
@@ -1278,18 +508,15 @@ module.exports = {
1278
508
  isSessionActiveAsync,
1279
509
  cleanupStaleLocks,
1280
510
  cleanupStaleLocksAsync,
1281
- // Session switching
1282
511
  switchSession,
1283
512
  clearActiveSession,
1284
513
  getActiveSession,
1285
- // Thread type tracking
1286
514
  THREAD_TYPES,
1287
515
  detectThreadType,
1288
516
  getSessionThreadType,
1289
517
  setSessionThreadType,
1290
518
  transitionThread,
1291
519
  getValidThreadTransitions,
1292
- // Merge operations (delegated to module)
1293
520
  getMainBranch,
1294
521
  checkMergeability,
1295
522
  getMergePreview,
@@ -1303,18 +530,15 @@ module.exports = {
1303
530
  categorizeFile,
1304
531
  getMergeStrategy,
1305
532
  getMergeHistory,
1306
- // Kanban visualization
1307
533
  SESSION_PHASES,
1308
534
  getSessionPhase,
1309
535
  getSessionPhaseAsync,
1310
536
  getSessionPhasesAsync,
1311
537
  renderKanbanBoard,
1312
538
  renderKanbanBoardAsync,
1313
- // Display
1314
539
  formatSessionsTable,
1315
540
  getFileDetails,
1316
541
  getSessionsHealth,
1317
- // Internal utilities (for testing)
1318
542
  execGitAsync,
1319
543
  gitCache,
1320
544
  };