agileflow 2.84.2 → 2.86.0

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.
@@ -156,8 +156,21 @@ function getCurrentStory() {
156
156
  return null;
157
157
  }
158
158
 
159
+ // Thread type enum values
160
+ const THREAD_TYPES = ['base', 'parallel', 'chained', 'fusion', 'big', 'long'];
161
+
162
+ // Auto-detect thread type from context
163
+ function detectThreadType(session, isWorktree = false) {
164
+ // Worktree sessions are parallel threads
165
+ if (isWorktree || (session && !session.is_main)) {
166
+ return 'parallel';
167
+ }
168
+ // Default to base
169
+ return 'base';
170
+ }
171
+
159
172
  // Register current session (called on startup)
160
- function registerSession(nickname = null) {
173
+ function registerSession(nickname = null, threadType = null) {
161
174
  const registry = loadRegistry();
162
175
  const cwd = process.cwd();
163
176
  const branch = getCurrentBranch();
@@ -179,6 +192,10 @@ function registerSession(nickname = null) {
179
192
  registry.sessions[existingId].story = story ? story.id : null;
180
193
  registry.sessions[existingId].last_active = new Date().toISOString();
181
194
  if (nickname) registry.sessions[existingId].nickname = nickname;
195
+ // Update thread_type if explicitly provided
196
+ if (threadType && THREAD_TYPES.includes(threadType)) {
197
+ registry.sessions[existingId].thread_type = threadType;
198
+ }
182
199
 
183
200
  writeLock(existingId, pid);
184
201
  saveRegistry(registry);
@@ -190,6 +207,11 @@ function registerSession(nickname = null) {
190
207
  const sessionId = String(registry.next_id);
191
208
  registry.next_id++;
192
209
 
210
+ const isMain = cwd === ROOT;
211
+ const detectedType = threadType && THREAD_TYPES.includes(threadType)
212
+ ? threadType
213
+ : detectThreadType(null, !isMain);
214
+
193
215
  registry.sessions[sessionId] = {
194
216
  path: cwd,
195
217
  branch,
@@ -197,13 +219,14 @@ function registerSession(nickname = null) {
197
219
  nickname: nickname || null,
198
220
  created: new Date().toISOString(),
199
221
  last_active: new Date().toISOString(),
200
- is_main: cwd === ROOT,
222
+ is_main: isMain,
223
+ thread_type: detectedType,
201
224
  };
202
225
 
203
226
  writeLock(sessionId, pid);
204
227
  saveRegistry(registry);
205
228
 
206
- return { id: sessionId, isNew: true };
229
+ return { id: sessionId, isNew: true, thread_type: detectedType };
207
230
  }
208
231
 
209
232
  // Unregister session (called on exit)
@@ -291,7 +314,7 @@ function createSession(options = {}) {
291
314
  };
292
315
  }
293
316
 
294
- // Register session
317
+ // Register session - worktree sessions are always parallel threads
295
318
  registry.next_id++;
296
319
  registry.sessions[sessionId] = {
297
320
  path: worktreePath,
@@ -301,6 +324,7 @@ function createSession(options = {}) {
301
324
  created: new Date().toISOString(),
302
325
  last_active: new Date().toISOString(),
303
326
  is_main: false,
327
+ thread_type: options.thread_type || 'parallel', // Worktrees default to parallel
304
328
  };
305
329
 
306
330
  saveRegistry(registry);
@@ -310,6 +334,7 @@ function createSession(options = {}) {
310
334
  sessionId,
311
335
  path: worktreePath,
312
336
  branch: branchName,
337
+ thread_type: registry.sessions[sessionId].thread_type,
313
338
  command: `cd "${worktreePath}" && claude`,
314
339
  };
315
340
  }
@@ -671,6 +696,172 @@ function integrateSession(sessionId, options = {}) {
671
696
  return result;
672
697
  }
673
698
 
699
+ // Session phases for Kanban-style visualization
700
+ const SESSION_PHASES = {
701
+ TODO: 'todo',
702
+ CODING: 'coding',
703
+ REVIEW: 'review',
704
+ MERGED: 'merged',
705
+ };
706
+
707
+ // Detect session phase based on git state
708
+ function getSessionPhase(session) {
709
+ // If merged_at field exists, session was merged
710
+ if (session.merged_at) {
711
+ return SESSION_PHASES.MERGED;
712
+ }
713
+
714
+ // If is_main, it's the merged/main column
715
+ if (session.is_main) {
716
+ return SESSION_PHASES.MERGED;
717
+ }
718
+
719
+ // Check git state for the session
720
+ try {
721
+ const sessionPath = session.path;
722
+ if (!fs.existsSync(sessionPath)) {
723
+ return SESSION_PHASES.TODO;
724
+ }
725
+
726
+ // Count commits since branch diverged from main
727
+ const mainBranch = getMainBranch();
728
+ const commitCount = execSync(
729
+ `git rev-list --count ${mainBranch}..HEAD 2>/dev/null || echo 0`,
730
+ { cwd: sessionPath, encoding: 'utf8' }
731
+ ).trim();
732
+
733
+ const commits = parseInt(commitCount, 10);
734
+
735
+ if (commits === 0) {
736
+ return SESSION_PHASES.TODO;
737
+ }
738
+
739
+ // Check for uncommitted changes
740
+ const status = execSync('git status --porcelain 2>/dev/null || echo ""', {
741
+ cwd: sessionPath,
742
+ encoding: 'utf8',
743
+ }).trim();
744
+
745
+ if (status === '') {
746
+ // No uncommitted changes = ready for review
747
+ return SESSION_PHASES.REVIEW;
748
+ }
749
+
750
+ // Has commits but also uncommitted changes = still coding
751
+ return SESSION_PHASES.CODING;
752
+ } catch (e) {
753
+ // On error, assume coding phase
754
+ return SESSION_PHASES.CODING;
755
+ }
756
+ }
757
+
758
+ // Render Kanban-style board visualization
759
+ function renderKanbanBoard(sessions) {
760
+ const lines = [];
761
+
762
+ // Group sessions by phase
763
+ const byPhase = {
764
+ [SESSION_PHASES.TODO]: [],
765
+ [SESSION_PHASES.CODING]: [],
766
+ [SESSION_PHASES.REVIEW]: [],
767
+ [SESSION_PHASES.MERGED]: [],
768
+ };
769
+
770
+ for (const session of sessions) {
771
+ const phase = getSessionPhase(session);
772
+ byPhase[phase].push(session);
773
+ }
774
+
775
+ // Calculate column widths (min 12 chars)
776
+ const colWidth = 14;
777
+ const separator = ' ';
778
+
779
+ // Header
780
+ lines.push(`${c.cyan}Sessions (Kanban View):${c.reset}`);
781
+ lines.push('');
782
+
783
+ // Column headers
784
+ const headers = [
785
+ `${c.dim}TO DO${c.reset}`,
786
+ `${c.yellow}CODING${c.reset}`,
787
+ `${c.blue}REVIEW${c.reset}`,
788
+ `${c.green}MERGED${c.reset}`,
789
+ ];
790
+ lines.push(headers.map(h => h.padEnd(colWidth + 10)).join(separator)); // +10 for ANSI codes
791
+
792
+ // Top borders
793
+ const topBorder = `┌${'─'.repeat(colWidth)}┐`;
794
+ lines.push([topBorder, topBorder, topBorder, topBorder].join(separator));
795
+
796
+ // Find max rows needed
797
+ const maxRows = Math.max(
798
+ 1,
799
+ byPhase[SESSION_PHASES.TODO].length,
800
+ byPhase[SESSION_PHASES.CODING].length,
801
+ byPhase[SESSION_PHASES.REVIEW].length,
802
+ byPhase[SESSION_PHASES.MERGED].length
803
+ );
804
+
805
+ // Render rows
806
+ for (let i = 0; i < maxRows; i++) {
807
+ const cells = [
808
+ SESSION_PHASES.TODO,
809
+ SESSION_PHASES.CODING,
810
+ SESSION_PHASES.REVIEW,
811
+ SESSION_PHASES.MERGED,
812
+ ].map(phase => {
813
+ const session = byPhase[phase][i];
814
+ if (!session) {
815
+ return `│${' '.repeat(colWidth)}│`;
816
+ }
817
+
818
+ // Format session info
819
+ const id = `[${session.id}]`;
820
+ const name = session.nickname || session.branch || '';
821
+ const truncName = name.length > colWidth - 5 ? name.slice(0, colWidth - 8) + '...' : name;
822
+ const content = `${id} ${truncName}`.slice(0, colWidth);
823
+
824
+ return `│${content.padEnd(colWidth)}│`;
825
+ });
826
+ lines.push(cells.join(separator));
827
+
828
+ // Second line with story
829
+ const storyCells = [
830
+ SESSION_PHASES.TODO,
831
+ SESSION_PHASES.CODING,
832
+ SESSION_PHASES.REVIEW,
833
+ SESSION_PHASES.MERGED,
834
+ ].map(phase => {
835
+ const session = byPhase[phase][i];
836
+ if (!session) {
837
+ return `│${' '.repeat(colWidth)}│`;
838
+ }
839
+
840
+ const story = session.story || '-';
841
+ const storyTrunc = story.length > colWidth - 2 ? story.slice(0, colWidth - 5) + '...' : story;
842
+
843
+ return `│${c.dim}${storyTrunc.padEnd(colWidth)}${c.reset}│`;
844
+ });
845
+ lines.push(storyCells.join(separator));
846
+ }
847
+
848
+ // Bottom borders
849
+ const bottomBorder = `└${'─'.repeat(colWidth)}┘`;
850
+ lines.push([bottomBorder, bottomBorder, bottomBorder, bottomBorder].join(separator));
851
+
852
+ // Summary
853
+ lines.push('');
854
+ const summary = [
855
+ `${c.dim}To Do: ${byPhase[SESSION_PHASES.TODO].length}${c.reset}`,
856
+ `${c.yellow}Coding: ${byPhase[SESSION_PHASES.CODING].length}${c.reset}`,
857
+ `${c.blue}Review: ${byPhase[SESSION_PHASES.REVIEW].length}${c.reset}`,
858
+ `${c.green}Merged: ${byPhase[SESSION_PHASES.MERGED].length}${c.reset}`,
859
+ ].join(' │ ');
860
+ lines.push(summary);
861
+
862
+ return lines.join('\n');
863
+ }
864
+
674
865
  // Format sessions for display
675
866
  function formatSessionsTable(sessions) {
676
867
  const lines = [];
@@ -747,6 +938,11 @@ function main() {
747
938
  const { sessions, cleaned } = getSessions();
748
939
  if (args.includes('--json')) {
749
940
  console.log(JSON.stringify({ sessions, cleaned }));
941
+ } else if (args.includes('--kanban')) {
942
+ console.log(renderKanbanBoard(sessions));
943
+ if (cleaned > 0) {
944
+ console.log(`${c.dim}Cleaned ${cleaned} stale lock(s)${c.reset}`);
945
+ }
750
946
  } else {
751
947
  console.log(formatSessionsTable(sessions));
752
948
  if (cleaned > 0) {
@@ -786,6 +982,85 @@ function main() {
786
982
  break;
787
983
  }
788
984
 
985
+ // PERFORMANCE: Combined command for welcome script (saves ~200ms from 3 subprocess calls)
986
+ case 'full-status': {
987
+ const nickname = args[1] || null;
988
+ const cwd = process.cwd();
989
+
990
+ // Register in single pass (combines register + count + status)
991
+ const registry = loadRegistry();
992
+ const cleaned = cleanupStaleLocks(registry);
993
+ const branch = getCurrentBranch();
994
+ const story = getCurrentStory();
995
+ const pid = process.ppid || process.pid;
996
+
997
+ // Find or create session
998
+ let sessionId = null;
999
+ let isNew = false;
1000
+ for (const [id, session] of Object.entries(registry.sessions)) {
1001
+ if (session.path === cwd) {
1002
+ sessionId = id;
1003
+ break;
1004
+ }
1005
+ }
1006
+
1007
+ if (sessionId) {
1008
+ // Update existing
1009
+ registry.sessions[sessionId].branch = branch;
1010
+ registry.sessions[sessionId].story = story ? story.id : null;
1011
+ registry.sessions[sessionId].last_active = new Date().toISOString();
1012
+ if (nickname) registry.sessions[sessionId].nickname = nickname;
1013
+ // Ensure thread_type exists (migration for old sessions)
1014
+ if (!registry.sessions[sessionId].thread_type) {
1015
+ registry.sessions[sessionId].thread_type = registry.sessions[sessionId].is_main ? 'base' : 'parallel';
1016
+ }
1017
+ writeLock(sessionId, pid);
1018
+ } else {
1019
+ // Create new
1020
+ sessionId = String(registry.next_id);
1021
+ registry.next_id++;
1022
+ const isMain = cwd === ROOT;
1023
+ registry.sessions[sessionId] = {
1024
+ path: cwd,
1025
+ branch,
1026
+ story: story ? story.id : null,
1027
+ nickname: nickname || null,
1028
+ created: new Date().toISOString(),
1029
+ last_active: new Date().toISOString(),
1030
+ is_main: isMain,
1031
+ thread_type: isMain ? 'base' : 'parallel',
1032
+ };
1033
+ writeLock(sessionId, pid);
1034
+ isNew = true;
1035
+ }
1036
+ saveRegistry(registry);
1037
+
1038
+ // Build session list and counts
1039
+ const sessions = [];
1040
+ let otherActive = 0;
1041
+ for (const [id, session] of Object.entries(registry.sessions)) {
1042
+ const active = isSessionActive(id);
1043
+ const isCurrent = session.path === cwd;
1044
+ sessions.push({ id, ...session, active, current: isCurrent });
1045
+ if (active && !isCurrent) otherActive++;
1046
+ }
1047
+
1048
+ const current = sessions.find(s => s.current) || null;
1049
+
1050
+ console.log(
1051
+ JSON.stringify({
1052
+ registered: true,
1053
+ id: sessionId,
1054
+ isNew,
1055
+ current,
1056
+ otherActive,
1057
+ total: sessions.length,
1058
+ cleaned,
1059
+ })
1060
+ );
1061
+ break;
1062
+ }
1063
+
789
1064
  case 'check-merge': {
790
1065
  const sessionId = args[1];
791
1066
  if (!sessionId) {
@@ -888,6 +1163,49 @@ function main() {
888
1163
  break;
889
1164
  }
890
1165
 
1166
+ case 'switch': {
1167
+ const sessionIdOrNickname = args[1];
1168
+ if (!sessionIdOrNickname) {
1169
+ console.log(JSON.stringify({ success: false, error: 'Session ID or nickname required' }));
1170
+ return;
1171
+ }
1172
+ const result = switchSession(sessionIdOrNickname);
1173
+ console.log(JSON.stringify(result, null, 2));
1174
+ break;
1175
+ }
1176
+
1177
+ case 'active': {
1178
+ const result = getActiveSession();
1179
+ console.log(JSON.stringify(result, null, 2));
1180
+ break;
1181
+ }
1182
+
1183
+ case 'clear-active': {
1184
+ const result = clearActiveSession();
1185
+ console.log(JSON.stringify(result));
1186
+ break;
1187
+ }
1188
+
1189
+ case 'thread-type': {
1190
+ const subCommand = args[1];
1191
+ if (subCommand === 'set') {
1192
+ const sessionId = args[2];
1193
+ const threadType = args[3];
1194
+ if (!sessionId || !threadType) {
1195
+ console.log(JSON.stringify({ success: false, error: 'Usage: thread-type set <sessionId> <type>' }));
1196
+ return;
1197
+ }
1198
+ const result = setSessionThreadType(sessionId, threadType);
1199
+ console.log(JSON.stringify(result));
1200
+ } else {
1201
+ // Default: get thread type
1202
+ const sessionId = args[1] || null;
1203
+ const result = getSessionThreadType(sessionId);
1204
+ console.log(JSON.stringify(result));
1205
+ }
1206
+ break;
1207
+ }
1208
+
891
1209
  case 'help':
892
1210
  default:
893
1211
  console.log(`
@@ -901,6 +1219,12 @@ ${c.cyan}Commands:${c.reset}
901
1219
  count Count other active sessions
902
1220
  delete <id> [--remove-worktree] Delete session
903
1221
  status Get current session status
1222
+ full-status Combined register+count+status (optimized)
1223
+ switch <id|nickname> Switch active session context (for /add-dir)
1224
+ active Get currently switched session (if any)
1225
+ clear-active Clear switched session (back to main)
1226
+ thread-type [id] Get thread type for session (default: current)
1227
+ thread-type set <id> <type> Set thread type (base|parallel|chained|fusion|big|long)
904
1228
  check-merge <id> Check if session is mergeable to main
905
1229
  merge-preview <id> Preview commits/files to be merged
906
1230
  integrate <id> [opts] Merge session to main and cleanup
@@ -1086,7 +1410,7 @@ function smartMerge(sessionId, options = {}) {
1086
1410
  }
1087
1411
 
1088
1412
  // Categorize and plan resolutions
1089
- const resolutions = conflictFiles.files.map((file) => {
1413
+ const resolutions = conflictFiles.files.map(file => {
1090
1414
  const category = categorizeFile(file);
1091
1415
  const strategyInfo = getMergeStrategy(category);
1092
1416
  return {
@@ -1195,7 +1519,10 @@ function smartMerge(sessionId, options = {}) {
1195
1519
  result.worktreeDeleted = true;
1196
1520
  } catch (e) {
1197
1521
  try {
1198
- execSync(`git worktree remove --force "${session.path}"`, { cwd: ROOT, encoding: 'utf8' });
1522
+ execSync(`git worktree remove --force "${session.path}"`, {
1523
+ cwd: ROOT,
1524
+ encoding: 'utf8',
1525
+ });
1199
1526
  result.worktreeDeleted = true;
1200
1527
  } catch (e2) {
1201
1528
  result.worktreeDeleted = false;
@@ -1295,7 +1622,7 @@ function getConflictingFiles(sessionId) {
1295
1622
  const branchSet = new Set((branchFiles.stdout || '').trim().split('\n').filter(Boolean));
1296
1623
 
1297
1624
  // Find intersection (files changed in both)
1298
- const conflicting = [...mainSet].filter((f) => branchSet.has(f));
1625
+ const conflicting = [...mainSet].filter(f => branchSet.has(f));
1299
1626
 
1300
1627
  return { success: true, files: conflicting };
1301
1628
  }
@@ -1392,6 +1719,185 @@ function getMergeHistory() {
1392
1719
  }
1393
1720
  }
1394
1721
 
1722
+ // Session state file path
1723
+ const SESSION_STATE_PATH = path.join(ROOT, 'docs', '09-agents', 'session-state.json');
1724
+
1725
+ /**
1726
+ * Switch active session context (for use with /add-dir).
1727
+ * Updates session-state.json with active_session info.
1728
+ *
1729
+ * @param {string} sessionIdOrNickname - Session ID or nickname to switch to
1730
+ * @returns {{ success: boolean, session?: object, path?: string, error?: string }}
1731
+ */
1732
+ function switchSession(sessionIdOrNickname) {
1733
+ const registry = loadRegistry();
1734
+
1735
+ // Find session by ID or nickname
1736
+ let targetSession = null;
1737
+ let targetId = null;
1738
+
1739
+ for (const [id, session] of Object.entries(registry.sessions)) {
1740
+ if (id === sessionIdOrNickname || session.nickname === sessionIdOrNickname) {
1741
+ targetSession = session;
1742
+ targetId = id;
1743
+ break;
1744
+ }
1745
+ }
1746
+
1747
+ if (!targetSession) {
1748
+ return { success: false, error: `Session "${sessionIdOrNickname}" not found` };
1749
+ }
1750
+
1751
+ // Verify the session path exists
1752
+ if (!fs.existsSync(targetSession.path)) {
1753
+ return {
1754
+ success: false,
1755
+ error: `Session directory does not exist: ${targetSession.path}`,
1756
+ };
1757
+ }
1758
+
1759
+ // Load or create session-state.json
1760
+ let sessionState = {};
1761
+ if (fs.existsSync(SESSION_STATE_PATH)) {
1762
+ try {
1763
+ sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
1764
+ } catch (e) {
1765
+ // Start fresh
1766
+ }
1767
+ }
1768
+
1769
+ // Update active_session
1770
+ sessionState.active_session = {
1771
+ id: targetId,
1772
+ nickname: targetSession.nickname,
1773
+ path: targetSession.path,
1774
+ branch: targetSession.branch,
1775
+ switched_at: new Date().toISOString(),
1776
+ original_cwd: ROOT,
1777
+ };
1778
+
1779
+ // Save session-state.json
1780
+ const stateDir = path.dirname(SESSION_STATE_PATH);
1781
+ if (!fs.existsSync(stateDir)) {
1782
+ fs.mkdirSync(stateDir, { recursive: true });
1783
+ }
1784
+ fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(sessionState, null, 2) + '\n');
1785
+
1786
+ // Update session last_active
1787
+ registry.sessions[targetId].last_active = new Date().toISOString();
1788
+ saveRegistry(registry);
1789
+
1790
+ return {
1791
+ success: true,
1792
+ session: {
1793
+ id: targetId,
1794
+ nickname: targetSession.nickname,
1795
+ path: targetSession.path,
1796
+ branch: targetSession.branch,
1797
+ },
1798
+ path: targetSession.path,
1799
+ addDirCommand: `/add-dir ${targetSession.path}`,
1800
+ };
1801
+ }
1802
+
1803
+ /**
1804
+ * Clear active session (switch back to main/original).
1805
+ * @returns {{ success: boolean }}
1806
+ */
1807
+ function clearActiveSession() {
1808
+ if (!fs.existsSync(SESSION_STATE_PATH)) {
1809
+ return { success: true };
1810
+ }
1811
+
1812
+ try {
1813
+ const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
1814
+ delete sessionState.active_session;
1815
+ fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(sessionState, null, 2) + '\n');
1816
+ return { success: true };
1817
+ } catch (e) {
1818
+ return { success: false, error: e.message };
1819
+ }
1820
+ }
1821
+
1822
+ /**
1823
+ * Get current active session (if switched).
1824
+ * @returns {{ active: boolean, session?: object }}
1825
+ */
1826
+ function getActiveSession() {
1827
+ if (!fs.existsSync(SESSION_STATE_PATH)) {
1828
+ return { active: false };
1829
+ }
1830
+
1831
+ try {
1832
+ const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
1833
+ if (sessionState.active_session) {
1834
+ return { active: true, session: sessionState.active_session };
1835
+ }
1836
+ return { active: false };
1837
+ } catch (e) {
1838
+ return { active: false };
1839
+ }
1840
+ }
1841
+
1842
+ /**
1843
+ * Get thread type for a session.
1844
+ * @param {string} sessionId - Session ID (or null for current session)
1845
+ * @returns {{ success: boolean, thread_type?: string, error?: string }}
1846
+ */
1847
+ function getSessionThreadType(sessionId = null) {
1848
+ const registry = loadRegistry();
1849
+ const cwd = process.cwd();
1850
+
1851
+ // Find session
1852
+ let targetId = sessionId;
1853
+ if (!targetId) {
1854
+ // Find current session by path
1855
+ for (const [id, session] of Object.entries(registry.sessions)) {
1856
+ if (session.path === cwd) {
1857
+ targetId = id;
1858
+ break;
1859
+ }
1860
+ }
1861
+ }
1862
+
1863
+ if (!targetId || !registry.sessions[targetId]) {
1864
+ return { success: false, error: 'Session not found' };
1865
+ }
1866
+
1867
+ const session = registry.sessions[targetId];
1868
+ // Return thread_type or auto-detect for legacy sessions
1869
+ const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
1870
+
1871
+ return {
1872
+ success: true,
1873
+ thread_type: threadType,
1874
+ session_id: targetId,
1875
+ is_main: session.is_main,
1876
+ };
1877
+ }
1878
+
1879
+ /**
1880
+ * Update thread type for a session.
1881
+ * @param {string} sessionId - Session ID
1882
+ * @param {string} threadType - New thread type
1883
+ * @returns {{ success: boolean, error?: string }}
1884
+ */
1885
+ function setSessionThreadType(sessionId, threadType) {
1886
+ if (!THREAD_TYPES.includes(threadType)) {
1887
+ return { success: false, error: `Invalid thread type: ${threadType}. Valid: ${THREAD_TYPES.join(', ')}` };
1888
+ }
1889
+
1890
+ const registry = loadRegistry();
1891
+ if (!registry.sessions[sessionId]) {
1892
+ return { success: false, error: `Session ${sessionId} not found` };
1893
+ }
1894
+
1895
+ registry.sessions[sessionId].thread_type = threadType;
1896
+ saveRegistry(registry);
1897
+
1898
+ return { success: true, thread_type: threadType };
1899
+ }
1900
+
1395
1901
  // Export for use as module
1396
1902
  module.exports = {
1397
1903
  loadRegistry,
@@ -1415,6 +1921,19 @@ module.exports = {
1415
1921
  categorizeFile,
1416
1922
  getMergeStrategy,
1417
1923
  getMergeHistory,
1924
+ // Session switching
1925
+ switchSession,
1926
+ clearActiveSession,
1927
+ getActiveSession,
1928
+ // Thread type tracking
1929
+ THREAD_TYPES,
1930
+ detectThreadType,
1931
+ getSessionThreadType,
1932
+ setSessionThreadType,
1933
+ // Kanban visualization
1934
+ SESSION_PHASES,
1935
+ getSessionPhase,
1936
+ renderKanbanBoard,
1418
1937
  };
1419
1938
 
1420
1939
  // Run CLI if executed directly