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.
- package/CHANGELOG.md +10 -0
- package/README.md +3 -3
- package/lib/colors.js +23 -0
- package/package.json +1 -1
- package/scripts/agileflow-statusline.sh +31 -44
- package/scripts/agileflow-welcome.js +378 -132
- package/scripts/batch-pmap-loop.js +528 -0
- package/scripts/lib/colors.sh +106 -0
- package/scripts/lib/file-tracking.js +5 -3
- package/scripts/obtain-context.js +132 -20
- package/scripts/session-boundary.js +138 -0
- package/scripts/session-manager.js +526 -7
- package/scripts/test-session-boundary.js +80 -0
- package/src/core/agents/mentor.md +40 -2
- package/src/core/agents/orchestrator.md +35 -2
- package/src/core/commands/babysit.md +198 -674
- package/src/core/commands/batch.md +117 -2
- package/src/core/commands/metrics.md +62 -9
- package/src/core/commands/rpi.md +500 -0
- package/src/core/commands/session/new.md +90 -51
- package/src/core/commands/session/resume.md +40 -16
- package/src/core/commands/session/status.md +35 -2
- package/src/core/templates/session-state.json +32 -3
- package/tools/cli/commands/config.js +43 -21
- package/tools/cli/commands/doctor.js +8 -5
- package/tools/cli/commands/setup.js +14 -7
- package/tools/cli/commands/uninstall.js +8 -5
- package/tools/cli/commands/update.js +20 -10
- package/tools/cli/lib/content-injector.js +80 -0
- package/tools/cli/lib/error-handler.js +173 -0
- package/tools/cli/lib/ui.js +3 -2
|
@@ -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:
|
|
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(
|
|
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}"`, {
|
|
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(
|
|
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
|